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(); } // Smooth curve through sample midpoints function midX(i) { const [bx, bw] = xOf(i); return bx + bw / 2; } function traceCurve() { ctx.moveTo(0, yOf(d[0])); if (n === 1) { ctx.lineTo(width, yOf(d[0])); } else { ctx.lineTo(midX(0), yOf(d[0])); for (let i = 0; i < n - 1; i++) { const mx = (midX(i) + midX(i + 1)) / 2; ctx.quadraticCurveTo(midX(i), yOf(d[i]), mx, (yOf(d[i]) + yOf(d[i + 1])) / 2); } ctx.lineTo(midX(n - 1), yOf(d[n - 1])); ctx.lineTo(width, yOf(d[n - 1])); } } // Filled polygon const baseY = lo < 0 ? yOf(0) : height; ctx.beginPath(); ctx.moveTo(0, baseY); traceCurve(); ctx.lineTo(width, baseY); ctx.closePath(); // Vertical gradient fill const grad = ctx.createLinearGradient(0, height, 0, 0); if (colorAt) { 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) { 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 { grad.addColorStop(0, strokeColor.toString()); grad.addColorStop(1, strokeColor.toString()); } ctx.fillStyle = grad; ctx.fill(); // Stroke line on top ctx.beginPath(); traceCurve(); ctx.strokeStyle = strokeColor.toString(); ctx.lineWidth = root.lineWidth; ctx.stroke(); } }