Compare commits
12
Commits
v1.0.8
..
2d7e67e315
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d7e67e315 | ||
|
|
2420a00777 | ||
|
|
539b42dde7 | ||
|
|
ad45e372e6 | ||
|
|
73ce30c5b0 | ||
|
|
4915a090a3 | ||
|
|
6d1b114b78 | ||
|
|
a9b6203dec | ||
|
|
36f49b216f | ||
|
|
796245fefe | ||
|
|
0bbb166b32 | ||
|
|
fe6b94aa7f |
@@ -2,6 +2,50 @@
|
|||||||
|
|
||||||
## [Niet vrijgegeven]
|
## [Niet vrijgegeven]
|
||||||
|
|
||||||
|
### Opgelost
|
||||||
|
- Het laatste label op de x-as viel over zijn buurman heen (`mei '26` en `26`
|
||||||
|
over elkaar). De stap werd vanaf het begin van de reeks geteld en het laatste
|
||||||
|
punt werd daar los bovenop geforceerd, zodat het op willekeurige afstand van
|
||||||
|
zijn voorganger landde — bij een reeks die niet toevallig op een veelvoud
|
||||||
|
uitkwam, pal ernaast. De stap telt nu terug vanaf het laatste punt: dat valt
|
||||||
|
daarmee per definitie op een stap en houdt volle tussenruimte, zonder dat er
|
||||||
|
nog een uitzondering voor nodig is
|
||||||
|
- De breedte waarop de grafiek rekent ging van 46 naar 54px per label, zodat er
|
||||||
|
lucht tussen staat in plaats van kop-aan-kop. Over reekslengtes van 13 tot 120
|
||||||
|
punten blijft de tussenruimte nu minimaal 57px
|
||||||
|
|
||||||
|
## [1.0.9] - 2026-09-17
|
||||||
|
|
||||||
|
### 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 `<polyline>` als rechte segmenten getekend; nu met dezelfde
|
||||||
|
Catmull-Rom afronding. De gradient eronder is daarmee ook een `<path>` in
|
||||||
|
plaats van een `<polygon>`
|
||||||
|
- `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
|
### Toegevoegd
|
||||||
- **Kleurthema's.** Negen paletten in `frontend/src/constants/themas.js`, te kiezen
|
- **Kleurthema's.** Negen paletten in `frontend/src/constants/themas.js`, te kiezen
|
||||||
onder Instellingen naast de dark-mode schakelaar. Thema en licht/donker staan
|
onder Instellingen naast de dark-mode schakelaar. Thema en licht/donker staan
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Eigen vermogen · Schulden · Voortgang · Gebruikersbeheer
|
|||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
<img src="docs/screenshot-dashboard.png" alt="VibeFinance dashboard" width="820">
|
<img src="docs/screenshot-dashboard.png" alt="VibeFinance dashboard" width="820">
|
||||||
@@ -181,6 +181,16 @@ inleglijn is per definitie gelijk aan de KPI "Totaal geïnvesteerde fiat" — be
|
|||||||
rekenen het oudste voortgangsmoment als startinleg en tellen daar de stortingen
|
rekenen het oudste voortgangsmoment als startinleg en tellen daar de stortingen
|
||||||
per rij bij op.
|
per rij bij op.
|
||||||
|
|
||||||
|
De x-as noemt maanden (`sep '26`), niet losse datums: de grafiek rekent uit
|
||||||
|
hoeveel labels er naast elkaar passen en toont er zoveel, terugtellend vanaf het
|
||||||
|
laatste punt zodat dat er altijd bij staat. Bij een langere reeks dunt hij
|
||||||
|
vanzelf verder uit. De exacte dag lees je in de tooltip. De vermogenslijn is een
|
||||||
|
Catmull-Rom spline; de inleglijn krijgt bewust rechte segmenten, omdat een
|
||||||
|
spline tussen twee punten iets buiten hun bereik uitschiet en dat bij een reeks
|
||||||
|
die nooit terugloopt een daling zou tekenen die er niet is. Beide lijnen delen
|
||||||
|
de tekenhulp in
|
||||||
|
[`frontend/src/utils/grafiek.js`](frontend/src/utils/grafiek.js).
|
||||||
|
|
||||||
Onder **Instellingen** kies je een van de negen kleurthema's. Thema en
|
Onder **Instellingen** kies je een van de negen kleurthema's. Thema en
|
||||||
licht/donker staan los van elkaar: het thema levert het accent, de schakelaar de
|
licht/donker staan los van elkaar: het thema levert het accent, de schakelaar de
|
||||||
ondergrond. De paletten staan in
|
ondergrond. De paletten staan in
|
||||||
@@ -233,7 +243,8 @@ vibefinance/
|
|||||||
│ ├── main.jsx
|
│ ├── main.jsx
|
||||||
│ ├── version.js # Draaiende versie (__APP_VERSION__ uit de build)
|
│ ├── version.js # Draaiende versie (__APP_VERSION__ uit de build)
|
||||||
│ ├── constants/ # index.js (kleuren, tabs, begindata), themas.js
|
│ ├── constants/ # index.js (kleuren, tabs, begindata), themas.js
|
||||||
│ ├── utils/ # format.js (bedragen/datums), storage.js
|
│ ├── utils/ # format.js (bedragen/datums), storage.js,
|
||||||
|
│ │ # grafiek.js (SVG-padberekening)
|
||||||
│ ├── hooks/ # useTheme, useUpdateCheck, useKolomBreedtes
|
│ ├── hooks/ # useTheme, useUpdateCheck, useKolomBreedtes
|
||||||
│ ├── context/ # AppContext (global state + API sync + backup)
|
│ ├── context/ # AppContext (global state + API sync + backup)
|
||||||
│ ├── components/
|
│ ├── components/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vibefinance-frontend",
|
"name": "vibefinance-frontend",
|
||||||
"version": "1.0.8-dev",
|
"version": "1.0.10-dev",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useApp } from "../context/AppContext.jsx";
|
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";
|
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 ────────────────────────────────────────────────────
|
// ── Mini inline SVG grafiek ────────────────────────────────────────────────────
|
||||||
function MiniLineChart({ vData, fiatData, darkMode, T }) {
|
function MiniLineChart({ vData, fiatData, darkMode, T }) {
|
||||||
const [hovered, setHovered] = useState(null);
|
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 vals = vData.map((d) => d.nettoWaarde || 0);
|
||||||
const fVals = fiatData;
|
const fVals = fiatData;
|
||||||
// Beide reeksen staan in euro's op dezelfde as, dus het bereik moet ze allebei
|
// Beide reeksen staan in euro's op dezelfde as, dus het bereik moet ze allebei
|
||||||
@@ -194,6 +179,13 @@ function MiniLineChart({ vData, fiatData, darkMode, T }) {
|
|||||||
const mx = Math.max(...vals, ...fVals) * 1.05;
|
const mx = Math.max(...vals, ...fVals) * 1.05;
|
||||||
const rng = mx - mn || 1;
|
const rng = mx - mn || 1;
|
||||||
const iW = W - pl - pr, iH = H - pt - pb;
|
const iW = W - pl - pr, iH = H - pt - pb;
|
||||||
|
// Eén label "sep '26" is bij deze lettergrootte ongeveer 54px breed inclusief
|
||||||
|
// wat lucht ernaast. Zoveel passen er naast elkaar; de rest wordt
|
||||||
|
// overgeslagen. De stap telt vanaf het láátste punt terug, niet vanaf het
|
||||||
|
// eerste: dat punt is de datum waar je naar kijkt en moet er altijd bij staan.
|
||||||
|
// Forceerden we hem er los bovenop, dan botste hij op willekeurige afstand
|
||||||
|
// tegen zijn voorganger — precies wat er eerst misging.
|
||||||
|
const labelStap = Math.max(1, Math.ceil(vData.length / Math.max(1, Math.floor(iW / 54))));
|
||||||
const xs = vData.map((_, i) => pl + (i / Math.max(vData.length - 1, 1)) * iW);
|
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 ys = vals.map((v) => pt + (1 - (v - mn) / rng) * iH);
|
||||||
const fYs = fVals.map((v) => pt + (1 - (v - mn) / rng) * iH);
|
const fYs = fVals.map((v) => pt + (1 - (v - mn) / rng) * iH);
|
||||||
@@ -320,13 +312,28 @@ function MiniLineChart({ vData, fiatData, darkMode, T }) {
|
|||||||
</g>
|
</g>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* X-as labels */}
|
{/* X-as: korte maandlabels, rechtop. Elk punt een volledige datum geven
|
||||||
{xs.map((x, i) => (
|
liep bij een lange reeks over in een onleesbare band — tien tekens
|
||||||
<text key={i} x={x} y={H - pb + 16} fill={lc} fontSize="8"
|
gedraaid kost ~45px en er passen er maar een stuk of veertien. De
|
||||||
textAnchor="end" transform={`rotate(-45, ${x}, ${H - pb + 16})`}>
|
grafiek rekent nu uit hoeveel er passen en toont elke n-de; het
|
||||||
{fmtDatum(vData[i].datum)}
|
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;
|
||||||
|
// Terugtellen vanaf het eind: het laatste punt valt zo vanzelf op een
|
||||||
|
// stap en houdt volle afstand tot zijn voorganger.
|
||||||
|
if ((xs.length - 1 - i) % labelStap !== 0) 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>
|
</text>
|
||||||
))}
|
</g>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useApp } from "../context/AppContext.jsx";
|
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 { useKolomBreedtes } from "../hooks/useKolomBreedtes.js";
|
||||||
import { RED, GREEN } from "../constants/index.js";
|
import { RED, GREEN } from "../constants/index.js";
|
||||||
import { VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter } from "../components/ui/index.jsx";
|
import { VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter } from "../components/ui/index.jsx";
|
||||||
@@ -16,8 +17,13 @@ function SvgLineChart({ pts, lines, T, H = 200 }) {
|
|||||||
</div>
|
</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;
|
const iW = W - pl - pr, iH = H - pt2 - pb;
|
||||||
|
// Eén label "sep '26" is ongeveer 54px breed inclusief lucht ernaast. De stap
|
||||||
|
// telt vanaf het laatste punt terug, zodat dat er altijd bij staat zonder op
|
||||||
|
// zijn voorganger te botsen. Zie DashboardTab voor de uitleg.
|
||||||
|
const labelStap = Math.max(1, Math.ceil(pts.length / Math.max(1, Math.floor(iW / 54))));
|
||||||
const allVals = lines.flatMap((l) => pts.map((p) => p[l.key] ?? 0));
|
const allVals = lines.flatMap((l) => pts.map((p) => p[l.key] ?? 0));
|
||||||
const dataMin = Math.min(...allVals);
|
const dataMin = Math.min(...allVals);
|
||||||
const dataMax = Math.max(...allVals);
|
const dataMax = Math.max(...allVals);
|
||||||
@@ -60,17 +66,21 @@ function SvgLineChart({ pts, lines, T, H = 200 }) {
|
|||||||
if (validPts.length < 2) return null;
|
if (validPts.length < 2) return null;
|
||||||
const xs = validPts.map((p) => xp(p._i));
|
const xs = validPts.map((p) => xp(p._i));
|
||||||
const ys = validPts.map((p) => yp(p[l.key]));
|
const ys = validPts.map((p) => yp(p[l.key]));
|
||||||
const ls = xs.map((x, i) => `${x},${ys[i]}`).join(" ");
|
// Dezelfde Catmull-Rom afronding als de dashboardgrafiek, in plaats
|
||||||
const ar = `${xs[0]},${pt2 + iH} ${ls} ${xs[xs.length - 1]},${pt2 + iH}`;
|
// van rechte segmenten tussen de punten.
|
||||||
|
const lijn = crPath(xs, ys);
|
||||||
|
const vlak = vlakPad(lijn, xs[0], xs[xs.length - 1], pt2 + iH);
|
||||||
return (
|
return (
|
||||||
<g key={l.key}>
|
<g key={l.key}>
|
||||||
<polygon points={ar} fill={`url(#g_${l.key})`} />
|
<path d={vlak} fill={`url(#g_${l.key})`} />
|
||||||
<polyline points={ls} fill="none" stroke={l.color} strokeWidth="2.5"
|
<path d={lijn} fill="none" stroke={l.color} strokeWidth="2.5"
|
||||||
strokeLinejoin="round" strokeLinecap="round" />
|
strokeLinejoin="round" strokeLinecap="round" />
|
||||||
{validPts.map((p, i) => {
|
{validPts.map((p, i) => {
|
||||||
const x = xs[i], y = ys[i];
|
const x = xs[i], y = ys[i];
|
||||||
const isHov = hovered?.lineKey === l.key && hovered?.ptIdx === 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 tipX = Math.min(Math.max(x - tipW / 2, pl), W - tipW - 4);
|
||||||
const tipY = y - tipH - 8 < pt2 ? y + 10 : y - tipH - 8;
|
const tipY = y - tipH - 8 < pt2 ? y + 10 : y - tipH - 8;
|
||||||
return (
|
return (
|
||||||
@@ -84,10 +94,14 @@ function SvgLineChart({ pts, lines, T, H = 200 }) {
|
|||||||
fill={l.color} stroke={T.card} strokeWidth="2" />
|
fill={l.color} stroke={T.card} strokeWidth="2" />
|
||||||
{isHov && (
|
{isHov && (
|
||||||
<g>
|
<g>
|
||||||
<rect x={tipX} y={tipY} width={tipW} height={22}
|
<rect x={tipX} y={tipY} width={tipW} height={tipH}
|
||||||
rx="5" fill={T.card}
|
rx="5" fill={T.card}
|
||||||
stroke={l.color} strokeWidth="1" />
|
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">
|
fontWeight="700" textAnchor="middle">
|
||||||
{fmt(p[l.key])}
|
{fmt(p[l.key])}
|
||||||
</text>
|
</text>
|
||||||
@@ -99,15 +113,24 @@ function SvgLineChart({ pts, lines, T, H = 200 }) {
|
|||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{pts.map((p, i) => (
|
{/* X-as: korte maandlabels, rechtop en uitgedund op wat er past. Elk
|
||||||
<text key={i}
|
punt een volledige datum geven liep bij een lange reeks over in een
|
||||||
x={xp(i)} y={H - pb + 16}
|
onleesbare band. De exacte dag staat in de tooltip op elk punt. */}
|
||||||
fill={T.muted} fontSize="8"
|
{pts.map((p, i) => {
|
||||||
textAnchor="end"
|
const laatste = i === pts.length - 1;
|
||||||
transform={`rotate(-45, ${xp(i)}, ${H - pb + 16})`}>
|
// Terugtellen vanaf het eind, zodat het laatste punt op een stap valt.
|
||||||
{fmtDatum(p.datum)}
|
if ((pts.length - 1 - i) % labelStap !== 0) 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>
|
</text>
|
||||||
))}
|
</g>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</svg>
|
</svg>
|
||||||
<div style={{ display: "flex", gap: 20, justifyContent: "center", marginTop: 10 }}>
|
<div style={{ display: "flex", gap: 20, justifyContent: "center", marginTop: 10 }}>
|
||||||
{lines.map((l) => (
|
{lines.map((l) => (
|
||||||
|
|||||||
@@ -14,6 +14,21 @@ export const fmtDatum = (s) => {
|
|||||||
return `${d}-${m}-${y}`;
|
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) */
|
/** Crypto-bedrag formatter (4 decimalen standaard) */
|
||||||
export const fmtCrypto = (n, d = 4) => (parseFloat(n) || 0).toFixed(d);
|
export const fmtCrypto = (n, d = 4) => (parseFloat(n) || 0).toFixed(d);
|
||||||
|
|
||||||
|
|||||||
@@ -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,2 +1,3 @@
|
|||||||
export * from "./format.js";
|
export * from "./format.js";
|
||||||
export * from "./storage.js";
|
export * from "./storage.js";
|
||||||
|
export * from "./grafiek.js";
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vibefinance",
|
"name": "vibefinance",
|
||||||
"version": "1.0.8",
|
"version": "1.0.10",
|
||||||
"description": "VibeFinance — persoonlijk vermogensbeheer",
|
"description": "VibeFinance — persoonlijk vermogensbeheer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance",
|
"repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance",
|
||||||
|
|||||||
Reference in New Issue
Block a user