Update various improvements and minor changes

This commit is contained in:
2026-02-02 17:35:11 +00:00
parent 02b1209c2d
commit f4260061f5
10 changed files with 308 additions and 181 deletions

View File

@@ -67,6 +67,7 @@ const SimulationChart = ({
}: any) => {
const totalHours = (parseInt(simulationDays, 10) || 3) * 24;
const dispDays = parseInt(displayedDays, 10) || 2;
const simDays = parseInt(simulationDays, 10) || 3;
// Calculate chart dimensions
const [containerWidth, setContainerWidth] = React.useState(1000);
@@ -84,9 +85,19 @@ const SimulationChart = ({
return () => window.removeEventListener('resize', updateWidth);
}, []);
// Y-axis takes ~80px, scrollable area gets the rest
const yAxisWidth = 80;
const scrollableWidth = containerWidth - yAxisWidth;
// Calculate chart width for scrollable area
const chartWidth = simDays <= dispDays
? scrollableWidth
: Math.ceil((scrollableWidth / dispDays) * simDays);
// Use shorter captions on narrow containers to reduce wrapping
const isCompactLabels = containerWidth < 640; // tweakable threshold for mobile
// Precompute series labels with translations
const seriesLabels = React.useMemo<Record<string, { full: string; short: string; display: string }>>(() => {
const damphFull = t('dAmphetamine');
const damphShort = t('dAmphetamineShort', { defaultValue: damphFull });
@@ -121,15 +132,10 @@ const SimulationChart = ({
};
}, [isCompactLabels, t]);
const simDays = parseInt(simulationDays, 10) || 3;
// Y-axis takes ~80px, scrollable area gets the rest
const yAxisWidth = 80;
const scrollableWidth = containerWidth - yAxisWidth;
// Dynamically calculate tick interval based on available pixel width
// Aim for ~46px per label to avoid overlaps on narrow screens
const xTickInterval = React.useMemo(() => {
// Aim for ~46px per label to avoid overlaps on narrow screens
//const MIN_PX_PER_TICK = 46;
const MIN_PX_PER_TICK = 46;
const intervals = [1, 2, 3, 4, 6, 8, 12, 24];
@@ -146,8 +152,8 @@ const SimulationChart = ({
return selected ?? 24;
}, [dispDays, scrollableWidth]);
// Generate ticks for continuous time axis
const chartTicks = React.useMemo(() => {
// Generate x-axis ticks for continuous time axis
const xAxisTicks = React.useMemo(() => {
const ticks = [];
for (let i = 0; i <= totalHours; i += xTickInterval) {
ticks.push(i);
@@ -155,79 +161,120 @@ const SimulationChart = ({
return ticks;
}, [totalHours, xTickInterval]);
const chartDomain = React.useMemo(() => {
const numMin = parseFloat(yAxisMin);
const numMax = parseFloat(yAxisMax);
// Calculate actual data range if auto is needed
let dataMin = Infinity;
let dataMax = -Infinity;
if (isNaN(numMin) || isNaN(numMax)) {
// Scan through combined profile data to find actual min/max
combinedProfile?.forEach((point: any) => {
if (chartView === 'damph' || chartView === 'both') {
dataMin = Math.min(dataMin, point.damph);
dataMax = Math.max(dataMax, point.damph);
// Custom tick renderer for x-axis to handle 12h/24h/continuous formats
const XAxisTick = (props: any) => {
const { x, y, payload } = props;
const h = payload.value as number;
let label: string;
if (showDayTimeOnXAxis === '24h') {
label = `${h % 24}${t('unitHour')}`;
} else if (showDayTimeOnXAxis === '12h') {
const hour12 = h % 24;
if (hour12 === 12) {
label = t('tickNoon');
return (
<text x={x} y={y + 12} textAnchor="middle" fontStyle="italic" fill="#666">
{label}
</text>
);
}
if (chartView === 'ldx' || chartView === 'both') {
dataMin = Math.min(dataMin, point.ldx);
dataMax = Math.max(dataMax, point.ldx);
const displayHour = hour12 === 0 ? 12 : hour12 > 12 ? hour12 - 12 : hour12;
const period = hour12 < 12 ? 'a' : 'p';
label = `${displayHour}${period}`;
} else {
label = `${h}`;
}
return (
<text x={x} y={y + 12} textAnchor="middle" fill="#666">
{label}
</text>
);
};
// Calculate Y-axis domain based on data and user settings
const yAxisDomain = React.useMemo(() => {
const numMin = parseFloat(yAxisMin);
const numMax = parseFloat(yAxisMax);
// Calculate actual data range if auto is needed
let dataMin = Infinity;
let dataMax = -Infinity;
if (isNaN(numMin) || isNaN(numMax)) {
// Scan through combined profile data to find actual min/max
combinedProfile?.forEach((point: any) => {
if (chartView === 'damph' || chartView === 'both') {
dataMin = Math.min(dataMin, point.damph);
dataMax = Math.max(dataMax, point.damph);
}
if (chartView === 'ldx' || chartView === 'both') {
dataMin = Math.min(dataMin, point.ldx);
dataMax = Math.max(dataMax, point.ldx);
}
});
// Also check template profile if shown
templateProfile?.forEach((point: any) => {
if (chartView === 'damph' || chartView === 'both') {
dataMin = Math.min(dataMin, point.damph);
dataMax = Math.max(dataMax, point.damph);
}
if (chartView === 'ldx' || chartView === 'both') {
dataMin = Math.min(dataMin, point.ldx);
dataMax = Math.max(dataMax, point.ldx);
}
});
}
// Calculate final domain min
let domainMin: number;
if (!isNaN(numMin)) { // max value provided via settings
// User set yAxisMin explicitly
domainMin = numMin;
} else if (dataMin !== Infinity) { // data exists
// Auto mode: add 10% padding below so the line is not flush with x-axis
const range = dataMax - dataMin;
const padding = range * 0.1;
domainMin = Math.max(0, dataMin - padding);
} else { // no data
domainMin = 0;
}
// Calculate final domain max
let domainMax: number;
if (!isNaN(numMax)) { // max value provided via settings
if (dataMax !== -Infinity) {
// User set yAxisMax explicitly
// Add padding to dataMax and use the higher of manual or (dataMax + padding)
const range = dataMax - dataMin;
const padding = range * 0.1;
const dataMaxWithPadding = dataMax + padding;
// Use manual max only if it's higher than dataMax + padding
domainMax = Math.max(numMax, dataMaxWithPadding);
} else {
// No data, use manual max as-is
domainMax = numMax;
}
});
} else if (dataMax !== -Infinity) { // data exists
// Auto mode: add 10% padding above
const range = dataMax - dataMin;
const padding = range * 0.1;
domainMax = dataMax + padding;
} else { // no data
domainMax = 100;
}
// Also check template profile if shown
templateProfile?.forEach((point: any) => {
if (chartView === 'damph' || chartView === 'both') {
dataMin = Math.min(dataMin, point.damph);
dataMax = Math.max(dataMax, point.damph);
}
if (chartView === 'ldx' || chartView === 'both') {
dataMin = Math.min(dataMin, point.ldx);
dataMax = Math.max(dataMax, point.ldx);
}
});
}
return [domainMin, domainMax];
}, [yAxisMin, yAxisMax, combinedProfile, templateProfile, chartView]);
// Calculate final domain min
let domainMin: number;
if (!isNaN(numMin)) { // max value provided via settings
// User set yAxisMin explicitly
domainMin = numMin;
} else if (dataMin !== Infinity) { // data exists
// Auto mode: add 5% padding below so the line is not flush with x-axis
const range = dataMax - dataMin;
const padding = range * 0.05;
domainMin = Math.max(0, dataMin - padding);
} else { // no data
domainMin = 0;
}
// Calculate final domain max
let domainMax: number;
if (!isNaN(numMax)) { // max value provided via settings
// User set yAxisMax explicitly - use it as-is without padding
domainMax = numMax;
} else if (dataMax !== -Infinity) { // data exists
// Auto mode: add 5% padding above
const range = dataMax - dataMin;
const padding = range * 0.05;
domainMax = dataMax + padding;
} else { // no data
domainMax = 100;
}
return [domainMin, domainMax];
}, [yAxisMin, yAxisMax, combinedProfile, templateProfile, chartView]);
// Check which days have deviations (differ from template)
// Check which days have deviations (differ from regular plan)
const daysWithDeviations = React.useMemo(() => {
if (!templateProfile || !combinedProfile) return new Set<number>();
const deviatingDays = new Set<number>();
const simDays = parseInt(simulationDays, 10) || 3;
// Check each day starting from day 2 (day 1 is always template)
// Check each day starting from day 2 (day 1 is always regular plan)
for (let day = 2; day <= simDays; day++) {
const dayStartHour = (day - 1) * 24;
const dayEndHour = day * 24;
@@ -302,11 +349,6 @@ const chartDomain = React.useMemo(() => {
return Array.from(dataMap.values()).sort((a, b) => a.timeHours - b.timeHours);
}, [combinedProfile, templateProfile, daysWithDeviations]);
// Calculate chart width for scrollable area
const chartWidth = simDays <= dispDays
? scrollableWidth
: Math.ceil((scrollableWidth / dispDays) * simDays);
// Render legend with tooltips for full names (custom legend renderer)
const renderLegend = React.useCallback((props: any) => {
const { payload } = props;
@@ -343,6 +385,7 @@ const chartDomain = React.useMemo(() => {
);
}, [seriesLabels]);
// Render the chart
return (
<div ref={containerRef} className="flex-grow w-full flex flex-col overflow-y-hidden">
{/* Fixed Legend at top */}
@@ -404,56 +447,30 @@ const chartDomain = React.useMemo(() => {
margin={{ top: 0, right: 20, left: 0, bottom: 5 }}
syncId="medPlanChart"
>
{/** Custom tick renderer to italicize 'Noon' only in 12h mode */}
{(() => {
const CustomTick = (props: any) => {
const { x, y, payload } = props;
const h = payload.value as number;
let label: string;
if (showDayTimeOnXAxis === '24h') {
label = `${h % 24}${t('unitHour')}`;
} else if (showDayTimeOnXAxis === '12h') {
const hour12 = h % 24;
if (hour12 === 12) {
label = t('tickNoon');
return (
<text x={x} y={y + 12} textAnchor="middle" fontStyle="italic" fill="#666">
{label}
</text>
);
}
const displayHour = hour12 === 0 ? 12 : hour12 > 12 ? hour12 - 12 : hour12;
const period = hour12 < 12 ? 'a' : 'p';
label = `${displayHour}${period}`;
} else {
label = `${h}`;
}
return (
<text x={x} y={y + 12} textAnchor="middle" fill="#666">
{label}
</text>
);
};
return <XAxis
{/** Custom tick renderer to italicize 'Noon' only in 12h mode */ }
<XAxis
xAxisId="hours"
//label={{ value: showDayTimeOnXAxis === 'continuous' ? t('axisLabelHours') : t('axisLabelTimeOfDay'), position: 'insideBottom', offset: -10, style: { fontStyle: 'italic', color: '#666' } }}
dataKey="timeHours"
type="number"
domain={[0, totalHours]}
ticks={chartTicks}
tickCount={chartTicks.length}
interval={0}
tick={<CustomTick />}
/>;
})()}
tick={<XAxisTick />}
ticks={xAxisTicks}
tickCount={xAxisTicks.length}
//tickCount={200}
//interval={1}
allowDecimals={false}
allowDataOverflow={false}
/>
<YAxis
yAxisId="concentration"
// FIXME
//label={{ value: t('axisLabelConcentration'), angle: -90, position: 'insideLeft', style: { fontStyle: 'italic', color: '#666' } }}
domain={chartDomain as any}
allowDecimals={false}
domain={yAxisDomain as any}
tickCount={20}
interval={1}
allowDecimals={false}
allowDataOverflow={false}
/>
<RechartsTooltip
content={({ active, payload, label }) => {