diff options
author | Berthold Stoeger <bstoeger@mail.tuwien.ac.at> | 2021-01-05 12:11:46 +0100 |
---|---|---|
committer | Dirk Hohndel <dirk@hohndel.org> | 2021-01-06 12:31:22 -0800 |
commit | 4ab9f1c6b06204285267e79b5ed993514e0213e2 (patch) | |
tree | 5e8e792ec76639b1493937d6401bb92d05dabb1c /stats | |
parent | 598058e21ebc93834b71cc1b66e881451a596ce6 (diff) | |
download | subsurface-4ab9f1c6b06204285267e79b5ed993514e0213e2.tar.gz |
statistics: replace QtCharts' axes
Replace by custom implementation, with the ultimate goal to
remove the QtCharts module. This doesn't yet display axis
titles or a grid.
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
Diffstat (limited to 'stats')
-rw-r--r-- | stats/barseries.cpp | 32 | ||||
-rw-r--r-- | stats/barseries.h | 7 | ||||
-rw-r--r-- | stats/boxseries.cpp | 22 | ||||
-rw-r--r-- | stats/boxseries.h | 2 | ||||
-rw-r--r-- | stats/pieseries.cpp | 5 | ||||
-rw-r--r-- | stats/scatterseries.cpp | 15 | ||||
-rw-r--r-- | stats/scatterseries.h | 6 | ||||
-rw-r--r-- | stats/statsaxis.cpp | 260 | ||||
-rw-r--r-- | stats/statsaxis.h | 66 | ||||
-rw-r--r-- | stats/statscolors.h | 1 | ||||
-rw-r--r-- | stats/statsseries.cpp | 15 | ||||
-rw-r--r-- | stats/statsseries.h | 2 | ||||
-rw-r--r-- | stats/statsview.cpp | 132 | ||||
-rw-r--r-- | stats/statsview.h | 15 | ||||
-rw-r--r-- | stats/zvalues.h | 12 |
15 files changed, 372 insertions, 220 deletions
diff --git a/stats/barseries.cpp b/stats/barseries.cpp index 2f7f9cca9..5727745a0 100644 --- a/stats/barseries.cpp +++ b/stats/barseries.cpp @@ -114,8 +114,7 @@ void BarSeries::BarLabel::highlight(bool highlight, int bin_nr, int binCount) item->setBrush(brush); } -void BarSeries::BarLabel::updatePosition(QtCharts::QChart *chart, QtCharts::QAbstractSeries *series, - bool horizontal, bool center, const QRectF &rect, +void BarSeries::BarLabel::updatePosition(bool horizontal, bool center, const QRectF &rect, int bin_nr, int binCount) { if (!horizontal) { @@ -191,7 +190,7 @@ BarSeries::Item::Item(QtCharts::QChart *chart, BarSeries *series, double lowerBo item.item->setZValue(ZValues::series); item.highlight(false, binCount); } - updatePosition(chart, series, horizontal, stacked, binCount); + updatePosition(series, horizontal, stacked, binCount); } void BarSeries::Item::highlight(int subitem, bool highlight, int binCount) @@ -214,7 +213,7 @@ void BarSeries::SubItem::highlight(bool highlight, int binCount) label->highlight(highlight, bin_nr, binCount); } -void BarSeries::Item::updatePosition(QtCharts::QChart *chart, BarSeries *series, bool horizontal, bool stacked, int binCount) +void BarSeries::Item::updatePosition(BarSeries *series, bool horizontal, bool stacked, int binCount) { if (subitems.empty()) return; @@ -233,28 +232,28 @@ void BarSeries::Item::updatePosition(QtCharts::QChart *chart, BarSeries *series, for (SubItem &item: subitems) { int idx = stacked ? 0 : item.bin_nr; double center = (idx + 0.5) * fullSubWidth + from; - item.updatePosition(chart, series, horizontal, stacked, center - subWidth / 2.0, center + subWidth / 2.0, binCount); + item.updatePosition(series, horizontal, stacked, center - subWidth / 2.0, center + subWidth / 2.0, binCount); } rect = subitems[0].item->rect(); for (auto it = std::next(subitems.begin()); it != subitems.end(); ++it) rect = rect.united(it->item->rect()); } -void BarSeries::SubItem::updatePosition(QtCharts::QChart *chart, BarSeries *series, bool horizontal, bool stacked, +void BarSeries::SubItem::updatePosition(BarSeries *series, bool horizontal, bool stacked, double from, double to, int binCount) { QPointF topLeft, bottomRight; if (horizontal) { - topLeft = chart->mapToPosition(QPointF(value_from, to), series); - bottomRight = chart->mapToPosition(QPointF(value_to, from), series); + topLeft = series->toScreen(QPointF(value_from, to)); + bottomRight = series->toScreen(QPointF(value_to, from)); } else { - topLeft = chart->mapToPosition(QPointF(from, value_to), series); - bottomRight = chart->mapToPosition(QPointF(to, value_from), series); + topLeft = series->toScreen(QPointF(from, value_to)); + bottomRight = series->toScreen(QPointF(to, value_from)); } QRectF rect(topLeft, bottomRight); item->setRect(rect); if (label) - label->updatePosition(chart, series, horizontal, stacked, rect, bin_nr, binCount); + label->updatePosition(horizontal, stacked, rect, bin_nr, binCount); } std::vector<BarSeries::SubItem> BarSeries::makeSubItems(const std::vector<std::pair<double, std::vector<QString>>> &values) const @@ -265,9 +264,9 @@ std::vector<BarSeries::SubItem> BarSeries::makeSubItems(const std::vector<std::p int bin_nr = 0; for (auto &[v, label]: values) { if (v > 0.0) { - res.push_back({ std::make_unique<QGraphicsRectItem>(chart()), {}, from, from + v, bin_nr }); + res.push_back({ std::make_unique<QGraphicsRectItem>(chart), {}, from, from + v, bin_nr }); if (!label.empty()) - res.back().label = std::make_unique<BarLabel>(chart(), label, bin_nr, binCount()); + res.back().label = std::make_unique<BarLabel>(chart, label, bin_nr, binCount()); } if (stacked) from += v; @@ -293,15 +292,14 @@ void BarSeries::add_item(double lowerBound, double upperBound, std::vector<SubIt // Don't add empty items, as that messes with the "find item under mouse" routine. if (subitems.empty()) return; - items.emplace_back(chart(), this, lowerBound, upperBound, std::move(subitems), binName, res, + items.emplace_back(chart, this, lowerBound, upperBound, std::move(subitems), binName, res, total, horizontal, stacked, binCount()); } void BarSeries::updatePositions() { - QtCharts::QChart *c = chart(); for (Item &item: items) - item.updatePosition(c, this, horizontal, stacked, binCount()); + item.updatePosition(this, horizontal, stacked, binCount()); } // Attention: this supposes that items are sorted by position and no bar is inside another bar! @@ -405,7 +403,7 @@ bool BarSeries::hover(QPointF pos) Item &item = items[highlighted.bar]; item.highlight(index.subitem, true, binCount()); if (!information) - information.reset(new InformationBox(chart())); + information.reset(new InformationBox(chart)); information->setText(makeInfo(item, highlighted.subitem), pos); } else { information.reset(); diff --git a/stats/barseries.h b/stats/barseries.h index 09d008094..114df540a 100644 --- a/stats/barseries.h +++ b/stats/barseries.h @@ -87,8 +87,7 @@ private: bool isOutside; // Is shown outside of bar BarLabel(QtCharts::QChart *chart, const std::vector<QString> &labels, int bin_nr, int binCount); void setVisible(bool visible); - void updatePosition(QtCharts::QChart *chart, QtCharts::QAbstractSeries *series, - bool horizontal, bool center, const QRectF &rect, int bin_nr, int binCount); + void updatePosition(bool horizontal, bool center, const QRectF &rect, int bin_nr, int binCount); void highlight(bool highlight, int bin_nr, int binCount); }; @@ -98,7 +97,7 @@ private: double value_from; double value_to; int bin_nr; - void updatePosition(QtCharts::QChart *chart, BarSeries *series, bool horizontal, bool stacked, + void updatePosition(BarSeries *series, bool horizontal, bool stacked, double from, double to, int binCount); void highlight(bool highlight, int binCount); }; @@ -114,7 +113,7 @@ private: std::vector<SubItem> subitems, const QString &binName, const StatsOperationResults &res, int total, bool horizontal, bool stacked, int binCount); - void updatePosition(QtCharts::QChart *chart, BarSeries *series, bool horizontal, bool stacked, int binCount); + void updatePosition(BarSeries *series, bool horizontal, bool stacked, int binCount); void highlight(int subitem, bool highlight, int binCount); int getSubItemUnderMouse(const QPointF &f, bool horizontal, bool stacked) const; }; diff --git a/stats/boxseries.cpp b/stats/boxseries.cpp index 99651345b..b5e4422ea 100644 --- a/stats/boxseries.cpp +++ b/stats/boxseries.cpp @@ -39,7 +39,7 @@ BoxSeries::Item::Item(QtCharts::QChart *chart, BoxSeries *series, double lowerBo bottomBar.setZValue(ZValues::series); center.setZValue(ZValues::series); highlight(false); - updatePosition(chart, series); + updatePosition(series); } BoxSeries::Item::~Item() @@ -59,7 +59,7 @@ void BoxSeries::Item::highlight(bool highlight) center.setPen(pen); } -void BoxSeries::Item::updatePosition(QtCharts::QChart *chart, BoxSeries *series) +void BoxSeries::Item::updatePosition(BoxSeries *series) { double delta = (upperBound - lowerBound) * boxWidth; double from = (lowerBound + upperBound - delta) / 2.0; @@ -68,17 +68,17 @@ void BoxSeries::Item::updatePosition(QtCharts::QChart *chart, BoxSeries *series) QPointF topLeft, bottomRight; QMarginsF margins(boxBorderWidth / 2.0, boxBorderWidth / 2.0, boxBorderWidth / 2.0, boxBorderWidth / 2.0); - topLeft = chart->mapToPosition(QPointF(from, q.max), series); - bottomRight = chart->mapToPosition(QPointF(to, q.min), series); + topLeft = series->toScreen(QPointF(from, q.max)); + bottomRight = series->toScreen(QPointF(to, q.min)); bounding = QRectF(topLeft, bottomRight).marginsAdded(margins); double left = topLeft.x(); double right = bottomRight.x(); double width = right - left; double top = topLeft.y(); double bottom = bottomRight.y(); - QPointF q1 = chart->mapToPosition(QPointF(mid, q.q1), series); - QPointF q2 = chart->mapToPosition(QPointF(mid, q.q2), series); - QPointF q3 = chart->mapToPosition(QPointF(mid, q.q3), series); + QPointF q1 = series->toScreen(QPointF(mid, q.q1)); + QPointF q2 = series->toScreen(QPointF(mid, q.q2)); + QPointF q3 = series->toScreen(QPointF(mid, q.q3)); box.setRect(left, q3.y(), width, q1.y() - q3.y()); topWhisker.setLine(q3.x(), top, q3.x(), q3.y()); bottomWhisker.setLine(q1.x(), q1.y(), q1.x(), bottom); @@ -89,15 +89,13 @@ void BoxSeries::Item::updatePosition(QtCharts::QChart *chart, BoxSeries *series) void BoxSeries::append(double lowerBound, double upperBound, const StatsQuartiles &q, const QString &binName) { - QtCharts::QChart *c = chart(); - items.emplace_back(new Item(c, this, lowerBound, upperBound, q, binName)); + items.emplace_back(new Item(chart, this, lowerBound, upperBound, q, binName)); } void BoxSeries::updatePositions() { - QtCharts::QChart *c = chart(); for (auto &item: items) - item->updatePosition(c, this); + item->updatePosition(this); } // Attention: this supposes that items are sorted by position and no box is inside another box! @@ -149,7 +147,7 @@ bool BoxSeries::hover(QPointF pos) Item &item = *items[highlighted]; item.highlight(true); if (!information) - information.reset(new InformationBox(chart())); + information.reset(new InformationBox(chart)); information->setText(formatInformation(item), pos); } else { information.reset(); diff --git a/stats/boxseries.h b/stats/boxseries.h index 964e3aec1..43b177619 100644 --- a/stats/boxseries.h +++ b/stats/boxseries.h @@ -45,7 +45,7 @@ private: StatsQuartiles q; QString binName; Item(QtCharts::QChart *chart, BoxSeries *series, double lowerBound, double upperBound, const StatsQuartiles &q, const QString &binName); - void updatePosition(QtCharts::QChart *chart, BoxSeries *series); + void updatePosition(BoxSeries *series); void highlight(bool highlight); }; diff --git a/stats/pieseries.cpp b/stats/pieseries.cpp index f30753294..108046b23 100644 --- a/stats/pieseries.cpp +++ b/stats/pieseries.cpp @@ -167,8 +167,7 @@ PieSeries::~PieSeries() void PieSeries::updatePositions() { - QtCharts::QChart *c = chart(); - QRectF plotRect = c->plotArea(); + QRectF plotRect = chart->plotArea(); center = plotRect.center(); radius = std::min(plotRect.width(), plotRect.height()) * pieSize / 2.0; QRectF rect(center.x() - radius, center.y() - radius, 2.0 * radius, 2.0 * radius); @@ -247,7 +246,7 @@ bool PieSeries::hover(QPointF pos) if (highlighted >= 0 && highlighted < (int)items.size()) { items[highlighted].highlight(highlighted, true, (int)items.size()); if (!information) - information.reset(new InformationBox(chart())); + information.reset(new InformationBox(chart)); information->setText(makeInfo(highlighted), pos); } else { information.reset(); diff --git a/stats/scatterseries.cpp b/stats/scatterseries.cpp index 8ef795edc..de7a8a3bd 100644 --- a/stats/scatterseries.cpp +++ b/stats/scatterseries.cpp @@ -65,12 +65,12 @@ ScatterSeries::Item::Item(QtCharts::QChart *chart, ScatterSeries *series, dive * value(value) { item->setZValue(ZValues::series); - updatePosition(chart, series); + updatePosition(series); } -void ScatterSeries::Item::updatePosition(QtCharts::QChart *chart, ScatterSeries *series) +void ScatterSeries::Item::updatePosition(ScatterSeries *series) { - QPointF center = chart->mapToPosition(QPointF(pos, value), series); + QPointF center = series->toScreen(QPointF(pos, value)); item->setPos(center.x() - scatterItemDiameter / 2.0, center.y() - scatterItemDiameter / 2.0); } @@ -82,14 +82,13 @@ void ScatterSeries::Item::highlight(bool highlight) void ScatterSeries::append(dive *d, double pos, double value) { - items.emplace_back(chart(), this, d, pos, value); + items.emplace_back(chart, this, d, pos, value); } void ScatterSeries::updatePositions() { - QtCharts::QChart *c = chart(); for (Item &item: items) - item.updatePosition(c, this); + item.updatePosition(this); } static double sq(double f) @@ -103,7 +102,7 @@ static double squareDist(const QPointF &p1, const QPointF &p2) return QPointF::dotProduct(diff, diff); } -std::vector<int> ScatterSeries::getItemsUnderMouse(const QPointF &point) +std::vector<int> ScatterSeries::getItemsUnderMouse(const QPointF &point) const { std::vector<int> res; double x = point.x(); @@ -174,7 +173,7 @@ bool ScatterSeries::hover(QPointF pos) return false; } else { if (!information) - information.reset(new InformationBox(chart())); + information.reset(new InformationBox(chart)); std::vector<QString> text; text.reserve(highlighted.size() * 5); diff --git a/stats/scatterseries.h b/stats/scatterseries.h index 335fb828c..212a8e4ea 100644 --- a/stats/scatterseries.h +++ b/stats/scatterseries.h @@ -30,16 +30,14 @@ public: private: // Get items under mouse. - // Super weird: this function can't be const, because QChart::mapToValue takes - // a non-const reference!? - std::vector<int> getItemsUnderMouse(const QPointF &f); + std::vector<int> getItemsUnderMouse(const QPointF &f) const; struct Item { std::unique_ptr<QGraphicsPixmapItem> item; dive *d; double pos, value; Item(QtCharts::QChart *chart, ScatterSeries *series, dive *d, double pos, double value); - void updatePosition(QtCharts::QChart *chart, ScatterSeries *series); + void updatePosition(ScatterSeries *series); void highlight(bool highlight); }; diff --git a/stats/statsaxis.cpp b/stats/statsaxis.cpp index 81dd18c10..1a425cc0c 100644 --- a/stats/statsaxis.cpp +++ b/stats/statsaxis.cpp @@ -1,7 +1,9 @@ // SPDX-License-Identifier: GPL-2.0 #include "statsaxis.h" +#include "statscolors.h" #include "statstranslations.h" #include "statsvariables.h" +#include "zvalues.h" #include "core/pref.h" #include "core/subsurface-time.h" #include <math.h> // for lrint @@ -10,8 +12,28 @@ #include <QFontMetrics> #include <QLocale> -StatsAxis::StatsAxis(QtCharts::QChart *chart, bool horizontal) : chart(chart), horizontal(horizontal) +// Define most constants for horizontal and vertical axes for more flexibility. +// Note: *Horizontal means that this is for the horizontal axis, so a vertical space. +static const double axisWidth = 0.5; +static const double axisTickWidth = 0.3; +static const double axisTickSizeHorizontal = 6.0; +static const double axisTickSizeVertical = 6.0; +static const double axisLabelSpaceHorizontal = 2.0; // Space between axis or ticks and labels +static const double axisLabelSpaceVertical = 2.0; // Space between axis or ticks and labels +static const double axisTitleSpaceHorizontal = 2.0; // Space between labels and title +static const double axisTitleSpaceVertical = 2.0; // Space between labels and title + +StatsAxis::StatsAxis(QtCharts::QChart *chart, bool horizontal, bool labelsBetweenTicks) : + QGraphicsLineItem(chart), + chart(chart), horizontal(horizontal), labelsBetweenTicks(labelsBetweenTicks), + size(1.0), zeroOnScreen(0.0), min(0.0), max(1.0) { + // use a Light version of the application fond for both labels and title + labelFont = QFont(); + labelFont.setWeight(QFont::Light); + titleFont = labelFont; + setPen(QPen(axisColor, axisWidth)); + setZValue(ZValues::axes); } StatsAxis::~StatsAxis() @@ -20,7 +42,13 @@ StatsAxis::~StatsAxis() std::pair<double, double> StatsAxis::minMax() const { - return { 0.0, 1.0 }; + return { min, max }; +} + +void StatsAxis::setRange(double minIn, double maxIn) +{ + min = minIn; + max = maxIn; } // Guess the number of tick marks based on example strings. @@ -28,14 +56,13 @@ std::pair<double, double> StatsAxis::minMax() const // maximum-size strings especially, when using proportional fonts or for // categorical data. Therefore, try to err on the safe side by adding enough // margins. -int StatsAxis::guessNumTicks(const QtCharts::QAbstractAxis *axis, const std::vector<QString> &strings) const +int StatsAxis::guessNumTicks(const std::vector<QString> &strings) const { - QFont font = axis->labelsFont(); - QFontMetrics fm(font); + QFontMetrics fm(labelFont); int minSize = fm.height(); for (const QString &s: strings) { - QSize size = fm.size(Qt::TextSingleLine, s); - int needed = horizontal ? size.width() : size.height(); + QSize labelSize = fm.size(Qt::TextSingleLine, s); + int needed = horizontal ? labelSize.width() : labelSize.height(); if (needed > minSize) minSize = needed; } @@ -45,31 +72,127 @@ int StatsAxis::guessNumTicks(const QtCharts::QAbstractAxis *axis, const std::vec minSize = minSize * 3 / 2; else minSize *= 2; - QRectF chartSize = chart->plotArea(); - double availableSpace = horizontal ? chartSize.width() : chartSize.height(); - int numTicks = lrint(availableSpace / minSize); + int numTicks = lrint(size / minSize); return std::max(numTicks, 2); } -ValueAxis::ValueAxis(QtCharts::QChart *chart, double min, double max, int decimals, bool horizontal) : - StatsAxisTemplate(chart, horizontal), - min(min), max(max), decimals(decimals) +double StatsAxis::width() const +{ + if (horizontal) + return 0.0; // Only supported for vertical axes + double labelWidth = 0.0; + for (const Label &label: labels) { + double w = label.label->boundingRect().width(); + if (w > labelWidth) + labelWidth = w; + } + return labelWidth + axisLabelSpaceVertical + + QFontMetrics(titleFont).height() + axisTitleSpaceVertical + + (labelsBetweenTicks ? 0.0 : axisTickSizeVertical); +} + +double StatsAxis::height() const +{ + if (!horizontal) + return 0.0; // Only supported for horizontal axes + return QFontMetrics(labelFont).height() + axisLabelSpaceHorizontal + + QFontMetrics(titleFont).height() + axisTitleSpaceHorizontal + + (labelsBetweenTicks ? 0.0 : axisTickSizeHorizontal); +} + +StatsAxis::Label::Label(const QString &name, double pos, QtCharts::QChart *chart, const QFont &font) : + label(new QGraphicsSimpleTextItem(name, chart)), + pos(pos) +{ + label->setBrush(QBrush(darkLabelColor)); + label->setFont(font); + label->setZValue(ZValues::axes); +} + +void StatsAxis::addLabel(const QString &label, double pos) +{ + labels.emplace_back(label, pos, chart, labelFont); +} + +StatsAxis::Tick::Tick(double pos, QtCharts::QChart *chart) : + item(new QGraphicsLineItem(chart)), + pos(pos) +{ + item->setPen(QPen(axisColor, axisTickWidth)); + item->setZValue(ZValues::axes); +} + +void StatsAxis::addTick(double pos) +{ + ticks.emplace_back(pos, chart); +} + +// Map x (horizontal) or y (vertical) coordinate to or from screen coordinate +double StatsAxis::toScreen(double pos) const +{ + // Vertical is bottom-up + return horizontal ? (pos - min) / (max - min) * size + zeroOnScreen + : (min - pos) / (max - min) * size + zeroOnScreen; +} + +double StatsAxis::toValue(double pos) const { + // Vertical is bottom-up + return horizontal ? (pos - zeroOnScreen) / size * (max - min) + min + : (zeroOnScreen - pos) / size * (max - min) + zeroOnScreen; } -std::pair<double, double> ValueAxis::minMax() const +void StatsAxis::setSize(double sizeIn) { - return { QValueAxis::min(), QValueAxis::max() }; + size = sizeIn; + updateLabels(); } -static QString makeFormatString(int decimals) +void StatsAxis::setPos(QPointF pos) +{ + if (horizontal) { + zeroOnScreen = pos.x(); + double labelY = pos.y() + axisLabelSpaceHorizontal + + (labelsBetweenTicks ? 0.0 : axisTickSizeHorizontal); + double y = pos.y(); + for (Label &label: labels) { + double x = toScreen(label.pos) - label.label->boundingRect().width() / 2.0; + label.label->setPos(QPointF(x, labelY)); + } + for (Tick &tick: ticks) { + double x = toScreen(tick.pos); + tick.item->setLine(x, y, x, y + axisTickSizeHorizontal); + } + setLine(zeroOnScreen, y, zeroOnScreen + size, y); + } else { + double fontHeight = QFontMetrics(labelFont).height(); + zeroOnScreen = pos.y(); + double x = pos.x(); + double labelX = x - axisLabelSpaceVertical - + (labelsBetweenTicks ? 0.0 : axisTickSizeVertical); + for (Label &label: labels) { + double y = toScreen(label.pos) - fontHeight / 2.0; + label.label->setPos(QPointF(labelX - label.label->boundingRect().width(), y)); + } + for (Tick &tick: ticks) { + double y = toScreen(tick.pos); + tick.item->setLine(x, y, x - axisTickSizeVertical, y); + } + setLine(x, zeroOnScreen, x, zeroOnScreen - size); + } +} + +ValueAxis::ValueAxis(QtCharts::QChart *chart, double min, double max, int decimals, bool horizontal) : + StatsAxis(chart, horizontal, false), + min(min), max(max), decimals(decimals) { - return QStringLiteral("%.%1f").arg(decimals < 0 ? 0 : decimals); } void ValueAxis::updateLabels() { using QtCharts::QValueAxis; + labels.clear(); + ticks.clear(); // Avoid degenerate cases if (max - min < 0.0001) { @@ -80,7 +203,7 @@ void ValueAxis::updateLabels() QLocale loc; QString minString = loc.toString(min, 'f', decimals); QString maxString = loc.toString(max, 'f', decimals); - int numTicks = guessNumTicks(this, { minString, maxString}); + int numTicks = guessNumTicks({ minString, maxString}); // Use full decimal increments double height = max - min; @@ -98,12 +221,20 @@ void ValueAxis::updateLabels() if (-digits_int > decimals) decimals = -digits_int; - setLabelFormat(makeFormatString(decimals)); double actMin = floor(min / inc) * inc; double actMax = ceil(max / inc) * inc; int num = lrint((actMax - actMin) / inc); setRange(actMin, actMax); - setTickCount(num + 1); + + double actStep = (actMax - actMin) / static_cast<double>(num); + double act = actMin; + labels.reserve(num + 1); + ticks.reserve(num + 1); + for (int i = 0; i <= num; ++i) { + addLabel(loc.toString(act, 'f', decimals), act); + addTick(act); + act += actStep; + } } CountAxis::CountAxis(QtCharts::QChart *chart, int count, bool horizontal) : @@ -114,9 +245,12 @@ CountAxis::CountAxis(QtCharts::QChart *chart, int count, bool horizontal) : void CountAxis::updateLabels() { + labels.clear(); + ticks.clear(); + QLocale loc; QString countString = loc.toString(count); - int numTicks = guessNumTicks(this, { countString }); + int numTicks = guessNumTicks({ countString }); // Get estimate of step size if (count <= 0) @@ -145,60 +279,43 @@ void CountAxis::updateLabels() // Make maximum an integer number of steps, equal or greater than the needed counts int num = (count - 1) / step + 1; int max = num * step; - numTicks = num + 1; // There is one more tick than steps - setLabelFormat("%.0f"); setRange(0, max); - setTickCount(numTicks); -} -CategoryAxis::CategoryAxis(QtCharts::QChart *chart, const std::vector<QString> &labels, bool horizontal) : - StatsAxisTemplate(chart, horizontal) -{ - for (const QString &s: labels) - append(s); + labels.reserve(max + 1); + ticks.reserve(max + 1); + for (int i = 0; i <= max; i += step) { + addLabel(loc.toString(i), static_cast<double>(i)); + addTick(static_cast<double>(i)); + } } -void CategoryAxis::updateLabels() +CategoryAxis::CategoryAxis(QtCharts::QChart *chart, const std::vector<QString> &labelsIn, bool horizontal) : + StatsAxis(chart, horizontal, true) { + labels.reserve(labelsIn.size()); + ticks.reserve(labelsIn.size() + 1); + double pos = 0.0; + addTick(-0.5); + for (const QString &s: labelsIn) { + addLabel(s, pos); + addTick(pos + 0.5); + pos += 1.0; + } + setRange(-0.5, static_cast<double>(labelsIn.size()) + 0.5); } -// A small helper class that makes strings unique. We need this, -// because QCategoryAxis can only handle unique category names. -// Disambiguate strings by adding unicode zero-width spaces. -// Keep track of a list of strings and how many spaces have to -// be added. -class LabelDisambiguator { - using Pair = std::pair<QString, int>; - std::vector<Pair> entries; -public: - QString transmogrify(const QString &s); -}; - -QString LabelDisambiguator::transmogrify(const QString &s) +void CategoryAxis::updateLabels() { - auto it = std::find_if(entries.begin(), entries.end(), - [&s](const Pair &p) { return p.first == s; }); - if (it == entries.end()) { - entries.emplace_back(s, 0); - return s; - } else { - ++(it->second); - return s + QString(it->second, QChar(0x200b)); - } } HistogramAxis::HistogramAxis(QtCharts::QChart *chart, std::vector<HistogramAxisEntry> bins, bool horizontal) : - StatsAxisTemplate(chart, horizontal), + StatsAxis(chart, horizontal, false), bin_values(std::move(bins)) { if (bin_values.size() < 2) // Less than two makes no sense -> there must be at least one category return; - LabelDisambiguator labeler; - for (HistogramAxisEntry &entry: bin_values) - entry.name = labeler.transmogrify(entry.name); - // The caller can declare some bin labels as preferred, when there are // too many labels to show all. Try to infer the preferred step size // by finding two consecutive preferred labels. This supposes that @@ -210,17 +327,7 @@ HistogramAxis::HistogramAxis(QtCharts::QChart *chart, std::vector<HistogramAxisE auto it2 = std::find_if(next_it, bin_values.end(), [](const HistogramAxisEntry &e) { return e.recommended; }); preferred_step = it2 == bin_values.end() ? 1 : it2 - it1; - setMin(bin_values.front().value); - setMax(bin_values.back().value); - setStartValue(bin_values.front().value); - setLabelsPosition(QCategoryAxis::AxisLabelsPositionOnValue); -} - -std::pair<double, double> HistogramAxis::minMax() const -{ - if (bin_values.size() < 2) // Less than two makes no sense -> there must be at least one category - return { 0.0, 1.0 }; - return { QValueAxis::min(), QValueAxis::max() }; + setRange(bin_values.front().value, bin_values.back().value); } // Initialize a histogram axis with the given labels. Labels are specified as (name, value, recommended) triplets. @@ -229,20 +336,17 @@ std::pair<double, double> HistogramAxis::minMax() const // There, we obviously want to show the years and not the quarters. void HistogramAxis::updateLabels() { + labels.clear(); + ticks.clear(); + if (bin_values.size() < 2) // Less than two makes no sense -> there must be at least one category return; - // There is no clear all labels function in QCategoryAxis!? You must be kidding. - for (const QString &label: categoriesLabels()) - remove(label); - if (count() > 0) - qWarning("HistogramAxis::updateLabels(): labels left after clearing!?"); - std::vector<QString> strings; strings.reserve(bin_values.size()); for (auto &[name, value, recommended]: bin_values) strings.push_back(name); - int maxLabels = guessNumTicks(this, strings); + int maxLabels = guessNumTicks(strings); int step = ((int)bin_values.size() - 1) / maxLabels + 1; if (step < preferred_step) { @@ -268,9 +372,11 @@ void HistogramAxis::updateLabels() } } } + labels.reserve((bin_values.size() - first) / step + 1); for (int i = first; i < (int)bin_values.size(); i += step) { const auto &[name, value, recommended] = bin_values[i]; - append(name, value); + addLabel(name, value); + addTick(value); } } diff --git a/stats/statsaxis.h b/stats/statsaxis.h index 468dee1c2..c9619a124 100644 --- a/stats/statsaxis.h +++ b/stats/statsaxis.h @@ -1,50 +1,73 @@ // SPDX-License-Identifier: GPL-2.0 -// Supported chart axes #ifndef STATS_AXIS_H #define STATS_AXIS_H +#include <memory> #include <vector> #include <QBarCategoryAxis> #include <QCategoryAxis> +#include <QFont> +#include <QGraphicsSimpleTextItem> +#include <QGraphicsLineItem> #include <QValueAxis> namespace QtCharts { class QChart; } -class StatsAxis { +class StatsAxis : QGraphicsLineItem { public: virtual ~StatsAxis(); - virtual void updateLabels() = 0; - virtual QtCharts::QAbstractAxis *qaxis() = 0; // Returns minimum and maximum of shown range, not of data points. - virtual std::pair<double, double> minMax() const; + std::pair<double, double> minMax() const; + + double width() const; // Only supported by vertical axes. Only valid after setSize(). + double height() const; // Only supported for horizontal axes. Always valid. + void setSize(double size); // Width for horizontal and height for vertical. + void setPos(QPointF pos); // Must be called after setSize(). + void setRange(double, double); + + // Map x (horizontal) or y (vertical) coordinate to or from screen coordinate + double toScreen(double) const; + double toValue(double) const; protected: + StatsAxis(QtCharts::QChart *chart, bool horizontal, bool labelsBetweenTicks); QtCharts::QChart *chart; - StatsAxis(QtCharts::QChart *chart, bool horizontal); - int guessNumTicks(const QtCharts::QAbstractAxis *axis, const std::vector<QString> &strings) const; + + struct Label { + std::unique_ptr<QGraphicsSimpleTextItem> label; + double pos; + Label(const QString &name, double pos, QtCharts::QChart *chart, const QFont &font); + }; + std::vector<Label> labels; + void addLabel(const QString &label, double pos); + virtual void updateLabels() = 0; + + struct Tick { + std::unique_ptr<QGraphicsLineItem> item; + double pos; + Tick(double pos, QtCharts::QChart *chart); + }; + std::vector<Tick> ticks; + void addTick(double pos); + + int guessNumTicks(const std::vector<QString> &strings) const; bool horizontal; -}; + bool labelsBetweenTicks; // When labels are between ticks, they can be moved closer to the axis -// Small template that derives from a QChart-axis and defines -// the corresponding virtual axis() accessor. -template<typename QAxis> -class StatsAxisTemplate : public StatsAxis, public QAxis -{ - using StatsAxis::StatsAxis; - QtCharts::QAbstractAxis *qaxis() override final { - return this; - } + QFont labelFont, titleFont; + double size; // width for horizontal, height for vertical + double zeroOnScreen; + double min, max; }; -class ValueAxis : public StatsAxisTemplate<QtCharts::QValueAxis> { +class ValueAxis : public StatsAxis { public: ValueAxis(QtCharts::QChart *chart, double min, double max, int decimals, bool horizontal); private: double min, max; int decimals; void updateLabels() override; - std::pair<double, double> minMax() const override; }; class CountAxis : public ValueAxis { @@ -55,7 +78,7 @@ private: void updateLabels() override; }; -class CategoryAxis : public StatsAxisTemplate<QtCharts::QBarCategoryAxis> { +class CategoryAxis : public StatsAxis { public: CategoryAxis(QtCharts::QChart *chart, const std::vector<QString> &labels, bool horizontal); private: @@ -68,12 +91,11 @@ struct HistogramAxisEntry { bool recommended; }; -class HistogramAxis : public StatsAxisTemplate<QtCharts::QCategoryAxis> { +class HistogramAxis : public StatsAxis { public: HistogramAxis(QtCharts::QChart *chart, std::vector<HistogramAxisEntry> bin_values, bool horizontal); private: void updateLabels() override; - std::pair<double, double> minMax() const override; std::vector<HistogramAxisEntry> bin_values; int preferred_step; }; diff --git a/stats/statscolors.h b/stats/statscolors.h index 92ed53377..926e2e489 100644 --- a/stats/statscolors.h +++ b/stats/statscolors.h @@ -11,6 +11,7 @@ inline const QColor highlightedColor(Qt::yellow); inline const QColor highlightedBorderColor(0xaa, 0xaa, 0x22); inline const QColor darkLabelColor(Qt::black); inline const QColor lightLabelColor(Qt::white); +inline const QColor axisColor(Qt::black); QColor binColor(int bin, int numBins); QColor labelColor(int bin, size_t numBins); diff --git a/stats/statsseries.cpp b/stats/statsseries.cpp index 48b31336e..5996508a4 100644 --- a/stats/statsseries.cpp +++ b/stats/statsseries.cpp @@ -2,18 +2,17 @@ #include "statsseries.h" #include "statsaxis.h" -#include <QChart> - StatsSeries::StatsSeries(QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis) : - xAxis(xAxis), yAxis(yAxis) + chart(chart), xAxis(xAxis), yAxis(yAxis) { - chart->addSeries(this); - if (xAxis && yAxis) { - attachAxis(xAxis->qaxis()); - attachAxis(yAxis->qaxis()); - } } StatsSeries::~StatsSeries() { } + +QPointF StatsSeries::toScreen(QPointF p) +{ + return xAxis && yAxis ? QPointF(xAxis->toScreen(p.x()), yAxis->toScreen(p.y())) + : QPointF(0.0, 0.0); +} diff --git a/stats/statsseries.h b/stats/statsseries.h index 9b247fb95..5c1833c8a 100644 --- a/stats/statsseries.h +++ b/stats/statsseries.h @@ -23,7 +23,9 @@ public: virtual bool hover(QPointF pos) = 0; // Called on mouse movement. Return true if an item of this series is highlighted. virtual void unhighlight() = 0; // Unhighlight any highlighted item. protected: + QtCharts::QChart *chart; StatsAxis *xAxis, *yAxis; // May be zero for charts without axes (pie charts). + QPointF toScreen(QPointF p); }; #endif diff --git a/stats/statsview.cpp b/stats/statsview.cpp index 6709407e1..fdf6e9dc3 100644 --- a/stats/statsview.cpp +++ b/stats/statsview.cpp @@ -65,6 +65,8 @@ bool StatsView::EventFilter::eventFilter(QObject *o, QEvent *event) StatsView::StatsView(QWidget *parent) : QQuickWidget(parent), highlightedSeries(nullptr), + xAxis(nullptr), + yAxis(nullptr), eventFilter(this) { setResizeMode(QQuickWidget::SizeRootObjectToView); @@ -89,10 +91,33 @@ StatsView::~StatsView() { } -void StatsView::plotAreaChanged(const QRectF &) +void StatsView::plotAreaChanged(const QRectF &r) { - for (auto &axis: axes) - axis->updateLabels(); + double left = r.x() + sceneBorder; + double top = r.y() + sceneBorder; + double right = r.right() - sceneBorder; + double bottom = r.bottom() - sceneBorder; + const double minSize = 30.0; + + if (title) + top += title->boundingRect().height() + titleBorder; + // Currently, we only have either none, or an x- and a y-axis + if (xAxis) + bottom -= xAxis->height(); + if (bottom - top < minSize) + return; + if (yAxis) { + yAxis->setSize(bottom - top); + left += yAxis->width(); + yAxis->setPos(QPointF(left, bottom)); + } + if (right - left < minSize) + return; + if (xAxis) { + xAxis->setSize(right - left); + xAxis->setPos(QPointF(left, bottom)); + } + for (auto &series: series) series->updatePositions(); for (QuartileMarker &marker: quartileMarkers) @@ -133,8 +158,6 @@ void StatsView::hover(QPointF pos) template <typename T, class... Args> T *StatsView::createSeries(Args&&... args) { - StatsAxis *xAxis = axes.size() >= 2 ? axes[0].get() : nullptr; - StatsAxis *yAxis = axes.size() >= 2 ? axes[1].get() : nullptr; T *res = new T(chart, xAxis, yAxis, std::forward<Args>(args)...); series.emplace_back(res); series.back()->updatePositions(); @@ -156,24 +179,23 @@ void StatsView::updateTitlePos() if (!title) return; QRectF rect = chart->plotArea(); - title->setPos((rect.width() - title->boundingRect().width()) / 2.0, + title->setPos(sceneBorder + (rect.width() - title->boundingRect().width()) / 2.0, sceneBorder); } template <typename T, class... Args> T *StatsView::createAxis(const QString &title, Args&&... args) { + // TODO: set title T *res = new T(chart, std::forward<Args>(args)...); axes.emplace_back(res); - axes.back()->updateLabels(); - axes.back()->qaxis()->setTitleText(title); return res; } -void StatsView::addAxes(StatsAxis *x, StatsAxis *y) +void StatsView::setAxes(StatsAxis *x, StatsAxis *y) { - chart->addAxis(x->qaxis(), Qt::AlignBottom); - chart->addAxis(y->qaxis(), Qt::AlignLeft); + xAxis = x; + yAxis = y; } void StatsView::reset() @@ -181,6 +203,7 @@ void StatsView::reset() if (!chart) return; highlightedSeries = nullptr; + xAxis = yAxis = nullptr; legend.reset(); series.clear(); quartileMarkers.clear(); @@ -365,9 +388,9 @@ void StatsView::plotBarChart(const std::vector<dive *> &dives, CountAxis *valAxis = createCountAxis(maxVal, isHorizontal); if (isHorizontal) - addAxes(valAxis, catAxis); + setAxes(valAxis, catAxis); else - addAxes(catAxis, valAxis); + setAxes(catAxis, valAxis); // Paint legend first, because the bin-names will be moved away from. if (showLegend) @@ -478,9 +501,9 @@ void StatsView::plotValueChart(const std::vector<dive *> &dives, 0.0, maxValue, valueVariable->decimals(), isHorizontal); if (isHorizontal) - addAxes(valAxis, catAxis); + setAxes(valAxis, catAxis); else - addAxes(catAxis, valAxis); + setAxes(catAxis, valAxis); std::vector<BarSeries::ValueItem> items; items.reserve(categoryBins.size()); @@ -546,9 +569,9 @@ void StatsView::plotDiscreteCountChart(const std::vector<dive *> &dives, CountAxis *valAxis = createCountAxis(maxCount, isHorizontal); if (isHorizontal) - addAxes(valAxis, catAxis); + setAxes(valAxis, catAxis); else - addAxes(catAxis, valAxis); + setAxes(catAxis, valAxis); std::vector<BarSeries::CountItem> items; items.reserve(categoryBins.size()); @@ -613,7 +636,7 @@ void StatsView::plotDiscreteBoxChart(const std::vector<dive *> &dives, ValueAxis *valueAxis = createAxis<ValueAxis>(valueVariable->nameWithUnit(), minY, maxY, valueVariable->decimals(), false); - addAxes(catAxis, valueAxis); + setAxes(catAxis, valueAxis); BoxSeries *series = createSeries<BoxSeries>(valueVariable->name(), valueVariable->unitSymbol(), valueVariable->decimals()); @@ -648,7 +671,7 @@ void StatsView::plotDiscreteScatter(const std::vector<dive *> &dives, ValueAxis *valAxis = createAxis<ValueAxis>(valueVariable->nameWithUnit(), minValue, maxValue, valueVariable->decimals(), false); - addAxes(catAxis, valAxis); + setAxes(catAxis, valAxis); ScatterSeries *series = createSeries<ScatterSeries>(*categoryVariable, *valueVariable); double x = 0.0; @@ -658,18 +681,18 @@ void StatsView::plotDiscreteScatter(const std::vector<dive *> &dives, if (quartiles) { StatsQuartiles quartiles = StatsVariable::quartiles(array); if (quartiles.isValid()) { - quartileMarkers.emplace_back(x, quartiles.q1, series); - quartileMarkers.emplace_back(x, quartiles.q2, series); - quartileMarkers.emplace_back(x, quartiles.q3, series); + quartileMarkers.emplace_back(x, quartiles.q1, chart, catAxis, valAxis); + quartileMarkers.emplace_back(x, quartiles.q2, chart, catAxis, valAxis); + quartileMarkers.emplace_back(x, quartiles.q3, chart, catAxis, valAxis); } } x += 1.0; } } -StatsView::QuartileMarker::QuartileMarker(double pos, double value, QtCharts::QAbstractSeries *series) : - item(new QGraphicsLineItem(series->chart())), - series(series), +StatsView::QuartileMarker::QuartileMarker(double pos, double value, QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis) : + item(new QGraphicsLineItem(chart)), + xAxis(xAxis), yAxis(yAxis), pos(pos), value(value) { @@ -680,15 +703,18 @@ StatsView::QuartileMarker::QuartileMarker(double pos, double value, QtCharts::QA void StatsView::QuartileMarker::updatePosition() { - QtCharts::QChart *chart = series->chart(); - QPointF center = chart->mapToPosition(QPointF(pos, value), series); - item->setLine(center.x() - quartileMarkerSize / 2.0, center.y(), - center.x() + quartileMarkerSize / 2.0, center.y()); + if (!xAxis || !yAxis) + return; + double x = xAxis->toScreen(pos); + double y = yAxis->toScreen(value); + item->setLine(x - quartileMarkerSize / 2.0, y, + x + quartileMarkerSize / 2.0, y); } -StatsView::LineMarker::LineMarker(QPointF from, QPointF to, QPen pen, QtCharts::QAbstractSeries *series) : - item(new QGraphicsLineItem(series->chart())), - series(series), from(from), to(to) +StatsView::LineMarker::LineMarker(QPointF from, QPointF to, QPen pen, QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis) : + item(new QGraphicsLineItem(chart)), + xAxis(xAxis), yAxis(yAxis), + from(from), to(to) { item->setZValue(ZValues::chartFeatures); item->setPen(pen); @@ -697,12 +723,16 @@ StatsView::LineMarker::LineMarker(QPointF from, QPointF to, QPen pen, QtCharts:: void StatsView::LineMarker::updatePosition() { - QtCharts::QChart *chart = series->chart(); - item->setLine(QLineF(chart->mapToPosition(from, series), - chart->mapToPosition(to, series))); + if (!xAxis || !yAxis) + return; + double x1 = xAxis->toScreen(from.x()); + double y1 = yAxis->toScreen(from.y()); + double x2 = xAxis->toScreen(to.x()); + double y2 = yAxis->toScreen(to.y()); + item->setLine(x1, y1, x2, y2); } -void StatsView::addLinearRegression(double a, double b, double minX, double maxX, double minY, double maxY, QtCharts::QAbstractSeries *series) +void StatsView::addLinearRegression(double a, double b, double minX, double maxX, double minY, double maxY, StatsAxis *xAxis, StatsAxis *yAxis) { // Sanity check: line above or below chart double y1 = a * minX + b; @@ -721,14 +751,14 @@ void StatsView::addLinearRegression(double a, double b, double minX, double maxX minX = std::max(minX, intersect_x1); maxX = std::min(maxX, intersect_x2); } - lineMarkers.emplace_back(QPointF(minX, a * minX + b), QPointF(maxX, a * maxX + b), QPen(Qt::red), series); + lineMarkers.emplace_back(QPointF(minX, a * minX + b), QPointF(maxX, a * maxX + b), QPen(Qt::red), chart, xAxis, yAxis); } -void StatsView::addHistogramMarker(double pos, double low, double high, const QPen &pen, bool isHorizontal, QtCharts::QAbstractSeries *series) +void StatsView::addHistogramMarker(double pos, double low, double high, const QPen &pen, bool isHorizontal, StatsAxis *xAxis, StatsAxis *yAxis) { QPointF from = isHorizontal ? QPointF(low, pos) : QPointF(pos, low); QPointF to = isHorizontal ? QPointF(high, pos) : QPointF(pos, high); - lineMarkers.emplace_back(from, to, pen, series); + lineMarkers.emplace_back(from, to, pen, chart, xAxis, yAxis); } // Yikes, we get our data in different kinds of (bin, value) pairs. @@ -780,9 +810,9 @@ void StatsView::plotHistogramCountChart(const std::vector<dive *> &dives, double chartHeight = valAxis->minMax().second; if (isHorizontal) - addAxes(valAxis, catAxis); + setAxes(valAxis, catAxis); else - addAxes(catAxis, valAxis); + setAxes(catAxis, valAxis); std::vector<BarSeries::CountItem> items; items.reserve(categoryBins.size()); @@ -797,7 +827,7 @@ void StatsView::plotHistogramCountChart(const std::vector<dive *> &dives, categoryBinner->formatWithUnit(*bin), total }); } - BarSeries *series = createSeries<BarSeries>(isHorizontal, categoryVariable->name(), items); + createSeries<BarSeries>(isHorizontal, categoryVariable->name(), items); if (categoryVariable->type() == StatsVariable::Type::Numeric) { if (showMean) { @@ -805,14 +835,14 @@ void StatsView::plotHistogramCountChart(const std::vector<dive *> &dives, QPen meanPen(Qt::green); meanPen.setWidth(2); if (!std::isnan(mean)) - addHistogramMarker(mean, 0.0, chartHeight, meanPen, isHorizontal, series); + addHistogramMarker(mean, 0.0, chartHeight, meanPen, isHorizontal, xAxis, yAxis); } if (showMedian) { double median = categoryVariable->quartiles(dives).q2; QPen medianPen(Qt::red); medianPen.setWidth(2); if (!std::isnan(median)) - addHistogramMarker(median, 0.0, chartHeight, medianPen, isHorizontal, series); + addHistogramMarker(median, 0.0, chartHeight, medianPen, isHorizontal, xAxis, yAxis); } } } @@ -845,9 +875,9 @@ void StatsView::plotHistogramValueChart(const std::vector<dive *> &dives, 0.0, maxValue, decimals, isHorizontal); if (isHorizontal) - addAxes(valAxis, catAxis); + setAxes(valAxis, catAxis); else - addAxes(catAxis, valAxis); + setAxes(catAxis, valAxis); std::vector<BarSeries::ValueItem> items; items.reserve(categoryBins.size()); @@ -894,9 +924,9 @@ void StatsView::plotHistogramStackedChart(const std::vector<dive *> &dives, CountAxis *valAxis = createCountAxis(data.maxCategoryCount, isHorizontal); if (isHorizontal) - addAxes(valAxis, catAxis); + setAxes(valAxis, catAxis); else - addAxes(catAxis, valAxis); + setAxes(catAxis, valAxis); std::vector<BarSeries::MultiItem> items; items.reserve(data.hbin_counts.size()); @@ -933,7 +963,7 @@ void StatsView::plotHistogramBoxChart(const std::vector<dive *> &dives, ValueAxis *valueAxis = createAxis<ValueAxis>(valueVariable->nameWithUnit(), minY, maxY, valueVariable->decimals(), false); - addAxes(catAxis, valueAxis); + setAxes(catAxis, valueAxis); BoxSeries *series = createSeries<BoxSeries>(valueVariable->name(), valueVariable->unitSymbol(), valueVariable->decimals()); @@ -1020,7 +1050,7 @@ void StatsView::plotScatter(const std::vector<dive *> &dives, const StatsVariabl StatsAxis *axisY = createAxis<ValueAxis>(valueVariable->nameWithUnit(), minY, maxY, valueVariable->decimals(), false); - addAxes(axisX, axisY); + setAxes(axisX, axisY); ScatterSeries *series = createSeries<ScatterSeries>(*categoryVariable, *valueVariable); for (auto [x, y, dive]: points) @@ -1031,6 +1061,6 @@ void StatsView::plotScatter(const std::vector<dive *> &dives, const StatsVariabl if (!std::isnan(a)) { auto [minx, maxx] = axisX->minMax(); auto [miny, maxy] = axisY->minMax(); - addLinearRegression(a, b, minx, maxx, miny, maxy, series); + addLinearRegression(a, b, minx, maxx, miny, maxy, xAxis, yAxis); } } diff --git a/stats/statsview.h b/stats/statsview.h index fac61a55b..5df05c15b 100644 --- a/stats/statsview.h +++ b/stats/statsview.h @@ -41,7 +41,7 @@ private slots: void replotIfVisible(); private: void reset(); // clears all series and axes - void addAxes(StatsAxis *x, StatsAxis *y); // Add new x- and y-axis + void setAxes(StatsAxis *x, StatsAxis *y); void plotBarChart(const std::vector<dive *> &dives, ChartSubType subType, const StatsVariable *categoryVariable, const StatsBinner *categoryBinner, @@ -99,23 +99,23 @@ private: // A short line used to mark quartiles struct QuartileMarker { std::unique_ptr<QGraphicsLineItem> item; - QtCharts::QAbstractSeries *series; // In case we ever support charts with multiple axes + StatsAxis *xAxis, *yAxis; double pos, value; - QuartileMarker(double pos, double value, QtCharts::QAbstractSeries *series); + QuartileMarker(double pos, double value, QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis); void updatePosition(); }; // A general line marker struct LineMarker { std::unique_ptr<QGraphicsLineItem> item; - QtCharts::QAbstractSeries *series; // In case we ever support charts with multiple axes + StatsAxis *xAxis, *yAxis; QPointF from, to; // In local coordinates void updatePosition(); - LineMarker(QPointF from, QPointF to, QPen pen, QtCharts::QAbstractSeries *series); + LineMarker(QPointF from, QPointF to, QPen pen, QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis); }; - void addLinearRegression(double a, double b, double minX, double maxX, double minY, double maxY, QtCharts::QAbstractSeries *series); - void addHistogramMarker(double pos, double low, double high, const QPen &pen, bool isHorizontal, QtCharts::QAbstractSeries *series); + void addLinearRegression(double a, double b, double minX, double maxX, double minY, double maxY, StatsAxis *xAxis, StatsAxis *yAxis); + void addHistogramMarker(double pos, double low, double high, const QPen &pen, bool isHorizontal, StatsAxis *xAxis, StatsAxis *yAxis); StatsState state; QtCharts::QChart *chart; @@ -127,6 +127,7 @@ private: std::vector<LineMarker> lineMarkers; std::unique_ptr<QGraphicsSimpleTextItem> title; StatsSeries *highlightedSeries; + StatsAxis *xAxis, *yAxis; // This is unfortunate: we can't derive from QChart, because the chart is allocated by QML. // Therefore, we have to listen to hover events using an events-filter. diff --git a/stats/zvalues.h b/stats/zvalues.h index f4e7d6f09..1b6dc5d22 100644 --- a/stats/zvalues.h +++ b/stats/zvalues.h @@ -6,12 +6,12 @@ #ifndef ZVALUES_H struct ZValues { - static constexpr double axes = 0.0; - static constexpr double series = 11.0; - static constexpr double seriesLabels = 12.0; - static constexpr double chartFeatures = 13.0; // quartile markers and regression lines - static constexpr double informationBox = 14.0; - static constexpr double legend = 15.0; + static constexpr double series = 0.0; + static constexpr double axes = 1.0; + static constexpr double seriesLabels = 2.0; + static constexpr double chartFeatures = 3.0; // quartile markers and regression lines + static constexpr double informationBox = 4.0; + static constexpr double legend = 5.0; }; #endif |