diff options
author | Berthold Stoeger <bstoeger@mail.tuwien.ac.at> | 2021-01-15 22:48:32 +0100 |
---|---|---|
committer | bstoeger <32835590+bstoeger@users.noreply.github.com> | 2021-01-20 08:47:18 +0100 |
commit | b42e19e36b4d80716059011581851c8600338cbb (patch) | |
tree | e752618c95a6ab4dcb980209800d3c3bbf292a0e | |
parent | 20088576604a3159cc9891bcfea888c15b096a96 (diff) | |
download | subsurface-b42e19e36b4d80716059011581851c8600338cbb.tar.gz |
statistics: convert bar series to QSGNodes
To this end, two new ChartItems were added: A "bar" (a rectangle
with a border) and a "text" (multiple lines of text).
It turns out that the text on the bars now looks atrocious.
The reason appears to be that the antialiasing of the font-rendering
does not blend into the alpha channel, but into a supposed
background color? This will have to be investigated.
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
-rw-r--r-- | stats/barseries.cpp | 96 | ||||
-rw-r--r-- | stats/barseries.h | 14 | ||||
-rw-r--r-- | stats/chartitem.cpp | 119 | ||||
-rw-r--r-- | stats/chartitem.h | 41 |
4 files changed, 200 insertions, 70 deletions
diff --git a/stats/barseries.cpp b/stats/barseries.cpp index 733549d3d..d7211212e 100644 --- a/stats/barseries.cpp +++ b/stats/barseries.cpp @@ -13,6 +13,7 @@ // Constants that control the bar layout static const double barWidth = 0.8; // 1.0 = full width of category static const double subBarWidth = 0.9; // For grouped bar charts +static const double barBorderWidth = 1.0; // Default constructor: invalid index. BarSeries::Index::Index() : bar(-1), subitem(-1) @@ -86,97 +87,76 @@ BarSeries::~BarSeries() { } -BarSeries::BarLabel::BarLabel(QGraphicsScene *scene, const std::vector<QString> &labels, int bin_nr, int binCount) : - totalWidth(0.0), totalHeight(0.0), isOutside(false) +BarSeries::BarLabel::BarLabel(StatsView &view, const std::vector<QString> &labels, int bin_nr, int binCount) : + isOutside(false) { - items.reserve(labels.size()); - for (const QString &label: labels) { - items.emplace_back(createItem<QGraphicsSimpleTextItem>(scene)); - items.back()->setText(label); - items.back()->setZValue(ZValues::seriesLabels); - QRectF rect = items.back()->boundingRect(); - if (rect.width() > totalWidth) - totalWidth = rect.width(); - totalHeight += rect.height(); - } + QFont f; // make configurable + item = view.createChartItem<ChartTextItem>(ChartZValue::SeriesLabels, f, labels, true); highlight(false, bin_nr, binCount); } void BarSeries::BarLabel::setVisible(bool visible) { - for (auto &item: items) - item->setVisible(visible); + // item->setVisible(visible); TODO! } void BarSeries::BarLabel::highlight(bool highlight, int bin_nr, int binCount) { - QBrush brush(highlight || isOutside ? darkLabelColor : labelColor(bin_nr, binCount)); - for (auto &item: items) - item->setBrush(brush); + item->setColor(highlight || isOutside ? darkLabelColor : labelColor(bin_nr, binCount)); } void BarSeries::BarLabel::updatePosition(bool horizontal, bool center, const QRectF &rect, int bin_nr, int binCount) { + QSizeF itemSize = item->getRect().size(); if (!horizontal) { - if (totalWidth > rect.width()) { + if (itemSize.width() > rect.width()) { setVisible(false); return; } QPointF pos = rect.center(); + pos.rx() -= round(itemSize.width() / 2.0); // Heuristics: if the label fits nicely into the bar (bar height is at least twice the label height), // then put the label in the middle of the bar. Otherwise, put it at the top of the bar. - isOutside = !center && rect.height() < 2.0 * totalHeight; + isOutside = !center && rect.height() < 2.0 * itemSize.height(); if (isOutside) { - pos.ry() = rect.top() - (totalHeight + 2.0); // Leave two pixels(?) space + pos.ry() = rect.top() - (itemSize.height() + 2.0); // Leave two pixels(?) space } else { - if (totalHeight > rect.height()) { + if (itemSize.height() > rect.height()) { setVisible(false); return; } - pos.ry() -= totalHeight / 2.0; - } - for (auto &it: items) { - QPointF itemPos = pos; - QRectF rect = it->boundingRect(); - itemPos.rx() -= rect.width() / 2.0; - it->setPos(itemPos); - pos.ry() += rect.height(); + pos.ry() -= round(itemSize.height() / 2.0); } + item->setPos(pos); } else { - if (totalHeight > rect.height()) { + if (itemSize.height() > rect.height()) { setVisible(false); return; } QPointF pos = rect.center(); - pos.ry() -= totalHeight / 2.0; + pos.ry() -= round(itemSize.height() / 2.0); // Heuristics: if the label fits nicely into the bar (bar width is at least twice the label height), // then put the label in the middle of the bar. Otherwise, put it to the right of the bar. - isOutside = !center && rect.width() < 2.0 * totalWidth; + isOutside = !center && rect.width() < 2.0 * itemSize.width(); if (isOutside) { - pos.rx() = rect.right() + (totalWidth / 2.0 + 2.0); // Leave two pixels(?) space + pos.rx() = round(rect.right() + 2.0); // Leave two pixels(?) space } else { - if (totalWidth > rect.width()) { + if (itemSize.width() > rect.width()) { setVisible(false); return; } } - for (auto &it: items) { - QPointF itemPos = pos; - QRectF rect = it->boundingRect(); - itemPos.rx() -= rect.width() / 2.0; - it->setPos(itemPos); - pos.ry() += rect.height(); - } + item->setPos(pos); } setVisible(true); // If label changed from inside to outside, or vice-versa, the color might change. highlight(false, bin_nr, binCount); } -BarSeries::Item::Item(QGraphicsScene *scene, BarSeries *series, double lowerBound, double upperBound, +BarSeries::Item::Item(BarSeries *series, double lowerBound, double upperBound, std::vector<SubItem> subitemsIn, const QString &binName, const StatsOperationResults &res, int total, bool horizontal, bool stacked, int binCount) : @@ -187,10 +167,8 @@ BarSeries::Item::Item(QGraphicsScene *scene, BarSeries *series, double lowerBoun res(res), total(total) { - for (SubItem &item: subitems) { - item.item->setZValue(ZValues::series); + for (SubItem &item: subitems) item.highlight(false, binCount); - } updatePosition(series, horizontal, stacked, binCount); } @@ -203,13 +181,10 @@ void BarSeries::Item::highlight(int subitem, bool highlight, int binCount) void BarSeries::SubItem::highlight(bool highlight, int binCount) { - if (highlight) { - item->setBrush(QBrush(highlightedColor)); - item->setPen(QPen(highlightedBorderColor)); - } else { - item->setBrush(QBrush(binColor(bin_nr, binCount))); - item->setPen(QPen(::borderColor)); - } + if (highlight) + item->setColor(highlightedColor, highlightedBorderColor); + else + item->setColor(binColor(bin_nr, binCount), ::borderColor); if (label) label->highlight(highlight, bin_nr, binCount); } @@ -235,9 +210,9 @@ void BarSeries::Item::updatePosition(BarSeries *series, bool horizontal, bool st double center = (idx + 0.5) * fullSubWidth + from; item.updatePosition(series, horizontal, stacked, center - subWidth / 2.0, center + subWidth / 2.0, binCount); } - rect = subitems[0].item->rect(); + rect = subitems[0].item->getRect(); for (auto it = std::next(subitems.begin()); it != subitems.end(); ++it) - rect = rect.united(it->item->rect()); + rect = rect.united(it->item->getRect()); } void BarSeries::SubItem::updatePosition(BarSeries *series, bool horizontal, bool stacked, @@ -265,9 +240,10 @@ std::vector<BarSeries::SubItem> BarSeries::makeSubItems(const std::vector<std::p int bin_nr = 0; for (auto &[v, label]: values) { if (v > 0.0) { - res.push_back({ createItemPtr<QGraphicsRectItem>(scene), {}, from, from + v, bin_nr }); + res.push_back({ view.createChartItem<ChartBarItem>(ChartZValue::Series, barBorderWidth, horizontal), + {}, from, from + v, bin_nr }); if (!label.empty()) - res.back().label = std::make_unique<BarLabel>(scene, label, bin_nr, binCount()); + res.back().label = std::make_unique<BarLabel>(view, label, bin_nr, binCount()); } if (stacked) from += v; @@ -293,7 +269,7 @@ void BarSeries::add_item(double lowerBound, double upperBound, std::vector<SubIt // Don't add empty items, as that messes with the "find item under mouse" routine. if (subitems.empty()) return; - items.emplace_back(scene, this, lowerBound, upperBound, std::move(subitems), binName, res, + items.emplace_back(this, lowerBound, upperBound, std::move(subitems), binName, res, total, horizontal, stacked, binCount()); } @@ -323,10 +299,10 @@ int BarSeries::Item::getSubItemUnderMouse(const QPointF &point, bool horizontal, // Search the first item whose "end" position is greater than the cursor position. bool search_x = horizontal == stacked; auto it = search_x ? std::lower_bound(subitems.begin(), subitems.end(), point.x(), - [] (const SubItem &item, double x) { return item.item->rect().right() < x; }) + [] (const SubItem &item, double x) { return item.item->getRect().right() < x; }) : std::lower_bound(subitems.begin(), subitems.end(), point.y(), - [] (const SubItem &item, double y) { return item.item->rect().top() > y; }); - return it != subitems.end() && it->item->rect().contains(point) ? it - subitems.begin() : -1; + [] (const SubItem &item, double y) { return item.item->getRect().top() > y; }); + return it != subitems.end() && it->item->getRect().contains(point) ? it - subitems.begin() : -1; } // Format information in a count-based bar chart. diff --git a/stats/barseries.h b/stats/barseries.h index 33a36395e..5655bc977 100644 --- a/stats/barseries.h +++ b/stats/barseries.h @@ -10,9 +10,11 @@ #include <memory> #include <vector> -#include <QGraphicsRectItem> +#include <QRectF> class QGraphicsScene; +class ChartBarItem; +class ChartTextItem; struct InformationBox; struct StatsVariable; @@ -80,17 +82,16 @@ private: // A label that is composed of multiple lines struct BarLabel { - std::vector<std::unique_ptr<QGraphicsSimpleTextItem>> items; - double totalWidth, totalHeight; // Size of the item + std::unique_ptr<ChartTextItem> item; bool isOutside; // Is shown outside of bar - BarLabel(QGraphicsScene *scene, const std::vector<QString> &labels, int bin_nr, int binCount); + BarLabel(StatsView &view, const std::vector<QString> &labels, int bin_nr, int binCount); void setVisible(bool visible); void updatePosition(bool horizontal, bool center, const QRectF &rect, int bin_nr, int binCount); void highlight(bool highlight, int bin_nr, int binCount); }; struct SubItem { - std::unique_ptr<QGraphicsRectItem> item; + std::unique_ptr<ChartBarItem> item; std::unique_ptr<BarLabel> label; double value_from; double value_to; @@ -107,7 +108,7 @@ private: const QString binName; StatsOperationResults res; int total; - Item(QGraphicsScene *scene, BarSeries *series, double lowerBound, double upperBound, + Item(BarSeries *series, double lowerBound, double upperBound, std::vector<SubItem> subitems, const QString &binName, const StatsOperationResults &res, int total, bool horizontal, bool stacked, int binCount); @@ -118,7 +119,6 @@ private: std::unique_ptr<InformationBox> information; std::vector<Item> items; - std::vector<BarLabel> barLabels; bool horizontal; bool stacked; QString categoryName; diff --git a/stats/chartitem.cpp b/stats/chartitem.cpp index aeb395d41..bb01c6e2c 100644 --- a/stats/chartitem.cpp +++ b/stats/chartitem.cpp @@ -6,6 +6,7 @@ #include <QQuickWindow> #include <QSGFlatColorMaterial> #include <QSGImageNode> +#include <QSGRectangleNode> #include <QSGTexture> static int round_up(double f) @@ -116,6 +117,40 @@ void ChartRectItem::resize(QSizeF size) painter->drawRoundedRect(rect, radius, radius, Qt::AbsoluteSize); } +ChartTextItem::ChartTextItem(StatsView &v, ChartZValue z, const QFont &f, const std::vector<QString> &text, bool center) : + ChartPixmapItem(v, z), f(f), center(center) +{ + QFontMetrics fm(f); + double totalWidth = 1.0; + fontHeight = static_cast<double>(fm.height()); + double totalHeight = std::max(1.0, static_cast<double>(text.size()) * fontHeight); + + items.reserve(text.size()); + for (const QString &s: text) { + double w = fm.size(Qt::TextSingleLine, s).width(); + items.push_back({ s, w }); + if (w > totalWidth) + totalWidth = w; + } + resize(QSizeF(totalWidth, totalHeight)); +} + +void ChartTextItem::setColor(const QColor &c) +{ + img->fill(Qt::transparent); + double y = 0.0; + painter->setPen(QPen(c)); + painter->setFont(f); + double totalWidth = getRect().width(); + for (const auto &[s, w]: items) { + double x = center ? round((totalWidth - w) / 2.0) : 0.0; + QRectF rect(x, y, w, fontHeight); + painter->drawText(rect, s); + y += fontHeight; + } + setTextureDirty(); +} + ChartLineItem::ChartLineItem(StatsView &v, ChartZValue z, QColor color, double width) : ChartItem(v, z), color(color), width(width), positionDirty(false), materialDirty(false) { @@ -125,6 +160,12 @@ ChartLineItem::~ChartLineItem() { } +// Helper function to set points +void setPoint(QSGGeometry::Point2D &v, const QPointF &p) +{ + v.set(static_cast<float>(p.x()), static_cast<float>(p.y())); +} + void ChartLineItem::render() { if (!node) { @@ -142,8 +183,8 @@ void ChartLineItem::render() // Attention: width is a geometry property and therefore handled by position dirty! geometry->setLineWidth(static_cast<float>(width)); auto vertices = geometry->vertexDataAsPoint2D(); - vertices[0].set(static_cast<float>(from.x()), static_cast<float>(from.y())); - vertices[1].set(static_cast<float>(to.x()), static_cast<float>(to.y())); + setPoint(vertices[0], from); + setPoint(vertices[1], to); node->markDirty(QSGNode::DirtyGeometry); } @@ -162,3 +203,77 @@ void ChartLineItem::setLine(QPointF fromIn, QPointF toIn) positionDirty = true; view.registerDirtyChartItem(*this); } + +ChartBarItem::ChartBarItem(StatsView &v, ChartZValue z, double borderWidth, bool horizontal) : ChartItem(v, z), + borderWidth(borderWidth), horizontal(horizontal), + positionDirty(false), colorDirty(false) +{ +} + +ChartBarItem::~ChartBarItem() +{ +} + +void ChartBarItem::render() +{ + if (!node) { + node.reset(view.w()->createRectangleNode()); + + borderGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4)); + borderGeometry->setDrawingMode(QSGGeometry::DrawLineLoop); + borderGeometry->setLineWidth(static_cast<float>(borderWidth)); + borderMaterial.reset(new QSGFlatColorMaterial); + borderNode.reset(new QSGGeometryNode); + borderNode->setGeometry(borderGeometry.get()); + borderNode->setMaterial(borderMaterial.get()); + + node->appendChildNode(borderNode.get()); + view.addQSGNode(node.get(), zValue); + positionDirty = colorDirty = true; + } + + if (colorDirty) { + node->setColor(color); + borderMaterial->setColor(borderColor); + borderNode->markDirty(QSGNode::DirtyMaterial); + } + + if (positionDirty) { + node->setRect(rect); + auto vertices = borderGeometry->vertexDataAsPoint2D(); + if (horizontal) { + setPoint(vertices[0], rect.topLeft()); + setPoint(vertices[1], rect.topRight()); + setPoint(vertices[2], rect.bottomRight()); + setPoint(vertices[3], rect.bottomLeft()); + } else { + setPoint(vertices[0], rect.bottomLeft()); + setPoint(vertices[1], rect.topLeft()); + setPoint(vertices[2], rect.topRight()); + setPoint(vertices[3], rect.bottomRight()); + } + borderNode->markDirty(QSGNode::DirtyGeometry); + } + + positionDirty = colorDirty = false; +} + +void ChartBarItem::setColor(QColor colorIn, QColor borderColorIn) +{ + color = colorIn; + borderColor = borderColorIn; + colorDirty = true; + view.registerDirtyChartItem(*this); +} + +void ChartBarItem::setRect(const QRectF &rectIn) +{ + rect = rectIn; + positionDirty = true; + view.registerDirtyChartItem(*this); +} + +QRectF ChartBarItem::getRect() const +{ + return rect; +} diff --git a/stats/chartitem.h b/stats/chartitem.h index f13cb3bc4..93c80547f 100644 --- a/stats/chartitem.h +++ b/stats/chartitem.h @@ -11,6 +11,7 @@ class QSGGeometry; class QSGGeometryNode; class QSGFlatColorMaterial; class QSGImageNode; +class QSGRectangleNode; class QSGTexture; class StatsView; enum class ChartZValue : int; @@ -20,7 +21,6 @@ public: ChartItem(StatsView &v, ChartZValue z); virtual ~ChartItem(); virtual void render() = 0; // Only call on render thread! - QRectF getRect() const; bool dirty; // If true, call render() when rebuilding the scene ChartItem *dirtyPrev, *dirtyNext; // Double linked list of dirty items const ChartZValue zValue; @@ -64,6 +64,22 @@ private: double radius; }; +// Attention: text is only drawn after calling setColor()! +class ChartTextItem : public ChartPixmapItem { +public: + ChartTextItem(StatsView &v, ChartZValue z, const QFont &f, const std::vector<QString> &text, bool center); + void setColor(const QColor &color); +private: + QFont f; + double fontHeight; + bool center; + struct Item { + QString s; + double width; + }; + std::vector<Item> items; +}; + class ChartLineItem : public ChartItem { public: ChartLineItem(StatsView &v, ChartZValue z, QColor color, double width); @@ -74,6 +90,7 @@ private: QPointF from, to; QColor color; double width; + bool horizontal; bool positionDirty; bool materialDirty; std::unique_ptr<QSGGeometryNode> node; @@ -81,4 +98,26 @@ private: std::unique_ptr<QSGGeometry> geometry; }; +// A bar in a bar chart: a rectangle bordered by lines. +class ChartBarItem : public ChartItem { +public: + ChartBarItem(StatsView &v, ChartZValue z, double borderWidth, bool horizontal); + ~ChartBarItem(); + void setColor(QColor color, QColor borderColor); + void setRect(const QRectF &rect); + QRectF getRect() const; + void render() override; // Only call on render thread! +private: + QColor color, borderColor; + double borderWidth; + QRectF rect; + bool horizontal; + bool positionDirty; + bool colorDirty; + std::unique_ptr<QSGRectangleNode> node; + std::unique_ptr<QSGGeometryNode> borderNode; + std::unique_ptr<QSGFlatColorMaterial> borderMaterial; + std::unique_ptr<QSGGeometry> borderGeometry; +}; + #endif |