nova-shell/shell/applets/SparklineCanvas.qml

162 lines
5.5 KiB
QML

import QtQuick
Canvas {
id: root
// Data - array of numeric values. Set maxValue/minValue to null for auto-range.
property var history: []
property bool active: true
property var maxValue: 100
property var minValue: 0
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)
// Stroke line color on top of the filled area
property color strokeColor: "white"
property real lineWidth: 1.5
// 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()
onStrokeColorChanged: if (root.active)
requestPaint()
onVisibleChanged: if (visible)
requestPaint()
onPaint: {
const ctx = getContext("2d");
if (!ctx)
return;
ctx.clearRect(0, 0, width, height);
const d = history;
if (!d.length)
return;
const n = d.length;
const lo = _min;
const hi = _max;
const range = hi - lo || 1;
function yOf(v) {
return height - height * ((v - lo) / range);
}
// x-axis mapping: returns [x, barWidth] for data index i
const k = logScale ? logCurve : 0;
const expK = k > 0 ? Math.exp(k) - 1 : 0;
function xOf(i) {
if (k > 0) {
const x0 = width * (Math.exp(k * i / n) - 1) / expK;
const x1 = width * (Math.exp(k * (i + 1) / n) - 1) / expK;
return [x0, x1 - x0];
}
const step = width / n;
return [i * step, step];
}
// 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) {
ctx.globalAlpha = 0.3;
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
for (let t = 0; t < thresholds.length; t++) {
const th = thresholds[t];
ctx.strokeStyle = th.color.toString();
ctx.beginPath();
ctx.moveTo(0, yOf(th.value));
ctx.lineTo(width, yOf(th.value));
ctx.stroke();
}
ctx.setLineDash([]);
ctx.globalAlpha = 1.0;
}
// Zero line when range spans negative values
if (lo < 0 && hi > 0) {
const zy = yOf(0);
ctx.strokeStyle = Qt.rgba(strokeColor.r, strokeColor.g, strokeColor.b, 0.2).toString();
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, zy);
ctx.lineTo(width, zy);
ctx.stroke();
}
// 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();
}
}