Compare commits

...

2 Commits

3 changed files with 45 additions and 22 deletions

View File

@@ -394,11 +394,12 @@ const Settings = ({
min={0} min={0}
max={500} max={500}
placeholder={t('min')} placeholder={t('min')}
required={true} required={false}
error={!!therapeuticRangeError || !therapeuticRange.min} error={!!therapeuticRangeError}
errorMessage={formatText(therapeuticRangeError || t('errorTherapeuticRangeMinRequired') || 'Minimum therapeutic range is required')} errorMessage={formatText(therapeuticRangeError)}
showResetButton={true} showResetButton={true}
defaultValue={defaultsForT.therapeuticRangeMin} defaultValue={defaultsForT.therapeuticRangeMin}
allowEmpty={true}
/> />
<span className="text-muted-foreground">-</span> <span className="text-muted-foreground">-</span>
<FormNumericInput <FormNumericInput
@@ -409,11 +410,12 @@ const Settings = ({
max={500} max={500}
placeholder={t('max')} placeholder={t('max')}
unit="ng/ml" unit="ng/ml"
required={true} required={false}
error={!!therapeuticRangeError || !therapeuticRange.max} error={!!therapeuticRangeError}
errorMessage={formatText(therapeuticRangeError || t('errorTherapeuticRangeMaxRequired') || 'Maximum therapeutic range is required')} errorMessage={formatText(therapeuticRangeError)}
showResetButton={true} showResetButton={true}
defaultValue={defaultsForT.therapeuticRangeMax} defaultValue={defaultsForT.therapeuticRangeMax}
allowEmpty={true}
/> />
</div> </div>
</div> </div>

View File

@@ -76,6 +76,11 @@ const SimulationChart = React.memo(({
const containerRef = React.useRef<HTMLDivElement>(null); const containerRef = React.useRef<HTMLDivElement>(null);
const { width: containerWidth } = useElementSize(containerRef, 150); const { width: containerWidth } = useElementSize(containerRef, 150);
// Guard against invalid dimensions during initial render
const yAxisWidth = 80;
const minContainerWidth = yAxisWidth + 100; // Minimum 100px for chart area
const safeContainerWidth = Math.max(containerWidth, minContainerWidth);
// Track current theme for chart styling // Track current theme for chart styling
const [isDarkTheme, setIsDarkTheme] = React.useState(false); const [isDarkTheme, setIsDarkTheme] = React.useState(false);
@@ -96,9 +101,8 @@ const SimulationChart = React.memo(({
return () => observer.disconnect(); return () => observer.disconnect();
}, []); }, []);
// Y-axis takes ~80px, scrollable area gets the rest // Calculate scrollable width using safe container width
const yAxisWidth = 80; const scrollableWidth = safeContainerWidth - yAxisWidth;
const scrollableWidth = containerWidth - yAxisWidth;
// Calculate chart width for scrollable area // Calculate chart width for scrollable area
const chartWidth = simDays <= dispDays const chartWidth = simDays <= dispDays
@@ -106,7 +110,7 @@ const SimulationChart = React.memo(({
: Math.ceil((scrollableWidth / dispDays) * simDays); : Math.ceil((scrollableWidth / dispDays) * simDays);
// Use shorter captions on narrow containers to reduce wrapping // Use shorter captions on narrow containers to reduce wrapping
const isCompactLabels = containerWidth < 640; // tweakable threshold for mobile const isCompactLabels = safeContainerWidth < 640; // tweakable threshold for mobile
// Precompute series labels with translations // Precompute series labels with translations
const seriesLabels = React.useMemo<Record<string, { full: string; short: string; display: string }>>(() => { const seriesLabels = React.useMemo<Record<string, { full: string; short: string; display: string }>>(() => {
@@ -446,6 +450,15 @@ const SimulationChart = React.memo(({
); );
}, [seriesLabels]); }, [seriesLabels]);
// Don't render chart if dimensions are invalid (prevents crash during initialization)
if (chartWidth <= 0 || scrollableWidth <= 0) {
return (
<div ref={containerRef} className="flex-grow w-full flex flex-col overflow-y-hidden items-center justify-center text-muted-foreground">
<p>{t('loadingChart', { defaultValue: 'Loading chart...' })}</p>
</div>
);
}
// Render the chart // Render the chart
return ( return (
<div ref={containerRef} className="flex-grow w-full flex flex-col overflow-y-hidden"> <div ref={containerRef} className="flex-grow w-full flex flex-col overflow-y-hidden">
@@ -625,20 +638,20 @@ const SimulationChart = React.memo(({
/> />
); );
})} })}
{showTherapeuticRange && (chartView === 'damph' || chartView === 'both') && ( {showTherapeuticRange && (chartView === 'damph' || chartView === 'both') && therapeuticRange.min && !isNaN(parseFloat(therapeuticRange.min)) && (
<ReferenceLine <ReferenceLine
y={parseFloat(therapeuticRange.min) || 0} y={parseFloat(therapeuticRange.min)}
label={{ value: t('refLineMin'), position: 'insideTopLeft' }} label={{ value: t('refLineMin'), position: 'insideBottomLeft', style: { fontSize: '0.75rem', fontStyle: 'italic', fill: CHART_COLORS.therapeuticMin } }}
stroke={CHART_COLORS.therapeuticMin} stroke={CHART_COLORS.therapeuticMin}
strokeDasharray="3 3" strokeDasharray="3 3"
xAxisId="hours" xAxisId="hours"
yAxisId="concentration" yAxisId="concentration"
/> />
)} )}
{showTherapeuticRange && (chartView === 'damph' || chartView === 'both') && ( {showTherapeuticRange && (chartView === 'damph' || chartView === 'both') && therapeuticRange.max && !isNaN(parseFloat(therapeuticRange.max)) && (
<ReferenceLine <ReferenceLine
y={parseFloat(therapeuticRange.max) || 0} y={parseFloat(therapeuticRange.max)}
label={{ value: t('refLineMax'), position: 'insideTopLeft' }} label={{ value: t('refLineMax'), position: 'insideTopLeft', style: { fontSize: '0.75rem', fontStyle: 'italic', fill: CHART_COLORS.therapeuticMax } }}
stroke={CHART_COLORS.therapeuticMax} stroke={CHART_COLORS.therapeuticMax}
strokeDasharray="3 3" strokeDasharray="3 3"
xAxisId="hours" xAxisId="hours"

View File

@@ -38,18 +38,26 @@ export function useElementSize<T extends HTMLElement>(
const element = ref.current; const element = ref.current;
if (!element) return; if (!element) return;
// Set initial size // Set initial size (guard against 0 dimensions)
const initialWidth = element.clientWidth;
const initialHeight = element.clientHeight;
if (initialWidth > 0 && initialHeight > 0) {
setSize({ setSize({
width: element.clientWidth, width: initialWidth,
height: element.clientHeight, height: initialHeight,
}); });
}
// Use ResizeObserver for efficient element size tracking // Use ResizeObserver for efficient element size tracking
const resizeObserver = new ResizeObserver((entries) => { const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) { for (const entry of entries) {
const { width, height } = entry.contentRect; const { width, height } = entry.contentRect;
// Guard against invalid dimensions
if (width > 0 && height > 0) {
setSize({ width, height }); setSize({ width, height });
} }
}
}); });
resizeObserver.observe(element); resizeObserver.observe(element);