desktop-sysstat: dual graph for network

This commit is contained in:
Lemmy
2026-01-31 16:52:05 -05:00
parent 87a9b2a4b5
commit 6115ef8060
3 changed files with 159 additions and 57 deletions
@@ -16,7 +16,7 @@ DraggableDesktopWidget {
readonly property string diskPath: (widgetData && widgetData.diskPath !== undefined) ? widgetData.diskPath : "/"
readonly property color color: (widgetData && widgetData.color !== undefined) ? widgetData.color : Color.mPrimary
// History from service (2 minutes of data)
// History from service
readonly property var history: {
switch (root.statType) {
case "CPU":
@@ -28,12 +28,15 @@ DraggableDesktopWidget {
case "Disk":
return SystemStatService.diskHistories[root.diskPath] || [];
case "Network":
return SystemStatService.networkHistory;
return SystemStatService.rxSpeedHistory;
default:
return [];
}
}
// Secondary history for Network (Tx)
readonly property var history2: root.statType === "Network" ? SystemStatService.txSpeedHistory : []
// Current value from service
readonly property real currentValue: {
switch (root.statType) {
@@ -45,8 +48,6 @@ DraggableDesktopWidget {
return SystemStatService.memPercent;
case "Disk":
return SystemStatService.diskPercents[root.diskPath] || 0;
case "Network":
return Math.max(SystemStatService.rxRatio, SystemStatService.txRatio) * 100;
default:
return 0;
}
@@ -92,6 +93,7 @@ DraggableDesktopWidget {
NText {
Layout.alignment: Qt.AlignHCenter
visible: root.statType !== "Network"
text: Math.round(root.currentValue) + (root.statType === "GPU" ? "°C" : "%")
color: root.color
pointSize: Style.fontSizeS * root.widgetScale
@@ -99,6 +101,28 @@ DraggableDesktopWidget {
horizontalAlignment: Text.AlignHCenter
}
// Network: show Rx speed
NText {
Layout.alignment: Qt.AlignHCenter
visible: root.statType === "Network"
text: "↓ " + SystemStatService.formatSpeed(SystemStatService.rxSpeed)
color: root.color
pointSize: Style.fontSizeXXS * root.widgetScale
font.weight: Style.fontWeightBold
horizontalAlignment: Text.AlignHCenter
}
// Network: show Tx speed
NText {
Layout.alignment: Qt.AlignHCenter
visible: root.statType === "Network"
text: "↑ " + SystemStatService.formatSpeed(SystemStatService.txSpeed)
color: Color.mError
pointSize: Style.fontSizeXXS * root.widgetScale
font.weight: Style.fontWeightBold
horizontalAlignment: Text.AlignHCenter
}
NText {
Layout.alignment: Qt.AlignHCenter
visible: root.statType === "CPU"
@@ -125,9 +149,27 @@ DraggableDesktopWidget {
Layout.fillWidth: true
Layout.fillHeight: true
values: root.history
autoScale: root.statType === "GPU"
maxValue: 100
values2: root.history2
minValue: root.statType === "GPU" ? SystemStatService.gpuTempHistoryMin : 0
maxValue: {
switch (root.statType) {
case "CPU":
return Math.max(SystemStatService.cpuHistoryMax, 1);
case "GPU":
return Math.max(SystemStatService.gpuTempHistoryMax, 1);
case "Memory":
return Math.max(SystemStatService.memHistoryMax, 1);
case "Network":
return Math.max(SystemStatService.rxMaxSpeed, 1);
default:
return 100;
}
}
// Secondary line (TX) has its own scale
minValue2: minValue
maxValue2: root.statType === "Network" ? Math.max(SystemStatService.txMaxSpeed, 1) : maxValue
color: root.color
color2: Color.mError
fill: true
}
}
+31 -7
View File
@@ -59,11 +59,22 @@ Singleton {
property var gpuTempHistory: new Array(gpuHistoryLength).fill(0)
property var memHistory: new Array(memHistoryLength).fill(0)
property var diskHistories: ({}) // Keyed by mount path, initialized on first update
property var networkHistory: new Array(networkHistoryLength).fill(0)
property var rxSpeedHistory: new Array(networkHistoryLength).fill(0)
property var txSpeedHistory: new Array(networkHistoryLength).fill(0)
// Historical min/max tracking (since shell started) for consistent graph scaling
property real cpuHistoryMax: 0
property real gpuTempHistoryMin: 100
property real gpuTempHistoryMax: 0
property real memHistoryMax: 0
// Network uses existing rxMaxSpeed/txMaxSpeed (7-day learned peaks)
// Disk is always 0-100%
// History management - called from update functions, not change handlers
// (change handlers don't fire when value stays the same)
function pushCpuHistory() {
if (cpuUsage > cpuHistoryMax)
cpuHistoryMax = cpuUsage;
let h = cpuHistory.slice();
h.push(cpuUsage);
if (h.length > cpuHistoryLength)
@@ -72,6 +83,12 @@ Singleton {
}
function pushGpuHistory() {
if (gpuTemp > 0) {
if (gpuTemp < gpuTempHistoryMin)
gpuTempHistoryMin = gpuTemp;
if (gpuTemp > gpuTempHistoryMax)
gpuTempHistoryMax = gpuTemp;
}
let h = gpuTempHistory.slice();
h.push(gpuTemp);
if (h.length > gpuHistoryLength)
@@ -80,6 +97,8 @@ Singleton {
}
function pushMemHistory() {
if (memPercent > memHistoryMax)
memHistoryMax = memPercent;
let h = memHistory.slice();
h.push(memPercent);
if (h.length > memHistoryLength)
@@ -101,12 +120,17 @@ Singleton {
}
function pushNetworkHistory() {
let value = Math.max(rxRatio, txRatio) * 100;
let h = networkHistory.slice();
h.push(value);
if (h.length > networkHistoryLength)
h.shift();
networkHistory = h;
let rxH = rxSpeedHistory.slice();
rxH.push(rxSpeed);
if (rxH.length > networkHistoryLength)
rxH.shift();
rxSpeedHistory = rxH;
let txH = txSpeedHistory.slice();
txH.push(txSpeed);
if (txH.length > networkHistoryLength)
txH.shift();
txSpeedHistory = txH;
}
// Network max speed tracking (learned over time, cached for 7 days)
+80 -44
View File
@@ -4,68 +4,61 @@ import qs.Commons
Item {
id: root
clip: true // Clip curves that overshoot bounds
clip: true // Clip bezier overshoot
// Primary line
property var values: []
property color color: Color.mPrimary
// Optional secondary line
property var values2: []
property color color2: Color.mError
// Range settings for primary line
property real minValue: 0
property real maxValue: 100
property bool autoScale: false
property color color: Color.mPrimary
// Range settings for secondary line (defaults to primary range)
property real minValue2: minValue
property real maxValue2: maxValue
// Style settings
property real strokeWidth: 2 * Style.uiScaleRatio
property bool fill: true
property real fillOpacity: 0.15
readonly property bool hasData: values.length >= 2
// Padding for bezier overshoot (percentage of range)
readonly property real curvePadding: 0.08
// Computed effective range for rendering (includes padding for bezier overshoot)
readonly property real effectiveMin: {
let min = minValue;
let max = maxValue;
if (autoScale && values && values.length > 0) {
min = Math.min(...values);
max = Math.max(...values);
}
let range = max - min;
let padding = range * curvePadding;
return min - padding;
}
readonly property real effectiveMax: {
let min = minValue;
let max = maxValue;
if (autoScale && values && values.length > 0) {
min = Math.min(...values);
max = Math.max(...values);
}
let range = max - min;
let padding = range * curvePadding;
return max + padding;
}
readonly property bool hasData: values.length >= 2
readonly property bool hasData2: values2.length >= 2
// Convert a value to Y coordinate (no clamping - let bezier control points overshoot naturally)
function valueToY(val) {
let range = effectiveMax - effectiveMin;
// Convert a value to Y coordinate (with padding for bezier curves)
function valueToY(val, minVal, maxVal) {
let range = maxVal - minVal;
if (range <= 0)
return height / 2;
let normalized = (val - effectiveMin) / range;
let padding = range * curvePadding;
let paddedMin = minVal - padding;
let paddedMax = maxVal + padding;
let paddedRange = paddedMax - paddedMin;
let normalized = (val - paddedMin) / paddedRange;
return height - normalized * height;
}
// Generate SVG path using monotone cubic interpolation (better for data visualization)
readonly property string curvePath: {
if (!values || values.length < 2 || width <= 0 || height <= 0)
// Generate SVG path for a given values array using monotone cubic interpolation
function generateCurvePath(vals, minVal, maxVal) {
if (!vals || vals.length < 2 || width <= 0 || height <= 0)
return "";
const n = values.length;
const n = vals.length;
// Build array of points
let points = [];
for (let i = 0; i < n; i++) {
points.push({
x: (i / (n - 1)) * width,
y: valueToY(values[i])
y: valueToY(vals[i], minVal, maxVal)
});
}
@@ -109,21 +102,29 @@ Item {
return path;
}
// Path for the filled area (curve + bottom edge)
readonly property string fillPath: {
// Generate fill path (curve + bottom edge)
function generateFillPath(curvePath) {
if (!curvePath || width <= 0 || height <= 0)
return "";
return curvePath + ` L ${width.toFixed(2)} ${height.toFixed(2)} L 0 ${height.toFixed(2)} Z`;
}
// Computed paths for primary line
readonly property string curvePath: generateCurvePath(values, minValue, maxValue)
readonly property string fillPath: generateFillPath(curvePath)
// Computed paths for secondary line
readonly property string curvePath2: generateCurvePath(values2, minValue2, maxValue2)
readonly property string fillPath2: generateFillPath(curvePath2)
Shape {
anchors.fill: parent
layer.enabled: true
layer.samples: 4
antialiasing: true
visible: root.hasData
visible: root.hasData || root.hasData2
// Filled area under the curve
// Primary line fill
ShapePath {
strokeColor: "transparent"
strokeWidth: 0
@@ -134,7 +135,7 @@ Item {
y2: root.height
GradientStop {
position: 0.0
color: Qt.rgba(root.color.r, root.color.g, root.color.b, root.fillOpacity)
color: Qt.rgba(root.color.r, root.color.g, root.color.b, root.fill ? root.fillOpacity : 0)
}
GradientStop {
position: 1.0
@@ -146,9 +147,32 @@ Item {
}
}
// Stroke on top
// Secondary line fill
ShapePath {
strokeColor: root.color
strokeColor: "transparent"
strokeWidth: 0
fillGradient: LinearGradient {
x1: 0
y1: 0
x2: 0
y2: root.height
GradientStop {
position: 0.0
color: Qt.rgba(root.color2.r, root.color2.g, root.color2.b, root.fill && root.hasData2 ? root.fillOpacity : 0)
}
GradientStop {
position: 1.0
color: "transparent"
}
}
PathSvg {
path: root.fillPath2
}
}
// Primary line stroke
ShapePath {
strokeColor: root.hasData ? root.color : "transparent"
strokeWidth: root.strokeWidth
fillColor: "transparent"
joinStyle: ShapePath.RoundJoin
@@ -157,5 +181,17 @@ Item {
path: root.curvePath
}
}
// Secondary line stroke
ShapePath {
strokeColor: root.hasData2 ? root.color2 : "transparent"
strokeWidth: root.strokeWidth
fillColor: "transparent"
joinStyle: ShapePath.RoundJoin
capStyle: ShapePath.RoundCap
PathSvg {
path: root.curvePath2
}
}
}
}