From 2d74da3ab4aed1c272cccd2194b17bd0c0c71ecc Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Thu, 1 Aug 2024 17:56:46 +0900 Subject: [PATCH 1/8] Save --- tslib/react/src/components/PlotEdf.tsx | 2 +- tslib/react/src/components/PlotHistory.tsx | 288 ++++++++++++--------- 2 files changed, 163 insertions(+), 127 deletions(-) diff --git a/tslib/react/src/components/PlotEdf.tsx b/tslib/react/src/components/PlotEdf.tsx index 65281dcf..f1cc5aa7 100644 --- a/tslib/react/src/components/PlotEdf.tsx +++ b/tslib/react/src/components/PlotEdf.tsx @@ -4,7 +4,7 @@ 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 { GraphContainer } from"./GraphContainer" import { plotlyDarkTemplate } from "./PlotlyDarkMode" export type EdfPlotInfo = { diff --git a/tslib/react/src/components/PlotHistory.tsx b/tslib/react/src/components/PlotHistory.tsx index 1ea68db3..ed0728a8 100644 --- a/tslib/react/src/components/PlotHistory.tsx +++ b/tslib/react/src/components/PlotHistory.tsx @@ -15,60 +15,94 @@ import { } from "@mui/material" import * as Optuna from "@optuna/types" import * as plotly from "plotly.js-dist-min" -import { ChangeEvent, FC, useEffect, useState } from "react" -import { plotlyDarkTemplate } from "./PlotlyDarkMode" +import { ChangeEvent, FC, useEffect, useState, useMemo } from "react" + +import { useGraphComponentState } from "../hooks/useGraphComponentState" +import { Target, useFilteredTrialsFromStudies } from "../utils/trialFilter" const plotDomId = "plot-history" +interface HistoryPlotInfo { + study_name: string + trials: Optuna.Trial[] + directions: Optuna.StudyDirection[] + metric_names?: string[] +} + export const PlotHistory: FC<{ - study: Optuna.Study | null -}> = ({ study = null }) => { + studies: Optuna.Study[] +}> = ({ studies }) => { + const { graphComponentState, notifyGraphDidRender } = useGraphComponentState() + const theme = useTheme() - const [xAxis, setXAxis] = useState("number") + + const [xAxis, setXAxis] = useState< + "number" | "datetime_start" | "datetime_complete" + >("number") + const [objectiveId, setObjectiveId] = useState(0) const [logScale, setLogScale] = useState(false) - const [filterCompleteTrial, setFilterCompleteTrial] = useState(false) const [filterPrunedTrial, setFilterPrunedTrial] = useState(false) + const [markerSize, setMarkerSize] = useState(5) + + const target = useMemo( + () => new Target("objective", objectiveId), + [objectiveId] + ) + const trials = useFilteredTrialsFromStudies( + studies, + [target], + filterPrunedTrial, + ) + const historyPlotInfos = studies.map((study, index) => { + const h: HistoryPlotInfo = { + study_name: study.name, + trials: trials[index], + directions: study.directions, + metric_names: study.metric_names, + } + return h + }) const handleObjectiveChange = (event: SelectChangeEvent) => { setObjectiveId(event.target.value as number) } const handleXAxisChange = (e: ChangeEvent) => { - setXAxis(e.target.value) + 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 = () => { setLogScale(!logScale) } - const handleFilterCompleteChange = () => { - setFilterCompleteTrial(!filterCompleteTrial) - } - const handleFilterPrunedChange = () => { setFilterPrunedTrial(!filterPrunedTrial) } useEffect(() => { - if (study !== null) { + if (graphComponentState !== "componentWillMount") { plotHistory( - study, - objectiveId, + historyPlotInfos, + target, xAxis, logScale, - filterCompleteTrial, - filterPrunedTrial, - theme.palette.mode + theme.palette.mode, + colorTheme, + ) } }, [ - study, - objectiveId, + studies, + target, logScale, xAxis, - filterPrunedTrial, - filterCompleteTrial, theme.palette.mode, ]) @@ -171,32 +205,24 @@ export const PlotHistory: FC<{ ) } -const filterFunc = (trial: Optuna.Trial, objectiveId: number): boolean => { - if (trial.state !== "Complete" && trial.state !== "Pruned") { - return false - } - if (trial.values === undefined) { - return false - } - return ( - trial.values.length > objectiveId && - trial.values[objectiveId] !== Infinity && - trial.values[objectiveId] !== -Infinity - ) -} - const plotHistory = ( - study: Optuna.Study, - objectiveId: number, - xAxis: string, + historyPlotInfos: HistoryPlotInfo[], + target: Target, + xAxis: "number" | "datetime_start" | "datetime_complete", logScale: boolean, - filterCompleteTrial: boolean, - filterPrunedTrial: boolean, - mode: string + mode: string, + colorTheme: Partial, + markerSize: number ) => { if (document.getElementById(plotDomId) === null) { return } + if (historyPlotInfos.length === 0) { + plotly.react(plotDomId, [], { + template: colorTheme, + }) + return + } const layout: Partial = { margin: { @@ -206,27 +232,19 @@ const plotHistory = ( b: 0, }, yaxis: { - title: "Objective Value", + 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 : {}, - } - - let filteredTrials = study.trials.filter((t) => filterFunc(t, objectiveId)) - if (filterCompleteTrial) { - filteredTrials = filteredTrials.filter((t) => t.state !== "Complete") - } - if (filterPrunedTrial) { - filteredTrials = filteredTrials.filter((t) => t.state !== "Pruned") - } - if (filteredTrials.length === 0) { - plotly.react(plotDomId, [], layout) - return + showlegend: historyPlotInfos.length === 1 ? false : true, + template: colorTheme, + legend: { + x: 1.0, + y: 0.95, + }, } const getAxisX = (trial: Optuna.Trial): number | Date => { @@ -237,80 +255,98 @@ const plotHistory = ( : trial.datetime_complete ?? new Date() } - const getValue = ( - trial: Optuna.Trial, - objectiveId: number - ): number | null => { - if ( - objectiveId === null || - trial.values === undefined || - trial.values.length <= objectiveId - ) { - return null - } - const value = trial.values[objectiveId] - if (value === Infinity || value === -Infinity) { - return null - } - return value - } - - const xForLinePlot: (number | Date)[] = [] - const yForLinePlot: number[] = [] - let currentBest: number | null = null - for (let i = 0; i < filteredTrials.length; i++) { - const t = filteredTrials[i] - const v = getValue(t, objectiveId) as number - if (currentBest === null) { - currentBest = v - xForLinePlot.push(getAxisX(t)) - yForLinePlot.push(v) - } else if ( - study.directions[objectiveId] === "maximize" && - v > currentBest - ) { - const p = filteredTrials[i - 1] - if (!xForLinePlot.includes(getAxisX(p))) { - xForLinePlot.push(getAxisX(p)) - yForLinePlot.push(currentBest) + const plotData: Partial[] = [] + const infeasiblePlotData: Partial[] = [] + historyPlotInfos.forEach((h) => { + const feasibleTrials: Optuna.Trial[] = [] + const infeasibleTrials: Optuna.Trial[] = [] + h.trials.forEach((t) => { + if (t.constraints.every((c) => c <= 0)) { + feasibleTrials.push(t) + } else { + infeasibleTrials.push(t) } - currentBest = v - xForLinePlot.push(getAxisX(t)) - yForLinePlot.push(v) - } else if ( - study.directions[objectiveId] === "minimize" && - v < currentBest - ) { - const p = filteredTrials[i - 1] - if (!xForLinePlot.includes(getAxisX(p))) { - xForLinePlot.push(getAxisX(p)) - yForLinePlot.push(currentBest) - } - currentBest = v - xForLinePlot.push(getAxisX(t)) - yForLinePlot.push(v) - } - } - xForLinePlot.push(getAxisX(filteredTrials[filteredTrials.length - 1])) - yForLinePlot.push(yForLinePlot[yForLinePlot.length - 1]) - - const plotData: Partial[] = [ - { - x: filteredTrials.map(getAxisX), - y: filteredTrials.map( - (t: Optuna.Trial): number => getValue(t, objectiveId) as number + }) + plotData.push({ + x: feasibleTrials.map(getAxisX), + y: feasibleTrials.map( + (t: Optuna.Trial): number => target.getTargetValue(t) as number ), - name: "Objective Value", + name: `${target.toLabel(h.objective_names)} of ${h.study_name}`, + marker: { + size: markerSize, + }, mode: "markers", type: "scatter", - }, - { - x: xForLinePlot, - y: yForLinePlot, - name: "Best Value", - mode: "lines", + }) + + 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] + const value = target.getTargetValue(t) as number + if (t.state !== "Complete") { + continue + } else if (currentBest === null) { + currentBest = value + xForLinePlot.push(getAxisX(t)) + yForLinePlot.push(value) + } else if ( + h.directions[objectiveId] === "maximize" && + value > currentBest + ) { + const p = feasibleTrials[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 = feasibleTrials[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", + }) + } + infeasiblePlotData.push({ + x: infeasibleTrials.map(getAxisX), + y: infeasibleTrials.map( + (t: Optuna.Trial): number => target.getTargetValue(t) as number + ), + name: `Infeasible Trial of ${h.study_name}`, + marker: { + size: markerSize, + color: mode === "dark" ? "#666666" : "#cccccc", + }, + mode: "markers", type: "scatter", - }, - ] + showlegend: false, + }) + }) + plotData.push(...infeasiblePlotData) plotly.react(plotDomId, plotData, layout) } From 270c406425c983468218f7fb790c599dc8b43a87 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Thu, 15 Aug 2024 14:07:10 +0900 Subject: [PATCH 2/8] Add plotly color theme to PlotImportance --- tslib/react/src/components/PlotEdf.tsx | 2 +- tslib/react/src/components/PlotHistory.tsx | 152 +++++++++------------ 2 files changed, 65 insertions(+), 89 deletions(-) diff --git a/tslib/react/src/components/PlotEdf.tsx b/tslib/react/src/components/PlotEdf.tsx index f1cc5aa7..65281dcf 100644 --- a/tslib/react/src/components/PlotEdf.tsx +++ b/tslib/react/src/components/PlotEdf.tsx @@ -4,7 +4,7 @@ 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 { GraphContainer } from "./GraphContainer" import { plotlyDarkTemplate } from "./PlotlyDarkMode" export type EdfPlotInfo = { diff --git a/tslib/react/src/components/PlotHistory.tsx b/tslib/react/src/components/PlotHistory.tsx index ed0728a8..6e235997 100644 --- a/tslib/react/src/components/PlotHistory.tsx +++ b/tslib/react/src/components/PlotHistory.tsx @@ -1,5 +1,4 @@ import { - Checkbox, FormControl, FormControlLabel, FormLabel, @@ -9,16 +8,21 @@ import { RadioGroup, Select, SelectChangeEvent, - Switch, + Slider, Typography, useTheme, } from "@mui/material" import * as Optuna from "@optuna/types" import * as plotly from "plotly.js-dist-min" -import { ChangeEvent, FC, useEffect, useState, useMemo } from "react" +import { ChangeEvent, FC, useEffect, useState } from "react" import { useGraphComponentState } from "../hooks/useGraphComponentState" -import { Target, useFilteredTrialsFromStudies } from "../utils/trialFilter" +import { + Target, + useFilteredTrialsFromStudies, + useObjectiveAndUserAttrTargetsFromStudies, +} from "../utils/trialFilter" +import { plotlyDarkTemplate } from "./PlotlyDarkMode" const plotDomId = "plot-history" @@ -31,29 +35,31 @@ interface HistoryPlotInfo { export const PlotHistory: FC<{ studies: Optuna.Study[] -}> = ({ studies }) => { + colorTheme?: Partial +}> = ({ studies, colorTheme }) => { + const logScale = false + const filterPrunedTrial = false + const { graphComponentState, notifyGraphDidRender } = useGraphComponentState() const theme = useTheme() + const colorThemeUsed = + colorTheme ?? (theme.palette.mode === "dark" ? plotlyDarkTemplate : {}) const [xAxis, setXAxis] = useState< "number" | "datetime_start" | "datetime_complete" >("number") - - const [objectiveId, setObjectiveId] = useState(0) - const [logScale, setLogScale] = useState(false) - const [filterPrunedTrial, setFilterPrunedTrial] = useState(false) const [markerSize, setMarkerSize] = useState(5) - const target = useMemo( - () => new Target("objective", objectiveId), - [objectiveId] - ) + const [targets, selected, setTarget] = + useObjectiveAndUserAttrTargetsFromStudies(studies) + const trials = useFilteredTrialsFromStudies( studies, - [target], - filterPrunedTrial, + [selected], + filterPrunedTrial ) + const historyPlotInfos = studies.map((study, index) => { const h: HistoryPlotInfo = { study_name: study.name, @@ -64,8 +70,8 @@ export const PlotHistory: FC<{ return h }) - const handleObjectiveChange = (event: SelectChangeEvent) => { - setObjectiveId(event.target.value as number) + const handleObjectiveChange = (event: SelectChangeEvent) => { + setTarget(event.target.value) } const handleXAxisChange = (e: ChangeEvent) => { @@ -78,33 +84,18 @@ export const PlotHistory: FC<{ } } - const handleLogScaleChange = () => { - setLogScale(!logScale) - } - - const handleFilterPrunedChange = () => { - setFilterPrunedTrial(!filterPrunedTrial) - } - useEffect(() => { if (graphComponentState !== "componentWillMount") { plotHistory( historyPlotInfos, - target, + selected, xAxis, logScale, - theme.palette.mode, - colorTheme, - + colorThemeUsed, + markerSize ) } - }, [ - studies, - target, - logScale, - xAxis, - theme.palette.mode, - ]) + }, [graphComponentState, studies, selected, logScale, xAxis, colorThemeUsed, markerSize]) return ( @@ -115,60 +106,30 @@ export const PlotHistory: FC<{ direction="column" sx={{ paddingRight: theme.spacing(2) }} > - + History - {study !== null && study.directions.length !== 1 ? ( + {studies[0] !== null && targets.length >= 2 ? ( - Objective ID: - + {targets.map((t, i) => ( + + {t.toLabel(studies[0].metric_names)} ))} ) : null} - - Log y scale: - - - - Filter state: - - } - label="Complete" - /> - - } - label="Pruned" - /> - + + Marker size: + { + // @ts-ignore + setMarkerSize(e.target.value as number) + }} + /> +
@@ -210,7 +185,6 @@ const plotHistory = ( target: Target, xAxis: "number" | "datetime_start" | "datetime_complete", logScale: boolean, - mode: string, colorTheme: Partial, markerSize: number ) => { @@ -232,7 +206,7 @@ const plotHistory = ( b: 0, }, yaxis: { - title: target.toLabel(historyPlotInfos[0].objective_names), + title: target.toLabel(historyPlotInfos[0].metric_names), type: logScale ? "log" : "linear", }, xaxis: { @@ -257,22 +231,22 @@ const plotHistory = ( const plotData: Partial[] = [] const infeasiblePlotData: Partial[] = [] - historyPlotInfos.forEach((h) => { + for (const h of historyPlotInfos) { const feasibleTrials: Optuna.Trial[] = [] const infeasibleTrials: Optuna.Trial[] = [] - h.trials.forEach((t) => { + for (const t of h.trials) { if (t.constraints.every((c) => c <= 0)) { feasibleTrials.push(t) } else { infeasibleTrials.push(t) } - }) + } plotData.push({ x: feasibleTrials.map(getAxisX), y: feasibleTrials.map( (t: Optuna.Trial): number => target.getTargetValue(t) as number ), - name: `${target.toLabel(h.objective_names)} of ${h.study_name}`, + name: `${target.toLabel(h.metric_names)} of ${h.study_name}`, marker: { size: markerSize, }, @@ -290,7 +264,9 @@ const plotHistory = ( const value = target.getTargetValue(t) as number if (t.state !== "Complete") { continue - } else if (currentBest === null) { + } + + if (currentBest === null) { currentBest = value xForLinePlot.push(getAxisX(t)) yForLinePlot.push(value) @@ -340,13 +316,13 @@ const plotHistory = ( name: `Infeasible Trial of ${h.study_name}`, marker: { size: markerSize, - color: mode === "dark" ? "#666666" : "#cccccc", + color: colorTheme === plotlyDarkTemplate ? "#666666" : "#cccccc", }, mode: "markers", type: "scatter", showlegend: false, }) - }) + } plotData.push(...infeasiblePlotData) plotly.react(plotDomId, plotData, layout) } From c1802ae6c2d5d77cf1b11645c9476a63cc3e962f Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Thu, 15 Aug 2024 14:27:20 +0900 Subject: [PATCH 3/8] Apply PlotHistory to standalone --- standalone_app/src/components/StudyDetail.tsx | 2 +- .../src/components/PlotHistory.stories.tsx | 3 ++- tslib/react/src/components/PlotHistory.tsx | 18 +++++++++++++----- .../PlotParallelCoordinate.stories.tsx | 3 ++- tslib/react/test/PlotHistory.test.tsx | 2 +- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/standalone_app/src/components/StudyDetail.tsx b/standalone_app/src/components/StudyDetail.tsx index ec37a2a0..42715824 100644 --- a/standalone_app/src/components/StudyDetail.tsx +++ b/standalone_app/src/components/StudyDetail.tsx @@ -173,7 +173,7 @@ export const StudyDetail: FC<{ - + {!!study && } diff --git a/tslib/react/src/components/PlotHistory.stories.tsx b/tslib/react/src/components/PlotHistory.stories.tsx index f274249e..159d836b 100644 --- a/tslib/react/src/components/PlotHistory.stories.tsx +++ b/tslib/react/src/components/PlotHistory.stories.tsx @@ -14,12 +14,13 @@ const meta: Meta = { (Story, storyContext) => { const { study } = useMockStudy(storyContext.parameters?.studyId) if (!study) return

loading...

+ const studies = [study] return ( diff --git a/tslib/react/src/components/PlotHistory.tsx b/tslib/react/src/components/PlotHistory.tsx index 6e235997..5bac6a18 100644 --- a/tslib/react/src/components/PlotHistory.tsx +++ b/tslib/react/src/components/PlotHistory.tsx @@ -84,6 +84,7 @@ export const PlotHistory: FC<{ } } + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { if (graphComponentState !== "componentWillMount") { plotHistory( @@ -93,9 +94,16 @@ export const PlotHistory: FC<{ logScale, colorThemeUsed, markerSize - ) + )?.then(notifyGraphDidRender) } - }, [graphComponentState, studies, selected, logScale, xAxis, colorThemeUsed, markerSize]) + }, [ + historyPlotInfos, + selected, + xAxis, + colorThemeUsed, + markerSize, + graphComponentState, + ]) return ( @@ -122,8 +130,8 @@ export const PlotHistory: FC<{ value={selected.identifier()} onChange={handleObjectiveChange} > - {targets.map((t, i) => ( - + {targets.map((t) => ( + {t.toLabel(studies[0].metric_names)} ))} @@ -324,5 +332,5 @@ const plotHistory = ( }) } plotData.push(...infeasiblePlotData) - plotly.react(plotDomId, plotData, layout) + return plotly.react(plotDomId, plotData, layout) } diff --git a/tslib/react/src/components/PlotParallelCoordinate.stories.tsx b/tslib/react/src/components/PlotParallelCoordinate.stories.tsx index 42443339..58657abc 100644 --- a/tslib/react/src/components/PlotParallelCoordinate.stories.tsx +++ b/tslib/react/src/components/PlotParallelCoordinate.stories.tsx @@ -14,12 +14,13 @@ const meta: Meta = { (Story, storyContext) => { const { study } = useMockStudy(storyContext.parameters?.studyId) if (!study) return

loading...

+ const studies = [study] return ( diff --git a/tslib/react/test/PlotHistory.test.tsx b/tslib/react/test/PlotHistory.test.tsx index 3d121fae..15258406 100644 --- a/tslib/react/test/PlotHistory.test.tsx +++ b/tslib/react/test/PlotHistory.test.tsx @@ -18,7 +18,7 @@ describe("PlotHistory Tests", async () => { }) =>
{children}
return render( - + ) } From bcf570227e1ab1e323e63eef70ebc2c9ff33f96e Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Tue, 20 Aug 2024 23:01:37 +0900 Subject: [PATCH 4/8] Apply PlotHistory to optuna-dashboard --- .../ts/components/GraphHistory.tsx | 368 +----------------- 1 file changed, 5 insertions(+), 363 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphHistory.tsx b/optuna_dashboard/ts/components/GraphHistory.tsx index 8a7fe3a9..4eb7a2cc 100644 --- a/optuna_dashboard/ts/components/GraphHistory.tsx +++ b/optuna_dashboard/ts/components/GraphHistory.tsx @@ -1,373 +1,15 @@ -import { - Box, - FormControl, - FormControlLabel, - FormLabel, - Grid, - MenuItem, - Radio, - RadioGroup, - Select, - SelectChangeEvent, - Slider, - Typography, - useTheme, -} from "@mui/material" -import { - Target, - useFilteredTrialsFromStudies, - useObjectiveAndUserAttrTargetsFromStudies, -} 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 { useTheme } from "@mui/material" +import { PlotHistory } from "@optuna/react" +import React, { FC } from "react" import { StudyDetail } from "ts/types/optuna" -import { useConstants } from "../constantsProvider" import { usePlotlyColorTheme } from "../state" -const plotDomId = "graph-history" - -interface HistoryPlotInfo { - study_name: string - trials: Optuna.Trial[] - directions: Optuna.StudyDirection[] - metric_names?: string[] -} - export const GraphHistory: FC<{ studies: StudyDetail[] logScale: boolean includePruned: boolean -}> = ({ studies, logScale, includePruned }) => { - const { url_prefix } = useConstants() - +}> = ({ studies }) => { const theme = useTheme() const colorTheme = usePlotlyColorTheme(theme.palette.mode) - const navigate = useNavigate() - const [xAxis, setXAxis] = useState< - "number" | "datetime_start" | "datetime_complete" - >("number") - const [markerSize, setMarkerSize] = useState(5) - - const [targets, selected, setTarget] = - useObjectiveAndUserAttrTargetsFromStudies(studies) - - const trials = useFilteredTrialsFromStudies( - studies, - [selected], - !includePruned - ) - const historyPlotInfos = studies.map((study, index) => { - const h: HistoryPlotInfo = { - study_name: study?.name, - trials: trials[index], - directions: study?.directions, - metric_names: study?.metric_names, - } - return h - }) - - useEffect(() => { - plotHistory( - historyPlotInfos, - selected, - xAxis, - logScale, - theme.palette.mode, - colorTheme, - markerSize - ) - const element = document.getElementById(plotDomId) - if (element !== null && studies.length >= 1) { - // @ts-ignore - element.on("plotly_click", (data) => { - if (data.points[0].data.mode !== "lines") { - let studyId = 1 - if (data.points[0].data.name.includes("Infeasible Trial of")) { - const studyInfo: { id: number; name: string }[] = [] - studies.forEach((study) => { - studyInfo.push({ id: study.id, name: study.name }) - }) - const dataPointStudyName = data.points[0].data.name.replace( - "Infeasible Trial of ", - "" - ) - const targetId = studyInfo.find( - (s) => s.name === dataPointStudyName - )?.id - if (targetId !== undefined) { - studyId = targetId - } - } else { - studyId = studies[Math.floor(data.points[0].curveNumber / 2)].id - } - navigate( - url_prefix + - `/studies/${studyId}/trials?numbers=${data.points[0].x}` - ) - } - }) - return () => { - // @ts-ignore - element.removeAllListeners("plotly_click") - } - } - }, [ - studies, - selected, - logScale, - xAxis, - theme.palette.mode, - colorTheme, - 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 - - {studies[0] !== null && 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) - }} - /> - - - - - - - ) -} - -const plotHistory = ( - historyPlotInfos: HistoryPlotInfo[], - target: Target, - xAxis: "number" | "datetime_start" | "datetime_complete", - logScale: boolean, - mode: string, - colorTheme: Partial, - markerSize: number -) => { - if (document.getElementById(plotDomId) === null) { - return - } - if (historyPlotInfos.length === 0) { - plotly.react(plotDomId, [], { - template: colorTheme, - }) - return - } - - const layout: Partial = { - margin: { - l: 50, - t: 0, - r: 50, - b: 0, - }, - yaxis: { - title: target.toLabel(historyPlotInfos[0].metric_names), - type: logScale ? "log" : "linear", - }, - xaxis: { - title: xAxis === "number" ? "Trial" : "Time", - type: xAxis === "number" ? "linear" : "date", - }, - showlegend: historyPlotInfos.length === 1 ? false : true, - template: colorTheme, - legend: { - x: 1.0, - y: 0.95, - }, - } - - const getAxisX = (trial: Optuna.Trial): number | Date => { - return xAxis === "number" - ? trial.number - : xAxis === "datetime_start" - ? trial.datetime_start ?? new Date() - : trial.datetime_complete ?? new Date() - } - - const plotData: Partial[] = [] - const infeasiblePlotData: Partial[] = [] - historyPlotInfos.forEach((h) => { - const feasibleTrials: Optuna.Trial[] = [] - const infeasibleTrials: Optuna.Trial[] = [] - h.trials.forEach((t) => { - if (t.constraints.every((c) => c <= 0)) { - feasibleTrials.push(t) - } else { - infeasibleTrials.push(t) - } - }) - plotData.push({ - x: feasibleTrials.map(getAxisX), - y: feasibleTrials.map( - (t: Optuna.Trial): number => target.getTargetValue(t) as number - ), - name: `${target.toLabel(h.metric_names)} of ${h.study_name}`, - 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] - const value = target.getTargetValue(t) as number - if (t.state !== "Complete") { - continue - } else if (currentBest === null) { - currentBest = value - xForLinePlot.push(getAxisX(t)) - yForLinePlot.push(value) - } else if ( - h.directions[objectiveId] === "maximize" && - value > currentBest - ) { - const p = feasibleTrials[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 = feasibleTrials[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", - }) - } - infeasiblePlotData.push({ - x: infeasibleTrials.map(getAxisX), - y: infeasibleTrials.map( - (t: Optuna.Trial): number => target.getTargetValue(t) as number - ), - name: `Infeasible Trial of ${h.study_name}`, - marker: { - size: markerSize, - color: mode === "dark" ? "#666666" : "#cccccc", - }, - mode: "markers", - type: "scatter", - showlegend: false, - }) - }) - plotData.push(...infeasiblePlotData) - plotly.react(plotDomId, plotData, layout) + return } From 947c5bb7621e9c1a07f3f54b2c21f309ec800ec7 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Tue, 20 Aug 2024 23:33:17 +0900 Subject: [PATCH 5/8] Add logScale as optional --- .../ts/components/GraphHistory.tsx | 10 +++++-- tslib/react/src/components/PlotHistory.tsx | 29 +++++++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphHistory.tsx b/optuna_dashboard/ts/components/GraphHistory.tsx index 4eb7a2cc..c1b3b40d 100644 --- a/optuna_dashboard/ts/components/GraphHistory.tsx +++ b/optuna_dashboard/ts/components/GraphHistory.tsx @@ -8,8 +8,14 @@ export const GraphHistory: FC<{ studies: StudyDetail[] logScale: boolean includePruned: boolean -}> = ({ studies }) => { +}> = ({ studies, logScale }) => { const theme = useTheme() const colorTheme = usePlotlyColorTheme(theme.palette.mode) - return + return ( + + ) } diff --git a/tslib/react/src/components/PlotHistory.tsx b/tslib/react/src/components/PlotHistory.tsx index 5bac6a18..d04aa716 100644 --- a/tslib/react/src/components/PlotHistory.tsx +++ b/tslib/react/src/components/PlotHistory.tsx @@ -9,6 +9,7 @@ import { Select, SelectChangeEvent, Slider, + Switch, Typography, useTheme, } from "@mui/material" @@ -35,9 +36,9 @@ interface HistoryPlotInfo { export const PlotHistory: FC<{ studies: Optuna.Study[] + logScale?: boolean colorTheme?: Partial -}> = ({ studies, colorTheme }) => { - const logScale = false +}> = ({ studies, logScale, colorTheme }) => { const filterPrunedTrial = false const { graphComponentState, notifyGraphDidRender } = useGraphComponentState() @@ -49,6 +50,9 @@ export const PlotHistory: FC<{ const [xAxis, setXAxis] = useState< "number" | "datetime_start" | "datetime_complete" >("number") + + const [logScaleInternal, setLogScaleInternal] = useState(false) + const [markerSize, setMarkerSize] = useState(5) const [targets, selected, setTarget] = @@ -74,6 +78,10 @@ export const PlotHistory: FC<{ setTarget(event.target.value) } + const handleLogScaleChange = () => { + setLogScaleInternal(!logScaleInternal) + } + const handleXAxisChange = (e: ChangeEvent) => { if (e.target.value === "number") { setXAxis("number") @@ -91,7 +99,7 @@ export const PlotHistory: FC<{ historyPlotInfos, selected, xAxis, - logScale, + logScale === undefined ? logScaleInternal : logScale, colorThemeUsed, markerSize )?.then(notifyGraphDidRender) @@ -100,6 +108,8 @@ export const PlotHistory: FC<{ historyPlotInfos, selected, xAxis, + logScale, + logScaleInternal, colorThemeUsed, markerSize, graphComponentState, @@ -166,6 +176,19 @@ export const PlotHistory: FC<{ /> + {logScale === undefined ? ( + + Log y scale: + + + ) : null} Marker size: Date: Tue, 20 Aug 2024 23:48:46 +0900 Subject: [PATCH 6/8] Add includePruned as optional --- .../ts/components/GraphHistory.tsx | 3 ++- tslib/react/src/components/PlotHistory.tsx | 26 ++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphHistory.tsx b/optuna_dashboard/ts/components/GraphHistory.tsx index c1b3b40d..6d90191b 100644 --- a/optuna_dashboard/ts/components/GraphHistory.tsx +++ b/optuna_dashboard/ts/components/GraphHistory.tsx @@ -8,13 +8,14 @@ export const GraphHistory: FC<{ studies: StudyDetail[] logScale: boolean includePruned: boolean -}> = ({ studies, logScale }) => { +}> = ({ studies, logScale, includePruned }) => { const theme = useTheme() const colorTheme = usePlotlyColorTheme(theme.palette.mode) return ( ) diff --git a/tslib/react/src/components/PlotHistory.tsx b/tslib/react/src/components/PlotHistory.tsx index d04aa716..968651af 100644 --- a/tslib/react/src/components/PlotHistory.tsx +++ b/tslib/react/src/components/PlotHistory.tsx @@ -37,10 +37,9 @@ interface HistoryPlotInfo { export const PlotHistory: FC<{ studies: Optuna.Study[] logScale?: boolean + includePruned?: boolean colorTheme?: Partial -}> = ({ studies, logScale, colorTheme }) => { - const filterPrunedTrial = false - +}> = ({ studies, logScale, includePruned, colorTheme }) => { const { graphComponentState, notifyGraphDidRender } = useGraphComponentState() const theme = useTheme() @@ -52,6 +51,8 @@ export const PlotHistory: FC<{ >("number") const [logScaleInternal, setLogScaleInternal] = useState(false) + const [includePrunedInternal, setIncludePrunedInternal] = + useState(true) const [markerSize, setMarkerSize] = useState(5) @@ -61,7 +62,7 @@ export const PlotHistory: FC<{ const trials = useFilteredTrialsFromStudies( studies, [selected], - filterPrunedTrial + includePruned === undefined ? !includePrunedInternal : !includePruned ) const historyPlotInfos = studies.map((study, index) => { @@ -82,6 +83,10 @@ export const PlotHistory: FC<{ setLogScaleInternal(!logScaleInternal) } + const handleIncludePrunedChange = () => { + setIncludePrunedInternal(!includePrunedInternal) + } + const handleXAxisChange = (e: ChangeEvent) => { if (e.target.value === "number") { setXAxis("number") @@ -189,6 +194,19 @@ export const PlotHistory: FC<{ /> ) : null} + {includePruned === undefined ? ( + + Include PRUNED trials: + + + ) : null} Marker size: Date: Wed, 21 Aug 2024 13:37:20 +0900 Subject: [PATCH 7/8] Fix typo --- tslib/react/src/components/PlotParallelCoordinate.stories.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tslib/react/src/components/PlotParallelCoordinate.stories.tsx b/tslib/react/src/components/PlotParallelCoordinate.stories.tsx index 58657abc..42443339 100644 --- a/tslib/react/src/components/PlotParallelCoordinate.stories.tsx +++ b/tslib/react/src/components/PlotParallelCoordinate.stories.tsx @@ -14,13 +14,12 @@ const meta: Meta = { (Story, storyContext) => { const { study } = useMockStudy(storyContext.parameters?.studyId) if (!study) return

loading...

- const studies = [study] return ( From de2efee490c3da8de53f1ff976ffbec4d3a858f7 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Tue, 3 Sep 2024 15:14:45 +0900 Subject: [PATCH 8/8] Implement a link for each trial --- .../ts/components/GraphHistory.tsx | 10 +++++ tslib/react/src/components/PlotHistory.tsx | 44 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/optuna_dashboard/ts/components/GraphHistory.tsx b/optuna_dashboard/ts/components/GraphHistory.tsx index 6d90191b..42536e29 100644 --- a/optuna_dashboard/ts/components/GraphHistory.tsx +++ b/optuna_dashboard/ts/components/GraphHistory.tsx @@ -1,7 +1,9 @@ import { useTheme } from "@mui/material" import { PlotHistory } from "@optuna/react" import React, { FC } from "react" +import { useNavigate } from "react-router-dom" import { StudyDetail } from "ts/types/optuna" +import { useConstants } from "../constantsProvider" import { usePlotlyColorTheme } from "../state" export const GraphHistory: FC<{ @@ -9,14 +11,22 @@ export const GraphHistory: FC<{ logScale: boolean includePruned: boolean }> = ({ studies, logScale, includePruned }) => { + const { url_prefix } = useConstants() const theme = useTheme() const colorTheme = usePlotlyColorTheme(theme.palette.mode) + const linkURL = (studyId: number, trialNumber: number) => { + return url_prefix + `/studies/${studyId}/trials?numbers=${trialNumber}` + } + const navigate = useNavigate() + return ( ) } diff --git a/tslib/react/src/components/PlotHistory.tsx b/tslib/react/src/components/PlotHistory.tsx index 968651af..294dc7e0 100644 --- a/tslib/react/src/components/PlotHistory.tsx +++ b/tslib/react/src/components/PlotHistory.tsx @@ -39,7 +39,10 @@ export const PlotHistory: FC<{ logScale?: boolean includePruned?: boolean colorTheme?: Partial -}> = ({ studies, logScale, includePruned, colorTheme }) => { + linkURL?: (studyId: number, trialNumber: number) => string + // biome-ignore lint/suspicious/noExplicitAny: It will accept any routers of each library. + router?: any +}> = ({ studies, logScale, includePruned, colorTheme, linkURL, router }) => { const { graphComponentState, notifyGraphDidRender } = useGraphComponentState() const theme = useTheme() @@ -108,6 +111,45 @@ export const PlotHistory: FC<{ colorThemeUsed, markerSize )?.then(notifyGraphDidRender) + + const element = document.getElementById(plotDomId) + if ( + element !== null && + studies.length >= 1 && + linkURL !== undefined && + router !== undefined + ) { + // @ts-ignore + element.on("plotly_click", (data) => { + if (data.points[0].data.mode !== "lines") { + let studyId = 1 + if (data.points[0].data.name.includes("Infeasible Trial of")) { + const studyInfo: { id: number; name: string }[] = [] + for (const study of studies) { + studyInfo.push({ id: study.id, name: study.name }) + } + const dataPointStudyName = data.points[0].data.name.replace( + "Infeasible Trial of ", + "" + ) + const targetId = studyInfo.find( + (s) => s.name === dataPointStudyName + )?.id + if (targetId !== undefined) { + studyId = targetId + } + } else { + studyId = studies[Math.floor(data.points[0].curveNumber / 2)].id + } + const trialNumber = data.points[0].x + router(linkURL(studyId, trialNumber)) + } + }) + return () => { + // @ts-ignore + element.removeAllListeners("plotly_click") + } + } } }, [ historyPlotInfos,