911 lines
22 KiB
C++
911 lines
22 KiB
C++
#include "linegraph.hpp"
|
|
|
|
#include "internal/linegeometry.hpp"
|
|
|
|
#include <QCanvasPainterItemRenderer>
|
|
#include <QFont>
|
|
#include <QQuickWindow>
|
|
#include <qcanvaspainter.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <limits>
|
|
|
|
namespace ZShell::components {
|
|
|
|
namespace {
|
|
constexpr qreal kNaN = std::numeric_limits<qreal>::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<QPointF>& 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<int>(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<QPointF> cutLeft(
|
|
const QVector<QPointF>& 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<QPointF> left = points.first(j + 1);
|
|
if (targetX > points[j].x())
|
|
left.append(internal::pointAtX(points, targetX, curveSmoothing));
|
|
return left;
|
|
}
|
|
|
|
QVector<QPointF> cutRight(
|
|
const QVector<QPointF>& 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<QPointF> 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<QPointF> fill;
|
|
QVector<QVector<QPointF>> strokes;
|
|
};
|
|
|
|
struct Marker {
|
|
bool hasLine = false;
|
|
qreal lineX = 0.0;
|
|
QVector<QPair<qreal, qreal>> 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<qreal> vGrid;
|
|
QVector<qreal> hGrid;
|
|
QVector<Label> xLabels;
|
|
QVector<Label> yLabels;
|
|
QVector<Segment> segments;
|
|
Marker marker;
|
|
qreal labelFontSize = 0.0;
|
|
};
|
|
|
|
void buildModel(const LineGraph* graph);
|
|
|
|
Model m_model;
|
|
QColor m_lineColor;
|
|
QColor m_gridColor;
|
|
QColor m_markerColor;
|
|
QColor m_labelColor;
|
|
qreal m_gridAlpha = 0.12;
|
|
qreal m_labelAlpha = 0.6;
|
|
qreal m_fillAlpha = 0.15;
|
|
qreal m_dpr = 1.0;
|
|
};
|
|
|
|
void LineGraphRenderer::synchronizeData(QCanvasPainterItem* item) {
|
|
auto* graph = static_cast<LineGraph*>(item);
|
|
m_lineColor = graph->m_lineColor;
|
|
m_gridColor = graph->m_gridColor;
|
|
m_markerColor = graph->m_markerColor;
|
|
m_labelColor = graph->m_labelColor;
|
|
m_gridAlpha = graph->m_gridAlpha;
|
|
m_labelAlpha = graph->m_labelAlpha;
|
|
m_fillAlpha = graph->m_fillAlpha;
|
|
m_dpr = graph->m_dpr > 0.0 ? graph->m_dpr : 1.0;
|
|
buildModel(graph);
|
|
}
|
|
|
|
void LineGraphRenderer::buildModel(const LineGraph* graph) {
|
|
Model model;
|
|
const qreal W = width();
|
|
const qreal H = height();
|
|
const qreal dpr = m_dpr;
|
|
model.height = H;
|
|
model.labelFontSize = graph->m_labelFontSize;
|
|
|
|
if (W < 2 || H < 2 || graph->m_xSteps < 1) {
|
|
m_model = model;
|
|
return;
|
|
}
|
|
|
|
const qreal padLeft = graph->m_labelFontSize * 5.0;
|
|
const qreal padBottom = graph->m_labelFontSize + 14.0;
|
|
model.plot = Plot {padLeft, 0.0, W - padLeft, H - padBottom};
|
|
if (model.plot.w < 2 || model.plot.h < 2) {
|
|
m_model = model;
|
|
return;
|
|
}
|
|
model.plotBottom = model.plot.y + model.plot.h;
|
|
|
|
// Raw series, holes as NaN.
|
|
QVector<qreal> raw;
|
|
raw.reserve(graph->m_values.size());
|
|
for (const QVariant& v : graph->m_values)
|
|
raw.append(parseValue(v));
|
|
|
|
// Moving average over the non-hole neighbours, the same window the sparkline
|
|
// applies at the same radius.
|
|
QVector<qreal> series = raw;
|
|
const int radius = qRound(graph->m_smoothing);
|
|
if (radius > 0 && !raw.isEmpty()) {
|
|
series.resize(raw.size());
|
|
for (int i = 0; i < raw.size(); ++i) {
|
|
if (std::isnan(raw[i])) {
|
|
series[i] = kNaN;
|
|
continue;
|
|
}
|
|
qreal sum = 0.0;
|
|
int n = 0;
|
|
for (int j = -radius; j <= radius; ++j) {
|
|
const int u = i + j;
|
|
if (u < 0 || u >= raw.size())
|
|
continue;
|
|
if (!std::isnan(raw[u])) {
|
|
sum += raw[u];
|
|
++n;
|
|
}
|
|
}
|
|
series[i] = n > 0 ? sum / qreal(n) : kNaN;
|
|
}
|
|
}
|
|
|
|
// y range: explicit yMin/yMax, or auto from the data snapped to the grid step.
|
|
qreal lo = graph->m_yMin;
|
|
qreal hi = graph->m_yMax;
|
|
const bool loAuto = std::isnan(lo);
|
|
const bool hiAuto = std::isnan(hi);
|
|
if (loAuto || hiAuto) {
|
|
qreal dmin = std::numeric_limits<qreal>::infinity();
|
|
qreal dmax = -std::numeric_limits<qreal>::infinity();
|
|
for (const qreal v : series) {
|
|
if (std::isnan(v))
|
|
continue;
|
|
dmin = std::min(dmin, v);
|
|
dmax = std::max(dmax, v);
|
|
}
|
|
if (std::isinf(dmin)) {
|
|
dmin = 0.0;
|
|
dmax = 1.0;
|
|
}
|
|
|
|
const int step = graph->m_horizontalLineStep;
|
|
const int minSteps = graph->m_minHorizontalLines > 1
|
|
? graph->m_minHorizontalLines - 1
|
|
: 0;
|
|
if (step > 0) {
|
|
if (hiAuto) {
|
|
const qreal baseLo =
|
|
loAuto ? std::floor(dmin / step) * step : lo;
|
|
const int span = std::max(
|
|
{static_cast<int>(std::ceil((dmax - baseLo) / step)),
|
|
minSteps,
|
|
1});
|
|
lo = baseLo;
|
|
hi = baseLo + qreal(span) * step;
|
|
} else {
|
|
const int span = std::max(
|
|
{static_cast<int>(std::ceil((hi - dmin) / step)),
|
|
minSteps,
|
|
1});
|
|
lo = hi - qreal(span) * step;
|
|
}
|
|
} else {
|
|
if (loAuto)
|
|
lo = dmin;
|
|
if (hiAuto)
|
|
hi = dmax;
|
|
if (hi <= lo)
|
|
hi = lo + 1.0;
|
|
}
|
|
}
|
|
|
|
const qreal scaleBottom = lo - graph->m_yPaddingBottom;
|
|
const qreal scaleTop = hi + graph->m_yPaddingTop;
|
|
const qreal range = scaleTop - scaleBottom;
|
|
if (range <= 0.0) {
|
|
m_model = model;
|
|
return;
|
|
}
|
|
|
|
const auto xAt = [&](qreal i) {
|
|
return model.plot.x + (i / qreal(graph->m_xSteps)) * model.plot.w;
|
|
};
|
|
const auto yAt = [&](qreal v) {
|
|
return model.plot.y
|
|
+ (1.0 - (v - scaleBottom) / range) * model.plot.h;
|
|
};
|
|
|
|
const auto devW = [&](qreal w) {
|
|
return std::max<qint64>(1, internal::toDev(w, dpr));
|
|
};
|
|
const qint64 gridWDev = devW(1.0);
|
|
const qint64 lineWDev = devW(graph->m_lineWidth);
|
|
model.gridWidth = internal::fromDev(gridWDev, dpr);
|
|
model.lineWidth = internal::fromDev(lineWDev, dpr);
|
|
|
|
const int vStep = graph->m_verticalLineStep;
|
|
const int hStep = graph->m_horizontalLineStep;
|
|
const auto tickVisible = [&](int i) {
|
|
if (i == 0 && !graph->m_firstXLine)
|
|
return false;
|
|
if (i == graph->m_xSteps && !graph->m_lastXLine)
|
|
return false;
|
|
return true;
|
|
};
|
|
|
|
if (vStep > 0) {
|
|
for (int i = 0; i <= graph->m_xSteps; i += vStep) {
|
|
if (!tickVisible(i))
|
|
continue;
|
|
model.vGrid.append(
|
|
internal::snapLine(xAt(i), gridWDev, dpr));
|
|
}
|
|
}
|
|
|
|
const qreal vStart = lo
|
|
- (graph->m_yPaddingBottom > 0.0
|
|
? std::floor(graph->m_yPaddingBottom / hStep)
|
|
* hStep
|
|
: 0.0);
|
|
if (hStep > 0) {
|
|
for (qreal v = vStart; v <= hi + 0.001; v += hStep)
|
|
model.hGrid.append(
|
|
internal::snapLine(yAt(v), gridWDev, dpr));
|
|
}
|
|
|
|
if (vStep > 0 && graph->m_xLabelMode != LineGraph::XLabelMode::None) {
|
|
for (int i = 0; i <= graph->m_xSteps; i += vStep) {
|
|
if (!tickVisible(i))
|
|
continue;
|
|
const QString text = formatXLabel(
|
|
graph->m_xLabelMode, i, graph->m_useTwelveHourClock);
|
|
if (!text.isEmpty())
|
|
model.xLabels.append(Label {
|
|
text, xAt(i) + 4.0,
|
|
model.plot.y + model.plot.h + 8.0});
|
|
}
|
|
}
|
|
|
|
if (hStep > 0 && graph->m_yLabelMode != LineGraph::YLabelMode::Off) {
|
|
for (qreal v = vStart; v <= hi + 0.001; v += hStep) {
|
|
const QString text = formatYLabel(
|
|
graph->m_yLabelMode, v, graph->m_yLabelDecimals,
|
|
graph->m_yLabelSuffix);
|
|
if (!text.isEmpty())
|
|
model.yLabels.append(Label {text, 2.0, yAt(v) - 2.0});
|
|
}
|
|
}
|
|
|
|
// Split the series into contiguous runs; each run becomes one curve. Points
|
|
// land on device-pixel columns so a flat run stays on whole columns.
|
|
const auto snapX = [&](qreal logical) {
|
|
return internal::fromDev(internal::toDev(logical, dpr), dpr);
|
|
};
|
|
|
|
QVector<QVector<QPointF>> runs;
|
|
QVector<QPointF> current;
|
|
for (int i = 0; i < series.size(); ++i) {
|
|
if (std::isnan(series[i])) {
|
|
if (!current.isEmpty()) {
|
|
runs.append(current);
|
|
current.clear();
|
|
}
|
|
continue;
|
|
}
|
|
current.append(QPointF(snapX(xAt(i)), yAt(series[i])));
|
|
}
|
|
if (!current.isEmpty())
|
|
runs.append(current);
|
|
|
|
const bool curve = graph->m_curveSmoothing;
|
|
const bool hasMarker =
|
|
graph->m_markerX >= 0.0 && graph->m_markerX <= graph->m_xSteps;
|
|
qreal markerPx = 0.0;
|
|
qreal gapPx = 0.0;
|
|
if (hasMarker) {
|
|
markerPx = xAt(graph->m_markerX);
|
|
qreal slope = 0.0;
|
|
for (const auto& run : runs) {
|
|
if (run.size() > 2 && run.first().x() <= markerPx
|
|
&& markerPx <= run.last().x()
|
|
&& markerPx - 1.0 >= run.first().x()
|
|
&& markerPx + 1.0 <= run.last().x()) {
|
|
slope = (internal::pointAtX(run, markerPx + 1.0, curve).y()
|
|
- internal::pointAtX(run, markerPx - 1.0, curve)
|
|
.y())
|
|
/ 2.0;
|
|
break;
|
|
}
|
|
}
|
|
gapPx = (graph->m_markerDotRadius + graph->m_markerGap)
|
|
/ std::sqrt(1.0 + slope * slope);
|
|
}
|
|
|
|
for (const auto& run : runs) {
|
|
if (run.size() < 2)
|
|
continue;
|
|
|
|
Segment seg;
|
|
seg.fill = internal::curveVertices(run, dpr, curve);
|
|
seg.fill.append(QPointF(run.last().x(), model.plotBottom));
|
|
seg.fill.append(QPointF(run.first().x(), model.plotBottom));
|
|
|
|
if (hasMarker) {
|
|
const QVector<QPointF> left =
|
|
cutLeft(run, markerPx - gapPx, curve);
|
|
const QVector<QPointF> right =
|
|
cutRight(run, markerPx + gapPx, curve);
|
|
if (left.size() >= 2)
|
|
seg.strokes.append(
|
|
internal::curveVertices(left, dpr, curve));
|
|
if (right.size() >= 2)
|
|
seg.strokes.append(
|
|
internal::curveVertices(right, dpr, curve));
|
|
} else {
|
|
seg.strokes.append(internal::curveVertices(run, dpr, curve));
|
|
}
|
|
model.segments.append(seg);
|
|
}
|
|
|
|
if (hasMarker) {
|
|
model.marker.hasLine = true;
|
|
model.marker.lineX =
|
|
internal::snapLine(markerPx, gridWDev, dpr);
|
|
|
|
// Dashes are drawn as solid segments: QCanvasPainter strokes are always
|
|
// solid, so a dashed line is a run of short strokes, as LineGraph.qml did.
|
|
qreal dashY = model.plot.y;
|
|
bool on = true;
|
|
while (dashY < H) {
|
|
const qreal dashEnd = std::min(dashY + 4.0, H);
|
|
if (on)
|
|
model.marker.dash.append(
|
|
{snapX(dashY), snapX(dashEnd)});
|
|
dashY = dashEnd;
|
|
on = !on;
|
|
}
|
|
|
|
const int i0 = static_cast<int>(std::floor(graph->m_markerX));
|
|
const qreal v0 = (i0 >= 0 && i0 < series.size()) ? series[i0]
|
|
: kNaN;
|
|
const qreal v1 = (i0 + 1 >= 0 && i0 + 1 < series.size())
|
|
? series[i0 + 1]
|
|
: kNaN;
|
|
qreal y = kNaN;
|
|
if (!std::isnan(v0) && !std::isnan(v1))
|
|
y = yAt(v0 + (v1 - v0) * (graph->m_markerX - qreal(i0)));
|
|
else if (!std::isnan(v0))
|
|
y = yAt(v0);
|
|
if (!std::isnan(y)) {
|
|
model.marker.hasDot = true;
|
|
model.marker.dot = QPointF(snapX(markerPx), y);
|
|
model.marker.dotRadius = graph->m_markerDotRadius;
|
|
}
|
|
}
|
|
|
|
// The plot is well-formed: grid, labels and marker can draw even with no curve
|
|
// segments, matching the QML which painted the grid regardless of data.
|
|
model.valid = true;
|
|
m_model = model;
|
|
}
|
|
|
|
void LineGraphRenderer::paint(QCanvasPainter* painter) {
|
|
painter->clearRect(0, 0, width(), height());
|
|
if (!m_model.valid)
|
|
return;
|
|
|
|
const Model& m = m_model;
|
|
|
|
// Grid: every line in one path, one stroke.
|
|
QColor grid = m_gridColor;
|
|
grid.setAlphaF(static_cast<float>(m_gridAlpha));
|
|
painter->setStrokeStyle(grid);
|
|
painter->setLineWidth(m.gridWidth);
|
|
painter->beginPath();
|
|
for (const qreal x : m.vGrid) {
|
|
painter->moveTo(x, m.plot.y);
|
|
painter->lineTo(x, m.height);
|
|
}
|
|
for (const qreal y : m.hGrid) {
|
|
painter->moveTo(0, y);
|
|
painter->lineTo(m.plot.x + m.plot.w, y);
|
|
}
|
|
painter->stroke();
|
|
|
|
// Axis labels.
|
|
QColor label = m_labelColor;
|
|
label.setAlphaF(static_cast<float>(m_labelAlpha));
|
|
painter->setFillStyle(label);
|
|
QFont font(QStringLiteral("sans-serif"));
|
|
font.setPointSizeF(m.labelFontSize);
|
|
painter->setFont(font);
|
|
painter->setTextAlign(QCanvasPainter::TextAlign::Left);
|
|
painter->setTextBaseline(QCanvasPainter::TextBaseline::Top);
|
|
for (const Label& l : m.xLabels)
|
|
painter->fillText(l.text, l.x, l.y);
|
|
painter->setTextBaseline(QCanvasPainter::TextBaseline::Bottom);
|
|
for (const Label& l : m.yLabels)
|
|
painter->fillText(l.text, l.x, l.y);
|
|
|
|
// Area fill, then the stroked curve pieces on top.
|
|
QColor fill = m_lineColor;
|
|
fill.setAlphaF(static_cast<float>(m_fillAlpha));
|
|
painter->setFillStyle(fill);
|
|
for (const Segment& seg : m.segments) {
|
|
painter->beginPath();
|
|
painter->moveTo(seg.fill.first());
|
|
for (int i = 1; i < seg.fill.size(); ++i)
|
|
painter->lineTo(seg.fill[i]);
|
|
painter->fill();
|
|
}
|
|
|
|
painter->setStrokeStyle(m_lineColor);
|
|
painter->setLineWidth(m.lineWidth);
|
|
painter->setLineCap(QCanvasPainter::LineCap::Round);
|
|
painter->setLineJoin(QCanvasPainter::LineJoin::Round);
|
|
for (const Segment& seg : m.segments) {
|
|
for (const auto& poly : seg.strokes) {
|
|
painter->beginPath();
|
|
painter->moveTo(poly.first());
|
|
for (int i = 1; i < poly.size(); ++i)
|
|
painter->lineTo(poly[i]);
|
|
painter->stroke();
|
|
}
|
|
}
|
|
|
|
if (m.marker.hasLine) {
|
|
QColor markerLine = m_markerColor;
|
|
markerLine.setAlphaF(0.6f);
|
|
painter->setStrokeStyle(markerLine);
|
|
painter->setLineWidth(m.gridWidth);
|
|
painter->beginPath();
|
|
for (const auto& d : m.marker.dash) {
|
|
painter->moveTo(m.marker.lineX, d.first);
|
|
painter->lineTo(m.marker.lineX, d.second);
|
|
}
|
|
painter->stroke();
|
|
}
|
|
|
|
if (m.marker.hasDot) {
|
|
painter->setFillStyle(m_markerColor);
|
|
painter->beginPath();
|
|
painter->circle(
|
|
m.marker.dot.x(), m.marker.dot.y(), m.marker.dotRadius);
|
|
painter->fill();
|
|
}
|
|
}
|
|
|
|
LineGraph::LineGraph(QQuickItem* parent) : QCanvasPainterItem(parent) {
|
|
setFillColor(Qt::transparent);
|
|
setAlphaBlending(true);
|
|
}
|
|
|
|
QCanvasPainterItemRenderer* LineGraph::createItemRenderer() const {
|
|
return new LineGraphRenderer;
|
|
}
|
|
|
|
void LineGraph::itemChange(ItemChange change, const ItemChangeData& value) {
|
|
QCanvasPainterItem::itemChange(change, value);
|
|
if (change == ItemSceneChange && value.window)
|
|
m_dpr = value.window->effectiveDevicePixelRatio();
|
|
else if (change == ItemDevicePixelRatioHasChanged && window())
|
|
m_dpr = window()->effectiveDevicePixelRatio();
|
|
}
|
|
|
|
QVariantList LineGraph::values() const {
|
|
return m_values;
|
|
}
|
|
|
|
void LineGraph::setValues(const QVariantList& values) {
|
|
if (m_values == values) return;
|
|
m_values = values;
|
|
emit valuesChanged();
|
|
update();
|
|
}
|
|
|
|
int LineGraph::xSteps() const {
|
|
return m_xSteps;
|
|
}
|
|
|
|
void LineGraph::setXSteps(int xSteps) {
|
|
if (m_xSteps == xSteps) return;
|
|
m_xSteps = xSteps;
|
|
emit xStepsChanged();
|
|
update();
|
|
}
|
|
|
|
qreal LineGraph::yMin() const {
|
|
return m_yMin;
|
|
}
|
|
|
|
void LineGraph::setYMin(qreal value) {
|
|
if (sameReal(m_yMin, value)) return;
|
|
m_yMin = value;
|
|
emit yMinChanged();
|
|
update();
|
|
}
|
|
|
|
qreal LineGraph::yMax() const {
|
|
return m_yMax;
|
|
}
|
|
|
|
void LineGraph::setYMax(qreal value) {
|
|
if (sameReal(m_yMax, value)) return;
|
|
m_yMax = value;
|
|
emit yMaxChanged();
|
|
update();
|
|
}
|
|
|
|
qreal LineGraph::yPaddingTop() const {
|
|
return m_yPaddingTop;
|
|
}
|
|
|
|
void LineGraph::setYPaddingTop(qreal value) {
|
|
if (sameReal(m_yPaddingTop, value)) return;
|
|
m_yPaddingTop = value;
|
|
emit yPaddingTopChanged();
|
|
update();
|
|
}
|
|
|
|
qreal LineGraph::yPaddingBottom() const {
|
|
return m_yPaddingBottom;
|
|
}
|
|
|
|
void LineGraph::setYPaddingBottom(qreal value) {
|
|
if (sameReal(m_yPaddingBottom, value)) return;
|
|
m_yPaddingBottom = value;
|
|
emit yPaddingBottomChanged();
|
|
update();
|
|
}
|
|
|
|
int LineGraph::verticalLineStep() const {
|
|
return m_verticalLineStep;
|
|
}
|
|
|
|
void LineGraph::setVerticalLineStep(int step) {
|
|
if (m_verticalLineStep == step) return;
|
|
m_verticalLineStep = step;
|
|
emit verticalLineStepChanged();
|
|
update();
|
|
}
|
|
|
|
int LineGraph::horizontalLineStep() const {
|
|
return m_horizontalLineStep;
|
|
}
|
|
|
|
void LineGraph::setHorizontalLineStep(int step) {
|
|
if (m_horizontalLineStep == step) return;
|
|
m_horizontalLineStep = step;
|
|
emit horizontalLineStepChanged();
|
|
update();
|
|
}
|
|
|
|
bool LineGraph::firstXLine() const {
|
|
return m_firstXLine;
|
|
}
|
|
|
|
void LineGraph::setFirstXLine(bool show) {
|
|
if (m_firstXLine == show) return;
|
|
m_firstXLine = show;
|
|
emit firstXLineChanged();
|
|
update();
|
|
}
|
|
|
|
bool LineGraph::lastXLine() const {
|
|
return m_lastXLine;
|
|
}
|
|
|
|
void LineGraph::setLastXLine(bool show) {
|
|
if (m_lastXLine == show) return;
|
|
m_lastXLine = show;
|
|
emit lastXLineChanged();
|
|
update();
|
|
}
|
|
|
|
int LineGraph::minHorizontalLines() const {
|
|
return m_minHorizontalLines;
|
|
}
|
|
|
|
void LineGraph::setMinHorizontalLines(int count) {
|
|
if (m_minHorizontalLines == count) return;
|
|
m_minHorizontalLines = count;
|
|
emit minHorizontalLinesChanged();
|
|
update();
|
|
}
|
|
|
|
LineGraph::XLabelMode LineGraph::xLabelMode() const {
|
|
return m_xLabelMode;
|
|
}
|
|
|
|
void LineGraph::setXLabelMode(XLabelMode mode) {
|
|
if (m_xLabelMode == mode) return;
|
|
m_xLabelMode = mode;
|
|
emit xLabelModeChanged();
|
|
update();
|
|
}
|
|
|
|
bool LineGraph::useTwelveHourClock() const {
|
|
return m_useTwelveHourClock;
|
|
}
|
|
|
|
void LineGraph::setUseTwelveHourClock(bool use) {
|
|
if (m_useTwelveHourClock == use) return;
|
|
m_useTwelveHourClock = use;
|
|
emit useTwelveHourClockChanged();
|
|
update();
|
|
}
|
|
|
|
LineGraph::YLabelMode LineGraph::yLabelMode() const {
|
|
return m_yLabelMode;
|
|
}
|
|
|
|
void LineGraph::setYLabelMode(YLabelMode mode) {
|
|
if (m_yLabelMode == mode) return;
|
|
m_yLabelMode = mode;
|
|
emit yLabelModeChanged();
|
|
update();
|
|
}
|
|
|
|
QString LineGraph::yLabelSuffix() const {
|
|
return m_yLabelSuffix;
|
|
}
|
|
|
|
void LineGraph::setYLabelSuffix(const QString& suffix) {
|
|
if (m_yLabelSuffix == suffix) return;
|
|
m_yLabelSuffix = suffix;
|
|
emit yLabelSuffixChanged();
|
|
update();
|
|
}
|
|
|
|
int LineGraph::yLabelDecimals() const {
|
|
return m_yLabelDecimals;
|
|
}
|
|
|
|
void LineGraph::setYLabelDecimals(int decimals) {
|
|
if (m_yLabelDecimals == decimals) return;
|
|
m_yLabelDecimals = decimals;
|
|
emit yLabelDecimalsChanged();
|
|
update();
|
|
}
|
|
|
|
qreal LineGraph::smoothing() const {
|
|
return m_smoothing;
|
|
}
|
|
|
|
void LineGraph::setSmoothing(qreal smoothing) {
|
|
smoothing = qMax<qreal>(0.0, smoothing);
|
|
if (sameReal(m_smoothing, smoothing)) return;
|
|
m_smoothing = smoothing;
|
|
emit smoothingChanged();
|
|
update();
|
|
}
|
|
|
|
bool LineGraph::curveSmoothing() const {
|
|
return m_curveSmoothing;
|
|
}
|
|
|
|
void LineGraph::setCurveSmoothing(bool enabled) {
|
|
if (m_curveSmoothing == enabled) return;
|
|
m_curveSmoothing = enabled;
|
|
emit curveSmoothingChanged();
|
|
update();
|
|
}
|
|
|
|
qreal LineGraph::markerX() const {
|
|
return m_markerX;
|
|
}
|
|
|
|
void LineGraph::setMarkerX(qreal markerX) {
|
|
if (sameReal(m_markerX, markerX)) return;
|
|
m_markerX = markerX;
|
|
emit markerXChanged();
|
|
update();
|
|
}
|
|
|
|
qreal LineGraph::markerGap() const {
|
|
return m_markerGap;
|
|
}
|
|
|
|
void LineGraph::setMarkerGap(qreal gap) {
|
|
if (sameReal(m_markerGap, gap)) return;
|
|
m_markerGap = gap;
|
|
emit markerGapChanged();
|
|
update();
|
|
}
|
|
|
|
QColor LineGraph::lineColor() const {
|
|
return m_lineColor;
|
|
}
|
|
|
|
void LineGraph::setLineColor(const QColor& color) {
|
|
if (m_lineColor == color) return;
|
|
m_lineColor = color;
|
|
emit lineColorChanged();
|
|
update();
|
|
}
|
|
|
|
QColor LineGraph::gridColor() const {
|
|
return m_gridColor;
|
|
}
|
|
|
|
void LineGraph::setGridColor(const QColor& color) {
|
|
if (m_gridColor == color) return;
|
|
m_gridColor = color;
|
|
emit gridColorChanged();
|
|
update();
|
|
}
|
|
|
|
QColor LineGraph::markerColor() const {
|
|
return m_markerColor;
|
|
}
|
|
|
|
void LineGraph::setMarkerColor(const QColor& color) {
|
|
if (m_markerColor == color) return;
|
|
m_markerColor = color;
|
|
emit markerColorChanged();
|
|
update();
|
|
}
|
|
|
|
QColor LineGraph::labelColor() const {
|
|
return m_labelColor;
|
|
}
|
|
|
|
void LineGraph::setLabelColor(const QColor& color) {
|
|
if (m_labelColor == color) return;
|
|
m_labelColor = color;
|
|
emit labelColorChanged();
|
|
update();
|
|
}
|
|
|
|
qreal LineGraph::gridAlpha() const {
|
|
return m_gridAlpha;
|
|
}
|
|
|
|
void LineGraph::setGridAlpha(qreal alpha) {
|
|
if (sameReal(m_gridAlpha, alpha)) return;
|
|
m_gridAlpha = alpha;
|
|
emit gridAlphaChanged();
|
|
update();
|
|
}
|
|
|
|
qreal LineGraph::labelAlpha() const {
|
|
return m_labelAlpha;
|
|
}
|
|
|
|
void LineGraph::setLabelAlpha(qreal alpha) {
|
|
if (sameReal(m_labelAlpha, alpha)) return;
|
|
m_labelAlpha = alpha;
|
|
emit labelAlphaChanged();
|
|
update();
|
|
}
|
|
|
|
qreal LineGraph::fillAlpha() const {
|
|
return m_fillAlpha;
|
|
}
|
|
|
|
void LineGraph::setFillAlpha(qreal alpha) {
|
|
if (sameReal(m_fillAlpha, alpha)) return;
|
|
m_fillAlpha = alpha;
|
|
emit fillAlphaChanged();
|
|
update();
|
|
}
|
|
|
|
qreal LineGraph::lineWidth() const {
|
|
return m_lineWidth;
|
|
}
|
|
|
|
void LineGraph::setLineWidth(qreal width) {
|
|
if (sameReal(m_lineWidth, width)) return;
|
|
m_lineWidth = width;
|
|
emit lineWidthChanged();
|
|
update();
|
|
}
|
|
|
|
qreal LineGraph::markerDotRadius() const {
|
|
return m_markerDotRadius;
|
|
}
|
|
|
|
void LineGraph::setMarkerDotRadius(qreal radius) {
|
|
if (sameReal(m_markerDotRadius, radius)) return;
|
|
m_markerDotRadius = radius;
|
|
emit markerDotRadiusChanged();
|
|
update();
|
|
}
|
|
|
|
qreal LineGraph::labelFontSize() const {
|
|
return m_labelFontSize;
|
|
}
|
|
|
|
void LineGraph::setLabelFontSize(qreal size) {
|
|
if (sameReal(m_labelFontSize, size)) return;
|
|
m_labelFontSize = size;
|
|
emit labelFontSizeChanged();
|
|
update();
|
|
}
|
|
|
|
} // namespace ZShell::components
|