Merge branch 'main' of github.com:porink0424/optuna-dashboard into followup/feature/tslib-trialtable

This commit is contained in:
porink0424
2024-08-23 12:49:26 +09:00
55 changed files with 1835 additions and 4710 deletions
+7 -3
View File
@@ -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 }}
+5 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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
+3 -9
View File
@@ -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
+3 -10
View File
@@ -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
+18 -1
View File
@@ -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:
+21 -15
View File
@@ -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
```
+9 -12
View File
@@ -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
@@ -27,7 +27,7 @@ def run_single_objective_study(storage: optuna.storages.InMemoryStorage) -> optu
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 run_single_trial_objective_study(storage: optuna.storages.InMemoryStorage) -
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 run_single_dynamic_objective_study(storage: optuna.storages.InMemoryStorage)
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 run_multi_objective_study(storage: optuna.storages.InMemoryStorage) -> optun
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 run_multi_dynamic_objective_study(storage: optuna.storages.InMemoryStorage)
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 run_single_pruned_without_report_objective_study(
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 @@ parameterize_studies = pytest.mark.parametrize(
[
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")
+1 -1
View File
@@ -17,4 +17,4 @@ from ._note import save_note # noqa
from ._preference_setting import register_preference_feedback_component # noqa
__version__ = "0.15.1"
__version__ = "0.16.0"
+13 -11
View File
@@ -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",
+1 -1
View File
@@ -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",
+1
View File
@@ -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][]
+1 -1
View File
@@ -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,
@@ -130,7 +130,7 @@ const ContourFrontend: FC<{
const searchSpace = useMergedUnionSearchSpace(study?.union_search_space)
const [xParam, setXParam] = useState<SearchSpaceItem | null>(null)
const [yParam, setYParam] = useState<SearchSpaceItem | null>(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<{
<Select value={objectiveId} onChange={handleObjectiveChange}>
{study.directions.map((d, i) => (
<MenuItem value={i} key={i}>
{objectiveNames.length === study?.directions.length
? objectiveNames[i]
{metricNames.length === study?.directions.length
? metricNames[i]
: `${i}`}
</MenuItem>
))}
@@ -32,7 +32,7 @@ interface HistoryPlotInfo {
study_name: string
trials: Optuna.Trial[]
directions: Optuna.StudyDirection[]
objective_names?: string[]
metric_names?: string[]
}
export const GraphHistory: FC<{
@@ -63,7 +63,7 @@ export const GraphHistory: FC<{
study_name: study?.name,
trials: trials[index],
directions: study?.directions,
objective_names: study?.objective_names,
metric_names: study?.metric_names,
}
return h
})
@@ -164,7 +164,7 @@ export const GraphHistory: FC<{
>
{targets.map((t, i) => (
<MenuItem value={t.identifier()} key={i}>
{t.toLabel(studies[0].objective_names)}
{t.toLabel(studies[0].metric_names)}
</MenuItem>
))}
</Select>
@@ -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,
},
@@ -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 (
<Card>
<CardContent>
@@ -39,6 +41,7 @@ export const GraphHyperparameterImportance: FC<{
study={study}
importance={importances}
graphHeight={graphHeight}
colorTheme={colorTheme}
/>
</CardContent>
</Card>
@@ -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 (
<Card>
<CardContent>
@@ -15,6 +18,7 @@ export const GraphIntermediateValues: FC<{
trials={trials}
includePruned={includePruned}
logScale={logScale}
colorTheme={colorTheme}
/>
</CardContent>
</Card>
@@ -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<boolean[]>([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<HTMLInputElement>) => {
setChecked(
checked.map((c, i) =>
i.toString() === event.target.name ? event.target.checked : c
)
)
}
const renderCheckBoxes = (): ReactNode => (
<FormGroup>
{allTargets.map((t, i) => {
return (
<FormControlLabel
key={i}
control={
<Checkbox
checked={checked.length > i ? checked[i] : true}
onChange={handleOnChange}
name={i.toString()}
/>
}
label={t.toLabel(study?.objective_names)}
/>
)
})}
</FormGroup>
)
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 <GraphParallelCoordinateBackend study={study} />
} else {
return <GraphParallelCoordinateFrontend study={study} />
return <PlotParallelCoordinate study={study} />
}
}
@@ -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 (
<Grid container direction="row">
<Grid
item
xs={3}
container
direction="column"
sx={{
paddingRight: theme.spacing(2),
display: "flex",
flexDirection: "column",
}}
>
<Typography
variant="h6"
sx={{ margin: "1em 0", fontWeight: theme.typography.fontWeightBold }}
>
Parallel Coordinate
</Typography>
{renderCheckBoxes()}
</Grid>
<Grid item xs={9}>
<GraphContainer
plotDomId={plotDomId}
graphComponentState={graphComponentState}
/>
</Grid>
</Grid>
)
}
const plotCoordinate = (
study: StudyDetail,
trials: Optuna.Trial[],
targets: Target[],
searchSpace: SearchSpaceItem[],
colorTheme: Partial<Plotly.Template>
) => {
if (document.getElementById(plotDomId) === null) {
return
}
const layout: Partial<plotly.Layout> = {
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 + "<br>" : 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<plotly.PlotData>[] = [
{
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)
}
@@ -69,7 +69,7 @@ const GraphParetoFrontFrontend: FC<{
const navigate = useNavigate()
const [objectiveXId, setObjectiveXId] = useState<number>(0)
const [objectiveYId, setObjectiveYId] = useState<number>(1)
const objectiveNames: string[] = study?.objective_names || []
const metricNames: string[] = study?.metric_names || []
const handleObjectiveXChange = (event: SelectChangeEvent<number>) => {
setObjectiveXId(event.target.value as number)
@@ -130,8 +130,8 @@ const GraphParetoFrontFrontend: FC<{
<Select value={objectiveXId} onChange={handleObjectiveXChange}>
{study.directions.map((d, i) => (
<MenuItem value={i} key={i}>
{objectiveNames.length === study?.directions.length
? objectiveNames[i]
{metricNames.length === study?.directions.length
? metricNames[i]
: `${i}`}
</MenuItem>
))}
@@ -142,8 +142,8 @@ const GraphParetoFrontFrontend: FC<{
<Select value={objectiveYId} onChange={handleObjectiveYChange}>
{study.directions.map((d, i) => (
<MenuItem value={i} key={i}>
{objectiveNames.length === study?.directions.length
? objectiveNames[i]
{metricNames.length === study?.directions.length
? metricNames[i]
: `${i}`}
</MenuItem>
))}
+3 -3
View File
@@ -91,7 +91,7 @@ const GraphRankFrontend: FC<{
const searchSpace = useMergedUnionSearchSpace(study?.union_search_space)
const [xParam, setXParam] = useState<SearchSpaceItem | null>(null)
const [yParam, setYParam] = useState<SearchSpaceItem | null>(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<{
<Select value={objectiveId} onChange={handleObjectiveChange}>
{study.directions.map((d, i) => (
<MenuItem value={i} key={i}>
{objectiveNames.length === study?.directions.length
? objectiveNames[i]
{metricNames.length === study?.directions.length
? metricNames[i]
: `${i}`}
</MenuItem>
))}
@@ -1,3 +1,4 @@
import { useTheme } from "@mui/material"
import {
GraphContainer,
PlotSlice,
@@ -8,7 +9,7 @@ import React, { FC, useEffect } from "react"
import { StudyDetail } from "ts/types/optuna"
import { PlotType } from "../apiClient"
import { usePlot } from "../hooks/usePlot"
import { useBackendRender } from "../state"
import { useBackendRender, usePlotlyColorTheme } from "../state"
export const GraphSlice: FC<{
study: StudyDetail | null
@@ -16,7 +17,9 @@ export const GraphSlice: FC<{
if (useBackendRender()) {
return <GraphSliceBackend study={study} />
} else {
return <PlotSlice study={study} />
const theme = useTheme()
const colorTheme = usePlotlyColorTheme(theme.palette.mode)
return <PlotSlice study={study} colorTheme={colorTheme} />
}
}
@@ -165,7 +165,7 @@ const CandidateTrial: FC<{
trial={trial}
isBestTrial={() => false}
directions={[]}
objectiveNames={[]}
metricNames={[]}
/>
</Box>
</Box>
@@ -629,7 +629,7 @@ export const PreferentialTrials: FC<{ studyDetail: StudyDetail | null }> = ({
?.state === "Complete" ?? false
}
directions={[]}
objectiveNames={[]}
metricNames={[]}
/>
</Box>
</Box>
@@ -35,10 +35,10 @@ type WidgetState = {
export const TrialFormWidgets: FC<{
trial: Trial
objectiveNames: string[]
metricNames: string[]
directions: Optuna.StudyDirection[]
formWidgets?: FormWidgets
}> = ({ trial, objectiveNames, directions, formWidgets }) => {
}> = ({ trial, metricNames, directions, formWidgets }) => {
if (
formWidgets === undefined ||
trial.state === "Pruned" ||
@@ -56,8 +56,8 @@ export const TrialFormWidgets: FC<{
: "Set Objective Value Form"
const widgetNames = formWidgets.widgets.map((widget, i) => {
if (formWidgets.output_type === "objective") {
if (objectiveNames.at(i) !== undefined) {
return objectiveNames[i]
if (metricNames.at(i) !== undefined) {
return metricNames[i]
}
return directions.length === 1 ? "Objective" : `Objective ${i}`
} else if (formWidgets.output_type === "user_attr") {
+4 -4
View File
@@ -126,9 +126,9 @@ export const TrialListDetail: FC<{
trial: Trial
isBestTrial: (trialId: number) => boolean
directions: Optuna.StudyDirection[]
objectiveNames: string[]
metricNames: string[]
formWidgets?: FormWidgets
}> = ({ trial, isBestTrial, directions, objectiveNames, formWidgets }) => {
}> = ({ trial, isBestTrial, directions, metricNames, formWidgets }) => {
const theme = useTheme()
const action = actionCreator()
const artifactEnabled = useRecoilValue<boolean>(artifactIsAvailable)
@@ -290,7 +290,7 @@ export const TrialListDetail: FC<{
<TrialFormWidgets
trial={trial}
directions={directions}
objectiveNames={objectiveNames}
metricNames={metricNames}
formWidgets={formWidgets}
/>
<Box
@@ -561,7 +561,7 @@ export const TrialList: FC<{ studyDetail: StudyDetail | null }> = ({
trial={t}
isBestTrial={isBestTrial}
directions={studyDetail?.directions || []}
objectiveNames={studyDetail?.objective_names || []}
metricNames={studyDetail?.metric_names || []}
formWidgets={studyDetail?.form_widgets}
/>
))}
@@ -0,0 +1,161 @@
import DownloadIcon from "@mui/icons-material/Download"
import LinkIcon from "@mui/icons-material/Link"
import { Button, IconButton, useTheme } from "@mui/material"
import React, { FC } from "react"
import { DataGrid } from "@optuna/react"
import { Link } from "react-router-dom"
import { StudyDetail, Trial } from "ts/types/optuna"
import {
ColumnDef,
FilterFn,
Row,
createColumnHelper,
} from "@tanstack/react-table"
import { useConstants } from "../constantsProvider"
const multiValueFilter: FilterFn<Trial> = <D extends object>(
row: Row<D>,
columnId: string,
filterValue: string[]
) => {
const rowValue = row.getValue(columnId) as string
return !filterValue.includes(rowValue)
}
export const TrialTable: FC<{
studyDetail: StudyDetail | null
}> = ({ studyDetail }) => {
const { url_prefix } = useConstants()
const theme = useTheme()
const trials: Trial[] = studyDetail !== null ? studyDetail.trials : []
const metricNames: string[] = studyDetail?.metric_names || []
const columnHelper = createColumnHelper<Trial>()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const columns: ColumnDef<Trial, any>[] = [
columnHelper.accessor("number", {
header: "Number",
enableColumnFilter: false,
}),
columnHelper.accessor("state", {
header: "State",
enableSorting: false,
enableColumnFilter: true,
filterFn: multiValueFilter,
}),
]
if (studyDetail === null || studyDetail.directions.length === 1) {
columns.push(
columnHelper.accessor("values", {
header: "Value",
enableSorting: true,
enableColumnFilter: false,
sortUndefined: "last",
})
)
} else {
columns.push(
...studyDetail.directions.map((s, objectiveId) =>
columnHelper.accessor((row) => row["values"]?.[objectiveId], {
id: `values_${objectiveId}`,
header:
metricNames.length === studyDetail?.directions.length
? metricNames[objectiveId]
: `Objective ${objectiveId}`,
enableSorting: true,
enableColumnFilter: false,
sortUndefined: "last",
})
)
)
}
const isDynamicSpace =
studyDetail?.union_search_space.length !==
studyDetail?.intersection_search_space.length
studyDetail?.union_search_space.forEach((s) => {
const sortable = s.distribution.type !== "CategoricalDistribution"
const filterChoices: (string | null)[] | undefined =
s.distribution.type === "CategoricalDistribution"
? s.distribution.choices.map((c) => c?.toString() ?? "null")
: undefined
const hasMissingValue = trials.some(
(t) => !t.params.some((p) => p.name === s.name)
)
if (filterChoices !== undefined && isDynamicSpace && hasMissingValue) {
filterChoices.push(null)
}
columns.push(
columnHelper.accessor(
(row) =>
row["params"].find((p) => p.name === s.name)?.param_external_value ||
null,
{
id: `params_${s.name}`,
header: `Param ${s.name}`,
enableSorting: sortable,
sortUndefined: "last",
enableColumnFilter: filterChoices !== undefined,
filterFn: multiValueFilter,
}
)
)
})
studyDetail?.union_user_attrs.forEach((attr_spec) => {
columns.push(
columnHelper.accessor(
(row) =>
row["user_attrs"].find((a) => a.key === attr_spec.key)?.value || null,
{
id: `user_attrs_${attr_spec.key}`,
header: `UserAttribute ${attr_spec.key}`,
enableSorting: attr_spec.sortable,
enableColumnFilter: false,
sortUndefined: "last",
}
)
)
})
columns.push(
columnHelper.accessor((row) => row, {
header: "Detail",
cell: (info) => (
<IconButton
component={Link}
to={
url_prefix +
`/studies/${info.getValue().study_id}/trials?numbers=${
info.getValue().number
}`
}
color="inherit"
title="Go to the trial's detail page"
size="small"
>
<LinkIcon />
</IconButton>
),
enableSorting: false,
enableColumnFilter: false,
})
)
return (
<>
<DataGrid data={trials} columns={columns} />
<Button
variant="outlined"
startIcon={<DownloadIcon />}
download
href={`/csv/${studyDetail?.id}`}
sx={{ marginRight: theme.spacing(2), minWidth: "120px" }}
>
Download CSV File
</Button>
</>
)
}
+1 -1
View File
@@ -155,7 +155,7 @@ export type StudyDetail = {
has_intermediate_values: boolean
note: Note
is_preferential: boolean
objective_names?: string[]
metric_names?: string[]
form_widgets?: FormWidgets
feedback_component_type: FeedbackComponentType
preferences?: [number, number][]
+9 -8
View File
@@ -62,15 +62,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",
@@ -78,7 +79,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"
+8 -3
View File
@@ -2,18 +2,19 @@ import type { StorybookConfig } from "@storybook/react-vite"
const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@chromatic-com/storybook",
],
framework: {
name: "@storybook/react-vite",
options: {},
},
docs: {
autodocs: "tag",
},
async viteFinal(config) {
const { mergeConfig } = await import("vite")
return mergeConfig(config, {
@@ -25,5 +26,9 @@ const config: StorybookConfig = {
},
})
},
typescript: {
reactDocgen: "react-docgen-typescript",
},
}
export default config
-14
View File
@@ -1,14 +0,0 @@
import type { Preview } from "@storybook/react"
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
}
export default preview
+32
View File
@@ -0,0 +1,32 @@
import {
Controls,
Description,
Primary,
Subtitle,
Title,
} from "@storybook/blocks"
import type { Preview } from "@storybook/react"
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
docs: {
page: () => (
<>
<Title />
<Subtitle />
<Description />
<Primary />
<Controls />
</>
),
},
},
}
export default preview
+945 -3945
View File
File diff suppressed because it is too large Load Diff
+10 -9
View File
@@ -38,15 +38,16 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
"@optuna/types": "file:../types",
"@chromatic-com/storybook": "^1.6.1",
"@optuna/storage": "file:../storage",
"@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",
"@optuna/types": "file:../types",
"@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",
@@ -54,7 +55,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"
+1 -1
View File
@@ -1,6 +1,6 @@
import * as Optuna from "@optuna/types"
import { useEffect, useState } from "react"
import { loadStorageFromFile } from "./utils/loadStorageFromFile"
import { loadStorageFromFile } from "../test/loadStorageFromFile"
const fetchMockStudies = async () => {
const filePath = "sample_db.sqlite3"
+30 -4
View File
@@ -2,19 +2,22 @@ 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 { lightTheme } from "../styles/lightTheme"
import { PlotEdf } from "./PlotEdf"
const meta: Meta<typeof PlotEdf> = {
component: PlotEdf,
title: "PlotEdf",
title: "Plot/EDF",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const { study } = useMockStudy(storyContext.parameters?.studyId)
const studyId = storyContext.parameters?.studyId
const { study } = useMockStudy(studyId)
if (!study) return <p>loading...</p>
return (
<ThemeProvider theme={lightTheme}>
<ThemeProvider theme={storyContext.parameters?.theme}>
<CssBaseline />
<Story
args={{
@@ -29,10 +32,33 @@ const meta: Meta<typeof PlotEdf> = {
}
export default meta
type Story = StoryObj<typeof PlotEdf>
export const MockStudyExample1: Story = {
export const LightTheme: Story = {
parameters: {
studyId: 1,
theme: lightTheme,
},
}
export const DarkTheme: Story = {
parameters: {
studyId: 1,
theme: darkTheme,
},
}
// TODO(c-bata): Add a story for the case where two studies are compared.
// export const CompareStudy: Story = {
// parameters: {
// ...
// },
// }
// TODO(c-bata): Add a story for multi objective study.
// export const MultiObjective: Story = {
// parameters: {
// ...
// },
// }
@@ -1,41 +0,0 @@
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 { PlotEdf } from "./PlotEdf"
const meta: Meta<typeof PlotEdf> = {
component: PlotEdf,
title: "PlotEdfDark",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const { study } = useMockStudy(storyContext.parameters?.studyId)
if (!study) return <p>loading...</p>
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<Story
args={{
studies: [study],
objectiveId: 0,
}}
/>
</ThemeProvider>
)
},
],
parameters: {
backgrounds: { default: "dark" },
},
}
export default meta
type Story = StoryObj<typeof PlotEdf>
export const MockStudyExample1: Story = {
parameters: {
studyId: 1,
},
}
@@ -2,19 +2,20 @@ 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 { lightTheme } from "../styles/lightTheme"
import { PlotHistory } from "./PlotHistory"
const meta: Meta<typeof PlotHistory> = {
component: PlotHistory,
title: "PlotHistory",
title: "Plot/History",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const { study } = useMockStudy(storyContext.parameters?.studyId)
if (!study) return <p>loading...</p>
return (
<ThemeProvider theme={lightTheme}>
<ThemeProvider theme={storyContext.parameters?.theme}>
<CssBaseline />
<Story
args={{
@@ -30,8 +31,30 @@ const meta: Meta<typeof PlotHistory> = {
export default meta
type Story = StoryObj<typeof PlotHistory>
export const MockStudyExample1: Story = {
export const LightTheme: Story = {
parameters: {
studyId: 1,
theme: lightTheme,
},
}
export const DarkTheme: Story = {
parameters: {
studyId: 1,
theme: darkTheme,
},
}
// TODO(c-bata): Add a story for the case where two studies are compared.
// export const CompareStudy: Story = {
// parameters: {
// ...
// },
// }
// TODO(c-bata): Add a story for multi objective study.
// export const MultiObjective: Story = {
// parameters: {
// ...
// },
// }
@@ -1,40 +0,0 @@
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 { PlotHistory } from "./PlotHistory"
const meta: Meta<typeof PlotHistory> = {
component: PlotHistory,
title: "PlotHistoryDark",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const { study } = useMockStudy(storyContext.parameters?.studyId)
if (!study) return <p>loading...</p>
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<Story
args={{
study,
}}
/>
</ThemeProvider>
)
},
],
parameters: {
backgrounds: { default: "dark" },
},
}
export default meta
type Story = StoryObj<typeof PlotHistory>
export const MockStudyExample1: Story = {
parameters: {
studyId: 1,
},
}
@@ -2,12 +2,13 @@ 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 { lightTheme } from "../styles/lightTheme"
import { PlotImportance } from "./PlotImportance"
const meta: Meta<typeof PlotImportance> = {
component: PlotImportance,
title: "PlotImportance",
title: "Plot/Importance",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
@@ -16,7 +17,7 @@ const meta: Meta<typeof PlotImportance> = {
)
if (!study) return <p>loading...</p>
return (
<ThemeProvider theme={lightTheme}>
<ThemeProvider theme={storyContext.parameters?.theme}>
<CssBaseline />
<Story
args={{
@@ -33,8 +34,23 @@ const meta: Meta<typeof PlotImportance> = {
export default meta
type Story = StoryObj<typeof PlotImportance>
export const MockStudyExample1: Story = {
export const LightTheme: Story = {
parameters: {
studyId: 1,
theme: lightTheme,
},
}
export const DarkTheme: Story = {
parameters: {
studyId: 1,
theme: darkTheme,
},
}
// TODO(c-bata): Add a story for multi objective study.
// export const MultiObjective: Story = {
// parameters: {
// ...
// },
// }
@@ -10,17 +10,19 @@ export const PlotImportance: FC<{
study: Optuna.Study | null
importance?: Optuna.ParamImportance[][]
graphHeight?: string
}> = ({ study = null, importance, graphHeight = "450px" }) => {
colorTheme?: Partial<Plotly.Template>
}> = ({ study = null, importance, graphHeight = "450px", colorTheme }) => {
const theme = useTheme()
const colorThemeUsed =
colorTheme ?? (theme.palette.mode === "dark" ? plotlyDarkTemplate : {})
const objectiveNames: string[] = study
? study.directions.map((_d, i) => `Objective ${i}`)
: []
useEffect(() => {
if (study !== null && importance !== undefined && importance.length > 0) {
plotParamImportancesBeta(importance, objectiveNames, theme.palette.mode)
plotParamImportancesBeta(importance, objectiveNames, colorThemeUsed)
}
}, [study, objectiveNames, importance, theme.palette.mode])
}, [study, objectiveNames, importance, colorThemeUsed])
return (
<>
@@ -38,7 +40,7 @@ export const PlotImportance: FC<{
const plotParamImportancesBeta = (
importances: Optuna.ParamImportance[][],
objectiveNames: string[],
mode: string
colorTheme: Partial<Plotly.Template>
) => {
const layout: Partial<plotly.Layout> = {
xaxis: {
@@ -58,7 +60,7 @@ const plotParamImportancesBeta = (
bargap: 0.15,
bargroupgap: 0.1,
uirevision: "true",
template: mode === "dark" ? plotlyDarkTemplate : {},
template: colorTheme,
legend: {
x: 1.0,
y: 0.95,
@@ -1,43 +0,0 @@
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 { PlotImportance } from "./PlotImportance"
const meta: Meta<typeof PlotImportance> = {
component: PlotImportance,
title: "PlotImportanceDark",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const { study, importance } = useMockStudy(
storyContext.parameters?.studyId
)
if (!study) return <p>loading...</p>
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<Story
args={{
study,
importance,
}}
/>
</ThemeProvider>
)
},
],
parameters: {
backgrounds: { default: "dark" },
},
}
export default meta
type Story = StoryObj<typeof PlotImportance>
export const MockStudyExample1: Story = {
parameters: {
studyId: 1,
},
}
@@ -2,19 +2,20 @@ 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 { lightTheme } from "../styles/lightTheme"
import { PlotIntermediateValues } from "./PlotIntermediateValues"
const meta: Meta<typeof PlotIntermediateValues> = {
component: PlotIntermediateValues,
title: "PlotIntermediateValues",
title: "Plot/IntermediateValues",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const { study } = useMockStudy(storyContext.parameters?.studyId)
if (!study) return <p>loading...</p>
return (
<ThemeProvider theme={lightTheme}>
<ThemeProvider theme={storyContext.parameters?.theme}>
<CssBaseline />
<Story
args={{
@@ -32,8 +33,16 @@ const meta: Meta<typeof PlotIntermediateValues> = {
export default meta
type Story = StoryObj<typeof PlotIntermediateValues>
export const MockStudyExample1: Story = {
export const LightTheme: Story = {
parameters: {
studyId: 1,
theme: lightTheme,
},
}
export const DarkTheme: Story = {
parameters: {
studyId: 1,
theme: darkTheme,
},
}
@@ -10,18 +10,21 @@ export const PlotIntermediateValues: FC<{
trials: Optuna.Trial[]
includePruned: boolean
logScale: boolean
}> = ({ trials, includePruned, logScale }) => {
colorTheme?: Partial<Plotly.Template>
}> = ({ trials, includePruned, logScale, colorTheme }) => {
const theme = useTheme()
const colorThemeUsed =
colorTheme ?? (theme.palette.mode === "dark" ? plotlyDarkTemplate : {})
useEffect(() => {
plotIntermediateValue(
trials,
theme.palette.mode,
colorThemeUsed,
false,
!includePruned,
logScale
)
}, [trials, theme.palette.mode, includePruned, logScale])
}, [trials, colorThemeUsed, includePruned, logScale])
return (
<>
@@ -38,7 +41,7 @@ export const PlotIntermediateValues: FC<{
const plotIntermediateValue = (
trials: Optuna.Trial[],
mode: string,
colorTheme: Partial<Plotly.Template>,
filterCompleteTrial: boolean,
filterPrunedTrial: boolean,
logScale: boolean
@@ -63,7 +66,7 @@ const plotIntermediateValue = (
type: "linear",
},
uirevision: "true",
template: mode === "dark" ? plotlyDarkTemplate : {},
template: colorTheme,
legend: {
x: 1.0,
y: 0.95,
@@ -1,42 +0,0 @@
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 { PlotIntermediateValues } from "./PlotIntermediateValues"
const meta: Meta<typeof PlotIntermediateValues> = {
component: PlotIntermediateValues,
title: "PlotIntermediateValuesDark",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const { study } = useMockStudy(storyContext.parameters?.studyId)
if (!study) return <p>loading...</p>
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<Story
args={{
trials: study.trials,
includePruned: false,
logScale: false,
}}
/>
</ThemeProvider>
)
},
],
parameters: {
backgrounds: { default: "dark" },
},
}
export default meta
type Story = StoryObj<typeof PlotIntermediateValues>
export const MockStudyExample1: Story = {
parameters: {
studyId: 1,
},
}
@@ -3,18 +3,19 @@ import { Meta, StoryObj } from "@storybook/react"
import React from "react"
import { useMockStudy } from "../MockStudies"
import { darkTheme } from "../styles/darkTheme"
import { TrialTable } from "./TrialTable"
import { lightTheme } from "../styles/lightTheme"
import { PlotParallelCoordinate } from "./PlotParallelCoordinate"
const meta: Meta<typeof TrialTable> = {
component: TrialTable,
title: "TrialTableDark",
const meta: Meta<typeof PlotParallelCoordinate> = {
component: PlotParallelCoordinate,
title: "Plot/ParallelCoordinate",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const { study } = useMockStudy(storyContext.parameters?.studyId)
if (!study) return <p>loading...</p>
return (
<ThemeProvider theme={darkTheme}>
<ThemeProvider theme={storyContext.parameters?.theme}>
<CssBaseline />
<Story
args={{
@@ -25,16 +26,28 @@ const meta: Meta<typeof TrialTable> = {
)
},
],
parameters: {
backgrounds: { default: "dark" },
},
}
export default meta
type Story = StoryObj<typeof TrialTable>
type Story = StoryObj<typeof PlotParallelCoordinate>
export const MockStudyExample1: Story = {
export const LightTheme: Story = {
parameters: {
studyId: 1,
theme: lightTheme,
},
}
export const DarkTheme: Story = {
parameters: {
studyId: 1,
theme: darkTheme,
},
}
// TODO(c-bata): Add a story for multi objective study.
// export const MultiObjective: Story = {
// parameters: {
// ...
// },
// }
@@ -0,0 +1,302 @@
import {
Checkbox,
FormControlLabel,
FormGroup,
Grid,
Typography,
useTheme,
} from "@mui/material"
import * as Optuna from "@optuna/types"
import * as plotly from "plotly.js-dist-min"
import React, { FC, ReactNode, useEffect, useState } from "react"
import {
GraphContainer,
Target,
useFilteredTrials,
useGraphComponentState,
useMergedUnionSearchSpace,
useObjectiveAndUserAttrTargets,
useParamTargets,
} from ".."
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
const plotDomId = "plot-parallel-coordinate"
export const PlotParallelCoordinate: FC<{
study: Optuna.Study | null
colorTheme?: Partial<Plotly.Template>
}> = ({ study = null, colorTheme }) => {
const { graphComponentState, notifyGraphDidRender } = useGraphComponentState()
const theme = useTheme()
const colorThemeUsed =
colorTheme ?? (theme.palette.mode === "dark" ? plotlyDarkTemplate : {})
const [targets, searchSpace, renderCheckBoxes] = useTargets(study)
const trials = useFilteredTrials(study, targets, false)
useEffect(() => {
if (study !== null && graphComponentState !== "componentWillMount") {
// TODO(c-bata): Fix the broken E2E tests.
// https://github.com/optuna/optuna-dashboard/pull/929#issuecomment-2296632106
// https://github.com/optuna/optuna-dashboard/actions/runs/10451985071/job/28939493755?pr=937
try {
plotCoordinate(
study,
trials,
targets,
searchSpace,
colorThemeUsed
)?.then(notifyGraphDidRender)
} catch (e) {
console.error(e)
}
}
}, [
study,
trials,
targets,
searchSpace,
colorThemeUsed,
graphComponentState,
notifyGraphDidRender,
])
return (
<Grid container direction="row">
<Grid
item
xs={3}
container
direction="column"
sx={{
paddingRight: theme.spacing(2),
display: "flex",
flexDirection: "column",
}}
>
<Typography
variant="h6"
sx={{ margin: "1em 0", fontWeight: theme.typography.fontWeightBold }}
>
Parallel Coordinate
</Typography>
{renderCheckBoxes()}
</Grid>
<Grid item xs={9}>
<GraphContainer
plotDomId={plotDomId}
graphComponentState={graphComponentState}
/>
</Grid>
</Grid>
)
}
const plotCoordinate = (
study: Optuna.Study,
trials: Optuna.Trial[],
targets: Target[],
searchSpace: Optuna.SearchSpaceItem[],
colorTheme: Partial<Plotly.Template>
) => {
if (document.getElementById(plotDomId) === null) {
return
}
const layout: Partial<plotly.Layout> = {
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}<br>` : 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) => `${(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.metric_names),
values: values,
range: [Math.min(...values), Math.max(...values)],
}
}
const s = searchSpace.find(
(s) => s.name === target.key
) as Optuna.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((_, i) => i)
return {
label: breakLabelIfTooLong(s.name),
values: values,
range: [0, s.distribution.choices.length - 1],
// @ts-ignore
tickvals: tickvals,
ticktext: vocabArr,
}
}
if (s.distribution.log) {
// numerical and log
const { logValues, range, tickvals, ticktext } = calculateLogScale(values)
return {
label: breakLabelIfTooLong(s.name),
values: logValues,
range,
tickvals,
ticktext,
}
}
// 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<plotly.PlotData>[] = [
{
type: "parcoords",
dimensions: dimensions,
labelangle: 30,
labelside: "bottom",
line: {
color: dimensions[0].values,
// @ts-ignore
colorscale: "Blues",
colorbar: {
title: targets[0].toLabel(study.metric_names),
},
showscale: true,
reversescale: reversescale,
},
},
]
return plotly.react(plotDomId, plotData, layout)
}
const useTargets = (
study: Optuna.Study | null
): [Target[], Optuna.SearchSpaceItem[], () => ReactNode] => {
const [targets1] = useObjectiveAndUserAttrTargets(study)
const searchSpace = useMergedUnionSearchSpace(study?.union_search_space)
const [targets2] = useParamTargets(searchSpace)
const [checked, setChecked] = useState<boolean[]>([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, study, checked.length])
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setChecked(
checked.map((c, i) =>
i.toString() === event.target.name ? event.target.checked : c
)
)
}
const renderCheckBoxes = (): ReactNode => (
<FormGroup>
{allTargets.map((t, i) => {
const key = t.toLabel(study?.metric_names)
return (
<FormControlLabel
key={key}
control={
<Checkbox
checked={checked.length > i ? checked[i] : true}
onChange={handleOnChange}
name={i.toString()}
/>
}
label={t.toLabel(study?.metric_names)}
/>
)
})}
</FormGroup>
)
const targets = allTargets.filter((_, i) =>
checked.length > i ? checked[i] : true
)
return [targets, searchSpace, renderCheckBoxes]
}
@@ -2,19 +2,20 @@ 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 { lightTheme } from "../styles/lightTheme"
import { PlotSlice } from "./PlotSlice"
const meta: Meta<typeof PlotSlice> = {
component: PlotSlice,
title: "PlotSlice",
title: "Plot/Slice",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const { study } = useMockStudy(storyContext.parameters?.studyId)
if (!study) return <p>loading...</p>
return (
<ThemeProvider theme={lightTheme}>
<ThemeProvider theme={storyContext.parameters?.theme}>
<CssBaseline />
<Story
args={{
@@ -30,8 +31,23 @@ const meta: Meta<typeof PlotSlice> = {
export default meta
type Story = StoryObj<typeof PlotSlice>
export const MockStudyExample1: Story = {
export const LightTheme: Story = {
parameters: {
studyId: 1,
theme: lightTheme,
},
}
export const DarkTheme: Story = {
parameters: {
studyId: 1,
theme: darkTheme,
},
}
// TODO(c-bata): Add a story for multi objective study.
// export const MultiObjective: Story = {
// parameters: {
// ...
// },
// }
+8 -5
View File
@@ -34,10 +34,13 @@ const domId = "plot-slice"
export const PlotSlice: FC<{
study: Optuna.Study | null
}> = ({ study = null }) => {
colorTheme?: Partial<Plotly.Template>
}> = ({ study = null, colorTheme }) => {
const { graphComponentState, notifyGraphDidRender } = useGraphComponentState()
const theme = useTheme()
const colorThemeUsed =
colorTheme ?? (theme.palette.mode === "dark" ? plotlyDarkTemplate : {})
const [objectiveTargets, selectedObjective, setObjectiveTarget] =
useObjectiveAndUserAttrTargets(study)
@@ -63,7 +66,7 @@ export const PlotSlice: FC<{
selectedParamTarget,
searchSpace.find((s) => s.name === selectedParamTarget?.key) || null,
logYScale,
theme.palette.mode
colorThemeUsed
)?.then(notifyGraphDidRender)
}
}, [
@@ -72,7 +75,7 @@ export const PlotSlice: FC<{
searchSpace,
selectedParamTarget,
logYScale,
theme.palette.mode,
colorThemeUsed,
graphComponentState,
])
@@ -160,7 +163,7 @@ const plotSlice = (
selectedParamTarget: Target | null,
selectedParamSpace: Optuna.SearchSpaceItem | null,
logYScale: boolean,
mode: string
colorTheme: Partial<Plotly.Template>
) => {
if (document.getElementById(domId) === null) {
return
@@ -190,7 +193,7 @@ const plotSlice = (
},
showlegend: false,
uirevision: "true",
template: mode === "dark" ? plotlyDarkTemplate : {},
template: colorTheme,
}
if (
selectedParamSpace === null ||
@@ -1,40 +0,0 @@
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<typeof PlotSlice> = {
component: PlotSlice,
title: "PlotSliceDark",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const { study } = useMockStudy(storyContext.parameters?.studyId)
if (!study) return <p>loading...</p>
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<Story
args={{
study,
}}
/>
</ThemeProvider>
)
},
],
parameters: {
backgrounds: { default: "dark" },
},
}
export default meta
type Story = StoryObj<typeof PlotSlice>
export const MockStudyExample1: Story = {
parameters: {
studyId: 1,
},
}
@@ -2,19 +2,20 @@ 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 { lightTheme } from "../styles/lightTheme"
import { TrialTable } from "./TrialTable"
const meta: Meta<typeof TrialTable> = {
component: TrialTable,
title: "TrialTable",
title: "Table/TrialTable",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const { study } = useMockStudy(storyContext.parameters?.studyId)
if (!study) return <p>loading...</p>
return (
<ThemeProvider theme={lightTheme}>
<ThemeProvider theme={storyContext.parameters?.theme}>
<CssBaseline />
<Story
args={{
@@ -30,8 +31,16 @@ const meta: Meta<typeof TrialTable> = {
export default meta
type Story = StoryObj<typeof TrialTable>
export const MockStudyExample1: Story = {
export const LightTheme: Story = {
parameters: {
studyId: 1,
theme: lightTheme,
},
}
export const DarkTheme: Story = {
parameters: {
studyId: 1,
theme: darkTheme,
},
}
+1
View File
@@ -6,6 +6,7 @@ export { PlotHistory } from "./components/PlotHistory"
export { PlotImportance } from "./components/PlotImportance"
export { PlotIntermediateValues } from "./components/PlotIntermediateValues"
export { PlotSlice } from "./components/PlotSlice"
export { PlotParallelCoordinate } from "./components/PlotParallelCoordinate"
export { TrialTable } from "./components/TrialTable"
export { GraphContainer } from "./components/GraphContainer"
export { useGraphComponentState } from "./hooks/useGraphComponentState"
@@ -0,0 +1,34 @@
import * as Optuna from "@optuna/types"
import { render, screen } from "@testing-library/react"
import React from "react"
import { describe, expect, test } from "vitest"
import { PlotParallelCoordinate } from "../src/components/PlotParallelCoordinate"
describe("PlotParallelCoordinate Tests", async () => {
const setup = ({
study,
dataTestId,
}: { study: Optuna.Study; dataTestId: string }) => {
const Wrapper = ({
dataTestId,
children,
}: {
dataTestId: string
children: React.ReactNode
}) => <div data-testid={dataTestId}>{children}</div>
return render(
<Wrapper dataTestId={dataTestId}>
<PlotParallelCoordinate study={study} />
</Wrapper>
)
}
for (const study of window.mockStudies) {
test(`PlotParallelCoordinate (study name: ${study.name})`, () => {
setup({ study, dataTestId: `plot-parallel-coordinate-${study.id}` })
expect(
screen.getByTestId(`plot-parallel-coordinate-${study.id}`)
).toBeInTheDocument()
})
}
})
+28
View File
@@ -447,6 +447,34 @@ def create_optuna_storage(
trial.report(trial.number, step=0)
trial.report(trial.number + 1, step=1)
# optuna-dashboard issue 410
# https://github.com/optuna/optuna-dashboard/issues/410
study = optuna.create_study(
study_name="optuna-dashboard-issue-410",
storage=storage,
sampler=optuna.samplers.RandomSampler(),
)
def objective_issue_410(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_issue_410, n_trials=20, catch=(Exception,))
def main() -> None:
remove_assets()
-1
View File
@@ -28,7 +28,6 @@
"src/**/*.stories.tsx",
"src/**/*.stories.ts",
"src/MockStudies.ts",
"src/utils/loadStorageFromFile.ts"
],
"references": [{ "path": "./tsconfig.node.json" }]
}