diff --git a/src/js/components/data-card/data-card.js b/src/js/components/data-card/data-card.js index aa09940..7d7ea45 100644 --- a/src/js/components/data-card/data-card.js +++ b/src/js/components/data-card/data-card.js @@ -2,83 +2,148 @@ import { CategoryScale, Chart as ChartJS, Filler, + Legend, LineController, LineElement, LinearScale, PointElement, + Tooltip, } from "chart.js"; -// Register Chart.js components ChartJS.register( CategoryScale, LinearScale, PointElement, + Legend, LineElement, LineController, Filler, + Tooltip, ); -// Initialise sparklines when DOM is ready function initialiseSparklines() { const sparklines = document.querySelectorAll(".iati-data-card__sparkline"); - sparklines.forEach((canvas) => { + sparklines.forEach(async (canvas) => { + if (ChartJS.getChart(canvas)) return; + const dataAttr = canvas.getAttribute("data-sparkline"); + const dataUrl = canvas.getAttribute("data-url"); - if (dataAttr) { - try { - const data = JSON.parse(dataAttr); - const ctx = canvas.getContext("2d"); - - new ChartJS(ctx, { - type: "line", - data: { - labels: data.labels, - datasets: [ - { - data: data.values, - borderColor: "#155366", - borderWidth: 2, - fill: true, - backgroundColor: "#E6F9FE", - pointRadius: 0, - }, - ], - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - display: false, + try { + let data = {}; + let isNested = false; + + if (dataAttr) { + data = JSON.parse(dataAttr); + } else if (dataUrl) { + const response = await fetch(dataUrl); + const result = await response.json(); + + const entries = Object.entries(result).sort( + ([a], [b]) => new Date(a) - new Date(b), + ); + + data.labels = entries.map(([key]) => + new Date(key).toLocaleDateString("en-GB", { + day: "numeric", + month: "short", + year: "numeric", + }), + ); + + isNested = entries.some( + ([, value]) => typeof value === "object" && value !== null, + ); + + if (isNested) { + canvas.parentElement.style.height = "100px"; + + const innerKeys = [ + ...new Set(entries.flatMap(([, value]) => Object.keys(value))), + ]; + + data.datasets = innerKeys.map((key) => ({ + label: key, + data: entries.map(([, value]) => value[key] ?? 0), + })); + } else { + data.datasets = [{ data: entries.map(([, value]) => value) }]; + } + } + + const ctx = canvas.getContext("2d"); + + const colors = ["#155366", "#00c497"]; + + new ChartJS(ctx, { + type: "line", + data: { + labels: data.labels, + datasets: (data.datasets ?? [{ data: data.values }]).map( + (dataset, i) => ({ + ...dataset, + borderColor: colors[i % colors.length], + borderWidth: 2, + fill: !isNested, + backgroundColor: "#E6F9FE", + pointRadius: 0, + }), + ), + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: isNested, + position: "bottom", + labels: { + pointStyle: "line", + usePointStyle: true, + color: "#155366", }, }, - elements: { - point: { - radius: 0, + tooltip: { + enabled: true, + mode: "nearest", + intersect: false, + displayColors: isNested, + boxPadding: 4, + backgroundColor: "rgba(255, 255, 255, 0.9)", + titleColor: "#155366", + bodyColor: "#155366", + callbacks: { + label: (context) => `${context.label}: ${context.parsed.y}`, + title: () => "", }, }, - scales: { - y: { - display: false, - }, - x: { - display: false, - }, + }, + elements: { + point: { + radius: 0, + hitRadius: 10, + hoverRadius: 0, }, }, - }); - } catch (e) { - console.error("Failed to create sparkline:", e); - } + scales: { + y: { display: false }, + x: { display: false }, + }, + }, + }); + } catch (e) { + console.error("Failed to create sparkline:", e); } }); } -// MutationObserver for Storybook dynamic content function setupMutationObserver() { + let debounceTimer; + const observer = new MutationObserver(() => { - setTimeout(initialiseSparklines, 50); + clearTimeout(debounceTimer); + debounceTimer = setTimeout(initialiseSparklines, 50); }); observer.observe(document.body, { @@ -87,7 +152,6 @@ function setupMutationObserver() { }); } -// Initialise when DOM is ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { initialiseSparklines();