nova-shell/shell/applets/SparklineCanvas.qml

156 lines
5.4 KiB
QML

import QtQuick
Canvas {
id: root
// 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
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)
// Max x-axis slots - bars get narrower as data fills up, then oldest drops off
property int maxSamples: 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
property real lineWidth: 1.5
// Per-bar color override: function(value) => color. Null = uniform `color`.
property var colorFunction: null
// Horizontal threshold lines: [{value: 70, color: "#..."}]
property var thresholds: []
onHistoryChanged: if (root.active)
requestPaint()
onColorChanged: 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 || (areaMode && d.length < 2))
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 / (maxSamples > n ? maxSamples : n);
const off = maxSamples > n ? (maxSamples - n) * step : 0;
return [off + 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);
}
// 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(color.r, color.g, color.b, 0.2).toString();
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, zy);
ctx.lineTo(width, zy);
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 {
// Bar chart - bars grow from zero line (or bottom if all positive)
const hasCF = typeof colorFunction === "function";
const zy = lo < 0 ? yOf(0) : height;
if (!hasCF)
ctx.fillStyle = color.toString();
for (let i = 0; i < n; i++) {
if (hasCF)
ctx.fillStyle = colorFunction(d[i]).toString();
const [bx, bw] = xOf(i);
const dy = yOf(d[i]);
const barTop = Math.min(dy, zy);
const barH = Math.max(1, Math.abs(dy - zy));
ctx.fillRect(bx, barTop, Math.max(1, bw - 0.5), barH);
}
}
}
}