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)
Canvas {
id: _sparkline
SparklineCanvas {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
height: 44
property var _hist: S.BatteryService.history
property color _col: root._stateColor
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 < 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);
history: S.BatteryService.history
color: root._stateColor
active: root.active
maxSamples: 1440
backgroundTint: 0.07
areaMode: true
thresholds: [
{
value: S.BatteryService.warnThresh,
color: S.Theme.base0A
},
{
value: S.BatteryService.critThresh,
color: S.Theme.base08
}
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

View file

@ -96,41 +96,16 @@ Column {
}
}
// Sparkline
Canvas {
SparklineCanvas {
id: sparkline
anchors.right: freqLabel.left
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
width: 32
height: 10
property var _hist: root.cores[parent.parent.index]?.history ?? []
property color _col: parent.parent._barColor
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);
}
}
history: root.cores[parent.parent.index]?.history ?? []
color: parent.parent._barColor
active: root.active
}
Text {

View file

@ -76,40 +76,16 @@ Column {
}
// Usage history sparkline
Canvas {
id: _sparkline
SparklineCanvas {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
height: 36
property var _hist: S.SystemStats.gpuHistory
on_HistChanged: 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 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);
}
}
history: S.SystemStats.gpuHistory
active: root.active
maxSamples: 60
colorFunction: v => S.Theme.loadColor(v)
}
// VRAM section

View file

@ -71,49 +71,36 @@ Column {
}
// Memory history sparkline
Canvas {
id: memSparkline
SparklineCanvas {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
height: 18
property var _hist: S.SystemStats.memHistory
property color _col: root.accentColor
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 / 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);
}
}
history: S.SystemStats.memHistory
color: root.accentColor
active: root.active
maxSamples: 30
backgroundTint: 0.15
}
// Breakdown rows
InfoRow { label: "Used"; value: root._fmt(root.usedGb) }
InfoRow { label: "Cached"; value: root._fmt(root.cachedGb) }
InfoRow { label: "Available"; value: root._fmt(root.availGb) }
InfoRow { label: "Total"; value: root._fmt(root.totalGb) }
InfoRow {
label: "Used"
value: root._fmt(root.usedGb)
}
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 {}

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
Canvas {
id: _sparkline
SparklineCanvas {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
height: 40
property var _hist: root.history
property color _col: root.stateColor
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 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);
history: root.history
color: root.stateColor
active: root.active
maxSamples: 150
backgroundTint: 0.08
thresholds: [
{
value: root.warm,
color: S.Theme.base0A
},
{
value: root.hot,
color: S.Theme.base08
}
}
]
colorFunction: v => v > root.hot ? S.Theme.base08 : v > root.warm ? S.Theme.base0A : root.stateColor
}
// Threshold labels

View file

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