mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-09 11:28:14 +08:00
wip
This commit is contained in:
@@ -18,6 +18,7 @@ from ._named_objectives import get_objective_names
|
||||
from ._preferential_history import _SYSTEM_ATTR_PREFIX_HISTORY
|
||||
from .artifact._backend import list_trial_artifacts
|
||||
from .preferential._study import _SYSTEM_ATTR_PREFERENTIAL_STUDY
|
||||
from .preferential._system_attrs import _get_preferences
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -162,6 +163,7 @@ def serialize_study_detail(
|
||||
serialized["form_widgets"] = form_widgets
|
||||
if serialized["is_preferential"]:
|
||||
serialized["preference_history"] = serialize_preference_history(system_attrs)
|
||||
serialized["preferences"] = _get_preferences(system_attrs)
|
||||
return serialized
|
||||
|
||||
|
||||
|
||||
@@ -35,12 +35,8 @@ def report_preferences(
|
||||
return preference_id
|
||||
|
||||
|
||||
def get_preferences(
|
||||
study_id: int,
|
||||
storage: BaseStorage,
|
||||
) -> list[tuple[int, int]]:
|
||||
def _get_preferences(system_attrs: dict[str, Any]) -> list[tuple[int, int]]:
|
||||
preferences: list[tuple[int, int]] = []
|
||||
system_attrs = storage.get_study_system_attrs(study_id)
|
||||
for k, v in system_attrs.items():
|
||||
if not k.startswith(_SYSTEM_ATTR_PREFIX_PREFERENCE):
|
||||
continue
|
||||
@@ -48,6 +44,13 @@ def get_preferences(
|
||||
return preferences
|
||||
|
||||
|
||||
def get_preferences(
|
||||
study_id: int,
|
||||
storage: BaseStorage,
|
||||
) -> list[tuple[int, int]]:
|
||||
return _get_preferences(storage.get_study_system_attrs(study_id))
|
||||
|
||||
|
||||
def report_skip(
|
||||
study_id: int,
|
||||
trial_id: int,
|
||||
|
||||
@@ -92,6 +92,7 @@ interface StudyDetailResponse {
|
||||
is_preferential: boolean
|
||||
objective_names?: string[]
|
||||
form_widgets?: FormWidgets
|
||||
preferences?: [number, number][]
|
||||
preference_history?: PreferenceHistoryResponce[]
|
||||
}
|
||||
|
||||
@@ -128,6 +129,7 @@ export const getStudyDetailAPI = (
|
||||
objective_names: res.data.objective_names,
|
||||
form_widgets: res.data.form_widgets,
|
||||
is_preferential: res.data.is_preferential,
|
||||
preferences: res.data.preferences,
|
||||
preference_history: res.data.preference_history?.map(
|
||||
convertPreferenceHistory
|
||||
),
|
||||
|
||||
@@ -87,6 +87,15 @@ export const App: FC = () => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={URL_PREFIX + "/studies/:studyId/graph"}
|
||||
element={
|
||||
<StudyDetail
|
||||
toggleColorMode={toggleColorMode}
|
||||
page={"graph"}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={URL_PREFIX + "/studies/:studyId"}
|
||||
element={
|
||||
|
||||
@@ -35,6 +35,7 @@ import OpenInNewIcon from "@mui/icons-material/OpenInNew"
|
||||
import QueryStatsIcon from "@mui/icons-material/QueryStats"
|
||||
import ThumbUpAltIcon from "@mui/icons-material/ThumbUpAlt"
|
||||
import HistoryIcon from "@mui/icons-material/History"
|
||||
import LanIcon from "@mui/icons-material/Lan"
|
||||
import { Switch } from "@mui/material"
|
||||
import { actionCreator } from "../action"
|
||||
|
||||
@@ -47,6 +48,7 @@ export type PageId =
|
||||
| "trialList"
|
||||
| "note"
|
||||
| "preferenceHistory"
|
||||
| "graph"
|
||||
|
||||
const openedMixin = (theme: Theme): CSSObject => ({
|
||||
width: drawerWidth,
|
||||
@@ -246,6 +248,21 @@ export const AppDrawer: FC<{
|
||||
<ListItemText primary="Analytics" sx={styleListItemText} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
{studyDetail?.is_preferential && (
|
||||
<ListItem key="Graph" disablePadding sx={styleListItem}>
|
||||
<ListItemButton
|
||||
component={Link}
|
||||
to={`${URL_PREFIX}/studies/${studyId}/graph`}
|
||||
sx={styleListItemButton}
|
||||
selected={page === "graph"}
|
||||
>
|
||||
<ListItemIcon sx={styleListItemIcon}>
|
||||
<LanIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Graph" sx={styleListItemText} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem key="TableList" disablePadding sx={styleListItem}>
|
||||
<ListItemButton
|
||||
component={Link}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import React, { FC, useState, useCallback, useMemo, useEffect } from "react"
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Paper,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import Grid2 from "@mui/material/Unstable_Grid2"
|
||||
import { DataGrid, DataGridColumn } from "./DataGrid"
|
||||
import { BestTrialsCard } from "./BestTrialsCard"
|
||||
import { useStudyDetailValue, useStudySummaryValue } from "../state"
|
||||
import { Contour } from "./GraphContour"
|
||||
import { MarkdownRenderer } from "./Note"
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
Node,
|
||||
NodeProps,
|
||||
NodeTypes,
|
||||
Edge,
|
||||
FitViewOptions,
|
||||
DefaultEdgeOptions,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
OnNodesChange,
|
||||
OnEdgesChange,
|
||||
OnConnect,
|
||||
Position,
|
||||
Handle,
|
||||
} from "reactflow"
|
||||
import "reactflow/dist/style.css"
|
||||
|
||||
const nodeWidth = 400
|
||||
const nodeHeight = 300
|
||||
type NodeData = {
|
||||
trial?: Trial
|
||||
}
|
||||
const GraphNode: FC<NodeProps<NodeData>> = ({ data, isConnectable }) => {
|
||||
const theme = useTheme()
|
||||
const trial = data.trial
|
||||
if (trial === undefined) {
|
||||
return null
|
||||
}
|
||||
const noteBody = trial.note.body
|
||||
const noteFC = useMemo(() => {
|
||||
return <MarkdownRenderer body={noteBody} />
|
||||
}, [noteBody])
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
width: nodeWidth,
|
||||
height: nodeHeight,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<CardHeader title={`Trial ${trial.number}`} />
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
style={{ background: "#555" }}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
<CardContent>{noteFC}</CardContent>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
style={{ background: "#555" }}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
note: GraphNode,
|
||||
}
|
||||
|
||||
const createNode = (x: number, y: number, trial: Trial): Node => {
|
||||
return {
|
||||
id: `${trial.number}`,
|
||||
type: "note",
|
||||
data: {
|
||||
label: `Trial ${trial.number}`,
|
||||
trial: trial,
|
||||
},
|
||||
position: {
|
||||
x: x * 500,
|
||||
y: y * 400,
|
||||
},
|
||||
style: {
|
||||
width: nodeWidth,
|
||||
height: nodeHeight,
|
||||
padding: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const defaultEdgeOptions: DefaultEdgeOptions = {
|
||||
animated: true,
|
||||
}
|
||||
|
||||
export const PreferentialGraph: FC<{ studyDetail: StudyDetail | null }> = ({
|
||||
studyDetail,
|
||||
}) => {
|
||||
if (studyDetail === null || !studyDetail.is_preferential) {
|
||||
return null
|
||||
}
|
||||
const [nodes, setNodes] = useState<Node[]>([])
|
||||
|
||||
const onNodesChange: OnNodesChange = useCallback(
|
||||
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
[setNodes]
|
||||
)
|
||||
useEffect(() => {
|
||||
setNodes((prev) => {
|
||||
const newNodes: Node[] = []
|
||||
studyDetail.best_trials.forEach((trial, i) => {
|
||||
newNodes.push(createNode(i, 0, trial))
|
||||
})
|
||||
if (studyDetail.preference_history !== undefined) {
|
||||
const histories = [...studyDetail.preference_history]
|
||||
histories?.reverse().forEach((history, i) => {
|
||||
const y = history.candidates.findIndex((c) => c === history.clicked)
|
||||
newNodes.push(
|
||||
createNode(y, i + 1, studyDetail.trials[history.clicked])
|
||||
)
|
||||
})
|
||||
}
|
||||
return newNodes
|
||||
})
|
||||
}, [studyDetail])
|
||||
|
||||
const edges: Edge[] =
|
||||
studyDetail.preferences?.map((p) => {
|
||||
return {
|
||||
id: `e${p[0]}-${p[1]}`,
|
||||
source: `${p[0]}`,
|
||||
target: `${p[1]}`,
|
||||
style: { stroke: "#fff" },
|
||||
} as Edge
|
||||
}) ?? []
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
nodeTypes={nodeTypes}
|
||||
zoomOnScroll={false}
|
||||
panOnScroll={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import { StudyHistory } from "./StudyHistory"
|
||||
import { PreferentialTrials } from "./PreferentialTrials"
|
||||
import { PreferenceHistory } from "./PreferenceHistory"
|
||||
import { PreferentialAnalytics } from "./PreferentialAnalytics"
|
||||
import { PreferentialGraph } from "./PreferentialGraph"
|
||||
|
||||
interface ParamTypes {
|
||||
studyId: string
|
||||
@@ -176,6 +177,17 @@ export const StudyDetail: FC<{
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
} else if (page === "graph") {
|
||||
content = (
|
||||
<Box
|
||||
sx={{
|
||||
height: `calc(100vh - ${theme.spacing(8)})`,
|
||||
padding: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<PreferentialGraph studyDetail={studyDetail} />
|
||||
</Box>
|
||||
)
|
||||
} else if (page == "preferenceHistory") {
|
||||
content = <PreferenceHistory studyDetail={studyDetail} />
|
||||
}
|
||||
|
||||
Vendored
+3
-1
@@ -198,6 +198,7 @@ type StudyDetail = {
|
||||
is_preferential: boolean
|
||||
objective_names?: string[]
|
||||
form_widgets?: FormWidgets
|
||||
preferences?: [number, number][]
|
||||
preference_history?: PreferenceHistory[]
|
||||
}
|
||||
|
||||
@@ -208,7 +209,6 @@ type StudyDetails = {
|
||||
type StudyParamImportance = {
|
||||
[study_id: string]: ParamImportance[][]
|
||||
}
|
||||
|
||||
type PreferenceHistory = {
|
||||
id: string
|
||||
preference_id: string
|
||||
@@ -217,3 +217,5 @@ type PreferenceHistory = {
|
||||
feedback_mode: PreferenceFeedbackMode
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
declare module "*.css"
|
||||
|
||||
Generated
+1411
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,7 @@
|
||||
"react-markdown": "^8.0.4",
|
||||
"react-router-dom": "^6.11.0",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"reactflow": "^11.8.3",
|
||||
"recoil": "^0.7.7",
|
||||
"rehype-mathjax": "^4.0.2",
|
||||
"rehype-raw": "^6.1.1",
|
||||
@@ -53,12 +54,14 @@
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.1",
|
||||
"@typescript-eslint/parser": "^4.26.1",
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"esbuild-loader": "^2.18.0",
|
||||
"eslint": "^7.28.0",
|
||||
"jest": "^29.2.1",
|
||||
"jest-canvas-mock": "^2.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"prettier": "^2.5.1",
|
||||
"style-loader": "^3.3.3",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-loader": "^9.2.7",
|
||||
"typescript": "^4.6.2",
|
||||
|
||||
+75
-59
@@ -1,65 +1,81 @@
|
||||
const webpack = require('webpack');
|
||||
const webpack = require("webpack")
|
||||
|
||||
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
||||
const isDev = mode === 'development';
|
||||
const mode =
|
||||
process.env.NODE_ENV === "production" ? "production" : "development"
|
||||
const isDev = mode === "development"
|
||||
|
||||
const typeScriptLoader = process.env.TYPESCRIPT_LOADER === "esbuild-loader" ? {
|
||||
test: /\.tsx?$/,
|
||||
exclude: [/node_modules/],
|
||||
loader: 'esbuild-loader',
|
||||
options: {
|
||||
loader: 'tsx',
|
||||
tsconfigRaw: require('./tsconfig.json')
|
||||
}
|
||||
} : {
|
||||
test: /\.tsx?$/,
|
||||
exclude: [/node_modules/],
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
configFile: __dirname + '/tsconfig.json',
|
||||
transpileOnly: isDev,
|
||||
happyPackMode: true
|
||||
}
|
||||
}
|
||||
const typeScriptLoader =
|
||||
process.env.TYPESCRIPT_LOADER === "esbuild-loader"
|
||||
? {
|
||||
test: /\.tsx?$/,
|
||||
exclude: [/node_modules/],
|
||||
loader: "esbuild-loader",
|
||||
options: {
|
||||
loader: "tsx",
|
||||
tsconfigRaw: require("./tsconfig.json"),
|
||||
},
|
||||
}
|
||||
: {
|
||||
test: /\.tsx?$/,
|
||||
exclude: [/node_modules/],
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
configFile: __dirname + "/tsconfig.json",
|
||||
transpileOnly: isDev,
|
||||
happyPackMode: true,
|
||||
},
|
||||
}
|
||||
|
||||
var config = {
|
||||
mode,
|
||||
entry: [__dirname + '/optuna_dashboard/ts/index.tsx'],
|
||||
output: {
|
||||
path: __dirname + '/optuna_dashboard/public/',
|
||||
filename: 'bundle.js',
|
||||
publicPath: '/public/'
|
||||
},
|
||||
module: {
|
||||
rules: [{oneOf: [typeScriptLoader]}]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js']
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'APP_BAR_TITLE': JSON.stringify(process.env.APP_BAR_TITLE || "Optuna Dashboard"),
|
||||
'API_ENDPOINT': JSON.stringify(process.env.API_ENDPOINT),
|
||||
'URL_PREFIX': JSON.stringify(process.env.URL_PREFIX || "/dashboard")
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
if (isDev) {
|
||||
config.devtool = 'source-map';
|
||||
config.cache = {
|
||||
type: 'filesystem',
|
||||
buildDependencies: {
|
||||
config: [__filename],
|
||||
}
|
||||
}
|
||||
console.log('= = = = = = = = = = = = = = = = = = =');
|
||||
console.log('DEVELOPMENT BUILD');
|
||||
console.log(process.env.TYPESCRIPT_LOADER === 'esbuild-loader' ? 'esbuild-loader' : 'ts-loader');
|
||||
console.log('= = = = = = = = = = = = = = = = = = =');
|
||||
} else {
|
||||
const CompressionPlugin = require("compression-webpack-plugin");
|
||||
config.plugins.push(new CompressionPlugin())
|
||||
mode,
|
||||
entry: [__dirname + "/optuna_dashboard/ts/index.tsx"],
|
||||
output: {
|
||||
path: __dirname + "/optuna_dashboard/public/",
|
||||
filename: "bundle.js",
|
||||
publicPath: "/public/",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ oneOf: [typeScriptLoader] },
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ["style-loader", "css-loader"],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js"],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
APP_BAR_TITLE: JSON.stringify(
|
||||
process.env.APP_BAR_TITLE || "Optuna Dashboard"
|
||||
),
|
||||
API_ENDPOINT: JSON.stringify(process.env.API_ENDPOINT),
|
||||
URL_PREFIX: JSON.stringify(process.env.URL_PREFIX || "/dashboard"),
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
if (isDev) {
|
||||
config.devtool = "source-map"
|
||||
config.cache = {
|
||||
type: "filesystem",
|
||||
buildDependencies: {
|
||||
config: [__filename],
|
||||
},
|
||||
}
|
||||
console.log("= = = = = = = = = = = = = = = = = = =")
|
||||
console.log("DEVELOPMENT BUILD")
|
||||
console.log(
|
||||
process.env.TYPESCRIPT_LOADER === "esbuild-loader"
|
||||
? "esbuild-loader"
|
||||
: "ts-loader"
|
||||
)
|
||||
console.log("= = = = = = = = = = = = = = = = = = =")
|
||||
} else {
|
||||
const CompressionPlugin = require("compression-webpack-plugin")
|
||||
config.plugins.push(new CompressionPlugin())
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
|
||||
Reference in New Issue
Block a user