From 5f888e0aa735de048e3029761d369c1c59d2c515 Mon Sep 17 00:00:00 2001 From: porink0424 Date: Fri, 21 Jun 2024 11:50:19 +0900 Subject: [PATCH 1/7] Move GraphSlice to tslib/react --- optuna_dashboard/ts/components/GraphSlice.tsx | 287 +----------------- tslib/react/src/components/PlotSlice.tsx | 281 +++++++++++++++++ 2 files changed, 289 insertions(+), 279 deletions(-) create mode 100644 tslib/react/src/components/PlotSlice.tsx diff --git a/optuna_dashboard/ts/components/GraphSlice.tsx b/optuna_dashboard/ts/components/GraphSlice.tsx index d404c2cf5..ecac22835 100644 --- a/optuna_dashboard/ts/components/GraphSlice.tsx +++ b/optuna_dashboard/ts/components/GraphSlice.tsx @@ -1,41 +1,15 @@ -import { - FormControl, - FormLabel, - Grid, - MenuItem, - Select, - SelectChangeEvent, - Switch, - Typography, - useTheme, -} from "@mui/material" import { GraphContainer, + PlotSlice, + getPlotSliceDomId, useGraphComponentState, - useMergedUnionSearchSpace, -} from "@optuna/react" -import { - Target, - useFilteredTrials, - useObjectiveAndUserAttrTargets, - useParamTargets, } from "@optuna/react" -import * as Optuna from "@optuna/types" import * as plotly from "plotly.js-dist-min" -import React, { FC, useEffect, useState } from "react" -import { SearchSpaceItem, StudyDetail } from "ts/types/optuna" +import React, { FC, useEffect } from "react" +import { StudyDetail } from "ts/types/optuna" import { PlotType } from "../apiClient" import { usePlot } from "../hooks/usePlot" -import { useBackendRender, usePlotlyColorTheme } from "../state" - -const plotDomId = "graph-slice" - -const isLogScale = (s: SearchSpaceItem): boolean => { - if (s.distribution.type === "CategoricalDistribution") { - return false - } - return s.distribution.log -} +import { useBackendRender } from "../state" export const GraphSlice: FC<{ study: StudyDetail | null @@ -43,7 +17,7 @@ export const GraphSlice: FC<{ if (useBackendRender()) { return } else { - return + return } } @@ -64,7 +38,7 @@ const GraphSliceBackend: FC<{ useEffect(() => { if (data && layout && graphComponentState !== "componentWillMount") { - plotly.react(plotDomId, data, layout).then(notifyGraphDidRender) + plotly.react(getPlotSliceDomId(), data, layout).then(notifyGraphDidRender) } }, [data, layout, graphComponentState]) useEffect(() => { @@ -75,253 +49,8 @@ const GraphSliceBackend: FC<{ return ( ) } - -const GraphSliceFrontend: FC<{ - study: StudyDetail | null -}> = ({ study = null }) => { - const { graphComponentState, notifyGraphDidRender } = useGraphComponentState() - - const theme = useTheme() - const colorTheme = usePlotlyColorTheme(theme.palette.mode) - - const [objectiveTargets, selectedObjective, setObjectiveTarget] = - useObjectiveAndUserAttrTargets(study) - const searchSpace = useMergedUnionSearchSpace(study?.union_search_space) - const [paramTargets, selectedParamTarget, setParamTarget] = - useParamTargets(searchSpace) - const [logYScale, setLogYScale] = useState(false) - - const trials = useFilteredTrials( - study, - selectedParamTarget !== null - ? [selectedObjective, selectedParamTarget] - : [selectedObjective], - false - ) - - useEffect(() => { - if (graphComponentState !== "componentWillMount") { - plotSlice( - trials, - selectedObjective, - selectedParamTarget, - searchSpace.find((s) => s.name === selectedParamTarget?.key) || null, - logYScale, - colorTheme - )?.then(notifyGraphDidRender) - } - }, [ - trials, - selectedObjective, - searchSpace, - selectedParamTarget, - logYScale, - colorTheme, - graphComponentState, - ]) - - const handleObjectiveChange = (event: SelectChangeEvent) => { - setObjectiveTarget(event.target.value) - } - - const handleSelectedParam = (e: SelectChangeEvent) => { - setParamTarget(e.target.value) - } - - const handleLogYScaleChange = () => { - setLogYScale(!logYScale) - } - - return ( - - - - Slice - - {objectiveTargets.length !== 1 && ( - - Objective: - - - )} - {paramTargets.length !== 0 && selectedParamTarget !== null && ( - - Parameter: - - - )} - - Log y scale: - - - - - - - - ) -} - -const plotSlice = ( - trials: Optuna.Trial[], - objectiveTarget: Target, - selectedParamTarget: Target | null, - selectedParamSpace: SearchSpaceItem | null, - logYScale: boolean, - colorTheme: Partial -) => { - if (document.getElementById(plotDomId) === null) { - return - } - - const layout: Partial = { - margin: { - l: 50, - t: 0, - r: 50, - b: 0, - }, - xaxis: { - title: selectedParamTarget?.toLabel() || "", - type: - selectedParamSpace !== null && isLogScale(selectedParamSpace) - ? "log" - : "linear", - gridwidth: 1, - automargin: true, - }, - yaxis: { - title: "Objective Value", - type: logYScale ? "log" : "linear", - gridwidth: 1, - automargin: true, - }, - showlegend: false, - uirevision: "true", - template: colorTheme, - } - if ( - selectedParamSpace === null || - selectedParamTarget === null || - trials.length === 0 - ) { - return plotly.react(plotDomId, [], layout) - } - - const feasibleTrials: Optuna.Trial[] = [] - const infeasibleTrials: Optuna.Trial[] = [] - trials.forEach((t) => { - if (t.constraints.every((c) => c <= 0)) { - feasibleTrials.push(t) - } else { - infeasibleTrials.push(t) - } - }) - - const feasibleObjectiveValues: number[] = feasibleTrials.map( - (t) => objectiveTarget.getTargetValue(t) as number - ) - const infeasibleObjectiveValues: number[] = infeasibleTrials.map( - (t) => objectiveTarget.getTargetValue(t) as number - ) - - const feasibleValues = feasibleTrials.map( - (t) => selectedParamTarget.getTargetValue(t) as number - ) - const infeasibleValues = infeasibleTrials.map( - (t) => selectedParamTarget.getTargetValue(t) as number - ) - const trace: plotly.Data[] = [ - { - type: "scatter", - x: feasibleValues, - y: feasibleObjectiveValues, - mode: "markers", - name: "Feasible Trial", - marker: { - color: feasibleTrials.map((t) => t.number), - colorscale: "Blues", - reversescale: true, - colorbar: { - title: "Trial", - }, - line: { - color: "Grey", - width: 0.5, - }, - }, - }, - { - type: "scatter", - x: infeasibleValues, - y: infeasibleObjectiveValues, - mode: "markers", - name: "Infeasible Trial", - marker: { - color: "#cccccc", - reversescale: true, - }, - }, - ] - if (selectedParamSpace.distribution.type !== "CategoricalDistribution") { - layout["xaxis"] = { - title: selectedParamTarget.toLabel(), - type: isLogScale(selectedParamSpace) ? "log" : "linear", - gridwidth: 1, - automargin: true, // Otherwise the label is outside of the plot - } - } else { - const vocabArr = selectedParamSpace.distribution.choices.map( - (c) => c?.toString() ?? "null" - ) - const tickvals: number[] = vocabArr.map((v, i) => i) - layout["xaxis"] = { - title: selectedParamTarget.toLabel(), - type: "linear", - gridwidth: 1, - tickvals: tickvals, - ticktext: vocabArr, - automargin: true, // Otherwise the label is outside of the plot - } - } - return plotly.react(plotDomId, trace, layout) -} diff --git a/tslib/react/src/components/PlotSlice.tsx b/tslib/react/src/components/PlotSlice.tsx new file mode 100644 index 000000000..5d7eea7c6 --- /dev/null +++ b/tslib/react/src/components/PlotSlice.tsx @@ -0,0 +1,281 @@ +import { + FormControl, + FormLabel, + Grid, + MenuItem, + Select, + SelectChangeEvent, + Switch, + Typography, + useTheme, + } from "@mui/material" + import * as Optuna from "@optuna/types" + import * as plotly from "plotly.js-dist-min" + import { FC, useEffect, useState } from "react" + import { useGraphComponentState } from "../hooks/useGraphComponentState" + import { useMergedUnionSearchSpace } from "../utils/searchSpace" + import { + Target, + useFilteredTrials, + useObjectiveAndUserAttrTargets, + useParamTargets, + } from "../utils/trialFilter" + import { GraphContainer } from "./GraphContainer" + import { plotlyDarkTemplate } from "./PlotlyDarkMode" + + export const getPlotSliceDomId = () => "graph-slice" + + const isLogScale = (s: Optuna.SearchSpaceItem): boolean => { + if (s.distribution.type === "CategoricalDistribution") { + return false + } + return s.distribution.log + } + + export const PlotSlice: FC<{ + study: Optuna.Study | null + }> = ({ study = null }) => { + const { graphComponentState, notifyGraphDidRender } = useGraphComponentState() + + const theme = useTheme() + + const [objectiveTargets, selectedObjective, setObjectiveTarget] = + useObjectiveAndUserAttrTargets(study) + const searchSpace = useMergedUnionSearchSpace(study?.union_search_space) + const [paramTargets, selectedParamTarget, setParamTarget] = + useParamTargets(searchSpace) + const [logYScale, setLogYScale] = useState(false) + + const trials = useFilteredTrials( + study, + selectedParamTarget !== null + ? [selectedObjective, selectedParamTarget] + : [selectedObjective], + false + ) + + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + if (graphComponentState !== "componentWillMount") { + plotSlice( + trials, + selectedObjective, + selectedParamTarget, + searchSpace.find((s) => s.name === selectedParamTarget?.key) || null, + logYScale, + theme.palette.mode + )?.then(notifyGraphDidRender) + } + }, [ + trials, + selectedObjective, + searchSpace, + selectedParamTarget, + logYScale, + theme.palette.mode, + graphComponentState, + ]) + + const handleObjectiveChange = (event: SelectChangeEvent) => { + setObjectiveTarget(event.target.value) + } + + const handleSelectedParam = (e: SelectChangeEvent) => { + setParamTarget(e.target.value) + } + + const handleLogYScaleChange = () => { + setLogYScale(!logYScale) + } + + return ( + + + + Slice + + {objectiveTargets.length !== 1 && ( + + Objective: + + + )} + {paramTargets.length !== 0 && selectedParamTarget !== null && ( + + Parameter: + + + )} + + Log y scale: + + + + + + + + ) + } + + const plotSlice = ( + trials: Optuna.Trial[], + objectiveTarget: Target, + selectedParamTarget: Target | null, + selectedParamSpace: Optuna.SearchSpaceItem | null, + logYScale: boolean, + mode: string + ) => { + if (document.getElementById(getPlotSliceDomId()) === null) { + return + } + + const layout: Partial = { + margin: { + l: 50, + t: 0, + r: 50, + b: 0, + }, + xaxis: { + title: selectedParamTarget?.toLabel() || "", + type: + selectedParamSpace !== null && isLogScale(selectedParamSpace) + ? "log" + : "linear", + gridwidth: 1, + automargin: true, + }, + yaxis: { + title: "Objective Value", + type: logYScale ? "log" : "linear", + gridwidth: 1, + automargin: true, + }, + showlegend: false, + uirevision: "true", + template: mode === "dark" ? plotlyDarkTemplate : {}, + } + if ( + selectedParamSpace === null || + selectedParamTarget === null || + trials.length === 0 + ) { + return plotly.react(getPlotSliceDomId(), [], layout) + } + + const feasibleTrials: Optuna.Trial[] = [] + const infeasibleTrials: Optuna.Trial[] = [] + // biome-ignore lint/complexity/noForEach: + trials.forEach((t) => { + if (t.constraints.every((c) => c <= 0)) { + feasibleTrials.push(t) + } else { + infeasibleTrials.push(t) + } + }) + + const feasibleObjectiveValues: number[] = feasibleTrials.map( + (t) => objectiveTarget.getTargetValue(t) as number + ) + const infeasibleObjectiveValues: number[] = infeasibleTrials.map( + (t) => objectiveTarget.getTargetValue(t) as number + ) + + const feasibleValues = feasibleTrials.map( + (t) => selectedParamTarget.getTargetValue(t) as number + ) + const infeasibleValues = infeasibleTrials.map( + (t) => selectedParamTarget.getTargetValue(t) as number + ) + const trace: plotly.Data[] = [ + { + type: "scatter", + x: feasibleValues, + y: feasibleObjectiveValues, + mode: "markers", + name: "Feasible Trial", + marker: { + color: feasibleTrials.map((t) => t.number), + colorscale: "Blues", + reversescale: true, + colorbar: { + title: "Trial", + }, + line: { + color: "Grey", + width: 0.5, + }, + }, + }, + { + type: "scatter", + x: infeasibleValues, + y: infeasibleObjectiveValues, + mode: "markers", + name: "Infeasible Trial", + marker: { + color: "#cccccc", + reversescale: true, + }, + }, + ] + if (selectedParamSpace.distribution.type !== "CategoricalDistribution") { + layout.xaxis = { + title: selectedParamTarget.toLabel(), + type: isLogScale(selectedParamSpace) ? "log" : "linear", + gridwidth: 1, + automargin: true, // Otherwise the label is outside of the plot + } + } else { + const vocabArr = selectedParamSpace.distribution.choices.map( + (c) => c?.toString() ?? "null" + ) + const tickvals: number[] = vocabArr.map((_v, i) => i) + layout.xaxis = { + title: selectedParamTarget.toLabel(), + type: "linear", + gridwidth: 1, + tickvals: tickvals, + ticktext: vocabArr, + automargin: true, // Otherwise the label is outside of the plot + } + } + return plotly.react(getPlotSliceDomId(), trace, layout) + } \ No newline at end of file From 82d0c4445b592a340ff31df237d520bd4d34157d Mon Sep 17 00:00:00 2001 From: porink0424 Date: Fri, 21 Jun 2024 11:50:43 +0900 Subject: [PATCH 2/7] Rename: getPlotDomId -> getPlotEdfDomId in PlotEdf --- tslib/react/src/components/PlotEdf.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tslib/react/src/components/PlotEdf.tsx b/tslib/react/src/components/PlotEdf.tsx index 17a0872e3..961463c84 100644 --- a/tslib/react/src/components/PlotEdf.tsx +++ b/tslib/react/src/components/PlotEdf.tsx @@ -12,7 +12,7 @@ export type EdfPlotInfo = { trials: Optuna.Trial[] } -export const getPlotDomId = (objectiveId: number) => `graph-edf-${objectiveId}` +export const getPlotEdfDomId = (objectiveId: number) => `graph-edf-${objectiveId}` export const PlotEdf: FC<{ studies: Optuna.Study[] @@ -22,7 +22,7 @@ export const PlotEdf: FC<{ const theme = useTheme() - const domId = getPlotDomId(objectiveId) + const domId = getPlotEdfDomId(objectiveId) const target = useMemo( () => new Target("objective", objectiveId), [objectiveId] From 63a4c8b105b6acd20dfbb327989b1b4af1a042cf Mon Sep 17 00:00:00 2001 From: porink0424 Date: Fri, 21 Jun 2024 11:51:10 +0900 Subject: [PATCH 3/7] Export the change in tslib/react --- optuna_dashboard/ts/components/GraphEdf.tsx | 4 ++-- tslib/react/src/index.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphEdf.tsx b/optuna_dashboard/ts/components/GraphEdf.tsx index e5c3a0832..30de9868f 100644 --- a/optuna_dashboard/ts/components/GraphEdf.tsx +++ b/optuna_dashboard/ts/components/GraphEdf.tsx @@ -1,7 +1,7 @@ import { GraphContainer, PlotEdf, - getPlotDomId, + getPlotEdfDomId, useGraphComponentState, } from "@optuna/react" import * as plotly from "plotly.js-dist-min" @@ -29,7 +29,7 @@ const GraphEdfBackend: FC<{ const { graphComponentState, notifyGraphDidRender } = useGraphComponentState() const studyIds = studies.map((s) => s.id) - const domId = getPlotDomId(-1) + const domId = getPlotEdfDomId(-1) const numCompletedTrials = studies.reduce( (acc, study) => acc + study?.trials.filter((t) => t.state === "Complete").length, diff --git a/tslib/react/src/index.ts b/tslib/react/src/index.ts index e1b4243ff..2b24e6a9c 100644 --- a/tslib/react/src/index.ts +++ b/tslib/react/src/index.ts @@ -1,10 +1,11 @@ export { DataGrid } from "./components/DataGrid" export { plotlyDarkTemplate } from "./components/PlotlyDarkMode" -export { PlotEdf, getPlotDomId } from "./components/PlotEdf" +export { PlotEdf, getPlotEdfDomId } from "./components/PlotEdf" export type { EdfPlotInfo } from "./components/PlotEdf" export { PlotHistory } from "./components/PlotHistory" export { PlotImportance } from "./components/PlotImportance" export { PlotIntermediateValues } from "./components/PlotIntermediateValues" +export { PlotSlice, getPlotSliceDomId } from "./components/PlotSlice" export { TrialTable } from "./components/TrialTable" export { GraphContainer } from "./components/GraphContainer" export { useGraphComponentState } from "./hooks/useGraphComponentState" From 0947d8b04fcb995978ef093c0429dfb1e42f3137 Mon Sep 17 00:00:00 2001 From: porink0424 Date: Fri, 21 Jun 2024 11:51:30 +0900 Subject: [PATCH 4/7] Add stories of PlotSlice --- .../src/components/PlotSlice.stories.tsx | 37 +++++++++++++++++ .../src/components/PlotSliceDark.stories.tsx | 40 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 tslib/react/src/components/PlotSlice.stories.tsx create mode 100644 tslib/react/src/components/PlotSliceDark.stories.tsx diff --git a/tslib/react/src/components/PlotSlice.stories.tsx b/tslib/react/src/components/PlotSlice.stories.tsx new file mode 100644 index 000000000..091f18046 --- /dev/null +++ b/tslib/react/src/components/PlotSlice.stories.tsx @@ -0,0 +1,37 @@ +import { CssBaseline, ThemeProvider } from "@mui/material" +import { Meta, StoryObj } from "@storybook/react" +import React from "react" +import { useMockStudy } from "../MockStudies" +import { lightTheme } from "../styles/lightTheme" +import { PlotSlice } from "./PlotSlice" + +const meta: Meta = { + component: PlotSlice, + title: "PlotSlice", + tags: ["autodocs"], + decorators: [ + (Story, storyContext) => { + const { study } = useMockStudy(storyContext.parameters?.studyId) + if (!study) return

loading...

+ return ( + + + + + ) + }, + ], +} + +export default meta +type Story = StoryObj + +export const MockStudyExample1: Story = { + parameters: { + studyId: 1, + }, +} \ No newline at end of file diff --git a/tslib/react/src/components/PlotSliceDark.stories.tsx b/tslib/react/src/components/PlotSliceDark.stories.tsx new file mode 100644 index 000000000..d2e85e268 --- /dev/null +++ b/tslib/react/src/components/PlotSliceDark.stories.tsx @@ -0,0 +1,40 @@ +import { CssBaseline, ThemeProvider } from "@mui/material" +import { Meta, StoryObj } from "@storybook/react" +import React from "react" +import { useMockStudy } from "../MockStudies" +import { darkTheme } from "../styles/darkTheme" +import { PlotSlice } from "./PlotSlice" + +const meta: Meta = { + component: PlotSlice, + title: "PlotSliceDark", + tags: ["autodocs"], + decorators: [ + (Story, storyContext) => { + const { study } = useMockStudy(storyContext.parameters?.studyId) + if (!study) return

loading...

+ return ( + + + + + ) + }, + ], + parameters: { + backgrounds: { default: "dark" }, + }, +} + +export default meta +type Story = StoryObj + +export const MockStudyExample1: Story = { + parameters: { + studyId: 1, + }, +} \ No newline at end of file From 0c1f9c7e5f4cb12df57dab1447545fe18810b5de Mon Sep 17 00:00:00 2001 From: porink0424 Date: Fri, 21 Jun 2024 11:51:44 +0900 Subject: [PATCH 5/7] Add test for PlotSlice --- tslib/react/test/PlotSlice.test.tsx | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tslib/react/test/PlotSlice.test.tsx diff --git a/tslib/react/test/PlotSlice.test.tsx b/tslib/react/test/PlotSlice.test.tsx new file mode 100644 index 000000000..889975716 --- /dev/null +++ b/tslib/react/test/PlotSlice.test.tsx @@ -0,0 +1,32 @@ +import * as Optuna from "@optuna/types" +import { render, screen } from "@testing-library/react" +import React from "react" +import { describe, expect, test } from "vitest" +import { PlotSlice } from "../src/components/PlotSlice" + +describe("PlotSlice Tests", async () => { + const setup = ({ + study, + dataTestId, + }: { study: Optuna.Study; dataTestId: string }) => { + const Wrapper = ({ + dataTestId, + children, + }: { + dataTestId: string + children: React.ReactNode + }) =>
{children}
+ return render( + + + + ) + } + + for (const study of window.mockStudies) { + test(`PlotSlice (study name: ${study.name})`, () => { + setup({ study, dataTestId: `plot-slice-${study.id}` }) + expect(screen.getByTestId(`plot-slice-${study.id}`)).toBeInTheDocument() + }) + } +}) \ No newline at end of file From 41e6c8c01fb76ad46cabbaf0f9c4c3b4143d6a37 Mon Sep 17 00:00:00 2001 From: porink0424 Date: Fri, 21 Jun 2024 18:59:47 +0900 Subject: [PATCH 6/7] Apply formatter --- tslib/react/src/components/PlotEdf.tsx | 3 +- .../src/components/PlotSlice.stories.tsx | 2 +- tslib/react/src/components/PlotSlice.tsx | 532 +++++++++--------- .../src/components/PlotSliceDark.stories.tsx | 2 +- tslib/react/test/PlotSlice.test.tsx | 2 +- 5 files changed, 271 insertions(+), 270 deletions(-) diff --git a/tslib/react/src/components/PlotEdf.tsx b/tslib/react/src/components/PlotEdf.tsx index 961463c84..d4dce2cb2 100644 --- a/tslib/react/src/components/PlotEdf.tsx +++ b/tslib/react/src/components/PlotEdf.tsx @@ -12,7 +12,8 @@ export type EdfPlotInfo = { trials: Optuna.Trial[] } -export const getPlotEdfDomId = (objectiveId: number) => `graph-edf-${objectiveId}` +export const getPlotEdfDomId = (objectiveId: number) => + `graph-edf-${objectiveId}` export const PlotEdf: FC<{ studies: Optuna.Study[] diff --git a/tslib/react/src/components/PlotSlice.stories.tsx b/tslib/react/src/components/PlotSlice.stories.tsx index 091f18046..bad059d1e 100644 --- a/tslib/react/src/components/PlotSlice.stories.tsx +++ b/tslib/react/src/components/PlotSlice.stories.tsx @@ -34,4 +34,4 @@ export const MockStudyExample1: Story = { parameters: { studyId: 1, }, -} \ No newline at end of file +} diff --git a/tslib/react/src/components/PlotSlice.tsx b/tslib/react/src/components/PlotSlice.tsx index 5d7eea7c6..40cf95bfa 100644 --- a/tslib/react/src/components/PlotSlice.tsx +++ b/tslib/react/src/components/PlotSlice.tsx @@ -1,281 +1,281 @@ import { - FormControl, - FormLabel, - Grid, - MenuItem, - Select, - SelectChangeEvent, - Switch, - Typography, - useTheme, - } from "@mui/material" - import * as Optuna from "@optuna/types" - import * as plotly from "plotly.js-dist-min" - import { FC, useEffect, useState } from "react" - import { useGraphComponentState } from "../hooks/useGraphComponentState" - import { useMergedUnionSearchSpace } from "../utils/searchSpace" - import { - Target, - useFilteredTrials, - useObjectiveAndUserAttrTargets, - useParamTargets, - } from "../utils/trialFilter" - import { GraphContainer } from "./GraphContainer" - import { plotlyDarkTemplate } from "./PlotlyDarkMode" - - export const getPlotSliceDomId = () => "graph-slice" - - const isLogScale = (s: Optuna.SearchSpaceItem): boolean => { - if (s.distribution.type === "CategoricalDistribution") { - return false - } - return s.distribution.log + FormControl, + FormLabel, + Grid, + MenuItem, + Select, + SelectChangeEvent, + Switch, + Typography, + useTheme, +} from "@mui/material" +import * as Optuna from "@optuna/types" +import * as plotly from "plotly.js-dist-min" +import { FC, useEffect, useState } from "react" +import { useGraphComponentState } from "../hooks/useGraphComponentState" +import { useMergedUnionSearchSpace } from "../utils/searchSpace" +import { + Target, + useFilteredTrials, + useObjectiveAndUserAttrTargets, + useParamTargets, +} from "../utils/trialFilter" +import { GraphContainer } from "./GraphContainer" +import { plotlyDarkTemplate } from "./PlotlyDarkMode" + +export const getPlotSliceDomId = () => "graph-slice" + +const isLogScale = (s: Optuna.SearchSpaceItem): boolean => { + if (s.distribution.type === "CategoricalDistribution") { + return false } - - export const PlotSlice: FC<{ - study: Optuna.Study | null - }> = ({ study = null }) => { - const { graphComponentState, notifyGraphDidRender } = useGraphComponentState() - - const theme = useTheme() - - const [objectiveTargets, selectedObjective, setObjectiveTarget] = - useObjectiveAndUserAttrTargets(study) - const searchSpace = useMergedUnionSearchSpace(study?.union_search_space) - const [paramTargets, selectedParamTarget, setParamTarget] = - useParamTargets(searchSpace) - const [logYScale, setLogYScale] = useState(false) - - const trials = useFilteredTrials( - study, - selectedParamTarget !== null - ? [selectedObjective, selectedParamTarget] - : [selectedObjective], - false - ) - - // biome-ignore lint/correctness/useExhaustiveDependencies: - useEffect(() => { - if (graphComponentState !== "componentWillMount") { - plotSlice( - trials, - selectedObjective, - selectedParamTarget, - searchSpace.find((s) => s.name === selectedParamTarget?.key) || null, - logYScale, - theme.palette.mode - )?.then(notifyGraphDidRender) - } - }, [ - trials, - selectedObjective, - searchSpace, - selectedParamTarget, - logYScale, - theme.palette.mode, - graphComponentState, - ]) - - const handleObjectiveChange = (event: SelectChangeEvent) => { - setObjectiveTarget(event.target.value) - } - - const handleSelectedParam = (e: SelectChangeEvent) => { - setParamTarget(e.target.value) + return s.distribution.log +} + +export const PlotSlice: FC<{ + study: Optuna.Study | null +}> = ({ study = null }) => { + const { graphComponentState, notifyGraphDidRender } = useGraphComponentState() + + const theme = useTheme() + + const [objectiveTargets, selectedObjective, setObjectiveTarget] = + useObjectiveAndUserAttrTargets(study) + const searchSpace = useMergedUnionSearchSpace(study?.union_search_space) + const [paramTargets, selectedParamTarget, setParamTarget] = + useParamTargets(searchSpace) + const [logYScale, setLogYScale] = useState(false) + + const trials = useFilteredTrials( + study, + selectedParamTarget !== null + ? [selectedObjective, selectedParamTarget] + : [selectedObjective], + false + ) + + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + if (graphComponentState !== "componentWillMount") { + plotSlice( + trials, + selectedObjective, + selectedParamTarget, + searchSpace.find((s) => s.name === selectedParamTarget?.key) || null, + logYScale, + theme.palette.mode + )?.then(notifyGraphDidRender) } - - const handleLogYScaleChange = () => { - setLogYScale(!logYScale) - } - - return ( - - ) => { + setObjectiveTarget(event.target.value) + } + + const handleSelectedParam = (e: SelectChangeEvent) => { + setParamTarget(e.target.value) + } + + const handleLogYScaleChange = () => { + setLogYScale(!logYScale) + } + + return ( + + + - - Slice - - {objectiveTargets.length !== 1 && ( - - Objective: - - - )} - {paramTargets.length !== 0 && selectedParamTarget !== null && ( - - Parameter: - - - )} + Slice + + {objectiveTargets.length !== 1 && ( + + Objective: + + + )} + {paramTargets.length !== 0 && selectedParamTarget !== null && ( - Log y scale: - + Parameter: + - - - + Log y scale: + - + - ) + + + + + ) +} + +const plotSlice = ( + trials: Optuna.Trial[], + objectiveTarget: Target, + selectedParamTarget: Target | null, + selectedParamSpace: Optuna.SearchSpaceItem | null, + logYScale: boolean, + mode: string +) => { + if (document.getElementById(getPlotSliceDomId()) === null) { + return + } + + const layout: Partial = { + margin: { + l: 50, + t: 0, + r: 50, + b: 0, + }, + xaxis: { + title: selectedParamTarget?.toLabel() || "", + type: + selectedParamSpace !== null && isLogScale(selectedParamSpace) + ? "log" + : "linear", + gridwidth: 1, + automargin: true, + }, + yaxis: { + title: "Objective Value", + type: logYScale ? "log" : "linear", + gridwidth: 1, + automargin: true, + }, + showlegend: false, + uirevision: "true", + template: mode === "dark" ? plotlyDarkTemplate : {}, + } + if ( + selectedParamSpace === null || + selectedParamTarget === null || + trials.length === 0 + ) { + return plotly.react(getPlotSliceDomId(), [], layout) } - - const plotSlice = ( - trials: Optuna.Trial[], - objectiveTarget: Target, - selectedParamTarget: Target | null, - selectedParamSpace: Optuna.SearchSpaceItem | null, - logYScale: boolean, - mode: string - ) => { - if (document.getElementById(getPlotSliceDomId()) === null) { - return + + const feasibleTrials: Optuna.Trial[] = [] + const infeasibleTrials: Optuna.Trial[] = [] + // biome-ignore lint/complexity/noForEach: + trials.forEach((t) => { + if (t.constraints.every((c) => c <= 0)) { + feasibleTrials.push(t) + } else { + infeasibleTrials.push(t) } - - const layout: Partial = { - margin: { - l: 50, - t: 0, - r: 50, - b: 0, - }, - xaxis: { - title: selectedParamTarget?.toLabel() || "", - type: - selectedParamSpace !== null && isLogScale(selectedParamSpace) - ? "log" - : "linear", - gridwidth: 1, - automargin: true, + }) + + const feasibleObjectiveValues: number[] = feasibleTrials.map( + (t) => objectiveTarget.getTargetValue(t) as number + ) + const infeasibleObjectiveValues: number[] = infeasibleTrials.map( + (t) => objectiveTarget.getTargetValue(t) as number + ) + + const feasibleValues = feasibleTrials.map( + (t) => selectedParamTarget.getTargetValue(t) as number + ) + const infeasibleValues = infeasibleTrials.map( + (t) => selectedParamTarget.getTargetValue(t) as number + ) + const trace: plotly.Data[] = [ + { + type: "scatter", + x: feasibleValues, + y: feasibleObjectiveValues, + mode: "markers", + name: "Feasible Trial", + marker: { + color: feasibleTrials.map((t) => t.number), + colorscale: "Blues", + reversescale: true, + colorbar: { + title: "Trial", + }, + line: { + color: "Grey", + width: 0.5, + }, }, - yaxis: { - title: "Objective Value", - type: logYScale ? "log" : "linear", - gridwidth: 1, - automargin: true, + }, + { + type: "scatter", + x: infeasibleValues, + y: infeasibleObjectiveValues, + mode: "markers", + name: "Infeasible Trial", + marker: { + color: "#cccccc", + reversescale: true, }, - showlegend: false, - uirevision: "true", - template: mode === "dark" ? plotlyDarkTemplate : {}, + }, + ] + if (selectedParamSpace.distribution.type !== "CategoricalDistribution") { + layout.xaxis = { + title: selectedParamTarget.toLabel(), + type: isLogScale(selectedParamSpace) ? "log" : "linear", + gridwidth: 1, + automargin: true, // Otherwise the label is outside of the plot } - if ( - selectedParamSpace === null || - selectedParamTarget === null || - trials.length === 0 - ) { - return plotly.react(getPlotSliceDomId(), [], layout) - } - - const feasibleTrials: Optuna.Trial[] = [] - const infeasibleTrials: Optuna.Trial[] = [] - // biome-ignore lint/complexity/noForEach: - trials.forEach((t) => { - if (t.constraints.every((c) => c <= 0)) { - feasibleTrials.push(t) - } else { - infeasibleTrials.push(t) - } - }) - - const feasibleObjectiveValues: number[] = feasibleTrials.map( - (t) => objectiveTarget.getTargetValue(t) as number - ) - const infeasibleObjectiveValues: number[] = infeasibleTrials.map( - (t) => objectiveTarget.getTargetValue(t) as number - ) - - const feasibleValues = feasibleTrials.map( - (t) => selectedParamTarget.getTargetValue(t) as number + } else { + const vocabArr = selectedParamSpace.distribution.choices.map( + (c) => c?.toString() ?? "null" ) - const infeasibleValues = infeasibleTrials.map( - (t) => selectedParamTarget.getTargetValue(t) as number - ) - const trace: plotly.Data[] = [ - { - type: "scatter", - x: feasibleValues, - y: feasibleObjectiveValues, - mode: "markers", - name: "Feasible Trial", - marker: { - color: feasibleTrials.map((t) => t.number), - colorscale: "Blues", - reversescale: true, - colorbar: { - title: "Trial", - }, - line: { - color: "Grey", - width: 0.5, - }, - }, - }, - { - type: "scatter", - x: infeasibleValues, - y: infeasibleObjectiveValues, - mode: "markers", - name: "Infeasible Trial", - marker: { - color: "#cccccc", - reversescale: true, - }, - }, - ] - if (selectedParamSpace.distribution.type !== "CategoricalDistribution") { - layout.xaxis = { - title: selectedParamTarget.toLabel(), - type: isLogScale(selectedParamSpace) ? "log" : "linear", - gridwidth: 1, - automargin: true, // Otherwise the label is outside of the plot - } - } else { - const vocabArr = selectedParamSpace.distribution.choices.map( - (c) => c?.toString() ?? "null" - ) - const tickvals: number[] = vocabArr.map((_v, i) => i) - layout.xaxis = { - title: selectedParamTarget.toLabel(), - type: "linear", - gridwidth: 1, - tickvals: tickvals, - ticktext: vocabArr, - automargin: true, // Otherwise the label is outside of the plot - } + const tickvals: number[] = vocabArr.map((_v, i) => i) + layout.xaxis = { + title: selectedParamTarget.toLabel(), + type: "linear", + gridwidth: 1, + tickvals: tickvals, + ticktext: vocabArr, + automargin: true, // Otherwise the label is outside of the plot } - return plotly.react(getPlotSliceDomId(), trace, layout) - } \ No newline at end of file + } + return plotly.react(getPlotSliceDomId(), trace, layout) +} diff --git a/tslib/react/src/components/PlotSliceDark.stories.tsx b/tslib/react/src/components/PlotSliceDark.stories.tsx index d2e85e268..b9b67eb7c 100644 --- a/tslib/react/src/components/PlotSliceDark.stories.tsx +++ b/tslib/react/src/components/PlotSliceDark.stories.tsx @@ -37,4 +37,4 @@ export const MockStudyExample1: Story = { parameters: { studyId: 1, }, -} \ No newline at end of file +} diff --git a/tslib/react/test/PlotSlice.test.tsx b/tslib/react/test/PlotSlice.test.tsx index 889975716..d0a7a41ee 100644 --- a/tslib/react/test/PlotSlice.test.tsx +++ b/tslib/react/test/PlotSlice.test.tsx @@ -29,4 +29,4 @@ describe("PlotSlice Tests", async () => { expect(screen.getByTestId(`plot-slice-${study.id}`)).toBeInTheDocument() }) } -}) \ No newline at end of file +}) From a2ef1ca6661b685672d4d5692f6826f8bd7c9e55 Mon Sep 17 00:00:00 2001 From: porink0424 Date: Wed, 26 Jun 2024 11:48:35 +0900 Subject: [PATCH 7/7] Remove exports of getPlotDomId functions --- optuna_dashboard/ts/components/GraphEdf.tsx | 10 +++------- optuna_dashboard/ts/components/GraphSlice.tsx | 7 ++++--- tslib/react/src/components/PlotEdf.tsx | 5 ++--- tslib/react/src/components/PlotSlice.tsx | 12 ++++++------ tslib/react/src/index.ts | 4 ++-- 5 files changed, 17 insertions(+), 21 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphEdf.tsx b/optuna_dashboard/ts/components/GraphEdf.tsx index 30de9868f..234b2e6fe 100644 --- a/optuna_dashboard/ts/components/GraphEdf.tsx +++ b/optuna_dashboard/ts/components/GraphEdf.tsx @@ -1,9 +1,4 @@ -import { - GraphContainer, - PlotEdf, - getPlotEdfDomId, - useGraphComponentState, -} from "@optuna/react" +import { GraphContainer, PlotEdf, useGraphComponentState } from "@optuna/react" import * as plotly from "plotly.js-dist-min" import React, { FC, useEffect } from "react" import { StudyDetail } from "ts/types/optuna" @@ -22,6 +17,8 @@ export const GraphEdf: FC<{ } } +const domId = "graph-edf" + const GraphEdfBackend: FC<{ studies: StudyDetail[] }> = ({ studies }) => { @@ -29,7 +26,6 @@ const GraphEdfBackend: FC<{ const { graphComponentState, notifyGraphDidRender } = useGraphComponentState() const studyIds = studies.map((s) => s.id) - const domId = getPlotEdfDomId(-1) const numCompletedTrials = studies.reduce( (acc, study) => acc + study?.trials.filter((t) => t.state === "Complete").length, diff --git a/optuna_dashboard/ts/components/GraphSlice.tsx b/optuna_dashboard/ts/components/GraphSlice.tsx index ecac22835..d758c6619 100644 --- a/optuna_dashboard/ts/components/GraphSlice.tsx +++ b/optuna_dashboard/ts/components/GraphSlice.tsx @@ -1,7 +1,6 @@ import { GraphContainer, PlotSlice, - getPlotSliceDomId, useGraphComponentState, } from "@optuna/react" import * as plotly from "plotly.js-dist-min" @@ -21,6 +20,8 @@ export const GraphSlice: FC<{ } } +const domId = "graph-slice" + const GraphSliceBackend: FC<{ study: StudyDetail | null }> = ({ study = null }) => { @@ -38,7 +39,7 @@ const GraphSliceBackend: FC<{ useEffect(() => { if (data && layout && graphComponentState !== "componentWillMount") { - plotly.react(getPlotSliceDomId(), data, layout).then(notifyGraphDidRender) + plotly.react(domId, data, layout).then(notifyGraphDidRender) } }, [data, layout, graphComponentState]) useEffect(() => { @@ -49,7 +50,7 @@ const GraphSliceBackend: FC<{ return ( ) diff --git a/tslib/react/src/components/PlotEdf.tsx b/tslib/react/src/components/PlotEdf.tsx index d4dce2cb2..56dce17e2 100644 --- a/tslib/react/src/components/PlotEdf.tsx +++ b/tslib/react/src/components/PlotEdf.tsx @@ -12,8 +12,7 @@ export type EdfPlotInfo = { trials: Optuna.Trial[] } -export const getPlotEdfDomId = (objectiveId: number) => - `graph-edf-${objectiveId}` +const getPlotDomId = (objectiveId: number) => `plot-edf-${objectiveId}` export const PlotEdf: FC<{ studies: Optuna.Study[] @@ -23,7 +22,7 @@ export const PlotEdf: FC<{ const theme = useTheme() - const domId = getPlotEdfDomId(objectiveId) + const domId = getPlotDomId(objectiveId) const target = useMemo( () => new Target("objective", objectiveId), [objectiveId] diff --git a/tslib/react/src/components/PlotSlice.tsx b/tslib/react/src/components/PlotSlice.tsx index 40cf95bfa..3a70c5e86 100644 --- a/tslib/react/src/components/PlotSlice.tsx +++ b/tslib/react/src/components/PlotSlice.tsx @@ -23,8 +23,6 @@ import { import { GraphContainer } from "./GraphContainer" import { plotlyDarkTemplate } from "./PlotlyDarkMode" -export const getPlotSliceDomId = () => "graph-slice" - const isLogScale = (s: Optuna.SearchSpaceItem): boolean => { if (s.distribution.type === "CategoricalDistribution") { return false @@ -32,6 +30,8 @@ const isLogScale = (s: Optuna.SearchSpaceItem): boolean => { return s.distribution.log } +const domId = "plot-slice" + export const PlotSlice: FC<{ study: Optuna.Study | null }> = ({ study = null }) => { @@ -146,7 +146,7 @@ export const PlotSlice: FC<{ @@ -162,7 +162,7 @@ const plotSlice = ( logYScale: boolean, mode: string ) => { - if (document.getElementById(getPlotSliceDomId()) === null) { + if (document.getElementById(domId) === null) { return } @@ -197,7 +197,7 @@ const plotSlice = ( selectedParamTarget === null || trials.length === 0 ) { - return plotly.react(getPlotSliceDomId(), [], layout) + return plotly.react(domId, [], layout) } const feasibleTrials: Optuna.Trial[] = [] @@ -277,5 +277,5 @@ const plotSlice = ( automargin: true, // Otherwise the label is outside of the plot } } - return plotly.react(getPlotSliceDomId(), trace, layout) + return plotly.react(domId, trace, layout) } diff --git a/tslib/react/src/index.ts b/tslib/react/src/index.ts index 2b24e6a9c..715189387 100644 --- a/tslib/react/src/index.ts +++ b/tslib/react/src/index.ts @@ -1,11 +1,11 @@ export { DataGrid } from "./components/DataGrid" export { plotlyDarkTemplate } from "./components/PlotlyDarkMode" -export { PlotEdf, getPlotEdfDomId } from "./components/PlotEdf" +export { PlotEdf } from "./components/PlotEdf" export type { EdfPlotInfo } from "./components/PlotEdf" export { PlotHistory } from "./components/PlotHistory" export { PlotImportance } from "./components/PlotImportance" export { PlotIntermediateValues } from "./components/PlotIntermediateValues" -export { PlotSlice, getPlotSliceDomId } from "./components/PlotSlice" +export { PlotSlice } from "./components/PlotSlice" export { TrialTable } from "./components/TrialTable" export { GraphContainer } from "./components/GraphContainer" export { useGraphComponentState } from "./hooks/useGraphComponentState"