import { CSSProperties, useMemo } from 'react'; import { MosaicPlotData } from '../types/plots/mosaicPlot'; import _ from 'lodash'; import { FacetedData } from '../types/plots'; import { isFaceted } from '../types/guards'; import Spinner from '../components/Spinner'; import { MEDIUM_DARK_GRAY, MEDIUM_GRAY } from '../constants/colors'; import { Table } from '@veupathdb/coreui'; interface ContingencyTableProps { data?: MosaicPlotData | FacetedData; independentVariable: string; dependentVariable: string; facetVariable?: string; /** * Styling for the component's data table(s). * Also doubles as the container styling when * the component is rendering unfaceted data. */ tableContainerStyles?: CSSProperties; /** Styling for a single facet (title + data table) */ singleFacetContainerStyles?: CSSProperties; /** Styling for the container of all facets */ facetedContainerStyles?: CSSProperties; enableSpinner?: boolean; /** * Reference values are read as [xAxisReferenceValue, yAxisReferenceValue] * The reference values are used to re-order the 2x2 table quadrants, as needed */ selectedReferenceValues?: Array; } function FacetedContingencyTable(props: ContingencyTableProps) { if (isFaceted(props.data) && props.facetVariable != null) { return (
{props.data.facets.map(({ label, data }, index) => (
{props.facetVariable}: {label}
))}
); } else { return null; } } export function ContingencyTable(props: ContingencyTableProps) { const { data, selectedReferenceValues } = props; if (data == null) { return (
{props.enableSpinner && } ); } if (isFaceted(data)) { return FacetedContingencyTable(props); } /** * JM: This is a hack to get 2x2 table data in the correct quadrants without affecting the mosaic plots. * The selectedReferenceValues (note that these are considered the quadrant A values in the UI) determine * the top-left quadrant (A), from which the converse relationships can naturally populate the remaining quadrants. * * For example, with the following variable selections in GEMS1 Case Control: * - Columns (X-axis) var: Case or control participant * - Columns (X-axis) reference var: Case * - Rows (Y-axis) var: Rotavirus, by ELISA * - Rows (Y-axis) reference var: No * We know 'Case' would be the left column and 'No' would be the top row, thus should render in quadrant A * From there, we know 'Control' must be the right column and 'Yes' the bottom row. */ const orderedData = useMemo(() => { if ( !selectedReferenceValues || !selectedReferenceValues[0] || !selectedReferenceValues[1] ) return data; const dataCopy = { ...data }; /** * If the selected xAxisRefValue doesn't match the first xAxis label, then * 1. reverse the column labels * 2. reverse the column data */ if (selectedReferenceValues[0] !== data.independentLabels[0]) { dataCopy.independentLabels = [...data.independentLabels].reverse(); dataCopy.values = data.values.map((arr) => [...arr].reverse()); } /** * If the selected yAxisRefValue doesn't match the first yAxis label, then * 1. reverse the row labels * 2. check if column data was reversed * Yes -> use dataCopy object to reverse the rows, thus capturing the previous column reversal * No -> use data object to reverse rows */ if (selectedReferenceValues[1] !== data.dependentLabels[0]) { dataCopy.dependentLabels = [...data.dependentLabels].reverse(); dataCopy.values = selectedReferenceValues[0] !== data.independentLabels[0] ? [...dataCopy.values].reverse() : [...data.values].reverse(); } return dataCopy as MosaicPlotData; }, [data, selectedReferenceValues]); const rowSums = orderedData.values.map((row) => _.sum(row)); return (
{orderedData.independentLabels.map((label) => ( ))} {orderedData.values.map((row, i) => ( {row.map((value, j) => ( ))} ))} {_.unzip(orderedData.values).map((col, i) => ( ))}
{props.independentVariable}
{props.dependentVariable} {label} Total
{orderedData.dependentLabels[i]} {value.toLocaleString()}
{makePercentString(value, rowSums)}
{rowSums[i].toLocaleString()}
{makePercentString(rowSums[i], rowSums)}
Total {_.sum(col).toLocaleString()}
{makePercentString(_.sum(col), rowSums)}
{_.sum(rowSums).toLocaleString()}
{makePercentString(_.sum(rowSums), rowSums)}
); } const makePercentString = (value: number, sumsArray: number[]) => { return ( ({_.round(_.divide(value, _.sum(sumsArray)) * 100, 1).toLocaleString()}%) ); };