mirror of
https://github.com/subsurface/subsurface.git
synced 2024-11-28 05:00:20 +00:00
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:
parent
faf3e7079d
commit
2008857660
6 changed files with 124 additions and 75 deletions
|
@ -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 \
|
||||
|
|
|
@ -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
87
stats/regressionitem.cpp
Normal 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
26
stats/regressionitem.h
Normal 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
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue