diff options
author | Berthold Stoeger <bstoeger@mail.tuwien.ac.at> | 2021-01-15 18:39:14 +0100 |
---|---|---|
committer | bstoeger <32835590+bstoeger@users.noreply.github.com> | 2021-01-20 08:47:18 +0100 |
commit | 20088576604a3159cc9891bcfea888c15b096a96 (patch) | |
tree | 244e1996c21adb6071346d754e5ac401d726aa15 | |
parent | faf3e7079ddd680ab0e53a27a7ddc0d863792117 (diff) | |
download | subsurface-20088576604a3159cc9891bcfea888c15b096a96.tar.gz |
statistics: render regression item using QSGNode
Render the confidence area and the regression line into a pixmap
and show that using a QSGNode.
It is unclear whether it is preferred to do it this way or to
triangulate the confidence area into triangles to be drawn by
the shader.
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
-rw-r--r-- | Subsurface-mobile.pro | 2 | ||||
-rw-r--r-- | stats/CMakeLists.txt | 2 | ||||
-rw-r--r-- | stats/regressionitem.cpp | 87 | ||||
-rw-r--r-- | stats/regressionitem.h | 26 | ||||
-rw-r--r-- | stats/statsview.cpp | 59 | ||||
-rw-r--r-- | stats/statsview.h | 23 |
6 files changed, 124 insertions, 75 deletions
diff --git a/Subsurface-mobile.pro b/Subsurface-mobile.pro index 588e431d4..8e35c835d 100644 --- a/Subsurface-mobile.pro +++ b/Subsurface-mobile.pro @@ -138,6 +138,7 @@ SOURCES += subsurface-mobile-main.cpp \ stats/legend.cpp \ stats/pieseries.cpp \ stats/quartilemarker.cpp \ + stats/regressionitem.cpp \ stats/scatterseries.cpp \ stats/statsaxis.cpp \ stats/statscolors.cpp \ @@ -289,6 +290,7 @@ HEADERS += \ stats/legend.h \ stats/pieseries.h \ stats/quartilemarker.h \ + stats/regressionitem.h \ stats/scatterseries.h \ stats/statsaxis.h \ stats/statscolors.h \ diff --git a/stats/CMakeLists.txt b/stats/CMakeLists.txt index 9048acf97..daeb22146 100644 --- a/stats/CMakeLists.txt +++ b/stats/CMakeLists.txt @@ -24,6 +24,8 @@ set(SUBSURFACE_STATS_SRCS pieseries.cpp quartilemarker.h quartilemarker.cpp + regressionitem.h + regressionitem.cpp scatterseries.h scatterseries.cpp statsaxis.h diff --git a/stats/regressionitem.cpp b/stats/regressionitem.cpp new file mode 100644 index 000000000..db02cf688 --- /dev/null +++ b/stats/regressionitem.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "regressionitem.h" +#include "statsaxis.h" +#include "zvalues.h" + +#include <cmath> + +static const QColor regressionItemColor(Qt::red); +static const double regressionLineWidth = 2.0; + +RegressionItem::RegressionItem(StatsView &view, regression_data reg, + StatsAxis *xAxis, StatsAxis *yAxis) : + ChartPixmapItem(view, ChartZValue::ChartFeatures), + xAxis(xAxis), yAxis(yAxis), reg(reg) +{ +} + +RegressionItem::~RegressionItem() +{ +} + +void RegressionItem::updatePosition() +{ + if (!xAxis || !yAxis) + return; + auto [minX, maxX] = xAxis->minMax(); + auto [minY, maxY] = yAxis->minMax(); + auto [screenMinX, screenMaxX] = xAxis->minMaxScreen(); + + // Draw the confidence interval according to http://www2.stat.duke.edu/~tjl13/s101/slides/unit6lec3H.pdf p.5 with t*=2 for 95% confidence + QPolygonF poly; + const int num_samples = 101; + poly.reserve(num_samples * 2); + for (int i = 0; i < num_samples; ++i) { + double x = (maxX - minX) / (num_samples - 1) * static_cast<double>(i) + minX; + poly << QPointF(xAxis->toScreen(x), + yAxis->toScreen(reg.a * x + reg.b + 2.0 * sqrt(reg.res2 / (reg.n - 2) * (1.0 / reg.n + (x - reg.xavg) * (x - reg.xavg) / (reg.n - 1) * (reg.n -2) / reg.sx2)))); + } + for (int i = num_samples - 1; i >= 0; --i) { + double x = (maxX - minX) / (num_samples - 1) * static_cast<double>(i) + minX; + poly << QPointF(xAxis->toScreen(x), + yAxis->toScreen(reg.a * x + reg.b - 2.0 * sqrt(reg.res2 / (reg.n - 2) * (1.0 / reg.n + (x - reg.xavg) * (x - reg.xavg) / (reg.n - 1) * (reg.n -2) / reg.sx2)))); + } + QPolygonF linePolygon; + linePolygon.reserve(2); + linePolygon << QPointF(screenMinX, yAxis->toScreen(reg.a * minX + reg.b)); + linePolygon << QPointF(screenMaxX, yAxis->toScreen(reg.a * maxX + reg.b)); + + QRectF box(QPointF(screenMinX, yAxis->toScreen(minY)), QPointF(screenMaxX, yAxis->toScreen(maxY))); + + poly = poly.intersected(box); + linePolygon = linePolygon.intersected(box); + if (poly.size() < 2 || linePolygon.size() < 2) + return; + + // Find lowest and highest point on screen. In principle, we need + // only check half of the polygon, but let's not optimize without reason. + double screenMinY = std::numeric_limits<double>::max(); + double screenMaxY = std::numeric_limits<double>::lowest(); + for (const QPointF &point: poly) { + double y = point.y(); + if (y < screenMinY) + screenMinY = y; + if (y > screenMaxY) + screenMaxY = y; + } + screenMinY = floor(screenMinY - 1.0); + screenMaxY = ceil(screenMaxY + 1.0); + QPointF offset(screenMinX, screenMinY); + for (QPointF &point: poly) + point -= offset; + for (QPointF &point: linePolygon) + point -= offset; + ChartPixmapItem::resize(QSizeF(screenMaxX - screenMinX, screenMaxY - screenMinY)); + + img->fill(Qt::transparent); + QColor col(regressionItemColor); + col.setAlphaF(reg.r2); + painter->setPen(Qt::NoPen); + painter->setBrush(QBrush(col)); + painter->drawPolygon(poly); + + painter->setPen(QPen(regressionItemColor, regressionLineWidth)); + painter->drawLine(QPointF(linePolygon[0]), QPointF(linePolygon[1])); + + ChartPixmapItem::setPos(offset); +} diff --git a/stats/regressionitem.h b/stats/regressionitem.h new file mode 100644 index 000000000..607317d08 --- /dev/null +++ b/stats/regressionitem.h @@ -0,0 +1,26 @@ +// A regression line and confidence area +#ifndef REGRESSION_H +#define REGRESSION_H + +#include "chartitem.h" + +class StatsAxis; +class StatsView; + +struct regression_data { + double a,b; + double res2, r2, sx2, xavg; + int n; +}; + +class RegressionItem : public ChartPixmapItem { +public: + RegressionItem(StatsView &view, regression_data data, StatsAxis *xAxis, StatsAxis *yAxis); + ~RegressionItem(); + void updatePosition(); +private: + StatsAxis *xAxis, *yAxis; + regression_data reg; +}; + +#endif diff --git a/stats/statsview.cpp b/stats/statsview.cpp index 40d23eed3..455589976 100644 --- a/stats/statsview.cpp +++ b/stats/statsview.cpp @@ -6,6 +6,7 @@ #include "legend.h" #include "pieseries.h" #include "quartilemarker.h" +#include "regressionitem.h" #include "scatterseries.h" #include "statsaxis.h" #include "statscolors.h" @@ -235,8 +236,8 @@ void StatsView::plotAreaChanged(const QSizeF &s) series->updatePositions(); for (auto &marker: quartileMarkers) marker->updatePosition(); - for (RegressionLine &line: regressionLines) - line.updatePosition(); + if (regressionItem) + regressionItem->updatePosition(); for (auto &marker: histogramMarkers) marker->updatePosition(); if (legend) @@ -347,8 +348,8 @@ void StatsView::reset() legend.reset(); series.clear(); quartileMarkers.clear(); - regressionLines.clear(); histogramMarkers.clear(); + regressionItem.reset(); grid.reset(); axes.clear(); title.reset(); @@ -835,61 +836,11 @@ void StatsView::plotDiscreteScatter(const std::vector<dive *> &dives, } } -StatsView::RegressionLine::RegressionLine(const struct regression_data reg, QBrush brush, QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis) : - item(createItemPtr<QGraphicsPolygonItem>(scene)), - central(createItemPtr<QGraphicsPolygonItem>(scene)), - xAxis(xAxis), yAxis(yAxis), - reg(reg) -{ - item->setZValue(ZValues::chartFeatures); - item->setPen(Qt::NoPen); - item->setBrush(brush); - - central->setZValue(ZValues::chartFeatures+1); - central->setPen(QPen(Qt::red)); -} - -void StatsView::RegressionLine::updatePosition() -{ - if (!xAxis || !yAxis) - return; - auto [minX, maxX] = xAxis->minMax(); - auto [minY, maxY] = yAxis->minMax(); - - QPolygonF line; - line << QPoint(xAxis->toScreen(minX), yAxis->toScreen(reg.a * minX + reg.b)) - << QPoint(xAxis->toScreen(maxX), yAxis->toScreen(reg.a * maxX + reg.b)); - - // Draw the confidence interval according to http://www2.stat.duke.edu/~tjl13/s101/slides/unit6lec3H.pdf p.5 with t*=2 for 95% confidence - QPolygonF poly; - for (double x = minX; x <= maxX + 1; x += (maxX - minX) / 100) - poly << QPointF(xAxis->toScreen(x), - yAxis->toScreen(reg.a * x + reg.b + 2.0 * sqrt(reg.res2 / (reg.n - 2) * (1.0 / reg.n + (x - reg.xavg) * (x - reg.xavg) / (reg.n - 1) * (reg.n -2) / reg.sx2)))); - for (double x = maxX; x >= minX - 1; x -= (maxX - minX) / 100) - poly << QPointF(xAxis->toScreen(x), - yAxis->toScreen(reg.a * x + reg.b - 2.0 * sqrt(reg.res2 / (reg.n - 2) * (1.0 / reg.n + (x - reg.xavg) * (x - reg.xavg) / (reg.n - 1) * (reg.n -2) / reg.sx2)))); - QRectF box(QPoint(xAxis->toScreen(minX), yAxis->toScreen(minY)), QPoint(xAxis->toScreen(maxX), yAxis->toScreen(maxY))); - - item->setPolygon(poly.intersected(box)); - central->setPolygon(line.intersected(box)); -} - void StatsView::addHistogramMarker(double pos, QColor color, bool isHorizontal, StatsAxis *xAxis, StatsAxis *yAxis) { histogramMarkers.push_back(createChartItem<HistogramMarker>(pos, isHorizontal, color, xAxis, yAxis)); } -void StatsView::addLinearRegression(const struct regression_data reg, StatsAxis *xAxis, StatsAxis *yAxis) -{ - QColor red = QColor(Qt::red); - red.setAlphaF(reg.r2); - QPen pen(red); - QBrush brush(red); - brush.setStyle(Qt::SolidPattern); - - regressionLines.emplace_back(reg, brush, &scene, xAxis, yAxis); -} - // Yikes, we get our data in different kinds of (bin, value) pairs. // To create a category axis from this, we have to templatify the function. template<typename T> @@ -1194,5 +1145,5 @@ void StatsView::plotScatter(const std::vector<dive *> &dives, const StatsVariabl // y = ax + b struct regression_data reg = linear_regression(points); if (!std::isnan(reg.a)) - addLinearRegression(reg, xAxis, yAxis); + regressionItem = createChartItem<RegressionItem>(reg, xAxis, yAxis); } diff --git a/stats/statsview.h b/stats/statsview.h index f472e1e16..b0a199200 100644 --- a/stats/statsview.h +++ b/stats/statsview.h @@ -9,7 +9,6 @@ #include <QImage> #include <QPainter> #include <QQuickItem> -#include <QGraphicsPolygonItem> struct dive; struct StatsBinner; @@ -17,7 +16,6 @@ struct StatsBin; struct StatsState; struct StatsVariable; -class QGraphicsLineItem; class QGraphicsSimpleTextItem; class StatsSeries; class CategoryAxis; @@ -26,6 +24,7 @@ class CountAxis; class HistogramAxis; class HistogramMarker; class QuartileMarker; +class RegressionItem; class StatsAxis; class StatsGrid; class Legend; @@ -36,13 +35,6 @@ enum class ChartSubType : int; enum class ChartZValue : int; enum class StatsOperation : int; -struct regression_data { - double a,b; - double res2, r2, sx2, xavg; - int n; -}; - - class StatsView : public QQuickItem { Q_OBJECT public: @@ -127,17 +119,6 @@ private: // Helper functions to add feature to the chart void addLineMarker(double pos, double low, double high, const QPen &pen, bool isHorizontal); - // A regression line - struct RegressionLine { - std::unique_ptr<QGraphicsPolygonItem> item; - std::unique_ptr<QGraphicsPolygonItem> central; - StatsAxis *xAxis, *yAxis; - const struct regression_data reg; - void updatePosition(); - RegressionLine(const struct regression_data reg, QBrush brush, QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis); - }; - - void addLinearRegression(const struct regression_data reg, StatsAxis *xAxis, StatsAxis *yAxis); void addHistogramMarker(double pos, QColor color, bool isHorizontal, StatsAxis *xAxis, StatsAxis *yAxis); StatsState state; @@ -147,9 +128,9 @@ private: std::vector<std::unique_ptr<StatsSeries>> series; std::unique_ptr<Legend> legend; std::vector<std::unique_ptr<QuartileMarker>> quartileMarkers; - std::vector<RegressionLine> regressionLines; std::vector<std::unique_ptr<HistogramMarker>> histogramMarkers; std::unique_ptr<QGraphicsSimpleTextItem> title; + std::unique_ptr<RegressionItem> regressionItem; StatsSeries *highlightedSeries; StatsAxis *xAxis, *yAxis; Legend *draggedItem; |