aboutsummaryrefslogtreecommitdiffstats
path: root/stats
diff options
context:
space:
mode:
authorGravatar Berthold Stoeger <bstoeger@mail.tuwien.ac.at>2021-01-01 22:23:29 +0100
committerGravatar Dirk Hohndel <dirk@hohndel.org>2021-01-02 11:04:03 -0800
commitb0bdef469ee142f09b3994eddc8dba0d5c6f79c3 (patch)
tree6bf21021c8556b57091d4d92fdda6b8e34f3ef53 /stats
parentca572acb0d23aef77fda896d08f2b1af360c5e99 (diff)
downloadsubsurface-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')
-rw-r--r--stats/CMakeLists.txt2
-rw-r--r--stats/boxseries.cpp165
-rw-r--r--stats/boxseries.h61
3 files changed, 228 insertions, 0 deletions
diff --git a/stats/CMakeLists.txt b/stats/CMakeLists.txt
index 03ef73b28..b672e4a53 100644
--- a/stats/CMakeLists.txt
+++ b/stats/CMakeLists.txt
@@ -7,6 +7,8 @@ include_directories(.
set(SUBSURFACE_STATS_SRCS
barseries.h
barseries.cpp
+ boxseries.h
+ boxseries.cpp
informationbox.h
informationbox.cpp
legend.h
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;
+}
diff --git a/stats/boxseries.h b/stats/boxseries.h
new file mode 100644
index 000000000..964e3aec1
--- /dev/null
+++ b/stats/boxseries.h
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: GPL-2.0
+// A small custom box plot series, which displays quartiles.
+// The QtCharts bar series were too inflexible with respect
+// to placement of the bars.
+#ifndef BOX_SERIES_H
+#define BOX_SERIES_H
+
+#include "statsseries.h"
+#include "statsvariables.h" // for StatsQuartiles
+
+#include <memory>
+#include <vector>
+#include <QGraphicsLineItem>
+#include <QGraphicsRectItem>
+
+class InformationBox;
+
+class BoxSeries : public StatsSeries {
+public:
+ BoxSeries(QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis,
+ const QString &variable, const QString &unit, int decimals);
+ ~BoxSeries();
+
+ void updatePositions() override;
+ bool hover(QPointF pos) override;
+ void unhighlight() override;
+
+ // Note: this expects that all items are added with increasing pos
+ // and that no bar is inside another bar, i.e. lowerBound and upperBound
+ // are ordered identically.
+ void append(double lowerBound, double upperBound, const StatsQuartiles &q, const QString &binName);
+
+private:
+ // Get item under mouse pointer, or -1 if none
+ int getItemUnderMouse(const QPointF &f);
+
+ struct Item {
+ QGraphicsRectItem box;
+ QGraphicsLineItem topWhisker, bottomWhisker;
+ QGraphicsLineItem topBar, bottomBar;
+ QGraphicsLineItem center;
+ QRectF bounding; // bounding box in screen coordinates
+ ~Item();
+ double lowerBound, upperBound;
+ 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 highlight(bool highlight);
+ };
+
+ QString variable, unit;
+ int decimals;
+
+ std::vector<QString> formatInformation(const Item &item) const;
+ std::unique_ptr<InformationBox> information;
+ std::vector<std::unique_ptr<Item>> items;
+ int highlighted; // -1: no item highlighted
+};
+
+#endif