mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
desktop-sysstat: dual graph for network
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user