Update shorten line names (legend, tooltip, buttons) based on window width
This commit is contained in:
22
src/App.tsx
22
src/App.tsx
@@ -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} />}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user