Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 31 additions & 70 deletions src/components/DrugInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { ApexAxisChartSeries } from "apexcharts";
import dynamic from "next/dynamic";
import comboDefinitions from "../assets/comboDefinitions.json";
import addDictionaryDefs from "./addDictionaryDefs";
import { parseDoseRangeMg } from "../utils/parseDoseRangeMg";

// We need to dynamically load this module
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
Expand Down Expand Up @@ -454,71 +455,23 @@ const addDosages = (drugData: Drug) => {
[strength]: strengthData,
};

// This essentially looks for "(any digit) (optional dash) (any digit) (optional unit)"
const regex = /(\d+(?:\.\d+)?)(?:-(\d+(?:\.\d+)?))?([a-zA-Z]+)?/;
const match = RegExp(regex).exec(strengthData);

// This is mostly for type-safety
if (match) {
// The minimum number is the first number to appear
const strengthMinNumber = parseFloat(match[1]);

// The maximum number is either the second number, or if there is no second number, the first number * 1.5
// Later, we have code that says "if there is no maximum, then do a fade out", the "1.5" is purely cosmetic
// The doseText above displays the "+" so it's clear to the user, they never see the (dosage * 1.5) value
const strengthMaxNumber = match[2]
? parseFloat(match[2])
: strengthMinNumber * 1.5;

// Get the color associated with the strength
const strengthColor =
doseColorData[strength as keyof typeof doseColorData];
// If it's not already in the doseColors table, add it
if (!doseColors.includes(strengthColor)) {
doseColors.push(strengthColor);
}

// Get the gradient color associated with the strength
// If it's not already in the doseGradientToColors table, add it
const strengthGradientColor = match[2]
? doseGradientData[strength as keyof typeof doseGradientData]
: "#ffffff";
if (!doseGradientToColors.includes(strengthGradientColor)) {
doseGradientToColors.push(strengthGradientColor);
}

// Check if the doseSeries already has a series with the same name
// If it does, push the new data to that series
// If it doesn't, create a new series
const existingSeries = doseSeries.find(
(series) => series.name === strength,
);
const range = parseDoseRangeMg(strengthData);
if (!range) return;

const roaUnit = match[3] ? ` (${match[3]})` : ``;
if (existingSeries) {
const existingSeriesData = existingSeries.data as {
x: string;
y: number[];
}[];

existingSeriesData.push({
x: `${roa}${roaUnit}`,
y: [strengthMinNumber, strengthMaxNumber],
});
return;
}

const dosePropertySeries = {
name: strength,
data: [
{
x: `${roa}${roaUnit}`,
y: [strengthMinNumber, strengthMaxNumber],
},
],
};
doseSeries.push(dosePropertySeries);
const point = { x: `${roa} (mg)`, y: range };
const existingSeries = doseSeries.find(
(series) => series.name === strength,
);
if (existingSeries) {
(existingSeries.data as (typeof point)[]).push(point);
return;
}

doseColors.push(doseColorData[strength as keyof typeof doseColorData]);
doseGradientToColors.push(
doseGradientData[strength as keyof typeof doseGradientData],
);
doseSeries.push({ name: strength, data: [point] });
});
});

Expand Down Expand Up @@ -594,13 +547,21 @@ const addDosages = (drugData: Drug) => {
<Typography variant="h5" style={{ color: "black" }}>
Dosages
</Typography>
<ReactApexChart
type="rangeBar"
options={option}
series={doseSeries}
height={200}
width={500}
/>
{doseSeries.length > 0 && (
<>
<Typography variant="body2">
Chart shows bounded mass ranges in mg. See the table for all
doses.
</Typography>
<ReactApexChart
type="rangeBar"
options={option}
series={doseSeries}
height={200}
width={500}
/>
</>
)}
</Grid>

{/* Dosage Text */}
Expand Down
24 changes: 24 additions & 0 deletions src/utils/__tests__/parseDoseRangeMg.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { parseDoseRangeMg } from "../parseDoseRangeMg";

describe("parseDoseRangeMg", () => {
it.each([
["800-1500ug", [0.8, 1.5]],
["1.5-3mg", [1.5, 3]],
["325mg-650mg", [325, 650]],
["800 µg – 1.5 mg", [0.8, 1.5]],
])("normalizes %s to milligrams", (source, expected) => {
expect(parseDoseRangeMg(source)).toEqual(expected);
});

it.each([
"25-50ug/hr",
"25ug/hr-50ug/hr",
"3mg+",
"3mg",
"1-3",
"3mg-1mg",
"1-3mg/kg",
])("leaves %s in the text table only", (source) =>
expect(parseDoseRangeMg(source)).toBeUndefined(),
);
});
28 changes: 28 additions & 0 deletions src/utils/parseDoseRangeMg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const massInMg = { ng: 0.000001, ug: 0.001, mg: 1, g: 1000 };
type MassUnit = keyof typeof massInMg;

export function parseDoseRangeMg(text: string): [number, number] | undefined {
// Require two explicit endpoints and at least one mass unit.
const endpoints = text
.toLowerCase()
.replace(/[\u00b5\u03bc]/gu, "u")
.split(/[-\u2013\u2014]/u);
if (endpoints.length !== 2) return;

const quantity = /^(\d+(?:\.\d+)?|\.\d+)\s*(ng|ug|mg|g)?$/u;
const lower = quantity.exec(endpoints[0].trim());
const upper = quantity.exec(endpoints[1].trim());
if (!lower || !upper) return;

const [, low, lowUnit] = lower;
const [, high, highUnit] = upper;
if (!lowUnit && !highUnit) return;

const startUnit = (lowUnit || highUnit) as MassUnit;
const endUnit = (highUnit || lowUnit) as MassUnit;
const start = Number(low) * massInMg[startUnit];
const end = Number(high) * massInMg[endUnit];
if (Number.isFinite(start) && Number.isFinite(end) && start < end) {
return [start, end];
}
}
Loading