Merge pull request #846 from keisuke-umezawa/feature/tanstack-table

Use TanStack Table instead of self-implemented DataGrid
This commit is contained in:
c-bata
2024-05-08 13:43:44 +09:00
committed by GitHub
8 changed files with 497 additions and 557 deletions
+32
View File
@@ -17,6 +17,7 @@
"@react-three/drei": "^9.96.4",
"@react-three/fiber": "^8.15.15",
"@tanstack/react-query": "^5.18.1",
"@tanstack/react-table": "^8.16.0",
"@tanstack/react-virtual": "^3.1.2",
"@types/three": "^0.160.0",
"axios": "^1.6.7",
@@ -3120,6 +3121,25 @@
"react": "^18.0.0"
}
},
"node_modules/@tanstack/react-table": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.16.0.tgz",
"integrity": "sha512-rKRjnt8ostqN2fercRVOIH/dq7MAmOENCMvVlKx6P9Iokhh6woBGnIZEkqsY/vEJf1jN3TqLOb34xQGLVRuhAg==",
"dependencies": {
"@tanstack/table-core": "8.16.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.4.0.tgz",
@@ -3136,6 +3156,18 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.16.0.tgz",
"integrity": "sha512-dCG8vQGk4js5v88/k83tTedWOwjGnIyONrKpHpfmSJB8jwFHl8GSu1sBBxbtACVAPtAQgwNxl0rw1d3RqRM1Tg==",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.4.0.tgz",
+1
View File
@@ -22,6 +22,7 @@
"@react-three/drei": "^9.96.4",
"@react-three/fiber": "^8.15.15",
"@tanstack/react-query": "^5.18.1",
"@tanstack/react-table": "^8.16.0",
"@tanstack/react-virtual": "^3.1.2",
"@types/three": "^0.160.0",
"axios": "^1.6.7",
@@ -61,7 +61,10 @@ export const WaveSurferArtifactViewer: React.FC<WaveSurferArtifactViewerProps> =
}, [wavesurfer])
return (
<Box style={{ width: "100%", display: "flex", flexDirection: "column" }}>
<Box
component="div"
style={{ width: "100%", display: "flex", flexDirection: "column" }}
>
<div ref={containerRef} style={{ minHeight: "120px", width: "100%" }} />
<button onClick={onPlayClick} style={{ marginTop: "1em" }}>
{isPlaying ? "Pause" : "Play"}
+321 -398
View File
@@ -1,11 +1,12 @@
import CheckBoxIcon from "@mui/icons-material/CheckBox"
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"
import FilterListIcon from "@mui/icons-material/FilterList"
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"
import FirstPageIcon from "@mui/icons-material/FirstPage"
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"
import LastPageIcon from "@mui/icons-material/LastPage"
import {
Box,
Collapse,
IconButton,
Menu,
MenuItem,
@@ -18,228 +19,29 @@ import {
TableRow,
TableSortLabel,
TextField,
useTheme,
} from "@mui/material"
import ListItemIcon from "@mui/material/ListItemIcon"
import Paper from "@mui/material/Paper"
import { TablePaginationActionsProps } from "@mui/material/TablePagination/TablePaginationActions"
import { styled } from "@mui/system"
import React from "react"
type Order = "asc" | "desc"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Value = any
const defaultRowsPerPageOption = [10, 50, 100, { label: "All", value: -1 }]
interface DataGridColumn<T> {
field: keyof T
label: string
sortable?: boolean
less?: (a: T, b: T, ascending: boolean) => number
filterChoices?: (string | null)[]
toCellValue?: (rowIndex: number) => string | React.ReactNode
padding?: "normal" | "checkbox" | "none"
}
interface RowFilter {
columnIdx: number
values: Value[]
}
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
}): React.ReactElement {
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[]>([])
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)
}
const PaginationForm: React.FC<{
onPageNumberSubmit: (value: number) => void
maxPageNumber: number
}> = ({ onPageNumberSubmit, maxPageNumber }) => {
// This component is separated from DataGrid to prevent `DataGrid` from re-rendering the page,
// every time any letters are input.
const [specifiedPageText, setSpecifiedPageText] = React.useState("")
const handleSubmitPageNumber = (
event: React.FormEvent<HTMLFormElement>
) => {
event.preventDefault()
const newPageNumber = parseInt(specifiedPageText, 10)
// Page is 0-indexed in `TablePagination`.
onPageNumberSubmit(newPageNumber - 1)
setSpecifiedPageText("") // reset the input field
}
return (
<form onSubmit={handleSubmitPageNumber}>
<TextField
size="small"
label={`Go to Page: n / ${maxPageNumber}`}
value={specifiedPageText}
type="number"
style={{ width: 200 }}
inputProps={{ min: 1, max: maxPageNumber }}
onChange={(e) => {
setSpecifiedPageText(e.target.value)
}}
/>
</form>
)
}
// Filtering
const filteredRows = rows.filter((row, rowIdx) => {
if (defaultFilter !== undefined && defaultFilter(row)) {
return false
}
return filters.length === 0
? true
: filters.every((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
const cellValue =
toCellValue !== undefined
? toCellValue(rowIdx)
: row[columns[f.columnIdx].field]
return f.values.some((v) => v === cellValue)
})
})
// Sorting
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 maxPageNumber = Math.ceil(filteredRows.length / rowsPerPage)
return (
<RootDiv>
<TableContainer>
<Table
aria-labelledby="tableTitle"
size={dense ? "small" : "medium"}
aria-label="data grid"
>
<TableHead>
<TableRow>
{collapseBody ? <TableCell /> : null}
{columns.map((column, columnIdx) => {
return (
<DataGridHeaderColumn<T>
key={columnIdx}
column={column}
order={orderBy === columnIdx ? order : null}
filter={
filters.find((f) => f.columnIdx === columnIdx) || null
}
onOrderByChange={(direction: Order) => {
setOrder(direction)
setOrderBy(columnIdx)
}}
onFilterChange={(values: Value[]) => {
const newFilters = filters.filter(
(f) => f.columnIdx !== columnIdx
)
newFilters.push({
columnIdx: columnIdx,
values: values,
})
setFilters(newFilters)
}}
/>
)
})}
</TableRow>
</TableHead>
<TableBody>
{currentPageRows.map((row) => (
<DataGridRow<T>
columns={columns}
rowIndex={getRowIndex(row)}
row={row}
keyField={keyField}
collapseBody={collapseBody}
key={`${row[keyField]}`}
/>
))}
{emptyRows > 0 && (
<TableRow style={{ height: (dense ? 33 : 53) * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
{filteredRows.length > 0 ? (
<>
{/* @ts-ignore */}
<Box display="flex" alignItems="center">
<TablePagination
rowsPerPageOptions={rowsPerPageOption}
component="div"
count={filteredRows.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
{maxPageNumber > 2 ? (
<PaginationForm
onPageNumberSubmit={(page) => setPage(page)}
maxPageNumber={maxPageNumber}
/>
) : null}
</Box>
</>
) : null}
</RootDiv>
)
}
import {
ColumnDef,
ColumnFiltersState,
Header,
PaginationState,
SortingState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
const TableHeaderCellSpan = styled("span")({
display: "inline-flex",
@@ -257,202 +59,323 @@ const HiddenSpan = styled("span")({
width: 1,
})
function DataGridHeaderColumn<T>(props: {
column: DataGridColumn<T>
order: Order | null
onOrderByChange: (order: Order) => void
filter: RowFilter | null
onFilterChange: (values: Value[]) => void
dense?: boolean
}) {
const { column, order, onOrderByChange, filter, onFilterChange, dense } =
props
function FilterMenu<T>({
header,
filterChoices,
}: {
header: Header<T, unknown>
filterChoices: string[]
}): React.ReactElement {
const [filterMenuAnchorEl, setFilterMenuAnchorEl] =
React.useState<null | HTMLElement>(null)
const filterChoices = column.filterChoices
return (
<TableCell
padding={column.padding || "normal"}
sortDirection={order !== null ? order : false}
>
<TableHeaderCellSpan>
{column.sortable ? (
<TableSortLabel
active={order !== null}
direction={order || "asc"}
<>
<IconButton
size="small"
onClick={(e) => {
setFilterMenuAnchorEl(e.currentTarget)
}}
>
<FilterListIcon fontSize="small" />
</IconButton>
<Menu
anchorEl={filterMenuAnchorEl}
open={filterMenuAnchorEl !== null}
onClose={() => {
setFilterMenuAnchorEl(null)
}}
>
{filterChoices.map((choice) => (
<MenuItem
key={choice}
onClick={() => {
onOrderByChange(order === "asc" ? "desc" : "asc")
const skippedValues = header.column.getFilterValue() as string[]
const isSkipped = skippedValues.includes(choice)
const newSkippedValues = isSkipped
? skippedValues.filter((v) => v !== choice)
: skippedValues.concat(choice)
header.column.setFilterValue(newSkippedValues)
}}
>
{column.label}
{order !== null ? (
<HiddenSpan>
{order === "desc" ? "sorted descending" : "sorted ascending"}
</HiddenSpan>
) : null}
</TableSortLabel>
) : (
column.label
)}
{filterChoices !== undefined ? (
<>
<IconButton
size={dense ? "small" : "medium"}
onClick={(e) => {
setFilterMenuAnchorEl(e.currentTarget)
}}
>
<FilterListIcon fontSize="small" />
</IconButton>
<Menu
anchorEl={filterMenuAnchorEl}
open={filterMenuAnchorEl !== null}
onClose={() => {
setFilterMenuAnchorEl(null)
}}
>
{filterChoices.map((choice) => (
<MenuItem
key={choice}
onClick={() => {
const newTickedValues =
filter === null
? filterChoices.filter((v) => v !== choice) // By default, every choice is ticked, so the chosen option will be unticked.
: filter.values.some((v) => v === choice)
? filter.values.filter((v) => v !== choice)
: [...filter.values, choice]
onFilterChange(newTickedValues)
}}
>
<ListItemIcon>
{!filter || filter.values.some((v) => v === choice) ? (
<CheckBoxIcon color="primary" />
) : (
<CheckBoxOutlineBlankIcon color="primary" />
)}
</ListItemIcon>
{choice ?? "(missing value)"}
</MenuItem>
))}
</Menu>
</>
) : null}
</TableHeaderCellSpan>
</TableCell>
<ListItemIcon>
{header.column.getFilterValue() !== undefined ? (
(header.column.getFilterValue() as string[]).includes(
choice
) ? (
<CheckBoxOutlineBlankIcon color="primary" />
) : (
<CheckBoxIcon color="primary" />
)
) : null}
</ListItemIcon>
{choice ?? "(missing value)"}
</MenuItem>
))}
</Menu>
</>
)
}
function DataGridRow<T>(props: {
columns: DataGridColumn<T>[]
rowIndex: number
row: T
keyField: keyof T
collapseBody?: (rowIndex: number) => React.ReactNode
}) {
const { columns, rowIndex, row, keyField, collapseBody } = props
const [open, setOpen] = React.useState(false)
function DataGrid<T>({
data,
columns,
}: {
data: T[]
columns: ColumnDef<T>[]
}): React.ReactElement {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 50,
})
const table = useReactTable({
data,
columns,
state: {
columnFilters,
sorting,
pagination,
},
onColumnFiltersChange: setColumnFilters,
onSortingChange: setSorting,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFilteredRowModel: getFilteredRowModel(),
autoResetPageIndex: false,
})
return (
<React.Fragment>
<TableRow hover tabIndex={-1}>
{collapseBody ? (
<TableCell>
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
<Box component="div" sx={{ width: "100%" }}>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
if (
header.column.getCanFilter() &&
!header.column.getIsFiltered()
) {
header.column.setFilterValue([])
}
const order = header.column.getIsSorted()
const filterChoices = header.column.getCanFilter()
? Array.from(
header.column.getFacetedUniqueValues().keys()
).sort()
: null
return (
<TableCell key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder ? null : (
<TableHeaderCellSpan>
{header.column.getCanSort() ? (
<TableSortLabel
active={order !== false}
direction={order || "asc"}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{order !== null ? (
<HiddenSpan>
{order === "desc"
? "sorted descending"
: "sorted ascending"}
</HiddenSpan>
) : null}
</TableSortLabel>
) : (
flexRender(
header.column.columnDef.header,
header.getContext()
)
)}
{filterChoices !== null ? (
<FilterMenu
header={header}
filterChoices={filterChoices}
/>
) : null}
</TableHeaderCellSpan>
)}
</TableCell>
)
})}
</TableRow>
))}
</TableHead>
<TableBody>
{table.getRowModel().rows.map((row) => {
return (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => {
return (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
)
})}
</TableRow>
)
})}
</TableBody>
</Table>
</TableContainer>
<Box component="div" display="flex" alignItems="center">
<TablePagination
rowsPerPageOptions={[
10,
50,
100,
{ label: "All", value: data.length },
]}
component="div"
count={table.getFilteredRowModel().rows.length}
rowsPerPage={table.getState().pagination.pageSize}
page={table.getState().pagination.pageIndex}
slotProps={{
select: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
}}
onPageChange={(_, page) => {
table.setPageIndex(page)
}}
onRowsPerPageChange={(e) => {
const size = e.target.value ? Number(e.target.value) : 10
table.setPageSize(size)
}}
ActionsComponent={TablePaginationActions}
/>
{table.getPageCount() > 2 ? (
<PaginationForm1
onPageNumberSubmit={(page) => table.setPageIndex(page)}
maxPageNumber={table.getPageCount()}
/>
) : 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 (
<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>
</Box>
</Box>
)
}
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
const TablePaginationActions = ({
count,
page,
rowsPerPage,
onPageChange,
}: TablePaginationActionsProps) => {
const theme = useTheme()
const handleFirstPageButtonClick = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
onPageChange(event, 0)
}
if (b[field] > a[field]) {
return 1
const handleBackButtonClick = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
onPageChange(event, page - 1)
}
return 0
const handleNextButtonClick = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
onPageChange(event, page + 1)
}
const handleLastPageButtonClick = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1))
}
return (
<Box component="div" sx={{ flexShrink: 0, ml: 2.5 }}>
<IconButton
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
{theme.direction === "rtl" ? <LastPageIcon /> : <FirstPageIcon />}
</IconButton>
<IconButton
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
{theme.direction === "rtl" ? (
<KeyboardArrowRight />
) : (
<KeyboardArrowLeft />
)}
</IconButton>
<IconButton
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
{theme.direction === "rtl" ? (
<KeyboardArrowLeft />
) : (
<KeyboardArrowRight />
)}
</IconButton>
<IconButton
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
{theme.direction === "rtl" ? <FirstPageIcon /> : <LastPageIcon />}
</IconButton>
</Box>
)
}
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 ascending = order === "asc"
const result = ascending
? -less(a[0], b[0], ascending)
: less(a[0], b[0], ascending)
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 PaginationForm1: React.FC<{
onPageNumberSubmit: (value: number) => void
maxPageNumber: number
}> = ({ onPageNumberSubmit, maxPageNumber }) => {
// This component is separated from DataGrid to prevent `DataGrid` from re-rendering the page,
// every time any letters are input.
const [specifiedPageText, setSpecifiedPageText] = React.useState("")
const handleSubmitPageNumber = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const newPageNumber = parseInt(specifiedPageText, 10)
// Page is 0-indexed in `TablePagination`.
onPageNumberSubmit(newPageNumber - 1)
setSpecifiedPageText("") // reset the input field
}
return (
<form onSubmit={handleSubmitPageNumber}>
<TextField
size="small"
label={`Go to Page: n / ${maxPageNumber}`}
value={specifiedPageText}
type="number"
style={{ width: 200 }}
inputProps={{ min: 1, max: maxPageNumber }}
onChange={(e) => {
setSpecifiedPageText(e.target.value)
}}
/>
</form>
)
}
const isNumber = (
rowsPerPage: number | { value: number; label: string }
): rowsPerPage is number => {
return typeof rowsPerPage === "number"
}
export { DataGrid, DataGridColumn }
export { DataGrid }
@@ -11,18 +11,30 @@ import * as Optuna from "@optuna/types"
import React, { FC } from "react"
import { useStudyDetailValue, useStudySummaryValue } from "../../state"
import { BestTrialsCard } from "../BestTrialsCard"
import { DataGrid, DataGridColumn } from "../DataGrid"
import { DataGrid } from "../DataGrid"
import { Contour } from "../GraphContour"
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
export const PreferentialAnalytics: FC<{ studyId: number }> = ({ studyId }) => {
const theme = useTheme()
const studySummary = useStudySummaryValue(studyId)
const studyDetail = useStudyDetailValue(studyId)
const userAttrs = studySummary?.user_attrs || studyDetail?.user_attrs || []
const userAttrColumns: DataGridColumn<Optuna.Attribute>[] = [
{ field: "key", label: "Key", sortable: true },
{ field: "value", label: "Value", sortable: true },
const columnHelper = createColumnHelper<Optuna.Attribute>()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const columns: ColumnDef<Optuna.Attribute, any>[] = [
columnHelper.accessor("key", {
header: "Key",
enableSorting: true,
enableColumnFilter: false,
}),
columnHelper.accessor("value", {
header: "Value",
enableSorting: true,
enableColumnFilter: false,
}),
]
return (
<Box
@@ -55,14 +67,7 @@ export const PreferentialAnalytics: FC<{ studyId: number }> = ({ studyId }) => {
>
Study User Attributes
</Typography>
<DataGrid<Optuna.Attribute>
columns={userAttrColumns}
rows={userAttrs}
keyField={"key"}
dense={true}
initialRowsPerPage={5}
rowsPerPageOption={[5, 10, { label: "All", value: -1 }]}
/>
<DataGrid data={userAttrs} columns={columns} />
</CardContent>
</Card>
</Grid2>
@@ -171,7 +171,7 @@ export const StudyDetail: FC<{
>
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<TrialTable studyDetail={studyDetail} initialRowsPerPage={50} />
<TrialTable studyDetail={studyDetail} />
</CardContent>
</Card>
</Box>
+17 -12
View File
@@ -21,7 +21,7 @@ import {
import { artifactIsAvailable } from "../state"
import { StudyArtifactCards } from "./Artifact/StudyArtifactCards"
import { BestTrialsCard } from "./BestTrialsCard"
import { DataGrid, DataGridColumn } from "./DataGrid"
import { DataGrid } from "./DataGrid"
import { GraphHistory } from "./GraphHistory"
import { GraphHyperparameterImportance } from "./GraphHyperparameterImportances"
import { GraphIntermediateValues } from "./GraphIntermediateValues"
@@ -29,6 +29,8 @@ import { GraphParetoFront } from "./GraphParetoFront"
import { GraphTimeline } from "./GraphTimeline"
import { UserDefinedPlot } from "./UserDefinedPlot"
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
const theme = useTheme()
const directions = useStudyDirections(studyId)
@@ -47,9 +49,19 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
}
const userAttrs = studySummary?.user_attrs || studyDetail?.user_attrs || []
const userAttrColumns: DataGridColumn<Optuna.Attribute>[] = [
{ field: "key", label: "Key", sortable: true },
{ field: "value", label: "Value", sortable: true },
const columnHelper = createColumnHelper<Optuna.Attribute>()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const columns: ColumnDef<Optuna.Attribute, any>[] = [
columnHelper.accessor("key", {
header: "Key",
enableSorting: true,
enableColumnFilter: false,
}),
columnHelper.accessor("value", {
header: "Value",
enableSorting: true,
enableColumnFilter: false,
}),
]
const trials: Trial[] = studyDetail?.trials || []
return (
@@ -153,14 +165,7 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
>
Study User Attributes
</Typography>
<DataGrid<Optuna.Attribute>
columns={userAttrColumns}
rows={userAttrs}
keyField={"key"}
dense={true}
initialRowsPerPage={5}
rowsPerPageOption={[5, 10, { label: "All", value: -1 }]}
/>
<DataGrid data={userAttrs} columns={columns} />
</CardContent>
</Card>
</Grid2>
+104 -133
View File
@@ -5,85 +5,69 @@ import React, { FC } from "react"
import { Link } from "react-router-dom"
import { StudyDetail, Trial } from "ts/types/optuna"
import { DataGrid, DataGridColumn } from "./DataGrid"
import { DataGrid } from "./DataGrid"
import {
ColumnDef,
FilterFn,
Row,
createColumnHelper,
} from "@tanstack/react-table"
const multiValueFilter: FilterFn<Trial> = <D extends object>(
row: Row<D>,
columnId: string,
filterValue: string[]
) => {
const rowValue = row.getValue(columnId) as string
return !filterValue.includes(rowValue)
}
export const TrialTable: FC<{
studyDetail: StudyDetail | null
initialRowsPerPage?: number
}> = ({ studyDetail, initialRowsPerPage }) => {
}> = ({ studyDetail }) => {
const theme = useTheme()
const trials: Trial[] = studyDetail !== null ? studyDetail.trials : []
const objectiveNames: string[] = studyDetail?.objective_names || []
const columns: DataGridColumn<Trial>[] = [
{ field: "number", label: "Number", sortable: true, padding: "none" },
{
field: "state",
label: "State",
sortable: true,
filterChoices: ["Complete", "Pruned", "Fail", "Running", "Waiting"],
padding: "none",
toCellValue: (i) => trials[i].state.toString(),
},
const columnHelper = createColumnHelper<Trial>()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const columns: ColumnDef<Trial, any>[] = [
columnHelper.accessor("number", {
header: "Number",
enableColumnFilter: false,
}),
columnHelper.accessor("state", {
header: "State",
enableSorting: false,
enableColumnFilter: true,
filterFn: multiValueFilter,
}),
]
const valueComparator = (
firstVal?: number,
secondVal?: number,
ascending = true
): number => {
if (firstVal === secondVal) {
return 0
}
if (firstVal === undefined) {
return ascending ? -1 : 1
} else if (secondVal === undefined) {
return ascending ? 1 : -1
}
return firstVal < secondVal ? 1 : -1
}
if (studyDetail === null || studyDetail.directions.length === 1) {
columns.push({
field: "values",
label: "Value",
sortable: true,
less: (firstEl, secondEl, ascending): number => {
return valueComparator(
firstEl.values?.[0],
secondEl.values?.[0],
ascending
)
},
toCellValue: (i) => {
if (trials[i].values === undefined) {
return null
}
return trials[i].values?.[0]
},
})
columns.push(
columnHelper.accessor("values", {
header: "Value",
enableSorting: true,
enableColumnFilter: false,
sortUndefined: "last",
})
)
} else {
const objectiveColumns: DataGridColumn<Trial>[] =
studyDetail.directions.map((s, objectiveId) => ({
field: "values",
label:
objectiveNames.length === studyDetail?.directions.length
? objectiveNames[objectiveId]
: `Objective ${objectiveId}`,
sortable: true,
less: (firstEl, secondEl, ascending): number => {
return valueComparator(
firstEl.values?.[objectiveId],
secondEl.values?.[objectiveId],
ascending
)
},
toCellValue: (i) => {
if (trials[i].values === undefined) {
return null
}
return trials[i].values?.[objectiveId]
},
}))
columns.push(...objectiveColumns)
columns.push(
...studyDetail.directions.map((s, objectiveId) =>
columnHelper.accessor((row) => row["values"]?.[objectiveId], {
id: `values_${objectiveId}`,
header:
objectiveNames.length === studyDetail?.directions.length
? objectiveNames[objectiveId]
: `Objective ${objectiveId}`,
enableSorting: true,
enableColumnFilter: false,
sortUndefined: "last",
})
)
)
}
const isDynamicSpace =
studyDetail?.union_search_space.length !==
@@ -100,78 +84,65 @@ export const TrialTable: FC<{
if (filterChoices !== undefined && isDynamicSpace && hasMissingValue) {
filterChoices.push(null)
}
columns.push({
field: "params",
label: `Param ${s.name}`,
toCellValue: (i) =>
trials[i].params.find((p) => p.name === s.name)?.param_external_value ||
null,
sortable: sortable,
filterChoices: filterChoices,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
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
return valueComparator(firstVal, secondVal)
},
})
columns.push(
columnHelper.accessor(
(row) =>
row["params"].find((p) => p.name === s.name)?.param_external_value ||
null,
{
id: `params_${s.name}`,
header: `Param ${s.name}`,
enableSorting: sortable,
sortUndefined: "last",
enableColumnFilter: filterChoices !== undefined,
filterFn: multiValueFilter,
}
)
)
})
studyDetail?.union_user_attrs.forEach((attr_spec) => {
columns.push({
field: "user_attrs",
label: `UserAttribute ${attr_spec.key}`,
toCellValue: (i) =>
trials[i].user_attrs.find((attr) => attr.key === attr_spec.key)
?.value || null,
sortable: attr_spec.sortable,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
less: (firstEl, secondEl, _): number => {
const firstValString = firstEl.user_attrs.find(
(attr) => attr.key === attr_spec.key
)?.value
const secondValString = secondEl.user_attrs.find(
(attr) => attr.key === attr_spec.key
)?.value
return valueComparator(
Number(firstValString) ?? firstValString,
Number(secondValString) ?? secondValString
)
},
})
})
columns.push({
field: "trial_id",
label: "Detail",
toCellValue: (i) => (
<IconButton
component={Link}
to={
URL_PREFIX +
`/studies/${trials[i].study_id}/trials?numbers=${trials[i].number}`
columns.push(
columnHelper.accessor(
(row) =>
row["user_attrs"].find((a) => a.key === attr_spec.key)?.value || null,
{
id: `user_attrs_${attr_spec.key}`,
header: `UserAttribute ${attr_spec.key}`,
enableSorting: attr_spec.sortable,
enableColumnFilter: false,
sortUndefined: "last",
}
color="inherit"
title="Go to the trial's detail page"
size="small"
>
<LinkIcon />
</IconButton>
),
)
)
})
columns.push(
columnHelper.accessor((row) => row, {
header: "Detail",
cell: (info) => (
<IconButton
component={Link}
to={
URL_PREFIX +
`/studies/${info.getValue().study_id}/trials?numbers=${
info.getValue().number
}`
}
color="inherit"
title="Go to the trial's detail page"
size="small"
>
<LinkIcon />
</IconButton>
),
enableSorting: false,
enableColumnFilter: false,
})
)
return (
<>
<DataGrid<Trial>
columns={columns}
rows={trials}
keyField={"trial_id"}
dense={true}
initialRowsPerPage={initialRowsPerPage}
/>
<DataGrid data={trials} columns={columns} />
<Button
variant="outlined"
startIcon={<DownloadIcon />}