Merge pull request #851 from porink0424/feat/tslib

Separated some UI components into `@optuna/react`
This commit is contained in:
c-bata
2024-04-03 13:09:36 +09:00
committed by GitHub
30 changed files with 11487 additions and 3176 deletions
+2 -1
View File
@@ -22,6 +22,7 @@ $(DASHBOARD_TS_OUT): $(DASHBOARD_TS_SRC)
tslib:
cd tslib/types && npm i && npm run build
cd tslib/storage && npm i && npm run build
cd tslib/react && npm i && npm run build
.PHONY: serve-browser-app
serve-browser-app: tslib $(RUSTLIB_OUT)
@@ -51,6 +52,6 @@ fmt:
.PHONY: clean
clean:
rm -rf tslib/types/pkg tslib/storage/pkg
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
+1 -1
View File
@@ -14,7 +14,7 @@
],
"ignore": [
"optuna_dashboard/ts/components/PlotlyColorTemplates.ts",
"standalone_app/src/PlotlyDarkMode.ts",
"tslib/react/src/components/PlotlyDarkMode.ts",
"tslib/**/pkg/*"
]
},
+629 -3143
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -25,6 +25,7 @@
"@mui/icons-material": "^5.15.12",
"@mui/lab": "^5.0.0-alpha.167",
"@mui/material": "^5.15.12",
"@optuna/react": "../tslib/react",
"@optuna/storage": "../tslib/storage",
"module-workers-polyfill": "^0.3.2",
"notistack": "^3.0.1",
@@ -33,5 +34,8 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "^4.13.2"
}
}
@@ -1,9 +1,9 @@
import { Box, Typography, useTheme } from "@mui/material"
import { plotlyDarkTemplate } from "@optuna/react"
import init, { wasm_fanova_calculate } from "optuna"
import * as plotly from "plotly.js-dist-min"
import React, { FC, useEffect, useState } from "react"
import { plotlyDarkTemplate } from "../PlotlyDarkMode"
const plotDomId = "graph-hyperparameter-importances"
export const PlotImportance: FC<{ study: Study }> = ({ study }) => {
@@ -1,7 +1,7 @@
import { Box, Typography, useTheme } from "@mui/material"
import { plotlyDarkTemplate } from "@optuna/react"
import * as plotly from "plotly.js-dist-min"
import React, { FC, useEffect } from "react"
import { plotlyDarkTemplate } from "../PlotlyDarkMode"
const plotDomId = "graph-intermediate-values"
@@ -13,13 +13,12 @@ import {
useTheme,
} from "@mui/material"
import Grid2 from "@mui/material/Unstable_Grid2"
import { PlotHistory, TrialTable } from "@optuna/react"
import React, { FC, useContext, useState, useEffect } from "react"
import { Link, useParams } from "react-router-dom"
import { PlotHistory } from "./PlotHistory"
import { PlotImportance } from "./PlotImportance"
import { PlotIntermediateValues } from "./PlotIntermediateValues"
import { StorageContext } from "./StorageProvider"
import { TrialTable } from "./TrialTable"
export const StudyDetail: FC<{
toggleColorMode: () => void
+1 -1
View File
@@ -2,7 +2,7 @@ declare const IS_VSCODE: boolean
type TrialState = "Running" | "Complete" | "Pruned" | "Fail" | "Waiting"
type TrialStateFinished = "Complete" | "Fail" | "Pruned"
type StudyDirection = "maximize" | "minimize" | "not_set"
type StudyDirection = "maximize" | "minimize"
type OptunaStorage = {
getStudies: () => Promise<StudySummary[]>
+26
View File
@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
pkg
types
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# sample db
!public/sample_db.sqlite3
+29
View File
@@ -0,0 +1,29 @@
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",
],
framework: {
name: "@storybook/react-vite",
options: {},
},
docs: {
autodocs: "tag",
},
async viteFinal(config) {
const { mergeConfig } = await import("vite")
return mergeConfig(config, {
server: {
fs: {
// Since storybook overwrites the `allow` option, here we set it for sqlite-wasm
allow: [...(config.server?.fs?.allow ?? []), ".."],
},
},
})
},
}
export default config
+14
View File
@@ -0,0 +1,14 @@
import type { Preview } from "@storybook/react"
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
}
export default preview
+10405
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
{
"name": "@optuna/react",
"private": true,
"version": "0.0.1",
"type": "module",
"description": "UI packages for Optuna-Dashboard",
"main": "pkg/index.js",
"types": "types/index.d.ts",
"scripts": {
"build": "tsc -d",
"storybook": "storybook dev -p 6006"
},
"files": [
"pkg",
"types"
],
"repository": {
"type": "git",
"url": "git+https://github.com/optuna/optuna-dashboard.git"
},
"author": "Optuna Development Team",
"license": "MIT",
"bugs": {
"url": "https://github.com/optuna/optuna-dashboard/issues"
},
"homepage": "https://github.com/optuna/optuna-dashboard#readme",
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.10",
"@mui/material": "^5.15.10",
"@mui/system": "^5.15.9",
"@optuna/storage": "../storage/",
"plotly.js-dist-min": "^2.30.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"recoil": "^0.7.7"
},
"devDependencies": {
"@optuna/types": "../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",
"@types/plotly.js-dist-min": "^2.3.4",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react-swc": "^3.5.0",
"storybook": "^8.0.4",
"typescript": "^5.2.2",
"vite": "^5.1.0"
}
}
Binary file not shown.
+32
View File
@@ -0,0 +1,32 @@
import * as Optuna from "@optuna/types"
import { useEffect, useState } from "react"
import { loadStorageFromFile } from "./utils/loadStorageFromFile"
const fetchMockStudies = async () => {
const filePath = "sample_db.sqlite3"
const res = await fetch(filePath)
const blob = await res.blob()
const file = new File([blob], filePath)
const mockStudies: Optuna.Study[] = []
await loadStorageFromFile(file, (value) => {
if (Array.isArray(value)) {
mockStudies.push(...value)
} else {
mockStudies.push(...value([]))
}
})
return mockStudies
}
const useMockStudies = () => {
const [mockStudies, setMockStudies] = useState<Optuna.Study[]>([])
useEffect(() => {
fetchMockStudies().then((studies) => setMockStudies(studies))
}, [])
return mockStudies
}
export const useMockStudy = (studyId: number | undefined) => {
const mockStudies = useMockStudies()
return mockStudies.find((study) => study.study_id === studyId)
}
@@ -69,7 +69,7 @@ function DataGrid<T>(props: {
: rowsPerPageOption[0].value
const [rowsPerPage, setRowsPerPage] = React.useState(initialRowsPerPage)
const handleChangePage = (event: unknown, newPage: number) => {
const handleChangePage = (_event: unknown, newPage: number) => {
setPage(newPage)
}
@@ -0,0 +1,31 @@
import { Meta, StoryObj } from "@storybook/react"
import { useMockStudy } from "../MockStudies"
import { PlotHistory } from "./PlotHistory"
const meta: Meta<typeof PlotHistory> = {
component: PlotHistory,
title: "PlotHistory",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const study = useMockStudy(storyContext.parameters?.studyId)
if (!study) return <p>loading...</p>
return (
<Story
args={{
study,
}}
/>
)
},
],
}
export default meta
type Story = StoryObj<typeof PlotHistory>
export const MockStudy1: Story = {
parameters: {
studyId: 1,
},
}
@@ -13,14 +13,15 @@ import {
Typography,
useTheme,
} from "@mui/material"
import * as Optuna from "@optuna/types"
import * as plotly from "plotly.js-dist-min"
import React, { ChangeEvent, FC, useEffect, useState } from "react"
import { plotlyDarkTemplate } from "../PlotlyDarkMode"
import { ChangeEvent, FC, useEffect, useState } from "react"
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
const plotDomId = "plot-history"
export const PlotHistory: FC<{
study: Study | null
study: Optuna.Study | null
}> = ({ study = null }) => {
const theme = useTheme()
const [xAxis, setXAxis] = useState<string>("number")
@@ -90,7 +91,7 @@ export const PlotHistory: FC<{
>
<FormLabel component="legend">Objective ID:</FormLabel>
<Select value={objectiveId} onChange={handleObjectiveChange}>
{study.directions.map((d, i) => (
{study.directions.map((_d, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<MenuItem value={i} key={i}>
{i}
@@ -170,7 +171,7 @@ export const PlotHistory: FC<{
)
}
const filterFunc = (trial: Trial, objectiveId: number): boolean => {
const filterFunc = (trial: Optuna.Trial, objectiveId: number): boolean => {
if (trial.state !== "Complete" && trial.state !== "Pruned") {
return false
}
@@ -185,7 +186,7 @@ const filterFunc = (trial: Trial, objectiveId: number): boolean => {
}
const plotHistory = (
study: Study,
study: Optuna.Study,
objectiveId: number,
xAxis: string,
logScale: boolean,
@@ -228,7 +229,7 @@ const plotHistory = (
return
}
const getAxisX = (trial: Trial): number | Date => {
const getAxisX = (trial: Optuna.Trial): number | Date => {
return xAxis === "number"
? trial.number
: xAxis === "datetime_start"
@@ -236,7 +237,10 @@ const plotHistory = (
: trial.datetime_complete ?? new Date()
}
const getValue = (trial: Trial, objectiveId: number): number | null => {
const getValue = (
trial: Optuna.Trial,
objectiveId: number
): number | null => {
if (
objectiveId === null ||
trial.values === undefined ||
@@ -294,7 +298,7 @@ const plotHistory = (
{
x: filteredTrials.map(getAxisX),
y: filteredTrials.map(
(t: Trial): number => getValue(t, objectiveId) as number
(t: Optuna.Trial): number => getValue(t, objectiveId) as number
),
name: "Objective Value",
mode: "markers",
@@ -0,0 +1,38 @@
import { ThemeProvider } from "@mui/material"
import { Meta, StoryObj } from "@storybook/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}>
<Story
args={{
study,
}}
/>
</ThemeProvider>
)
},
],
parameters: {
backgrounds: { default: "dark" },
},
}
export default meta
type Story = StoryObj<typeof PlotHistory>
export const MockStudy1: Story = {
parameters: {
studyId: 1,
},
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,31 @@
import { Meta, StoryObj } from "@storybook/react"
import { useMockStudy } from "../MockStudies"
import { TrialTable } from "./TrialTable"
const meta: Meta<typeof TrialTable> = {
component: TrialTable,
title: "TrialTable",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const study = useMockStudy(storyContext.parameters?.studyId)
if (!study) return <p>loading...</p>
return (
<Story
args={{
study,
}}
/>
)
},
],
}
export default meta
type Story = StoryObj<typeof TrialTable>
export const MockStudy1: Story = {
parameters: {
studyId: 1,
},
}
@@ -1,14 +1,15 @@
import React, { FC } from "react"
import { FC } from "react"
import * as Optuna from "@optuna/types"
import { DataGrid, DataGridColumn } from "./DataGrid"
export const TrialTable: FC<{
study: Study
study: Optuna.Study
initialRowsPerPage?: number
}> = ({ study, initialRowsPerPage }) => {
const trials: Trial[] = study.trials
const trials: Optuna.Trial[] = study.trials
const columns: DataGridColumn<Trial>[] = [
const columns: DataGridColumn<Optuna.Trial>[] = [
{ field: "number", label: "Number", sortable: true, padding: "none" },
{
field: "state",
@@ -48,8 +49,8 @@ export const TrialTable: FC<{
},
})
} else {
const objectiveColumns: DataGridColumn<Trial>[] = study.directions.map(
(s, objectiveId) => ({
const objectiveColumns: DataGridColumn<Optuna.Trial>[] =
study.directions.map((_s, objectiveId) => ({
field: "values",
label: `Objective ${objectiveId}`,
sortable: true,
@@ -74,13 +75,11 @@ export const TrialTable: FC<{
}
return trials[i].values?.[objectiveId]
},
})
)
}))
columns.push(...objectiveColumns)
}
// biome-ignore lint/complexity/noForEach: <explanation>
study.union_search_space.forEach((s) => {
for (const s of study.union_search_space) {
columns.push({
field: "params",
label: `Param ${s.name}`,
@@ -109,10 +108,9 @@ export const TrialTable: FC<{
return 1
},
})
})
}
// biome-ignore lint/complexity/noForEach: <explanation>
study.union_user_attrs.forEach((attr_spec) => {
for (const attr_spec of study.union_user_attrs) {
columns.push({
field: "user_attrs",
label: `UserAttribute ${attr_spec.key}`,
@@ -141,10 +139,10 @@ export const TrialTable: FC<{
return 1
},
})
})
}
return (
<DataGrid<Trial>
<DataGrid<Optuna.Trial>
columns={columns}
rows={trials}
keyField={"trial_id"}
@@ -0,0 +1,38 @@
import { ThemeProvider } from "@mui/material"
import { Meta, StoryObj } from "@storybook/react"
import { useMockStudy } from "../MockStudies"
import { darkTheme } from "../styles/darkTheme"
import { TrialTable } from "./TrialTable"
const meta: Meta<typeof TrialTable> = {
component: TrialTable,
title: "TrialTableDark",
tags: ["autodocs"],
decorators: [
(Story, storyContext) => {
const study = useMockStudy(storyContext.parameters?.studyId)
if (!study) return <p>loading...</p>
return (
<ThemeProvider theme={darkTheme}>
<Story
args={{
study,
}}
/>
</ThemeProvider>
)
},
],
parameters: {
backgrounds: { default: "dark" },
},
}
export default meta
type Story = StoryObj<typeof TrialTable>
export const MockStudy1: Story = {
parameters: {
studyId: 1,
},
}
+5
View File
@@ -0,0 +1,5 @@
export { DataGrid } from "./components/DataGrid"
export type { DataGridColumn } from "./components/DataGrid"
export { PlotHistory } from "./components/PlotHistory"
export { TrialTable } from "./components/TrialTable"
export { plotlyDarkTemplate } from "./components/PlotlyDarkMode"
+7
View File
@@ -0,0 +1,7 @@
import { createTheme } from "@mui/material"
export const darkTheme = createTheme({
palette: {
mode: "dark",
},
})
@@ -0,0 +1,45 @@
import { JournalFileStorage, SQLite3Storage } from "@optuna/storage"
import * as Optuna from "@optuna/types"
import { SetterOrUpdater } from "recoil"
const readFile = async (file: File) => {
return new Promise<ArrayBuffer>((resolve, reject) => {
const reader = new FileReader()
reader.addEventListener("load", () => {
const arrayBuffer = reader.result as ArrayBuffer | null
if (arrayBuffer !== null) {
resolve(arrayBuffer)
} else {
reject(new Error("Failed to load file"))
}
})
reader.readAsArrayBuffer(file)
})
}
const loadStudiesFromStorage = async (
storage: SQLite3Storage | JournalFileStorage,
setter: SetterOrUpdater<Optuna.Study[]>
) => {
const studySummaries = await storage.getStudies()
const studies = (
await Promise.all(
studySummaries.map((_summary, index) => storage.getStudy(index))
)
).filter((s) => s !== null) as Optuna.Study[]
setter((prev) => [...prev, ...studies])
}
export const loadStorageFromFile = async (
file: File,
setStudies: SetterOrUpdater<Optuna.Study[]>
) => {
const arrayBuf = await readFile(file)
const header = new Uint8Array(arrayBuf, 0, 16)
const headerString = new TextDecoder().decode(header)
if (headerString === "SQLite format 3\u0000") {
await loadStudiesFromStorage(new SQLite3Storage(arrayBuf), setStudies)
} else {
await loadStudiesFromStorage(new JournalFileStorage(arrayBuf), setStudies)
}
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"isolatedModules": true,
"skipLibCheck": true,
"strictNullChecks": true,
"moduleResolution": "Node",
"noUnusedLocals": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noImplicitAny": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"esModuleInterop": true,
"rootDir": "./src",
"outDir": "./pkg",
"allowSyntheticDefaultImports": true,
"target": "ES2020",
"sourceMap": true,
"declaration": true,
"declarationDir": "./types",
"strict": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"jsx": "react-jsx"
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": [
"src/**/*.stories.tsx",
"src/**/*.stories.ts",
"src/MockStudies.ts",
"src/utils/loadStorageFromFile.ts"
],
"references": [{ "path": "./tsconfig.node.json" }]
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
+16
View File
@@ -0,0 +1,16 @@
import react from "@vitejs/plugin-react-swc"
import { defineConfig } from "vite"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
optimizeDeps: {
exclude: ["@sqlite.org/sqlite-wasm"],
},
server: {
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
},
})