#include "sparklinecontent.hpp" #include "internal/linegeometry.hpp" #include #include #include #include #include #include namespace ZShell::components { using internal::fromDev; using internal::snapLine; using internal::toDev; namespace { 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; } return std::max(1.0, grow ? whole : std::floor(logical)); } } // 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)); return m_sliding ? m_viewportW / static_cast(m_historyLength - 1) : m_viewportW / static_cast(len - 1); } 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::setSlideInterval(qreal intervalMs) { if (qFuzzyCompare(m_slideIntervalMs, intervalMs)) return; m_slideIntervalMs = intervalMs; invalidate(); } qreal SparklineContent::slidePhase(qreal progress) const { if (m_slideIntervalMs <= 0.0) return qBound(0.0, progress, 1.0); if (!m_sampleClock.isValid()) return 1.0; const qreal elapsed = static_cast(m_sampleClock.elapsed()); return qBound(0.0, elapsed / m_slideIntervalMs, 1.0); } bool SparklineContent::slideActive() const { return m_slideIntervalMs > 0.0 && m_sampleClock.isValid() && static_cast(m_sampleClock.elapsed()) < m_slideIntervalMs; } 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); 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::onSampleArrived() { m_sampleClock.restart(); invalidate(); setSlideProgress(0.0); } void SparklineContent::connectBuffer(internal::CircularBuffer* buffer) { if (!buffer) return; connect( buffer, &internal::CircularBuffer::valuesChanged, this, &SparklineContent::onSampleArrived); 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 dpr = m_dpr > 0.0 ? m_dpr : 1.0; const qreal pad = m_sliding ? 2.0 * stepX() : 0.0; 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( internal::CircularBuffer* buffer) const { LineGeometry geom; 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_paintedH < 1.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 lineDev = std::isfinite(m_lineWidth) ? (std::max)(qint64(1), toDev(m_lineWidth, dpr)) : qint64(1); 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 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); const qreal value = std::isfinite(raw) ? qBound(0.0, raw, m_maxValue) : 0.0; const qreal y = fromDev( plotTopDev + static_cast(std::llround( (1.0 - value / m_maxValue) * static_cast(plotHDev))), dpr); points.append( QPointF(startX + qreal(i) * step, snapLine(y, lineDev, dpr))); } const QVector verts = internal::curveVertices(points, dpr, m_curveSmoothing); QPainterPath linePath; linePath.moveTo(verts.first()); for (int i = 1; i < verts.size(); ++i) linePath.lineTo(verts[i]); 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