diff options
Diffstat (limited to 'src/lib/components/HealthChart.svelte')
| -rw-r--r-- | src/lib/components/HealthChart.svelte | 110 |
1 files changed, 110 insertions, 0 deletions
diff --git a/src/lib/components/HealthChart.svelte b/src/lib/components/HealthChart.svelte new file mode 100644 index 0000000..f1f87ed --- /dev/null +++ b/src/lib/components/HealthChart.svelte @@ -0,0 +1,110 @@ +<script lang="ts"> + import { AreaChart, Area, Points, Text } from 'layerchart'; + import type { HealthPoint } from '$lib/shared/routine-metrics'; + + let { + series, + markers = [], + ariaLabel, + }: { + series: HealthPoint[]; + markers?: number[]; + ariaLabel: string; + } = $props(); + + const dateFormatter = new Intl.DateTimeFormat('fr-FR', { + day: 'numeric', + month: 'short', + }); + + const data = $derived( + series.map((p) => ({ date: new Date(p.at), health: p.health })), + ); + const dots = $derived( + markers.map((t) => ({ date: new Date(t), health: 100 })), + ); + + // Up to 5 x ticks spread across the series, skipping repeats (two samples + // the same day would print the same label twice). + const xTicks = $derived.by(() => { + if (data.length < 2) return undefined; + const step = Math.max(1, Math.floor((data.length - 1) / 4)); + const ticks: Date[] = []; + for (let i = 0; i < data.length; i += step) { + const last = i + step >= data.length; + const date = last ? data[data.length - 1].date : data[i].date; + const label = dateFormatter.format(date); + if ( + ticks.length > 0 && + dateFormatter.format(ticks[ticks.length - 1]) === label + ) + continue; + ticks.push(date); + if (last) break; + } + return ticks; + }); +</script> + +{#if data.length >= 2} + <div class="health-chart" role="img" aria-label={ariaLabel}> + <AreaChart + {data} + x="date" + y="health" + yDomain={[0, 100]} + series={[{ key: 'santé', value: 'health', color: 'var(--accent)' }]} + axis + grid={{ y: true, yTicks: [25, 50, 75, 100] }} + rule={false} + props={{ + xAxis: { + ticks: xTicks, + format: (d: Date) => dateFormatter.format(d), + tickLabel: xTickLabel, + }, + yAxis: { ticks: [25, 50, 75, 100] }, + }} + marks={chartMarks} + /> + </div> +{:else} + <p class="empty">Pas encore assez d'historique pour tracer la courbe.</p> +{/if} + +{#snippet xTickLabel({ + props, + index, +}: { + props: Record<string, unknown>; + index: number; +})} + <Text + {...props} + textAnchor={index === 0 + ? 'start' + : index === (xTicks?.length ?? 0) - 1 + ? 'end' + : 'middle'} + /> +{/snippet} + +{#snippet chartMarks()} + <Area seriesKey="santé" fillOpacity={0.2} line={{ strokeWidth: 2 }} /> + <Points + data={dots} + x="date" + y="health" + r={4} + fill="var(--success)" + stroke="var(--white)" + strokeWidth={1.5} + /> +{/snippet} + +<style> + .health-chart { + width: 100%; + height: 11rem; + } +</style> |
