mirror of
https://github.com/subsurface/subsurface.git
synced 2024-11-30 22:20:21 +00:00
statistics: convert box-and-whiskers plot to QSG
This is lazy: Derive from the bar chart item and add whiskers in the subclassed render() function. The code is ugly, because the base class function clears the dirty flags and therefore the derived class has to remember them. Oh well. Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
This commit is contained in:
parent
5d5ebfcf3c
commit
409f159e1d
4 changed files with 117 additions and 56 deletions
|
@ -1,6 +1,7 @@
|
||||||
// SPDX-License-Identifier: GPL-2.0
|
// SPDX-License-Identifier: GPL-2.0
|
||||||
#include "boxseries.h"
|
#include "boxseries.h"
|
||||||
#include "informationbox.h"
|
#include "informationbox.h"
|
||||||
|
#include "statsaxis.h"
|
||||||
#include "statscolors.h"
|
#include "statscolors.h"
|
||||||
#include "statshelper.h"
|
#include "statshelper.h"
|
||||||
#include "statstranslations.h"
|
#include "statstranslations.h"
|
||||||
|
@ -11,7 +12,7 @@
|
||||||
|
|
||||||
// Constants that control the bar layout
|
// Constants that control the bar layout
|
||||||
static const double boxWidth = 0.8; // 1.0 = full width of category
|
static const double boxWidth = 0.8; // 1.0 = full width of category
|
||||||
static const int boxBorderWidth = 2;
|
static const int boxBorderWidth = 2.0;
|
||||||
|
|
||||||
BoxSeries::BoxSeries(QGraphicsScene *scene, StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,
|
BoxSeries::BoxSeries(QGraphicsScene *scene, StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,
|
||||||
const QString &variable, const QString &unit, int decimals) :
|
const QString &variable, const QString &unit, int decimals) :
|
||||||
|
@ -24,23 +25,12 @@ BoxSeries::~BoxSeries()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
BoxSeries::Item::Item(QGraphicsScene *scene, BoxSeries *series, double lowerBound, double upperBound,
|
BoxSeries::Item::Item(StatsView &view, BoxSeries *series, double lowerBound, double upperBound,
|
||||||
const StatsQuartiles &q, const QString &binName) :
|
const StatsQuartiles &q, const QString &binName) :
|
||||||
lowerBound(lowerBound), upperBound(upperBound), q(q),
|
lowerBound(lowerBound), upperBound(upperBound), q(q),
|
||||||
binName(binName)
|
binName(binName)
|
||||||
{
|
{
|
||||||
box.setZValue(ZValues::series);
|
item = view.createChartItem<ChartBoxItem>(ChartZValue::Series, boxBorderWidth);
|
||||||
topWhisker.setZValue(ZValues::series);
|
|
||||||
bottomWhisker.setZValue(ZValues::series);
|
|
||||||
topBar.setZValue(ZValues::series);
|
|
||||||
bottomBar.setZValue(ZValues::series);
|
|
||||||
center.setZValue(ZValues::series);
|
|
||||||
scene->addItem(&box);
|
|
||||||
scene->addItem(&topWhisker);
|
|
||||||
scene->addItem(&bottomWhisker);
|
|
||||||
scene->addItem(&topBar);
|
|
||||||
scene->addItem(&bottomBar);
|
|
||||||
scene->addItem(¢er);
|
|
||||||
highlight(false);
|
highlight(false);
|
||||||
updatePosition(series);
|
updatePosition(series);
|
||||||
}
|
}
|
||||||
|
@ -51,48 +41,34 @@ BoxSeries::Item::~Item()
|
||||||
|
|
||||||
void BoxSeries::Item::highlight(bool highlight)
|
void BoxSeries::Item::highlight(bool highlight)
|
||||||
{
|
{
|
||||||
QBrush brush = highlight ? QBrush(highlightedColor) : QBrush(fillColor);
|
if (highlight)
|
||||||
QPen pen = highlight ? QPen(highlightedBorderColor, boxBorderWidth) : QPen(::borderColor, boxBorderWidth);
|
item->setColor(highlightedColor, highlightedBorderColor);
|
||||||
box.setBrush(brush);
|
else
|
||||||
box.setPen(pen);
|
item->setColor(fillColor, ::borderColor);
|
||||||
topWhisker.setPen(pen);
|
|
||||||
bottomWhisker.setPen(pen);
|
|
||||||
topBar.setPen(pen);
|
|
||||||
bottomBar.setPen(pen);
|
|
||||||
center.setPen(pen);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BoxSeries::Item::updatePosition(BoxSeries *series)
|
void BoxSeries::Item::updatePosition(BoxSeries *series)
|
||||||
{
|
{
|
||||||
|
StatsAxis *xAxis = series->xAxis;
|
||||||
|
StatsAxis *yAxis = series->yAxis;
|
||||||
|
if (!xAxis || !yAxis)
|
||||||
|
return;
|
||||||
|
|
||||||
double delta = (upperBound - lowerBound) * boxWidth;
|
double delta = (upperBound - lowerBound) * boxWidth;
|
||||||
double from = (lowerBound + upperBound - delta) / 2.0;
|
double from = (lowerBound + upperBound - delta) / 2.0;
|
||||||
double to = (lowerBound + upperBound + delta) / 2.0;
|
double to = (lowerBound + upperBound + delta) / 2.0;
|
||||||
double mid = (from + to) / 2.0;
|
|
||||||
|
|
||||||
QPointF topLeft, bottomRight;
|
double fromScreen = xAxis->toScreen(from);
|
||||||
QMarginsF margins(boxBorderWidth / 2.0, boxBorderWidth / 2.0, boxBorderWidth / 2.0, boxBorderWidth / 2.0);
|
double toScreen = xAxis->toScreen(to);
|
||||||
topLeft = series->toScreen(QPointF(from, q.max));
|
double q1 = yAxis->toScreen(q.q1);
|
||||||
bottomRight = series->toScreen(QPointF(to, q.min));
|
double q3 = yAxis->toScreen(q.q3);
|
||||||
bounding = QRectF(topLeft, bottomRight).marginsAdded(margins);
|
QRectF rect(fromScreen, q3, toScreen - fromScreen, q1 - q3);
|
||||||
double left = topLeft.x();
|
item->setBox(rect, yAxis->toScreen(q.min), yAxis->toScreen(q.max), yAxis->toScreen(q.q2));
|
||||||
double right = bottomRight.x();
|
|
||||||
double width = right - left;
|
|
||||||
double top = topLeft.y();
|
|
||||||
double bottom = bottomRight.y();
|
|
||||||
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);
|
|
||||||
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)
|
void BoxSeries::append(double lowerBound, double upperBound, const StatsQuartiles &q, const QString &binName)
|
||||||
{
|
{
|
||||||
items.emplace_back(new Item(scene, this, lowerBound, upperBound, q, binName));
|
items.emplace_back(new Item(view, this, lowerBound, upperBound, q, binName));
|
||||||
}
|
}
|
||||||
|
|
||||||
void BoxSeries::updatePositions()
|
void BoxSeries::updatePositions()
|
||||||
|
@ -106,8 +82,8 @@ int BoxSeries::getItemUnderMouse(const QPointF &point)
|
||||||
{
|
{
|
||||||
// Search the first item whose "end" position is greater than the cursor position.
|
// Search the first item whose "end" position is greater than the cursor position.
|
||||||
auto it = std::lower_bound(items.begin(), items.end(), point.x(),
|
auto it = std::lower_bound(items.begin(), items.end(), point.x(),
|
||||||
[] (const std::unique_ptr<Item> &item, double x) { return item->bounding.right() < x; });
|
[] (const std::unique_ptr<Item> &item, double x) { return item->item->getRect().right() < x; });
|
||||||
return it != items.end() && (*it)->bounding.contains(point) ? it - items.begin() : -1;
|
return it != items.end() && (*it)->item->getRect().contains(point) ? it - items.begin() : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static QString infoItem(const QString &name, const QString &unit, int decimals, double value)
|
static QString infoItem(const QString &name, const QString &unit, int decimals, double value)
|
||||||
|
|
|
@ -5,13 +5,12 @@
|
||||||
#ifndef BOX_SERIES_H
|
#ifndef BOX_SERIES_H
|
||||||
#define BOX_SERIES_H
|
#define BOX_SERIES_H
|
||||||
|
|
||||||
|
#include "chartitem.h"
|
||||||
#include "statsseries.h"
|
#include "statsseries.h"
|
||||||
#include "statsvariables.h" // for StatsQuartiles
|
#include "statsvariables.h" // for StatsQuartiles
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <QGraphicsLineItem>
|
|
||||||
#include <QGraphicsRectItem>
|
|
||||||
|
|
||||||
struct InformationBox;
|
struct InformationBox;
|
||||||
class QGraphicsScene;
|
class QGraphicsScene;
|
||||||
|
@ -36,16 +35,12 @@ private:
|
||||||
int getItemUnderMouse(const QPointF &f);
|
int getItemUnderMouse(const QPointF &f);
|
||||||
|
|
||||||
struct Item {
|
struct Item {
|
||||||
QGraphicsRectItem box;
|
std::unique_ptr<ChartBoxItem> item;
|
||||||
QGraphicsLineItem topWhisker, bottomWhisker;
|
|
||||||
QGraphicsLineItem topBar, bottomBar;
|
|
||||||
QGraphicsLineItem center;
|
|
||||||
QRectF bounding; // bounding box in screen coordinates
|
|
||||||
~Item();
|
|
||||||
double lowerBound, upperBound;
|
double lowerBound, upperBound;
|
||||||
StatsQuartiles q;
|
StatsQuartiles q;
|
||||||
QString binName;
|
QString binName;
|
||||||
Item(QGraphicsScene *scene, BoxSeries *series, double lowerBound, double upperBound, const StatsQuartiles &q, const QString &binName);
|
Item(StatsView &view, BoxSeries *series, double lowerBound, double upperBound, const StatsQuartiles &q, const QString &binName);
|
||||||
|
~Item();
|
||||||
void updatePosition(BoxSeries *series);
|
void updatePosition(BoxSeries *series);
|
||||||
void highlight(bool highlight);
|
void highlight(bool highlight);
|
||||||
};
|
};
|
||||||
|
|
|
@ -279,3 +279,77 @@ QRectF ChartBarItem::getRect() const
|
||||||
{
|
{
|
||||||
return rect;
|
return rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChartBoxItem::ChartBoxItem(StatsView &v, ChartZValue z, double borderWidth) :
|
||||||
|
ChartBarItem(v, z, borderWidth, false) // Only support for vertical boxes
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ChartBoxItem::~ChartBoxItem()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartBoxItem::render()
|
||||||
|
{
|
||||||
|
// Remember old dirty values, since ChartBarItem::render() will clear them
|
||||||
|
bool oldPositionDirty = positionDirty;
|
||||||
|
bool oldColorDirty = colorDirty;
|
||||||
|
ChartBarItem::render(); // This will create the base node, so no need to check for that.
|
||||||
|
if (!whiskersNode) {
|
||||||
|
whiskersGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 10));
|
||||||
|
whiskersGeometry->setDrawingMode(QSGGeometry::DrawLines);
|
||||||
|
whiskersGeometry->setLineWidth(static_cast<float>(borderWidth));
|
||||||
|
whiskersMaterial.reset(new QSGFlatColorMaterial);
|
||||||
|
whiskersNode.reset(new QSGGeometryNode);
|
||||||
|
whiskersNode->setGeometry(whiskersGeometry.get());
|
||||||
|
whiskersNode->setMaterial(whiskersMaterial.get());
|
||||||
|
|
||||||
|
node->node->appendChildNode(whiskersNode.get());
|
||||||
|
// If this is the first time, make sure to update the geometry.
|
||||||
|
oldPositionDirty = oldColorDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldColorDirty) {
|
||||||
|
whiskersMaterial->setColor(borderColor);
|
||||||
|
whiskersNode->markDirty(QSGNode::DirtyMaterial);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldPositionDirty) {
|
||||||
|
auto vertices = whiskersGeometry->vertexDataAsPoint2D();
|
||||||
|
double left = rect.left();
|
||||||
|
double right = rect.right();
|
||||||
|
double mid = (left + right) / 2.0;
|
||||||
|
// top bar
|
||||||
|
setPoint(vertices[0], QPointF(left, max));
|
||||||
|
setPoint(vertices[1], QPointF(right, max));
|
||||||
|
// top whisker
|
||||||
|
setPoint(vertices[2], QPointF(mid, max));
|
||||||
|
setPoint(vertices[3], QPointF(mid, rect.top()));
|
||||||
|
// bottom bar
|
||||||
|
setPoint(vertices[4], QPointF(left, min));
|
||||||
|
setPoint(vertices[5], QPointF(right, min));
|
||||||
|
// bottom whisker
|
||||||
|
setPoint(vertices[6], QPointF(mid, min));
|
||||||
|
setPoint(vertices[7], QPointF(mid, rect.bottom()));
|
||||||
|
// median indicator
|
||||||
|
setPoint(vertices[8], QPointF(left, median));
|
||||||
|
setPoint(vertices[9], QPointF(right, median));
|
||||||
|
whiskersNode->markDirty(QSGNode::DirtyGeometry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartBoxItem::setBox(const QRectF &rect, double minIn, double maxIn, double medianIn)
|
||||||
|
{
|
||||||
|
min = minIn;
|
||||||
|
max = maxIn;
|
||||||
|
median = medianIn;
|
||||||
|
setRect(rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRectF ChartBoxItem::getRect() const
|
||||||
|
{
|
||||||
|
QRectF res = rect;
|
||||||
|
res.setTop(min);
|
||||||
|
res.setBottom(max);
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
|
@ -123,7 +123,7 @@ public:
|
||||||
void setRect(const QRectF &rect);
|
void setRect(const QRectF &rect);
|
||||||
QRectF getRect() const;
|
QRectF getRect() const;
|
||||||
void render() override; // Only call on render thread!
|
void render() override; // Only call on render thread!
|
||||||
private:
|
protected:
|
||||||
QColor color, borderColor;
|
QColor color, borderColor;
|
||||||
double borderWidth;
|
double borderWidth;
|
||||||
QRectF rect;
|
QRectF rect;
|
||||||
|
@ -135,6 +135,22 @@ private:
|
||||||
std::unique_ptr<QSGGeometry> borderGeometry;
|
std::unique_ptr<QSGGeometry> borderGeometry;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A box-and-whiskers item. This is a bit lazy: derive from the bar item and add whiskers.
|
||||||
|
class ChartBoxItem : public ChartBarItem {
|
||||||
|
public:
|
||||||
|
ChartBoxItem(StatsView &v, ChartZValue z, double borderWidth);
|
||||||
|
~ChartBoxItem();
|
||||||
|
void setBox(const QRectF &rect, double min, double max, double median); // The rect describes Q1, Q3.
|
||||||
|
QRectF getRect() const; // Note: this extends the center rectangle to include the whiskers.
|
||||||
|
void render() override; // Only call on render thread!
|
||||||
|
private:
|
||||||
|
double min, max, median;
|
||||||
|
std::unique_ptr<QSGGeometryNode> whiskersNode;
|
||||||
|
std::unique_ptr<QSGFlatColorMaterial> whiskersMaterial;
|
||||||
|
std::unique_ptr<QSGGeometry> whiskersGeometry;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Implementation detail of templates - move to serparate header file
|
// Implementation detail of templates - move to serparate header file
|
||||||
template <typename Node>
|
template <typename Node>
|
||||||
void HideableChartItem<Node>::setVisible(bool visibleIn)
|
void HideableChartItem<Node>::setVisible(bool visibleIn)
|
||||||
|
|
Loading…
Reference in a new issue