Update various improvements and minor changes
This commit is contained in:
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user