mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
sysstat: better default fill and some graphing improvments
This commit is contained in:
@@ -290,6 +290,7 @@ Item {
|
||||
id: contentContainer
|
||||
anchors.fill: parent
|
||||
z: 1
|
||||
clip: true
|
||||
}
|
||||
|
||||
// Context menu model and handler - menu is created dynamically in PopupMenuWindow
|
||||
|
||||
@@ -160,18 +160,6 @@ DraggableDesktopWidget {
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
// Auto-scale settings per stat type (CPU/Memory/Disk are 0-100%, others auto-scale)
|
||||
readonly property bool graphAutoScale: {
|
||||
switch (root.statType) {
|
||||
case "CPU":
|
||||
case "Memory":
|
||||
case "Disk":
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update interval per stat type
|
||||
readonly property int graphUpdateInterval: {
|
||||
switch (root.statType) {
|
||||
@@ -200,19 +188,18 @@ DraggableDesktopWidget {
|
||||
maxValue: root.graphMaxValue
|
||||
minValue2: root.graphMinValue2
|
||||
maxValue2: root.graphMaxValue2
|
||||
autoScale: root.graphAutoScale
|
||||
autoScale2: root.graphAutoScale
|
||||
color: root.color
|
||||
color2: Color.mError
|
||||
fill: true
|
||||
updateInterval: root.graphUpdateInterval
|
||||
animateScale: root.statType === "Network"
|
||||
}
|
||||
}
|
||||
|
||||
// Side layout: icon + legend on left, graph on right
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Math.round(Style.marginL * widgetScale)
|
||||
anchors.margins: Math.round(Style.marginM * widgetScale)
|
||||
spacing: Math.round(Style.marginL * widgetScale)
|
||||
visible: root.layout === "side"
|
||||
|
||||
@@ -252,6 +239,7 @@ DraggableDesktopWidget {
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.layout === "side"
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
sourceComponent: graphComponent
|
||||
@@ -261,11 +249,12 @@ DraggableDesktopWidget {
|
||||
// Bottom layout: full-width graph, horizontal legend at bottom
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Math.round(Style.marginL * widgetScale)
|
||||
anchors.margins: Math.round(Style.marginM * widgetScale)
|
||||
spacing: Math.round(Style.marginS * widgetScale)
|
||||
visible: root.layout === "bottom"
|
||||
|
||||
Loader {
|
||||
active: root.layout === "bottom"
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
sourceComponent: graphComponent
|
||||
|
||||
@@ -90,10 +90,10 @@ SmartPanel {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: `${Math.round(SystemStatService.cpuUsage)}%`
|
||||
text: `${Math.round(SystemStatService.cpuUsage)}% ${SystemStatService.cpuFreq}`
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mPrimary
|
||||
Layout.rightMargin: Style.marginS
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
}
|
||||
|
||||
NIcon {
|
||||
@@ -106,21 +106,10 @@ SmartPanel {
|
||||
text: `${Math.round(SystemStatService.cpuTemp)}°C`
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mError
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
Layout.rightMargin: Style.marginS
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "bolt"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: SystemStatService.cpuFreq
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -141,7 +130,6 @@ SmartPanel {
|
||||
maxValue: 100
|
||||
minValue2: Math.max(SystemStatService.cpuTempHistoryMin - 5, 0)
|
||||
maxValue2: Math.max(SystemStatService.cpuTempHistoryMax + 5, 1)
|
||||
autoScale: false
|
||||
color: Color.mPrimary
|
||||
color2: Color.mError
|
||||
fill: true
|
||||
@@ -172,23 +160,10 @@ SmartPanel {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: `${Math.round(SystemStatService.memPercent)}% • ${SystemStatService.formatGigabytes(SystemStatService.memGb).replace(/[^0-9.]/g, "")} GB`
|
||||
text: `${Math.round(SystemStatService.memPercent)}% ${SystemStatService.formatGigabytes(SystemStatService.memGb).replace(/[^0-9.]/g, "")} GB`
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NIcon {
|
||||
visible: SystemStatService.swapTotalGb > 0
|
||||
icon: "exchange"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: SystemStatService.swapTotalGb > 0
|
||||
text: `${Math.round(SystemStatService.swapPercent)}%`
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -208,7 +183,6 @@ SmartPanel {
|
||||
values: SystemStatService.memHistory
|
||||
minValue: 0
|
||||
maxValue: 100
|
||||
autoScale: false
|
||||
color: Color.mPrimary
|
||||
fill: true
|
||||
fillOpacity: 0.15
|
||||
@@ -241,6 +215,7 @@ SmartPanel {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2") + "/s"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mPrimary
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
Layout.rightMargin: Style.marginS
|
||||
}
|
||||
|
||||
@@ -254,6 +229,7 @@ SmartPanel {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.txSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2") + "/s"
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mError
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -281,6 +257,7 @@ SmartPanel {
|
||||
fill: true
|
||||
fillOpacity: 0.15
|
||||
updateInterval: Settings.data.systemMonitor.networkPollingInterval
|
||||
animateScale: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,28 +58,25 @@ Singleton {
|
||||
readonly property int networkHistoryLength: Math.ceil(historyDurationMs / normalizeInterval(Settings.data.systemMonitor.networkPollingInterval))
|
||||
|
||||
property var cpuHistory: new Array(cpuHistoryLength).fill(0)
|
||||
property var cpuTempHistory: new Array(cpuHistoryLength).fill(0)
|
||||
property var gpuTempHistory: new Array(gpuHistoryLength).fill(0)
|
||||
property var cpuTempHistory: new Array(cpuHistoryLength).fill(40) // Reasonable default temp
|
||||
property var gpuTempHistory: new Array(gpuHistoryLength).fill(40) // Reasonable default temp
|
||||
property var memHistory: new Array(memHistoryLength).fill(0)
|
||||
property var diskHistories: ({}) // Keyed by mount path, initialized on first update
|
||||
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 cpuTempHistoryMin: 100
|
||||
property real cpuTempHistoryMax: 0
|
||||
property real gpuTempHistoryMin: 100
|
||||
property real gpuTempHistoryMax: 0
|
||||
property real memHistoryMax: 0
|
||||
// Temperature defaults create a valid 30-80°C range that expands as real data comes in
|
||||
property real cpuTempHistoryMin: 30
|
||||
property real cpuTempHistoryMax: 80
|
||||
property real gpuTempHistoryMin: 30
|
||||
property real gpuTempHistoryMax: 80
|
||||
// Network uses autoscaling from current history window
|
||||
// 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)
|
||||
@@ -116,8 +113,6 @@ Singleton {
|
||||
}
|
||||
|
||||
function pushMemHistory() {
|
||||
if (memPercent > memHistoryMax)
|
||||
memHistoryMax = memPercent;
|
||||
let h = memHistory.slice();
|
||||
h.push(memPercent);
|
||||
if (h.length > memHistoryLength)
|
||||
@@ -508,7 +503,7 @@ Singleton {
|
||||
totalFreq += parseFloat(matches[i].split(":")[1]);
|
||||
}
|
||||
let avgFreq = (totalFreq / matches.length) / 1000.0;
|
||||
root.cpuFreq = avgFreq.toFixed(1) + " GHz";
|
||||
root.cpuFreq = avgFreq.toFixed(1) + "GHz";
|
||||
cpuMaxFreqProcess.running = true;
|
||||
if (avgFreq > root.cpuGlobalMaxFreq)
|
||||
root.cpuGlobalMaxFreq = avgFreq;
|
||||
|
||||
+92
-69
@@ -29,32 +29,60 @@ Item {
|
||||
// Smooth scrolling interval (how often data updates)
|
||||
property int updateInterval: 1000
|
||||
|
||||
// Auto-scale: when false, use minValue/maxValue directly (e.g., for 0-100% graphs)
|
||||
property bool autoScale: true
|
||||
property bool autoScale2: true
|
||||
// Animate scale changes (for network graphs with dynamic max)
|
||||
property bool animateScale: false
|
||||
|
||||
// Padding for bezier overshoot (percentage of range)
|
||||
readonly property real curvePadding: 0.08
|
||||
readonly property real curvePadding: 0.12
|
||||
|
||||
readonly property bool hasData: values.length >= 3
|
||||
readonly property bool hasData2: values2.length >= 3
|
||||
// Edge padding to hide bezier curve overshoot (in pixels)
|
||||
readonly property real edgePadding: Math.max(8, width * 0.02)
|
||||
|
||||
readonly property bool hasData: values.length >= 4
|
||||
readonly property bool hasData2: values2.length >= 4
|
||||
|
||||
// Target max values (what we're animating toward)
|
||||
property real _targetMax1: maxValue
|
||||
property real _targetMax2: maxValue2
|
||||
|
||||
// Current animated max values (interpolated in timer when animateScale is true)
|
||||
property real _animMax1: maxValue
|
||||
property real _animMax2: maxValue2
|
||||
|
||||
onMaxValueChanged: {
|
||||
_targetMax1 = maxValue;
|
||||
if (animateScale && _ready1) {
|
||||
_animTimer.start();
|
||||
} else {
|
||||
_animMax1 = maxValue;
|
||||
}
|
||||
}
|
||||
|
||||
onMaxValue2Changed: {
|
||||
_targetMax2 = maxValue2;
|
||||
if (animateScale && _ready2) {
|
||||
_animTimer.start();
|
||||
} else {
|
||||
_animMax2 = maxValue2;
|
||||
}
|
||||
}
|
||||
|
||||
// Effective max values (animated or direct)
|
||||
readonly property real _effectiveMax1: animateScale ? _animMax1 : maxValue
|
||||
readonly property real _effectiveMax2: animateScale ? _animMax2 : maxValue2
|
||||
|
||||
// Animation state for primary line
|
||||
property real _t1: 1.0
|
||||
property bool _ready1: false
|
||||
property real _pred1: 0
|
||||
property real _ghost1: 0 // Value scrolling off left (the removed point)
|
||||
property real _nextGhost1: 0 // Current first value, becomes ghost on next update
|
||||
|
||||
// Animation state for secondary line
|
||||
property real _t2: 1.0
|
||||
property bool _ready2: false
|
||||
property real _pred2: 0
|
||||
property real _ghost2: 0
|
||||
property real _nextGhost2: 0
|
||||
|
||||
onValuesChanged: {
|
||||
if (values.length < 2)
|
||||
if (values.length < 4)
|
||||
return;
|
||||
|
||||
const last = values[values.length - 1];
|
||||
@@ -63,21 +91,15 @@ Item {
|
||||
|
||||
if (!_ready1) {
|
||||
_ready1 = true;
|
||||
_ghost1 = values[0];
|
||||
_nextGhost1 = values[0];
|
||||
_t1 = 0;
|
||||
} else {
|
||||
// Use the saved first value as the ghost (it's now conceptually removed)
|
||||
_ghost1 = _nextGhost1;
|
||||
_t1 = _t1 - 1.0;
|
||||
}
|
||||
// Save current first value for next update
|
||||
_nextGhost1 = values[0];
|
||||
_animTimer.start();
|
||||
}
|
||||
|
||||
onValues2Changed: {
|
||||
if (values2.length < 2)
|
||||
if (values2.length < 4)
|
||||
return;
|
||||
|
||||
const last = values2[values2.length - 1];
|
||||
@@ -86,14 +108,10 @@ Item {
|
||||
|
||||
if (!_ready2) {
|
||||
_ready2 = true;
|
||||
_ghost2 = values2[0];
|
||||
_nextGhost2 = values2[0];
|
||||
_t2 = 0;
|
||||
} else {
|
||||
_ghost2 = _nextGhost2;
|
||||
_t2 = _t2 - 1.0;
|
||||
}
|
||||
_nextGhost2 = values2[0];
|
||||
_animTimer.start();
|
||||
}
|
||||
|
||||
@@ -105,6 +123,7 @@ Item {
|
||||
const dt = 16 / root.updateInterval;
|
||||
let stillAnimating = false;
|
||||
|
||||
// Scroll animation
|
||||
if (root._t1 < 1.0) {
|
||||
root._t1 = Math.min(1.0, root._t1 + dt);
|
||||
stillAnimating = true;
|
||||
@@ -114,6 +133,26 @@ Item {
|
||||
stillAnimating = true;
|
||||
}
|
||||
|
||||
// Scale animation (lerp toward target) - synchronized with scroll
|
||||
if (root.animateScale) {
|
||||
const scaleLerp = 0.15; // Smooth lerp factor per frame
|
||||
const threshold = 0.5; // Snap when close enough
|
||||
|
||||
if (Math.abs(root._animMax1 - root._targetMax1) > threshold) {
|
||||
root._animMax1 += (root._targetMax1 - root._animMax1) * scaleLerp;
|
||||
stillAnimating = true;
|
||||
} else if (root._animMax1 !== root._targetMax1) {
|
||||
root._animMax1 = root._targetMax1;
|
||||
}
|
||||
|
||||
if (Math.abs(root._animMax2 - root._targetMax2) > threshold) {
|
||||
root._animMax2 += (root._targetMax2 - root._animMax2) * scaleLerp;
|
||||
stillAnimating = true;
|
||||
} else if (root._animMax2 !== root._targetMax2) {
|
||||
root._animMax2 = root._targetMax2;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.requestPaint();
|
||||
|
||||
if (!stillAnimating)
|
||||
@@ -121,33 +160,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Effective max values that include current data and predictions (when autoScale is true)
|
||||
readonly property real _effectiveMax: {
|
||||
if (!autoScale || !hasData)
|
||||
return maxValue;
|
||||
let m = maxValue;
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (values[i] > m)
|
||||
m = values[i];
|
||||
}
|
||||
if (_pred1 > m)
|
||||
m = _pred1;
|
||||
return m;
|
||||
}
|
||||
|
||||
readonly property real _effectiveMax2: {
|
||||
if (!autoScale2 || !hasData2)
|
||||
return maxValue2;
|
||||
let m = maxValue2;
|
||||
for (let i = 0; i < values2.length; i++) {
|
||||
if (values2[i] > m)
|
||||
m = values2[i];
|
||||
}
|
||||
if (_pred2 > m)
|
||||
m = _pred2;
|
||||
return m;
|
||||
}
|
||||
|
||||
// Convert a value to Y coordinate (with padding for bezier curves)
|
||||
function valueToY(val, minVal, maxVal) {
|
||||
let range = maxVal - minVal;
|
||||
@@ -172,48 +184,59 @@ Item {
|
||||
if (width <= 0 || height <= 0)
|
||||
return;
|
||||
|
||||
// Apply edge clipping to hide bezier overshoot (symmetric horizontal)
|
||||
const pad = root.edgePadding;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(pad, 0, width - pad * 2, height);
|
||||
ctx.clip();
|
||||
|
||||
// Draw primary line
|
||||
if (root.hasData) {
|
||||
const n = root.values.length;
|
||||
// Step based on visible points (n-1 since vals[0] is off-screen buffer)
|
||||
const step = width / (n - 2);
|
||||
drawGraph(ctx, root.values, root._pred1, root._ghost1, root.minValue, root._effectiveMax, root.color, root._t1, step);
|
||||
// Step based on visible points (n-2 since vals[0] and vals[1] are off-screen buffers)
|
||||
const step = width / (n - 3);
|
||||
drawGraph(ctx, root.values, root._pred1, root.minValue, root._effectiveMax1, root.color, root._t1, step);
|
||||
}
|
||||
|
||||
// Draw secondary line (independent animation)
|
||||
if (root.hasData2) {
|
||||
const n2 = root.values2.length;
|
||||
const step2 = width / (n2 - 2);
|
||||
drawGraph(ctx, root.values2, root._pred2, root._ghost2, root.minValue2, root._effectiveMax2, root.color2, root._t2, step2);
|
||||
const step2 = width / (n2 - 3);
|
||||
drawGraph(ctx, root.values2, root._pred2, root.minValue2, root._effectiveMax2, root.color2, root._t2, step2);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawGraph(ctx, vals, pred, ghost, minVal, maxVal, lineColor, t, step) {
|
||||
if (!vals || vals.length < 3)
|
||||
function drawGraph(ctx, vals, pred, minVal, maxVal, lineColor, t, step) {
|
||||
if (!vals || vals.length < 4)
|
||||
return;
|
||||
|
||||
// Safety check for invalid step
|
||||
if (!isFinite(step) || step <= 0)
|
||||
return;
|
||||
|
||||
const n = vals.length;
|
||||
|
||||
// Build points with interpolated X positions for smooth scrolling
|
||||
// We skip vals[0] (use it as off-screen buffer for bezier continuity)
|
||||
// This avoids the visual artifact when data scrolls off the left edge
|
||||
// vals[0] and vals[1] are off-screen buffers for bezier continuity
|
||||
// Visible data starts from vals[2]
|
||||
let pts = [];
|
||||
|
||||
// Ghost point (old removed value) at position -2
|
||||
// Buffer points (always off-screen, provide bezier continuity)
|
||||
pts.push({
|
||||
x: (-2 - t) * step,
|
||||
y: root.valueToY(ghost, minVal, maxVal)
|
||||
});
|
||||
|
||||
// Buffer point (vals[0]) at position -1, always off-screen
|
||||
pts.push({
|
||||
x: (-1 - t) * step,
|
||||
y: root.valueToY(vals[0], minVal, maxVal)
|
||||
});
|
||||
pts.push({
|
||||
x: (-1 - t) * step,
|
||||
y: root.valueToY(vals[1], minVal, maxVal)
|
||||
});
|
||||
|
||||
// Visible data points start from vals[1]
|
||||
for (let i = 1; i < n; i++) {
|
||||
const x = (i - 1 - t) * step;
|
||||
// Visible data points start from vals[2]
|
||||
for (let i = 2; i < n; i++) {
|
||||
const x = (i - 2 - t) * step;
|
||||
const y = root.valueToY(vals[i], minVal, maxVal);
|
||||
pts.push({
|
||||
x: x,
|
||||
@@ -223,7 +246,7 @@ Item {
|
||||
|
||||
// Prediction point
|
||||
pts.push({
|
||||
x: (n - 1 - t) * step,
|
||||
x: (n - 2 - t) * step,
|
||||
y: root.valueToY(pred, minVal, maxVal)
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user