feat: graphs now properly sized + port static graphs to c++

This commit is contained in:
2026-11-08 01:28:27 +01:00
parent a7767d2a80
commit d69a6bcafd
17 changed files with 1592 additions and 1257 deletions
+910
View File
@@ -0,0 +1,910 @@
#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