Skip to content

Commit

Permalink
Merge pull request optuna#893 from porink0424/fix/move-graphSlice-to-…
Browse files Browse the repository at this point in the history
…tslib

Move `GraphSlice` from `optuna_dashboard/ts` to `tslib/react`
  • Loading branch information
c-bata authored Jun 26, 2024
2 parents 49fcfaa + a2ef1ca commit 42339ca
Show file tree
Hide file tree
Showing 8 changed files with 405 additions and 288 deletions.
10 changes: 3 additions & 7 deletions optuna_dashboard/ts/components/GraphEdf.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
GraphContainer,
PlotEdf,
getPlotDomId,
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"
Expand All @@ -22,14 +17,15 @@ export const GraphEdf: FC<{
}
}

const domId = "graph-edf"

const GraphEdfBackend: FC<{
studies: StudyDetail[]
}> = ({ studies }) => {
const { apiClient } = useAPIClient()
const { graphComponentState, notifyGraphDidRender } = useGraphComponentState()

const studyIds = studies.map((s) => s.id)
const domId = getPlotDomId(-1)
const numCompletedTrials = studies.reduce(
(acc, study) =>
acc + study?.trials.filter((t) => t.state === "Complete").length,
Expand Down
288 changes: 9 additions & 279 deletions optuna_dashboard/ts/components/GraphSlice.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,27 @@
import {
FormControl,
FormLabel,
Grid,
MenuItem,
Select,
SelectChangeEvent,
Switch,
Typography,
useTheme,
} from "@mui/material"
import {
GraphContainer,
PlotSlice,
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
}> = ({ study = null }) => {
if (useBackendRender()) {
return <GraphSliceBackend study={study} />
} else {
return <GraphSliceFrontend study={study} />
return <PlotSlice study={study} />
}
}

const domId = "graph-slice"

const GraphSliceBackend: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
Expand All @@ -64,7 +39,7 @@ const GraphSliceBackend: FC<{

useEffect(() => {
if (data && layout && graphComponentState !== "componentWillMount") {
plotly.react(plotDomId, data, layout).then(notifyGraphDidRender)
plotly.react(domId, data, layout).then(notifyGraphDidRender)
}
}, [data, layout, graphComponentState])
useEffect(() => {
Expand All @@ -75,253 +50,8 @@ const GraphSliceBackend: FC<{

return (
<GraphContainer
plotDomId={plotDomId}
plotDomId={domId}
graphComponentState={graphComponentState}
/>
)
}

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<boolean>(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<string>) => {
setObjectiveTarget(event.target.value)
}

const handleSelectedParam = (e: SelectChangeEvent<string>) => {
setParamTarget(e.target.value)
}

const handleLogYScaleChange = () => {
setLogYScale(!logYScale)
}

return (
<Grid container direction="row">
<Grid
item
xs={3}
container
direction="column"
sx={{ paddingRight: theme.spacing(2) }}
>
<Typography
variant="h6"
sx={{ margin: "1em 0", fontWeight: theme.typography.fontWeightBold }}
>
Slice
</Typography>
{objectiveTargets.length !== 1 && (
<FormControl component="fieldset">
<FormLabel component="legend">Objective:</FormLabel>
<Select
value={selectedObjective.identifier()}
onChange={handleObjectiveChange}
>
{objectiveTargets.map((t, i) => (
<MenuItem value={t.identifier()} key={i}>
{t.toLabel(study?.objective_names)}
</MenuItem>
))}
</Select>
</FormControl>
)}
{paramTargets.length !== 0 && selectedParamTarget !== null && (
<FormControl component="fieldset">
<FormLabel component="legend">Parameter:</FormLabel>
<Select
value={selectedParamTarget.identifier()}
onChange={handleSelectedParam}
>
{paramTargets.map((t, i) => (
<MenuItem value={t.identifier()} key={i}>
{t.toLabel()}
</MenuItem>
))}
</Select>
</FormControl>
)}
<FormControl component="fieldset">
<FormLabel component="legend">Log y scale:</FormLabel>
<Switch
checked={logYScale}
onChange={handleLogYScaleChange}
value="enable"
/>
</FormControl>
</Grid>
<Grid item xs={9}>
<GraphContainer
plotDomId={plotDomId}
graphComponentState={graphComponentState}
/>
</Grid>
</Grid>
)
}

const plotSlice = (
trials: Optuna.Trial[],
objectiveTarget: Target,
selectedParamTarget: Target | null,
selectedParamSpace: SearchSpaceItem | null,
logYScale: boolean,
colorTheme: Partial<Plotly.Template>
) => {
if (document.getElementById(plotDomId) === null) {
return
}

const layout: Partial<plotly.Layout> = {
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)
}
2 changes: 1 addition & 1 deletion tslib/react/src/components/PlotEdf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type EdfPlotInfo = {
trials: Optuna.Trial[]
}

export const getPlotDomId = (objectiveId: number) => `graph-edf-${objectiveId}`
const getPlotDomId = (objectiveId: number) => `plot-edf-${objectiveId}`

export const PlotEdf: FC<{
studies: Optuna.Study[]
Expand Down
37 changes: 37 additions & 0 deletions tslib/react/src/components/PlotSlice.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof PlotSlice> = {
component: PlotSlice,
title: "PlotSlice",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const { study } = useMockStudy(storyContext.parameters?.studyId)
if (!study) return <p>loading...</p>
return (
<ThemeProvider theme={lightTheme}>
<CssBaseline />
<Story
args={{
study,
}}
/>
</ThemeProvider>
)
},
],
}

export default meta
type Story = StoryObj<typeof PlotSlice>

export const MockStudyExample1: Story = {
parameters: {
studyId: 1,
},
}
Loading

0 comments on commit 42339ca

Please sign in to comment.