diff options
author | Berthold Stoeger <bstoeger@mail.tuwien.ac.at> | 2021-01-01 22:23:29 +0100 |
---|---|---|
committer | Dirk Hohndel <dirk@hohndel.org> | 2021-01-02 11:04:03 -0800 |
commit | b0bdef469ee142f09b3994eddc8dba0d5c6f79c3 (patch) | |
tree | 6bf21021c8556b57091d4d92fdda6b8e34f3ef53 /stats/boxseries.cpp | |
parent | ca572acb0d23aef77fda896d08f2b1af360c5e99 (diff) | |
download | subsurface-b0bdef469ee142f09b3994eddc8dba0d5c6f79c3.tar.gz |
statistics: implement a box-and-whisker series
Implements a simple box-and-whisker series to display
quartile based data. When hovering over a box-and-whiskers
item the precise data of the quartiles is shown.
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
Diffstat (limited to 'stats/boxseries.cpp')
-rw-r--r-- | stats/boxseries.cpp | 165 |
1 files changed, 165 insertions, 0 deletions
diff --git a/stats/boxseries.cpp b/stats/boxseries.cpp new file mode 100644 index 000000000..99651345b --- /dev/null +++ b/stats/boxseries.cpp @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "boxseries.h" +#include "informationbox.h" +#include "statscolors.h" +#include "statstranslations.h" +#include "zvalues.h" + +#include <QChart> +#include <QLocale> + +// Constants that control the bar layout +static const double boxWidth = 0.8; // 1.0 = full width of category +static const int boxBorderWidth = 2; + +BoxSeries::BoxSeries(QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis, + const QString &variable, const QString &unit, int decimals) : + StatsSeries(chart, xAxis, yAxis), + variable(variable), unit(unit), decimals(decimals), highlighted(-1) +{ +} + +BoxSeries::~BoxSeries() +{ +} + +BoxSeries::Item::Item(QtCharts::QChart *chart, BoxSeries *series, double lowerBound, double upperBound, + const StatsQuartiles &q, const QString &binName) : + box(chart), + topWhisker(chart), bottomWhisker(chart), + topBar(chart), bottomBar(chart), + center(chart), + lowerBound(lowerBound), upperBound(upperBound), q(q), + binName(binName) +{ + box.setZValue(ZValues::series); + topWhisker.setZValue(ZValues::series); + bottomWhisker.setZValue(ZValues::series); + topBar.setZValue(ZValues::series); + bottomBar.setZValue(ZValues::series); + center.setZValue(ZValues::series); + highlight(false); + updatePosition(chart, series); +} + +BoxSeries::Item::~Item() +{ +} + +void BoxSeries::Item::highlight(bool highlight) +{ + QBrush brush = highlight ? QBrush(highlightedColor) : QBrush(fillColor); + QPen pen = highlight ? QPen(highlightedBorderColor, boxBorderWidth) : QPen(::borderColor, boxBorderWidth); + box.setBrush(brush); + box.setPen(pen); + topWhisker.setPen(pen); + bottomWhisker.setPen(pen); + topBar.setPen(pen); + bottomBar.setPen(pen); + center.setPen(pen); +} + +void BoxSeries::Item::updatePosition(QtCharts::QChart *chart, BoxSeries *series) +{ + double delta = (upperBound - lowerBound) * boxWidth; + double from = (lowerBound + upperBound - delta) / 2.0; + double to = (lowerBound + upperBound + delta) / 2.0; + double mid = (from + to) / 2.0; + + 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); + 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); + 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); + topBar.setLine(left, top, right, top); + bottomBar.setLine(left, bottom, right, bottom); + center.setLine(left, q2.y(), right, q2.y()); +} + +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)); +} + +void BoxSeries::updatePositions() +{ + QtCharts::QChart *c = chart(); + for (auto &item: items) + item->updatePosition(c, this); +} + +// Attention: this supposes that items are sorted by position and no box is inside another box! +int BoxSeries::getItemUnderMouse(const QPointF &point) +{ + // Search the first item whose "end" position is greater than the cursor position. + auto it = std::lower_bound(items.begin(), items.end(), point.x(), + [] (const std::unique_ptr<Item> &item, double x) { return item->bounding.right() < x; }); + return it != items.end() && (*it)->bounding.contains(point) ? it - items.begin() : -1; +} + +static QString infoItem(const QString &name, const QString &unit, int decimals, double value) +{ + QLocale loc; + QString formattedValue = loc.toString(value, 'f', decimals); + return unit.isEmpty() ? QStringLiteral(" %1: %2").arg(name, formattedValue) + : QStringLiteral(" %1: %2 %3").arg(name, formattedValue, unit); +} + +std::vector<QString> BoxSeries::formatInformation(const Item &item) const +{ + QLocale loc; + return { + item.binName, + QStringLiteral("%1:").arg(variable), + infoItem(StatsTranslations::tr("min"), unit, decimals, item.q.min), + infoItem(StatsTranslations::tr("Q1"), unit, decimals, item.q.q1), + infoItem(StatsTranslations::tr("mean"), unit, decimals, item.q.q2), + infoItem(StatsTranslations::tr("Q3"), unit, decimals, item.q.q3), + infoItem(StatsTranslations::tr("max"), unit, decimals, item.q.max) + }; +} + +// Highlight item when hovering over item +bool BoxSeries::hover(QPointF pos) +{ + int index = getItemUnderMouse(pos); + if (index == highlighted) { + if (information) + information->setPos(pos); + return index >= 0; + } + + unhighlight(); + highlighted = index; + + // Highlight new item (if any) + if (highlighted >= 0 && highlighted < (int)items.size()) { + Item &item = *items[highlighted]; + item.highlight(true); + if (!information) + information.reset(new InformationBox(chart())); + information->setText(formatInformation(item), pos); + } else { + information.reset(); + } + return highlighted >= 0; +} + +void BoxSeries::unhighlight() +{ + if (highlighted >= 0 && highlighted < (int)items.size()) + items[highlighted]->highlight(false); + highlighted = -1; +} |