Skip to content

Commit

Permalink
Merge pull request #700 from c-bata/refactor-datagrid-filter
Browse files Browse the repository at this point in the history
Filter DataGrid rows by multiple conditions
  • Loading branch information
c-bata authored Nov 28, 2023
2 parents 520d4d3 + fc77515 commit c8dfa45
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 155 deletions.
222 changes: 115 additions & 107 deletions optuna_dashboard/ts/components/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ import {
TableSortLabel,
Collapse,
IconButton,
useTheme,
Menu,
MenuItem,
} 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"
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"
import CheckBoxIcon from "@mui/icons-material/CheckBox"
import FilterListIcon from "@mui/icons-material/FilterList"
import ListItemIcon from "@mui/material/ListItemIcon"

type Order = "asc" | "desc"

Expand All @@ -29,14 +33,14 @@ interface DataGridColumn<T> {
label: string
sortable?: boolean
less?: (a: T, b: T, ascending: boolean) => number
filterable?: boolean
filterChoices?: string[]
toCellValue?: (rowIndex: number) => string | React.ReactNode
padding?: "normal" | "checkbox" | "none"
}

interface RowFilter {
columnIdx: number
value: Value
values: Value[]
}

function DataGrid<T>(props: {
Expand Down Expand Up @@ -81,36 +85,25 @@ function DataGrid<T>(props: {
}

// Filtering
const fieldAlreadyFiltered = (columnIdx: number): boolean =>
filters.some((f) => f.columnIdx === columnIdx)

const handleClickFilterCell = (columnIdx: number, value: Value) => {
if (fieldAlreadyFiltered(columnIdx)) {
return
}
const newFilters = [...filters, { columnIdx: columnIdx, value: value }]
setFilters(newFilters)
}

const filteredRows = rows.filter((row, rowIdx) => {
if (defaultFilter !== undefined && defaultFilter(row)) {
return false
}
return filters.length === 0
? true
: filters.some((f) => {
: 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
if (toCellValue !== undefined) {
return toCellValue(rowIdx) === f.value
}
const field = columns[f.columnIdx].field
return row[field] === f.value
const cellValue =
toCellValue !== undefined
? toCellValue(rowIdx)
: row[columns[f.columnIdx].field]
return f.values.some((v) => v === cellValue)
})
})

Expand All @@ -137,21 +130,32 @@ function DataGrid<T>(props: {
<TableHead>
<TableRow>
{collapseBody ? <TableCell /> : null}
{columns.map((column, columnIdx) => (
<DataGridHeaderColumn<T>
key={column.label}
column={column}
orderBy={orderBy === columnIdx ? order : null}
onOrderByChange={(direction: Order) => {
setOrder(direction)
setOrderBy(columnIdx)
}}
onFilterClear={() => {
setFilters(filters.filter((f) => f.columnIdx !== columnIdx))
}}
filtered={fieldAlreadyFiltered(columnIdx)}
/>
))}
{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>
Expand All @@ -163,7 +167,6 @@ function DataGrid<T>(props: {
keyField={keyField}
collapseBody={collapseBody}
key={`${row[keyField]}`}
handleClickFilterCell={handleClickFilterCell}
/>
))}
{emptyRows > 0 && (
Expand All @@ -187,70 +190,103 @@ function DataGrid<T>(props: {
)
}

const TableHeaderCellSpan = styled("span")({
display: "inline-flex",
})

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,
})

function DataGridHeaderColumn<T>(props: {
column: DataGridColumn<T>
orderBy: Order | null
onOrderByChange: (direction: Order) => void
filtered: boolean
onFilterClear: () => void
order: Order | null
onOrderByChange: (order: Order) => void
filter: RowFilter | null
onFilterChange: (values: Value[]) => void
dense?: boolean
}) {
const { column, orderBy, onOrderByChange, filtered, onFilterClear, dense } =
const { column, order, onOrderByChange, filter, onFilterChange, dense } =
props
const [filterMenuAnchorEl, setFilterMenuAnchorEl] =
React.useState<null | HTMLElement>(null)

const filterChoices = column.filterChoices

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 (
<TableCell
padding={column.padding || "normal"}
sortDirection={orderBy || false}
sortDirection={order !== null ? order : false}
>
<TableHeaderCellSpan>
{column.sortable ? (
<TableSortLabel
active={orderBy !== null}
direction={orderBy || "asc"}
active={order !== null}
direction={order || "asc"}
onClick={() => {
if (orderBy === null) {
onOrderByChange("asc")
} else {
onOrderByChange(orderBy === "desc" ? "asc" : "desc")
}
onOrderByChange(order === "asc" ? "desc" : "asc")
}}
>
{column.label}
{orderBy !== null ? (
{order !== null ? (
<HiddenSpan>
{orderBy === "desc" ? "sorted descending" : "sorted ascending"}
{order === "desc" ? "sorted descending" : "sorted ascending"}
</HiddenSpan>
) : null}
</TableSortLabel>
) : (
column.label
)}
{column.filterable ? (
<IconButton
size={dense ? "small" : "medium"}
style={filtered ? {} : { visibility: "hidden" }}
color="inherit"
onClick={() => {
onFilterClear()
}}
>
<Clear />
</IconButton>
{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}
</MenuItem>
))}
</Menu>
</>
) : null}
</TableHeaderCellSpan>
</TableCell>
Expand All @@ -263,24 +299,10 @@ function DataGridRow<T>(props: {
row: T
keyField: keyof T
collapseBody?: (rowIndex: number) => React.ReactNode
handleClickFilterCell: (columnIdx: number, value: Value) => void
}) {
const {
columns,
rowIndex,
row,
keyField,
collapseBody,
handleClickFilterCell,
} = props
const { columns, rowIndex, row, keyField, collapseBody } = 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}>
Expand All @@ -301,21 +323,7 @@ function DataGridRow<T>(props: {
: // 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={() => {
const value =
column.toCellValue !== undefined
? column.toCellValue(rowIndex)
: row[column.field]
handleClickFilterCell(columnIndex, value)
}}
>
<FilterableDiv>{cellItem}</FilterableDiv>
</TableCell>
) : (
return (
<TableCell
key={`${row[keyField]}:${column.field.toString()}:${columnIndex}`}
padding={column.padding || "normal"}
Expand Down
10 changes: 6 additions & 4 deletions optuna_dashboard/ts/components/TrialTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const TrialTable: FC<{
field: "state",
label: "State",
sortable: true,
filterable: true,
filterChoices: ["Complete", "Pruned", "Fail", "Running", "Waiting"],
padding: "none",
toCellValue: (i) => trials[i].state.toString(),
},
Expand Down Expand Up @@ -97,15 +97,18 @@ export const TrialTable: FC<{
) {
studyDetail?.intersection_search_space.forEach((s) => {
const sortable = s.distribution.type !== "CategoricalDistribution"
const filterable = s.distribution.type === "CategoricalDistribution"
const filterChoices =
s.distribution.type === "CategoricalDistribution"
? s.distribution.choices.map((c) => c.value)
: undefined
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,
filterable: filterable,
filterChoices: filterChoices,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
less: (firstEl, secondEl, _): number => {
const firstVal = firstEl.params.find(
Expand Down Expand Up @@ -146,7 +149,6 @@ export const TrialTable: FC<{
trials[i].user_attrs.find((attr) => attr.key === attr_spec.key)
?.value || null,
sortable: attr_spec.sortable,
filterable: !attr_spec.sortable,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
less: (firstEl, secondEl, _): number => {
const firstVal = firstEl.user_attrs.find(
Expand Down
Loading

0 comments on commit c8dfa45

Please sign in to comment.