77 lines
2.9 KiB
TypeScript
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>
|
|
)
|
|
}
|