mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-11 12:30:25 +08:00
Merge branch 'main' of github.com:porink0424/optuna-dashboard into feat/jupyter
This commit is contained in:
@@ -23,7 +23,10 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup tslib
|
||||
run: make tslib
|
||||
|
||||
- name: Build bundle.js
|
||||
working-directory: optuna_dashboard
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- run: npm run lint
|
||||
|
||||
type-check:
|
||||
name: Type checking on optuna-dashboard
|
||||
name: Type checking on optuna-dashboard & standalone_app
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -44,12 +44,24 @@ jobs:
|
||||
- name: Setup tslib
|
||||
run: make tslib
|
||||
|
||||
- name: Type Check
|
||||
- name: Type Check optuna_dashboard
|
||||
working-directory: optuna_dashboard
|
||||
run: |
|
||||
npm install
|
||||
npm run type-check
|
||||
|
||||
- name: Build rustlib for standalone_app
|
||||
working-directory: rustlib
|
||||
run: |
|
||||
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
wasm-pack build --target web
|
||||
|
||||
- name: Type Check standalone_app
|
||||
working-directory: standalone_app
|
||||
run: |
|
||||
npm install
|
||||
npm run type-check
|
||||
|
||||
check-package-lock-json:
|
||||
name: Check package-lock.json
|
||||
runs-on: ubuntu-latest
|
||||
@@ -63,14 +75,14 @@ jobs:
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- working-directory: optuna_dashboard
|
||||
run: npm install
|
||||
- working-directory: tslib/types
|
||||
run: npm install
|
||||
- working-directory: tslib/storage
|
||||
run: npm install
|
||||
- working-directory: tslib/react
|
||||
run: npm install
|
||||
- working-directory: optuna_dashboard
|
||||
run: npm install
|
||||
- working-directory: standalone_app
|
||||
run: npm install
|
||||
- working-directory: vscode
|
||||
@@ -88,7 +100,11 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup tslib
|
||||
run: make tslib
|
||||
|
||||
- name: Build bundle.js
|
||||
working-directory: optuna_dashboard
|
||||
run: |
|
||||
|
||||
+8
-2
@@ -86,7 +86,13 @@ Note that `OPTUNA_DASHBOARD_DEBUG=1` makes the server will automatically restart
|
||||
|
||||
### Running tests, lint checks and formatters
|
||||
|
||||
#### Running Python unit tests
|
||||
#### Running unit tests for `tslib/`
|
||||
|
||||
```
|
||||
$ make tslib-test
|
||||
```
|
||||
|
||||
#### Running unit tests for `python_tests/`
|
||||
|
||||
```
|
||||
$ pytest python_tests/
|
||||
@@ -147,7 +153,7 @@ Please install [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) and
|
||||
$ make serve-browser-app
|
||||
```
|
||||
|
||||
Open http://127.0.0.1:9000/
|
||||
Open http://localhost:5173/
|
||||
|
||||
|
||||
## VS Code Extension
|
||||
|
||||
@@ -24,6 +24,11 @@ tslib:
|
||||
cd tslib/storage && npm i && npm run build
|
||||
cd tslib/react && npm i && npm run build
|
||||
|
||||
.PHONY: tslib-test
|
||||
tslib-test: tslib
|
||||
cd tslib/react/test && python generate_assets.py && npm run test
|
||||
cd tslib/storage/test && python generate_assets.py && npm run test
|
||||
|
||||
.PHONY: serve-browser-app
|
||||
serve-browser-app: tslib $(RUSTLIB_OUT)
|
||||
cd standalone_app && npm run watch
|
||||
|
||||
@@ -180,7 +180,10 @@ def create_app(
|
||||
@app.delete("/api/studies/<study_id:int>")
|
||||
@json_api_view
|
||||
def delete_study(study_id: int) -> dict[str, Any]:
|
||||
if artifact_store is not None:
|
||||
data = request.json or {}
|
||||
remove_associated_artifacts = data.get("remove_associated_artifacts", True)
|
||||
|
||||
if artifact_store is not None and remove_associated_artifacts:
|
||||
delete_all_artifacts(artifact_store, storage, study_id)
|
||||
|
||||
try:
|
||||
|
||||
Generated
+13107
-2682
File diff suppressed because it is too large
Load Diff
@@ -16,21 +16,26 @@
|
||||
"author": "Masashi Shibata",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.6",
|
||||
"@mui/lab": "^5.0.0-alpha.162",
|
||||
"@mui/material": "^5.15.6",
|
||||
"@emotion/react": "file:../tslib/react/node_modules/@emotion/react",
|
||||
"@emotion/styled": "file:../tslib/react/node_modules/@emotion/styled",
|
||||
"@mui/icons-material": "file:../tslib/react/node_modules/@mui/icons-material",
|
||||
"@mui/lab": "file:../tslib/react/node_modules/@mui/lab",
|
||||
"@mui/material": "file:../tslib/react/node_modules/@mui/material",
|
||||
"@mui/system": "file:../tslib/react/node_modules/@mui/system",
|
||||
"@optuna/react": "../tslib/react",
|
||||
"@react-three/drei": "^9.96.4",
|
||||
"@react-three/fiber": "^8.15.15",
|
||||
"@tanstack/react-query": "^5.18.1",
|
||||
"@tanstack/react-table": "^8.16.0",
|
||||
"@tanstack/react-virtual": "^3.1.2",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/three": "^0.160.0",
|
||||
"axios": "^1.6.7",
|
||||
"elkjs": "^0.9.1",
|
||||
"notistack": "^3.0.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"plotly.js-dist-min": "^2.28.0",
|
||||
"react": "^18.2.0",
|
||||
"react": "file:../tslib/react/node_modules/react",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-router-dom": "^6.21.3",
|
||||
@@ -50,7 +55,7 @@
|
||||
"@babel/preset-env": "^7.23.9",
|
||||
"@optuna/types": "../tslib/types/",
|
||||
"@types/plotly.js": "^2.12.32",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react": "file:../tslib/react/node_modules/@types/react",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-syntax-highlighter": "^15.5.11",
|
||||
"compression-webpack-plugin": "^11.0.0",
|
||||
|
||||
@@ -317,7 +317,7 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler):
|
||||
|
||||
self._rng = np.random.RandomState(seed)
|
||||
self.independent_sampler = independent_sampler or optuna.samplers.RandomSampler(
|
||||
seed=self._rng.randint(2**32, dtype=np.int64)
|
||||
seed=self._rng.randint(2**32, dtype=np.int64) # type: ignore
|
||||
)
|
||||
|
||||
self._search_space = optuna.search_space.IntersectionSearchSpace()
|
||||
|
||||
@@ -284,9 +284,9 @@ export const actionCreator = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const deleteStudy = (studyId: number) => {
|
||||
const deleteStudy = (studyId: number, removeAssociatedArtifacts: boolean) => {
|
||||
apiClient
|
||||
.deleteStudy(studyId)
|
||||
.deleteStudy(studyId, removeAssociatedArtifacts)
|
||||
.then(() => {
|
||||
setStudySummaries(studySummaries.filter((s) => s.study_id !== studyId))
|
||||
enqueueSnackbar(`Success to delete a study (id=${studyId})`, {
|
||||
|
||||
@@ -185,7 +185,10 @@ export abstract class APIClient {
|
||||
studyName: string,
|
||||
directions: Optuna.StudyDirection[]
|
||||
): Promise<StudySummary>
|
||||
abstract deleteStudy(studyId: number): Promise<void>
|
||||
abstract deleteStudy(
|
||||
studyId: number,
|
||||
removeAssociatedArtifacts: boolean
|
||||
): Promise<void>
|
||||
abstract renameStudy(
|
||||
studyId: number,
|
||||
studyName: string
|
||||
|
||||
@@ -116,10 +116,19 @@ export class AxiosClient extends APIClient {
|
||||
: undefined,
|
||||
}
|
||||
})
|
||||
deleteStudy = (studyId: number): Promise<void> =>
|
||||
this.axiosInstance.delete(`/api/studies/${studyId}`).then(() => {
|
||||
return
|
||||
})
|
||||
deleteStudy = (
|
||||
studyId: number,
|
||||
removeAssociatedArtifacts: boolean
|
||||
): Promise<void> =>
|
||||
this.axiosInstance
|
||||
.delete(`/api/studies/${studyId}`, {
|
||||
data: {
|
||||
remove_associated_artifacts: removeAssociatedArtifacts,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
return
|
||||
})
|
||||
renameStudy = (studyId: number, studyName: string): Promise<StudySummary> =>
|
||||
this.axiosInstance
|
||||
.post<RenameStudyResponse>(`/api/studies/${studyId}/rename`, {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import React, { ReactNode, useState, FC } from "react"
|
||||
import { Artifact } from "ts/types/optuna"
|
||||
@@ -111,6 +113,7 @@ const DeleteDialog: FC<{
|
||||
filename,
|
||||
handleDeleteArtifact,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<Dialog
|
||||
open={openDeleteArtifactDialog}
|
||||
@@ -123,10 +126,18 @@ const DeleteDialog: FC<{
|
||||
Delete artifact
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<DialogContentText
|
||||
sx={{
|
||||
marginBottom: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
Are you sure you want to delete an artifact ("
|
||||
{filename}")?
|
||||
</DialogContentText>
|
||||
<Alert severity="warning">
|
||||
If this artifact is linked to another study or trial, it will no
|
||||
longer be accessible from that study or trial as well.
|
||||
</Alert>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDeleteArtifactDialog} color="primary">
|
||||
|
||||
@@ -24,6 +24,7 @@ import { StudyDetail } from "ts/types/optuna"
|
||||
import { actionCreator } from "../../action"
|
||||
import { ArtifactCardMedia } from "./ArtifactCardMedia"
|
||||
import { useDeleteStudyArtifactDialog } from "./DeleteArtifactDialog"
|
||||
import { isTableArtifact, useTableArtifactModal } from "./TableArtifactViewer"
|
||||
import {
|
||||
isThreejsArtifact,
|
||||
useThreejsArtifactModal,
|
||||
@@ -35,6 +36,8 @@ export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => {
|
||||
useDeleteStudyArtifactDialog()
|
||||
const [openThreejsArtifactModal, renderThreejsArtifactModal] =
|
||||
useThreejsArtifactModal()
|
||||
const [openTableArtifactModal, renderTableArtifactModal] =
|
||||
useTableArtifactModal()
|
||||
|
||||
const width = "200px"
|
||||
const height = "150px"
|
||||
@@ -96,6 +99,19 @@ export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => {
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
{isTableArtifact(artifact) ? (
|
||||
<IconButton
|
||||
aria-label="show artifact table"
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ margin: "auto 0" }}
|
||||
onClick={() => {
|
||||
openTableArtifactModal(urlPath, artifact)
|
||||
}}
|
||||
>
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
<IconButton
|
||||
aria-label="delete artifact"
|
||||
size="small"
|
||||
@@ -125,6 +141,7 @@ export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => {
|
||||
</Box>
|
||||
{renderDeleteArtifactDialog()}
|
||||
{renderThreejsArtifactModal()}
|
||||
{renderTableArtifactModal()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import ClearIcon from "@mui/icons-material/Clear"
|
||||
import { Box, Modal, useTheme } from "@mui/material"
|
||||
import IconButton from "@mui/material/IconButton"
|
||||
import { useSnackbar } from "notistack"
|
||||
import Papa from "papaparse"
|
||||
import React, { useState, useEffect, ReactNode } from "react"
|
||||
import { DataGrid } from "../DataGrid"
|
||||
|
||||
import { Artifact } from "ts/types/optuna"
|
||||
|
||||
export const isTableArtifact = (artifact: Artifact): boolean => {
|
||||
return artifact.filename.endsWith(".csv")
|
||||
}
|
||||
|
||||
interface TableArtifactViewerProps {
|
||||
src: string
|
||||
filetype: string | undefined
|
||||
}
|
||||
|
||||
type Data = {
|
||||
[key: string]: string | number
|
||||
}
|
||||
|
||||
export const TableArtifactViewer: React.FC<TableArtifactViewerProps> = (
|
||||
props
|
||||
) => {
|
||||
const [data, setData] = useState<Data[]>([])
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
|
||||
useEffect(() => {
|
||||
const handleFileChange = async () => {
|
||||
try {
|
||||
const loadedData = await loadCSV(props)
|
||||
setData(loadedData)
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar("Failed to load the csv file.", {
|
||||
variant: "error",
|
||||
})
|
||||
}
|
||||
}
|
||||
handleFileChange()
|
||||
}, [props])
|
||||
|
||||
const columns = React.useMemo(() => {
|
||||
const keys = data[0] ? Object.keys(data[0]) : []
|
||||
return keys.map((key) => ({
|
||||
header: key,
|
||||
accessorKey: key,
|
||||
enableSorting: true,
|
||||
enableColumnFilter: false,
|
||||
}))
|
||||
}, [data])
|
||||
|
||||
return <DataGrid data={data} columns={columns} initialRowsPerPage={10} />
|
||||
}
|
||||
|
||||
export const useTableArtifactModal = (): [
|
||||
(path: string, artifact: Artifact) => void,
|
||||
() => ReactNode,
|
||||
] => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [target, setTarget] = useState<[string, Artifact | null]>(["", null])
|
||||
const theme = useTheme()
|
||||
|
||||
const openModal = (artifactUrlPath: string, artifact: Artifact) => {
|
||||
setTarget([artifactUrlPath, artifact])
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const renderDeleteStudyDialog = () => {
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false)
|
||||
setTarget(["", null])
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="div"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
bgcolor: "background.paper",
|
||||
borderRadius: "15px",
|
||||
width: "80%",
|
||||
maxHeight: "80%",
|
||||
overflowY: "auto",
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: theme.spacing(2),
|
||||
right: theme.spacing(2),
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setTarget(["", null])
|
||||
}}
|
||||
>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
<TableArtifactViewer
|
||||
src={target[0]}
|
||||
filetype={target[1]?.filename.split(".").pop()}
|
||||
/>
|
||||
</Box>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
return [openModal, renderDeleteStudyDialog]
|
||||
}
|
||||
|
||||
const loadCSV = (props: TableArtifactViewerProps): Promise<Data[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
Papa.parse(props.src, {
|
||||
header: true,
|
||||
download: true,
|
||||
complete: (results: Papa.ParseResult<Data>) => {
|
||||
resolve(results?.data)
|
||||
},
|
||||
error: () => {
|
||||
reject(new Error("csv parse err"))
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -61,7 +61,10 @@ export const WaveSurferArtifactViewer: React.FC<WaveSurferArtifactViewerProps> =
|
||||
}, [wavesurfer])
|
||||
|
||||
return (
|
||||
<Box style={{ width: "100%", display: "flex", flexDirection: "column" }}>
|
||||
<Box
|
||||
component="div"
|
||||
style={{ width: "100%", display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<div ref={containerRef} style={{ minHeight: "120px", width: "100%" }} />
|
||||
<button onClick={onPlayClick} style={{ marginTop: "1em" }}>
|
||||
{isPlaying ? "Pause" : "Play"}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import CheckBoxIcon from "@mui/icons-material/CheckBox"
|
||||
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"
|
||||
import FilterListIcon from "@mui/icons-material/FilterList"
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"
|
||||
import FirstPageIcon from "@mui/icons-material/FirstPage"
|
||||
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"
|
||||
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"
|
||||
import LastPageIcon from "@mui/icons-material/LastPage"
|
||||
import {
|
||||
Box,
|
||||
Collapse,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
@@ -18,228 +19,29 @@ import {
|
||||
TableRow,
|
||||
TableSortLabel,
|
||||
TextField,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import ListItemIcon from "@mui/material/ListItemIcon"
|
||||
import Paper from "@mui/material/Paper"
|
||||
import { TablePaginationActionsProps } from "@mui/material/TablePagination/TablePaginationActions"
|
||||
import { styled } from "@mui/system"
|
||||
import React from "react"
|
||||
|
||||
type Order = "asc" | "desc"
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Value = any
|
||||
|
||||
const defaultRowsPerPageOption = [10, 50, 100, { label: "All", value: -1 }]
|
||||
|
||||
interface DataGridColumn<T> {
|
||||
field: keyof T
|
||||
label: string
|
||||
sortable?: boolean
|
||||
less?: (a: T, b: T, ascending: boolean) => number
|
||||
filterChoices?: (string | null)[]
|
||||
toCellValue?: (rowIndex: number) => string | React.ReactNode
|
||||
padding?: "normal" | "checkbox" | "none"
|
||||
}
|
||||
|
||||
interface RowFilter {
|
||||
columnIdx: number
|
||||
values: Value[]
|
||||
}
|
||||
|
||||
function DataGrid<T>(props: {
|
||||
columns: DataGridColumn<T>[]
|
||||
rows: T[]
|
||||
keyField: keyof T
|
||||
dense?: boolean
|
||||
collapseBody?: (rowIndex: number) => React.ReactNode
|
||||
initialRowsPerPage?: number
|
||||
rowsPerPageOption?: Array<number | { value: number; label: string }>
|
||||
defaultFilter?: (row: T) => boolean
|
||||
}): React.ReactElement {
|
||||
const { columns, rows, keyField, dense, collapseBody, defaultFilter } = props
|
||||
let { initialRowsPerPage, rowsPerPageOption } = props
|
||||
const [order, setOrder] = React.useState<Order>("asc")
|
||||
const [orderBy, setOrderBy] = React.useState<number>(0) // index of columns
|
||||
const [page, setPage] = React.useState(0)
|
||||
const [filters, setFilters] = React.useState<RowFilter[]>([])
|
||||
|
||||
const getRowIndex = (row: T): number => {
|
||||
return rows.findIndex((row2) => row[keyField] === row2[keyField])
|
||||
}
|
||||
|
||||
// Pagination
|
||||
rowsPerPageOption = rowsPerPageOption || defaultRowsPerPageOption
|
||||
initialRowsPerPage = initialRowsPerPage // use first element as default
|
||||
? initialRowsPerPage
|
||||
: isNumber(rowsPerPageOption[0])
|
||||
? rowsPerPageOption[0]
|
||||
: rowsPerPageOption[0].value
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(initialRowsPerPage)
|
||||
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
setPage(newPage)
|
||||
}
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10))
|
||||
setPage(0)
|
||||
}
|
||||
|
||||
const PaginationForm: React.FC<{
|
||||
onPageNumberSubmit: (value: number) => void
|
||||
maxPageNumber: number
|
||||
}> = ({ onPageNumberSubmit, maxPageNumber }) => {
|
||||
// This component is separated from DataGrid to prevent `DataGrid` from re-rendering the page,
|
||||
// every time any letters are input.
|
||||
const [specifiedPageText, setSpecifiedPageText] = React.useState("")
|
||||
|
||||
const handleSubmitPageNumber = (
|
||||
event: React.FormEvent<HTMLFormElement>
|
||||
) => {
|
||||
event.preventDefault()
|
||||
const newPageNumber = parseInt(specifiedPageText, 10)
|
||||
// Page is 0-indexed in `TablePagination`.
|
||||
onPageNumberSubmit(newPageNumber - 1)
|
||||
setSpecifiedPageText("") // reset the input field
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmitPageNumber}>
|
||||
<TextField
|
||||
size="small"
|
||||
label={`Go to Page: n / ${maxPageNumber}`}
|
||||
value={specifiedPageText}
|
||||
type="number"
|
||||
style={{ width: 200 }}
|
||||
inputProps={{ min: 1, max: maxPageNumber }}
|
||||
onChange={(e) => {
|
||||
setSpecifiedPageText(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// Filtering
|
||||
const filteredRows = rows.filter((row, rowIdx) => {
|
||||
if (defaultFilter !== undefined && defaultFilter(row)) {
|
||||
return false
|
||||
}
|
||||
return filters.length === 0
|
||||
? true
|
||||
: filters.every((f) => {
|
||||
if (columns.length <= f.columnIdx) {
|
||||
console.log(
|
||||
`columnIdx=${f.columnIdx} must be smaller than columns.length=${columns.length}`
|
||||
)
|
||||
return true
|
||||
}
|
||||
const toCellValue = columns[f.columnIdx].toCellValue
|
||||
const cellValue =
|
||||
toCellValue !== undefined
|
||||
? toCellValue(rowIdx)
|
||||
: row[columns[f.columnIdx].field]
|
||||
return f.values.some((v) => v === cellValue)
|
||||
})
|
||||
})
|
||||
|
||||
// Sorting
|
||||
const sortedRows = stableSort<T>(filteredRows, order, orderBy, columns)
|
||||
const currentPageRows =
|
||||
rowsPerPage > 0
|
||||
? sortedRows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
: sortedRows
|
||||
const emptyRows =
|
||||
rowsPerPage - Math.min(rowsPerPage, sortedRows.length - page * rowsPerPage)
|
||||
|
||||
const RootDiv = styled("div")({
|
||||
width: "100%",
|
||||
})
|
||||
const maxPageNumber = Math.ceil(filteredRows.length / rowsPerPage)
|
||||
return (
|
||||
<RootDiv>
|
||||
<TableContainer>
|
||||
<Table
|
||||
aria-labelledby="tableTitle"
|
||||
size={dense ? "small" : "medium"}
|
||||
aria-label="data grid"
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{collapseBody ? <TableCell /> : null}
|
||||
{columns.map((column, columnIdx) => {
|
||||
return (
|
||||
<DataGridHeaderColumn<T>
|
||||
key={columnIdx}
|
||||
column={column}
|
||||
order={orderBy === columnIdx ? order : null}
|
||||
filter={
|
||||
filters.find((f) => f.columnIdx === columnIdx) || null
|
||||
}
|
||||
onOrderByChange={(direction: Order) => {
|
||||
setOrder(direction)
|
||||
setOrderBy(columnIdx)
|
||||
}}
|
||||
onFilterChange={(values: Value[]) => {
|
||||
const newFilters = filters.filter(
|
||||
(f) => f.columnIdx !== columnIdx
|
||||
)
|
||||
newFilters.push({
|
||||
columnIdx: columnIdx,
|
||||
values: values,
|
||||
})
|
||||
setFilters(newFilters)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{currentPageRows.map((row) => (
|
||||
<DataGridRow<T>
|
||||
columns={columns}
|
||||
rowIndex={getRowIndex(row)}
|
||||
row={row}
|
||||
keyField={keyField}
|
||||
collapseBody={collapseBody}
|
||||
key={`${row[keyField]}`}
|
||||
/>
|
||||
))}
|
||||
{emptyRows > 0 && (
|
||||
<TableRow style={{ height: (dense ? 33 : 53) * emptyRows }}>
|
||||
<TableCell colSpan={6} />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{filteredRows.length > 0 ? (
|
||||
<>
|
||||
{/* @ts-ignore */}
|
||||
<Box display="flex" alignItems="center">
|
||||
<TablePagination
|
||||
rowsPerPageOptions={rowsPerPageOption}
|
||||
component="div"
|
||||
count={filteredRows.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
{maxPageNumber > 2 ? (
|
||||
<PaginationForm
|
||||
onPageNumberSubmit={(page) => setPage(page)}
|
||||
maxPageNumber={maxPageNumber}
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
</>
|
||||
) : null}
|
||||
</RootDiv>
|
||||
)
|
||||
}
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
Header,
|
||||
PaginationState,
|
||||
SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
const TableHeaderCellSpan = styled("span")({
|
||||
display: "inline-flex",
|
||||
@@ -257,202 +59,325 @@ const HiddenSpan = styled("span")({
|
||||
width: 1,
|
||||
})
|
||||
|
||||
function DataGridHeaderColumn<T>(props: {
|
||||
column: DataGridColumn<T>
|
||||
order: Order | null
|
||||
onOrderByChange: (order: Order) => void
|
||||
filter: RowFilter | null
|
||||
onFilterChange: (values: Value[]) => void
|
||||
dense?: boolean
|
||||
}) {
|
||||
const { column, order, onOrderByChange, filter, onFilterChange, dense } =
|
||||
props
|
||||
function FilterMenu<T>({
|
||||
header,
|
||||
filterChoices,
|
||||
}: {
|
||||
header: Header<T, unknown>
|
||||
filterChoices: string[]
|
||||
}): React.ReactElement {
|
||||
const [filterMenuAnchorEl, setFilterMenuAnchorEl] =
|
||||
React.useState<null | HTMLElement>(null)
|
||||
|
||||
const filterChoices = column.filterChoices
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
padding={column.padding || "normal"}
|
||||
sortDirection={order !== null ? order : false}
|
||||
>
|
||||
<TableHeaderCellSpan>
|
||||
{column.sortable ? (
|
||||
<TableSortLabel
|
||||
active={order !== null}
|
||||
direction={order || "asc"}
|
||||
<>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
setFilterMenuAnchorEl(e.currentTarget)
|
||||
}}
|
||||
>
|
||||
<FilterListIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={filterMenuAnchorEl}
|
||||
open={filterMenuAnchorEl !== null}
|
||||
onClose={() => {
|
||||
setFilterMenuAnchorEl(null)
|
||||
}}
|
||||
>
|
||||
{filterChoices.map((choice) => (
|
||||
<MenuItem
|
||||
key={choice}
|
||||
onClick={() => {
|
||||
onOrderByChange(order === "asc" ? "desc" : "asc")
|
||||
const skippedValues = header.column.getFilterValue() as string[]
|
||||
const isSkipped = skippedValues.includes(choice)
|
||||
const newSkippedValues = isSkipped
|
||||
? skippedValues.filter((v) => v !== choice)
|
||||
: skippedValues.concat(choice)
|
||||
header.column.setFilterValue(newSkippedValues)
|
||||
}}
|
||||
>
|
||||
{column.label}
|
||||
{order !== null ? (
|
||||
<HiddenSpan>
|
||||
{order === "desc" ? "sorted descending" : "sorted ascending"}
|
||||
</HiddenSpan>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
) : (
|
||||
column.label
|
||||
)}
|
||||
{filterChoices !== undefined ? (
|
||||
<>
|
||||
<IconButton
|
||||
size={dense ? "small" : "medium"}
|
||||
onClick={(e) => {
|
||||
setFilterMenuAnchorEl(e.currentTarget)
|
||||
}}
|
||||
>
|
||||
<FilterListIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={filterMenuAnchorEl}
|
||||
open={filterMenuAnchorEl !== null}
|
||||
onClose={() => {
|
||||
setFilterMenuAnchorEl(null)
|
||||
}}
|
||||
>
|
||||
{filterChoices.map((choice) => (
|
||||
<MenuItem
|
||||
key={choice}
|
||||
onClick={() => {
|
||||
const newTickedValues =
|
||||
filter === null
|
||||
? filterChoices.filter((v) => v !== choice) // By default, every choice is ticked, so the chosen option will be unticked.
|
||||
: filter.values.some((v) => v === choice)
|
||||
? filter.values.filter((v) => v !== choice)
|
||||
: [...filter.values, choice]
|
||||
onFilterChange(newTickedValues)
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
{!filter || filter.values.some((v) => v === choice) ? (
|
||||
<CheckBoxIcon color="primary" />
|
||||
) : (
|
||||
<CheckBoxOutlineBlankIcon color="primary" />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
{choice ?? "(missing value)"}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
) : null}
|
||||
</TableHeaderCellSpan>
|
||||
</TableCell>
|
||||
<ListItemIcon>
|
||||
{header.column.getFilterValue() !== undefined ? (
|
||||
(header.column.getFilterValue() as string[]).includes(
|
||||
choice
|
||||
) ? (
|
||||
<CheckBoxOutlineBlankIcon color="primary" />
|
||||
) : (
|
||||
<CheckBoxIcon color="primary" />
|
||||
)
|
||||
) : null}
|
||||
</ListItemIcon>
|
||||
{choice ?? "(missing value)"}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DataGridRow<T>(props: {
|
||||
columns: DataGridColumn<T>[]
|
||||
rowIndex: number
|
||||
row: T
|
||||
keyField: keyof T
|
||||
collapseBody?: (rowIndex: number) => React.ReactNode
|
||||
}) {
|
||||
const { columns, rowIndex, row, keyField, collapseBody } = props
|
||||
const [open, setOpen] = React.useState(false)
|
||||
function DataGrid<T>({
|
||||
data,
|
||||
columns,
|
||||
initialRowsPerPage,
|
||||
}: {
|
||||
data: T[]
|
||||
columns: ColumnDef<T>[]
|
||||
initialRowsPerPage?: number
|
||||
}): React.ReactElement {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
const rowsPerPageOptions = [10, 50, 100, { label: "All", value: data.length }]
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize:
|
||||
initialRowsPerPage && rowsPerPageOptions.includes(initialRowsPerPage)
|
||||
? initialRowsPerPage
|
||||
: 50,
|
||||
})
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
columnFilters,
|
||||
sorting,
|
||||
pagination,
|
||||
},
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onSortingChange: setSorting,
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
autoResetPageIndex: false,
|
||||
})
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TableRow hover tabIndex={-1}>
|
||||
{collapseBody ? (
|
||||
<TableCell>
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<Box component="div" sx={{ width: "100%" }}>
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||
<TableHead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
if (
|
||||
header.column.getCanFilter() &&
|
||||
!header.column.getIsFiltered()
|
||||
) {
|
||||
header.column.setFilterValue([])
|
||||
}
|
||||
const order = header.column.getIsSorted()
|
||||
const filterChoices = header.column.getCanFilter()
|
||||
? Array.from(
|
||||
header.column.getFacetedUniqueValues().keys()
|
||||
).sort()
|
||||
: null
|
||||
return (
|
||||
<TableCell key={header.id} colSpan={header.colSpan}>
|
||||
{header.isPlaceholder ? null : (
|
||||
<TableHeaderCellSpan>
|
||||
{header.column.getCanSort() ? (
|
||||
<TableSortLabel
|
||||
active={order !== false}
|
||||
direction={order || "asc"}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{order !== null ? (
|
||||
<HiddenSpan>
|
||||
{order === "desc"
|
||||
? "sorted descending"
|
||||
: "sorted ascending"}
|
||||
</HiddenSpan>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
) : (
|
||||
flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)
|
||||
)}
|
||||
{filterChoices !== null ? (
|
||||
<FilterMenu
|
||||
header={header}
|
||||
filterChoices={filterChoices}
|
||||
/>
|
||||
) : null}
|
||||
</TableHeaderCellSpan>
|
||||
)}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
return (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Box component="div" display="flex" alignItems="center">
|
||||
<TablePagination
|
||||
rowsPerPageOptions={rowsPerPageOptions}
|
||||
component="div"
|
||||
count={table.getFilteredRowModel().rows.length}
|
||||
rowsPerPage={table.getState().pagination.pageSize}
|
||||
page={table.getState().pagination.pageIndex}
|
||||
slotProps={{
|
||||
select: {
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true,
|
||||
},
|
||||
}}
|
||||
onPageChange={(_, page) => {
|
||||
table.setPageIndex(page)
|
||||
}}
|
||||
onRowsPerPageChange={(e) => {
|
||||
const size = e.target.value ? Number(e.target.value) : 10
|
||||
table.setPageSize(size)
|
||||
}}
|
||||
ActionsComponent={TablePaginationActions}
|
||||
/>
|
||||
{table.getPageCount() > 2 ? (
|
||||
<PaginationForm1
|
||||
onPageNumberSubmit={(page) => table.setPageIndex(page)}
|
||||
maxPageNumber={table.getPageCount()}
|
||||
/>
|
||||
) : null}
|
||||
{columns.map((column, columnIndex) => {
|
||||
const cellItem = column.toCellValue
|
||||
? column.toCellValue(rowIndex)
|
||||
: // TODO(c-bata): Avoid this implicit type conversion.
|
||||
(row[column.field] as number | string | null | undefined)
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
key={`${row[keyField]}:${column.field.toString()}:${columnIndex}`}
|
||||
padding={column.padding || "normal"}
|
||||
>
|
||||
{cellItem}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
{collapseBody ? (
|
||||
<TableRow>
|
||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
{collapseBody(rowIndex)}
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function getComparator<T>(
|
||||
order: Order,
|
||||
columns: DataGridColumn<T>[],
|
||||
orderBy: number
|
||||
): (a: T, b: T) => number {
|
||||
return order === "desc"
|
||||
? (a, b) => descendingComparator<T>(a, b, columns, orderBy)
|
||||
: (a, b) => -descendingComparator<T>(a, b, columns, orderBy)
|
||||
}
|
||||
|
||||
function descendingComparator<T>(
|
||||
a: T,
|
||||
b: T,
|
||||
columns: DataGridColumn<T>[],
|
||||
orderBy: number
|
||||
): number {
|
||||
const field = columns[orderBy].field
|
||||
if (b[field] < a[field]) {
|
||||
return -1
|
||||
const TablePaginationActions = ({
|
||||
count,
|
||||
page,
|
||||
rowsPerPage,
|
||||
onPageChange,
|
||||
}: TablePaginationActionsProps) => {
|
||||
const theme = useTheme()
|
||||
const handleFirstPageButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
onPageChange(event, 0)
|
||||
}
|
||||
if (b[field] > a[field]) {
|
||||
return 1
|
||||
|
||||
const handleBackButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
onPageChange(event, page - 1)
|
||||
}
|
||||
return 0
|
||||
|
||||
const handleNextButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
onPageChange(event, page + 1)
|
||||
}
|
||||
|
||||
const handleLastPageButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1))
|
||||
}
|
||||
|
||||
return (
|
||||
<Box component="div" sx={{ flexShrink: 0, ml: 2.5 }}>
|
||||
<IconButton
|
||||
onClick={handleFirstPageButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
>
|
||||
{theme.direction === "rtl" ? <LastPageIcon /> : <FirstPageIcon />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="previous page"
|
||||
>
|
||||
{theme.direction === "rtl" ? (
|
||||
<KeyboardArrowRight />
|
||||
) : (
|
||||
<KeyboardArrowLeft />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleNextButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="next page"
|
||||
>
|
||||
{theme.direction === "rtl" ? (
|
||||
<KeyboardArrowLeft />
|
||||
) : (
|
||||
<KeyboardArrowRight />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
>
|
||||
{theme.direction === "rtl" ? <FirstPageIcon /> : <LastPageIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function stableSort<T>(
|
||||
array: T[],
|
||||
order: Order,
|
||||
orderBy: number,
|
||||
columns: DataGridColumn<T>[]
|
||||
) {
|
||||
// TODO(c-bata): Refactor here by implementing as the same comparator interface.
|
||||
const less = columns[orderBy].less
|
||||
const comparator = getComparator(order, columns, orderBy)
|
||||
const stabilizedThis = array.map((el, index) => [el, index] as [T, number])
|
||||
stabilizedThis.sort((a, b) => {
|
||||
if (less) {
|
||||
const ascending = order === "asc"
|
||||
const result = ascending
|
||||
? -less(a[0], b[0], ascending)
|
||||
: less(a[0], b[0], ascending)
|
||||
if (result !== 0) return result
|
||||
} else {
|
||||
const result = comparator(a[0], b[0])
|
||||
if (result !== 0) return result
|
||||
}
|
||||
return a[1] - b[1]
|
||||
})
|
||||
return stabilizedThis.map((el) => el[0])
|
||||
const PaginationForm1: React.FC<{
|
||||
onPageNumberSubmit: (value: number) => void
|
||||
maxPageNumber: number
|
||||
}> = ({ onPageNumberSubmit, maxPageNumber }) => {
|
||||
// This component is separated from DataGrid to prevent `DataGrid` from re-rendering the page,
|
||||
// every time any letters are input.
|
||||
const [specifiedPageText, setSpecifiedPageText] = React.useState("")
|
||||
|
||||
const handleSubmitPageNumber = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
const newPageNumber = parseInt(specifiedPageText, 10)
|
||||
// Page is 0-indexed in `TablePagination`.
|
||||
onPageNumberSubmit(newPageNumber - 1)
|
||||
setSpecifiedPageText("") // reset the input field
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmitPageNumber}>
|
||||
<TextField
|
||||
size="small"
|
||||
label={`Go to Page: n / ${maxPageNumber}`}
|
||||
value={specifiedPageText}
|
||||
type="number"
|
||||
style={{ width: 200 }}
|
||||
inputProps={{ min: 1, max: maxPageNumber }}
|
||||
onChange={(e) => {
|
||||
setSpecifiedPageText(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
const isNumber = (
|
||||
rowsPerPage: number | { value: number; label: string }
|
||||
): rowsPerPage is number => {
|
||||
return typeof rowsPerPage === "number"
|
||||
}
|
||||
|
||||
export { DataGrid, DataGridColumn }
|
||||
export { DataGrid }
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
} from "@mui/material"
|
||||
import React, { ReactNode, useState } from "react"
|
||||
import { useRecoilValue } from "recoil"
|
||||
import { actionCreator } from "../action"
|
||||
import { artifactIsAvailable as artifactIsAvailableState } from "../state"
|
||||
|
||||
export const useDeleteStudyDialog = (): [
|
||||
(studyId: number) => void,
|
||||
() => ReactNode,
|
||||
] => {
|
||||
const action = actionCreator()
|
||||
const artifactIsAvailable = useRecoilValue(artifactIsAvailableState)
|
||||
|
||||
const [openDeleteStudyDialog, setOpenDeleteStudyDialog] = useState(false)
|
||||
const [deleteStudyID, setDeleteStudyID] = useState(-1)
|
||||
const [removeAssociatedArtifacts, setRemoveAssociatedArtifacts] =
|
||||
useState(false)
|
||||
|
||||
const handleCloseDeleteStudyDialog = () => {
|
||||
setOpenDeleteStudyDialog(false)
|
||||
setDeleteStudyID(-1)
|
||||
setRemoveAssociatedArtifacts(false)
|
||||
}
|
||||
|
||||
const handleDeleteStudy = () => {
|
||||
action.deleteStudy(deleteStudyID)
|
||||
setOpenDeleteStudyDialog(false)
|
||||
setDeleteStudyID(-1)
|
||||
action.deleteStudy(deleteStudyID, removeAssociatedArtifacts)
|
||||
handleCloseDeleteStudyDialog()
|
||||
}
|
||||
|
||||
const openDialog = (studyId: number) => {
|
||||
@@ -42,12 +50,33 @@ export const useDeleteStudyDialog = (): [
|
||||
handleCloseDeleteStudyDialog()
|
||||
}}
|
||||
aria-labelledby="delete-study-dialog-title"
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
>
|
||||
<DialogTitle id="delete-study-dialog-title">Delete study</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Are you sure you want to delete a study (id={deleteStudyID})?
|
||||
</DialogContentText>
|
||||
{artifactIsAvailable && (
|
||||
<>
|
||||
<FormControlLabel
|
||||
label="Remove associated trial/study artifacts."
|
||||
control={
|
||||
<Checkbox
|
||||
checked={removeAssociatedArtifacts}
|
||||
onChange={() => setRemoveAssociatedArtifacts((cur) => !cur)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{removeAssociatedArtifacts && (
|
||||
<Alert severity="warning">
|
||||
If artifacts are linked to another study or trial, they will
|
||||
no longer be accessible from that study or trial as well.
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDeleteStudyDialog} color="primary">
|
||||
|
||||
@@ -12,17 +12,19 @@ import {
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import blue from "@mui/material/colors/blue"
|
||||
import {
|
||||
GraphContainer,
|
||||
useGraphComponentState,
|
||||
useMergedUnionSearchSpace,
|
||||
} from "@optuna/react"
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import React, { FC, useEffect, useMemo, useState } from "react"
|
||||
import { SearchSpaceItem, StudyDetail, Trial } from "ts/types/optuna"
|
||||
import { PlotType } from "../apiClient"
|
||||
import { getAxisInfo } from "../graphUtil"
|
||||
import { useGraphComponentState } from "../hooks/useGraphComponentState"
|
||||
import { usePlot } from "../hooks/usePlot"
|
||||
import { useMergedUnionSearchSpace } from "../searchSpace"
|
||||
import { usePlotlyColorTheme } from "../state"
|
||||
import { useBackendRender } from "../state"
|
||||
import GraphContainer from "./GraphContainer"
|
||||
|
||||
const plotDomId = "graph-contour"
|
||||
const CONTOUR_DISABLED_THRESHOLD = 100
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import { Box, Typography, useTheme } from "@mui/material"
|
||||
import { GraphContainer, PlotEdf, useGraphComponentState } from "@optuna/react"
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import React, { FC, useEffect, useMemo } from "react"
|
||||
import { StudyDetail, Trial } from "ts/types/optuna"
|
||||
import React, { FC, useEffect } from "react"
|
||||
import { StudyDetail } from "ts/types/optuna"
|
||||
import { CompareStudiesPlotType } from "../apiClient"
|
||||
import { useAPIClient } from "../apiClientProvider"
|
||||
import { useGraphComponentState } from "../hooks/useGraphComponentState"
|
||||
import { useBackendRender, usePlotlyColorTheme } from "../state"
|
||||
import { Target, useFilteredTrialsFromStudies } from "../trialFilter"
|
||||
import GraphContainer from "./GraphContainer"
|
||||
|
||||
const getPlotDomId = (objectiveId: number) => `graph-edf-${objectiveId}`
|
||||
|
||||
interface EdfPlotInfo {
|
||||
study_name: string
|
||||
trials: Trial[]
|
||||
}
|
||||
import { useBackendRender } from "../state"
|
||||
|
||||
export const GraphEdf: FC<{
|
||||
studies: StudyDetail[]
|
||||
@@ -23,10 +13,12 @@ export const GraphEdf: FC<{
|
||||
if (useBackendRender()) {
|
||||
return <GraphEdfBackend studies={studies} />
|
||||
} else {
|
||||
return <GraphEdfFrontend studies={studies} objectiveId={objectiveId} />
|
||||
return <PlotEdf studies={studies} objectiveId={objectiveId} />
|
||||
}
|
||||
}
|
||||
|
||||
const domId = "graph-edf"
|
||||
|
||||
const GraphEdfBackend: FC<{
|
||||
studies: StudyDetail[]
|
||||
}> = ({ studies }) => {
|
||||
@@ -34,7 +26,6 @@ const GraphEdfBackend: FC<{
|
||||
const { graphComponentState, notifyGraphDidRender } = useGraphComponentState()
|
||||
|
||||
const studyIds = studies.map((s) => s.id)
|
||||
const domId = getPlotDomId(-1)
|
||||
const numCompletedTrials = studies.reduce(
|
||||
(acc, study) =>
|
||||
acc + study?.trials.filter((t) => t.state === "Complete").length,
|
||||
@@ -62,112 +53,3 @@ const GraphEdfBackend: FC<{
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const GraphEdfFrontend: FC<{
|
||||
studies: StudyDetail[]
|
||||
objectiveId: number
|
||||
}> = ({ studies, objectiveId }) => {
|
||||
const { graphComponentState, notifyGraphDidRender } = useGraphComponentState()
|
||||
|
||||
const theme = useTheme()
|
||||
const colorTheme = usePlotlyColorTheme(theme.palette.mode)
|
||||
|
||||
const domId = getPlotDomId(objectiveId)
|
||||
const target = useMemo<Target>(
|
||||
() => new Target("objective", objectiveId),
|
||||
[objectiveId]
|
||||
)
|
||||
const trials = useFilteredTrialsFromStudies(studies, [target], false)
|
||||
const edfPlotInfos = studies.map((study, index) => {
|
||||
const e: EdfPlotInfo = {
|
||||
study_name: study?.name,
|
||||
trials: trials[index],
|
||||
}
|
||||
return e
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (graphComponentState !== "componentWillMount") {
|
||||
plotEdf(edfPlotInfos, target, domId, colorTheme)?.then(
|
||||
notifyGraphDidRender
|
||||
)
|
||||
}
|
||||
}, [studies, target, colorTheme, graphComponentState])
|
||||
|
||||
return (
|
||||
<Box component="div">
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ margin: "1em 0", fontWeight: theme.typography.fontWeightBold }}
|
||||
>
|
||||
{`EDF for ${target.toLabel(studies[0].objective_names)}`}
|
||||
</Typography>
|
||||
<GraphContainer
|
||||
plotDomId={domId}
|
||||
graphComponentState={graphComponentState}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const plotEdf = (
|
||||
edfPlotInfos: EdfPlotInfo[],
|
||||
target: Target,
|
||||
domId: string,
|
||||
colorTheme: Partial<Plotly.Template>
|
||||
) => {
|
||||
if (document.getElementById(domId) === null) {
|
||||
return
|
||||
}
|
||||
if (edfPlotInfos.length === 0) {
|
||||
return plotly.react(domId, [], {
|
||||
template: colorTheme,
|
||||
})
|
||||
}
|
||||
|
||||
const target_name = "Objective Value"
|
||||
const layout: Partial<plotly.Layout> = {
|
||||
xaxis: {
|
||||
title: target_name,
|
||||
},
|
||||
yaxis: {
|
||||
title: "Cumulative Probability",
|
||||
},
|
||||
margin: {
|
||||
l: 50,
|
||||
t: 0,
|
||||
r: 50,
|
||||
b: 50,
|
||||
},
|
||||
template: colorTheme,
|
||||
legend: {
|
||||
x: 1.0,
|
||||
y: 0.95,
|
||||
},
|
||||
}
|
||||
|
||||
const plotData: Partial<plotly.PlotData>[] = edfPlotInfos.map((h) => {
|
||||
const values = h.trials.map((t) => target.getTargetValue(t) as number)
|
||||
const numValues = values.length
|
||||
const minX = Math.min(...values)
|
||||
const maxX = Math.max(...values)
|
||||
const numStep = 100
|
||||
const _step = (maxX - minX) / (numStep - 1)
|
||||
|
||||
const xValues = []
|
||||
const yValues = []
|
||||
for (let i = 0; i < numStep; i++) {
|
||||
const boundary_right = minX + _step * i
|
||||
xValues.push(boundary_right)
|
||||
yValues.push(values.filter((v) => v <= boundary_right).length / numValues)
|
||||
}
|
||||
|
||||
return {
|
||||
type: "scatter",
|
||||
name: `${h.study_name}`,
|
||||
x: xValues,
|
||||
y: yValues,
|
||||
}
|
||||
})
|
||||
return plotly.react(domId, plotData, layout)
|
||||
}
|
||||
|
||||
@@ -13,23 +13,23 @@ import {
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import * as Optuna from "@optuna/types"
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import React, { ChangeEvent, FC, useEffect, useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { StudyDetail, Trial } from "ts/types/optuna"
|
||||
import { usePlotlyColorTheme } from "../state"
|
||||
import {
|
||||
Target,
|
||||
useFilteredTrialsFromStudies,
|
||||
useObjectiveAndUserAttrTargetsFromStudies,
|
||||
} from "../trialFilter"
|
||||
} from "@optuna/react"
|
||||
import * as Optuna from "@optuna/types"
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import React, { ChangeEvent, FC, useEffect, useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { StudyDetail } from "ts/types/optuna"
|
||||
import { usePlotlyColorTheme } from "../state"
|
||||
|
||||
const plotDomId = "graph-history"
|
||||
|
||||
interface HistoryPlotInfo {
|
||||
study_name: string
|
||||
trials: Trial[]
|
||||
trials: Optuna.Trial[]
|
||||
directions: Optuna.StudyDirection[]
|
||||
objective_names?: string[]
|
||||
}
|
||||
@@ -265,7 +265,7 @@ const plotHistory = (
|
||||
},
|
||||
}
|
||||
|
||||
const getAxisX = (trial: Trial): number | Date => {
|
||||
const getAxisX = (trial: Optuna.Trial): number | Date => {
|
||||
return xAxis === "number"
|
||||
? trial.number
|
||||
: xAxis === "datetime_start"
|
||||
@@ -276,8 +276,8 @@ const plotHistory = (
|
||||
const plotData: Partial<plotly.PlotData>[] = []
|
||||
const infeasiblePlotData: Partial<plotly.PlotData>[] = []
|
||||
historyPlotInfos.forEach((h) => {
|
||||
const feasibleTrials: Trial[] = []
|
||||
const infeasibleTrials: Trial[] = []
|
||||
const feasibleTrials: Optuna.Trial[] = []
|
||||
const infeasibleTrials: Optuna.Trial[] = []
|
||||
h.trials.forEach((t) => {
|
||||
if (t.constraints.every((c) => c <= 0)) {
|
||||
feasibleTrials.push(t)
|
||||
@@ -288,7 +288,7 @@ const plotHistory = (
|
||||
plotData.push({
|
||||
x: feasibleTrials.map(getAxisX),
|
||||
y: feasibleTrials.map(
|
||||
(t: Trial): number => target.getTargetValue(t) as number
|
||||
(t: Optuna.Trial): number => target.getTargetValue(t) as number
|
||||
),
|
||||
name: `${target.toLabel(h.objective_names)} of ${h.study_name}`,
|
||||
marker: {
|
||||
@@ -353,7 +353,7 @@ const plotHistory = (
|
||||
infeasiblePlotData.push({
|
||||
x: infeasibleTrials.map(getAxisX),
|
||||
y: infeasibleTrials.map(
|
||||
(t: Trial): number => target.getTargetValue(t) as number
|
||||
(t: Optuna.Trial): number => target.getTargetValue(t) as number
|
||||
),
|
||||
name: `Infeasible Trial of ${h.study_name}`,
|
||||
marker: {
|
||||
|
||||
@@ -6,22 +6,25 @@ import {
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import React, { FC, ReactNode, useEffect, useState } from "react"
|
||||
import { SearchSpaceItem, StudyDetail, Trial } from "ts/types/optuna"
|
||||
import { PlotType } from "../apiClient"
|
||||
import { useGraphComponentState } from "../hooks/useGraphComponentState"
|
||||
import { usePlot } from "../hooks/usePlot"
|
||||
import { useMergedUnionSearchSpace } from "../searchSpace"
|
||||
import { usePlotlyColorTheme } from "../state"
|
||||
import { useBackendRender } from "../state"
|
||||
import {
|
||||
GraphContainer,
|
||||
useGraphComponentState,
|
||||
useMergedUnionSearchSpace,
|
||||
} from "@optuna/react"
|
||||
import {
|
||||
Target,
|
||||
useFilteredTrials,
|
||||
useObjectiveAndUserAttrTargets,
|
||||
useParamTargets,
|
||||
} from "../trialFilter"
|
||||
import GraphContainer from "./GraphContainer"
|
||||
} from "@optuna/react"
|
||||
import * as Optuna from "@optuna/types"
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import React, { FC, ReactNode, useEffect, useState } from "react"
|
||||
import { SearchSpaceItem, StudyDetail } from "ts/types/optuna"
|
||||
import { PlotType } from "../apiClient"
|
||||
import { usePlot } from "../hooks/usePlot"
|
||||
import { usePlotlyColorTheme } from "../state"
|
||||
import { useBackendRender } from "../state"
|
||||
|
||||
const plotDomId = "graph-parallel-coordinate"
|
||||
|
||||
@@ -185,7 +188,7 @@ const GraphParallelCoordinateFrontend: FC<{
|
||||
|
||||
const plotCoordinate = (
|
||||
study: StudyDetail,
|
||||
trials: Trial[],
|
||||
trials: Optuna.Trial[],
|
||||
targets: Target[],
|
||||
searchSpace: SearchSpaceItem[],
|
||||
colorTheme: Partial<Plotly.Template>
|
||||
|
||||
@@ -8,16 +8,18 @@ import {
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import {
|
||||
GraphContainer,
|
||||
useGraphComponentState,
|
||||
useMergedUnionSearchSpace,
|
||||
} from "@optuna/react"
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import React, { FC, useEffect, useState } from "react"
|
||||
import { SearchSpaceItem, StudyDetail, Trial } from "ts/types/optuna"
|
||||
import { PlotType } from "../apiClient"
|
||||
import { getAxisInfo, makeHovertext } from "../graphUtil"
|
||||
import { useGraphComponentState } from "../hooks/useGraphComponentState"
|
||||
import { usePlot } from "../hooks/usePlot"
|
||||
import { useMergedUnionSearchSpace } from "../searchSpace"
|
||||
import { useBackendRender, usePlotlyColorTheme } from "../state"
|
||||
import GraphContainer from "./GraphContainer"
|
||||
|
||||
const plotDomId = "graph-rank"
|
||||
|
||||
|
||||
@@ -1,38 +1,14 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Grid,
|
||||
MenuItem,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
Switch,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
GraphContainer,
|
||||
PlotSlice,
|
||||
useGraphComponentState,
|
||||
} from "@optuna/react"
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import React, { FC, useEffect, useState } from "react"
|
||||
import { SearchSpaceItem, StudyDetail, Trial } from "ts/types/optuna"
|
||||
import React, { FC, useEffect } from "react"
|
||||
import { StudyDetail } from "ts/types/optuna"
|
||||
import { PlotType } from "../apiClient"
|
||||
import { useGraphComponentState } from "../hooks/useGraphComponentState"
|
||||
import { usePlot } from "../hooks/usePlot"
|
||||
import { useMergedUnionSearchSpace } from "../searchSpace"
|
||||
import { useBackendRender, usePlotlyColorTheme } from "../state"
|
||||
import {
|
||||
Target,
|
||||
useFilteredTrials,
|
||||
useObjectiveAndUserAttrTargets,
|
||||
useParamTargets,
|
||||
} from "../trialFilter"
|
||||
import GraphContainer from "./GraphContainer"
|
||||
|
||||
const plotDomId = "graph-slice"
|
||||
|
||||
const isLogScale = (s: SearchSpaceItem): boolean => {
|
||||
if (s.distribution.type === "CategoricalDistribution") {
|
||||
return false
|
||||
}
|
||||
return s.distribution.log
|
||||
}
|
||||
import { useBackendRender } from "../state"
|
||||
|
||||
export const GraphSlice: FC<{
|
||||
study: StudyDetail | null
|
||||
@@ -40,10 +16,12 @@ export const GraphSlice: FC<{
|
||||
if (useBackendRender()) {
|
||||
return <GraphSliceBackend study={study} />
|
||||
} else {
|
||||
return <GraphSliceFrontend study={study} />
|
||||
return <PlotSlice study={study} />
|
||||
}
|
||||
}
|
||||
|
||||
const domId = "graph-slice"
|
||||
|
||||
const GraphSliceBackend: FC<{
|
||||
study: StudyDetail | null
|
||||
}> = ({ study = null }) => {
|
||||
@@ -61,7 +39,7 @@ const GraphSliceBackend: FC<{
|
||||
|
||||
useEffect(() => {
|
||||
if (data && layout && graphComponentState !== "componentWillMount") {
|
||||
plotly.react(plotDomId, data, layout).then(notifyGraphDidRender)
|
||||
plotly.react(domId, data, layout).then(notifyGraphDidRender)
|
||||
}
|
||||
}, [data, layout, graphComponentState])
|
||||
useEffect(() => {
|
||||
@@ -72,253 +50,8 @@ const GraphSliceBackend: FC<{
|
||||
|
||||
return (
|
||||
<GraphContainer
|
||||
plotDomId={plotDomId}
|
||||
plotDomId={domId}
|
||||
graphComponentState={graphComponentState}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const GraphSliceFrontend: FC<{
|
||||
study: StudyDetail | null
|
||||
}> = ({ study = null }) => {
|
||||
const { graphComponentState, notifyGraphDidRender } = useGraphComponentState()
|
||||
|
||||
const theme = useTheme()
|
||||
const colorTheme = usePlotlyColorTheme(theme.palette.mode)
|
||||
|
||||
const [objectiveTargets, selectedObjective, setObjectiveTarget] =
|
||||
useObjectiveAndUserAttrTargets(study)
|
||||
const searchSpace = useMergedUnionSearchSpace(study?.union_search_space)
|
||||
const [paramTargets, selectedParamTarget, setParamTarget] =
|
||||
useParamTargets(searchSpace)
|
||||
const [logYScale, setLogYScale] = useState<boolean>(false)
|
||||
|
||||
const trials = useFilteredTrials(
|
||||
study,
|
||||
selectedParamTarget !== null
|
||||
? [selectedObjective, selectedParamTarget]
|
||||
: [selectedObjective],
|
||||
false
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (graphComponentState !== "componentWillMount") {
|
||||
plotSlice(
|
||||
trials,
|
||||
selectedObjective,
|
||||
selectedParamTarget,
|
||||
searchSpace.find((s) => s.name === selectedParamTarget?.key) || null,
|
||||
logYScale,
|
||||
colorTheme
|
||||
)?.then(notifyGraphDidRender)
|
||||
}
|
||||
}, [
|
||||
trials,
|
||||
selectedObjective,
|
||||
searchSpace,
|
||||
selectedParamTarget,
|
||||
logYScale,
|
||||
colorTheme,
|
||||
graphComponentState,
|
||||
])
|
||||
|
||||
const handleObjectiveChange = (event: SelectChangeEvent<string>) => {
|
||||
setObjectiveTarget(event.target.value)
|
||||
}
|
||||
|
||||
const handleSelectedParam = (e: SelectChangeEvent<string>) => {
|
||||
setParamTarget(e.target.value)
|
||||
}
|
||||
|
||||
const handleLogYScaleChange = () => {
|
||||
setLogYScale(!logYScale)
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container direction="row">
|
||||
<Grid
|
||||
item
|
||||
xs={3}
|
||||
container
|
||||
direction="column"
|
||||
sx={{ paddingRight: theme.spacing(2) }}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ margin: "1em 0", fontWeight: theme.typography.fontWeightBold }}
|
||||
>
|
||||
Slice
|
||||
</Typography>
|
||||
{objectiveTargets.length !== 1 && (
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Objective:</FormLabel>
|
||||
<Select
|
||||
value={selectedObjective.identifier()}
|
||||
onChange={handleObjectiveChange}
|
||||
>
|
||||
{objectiveTargets.map((t, i) => (
|
||||
<MenuItem value={t.identifier()} key={i}>
|
||||
{t.toLabel(study?.objective_names)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
{paramTargets.length !== 0 && selectedParamTarget !== null && (
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Parameter:</FormLabel>
|
||||
<Select
|
||||
value={selectedParamTarget.identifier()}
|
||||
onChange={handleSelectedParam}
|
||||
>
|
||||
{paramTargets.map((t, i) => (
|
||||
<MenuItem value={t.identifier()} key={i}>
|
||||
{t.toLabel()}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Log y scale:</FormLabel>
|
||||
<Switch
|
||||
checked={logYScale}
|
||||
onChange={handleLogYScaleChange}
|
||||
value="enable"
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<GraphContainer
|
||||
plotDomId={plotDomId}
|
||||
graphComponentState={graphComponentState}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const plotSlice = (
|
||||
trials: Trial[],
|
||||
objectiveTarget: Target,
|
||||
selectedParamTarget: Target | null,
|
||||
selectedParamSpace: SearchSpaceItem | null,
|
||||
logYScale: boolean,
|
||||
colorTheme: Partial<Plotly.Template>
|
||||
) => {
|
||||
if (document.getElementById(plotDomId) === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const layout: Partial<plotly.Layout> = {
|
||||
margin: {
|
||||
l: 50,
|
||||
t: 0,
|
||||
r: 50,
|
||||
b: 0,
|
||||
},
|
||||
xaxis: {
|
||||
title: selectedParamTarget?.toLabel() || "",
|
||||
type:
|
||||
selectedParamSpace !== null && isLogScale(selectedParamSpace)
|
||||
? "log"
|
||||
: "linear",
|
||||
gridwidth: 1,
|
||||
automargin: true,
|
||||
},
|
||||
yaxis: {
|
||||
title: "Objective Value",
|
||||
type: logYScale ? "log" : "linear",
|
||||
gridwidth: 1,
|
||||
automargin: true,
|
||||
},
|
||||
showlegend: false,
|
||||
uirevision: "true",
|
||||
template: colorTheme,
|
||||
}
|
||||
if (
|
||||
selectedParamSpace === null ||
|
||||
selectedParamTarget === null ||
|
||||
trials.length === 0
|
||||
) {
|
||||
return plotly.react(plotDomId, [], layout)
|
||||
}
|
||||
|
||||
const feasibleTrials: Trial[] = []
|
||||
const infeasibleTrials: Trial[] = []
|
||||
trials.forEach((t) => {
|
||||
if (t.constraints.every((c) => c <= 0)) {
|
||||
feasibleTrials.push(t)
|
||||
} else {
|
||||
infeasibleTrials.push(t)
|
||||
}
|
||||
})
|
||||
|
||||
const feasibleObjectiveValues: number[] = feasibleTrials.map(
|
||||
(t) => objectiveTarget.getTargetValue(t) as number
|
||||
)
|
||||
const infeasibleObjectiveValues: number[] = infeasibleTrials.map(
|
||||
(t) => objectiveTarget.getTargetValue(t) as number
|
||||
)
|
||||
|
||||
const feasibleValues = feasibleTrials.map(
|
||||
(t) => selectedParamTarget.getTargetValue(t) as number
|
||||
)
|
||||
const infeasibleValues = infeasibleTrials.map(
|
||||
(t) => selectedParamTarget.getTargetValue(t) as number
|
||||
)
|
||||
const trace: plotly.Data[] = [
|
||||
{
|
||||
type: "scatter",
|
||||
x: feasibleValues,
|
||||
y: feasibleObjectiveValues,
|
||||
mode: "markers",
|
||||
name: "Feasible Trial",
|
||||
marker: {
|
||||
color: feasibleTrials.map((t) => t.number),
|
||||
colorscale: "Blues",
|
||||
reversescale: true,
|
||||
colorbar: {
|
||||
title: "Trial",
|
||||
},
|
||||
line: {
|
||||
color: "Grey",
|
||||
width: 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "scatter",
|
||||
x: infeasibleValues,
|
||||
y: infeasibleObjectiveValues,
|
||||
mode: "markers",
|
||||
name: "Infeasible Trial",
|
||||
marker: {
|
||||
color: "#cccccc",
|
||||
reversescale: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
if (selectedParamSpace.distribution.type !== "CategoricalDistribution") {
|
||||
layout["xaxis"] = {
|
||||
title: selectedParamTarget.toLabel(),
|
||||
type: isLogScale(selectedParamSpace) ? "log" : "linear",
|
||||
gridwidth: 1,
|
||||
automargin: true, // Otherwise the label is outside of the plot
|
||||
}
|
||||
} else {
|
||||
const vocabArr = selectedParamSpace.distribution.choices.map(
|
||||
(c) => c?.toString() ?? "null"
|
||||
)
|
||||
const tickvals: number[] = vocabArr.map((v, i) => i)
|
||||
layout["xaxis"] = {
|
||||
title: selectedParamTarget.toLabel(),
|
||||
type: "linear",
|
||||
gridwidth: 1,
|
||||
tickvals: tickvals,
|
||||
ticktext: vocabArr,
|
||||
automargin: true, // Otherwise the label is outside of the plot
|
||||
}
|
||||
}
|
||||
return plotly.react(plotDomId, trace, layout)
|
||||
}
|
||||
|
||||
@@ -11,18 +11,30 @@ import * as Optuna from "@optuna/types"
|
||||
import React, { FC } from "react"
|
||||
import { useStudyDetailValue, useStudySummaryValue } from "../../state"
|
||||
import { BestTrialsCard } from "../BestTrialsCard"
|
||||
import { DataGrid, DataGridColumn } from "../DataGrid"
|
||||
import { DataGrid } from "../DataGrid"
|
||||
import { Contour } from "../GraphContour"
|
||||
|
||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
||||
|
||||
export const PreferentialAnalytics: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
const theme = useTheme()
|
||||
const studySummary = useStudySummaryValue(studyId)
|
||||
const studyDetail = useStudyDetailValue(studyId)
|
||||
|
||||
const userAttrs = studySummary?.user_attrs || studyDetail?.user_attrs || []
|
||||
const userAttrColumns: DataGridColumn<Optuna.Attribute>[] = [
|
||||
{ field: "key", label: "Key", sortable: true },
|
||||
{ field: "value", label: "Value", sortable: true },
|
||||
const columnHelper = createColumnHelper<Optuna.Attribute>()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const columns: ColumnDef<Optuna.Attribute, any>[] = [
|
||||
columnHelper.accessor("key", {
|
||||
header: "Key",
|
||||
enableSorting: true,
|
||||
enableColumnFilter: false,
|
||||
}),
|
||||
columnHelper.accessor("value", {
|
||||
header: "Value",
|
||||
enableSorting: true,
|
||||
enableColumnFilter: false,
|
||||
}),
|
||||
]
|
||||
return (
|
||||
<Box
|
||||
@@ -55,14 +67,7 @@ export const PreferentialAnalytics: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
>
|
||||
Study User Attributes
|
||||
</Typography>
|
||||
<DataGrid<Optuna.Attribute>
|
||||
columns={userAttrColumns}
|
||||
rows={userAttrs}
|
||||
keyField={"key"}
|
||||
dense={true}
|
||||
initialRowsPerPage={5}
|
||||
rowsPerPageOption={[5, 10, { label: "All", value: -1 }]}
|
||||
/>
|
||||
<DataGrid data={userAttrs} columns={columns} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid2>
|
||||
|
||||
@@ -171,7 +171,7 @@ export const StudyDetail: FC<{
|
||||
>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
<TrialTable studyDetail={studyDetail} initialRowsPerPage={50} />
|
||||
<TrialTable studyDetail={studyDetail} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { artifactIsAvailable } from "../state"
|
||||
import { StudyArtifactCards } from "./Artifact/StudyArtifactCards"
|
||||
import { BestTrialsCard } from "./BestTrialsCard"
|
||||
import { DataGrid, DataGridColumn } from "./DataGrid"
|
||||
import { DataGrid } from "./DataGrid"
|
||||
import { GraphHistory } from "./GraphHistory"
|
||||
import { GraphHyperparameterImportance } from "./GraphHyperparameterImportances"
|
||||
import { GraphIntermediateValues } from "./GraphIntermediateValues"
|
||||
@@ -29,6 +29,8 @@ import { GraphParetoFront } from "./GraphParetoFront"
|
||||
import { GraphTimeline } from "./GraphTimeline"
|
||||
import { UserDefinedPlot } from "./UserDefinedPlot"
|
||||
|
||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
||||
|
||||
export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
const theme = useTheme()
|
||||
const directions = useStudyDirections(studyId)
|
||||
@@ -47,9 +49,19 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
}
|
||||
|
||||
const userAttrs = studySummary?.user_attrs || studyDetail?.user_attrs || []
|
||||
const userAttrColumns: DataGridColumn<Optuna.Attribute>[] = [
|
||||
{ field: "key", label: "Key", sortable: true },
|
||||
{ field: "value", label: "Value", sortable: true },
|
||||
const columnHelper = createColumnHelper<Optuna.Attribute>()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const columns: ColumnDef<Optuna.Attribute, any>[] = [
|
||||
columnHelper.accessor("key", {
|
||||
header: "Key",
|
||||
enableSorting: true,
|
||||
enableColumnFilter: false,
|
||||
}),
|
||||
columnHelper.accessor("value", {
|
||||
header: "Value",
|
||||
enableSorting: true,
|
||||
enableColumnFilter: false,
|
||||
}),
|
||||
]
|
||||
const trials: Trial[] = studyDetail?.trials || []
|
||||
return (
|
||||
@@ -153,14 +165,7 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
>
|
||||
Study User Attributes
|
||||
</Typography>
|
||||
<DataGrid<Optuna.Attribute>
|
||||
columns={userAttrColumns}
|
||||
rows={userAttrs}
|
||||
keyField={"key"}
|
||||
dense={true}
|
||||
initialRowsPerPage={5}
|
||||
rowsPerPageOption={[5, 10, { label: "All", value: -1 }]}
|
||||
/>
|
||||
<DataGrid data={userAttrs} columns={columns} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid2>
|
||||
|
||||
@@ -5,85 +5,69 @@ import React, { FC } from "react"
|
||||
|
||||
import { Link } from "react-router-dom"
|
||||
import { StudyDetail, Trial } from "ts/types/optuna"
|
||||
import { DataGrid, DataGridColumn } from "./DataGrid"
|
||||
import { DataGrid } from "./DataGrid"
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
FilterFn,
|
||||
Row,
|
||||
createColumnHelper,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
const multiValueFilter: FilterFn<Trial> = <D extends object>(
|
||||
row: Row<D>,
|
||||
columnId: string,
|
||||
filterValue: string[]
|
||||
) => {
|
||||
const rowValue = row.getValue(columnId) as string
|
||||
return !filterValue.includes(rowValue)
|
||||
}
|
||||
|
||||
export const TrialTable: FC<{
|
||||
studyDetail: StudyDetail | null
|
||||
initialRowsPerPage?: number
|
||||
}> = ({ studyDetail, initialRowsPerPage }) => {
|
||||
}> = ({ studyDetail }) => {
|
||||
const theme = useTheme()
|
||||
const trials: Trial[] = studyDetail !== null ? studyDetail.trials : []
|
||||
const objectiveNames: string[] = studyDetail?.objective_names || []
|
||||
|
||||
const columns: DataGridColumn<Trial>[] = [
|
||||
{ field: "number", label: "Number", sortable: true, padding: "none" },
|
||||
{
|
||||
field: "state",
|
||||
label: "State",
|
||||
sortable: true,
|
||||
filterChoices: ["Complete", "Pruned", "Fail", "Running", "Waiting"],
|
||||
padding: "none",
|
||||
toCellValue: (i) => trials[i].state.toString(),
|
||||
},
|
||||
const columnHelper = createColumnHelper<Trial>()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const columns: ColumnDef<Trial, any>[] = [
|
||||
columnHelper.accessor("number", {
|
||||
header: "Number",
|
||||
enableColumnFilter: false,
|
||||
}),
|
||||
columnHelper.accessor("state", {
|
||||
header: "State",
|
||||
enableSorting: false,
|
||||
enableColumnFilter: true,
|
||||
filterFn: multiValueFilter,
|
||||
}),
|
||||
]
|
||||
const valueComparator = (
|
||||
firstVal?: number,
|
||||
secondVal?: number,
|
||||
ascending = true
|
||||
): number => {
|
||||
if (firstVal === secondVal) {
|
||||
return 0
|
||||
}
|
||||
if (firstVal === undefined) {
|
||||
return ascending ? -1 : 1
|
||||
} else if (secondVal === undefined) {
|
||||
return ascending ? 1 : -1
|
||||
}
|
||||
return firstVal < secondVal ? 1 : -1
|
||||
}
|
||||
if (studyDetail === null || studyDetail.directions.length === 1) {
|
||||
columns.push({
|
||||
field: "values",
|
||||
label: "Value",
|
||||
sortable: true,
|
||||
less: (firstEl, secondEl, ascending): number => {
|
||||
return valueComparator(
|
||||
firstEl.values?.[0],
|
||||
secondEl.values?.[0],
|
||||
ascending
|
||||
)
|
||||
},
|
||||
toCellValue: (i) => {
|
||||
if (trials[i].values === undefined) {
|
||||
return null
|
||||
}
|
||||
return trials[i].values?.[0]
|
||||
},
|
||||
})
|
||||
columns.push(
|
||||
columnHelper.accessor("values", {
|
||||
header: "Value",
|
||||
enableSorting: true,
|
||||
enableColumnFilter: false,
|
||||
sortUndefined: "last",
|
||||
})
|
||||
)
|
||||
} else {
|
||||
const objectiveColumns: DataGridColumn<Trial>[] =
|
||||
studyDetail.directions.map((s, objectiveId) => ({
|
||||
field: "values",
|
||||
label:
|
||||
objectiveNames.length === studyDetail?.directions.length
|
||||
? objectiveNames[objectiveId]
|
||||
: `Objective ${objectiveId}`,
|
||||
sortable: true,
|
||||
less: (firstEl, secondEl, ascending): number => {
|
||||
return valueComparator(
|
||||
firstEl.values?.[objectiveId],
|
||||
secondEl.values?.[objectiveId],
|
||||
ascending
|
||||
)
|
||||
},
|
||||
toCellValue: (i) => {
|
||||
if (trials[i].values === undefined) {
|
||||
return null
|
||||
}
|
||||
return trials[i].values?.[objectiveId]
|
||||
},
|
||||
}))
|
||||
columns.push(...objectiveColumns)
|
||||
columns.push(
|
||||
...studyDetail.directions.map((s, objectiveId) =>
|
||||
columnHelper.accessor((row) => row["values"]?.[objectiveId], {
|
||||
id: `values_${objectiveId}`,
|
||||
header:
|
||||
objectiveNames.length === studyDetail?.directions.length
|
||||
? objectiveNames[objectiveId]
|
||||
: `Objective ${objectiveId}`,
|
||||
enableSorting: true,
|
||||
enableColumnFilter: false,
|
||||
sortUndefined: "last",
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
const isDynamicSpace =
|
||||
studyDetail?.union_search_space.length !==
|
||||
@@ -100,78 +84,65 @@ export const TrialTable: FC<{
|
||||
if (filterChoices !== undefined && isDynamicSpace && hasMissingValue) {
|
||||
filterChoices.push(null)
|
||||
}
|
||||
columns.push({
|
||||
field: "params",
|
||||
label: `Param ${s.name}`,
|
||||
toCellValue: (i) =>
|
||||
trials[i].params.find((p) => p.name === s.name)?.param_external_value ||
|
||||
null,
|
||||
sortable: sortable,
|
||||
filterChoices: filterChoices,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
less: (firstEl, secondEl, _): number => {
|
||||
const firstVal = firstEl.params.find(
|
||||
(p) => p.name === s.name
|
||||
)?.param_internal_value
|
||||
const secondVal = secondEl.params.find(
|
||||
(p) => p.name === s.name
|
||||
)?.param_internal_value
|
||||
return valueComparator(firstVal, secondVal)
|
||||
},
|
||||
})
|
||||
columns.push(
|
||||
columnHelper.accessor(
|
||||
(row) =>
|
||||
row["params"].find((p) => p.name === s.name)?.param_external_value ||
|
||||
null,
|
||||
{
|
||||
id: `params_${s.name}`,
|
||||
header: `Param ${s.name}`,
|
||||
enableSorting: sortable,
|
||||
sortUndefined: "last",
|
||||
enableColumnFilter: filterChoices !== undefined,
|
||||
filterFn: multiValueFilter,
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
studyDetail?.union_user_attrs.forEach((attr_spec) => {
|
||||
columns.push({
|
||||
field: "user_attrs",
|
||||
label: `UserAttribute ${attr_spec.key}`,
|
||||
toCellValue: (i) =>
|
||||
trials[i].user_attrs.find((attr) => attr.key === attr_spec.key)
|
||||
?.value || null,
|
||||
sortable: attr_spec.sortable,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
less: (firstEl, secondEl, _): number => {
|
||||
const firstValString = firstEl.user_attrs.find(
|
||||
(attr) => attr.key === attr_spec.key
|
||||
)?.value
|
||||
const secondValString = secondEl.user_attrs.find(
|
||||
(attr) => attr.key === attr_spec.key
|
||||
)?.value
|
||||
return valueComparator(
|
||||
Number(firstValString) ?? firstValString,
|
||||
Number(secondValString) ?? secondValString
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
columns.push({
|
||||
field: "trial_id",
|
||||
label: "Detail",
|
||||
toCellValue: (i) => (
|
||||
<IconButton
|
||||
component={Link}
|
||||
to={
|
||||
URL_PREFIX +
|
||||
`/studies/${trials[i].study_id}/trials?numbers=${trials[i].number}`
|
||||
columns.push(
|
||||
columnHelper.accessor(
|
||||
(row) =>
|
||||
row["user_attrs"].find((a) => a.key === attr_spec.key)?.value || null,
|
||||
{
|
||||
id: `user_attrs_${attr_spec.key}`,
|
||||
header: `UserAttribute ${attr_spec.key}`,
|
||||
enableSorting: attr_spec.sortable,
|
||||
enableColumnFilter: false,
|
||||
sortUndefined: "last",
|
||||
}
|
||||
color="inherit"
|
||||
title="Go to the trial's detail page"
|
||||
size="small"
|
||||
>
|
||||
<LinkIcon />
|
||||
</IconButton>
|
||||
),
|
||||
)
|
||||
)
|
||||
})
|
||||
columns.push(
|
||||
columnHelper.accessor((row) => row, {
|
||||
header: "Detail",
|
||||
cell: (info) => (
|
||||
<IconButton
|
||||
component={Link}
|
||||
to={
|
||||
URL_PREFIX +
|
||||
`/studies/${info.getValue().study_id}/trials?numbers=${
|
||||
info.getValue().number
|
||||
}`
|
||||
}
|
||||
color="inherit"
|
||||
title="Go to the trial's detail page"
|
||||
size="small"
|
||||
>
|
||||
<LinkIcon />
|
||||
</IconButton>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableColumnFilter: false,
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataGrid<Trial>
|
||||
columns={columns}
|
||||
rows={trials}
|
||||
keyField={"trial_id"}
|
||||
dense={true}
|
||||
initialRowsPerPage={initialRowsPerPage}
|
||||
/>
|
||||
<DataGrid data={trials} columns={columns} />
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<DownloadIcon />}
|
||||
|
||||
@@ -196,8 +196,3 @@ export type PlotlyColorTheme = {
|
||||
dark: PlotlyColorThemeDark
|
||||
light: PlotlyColorThemeLight
|
||||
}
|
||||
|
||||
export type GraphComponentState =
|
||||
| "componentWillMount"
|
||||
| "componentDidMount"
|
||||
| "graphDidRender"
|
||||
|
||||
@@ -41,6 +41,12 @@ var config = {
|
||||
test: /\.css$/,
|
||||
use: ["style-loader", "css-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
resolve: {
|
||||
fullySpecified: false,
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
|
||||
Generated
+7
-7
@@ -755,12 +755,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -1183,9 +1183,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from unittest import TestCase
|
||||
|
||||
import optuna
|
||||
@@ -523,6 +524,78 @@ class APITestCase(TestCase):
|
||||
self.assertEqual(status, 204)
|
||||
self.assertEqual(len(get_all_study_summaries(storage)), 1)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
version.parse(optuna.__version__) < version.parse("3.4.0"),
|
||||
reason="Needs optuna.artifacts",
|
||||
)
|
||||
def test_delete_study_with_removing_artifacts(self) -> None:
|
||||
from optuna.artifacts import upload_artifact
|
||||
from optuna.artifacts.exceptions import ArtifactNotFound
|
||||
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
with tempfile.TemporaryDirectory() as tmpdir_name:
|
||||
artifact_store = optuna.artifacts.FileSystemArtifactStore(base_path=tmpdir_name)
|
||||
with tempfile.NamedTemporaryFile() as f:
|
||||
f.write(b"dummy")
|
||||
f.flush()
|
||||
artifact_id = upload_artifact(study, f.name, artifact_store)
|
||||
|
||||
app = create_app(storage, artifact_store)
|
||||
|
||||
with artifact_store.open_reader(artifact_id) as reader:
|
||||
self.assertEqual(reader.read(), b"dummy")
|
||||
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/studies/{study._study_id}",
|
||||
"DELETE",
|
||||
body=json.dumps({"remove_associated_artifacts": True}),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 204)
|
||||
|
||||
with self.assertRaises(ArtifactNotFound):
|
||||
with artifact_store.open_reader(artifact_id) as reader:
|
||||
reader.read()
|
||||
|
||||
self.assertEqual(len(get_all_study_summaries(storage)), 0)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
version.parse(optuna.__version__) < version.parse("3.4.0"),
|
||||
reason="Needs optuna.artifacts",
|
||||
)
|
||||
def test_delete_study_without_removing_artifacts(self) -> None:
|
||||
from optuna.artifacts import upload_artifact
|
||||
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
with tempfile.TemporaryDirectory() as tmpdir_name:
|
||||
artifact_store = optuna.artifacts.FileSystemArtifactStore(base_path=tmpdir_name)
|
||||
with tempfile.NamedTemporaryFile() as f:
|
||||
f.write(b"dummy")
|
||||
f.flush()
|
||||
artifact_id = upload_artifact(study, f.name, artifact_store)
|
||||
|
||||
app = create_app(storage, artifact_store)
|
||||
|
||||
with artifact_store.open_reader(artifact_id) as reader:
|
||||
self.assertEqual(reader.read(), b"dummy")
|
||||
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/studies/{study._study_id}",
|
||||
"DELETE",
|
||||
body=json.dumps({"remove_associated_artifacts": False}),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 204)
|
||||
|
||||
with artifact_store.open_reader(artifact_id) as reader:
|
||||
self.assertEqual(reader.read(), b"dummy")
|
||||
|
||||
self.assertEqual(len(get_all_study_summaries(storage)), 0)
|
||||
|
||||
def test_delete_study_not_found(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
app = create_app(storage)
|
||||
|
||||
Generated
+971
-1027
File diff suppressed because it is too large
Load Diff
@@ -5,12 +5,13 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"watch": "vite",
|
||||
"build:vscode": "webpack"
|
||||
"build:vscode": "webpack",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@optuna/types": "../tslib/types",
|
||||
"@types/plotly.js": "^2.29.2",
|
||||
"@types/react": "^18.2.64",
|
||||
"@types/react": "file:../tslib/react/node_modules/@types/react",
|
||||
"@types/react-dom": "^18.2.21",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
@@ -21,18 +22,19 @@
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.12",
|
||||
"@mui/lab": "^5.0.0-alpha.167",
|
||||
"@mui/material": "^5.15.12",
|
||||
"@emotion/react": "file:../tslib/react/node_modules/@emotion/react",
|
||||
"@emotion/styled": "file:../tslib/react/node_modules/@emotion/styled",
|
||||
"@mui/icons-material": "file:../tslib/react/node_modules/@mui/icons-material",
|
||||
"@mui/lab": "file:../tslib/react/node_modules/@mui/lab",
|
||||
"@mui/material": "file:../tslib/react/node_modules/@mui/material",
|
||||
"@mui/system": "file:../tslib/react/node_modules/@mui/system",
|
||||
"@optuna/react": "../tslib/react",
|
||||
"@optuna/storage": "../tslib/storage",
|
||||
"module-workers-polyfill": "^0.3.2",
|
||||
"notistack": "^3.0.1",
|
||||
"optuna": "../rustlib/pkg",
|
||||
"plotly.js-dist-min": "^2.30.0",
|
||||
"react": "^18.2.0",
|
||||
"react": "file:../tslib/react/node_modules/react",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { JournalFileStorage } from "@optuna/storage"
|
||||
import { SQLite3Storage } from "@optuna/storage"
|
||||
import type { OptunaStorage } from "@optuna/storage"
|
||||
import React, { FC, createContext, useState } from "react"
|
||||
|
||||
export const StorageContext = createContext<{
|
||||
|
||||
@@ -33,7 +33,7 @@ export const StudyDetail: FC<{
|
||||
const idxNumber = parseInt(idx || "", 10)
|
||||
|
||||
const { storage } = useContext(StorageContext)
|
||||
const [study, setStudy] = useState<Study | null>(null)
|
||||
const [study, setStudy] = useState<Optuna.Study | null>(null)
|
||||
useEffect(() => {
|
||||
const fetchStudy = async () => {
|
||||
if (storage === null) {
|
||||
@@ -169,7 +169,7 @@ export const StudyDetail: FC<{
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{study?.study_name || "Not Found"}
|
||||
{study?.name || "Not Found"}
|
||||
</Typography>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import { styled } from "@mui/system"
|
||||
import * as Optuna from "@optuna/types"
|
||||
import React, {
|
||||
FC,
|
||||
useEffect,
|
||||
@@ -36,7 +37,7 @@ export const StudyList: FC<{
|
||||
}> = ({ toggleColorMode }) => {
|
||||
const theme = useTheme()
|
||||
const { storage } = useContext(StorageContext)
|
||||
const [studies, setStudies] = useState<StudySummary[]>([])
|
||||
const [studies, setStudies] = useState<Optuna.StudySummary[]>([])
|
||||
|
||||
const [_studyFilterText, setStudyFilterText] = useState<string>("")
|
||||
const [sortBy, setSortBy] = useState<"id-asc" | "id-desc">("id-asc")
|
||||
@@ -52,16 +53,18 @@ export const StudyList: FC<{
|
||||
fetchStudies()
|
||||
}, [storage])
|
||||
const filteredStudies = useMemo(() => {
|
||||
const studyFilter = (row: StudySummary): boolean => {
|
||||
const studyFilter = (row: Optuna.StudySummary): boolean => {
|
||||
const keywords = studyFilterText.split(" ")
|
||||
return !keywords.every((k) => {
|
||||
if (k === "") {
|
||||
return true
|
||||
}
|
||||
return row.study_name.indexOf(k) >= 0
|
||||
return row.name.indexOf(k) >= 0
|
||||
})
|
||||
}
|
||||
let filteredStudies: StudySummary[] = studies.filter((s) => !studyFilter(s))
|
||||
let filteredStudies: Optuna.StudySummary[] = studies.filter(
|
||||
(s) => !studyFilter(s)
|
||||
)
|
||||
if (sortBy === "id-desc") {
|
||||
filteredStudies = filteredStudies.reverse()
|
||||
}
|
||||
@@ -178,13 +181,13 @@ export const StudyList: FC<{
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap" }}>
|
||||
{filteredStudies.map((study, idx) => (
|
||||
<Card
|
||||
key={study.study_id}
|
||||
key={study.id}
|
||||
sx={{ margin: theme.spacing(2), width: "500px" }}
|
||||
>
|
||||
<CardActionArea component={Link} to={`/${idx}`}>
|
||||
<CardContent>
|
||||
<Typography variant="h5" sx={{ wordBreak: "break-all" }}>
|
||||
{study.study_id}. {study.study_name}
|
||||
{study.id}. {study.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
|
||||
Vendored
-98
@@ -1,99 +1 @@
|
||||
declare const IS_VSCODE: boolean
|
||||
|
||||
type TrialState = "Running" | "Complete" | "Pruned" | "Fail" | "Waiting"
|
||||
type TrialStateFinished = "Complete" | "Fail" | "Pruned"
|
||||
type StudyDirection = "maximize" | "minimize"
|
||||
|
||||
type OptunaStorage = {
|
||||
getStudies: () => Promise<StudySummary[]>
|
||||
getStudy: (idx: number) => Promise<Study | null>
|
||||
}
|
||||
|
||||
type FloatDistribution = {
|
||||
type: "FloatDistribution"
|
||||
low: number
|
||||
high: number
|
||||
step: number | null
|
||||
log: boolean
|
||||
}
|
||||
|
||||
type IntDistribution = {
|
||||
type: "IntDistribution"
|
||||
low: number
|
||||
high: number
|
||||
step: number | null
|
||||
log: boolean
|
||||
}
|
||||
|
||||
type CategoricalChoiceType = null | boolean | number | string
|
||||
type CategoricalDistribution = {
|
||||
type: "CategoricalDistribution"
|
||||
choices: CategoricalChoiceType[]
|
||||
}
|
||||
|
||||
type TrialIntermediateValue = {
|
||||
step: number
|
||||
value: number
|
||||
}
|
||||
|
||||
type Distribution =
|
||||
| FloatDistribution
|
||||
| IntDistribution
|
||||
| CategoricalDistribution
|
||||
|
||||
type Attribute = {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
type AttributeSpec = {
|
||||
key: string
|
||||
sortable: boolean
|
||||
}
|
||||
|
||||
type StudySummary = {
|
||||
study_id: number
|
||||
study_name: string
|
||||
directions: StudyDirection[]
|
||||
}
|
||||
|
||||
type Study = {
|
||||
study_id: number
|
||||
study_name: string
|
||||
directions: StudyDirection[]
|
||||
union_search_space: SearchSpaceItem[]
|
||||
intersection_search_space: SearchSpaceItem[]
|
||||
union_user_attrs: AttributeSpec[]
|
||||
datetime_start?: Date
|
||||
trials: Trial[]
|
||||
}
|
||||
|
||||
type Trial = {
|
||||
trial_id: number
|
||||
number: number
|
||||
study_id: number
|
||||
state: TrialState
|
||||
values?: number[]
|
||||
params: TrialParam[]
|
||||
intermediate_values: TrialIntermediateValue[]
|
||||
user_attrs: Attribute[]
|
||||
datetime_start?: Date
|
||||
datetime_complete?: Date
|
||||
}
|
||||
|
||||
type TrialParam = {
|
||||
name: string
|
||||
param_internal_value: number
|
||||
param_external_value: CategoricalChoiceType
|
||||
param_external_type: string
|
||||
distribution: Distribution
|
||||
}
|
||||
|
||||
type SearchSpaceItem = {
|
||||
name: string
|
||||
}
|
||||
|
||||
type ParamImportance = {
|
||||
name: string
|
||||
importance: number
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ lerna-debug.log*
|
||||
node_modules
|
||||
pkg
|
||||
types
|
||||
!src/types
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
Generated
+2395
-1554
File diff suppressed because it is too large
Load Diff
@@ -29,16 +29,18 @@
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.10",
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "^5.15.10",
|
||||
"@mui/system": "^5.15.9",
|
||||
"@optuna/storage": "../storage/",
|
||||
"@optuna/storage": "file:../storage",
|
||||
"@tanstack/react-table": "^8.17.3",
|
||||
"plotly.js-dist-min": "^2.30.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"recoil": "^0.7.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@optuna/types": "../types/",
|
||||
"@optuna/types": "file:../types",
|
||||
"@storybook/addon-essentials": "^8.0.4",
|
||||
"@storybook/addon-interactions": "^8.0.4",
|
||||
"@storybook/addon-links": "^8.0.4",
|
||||
|
||||
@@ -28,7 +28,7 @@ const useMockStudies = () => {
|
||||
|
||||
export const useMockStudy = (studyId: number | undefined) => {
|
||||
const mockStudies = useMockStudies()
|
||||
const mockStudy = mockStudies.find((study) => study.study_id === studyId)
|
||||
const mockStudy = mockStudies.find((study) => study.id === studyId)
|
||||
const mockImportance: Optuna.ParamImportance[][] = [
|
||||
[
|
||||
{ name: "dropout_l0", importance: 0.07990265450296263 },
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { Clear } from "@mui/icons-material"
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"
|
||||
import CheckBoxIcon from "@mui/icons-material/CheckBox"
|
||||
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"
|
||||
import FilterListIcon from "@mui/icons-material/FilterList"
|
||||
import FirstPageIcon from "@mui/icons-material/FirstPage"
|
||||
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"
|
||||
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"
|
||||
import LastPageIcon from "@mui/icons-material/LastPage"
|
||||
import {
|
||||
Collapse,
|
||||
Box,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -12,372 +18,365 @@ import {
|
||||
TablePagination,
|
||||
TableRow,
|
||||
TableSortLabel,
|
||||
TextField,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import ListItemIcon from "@mui/material/ListItemIcon"
|
||||
import Paper from "@mui/material/Paper"
|
||||
import { TablePaginationActionsProps } from "@mui/material/TablePagination/TablePaginationActions"
|
||||
import { styled } from "@mui/system"
|
||||
import React from "react"
|
||||
|
||||
type Order = "asc" | "desc"
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
Header,
|
||||
PaginationState,
|
||||
SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
type Value = any
|
||||
const TableHeaderCellSpan = styled("span")({
|
||||
display: "inline-flex",
|
||||
})
|
||||
|
||||
const defaultRowsPerPageOption = [10, 50, 100, { label: "All", value: -1 }]
|
||||
const HiddenSpan = styled("span")({
|
||||
border: 0,
|
||||
clip: "rect(0 0 0 0)",
|
||||
height: 1,
|
||||
margin: -1,
|
||||
overflow: "hidden",
|
||||
padding: 0,
|
||||
position: "absolute",
|
||||
top: 20,
|
||||
width: 1,
|
||||
})
|
||||
|
||||
interface DataGridColumn<T> {
|
||||
field: keyof T
|
||||
label: string
|
||||
sortable?: boolean
|
||||
less?: (a: T, b: T, ascending: boolean) => number
|
||||
filterable?: boolean
|
||||
toCellValue?: (rowIndex: number) => string | React.ReactNode
|
||||
padding?: "normal" | "checkbox" | "none"
|
||||
}
|
||||
|
||||
interface RowFilter {
|
||||
columnIdx: number
|
||||
value: Value
|
||||
}
|
||||
|
||||
function DataGrid<T>(props: {
|
||||
columns: DataGridColumn<T>[]
|
||||
rows: T[]
|
||||
keyField: keyof T
|
||||
dense?: boolean
|
||||
collapseBody?: (rowIndex: number) => React.ReactNode
|
||||
initialRowsPerPage?: number
|
||||
rowsPerPageOption?: Array<number | { value: number; label: string }>
|
||||
defaultFilter?: (row: T) => boolean
|
||||
function FilterMenu<T>({
|
||||
header,
|
||||
filterChoices,
|
||||
}: {
|
||||
header: Header<T, unknown>
|
||||
filterChoices: string[]
|
||||
}): React.ReactElement {
|
||||
const { columns, rows, keyField, dense, collapseBody, defaultFilter } = props
|
||||
let { initialRowsPerPage, rowsPerPageOption } = props
|
||||
const [order, setOrder] = React.useState<Order>("asc")
|
||||
const [orderBy, setOrderBy] = React.useState<number>(0) // index of columns
|
||||
const [page, setPage] = React.useState(0)
|
||||
const [filters, setFilters] = React.useState<RowFilter[]>([])
|
||||
|
||||
const getRowIndex = (row: T): number => {
|
||||
return rows.findIndex((row2) => row[keyField] === row2[keyField])
|
||||
}
|
||||
|
||||
// Pagination
|
||||
rowsPerPageOption = rowsPerPageOption || defaultRowsPerPageOption
|
||||
initialRowsPerPage = initialRowsPerPage // use first element as default
|
||||
? initialRowsPerPage
|
||||
: isNumber(rowsPerPageOption[0])
|
||||
? rowsPerPageOption[0]
|
||||
: rowsPerPageOption[0].value
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(initialRowsPerPage)
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => {
|
||||
setPage(newPage)
|
||||
}
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10))
|
||||
setPage(0)
|
||||
}
|
||||
|
||||
// Filtering
|
||||
const fieldAlreadyFiltered = (columnIdx: number): boolean =>
|
||||
filters.some((f) => f.columnIdx === columnIdx)
|
||||
|
||||
const handleClickFilterCell = (columnIdx: number, value: Value) => {
|
||||
if (fieldAlreadyFiltered(columnIdx)) {
|
||||
return
|
||||
}
|
||||
const newFilters = [...filters, { columnIdx: columnIdx, value: value }]
|
||||
setFilters(newFilters)
|
||||
}
|
||||
|
||||
const clearFilter = (columnIdx: number): void => {
|
||||
setFilters(filters.filter((f) => f.columnIdx !== columnIdx))
|
||||
}
|
||||
|
||||
const filteredRows = rows.filter((row, rowIdx) => {
|
||||
if (defaultFilter?.(row)) {
|
||||
return false
|
||||
}
|
||||
return filters.length === 0
|
||||
? true
|
||||
: filters.some((f) => {
|
||||
if (columns.length <= f.columnIdx) {
|
||||
console.log(
|
||||
`columnIdx=${f.columnIdx} must be smaller than columns.length=${columns.length}`
|
||||
)
|
||||
return true
|
||||
}
|
||||
const toCellValue = columns[f.columnIdx].toCellValue
|
||||
if (toCellValue !== undefined) {
|
||||
return toCellValue(rowIdx) === f.value
|
||||
}
|
||||
const field = columns[f.columnIdx].field
|
||||
return row[field] === f.value
|
||||
})
|
||||
})
|
||||
|
||||
// Sorting
|
||||
const createSortHandler = (columnId: number) => () => {
|
||||
const isAsc = orderBy === columnId && order === "asc"
|
||||
setOrder(isAsc ? "desc" : "asc")
|
||||
setOrderBy(columnId)
|
||||
}
|
||||
const sortedRows = stableSort<T>(filteredRows, order, orderBy, columns)
|
||||
const currentPageRows =
|
||||
rowsPerPage > 0
|
||||
? sortedRows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
: sortedRows
|
||||
const emptyRows =
|
||||
rowsPerPage - Math.min(rowsPerPage, sortedRows.length - page * rowsPerPage)
|
||||
|
||||
const RootDiv = styled("div")({
|
||||
width: "100%",
|
||||
})
|
||||
const HiddenSpan = styled("span")({
|
||||
border: 0,
|
||||
clip: "rect(0 0 0 0)",
|
||||
height: 1,
|
||||
margin: -1,
|
||||
overflow: "hidden",
|
||||
padding: 0,
|
||||
position: "absolute",
|
||||
top: 20,
|
||||
width: 1,
|
||||
})
|
||||
const TableHeaderCellSpan = styled("span")({
|
||||
display: "inline-flex",
|
||||
})
|
||||
const [filterMenuAnchorEl, setFilterMenuAnchorEl] =
|
||||
React.useState<null | HTMLElement>(null)
|
||||
return (
|
||||
<RootDiv>
|
||||
<TableContainer>
|
||||
<Table
|
||||
aria-labelledby="tableTitle"
|
||||
size={dense ? "small" : "medium"}
|
||||
aria-label="data grid"
|
||||
>
|
||||
<>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
setFilterMenuAnchorEl(e.currentTarget)
|
||||
}}
|
||||
>
|
||||
<FilterListIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={filterMenuAnchorEl}
|
||||
open={filterMenuAnchorEl !== null}
|
||||
onClose={() => {
|
||||
setFilterMenuAnchorEl(null)
|
||||
}}
|
||||
>
|
||||
{filterChoices.map((choice) => (
|
||||
<MenuItem
|
||||
key={choice}
|
||||
onClick={() => {
|
||||
const skippedValues = header.column.getFilterValue() as string[]
|
||||
const isSkipped = skippedValues.includes(choice)
|
||||
const newSkippedValues = isSkipped
|
||||
? skippedValues.filter((v) => v !== choice)
|
||||
: skippedValues.concat(choice)
|
||||
header.column.setFilterValue(newSkippedValues)
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
{header.column.getFilterValue() !== undefined ? (
|
||||
(header.column.getFilterValue() as string[]).includes(
|
||||
choice
|
||||
) ? (
|
||||
<CheckBoxOutlineBlankIcon color="primary" />
|
||||
) : (
|
||||
<CheckBoxIcon color="primary" />
|
||||
)
|
||||
) : null}
|
||||
</ListItemIcon>
|
||||
{choice ?? "(missing value)"}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DataGrid<T>({
|
||||
data,
|
||||
columns,
|
||||
initialRowsPerPage,
|
||||
}: {
|
||||
data: T[]
|
||||
columns: ColumnDef<T>[]
|
||||
initialRowsPerPage?: number
|
||||
}): React.ReactElement {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
const rowsPerPageOptions = [10, 50, 100, { label: "All", value: data.length }]
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize:
|
||||
initialRowsPerPage && rowsPerPageOptions.includes(initialRowsPerPage)
|
||||
? initialRowsPerPage
|
||||
: 50,
|
||||
})
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
columnFilters,
|
||||
sorting,
|
||||
pagination,
|
||||
},
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onSortingChange: setSorting,
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
autoResetPageIndex: false,
|
||||
})
|
||||
|
||||
return (
|
||||
<Box component="div" sx={{ width: "100%" }}>
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{collapseBody ? <TableCell /> : null}
|
||||
{columns.map((column, columnIdx) => (
|
||||
<TableCell
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
key={columnIdx}
|
||||
padding={column.padding || "normal"}
|
||||
sortDirection={orderBy === column.field ? order : false}
|
||||
>
|
||||
<TableHeaderCellSpan>
|
||||
{column.sortable ? (
|
||||
<TableSortLabel
|
||||
active={orderBy === columnIdx}
|
||||
direction={orderBy === columnIdx ? order : "asc"}
|
||||
onClick={createSortHandler(columnIdx)}
|
||||
>
|
||||
{column.label}
|
||||
{orderBy === column.field ? (
|
||||
<HiddenSpan>
|
||||
{order === "desc"
|
||||
? "sorted descending"
|
||||
: "sorted ascending"}
|
||||
</HiddenSpan>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
) : (
|
||||
column.label
|
||||
)}
|
||||
{column.filterable ? (
|
||||
<IconButton
|
||||
size={dense ? "small" : "medium"}
|
||||
style={
|
||||
fieldAlreadyFiltered(columnIdx)
|
||||
? {}
|
||||
: { visibility: "hidden" }
|
||||
}
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
clearFilter(columnIdx)
|
||||
}}
|
||||
>
|
||||
<Clear />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</TableHeaderCellSpan>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
if (
|
||||
header.column.getCanFilter() &&
|
||||
!header.column.getIsFiltered()
|
||||
) {
|
||||
header.column.setFilterValue([])
|
||||
}
|
||||
const order = header.column.getIsSorted()
|
||||
const filterChoices = header.column.getCanFilter()
|
||||
? Array.from(
|
||||
header.column.getFacetedUniqueValues().keys()
|
||||
).sort()
|
||||
: null
|
||||
return (
|
||||
<TableCell key={header.id} colSpan={header.colSpan}>
|
||||
{header.isPlaceholder ? null : (
|
||||
<TableHeaderCellSpan>
|
||||
{header.column.getCanSort() ? (
|
||||
<TableSortLabel
|
||||
active={order !== false}
|
||||
direction={order || "asc"}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{order !== null ? (
|
||||
<HiddenSpan>
|
||||
{order === "desc"
|
||||
? "sorted descending"
|
||||
: "sorted ascending"}
|
||||
</HiddenSpan>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
) : (
|
||||
flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)
|
||||
)}
|
||||
{filterChoices !== null ? (
|
||||
<FilterMenu
|
||||
header={header}
|
||||
filterChoices={filterChoices}
|
||||
/>
|
||||
) : null}
|
||||
</TableHeaderCellSpan>
|
||||
)}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{currentPageRows.map((row) => (
|
||||
<DataGridRow<T>
|
||||
columns={columns}
|
||||
rowIndex={getRowIndex(row)}
|
||||
row={row}
|
||||
keyField={keyField}
|
||||
collapseBody={collapseBody}
|
||||
key={`${row[keyField]}`}
|
||||
handleClickFilterCell={handleClickFilterCell}
|
||||
/>
|
||||
))}
|
||||
{emptyRows > 0 && (
|
||||
<TableRow style={{ height: (dense ? 33 : 53) * emptyRows }}>
|
||||
<TableCell colSpan={6} />
|
||||
</TableRow>
|
||||
)}
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
return (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={rowsPerPageOption}
|
||||
component="div"
|
||||
count={filteredRows.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
</RootDiv>
|
||||
)
|
||||
}
|
||||
|
||||
function DataGridRow<T>(props: {
|
||||
columns: DataGridColumn<T>[]
|
||||
rowIndex: number
|
||||
row: T
|
||||
keyField: keyof T
|
||||
collapseBody?: (rowIndex: number) => React.ReactNode
|
||||
handleClickFilterCell: (columnIdx: number, value: Value) => void
|
||||
}) {
|
||||
const {
|
||||
columns,
|
||||
rowIndex,
|
||||
row,
|
||||
keyField,
|
||||
collapseBody,
|
||||
handleClickFilterCell,
|
||||
} = props
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const theme = useTheme()
|
||||
|
||||
const FilterableDiv = styled("div")({
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
})
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TableRow hover tabIndex={-1}>
|
||||
{collapseBody ? (
|
||||
<TableCell>
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<Box component="div" display="flex" alignItems="center">
|
||||
<TablePagination
|
||||
rowsPerPageOptions={rowsPerPageOptions}
|
||||
component="div"
|
||||
count={table.getFilteredRowModel().rows.length}
|
||||
rowsPerPage={table.getState().pagination.pageSize}
|
||||
page={table.getState().pagination.pageIndex}
|
||||
slotProps={{
|
||||
select: {
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true,
|
||||
},
|
||||
}}
|
||||
onPageChange={(_, page) => {
|
||||
table.setPageIndex(page)
|
||||
}}
|
||||
onRowsPerPageChange={(e) => {
|
||||
const size = e.target.value ? Number(e.target.value) : 10
|
||||
table.setPageSize(size)
|
||||
}}
|
||||
ActionsComponent={TablePaginationActions}
|
||||
/>
|
||||
{table.getPageCount() > 2 ? (
|
||||
<PaginationForm1
|
||||
onPageNumberSubmit={(page) => table.setPageIndex(page)}
|
||||
maxPageNumber={table.getPageCount()}
|
||||
/>
|
||||
) : null}
|
||||
{columns.map((column, columnIndex) => {
|
||||
const cellItem = column.toCellValue
|
||||
? column.toCellValue(rowIndex)
|
||||
: // TODO(c-bata): Avoid this implicit type conversion.
|
||||
(row[column.field] as number | string | null | undefined)
|
||||
|
||||
return column.filterable ? (
|
||||
<TableCell
|
||||
key={`${row[keyField]}:${column.field.toString()}:${columnIndex}`}
|
||||
padding={column.padding || "normal"}
|
||||
onClick={() => {
|
||||
const value =
|
||||
column.toCellValue !== undefined
|
||||
? column.toCellValue(rowIndex)
|
||||
: row[column.field]
|
||||
handleClickFilterCell(columnIndex, value)
|
||||
}}
|
||||
>
|
||||
<FilterableDiv>{cellItem}</FilterableDiv>
|
||||
</TableCell>
|
||||
) : (
|
||||
<TableCell
|
||||
key={`${row[keyField]}:${column.field.toString()}:${columnIndex}`}
|
||||
padding={column.padding || "normal"}
|
||||
>
|
||||
{cellItem}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
{collapseBody ? (
|
||||
<TableRow>
|
||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
{collapseBody(rowIndex)}
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function getComparator<T>(
|
||||
order: Order,
|
||||
columns: DataGridColumn<T>[],
|
||||
orderBy: number
|
||||
): (a: T, b: T) => number {
|
||||
return order === "desc"
|
||||
? (a, b) => descendingComparator<T>(a, b, columns, orderBy)
|
||||
: (a, b) => -descendingComparator<T>(a, b, columns, orderBy)
|
||||
}
|
||||
|
||||
function descendingComparator<T>(
|
||||
a: T,
|
||||
b: T,
|
||||
columns: DataGridColumn<T>[],
|
||||
orderBy: number
|
||||
): number {
|
||||
const field = columns[orderBy].field
|
||||
if (b[field] < a[field]) {
|
||||
return -1
|
||||
const TablePaginationActions = ({
|
||||
count,
|
||||
page,
|
||||
rowsPerPage,
|
||||
onPageChange,
|
||||
}: TablePaginationActionsProps) => {
|
||||
const theme = useTheme()
|
||||
const handleFirstPageButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
onPageChange(event, 0)
|
||||
}
|
||||
if (b[field] > a[field]) {
|
||||
return 1
|
||||
|
||||
const handleBackButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
onPageChange(event, page - 1)
|
||||
}
|
||||
return 0
|
||||
|
||||
const handleNextButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
onPageChange(event, page + 1)
|
||||
}
|
||||
|
||||
const handleLastPageButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1))
|
||||
}
|
||||
|
||||
return (
|
||||
<Box component="div" sx={{ flexShrink: 0, ml: 2.5 }}>
|
||||
<IconButton
|
||||
onClick={handleFirstPageButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
>
|
||||
{theme.direction === "rtl" ? <LastPageIcon /> : <FirstPageIcon />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="previous page"
|
||||
>
|
||||
{theme.direction === "rtl" ? (
|
||||
<KeyboardArrowRight />
|
||||
) : (
|
||||
<KeyboardArrowLeft />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleNextButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="next page"
|
||||
>
|
||||
{theme.direction === "rtl" ? (
|
||||
<KeyboardArrowLeft />
|
||||
) : (
|
||||
<KeyboardArrowRight />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
>
|
||||
{theme.direction === "rtl" ? <FirstPageIcon /> : <LastPageIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function stableSort<T>(
|
||||
array: T[],
|
||||
order: Order,
|
||||
orderBy: number,
|
||||
columns: DataGridColumn<T>[]
|
||||
) {
|
||||
// TODO(c-bata): Refactor here by implementing as the same comparator interface.
|
||||
const less = columns[orderBy].less
|
||||
const comparator = getComparator(order, columns, orderBy)
|
||||
const stabilizedThis = array.map((el, index) => [el, index] as [T, number])
|
||||
stabilizedThis.sort((a, b) => {
|
||||
if (less) {
|
||||
const ascending = order === "asc"
|
||||
const result = ascending
|
||||
? -less(a[0], b[0], ascending)
|
||||
: less(a[0], b[0], ascending)
|
||||
if (result !== 0) return result
|
||||
} else {
|
||||
const result = comparator(a[0], b[0])
|
||||
if (result !== 0) return result
|
||||
}
|
||||
return a[1] - b[1]
|
||||
})
|
||||
return stabilizedThis.map((el) => el[0])
|
||||
}
|
||||
const PaginationForm1: React.FC<{
|
||||
onPageNumberSubmit: (value: number) => void
|
||||
maxPageNumber: number
|
||||
}> = ({ onPageNumberSubmit, maxPageNumber }) => {
|
||||
// This component is separated from DataGrid to prevent `DataGrid` from re-rendering the page,
|
||||
// every time any letters are input.
|
||||
const [specifiedPageText, setSpecifiedPageText] = React.useState("")
|
||||
|
||||
const isNumber = (
|
||||
rowsPerPage: number | { value: number; label: string }
|
||||
): rowsPerPage is number => {
|
||||
return typeof rowsPerPage === "number"
|
||||
const handleSubmitPageNumber = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
const newPageNumber = parseInt(specifiedPageText, 10)
|
||||
// Page is 0-indexed in `TablePagination`.
|
||||
onPageNumberSubmit(newPageNumber - 1)
|
||||
setSpecifiedPageText("") // reset the input field
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmitPageNumber}>
|
||||
<TextField
|
||||
size="small"
|
||||
label={`Go to Page: n / ${maxPageNumber}`}
|
||||
value={specifiedPageText}
|
||||
type="number"
|
||||
style={{ width: 200 }}
|
||||
inputProps={{ min: 1, max: maxPageNumber }}
|
||||
onChange={(e) => {
|
||||
setSpecifiedPageText(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export { DataGrid }
|
||||
export type { DataGridColumn }
|
||||
|
||||
+4
-9
@@ -1,14 +1,11 @@
|
||||
import { Box, Typography, useTheme } from "@mui/material"
|
||||
import React from "react"
|
||||
import { GraphComponentState } from "ts/types/optuna"
|
||||
import { FC } from "react"
|
||||
import { GraphComponentState } from "../types"
|
||||
|
||||
function GraphContainer({
|
||||
plotDomId,
|
||||
graphComponentState,
|
||||
}: {
|
||||
export const GraphContainer: FC<{
|
||||
plotDomId: string
|
||||
graphComponentState: GraphComponentState
|
||||
}) {
|
||||
}> = ({ plotDomId, graphComponentState }) => {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<Box component="div" id={plotDomId} sx={{ height: "450px" }}>
|
||||
@@ -28,5 +25,3 @@ function GraphContainer({
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default GraphContainer
|
||||
@@ -0,0 +1,38 @@
|
||||
import { CssBaseline, ThemeProvider } from "@mui/material"
|
||||
import { Meta, StoryObj } from "@storybook/react"
|
||||
import React from "react"
|
||||
import { useMockStudy } from "../MockStudies"
|
||||
import { lightTheme } from "../styles/lightTheme"
|
||||
import { PlotEdf } from "./PlotEdf"
|
||||
|
||||
const meta: Meta<typeof PlotEdf> = {
|
||||
component: PlotEdf,
|
||||
title: "PlotEdf",
|
||||
tags: ["autodocs"],
|
||||
decorators: [
|
||||
(Story, storyContext) => {
|
||||
const { study } = useMockStudy(storyContext.parameters?.studyId)
|
||||
if (!study) return <p>loading...</p>
|
||||
return (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<CssBaseline />
|
||||
<Story
|
||||
args={{
|
||||
studies: [study],
|
||||
objectiveId: 0,
|
||||
}}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
)
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof PlotEdf>
|
||||
|
||||
export const MockStudyExample1: Story = {
|
||||
parameters: {
|
||||
studyId: 1,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Box, Typography, useTheme } from "@mui/material"
|
||||
import * as Optuna from "@optuna/types"
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import { FC, useEffect, useMemo } from "react"
|
||||
import { useGraphComponentState } from "../hooks/useGraphComponentState"
|
||||
import { Target, useFilteredTrialsFromStudies } from "../utils/trialFilter"
|
||||
import { GraphContainer } from "./GraphContainer"
|
||||
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
|
||||
|
||||
export type EdfPlotInfo = {
|
||||
study_name: string
|
||||
trials: Optuna.Trial[]
|
||||
}
|
||||
|
||||
const getPlotDomId = (objectiveId: number) => `plot-edf-${objectiveId}`
|
||||
|
||||
export const PlotEdf: FC<{
|
||||
studies: Optuna.Study[]
|
||||
objectiveId: number
|
||||
}> = ({ studies, objectiveId }) => {
|
||||
const { graphComponentState, notifyGraphDidRender } = useGraphComponentState()
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
const domId = getPlotDomId(objectiveId)
|
||||
const target = useMemo<Target>(
|
||||
() => new Target("objective", objectiveId),
|
||||
[objectiveId]
|
||||
)
|
||||
const trials = useFilteredTrialsFromStudies(studies, [target], false)
|
||||
const edfPlotInfos = studies.map((study, index) => {
|
||||
const e: EdfPlotInfo = {
|
||||
study_name: study?.name,
|
||||
trials: trials[index],
|
||||
}
|
||||
return e
|
||||
})
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
useEffect(() => {
|
||||
if (graphComponentState !== "componentWillMount") {
|
||||
plotEdf(edfPlotInfos, target, domId, theme.palette.mode)?.then(
|
||||
notifyGraphDidRender
|
||||
)
|
||||
}
|
||||
}, [studies, target, theme.palette.mode, graphComponentState])
|
||||
|
||||
return (
|
||||
<Box component="div">
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ margin: "1em 0", fontWeight: theme.typography.fontWeightBold }}
|
||||
>
|
||||
{`EDF for ${target.toLabel(studies[0].metric_names)}`}
|
||||
</Typography>
|
||||
<GraphContainer
|
||||
plotDomId={domId}
|
||||
graphComponentState={graphComponentState}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const plotEdf = (
|
||||
edfPlotInfos: EdfPlotInfo[],
|
||||
target: Target,
|
||||
domId: string,
|
||||
mode: string
|
||||
) => {
|
||||
if (document.getElementById(domId) === null) {
|
||||
return
|
||||
}
|
||||
if (edfPlotInfos.length === 0) {
|
||||
return plotly.react(domId, [], {
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
})
|
||||
}
|
||||
|
||||
const target_name = "Objective Value"
|
||||
const layout: Partial<plotly.Layout> = {
|
||||
xaxis: {
|
||||
title: target_name,
|
||||
},
|
||||
yaxis: {
|
||||
title: "Cumulative Probability",
|
||||
},
|
||||
margin: {
|
||||
l: 50,
|
||||
t: 0,
|
||||
r: 50,
|
||||
b: 50,
|
||||
},
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
legend: {
|
||||
x: 1.0,
|
||||
y: 0.95,
|
||||
},
|
||||
}
|
||||
|
||||
const plotData: Partial<plotly.PlotData>[] = edfPlotInfos.map((h) => {
|
||||
const values = h.trials.map((t) => target.getTargetValue(t) as number)
|
||||
const numValues = values.length
|
||||
const minX = Math.min(...values)
|
||||
const maxX = Math.max(...values)
|
||||
const numStep = 100
|
||||
const _step = (maxX - minX) / (numStep - 1)
|
||||
|
||||
const xValues = []
|
||||
const yValues = []
|
||||
for (let i = 0; i < numStep; i++) {
|
||||
const boundary_right = minX + _step * i
|
||||
xValues.push(boundary_right)
|
||||
yValues.push(values.filter((v) => v <= boundary_right).length / numValues)
|
||||
}
|
||||
|
||||
return {
|
||||
type: "scatter",
|
||||
name: `${h.study_name}`,
|
||||
x: xValues,
|
||||
y: yValues,
|
||||
}
|
||||
})
|
||||
return plotly.react(domId, plotData, layout)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { CssBaseline, ThemeProvider } from "@mui/material"
|
||||
import { Meta, StoryObj } from "@storybook/react"
|
||||
import React from "react"
|
||||
import { useMockStudy } from "../MockStudies"
|
||||
import { darkTheme } from "../styles/darkTheme"
|
||||
import { PlotEdf } from "./PlotEdf"
|
||||
|
||||
const meta: Meta<typeof PlotEdf> = {
|
||||
component: PlotEdf,
|
||||
title: "PlotEdfDark",
|
||||
tags: ["autodocs"],
|
||||
decorators: [
|
||||
(Story, storyContext) => {
|
||||
const { study } = useMockStudy(storyContext.parameters?.studyId)
|
||||
if (!study) return <p>loading...</p>
|
||||
return (
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<CssBaseline />
|
||||
<Story
|
||||
args={{
|
||||
studies: [study],
|
||||
objectiveId: 0,
|
||||
}}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
)
|
||||
},
|
||||
],
|
||||
parameters: {
|
||||
backgrounds: { default: "dark" },
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof PlotEdf>
|
||||
|
||||
export const MockStudyExample1: Story = {
|
||||
parameters: {
|
||||
studyId: 1,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { CssBaseline, ThemeProvider } from "@mui/material"
|
||||
import { Meta, StoryObj } from "@storybook/react"
|
||||
import React from "react"
|
||||
import { useMockStudy } from "../MockStudies"
|
||||
import { lightTheme } from "../styles/lightTheme"
|
||||
import { PlotSlice } from "./PlotSlice"
|
||||
|
||||
const meta: Meta<typeof PlotSlice> = {
|
||||
component: PlotSlice,
|
||||
title: "PlotSlice",
|
||||
tags: ["autodocs"],
|
||||
decorators: [
|
||||
(Story, storyContext) => {
|
||||
const { study } = useMockStudy(storyContext.parameters?.studyId)
|
||||
if (!study) return <p>loading...</p>
|
||||
return (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<CssBaseline />
|
||||
<Story
|
||||
args={{
|
||||
study,
|
||||
}}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
)
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof PlotSlice>
|
||||
|
||||
export const MockStudyExample1: Story = {
|
||||
parameters: {
|
||||
studyId: 1,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Grid,
|
||||
MenuItem,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
Switch,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import * as Optuna from "@optuna/types"
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import { FC, useEffect, useState } from "react"
|
||||
import { useGraphComponentState } from "../hooks/useGraphComponentState"
|
||||
import { useMergedUnionSearchSpace } from "../utils/searchSpace"
|
||||
import {
|
||||
Target,
|
||||
useFilteredTrials,
|
||||
useObjectiveAndUserAttrTargets,
|
||||
useParamTargets,
|
||||
} from "../utils/trialFilter"
|
||||
import { GraphContainer } from "./GraphContainer"
|
||||
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
|
||||
|
||||
const isLogScale = (s: Optuna.SearchSpaceItem): boolean => {
|
||||
if (s.distribution.type === "CategoricalDistribution") {
|
||||
return false
|
||||
}
|
||||
return s.distribution.log
|
||||
}
|
||||
|
||||
const domId = "plot-slice"
|
||||
|
||||
export const PlotSlice: FC<{
|
||||
study: Optuna.Study | null
|
||||
}> = ({ study = null }) => {
|
||||
const { graphComponentState, notifyGraphDidRender } = useGraphComponentState()
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
const [objectiveTargets, selectedObjective, setObjectiveTarget] =
|
||||
useObjectiveAndUserAttrTargets(study)
|
||||
const searchSpace = useMergedUnionSearchSpace(study?.union_search_space)
|
||||
const [paramTargets, selectedParamTarget, setParamTarget] =
|
||||
useParamTargets(searchSpace)
|
||||
const [logYScale, setLogYScale] = useState(false)
|
||||
|
||||
const trials = useFilteredTrials(
|
||||
study,
|
||||
selectedParamTarget !== null
|
||||
? [selectedObjective, selectedParamTarget]
|
||||
: [selectedObjective],
|
||||
false
|
||||
)
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
useEffect(() => {
|
||||
if (graphComponentState !== "componentWillMount") {
|
||||
plotSlice(
|
||||
trials,
|
||||
selectedObjective,
|
||||
selectedParamTarget,
|
||||
searchSpace.find((s) => s.name === selectedParamTarget?.key) || null,
|
||||
logYScale,
|
||||
theme.palette.mode
|
||||
)?.then(notifyGraphDidRender)
|
||||
}
|
||||
}, [
|
||||
trials,
|
||||
selectedObjective,
|
||||
searchSpace,
|
||||
selectedParamTarget,
|
||||
logYScale,
|
||||
theme.palette.mode,
|
||||
graphComponentState,
|
||||
])
|
||||
|
||||
const handleObjectiveChange = (event: SelectChangeEvent<string>) => {
|
||||
setObjectiveTarget(event.target.value)
|
||||
}
|
||||
|
||||
const handleSelectedParam = (e: SelectChangeEvent<string>) => {
|
||||
setParamTarget(e.target.value)
|
||||
}
|
||||
|
||||
const handleLogYScaleChange = () => {
|
||||
setLogYScale(!logYScale)
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container direction="row">
|
||||
<Grid
|
||||
item
|
||||
xs={3}
|
||||
container
|
||||
direction="column"
|
||||
sx={{ paddingRight: theme.spacing(2) }}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ margin: "1em 0", fontWeight: theme.typography.fontWeightBold }}
|
||||
>
|
||||
Slice
|
||||
</Typography>
|
||||
{objectiveTargets.length !== 1 && (
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Objective:</FormLabel>
|
||||
<Select
|
||||
value={selectedObjective.identifier()}
|
||||
onChange={handleObjectiveChange}
|
||||
>
|
||||
{objectiveTargets.map((t, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
<MenuItem value={t.identifier()} key={i}>
|
||||
{t.toLabel(study?.metric_names)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
{paramTargets.length !== 0 && selectedParamTarget !== null && (
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Parameter:</FormLabel>
|
||||
<Select
|
||||
value={selectedParamTarget.identifier()}
|
||||
onChange={handleSelectedParam}
|
||||
>
|
||||
{paramTargets.map((t, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
<MenuItem value={t.identifier()} key={i}>
|
||||
{t.toLabel()}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Log y scale:</FormLabel>
|
||||
<Switch
|
||||
checked={logYScale}
|
||||
onChange={handleLogYScaleChange}
|
||||
value="enable"
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<GraphContainer
|
||||
plotDomId={domId}
|
||||
graphComponentState={graphComponentState}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const plotSlice = (
|
||||
trials: Optuna.Trial[],
|
||||
objectiveTarget: Target,
|
||||
selectedParamTarget: Target | null,
|
||||
selectedParamSpace: Optuna.SearchSpaceItem | null,
|
||||
logYScale: boolean,
|
||||
mode: string
|
||||
) => {
|
||||
if (document.getElementById(domId) === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const layout: Partial<plotly.Layout> = {
|
||||
margin: {
|
||||
l: 50,
|
||||
t: 0,
|
||||
r: 50,
|
||||
b: 0,
|
||||
},
|
||||
xaxis: {
|
||||
title: selectedParamTarget?.toLabel() || "",
|
||||
type:
|
||||
selectedParamSpace !== null && isLogScale(selectedParamSpace)
|
||||
? "log"
|
||||
: "linear",
|
||||
gridwidth: 1,
|
||||
automargin: true,
|
||||
},
|
||||
yaxis: {
|
||||
title: "Objective Value",
|
||||
type: logYScale ? "log" : "linear",
|
||||
gridwidth: 1,
|
||||
automargin: true,
|
||||
},
|
||||
showlegend: false,
|
||||
uirevision: "true",
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
}
|
||||
if (
|
||||
selectedParamSpace === null ||
|
||||
selectedParamTarget === null ||
|
||||
trials.length === 0
|
||||
) {
|
||||
return plotly.react(domId, [], layout)
|
||||
}
|
||||
|
||||
const feasibleTrials: Optuna.Trial[] = []
|
||||
const infeasibleTrials: Optuna.Trial[] = []
|
||||
// biome-ignore lint/complexity/noForEach: <explanation>
|
||||
trials.forEach((t) => {
|
||||
if (t.constraints.every((c) => c <= 0)) {
|
||||
feasibleTrials.push(t)
|
||||
} else {
|
||||
infeasibleTrials.push(t)
|
||||
}
|
||||
})
|
||||
|
||||
const feasibleObjectiveValues: number[] = feasibleTrials.map(
|
||||
(t) => objectiveTarget.getTargetValue(t) as number
|
||||
)
|
||||
const infeasibleObjectiveValues: number[] = infeasibleTrials.map(
|
||||
(t) => objectiveTarget.getTargetValue(t) as number
|
||||
)
|
||||
|
||||
const feasibleValues = feasibleTrials.map(
|
||||
(t) => selectedParamTarget.getTargetValue(t) as number
|
||||
)
|
||||
const infeasibleValues = infeasibleTrials.map(
|
||||
(t) => selectedParamTarget.getTargetValue(t) as number
|
||||
)
|
||||
const trace: plotly.Data[] = [
|
||||
{
|
||||
type: "scatter",
|
||||
x: feasibleValues,
|
||||
y: feasibleObjectiveValues,
|
||||
mode: "markers",
|
||||
name: "Feasible Trial",
|
||||
marker: {
|
||||
color: feasibleTrials.map((t) => t.number),
|
||||
colorscale: "Blues",
|
||||
reversescale: true,
|
||||
colorbar: {
|
||||
title: "Trial",
|
||||
},
|
||||
line: {
|
||||
color: "Grey",
|
||||
width: 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "scatter",
|
||||
x: infeasibleValues,
|
||||
y: infeasibleObjectiveValues,
|
||||
mode: "markers",
|
||||
name: "Infeasible Trial",
|
||||
marker: {
|
||||
color: "#cccccc",
|
||||
reversescale: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
if (selectedParamSpace.distribution.type !== "CategoricalDistribution") {
|
||||
layout.xaxis = {
|
||||
title: selectedParamTarget.toLabel(),
|
||||
type: isLogScale(selectedParamSpace) ? "log" : "linear",
|
||||
gridwidth: 1,
|
||||
automargin: true, // Otherwise the label is outside of the plot
|
||||
}
|
||||
} else {
|
||||
const vocabArr = selectedParamSpace.distribution.choices.map(
|
||||
(c) => c?.toString() ?? "null"
|
||||
)
|
||||
const tickvals: number[] = vocabArr.map((_v, i) => i)
|
||||
layout.xaxis = {
|
||||
title: selectedParamTarget.toLabel(),
|
||||
type: "linear",
|
||||
gridwidth: 1,
|
||||
tickvals: tickvals,
|
||||
ticktext: vocabArr,
|
||||
automargin: true, // Otherwise the label is outside of the plot
|
||||
}
|
||||
}
|
||||
return plotly.react(domId, trace, layout)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { CssBaseline, ThemeProvider } from "@mui/material"
|
||||
import { Meta, StoryObj } from "@storybook/react"
|
||||
import React from "react"
|
||||
import { useMockStudy } from "../MockStudies"
|
||||
import { darkTheme } from "../styles/darkTheme"
|
||||
import { PlotSlice } from "./PlotSlice"
|
||||
|
||||
const meta: Meta<typeof PlotSlice> = {
|
||||
component: PlotSlice,
|
||||
title: "PlotSliceDark",
|
||||
tags: ["autodocs"],
|
||||
decorators: [
|
||||
(Story, storyContext) => {
|
||||
const { study } = useMockStudy(storyContext.parameters?.studyId)
|
||||
if (!study) return <p>loading...</p>
|
||||
return (
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<CssBaseline />
|
||||
<Story
|
||||
args={{
|
||||
study,
|
||||
}}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
)
|
||||
},
|
||||
],
|
||||
parameters: {
|
||||
backgrounds: { default: "dark" },
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof PlotSlice>
|
||||
|
||||
export const MockStudyExample1: Story = {
|
||||
parameters: {
|
||||
studyId: 1,
|
||||
},
|
||||
}
|
||||
@@ -1,7 +1,23 @@
|
||||
import { FC } from "react"
|
||||
|
||||
import * as Optuna from "@optuna/types"
|
||||
import { DataGrid, DataGridColumn } from "./DataGrid"
|
||||
import { DataGrid } from "./DataGrid"
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
FilterFn,
|
||||
Row,
|
||||
createColumnHelper,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
const multiValueFilter: FilterFn<Optuna.Trial> = <D extends object>(
|
||||
row: Row<D>,
|
||||
columnId: string,
|
||||
filterValue: string[]
|
||||
) => {
|
||||
const rowValue = row.getValue(columnId) as string
|
||||
return !filterValue.includes(rowValue)
|
||||
}
|
||||
|
||||
export const TrialTable: FC<{
|
||||
study: Optuna.Study
|
||||
@@ -9,144 +25,85 @@ export const TrialTable: FC<{
|
||||
}> = ({ study, initialRowsPerPage }) => {
|
||||
const trials: Optuna.Trial[] = study.trials
|
||||
|
||||
const columns: DataGridColumn<Optuna.Trial>[] = [
|
||||
{ field: "number", label: "Number", sortable: true, padding: "none" },
|
||||
{
|
||||
field: "state",
|
||||
label: "State",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
padding: "none",
|
||||
toCellValue: (i) => trials[i].state.toString(),
|
||||
},
|
||||
const columnHelper = createColumnHelper<Optuna.Trial>()
|
||||
// biome-ignore lint/suspicious/noExplicitAny: It is difficult to specify this type.
|
||||
const columns: ColumnDef<Optuna.Trial, any>[] = [
|
||||
columnHelper.accessor("number", {
|
||||
header: "Number",
|
||||
enableColumnFilter: false,
|
||||
}),
|
||||
columnHelper.accessor("state", {
|
||||
header: "State",
|
||||
enableSorting: false,
|
||||
enableColumnFilter: true,
|
||||
filterFn: multiValueFilter,
|
||||
}),
|
||||
]
|
||||
|
||||
if (study === null || study.directions.length === 1) {
|
||||
columns.push({
|
||||
field: "values",
|
||||
label: "Value",
|
||||
sortable: true,
|
||||
less: (firstEl, secondEl, ascending): number => {
|
||||
const firstVal = firstEl.values?.[0]
|
||||
const secondVal = secondEl.values?.[0]
|
||||
|
||||
if (firstVal === secondVal) {
|
||||
return 0
|
||||
}
|
||||
if (firstVal === undefined) {
|
||||
return ascending ? -1 : 1
|
||||
}
|
||||
if (secondVal === undefined) {
|
||||
return ascending ? 1 : -1
|
||||
}
|
||||
return firstVal < secondVal ? 1 : -1
|
||||
},
|
||||
toCellValue: (i) => {
|
||||
if (trials[i].values === undefined) {
|
||||
return null
|
||||
}
|
||||
return trials[i].values?.[0]
|
||||
},
|
||||
})
|
||||
if (study.directions.length === 1) {
|
||||
columns.push(
|
||||
columnHelper.accessor("values", {
|
||||
header: "Value",
|
||||
enableSorting: true,
|
||||
enableColumnFilter: false,
|
||||
sortUndefined: "last",
|
||||
})
|
||||
)
|
||||
} else {
|
||||
const objectiveColumns: DataGridColumn<Optuna.Trial>[] =
|
||||
study.directions.map((_s, objectiveId) => ({
|
||||
field: "values",
|
||||
label: `Objective ${objectiveId}`,
|
||||
sortable: true,
|
||||
less: (firstEl, secondEl, ascending): number => {
|
||||
const firstVal = firstEl.values?.[objectiveId]
|
||||
const secondVal = secondEl.values?.[objectiveId]
|
||||
|
||||
if (firstVal === secondVal) {
|
||||
return 0
|
||||
}
|
||||
if (firstVal === undefined) {
|
||||
return ascending ? -1 : 1
|
||||
}
|
||||
if (secondVal === undefined) {
|
||||
return ascending ? 1 : -1
|
||||
}
|
||||
return firstVal < secondVal ? 1 : -1
|
||||
},
|
||||
toCellValue: (i) => {
|
||||
if (trials[i].values === undefined) {
|
||||
return null
|
||||
}
|
||||
return trials[i].values?.[objectiveId]
|
||||
},
|
||||
}))
|
||||
columns.push(...objectiveColumns)
|
||||
columns.push(
|
||||
...study.directions.map((_s, objectiveId) =>
|
||||
columnHelper.accessor((row) => row.values?.[objectiveId], {
|
||||
id: `values_${objectiveId}`,
|
||||
header: `Objective ${objectiveId}`,
|
||||
enableSorting: true,
|
||||
enableColumnFilter: false,
|
||||
sortUndefined: "last",
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
for (const s of study.union_search_space) {
|
||||
columns.push({
|
||||
field: "params",
|
||||
label: `Param ${s.name}`,
|
||||
toCellValue: (i) =>
|
||||
trials[i].params.find((p) => p.name === s.name)?.param_external_value ??
|
||||
null,
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
less: (firstEl, secondEl): number => {
|
||||
const firstVal = firstEl.params.find(
|
||||
(p) => p.name === s.name
|
||||
)?.param_internal_value
|
||||
const secondVal = secondEl.params.find(
|
||||
(p) => p.name === s.name
|
||||
)?.param_internal_value
|
||||
|
||||
if (firstVal === secondVal) {
|
||||
return 0
|
||||
}
|
||||
if (firstVal && secondVal) {
|
||||
return firstVal < secondVal ? 1 : -1
|
||||
}
|
||||
if (firstVal) {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
},
|
||||
})
|
||||
if (study?.union_search_space != null) {
|
||||
for (const s of study.union_search_space) {
|
||||
columns.push(
|
||||
columnHelper.accessor(
|
||||
(row) =>
|
||||
row.params.find((p) => p.name === s.name)?.param_external_value ||
|
||||
null,
|
||||
{
|
||||
id: `params_${s.name}`,
|
||||
header: `Param ${s.name}`,
|
||||
enableSorting: true,
|
||||
sortUndefined: "last",
|
||||
enableColumnFilter: false,
|
||||
filterFn: multiValueFilter,
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const attr_spec of study.union_user_attrs) {
|
||||
columns.push({
|
||||
field: "user_attrs",
|
||||
label: `UserAttribute ${attr_spec.key}`,
|
||||
toCellValue: (i) =>
|
||||
trials[i].user_attrs.find((attr) => attr.key === attr_spec.key)
|
||||
?.value || null,
|
||||
sortable: attr_spec.sortable,
|
||||
filterable: false,
|
||||
less: (firstEl, secondEl): number => {
|
||||
const firstVal = firstEl.user_attrs.find(
|
||||
(attr) => attr.key === attr_spec.key
|
||||
)?.value
|
||||
const secondVal = secondEl.user_attrs.find(
|
||||
(attr) => attr.key === attr_spec.key
|
||||
)?.value
|
||||
|
||||
if (firstVal === secondVal) {
|
||||
return 0
|
||||
}
|
||||
if (firstVal && secondVal) {
|
||||
return firstVal < secondVal ? 1 : -1
|
||||
}
|
||||
if (firstVal) {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
},
|
||||
})
|
||||
if (study?.union_search_space != null) {
|
||||
for (const attr_spec of study.union_user_attrs) {
|
||||
columns.push(
|
||||
columnHelper.accessor(
|
||||
(row) =>
|
||||
row.user_attrs.find((a) => a.key === attr_spec.key)?.value || null,
|
||||
{
|
||||
id: `user_attrs_${attr_spec.key}`,
|
||||
header: `UserAttribute ${attr_spec.key}`,
|
||||
enableSorting: attr_spec.sortable,
|
||||
sortUndefined: "last",
|
||||
enableColumnFilter: false,
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DataGrid<Optuna.Trial>
|
||||
<DataGrid
|
||||
data={trials}
|
||||
columns={columns}
|
||||
rows={trials}
|
||||
keyField={"trial_id"}
|
||||
dense={false}
|
||||
initialRowsPerPage={initialRowsPerPage}
|
||||
/>
|
||||
)
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { GraphComponentState } from "ts/types/optuna"
|
||||
import { GraphComponentState } from "../types"
|
||||
|
||||
export const useGraphComponentState = () => {
|
||||
const [graphComponentState, setGraphComponentState] =
|
||||
@@ -1,7 +1,22 @@
|
||||
export { DataGrid } from "./components/DataGrid"
|
||||
export type { DataGridColumn } from "./components/DataGrid"
|
||||
export { plotlyDarkTemplate } from "./components/PlotlyDarkMode"
|
||||
export { PlotEdf } from "./components/PlotEdf"
|
||||
export type { EdfPlotInfo } from "./components/PlotEdf"
|
||||
export { PlotHistory } from "./components/PlotHistory"
|
||||
export { PlotImportance } from "./components/PlotImportance"
|
||||
export { PlotIntermediateValues } from "./components/PlotIntermediateValues"
|
||||
export { PlotSlice } from "./components/PlotSlice"
|
||||
export { TrialTable } from "./components/TrialTable"
|
||||
export { GraphContainer } from "./components/GraphContainer"
|
||||
export { useGraphComponentState } from "./hooks/useGraphComponentState"
|
||||
export {
|
||||
Target,
|
||||
useFilteredTrials,
|
||||
useFilteredTrialsFromStudies,
|
||||
useObjectiveTargets,
|
||||
useParamTargets,
|
||||
useObjectiveAndUserAttrTargets,
|
||||
useObjectiveAndUserAttrTargetsFromStudies,
|
||||
} from "./utils/trialFilter"
|
||||
export { useMergedUnionSearchSpace } from "./utils/searchSpace"
|
||||
export type { GraphComponentState } from "./types"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export type GraphComponentState =
|
||||
| "componentWillMount"
|
||||
| "componentDidMount"
|
||||
| "graphDidRender"
|
||||
@@ -1,23 +1,22 @@
|
||||
import * as Optuna from "@optuna/types"
|
||||
import { useMemo } from "react"
|
||||
import { SearchSpaceItem } from "./types/optuna"
|
||||
|
||||
export const mergeUnionSearchSpace = (
|
||||
unionSearchSpace: SearchSpaceItem[]
|
||||
): SearchSpaceItem[] => {
|
||||
const mergeUnionSearchSpace = (
|
||||
unionSearchSpace: Optuna.SearchSpaceItem[]
|
||||
): Optuna.SearchSpaceItem[] => {
|
||||
const knownElements = new Map<string, Optuna.Distribution>()
|
||||
unionSearchSpace.forEach((s) => {
|
||||
for (const s of unionSearchSpace) {
|
||||
const d = knownElements.get(s.name)
|
||||
if (d === undefined) {
|
||||
knownElements.set(s.name, s.distribution)
|
||||
return
|
||||
continue
|
||||
}
|
||||
if (
|
||||
d.type === "CategoricalDistribution" ||
|
||||
s.distribution.type === "CategoricalDistribution"
|
||||
) {
|
||||
// CategoricalDistribution.choices will never be changed
|
||||
return
|
||||
continue
|
||||
}
|
||||
const updated: Optuna.Distribution = {
|
||||
...d,
|
||||
@@ -25,7 +24,7 @@ export const mergeUnionSearchSpace = (
|
||||
high: Math.max(d.high, s.distribution.high),
|
||||
}
|
||||
knownElements.set(s.name, updated)
|
||||
})
|
||||
}
|
||||
return Array.from(knownElements.keys())
|
||||
.sort((a, b) => (a > b ? 1 : a < b ? -1 : 0))
|
||||
.map((name) => ({
|
||||
@@ -35,8 +34,8 @@ export const mergeUnionSearchSpace = (
|
||||
}
|
||||
|
||||
export const useMergedUnionSearchSpace = (
|
||||
unionSearchSpaces?: SearchSpaceItem[]
|
||||
): SearchSpaceItem[] =>
|
||||
unionSearchSpaces?: Optuna.SearchSpaceItem[]
|
||||
): Optuna.SearchSpaceItem[] =>
|
||||
useMemo(() => {
|
||||
return mergeUnionSearchSpace(unionSearchSpaces || [])
|
||||
}, [unionSearchSpaces])
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as Optuna from "@optuna/types"
|
||||
import { useMemo, useState } from "react"
|
||||
import { SearchSpaceItem, StudyDetail, Trial } from "./types/optuna"
|
||||
|
||||
type TargetKind = "objective" | "user_attr" | "params"
|
||||
|
||||
@@ -41,11 +40,11 @@ export class Target {
|
||||
return objectiveNames[objectiveId]
|
||||
}
|
||||
return `Objective ${objectiveId}`
|
||||
} else if (this.kind === "user_attr") {
|
||||
return `User Attribute ${this.key}`
|
||||
} else {
|
||||
return `Param ${this.key}`
|
||||
}
|
||||
if (this.kind === "user_attr") {
|
||||
return `User Attribute ${this.key}`
|
||||
}
|
||||
return `Param ${this.key}`
|
||||
}
|
||||
|
||||
getObjectiveId(): number | null {
|
||||
@@ -55,7 +54,7 @@ export class Target {
|
||||
return this.key as number
|
||||
}
|
||||
|
||||
getTargetValue(trial: Trial): number | null {
|
||||
getTargetValue(trial: Optuna.Trial): number | null {
|
||||
if (!this.validate()) {
|
||||
return null
|
||||
}
|
||||
@@ -73,7 +72,8 @@ export class Target {
|
||||
return null
|
||||
}
|
||||
return value
|
||||
} else if (this.kind === "user_attr") {
|
||||
}
|
||||
if (this.kind === "user_attr") {
|
||||
const attr = trial.user_attrs.find((attr) => attr.key === this.key)
|
||||
if (attr === undefined) {
|
||||
return null
|
||||
@@ -83,7 +83,8 @@ export class Target {
|
||||
return null
|
||||
}
|
||||
return value
|
||||
} else if (this.kind === "params") {
|
||||
}
|
||||
if (this.kind === "params") {
|
||||
const param = trial.params.find((p) => p.name === this.key)
|
||||
if (param === undefined) {
|
||||
return null
|
||||
@@ -95,10 +96,10 @@ export class Target {
|
||||
}
|
||||
|
||||
const filterTrials = (
|
||||
study: StudyDetail | null,
|
||||
study: Optuna.Study | null,
|
||||
targets: Target[],
|
||||
filterPruned: boolean
|
||||
): Trial[] => {
|
||||
): Optuna.Trial[] => {
|
||||
if (study === null) {
|
||||
return []
|
||||
}
|
||||
@@ -114,37 +115,39 @@ const filterTrials = (
|
||||
}
|
||||
|
||||
export const useFilteredTrials = (
|
||||
study: StudyDetail | null,
|
||||
study: Optuna.Study | null,
|
||||
targets: Target[],
|
||||
filterPruned: boolean
|
||||
): Trial[] =>
|
||||
useMemo<Trial[]>(() => {
|
||||
): Optuna.Trial[] =>
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
useMemo<Optuna.Trial[]>(() => {
|
||||
return filterTrials(study, targets, filterPruned)
|
||||
}, [study?.trials, targets, filterPruned])
|
||||
|
||||
export const useFilteredTrialsFromStudies = (
|
||||
studies: StudyDetail[],
|
||||
studies: Optuna.Study[],
|
||||
targets: Target[],
|
||||
filterPruned: boolean
|
||||
): Trial[][] =>
|
||||
useMemo<Trial[][]>(() => {
|
||||
): Optuna.Trial[][] =>
|
||||
useMemo<Optuna.Trial[][]>(() => {
|
||||
return studies.map((s) => filterTrials(s, targets, filterPruned))
|
||||
}, [studies, targets, filterPruned])
|
||||
|
||||
export const useObjectiveTargets = (
|
||||
study: StudyDetail | null
|
||||
study: Optuna.Study | null
|
||||
): [Target[], Target, (ident: string) => void] => {
|
||||
const defaultTarget = new Target("objective", 0)
|
||||
const [selected, setTargetIdent] = useState<string>(
|
||||
defaultTarget.identifier()
|
||||
)
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
const targetList = useMemo<Target[]>(() => {
|
||||
if (study !== null) {
|
||||
return study.directions.map((v, i) => new Target("objective", i))
|
||||
} else {
|
||||
return [defaultTarget]
|
||||
return study.directions.map((_v, i) => new Target("objective", i))
|
||||
}
|
||||
return [defaultTarget]
|
||||
}, [study?.directions])
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
const selectedTarget = useMemo<Target>(
|
||||
() => targetList.find((t) => t.identifier() === selected) || defaultTarget,
|
||||
[targetList, selected]
|
||||
@@ -153,9 +156,10 @@ export const useObjectiveTargets = (
|
||||
}
|
||||
|
||||
export const useParamTargets = (
|
||||
searchSpace: SearchSpaceItem[]
|
||||
searchSpace: Optuna.SearchSpaceItem[]
|
||||
): [Target[], Target | null, (ident: string) => void] => {
|
||||
const [selected, setTargetIdent] = useState<string>("")
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
const targetList = useMemo<Target[]>(() => {
|
||||
const targets = searchSpace.map((s) => new Target("params", s.name))
|
||||
if (selected === "" && targets.length > 0)
|
||||
@@ -170,24 +174,25 @@ export const useParamTargets = (
|
||||
}
|
||||
|
||||
export const useObjectiveAndUserAttrTargets = (
|
||||
study: StudyDetail | null
|
||||
study: Optuna.Study | null
|
||||
): [Target[], Target, (ident: string) => void] => {
|
||||
const defaultTarget = new Target("objective", 0)
|
||||
const [selected, setTargetIdent] = useState<string>(
|
||||
defaultTarget.identifier()
|
||||
)
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
const targetList = useMemo<Target[]>(() => {
|
||||
if (study !== null) {
|
||||
return [
|
||||
...study.directions.map((v, i) => new Target("objective", i)),
|
||||
...study.directions.map((_v, i) => new Target("objective", i)),
|
||||
...study.union_user_attrs
|
||||
.filter((attr) => attr.sortable)
|
||||
.map((attr) => new Target("user_attr", attr.key)),
|
||||
]
|
||||
} else {
|
||||
return [defaultTarget]
|
||||
}
|
||||
return [defaultTarget]
|
||||
}, [study?.directions, study?.union_user_attrs])
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
const selectedTarget = useMemo<Target>(
|
||||
() => targetList.find((t) => t.identifier() === selected) || defaultTarget,
|
||||
[targetList, selected]
|
||||
@@ -196,7 +201,7 @@ export const useObjectiveAndUserAttrTargets = (
|
||||
}
|
||||
|
||||
export const useObjectiveAndUserAttrTargetsFromStudies = (
|
||||
studies: StudyDetail[]
|
||||
studies: Optuna.Study[]
|
||||
): [Target[], Target, (ident: string) => void] => {
|
||||
const defaultTarget = new Target("objective", 0)
|
||||
const [selected, setTargetIdent] = useState<string>(
|
||||
@@ -223,6 +228,7 @@ export const useObjectiveAndUserAttrTargetsFromStudies = (
|
||||
)
|
||||
}
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
const attrTargets = useMemo<Target[]>(() => {
|
||||
if (studies.length === 0) {
|
||||
return []
|
||||
@@ -235,6 +241,7 @@ export const useObjectiveAndUserAttrTargetsFromStudies = (
|
||||
.map((attr) => new Target("user_attr", attr.key))
|
||||
}, [studies])
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
const targetList = useMemo<Target[]>(() => {
|
||||
if (studies !== null) {
|
||||
return [
|
||||
@@ -244,11 +251,11 @@ export const useObjectiveAndUserAttrTargetsFromStudies = (
|
||||
),
|
||||
...attrTargets,
|
||||
]
|
||||
} else {
|
||||
return [defaultTarget]
|
||||
}
|
||||
return [defaultTarget]
|
||||
}, [minDirections, attrTargets])
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
const selectedTarget = useMemo<Target>(
|
||||
() => targetList.find((t) => t.identifier() === selected) || defaultTarget,
|
||||
[targetList, selected]
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as Optuna from "@optuna/types"
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import React from "react"
|
||||
import { describe, expect, test } from "vitest"
|
||||
import { PlotEdf } from "../src/components/PlotEdf"
|
||||
|
||||
describe("PlotEdf Tests", async () => {
|
||||
const setup = ({
|
||||
study,
|
||||
dataTestId,
|
||||
}: { study: Optuna.Study; dataTestId: string }) => {
|
||||
const Wrapper = ({
|
||||
dataTestId,
|
||||
children,
|
||||
}: {
|
||||
dataTestId: string
|
||||
children: React.ReactNode
|
||||
}) => <div data-testid={dataTestId}>{children}</div>
|
||||
return render(
|
||||
<Wrapper dataTestId={dataTestId}>
|
||||
<PlotEdf studies={[study]} objectiveId={0} />
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
for (const study of window.mockStudies) {
|
||||
test(`PlotEdf (study name: ${study.name})`, () => {
|
||||
setup({ study, dataTestId: `plot-edf-${study.id}` })
|
||||
expect(screen.getByTestId(`plot-edf-${study.id}`)).toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -24,11 +24,9 @@ describe("PlotHistory Tests", async () => {
|
||||
}
|
||||
|
||||
for (const study of window.mockStudies) {
|
||||
test(`PlotHistory (study name: ${study.study_name})`, () => {
|
||||
setup({ study, dataTestId: `plot-history-${study.study_id}` })
|
||||
expect(
|
||||
screen.getByTestId(`plot-history-${study.study_id}`)
|
||||
).toBeInTheDocument()
|
||||
test(`PlotHistory (study name: ${study.name})`, () => {
|
||||
setup({ study, dataTestId: `plot-history-${study.id}` })
|
||||
expect(screen.getByTestId(`plot-history-${study.id}`)).toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -29,15 +29,15 @@ describe("PlotImportance Tests", async () => {
|
||||
}
|
||||
|
||||
for (const study of window.mockStudies) {
|
||||
test(`PlotImportance (study name: ${study.study_name})`, () => {
|
||||
const importance = window.mockImportances[study.study_name] ?? []
|
||||
test(`PlotImportance (study name: ${study.name})`, () => {
|
||||
const importance = window.mockImportances[study.name] ?? []
|
||||
setup({
|
||||
study,
|
||||
importance,
|
||||
dataTestId: `plot-importance-${study.study_id}`,
|
||||
dataTestId: `plot-importance-${study.id}`,
|
||||
})
|
||||
expect(
|
||||
screen.getByTestId(`plot-importance-${study.study_id}`)
|
||||
screen.getByTestId(`plot-importance-${study.id}`)
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@ describe("PlotIntermediateValues Tests", async () => {
|
||||
}
|
||||
|
||||
for (const study of window.mockStudies) {
|
||||
test(`PlotIntermediateValues (study name: ${study.study_name})`, () => {
|
||||
setup({ study, dataTestId: `plot-intermediatevalues-${study.study_id}` })
|
||||
test(`PlotIntermediateValues (study name: ${study.name})`, () => {
|
||||
setup({ study, dataTestId: `plot-intermediatevalues-${study.id}` })
|
||||
expect(
|
||||
screen.getByTestId(`plot-intermediatevalues-${study.study_id}`)
|
||||
screen.getByTestId(`plot-intermediatevalues-${study.id}`)
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as Optuna from "@optuna/types"
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import React from "react"
|
||||
import { describe, expect, test } from "vitest"
|
||||
import { PlotSlice } from "../src/components/PlotSlice"
|
||||
|
||||
describe("PlotSlice Tests", async () => {
|
||||
const setup = ({
|
||||
study,
|
||||
dataTestId,
|
||||
}: { study: Optuna.Study; dataTestId: string }) => {
|
||||
const Wrapper = ({
|
||||
dataTestId,
|
||||
children,
|
||||
}: {
|
||||
dataTestId: string
|
||||
children: React.ReactNode
|
||||
}) => <div data-testid={dataTestId}>{children}</div>
|
||||
return render(
|
||||
<Wrapper dataTestId={dataTestId}>
|
||||
<PlotSlice study={study} />
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
for (const study of window.mockStudies) {
|
||||
test(`PlotSlice (study name: ${study.name})`, () => {
|
||||
setup({ study, dataTestId: `plot-slice-${study.id}` })
|
||||
expect(screen.getByTestId(`plot-slice-${study.id}`)).toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -24,11 +24,9 @@ describe("TrialTable Tests", async () => {
|
||||
}
|
||||
|
||||
for (const study of window.mockStudies) {
|
||||
test(`TrialTable (study name: ${study.study_name})`, () => {
|
||||
setup({ study, dataTestId: `trial-table-${study.study_id}` })
|
||||
expect(
|
||||
screen.getByTestId(`trial-table-${study.study_id}`)
|
||||
).toBeInTheDocument()
|
||||
test(`TrialTable (study name: ${study.name})`, () => {
|
||||
setup({ study, dataTestId: `trial-table-${study.id}` })
|
||||
expect(screen.getByTestId(`trial-table-${study.id}`)).toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { JournalFileStorage } from "./journal"
|
||||
export { SQLite3Storage } from "./sqlite"
|
||||
export type { OptunaStorage } from "./storage"
|
||||
|
||||
+105
-28
@@ -1,6 +1,36 @@
|
||||
import * as Optuna from "@optuna/types"
|
||||
import { OptunaStorage } from "./storage"
|
||||
|
||||
// TODO(porink0424): Refactor to common function with sqlite.ts (current workaround duplicates code due to missing file extensions in tsc build output).
|
||||
const isDistributionEqual = (
|
||||
a: Optuna.Distribution,
|
||||
b: Optuna.Distribution
|
||||
) => {
|
||||
if (a.type !== b.type) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (a.type === "IntDistribution" || a.type === "FloatDistribution") {
|
||||
if (b.type !== "IntDistribution" && b.type !== "FloatDistribution") {
|
||||
throw new Error("Invalid distribution type")
|
||||
}
|
||||
return (
|
||||
a.low === b.low &&
|
||||
a.high === b.high &&
|
||||
a.step === b.step &&
|
||||
a.log === b.log
|
||||
)
|
||||
}
|
||||
if (a.type === "CategoricalDistribution") {
|
||||
if (b.type !== "CategoricalDistribution") {
|
||||
throw new Error("Invalid distribution type")
|
||||
}
|
||||
return JSON.stringify(a.choices) === JSON.stringify(b.choices)
|
||||
}
|
||||
|
||||
throw new Error("Invalid distribution type")
|
||||
}
|
||||
|
||||
// JournalStorage
|
||||
enum JournalOperation {
|
||||
CREATE_STUDY = 0,
|
||||
@@ -29,6 +59,13 @@ interface JournalOpDeleteStudy extends JournalOpBase {
|
||||
study_id: number
|
||||
}
|
||||
|
||||
interface JournalOpSetStudySystemAttr extends JournalOpBase {
|
||||
study_id: number
|
||||
system_attr: {
|
||||
"study:metric_names"?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
interface JournalOpCreateTrial extends JournalOpBase {
|
||||
study_id: number
|
||||
datetime_start?: string
|
||||
@@ -73,6 +110,13 @@ interface JournalOpSetTrialUserAttr extends JournalOpBase {
|
||||
user_attr: { [key: string]: any } // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}
|
||||
|
||||
interface JournalOpSetTrialSystemAttr extends JournalOpBase {
|
||||
trial_id: number
|
||||
system_attr: {
|
||||
constraints?: number[]
|
||||
}
|
||||
}
|
||||
|
||||
const trialStateNumToTrialState = (state: number): Optuna.TrialState => {
|
||||
switch (state) {
|
||||
case 0:
|
||||
@@ -123,22 +167,42 @@ class JournalStorage {
|
||||
public getStudies(): Optuna.Study[] {
|
||||
for (const study of this.studies) {
|
||||
const unionUserAttrs: Set<string> = new Set()
|
||||
const unionSearchSpace: Set<string> = new Set()
|
||||
let intersectionSearchSpace: string[] = []
|
||||
const unionSearchSpace: Optuna.SearchSpaceItem[] = []
|
||||
let intersectionSearchSpace: Optuna.SearchSpaceItem[] = []
|
||||
|
||||
study.trials.forEach((trial, index) => {
|
||||
for (const userAttr of trial.user_attrs) {
|
||||
unionUserAttrs.add(userAttr.key)
|
||||
}
|
||||
for (const param of trial.params) {
|
||||
unionSearchSpace.add(param.name)
|
||||
if (
|
||||
!unionSearchSpace.some(
|
||||
(item) =>
|
||||
item.name === param.name &&
|
||||
isDistributionEqual(item.distribution, param.distribution)
|
||||
)
|
||||
) {
|
||||
unionSearchSpace.push({
|
||||
name: param.name,
|
||||
distribution: param.distribution,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (index === 0) {
|
||||
intersectionSearchSpace = Array.from(unionSearchSpace)
|
||||
intersectionSearchSpace = [...unionSearchSpace]
|
||||
} else {
|
||||
intersectionSearchSpace = intersectionSearchSpace.filter((name) => {
|
||||
return trial.params.some((param) => param.name === name)
|
||||
})
|
||||
intersectionSearchSpace = intersectionSearchSpace.filter(
|
||||
(searchSpaceItem) => {
|
||||
return trial.params.some(
|
||||
(param) =>
|
||||
param.name === searchSpaceItem.name &&
|
||||
isDistributionEqual(
|
||||
param.distribution,
|
||||
searchSpaceItem.distribution
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
study.union_user_attrs = Array.from(unionUserAttrs).map((key) => {
|
||||
@@ -147,16 +211,8 @@ class JournalStorage {
|
||||
sortable: false,
|
||||
}
|
||||
})
|
||||
study.union_search_space = Array.from(unionSearchSpace).map((name) => {
|
||||
return {
|
||||
name: name,
|
||||
}
|
||||
})
|
||||
study.intersection_search_space = intersectionSearchSpace.map((name) => {
|
||||
return {
|
||||
name: name,
|
||||
}
|
||||
})
|
||||
study.union_search_space = unionSearchSpace
|
||||
study.intersection_search_space = intersectionSearchSpace
|
||||
}
|
||||
|
||||
return this.studies
|
||||
@@ -164,8 +220,8 @@ class JournalStorage {
|
||||
|
||||
public applyCreateStudy(log: JournalOpCreateStudy): void {
|
||||
this.studies.push({
|
||||
study_id: this.nextStudyId,
|
||||
study_name: log.study_name,
|
||||
id: this.nextStudyId,
|
||||
name: log.study_name,
|
||||
directions: [log.directions[0] === 1 ? "minimize" : "maximize"],
|
||||
union_search_space: [],
|
||||
intersection_search_space: [],
|
||||
@@ -176,13 +232,19 @@ class JournalStorage {
|
||||
}
|
||||
|
||||
public applyDeleteStudy(log: JournalOpDeleteStudy): void {
|
||||
this.studies = this.studies.filter((item) => item.study_id !== log.study_id)
|
||||
this.studies = this.studies.filter((item) => item.id !== log.study_id)
|
||||
}
|
||||
|
||||
public applyStudySystemAttr(log: JournalOpSetStudySystemAttr): void {
|
||||
const thisStudy = this.studies.find((item) => item.id === log.study_id)
|
||||
if (thisStudy === undefined) {
|
||||
return
|
||||
}
|
||||
thisStudy.metric_names = log.system_attr["study:metric_names"]
|
||||
}
|
||||
|
||||
public applyCreateTrial(log: JournalOpCreateTrial): void {
|
||||
const thisStudy = this.studies.find(
|
||||
(item) => item.study_id === log.study_id
|
||||
)
|
||||
const thisStudy = this.studies.find((item) => item.id === log.study_id)
|
||||
if (thisStudy === undefined) {
|
||||
return
|
||||
}
|
||||
@@ -211,7 +273,7 @@ class JournalStorage {
|
||||
}
|
||||
})
|
||||
|
||||
const userAtter = log.user_attrs
|
||||
const userAttrs = log.user_attrs
|
||||
? Object.entries(log.user_attrs).map(([key, value]) => {
|
||||
return {
|
||||
key: key,
|
||||
@@ -236,7 +298,8 @@ class JournalStorage {
|
||||
})(),
|
||||
params: params,
|
||||
intermediate_values: [],
|
||||
user_attrs: userAtter,
|
||||
user_attrs: userAttrs,
|
||||
constraints: [],
|
||||
datetime_start: log.datetime_start
|
||||
? new Date(log.datetime_start)
|
||||
: undefined,
|
||||
@@ -256,7 +319,7 @@ class JournalStorage {
|
||||
|
||||
private getStudyAndTrial(trial_id: number): [Optuna.Study?, Optuna.Trial?] {
|
||||
const study = this.studies.find(
|
||||
(item) => item.study_id === this.trialIdToStudyId.get(trial_id)
|
||||
(item) => item.id === this.trialIdToStudyId.get(trial_id)
|
||||
)
|
||||
if (study === undefined) {
|
||||
return [undefined, undefined]
|
||||
@@ -328,6 +391,16 @@ class JournalStorage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public applySetTrialSystemAttr(log: JournalOpSetTrialSystemAttr) {
|
||||
const [thisStudy, thisTrial] = this.getStudyAndTrial(log.trial_id)
|
||||
if (thisStudy === undefined || thisTrial === undefined) {
|
||||
return
|
||||
}
|
||||
if (log.system_attr.constraints) {
|
||||
thisTrial.constraints = log.system_attr.constraints
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadJournalStorage = (
|
||||
@@ -395,7 +468,9 @@ const loadJournalStorage = (
|
||||
// Unsupported
|
||||
break
|
||||
case JournalOperation.SET_STUDY_SYSTEM_ATTR:
|
||||
// Unsupported
|
||||
journalStorage.applyStudySystemAttr(
|
||||
parsedLog as JournalOpSetStudySystemAttr
|
||||
)
|
||||
break
|
||||
case JournalOperation.CREATE_TRIAL:
|
||||
journalStorage.applyCreateTrial(parsedLog as JournalOpCreateTrial)
|
||||
@@ -419,7 +494,9 @@ const loadJournalStorage = (
|
||||
)
|
||||
break
|
||||
case JournalOperation.SET_TRIAL_SYSTEM_ATTR:
|
||||
// Unsupported
|
||||
journalStorage.applySetTrialSystemAttr(
|
||||
parsedLog as JournalOpSetTrialSystemAttr
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
+92
-20
@@ -3,6 +3,36 @@ import * as Optuna from "@optuna/types"
|
||||
import sqlite3InitModule from "@sqlite.org/sqlite-wasm"
|
||||
import { OptunaStorage } from "./storage"
|
||||
|
||||
// TODO(porink0424): Refactor to common function with journal.ts (current workaround duplicates code due to missing file extensions in tsc build output).
|
||||
const isDistributionEqual = (
|
||||
a: Optuna.Distribution,
|
||||
b: Optuna.Distribution
|
||||
) => {
|
||||
if (a.type !== b.type) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (a.type === "IntDistribution" || a.type === "FloatDistribution") {
|
||||
if (b.type !== "IntDistribution" && b.type !== "FloatDistribution") {
|
||||
throw new Error("Invalid distribution type")
|
||||
}
|
||||
return (
|
||||
a.low === b.low &&
|
||||
a.high === b.high &&
|
||||
a.step === b.step &&
|
||||
a.log === b.log
|
||||
)
|
||||
}
|
||||
if (a.type === "CategoricalDistribution") {
|
||||
if (b.type !== "CategoricalDistribution") {
|
||||
throw new Error("Invalid distribution type")
|
||||
}
|
||||
return JSON.stringify(a.choices) === JSON.stringify(b.choices)
|
||||
}
|
||||
|
||||
throw new Error("Invalid distribution type")
|
||||
}
|
||||
|
||||
type SQLite3DB = {
|
||||
exec(options: {
|
||||
sql: string
|
||||
@@ -116,13 +146,13 @@ const getStudySummaries = (db: SQLite3DB): Optuna.StudySummary[] => {
|
||||
|
||||
if (objective === 0) {
|
||||
summaries.push({
|
||||
study_id: studyId,
|
||||
study_name: studyName,
|
||||
id: studyId,
|
||||
name: studyName,
|
||||
directions: [direction],
|
||||
})
|
||||
return
|
||||
}
|
||||
const index = summaries.findIndex((s) => s.study_id === studyId)
|
||||
const index = summaries.findIndex((s) => s.id === studyId)
|
||||
summaries[index].directions.push(direction)
|
||||
},
|
||||
})
|
||||
@@ -135,8 +165,8 @@ const getStudy = (
|
||||
summary: Optuna.StudySummary
|
||||
): Optuna.Study => {
|
||||
const study: Optuna.Study = {
|
||||
study_id: summary.study_id,
|
||||
study_name: summary.study_name,
|
||||
id: summary.id,
|
||||
name: summary.name,
|
||||
directions: summary.directions,
|
||||
union_search_space: [],
|
||||
intersection_search_space: [],
|
||||
@@ -144,8 +174,13 @@ const getStudy = (
|
||||
trials: [],
|
||||
}
|
||||
|
||||
let intersection_search_space: Set<Optuna.SearchSpaceItem> = new Set()
|
||||
study.trials = getTrials(db, summary.study_id, schemaVersion)
|
||||
const studySystemAttrs = getStudySystemAttributes(db, summary.id)
|
||||
if (studySystemAttrs !== undefined) {
|
||||
study.metric_names = studySystemAttrs.metric_names
|
||||
}
|
||||
|
||||
let intersectionSearchSpace: Optuna.SearchSpaceItem[] = []
|
||||
study.trials = getTrials(db, summary.id, schemaVersion)
|
||||
for (const trial of study.trials) {
|
||||
const userAttrs = getTrialUserAttributes(db, trial.trial_id)
|
||||
for (const attr of userAttrs) {
|
||||
@@ -154,32 +189,40 @@ const getStudy = (
|
||||
}
|
||||
}
|
||||
|
||||
const systemAttrs = getTrialSystemAttributes(db, trial.trial_id)
|
||||
if (systemAttrs !== undefined) {
|
||||
trial.constraints = systemAttrs.constraints
|
||||
}
|
||||
|
||||
const params = getTrialParams(db, trial.trial_id)
|
||||
const param_names = new Set<string>()
|
||||
for (const param of params) {
|
||||
param_names.add(param.name)
|
||||
if (
|
||||
study.union_search_space.findIndex((s) => s.name === param.name) === -1
|
||||
) {
|
||||
study.union_search_space.push({ name: param.name })
|
||||
study.union_search_space.push({
|
||||
name: param.name,
|
||||
distribution: param.distribution,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (intersection_search_space.size === 0) {
|
||||
// biome-ignore lint/complexity/noForEach: <explanation>
|
||||
param_names.forEach((s) => {
|
||||
intersection_search_space.add({ name: s })
|
||||
})
|
||||
if (intersectionSearchSpace.length === 0) {
|
||||
intersectionSearchSpace = params.map((param) => ({
|
||||
name: param.name,
|
||||
distribution: param.distribution,
|
||||
}))
|
||||
} else {
|
||||
intersection_search_space = new Set(
|
||||
Array.from(intersection_search_space).filter((s) =>
|
||||
param_names.has(s.name)
|
||||
intersectionSearchSpace = intersectionSearchSpace.filter((item) => {
|
||||
return params.some(
|
||||
(param) =>
|
||||
item.name === param.name &&
|
||||
isDistributionEqual(item.distribution, param.distribution)
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
trial.params = params
|
||||
trial.user_attrs = userAttrs
|
||||
}
|
||||
study.intersection_search_space = Array.from(intersection_search_space)
|
||||
study.intersection_search_space = intersectionSearchSpace
|
||||
return study
|
||||
}
|
||||
|
||||
@@ -217,6 +260,7 @@ const getTrials = (
|
||||
),
|
||||
params: [], // Set this column later
|
||||
user_attrs: [], // Set this column later
|
||||
constraints: [],
|
||||
datetime_start: vals[3],
|
||||
datetime_complete: vals[4],
|
||||
}
|
||||
@@ -367,6 +411,20 @@ const parseDistributionJSON = (t: string): Optuna.Distribution => {
|
||||
}
|
||||
}
|
||||
|
||||
const getStudySystemAttributes = (db: SQLite3DB, studyId: number) => {
|
||||
let attrs: { metric_names: string[] } | undefined
|
||||
db.exec({
|
||||
sql: `SELECT key, value_json FROM study_system_attributes WHERE study_id = ${studyId} AND key = 'dashboard:objective_names'`,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
callback: (vals: any[]) => {
|
||||
attrs = {
|
||||
metric_names: JSON.parse(vals[1]),
|
||||
}
|
||||
},
|
||||
})
|
||||
return attrs
|
||||
}
|
||||
|
||||
const getTrialUserAttributes = (
|
||||
db: SQLite3DB,
|
||||
trialId: number
|
||||
@@ -385,6 +443,20 @@ const getTrialUserAttributes = (
|
||||
return attrs
|
||||
}
|
||||
|
||||
const getTrialSystemAttributes = (db: SQLite3DB, trialId: number) => {
|
||||
let attrs: { constraints: number[] } | undefined
|
||||
db.exec({
|
||||
sql: `SELECT key, value_json FROM trial_system_attributes WHERE trial_id = ${trialId} AND key = 'constraints'`,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
callback: (vals: any[]) => {
|
||||
attrs = {
|
||||
constraints: JSON.parse(vals[1]),
|
||||
}
|
||||
},
|
||||
})
|
||||
return attrs
|
||||
}
|
||||
|
||||
const getTrialIntermediateValues = (
|
||||
db: SQLite3DB,
|
||||
trialId: number,
|
||||
|
||||
@@ -44,9 +44,9 @@ def create_optuna_storage(storage: BaseStorage) -> None:
|
||||
def objective_single_dynamic(trial: optuna.Trial) -> float:
|
||||
category = trial.suggest_categorical("category", ["foo", "bar"])
|
||||
if category == "foo":
|
||||
return (trial.suggest_float("x1", 0, 10) - 2) ** 2
|
||||
return (trial.suggest_float("x", 0, 10) - 2) ** 2
|
||||
else:
|
||||
return -((trial.suggest_float("x2", -10, 0) + 5) ** 2)
|
||||
return -((trial.suggest_float("x", -10, 0) + 5) ** 2)
|
||||
|
||||
study.optimize(objective_single_dynamic, n_trials=50)
|
||||
|
||||
@@ -78,6 +78,48 @@ def create_optuna_storage(storage: BaseStorage) -> None:
|
||||
|
||||
study.optimize(objective_single_nan_report, n_trials=100)
|
||||
|
||||
# Multi-objective study with metric names
|
||||
study = optuna.create_study(
|
||||
study_name="multi-objective-metric-names",
|
||||
storage=storage,
|
||||
directions=["minimize", "minimize"],
|
||||
)
|
||||
print(f"Generating {study.study_name} for {type(storage).__name__}...")
|
||||
study.set_metric_names(["value1", "value2"])
|
||||
|
||||
def objective_multi(trial: optuna.Trial) -> tuple[float, float]:
|
||||
x = trial.suggest_float("x", 0, 5)
|
||||
y = trial.suggest_float("y", 0, 3)
|
||||
v0 = 4 * x**2 + 4 * y**2
|
||||
v1 = (x - 5) ** 2 + (y - 5) ** 2
|
||||
return v0, v1
|
||||
|
||||
study.optimize(objective_multi, n_trials=50)
|
||||
|
||||
# Multi-objective study with constraints
|
||||
def objective_constraints(trial: optuna.Trial) -> tuple[float, float]:
|
||||
x = trial.suggest_float("x", -15, 30)
|
||||
y = trial.suggest_float("y", -15, 30)
|
||||
c0 = (x - 5) ** 2 + y**2 - 25
|
||||
c1 = -((x - 8) ** 2) - (y + 3) ** 2 + 7.7
|
||||
trial.set_user_attr("constraint", (c0, c1))
|
||||
v0 = 4 * x**2 + 4 * y**2
|
||||
v1 = (x - 5) ** 2 + (y - 5) ** 2
|
||||
return v0, v1
|
||||
|
||||
def constraints(trial: optuna.Trial):
|
||||
return trial.user_attrs["constraint"]
|
||||
|
||||
sampler = optuna.samplers.NSGAIISampler(constraints_func=constraints)
|
||||
study = optuna.create_study(
|
||||
study_name="multi-objective-constraints",
|
||||
storage=storage,
|
||||
sampler=sampler,
|
||||
directions=["minimize", "minimize"],
|
||||
)
|
||||
print(f"Generating {study.study_name} for {type(storage).__name__}...")
|
||||
study.optimize(objective_constraints, n_trials=32, timeout=600)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
remove_assets()
|
||||
|
||||
@@ -16,8 +16,62 @@ describe("Test Journal File Storage", async () => {
|
||||
studySummaries.map((_summary, index) => storage.getStudy(index))
|
||||
)
|
||||
|
||||
it("Check the study with dynamic search space", () => {
|
||||
const study = studies.find((s) => s.name === "single-objective-dynamic")
|
||||
assert.deepStrictEqual(
|
||||
study.union_search_space.map((item) => item.name).sort(),
|
||||
["x", "x", "category"].sort()
|
||||
)
|
||||
assert.strictEqual(
|
||||
study.union_search_space.some(
|
||||
(item) =>
|
||||
item.name === "category" &&
|
||||
item.distribution.type === "CategoricalDistribution" &&
|
||||
item.distribution.choices.length === 2
|
||||
),
|
||||
true
|
||||
)
|
||||
assert.strictEqual(
|
||||
study.union_search_space.some(
|
||||
(item) =>
|
||||
item.name === "x" &&
|
||||
item.distribution.type === "FloatDistribution" &&
|
||||
item.distribution.low === 0 &&
|
||||
item.distribution.high === 10 &&
|
||||
item.distribution.step === null &&
|
||||
item.distribution.log === false
|
||||
),
|
||||
true
|
||||
)
|
||||
assert.strictEqual(
|
||||
study.union_search_space.some(
|
||||
(item) =>
|
||||
item.name === "x" &&
|
||||
item.distribution.type === "FloatDistribution" &&
|
||||
item.distribution.low === -10 &&
|
||||
item.distribution.high === 0 &&
|
||||
item.distribution.step === null &&
|
||||
item.distribution.log === false
|
||||
),
|
||||
true
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
study.intersection_search_space.map((item) => item.name).sort(),
|
||||
["category"].sort()
|
||||
)
|
||||
assert.strictEqual(
|
||||
study.intersection_search_space.some(
|
||||
(item) =>
|
||||
item.name === "category" &&
|
||||
item.distribution.type === "CategoricalDistribution" &&
|
||||
item.distribution.choices.length === 2
|
||||
),
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("Check the study including Infinities", () => {
|
||||
const study = studies.find((s) => s.study_name === "single-inf")
|
||||
const study = studies.find((s) => s.name === "single-inf")
|
||||
study.trials.forEach((trial, index) => {
|
||||
if (index % 3 === 0) {
|
||||
assert.strictEqual(trial.values[0], Infinity)
|
||||
@@ -28,7 +82,7 @@ describe("Test Journal File Storage", async () => {
|
||||
})
|
||||
|
||||
it("Check the study including NaNs", () => {
|
||||
const study = studies.find((s) => s.study_name === "single-nan-report")
|
||||
const study = studies.find((s) => s.name === "single-nan-report")
|
||||
for (const trial of study.trials) {
|
||||
assert.strictEqual(
|
||||
trial.intermediate_values.find((v) => v.step === 1).value,
|
||||
@@ -52,8 +106,20 @@ describe("Test Journal File Storage", async () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("Check metric_names function", () => {
|
||||
const study = studies.find((s) => s.name === "multi-objective-metric-names")
|
||||
assert.deepStrictEqual(study.metric_names, ["value1", "value2"])
|
||||
})
|
||||
|
||||
it("Check the study with constraints", () => {
|
||||
const study = studies.find((s) => s.name === "multi-objective-constraints")
|
||||
for (const trial of study.trials) {
|
||||
assert.strictEqual(trial.constraints.length, 2)
|
||||
}
|
||||
})
|
||||
|
||||
it("Check the number of studies", () => {
|
||||
const N_STUDIES = 4
|
||||
const N_STUDIES = 6
|
||||
assert.strictEqual(studies.length, N_STUDIES)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,26 +46,27 @@ export type AttributeSpec = {
|
||||
}
|
||||
|
||||
export type StudySummary = {
|
||||
study_id: number
|
||||
study_name: string
|
||||
id: number
|
||||
name: string
|
||||
directions: StudyDirection[]
|
||||
}
|
||||
|
||||
export type Study = {
|
||||
study_id: number
|
||||
study_name: string
|
||||
id: number
|
||||
name: string
|
||||
directions: StudyDirection[]
|
||||
union_search_space: SearchSpaceItem[]
|
||||
intersection_search_space: SearchSpaceItem[]
|
||||
union_user_attrs: AttributeSpec[]
|
||||
datetime_start?: Date
|
||||
trials: Trial[]
|
||||
metric_names?: string[]
|
||||
}
|
||||
|
||||
export type Trial = {
|
||||
trial_id: number
|
||||
number: number
|
||||
study_id: number
|
||||
number: number
|
||||
state: TrialState
|
||||
values?: number[]
|
||||
params: TrialParam[]
|
||||
@@ -73,6 +74,7 @@ export type Trial = {
|
||||
user_attrs: Attribute[]
|
||||
datetime_start?: Date
|
||||
datetime_complete?: Date
|
||||
constraints: number[]
|
||||
}
|
||||
|
||||
export type TrialParam = {
|
||||
@@ -85,6 +87,7 @@ export type TrialParam = {
|
||||
|
||||
export type SearchSpaceItem = {
|
||||
name: string
|
||||
distribution: Distribution
|
||||
}
|
||||
|
||||
export type ParamImportance = {
|
||||
|
||||
Generated
+21
-60
@@ -955,12 +955,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -1774,9 +1774,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -2793,18 +2793,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -3734,13 +3722,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz",
|
||||
"integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==",
|
||||
"version": "7.6.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
|
||||
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
@@ -4470,12 +4455,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
@@ -5241,12 +5220,12 @@
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
}
|
||||
},
|
||||
"browser-stdout": {
|
||||
@@ -5854,9 +5833,9 @@
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -6616,15 +6595,6 @@
|
||||
"is-unicode-supported": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -7311,13 +7281,10 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz",
|
||||
"integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
"version": "7.6.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
|
||||
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
|
||||
"dev": true
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "6.0.0",
|
||||
@@ -7838,12 +7805,6 @@
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
|
||||
Reference in New Issue
Block a user