Files
geointel/frontend/src/components/map/TemporalTrendChart.tsx
T
Codex beacdf2560
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s
feat: add source-grounded evolution and Ollama assistant
2026-07-15 06:45:56 +02:00

77 lines
2.9 KiB
TypeScript

import type { TemporalObservation } from '../../types'
interface TemporalTrendChartProps {
timeline: TemporalObservation[]
metricKey: string
}
const WIDTH = 560
const HEIGHT = 150
const PADDING_X = 34
const PADDING_Y = 24
function formatValue(value: number, unit: string): string {
const maximumFractionDigits = unit === 'inwoners' || unit === 'objecten' ? 0 : 2
return `${value.toLocaleString('nl-BE', { maximumFractionDigits })} ${unit}`
}
export function TemporalTrendChart({ timeline, metricKey }: TemporalTrendChartProps): JSX.Element | null {
const observations = timeline.flatMap((observation) => {
const metric = observation.metrics.find((item) => item.metric_key === metricKey)
return metric ? [{ observation, metric }] : []
})
if (observations.length < 2) {
return null
}
const values = observations.map((item) => item.metric.value)
const minimum = Math.min(...values)
const maximum = Math.max(...values)
const range = maximum - minimum
const innerWidth = WIDTH - PADDING_X * 2
const innerHeight = HEIGHT - PADDING_Y * 2
const points = observations.map((item, index) => {
const x = PADDING_X + (index / (observations.length - 1)) * innerWidth
const normalized = range === 0 ? 0.5 : (item.metric.value - minimum) / range
const y = HEIGHT - PADDING_Y - normalized * innerHeight
return { ...item, x, y }
})
const pointString = points.map((point) => `${point.x},${point.y}`).join(' ')
const label = `${points[0].metric.label}: ${formatValue(points[0].metric.value, points[0].metric.unit)} tot ${formatValue(
points[points.length - 1].metric.value,
points[points.length - 1].metric.unit,
)}`
return (
<div className="geo-temporal-chart" aria-label={label}>
<div className="geo-temporal-chart-heading">
<div>
<span>Volledige tijdreeks</span>
<strong>{points[0].metric.label}</strong>
</div>
<span>{points.length} meetmomenten</span>
</div>
<svg viewBox={`0 0 ${WIDTH} ${HEIGHT}`} role="img" aria-label={label} preserveAspectRatio="none">
<line x1={PADDING_X} x2={WIDTH - PADDING_X} y1={HEIGHT - PADDING_Y} y2={HEIGHT - PADDING_Y} />
<polyline points={pointString} />
{points.map((point) => (
<g key={point.observation.dataset.id}>
<circle cx={point.x} cy={point.y} r="4" />
<text x={point.x} y={HEIGHT - 6} textAnchor="middle">
{new Date(point.observation.dataset.observed_at).getUTCFullYear()}
</text>
</g>
))}
</svg>
<div className="geo-temporal-chart-values">
{points.map((point) => (
<div key={point.observation.dataset.id}>
<span>{new Date(point.observation.dataset.observed_at).getUTCFullYear()}</span>
<strong>{formatValue(point.metric.value, point.metric.unit)}</strong>
</div>
))}
</div>
</div>
)
}