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>
This commit is contained in:
Berthold Stoeger 2021-01-15 18:39:14 +01:00 committed by bstoeger
parent faf3e7079d
commit 2008857660
6 changed files with 124 additions and 75 deletions

View file

@ -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 \

View file

@ -24,6 +24,8 @@ set(SUBSURFACE_STATS_SRCS
pieseries.cpp
quartilemarker.h
quartilemarker.cpp
regressionitem.h
regressionitem.cpp
scatterseries.h
scatterseries.cpp
statsaxis.h

87
stats/regressionitem.cpp Normal file
View file

@ -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);
}

26
stats/regressionitem.h Normal file
View file

@ -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

View file

@ -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);
}

View file

@ -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;