aboutsummaryrefslogtreecommitdiff
path: root/src/lib/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/components')
-rw-r--r--src/lib/components/HealthChart.svelte110
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>