diff --git a/optuna_dashboard/ts/components/CompareStudies.tsx b/optuna_dashboard/ts/components/CompareStudies.tsx index a60e037e..692c826d 100644 --- a/optuna_dashboard/ts/components/CompareStudies.tsx +++ b/optuna_dashboard/ts/components/CompareStudies.tsx @@ -27,7 +27,7 @@ import { actionCreator } from "../action" import { studySummariesState, studyDetailsState } from "../state" import { AppDrawer } from "./AppDrawer" import { GraphEdfMultiStudies } from "./GraphEdf" -import { GraphHistoryMultiStudies } from "./GraphHistory" +import { GraphHistory } from "./GraphHistory" import { useNavigate, useLocation } from "react-router-dom" const useQuery = (): URLSearchParams => { @@ -313,7 +313,7 @@ const StudiesGraph: FC<{ studies: StudySummary[] }> = ({ studies }) => { }} > - = ({ study, logScale, includePruned }) => { - const theme = useTheme() - const [xAxis, setXAxis] = useState< - "number" | "datetime_start" | "datetime_complete" - >("number") - const [markerSize, setMarkerSize] = useState(5) - - const [targets, selected, setTarget] = useObjectiveAndUserAttrTargets(study) - const trials = useFilteredTrials(study, [selected], !includePruned) - - useEffect(() => { - if (study !== null) { - plotHistory( - trials, - study.directions, - selected, - xAxis, - logScale, - theme.palette.mode, - study?.objective_names, - markerSize - ) - } - }, [ - trials, - study?.directions, - selected, - logScale, - xAxis, - theme.palette.mode, - study?.objective_names, - markerSize, - ]) - - const handleObjectiveChange = (event: SelectChangeEvent) => { - setTarget(event.target.value) - } - - const handleXAxisChange = (e: ChangeEvent) => { - 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") - } - } - - return ( - - - - History - - {targets.length >= 2 ? ( - - y Axis - - - ) : null} - - X-axis: - - } - label="Number" - /> - } - label="Datetime start" - /> - } - label="Datetime complete" - /> - - - - Marker size: - { - // @ts-ignore - setMarkerSize(e.target.value as number) - }} - /> - - - -
- - - ) -} - -export const GraphHistoryMultiStudies: FC<{ studies: StudyDetail[] logScale: boolean includePruned: boolean @@ -177,10 +44,8 @@ export const GraphHistoryMultiStudies: FC<{ >("number") const [markerSize, setMarkerSize] = useState(5) - // TODO(umezawa): Prepare targets with all studies. - const [targets, selected, setTarget] = useObjectiveAndUserAttrTargets( - studies.length !== 0 ? studies[0] : null - ) + const [targets, selected, setTarget] = + useObjectiveAndUserAttrTargetsFromStudies(studies) const trials = useFilteredTrialsFromStudies( studies, @@ -198,7 +63,7 @@ export const GraphHistoryMultiStudies: FC<{ }) useEffect(() => { - plotHistoryMultiStudies( + plotHistory( historyPlotInfos, selected, xAxis, @@ -206,7 +71,7 @@ export const GraphHistoryMultiStudies: FC<{ theme.palette.mode, markerSize ) - }, [studies, selected, logScale, xAxis, theme.palette.mode]) + }, [studies, selected, logScale, xAxis, theme.palette.mode, markerSize]) const handleObjectiveChange = (event: SelectChangeEvent) => { setTarget(event.target.value) @@ -299,148 +164,18 @@ export const GraphHistoryMultiStudies: FC<{ -
+ ) } const plotHistory = ( - trials: Trial[], - directions: StudyDirection[], - target: Target, - xAxis: "number" | "datetime_start" | "datetime_complete", - logScale: boolean, - mode: string, - objectiveNames?: string[], - markerSize: number -) => { - if (document.getElementById(plotDomId) === null) { - return - } - - const layout: Partial = { - margin: { - l: 50, - t: 0, - r: 50, - b: 0, - }, - yaxis: { - title: target.toLabel(objectiveNames), - type: logScale ? "log" : "linear", - }, - xaxis: { - title: xAxis === "number" ? "Trial" : "Time", - type: xAxis === "number" ? "linear" : "date", - }, - showlegend: true, - uirevision: "true", - template: mode === "dark" ? plotlyDarkTemplate : {}, - } - if (trials.length === 0) { - plotly.react(plotDomId, [], layout) - return - } - - const feasibleTrials: Trial[] = [] - const infeasibleTrials: Trial[] = [] - trials.forEach((t) => { - if (t.constraints.every((c) => c <= 0)) { - feasibleTrials.push(t) - } else { - infeasibleTrials.push(t) - } - }) - - const getAxisX = (trial: Trial): number | Date => { - return xAxis === "number" - ? trial.number - : xAxis === "datetime_start" - ? trial.datetime_start! - : trial.datetime_complete! - } - - const plotData: Partial[] = [ - { - x: feasibleTrials.map(getAxisX), - y: feasibleTrials.map( - (t: Trial): number => target.getTargetValue(t) as number - ), - name: target.toLabel(objectiveNames), - marker: { - size: markerSize, - }, - 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 < feasibleTrials.length; i++) { - const t = feasibleTrials[i] - if (currentBest === null) { - currentBest = t.values![objectiveId] as number - xForLinePlot.push(getAxisX(t)) - yForLinePlot.push(t.values![objectiveId] as number) - } else if ( - directions[objectiveId] === "maximize" && - t.values![objectiveId] > currentBest - ) { - const p = trials[i - 1] - if (!xForLinePlot.includes(getAxisX(p))) { - xForLinePlot.push(getAxisX(p)) - yForLinePlot.push(currentBest) - } - currentBest = t.values![objectiveId] as number - xForLinePlot.push(getAxisX(t)) - yForLinePlot.push(t.values![objectiveId] as number) - } else if ( - directions[objectiveId] === "minimize" && - t.values![objectiveId] < currentBest - ) { - const p = feasibleTrials[i - 1] - if (!xForLinePlot.includes(getAxisX(p))) { - xForLinePlot.push(getAxisX(p)) - yForLinePlot.push(currentBest) - } - currentBest = t.values![objectiveId] as number - xForLinePlot.push(getAxisX(t)) - yForLinePlot.push(t.values![objectiveId] as number) - } - } - xForLinePlot.push(getAxisX(trials[trials.length - 1])) - yForLinePlot.push(yForLinePlot[yForLinePlot.length - 1]) - plotData.push({ - x: xForLinePlot, - y: yForLinePlot, - name: "Best Value", - mode: "lines", - type: "scatter", - }) - } - plotData.push({ - x: infeasibleTrials.map(getAxisX), - y: infeasibleTrials.map( - (t: Trial): number => target.getTargetValue(t) as number - ), - name: "Infeasible Trial", - marker: { - size: markerSize, - color: mode === "dark" ? "#666666" : "#cccccc", - }, - mode: "markers", - type: "scatter", - showlegend: false, - }) - plotly.react(plotDomId, plotData, layout) -} - -const plotHistoryMultiStudies = ( historyPlotInfos: HistoryPlotInfo[], target: Target, xAxis: "number" | "datetime_start" | "datetime_complete", diff --git a/optuna_dashboard/ts/components/StudyHistory.tsx b/optuna_dashboard/ts/components/StudyHistory.tsx index c39f3e95..ce30fc44 100644 --- a/optuna_dashboard/ts/components/StudyHistory.tsx +++ b/optuna_dashboard/ts/components/StudyHistory.tsx @@ -96,7 +96,7 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { > diff --git a/optuna_dashboard/ts/trialFilter.ts b/optuna_dashboard/ts/trialFilter.ts index 96fb308f..4fe7e35a 100644 --- a/optuna_dashboard/ts/trialFilter.ts +++ b/optuna_dashboard/ts/trialFilter.ts @@ -192,3 +192,61 @@ export const useObjectiveAndUserAttrTargets = ( ) return [targetList, selectedTarget, setTargetIdent] } + +export const useObjectiveAndUserAttrTargetsFromStudies = ( + studies: StudyDetail[] +): [Target[], Target, (ident: string) => void] => { + const defaultTarget = new Target("objective", 0) + const [selected, setTargetIdent] = useState( + defaultTarget.identifier() + ) + const minDirections = useMemo(() => { + if (studies.length === 0) { + return 0 + } + return studies.reduce((acc, study) => { + return Math.min(acc, study.directions.length) + }, Number.MAX_VALUE) + }, [studies]) + + const intersect = (arrays: AttributeSpec[][]) => { + const atrEqual = (obj1: AttributeSpec, obj2: AttributeSpec) => { + return obj1.key === obj2.key + } + return arrays.reduce((a, b) => + a.filter((c) => b.some((d) => atrEqual(c, d))) + ) + } + + const attrTargets = useMemo(() => { + if (studies.length === 0) { + return [] + } + const intersection = intersect( + studies.map((study) => study.union_user_attrs) + ) + return intersection + .filter((attr) => attr.sortable) + .map((attr) => new Target("user_attr", attr.key)) + }, [studies]) + + const targetList = useMemo(() => { + if (studies !== null) { + return [ + ...Array.from( + { length: minDirections }, + (_, i) => new Target("objective", i) + ), + ...attrTargets, + ] + } else { + return [defaultTarget] + } + }, [minDirections, attrTargets]) + + const selectedTarget = useMemo( + () => targetList.find((t) => t.identifier() === selected) || defaultTarget, + [targetList, selected] + ) + return [targetList, selectedTarget, setTargetIdent] +}