statistics: collect colors in a StatsTheme class

To enable rudimentary theming, collect all colors in a new
theme class. The class has to be passed down to the various
items.

In general the items save a reference to the them in the
constructor. Alternatively, they might also just query
the StatsView everytime they need to access a color.
For now, it's hard the say what is preferred: a reference
per item or a function call per invokation?

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
This commit is contained in:
Berthold Stoeger 2021-02-16 17:05:39 +01:00 committed by Dirk Hohndel
parent 56e02dbcc0
commit b5aac29cea
25 changed files with 272 additions and 166 deletions

View file

@ -116,16 +116,17 @@ void BarSeries::BarLabel::setVisible(bool visible)
item->setVisible(visible); item->setVisible(visible);
} }
void BarSeries::BarLabel::highlight(bool highlight, int bin_nr, int binCount, const QColor &background) void BarSeries::BarLabel::highlight(bool highlight, int bin_nr, int binCount, const QColor &background, const StatsTheme &theme)
{ {
// For labels that are on top of a bar, use the corresponding bar color // For labels that are on top of a bar, use the corresponding bar color
// as background. Rendering on a transparent background gives ugly artifacts. // as background. Rendering on a transparent background gives ugly artifacts.
item->setColor(highlight || isOutside ? darkLabelColor : labelColor(bin_nr, binCount), item->setColor(highlight || isOutside ? theme.darkLabelColor : theme.labelColor(bin_nr, binCount),
isOutside ? Qt::transparent : background); isOutside ? Qt::transparent : background);
} }
void BarSeries::BarLabel::updatePosition(bool horizontal, bool center, const QRectF &rect, void BarSeries::BarLabel::updatePosition(bool horizontal, bool center, const QRectF &rect,
int bin_nr, int binCount, const QColor &background) int bin_nr, int binCount, const QColor &background,
const StatsTheme &theme)
{ {
QSizeF itemSize = item->getRect().size(); QSizeF itemSize = item->getRect().size();
if (!horizontal) { if (!horizontal) {
@ -173,13 +174,14 @@ void BarSeries::BarLabel::updatePosition(bool horizontal, bool center, const QRe
} }
setVisible(true); setVisible(true);
// If label changed from inside to outside, or vice-versa, the color might change. // If label changed from inside to outside, or vice-versa, the color might change.
highlight(false, bin_nr, binCount, background); highlight(false, bin_nr, binCount, background, theme);
} }
BarSeries::Item::Item(BarSeries *series, double lowerBound, double upperBound, BarSeries::Item::Item(BarSeries *series, double lowerBound, double upperBound,
std::vector<SubItem> subitemsIn, std::vector<SubItem> subitemsIn,
const QString &binName, const StatsOperationResults &res, int total, const QString &binName, const StatsOperationResults &res, int total,
bool horizontal, bool stacked, int binCount) : bool horizontal, bool stacked, int binCount,
const StatsTheme &theme) :
lowerBound(lowerBound), lowerBound(lowerBound),
upperBound(upperBound), upperBound(upperBound),
subitems(std::move(subitemsIn)), subitems(std::move(subitemsIn)),
@ -188,30 +190,30 @@ BarSeries::Item::Item(BarSeries *series, double lowerBound, double upperBound,
total(total) total(total)
{ {
for (SubItem &item: subitems) for (SubItem &item: subitems)
item.highlight(false, binCount); item.highlight(false, binCount, theme);
updatePosition(series, horizontal, stacked, binCount); updatePosition(series, horizontal, stacked, binCount, theme);
} }
void BarSeries::Item::highlight(int subitem, bool highlight, int binCount) void BarSeries::Item::highlight(int subitem, bool highlight, int binCount, const StatsTheme &theme)
{ {
if (subitem < 0 || subitem >= (int)subitems.size()) if (subitem < 0 || subitem >= (int)subitems.size())
return; return;
subitems[subitem].highlight(highlight, binCount); subitems[subitem].highlight(highlight, binCount, theme);
} }
// For single-bin charts, selected items are marked with a special fill and border color. // For single-bin charts, selected items are marked with a special fill and border color.
// For multi-bin charts, they are marked by a differend border color and border width. // For multi-bin charts, they are marked by a differend border color and border width.
void BarSeries::SubItem::highlight(bool highlight, int binCount) void BarSeries::SubItem::highlight(bool highlight, int binCount, const StatsTheme &theme)
{ {
fill = highlight ? highlightedColor : binColor(bin_nr, binCount); fill = highlight ? theme.highlightedColor : theme.binColor(bin_nr, binCount);
QColor border = highlight ? highlightedBorderColor : ::borderColor; QColor border = highlight ? theme.highlightedBorderColor : theme.borderColor;
item->setColor(fill, border); item->setColor(fill, border);
item->setSelected(selected); item->setSelected(selected);
if (label) if (label)
label->highlight(highlight, bin_nr, binCount, fill); label->highlight(highlight, bin_nr, binCount, fill, theme);
} }
void BarSeries::Item::updatePosition(BarSeries *series, bool horizontal, bool stacked, int binCount) void BarSeries::Item::updatePosition(BarSeries *series, bool horizontal, bool stacked, int binCount, const StatsTheme &theme)
{ {
if (subitems.empty()) if (subitems.empty())
return; return;
@ -230,7 +232,7 @@ void BarSeries::Item::updatePosition(BarSeries *series, bool horizontal, bool st
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(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, theme);
} }
rect = subitems[0].item->getRect(); rect = subitems[0].item->getRect();
for (auto it = std::next(subitems.begin()); it != subitems.end(); ++it) for (auto it = std::next(subitems.begin()); it != subitems.end(); ++it)
@ -238,7 +240,8 @@ void BarSeries::Item::updatePosition(BarSeries *series, bool horizontal, bool st
} }
void BarSeries::SubItem::updatePosition(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,
const StatsTheme &theme)
{ {
QPointF topLeft, bottomRight; QPointF topLeft, bottomRight;
if (horizontal) { if (horizontal) {
@ -251,7 +254,7 @@ void BarSeries::SubItem::updatePosition(BarSeries *series, bool horizontal, bool
QRectF rect(topLeft, bottomRight); QRectF rect(topLeft, bottomRight);
item->setRect(rect); item->setRect(rect);
if (label) if (label)
label->updatePosition(horizontal, stacked, rect, bin_nr, binCount, fill); label->updatePosition(horizontal, stacked, rect, bin_nr, binCount, fill, theme);
} }
std::vector<BarSeries::SubItem> BarSeries::makeSubItems(std::vector<SubItemDesc> items) const std::vector<BarSeries::SubItem> BarSeries::makeSubItems(std::vector<SubItemDesc> items) const
@ -294,13 +297,13 @@ void BarSeries::add_item(double lowerBound, double upperBound, std::vector<SubIt
if (subitems.empty()) if (subitems.empty())
return; return;
items.emplace_back(this, lowerBound, upperBound, std::move(subitems), binName, res, items.emplace_back(this, lowerBound, upperBound, std::move(subitems), binName, res,
total, horizontal, stacked, binCount()); total, horizontal, stacked, binCount(), theme);
} }
void BarSeries::updatePositions() void BarSeries::updatePositions()
{ {
for (Item &item: items) for (Item &item: items)
item.updatePosition(this, horizontal, stacked, binCount()); item.updatePosition(this, horizontal, stacked, binCount(), theme);
} }
// 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!
@ -407,7 +410,7 @@ bool BarSeries::hover(QPointF pos)
// Highlight new item (if any) // Highlight new item (if any)
if (highlighted.bar >= 0 && highlighted.bar < (int)items.size()) { if (highlighted.bar >= 0 && highlighted.bar < (int)items.size()) {
Item &item = items[highlighted.bar]; Item &item = items[highlighted.bar];
item.highlight(index.subitem, true, binCount()); item.highlight(index.subitem, true, binCount(), theme);
if (!information) if (!information)
information = view.createChartItem<InformationBox>(); information = view.createChartItem<InformationBox>();
information->setText(makeInfo(item, highlighted.subitem), pos); information->setText(makeInfo(item, highlighted.subitem), pos);
@ -422,7 +425,7 @@ bool BarSeries::hover(QPointF pos)
void BarSeries::unhighlight() void BarSeries::unhighlight()
{ {
if (highlighted.bar >= 0 && highlighted.bar < (int)items.size()) if (highlighted.bar >= 0 && highlighted.bar < (int)items.size())
items[highlighted.bar].highlight(highlighted.subitem, false, binCount()); items[highlighted.bar].highlight(highlighted.subitem, false, binCount(), theme);
highlighted = Index(); highlighted = Index();
} }
@ -464,7 +467,7 @@ void BarSeries::divesSelected(const QVector<dive *> &)
Index idx(&item - &items[0], &subitem - &item.subitems[0]); Index idx(&item - &items[0], &subitem - &item.subitems[0]);
bool highlight = idx == highlighted; bool highlight = idx == highlighted;
item.highlight(idx.subitem, highlight, binCount()); item.highlight(idx.subitem, highlight, binCount(), theme);
} }
} }
} }

View file

@ -95,8 +95,9 @@ private:
bool isOutside; // Is shown outside of bar bool isOutside; // Is shown outside of bar
BarLabel(StatsView &view, const std::vector<QString> &labels, int bin_nr, int binCount); BarLabel(StatsView &view, const std::vector<QString> &labels, int bin_nr, int binCount);
void setVisible(bool visible); void setVisible(bool visible);
void updatePosition(bool horizontal, bool center, const QRectF &rect, int bin_nr, int binCount, const QColor &background); void updatePosition(bool horizontal, bool center, const QRectF &rect, int bin_nr, int binCount,
void highlight(bool highlight, int bin_nr, int binCount, const QColor &background); const QColor &background, const StatsTheme &theme);
void highlight(bool highlight, int bin_nr, int binCount, const QColor &background, const StatsTheme &theme);
}; };
struct SubItem { struct SubItem {
@ -109,8 +110,8 @@ private:
bool selected; bool selected;
QColor fill; QColor fill;
void updatePosition(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, const StatsTheme &theme);
void highlight(bool highlight, int binCount); void highlight(bool highlight, int binCount, const StatsTheme &theme);
}; };
struct Item { struct Item {
@ -123,9 +124,11 @@ private:
Item(BarSeries *series, double lowerBound, double upperBound, Item(BarSeries *series, double lowerBound, double upperBound,
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(BarSeries *series, bool horizontal, bool stacked, int binCount); const StatsTheme &theme);
void highlight(int subitem, bool highlight, int binCount); void updatePosition(BarSeries *series, bool horizontal, bool stacked, int binCount,
const StatsTheme &theme);
void highlight(int subitem, bool highlight, int binCount, const StatsTheme &theme);
int getSubItemUnderMouse(const QPointF &f, bool horizontal, bool stacked) const; int getSubItemUnderMouse(const QPointF &f, bool horizontal, bool stacked) const;
}; };

View file

@ -27,13 +27,13 @@ BoxSeries::~BoxSeries()
} }
BoxSeries::Item::Item(StatsView &view, BoxSeries *series, double lowerBound, double upperBound, BoxSeries::Item::Item(StatsView &view, BoxSeries *series, double lowerBound, double upperBound,
const StatsQuartiles &qIn, const QString &binName) : const StatsQuartiles &qIn, const QString &binName, const StatsTheme &theme) :
lowerBound(lowerBound), upperBound(upperBound), q(qIn), lowerBound(lowerBound), upperBound(upperBound), q(qIn),
binName(binName), binName(binName),
selected(allDivesSelected(q.dives)) selected(allDivesSelected(q.dives))
{ {
item = view.createChartItem<ChartBoxItem>(ChartZValue::Series, boxBorderWidth); item = view.createChartItem<ChartBoxItem>(ChartZValue::Series, boxBorderWidth);
highlight(false); highlight(false, theme);
updatePosition(series); updatePosition(series);
} }
@ -41,14 +41,14 @@ BoxSeries::Item::~Item()
{ {
} }
void BoxSeries::Item::highlight(bool highlight) void BoxSeries::Item::highlight(bool highlight, const StatsTheme &theme)
{ {
if (highlight) if (highlight)
item->setColor(highlightedColor, highlightedBorderColor); item->setColor(theme.highlightedColor, theme.highlightedBorderColor);
else if (selected) else if (selected)
item->setColor(selectedColor, selectedBorderColor); item->setColor(theme.selectedColor, theme.selectedBorderColor);
else else
item->setColor(fillColor, ::borderColor); item->setColor(theme.fillColor, theme.borderColor);
} }
void BoxSeries::Item::updatePosition(BoxSeries *series) void BoxSeries::Item::updatePosition(BoxSeries *series)
@ -72,7 +72,7 @@ void BoxSeries::Item::updatePosition(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)
{ {
items.emplace_back(new Item(view, this, lowerBound, upperBound, q, binName)); items.emplace_back(new Item(view, this, lowerBound, upperBound, q, binName, theme));
} }
void BoxSeries::updatePositions() void BoxSeries::updatePositions()
@ -128,7 +128,7 @@ bool BoxSeries::hover(QPointF pos)
// Highlight new item (if any) // Highlight new item (if any)
if (highlighted >= 0 && highlighted < (int)items.size()) { if (highlighted >= 0 && highlighted < (int)items.size()) {
Item &item = *items[highlighted]; Item &item = *items[highlighted];
item.highlight(true); item.highlight(true, theme);
if (!information) if (!information)
information = view.createChartItem<InformationBox>(); information = view.createChartItem<InformationBox>();
information->setText(formatInformation(item), pos); information->setText(formatInformation(item), pos);
@ -142,7 +142,7 @@ bool BoxSeries::hover(QPointF pos)
void BoxSeries::unhighlight() void BoxSeries::unhighlight()
{ {
if (highlighted >= 0 && highlighted < (int)items.size()) if (highlighted >= 0 && highlighted < (int)items.size())
items[highlighted]->highlight(false); items[highlighted]->highlight(false, theme);
highlighted = -1; highlighted = -1;
} }
@ -183,7 +183,7 @@ void BoxSeries::divesSelected(const QVector<dive *> &)
int idx = &item - &items[0]; int idx = &item - &items[0];
bool highlight = idx == highlighted; bool highlight = idx == highlighted;
item->highlight(highlight); item->highlight(highlight, theme);
} }
} }
} }

View file

@ -40,10 +40,11 @@ private:
StatsQuartiles q; StatsQuartiles q;
QString binName; QString binName;
bool selected; bool selected;
Item(StatsView &view, BoxSeries *series, double lowerBound, double upperBound, const StatsQuartiles &q, const QString &binName); Item(StatsView &view, BoxSeries *series, double lowerBound, double upperBound, const StatsQuartiles &q,
const QString &binName, const StatsTheme &theme);
~Item(); ~Item();
void updatePosition(BoxSeries *series); void updatePosition(BoxSeries *series);
void highlight(bool highlight); void highlight(bool highlight, const StatsTheme &theme);
}; };
QString variable, unit; QString variable, unit;

View file

@ -64,7 +64,7 @@ void ChartPixmapItem::setPositionDirty()
markDirty(); markDirty();
} }
void ChartPixmapItem::render() void ChartPixmapItem::render(const StatsTheme &)
{ {
if (!node) { if (!node) {
createNode(view.w()->createImageNode()); createNode(view.w()->createImageNode());
@ -137,30 +137,25 @@ static QSGTexture *createScatterTexture(StatsView &view, const QColor &color, co
return view.w()->createTextureFromImage(img, QQuickWindow::TextureHasAlphaChannel); return view.w()->createTextureFromImage(img, QQuickWindow::TextureHasAlphaChannel);
} }
static QSGTexture *scatterItemTexture = nullptr; QSGTexture *ChartScatterItem::getTexture(const StatsTheme &theme) const
static QSGTexture *scatterItemSelectedTexture = nullptr;
static QSGTexture *scatterItemHighlightedTexture = nullptr;
static QSGTexture *selectedTexture = nullptr; // A checkerboard pattern.
QSGTexture *ChartScatterItem::getTexture() const
{ {
switch (highlight) { switch (highlight) {
default: default:
case Highlight::Unselected: case Highlight::Unselected:
return scatterItemTexture; return theme.scatterItemTexture;
case Highlight::Selected: case Highlight::Selected:
return scatterItemSelectedTexture; return theme.scatterItemSelectedTexture;
case Highlight::Highlighted: case Highlight::Highlighted:
return scatterItemHighlightedTexture; return theme.scatterItemHighlightedTexture;
} }
} }
void ChartScatterItem::render() void ChartScatterItem::render(const StatsTheme &theme)
{ {
if (!scatterItemTexture) { if (!theme.scatterItemTexture) {
scatterItemTexture = register_global(createScatterTexture(view, fillColor, borderColor)); theme.scatterItemTexture = register_global(createScatterTexture(view, theme.fillColor, theme.borderColor));
scatterItemSelectedTexture = register_global(createScatterTexture(view, selectedColor, selectedBorderColor)); theme.scatterItemSelectedTexture = register_global(createScatterTexture(view, theme.selectedColor, theme.selectedBorderColor));
scatterItemHighlightedTexture = register_global(createScatterTexture(view, highlightedColor, highlightedBorderColor)); theme.scatterItemHighlightedTexture = register_global(createScatterTexture(view, theme.highlightedColor, theme.highlightedBorderColor));
} }
if (!node) { if (!node) {
createNode(view.w()->createImageNode()); createNode(view.w()->createImageNode());
@ -169,7 +164,7 @@ void ChartScatterItem::render()
} }
updateVisible(); updateVisible();
if (textureDirty) { if (textureDirty) {
node->node->setTexture(getTexture()); node->node->setTexture(getTexture(theme));
textureDirty = false; textureDirty = false;
} }
if (positionDirty) { if (positionDirty) {
@ -288,7 +283,7 @@ ChartPieItem::ChartPieItem(StatsView &v, ChartZValue z, double borderWidth) : Ch
{ {
} }
static QBrush makeBrush(QColor fill, bool selected) static QBrush makeBrush(QColor fill, bool selected, const StatsTheme &theme)
{ {
if (!selected) if (!selected)
return QBrush(fill); return QBrush(fill);
@ -296,18 +291,18 @@ static QBrush makeBrush(QColor fill, bool selected)
img.fill(fill); img.fill(fill);
for (int x = 0; x < selectionOverlayPixelSize; ++x) { for (int x = 0; x < selectionOverlayPixelSize; ++x) {
for (int y = 0; y < selectionOverlayPixelSize; ++y) { for (int y = 0; y < selectionOverlayPixelSize; ++y) {
img.setPixelColor(x, y, selectionOverlayColor); img.setPixelColor(x, y, theme.selectionOverlayColor);
img.setPixelColor(x + selectionOverlayPixelSize, y + selectionOverlayPixelSize, img.setPixelColor(x + selectionOverlayPixelSize, y + selectionOverlayPixelSize,
selectionOverlayColor); theme.selectionOverlayColor);
} }
} }
return QBrush(img); return QBrush(img);
} }
void ChartPieItem::drawSegment(double from, double to, QColor fill, QColor border, bool selected) void ChartPieItem::drawSegment(double from, double to, QColor fill, QColor border, bool selected, const StatsTheme &theme)
{ {
painter->setPen(QPen(border, borderWidth)); painter->setPen(QPen(border, borderWidth));
painter->setBrush(makeBrush(fill, selected)); painter->setBrush(makeBrush(fill, selected, theme));
// For whatever obscure reason, angles of pie pieces are given as 16th of a degree...? // For whatever obscure reason, angles of pie pieces are given as 16th of a degree...?
// Angles increase CCW, whereas pie charts usually are read CW. Therfore, startAngle // Angles increase CCW, whereas pie charts usually are read CW. Therfore, startAngle
// is dervied from "from" and subtracted from the origin angle at 12:00. // is dervied from "from" and subtracted from the origin angle at 12:00.
@ -353,7 +348,7 @@ void setPoint(QSGGeometry::TexturedPoint2D &v, const QPointF &p, const QPointF &
static_cast<float>(t.x()), static_cast<float>(t.y())); static_cast<float>(t.x()), static_cast<float>(t.y()));
} }
void ChartLineItem::render() void ChartLineItem::render(const StatsTheme &)
{ {
if (!node) { if (!node) {
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2)); geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2));
@ -384,7 +379,7 @@ void ChartLineItem::render()
positionDirty = materialDirty = false; positionDirty = materialDirty = false;
} }
void ChartRectLineItem::render() void ChartRectLineItem::render(const StatsTheme &)
{ {
if (!node) { if (!node) {
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4)); geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4));
@ -427,19 +422,19 @@ ChartBarItem::~ChartBarItem()
{ {
} }
QSGTexture *ChartBarItem::getSelectedTexture() const QSGTexture *ChartBarItem::getSelectedTexture(const StatsTheme &theme) const
{ {
if (!selectedTexture) { if (!theme.selectedTexture) {
QImage img(2, 2, QImage::Format_ARGB32); QImage img(2, 2, QImage::Format_ARGB32);
img.fill(Qt::transparent); img.fill(Qt::transparent);
img.setPixelColor(0, 0, selectionOverlayColor); img.setPixelColor(0, 0, theme.selectionOverlayColor);
img.setPixelColor(1, 1, selectionOverlayColor); img.setPixelColor(1, 1, theme.selectionOverlayColor);
selectedTexture = register_global(view.w()->createTextureFromImage(img, QQuickWindow::TextureHasAlphaChannel)); theme.selectedTexture = register_global(view.w()->createTextureFromImage(img, QQuickWindow::TextureHasAlphaChannel));
} }
return selectedTexture; return theme.selectedTexture;
} }
void ChartBarItem::render() void ChartBarItem::render(const StatsTheme &theme)
{ {
if (!node) { if (!node) {
createNode(view.w()->createRectangleNode()); createNode(view.w()->createRectangleNode());
@ -483,7 +478,7 @@ void ChartBarItem::render()
selectionGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4)); selectionGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4));
selectionGeometry->setDrawingMode(QSGGeometry::DrawTriangleFan); selectionGeometry->setDrawingMode(QSGGeometry::DrawTriangleFan);
selectionMaterial.reset(new QSGTextureMaterial); selectionMaterial.reset(new QSGTextureMaterial);
selectionMaterial->setTexture(getSelectedTexture()); selectionMaterial->setTexture(getSelectedTexture(theme));
selectionMaterial->setHorizontalWrapMode(QSGTexture::Repeat); selectionMaterial->setHorizontalWrapMode(QSGTexture::Repeat);
selectionMaterial->setVerticalWrapMode(QSGTexture::Repeat); selectionMaterial->setVerticalWrapMode(QSGTexture::Repeat);
selectionNode.reset(new QSGGeometryNode); selectionNode.reset(new QSGGeometryNode);
@ -556,12 +551,12 @@ ChartBoxItem::~ChartBoxItem()
{ {
} }
void ChartBoxItem::render() void ChartBoxItem::render(const StatsTheme &theme)
{ {
// Remember old dirty values, since ChartBarItem::render() will clear them // Remember old dirty values, since ChartBarItem::render() will clear them
bool oldPositionDirty = positionDirty; bool oldPositionDirty = positionDirty;
bool oldColorDirty = colorDirty; bool oldColorDirty = colorDirty;
ChartBarItem::render(); // This will create the base node, so no need to check for that. ChartBarItem::render(theme); // This will create the base node, so no need to check for that.
if (!whiskersNode) { if (!whiskersNode) {
whiskersGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 10)); whiskersGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 10));
whiskersGeometry->setDrawingMode(QSGGeometry::DrawLines); whiskersGeometry->setDrawingMode(QSGGeometry::DrawLines);

View file

@ -16,12 +16,14 @@ class QSGImageNode;
class QSGRectangleNode; class QSGRectangleNode;
class QSGTexture; class QSGTexture;
class QSGTextureMaterial; class QSGTextureMaterial;
class StatsTheme;
class StatsView; class StatsView;
enum class ChartZValue : int; enum class ChartZValue : int;
class ChartItem { class ChartItem {
public: public:
virtual void render() = 0; // Only call on render thread! // Only call on render thread!
virtual void render(const StatsTheme &theme) = 0;
bool dirty; // If true, call render() when rebuilding the scene bool dirty; // If true, call render() when rebuilding the scene
ChartItem *prev, *next; // Double linked list of items ChartItem *prev, *next; // Double linked list of items
const ChartZValue zValue; const ChartZValue zValue;
@ -58,7 +60,7 @@ public:
~ChartPixmapItem(); ~ChartPixmapItem();
void setPos(QPointF pos); void setPos(QPointF pos);
void render() override; // Only call on render thread! void render(const StatsTheme &theme) override;
QRectF getRect() const; QRectF getRect() const;
protected: protected:
void resize(QSizeF size); // Resets the canvas. Attention: image is *unitialized*. void resize(QSizeF size); // Resets the canvas. Attention: image is *unitialized*.
@ -107,7 +109,7 @@ private:
class ChartPieItem : public ChartPixmapItem { class ChartPieItem : public ChartPixmapItem {
public: public:
ChartPieItem(StatsView &v, ChartZValue z, double borderWidth); ChartPieItem(StatsView &v, ChartZValue z, double borderWidth);
void drawSegment(double from, double to, QColor fill, QColor border, bool selected); // from and to are relative (0-1 is full disk). void drawSegment(double from, double to, QColor fill, QColor border, bool selected, const StatsTheme &theme); // from and to are relative (0-1 is full disk).
void resize(QSizeF size); // As in base class, but clears the canvas void resize(QSizeF size); // As in base class, but clears the canvas
private: private:
double borderWidth; double borderWidth;
@ -132,14 +134,14 @@ protected:
class ChartLineItem : public ChartLineItemBase { class ChartLineItem : public ChartLineItemBase {
public: public:
using ChartLineItemBase::ChartLineItemBase; using ChartLineItemBase::ChartLineItemBase;
void render() override; // Only call on render thread! void render(const StatsTheme &theme) override;
}; };
// A simple rectangle without fill. Specified by any two opposing vertices. // A simple rectangle without fill. Specified by any two opposing vertices.
class ChartRectLineItem : public ChartLineItemBase { class ChartRectLineItem : public ChartLineItemBase {
public: public:
using ChartLineItemBase::ChartLineItemBase; using ChartLineItemBase::ChartLineItemBase;
void render() override; // Only call on render thread! void render(const StatsTheme &theme) override;
}; };
// A bar in a bar chart: a rectangle bordered by lines. // A bar in a bar chart: a rectangle bordered by lines.
@ -151,7 +153,7 @@ public:
void setRect(const QRectF &rect); void setRect(const QRectF &rect);
void setSelected(bool selected); void setSelected(bool selected);
QRectF getRect() const; QRectF getRect() const;
void render() override; // Only call on render thread! void render(const StatsTheme &theme) override;
protected: protected:
QColor color, borderColor; QColor color, borderColor;
double borderWidth; double borderWidth;
@ -168,7 +170,7 @@ private:
std::unique_ptr<QSGGeometryNode> selectionNode; std::unique_ptr<QSGGeometryNode> selectionNode;
std::unique_ptr<QSGTextureMaterial> selectionMaterial; std::unique_ptr<QSGTextureMaterial> selectionMaterial;
std::unique_ptr<QSGGeometry> selectionGeometry; std::unique_ptr<QSGGeometry> selectionGeometry;
QSGTexture *getSelectedTexture() const; QSGTexture *getSelectedTexture(const StatsTheme &theme) const;
}; };
// A box-and-whiskers item. This is a bit lazy: derive from the bar item and add whiskers. // A box-and-whiskers item. This is a bit lazy: derive from the bar item and add whiskers.
@ -178,7 +180,7 @@ public:
~ChartBoxItem(); ~ChartBoxItem();
void setBox(const QRectF &rect, double min, double max, double median); // The rect describes Q1, Q3. void setBox(const QRectF &rect, double min, double max, double median); // The rect describes Q1, Q3.
QRectF getRect() const; // Note: this extends the center rectangle to include the whiskers. QRectF getRect() const; // Note: this extends the center rectangle to include the whiskers.
void render() override; // Only call on render thread! void render(const StatsTheme &theme) override;
private: private:
double min, max, median; double min, max, median;
std::unique_ptr<QSGGeometryNode> whiskersNode; std::unique_ptr<QSGGeometryNode> whiskersNode;
@ -203,12 +205,12 @@ public:
}; };
void setPos(QPointF pos); // Specifies the *center* of the item. void setPos(QPointF pos); // Specifies the *center* of the item.
void setHighlight(Highlight highlight); // In the future, support different kinds of scatter items. void setHighlight(Highlight highlight); // In the future, support different kinds of scatter items.
void render() override; // Only call on render thread! void render(const StatsTheme &theme) override;
QRectF getRect() const; QRectF getRect() const;
bool contains(QPointF point) const; bool contains(QPointF point) const;
bool inRect(const QRectF &rect) const; bool inRect(const QRectF &rect) const;
private: private:
QSGTexture *getTexture() const; QSGTexture *getTexture(const StatsTheme &theme) const;
QRectF rect; QRectF rect;
QSizeF textureSize; QSizeF textureSize;
bool positionDirty, textureDirty; bool positionDirty, textureDirty;

View file

@ -11,8 +11,9 @@ static const int distanceFromPointer = 10; // Distance to place box from mouse p
InformationBox::InformationBox(StatsView &v) : InformationBox::InformationBox(StatsView &v) :
ChartRectItem(v, ChartZValue::InformationBox, ChartRectItem(v, ChartZValue::InformationBox,
QPen(informationBorderColor, informationBorder), QPen(v.getCurrentTheme().informationBorderColor, informationBorder),
QBrush(informationColor), informationBorderRadius), QBrush(v.getCurrentTheme().informationColor), informationBorderRadius),
theme(v.getCurrentTheme()),
width(0.0), width(0.0),
height(0.0) height(0.0)
{ {
@ -36,7 +37,7 @@ void InformationBox::setText(const std::vector<QString> &text, QPointF pos)
ChartRectItem::resize(QSizeF(width, height)); ChartRectItem::resize(QSizeF(width, height));
painter->setPen(QPen(darkLabelColor)); // QPainter uses QPen to set text color! painter->setPen(QPen(theme.darkLabelColor)); // QPainter uses QPen to set text color!
double y = 2.0 * informationBorder; double y = 2.0 * informationBorder;
for (size_t i = 0; i < widths.size(); ++i) { for (size_t i = 0; i < widths.size(); ++i) {
QRectF rect(2.0 * informationBorder, y, widths[i], fontHeight); QRectF rect(2.0 * informationBorder, y, widths[i], fontHeight);

View file

@ -20,6 +20,7 @@ struct InformationBox : ChartRectItem {
void setPos(QPointF pos); void setPos(QPointF pos);
int recommendedMaxLines() const; int recommendedMaxLines() const;
private: private:
const StatsTheme &theme; // Set once in constructor.
QFont font; // For future specialization. QFont font; // For future specialization.
double width, height; double width, height;
}; };

View file

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
#include "legend.h" #include "legend.h"
#include "statscolors.h" #include "statscolors.h"
#include "statsview.h"
#include "zvalues.h" #include "zvalues.h"
#include <cmath> #include <cmath>
@ -15,8 +16,10 @@ static const double legendInternalBorderSize = 2.0;
Legend::Legend(StatsView &view, const std::vector<QString> &names) : Legend::Legend(StatsView &view, const std::vector<QString> &names) :
ChartRectItem(view, ChartZValue::Legend, ChartRectItem(view, ChartZValue::Legend,
QPen(legendBorderColor, legendBorderSize), QBrush(legendColor), legendBoxBorderRadius), QPen(view.getCurrentTheme().legendBorderColor, legendBorderSize),
QBrush(view.getCurrentTheme().legendColor), legendBoxBorderRadius),
displayedItems(0), width(0.0), height(0.0), displayedItems(0), width(0.0), height(0.0),
theme(view.getCurrentTheme()),
font(QFont()), // Make configurable font(QFont()), // Make configurable
posInitialized(false) posInitialized(false)
{ {
@ -25,12 +28,12 @@ Legend::Legend(StatsView &view, const std::vector<QString> &names) :
fontHeight = fm.height(); fontHeight = fm.height();
int idx = 0; int idx = 0;
for (const QString &name: names) for (const QString &name: names)
entries.emplace_back(name, idx++, (int)names.size(), fm); entries.emplace_back(name, idx++, (int)names.size(), fm, theme);
} }
Legend::Entry::Entry(const QString &name, int idx, int numBins, const QFontMetrics &fm) : Legend::Entry::Entry(const QString &name, int idx, int numBins, const QFontMetrics &fm, const StatsTheme &theme) :
name(name), name(name),
rectBrush(QBrush(binColor(idx, numBins))) rectBrush(QBrush(theme.binColor(idx, numBins)))
{ {
width = fm.height() + 2.0 * legendBoxBorderSize + fm.size(Qt::TextSingleLine, name).width(); width = fm.height() + 2.0 * legendBoxBorderSize + fm.size(Qt::TextSingleLine, name).width();
} }
@ -82,7 +85,7 @@ void Legend::resize()
ChartRectItem::resize(QSizeF(width, height)); ChartRectItem::resize(QSizeF(width, height));
// Paint rectangles // Paint rectangles
painter->setPen(QPen(legendBorderColor, legendBoxBorderSize)); painter->setPen(QPen(theme.legendBorderColor, legendBoxBorderSize));
for (int i = 0; i < displayedItems; ++i) { for (int i = 0; i < displayedItems; ++i) {
QPointF itemPos = entries[i].pos; QPointF itemPos = entries[i].pos;
painter->setBrush(entries[i].rectBrush); painter->setBrush(entries[i].rectBrush);
@ -94,7 +97,7 @@ void Legend::resize()
} }
// Paint labels // Paint labels
painter->setPen(darkLabelColor); // QPainter uses pen not brush for text! painter->setPen(theme.darkLabelColor); // QPainter uses pen not brush for text!
painter->setFont(font); painter->setFont(font);
for (int i = 0; i < displayedItems; ++i) { for (int i = 0; i < displayedItems; ++i) {
QPointF itemPos = entries[i].pos; QPointF itemPos = entries[i].pos;

View file

@ -10,6 +10,7 @@
#include <QFont> #include <QFont>
class QFontMetrics; class QFontMetrics;
class StatsTheme;
class Legend : public ChartRectItem { class Legend : public ChartRectItem {
public: public:
@ -23,11 +24,12 @@ private:
QBrush rectBrush; QBrush rectBrush;
QPointF pos; QPointF pos;
double width; double width;
Entry(const QString &name, int idx, int numBins, const QFontMetrics &fm); Entry(const QString &name, int idx, int numBins, const QFontMetrics &fm, const StatsTheme &);
}; };
int displayedItems; int displayedItems;
double width; double width;
double height; double height;
const StatsTheme &theme; // Set once in constructor.
QFont font; QFont font;
// The position is specified with respect to the center and in relative terms // The position is specified with respect to the center and in relative terms
// with respect to the canvas. // with respect to the canvas.

View file

@ -18,7 +18,7 @@ static const double innerLabelRadius = 0.75; // 1.0 = at outer border of pie
static const double outerLabelRadius = 1.01; // 1.0 = at outer border of pie static const double outerLabelRadius = 1.01; // 1.0 = at outer border of pie
PieSeries::Item::Item(StatsView &view, const QString &name, int from, std::vector<dive *> divesIn, int totalCount, PieSeries::Item::Item(StatsView &view, const QString &name, int from, std::vector<dive *> divesIn, int totalCount,
int bin_nr, int numBins) : int bin_nr, int numBins, const StatsTheme &theme) :
name(name), name(name),
dives(std::move(divesIn)), dives(std::move(divesIn)),
selected(allDivesSelected(dives)) selected(allDivesSelected(dives))
@ -43,7 +43,7 @@ PieSeries::Item::Item(StatsView &view, const QString &name, int from, std::vecto
innerLabel = view.createChartItem<ChartTextItem>(ChartZValue::SeriesLabels, f, innerLabelText); innerLabel = view.createChartItem<ChartTextItem>(ChartZValue::SeriesLabels, f, innerLabelText);
outerLabel = view.createChartItem<ChartTextItem>(ChartZValue::SeriesLabels, f, name); outerLabel = view.createChartItem<ChartTextItem>(ChartZValue::SeriesLabels, f, name);
outerLabel->setColor(darkLabelColor); outerLabel->setColor(theme.darkLabelColor);
} }
void PieSeries::Item::updatePositions(const QPointF &center, double radius) void PieSeries::Item::updatePositions(const QPointF &center, double radius)
@ -72,13 +72,13 @@ void PieSeries::Item::updatePositions(const QPointF &center, double radius)
} }
} }
void PieSeries::Item::highlight(ChartPieItem &item, int bin_nr, bool highlight, int numBins) void PieSeries::Item::highlight(ChartPieItem &item, int bin_nr, bool highlight, int numBins, const StatsTheme &theme)
{ {
QColor fill = highlight ? highlightedColor : binColor(bin_nr, numBins); QColor fill = highlight ? theme.highlightedColor : theme.binColor(bin_nr, numBins);
QColor border = highlight ? highlightedBorderColor : ::borderColor; QColor border = highlight ? theme.highlightedBorderColor : theme.borderColor;
if (innerLabel) if (innerLabel)
innerLabel->setColor(highlight ? darkLabelColor : labelColor(bin_nr, numBins), fill); innerLabel->setColor(highlight ? theme.darkLabelColor : theme.labelColor(bin_nr, numBins), fill);
item.drawSegment(angleFrom, angleTo, fill, border, selected); item.drawSegment(angleFrom, angleTo, fill, border, selected, theme);
} }
PieSeries::PieSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis, const QString &categoryName, PieSeries::PieSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis, const QString &categoryName,
@ -147,7 +147,7 @@ PieSeries::PieSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis, const
for (auto it2 = sorted.begin(); it2 != it; ++it2) { for (auto it2 = sorted.begin(); it2 != it; ++it2) {
int count = (int)data[*it2].second.size(); int count = (int)data[*it2].second.size();
items.emplace_back(view, data[*it2].first, act, std::move(data[*it2].second), items.emplace_back(view, data[*it2].first, act, std::move(data[*it2].second),
totalCount, (int)items.size(), numBins); totalCount, (int)items.size(), numBins, theme);
act += count; act += count;
} }
@ -162,7 +162,7 @@ PieSeries::PieSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis, const
otherDives.push_back(d); otherDives.push_back(d);
} }
QString name = StatsTranslations::tr("other (%1 items)").arg(other.size()); QString name = StatsTranslations::tr("other (%1 items)").arg(other.size());
items.emplace_back(view, name, act, std::move(otherDives), totalCount, (int)items.size(), numBins); items.emplace_back(view, name, act, std::move(otherDives), totalCount, (int)items.size(), numBins, theme);
} }
} }
@ -181,7 +181,7 @@ void PieSeries::updatePositions()
int i = 0; int i = 0;
for (Item &segment: items) { for (Item &segment: items) {
segment.updatePositions(center, radius); segment.updatePositions(center, radius);
segment.highlight(*item, i, i == highlighted, (int)items.size()); // Draw segment segment.highlight(*item, i, i == highlighted, (int)items.size(), theme); // Draw segment
++i; ++i;
} }
} }
@ -255,7 +255,7 @@ bool PieSeries::hover(QPointF pos)
// Highlight new item (if any) // Highlight new item (if any)
if (highlighted >= 0 && highlighted < (int)items.size()) { if (highlighted >= 0 && highlighted < (int)items.size()) {
items[highlighted].highlight(*item, highlighted, true, (int)items.size()); items[highlighted].highlight(*item, highlighted, true, (int)items.size(), theme);
if (!information) if (!information)
information = view.createChartItem<InformationBox>(); information = view.createChartItem<InformationBox>();
information->setText(makeInfo(highlighted), pos); information->setText(makeInfo(highlighted), pos);
@ -269,7 +269,7 @@ bool PieSeries::hover(QPointF pos)
void PieSeries::unhighlight() void PieSeries::unhighlight()
{ {
if (highlighted >= 0 && highlighted < (int)items.size()) if (highlighted >= 0 && highlighted < (int)items.size())
items[highlighted].highlight(*item, highlighted, false, (int)items.size()); items[highlighted].highlight(*item, highlighted, false, (int)items.size(), theme);
highlighted = -1; highlighted = -1;
} }
@ -314,7 +314,7 @@ void PieSeries::divesSelected(const QVector<dive *> &)
segment.selected = selected; segment.selected = selected;
int idx = &segment - &items[0]; int idx = &segment - &items[0];
segment.highlight(*item, idx, idx == highlighted, (int)items.size()); segment.highlight(*item, idx, idx == highlighted, (int)items.size(), theme);
} }
} }
} }

View file

@ -49,9 +49,9 @@ private:
QPointF innerLabelPos, outerLabelPos; // With respect to a (-1, -1)-(1, 1) rectangle. QPointF innerLabelPos, outerLabelPos; // With respect to a (-1, -1)-(1, 1) rectangle.
bool selected; bool selected;
Item(StatsView &view, const QString &name, int from, std::vector<dive *> dives, int totalCount, Item(StatsView &view, const QString &name, int from, std::vector<dive *> dives, int totalCount,
int bin_nr, int numBins); int bin_nr, int numBins, const StatsTheme &theme);
void updatePositions(const QPointF &center, double radius); void updatePositions(const QPointF &center, double radius);
void highlight(ChartPieItem &item, int bin_nr, bool highlight, int numBins); void highlight(ChartPieItem &item, int bin_nr, bool highlight, int numBins, const StatsTheme &theme);
}; };
std::vector<Item> items; std::vector<Item> items;
int totalCount; int totalCount;

View file

@ -1,13 +1,14 @@
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
#include "quartilemarker.h" #include "quartilemarker.h"
#include "statscolors.h"
#include "statsaxis.h" #include "statsaxis.h"
#include "statscolors.h"
#include "statsview.h"
#include "zvalues.h" #include "zvalues.h"
static const double quartileMarkerSize = 15.0; static const double quartileMarkerSize = 15.0;
QuartileMarker::QuartileMarker(StatsView &view, double pos, double value, StatsAxis *xAxis, StatsAxis *yAxis) : QuartileMarker::QuartileMarker(StatsView &view, double pos, double value, StatsAxis *xAxis, StatsAxis *yAxis) :
ChartLineItem(view, ChartZValue::ChartFeatures, quartileMarkerColor, 2.0), ChartLineItem(view, ChartZValue::ChartFeatures, view.getCurrentTheme().quartileMarkerColor, 2.0),
xAxis(xAxis), yAxis(yAxis), xAxis(xAxis), yAxis(yAxis),
pos(pos), pos(pos),
value(value) value(value)

View file

@ -2,6 +2,7 @@
#include "regressionitem.h" #include "regressionitem.h"
#include "statsaxis.h" #include "statsaxis.h"
#include "statscolors.h" #include "statscolors.h"
#include "statsview.h"
#include "zvalues.h" #include "zvalues.h"
#include <cmath> #include <cmath>
@ -11,6 +12,7 @@ static const double regressionLineWidth = 2.0;
RegressionItem::RegressionItem(StatsView &view, regression_data reg, RegressionItem::RegressionItem(StatsView &view, regression_data reg,
StatsAxis *xAxis, StatsAxis *yAxis) : StatsAxis *xAxis, StatsAxis *yAxis) :
ChartPixmapItem(view, ChartZValue::ChartFeatures), ChartPixmapItem(view, ChartZValue::ChartFeatures),
theme(view.getCurrentTheme()),
xAxis(xAxis), yAxis(yAxis), reg(reg), xAxis(xAxis), yAxis(yAxis), reg(reg),
regression(true), confidence(true) regression(true), confidence(true)
{ {
@ -86,7 +88,7 @@ void RegressionItem::updatePosition()
img->fill(Qt::transparent); img->fill(Qt::transparent);
if (confidence) { if (confidence) {
QColor col(regressionItemColor); QColor col(theme.regressionItemColor);
col.setAlphaF((float)reg.r2); col.setAlphaF((float)reg.r2);
painter->setPen(Qt::NoPen); painter->setPen(Qt::NoPen);
painter->setBrush(QBrush(col)); painter->setBrush(QBrush(col));
@ -94,7 +96,7 @@ void RegressionItem::updatePosition()
} }
if (regression) { if (regression) {
painter->setPen(QPen(regressionItemColor, regressionLineWidth)); painter->setPen(QPen(theme.regressionItemColor, regressionLineWidth));
painter->drawLine(QPointF(linePolygon[0]), QPointF(linePolygon[1])); painter->drawLine(QPointF(linePolygon[0]), QPointF(linePolygon[1]));
} }

View file

@ -5,6 +5,7 @@
#include "chartitem.h" #include "chartitem.h"
class StatsAxis; class StatsAxis;
class StatsTheme;
class StatsView; class StatsView;
struct regression_data { struct regression_data {
@ -20,6 +21,7 @@ public:
void updatePosition(); void updatePosition();
void setFeatures(bool regression, bool confidence); void setFeatures(bool regression, bool confidence);
private: private:
const StatsTheme &theme; // Initialized once in constructor
StatsAxis *xAxis, *yAxis; StatsAxis *xAxis, *yAxis;
regression_data reg; regression_data reg;
bool regression, confidence; bool regression, confidence;

View file

@ -26,7 +26,8 @@ static const double axisTitleSpaceVertical = 2.0; // Space between labels and ti
StatsAxis::StatsAxis(StatsView &view, const QString &title, bool horizontal, bool labelsBetweenTicks) : StatsAxis::StatsAxis(StatsView &view, const QString &title, bool horizontal, bool labelsBetweenTicks) :
ChartPixmapItem(view, ChartZValue::Axes), ChartPixmapItem(view, ChartZValue::Axes),
line(view.createChartItem<ChartLineItem>(ChartZValue::Axes, axisColor, axisWidth)), theme(view.getCurrentTheme()),
line(view.createChartItem<ChartLineItem>(ChartZValue::Axes, theme.axisColor, axisWidth)),
title(title), horizontal(horizontal), labelsBetweenTicks(labelsBetweenTicks), title(title), horizontal(horizontal), labelsBetweenTicks(labelsBetweenTicks),
size(1.0), zeroOnScreen(0.0), min(0.0), max(1.0), labelWidth(0.0) size(1.0), zeroOnScreen(0.0), min(0.0), max(1.0), labelWidth(0.0)
{ {
@ -128,7 +129,7 @@ void StatsAxis::addLabel(const QFontMetrics &fm, const QString &label, double po
void StatsAxis::addTick(double pos) void StatsAxis::addTick(double pos)
{ {
ticks.push_back({ view.createChartItem<ChartLineItem>(ChartZValue::Axes, axisColor, axisTickWidth), pos }); ticks.push_back({ view.createChartItem<ChartLineItem>(ChartZValue::Axes, theme.axisColor, axisTickWidth), pos });
} }
std::vector<double> StatsAxis::ticksPositions() const std::vector<double> StatsAxis::ticksPositions() const
@ -190,7 +191,7 @@ void StatsAxis::setSize(double sizeIn)
offset = QPointF(round(offsetX), round(offsetY)); offset = QPointF(round(offsetX), round(offsetY));
img->fill(Qt::transparent); img->fill(Qt::transparent);
painter->setPen(QPen(darkLabelColor)); painter->setPen(QPen(theme.darkLabelColor));
painter->setFont(labelFont); painter->setFont(labelFont);
for (const Label &label: labels) { for (const Label &label: labels) {
double x = (label.pos - min) / (max - min) * size + offset.x() - round(label.width / 2.0); double x = (label.pos - min) / (max - min) * size + offset.x() - round(label.width / 2.0);
@ -219,7 +220,7 @@ void StatsAxis::setSize(double sizeIn)
offset = QPointF(round(offsetX), round(offsetY)); offset = QPointF(round(offsetX), round(offsetY));
img->fill(Qt::transparent); img->fill(Qt::transparent);
painter->setPen(QPen(darkLabelColor)); painter->setPen(QPen(theme.darkLabelColor));
painter->setFont(labelFont); painter->setFont(labelFont);
for (const Label &label: labels) { for (const Label &label: labels) {
double y = (min - label.pos) / (max - min) * size + offset.y() - round(fontHeight / 2.0); double y = (min - label.pos) / (max - min) * size + offset.y() - round(fontHeight / 2.0);

View file

@ -37,6 +37,7 @@ public:
protected: protected:
StatsAxis(StatsView &view, const QString &title, bool horizontal, bool labelsBetweenTicks); StatsAxis(StatsView &view, const QString &title, bool horizontal, bool labelsBetweenTicks);
const StatsTheme &theme; // Initialized once in constructor.
ChartItemPtr<ChartLineItem> line; ChartItemPtr<ChartLineItem> line;
QString title; QString title;
double titleWidth; double titleWidth;

View file

@ -1,4 +1,5 @@
#include "statscolors.h" #include "statscolors.h"
#include "statstranslations.h"
// Colors created using the Chroma.js Color Palette Helper // Colors created using the Chroma.js Color Palette Helper
// https://vis4.net/palettes/#/50|d|00108c,3ed8ff,ffffe0|ffffe0,ff005e,743535|1|1 // https://vis4.net/palettes/#/50|d|00108c,3ed8ff,ffffe0|ffffe0,ff005e,743535|1|1
@ -15,22 +16,65 @@ static const QColor binColors[] = {
QRgb(0xa83e49), QRgb(0x9d3a44), QRgb(0x90383f), QRgb(0x83363a), QRgb(0x743535) QRgb(0xa83e49), QRgb(0x9d3a44), QRgb(0x90383f), QRgb(0x83363a), QRgb(0x743535)
}; };
// Pick roughly equidistant colors out of the color set above StatsTheme::StatsTheme() :
// if we need more bins than we have colors (what chart is THAT?) simply loop scatterItemTexture(nullptr),
QColor binColor(int bin, int numBins) scatterItemSelectedTexture(nullptr),
scatterItemHighlightedTexture(nullptr),
selectedTexture(nullptr)
{ {
if (numBins == 1 || bin < 0 || bin >= numBins)
return fillColor;
if (numBins > (int)std::size(binColors))
return binColors[bin % std::size(binColors)];
// use integer math to spread out the indices
int idx = bin * (std::size(binColors) - 1) / (numBins - 1);
return binColors[idx];
} }
// Figure out if we want a light or a dark label class StatsThemeLight : public StatsTheme {
QColor labelColor(int bin, size_t numBins) public:
{ StatsThemeLight()
return (binColor(bin, numBins).lightness() < 150) ? lightLabelColor : darkLabelColor; {
} backgroundColor = Qt::white;
fillColor = QColor(0x44, 0x76, 0xaa);
borderColor = QColor(0x66, 0xb2, 0xff);
selectedColor = QColor(0xaa, 0x76, 0x44);
selectedBorderColor = QColor(0xff, 0xb2, 0x66);
highlightedColor = Qt::yellow;
highlightedBorderColor = QColor(0xaa, 0xaa, 0x22);
darkLabelColor = Qt::black;
lightLabelColor = Qt::white;
axisColor = Qt::black;
gridColor = QColor(0xcc, 0xcc, 0xcc);
informationBorderColor = Qt::black;
informationColor = QColor(0xff, 0xff, 0x00, 192); // Note: fourth argument is opacity
legendColor = QColor(0x00, 0x8e, 0xcc, 192); // Note: fourth argument is opacity
legendBorderColor = Qt::black;
quartileMarkerColor = Qt::red;
regressionItemColor = Qt::red;
meanMarkerColor = Qt::green;
medianMarkerColor = Qt::red;
selectionLassoColor = Qt::black;
selectionOverlayColor = Qt::lightGray;
}
private:
QString name() const
{
return StatsTranslations::tr("Light");
}
// Pick roughly equidistant colors out of the color set above
// if we need more bins than we have colors (what chart is THAT?) simply loop
QColor binColor(int bin, int numBins) const override
{
if (numBins == 1 || bin < 0 || bin >= numBins)
return fillColor;
if (numBins > (int)std::size(binColors))
return binColors[bin % std::size(binColors)];
// use integer math to spread out the indices
int idx = bin * (std::size(binColors) - 1) / (numBins - 1);
return binColors[idx];
}
QColor labelColor(int bin, size_t numBins) const override
{
return (binColor(bin, numBins).lightness() < 150) ? lightLabelColor : darkLabelColor;
}
};
static StatsThemeLight statsThemeLight;
std::vector<const StatsTheme *> statsThemes = { &statsThemeLight };

View file

@ -3,31 +3,53 @@
#ifndef STATSCOLORS_H #ifndef STATSCOLORS_H
#define STATSCOLORS_H #define STATSCOLORS_H
#include <vector>
#include <QColor> #include <QColor>
#include <QString>
inline const QColor backgroundColor(Qt::white); class QSGTexture;
inline const QColor fillColor(0x44, 0x76, 0xaa);
inline const QColor borderColor(0x66, 0xb2, 0xff);
inline const QColor selectedColor(0xaa, 0x76, 0x44);
inline const QColor selectedBorderColor(0xff, 0xb2, 0x66);
inline const QColor highlightedColor(Qt::yellow);
inline const QColor highlightedBorderColor(0xaa, 0xaa, 0x22);
inline const QColor darkLabelColor(Qt::black);
inline const QColor lightLabelColor(Qt::white);
inline const QColor axisColor(Qt::black);
inline const QColor gridColor(0xcc, 0xcc, 0xcc);
inline const QColor informationBorderColor(Qt::black);
inline const QColor informationColor(0xff, 0xff, 0x00, 192); // Note: fourth argument is opacity
inline const QColor legendColor(0x00, 0x8e, 0xcc, 192); // Note: fourth argument is opacity
inline const QColor legendBorderColor(Qt::black);
inline const QColor quartileMarkerColor(Qt::red);
inline const QColor regressionItemColor(Qt::red);
inline const QColor meanMarkerColor(Qt::green);
inline const QColor medianMarkerColor(Qt::red);
inline const QColor selectionLassoColor(Qt::black);
inline const QColor selectionOverlayColor(Qt::lightGray);
QColor binColor(int bin, int numBins); class StatsTheme {
QColor labelColor(int bin, size_t numBins); public:
StatsTheme();
virtual QString name() const = 0;
QColor backgroundColor;
QColor fillColor;
QColor borderColor;
QColor selectedColor;
QColor selectedBorderColor;
QColor highlightedColor;
QColor highlightedBorderColor;
// The dark and light label colors are with respect to the light theme.
// In the dark theme, dark is light and light is dark. Might want to change the names!
QColor darkLabelColor;
QColor lightLabelColor;
QColor axisColor;
QColor gridColor;
QColor informationBorderColor;
QColor informationColor;
QColor legendColor;
QColor legendBorderColor;
QColor quartileMarkerColor;
QColor regressionItemColor;
QColor meanMarkerColor;
QColor medianMarkerColor;
QColor selectionLassoColor;
QColor selectionOverlayColor;
virtual QColor binColor(int bin, int numBins) const = 0;
virtual QColor labelColor(int bin, size_t numBins) const = 0;
// These are cashes for the QSG rendering engine
// Note: Originally these were std::unique_ptrs, which automatically
// freed the textures on exit. However, destroying textures after
// QApplication finished its thread leads to crashes. Therefore, these
// are now normal pointers and the texture objects are leaked.
mutable QSGTexture *scatterItemTexture = nullptr;
mutable QSGTexture *scatterItemSelectedTexture = nullptr;
mutable QSGTexture *scatterItemHighlightedTexture = nullptr;
mutable QSGTexture *selectedTexture = nullptr; // A checkerboard pattern.
};
extern std::vector<const StatsTheme *> statsThemes;
#endif #endif

View file

@ -9,7 +9,7 @@
static const double gridWidth = 1.0; static const double gridWidth = 1.0;
StatsGrid::StatsGrid(StatsView &view, const StatsAxis &xAxis, const StatsAxis &yAxis) StatsGrid::StatsGrid(StatsView &view, const StatsAxis &xAxis, const StatsAxis &yAxis)
: view(view), xAxis(xAxis), yAxis(yAxis) : view(view), theme(view.getCurrentTheme()), xAxis(xAxis), yAxis(yAxis)
{ {
} }
@ -28,11 +28,11 @@ void StatsGrid::updatePositions()
return; return;
for (double x: xtics) { for (double x: xtics) {
lines.push_back(view.createChartItem<ChartLineItem>(ChartZValue::Grid, gridColor, gridWidth)); lines.push_back(view.createChartItem<ChartLineItem>(ChartZValue::Grid, theme.gridColor, gridWidth));
lines.back()->setLine(QPointF(x, ytics.front()), QPointF(x, ytics.back())); lines.back()->setLine(QPointF(x, ytics.front()), QPointF(x, ytics.back()));
} }
for (double y: ytics) { for (double y: ytics) {
lines.push_back(view.createChartItem<ChartLineItem>(ChartZValue::Grid, gridColor, gridWidth)); lines.push_back(view.createChartItem<ChartLineItem>(ChartZValue::Grid, theme.gridColor, gridWidth));
lines.back()->setLine(QPointF(xtics.front(), y), QPointF(xtics.back(), y)); lines.back()->setLine(QPointF(xtics.front(), y), QPointF(xtics.back(), y));
} }
} }

View file

@ -7,6 +7,7 @@
#include <vector> #include <vector>
class StatsAxis; class StatsAxis;
class StatsTheme;
class StatsView; class StatsView;
class ChartLineItem; class ChartLineItem;
@ -16,6 +17,7 @@ public:
void updatePositions(); void updatePositions();
private: private:
StatsView &view; StatsView &view;
const StatsTheme &theme; // Initialized once in constructor.
const StatsAxis &xAxis, &yAxis; const StatsAxis &xAxis, &yAxis;
std::vector<ChartItemPtr<ChartLineItem>> lines; std::vector<ChartItemPtr<ChartLineItem>> lines;
}; };

View file

@ -1,9 +1,10 @@
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
#include "statsseries.h" #include "statsseries.h"
#include "statsaxis.h" #include "statsaxis.h"
#include "statsview.h"
StatsSeries::StatsSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis) : StatsSeries::StatsSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis) :
view(view), xAxis(xAxis), yAxis(yAxis) view(view), theme(view.getCurrentTheme()), xAxis(xAxis), yAxis(yAxis)
{ {
} }

View file

@ -10,6 +10,7 @@
#include <QPointF> #include <QPointF>
class StatsAxis; class StatsAxis;
class StatsTheme;
class StatsView; class StatsView;
struct dive; struct dive;
class QRectF; class QRectF;
@ -30,6 +31,7 @@ public:
protected: protected:
StatsView &view; StatsView &view;
const StatsTheme &theme; // Theme is only set once in the constructor
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); QPointF toScreen(QPointF p);
}; };

View file

@ -35,6 +35,7 @@ static const double selectionLassoWidth = 2.0; // Border between title and char
StatsView::StatsView(QQuickItem *parent) : QQuickItem(parent), StatsView::StatsView(QQuickItem *parent) : QQuickItem(parent),
backgroundDirty(true), backgroundDirty(true),
currentTheme(statsThemes[0]),
highlightedSeries(nullptr), highlightedSeries(nullptr),
xAxis(nullptr), xAxis(nullptr),
yAxis(nullptr), yAxis(nullptr),
@ -99,7 +100,7 @@ void StatsView::mousePressEvent(QMouseEvent *event)
if (selectionRect) if (selectionRect)
deleteChartItem(selectionRect); // Ooops. Already a selection in place. deleteChartItem(selectionRect); // Ooops. Already a selection in place.
dragStartMouse = pos; dragStartMouse = pos;
selectionRect = createChartItem<ChartRectLineItem>(ChartZValue::Selection, selectionLassoColor, selectionLassoWidth); selectionRect = createChartItem<ChartRectLineItem>(ChartZValue::Selection, currentTheme->selectionLassoColor, selectionLassoWidth);
selectionModifier = modifier; selectionModifier = modifier;
oldSelection = modifier.ctrl ? getDiveSelection() : std::vector<dive *>(); oldSelection = modifier.ctrl ? getDiveSelection() : std::vector<dive *>();
grabMouse(); grabMouse();
@ -143,7 +144,7 @@ RootNode::RootNode(StatsView &view) : view(view)
// also be done on the widget level, but would have to be done // also be done on the widget level, but would have to be done
// separately for desktop and mobile, so do it here. // separately for desktop and mobile, so do it here.
backgroundNode.reset(view.w()->createRectangleNode()); backgroundNode.reset(view.w()->createRectangleNode());
backgroundNode->setColor(backgroundColor); backgroundNode->setColor(view.getCurrentTheme().backgroundColor);
appendChildNode(backgroundNode.get()); appendChildNode(backgroundNode.get());
for (auto &zNode: zNodes) { for (auto &zNode: zNodes) {
@ -184,7 +185,7 @@ QSGNode *StatsView::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNod
} }
for (ChartItem *item = dirtyItems.first; item; item = item->next) { for (ChartItem *item = dirtyItems.first; item; item = item->next) {
item->render(); item->render(*currentTheme);
item->dirty = false; item->dirty = false;
} }
dirtyItems.splice(cleanItems); dirtyItems.splice(cleanItems);
@ -299,6 +300,18 @@ QQuickWindow *StatsView::w() const
return window(); return window();
} }
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;
}
QSizeF StatsView::size() const QSizeF StatsView::size() const
{ {
return boundingRect().size(); return boundingRect().size();
@ -459,7 +472,7 @@ void StatsView::setTitle(const QString &s)
return; return;
} }
title = createChartItem<ChartTextItem>(ChartZValue::Legend, titleFont, s); title = createChartItem<ChartTextItem>(ChartZValue::Legend, titleFont, s);
title->setColor(darkLabelColor); title->setColor(currentTheme->darkLabelColor);
} }
void StatsView::updateTitlePos() void StatsView::updateTitlePos()
@ -1128,10 +1141,10 @@ void StatsView::plotHistogramCountChart(const std::vector<dive *> &dives,
if (categoryVariable->type() == StatsVariable::Type::Numeric) { if (categoryVariable->type() == StatsVariable::Type::Numeric) {
double mean = categoryVariable->mean(dives); double mean = categoryVariable->mean(dives);
if (!std::isnan(mean)) if (!std::isnan(mean))
meanMarker = createChartItem<HistogramMarker>(mean, isHorizontal, meanMarkerColor, xAxis, yAxis); meanMarker = createChartItem<HistogramMarker>(mean, isHorizontal, currentTheme->meanMarkerColor, xAxis, yAxis);
double median = categoryVariable->quartiles(dives).q2; double median = categoryVariable->quartiles(dives).q2;
if (!std::isnan(median)) if (!std::isnan(median))
medianMarker = createChartItem<HistogramMarker>(median, isHorizontal, medianMarkerColor, xAxis, yAxis); medianMarker = createChartItem<HistogramMarker>(median, isHorizontal, currentTheme->medianMarkerColor, xAxis, yAxis);
} }
} }

View file

@ -29,6 +29,7 @@ class QuartileMarker;
class RegressionItem; class RegressionItem;
class StatsAxis; class StatsAxis;
class StatsGrid; class StatsGrid;
class StatsTheme;
class Legend; class Legend;
class QSGTexture; class QSGTexture;
class RootNode; // Internal implementation detail class RootNode; // Internal implementation detail
@ -53,6 +54,8 @@ public:
QQuickWindow *w() const; // Make window available to items QQuickWindow *w() const; // Make window available to items
QSizeF size() const; QSizeF size() const;
QRectF plotArea() const; QRectF plotArea() const;
void setTheme(int idx); // Invalid indexes will result in the default theme. Chart must be replot for theme to become effective.
const StatsTheme &getCurrentTheme() const;
void addQSGNode(QSGNode *node, ChartZValue z); // Must only be called in render thread! void addQSGNode(QSGNode *node, ChartZValue z); // Must only be called in render thread!
void registerChartItem(ChartItem &item); void registerChartItem(ChartItem &item);
void registerDirtyChartItem(ChartItem &item); void registerDirtyChartItem(ChartItem &item);
@ -140,6 +143,7 @@ private:
void addLineMarker(double pos, double low, double high, const QPen &pen, bool isHorizontal); void addLineMarker(double pos, double low, double high, const QPen &pen, bool isHorizontal);
StatsState state; StatsState state;
const StatsTheme *currentTheme;
QFont titleFont; QFont titleFont;
std::vector<std::unique_ptr<StatsSeries>> series; std::vector<std::unique_ptr<StatsSeries>> series;
std::unique_ptr<StatsGrid> grid; std::unique_ptr<StatsGrid> grid;