#include "sparklinecontent.hpp" #include #include #include namespace ZShell::components { SparklineContent::SparklineContent(QQuickItem* parent) : QQuickPaintedItem(parent) { setAntialiasing(true); } qreal SparklineContent::stepX() const { if (m_historyLength < 2 || m_viewportW <= 0.0) return 0.0; if (m_sliding) return m_viewportW / static_cast(m_historyLength - 1); const int len = qMax( 2, (m_line1 && m_line1->count() >= 2) ? m_line1->count() : (m_line2 ? m_line2->count() : 2)); return 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::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; if (!buffer || buffer->count() < 2 || m_maxValue <= 0.0 || m_viewportW <= 0.0) return geom; if (m_historyLength < 2) return geom; const int len = buffer->count(); const qreal step = stepX(); if (step <= 0.0) return geom; qreal startX = 0.0; if (m_sliding) startX = m_viewportW - (len - 1) * step + step; else startX = 0.0; const qreal h = m_viewportH; const qreal strokePad = qCeil(m_lineWidth / 2); const qreal curvePad = 3.0; const qreal plotTop = strokePad + curvePad; const qreal plotBottom = h - strokePad; const qreal fillBottom = h; const qreal plotH = qMax(1.0, plotBottom - plotTop); QVector points; points.reserve(len); for (int i = 0; i < len; ++i) { const qreal value = qBound(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 linePath; linePath.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(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)); linePath.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; linePath.cubicTo(QPointF(ctrlX, p0.y()), QPointF(ctrlX, p1.y()), p1); } } geom.stroke = linePath; QPainterPath fillPath = linePath; fillPath.lineTo(startX + (len - 1) * step, fillBottom); fillPath.lineTo(startX, 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); 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, m_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