2021-01-01 21:43:21 +00:00
|
|
|
// SPDX-License-Identifier: GPL-2.0
|
|
|
|
#include "statsview.h"
|
|
|
|
#include "barseries.h"
|
|
|
|
#include "boxseries.h"
|
2021-01-14 08:48:44 +00:00
|
|
|
#include "histogrammarker.h"
|
2021-01-01 21:43:21 +00:00
|
|
|
#include "legend.h"
|
|
|
|
#include "pieseries.h"
|
2021-01-14 07:48:56 +00:00
|
|
|
#include "quartilemarker.h"
|
2021-01-15 17:39:14 +00:00
|
|
|
#include "regressionitem.h"
|
2021-01-01 21:43:21 +00:00
|
|
|
#include "scatterseries.h"
|
|
|
|
#include "statsaxis.h"
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
#include "statscolors.h"
|
2021-01-05 12:51:39 +00:00
|
|
|
#include "statsgrid.h"
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
#include "statshelper.h"
|
2021-01-01 21:43:21 +00:00
|
|
|
#include "statsstate.h"
|
|
|
|
#include "statstranslations.h"
|
|
|
|
#include "statsvariables.h"
|
|
|
|
#include "zvalues.h"
|
|
|
|
#include "core/divefilter.h"
|
|
|
|
#include "core/subsurface-qt/divelistnotifier.h"
|
2021-02-01 22:17:04 +00:00
|
|
|
#include "core/selection.h"
|
2021-01-26 20:57:41 +00:00
|
|
|
#include "core/trip.h"
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
#include <cmath>
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
#include <QQuickItem>
|
|
|
|
#include <QQuickWindow>
|
|
|
|
#include <QSGImageNode>
|
2021-01-14 11:33:03 +00:00
|
|
|
#include <QSGRectangleNode>
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
#include <QSGTexture>
|
|
|
|
|
2021-01-12 14:20:05 +00:00
|
|
|
// 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
|
2021-02-01 22:17:04 +00:00
|
|
|
static const double selectionLassoWidth = 2.0; // Border between title and chart
|
2021-01-12 14:20:05 +00:00
|
|
|
|
|
|
|
StatsView::StatsView(QQuickItem *parent) : QQuickItem(parent),
|
2021-01-20 22:13:54 +00:00
|
|
|
backgroundDirty(true),
|
2021-02-16 16:05:39 +00:00
|
|
|
currentTheme(statsThemes[0]),
|
2021-01-12 14:20:05 +00:00
|
|
|
highlightedSeries(nullptr),
|
|
|
|
xAxis(nullptr),
|
|
|
|
yAxis(nullptr),
|
2021-01-13 12:23:41 +00:00
|
|
|
draggedItem(nullptr),
|
2021-02-12 09:56:48 +00:00
|
|
|
restrictDives(false),
|
2021-01-18 21:29:34 +00:00
|
|
|
rootNode(nullptr)
|
2021-01-12 14:20:05 +00:00
|
|
|
{
|
|
|
|
setFlag(ItemHasContents, true);
|
|
|
|
|
|
|
|
connect(&diveListNotifier, &DiveListNotifier::numShownChanged, this, &StatsView::replotIfVisible);
|
2021-01-26 20:57:41 +00:00
|
|
|
connect(&diveListNotifier, &DiveListNotifier::divesAdded, this, &StatsView::replotIfVisible);
|
|
|
|
connect(&diveListNotifier, &DiveListNotifier::divesDeleted, this, &StatsView::replotIfVisible);
|
|
|
|
connect(&diveListNotifier, &DiveListNotifier::dataReset, this, &StatsView::replotIfVisible);
|
|
|
|
connect(&diveListNotifier, &DiveListNotifier::settingsChanged, this, &StatsView::replotIfVisible);
|
2021-01-31 19:48:12 +00:00
|
|
|
connect(&diveListNotifier, &DiveListNotifier::divesSelected, this, &StatsView::divesSelected);
|
2021-01-12 14:20:05 +00:00
|
|
|
|
|
|
|
setAcceptHoverEvents(true);
|
2021-01-13 12:23:41 +00:00
|
|
|
setAcceptedMouseButtons(Qt::LeftButton);
|
2021-01-12 14:20:05 +00:00
|
|
|
|
|
|
|
QFont font;
|
|
|
|
titleFont = QFont(font.family(), font.pointSize(), QFont::Light); // Make configurable
|
|
|
|
}
|
|
|
|
|
|
|
|
StatsView::StatsView() : StatsView(nullptr)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
StatsView::~StatsView()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2021-01-13 12:23:41 +00:00
|
|
|
void StatsView::mousePressEvent(QMouseEvent *event)
|
|
|
|
{
|
2021-01-17 12:34:18 +00:00
|
|
|
QPointF pos = event->localPos();
|
|
|
|
|
2021-01-13 12:23:41 +00:00
|
|
|
// Currently, we only support dragging of the legend. If other objects
|
|
|
|
// should be made draggable, this needs to be generalized.
|
|
|
|
if (legend) {
|
|
|
|
QRectF rect = legend->getRect();
|
|
|
|
if (legend->getRect().contains(pos)) {
|
|
|
|
dragStartMouse = pos;
|
|
|
|
dragStartItem = rect.topLeft();
|
2021-01-18 21:29:34 +00:00
|
|
|
draggedItem = &*legend;
|
2021-01-13 12:23:41 +00:00
|
|
|
grabMouse();
|
2021-01-19 23:46:48 +00:00
|
|
|
setKeepMouseGrab(true); // don't allow Qt to steal the grab
|
2021-01-17 12:34:18 +00:00
|
|
|
return;
|
2021-01-13 12:23:41 +00:00
|
|
|
}
|
|
|
|
}
|
2021-01-17 12:34:18 +00:00
|
|
|
|
2021-02-08 16:07:37 +00:00
|
|
|
SelectionModifier modifier;
|
|
|
|
modifier.shift = (event->modifiers() & Qt::ShiftModifier) != 0;
|
|
|
|
modifier.ctrl = (event->modifiers() & Qt::ControlModifier) != 0;
|
2021-02-01 22:17:04 +00:00
|
|
|
bool itemSelected = false;
|
2021-01-17 12:34:18 +00:00
|
|
|
for (auto &series: series)
|
2021-02-08 16:07:37 +00:00
|
|
|
itemSelected |= series->selectItemsUnderMouse(pos, modifier);
|
2021-02-01 22:17:04 +00:00
|
|
|
|
|
|
|
// The user clicked in "empty" space. If there is a series supporting lasso-select,
|
|
|
|
// got into lasso mode. For now, we only support a rectangular lasso.
|
|
|
|
if (!itemSelected && std::any_of(series.begin(), series.end(),
|
|
|
|
[] (const std::unique_ptr<StatsSeries> &s)
|
|
|
|
{ return s->supportsLassoSelection(); })) {
|
|
|
|
if (selectionRect)
|
|
|
|
deleteChartItem(selectionRect); // Ooops. Already a selection in place.
|
|
|
|
dragStartMouse = pos;
|
2021-02-16 16:05:39 +00:00
|
|
|
selectionRect = createChartItem<ChartRectLineItem>(ChartZValue::Selection, currentTheme->selectionLassoColor, selectionLassoWidth);
|
2021-02-08 16:07:37 +00:00
|
|
|
selectionModifier = modifier;
|
|
|
|
oldSelection = modifier.ctrl ? getDiveSelection() : std::vector<dive *>();
|
2021-02-01 22:17:04 +00:00
|
|
|
grabMouse();
|
|
|
|
setKeepMouseGrab(true); // don't allow Qt to steal the grab
|
|
|
|
update();
|
|
|
|
}
|
2021-01-13 12:23:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::mouseReleaseEvent(QMouseEvent *)
|
|
|
|
{
|
|
|
|
if (draggedItem) {
|
|
|
|
draggedItem = nullptr;
|
|
|
|
ungrabMouse();
|
|
|
|
}
|
2021-02-01 22:17:04 +00:00
|
|
|
|
|
|
|
if (selectionRect) {
|
|
|
|
deleteChartItem(selectionRect);
|
|
|
|
ungrabMouse();
|
|
|
|
update();
|
|
|
|
}
|
2021-01-13 12:23:41 +00:00
|
|
|
}
|
|
|
|
|
2021-01-19 08:54:39 +00:00
|
|
|
// 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>;
|
|
|
|
|
2021-01-13 15:19:27 +00:00
|
|
|
class RootNode : public QSGNode
|
|
|
|
{
|
|
|
|
public:
|
2021-01-21 12:51:03 +00:00
|
|
|
RootNode(StatsView &view);
|
|
|
|
~RootNode();
|
|
|
|
StatsView &view;
|
2021-01-18 12:14:38 +00:00
|
|
|
std::unique_ptr<QSGRectangleNode> backgroundNode; // solid background
|
2021-01-13 15:19:27 +00:00
|
|
|
// We entertain one node per Z-level.
|
2021-01-19 08:54:39 +00:00
|
|
|
std::array<std::unique_ptr<ZNode>, (size_t)ChartZValue::Count> zNodes;
|
2021-01-13 15:19:27 +00:00
|
|
|
};
|
|
|
|
|
2021-01-21 12:51:03 +00:00
|
|
|
RootNode::RootNode(StatsView &view) : view(view)
|
2021-01-13 15:19:27 +00:00
|
|
|
{
|
2021-01-14 11:33:03 +00:00
|
|
|
// 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.
|
2021-01-21 12:51:03 +00:00
|
|
|
backgroundNode.reset(view.w()->createRectangleNode());
|
2021-02-16 16:05:39 +00:00
|
|
|
backgroundNode->setColor(view.getCurrentTheme().backgroundColor);
|
2021-01-18 12:14:38 +00:00
|
|
|
appendChildNode(backgroundNode.get());
|
2021-01-14 11:33:03 +00:00
|
|
|
|
2021-01-18 12:14:38 +00:00
|
|
|
for (auto &zNode: zNodes) {
|
2021-01-19 08:54:39 +00:00
|
|
|
zNode.reset(new ZNode(true));
|
2021-01-18 12:14:38 +00:00
|
|
|
appendChildNode(zNode.get());
|
2021-01-13 15:19:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-21 12:51:03 +00:00
|
|
|
RootNode::~RootNode()
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
{
|
2021-01-21 12:51:03 +00:00
|
|
|
view.emergencyShutdown();
|
|
|
|
}
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
|
2021-01-21 12:51:03 +00:00
|
|
|
void StatsView::freeDeletedChartItems()
|
|
|
|
{
|
2021-01-18 21:29:34 +00:00
|
|
|
ChartItem *nextitem;
|
|
|
|
for (ChartItem *item = deletedItems.first; item; item = nextitem) {
|
|
|
|
nextitem = item->next;
|
|
|
|
delete item;
|
|
|
|
}
|
|
|
|
deletedItems.clear();
|
2021-01-21 12:51:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
|
|
|
RootNode *n = static_cast<RootNode *>(oldNode);
|
|
|
|
if (!n)
|
|
|
|
n = rootNode = new RootNode(*this);
|
|
|
|
|
|
|
|
// Delete all chart items that are marked for deletion.
|
|
|
|
freeDeletedChartItems();
|
2021-01-18 21:29:34 +00:00
|
|
|
|
2021-01-20 22:13:54 +00:00
|
|
|
if (backgroundDirty) {
|
|
|
|
rootNode->backgroundNode->setRect(plotRect);
|
|
|
|
backgroundDirty = false;
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
}
|
|
|
|
|
2021-01-18 21:29:34 +00:00
|
|
|
for (ChartItem *item = dirtyItems.first; item; item = item->next) {
|
2021-02-16 16:05:39 +00:00
|
|
|
item->render(*currentTheme);
|
2021-01-15 11:22:32 +00:00
|
|
|
item->dirty = false;
|
2021-01-13 15:19:27 +00:00
|
|
|
}
|
2021-01-18 21:29:34 +00:00
|
|
|
dirtyItems.splice(cleanItems);
|
2021-01-13 15:19:27 +00:00
|
|
|
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
return n;
|
|
|
|
}
|
2021-01-01 21:43:21 +00:00
|
|
|
|
2021-01-21 12:51:03 +00:00
|
|
|
// When reparenting the QQuickWidget, QtQuick decides to delete our rootNode
|
|
|
|
// and with it all the QSG nodes, even though we have *not* given the
|
|
|
|
// permission to do so! If the widget is reused, we try to delete the
|
|
|
|
// stale items, whose nodes have already been deleted by QtQuick, leading
|
|
|
|
// to a double-free(). Instead of searching for the cause of this behavior,
|
|
|
|
// let's just hook into the rootNodes destructor and delete the objects
|
|
|
|
// in a controlled manner, so that QtQuick has no more access to them.
|
|
|
|
void StatsView::emergencyShutdown()
|
|
|
|
{
|
|
|
|
// Mark clean and dirty chart items for deletion...
|
|
|
|
cleanItems.splice(deletedItems);
|
|
|
|
dirtyItems.splice(deletedItems);
|
|
|
|
|
|
|
|
// ...and delete them.
|
|
|
|
freeDeletedChartItems();
|
|
|
|
|
|
|
|
// Now delete all the pointers we might have to chart features,
|
|
|
|
// axes, etc. Note that all pointers to chart items are non
|
|
|
|
// owning, so this only resets stale references, but does not
|
|
|
|
// lead to any additional deletion of chart items.
|
|
|
|
reset();
|
|
|
|
|
|
|
|
// The rootNode is being deleted -> remove the reference to that
|
|
|
|
rootNode = nullptr;
|
|
|
|
}
|
|
|
|
|
2021-01-13 15:19:27 +00:00
|
|
|
void StatsView::addQSGNode(QSGNode *node, ChartZValue z)
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
{
|
2021-01-13 15:19:27 +00:00
|
|
|
int idx = std::clamp((int)z, 0, (int)ChartZValue::Count - 1);
|
|
|
|
rootNode->zNodes[idx]->appendChildNode(node);
|
2021-01-12 14:20:05 +00:00
|
|
|
}
|
2021-01-04 20:41:30 +00:00
|
|
|
|
2021-01-18 21:29:34 +00:00
|
|
|
void StatsView::registerChartItem(ChartItem &item)
|
|
|
|
{
|
|
|
|
cleanItems.append(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::registerDirtyChartItem(ChartItem &item)
|
2021-01-12 14:20:05 +00:00
|
|
|
{
|
2021-01-18 21:29:34 +00:00
|
|
|
if (item.dirty)
|
2021-01-15 11:22:32 +00:00
|
|
|
return;
|
2021-01-18 21:29:34 +00:00
|
|
|
cleanItems.remove(item);
|
|
|
|
dirtyItems.append(item);
|
|
|
|
item.dirty = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::deleteChartItemInternal(ChartItem &item)
|
|
|
|
{
|
|
|
|
if (item.dirty)
|
|
|
|
dirtyItems.remove(item);
|
2021-01-15 11:22:32 +00:00
|
|
|
else
|
2021-01-18 21:29:34 +00:00
|
|
|
cleanItems.remove(item);
|
|
|
|
deletedItems.append(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
StatsView::ChartItemList::ChartItemList() : first(nullptr), last(nullptr)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::ChartItemList::clear()
|
|
|
|
{
|
|
|
|
first = last = nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2021-01-15 11:22:32 +00:00
|
|
|
else
|
2021-01-18 21:29:34 +00:00
|
|
|
first = item.next;
|
|
|
|
item.prev = item.next = nullptr;
|
2021-01-13 15:19:27 +00:00
|
|
|
}
|
|
|
|
|
2021-01-18 21:29:34 +00:00
|
|
|
void StatsView::ChartItemList::append(ChartItem &item)
|
2021-01-13 15:19:27 +00:00
|
|
|
{
|
2021-01-18 21:29:34 +00:00
|
|
|
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.
|
2021-01-15 11:22:32 +00:00
|
|
|
return;
|
2021-01-18 21:29:34 +00:00
|
|
|
if (!l2.first) {
|
|
|
|
l2 = *this;
|
2021-01-15 11:22:32 +00:00
|
|
|
} else {
|
2021-01-18 21:29:34 +00:00
|
|
|
l2.last->next = first;
|
|
|
|
first->prev = l2.last;
|
|
|
|
l2.last = last;
|
2021-01-15 11:22:32 +00:00
|
|
|
}
|
2021-01-18 21:29:34 +00:00
|
|
|
clear();
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
2021-01-12 14:20:05 +00:00
|
|
|
QQuickWindow *StatsView::w() const
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
{
|
2021-01-12 14:20:05 +00:00
|
|
|
return window();
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
}
|
|
|
|
|
2021-02-16 16:05:39 +00:00
|
|
|
void StatsView::setTheme(int idx)
|
|
|
|
{
|
|
|
|
idx = std::clamp(idx, 0, (int)statsThemes.size() - 1);
|
|
|
|
currentTheme = statsThemes[idx];
|
|
|
|
rootNode->backgroundNode->setColor(currentTheme->backgroundColor);
|
|
|
|
}
|
|
|
|
|
|
|
|
const StatsTheme &StatsView::getCurrentTheme() const
|
|
|
|
{
|
|
|
|
return *currentTheme;
|
|
|
|
}
|
|
|
|
|
2021-01-12 14:20:05 +00:00
|
|
|
QSizeF StatsView::size() const
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
2021-01-12 14:20:05 +00:00
|
|
|
return boundingRect().size();
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
2021-01-18 11:08:46 +00:00
|
|
|
QRectF StatsView::plotArea() const
|
|
|
|
{
|
|
|
|
return plotRect;
|
|
|
|
}
|
|
|
|
|
2022-02-10 01:13:03 +00:00
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
|
|
void StatsView::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
|
|
|
|
#else
|
2021-01-20 22:13:54 +00:00
|
|
|
void StatsView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
|
2022-02-10 01:13:03 +00:00
|
|
|
#endif
|
2021-01-20 22:13:54 +00:00
|
|
|
{
|
|
|
|
plotRect = QRectF(QPointF(0.0, 0.0), newGeometry.size());
|
|
|
|
backgroundDirty = true;
|
|
|
|
plotAreaChanged(plotRect.size());
|
|
|
|
|
|
|
|
// Do we need to call the base-class' version of geometryChanged? Probably for QML?
|
2022-02-10 01:13:03 +00:00
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
|
|
QQuickItem::geometryChange(newGeometry, oldGeometry);
|
|
|
|
#else
|
2021-01-20 22:13:54 +00:00
|
|
|
QQuickItem::geometryChanged(newGeometry, oldGeometry);
|
2022-02-10 01:13:03 +00:00
|
|
|
#endif
|
2021-01-20 22:13:54 +00:00
|
|
|
}
|
|
|
|
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
void StatsView::plotAreaChanged(const QSizeF &s)
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
double left = sceneBorder;
|
|
|
|
double top = sceneBorder;
|
|
|
|
double right = s.width() - sceneBorder;
|
|
|
|
double bottom = s.height() - sceneBorder;
|
2021-01-05 11:11:46 +00:00
|
|
|
const double minSize = 30.0;
|
|
|
|
|
|
|
|
if (title)
|
2021-01-18 11:47:24 +00:00
|
|
|
top += title->getRect().height() + titleBorder;
|
2021-01-05 11:11:46 +00:00
|
|
|
// Currently, we only have either none, or an x- and a y-axis
|
2021-01-11 11:59:17 +00:00
|
|
|
std::pair<double,double> horizontalSpace{ 0.0, 0.0 };
|
|
|
|
if (xAxis) {
|
2021-01-05 11:11:46 +00:00
|
|
|
bottom -= xAxis->height();
|
2021-01-11 11:59:17 +00:00
|
|
|
horizontalSpace = xAxis->horizontalOverhang();
|
|
|
|
}
|
2021-01-05 11:11:46 +00:00
|
|
|
if (bottom - top < minSize)
|
|
|
|
return;
|
|
|
|
if (yAxis) {
|
|
|
|
yAxis->setSize(bottom - top);
|
2021-01-11 11:59:17 +00:00
|
|
|
horizontalSpace.first = std::max(horizontalSpace.first, yAxis->width());
|
2021-01-05 11:11:46 +00:00
|
|
|
}
|
2021-01-11 11:59:17 +00:00
|
|
|
left += horizontalSpace.first;
|
|
|
|
right -= horizontalSpace.second;
|
|
|
|
if (yAxis)
|
|
|
|
yAxis->setPos(QPointF(left, bottom));
|
2021-01-05 11:11:46 +00:00
|
|
|
if (right - left < minSize)
|
|
|
|
return;
|
|
|
|
if (xAxis) {
|
|
|
|
xAxis->setSize(right - left);
|
|
|
|
xAxis->setPos(QPointF(left, bottom));
|
|
|
|
}
|
|
|
|
|
2021-01-05 12:51:39 +00:00
|
|
|
if (grid)
|
|
|
|
grid->updatePositions();
|
2021-01-01 21:43:21 +00:00
|
|
|
for (auto &series: series)
|
|
|
|
series->updatePositions();
|
2021-01-14 07:48:56 +00:00
|
|
|
for (auto &marker: quartileMarkers)
|
|
|
|
marker->updatePosition();
|
2021-01-15 17:39:14 +00:00
|
|
|
if (regressionItem)
|
|
|
|
regressionItem->updatePosition();
|
2021-01-19 08:54:39 +00:00
|
|
|
if (meanMarker)
|
|
|
|
meanMarker->updatePosition();
|
|
|
|
if (medianMarker)
|
|
|
|
medianMarker->updatePosition();
|
2021-01-01 21:43:21 +00:00
|
|
|
if (legend)
|
|
|
|
legend->resize();
|
2021-01-04 20:41:30 +00:00
|
|
|
updateTitlePos();
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::replotIfVisible()
|
|
|
|
{
|
|
|
|
if (isVisible())
|
|
|
|
plot(state);
|
|
|
|
}
|
|
|
|
|
2021-01-31 19:48:12 +00:00
|
|
|
void StatsView::divesSelected(const QVector<dive *> &dives)
|
|
|
|
{
|
|
|
|
if (isVisible()) {
|
|
|
|
for (auto &series: series)
|
|
|
|
series->divesSelected(dives);
|
|
|
|
}
|
|
|
|
update();
|
|
|
|
}
|
|
|
|
|
2021-01-13 12:23:41 +00:00
|
|
|
void StatsView::mouseMoveEvent(QMouseEvent *event)
|
|
|
|
{
|
2021-02-01 22:17:04 +00:00
|
|
|
if (draggedItem) {
|
|
|
|
QSizeF sceneSize = size();
|
|
|
|
if (sceneSize.width() <= 1.0 || sceneSize.height() <= 1.0)
|
|
|
|
return;
|
|
|
|
draggedItem->setPos(event->pos() - dragStartMouse + dragStartItem);
|
|
|
|
update();
|
|
|
|
}
|
2021-01-13 12:23:41 +00:00
|
|
|
|
2021-02-01 22:17:04 +00:00
|
|
|
if (selectionRect) {
|
|
|
|
QPointF p1 = event->pos();
|
|
|
|
QPointF p2 = dragStartMouse;
|
|
|
|
selectionRect->setLine(p1, p2);
|
|
|
|
QRectF rect(std::min(p1.x(), p2.x()), std::min(p1.y(), p2.y()),
|
|
|
|
fabs(p2.x() - p1.x()), fabs(p2.y() - p1.y()));
|
|
|
|
for (auto &series: series) {
|
|
|
|
if (series->supportsLassoSelection())
|
2021-02-08 16:07:37 +00:00
|
|
|
series->selectItemsInRect(rect, selectionModifier, oldSelection);
|
2021-02-01 22:17:04 +00:00
|
|
|
}
|
|
|
|
update();
|
|
|
|
}
|
2021-01-13 12:23:41 +00:00
|
|
|
}
|
|
|
|
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
void StatsView::hoverEnterEvent(QHoverEvent *)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::hoverMoveEvent(QHoverEvent *event)
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
2021-01-13 12:23:41 +00:00
|
|
|
QPointF pos = event->pos();
|
|
|
|
|
2021-01-01 21:43:21 +00:00
|
|
|
for (auto &series: series) {
|
|
|
|
if (series->hover(pos)) {
|
|
|
|
if (series.get() != highlightedSeries) {
|
|
|
|
if (highlightedSeries)
|
|
|
|
highlightedSeries->unhighlight();
|
|
|
|
highlightedSeries = series.get();
|
|
|
|
}
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
return update();
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// No series was highlighted -> unhighlight any previously highlighted series.
|
|
|
|
if (highlightedSeries) {
|
|
|
|
highlightedSeries->unhighlight();
|
|
|
|
highlightedSeries = nullptr;
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
update();
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T, class... Args>
|
|
|
|
T *StatsView::createSeries(Args&&... args)
|
|
|
|
{
|
2021-01-18 11:35:22 +00:00
|
|
|
T *res = new T(*this, xAxis, yAxis, std::forward<Args>(args)...);
|
2021-01-01 21:43:21 +00:00
|
|
|
series.emplace_back(res);
|
|
|
|
series.back()->updatePositions();
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::setTitle(const QString &s)
|
|
|
|
{
|
2021-01-18 21:29:34 +00:00
|
|
|
if (title) {
|
|
|
|
// Ooops. Currently we do not support setting the title twice.
|
2021-01-04 20:41:30 +00:00
|
|
|
return;
|
|
|
|
}
|
2021-01-18 11:47:24 +00:00
|
|
|
title = createChartItem<ChartTextItem>(ChartZValue::Legend, titleFont, s);
|
2021-02-16 16:05:39 +00:00
|
|
|
title->setColor(currentTheme->darkLabelColor);
|
2021-01-04 20:41:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::updateTitlePos()
|
|
|
|
{
|
|
|
|
if (!title)
|
|
|
|
return;
|
2021-02-06 11:26:54 +00:00
|
|
|
QPointF pos(sceneBorder + (boundingRect().width() - title->getRect().width()) / 2.0, sceneBorder);
|
|
|
|
title->setPos(roundPos(pos));
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T, class... Args>
|
|
|
|
T *StatsView::createAxis(const QString &title, Args&&... args)
|
|
|
|
{
|
2021-01-18 21:29:34 +00:00
|
|
|
return &*createChartItem<T>(title, std::forward<Args>(args)...);
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
2021-01-05 11:11:46 +00:00
|
|
|
void StatsView::setAxes(StatsAxis *x, StatsAxis *y)
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
2021-01-05 11:11:46 +00:00
|
|
|
xAxis = x;
|
|
|
|
yAxis = y;
|
2021-01-05 12:51:39 +00:00
|
|
|
if (x && y)
|
2021-01-14 11:37:26 +00:00
|
|
|
grid = std::make_unique<StatsGrid>(*this, *x, *y);
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::reset()
|
|
|
|
{
|
|
|
|
highlightedSeries = nullptr;
|
2021-01-05 11:11:46 +00:00
|
|
|
xAxis = yAxis = nullptr;
|
2021-01-13 12:23:41 +00:00
|
|
|
draggedItem = nullptr;
|
2021-01-18 21:29:34 +00:00
|
|
|
title.reset();
|
2021-01-01 21:43:21 +00:00
|
|
|
legend.reset();
|
2021-01-18 21:29:34 +00:00
|
|
|
regressionItem.reset();
|
2021-01-19 08:54:39 +00:00
|
|
|
meanMarker.reset();
|
|
|
|
medianMarker.reset();
|
2021-02-01 22:17:04 +00:00
|
|
|
selectionRect.reset();
|
2021-01-18 21:29:34 +00:00
|
|
|
|
|
|
|
// Mark clean and dirty chart items for deletion
|
|
|
|
cleanItems.splice(deletedItems);
|
|
|
|
dirtyItems.splice(deletedItems);
|
|
|
|
|
2021-01-01 21:43:21 +00:00
|
|
|
series.clear();
|
|
|
|
quartileMarkers.clear();
|
2021-01-05 12:51:39 +00:00
|
|
|
grid.reset();
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
2021-02-12 09:56:48 +00:00
|
|
|
void StatsView::restrictToSelection()
|
|
|
|
{
|
|
|
|
restrictedDives = getDiveSelection();
|
|
|
|
std::sort(restrictedDives.begin(), restrictedDives.end()); // Sort by pointer for quick lookup
|
|
|
|
restrictDives = true;
|
|
|
|
plot(state);
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::unrestrict()
|
|
|
|
{
|
|
|
|
restrictDives = false;
|
|
|
|
plot(state);
|
|
|
|
}
|
|
|
|
|
|
|
|
int StatsView::restrictionCount() const
|
|
|
|
{
|
|
|
|
return restrictDives ? (int)restrictedDives.size() : -1;
|
|
|
|
}
|
|
|
|
|
2021-01-01 21:43:21 +00:00
|
|
|
void StatsView::plot(const StatsState &stateIn)
|
|
|
|
{
|
|
|
|
state = stateIn;
|
2021-01-04 20:41:30 +00:00
|
|
|
plotChart();
|
2021-01-19 08:54:39 +00:00
|
|
|
updateFeatures(); // Show / hide chart features, such as legend, etc.
|
2021-01-20 22:13:54 +00:00
|
|
|
plotAreaChanged(plotRect.size());
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
update();
|
2021-01-04 20:41:30 +00:00
|
|
|
}
|
|
|
|
|
2021-01-19 08:54:39 +00:00
|
|
|
void StatsView::updateFeatures(const StatsState &stateIn)
|
|
|
|
{
|
|
|
|
state = stateIn;
|
|
|
|
updateFeatures();
|
|
|
|
update();
|
|
|
|
}
|
|
|
|
|
2021-01-04 20:41:30 +00:00
|
|
|
void StatsView::plotChart()
|
|
|
|
{
|
statistics: convert chart to QQuickItem
It turns out that the wrong base class was used for the chart.
QQuickWidget can only be used on desktop, not in a mobile UI.
Therefore, turn this into a QQuickItem and move the container
QQuickWidget into desktop-only code.
Currently, this code is insane: The chart is rendered onto a
QGraphicsScene (as it was before), which is then rendered into
a QImage, which is transformed into a QSGTexture, which is then
projected onto the device. This is performed on every mouse
move event, since these events in general change the position
of the info-box.
The plan is to slowly convert elements such as the info-box into
QQuickItems. Browsing the QtQuick documentation, this will
not be much fun.
Also note that the rendering currently tears, flickers and has
antialiasing artifacts, most likely owing to integer (QImage)
to floating point (QGraphicsScene, QQuickItem) conversion
problems. The data flow is
QGraphicsScene (float) -> QImage (int) -> QQuickItem (float).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-07 13:38:37 +00:00
|
|
|
if (!state.var1)
|
2021-01-01 21:43:21 +00:00
|
|
|
return;
|
|
|
|
reset();
|
|
|
|
|
2021-02-12 09:56:48 +00:00
|
|
|
std::vector<dive *> dives;
|
|
|
|
if (restrictDives) {
|
|
|
|
std::vector<dive *> visible = DiveFilter::instance()->visibleDives();
|
|
|
|
dives.reserve(visible.size());
|
|
|
|
for (dive *d: visible) {
|
|
|
|
// binary search
|
|
|
|
auto it = std::lower_bound(restrictedDives.begin(), restrictedDives.end(), d);
|
|
|
|
if (it != restrictedDives.end() && *it == d)
|
|
|
|
dives.push_back(d);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
dives = DiveFilter::instance()->visibleDives();
|
|
|
|
}
|
2021-01-01 21:43:21 +00:00
|
|
|
switch (state.type) {
|
|
|
|
case ChartType::DiscreteBar:
|
2021-12-31 17:29:06 +00:00
|
|
|
return plotBarChart(dives, state.subtype, state.sortMode1, state.var1, state.var1Binner,
|
|
|
|
state.var2, state.var2Binner);
|
2021-01-01 21:43:21 +00:00
|
|
|
case ChartType::DiscreteValue:
|
2021-12-31 17:29:06 +00:00
|
|
|
return plotValueChart(dives, state.subtype, state.sortMode1,
|
|
|
|
state.var1, state.var1Binner, state.var2, state.var2Operation);
|
2021-01-01 21:43:21 +00:00
|
|
|
case ChartType::DiscreteCount:
|
2021-12-31 17:29:06 +00:00
|
|
|
return plotDiscreteCountChart(dives, state.subtype, state.sortMode1, state.var1, state.var1Binner);
|
2021-01-01 21:43:21 +00:00
|
|
|
case ChartType::Pie:
|
2021-12-31 17:29:06 +00:00
|
|
|
return plotPieChart(dives, state.sortMode1, state.var1, state.var1Binner);
|
2021-01-01 21:43:21 +00:00
|
|
|
case ChartType::DiscreteBox:
|
|
|
|
return plotDiscreteBoxChart(dives, state.var1, state.var1Binner, state.var2);
|
|
|
|
case ChartType::DiscreteScatter:
|
2021-01-19 08:54:39 +00:00
|
|
|
return plotDiscreteScatter(dives, state.var1, state.var1Binner, state.var2);
|
2021-01-01 21:43:21 +00:00
|
|
|
case ChartType::HistogramCount:
|
2021-01-19 08:54:39 +00:00
|
|
|
return plotHistogramCountChart(dives, state.subtype, state.var1, state.var1Binner);
|
2021-01-01 21:43:21 +00:00
|
|
|
case ChartType::HistogramValue:
|
|
|
|
return plotHistogramValueChart(dives, state.subtype, state.var1, state.var1Binner, state.var2,
|
2021-01-19 08:54:39 +00:00
|
|
|
state.var2Operation);
|
2021-01-01 21:43:21 +00:00
|
|
|
case ChartType::HistogramStacked:
|
|
|
|
return plotHistogramStackedChart(dives, state.subtype, state.var1, state.var1Binner,
|
2021-01-19 08:54:39 +00:00
|
|
|
state.var2, state.var2Binner);
|
2021-01-01 21:43:21 +00:00
|
|
|
case ChartType::HistogramBox:
|
|
|
|
return plotHistogramBoxChart(dives, state.var1, state.var1Binner, state.var2);
|
|
|
|
case ChartType::ScatterPlot:
|
|
|
|
return plotScatter(dives, state.var1, state.var2);
|
2021-01-02 23:31:55 +00:00
|
|
|
case ChartType::Invalid:
|
|
|
|
return;
|
2021-01-01 21:43:21 +00:00
|
|
|
default:
|
|
|
|
qWarning("Unknown chart type: %d", (int)state.type);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-19 08:54:39 +00:00
|
|
|
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);
|
|
|
|
|
2021-01-19 10:18:10 +00:00
|
|
|
if (regressionItem) {
|
|
|
|
regressionItem->setVisible(state.regression || state.confidence);
|
|
|
|
if (state.regression || state.confidence)
|
|
|
|
regressionItem->setFeatures(state.regression, state.confidence);
|
|
|
|
}
|
2021-01-19 08:54:39 +00:00
|
|
|
for (ChartItemPtr<QuartileMarker> &marker: quartileMarkers)
|
|
|
|
marker->setVisible(state.quartiles);
|
|
|
|
}
|
|
|
|
|
2021-01-01 21:43:21 +00:00
|
|
|
template<typename T>
|
|
|
|
CategoryAxis *StatsView::createCategoryAxis(const QString &name, const StatsBinner &binner,
|
|
|
|
const std::vector<T> &bins, bool isHorizontal)
|
|
|
|
{
|
|
|
|
std::vector<QString> labels;
|
|
|
|
labels.reserve(bins.size());
|
|
|
|
for (const auto &[bin, dummy]: bins)
|
|
|
|
labels.push_back(binner.format(*bin));
|
|
|
|
return createAxis<CategoryAxis>(name, labels, isHorizontal);
|
|
|
|
}
|
|
|
|
|
|
|
|
CountAxis *StatsView::createCountAxis(int maxVal, bool isHorizontal)
|
|
|
|
{
|
|
|
|
return createAxis<CountAxis>(StatsTranslations::tr("No. dives"), maxVal, isHorizontal);
|
|
|
|
}
|
|
|
|
|
|
|
|
// For "two-dimensionally" binned plots (eg. stacked bar or grouped bar):
|
2021-01-20 13:36:59 +00:00
|
|
|
// Dives for each bin on the independent variable, including the total counts for that bin.
|
|
|
|
struct BinDives {
|
2021-01-01 21:43:21 +00:00
|
|
|
StatsBinPtr bin;
|
2021-01-20 13:36:59 +00:00
|
|
|
std::vector<std::vector<dive *>> dives;
|
2021-01-01 21:43:21 +00:00
|
|
|
int total;
|
|
|
|
};
|
|
|
|
|
|
|
|
// The problem with bar plots is that for different category
|
|
|
|
// bins, we might get different value bins. So we have to keep track
|
|
|
|
// of our counts and adjust accordingly. That's a bit annoying.
|
|
|
|
// Perhaps we should determine the bins of all dives first and then
|
|
|
|
// query the counts for precisely those bins?
|
|
|
|
struct BarPlotData {
|
2021-01-20 13:36:59 +00:00
|
|
|
std::vector<BinDives> hbins; // For each category bin the counts for all value bins
|
2021-01-01 21:43:21 +00:00
|
|
|
std::vector<StatsBinPtr> vbins;
|
|
|
|
std::vector<QString> vbinNames;
|
|
|
|
int maxCount; // Highest count of any bin-combination
|
|
|
|
int maxCategoryCount; // Highest count of any category bin
|
|
|
|
// Attention: categoryBin argument will be consumed!
|
|
|
|
BarPlotData(std::vector<StatsBinDives> &categoryBins, const StatsBinner &valuebinner);
|
|
|
|
};
|
|
|
|
|
|
|
|
BarPlotData::BarPlotData(std::vector<StatsBinDives> &categoryBins, const StatsBinner &valueBinner) :
|
|
|
|
maxCount(0), maxCategoryCount(0)
|
|
|
|
{
|
|
|
|
for (auto &[bin, dives]: categoryBins) {
|
|
|
|
// This moves the bin - the original pointer is invalidated
|
2021-01-20 13:36:59 +00:00
|
|
|
hbins.push_back({ std::move(bin), std::vector<std::vector<dive *>>(vbins.size()), 0 });
|
|
|
|
for (auto &[vbin, dives]: valueBinner.bin_dives(dives, false)) {
|
2021-01-01 21:43:21 +00:00
|
|
|
// Note: we assume that the bins are sorted!
|
|
|
|
auto it = std::lower_bound(vbins.begin(), vbins.end(), vbin,
|
|
|
|
[] (const StatsBinPtr &p, const StatsBinPtr &bin)
|
|
|
|
{ return *p < *bin; });
|
|
|
|
ssize_t pos = it - vbins.begin();
|
|
|
|
if (it == vbins.end() || **it != *vbin) {
|
|
|
|
// Add a new value bin.
|
|
|
|
// Attn: this invalidates "vbin", which must not be used henceforth!
|
|
|
|
vbins.insert(it, std::move(vbin));
|
|
|
|
// Fix the old arrays
|
2021-01-20 13:36:59 +00:00
|
|
|
for (auto &[bin, v, total]: hbins)
|
|
|
|
v.insert(v.begin() + pos, std::vector<dive *>());
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
2021-01-20 13:36:59 +00:00
|
|
|
int count = (int)dives.size();
|
|
|
|
hbins.back().dives[pos] = std::move(dives);
|
|
|
|
hbins.back().total += count;
|
2021-01-01 21:43:21 +00:00
|
|
|
if (count > maxCount)
|
|
|
|
maxCount = count;
|
|
|
|
}
|
2021-01-20 13:36:59 +00:00
|
|
|
maxCategoryCount = std::max(maxCategoryCount, hbins.back().total);
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
vbinNames.reserve(vbins.size());
|
|
|
|
for (const auto &vbin: vbins)
|
|
|
|
vbinNames.push_back(valueBinner.formatWithUnit(*vbin));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Formats "x (y%)" as either a single or two strings for horizontal and non-horizontal cases, respectively.
|
|
|
|
static std::vector<QString> makePercentageLabels(int count, int total, bool isHorizontal)
|
|
|
|
{
|
|
|
|
double percentage = count * 100.0 / total;
|
|
|
|
QString countString = QString("%L1").arg(count);
|
|
|
|
QString percentageString = QString("%L1%").arg(percentage, 0, 'f', 1);
|
|
|
|
if (isHorizontal)
|
2021-01-03 10:19:56 +00:00
|
|
|
return { QString("%1 (%2)").arg(countString, percentageString) };
|
2021-01-01 21:43:21 +00:00
|
|
|
else
|
|
|
|
return { countString, percentageString };
|
|
|
|
}
|
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
// From a list of dive bins, make (dives, label) pairs, where the label
|
2021-01-01 21:43:21 +00:00
|
|
|
// formats the total number and the percentage of dives.
|
2021-01-20 13:36:59 +00:00
|
|
|
static std::vector<BarSeries::MultiItem::Item> makeMultiItems(std::vector<std::vector<dive *>> bins, int total, bool isHorizontal)
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
2021-01-20 13:36:59 +00:00
|
|
|
std::vector<BarSeries::MultiItem::Item> res;
|
|
|
|
res.reserve(bins.size());
|
|
|
|
for (std::vector<dive*> &bin: bins) {
|
|
|
|
std::vector<QString> label = makePercentageLabels((int)bin.size(), total, isHorizontal);
|
|
|
|
res.push_back({ std::move(bin), std::move(label) });
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
2021-01-20 13:36:59 +00:00
|
|
|
return res;
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::plotBarChart(const std::vector<dive *> &dives,
|
2021-12-31 17:29:06 +00:00
|
|
|
ChartSubType subType, ChartSortMode sortMode,
|
2021-01-01 21:43:21 +00:00
|
|
|
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
|
2021-01-19 08:54:39 +00:00
|
|
|
const StatsVariable *valueVariable, const StatsBinner *valueBinner)
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
|
|
|
if (!categoryBinner || !valueBinner)
|
|
|
|
return;
|
|
|
|
|
|
|
|
setTitle(valueVariable->nameWithBinnerUnit(*valueBinner));
|
|
|
|
|
|
|
|
std::vector<StatsBinDives> categoryBins = categoryBinner->bin_dives(dives, false);
|
|
|
|
|
2021-12-31 17:29:06 +00:00
|
|
|
if (sortMode == ChartSortMode::Count) {
|
|
|
|
// Note: we sort by count in reverse order, as this is probably what the user desires(?).
|
|
|
|
std::sort(categoryBins.begin(), categoryBins.end(),
|
|
|
|
[](const StatsBinDives &b1, const StatsBinDives &b2)
|
|
|
|
{ return b1.value.size() > b2.value.size(); });
|
|
|
|
}
|
|
|
|
|
2021-01-01 21:43:21 +00:00
|
|
|
bool isStacked = subType == ChartSubType::VerticalStacked || subType == ChartSubType::HorizontalStacked;
|
|
|
|
bool isHorizontal = subType == ChartSubType::HorizontalGrouped || subType == ChartSubType::HorizontalStacked;
|
|
|
|
|
|
|
|
// Construct the histogram axis now, because the pointers to the bins
|
|
|
|
// will be moved away when constructing BarPlotData below.
|
|
|
|
CategoryAxis *catAxis = createCategoryAxis(categoryVariable->nameWithBinnerUnit(*categoryBinner),
|
|
|
|
*categoryBinner, categoryBins, !isHorizontal);
|
|
|
|
|
|
|
|
BarPlotData data(categoryBins, *valueBinner);
|
|
|
|
|
|
|
|
int maxVal = isStacked ? data.maxCategoryCount : data.maxCount;
|
|
|
|
CountAxis *valAxis = createCountAxis(maxVal, isHorizontal);
|
|
|
|
|
|
|
|
if (isHorizontal)
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(valAxis, catAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
else
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(catAxis, valAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
// Paint legend first, because the bin-names will be moved away from.
|
2021-01-19 08:54:39 +00:00
|
|
|
legend = createChartItem<Legend>(data.vbinNames);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
std::vector<BarSeries::MultiItem> items;
|
2021-01-20 13:36:59 +00:00
|
|
|
items.reserve(data.hbins.size());
|
2021-01-01 21:43:21 +00:00
|
|
|
double pos = 0.0;
|
2021-01-20 13:36:59 +00:00
|
|
|
for (auto &[hbin, dives, total]: data.hbins) {
|
|
|
|
items.push_back({ pos - 0.5, pos + 0.5, makeMultiItems(std::move(dives), total, isHorizontal),
|
2021-01-01 21:43:21 +00:00
|
|
|
categoryBinner->formatWithUnit(*hbin) });
|
|
|
|
pos += 1.0;
|
|
|
|
}
|
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
createSeries<BarSeries>(isHorizontal, isStacked, categoryVariable->name(), valueVariable, std::move(data.vbinNames), std::move(items));
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const double NaN = std::numeric_limits<double>::quiet_NaN();
|
|
|
|
|
|
|
|
// These templates are used to extract min and max y-values of various lists.
|
|
|
|
// A bit too convoluted for my tastes - can we make that simpler?
|
|
|
|
static std::pair<double, double> getMinMaxValueBase(const std::vector<StatsValue> &values)
|
|
|
|
{
|
|
|
|
// Attention: this supposes that the list is sorted!
|
|
|
|
return values.empty() ? std::make_pair(NaN, NaN) : std::make_pair(values.front().v, values.back().v);
|
|
|
|
}
|
|
|
|
static std::pair<double, double> getMinMaxValueBase(double v)
|
|
|
|
{
|
|
|
|
return { v, v };
|
|
|
|
}
|
|
|
|
static std::pair<double, double> getMinMaxValueBase(const StatsQuartiles &q)
|
|
|
|
{
|
|
|
|
return { q.min, q.max };
|
|
|
|
}
|
|
|
|
static std::pair<double, double> getMinMaxValueBase(const StatsScatterItem &s)
|
|
|
|
{
|
|
|
|
return { s.y, s.y };
|
|
|
|
}
|
|
|
|
template <typename T1, typename T2>
|
|
|
|
static std::pair<double, double> getMinMaxValueBase(const std::pair<T1, T2> &p)
|
|
|
|
{
|
|
|
|
return getMinMaxValueBase(p.second);
|
|
|
|
}
|
|
|
|
template <typename T>
|
|
|
|
static std::pair<double, double> getMinMaxValueBase(const StatsBinValue<T> &v)
|
|
|
|
{
|
|
|
|
return getMinMaxValueBase(v.value);
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
static void updateMinMax(double &min, double &max, bool &found, const T &v)
|
|
|
|
{
|
|
|
|
const auto [mi, ma] = getMinMaxValueBase(v);
|
|
|
|
if (!std::isnan(mi) && mi < min)
|
|
|
|
min = mi;
|
|
|
|
if (!std::isnan(ma) && ma > max)
|
|
|
|
max = ma;
|
|
|
|
if (!std::isnan(mi) || !std::isnan(ma))
|
|
|
|
found = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
static std::pair<double, double> getMinMaxValue(const std::vector<T> &values)
|
|
|
|
{
|
|
|
|
double min = 1e14, max = 0.0;
|
|
|
|
bool found = false;
|
|
|
|
for (const T &v: values)
|
|
|
|
updateMinMax(min, max, found, v);
|
|
|
|
return found ? std::make_pair(min, max) : std::make_pair(0.0, 0.0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static std::pair<double, double> getMinMaxValue(const std::vector<StatsBinOp> &bins, StatsOperation op)
|
|
|
|
{
|
|
|
|
double min = 1e14, max = 0.0;
|
|
|
|
bool found = false;
|
|
|
|
for (auto &[bin, res]: bins) {
|
|
|
|
if (!res.isValid())
|
|
|
|
continue;
|
|
|
|
updateMinMax(min, max, found, res.get(op));
|
|
|
|
}
|
|
|
|
return found ? std::make_pair(min, max) : std::make_pair(0.0, 0.0);
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::plotValueChart(const std::vector<dive *> &dives,
|
2021-12-31 17:29:06 +00:00
|
|
|
ChartSubType subType, ChartSortMode sortMode,
|
2021-01-01 21:43:21 +00:00
|
|
|
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
|
2021-01-19 08:54:39 +00:00
|
|
|
const StatsVariable *valueVariable, StatsOperation valueAxisOperation)
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
|
|
|
if (!categoryBinner)
|
|
|
|
return;
|
|
|
|
|
|
|
|
setTitle(QStringLiteral("%1 (%2)").arg(valueVariable->name(), StatsVariable::operationName(valueAxisOperation)));
|
|
|
|
|
|
|
|
std::vector<StatsBinOp> categoryBins = valueVariable->bin_operations(*categoryBinner, dives, false);
|
|
|
|
|
|
|
|
// If there is nothing to display, quit
|
|
|
|
if (categoryBins.empty())
|
|
|
|
return;
|
|
|
|
|
2021-12-31 17:29:06 +00:00
|
|
|
if (sortMode == ChartSortMode::Count) {
|
|
|
|
// Note: we sort by count in reverse order, as this is probably what the user desires(?).
|
|
|
|
std::sort(categoryBins.begin(), categoryBins.end(),
|
|
|
|
[](const StatsBinOp &b1, const StatsBinOp &b2)
|
|
|
|
{ return b1.value.dives.size() > b2.value.dives.size(); });
|
|
|
|
} else if (sortMode == ChartSortMode::Value) {
|
|
|
|
std::sort(categoryBins.begin(), categoryBins.end(),
|
|
|
|
[valueAxisOperation](const StatsBinOp &b1, const StatsBinOp &b2)
|
|
|
|
{ return b1.value.get(valueAxisOperation) < b2.value.get(valueAxisOperation); });
|
|
|
|
}
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
bool isHorizontal = subType == ChartSubType::Horizontal;
|
|
|
|
const auto [minValue, maxValue] = getMinMaxValue(categoryBins, valueAxisOperation);
|
|
|
|
int decimals = valueVariable->decimals();
|
|
|
|
CategoryAxis *catAxis = createCategoryAxis(categoryVariable->nameWithBinnerUnit(*categoryBinner),
|
|
|
|
*categoryBinner, categoryBins, !isHorizontal);
|
|
|
|
ValueAxis *valAxis = createAxis<ValueAxis>(valueVariable->nameWithUnit(),
|
|
|
|
0.0, maxValue, valueVariable->decimals(), isHorizontal);
|
|
|
|
|
|
|
|
if (isHorizontal)
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(valAxis, catAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
else
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(catAxis, valAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
std::vector<BarSeries::ValueItem> items;
|
|
|
|
items.reserve(categoryBins.size());
|
|
|
|
double pos = 0.0;
|
|
|
|
QString unit = valueVariable->unitSymbol();
|
|
|
|
for (auto &[bin, res]: categoryBins) {
|
|
|
|
if (res.isValid()) {
|
|
|
|
double height = res.get(valueAxisOperation);
|
|
|
|
QString value = QString("%L1").arg(height, 0, 'f', decimals);
|
2021-01-19 08:54:39 +00:00
|
|
|
std::vector<QString> label = std::vector<QString> { value };
|
2021-01-01 21:43:21 +00:00
|
|
|
items.push_back({ pos - 0.5, pos + 0.5, height, label,
|
|
|
|
categoryBinner->formatWithUnit(*bin), res });
|
|
|
|
}
|
|
|
|
pos += 1.0;
|
|
|
|
}
|
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
createSeries<BarSeries>(isHorizontal, categoryVariable->name(), valueVariable, std::move(items));
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
static int getTotalCount(const std::vector<StatsBinDives> &bins)
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
|
|
|
int total = 0;
|
2021-01-20 13:36:59 +00:00
|
|
|
for (const auto &[bin, dives]: bins)
|
|
|
|
total += (int)dives.size();
|
2021-01-01 21:43:21 +00:00
|
|
|
return total;
|
|
|
|
}
|
|
|
|
|
|
|
|
template<typename T>
|
|
|
|
static int getMaxCount(const std::vector<T> &bins)
|
|
|
|
{
|
|
|
|
int res = 0;
|
2021-01-20 13:36:59 +00:00
|
|
|
for (auto const &[dummy, dives]: bins)
|
|
|
|
res = std::max(res, (int)(dives.size()));
|
2021-01-01 21:43:21 +00:00
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::plotDiscreteCountChart(const std::vector<dive *> &dives,
|
2021-12-31 17:29:06 +00:00
|
|
|
ChartSubType subType, ChartSortMode sortMode,
|
2021-01-19 08:54:39 +00:00
|
|
|
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner)
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
|
|
|
if (!categoryBinner)
|
|
|
|
return;
|
|
|
|
|
|
|
|
setTitle(categoryVariable->nameWithBinnerUnit(*categoryBinner));
|
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
std::vector<StatsBinDives> categoryBins = categoryBinner->bin_dives(dives, false);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
// If there is nothing to display, quit
|
|
|
|
if (categoryBins.empty())
|
|
|
|
return;
|
|
|
|
|
2021-12-31 17:29:06 +00:00
|
|
|
if (sortMode == ChartSortMode::Count) {
|
|
|
|
// Note: we sort by count in reverse order, as this is probably what the user desires(?).
|
|
|
|
std::sort(categoryBins.begin(), categoryBins.end(),
|
|
|
|
[](const StatsBinDives &b1, const StatsBinDives &b2)
|
|
|
|
{ return b1.value.size() > b2.value.size(); });
|
|
|
|
}
|
|
|
|
|
2021-01-01 21:43:21 +00:00
|
|
|
int total = getTotalCount(categoryBins);
|
|
|
|
bool isHorizontal = subType != ChartSubType::Vertical;
|
|
|
|
|
|
|
|
CategoryAxis *catAxis = createCategoryAxis(categoryVariable->nameWithBinnerUnit(*categoryBinner),
|
|
|
|
*categoryBinner, categoryBins, !isHorizontal);
|
|
|
|
|
|
|
|
int maxCount = getMaxCount(categoryBins);
|
|
|
|
CountAxis *valAxis = createCountAxis(maxCount, isHorizontal);
|
|
|
|
|
|
|
|
if (isHorizontal)
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(valAxis, catAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
else
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(catAxis, valAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
std::vector<BarSeries::CountItem> items;
|
|
|
|
items.reserve(categoryBins.size());
|
|
|
|
double pos = 0.0;
|
2021-01-20 13:36:59 +00:00
|
|
|
for (auto const &[bin, dives]: categoryBins) {
|
|
|
|
std::vector<QString> label = makePercentageLabels((int)dives.size(), total, isHorizontal);
|
|
|
|
items.push_back({ pos - 0.5, pos + 0.5, std::move(dives), label,
|
2021-01-01 21:43:21 +00:00
|
|
|
categoryBinner->formatWithUnit(*bin), total });
|
|
|
|
pos += 1.0;
|
|
|
|
}
|
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
createSeries<BarSeries>(isHorizontal, categoryVariable->name(), std::move(items));
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
2021-12-31 17:29:06 +00:00
|
|
|
void StatsView::plotPieChart(const std::vector<dive *> &dives, ChartSortMode sortMode,
|
2021-01-19 08:54:39 +00:00
|
|
|
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner)
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
|
|
|
if (!categoryBinner)
|
|
|
|
return;
|
|
|
|
|
|
|
|
setTitle(categoryVariable->nameWithBinnerUnit(*categoryBinner));
|
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
std::vector<StatsBinDives> categoryBins = categoryBinner->bin_dives(dives, false);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
// If there is nothing to display, quit
|
|
|
|
if (categoryBins.empty())
|
|
|
|
return;
|
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
std::vector<std::pair<QString, std::vector<dive *>>> data;
|
2021-01-01 21:43:21 +00:00
|
|
|
data.reserve(categoryBins.size());
|
2021-01-20 13:36:59 +00:00
|
|
|
for (auto &[bin, dives]: categoryBins)
|
|
|
|
data.emplace_back(categoryBinner->formatWithUnit(*bin), std::move(dives));
|
2021-01-01 21:43:21 +00:00
|
|
|
|
2021-12-31 17:29:06 +00:00
|
|
|
PieSeries *series = createSeries<PieSeries>(categoryVariable->name(), std::move(data), sortMode);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
2021-01-19 08:54:39 +00:00
|
|
|
legend = createChartItem<Legend>(series->binNames());
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::plotDiscreteBoxChart(const std::vector<dive *> &dives,
|
|
|
|
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
|
|
|
|
const StatsVariable *valueVariable)
|
|
|
|
{
|
|
|
|
if (!categoryBinner)
|
|
|
|
return;
|
|
|
|
|
|
|
|
setTitle(valueVariable->name());
|
|
|
|
|
|
|
|
std::vector<StatsBinQuartiles> categoryBins = valueVariable->bin_quartiles(*categoryBinner, dives, false);
|
|
|
|
|
|
|
|
// If there is nothing to display, quit
|
|
|
|
if (categoryBins.empty())
|
|
|
|
return;
|
|
|
|
|
|
|
|
CategoryAxis *catAxis = createCategoryAxis(categoryVariable->nameWithBinnerUnit(*categoryBinner),
|
|
|
|
*categoryBinner, categoryBins, true);
|
|
|
|
|
|
|
|
auto [minY, maxY] = getMinMaxValue(categoryBins);
|
|
|
|
ValueAxis *valueAxis = createAxis<ValueAxis>(valueVariable->nameWithUnit(),
|
|
|
|
minY, maxY, valueVariable->decimals(), false);
|
|
|
|
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(catAxis, valueAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
BoxSeries *series = createSeries<BoxSeries>(valueVariable->name(), valueVariable->unitSymbol(), valueVariable->decimals());
|
|
|
|
|
|
|
|
double pos = 0.0;
|
|
|
|
for (auto &[bin, q]: categoryBins) {
|
|
|
|
if (q.isValid())
|
|
|
|
series->append(pos - 0.5, pos + 0.5, q, categoryBinner->formatWithUnit(*bin));
|
|
|
|
pos += 1.0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::plotDiscreteScatter(const std::vector<dive *> &dives,
|
|
|
|
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
|
2021-01-19 08:54:39 +00:00
|
|
|
const StatsVariable *valueVariable)
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
|
|
|
if (!categoryBinner)
|
|
|
|
return;
|
|
|
|
|
|
|
|
setTitle(valueVariable->name());
|
|
|
|
|
|
|
|
std::vector<StatsBinValues> categoryBins = valueVariable->bin_values(*categoryBinner, dives, false);
|
|
|
|
|
|
|
|
// If there is nothing to display, quit
|
|
|
|
if (categoryBins.empty())
|
|
|
|
return;
|
|
|
|
|
|
|
|
CategoryAxis *catAxis = createCategoryAxis(categoryVariable->nameWithBinnerUnit(*categoryBinner),
|
|
|
|
*categoryBinner, categoryBins, true);
|
|
|
|
|
|
|
|
auto [minValue, maxValue] = getMinMaxValue(categoryBins);
|
|
|
|
|
|
|
|
ValueAxis *valAxis = createAxis<ValueAxis>(valueVariable->nameWithUnit(),
|
|
|
|
minValue, maxValue, valueVariable->decimals(), false);
|
|
|
|
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(catAxis, valAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
ScatterSeries *series = createSeries<ScatterSeries>(*categoryVariable, *valueVariable);
|
|
|
|
|
|
|
|
double x = 0.0;
|
|
|
|
for (const auto &[bin, array]: categoryBins) {
|
|
|
|
for (auto [v, d]: array)
|
|
|
|
series->append(d, x, v);
|
2021-01-19 08:54:39 +00:00
|
|
|
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));
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
x += 1.0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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>
|
|
|
|
HistogramAxis *StatsView::createHistogramAxis(const QString &name, const StatsBinner &binner,
|
|
|
|
const std::vector<T> &bins, bool isHorizontal)
|
|
|
|
{
|
|
|
|
std::vector<HistogramAxisEntry> labels;
|
|
|
|
for (auto const &[bin, dummy]: bins) {
|
|
|
|
QString label = binner.formatLowerBound(*bin);
|
|
|
|
double lowerBound = binner.lowerBoundToFloat(*bin);
|
|
|
|
bool prefer = binner.preferBin(*bin);
|
|
|
|
labels.push_back({ label, lowerBound, prefer });
|
|
|
|
}
|
|
|
|
|
|
|
|
const StatsBin &lastBin = *bins.back().bin;
|
|
|
|
QString lastLabel = binner.formatUpperBound(lastBin);
|
|
|
|
double upperBound = binner.upperBoundToFloat(lastBin);
|
|
|
|
labels.push_back({ lastLabel, upperBound, false });
|
|
|
|
|
|
|
|
return createAxis<HistogramAxis>(name, std::move(labels), isHorizontal);
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::plotHistogramCountChart(const std::vector<dive *> &dives,
|
|
|
|
ChartSubType subType,
|
2021-01-19 08:54:39 +00:00
|
|
|
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner)
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
|
|
|
if (!categoryBinner)
|
|
|
|
return;
|
|
|
|
|
|
|
|
setTitle(categoryVariable->name());
|
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
std::vector<StatsBinDives> categoryBins = categoryBinner->bin_dives(dives, true);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
// If there is nothing to display, quit
|
|
|
|
if (categoryBins.empty())
|
|
|
|
return;
|
|
|
|
|
|
|
|
bool isHorizontal = subType == ChartSubType::Horizontal;
|
|
|
|
HistogramAxis *catAxis = createHistogramAxis(categoryVariable->nameWithBinnerUnit(*categoryBinner),
|
|
|
|
*categoryBinner, categoryBins, !isHorizontal);
|
|
|
|
|
|
|
|
int maxCategoryCount = getMaxCount(categoryBins);
|
|
|
|
int total = getTotalCount(categoryBins);
|
|
|
|
|
|
|
|
StatsAxis *valAxis = createCountAxis(maxCategoryCount, isHorizontal);
|
|
|
|
|
|
|
|
if (isHorizontal)
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(valAxis, catAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
else
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(catAxis, valAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
std::vector<BarSeries::CountItem> items;
|
|
|
|
items.reserve(categoryBins.size());
|
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
// Attention: this moves away the dives
|
|
|
|
for (auto &[bin, dives]: categoryBins) {
|
2021-01-01 21:43:21 +00:00
|
|
|
double lowerBound = categoryBinner->lowerBoundToFloat(*bin);
|
|
|
|
double upperBound = categoryBinner->upperBoundToFloat(*bin);
|
2021-01-20 13:36:59 +00:00
|
|
|
std::vector<QString> label = makePercentageLabels((int)dives.size(), total, isHorizontal);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
items.push_back({ lowerBound, upperBound, std::move(dives), label,
|
2021-01-01 21:43:21 +00:00
|
|
|
categoryBinner->formatWithUnit(*bin), total });
|
|
|
|
}
|
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
createSeries<BarSeries>(isHorizontal, categoryVariable->name(), std::move(items));
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
if (categoryVariable->type() == StatsVariable::Type::Numeric) {
|
2021-01-19 08:54:39 +00:00
|
|
|
double mean = categoryVariable->mean(dives);
|
|
|
|
if (!std::isnan(mean))
|
2021-02-16 16:05:39 +00:00
|
|
|
meanMarker = createChartItem<HistogramMarker>(mean, isHorizontal, currentTheme->meanMarkerColor, xAxis, yAxis);
|
2021-01-19 08:54:39 +00:00
|
|
|
double median = categoryVariable->quartiles(dives).q2;
|
|
|
|
if (!std::isnan(median))
|
2021-02-16 16:05:39 +00:00
|
|
|
medianMarker = createChartItem<HistogramMarker>(median, isHorizontal, currentTheme->medianMarkerColor, xAxis, yAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::plotHistogramValueChart(const std::vector<dive *> &dives,
|
|
|
|
ChartSubType subType,
|
|
|
|
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
|
2021-01-19 08:54:39 +00:00
|
|
|
const StatsVariable *valueVariable, StatsOperation valueAxisOperation)
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
|
|
|
if (!categoryBinner)
|
|
|
|
return;
|
|
|
|
|
|
|
|
setTitle(QStringLiteral("%1 (%2)").arg(valueVariable->name(), StatsVariable::operationName(valueAxisOperation)));
|
|
|
|
|
|
|
|
std::vector<StatsBinOp> categoryBins = valueVariable->bin_operations(*categoryBinner, dives, true);
|
|
|
|
|
|
|
|
// If there is nothing to display, quit
|
|
|
|
if (categoryBins.empty())
|
|
|
|
return;
|
|
|
|
|
|
|
|
bool isHorizontal = subType == ChartSubType::Horizontal;
|
|
|
|
HistogramAxis *catAxis = createHistogramAxis(categoryVariable->nameWithBinnerUnit(*categoryBinner),
|
|
|
|
*categoryBinner, categoryBins, !isHorizontal);
|
|
|
|
|
|
|
|
const auto [minValue, maxValue] = getMinMaxValue(categoryBins, valueAxisOperation);
|
|
|
|
|
|
|
|
int decimals = valueVariable->decimals();
|
|
|
|
ValueAxis *valAxis = createAxis<ValueAxis>(valueVariable->nameWithUnit(),
|
|
|
|
0.0, maxValue, decimals, isHorizontal);
|
|
|
|
|
|
|
|
if (isHorizontal)
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(valAxis, catAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
else
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(catAxis, valAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
std::vector<BarSeries::ValueItem> items;
|
|
|
|
items.reserve(categoryBins.size());
|
|
|
|
|
|
|
|
QString unit = valueVariable->unitSymbol();
|
|
|
|
for (auto const &[bin, res]: categoryBins) {
|
|
|
|
if (!res.isValid())
|
|
|
|
continue;
|
|
|
|
double height = res.get(valueAxisOperation);
|
|
|
|
double lowerBound = categoryBinner->lowerBoundToFloat(*bin);
|
|
|
|
double upperBound = categoryBinner->upperBoundToFloat(*bin);
|
|
|
|
QString value = QString("%L1").arg(height, 0, 'f', decimals);
|
2021-01-19 08:54:39 +00:00
|
|
|
std::vector<QString> label = std::vector<QString> { value };
|
2021-01-01 21:43:21 +00:00
|
|
|
items.push_back({ lowerBound, upperBound, height, label,
|
|
|
|
categoryBinner->formatWithUnit(*bin), res });
|
|
|
|
}
|
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
createSeries<BarSeries>(isHorizontal, categoryVariable->name(), valueVariable, std::move(items));
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::plotHistogramStackedChart(const std::vector<dive *> &dives,
|
|
|
|
ChartSubType subType,
|
|
|
|
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
|
2021-01-19 08:54:39 +00:00
|
|
|
const StatsVariable *valueVariable, const StatsBinner *valueBinner)
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
|
|
|
if (!categoryBinner || !valueBinner)
|
|
|
|
return;
|
|
|
|
|
|
|
|
setTitle(valueVariable->nameWithBinnerUnit(*valueBinner));
|
|
|
|
|
|
|
|
std::vector<StatsBinDives> categoryBins = categoryBinner->bin_dives(dives, true);
|
|
|
|
|
|
|
|
// Construct the histogram axis now, because the pointers to the bins
|
|
|
|
// will be moved away when constructing BarPlotData below.
|
|
|
|
bool isHorizontal = subType == ChartSubType::HorizontalStacked;
|
|
|
|
HistogramAxis *catAxis = createHistogramAxis(categoryVariable->nameWithBinnerUnit(*categoryBinner),
|
|
|
|
*categoryBinner, categoryBins, !isHorizontal);
|
|
|
|
|
|
|
|
BarPlotData data(categoryBins, *valueBinner);
|
2021-01-19 08:54:39 +00:00
|
|
|
legend = createChartItem<Legend>(data.vbinNames);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
CountAxis *valAxis = createCountAxis(data.maxCategoryCount, isHorizontal);
|
|
|
|
|
|
|
|
if (isHorizontal)
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(valAxis, catAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
else
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(catAxis, valAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
std::vector<BarSeries::MultiItem> items;
|
2021-01-20 13:36:59 +00:00
|
|
|
items.reserve(data.hbins.size());
|
2021-01-01 21:43:21 +00:00
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
for (auto &[hbin, dives, total]: data.hbins) {
|
2021-01-01 21:43:21 +00:00
|
|
|
double lowerBound = categoryBinner->lowerBoundToFloat(*hbin);
|
|
|
|
double upperBound = categoryBinner->upperBoundToFloat(*hbin);
|
2021-01-20 13:36:59 +00:00
|
|
|
items.push_back({ lowerBound, upperBound, makeMultiItems(std::move(dives), total, isHorizontal),
|
2021-01-01 21:43:21 +00:00
|
|
|
categoryBinner->formatWithUnit(*hbin) });
|
|
|
|
}
|
|
|
|
|
2021-01-20 13:36:59 +00:00
|
|
|
createSeries<BarSeries>(isHorizontal, true, categoryVariable->name(), valueVariable, std::move(data.vbinNames), std::move(items));
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::plotHistogramBoxChart(const std::vector<dive *> &dives,
|
|
|
|
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
|
|
|
|
const StatsVariable *valueVariable)
|
|
|
|
{
|
|
|
|
if (!categoryBinner)
|
|
|
|
return;
|
|
|
|
|
|
|
|
setTitle(valueVariable->name());
|
|
|
|
|
|
|
|
std::vector<StatsBinQuartiles> categoryBins = valueVariable->bin_quartiles(*categoryBinner, dives, true);
|
|
|
|
|
|
|
|
// If there is nothing to display, quit
|
|
|
|
if (categoryBins.empty())
|
|
|
|
return;
|
|
|
|
|
|
|
|
HistogramAxis *catAxis = createHistogramAxis(categoryVariable->nameWithBinnerUnit(*categoryBinner),
|
|
|
|
*categoryBinner, categoryBins, true);
|
|
|
|
|
|
|
|
auto [minY, maxY] = getMinMaxValue(categoryBins);
|
|
|
|
ValueAxis *valueAxis = createAxis<ValueAxis>(valueVariable->nameWithUnit(),
|
|
|
|
minY, maxY, valueVariable->decimals(), false);
|
|
|
|
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(catAxis, valueAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
BoxSeries *series = createSeries<BoxSeries>(valueVariable->name(), valueVariable->unitSymbol(), valueVariable->decimals());
|
|
|
|
|
|
|
|
for (auto &[bin, q]: categoryBins) {
|
|
|
|
if (!q.isValid())
|
|
|
|
continue;
|
|
|
|
double lowerBound = categoryBinner->lowerBoundToFloat(*bin);
|
|
|
|
double upperBound = categoryBinner->upperBoundToFloat(*bin);
|
|
|
|
series->append(lowerBound, upperBound, q, categoryBinner->formatWithUnit(*bin));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool is_linear_regression(int sample_size, double cov, double sx2, double sy2)
|
|
|
|
{
|
|
|
|
// One point never, two points always form a line
|
|
|
|
if (sample_size < 2)
|
|
|
|
return false;
|
|
|
|
if (sample_size <= 2)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
const double tval[] = { 12.709, 4.303, 3.182, 2.776, 2.571, 2.447, 2.201, 2.120, 2.080, 2.056, 2.021, 1.960, 1.960 };
|
|
|
|
const int t_df[] = { 1, 2, 3, 4, 5, 6, 11, 16, 21, 26, 40, 100, 100000 };
|
|
|
|
int df = sample_size - 2; // Following is the one-tailed t-value at p < 0.05 and [sample_size - 2] degrees of freedom for the dive data:
|
|
|
|
double t = (cov / sx2) / sqrt(((sy2 - cov * cov / sx2) / (double)df) / sx2);
|
|
|
|
for (int i = std::size(tval) - 2; i >= 0; i--) { // We do linear interpolation rather than having a large lookup table.
|
|
|
|
if (df >= t_df[i]) { // Look up the appropriate reference t-value at p < 0.05 and df degrees of freedom
|
|
|
|
double t_lookup = tval[i] - (tval[i] - tval[i+1]) * (df - t_df[i]) / (t_df[i+1] - t_df[i]);
|
|
|
|
return abs(t) >= t_lookup;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true; // can't happen, as we tested for sample_size above.
|
|
|
|
}
|
|
|
|
|
2021-01-09 23:12:41 +00:00
|
|
|
// Returns the coefficients a,b of the line y = ax + b
|
|
|
|
// as well as the variance of the residuals (averaged residual squared) as res2
|
|
|
|
// and r^2 = 1.0 - variance of data / res2 which is the fraction of the variance of
|
|
|
|
// the data that is explained by the linear regression.
|
|
|
|
// If case of an undetermined regression or one with infinite slope, returns {nan, nan, 0.0, 0.0}
|
|
|
|
|
|
|
|
static struct regression_data linear_regression(const std::vector<StatsScatterItem> &v)
|
2021-01-01 21:43:21 +00:00
|
|
|
{
|
2021-01-12 18:39:25 +00:00
|
|
|
struct regression_data ret = { .a = NaN, .b = NaN, .res2 = 0.0, .r2 = 0.0, .sx2 = 0.0, .xavg = 0.0};
|
|
|
|
ret.n = v.size();
|
|
|
|
if (ret.n < 2)
|
|
|
|
return ret;
|
2021-01-01 21:43:21 +00:00
|
|
|
// First, calculate the x and y average
|
|
|
|
double avg_x = 0.0, avg_y = 0.0;
|
|
|
|
for (auto [x, y, d]: v) {
|
|
|
|
avg_x += x;
|
|
|
|
avg_y += y;
|
|
|
|
}
|
2021-01-12 18:39:25 +00:00
|
|
|
avg_x /= ret.n;
|
|
|
|
avg_y /= ret.n;
|
2021-01-01 21:43:21 +00:00
|
|
|
|
|
|
|
double cov = 0.0, sx2 = 0.0, sy2 = 0.0;
|
|
|
|
for (auto [x, y, d]: v) {
|
|
|
|
cov += (x - avg_x) * (y - avg_y);
|
|
|
|
sx2 += (x - avg_x) * (x - avg_x);
|
|
|
|
sy2 += (y - avg_y) * (y - avg_y);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool is_linear = is_linear_regression((int)v.size(), cov, sx2, sy2);
|
|
|
|
|
|
|
|
if (fabs(sx2) < 1e-10 || !is_linear) // If t is not statistically significant, do not plot the regression line.
|
2021-01-12 18:39:25 +00:00
|
|
|
return ret;
|
|
|
|
ret.xavg = avg_x;
|
|
|
|
ret.sx2 = sx2;
|
|
|
|
ret.a = cov / sx2;
|
|
|
|
ret.b = avg_y - ret.a * avg_x;
|
2021-01-09 23:12:41 +00:00
|
|
|
|
|
|
|
for (auto [x, y, d]: v)
|
2021-01-12 18:39:25 +00:00
|
|
|
ret.res2 += (y - ret.a * x - ret.b) * (y - ret.a * x - ret.b);
|
|
|
|
ret.r2 = sy2 > 0.0 ? 1.0 - ret.res2 / sy2 : 1.0;
|
|
|
|
return ret;
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void StatsView::plotScatter(const std::vector<dive *> &dives, const StatsVariable *categoryVariable, const StatsVariable *valueVariable)
|
|
|
|
{
|
|
|
|
setTitle(StatsTranslations::tr("%1 vs. %2").arg(valueVariable->name(), categoryVariable->name()));
|
|
|
|
|
|
|
|
std::vector<StatsScatterItem> points = categoryVariable->scatter(*valueVariable, dives);
|
|
|
|
if (points.empty())
|
|
|
|
return;
|
|
|
|
|
|
|
|
double minX = points.front().x;
|
|
|
|
double maxX = points.back().x;
|
|
|
|
auto [minY, maxY] = getMinMaxValue(points);
|
|
|
|
|
|
|
|
StatsAxis *axisX = categoryVariable->type() == StatsVariable::Type::Continuous ?
|
|
|
|
static_cast<StatsAxis *>(createAxis<DateAxis>(categoryVariable->nameWithUnit(),
|
|
|
|
minX, maxX, true)) :
|
|
|
|
static_cast<StatsAxis *>(createAxis<ValueAxis>(categoryVariable->nameWithUnit(),
|
|
|
|
minX, maxX, categoryVariable->decimals(), true));
|
|
|
|
|
|
|
|
StatsAxis *axisY = createAxis<ValueAxis>(valueVariable->nameWithUnit(), minY, maxY, valueVariable->decimals(), false);
|
|
|
|
|
2021-01-05 11:11:46 +00:00
|
|
|
setAxes(axisX, axisY);
|
2021-01-01 21:43:21 +00:00
|
|
|
ScatterSeries *series = createSeries<ScatterSeries>(*categoryVariable, *valueVariable);
|
|
|
|
|
|
|
|
for (auto [x, y, dive]: points)
|
|
|
|
series->append(dive, x, y);
|
|
|
|
|
|
|
|
// y = ax + b
|
2021-01-09 23:12:41 +00:00
|
|
|
struct regression_data reg = linear_regression(points);
|
2021-01-12 18:39:25 +00:00
|
|
|
if (!std::isnan(reg.a))
|
2021-01-15 17:39:14 +00:00
|
|
|
regressionItem = createChartItem<RegressionItem>(reg, xAxis, yAxis);
|
2021-01-01 21:43:21 +00:00
|
|
|
}
|