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 (0 = use history.length, right-aligns shorter data) property int maxSamples: 0 // 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 samples = maxSamples > 0 ? maxSamples : d.length; const step = width / samples; const offset = samples - d.length; const lo = _min; const hi = _max; const range = hi - lo || 1; function yOf(v) { return height - height * ((v - lo) / range); } // 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 xOff = offset * step; const baseY = lo < 0 ? yOf(0) : height; ctx.beginPath(); ctx.moveTo(xOff, baseY); for (let i = 0; i < d.length; i++) { ctx.lineTo(xOff + i * step, yOf(d[i])); } ctx.lineTo(xOff + (d.length - 1) * step, 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 < d.length; i++) { const x = xOff + i * step; 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 < d.length; i++) { if (hasCF) ctx.fillStyle = colorFunction(d[i]).toString(); const dy = yOf(d[i]); const barTop = Math.min(dy, zy); const barH = Math.max(1, Math.abs(dy - zy)); ctx.fillRect((offset + i) * step, barTop, Math.max(1, step - 0.5), barH); } } } }