diff --git a/optuna_dashboard/static/components/GraphParetoFront.tsx b/optuna_dashboard/static/components/GraphParetoFront.tsx new file mode 100644 index 00000000..e0d4491b --- /dev/null +++ b/optuna_dashboard/static/components/GraphParetoFront.tsx @@ -0,0 +1,175 @@ +import * as plotly from "plotly.js-dist" +import React, { FC, useEffect, useState } from "react" +import { + Grid, + FormControl, + FormLabel, + MenuItem, + Select, +} from "@material-ui/core" +import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" + +const plotDomId = "graph-pareto-front" + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + formControl: { + marginBottom: theme.spacing(2), + marginRight: theme.spacing(5), + marginTop: theme.spacing(10), + }, + }) +) + +export const GraphParetoFront: FC<{ + study: StudyDetail | null +}> = ({ study = null }) => { + const classes = useStyles() + const [objectiveXId, setObjectiveXId] = useState(0) + const [objectiveYId, setObjectiveYId] = useState(1) + + const handleObjectiveXChange = ( + event: React.ChangeEvent<{ value: unknown }> + ) => { + setObjectiveXId(event.target.value as number) + } + + const handleObjectiveYChange = ( + event: React.ChangeEvent<{ value: unknown }> + ) => { + setObjectiveYId(event.target.value as number) + } + + useEffect(() => { + if (study != null) { + plotParetoFront(study, objectiveXId, objectiveYId) + } + }, [study, objectiveXId, objectiveYId]) + + return ( + + {study !== null && study.directions.length !== 1 ? ( + + + + Objective X ID: + + + + Objective Y ID: + + + + + ) : null} + +
+ + + ) +} + +const plotParetoFront = ( + study: StudyDetail, + objectiveXId: number, + objectiveYId: number +) => { + if (document.getElementById(plotDomId) === null) { + return + } + + const dim: number = study.directions.length + if (dim != 2) { + return + } + const layout: Partial = { + title: "Pareto-front plot", + margin: { + l: 50, + r: 50, + b: 0, + }, + } + + const trials: Trial[] = study !== null ? study.trials : [] + const completedTrials = trials.filter((t) => t.state === "Complete") + + if (completedTrials.length === 0) { + plotly.react(plotDomId, [], layout) + return + } + + const normalizedValues: number[][] = [] + completedTrials.forEach((t) => { + if (t.values && t.values.length == dim) { + const trialValues = t.values.map((v: number, i: number) => { + return study.directions[i] === "minimize" ? v : -v + }) + normalizedValues.push(trialValues) + } + }) + + const pointColors: string[] = [] + normalizedValues.forEach((values0: number[], i: number) => { + let dominated = false + + dominated = normalizedValues.some((values1: number[], j: number) => { + if (i === j) { + return false + } + return values0.every((value0: number, k: number) => { + return values1[k] <= value0 + }) + }) + + if (dominated) { + pointColors.push("blue") + } else { + pointColors.push("red") + } + }) + + const plotData: Partial[] = [ + { + type: "scatter", + x: completedTrials.map((t: Trial): number => { + return t.values![objectiveXId] + }), + y: completedTrials.map((t: Trial): number => { + return t.values![objectiveYId] + }), + mode: "markers", + xaxis: "Objective X", + yaxis: "Objective Y", + marker: { + color: pointColors, + }, + text: completedTrials.map((t: Trial): string => { + return JSON.stringify( + { + number: t.number, + values: t.values, + params: t.params, + }, + null, + 2 + ).replaceAll("\n", "
") + }), + hovertemplate: "%{text}", + }, + ] + + plotly.react(plotDomId, plotData, layout) +} diff --git a/optuna_dashboard/static/components/StudyDetail.tsx b/optuna_dashboard/static/components/StudyDetail.tsx index bd47d1ae..4f1590d1 100644 --- a/optuna_dashboard/static/components/StudyDetail.tsx +++ b/optuna_dashboard/static/components/StudyDetail.tsx @@ -23,6 +23,7 @@ import { GraphParallelCoordinate } from "./GraphParallelCoordinate" import { GraphIntermediateValues } from "./GraphIntermediateValues" import { GraphSlice } from "./GraphSlice" import { GraphHistory } from "./GraphHistory" +import { GraphParetoFront } from "./GraphParetoFront" import { actionCreator } from "../action" import { studyDetailsState } from "../state" @@ -203,6 +204,13 @@ export const StudyDetail: FC = () => { ) : null} + {studyDetail !== null && !isSingleObjectiveStudy(studyDetail) ? ( + + + + + + ) : null}