blob: a1291709e1c91cb00a45fdc1f265d10cc04c367b [file] [log] [blame]
import React, { FC } from "react";
import { HiChevronDown, HiChevronUp } from "react-icons/hi";
// TODO -- replace all these JSX functions with FC :/
type GenericTableProps<T> = {
data: [string, T][];
name?: string;
columns: {
displayName: string | JSX.Element;
Render: (value: T) => React.ReactNode;
}[];
// Any extra data to go below in the row
// rows are supposed to manage their own state if this needs to come/go
extraRowData?: {
Render: FC<{ value: T }>;
shouldRender: (value: T) => boolean;
};
dataTypeName: string;
comparison?: (a: [string, T], b: [string, T]) => number;
dataTypeDisplay?: (label: string, data: T) => React.ReactNode | undefined;
};
const TableRow = <T extends object>(props: {
data: T;
columns: {
displayName: string | JSX.Element;
Render: (value: T) => React.ReactNode;
}[];
extraRowData?: {
Render: FC<{ value: T }>;
shouldRender: (value: T) => boolean;
};
name: string;
dataTypeDisplay?: (label: string, value: T) => React.ReactNode | undefined;
}) => {
const [highlighted, setHighlighted] = React.useState(false);
const labelData = (
<td
key={props.name}
className="py-4 px-3 text-sm font-semibold text-gray-500"
>
{props.dataTypeDisplay
? props.dataTypeDisplay(props.name, props.data)
: props.name}
</td>
);
const out = props.columns.map((column, index) => {
return (
<td
key={index + "_expanded"}
className="whitespace-nowrap py-4 px-3 text-sm text-gray-500"
>
{column.Render(props.data)}
</td>
);
});
const Render =
props.extraRowData?.Render !== undefined &&
props.extraRowData?.shouldRender(props.data)
? props.extraRowData.Render
: undefined;
return (
<>
<tr
className={`${highlighted ? "bg-slate-100" : ""} h-12`}
onMouseEnter={() => setHighlighted(true)}
onMouseLeave={() => setHighlighted(false)}
key={props.name}
>
{[labelData, ...out]}
</tr>
{Render !== undefined ? (
<tr>
<td colSpan={1000}>{<Render value={props.data}></Render>}</td>
{/* <td colSpan={1000}>{props.extraRowData.Render(props.data)}</td> */}
</tr>
) : (
<> </>
)}
</>
);
};
export const GenericTable = <T extends object>(props: GenericTableProps<T>) => {
const sortedData = props.comparison
? props.data.sort(props.comparison)
: props.data;
return (
<div className="px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
{props.name ? (
<h1 className="text-base font-semibold leading-6 text-gray-900">
{props.name}
</h1>
) : null}
</div>
</div>
<div className="mt-8 flow-root overflow-clip">
<div className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full align-middle sm:px-6 lg:px-8">
<table className="min-w-full divide-y divide-gray-300">
<thead>
<tr>
{
<th
key={"header"}
scope="col"
className="py-3.5 pl-3 pr-3 text-left text-sm font-semibold text-gray-900"
>
{props.dataTypeName}
</th>
}
{props.columns.map((column, index) => {
return (
<th
key={index}
scope="col"
className="py-3.5 pl-3 pr-3 text-left text-sm font-semibold text-gray-900"
>
{column.displayName}
</th>
);
})}
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{sortedData.map(([name, data]) => {
return (
<>
<TableRow
key={name}
data={data}
name={name}
columns={props.columns}
extraRowData={props.extraRowData}
dataTypeDisplay={props.dataTypeDisplay}
/>
</>
);
})}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
};
type GenericGroupedTableProps<T> = {
data: [string, T][];
name?: string;
columns: {
displayName: string | JSX.Element;
Render: (
value: T[],
isSummaryRow: boolean,
isExpanded: boolean,
index?: number | undefined,
toggleExpanded?: () => void
) => React.ReactNode;
}[];
dataTypeName: string;
comparison?: (a: [string, T], b: [string, T]) => number;
label?: (props: { value: T }) => JSX.Element;
};
const GroupedTableRow = <T extends object>(props: {
data: T[];
columns: {
displayName: string | JSX.Element;
// TODO -- make this use props
Render: (
value: T[],
isSummaryRow: boolean,
isExpanded: boolean,
index?: number | undefined,
toggleExpanded?: () => void
) => React.ReactNode;
}[];
label?: (props: { value: T }) => JSX.Element;
name: string;
isSummaryRow?: boolean;
highlighted?: boolean;
index?: number | undefined;
}) => {
const [highlighted, setHighlighted] = React.useState(false);
const [expanded, setExpanded] = React.useState(false);
const Icon = expanded ? HiChevronUp : HiChevronDown;
// This is hacky -- we should not force it into this shape
const expandable = props.data.length > 1 && props.isSummaryRow;
const Label = props.label;
const labelData = (
<td key={props.name} className="px-3 text-sm font-semibold text-gray-500">
{props.isSummaryRow ? (
Label !== undefined ? (
<Label value={props.data[0]} />
) : (
props.name
)
) : (
<></>
)}
</td>
);
const columns = props.columns;
const out = columns.map((column, index) => {
return (
<td
key={index}
className="whitespace-nowrap py-4 px-3 text-sm text-gray-500"
onClick={(e) => {
if (expandable) {
setExpanded(!expanded);
}
e.stopPropagation();
}}
>
{column.Render(
props.data,
expandable as boolean,
expanded,
props.index,
expandable ? () => setExpanded(!expanded) : undefined
)}
</td>
);
});
return (
<>
<tr
className={`${
highlighted || props.highlighted ? "bg-slate-100" : ""
} h-12 ${expandable ? "cursor-cell" : "cursor-default"}`}
onMouseEnter={() => {
setHighlighted(true);
}}
onMouseLeave={() => {
setHighlighted(false);
}}
key={props.name}
>
<td>
{expandable ? (
<div className="h-full align-middle items-center">
<Icon
className="hover:scale-125 hover:cursor-pointer text-2xl text-gray-400 mr-1"
onClick={() => {
expandable && setExpanded(!expanded);
}}
/>
</div>
) : null}
</td>
{[labelData, ...out]}
</tr>
{expanded
? props.data.map((data, index) => (
<GroupedTableRow
data={[data]}
name={props.name}
columns={props.columns}
key={index}
isSummaryRow={false}
highlighted={highlighted}
label={props.label}
index={index}
/>
))
: null}
</>
);
};
export const GenericGroupedTable = <T extends object>(
props: GenericGroupedTableProps<T>
) => {
const groupedData = new Map<string, T[]>();
// Grouping table data
props.data.forEach(([name, data]) => {
const group = groupedData.get(name);
if (group) {
group.push(data);
} else {
groupedData.set(name, [data]);
}
});
const sortedDataWithDuplicates = props.comparison
? props.data.sort(props.comparison)
: props.data;
const sortedDataWithoutDuplicates = [] as [string, T][];
const seen = new Set<string>();
sortedDataWithDuplicates.forEach(([name, data]) => {
if (!seen.has(name)) {
sortedDataWithoutDuplicates.push([name, data]);
seen.add(name);
}
});
return (
<div className="px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
{props.name ? (
<h1 className="text-base font-semibold leading-6 text-gray-900">
{props.name}
</h1>
) : null}
</div>
</div>
<div className="mt-8 flow-root overflow-clip">
<div className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full align-middle sm:px-6 lg:px-8">
<table className="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th
key={"expandIcon"}
scope="col"
className="py-3.5 pl-3 pr-3 text-left text-sm font-semibold text-gray-900"
></th>
{
<th
key={"header"}
scope="col"
className="py-3.5 pl-3 pr-3 text-left text-sm font-semibold text-gray-900"
>
{props.dataTypeName}
</th>
}
{props.columns.map((column, index) => {
return (
<th
key={index}
scope="col"
className="py-3.5 pl-3 pr-3 text-left text-sm font-semibold text-gray-900"
>
{column.displayName}
</th>
);
})}
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{sortedDataWithoutDuplicates.map((item) => {
const name = item[0];
const group = groupedData.get(name);
return (
<GroupedTableRow
key={name}
data={group || []}
name={name}
columns={props.columns}
isSummaryRow={true}
label={props.label}
/>
);
})}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
};
export const getMeanStdDev = (data: number[]) => {
const mean = data.reduce((a, b) => a + b, 0) / data.length;
const stdDev = Math.sqrt(
data.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / data.length
);
return { mean, stdDev };
};
export const formatMeanStdDev = (
mean: number,
stdDev: number,
meanDecimalPoints?: number,
stdDevDecimalPoints?: number
) => {
if (meanDecimalPoints === undefined) {
meanDecimalPoints = 2;
}
if (stdDevDecimalPoints === undefined) {
stdDevDecimalPoints = 2;
}
if (stdDev === 0) {
return `${mean.toFixed(meanDecimalPoints)}`;
}
return `${mean.toFixed(meanDecimalPoints)} ± ${stdDev.toFixed(
stdDevDecimalPoints
)}`;
};