summaryrefslogtreecommitdiffstats
path: root/stats
diff options
context:
space:
mode:
Diffstat (limited to 'stats')
-rw-r--r--stats/CMakeLists.txt9
-rw-r--r--stats/barseries.cpp132
-rw-r--r--stats/barseries.h32
-rw-r--r--stats/boxseries.cpp80
-rw-r--r--stats/boxseries.h18
-rw-r--r--stats/chartitem.cpp492
-rw-r--r--stats/chartitem.h222
-rw-r--r--stats/histogrammarker.cpp29
-rw-r--r--stats/histogrammarker.h21
-rw-r--r--stats/informationbox.cpp68
-rw-r--r--stats/informationbox.h10
-rw-r--r--stats/legend.cpp124
-rw-r--r--stats/legend.h24
-rw-r--r--stats/pieseries.cpp97
-rw-r--r--stats/pieseries.h26
-rw-r--r--stats/quartilemarker.cpp30
-rw-r--r--stats/quartilemarker.h20
-rw-r--r--stats/regressionitem.cpp102
-rw-r--r--stats/regressionitem.h28
-rw-r--r--stats/scatterseries.cpp86
-rw-r--r--stats/scatterseries.h13
-rw-r--r--stats/statsaxis.cpp209
-rw-r--r--stats/statsaxis.h40
-rw-r--r--stats/statscolors.h8
-rw-r--r--stats/statsgrid.cpp26
-rw-r--r--stats/statsgrid.h13
-rw-r--r--stats/statshelper.h135
-rw-r--r--stats/statsicons.qrc15
-rw-r--r--stats/statsseries.cpp4
-rw-r--r--stats/statsseries.h6
-rw-r--r--stats/statsstate.cpp38
-rw-r--r--stats/statsstate.h2
-rw-r--r--stats/statsview.cpp526
-rw-r--r--stats/statsview.h139
-rw-r--r--stats/zvalues.h19
35 files changed, 1982 insertions, 861 deletions
diff --git a/stats/CMakeLists.txt b/stats/CMakeLists.txt
index a084dd0b5..daeb22146 100644
--- a/stats/CMakeLists.txt
+++ b/stats/CMakeLists.txt
@@ -9,14 +9,23 @@ set(SUBSURFACE_STATS_SRCS
barseries.cpp
boxseries.h
boxseries.cpp
+ chartitem.h
+ chartitem.cpp
chartlistmodel.h
chartlistmodel.cpp
+ histogrammarker.h
+ histogrammarker.cpp
+ chartlistmodel.cpp
informationbox.h
informationbox.cpp
legend.h
legend.cpp
pieseries.h
pieseries.cpp
+ quartilemarker.h
+ quartilemarker.cpp
+ regressionitem.h
+ regressionitem.cpp
scatterseries.h
scatterseries.cpp
statsaxis.h
diff --git a/stats/barseries.cpp b/stats/barseries.cpp
index 49170bf80..766843703 100644
--- a/stats/barseries.cpp
+++ b/stats/barseries.cpp
@@ -4,6 +4,7 @@
#include "statscolors.h"
#include "statshelper.h"
#include "statstranslations.h"
+#include "statsview.h"
#include "zvalues.h"
#include <math.h> // for lrint()
@@ -12,6 +13,7 @@
// Constants that control the bar layout
static const double barWidth = 0.8; // 1.0 = full width of category
static const double subBarWidth = 0.9; // For grouped bar charts
+static const double barBorderWidth = 1.0;
// Default constructor: invalid index.
BarSeries::Index::Index() : bar(-1), subitem(-1)
@@ -27,19 +29,19 @@ bool BarSeries::Index::operator==(const Index &i2) const
return std::tie(bar, subitem) == std::tie(i2.bar, i2.subitem);
}
-BarSeries::BarSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
+BarSeries::BarSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,
bool horizontal, bool stacked, const QString &categoryName,
const StatsVariable *valueVariable, std::vector<QString> valueBinNames) :
- StatsSeries(scene, xAxis, yAxis),
+ StatsSeries(view, xAxis, yAxis),
horizontal(horizontal), stacked(stacked), categoryName(categoryName),
valueVariable(valueVariable), valueBinNames(std::move(valueBinNames))
{
}
-BarSeries::BarSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
+BarSeries::BarSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,
bool horizontal, const QString &categoryName,
const std::vector<CountItem> &items) :
- BarSeries(scene, xAxis, yAxis, horizontal, false, categoryName, nullptr, std::vector<QString>())
+ BarSeries(view, xAxis, yAxis, horizontal, false, categoryName, nullptr, std::vector<QString>())
{
for (const CountItem &item: items) {
StatsOperationResults res;
@@ -50,10 +52,10 @@ BarSeries::BarSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
}
}
-BarSeries::BarSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
+BarSeries::BarSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,
bool horizontal, const QString &categoryName, const StatsVariable *valueVariable,
const std::vector<ValueItem> &items) :
- BarSeries(scene, xAxis, yAxis, horizontal, false, categoryName, valueVariable, std::vector<QString>())
+ BarSeries(view, xAxis, yAxis, horizontal, false, categoryName, valueVariable, std::vector<QString>())
{
for (const ValueItem &item: items) {
add_item(item.lowerBound, item.upperBound, makeSubItems(item.value, item.label),
@@ -61,11 +63,11 @@ BarSeries::BarSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
}
}
-BarSeries::BarSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
+BarSeries::BarSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,
bool horizontal, bool stacked, const QString &categoryName, const StatsVariable *valueVariable,
std::vector<QString> valueBinNames,
const std::vector<MultiItem> &items) :
- BarSeries(scene, xAxis, yAxis, horizontal, stacked, categoryName, valueVariable, std::move(valueBinNames))
+ BarSeries(view, xAxis, yAxis, horizontal, stacked, categoryName, valueVariable, std::move(valueBinNames))
{
for (const MultiItem &item: items) {
StatsOperationResults res;
@@ -85,97 +87,79 @@ BarSeries::~BarSeries()
{
}
-BarSeries::BarLabel::BarLabel(QGraphicsScene *scene, const std::vector<QString> &labels, int bin_nr, int binCount) :
- totalWidth(0.0), totalHeight(0.0), isOutside(false)
+BarSeries::BarLabel::BarLabel(StatsView &view, const std::vector<QString> &labels, int bin_nr, int binCount) :
+ isOutside(false)
{
- items.reserve(labels.size());
- for (const QString &label: labels) {
- items.emplace_back(createItem<QGraphicsSimpleTextItem>(scene));
- items.back()->setText(label);
- items.back()->setZValue(ZValues::seriesLabels);
- QRectF rect = items.back()->boundingRect();
- if (rect.width() > totalWidth)
- totalWidth = rect.width();
- totalHeight += rect.height();
- }
- highlight(false, bin_nr, binCount);
+ QFont f; // make configurable
+ item = view.createChartItem<ChartTextItem>(ChartZValue::SeriesLabels, f, labels, true);
+ //highlight(false, bin_nr, binCount);
}
void BarSeries::BarLabel::setVisible(bool visible)
{
- for (auto &item: items)
- item->setVisible(visible);
+ item->setVisible(visible);
}
-void BarSeries::BarLabel::highlight(bool highlight, int bin_nr, int binCount)
+void BarSeries::BarLabel::highlight(bool highlight, int bin_nr, int binCount, const QColor &background)
{
- QBrush brush(highlight || isOutside ? darkLabelColor : labelColor(bin_nr, binCount));
- for (auto &item: items)
- item->setBrush(brush);
+ // For labels that are on top of a bar, use the corresponding bar color
+ // as background. Rendering on a transparent background gives ugly artifacts.
+ item->setColor(highlight || isOutside ? darkLabelColor : labelColor(bin_nr, binCount),
+ isOutside ? Qt::transparent : background);
}
void BarSeries::BarLabel::updatePosition(bool horizontal, bool center, const QRectF &rect,
- int bin_nr, int binCount)
+ int bin_nr, int binCount, const QColor &background)
{
+ QSizeF itemSize = item->getRect().size();
if (!horizontal) {
- if (totalWidth > rect.width()) {
+ if (itemSize.width() > rect.width()) {
setVisible(false);
return;
}
QPointF pos = rect.center();
+ pos.rx() -= round(itemSize.width() / 2.0);
// Heuristics: if the label fits nicely into the bar (bar height is at least twice the label height),
// then put the label in the middle of the bar. Otherwise, put it at the top of the bar.
- isOutside = !center && rect.height() < 2.0 * totalHeight;
+ isOutside = !center && rect.height() < 2.0 * itemSize.height();
if (isOutside) {
- pos.ry() = rect.top() - (totalHeight + 2.0); // Leave two pixels(?) space
+ pos.ry() = rect.top() - (itemSize.height() + 2.0); // Leave two pixels(?) space
} else {
- if (totalHeight > rect.height()) {
+ if (itemSize.height() > rect.height()) {
setVisible(false);
return;
}
- pos.ry() -= totalHeight / 2.0;
- }
- for (auto &it: items) {
- QPointF itemPos = pos;
- QRectF rect = it->boundingRect();
- itemPos.rx() -= rect.width() / 2.0;
- it->setPos(itemPos);
- pos.ry() += rect.height();
+ pos.ry() -= round(itemSize.height() / 2.0);
}
+ item->setPos(pos);
} else {
- if (totalHeight > rect.height()) {
+ if (itemSize.height() > rect.height()) {
setVisible(false);
return;
}
QPointF pos = rect.center();
- pos.ry() -= totalHeight / 2.0;
+ pos.ry() -= round(itemSize.height() / 2.0);
// Heuristics: if the label fits nicely into the bar (bar width is at least twice the label height),
// then put the label in the middle of the bar. Otherwise, put it to the right of the bar.
- isOutside = !center && rect.width() < 2.0 * totalWidth;
+ isOutside = !center && rect.width() < 2.0 * itemSize.width();
if (isOutside) {
- pos.rx() = rect.right() + (totalWidth / 2.0 + 2.0); // Leave two pixels(?) space
+ pos.rx() = round(rect.right() + 2.0); // Leave two pixels(?) space
} else {
- if (totalWidth > rect.width()) {
+ if (itemSize.width() > rect.width()) {
setVisible(false);
return;
}
}
- for (auto &it: items) {
- QPointF itemPos = pos;
- QRectF rect = it->boundingRect();
- itemPos.rx() -= rect.width() / 2.0;
- it->setPos(itemPos);
- pos.ry() += rect.height();
- }
+ item->setPos(pos);
}
setVisible(true);
// If label changed from inside to outside, or vice-versa, the color might change.
- highlight(false, bin_nr, binCount);
+ highlight(false, bin_nr, binCount, background);
}
-BarSeries::Item::Item(QGraphicsScene *scene, BarSeries *series, double lowerBound, double upperBound,
+BarSeries::Item::Item(BarSeries *series, double lowerBound, double upperBound,
std::vector<SubItem> subitemsIn,
const QString &binName, const StatsOperationResults &res, int total,
bool horizontal, bool stacked, int binCount) :
@@ -186,10 +170,8 @@ BarSeries::Item::Item(QGraphicsScene *scene, BarSeries *series, double lowerBoun
res(res),
total(total)
{
- for (SubItem &item: subitems) {
- item.item->setZValue(ZValues::series);
+ for (SubItem &item: subitems)
item.highlight(false, binCount);
- }
updatePosition(series, horizontal, stacked, binCount);
}
@@ -202,15 +184,11 @@ void BarSeries::Item::highlight(int subitem, bool highlight, int binCount)
void BarSeries::SubItem::highlight(bool highlight, int binCount)
{
- if (highlight) {
- item->setBrush(QBrush(highlightedColor));
- item->setPen(QPen(highlightedBorderColor));
- } else {
- item->setBrush(QBrush(binColor(bin_nr, binCount)));
- item->setPen(QPen(::borderColor));
- }
+ fill = highlight ? highlightedColor : binColor(bin_nr, binCount);
+ QColor border = highlight ? highlightedBorderColor : ::borderColor;
+ item->setColor(fill, border);
if (label)
- label->highlight(highlight, bin_nr, binCount);
+ label->highlight(highlight, bin_nr, binCount, fill);
}
void BarSeries::Item::updatePosition(BarSeries *series, bool horizontal, bool stacked, int binCount)
@@ -234,9 +212,9 @@ void BarSeries::Item::updatePosition(BarSeries *series, bool horizontal, bool st
double center = (idx + 0.5) * fullSubWidth + from;
item.updatePosition(series, horizontal, stacked, center - subWidth / 2.0, center + subWidth / 2.0, binCount);
}
- rect = subitems[0].item->rect();
+ rect = subitems[0].item->getRect();
for (auto it = std::next(subitems.begin()); it != subitems.end(); ++it)
- rect = rect.united(it->item->rect());
+ rect = rect.united(it->item->getRect());
}
void BarSeries::SubItem::updatePosition(BarSeries *series, bool horizontal, bool stacked,
@@ -253,7 +231,7 @@ void BarSeries::SubItem::updatePosition(BarSeries *series, bool horizontal, bool
QRectF rect(topLeft, bottomRight);
item->setRect(rect);
if (label)
- label->updatePosition(horizontal, stacked, rect, bin_nr, binCount);
+ label->updatePosition(horizontal, stacked, rect, bin_nr, binCount, fill);
}
std::vector<BarSeries::SubItem> BarSeries::makeSubItems(const std::vector<std::pair<double, std::vector<QString>>> &values) const
@@ -264,9 +242,10 @@ std::vector<BarSeries::SubItem> BarSeries::makeSubItems(const std::vector<std::p
int bin_nr = 0;
for (auto &[v, label]: values) {
if (v > 0.0) {
- res.push_back({ createItemPtr<QGraphicsRectItem>(scene), {}, from, from + v, bin_nr });
+ res.push_back({ view.createChartItem<ChartBarItem>(ChartZValue::Series, barBorderWidth, horizontal),
+ {}, from, from + v, bin_nr });
if (!label.empty())
- res.back().label = std::make_unique<BarLabel>(scene, label, bin_nr, binCount());
+ res.back().label = std::make_unique<BarLabel>(view, label, bin_nr, binCount());
}
if (stacked)
from += v;
@@ -292,7 +271,7 @@ void BarSeries::add_item(double lowerBound, double upperBound, std::vector<SubIt
// Don't add empty items, as that messes with the "find item under mouse" routine.
if (subitems.empty())
return;
- items.emplace_back(scene, this, lowerBound, upperBound, std::move(subitems), binName, res,
+ items.emplace_back(this, lowerBound, upperBound, std::move(subitems), binName, res,
total, horizontal, stacked, binCount());
}
@@ -322,10 +301,10 @@ int BarSeries::Item::getSubItemUnderMouse(const QPointF &point, bool horizontal,
// Search the first item whose "end" position is greater than the cursor position.
bool search_x = horizontal == stacked;
auto it = search_x ? std::lower_bound(subitems.begin(), subitems.end(), point.x(),
- [] (const SubItem &item, double x) { return item.item->rect().right() < x; })
+ [] (const SubItem &item, double x) { return item.item->getRect().right() < x; })
: std::lower_bound(subitems.begin(), subitems.end(), point.y(),
- [] (const SubItem &item, double y) { return item.item->rect().top() > y; });
- return it != subitems.end() && it->item->rect().contains(point) ? it - subitems.begin() : -1;
+ [] (const SubItem &item, double y) { return item.item->getRect().top() > y; });
+ return it != subitems.end() && it->item->getRect().contains(point) ? it - subitems.begin() : -1;
}
// Format information in a count-based bar chart.
@@ -403,10 +382,11 @@ bool BarSeries::hover(QPointF pos)
Item &item = items[highlighted.bar];
item.highlight(index.subitem, true, binCount());
if (!information)
- information = createItemPtr<InformationBox>(scene);
+ information = view.createChartItem<InformationBox>();
information->setText(makeInfo(item, highlighted.subitem), pos);
+ information->setVisible(true);
} else {
- information.reset();
+ information->setVisible(false);
}
return highlighted.bar >= 0;
diff --git a/stats/barseries.h b/stats/barseries.h
index 0c8c34ffd..9f9586fe8 100644
--- a/stats/barseries.h
+++ b/stats/barseries.h
@@ -5,14 +5,17 @@
#ifndef BAR_SERIES_H
#define BAR_SERIES_H
+#include "statshelper.h"
#include "statsseries.h"
#include "statsvariables.h"
#include <memory>
#include <vector>
-#include <QGraphicsRectItem>
+#include <QColor>
+#include <QRectF>
-class QGraphicsScene;
+class ChartBarItem;
+class ChartTextItem;
struct InformationBox;
struct StatsVariable;
@@ -47,13 +50,13 @@ public:
// Note: this expects that all items are added with increasing pos
// and that no bar is inside another bar, i.e. lowerBound and upperBound
// are ordered identically.
- BarSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
+ BarSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,
bool horizontal, const QString &categoryName,
const std::vector<CountItem> &items);
- BarSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
+ BarSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,
bool horizontal, const QString &categoryName, const StatsVariable *valueVariable,
const std::vector<ValueItem> &items);
- BarSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
+ BarSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,
bool horizontal, bool stacked, const QString &categoryName, const StatsVariable *valueVariable,
std::vector<QString> valueBinNames,
const std::vector<MultiItem> &items);
@@ -63,7 +66,7 @@ public:
bool hover(QPointF pos) override;
void unhighlight() override;
private:
- BarSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
+ BarSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,
bool horizontal, bool stacked, const QString &categoryName, const StatsVariable *valueVariable,
std::vector<QString> valueBinNames);
@@ -80,21 +83,21 @@ private:
// A label that is composed of multiple lines
struct BarLabel {
- std::vector<std::unique_ptr<QGraphicsSimpleTextItem>> items;
- double totalWidth, totalHeight; // Size of the item
+ ChartItemPtr<ChartTextItem> item;
bool isOutside; // Is shown outside of bar
- BarLabel(QGraphicsScene *scene, const std::vector<QString> &labels, int bin_nr, int binCount);
+ BarLabel(StatsView &view, const std::vector<QString> &labels, int bin_nr, int binCount);
void setVisible(bool visible);
- void updatePosition(bool horizontal, bool center, const QRectF &rect, int bin_nr, int binCount);
- void highlight(bool highlight, int bin_nr, int binCount);
+ void updatePosition(bool horizontal, bool center, const QRectF &rect, int bin_nr, int binCount, const QColor &background);
+ void highlight(bool highlight, int bin_nr, int binCount, const QColor &background);
};
struct SubItem {
- std::unique_ptr<QGraphicsRectItem> item;
+ ChartItemPtr<ChartBarItem> item;
std::unique_ptr<BarLabel> label;
double value_from;
double value_to;
int bin_nr;
+ QColor fill;
void updatePosition(BarSeries *series, bool horizontal, bool stacked,
double from, double to, int binCount);
void highlight(bool highlight, int binCount);
@@ -107,7 +110,7 @@ private:
const QString binName;
StatsOperationResults res;
int total;
- Item(QGraphicsScene *scene, BarSeries *series, double lowerBound, double upperBound,
+ Item(BarSeries *series, double lowerBound, double upperBound,
std::vector<SubItem> subitems,
const QString &binName, const StatsOperationResults &res, int total, bool horizontal,
bool stacked, int binCount);
@@ -116,9 +119,8 @@ private:
int getSubItemUnderMouse(const QPointF &f, bool horizontal, bool stacked) const;
};
- std::unique_ptr<InformationBox> information;
+ ChartItemPtr<InformationBox> information;
std::vector<Item> items;
- std::vector<BarLabel> barLabels;
bool horizontal;
bool stacked;
QString categoryName;
diff --git a/stats/boxseries.cpp b/stats/boxseries.cpp
index 08a421205..c4f34dbb7 100644
--- a/stats/boxseries.cpp
+++ b/stats/boxseries.cpp
@@ -1,20 +1,22 @@
// SPDX-License-Identifier: GPL-2.0
#include "boxseries.h"
#include "informationbox.h"
+#include "statsaxis.h"
#include "statscolors.h"
#include "statshelper.h"
#include "statstranslations.h"
+#include "statsview.h"
#include "zvalues.h"
#include <QLocale>
// 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, StatsAxis *xAxis, StatsAxis *yAxis,
+BoxSeries::BoxSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,
const QString &variable, const QString &unit, int decimals) :
- StatsSeries(scene, xAxis, yAxis),
+ StatsSeries(view, xAxis, yAxis),
variable(variable), unit(unit), decimals(decimals), highlighted(-1)
{
}
@@ -23,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(&center);
+ item = view.createChartItem<ChartBoxItem>(ChartZValue::Series, boxBorderWidth);
highlight(false);
updatePosition(series);
}
@@ -50,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()
@@ -105,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)
@@ -149,10 +126,11 @@ bool BoxSeries::hover(QPointF pos)
Item &item = *items[highlighted];
item.highlight(true);
if (!information)
- information = createItemPtr<InformationBox>(scene);
+ information = view.createChartItem<InformationBox>();
information->setText(formatInformation(item), pos);
+ information->setVisible(true);
} else {
- information.reset();
+ information->setVisible(false);
}
return highlighted >= 0;
}
diff --git a/stats/boxseries.h b/stats/boxseries.h
index dde9014f6..ce48397ea 100644
--- a/stats/boxseries.h
+++ b/stats/boxseries.h
@@ -5,20 +5,18 @@
#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;
class BoxSeries : public StatsSeries {
public:
- BoxSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
+ BoxSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,
const QString &variable, const QString &unit, int decimals);
~BoxSeries();
@@ -36,16 +34,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();
+ ChartItemPtr<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);
};
@@ -54,7 +48,7 @@ private:
int decimals;
std::vector<QString> formatInformation(const Item &item) const;
- std::unique_ptr<InformationBox> information;
+ ChartItemPtr<InformationBox> information;
std::vector<std::unique_ptr<Item>> items;
int highlighted; // -1: no item highlighted
};
diff --git a/stats/chartitem.cpp b/stats/chartitem.cpp
new file mode 100644
index 000000000..c8bdd130e
--- /dev/null
+++ b/stats/chartitem.cpp
@@ -0,0 +1,492 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "chartitem.h"
+#include "statscolors.h"
+#include "statsview.h"
+
+#include <cmath>
+#include <QQuickWindow>
+#include <QSGFlatColorMaterial>
+#include <QSGImageNode>
+#include <QSGRectangleNode>
+#include <QSGTexture>
+
+static int round_up(double f)
+{
+ return static_cast<int>(ceil(f));
+}
+
+ChartItem::ChartItem(StatsView &v, ChartZValue z) :
+ dirty(false), prev(nullptr), next(nullptr),
+ zValue(z), view(v)
+{
+ // Register before the derived constructors run, so that the
+ // derived classes can mark the item as dirty in the constructor.
+ v.registerChartItem(*this);
+}
+
+ChartItem::~ChartItem()
+{
+}
+
+QSizeF ChartItem::sceneSize() const
+{
+ return view.size();
+}
+
+void ChartItem::markDirty()
+{
+ view.registerDirtyChartItem(*this);
+}
+
+ChartPixmapItem::ChartPixmapItem(StatsView &v, ChartZValue z) : HideableChartItem(v, z),
+ positionDirty(false), textureDirty(false)
+{
+}
+
+ChartPixmapItem::~ChartPixmapItem()
+{
+ painter.reset(); // Make sure to destroy painter before image that is painted on
+}
+
+void ChartPixmapItem::setTextureDirty()
+{
+ textureDirty = true;
+ markDirty();
+}
+
+void ChartPixmapItem::setPositionDirty()
+{
+ positionDirty = true;
+ markDirty();
+}
+
+void ChartPixmapItem::render()
+{
+ if (!node) {
+ createNode(view.w()->createImageNode());
+ view.addQSGNode(node.get(), zValue);
+ }
+ updateVisible();
+
+ if (!img) {
+ resize(QSizeF(1,1));
+ img->fill(Qt::transparent);
+ }
+ if (textureDirty) {
+ texture.reset(view.w()->createTextureFromImage(*img, QQuickWindow::TextureHasAlphaChannel));
+ node->node->setTexture(texture.get());
+ textureDirty = false;
+ }
+ if (positionDirty) {
+ node->node->setRect(rect);
+ positionDirty = false;
+ }
+}
+
+void ChartPixmapItem::resize(QSizeF size)
+{
+ painter.reset();
+ img.reset(new QImage(round_up(size.width()), round_up(size.height()), QImage::Format_ARGB32));
+ painter.reset(new QPainter(img.get()));
+ painter->setRenderHint(QPainter::Antialiasing);
+ rect.setSize(size);
+ setTextureDirty();
+}
+
+void ChartPixmapItem::setPos(QPointF pos)
+{
+ rect.moveTopLeft(pos);
+ setPositionDirty();
+}
+
+QRectF ChartPixmapItem::getRect() const
+{
+ return rect;
+}
+
+static const int scatterItemDiameter = 10;
+static const int scatterItemBorder = 1;
+
+ChartScatterItem::ChartScatterItem(StatsView &v, ChartZValue z) : HideableChartItem(v, z),
+ positionDirty(false), textureDirty(false), highlighted(false)
+{
+ rect.setSize(QSizeF(static_cast<double>(scatterItemDiameter), static_cast<double>(scatterItemDiameter)));
+}
+
+ChartScatterItem::~ChartScatterItem()
+{
+}
+
+static QSGTexture *createScatterTexture(StatsView &view, const QColor &color, const QColor &borderColor)
+{
+ QImage img(scatterItemDiameter, scatterItemDiameter, QImage::Format_ARGB32);
+ img.fill(Qt::transparent);
+ QPainter painter(&img);
+ painter.setPen(Qt::NoPen);
+ painter.setRenderHint(QPainter::Antialiasing);
+ painter.setBrush(borderColor);
+ painter.drawEllipse(0, 0, scatterItemDiameter, scatterItemDiameter);
+ painter.setBrush(color);
+ painter.drawEllipse(scatterItemBorder, scatterItemBorder,
+ scatterItemDiameter - 2 * scatterItemBorder,
+ scatterItemDiameter - 2 * scatterItemBorder);
+ return view.w()->createTextureFromImage(img, QQuickWindow::TextureHasAlphaChannel);
+}
+
+// Note: Originally these were std::unique_ptrs, which automatically
+// freed the textures on exit. However, destroying textures after
+// QApplication finished its thread leads to crashes. Therefore, these
+// are now normal pointers and the texture objects are leaked.
+static QSGTexture *scatterItemTexture = nullptr;
+static QSGTexture *scatterItemHighlightedTexture = nullptr;
+
+void ChartScatterItem::render()
+{
+ if (!scatterItemTexture) {
+ scatterItemTexture = createScatterTexture(view, fillColor, borderColor);
+ scatterItemHighlightedTexture = createScatterTexture(view, highlightedColor, highlightedBorderColor);
+ }
+ if (!node) {
+ createNode(view.w()->createImageNode());
+ view.addQSGNode(node.get(), zValue);
+ textureDirty = positionDirty = true;
+ }
+ updateVisible();
+ if (textureDirty) {
+ node->node->setTexture(highlighted ? scatterItemHighlightedTexture : scatterItemTexture);
+ textureDirty = false;
+ }
+ if (positionDirty) {
+ node->node->setRect(rect);
+ positionDirty = false;
+ }
+}
+
+void ChartScatterItem::setPos(QPointF pos)
+{
+ pos -= QPointF(scatterItemDiameter / 2.0, scatterItemDiameter / 2.0);
+ rect.moveTopLeft(pos);
+ positionDirty = true;
+ markDirty();
+}
+
+static double squareDist(const QPointF &p1, const QPointF &p2)
+{
+ QPointF diff = p1 - p2;
+ return QPointF::dotProduct(diff, diff);
+}
+
+bool ChartScatterItem::contains(QPointF point) const
+{
+ return squareDist(point, rect.center()) <= (scatterItemDiameter / 2.0) * (scatterItemDiameter / 2.0);
+}
+
+void ChartScatterItem::setHighlight(bool highlightedIn)
+{
+ if (highlighted == highlightedIn)
+ return;
+ highlighted = highlightedIn;
+ textureDirty = true;
+ markDirty();
+}
+
+QRectF ChartScatterItem::getRect() const
+{
+ return rect;
+}
+
+ChartRectItem::ChartRectItem(StatsView &v, ChartZValue z,
+ const QPen &pen, const QBrush &brush, double radius) : ChartPixmapItem(v, z),
+ pen(pen), brush(brush), radius(radius)
+{
+}
+
+ChartRectItem::~ChartRectItem()
+{
+}
+
+void ChartRectItem::resize(QSizeF size)
+{
+ ChartPixmapItem::resize(size);
+ img->fill(Qt::transparent);
+ painter->setPen(pen);
+ painter->setBrush(brush);
+ QSize imgSize = img->size();
+ int width = pen.width();
+ QRect rect(width / 2, width / 2, imgSize.width() - width, imgSize.height() - width);
+ painter->drawRoundedRect(rect, radius, radius, Qt::AbsoluteSize);
+}
+
+ChartTextItem::ChartTextItem(StatsView &v, ChartZValue z, const QFont &f, const std::vector<QString> &text, bool center) :
+ ChartPixmapItem(v, z), f(f), center(center)
+{
+ QFontMetrics fm(f);
+ double totalWidth = 1.0;
+ fontHeight = static_cast<double>(fm.height());
+ double totalHeight = std::max(1.0, static_cast<double>(text.size()) * fontHeight);
+
+ items.reserve(text.size());
+ for (const QString &s: text) {
+ double w = fm.size(Qt::TextSingleLine, s).width();
+ items.push_back({ s, w });
+ if (w > totalWidth)
+ totalWidth = w;
+ }
+ resize(QSizeF(totalWidth, totalHeight));
+}
+
+ChartTextItem::ChartTextItem(StatsView &v, ChartZValue z, const QFont &f, const QString &text) :
+ ChartTextItem(v, z, f, std::vector<QString>({ text }), true)
+{
+}
+
+void ChartTextItem::setColor(const QColor &c)
+{
+ setColor(c, Qt::transparent);
+}
+
+void ChartTextItem::setColor(const QColor &c, const QColor &background)
+{
+ img->fill(background);
+ double y = 0.0;
+ painter->setPen(QPen(c));
+ painter->setFont(f);
+ double totalWidth = getRect().width();
+ for (const auto &[s, w]: items) {
+ double x = center ? round((totalWidth - w) / 2.0) : 0.0;
+ QRectF rect(x, y, w, fontHeight);
+ painter->drawText(rect, s);
+ y += fontHeight;
+ }
+ setTextureDirty();
+}
+
+ChartPieItem::ChartPieItem(StatsView &v, ChartZValue z, double borderWidth) : ChartPixmapItem(v, z),
+ borderWidth(borderWidth)
+{
+}
+
+void ChartPieItem::drawSegment(double from, double to, QColor fill, QColor border)
+{
+ painter->setPen(QPen(border, borderWidth));
+ painter->setBrush(QBrush(fill));
+ // For whatever obscure reason, angles of pie pieces are given as 16th of a degree...?
+ // Angles increase CCW, whereas pie charts usually are read CW. Therfore, startAngle
+ // is dervied from "from" and subtracted from the origin angle at 12:00.
+ int startAngle = 90 * 16 - static_cast<int>(round(to * 360.0 * 16.0));
+ int spanAngle = static_cast<int>(round((to - from) * 360.0 * 16.0));
+ QRectF drawRect(QPointF(0.0, 0.0), rect.size());
+ painter->drawPie(drawRect, startAngle, spanAngle);
+ setTextureDirty();
+}
+
+void ChartPieItem::resize(QSizeF size)
+{
+ ChartPixmapItem::resize(size);
+ img->fill(Qt::transparent);
+}
+
+ChartLineItem::ChartLineItem(StatsView &v, ChartZValue z, QColor color, double width) : HideableChartItem(v, z),
+ color(color), width(width), positionDirty(false), materialDirty(false)
+{
+}
+
+ChartLineItem::~ChartLineItem()
+{
+}
+
+// Helper function to set points
+void setPoint(QSGGeometry::Point2D &v, const QPointF &p)
+{
+ v.set(static_cast<float>(p.x()), static_cast<float>(p.y()));
+}
+
+void ChartLineItem::render()
+{
+ if (!node) {
+ geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2));
+ geometry->setDrawingMode(QSGGeometry::DrawLines);
+ material.reset(new QSGFlatColorMaterial);
+ createNode();
+ node->setGeometry(geometry.get());
+ node->setMaterial(material.get());
+ view.addQSGNode(node.get(), zValue);
+ positionDirty = materialDirty = true;
+ }
+ updateVisible();
+
+ if (positionDirty) {
+ // Attention: width is a geometry property and therefore handled by position dirty!
+ geometry->setLineWidth(static_cast<float>(width));
+ auto vertices = geometry->vertexDataAsPoint2D();
+ setPoint(vertices[0], from);
+ setPoint(vertices[1], to);
+ node->markDirty(QSGNode::DirtyGeometry);
+ }
+
+ if (materialDirty) {
+ material->setColor(color);
+ node->markDirty(QSGNode::DirtyMaterial);
+ }
+
+ positionDirty = materialDirty = false;
+}
+
+void ChartLineItem::setLine(QPointF fromIn, QPointF toIn)
+{
+ from = fromIn;
+ to = toIn;
+ positionDirty = true;
+ markDirty();
+}
+
+ChartBarItem::ChartBarItem(StatsView &v, ChartZValue z, double borderWidth, bool horizontal) : HideableChartItem(v, z),
+ borderWidth(borderWidth), horizontal(horizontal),
+ positionDirty(false), colorDirty(false)
+{
+}
+
+ChartBarItem::~ChartBarItem()
+{
+}
+
+void ChartBarItem::render()
+{
+ if (!node) {
+ createNode(view.w()->createRectangleNode());
+
+ borderGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4));
+ borderGeometry->setDrawingMode(QSGGeometry::DrawLineLoop);
+ borderGeometry->setLineWidth(static_cast<float>(borderWidth));
+ borderMaterial.reset(new QSGFlatColorMaterial);
+ borderNode.reset(new QSGGeometryNode);
+ borderNode->setGeometry(borderGeometry.get());
+ borderNode->setMaterial(borderMaterial.get());
+
+ node->node->appendChildNode(borderNode.get());
+ view.addQSGNode(node.get(), zValue);
+ positionDirty = colorDirty = true;
+ }
+ updateVisible();
+
+ if (colorDirty) {
+ node->node->setColor(color);
+ borderMaterial->setColor(borderColor);
+ node->node->markDirty(QSGNode::DirtyMaterial);
+ borderNode->markDirty(QSGNode::DirtyMaterial);
+ }
+
+ if (positionDirty) {
+ node->node->setRect(rect);
+ auto vertices = borderGeometry->vertexDataAsPoint2D();
+ if (horizontal) {
+ setPoint(vertices[0], rect.topLeft());
+ setPoint(vertices[1], rect.topRight());
+ setPoint(vertices[2], rect.bottomRight());
+ setPoint(vertices[3], rect.bottomLeft());
+ } else {
+ setPoint(vertices[0], rect.bottomLeft());
+ setPoint(vertices[1], rect.topLeft());
+ setPoint(vertices[2], rect.topRight());
+ setPoint(vertices[3], rect.bottomRight());
+ }
+ node->node->markDirty(QSGNode::DirtyGeometry);
+ borderNode->markDirty(QSGNode::DirtyGeometry);
+ }
+
+ positionDirty = colorDirty = false;
+}
+
+void ChartBarItem::setColor(QColor colorIn, QColor borderColorIn)
+{
+ color = colorIn;
+ borderColor = borderColorIn;
+ colorDirty = true;
+ markDirty();
+}
+
+void ChartBarItem::setRect(const QRectF &rectIn)
+{
+ rect = rectIn;
+ positionDirty = true;
+ markDirty();
+}
+
+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
new file mode 100644
index 000000000..cf20f55a8
--- /dev/null
+++ b/stats/chartitem.h
@@ -0,0 +1,222 @@
+// SPDX-License-Identifier: GPL-2.0
+// Wrappers around QSGImageNode that allow painting onto an image
+// and then turning that into a texture to be displayed in a QQuickItem.
+#ifndef CHART_ITEM_H
+#define CHART_ITEM_H
+
+#include "statshelper.h"
+
+#include <memory>
+#include <QPainter>
+
+class QSGGeometry;
+class QSGGeometryNode;
+class QSGFlatColorMaterial;
+class QSGImageNode;
+class QSGRectangleNode;
+class QSGTexture;
+class StatsView;
+enum class ChartZValue : int;
+
+class ChartItem {
+public:
+ virtual void render() = 0; // Only call on render thread!
+ bool dirty; // If true, call render() when rebuilding the scene
+ ChartItem *prev, *next; // Double linked list of items
+ const ChartZValue zValue;
+ virtual ~ChartItem(); // Attention: must only be called by render thread.
+protected:
+ ChartItem(StatsView &v, ChartZValue z);
+ QSizeF sceneSize() const;
+ StatsView &view;
+ void markDirty();
+};
+
+template <typename Node>
+class HideableChartItem : public ChartItem {
+protected:
+ HideableChartItem(StatsView &v, ChartZValue z);
+ std::unique_ptr<Node> node;
+ bool visible;
+ bool visibleChanged;
+ template<class... Args>
+ void createNode(Args&&... args); // Call to create node with visibility flag.
+ void updateVisible(); // Must be called by child class to update visibility flag!
+public:
+ void setVisible(bool visible);
+};
+
+// A shortcut for ChartItems based on a hideable proxy item
+template <typename Node>
+using HideableChartProxyItem = HideableChartItem<HideableQSGNode<QSGProxyNode<Node>>>;
+
+// A chart item that blits a precalculated pixmap onto the scene.
+class ChartPixmapItem : public HideableChartProxyItem<QSGImageNode> {
+public:
+ ChartPixmapItem(StatsView &v, ChartZValue z);
+ ~ChartPixmapItem();
+
+ void setPos(QPointF pos);
+ void render() override; // Only call on render thread!
+ QRectF getRect() const;
+protected:
+ void resize(QSizeF size); // Resets the canvas. Attention: image is *unitialized*.
+ std::unique_ptr<QPainter> painter;
+ std::unique_ptr<QImage> img;
+ void setTextureDirty();
+ void setPositionDirty();
+ QRectF rect;
+private:
+ bool positionDirty; // true if the position changed since last render
+ bool textureDirty; // true if the pixmap changed since last render
+ std::unique_ptr<QSGTexture> texture;
+};
+
+// Draw a rectangular background after resize. Children are responsible for calling update().
+class ChartRectItem : public ChartPixmapItem {
+public:
+ ChartRectItem(StatsView &v, ChartZValue z, const QPen &pen, const QBrush &brush, double radius);
+ ~ChartRectItem();
+ void resize(QSizeF size);
+private:
+ QPen pen;
+ QBrush brush;
+ double radius;
+};
+
+// Attention: text is only drawn after calling setColor()!
+class ChartTextItem : public ChartPixmapItem {
+public:
+ ChartTextItem(StatsView &v, ChartZValue z, const QFont &f, const std::vector<QString> &text, bool center);
+ ChartTextItem(StatsView &v, ChartZValue z, const QFont &f, const QString &text);
+ void setColor(const QColor &color); // Draw on transparent background
+ void setColor(const QColor &color, const QColor &background); // Fill rectangle with given background color
+private:
+ QFont f;
+ double fontHeight;
+ bool center;
+ struct Item {
+ QString s;
+ double width;
+ };
+ std::vector<Item> items;
+};
+
+// A pie chart item: draws disk segments onto a pixmap.
+class ChartPieItem : public ChartPixmapItem {
+public:
+ ChartPieItem(StatsView &v, ChartZValue z, double borderWidth);
+ void drawSegment(double from, double to, QColor fill, QColor border); // from and to are relative (0-1 is full disk).
+ void resize(QSizeF size); // As in base class, but clears the canvas
+private:
+ double borderWidth;
+};
+
+class ChartLineItem : public HideableChartItem<HideableQSGNode<QSGGeometryNode>> {
+public:
+ ChartLineItem(StatsView &v, ChartZValue z, QColor color, double width);
+ ~ChartLineItem();
+ void setLine(QPointF from, QPointF to);
+ void render() override; // Only call on render thread!
+private:
+ QPointF from, to;
+ QColor color;
+ double width;
+ bool horizontal;
+ bool positionDirty;
+ bool materialDirty;
+ std::unique_ptr<QSGFlatColorMaterial> material;
+ std::unique_ptr<QSGGeometry> geometry;
+};
+
+// A bar in a bar chart: a rectangle bordered by lines.
+class ChartBarItem : public HideableChartProxyItem<QSGRectangleNode> {
+public:
+ ChartBarItem(StatsView &v, ChartZValue z, double borderWidth, bool horizontal);
+ ~ChartBarItem();
+ void setColor(QColor color, QColor borderColor);
+ void setRect(const QRectF &rect);
+ QRectF getRect() const;
+ void render() override; // Only call on render thread!
+protected:
+ QColor color, borderColor;
+ double borderWidth;
+ QRectF rect;
+ bool horizontal;
+ bool positionDirty;
+ bool colorDirty;
+ std::unique_ptr<QSGGeometryNode> borderNode;
+ std::unique_ptr<QSGFlatColorMaterial> borderMaterial;
+ 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;
+};
+
+// An item in a scatter chart. This is not simply a normal pixmap item,
+// because we want that all items share the *same* texture for memory
+// efficiency. It is somewhat questionable to define the form of the
+// scatter item here, but so it is for now.
+class ChartScatterItem : public HideableChartProxyItem<QSGImageNode> {
+public:
+ ChartScatterItem(StatsView &v, ChartZValue z);
+ ~ChartScatterItem();
+
+ void setPos(QPointF pos); // Specifies the *center* of the item.
+ void setHighlight(bool highlight); // In the future, support different kinds of scatter items.
+ void render() override; // Only call on render thread!
+ QRectF getRect() const;
+ bool contains(QPointF point) const;
+private:
+ QRectF rect;
+ QSizeF textureSize;
+ bool positionDirty, textureDirty;
+ bool highlighted;
+};
+
+// Implementation detail of templates - move to serparate header file
+template <typename Node>
+void HideableChartItem<Node>::setVisible(bool visibleIn)
+{
+ if (visible == visibleIn)
+ return;
+ visible = visibleIn;
+ visibleChanged = true;
+ markDirty();
+}
+
+template <typename Node>
+template<class... Args>
+void HideableChartItem<Node>::createNode(Args&&... args)
+{
+ node.reset(new Node(visible, std::forward<Args>(args)...));
+ visibleChanged = false;
+}
+
+template <typename Node>
+HideableChartItem<Node>::HideableChartItem(StatsView &v, ChartZValue z) : ChartItem(v, z),
+ visible(true), visibleChanged(false)
+{
+}
+
+template <typename Node>
+void HideableChartItem<Node>::updateVisible()
+{
+ if (visibleChanged)
+ node->setVisible(visible);
+ visibleChanged = false;
+}
+
+#endif
diff --git a/stats/histogrammarker.cpp b/stats/histogrammarker.cpp
new file mode 100644
index 000000000..e7b2512e3
--- /dev/null
+++ b/stats/histogrammarker.cpp
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "histogrammarker.h"
+#include "statsaxis.h"
+#include "zvalues.h"
+
+static const double histogramMarkerWidth = 2.0;
+
+HistogramMarker::HistogramMarker(StatsView &view, double val, bool horizontal,
+ QColor color, StatsAxis *xAxis, StatsAxis *yAxis) :
+ ChartLineItem(view, ChartZValue::ChartFeatures, color, histogramMarkerWidth),
+ xAxis(xAxis), yAxis(yAxis),
+ val(val), horizontal(horizontal)
+{
+}
+
+void HistogramMarker::updatePosition()
+{
+ if (!xAxis || !yAxis)
+ return;
+ if (horizontal) {
+ double y = yAxis->toScreen(val);
+ auto [x1, x2] = xAxis->minMaxScreen();
+ setLine(QPointF(x1, y), QPointF(x2, y));
+ } else {
+ double x = xAxis->toScreen(val);
+ auto [y1, y2] = yAxis->minMaxScreen();
+ setLine(QPointF(x, y1), QPointF(x, y2));
+ }
+}
diff --git a/stats/histogrammarker.h b/stats/histogrammarker.h
new file mode 100644
index 000000000..14d6410bd
--- /dev/null
+++ b/stats/histogrammarker.h
@@ -0,0 +1,21 @@
+// A line to show median an mean in histograms
+#ifndef HISTOGRAM_MARKER_H
+#define HISTOGRAM_MARKER_H
+
+#include "chartitem.h"
+
+class StatsAxis;
+class StatsView;
+
+// A line marking median or mean in histograms
+class HistogramMarker : public ChartLineItem {
+public:
+ HistogramMarker(StatsView &view, double val, bool horizontal, QColor color, StatsAxis *xAxis, StatsAxis *yAxis);
+ void updatePosition();
+private:
+ StatsAxis *xAxis, *yAxis;
+ double val;
+ bool horizontal;
+};
+
+#endif
diff --git a/stats/informationbox.cpp b/stats/informationbox.cpp
index 189fd1ab1..29731acc6 100644
--- a/stats/informationbox.cpp
+++ b/stats/informationbox.cpp
@@ -1,84 +1,74 @@
#include "informationbox.h"
#include "statscolors.h"
+#include "statsview.h"
#include "zvalues.h"
#include <QFontMetrics>
-#include <QGraphicsScene>
-static const QColor informationBorderColor(Qt::black);
-static const QColor informationColor(0xff, 0xff, 0x00, 192); // Note: fourth argument is opacity
static const int informationBorder = 2;
static const double informationBorderRadius = 4.0; // Radius of rounded corners
static const int distanceFromPointer = 10; // Distance to place box from mouse pointer or scatter item
-InformationBox::InformationBox() : RoundRectItem(informationBorderRadius, nullptr)
+InformationBox::InformationBox(StatsView &v) :
+ ChartRectItem(v, ChartZValue::InformationBox,
+ QPen(informationBorderColor, informationBorder),
+ QBrush(informationColor), informationBorderRadius)
{
- setPen(QPen(informationBorderColor, informationBorder));
- setBrush(informationColor);
- setZValue(ZValues::informationBox);
}
void InformationBox::setText(const std::vector<QString> &text, QPointF pos)
{
- width = height = 0.0;
- textItems.clear();
+ QFontMetrics fm(font);
+ double fontHeight = fm.height();
+ std::vector<double> widths;
+ widths.reserve(text.size());
+ width = 0.0;
for (const QString &s: text) {
- if (!s.isEmpty())
- addLine(s);
+ widths.push_back(static_cast<double>(fm.size(Qt::TextSingleLine, s).width()));
+ width = std::max(width, widths.back());
}
width += 4.0 * informationBorder;
- height += 4.0 * informationBorder;
+ height = widths.size() * fontHeight + 4.0 * informationBorder;
+
+ ChartRectItem::resize(QSizeF(width, height));
- // Setting the position will also set the proper size
- setPos(pos);
+ painter->setPen(QPen(darkLabelColor)); // QPainter uses QPen to set text color!
+ double y = 2.0 * informationBorder;
+ for (size_t i = 0; i < widths.size(); ++i) {
+ QRectF rect(2.0 * informationBorder, y, widths[i], fontHeight);
+ painter->drawText(rect, text[i]);
+ y += fontHeight;
+ }
}
void InformationBox::setPos(QPointF pos)
{
- QRectF plotArea = scene()->sceneRect();
+ QSizeF size = sceneSize();
double x = pos.x() + distanceFromPointer;
- if (x + width >= plotArea.right()) {
- if (pos.x() - width >= plotArea.x())
+ if (x + width >= size.width()) {
+ if (pos.x() - width >= 0.0)
x = pos.x() - width;
else
x = pos.x() - width / 2.0;
}
double y = pos.y() + distanceFromPointer;
- if (y + height >= plotArea.bottom()) {
- if (pos.y() - height >= plotArea.y())
+ if (y + height >= size.height()) {
+ if (pos.y() - height >= 0.0)
y = pos.y() - height;
else
y = pos.y() - height / 2.0;
}
- setRect(x, y, width, height);
- double actY = y + 2.0 * informationBorder;
- for (auto &item: textItems) {
- item->setPos(QPointF(x + 2.0 * informationBorder, actY));
- actY += item->boundingRect().height();
- }
-}
-
-void InformationBox::addLine(const QString &s)
-{
- textItems.emplace_back(new QGraphicsSimpleTextItem(s, this));
- QGraphicsSimpleTextItem &item = *textItems.back();
- item.setBrush(QBrush(darkLabelColor));
- item.setPos(QPointF(0.0, height));
- item.setFont(font);
- item.setZValue(ZValues::informationBox);
- QRectF rect = item.boundingRect();
- width = std::max(width, rect.width());
- height += rect.height();
+ ChartRectItem::setPos(QPointF(x, y));
}
// Try to stay within three-thirds of the chart height
int InformationBox::recommendedMaxLines() const
{
QFontMetrics fm(font);
- int maxHeight = static_cast<int>(scene()->sceneRect().height());
+ int maxHeight = static_cast<int>(sceneSize().height());
return maxHeight * 2 / fm.height() / 3;
}
diff --git a/stats/informationbox.h b/stats/informationbox.h
index 741df537f..6ff2bb43e 100644
--- a/stats/informationbox.h
+++ b/stats/informationbox.h
@@ -4,26 +4,24 @@
#ifndef INFORMATION_BOX_H
#define INFORMATION_BOX_H
-#include "backend-shared/roundrectitem.h"
+#include "chartitem.h"
#include <vector>
#include <memory>
#include <QFont>
struct dive;
-class QGraphicsScene;
+class StatsView;
// Information window showing data of highlighted dive
-struct InformationBox : RoundRectItem {
- InformationBox();
+struct InformationBox : ChartRectItem {
+ InformationBox(StatsView &);
void setText(const std::vector<QString> &text, QPointF pos);
void setPos(QPointF pos);
int recommendedMaxLines() const;
private:
QFont font; // For future specialization.
double width, height;
- void addLine(const QString &s);
- std::vector<std::unique_ptr<QGraphicsSimpleTextItem>> textItems;
};
#endif
diff --git a/stats/legend.cpp b/stats/legend.cpp
index 27607fb51..fc8656828 100644
--- a/stats/legend.cpp
+++ b/stats/legend.cpp
@@ -3,9 +3,8 @@
#include "statscolors.h"
#include "zvalues.h"
+#include <cmath>
#include <QFontMetrics>
-#include <QGraphicsScene>
-#include <QGraphicsSceneMouseEvent>
#include <QPen>
static const double legendBorderSize = 2.0;
@@ -13,54 +12,33 @@ static const double legendBoxBorderSize = 1.0;
static const double legendBoxBorderRadius = 4.0; // radius of rounded corners
static const double legendBoxScale = 0.8; // 1.0: text-height of the used font
static const double legendInternalBorderSize = 2.0;
-static const QColor legendColor(0x00, 0x8e, 0xcc, 192); // Note: fourth argument is opacity
-static const QColor legendBorderColor(Qt::black);
-Legend::Legend(const std::vector<QString> &names) :
- RoundRectItem(legendBoxBorderRadius),
- displayedItems(0), width(0.0), height(0.0)
+Legend::Legend(StatsView &view, const std::vector<QString> &names) :
+ ChartRectItem(view, ChartZValue::Legend,
+ QPen(legendBorderColor, legendBorderSize), QBrush(legendColor), legendBoxBorderRadius),
+ displayedItems(0), width(0.0), height(0.0),
+ font(QFont()), // Make configurable
+ posInitialized(false)
{
- setZValue(ZValues::legend);
entries.reserve(names.size());
+ QFontMetrics fm(font);
+ fontHeight = fm.height();
int idx = 0;
for (const QString &name: names)
- entries.emplace_back(name, idx++, (int)names.size(), this);
-
- // Calculate the height and width of the elements
- if (!entries.empty()) {
- QFontMetrics fm(entries[0].text->font());
- fontHeight = fm.height();
- for (Entry &e: entries)
- e.width = fontHeight + 2.0 * legendBoxBorderSize +
- fm.size(Qt::TextSingleLine, e.text->text()).width();
- } else {
- // Set to an arbitrary non-zero value, because Coverity doesn't understand
- // that we don't use the value as divisor below if entries is empty.
- fontHeight = 10.0;
- }
- setPen(QPen(legendBorderColor, legendBorderSize));
- setBrush(QBrush(legendColor));
+ entries.emplace_back(name, idx++, (int)names.size(), fm);
}
-Legend::Entry::Entry(const QString &name, int idx, int numBins, QGraphicsItem *parent) :
- rect(new QGraphicsRectItem(parent)),
- text(new QGraphicsSimpleTextItem(name, parent)),
- width(0)
+Legend::Entry::Entry(const QString &name, int idx, int numBins, const QFontMetrics &fm) :
+ name(name),
+ rectBrush(QBrush(binColor(idx, numBins)))
{
- rect->setZValue(ZValues::legend);
- rect->setPen(QPen(legendBorderColor, legendBoxBorderSize));
- rect->setBrush(QBrush(binColor(idx, numBins)));
- text->setZValue(ZValues::legend);
- text->setBrush(QBrush(darkLabelColor));
+ width = fm.height() + 2.0 * legendBoxBorderSize + fm.size(Qt::TextSingleLine, name).width();
}
void Legend::hide()
{
- for (Entry &e: entries) {
- e.rect->hide();
- e.text->hide();
- }
- QGraphicsRectItem::hide();
+ ChartRectItem::resize(QSizeF(1,1));
+ img->fill(Qt::transparent);
}
void Legend::resize()
@@ -68,7 +46,7 @@ void Legend::resize()
if (entries.empty())
return hide();
- QSizeF size = scene()->sceneRect().size();
+ QSizeF size = sceneSize();
// Silly heuristics: make the legend at most half as high and half as wide as the chart.
// Not sure if that makes sense - this might need some optimization.
@@ -100,31 +78,63 @@ void Legend::resize()
}
width += legendInternalBorderSize;
height = 2 * legendInternalBorderSize + numRows * fontHeight;
- updatePosition();
-}
-void Legend::updatePosition()
-{
- if (displayedItems <= 0)
- return hide();
- // For now, place the legend in the top right corner.
- QPointF pos(scene()->sceneRect().width() - width - 10.0, 10.0);
- setRect(QRectF(pos, QSizeF(width, height)));
+ ChartRectItem::resize(QSizeF(width, height));
+
+ // Paint rectangles
+ painter->setPen(QPen(legendBorderColor, legendBoxBorderSize));
for (int i = 0; i < displayedItems; ++i) {
- QPointF itemPos = pos + entries[i].pos;
+ QPointF itemPos = entries[i].pos;
+ painter->setBrush(entries[i].rectBrush);
QRectF rect(itemPos, QSizeF(fontHeight, fontHeight));
// Decrease box size by legendBoxScale factor
double delta = fontHeight * (1.0 - legendBoxScale) / 2.0;
rect = rect.adjusted(delta, delta, -delta, -delta);
- entries[i].rect->setRect(rect);
+ painter->drawRect(rect);
+ }
+
+ // Paint labels
+ painter->setPen(darkLabelColor); // QPainter uses pen not brush for text!
+ painter->setFont(font);
+ for (int i = 0; i < displayedItems; ++i) {
+ QPointF itemPos = entries[i].pos;
itemPos.rx() += fontHeight + 2.0 * legendBoxBorderSize;
- entries[i].text->setPos(itemPos);
- entries[i].rect->show();
- entries[i].text->show();
+ QRectF rect(itemPos, QSizeF(entries[i].width, fontHeight));
+ painter->drawText(rect, entries[i].name);
}
- for (int i = displayedItems; i < (int)entries.size(); ++i) {
- entries[i].rect->hide();
- entries[i].text->hide();
+
+ if (!posInitialized) {
+ // At first, place in top right corner
+ setPos(QPointF(size.width() - width - 10.0, 10.0));
+ posInitialized = true;
+ } else {
+ // Try to keep relative position with what it was before
+ setPos(QPointF(size.width() * centerPos.x() - width / 2.0,
+ size.height() * centerPos.y() - height / 2.0));
}
- show();
+}
+
+void Legend::setPos(QPointF newPos)
+{
+ // Round the position to integers or horrible artifacts appear (at least on desktop)
+ QPointF posInt(round(newPos.x()), round(newPos.y()));
+
+ // Make sure that the center is inside the drawing area,
+ // so that the user can't lose the legend.
+ QSizeF size = sceneSize();
+ if (size.width() < 1.0 || size.height() < 1.0)
+ return;
+ double widthHalf = floor(width / 2.0);
+ double heightHalf = floor(height / 2.0);
+ QPointF sanitizedPos(std::clamp(posInt.x(), -widthHalf, size.width() - widthHalf - 1.0),
+ std::clamp(posInt.y(), -heightHalf, size.height() - heightHalf - 1.0));
+
+ // Set position
+ ChartRectItem::setPos(sanitizedPos);
+
+ // Remember relative position of center for next time
+ QPointF centerPosAbsolute(sanitizedPos.x() + width / 2.0,
+ sanitizedPos.y() + height / 2.0);
+ centerPos = QPointF(centerPosAbsolute.x() / size.width(),
+ centerPosAbsolute.y() / size.height());
}
diff --git a/stats/legend.h b/stats/legend.h
index c643a41f3..fb88920bd 100644
--- a/stats/legend.h
+++ b/stats/legend.h
@@ -3,34 +3,38 @@
#ifndef STATS_LEGEND_H
#define STATS_LEGEND_H
-#include "backend-shared/roundrectitem.h"
+#include "chartitem.h"
#include <memory>
#include <vector>
+#include <QFont>
-class QGraphicsScene;
-class QGraphicsSceneMouseEvent;
+class QFontMetrics;
-class Legend : public RoundRectItem {
+class Legend : public ChartRectItem {
public:
- Legend(const std::vector<QString> &names);
- void hover(QPointF pos);
+ Legend(StatsView &view, const std::vector<QString> &names);
void resize(); // called when the chart size changes.
+ void setPos(QPointF pos); // Attention: not virtual - always call on this class.
private:
// Each entry is a text besides a rectangle showing the color
struct Entry {
- std::unique_ptr<QGraphicsRectItem> rect;
- std::unique_ptr<QGraphicsSimpleTextItem> text;
+ QString name;
+ QBrush rectBrush;
QPointF pos;
double width;
- Entry(const QString &name, int idx, int numBins, QGraphicsItem *parent);
+ Entry(const QString &name, int idx, int numBins, const QFontMetrics &fm);
};
int displayedItems;
double width;
double height;
+ QFont font;
+ // The position is specified with respect to the center and in relative terms
+ // with respect to the canvas.
+ QPointF centerPos;
+ bool posInitialized;
int fontHeight;
std::vector<Entry> entries;
- void updatePosition();
void hide();
};
diff --git a/stats/pieseries.cpp b/stats/pieseries.cpp
index eeecac36c..8db3bdbe3 100644
--- a/stats/pieseries.cpp
+++ b/stats/pieseries.cpp
@@ -4,11 +4,11 @@
#include "statscolors.h"
#include "statshelper.h"
#include "statstranslations.h"
+#include "statsview.h"
#include "zvalues.h"
#include <numeric>
#include <math.h>
-#include <QGraphicsEllipseItem>
#include <QLocale>
static const double pieSize = 0.9; // 1.0 = occupy full width of chart
@@ -16,49 +16,39 @@ static const double pieBorderWidth = 1.0;
static const double innerLabelRadius = 0.75; // 1.0 = at outer border of pie
static const double outerLabelRadius = 1.01; // 1.0 = at outer border of pie
-PieSeries::Item::Item(QGraphicsScene *scene, const QString &name, int from, int count, int totalCount,
- int bin_nr, int numBins, bool labels) :
- item(createItemPtr<QGraphicsEllipseItem>(scene)),
+PieSeries::Item::Item(StatsView &view, const QString &name, int from, int count, int totalCount,
+ int bin_nr, int numBins) :
name(name),
count(count)
{
+ QFont f; // make configurable
QLocale loc;
- // For whatever obscure reason, angles in QGraphicsEllipseItem are given as 16th of a degree...?
- // Angles increase CCW, whereas pie charts usually are read CW.
- item->setStartAngle(90 * 16 - (from + count) * 360 * 16 / totalCount);
- item->setSpanAngle(count * 360 * 16 / totalCount);
- item->setPen(QPen(::borderColor));
- item->setZValue(ZValues::series);
+ angleFrom = static_cast<double>(from) / totalCount;
angleTo = static_cast<double>(from + count) / totalCount;
double meanAngle = M_PI / 2.0 - (from + count / 2.0) / totalCount * M_PI * 2.0; // Note: "-" because we go CW.
innerLabelPos = QPointF(cos(meanAngle) * innerLabelRadius, -sin(meanAngle) * innerLabelRadius);
outerLabelPos = QPointF(cos(meanAngle) * outerLabelRadius, -sin(meanAngle) * outerLabelRadius);
- if (labels) {
- double percentage = count * 100.0 / totalCount;
- QString innerLabelText = QStringLiteral("%1\%").arg(loc.toString(percentage, 'f', 1));
- innerLabel = createItemPtr<QGraphicsSimpleTextItem>(scene, innerLabelText);
- innerLabel->setZValue(ZValues::seriesLabels);
+ double percentage = count * 100.0 / totalCount;
+ QString innerLabelText = QStringLiteral("%1\%").arg(loc.toString(percentage, 'f', 1));
+ innerLabel = view.createChartItem<ChartTextItem>(ChartZValue::SeriesLabels, f, innerLabelText);
- outerLabel = createItemPtr<QGraphicsSimpleTextItem>(scene, name);
- outerLabel->setBrush(QBrush(darkLabelColor));
- outerLabel->setZValue(ZValues::seriesLabels);
- }
-
- highlight(bin_nr, false, numBins);
+ outerLabel = view.createChartItem<ChartTextItem>(ChartZValue::SeriesLabels, f, name);
+ outerLabel->setColor(darkLabelColor);
}
-void PieSeries::Item::updatePositions(const QRectF &rect, const QPointF &center, double radius)
+void PieSeries::Item::updatePositions(const QPointF &center, double radius)
{
- item->setRect(rect);
+ // Note: the positions in this functions are rounded to integer values,
+ // because half-integer values gives horrible aliasing artifacts.
if (innerLabel) {
- QRectF labelRect = innerLabel->boundingRect();
- innerLabel->setPos(center.x() + innerLabelPos.x() * radius - labelRect.width() / 2.0,
- center.y() + innerLabelPos.y() * radius - labelRect.height() / 2.0);
+ QRectF labelRect = innerLabel->getRect();
+ innerLabel->setPos(QPointF(round(center.x() + innerLabelPos.x() * radius - labelRect.width() / 2.0),
+ round(center.y() + innerLabelPos.y() * radius - labelRect.height() / 2.0)));
}
if (outerLabel) {
- QRectF labelRect = outerLabel->boundingRect();
+ QRectF labelRect = outerLabel->getRect();
QPointF pos(center.x() + outerLabelPos.x() * radius, center.y() + outerLabelPos.y() * radius);
if (outerLabelPos.x() < 0.0) {
if (outerLabelPos.y() < 0.0)
@@ -69,25 +59,23 @@ void PieSeries::Item::updatePositions(const QRectF &rect, const QPointF &center,
pos.ry() -= labelRect.height();
}
- outerLabel->setPos(pos);
+ outerLabel->setPos(QPointF(round(pos.x()), round(pos.y())));
}
}
-void PieSeries::Item::highlight(int bin_nr, bool highlight, int numBins)
+void PieSeries::Item::highlight(ChartPieItem &item, int bin_nr, bool highlight, int numBins)
{
- QBrush brush(highlight ? highlightedColor : binColor(bin_nr, numBins));
- QPen pen(highlight ? highlightedBorderColor : ::borderColor, pieBorderWidth);
- item->setBrush(brush);
- item->setPen(pen);
- if (innerLabel) {
- QBrush labelBrush(highlight ? darkLabelColor : labelColor(bin_nr, numBins));
- innerLabel->setBrush(labelBrush);
- }
+ QColor fill = highlight ? highlightedColor : binColor(bin_nr, numBins);
+ QColor border = highlight ? highlightedBorderColor : ::borderColor;
+ if (innerLabel)
+ innerLabel->setColor(highlight ? darkLabelColor : labelColor(bin_nr, numBins), fill);
+ item.drawSegment(angleFrom, angleTo, fill, border);
}
-PieSeries::PieSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis, const QString &categoryName,
- const std::vector<std::pair<QString, int>> &data, bool keepOrder, bool labels) :
- StatsSeries(scene, xAxis, yAxis),
+PieSeries::PieSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis, const QString &categoryName,
+ const std::vector<std::pair<QString, int>> &data, bool keepOrder) :
+ StatsSeries(view, xAxis, yAxis),
+ item(view.createChartItem<ChartPieItem>(ChartZValue::Series, pieBorderWidth)),
categoryName(categoryName),
highlighted(-1)
{
@@ -147,7 +135,7 @@ PieSeries::PieSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
int act = 0;
for (auto it2 = sorted.begin(); it2 != it; ++it2) {
int count = data[*it2].second;
- items.emplace_back(scene, data[*it2].first, act, count, totalCount, (int)items.size(), numBins, labels);
+ items.emplace_back(view, data[*it2].first, act, count, totalCount, (int)items.size(), numBins);
act += count;
}
@@ -157,7 +145,7 @@ PieSeries::PieSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
for (auto it2 = it; it2 != sorted.end(); ++it2)
other.push_back({ data[*it2].first, data[*it2].second });
QString name = StatsTranslations::tr("other (%1 items)").arg(other.size());
- items.emplace_back(scene, name, act, totalCount - act, totalCount, (int)items.size(), numBins, labels);
+ items.emplace_back(view, name, act, totalCount - act, totalCount, (int)items.size(), numBins);
}
}
@@ -167,12 +155,18 @@ PieSeries::~PieSeries()
void PieSeries::updatePositions()
{
- QRectF plotRect = scene->sceneRect();
+ QRectF plotRect = view.plotArea();
center = plotRect.center();
- radius = std::min(plotRect.width(), plotRect.height()) * pieSize / 2.0;
- QRectF rect(center.x() - radius, center.y() - radius, 2.0 * radius, 2.0 * radius);
- for (Item &item: items)
- item.updatePositions(rect, center, radius);
+ radius = ceil(std::min(plotRect.width(), plotRect.height()) * pieSize / 2.0);
+ QRectF rect(round(center.x() - radius), round(center.y() - radius), ceil(2.0 * radius), ceil(2.0 * radius));
+ item->resize(rect.size());
+ item->setPos(rect.topLeft());
+ int i = 0;
+ for (Item &segment: items) {
+ segment.updatePositions(center, radius);
+ segment.highlight(*item, i, i == highlighted, (int)items.size()); // Draw segment
+ ++i;
+ }
}
std::vector<QString> PieSeries::binNames()
@@ -244,12 +238,13 @@ bool PieSeries::hover(QPointF pos)
// Highlight new item (if any)
if (highlighted >= 0 && highlighted < (int)items.size()) {
- items[highlighted].highlight(highlighted, true, (int)items.size());
+ items[highlighted].highlight(*item, highlighted, true, (int)items.size());
if (!information)
- information = createItemPtr<InformationBox>(scene);
+ information = view.createChartItem<InformationBox>();
information->setText(makeInfo(highlighted), pos);
+ information->setVisible(true);
} else {
- information.reset();
+ information->setVisible(false);
}
return highlighted >= 0;
}
@@ -257,6 +252,6 @@ bool PieSeries::hover(QPointF pos)
void PieSeries::unhighlight()
{
if (highlighted >= 0 && highlighted < (int)items.size())
- items[highlighted].highlight(highlighted, false, (int)items.size());
+ items[highlighted].highlight(*item, highlighted, false, (int)items.size());
highlighted = -1;
}
diff --git a/stats/pieseries.h b/stats/pieseries.h
index 646c4bfbe..0cb5e12cb 100644
--- a/stats/pieseries.h
+++ b/stats/pieseries.h
@@ -3,6 +3,7 @@
#ifndef PIE_SERIES_H
#define PIE_SERIES_H
+#include "statshelper.h"
#include "statsseries.h"
#include <memory>
@@ -10,9 +11,8 @@
#include <QString>
struct InformationBox;
-class QGraphicsEllipseItem;
-class QGraphicsScene;
-class QGraphicsSimpleTextItem;
+struct ChartPieItem;
+struct ChartTextItem;
class QRectF;
class PieSeries : public StatsSeries {
@@ -20,8 +20,8 @@ public:
// The pie series is initialized with (name, count) pairs.
// If keepOrder is false, bins will be sorted by size, otherwise the sorting
// of the shown bins will be retained. Small bins are omitted for clarity.
- PieSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis, const QString &categoryName,
- const std::vector<std::pair<QString, int>> &data, bool keepOrder, bool labels);
+ PieSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis, const QString &categoryName,
+ const std::vector<std::pair<QString, int>> &data, bool keepOrder);
~PieSeries();
void updatePositions() override;
@@ -34,20 +34,20 @@ private:
// Get item under mouse pointer, or -1 if none
int getItemUnderMouse(const QPointF &f) const;
+ ChartItemPtr<ChartPieItem> item;
QString categoryName;
std::vector<QString> makeInfo(int idx) const;
struct Item {
- std::unique_ptr<QGraphicsEllipseItem> item;
- std::unique_ptr<QGraphicsSimpleTextItem> innerLabel, outerLabel;
+ ChartItemPtr<ChartTextItem> innerLabel, outerLabel;
QString name;
- double angleTo; // In fraction of total
+ double angleFrom, angleTo; // In fraction of total
int count;
QPointF innerLabelPos, outerLabelPos; // With respect to a (-1, -1)-(1, 1) rectangle.
- Item(QGraphicsScene *scene, const QString &name, int from, int count, int totalCount,
- int bin_nr, int numBins, bool labels);
- void updatePositions(const QRectF &rect, const QPointF &center, double radius);
- void highlight(int bin_nr, bool highlight, int numBins);
+ Item(StatsView &view, const QString &name, int from, int count, int totalCount,
+ int bin_nr, int numBins);
+ void updatePositions(const QPointF &center, double radius);
+ void highlight(ChartPieItem &item, int bin_nr, bool highlight, int numBins);
};
std::vector<Item> items;
int totalCount;
@@ -59,7 +59,7 @@ private:
};
std::vector<OtherItem> other;
- std::unique_ptr<InformationBox> information;
+ ChartItemPtr<InformationBox> information;
QPointF center; // center of drawing area
double radius; // radius of pie
int highlighted;
diff --git a/stats/quartilemarker.cpp b/stats/quartilemarker.cpp
new file mode 100644
index 000000000..611737d76
--- /dev/null
+++ b/stats/quartilemarker.cpp
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "quartilemarker.h"
+#include "statscolors.h"
+#include "statsaxis.h"
+#include "zvalues.h"
+
+static const double quartileMarkerSize = 15.0;
+
+QuartileMarker::QuartileMarker(StatsView &view, double pos, double value, StatsAxis *xAxis, StatsAxis *yAxis) :
+ ChartLineItem(view, ChartZValue::ChartFeatures, quartileMarkerColor, 2.0),
+ xAxis(xAxis), yAxis(yAxis),
+ pos(pos),
+ value(value)
+{
+ updatePosition();
+}
+
+QuartileMarker::~QuartileMarker()
+{
+}
+
+void QuartileMarker::updatePosition()
+{
+ if (!xAxis || !yAxis)
+ return;
+ double x = xAxis->toScreen(pos);
+ double y = yAxis->toScreen(value);
+ setLine(QPointF(x - quartileMarkerSize / 2.0, y),
+ QPointF(x + quartileMarkerSize / 2.0, y));
+}
diff --git a/stats/quartilemarker.h b/stats/quartilemarker.h
new file mode 100644
index 000000000..2e754248d
--- /dev/null
+++ b/stats/quartilemarker.h
@@ -0,0 +1,20 @@
+// A short line used to mark quartiles
+#ifndef QUARTILE_MARKER_H
+#define QUARTILE_MARKER_H
+
+#include "chartitem.h"
+
+class StatsAxis;
+class StatsView;
+
+class QuartileMarker : public ChartLineItem {
+public:
+ QuartileMarker(StatsView &view, double pos, double value, StatsAxis *xAxis, StatsAxis *yAxis);
+ ~QuartileMarker();
+ void updatePosition();
+private:
+ StatsAxis *xAxis, *yAxis;
+ double pos, value;
+};
+
+#endif
diff --git a/stats/regressionitem.cpp b/stats/regressionitem.cpp
new file mode 100644
index 000000000..9f176c6ae
--- /dev/null
+++ b/stats/regressionitem.cpp
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "regressionitem.h"
+#include "statsaxis.h"
+#include "statscolors.h"
+#include "zvalues.h"
+
+#include <cmath>
+
+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),
+ regression(true), confidence(true)
+{
+}
+
+RegressionItem::~RegressionItem()
+{
+}
+
+void RegressionItem::setFeatures(bool regressionIn, bool confidenceIn)
+{
+ if (regressionIn == regression && confidenceIn == confidence)
+ return;
+ regression = regressionIn;
+ confidence = confidenceIn;
+ updatePosition();
+}
+
+// Note: this calculates the confidence area, even if it isn't shown. Might want to optimize this.
+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 + 1.960 * 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 - 1.960 * 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);
+ if (confidence) {
+ QColor col(regressionItemColor);
+ col.setAlphaF(reg.r2);
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(QBrush(col));
+ painter->drawPolygon(poly);
+ }
+
+ if (regression) {
+ 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..24141122c
--- /dev/null
+++ b/stats/regressionitem.h
@@ -0,0 +1,28 @@
+// 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();
+ void setFeatures(bool regression, bool confidence);
+private:
+ StatsAxis *xAxis, *yAxis;
+ regression_data reg;
+ bool regression, confidence;
+};
+
+#endif
diff --git a/stats/scatterseries.cpp b/stats/scatterseries.cpp
index 8e2399008..791bb81ca 100644
--- a/stats/scatterseries.cpp
+++ b/stats/scatterseries.cpp
@@ -1,24 +1,20 @@
// SPDX-License-Identifier: GPL-2.0
#include "scatterseries.h"
+#include "chartitem.h"
#include "informationbox.h"
#include "statscolors.h"
#include "statshelper.h"
#include "statstranslations.h"
#include "statsvariables.h"
+#include "statsview.h"
#include "zvalues.h"
#include "core/dive.h"
#include "core/divelist.h"
#include "core/qthelper.h"
-#include <QGraphicsPixmapItem>
-#include <QPainter>
-
-static const int scatterItemDiameter = 10;
-static const int scatterItemBorder = 1;
-
-ScatterSeries::ScatterSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
+ScatterSeries::ScatterSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,
const StatsVariable &varX, const StatsVariable &varY) :
- StatsSeries(scene, xAxis, yAxis),
+ StatsSeries(view, xAxis, yAxis),
varX(varX), varY(varY)
{
}
@@ -27,62 +23,28 @@ ScatterSeries::~ScatterSeries()
{
}
-static QPixmap createScatterPixmap(const QColor &color, const QColor &borderColor)
-{
- QPixmap res(scatterItemDiameter, scatterItemDiameter);
- res.fill(Qt::transparent);
- QPainter painter(&res);
- painter.setPen(Qt::NoPen);
- painter.setRenderHint(QPainter::Antialiasing);
- painter.setBrush(borderColor);
- painter.drawEllipse(0, 0, scatterItemDiameter, scatterItemDiameter);
- painter.setBrush(color);
- painter.drawEllipse(scatterItemBorder, scatterItemBorder,
- scatterItemDiameter - 2 * scatterItemBorder,
- scatterItemDiameter - 2 * scatterItemBorder);
- return res;
-}
-
-// Annoying: we can create a QPixmap only after the application was initialized.
-// Therefore, do this as a on-demand initialized pointer. A function local static
-// variable does unnecesssary (in this case) thread synchronization.
-static std::unique_ptr<QPixmap> scatterPixmapPtr;
-static std::unique_ptr<QPixmap> scatterPixmapHighlightedPtr;
-
-static const QPixmap &scatterPixmap(bool highlight)
-{
- if (!scatterPixmapPtr) {
- scatterPixmapPtr.reset(new QPixmap(createScatterPixmap(fillColor, ::borderColor)));
- scatterPixmapHighlightedPtr.reset(new QPixmap(createScatterPixmap(highlightedColor, highlightedBorderColor)));
- }
- return highlight ? *scatterPixmapHighlightedPtr : *scatterPixmapPtr;
-}
-
-ScatterSeries::Item::Item(QGraphicsScene *scene, ScatterSeries *series, dive *d, double pos, double value) :
- item(createItemPtr<QGraphicsPixmapItem>(scene, scatterPixmap(false))),
+ScatterSeries::Item::Item(StatsView &view, ScatterSeries *series, dive *d, double pos, double value) :
+ item(view.createChartItem<ChartScatterItem>(ChartZValue::Series)),
d(d),
pos(pos),
value(value)
{
- item->setZValue(ZValues::series);
updatePosition(series);
}
void ScatterSeries::Item::updatePosition(ScatterSeries *series)
{
- QPointF center = series->toScreen(QPointF(pos, value));
- item->setPos(center.x() - scatterItemDiameter / 2.0,
- center.y() - scatterItemDiameter / 2.0);
+ item->setPos(series->toScreen(QPointF(pos, value)));
}
void ScatterSeries::Item::highlight(bool highlight)
{
- item->setPixmap(scatterPixmap(highlight));
+ item->setHighlight(highlight);
}
void ScatterSeries::append(dive *d, double pos, double value)
{
- items.emplace_back(scene, this, d, pos, value);
+ items.emplace_back(view, this, d, pos, value);
}
void ScatterSeries::updatePositions()
@@ -91,35 +53,20 @@ void ScatterSeries::updatePositions()
item.updatePosition(this);
}
-static double sq(double f)
-{
- return f * f;
-}
-
-static double squareDist(const QPointF &p1, const QPointF &p2)
-{
- QPointF diff = p1 - p2;
- return QPointF::dotProduct(diff, diff);
-}
-
std::vector<int> ScatterSeries::getItemsUnderMouse(const QPointF &point) const
{
std::vector<int> res;
double x = point.x();
- auto low = std::lower_bound(items.begin(), items.end(), x - scatterItemDiameter,
- [] (const Item &item, double x) { return item.item->pos().x() < x; });
- auto high = std::upper_bound(low, items.end(), x + scatterItemDiameter,
- [] (double x, const Item &item) { return x < item.item->pos().x(); });
+ auto low = std::lower_bound(items.begin(), items.end(), x,
+ [] (const Item &item, double x) { return item.item->getRect().right() < x; });
+ auto high = std::upper_bound(low, items.end(), x,
+ [] (double x, const Item &item) { return x < item.item->getRect().left(); });
// Hopefully that narrows it down enough. For discrete scatter plots, we could also partition
// by equal x and do a binary search in these partitions. But that's probably not worth it.
res.reserve(high - low);
- double minSquare = sq(scatterItemDiameter / 2.0 + scatterItemBorder);
for (auto it = low; it < high; ++it) {
- QPointF pos = it->item->pos();
- pos.rx() += scatterItemDiameter / 2.0 + scatterItemBorder;
- pos.ry() += scatterItemDiameter / 2.0 + scatterItemBorder;
- if (squareDist(pos, point) <= minSquare)
+ if (it->item->contains(point))
res.push_back(it - items.begin());
}
return res;
@@ -169,11 +116,11 @@ bool ScatterSeries::hover(QPointF pos)
highlighted = std::move(newHighlighted);
if (highlighted.empty()) {
- information.reset();
+ information->setVisible(false);
return false;
} else {
if (!information)
- information = createItemPtr<InformationBox>(scene);
+ information = view.createChartItem<InformationBox>();
std::vector<QString> text;
text.reserve(highlighted.size() * 5);
@@ -201,6 +148,7 @@ bool ScatterSeries::hover(QPointF pos)
}
information->setText(text, pos);
+ information->setVisible(true);
return true;
}
}
diff --git a/stats/scatterseries.h b/stats/scatterseries.h
index 5f8b4b2e6..e1642f4c6 100644
--- a/stats/scatterseries.h
+++ b/stats/scatterseries.h
@@ -4,21 +4,20 @@
#ifndef SCATTER_SERIES_H
#define SCATTER_SERIES_H
+#include "statshelper.h"
#include "statsseries.h"
#include <memory>
#include <vector>
-#include <QGraphicsRectItem>
-class QGraphicsPixmapItem;
-class QGraphicsScene;
+class ChartScatterItem;
struct InformationBox;
struct StatsVariable;
struct dive;
class ScatterSeries : public StatsSeries {
public:
- ScatterSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis,
+ ScatterSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis,
const StatsVariable &varX, const StatsVariable &varY);
~ScatterSeries();
@@ -34,15 +33,15 @@ private:
std::vector<int> getItemsUnderMouse(const QPointF &f) const;
struct Item {
- std::unique_ptr<QGraphicsPixmapItem> item;
+ ChartItemPtr<ChartScatterItem> item;
dive *d;
double pos, value;
- Item(QGraphicsScene *scene, ScatterSeries *series, dive *d, double pos, double value);
+ Item(StatsView &view, ScatterSeries *series, dive *d, double pos, double value);
void updatePosition(ScatterSeries *series);
void highlight(bool highlight);
};
- std::unique_ptr<InformationBox> information;
+ ChartItemPtr<InformationBox> information;
std::vector<Item> items;
std::vector<int> highlighted;
const StatsVariable &varX;
diff --git a/stats/statsaxis.cpp b/stats/statsaxis.cpp
index 2c5a5d961..92c972e0e 100644
--- a/stats/statsaxis.cpp
+++ b/stats/statsaxis.cpp
@@ -4,6 +4,7 @@
#include "statshelper.h"
#include "statstranslations.h"
#include "statsvariables.h"
+#include "statsview.h"
#include "zvalues.h"
#include "core/pref.h"
#include "core/subsurface-time.h"
@@ -23,23 +24,19 @@ static const double axisLabelSpaceVertical = 2.0; // Space between axis or ticks
static const double axisTitleSpaceHorizontal = 2.0; // Space between labels and title
static const double axisTitleSpaceVertical = 2.0; // Space between labels and title
-StatsAxis::StatsAxis(const QString &titleIn, bool horizontal, bool labelsBetweenTicks) :
- horizontal(horizontal), labelsBetweenTicks(labelsBetweenTicks),
+StatsAxis::StatsAxis(StatsView &view, const QString &title, bool horizontal, bool labelsBetweenTicks) :
+ ChartPixmapItem(view, ChartZValue::Axes),
+ line(view.createChartItem<ChartLineItem>(ChartZValue::Axes, axisColor, axisWidth)),
+ title(title), horizontal(horizontal), labelsBetweenTicks(labelsBetweenTicks),
size(1.0), zeroOnScreen(0.0), min(0.0), max(1.0), labelWidth(0.0)
{
- // use a Light version of the application fond for both labels and title
+ // use a Light version of the application font for both labels and title
labelFont = QFont();
labelFont.setWeight(QFont::Light);
titleFont = labelFont;
- setPen(QPen(axisColor, axisWidth));
- setZValue(ZValues::axes);
- if (!titleIn.isEmpty()) {
- title.reset(new QGraphicsSimpleTextItem(titleIn, this));
- title->setFont(titleFont);
- title->setBrush(darkLabelColor);
- if (!horizontal)
- title->setRotation(-90.0);
- }
+ QFontMetrics fm(titleFont);
+ titleWidth = title.isEmpty() ? 0.0
+ : static_cast<double>(fm.size(Qt::TextSingleLine, title).width());
}
StatsAxis::~StatsAxis()
@@ -101,7 +98,7 @@ int StatsAxis::guessNumTicks(const std::vector<QString> &strings) const
double StatsAxis::titleSpace() const
{
- if (!title)
+ if (title.isEmpty())
return 0.0;
return horizontal ? QFontMetrics(titleFont).height() + axisTitleSpaceHorizontal
: QFontMetrics(titleFont).height() + axisTitleSpaceVertical;
@@ -124,31 +121,14 @@ double StatsAxis::height() const
(labelsBetweenTicks ? 0.0 : axisTickSizeHorizontal);
}
-StatsAxis::Label::Label(const QString &name, double pos, QGraphicsScene *scene, const QFont &font) :
- label(createItem<QGraphicsSimpleTextItem>(scene, name)),
- pos(pos)
-{
- label->setBrush(QBrush(darkLabelColor));
- label->setFont(font);
- label->setZValue(ZValues::axes);
-}
-
-void StatsAxis::addLabel(const QString &label, double pos)
-{
- labels.emplace_back(label, pos, scene(), labelFont);
-}
-
-StatsAxis::Tick::Tick(double pos, QGraphicsScene *scene) :
- item(createItemPtr<QGraphicsLineItem>(scene)),
- pos(pos)
+void StatsAxis::addLabel(const QFontMetrics &fm, const QString &label, double pos)
{
- item->setPen(QPen(axisColor, axisTickWidth));
- item->setZValue(ZValues::axes);
+ labels.push_back({ label, fm.size(Qt::TextSingleLine, label).width(), pos });
}
void StatsAxis::addTick(double pos)
{
- ticks.emplace_back(pos, scene());
+ ticks.push_back({ view.createChartItem<ChartLineItem>(ChartZValue::Axes, axisColor, axisTickWidth), pos });
}
std::vector<double> StatsAxis::ticksPositions() const
@@ -178,60 +158,108 @@ double StatsAxis::toValue(double pos) const
void StatsAxis::setSize(double sizeIn)
{
size = sizeIn;
+
+ // Ticks (and labels) should probably be reused. For now, clear them.
+ for (Tick &tick: ticks)
+ view.deleteChartItem(tick.item);
+ labels.clear();
+ ticks.clear();
updateLabels();
+
labelWidth = 0.0;
for (const Label &label: labels) {
- double w = label.label->boundingRect().width();
- if (w > labelWidth)
- labelWidth = w;
+ if (label.width > labelWidth)
+ labelWidth = label.width;
+ }
+
+ QFontMetrics fm(labelFont);
+ int fontHeight = fm.height();
+ if (horizontal) {
+ double pixmapWidth = size;
+ double offsetX = 0.0;
+ if (!labels.empty() && !labelsBetweenTicks) {
+ pixmapWidth += labels.front().width / 2.0 + labels.back().width / 2.0;
+ offsetX += labels.front().width / 2.0;
+ }
+
+ double pixmapHeight = fontHeight + titleSpace();
+ double offsetY = -axisWidth / 2.0 - axisLabelSpaceHorizontal -
+ (labelsBetweenTicks ? 0.0 : axisTickSizeHorizontal);
+
+ ChartPixmapItem::resize(QSizeF(pixmapWidth, pixmapHeight)); // Note: this rounds the dimensions up
+ offset = QPointF(round(offsetX), round(offsetY));
+ img->fill(Qt::transparent);
+
+ painter->setPen(QPen(darkLabelColor));
+ painter->setFont(labelFont);
+ for (const Label &label: labels) {
+ double x = (label.pos - min) / (max - min) * size + offset.x() - round(label.width / 2.0);
+ QRectF rect(x, 0.0, label.width, fontHeight);
+ painter->drawText(rect, label.label);
+ }
+ if (!title.isEmpty()) {
+ QRectF rect(offset.x() + round((size - titleWidth) / 2.0),
+ fontHeight + axisTitleSpaceHorizontal,
+ titleWidth, fontHeight);
+ painter->setFont(titleFont);
+ painter->drawText(rect, title);
+ }
+ } else {
+ double pixmapWidth = labelWidth + titleSpace();
+ double offsetX = pixmapWidth + axisLabelSpaceVertical + (labelsBetweenTicks ? 0.0 : axisTickSizeVertical);
+
+ double pixmapHeight = ceil(size + axisTickWidth);
+ double offsetY = size;
+ if (!labels.empty() && !labelsBetweenTicks) {
+ pixmapHeight += fontHeight;
+ offsetY += fontHeight / 2.0;
+ }
+
+ ChartPixmapItem::resize(QSizeF(pixmapWidth, pixmapHeight)); // Note: this rounds the dimensions up
+ offset = QPointF(round(offsetX), round(offsetY));
+ img->fill(Qt::transparent);
+
+ painter->setPen(QPen(darkLabelColor));
+ painter->setFont(labelFont);
+ for (const Label &label: labels) {
+ double y = (min - label.pos) / (max - min) * size + offset.y() - round(fontHeight / 2.0);
+ QRectF rect(pixmapWidth - label.width, y, label.width, fontHeight);
+ painter->drawText(rect, label.label);
+ }
+ if (!title.isEmpty()) {
+ painter->rotate(-90.0);
+ QRectF rect(round(-(offsetY + titleWidth) / 2.0), 0.0, titleWidth, fontHeight);
+ painter->setFont(titleFont);
+ painter->drawText(rect, title);
+ painter->resetTransform();
+ }
}
}
void StatsAxis::setPos(QPointF pos)
{
+ zeroOnScreen = horizontal ? pos.x() : pos.y();
+ ChartPixmapItem::setPos(pos - offset);
+
if (horizontal) {
- zeroOnScreen = pos.x();
- double labelY = pos.y() + axisLabelSpaceHorizontal +
- (labelsBetweenTicks ? 0.0 : axisTickSizeHorizontal);
double y = pos.y();
- for (Label &label: labels) {
- double x = toScreen(label.pos) - label.label->boundingRect().width() / 2.0;
- label.label->setPos(QPointF(x, labelY));
- }
- for (Tick &tick: ticks) {
+ for (const Tick &tick: ticks) {
double x = toScreen(tick.pos);
- tick.item->setLine(x, y, x, y + axisTickSizeHorizontal);
+ tick.item->setLine(QPointF(x, y), QPointF(x, y + axisTickSizeHorizontal));
}
- setLine(zeroOnScreen, y, zeroOnScreen + size, y);
- if (title)
- title->setPos(zeroOnScreen + (size - title->boundingRect().width()) / 2.0,
- labelY + QFontMetrics(labelFont).height() + axisTitleSpaceHorizontal);
+ line->setLine(QPointF(zeroOnScreen, y), QPointF(zeroOnScreen + size, y));
} else {
- double fontHeight = QFontMetrics(labelFont).height();
- zeroOnScreen = pos.y();
double x = pos.x();
- double labelX = x - axisLabelSpaceVertical -
- (labelsBetweenTicks ? 0.0 : axisTickSizeVertical);
- for (Label &label: labels) {
- double y = toScreen(label.pos) - fontHeight / 2.0;
- label.label->setPos(QPointF(labelX - label.label->boundingRect().width(), y));
- }
- for (Tick &tick: ticks) {
+ for (const Tick &tick: ticks) {
double y = toScreen(tick.pos);
- tick.item->setLine(x, y, x - axisTickSizeVertical, y);
+ tick.item->setLine(QPointF(x, y), QPointF(x - axisTickSizeVertical, y));
}
- // This is very confusing: even though we need the height of the title, the correct
- // size is stored in boundingRect().width(). Presumably because the item is rotated
- // by -90°. Apparently, the boundingRect is in item-local coordinates?
- if (title)
- title->setPos(labelX - labelWidth - QFontMetrics(labelFont).height() - axisTitleSpaceVertical,
- zeroOnScreen - (size - title->boundingRect().width()) / 2.0);
- setLine(x, zeroOnScreen, x, zeroOnScreen - size);
+ line->setLine(QPointF(x, zeroOnScreen), QPointF(x, zeroOnScreen - size));
}
}
-ValueAxis::ValueAxis(const QString &title, double min, double max, int decimals, bool horizontal) :
- StatsAxis(title, horizontal, false),
+ValueAxis::ValueAxis(StatsView &view, const QString &title, double min, double max, int decimals, bool horizontal) :
+ StatsAxis(view, title, horizontal, false),
min(min), max(max), decimals(decimals)
{
// Avoid degenerate cases
@@ -251,9 +279,6 @@ std::pair<QString, QString> ValueAxis::getFirstLastLabel() const
void ValueAxis::updateLabels()
{
- labels.clear();
- ticks.clear();
-
QLocale loc;
auto [minString, maxString] = getFirstLastLabel();
int numTicks = guessNumTicks({ minString, maxString});
@@ -283,15 +308,16 @@ void ValueAxis::updateLabels()
double act = actMin;
labels.reserve(num + 1);
ticks.reserve(num + 1);
+ QFontMetrics fm(labelFont);
for (int i = 0; i <= num; ++i) {
- addLabel(loc.toString(act, 'f', decimals), act);
+ addLabel(fm, loc.toString(act, 'f', decimals), act);
addTick(act);
act += actStep;
}
}
-CountAxis::CountAxis(const QString &title, int count, bool horizontal) :
- ValueAxis(title, 0.0, (double)count, 0, horizontal),
+CountAxis::CountAxis(StatsView &view, const QString &title, int count, bool horizontal) :
+ ValueAxis(view, title, 0.0, (double)count, 0, horizontal),
count(count)
{
}
@@ -306,9 +332,6 @@ std::pair<QString, QString> CountAxis::getFirstLastLabel() const
void CountAxis::updateLabels()
{
- labels.clear();
- ticks.clear();
-
QLocale loc;
QString countString = loc.toString(count);
int numTicks = guessNumTicks({ countString });
@@ -345,17 +368,21 @@ void CountAxis::updateLabels()
labels.reserve(max + 1);
ticks.reserve(max + 1);
+ QFontMetrics fm(labelFont);
for (int i = 0; i <= max; i += step) {
- addLabel(loc.toString(i), static_cast<double>(i));
+ addLabel(fm, loc.toString(i), static_cast<double>(i));
addTick(static_cast<double>(i));
}
}
-CategoryAxis::CategoryAxis(const QString &title, const std::vector<QString> &labels, bool horizontal) :
- StatsAxis(title, horizontal, true),
+CategoryAxis::CategoryAxis(StatsView &view, const QString &title, const std::vector<QString> &labels, bool horizontal) :
+ StatsAxis(view, title, horizontal, true),
labelsText(labels)
{
- setRange(-0.5, static_cast<double>(labels.size()) + 0.5);
+ if (!labels.empty())
+ setRange(-0.5, static_cast<double>(labels.size()) - 0.5);
+ else
+ setRange(-1.0, 1.0);
}
// No implementation because the labels are inside ticks and this
@@ -392,8 +419,6 @@ void CategoryAxis::updateLabels()
QString ellipsis = horizontal ? getEllipsis(fm, size_per_label) : QString();
- labels.clear();
- ticks.clear();
labels.reserve(labelsText.size());
ticks.reserve(labelsText.size() + 1);
double pos = 0.0;
@@ -401,18 +426,18 @@ void CategoryAxis::updateLabels()
for (const QString &s: labelsText) {
if (horizontal) {
double width = static_cast<double>(fm.size(Qt::TextSingleLine, s).width());
- addLabel(width < size_per_label ? s : ellipsis, pos);
+ addLabel(fm, width < size_per_label ? s : ellipsis, pos);
} else {
if (fontHeight < size_per_label)
- addLabel(s, pos);
+ addLabel(fm, s, pos);
}
addTick(pos + 0.5);
pos += 1.0;
}
}
-HistogramAxis::HistogramAxis(const QString &title, std::vector<HistogramAxisEntry> bins, bool horizontal) :
- StatsAxis(title, horizontal, false),
+HistogramAxis::HistogramAxis(StatsView &view, const QString &title, std::vector<HistogramAxisEntry> bins, bool horizontal) :
+ StatsAxis(view, title, horizontal, false),
bin_values(std::move(bins))
{
if (bin_values.size() < 2) // Less than two makes no sense -> there must be at least one category
@@ -446,9 +471,6 @@ std::pair<QString, QString> HistogramAxis::getFirstLastLabel() const
// There, we obviously want to show the years and not the quarters.
void HistogramAxis::updateLabels()
{
- labels.clear();
- ticks.clear();
-
if (bin_values.size() < 2) // Less than two makes no sense -> there must be at least one category
return;
@@ -488,9 +510,10 @@ void HistogramAxis::updateLabels()
if (first != 0)
addTick(bin_values.front().value);
int last = first;
+ QFontMetrics fm(labelFont);
for (int i = first; i < (int)bin_values.size(); i += step) {
const auto &[name, value, recommended] = bin_values[i];
- addLabel(name, value);
+ addLabel(fm, name, value);
addTick(value);
last = i;
}
@@ -617,7 +640,7 @@ static std::vector<HistogramAxisEntry> timeRangeToBins(double from, double to)
return res;
}
-DateAxis::DateAxis(const QString &title, double from, double to, bool horizontal) :
- HistogramAxis(title, timeRangeToBins(from, to), horizontal)
+DateAxis::DateAxis(StatsView &view, const QString &title, double from, double to, bool horizontal) :
+ HistogramAxis(view, title, timeRangeToBins(from, to), horizontal)
{
}
diff --git a/stats/statsaxis.h b/stats/statsaxis.h
index 9d46f753a..02662ddd9 100644
--- a/stats/statsaxis.h
+++ b/stats/statsaxis.h
@@ -2,15 +2,20 @@
#ifndef STATS_AXIS_H
#define STATS_AXIS_H
+#include "chartitem.h"
+#include "statshelper.h"
+
#include <memory>
#include <vector>
#include <QFont>
-#include <QGraphicsSimpleTextItem>
-#include <QGraphicsLineItem>
-class QGraphicsScene;
+class StatsView;
+class ChartLineItem;
+class QFontMetrics;
-class StatsAxis : public QGraphicsLineItem {
+// The labels and the title of the axis are rendered into a pixmap.
+// The ticks and the baseline are realized as individual ChartLineItems.
+class StatsAxis : public ChartPixmapItem {
public:
virtual ~StatsAxis();
// Returns minimum and maximum of shown range, not of data points.
@@ -30,22 +35,25 @@ public:
std::vector<double> ticksPositions() const; // Positions in screen coordinates
protected:
- StatsAxis(const QString &title, bool horizontal, bool labelsBetweenTicks);
+ StatsAxis(StatsView &view, const QString &title, bool horizontal, bool labelsBetweenTicks);
+
+ ChartItemPtr<ChartLineItem> line;
+ QString title;
+ double titleWidth;
struct Label {
- std::unique_ptr<QGraphicsSimpleTextItem> label;
+ QString label;
+ int width;
double pos;
- Label(const QString &name, double pos, QGraphicsScene *scene, const QFont &font);
};
std::vector<Label> labels;
- void addLabel(const QString &label, double pos);
+ void addLabel(const QFontMetrics &fm, const QString &label, double pos);
virtual void updateLabels() = 0;
virtual std::pair<QString, QString> getFirstLastLabel() const = 0;
struct Tick {
- std::unique_ptr<QGraphicsLineItem> item;
+ ChartItemPtr<ChartLineItem> item;
double pos;
- Tick(double pos, QGraphicsScene *scene);
};
std::vector<Tick> ticks;
void addTick(double pos);
@@ -55,9 +63,9 @@ protected:
bool labelsBetweenTicks; // When labels are between ticks, they can be moved closer to the axis
QFont labelFont, titleFont;
- std::unique_ptr<QGraphicsSimpleTextItem> title;
double size; // width for horizontal, height for vertical
double zeroOnScreen;
+ QPointF offset; // Offset of the label and title pixmap with respect to the (0,0) position.
double min, max;
double labelWidth; // Maximum width of labels
private:
@@ -66,7 +74,7 @@ private:
class ValueAxis : public StatsAxis {
public:
- ValueAxis(const QString &title, double min, double max, int decimals, bool horizontal);
+ ValueAxis(StatsView &view, const QString &title, double min, double max, int decimals, bool horizontal);
private:
double min, max;
int decimals;
@@ -76,7 +84,7 @@ private:
class CountAxis : public ValueAxis {
public:
- CountAxis(const QString &title, int count, bool horizontal);
+ CountAxis(StatsView &view, const QString &title, int count, bool horizontal);
private:
int count;
void updateLabels() override;
@@ -85,7 +93,7 @@ private:
class CategoryAxis : public StatsAxis {
public:
- CategoryAxis(const QString &title, const std::vector<QString> &labels, bool horizontal);
+ CategoryAxis(StatsView &view, const QString &title, const std::vector<QString> &labels, bool horizontal);
private:
std::vector<QString> labelsText;
void updateLabels();
@@ -100,7 +108,7 @@ struct HistogramAxisEntry {
class HistogramAxis : public StatsAxis {
public:
- HistogramAxis(const QString &title, std::vector<HistogramAxisEntry> bin_values, bool horizontal);
+ HistogramAxis(StatsView &view, const QString &title, std::vector<HistogramAxisEntry> bin_values, bool horizontal);
private:
void updateLabels() override;
std::pair<QString, QString> getFirstLastLabel() const override;
@@ -110,7 +118,7 @@ private:
class DateAxis : public HistogramAxis {
public:
- DateAxis(const QString &title, double from, double to, bool horizontal);
+ DateAxis(StatsView &view, const QString &title, double from, double to, bool horizontal);
};
#endif
diff --git a/stats/statscolors.h b/stats/statscolors.h
index 050b8a3ab..e1800b550 100644
--- a/stats/statscolors.h
+++ b/stats/statscolors.h
@@ -14,6 +14,14 @@ inline const QColor darkLabelColor(Qt::black);
inline const QColor lightLabelColor(Qt::white);
inline const QColor axisColor(Qt::black);
inline const QColor gridColor(0xcc, 0xcc, 0xcc);
+inline const QColor informationBorderColor(Qt::black);
+inline const QColor informationColor(0xff, 0xff, 0x00, 192); // Note: fourth argument is opacity
+inline const QColor legendColor(0x00, 0x8e, 0xcc, 192); // Note: fourth argument is opacity
+inline const QColor legendBorderColor(Qt::black);
+inline const QColor quartileMarkerColor(Qt::red);
+inline const QColor regressionItemColor(Qt::red);
+inline const QColor meanMarkerColor(Qt::green);
+inline const QColor medianMarkerColor(Qt::red);
QColor binColor(int bin, int numBins);
QColor labelColor(int bin, size_t numBins);
diff --git a/stats/statsgrid.cpp b/stats/statsgrid.cpp
index 66c720c33..f29069341 100644
--- a/stats/statsgrid.cpp
+++ b/stats/statsgrid.cpp
@@ -1,17 +1,15 @@
// SPDX-License-Identifier: GPL-2.0
#include "statsgrid.h"
+#include "chartitem.h"
#include "statsaxis.h"
#include "statscolors.h"
-#include "statshelper.h"
+#include "statsview.h"
#include "zvalues.h"
-#include <QGraphicsLineItem>
-
static const double gridWidth = 1.0;
-static const Qt::PenStyle gridStyle = Qt::SolidLine;
-StatsGrid::StatsGrid(QGraphicsScene *scene, const StatsAxis &xAxis, const StatsAxis &yAxis)
- : scene(scene), xAxis(xAxis), yAxis(yAxis)
+StatsGrid::StatsGrid(StatsView &view, const StatsAxis &xAxis, const StatsAxis &yAxis)
+ : view(view), xAxis(xAxis), yAxis(yAxis)
{
}
@@ -19,18 +17,22 @@ void StatsGrid::updatePositions()
{
std::vector<double> xtics = xAxis.ticksPositions();
std::vector<double> ytics = yAxis.ticksPositions();
+
+ // We probably should be smarter and reuse existing lines.
+ // For now, this does it.
+ for (auto &line: lines)
+ view.deleteChartItem(line);
lines.clear();
+
if (xtics.empty() || ytics.empty())
return;
for (double x: xtics) {
- lines.emplace_back(createItem<QGraphicsLineItem>(scene, x, ytics.front(), x, ytics.back()));
- lines.back()->setPen(QPen(gridColor, gridWidth, gridStyle));
- lines.back()->setZValue(ZValues::grid);
+ lines.push_back(view.createChartItem<ChartLineItem>(ChartZValue::Grid, gridColor, gridWidth));
+ lines.back()->setLine(QPointF(x, ytics.front()), QPointF(x, ytics.back()));
}
for (double y: ytics) {
- lines.emplace_back(createItem<QGraphicsLineItem>(scene, xtics.front(), y, xtics.back(), y));
- lines.back()->setPen(QPen(gridColor, gridWidth, gridStyle));
- lines.back()->setZValue(ZValues::grid);
+ lines.push_back(view.createChartItem<ChartLineItem>(ChartZValue::Grid, gridColor, gridWidth));
+ lines.back()->setLine(QPointF(xtics.front(), y), QPointF(xtics.back(), y));
}
}
diff --git a/stats/statsgrid.h b/stats/statsgrid.h
index 47b48b3ac..696341c0b 100644
--- a/stats/statsgrid.h
+++ b/stats/statsgrid.h
@@ -1,20 +1,21 @@
// SPDX-License-Identifier: GPL-2.0
// The background grid of a chart
+#include "statshelper.h"
+
#include <memory>
#include <vector>
-#include <QVector>
-#include <QGraphicsLineItem>
class StatsAxis;
-class QGraphicsScene;
+class StatsView;
+class ChartLineItem;
class StatsGrid {
public:
- StatsGrid(QGraphicsScene *scene, const StatsAxis &xAxis, const StatsAxis &yAxis);
+ StatsGrid(StatsView &view, const StatsAxis &xAxis, const StatsAxis &yAxis);
void updatePositions();
private:
- QGraphicsScene *scene;
+ StatsView &view;
const StatsAxis &xAxis, &yAxis;
- std::vector<std::unique_ptr<QGraphicsLineItem>> lines;
+ std::vector<ChartItemPtr<ChartLineItem>> lines;
};
diff --git a/stats/statshelper.h b/stats/statshelper.h
index 0ea39763d..6b4a30ab5 100644
--- a/stats/statshelper.h
+++ b/stats/statshelper.h
@@ -1,25 +1,134 @@
// SPDX-License-Identifier: GPL-2.0
-// Helper functions to render the stats. Currently only
-// contains a small template to create scene-items. This
-// is for historical reasons to ease transition from QtCharts
-// and might be removed.
+// Helper functions to render the stats. Currently contains
+// QSGNode template jugglery to overcome API flaws.
#ifndef STATSHELPER_H
+#define STATSHELPER_H
#include <memory>
-#include <QGraphicsScene>
+#include <QSGNode>
-template <typename T, class... Args>
-T *createItem(QGraphicsScene *scene, Args&&... args)
+// A stupid pointer class that initializes to null and can be copy
+// assigned. This is for historical reasons: unique_ptrs to ChartItems
+// were replaced by plain pointers. Instead of nulling the plain pointers
+// in the constructors, use this. Ultimately, we might think about making
+// this thing smarter, once removal of individual ChartItems is implemented.
+template <typename T>
+class ChartItemPtr {
+ friend class StatsView; // Only the stats view can create these pointers
+ T *ptr;
+ ChartItemPtr(T *ptr) : ptr(ptr)
+ {
+ }
+public:
+ ChartItemPtr() : ptr(nullptr)
+ {
+ }
+ ChartItemPtr(const ChartItemPtr &p) : ptr(p.ptr)
+ {
+ }
+ void reset()
+ {
+ ptr = nullptr;
+ }
+ ChartItemPtr &operator=(const ChartItemPtr &p)
+ {
+ ptr = p.ptr;
+ return *this;
+ }
+ operator bool() const
+ {
+ return !!ptr;
+ }
+ bool operator!() const
+ {
+ return !ptr;
+ }
+ T &operator*() const
+ {
+ return *ptr;
+ }
+ T *operator->() const
+ {
+ return ptr;
+ }
+};
+
+// In general, we want chart items to be hideable. For example to show/hide
+// labels on demand. Very sadly, the QSG API is absolutely terrible with
+// respect to temporarily disabling. Instead of simply having a flag,
+// a QSGNode is queried using the "isSubtreeBlocked()" virtual function(!).
+//
+// Not only is this a slow operation performed on every single node, it
+// also is often not possible to override this function: For improved
+// performance, the documentation recommends to create QSG nodes via
+// QQuickWindow. This provides nodes optimized for the actual hardware.
+// However, this obviously means that these nodes cannot be derived from!
+//
+// In that case, there are two possibilities: Add a proxy node with an
+// overridden "isSubtreeBlocked()" function or remove the node from the
+// scene. The former was chosen here, because it is less complex.
+//
+// The following slightly cryptic templates are used to unify the two
+// cases: The QSGNode is generated by our own code or the QSGNode is
+// obtained from QQuickWindow.
+//
+// The "HideableQSGNode<Node>" template augments the QSGNode "Node"
+// by a "setVisible()" function and overrides "isSubtreeBlocked()"
+//
+// The "QSGProxyNode<Node>" template is a QSGNode with a single
+// child of type "Node".
+//
+// Thus, if the node can be created, use:
+// HideableQSGNode<NodeTypeThatCanBeCreated> node
+// and if the node can only be obtained from QQuickWindow, use:
+// HideableQSGNode<QSGProxyNode<NodeThatCantBeCreated>> node
+// The latter should obviously be typedef-ed.
+//
+// Yes, that's all horrible, but if nothing else it teaches us about
+// composition.
+template <typename Node>
+class HideableQSGNode : public Node {
+ bool hidden;
+ bool isSubtreeBlocked() const override final;
+public:
+ template<class... Args>
+ HideableQSGNode(bool visible, Args&&... args);
+ void setVisible(bool visible);
+};
+
+template <typename Node>
+class QSGProxyNode : public QSGNode {
+public:
+ std::unique_ptr<Node> node;
+ QSGProxyNode(Node *node);
+};
+
+// Implementation detail of templates - move to serparate header file
+template <typename Node>
+QSGProxyNode<Node>::QSGProxyNode(Node *node) : node(node)
+{
+ appendChildNode(node);
+}
+
+template <typename Node>
+bool HideableQSGNode<Node>::isSubtreeBlocked() const
+{
+ return hidden;
+}
+
+template <typename Node>
+template<class... Args>
+HideableQSGNode<Node>::HideableQSGNode(bool visible, Args&&... args) :
+ Node(std::forward<Args>(args)...),
+ hidden(!visible)
{
- T *res = new T(std::forward<Args>(args)...);
- scene->addItem(res);
- return res;
}
-template <typename T, class... Args>
-std::unique_ptr<T> createItemPtr(QGraphicsScene *scene, Args&&... args)
+template <typename Node>
+void HideableQSGNode<Node>::setVisible(bool visible)
{
- return std::unique_ptr<T>(createItem<T>(scene, std::forward<Args>(args)...));
+ hidden = !visible;
+ Node::markDirty(QSGNode::DirtySubtreeBlocked);
}
#endif
diff --git a/stats/statsicons.qrc b/stats/statsicons.qrc
new file mode 100644
index 000000000..18a79c921
--- /dev/null
+++ b/stats/statsicons.qrc
@@ -0,0 +1,15 @@
+<RCC>
+ <qresource prefix="/">
+ <file alias="chart-bar-grouped-horizontal-icon">../icons/chart_bar_grouped_horizontal.svg</file>
+ <file alias="chart-bar-grouped-vertical-icon">../icons/chart_bar_grouped_vertical.svg</file>
+ <file alias="chart-bar-stacked-horizontal-icon">../icons/chart_bar_stacked_horizontal.svg</file>
+ <file alias="chart-bar-stacked-vertical-icon">../icons/chart_bar_stacked_vertical.svg</file>
+ <file alias="chart-bar-horizontal-icon">../icons/chart_bar_horizontal.svg</file>
+ <file alias="chart-bar-vertical-icon">../icons/chart_bar_vertical.svg</file>
+ <file alias="chart-box-icon">../icons/chart_box.svg</file>
+ <file alias="chart-pie-icon">../icons/chart_pie.svg</file>
+ <file alias="chart-points-icon">../icons/chart_points.svg</file>
+ <file alias="chart-warning-icon">../icons/warning-icon.svg</file>
+ </qresource>
+</RCC>
+
diff --git a/stats/statsseries.cpp b/stats/statsseries.cpp
index 2b7a5adea..60e54e127 100644
--- a/stats/statsseries.cpp
+++ b/stats/statsseries.cpp
@@ -2,8 +2,8 @@
#include "statsseries.h"
#include "statsaxis.h"
-StatsSeries::StatsSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis) :
- scene(scene), xAxis(xAxis), yAxis(yAxis)
+StatsSeries::StatsSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis) :
+ view(view), xAxis(xAxis), yAxis(yAxis)
{
}
diff --git a/stats/statsseries.h b/stats/statsseries.h
index 2494569e6..360396601 100644
--- a/stats/statsseries.h
+++ b/stats/statsseries.h
@@ -6,18 +6,18 @@
#include <QPointF>
-class QGraphicsScene;
class StatsAxis;
+class StatsView;
class StatsSeries {
public:
- StatsSeries(QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis);
+ StatsSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis);
virtual ~StatsSeries();
virtual void updatePositions() = 0; // Called if chart geometry changes.
virtual bool hover(QPointF pos) = 0; // Called on mouse movement. Return true if an item of this series is highlighted.
virtual void unhighlight() = 0; // Unhighlight any highlighted item.
protected:
- QGraphicsScene *scene;
+ StatsView &view;
StatsAxis *xAxis, *yAxis; // May be zero for charts without axes (pie charts).
QPointF toScreen(QPointF p);
};
diff --git a/stats/statsstate.cpp b/stats/statsstate.cpp
index 130a0e5a8..65d5c8656 100644
--- a/stats/statsstate.cpp
+++ b/stats/statsstate.cpp
@@ -23,11 +23,13 @@ enum class SupportedVariable {
Numeric
};
-static const int ChartFeatureLabels = 1 << 0;
-static const int ChartFeatureLegend = 1 << 1;
-static const int ChartFeatureMedian = 1 << 2;
-static const int ChartFeatureMean = 1 << 3;
-static const int ChartFeatureQuartiles = 1 << 4;
+static const int ChartFeatureLabels = 1 << 0;
+static const int ChartFeatureLegend = 1 << 1;
+static const int ChartFeatureMedian = 1 << 2;
+static const int ChartFeatureMean = 1 << 3;
+static const int ChartFeatureQuartiles = 1 << 4;
+static const int ChartFeatureRegression = 1 << 5;
+static const int ChartFeatureConfidence = 1 << 6;
static const struct ChartTypeDesc {
ChartType id;
@@ -45,7 +47,7 @@ static const struct ChartTypeDesc {
SupportedVariable::Numeric,
false, false, false,
{ ChartSubType::Dots },
- 0
+ ChartFeatureRegression | ChartFeatureConfidence
},
{
ChartType::HistogramCount,
@@ -161,6 +163,8 @@ StatsState::StatsState() :
median(false),
mean(false),
quartiles(true),
+ regression(true),
+ confidence(true),
var1Binner(nullptr),
var2Binner(nullptr),
var2Operation(StatsOperation::Invalid)
@@ -353,19 +357,23 @@ static StatsState::VariableList createOperationsList(const StatsVariable *var, S
return res;
}
-static std::vector<StatsState::Feature> createFeaturesList(int chartFeatures, bool labels, bool legend, bool median, bool mean, bool quartiles)
+static std::vector<StatsState::Feature> createFeaturesList(int chartFeatures, const StatsState &state)
{
std::vector<StatsState::Feature> res;
if (chartFeatures & ChartFeatureLabels)
- res.push_back({ StatsTranslations::tr("labels"), ChartFeatureLabels, labels });
+ res.push_back({ StatsTranslations::tr("labels"), ChartFeatureLabels, state.labels });
if (chartFeatures & ChartFeatureLegend)
- res.push_back({ StatsTranslations::tr("legend"), ChartFeatureLegend, legend });
+ res.push_back({ StatsTranslations::tr("legend"), ChartFeatureLegend, state.legend });
if (chartFeatures & ChartFeatureMedian)
- res.push_back({ StatsTranslations::tr("median"), ChartFeatureMedian, median });
+ res.push_back({ StatsTranslations::tr("median"), ChartFeatureMedian, state.median });
if (chartFeatures & ChartFeatureMean)
- res.push_back({ StatsTranslations::tr("mean"), ChartFeatureMean, mean });
+ res.push_back({ StatsTranslations::tr("mean"), ChartFeatureMean, state.mean });
if (chartFeatures & ChartFeatureQuartiles)
- res.push_back({ StatsTranslations::tr("quartiles"), ChartFeatureQuartiles, quartiles });
+ res.push_back({ StatsTranslations::tr("quartiles"), ChartFeatureQuartiles, state.quartiles });
+ if (chartFeatures & ChartFeatureRegression)
+ res.push_back({ StatsTranslations::tr("linear regression"), ChartFeatureRegression, state.regression });
+ if (chartFeatures & ChartFeatureConfidence)
+ res.push_back({ StatsTranslations::tr("95% confidence area"), ChartFeatureConfidence, state.confidence });
return res;
}
@@ -381,7 +389,7 @@ StatsState::UIState StatsState::getUIState() const
// Second variable can only be binned if first variable is binned.
res.binners2 = createBinnerList(var2, var2Binner, var1Binner != nullptr, true);
res.operations2 = createOperationsList(var2, var2Operation, var1Binner);
- res.features = createFeaturesList(chartFeatures, labels, legend, median, mean, quartiles);
+ res.features = createFeaturesList(chartFeatures, *this);
return res;
}
@@ -471,6 +479,10 @@ void StatsState::featureChanged(int id, bool state)
mean = state;
else if (id == ChartFeatureQuartiles)
quartiles = state;
+ else if (id == ChartFeatureRegression)
+ regression = state;
+ else if (id == ChartFeatureConfidence)
+ confidence = state;
}
// Creates the new chart-type from the current chart-type and a list of possible chart types.
diff --git a/stats/statsstate.h b/stats/statsstate.h
index 1d8fe0b05..8fa6bb176 100644
--- a/stats/statsstate.h
+++ b/stats/statsstate.h
@@ -108,6 +108,8 @@ public:
bool median;
bool mean;
bool quartiles;
+ bool regression;
+ bool confidence;
const StatsBinner *var1Binner; // nullptr: undefined
const StatsBinner *var2Binner; // nullptr: undefined
StatsOperation var2Operation;
diff --git a/stats/statsview.cpp b/stats/statsview.cpp
index be296cf44..6560360cc 100644
--- a/stats/statsview.cpp
+++ b/stats/statsview.cpp
@@ -2,8 +2,11 @@
#include "statsview.h"
#include "barseries.h"
#include "boxseries.h"
+#include "histogrammarker.h"
#include "legend.h"
#include "pieseries.h"
+#include "quartilemarker.h"
+#include "regressionitem.h"
#include "scatterseries.h"
#include "statsaxis.h"
#include "statscolors.h"
@@ -17,77 +20,220 @@
#include "core/subsurface-qt/divelistnotifier.h"
#include <cmath>
-#include <QGraphicsScene>
-#include <QGraphicsSceneHoverEvent>
-#include <QGraphicsSimpleTextItem>
#include <QQuickItem>
#include <QQuickWindow>
#include <QSGImageNode>
+#include <QSGRectangleNode>
#include <QSGTexture>
+// Constants that control the graph layouts
+static const double sceneBorder = 5.0; // Border between scene edges and statitistics view
+static const double titleBorder = 2.0; // Border between title and chart
+
+StatsView::StatsView(QQuickItem *parent) : QQuickItem(parent),
+ highlightedSeries(nullptr),
+ xAxis(nullptr),
+ yAxis(nullptr),
+ draggedItem(nullptr),
+ rootNode(nullptr)
+{
+ setFlag(ItemHasContents, true);
+
+ connect(&diveListNotifier, &DiveListNotifier::numShownChanged, this, &StatsView::replotIfVisible);
+
+ setAcceptHoverEvents(true);
+ setAcceptedMouseButtons(Qt::LeftButton);
+
+ QFont font;
+ titleFont = QFont(font.family(), font.pointSize(), QFont::Light); // Make configurable
+}
+
+StatsView::StatsView() : StatsView(nullptr)
+{
+}
+
+StatsView::~StatsView()
+{
+}
+
+void StatsView::mousePressEvent(QMouseEvent *event)
+{
+ // Currently, we only support dragging of the legend. If other objects
+ // should be made draggable, this needs to be generalized.
+ if (legend) {
+ QPointF pos = event->localPos();
+ QRectF rect = legend->getRect();
+ if (legend->getRect().contains(pos)) {
+ dragStartMouse = pos;
+ dragStartItem = rect.topLeft();
+ draggedItem = &*legend;
+ grabMouse();
+ setKeepMouseGrab(true); // don't allow Qt to steal the grab
+ }
+ }
+}
+
+void StatsView::mouseReleaseEvent(QMouseEvent *)
+{
+ if (draggedItem) {
+ draggedItem = nullptr;
+ ungrabMouse();
+ }
+}
+
+// Define a hideable dummy QSG node that is used as a parent node to make
+// all objects of a z-level visible / invisible.
+using ZNode = HideableQSGNode<QSGNode>;
+
+class RootNode : public QSGNode
+{
+public:
+ RootNode(QQuickWindow *w);
+ std::unique_ptr<QSGRectangleNode> backgroundNode; // solid background
+ // We entertain one node per Z-level.
+ std::array<std::unique_ptr<ZNode>, (size_t)ChartZValue::Count> zNodes;
+};
+
+RootNode::RootNode(QQuickWindow *w)
+{
+ // Add a background rectangle with a solid color. This could
+ // also be done on the widget level, but would have to be done
+ // separately for desktop and mobile, so do it here.
+ backgroundNode.reset(w->createRectangleNode());
+ backgroundNode->setColor(backgroundColor);
+ appendChildNode(backgroundNode.get());
+
+ for (auto &zNode: zNodes) {
+ zNode.reset(new ZNode(true));
+ appendChildNode(zNode.get());
+ }
+}
+
QSGNode *StatsView::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *)
{
// The QtQuick drawing interface is utterly bizzare with a distinct 1980ies-style memory management.
// This is just a copy of what is found in Qt's documentation.
- QSGImageNode *n = static_cast<QSGImageNode *>(oldNode);
+ RootNode *n = static_cast<RootNode *>(oldNode);
if (!n)
- n = window()->createImageNode();
+ n = rootNode = new RootNode(window());
+
+ // Delete all chart items that are marked for deletion.
+ ChartItem *nextitem;
+ for (ChartItem *item = deletedItems.first; item; item = nextitem) {
+ nextitem = item->next;
+ delete item;
+ }
+ deletedItems.clear();
QRectF rect = boundingRect();
if (plotRect != rect) {
plotRect = rect;
+ rootNode->backgroundNode->setRect(rect);
plotAreaChanged(plotRect.size());
}
- img->fill(backgroundColor);
- scene.render(painter.get());
- texture.reset(window()->createTextureFromImage(*img, QQuickWindow::TextureIsOpaque));
- n->setTexture(texture.get());
- n->setRect(rect);
+ for (ChartItem *item = dirtyItems.first; item; item = item->next) {
+ item->render();
+ item->dirty = false;
+ }
+ dirtyItems.splice(cleanItems);
+
return n;
}
-// Constants that control the graph layouts
-static const QColor quartileMarkerColor(Qt::red);
-static const double quartileMarkerSize = 15.0;
-static const double sceneBorder = 5.0; // Border between scene edges and statitistics view
-static const double titleBorder = 2.0; // Border between title and chart
+void StatsView::addQSGNode(QSGNode *node, ChartZValue z)
+{
+ int idx = std::clamp((int)z, 0, (int)ChartZValue::Count - 1);
+ rootNode->zNodes[idx]->appendChildNode(node);
+}
-StatsView::StatsView(QQuickItem *parent) : QQuickItem(parent),
- highlightedSeries(nullptr),
- xAxis(nullptr),
- yAxis(nullptr)
+void StatsView::registerChartItem(ChartItem &item)
{
- setFlag(ItemHasContents, true);
+ cleanItems.append(item);
+}
- connect(&diveListNotifier, &DiveListNotifier::numShownChanged, this, &StatsView::replotIfVisible);
+void StatsView::registerDirtyChartItem(ChartItem &item)
+{
+ if (item.dirty)
+ return;
+ cleanItems.remove(item);
+ dirtyItems.append(item);
+ item.dirty = true;
+}
- setAcceptHoverEvents(true);
+void StatsView::deleteChartItemInternal(ChartItem &item)
+{
+ if (item.dirty)
+ dirtyItems.remove(item);
+ else
+ cleanItems.remove(item);
+ deletedItems.append(item);
+}
- QFont font;
- titleFont = QFont(font.family(), font.pointSize(), QFont::Light); // Make configurable
+StatsView::ChartItemList::ChartItemList() : first(nullptr), last(nullptr)
+{
}
-StatsView::StatsView() : StatsView(nullptr)
+void StatsView::ChartItemList::clear()
{
+ first = last = nullptr;
}
-StatsView::~StatsView()
+void StatsView::ChartItemList::remove(ChartItem &item)
{
+ if (item.next)
+ item.next->prev = item.prev;
+ else
+ last = item.prev;
+ if (item.prev)
+ item.prev->next = item.next;
+ else
+ first = item.next;
+ item.prev = item.next = nullptr;
}
-void StatsView::plotAreaChanged(const QSizeF &s)
+void StatsView::ChartItemList::append(ChartItem &item)
+{
+ if (!first) {
+ first = &item;
+ } else {
+ item.prev = last;
+ last->next = &item;
+ }
+ last = &item;
+}
+
+void StatsView::ChartItemList::splice(ChartItemList &l2)
+{
+ if (!first) // if list is empty -> nothing to do.
+ return;
+ if (!l2.first) {
+ l2 = *this;
+ } else {
+ l2.last->next = first;
+ first->prev = l2.last;
+ l2.last = last;
+ }
+ clear();
+}
+
+QQuickWindow *StatsView::w() const
+{
+ return window();
+}
+
+QSizeF StatsView::size() const
+{
+ return boundingRect().size();
+}
+
+QRectF StatsView::plotArea() const
{
- // Make sure that image is at least one pixel wide / high, otherwise
- // the painter starts acting up.
- int w = std::max(1, static_cast<int>(floor(s.width())));
- int h = std::max(1, static_cast<int>(floor(s.height())));
- scene.setSceneRect(QRectF(0, 0, static_cast<double>(w), static_cast<double>(h)));
- painter.reset();
- img.reset(new QImage(w, h, QImage::Format_RGB32));
- painter.reset(new QPainter(img.get()));
- painter->setRenderHint(QPainter::Antialiasing);
+ return plotRect;
+}
+void StatsView::plotAreaChanged(const QSizeF &s)
+{
double left = sceneBorder;
double top = sceneBorder;
double right = s.width() - sceneBorder;
@@ -95,7 +241,7 @@ void StatsView::plotAreaChanged(const QSizeF &s)
const double minSize = 30.0;
if (title)
- top += title->boundingRect().height() + titleBorder;
+ top += title->getRect().height() + titleBorder;
// Currently, we only have either none, or an x- and a y-axis
std::pair<double,double> horizontalSpace{ 0.0, 0.0 };
if (xAxis) {
@@ -123,12 +269,14 @@ void StatsView::plotAreaChanged(const QSizeF &s)
grid->updatePositions();
for (auto &series: series)
series->updatePositions();
- for (QuartileMarker &marker: quartileMarkers)
- marker.updatePosition();
- for (RegressionLine &line: regressionLines)
- line.updatePosition();
- for (HistogramMarker &marker: histogramMarkers)
- marker.updatePosition();
+ for (auto &marker: quartileMarkers)
+ marker->updatePosition();
+ if (regressionItem)
+ regressionItem->updatePosition();
+ if (meanMarker)
+ meanMarker->updatePosition();
+ if (medianMarker)
+ medianMarker->updatePosition();
if (legend)
legend->resize();
updateTitlePos();
@@ -140,13 +288,26 @@ void StatsView::replotIfVisible()
plot(state);
}
+void StatsView::mouseMoveEvent(QMouseEvent *event)
+{
+ if (!draggedItem)
+ return;
+
+ QSizeF sceneSize = size();
+ if (sceneSize.width() <= 1.0 || sceneSize.height() <= 1.0)
+ return;
+ draggedItem->setPos(event->pos() - dragStartMouse + dragStartItem);
+ update();
+}
+
void StatsView::hoverEnterEvent(QHoverEvent *)
{
}
void StatsView::hoverMoveEvent(QHoverEvent *event)
{
- QPointF pos(event->pos());
+ QPointF pos = event->pos();
+
for (auto &series: series) {
if (series->hover(pos)) {
if (series.get() != highlightedSeries) {
@@ -169,7 +330,7 @@ void StatsView::hoverMoveEvent(QHoverEvent *event)
template <typename T, class... Args>
T *StatsView::createSeries(Args&&... args)
{
- T *res = new T(&scene, xAxis, yAxis, std::forward<Args>(args)...);
+ T *res = new T(*this, xAxis, yAxis, std::forward<Args>(args)...);
series.emplace_back(res);
series.back()->updatePositions();
return res;
@@ -177,29 +338,26 @@ T *StatsView::createSeries(Args&&... args)
void StatsView::setTitle(const QString &s)
{
- if (s.isEmpty()) {
- title.reset();
+ if (title) {
+ // Ooops. Currently we do not support setting the title twice.
return;
}
- title = createItemPtr<QGraphicsSimpleTextItem>(&scene, s);
- title->setFont(titleFont);
+ title = createChartItem<ChartTextItem>(ChartZValue::Legend, titleFont, s);
+ title->setColor(darkLabelColor);
}
void StatsView::updateTitlePos()
{
if (!title)
return;
- QRectF rect = scene.sceneRect();
- title->setPos(sceneBorder + (rect.width() - title->boundingRect().width()) / 2.0,
- sceneBorder);
+ title->setPos(QPointF(round(sceneBorder + (boundingRect().width() - title->getRect().width()) / 2.0),
+ round(sceneBorder)));
}
template <typename T, class... Args>
T *StatsView::createAxis(const QString &title, Args&&... args)
{
- T *res = createItem<T>(&scene, title, std::forward<Args>(args)...);
- axes.emplace_back(res);
- return res;
+ return &*createChartItem<T>(title, std::forward<Args>(args)...);
}
void StatsView::setAxes(StatsAxis *x, StatsAxis *y)
@@ -207,28 +365,42 @@ void StatsView::setAxes(StatsAxis *x, StatsAxis *y)
xAxis = x;
yAxis = y;
if (x && y)
- grid = std::make_unique<StatsGrid>(&scene, *x, *y);
+ grid = std::make_unique<StatsGrid>(*this, *x, *y);
}
void StatsView::reset()
{
highlightedSeries = nullptr;
xAxis = yAxis = nullptr;
+ draggedItem = nullptr;
+ title.reset();
legend.reset();
+ regressionItem.reset();
+ meanMarker.reset();
+ medianMarker.reset();
+
+ // Mark clean and dirty chart items for deletion
+ cleanItems.splice(deletedItems);
+ dirtyItems.splice(deletedItems);
+
series.clear();
quartileMarkers.clear();
- regressionLines.clear();
- histogramMarkers.clear();
grid.reset();
- axes.clear();
- title.reset();
}
void StatsView::plot(const StatsState &stateIn)
{
state = stateIn;
plotChart();
- plotAreaChanged(scene.sceneRect().size());
+ updateFeatures(); // Show / hide chart features, such as legend, etc.
+ plotAreaChanged(boundingRect().size());
+ update();
+}
+
+void StatsView::updateFeatures(const StatsState &stateIn)
+{
+ state = stateIn;
+ updateFeatures();
update();
}
@@ -242,27 +414,26 @@ void StatsView::plotChart()
switch (state.type) {
case ChartType::DiscreteBar:
return plotBarChart(dives, state.subtype, state.var1, state.var1Binner, state.var2,
- state.var2Binner, state.labels, state.legend);
+ state.var2Binner);
case ChartType::DiscreteValue:
return plotValueChart(dives, state.subtype, state.var1, state.var1Binner, state.var2,
- state.var2Operation, state.labels);
+ state.var2Operation);
case ChartType::DiscreteCount:
- return plotDiscreteCountChart(dives, state.subtype, state.var1, state.var1Binner, state.labels);
+ return plotDiscreteCountChart(dives, state.subtype, state.var1, state.var1Binner);
case ChartType::Pie:
- return plotPieChart(dives, state.var1, state.var1Binner, state.labels, state.legend);
+ return plotPieChart(dives, state.var1, state.var1Binner);
case ChartType::DiscreteBox:
return plotDiscreteBoxChart(dives, state.var1, state.var1Binner, state.var2);
case ChartType::DiscreteScatter:
- return plotDiscreteScatter(dives, state.var1, state.var1Binner, state.var2, state.quartiles);
+ return plotDiscreteScatter(dives, state.var1, state.var1Binner, state.var2);
case ChartType::HistogramCount:
- return plotHistogramCountChart(dives, state.subtype, state.var1, state.var1Binner,
- state.labels, state.median, state.mean);
+ return plotHistogramCountChart(dives, state.subtype, state.var1, state.var1Binner);
case ChartType::HistogramValue:
return plotHistogramValueChart(dives, state.subtype, state.var1, state.var1Binner, state.var2,
- state.var2Operation, state.labels);
+ state.var2Operation);
case ChartType::HistogramStacked:
return plotHistogramStackedChart(dives, state.subtype, state.var1, state.var1Binner,
- state.var2, state.var2Binner, state.labels, state.legend);
+ state.var2, state.var2Binner);
case ChartType::HistogramBox:
return plotHistogramBoxChart(dives, state.var1, state.var1Binner, state.var2);
case ChartType::ScatterPlot:
@@ -275,6 +446,30 @@ void StatsView::plotChart()
}
}
+void StatsView::updateFeatures()
+{
+ if (legend)
+ legend->setVisible(state.legend);
+
+ // For labels, we are brutal: simply show/hide the whole z-level with the labels
+ if (rootNode)
+ rootNode->zNodes[(int)ChartZValue::SeriesLabels]->setVisible(state.labels);
+
+ if (meanMarker)
+ meanMarker->setVisible(state.mean);
+
+ if (medianMarker)
+ medianMarker->setVisible(state.median);
+
+ if (regressionItem) {
+ regressionItem->setVisible(state.regression || state.confidence);
+ if (state.regression || state.confidence)
+ regressionItem->setFeatures(state.regression, state.confidence);
+ }
+ for (ChartItemPtr<QuartileMarker> &marker: quartileMarkers)
+ marker->setVisible(state.quartiles);
+}
+
template<typename T>
CategoryAxis *StatsView::createCategoryAxis(const QString &name, const StatsBinner &binner,
const std::vector<T> &bins, bool isHorizontal)
@@ -361,14 +556,12 @@ static std::vector<QString> makePercentageLabels(int count, int total, bool isHo
// From a list of counts, make (count, label) pairs, where the label
// formats the total number and the percentage of dives.
-static std::vector<std::pair<int, std::vector<QString>>> makeCountLabels(const std::vector<int> &counts, int total,
- bool labels, bool isHorizontal)
+static std::vector<std::pair<int, std::vector<QString>>> makeCountLabels(const std::vector<int> &counts, int total, bool isHorizontal)
{
std::vector<std::pair<int, std::vector<QString>>> count_labels;
count_labels.reserve(counts.size());
for (int count: counts) {
- std::vector<QString> label = labels ? makePercentageLabels(count, total, isHorizontal)
- : std::vector<QString>();
+ std::vector<QString> label = makePercentageLabels(count, total, isHorizontal);
count_labels.push_back(std::make_pair(count, label));
}
return count_labels;
@@ -377,7 +570,7 @@ static std::vector<std::pair<int, std::vector<QString>>> makeCountLabels(const s
void StatsView::plotBarChart(const std::vector<dive *> &dives,
ChartSubType subType,
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
- const StatsVariable *valueVariable, const StatsBinner *valueBinner, bool labels, bool showLegend)
+ const StatsVariable *valueVariable, const StatsBinner *valueBinner)
{
if (!categoryBinner || !valueBinner)
return;
@@ -405,14 +598,13 @@ void StatsView::plotBarChart(const std::vector<dive *> &dives,
setAxes(catAxis, valAxis);
// Paint legend first, because the bin-names will be moved away from.
- if (showLegend)
- legend = createItemPtr<Legend>(&scene, data.vbinNames);
+ legend = createChartItem<Legend>(data.vbinNames);
std::vector<BarSeries::MultiItem> items;
items.reserve(data.hbin_counts.size());
double pos = 0.0;
for (auto &[hbin, counts, total]: data.hbin_counts) {
- items.push_back({ pos - 0.5, pos + 0.5, makeCountLabels(counts, total, labels, isHorizontal),
+ items.push_back({ pos - 0.5, pos + 0.5, makeCountLabels(counts, total, isHorizontal),
categoryBinner->formatWithUnit(*hbin) });
pos += 1.0;
}
@@ -489,8 +681,7 @@ static std::pair<double, double> getMinMaxValue(const std::vector<StatsBinOp> &b
void StatsView::plotValueChart(const std::vector<dive *> &dives,
ChartSubType subType,
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
- const StatsVariable *valueVariable, StatsOperation valueAxisOperation,
- bool labels)
+ const StatsVariable *valueVariable, StatsOperation valueAxisOperation)
{
if (!categoryBinner)
return;
@@ -525,8 +716,7 @@ void StatsView::plotValueChart(const std::vector<dive *> &dives,
if (res.isValid()) {
double height = res.get(valueAxisOperation);
QString value = QString("%L1").arg(height, 0, 'f', decimals);
- std::vector<QString> label = labels ? std::vector<QString> { value }
- : std::vector<QString>();
+ std::vector<QString> label = std::vector<QString> { value };
items.push_back({ pos - 0.5, pos + 0.5, height, label,
categoryBinner->formatWithUnit(*bin), res });
}
@@ -557,8 +747,7 @@ static int getMaxCount(const std::vector<T> &bins)
void StatsView::plotDiscreteCountChart(const std::vector<dive *> &dives,
ChartSubType subType,
- const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
- bool labels)
+ const StatsVariable *categoryVariable, const StatsBinner *categoryBinner)
{
if (!categoryBinner)
return;
@@ -589,8 +778,7 @@ void StatsView::plotDiscreteCountChart(const std::vector<dive *> &dives,
items.reserve(categoryBins.size());
double pos = 0.0;
for (auto const &[bin, count]: categoryBins) {
- std::vector<QString> label = labels ? makePercentageLabels(count, total, isHorizontal)
- : std::vector<QString>();
+ std::vector<QString> label = makePercentageLabels(count, total, isHorizontal);
items.push_back({ pos - 0.5, pos + 0.5, count, label,
categoryBinner->formatWithUnit(*bin), total });
pos += 1.0;
@@ -600,8 +788,7 @@ void StatsView::plotDiscreteCountChart(const std::vector<dive *> &dives,
}
void StatsView::plotPieChart(const std::vector<dive *> &dives,
- const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
- bool labels, bool showLegend)
+ const StatsVariable *categoryVariable, const StatsBinner *categoryBinner)
{
if (!categoryBinner)
return;
@@ -620,10 +807,9 @@ void StatsView::plotPieChart(const std::vector<dive *> &dives,
data.emplace_back(categoryBinner->formatWithUnit(*bin), count);
bool keepOrder = categoryVariable->type() != StatsVariable::Type::Discrete;
- PieSeries *series = createSeries<PieSeries>(categoryVariable->name(), data, keepOrder, labels);
+ PieSeries *series = createSeries<PieSeries>(categoryVariable->name(), data, keepOrder);
- if (showLegend)
- legend = createItemPtr<Legend>(&scene, series->binNames());
+ legend = createChartItem<Legend>(series->binNames());
}
void StatsView::plotDiscreteBoxChart(const std::vector<dive *> &dives,
@@ -662,7 +848,7 @@ void StatsView::plotDiscreteBoxChart(const std::vector<dive *> &dives,
void StatsView::plotDiscreteScatter(const std::vector<dive *> &dives,
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
- const StatsVariable *valueVariable, bool quartiles)
+ const StatsVariable *valueVariable)
{
if (!categoryBinner)
return;
@@ -690,118 +876,19 @@ void StatsView::plotDiscreteScatter(const std::vector<dive *> &dives,
for (const auto &[bin, array]: categoryBins) {
for (auto [v, d]: array)
series->append(d, x, v);
- if (quartiles) {
- StatsQuartiles quartiles = StatsVariable::quartiles(array);
- if (quartiles.isValid()) {
- quartileMarkers.emplace_back(x, quartiles.q1, &scene, catAxis, valAxis);
- quartileMarkers.emplace_back(x, quartiles.q2, &scene, catAxis, valAxis);
- quartileMarkers.emplace_back(x, quartiles.q3, &scene, catAxis, valAxis);
- }
+ StatsQuartiles quartiles = StatsVariable::quartiles(array);
+ if (quartiles.isValid()) {
+ quartileMarkers.push_back(createChartItem<QuartileMarker>(
+ x, quartiles.q1, catAxis, valAxis));
+ quartileMarkers.push_back(createChartItem<QuartileMarker>(
+ x, quartiles.q2, catAxis, valAxis));
+ quartileMarkers.push_back(createChartItem<QuartileMarker>(
+ x, quartiles.q3, catAxis, valAxis));
}
x += 1.0;
}
}
-StatsView::QuartileMarker::QuartileMarker(double pos, double value, QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis) :
- item(createItemPtr<QGraphicsLineItem>(scene)),
- xAxis(xAxis), yAxis(yAxis),
- pos(pos),
- value(value)
-{
- item->setZValue(ZValues::chartFeatures);
- item->setPen(QPen(quartileMarkerColor, 2.0));
- updatePosition();
-}
-
-void StatsView::QuartileMarker::updatePosition()
-{
- if (!xAxis || !yAxis)
- return;
- double x = xAxis->toScreen(pos);
- double y = yAxis->toScreen(value);
- item->setLine(x - quartileMarkerSize / 2.0, y,
- x + quartileMarkerSize / 2.0, y);
-}
-
-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 + 1.960 * 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 - 1.960 * 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));
-}
-
-StatsView::HistogramMarker::HistogramMarker(double val, bool horizontal, QPen pen, QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis) :
- item(createItemPtr<QGraphicsLineItem>(scene)),
- xAxis(xAxis), yAxis(yAxis),
- val(val), horizontal(horizontal)
-{
- item->setZValue(ZValues::chartFeatures);
- item->setPen(pen);
-}
-
-void StatsView::HistogramMarker::updatePosition()
-{
- if (!xAxis || !yAxis)
- return;
- if (horizontal) {
- double y = yAxis->toScreen(val);
- auto [x1, x2] = xAxis->minMaxScreen();
- item->setLine(x1, y, x2, y);
- } else {
- double x = xAxis->toScreen(val);
- auto [y1, y2] = yAxis->minMaxScreen();
- item->setLine(x, y1, x, y2);
- }
-}
-
-void StatsView::addHistogramMarker(double pos, const QPen &pen, bool isHorizontal, StatsAxis *xAxis, StatsAxis *yAxis)
-{
- histogramMarkers.emplace_back(pos, isHorizontal, pen, &scene, 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>
@@ -826,8 +913,7 @@ HistogramAxis *StatsView::createHistogramAxis(const QString &name, const StatsBi
void StatsView::plotHistogramCountChart(const std::vector<dive *> &dives,
ChartSubType subType,
- const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
- bool labels, bool showMedian, bool showMean)
+ const StatsVariable *categoryVariable, const StatsBinner *categoryBinner)
{
if (!categoryBinner)
return;
@@ -860,8 +946,7 @@ void StatsView::plotHistogramCountChart(const std::vector<dive *> &dives,
for (auto const &[bin, count]: categoryBins) {
double lowerBound = categoryBinner->lowerBoundToFloat(*bin);
double upperBound = categoryBinner->upperBoundToFloat(*bin);
- std::vector<QString> label = labels ? makePercentageLabels(count, total, isHorizontal)
- : std::vector<QString>();
+ std::vector<QString> label = makePercentageLabels(count, total, isHorizontal);
items.push_back({ lowerBound, upperBound, count, label,
categoryBinner->formatWithUnit(*bin), total });
@@ -870,28 +955,19 @@ void StatsView::plotHistogramCountChart(const std::vector<dive *> &dives,
createSeries<BarSeries>(isHorizontal, categoryVariable->name(), items);
if (categoryVariable->type() == StatsVariable::Type::Numeric) {
- if (showMean) {
- double mean = categoryVariable->mean(dives);
- QPen meanPen(Qt::green);
- meanPen.setWidth(2);
- if (!std::isnan(mean))
- addHistogramMarker(mean, meanPen, isHorizontal, xAxis, yAxis);
- }
- if (showMedian) {
- double median = categoryVariable->quartiles(dives).q2;
- QPen medianPen(Qt::red);
- medianPen.setWidth(2);
- if (!std::isnan(median))
- addHistogramMarker(median, medianPen, isHorizontal, xAxis, yAxis);
- }
+ double mean = categoryVariable->mean(dives);
+ if (!std::isnan(mean))
+ meanMarker = createChartItem<HistogramMarker>(mean, isHorizontal, meanMarkerColor, xAxis, yAxis);
+ double median = categoryVariable->quartiles(dives).q2;
+ if (!std::isnan(median))
+ medianMarker = createChartItem<HistogramMarker>(median, isHorizontal, medianMarkerColor, xAxis, yAxis);
}
}
void StatsView::plotHistogramValueChart(const std::vector<dive *> &dives,
ChartSubType subType,
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
- const StatsVariable *valueVariable, StatsOperation valueAxisOperation,
- bool labels)
+ const StatsVariable *valueVariable, StatsOperation valueAxisOperation)
{
if (!categoryBinner)
return;
@@ -930,8 +1006,7 @@ void StatsView::plotHistogramValueChart(const std::vector<dive *> &dives,
double lowerBound = categoryBinner->lowerBoundToFloat(*bin);
double upperBound = categoryBinner->upperBoundToFloat(*bin);
QString value = QString("%L1").arg(height, 0, 'f', decimals);
- std::vector<QString> label = labels ? std::vector<QString> { value }
- : std::vector<QString>();
+ std::vector<QString> label = std::vector<QString> { value };
items.push_back({ lowerBound, upperBound, height, label,
categoryBinner->formatWithUnit(*bin), res });
}
@@ -942,7 +1017,7 @@ void StatsView::plotHistogramValueChart(const std::vector<dive *> &dives,
void StatsView::plotHistogramStackedChart(const std::vector<dive *> &dives,
ChartSubType subType,
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
- const StatsVariable *valueVariable, const StatsBinner *valueBinner, bool labels, bool showLegend)
+ const StatsVariable *valueVariable, const StatsBinner *valueBinner)
{
if (!categoryBinner || !valueBinner)
return;
@@ -958,8 +1033,7 @@ void StatsView::plotHistogramStackedChart(const std::vector<dive *> &dives,
*categoryBinner, categoryBins, !isHorizontal);
BarPlotData data(categoryBins, *valueBinner);
- if (showLegend)
- legend = createItemPtr<Legend>(&scene, data.vbinNames);
+ legend = createChartItem<Legend>(data.vbinNames);
CountAxis *valAxis = createCountAxis(data.maxCategoryCount, isHorizontal);
@@ -974,7 +1048,7 @@ void StatsView::plotHistogramStackedChart(const std::vector<dive *> &dives,
for (auto &[hbin, counts, total]: data.hbin_counts) {
double lowerBound = categoryBinner->lowerBoundToFloat(*hbin);
double upperBound = categoryBinner->upperBoundToFloat(*hbin);
- items.push_back({ lowerBound, upperBound, makeCountLabels(counts, total, labels, isHorizontal),
+ items.push_back({ lowerBound, upperBound, makeCountLabels(counts, total, isHorizontal),
categoryBinner->formatWithUnit(*hbin) });
}
@@ -1110,5 +1184,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 b1e178565..0af91b382 100644
--- a/stats/statsview.h
+++ b/stats/statsview.h
@@ -3,13 +3,12 @@
#define STATS_VIEW_H
#include "statsstate.h"
+#include "statshelper.h"
#include <memory>
#include <QFont>
-#include <QGraphicsScene>
#include <QImage>
#include <QPainter>
#include <QQuickItem>
-#include <QGraphicsPolygonItem>
struct dive;
struct StatsBinner;
@@ -17,27 +16,25 @@ struct StatsBin;
struct StatsState;
struct StatsVariable;
-class QGraphicsLineItem;
-class QGraphicsSimpleTextItem;
class StatsSeries;
class CategoryAxis;
+class ChartItem;
+class ChartTextItem;
class CountAxis;
class HistogramAxis;
+class HistogramMarker;
+class QuartileMarker;
+class RegressionItem;
class StatsAxis;
class StatsGrid;
class Legend;
class QSGTexture;
+class RootNode; // Internal implementation detail
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:
@@ -46,16 +43,32 @@ public:
~StatsView();
void plot(const StatsState &state);
+ void updateFeatures(const StatsState &state); // Updates the visibility of chart features, such as legend, regression, etc.
+ QQuickWindow *w() const; // Make window available to items
+ QSizeF size() const;
+ QRectF plotArea() const;
+ void addQSGNode(QSGNode *node, ChartZValue z); // Must only be called in render thread!
+ void registerChartItem(ChartItem &item);
+ void registerDirtyChartItem(ChartItem &item);
+
+ // Create a chart item and add it to the scene.
+ // The item must not be deleted by the caller, but can be
+ // scheduled for deletion using deleteChartItem() below.
+ // Most items can be made invisible, which is preferred over deletion.
+ // All items on the scene will be deleted once the chart is reset.
+ template <typename T, class... Args>
+ ChartItemPtr<T> createChartItem(Args&&... args);
+
+ template <typename T>
+ void deleteChartItem(ChartItemPtr<T> &item);
private slots:
void replotIfVisible();
private:
+ bool resetChart;
+
// QtQuick related things
QRectF plotRect;
QSGNode *updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override;
- std::unique_ptr<QImage> img;
- std::unique_ptr<QPainter> painter;
- QGraphicsScene scene;
- std::unique_ptr<QSGTexture> texture;
void plotAreaChanged(const QSizeF &size);
void reset(); // clears all series and axes
@@ -63,39 +76,39 @@ private:
void plotBarChart(const std::vector<dive *> &dives,
ChartSubType subType,
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
- const StatsVariable *valueVariable, const StatsBinner *valueBinner, bool labels, bool legend);
+ const StatsVariable *valueVariable, const StatsBinner *valueBinner);
void plotValueChart(const std::vector<dive *> &dives,
ChartSubType subType,
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
- const StatsVariable *valueVariable, StatsOperation valueAxisOperation, bool labels);
+ const StatsVariable *valueVariable, StatsOperation valueAxisOperation);
void plotDiscreteCountChart(const std::vector<dive *> &dives,
ChartSubType subType,
- const StatsVariable *categoryVariable, const StatsBinner *categoryBinner, bool labels);
+ const StatsVariable *categoryVariable, const StatsBinner *categoryBinner);
void plotPieChart(const std::vector<dive *> &dives,
- const StatsVariable *categoryVariable, const StatsBinner *categoryBinner, bool labels, bool legend);
+ const StatsVariable *categoryVariable, const StatsBinner *categoryBinner);
void plotDiscreteBoxChart(const std::vector<dive *> &dives,
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner, const StatsVariable *valueVariable);
void plotDiscreteScatter(const std::vector<dive *> &dives,
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
- const StatsVariable *valueVariable, bool quartiles);
+ const StatsVariable *valueVariable);
void plotHistogramCountChart(const std::vector<dive *> &dives,
ChartSubType subType,
- const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
- bool labels, bool showMedian, bool showMean);
+ const StatsVariable *categoryVariable, const StatsBinner *categoryBinner);
void plotHistogramValueChart(const std::vector<dive *> &dives,
ChartSubType subType,
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
- const StatsVariable *valueVariable, StatsOperation valueAxisOperation, bool labels);
+ const StatsVariable *valueVariable, StatsOperation valueAxisOperation);
void plotHistogramStackedChart(const std::vector<dive *> &dives,
ChartSubType subType,
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
- const StatsVariable *valueVariable, const StatsBinner *valueBinner, bool labels, bool legend);
+ const StatsVariable *valueVariable, const StatsBinner *valueBinner);
void plotHistogramBoxChart(const std::vector<dive *> &dives,
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner, const StatsVariable *valueVariable);
void plotScatter(const std::vector<dive *> &dives, const StatsVariable *categoryVariable, const StatsVariable *valueVariable);
void setTitle(const QString &);
void updateTitlePos(); // After resizing, set title to correct position
void plotChart();
+ void updateFeatures(); // Updates the visibility of chart features, such as legend, regression, etc.
template <typename T, class... Args>
T *createSeries(Args&&... args);
@@ -114,53 +127,55 @@ private:
// Helper functions to add feature to the chart
void addLineMarker(double pos, double low, double high, const QPen &pen, bool isHorizontal);
- // A short line used to mark quartiles
- struct QuartileMarker {
- std::unique_ptr<QGraphicsLineItem> item;
- StatsAxis *xAxis, *yAxis;
- double pos, value;
- QuartileMarker(double pos, double value, QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis);
- void updatePosition();
- };
-
- // 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);
- };
-
- // A line marking median or mean in histograms
- struct HistogramMarker {
- std::unique_ptr<QGraphicsLineItem> item;
- StatsAxis *xAxis, *yAxis;
- double val;
- bool horizontal;
- void updatePosition();
- HistogramMarker(double val, bool horizontal, QPen pen, QGraphicsScene *scene, StatsAxis *xAxis, StatsAxis *yAxis);
- };
-
- void addLinearRegression(const struct regression_data reg, StatsAxis *xAxis, StatsAxis *yAxis);
- void addHistogramMarker(double pos, const QPen &pen, bool isHorizontal, StatsAxis *xAxis, StatsAxis *yAxis);
StatsState state;
QFont titleFont;
- std::vector<std::unique_ptr<StatsAxis>> axes;
- std::unique_ptr<StatsGrid> grid;
std::vector<std::unique_ptr<StatsSeries>> series;
- std::unique_ptr<Legend> legend;
- std::vector<QuartileMarker> quartileMarkers;
- std::vector<RegressionLine> regressionLines;
- std::vector<HistogramMarker> histogramMarkers;
- std::unique_ptr<QGraphicsSimpleTextItem> title;
+ std::unique_ptr<StatsGrid> grid;
+ std::vector<ChartItemPtr<QuartileMarker>> quartileMarkers;
+ ChartItemPtr<HistogramMarker> medianMarker, meanMarker;
StatsSeries *highlightedSeries;
StatsAxis *xAxis, *yAxis;
+ ChartItemPtr<ChartTextItem> title;
+ ChartItemPtr<Legend> legend;
+ Legend *draggedItem;
+ ChartItemPtr<RegressionItem> regressionItem;
+ QPointF dragStartMouse, dragStartItem;
void hoverEnterEvent(QHoverEvent *event) override;
void hoverMoveEvent(QHoverEvent *event) override;
+ void mousePressEvent(QMouseEvent *event) override;
+ void mouseReleaseEvent(QMouseEvent *event) override;
+ void mouseMoveEvent(QMouseEvent *event) override;
+ RootNode *rootNode;
+
+ // There are three double linked lists of chart items:
+ // clean items, dirty items and items to be deleted.
+ struct ChartItemList {
+ ChartItemList();
+ ChartItem *first, *last;
+ void append(ChartItem &item);
+ void remove(ChartItem &item);
+ void clear();
+ void splice(ChartItemList &list);
+ };
+ ChartItemList cleanItems, dirtyItems, deletedItems;
+ void deleteChartItemInternal(ChartItem &item);
};
+// This implementation detail must be known to users of the class.
+// Perhaps move it into a statsview_impl.h file.
+template <typename T, class... Args>
+ChartItemPtr<T> StatsView::createChartItem(Args&&... args)
+{
+ return ChartItemPtr(new T(*this, std::forward<Args>(args)...));
+}
+
+template <typename T>
+void StatsView::deleteChartItem(ChartItemPtr<T> &item)
+{
+ deleteChartItemInternal(*item);
+ item.reset();
+}
+
#endif
diff --git a/stats/zvalues.h b/stats/zvalues.h
index 118c488c0..58222f5ab 100644
--- a/stats/zvalues.h
+++ b/stats/zvalues.h
@@ -2,17 +2,18 @@
// Defines the z-values of features in the chart view.
// Objects with higher z-values are painted on top of objects
// with smaller z-values. For the same z-value objects are
-// drawn in order of addition to the graphics scene.
+// drawn in order of addition to the scene.
#ifndef ZVALUES_H
-struct ZValues {
- static constexpr double grid = -1.0;
- static constexpr double series = 0.0;
- static constexpr double axes = 1.0;
- static constexpr double seriesLabels = 2.0;
- static constexpr double chartFeatures = 3.0; // quartile markers and regression lines
- static constexpr double informationBox = 4.0;
- static constexpr double legend = 5.0;
+enum class ChartZValue {
+ Grid = 0,
+ Series,
+ Axes,
+ SeriesLabels,
+ ChartFeatures, // quartile markers and regression lines
+ InformationBox,
+ Legend,
+ Count
};
#endif