import React, {useMemo} from 'react'; import {StyleSheet, Text, View} from 'react-native'; import Svg, {Circle, Line, Path, Rect} from 'react-native-svg'; import {colors, spacing} from '../../../theme'; import type {MetricTrendPoint} from '../insights/metricInsights'; interface MetricTrendChartProps { points: readonly MetricTrendPoint[]; color: string; unit: string; digits: number; referenceRange: {minimum: number; maximum: number} | null; } const WIDTH = 320; const HEIGHT = 104; const PAD_X = 8; const PAD_Y = 13; const dateLabel = (value: string): string => new Date(value).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', }); const createPath = (coordinates: readonly {x: number; y: number}[]): string => { if (coordinates.length === 0) return ''; if (coordinates.length === 1) { const only = coordinates[0]!; return `M ${only.x} ${only.y}`; } return coordinates.reduce((path, point, index) => { if (index === 0) return `M ${point.x} ${point.y}`; const previous = coordinates[index - 1]!; const controlX = (previous.x + point.x) / 2; return `${path} C ${controlX} ${previous.y}, ${controlX} ${point.y}, ${point.x} ${point.y}`; }, ''); }; export function MetricTrendChart({ points, color, unit, digits, referenceRange, }: MetricTrendChartProps): React.JSX.Element { const model = useMemo(() => { if (points.length === 0) return null; const allValues = [ ...points.map(point => point.value), ...(referenceRange === null ? [] : [referenceRange.minimum, referenceRange.maximum]), ]; const rawMinimum = Math.min(...allValues); const rawMaximum = Math.max(...allValues); const padding = Math.max((rawMaximum - rawMinimum) * 0.18, Math.abs(rawMaximum) * 0.02, 0.5); const minimum = rawMinimum - padding; const maximum = rawMaximum + padding; const scaleY = (value: number): number => PAD_Y + ((maximum - value) / Math.max(maximum - minimum, 0.0001)) * (HEIGHT - PAD_Y * 2); const coordinates = points.map((point, index) => ({ x: points.length === 1 ? WIDTH / 2 : PAD_X + (index / (points.length - 1)) * (WIDTH - PAD_X * 2), y: scaleY(point.value), })); return { coordinates, path: createPath(coordinates), rangeY: referenceRange === null ? null : { top: scaleY(referenceRange.maximum), bottom: scaleY(referenceRange.minimum), }, }; }, [points, referenceRange]); if (model === null || points.length < 2) { return ( Tendance en construction Encore {Math.max(0, 2 - points.length)} mesure comparable pour tracer une évolution. ); } const first = points[0]!; const last = points[points.length - 1]!; const firstCoordinate = model.coordinates[0]!; const lastCoordinate = model.coordinates[model.coordinates.length - 1]!; const valueLabel = `${last.value.toFixed(digits)} ${unit}`; return ( {model.rangeY === null ? null : ( )} {model.coordinates.map((coordinate, index) => ( ))} {dateLabel(first.measuredAt)} {valueLabel} {dateLabel(last.measuredAt)} {points.length} mesures réelles{referenceRange === null ? '' : ' · zone pâle : repère personnel'} ); } const styles = StyleSheet.create({ empty: { minHeight: 96, justifyContent: 'center', paddingVertical: spacing.md, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: colors.border, }, emptyTitle: {color: colors.text, fontSize: 13, fontWeight: '700'}, emptyText: {color: colors.muted, fontSize: 11, lineHeight: 16, marginTop: 3}, labels: { marginTop: -2, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, date: {color: colors.muted, fontSize: 10}, lastValue: {fontSize: 11, fontWeight: '800', fontVariant: ['tabular-nums']}, caption: {color: colors.muted, fontSize: 9, marginTop: spacing.xs}, });