Merge pull request #329 from c-bata/beta-ui

Make a lot of improvements in the new Dashboard UI
This commit is contained in:
Masashi Shibata
2023-01-02 01:13:53 +09:00
committed by GitHub
24 changed files with 3276 additions and 243 deletions
+32 -10
View File
@@ -336,27 +336,26 @@ def create_app(storage: BaseStorage, debug: bool = False) -> Bottle:
@app.get("/api/studies/<study_id:int>/param_importances")
@json_api_view
def get_param_importances(study_id: int) -> BottleViewReturn:
# TODO(chenghuzi): add support for selecting params via query parameters.
objective_id = int(request.params.get("objective_id", 0))
try:
n_directions = len(storage.get_study_directions(study_id))
except KeyError:
response.status = 404 # Study is not found
return {"reason": f"study_id={study_id} is not found"}
if objective_id >= n_directions:
response.status = 400 # Bad request
return {"reason": f"study_id={study_id} has only {n_directions} direction(s)."}
trials = get_trials(storage, study_id)
try:
return get_param_importance_from_trials_cache(storage, study_id, objective_id, trials)
importances = [
get_param_importance_from_trials_cache(storage, study_id, objective_id, trials)
for objective_id in range(n_directions)
]
return {"param_importances": importances}
except ValueError as e:
response.status = 400 # Bad request
return {"reason": str(e)}
@app.put("/api/studies/<study_id:int>/note")
@json_api_view
def save_note(study_id: int) -> BottleViewReturn:
def save_study_note(study_id: int) -> BottleViewReturn:
req_note_ver = request.json.get("version", None)
req_note_body = request.json.get("body", None)
if req_note_ver is None or req_note_body is None:
@@ -364,15 +363,38 @@ def create_app(storage: BaseStorage, debug: bool = False) -> Bottle:
return {"reason": "Invalid request."}
system_attrs = storage.get_study_system_attrs(study_id)
if not note.version_is_incremented(system_attrs, req_note_ver):
if not note.version_is_incremented(system_attrs, None, req_note_ver):
response.status = 409 # Conflict
return {
"reason": "The text you are editing has changed. "
"Please copy your edits and refresh the page.",
"note": note.get_note_from_system_attrs(system_attrs),
"note": note.get_note_from_system_attrs(system_attrs, None),
}
note.save_note(storage, study_id, req_note_ver, req_note_body)
note.save_note(storage, study_id, None, req_note_ver, req_note_body)
response.status = 204 # No content
return {}
@app.put("/api/studies/<study_id:int>/<trial_id:int>/note")
@json_api_view
def save_trial_note(study_id: int, trial_id: int) -> BottleViewReturn:
req_note_ver = request.json.get("version", None)
req_note_body = request.json.get("body", None)
if req_note_ver is None or req_note_body is None:
response.status = 400 # Bad request
return {"reason": "Invalid request."}
# Store note content in study system attrs since it's always updatable.
system_attrs = storage.get_study_system_attrs(study_id=study_id)
if not note.version_is_incremented(system_attrs, trial_id, req_note_ver):
response.status = 409 # Conflict
return {
"reason": "The text you are editing has changed. "
"Please copy your edits and refresh the page.",
"note": note.get_note_from_system_attrs(system_attrs, trial_id),
}
note.save_note(storage, study_id, trial_id, req_note_ver, req_note_body)
response.status = 204 # No content
return {}
+15 -28
View File
@@ -25,26 +25,18 @@ except Exception as e:
if TYPE_CHECKING:
from typing import TypedDict
ImportanceItemType = TypedDict(
"ImportanceItemType",
ImportanceType = TypedDict(
"ImportanceType",
{
"name": str,
"importance": float,
"distribution": str,
},
)
ImportanceType = TypedDict(
"ImportanceType",
{
"target_name": str,
"param_importances": list[ImportanceItemType],
},
)
target_name = "Objective Value"
param_importance_cache_lock = threading.Lock()
# { "{study_id}:{objective_id}" : (n_completed_trials, importance) }
param_importance_cache: dict[str, tuple[int, ImportanceType]] = {}
param_importance_cache: dict[str, tuple[int, list[ImportanceType]]] = {}
class StudyWrapper(Study):
@@ -62,17 +54,15 @@ class StudyWrapper(Study):
def get_param_importance_from_trials_cache(
storage: BaseStorage, study_id: int, objective_id: int, trials: list[FrozenTrial]
) -> ImportanceType:
) -> list[ImportanceType]:
completed_trials = [t for t in trials if t.state == TrialState.COMPLETE]
n_completed_trials = len(completed_trials)
if n_completed_trials == 0:
return {"target_name": target_name, "param_importances": []}
return []
cache_key = f"{study_id}:{objective_id}"
with param_importance_cache_lock:
cache_n_trial, cache_importance = param_importance_cache.get(
cache_key, (0, {"target_name": target_name, "param_importances": []})
)
cache_n_trial, cache_importance = param_importance_cache.get(cache_key, (0, []))
if n_completed_trials == cache_n_trial:
return cache_importance
@@ -95,18 +85,15 @@ def get_param_importance_from_trials_cache(
def convert_to_importance_type(
importance: dict[str, float], trials: list[FrozenTrial]
) -> ImportanceType:
return {
"target_name": target_name,
"param_importances": [
{
"name": name,
"importance": importance,
"distribution": get_distribution_name(name, trials),
}
for name, importance in importance.items()
],
}
) -> list[ImportanceType]:
return [
{
"name": name,
"importance": importance,
"distribution": get_distribution_name(name, trials),
}
for name, importance in importance.items()
]
def get_distribution_name(param_name: str, trials: list[FrozenTrial]) -> str:
+39 -18
View File
@@ -8,6 +8,7 @@ from optuna.storages import BaseStorage
if TYPE_CHECKING:
from typing import Optional
from typing import TypedDict
NoteType = TypedDict(
@@ -19,32 +20,50 @@ if TYPE_CHECKING:
)
SYSTEM_ATTR_MAX_LENGTH = 2045
NOTE_VER_KEY = "dashboard:note_ver"
NOTE_STR_KEY_PREFIX = "dashboard:note_str:"
def get_note_from_system_attrs(system_attrs: dict[str, Any]) -> NoteType:
if NOTE_VER_KEY not in system_attrs:
def note_ver_key(trial_id: Optional[int]) -> str:
prefix = "dashboard:note_ver"
if trial_id is None:
return prefix
return f"dashboard:{trial_id}:note_ver"
def note_str_key_prefix(trial_id: Optional[int]) -> str:
prefix = "dashboard:note_str:"
if trial_id is None:
return prefix
return f"dashboard:{trial_id}:note_str:"
def get_note_from_system_attrs(system_attrs: dict[str, Any], trial_id: Optional[int]) -> NoteType:
if note_ver_key(trial_id) not in system_attrs:
return {
"version": 0,
"body": "",
}
note_ver = int(system_attrs[NOTE_VER_KEY])
note_ver = int(system_attrs[note_ver_key(trial_id)])
note_attrs: dict[str, str] = {
key: value for key, value in system_attrs.items() if key.startswith(NOTE_STR_KEY_PREFIX)
key: value
for key, value in system_attrs.items()
if key.startswith(note_str_key_prefix(trial_id))
}
return {"version": note_ver, "body": concat_body(note_attrs)}
return {"version": note_ver, "body": concat_body(note_attrs, trial_id)}
def version_is_incremented(system_attrs: dict[str, Any], req_note_ver: int) -> bool:
db_note_ver = system_attrs.get(NOTE_VER_KEY, 0)
def version_is_incremented(
system_attrs: dict[str, Any], trial_id: Optional[int], req_note_ver: int
) -> bool:
db_note_ver = system_attrs.get(note_ver_key(trial_id), 0)
return req_note_ver == db_note_ver + 1
def save_note(storage: BaseStorage, study_id: int, ver: int, body: str) -> None:
storage.set_study_system_attr(study_id, NOTE_VER_KEY, ver)
def save_note(
storage: BaseStorage, study_id: int, trial_id: Optional[int], ver: int, body: str
) -> None:
storage.set_study_system_attr(study_id, note_ver_key(trial_id), ver)
attrs = split_body(body)
attrs = split_body(body, trial_id)
for k, v in attrs.items():
storage.set_study_system_attr(study_id, k, v)
@@ -52,22 +71,24 @@ def save_note(storage: BaseStorage, study_id: int, ver: int, body: str) -> None:
all_note_attrs: dict[str, str] = {
key: value
for key, value in storage.get_study_system_attrs(study_id).items()
if key.startswith(NOTE_STR_KEY_PREFIX)
if key.startswith(note_str_key_prefix(trial_id))
}
if len(all_note_attrs) > len(attrs):
for i in range(len(attrs), len(all_note_attrs)):
storage.set_study_system_attr(study_id, f"{NOTE_STR_KEY_PREFIX}{i}", "")
storage.set_study_system_attr(study_id, f"{note_str_key_prefix(trial_id)}{i}", "")
def split_body(note_str: str) -> dict[str, str]:
def split_body(note_str: str, trial_id: Optional[int]) -> dict[str, str]:
note_len = len(note_str)
attrs = {}
for i in range(math.ceil(note_len / SYSTEM_ATTR_MAX_LENGTH)):
start = i * SYSTEM_ATTR_MAX_LENGTH
end = min((i + 1) * SYSTEM_ATTR_MAX_LENGTH, note_len)
attrs[f"{NOTE_STR_KEY_PREFIX}{i}"] = note_str[start:end]
attrs[f"{note_str_key_prefix(trial_id)}{i}"] = note_str[start:end]
return attrs
def concat_body(note_attrs: dict[str, str]) -> str:
return "".join(note_attrs[f"{NOTE_STR_KEY_PREFIX}{i}"] for i in range(len(note_attrs)))
def concat_body(note_attrs: dict[str, str], trial_id: Optional[int]) -> str:
return "".join(
note_attrs[f"{note_str_key_prefix(trial_id)}{i}"] for i in range(len(note_attrs))
)
+10 -4
View File
@@ -84,22 +84,27 @@ def serialize_study_detail(
"name": summary.study_name,
"directions": [d.name.lower() for d in summary.directions],
}
system_attrs = getattr(summary, "system_attrs", {})
if summary.datetime_start is not None:
serialized["datetime_start"] = summary.datetime_start.isoformat()
serialized["trials"] = [serialize_frozen_trial(summary._study_id, trial) for trial in trials]
serialized["trials"] = [
serialize_frozen_trial(summary._study_id, trial, system_attrs) for trial in trials
]
serialized["best_trials"] = [
serialize_frozen_trial(summary._study_id, trial) for trial in best_trials
serialize_frozen_trial(summary._study_id, trial, system_attrs) for trial in best_trials
]
serialized["intersection_search_space"] = serialize_search_space(intersection)
serialized["union_search_space"] = serialize_search_space(union)
serialized["union_user_attrs"] = [{"key": a[0], "sortable": a[1]} for a in union_user_attrs]
serialized["has_intermediate_values"] = has_intermediate_values
serialized["note"] = note.get_note_from_system_attrs(getattr(summary, "system_attrs", {}))
serialized["note"] = note.get_note_from_system_attrs(system_attrs, None)
return serialized
def serialize_frozen_trial(study_id: int, trial: FrozenTrial) -> dict[str, Any]:
def serialize_frozen_trial(
study_id: int, trial: FrozenTrial, study_system_attrs: dict[str, Any]
) -> dict[str, Any]:
serialized = {
"trial_id": trial._trial_id,
"study_id": study_id,
@@ -108,6 +113,7 @@ def serialize_frozen_trial(study_id: int, trial: FrozenTrial) -> dict[str, Any]:
"params": [{"name": name, "value": str(value)} for name, value in trial.params.items()],
"user_attrs": serialize_attrs(trial.user_attrs),
"system_attrs": serialize_attrs(getattr(trial, "_system_attrs", {})),
"note": note.get_note_from_system_attrs(study_system_attrs, trial._trial_id),
}
serialized_intermediate_values: list[IntermediateValue] = []
+97 -4
View File
@@ -3,14 +3,17 @@ import { useSnackbar } from "notistack"
import {
getStudyDetailAPI,
getStudySummariesAPI,
getParamImportances,
createNewStudyAPI,
deleteStudyAPI,
saveNoteAPI,
saveStudyNoteAPI,
saveTrialNoteAPI,
} from "./apiClient"
import {
graphVisibilityState,
studyDetailsState,
studySummariesState,
paramImportanceState,
} from "./state"
const localStorageGraphVisibility = "graphVisibility"
@@ -23,6 +26,8 @@ export const actionCreator = () => {
useRecoilState<StudyDetails>(studyDetailsState)
const [graphVisibility, setGraphVisibility] =
useRecoilState<GraphVisibility>(graphVisibilityState)
const [paramImportance, setParamImportance] =
useRecoilState<StudyParamImportance>(paramImportanceState)
const setStudyDetailState = (studyId: number, study: StudyDetail) => {
const newVal = Object.assign({}, studyDetails)
@@ -30,6 +35,28 @@ export const actionCreator = () => {
setStudyDetails(newVal)
}
const setTrialNote = (studyId: number, index: number, note: Note) => {
const newTrial: Trial = Object.assign(
{},
studyDetails[studyId].trials[index]
)
newTrial.note = note
const newTrials: Trial[] = [...studyDetails[studyId].trials]
newTrials[index] = newTrial
const newStudy: StudyDetail = Object.assign({}, studyDetails[studyId])
newStudy.trials = newTrials
setStudyDetailState(studyId, newStudy)
}
const setStudyParamImportanceState = (
studyId: number,
importance: ParamImportance[][]
) => {
const newVal = Object.assign({}, paramImportance)
newVal[studyId] = importance
setParamImportance(newVal)
}
const updateStudySummaries = (successMsg?: string) => {
getStudySummariesAPI()
.then((studySummaries: StudySummary[]) => {
@@ -77,6 +104,22 @@ export const actionCreator = () => {
})
}
const updateParamImportance = (studyId: number) => {
getParamImportances(studyId)
.then((importance) => {
setStudyParamImportanceState(studyId, importance)
})
.catch((err) => {
const reason = err.response?.data.reason
enqueueSnackbar(
`Failed to load hyperparameter importance (reason=${reason})`,
{
variant: "error",
}
)
})
}
const createNewStudy = (studyName: string, directions: StudyDirection[]) => {
createNewStudyAPI(studyName, directions)
.then((study_summary) => {
@@ -128,8 +171,8 @@ export const actionCreator = () => {
localStorage.setItem(localStorageGraphVisibility, JSON.stringify(value))
}
const saveNote = (studyId: number, note: Note): Promise<void> => {
return saveNoteAPI(studyId, note)
const saveStudyNote = (studyId: number, note: Note): Promise<void> => {
return saveStudyNoteAPI(studyId, note)
.then(() => {
const newStudy = Object.assign({}, studyDetails[studyId])
newStudy.note = note
@@ -154,14 +197,64 @@ export const actionCreator = () => {
})
}
const saveTrialNote = (
studyId: number,
trialId: number,
note: Note
): Promise<void> => {
return saveTrialNoteAPI(studyId, trialId, note)
.then(() => {
const index = studyDetails[studyId].trials.findIndex(
(t) => t.trial_id === trialId
)
if (index === -1) {
enqueueSnackbar(`Unexpected error happens. Please reload the page.`, {
variant: "error",
})
return
}
setTrialNote(studyId, index, note)
enqueueSnackbar(`Success to save the note`, {
variant: "success",
})
})
.catch((err) => {
console.dir(err)
if (err.response.status === 409) {
const index = studyDetails[studyId].trials.findIndex(
(t) => t.trial_id === trialId
)
if (index === -1) {
enqueueSnackbar(
`Unexpected error happens. Please reload the page.`,
{
variant: "error",
}
)
return
}
setTrialNote(studyId, index, note)
}
const reason = err.response?.data.reason
if (reason !== undefined) {
enqueueSnackbar(`Failed: ${reason}`, {
variant: "error",
})
}
throw err
})
}
return {
updateStudyDetail,
updateStudySummaries,
updateParamImportance,
createNewStudy,
deleteStudy,
getGraphVisibility,
saveGraphVisibility,
saveNote,
saveStudyNote,
saveTrialNote,
}
}
+21 -19
View File
@@ -14,6 +14,7 @@ interface TrialResponse {
params: TrialParam[]
user_attrs: Attribute[]
system_attrs: Attribute[]
note: Note
}
const convertTrialResponse = (res: TrialResponse): Trial => {
@@ -33,6 +34,7 @@ const convertTrialResponse = (res: TrialResponse): Trial => {
params: res.params,
user_attrs: res.user_attrs,
system_attrs: res.system_attrs,
note: res.note,
}
}
@@ -46,10 +48,7 @@ interface StudyDetailResponse {
union_search_space: SearchSpace[]
union_user_attrs: AttributeSpec[]
has_intermediate_values: boolean
note: {
version: number
body: string
}
note: Note
}
export const getStudyDetailAPI = (
@@ -183,7 +182,7 @@ export const deleteStudyAPI = (studyId: number) => {
})
}
export const saveNoteAPI = (
export const saveStudyNoteAPI = (
studyId: number,
note: { version: number; body: string }
): Promise<void> => {
@@ -194,25 +193,28 @@ export const saveNoteAPI = (
})
}
export const saveTrialNoteAPI = (
studyId: number,
trialId: number,
note: { version: number; body: string }
): Promise<void> => {
return axiosInstance
.put<void>(`/api/studies/${studyId}/${trialId}/note`, note)
.then((res) => {
return
})
}
interface ParamImportancesResponse {
target_name: string
param_importances: ParamImportance[]
param_importances: ParamImportance[][]
}
export const getParamImportances = (
studyId: number,
objectiveId = 0
): Promise<ParamImportances> => {
studyId: number
): Promise<ParamImportance[][]> => {
return axiosInstance
.get<ParamImportancesResponse>(
`/api/studies/${studyId}/param_importances`,
{
params: {
objective_id: objectiveId,
},
}
)
.get<ParamImportancesResponse>(`/api/studies/${studyId}/param_importances`)
.then((res) => {
return res.data
return res.data.param_importances
})
}
+10 -1
View File
@@ -76,7 +76,16 @@ export const App: FC = () => {
children={
<StudyDetailBeta
toggleColorMode={toggleColorMode}
page={"trials"}
page={"trialList"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/trialTable"}
children={
<StudyDetailBeta
toggleColorMode={toggleColorMode}
page={"trialTable"}
/>
}
/>
+24 -7
View File
@@ -14,9 +14,10 @@ import ListItem from "@mui/material/ListItem"
import ListItemButton from "@mui/material/ListItemButton"
import ListItemIcon from "@mui/material/ListItemIcon"
import ListItemText from "@mui/material/ListItemText"
import { reloadIntervalState } from "../state"
import { drawerOpenState, reloadIntervalState } from "../state"
import { Link } from "react-router-dom"
import AutoGraphIcon from "@mui/icons-material/AutoGraph"
import ViewListIcon from "@mui/icons-material/ViewList"
import SyncIcon from "@mui/icons-material/Sync"
import SyncDisabledIcon from "@mui/icons-material/SyncDisabled"
import Brightness4Icon from "@mui/icons-material/Brightness4"
@@ -104,12 +105,12 @@ const Drawer = styled(MuiDrawer, {
export const AppDrawer: FC<{
studyId?: number
toggleColorMode: () => void
page?: "history" | "analytics" | "trials" | "note"
page?: PageId
toolbar: React.ReactNode
children?: React.ReactNode
}> = ({ studyId, toggleColorMode, page, toolbar, children }) => {
const theme = useTheme()
const [open, setOpen] = React.useState(false)
const [open, setOpen] = useRecoilState<boolean>(drawerOpenState)
const [reloadInterval, updateReloadInterval] =
useRecoilState<number>(reloadIntervalState)
@@ -199,17 +200,30 @@ export const AppDrawer: FC<{
<ListItemText primary="Analytics" sx={styleListItemText} />
</ListItemButton>
</ListItem>
<ListItem key="Table" disablePadding sx={styleListItem}>
<ListItem key="TableList" disablePadding sx={styleListItem}>
<ListItemButton
component={Link}
to={`${URL_PREFIX}/studies/${studyId}/trials`}
sx={styleListItemButton}
selected={page === "trials"}
selected={page === "trialList"}
>
<ListItemIcon sx={styleListItemIcon}>
<ViewListIcon />
</ListItemIcon>
<ListItemText primary="Trials (List)" sx={styleListItemText} />
</ListItemButton>
</ListItem>
<ListItem key="TrialTable" disablePadding sx={styleListItem}>
<ListItemButton
component={Link}
to={`${URL_PREFIX}/studies/${studyId}/trialTable`}
sx={styleListItemButton}
selected={page === "trialTable"}
>
<ListItemIcon sx={styleListItemIcon}>
<TableViewIcon />
</ListItemIcon>
<ListItemText primary="Trials" sx={styleListItemText} />
<ListItemText primary="Trials (Table)" sx={styleListItemText} />
</ListItemButton>
</ListItem>
<ListItem key="Note" disablePadding sx={styleListItem}>
@@ -301,7 +315,10 @@ export const AppDrawer: FC<{
<ListItemIcon sx={styleListItemIcon}>
<ClearIcon />
</ListItemIcon>
<ListItemText primary="Quit Beta UI" sx={styleListItemText} />
<ListItemText
primary="Switch to stable UI"
sx={styleListItemText}
/>
</ListItemButton>
</ListItem>
</List>
@@ -127,6 +127,7 @@ export const GraphHistory: FC<{
control={
<Checkbox
checked={!filterPrunedTrial}
disabled={!study?.has_intermediate_values}
onChange={handleFilterPrunedChange}
/>
}
@@ -10,51 +10,141 @@ import {
SelectChangeEvent,
useTheme,
Box,
Card,
CardContent,
} from "@mui/material"
import Grid2 from "@mui/material/Unstable_Grid2"
import { getParamImportances } from "../apiClient"
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
import { useSnackbar } from "notistack"
import { actionCreator } from "../action"
import { useParamImportanceValue, useStudyDirections } from "../state"
const plotDomId = "graph-hyperparameter-importances"
const getPlotDomId = (objectiveId: number) => `graph-importance-${objectiveId}`
export const GraphHyperparameterImportanceBeta: FC<{
studyId: number
study: StudyDetail | null
graphHeight: string
}> = ({ studyId, study = null, graphHeight }) => {
const theme = useTheme()
const action = actionCreator()
const importances = useParamImportanceValue(studyId)
const numCompletedTrials =
study?.trials.filter((t) => t.state === "Complete").length || 0
const nObjectives = useStudyDirections(studyId)?.length
useEffect(() => {
action.updateParamImportance(studyId)
}, [numCompletedTrials])
useEffect(() => {
if (importances !== null && nObjectives === importances.length) {
plotParamImportancesBeta(importances, theme.palette.mode)
}
}, [nObjectives, importances, theme.palette.mode])
return (
<>
{Array.from({ length: nObjectives || 1 }, (_, i) => {
let title = `Importance for the Objective Value`
if (nObjectives != null && nObjectives > 1) {
title = `Importance for the Objective ${i}`
}
return (
<Grid2 key={i} xs={6}>
<Card>
<CardContent>
<Typography
variant="h6"
sx={{ margin: "1em 0", fontWeight: 600, textAlign: "center" }}
>
{title}
</Typography>
<Box id={getPlotDomId(i)} sx={{ height: graphHeight }} />
</CardContent>
</Card>
</Grid2>
)
})}
</>
)
}
const plotParamImportancesBeta = (
importances: ParamImportance[][],
mode: string
) => {
const layout: Partial<plotly.Layout> = {
xaxis: {
title: "Hyperparameter Importance",
},
yaxis: {
title: "Hyperparameter",
automargin: true,
},
margin: {
l: 50,
t: 0,
r: 50,
b: 50,
},
showlegend: false,
template: mode === "dark" ? plotlyDarkTemplate : {},
}
importances.forEach((importance, objectiveId) => {
if (document.getElementById(getPlotDomId(objectiveId)) === null) {
return
}
const reversed = [...importance].reverse()
const importance_values = reversed.map((p) => p.importance)
const param_names = reversed.map((p) => p.name)
const param_hover_templates = reversed.map(
(p) => `${p.name} (${p.distribution}): ${p.importance} <extra></extra>`
)
const plotData: Partial<plotly.PlotData>[] = [
{
type: "bar",
orientation: "h",
x: importance_values,
y: param_names,
text: importance_values.map((v) => String(v.toFixed(2))),
textposition: "outside",
hovertemplate: param_hover_templates,
marker: {
color: "rgb(66,146,198)",
},
},
]
plotly.react(getPlotDomId(objectiveId), plotData, layout)
})
}
export const GraphHyperparameterImportances: FC<{
study: StudyDetail | null
studyId: number
}> = ({ study = null, studyId }) => {
const theme = useTheme()
const action = actionCreator()
const importances = useParamImportanceValue(studyId)
const [objectiveId, setObjectiveId] = useState<number>(0)
const numCompletedTrials =
study?.trials.filter((t) => t.state === "Complete").length || 0
const [importances, setImportances] = useState<ParamImportances | null>(null)
const { enqueueSnackbar } = useSnackbar()
const handleObjectiveChange = (event: SelectChangeEvent<number>) => {
setObjectiveId(event.target.value as number)
}
useEffect(() => {
if (numCompletedTrials > 0) {
getParamImportances(studyId, objectiveId)
.then((p) => {
setImportances(p)
})
.catch((err) => {
const reason = err.response?.data.reason
enqueueSnackbar(
`Failed to load hyperparameter importance (reason=${reason})`,
{
variant: "error",
}
)
})
}
}, [numCompletedTrials, objectiveId, theme.palette.mode])
action.updateParamImportance(studyId)
}, [numCompletedTrials])
useEffect(() => {
if (importances !== null) {
plotParamImportances(importances, theme.palette.mode)
if (importances !== null && importances.length > objectiveId) {
plotParamImportances(importances[objectiveId], theme.palette.mode)
}
}, [importances, theme.palette.mode])
}, [importances, objectiveId, theme.palette.mode])
return (
<Grid container direction="row">
@@ -88,25 +178,20 @@ export const GraphHyperparameterImportances: FC<{
)
}
const plotParamImportances = (
paramsImportanceData: ParamImportances,
mode: string
) => {
const plotParamImportances = (importance: ParamImportance[], mode: string) => {
if (document.getElementById(plotDomId) === null) {
return
}
const param_importances = [
...paramsImportanceData.param_importances,
].reverse()
const importance_values = param_importances.map((p) => p.importance)
const param_names = param_importances.map((p) => p.name)
const param_hover_templates = param_importances.map(
const reversed = [...importance].reverse()
const importance_values = reversed.map((p) => p.importance)
const param_names = reversed.map((p) => p.name)
const param_hover_templates = reversed.map(
(p) => `${p.name} (${p.distribution}): ${p.importance} <extra></extra>`
)
const layout: Partial<plotly.Layout> = {
xaxis: {
title: `Importance for ${paramsImportanceData.target_name}`,
title: `Importance for the Objective Value`,
},
yaxis: {
title: "Hyperparameter",
@@ -61,7 +61,6 @@ export const GraphSlice: FC<{
}
const handleLogYScaleChange = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
setLogYScale(!logYScale)
}
+130 -11
View File
@@ -3,22 +3,96 @@ import {
Button,
Card,
CardContent,
CardHeader,
IconButton,
SxProps,
TextField,
Typography,
useTheme,
} from "@mui/material"
import React, { FC, createRef, useState, useEffect } from "react"
import ReactMarkdown from "react-markdown"
import remarkGfm from "remark-gfm"
import LoadingButton from "@mui/lab/LoadingButton"
import SaveIcon from "@mui/icons-material/Save"
import EditIcon from "@mui/icons-material/Edit"
import CloseIcon from "@mui/icons-material/Close"
import Divider from "@mui/material/Divider"
import { Theme } from "@mui/material/styles"
import {
CodeComponent,
ReactMarkdownNames,
} from "react-markdown/lib/ast-to-react"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { darcula } from "react-syntax-highlighter/dist/esm/styles/prism"
import { actionCreator } from "../action"
export const Note: FC<{
const CodeBlock: CodeComponent | ReactMarkdownNames = ({
inline,
className,
children,
...props
}) => {
const match = /language-(\w+)/.exec(className || "")
return !inline && match ? (
<SyntaxHighlighter
style={darcula}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
export const TrialNote: FC<{
studyId: number
trialId: number
latestNote: Note
cardSx?: SxProps<Theme>
}> = ({ studyId, trialId, latestNote, cardSx }) => {
return (
<NoteBase
studyId={studyId}
trialId={trialId}
latestNote={latestNote}
minRows={30}
cardSx={cardSx}
/>
)
}
export const StudyNote: FC<{
studyId: number
latestNote: Note
minRows: number
}> = ({ studyId, latestNote, minRows }) => {
cardSx?: SxProps<Theme>
}> = ({ studyId, latestNote, minRows, cardSx }) => {
return (
<NoteBase
studyId={studyId}
latestNote={latestNote}
minRows={minRows}
cardSx={cardSx}
/>
)
}
const NoteBase: FC<{
studyId: number
trialId?: number
latestNote: Note
minRows: number
cardSx?: SxProps<Theme>
}> = ({ studyId, trialId, latestNote, minRows, cardSx }) => {
const theme = useTheme()
const [renderMarkdown, setRenderMarkdown] = useState(true)
const [saving, setSaving] = useState(false)
const [edited, setEdited] = useState(false)
const [curNote, setCurNote] = useState({ version: 0, body: "" })
@@ -45,10 +119,17 @@ export const Note: FC<{
body: textAreaRef.current ? textAreaRef.current.value : "",
}
setSaving(true)
action
.saveNote(studyId, newNote)
let actionResponse: Promise<void>
if (trialId === undefined) {
actionResponse = action.saveStudyNote(studyId, newNote)
} else {
actionResponse = action.saveTrialNote(studyId, trialId, newNote)
}
actionResponse
.then(() => {
setCurNote(newNote)
setRenderMarkdown(true)
window.onbeforeunload = null
})
.finally(() => {
@@ -65,17 +146,27 @@ export const Note: FC<{
window.onbeforeunload = null
}
return (
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<Typography variant="h6" sx={{ fontSize: "1.25rem", fontWeight: 600 }}>
Note
</Typography>
let content
if (renderMarkdown) {
const defaultBody =
"*A markdown editor for taking a memo, related to the study. Click the 'Edit' button in the upper right corner to access the editor.*"
content = (
<ReactMarkdown
children={latestNote.body || defaultBody}
remarkPlugins={[remarkGfm]}
components={{ code: CodeBlock }}
/>
)
} else {
content = (
<>
<TextField
disabled={saving}
minRows={minRows}
multiline={true}
placeholder="Description about the study... (This note is saved to study's system_attrs)"
placeholder={`Description about the ${
trialId === undefined ? "study" : "trial"
}...`}
sx={{ width: "100%", margin: `${theme.spacing(1)} 0` }}
inputProps={{ style: { resize: "vertical" } }}
inputRef={textAreaRef}
@@ -123,6 +214,34 @@ export const Note: FC<{
Save
</LoadingButton>
</Box>
</>
)
}
return (
<Card sx={{ margin: theme.spacing(2), ...cardSx }}>
<CardHeader
title="Note"
action={
!renderMarkdown ? (
<IconButton
onClick={() => {
setRenderMarkdown(true)
}}
>
<CloseIcon />
</IconButton>
) : (
<IconButton onClick={() => setRenderMarkdown(false)}>
<EditIcon />
</IconButton>
)
}
sx={{ paddingBottom: 0 }}
/>
<CardContent sx={{ paddingTop: theme.spacing(1) }}>
<Divider />
{content}
</CardContent>
</Card>
)
@@ -24,7 +24,7 @@ import { GraphIntermediateValues } from "./GraphIntermediateValues"
import { GraphSlice } from "./GraphSlice"
import { GraphHistory } from "./GraphHistory"
import { GraphParetoFront } from "./GraphParetoFront"
import { Note } from "./Note"
import { StudyNote } from "./Note"
import { actionCreator } from "../action"
import {
graphVisibilityState,
@@ -235,7 +235,7 @@ export const StudyDetail: FC<{
<TrialTable studyDetail={studyDetail} />
</Card>
{studyDetail !== null ? (
<Note
<StudyNote
studyId={studyIdNumber}
latestNote={studyDetail.note}
minRows={5}
@@ -2,12 +2,11 @@ import React, { FC, useEffect } from "react"
import { useRecoilValue } from "recoil"
import { Link, useParams } from "react-router-dom"
import {
Box,
Card,
CardContent,
Box,
Typography,
useTheme,
ListItem,
IconButton,
} from "@mui/material"
import Grid2 from "@mui/material/Unstable_Grid2"
@@ -15,40 +14,31 @@ import ChevronRightIcon from "@mui/icons-material/ChevronRight"
import HomeIcon from "@mui/icons-material/Home"
import { GraphHistory } from "./GraphHistory"
import { Note } from "./Note"
import { StudyNote } from "./Note"
import { actionCreator } from "../action"
import {
reloadIntervalState,
studyDetailsState,
studySummariesState,
useStudyDetailValue,
useStudyDirections,
useStudyName,
useStudySummaryValue,
} from "../state"
import { TrialTable } from "./TrialTable"
import { AppDrawer } from "./AppDrawer"
import { GraphParallelCoordinate } from "./GraphParallelCoordinate"
import { Contour } from "./GraphContour"
import { GraphHyperparameterImportances } from "./GraphHyperparameterImportances"
import { GraphHyperparameterImportanceBeta } from "./GraphHyperparameterImportances"
import { GraphSlice } from "./GraphSlice"
import { GraphParetoFront } from "./GraphParetoFront"
import { DataGrid, DataGridColumn } from "./DataGrid"
import List from "@mui/material/List"
import { GraphIntermediateValues } from "./GraphIntermediateValues"
import { Edf } from "./GraphEdf"
import { TrialList } from "./TrialList"
interface ParamTypes {
studyId: string
}
type PageId = "history" | "analytics" | "trials" | "note"
const useStudyDetailValue = (studyId: number): StudyDetail | null => {
const studyDetails = useRecoilValue<StudyDetails>(studyDetailsState)
return studyDetails[studyId] || null
}
const useStudySummaryValue = (studyId: number): StudySummary | null => {
const studySummaries = useRecoilValue<StudySummary[]>(studySummariesState)
return studySummaries.find((s) => s.study_id == studyId) || null
}
export const StudyDetailBeta: FC<{
toggleColorMode: () => void
page: PageId
@@ -60,20 +50,19 @@ export const StudyDetailBeta: FC<{
const studyDetail = useStudyDetailValue(studyIdNumber)
const reloadInterval = useRecoilValue<number>(reloadIntervalState)
const studySummary = useStudySummaryValue(studyIdNumber)
const directions = studyDetail?.directions || studySummary?.directions || null
const directions = useStudyDirections(studyIdNumber)
const studyName = useStudyName(studyIdNumber)
const userAttrs = studySummary?.user_attrs || []
const title =
studyDetail !== null || studySummary !== null
? `${studyDetail?.name || studySummary?.study_name} (id=${studyId})`
: `Study #${studyId}`
studyName !== null ? `${studyName} (id=${studyId})` : `Study #${studyId}`
useEffect(() => {
action.updateStudyDetail(studyIdNumber)
}, [])
useEffect(() => {
if (reloadInterval < 0 || page === "trials") {
if (reloadInterval < 0 || page === "trialTable" || page === "trialList") {
return
}
const intervalId = setInterval(function () {
@@ -92,6 +81,13 @@ export const StudyDetailBeta: FC<{
if (page === "history") {
content = (
<Box sx={{ display: "flex", width: "100%", flexDirection: "column" }}>
{directions !== null && directions.length > 1 ? (
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<GraphParetoFront study={studyDetail} />
</CardContent>
</Card>
) : null}
<Card
sx={{
margin: theme.spacing(2),
@@ -110,17 +106,21 @@ export const StudyDetailBeta: FC<{
</CardContent>
</Card>
) : null}
{directions !== null && directions.length > 1 ? (
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<GraphParetoFront study={studyDetail} />
</CardContent>
</Card>
) : null}
<Grid2 container spacing={2}>
<Grid2 xs={6}>
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<Grid2 container spacing={2} sx={{ padding: theme.spacing(0, 2) }}>
<GraphHyperparameterImportanceBeta
studyId={studyIdNumber}
study={studyDetail}
graphHeight="450px"
/>
<Grid2 xs={6} spacing={2}>
<Card>
<CardContent
sx={{
alignItems: "center",
display: "flex",
flexDirection: "column",
}}
>
{studyDetail !== null &&
studyDetail.best_trials.length === 1 && (
<>
@@ -131,37 +131,89 @@ export const StudyDetailBeta: FC<{
Best Trial
</Typography>
<Typography
variant="h2"
sx={{ fontWeight: 600 }}
variant="h3"
sx={{ fontWeight: 600, marginBottom: theme.spacing(2) }}
color="secondary"
>
{studyDetail.best_trials[0].values}
</Typography>
<List>
{studyDetail.best_trials[0].params.map((param) => (
<ListItem>
{param.name} {param.value}
</ListItem>
))}
</List>
<Typography>
number={studyDetail.best_trials[0].number}
</Typography>
<Typography>
trial_id={studyDetail.best_trials[0].trial_id}
</Typography>
<Typography>
Params = [
{studyDetail.best_trials[0].params
.map((p) => `${p.name}: ${p.value}`)
.join(", ")}
]
</Typography>
<Typography>
Intermediate Values = [
{studyDetail.best_trials[0].intermediate_values
.map((p) => `${p.step}: ${p.value}`)
.join(", ")}
]
</Typography>
<Typography>
User Attributes = [
{studyDetail.best_trials[0].user_attrs
.map((p) => `${p.key}: ${p.value}`)
.join(", ")}
]
</Typography>
</>
)}
{studyDetail !== null && studyDetail.best_trials.length > 1 && (
{studyDetail !== null && studyDetail.directions.length > 1 && (
<>
<Typography
variant="h6"
sx={{ margin: "1em 0", fontWeight: 600 }}
>
Best Trials
Best Trials ({studyDetail.best_trials.length} trials)
</Typography>
{studyDetail.best_trials.map((trial, i) => (
<Card
key={i}
sx={{
border: "1px solid rgba(128,128,128,0.5)",
margin: theme.spacing(1, 0),
}}
>
<CardContent>
<Typography variant="h6">
Trial number={trial.number} (trial_id=
{trial.trial_id})
</Typography>
<Typography>
Objective Values = [{trial.values?.join(", ")}]
</Typography>
<Typography>
Params = [
{trial.params
.map((p) => `${p.name}: ${p.value}`)
.join(", ")}
]
</Typography>
</CardContent>
</Card>
))}
</>
)}
</CardContent>
</Card>
</Grid2>
<Grid2 xs={6}>
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<Card>
<CardContent
sx={{
alignItems: "center",
display: "flex",
flexDirection: "column",
}}
>
<Typography
variant="h6"
sx={{ margin: "1em 0", fontWeight: 600 }}
@@ -185,17 +237,6 @@ export const StudyDetailBeta: FC<{
} else if (page === "analytics") {
content = (
<Box sx={{ display: "flex", width: "100%", flexDirection: "column" }}>
<Typography variant="h5" sx={{ margin: theme.spacing(2) }}>
Hyperparameter Importance
</Typography>
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<GraphHyperparameterImportances
study={studyDetail}
studyId={studyIdNumber}
/>
</CardContent>
</Card>
<Typography variant="h5" sx={{ margin: theme.spacing(2) }}>
Hyperparameter Relationships
</Typography>
@@ -214,25 +255,35 @@ export const StudyDetailBeta: FC<{
<Contour study={studyDetail} />
</CardContent>
</Card>
<Typography variant="h5" sx={{ margin: theme.spacing(2) }}>
Empirical Distribution of the Objective Value
</Typography>
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<Edf study={studyDetail} />
</CardContent>
</Card>
</Box>
)
} else if (page === "trials") {
} else if (page === "trialTable") {
content = (
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<TrialTable studyDetail={studyDetail} />
<TrialTable studyDetail={studyDetail} initialRowsPerPage={50} />
</CardContent>
</Card>
)
} else if (page === "note") {
content =
studyDetail !== null ? (
<Note
studyId={studyIdNumber}
latestNote={studyDetail.note}
minRows={30}
/>
) : null
} else if (page === "trialList") {
content = <TrialList studyDetail={studyDetail} />
} else if (page === "note" && studyDetail !== null) {
content = (
<StudyNote
studyId={studyIdNumber}
latestNote={studyDetail.note}
minRows={30}
cardSx={{ height: "90vh" }}
/>
)
}
const toolbar = (
+1 -16
View File
@@ -23,7 +23,6 @@ import { DebouncedInputTextField } from "./Debounce"
import { studySummariesState } from "../state"
import Brightness7Icon from "@mui/icons-material/Brightness7"
import Brightness4Icon from "@mui/icons-material/Brightness4"
import { useSnackbar } from "notistack"
import { useDeleteStudyDialog } from "./DeleteStudyDialog"
import { useCreateStudyDialog } from "./CreateStudyDialog"
@@ -31,7 +30,6 @@ export const StudyList: FC<{
toggleColorMode: () => void
}> = ({ toggleColorMode }) => {
const theme = useTheme()
const { enqueueSnackbar } = useSnackbar()
const [studyFilterText, setStudyFilterText] = React.useState<string>("")
const studyFilter = (row: StudySummary) => {
@@ -150,15 +148,6 @@ export const StudyList: FC<{
)
}
const sayThankYouForBetaUsers = () => {
enqueueSnackbar(
`Thanks for testing our beta UI. Share your feedback via a GitHub issue.`,
{
variant: "success",
}
)
}
return (
<>
<AppBar position="static">
@@ -225,11 +214,7 @@ export const StudyList: FC<{
<CardContent>
<Typography>
{`We would appreciate your feedback on our beta UI. Click `}
<Link
to={`${URL_PREFIX}/beta`}
style={{ color: linkColor }}
onClick={sayThankYouForBetaUsers}
>
<Link to={`${URL_PREFIX}/beta`} style={{ color: linkColor }}>
here
</Link>
{" to try it out and share your thoughts."}
@@ -19,6 +19,7 @@ import {
} from "@mui/material"
import { Delete, Refresh, Search } from "@mui/icons-material"
import SortIcon from "@mui/icons-material/Sort"
import HomeIcon from "@mui/icons-material/Home"
import AddBoxIcon from "@mui/icons-material/AddBox"
import { actionCreator } from "../action"
@@ -101,11 +102,7 @@ export const StudyListBeta: FC<{
</Box>
)
const toolbar = (
<Typography variant="h5" noWrap component="div">
Optuna Dashboard (Beta ver.)
</Typography>
)
const toolbar = <HomeIcon sx={{ margin: theme.spacing(0, 1) }} />
return (
<Box sx={{ display: "flex" }}>
@@ -153,7 +150,7 @@ export const StudyListBeta: FC<{
}}
sx={{ marginRight: theme.spacing(2), minWidth: "120px" }}
>
Refresh
Reload
</Button>
<Button
variant="outlined"
@@ -181,7 +178,7 @@ export const StudyListBeta: FC<{
>
<CardContent>
<Typography variant="h5">
{study.study_id} {study.study_name}
{study.study_id}. {study.study_name}
</Typography>
<Typography
variant="subtitle1"
@@ -0,0 +1,214 @@
import React, { FC, useState } from "react"
import {
Typography,
Box,
Card,
CardContent,
useTheme,
CardHeader,
Grid,
} from "@mui/material"
import Chip from "@mui/material/Chip"
import Divider from "@mui/material/Divider"
import List from "@mui/material/List"
import ListItem from "@mui/material/ListItem"
import ListItemButton from "@mui/material/ListItemButton"
import ListItemText from "@mui/material/ListItemText"
import ListSubheader from "@mui/material/ListSubheader"
import { TrialNote } from "./Note"
import { DataGrid, DataGridColumn } from "./DataGrid"
export const TrialList: FC<{
studyDetail: StudyDetail | null
}> = ({ studyDetail }) => {
const theme = useTheme()
const [selected, setSelected] = useState<number>(0)
const trials: Trial[] = studyDetail !== null ? studyDetail.trials : []
const trialListWidth = 240
const isBestTrial = (trialId: number) => {
const bestTrialIDs = studyDetail?.best_trials.map((t) => t.trial_id) || []
return bestTrialIDs.findIndex((a) => a === trialId) != -1
}
const collapseIntermediateValueColumns: DataGridColumn<TrialIntermediateValue>[] =
[
{ field: "step", label: "Step", sortable: true },
{
field: "value",
label: "Value",
sortable: true,
less: (firstEl, secondEl): number => {
const firstVal = firstEl.value
const secondVal = secondEl.value
if (firstVal === secondVal) {
return 0
}
if (firstVal === "nan") {
return -1
} else if (secondVal === "nan") {
return 1
}
if (firstVal === "-inf" || secondVal === "inf") {
return 1
} else if (secondVal === "-inf" || firstVal === "inf") {
return -1
}
return firstVal < secondVal ? 1 : -1
},
},
]
const collapseAttrColumns: DataGridColumn<Attribute>[] = [
{ field: "key", label: "Key", sortable: true },
{ field: "value", label: "Value", sortable: true },
]
let content = null
if (trials.length > selected) {
const trial = trials[selected]
content = (
<>
<Card sx={{ margin: theme.spacing(2) }}>
<CardHeader
title={`Trial ${trial.number} (trial_id=${trial.trial_id})`}
/>
<CardContent>
{isBestTrial(trial.trial_id) ? (
<Chip
label={"Best Trial"}
color="primary"
sx={{ marginBottom: theme.spacing(1) }}
size="small"
/>
) : null}
<Typography>
Values:{" "}
{trial.values?.map((v) => v.toString()).join(" ") || "None"}
</Typography>
<Typography>
Params = [
{trial.params.map((p) => `${p.name}: ${p.value}`).join(", ")}]
</Typography>
</CardContent>
</Card>
<TrialNote
studyId={trial.study_id}
trialId={trial.trial_id}
latestNote={trial.note}
/>
<Grid
container
direction="row"
spacing={2}
sx={{ p: theme.spacing(0, 2) }}
>
<Grid item xs={6}>
<Card>
<CardHeader title="Intermediate Values" />
<CardContent>
<DataGrid<TrialIntermediateValue>
columns={collapseIntermediateValueColumns}
rows={trial.intermediate_values}
keyField={"step"}
dense={true}
rowsPerPageOption={[5, 10, { label: "All", value: -1 }]}
/>
</CardContent>
</Card>
</Grid>
<Grid item xs={6}>
<Card>
<CardHeader title="User Attributes" />
<CardContent>
<DataGrid<Attribute>
columns={collapseAttrColumns}
rows={trial.user_attrs}
keyField={"key"}
dense={true}
rowsPerPageOption={[5, 10, { label: "All", value: -1 }]}
/>
</CardContent>
</Card>
</Grid>
</Grid>
</>
)
}
return (
<Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}>
<Box
sx={{
width: trialListWidth,
overflow: "auto",
height: `calc(100vh - ${theme.spacing(8)})`,
}}
>
<List>
<ListSubheader>{`${
studyDetail?.trials.length || 0
} Trials`}</ListSubheader>
{trials.map((trial, i) => {
let color:
| "default"
| "primary"
| "secondary"
| "error"
| "info"
| "success"
| "warning" = "default"
if (trial.state === "Complete") {
color = "success"
} else if (trial.state === "Running") {
color = "secondary"
} else if (trial.state === "Waiting") {
color = "secondary"
} else if (trial.state === "Pruned") {
color = "warning"
} else if (trial.state === "Fail") {
color = "error"
}
return (
<ListItem key={trial.trial_id} disablePadding>
<ListItemButton
onClick={() => {
setSelected(i)
}}
selected={i === selected}
>
<ListItemText
primary={`Trial ${trial.trial_id}`}
secondary={
<Box sx={{ padding: theme.spacing(1, 0) }}>
<Chip color={color} label={trial.state} size="small" />
{isBestTrial(trial.trial_id) ? (
<Chip
label={"Best Trial"}
color="primary"
sx={{ marginLeft: theme.spacing(1) }}
size="small"
/>
) : null}
</Box>
}
/>
</ListItemButton>
</ListItem>
)
})}
</List>
</Box>
<Divider orientation="vertical" flexItem />
<Box
sx={{
flexGrow: 1,
overflow: "auto",
height: `calc(100vh - ${theme.spacing(8)})`,
}}
>
{content}
</Box>
</Box>
)
}
@@ -3,9 +3,10 @@ import { Typography, Grid, Box } from "@mui/material"
import { DataGridColumn, DataGrid } from "./DataGrid"
export const TrialTable: FC<{ studyDetail: StudyDetail | null }> = ({
studyDetail,
}) => {
export const TrialTable: FC<{
studyDetail: StudyDetail | null
initialRowsPerPage?: number
}> = ({ studyDetail, initialRowsPerPage }) => {
const trials: Trial[] = studyDetail !== null ? studyDetail.trials : []
const columns: DataGridColumn<Trial>[] = [
@@ -228,6 +229,7 @@ export const TrialTable: FC<{ studyDetail: StudyDetail | null }> = ({
]
const collapseBody = (index: number) => {
console.dir(trials)
return (
<Grid container direction="row">
<Grid item xs={6}>
@@ -269,6 +271,7 @@ export const TrialTable: FC<{ studyDetail: StudyDetail | null }> = ({
keyField={"trial_id"}
dense={true}
collapseBody={collapseBody}
initialRowsPerPage={initialRowsPerPage}
/>
)
}
+43 -1
View File
@@ -1,4 +1,4 @@
import { atom } from "recoil"
import { atom, useRecoilValue } from "recoil"
export const studySummariesState = atom<StudySummary[]>({
key: "studySummaries",
@@ -10,6 +10,11 @@ export const studyDetailsState = atom<StudyDetails>({
default: {},
})
export const paramImportanceState = atom<StudyParamImportance>({
key: "paramImportance",
default: {},
})
export const graphVisibilityState = atom<GraphVisibility>({
key: "graphVisibility",
default: {
@@ -28,3 +33,40 @@ export const reloadIntervalState = atom<number>({
key: "reloadInterval",
default: 10,
})
export const drawerOpenState = atom<boolean>({
key: "drawerOpen",
default: false,
})
export const useStudyDetailValue = (studyId: number): StudyDetail | null => {
const studyDetails = useRecoilValue<StudyDetails>(studyDetailsState)
return studyDetails[studyId] || null
}
export const useStudySummaryValue = (studyId: number): StudySummary | null => {
const studySummaries = useRecoilValue<StudySummary[]>(studySummariesState)
return studySummaries.find((s) => s.study_id == studyId) || null
}
export const useParamImportanceValue = (
studyId: number
): ParamImportance[][] | null => {
const studyParamImportance =
useRecoilValue<StudyParamImportance>(paramImportanceState)
return studyParamImportance[studyId] || null
}
export const useStudyDirections = (
studyId: number
): StudyDirection[] | null => {
const studyDetail = useStudyDetailValue(studyId)
const studySummary = useStudySummaryValue(studyId)
return studyDetail?.directions || studySummary?.directions || null
}
export const useStudyName = (studyId: number): string | null => {
const studyDetail = useStudyDetailValue(studyId)
const studySummary = useStudySummaryValue(studyId)
return studyDetail?.name || studySummary?.study_name || null
}
+5 -3
View File
@@ -21,6 +21,8 @@ type Distribution =
| "IntLogUniformDistribution"
| "CategoricalDistribution"
type PageId = "history" | "analytics" | "trialTable" | "trialList" | "note"
type GraphVisibility = {
history: boolean
paretoFront: boolean
@@ -80,6 +82,7 @@ declare interface Trial {
params: TrialParam[]
user_attrs: Attribute[]
system_attrs: Attribute[]
note: Note
}
declare interface StudySummary {
@@ -109,7 +112,6 @@ declare interface StudyDetails {
[study_id: string]: StudyDetail
}
declare interface ParamImportances {
target_name: string
param_importances: ParamImportance[]
declare interface StudyParamImportance {
[study_id: string]: ParamImportance[][]
}
+2367 -1
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -26,8 +26,11 @@
"plotly.js-dist-min": "^2.17.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-markdown": "^8.0.4",
"react-router-dom": "^5.3.4",
"recoil": "^0.7.6"
"react-syntax-highlighter": "^15.5.0",
"recoil": "^0.7.6",
"remark-gfm": "^3.0.1"
},
"devDependencies": {
"@babel/core": "^7.14.3",
@@ -38,6 +41,7 @@
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"@types/react-router-dom": "^5.3.3",
"@types/react-syntax-highlighter": "^15.5.5",
"@typescript-eslint/eslint-plugin": "^4.26.1",
"@typescript-eslint/parser": "^4.26.1",
"compression-webpack-plugin": "^10.0.0",
+2 -2
View File
@@ -15,7 +15,7 @@ class NoteTestCase(TestCase):
("012345", 2),
]:
with self.subTest(f"with_{dummy_body_str}_{attr_len}"):
attrs = note.split_body(dummy_body_str)
attrs = note.split_body(dummy_body_str, None)
assert len(attrs) == attr_len
actual = note.concat_body(attrs)
actual = note.concat_body(attrs, None)
assert dummy_body_str == actual
+8
View File
@@ -22,6 +22,10 @@ const trials = [
],
user_attrs: [],
system_attrs: [],
note: {
body: "",
version: 0,
},
},
{
trial_id: 2,
@@ -38,6 +42,10 @@ const trials = [
],
user_attrs: [],
system_attrs: [],
note: {
body: "",
version: 0,
},
},
]