extract SparklineCanvas component from 5 applets

This commit is contained in:
Damocles 2026-04-24 00:43:40 +02:00
parent 732a14e5cb
commit 8d76df6ef5
8 changed files with 178 additions and 233 deletions

View file

@ -87,91 +87,28 @@ Column {
} }
// 24h history sparkline (area chart) // 24h history sparkline (area chart)
Canvas { SparklineCanvas {
id: _sparkline
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 12 anchors.leftMargin: 12
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 12 anchors.rightMargin: 12
height: 44 height: 44
history: S.BatteryService.history
property var _hist: S.BatteryService.history color: root._stateColor
property color _col: root._stateColor active: root.active
maxSamples: 1440
on_HistChanged: if (root.active) backgroundTint: 0.07
requestPaint() areaMode: true
on_ColChanged: if (root.active) thresholds: [
requestPaint() {
value: S.BatteryService.warnThresh,
onVisibleChanged: if (visible) color: S.Theme.base0A
requestPaint() },
{
onPaint: { value: S.BatteryService.critThresh,
const ctx = getContext("2d"); color: S.Theme.base08
if (!ctx)
return;
ctx.clearRect(0, 0, width, height);
const d = _hist;
if (d.length < 2)
return;
const maxSamples = 1440;
const xScale = width / maxSamples;
const xOffset = (maxSamples - d.length) * xScale;
// Background tint
ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.07).toString();
ctx.fillRect(0, 0, width, height);
// Warning threshold line
const warnY = height - height * (S.BatteryService.warnThresh / 100);
ctx.strokeStyle = S.Theme.base0A.toString();
ctx.globalAlpha = 0.3;
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.beginPath();
ctx.moveTo(0, warnY);
ctx.lineTo(width, warnY);
ctx.stroke();
// Critical threshold line
const critY = height - height * (S.BatteryService.critThresh / 100);
ctx.strokeStyle = S.Theme.base08.toString();
ctx.beginPath();
ctx.moveTo(0, critY);
ctx.lineTo(width, critY);
ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1.0;
// Filled area under the curve
ctx.beginPath();
ctx.moveTo(xOffset, height);
for (let i = 0; i < d.length; i++) {
const x = xOffset + i * xScale;
const y = height - height * (d[i] / 100);
ctx.lineTo(x, y);
}
ctx.lineTo(xOffset + (d.length - 1) * xScale, height);
ctx.closePath();
ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.18).toString();
ctx.fill();
// Top line
ctx.beginPath();
for (let i = 0; i < d.length; i++) {
const x = xOffset + i * xScale;
const y = height - height * (d[i] / 100);
if (i === 0)
ctx.moveTo(x, y);
else
ctx.lineTo(x, y);
}
ctx.strokeStyle = _col.toString();
ctx.lineWidth = 1.5;
ctx.stroke();
} }
]
} }
// Footer: thresholds + time label // Footer: thresholds + time label

View file

@ -96,41 +96,16 @@ Column {
} }
} }
// Sparkline SparklineCanvas {
Canvas {
id: sparkline id: sparkline
anchors.right: freqLabel.left anchors.right: freqLabel.left
anchors.rightMargin: 6 anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 32 width: 32
height: 10 height: 10
history: root.cores[parent.parent.index]?.history ?? []
property var _hist: root.cores[parent.parent.index]?.history ?? [] color: parent.parent._barColor
property color _col: parent.parent._barColor active: root.active
on_HistChanged: if (root.active)
requestPaint()
on_ColChanged: if (root.active)
requestPaint()
onVisibleChanged: if (visible)
requestPaint()
onPaint: {
const ctx = getContext("2d");
if (!ctx)
return;
ctx.clearRect(0, 0, width, height);
const d = _hist;
if (!d.length)
return;
const bw = width / d.length;
ctx.fillStyle = _col.toString();
for (let i = 0; i < d.length; i++) {
const h = Math.max(1, height * d[i] / 100);
ctx.fillRect(i * bw, height - h, Math.max(1, bw - 0.5), h);
}
}
} }
Text { Text {

View file

@ -76,40 +76,16 @@ Column {
} }
// Usage history sparkline // Usage history sparkline
Canvas { SparklineCanvas {
id: _sparkline
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 12 anchors.leftMargin: 12
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 12 anchors.rightMargin: 12
height: 36 height: 36
history: S.SystemStats.gpuHistory
property var _hist: S.SystemStats.gpuHistory active: root.active
maxSamples: 60
on_HistChanged: if (root.active) colorFunction: v => S.Theme.loadColor(v)
requestPaint()
onVisibleChanged: if (visible)
requestPaint()
onPaint: {
const ctx = getContext("2d");
if (!ctx)
return;
ctx.clearRect(0, 0, width, height);
const d = _hist;
if (!d.length)
return;
const maxSamples = 60;
const bw = width / maxSamples;
const offset = maxSamples - d.length;
for (let i = 0; i < d.length; i++) {
const barH = Math.max(1, height * d[i] / 100);
const col = S.Theme.loadColor(d[i]);
ctx.fillStyle = col.toString();
ctx.fillRect((offset + i) * bw, height - barH, Math.max(1, bw - 0.5), barH);
}
}
} }
// VRAM section // VRAM section

View file

@ -71,49 +71,36 @@ Column {
} }
// Memory history sparkline // Memory history sparkline
Canvas { SparklineCanvas {
id: memSparkline
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 12 anchors.leftMargin: 12
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 12 anchors.rightMargin: 12
height: 18 height: 18
history: S.SystemStats.memHistory
property var _hist: S.SystemStats.memHistory color: root.accentColor
property color _col: root.accentColor active: root.active
maxSamples: 30
on_HistChanged: if (root.active) backgroundTint: 0.15
requestPaint()
on_ColChanged: if (root.active)
requestPaint()
onVisibleChanged: if (visible)
requestPaint()
onPaint: {
const ctx = getContext("2d");
if (!ctx)
return;
ctx.clearRect(0, 0, width, height);
const d = _hist;
if (!d.length)
return;
const bw = width / 30;
ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.15).toString();
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = _col.toString();
for (let i = 0; i < d.length; i++) {
const h = Math.max(1, height * d[i] / 100);
ctx.fillRect((30 - d.length + i) * bw, height - h, Math.max(1, bw - 0.5), h);
}
}
} }
// Breakdown rows // Breakdown rows
InfoRow { label: "Used"; value: root._fmt(root.usedGb) } InfoRow {
InfoRow { label: "Cached"; value: root._fmt(root.cachedGb) } label: "Used"
InfoRow { label: "Available"; value: root._fmt(root.availGb) } value: root._fmt(root.usedGb)
InfoRow { label: "Total"; value: root._fmt(root.totalGb) } }
InfoRow {
label: "Cached"
value: root._fmt(root.cachedGb)
}
InfoRow {
label: "Available"
value: root._fmt(root.availGb)
}
InfoRow {
label: "Total"
value: root._fmt(root.totalGb)
}
Separator {} Separator {}

View file

@ -0,0 +1,112 @@
import QtQuick
Canvas {
id: root
// Data - array of 0-100 percentage values
property var history: []
property color color: "white"
property bool active: true
// 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;
// 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];
const ty = height - height * (th.value / 100);
ctx.strokeStyle = th.color.toString();
ctx.beginPath();
ctx.moveTo(0, ty);
ctx.lineTo(width, ty);
ctx.stroke();
}
ctx.setLineDash([]);
ctx.globalAlpha = 1.0;
}
if (areaMode) {
// Filled area under curve
const xOff = offset * step;
ctx.beginPath();
ctx.moveTo(xOff, height);
for (let i = 0; i < d.length; i++) {
ctx.lineTo(xOff + i * step, height - height * (d[i] / 100));
}
ctx.lineTo(xOff + (d.length - 1) * step, height);
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;
const y = height - height * (d[i] / 100);
if (i === 0)
ctx.moveTo(x, y);
else
ctx.lineTo(x, y);
}
ctx.strokeStyle = color.toString();
ctx.lineWidth = root.lineWidth;
ctx.stroke();
} else {
// Bar chart
const hasCF = typeof colorFunction === "function";
if (!hasCF)
ctx.fillStyle = color.toString();
for (let i = 0; i < d.length; i++) {
const barH = Math.max(1, height * d[i] / 100);
if (hasCF)
ctx.fillStyle = colorFunction(d[i]).toString();
ctx.fillRect((offset + i) * step, height - barH, Math.max(1, step - 0.5), barH);
}
}
}
}

View file

@ -104,72 +104,28 @@ Column {
} }
// History sparkline // History sparkline
Canvas { SparklineCanvas {
id: _sparkline
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 12 anchors.leftMargin: 12
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 12 anchors.rightMargin: 12
height: 40 height: 40
history: root.history
property var _hist: root.history color: root.stateColor
property color _col: root.stateColor active: root.active
maxSamples: 150
on_HistChanged: if (root.active) backgroundTint: 0.08
requestPaint() thresholds: [
on_ColChanged: if (root.active) {
requestPaint() value: root.warm,
color: S.Theme.base0A
onVisibleChanged: if (visible) },
requestPaint() {
value: root.hot,
onPaint: { color: S.Theme.base08
const ctx = getContext("2d");
if (!ctx)
return;
ctx.clearRect(0, 0, width, height);
const d = _hist;
if (!d.length)
return;
const maxSamples = 150;
const bw = width / maxSamples;
// Background tint
ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.08).toString();
ctx.fillRect(0, 0, width, height);
// Warm threshold line
const warmY = height - height * (root.warm / 100);
ctx.strokeStyle = S.Theme.base0A.toString();
ctx.globalAlpha = 0.3;
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.beginPath();
ctx.moveTo(0, warmY);
ctx.lineTo(width, warmY);
ctx.stroke();
// Hot threshold line
const hotY = height - height * (root.hot / 100);
ctx.strokeStyle = S.Theme.base08.toString();
ctx.beginPath();
ctx.moveTo(0, hotY);
ctx.lineTo(width, hotY);
ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1.0;
// Bars
const offset = maxSamples - d.length;
for (let i = 0; i < d.length; i++) {
const barH = Math.max(1, height * d[i] / 100);
const barColor = d[i] > root.hot ? S.Theme.base08 : d[i] > root.warm ? S.Theme.base0A : _col;
ctx.fillStyle = barColor.toString();
ctx.fillRect((offset + i) * bw, height - barH, Math.max(1, bw - 0.5), barH);
}
} }
]
colorFunction: v => v > root.hot ? S.Theme.base08 : v > root.warm ? S.Theme.base0A : root.stateColor
} }
// Threshold labels // Threshold labels

View file

@ -12,8 +12,9 @@ InfoRow 1.0 InfoRow.qml
MemoryApplet 1.0 MemoryApplet.qml MemoryApplet 1.0 MemoryApplet.qml
MprisApplet 1.0 MprisApplet.qml MprisApplet 1.0 MprisApplet.qml
NetworkApplet 1.0 NetworkApplet.qml NetworkApplet 1.0 NetworkApplet.qml
Separator 1.0 Separator.qml
NotifApplet 1.0 NotifApplet.qml NotifApplet 1.0 NotifApplet.qml
Separator 1.0 Separator.qml
SparklineCanvas 1.0 SparklineCanvas.qml
TemperatureApplet 1.0 TemperatureApplet.qml TemperatureApplet 1.0 TemperatureApplet.qml
VolumeApplet 1.0 VolumeApplet.qml VolumeApplet 1.0 VolumeApplet.qml
WeatherApplet 1.0 WeatherApplet.qml WeatherApplet 1.0 WeatherApplet.qml

View file

@ -14,6 +14,7 @@ shell/applets/NetworkApplet.qml: Unqualified access [unqualified]
shell/applets/NotifApplet.qml: Member "_notif" not found on type "QQuickItem" [missing-property] shell/applets/NotifApplet.qml: Member "_notif" not found on type "QQuickItem" [missing-property]
shell/applets/NotifApplet.qml: Member "_type" not found on type "QQuickItem" [missing-property] shell/applets/NotifApplet.qml: Member "_type" not found on type "QQuickItem" [missing-property]
shell/applets/NotifApplet.qml: Unqualified access [unqualified] shell/applets/NotifApplet.qml: Unqualified access [unqualified]
shell/applets/SparklineCanvas.qml: Property "colorFunction" is a var property. It may or may not be a method. Use a regular function instead. [use-proper-function]
shell/applets/TemperatureApplet.qml: Unqualified access [unqualified] shell/applets/TemperatureApplet.qml: Unqualified access [unqualified]
shell/applets/VolumeApplet.qml: Unqualified access [unqualified] shell/applets/VolumeApplet.qml: Unqualified access [unqualified]
shell/lock/Lock.qml: Unqualified access [unqualified] shell/lock/Lock.qml: Unqualified access [unqualified]