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); } } } }