feat: graphs now properly sized + port static graphs to c++
This commit is contained in:
@@ -1,506 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtCanvas2D
|
|
||||||
import ZShell
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property list<var> values
|
|
||||||
|
|
||||||
property int xSteps: 24
|
|
||||||
property real yMin: NaN
|
|
||||||
property real yMax: NaN
|
|
||||||
property real yPaddingTop: 0
|
|
||||||
property real yPaddingBottom: 0
|
|
||||||
|
|
||||||
property int verticalLineStep: 0
|
|
||||||
property int horizontalLineStep: 0
|
|
||||||
property bool firstXLine: true
|
|
||||||
property bool lastXLine: false
|
|
||||||
property int minHorizontalLines: 0
|
|
||||||
|
|
||||||
property var xLabel: null
|
|
||||||
property var yLabel: null
|
|
||||||
|
|
||||||
property real smoothing: 0
|
|
||||||
property bool curveSmoothing: true
|
|
||||||
property real markerX: -1
|
|
||||||
property real markerGap: 2 // px gap between line and marker dot edge, each side
|
|
||||||
|
|
||||||
property color lineColor: "white"
|
|
||||||
property color gridColor: "white"
|
|
||||||
property color markerColor: "white"
|
|
||||||
property color labelColor: "white"
|
|
||||||
property real gridAlpha: 0.12
|
|
||||||
property real labelAlpha: 0.6
|
|
||||||
property real fillAlpha: 0.15
|
|
||||||
property real lineWidth: 2
|
|
||||||
property real markerDotRadius: 4
|
|
||||||
property real labelFontSize: 8
|
|
||||||
|
|
||||||
Canvas2D {
|
|
||||||
id: canvas
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
alphaBlending: true
|
|
||||||
fillColor: "transparent"
|
|
||||||
|
|
||||||
onWidthChanged: requestPaint()
|
|
||||||
onHeightChanged: requestPaint()
|
|
||||||
|
|
||||||
function snapV(v) {
|
|
||||||
return ZUtils.snap(v, canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A value is plottable only if it is a real number. Everything else -
|
|
||||||
// null, undefined, NaN, an unparsable string from the API - is a hole, so
|
|
||||||
// the series splits there. Non-finite coordinates must never reach the
|
|
||||||
// path: one NaN lineTo makes the canvas ignore the rest of the curve, and
|
|
||||||
// the fill then runs from whatever y it last had straight down.
|
|
||||||
function numeric(v) {
|
|
||||||
if (typeof v === "number")
|
|
||||||
return Number.isFinite(v) ? v : null;
|
|
||||||
if (typeof v === "string") {
|
|
||||||
const n = Number(v);
|
|
||||||
return Number.isFinite(n) ? n : null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function strokeCurve(ctx, pts) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(pts[0].x, pts[0].y);
|
|
||||||
|
|
||||||
const dpr = ZUtils.dprFor(canvas);
|
|
||||||
for (let i = 0; i < pts.length - 1; i++) {
|
|
||||||
const p1 = pts[i];
|
|
||||||
const p2 = pts[i + 1];
|
|
||||||
const [c1x, c1y, c2x, c2y] = segmentControls(pts, i);
|
|
||||||
|
|
||||||
const firstCol = Math.ceil(p1.x * dpr) + 1;
|
|
||||||
const lastCol = Math.floor(p2.x * dpr) - 1;
|
|
||||||
for (let col = firstCol; col <= lastCol; ++col) {
|
|
||||||
const x = col / dpr;
|
|
||||||
ctx.lineTo(x, bezierY(p1, c1x, c1y, c2x, c2y, p2, x));
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.lineTo(p2.x, p2.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bezierY(p1, c1x, c1y, c2x, c2y, p2, targetX) {
|
|
||||||
let tlo = 0;
|
|
||||||
let thi = 1;
|
|
||||||
for (let it = 0; it < 16; it++) {
|
|
||||||
const tm = (tlo + thi) / 2;
|
|
||||||
const u = 1 - tm;
|
|
||||||
const x = u * u * u * p1.x + 3 * u * u * tm * c1x + 3 * u * tm * tm * c2x + tm * tm * tm * p2.x;
|
|
||||||
if (x < targetX)
|
|
||||||
tlo = tm;
|
|
||||||
else
|
|
||||||
thi = tm;
|
|
||||||
}
|
|
||||||
|
|
||||||
const t = (tlo + thi) / 2;
|
|
||||||
const u = 1 - t;
|
|
||||||
return u * u * u * p1.y + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * p2.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
function segmentControls(pts, i) {
|
|
||||||
const p0 = pts[Math.max(i - 1, 0)];
|
|
||||||
const p1 = pts[i];
|
|
||||||
const p2 = pts[i + 1];
|
|
||||||
const p3 = pts[Math.min(i + 2, pts.length - 1)];
|
|
||||||
|
|
||||||
if (root.curveSmoothing && pts.length > 2) {
|
|
||||||
const k = 1 / 6;
|
|
||||||
let c1x = p1.x + (p2.x - p0.x) * k;
|
|
||||||
let c1y = p1.y + (p2.y - p0.y) * k;
|
|
||||||
let c2x = p2.x - (p3.x - p1.x) * k;
|
|
||||||
let c2y = p2.y - (p3.y - p1.y) * k;
|
|
||||||
|
|
||||||
const minY = Math.min(p1.y, p2.y);
|
|
||||||
const maxY = Math.max(p1.y, p2.y);
|
|
||||||
c1y = Math.max(minY, Math.min(maxY, c1y));
|
|
||||||
c2y = Math.max(minY, Math.min(maxY, c2y));
|
|
||||||
|
|
||||||
return [c1x, c1y, c2x, c2y];
|
|
||||||
}
|
|
||||||
|
|
||||||
const cx = (p1.x + p2.x) * 0.5;
|
|
||||||
return [cx, p1.y, cx, p2.y];
|
|
||||||
}
|
|
||||||
|
|
||||||
function pointAtX(pts, targetX) {
|
|
||||||
let j = pts.length - 2;
|
|
||||||
for (let i = 0; i < pts.length - 1; i++) {
|
|
||||||
if (targetX <= pts[i + 1].x) {
|
|
||||||
j = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const p1 = pts[j];
|
|
||||||
const p2 = pts[j + 1];
|
|
||||||
const [c1x, c1y, c2x, c2y] = segmentControls(pts, j);
|
|
||||||
|
|
||||||
let tlo = 0;
|
|
||||||
let thi = 1;
|
|
||||||
for (let it = 0; it < 24; it++) {
|
|
||||||
const tm = (tlo + thi) / 2;
|
|
||||||
const u = 1 - tm;
|
|
||||||
const x = u * u * u * p1.x + 3 * u * u * tm * c1x + 3 * u * tm * tm * c2x + tm * tm * tm * p2.x;
|
|
||||||
if (x < targetX)
|
|
||||||
tlo = tm;
|
|
||||||
else
|
|
||||||
thi = tm;
|
|
||||||
}
|
|
||||||
|
|
||||||
const t = (tlo + thi) / 2;
|
|
||||||
const u = 1 - t;
|
|
||||||
const y = u * u * u * p1.y + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * p2.y;
|
|
||||||
return {
|
|
||||||
x: targetX,
|
|
||||||
y
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function cutPoint(pts, targetX) {
|
|
||||||
return pointAtX(pts, targetX);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cutLeft(pts, targetX) {
|
|
||||||
if (targetX <= pts[0].x)
|
|
||||||
return [];
|
|
||||||
if (targetX >= pts[pts.length - 1].x)
|
|
||||||
return pts.slice();
|
|
||||||
|
|
||||||
let j = pts.length - 2;
|
|
||||||
for (let i = 0; i < pts.length - 1; i++) {
|
|
||||||
if (targetX <= pts[i + 1].x) {
|
|
||||||
j = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const left = pts.slice(0, j + 1);
|
|
||||||
if (targetX > pts[j].x)
|
|
||||||
left.push(cutPoint(pts, targetX));
|
|
||||||
return left;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cutRight(pts, targetX) {
|
|
||||||
if (targetX >= pts[pts.length - 1].x)
|
|
||||||
return [];
|
|
||||||
if (targetX <= pts[0].x)
|
|
||||||
return pts.slice();
|
|
||||||
|
|
||||||
let j = pts.length - 2;
|
|
||||||
for (let i = 0; i < pts.length - 1; i++) {
|
|
||||||
if (targetX <= pts[i + 1].x) {
|
|
||||||
j = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const right = pts.slice(j + 1);
|
|
||||||
if (targetX < pts[j + 1].x)
|
|
||||||
right.unshift(pointAtX(pts, targetX));
|
|
||||||
return right;
|
|
||||||
}
|
|
||||||
|
|
||||||
onPaint: {
|
|
||||||
const ctx = getContext("2d");
|
|
||||||
if (!ctx)
|
|
||||||
return;
|
|
||||||
ctx.reset();
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
|
||||||
|
|
||||||
const W = width;
|
|
||||||
const H = height;
|
|
||||||
if (W < 2 || H < 2)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const padLeft = root.labelFontSize * 5;
|
|
||||||
const padRight = 0;
|
|
||||||
const padTop = 0;
|
|
||||||
const padBottom = root.labelFontSize + 14;
|
|
||||||
const plot = {
|
|
||||||
x: padLeft,
|
|
||||||
y: padTop,
|
|
||||||
w: W - padLeft - padRight,
|
|
||||||
h: H - padTop - padBottom
|
|
||||||
};
|
|
||||||
if (plot.w < 2 || plot.h < 2)
|
|
||||||
return;
|
|
||||||
|
|
||||||
let lo = root.yMin;
|
|
||||||
let hi = root.yMax;
|
|
||||||
const loAuto = Number.isNaN(lo);
|
|
||||||
const hiAuto = Number.isNaN(hi);
|
|
||||||
|
|
||||||
if (loAuto || hiAuto) {
|
|
||||||
let dmin = Infinity;
|
|
||||||
let dmax = -Infinity;
|
|
||||||
for (const value of root.values) {
|
|
||||||
const v = numeric(value);
|
|
||||||
if (v == null)
|
|
||||||
continue;
|
|
||||||
if (v < dmin)
|
|
||||||
dmin = v;
|
|
||||||
if (v > dmax)
|
|
||||||
dmax = v;
|
|
||||||
}
|
|
||||||
if (dmin === Infinity) {
|
|
||||||
dmin = 0;
|
|
||||||
dmax = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const step = root.horizontalLineStep;
|
|
||||||
const minSteps = root.minHorizontalLines > 1 ? root.minHorizontalLines - 1 : 0;
|
|
||||||
|
|
||||||
if (step > 0) {
|
|
||||||
if (hiAuto) {
|
|
||||||
const baseLo = loAuto ? Math.floor(dmin / step) * step : lo;
|
|
||||||
const span = Math.max(Math.ceil((dmax - baseLo) / step), minSteps, 1);
|
|
||||||
lo = baseLo;
|
|
||||||
hi = baseLo + span * step;
|
|
||||||
} else {
|
|
||||||
const span = Math.max(Math.ceil((hi - dmin) / step), minSteps, 1);
|
|
||||||
lo = hi - span * step;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (loAuto)
|
|
||||||
lo = dmin;
|
|
||||||
if (hiAuto)
|
|
||||||
hi = dmax;
|
|
||||||
if (hi <= lo)
|
|
||||||
hi = lo + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const scaleBottom = lo - root.yPaddingBottom;
|
|
||||||
const scaleTop = hi + root.yPaddingTop;
|
|
||||||
const range = scaleTop - scaleBottom;
|
|
||||||
if (range <= 0 || root.xSteps < 1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
let series = root.values.map(v => numeric(v));
|
|
||||||
if (root.smoothing > 0 && series.length > 0) {
|
|
||||||
const radius = Math.round(root.smoothing);
|
|
||||||
const raw = series;
|
|
||||||
series = raw.map((v, i) => {
|
|
||||||
if (v == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
let sum = 0;
|
|
||||||
let n = 0;
|
|
||||||
for (let j = -radius; j <= radius; j++) {
|
|
||||||
const u = raw[i + j];
|
|
||||||
if (u != null) {
|
|
||||||
sum += u;
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n > 0 ? sum / n : null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const xAt = i => plot.x + (i / root.xSteps) * plot.w;
|
|
||||||
const yAt = v => plot.y + (1 - (v - scaleBottom) / range) * plot.h;
|
|
||||||
|
|
||||||
const dpr = ZUtils.dprFor(canvas);
|
|
||||||
const devW = w => Math.max(1, Math.round(w * dpr));
|
|
||||||
const snapLine = (v, w) => {
|
|
||||||
const dw = devW(w);
|
|
||||||
return (Math.round(v * dpr - dw / 2) + dw / 2) / dpr;
|
|
||||||
};
|
|
||||||
const gridW = devW(1) / dpr;
|
|
||||||
|
|
||||||
ctx.strokeStyle = Qt.alpha(root.gridColor, root.gridAlpha);
|
|
||||||
ctx.lineWidth = gridW;
|
|
||||||
|
|
||||||
if (root.verticalLineStep > 0) {
|
|
||||||
for (let i = 0; i <= root.xSteps; i += root.verticalLineStep) {
|
|
||||||
if (i === 0 && !root.firstXLine)
|
|
||||||
continue;
|
|
||||||
if (i === root.xSteps && !root.lastXLine)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const x = snapLine(xAt(i), 1);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, plot.y);
|
|
||||||
ctx.lineTo(x, H);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const vStart = lo - (root.yPaddingBottom > 0 ? Math.floor(root.yPaddingBottom / root.horizontalLineStep) * root.horizontalLineStep : 0);
|
|
||||||
|
|
||||||
if (root.horizontalLineStep > 0) {
|
|
||||||
for (let v = vStart; v <= hi + 0.001; v += root.horizontalLineStep) {
|
|
||||||
const y = snapLine(yAt(v), 1);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, y);
|
|
||||||
ctx.lineTo(plot.x + plot.w, y);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.fillStyle = Qt.alpha(root.labelColor, root.labelAlpha);
|
|
||||||
ctx.font = root.labelFontSize + "pt 'sans-serif'";
|
|
||||||
|
|
||||||
ctx.textAlign = "left";
|
|
||||||
ctx.textBaseline = "top";
|
|
||||||
if (root.verticalLineStep > 0 && root.xLabel) {
|
|
||||||
for (let i = 0; i <= root.xSteps; i += root.verticalLineStep) {
|
|
||||||
if (i === 0 && !root.firstXLine)
|
|
||||||
continue;
|
|
||||||
if (i === root.xSteps && !root.lastXLine)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const text = root.xLabel(i);
|
|
||||||
if (text)
|
|
||||||
ctx.fillText(text, xAt(i) + 4, plot.y + plot.h + 8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.textBaseline = "bottom";
|
|
||||||
if (root.horizontalLineStep > 0 && root.yLabel) {
|
|
||||||
for (let v = vStart; v <= hi + 0.001; v += root.horizontalLineStep) {
|
|
||||||
const text = root.yLabel(v);
|
|
||||||
if (text)
|
|
||||||
ctx.fillText(text, 2, yAt(v) - 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const segments = [];
|
|
||||||
let current = null;
|
|
||||||
for (let i = 0; i < series.length; i++) {
|
|
||||||
if (series[i] == null) {
|
|
||||||
if (current)
|
|
||||||
segments.push(current);
|
|
||||||
current = null;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const pt = {
|
|
||||||
x: snapV(xAt(i)),
|
|
||||||
y: yAt(series[i])
|
|
||||||
};
|
|
||||||
if (!current)
|
|
||||||
current = [];
|
|
||||||
current.push(pt);
|
|
||||||
}
|
|
||||||
if (current)
|
|
||||||
segments.push(current);
|
|
||||||
|
|
||||||
ctx.lineCap = "round";
|
|
||||||
ctx.lineJoin = "round";
|
|
||||||
|
|
||||||
const hasMarker = root.markerX >= 0 && root.markerX <= root.xSteps;
|
|
||||||
const markerPx = hasMarker ? xAt(root.markerX) : 0;
|
|
||||||
let gapPx = 0;
|
|
||||||
if (hasMarker) {
|
|
||||||
let slope = 0;
|
|
||||||
const seg = segments.find(s => s[0].x <= markerPx && markerPx <= s[s.length - 1].x);
|
|
||||||
if (seg && seg.length > 2 && markerPx - 1 >= seg[0].x && markerPx + 1 <= seg[seg.length - 1].x)
|
|
||||||
slope = (pointAtX(seg, markerPx + 1).y - pointAtX(seg, markerPx - 1).y) / 2;
|
|
||||||
gapPx = (root.markerDotRadius + root.markerGap) / Math.sqrt(1 + slope * slope);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const seg of segments) {
|
|
||||||
if (seg.length < 2)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ctx.fillStyle = Qt.alpha(root.lineColor, root.fillAlpha);
|
|
||||||
strokeCurve(ctx, seg);
|
|
||||||
ctx.lineTo(seg[seg.length - 1].x, plot.y + plot.h);
|
|
||||||
ctx.lineTo(seg[0].x, plot.y + plot.h);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
const pieces = hasMarker ? [cutLeft(seg, markerPx - gapPx), cutRight(seg, markerPx + gapPx)] : [seg];
|
|
||||||
|
|
||||||
for (const pts of pieces) {
|
|
||||||
if (pts.length < 2)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ctx.strokeStyle = root.lineColor;
|
|
||||||
ctx.lineWidth = ZUtils.snap(root.lineWidth, root);
|
|
||||||
strokeCurve(ctx, pts);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.markerX >= 0 && root.markerX <= root.xSteps) {
|
|
||||||
const x = markerPx;
|
|
||||||
|
|
||||||
ctx.strokeStyle = Qt.alpha(root.markerColor, 0.6);
|
|
||||||
ctx.lineWidth = gridW;
|
|
||||||
const lineX = snapLine(x, 1);
|
|
||||||
let dashY = plot.y;
|
|
||||||
let dashOn = true;
|
|
||||||
|
|
||||||
while (dashY < H) {
|
|
||||||
const dashEnd = Math.min(dashY + 4, H);
|
|
||||||
if (dashOn) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(lineX, snapV(dashY));
|
|
||||||
ctx.lineTo(lineX, snapV(dashEnd));
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
dashY = dashEnd;
|
|
||||||
dashOn = !dashOn;
|
|
||||||
}
|
|
||||||
|
|
||||||
const i0 = Math.floor(root.markerX);
|
|
||||||
const v0 = series[i0];
|
|
||||||
const v1 = series[i0 + 1];
|
|
||||||
let y = null;
|
|
||||||
|
|
||||||
if (v0 != null && v1 != null)
|
|
||||||
y = yAt(v0 + (v1 - v0) * (root.markerX - i0));
|
|
||||||
else if (v0 != null)
|
|
||||||
y = yAt(v0);
|
|
||||||
|
|
||||||
if (y != null) {
|
|
||||||
ctx.fillStyle = root.markerColor;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.circle(snapV(x), y, root.markerDotRadius);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onValuesChanged: canvas.requestPaint()
|
|
||||||
onXStepsChanged: canvas.requestPaint()
|
|
||||||
onYMinChanged: canvas.requestPaint()
|
|
||||||
onYMaxChanged: canvas.requestPaint()
|
|
||||||
onYPaddingTopChanged: canvas.requestPaint()
|
|
||||||
onYPaddingBottomChanged: canvas.requestPaint()
|
|
||||||
onVerticalLineStepChanged: canvas.requestPaint()
|
|
||||||
onHorizontalLineStepChanged: canvas.requestPaint()
|
|
||||||
onFirstXLineChanged: canvas.requestPaint()
|
|
||||||
onLastXLineChanged: canvas.requestPaint()
|
|
||||||
onMinHorizontalLinesChanged: canvas.requestPaint()
|
|
||||||
onXLabelChanged: canvas.requestPaint()
|
|
||||||
onYLabelChanged: canvas.requestPaint()
|
|
||||||
onSmoothingChanged: canvas.requestPaint()
|
|
||||||
onCurveSmoothingChanged: canvas.requestPaint()
|
|
||||||
onMarkerXChanged: canvas.requestPaint()
|
|
||||||
onMarkerGapChanged: canvas.requestPaint()
|
|
||||||
onLineColorChanged: canvas.requestPaint()
|
|
||||||
onGridColorChanged: canvas.requestPaint()
|
|
||||||
onMarkerColorChanged: canvas.requestPaint()
|
|
||||||
onLabelColorChanged: canvas.requestPaint()
|
|
||||||
onGridAlphaChanged: canvas.requestPaint()
|
|
||||||
onLabelAlphaChanged: canvas.requestPaint()
|
|
||||||
onFillAlphaChanged: canvas.requestPaint()
|
|
||||||
onLineWidthChanged: canvas.requestPaint()
|
|
||||||
onMarkerDotRadiusChanged: canvas.requestPaint()
|
|
||||||
onLabelFontSizeChanged: canvas.requestPaint()
|
|
||||||
}
|
|
||||||
@@ -101,6 +101,7 @@ orientation: Qt.Vertical
|
|||||||
line1Color: card.accent
|
line1Color: card.accent
|
||||||
line1FillAlpha: 0.15
|
line1FillAlpha: 0.15
|
||||||
maxValue: 1.0
|
maxValue: 1.0
|
||||||
|
slideInterval: Config.dashboard.resourceUpdateInterval
|
||||||
slideProgress: 1
|
slideProgress: 1
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -114,9 +115,14 @@ orientation: Qt.Vertical
|
|||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
id: slideAnim
|
id: slideAnim
|
||||||
|
|
||||||
|
// Only drives frames; the slide phase is the sample
|
||||||
|
// clock inside SparklineItem. It must not stop at the
|
||||||
|
// end of a cycle, or the newest value is held on the
|
||||||
|
// clipped edge until the next sample arrives.
|
||||||
duration: Config.dashboard.resourceUpdateInterval
|
duration: Config.dashboard.resourceUpdateInterval
|
||||||
easing.type: Easing.Linear
|
easing.type: Easing.Linear
|
||||||
from: 0
|
from: 0
|
||||||
|
loops: Animation.Infinite
|
||||||
property: "slideProgress"
|
property: "slideProgress"
|
||||||
target: sparkline
|
target: sparkline
|
||||||
to: 1
|
to: 1
|
||||||
|
|||||||
@@ -25,14 +25,6 @@ SnapLayout {
|
|||||||
return ZUtils.snap(d.getHours() + d.getMinutes() / 60, root);
|
return ZUtils.snap(d.getHours() + d.getMinutes() / 60, root);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatHour(h) {
|
|
||||||
if (root.useTwelveHourClock) {
|
|
||||||
const hr = h % 12 === 0 ? 12 : h % 12;
|
|
||||||
return hr + (h < 12 ? " AM" : " PM");
|
|
||||||
}
|
|
||||||
return h;
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 60000
|
interval: 60000
|
||||||
repeat: true
|
repeat: true
|
||||||
@@ -78,8 +70,10 @@ SnapLayout {
|
|||||||
minHorizontalLines: 3
|
minHorizontalLines: 3
|
||||||
yPaddingTop: horizontalLineStep
|
yPaddingTop: horizontalLineStep
|
||||||
yPaddingBottom: horizontalLineStep
|
yPaddingBottom: horizontalLineStep
|
||||||
xLabel: i => root.formatHour(i)
|
xLabelMode: LineGraph.Hour
|
||||||
yLabel: v => Math.round(v) + "°"
|
useTwelveHourClock: root.useTwelveHourClock
|
||||||
|
yLabelMode: LineGraph.Value
|
||||||
|
yLabelSuffix: "°"
|
||||||
|
|
||||||
markerX: root.markerX
|
markerX: root.markerX
|
||||||
smoothing: root.smoothing
|
smoothing: root.smoothing
|
||||||
@@ -133,8 +127,10 @@ SnapLayout {
|
|||||||
verticalLineStep: 3
|
verticalLineStep: 3
|
||||||
lastXLine: false
|
lastXLine: false
|
||||||
horizontalLineStep: 30
|
horizontalLineStep: 30
|
||||||
xLabel: i => root.formatHour(i)
|
xLabelMode: LineGraph.Hour
|
||||||
yLabel: v => v + "%"
|
useTwelveHourClock: root.useTwelveHourClock
|
||||||
|
yLabelMode: LineGraph.Value
|
||||||
|
yLabelSuffix: "%"
|
||||||
|
|
||||||
markerX: root.markerX
|
markerX: root.markerX
|
||||||
smoothing: root.smoothing
|
smoothing: root.smoothing
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ CustomRect {
|
|||||||
SparklineItem {
|
SparklineItem {
|
||||||
id: sparkline
|
id: sparkline
|
||||||
|
|
||||||
property bool initialized: false
|
|
||||||
property real smoothMax: targetMax * 1.6
|
property real smoothMax: targetMax * 1.6
|
||||||
property real targetMax: 1024
|
property real targetMax: 1024
|
||||||
|
|
||||||
@@ -63,20 +62,15 @@ CustomRect {
|
|||||||
line2Color: Colors.palette.m3tertiary
|
line2Color: Colors.palette.m3tertiary
|
||||||
line2FillAlpha: 0.2
|
line2FillAlpha: 0.2
|
||||||
maxValue: smoothMax
|
maxValue: smoothMax
|
||||||
|
slideInterval: Config.dashboard.resourceUpdateInterval
|
||||||
slideProgress: 1
|
slideProgress: 1
|
||||||
|
|
||||||
Behavior on smoothMax {
|
Behavior on smoothMax {
|
||||||
enabled: sparkline.initialized
|
|
||||||
|
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024);
|
sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024);
|
||||||
|
|
||||||
sparkline.smoothMax = Qt.binding(() => sparkline.targetMax * 1.6);
|
|
||||||
|
|
||||||
sparkline.initialized = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -94,6 +88,7 @@ CustomRect {
|
|||||||
duration: Config.dashboard.resourceUpdateInterval
|
duration: Config.dashboard.resourceUpdateInterval
|
||||||
easing.type: Easing.Linear
|
easing.type: Easing.Linear
|
||||||
from: 0
|
from: 0
|
||||||
|
loops: Animation.Infinite
|
||||||
property: "slideProgress"
|
property: "slideProgress"
|
||||||
target: sparkline
|
target: sparkline
|
||||||
to: 1
|
to: 1
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ Item {
|
|||||||
label: qsTr("Video memory")
|
label: qsTr("Video memory")
|
||||||
icon: "memory_alt"
|
icon: "memory_alt"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
onClicked: {
|
||||||
|
ZUtils.logPlacement(card, "card");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WrappedLoader {
|
WrappedLoader {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ qml_module(ZShell-components
|
|||||||
carouselview.hpp carouselview.cpp
|
carouselview.hpp carouselview.cpp
|
||||||
sparklinecontent.hpp sparklinecontent.cpp
|
sparklinecontent.hpp sparklinecontent.cpp
|
||||||
sparklineitem.hpp sparklineitem.cpp
|
sparklineitem.hpp sparklineitem.cpp
|
||||||
sparklinesceneitem.hpp sparklinesceneitem.cpp
|
linegraph.hpp linegraph.cpp
|
||||||
roundedmask.hpp roundedmask.cpp
|
roundedmask.hpp roundedmask.cpp
|
||||||
roundedclip.hpp roundedclip.cpp
|
roundedclip.hpp roundedclip.cpp
|
||||||
snaplayout.hpp snaplayout.cpp
|
snaplayout.hpp snaplayout.cpp
|
||||||
|
|||||||
@@ -0,0 +1,910 @@
|
|||||||
|
#include "linegraph.hpp"
|
||||||
|
|
||||||
|
#include "internal/linegeometry.hpp"
|
||||||
|
|
||||||
|
#include <QCanvasPainterItemRenderer>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QQuickWindow>
|
||||||
|
#include <qcanvaspainter.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace ZShell::components {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr qreal kNaN = std::numeric_limits<qreal>::quiet_NaN();
|
||||||
|
|
||||||
|
bool sameReal(qreal a, qreal b) {
|
||||||
|
if (std::isnan(a) || std::isnan(b))
|
||||||
|
return std::isnan(a) && std::isnan(b);
|
||||||
|
return qFuzzyCompare(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A value is plottable only if it is a real number. Everything else - null,
|
||||||
|
// undefined, NaN, an unparsable string from the API - is NaN, so the series splits
|
||||||
|
// there. Non-finite coordinates must never reach the path.
|
||||||
|
qreal parseValue(const QVariant& v) {
|
||||||
|
bool ok = false;
|
||||||
|
const double d = v.toDouble(&ok);
|
||||||
|
return ok && std::isfinite(d) ? qreal(d) : kNaN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index of the segment whose right endpoint is at or past targetX.
|
||||||
|
int indexAt(const QVector<QPointF>& points, qreal targetX) {
|
||||||
|
for (int i = 0; i < points.size() - 1; ++i) {
|
||||||
|
if (targetX <= points[i + 1].x())
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
const int last = static_cast<int>(points.size()) - 2;
|
||||||
|
return last > 0 ? last : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The part of the polyline left/right of targetX, ending or beginning exactly at
|
||||||
|
// the curve point there. Used to carve a gap around the marker dot.
|
||||||
|
QVector<QPointF> cutLeft(
|
||||||
|
const QVector<QPointF>& points,
|
||||||
|
qreal targetX,
|
||||||
|
bool curveSmoothing) {
|
||||||
|
if (targetX <= points.first().x())
|
||||||
|
return {};
|
||||||
|
if (targetX >= points.last().x())
|
||||||
|
return points;
|
||||||
|
|
||||||
|
const int j = indexAt(points, targetX);
|
||||||
|
QVector<QPointF> left = points.first(j + 1);
|
||||||
|
if (targetX > points[j].x())
|
||||||
|
left.append(internal::pointAtX(points, targetX, curveSmoothing));
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<QPointF> cutRight(
|
||||||
|
const QVector<QPointF>& points,
|
||||||
|
qreal targetX,
|
||||||
|
bool curveSmoothing) {
|
||||||
|
if (targetX >= points.last().x())
|
||||||
|
return {};
|
||||||
|
if (targetX <= points.first().x())
|
||||||
|
return points;
|
||||||
|
|
||||||
|
const int j = indexAt(points, targetX);
|
||||||
|
QVector<QPointF> right = points.mid(j + 1);
|
||||||
|
if (targetX < points[j + 1].x())
|
||||||
|
right.prepend(internal::pointAtX(points, targetX, curveSmoothing));
|
||||||
|
return right;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString formatXLabel(LineGraph::XLabelMode mode, int i, bool use12) {
|
||||||
|
if (mode == LineGraph::XLabelMode::None)
|
||||||
|
return {};
|
||||||
|
if (mode == LineGraph::XLabelMode::Number)
|
||||||
|
return QString::number(i);
|
||||||
|
// Hour: an hour of day 0..24, on a 12- or 24-hour clock.
|
||||||
|
if (use12) {
|
||||||
|
const int hr = (i % 12 == 0) ? 12 : (i % 12);
|
||||||
|
return QString::number(hr)
|
||||||
|
+ (i < 12 ? QStringLiteral(" AM")
|
||||||
|
: QStringLiteral(" PM"));
|
||||||
|
}
|
||||||
|
return QString::number(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString formatYLabel(
|
||||||
|
LineGraph::YLabelMode mode,
|
||||||
|
qreal v,
|
||||||
|
int decimals,
|
||||||
|
const QString& suffix) {
|
||||||
|
if (mode == LineGraph::YLabelMode::Off)
|
||||||
|
return {};
|
||||||
|
return QString::number(v, 'f', qMax(0, decimals)) + suffix;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Everything paint needs, computed once on the GUI thread in synchronizeData so
|
||||||
|
// paint() (render thread) only reads it. Curve vertices already carry their
|
||||||
|
// device-pixel column quantization from internal::linegeometry.
|
||||||
|
class LineGraphRenderer final : public QCanvasPainterItemRenderer {
|
||||||
|
public:
|
||||||
|
void synchronizeData(QCanvasPainterItem* item) override;
|
||||||
|
void paint(QCanvasPainter* painter) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Plot {
|
||||||
|
qreal x = 0.0;
|
||||||
|
qreal y = 0.0;
|
||||||
|
qreal w = 0.0;
|
||||||
|
qreal h = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Label {
|
||||||
|
QString text;
|
||||||
|
qreal x = 0.0;
|
||||||
|
qreal y = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Segment {
|
||||||
|
QVector<QPointF> fill;
|
||||||
|
QVector<QVector<QPointF>> strokes;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Marker {
|
||||||
|
bool hasLine = false;
|
||||||
|
qreal lineX = 0.0;
|
||||||
|
QVector<QPair<qreal, qreal>> dash;
|
||||||
|
bool hasDot = false;
|
||||||
|
QPointF dot;
|
||||||
|
qreal dotRadius = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Model {
|
||||||
|
bool valid = false;
|
||||||
|
qreal height = 0.0;
|
||||||
|
Plot plot;
|
||||||
|
qreal plotBottom = 0.0;
|
||||||
|
qreal gridWidth = 0.0;
|
||||||
|
qreal lineWidth = 0.0;
|
||||||
|
QVector<qreal> vGrid;
|
||||||
|
QVector<qreal> hGrid;
|
||||||
|
QVector<Label> xLabels;
|
||||||
|
QVector<Label> yLabels;
|
||||||
|
QVector<Segment> segments;
|
||||||
|
Marker marker;
|
||||||
|
qreal labelFontSize = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void buildModel(const LineGraph* graph);
|
||||||
|
|
||||||
|
Model m_model;
|
||||||
|
QColor m_lineColor;
|
||||||
|
QColor m_gridColor;
|
||||||
|
QColor m_markerColor;
|
||||||
|
QColor m_labelColor;
|
||||||
|
qreal m_gridAlpha = 0.12;
|
||||||
|
qreal m_labelAlpha = 0.6;
|
||||||
|
qreal m_fillAlpha = 0.15;
|
||||||
|
qreal m_dpr = 1.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void LineGraphRenderer::synchronizeData(QCanvasPainterItem* item) {
|
||||||
|
auto* graph = static_cast<LineGraph*>(item);
|
||||||
|
m_lineColor = graph->m_lineColor;
|
||||||
|
m_gridColor = graph->m_gridColor;
|
||||||
|
m_markerColor = graph->m_markerColor;
|
||||||
|
m_labelColor = graph->m_labelColor;
|
||||||
|
m_gridAlpha = graph->m_gridAlpha;
|
||||||
|
m_labelAlpha = graph->m_labelAlpha;
|
||||||
|
m_fillAlpha = graph->m_fillAlpha;
|
||||||
|
m_dpr = graph->m_dpr > 0.0 ? graph->m_dpr : 1.0;
|
||||||
|
buildModel(graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraphRenderer::buildModel(const LineGraph* graph) {
|
||||||
|
Model model;
|
||||||
|
const qreal W = width();
|
||||||
|
const qreal H = height();
|
||||||
|
const qreal dpr = m_dpr;
|
||||||
|
model.height = H;
|
||||||
|
model.labelFontSize = graph->m_labelFontSize;
|
||||||
|
|
||||||
|
if (W < 2 || H < 2 || graph->m_xSteps < 1) {
|
||||||
|
m_model = model;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qreal padLeft = graph->m_labelFontSize * 5.0;
|
||||||
|
const qreal padBottom = graph->m_labelFontSize + 14.0;
|
||||||
|
model.plot = Plot {padLeft, 0.0, W - padLeft, H - padBottom};
|
||||||
|
if (model.plot.w < 2 || model.plot.h < 2) {
|
||||||
|
m_model = model;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
model.plotBottom = model.plot.y + model.plot.h;
|
||||||
|
|
||||||
|
// Raw series, holes as NaN.
|
||||||
|
QVector<qreal> raw;
|
||||||
|
raw.reserve(graph->m_values.size());
|
||||||
|
for (const QVariant& v : graph->m_values)
|
||||||
|
raw.append(parseValue(v));
|
||||||
|
|
||||||
|
// Moving average over the non-hole neighbours, the same window the sparkline
|
||||||
|
// applies at the same radius.
|
||||||
|
QVector<qreal> series = raw;
|
||||||
|
const int radius = qRound(graph->m_smoothing);
|
||||||
|
if (radius > 0 && !raw.isEmpty()) {
|
||||||
|
series.resize(raw.size());
|
||||||
|
for (int i = 0; i < raw.size(); ++i) {
|
||||||
|
if (std::isnan(raw[i])) {
|
||||||
|
series[i] = kNaN;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
qreal sum = 0.0;
|
||||||
|
int n = 0;
|
||||||
|
for (int j = -radius; j <= radius; ++j) {
|
||||||
|
const int u = i + j;
|
||||||
|
if (u < 0 || u >= raw.size())
|
||||||
|
continue;
|
||||||
|
if (!std::isnan(raw[u])) {
|
||||||
|
sum += raw[u];
|
||||||
|
++n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
series[i] = n > 0 ? sum / qreal(n) : kNaN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// y range: explicit yMin/yMax, or auto from the data snapped to the grid step.
|
||||||
|
qreal lo = graph->m_yMin;
|
||||||
|
qreal hi = graph->m_yMax;
|
||||||
|
const bool loAuto = std::isnan(lo);
|
||||||
|
const bool hiAuto = std::isnan(hi);
|
||||||
|
if (loAuto || hiAuto) {
|
||||||
|
qreal dmin = std::numeric_limits<qreal>::infinity();
|
||||||
|
qreal dmax = -std::numeric_limits<qreal>::infinity();
|
||||||
|
for (const qreal v : series) {
|
||||||
|
if (std::isnan(v))
|
||||||
|
continue;
|
||||||
|
dmin = std::min(dmin, v);
|
||||||
|
dmax = std::max(dmax, v);
|
||||||
|
}
|
||||||
|
if (std::isinf(dmin)) {
|
||||||
|
dmin = 0.0;
|
||||||
|
dmax = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int step = graph->m_horizontalLineStep;
|
||||||
|
const int minSteps = graph->m_minHorizontalLines > 1
|
||||||
|
? graph->m_minHorizontalLines - 1
|
||||||
|
: 0;
|
||||||
|
if (step > 0) {
|
||||||
|
if (hiAuto) {
|
||||||
|
const qreal baseLo =
|
||||||
|
loAuto ? std::floor(dmin / step) * step : lo;
|
||||||
|
const int span = std::max(
|
||||||
|
{static_cast<int>(std::ceil((dmax - baseLo) / step)),
|
||||||
|
minSteps,
|
||||||
|
1});
|
||||||
|
lo = baseLo;
|
||||||
|
hi = baseLo + qreal(span) * step;
|
||||||
|
} else {
|
||||||
|
const int span = std::max(
|
||||||
|
{static_cast<int>(std::ceil((hi - dmin) / step)),
|
||||||
|
minSteps,
|
||||||
|
1});
|
||||||
|
lo = hi - qreal(span) * step;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (loAuto)
|
||||||
|
lo = dmin;
|
||||||
|
if (hiAuto)
|
||||||
|
hi = dmax;
|
||||||
|
if (hi <= lo)
|
||||||
|
hi = lo + 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const qreal scaleBottom = lo - graph->m_yPaddingBottom;
|
||||||
|
const qreal scaleTop = hi + graph->m_yPaddingTop;
|
||||||
|
const qreal range = scaleTop - scaleBottom;
|
||||||
|
if (range <= 0.0) {
|
||||||
|
m_model = model;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto xAt = [&](qreal i) {
|
||||||
|
return model.plot.x + (i / qreal(graph->m_xSteps)) * model.plot.w;
|
||||||
|
};
|
||||||
|
const auto yAt = [&](qreal v) {
|
||||||
|
return model.plot.y
|
||||||
|
+ (1.0 - (v - scaleBottom) / range) * model.plot.h;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto devW = [&](qreal w) {
|
||||||
|
return std::max<qint64>(1, internal::toDev(w, dpr));
|
||||||
|
};
|
||||||
|
const qint64 gridWDev = devW(1.0);
|
||||||
|
const qint64 lineWDev = devW(graph->m_lineWidth);
|
||||||
|
model.gridWidth = internal::fromDev(gridWDev, dpr);
|
||||||
|
model.lineWidth = internal::fromDev(lineWDev, dpr);
|
||||||
|
|
||||||
|
const int vStep = graph->m_verticalLineStep;
|
||||||
|
const int hStep = graph->m_horizontalLineStep;
|
||||||
|
const auto tickVisible = [&](int i) {
|
||||||
|
if (i == 0 && !graph->m_firstXLine)
|
||||||
|
return false;
|
||||||
|
if (i == graph->m_xSteps && !graph->m_lastXLine)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (vStep > 0) {
|
||||||
|
for (int i = 0; i <= graph->m_xSteps; i += vStep) {
|
||||||
|
if (!tickVisible(i))
|
||||||
|
continue;
|
||||||
|
model.vGrid.append(
|
||||||
|
internal::snapLine(xAt(i), gridWDev, dpr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const qreal vStart = lo
|
||||||
|
- (graph->m_yPaddingBottom > 0.0
|
||||||
|
? std::floor(graph->m_yPaddingBottom / hStep)
|
||||||
|
* hStep
|
||||||
|
: 0.0);
|
||||||
|
if (hStep > 0) {
|
||||||
|
for (qreal v = vStart; v <= hi + 0.001; v += hStep)
|
||||||
|
model.hGrid.append(
|
||||||
|
internal::snapLine(yAt(v), gridWDev, dpr));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vStep > 0 && graph->m_xLabelMode != LineGraph::XLabelMode::None) {
|
||||||
|
for (int i = 0; i <= graph->m_xSteps; i += vStep) {
|
||||||
|
if (!tickVisible(i))
|
||||||
|
continue;
|
||||||
|
const QString text = formatXLabel(
|
||||||
|
graph->m_xLabelMode, i, graph->m_useTwelveHourClock);
|
||||||
|
if (!text.isEmpty())
|
||||||
|
model.xLabels.append(Label {
|
||||||
|
text, xAt(i) + 4.0,
|
||||||
|
model.plot.y + model.plot.h + 8.0});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hStep > 0 && graph->m_yLabelMode != LineGraph::YLabelMode::Off) {
|
||||||
|
for (qreal v = vStart; v <= hi + 0.001; v += hStep) {
|
||||||
|
const QString text = formatYLabel(
|
||||||
|
graph->m_yLabelMode, v, graph->m_yLabelDecimals,
|
||||||
|
graph->m_yLabelSuffix);
|
||||||
|
if (!text.isEmpty())
|
||||||
|
model.yLabels.append(Label {text, 2.0, yAt(v) - 2.0});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the series into contiguous runs; each run becomes one curve. Points
|
||||||
|
// land on device-pixel columns so a flat run stays on whole columns.
|
||||||
|
const auto snapX = [&](qreal logical) {
|
||||||
|
return internal::fromDev(internal::toDev(logical, dpr), dpr);
|
||||||
|
};
|
||||||
|
|
||||||
|
QVector<QVector<QPointF>> runs;
|
||||||
|
QVector<QPointF> current;
|
||||||
|
for (int i = 0; i < series.size(); ++i) {
|
||||||
|
if (std::isnan(series[i])) {
|
||||||
|
if (!current.isEmpty()) {
|
||||||
|
runs.append(current);
|
||||||
|
current.clear();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current.append(QPointF(snapX(xAt(i)), yAt(series[i])));
|
||||||
|
}
|
||||||
|
if (!current.isEmpty())
|
||||||
|
runs.append(current);
|
||||||
|
|
||||||
|
const bool curve = graph->m_curveSmoothing;
|
||||||
|
const bool hasMarker =
|
||||||
|
graph->m_markerX >= 0.0 && graph->m_markerX <= graph->m_xSteps;
|
||||||
|
qreal markerPx = 0.0;
|
||||||
|
qreal gapPx = 0.0;
|
||||||
|
if (hasMarker) {
|
||||||
|
markerPx = xAt(graph->m_markerX);
|
||||||
|
qreal slope = 0.0;
|
||||||
|
for (const auto& run : runs) {
|
||||||
|
if (run.size() > 2 && run.first().x() <= markerPx
|
||||||
|
&& markerPx <= run.last().x()
|
||||||
|
&& markerPx - 1.0 >= run.first().x()
|
||||||
|
&& markerPx + 1.0 <= run.last().x()) {
|
||||||
|
slope = (internal::pointAtX(run, markerPx + 1.0, curve).y()
|
||||||
|
- internal::pointAtX(run, markerPx - 1.0, curve)
|
||||||
|
.y())
|
||||||
|
/ 2.0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gapPx = (graph->m_markerDotRadius + graph->m_markerGap)
|
||||||
|
/ std::sqrt(1.0 + slope * slope);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& run : runs) {
|
||||||
|
if (run.size() < 2)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Segment seg;
|
||||||
|
seg.fill = internal::curveVertices(run, dpr, curve);
|
||||||
|
seg.fill.append(QPointF(run.last().x(), model.plotBottom));
|
||||||
|
seg.fill.append(QPointF(run.first().x(), model.plotBottom));
|
||||||
|
|
||||||
|
if (hasMarker) {
|
||||||
|
const QVector<QPointF> left =
|
||||||
|
cutLeft(run, markerPx - gapPx, curve);
|
||||||
|
const QVector<QPointF> right =
|
||||||
|
cutRight(run, markerPx + gapPx, curve);
|
||||||
|
if (left.size() >= 2)
|
||||||
|
seg.strokes.append(
|
||||||
|
internal::curveVertices(left, dpr, curve));
|
||||||
|
if (right.size() >= 2)
|
||||||
|
seg.strokes.append(
|
||||||
|
internal::curveVertices(right, dpr, curve));
|
||||||
|
} else {
|
||||||
|
seg.strokes.append(internal::curveVertices(run, dpr, curve));
|
||||||
|
}
|
||||||
|
model.segments.append(seg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMarker) {
|
||||||
|
model.marker.hasLine = true;
|
||||||
|
model.marker.lineX =
|
||||||
|
internal::snapLine(markerPx, gridWDev, dpr);
|
||||||
|
|
||||||
|
// Dashes are drawn as solid segments: QCanvasPainter strokes are always
|
||||||
|
// solid, so a dashed line is a run of short strokes, as LineGraph.qml did.
|
||||||
|
qreal dashY = model.plot.y;
|
||||||
|
bool on = true;
|
||||||
|
while (dashY < H) {
|
||||||
|
const qreal dashEnd = std::min(dashY + 4.0, H);
|
||||||
|
if (on)
|
||||||
|
model.marker.dash.append(
|
||||||
|
{snapX(dashY), snapX(dashEnd)});
|
||||||
|
dashY = dashEnd;
|
||||||
|
on = !on;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int i0 = static_cast<int>(std::floor(graph->m_markerX));
|
||||||
|
const qreal v0 = (i0 >= 0 && i0 < series.size()) ? series[i0]
|
||||||
|
: kNaN;
|
||||||
|
const qreal v1 = (i0 + 1 >= 0 && i0 + 1 < series.size())
|
||||||
|
? series[i0 + 1]
|
||||||
|
: kNaN;
|
||||||
|
qreal y = kNaN;
|
||||||
|
if (!std::isnan(v0) && !std::isnan(v1))
|
||||||
|
y = yAt(v0 + (v1 - v0) * (graph->m_markerX - qreal(i0)));
|
||||||
|
else if (!std::isnan(v0))
|
||||||
|
y = yAt(v0);
|
||||||
|
if (!std::isnan(y)) {
|
||||||
|
model.marker.hasDot = true;
|
||||||
|
model.marker.dot = QPointF(snapX(markerPx), y);
|
||||||
|
model.marker.dotRadius = graph->m_markerDotRadius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The plot is well-formed: grid, labels and marker can draw even with no curve
|
||||||
|
// segments, matching the QML which painted the grid regardless of data.
|
||||||
|
model.valid = true;
|
||||||
|
m_model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraphRenderer::paint(QCanvasPainter* painter) {
|
||||||
|
painter->clearRect(0, 0, width(), height());
|
||||||
|
if (!m_model.valid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const Model& m = m_model;
|
||||||
|
|
||||||
|
// Grid: every line in one path, one stroke.
|
||||||
|
QColor grid = m_gridColor;
|
||||||
|
grid.setAlphaF(static_cast<float>(m_gridAlpha));
|
||||||
|
painter->setStrokeStyle(grid);
|
||||||
|
painter->setLineWidth(m.gridWidth);
|
||||||
|
painter->beginPath();
|
||||||
|
for (const qreal x : m.vGrid) {
|
||||||
|
painter->moveTo(x, m.plot.y);
|
||||||
|
painter->lineTo(x, m.height);
|
||||||
|
}
|
||||||
|
for (const qreal y : m.hGrid) {
|
||||||
|
painter->moveTo(0, y);
|
||||||
|
painter->lineTo(m.plot.x + m.plot.w, y);
|
||||||
|
}
|
||||||
|
painter->stroke();
|
||||||
|
|
||||||
|
// Axis labels.
|
||||||
|
QColor label = m_labelColor;
|
||||||
|
label.setAlphaF(static_cast<float>(m_labelAlpha));
|
||||||
|
painter->setFillStyle(label);
|
||||||
|
QFont font(QStringLiteral("sans-serif"));
|
||||||
|
font.setPointSizeF(m.labelFontSize);
|
||||||
|
painter->setFont(font);
|
||||||
|
painter->setTextAlign(QCanvasPainter::TextAlign::Left);
|
||||||
|
painter->setTextBaseline(QCanvasPainter::TextBaseline::Top);
|
||||||
|
for (const Label& l : m.xLabels)
|
||||||
|
painter->fillText(l.text, l.x, l.y);
|
||||||
|
painter->setTextBaseline(QCanvasPainter::TextBaseline::Bottom);
|
||||||
|
for (const Label& l : m.yLabels)
|
||||||
|
painter->fillText(l.text, l.x, l.y);
|
||||||
|
|
||||||
|
// Area fill, then the stroked curve pieces on top.
|
||||||
|
QColor fill = m_lineColor;
|
||||||
|
fill.setAlphaF(static_cast<float>(m_fillAlpha));
|
||||||
|
painter->setFillStyle(fill);
|
||||||
|
for (const Segment& seg : m.segments) {
|
||||||
|
painter->beginPath();
|
||||||
|
painter->moveTo(seg.fill.first());
|
||||||
|
for (int i = 1; i < seg.fill.size(); ++i)
|
||||||
|
painter->lineTo(seg.fill[i]);
|
||||||
|
painter->fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
painter->setStrokeStyle(m_lineColor);
|
||||||
|
painter->setLineWidth(m.lineWidth);
|
||||||
|
painter->setLineCap(QCanvasPainter::LineCap::Round);
|
||||||
|
painter->setLineJoin(QCanvasPainter::LineJoin::Round);
|
||||||
|
for (const Segment& seg : m.segments) {
|
||||||
|
for (const auto& poly : seg.strokes) {
|
||||||
|
painter->beginPath();
|
||||||
|
painter->moveTo(poly.first());
|
||||||
|
for (int i = 1; i < poly.size(); ++i)
|
||||||
|
painter->lineTo(poly[i]);
|
||||||
|
painter->stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m.marker.hasLine) {
|
||||||
|
QColor markerLine = m_markerColor;
|
||||||
|
markerLine.setAlphaF(0.6f);
|
||||||
|
painter->setStrokeStyle(markerLine);
|
||||||
|
painter->setLineWidth(m.gridWidth);
|
||||||
|
painter->beginPath();
|
||||||
|
for (const auto& d : m.marker.dash) {
|
||||||
|
painter->moveTo(m.marker.lineX, d.first);
|
||||||
|
painter->lineTo(m.marker.lineX, d.second);
|
||||||
|
}
|
||||||
|
painter->stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m.marker.hasDot) {
|
||||||
|
painter->setFillStyle(m_markerColor);
|
||||||
|
painter->beginPath();
|
||||||
|
painter->circle(
|
||||||
|
m.marker.dot.x(), m.marker.dot.y(), m.marker.dotRadius);
|
||||||
|
painter->fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LineGraph::LineGraph(QQuickItem* parent) : QCanvasPainterItem(parent) {
|
||||||
|
setFillColor(Qt::transparent);
|
||||||
|
setAlphaBlending(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
QCanvasPainterItemRenderer* LineGraph::createItemRenderer() const {
|
||||||
|
return new LineGraphRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::itemChange(ItemChange change, const ItemChangeData& value) {
|
||||||
|
QCanvasPainterItem::itemChange(change, value);
|
||||||
|
if (change == ItemSceneChange && value.window)
|
||||||
|
m_dpr = value.window->effectiveDevicePixelRatio();
|
||||||
|
else if (change == ItemDevicePixelRatioHasChanged && window())
|
||||||
|
m_dpr = window()->effectiveDevicePixelRatio();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList LineGraph::values() const {
|
||||||
|
return m_values;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setValues(const QVariantList& values) {
|
||||||
|
if (m_values == values) return;
|
||||||
|
m_values = values;
|
||||||
|
emit valuesChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
int LineGraph::xSteps() const {
|
||||||
|
return m_xSteps;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setXSteps(int xSteps) {
|
||||||
|
if (m_xSteps == xSteps) return;
|
||||||
|
m_xSteps = xSteps;
|
||||||
|
emit xStepsChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal LineGraph::yMin() const {
|
||||||
|
return m_yMin;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setYMin(qreal value) {
|
||||||
|
if (sameReal(m_yMin, value)) return;
|
||||||
|
m_yMin = value;
|
||||||
|
emit yMinChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal LineGraph::yMax() const {
|
||||||
|
return m_yMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setYMax(qreal value) {
|
||||||
|
if (sameReal(m_yMax, value)) return;
|
||||||
|
m_yMax = value;
|
||||||
|
emit yMaxChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal LineGraph::yPaddingTop() const {
|
||||||
|
return m_yPaddingTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setYPaddingTop(qreal value) {
|
||||||
|
if (sameReal(m_yPaddingTop, value)) return;
|
||||||
|
m_yPaddingTop = value;
|
||||||
|
emit yPaddingTopChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal LineGraph::yPaddingBottom() const {
|
||||||
|
return m_yPaddingBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setYPaddingBottom(qreal value) {
|
||||||
|
if (sameReal(m_yPaddingBottom, value)) return;
|
||||||
|
m_yPaddingBottom = value;
|
||||||
|
emit yPaddingBottomChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
int LineGraph::verticalLineStep() const {
|
||||||
|
return m_verticalLineStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setVerticalLineStep(int step) {
|
||||||
|
if (m_verticalLineStep == step) return;
|
||||||
|
m_verticalLineStep = step;
|
||||||
|
emit verticalLineStepChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
int LineGraph::horizontalLineStep() const {
|
||||||
|
return m_horizontalLineStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setHorizontalLineStep(int step) {
|
||||||
|
if (m_horizontalLineStep == step) return;
|
||||||
|
m_horizontalLineStep = step;
|
||||||
|
emit horizontalLineStepChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LineGraph::firstXLine() const {
|
||||||
|
return m_firstXLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setFirstXLine(bool show) {
|
||||||
|
if (m_firstXLine == show) return;
|
||||||
|
m_firstXLine = show;
|
||||||
|
emit firstXLineChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LineGraph::lastXLine() const {
|
||||||
|
return m_lastXLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setLastXLine(bool show) {
|
||||||
|
if (m_lastXLine == show) return;
|
||||||
|
m_lastXLine = show;
|
||||||
|
emit lastXLineChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
int LineGraph::minHorizontalLines() const {
|
||||||
|
return m_minHorizontalLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setMinHorizontalLines(int count) {
|
||||||
|
if (m_minHorizontalLines == count) return;
|
||||||
|
m_minHorizontalLines = count;
|
||||||
|
emit minHorizontalLinesChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
LineGraph::XLabelMode LineGraph::xLabelMode() const {
|
||||||
|
return m_xLabelMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setXLabelMode(XLabelMode mode) {
|
||||||
|
if (m_xLabelMode == mode) return;
|
||||||
|
m_xLabelMode = mode;
|
||||||
|
emit xLabelModeChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LineGraph::useTwelveHourClock() const {
|
||||||
|
return m_useTwelveHourClock;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setUseTwelveHourClock(bool use) {
|
||||||
|
if (m_useTwelveHourClock == use) return;
|
||||||
|
m_useTwelveHourClock = use;
|
||||||
|
emit useTwelveHourClockChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
LineGraph::YLabelMode LineGraph::yLabelMode() const {
|
||||||
|
return m_yLabelMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setYLabelMode(YLabelMode mode) {
|
||||||
|
if (m_yLabelMode == mode) return;
|
||||||
|
m_yLabelMode = mode;
|
||||||
|
emit yLabelModeChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString LineGraph::yLabelSuffix() const {
|
||||||
|
return m_yLabelSuffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setYLabelSuffix(const QString& suffix) {
|
||||||
|
if (m_yLabelSuffix == suffix) return;
|
||||||
|
m_yLabelSuffix = suffix;
|
||||||
|
emit yLabelSuffixChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
int LineGraph::yLabelDecimals() const {
|
||||||
|
return m_yLabelDecimals;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setYLabelDecimals(int decimals) {
|
||||||
|
if (m_yLabelDecimals == decimals) return;
|
||||||
|
m_yLabelDecimals = decimals;
|
||||||
|
emit yLabelDecimalsChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal LineGraph::smoothing() const {
|
||||||
|
return m_smoothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setSmoothing(qreal smoothing) {
|
||||||
|
smoothing = qMax<qreal>(0.0, smoothing);
|
||||||
|
if (sameReal(m_smoothing, smoothing)) return;
|
||||||
|
m_smoothing = smoothing;
|
||||||
|
emit smoothingChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LineGraph::curveSmoothing() const {
|
||||||
|
return m_curveSmoothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setCurveSmoothing(bool enabled) {
|
||||||
|
if (m_curveSmoothing == enabled) return;
|
||||||
|
m_curveSmoothing = enabled;
|
||||||
|
emit curveSmoothingChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal LineGraph::markerX() const {
|
||||||
|
return m_markerX;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setMarkerX(qreal markerX) {
|
||||||
|
if (sameReal(m_markerX, markerX)) return;
|
||||||
|
m_markerX = markerX;
|
||||||
|
emit markerXChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal LineGraph::markerGap() const {
|
||||||
|
return m_markerGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setMarkerGap(qreal gap) {
|
||||||
|
if (sameReal(m_markerGap, gap)) return;
|
||||||
|
m_markerGap = gap;
|
||||||
|
emit markerGapChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor LineGraph::lineColor() const {
|
||||||
|
return m_lineColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setLineColor(const QColor& color) {
|
||||||
|
if (m_lineColor == color) return;
|
||||||
|
m_lineColor = color;
|
||||||
|
emit lineColorChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor LineGraph::gridColor() const {
|
||||||
|
return m_gridColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setGridColor(const QColor& color) {
|
||||||
|
if (m_gridColor == color) return;
|
||||||
|
m_gridColor = color;
|
||||||
|
emit gridColorChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor LineGraph::markerColor() const {
|
||||||
|
return m_markerColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setMarkerColor(const QColor& color) {
|
||||||
|
if (m_markerColor == color) return;
|
||||||
|
m_markerColor = color;
|
||||||
|
emit markerColorChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor LineGraph::labelColor() const {
|
||||||
|
return m_labelColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setLabelColor(const QColor& color) {
|
||||||
|
if (m_labelColor == color) return;
|
||||||
|
m_labelColor = color;
|
||||||
|
emit labelColorChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal LineGraph::gridAlpha() const {
|
||||||
|
return m_gridAlpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setGridAlpha(qreal alpha) {
|
||||||
|
if (sameReal(m_gridAlpha, alpha)) return;
|
||||||
|
m_gridAlpha = alpha;
|
||||||
|
emit gridAlphaChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal LineGraph::labelAlpha() const {
|
||||||
|
return m_labelAlpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setLabelAlpha(qreal alpha) {
|
||||||
|
if (sameReal(m_labelAlpha, alpha)) return;
|
||||||
|
m_labelAlpha = alpha;
|
||||||
|
emit labelAlphaChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal LineGraph::fillAlpha() const {
|
||||||
|
return m_fillAlpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setFillAlpha(qreal alpha) {
|
||||||
|
if (sameReal(m_fillAlpha, alpha)) return;
|
||||||
|
m_fillAlpha = alpha;
|
||||||
|
emit fillAlphaChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal LineGraph::lineWidth() const {
|
||||||
|
return m_lineWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setLineWidth(qreal width) {
|
||||||
|
if (sameReal(m_lineWidth, width)) return;
|
||||||
|
m_lineWidth = width;
|
||||||
|
emit lineWidthChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal LineGraph::markerDotRadius() const {
|
||||||
|
return m_markerDotRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setMarkerDotRadius(qreal radius) {
|
||||||
|
if (sameReal(m_markerDotRadius, radius)) return;
|
||||||
|
m_markerDotRadius = radius;
|
||||||
|
emit markerDotRadiusChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal LineGraph::labelFontSize() const {
|
||||||
|
return m_labelFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineGraph::setLabelFontSize(qreal size) {
|
||||||
|
if (sameReal(m_labelFontSize, size)) return;
|
||||||
|
m_labelFontSize = size;
|
||||||
|
emit labelFontSizeChanged();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZShell::components
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QCanvasPainterItem>
|
||||||
|
#include <QColor>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
#include <qvariant.h>
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace ZShell::components {
|
||||||
|
|
||||||
|
class LineGraphRenderer;
|
||||||
|
|
||||||
|
// A line graph with an optional grid, axis labels, y auto-scaling, a moving
|
||||||
|
// average, smoothed Catmull-Rom curve, an area fill and a marker. Static: it draws
|
||||||
|
// whatever `values` holds at the given x steps, unlike Sparkline which slides a
|
||||||
|
// rolling buffer. Both share curve smoothing and device-pixel alignment through
|
||||||
|
// internal::linegeometry.
|
||||||
|
class LineGraph : public QCanvasPainterItem {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
Q_PROPERTY(
|
||||||
|
QVariantList values READ values WRITE setValues NOTIFY valuesChanged
|
||||||
|
FINAL)
|
||||||
|
Q_PROPERTY(int xSteps READ xSteps WRITE setXSteps NOTIFY xStepsChanged FINAL)
|
||||||
|
Q_PROPERTY(qreal yMin READ yMin WRITE setYMin NOTIFY yMinChanged FINAL)
|
||||||
|
Q_PROPERTY(qreal yMax READ yMax WRITE setYMax NOTIFY yMaxChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
qreal yPaddingTop READ yPaddingTop WRITE setYPaddingTop NOTIFY
|
||||||
|
yPaddingTopChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
qreal yPaddingBottom READ yPaddingBottom WRITE setYPaddingBottom NOTIFY
|
||||||
|
yPaddingBottomChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
int verticalLineStep READ verticalLineStep WRITE setVerticalLineStep
|
||||||
|
NOTIFY verticalLineStepChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
int horizontalLineStep READ horizontalLineStep WRITE
|
||||||
|
setHorizontalLineStep NOTIFY horizontalLineStepChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
bool firstXLine READ firstXLine WRITE setFirstXLine NOTIFY
|
||||||
|
firstXLineChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
bool lastXLine READ lastXLine WRITE setLastXLine NOTIFY
|
||||||
|
lastXLineChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
int minHorizontalLines READ minHorizontalLines WRITE
|
||||||
|
setMinHorizontalLines NOTIFY minHorizontalLinesChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
XLabelMode xLabelMode READ xLabelMode WRITE setXLabelMode NOTIFY
|
||||||
|
xLabelModeChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
bool useTwelveHourClock READ useTwelveHourClock WRITE
|
||||||
|
setUseTwelveHourClock NOTIFY useTwelveHourClockChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
YLabelMode yLabelMode READ yLabelMode WRITE setYLabelMode NOTIFY
|
||||||
|
yLabelModeChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QString yLabelSuffix READ yLabelSuffix WRITE setYLabelSuffix NOTIFY
|
||||||
|
yLabelSuffixChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
int yLabelDecimals READ yLabelDecimals WRITE setYLabelDecimals NOTIFY
|
||||||
|
yLabelDecimalsChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
qreal smoothing READ smoothing WRITE setSmoothing NOTIFY
|
||||||
|
smoothingChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
bool curveSmoothing READ curveSmoothing WRITE setCurveSmoothing NOTIFY
|
||||||
|
curveSmoothingChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
qreal markerX READ markerX WRITE setMarkerX NOTIFY markerXChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
qreal markerGap READ markerGap WRITE setMarkerGap NOTIFY
|
||||||
|
markerGapChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QColor lineColor READ lineColor WRITE setLineColor NOTIFY
|
||||||
|
lineColorChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QColor gridColor READ gridColor WRITE setGridColor NOTIFY
|
||||||
|
gridColorChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QColor markerColor READ markerColor WRITE setMarkerColor NOTIFY
|
||||||
|
markerColorChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QColor labelColor READ labelColor WRITE setLabelColor NOTIFY
|
||||||
|
labelColorChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
qreal gridAlpha READ gridAlpha WRITE setGridAlpha NOTIFY
|
||||||
|
gridAlphaChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
qreal labelAlpha READ labelAlpha WRITE setLabelAlpha NOTIFY
|
||||||
|
labelAlphaChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
qreal fillAlpha READ fillAlpha WRITE setFillAlpha NOTIFY
|
||||||
|
fillAlphaChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY
|
||||||
|
lineWidthChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
qreal markerDotRadius READ markerDotRadius WRITE setMarkerDotRadius
|
||||||
|
NOTIFY markerDotRadiusChanged FINAL)
|
||||||
|
Q_PROPERTY(
|
||||||
|
qreal labelFontSize READ labelFontSize WRITE setLabelFontSize NOTIFY
|
||||||
|
labelFontSizeChanged FINAL)
|
||||||
|
|
||||||
|
public:
|
||||||
|
// How an x tick (integer index 0..xSteps) is labelled. Hour renders an
|
||||||
|
// hour-of-day, honouring useTwelveHourClock.
|
||||||
|
enum class XLabelMode { None, Number, Hour };
|
||||||
|
Q_ENUM(XLabelMode)
|
||||||
|
|
||||||
|
// How a y tick value is labelled. Value renders the number rounded to
|
||||||
|
// yLabelDecimals places, followed by yLabelSuffix.
|
||||||
|
enum class YLabelMode { Off, Value };
|
||||||
|
Q_ENUM(YLabelMode)
|
||||||
|
|
||||||
|
explicit LineGraph(QQuickItem* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] QVariantList values() const;
|
||||||
|
void setValues(const QVariantList& values);
|
||||||
|
|
||||||
|
[[nodiscard]] int xSteps() const;
|
||||||
|
void setXSteps(int xSteps);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal yMin() const;
|
||||||
|
void setYMin(qreal value);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal yMax() const;
|
||||||
|
void setYMax(qreal value);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal yPaddingTop() const;
|
||||||
|
void setYPaddingTop(qreal value);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal yPaddingBottom() const;
|
||||||
|
void setYPaddingBottom(qreal value);
|
||||||
|
|
||||||
|
[[nodiscard]] int verticalLineStep() const;
|
||||||
|
void setVerticalLineStep(int step);
|
||||||
|
|
||||||
|
[[nodiscard]] int horizontalLineStep() const;
|
||||||
|
void setHorizontalLineStep(int step);
|
||||||
|
|
||||||
|
[[nodiscard]] bool firstXLine() const;
|
||||||
|
void setFirstXLine(bool show);
|
||||||
|
|
||||||
|
[[nodiscard]] bool lastXLine() const;
|
||||||
|
void setLastXLine(bool show);
|
||||||
|
|
||||||
|
[[nodiscard]] int minHorizontalLines() const;
|
||||||
|
void setMinHorizontalLines(int count);
|
||||||
|
|
||||||
|
[[nodiscard]] XLabelMode xLabelMode() const;
|
||||||
|
void setXLabelMode(XLabelMode mode);
|
||||||
|
|
||||||
|
[[nodiscard]] bool useTwelveHourClock() const;
|
||||||
|
void setUseTwelveHourClock(bool use);
|
||||||
|
|
||||||
|
[[nodiscard]] YLabelMode yLabelMode() const;
|
||||||
|
void setYLabelMode(YLabelMode mode);
|
||||||
|
|
||||||
|
[[nodiscard]] QString yLabelSuffix() const;
|
||||||
|
void setYLabelSuffix(const QString& suffix);
|
||||||
|
|
||||||
|
[[nodiscard]] int yLabelDecimals() const;
|
||||||
|
void setYLabelDecimals(int decimals);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal smoothing() const;
|
||||||
|
void setSmoothing(qreal smoothing);
|
||||||
|
|
||||||
|
[[nodiscard]] bool curveSmoothing() const;
|
||||||
|
void setCurveSmoothing(bool enabled);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal markerX() const;
|
||||||
|
void setMarkerX(qreal markerX);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal markerGap() const;
|
||||||
|
void setMarkerGap(qreal gap);
|
||||||
|
|
||||||
|
[[nodiscard]] QColor lineColor() const;
|
||||||
|
void setLineColor(const QColor& color);
|
||||||
|
|
||||||
|
[[nodiscard]] QColor gridColor() const;
|
||||||
|
void setGridColor(const QColor& color);
|
||||||
|
|
||||||
|
[[nodiscard]] QColor markerColor() const;
|
||||||
|
void setMarkerColor(const QColor& color);
|
||||||
|
|
||||||
|
[[nodiscard]] QColor labelColor() const;
|
||||||
|
void setLabelColor(const QColor& color);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal gridAlpha() const;
|
||||||
|
void setGridAlpha(qreal alpha);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal labelAlpha() const;
|
||||||
|
void setLabelAlpha(qreal alpha);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal fillAlpha() const;
|
||||||
|
void setFillAlpha(qreal alpha);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal lineWidth() const;
|
||||||
|
void setLineWidth(qreal width);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal markerDotRadius() const;
|
||||||
|
void setMarkerDotRadius(qreal radius);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal labelFontSize() const;
|
||||||
|
void setLabelFontSize(qreal size);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void valuesChanged();
|
||||||
|
void xStepsChanged();
|
||||||
|
void yMinChanged();
|
||||||
|
void yMaxChanged();
|
||||||
|
void yPaddingTopChanged();
|
||||||
|
void yPaddingBottomChanged();
|
||||||
|
void verticalLineStepChanged();
|
||||||
|
void horizontalLineStepChanged();
|
||||||
|
void firstXLineChanged();
|
||||||
|
void lastXLineChanged();
|
||||||
|
void minHorizontalLinesChanged();
|
||||||
|
void xLabelModeChanged();
|
||||||
|
void useTwelveHourClockChanged();
|
||||||
|
void yLabelModeChanged();
|
||||||
|
void yLabelSuffixChanged();
|
||||||
|
void yLabelDecimalsChanged();
|
||||||
|
void smoothingChanged();
|
||||||
|
void curveSmoothingChanged();
|
||||||
|
void markerXChanged();
|
||||||
|
void markerGapChanged();
|
||||||
|
void lineColorChanged();
|
||||||
|
void gridColorChanged();
|
||||||
|
void markerColorChanged();
|
||||||
|
void labelColorChanged();
|
||||||
|
void gridAlphaChanged();
|
||||||
|
void labelAlphaChanged();
|
||||||
|
void fillAlphaChanged();
|
||||||
|
void lineWidthChanged();
|
||||||
|
void markerDotRadiusChanged();
|
||||||
|
void labelFontSizeChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
[[nodiscard]] QCanvasPainterItemRenderer* createItemRenderer()
|
||||||
|
const override;
|
||||||
|
|
||||||
|
// Device-pixel snapping and stroke width are quantized in device pixels, so
|
||||||
|
// they must use the ratio the window actually has.
|
||||||
|
void itemChange(ItemChange change, const ItemChangeData& value) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class LineGraphRenderer;
|
||||||
|
|
||||||
|
QVariantList m_values;
|
||||||
|
int m_xSteps = 24;
|
||||||
|
qreal m_yMin = std::numeric_limits<qreal>::quiet_NaN();
|
||||||
|
qreal m_yMax = std::numeric_limits<qreal>::quiet_NaN();
|
||||||
|
qreal m_yPaddingTop = 0.0;
|
||||||
|
qreal m_yPaddingBottom = 0.0;
|
||||||
|
int m_verticalLineStep = 0;
|
||||||
|
int m_horizontalLineStep = 0;
|
||||||
|
bool m_firstXLine = true;
|
||||||
|
bool m_lastXLine = false;
|
||||||
|
int m_minHorizontalLines = 0;
|
||||||
|
XLabelMode m_xLabelMode = XLabelMode::None;
|
||||||
|
bool m_useTwelveHourClock = false;
|
||||||
|
YLabelMode m_yLabelMode = YLabelMode::Off;
|
||||||
|
QString m_yLabelSuffix;
|
||||||
|
int m_yLabelDecimals = 0;
|
||||||
|
qreal m_smoothing = 0.0;
|
||||||
|
bool m_curveSmoothing = true;
|
||||||
|
qreal m_markerX = -1.0;
|
||||||
|
qreal m_markerGap = 2.0;
|
||||||
|
QColor m_lineColor = Qt::white;
|
||||||
|
QColor m_gridColor = Qt::white;
|
||||||
|
QColor m_markerColor = Qt::white;
|
||||||
|
QColor m_labelColor = Qt::white;
|
||||||
|
qreal m_gridAlpha = 0.12;
|
||||||
|
qreal m_labelAlpha = 0.6;
|
||||||
|
qreal m_fillAlpha = 0.15;
|
||||||
|
qreal m_lineWidth = 2.0;
|
||||||
|
qreal m_markerDotRadius = 4.0;
|
||||||
|
qreal m_labelFontSize = 8.0;
|
||||||
|
qreal m_dpr = 1.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZShell::components
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "sparklinecontent.hpp"
|
#include "sparklinecontent.hpp"
|
||||||
|
|
||||||
|
#include "internal/linegeometry.hpp"
|
||||||
|
|
||||||
#include <qmath.h>
|
#include <qmath.h>
|
||||||
#include <qpainter.h>
|
#include <qpainter.h>
|
||||||
#include <qpen.h>
|
#include <qpen.h>
|
||||||
@@ -10,85 +12,47 @@
|
|||||||
|
|
||||||
namespace ZShell::components {
|
namespace ZShell::components {
|
||||||
|
|
||||||
|
using internal::fromDev;
|
||||||
|
using internal::snapLine;
|
||||||
|
using internal::toDev;
|
||||||
|
|
||||||
|
// Device-pixel snapping, bezier sampling and Catmull-Rom controls come from
|
||||||
|
// internal::linegeometry, shared with LineGraph so both graphs align and smooth
|
||||||
|
// their lines identically.
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
qint64 toDev(qreal logical, qreal dpr) {
|
// A QQuickPaintedItem only lands on the device grid when its logical size is a
|
||||||
return std::llround(logical * dpr);
|
// whole number *and* that whole number times dpr is a whole number as well.
|
||||||
}
|
// Qt derives the painter node from qRound(size) (qquickpainteditem.cpp), makes
|
||||||
|
// the texture from that rounded size times dpr, scales the painter by
|
||||||
qreal fromDev(qint64 device, qreal dpr) {
|
// texture/size rather than by dpr, and draws the texture into a quad of the
|
||||||
return qreal(device) / dpr;
|
// rounded size (qsgdefaultpainternode.cpp). Any fraction therefore moves the
|
||||||
}
|
// painter scale off dpr - a height of 75.3333 at dpr 1.5 becomes a node of 75
|
||||||
|
// and a texture of qRound(112.5) = 113, a scale of 1.5067, so every y snapped
|
||||||
// A line center whose device position leaves a whole-pixel-wide stroke: an even
|
// to the dpr 1.5 grid falls between texture rows and the whole texture is then
|
||||||
// width sits on pixel boundaries, an odd width on pixel centers. Same rule as
|
// resampled to a 112.5 pixel tall quad. That is the fuzz. 74 -> 111 and
|
||||||
// snapLine() in Components/LineGraph.qml.
|
// 76 -> 114 are both exact.
|
||||||
qreal snapLine(qreal logical, qint64 widthDev, qreal dpr) {
|
//
|
||||||
const qreal half = qreal(widthDev) / 2.0;
|
// `grow` picks the direction: shrink to stay inside the slot, grow to stay
|
||||||
return (static_cast<qreal>(std::llround(logical * dpr - half)) + half) /
|
// outside it. The overhang on the right has to grow, or the texture's own edge
|
||||||
dpr;
|
// moves inside the parent's clip and slides left and right with the scroll.
|
||||||
}
|
qreal snapPaintedExtent(qreal logical, qreal dpr, bool grow) {
|
||||||
|
qreal whole = grow ? std::ceil(logical - 1e-6) : std::floor(logical + 1e-6);
|
||||||
// y of a cubic bezier at targetX, found by bisection on the parameter. Used to
|
for (int guard = 0; guard < 16 && whole >= 1.0; ++guard) {
|
||||||
// place one vertex per device pixel column, so a segment never interpolates its
|
const qreal device = whole * dpr;
|
||||||
// edge across several columns.
|
if (std::abs(device - std::round(device)) < 1e-6)
|
||||||
qreal cubicY(
|
return whole;
|
||||||
const QPointF& p1,
|
whole += grow ? 1.0 : -1.0;
|
||||||
const QPointF& c1,
|
|
||||||
const QPointF& c2,
|
|
||||||
const QPointF& p2,
|
|
||||||
qreal targetX) {
|
|
||||||
qreal lo = 0.0;
|
|
||||||
qreal hi = 1.0;
|
|
||||||
for (int it = 0; it < 16; ++it) {
|
|
||||||
const qreal t = (lo + hi) / 2.0;
|
|
||||||
const qreal u = 1.0 - t;
|
|
||||||
const qreal x = u * u * u * p1.x() + 3 * u * u * t * c1.x() +
|
|
||||||
3 * u * t * t * c2.x() + t * t * t * p2.x();
|
|
||||||
if (x < targetX)
|
|
||||||
lo = t;
|
|
||||||
else
|
|
||||||
hi = t;
|
|
||||||
}
|
}
|
||||||
|
// No whole extent within the guard window (an exotic dpr): a whole number is
|
||||||
const qreal t = (lo + hi) / 2.0;
|
// still closer than a fractional one, which Qt rounds away anyway.
|
||||||
const qreal u = 1.0 - t;
|
return std::max(1.0, grow ? whole : std::floor(logical));
|
||||||
return u * u * u * p1.y() + 3 * u * u * t * c1.y() +
|
|
||||||
3 * u * t * t * c2.y() + t * t * t * p2.y();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Catmull-Rom controls with the control y clamped to the segment's own y span,
|
|
||||||
// so a curve never overshoots past its endpoints.
|
|
||||||
void segmentControls(
|
|
||||||
const QVector<QPointF>& points,
|
|
||||||
int i,
|
|
||||||
bool curveSmoothing,
|
|
||||||
QPointF& c1,
|
|
||||||
QPointF& c2) {
|
|
||||||
const QPointF& p0 = points[qMax(i - 1, 0)];
|
|
||||||
const QPointF& p1 = points[i];
|
|
||||||
const QPointF& p2 = points[i + 1];
|
|
||||||
const QPointF& p3 = points[qMin(i + 2, points.size() - 1)];
|
|
||||||
|
|
||||||
if (curveSmoothing && points.size() > 2) {
|
|
||||||
const qreal k = 1.0 / 6.0;
|
|
||||||
c1 = p1 + (p2 - p0) * k;
|
|
||||||
c2 = p2 - (p3 - p1) * k;
|
|
||||||
|
|
||||||
const qreal minY = qMin(p1.y(), p2.y());
|
|
||||||
const qreal maxY = qMax(p1.y(), p2.y());
|
|
||||||
c1.setY(qBound(minY, c1.y(), maxY));
|
|
||||||
c2.setY(qBound(minY, c2.y(), maxY));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const qreal cx = (p1.x() + p2.x()) * 0.5;
|
|
||||||
c1 = QPointF(cx, p1.y());
|
|
||||||
c2 = QPointF(cx, p2.y());
|
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
SparklineContent::SparklineContent(QQuickItem* parent)
|
SparklineContent::SparklineContent(QQuickItem* parent)
|
||||||
: QQuickPaintedItem(parent) {
|
: QQuickPaintedItem(parent) {
|
||||||
|
m_sampleClock.start();
|
||||||
setAntialiasing(true);
|
setAntialiasing(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,12 +75,15 @@ qreal SparklineContent::stepX() const {
|
|||||||
2,
|
2,
|
||||||
(m_line1 && m_line1->count() >= 2) ? m_line1->count()
|
(m_line1 && m_line1->count() >= 2) ? m_line1->count()
|
||||||
: (m_line2 ? m_line2->count() : 2));
|
: (m_line2 ? m_line2->count() : 2));
|
||||||
const qreal logical =
|
// Exactly the slot divided by the history, in logical pixels. Rounding this
|
||||||
m_sliding ? m_viewportW / static_cast<qreal>(m_historyLength - 1)
|
// to a whole device pixel was tempting - it puts every sample on a pixel
|
||||||
|
// column - but the rounding error accumulates over the whole window: history
|
||||||
|
// - 1 steps then no longer span the slot, so the oldest value either leaves
|
||||||
|
// the clip before its tick (rounding up) or sits at the edge until the buffer
|
||||||
|
// evicts it, popping out at the tick (rounding down). crispness does not need
|
||||||
|
// it: internal::curveVertices puts a vertex on every device column anyway.
|
||||||
|
return m_sliding ? m_viewportW / static_cast<qreal>(m_historyLength - 1)
|
||||||
: m_viewportW / static_cast<qreal>(len - 1);
|
: m_viewportW / static_cast<qreal>(len - 1);
|
||||||
// A whole device pixel per step: every sample lands on a pixel column, and
|
|
||||||
// one slide step moves the content by an exact pixel count.
|
|
||||||
return fromDev((std::max)(qint64(1), toDev(logical, m_dpr)), m_dpr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SparklineContent::setViewportSize(
|
void SparklineContent::setViewportSize(
|
||||||
@@ -129,6 +96,30 @@ void SparklineContent::setViewportSize(
|
|||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SparklineContent::setSlideInterval(qreal intervalMs) {
|
||||||
|
if (qFuzzyCompare(m_slideIntervalMs, intervalMs)) return;
|
||||||
|
m_slideIntervalMs = intervalMs;
|
||||||
|
m_sampleClock.restart();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal SparklineContent::slidePhase(qreal progress) const {
|
||||||
|
if (m_slideIntervalMs <= 0.0) return qBound(0.0, progress, 1.0);
|
||||||
|
// The sample clock, not the animation's timeline. The NumberAnimation runs on
|
||||||
|
// the render-loop clock while the sample arrives from a QTimer of the same
|
||||||
|
// duration, so it reaches 1 on the last frame before that timer fires and then
|
||||||
|
// stops. For that frame the newest value is parked exactly on the parent's
|
||||||
|
// clip edge - it shows up at the clipped edge instead of sliding in from
|
||||||
|
// beyond it, and the following frame travels nothing. Holding is capped at one
|
||||||
|
// step, so a stalled producer cannot scroll the curve into empty space.
|
||||||
|
const qreal elapsed = static_cast<qreal>(m_sampleClock.elapsed());
|
||||||
|
return qBound(0.0, elapsed / m_slideIntervalMs, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SparklineContent::setSlideProgress(qreal progress) {
|
||||||
|
setX(-stepX() * slidePhase(progress));
|
||||||
|
}
|
||||||
|
|
||||||
void SparklineContent::setLine1(internal::CircularBuffer* buffer) {
|
void SparklineContent::setLine1(internal::CircularBuffer* buffer) {
|
||||||
if (m_line1 == buffer) return;
|
if (m_line1 == buffer) return;
|
||||||
if (m_line1) disconnect(m_line1, nullptr, this, nullptr);
|
if (m_line1) disconnect(m_line1, nullptr, this, nullptr);
|
||||||
@@ -145,13 +136,24 @@ void SparklineContent::setLine2(internal::CircularBuffer* buffer) {
|
|||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SparklineContent::onSampleArrived() {
|
||||||
|
m_sampleClock.restart();
|
||||||
|
invalidate();
|
||||||
|
// The offset reset and the buffer shifting its values by one step are a
|
||||||
|
// single change: applied here, both are pending before the next sync, so they
|
||||||
|
// cannot land in different frames. Left to the animation to write, a frame
|
||||||
|
// can paint the reindexed values with the previous cycle's full offset still
|
||||||
|
// in place, which moves the whole graph one extra step left for that frame.
|
||||||
|
setSlideProgress(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
void SparklineContent::connectBuffer(internal::CircularBuffer* buffer) {
|
void SparklineContent::connectBuffer(internal::CircularBuffer* buffer) {
|
||||||
if (!buffer) return;
|
if (!buffer) return;
|
||||||
connect(
|
connect(
|
||||||
buffer,
|
buffer,
|
||||||
&internal::CircularBuffer::valuesChanged,
|
&internal::CircularBuffer::valuesChanged,
|
||||||
this,
|
this,
|
||||||
&SparklineContent::invalidate);
|
&SparklineContent::onSampleArrived);
|
||||||
connect(buffer, &QObject::destroyed, this, [this, buffer]() {
|
connect(buffer, &QObject::destroyed, this, [this, buffer]() {
|
||||||
if (m_line1 == buffer) m_line1 = nullptr;
|
if (m_line1 == buffer) m_line1 = nullptr;
|
||||||
if (m_line2 == buffer) m_line2 = nullptr;
|
if (m_line2 == buffer) m_line2 = nullptr;
|
||||||
@@ -220,8 +222,20 @@ void SparklineContent::invalidate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SparklineContent::applyGeometry() {
|
void SparklineContent::applyGeometry() {
|
||||||
const qreal pad = m_sliding ? stepX() : 0.0;
|
const qreal dpr = m_dpr > 0.0 ? m_dpr : 1.0;
|
||||||
setSize(QSizeF(m_viewportW + pad, m_viewportH));
|
// The newest value is drawn one step past the slot's right edge, so the child
|
||||||
|
// has to overhang the slot and the overhang is offset left as it slides. Two
|
||||||
|
// steps, not one: the newest vertex sits at one step past the edge and its
|
||||||
|
// stroke cap extends beyond that, so a one-step overhang puts the texture's
|
||||||
|
// own right edge exactly on the clip boundary at the end of a cycle, where the
|
||||||
|
// cut-off ink becomes visible for a frame.
|
||||||
|
const qreal pad = m_sliding ? 2.0 * stepX() : 0.0;
|
||||||
|
// Qt rounds the painted size anyway, so choose a whole value that keeps
|
||||||
|
// device alignment. Width grows, to keep that overhang outside the clip;
|
||||||
|
// height shrinks, to stay inside the slot.
|
||||||
|
const qreal paintedW = snapPaintedExtent(m_viewportW + pad, dpr, true);
|
||||||
|
m_paintedH = snapPaintedExtent(m_viewportH, dpr, false);
|
||||||
|
setSize(QSizeF(paintedW, m_paintedH));
|
||||||
}
|
}
|
||||||
|
|
||||||
SparklineContent::LineGeometry SparklineContent::buildGeometry(
|
SparklineContent::LineGeometry SparklineContent::buildGeometry(
|
||||||
@@ -232,7 +246,8 @@ SparklineContent::LineGeometry SparklineContent::buildGeometry(
|
|||||||
// path stroker as garbage, which draws a shape that ignores the curve.
|
// path stroker as garbage, which draws a shape that ignores the curve.
|
||||||
if (!buffer || buffer->count() < 2 || !std::isfinite(m_maxValue) ||
|
if (!buffer || buffer->count() < 2 || !std::isfinite(m_maxValue) ||
|
||||||
m_maxValue <= 0.0 || !std::isfinite(m_viewportW) ||
|
m_maxValue <= 0.0 || !std::isfinite(m_viewportW) ||
|
||||||
m_viewportW <= 0.0 || !std::isfinite(m_viewportH) || m_viewportH <= 0.0)
|
m_viewportW <= 0.0 || !std::isfinite(m_viewportH) || m_viewportH <= 0.0 ||
|
||||||
|
m_paintedH < 1.0)
|
||||||
return geom;
|
return geom;
|
||||||
if (m_historyLength < 2) return geom;
|
if (m_historyLength < 2) return geom;
|
||||||
|
|
||||||
@@ -241,18 +256,18 @@ SparklineContent::LineGeometry SparklineContent::buildGeometry(
|
|||||||
const qreal step = stepX();
|
const qreal step = stepX();
|
||||||
if (!std::isfinite(step) || step <= 0.0) return geom;
|
if (!std::isfinite(step) || step <= 0.0) return geom;
|
||||||
|
|
||||||
const qint64 stepDev = toDev(step, dpr);
|
|
||||||
const qint64 lineDev = std::isfinite(m_lineWidth)
|
const qint64 lineDev = std::isfinite(m_lineWidth)
|
||||||
? (std::max)(qint64(1), toDev(m_lineWidth, dpr))
|
? (std::max)(qint64(1), toDev(m_lineWidth, dpr))
|
||||||
: qint64(1);
|
: qint64(1);
|
||||||
const qint64 heightDev = toDev(m_viewportH, dpr);
|
// The painted height, not the slot's: it is the number of texture rows that
|
||||||
|
// actually exist, so the plot and the fill bottom match them exactly.
|
||||||
|
const qint64 heightDev = toDev(m_paintedH, dpr);
|
||||||
|
|
||||||
qreal startX = 0.0;
|
qreal startX = 0.0;
|
||||||
if (m_sliding)
|
if (m_sliding)
|
||||||
startX = m_viewportW - (len - 1) * step + step;
|
startX = m_viewportW - (len - 1) * step + step;
|
||||||
else
|
else
|
||||||
startX = 0.0;
|
startX = 0.0;
|
||||||
const qint64 startDev = toDev(startX, dpr);
|
|
||||||
|
|
||||||
// Plot bounds in device pixels: half a stroke plus the curve overshoot at
|
// Plot bounds in device pixels: half a stroke plus the curve overshoot at
|
||||||
// the top, a half stroke and one pixel of antialiasing feather at the
|
// the top, a half stroke and one pixel of antialiasing feather at the
|
||||||
@@ -276,42 +291,27 @@ SparklineContent::LineGeometry SparklineContent::buildGeometry(
|
|||||||
const qreal value =
|
const qreal value =
|
||||||
std::isfinite(raw) ? qBound<qreal>(0.0, raw, m_maxValue) : 0.0;
|
std::isfinite(raw) ? qBound<qreal>(0.0, raw, m_maxValue) : 0.0;
|
||||||
|
|
||||||
// Vertices on a pixel column, and on the y grid the stroke width needs.
|
// The y of every sample goes on the grid the stroke width needs; x stays
|
||||||
// Item-local, so the parent's placement cannot move the shape. A flat run
|
// exact so the window's span is exactly the slot's width.
|
||||||
// then lands on whole rows; slopes are interpolated, not snapped.
|
|
||||||
const qreal y = fromDev(
|
const qreal y = fromDev(
|
||||||
plotTopDev + static_cast<qint64>(std::llround(
|
plotTopDev + static_cast<qint64>(std::llround(
|
||||||
(1.0 - value / m_maxValue) *
|
(1.0 - value / m_maxValue) *
|
||||||
static_cast<double>(plotHDev))),
|
static_cast<double>(plotHDev))),
|
||||||
dpr);
|
dpr);
|
||||||
points.append(QPointF(
|
points.append(QPointF(startX + qreal(i) * step, snapLine(y, lineDev, dpr)));
|
||||||
fromDev(startDev + qint64(i) * stepDev, dpr),
|
|
||||||
snapLine(y, lineDev, dpr)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One vertex per device-pixel column, shared with LineGraphItem through
|
||||||
|
// internal::curveVertices: a segment spanning several columns would otherwise
|
||||||
|
// interpolate its edge across all of them. y stays continuous, so a slope is
|
||||||
|
// interpolated rather than stepped one pixel at a time.
|
||||||
|
const QVector<QPointF> verts =
|
||||||
|
internal::curveVertices(points, dpr, m_curveSmoothing);
|
||||||
|
|
||||||
QPainterPath linePath;
|
QPainterPath linePath;
|
||||||
linePath.moveTo(points.first());
|
linePath.moveTo(verts.first());
|
||||||
|
for (int i = 1; i < verts.size(); ++i)
|
||||||
for (int i = 0; i < points.size() - 1; ++i) {
|
linePath.lineTo(verts[i]);
|
||||||
const QPointF& p1 = points[i];
|
|
||||||
const QPointF& p2 = points[i + 1];
|
|
||||||
QPointF c1;
|
|
||||||
QPointF c2;
|
|
||||||
segmentControls(points, i, m_curveSmoothing, c1, c2);
|
|
||||||
|
|
||||||
// One vertex per device pixel column, as Components/LineGraph.qml does: a
|
|
||||||
// segment spanning several columns would otherwise interpolate its edge
|
|
||||||
// across all of them. y stays continuous here - snapping it per column
|
|
||||||
// would step the slope one pixel at a time and read as a staircase.
|
|
||||||
const qint64 firstCol = std::llround(p1.x() * dpr) + 1;
|
|
||||||
const qint64 lastCol = std::llround(p2.x() * dpr) - 1;
|
|
||||||
for (qint64 col = firstCol; col <= lastCol; ++col) {
|
|
||||||
const qreal x = fromDev(col, dpr);
|
|
||||||
linePath.lineTo(x, cubicY(p1, c1, c2, p2, x));
|
|
||||||
}
|
|
||||||
|
|
||||||
linePath.lineTo(p2);
|
|
||||||
}
|
|
||||||
|
|
||||||
geom.stroke = linePath;
|
geom.stroke = linePath;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <qcolor.h>
|
#include <qcolor.h>
|
||||||
|
#include <qelapsedtimer.h>
|
||||||
#include <qpainterpath.h>
|
#include <qpainterpath.h>
|
||||||
#include <qquickpainteditem.h>
|
#include <qquickpainteditem.h>
|
||||||
|
|
||||||
@@ -30,6 +31,15 @@ class SparklineContent : public QQuickPaintedItem {
|
|||||||
|
|
||||||
void setViewportSize(qreal viewportWidth, qreal viewportHeight);
|
void setViewportSize(qreal viewportWidth, qreal viewportHeight);
|
||||||
|
|
||||||
|
// How often a new sample arrives, in milliseconds. With this set the slide
|
||||||
|
// phase is read from the sample clock rather than from `progress`, which
|
||||||
|
// makes the wrap exact; without it the phase is the value QML supplies.
|
||||||
|
void setSlideInterval(qreal intervalMs);
|
||||||
|
|
||||||
|
// Offsets the item for the slide. Kept here rather than in SparklineItem
|
||||||
|
// because the quantization needs both stepX() and the window's dpr.
|
||||||
|
void setSlideProgress(qreal progress);
|
||||||
|
|
||||||
[[nodiscard]] qreal stepX() const;
|
[[nodiscard]] qreal stepX() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -44,12 +54,15 @@ class SparklineContent : public QQuickPaintedItem {
|
|||||||
bool hasData = false;
|
bool hasData = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] qreal slidePhase(qreal progress) const;
|
||||||
|
|
||||||
void invalidate();
|
void invalidate();
|
||||||
void rebuildIfNeeded();
|
void rebuildIfNeeded();
|
||||||
[[nodiscard]] LineGeometry buildGeometry(
|
[[nodiscard]] LineGeometry buildGeometry(
|
||||||
internal::CircularBuffer* buffer) const;
|
internal::CircularBuffer* buffer) const;
|
||||||
void connectBuffer(internal::CircularBuffer* buffer);
|
void connectBuffer(internal::CircularBuffer* buffer);
|
||||||
void applyGeometry();
|
void applyGeometry();
|
||||||
|
void onSampleArrived();
|
||||||
|
|
||||||
internal::CircularBuffer* m_line1 = nullptr;
|
internal::CircularBuffer* m_line1 = nullptr;
|
||||||
internal::CircularBuffer* m_line2 = nullptr;
|
internal::CircularBuffer* m_line2 = nullptr;
|
||||||
@@ -65,7 +78,14 @@ class SparklineContent : public QQuickPaintedItem {
|
|||||||
|
|
||||||
qreal m_viewportW = 0.0;
|
qreal m_viewportW = 0.0;
|
||||||
qreal m_viewportH = 0.0;
|
qreal m_viewportH = 0.0;
|
||||||
|
// The height actually painted, snapped so the texture maps 1:1 onto device
|
||||||
|
// rows. The plot bounds are built from this, not from the slot's height.
|
||||||
|
qreal m_paintedH = 0.0;
|
||||||
qreal m_dpr = 1.0;
|
qreal m_dpr = 1.0;
|
||||||
|
qreal m_slideIntervalMs = 0.0;
|
||||||
|
// Restarted whenever a sample arrives, so the slide phase follows the sample
|
||||||
|
// cadence instead of the animation's timeline.
|
||||||
|
QElapsedTimer m_sampleClock;
|
||||||
|
|
||||||
LineGeometry m_geom1;
|
LineGeometry m_geom1;
|
||||||
LineGeometry m_geom2;
|
LineGeometry m_geom2;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ void SparklineItem::geometryChange(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SparklineItem::updateContentOffset() {
|
void SparklineItem::updateContentOffset() {
|
||||||
m_content->setX(-m_content->stepX() * m_slideProgress);
|
m_content->setSlideProgress(m_slideProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SparklineItem::applySmoothingRadius(
|
void SparklineItem::applySmoothingRadius(
|
||||||
@@ -111,12 +111,26 @@ qreal SparklineItem::slideProgress() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SparklineItem::setSlideProgress(qreal progress) {
|
void SparklineItem::setSlideProgress(qreal progress) {
|
||||||
if (qFuzzyCompare(m_slideProgress, progress)) return;
|
// Always forwarded: with a slide interval set the phase comes from the sample
|
||||||
|
// clock, so the offset can move even when the animation value repeats. An
|
||||||
|
// unchanged position is a no-op in QQuickItem.
|
||||||
m_slideProgress = progress;
|
m_slideProgress = progress;
|
||||||
updateContentOffset();
|
updateContentOffset();
|
||||||
emit slideProgressChanged();
|
emit slideProgressChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qreal SparklineItem::slideInterval() const {
|
||||||
|
return m_slideInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SparklineItem::setSlideInterval(qreal intervalMs) {
|
||||||
|
if (qFuzzyCompare(m_slideInterval, intervalMs)) return;
|
||||||
|
m_slideInterval = intervalMs;
|
||||||
|
m_content->setSlideInterval(intervalMs);
|
||||||
|
updateContentOffset();
|
||||||
|
emit slideIntervalChanged();
|
||||||
|
}
|
||||||
|
|
||||||
int SparklineItem::historyLength() const {
|
int SparklineItem::historyLength() const {
|
||||||
return m_historyLength;
|
return m_historyLength;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ class SparklineItem : public QQuickItem {
|
|||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
qreal slideProgress READ slideProgress WRITE setSlideProgress NOTIFY
|
qreal slideProgress READ slideProgress WRITE setSlideProgress NOTIFY
|
||||||
slideProgressChanged)
|
slideProgressChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
qreal slideInterval READ slideInterval WRITE setSlideInterval NOTIFY
|
||||||
|
slideIntervalChanged)
|
||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
int historyLength READ historyLength WRITE setHistoryLength NOTIFY
|
int historyLength READ historyLength WRITE setHistoryLength NOTIFY
|
||||||
historyLengthChanged)
|
historyLengthChanged)
|
||||||
@@ -80,6 +83,9 @@ class SparklineItem : public QQuickItem {
|
|||||||
[[nodiscard]] qreal slideProgress() const;
|
[[nodiscard]] qreal slideProgress() const;
|
||||||
void setSlideProgress(qreal progress);
|
void setSlideProgress(qreal progress);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal slideInterval() const;
|
||||||
|
void setSlideInterval(qreal intervalMs);
|
||||||
|
|
||||||
[[nodiscard]] int historyLength() const;
|
[[nodiscard]] int historyLength() const;
|
||||||
void setHistoryLength(int length);
|
void setHistoryLength(int length);
|
||||||
|
|
||||||
@@ -104,6 +110,7 @@ class SparklineItem : public QQuickItem {
|
|||||||
void line2FillAlphaChanged();
|
void line2FillAlphaChanged();
|
||||||
void maxValueChanged();
|
void maxValueChanged();
|
||||||
void slideProgressChanged();
|
void slideProgressChanged();
|
||||||
|
void slideIntervalChanged();
|
||||||
void historyLengthChanged();
|
void historyLengthChanged();
|
||||||
void lineWidthChanged();
|
void lineWidthChanged();
|
||||||
void slidingChanged();
|
void slidingChanged();
|
||||||
@@ -127,6 +134,7 @@ class SparklineItem : public QQuickItem {
|
|||||||
qreal m_line2FillAlpha = 0.2;
|
qreal m_line2FillAlpha = 0.2;
|
||||||
qreal m_maxValue = 1024.0;
|
qreal m_maxValue = 1024.0;
|
||||||
qreal m_slideProgress = 0.0;
|
qreal m_slideProgress = 0.0;
|
||||||
|
qreal m_slideInterval = 0.0;
|
||||||
int m_historyLength = 30;
|
int m_historyLength = 30;
|
||||||
qreal m_lineWidth = 2.0;
|
qreal m_lineWidth = 2.0;
|
||||||
bool m_sliding = true;
|
bool m_sliding = true;
|
||||||
|
|||||||
@@ -1,449 +0,0 @@
|
|||||||
#include "sparklinesceneitem.hpp"
|
|
||||||
|
|
||||||
#include <qmath.h>
|
|
||||||
#include <qpainterpath.h>
|
|
||||||
#include <qsgflatcolormaterial.h>
|
|
||||||
#include <qsggeometry.h>
|
|
||||||
|
|
||||||
namespace ZShell::components {
|
|
||||||
|
|
||||||
SparklineSceneItem::SparklineSceneItem(QQuickItem* parent)
|
|
||||||
: QQuickItem(parent) {
|
|
||||||
setClip(true);
|
|
||||||
setFlag(QQuickItem::ItemHasContents, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::geometryChange(
|
|
||||||
const QRectF& newGeometry, const QRectF& oldGeometry) {
|
|
||||||
QQuickItem::geometryChange(newGeometry, oldGeometry);
|
|
||||||
if (newGeometry.size() != oldGeometry.size()) invalidateGeometry();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::invalidateGeometry() {
|
|
||||||
m_geometryDirty = true;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::applySmoothingRadius(
|
|
||||||
internal::CircularBuffer* buffer) const {
|
|
||||||
if (buffer) buffer->setSmoothingRadius(qRound(m_smoothing));
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::connectBuffer(internal::CircularBuffer* buffer) {
|
|
||||||
if (!buffer) return;
|
|
||||||
connect(
|
|
||||||
buffer,
|
|
||||||
&internal::CircularBuffer::valuesChanged,
|
|
||||||
this,
|
|
||||||
&SparklineSceneItem::invalidateGeometry);
|
|
||||||
connect(buffer, &QObject::destroyed, this, [this, buffer]() {
|
|
||||||
if (m_line1 == buffer) {
|
|
||||||
m_line1 = nullptr;
|
|
||||||
emit line1Changed();
|
|
||||||
}
|
|
||||||
if (m_line2 == buffer) {
|
|
||||||
m_line2 = nullptr;
|
|
||||||
emit line2Changed();
|
|
||||||
}
|
|
||||||
invalidateGeometry();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal SparklineSceneItem::stepX() const {
|
|
||||||
if (m_historyLength < 2 || width() <= 0.0) return 0.0;
|
|
||||||
if (m_sliding) return width() / static_cast<qreal>(m_historyLength - 1);
|
|
||||||
|
|
||||||
const int len = qMax(
|
|
||||||
2,
|
|
||||||
(m_line1 && m_line1->count() >= 2) ? m_line1->count()
|
|
||||||
: (m_line2 ? m_line2->count() : 2));
|
|
||||||
return width() / static_cast<qreal>(len - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
SparklineSceneItem::LineSample SparklineSceneItem::buildSample(
|
|
||||||
internal::CircularBuffer* buffer) const {
|
|
||||||
LineSample sample;
|
|
||||||
if (!buffer || buffer->count() < 2 || m_maxValue <= 0.0 || width() <= 0.0)
|
|
||||||
return sample;
|
|
||||||
if (m_historyLength < 2) return sample;
|
|
||||||
|
|
||||||
const int len = buffer->count();
|
|
||||||
const qreal step = stepX();
|
|
||||||
if (step <= 0.0) return sample;
|
|
||||||
|
|
||||||
qreal startX = m_sliding ? (width() - (len - 1) * step + step) : 0.0;
|
|
||||||
|
|
||||||
const qreal h = height();
|
|
||||||
const qreal strokePad = qCeil(m_lineWidth / 2);
|
|
||||||
const qreal curvePad = 3.0;
|
|
||||||
const qreal plotTop = strokePad + curvePad;
|
|
||||||
const qreal plotBottom = h - strokePad;
|
|
||||||
const qreal plotH = qMax<qreal>(1.0, plotBottom - plotTop);
|
|
||||||
|
|
||||||
QVector<QPointF> points;
|
|
||||||
points.reserve(len);
|
|
||||||
|
|
||||||
for (int i = 0; i < len; ++i) {
|
|
||||||
const qreal value =
|
|
||||||
qBound<qreal>(0.0, buffer->smoothedAt(i), m_maxValue);
|
|
||||||
|
|
||||||
const qreal x = startX + i * step;
|
|
||||||
const qreal y = plotTop + (1.0 - (value / m_maxValue)) * plotH;
|
|
||||||
points.append(QPointF(x, y));
|
|
||||||
}
|
|
||||||
|
|
||||||
QPainterPath path;
|
|
||||||
path.moveTo(points[0]);
|
|
||||||
|
|
||||||
if (m_curveSmoothing && points.size() > 2) {
|
|
||||||
const qreal tension = 1.0;
|
|
||||||
const qreal k = tension / 6.0;
|
|
||||||
const int n = static_cast<int>(points.size());
|
|
||||||
for (int i = 0; i < n - 1; ++i) {
|
|
||||||
const QPointF& p0 = points[qMax(i - 1, 0)];
|
|
||||||
const QPointF& p1 = points[i];
|
|
||||||
const QPointF& p2 = points[i + 1];
|
|
||||||
const QPointF& p3 = points[qMin(i + 2, n - 1)];
|
|
||||||
|
|
||||||
QPointF c1 = p1 + (p2 - p0) * k;
|
|
||||||
QPointF c2 = p2 - (p3 - p1) * k;
|
|
||||||
|
|
||||||
const qreal segMinY = qMin(p1.y(), p2.y());
|
|
||||||
const qreal segMaxY = qMax(p1.y(), p2.y());
|
|
||||||
c1.setY(qBound(segMinY, c1.y(), segMaxY));
|
|
||||||
c2.setY(qBound(segMinY, c2.y(), segMaxY));
|
|
||||||
|
|
||||||
path.cubicTo(c1, c2, p2);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < points.size() - 1; ++i) {
|
|
||||||
const QPointF& p0 = points[i];
|
|
||||||
const QPointF& p1 = points[i + 1];
|
|
||||||
const qreal ctrlX = (p0.x() + p1.x()) * 0.5;
|
|
||||||
path.cubicTo(QPointF(ctrlX, p0.y()), QPointF(ctrlX, p1.y()), p1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto subpaths = path.toSubpathPolygons();
|
|
||||||
if (subpaths.isEmpty()) return sample;
|
|
||||||
|
|
||||||
sample.polyline = subpaths.first();
|
|
||||||
sample.fillBottom = h;
|
|
||||||
sample.hasData = sample.polyline.size() >= 2;
|
|
||||||
return sample;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::writeFillGeometry(
|
|
||||||
QSGGeometryNode* node, const LineSample& sample) {
|
|
||||||
const int n = static_cast<int>(sample.polyline.size());
|
|
||||||
auto* geometry = node->geometry();
|
|
||||||
geometry->allocate(n * 2);
|
|
||||||
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
|
|
||||||
|
|
||||||
auto* vertices = geometry->vertexDataAsPoint2D();
|
|
||||||
for (int i = 0; i < n; ++i) {
|
|
||||||
const QPointF& p = sample.polyline[i];
|
|
||||||
vertices[i * 2].set(
|
|
||||||
static_cast<float>(p.x()), static_cast<float>(p.y()));
|
|
||||||
vertices[i * 2 + 1].set(
|
|
||||||
static_cast<float>(p.x()), static_cast<float>(sample.fillBottom));
|
|
||||||
}
|
|
||||||
|
|
||||||
node->markDirty(QSGNode::DirtyGeometry);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::writeStrokeGeometry(
|
|
||||||
QSGGeometryNode* node, const LineSample& sample, qreal lineWidth) {
|
|
||||||
const int n = static_cast<int>(sample.polyline.size());
|
|
||||||
auto* geometry = node->geometry();
|
|
||||||
geometry->allocate(n * 2);
|
|
||||||
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
|
|
||||||
|
|
||||||
auto* vertices = geometry->vertexDataAsPoint2D();
|
|
||||||
const qreal half = lineWidth * 0.5;
|
|
||||||
|
|
||||||
for (int i = 0; i < n; ++i) {
|
|
||||||
QPointF dir;
|
|
||||||
if (i == 0)
|
|
||||||
dir = sample.polyline[1] - sample.polyline[0];
|
|
||||||
else if (i == n - 1)
|
|
||||||
dir = sample.polyline[n - 1] - sample.polyline[n - 2];
|
|
||||||
else
|
|
||||||
dir = sample.polyline[i + 1] - sample.polyline[i - 1];
|
|
||||||
|
|
||||||
const qreal len = std::hypot(dir.x(), dir.y());
|
|
||||||
QPointF normal = len > 1e-6 ? QPointF(-dir.y() / len, dir.x() / len)
|
|
||||||
: QPointF(0, 1);
|
|
||||||
|
|
||||||
const QPointF& p = sample.polyline[i];
|
|
||||||
const QPointF offset = normal * half;
|
|
||||||
vertices[i * 2].set(
|
|
||||||
static_cast<float>(p.x() + offset.x()),
|
|
||||||
static_cast<float>(p.y() + offset.y()));
|
|
||||||
vertices[i * 2 + 1].set(
|
|
||||||
static_cast<float>(p.x() - offset.x()),
|
|
||||||
static_cast<float>(p.y() - offset.y()));
|
|
||||||
}
|
|
||||||
|
|
||||||
node->markDirty(QSGNode::DirtyGeometry);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::setMaterialColor(
|
|
||||||
QSGGeometryNode* node, const QColor& color, qreal alpha) {
|
|
||||||
auto* material = static_cast<QSGFlatColorMaterial*>(node->material());
|
|
||||||
QColor c = color;
|
|
||||||
c.setAlphaF(static_cast<float>(alpha));
|
|
||||||
material->setColor(c);
|
|
||||||
node->markDirty(QSGNode::DirtyMaterial);
|
|
||||||
}
|
|
||||||
|
|
||||||
QSGNode* SparklineSceneItem::updatePaintNode(
|
|
||||||
QSGNode* node, UpdatePaintNodeData*) {
|
|
||||||
bool samplesRebuilt = false;
|
|
||||||
if (m_geometryDirty) {
|
|
||||||
m_sample1 = buildSample(m_line1);
|
|
||||||
m_sample2 = buildSample(m_line2);
|
|
||||||
m_geometryDirty = false;
|
|
||||||
samplesRebuilt = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool hasAny = m_sample1.hasData || m_sample2.hasData;
|
|
||||||
if (!hasAny) {
|
|
||||||
delete node;
|
|
||||||
m_transformNode = nullptr;
|
|
||||||
m_line1FillNode = m_line1StrokeNode = nullptr;
|
|
||||||
m_line2FillNode = m_line2StrokeNode = nullptr;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* transformNode = static_cast<QSGTransformNode*>(node);
|
|
||||||
bool justCreated = false;
|
|
||||||
if (!transformNode) {
|
|
||||||
transformNode = new QSGTransformNode();
|
|
||||||
m_transformNode = transformNode;
|
|
||||||
|
|
||||||
auto makeNode = [] {
|
|
||||||
auto* n = new QSGGeometryNode();
|
|
||||||
n->setGeometry(
|
|
||||||
new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0));
|
|
||||||
n->setFlag(QSGNode::OwnsGeometry);
|
|
||||||
n->setMaterial(new QSGFlatColorMaterial());
|
|
||||||
n->setFlag(QSGNode::OwnsMaterial);
|
|
||||||
return n;
|
|
||||||
};
|
|
||||||
|
|
||||||
m_line1FillNode = makeNode();
|
|
||||||
m_line1StrokeNode = makeNode();
|
|
||||||
m_line2FillNode = makeNode();
|
|
||||||
m_line2StrokeNode = makeNode();
|
|
||||||
transformNode->appendChildNode(m_line1FillNode);
|
|
||||||
transformNode->appendChildNode(m_line1StrokeNode);
|
|
||||||
transformNode->appendChildNode(m_line2FillNode);
|
|
||||||
transformNode->appendChildNode(m_line2StrokeNode);
|
|
||||||
|
|
||||||
justCreated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QMatrix4x4 matrix;
|
|
||||||
matrix.translate(static_cast<float>(-stepX() * m_slideProgress), 0.0f);
|
|
||||||
transformNode->setMatrix(matrix);
|
|
||||||
|
|
||||||
const bool needsGeometryWrite = samplesRebuilt || justCreated;
|
|
||||||
const bool needsColorWrite = m_colorsDirty || justCreated;
|
|
||||||
|
|
||||||
auto writeLine = [&](QSGGeometryNode* fillNode,
|
|
||||||
QSGGeometryNode* strokeNode,
|
|
||||||
const LineSample& sample,
|
|
||||||
const QColor& color,
|
|
||||||
qreal fillAlpha) {
|
|
||||||
if (needsGeometryWrite) {
|
|
||||||
if (sample.hasData) {
|
|
||||||
writeFillGeometry(fillNode, sample);
|
|
||||||
writeStrokeGeometry(strokeNode, sample, m_lineWidth);
|
|
||||||
} else {
|
|
||||||
fillNode->geometry()->allocate(0);
|
|
||||||
strokeNode->geometry()->allocate(0);
|
|
||||||
fillNode->markDirty(QSGNode::DirtyGeometry);
|
|
||||||
strokeNode->markDirty(QSGNode::DirtyGeometry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (needsColorWrite) {
|
|
||||||
setMaterialColor(fillNode, color, fillAlpha);
|
|
||||||
setMaterialColor(strokeNode, color, 1.0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
writeLine(
|
|
||||||
m_line1FillNode,
|
|
||||||
m_line1StrokeNode,
|
|
||||||
m_sample1,
|
|
||||||
m_line1Color,
|
|
||||||
m_line1FillAlpha);
|
|
||||||
writeLine(
|
|
||||||
m_line2FillNode,
|
|
||||||
m_line2StrokeNode,
|
|
||||||
m_sample2,
|
|
||||||
m_line2Color,
|
|
||||||
m_line2FillAlpha);
|
|
||||||
|
|
||||||
m_colorsDirty = false;
|
|
||||||
|
|
||||||
return transformNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal::CircularBuffer* SparklineSceneItem::line1() const {
|
|
||||||
return m_line1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::setLine1(internal::CircularBuffer* buffer) {
|
|
||||||
if (m_line1 == buffer) return;
|
|
||||||
if (m_line1) disconnect(m_line1, nullptr, this, nullptr);
|
|
||||||
m_line1 = buffer;
|
|
||||||
connectBuffer(buffer);
|
|
||||||
applySmoothingRadius(buffer);
|
|
||||||
invalidateGeometry();
|
|
||||||
emit line1Changed();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal::CircularBuffer* SparklineSceneItem::line2() const {
|
|
||||||
return m_line2;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::setLine2(internal::CircularBuffer* buffer) {
|
|
||||||
if (m_line2 == buffer) return;
|
|
||||||
if (m_line2) disconnect(m_line2, nullptr, this, nullptr);
|
|
||||||
m_line2 = buffer;
|
|
||||||
connectBuffer(buffer);
|
|
||||||
applySmoothingRadius(buffer);
|
|
||||||
invalidateGeometry();
|
|
||||||
emit line2Changed();
|
|
||||||
}
|
|
||||||
|
|
||||||
QColor SparklineSceneItem::line1Color() const {
|
|
||||||
return m_line1Color;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::setLine1Color(const QColor& color) {
|
|
||||||
if (m_line1Color == color) return;
|
|
||||||
m_line1Color = color;
|
|
||||||
m_colorsDirty = true;
|
|
||||||
update();
|
|
||||||
emit line1ColorChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QColor SparklineSceneItem::line2Color() const {
|
|
||||||
return m_line2Color;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::setLine2Color(const QColor& color) {
|
|
||||||
if (m_line2Color == color) return;
|
|
||||||
m_line2Color = color;
|
|
||||||
m_colorsDirty = true;
|
|
||||||
update();
|
|
||||||
emit line2ColorChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal SparklineSceneItem::line1FillAlpha() const {
|
|
||||||
return m_line1FillAlpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::setLine1FillAlpha(qreal alpha) {
|
|
||||||
if (qFuzzyCompare(m_line1FillAlpha, alpha)) return;
|
|
||||||
m_line1FillAlpha = alpha;
|
|
||||||
m_colorsDirty = true;
|
|
||||||
update();
|
|
||||||
emit line1FillAlphaChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal SparklineSceneItem::line2FillAlpha() const {
|
|
||||||
return m_line2FillAlpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::setLine2FillAlpha(qreal alpha) {
|
|
||||||
if (qFuzzyCompare(m_line2FillAlpha, alpha)) return;
|
|
||||||
m_line2FillAlpha = alpha;
|
|
||||||
m_colorsDirty = true;
|
|
||||||
update();
|
|
||||||
emit line2FillAlphaChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal SparklineSceneItem::maxValue() const {
|
|
||||||
return m_maxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::setMaxValue(qreal value) {
|
|
||||||
if (qFuzzyCompare(m_maxValue, value)) return;
|
|
||||||
m_maxValue = value;
|
|
||||||
invalidateGeometry();
|
|
||||||
emit maxValueChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal SparklineSceneItem::slideProgress() const {
|
|
||||||
return m_slideProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::setSlideProgress(qreal progress) {
|
|
||||||
if (qFuzzyCompare(m_slideProgress, progress)) return;
|
|
||||||
m_slideProgress = progress;
|
|
||||||
update();
|
|
||||||
emit slideProgressChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
int SparklineSceneItem::historyLength() const {
|
|
||||||
return m_historyLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::setHistoryLength(int length) {
|
|
||||||
if (m_historyLength == length) return;
|
|
||||||
m_historyLength = length;
|
|
||||||
invalidateGeometry();
|
|
||||||
emit historyLengthChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal SparklineSceneItem::lineWidth() const {
|
|
||||||
return m_lineWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::setLineWidth(qreal width) {
|
|
||||||
if (qFuzzyCompare(m_lineWidth, width)) return;
|
|
||||||
m_lineWidth = width;
|
|
||||||
invalidateGeometry();
|
|
||||||
emit lineWidthChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SparklineSceneItem::sliding() const {
|
|
||||||
return m_sliding;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::setSliding(bool sliding) {
|
|
||||||
if (m_sliding == sliding) return;
|
|
||||||
m_sliding = sliding;
|
|
||||||
invalidateGeometry();
|
|
||||||
emit slidingChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal SparklineSceneItem::smoothing() const {
|
|
||||||
return m_smoothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::setSmoothing(qreal smoothing) {
|
|
||||||
smoothing = qMax<qreal>(0.0, smoothing);
|
|
||||||
if (qFuzzyCompare(m_smoothing, smoothing)) return;
|
|
||||||
m_smoothing = smoothing;
|
|
||||||
applySmoothingRadius(m_line1);
|
|
||||||
applySmoothingRadius(m_line2);
|
|
||||||
emit smoothingChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SparklineSceneItem::curveSmoothing() const {
|
|
||||||
return m_curveSmoothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SparklineSceneItem::setCurveSmoothing(bool enabled) {
|
|
||||||
if (m_curveSmoothing == enabled) return;
|
|
||||||
m_curveSmoothing = enabled;
|
|
||||||
invalidateGeometry();
|
|
||||||
emit curveSmoothingChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace ZShell::components
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <qcolor.h>
|
|
||||||
#include <qmatrix4x4.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qqmlintegration.h>
|
|
||||||
#include <qquickitem.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qvector.h>
|
|
||||||
|
|
||||||
#include "internal/circularbuffer.hpp"
|
|
||||||
|
|
||||||
class QSGGeometryNode;
|
|
||||||
class QSGTransformNode;
|
|
||||||
|
|
||||||
namespace ZShell::components {
|
|
||||||
|
|
||||||
class SparklineSceneItem : public QQuickItem {
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
Q_PROPERTY(
|
|
||||||
internal::CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY
|
|
||||||
line1Changed)
|
|
||||||
Q_PROPERTY(
|
|
||||||
internal::CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY
|
|
||||||
line2Changed)
|
|
||||||
Q_PROPERTY(
|
|
||||||
QColor line1Color READ line1Color WRITE setLine1Color NOTIFY
|
|
||||||
line1ColorChanged)
|
|
||||||
Q_PROPERTY(
|
|
||||||
QColor line2Color READ line2Color WRITE setLine2Color NOTIFY
|
|
||||||
line2ColorChanged)
|
|
||||||
Q_PROPERTY(
|
|
||||||
qreal line1FillAlpha READ line1FillAlpha WRITE setLine1FillAlpha NOTIFY
|
|
||||||
line1FillAlphaChanged)
|
|
||||||
Q_PROPERTY(
|
|
||||||
qreal line2FillAlpha READ line2FillAlpha WRITE setLine2FillAlpha NOTIFY
|
|
||||||
line2FillAlphaChanged)
|
|
||||||
Q_PROPERTY(
|
|
||||||
qreal maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged)
|
|
||||||
Q_PROPERTY(
|
|
||||||
qreal slideProgress READ slideProgress WRITE setSlideProgress NOTIFY
|
|
||||||
slideProgressChanged)
|
|
||||||
Q_PROPERTY(
|
|
||||||
int historyLength READ historyLength WRITE setHistoryLength NOTIFY
|
|
||||||
historyLengthChanged)
|
|
||||||
Q_PROPERTY(
|
|
||||||
qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY
|
|
||||||
lineWidthChanged)
|
|
||||||
Q_PROPERTY(bool sliding READ sliding WRITE setSliding NOTIFY slidingChanged)
|
|
||||||
Q_PROPERTY(
|
|
||||||
qreal smoothing READ smoothing WRITE setSmoothing NOTIFY
|
|
||||||
smoothingChanged)
|
|
||||||
Q_PROPERTY(
|
|
||||||
bool curveSmoothing READ curveSmoothing WRITE setCurveSmoothing NOTIFY
|
|
||||||
curveSmoothingChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit SparklineSceneItem(QQuickItem* parent = nullptr);
|
|
||||||
|
|
||||||
[[nodiscard]] internal::CircularBuffer* line1() const;
|
|
||||||
void setLine1(internal::CircularBuffer* buffer);
|
|
||||||
|
|
||||||
[[nodiscard]] internal::CircularBuffer* line2() const;
|
|
||||||
void setLine2(internal::CircularBuffer* buffer);
|
|
||||||
|
|
||||||
[[nodiscard]] QColor line1Color() const;
|
|
||||||
void setLine1Color(const QColor& color);
|
|
||||||
|
|
||||||
[[nodiscard]] QColor line2Color() const;
|
|
||||||
void setLine2Color(const QColor& color);
|
|
||||||
|
|
||||||
[[nodiscard]] qreal line1FillAlpha() const;
|
|
||||||
void setLine1FillAlpha(qreal alpha);
|
|
||||||
|
|
||||||
[[nodiscard]] qreal line2FillAlpha() const;
|
|
||||||
void setLine2FillAlpha(qreal alpha);
|
|
||||||
|
|
||||||
[[nodiscard]] qreal maxValue() const;
|
|
||||||
void setMaxValue(qreal value);
|
|
||||||
|
|
||||||
[[nodiscard]] qreal slideProgress() const;
|
|
||||||
void setSlideProgress(qreal progress);
|
|
||||||
|
|
||||||
[[nodiscard]] int historyLength() const;
|
|
||||||
void setHistoryLength(int length);
|
|
||||||
|
|
||||||
[[nodiscard]] qreal lineWidth() const;
|
|
||||||
void setLineWidth(qreal width);
|
|
||||||
|
|
||||||
[[nodiscard]] bool sliding() const;
|
|
||||||
void setSliding(bool sliding);
|
|
||||||
|
|
||||||
[[nodiscard]] qreal smoothing() const;
|
|
||||||
void setSmoothing(qreal smoothing);
|
|
||||||
|
|
||||||
[[nodiscard]] bool curveSmoothing() const;
|
|
||||||
void setCurveSmoothing(bool curveSmoothing);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void line1Changed();
|
|
||||||
void line2Changed();
|
|
||||||
void line1ColorChanged();
|
|
||||||
void line2ColorChanged();
|
|
||||||
void line1FillAlphaChanged();
|
|
||||||
void line2FillAlphaChanged();
|
|
||||||
void maxValueChanged();
|
|
||||||
void slideProgressChanged();
|
|
||||||
void historyLengthChanged();
|
|
||||||
void lineWidthChanged();
|
|
||||||
void slidingChanged();
|
|
||||||
void smoothingChanged();
|
|
||||||
void curveSmoothingChanged();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
QSGNode* updatePaintNode(QSGNode* node, UpdatePaintNodeData* data) override;
|
|
||||||
void geometryChange(
|
|
||||||
const QRectF& newGeometry, const QRectF& oldGeometry) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct LineSample {
|
|
||||||
QVector<QPointF> polyline; // local coords, slideProgress baked to 0
|
|
||||||
qreal fillBottom = 0.0;
|
|
||||||
bool hasData = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
void connectBuffer(internal::CircularBuffer* buffer);
|
|
||||||
void invalidateGeometry();
|
|
||||||
void applySmoothingRadius(internal::CircularBuffer* buffer) const;
|
|
||||||
[[nodiscard]] qreal stepX() const;
|
|
||||||
[[nodiscard]] LineSample buildSample(internal::CircularBuffer* buffer) const;
|
|
||||||
static void writeFillGeometry(
|
|
||||||
QSGGeometryNode* node, const LineSample& sample);
|
|
||||||
static void writeStrokeGeometry(
|
|
||||||
QSGGeometryNode* node, const LineSample& sample, qreal lineWidth);
|
|
||||||
static void setMaterialColor(
|
|
||||||
QSGGeometryNode* node, const QColor& color, qreal alpha);
|
|
||||||
|
|
||||||
internal::CircularBuffer* m_line1 = nullptr;
|
|
||||||
internal::CircularBuffer* m_line2 = nullptr;
|
|
||||||
QColor m_line1Color;
|
|
||||||
QColor m_line2Color;
|
|
||||||
qreal m_line1FillAlpha = 0.15;
|
|
||||||
qreal m_line2FillAlpha = 0.2;
|
|
||||||
qreal m_maxValue = 1024.0;
|
|
||||||
qreal m_slideProgress = 0.0;
|
|
||||||
int m_historyLength = 30;
|
|
||||||
qreal m_lineWidth = 2.0;
|
|
||||||
bool m_sliding = true;
|
|
||||||
qreal m_smoothing = 0.0;
|
|
||||||
bool m_curveSmoothing = false;
|
|
||||||
|
|
||||||
LineSample m_sample1;
|
|
||||||
LineSample m_sample2;
|
|
||||||
|
|
||||||
bool m_geometryDirty = true;
|
|
||||||
bool m_colorsDirty = true;
|
|
||||||
|
|
||||||
QSGTransformNode* m_transformNode = nullptr;
|
|
||||||
QSGGeometryNode* m_line1FillNode = nullptr;
|
|
||||||
QSGGeometryNode* m_line1StrokeNode = nullptr;
|
|
||||||
QSGGeometryNode* m_line2FillNode = nullptr;
|
|
||||||
QSGGeometryNode* m_line2StrokeNode = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace ZShell::components
|
|
||||||
@@ -7,6 +7,7 @@ qml_module(ZShell-internal
|
|||||||
cachingimagemanager.hpp cachingimagemanager.cpp
|
cachingimagemanager.hpp cachingimagemanager.cpp
|
||||||
circularindicatormanager.hpp circularindicatormanager.cpp
|
circularindicatormanager.hpp circularindicatormanager.cpp
|
||||||
circularbuffer.hpp circularbuffer.cpp
|
circularbuffer.hpp circularbuffer.cpp
|
||||||
|
linegeometry.hpp linegeometry.cpp
|
||||||
arcgauge.hpp arcgauge.cpp
|
arcgauge.hpp arcgauge.cpp
|
||||||
lidwatcher.hpp lidwatcher.cpp
|
lidwatcher.hpp lidwatcher.cpp
|
||||||
visualizerbars.hpp visualizerbars.cpp
|
visualizerbars.hpp visualizerbars.cpp
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
#include "linegeometry.hpp"
|
||||||
|
|
||||||
|
#include <qglobal.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace ZShell::internal {
|
||||||
|
|
||||||
|
qreal snapLine(qreal logical, qint64 widthDev, qreal dpr) {
|
||||||
|
const qreal half = qreal(widthDev) / 2.0;
|
||||||
|
return (static_cast<qreal>(std::llround(logical * dpr - half)) + half)
|
||||||
|
/ dpr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void segmentControls(
|
||||||
|
const QVector<QPointF>& points,
|
||||||
|
int i,
|
||||||
|
bool curveSmoothing,
|
||||||
|
QPointF& c1,
|
||||||
|
QPointF& c2) {
|
||||||
|
const QPointF& p0 = points[qMax(i - 1, 0)];
|
||||||
|
const QPointF& p1 = points[i];
|
||||||
|
const QPointF& p2 = points[i + 1];
|
||||||
|
const QPointF& p3 = points[qMin(i + 2, points.size() - 1)];
|
||||||
|
|
||||||
|
if (curveSmoothing && points.size() > 2) {
|
||||||
|
const qreal k = 1.0 / 6.0;
|
||||||
|
c1 = p1 + (p2 - p0) * k;
|
||||||
|
c2 = p2 - (p3 - p1) * k;
|
||||||
|
|
||||||
|
const qreal minY = qMin(p1.y(), p2.y());
|
||||||
|
const qreal maxY = qMax(p1.y(), p2.y());
|
||||||
|
c1.setY(qBound(minY, c1.y(), maxY));
|
||||||
|
c2.setY(qBound(minY, c2.y(), maxY));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qreal cx = (p1.x() + p2.x()) * 0.5;
|
||||||
|
c1 = QPointF(cx, p1.y());
|
||||||
|
c2 = QPointF(cx, p2.y());
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal cubicY(
|
||||||
|
const QPointF& p1,
|
||||||
|
const QPointF& c1,
|
||||||
|
const QPointF& c2,
|
||||||
|
const QPointF& p2,
|
||||||
|
qreal targetX) {
|
||||||
|
qreal lo = 0.0;
|
||||||
|
qreal hi = 1.0;
|
||||||
|
for (int it = 0; it < 16; ++it) {
|
||||||
|
const qreal t = (lo + hi) / 2.0;
|
||||||
|
const qreal u = 1.0 - t;
|
||||||
|
const qreal x = u * u * u * p1.x() + 3 * u * u * t * c1.x()
|
||||||
|
+ 3 * u * t * t * c2.x() + t * t * t * p2.x();
|
||||||
|
if (x < targetX)
|
||||||
|
lo = t;
|
||||||
|
else
|
||||||
|
hi = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qreal t = (lo + hi) / 2.0;
|
||||||
|
const qreal u = 1.0 - t;
|
||||||
|
return u * u * u * p1.y() + 3 * u * u * t * c1.y()
|
||||||
|
+ 3 * u * t * t * c2.y() + t * t * t * p2.y();
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Index of the segment whose right endpoint is at or past targetX, i.e. the
|
||||||
|
// segment that contains it. Clamps to the last segment when targetX is beyond the
|
||||||
|
// end. Assumes points ascend in x.
|
||||||
|
int segmentIndexAt(const QVector<QPointF>& points, qreal targetX) {
|
||||||
|
const int last = points.size() - 2;
|
||||||
|
for (int i = 0; i < points.size() - 1; ++i) {
|
||||||
|
if (targetX <= points[i + 1].x())
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return qMax(0, last);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QPointF pointAtX(
|
||||||
|
const QVector<QPointF>& points,
|
||||||
|
qreal targetX,
|
||||||
|
bool curveSmoothing) {
|
||||||
|
const int j = segmentIndexAt(points, targetX);
|
||||||
|
QPointF c1;
|
||||||
|
QPointF c2;
|
||||||
|
segmentControls(points, j, curveSmoothing, c1, c2);
|
||||||
|
|
||||||
|
qreal lo = 0.0;
|
||||||
|
qreal hi = 1.0;
|
||||||
|
for (int it = 0; it < 24; ++it) {
|
||||||
|
const qreal t = (lo + hi) / 2.0;
|
||||||
|
const qreal u = 1.0 - t;
|
||||||
|
const qreal x = u * u * u * points[j].x()
|
||||||
|
+ 3 * u * u * t * c1.x() + 3 * u * t * t * c2.x()
|
||||||
|
+ t * t * t * points[j + 1].x();
|
||||||
|
if (x < targetX)
|
||||||
|
lo = t;
|
||||||
|
else
|
||||||
|
hi = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qreal t = (lo + hi) / 2.0;
|
||||||
|
const qreal u = 1.0 - t;
|
||||||
|
const qreal y = u * u * u * points[j].y() + 3 * u * u * t * c1.y()
|
||||||
|
+ 3 * u * t * t * c2.y() + t * t * t * points[j + 1].y();
|
||||||
|
return QPointF(targetX, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<QPointF> curveVertices(
|
||||||
|
const QVector<QPointF>& points,
|
||||||
|
qreal dpr,
|
||||||
|
bool curveSmoothing) {
|
||||||
|
QVector<QPointF> out;
|
||||||
|
if (points.size() < 2)
|
||||||
|
return out;
|
||||||
|
|
||||||
|
out.reserve(points.size());
|
||||||
|
out.append(points.first());
|
||||||
|
|
||||||
|
for (int i = 0; i < points.size() - 1; ++i) {
|
||||||
|
const QPointF& p1 = points[i];
|
||||||
|
const QPointF& p2 = points[i + 1];
|
||||||
|
QPointF c1;
|
||||||
|
QPointF c2;
|
||||||
|
segmentControls(points, i, curveSmoothing, c1, c2);
|
||||||
|
|
||||||
|
// One vertex per device-pixel column. A segment spanning several columns
|
||||||
|
// would otherwise interpolate its edge across all of them, which reads as
|
||||||
|
// a soft rather than crisply anti-aliased stroke. y stays continuous:
|
||||||
|
// snapping it per column would step the slope a pixel at a time and look
|
||||||
|
// like a staircase.
|
||||||
|
const qint64 firstCol = toDev(p1.x(), dpr) + 1;
|
||||||
|
const qint64 lastCol = toDev(p2.x(), dpr) - 1;
|
||||||
|
for (qint64 col = firstCol; col <= lastCol; ++col) {
|
||||||
|
const qreal x = fromDev(col, dpr);
|
||||||
|
out.append(QPointF(x, cubicY(p1, c1, c2, p2, x)));
|
||||||
|
}
|
||||||
|
|
||||||
|
out.append(p2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
} // namespace ZShell::internal
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qpoint.h>
|
||||||
|
#include <qvector.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace ZShell::internal {
|
||||||
|
|
||||||
|
// Shared curve construction for SparklineContent (QPainter) and LineGraphItem
|
||||||
|
// (QCanvasPainter). Input and output coordinates are logical (item-local);
|
||||||
|
// device-pixel quantization happens inside so a stroke lands on whole pixel
|
||||||
|
// columns and its edges are not interpolated across several columns. Keeping the
|
||||||
|
// math in one place means both graphs align and smooth their lines identically,
|
||||||
|
// whatever backend they paint through.
|
||||||
|
|
||||||
|
inline qint64 toDev(qreal logical, qreal dpr) {
|
||||||
|
return std::llround(logical * dpr);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline qreal fromDev(qint64 device, qreal dpr) {
|
||||||
|
return qreal(device) / dpr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap a line center so a whole-pixel-wide stroke sits on pixel boundaries when
|
||||||
|
// the device width is even and on pixel centers when it is odd.
|
||||||
|
qreal snapLine(qreal logical, qint64 widthDev, qreal dpr);
|
||||||
|
|
||||||
|
// Catmull-Rom control points for the segment points[i] -> points[i + 1], with the
|
||||||
|
// control y clamped to the segment's own y span so a curve never overshoots past
|
||||||
|
// its endpoints. When curveSmoothing is off (or there are too few points) the
|
||||||
|
// tangents are flat.
|
||||||
|
void segmentControls(
|
||||||
|
const QVector<QPointF>& points,
|
||||||
|
int i,
|
||||||
|
bool curveSmoothing,
|
||||||
|
QPointF& c1,
|
||||||
|
QPointF& c2);
|
||||||
|
|
||||||
|
// y of the cubic bezier p1-c1-c2-p2 at targetX, found by bisection on t.
|
||||||
|
qreal cubicY(
|
||||||
|
const QPointF& p1,
|
||||||
|
const QPointF& c1,
|
||||||
|
const QPointF& c2,
|
||||||
|
const QPointF& p2,
|
||||||
|
qreal targetX);
|
||||||
|
|
||||||
|
// The curve point at targetX on the polyline, interpolating across the segment
|
||||||
|
// that contains it. Assumes points ascend in x.
|
||||||
|
QPointF pointAtX(
|
||||||
|
const QVector<QPointF>& points,
|
||||||
|
qreal targetX,
|
||||||
|
bool curveSmoothing);
|
||||||
|
|
||||||
|
// A dense polyline through points, adding one vertex per device-pixel column
|
||||||
|
// inside each segment so an edge never interpolates across several columns. The
|
||||||
|
// result starts at points.first() and ends at points.last(). points must ascend in
|
||||||
|
// x and hold at least two entries. Backends consume it as a moveTo/lineTo chain.
|
||||||
|
QVector<QPointF> curveVertices(
|
||||||
|
const QVector<QPointF>& points,
|
||||||
|
qreal dpr,
|
||||||
|
bool curveSmoothing);
|
||||||
|
} // namespace ZShell::internal
|
||||||
Reference in New Issue
Block a user