From ad45e372e6a5cf44e49ea9fcd65199c66e24f925 Mon Sep 17 00:00:00 2001 From: Jordy Hoogeveen Date: Thu, 17 Sep 2026 16:23:39 +0200 Subject: [PATCH] bugfix dashboard --- Changelog.md | 30 ++++++++++++++++ README.md | 2 +- frontend/package.json | 2 +- frontend/src/pages/DashboardTab.jsx | 55 ++++++++++++++-------------- frontend/src/pages/VoortgangTab.jsx | 56 +++++++++++++++++++---------- frontend/src/utils/format.js | 15 ++++++++ frontend/src/utils/grafiek.js | 43 ++++++++++++++++++++++ frontend/src/utils/index.js | 1 + package.json | 2 +- 9 files changed, 158 insertions(+), 48 deletions(-) create mode 100644 frontend/src/utils/grafiek.js diff --git a/Changelog.md b/Changelog.md index 5e85575..4a74004 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,36 @@ ## [Niet vrijgegeven] +### Gewijzigd +- De x-as van beide grafieken toont korte maandlabels (`sep '26`) in plaats van + een volledige datum bij elk punt, en ze staan rechtop in plaats van 45 graden + gedraaid. Tien tekens gedraaid kosten zo'n 45px en er passen er maar een stuk + of veertien op de tekenbreedte; bij een langere reeks liepen ze door elkaar + heen tot één grijze band. De grafiek rekent nu uit hoeveel labels er passen en + toont elke n-de, met het laatste punt er altijd bij — groeit je reeks, dan dunt + hij vanzelf verder uit +- Doordat de labels niet meer gedraaid zijn is de ruimte eronder kleiner: `pb` + van 52 naar 30 op het dashboard en van 62 naar 34 op Voortgang. Die hoogte gaat + naar de lijn zelf +- Nieuwe formatter `fmtMaand` in `utils/format.js`, naast `fmtDatum` +- De lijnen in de Voortgang-grafieken zijn vloeiend, net als op het dashboard. + Ze werden met `` als rechte segmenten getekend; nu met dezelfde + Catmull-Rom afronding. De gradient eronder is daarmee ook een `` in + plaats van een `` +- `crPath` stond als lokale functie in `DashboardTab.jsx` en is verhuisd naar + `utils/grafiek.js`, samen met `vlakPad` voor de vulling onder een lijn. Twee + grafieken die dezelfde curve horen te tekenen moeten niet elk hun eigen kopie + hebben. In dat bestand staat ook waaróm de inleglijn bewust géén afronding + krijgt: een spline schiet tussen twee punten iets buiten hun bereik, en bij + een reeks die nooit terugloopt tekent dat een daling die er niet is + +### Opgelost +- De tooltip van de Voortgang-grafiek toonde alleen een bedrag. Nu de x-as geen + dagen meer noemt was dat de enige plek waar de exacte datum nog te lezen was, + en die stond er niet in. De tooltip heeft er een datumregel bij + +## [1.0.8] - 2026-09-15 + ### Toegevoegd - **Kleurthema's.** Negen paletten in `frontend/src/constants/themas.js`, te kiezen onder Instellingen naast de dark-mode schakelaar. Thema en licht/donker staan diff --git a/README.md b/README.md index 541afd1..c1bae42 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Eigen vermogen · Schulden · Voortgang · Gebruikersbeheer ![Node.js](https://img.shields.io/badge/Node.js_22-339933?style=flat&logo=node.js&logoColor=white) ![PostgreSQL](https://img.shields.io/badge/PostgreSQL_18-4169E1?style=flat&logo=postgresql&logoColor=white) ![Docker](https://img.shields.io/badge/Docker_Compose-2496ED?style=flat&logo=docker&logoColor=white) -![Version](https://img.shields.io/badge/versie-1.0.7-8b5cf6?style=flat) +![Version](https://img.shields.io/badge/versie-1.0.8-8b5cf6?style=flat) ![Built with Claude](https://img.shields.io/badge/Gebouwd_met-Claude.ai-8b5cf6?style=flat&logo=anthropic&logoColor=white) VibeFinance dashboard diff --git a/frontend/package.json b/frontend/package.json index 40e5be4..5b171cc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "vibefinance-frontend", - "version": "1.0.8-dev", + "version": "1.0.8", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/pages/DashboardTab.jsx b/frontend/src/pages/DashboardTab.jsx index bf28d23..b5d952b 100644 --- a/frontend/src/pages/DashboardTab.jsx +++ b/frontend/src/pages/DashboardTab.jsx @@ -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 }) { )} - {/* X-as labels */} - {xs.map((x, i) => ( - - {fmtDatum(vData[i].datum)} - - ))} + {/* 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 ( + + + + {fmtMaand(vData[i].datum)} + + + ); + })} ); } diff --git a/frontend/src/pages/VoortgangTab.jsx b/frontend/src/pages/VoortgangTab.jsx index 118e7eb..fd6e856 100644 --- a/frontend/src/pages/VoortgangTab.jsx +++ b/frontend/src/pages/VoortgangTab.jsx @@ -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 }) { ); } - 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 ( - - + {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 && ( - - + {fmtDatum(p.datum)} + + {fmt(p[l.key])} @@ -99,15 +111,23 @@ function SvgLineChart({ pts, lines, T, H = 200 }) { ); })} - {pts.map((p, i) => ( - - {fmtDatum(p.datum)} - - ))} + {/* 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 ( + + + + {fmtMaand(p.datum)} + + + ); + })}
{lines.map((l) => ( diff --git a/frontend/src/utils/format.js b/frontend/src/utils/format.js index 523235e..387ed1f 100644 --- a/frontend/src/utils/format.js +++ b/frontend/src/utils/format.js @@ -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); diff --git a/frontend/src/utils/grafiek.js b/frontend/src/utils/grafiek.js new file mode 100644 index 0000000..6c96f26 --- /dev/null +++ b/frontend/src/utils/grafiek.js @@ -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`; diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js index 9b7f60d..17efd31 100644 --- a/frontend/src/utils/index.js +++ b/frontend/src/utils/index.js @@ -1,2 +1,3 @@ export * from "./format.js"; export * from "./storage.js"; +export * from "./grafiek.js"; diff --git a/package.json b/package.json index ce20d32..68be347 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vibefinance", - "version": "1.0.8", + "version": "1.0.9-dev", "description": "VibeFinance — persoonlijk vermogensbeheer", "private": true, "repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance",