Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Heatmap Attribute Data Scaling (#312) #316

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) {
Expand All @@ -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()];
Expand Down Expand Up @@ -113,9 +125,9 @@ function ExtraPairViolin({
</g>
<line
style={{ stroke: '#e5ab73', strokeWidth: '2', strokeDasharray: '5,5' }}
x1={valueScale(name === 'Preop HGB' ? HGB_HIGH_STANDARD : HGB_LOW_STANDARD)}
x2={valueScale(name === 'Preop HGB' ? HGB_HIGH_STANDARD : HGB_LOW_STANDARD)}
opacity={name === 'RISK' ? 0 : 1}
x1={valueScale(name === 'PREOP_HEMO' ? HGB_HIGH_STANDARD : HGB_LOW_STANDARD)}
x2={valueScale(name === 'PREOP_HEMO' ? HGB_HIGH_STANDARD : HGB_LOW_STANDARD)}
opacity={name === 'DRG_WEIGHT' ? 0 : 1}
y1={aggregationScale().range()[0]}
y2={aggregationScale().range()[1] - 0.25 * aggregationScale().bandwidth()}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)))}`;
Expand Down
87 changes: 23 additions & 64 deletions frontend/src/HelperFunctions/ExtraPairDataGenerator.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<string, number[]>): { 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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/Presets/DataDict.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ 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' },
{ key: 'POSTOP_HEMO', value: 'Posop HGB' },
{ 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' }];
Expand Down Expand Up @@ -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',
Expand Down