mirror of
https://github.com/subsurface/subsurface.git
synced 2025-02-19 22:16:15 +00:00
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>
This commit is contained in:
parent
ca572acb0d
commit
b0bdef469e
3 changed files with 228 additions and 0 deletions
|
@ -7,6 +7,8 @@ include_directories(.
|
||||||
set(SUBSURFACE_STATS_SRCS
|
set(SUBSURFACE_STATS_SRCS
|
||||||
barseries.h
|
barseries.h
|
||||||
barseries.cpp
|
barseries.cpp
|
||||||
|
boxseries.h
|
||||||
|
boxseries.cpp
|
||||||
informationbox.h
|
informationbox.h
|
||||||
informationbox.cpp
|
informationbox.cpp
|
||||||
legend.h
|
legend.h
|
||||||
|
|
165
stats/boxseries.cpp
Normal file
165
stats/boxseries.cpp
Normal file
|
@ -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;
|
||||||
|
}
|
61
stats/boxseries.h
Normal file
61
stats/boxseries.h
Normal file
|
@ -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
|
Loading…
Add table
Reference in a new issue