From c64373313dabafdd50c3a8348531ce2cd3389d1a Mon Sep 17 00:00:00 2001 From: Damocles Date: Sat, 25 Apr 2026 10:40:51 +0200 Subject: [PATCH] sparkline: unified coloring - vertical gradient from colorAt/thresholds, stroke line, 1px border, consistent 32px height --- shell/applets/BatteryApplet.qml | 13 +-- shell/applets/CpuApplet.qml | 3 +- shell/applets/GpuApplet.qml | 5 +- shell/applets/MemoryApplet.qml | 6 +- shell/applets/SparklineCanvas.qml | 150 ++++++++++++++-------------- shell/applets/TemperatureApplet.qml | 6 +- 6 files changed, 90 insertions(+), 93 deletions(-) diff --git a/shell/applets/BatteryApplet.qml b/shell/applets/BatteryApplet.qml index 327c43a..66800a4 100644 --- a/shell/applets/BatteryApplet.qml +++ b/shell/applets/BatteryApplet.qml @@ -86,18 +86,16 @@ Column { } } - // 24h history sparkline (area chart) + // 24h charge history sparkline SparklineCanvas { anchors.left: parent.left anchors.leftMargin: 12 anchors.right: parent.right anchors.rightMargin: 12 - height: 44 + height: 32 history: S.BatteryService.history - color: root._stateColor + strokeColor: root.accentColor active: root.active - backgroundTint: 0.07 - areaMode: true thresholds: [ { value: S.BatteryService.warnThresh, @@ -160,12 +158,11 @@ Column { anchors.rightMargin: 12 height: 24 history: S.BatteryService.rateHistory - color: root._stateColor + strokeColor: root._stateColor + colorAt: v => v >= 0 ? S.Theme.base0B : root._stateColor active: root.active maxValue: null minValue: null - backgroundTint: 0.08 - colorFunction: v => v >= 0 ? S.Theme.base0B : root._stateColor } // Health row diff --git a/shell/applets/CpuApplet.qml b/shell/applets/CpuApplet.qml index 1ea97ec..2068cce 100644 --- a/shell/applets/CpuApplet.qml +++ b/shell/applets/CpuApplet.qml @@ -104,7 +104,8 @@ Column { width: 32 height: 10 history: root.cores[parent.parent.index]?.history ?? [] - color: parent.parent._barColor + strokeColor: parent.parent._barColor + colorAt: v => S.Theme.loadColor(v) active: root.active } diff --git a/shell/applets/GpuApplet.qml b/shell/applets/GpuApplet.qml index 1d10648..09fab4d 100644 --- a/shell/applets/GpuApplet.qml +++ b/shell/applets/GpuApplet.qml @@ -81,10 +81,11 @@ Column { anchors.leftMargin: 12 anchors.right: parent.right anchors.rightMargin: 12 - height: 36 + height: 32 history: S.SystemStats.gpuHistory + strokeColor: root.accentColor + colorAt: v => S.Theme.loadColor(v) active: root.active - colorFunction: v => S.Theme.loadColor(v) } // VRAM section diff --git a/shell/applets/MemoryApplet.qml b/shell/applets/MemoryApplet.qml index fe69183..065e686 100644 --- a/shell/applets/MemoryApplet.qml +++ b/shell/applets/MemoryApplet.qml @@ -76,11 +76,11 @@ Column { anchors.leftMargin: 12 anchors.right: parent.right anchors.rightMargin: 12 - height: 18 + height: 32 history: S.SystemStats.memHistory - color: root.accentColor + strokeColor: root.accentColor + colorAt: v => S.Theme.loadColor(v) active: root.active - backgroundTint: 0.15 } // Breakdown rows diff --git a/shell/applets/SparklineCanvas.qml b/shell/applets/SparklineCanvas.qml index 8f32f47..8f73858 100644 --- a/shell/applets/SparklineCanvas.qml +++ b/shell/applets/SparklineCanvas.qml @@ -5,7 +5,6 @@ Canvas { // Data - array of numeric values. Set maxValue/minValue to null for auto-range. property var history: [] - property color color: "white" property bool active: true property var maxValue: 100 property var minValue: 0 @@ -13,28 +12,29 @@ Canvas { readonly property real _max: maxValue !== null && maxValue !== undefined ? maxValue : (history.length ? Math.max(...history) : 100) readonly property real _min: minValue !== null && minValue !== undefined ? minValue : (history.length ? Math.min(...history) : 0) - // Logarithmic x-axis: compresses old data on the left, expands recent data on the right. - // logCurve controls strength (0 = linear, 3 = strong compression). Only visual - does not affect data. - property bool logScale: false - property real logCurve: 3 - - // Optional background tint using `color` at low opacity - property real backgroundTint: 0 // 0 = off, e.g. 0.15 for memory, 0.08 for temperature - - // Area chart mode (battery-style filled curve + stroke line) - property bool areaMode: false - property real areaOpacity: 0.18 + // Stroke line color on top of the filled area + property color strokeColor: "white" property real lineWidth: 1.5 - // Per-bar color override: function(value) => color. Null = uniform `color`. - property var colorFunction: null + // Maps a data value to a fill color. Applied as a vertical gradient. + // When null, auto-derived from thresholds as a smooth gradient + // (strokeColor below first threshold, blending through threshold colors). + // Set explicitly to override. + // qmllint disable use-proper-function + property var colorAt: null + // qmllint enable use-proper-function + + // Logarithmic x-axis: compresses old data on the left, expands recent data on the right. + // logCurve controls strength (0 = linear, 3 = strong compression). Only visual. + property bool logScale: false + property real logCurve: 3 // Horizontal threshold lines: [{value: 70, color: "#..."}] property var thresholds: [] onHistoryChanged: if (root.active) requestPaint() - onColorChanged: if (root.active) + onStrokeColorChanged: if (root.active) requestPaint() onVisibleChanged: if (visible) @@ -46,7 +46,7 @@ Canvas { return; ctx.clearRect(0, 0, width, height); const d = history; - if (!d.length || (areaMode && d.length < 2)) + if (!d.length) return; const n = d.length; @@ -70,11 +70,10 @@ Canvas { return [i * step, step]; } - // Background tint - if (backgroundTint > 0) { - ctx.fillStyle = Qt.rgba(color.r, color.g, color.b, backgroundTint).toString(); - ctx.fillRect(0, 0, width, height); - } + // 1px border + ctx.strokeStyle = Qt.rgba(strokeColor.r, strokeColor.g, strokeColor.b, 0.15).toString(); + ctx.lineWidth = 1; + ctx.strokeRect(0.5, 0.5, width - 1, height - 1); // Threshold lines if (thresholds.length) { @@ -96,7 +95,7 @@ Canvas { // Zero line when range spans negative values if (lo < 0 && hi > 0) { const zy = yOf(0); - ctx.strokeStyle = Qt.rgba(color.r, color.g, color.b, 0.2).toString(); + ctx.strokeStyle = Qt.rgba(strokeColor.r, strokeColor.g, strokeColor.b, 0.2).toString(); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, zy); @@ -104,59 +103,60 @@ Canvas { ctx.stroke(); } - if (areaMode) { - // Filled area under curve - const baseY = lo < 0 ? yOf(0) : height; - const [x0] = xOf(0); - ctx.beginPath(); - ctx.moveTo(x0, baseY); - for (let i = 0; i < n; i++) { - const [x] = xOf(i); - ctx.lineTo(x, yOf(d[i])); - } - const [xLast] = xOf(n - 1); - ctx.lineTo(xLast, baseY); - ctx.closePath(); - ctx.fillStyle = Qt.rgba(color.r, color.g, color.b, areaOpacity).toString(); - ctx.fill(); - - // Top stroke - ctx.beginPath(); - for (let i = 0; i < n; i++) { - const [x] = xOf(i); - if (i === 0) - ctx.moveTo(x, yOf(d[i])); - else - ctx.lineTo(x, yOf(d[i])); - } - ctx.strokeStyle = color.toString(); - ctx.lineWidth = root.lineWidth; - ctx.stroke(); - } else { - // Filled area - continuous polygon, no visible bar boundaries - const hasCF = typeof colorFunction === "function"; - const baseY = lo < 0 ? yOf(0) : height; - ctx.beginPath(); - ctx.moveTo(0, baseY); - for (let i = 0; i < n; i++) { - const [bx, bw] = xOf(i); - const y = yOf(d[i]); - ctx.lineTo(bx, y); - ctx.lineTo(bx + bw, y); - } - ctx.lineTo(width, baseY); - ctx.closePath(); - if (hasCF) { - const grad = ctx.createLinearGradient(0, 0, width, 0); - for (let i = 0; i < n; i++) { - const [bx, bw] = xOf(i); - grad.addColorStop(Math.min(1, (bx + bw / 2) / width), colorFunction(d[i]).toString()); - } - ctx.fillStyle = grad; - } else { - ctx.fillStyle = color.toString(); - } - ctx.fill(); + // Build polygon path + const baseY = lo < 0 ? yOf(0) : height; + ctx.beginPath(); + ctx.moveTo(0, baseY); + for (let i = 0; i < n; i++) { + const [bx, bw] = xOf(i); + const y = yOf(d[i]); + ctx.lineTo(bx, y); + ctx.lineTo(bx + bw, y); } + ctx.lineTo(width, baseY); + ctx.closePath(); + + // Vertical gradient fill + const grad = ctx.createLinearGradient(0, height, 0, 0); + if (colorAt) { + // Explicit colorAt: sample across the value range + const steps = 8; + for (let s = 0; s <= steps; s++) { + const frac = s / steps; + grad.addColorStop(frac, colorAt(lo + frac * range).toString()); + } + } else if (thresholds.length) { + // Auto-derive from thresholds as smooth gradient + const sorted = thresholds.slice().sort((a, b) => a.value - b.value); + grad.addColorStop(0, strokeColor.toString()); + for (const th of sorted) { + const frac = Math.max(0, Math.min(1, (th.value - lo) / range)); + if (frac > 0) + grad.addColorStop(Math.max(0, frac - 0.01), strokeColor.toString()); + grad.addColorStop(frac, th.color.toString()); + } + grad.addColorStop(1, sorted[sorted.length - 1].color.toString()); + } else { + // Uniform fill + grad.addColorStop(0, strokeColor.toString()); + grad.addColorStop(1, strokeColor.toString()); + } + ctx.fillStyle = grad; + ctx.fill(); + + // Stroke line on top + ctx.beginPath(); + for (let i = 0; i < n; i++) { + const [bx, bw] = xOf(i); + const y = yOf(d[i]); + if (i === 0) + ctx.moveTo(bx, y); + else + ctx.lineTo(bx, y); + ctx.lineTo(bx + bw, y); + } + ctx.strokeStyle = strokeColor.toString(); + ctx.lineWidth = root.lineWidth; + ctx.stroke(); } } diff --git a/shell/applets/TemperatureApplet.qml b/shell/applets/TemperatureApplet.qml index 45534e5..1e5b9a1 100644 --- a/shell/applets/TemperatureApplet.qml +++ b/shell/applets/TemperatureApplet.qml @@ -109,11 +109,10 @@ Column { anchors.leftMargin: 12 anchors.right: parent.right anchors.rightMargin: 12 - height: 40 + height: 32 history: root.history - color: root.stateColor + strokeColor: root.accentColor active: root.active - backgroundTint: 0.08 thresholds: [ { value: root.warm, @@ -124,7 +123,6 @@ Column { color: S.Theme.base08 } ] - colorFunction: v => v > root.hot ? S.Theme.base08 : v > root.warm ? S.Theme.base0A : root.stateColor } // Threshold labels