Add standalone app

This commit is contained in:
c-bata
2023-05-17 01:22:33 +09:00
parent ab00a300bd
commit 9394ef175a
24 changed files with 8057 additions and 3 deletions
+2 -1
View File
@@ -1 +1,2 @@
optuna_dashboard/ts/components/PlotlyDarkMode.ts
optuna_dashboard/ts/components/PlotlyDarkMode.ts
standalone_app/PlotlyDarkMode.ts
+4 -2
View File
@@ -5,8 +5,10 @@
"description": "Dashboard for Optuna",
"main": "index.js",
"scripts": {
"fmt": "prettier --write \"optuna_dashboard/ts/**/*.{ts,tsx}\" \"typescript_tests/*.{ts,tsx}\"",
"lint": "eslint . --ext .ts,.tsx && prettier --list-different \"optuna_dashboard/ts/**/*.{ts,tsx}\" \"typescript_tests/*.{ts,tsx}\"",
"fmt": "prettier --write \"optuna_dashboard/ts/**/*.{ts,tsx}\" \"typescript_tests/*.{ts,tsx}\" \"standalone_app/*.{ts,tsx}\"",
"lint": "npm run lint:eslint && npm run lint:fmt",
"lint:eslint": "eslint . --ext .ts,.tsx",
"lint:fmt": "prettier --list-different \"optuna_dashboard/ts/**/*.{ts,tsx}\" \"typescript_tests/*.{ts,tsx}\" \"standalone_app/*.{ts,tsx}\"",
"watch": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack --watch",
"build": "webpack",
"build:dev": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack",
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Optuna Dashboard (Wasm ver.)</title>
<script defer type="module" src="/bundle.js"></script>
<link rel="icon" href="/favicon.ico" />
</head>
<body>
<div id="root"></div>
</body>
</html>
+6087
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
{
"name": "optuna-dashboard-wasm",
"private": true,
"version": "0.0.0",
"description": "",
"scripts": {
"watch": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack --watch",
"serve": "python3 -m http.server 9000 --directory ./public",
"build:dev": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack",
"build:prd": "NODE_ENV=production webpack"
},
"devDependencies": {
"@types/plotly.js": "^2.12.18",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.0.0",
"esbuild-loader": "^3.0.1",
"prettier": "^2.8.8",
"ts-loader": "^9.4.2",
"typescript": "^5.0.4",
"vite": "^4.3.2",
"webpack": "^5.82.1",
"webpack-cli": "^5.1.1"
},
"dependencies": {
"@emotion/react": "^11.10.8",
"@emotion/styled": "^11.10.8",
"@mui/icons-material": "^5.11.16",
"@mui/lab": "^5.0.0-alpha.128",
"@mui/material": "^5.12.2",
"@sqlite.org/sqlite-wasm": "^3.41.2-build11",
"module-workers-polyfill": "^0.3.2",
"notistack": "^3.0.1",
"plotly.js-dist-min": "^2.22.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.11.0",
"recoil": "^0.7.7",
"optuna": "../rustlib/pkg"
}
}
File diff suppressed because one or more lines are too long
+13
View File
@@ -0,0 +1,13 @@
import React from "react"
import ReactDOM from "react-dom/client"
import "./index.css"
import { App } from "./components/App"
import { RecoilRoot } from "recoil"
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>
)
+61
View File
@@ -0,0 +1,61 @@
import React, { FC, useMemo, useState, useEffect } from "react"
import { HashRouter as Router, Routes, Route } from "react-router-dom"
import { SnackbarProvider } from "notistack"
import blue from "@mui/material/colors/blue"
import pink from "@mui/material/colors/pink"
import {
createTheme,
useMediaQuery,
ThemeProvider,
Box,
CssBaseline,
} from "@mui/material"
import { StudyDetail } from "./StudyDetail"
import { StudyList } from "./StudyList"
export const App: FC = () => {
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)")
const [colorMode, setColorMode] = useState<"light" | "dark">("light")
const theme = useMemo(
() =>
createTheme({
palette: {
mode: colorMode,
primary: blue,
secondary: pink,
},
}),
[colorMode]
)
useEffect(() => {
setColorMode(prefersDarkMode ? "dark" : "light")
}, [prefersDarkMode])
const toggleColorMode = () => {
setColorMode(colorMode === "dark" ? "light" : "dark")
}
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box>
<SnackbarProvider maxSnack={3}>
<Router>
<Routes>
<Route
path=""
element={<StudyList toggleColorMode={toggleColorMode} />}
/>
<Route
path=":idx"
element={<StudyDetail toggleColorMode={toggleColorMode} />}
/>
</Routes>
</Router>
</SnackbarProvider>
</Box>
</ThemeProvider>
)
}
+376
View File
@@ -0,0 +1,376 @@
import React from "react"
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TablePagination,
TableRow,
TableSortLabel,
Collapse,
IconButton,
useTheme,
} from "@mui/material"
import { styled } from "@mui/system"
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"
import { Clear } from "@mui/icons-material"
type Order = "asc" | "desc"
const defaultRowsPerPageOption = [10, 50, 100, { label: "All", value: -1 }]
interface DataGridColumn<T> {
field: keyof T
label: string
sortable?: boolean
less?: (a: T, b: T) => number
filterable?: boolean
toCellValue?: (rowIndex: number) => string | React.ReactNode
padding?: "normal" | "checkbox" | "none"
}
interface RowFilter<T> {
columnIdx: number
value: any
}
function DataGrid<T>(props: {
columns: DataGridColumn<T>[]
rows: T[]
keyField: keyof T
dense?: boolean
collapseBody?: (rowIndex: number) => React.ReactNode
initialRowsPerPage?: number
rowsPerPageOption?: Array<number | { value: number; label: string }>
defaultFilter?: (row: T) => boolean
}) {
const { columns, rows, keyField, dense, collapseBody, defaultFilter } = props
let { initialRowsPerPage, rowsPerPageOption } = props
const [order, setOrder] = React.useState<Order>("asc")
const [orderBy, setOrderBy] = React.useState<number>(0) // index of columns
const [page, setPage] = React.useState(0)
const [filters, setFilters] = React.useState<RowFilter<T>[]>([])
const getRowIndex = (row: T): number => {
return rows.findIndex((row2) => row[keyField] === row2[keyField])
}
// Pagination
rowsPerPageOption = rowsPerPageOption || defaultRowsPerPageOption
initialRowsPerPage = initialRowsPerPage // use first element as default
? initialRowsPerPage
: isNumber(rowsPerPageOption[0])
? rowsPerPageOption[0]
: rowsPerPageOption[0].value
const [rowsPerPage, setRowsPerPage] = React.useState(initialRowsPerPage)
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage)
}
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setRowsPerPage(parseInt(event.target.value, 10))
setPage(0)
}
// Filtering
const fieldAlreadyFiltered = (columnIdx: number): boolean =>
filters.some((f) => f.columnIdx === columnIdx)
const handleClickFilterCell = (columnIdx: number, value: any) => {
if (fieldAlreadyFiltered(columnIdx)) {
return
}
const newFilters = [...filters, { columnIdx: columnIdx, value: value }]
setFilters(newFilters)
}
const clearFilter = (columnIdx: number): void => {
setFilters(filters.filter((f) => f.columnIdx !== columnIdx))
}
const filteredRows = rows.filter((row, rowIdx) => {
if (defaultFilter !== undefined && defaultFilter(row)) {
return false
}
return filters.length === 0
? true
: filters.some((f) => {
if (columns.length <= f.columnIdx) {
console.log(
`columnIdx=${f.columnIdx} must be smaller than columns.length=${columns.length}`
)
return true
}
const toCellValue = columns[f.columnIdx].toCellValue
if (toCellValue !== undefined) {
return toCellValue(rowIdx) === f.value
}
const field = columns[f.columnIdx].field
return row[field] === f.value
})
})
// Sorting
const createSortHandler =
(columnId: number) => (event: React.MouseEvent<unknown>) => {
const isAsc = orderBy === columnId && order === "asc"
setOrder(isAsc ? "desc" : "asc")
setOrderBy(columnId)
}
const sortedRows = stableSort<T>(filteredRows, order, orderBy, columns)
const currentPageRows =
rowsPerPage > 0
? sortedRows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
: sortedRows
const emptyRows =
rowsPerPage - Math.min(rowsPerPage, sortedRows.length - page * rowsPerPage)
const RootDiv = styled("div")({
width: "100%",
})
const HiddenSpan = styled("span")({
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
})
const TableHeaderCellSpan = styled("span")({
display: "inline-flex",
})
return (
<RootDiv>
<TableContainer>
<Table
aria-labelledby="tableTitle"
size={dense ? "small" : "medium"}
aria-label="data grid"
>
<TableHead>
<TableRow>
{collapseBody ? <TableCell /> : null}
{columns.map((column, columnIdx) => (
<TableCell
key={columnIdx}
padding={column.padding || "normal"}
sortDirection={orderBy === column.field ? order : false}
>
<TableHeaderCellSpan>
{column.sortable ? (
<TableSortLabel
active={orderBy === columnIdx}
direction={orderBy === columnIdx ? order : "asc"}
onClick={createSortHandler(columnIdx)}
>
{column.label}
{orderBy === column.field ? (
<HiddenSpan>
{order === "desc"
? "sorted descending"
: "sorted ascending"}
</HiddenSpan>
) : null}
</TableSortLabel>
) : (
column.label
)}
{column.filterable ? (
<IconButton
size={dense ? "small" : "medium"}
style={
fieldAlreadyFiltered(columnIdx)
? {}
: { visibility: "hidden" }
}
color="inherit"
onClick={(e) => {
clearFilter(columnIdx)
}}
>
<Clear />
</IconButton>
) : null}
</TableHeaderCellSpan>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{currentPageRows.map((row, index) => (
<DataGridRow<T>
columns={columns}
rowIndex={getRowIndex(row)}
row={row}
keyField={keyField}
collapseBody={collapseBody}
key={`${row[keyField]}`}
handleClickFilterCell={handleClickFilterCell}
/>
))}
{emptyRows > 0 && (
<TableRow style={{ height: (dense ? 33 : 53) * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={rowsPerPageOption}
component="div"
count={filteredRows.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
</RootDiv>
)
}
function DataGridRow<T>(props: {
columns: DataGridColumn<T>[]
rowIndex: number
row: T
keyField: keyof T
collapseBody?: (rowIndex: number) => React.ReactNode
handleClickFilterCell: (columnIdx: number, value: any) => void
}) {
const {
columns,
rowIndex,
row,
keyField,
collapseBody,
handleClickFilterCell,
} = props
const [open, setOpen] = React.useState(false)
const theme = useTheme()
const FilterableDiv = styled("div")({
color: theme.palette.primary.main,
textDecoration: "underline",
cursor: "pointer",
})
return (
<React.Fragment>
<TableRow hover tabIndex={-1}>
{collapseBody ? (
<TableCell>
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
) : null}
{columns.map((column, columnIndex) => {
const cellItem = column.toCellValue
? column.toCellValue(rowIndex)
: // TODO(c-bata): Avoid this implicit type conversion.
(row[column.field] as number | string | null | undefined)
return column.filterable ? (
<TableCell
key={`${row[keyField]}:${column.field.toString()}:${columnIndex}`}
padding={column.padding || "normal"}
onClick={(e) => {
const value =
column.toCellValue !== undefined
? column.toCellValue(rowIndex)
: row[column.field]
handleClickFilterCell(columnIndex, value)
}}
>
<FilterableDiv>{cellItem}</FilterableDiv>
</TableCell>
) : (
<TableCell
key={`${row[keyField]}:${column.field.toString()}:${columnIndex}`}
padding={column.padding || "normal"}
>
{cellItem}
</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,
columns: DataGridColumn<T>[],
orderBy: number
): (a: T, b: T) => number {
return order === "desc"
? (a, b) => descendingComparator<T>(a, b, columns, orderBy)
: (a, b) => -descendingComparator<T>(a, b, columns, orderBy)
}
function descendingComparator<T>(
a: T,
b: T,
columns: DataGridColumn<T>[],
orderBy: number
): number {
const field = columns[orderBy].field
if (b[field] < a[field]) {
return -1
}
if (b[field] > a[field]) {
return 1
}
return 0
}
function stableSort<T>(
array: T[],
order: Order,
orderBy: number,
columns: DataGridColumn<T>[]
) {
// TODO(c-bata): Refactor here by implementing as the same comparator interface.
const less = columns[orderBy].less
const comparator = getComparator(order, columns, orderBy)
const stabilizedThis = array.map((el, index) => [el, index] as [T, number])
stabilizedThis.sort((a, b) => {
if (less) {
const result = order == "asc" ? -less(a[0], b[0]) : less(a[0], b[0])
if (result !== 0) return result
} else {
const result = comparator(a[0], b[0])
if (result !== 0) return result
}
return a[1] - b[1]
})
return stabilizedThis.map((el) => el[0])
}
const isNumber = (
rowsPerPage: number | { value: number; label: string }
): rowsPerPage is number => {
return typeof rowsPerPage === "number"
}
export { DataGrid, DataGridColumn }
@@ -0,0 +1,28 @@
import React, { FC, useEffect } from "react"
import { TextField, TextFieldProps } from "@mui/material"
export const DebouncedInputTextField: FC<{
onChange: (s: string, valid: boolean) => void
delay: number
textFieldProps: TextFieldProps
}> = ({ onChange, delay, textFieldProps }) => {
const [text, setText] = React.useState<string>("")
const [valid, setValidity] = React.useState<boolean>(true)
useEffect(() => {
const timer = setTimeout(() => {
onChange(text, valid)
}, delay)
return () => {
clearTimeout(timer)
}
}, [text, delay])
return (
<TextField
onChange={(e) => {
setText(e.target.value)
setValidity(e.target.validity.valid)
}}
{...textFieldProps}
/>
)
}
@@ -0,0 +1,295 @@
import * as plotly from "plotly.js-dist-min"
import React, { ChangeEvent, FC, useEffect, useState } from "react"
import {
Grid,
FormControl,
FormLabel,
FormControlLabel,
Checkbox,
MenuItem,
Switch,
Select,
Radio,
RadioGroup,
Typography,
SelectChangeEvent,
useTheme,
} from "@mui/material"
import { plotlyDarkTemplate } from "../PlotlyDarkMode"
const plotDomId = "plot-history"
export const PlotHistory: FC<{
study: Study | null
}> = ({ study = null }) => {
const theme = useTheme()
const [xAxis, setXAxis] = useState<string>("number")
const [objectiveId, setObjectiveId] = useState<number>(0)
const [logScale, setLogScale] = useState<boolean>(false)
const [filterCompleteTrial, setFilterCompleteTrial] = useState<boolean>(false)
const [filterPrunedTrial, setFilterPrunedTrial] = useState<boolean>(false)
const handleObjectiveChange = (event: SelectChangeEvent<number>) => {
setObjectiveId(event.target.value as number)
}
const handleXAxisChange = (e: ChangeEvent<HTMLInputElement>) => {
setXAxis(e.target.value)
}
const handleLogScaleChange = (e: ChangeEvent<HTMLInputElement>) => {
setLogScale(!logScale)
}
const handleFilterCompleteChange = (e: ChangeEvent<HTMLInputElement>) => {
setFilterCompleteTrial(!filterCompleteTrial)
}
const handleFilterPrunedChange = (e: ChangeEvent<HTMLInputElement>) => {
setFilterPrunedTrial(!filterPrunedTrial)
}
useEffect(() => {
if (study !== null) {
plotHistory(
study,
objectiveId,
xAxis,
logScale,
filterCompleteTrial,
filterPrunedTrial,
theme.palette.mode
)
}
}, [
study,
objectiveId,
logScale,
xAxis,
filterPrunedTrial,
filterCompleteTrial,
theme.palette.mode,
])
return (
<Grid container direction="row">
<Grid
item
xs={3}
container
direction="column"
sx={{ paddingRight: theme.spacing(2) }}
>
<Typography variant="h6" sx={{ margin: "1em 0", fontWeight: 600 }}>
History
</Typography>
{study !== null && study.directions.length !== 1 ? (
<FormControl
component="fieldset"
sx={{ marginBottom: theme.spacing(2) }}
>
<FormLabel component="legend">Objective ID:</FormLabel>
<Select value={objectiveId} onChange={handleObjectiveChange}>
{study.directions.map((d, i) => (
<MenuItem value={i} key={i}>
{i}
</MenuItem>
))}
</Select>
</FormControl>
) : null}
<FormControl
component="fieldset"
sx={{ marginBottom: theme.spacing(2) }}
>
<FormLabel component="legend">Log y scale:</FormLabel>
<Switch
checked={logScale}
onChange={handleLogScaleChange}
value="enable"
/>
</FormControl>
<FormControl
component="fieldset"
sx={{ marginBottom: theme.spacing(2) }}
>
<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"
sx={{ marginBottom: theme.spacing(2) }}
>
<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 item xs={9}>
<div id={plotDomId} />
</Grid>
</Grid>
)
}
const filterFunc = (trial: Trial, objectiveId: number): boolean => {
if (trial.state !== "Complete" && trial.state !== "Pruned") {
return false
}
if (trial.values === undefined) {
return false
}
return (
trial.values.length > objectiveId &&
trial.values[objectiveId] !== "inf" &&
trial.values[objectiveId] !== "-inf"
)
}
const plotHistory = (
study: Study,
objectiveId: number,
xAxis: string,
logScale: boolean,
filterCompleteTrial: boolean,
filterPrunedTrial: boolean,
mode: string
) => {
if (document.getElementById(plotDomId) === null) {
return
}
const layout: Partial<plotly.Layout> = {
margin: {
l: 50,
t: 0,
r: 50,
b: 0,
},
yaxis: {
title: "Objective Value",
type: logScale ? "log" : "linear",
},
xaxis: {
title: xAxis === "number" ? "Trial" : "Time",
type: xAxis === "number" ? "linear" : "date",
},
showlegend: true,
template: mode === "dark" ? plotlyDarkTemplate : {},
}
let filteredTrials = study.trials.filter((t) => filterFunc(t, objectiveId))
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, [], layout)
return
}
const getAxisX = (trial: Trial): number | Date => {
return xAxis === "number"
? trial.number
: xAxis === "datetime_start"
? trial.datetime_start!
: trial.datetime_complete!
}
const xForLinePlot: (number | Date)[] = []
const yForLinePlot: number[] = []
let currentBest: number | null = null
for (let i = 0; i < filteredTrials.length; i++) {
const t = filteredTrials[i]
if (currentBest === null) {
currentBest = t.values![objectiveId] as number
xForLinePlot.push(getAxisX(t))
yForLinePlot.push(t.values![objectiveId] as number)
} else if (
study.directions[objectiveId] === "maximize" &&
t.values![objectiveId] > currentBest
) {
const p = filteredTrials[i - 1]
if (!xForLinePlot.includes(getAxisX(p))) {
xForLinePlot.push(getAxisX(p))
yForLinePlot.push(currentBest)
}
currentBest = t.values![objectiveId] as number
xForLinePlot.push(getAxisX(t))
yForLinePlot.push(t.values![objectiveId] as number)
} else if (
study.directions[objectiveId] === "minimize" &&
t.values![objectiveId] < currentBest
) {
const p = filteredTrials[i - 1]
if (!xForLinePlot.includes(getAxisX(p))) {
xForLinePlot.push(getAxisX(p))
yForLinePlot.push(currentBest)
}
currentBest = t.values![objectiveId] as number
xForLinePlot.push(getAxisX(t))
yForLinePlot.push(t.values![objectiveId] as number)
}
}
xForLinePlot.push(getAxisX(filteredTrials[filteredTrials.length - 1]))
yForLinePlot.push(yForLinePlot[yForLinePlot.length - 1])
const plotData: Partial<plotly.PlotData>[] = [
{
x: filteredTrials.map(getAxisX),
y: filteredTrials.map(
(t: Trial): number => t.values![objectiveId] as number
),
name: "Objective Value",
mode: "markers",
type: "scatter",
},
{
x: xForLinePlot,
y: yForLinePlot,
name: "Best Value",
mode: "lines",
type: "scatter",
},
]
plotly.react(plotDomId, plotData, layout)
}
@@ -0,0 +1,139 @@
import * as plotly from "plotly.js-dist-min"
import React, { FC, useEffect, useState } from "react"
import { Typography, useTheme, Box, Card, CardContent } from "@mui/material"
import init, { wasm_fanova_calculate } from "optuna"
import { plotlyDarkTemplate } from "../PlotlyDarkMode"
const plotDomId = "graph-hyperparameter-importances"
export const PlotImportance: FC<{ study: Study }> = ({ study }) => {
const theme = useTheme()
const nObjectives = study.directions.length
const objectiveNames: string[] = study.directions.map(
(d, i) => `Objective ${i}`
)
const [importance, setImportance] = useState<ParamImportance[][]>([])
useEffect(() => {
async function run_wasm() {
await init()
const x: ParamImportance[][] = study.directions.map((d, objectiveId) => {
let filteredTrials = study.trials.filter((t) =>
filterFunc(t, objectiveId)
)
if (filteredTrials.length === 0) {
return study.union_search_space.map((s) => {
return {
name: s.name,
importance: 0.5,
}
})
}
const features = study.intersection_search_space.map((s) =>
filteredTrials
.map((t) => t.params.find((p) => p.name === s.name) as TrialParam)
.map((p) => p.param_internal_value)
)
const values = filteredTrials.map(
(t) => t.values?.[objectiveId] as number
)
const importance = wasm_fanova_calculate(features, values)
return study.intersection_search_space.map((s, i) => ({
name: s.name,
importance: importance[i],
}))
})
setImportance(x)
}
run_wasm()
}, [])
useEffect(() => {
if (importance.length > 0) {
plotParamImportancesBeta(importance, objectiveNames, theme.palette.mode)
}
}, [nObjectives, importance, theme.palette.mode])
return (
<Card>
<CardContent>
<Typography
variant="h6"
sx={{ margin: "1em 0", fontWeight: theme.typography.fontWeightBold }}
>
Hyperparameter Importance
</Typography>
<Box id={plotDomId} sx={{ height: "450px" }} />
</CardContent>
</Card>
)
}
const filterFunc = (trial: Trial, objectiveId: number): boolean => {
if (trial.state !== "Complete" && trial.state !== "Pruned") {
return false
}
if (trial.values === undefined) {
return false
}
return (
trial.values.length > objectiveId &&
trial.values[objectiveId] !== "inf" &&
trial.values[objectiveId] !== "-inf"
)
}
const plotParamImportancesBeta = (
importances: ParamImportance[][],
objectiveNames: string[],
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,
},
barmode: "group",
bargap: 0.15,
bargroupgap: 0.1,
uirevision: "true",
template: mode === "dark" ? plotlyDarkTemplate : {},
}
if (document.getElementById(plotDomId) === null) {
return
}
const traces: Partial<plotly.PlotData>[] = importances.map(
(importance, i) => {
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.importance} <extra></extra>`
)
return {
type: "bar",
orientation: "h",
name: objectiveNames[i],
x: importance_values,
y: param_names,
text: importance_values.map((v) => String(v.toFixed(2))),
textposition: "outside",
hovertemplate: param_hover_templates,
}
}
)
plotly.react(plotDomId, traces, layout)
}
@@ -0,0 +1,117 @@
import React, {
ChangeEvent,
DragEventHandler,
FC,
MouseEventHandler,
useRef,
useState,
} from "react"
import { loadStorage } from "../sqlite3"
import { useSetRecoilState } from "recoil"
import { studiesState } from "../state"
import {
Card,
CardActionArea,
CardContent,
Typography,
useTheme,
} from "@mui/material"
import UploadFileIcon from "@mui/icons-material/UploadFile"
export const StorageLoader: FC<{}> = () => {
const theme = useTheme()
const [dragOver, setDragOver] = useState<boolean>(false)
const setStudies = useSetRecoilState<Study[]>(studiesState)
const inputRef = useRef<HTMLInputElement>(null)
const loadStorageFromFile = (file: File): void => {
const r = new FileReader()
r.addEventListener("load", () => {
const arrayBuffer = r.result as ArrayBuffer | null
if (arrayBuffer !== null) {
loadStorage(arrayBuffer, setStudies)
}
})
r.readAsArrayBuffer(file)
}
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
const f = e.target.files?.[0]
if (!f) {
return
}
loadStorageFromFile(f)
}
const handleClick: MouseEventHandler = (e) => {
if (!inputRef || !inputRef.current) {
return
}
inputRef.current.click()
}
const handleDrop: DragEventHandler = (e) => {
e.stopPropagation()
e.preventDefault()
const file = e.dataTransfer.files?.[0]
setDragOver(false)
if (!file) {
return
}
loadStorageFromFile(file)
}
const handleDragOver: DragEventHandler = (e) => {
e.stopPropagation()
e.preventDefault()
e.dataTransfer.dropEffect = "copy"
setDragOver(true)
}
const handleDragLeave: DragEventHandler = (e) => {
e.stopPropagation()
e.preventDefault()
e.dataTransfer.dropEffect = "copy"
setDragOver(false)
}
return (
<Card
sx={{
margin: theme.spacing(2),
border: dragOver
? `3px dashed ${theme.palette.mode === "dark" ? "white" : "black"}`
: `1px solid ${theme.palette.divider}`,
}}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<CardActionArea onClick={handleClick}>
<CardContent
sx={{
display: "flex",
height: "100%",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
<UploadFileIcon
sx={{ fontSize: 80, marginBottom: theme.spacing(2) }}
/>
<input
type="file"
ref={inputRef}
onChange={handleFileChange}
style={{ display: "none" }}
/>
<Typography>Load an Optuna Storage</Typography>
<Typography
sx={{ textAlign: "center", color: theme.palette.grey.A400 }}
>
Drag your SQLite3 file here or click to browse.
</Typography>
</CardContent>
</CardActionArea>
</Card>
)
}
@@ -0,0 +1,119 @@
import React, { FC } from "react"
import { Link, useParams } from "react-router-dom"
import {
AppBar,
Typography,
Container,
Toolbar,
Box,
IconButton,
useTheme,
Card,
CardContent,
} from "@mui/material"
import { Home } from "@mui/icons-material"
import Brightness4Icon from "@mui/icons-material/Brightness4"
import Brightness7Icon from "@mui/icons-material/Brightness7"
import { useRecoilValue } from "recoil"
import { studiesState } from "../state"
import { TrialTable } from "./TrialTable"
import { PlotHistory } from "./PlotHistory"
import { PlotImportance } from "./PlotImportance"
const useStudyValue = (idx: number): Study | null => {
const studies = useRecoilValue<Study[]>(studiesState)
return studies[idx] || null
}
export const StudyDetail: FC<{
toggleColorMode: () => void
}> = ({ toggleColorMode }) => {
const theme = useTheme()
const { idx } = useParams<{ idx: string }>()
const idxNumber = parseInt(idx || "", 10)
const study = useStudyValue(idxNumber)
return (
<div>
<AppBar position="static">
<Container
sx={{
["@media (min-width: 1280px)"]: {
maxWidth: "100%",
},
}}
>
<Toolbar>
<Typography variant="h6">Optuna Dashboard</Typography>
<Box sx={{ flexGrow: 1 }} />
<IconButton
onClick={() => {
toggleColorMode()
}}
color="inherit"
title={
theme.palette.mode === "dark"
? "Switch to light mode"
: "Switch to dark mode"
}
>
{theme.palette.mode === "dark" ? (
<Brightness7Icon />
) : (
<Brightness4Icon />
)}
</IconButton>
<IconButton
aria-controls="menu-appbar"
aria-haspopup="true"
component={Link}
to={"/"}
color="inherit"
title="Return to the top"
>
<Home />
</IconButton>
</Toolbar>
</Container>
</AppBar>
<Container
sx={{
["@media (min-width: 1280px)"]: {
maxWidth: "100%",
},
}}
>
<div>
<Typography
variant="h4"
sx={{
margin: `${theme.spacing(4)} ${theme.spacing(2)}`,
fontWeight: theme.typography.fontWeightBold,
fontSize: "1.8rem",
...(theme.palette.mode === "dark" && {
color: theme.palette.primary.light,
}),
}}
>
{study?.study_name || "Not Found"}
</Typography>
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<PlotHistory study={study} />
</CardContent>
</Card>
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
{!!study && <PlotImportance study={study} />}
</CardContent>
</Card>
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
{!!study && <TrialTable study={study} initialRowsPerPage={10} />}
</CardContent>
</Card>
</div>
</Container>
</div>
)
}
+189
View File
@@ -0,0 +1,189 @@
import React, { FC, useState } from "react"
import {
AppBar,
Typography,
Container,
Toolbar,
Box,
IconButton,
MenuItem,
useTheme,
Card,
CardContent,
CardActionArea,
TextField,
InputAdornment,
SvgIcon,
} from "@mui/material"
import { styled } from "@mui/system"
import SortIcon from "@mui/icons-material/Sort"
import Brightness4Icon from "@mui/icons-material/Brightness4"
import Brightness7Icon from "@mui/icons-material/Brightness7"
import { useRecoilValue } from "recoil"
import { studiesState } from "../state"
import { Link } from "react-router-dom"
import { DebouncedInputTextField } from "./Debounce"
import { Search } from "@mui/icons-material"
import { StorageLoader } from "./StorageLoader"
export const StudyList: FC<{
toggleColorMode: () => void
}> = ({ toggleColorMode }) => {
const theme = useTheme()
const studies = useRecoilValue<Study[]>(studiesState)
const [studyFilterText, setStudyFilterText] = useState<string>("")
const [sortBy, setSortBy] = useState<"id-asc" | "id-desc">("id-asc")
const studyFilter = (row: Study): boolean => {
const keywords = studyFilterText.split(" ")
return !keywords.every((k) => {
if (k === "") {
return true
}
return row.study_name.indexOf(k) >= 0
})
}
let filteredStudies: Study[] = studies.filter((s) => !studyFilter(s))
if (sortBy === "id-desc") {
filteredStudies = filteredStudies.reverse()
}
const Select = styled(TextField)(({ theme }) => ({
"& .MuiInputBase-input": {
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
},
}))
const sortBySelect = (
<Box
sx={{
position: "relative",
borderRadius: theme.shape.borderRadius,
margin: theme.spacing(0, 2),
}}
>
<Box
sx={{
padding: theme.spacing(0, 2),
height: "100%",
position: "absolute",
pointerEvents: "none",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<SortIcon />
</Box>
<Select
select
value={sortBy}
onChange={(e) => {
setSortBy(e.target.value as "id-asc" | "id-desc")
}}
>
<MenuItem value={"id-asc"}>Sort ascending</MenuItem>
<MenuItem value={"id-desc"}>Sort descending</MenuItem>
</Select>
</Box>
)
return (
<div>
<AppBar position="static">
<Container
sx={{
["@media (min-width: 1280px)"]: {
maxWidth: "100%",
},
}}
>
<Toolbar>
<Typography variant="h6">Optuna Dashboard (Wasm ver.)</Typography>
<Box sx={{ flexGrow: 1 }} />
<IconButton
onClick={() => {
toggleColorMode()
}}
color="inherit"
title={
theme.palette.mode === "dark"
? "Switch to light mode"
: "Switch to dark mode"
}
>
{theme.palette.mode === "dark" ? (
<Brightness7Icon />
) : (
<Brightness4Icon />
)}
</IconButton>
</Toolbar>
</Container>
</AppBar>
<Container
sx={{
["@media (min-width: 1280px)"]: {
maxWidth: "100%",
},
}}
>
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<Box sx={{ display: "flex" }}>
<DebouncedInputTextField
onChange={(s) => {
setStudyFilterText(s)
}}
delay={500}
textFieldProps={{
fullWidth: true,
id: "search-study",
variant: "outlined",
placeholder: "Search study",
sx: { maxWidth: 500 },
InputProps: {
startAdornment: (
<InputAdornment position="start">
<SvgIcon fontSize="small" color="action">
<Search />
</SvgIcon>
</InputAdornment>
),
},
}}
/>
{sortBySelect}
<Box sx={{ flexGrow: 1 }} />
</Box>
</CardContent>
</Card>
<Box sx={{ display: "flex", flexWrap: "wrap" }}>
{filteredStudies.map((study, idx) => (
<Card
key={study.study_id}
sx={{ margin: theme.spacing(2), width: "500px" }}
>
<CardActionArea component={Link} to={`/${idx}`}>
<CardContent>
<Typography variant="h5">
{study.study_id}. {study.study_name}
</Typography>
<Typography
variant="subtitle1"
color="text.secondary"
component="div"
>
{"Direction: " +
study.directions.map((d) => d.toUpperCase()).join(", ")}
</Typography>
</CardContent>
</CardActionArea>
</Card>
))}
</Box>
{!IS_VSCODE && <StorageLoader />}
</Container>
</div>
)
}
@@ -0,0 +1,129 @@
import React, { FC } from "react"
import { DataGridColumn, DataGrid } from "./DataGrid"
export const TrialTable: FC<{
study: Study
initialRowsPerPage?: number
}> = ({ study, initialRowsPerPage }) => {
const trials: Trial[] = study.trials
const columns: DataGridColumn<Trial>[] = [
{ field: "number", label: "Number", sortable: true, padding: "none" },
{
field: "state",
label: "State",
sortable: true,
filterable: true,
padding: "none",
toCellValue: (i) => trials[i].state.toString(),
},
]
study.union_search_space.forEach((s) => {
columns.push({
field: "params",
label: `Param ${s.name}`,
toCellValue: (i) =>
trials[i].params.find((p) => p.name === s.name)?.param_internal_value ||
null,
sortable: true,
filterable: false,
less: (firstEl, secondEl): number => {
const firstVal = firstEl.params.find(
(p) => p.name === s.name
)?.param_internal_value
const secondVal = secondEl.params.find(
(p) => p.name === s.name
)?.param_internal_value
if (firstVal === secondVal) {
return 0
} else if (firstVal && secondVal) {
return firstVal < secondVal ? 1 : -1
} else if (firstVal) {
return -1
} else {
return 1
}
},
})
})
if (study === null || study.directions.length == 1) {
columns.push({
field: "values",
label: "Value",
sortable: true,
less: (firstEl, secondEl): number => {
const firstVal = firstEl.values?.[0]
const secondVal = secondEl.values?.[0]
if (firstVal === secondVal) {
return 0
}
if (firstVal === undefined) {
return -1
} else if (secondVal === undefined) {
return 1
}
if (firstVal === "-inf" || secondVal === "inf") {
return 1
} else if (secondVal === "-inf" || firstVal === "inf") {
return -1
}
return firstVal < secondVal ? 1 : -1
},
toCellValue: (i) => {
if (trials[i].values === undefined) {
return null
}
return trials[i].values?.[0]
},
})
} else {
const objectiveColumns: DataGridColumn<Trial>[] = study.directions.map(
(s, objectiveId) => ({
field: "values",
label: `Objective ${objectiveId}`,
sortable: true,
less: (firstEl, secondEl): number => {
const firstVal = firstEl.values?.[objectiveId]
const secondVal = secondEl.values?.[objectiveId]
if (firstVal === secondVal) {
return 0
}
if (firstVal === undefined) {
return -1
} else if (secondVal === undefined) {
return 1
}
if (firstVal === "-inf" || secondVal === "inf") {
return 1
} else if (secondVal === "-inf" || firstVal === "inf") {
return -1
}
return firstVal < secondVal ? 1 : -1
},
toCellValue: (i) => {
if (trials[i].values === undefined) {
return null
}
return trials[i].values?.[objectiveId]
},
})
)
columns.push(...objectiveColumns)
}
return (
<DataGrid<Trial>
columns={columns}
rows={trials}
keyField={"trial_id"}
dense={false}
initialRowsPerPage={initialRowsPerPage}
/>
)
}
View File
+177
View File
@@ -0,0 +1,177 @@
// @ts-ignore
import sqlite3InitModule from "@sqlite.org/sqlite-wasm"
import { SetterOrUpdater } from "recoil"
export const loadStorage = (
arrayBuffer: ArrayBuffer,
setter: SetterOrUpdater<Study[]>
): void => {
sqlite3InitModule({
print: (...args: any): void => {
console.log(args)
},
printErr: (...args: any): void => {
console.log(args)
},
// @ts-ignore
}).then((sqlite3) => {
const p = sqlite3.wasm.allocFromTypedArray(arrayBuffer)
const db = new sqlite3.oo1.DB()
const rc = sqlite3.capi.sqlite3_deserialize(
// @ts-ignore
db.pointer,
"main",
p,
arrayBuffer.byteLength,
arrayBuffer.byteLength,
sqlite3.capi.SQLITE_DESERIALIZE_FREEONCLOSE
)
db.checkRc(rc)
try {
// Check version_info table
let supported = true
db.exec({
sql: "SELECT schema_version FROM version_info LIMIT 1",
callback: (vals: any[]) => {
if (vals[0] != 12) {
supported = false
}
},
})
if (!supported) {
return
}
// Get studies
let studies: Study[] = []
db.exec({
sql:
"SELECT s.study_id, s.study_name, sd.direction, sd.objective" +
" FROM studies AS s INNER JOIN study_directions AS sd" +
" ON s.study_id = sd.study_id ORDER BY sd.study_direction_id",
callback: (vals: any[]) => {
const study_id = vals[0]
const study_name = vals[1]
const direction: StudyDirection = vals[2].toLowerCase()
const objective = vals[3]
let index = 0
if (objective === 0) {
studies.push({
study_id: study_id,
study_name: study_name,
directions: [direction],
union_search_space: [],
intersection_search_space: [],
user_attrs: [],
system_attrs: [],
trials: [],
})
} else {
index = studies.findIndex((s) => s.study_id === study_id)
studies[index].directions.push(direction)
}
},
})
studies.forEach((s) => {
db.exec({
sql:
"SELECT t.trial_id, t.number, t.study_id, t.state, t.datetime_start, t.datetime_complete," +
" tv.objective, tv.value, tv.value_type" +
" FROM trials AS t LEFT JOIN trial_values AS tv ON tv.trial_id = t.trial_id" +
` WHERE t.study_id = ${s.study_id}` +
" ORDER BY t.number",
callback: (vals: any[]) => {
const state: TrialState =
vals[3] === "COMPLETE"
? "Complete"
: "PRUNED"
? "Pruned"
: "RUNNING"
? "Running"
: "WAITING"
? "Waiting"
: "Fail"
const trial: Trial = {
trial_id: vals[0],
number: vals[1],
study_id: vals[2],
state: state,
params: [],
intermediate_values: [],
user_attrs: [],
system_attrs: [],
}
s.trials.push(trial)
},
})
const union_search_space: SearchSpaceItem[] = []
let intersection_search_space: Set<SearchSpaceItem> = new Set()
s.trials.forEach((trial) => {
const params: TrialParam[] = []
const param_names = new Set<string>()
db.exec({
sql:
"SELECT param_name, param_value" +
` FROM trial_params WHERE trial_id = ${trial.trial_id}`,
callback: (vals: any[]) => {
const param_name = vals[0]
params.push({
name: param_name,
param_internal_value: vals[1],
})
param_names.add(param_name)
if (
union_search_space.findIndex((s) => s.name === param_name) == -1
) {
union_search_space.push({ name: param_name })
}
},
})
if (intersection_search_space.size === 0) {
param_names.forEach((s) => {
intersection_search_space.add({
name: s,
})
})
} else {
intersection_search_space = new Set(
Array.from(intersection_search_space).filter((s) =>
param_names.has(s.name)
)
)
}
trial.params = params
const values: TrialValueNumber[] = []
db.exec({
sql:
"SELECT value, value_type" +
` FROM trial_values WHERE trial_id = ${trial.trial_id}` +
" ORDER BY objective",
callback: (vals: any[]) => {
values.push(
vals[1] === "INF_NEG"
? "-inf"
: vals[1] === "INF_POS"
? "+inf"
: vals[0]
)
},
})
if (s.directions.length === values.length) {
trial.values = values
}
})
s.union_search_space = union_search_space
s.intersection_search_space = Array.from(intersection_search_space)
})
setter((prev) => [...prev, ...studies])
} finally {
db.close()
}
})
}
+6
View File
@@ -0,0 +1,6 @@
import { atom } from "recoil"
export const studiesState = atom<Study[]>({
key: "studies",
default: [],
})
+78
View File
@@ -0,0 +1,78 @@
declare const IS_VSCODE: boolean
type TrialValueNumber = number | "inf" | "-inf"
type TrialIntermediateValueNumber = number | "inf" | "-inf" | "nan"
type TrialState = "Running" | "Complete" | "Pruned" | "Fail" | "Waiting"
type TrialStateFinished = "Complete" | "Fail" | "Pruned"
type StudyDirection = "maximize" | "minimize" | "not_set"
type FloatDistribution = {
type: "FloatDistribution"
low: number
high: number
step: number
log: boolean
}
type IntDistribution = {
type: "IntDistribution"
low: number
high: number
step: number
log: boolean
}
type CategoricalDistribution = {
type: "CategoricalDistribution"
choices: { pytype: string; value: string }[]
}
type Distribution =
| FloatDistribution
| IntDistribution
| CategoricalDistribution
type Attribute = {
key: string
value: string
}
type Study = {
study_id: number
study_name: string
directions: StudyDirection[]
user_attrs: Attribute[]
union_search_space: SearchSpaceItem[]
intersection_search_space: SearchSpaceItem[]
system_attrs: Attribute[]
datetime_start?: Date
trials: Trial[]
}
type Trial = {
trial_id: number
number: number
study_id: number
state: TrialState
values?: TrialValueNumber[]
params: TrialParam[]
intermediate_values: TrialIntermediateValue[]
datetime_start?: Date
datetime_complete?: Date
user_attrs: Attribute[]
system_attrs: Attribute[]
}
type TrialParam = {
name: string
param_internal_value: number
}
type SearchSpaceItem = {
name: string
}
type ParamImportance = {
name: string
importance: number
}
+46
View File
@@ -0,0 +1,46 @@
import React, { FC, useEffect } from "react"
import ReactDOM from "react-dom/client"
import "./index.css"
import { App } from "./components/App"
import { RecoilRoot, useSetRecoilState, SetterOrUpdater } from "recoil"
import { studiesState } from "./state"
import { loadStorage } from "./sqlite3"
export const AppWrapper: FC = () => {
const setStudies = useSetRecoilState<Study[]>(studiesState)
const onceSetStudies: SetterOrUpdater<Study[]> = (
setter: ((currVal: Study) => Study) | Study
): void => {
const studies = setter([])
setStudies(studies)
}
useEffect(() => {
window.addEventListener("message", (event) => {
const message = event.data
switch (message.type) {
case "optunaStorage":
const fileContentBase64 = message.content
const binaryString = atob(fileContentBase64)
const len = binaryString.length
const bytes = new Uint8Array(len)
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
const arrayBuffer = bytes.buffer
loadStorage(arrayBuffer, onceSetStudies)
break
}
})
}, [])
return <App />
}
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RecoilRoot>
<AppWrapper />
</RecoilRoot>
</React.StrictMode>
)
+32
View File
@@ -0,0 +1,32 @@
{
"compilerOptions": {
"isolatedModules": true,
"skipLibCheck": true,
"baseUrl": ".",
"strictNullChecks": true,
"moduleResolution": "node",
"noUnusedLocals": true,
"noImplicitThis": true,
"alwaysStrict": true,
"paths": {
"plotly.js-dist-min": ["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": [
"./src/browser_app_entry.tsx",
"./src/vscode_entry.tsx"
],
"include": [
"./src/types/**/*"
],
"types": ["node"]
}
+97
View File
@@ -0,0 +1,97 @@
const webpack = require('webpack');
const path = require('path');
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
}
}
var config = [
{
mode,
experiments: {
syncWebAssembly: true,
asyncWebAssembly: true,
},
entry: [__dirname + '/src/browser_app_entry.tsx'],
output: {
path: __dirname + '/public/',
filename: 'bundle.js',
publicPath: '/'
},
module: {
rules: [{ oneOf: [typeScriptLoader] }]
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
plugins: [
new webpack.DefinePlugin({'IS_VSCODE': JSON.stringify(false)})
]
},
{
mode,
experiments: {
syncWebAssembly: true,
asyncWebAssembly: true,
},
entry: [__dirname + '/src/vscode_entry.tsx'],
output: {
path: path.resolve(__dirname, '../vscode/assets/'),
filename: 'bundle.js',
publicPath: '/'
},
module: {
rules: [
{ oneOf: [typeScriptLoader] },
{
test: /\.wasm$/,
type: "asset/inline",
},
]
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
plugins: [
new webpack.DefinePlugin({'IS_VSCODE': JSON.stringify(true)})
]
},
];
if (isDev) {
config[0].devtool = 'source-map';
config[0].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[0].plugins.push(new CompressionPlugin())
config[1].plugins.push(new CompressionPlugin())
}
module.exports = config;