diff --git a/.github/workflows/e2e-dashboard-tests.yml b/.github/workflows/e2e-dashboard-tests.yml index 0f293273d..71d77f0e2 100644 --- a/.github/workflows/e2e-dashboard-tests.yml +++ b/.github/workflows/e2e-dashboard-tests.yml @@ -6,8 +6,12 @@ on: paths: - '.github/workflows/e2e-dashboard-tests.yml' - '**.py' - - '**.ts' - - '**.tsx' + - 'tslib/**.ts' + - 'tslib/**.tsx' + - 'tslib/**/package.json' + - 'tslib/**/package-lock.json' + - 'optuna_dashboard/**.ts' + - 'optuna_dashboard/**.tsx' - 'optuna_dashboard/package.json' - 'optuna_dashboard/package-lock.json' - 'optuna_dashboard/tsconfig.json' @@ -37,7 +41,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: '3.10' + python-version: '3.11' architecture: x64 - name: Setup Optuna ${{ matrix.optuna-version }} diff --git a/.github/workflows/e2e-standalone-tests.yml b/.github/workflows/e2e-standalone-tests.yml index d449e2b71..a599336bd 100644 --- a/.github/workflows/e2e-standalone-tests.yml +++ b/.github/workflows/e2e-standalone-tests.yml @@ -5,6 +5,10 @@ on: - main paths: - '.github/workflows/e2e-standalone-tests.yml' + - 'tslib/**.ts' + - 'tslib/**.tsx' + - 'tslib/**/package.json' + - 'tslib/**/package-lock.json' - 'standalone_app/**.ts' - 'standalone_app/**.tsx' - 'standalone_app/package.json' @@ -34,7 +38,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: '3.10' + python-version: '3.11' architecture: x64 - name: Setup Optuna ${{ matrix.optuna-version }} diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 93c68a3ae..0aa43bd89 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -29,7 +29,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v2 with: - node-version: '18' + node-version: '20' - name: Build rustlib working-directory: rustlib run: wasm-pack build --target web diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index f183fe4e6..286947198 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -16,22 +16,16 @@ jobs: uses: actions/setup-node@v2 with: node-version: '20' - - - name: Build bundle.js - working-directory: optuna_dashboard - run: | - npm install - npm run build:prd - - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip setuptools pip install --progress-bar off wheel twine build - - run: python -m build --sdist --wheel + + - run: make python-package - run: twine check dist/* - name: Create GitHub release diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 2b0670bd8..0c5faf81c 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -13,27 +13,20 @@ jobs: id-token: write steps: - uses: actions/checkout@v2 - - name: Setup Node uses: actions/setup-node@v2 with: node-version: '20' - - - name: Build bundle.js - working-directory: optuna_dashboard - run: | - npm install - npm run build:prd - - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip setuptools pip install --progress-bar off wheel twine build - - run: python -m build --sdist --wheel + + - run: make python-package - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 1c294b336..32bf1a4a7 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' architecture: x64 - name: Install dependencies run: | @@ -26,6 +26,23 @@ jobs: - run: black --check --diff . - run: isort --check --diff . - run: mypy optuna_dashboard python_tests + build-python-package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: '20' + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools + pip install --progress-bar off wheel twine build + - run: make python-package test: runs-on: ubuntu-latest strategy: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 81adb196d..7f9107299 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,18 +9,21 @@ The repository is organized as follows: ``` . -├── optuna_dashboard/ # The Python package. -│ └── ts/ # TypeScript code for the Python package. -├── standalone_app/ # Standalone application that can be run in browser or within the WebView of the VS Code extension. -│ ├── browser_app_entry.tsx # Entry point for browser app, hosted on GitHub pages. -│ └── vscode_entry.tsx # Entry point for VS Code app, output placed under `vscode/assets`. -├── vscode/ # The VS Code extension. -├── rustlib/ # Rust library exporting Wasm functions. -│ └── pkg/ # Output directory for rustlib, installed from package.json via `"./rustlib/pkg"`. -└── tslib/ # TypeScript library shared for common use. - ├── react/ # Common React components. - ├── storage/ # Common code for handling storage. - └── types/ # Common TypeScript types. +├── optuna_dashboard/ # The Python package. +│ └── ts/ # TypeScript code for the Python package. +│ ├── index.tsx # Entry point for the Python package. +│ └── pkg_index.tsx # Entry point for Jupyter Lab extension, output placed under `optuna_dashboard/pkg/`. +├── standalone_app/ # Standalone application that can be run in browser or within the WebView of the VS Code extension. +│ ├── browser_app_entry.tsx # Entry point for browser app, hosted on GitHub pages. +│ └── vscode_entry.tsx # Entry point for VS Code extension, output placed under `vscode/assets`. +├── vscode/ # The VS Code extension. +├── jupyterlab/ # The Jupyter Lab extension. +├── rustlib/ # Rust library exporting Wasm functions. +│ └── pkg/ # Output directory for rustlib, installed from package.json via `"./rustlib/pkg"`. +└── tslib/ # TypeScript library shared for common use. + ├── react/ # Common React components. + ├── storage/ # Common code for handling storage. + └── types/ # Common TypeScript types. ``` ## Python package @@ -145,8 +148,6 @@ The release process(compiling TypeScript files, packaging Python distributions a ## Standalone Single-page Application -### Compiling Rust library and TypeScript files - Please install [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) and execute the following command. ``` @@ -155,10 +156,15 @@ $ make serve-browser-app Open http://localhost:5173/ - ## VS Code Extension ``` $ npm i -g vsce $ make vscode-extension ``` + +## Jupyter Lab Extension + +``` +$ make jupyterlab-extension +``` diff --git a/Makefile b/Makefile index 78be643b6..06dbad493 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.DEFAULT_GOAL := sdist +.DEFAULT_GOAL := python-package PYTHON ?= python3 MODE ?= dev @@ -15,7 +15,7 @@ $(RUSTLIB_OUT): rustlib/src/*.rs rustlib/Cargo.toml vscode/assets/bundle.js: $(RUSTLIB_OUT) $(STANDALONE_SRC) tslib cd standalone_app && npm install && npm run build:vscode -$(DASHBOARD_TS_OUT): $(DASHBOARD_TS_SRC) +$(DASHBOARD_TS_OUT): $(DASHBOARD_TS_SRC) tslib cd optuna_dashboard && npm install && npm run build:$(MODE) .PHONY: tslib @@ -31,24 +31,21 @@ tslib-test: tslib .PHONY: serve-browser-app serve-browser-app: tslib $(RUSTLIB_OUT) - cd standalone_app && npm install && npm run watch + cd standalone_app && npm i && npm run watch .PHONY: vscode-extension vscode-extension: vscode/assets/bundle.js - cd vscode && npm install && npm run vscode:prepublish && vsce package + cd vscode && npm i && npm run vscode:prepublish && vsce package .PHONY: jupyterlab-extension jupyterlab-extension: tslib cd optuna_dashboard && npm install && npm run build:pkg cd jupyterlab && python -m build --sdist -.PHONY: sdist -sdist: pyproject.toml $(DASHBOARD_TS_OUT) - python -m build --sdist - -.PHONY: wheel -wheel: pyproject.toml $(DASHBOARD_TS_OUT) - python -m build --wheel +.PHONY: python-package +python-package: pyproject.toml tslib + cd optuna_dashboard && npm i && npm run build:prd + python -m build --sdist --wheel .PHONY: docs docs: docs/conf.py $(RST_FILES) @@ -64,4 +61,4 @@ fmt: clean: rm -rf tslib/types/pkg tslib/storage/pkg tslib/react/pkg tslib/react/types rm -rf optuna_dashboard/public/ doc/_build/ - rm -rf rustlib/pkg standalone_app/public/ vscode/assets/ vscode/*.vsix + rm -rf rustlib/pkg vscode/assets/ vscode/*.vsix diff --git a/e2e_tests/test_dashboard/visual_regression_test.py b/e2e_tests/test_dashboard/visual_regression_test.py index ea6ff9305..3d38d6de0 100644 --- a/e2e_tests/test_dashboard/visual_regression_test.py +++ b/e2e_tests/test_dashboard/visual_regression_test.py @@ -27,7 +27,7 @@ def objective(trial: optuna.Trial) -> float: x2 = trial.suggest_float("x2", 0, 10) return (x1 - 2) ** 2 + (x2 - 5) ** 2 - study.optimize(objective, n_trials=50) + study.optimize(objective, n_trials=20) return study @@ -46,20 +46,6 @@ def objective(trial: optuna.Trial) -> float: return study -def run_single_1param_objective_study(storage: optuna.storages.InMemoryStorage) -> optuna.Study: - sampler = optuna.samplers.RandomSampler(seed=0) - study = optuna.create_study( - study_name="single-1-param", storage=storage, direction="maximize", sampler=sampler - ) - - def objective(trial: optuna.Trial) -> float: - x1 = trial.suggest_float("x1", 0, 10) - return -((x1 - 2) ** 2) - - study.optimize(objective, n_trials=50) - return study - - def run_single_dynamic_objective_study(storage: optuna.storages.InMemoryStorage) -> optuna.Study: # Single-objective study with dynamic search space sampler = optuna.samplers.RandomSampler(seed=0) @@ -78,24 +64,6 @@ def objective(trial: optuna.Trial) -> float: return study -def run_single_inf_objective_study(storage: optuna.storages.InMemoryStorage) -> optuna.Study: - # Single objective study with 'inf', '-inf', or 'nan' value - sampler = optuna.samplers.RandomSampler(seed=0) - study = optuna.create_study(study_name="single-inf", storage=storage, sampler=sampler) - - def objective(trial: optuna.Trial) -> float: - x = trial.suggest_float("x", -10, 10) - if trial.number % 3 == 0: - return float("inf") - elif trial.number % 3 == 1: - return float("-inf") - else: - return x**2 - - study.optimize(objective, n_trials=50) - return study - - def run_multi_objective_study(storage: optuna.storages.InMemoryStorage) -> optuna.Study: # Multi-objective study sampler = optuna.samplers.RandomSampler(seed=0) @@ -113,7 +81,7 @@ def objective(trial: optuna.Trial) -> tuple[float, float]: v1 = (x - 5) ** 2 + (y - 5) ** 2 return v0, v1 - study.optimize(objective, n_trials=50) + study.optimize(objective, n_trials=20) return study @@ -142,7 +110,7 @@ def objective(trial: optuna.Trial) -> tuple[float, float]: v1 = (x - 2) ** 2 + (y - 3) ** 2 return v0, v1 - study.optimize(objective, n_trials=50) + study.optimize(objective, n_trials=20) return study @@ -167,66 +135,6 @@ def objective(trial: optuna.Trial) -> float: return study -def run_single_inf_report_objective_study( - storage: optuna.storages.InMemoryStorage, -) -> optuna.Study: - # Single objective pruned after reported 'inf', '-inf', or 'nan' - sampler = optuna.samplers.RandomSampler(seed=0) - study = optuna.create_study(study_name="single-inf-report", storage=storage, sampler=sampler) - - def objective(trial: optuna.Trial) -> float: - x = trial.suggest_float("x", -10, 10) - if trial.number % 3 == 0: - trial.report(float("inf"), 1) - elif trial.number % 3 == 1: - trial.report(float("-inf"), 1) - else: - trial.report(float("nan"), 1) - - if x > 0: - raise optuna.TrialPruned() - else: - return x**2 - - study.optimize(objective, n_trials=50) - return study - - -def run_issue_410_objective_study(storage: optuna.storages.InMemoryStorage) -> optuna.Study: - # Issue 410 - sampler = optuna.samplers.RandomSampler(seed=0) - study = optuna.create_study(study_name="issue-410", storage=storage, sampler=sampler) - - def objective(trial: optuna.Trial) -> float: - trial.suggest_categorical("resample_rate", ["50ms"]) - trial.suggest_categorical("channels", ["all"]) - trial.suggest_categorical("window_size", [256]) - if trial.number > 15: - raise Exception("Unexpected error") - trial.suggest_categorical("cbow", [True]) - trial.suggest_categorical("model", ["m1"]) - - trial.set_user_attr("epochs", 0) - trial.set_user_attr("deterministic", True) - if trial.number > 10: - raise Exception("unexpeccted error") - trial.set_user_attr("folder", "/path/to/folder") - trial.set_user_attr("resample_type", "foo") - trial.set_user_attr("run_id", "0001") - return 1.0 - - study.optimize(objective, n_trials=20, catch=(Exception,)) - return study - - -def run_single_no_trials_objective_study(storage: optuna.storages.InMemoryStorage) -> optuna.Study: - # No trials single-objective study - sampler = optuna.samplers.RandomSampler(seed=0) - study = optuna.create_study(study_name="single-no-trials", storage=storage, sampler=sampler) - - return study - - def run_multi_no_trials_objective_study(storage: optuna.storages.InMemoryStorage) -> optuna.Study: # No trials multi-objective study sampler = optuna.samplers.RandomSampler(seed=0) @@ -244,15 +152,10 @@ def run_multi_no_trials_objective_study(storage: optuna.storages.InMemoryStorage [ run_single_objective_study, run_single_trial_objective_study, - run_single_1param_objective_study, run_single_dynamic_objective_study, - run_single_inf_objective_study, run_multi_objective_study, run_multi_dynamic_objective_study, run_single_pruned_without_report_objective_study, - run_single_inf_report_objective_study, - run_issue_410_objective_study, - run_single_no_trials_objective_study, run_multi_no_trials_objective_study, ], ) @@ -273,6 +176,7 @@ def test_study_list( page.goto(server_url) page.click(f"a[href='/dashboard/studies/{study_id}']") + page.wait_for_selector(".MuiTypography-body1") element = page.query_selector(".MuiTypography-body1") assert element is not None @@ -289,14 +193,15 @@ def test_study_analytics( run_study: Callable[[optuna.storages.InMemoryStorage], optuna.Study], ) -> None: study = run_study(storage) - study_id = study._study_id study_name = study.study_name url = f"{server_url}/studies/{study_id}" + page.on("console", lambda msg: print(f"error: {msg.text}") if msg.type == "error" else None) page.goto(url) page.click(f"a[href='/dashboard/studies/{study_id}/analytics']") + page.wait_for_selector(".MuiTypography-body1", timeout=60 * 1000) element = page.query_selector(".MuiTypography-body1") assert element is not None @@ -321,6 +226,7 @@ def test_trial_list( page.goto(url) page.click(f"a[href='/dashboard/studies/{study_id}/trials']") + page.wait_for_selector(".MuiTypography-body1") element = page.query_selector(".MuiTypography-body1") assert element is not None @@ -345,6 +251,7 @@ def test_trial_table( page.goto(url) page.click(f"a[href='/dashboard/studies/{study_id}/trialTable']") + page.wait_for_selector(".MuiTypography-body1") element = page.query_selector(".MuiTypography-body1") assert element is not None @@ -367,6 +274,7 @@ def test_trial_note( url = f"{server_url}/studies/{study_id}" page.goto(url) + page.wait_for_selector(".MuiTypography-body1") page.click(f"a[href='/dashboard/studies/{study_id}/note']") element = page.query_selector(".MuiTypography-body1") diff --git a/optuna_dashboard/__init__.py b/optuna_dashboard/__init__.py index 1bc74e952..04ff06869 100644 --- a/optuna_dashboard/__init__.py +++ b/optuna_dashboard/__init__.py @@ -17,4 +17,4 @@ from ._preference_setting import register_preference_feedback_component # noqa -__version__ = "0.15.1" +__version__ = "0.16.0" diff --git a/optuna_dashboard/package-lock.json b/optuna_dashboard/package-lock.json index 95bb2e824..af6c9a1db 100644 --- a/optuna_dashboard/package-lock.json +++ b/optuna_dashboard/package-lock.json @@ -23,7 +23,7 @@ "@tanstack/react-virtual": "^3.1.2", "@types/papaparse": "^5.3.14", "@types/three": "^0.160.0", - "axios": "^1.6.7", + "axios": "^1.7.4", "elkjs": "^0.9.1", "notistack": "^3.0.1", "papaparse": "^5.4.1", @@ -79,15 +79,16 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@chromatic-com/storybook": "^1.6.1", "@optuna/storage": "file:../storage", "@optuna/types": "file:../types", - "@storybook/addon-essentials": "^8.0.4", - "@storybook/addon-interactions": "^8.0.4", - "@storybook/addon-links": "^8.0.4", - "@storybook/blocks": "^8.0.4", - "@storybook/react": "^8.0.4", - "@storybook/react-vite": "^8.0.4", - "@storybook/test": "^8.0.4", + "@storybook/addon-essentials": "^8.2.9", + "@storybook/addon-interactions": "^8.2.9", + "@storybook/addon-links": "^8.2.9", + "@storybook/blocks": "^8.2.9", + "@storybook/react": "^8.2.9", + "@storybook/react-vite": "^8.2.9", + "@storybook/test": "^8.2.9", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.2", "@types/plotly.js-dist-min": "^2.3.4", @@ -95,7 +96,7 @@ "@types/react-dom": "^18.2.19", "@vitejs/plugin-react-swc": "^3.5.0", "jsdom": "^24.0.0", - "storybook": "^8.0.4", + "storybook": "^8.2.9", "typescript": "^5.2.2", "vite": "^5.1.0", "vitest": "^1.4.0" @@ -14913,8 +14914,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.7.2", - "license": "MIT", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", diff --git a/optuna_dashboard/package.json b/optuna_dashboard/package.json index b4097d557..1d2a32432 100644 --- a/optuna_dashboard/package.json +++ b/optuna_dashboard/package.json @@ -30,7 +30,7 @@ "@tanstack/react-virtual": "^3.1.2", "@types/papaparse": "^5.3.14", "@types/three": "^0.160.0", - "axios": "^1.6.7", + "axios": "^1.7.4", "elkjs": "^0.9.1", "notistack": "^3.0.1", "papaparse": "^5.4.1", diff --git a/optuna_dashboard/ts/apiClient.ts b/optuna_dashboard/ts/apiClient.ts index 0afd58691..f0edc2288 100644 --- a/optuna_dashboard/ts/apiClient.ts +++ b/optuna_dashboard/ts/apiClient.ts @@ -65,6 +65,7 @@ export interface StudyDetailResponse { has_intermediate_values: boolean note: Note is_preferential: boolean + // TODO(c-bata): Rename this to metric_names after releasing the new Jupyter Lab extension. objective_names?: string[] form_widgets?: FormWidgets preferences?: [number, number][] diff --git a/optuna_dashboard/ts/axiosClient.ts b/optuna_dashboard/ts/axiosClient.ts index dd68d7792..851ba4941 100644 --- a/optuna_dashboard/ts/axiosClient.ts +++ b/optuna_dashboard/ts/axiosClient.ts @@ -62,7 +62,7 @@ export class AxiosClient extends APIClient { union_user_attrs: res.data.union_user_attrs, has_intermediate_values: res.data.has_intermediate_values, note: res.data.note, - objective_names: res.data.objective_names, + metric_names: res.data.objective_names, form_widgets: res.data.form_widgets, is_preferential: res.data.is_preferential, feedback_component_type: res.data.feedback_component_type, diff --git a/optuna_dashboard/ts/components/GraphContour.tsx b/optuna_dashboard/ts/components/GraphContour.tsx index 76191ce8f..ade974b6f 100644 --- a/optuna_dashboard/ts/components/GraphContour.tsx +++ b/optuna_dashboard/ts/components/GraphContour.tsx @@ -130,7 +130,7 @@ const ContourFrontend: FC<{ const searchSpace = useMergedUnionSearchSpace(study?.union_search_space) const [xParam, setXParam] = useState(null) const [yParam, setYParam] = useState(null) - const objectiveNames: string[] = study?.objective_names || [] + const metricNames: string[] = study?.metric_names || [] if (xParam === null && searchSpace.length > 0) { setXParam(searchSpace[0]) @@ -182,8 +182,8 @@ const ContourFrontend: FC<{ @@ -253,7 +253,7 @@ const plotHistory = ( b: 0, }, yaxis: { - title: target.toLabel(historyPlotInfos[0].objective_names), + title: target.toLabel(historyPlotInfos[0].metric_names), type: logScale ? "log" : "linear", }, xaxis: { @@ -293,7 +293,7 @@ const plotHistory = ( y: feasibleTrials.map( (t: Optuna.Trial): number => target.getTargetValue(t) as number ), - name: `${target.toLabel(h.objective_names)} of ${h.study_name}`, + name: `${target.toLabel(h.metric_names)} of ${h.study_name}`, marker: { size: markerSize, }, diff --git a/optuna_dashboard/ts/components/GraphHyperparameterImportances.tsx b/optuna_dashboard/ts/components/GraphHyperparameterImportances.tsx index 6aadbba12..b69c1fef4 100644 --- a/optuna_dashboard/ts/components/GraphHyperparameterImportances.tsx +++ b/optuna_dashboard/ts/components/GraphHyperparameterImportances.tsx @@ -1,4 +1,4 @@ -import { Box, Card, CardContent } from "@mui/material" +import { Box, Card, CardContent, useTheme } from "@mui/material" import * as plotly from "plotly.js-dist-min" import React, { FC, useEffect } from "react" @@ -7,7 +7,7 @@ import { StudyDetail } from "ts/types/optuna" import { PlotType } from "../apiClient" import { useParamImportance } from "../hooks/useParamImportance" import { usePlot } from "../hooks/usePlot" -import { useBackendRender } from "../state" +import { useBackendRender, usePlotlyColorTheme } from "../state" const plotDomId = "graph-hyperparameter-importances" @@ -32,6 +32,8 @@ export const GraphHyperparameterImportance: FC<{ /> ) } else { + const theme = useTheme() + const colorTheme = usePlotlyColorTheme(theme.palette.mode) return ( @@ -39,6 +41,7 @@ export const GraphHyperparameterImportance: FC<{ study={study} importance={importances} graphHeight={graphHeight} + colorTheme={colorTheme} /> diff --git a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx index eae2c43e0..89f3e5e7d 100644 --- a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx +++ b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx @@ -1,13 +1,16 @@ -import { Card, CardContent } from "@mui/material" +import { Card, CardContent, useTheme } from "@mui/material" import { PlotIntermediateValues } from "@optuna/react" import React, { FC } from "react" import { Trial } from "ts/types/optuna" +import { usePlotlyColorTheme } from "../state" export const GraphIntermediateValues: FC<{ trials: Trial[] includePruned: boolean logScale: boolean }> = ({ trials, includePruned, logScale }) => { + const theme = useTheme() + const colorTheme = usePlotlyColorTheme(theme.palette.mode) return ( @@ -15,6 +18,7 @@ export const GraphIntermediateValues: FC<{ trials={trials} includePruned={includePruned} logScale={logScale} + colorTheme={colorTheme} /> diff --git a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx index 0f26025d2..d6cdcdc90 100644 --- a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx +++ b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx @@ -1,104 +1,24 @@ -import { - Checkbox, - FormControlLabel, - FormGroup, - Grid, - Typography, - useTheme, -} from "@mui/material" import { GraphContainer, + PlotParallelCoordinate, 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, ReactNode, 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 { usePlotlyColorTheme } from "../state" import { useBackendRender } from "../state" const plotDomId = "graph-parallel-coordinate" -const useTargets = ( - study: StudyDetail | null -): [Target[], SearchSpaceItem[], () => ReactNode] => { - const [targets1] = useObjectiveAndUserAttrTargets(study) - const searchSpace = useMergedUnionSearchSpace(study?.union_search_space) - const [targets2] = useParamTargets(searchSpace) - const [checked, setChecked] = useState([true]) - - const allTargets = [...targets1, ...targets2] - useEffect(() => { - if (allTargets.length !== checked.length) { - setChecked( - allTargets.map((t) => { - if (t.kind === "user_attr") { - return false - } - if (t.kind !== "params" || study === null) { - return true - } - // By default, params that is not included in intersection search space should be disabled, - // otherwise all trials are filtered. - return ( - study.intersection_search_space.find((s) => s.name === t.key) !== - undefined - ) - }) - ) - } - }, [allTargets]) - - const handleOnChange = (event: React.ChangeEvent) => { - setChecked( - checked.map((c, i) => - i.toString() === event.target.name ? event.target.checked : c - ) - ) - } - - const renderCheckBoxes = (): ReactNode => ( - - {allTargets.map((t, i) => { - return ( - i ? checked[i] : true} - onChange={handleOnChange} - name={i.toString()} - /> - } - label={t.toLabel(study?.objective_names)} - /> - ) - })} - - ) - - const targets = allTargets.filter((t, i) => - checked.length > i ? checked[i] : true - ) - return [targets, searchSpace, renderCheckBoxes] -} - export const GraphParallelCoordinate: FC<{ study: StudyDetail | null }> = ({ study = null }) => { if (useBackendRender()) { return } else { - return + return } } @@ -135,198 +55,3 @@ const GraphParallelCoordinateBackend: FC<{ /> ) } - -const GraphParallelCoordinateFrontend: FC<{ - study: StudyDetail | null -}> = ({ study = null }) => { - const { graphComponentState, notifyGraphDidRender } = useGraphComponentState() - - const theme = useTheme() - const colorTheme = usePlotlyColorTheme(theme.palette.mode) - - const [targets, searchSpace, renderCheckBoxes] = useTargets(study) - - const trials = useFilteredTrials(study, targets, false) - useEffect(() => { - if (study !== null && graphComponentState !== "componentWillMount") { - plotCoordinate(study, trials, targets, searchSpace, colorTheme)?.then( - notifyGraphDidRender - ) - } - }, [study, trials, targets, searchSpace, colorTheme, graphComponentState]) - - return ( - - - - Parallel Coordinate - - {renderCheckBoxes()} - - - - - - ) -} - -const plotCoordinate = ( - study: StudyDetail, - trials: Optuna.Trial[], - targets: Target[], - searchSpace: SearchSpaceItem[], - colorTheme: Partial -) => { - if (document.getElementById(plotDomId) === null) { - return - } - - const layout: Partial = { - margin: { - l: 70, - t: 50, - r: 50, - b: 100, - }, - template: colorTheme, - uirevision: "true", - } - if (trials.length === 0 || targets.length === 0) { - return plotly.react(plotDomId, [], layout) - } - - const maxLabelLength = 40 - const breakLength = maxLabelLength / 2 - const ellipsis = "…" - const truncateLabelIfTooLong = (originalLabel: string): string => { - return originalLabel.length > maxLabelLength - ? originalLabel.substring(0, maxLabelLength - ellipsis.length) + ellipsis - : originalLabel - } - const breakLabelIfTooLong = (originalLabel: string): string => { - const truncated = truncateLabelIfTooLong(originalLabel) - return truncated - .split("") - .map((c, i) => { - return (i + 1) % breakLength === 0 ? c + "
" : c - }) - .join("") - } - - const calculateLogScale = (values: number[]) => { - const logValues = values.map((v) => { - return Math.log10(v) - }) - const minValue = Math.min(...logValues) - const maxValue = Math.max(...logValues) - const range = [Math.floor(minValue), Math.ceil(maxValue)] - const tickvals = Array.from( - { length: Math.ceil(maxValue) - Math.floor(minValue) + 1 }, - (_, i) => i + Math.floor(minValue) - ) - const ticktext = tickvals.map((x) => `${Math.pow(10, x).toPrecision(3)}`) - return { logValues, range, tickvals, ticktext } - } - - const dimensions = targets.map((target) => { - if (target.kind === "objective" || target.kind === "user_attr") { - const values: number[] = trials.map( - (t) => target.getTargetValue(t) as number - ) - return { - label: target.toLabel(study.objective_names), - values: values, - range: [Math.min(...values), Math.max(...values)], - } - } else { - const s = searchSpace.find( - (s) => s.name === target.key - ) as SearchSpaceItem // Must be already filtered. - - const values: number[] = trials.map( - (t) => target.getTargetValue(t) as number - ) - if (s.distribution.type === "CategoricalDistribution") { - // categorical - const vocabArr: string[] = s.distribution.choices.map( - (c) => c?.toString() ?? "null" - ) - const tickvals: number[] = vocabArr.map((v, i) => i) - return { - label: breakLabelIfTooLong(s.name), - values: values, - range: [0, s.distribution.choices.length - 1], - // @ts-ignore - tickvals: tickvals, - ticktext: vocabArr, - } - } else if (s.distribution.log) { - // numerical and log - const { logValues, range, tickvals, ticktext } = - calculateLogScale(values) - return { - label: breakLabelIfTooLong(s.name), - values: logValues, - range, - tickvals, - ticktext, - } - } else { - // numerical and linear - return { - label: breakLabelIfTooLong(s.name), - values: values, - range: [s.distribution.low, s.distribution.high], - } - } - } - }) - if (dimensions.length === 0) { - console.log("Must not reach here.") - return plotly.react(plotDomId, [], layout) - } - let reversescale = false - if ( - targets[0].kind === "objective" && - (targets[0].getObjectiveId() as number) < study.directions.length && - study.directions[targets[0].getObjectiveId() as number] === "maximize" - ) { - reversescale = true - } - const plotData: Partial[] = [ - { - type: "parcoords", - dimensions: dimensions, - labelangle: 30, - labelside: "bottom", - line: { - color: dimensions[0]["values"], - // @ts-ignore - colorscale: "Blues", - colorbar: { - title: targets[0].toLabel(study.objective_names), - }, - showscale: true, - reversescale: reversescale, - }, - }, - ] - - return plotly.react(plotDomId, plotData, layout) -} diff --git a/optuna_dashboard/ts/components/GraphParetoFront.tsx b/optuna_dashboard/ts/components/GraphParetoFront.tsx index 41444ed5f..3f8214ca3 100644 --- a/optuna_dashboard/ts/components/GraphParetoFront.tsx +++ b/optuna_dashboard/ts/components/GraphParetoFront.tsx @@ -69,7 +69,7 @@ const GraphParetoFrontFrontend: FC<{ const navigate = useNavigate() const [objectiveXId, setObjectiveXId] = useState(0) const [objectiveYId, setObjectiveYId] = useState(1) - const objectiveNames: string[] = study?.objective_names || [] + const metricNames: string[] = study?.metric_names || [] const handleObjectiveXChange = (event: SelectChangeEvent) => { setObjectiveXId(event.target.value as number) @@ -130,8 +130,8 @@ const GraphParetoFrontFrontend: FC<{ {study.directions.map((d, i) => ( - {objectiveNames.length === study?.directions.length - ? objectiveNames[i] + {metricNames.length === study?.directions.length + ? metricNames[i] : `${i}`} ))} diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index bf3c0711f..c8b55a42f 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -91,7 +91,7 @@ const GraphRankFrontend: FC<{ const searchSpace = useMergedUnionSearchSpace(study?.union_search_space) const [xParam, setXParam] = useState(null) const [yParam, setYParam] = useState(null) - const objectiveNames: string[] = study?.objective_names || [] + const metricNames: string[] = study?.metric_names || [] if (xParam === null && searchSpace.length > 0) { setXParam(searchSpace[0]) @@ -150,8 +150,8 @@ const GraphRankFrontend: FC<{