Update shorten line names (legend, tooltip, buttons) based on window width

This commit is contained in:
2026-01-16 15:49:43 +00:00
parent dbfaf26591
commit 966006db6a
4 changed files with 175 additions and 76 deletions

View File

@@ -49,6 +49,19 @@ const MedPlanAssistant = () => {
setShowDisclaimer(true); setShowDisclaimer(true);
}; };
// Use shorter button labels on narrow screens to keep the pin control visible
const [useCompactButtons, setUseCompactButtons] = React.useState(false);
React.useEffect(() => {
const updateCompact = () => {
setUseCompactButtons(window.innerWidth < 520); // tweakable threshold
};
updateCompact();
window.addEventListener('resize', updateCompact);
return () => window.removeEventListener('resize', updateCompact);
}, []);
const { const {
appState, appState,
updateNestedState, updateNestedState,
@@ -113,19 +126,19 @@ const MedPlanAssistant = () => {
{/* Both Columns - Chart */} {/* Both Columns - Chart */}
<div className={`xl:col-span-2 bg-card p-6 rounded-lg border min-h-[600px] flex flex-col ${uiSettings.stickyChart ? 'sticky top-2 z-30 shadow-lg' : ''}`} <div className={`xl:col-span-2 bg-card p-6 rounded-lg border min-h-[600px] flex flex-col ${uiSettings.stickyChart ? 'sticky top-2 z-30 shadow-lg' : ''}`}
style={uiSettings.stickyChart ? { borderColor: 'hsl(var(--primary))' } : {}}> style={uiSettings.stickyChart ? { borderColor: 'hsl(var(--primary))' } : {}}>
<div className="flex justify-between items-center mb-4"> <div className="flex flex-wrap items-center gap-3 justify-between mb-4">
<div className="flex justify-center gap-2"> <div className="flex flex-wrap justify-center gap-2">
<Button <Button
onClick={() => updateUiSetting('chartView', 'damph')} onClick={() => updateUiSetting('chartView', 'damph')}
variant={chartView === 'damph' ? 'default' : 'secondary'} variant={chartView === 'damph' ? 'default' : 'secondary'}
> >
{t('dAmphetamine')} {t(useCompactButtons ? 'dAmphetamineShort' : 'dAmphetamine')}
</Button> </Button>
<Button <Button
onClick={() => updateUiSetting('chartView', 'ldx')} onClick={() => updateUiSetting('chartView', 'ldx')}
variant={chartView === 'ldx' ? 'default' : 'secondary'} variant={chartView === 'ldx' ? 'default' : 'secondary'}
> >
{t('lisdexamfetamine')} {t(useCompactButtons ? 'lisdexamfetamineShort' : 'lisdexamfetamine')}
</Button> </Button>
<Button <Button
onClick={() => updateUiSetting('chartView', 'both')} onClick={() => updateUiSetting('chartView', 'both')}
@@ -138,6 +151,7 @@ const MedPlanAssistant = () => {
onClick={() => updateUiSetting('stickyChart', !uiSettings.stickyChart)} onClick={() => updateUiSetting('stickyChart', !uiSettings.stickyChart)}
variant={uiSettings.stickyChart ? 'default' : 'outline'} variant={uiSettings.stickyChart ? 'default' : 'outline'}
size="sm" size="sm"
className="shrink-0"
title={uiSettings.stickyChart ? t('unpinChart') : t('pinChart')} title={uiSettings.stickyChart ? t('unpinChart') : t('pinChart')}
> >
{uiSettings.stickyChart ? <Pin size={16} /> : <PinOff size={16} />} {uiSettings.stickyChart ? <Pin size={16} /> : <PinOff size={16} />}

View File

@@ -10,7 +10,23 @@
*/ */
import React from 'react'; import React from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ReferenceLine, ResponsiveContainer } from 'recharts'; import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
Legend,
ReferenceLine,
ResponsiveContainer,
} from 'recharts';
import {
Tooltip as UiTooltip,
TooltipTrigger as UiTooltipTrigger,
TooltipContent as UiTooltipContent,
TooltipProvider as UiTooltipProvider,
} from './ui/tooltip';
// Chart color scheme // Chart color scheme
const CHART_COLORS = { const CHART_COLORS = {
@@ -69,6 +85,43 @@ const SimulationChart = ({
return () => window.removeEventListener('resize', updateWidth); return () => window.removeEventListener('resize', updateWidth);
}, []); }, []);
// Use shorter captions on narrow containers to reduce wrapping
const isCompactLabels = containerWidth < 640; // tweakable threshold for mobile
const seriesLabels = React.useMemo<Record<string, { full: string; short: string; display: string }>>(() => {
const damphFull = t('dAmphetamine');
const damphShort = t('dAmphetamineShort', { defaultValue: damphFull });
const ldxFull = t('lisdexamfetamine');
const ldxShort = t('lisdexamfetamineShort', { defaultValue: ldxFull });
const overlayFull = t('regularPlanOverlay');
const overlayShort = t('regularPlanOverlayShort', { defaultValue: overlayFull });
const useShort = isCompactLabels;
return {
combinedDamph: {
full: damphFull,
short: damphShort,
display: useShort ? damphShort : damphFull,
},
combinedLdx: {
full: ldxFull,
short: ldxShort,
display: useShort ? ldxShort : ldxFull,
},
templateDamph: {
full: `${damphFull} (${overlayFull})`,
short: `${damphShort} (${overlayShort})`,
display: useShort ? `${damphShort} (${overlayShort})` : `${damphFull} (${overlayFull})`,
},
templateLdx: {
full: `${ldxFull} (${overlayFull})`,
short: `${ldxShort} (${overlayShort})`,
display: useShort ? `${ldxShort} (${overlayShort})` : `${ldxFull} (${overlayFull})`,
},
};
}, [isCompactLabels, t]);
const simDays = parseInt(simulationDays, 10) || 3; const simDays = parseInt(simulationDays, 10) || 3;
// Y-axis takes ~80px, scrollable area gets the rest // Y-axis takes ~80px, scrollable area gets the rest
@@ -236,71 +289,92 @@ const SimulationChart = ({
? scrollableWidth ? scrollableWidth
: Math.ceil((scrollableWidth / dispDays) * simDays); : Math.ceil((scrollableWidth / dispDays) * simDays);
const renderLegend = React.useCallback((props: any) => {
const { payload } = props;
if (!payload) return null;
return (
<UiTooltipProvider>
<ul className="flex flex-wrap gap-2 text-xs leading-tight">
{payload.map((item: any) => {
const labelInfo = seriesLabels[item.dataKey] || { display: item.value, full: item.value };
const opacity = item.payload?.opacity ?? 1;
return (
<li key={item.dataKey} className="flex items-center gap-1 max-w-[140px]">
<span
className="inline-block w-3 h-3 rounded-sm"
style={{ backgroundColor: item.color, opacity }}
/>
<UiTooltip>
<UiTooltipTrigger asChild>
<span
className="px-1 py-0.5 rounded-sm bg-white text-black shadow-sm border border-muted truncate inline-block max-w-[100px]"
title={labelInfo.full}
>
{labelInfo.display}
</span>
</UiTooltipTrigger>
<UiTooltipContent className="bg-white text-black shadow-md border max-w-xs">
<span className="font-medium">{labelInfo.full}</span>
</UiTooltipContent>
</UiTooltip>
</li>
);
})}
</ul>
</UiTooltipProvider>
);
}, [seriesLabels]);
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">
{/* Fixed Legend at top */} {/* Fixed Legend at top */}
<div style={{ height: 40, marginBottom: 8, paddingLeft: yAxisWidth + 10 }}> <div style={{ marginBottom: 8, paddingLeft: yAxisWidth + 10 }}>
<ResponsiveContainer width="100%" height="100%"> {renderLegend({
<LineChart data={mergedData} margin={{ top: 0, right: 20, left: 0, bottom: 0 }}> payload: [
<Legend ...(chartView === 'damph' || chartView === 'both'
verticalAlign="top" ? [
align="left" {
height={36} dataKey: 'combinedDamph',
wrapperStyle={{ paddingLeft: 0 }} value: seriesLabels.combinedDamph.display,
formatter={(value: string) => { color: CHART_COLORS.idealDamph,
// Apply lighter color to template overlay entries in legend payload: { opacity: 1 },
const isTemplate = value.includes(t('regularPlanOverlay')); },
return <span style={{ opacity: isTemplate ? 0.5 : 1 }}>{value}</span>; ]
}} : []),
/> ...(chartView === 'ldx' || chartView === 'both'
{/* Invisible lines just to show in legend */} ? [
{(chartView === 'damph' || chartView === 'both') && ( {
<Line dataKey: 'combinedLdx',
dataKey="combinedDamph" value: seriesLabels.combinedLdx.display,
name={`${t('dAmphetamine')}`} color: CHART_COLORS.idealLdx,
stroke={CHART_COLORS.idealDamph} payload: { opacity: 1 },
strokeWidth={2.5} },
dot={false} ]
strokeOpacity={0} : []),
/> ...(templateProfile && daysWithDeviations.size > 0 && (chartView === 'damph' || chartView === 'both')
)} ? [
{(chartView === 'ldx' || chartView === 'both') && ( {
<Line dataKey: 'templateDamph',
dataKey="combinedLdx" value: seriesLabels.templateDamph.display,
name={`${t('lisdexamfetamine')}`} color: CHART_COLORS.idealDamph,
stroke={CHART_COLORS.idealLdx} payload: { opacity: 0.5 },
strokeWidth={2} },
strokeDasharray="3 3" ]
dot={false} : []),
strokeOpacity={0} ...(templateProfile && daysWithDeviations.size > 0 && (chartView === 'ldx' || chartView === 'both')
/> ? [
)} {
{templateProfile && daysWithDeviations.size > 0 && (chartView === 'damph' || chartView === 'both') && ( dataKey: 'templateLdx',
<Line value: seriesLabels.templateLdx.display,
dataKey="templateDamph" color: CHART_COLORS.idealLdx,
name={`${t('dAmphetamine')} (${t('regularPlanOverlay')})`} payload: { opacity: 0.5 },
stroke={CHART_COLORS.idealDamph} },
strokeWidth={2} ]
strokeDasharray="3 3" : []),
dot={false} ],
strokeOpacity={0} })}
opacity={0.5}
/>
)}
{templateProfile && daysWithDeviations.size > 0 && (chartView === 'ldx' || chartView === 'both') && (
<Line
dataKey="templateLdx"
name={`${t('lisdexamfetamine')} (${t('regularPlanOverlay')})`}
stroke={CHART_COLORS.idealLdx}
strokeWidth={1.5}
strokeDasharray="3 3"
dot={false}
strokeOpacity={0}
opacity={0.5}
/>
)}
</LineChart>
</ResponsiveContainer>
</div> </div>
{/* Chart */} {/* Chart */}
@@ -321,7 +395,7 @@ const SimulationChart = ({
const h = payload.value as number; const h = payload.value as number;
let label: string; let label: string;
if (showDayTimeOnXAxis === '24h') { if (showDayTimeOnXAxis === '24h') {
label = `${h % 24}h`; label = `${h % 24}${t('unitHour')}`;
} else if (showDayTimeOnXAxis === '12h') { } else if (showDayTimeOnXAxis === '12h') {
const hour12 = h % 24; const hour12 = h % 24;
if (hour12 === 12) { if (hour12 === 12) {
@@ -365,7 +439,7 @@ const SimulationChart = ({
allowDecimals={false} allowDecimals={false}
tickCount={20} tickCount={20}
/> />
<Tooltip <RechartsTooltip
content={({ active, payload, label }) => { content={({ active, payload, label }) => {
if (!active || !payload || payload.length === 0) return null; if (!active || !payload || payload.length === 0) return null;
@@ -395,13 +469,18 @@ const SimulationChart = ({
<p className="recharts-tooltip-label" style={{ margin: 0 }}>{t('time')}: {timeLabel}</p> <p className="recharts-tooltip-label" style={{ margin: 0 }}>{t('time')}: {timeLabel}</p>
<ul className="recharts-tooltip-item-list" style={{ padding: 0, margin: 0 }}> <ul className="recharts-tooltip-item-list" style={{ padding: 0, margin: 0 }}>
{payload.map((entry: any, index: number) => { {payload.map((entry: any, index: number) => {
const isTemplate = entry.name?.includes(t('regularPlanOverlay')); const labelInfo = seriesLabels[entry.dataKey] || { display: entry.name, full: entry.name };
const isTemplate = entry.dataKey?.toString().includes('template');
const opacity = isTemplate ? 0.5 : 1; const opacity = isTemplate ? 0.5 : 1;
const value = typeof entry.value === 'number' ? entry.value.toFixed(1) : entry.value; const value = typeof entry.value === 'number' ? entry.value.toFixed(1) : entry.value;
return ( return (
<li key={`item-${index}`} className="recharts-tooltip-item" style={{ display: 'block', paddingTop: 4, paddingBottom: 4, color: entry.color, opacity }}> <li
<span className="recharts-tooltip-item-name">{entry.name}</span> key={`item-${index}`}
className="recharts-tooltip-item"
style={{ display: 'block', paddingTop: 4, paddingBottom: 4, color: entry.color, opacity }}
>
<span className="recharts-tooltip-item-name" title={labelInfo.full}>{labelInfo.display}</span>
<span className="recharts-tooltip-item-separator">: </span> <span className="recharts-tooltip-item-separator">: </span>
<span className="recharts-tooltip-item-value">{value} {t('unitNgml')}</span> <span className="recharts-tooltip-item-value">{value} {t('unitNgml')}</span>
</li> </li>
@@ -486,7 +565,7 @@ const SimulationChart = ({
<Line <Line
type="monotone" type="monotone"
dataKey="combinedDamph" dataKey="combinedDamph"
name={`${t('dAmphetamine')}`} name={seriesLabels.combinedDamph.display}
stroke={CHART_COLORS.idealDamph} stroke={CHART_COLORS.idealDamph}
strokeWidth={2.5} strokeWidth={2.5}
dot={false} dot={false}
@@ -499,7 +578,7 @@ const SimulationChart = ({
<Line <Line
type="monotone" type="monotone"
dataKey="combinedLdx" dataKey="combinedLdx"
name={`${t('lisdexamfetamine')}`} name={seriesLabels.combinedLdx.display}
stroke={CHART_COLORS.idealLdx} stroke={CHART_COLORS.idealLdx}
strokeWidth={2} strokeWidth={2}
dot={false} dot={false}
@@ -514,7 +593,7 @@ const SimulationChart = ({
<Line <Line
type="monotone" type="monotone"
dataKey="templateDamph" dataKey="templateDamph"
name={`${t('dAmphetamine')} (${t('regularPlanOverlay')})`} name={seriesLabels.templateDamph.display}
stroke={CHART_COLORS.idealDamph} stroke={CHART_COLORS.idealDamph}
strokeWidth={2} strokeWidth={2}
strokeDasharray="3 3" strokeDasharray="3 3"
@@ -529,7 +608,7 @@ const SimulationChart = ({
<Line <Line
type="monotone" type="monotone"
dataKey="templateLdx" dataKey="templateLdx"
name={`${t('lisdexamfetamine')} (${t('regularPlanOverlay')})`} name={seriesLabels.templateLdx.display}
stroke={CHART_COLORS.idealLdx} stroke={CHART_COLORS.idealLdx}
strokeWidth={1.5} strokeWidth={1.5}
strokeDasharray="3 3" strokeDasharray="3 3"

View File

@@ -6,8 +6,11 @@ export const de = {
// Chart view buttons // Chart view buttons
dAmphetamine: "d-Amphetamin", dAmphetamine: "d-Amphetamin",
dAmphetamineShort: "d-Amph",
lisdexamfetamine: "Lisdexamfetamin", lisdexamfetamine: "Lisdexamfetamin",
lisdexamfetamineShort: "LDX",
both: "Beide", both: "Beide",
regularPlanOverlayShort: "Reg.",
// Language selector // Language selector
languageSelectorLabel: "Sprache", languageSelectorLabel: "Sprache",

View File

@@ -6,8 +6,11 @@ export const en = {
// Chart view buttons // Chart view buttons
dAmphetamine: "d-Amphetamine", dAmphetamine: "d-Amphetamine",
dAmphetamineShort: "d-Amph",
lisdexamfetamine: "Lisdexamfetamine", lisdexamfetamine: "Lisdexamfetamine",
lisdexamfetamineShort: "LDX",
both: "Both", both: "Both",
regularPlanOverlayShort: "Reg.",
// Language selector // Language selector
languageSelectorLabel: "Language", languageSelectorLabel: "Language",