diff options
-rw-r--r-- | stats/boxseries.cpp | 70 | ||||
-rw-r--r-- | stats/boxseries.h | 13 | ||||
-rw-r--r-- | stats/chartitem.cpp | 74 | ||||
-rw-r--r-- | stats/chartitem.h | 18 |
4 files changed, 118 insertions, 57 deletions
diff --git a/stats/boxseries.cpp b/stats/boxseries.cpp index b3dc2e5bd..2d6800e05 100644 --- a/stats/boxseries.cpp +++ b/stats/boxseries.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0 #include "boxseries.h" #include "informationbox.h" +#include "statsaxis.h" #include "statscolors.h" #include "statshelper.h" #include "statstranslations.h" @@ -11,7 +12,7 @@ // Constants that control the bar layout 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, 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) : 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); - scene->addItem(&box); - scene->addItem(&topWhisker); - scene->addItem(&bottomWhisker); - scene->addItem(&topBar); - scene->addItem(&bottomBar); - scene->addItem(¢er); + item = view.createChartItem<ChartBoxItem>(ChartZValue::Series, boxBorderWidth); highlight(false); updatePosition(series); } @@ -51,48 +41,34 @@ 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); + if (highlight) + item->setColor(highlightedColor, highlightedBorderColor); + else + item->setColor(fillColor, ::borderColor); } void BoxSeries::Item::updatePosition(BoxSeries *series) { + StatsAxis *xAxis = series->xAxis; + StatsAxis *yAxis = series->yAxis; + if (!xAxis || !yAxis) + return; + 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 = 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 = 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()); + + double fromScreen = xAxis->toScreen(from); + double toScreen = xAxis->toScreen(to); + double q1 = yAxis->toScreen(q.q1); + double q3 = yAxis->toScreen(q.q3); + QRectF rect(fromScreen, q3, toScreen - fromScreen, q1 - q3); + item->setBox(rect, yAxis->toScreen(q.min), yAxis->toScreen(q.max), yAxis->toScreen(q.q2)); } 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() @@ -106,8 +82,8 @@ 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; + [] (const std::unique_ptr<Item> &item, double x) { return item->item->getRect().right() < x; }); + 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) diff --git a/stats/boxseries.h b/stats/boxseries.h index c32f903ca..487870701 100644 --- a/stats/boxseries.h +++ b/stats/boxseries.h @@ -5,13 +5,12 @@ #ifndef BOX_SERIES_H #define BOX_SERIES_H +#include "chartitem.h" #include "statsseries.h" #include "statsvariables.h" // for StatsQuartiles #include <memory> #include <vector> -#include <QGraphicsLineItem> -#include <QGraphicsRectItem> struct InformationBox; class QGraphicsScene; @@ -36,16 +35,12 @@ private: 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(); + std::unique_ptr<ChartBoxItem> item; double lowerBound, upperBound; StatsQuartiles q; 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 highlight(bool highlight); }; diff --git a/stats/chartitem.cpp b/stats/chartitem.cpp index ce5e6860c..67cfb4937 100644 --- a/stats/chartitem.cpp +++ b/stats/chartitem.cpp @@ -279,3 +279,77 @@ QRectF ChartBarItem::getRect() const { 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; +} diff --git a/stats/chartitem.h b/stats/chartitem.h index a87d5fe10..650c45d53 100644 --- a/stats/chartitem.h +++ b/stats/chartitem.h @@ -123,7 +123,7 @@ public: void setRect(const QRectF &rect); QRectF getRect() const; void render() override; // Only call on render thread! -private: +protected: QColor color, borderColor; double borderWidth; QRectF rect; @@ -135,6 +135,22 @@ private: 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 template <typename Node> void HideableChartItem<Node>::setVisible(bool visibleIn) |