aboutsummaryrefslogtreecommitdiff
path: root/src/lib/components/HealthChart.svelte
blob: c6121e350efc9418702840e9f54c76f3ea63f74c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110

<script lang="ts">
	import { AreaChart, Area, Points, Text } from 'layerchart';
	import type { HealthPoint } from '$lib/shared/routine-metrics';
	import { formatDayMonth } from '$lib/shared/temporal';

	let {
		series,
		markers = [],
		ariaLabel,
	}: {
		series: HealthPoint[];
		markers?: number[];
		ariaLabel: string;
	} = $props();

	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). Dates stay Date objects
	// because LayerChart's time scale consumes them; labels go through Temporal.
	const xTicks = $derived.by(() => {
		if (data.length < 2) return undefined;
		const step = Math.max(1, Math.floor((data.length - 1) / 4));
		const dayKey = (date: Date): string =>
			Temporal.Instant.fromEpochMilliseconds(date.getTime())
				.toString()
				.slice(0, 10);
		const values: 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;
			if (
				values.length > 0 &&
				dayKey(values[values.length - 1]) === dayKey(date)
			)
				continue;
			values.push(date);
			if (last) break;
		}
		return values;
	});
</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) => formatDayMonth(d.toISOString()),
					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>