fix: graph jitter on load + format

This commit is contained in:
2026-11-09 20:20:27 +01:00
parent d69a6bcafd
commit e49ecabf62
13 changed files with 181 additions and 320 deletions
+65 -106
View File
@@ -17,8 +17,7 @@ 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);
if (std::isnan(a) || std::isnan(b)) return std::isnan(a) && std::isnan(b);
return qFuzzyCompare(a, b);
}
@@ -34,8 +33,7 @@ qreal parseValue(const QVariant& v) {
// 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;
if (targetX <= points[i + 1].x()) return i;
}
const int last = static_cast<int>(points.size()) - 2;
return last > 0 ? last : 0;
@@ -44,13 +42,9 @@ int indexAt(const QVector<QPointF>& points, qreal targetX) {
// 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 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);
@@ -60,13 +54,9 @@ QVector<QPointF> cutLeft(
}
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 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);
@@ -76,27 +66,20 @@ QVector<QPointF> cutRight(
}
QString formatXLabel(LineGraph::XLabelMode mode, int i, bool use12) {
if (mode == LineGraph::XLabelMode::None)
return {};
if (mode == LineGraph::XLabelMode::Number)
return QString::number(i);
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(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 {};
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
@@ -194,7 +177,7 @@ void LineGraphRenderer::buildModel(const LineGraph* graph) {
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};
model.plot = Plot{padLeft, 0.0, W - padLeft, H - padBottom};
if (model.plot.w < 2 || model.plot.h < 2) {
m_model = model;
return;
@@ -222,8 +205,7 @@ void LineGraphRenderer::buildModel(const LineGraph* graph) {
int n = 0;
for (int j = -radius; j <= radius; ++j) {
const int u = i + j;
if (u < 0 || u >= raw.size())
continue;
if (u < 0 || u >= raw.size()) continue;
if (!std::isnan(raw[u])) {
sum += raw[u];
++n;
@@ -242,8 +224,7 @@ void LineGraphRenderer::buildModel(const LineGraph* graph) {
qreal dmin = std::numeric_limits<qreal>::infinity();
qreal dmax = -std::numeric_limits<qreal>::infinity();
for (const qreal v : series) {
if (std::isnan(v))
continue;
if (std::isnan(v)) continue;
dmin = std::min(dmin, v);
dmax = std::max(dmax, v);
}
@@ -254,12 +235,12 @@ void LineGraphRenderer::buildModel(const LineGraph* graph) {
const int step = graph->m_horizontalLineStep;
const int minSteps = graph->m_minHorizontalLines > 1
? graph->m_minHorizontalLines - 1
: 0;
? graph->m_minHorizontalLines - 1
: 0;
if (step > 0) {
if (hiAuto) {
const qreal baseLo =
loAuto ? std::floor(dmin / step) * step : lo;
const qreal baseLo = loAuto ? std::floor(dmin / step) * step
: lo;
const int span = std::max(
{static_cast<int>(std::ceil((dmax - baseLo) / step)),
minSteps,
@@ -274,12 +255,9 @@ void LineGraphRenderer::buildModel(const LineGraph* graph) {
lo = hi - qreal(span) * step;
}
} else {
if (loAuto)
lo = dmin;
if (hiAuto)
hi = dmax;
if (hi <= lo)
hi = lo + 1.0;
if (loAuto) lo = dmin;
if (hiAuto) hi = dmax;
if (hi <= lo) hi = lo + 1.0;
}
}
@@ -295,8 +273,7 @@ void LineGraphRenderer::buildModel(const LineGraph* graph) {
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;
return model.plot.y + (1.0 - (v - scaleBottom) / range) * model.plot.h;
};
const auto devW = [&](qreal w) {
@@ -310,53 +287,48 @@ void LineGraphRenderer::buildModel(const LineGraph* graph) {
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;
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));
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);
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));
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;
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});
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_yLabelMode,
v,
graph->m_yLabelDecimals,
graph->m_yLabelSuffix);
if (!text.isEmpty())
model.yLabels.append(Label {text, 2.0, yAt(v) - 2.0});
model.yLabels.append(Label{text, 2.0, yAt(v) - 2.0});
}
}
@@ -378,36 +350,33 @@ void LineGraphRenderer::buildModel(const LineGraph* graph) {
}
current.append(QPointF(snapX(xAt(i)), yAt(series[i])));
}
if (!current.isEmpty())
runs.append(current);
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;
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;
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);
gapPx = (graph->m_markerDotRadius + graph->m_markerGap) /
std::sqrt(1.0 + slope * slope);
}
for (const auto& run : runs) {
if (run.size() < 2)
continue;
if (run.size() < 2) continue;
Segment seg;
seg.fill = internal::curveVertices(run, dpr, curve);
@@ -415,16 +384,13 @@ void LineGraphRenderer::buildModel(const LineGraph* graph) {
seg.fill.append(QPointF(run.first().x(), model.plotBottom));
if (hasMarker) {
const QVector<QPointF> left =
cutLeft(run, markerPx - gapPx, curve);
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));
seg.strokes.append(internal::curveVertices(left, dpr, curve));
if (right.size() >= 2)
seg.strokes.append(
internal::curveVertices(right, dpr, curve));
seg.strokes.append(internal::curveVertices(right, dpr, curve));
} else {
seg.strokes.append(internal::curveVertices(run, dpr, curve));
}
@@ -433,8 +399,7 @@ void LineGraphRenderer::buildModel(const LineGraph* graph) {
if (hasMarker) {
model.marker.hasLine = true;
model.marker.lineX =
internal::snapLine(markerPx, gridWDev, dpr);
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.
@@ -442,19 +407,15 @@ void LineGraphRenderer::buildModel(const LineGraph* graph) {
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)});
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;
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)));
@@ -475,8 +436,7 @@ void LineGraphRenderer::buildModel(const LineGraph* graph) {
void LineGraphRenderer::paint(QCanvasPainter* painter) {
painter->clearRect(0, 0, width(), height());
if (!m_model.valid)
return;
if (!m_model.valid) return;
const Model& m = m_model;
@@ -553,8 +513,7 @@ void LineGraphRenderer::paint(QCanvasPainter* painter) {
if (m.marker.hasDot) {
painter->setFillStyle(m_markerColor);
painter->beginPath();
painter->circle(
m.marker.dot.x(), m.marker.dot.y(), m.marker.dotRadius);
painter->circle(m.marker.dot.x(), m.marker.dot.y(), m.marker.dotRadius);
painter->fill();
}
}