bugfix dashboard

This commit is contained in:
Jordy Hoogeveen
2026-09-17 16:23:39 +02:00
parent 0b67f346e1
commit ad45e372e6
9 changed files with 158 additions and 48 deletions
+28 -27
View File
@@ -1,6 +1,7 @@
import { useState } from "react";
import { useApp } from "../context/AppContext.jsx";
import { fmt, pct, fmtDatum } from "../utils/format.js";
import { fmt, pct, fmtDatum, fmtMaand } from "../utils/format.js";
import { crPath } from "../utils/grafiek.js";
import { GREEN, RED, GOLD } from "../constants/index.js";
@@ -164,28 +165,12 @@ function Legenda({ kleur, label }) {
);
}
// ── Smooth Catmull-Rom bezier path ──────────────────────────────────────────
function crPath(xs, ys, t = 0.4) {
if (xs.length < 2) return "";
let d = `M ${xs[0]},${ys[0]}`;
for (let i = 0; i < xs.length - 1; i++) {
const p0 = i > 0 ? [xs[i-1], ys[i-1]] : [xs[i], ys[i]];
const p1 = [xs[i], ys[i]];
const p2 = [xs[i+1], ys[i+1]];
const p3 = i < xs.length - 2 ? [xs[i+2], ys[i+2]] : [xs[i+1], ys[i+1]];
const cp1x = (p1[0] + (p2[0] - p0[0]) * t).toFixed(2);
const cp1y = (p1[1] + (p2[1] - p0[1]) * t).toFixed(2);
const cp2x = (p2[0] - (p3[0] - p1[0]) * t).toFixed(2);
const cp2y = (p2[1] - (p3[1] - p1[1]) * t).toFixed(2);
d += ` C ${cp1x},${cp1y} ${cp2x},${cp2y} ${p2[0]},${p2[1]}`;
}
return d;
}
// ── Mini inline SVG grafiek ────────────────────────────────────────────────────
function MiniLineChart({ vData, fiatData, darkMode, T }) {
const [hovered, setHovered] = useState(null);
const W = 700, H = 230, pl = 62, pr = 16, pt = 16, pb = 52;
// pb was 52 voor gedraaide datumlabels. Die staan nu rechtop en zijn korter,
// dus er is minder hoogte nodig en houdt de lijn zelf meer ruimte over.
const W = 700, H = 230, pl = 62, pr = 16, pt = 16, pb = 30;
const vals = vData.map((d) => d.nettoWaarde || 0);
const fVals = fiatData;
// Beide reeksen staan in euro's op dezelfde as, dus het bereik moet ze allebei
@@ -194,6 +179,9 @@ function MiniLineChart({ vData, fiatData, darkMode, T }) {
const mx = Math.max(...vals, ...fVals) * 1.05;
const rng = mx - mn || 1;
const iW = W - pl - pr, iH = H - pt - pb;
// Eén label "sep '26" is bij deze lettergrootte ongeveer 46px breed. Zoveel
// passen er dus naast elkaar; de rest wordt overgeslagen.
const labelStap = Math.max(1, Math.ceil(vData.length / Math.max(1, Math.floor(iW / 46))));
const xs = vData.map((_, i) => pl + (i / Math.max(vData.length - 1, 1)) * iW);
const ys = vals.map((v) => pt + (1 - (v - mn) / rng) * iH);
const fYs = fVals.map((v) => pt + (1 - (v - mn) / rng) * iH);
@@ -320,13 +308,26 @@ function MiniLineChart({ vData, fiatData, darkMode, T }) {
</g>
)}
{/* X-as labels */}
{xs.map((x, i) => (
<text key={i} x={x} y={H - pb + 16} fill={lc} fontSize="8"
textAnchor="end" transform={`rotate(-45, ${x}, ${H - pb + 16})`}>
{fmtDatum(vData[i].datum)}
</text>
))}
{/* X-as: korte maandlabels, rechtop. Elk punt een volledige datum geven
liep bij een lange reeks over in een onleesbare band — tien tekens
gedraaid kost ~45px en er passen er maar een stuk of veertien. De
grafiek rekent nu uit hoeveel er passen en toont elke n-de; het
laatste punt staat er altijd bij, want dat is de datum waar je naar
kijkt. De exacte dag lees je in de tooltip. */}
{xs.map((x, i) => {
const laatste = i === xs.length - 1;
if (i % labelStap !== 0 && !laatste) return null;
return (
<g key={i}>
<line x1={x} y1={pt + iH} x2={x} y2={pt + iH + 4}
stroke={lc} strokeOpacity="0.5" strokeWidth="1" />
<text x={x} y={pt + iH + 17} fill={lc} fontSize="9"
textAnchor={i === 0 ? "start" : laatste ? "end" : "middle"}>
{fmtMaand(vData[i].datum)}
</text>
</g>
);
})}
</svg>
);
}
+38 -18
View File
@@ -1,6 +1,7 @@
import { useState } from "react";
import { useApp } from "../context/AppContext.jsx";
import { fmt, fmtDatum } from "../utils/format.js";
import { fmt, fmtDatum, fmtMaand } from "../utils/format.js";
import { crPath, vlakPad } from "../utils/grafiek.js";
import { useKolomBreedtes } from "../hooks/useKolomBreedtes.js";
import { RED, GREEN } from "../constants/index.js";
import { VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter } from "../components/ui/index.jsx";
@@ -16,8 +17,11 @@ function SvgLineChart({ pts, lines, T, H = 200 }) {
</div>
);
}
const W = 700, pl = 64, pr = 16, pt2 = 16, pb = 62;
// pb was 62 voor gedraaide datumlabels. Die staan nu rechtop en zijn korter.
const W = 700, pl = 64, pr = 16, pt2 = 16, pb = 34;
const iW = W - pl - pr, iH = H - pt2 - pb;
// Eén label "sep '26" is ongeveer 46px breed; zoveel passen er naast elkaar.
const labelStap = Math.max(1, Math.ceil(pts.length / Math.max(1, Math.floor(iW / 46))));
const allVals = lines.flatMap((l) => pts.map((p) => p[l.key] ?? 0));
const dataMin = Math.min(...allVals);
const dataMax = Math.max(...allVals);
@@ -60,17 +64,21 @@ function SvgLineChart({ pts, lines, T, H = 200 }) {
if (validPts.length < 2) return null;
const xs = validPts.map((p) => xp(p._i));
const ys = validPts.map((p) => yp(p[l.key]));
const ls = xs.map((x, i) => `${x},${ys[i]}`).join(" ");
const ar = `${xs[0]},${pt2 + iH} ${ls} ${xs[xs.length - 1]},${pt2 + iH}`;
// Dezelfde Catmull-Rom afronding als de dashboardgrafiek, in plaats
// van rechte segmenten tussen de punten.
const lijn = crPath(xs, ys);
const vlak = vlakPad(lijn, xs[0], xs[xs.length - 1], pt2 + iH);
return (
<g key={l.key}>
<polygon points={ar} fill={`url(#g_${l.key})`} />
<polyline points={ls} fill="none" stroke={l.color} strokeWidth="2.5"
<path d={vlak} fill={`url(#g_${l.key})`} />
<path d={lijn} fill="none" stroke={l.color} strokeWidth="2.5"
strokeLinejoin="round" strokeLinecap="round" />
{validPts.map((p, i) => {
const x = xs[i], y = ys[i];
const isHov = hovered?.lineKey === l.key && hovered?.ptIdx === i;
const tipW = 90, tipH = 22;
// Twee regels: sinds de x-as alleen nog maanden noemt, is dit
// de enige plek in de grafiek waar de exacte dag te lezen is.
const tipW = 104, tipH = 34;
const tipX = Math.min(Math.max(x - tipW / 2, pl), W - tipW - 4);
const tipY = y - tipH - 8 < pt2 ? y + 10 : y - tipH - 8;
return (
@@ -84,10 +92,14 @@ function SvgLineChart({ pts, lines, T, H = 200 }) {
fill={l.color} stroke={T.card} strokeWidth="2" />
{isHov && (
<g>
<rect x={tipX} y={tipY} width={tipW} height={22}
<rect x={tipX} y={tipY} width={tipW} height={tipH}
rx="5" fill={T.card}
stroke={l.color} strokeWidth="1" />
<text x={tipX + tipW / 2} y={tipY + 15} fill={l.color} fontSize="8"
<text x={tipX + tipW / 2} y={tipY + 13} fill={T.muted} fontSize="8"
textAnchor="middle">
{fmtDatum(p.datum)}
</text>
<text x={tipX + tipW / 2} y={tipY + 26} fill={l.color} fontSize="9"
fontWeight="700" textAnchor="middle">
{fmt(p[l.key])}
</text>
@@ -99,15 +111,23 @@ function SvgLineChart({ pts, lines, T, H = 200 }) {
</g>
);
})}
{pts.map((p, i) => (
<text key={i}
x={xp(i)} y={H - pb + 16}
fill={T.muted} fontSize="8"
textAnchor="end"
transform={`rotate(-45, ${xp(i)}, ${H - pb + 16})`}>
{fmtDatum(p.datum)}
</text>
))}
{/* X-as: korte maandlabels, rechtop en uitgedund op wat er past. Elk
punt een volledige datum geven liep bij een lange reeks over in een
onleesbare band. De exacte dag staat in de tooltip op elk punt. */}
{pts.map((p, i) => {
const laatste = i === pts.length - 1;
if (i % labelStap !== 0 && !laatste) return null;
return (
<g key={i}>
<line x1={xp(i)} y1={pt2 + iH} x2={xp(i)} y2={pt2 + iH + 4}
stroke={T.muted} strokeOpacity="0.5" strokeWidth="1" />
<text x={xp(i)} y={pt2 + iH + 17} fill={T.muted} fontSize="9"
textAnchor={i === 0 ? "start" : laatste ? "end" : "middle"}>
{fmtMaand(p.datum)}
</text>
</g>
);
})}
</svg>
<div style={{ display: "flex", gap: 20, justifyContent: "center", marginTop: 10 }}>
{lines.map((l) => (
+15
View File
@@ -14,6 +14,21 @@ export const fmtDatum = (s) => {
return `${d}-${m}-${y}`;
};
const MAANDEN = ["jan", "feb", "mrt", "apr", "mei", "jun", "jul", "aug", "sep", "okt", "nov", "dec"];
/**
* ISO datum → korte maandnotatie (`2026-09-16` → `sep '26`).
*
* Voor de x-as van de grafieken: een volledige datum is tien tekens en past
* daar alleen gedraaid, wat bij een lange reeks onleesbaar wordt. De exacte
* dag staat in de tooltip.
*/
export const fmtMaand = (s) => {
if (!s) return s;
const [y, m] = s.split("-");
return `${MAANDEN[parseInt(m, 10) - 1] ?? m} '${y.slice(2)}`;
};
/** Crypto-bedrag formatter (4 decimalen standaard) */
export const fmtCrypto = (n, d = 4) => (parseFloat(n) || 0).toFixed(d);
+43
View File
@@ -0,0 +1,43 @@
/**
* Tekenhulp voor de SVG-grafieken.
*
* Stond eerder als lokale functie in `DashboardTab.jsx`. Nu de Voortgang-grafiek
* dezelfde afronding gebruikt, hoort hij op één plek te staan — anders lopen de
* twee vroeg of laat uit elkaar.
*/
/**
* Catmull-Rom spline als bezier-pad: een vloeiende lijn door alle punten heen.
*
* @param {number[]} xs x-posities in SVG-eenheden
* @param {number[]} ys y-posities in SVG-eenheden
* @param {number} t spanning; hoger is boller, 0.4 is de huisstijl
*
* Let op: een spline kan tussen twee punten iets buiten hun bereik uitschieten.
* Voor een reeks die per definitie nooit terugloopt — zoals de cumulatief
* geïnvesteerde fiat — tekent dat een daling die niet in de data zit. Gebruik
* daar rechte segmenten.
*/
export function crPath(xs, ys, t = 0.4) {
if (xs.length < 2) return "";
let d = `M ${xs[0]},${ys[0]}`;
for (let i = 0; i < xs.length - 1; i++) {
const p0 = i > 0 ? [xs[i-1], ys[i-1]] : [xs[i], ys[i]];
const p1 = [xs[i], ys[i]];
const p2 = [xs[i+1], ys[i+1]];
const p3 = i < xs.length - 2 ? [xs[i+2], ys[i+2]] : [xs[i+1], ys[i+1]];
const cp1x = (p1[0] + (p2[0] - p0[0]) * t).toFixed(2);
const cp1y = (p1[1] + (p2[1] - p0[1]) * t).toFixed(2);
const cp2x = (p2[0] - (p3[0] - p1[0]) * t).toFixed(2);
const cp2y = (p2[1] - (p3[1] - p1[1]) * t).toFixed(2);
d += ` C ${cp1x},${cp1y} ${cp2x},${cp2y} ${p2[0]},${p2[1]}`;
}
return d;
}
/**
* Sluit een lijnpad af tot een vlak: door naar de basislijn en terug naar de
* start. Voor de gradient-vulling onder een lijn.
*/
export const vlakPad = (lijn, xEerste, xLaatste, yBasis) =>
`${lijn} L ${xLaatste},${yBasis} L ${xEerste},${yBasis} Z`;
+1
View File
@@ -1,2 +1,3 @@
export * from "./format.js";
export * from "./storage.js";
export * from "./grafiek.js";