diff --git a/CHANGELOG.md b/CHANGELOG.md index 663ad0f..f45b639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [5.0.3] — 2026-07-20 + +### Fixed (dashboard) +- **Storage Growth chart redesigned.** The previous chart used + `preserveAspectRatio="none"`, which stretched the viewBox and rendered blurry, + non-uniformly-scaled strokes and text. It now renders at the container's exact + pixel size (1 unit = 1 px) for crisp output, with a **smooth gradient area + curve** (Catmull-Rom bezier), light gridlines, MB y-axis labels, and dated + x-axis ticks. Interactive hover/focus tooltip retained. +- Updated `dashboard-insights.svg` and USERGUIDE §15A to match. + +--- + ## [5.0.2] — 2026-07-20 ### Changed (dashboard UX) diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index e16304f..2068c21 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -963,7 +963,7 @@ Directly under the stat cards, the **System Health** grid gives a traffic-light ### Storage Growth chart -An inline SVG area chart plots the size (MB) of the last 10 backup sessions oldest→newest. **Hover (or keyboard-focus) any point** to see an interactive tooltip with that session's name, size, delta mode (full/delta/unchanged), and date; the point enlarges to mark the active selection. Use it to spot steady growth (plan capacity) or sudden drops (investigate with Session Diff). +A modern, gradient-filled **smooth area chart** plots the size (MB) of the last 10 backup sessions oldest→newest, with light gridlines, MB y-axis labels, and dated x-axis ticks. It renders at the container's exact pixel size (1 SVG unit = 1 px) so strokes and text stay crisp at any width — no stretching. **Hover (or keyboard-focus) any point** to see an interactive tooltip with that session's name, size, delta mode (full/delta/unchanged), and date; the point enlarges to mark the active selection. Use it to spot steady growth (plan capacity) or sudden drops (investigate with Session Diff). ### Backup Timeline heatmap diff --git a/docs/index.html b/docs/index.html index 6302d82..7fc09e6 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1046,16 +1046,16 @@ .health-val { font-size: 13.5px; font-weight: 700; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .health-arrow { width: 15px; height: 15px; flex-shrink: 0; color: var(--muted-light); opacity: 0; transform: translateX(-3px); transition: opacity var(--t), transform var(--t), color var(--t); } - /* ── Storage growth area chart ────────────────────────────────────────── */ - .area-chart { width: 100%; height: 140px; display: block; } - .area-chart .grid-line { stroke: var(--border); stroke-width: 1; } - .area-chart .area-fill { fill: var(--accent); opacity: .12; } - .area-chart .area-line { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; } - .area-chart .area-dot { fill: var(--bg-card); stroke: var(--accent); stroke-width: 2; transition: r .12s ease; } - .area-chart .area-dot.active { r: 5; fill: var(--accent); } - .area-chart .area-hit { fill: transparent; cursor: pointer; outline: none; } - .area-chart .area-hit:focus-visible + text, .area-chart .area-hit:focus-visible { stroke: var(--accent); } - .area-chart text { fill: var(--muted); font-size: 10px; } + /* ── Storage growth area chart (crisp, 1 unit = 1px) ──────────────────── */ + .sg-chart { display: block; width: 100%; overflow: visible; } + .sg-grid { stroke: var(--border); stroke-width: 1; shape-rendering: crispEdges; opacity: .7; } + .sg-area { stroke: none; } + .sg-line { fill: none; stroke: var(--accent); stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; } + .sg-dot { fill: var(--bg-card); stroke: var(--accent); stroke-width: 2; transition: r .12s ease; } + .sg-dot.active { r: 5.5; fill: var(--accent); } + .sg-hit { fill: transparent; cursor: pointer; outline: none; } + .sg-ylabel, .sg-xlabel, .sg-unit { fill: var(--muted); font-size: 11px; font-family: inherit; } + .sg-unit { font-weight: 700; } /* Interactive chart tooltip */ .chart-tip { position: absolute; z-index: 5; transform: translate(-50%, calc(-100% - 12px)); @@ -3124,30 +3124,81 @@
Not enough session data to chart.
'; return; } - const W = 380, H = 140, padL = 34, padB = 20, padT = 10, padR = 8; + + // Render at the container's real pixel size so 1 unit == 1px — no stretching, + // so strokes and text stay crisp (the old preserveAspectRatio="none" distorted them). + el.style.position = 'relative'; + // viewBox width == the element's real pixel width → exact 1:1 mapping (crisp, + // no letterboxing or non-uniform text scaling). + const W = Math.round(el.getBoundingClientRect().width) || 420; + const H = 180; + const padL = 40, padT = 16, padB = 30, padR = 14; const iw = W - padL - padR, ih = H - padT - padB; - const max = Math.max(...data.map(d => d.sizeMB)) * 1.1 || 1; + const uid = 'sg' + Math.random().toString(36).slice(2, 8); + + const maxVal = Math.max(...data.map(d => d.sizeMB)); + const max = Math.max(1, Math.ceil(maxVal * 1.15 / 50) * 50); // round headroom to 50 MB const x = i => padL + (iw * i / (data.length - 1)); const y = v => padT + ih - (ih * v / max); const pts = data.map((d, i) => [x(i), y(d.sizeMB)]); - const line = pts.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' '); - const area = `M${pts[0][0].toFixed(1)} ${(padT+ih).toFixed(1)} ` + pts.map(p => 'L'+p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' ') + ` L${pts[pts.length-1][0].toFixed(1)} ${(padT+ih).toFixed(1)} Z`; - const gridY = [0, 0.5, 1].map(f => { const gy = padT + ih - ih*f; return `