summaryrefslogtreecommitdiffstats
path: root/stats
diff options
context:
space:
mode:
Diffstat (limited to 'stats')
-rw-r--r--stats/CMakeLists.txt2
-rw-r--r--stats/chartitem.cpp107
-rw-r--r--stats/chartitem.h51
-rw-r--r--stats/legend.cpp90
-rw-r--r--stats/legend.h18
-rw-r--r--stats/statsview.cpp87
-rw-r--r--stats/statsview.h12
7 files changed, 279 insertions, 88 deletions
diff --git a/stats/CMakeLists.txt b/stats/CMakeLists.txt
index a084dd0b5..c0fbe3c51 100644
--- a/stats/CMakeLists.txt
+++ b/stats/CMakeLists.txt
@@ -9,6 +9,8 @@ set(SUBSURFACE_STATS_SRCS
barseries.cpp
boxseries.h
boxseries.cpp
+ chartitem.h
+ chartitem.cpp
chartlistmodel.h
chartlistmodel.cpp
informationbox.h
diff --git a/stats/chartitem.cpp b/stats/chartitem.cpp
new file mode 100644
index 000000000..c0fc50841
--- /dev/null
+++ b/stats/chartitem.cpp
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "chartitem.h"
+#include "statsview.h"
+
+#include <cmath>
+#include <QQuickWindow>
+#include <QSGImageNode>
+#include <QSGTexture>
+
+static int round_up(double f)
+{
+ return static_cast<int>(ceil(f));
+}
+
+ChartItem::ChartItem(StatsView &v) :
+ dirty(false), view(v), positionDirty(false), textureDirty(false)
+{
+}
+
+ChartItem::~ChartItem()
+{
+ painter.reset(); // Make sure to destroy painter before image that is painted on
+ view.unregisterChartItem(this);
+}
+
+QSizeF ChartItem::sceneSize() const
+{
+ return view.size();
+}
+
+void ChartItem::setTextureDirty()
+{
+ textureDirty = true;
+ dirty = true;
+}
+
+void ChartItem::setPositionDirty()
+{
+ positionDirty = true;
+ dirty = true;
+}
+
+void ChartItem::render()
+{
+ if (!dirty)
+ return;
+ if (!node) {
+ node.reset(view.w()->createImageNode());
+ view.addQSGNode(node.get(), 0);
+ }
+ if (!img) {
+ resize(QSizeF(1,1));
+ img->fill(Qt::transparent);
+ }
+ if (textureDirty) {
+ texture.reset(view.w()->createTextureFromImage(*img, QQuickWindow::TextureHasAlphaChannel));
+ node->setTexture(texture.get());
+ textureDirty = false;
+ }
+ if (positionDirty) {
+ node->setRect(rect);
+ positionDirty = false;
+ }
+ dirty = false;
+}
+
+void ChartItem::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 ChartItem::setPos(QPointF pos)
+{
+ rect.moveTopLeft(pos);
+ setPositionDirty();
+}
+
+QRectF ChartItem::getRect() const
+{
+ return rect;
+}
+
+ChartRectItem::ChartRectItem(StatsView &v, const QPen &pen, const QBrush &brush, double radius) : ChartItem(v),
+ pen(pen), brush(brush), radius(radius)
+{
+}
+
+ChartRectItem::~ChartRectItem()
+{
+}
+
+void ChartRectItem::resize(QSizeF size)
+{
+ ChartItem::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);
+}
diff --git a/stats/chartitem.h b/stats/chartitem.h
new file mode 100644
index 000000000..fb4b67dff
--- /dev/null
+++ b/stats/chartitem.h
@@ -0,0 +1,51 @@
+// 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 <memory>
+#include <QPainter>
+
+class QSGImageNode;
+class QSGTexture;
+class StatsView;
+
+class ChartItem {
+public:
+ ChartItem(StatsView &v);
+ ~ChartItem();
+ // Attention: The children are responsible for updating the item. None of these calls will.
+ void resize(QSizeF size); // Resets the canvas. Attention: image is *unitialized*.
+ void setPos(QPointF pos);
+ void render(); // Only call on render thread!
+ QRectF getRect() const;
+ bool dirty; // If true, call render() when rebuilding the scene
+protected:
+ std::unique_ptr<QPainter> painter;
+ std::unique_ptr<QImage> img;
+ QSizeF sceneSize() const;
+ void setTextureDirty();
+ void setPositionDirty();
+private:
+ StatsView &view;
+ QRectF rect;
+ bool positionDirty;
+ bool textureDirty;
+ std::unique_ptr<QSGImageNode> node;
+ std::unique_ptr<QSGTexture> texture;
+};
+
+// Draw a rectangular background after resize. Children are responsible for calling update().
+class ChartRectItem : public ChartItem {
+public:
+ ChartRectItem(StatsView &v, const QPen &pen, const QBrush &brush, double radius);
+ ~ChartRectItem();
+ void resize(QSizeF size);
+private:
+ QPen pen;
+ QBrush brush;
+ double radius;
+};
+
+#endif
diff --git a/stats/legend.cpp b/stats/legend.cpp
index 27607fb51..2cbd883db 100644
--- a/stats/legend.cpp
+++ b/stats/legend.cpp
@@ -4,8 +4,6 @@
#include "zvalues.h"
#include <QFontMetrics>
-#include <QGraphicsScene>
-#include <QGraphicsSceneMouseEvent>
#include <QPen>
static const double legendBorderSize = 2.0;
@@ -16,51 +14,30 @@ 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, QPen(legendBorderColor, legendBorderSize), QBrush(legendColor), legendBoxBorderRadius),
+ displayedItems(0), width(0.0), height(0.0),
+ font(QFont()) // Make configurable
{
- 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 +45,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 +77,32 @@ 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);
- itemPos.rx() += fontHeight + 2.0 * legendBoxBorderSize;
- entries[i].text->setPos(itemPos);
- entries[i].rect->show();
- entries[i].text->show();
+ painter->drawRect(rect);
}
- for (int i = displayedItems; i < (int)entries.size(); ++i) {
- entries[i].rect->hide();
- entries[i].text->hide();
+
+ // 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;
+ QRectF rect(itemPos, QSizeF(entries[i].width, fontHeight));
+ painter->drawText(rect, entries[i].name);
}
- show();
+
+ // For now, place the legend in the top right corner.
+ QPointF pos(size.width() - width - 10.0, 10.0);
+ setPos(pos);
}
diff --git a/stats/legend.h b/stats/legend.h
index c643a41f3..a9d42bf39 100644
--- a/stats/legend.h
+++ b/stats/legend.h
@@ -3,34 +3,34 @@
#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);
+ Legend(StatsView &view, const std::vector<QString> &names);
void hover(QPointF pos);
void resize(); // called when the chart size changes.
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;
int fontHeight;
std::vector<Entry> entries;
- void updatePosition();
void hide();
};
diff --git a/stats/statsview.cpp b/stats/statsview.cpp
index e5e6b0b0d..61510d60c 100644
--- a/stats/statsview.cpp
+++ b/stats/statsview.cpp
@@ -25,13 +25,48 @@
#include <QSGImageNode>
#include <QSGTexture>
+// 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
+
+StatsView::StatsView(QQuickItem *parent) : QQuickItem(parent),
+ highlightedSeries(nullptr),
+ xAxis(nullptr),
+ yAxis(nullptr),
+ rootNode(nullptr)
+{
+ setFlag(ItemHasContents, true);
+
+ connect(&diveListNotifier, &DiveListNotifier::numShownChanged, this, &StatsView::replotIfVisible);
+
+ setAcceptHoverEvents(true);
+
+ QFont font;
+ titleFont = QFont(font.family(), font.pointSize(), QFont::Light); // Make configurable
+}
+
+StatsView::StatsView() : StatsView(nullptr)
+{
+}
+
+StatsView::~StatsView()
+{
+}
+
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);
if (!n)
- n = window()->createImageNode();
+ n = rootNode = window()->createImageNode();
+
+ for (ChartItem *item: items) {
+ if (item->dirty)
+ item->render();
+ }
QRectF rect = boundingRect();
if (plotRect != rect) {
@@ -47,33 +82,30 @@ QSGNode *StatsView::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNod
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
-
-StatsView::StatsView(QQuickItem *parent) : QQuickItem(parent),
- highlightedSeries(nullptr),
- xAxis(nullptr),
- yAxis(nullptr)
+void StatsView::addQSGNode(QSGNode *node, int)
{
- setFlag(ItemHasContents, true);
-
- connect(&diveListNotifier, &DiveListNotifier::numShownChanged, this, &StatsView::replotIfVisible);
-
- setAcceptHoverEvents(true);
+ rootNode->appendChildNode(node);
+}
- QFont font;
- titleFont = QFont(font.family(), font.pointSize(), QFont::Light); // Make configurable
+// Currently this does an inefficient linear search in the chart-item vector.
+// The reason is that removing individual chart items is very rare: for now,
+// it is only done when hiding an InfoBox. In the future, this might have to
+// be improved.
+void StatsView::unregisterChartItem(const ChartItem *item)
+{
+ auto it = std::find(items.begin(), items.end(), item);
+ if (it != items.end())
+ items.erase(it);
}
-StatsView::StatsView() : StatsView(nullptr)
+QQuickWindow *StatsView::w() const
{
+ return window();
}
-StatsView::~StatsView()
+QSizeF StatsView::size() const
{
+ return boundingRect().size();
}
void StatsView::plotAreaChanged(const QSizeF &s)
@@ -202,6 +234,14 @@ T *StatsView::createAxis(const QString &title, Args&&... args)
return res;
}
+template <typename T, class... Args>
+std::unique_ptr<T> StatsView::createChartItem(Args&&... args)
+{
+ std::unique_ptr<T> res(new T(*this, std::forward<Args>(args)...));
+ items.push_back(res.get());
+ return res;
+}
+
void StatsView::setAxes(StatsAxis *x, StatsAxis *y)
{
xAxis = x;
@@ -214,6 +254,7 @@ void StatsView::reset()
{
highlightedSeries = nullptr;
xAxis = yAxis = nullptr;
+ items.clear(); // non-owning pointers
legend.reset();
series.clear();
quartileMarkers.clear();
@@ -406,7 +447,7 @@ void StatsView::plotBarChart(const std::vector<dive *> &dives,
// 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());
@@ -623,7 +664,7 @@ void StatsView::plotPieChart(const std::vector<dive *> &dives,
PieSeries *series = createSeries<PieSeries>(categoryVariable->name(), data, keepOrder, labels);
if (showLegend)
- legend = createItemPtr<Legend>(&scene, series->binNames());
+ legend = createChartItem<Legend>(series->binNames());
}
void StatsView::plotDiscreteBoxChart(const std::vector<dive *> &dives,
@@ -959,7 +1000,7 @@ void StatsView::plotHistogramStackedChart(const std::vector<dive *> &dives,
BarPlotData data(categoryBins, *valueBinner);
if (showLegend)
- legend = createItemPtr<Legend>(&scene, data.vbinNames);
+ legend = createChartItem<Legend>(data.vbinNames);
CountAxis *valAxis = createCountAxis(data.maxCategoryCount, isHorizontal);
diff --git a/stats/statsview.h b/stats/statsview.h
index b1e178565..385a591f4 100644
--- a/stats/statsview.h
+++ b/stats/statsview.h
@@ -19,8 +19,10 @@ struct StatsVariable;
class QGraphicsLineItem;
class QGraphicsSimpleTextItem;
+class QSGImageNode;
class StatsSeries;
class CategoryAxis;
+class ChartItem;
class CountAxis;
class HistogramAxis;
class StatsAxis;
@@ -46,6 +48,10 @@ public:
~StatsView();
void plot(const StatsState &state);
+ QQuickWindow *w() const; // Make window available to items
+ QSizeF size() const;
+ void addQSGNode(QSGNode *node, int z); // Must only be called in render thread!
+ void unregisterChartItem(const ChartItem *item);
private slots:
void replotIfVisible();
private:
@@ -103,6 +109,9 @@ private:
template <typename T, class... Args>
T *createAxis(const QString &title, Args&&... args);
+ template <typename T, class... Args>
+ std::unique_ptr<T> createChartItem(Args&&... args);
+
template<typename T>
CategoryAxis *createCategoryAxis(const QString &title, const StatsBinner &binner,
const std::vector<T> &bins, bool isHorizontal);
@@ -156,11 +165,14 @@ private:
std::vector<RegressionLine> regressionLines;
std::vector<HistogramMarker> histogramMarkers;
std::unique_ptr<QGraphicsSimpleTextItem> title;
+ std::vector<ChartItem *> items; // Attention: currently, items are not automatically removed on destruction!
StatsSeries *highlightedSeries;
StatsAxis *xAxis, *yAxis;
void hoverEnterEvent(QHoverEvent *event) override;
void hoverMoveEvent(QHoverEvent *event) override;
+
+ QSGImageNode *rootNode;
};
#endif