-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #113 from codytodonnell/feature/table-component
Add SciDataGrid component
- Loading branch information
Showing
10 changed files
with
615 additions
and
167 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { Box, Stack, Chip, Popover, Grid } from "@mui/material"; | ||
import { useState } from "react"; | ||
|
||
interface ArrayWithPopoverProps { | ||
values: string[] | number[] | ||
} | ||
|
||
/** | ||
* Array of Chips with a popover to show the full list. | ||
* This is used to render arrays in table cells where the | ||
* list is cut off by the edge of the cell. | ||
*/ | ||
export const ArrayWithPopover: React.FC<ArrayWithPopoverProps> = ({ values }) => { | ||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null); | ||
|
||
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => { | ||
setAnchorEl(event.currentTarget); | ||
}; | ||
|
||
const handlePopoverClose = () => { | ||
setAnchorEl(null); | ||
}; | ||
|
||
const open = Boolean(anchorEl); | ||
return ( | ||
<Box | ||
sx={{ height: '100%' }} | ||
> | ||
<Stack | ||
direction="row" | ||
spacing={1} | ||
alignItems="center" | ||
onMouseEnter={handlePopoverOpen} | ||
onMouseLeave={handlePopoverClose} | ||
sx={{ height: '100%' }} | ||
> | ||
{values.map((v) => ( | ||
<Chip key={v} label={v} size="small" /> | ||
))} | ||
</Stack> | ||
<Popover | ||
id="mouse-over-popover" | ||
sx={{ | ||
pointerEvents: 'none', | ||
}} | ||
open={open} | ||
anchorEl={anchorEl} | ||
anchorOrigin={{ | ||
vertical: 'top', | ||
horizontal: 'left', | ||
}} | ||
transformOrigin={{ | ||
vertical: 'top', | ||
horizontal: 'left', | ||
}} | ||
onClose={handlePopoverClose} | ||
disableRestoreFocus | ||
> | ||
<Grid | ||
container | ||
rowGap={1} | ||
columnGap={1} | ||
sx={{ | ||
maxWidth: '300px', | ||
padding: 2, | ||
}} | ||
> | ||
{values.map((v) => ( | ||
<Grid key={v} item> | ||
<Chip label={v} size="small" /> | ||
</Grid> | ||
))} | ||
</Grid> | ||
</Popover> | ||
</Box> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { Box, Popover } from "@mui/material"; | ||
import { PropsWithChildren, useState } from "react"; | ||
|
||
/** | ||
* Generic inner cell content with a popover to show the full contents. | ||
* This is used to render cells with too much content to display | ||
* inside a single cell. Full content is displayed on hover in a popover box. | ||
*/ | ||
export const CellWithPopover: React.FC<PropsWithChildren> = ({ children }) => { | ||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null); | ||
|
||
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => { | ||
setAnchorEl(event.currentTarget); | ||
}; | ||
|
||
const handlePopoverClose = () => { | ||
setAnchorEl(null); | ||
}; | ||
|
||
const open = Boolean(anchorEl); | ||
return ( | ||
<Box | ||
sx={{ height: '100%' }} | ||
> | ||
<Box | ||
onMouseEnter={handlePopoverOpen} | ||
onMouseLeave={handlePopoverClose} | ||
sx={{ | ||
height: '100%', | ||
overflow: 'hidden', | ||
textOverflow: 'ellipsis', | ||
}} | ||
> | ||
{children} | ||
</Box> | ||
<Popover | ||
id="mouse-over-popover" | ||
sx={{ | ||
pointerEvents: 'none', | ||
}} | ||
open={open} | ||
anchorEl={anchorEl} | ||
anchorOrigin={{ | ||
vertical: 'top', | ||
horizontal: 'left', | ||
}} | ||
transformOrigin={{ | ||
vertical: 'top', | ||
horizontal: 'left', | ||
}} | ||
onClose={handlePopoverClose} | ||
disableRestoreFocus | ||
> | ||
<Box | ||
sx={{ | ||
maxWidth: '300px', | ||
padding: 2, | ||
}} | ||
> | ||
{children} | ||
</Box> | ||
</Popover> | ||
</Box> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
export const VALID_ELEMENTS = | ||
'H He Li Be B C N O F Ne Na Mg Al Si P S Cl Ar Kr K Ca Sc Ti V Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Ar Rb Sr Y Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I Xe Cs Ba La-Lu Hf Ta W Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn Fr Ra Ac-Lr Rf Db Sg Bh Hs Mt Ds Rg Cn La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu Ac Th Pa U Np Pu Am Cm Bk Cf Es Fm Md No Lr'.split( | ||
' ' | ||
); | ||
|
||
export const ELEMENTS_REGEX = | ||
/A[cglmrstu]|B[aehikr]?|C[adeflmnorsu]?|D[bsy]|E[rsu]|F[elmr]?|G[ade]|H[efgos]?|I[nr]?|Kr?|L[airuv]|M[dgnot]|N[abdeiop]?|Os?|P[abdmortu]?|R[abefghnu]|S[bcegimnr]?|T[abcehilm]|U(u[opst])?|V|W|Xe|Yb?|Z[nr]|La\-Lu?|Ac\-Lr?/g; | ||
|
||
export const ELEMENTS_SPLIT_REGEX = | ||
/(A[cglmrstu]|B[aehikr]?|C[adeflmnorsu]?|D[bsy]|E[rsu]|F[elmr]?|G[ade]|H[efgos]?|I[nr]?|Kr?|L[airuv]|M[dgnot]|N[abdeiop]?|Os?|P[abdmortu]?|R[abefghnu]|S[bcegimnr]?|T[abcehilm]|U(u[opst])?|V|W|Xe|Yb?|Z[nr]|La\-Lu?|Ac\-Lr?)|(.)/g; | ||
|
||
interface FormulaProps extends React.HTMLAttributes<HTMLSpanElement> { | ||
content: string; | ||
} | ||
|
||
const formulaItem = (str: string) => { | ||
if (!str.match(/\(|\)|\*/g) && !str.match(ELEMENTS_REGEX)) { | ||
return <sub>{str}</sub>; | ||
} else { | ||
return <span>{str}</span>; | ||
} | ||
}; | ||
|
||
/** | ||
* Render a formula string with proper subscripts. | ||
* Non-elements will be interpreted as subscripts. | ||
*/ | ||
export const Formula: React.FC<FormulaProps> = ({ content, ...rest }) => { | ||
let formula: React.ReactNode; | ||
const splitFormula = content.match(ELEMENTS_SPLIT_REGEX); | ||
formula = ( | ||
<span> | ||
{splitFormula?.map((s, i) => ( | ||
<span key={i}>{formulaItem(s)}</span> | ||
))} | ||
</span> | ||
); | ||
|
||
return ( | ||
<span {...rest}> | ||
{formula} | ||
</span> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import { Stack, Typography } from '@mui/material'; | ||
import { DataGrid, DataGridProps, GridColDef, GridColumnHeaderParams, GridRenderCellParams } from '@mui/x-data-grid'; | ||
import React, { ReactNode } from 'react'; | ||
import { ArrayWithPopover } from './ArrayWithPopover'; | ||
import { CellWithPopover } from './CellWithPopover'; | ||
import { hasValue } from './FilterField'; | ||
import { Formula } from './Formula'; | ||
|
||
export type SciDataGridColDef = GridColDef & { | ||
units?: string; | ||
decimals?: number; | ||
sigFigs?: number; | ||
isFormula?: boolean; | ||
hasPopover?: boolean; | ||
} | ||
|
||
interface SciDataGridProps extends Omit<DataGridProps, 'columns'> { | ||
columns: SciDataGridColDef[]; | ||
} | ||
|
||
const CellWrapper: React.FC<{ hasPopover?: boolean, children: ReactNode }> = ({ | ||
hasPopover, | ||
children | ||
}) => { | ||
if (hasPopover) { | ||
return <CellWithPopover>{children}</CellWithPopover> | ||
} else { | ||
return children | ||
} | ||
} | ||
|
||
const getGridColumns = (columns: SciDataGridColDef[]) => { | ||
return columns.map((column) => { | ||
const { | ||
units, | ||
decimals, | ||
sigFigs, | ||
isFormula, | ||
hasPopover, | ||
...gridColumn | ||
} = column; | ||
|
||
/** Render unit label underneath the headerName */ | ||
if (units) { | ||
gridColumn.renderHeader = (params: GridColumnHeaderParams) => ( | ||
<Stack> | ||
<Typography fontSize="0.875rem" fontWeight="bold">{params.colDef.headerName}</Typography> | ||
<Typography fontSize="small" color="grey.700">{units}</Typography> | ||
</Stack> | ||
) | ||
} | ||
|
||
/** Handle value transformation options */ | ||
if (!gridColumn.valueFormatter) { | ||
gridColumn.valueFormatter = (value: number) => { | ||
/** Empty cells should render as '-' */ | ||
if (!hasValue(value)) { | ||
return '-' | ||
/** | ||
* Round display values to nearest n decimals. | ||
* Exactly zero should display as just 0. | ||
* Values that would require more decimals to display | ||
* a non-zero digit should display "> 0.001" (decimals would be based on decimals value). | ||
*/ | ||
} else if (!isNaN(value) && decimals || decimals === 0) { | ||
if (value === 0) { | ||
return value; | ||
} else if (value < (1 / Math.pow(10, decimals))) { | ||
return `> ${1 / Math.pow(10, decimals)}` | ||
} else { | ||
return value.toLocaleString(undefined, { | ||
minimumFractionDigits: decimals, | ||
maximumFractionDigits: decimals | ||
}); | ||
} | ||
/** | ||
* Round display values to a certain number of significant figures | ||
* and convert to scientific notation. | ||
*/ | ||
} else if (!isNaN(value) && sigFigs) { | ||
return value.toPrecision(sigFigs); | ||
} else { | ||
return value.toLocaleString(); | ||
} | ||
} | ||
} | ||
|
||
/** Handle value transformation options */ | ||
if (!gridColumn.renderCell) { | ||
gridColumn.renderCell = (params: GridRenderCellParams) => { | ||
if (Array.isArray(params.value)) { | ||
return ( | ||
<ArrayWithPopover values={params.value} /> | ||
) | ||
} if (isFormula) { | ||
return ( | ||
<CellWrapper hasPopover={hasPopover}> | ||
<Formula content={params.value} /> | ||
</CellWrapper> | ||
) | ||
} else { | ||
return ( | ||
<CellWrapper hasPopover={hasPopover}> | ||
{params.formattedValue} | ||
</CellWrapper> | ||
) | ||
} | ||
} | ||
} | ||
|
||
return gridColumn; | ||
}) | ||
} | ||
|
||
/** | ||
* Extension of the MUI DataGrid that adds extra functionality | ||
* and options for scientific data tables. | ||
*/ | ||
export const SciDataGrid: React.FC<SciDataGridProps> = ({ | ||
rows, | ||
columns, | ||
...rest | ||
}) => { | ||
return ( | ||
<DataGrid | ||
rows={rows} | ||
columns={getGridColumns(columns)} | ||
disableColumnSelector | ||
initialState={{ | ||
pagination: { paginationModel: { page: 0, pageSize: 5 } } | ||
}} | ||
sx={{ | ||
'& .MuiDataGrid-columnHeaderTitle': { | ||
fontWeight: 'bold' | ||
}, | ||
'& .MuiDataGrid-cell:focus-within': { | ||
outline: 'none' | ||
}, | ||
'& .MuiDataGrid-overlayWrapper': { | ||
minHeight: '4rem' | ||
} | ||
}} | ||
{...rest} | ||
/> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.