mirror of
https://github.com/subsurface/subsurface.git
synced 2024-12-03 15:43:09 +00:00
statistics: replace QtCharts' axes
Replace by custom implementation, with the ultimate goal to remove the QtCharts module. This doesn't yet display axis titles or a grid. Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at> Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
This commit is contained in:
parent
598058e21e
commit
4ab9f1c6b0
15 changed files with 377 additions and 225 deletions
|
@ -114,8 +114,7 @@ void BarSeries::BarLabel::highlight(bool highlight, int bin_nr, int binCount)
|
||||||
item->setBrush(brush);
|
item->setBrush(brush);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BarSeries::BarLabel::updatePosition(QtCharts::QChart *chart, QtCharts::QAbstractSeries *series,
|
void BarSeries::BarLabel::updatePosition(bool horizontal, bool center, const QRectF &rect,
|
||||||
bool horizontal, bool center, const QRectF &rect,
|
|
||||||
int bin_nr, int binCount)
|
int bin_nr, int binCount)
|
||||||
{
|
{
|
||||||
if (!horizontal) {
|
if (!horizontal) {
|
||||||
|
@ -191,7 +190,7 @@ BarSeries::Item::Item(QtCharts::QChart *chart, BarSeries *series, double lowerBo
|
||||||
item.item->setZValue(ZValues::series);
|
item.item->setZValue(ZValues::series);
|
||||||
item.highlight(false, binCount);
|
item.highlight(false, binCount);
|
||||||
}
|
}
|
||||||
updatePosition(chart, series, horizontal, stacked, binCount);
|
updatePosition(series, horizontal, stacked, binCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BarSeries::Item::highlight(int subitem, bool highlight, int binCount)
|
void BarSeries::Item::highlight(int subitem, bool highlight, int binCount)
|
||||||
|
@ -214,7 +213,7 @@ void BarSeries::SubItem::highlight(bool highlight, int binCount)
|
||||||
label->highlight(highlight, bin_nr, binCount);
|
label->highlight(highlight, bin_nr, binCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BarSeries::Item::updatePosition(QtCharts::QChart *chart, BarSeries *series, bool horizontal, bool stacked, int binCount)
|
void BarSeries::Item::updatePosition(BarSeries *series, bool horizontal, bool stacked, int binCount)
|
||||||
{
|
{
|
||||||
if (subitems.empty())
|
if (subitems.empty())
|
||||||
return;
|
return;
|
||||||
|
@ -233,28 +232,28 @@ void BarSeries::Item::updatePosition(QtCharts::QChart *chart, BarSeries *series,
|
||||||
for (SubItem &item: subitems) {
|
for (SubItem &item: subitems) {
|
||||||
int idx = stacked ? 0 : item.bin_nr;
|
int idx = stacked ? 0 : item.bin_nr;
|
||||||
double center = (idx + 0.5) * fullSubWidth + from;
|
double center = (idx + 0.5) * fullSubWidth + from;
|
||||||
item.updatePosition(chart, series, horizontal, stacked, center - subWidth / 2.0, center + subWidth / 2.0, binCount);
|
item.updatePosition(series, horizontal, stacked, center - subWidth / 2.0, center + subWidth / 2.0, binCount);
|
||||||
}
|
}
|
||||||
rect = subitems[0].item->rect();
|
rect = subitems[0].item->rect();
|
||||||
for (auto it = std::next(subitems.begin()); it != subitems.end(); ++it)
|
for (auto it = std::next(subitems.begin()); it != subitems.end(); ++it)
|
||||||
rect = rect.united(it->item->rect());
|
rect = rect.united(it->item->rect());
|
||||||
}
|
}
|
||||||
|
|
||||||
void BarSeries::SubItem::updatePosition(QtCharts::QChart *chart, BarSeries *series, bool horizontal, bool stacked,
|
void BarSeries::SubItem::updatePosition(BarSeries *series, bool horizontal, bool stacked,
|
||||||
double from, double to, int binCount)
|
double from, double to, int binCount)
|
||||||
{
|
{
|
||||||
QPointF topLeft, bottomRight;
|
QPointF topLeft, bottomRight;
|
||||||
if (horizontal) {
|
if (horizontal) {
|
||||||
topLeft = chart->mapToPosition(QPointF(value_from, to), series);
|
topLeft = series->toScreen(QPointF(value_from, to));
|
||||||
bottomRight = chart->mapToPosition(QPointF(value_to, from), series);
|
bottomRight = series->toScreen(QPointF(value_to, from));
|
||||||
} else {
|
} else {
|
||||||
topLeft = chart->mapToPosition(QPointF(from, value_to), series);
|
topLeft = series->toScreen(QPointF(from, value_to));
|
||||||
bottomRight = chart->mapToPosition(QPointF(to, value_from), series);
|
bottomRight = series->toScreen(QPointF(to, value_from));
|
||||||
}
|
}
|
||||||
QRectF rect(topLeft, bottomRight);
|
QRectF rect(topLeft, bottomRight);
|
||||||
item->setRect(rect);
|
item->setRect(rect);
|
||||||
if (label)
|
if (label)
|
||||||
label->updatePosition(chart, series, horizontal, stacked, rect, bin_nr, binCount);
|
label->updatePosition(horizontal, stacked, rect, bin_nr, binCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<BarSeries::SubItem> BarSeries::makeSubItems(const std::vector<std::pair<double, std::vector<QString>>> &values) const
|
std::vector<BarSeries::SubItem> BarSeries::makeSubItems(const std::vector<std::pair<double, std::vector<QString>>> &values) const
|
||||||
|
@ -265,9 +264,9 @@ std::vector<BarSeries::SubItem> BarSeries::makeSubItems(const std::vector<std::p
|
||||||
int bin_nr = 0;
|
int bin_nr = 0;
|
||||||
for (auto &[v, label]: values) {
|
for (auto &[v, label]: values) {
|
||||||
if (v > 0.0) {
|
if (v > 0.0) {
|
||||||
res.push_back({ std::make_unique<QGraphicsRectItem>(chart()), {}, from, from + v, bin_nr });
|
res.push_back({ std::make_unique<QGraphicsRectItem>(chart), {}, from, from + v, bin_nr });
|
||||||
if (!label.empty())
|
if (!label.empty())
|
||||||
res.back().label = std::make_unique<BarLabel>(chart(), label, bin_nr, binCount());
|
res.back().label = std::make_unique<BarLabel>(chart, label, bin_nr, binCount());
|
||||||
}
|
}
|
||||||
if (stacked)
|
if (stacked)
|
||||||
from += v;
|
from += v;
|
||||||
|
@ -293,15 +292,14 @@ void BarSeries::add_item(double lowerBound, double upperBound, std::vector<SubIt
|
||||||
// Don't add empty items, as that messes with the "find item under mouse" routine.
|
// Don't add empty items, as that messes with the "find item under mouse" routine.
|
||||||
if (subitems.empty())
|
if (subitems.empty())
|
||||||
return;
|
return;
|
||||||
items.emplace_back(chart(), this, lowerBound, upperBound, std::move(subitems), binName, res,
|
items.emplace_back(chart, this, lowerBound, upperBound, std::move(subitems), binName, res,
|
||||||
total, horizontal, stacked, binCount());
|
total, horizontal, stacked, binCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
void BarSeries::updatePositions()
|
void BarSeries::updatePositions()
|
||||||
{
|
{
|
||||||
QtCharts::QChart *c = chart();
|
|
||||||
for (Item &item: items)
|
for (Item &item: items)
|
||||||
item.updatePosition(c, this, horizontal, stacked, binCount());
|
item.updatePosition(this, horizontal, stacked, binCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attention: this supposes that items are sorted by position and no bar is inside another bar!
|
// Attention: this supposes that items are sorted by position and no bar is inside another bar!
|
||||||
|
@ -405,7 +403,7 @@ bool BarSeries::hover(QPointF pos)
|
||||||
Item &item = items[highlighted.bar];
|
Item &item = items[highlighted.bar];
|
||||||
item.highlight(index.subitem, true, binCount());
|
item.highlight(index.subitem, true, binCount());
|
||||||
if (!information)
|
if (!information)
|
||||||
information.reset(new InformationBox(chart()));
|
information.reset(new InformationBox(chart));
|
||||||
information->setText(makeInfo(item, highlighted.subitem), pos);
|
information->setText(makeInfo(item, highlighted.subitem), pos);
|
||||||
} else {
|
} else {
|
||||||
information.reset();
|
information.reset();
|
||||||
|
|
|
@ -87,8 +87,7 @@ private:
|
||||||
bool isOutside; // Is shown outside of bar
|
bool isOutside; // Is shown outside of bar
|
||||||
BarLabel(QtCharts::QChart *chart, const std::vector<QString> &labels, int bin_nr, int binCount);
|
BarLabel(QtCharts::QChart *chart, const std::vector<QString> &labels, int bin_nr, int binCount);
|
||||||
void setVisible(bool visible);
|
void setVisible(bool visible);
|
||||||
void updatePosition(QtCharts::QChart *chart, QtCharts::QAbstractSeries *series,
|
void updatePosition(bool horizontal, bool center, const QRectF &rect, int bin_nr, int binCount);
|
||||||
bool horizontal, bool center, const QRectF &rect, int bin_nr, int binCount);
|
|
||||||
void highlight(bool highlight, int bin_nr, int binCount);
|
void highlight(bool highlight, int bin_nr, int binCount);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -98,7 +97,7 @@ private:
|
||||||
double value_from;
|
double value_from;
|
||||||
double value_to;
|
double value_to;
|
||||||
int bin_nr;
|
int bin_nr;
|
||||||
void updatePosition(QtCharts::QChart *chart, BarSeries *series, bool horizontal, bool stacked,
|
void updatePosition(BarSeries *series, bool horizontal, bool stacked,
|
||||||
double from, double to, int binCount);
|
double from, double to, int binCount);
|
||||||
void highlight(bool highlight, int binCount);
|
void highlight(bool highlight, int binCount);
|
||||||
};
|
};
|
||||||
|
@ -114,7 +113,7 @@ private:
|
||||||
std::vector<SubItem> subitems,
|
std::vector<SubItem> subitems,
|
||||||
const QString &binName, const StatsOperationResults &res, int total, bool horizontal,
|
const QString &binName, const StatsOperationResults &res, int total, bool horizontal,
|
||||||
bool stacked, int binCount);
|
bool stacked, int binCount);
|
||||||
void updatePosition(QtCharts::QChart *chart, BarSeries *series, bool horizontal, bool stacked, int binCount);
|
void updatePosition(BarSeries *series, bool horizontal, bool stacked, int binCount);
|
||||||
void highlight(int subitem, bool highlight, int binCount);
|
void highlight(int subitem, bool highlight, int binCount);
|
||||||
int getSubItemUnderMouse(const QPointF &f, bool horizontal, bool stacked) const;
|
int getSubItemUnderMouse(const QPointF &f, bool horizontal, bool stacked) const;
|
||||||
};
|
};
|
||||||
|
|
|
@ -39,7 +39,7 @@ BoxSeries::Item::Item(QtCharts::QChart *chart, BoxSeries *series, double lowerBo
|
||||||
bottomBar.setZValue(ZValues::series);
|
bottomBar.setZValue(ZValues::series);
|
||||||
center.setZValue(ZValues::series);
|
center.setZValue(ZValues::series);
|
||||||
highlight(false);
|
highlight(false);
|
||||||
updatePosition(chart, series);
|
updatePosition(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
BoxSeries::Item::~Item()
|
BoxSeries::Item::~Item()
|
||||||
|
@ -59,7 +59,7 @@ void BoxSeries::Item::highlight(bool highlight)
|
||||||
center.setPen(pen);
|
center.setPen(pen);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BoxSeries::Item::updatePosition(QtCharts::QChart *chart, BoxSeries *series)
|
void BoxSeries::Item::updatePosition(BoxSeries *series)
|
||||||
{
|
{
|
||||||
double delta = (upperBound - lowerBound) * boxWidth;
|
double delta = (upperBound - lowerBound) * boxWidth;
|
||||||
double from = (lowerBound + upperBound - delta) / 2.0;
|
double from = (lowerBound + upperBound - delta) / 2.0;
|
||||||
|
@ -68,17 +68,17 @@ void BoxSeries::Item::updatePosition(QtCharts::QChart *chart, BoxSeries *series)
|
||||||
|
|
||||||
QPointF topLeft, bottomRight;
|
QPointF topLeft, bottomRight;
|
||||||
QMarginsF margins(boxBorderWidth / 2.0, boxBorderWidth / 2.0, boxBorderWidth / 2.0, boxBorderWidth / 2.0);
|
QMarginsF margins(boxBorderWidth / 2.0, boxBorderWidth / 2.0, boxBorderWidth / 2.0, boxBorderWidth / 2.0);
|
||||||
topLeft = chart->mapToPosition(QPointF(from, q.max), series);
|
topLeft = series->toScreen(QPointF(from, q.max));
|
||||||
bottomRight = chart->mapToPosition(QPointF(to, q.min), series);
|
bottomRight = series->toScreen(QPointF(to, q.min));
|
||||||
bounding = QRectF(topLeft, bottomRight).marginsAdded(margins);
|
bounding = QRectF(topLeft, bottomRight).marginsAdded(margins);
|
||||||
double left = topLeft.x();
|
double left = topLeft.x();
|
||||||
double right = bottomRight.x();
|
double right = bottomRight.x();
|
||||||
double width = right - left;
|
double width = right - left;
|
||||||
double top = topLeft.y();
|
double top = topLeft.y();
|
||||||
double bottom = bottomRight.y();
|
double bottom = bottomRight.y();
|
||||||
QPointF q1 = chart->mapToPosition(QPointF(mid, q.q1), series);
|
QPointF q1 = series->toScreen(QPointF(mid, q.q1));
|
||||||
QPointF q2 = chart->mapToPosition(QPointF(mid, q.q2), series);
|
QPointF q2 = series->toScreen(QPointF(mid, q.q2));
|
||||||
QPointF q3 = chart->mapToPosition(QPointF(mid, q.q3), series);
|
QPointF q3 = series->toScreen(QPointF(mid, q.q3));
|
||||||
box.setRect(left, q3.y(), width, q1.y() - q3.y());
|
box.setRect(left, q3.y(), width, q1.y() - q3.y());
|
||||||
topWhisker.setLine(q3.x(), top, q3.x(), q3.y());
|
topWhisker.setLine(q3.x(), top, q3.x(), q3.y());
|
||||||
bottomWhisker.setLine(q1.x(), q1.y(), q1.x(), bottom);
|
bottomWhisker.setLine(q1.x(), q1.y(), q1.x(), bottom);
|
||||||
|
@ -89,15 +89,13 @@ void BoxSeries::Item::updatePosition(QtCharts::QChart *chart, BoxSeries *series)
|
||||||
|
|
||||||
void BoxSeries::append(double lowerBound, double upperBound, const StatsQuartiles &q, const QString &binName)
|
void BoxSeries::append(double lowerBound, double upperBound, const StatsQuartiles &q, const QString &binName)
|
||||||
{
|
{
|
||||||
QtCharts::QChart *c = chart();
|
items.emplace_back(new Item(chart, this, lowerBound, upperBound, q, binName));
|
||||||
items.emplace_back(new Item(c, this, lowerBound, upperBound, q, binName));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BoxSeries::updatePositions()
|
void BoxSeries::updatePositions()
|
||||||
{
|
{
|
||||||
QtCharts::QChart *c = chart();
|
|
||||||
for (auto &item: items)
|
for (auto &item: items)
|
||||||
item->updatePosition(c, this);
|
item->updatePosition(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attention: this supposes that items are sorted by position and no box is inside another box!
|
// Attention: this supposes that items are sorted by position and no box is inside another box!
|
||||||
|
@ -149,7 +147,7 @@ bool BoxSeries::hover(QPointF pos)
|
||||||
Item &item = *items[highlighted];
|
Item &item = *items[highlighted];
|
||||||
item.highlight(true);
|
item.highlight(true);
|
||||||
if (!information)
|
if (!information)
|
||||||
information.reset(new InformationBox(chart()));
|
information.reset(new InformationBox(chart));
|
||||||
information->setText(formatInformation(item), pos);
|
information->setText(formatInformation(item), pos);
|
||||||
} else {
|
} else {
|
||||||
information.reset();
|
information.reset();
|
||||||
|
|
|
@ -45,7 +45,7 @@ private:
|
||||||
StatsQuartiles q;
|
StatsQuartiles q;
|
||||||
QString binName;
|
QString binName;
|
||||||
Item(QtCharts::QChart *chart, BoxSeries *series, double lowerBound, double upperBound, const StatsQuartiles &q, const QString &binName);
|
Item(QtCharts::QChart *chart, BoxSeries *series, double lowerBound, double upperBound, const StatsQuartiles &q, const QString &binName);
|
||||||
void updatePosition(QtCharts::QChart *chart, BoxSeries *series);
|
void updatePosition(BoxSeries *series);
|
||||||
void highlight(bool highlight);
|
void highlight(bool highlight);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -167,8 +167,7 @@ PieSeries::~PieSeries()
|
||||||
|
|
||||||
void PieSeries::updatePositions()
|
void PieSeries::updatePositions()
|
||||||
{
|
{
|
||||||
QtCharts::QChart *c = chart();
|
QRectF plotRect = chart->plotArea();
|
||||||
QRectF plotRect = c->plotArea();
|
|
||||||
center = plotRect.center();
|
center = plotRect.center();
|
||||||
radius = std::min(plotRect.width(), plotRect.height()) * pieSize / 2.0;
|
radius = std::min(plotRect.width(), plotRect.height()) * pieSize / 2.0;
|
||||||
QRectF rect(center.x() - radius, center.y() - radius, 2.0 * radius, 2.0 * radius);
|
QRectF rect(center.x() - radius, center.y() - radius, 2.0 * radius, 2.0 * radius);
|
||||||
|
@ -247,7 +246,7 @@ bool PieSeries::hover(QPointF pos)
|
||||||
if (highlighted >= 0 && highlighted < (int)items.size()) {
|
if (highlighted >= 0 && highlighted < (int)items.size()) {
|
||||||
items[highlighted].highlight(highlighted, true, (int)items.size());
|
items[highlighted].highlight(highlighted, true, (int)items.size());
|
||||||
if (!information)
|
if (!information)
|
||||||
information.reset(new InformationBox(chart()));
|
information.reset(new InformationBox(chart));
|
||||||
information->setText(makeInfo(highlighted), pos);
|
information->setText(makeInfo(highlighted), pos);
|
||||||
} else {
|
} else {
|
||||||
information.reset();
|
information.reset();
|
||||||
|
|
|
@ -65,12 +65,12 @@ ScatterSeries::Item::Item(QtCharts::QChart *chart, ScatterSeries *series, dive *
|
||||||
value(value)
|
value(value)
|
||||||
{
|
{
|
||||||
item->setZValue(ZValues::series);
|
item->setZValue(ZValues::series);
|
||||||
updatePosition(chart, series);
|
updatePosition(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScatterSeries::Item::updatePosition(QtCharts::QChart *chart, ScatterSeries *series)
|
void ScatterSeries::Item::updatePosition(ScatterSeries *series)
|
||||||
{
|
{
|
||||||
QPointF center = chart->mapToPosition(QPointF(pos, value), series);
|
QPointF center = series->toScreen(QPointF(pos, value));
|
||||||
item->setPos(center.x() - scatterItemDiameter / 2.0,
|
item->setPos(center.x() - scatterItemDiameter / 2.0,
|
||||||
center.y() - scatterItemDiameter / 2.0);
|
center.y() - scatterItemDiameter / 2.0);
|
||||||
}
|
}
|
||||||
|
@ -82,14 +82,13 @@ void ScatterSeries::Item::highlight(bool highlight)
|
||||||
|
|
||||||
void ScatterSeries::append(dive *d, double pos, double value)
|
void ScatterSeries::append(dive *d, double pos, double value)
|
||||||
{
|
{
|
||||||
items.emplace_back(chart(), this, d, pos, value);
|
items.emplace_back(chart, this, d, pos, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScatterSeries::updatePositions()
|
void ScatterSeries::updatePositions()
|
||||||
{
|
{
|
||||||
QtCharts::QChart *c = chart();
|
|
||||||
for (Item &item: items)
|
for (Item &item: items)
|
||||||
item.updatePosition(c, this);
|
item.updatePosition(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static double sq(double f)
|
static double sq(double f)
|
||||||
|
@ -103,7 +102,7 @@ static double squareDist(const QPointF &p1, const QPointF &p2)
|
||||||
return QPointF::dotProduct(diff, diff);
|
return QPointF::dotProduct(diff, diff);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<int> ScatterSeries::getItemsUnderMouse(const QPointF &point)
|
std::vector<int> ScatterSeries::getItemsUnderMouse(const QPointF &point) const
|
||||||
{
|
{
|
||||||
std::vector<int> res;
|
std::vector<int> res;
|
||||||
double x = point.x();
|
double x = point.x();
|
||||||
|
@ -174,7 +173,7 @@ bool ScatterSeries::hover(QPointF pos)
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
if (!information)
|
if (!information)
|
||||||
information.reset(new InformationBox(chart()));
|
information.reset(new InformationBox(chart));
|
||||||
|
|
||||||
std::vector<QString> text;
|
std::vector<QString> text;
|
||||||
text.reserve(highlighted.size() * 5);
|
text.reserve(highlighted.size() * 5);
|
||||||
|
|
|
@ -30,16 +30,14 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Get items under mouse.
|
// Get items under mouse.
|
||||||
// Super weird: this function can't be const, because QChart::mapToValue takes
|
std::vector<int> getItemsUnderMouse(const QPointF &f) const;
|
||||||
// a non-const reference!?
|
|
||||||
std::vector<int> getItemsUnderMouse(const QPointF &f);
|
|
||||||
|
|
||||||
struct Item {
|
struct Item {
|
||||||
std::unique_ptr<QGraphicsPixmapItem> item;
|
std::unique_ptr<QGraphicsPixmapItem> item;
|
||||||
dive *d;
|
dive *d;
|
||||||
double pos, value;
|
double pos, value;
|
||||||
Item(QtCharts::QChart *chart, ScatterSeries *series, dive *d, double pos, double value);
|
Item(QtCharts::QChart *chart, ScatterSeries *series, dive *d, double pos, double value);
|
||||||
void updatePosition(QtCharts::QChart *chart, ScatterSeries *series);
|
void updatePosition(ScatterSeries *series);
|
||||||
void highlight(bool highlight);
|
void highlight(bool highlight);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
// SPDX-License-Identifier: GPL-2.0
|
// SPDX-License-Identifier: GPL-2.0
|
||||||
#include "statsaxis.h"
|
#include "statsaxis.h"
|
||||||
|
#include "statscolors.h"
|
||||||
#include "statstranslations.h"
|
#include "statstranslations.h"
|
||||||
#include "statsvariables.h"
|
#include "statsvariables.h"
|
||||||
|
#include "zvalues.h"
|
||||||
#include "core/pref.h"
|
#include "core/pref.h"
|
||||||
#include "core/subsurface-time.h"
|
#include "core/subsurface-time.h"
|
||||||
#include <math.h> // for lrint
|
#include <math.h> // for lrint
|
||||||
|
@ -10,8 +12,28 @@
|
||||||
#include <QFontMetrics>
|
#include <QFontMetrics>
|
||||||
#include <QLocale>
|
#include <QLocale>
|
||||||
|
|
||||||
StatsAxis::StatsAxis(QtCharts::QChart *chart, bool horizontal) : chart(chart), horizontal(horizontal)
|
// Define most constants for horizontal and vertical axes for more flexibility.
|
||||||
|
// Note: *Horizontal means that this is for the horizontal axis, so a vertical space.
|
||||||
|
static const double axisWidth = 0.5;
|
||||||
|
static const double axisTickWidth = 0.3;
|
||||||
|
static const double axisTickSizeHorizontal = 6.0;
|
||||||
|
static const double axisTickSizeVertical = 6.0;
|
||||||
|
static const double axisLabelSpaceHorizontal = 2.0; // Space between axis or ticks and labels
|
||||||
|
static const double axisLabelSpaceVertical = 2.0; // Space between axis or ticks and labels
|
||||||
|
static const double axisTitleSpaceHorizontal = 2.0; // Space between labels and title
|
||||||
|
static const double axisTitleSpaceVertical = 2.0; // Space between labels and title
|
||||||
|
|
||||||
|
StatsAxis::StatsAxis(QtCharts::QChart *chart, bool horizontal, bool labelsBetweenTicks) :
|
||||||
|
QGraphicsLineItem(chart),
|
||||||
|
chart(chart), horizontal(horizontal), labelsBetweenTicks(labelsBetweenTicks),
|
||||||
|
size(1.0), zeroOnScreen(0.0), min(0.0), max(1.0)
|
||||||
{
|
{
|
||||||
|
// use a Light version of the application fond for both labels and title
|
||||||
|
labelFont = QFont();
|
||||||
|
labelFont.setWeight(QFont::Light);
|
||||||
|
titleFont = labelFont;
|
||||||
|
setPen(QPen(axisColor, axisWidth));
|
||||||
|
setZValue(ZValues::axes);
|
||||||
}
|
}
|
||||||
|
|
||||||
StatsAxis::~StatsAxis()
|
StatsAxis::~StatsAxis()
|
||||||
|
@ -20,7 +42,13 @@ StatsAxis::~StatsAxis()
|
||||||
|
|
||||||
std::pair<double, double> StatsAxis::minMax() const
|
std::pair<double, double> StatsAxis::minMax() const
|
||||||
{
|
{
|
||||||
return { 0.0, 1.0 };
|
return { min, max };
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsAxis::setRange(double minIn, double maxIn)
|
||||||
|
{
|
||||||
|
min = minIn;
|
||||||
|
max = maxIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guess the number of tick marks based on example strings.
|
// Guess the number of tick marks based on example strings.
|
||||||
|
@ -28,14 +56,13 @@ std::pair<double, double> StatsAxis::minMax() const
|
||||||
// maximum-size strings especially, when using proportional fonts or for
|
// maximum-size strings especially, when using proportional fonts or for
|
||||||
// categorical data. Therefore, try to err on the safe side by adding enough
|
// categorical data. Therefore, try to err on the safe side by adding enough
|
||||||
// margins.
|
// margins.
|
||||||
int StatsAxis::guessNumTicks(const QtCharts::QAbstractAxis *axis, const std::vector<QString> &strings) const
|
int StatsAxis::guessNumTicks(const std::vector<QString> &strings) const
|
||||||
{
|
{
|
||||||
QFont font = axis->labelsFont();
|
QFontMetrics fm(labelFont);
|
||||||
QFontMetrics fm(font);
|
|
||||||
int minSize = fm.height();
|
int minSize = fm.height();
|
||||||
for (const QString &s: strings) {
|
for (const QString &s: strings) {
|
||||||
QSize size = fm.size(Qt::TextSingleLine, s);
|
QSize labelSize = fm.size(Qt::TextSingleLine, s);
|
||||||
int needed = horizontal ? size.width() : size.height();
|
int needed = horizontal ? labelSize.width() : labelSize.height();
|
||||||
if (needed > minSize)
|
if (needed > minSize)
|
||||||
minSize = needed;
|
minSize = needed;
|
||||||
}
|
}
|
||||||
|
@ -45,31 +72,127 @@ int StatsAxis::guessNumTicks(const QtCharts::QAbstractAxis *axis, const std::vec
|
||||||
minSize = minSize * 3 / 2;
|
minSize = minSize * 3 / 2;
|
||||||
else
|
else
|
||||||
minSize *= 2;
|
minSize *= 2;
|
||||||
QRectF chartSize = chart->plotArea();
|
int numTicks = lrint(size / minSize);
|
||||||
double availableSpace = horizontal ? chartSize.width() : chartSize.height();
|
|
||||||
int numTicks = lrint(availableSpace / minSize);
|
|
||||||
return std::max(numTicks, 2);
|
return std::max(numTicks, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double StatsAxis::width() const
|
||||||
|
{
|
||||||
|
if (horizontal)
|
||||||
|
return 0.0; // Only supported for vertical axes
|
||||||
|
double labelWidth = 0.0;
|
||||||
|
for (const Label &label: labels) {
|
||||||
|
double w = label.label->boundingRect().width();
|
||||||
|
if (w > labelWidth)
|
||||||
|
labelWidth = w;
|
||||||
|
}
|
||||||
|
return labelWidth + axisLabelSpaceVertical +
|
||||||
|
QFontMetrics(titleFont).height() + axisTitleSpaceVertical +
|
||||||
|
(labelsBetweenTicks ? 0.0 : axisTickSizeVertical);
|
||||||
|
}
|
||||||
|
|
||||||
|
double StatsAxis::height() const
|
||||||
|
{
|
||||||
|
if (!horizontal)
|
||||||
|
return 0.0; // Only supported for horizontal axes
|
||||||
|
return QFontMetrics(labelFont).height() + axisLabelSpaceHorizontal +
|
||||||
|
QFontMetrics(titleFont).height() + axisTitleSpaceHorizontal +
|
||||||
|
(labelsBetweenTicks ? 0.0 : axisTickSizeHorizontal);
|
||||||
|
}
|
||||||
|
|
||||||
|
StatsAxis::Label::Label(const QString &name, double pos, QtCharts::QChart *chart, const QFont &font) :
|
||||||
|
label(new QGraphicsSimpleTextItem(name, chart)),
|
||||||
|
pos(pos)
|
||||||
|
{
|
||||||
|
label->setBrush(QBrush(darkLabelColor));
|
||||||
|
label->setFont(font);
|
||||||
|
label->setZValue(ZValues::axes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsAxis::addLabel(const QString &label, double pos)
|
||||||
|
{
|
||||||
|
labels.emplace_back(label, pos, chart, labelFont);
|
||||||
|
}
|
||||||
|
|
||||||
|
StatsAxis::Tick::Tick(double pos, QtCharts::QChart *chart) :
|
||||||
|
item(new QGraphicsLineItem(chart)),
|
||||||
|
pos(pos)
|
||||||
|
{
|
||||||
|
item->setPen(QPen(axisColor, axisTickWidth));
|
||||||
|
item->setZValue(ZValues::axes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsAxis::addTick(double pos)
|
||||||
|
{
|
||||||
|
ticks.emplace_back(pos, chart);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map x (horizontal) or y (vertical) coordinate to or from screen coordinate
|
||||||
|
double StatsAxis::toScreen(double pos) const
|
||||||
|
{
|
||||||
|
// Vertical is bottom-up
|
||||||
|
return horizontal ? (pos - min) / (max - min) * size + zeroOnScreen
|
||||||
|
: (min - pos) / (max - min) * size + zeroOnScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
double StatsAxis::toValue(double pos) const
|
||||||
|
{
|
||||||
|
// Vertical is bottom-up
|
||||||
|
return horizontal ? (pos - zeroOnScreen) / size * (max - min) + min
|
||||||
|
: (zeroOnScreen - pos) / size * (max - min) + zeroOnScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsAxis::setSize(double sizeIn)
|
||||||
|
{
|
||||||
|
size = sizeIn;
|
||||||
|
updateLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StatsAxis::setPos(QPointF pos)
|
||||||
|
{
|
||||||
|
if (horizontal) {
|
||||||
|
zeroOnScreen = pos.x();
|
||||||
|
double labelY = pos.y() + axisLabelSpaceHorizontal +
|
||||||
|
(labelsBetweenTicks ? 0.0 : axisTickSizeHorizontal);
|
||||||
|
double y = pos.y();
|
||||||
|
for (Label &label: labels) {
|
||||||
|
double x = toScreen(label.pos) - label.label->boundingRect().width() / 2.0;
|
||||||
|
label.label->setPos(QPointF(x, labelY));
|
||||||
|
}
|
||||||
|
for (Tick &tick: ticks) {
|
||||||
|
double x = toScreen(tick.pos);
|
||||||
|
tick.item->setLine(x, y, x, y + axisTickSizeHorizontal);
|
||||||
|
}
|
||||||
|
setLine(zeroOnScreen, y, zeroOnScreen + size, y);
|
||||||
|
} else {
|
||||||
|
double fontHeight = QFontMetrics(labelFont).height();
|
||||||
|
zeroOnScreen = pos.y();
|
||||||
|
double x = pos.x();
|
||||||
|
double labelX = x - axisLabelSpaceVertical -
|
||||||
|
(labelsBetweenTicks ? 0.0 : axisTickSizeVertical);
|
||||||
|
for (Label &label: labels) {
|
||||||
|
double y = toScreen(label.pos) - fontHeight / 2.0;
|
||||||
|
label.label->setPos(QPointF(labelX - label.label->boundingRect().width(), y));
|
||||||
|
}
|
||||||
|
for (Tick &tick: ticks) {
|
||||||
|
double y = toScreen(tick.pos);
|
||||||
|
tick.item->setLine(x, y, x - axisTickSizeVertical, y);
|
||||||
|
}
|
||||||
|
setLine(x, zeroOnScreen, x, zeroOnScreen - size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ValueAxis::ValueAxis(QtCharts::QChart *chart, double min, double max, int decimals, bool horizontal) :
|
ValueAxis::ValueAxis(QtCharts::QChart *chart, double min, double max, int decimals, bool horizontal) :
|
||||||
StatsAxisTemplate(chart, horizontal),
|
StatsAxis(chart, horizontal, false),
|
||||||
min(min), max(max), decimals(decimals)
|
min(min), max(max), decimals(decimals)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
std::pair<double, double> ValueAxis::minMax() const
|
|
||||||
{
|
|
||||||
return { QValueAxis::min(), QValueAxis::max() };
|
|
||||||
}
|
|
||||||
|
|
||||||
static QString makeFormatString(int decimals)
|
|
||||||
{
|
|
||||||
return QStringLiteral("%.%1f").arg(decimals < 0 ? 0 : decimals);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ValueAxis::updateLabels()
|
void ValueAxis::updateLabels()
|
||||||
{
|
{
|
||||||
using QtCharts::QValueAxis;
|
using QtCharts::QValueAxis;
|
||||||
|
labels.clear();
|
||||||
|
ticks.clear();
|
||||||
|
|
||||||
// Avoid degenerate cases
|
// Avoid degenerate cases
|
||||||
if (max - min < 0.0001) {
|
if (max - min < 0.0001) {
|
||||||
|
@ -80,7 +203,7 @@ void ValueAxis::updateLabels()
|
||||||
QLocale loc;
|
QLocale loc;
|
||||||
QString minString = loc.toString(min, 'f', decimals);
|
QString minString = loc.toString(min, 'f', decimals);
|
||||||
QString maxString = loc.toString(max, 'f', decimals);
|
QString maxString = loc.toString(max, 'f', decimals);
|
||||||
int numTicks = guessNumTicks(this, { minString, maxString});
|
int numTicks = guessNumTicks({ minString, maxString});
|
||||||
|
|
||||||
// Use full decimal increments
|
// Use full decimal increments
|
||||||
double height = max - min;
|
double height = max - min;
|
||||||
|
@ -98,12 +221,20 @@ void ValueAxis::updateLabels()
|
||||||
if (-digits_int > decimals)
|
if (-digits_int > decimals)
|
||||||
decimals = -digits_int;
|
decimals = -digits_int;
|
||||||
|
|
||||||
setLabelFormat(makeFormatString(decimals));
|
|
||||||
double actMin = floor(min / inc) * inc;
|
double actMin = floor(min / inc) * inc;
|
||||||
double actMax = ceil(max / inc) * inc;
|
double actMax = ceil(max / inc) * inc;
|
||||||
int num = lrint((actMax - actMin) / inc);
|
int num = lrint((actMax - actMin) / inc);
|
||||||
setRange(actMin, actMax);
|
setRange(actMin, actMax);
|
||||||
setTickCount(num + 1);
|
|
||||||
|
double actStep = (actMax - actMin) / static_cast<double>(num);
|
||||||
|
double act = actMin;
|
||||||
|
labels.reserve(num + 1);
|
||||||
|
ticks.reserve(num + 1);
|
||||||
|
for (int i = 0; i <= num; ++i) {
|
||||||
|
addLabel(loc.toString(act, 'f', decimals), act);
|
||||||
|
addTick(act);
|
||||||
|
act += actStep;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CountAxis::CountAxis(QtCharts::QChart *chart, int count, bool horizontal) :
|
CountAxis::CountAxis(QtCharts::QChart *chart, int count, bool horizontal) :
|
||||||
|
@ -114,9 +245,12 @@ CountAxis::CountAxis(QtCharts::QChart *chart, int count, bool horizontal) :
|
||||||
|
|
||||||
void CountAxis::updateLabels()
|
void CountAxis::updateLabels()
|
||||||
{
|
{
|
||||||
|
labels.clear();
|
||||||
|
ticks.clear();
|
||||||
|
|
||||||
QLocale loc;
|
QLocale loc;
|
||||||
QString countString = loc.toString(count);
|
QString countString = loc.toString(count);
|
||||||
int numTicks = guessNumTicks(this, { countString });
|
int numTicks = guessNumTicks({ countString });
|
||||||
|
|
||||||
// Get estimate of step size
|
// Get estimate of step size
|
||||||
if (count <= 0)
|
if (count <= 0)
|
||||||
|
@ -145,60 +279,43 @@ void CountAxis::updateLabels()
|
||||||
// Make maximum an integer number of steps, equal or greater than the needed counts
|
// Make maximum an integer number of steps, equal or greater than the needed counts
|
||||||
int num = (count - 1) / step + 1;
|
int num = (count - 1) / step + 1;
|
||||||
int max = num * step;
|
int max = num * step;
|
||||||
numTicks = num + 1; // There is one more tick than steps
|
|
||||||
|
|
||||||
setLabelFormat("%.0f");
|
|
||||||
setRange(0, max);
|
setRange(0, max);
|
||||||
setTickCount(numTicks);
|
|
||||||
|
labels.reserve(max + 1);
|
||||||
|
ticks.reserve(max + 1);
|
||||||
|
for (int i = 0; i <= max; i += step) {
|
||||||
|
addLabel(loc.toString(i), static_cast<double>(i));
|
||||||
|
addTick(static_cast<double>(i));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CategoryAxis::CategoryAxis(QtCharts::QChart *chart, const std::vector<QString> &labels, bool horizontal) :
|
CategoryAxis::CategoryAxis(QtCharts::QChart *chart, const std::vector<QString> &labelsIn, bool horizontal) :
|
||||||
StatsAxisTemplate(chart, horizontal)
|
StatsAxis(chart, horizontal, true)
|
||||||
{
|
{
|
||||||
for (const QString &s: labels)
|
labels.reserve(labelsIn.size());
|
||||||
append(s);
|
ticks.reserve(labelsIn.size() + 1);
|
||||||
|
double pos = 0.0;
|
||||||
|
addTick(-0.5);
|
||||||
|
for (const QString &s: labelsIn) {
|
||||||
|
addLabel(s, pos);
|
||||||
|
addTick(pos + 0.5);
|
||||||
|
pos += 1.0;
|
||||||
|
}
|
||||||
|
setRange(-0.5, static_cast<double>(labelsIn.size()) + 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CategoryAxis::updateLabels()
|
void CategoryAxis::updateLabels()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
// A small helper class that makes strings unique. We need this,
|
|
||||||
// because QCategoryAxis can only handle unique category names.
|
|
||||||
// Disambiguate strings by adding unicode zero-width spaces.
|
|
||||||
// Keep track of a list of strings and how many spaces have to
|
|
||||||
// be added.
|
|
||||||
class LabelDisambiguator {
|
|
||||||
using Pair = std::pair<QString, int>;
|
|
||||||
std::vector<Pair> entries;
|
|
||||||
public:
|
|
||||||
QString transmogrify(const QString &s);
|
|
||||||
};
|
|
||||||
|
|
||||||
QString LabelDisambiguator::transmogrify(const QString &s)
|
|
||||||
{
|
|
||||||
auto it = std::find_if(entries.begin(), entries.end(),
|
|
||||||
[&s](const Pair &p) { return p.first == s; });
|
|
||||||
if (it == entries.end()) {
|
|
||||||
entries.emplace_back(s, 0);
|
|
||||||
return s;
|
|
||||||
} else {
|
|
||||||
++(it->second);
|
|
||||||
return s + QString(it->second, QChar(0x200b));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HistogramAxis::HistogramAxis(QtCharts::QChart *chart, std::vector<HistogramAxisEntry> bins, bool horizontal) :
|
HistogramAxis::HistogramAxis(QtCharts::QChart *chart, std::vector<HistogramAxisEntry> bins, bool horizontal) :
|
||||||
StatsAxisTemplate(chart, horizontal),
|
StatsAxis(chart, horizontal, false),
|
||||||
bin_values(std::move(bins))
|
bin_values(std::move(bins))
|
||||||
{
|
{
|
||||||
if (bin_values.size() < 2) // Less than two makes no sense -> there must be at least one category
|
if (bin_values.size() < 2) // Less than two makes no sense -> there must be at least one category
|
||||||
return;
|
return;
|
||||||
|
|
||||||
LabelDisambiguator labeler;
|
|
||||||
for (HistogramAxisEntry &entry: bin_values)
|
|
||||||
entry.name = labeler.transmogrify(entry.name);
|
|
||||||
|
|
||||||
// The caller can declare some bin labels as preferred, when there are
|
// The caller can declare some bin labels as preferred, when there are
|
||||||
// too many labels to show all. Try to infer the preferred step size
|
// too many labels to show all. Try to infer the preferred step size
|
||||||
// by finding two consecutive preferred labels. This supposes that
|
// by finding two consecutive preferred labels. This supposes that
|
||||||
|
@ -210,17 +327,7 @@ HistogramAxis::HistogramAxis(QtCharts::QChart *chart, std::vector<HistogramAxisE
|
||||||
auto it2 = std::find_if(next_it, bin_values.end(),
|
auto it2 = std::find_if(next_it, bin_values.end(),
|
||||||
[](const HistogramAxisEntry &e) { return e.recommended; });
|
[](const HistogramAxisEntry &e) { return e.recommended; });
|
||||||
preferred_step = it2 == bin_values.end() ? 1 : it2 - it1;
|
preferred_step = it2 == bin_values.end() ? 1 : it2 - it1;
|
||||||
setMin(bin_values.front().value);
|
setRange(bin_values.front().value, bin_values.back().value);
|
||||||
setMax(bin_values.back().value);
|
|
||||||
setStartValue(bin_values.front().value);
|
|
||||||
setLabelsPosition(QCategoryAxis::AxisLabelsPositionOnValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::pair<double, double> HistogramAxis::minMax() const
|
|
||||||
{
|
|
||||||
if (bin_values.size() < 2) // Less than two makes no sense -> there must be at least one category
|
|
||||||
return { 0.0, 1.0 };
|
|
||||||
return { QValueAxis::min(), QValueAxis::max() };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a histogram axis with the given labels. Labels are specified as (name, value, recommended) triplets.
|
// Initialize a histogram axis with the given labels. Labels are specified as (name, value, recommended) triplets.
|
||||||
|
@ -229,20 +336,17 @@ std::pair<double, double> HistogramAxis::minMax() const
|
||||||
// There, we obviously want to show the years and not the quarters.
|
// There, we obviously want to show the years and not the quarters.
|
||||||
void HistogramAxis::updateLabels()
|
void HistogramAxis::updateLabels()
|
||||||
{
|
{
|
||||||
|
labels.clear();
|
||||||
|
ticks.clear();
|
||||||
|
|
||||||
if (bin_values.size() < 2) // Less than two makes no sense -> there must be at least one category
|
if (bin_values.size() < 2) // Less than two makes no sense -> there must be at least one category
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// There is no clear all labels function in QCategoryAxis!? You must be kidding.
|
|
||||||
for (const QString &label: categoriesLabels())
|
|
||||||
remove(label);
|
|
||||||
if (count() > 0)
|
|
||||||
qWarning("HistogramAxis::updateLabels(): labels left after clearing!?");
|
|
||||||
|
|
||||||
std::vector<QString> strings;
|
std::vector<QString> strings;
|
||||||
strings.reserve(bin_values.size());
|
strings.reserve(bin_values.size());
|
||||||
for (auto &[name, value, recommended]: bin_values)
|
for (auto &[name, value, recommended]: bin_values)
|
||||||
strings.push_back(name);
|
strings.push_back(name);
|
||||||
int maxLabels = guessNumTicks(this, strings);
|
int maxLabels = guessNumTicks(strings);
|
||||||
|
|
||||||
int step = ((int)bin_values.size() - 1) / maxLabels + 1;
|
int step = ((int)bin_values.size() - 1) / maxLabels + 1;
|
||||||
if (step < preferred_step) {
|
if (step < preferred_step) {
|
||||||
|
@ -268,9 +372,11 @@ void HistogramAxis::updateLabels()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
labels.reserve((bin_values.size() - first) / step + 1);
|
||||||
for (int i = first; i < (int)bin_values.size(); i += step) {
|
for (int i = first; i < (int)bin_values.size(); i += step) {
|
||||||
const auto &[name, value, recommended] = bin_values[i];
|
const auto &[name, value, recommended] = bin_values[i];
|
||||||
append(name, value);
|
addLabel(name, value);
|
||||||
|
addTick(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,50 +1,73 @@
|
||||||
// SPDX-License-Identifier: GPL-2.0
|
// SPDX-License-Identifier: GPL-2.0
|
||||||
// Supported chart axes
|
|
||||||
#ifndef STATS_AXIS_H
|
#ifndef STATS_AXIS_H
|
||||||
#define STATS_AXIS_H
|
#define STATS_AXIS_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <QBarCategoryAxis>
|
#include <QBarCategoryAxis>
|
||||||
#include <QCategoryAxis>
|
#include <QCategoryAxis>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QGraphicsSimpleTextItem>
|
||||||
|
#include <QGraphicsLineItem>
|
||||||
#include <QValueAxis>
|
#include <QValueAxis>
|
||||||
|
|
||||||
namespace QtCharts {
|
namespace QtCharts {
|
||||||
class QChart;
|
class QChart;
|
||||||
}
|
}
|
||||||
|
|
||||||
class StatsAxis {
|
class StatsAxis : QGraphicsLineItem {
|
||||||
public:
|
public:
|
||||||
virtual ~StatsAxis();
|
virtual ~StatsAxis();
|
||||||
virtual void updateLabels() = 0;
|
|
||||||
virtual QtCharts::QAbstractAxis *qaxis() = 0;
|
|
||||||
// Returns minimum and maximum of shown range, not of data points.
|
// Returns minimum and maximum of shown range, not of data points.
|
||||||
virtual std::pair<double, double> minMax() const;
|
std::pair<double, double> minMax() const;
|
||||||
|
|
||||||
|
double width() const; // Only supported by vertical axes. Only valid after setSize().
|
||||||
|
double height() const; // Only supported for horizontal axes. Always valid.
|
||||||
|
void setSize(double size); // Width for horizontal and height for vertical.
|
||||||
|
void setPos(QPointF pos); // Must be called after setSize().
|
||||||
|
void setRange(double, double);
|
||||||
|
|
||||||
|
// Map x (horizontal) or y (vertical) coordinate to or from screen coordinate
|
||||||
|
double toScreen(double) const;
|
||||||
|
double toValue(double) const;
|
||||||
protected:
|
protected:
|
||||||
|
StatsAxis(QtCharts::QChart *chart, bool horizontal, bool labelsBetweenTicks);
|
||||||
QtCharts::QChart *chart;
|
QtCharts::QChart *chart;
|
||||||
StatsAxis(QtCharts::QChart *chart, bool horizontal);
|
|
||||||
int guessNumTicks(const QtCharts::QAbstractAxis *axis, const std::vector<QString> &strings) const;
|
struct Label {
|
||||||
|
std::unique_ptr<QGraphicsSimpleTextItem> label;
|
||||||
|
double pos;
|
||||||
|
Label(const QString &name, double pos, QtCharts::QChart *chart, const QFont &font);
|
||||||
|
};
|
||||||
|
std::vector<Label> labels;
|
||||||
|
void addLabel(const QString &label, double pos);
|
||||||
|
virtual void updateLabels() = 0;
|
||||||
|
|
||||||
|
struct Tick {
|
||||||
|
std::unique_ptr<QGraphicsLineItem> item;
|
||||||
|
double pos;
|
||||||
|
Tick(double pos, QtCharts::QChart *chart);
|
||||||
|
};
|
||||||
|
std::vector<Tick> ticks;
|
||||||
|
void addTick(double pos);
|
||||||
|
|
||||||
|
int guessNumTicks(const std::vector<QString> &strings) const;
|
||||||
bool horizontal;
|
bool horizontal;
|
||||||
|
bool labelsBetweenTicks; // When labels are between ticks, they can be moved closer to the axis
|
||||||
|
|
||||||
|
QFont labelFont, titleFont;
|
||||||
|
double size; // width for horizontal, height for vertical
|
||||||
|
double zeroOnScreen;
|
||||||
|
double min, max;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Small template that derives from a QChart-axis and defines
|
class ValueAxis : public StatsAxis {
|
||||||
// the corresponding virtual axis() accessor.
|
|
||||||
template<typename QAxis>
|
|
||||||
class StatsAxisTemplate : public StatsAxis, public QAxis
|
|
||||||
{
|
|
||||||
using StatsAxis::StatsAxis;
|
|
||||||
QtCharts::QAbstractAxis *qaxis() override final {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class ValueAxis : public StatsAxisTemplate<QtCharts::QValueAxis> {
|
|
||||||
public:
|
public:
|
||||||
ValueAxis(QtCharts::QChart *chart, double min, double max, int decimals, bool horizontal);
|
ValueAxis(QtCharts::QChart *chart, double min, double max, int decimals, bool horizontal);
|
||||||
private:
|
private:
|
||||||
double min, max;
|
double min, max;
|
||||||
int decimals;
|
int decimals;
|
||||||
void updateLabels() override;
|
void updateLabels() override;
|
||||||
std::pair<double, double> minMax() const override;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class CountAxis : public ValueAxis {
|
class CountAxis : public ValueAxis {
|
||||||
|
@ -55,7 +78,7 @@ private:
|
||||||
void updateLabels() override;
|
void updateLabels() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CategoryAxis : public StatsAxisTemplate<QtCharts::QBarCategoryAxis> {
|
class CategoryAxis : public StatsAxis {
|
||||||
public:
|
public:
|
||||||
CategoryAxis(QtCharts::QChart *chart, const std::vector<QString> &labels, bool horizontal);
|
CategoryAxis(QtCharts::QChart *chart, const std::vector<QString> &labels, bool horizontal);
|
||||||
private:
|
private:
|
||||||
|
@ -68,12 +91,11 @@ struct HistogramAxisEntry {
|
||||||
bool recommended;
|
bool recommended;
|
||||||
};
|
};
|
||||||
|
|
||||||
class HistogramAxis : public StatsAxisTemplate<QtCharts::QCategoryAxis> {
|
class HistogramAxis : public StatsAxis {
|
||||||
public:
|
public:
|
||||||
HistogramAxis(QtCharts::QChart *chart, std::vector<HistogramAxisEntry> bin_values, bool horizontal);
|
HistogramAxis(QtCharts::QChart *chart, std::vector<HistogramAxisEntry> bin_values, bool horizontal);
|
||||||
private:
|
private:
|
||||||
void updateLabels() override;
|
void updateLabels() override;
|
||||||
std::pair<double, double> minMax() const override;
|
|
||||||
std::vector<HistogramAxisEntry> bin_values;
|
std::vector<HistogramAxisEntry> bin_values;
|
||||||
int preferred_step;
|
int preferred_step;
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,6 +11,7 @@ inline const QColor highlightedColor(Qt::yellow);
|
||||||
inline const QColor highlightedBorderColor(0xaa, 0xaa, 0x22);
|
inline const QColor highlightedBorderColor(0xaa, 0xaa, 0x22);
|
||||||
inline const QColor darkLabelColor(Qt::black);
|
inline const QColor darkLabelColor(Qt::black);
|
||||||
inline const QColor lightLabelColor(Qt::white);
|
inline const QColor lightLabelColor(Qt::white);
|
||||||
|
inline const QColor axisColor(Qt::black);
|
||||||
|
|
||||||
QColor binColor(int bin, int numBins);
|
QColor binColor(int bin, int numBins);
|
||||||
QColor labelColor(int bin, size_t numBins);
|
QColor labelColor(int bin, size_t numBins);
|
||||||
|
|
|
@ -2,18 +2,17 @@
|
||||||
#include "statsseries.h"
|
#include "statsseries.h"
|
||||||
#include "statsaxis.h"
|
#include "statsaxis.h"
|
||||||
|
|
||||||
#include <QChart>
|
|
||||||
|
|
||||||
StatsSeries::StatsSeries(QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis) :
|
StatsSeries::StatsSeries(QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis) :
|
||||||
xAxis(xAxis), yAxis(yAxis)
|
chart(chart), xAxis(xAxis), yAxis(yAxis)
|
||||||
{
|
{
|
||||||
chart->addSeries(this);
|
|
||||||
if (xAxis && yAxis) {
|
|
||||||
attachAxis(xAxis->qaxis());
|
|
||||||
attachAxis(yAxis->qaxis());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StatsSeries::~StatsSeries()
|
StatsSeries::~StatsSeries()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QPointF StatsSeries::toScreen(QPointF p)
|
||||||
|
{
|
||||||
|
return xAxis && yAxis ? QPointF(xAxis->toScreen(p.x()), yAxis->toScreen(p.y()))
|
||||||
|
: QPointF(0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
|
@ -23,7 +23,9 @@ public:
|
||||||
virtual bool hover(QPointF pos) = 0; // Called on mouse movement. Return true if an item of this series is highlighted.
|
virtual bool hover(QPointF pos) = 0; // Called on mouse movement. Return true if an item of this series is highlighted.
|
||||||
virtual void unhighlight() = 0; // Unhighlight any highlighted item.
|
virtual void unhighlight() = 0; // Unhighlight any highlighted item.
|
||||||
protected:
|
protected:
|
||||||
|
QtCharts::QChart *chart;
|
||||||
StatsAxis *xAxis, *yAxis; // May be zero for charts without axes (pie charts).
|
StatsAxis *xAxis, *yAxis; // May be zero for charts without axes (pie charts).
|
||||||
|
QPointF toScreen(QPointF p);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -65,6 +65,8 @@ bool StatsView::EventFilter::eventFilter(QObject *o, QEvent *event)
|
||||||
|
|
||||||
StatsView::StatsView(QWidget *parent) : QQuickWidget(parent),
|
StatsView::StatsView(QWidget *parent) : QQuickWidget(parent),
|
||||||
highlightedSeries(nullptr),
|
highlightedSeries(nullptr),
|
||||||
|
xAxis(nullptr),
|
||||||
|
yAxis(nullptr),
|
||||||
eventFilter(this)
|
eventFilter(this)
|
||||||
{
|
{
|
||||||
setResizeMode(QQuickWidget::SizeRootObjectToView);
|
setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||||
|
@ -89,10 +91,33 @@ StatsView::~StatsView()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void StatsView::plotAreaChanged(const QRectF &)
|
void StatsView::plotAreaChanged(const QRectF &r)
|
||||||
{
|
{
|
||||||
for (auto &axis: axes)
|
double left = r.x() + sceneBorder;
|
||||||
axis->updateLabels();
|
double top = r.y() + sceneBorder;
|
||||||
|
double right = r.right() - sceneBorder;
|
||||||
|
double bottom = r.bottom() - sceneBorder;
|
||||||
|
const double minSize = 30.0;
|
||||||
|
|
||||||
|
if (title)
|
||||||
|
top += title->boundingRect().height() + titleBorder;
|
||||||
|
// Currently, we only have either none, or an x- and a y-axis
|
||||||
|
if (xAxis)
|
||||||
|
bottom -= xAxis->height();
|
||||||
|
if (bottom - top < minSize)
|
||||||
|
return;
|
||||||
|
if (yAxis) {
|
||||||
|
yAxis->setSize(bottom - top);
|
||||||
|
left += yAxis->width();
|
||||||
|
yAxis->setPos(QPointF(left, bottom));
|
||||||
|
}
|
||||||
|
if (right - left < minSize)
|
||||||
|
return;
|
||||||
|
if (xAxis) {
|
||||||
|
xAxis->setSize(right - left);
|
||||||
|
xAxis->setPos(QPointF(left, bottom));
|
||||||
|
}
|
||||||
|
|
||||||
for (auto &series: series)
|
for (auto &series: series)
|
||||||
series->updatePositions();
|
series->updatePositions();
|
||||||
for (QuartileMarker &marker: quartileMarkers)
|
for (QuartileMarker &marker: quartileMarkers)
|
||||||
|
@ -133,8 +158,6 @@ void StatsView::hover(QPointF pos)
|
||||||
template <typename T, class... Args>
|
template <typename T, class... Args>
|
||||||
T *StatsView::createSeries(Args&&... args)
|
T *StatsView::createSeries(Args&&... args)
|
||||||
{
|
{
|
||||||
StatsAxis *xAxis = axes.size() >= 2 ? axes[0].get() : nullptr;
|
|
||||||
StatsAxis *yAxis = axes.size() >= 2 ? axes[1].get() : nullptr;
|
|
||||||
T *res = new T(chart, xAxis, yAxis, std::forward<Args>(args)...);
|
T *res = new T(chart, xAxis, yAxis, std::forward<Args>(args)...);
|
||||||
series.emplace_back(res);
|
series.emplace_back(res);
|
||||||
series.back()->updatePositions();
|
series.back()->updatePositions();
|
||||||
|
@ -156,24 +179,23 @@ void StatsView::updateTitlePos()
|
||||||
if (!title)
|
if (!title)
|
||||||
return;
|
return;
|
||||||
QRectF rect = chart->plotArea();
|
QRectF rect = chart->plotArea();
|
||||||
title->setPos((rect.width() - title->boundingRect().width()) / 2.0,
|
title->setPos(sceneBorder + (rect.width() - title->boundingRect().width()) / 2.0,
|
||||||
sceneBorder);
|
sceneBorder);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T, class... Args>
|
template <typename T, class... Args>
|
||||||
T *StatsView::createAxis(const QString &title, Args&&... args)
|
T *StatsView::createAxis(const QString &title, Args&&... args)
|
||||||
{
|
{
|
||||||
|
// TODO: set title
|
||||||
T *res = new T(chart, std::forward<Args>(args)...);
|
T *res = new T(chart, std::forward<Args>(args)...);
|
||||||
axes.emplace_back(res);
|
axes.emplace_back(res);
|
||||||
axes.back()->updateLabels();
|
|
||||||
axes.back()->qaxis()->setTitleText(title);
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
void StatsView::addAxes(StatsAxis *x, StatsAxis *y)
|
void StatsView::setAxes(StatsAxis *x, StatsAxis *y)
|
||||||
{
|
{
|
||||||
chart->addAxis(x->qaxis(), Qt::AlignBottom);
|
xAxis = x;
|
||||||
chart->addAxis(y->qaxis(), Qt::AlignLeft);
|
yAxis = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
void StatsView::reset()
|
void StatsView::reset()
|
||||||
|
@ -181,6 +203,7 @@ void StatsView::reset()
|
||||||
if (!chart)
|
if (!chart)
|
||||||
return;
|
return;
|
||||||
highlightedSeries = nullptr;
|
highlightedSeries = nullptr;
|
||||||
|
xAxis = yAxis = nullptr;
|
||||||
legend.reset();
|
legend.reset();
|
||||||
series.clear();
|
series.clear();
|
||||||
quartileMarkers.clear();
|
quartileMarkers.clear();
|
||||||
|
@ -365,9 +388,9 @@ void StatsView::plotBarChart(const std::vector<dive *> &dives,
|
||||||
CountAxis *valAxis = createCountAxis(maxVal, isHorizontal);
|
CountAxis *valAxis = createCountAxis(maxVal, isHorizontal);
|
||||||
|
|
||||||
if (isHorizontal)
|
if (isHorizontal)
|
||||||
addAxes(valAxis, catAxis);
|
setAxes(valAxis, catAxis);
|
||||||
else
|
else
|
||||||
addAxes(catAxis, valAxis);
|
setAxes(catAxis, valAxis);
|
||||||
|
|
||||||
// Paint legend first, because the bin-names will be moved away from.
|
// Paint legend first, because the bin-names will be moved away from.
|
||||||
if (showLegend)
|
if (showLegend)
|
||||||
|
@ -478,9 +501,9 @@ void StatsView::plotValueChart(const std::vector<dive *> &dives,
|
||||||
0.0, maxValue, valueVariable->decimals(), isHorizontal);
|
0.0, maxValue, valueVariable->decimals(), isHorizontal);
|
||||||
|
|
||||||
if (isHorizontal)
|
if (isHorizontal)
|
||||||
addAxes(valAxis, catAxis);
|
setAxes(valAxis, catAxis);
|
||||||
else
|
else
|
||||||
addAxes(catAxis, valAxis);
|
setAxes(catAxis, valAxis);
|
||||||
|
|
||||||
std::vector<BarSeries::ValueItem> items;
|
std::vector<BarSeries::ValueItem> items;
|
||||||
items.reserve(categoryBins.size());
|
items.reserve(categoryBins.size());
|
||||||
|
@ -546,9 +569,9 @@ void StatsView::plotDiscreteCountChart(const std::vector<dive *> &dives,
|
||||||
CountAxis *valAxis = createCountAxis(maxCount, isHorizontal);
|
CountAxis *valAxis = createCountAxis(maxCount, isHorizontal);
|
||||||
|
|
||||||
if (isHorizontal)
|
if (isHorizontal)
|
||||||
addAxes(valAxis, catAxis);
|
setAxes(valAxis, catAxis);
|
||||||
else
|
else
|
||||||
addAxes(catAxis, valAxis);
|
setAxes(catAxis, valAxis);
|
||||||
|
|
||||||
std::vector<BarSeries::CountItem> items;
|
std::vector<BarSeries::CountItem> items;
|
||||||
items.reserve(categoryBins.size());
|
items.reserve(categoryBins.size());
|
||||||
|
@ -613,7 +636,7 @@ void StatsView::plotDiscreteBoxChart(const std::vector<dive *> &dives,
|
||||||
ValueAxis *valueAxis = createAxis<ValueAxis>(valueVariable->nameWithUnit(),
|
ValueAxis *valueAxis = createAxis<ValueAxis>(valueVariable->nameWithUnit(),
|
||||||
minY, maxY, valueVariable->decimals(), false);
|
minY, maxY, valueVariable->decimals(), false);
|
||||||
|
|
||||||
addAxes(catAxis, valueAxis);
|
setAxes(catAxis, valueAxis);
|
||||||
|
|
||||||
BoxSeries *series = createSeries<BoxSeries>(valueVariable->name(), valueVariable->unitSymbol(), valueVariable->decimals());
|
BoxSeries *series = createSeries<BoxSeries>(valueVariable->name(), valueVariable->unitSymbol(), valueVariable->decimals());
|
||||||
|
|
||||||
|
@ -648,7 +671,7 @@ void StatsView::plotDiscreteScatter(const std::vector<dive *> &dives,
|
||||||
ValueAxis *valAxis = createAxis<ValueAxis>(valueVariable->nameWithUnit(),
|
ValueAxis *valAxis = createAxis<ValueAxis>(valueVariable->nameWithUnit(),
|
||||||
minValue, maxValue, valueVariable->decimals(), false);
|
minValue, maxValue, valueVariable->decimals(), false);
|
||||||
|
|
||||||
addAxes(catAxis, valAxis);
|
setAxes(catAxis, valAxis);
|
||||||
ScatterSeries *series = createSeries<ScatterSeries>(*categoryVariable, *valueVariable);
|
ScatterSeries *series = createSeries<ScatterSeries>(*categoryVariable, *valueVariable);
|
||||||
|
|
||||||
double x = 0.0;
|
double x = 0.0;
|
||||||
|
@ -658,18 +681,18 @@ void StatsView::plotDiscreteScatter(const std::vector<dive *> &dives,
|
||||||
if (quartiles) {
|
if (quartiles) {
|
||||||
StatsQuartiles quartiles = StatsVariable::quartiles(array);
|
StatsQuartiles quartiles = StatsVariable::quartiles(array);
|
||||||
if (quartiles.isValid()) {
|
if (quartiles.isValid()) {
|
||||||
quartileMarkers.emplace_back(x, quartiles.q1, series);
|
quartileMarkers.emplace_back(x, quartiles.q1, chart, catAxis, valAxis);
|
||||||
quartileMarkers.emplace_back(x, quartiles.q2, series);
|
quartileMarkers.emplace_back(x, quartiles.q2, chart, catAxis, valAxis);
|
||||||
quartileMarkers.emplace_back(x, quartiles.q3, series);
|
quartileMarkers.emplace_back(x, quartiles.q3, chart, catAxis, valAxis);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
x += 1.0;
|
x += 1.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatsView::QuartileMarker::QuartileMarker(double pos, double value, QtCharts::QAbstractSeries *series) :
|
StatsView::QuartileMarker::QuartileMarker(double pos, double value, QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis) :
|
||||||
item(new QGraphicsLineItem(series->chart())),
|
item(new QGraphicsLineItem(chart)),
|
||||||
series(series),
|
xAxis(xAxis), yAxis(yAxis),
|
||||||
pos(pos),
|
pos(pos),
|
||||||
value(value)
|
value(value)
|
||||||
{
|
{
|
||||||
|
@ -680,15 +703,18 @@ StatsView::QuartileMarker::QuartileMarker(double pos, double value, QtCharts::QA
|
||||||
|
|
||||||
void StatsView::QuartileMarker::updatePosition()
|
void StatsView::QuartileMarker::updatePosition()
|
||||||
{
|
{
|
||||||
QtCharts::QChart *chart = series->chart();
|
if (!xAxis || !yAxis)
|
||||||
QPointF center = chart->mapToPosition(QPointF(pos, value), series);
|
return;
|
||||||
item->setLine(center.x() - quartileMarkerSize / 2.0, center.y(),
|
double x = xAxis->toScreen(pos);
|
||||||
center.x() + quartileMarkerSize / 2.0, center.y());
|
double y = yAxis->toScreen(value);
|
||||||
|
item->setLine(x - quartileMarkerSize / 2.0, y,
|
||||||
|
x + quartileMarkerSize / 2.0, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
StatsView::LineMarker::LineMarker(QPointF from, QPointF to, QPen pen, QtCharts::QAbstractSeries *series) :
|
StatsView::LineMarker::LineMarker(QPointF from, QPointF to, QPen pen, QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis) :
|
||||||
item(new QGraphicsLineItem(series->chart())),
|
item(new QGraphicsLineItem(chart)),
|
||||||
series(series), from(from), to(to)
|
xAxis(xAxis), yAxis(yAxis),
|
||||||
|
from(from), to(to)
|
||||||
{
|
{
|
||||||
item->setZValue(ZValues::chartFeatures);
|
item->setZValue(ZValues::chartFeatures);
|
||||||
item->setPen(pen);
|
item->setPen(pen);
|
||||||
|
@ -697,12 +723,16 @@ StatsView::LineMarker::LineMarker(QPointF from, QPointF to, QPen pen, QtCharts::
|
||||||
|
|
||||||
void StatsView::LineMarker::updatePosition()
|
void StatsView::LineMarker::updatePosition()
|
||||||
{
|
{
|
||||||
QtCharts::QChart *chart = series->chart();
|
if (!xAxis || !yAxis)
|
||||||
item->setLine(QLineF(chart->mapToPosition(from, series),
|
return;
|
||||||
chart->mapToPosition(to, series)));
|
double x1 = xAxis->toScreen(from.x());
|
||||||
|
double y1 = yAxis->toScreen(from.y());
|
||||||
|
double x2 = xAxis->toScreen(to.x());
|
||||||
|
double y2 = yAxis->toScreen(to.y());
|
||||||
|
item->setLine(x1, y1, x2, y2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void StatsView::addLinearRegression(double a, double b, double minX, double maxX, double minY, double maxY, QtCharts::QAbstractSeries *series)
|
void StatsView::addLinearRegression(double a, double b, double minX, double maxX, double minY, double maxY, StatsAxis *xAxis, StatsAxis *yAxis)
|
||||||
{
|
{
|
||||||
// Sanity check: line above or below chart
|
// Sanity check: line above or below chart
|
||||||
double y1 = a * minX + b;
|
double y1 = a * minX + b;
|
||||||
|
@ -721,14 +751,14 @@ void StatsView::addLinearRegression(double a, double b, double minX, double maxX
|
||||||
minX = std::max(minX, intersect_x1);
|
minX = std::max(minX, intersect_x1);
|
||||||
maxX = std::min(maxX, intersect_x2);
|
maxX = std::min(maxX, intersect_x2);
|
||||||
}
|
}
|
||||||
lineMarkers.emplace_back(QPointF(minX, a * minX + b), QPointF(maxX, a * maxX + b), QPen(Qt::red), series);
|
lineMarkers.emplace_back(QPointF(minX, a * minX + b), QPointF(maxX, a * maxX + b), QPen(Qt::red), chart, xAxis, yAxis);
|
||||||
}
|
}
|
||||||
|
|
||||||
void StatsView::addHistogramMarker(double pos, double low, double high, const QPen &pen, bool isHorizontal, QtCharts::QAbstractSeries *series)
|
void StatsView::addHistogramMarker(double pos, double low, double high, const QPen &pen, bool isHorizontal, StatsAxis *xAxis, StatsAxis *yAxis)
|
||||||
{
|
{
|
||||||
QPointF from = isHorizontal ? QPointF(low, pos) : QPointF(pos, low);
|
QPointF from = isHorizontal ? QPointF(low, pos) : QPointF(pos, low);
|
||||||
QPointF to = isHorizontal ? QPointF(high, pos) : QPointF(pos, high);
|
QPointF to = isHorizontal ? QPointF(high, pos) : QPointF(pos, high);
|
||||||
lineMarkers.emplace_back(from, to, pen, series);
|
lineMarkers.emplace_back(from, to, pen, chart, xAxis, yAxis);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yikes, we get our data in different kinds of (bin, value) pairs.
|
// Yikes, we get our data in different kinds of (bin, value) pairs.
|
||||||
|
@ -780,9 +810,9 @@ void StatsView::plotHistogramCountChart(const std::vector<dive *> &dives,
|
||||||
double chartHeight = valAxis->minMax().second;
|
double chartHeight = valAxis->minMax().second;
|
||||||
|
|
||||||
if (isHorizontal)
|
if (isHorizontal)
|
||||||
addAxes(valAxis, catAxis);
|
setAxes(valAxis, catAxis);
|
||||||
else
|
else
|
||||||
addAxes(catAxis, valAxis);
|
setAxes(catAxis, valAxis);
|
||||||
|
|
||||||
std::vector<BarSeries::CountItem> items;
|
std::vector<BarSeries::CountItem> items;
|
||||||
items.reserve(categoryBins.size());
|
items.reserve(categoryBins.size());
|
||||||
|
@ -797,7 +827,7 @@ void StatsView::plotHistogramCountChart(const std::vector<dive *> &dives,
|
||||||
categoryBinner->formatWithUnit(*bin), total });
|
categoryBinner->formatWithUnit(*bin), total });
|
||||||
}
|
}
|
||||||
|
|
||||||
BarSeries *series = createSeries<BarSeries>(isHorizontal, categoryVariable->name(), items);
|
createSeries<BarSeries>(isHorizontal, categoryVariable->name(), items);
|
||||||
|
|
||||||
if (categoryVariable->type() == StatsVariable::Type::Numeric) {
|
if (categoryVariable->type() == StatsVariable::Type::Numeric) {
|
||||||
if (showMean) {
|
if (showMean) {
|
||||||
|
@ -805,14 +835,14 @@ void StatsView::plotHistogramCountChart(const std::vector<dive *> &dives,
|
||||||
QPen meanPen(Qt::green);
|
QPen meanPen(Qt::green);
|
||||||
meanPen.setWidth(2);
|
meanPen.setWidth(2);
|
||||||
if (!std::isnan(mean))
|
if (!std::isnan(mean))
|
||||||
addHistogramMarker(mean, 0.0, chartHeight, meanPen, isHorizontal, series);
|
addHistogramMarker(mean, 0.0, chartHeight, meanPen, isHorizontal, xAxis, yAxis);
|
||||||
}
|
}
|
||||||
if (showMedian) {
|
if (showMedian) {
|
||||||
double median = categoryVariable->quartiles(dives).q2;
|
double median = categoryVariable->quartiles(dives).q2;
|
||||||
QPen medianPen(Qt::red);
|
QPen medianPen(Qt::red);
|
||||||
medianPen.setWidth(2);
|
medianPen.setWidth(2);
|
||||||
if (!std::isnan(median))
|
if (!std::isnan(median))
|
||||||
addHistogramMarker(median, 0.0, chartHeight, medianPen, isHorizontal, series);
|
addHistogramMarker(median, 0.0, chartHeight, medianPen, isHorizontal, xAxis, yAxis);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -845,9 +875,9 @@ void StatsView::plotHistogramValueChart(const std::vector<dive *> &dives,
|
||||||
0.0, maxValue, decimals, isHorizontal);
|
0.0, maxValue, decimals, isHorizontal);
|
||||||
|
|
||||||
if (isHorizontal)
|
if (isHorizontal)
|
||||||
addAxes(valAxis, catAxis);
|
setAxes(valAxis, catAxis);
|
||||||
else
|
else
|
||||||
addAxes(catAxis, valAxis);
|
setAxes(catAxis, valAxis);
|
||||||
|
|
||||||
std::vector<BarSeries::ValueItem> items;
|
std::vector<BarSeries::ValueItem> items;
|
||||||
items.reserve(categoryBins.size());
|
items.reserve(categoryBins.size());
|
||||||
|
@ -894,9 +924,9 @@ void StatsView::plotHistogramStackedChart(const std::vector<dive *> &dives,
|
||||||
CountAxis *valAxis = createCountAxis(data.maxCategoryCount, isHorizontal);
|
CountAxis *valAxis = createCountAxis(data.maxCategoryCount, isHorizontal);
|
||||||
|
|
||||||
if (isHorizontal)
|
if (isHorizontal)
|
||||||
addAxes(valAxis, catAxis);
|
setAxes(valAxis, catAxis);
|
||||||
else
|
else
|
||||||
addAxes(catAxis, valAxis);
|
setAxes(catAxis, valAxis);
|
||||||
|
|
||||||
std::vector<BarSeries::MultiItem> items;
|
std::vector<BarSeries::MultiItem> items;
|
||||||
items.reserve(data.hbin_counts.size());
|
items.reserve(data.hbin_counts.size());
|
||||||
|
@ -933,7 +963,7 @@ void StatsView::plotHistogramBoxChart(const std::vector<dive *> &dives,
|
||||||
ValueAxis *valueAxis = createAxis<ValueAxis>(valueVariable->nameWithUnit(),
|
ValueAxis *valueAxis = createAxis<ValueAxis>(valueVariable->nameWithUnit(),
|
||||||
minY, maxY, valueVariable->decimals(), false);
|
minY, maxY, valueVariable->decimals(), false);
|
||||||
|
|
||||||
addAxes(catAxis, valueAxis);
|
setAxes(catAxis, valueAxis);
|
||||||
|
|
||||||
BoxSeries *series = createSeries<BoxSeries>(valueVariable->name(), valueVariable->unitSymbol(), valueVariable->decimals());
|
BoxSeries *series = createSeries<BoxSeries>(valueVariable->name(), valueVariable->unitSymbol(), valueVariable->decimals());
|
||||||
|
|
||||||
|
@ -1020,7 +1050,7 @@ void StatsView::plotScatter(const std::vector<dive *> &dives, const StatsVariabl
|
||||||
|
|
||||||
StatsAxis *axisY = createAxis<ValueAxis>(valueVariable->nameWithUnit(), minY, maxY, valueVariable->decimals(), false);
|
StatsAxis *axisY = createAxis<ValueAxis>(valueVariable->nameWithUnit(), minY, maxY, valueVariable->decimals(), false);
|
||||||
|
|
||||||
addAxes(axisX, axisY);
|
setAxes(axisX, axisY);
|
||||||
ScatterSeries *series = createSeries<ScatterSeries>(*categoryVariable, *valueVariable);
|
ScatterSeries *series = createSeries<ScatterSeries>(*categoryVariable, *valueVariable);
|
||||||
|
|
||||||
for (auto [x, y, dive]: points)
|
for (auto [x, y, dive]: points)
|
||||||
|
@ -1031,6 +1061,6 @@ void StatsView::plotScatter(const std::vector<dive *> &dives, const StatsVariabl
|
||||||
if (!std::isnan(a)) {
|
if (!std::isnan(a)) {
|
||||||
auto [minx, maxx] = axisX->minMax();
|
auto [minx, maxx] = axisX->minMax();
|
||||||
auto [miny, maxy] = axisY->minMax();
|
auto [miny, maxy] = axisY->minMax();
|
||||||
addLinearRegression(a, b, minx, maxx, miny, maxy, series);
|
addLinearRegression(a, b, minx, maxx, miny, maxy, xAxis, yAxis);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ private slots:
|
||||||
void replotIfVisible();
|
void replotIfVisible();
|
||||||
private:
|
private:
|
||||||
void reset(); // clears all series and axes
|
void reset(); // clears all series and axes
|
||||||
void addAxes(StatsAxis *x, StatsAxis *y); // Add new x- and y-axis
|
void setAxes(StatsAxis *x, StatsAxis *y);
|
||||||
void plotBarChart(const std::vector<dive *> &dives,
|
void plotBarChart(const std::vector<dive *> &dives,
|
||||||
ChartSubType subType,
|
ChartSubType subType,
|
||||||
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
|
const StatsVariable *categoryVariable, const StatsBinner *categoryBinner,
|
||||||
|
@ -99,23 +99,23 @@ private:
|
||||||
// A short line used to mark quartiles
|
// A short line used to mark quartiles
|
||||||
struct QuartileMarker {
|
struct QuartileMarker {
|
||||||
std::unique_ptr<QGraphicsLineItem> item;
|
std::unique_ptr<QGraphicsLineItem> item;
|
||||||
QtCharts::QAbstractSeries *series; // In case we ever support charts with multiple axes
|
StatsAxis *xAxis, *yAxis;
|
||||||
double pos, value;
|
double pos, value;
|
||||||
QuartileMarker(double pos, double value, QtCharts::QAbstractSeries *series);
|
QuartileMarker(double pos, double value, QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis);
|
||||||
void updatePosition();
|
void updatePosition();
|
||||||
};
|
};
|
||||||
|
|
||||||
// A general line marker
|
// A general line marker
|
||||||
struct LineMarker {
|
struct LineMarker {
|
||||||
std::unique_ptr<QGraphicsLineItem> item;
|
std::unique_ptr<QGraphicsLineItem> item;
|
||||||
QtCharts::QAbstractSeries *series; // In case we ever support charts with multiple axes
|
StatsAxis *xAxis, *yAxis;
|
||||||
QPointF from, to; // In local coordinates
|
QPointF from, to; // In local coordinates
|
||||||
void updatePosition();
|
void updatePosition();
|
||||||
LineMarker(QPointF from, QPointF to, QPen pen, QtCharts::QAbstractSeries *series);
|
LineMarker(QPointF from, QPointF to, QPen pen, QtCharts::QChart *chart, StatsAxis *xAxis, StatsAxis *yAxis);
|
||||||
};
|
};
|
||||||
|
|
||||||
void addLinearRegression(double a, double b, double minX, double maxX, double minY, double maxY, QtCharts::QAbstractSeries *series);
|
void addLinearRegression(double a, double b, double minX, double maxX, double minY, double maxY, StatsAxis *xAxis, StatsAxis *yAxis);
|
||||||
void addHistogramMarker(double pos, double low, double high, const QPen &pen, bool isHorizontal, QtCharts::QAbstractSeries *series);
|
void addHistogramMarker(double pos, double low, double high, const QPen &pen, bool isHorizontal, StatsAxis *xAxis, StatsAxis *yAxis);
|
||||||
|
|
||||||
StatsState state;
|
StatsState state;
|
||||||
QtCharts::QChart *chart;
|
QtCharts::QChart *chart;
|
||||||
|
@ -127,6 +127,7 @@ private:
|
||||||
std::vector<LineMarker> lineMarkers;
|
std::vector<LineMarker> lineMarkers;
|
||||||
std::unique_ptr<QGraphicsSimpleTextItem> title;
|
std::unique_ptr<QGraphicsSimpleTextItem> title;
|
||||||
StatsSeries *highlightedSeries;
|
StatsSeries *highlightedSeries;
|
||||||
|
StatsAxis *xAxis, *yAxis;
|
||||||
|
|
||||||
// This is unfortunate: we can't derive from QChart, because the chart is allocated by QML.
|
// This is unfortunate: we can't derive from QChart, because the chart is allocated by QML.
|
||||||
// Therefore, we have to listen to hover events using an events-filter.
|
// Therefore, we have to listen to hover events using an events-filter.
|
||||||
|
|
|
@ -6,12 +6,12 @@
|
||||||
#ifndef ZVALUES_H
|
#ifndef ZVALUES_H
|
||||||
|
|
||||||
struct ZValues {
|
struct ZValues {
|
||||||
static constexpr double axes = 0.0;
|
static constexpr double series = 0.0;
|
||||||
static constexpr double series = 11.0;
|
static constexpr double axes = 1.0;
|
||||||
static constexpr double seriesLabels = 12.0;
|
static constexpr double seriesLabels = 2.0;
|
||||||
static constexpr double chartFeatures = 13.0; // quartile markers and regression lines
|
static constexpr double chartFeatures = 3.0; // quartile markers and regression lines
|
||||||
static constexpr double informationBox = 14.0;
|
static constexpr double informationBox = 4.0;
|
||||||
static constexpr double legend = 15.0;
|
static constexpr double legend = 5.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
Loading…
Reference in a new issue