162 lines
5.5 KiB
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();
|
|
}
|
|
}
|