mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-11 12:30:25 +08:00
Screen to compare the multiple studies (#380)
* Add compare studies * Able to select multiple studies Save Save * Able to plot multiple studies * Remove warning * Add toolbar Save * Fix setStudyDetailState * Devide the file * Add best lines * Add x-axis, y-axis and pruned button * Use compare icon * Rename StudiesDetail to CompareStudies * Stop useless server access * Add edf plot * Use filterTrials * Fix legend * Add object id to history plot * Remove errors * Add study information to list * Disable studies * Only enable onClick for the same n_obj * Add link to home * Unify GraphEdf * Unify GraphHistory * Change that one of studies is always selected and show an error message when a study can not be added * Rebase against main * Update optuna_dashboard/ts/components/CompareStudies.tsx Co-authored-by: Hiroyuki Vincent Yamazaki <hiroyuki.vincent.yamazaki@gmail.com> * Show the first study if query is not specified * Revert TrialList change * Change query paraater from numbers to ids * Align the implementation with TrialList * Unified implementation of useFilteredTrials and useFilteredTrialsFromStudies * Use useMemo to get rid of unnecessary API requests * Only enable onClick for the same directions * Follow review comments * Follow review comments * Follow review comments * Follow review comments * Fix integration test * Separate the implementation of edf and history for multi studies * Revert GraphEdf and GraphEdfBeta * Revert GraphHistory * Follow review comments * Follow review comments --------- Co-authored-by: Hiroyuki Vincent Yamazaki <hiroyuki.vincent.yamazaki@gmail.com>
This commit is contained in:
co-authored by
Hiroyuki Vincent Yamazaki
parent
f34bd66143
commit
3ab18cf87f
@@ -48,9 +48,11 @@ export const actionCreator = () => {
|
||||
const setArtifactIsAvailable = useSetRecoilState<boolean>(artifactIsAvailable)
|
||||
|
||||
const setStudyDetailState = (studyId: number, study: StudyDetail) => {
|
||||
const newVal = Object.assign({}, studyDetails)
|
||||
newVal[studyId] = study
|
||||
setStudyDetails(newVal)
|
||||
setStudyDetails((prevVal) => {
|
||||
const newVal = Object.assign({}, prevVal)
|
||||
newVal[studyId] = study
|
||||
return newVal
|
||||
})
|
||||
}
|
||||
|
||||
const setTrial = (studyId: number, trialIndex: number, trial: Trial) => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
CssBaseline,
|
||||
} from "@mui/material"
|
||||
|
||||
import { CompareStudies } from "./CompareStudies"
|
||||
import { StudyDetail } from "./StudyDetail"
|
||||
import { StudyList } from "./StudyList"
|
||||
import { StudyDetailBeta } from "./StudyDetailBeta"
|
||||
@@ -110,6 +111,12 @@ export const App: FC = () => {
|
||||
path={URL_PREFIX + "/studies/:studyId"}
|
||||
children={<StudyDetail toggleColorMode={toggleColorMode} />}
|
||||
/>
|
||||
<Route
|
||||
path={URL_PREFIX + "/compare-studies"}
|
||||
children={
|
||||
<CompareStudies toggleColorMode={toggleColorMode} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={URL_PREFIX + "/beta"}
|
||||
children={<StudyListBeta toggleColorMode={toggleColorMode} />}
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
import React, { ChangeEvent, FC, useEffect, useMemo, useState } from "react"
|
||||
import { useRecoilValue } from "recoil"
|
||||
import { useSnackbar } from "notistack"
|
||||
import { Link } from "react-router-dom"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
FormControl,
|
||||
Switch,
|
||||
Typography,
|
||||
Box,
|
||||
useTheme,
|
||||
IconButton,
|
||||
} from "@mui/material"
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight"
|
||||
import Chip from "@mui/material/Chip"
|
||||
import FormControlLabel from "@mui/material/FormControlLabel"
|
||||
import Divider from "@mui/material/Divider"
|
||||
import List from "@mui/material/List"
|
||||
import ListItem from "@mui/material/ListItem"
|
||||
import ListItemButton from "@mui/material/ListItemButton"
|
||||
import ListItemText from "@mui/material/ListItemText"
|
||||
import ListSubheader from "@mui/material/ListSubheader"
|
||||
import HomeIcon from "@mui/icons-material/Home"
|
||||
|
||||
import { actionCreator } from "../action"
|
||||
import { studySummariesState, studyDetailsState } from "../state"
|
||||
import { AppDrawer } from "./AppDrawer"
|
||||
import { GraphEdfMultiStudies } from "./GraphEdf"
|
||||
import { GraphHistoryMultiStudies } from "./GraphHistory"
|
||||
import { useHistory, useLocation } from "react-router-dom"
|
||||
|
||||
const useQuery = (): URLSearchParams => {
|
||||
const { search } = useLocation()
|
||||
return useMemo(() => new URLSearchParams(search), [search])
|
||||
}
|
||||
|
||||
const useQueriedStudies = (
|
||||
studies: StudySummary[],
|
||||
query: URLSearchParams
|
||||
): StudySummary[] => {
|
||||
return useMemo(() => {
|
||||
const queried = query.get("ids")
|
||||
if (queried === null) {
|
||||
return []
|
||||
}
|
||||
const ids = queried
|
||||
.split(",")
|
||||
.map((s) => parseInt(s))
|
||||
.filter((n) => !isNaN(n))
|
||||
return studies.filter((t) => ids.findIndex((n) => n === t.study_id) !== -1)
|
||||
}, [studies, query])
|
||||
}
|
||||
|
||||
const getStudyListLink = (ids: number[]): string => {
|
||||
const base = URL_PREFIX + "/compare-studies"
|
||||
if (ids.length > 0) {
|
||||
return base + "?ids=" + ids.map((n) => n.toString()).join(",")
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
const isEqualDirections = (
|
||||
array1: StudyDirection[],
|
||||
array2: StudyDirection[]
|
||||
): boolean => {
|
||||
let i = array1.length
|
||||
if (i !== array2.length) return false
|
||||
|
||||
while (i--) {
|
||||
if (array1[i] !== array2[i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const CompareStudies: FC<{
|
||||
toggleColorMode: () => void
|
||||
}> = ({ toggleColorMode }) => {
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
const theme = useTheme()
|
||||
const query = useQuery()
|
||||
const history = useHistory()
|
||||
|
||||
const action = actionCreator()
|
||||
const studies = useRecoilValue<StudySummary[]>(studySummariesState)
|
||||
const queried = useQueriedStudies(studies, query)
|
||||
const selected = useMemo(() => {
|
||||
return queried.length > 0 ? queried : studies.length > 0 ? [studies[0]] : []
|
||||
}, [studies, query])
|
||||
|
||||
const studyListWidth = 200
|
||||
const title = "Compare Studies"
|
||||
|
||||
useEffect(() => {
|
||||
action.updateStudySummaries()
|
||||
}, [])
|
||||
|
||||
const toolbar = (
|
||||
<>
|
||||
<IconButton
|
||||
component={Link}
|
||||
to={URL_PREFIX + "/beta"}
|
||||
sx={{ marginRight: theme.spacing(1) }}
|
||||
color="inherit"
|
||||
title="Return to the top page"
|
||||
>
|
||||
<HomeIcon />
|
||||
</IconButton>
|
||||
<ChevronRightIcon sx={{ marginRight: theme.spacing(1) }} />
|
||||
<Typography
|
||||
noWrap
|
||||
component="div"
|
||||
sx={{ fontWeight: theme.typography.fontWeightBold }}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<AppDrawer toggleColorMode={toggleColorMode} toolbar={toolbar}>
|
||||
<Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}>
|
||||
<Box
|
||||
sx={{
|
||||
minWidth: studyListWidth,
|
||||
overflow: "auto",
|
||||
height: `calc(100vh - ${theme.spacing(8)})`,
|
||||
}}
|
||||
>
|
||||
<List>
|
||||
<ListSubheader sx={{ display: "flex", flexDirection: "row" }}>
|
||||
<Typography sx={{ p: theme.spacing(1, 0) }}>
|
||||
{studies.length} Studies
|
||||
</Typography>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
</ListSubheader>
|
||||
<Divider />
|
||||
{studies.map((study, i) => {
|
||||
return (
|
||||
<ListItem key={study.study_id} disablePadding>
|
||||
<ListItemButton
|
||||
onClick={(e) => {
|
||||
if (e.shiftKey) {
|
||||
let next: number[]
|
||||
const selectedIds = selected.map((s) => s.study_id)
|
||||
const alreadySelected =
|
||||
selectedIds.findIndex(
|
||||
(n) => n === study.study_id
|
||||
) >= 0
|
||||
if (alreadySelected) {
|
||||
next = selectedIds.filter(
|
||||
(n) => n !== study.study_id
|
||||
)
|
||||
} else {
|
||||
if (
|
||||
selected.length > 0 &&
|
||||
selected[0].directions.length !==
|
||||
study.directions.length
|
||||
) {
|
||||
enqueueSnackbar(
|
||||
"You can only compare studies that has the same number of objectives.",
|
||||
{
|
||||
variant: "info",
|
||||
}
|
||||
)
|
||||
next = selectedIds
|
||||
} else if (
|
||||
selected.length > 0 &&
|
||||
!isEqualDirections(
|
||||
selected[0].directions,
|
||||
study.directions
|
||||
)
|
||||
) {
|
||||
enqueueSnackbar(
|
||||
"You can only compare studies that has the same directions.",
|
||||
{
|
||||
variant: "info",
|
||||
}
|
||||
)
|
||||
next = selectedIds
|
||||
} else {
|
||||
next = [...selectedIds, study.study_id]
|
||||
}
|
||||
}
|
||||
history.push(getStudyListLink(next))
|
||||
} else {
|
||||
history.push(getStudyListLink([study.study_id]))
|
||||
}
|
||||
}}
|
||||
selected={
|
||||
selected.findIndex(
|
||||
(s) => s.study_id === study.study_id
|
||||
) !== -1
|
||||
}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={study.study_name} />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
color={theme.palette.grey.A400}
|
||||
sx={{ p: theme.spacing(0, 1) }}
|
||||
>
|
||||
{`# ${study.study_id}`}
|
||||
</Typography>
|
||||
<Chip
|
||||
color="primary"
|
||||
label={
|
||||
study.directions.length === 1
|
||||
? `${study.directions.length} objective`
|
||||
: `${study.directions.length} objectives`
|
||||
}
|
||||
sx={{ margin: theme.spacing(0) }}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
overflow: "auto",
|
||||
height: `calc(100vh - ${theme.spacing(8)})`,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}>
|
||||
<StudiesGraph studies={selected} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</AppDrawer>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const StudiesGraph: FC<{ studies: StudySummary[] }> = ({ studies }) => {
|
||||
const theme = useTheme()
|
||||
const action = actionCreator()
|
||||
const studyDetails = useRecoilValue<StudyDetails>(studyDetailsState)
|
||||
const [logScale, setLogScale] = useState<boolean>(false)
|
||||
const [includePruned, setIncludePruned] = useState<boolean>(true)
|
||||
|
||||
const handleLogScaleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setLogScale(!logScale)
|
||||
}
|
||||
|
||||
const handleIncludePrunedChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setIncludePruned(!includePruned)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
studies.forEach((study) => {
|
||||
action.updateStudyDetail(study.study_id)
|
||||
})
|
||||
}, [studies])
|
||||
|
||||
const showStudyDetails = studies.map((study) => studyDetails[study.study_id])
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", width: "100%", flexDirection: "column" }}>
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
padding: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={logScale}
|
||||
onChange={handleLogScaleChange}
|
||||
value="enable"
|
||||
/>
|
||||
}
|
||||
label="Log y scale"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={includePruned}
|
||||
onChange={handleIncludePrunedChange}
|
||||
value="enable"
|
||||
/>
|
||||
}
|
||||
label="Include PRUNED trials"
|
||||
/>
|
||||
</FormControl>
|
||||
{showStudyDetails !== null &&
|
||||
showStudyDetails.length > 0 &&
|
||||
showStudyDetails.every((s) => s) ? (
|
||||
<Card
|
||||
sx={{
|
||||
margin: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<GraphHistoryMultiStudies
|
||||
studies={showStudyDetails}
|
||||
betaIncludePruned={includePruned}
|
||||
betaLogScale={logScale}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
{showStudyDetails !== null &&
|
||||
showStudyDetails.length > 0 &&
|
||||
showStudyDetails.every((s) => s) ? (
|
||||
<Card
|
||||
sx={{
|
||||
margin: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<GraphEdfMultiStudies studies={showStudyDetails} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -12,11 +12,21 @@ import {
|
||||
Box,
|
||||
} from "@mui/material"
|
||||
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
|
||||
import { Target, useFilteredTrials, useObjectiveTargets } from "../trialFilter"
|
||||
import {
|
||||
Target,
|
||||
useFilteredTrials,
|
||||
useFilteredTrialsFromStudies,
|
||||
useObjectiveTargets,
|
||||
} from "../trialFilter"
|
||||
|
||||
const plotDomId = "graph-edf"
|
||||
const getPlotDomId = (objectiveId: number) => `graph-edf-${objectiveId}`
|
||||
|
||||
interface EdfPlotInfo {
|
||||
study_name: string
|
||||
trials: Trial[]
|
||||
}
|
||||
|
||||
export const GraphEdfBeta: FC<{
|
||||
study: StudyDetail | null
|
||||
objectiveId: number
|
||||
@@ -101,6 +111,69 @@ export const GraphEdf: FC<{
|
||||
)
|
||||
}
|
||||
|
||||
export const GraphEdfMultiStudies: FC<{
|
||||
studies: StudyDetail[]
|
||||
}> = ({ studies }) => {
|
||||
const theme = useTheme()
|
||||
const [targets, selected, setTarget] = useObjectiveTargets(
|
||||
studies.length !== 0 ? studies[0] : null
|
||||
)
|
||||
|
||||
const trials = useFilteredTrialsFromStudies(studies, [selected], false, false)
|
||||
const edfPlotInfos = studies.map((study, index) => {
|
||||
const e: EdfPlotInfo = {
|
||||
study_name: study?.name,
|
||||
trials: trials[index],
|
||||
}
|
||||
return e
|
||||
})
|
||||
|
||||
const handleObjectiveChange = (event: SelectChangeEvent<string>) => {
|
||||
setTarget(event.target.value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
plotEdfMultiStudies(edfPlotInfos, selected, plotDomId, theme.palette.mode)
|
||||
}, [studies, selected, theme.palette.mode])
|
||||
|
||||
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 }}
|
||||
>
|
||||
EDF
|
||||
</Typography>
|
||||
{studies.length > 0 && studies[0].directions.length !== 1 ? (
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Objective:</FormLabel>
|
||||
<Select
|
||||
value={selected.identifier()}
|
||||
onChange={handleObjectiveChange}
|
||||
>
|
||||
{targets.map((target, i) => (
|
||||
<MenuItem value={target.identifier()} key={i}>
|
||||
{target.toLabel(studies[0].objective_names)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
) : null}
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<Box id={plotDomId} sx={{ height: "450px" }} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const plotEdf = (
|
||||
trials: Trial[],
|
||||
target: Target,
|
||||
@@ -159,3 +232,62 @@ const plotEdf = (
|
||||
]
|
||||
plotly.react(domId, plotData, layout)
|
||||
}
|
||||
|
||||
const plotEdfMultiStudies = (
|
||||
edfPlotInfos: EdfPlotInfo[],
|
||||
target: Target,
|
||||
domId: string,
|
||||
mode: string
|
||||
) => {
|
||||
if (document.getElementById(domId) === null) {
|
||||
return
|
||||
}
|
||||
if (edfPlotInfos.length === 0) {
|
||||
plotly.react(domId, [], {
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
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 : {},
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
})
|
||||
plotly.react(domId, plotData, layout)
|
||||
}
|
||||
|
||||
@@ -18,12 +18,20 @@ import {
|
||||
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
|
||||
import {
|
||||
useFilteredTrials,
|
||||
useFilteredTrialsFromStudies,
|
||||
Target,
|
||||
useObjectiveAndUserAttrTargets,
|
||||
} from "../trialFilter"
|
||||
|
||||
const plotDomId = "graph-history"
|
||||
|
||||
interface HistoryPlotInfo {
|
||||
study_name: string
|
||||
trials: Trial[]
|
||||
directions: StudyDirection[]
|
||||
objective_names?: string[]
|
||||
}
|
||||
|
||||
export const GraphHistory: FC<{
|
||||
study: StudyDetail | null
|
||||
betaLogScale?: boolean
|
||||
@@ -203,6 +211,185 @@ export const GraphHistory: FC<{
|
||||
)
|
||||
}
|
||||
|
||||
export const GraphHistoryMultiStudies: FC<{
|
||||
studies: StudyDetail[]
|
||||
betaLogScale?: boolean
|
||||
betaIncludePruned?: boolean
|
||||
}> = ({ studies, betaLogScale, betaIncludePruned }) => {
|
||||
const theme = useTheme()
|
||||
const [xAxis, setXAxis] = useState<
|
||||
"number" | "datetime_start" | "datetime_complete"
|
||||
>("number")
|
||||
const [logScale, setLogScale] = useState<boolean>(false)
|
||||
const [filterCompleteTrial, setFilterCompleteTrial] = useState<boolean>(false)
|
||||
const [filterPrunedTrial, setFilterPrunedTrial] = useState<boolean>(false)
|
||||
|
||||
// TODO(umezawa): Prepare targets with all studies.
|
||||
const [targets, selected, setTarget] = useObjectiveAndUserAttrTargets(
|
||||
studies.length !== 0 ? studies[0] : null
|
||||
)
|
||||
|
||||
const trials = useFilteredTrialsFromStudies(
|
||||
studies,
|
||||
[selected],
|
||||
filterCompleteTrial,
|
||||
betaIncludePruned === undefined ? filterPrunedTrial : !betaIncludePruned
|
||||
)
|
||||
const historyPlotInfos = studies.map((study, index) => {
|
||||
const h: HistoryPlotInfo = {
|
||||
study_name: study?.name,
|
||||
trials: trials[index],
|
||||
directions: study?.directions,
|
||||
objective_names: study?.objective_names,
|
||||
}
|
||||
return h
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
plotHistoryMultiStudies(
|
||||
historyPlotInfos,
|
||||
selected,
|
||||
xAxis,
|
||||
betaLogScale === undefined ? logScale : betaLogScale,
|
||||
theme.palette.mode
|
||||
)
|
||||
}, [studies, selected, logScale, betaLogScale, xAxis, theme.palette.mode])
|
||||
|
||||
const handleObjectiveChange = (event: SelectChangeEvent<string>) => {
|
||||
setTarget(event.target.value)
|
||||
}
|
||||
|
||||
const handleXAxisChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.value === "number") {
|
||||
setXAxis("number")
|
||||
} else if (e.target.value === "datetime_start") {
|
||||
setXAxis("datetime_start")
|
||||
} else if (e.target.value === "datetime_complete") {
|
||||
setXAxis("datetime_complete")
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogScaleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setLogScale(!logScale)
|
||||
}
|
||||
|
||||
const handleFilterCompleteChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterCompleteTrial(!filterCompleteTrial)
|
||||
}
|
||||
|
||||
const handleFilterPrunedChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterPrunedTrial(!filterPrunedTrial)
|
||||
}
|
||||
|
||||
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 }}
|
||||
>
|
||||
History
|
||||
</Typography>
|
||||
{studies[0] !== null && targets.length >= 2 ? (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ marginBottom: theme.spacing(2) }}
|
||||
>
|
||||
<FormLabel component="legend">y Axis</FormLabel>
|
||||
<Select
|
||||
value={selected.identifier()}
|
||||
onChange={handleObjectiveChange}
|
||||
>
|
||||
{targets.map((t, i) => (
|
||||
<MenuItem value={t.identifier()} key={i}>
|
||||
{t.toLabel(studies[0].objective_names)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
) : null}
|
||||
{betaLogScale === undefined ? (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ marginBottom: theme.spacing(2) }}
|
||||
>
|
||||
<FormLabel component="legend">Log y scale:</FormLabel>
|
||||
<Switch
|
||||
checked={logScale}
|
||||
onChange={handleLogScaleChange}
|
||||
value="enable"
|
||||
/>
|
||||
</FormControl>
|
||||
) : null}
|
||||
{betaIncludePruned === undefined ? (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ marginBottom: theme.spacing(2) }}
|
||||
>
|
||||
<FormLabel component="legend">Filter state:</FormLabel>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={!filterCompleteTrial}
|
||||
onChange={handleFilterCompleteChange}
|
||||
/>
|
||||
}
|
||||
label="Complete"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={!filterPrunedTrial}
|
||||
disabled={!studies[0]?.has_intermediate_values}
|
||||
onChange={handleFilterPrunedChange}
|
||||
/>
|
||||
}
|
||||
label="Pruned"
|
||||
/>
|
||||
</FormControl>
|
||||
) : null}
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
sx={{ marginBottom: theme.spacing(2) }}
|
||||
>
|
||||
<FormLabel component="legend">X-axis:</FormLabel>
|
||||
<RadioGroup
|
||||
aria-label="gender"
|
||||
name="gender1"
|
||||
value={xAxis}
|
||||
onChange={handleXAxisChange}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="number"
|
||||
control={<Radio />}
|
||||
label="Number"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="datetime_start"
|
||||
control={<Radio />}
|
||||
label="Datetime start"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="datetime_complete"
|
||||
control={<Radio />}
|
||||
label="Datetime complete"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<div id={plotDomId} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const plotHistory = (
|
||||
trials: Trial[],
|
||||
directions: StudyDirection[],
|
||||
@@ -307,3 +494,118 @@ const plotHistory = (
|
||||
}
|
||||
plotly.react(plotDomId, plotData, layout)
|
||||
}
|
||||
|
||||
const plotHistoryMultiStudies = (
|
||||
historyPlotInfos: HistoryPlotInfo[],
|
||||
target: Target,
|
||||
xAxis: "number" | "datetime_start" | "datetime_complete",
|
||||
logScale: boolean,
|
||||
mode: string
|
||||
) => {
|
||||
if (document.getElementById(plotDomId) === null) {
|
||||
return
|
||||
}
|
||||
if (historyPlotInfos.length === 0) {
|
||||
plotly.react(plotDomId, [], {
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const layout: Partial<plotly.Layout> = {
|
||||
margin: {
|
||||
l: 50,
|
||||
t: 0,
|
||||
r: 50,
|
||||
b: 0,
|
||||
},
|
||||
yaxis: {
|
||||
title: target.toLabel(historyPlotInfos[0].objective_names),
|
||||
type: logScale ? "log" : "linear",
|
||||
},
|
||||
xaxis: {
|
||||
title: xAxis === "number" ? "Trial" : "Time",
|
||||
type: xAxis === "number" ? "linear" : "date",
|
||||
},
|
||||
showlegend: true,
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
}
|
||||
|
||||
const getAxisX = (trial: Trial): number | Date => {
|
||||
return xAxis === "number"
|
||||
? trial.number
|
||||
: xAxis === "datetime_start"
|
||||
? trial.datetime_start!
|
||||
: trial.datetime_complete!
|
||||
}
|
||||
|
||||
const plotData: Partial<plotly.PlotData>[] = []
|
||||
historyPlotInfos.forEach((h) => {
|
||||
const x = h.trials.map(getAxisX)
|
||||
const y = h.trials.map(
|
||||
(t: Trial): number => target.getTargetValue(t) as number
|
||||
)
|
||||
plotData.push({
|
||||
x: x,
|
||||
y: y,
|
||||
name: `${target.toLabel(h.objective_names)} of ${h.study_name}`,
|
||||
mode: "markers",
|
||||
type: "scatter",
|
||||
})
|
||||
|
||||
const objectiveId = target.getObjectiveId()
|
||||
if (objectiveId !== null) {
|
||||
const xForLinePlot: (number | Date)[] = []
|
||||
const yForLinePlot: number[] = []
|
||||
let currentBest: number | null = null
|
||||
for (let i = 0; i < h.trials.length; i++) {
|
||||
const t = h.trials[i]
|
||||
const value = target.getTargetValue(t) as number
|
||||
if (value === null) {
|
||||
continue
|
||||
} else if (currentBest === null) {
|
||||
currentBest = value
|
||||
xForLinePlot.push(getAxisX(t))
|
||||
yForLinePlot.push(value)
|
||||
} else if (
|
||||
h.directions[objectiveId] === "maximize" &&
|
||||
value > currentBest
|
||||
) {
|
||||
const p = h.trials[i - 1]
|
||||
if (!xForLinePlot.includes(getAxisX(p))) {
|
||||
xForLinePlot.push(getAxisX(p))
|
||||
yForLinePlot.push(currentBest)
|
||||
}
|
||||
currentBest = value
|
||||
xForLinePlot.push(getAxisX(t))
|
||||
yForLinePlot.push(value)
|
||||
} else if (
|
||||
h.directions[objectiveId] === "minimize" &&
|
||||
value < currentBest
|
||||
) {
|
||||
const p = h.trials[i - 1]
|
||||
if (!xForLinePlot.includes(getAxisX(p))) {
|
||||
xForLinePlot.push(getAxisX(p))
|
||||
yForLinePlot.push(currentBest)
|
||||
}
|
||||
currentBest = value
|
||||
xForLinePlot.push(getAxisX(t))
|
||||
yForLinePlot.push(value)
|
||||
}
|
||||
}
|
||||
if (h.trials.length !== 0) {
|
||||
xForLinePlot.push(getAxisX(h.trials[h.trials.length - 1]))
|
||||
yForLinePlot.push(yForLinePlot[yForLinePlot.length - 1])
|
||||
}
|
||||
plotData.push({
|
||||
x: xForLinePlot,
|
||||
y: yForLinePlot,
|
||||
name: `Best Value of ${h.study_name}`,
|
||||
mode: "lines",
|
||||
type: "scatter",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
plotly.react(plotDomId, plotData, layout)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Delete, Refresh, Search } from "@mui/icons-material"
|
||||
import SortIcon from "@mui/icons-material/Sort"
|
||||
import HomeIcon from "@mui/icons-material/Home"
|
||||
import AddBoxIcon from "@mui/icons-material/AddBox"
|
||||
import CompareIcon from "@mui/icons-material/Compare"
|
||||
import DriveFileRenameOutlineIcon from "@mui/icons-material/DriveFileRenameOutline"
|
||||
|
||||
import { actionCreator } from "../action"
|
||||
@@ -180,6 +181,15 @@ export const StudyListBeta: FC<{
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<CompareIcon />}
|
||||
component={Link}
|
||||
to={`${URL_PREFIX}/compare-studies`}
|
||||
sx={{ marginRight: theme.spacing(2), minWidth: "120px" }}
|
||||
>
|
||||
Compare
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -92,6 +92,29 @@ export class Target {
|
||||
}
|
||||
}
|
||||
|
||||
const filterTrials = (
|
||||
study: StudyDetail | null,
|
||||
targets: Target[],
|
||||
filterComplete: boolean,
|
||||
filterPruned: boolean
|
||||
): Trial[] => {
|
||||
if (study === null) {
|
||||
return []
|
||||
}
|
||||
return study.trials.filter((t) => {
|
||||
if (t.state !== "Complete" && t.state !== "Pruned") {
|
||||
return false
|
||||
}
|
||||
if (t.state === "Complete" && filterComplete) {
|
||||
return false
|
||||
}
|
||||
if (t.state === "Pruned" && filterPruned) {
|
||||
return false
|
||||
}
|
||||
return targets.every((target) => target.getTargetValue(t) !== null)
|
||||
})
|
||||
}
|
||||
|
||||
export const useFilteredTrials = (
|
||||
study: StudyDetail | null,
|
||||
targets: Target[],
|
||||
@@ -99,23 +122,21 @@ export const useFilteredTrials = (
|
||||
filterPruned: boolean
|
||||
): Trial[] =>
|
||||
useMemo<Trial[]>(() => {
|
||||
if (study === null) {
|
||||
return []
|
||||
}
|
||||
return study.trials.filter((t) => {
|
||||
if (t.state !== "Complete" && t.state !== "Pruned") {
|
||||
return false
|
||||
}
|
||||
if (t.state === "Complete" && filterComplete) {
|
||||
return false
|
||||
}
|
||||
if (t.state === "Pruned" && filterPruned) {
|
||||
return false
|
||||
}
|
||||
return targets.every((target) => target.getTargetValue(t) !== null)
|
||||
})
|
||||
return filterTrials(study, targets, filterComplete, filterPruned)
|
||||
}, [study?.trials, targets, filterComplete, filterPruned])
|
||||
|
||||
export const useFilteredTrialsFromStudies = (
|
||||
studies: StudyDetail[],
|
||||
targets: Target[],
|
||||
filterComplete: boolean,
|
||||
filterPruned: boolean
|
||||
): Trial[][] =>
|
||||
useMemo<Trial[][]>(() => {
|
||||
return studies.map((s) =>
|
||||
filterTrials(s, targets, filterComplete, filterPruned)
|
||||
)
|
||||
}, [studies, targets, filterComplete, filterPruned])
|
||||
|
||||
export const useObjectiveTargets = (
|
||||
study: StudyDetail | null
|
||||
): [Target[], Target, (ident: string) => void] => {
|
||||
|
||||
Reference in New Issue
Block a user