#include "linegraph.hpp" #include "internal/linegeometry.hpp" #include #include #include #include #include #include #include namespace ZShell::components { namespace { constexpr qreal kNaN = std::numeric_limits::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& 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(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 cutLeft( const QVector& 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 left = points.first(j + 1); if (targetX > points[j].x()) left.append(internal::pointAtX(points, targetX, curveSmoothing)); return left; } QVector cutRight( const QVector& 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 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 fill; QVector> strokes; }; struct Marker { bool hasLine = false; qreal lineX = 0.0; QVector> 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 vGrid; QVector hGrid; QVector