From 4ab9f1c6b06204285267e79b5ed993514e0213e2 Mon Sep 17 00:00:00 2001 From: Berthold Stoeger Date: Tue, 5 Jan 2021 12:11:46 +0100 Subject: [PATCH] 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 Signed-off-by: Dirk Hohndel --- stats/barseries.cpp | 32 +++-- stats/barseries.h | 7 +- stats/boxseries.cpp | 22 ++-- stats/boxseries.h | 2 +- stats/pieseries.cpp | 5 +- stats/scatterseries.cpp | 15 ++- stats/scatterseries.h | 6 +- stats/statsaxis.cpp | 268 ++++++++++++++++++++++++++++------------ stats/statsaxis.h | 68 ++++++---- stats/statscolors.h | 1 + stats/statsseries.cpp | 15 ++- stats/statsseries.h | 2 + stats/statsview.cpp | 132 ++++++++++++-------- stats/statsview.h | 15 +-- stats/zvalues.h | 12 +- 15 files changed, 377 insertions(+), 225 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::makeSubItems(const std::vector>> &values) const @@ -265,9 +264,9 @@ std::vector BarSeries::makeSubItems(const std::vector 0.0) { - res.push_back({ std::make_unique(chart()), {}, from, from + v, bin_nr }); + res.push_back({ std::make_unique(chart), {}, from, from + v, bin_nr }); if (!label.empty()) - res.back().label = std::make_unique(chart(), label, bin_nr, binCount()); + res.back().label = std::make_unique(chart, label, bin_nr, binCount()); } if (stacked) from += v; @@ -293,15 +292,14 @@ void BarSeries::add_item(double lowerBound, double upperBound, std::vectorsetText(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 &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 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 ScatterSeries::getItemsUnderMouse(const QPointF &point) +std::vector ScatterSeries::getItemsUnderMouse(const QPointF &point) const { std::vector 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 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 getItemsUnderMouse(const QPointF &f); + std::vector getItemsUnderMouse(const QPointF &f) const; struct Item { std::unique_ptr 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 // for lrint @@ -10,8 +12,28 @@ #include #include -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 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 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 &strings) const +int StatsAxis::guessNumTicks(const std::vector &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); } +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; +} + +void StatsAxis::setSize(double sizeIn) +{ + size = sizeIn; + updateLabels(); +} + +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) : - StatsAxisTemplate(chart, horizontal), + StatsAxis(chart, horizontal, false), min(min), max(max), decimals(decimals) { } -std::pair ValueAxis::minMax() const -{ - return { QValueAxis::min(), QValueAxis::max() }; -} - -static QString makeFormatString(int 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(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); + + labels.reserve(max + 1); + ticks.reserve(max + 1); + for (int i = 0; i <= max; i += step) { + addLabel(loc.toString(i), static_cast(i)); + addTick(static_cast(i)); + } } -CategoryAxis::CategoryAxis(QtCharts::QChart *chart, const std::vector &labels, bool horizontal) : - StatsAxisTemplate(chart, horizontal) +CategoryAxis::CategoryAxis(QtCharts::QChart *chart, const std::vector &labelsIn, bool horizontal) : + StatsAxis(chart, horizontal, true) { - for (const QString &s: labels) - append(s); + 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(labelsIn.size()) + 0.5); } void CategoryAxis::updateLabels() { } -// 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; - std::vector entries; -public: - QString transmogrify(const QString &s); -}; - -QString LabelDisambiguator::transmogrify(const QString &s) -{ - 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 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 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 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 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 #include #include #include +#include +#include +#include #include 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 minMax() const; + std::pair 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 &strings) const; + + struct Label { + std::unique_ptr label; + double pos; + Label(const QString &name, double pos, QtCharts::QChart *chart, const QFont &font); + }; + std::vector