Copy type script files from Goptuna

This commit is contained in:
c-bata
2020-10-24 14:51:14 +09:00
parent f74fbb7d1e
commit 4f51ac6ebd
18 changed files with 3965 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
14.14.0
+4
View File
@@ -0,0 +1,4 @@
trailingComma: "es5"
tabWidth: 2
semi: false
singleQuote: false
+75
View File
@@ -0,0 +1,75 @@
import { useRecoilState } from "recoil"
import { useSnackbar } from "notistack"
import {
getStudyDetailAPI,
getStudySummariesAPI,
createNewStudyAPI,
} from "./apiClient"
import { studyDetailsState, studySummariesState } from "./state"
export const actionCreator = () => {
const { enqueueSnackbar } = useSnackbar()
const [studySummaries, setStudySummaries] = useRecoilState<StudySummary[]>(
studySummariesState
)
const [studyDetails, setStudyDetails] = useRecoilState<StudyDetails>(
studyDetailsState
)
const updateStudySummaries = (successMsg?: string) => {
getStudySummariesAPI()
.then((studySummaries: StudySummary[]) => {
setStudySummaries(studySummaries)
if (successMsg) {
enqueueSnackbar(successMsg, { variant: "success" })
}
})
.catch((err) => {
enqueueSnackbar(`Failed to fetch study list.`, {
variant: "error",
})
console.log(err)
})
}
const updateStudyDetail = (studyId: number) => {
getStudyDetailAPI(studyId)
.then((study) => {
let newVal = Object.assign({}, studyDetails)
newVal[studyId] = study
setStudyDetails(newVal)
})
.catch((err) => {
enqueueSnackbar(`Failed to fetch study (id=${studyId})`, {
variant: "error",
})
console.log(err)
})
}
const createNewStudy = (studyName: string, direction: StudyDirection) => {
createNewStudyAPI(studyName, direction)
.then((study_summary) => {
const newVal = [...studySummaries, study_summary]
setStudySummaries(newVal)
enqueueSnackbar(`Success to create a study (study_name=${studyName})`, {
variant: "success",
})
})
.catch((err) => {
enqueueSnackbar(`Failed to create a study (study_name=${studyName})`, {
variant: "error",
})
console.log(err)
})
}
return {
updateStudyDetail,
updateStudySummaries,
createNewStudy,
}
}
export type Action = ReturnType<typeof actionCreator>
+138
View File
@@ -0,0 +1,138 @@
import axios from "axios"
const axiosInstance = axios.create({ baseURL: API_ENDPOINT })
interface TrialResponse {
trial_id: number
study_id: number
number: number
state: TrialState
value?: number
intermediate_values: TrialIntermediateValue[]
datetime_start: string
datetime_complete?: string
params: TrialParam[]
user_attrs: Attribute[]
system_attrs: Attribute[]
}
const convertTrialResponse = (res: TrialResponse): Trial => {
return {
trial_id: res.trial_id,
study_id: res.study_id,
number: res.number,
state: res.state,
value: res.value,
intermediate_values: res.intermediate_values,
datetime_start: new Date(res.datetime_start),
datetime_complete: res.datetime_complete
? new Date(res.datetime_complete)
: undefined,
params: res.params,
user_attrs: res.user_attrs,
system_attrs: res.system_attrs,
}
}
interface StudyDetailResponse {
name: string
datetime_start: string
direction: StudyDirection
best_trial?: TrialResponse
trials: TrialResponse[]
}
export const getStudyDetailAPI = (studyId: number): Promise<StudyDetail> => {
return axiosInstance
.get<StudyDetailResponse>(`/api/studies/${studyId}`, {})
.then((res) => {
const trials = res.data.trials.map(
(trial): Trial => {
return convertTrialResponse(trial)
}
)
return {
name: res.data.name,
datetime_start: new Date(res.data.datetime_start),
direction: res.data.direction,
best_trial: res.data.best_trial
? convertTrialResponse(res.data.best_trial)
: undefined,
trials: trials,
}
})
}
interface StudySummariesResponse {
study_summaries: {
study_id: number
study_name: string
direction: StudyDirection
best_trial?: {
trial_id: number
study_id: number
number: number
state: TrialState
value?: number
intermediate_values: TrialIntermediateValue[]
datetime_start: string
datetime_complete?: string
params: TrialParam[]
user_attrs: Attribute[]
system_attrs: Attribute[]
}
user_attrs: Attribute[]
system_attrs: Attribute[]
datetime_start: string
}[]
}
export const getStudySummariesAPI = (): Promise<StudySummary[]> => {
return axiosInstance
.get<StudySummariesResponse>(`/api/studies`, {})
.then((res) => {
return res.data.study_summaries.map(
(study): StudySummary => {
const best_trial = study.best_trial
? convertTrialResponse(study.best_trial)
: undefined
return {
study_id: study.study_id,
study_name: study.study_name,
direction: study.direction,
best_trial: best_trial,
user_attrs: study.user_attrs,
system_attrs: study.system_attrs,
datetime_start: new Date(study.datetime_start),
}
}
)
})
}
interface CreateNewStudyResponse {
study_summary: StudySummary
}
export const createNewStudyAPI = (
studyName: string,
direction: StudyDirection
): Promise<StudySummary> => {
return axiosInstance
.post<CreateNewStudyResponse>(`/api/studies`, {
study_name: studyName,
direction,
})
.then((res) => {
const study_summary = res.data.study_summary
return {
study_id: study_summary.study_id,
study_name: study_summary.study_name,
direction: study_summary.direction,
// best_trial: undefined,
user_attrs: study_summary.user_attrs,
system_attrs: study_summary.system_attrs,
datetime_start: new Date(study_summary.datetime_start),
}
})
}
+257
View File
@@ -0,0 +1,257 @@
import React from "react"
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TablePagination,
TableRow,
TableSortLabel,
Collapse,
IconButton,
} from "@material-ui/core"
import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown"
import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp"
type Order = "asc" | "desc"
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: "100%",
},
table: {
minWidth: 750,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
})
)
const defaultInitialRowsPerPage = 10
const defaultRowsPerPageOption = [10, 50, 100, { label: "All", value: -1 }]
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
if (b[orderBy] < a[orderBy]) {
return -1
}
if (b[orderBy] > a[orderBy]) {
return 1
}
return 0
}
interface DataGridColumn<T> {
field: keyof T
label: string
sortable: boolean
toCellValue?: (dataIndex: number) => string | React.ReactNode
}
function DataGrid<T>(props: {
columns: DataGridColumn<T>[]
rows: T[]
keyField: keyof T
dense?: boolean
collapseBody?: (dataIndex: number) => React.ReactNode
initialRowsPerPage?: number
rowsPerPageOption?: Array<number | { value: number; label: string }>
}) {
const classes = useStyles()
const {
columns,
rows,
keyField,
dense,
collapseBody,
initialRowsPerPage,
rowsPerPageOption,
} = props
const [order, setOrder] = React.useState<Order>("asc")
const [orderBy, setOrderBy] = React.useState<keyof T>(keyField)
const [page, setPage] = React.useState(0)
const [rowsPerPage, setRowsPerPage] = React.useState(
initialRowsPerPage || defaultInitialRowsPerPage
)
const handleRequestSort = (
event: React.MouseEvent<unknown>,
property: keyof T
) => {
const isAsc = orderBy === property && order === "asc"
setOrder(isAsc ? "desc" : "asc")
setOrderBy(property)
}
const createSortHandler = (property: keyof T) => (
event: React.MouseEvent<unknown>
) => {
handleRequestSort(event, property)
}
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage)
}
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setRowsPerPage(parseInt(event.target.value, 10))
setPage(0)
}
const emptyRows =
rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage)
const sortedRows = stableSort<T>(rows, getComparator(order, orderBy))
const paginateRows =
rowsPerPage > 0
? sortedRows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
: sortedRows
return (
<div className={classes.root}>
<TableContainer>
<Table
className={classes.table}
aria-labelledby="tableTitle"
size={dense ? "small" : "medium"}
aria-label="enhanced table"
>
<TableHead>
<TableRow>
{collapseBody ? <TableCell /> : null}
{columns.map((column, index) => (
<TableCell
key={index}
sortDirection={orderBy === column.field ? order : false}
>
{column.sortable ? (
<TableSortLabel
active={orderBy === column.field}
direction={orderBy === column.field ? order : "asc"}
onClick={createSortHandler(column.field)}
>
{column.label}
{orderBy === column.field ? (
<span className={classes.visuallyHidden}>
{order === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
) : (
column.label
)}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{paginateRows.map((row, index) => (
<DataGridRow<T>
columns={columns}
rowIndex={page * rowsPerPage + index}
row={row}
keyField={keyField}
collapseBody={collapseBody}
key={`data-grid-row-${row[keyField]}`}
/>
))}
{emptyRows > 0 && (
<TableRow style={{ height: (dense ? 33 : 53) * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={rowsPerPageOption || defaultRowsPerPageOption}
component="div"
count={rows.length}
rowsPerPage={rowsPerPage}
page={page}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
/>
</div>
)
}
function DataGridRow<T>(props: {
columns: DataGridColumn<T>[]
rowIndex: number
row: T
keyField: keyof T
collapseBody?: (dataIndex: number) => React.ReactNode
}) {
const { columns, rowIndex, row, keyField, collapseBody } = props
const [open, setOpen] = React.useState(false)
return (
<React.Fragment>
<TableRow hover role="checkbox" tabIndex={-1}>
{collapseBody ? (
<TableCell>
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
) : null}
{columns.map((column) => (
<TableCell key={`${row[keyField]}:${column.field}`}>
{column.toCellValue
? column.toCellValue(rowIndex)
: row[column.field]}
</TableCell>
))}
</TableRow>
{collapseBody ? (
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" unmountOnExit>
{collapseBody(rowIndex)}
</Collapse>
</TableCell>
</TableRow>
) : null}
</React.Fragment>
)
}
function getComparator<T>(
order: Order,
orderBy: keyof T
): (a: T, b: T) => number {
return order === "desc"
? (a, b) => descendingComparator<T>(a, b, orderBy)
: (a, b) => -descendingComparator<T>(a, b, orderBy)
}
function stableSort<T>(array: T[], comparator: (a: T, b: T) => number) {
const stabilizedThis = array.map((el, index) => [el, index] as [T, number])
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0])
if (order !== 0) return order
return a[1] - b[1]
})
return stabilizedThis.map((el) => el[0])
}
export { DataGrid, DataGridColumn }
@@ -0,0 +1,205 @@
import * as plotly from "plotly.js-dist"
import React, { ChangeEvent, FC, useEffect, useState } from "react"
import {
Grid,
FormControl,
FormLabel,
FormControlLabel,
Checkbox,
Switch,
Radio,
RadioGroup,
} from "@material-ui/core"
const plotDomId = "graph-history"
export const GraphHistory: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
const [xAxis, setXAxis] = useState<string>("number")
const [logScale, setLogScale] = useState<boolean>(false)
const [filterCompleteTrial, setFilterCompleteTrial] = useState<boolean>(false)
const [filterPrunedTrial, setFilterPrunedTrial] = useState<boolean>(false)
const handleXAxisChange = (e: ChangeEvent<HTMLInputElement>) => {
setXAxis(e.target.value)
}
const handleLogScaleChange = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
setLogScale(!logScale)
}
const handleFilterCompleteChange = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
setFilterCompleteTrial(!filterCompleteTrial)
}
const handleFilterPrunedChange = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
setFilterPrunedTrial(!filterPrunedTrial)
}
useEffect(() => {
if (study !== null) {
plotHistory(
study,
xAxis,
logScale,
filterCompleteTrial,
filterPrunedTrial
)
}
}, [study, logScale, xAxis, filterPrunedTrial, filterCompleteTrial])
return (
<Grid container direction="row">
<Grid item xs={3}>
<Grid container direction="column">
<FormControl component="fieldset">
<FormLabel component="legend">Log scale:</FormLabel>
<Switch
checked={logScale}
onChange={handleLogScaleChange}
value="enable"
/>
</FormControl>
<FormControl component="fieldset">
<FormLabel component="legend">Filter state:</FormLabel>
<FormControlLabel
control={
<Checkbox
checked={!filterCompleteTrial}
onChange={handleFilterCompleteChange}
/>
}
label="Complete"
/>
<FormControlLabel
control={
<Checkbox
checked={!filterPrunedTrial}
onChange={handleFilterPrunedChange}
/>
}
label="Pruned"
/>
</FormControl>
<FormControl component="fieldset">
<FormLabel component="legend">X-axis:</FormLabel>
<RadioGroup
aria-label="gender"
name="gender1"
value={xAxis}
onChange={handleXAxisChange}
>
<FormControlLabel
value="number"
control={<Radio />}
label="Number"
/>
<FormControlLabel
value="datetime_start"
control={<Radio />}
label="Datetime start"
/>
<FormControlLabel
value="datetime_complete"
control={<Radio />}
label="Datetime complete"
/>
</RadioGroup>
</FormControl>
</Grid>
</Grid>
<Grid item xs={9}>
<div id={plotDomId} />
</Grid>
</Grid>
)
}
const plotHistory = (
study: StudyDetail,
xAxis: string,
logScale: boolean,
filterCompleteTrial: boolean,
filterPrunedTrial: boolean
) => {
if (document.getElementById(plotDomId) === null) {
return
}
const layout: Partial<plotly.Layout> = {
margin: {
l: 50,
t: 0,
r: 50,
b: 0,
},
yaxis: {
type: logScale ? "log" : "linear",
},
xaxis: {
type: xAxis === "number" ? "linear" : "date",
},
showlegend: false,
}
let filteredTrials = study.trials.filter(
(t) => t.state === "Complete" || t.state === "Pruned"
)
if (filterCompleteTrial) {
filteredTrials = filteredTrials.filter((t) => t.state !== "Complete")
}
if (filterPrunedTrial) {
filteredTrials = filteredTrials.filter((t) => t.state !== "Pruned")
}
if (filteredTrials.length === 0) {
plotly.react(plotDomId, [])
return
}
let trialsForLinePlot: Trial[] = []
let currentBest: number | null = null
filteredTrials.forEach((item) => {
if (currentBest === null) {
currentBest = item.value!
trialsForLinePlot.push(item)
} else if (study.direction === "maximize" && item.value! > currentBest) {
currentBest = item.value!
trialsForLinePlot.push(item)
} else if (study.direction === "minimize" && item.value! < currentBest) {
currentBest = item.value!
trialsForLinePlot.push(item)
}
})
const getAxisX = (trial: Trial): number | Date => {
return xAxis === "number"
? trial.number
: xAxis === "datetime_start"
? trial.datetime_start
: trial.datetime_complete!
}
let xForLinePlot = trialsForLinePlot.map(getAxisX)
xForLinePlot.push(getAxisX(filteredTrials[filteredTrials.length - 1]))
let yForLinePlot = trialsForLinePlot.map((t: Trial): number => t.value!)
yForLinePlot.push(yForLinePlot[yForLinePlot.length - 1])
const plotData: Partial<plotly.PlotData>[] = [
{
x: filteredTrials.map(getAxisX),
y: filteredTrials.map((t: Trial): number => t.value!),
mode: "markers",
type: "scatter",
},
{
x: xForLinePlot,
y: yForLinePlot,
mode: "lines",
type: "scatter",
},
]
plotly.react(plotDomId, plotData, layout)
}
@@ -0,0 +1,46 @@
import * as plotly from "plotly.js-dist"
import React, { FC, useEffect } from "react"
const plotDomId = "graph-intermediate-values"
export const GraphIntermediateValues: FC<{
trials: Trial[]
}> = ({ trials = [] }) => {
useEffect(() => {
plotIntermediateValue(trials)
}, [trials])
return <div id={plotDomId} />
}
const plotIntermediateValue = (trials: Trial[]) => {
if (document.getElementById(plotDomId) === null) {
return
}
const layout: Partial<plotly.Layout> = {
title: "Intermediate values",
margin: {
l: 50,
r: 50,
b: 0,
},
}
if (trials.length === 0) {
plotly.react(plotDomId, [], layout)
return
}
let filteredTrials = trials.filter(
(t) => t.state === "Complete" || t.state === "Pruned"
)
const plotData: Partial<plotly.PlotData>[] = filteredTrials.map((trial) => {
return {
x: trial.intermediate_values.map((iv) => iv.step),
y: trial.intermediate_values.map((iv) => iv.value),
mode: "lines+markers",
type: "scatter",
name: `trial #${trial.number}`,
}
})
plotly.react(plotDomId, plotData, layout)
}
@@ -0,0 +1,100 @@
import * as plotly from "plotly.js-dist"
import React, { FC, useEffect } from "react"
const plotDomId = "graph-parallel-coordinate"
export const GraphParallelCoordinate: FC<{
trials: Trial[]
}> = ({ trials = [] }) => {
useEffect(() => {
plotCoordinate(trials)
}, [trials])
return <div id={plotDomId} />
}
const plotCoordinate = (trials: Trial[]) => {
if (document.getElementById(plotDomId) === null) {
return
}
const layout: Partial<plotly.Layout> = {
title: "Parallel coordinate",
margin: {
l: 50,
r: 50,
b: 0,
},
}
if (trials.length === 0) {
plotly.react(plotDomId, [])
return
}
let filteredTrials = trials.filter(
(t) => t.state === "Complete" || t.state === "Pruned"
)
// Intersection param names
let paramNames = new Set<string>(trials[0].params.map((p) => p.name))
filteredTrials.forEach((t) => {
paramNames = new Set<string>(
t.params.filter((p) => paramNames.has(p.name)).map((p) => p.name)
)
})
if (paramNames.size === 0) {
plotly.react(plotDomId, [])
return
}
const objectiveValues: number[] = filteredTrials.map((t) => t.value!)
let dimensions = [
{
label: "Objective value",
values: objectiveValues,
range: [Math.min(...objectiveValues), Math.max(...objectiveValues)],
},
]
paramNames.forEach((paramName) => {
const valueStrings = filteredTrials.map((t) => {
const param = t.params.find((p) => p.name == paramName)
return param!.value
})
const isnum = valueStrings.every((v) => {
return /^-?\d+\.\d+$/.test(v)
})
if (isnum) {
const values: number[] = valueStrings.map((v) => parseFloat(v))
dimensions.push({
label: paramName,
values: values,
range: [Math.min(...values), Math.max(...values)],
})
} else {
// categorical
const vocabSet = new Set<string>(valueStrings)
const vocabArr = Array.from<string>(vocabSet)
const values: number[] = valueStrings.map((v) =>
vocabArr.findIndex((vocab) => v === vocab)
)
const tickvals: number[] = vocabArr.map((v, i) => i)
dimensions.push({
label: paramName,
values: values,
range: [Math.min(...values), Math.max(...values)],
// @ts-ignore
tickvals: tickvals,
ticktext: vocabArr,
})
}
})
const plotData: Partial<plotly.PlotData>[] = [
{
type: "parcoords",
// @ts-ignore
dimensions: dimensions,
},
]
plotly.react(plotDomId, plotData, layout)
}
+201
View File
@@ -0,0 +1,201 @@
import React, { FC, useEffect } from "react"
import { useRecoilValue } from "recoil"
import { Link, useParams } from "react-router-dom"
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"
import {
AppBar,
Card,
Typography,
CardContent,
Container,
Grid,
Toolbar,
Paper,
Box,
IconButton,
} from "@material-ui/core"
import { Home } from "@material-ui/icons"
import { DataGridColumn, DataGrid } from "./dataGrid"
import { GraphParallelCoordinate } from "./graphParallelCoordinate"
import { GraphIntermediateValues } from "./graphIntermediateValues"
import { GraphHistory } from "./graphHistory"
import { Action, actionCreator } from "../action"
import { studyDetailsState } from "../state"
const useStyles = makeStyles((theme: Theme) =>
createStyles({
paper: {
margin: theme.spacing(2),
padding: theme.spacing(2),
},
card: {
margin: theme.spacing(2),
},
grow: {
flexGrow: 1,
},
})
)
interface ParamTypes {
studyId: string
}
export const useStudyDetail = (
action: Action,
studyId: number
): StudyDetail | null => {
const studyDetails = useRecoilValue<StudyDetails>(studyDetailsState)
useEffect(() => {
action.updateStudyDetail(studyId)
const intervalId = setInterval(function () {
action.updateStudyDetail(studyId)
}, 10 * 1000)
return () => clearInterval(intervalId)
}, [])
return studyDetails[studyId] || null
}
export const StudyDetail: FC<{}> = () => {
const classes = useStyles()
const action = actionCreator()
const { studyId } = useParams<ParamTypes>()
const studyIdNumber = parseInt(studyId, 10)
const studyDetail = useStudyDetail(action, studyIdNumber)
const title = studyDetail !== null ? studyDetail.name : `Study #${studyId}`
const trials: Trial[] = studyDetail !== null ? studyDetail.trials : []
return (
<div>
<AppBar position="static">
<Container>
<Toolbar>
<Typography variant="h6">{APP_BAR_TITLE}</Typography>
<div className={classes.grow} />
<IconButton
aria-controls="menu-appbar"
aria-haspopup="true"
component={Link}
to={URL_PREFIX + "/"}
color="inherit"
>
<Home />
</IconButton>
</Toolbar>
</Container>
</AppBar>
<Container>
<div>
<Paper className={classes.paper}>
<Typography variant="h6">{title}</Typography>
</Paper>
<Card className={classes.card}>
<CardContent>
<GraphHistory study={studyDetail} />
</CardContent>
</Card>
<Grid container direction="row">
<Grid item xs={6}>
<Card className={classes.card}>
<CardContent>
<GraphParallelCoordinate trials={trials} />
</CardContent>
</Card>
</Grid>
<Grid item xs={6}>
<Card className={classes.card}>
<CardContent>
<GraphIntermediateValues trials={trials} />
</CardContent>
</Card>
</Grid>
</Grid>
<Card className={classes.card}>
<TrialTable trials={trials} />
</Card>
</div>
</Container>
</div>
)
}
const TrialTable: FC<{ trials: Trial[] }> = ({ trials = [] }) => {
const columns: DataGridColumn<Trial>[] = [
{ field: "number", label: "Number", sortable: true },
{
field: "state",
label: "State",
sortable: false,
toCellValue: (i) => trials[i].state.toString(),
},
{ field: "value", label: "Value", sortable: true },
{
field: "params",
label: "Params",
sortable: false,
toCellValue: (i) =>
trials[i].params.map((p) => p.name + ": " + p.value).join(", "),
},
]
const collapseIntermediateValueColumns: DataGridColumn<
TrialIntermediateValue
>[] = [
{ field: "step", label: "Step", sortable: true },
{ field: "value", label: "Value", sortable: true },
]
const collapseAttrColumns: DataGridColumn<Attribute>[] = [
{ field: "key", label: "Key", sortable: true },
{ field: "value", label: "Value", sortable: true },
]
const collapseBody = (index: number) => {
return (
<Grid container direction="row">
<Grid item xs={6}>
<Box margin={1}>
<Typography variant="h6" gutterBottom component="div">
Intermediate values
</Typography>
<DataGrid<TrialIntermediateValue>
columns={collapseIntermediateValueColumns}
rows={trials[index].intermediate_values}
keyField={"step"}
dense={true}
initialRowsPerPage={5}
rowsPerPageOption={[5, 10, { label: "All", value: -1 }]}
/>
</Box>
</Grid>
<Grid item xs={6}>
<Box margin={1}>
<Typography variant="h6" gutterBottom component="div">
Trial user attributes
</Typography>
<DataGrid<Attribute>
columns={collapseAttrColumns}
rows={trials[index].user_attrs}
keyField={"key"}
dense={true}
initialRowsPerPage={5}
rowsPerPageOption={[5, 10, { label: "All", value: -1 }]}
/>
</Box>
</Grid>
</Grid>
)
}
return (
<DataGrid<Trial>
columns={columns}
rows={trials}
keyField={"trial_id"}
dense={true}
collapseBody={collapseBody}
/>
)
}
+235
View File
@@ -0,0 +1,235 @@
import React, { FC, useEffect } from "react"
import { useRecoilValue } from "recoil"
import { Link } from "react-router-dom"
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"
import {
AppBar,
Toolbar,
Typography,
Container,
Card,
Grid,
Box,
Button,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
TextField,
DialogActions,
FormControlLabel,
Checkbox,
} from "@material-ui/core"
import { AddBox, Refresh } from "@material-ui/icons"
import { actionCreator } from "../action"
import { DataGrid, DataGridColumn } from "./dataGrid"
import { studySummariesState } from "../state"
const useStyles = makeStyles((theme: Theme) =>
createStyles({
card: {
margin: theme.spacing(2),
},
grow: {
flexGrow: 1,
},
})
)
export const StudyList: FC<{}> = () => {
const classes = useStyles()
const [openDialog, setOpenDialog] = React.useState(false)
const [newStudyName, setNewStudyName] = React.useState("")
const [maximize, setMaximize] = React.useState<boolean>(false)
const action = actionCreator()
const studies = useRecoilValue<StudySummary[]>(studySummariesState)
const newStudyNameAlreadyUsed = studies.some(
(v) => v.study_name === newStudyName
)
useEffect(() => {
action.updateStudySummaries()
}, [])
const columns: DataGridColumn<StudySummary>[] = [
{
field: "study_id",
label: "Study ID",
sortable: true,
},
{
field: "study_name",
label: "Name",
sortable: true,
toCellValue: (i) => (
<Link to={`${URL_PREFIX}/studies/${studies[i].study_id}`}>
{studies[i].study_name}
</Link>
),
},
{
field: "direction",
label: "Direction",
sortable: false,
toCellValue: (i) => studies[i].direction.toString(),
},
{
field: "best_trial",
label: "Best value",
sortable: false,
toCellValue: (i) => studies[i].best_trial?.value || null,
},
]
const collapseAttrColumns: DataGridColumn<Attribute>[] = [
{ field: "key", label: "Key", sortable: true },
{ field: "value", label: "Value", sortable: true },
]
const handleCloseNewStudyDialog = () => {
setNewStudyName("")
setOpenDialog(false)
}
const handleCreateNewStudy = () => {
const direction = maximize ? "maximize" : "minimize"
action.createNewStudy(newStudyName, direction)
setOpenDialog(false)
setNewStudyName("")
}
const collapseBody = (index: number) => {
return (
<Grid container direction="row">
<Grid item xs={6}>
<Box margin={1}>
<Typography variant="h6" gutterBottom component="div">
Study user attributes
</Typography>
<DataGrid<Attribute>
columns={collapseAttrColumns}
rows={studies[index].user_attrs}
keyField={"key"}
dense={true}
initialRowsPerPage={5}
rowsPerPageOption={[5, 10, { label: "All", value: -1 }]}
/>
</Box>
</Grid>
<Grid item xs={6}>
<Box margin={1}>
<Typography variant="h6" gutterBottom component="div">
Study system attributes
</Typography>
<DataGrid<Attribute>
columns={collapseAttrColumns}
rows={studies[index].system_attrs}
keyField={"key"}
dense={true}
initialRowsPerPage={5}
rowsPerPageOption={[5, 10, { label: "All", value: -1 }]}
/>
</Box>
</Grid>
</Grid>
)
}
return (
<div>
<AppBar position="static">
<Container>
<Toolbar>
<Typography variant="h6">{APP_BAR_TITLE}</Typography>
<div className={classes.grow} />
<IconButton
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={(e) => {
action.updateStudySummaries("Success to reload")
}}
color="inherit"
>
<Refresh />
</IconButton>
<IconButton
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={(e) => {
setOpenDialog(true)
}}
color="inherit"
>
<AddBox />
</IconButton>
</Toolbar>
</Container>
</AppBar>
<Container>
<Card className={classes.card}>
<DataGrid<StudySummary>
columns={columns}
rows={studies}
keyField={"study_id"}
collapseBody={collapseBody}
initialRowsPerPage={-1}
rowsPerPageOption={[5, 10, { label: "All", value: -1 }]}
/>
</Card>
</Container>
<Dialog
open={openDialog}
onClose={(e) => handleCloseNewStudyDialog}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">New study</DialogTitle>
<DialogContent>
<DialogContentText>
To create a new study, please enter the study name here.
</DialogContentText>
<TextField
autoFocus
error={newStudyNameAlreadyUsed}
helperText={
newStudyNameAlreadyUsed ? `"${newStudyName}" is already used` : ""
}
label="Study name"
type="text"
onChange={(e) => {
setNewStudyName(e.target.value)
}}
fullWidth
/>
<FormControlLabel
control={
<Checkbox
checked={maximize}
onChange={(e) => {
setMaximize(!maximize)
}}
color="primary"
/>
}
label="Set maximize direction"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseNewStudyDialog} color="primary">
Cancel
</Button>
<Button
onClick={handleCreateNewStudy}
color="primary"
disabled={newStudyName === "" || newStudyNameAlreadyUsed}
>
Create
</Button>
</DialogActions>
</Dialog>
</div>
)
}
+11
View File
@@ -0,0 +1,11 @@
export const formatDate = (date: Date): string => {
const options = {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
}
return new Intl.DateTimeFormat("ja-JP", options).format(date)
}
+33
View File
@@ -0,0 +1,33 @@
import React, { FC } from "react"
import { render } from "react-dom"
import { RecoilRoot } from "recoil"
import { BrowserRouter as Router, Switch, Route } from "react-router-dom"
import { SnackbarProvider } from "notistack"
import { StudyDetail } from "./components/studyDetail"
import { StudyList } from "./components/studyList"
const DashboardApp: FC<{}> = () => {
return (
<RecoilRoot>
<SnackbarProvider maxSnack={3}>
<Router>
<Switch>
<Route
path={URL_PREFIX + "/studies/:studyId"}
children={<StudyDetail />}
/>
<Route path={URL_PREFIX + "/"} children={<StudyList />} />
</Switch>
</Router>
</SnackbarProvider>
</RecoilRoot>
)
}
render(
<React.StrictMode>
<DashboardApp />
</React.StrictMode>,
document.getElementById("dashboard")
)
+11
View File
@@ -0,0 +1,11 @@
import { atom } from "recoil"
export const studySummariesState = atom<StudySummary[]>({
key: "studySummaries",
default: [],
})
export const studyDetailsState = atom<StudyDetails>({
key: "studyDetails",
default: {},
})
+62
View File
@@ -0,0 +1,62 @@
declare module "*.css"
declare module "*.png"
declare module "*.jpg"
declare module "*.svg"
declare const APP_BAR_TITLE: string
declare const API_ENDPOINT: string
declare const URL_PREFIX: string
type TrialState = "Running" | "Complete" | "Pruned" | "Fail" | "Waiting"
type StudyDirection = "maximize" | "minimize"
declare interface TrialIntermediateValue {
step: number
value: number
}
declare interface TrialParam {
name: string
value: string
}
declare interface Attribute {
key: string
value: string
}
declare interface Trial {
trial_id: number
study_id: number
number: number
state: TrialState
value?: number
intermediate_values: TrialIntermediateValue[]
datetime_start: Date
datetime_complete?: Date
params: TrialParam[]
user_attrs: Attribute[]
system_attrs: Attribute[]
}
declare interface StudySummary {
study_id: number
study_name: string
direction: StudyDirection
best_trial?: Trial
user_attrs: Attribute[]
system_attrs: Attribute[]
datetime_start: Date
}
declare interface StudyDetail {
name: string
direction: StudyDirection
datetime_start: Date
best_trial?: Trial
trials: Trial[]
}
declare interface StudyDetails {
[study_id: string]: StudyDetail
}
+2424
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
{
"name": "goptuna-dashboard",
"private": true,
"version": "0.0.1",
"description": "Dashboard for Goptuna",
"main": "index.js",
"scripts": {
"fmt": "prettier --write \"dashboard/static/**/*.{ts,tsx}\"",
"watch": "webpack --watch",
"build": "webpack",
"build:dev": "NODE_ENV=development webpack",
"build:prd": "NODE_ENV=production webpack"
},
"author": "Masashi Shibata",
"license": "MIT",
"dependencies": {
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"axios": "^0.20.0",
"dayjs": "^1.9.3",
"notistack": "^1.0.1",
"plotly.js-dist": "^1.57.0",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-router-dom": "^5.2.0",
"recoil": "0.0.13"
},
"devDependencies": {
"@babel/core": "^7.12.3",
"@types/plotly.js": "^1.50.22",
"@types/react": "^16.9.53",
"@types/react-dom": "^16.9.8",
"@types/react-router-dom": "^5.1.6",
"autoprefixer": "^10.0.1",
"css-loader": "^5.0.0",
"file-loader": "^6.1.1",
"prettier": "^2.1.2",
"style-loader": "^2.0.0",
"ts-loader": "^8.0.6",
"typescript": "^4.0.3",
"url-loader": "^4.1.1",
"webpack": "^5.1.3",
"webpack-cli": "^4.1.0"
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"compilerOptions": {
"baseUrl": ".",
"strictNullChecks": true,
"moduleResolution": "node",
"noUnusedLocals": true,
"noImplicitThis": true,
"alwaysStrict": true,
"outDir": "./dashboard/public/",
"paths": {
"plotly.js-dist": ["node_modules/@types/plotly.js"]
},
"noImplicitAny": true,
"lib": ["dom", "esnext"],
"module": "esnext",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"target": "es5",
"jsx": "react",
"sourceMap": true,
"strict": true
},
"files": [
"./dashboard/static/index.tsx"
],
"include": [
"./dashboard/static/types/**/*"
],
"types": ["node"]
}
+87
View File
@@ -0,0 +1,87 @@
const webpack = require('webpack');
const externals = [];
var mode = 'development';
if (process.env.NODE_ENV === 'production') {
mode = 'production';
}
const isDev = mode === 'development';
if (isDev) {
const _externals = {};
externals.push(_externals);
}
var config = {
mode,
entry: [__dirname + '/dashboard/static/index.tsx'],
output: {
path: __dirname + '/dashboard/public/',
filename: 'bundle.js',
publicPath: '/public/'
},
module: {
rules: [{
oneOf: [
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
limit: 10000,
name: 'media/[name].[hash:8].[ext]',
publicPath: '/public/'
},
},
{
test: /\.tsx?$/,
exclude: [/node_modules/],
loader: 'ts-loader',
options: {
configFile: __dirname + '/tsconfig.json'
}
},
{
test: /\.p?css$/,
use: [
'style-loader',
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
sourceMap: isDev
},
}
]
},
{
exclude: [/\.(ts|tsx|js)$/, /\.html$/, /\.json$/],
loader: require.resolve('file-loader'),
options: {
name: 'media/[name].[hash:8].[ext]',
publicPath: '/public/'
}
}
]}]
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'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.API_ENDPOINT || "/dashboard")
})
],
externals
};
if (isDev) {
config.devtool = 'source-map';
console.log('= = = = = = = = = = = = = = = = = = =');
console.log('DEVELOPMENT BUILD');
console.log('= = = = = = = = = = = = = = = = = = =');
}
module.exports = config;