diff --git a/optuna_dashboard/ts/action.ts b/optuna_dashboard/ts/action.ts index 0c448c2e..a0037f46 100644 --- a/optuna_dashboard/ts/action.ts +++ b/optuna_dashboard/ts/action.ts @@ -3,7 +3,6 @@ import { useSnackbar } from "notistack" import { getStudyDetailAPI, getStudySummariesAPI, - getParamImportances, createNewStudyAPI, deleteStudyAPI, saveStudyNoteAPI, @@ -25,7 +24,6 @@ import { import { studyDetailsState, studySummariesState, - paramImportanceState, isFileUploading, artifactIsAvailable, plotlypyIsAvailableState, @@ -43,8 +41,6 @@ export const actionCreator = () => { const [studyDetails, setStudyDetails] = useRecoilState(studyDetailsState) const setReloadInterval = useSetRecoilState(reloadIntervalState) - const [paramImportance, setParamImportance] = - useRecoilState(paramImportanceState) const setUploading = useSetRecoilState(isFileUploading) const setTrialsUpdating = useSetRecoilState(trialsUpdatingState) const setArtifactIsAvailable = useSetRecoilState(artifactIsAvailable) @@ -207,15 +203,6 @@ export const actionCreator = () => { setStudyDetailState(studyId, newStudy) } - const setStudyParamImportanceState = ( - studyId: number, - importance: ParamImportance[][] - ) => { - const newVal = Object.assign({}, paramImportance) - newVal[studyId] = importance - setParamImportance(newVal) - } - const updateAPIMeta = () => { getMetaInfoAPI().then((r) => { setArtifactIsAvailable(r.artifact_is_available) @@ -273,22 +260,6 @@ export const actionCreator = () => { }) } - const updateParamImportance = (studyId: number) => { - getParamImportances(studyId) - .then((importance) => { - setStudyParamImportanceState(studyId, importance) - }) - .catch((err) => { - const reason = err.response?.data.reason - enqueueSnackbar( - `Failed to load hyperparameter importance (reason=${reason})`, - { - variant: "error", - } - ) - }) - } - const createNewStudy = (studyName: string, directions: StudyDirection[]) => { createNewStudyAPI(studyName, directions) .then((study_summary) => { @@ -714,7 +685,6 @@ export const actionCreator = () => { updateAPIMeta, updateStudyDetail, updateStudySummaries, - updateParamImportance, createNewStudy, deleteStudy, renameStudy, diff --git a/optuna_dashboard/ts/components/App.tsx b/optuna_dashboard/ts/components/App.tsx index 4772687d..7c0cbff6 100644 --- a/optuna_dashboard/ts/components/App.tsx +++ b/optuna_dashboard/ts/components/App.tsx @@ -15,6 +15,18 @@ import { import { CompareStudies } from "./CompareStudies" import { StudyDetail } from "./StudyDetail" import { StudyList } from "./StudyList" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + }, + }, +}) export const App: FC = () => { const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)") @@ -38,95 +50,99 @@ export const App: FC = () => { } return ( - - - - - - - - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - } - /> - } - /> - - - - - - + + + + + + + + + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + } + /> + + + + + + + ) } diff --git a/optuna_dashboard/ts/components/GraphContour.tsx b/optuna_dashboard/ts/components/GraphContour.tsx index 67567933..d827194d 100644 --- a/optuna_dashboard/ts/components/GraphContour.tsx +++ b/optuna_dashboard/ts/components/GraphContour.tsx @@ -15,8 +15,9 @@ import blue from "@mui/material/colors/blue" import { useMergedUnionSearchSpace } from "../searchSpace" import { usePlotlyColorTheme } from "../state" import { getAxisInfo } from "../graphUtil" -import { getPlotAPI, PlotType } from "../apiClient" +import { PlotType } from "../apiClient" import { useBackendRender } from "../state" +import { usePlot } from "../hooks/usePlot" const plotDomId = "graph-contour" @@ -36,18 +37,23 @@ const ContourBackend: FC<{ const studyId = study?.id const numCompletedTrials = study?.trials.filter((t) => t.state === "Complete").length || 0 + const { data, layout, error } = usePlot({ + numCompletedTrials, + studyId, + plotType: PlotType.Contour, + }) + useEffect(() => { - if (studyId === undefined) { - return + if (data && layout) { + plotly.react(plotDomId, data, layout) } - getPlotAPI(studyId, PlotType.Contour) - .then(({ data, layout }) => { - plotly.react(plotDomId, data, layout) - }) - .catch((err) => { - console.error(err) - }) - }, [studyId, numCompletedTrials]) + }, [data, layout]) + useEffect(() => { + if (error) { + console.error(error) + } + }, [error]) + return } diff --git a/optuna_dashboard/ts/components/GraphHyperparameterImportances.tsx b/optuna_dashboard/ts/components/GraphHyperparameterImportances.tsx index deee6536..88cce45f 100644 --- a/optuna_dashboard/ts/components/GraphHyperparameterImportances.tsx +++ b/optuna_dashboard/ts/components/GraphHyperparameterImportances.tsx @@ -2,9 +2,8 @@ import * as plotly from "plotly.js-dist-min" import React, { FC, useEffect } from "react" import { Typography, useTheme, Box, Card, CardContent } from "@mui/material" -import { actionCreator } from "../action" -import { useParamImportanceValue, useStudyDirections } from "../state" -import { usePlotlyColorTheme } from "../state" +import { useParamImportance } from "../hooks/useParamImportance" +import { useStudyDirections, usePlotlyColorTheme } from "../state" const plotDomId = "graph-hyperparameter-importances" @@ -16,10 +15,12 @@ export const GraphHyperparameterImportance: FC<{ const theme = useTheme() const colorTheme = usePlotlyColorTheme(theme.palette.mode) - const action = actionCreator() - const importances = useParamImportanceValue(studyId) const numCompletedTrials = study?.trials.filter((t) => t.state === "Complete").length || 0 + const { importances } = useParamImportance({ + numCompletedTrials, + studyId, + }) const nObjectives = useStudyDirections(studyId)?.length const objectiveNames: string[] = study?.objective_names || @@ -27,11 +28,7 @@ export const GraphHyperparameterImportance: FC<{ [] useEffect(() => { - action.updateParamImportance(studyId) - }, [numCompletedTrials]) - - useEffect(() => { - if (importances !== null && nObjectives === importances.length) { + if (importances !== undefined && nObjectives === importances.length) { plotParamImportance(importances, objectiveNames, colorTheme) } }, [nObjectives, importances, colorTheme]) diff --git a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx index ec1e3430..e6c3fec0 100644 --- a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx +++ b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx @@ -17,8 +17,9 @@ import { useParamTargets, } from "../trialFilter" import { useMergedUnionSearchSpace } from "../searchSpace" -import { getPlotAPI, PlotType } from "../apiClient" +import { PlotType } from "../apiClient" import { useBackendRender } from "../state" +import { usePlot } from "../hooks/usePlot" const plotDomId = "graph-parallel-coordinate" @@ -102,18 +103,24 @@ const GraphParallelCoordinateBackend: FC<{ const studyId = study?.id const numCompletedTrials = study?.trials.filter((t) => t.state === "Complete").length || 0 + + const { data, layout, error } = usePlot({ + numCompletedTrials, + studyId, + plotType: PlotType.ParallelCoordinate, + }) + useEffect(() => { - if (studyId === undefined) { - return + if (data && layout) { + plotly.react(plotDomId, data, layout) } - getPlotAPI(studyId, PlotType.ParallelCoordinate) - .then(({ data, layout }) => { - plotly.react(plotDomId, data, layout) - }) - .catch((err) => { - console.error(err) - }) - }, [studyId, numCompletedTrials]) + }, [data, layout]) + useEffect(() => { + if (error) { + console.error(error) + } + }, [error]) + return } diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index 280476f3..a059322c 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -13,8 +13,9 @@ import { } from "@mui/material" import { getAxisInfo, makeHovertext } from "../graphUtil" import { useMergedUnionSearchSpace } from "../searchSpace" +import { PlotType } from "../apiClient" import { usePlotlyColorTheme, useBackendRender } from "../state" -import { getPlotAPI, PlotType } from "../apiClient" +import { usePlot } from "../hooks/usePlot" const plotDomId = "graph-rank" @@ -46,18 +47,24 @@ const GraphRankBackend: FC<{ const studyId = study?.id const numCompletedTrials = study?.trials.filter((t) => t.state === "Complete").length || 0 + + const { data, layout, error } = usePlot({ + numCompletedTrials, + studyId, + plotType: PlotType.Rank, + }) + useEffect(() => { - if (studyId === undefined) { - return + if (data && layout) { + plotly.react(plotDomId, data, layout) } - getPlotAPI(studyId, PlotType.Rank) - .then(({ data, layout }) => { - plotly.react(plotDomId, data, layout) - }) - .catch((err) => { - console.error(err) - }) - }, [studyId, numCompletedTrials]) + }, [data, layout]) + useEffect(() => { + if (error) { + console.error(error) + } + }, [error]) + return } diff --git a/optuna_dashboard/ts/components/GraphSlice.tsx b/optuna_dashboard/ts/components/GraphSlice.tsx index fcea5956..b41a180c 100644 --- a/optuna_dashboard/ts/components/GraphSlice.tsx +++ b/optuna_dashboard/ts/components/GraphSlice.tsx @@ -19,8 +19,9 @@ import { useParamTargets, } from "../trialFilter" import { useMergedUnionSearchSpace } from "../searchSpace" +import { PlotType } from "../apiClient" import { usePlotlyColorTheme, useBackendRender } from "../state" -import { getPlotAPI, PlotType } from "../apiClient" +import { usePlot } from "../hooks/usePlot" const plotDomId = "graph-slice" @@ -47,18 +48,24 @@ const GraphSliceBackend: FC<{ const studyId = study?.id const numCompletedTrials = study?.trials.filter((t) => t.state === "Complete").length || 0 + + const { data, layout, error } = usePlot({ + numCompletedTrials, + studyId, + plotType: PlotType.Slice, + }) + useEffect(() => { - if (studyId === undefined) { - return + if (data && layout) { + plotly.react(plotDomId, data, layout) } - getPlotAPI(studyId, PlotType.Slice) - .then(({ data, layout }) => { - plotly.react(plotDomId, data, layout) - }) - .catch((err) => { - console.error(err) - }) - }, [studyId, numCompletedTrials]) + }, [data, layout]) + useEffect(() => { + if (error) { + console.error(error) + } + }, [error]) + return } diff --git a/optuna_dashboard/ts/hooks/useParamImportance.ts b/optuna_dashboard/ts/hooks/useParamImportance.ts new file mode 100644 index 00000000..173f1d32 --- /dev/null +++ b/optuna_dashboard/ts/hooks/useParamImportance.ts @@ -0,0 +1,40 @@ +import { useEffect } from "react" +import { useSnackbar } from "notistack" +import { getParamImportances } from "../../ts/apiClient" +import { useQuery } from "@tanstack/react-query" +import { AxiosError } from "axios" + +export const useParamImportance = ({ + numCompletedTrials, + studyId, +}: { numCompletedTrials: number; studyId: number }) => { + const { enqueueSnackbar } = useSnackbar() + + const { data, isLoading, error } = useQuery< + ParamImportance[][], + AxiosError<{ reason: string }> + >({ + queryKey: ["paramImportance", studyId, numCompletedTrials], + queryFn: () => getParamImportances(studyId), + staleTime: Infinity, + gcTime: 30 * 60 * 1000, // 30 minutes + }) + + useEffect(() => { + if (error) { + const reason = error.response?.data.reason + enqueueSnackbar( + `Failed to load hyperparameter importance (reason=${reason})`, + { + variant: "error", + } + ) + } + }, [error]) + + return { + importances: data, + isLoading, + error, + } +} diff --git a/optuna_dashboard/ts/hooks/usePlot.ts b/optuna_dashboard/ts/hooks/usePlot.ts new file mode 100644 index 00000000..9ce6edd9 --- /dev/null +++ b/optuna_dashboard/ts/hooks/usePlot.ts @@ -0,0 +1,37 @@ +import * as plotly from "plotly.js-dist-min" +import { useQuery } from "@tanstack/react-query" +import { AxiosError } from "axios" +import { PlotType, getPlotAPI } from "../apiClient" + +export const usePlot = ({ + numCompletedTrials, + studyId, + plotType, +}: { + numCompletedTrials: number + studyId: number | undefined + plotType: PlotType +}) => { + const { data, isLoading, error } = useQuery< + { data: plotly.Data[]; layout: plotly.Layout }, + AxiosError + >({ + enabled: studyId !== undefined, + queryKey: ["plot", studyId, numCompletedTrials, plotType], + queryFn: () => { + if (studyId === undefined) { + return Promise.reject(new Error("Invalid studyId")) + } + return getPlotAPI(studyId, plotType) + }, + staleTime: Infinity, + gcTime: 30 * 60 * 1000, // 30 minutes + }) + + return { + data: data?.data, + layout: data?.layout, + isLoading, + error, + } +} diff --git a/optuna_dashboard/ts/state.ts b/optuna_dashboard/ts/state.ts index 8ca7586a..dd07319e 100644 --- a/optuna_dashboard/ts/state.ts +++ b/optuna_dashboard/ts/state.ts @@ -22,11 +22,6 @@ export const trialsUpdatingState = atom<{ default: {}, }) -export const paramImportanceState = atom({ - key: "paramImportance", - default: {}, -}) - // TODO(c-bata): Consider representing the state as boolean. export const reloadIntervalState = atom({ key: "reloadInterval", @@ -81,14 +76,6 @@ export const useTrialUpdatingValue = (trialId: number): boolean => { return updating[trialId] || false } -export const useParamImportanceValue = ( - studyId: number -): ParamImportance[][] | null => { - const studyParamImportance = - useRecoilValue(paramImportanceState) - return studyParamImportance[studyId] || null -} - export const useStudyDirections = ( studyId: number ): StudyDirection[] | null => { diff --git a/optuna_dashboard/ts/types/index.d.ts b/optuna_dashboard/ts/types/index.d.ts index 30d6708f..5127c22c 100644 --- a/optuna_dashboard/ts/types/index.d.ts +++ b/optuna_dashboard/ts/types/index.d.ts @@ -226,10 +226,6 @@ type StudyDetails = { [study_id: string]: StudyDetail } -type StudyParamImportance = { - [study_id: string]: ParamImportance[][] -} - type PreferenceHistory = { id: string candidates: number[] diff --git a/package-lock.json b/package-lock.json index f6a8bdc3..ea4f2825 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@mui/material": "^5.15.6", "@react-three/drei": "^9.96.4", "@react-three/fiber": "^8.15.15", + "@tanstack/react-query": "^5.18.1", "@types/three": "^0.160.0", "axios": "^1.6.7", "elkjs": "^0.9.1", @@ -4247,6 +4248,30 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.18.1.tgz", + "integrity": "sha512-fYhrG7bHgSNbnkIJF2R4VUXb4lF7EBiQjKkDc5wOlB7usdQOIN4LxxHpDxyE3qjqIst1WBGvDtL48T0sHJGKCw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.18.1.tgz", + "integrity": "sha512-PdI07BbsahZ+04PxSuDQsQvBWe008eWFk/YYWzt8fvzt2sALUM0TpAJa/DFpqa7+SSo7j1EQR6Jx6znXNHyaXw==", + "dependencies": { + "@tanstack/query-core": "5.18.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@testing-library/dom": { "version": "9.3.4", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", @@ -18499,6 +18524,19 @@ "@sinonjs/commons": "^3.0.0" } }, + "@tanstack/query-core": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.18.1.tgz", + "integrity": "sha512-fYhrG7bHgSNbnkIJF2R4VUXb4lF7EBiQjKkDc5wOlB7usdQOIN4LxxHpDxyE3qjqIst1WBGvDtL48T0sHJGKCw==" + }, + "@tanstack/react-query": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.18.1.tgz", + "integrity": "sha512-PdI07BbsahZ+04PxSuDQsQvBWe008eWFk/YYWzt8fvzt2sALUM0TpAJa/DFpqa7+SSo7j1EQR6Jx6znXNHyaXw==", + "requires": { + "@tanstack/query-core": "5.18.1" + } + }, "@testing-library/dom": { "version": "9.3.4", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", diff --git a/package.json b/package.json index 08bd980d..bfefbb00 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@mui/material": "^5.15.6", "@react-three/drei": "^9.96.4", "@react-three/fiber": "^8.15.15", + "@tanstack/react-query": "^5.18.1", "@types/three": "^0.160.0", "axios": "^1.6.7", "elkjs": "^0.9.1",