sparkline: unified coloring - vertical gradient from colorAt/thresholds, stroke line, 1px border, consistent 32px height

This commit is contained in:
Damocles 2026-04-25 10:40:51 +02:00
parent e1d20c2407
commit c64373313d
6 changed files with 90 additions and 93 deletions

View file

@ -86,18 +86,16 @@ Column {
} }
} }
// 24h history sparkline (area chart) // 24h charge history sparkline
SparklineCanvas { SparklineCanvas {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 12 anchors.leftMargin: 12
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 12 anchors.rightMargin: 12
height: 44 height: 32
history: S.BatteryService.history history: S.BatteryService.history
color: root._stateColor strokeColor: root.accentColor
active: root.active active: root.active
backgroundTint: 0.07
areaMode: true
thresholds: [ thresholds: [
{ {
value: S.BatteryService.warnThresh, value: S.BatteryService.warnThresh,
@ -160,12 +158,11 @@ Column {
anchors.rightMargin: 12 anchors.rightMargin: 12
height: 24 height: 24
history: S.BatteryService.rateHistory history: S.BatteryService.rateHistory
color: root._stateColor strokeColor: root._stateColor
colorAt: v => v >= 0 ? S.Theme.base0B : root._stateColor
active: root.active active: root.active
maxValue: null maxValue: null
minValue: null minValue: null
backgroundTint: 0.08
colorFunction: v => v >= 0 ? S.Theme.base0B : root._stateColor
} }
// Health row // Health row

View file

@ -104,7 +104,8 @@ Column {
width: 32 width: 32
height: 10 height: 10
history: root.cores[parent.parent.index]?.history ?? [] history: root.cores[parent.parent.index]?.history ?? []
color: parent.parent._barColor strokeColor: parent.parent._barColor
colorAt: v => S.Theme.loadColor(v)
active: root.active active: root.active
} }

View file

@ -81,10 +81,11 @@ Column {
anchors.leftMargin: 12 anchors.leftMargin: 12
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 12 anchors.rightMargin: 12
height: 36 height: 32
history: S.SystemStats.gpuHistory history: S.SystemStats.gpuHistory
strokeColor: root.accentColor
colorAt: v => S.Theme.loadColor(v)
active: root.active active: root.active
colorFunction: v => S.Theme.loadColor(v)
} }
// VRAM section // VRAM section

View file

@ -76,11 +76,11 @@ Column {
anchors.leftMargin: 12 anchors.leftMargin: 12
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 12 anchors.rightMargin: 12
height: 18 height: 32
history: S.SystemStats.memHistory history: S.SystemStats.memHistory
color: root.accentColor strokeColor: root.accentColor
colorAt: v => S.Theme.loadColor(v)
active: root.active active: root.active
backgroundTint: 0.15
} }
// Breakdown rows // Breakdown rows

View file

@ -5,7 +5,6 @@ Canvas {
// Data - array of numeric values. Set maxValue/minValue to null for auto-range. // Data - array of numeric values. Set maxValue/minValue to null for auto-range.
property var history: [] property var history: []
property color color: "white"
property bool active: true property bool active: true
property var maxValue: 100 property var maxValue: 100
property var minValue: 0 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 _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) 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. // Stroke line color on top of the filled area
// logCurve controls strength (0 = linear, 3 = strong compression). Only visual - does not affect data. property color strokeColor: "white"
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
property real lineWidth: 1.5 property real lineWidth: 1.5
// Per-bar color override: function(value) => color. Null = uniform `color`. // Maps a data value to a fill color. Applied as a vertical gradient.
property var colorFunction: null // 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: "#..."}] // Horizontal threshold lines: [{value: 70, color: "#..."}]
property var thresholds: [] property var thresholds: []
onHistoryChanged: if (root.active) onHistoryChanged: if (root.active)
requestPaint() requestPaint()
onColorChanged: if (root.active) onStrokeColorChanged: if (root.active)
requestPaint() requestPaint()
onVisibleChanged: if (visible) onVisibleChanged: if (visible)
@ -46,7 +46,7 @@ Canvas {
return; return;
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
const d = history; const d = history;
if (!d.length || (areaMode && d.length < 2)) if (!d.length)
return; return;
const n = d.length; const n = d.length;
@ -70,11 +70,10 @@ Canvas {
return [i * step, step]; return [i * step, step];
} }
// Background tint // 1px border
if (backgroundTint > 0) { ctx.strokeStyle = Qt.rgba(strokeColor.r, strokeColor.g, strokeColor.b, 0.15).toString();
ctx.fillStyle = Qt.rgba(color.r, color.g, color.b, backgroundTint).toString(); ctx.lineWidth = 1;
ctx.fillRect(0, 0, width, height); ctx.strokeRect(0.5, 0.5, width - 1, height - 1);
}
// Threshold lines // Threshold lines
if (thresholds.length) { if (thresholds.length) {
@ -96,7 +95,7 @@ Canvas {
// Zero line when range spans negative values // Zero line when range spans negative values
if (lo < 0 && hi > 0) { if (lo < 0 && hi > 0) {
const zy = yOf(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.lineWidth = 1;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, zy); ctx.moveTo(0, zy);
@ -104,37 +103,7 @@ Canvas {
ctx.stroke(); ctx.stroke();
} }
if (areaMode) { // Build polygon path
// 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; const baseY = lo < 0 ? yOf(0) : height;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, baseY); ctx.moveTo(0, baseY);
@ -146,17 +115,48 @@ Canvas {
} }
ctx.lineTo(width, baseY); ctx.lineTo(width, baseY);
ctx.closePath(); ctx.closePath();
if (hasCF) {
const grad = ctx.createLinearGradient(0, 0, width, 0); // Vertical gradient fill
for (let i = 0; i < n; i++) { const grad = ctx.createLinearGradient(0, height, 0, 0);
const [bx, bw] = xOf(i); if (colorAt) {
grad.addColorStop(Math.min(1, (bx + bw / 2) / width), colorFunction(d[i]).toString()); // 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.fillStyle = grad;
} else {
ctx.fillStyle = color.toString();
}
ctx.fill(); 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();
} }
} }

View file

@ -109,11 +109,10 @@ Column {
anchors.leftMargin: 12 anchors.leftMargin: 12
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 12 anchors.rightMargin: 12
height: 40 height: 32
history: root.history history: root.history
color: root.stateColor strokeColor: root.accentColor
active: root.active active: root.active
backgroundTint: 0.08
thresholds: [ thresholds: [
{ {
value: root.warm, value: root.warm,
@ -124,7 +123,6 @@ Column {
color: S.Theme.base08 color: S.Theme.base08
} }
] ]
colorFunction: v => v > root.hot ? S.Theme.base08 : v > root.warm ? S.Theme.base0A : root.stateColor
} }
// Threshold labels // Threshold labels