311 lines
8.3 KiB
C++
311 lines
8.3 KiB
C++
#include "sparklinecontent.hpp"
|
|
|
|
#include "internal/linegeometry.hpp"
|
|
|
|
#include <qmath.h>
|
|
#include <qpainter.h>
|
|
#include <qpen.h>
|
|
#include <qquickwindow.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
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<qreal>(m_historyLength - 1)
|
|
: m_viewportW / static_cast<qreal>(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<qreal>(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<qreal>(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<QPointF> points;
|
|
points.reserve(len);
|
|
|
|
for (int i = 0; i < len; ++i) {
|
|
const qreal raw = buffer->smoothedAt(i);
|
|
|
|
const qreal value =
|
|
std::isfinite(raw) ? qBound<qreal>(0.0, raw, m_maxValue) : 0.0;
|
|
|
|
const qreal y = fromDev(
|
|
plotTopDev + static_cast<qint64>(std::llround(
|
|
(1.0 - value / m_maxValue) *
|
|
static_cast<double>(plotHDev))),
|
|
dpr);
|
|
points.append(
|
|
QPointF(startX + qreal(i) * step, snapLine(y, lineDev, dpr)));
|
|
}
|
|
|
|
const QVector<QPointF> 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<float>(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
|