feat: graphs now properly sized + port static graphs to c++
This commit is contained in:
@@ -10,7 +10,7 @@ qml_module(ZShell-components
|
||||
carouselview.hpp carouselview.cpp
|
||||
sparklinecontent.hpp sparklinecontent.cpp
|
||||
sparklineitem.hpp sparklineitem.cpp
|
||||
sparklinesceneitem.hpp sparklinesceneitem.cpp
|
||||
linegraph.hpp linegraph.cpp
|
||||
roundedmask.hpp roundedmask.cpp
|
||||
roundedclip.hpp roundedclip.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 "internal/linegeometry.hpp"
|
||||
|
||||
#include <qmath.h>
|
||||
#include <qpainter.h>
|
||||
#include <qpen.h>
|
||||
@@ -10,85 +12,47 @@
|
||||
|
||||
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 {
|
||||
qint64 toDev(qreal logical, qreal dpr) {
|
||||
return std::llround(logical * dpr);
|
||||
}
|
||||
|
||||
qreal fromDev(qint64 device, qreal dpr) {
|
||||
return qreal(device) / dpr;
|
||||
}
|
||||
|
||||
// A line center whose device position leaves a whole-pixel-wide stroke: an even
|
||||
// width sits on pixel boundaries, an odd width on pixel centers. Same rule as
|
||||
// snapLine() in Components/LineGraph.qml.
|
||||
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;
|
||||
}
|
||||
|
||||
// y of a cubic bezier at targetX, found by bisection on the parameter. Used to
|
||||
// place one vertex per device pixel column, so a segment never interpolates its
|
||||
// edge across several columns.
|
||||
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;
|
||||
// A QQuickPaintedItem only lands on the device grid when its logical size is a
|
||||
// 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
|
||||
// texture/size rather than by dpr, and draws the texture into a quad of the
|
||||
// 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
|
||||
// to the dpr 1.5 grid falls between texture rows and the whole texture is then
|
||||
// resampled to a 112.5 pixel tall quad. That is the fuzz. 74 -> 111 and
|
||||
// 76 -> 114 are both exact.
|
||||
//
|
||||
// `grow` picks the direction: shrink to stay inside the slot, grow to stay
|
||||
// outside it. The overhang on the right has to grow, or the texture's own edge
|
||||
// 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);
|
||||
for (int guard = 0; guard < 16 && whole >= 1.0; ++guard) {
|
||||
const qreal device = whole * dpr;
|
||||
if (std::abs(device - std::round(device)) < 1e-6)
|
||||
return whole;
|
||||
whole += grow ? 1.0 : -1.0;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// 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());
|
||||
// No whole extent within the guard window (an exotic dpr): a whole number is
|
||||
// still closer than a fractional one, which Qt rounds away anyway.
|
||||
return std::max(1.0, grow ? whole : std::floor(logical));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SparklineContent::SparklineContent(QQuickItem* parent)
|
||||
: QQuickPaintedItem(parent) {
|
||||
m_sampleClock.start();
|
||||
setAntialiasing(true);
|
||||
}
|
||||
|
||||
@@ -111,12 +75,15 @@ qreal SparklineContent::stepX() const {
|
||||
2,
|
||||
(m_line1 && m_line1->count() >= 2) ? m_line1->count()
|
||||
: (m_line2 ? m_line2->count() : 2));
|
||||
const qreal logical =
|
||||
m_sliding ? m_viewportW / static_cast<qreal>(m_historyLength - 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);
|
||||
// Exactly the slot divided by the history, in logical pixels. Rounding this
|
||||
// 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);
|
||||
}
|
||||
|
||||
void SparklineContent::setViewportSize(
|
||||
@@ -129,6 +96,30 @@ void SparklineContent::setViewportSize(
|
||||
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) {
|
||||
if (m_line1 == buffer) return;
|
||||
if (m_line1) disconnect(m_line1, nullptr, this, nullptr);
|
||||
@@ -145,13 +136,24 @@ void SparklineContent::setLine2(internal::CircularBuffer* buffer) {
|
||||
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) {
|
||||
if (!buffer) return;
|
||||
connect(
|
||||
buffer,
|
||||
&internal::CircularBuffer::valuesChanged,
|
||||
this,
|
||||
&SparklineContent::invalidate);
|
||||
&SparklineContent::onSampleArrived);
|
||||
connect(buffer, &QObject::destroyed, this, [this, buffer]() {
|
||||
if (m_line1 == buffer) m_line1 = nullptr;
|
||||
if (m_line2 == buffer) m_line2 = nullptr;
|
||||
@@ -220,8 +222,20 @@ void SparklineContent::invalidate() {
|
||||
}
|
||||
|
||||
void SparklineContent::applyGeometry() {
|
||||
const qreal pad = m_sliding ? stepX() : 0.0;
|
||||
setSize(QSizeF(m_viewportW + pad, m_viewportH));
|
||||
const qreal dpr = m_dpr > 0.0 ? m_dpr : 1.0;
|
||||
// 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(
|
||||
@@ -232,7 +246,8 @@ SparklineContent::LineGeometry SparklineContent::buildGeometry(
|
||||
// path stroker as garbage, which draws a shape that ignores the curve.
|
||||
if (!buffer || buffer->count() < 2 || !std::isfinite(m_maxValue) ||
|
||||
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;
|
||||
if (m_historyLength < 2) return geom;
|
||||
|
||||
@@ -241,18 +256,18 @@ SparklineContent::LineGeometry SparklineContent::buildGeometry(
|
||||
const qreal step = stepX();
|
||||
if (!std::isfinite(step) || step <= 0.0) return geom;
|
||||
|
||||
const qint64 stepDev = toDev(step, dpr);
|
||||
const qint64 lineDev = std::isfinite(m_lineWidth)
|
||||
? (std::max)(qint64(1), toDev(m_lineWidth, dpr))
|
||||
: 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;
|
||||
if (m_sliding)
|
||||
startX = m_viewportW - (len - 1) * step + step;
|
||||
else
|
||||
startX = 0.0;
|
||||
const qint64 startDev = toDev(startX, dpr);
|
||||
|
||||
// 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
|
||||
@@ -276,42 +291,27 @@ SparklineContent::LineGeometry SparklineContent::buildGeometry(
|
||||
const qreal value =
|
||||
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.
|
||||
// Item-local, so the parent's placement cannot move the shape. A flat run
|
||||
// then lands on whole rows; slopes are interpolated, not snapped.
|
||||
// The y of every sample goes on the grid the stroke width needs; x stays
|
||||
// exact so the window's span is exactly the slot's width.
|
||||
const qreal y = fromDev(
|
||||
plotTopDev + static_cast<qint64>(std::llround(
|
||||
(1.0 - value / m_maxValue) *
|
||||
static_cast<double>(plotHDev))),
|
||||
dpr);
|
||||
points.append(QPointF(
|
||||
fromDev(startDev + qint64(i) * stepDev, dpr),
|
||||
snapLine(y, lineDev, dpr)));
|
||||
points.append(QPointF(startX + qreal(i) * step, 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;
|
||||
linePath.moveTo(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, 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);
|
||||
}
|
||||
linePath.moveTo(verts.first());
|
||||
for (int i = 1; i < verts.size(); ++i)
|
||||
linePath.lineTo(verts[i]);
|
||||
|
||||
geom.stroke = linePath;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <qcolor.h>
|
||||
#include <qelapsedtimer.h>
|
||||
#include <qpainterpath.h>
|
||||
#include <qquickpainteditem.h>
|
||||
|
||||
@@ -30,6 +31,15 @@ class SparklineContent : public QQuickPaintedItem {
|
||||
|
||||
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;
|
||||
|
||||
protected:
|
||||
@@ -44,12 +54,15 @@ class SparklineContent : public QQuickPaintedItem {
|
||||
bool hasData = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] qreal slidePhase(qreal progress) const;
|
||||
|
||||
void invalidate();
|
||||
void rebuildIfNeeded();
|
||||
[[nodiscard]] LineGeometry buildGeometry(
|
||||
internal::CircularBuffer* buffer) const;
|
||||
void connectBuffer(internal::CircularBuffer* buffer);
|
||||
void applyGeometry();
|
||||
void onSampleArrived();
|
||||
|
||||
internal::CircularBuffer* m_line1 = nullptr;
|
||||
internal::CircularBuffer* m_line2 = nullptr;
|
||||
@@ -65,7 +78,14 @@ class SparklineContent : public QQuickPaintedItem {
|
||||
|
||||
qreal m_viewportW = 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_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_geom2;
|
||||
|
||||
@@ -19,7 +19,7 @@ void SparklineItem::geometryChange(
|
||||
}
|
||||
|
||||
void SparklineItem::updateContentOffset() {
|
||||
m_content->setX(-m_content->stepX() * m_slideProgress);
|
||||
m_content->setSlideProgress(m_slideProgress);
|
||||
}
|
||||
|
||||
void SparklineItem::applySmoothingRadius(
|
||||
@@ -111,12 +111,26 @@ qreal SparklineItem::slideProgress() const {
|
||||
}
|
||||
|
||||
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;
|
||||
updateContentOffset();
|
||||
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 {
|
||||
return m_historyLength;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ class SparklineItem : public QQuickItem {
|
||||
Q_PROPERTY(
|
||||
qreal slideProgress READ slideProgress WRITE setSlideProgress NOTIFY
|
||||
slideProgressChanged)
|
||||
Q_PROPERTY(
|
||||
qreal slideInterval READ slideInterval WRITE setSlideInterval NOTIFY
|
||||
slideIntervalChanged)
|
||||
Q_PROPERTY(
|
||||
int historyLength READ historyLength WRITE setHistoryLength NOTIFY
|
||||
historyLengthChanged)
|
||||
@@ -80,6 +83,9 @@ class SparklineItem : public QQuickItem {
|
||||
[[nodiscard]] qreal slideProgress() const;
|
||||
void setSlideProgress(qreal progress);
|
||||
|
||||
[[nodiscard]] qreal slideInterval() const;
|
||||
void setSlideInterval(qreal intervalMs);
|
||||
|
||||
[[nodiscard]] int historyLength() const;
|
||||
void setHistoryLength(int length);
|
||||
|
||||
@@ -104,6 +110,7 @@ class SparklineItem : public QQuickItem {
|
||||
void line2FillAlphaChanged();
|
||||
void maxValueChanged();
|
||||
void slideProgressChanged();
|
||||
void slideIntervalChanged();
|
||||
void historyLengthChanged();
|
||||
void lineWidthChanged();
|
||||
void slidingChanged();
|
||||
@@ -127,6 +134,7 @@ class SparklineItem : public QQuickItem {
|
||||
qreal m_line2FillAlpha = 0.2;
|
||||
qreal m_maxValue = 1024.0;
|
||||
qreal m_slideProgress = 0.0;
|
||||
qreal m_slideInterval = 0.0;
|
||||
int m_historyLength = 30;
|
||||
qreal m_lineWidth = 2.0;
|
||||
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
|
||||
Reference in New Issue
Block a user