Skip to content

Commit

Permalink
Merge pull request #113 from codytodonnell/feature/table-component
Browse files Browse the repository at this point in the history
Add SciDataGrid component
  • Loading branch information
codytodonnell authored Aug 2, 2024
2 parents 3de42be + 88e2e2a commit 96612bd
Show file tree
Hide file tree
Showing 10 changed files with 615 additions and 167 deletions.
77 changes: 77 additions & 0 deletions strudel-components/lib/components/ArrayWithPopover.tsx
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>
)
}
65 changes: 65 additions & 0 deletions strudel-components/lib/components/CellWithPopover.tsx
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>
)
}
44 changes: 44 additions & 0 deletions strudel-components/lib/components/Formula.tsx
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>
);
};
146 changes: 146 additions & 0 deletions strudel-components/lib/components/SciDataGrid.tsx
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}
/>
)
}
3 changes: 2 additions & 1 deletion strudel-components/lib/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export { LinearMeter } from './components/LinearMeter'
export { Filters } from './components/Filters'
export { FilterField } from './components/FilterField'
export { FilterGroup } from './components/FilterGroup'
export { FilterContext } from './components/FilterContext'
export { FilterContext } from './components/FilterContext'
export { SciDataGrid } from './components/SciDataGrid'
Loading

0 comments on commit 96612bd

Please sign in to comment.