diff --git a/optuna_dashboard/static/components/GraphSlice.tsx b/optuna_dashboard/static/components/GraphSlice.tsx new file mode 100644 index 00000000..5d5a1a7e --- /dev/null +++ b/optuna_dashboard/static/components/GraphSlice.tsx @@ -0,0 +1,241 @@ +import * as plotly from "plotly.js-basic-dist" +import React, { ChangeEvent, FC, useEffect, useState } from "react" +import { + Grid, + FormControl, + InputLabel, + MenuItem, + Select, +} from "@material-ui/core" +import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" + +const plotDomId = "graph-slice" + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + formControl: { + marginBottom: theme.spacing(2), + marginRight: theme.spacing(5), + marginTop: theme.spacing(10), + }, + }) +) + +export const GraphSlice: FC<{ + trials: Trial[] +}> = ({ trials = [] }) => { + const filteredTrials = trials.filter( + (t) => t.state === "Complete" || t.state === "Pruned" + ) + + let paramNames = new Set(trials[0].params.map((p) => p.name)) + filteredTrials.forEach((t) => { + paramNames = new Set( + t.params.filter((p) => paramNames.has(p.name)).map((p) => p.name) + ) + }) + const paramnames = Array.from(paramNames) + + const classes = useStyles() + const [xAxis, setXAxis] = useState(paramnames[0]) + + const handleXAxisChange = (e: ChangeEvent<{ value: unknown }>) => { + setXAxis(e.target.value as string) + } + + useEffect(() => { + if (trials != null) { + plotSlice(trials, 0, xAxis) + } + }, [trials, xAxis]) + + return ( + + + + + Parameter + + + + + +
+ + + ) +} + +const plotSlice = (trials: Trial[], objectiveId: number, xAxis: string) => { + if (document.getElementById(plotDomId) === null) { + return + } + + const layout: Partial = { + title: "Slice", + margin: { + l: 50, + r: 50, + b: 0, + }, + } + + if (trials.length === 0) { + plotly.react(plotDomId, [], layout) + return + } + + const filteredTrials = trials.filter( + (t) => t.state === "Complete" || t.state === "Pruned" + ) + + let paramNames = new Set(trials[0].params.map((p) => p.name)) + filteredTrials.forEach((t) => { + paramNames = new Set( + t.params.filter((p) => paramNames.has(p.name)).map((p) => p.name) + ) + }) + + const objectiveValues: number[] = filteredTrials.map( + (t) => t.values![objectiveId] + ) + + if (paramNames.size === 0) { + plotly.react(plotDomId, []) + return + } else { + let trace: Partial[] = [ + { + type: "scatter", + x: [], + y: [], + mode: "markers", + xaxis: "x", + marker: { + color: "#185799", + }, + }, + ] + let updateLayout: Partial = { + title: "Slice", + margin: { + l: 50, + r: 50, + }, + xaxis: { + title: "x", + zerolinecolor: "#f2f5fa", + zerolinewidth: 1.5, + linecolor: "#f2f5fa", + linewidth: 5, + gridcolor: "#f2f5fa", + gridwidth: 1, + }, + yaxis: { + title: "Objective Values", + zerolinecolor: "#f2f5fa", + zerolinewidth: 2, + linecolor: "#f2f5fa", + linewidth: 5, + gridcolor: "#f2f5fa", + gridwidth: 1, + }, + plot_bgcolor: "#E5ecf6", + showlegend: false, + } + paramNames.forEach((paramName) => { + const valueStrings = filteredTrials.map((t) => { + const param = t.params.find((p) => p.name == paramName) + return param!.value + }) + const isnum = valueStrings.every((v) => { + return !isNaN(parseFloat(v)) + }) + if (paramName === xAxis) { + if (isnum) { + const valuesNum: number[] = valueStrings.map((v) => parseFloat(v)) + trace = [ + { + type: "scatter", + x: valuesNum, + y: objectiveValues, + mode: "markers", + xaxis: paramName, + marker: { + color: "#185799", + }, + }, + ] + updateLayout["xaxis"] = { + title: paramName, + zerolinecolor: "#f2f5fa", + zerolinewidth: 1.5, + linecolor: "#f2f5fa", + linewidth: 5, + gridcolor: "#f2f5fa", + gridwidth: 1, + } + plotly.react(plotDomId, trace, updateLayout) + } else { + const vocabSet = new Set(valueStrings) + const vocabArr = Array.from(vocabSet) + const valuesCategorical: number[] = valueStrings.map((v) => + vocabArr.findIndex((vocab) => v === vocab) + ) + const tickvals: number[] = vocabArr.map((v, i) => i) + trace = [ + { + type: "scatter", + x: valuesCategorical, + y: objectiveValues, + mode: "markers", + // xaxis: paramName, + marker: { + color: "#185799", + }, + }, + ] + updateLayout = { + title: "Slice", + margin: { + l: 50, + r: 50, + }, + xaxis: { + title: paramName, + zerolinecolor: "#f2f5fa", + zerolinewidth: 1.5, + linecolor: "#f2f5fa", + linewidth: 5, + gridcolor: "#f2f5fa", + gridwidth: 1, + tickfont: { + color: "#000000", + }, + tickvals: tickvals, + ticktext: vocabArr, + }, + yaxis: { + title: "Objective Values", + zerolinecolor: "#f2f5fa", + zerolinewidth: 2, + linecolor: "#f2f5fa", + linewidth: 5, + gridcolor: "#f2f5fa", + gridwidth: 1, + }, + plot_bgcolor: "#E5ecf6", + showlegend: false, + } + plotly.react(plotDomId, trace, updateLayout) + } + } + }) + } +} diff --git a/optuna_dashboard/static/components/StudyDetail.tsx b/optuna_dashboard/static/components/StudyDetail.tsx index 91f53865..55a2a61f 100644 --- a/optuna_dashboard/static/components/StudyDetail.tsx +++ b/optuna_dashboard/static/components/StudyDetail.tsx @@ -21,6 +21,7 @@ import { Home, Cached } from "@material-ui/icons" import { DataGridColumn, DataGrid } from "./DataGrid" import { GraphParallelCoordinate } from "./GraphParallelCoordinate" import { GraphIntermediateValues } from "./GraphIntermediateValues" +import { GraphSlice } from "./GraphSlice" import { GraphHistory } from "./GraphHistory" import { actionCreator } from "../action" import { studyDetailsState } from "../state" @@ -195,6 +196,13 @@ export const StudyDetail: FC = () => { ) : null} + {studyDetail !== null && isSingleObjectiveStudy(studyDetail) ? ( + + + + + + ) : null}