diff --git a/frontend/src/Components/Charts/ChartAccessories/ExtraPairPlots/ExtraPairViolin.tsx b/frontend/src/Components/Charts/ChartAccessories/ExtraPairPlots/ExtraPairViolin.tsx index 601691dd..24faf77f 100644 --- a/frontend/src/Components/Charts/ChartAccessories/ExtraPairPlots/ExtraPairViolin.tsx +++ b/frontend/src/Components/Charts/ChartAccessories/ExtraPairPlots/ExtraPairViolin.tsx @@ -3,7 +3,7 @@ import { } from 'react'; import { observer } from 'mobx-react'; import { - scaleLinear, line, curveCatmullRom, format, scaleBand, select, axisBottom, + scaleLinear, line, min, max, curveCatmullRom, format, scaleBand, select, extent, axisBottom, } from 'd3'; import { Tooltip } from '@mui/material'; import styled from '@emotion/styled'; @@ -28,6 +28,16 @@ type Props = { secondaryMedianSet?: ExtraPairPoint['medianSet']; }; +/** + * Get the x domain of the KDE for the attribute. + * @param dataSet - The data set to get the domain from. + * @returns The x domain of the KDE from the data set. + */ +function getAttributeXDomain(dataSet: ExtraPairPoint['data']) { + const allKdeX = Object.values(dataSet).flatMap((result) => result.kdeArray.map((point: { x: number }) => point.x)); + return [min(allKdeX) ?? 0, max(allKdeX) ?? 20]; +} + function ExtraPairViolin({ kdeMax, dataSet, aggregationScaleDomain, aggregationScaleRange, medianSet, name, secondaryDataSet, secondaryMedianSet, }: Props) { @@ -38,10 +48,12 @@ function ExtraPairViolin({ return aggScale; }, [aggregationScaleDomain, aggregationScaleRange]); - const valueScale = scaleLinear().domain([0, 18]).range([0, ExtraPairWidth.Violin]); - if (name === 'RISK') { - valueScale.domain([0, 30]); - } + // Get the x domain dynamically for the attribute + const attributeXDomain = getAttributeXDomain(dataSet); + // Set x scale for the entire attribute (Violin plots, etc.) + const valueScale = scaleLinear() + .domain(attributeXDomain) + .range([0, ExtraPairWidth.Violin]); const lineFunction = useCallback(() => { const calculatedKdeRange = secondaryDataSet ? [-0.25 * aggregationScale().bandwidth(), 0.25 * aggregationScale().bandwidth()] : [-0.5 * aggregationScale().bandwidth(), 0.5 * aggregationScale().bandwidth()]; @@ -113,9 +125,9 @@ function ExtraPairViolin({ diff --git a/frontend/src/Components/Charts/ChartAccessories/ExtraPairPlots/GeneratorExtraPair.tsx b/frontend/src/Components/Charts/ChartAccessories/ExtraPairPlots/GeneratorExtraPair.tsx index 9b4efdbe..5d7531ca 100644 --- a/frontend/src/Components/Charts/ChartAccessories/ExtraPairPlots/GeneratorExtraPair.tsx +++ b/frontend/src/Components/Charts/ChartAccessories/ExtraPairPlots/GeneratorExtraPair.tsx @@ -130,7 +130,7 @@ function GeneratorExtraPair({ explanation = 'Percentage of Patients'; break; case 'Violin': - explanation = nameInput === 'RISK' ? 'Scaled 0-30' : (`Scaled 0-18, line at ${nameInput as string === 'Preop HGB' ? HGB_HIGH_STANDARD : HGB_LOW_STANDARD}`); + explanation = nameInput === 'DRG_WEIGHT' ? 'Scaled 0-30' : (`Scaled 0-18, line at ${nameInput as string === 'Preop HGB' ? HGB_HIGH_STANDARD : HGB_LOW_STANDARD}`); break; case 'BarChart': explanation = `Scaled 0-${format('.4r')(max(Object.values(extraPairDataPoint.data)))}`; diff --git a/frontend/src/HelperFunctions/ExtraPairDataGenerator.ts b/frontend/src/HelperFunctions/ExtraPairDataGenerator.ts index 6fd839a5..072a5055 100644 --- a/frontend/src/HelperFunctions/ExtraPairDataGenerator.ts +++ b/frontend/src/HelperFunctions/ExtraPairDataGenerator.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { - median, max, mean, sum, + median, min, max, mean, sum, } from 'd3'; import { create as createpd } from 'pdfast'; import { @@ -30,6 +30,16 @@ const outcomeDataGenerate = (aggregatedBy: string, name: string, label: string, }) as ExtraPairPoint; }; +/** + * Compute an attribute-wide min and max. + * @param input - The object to compute the min and max over. + * @returns The min and max of the object. + */ +function getAttributeMinMax(input: Record): { attributeMin: number, attributeMax: number } { + const attributeData = Object.values(input).flat(); + return { attributeMin: min(attributeData)!, attributeMax: max(attributeData)! }; +} + export const generateExtrapairPlotData = (aggregatedBy: string, hemoglobinDataSet: SingleCasePoint[], extraPairArray: string[], data: BasicAggregatedDatePoint[]) => { const newExtraPairData: ExtraPairPoint[] = []; if (extraPairArray.length > 0) { @@ -93,82 +103,30 @@ export const generateExtrapairPlotData = (aggregatedBy: string, hemoglobinDataSe newExtraPairData.push(outcomeDataGenerate(aggregatedBy, 'IRON', 'Iron', data, hemoglobinDataSet)); break; - case 'RISK': - // let temporaryDataHolder: any = {} - data.forEach((dataPoint: BasicAggregatedDatePoint) => { - temporaryDataHolder[dataPoint.aggregateAttribute] = []; - caseDictionary[dataPoint.aggregateAttribute] = new Set(dataPoint.caseIDList); - }); - hemoglobinDataSet.forEach((ob: any) => { - if (temporaryDataHolder[ob[aggregatedBy]] && caseDictionary[ob[aggregatedBy]].has(ob.CASE_ID)) { - temporaryDataHolder[ob[aggregatedBy]].push(ob.DRG_WEIGHT); - } - }); - for (const [key, value] of Object.entries(temporaryDataHolder)) { - medianData[key] = median(value as any); - let pd = createpd(value, { min: 0, max: 30 }); - pd = [{ x: 0, y: 0 }].concat(pd); - - if ((value as any).length > 5) { - kdeMaxTemp = (max(pd, (val: any) => val.y) as any) > kdeMaxTemp ? max(pd, (val: any) => val.y) : kdeMaxTemp; - } - - const reversePd = pd.map((pair: any) => ({ x: pair.x, y: -pair.y })).reverse(); - pd = pd.concat(reversePd); - newData[key] = { kdeArray: pd, dataPoints: value }; - } - newExtraPairData.push({ - name: 'RISK', label: 'DRG Weight', data: newData, type: 'Violin', medianSet: medianData, kdeMax: kdeMaxTemp, - }); - break; - + case 'DRG_WEIGHT': case 'PREOP_HEMO': - data.forEach((dataPoint: BasicAggregatedDatePoint) => { - newData[dataPoint.aggregateAttribute] = []; - caseDictionary[dataPoint.aggregateAttribute] = new Set(dataPoint.caseIDList); - }); - - hemoglobinDataSet.forEach((ob: SingleCasePoint) => { - const resultValue = ob.PREOP_HEMO; - if (newData[ob[aggregatedBy]] && resultValue > 0 && caseDictionary[ob[aggregatedBy]].has(ob.CASE_ID)) { - newData[ob[aggregatedBy]].push(resultValue); - } - }); - for (const prop in newData) { - if (Object.hasOwn(newData, prop)) { - medianData[prop] = median(newData[prop]); - let pd = createpd(newData[prop], { width: 2, min: 0, max: 18 }); - pd = [{ x: 0, y: 0 }].concat(pd); - - if ((newData[prop] as any).length > 5) { - kdeMaxTemp = (max(pd, (val: any) => val.y) as any) > kdeMaxTemp ? max(pd, (val: any) => val.y) : kdeMaxTemp; - } - - const reversePd = pd.map((pair: any) => ({ x: pair.x, y: -pair.y })).reverse(); - pd = pd.concat(reversePd); - newData[prop] = { kdeArray: pd, dataPoints: newData[prop] }; - } - } - newExtraPairData.push({ - name: 'PREOP_HEMO', label: 'Preop HGB', data: newData, type: 'Violin', medianSet: medianData, kdeMax: kdeMaxTemp, - }); - break; - case 'POSTOP_HEMO': + case 'POSTOP_HEMO': { // let newData = {} as any; data.forEach((dataPoint: BasicAggregatedDatePoint) => { newData[dataPoint.aggregateAttribute] = []; caseDictionary[dataPoint.aggregateAttribute] = new Set(dataPoint.caseIDList); }); hemoglobinDataSet.forEach((ob: any) => { - const resultValue = ob.POSTOP_HEMO; + const resultValue = ob[variable]; if (newData[ob[aggregatedBy]] && resultValue > 0 && caseDictionary[ob[aggregatedBy]].has(ob.CASE_ID)) { newData[ob[aggregatedBy]].push(resultValue); } }); + + // Compute the attribute-wide min and max for 'POSTOP_HEMO' + const { attributeMin, attributeMax } = getAttributeMinMax(newData); + for (const prop in newData) { if (Object.hasOwn(newData, prop)) { medianData[prop] = median(newData[prop]); - let pd = createpd(newData[prop], { width: 2, min: 0, max: 18 }); + + // Create the KDE for the attribute using the computed min and max + let pd = createpd(newData[prop], { width: 2, min: attributeMin, max: attributeMax }); pd = [{ x: 0, y: 0 }].concat(pd); if ((newData[prop] as any).length > 5) { @@ -181,9 +139,10 @@ export const generateExtrapairPlotData = (aggregatedBy: string, hemoglobinDataSe } } newExtraPairData.push({ - name: 'POSTOP_HEMO', label: 'Postop HGB', data: newData, type: 'Violin', medianSet: medianData, kdeMax: kdeMaxTemp, + name: variable, label: variable, data: newData, type: 'Violin', medianSet: medianData, kdeMax: kdeMaxTemp, }); break; + } default: break; } diff --git a/frontend/src/Presets/DataDict.ts b/frontend/src/Presets/DataDict.ts index 367a4481..1298c3b5 100644 --- a/frontend/src/Presets/DataDict.ts +++ b/frontend/src/Presets/DataDict.ts @@ -46,7 +46,7 @@ export const OutcomeOptions: { key: typeof OUTCOMES[number]; value: string }[] = { key: 'AMICAR', value: 'Amicar' }, ]; -export const EXTRA_PAIR_OPTIONS = [...OUTCOMES, 'PREOP_HEMO', 'POSTOP_HEMO', 'TOTAL_TRANS', 'PER_CASE', 'ZERO_TRANS', 'RISK'] as const; +export const EXTRA_PAIR_OPTIONS = [...OUTCOMES, 'PREOP_HEMO', 'POSTOP_HEMO', 'TOTAL_TRANS', 'PER_CASE', 'ZERO_TRANS', 'DRG_WEIGHT'] as const; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const ExtraPairOptions: { key: typeof EXTRA_PAIR_OPTIONS[number]; value: string }[] = (OutcomeOptions as any).concat([ { key: 'PREOP_HEMO', value: 'Preop HGB' }, @@ -54,7 +54,7 @@ export const ExtraPairOptions: { key: typeof EXTRA_PAIR_OPTIONS[number]; value: { key: 'TOTAL_TRANS', value: 'Total Transfused' }, { key: 'PER_CASE', value: 'Per Case' }, { key: 'ZERO_TRANS', value: 'Zero Transfused' }, - { key: 'RISK', value: 'APR-DRG Weight' }, + { key: 'DRG_WEIGHT', value: 'APR-DRG Weight' }, ]); const dumbbellValueOptions = [{ key: 'HGB_VALUE', value: 'Hemoglobin Value' }]; @@ -89,7 +89,6 @@ export const AcronymDictionary = { TVR: 'Tricuspid Valve Repair', PVR: 'Proliferative Vitreoretinopathy', VENT: 'Over 24 Hours Ventilator Usage', - RISK: 'Diagnosis-related Group Weight (Risk Score)', 'Zero %': 'Zero Transfusion', DEATH: 'Death in hospital', STROKE: 'Stroke',