#include "sparklinecontent.hpp" #include #include #include #include #include #include namespace ZShell::components { 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(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; } 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& points, int i, bool curveSmoothing, QPointF& c1, QPointF& c2) { const QPointF& p0 = points[qMax(i - 1, 0)]; const QPointF& p1 = points[i]; const QPointF& p2 = points[i + 1]; const QPointF& p3 = points[qMin(i + 2, points.size() - 1)]; if (curveSmoothing && points.size() > 2) { const qreal k = 1.0 / 6.0; c1 = p1 + (p2 - p0) * k; c2 = p2 - (p3 - p1) * k; const qreal minY = qMin(p1.y(), p2.y()); const qreal maxY = qMax(p1.y(), p2.y()); c1.setY(qBound(minY, c1.y(), maxY)); c2.setY(qBound(minY, c2.y(), maxY)); return; } const qreal cx = (p1.x() + p2.x()) * 0.5; c1 = QPointF(cx, p1.y()); c2 = QPointF(cx, p2.y()); } } // namespace SparklineContent::SparklineContent(QQuickItem* parent) : QQuickPaintedItem(parent) { setAntialiasing(true); } void SparklineContent::itemChange( ItemChange change, const ItemChangeData& value) { QQuickPaintedItem::itemChange(change, value); if (change == ItemSceneChange && value.window) m_dpr = value.window->effectiveDevicePixelRatio(); else if (change == ItemDevicePixelRatioHasChanged && window()) m_dpr = window()->effectiveDevicePixelRatio(); else return; invalidate(); } qreal SparklineContent::stepX() const { if (m_historyLength < 2 || m_viewportW <= 0.0) return 0.0; const int len = qMax( 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(m_historyLength - 1) : m_viewportW / static_cast(len - 1); // A whole device pixel per step: every sample lands on a pixel column, and // one slide step moves the content by an exact pixel count. return fromDev((std::max)(qint64(1), toDev(logical, m_dpr)), m_dpr); } void SparklineContent::setViewportSize( qreal viewportWidth, qreal viewportHeight) { if (qFuzzyCompare(m_viewportW, viewportWidth) && qFuzzyCompare(m_viewportH, viewportHeight)) return; m_viewportW = viewportWidth; m_viewportH = viewportHeight; invalidate(); } void SparklineContent::setLine1(internal::CircularBuffer* buffer) { if (m_line1 == buffer) return; if (m_line1) disconnect(m_line1, nullptr, this, nullptr); m_line1 = buffer; connectBuffer(buffer); invalidate(); } void SparklineContent::setLine2(internal::CircularBuffer* buffer) { if (m_line2 == buffer) return; if (m_line2) disconnect(m_line2, nullptr, this, nullptr); m_line2 = buffer; connectBuffer(buffer); invalidate(); } void SparklineContent::connectBuffer(internal::CircularBuffer* buffer) { if (!buffer) return; connect( buffer, &internal::CircularBuffer::valuesChanged, this, &SparklineContent::invalidate); connect(buffer, &QObject::destroyed, this, [this, buffer]() { if (m_line1 == buffer) m_line1 = nullptr; if (m_line2 == buffer) m_line2 = nullptr; invalidate(); }); } void SparklineContent::setLine1Color(const QColor& color) { if (m_line1Color == color) return; m_line1Color = color; update(); } void SparklineContent::setLine2Color(const QColor& color) { if (m_line2Color == color) return; m_line2Color = color; update(); } void SparklineContent::setLine1FillAlpha(qreal alpha) { if (qFuzzyCompare(m_line1FillAlpha, alpha)) return; m_line1FillAlpha = alpha; update(); } void SparklineContent::setLine2FillAlpha(qreal alpha) { if (qFuzzyCompare(m_line2FillAlpha, alpha)) return; m_line2FillAlpha = alpha; update(); } void SparklineContent::setMaxValue(qreal value) { if (qFuzzyCompare(m_maxValue, value)) return; m_maxValue = value; invalidate(); } void SparklineContent::setLineWidth(qreal width) { if (qFuzzyCompare(m_lineWidth, width)) return; m_lineWidth = width; invalidate(); } void SparklineContent::setCurveSmoothing(bool enabled) { if (m_curveSmoothing == enabled) return; m_curveSmoothing = enabled; invalidate(); } void SparklineContent::setHistoryLength(int length) { if (m_historyLength == length) return; m_historyLength = length; invalidate(); } void SparklineContent::setSliding(bool sliding) { if (m_sliding == sliding) return; m_sliding = sliding; invalidate(); } void SparklineContent::invalidate() { m_dirty = true; applyGeometry(); update(); } void SparklineContent::applyGeometry() { const qreal pad = m_sliding ? stepX() : 0.0; setSize(QSizeF(m_viewportW + pad, m_viewportH)); } SparklineContent::LineGeometry SparklineContent::buildGeometry( internal::CircularBuffer* buffer) const { LineGeometry geom; // Every input has to be a usable number: a NaN from QML (an animated maxValue // fed by an empty buffer, for instance) would otherwise reach llround and the // 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) return geom; if (m_historyLength < 2) return geom; const qreal dpr = m_dpr > 0.0 ? m_dpr : 1.0; const int len = buffer->count(); 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); 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 // bottom, so neither edge of the stroke is cut by the item's own rect. const qint64 curvePadDev = toDev(m_curveSmoothing ? 3.0 : 0.0, dpr); const qint64 plotTopDev = lineDev / 2 + curvePadDev; const qint64 plotBottomDev = (std::max)(plotTopDev + 1, heightDev - (lineDev + 1) / 2 - 1); const qint64 plotHDev = plotBottomDev - plotTopDev; const qreal fillBottom = fromDev(heightDev, dpr); QVector points; points.reserve(len); for (int i = 0; i < len; ++i) { const qreal raw = buffer->smoothedAt(i); // A non-finite sample poisons the whole path: llround of it is undefined, // and the stroke and fill then ignore everything after that vertex. Treat // it as no traffic instead. const qreal value = std::isfinite(raw) ? qBound(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. const qreal y = fromDev( plotTopDev + static_cast(std::llround( (1.0 - value / m_maxValue) * static_cast(plotHDev))), dpr); points.append(QPointF( fromDev(startDev + qint64(i) * stepDev, dpr), snapLine(y, lineDev, dpr))); } 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); } geom.stroke = linePath; QPainterPath fillPath = linePath; fillPath.lineTo(points.last().x(), fillBottom); fillPath.lineTo(points.first().x(), fillBottom); fillPath.closeSubpath(); geom.fill = fillPath; geom.hasData = true; return geom; } void SparklineContent::rebuildIfNeeded() { if (!m_dirty) return; m_geom1 = buildGeometry(m_line1); m_geom2 = buildGeometry(m_line2); m_dirty = false; } void SparklineContent::paint(QPainter* painter) { rebuildIfNeeded(); if (!m_geom1.hasData && !m_geom2.hasData) return; painter->setRenderHint(QPainter::Antialiasing, true); const qreal dpr = m_dpr > 0.0 ? m_dpr : 1.0; const qreal lineWidth = std::isfinite(m_lineWidth) ? fromDev((std::max)(qint64(1), toDev(m_lineWidth, dpr)), dpr) : 1.0; auto drawOne = [&](const LineGeometry& geom, const QColor& color, qreal fillAlpha) { if (!geom.hasData) return; QColor fillColor = color; fillColor.setAlphaF(static_cast(fillAlpha)); painter->setPen(Qt::NoPen); painter->setBrush(fillColor); painter->drawPath(geom.fill); QPen pen(color, lineWidth); pen.setCapStyle(Qt::RoundCap); pen.setJoinStyle(Qt::RoundJoin); painter->setPen(pen); painter->setBrush(Qt::NoBrush); painter->drawPath(geom.stroke); }; drawOne(m_geom1, m_line1Color, m_line1FillAlpha); drawOne(m_geom2, m_line2Color, m_line2FillAlpha); } } // namespace ZShell::components