subsurface/stats/chartitem.cpp
Berthold Stoeger 06a091643e statistics: highlight selected bar
When all items of a bar in a bar chart are selected, highlight them
by overlaying with a checkerboard pattern. A gray checkerboard seems to
work reasonably well, regardless of base color.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2021-02-13 13:02:54 -08:00

614 lines
17 KiB
C++

// SPDX-License-Identifier: GPL-2.0
#include "chartitem.h"
#include "statscolors.h"
#include "statsview.h"
#include <cmath>
#include <QQuickWindow>
#include <QSGFlatColorMaterial>
#include <QSGImageNode>
#include <QSGRectangleNode>
#include <QSGTexture>
#include <QSGTextureMaterial>
static int round_up(double f)
{
return static_cast<int>(ceil(f));
}
ChartItem::ChartItem(StatsView &v, ChartZValue z) :
dirty(false), prev(nullptr), next(nullptr),
zValue(z), view(v)
{
// Register before the derived constructors run, so that the
// derived classes can mark the item as dirty in the constructor.
v.registerChartItem(*this);
}
ChartItem::~ChartItem()
{
}
QSizeF ChartItem::sceneSize() const
{
return view.size();
}
void ChartItem::markDirty()
{
view.registerDirtyChartItem(*this);
}
ChartPixmapItem::ChartPixmapItem(StatsView &v, ChartZValue z) : HideableChartItem(v, z),
positionDirty(false), textureDirty(false)
{
}
ChartPixmapItem::~ChartPixmapItem()
{
painter.reset(); // Make sure to destroy painter before image that is painted on
}
void ChartPixmapItem::setTextureDirty()
{
textureDirty = true;
markDirty();
}
void ChartPixmapItem::setPositionDirty()
{
positionDirty = true;
markDirty();
}
void ChartPixmapItem::render()
{
if (!node) {
createNode(view.w()->createImageNode());
view.addQSGNode(node.get(), zValue);
}
updateVisible();
if (!img) {
resize(QSizeF(1,1));
img->fill(Qt::transparent);
}
if (textureDirty) {
texture.reset(view.w()->createTextureFromImage(*img, QQuickWindow::TextureHasAlphaChannel));
node->node->setTexture(texture.get());
textureDirty = false;
}
if (positionDirty) {
node->node->setRect(rect);
positionDirty = false;
}
}
void ChartPixmapItem::resize(QSizeF size)
{
painter.reset();
img.reset(new QImage(round_up(size.width()), round_up(size.height()), QImage::Format_ARGB32));
painter.reset(new QPainter(img.get()));
painter->setRenderHint(QPainter::Antialiasing);
rect.setSize(size);
setTextureDirty();
}
void ChartPixmapItem::setPos(QPointF pos)
{
rect.moveTopLeft(pos);
setPositionDirty();
}
QRectF ChartPixmapItem::getRect() const
{
return rect;
}
static const int scatterItemDiameter = 10;
static const int scatterItemBorder = 1;
ChartScatterItem::ChartScatterItem(StatsView &v, ChartZValue z) : HideableChartItem(v, z),
positionDirty(false), textureDirty(false), highlight(Highlight::Unselected)
{
rect.setSize(QSizeF(static_cast<double>(scatterItemDiameter), static_cast<double>(scatterItemDiameter)));
}
ChartScatterItem::~ChartScatterItem()
{
}
static QSGTexture *createScatterTexture(StatsView &view, const QColor &color, const QColor &borderColor)
{
QImage img(scatterItemDiameter, scatterItemDiameter, QImage::Format_ARGB32);
img.fill(Qt::transparent);
QPainter painter(&img);
painter.setPen(Qt::NoPen);
painter.setRenderHint(QPainter::Antialiasing);
painter.setBrush(borderColor);
painter.drawEllipse(0, 0, scatterItemDiameter, scatterItemDiameter);
painter.setBrush(color);
painter.drawEllipse(scatterItemBorder, scatterItemBorder,
scatterItemDiameter - 2 * scatterItemBorder,
scatterItemDiameter - 2 * scatterItemBorder);
return view.w()->createTextureFromImage(img, QQuickWindow::TextureHasAlphaChannel);
}
// 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.
static QSGTexture *scatterItemTexture = nullptr;
static QSGTexture *scatterItemSelectedTexture = nullptr;
static QSGTexture *scatterItemHighlightedTexture = nullptr;
static QSGTexture *selectedTexture = nullptr; // A checkerboard pattern.
QSGTexture *ChartScatterItem::getTexture() const
{
switch (highlight) {
default:
case Highlight::Unselected:
return scatterItemTexture;
case Highlight::Selected:
return scatterItemSelectedTexture;
case Highlight::Highlighted:
return scatterItemHighlightedTexture;
}
}
void ChartScatterItem::render()
{
if (!scatterItemTexture) {
scatterItemTexture = createScatterTexture(view, fillColor, borderColor);
scatterItemSelectedTexture = createScatterTexture(view, selectedColor, selectedBorderColor);
scatterItemHighlightedTexture = createScatterTexture(view, highlightedColor, highlightedBorderColor);
}
if (!node) {
createNode(view.w()->createImageNode());
view.addQSGNode(node.get(), zValue);
textureDirty = positionDirty = true;
}
updateVisible();
if (textureDirty) {
node->node->setTexture(getTexture());
textureDirty = false;
}
if (positionDirty) {
node->node->setRect(rect);
positionDirty = false;
}
}
void ChartScatterItem::setPos(QPointF pos)
{
pos -= QPointF(scatterItemDiameter / 2.0, scatterItemDiameter / 2.0);
rect.moveTopLeft(pos);
positionDirty = true;
markDirty();
}
static double squareDist(const QPointF &p1, const QPointF &p2)
{
QPointF diff = p1 - p2;
return QPointF::dotProduct(diff, diff);
}
bool ChartScatterItem::contains(QPointF point) const
{
return squareDist(point, rect.center()) <= (scatterItemDiameter / 2.0) * (scatterItemDiameter / 2.0);
}
// For rectangular selections, we are more crude: simply check whether the center is in the selection.
bool ChartScatterItem::inRect(const QRectF &selection) const
{
return selection.contains(rect.center());
}
void ChartScatterItem::setHighlight(Highlight highlightIn)
{
if (highlight == highlightIn)
return;
highlight = highlightIn;
textureDirty = true;
markDirty();
}
QRectF ChartScatterItem::getRect() const
{
return rect;
}
ChartRectItem::ChartRectItem(StatsView &v, ChartZValue z,
const QPen &pen, const QBrush &brush, double radius) : ChartPixmapItem(v, z),
pen(pen), brush(brush), radius(radius)
{
}
ChartRectItem::~ChartRectItem()
{
}
void ChartRectItem::resize(QSizeF size)
{
ChartPixmapItem::resize(size);
img->fill(Qt::transparent);
painter->setPen(pen);
painter->setBrush(brush);
QSize imgSize = img->size();
int width = pen.width();
QRect rect(width / 2, width / 2, imgSize.width() - width, imgSize.height() - width);
painter->drawRoundedRect(rect, radius, radius, Qt::AbsoluteSize);
}
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));
}
ChartTextItem::ChartTextItem(StatsView &v, ChartZValue z, const QFont &f, const QString &text) :
ChartTextItem(v, z, f, std::vector<QString>({ text }), true)
{
}
void ChartTextItem::setColor(const QColor &c)
{
setColor(c, Qt::transparent);
}
void ChartTextItem::setColor(const QColor &c, const QColor &background)
{
img->fill(background);
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();
}
ChartPieItem::ChartPieItem(StatsView &v, ChartZValue z, double borderWidth) : ChartPixmapItem(v, z),
borderWidth(borderWidth)
{
}
void ChartPieItem::drawSegment(double from, double to, QColor fill, QColor border)
{
painter->setPen(QPen(border, borderWidth));
painter->setBrush(QBrush(fill));
// 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
// is dervied from "from" and subtracted from the origin angle at 12:00.
int startAngle = 90 * 16 - static_cast<int>(round(to * 360.0 * 16.0));
int spanAngle = static_cast<int>(round((to - from) * 360.0 * 16.0));
QRectF drawRect(QPointF(0.0, 0.0), rect.size());
painter->drawPie(drawRect, startAngle, spanAngle);
setTextureDirty();
}
void ChartPieItem::resize(QSizeF size)
{
ChartPixmapItem::resize(size);
img->fill(Qt::transparent);
}
ChartLineItemBase::ChartLineItemBase(StatsView &v, ChartZValue z, QColor color, double width) : HideableChartItem(v, z),
color(color), width(width), positionDirty(false), materialDirty(false)
{
}
ChartLineItemBase::~ChartLineItemBase()
{
}
void ChartLineItemBase::setLine(QPointF fromIn, QPointF toIn)
{
from = fromIn;
to = toIn;
positionDirty = true;
markDirty();
}
// 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 setPoint(QSGGeometry::TexturedPoint2D &v, const QPointF &p, const QPointF &t)
{
v.set(static_cast<float>(p.x()), static_cast<float>(p.y()),
static_cast<float>(t.x()), static_cast<float>(t.y()));
}
void ChartLineItem::render()
{
if (!node) {
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2));
geometry->setDrawingMode(QSGGeometry::DrawLines);
material.reset(new QSGFlatColorMaterial);
createNode();
node->setGeometry(geometry.get());
node->setMaterial(material.get());
view.addQSGNode(node.get(), zValue);
positionDirty = materialDirty = true;
}
updateVisible();
if (positionDirty) {
// Attention: width is a geometry property and therefore handled by position dirty!
geometry->setLineWidth(static_cast<float>(width));
auto vertices = geometry->vertexDataAsPoint2D();
setPoint(vertices[0], from);
setPoint(vertices[1], to);
node->markDirty(QSGNode::DirtyGeometry);
}
if (materialDirty) {
material->setColor(color);
node->markDirty(QSGNode::DirtyMaterial);
}
positionDirty = materialDirty = false;
}
void ChartRectLineItem::render()
{
if (!node) {
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4));
geometry->setDrawingMode(QSGGeometry::DrawLineLoop);
material.reset(new QSGFlatColorMaterial);
createNode();
node->setGeometry(geometry.get());
node->setMaterial(material.get());
view.addQSGNode(node.get(), zValue);
positionDirty = materialDirty = true;
}
updateVisible();
if (positionDirty) {
// Attention: width is a geometry property and therefore handled by position dirty!
geometry->setLineWidth(static_cast<float>(width));
auto vertices = geometry->vertexDataAsPoint2D();
setPoint(vertices[0], from);
setPoint(vertices[1], QPointF(from.x(), to.y()));
setPoint(vertices[2], to);
setPoint(vertices[3], QPointF(to.x(), from.y()));
node->markDirty(QSGNode::DirtyGeometry);
}
if (materialDirty) {
material->setColor(color);
node->markDirty(QSGNode::DirtyMaterial);
}
positionDirty = materialDirty = false;
}
ChartBarItem::ChartBarItem(StatsView &v, ChartZValue z, double borderWidth, bool horizontal) : HideableChartItem(v, z),
borderWidth(borderWidth), selected(false), horizontal(horizontal),
positionDirty(false), colorDirty(false), selectedDirty(false)
{
}
ChartBarItem::~ChartBarItem()
{
}
QSGTexture *ChartBarItem::getSelectedTexture() const
{
if (!selectedTexture) {
QImage img(2, 2, QImage::Format_ARGB32);
img.fill(Qt::transparent);
img.setPixelColor(0, 0, selectionOverlayColor);
img.setPixelColor(1, 1, selectionOverlayColor);
selectedTexture = view.w()->createTextureFromImage(img, QQuickWindow::TextureHasAlphaChannel);
}
return selectedTexture;
}
void ChartBarItem::render()
{
if (!node) {
createNode(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->node->appendChildNode(borderNode.get());
view.addQSGNode(node.get(), zValue);
positionDirty = colorDirty = selectedDirty = true;
}
updateVisible();
if (colorDirty) {
node->node->setColor(color);
borderMaterial->setColor(borderColor);
node->node->markDirty(QSGNode::DirtyMaterial);
borderNode->markDirty(QSGNode::DirtyMaterial);
}
if (positionDirty) {
node->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());
}
node->node->markDirty(QSGNode::DirtyGeometry);
borderNode->markDirty(QSGNode::DirtyGeometry);
}
if (selectedDirty) {
if (selected) {
if (!selectionNode) {
// Create the selection overlay if it didn't exist up to now.
selectionGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4));
selectionGeometry->setDrawingMode(QSGGeometry::DrawTriangleFan);
selectionMaterial.reset(new QSGTextureMaterial);
selectionMaterial->setTexture(getSelectedTexture());
selectionMaterial->setHorizontalWrapMode(QSGTexture::Repeat);
selectionMaterial->setVerticalWrapMode(QSGTexture::Repeat);
selectionNode.reset(new QSGGeometryNode);
selectionNode->setGeometry(selectionGeometry.get());
selectionNode->setMaterial(selectionMaterial.get());
}
node->node->appendChildNode(selectionNode.get());
// Update the position of the selection overlay, even if the position didn't change.
positionDirty = true;
} else {
if (selectionNode)
node->node->removeChildNode(selectionNode.get());
}
}
if (selected && positionDirty) {
// The checkerboard texture is 2x2. By dividing the coordinates by 4, every square is 2x2 pixels on the screen.
auto selectionVertices = selectionGeometry->vertexDataAsTexturedPoint2D();
selectionNode->markDirty(QSGNode::DirtyGeometry);
setPoint(selectionVertices[0], rect.topLeft(), QPointF());
setPoint(selectionVertices[1], rect.topRight(), QPointF(rect.width() / 4.0, 0.0));
setPoint(selectionVertices[2], rect.bottomRight(), QPointF(rect.width() / 4.0, rect.height() / 4.0));
setPoint(selectionVertices[3], rect.bottomLeft(), QPointF(0.0, rect.height() / 4.0));
}
positionDirty = colorDirty = selectedDirty = false;
}
void ChartBarItem::setColor(QColor colorIn, QColor borderColorIn)
{
if (color == colorIn)
return;
color = colorIn;
borderColor = borderColorIn;
colorDirty = true;
markDirty();
}
void ChartBarItem::setRect(const QRectF &rectIn)
{
if (rect == rectIn)
return;
rect = rectIn;
positionDirty = true;
markDirty();
}
void ChartBarItem::setSelected(bool selectedIn)
{
if (selected == selectedIn)
return;
selected = selectedIn;
selectedDirty = true;
markDirty();
}
QRectF ChartBarItem::getRect() const
{
return rect;
}
ChartBoxItem::ChartBoxItem(StatsView &v, ChartZValue z, double borderWidth) :
ChartBarItem(v, z, borderWidth, false) // Only support for vertical boxes
{
}
ChartBoxItem::~ChartBoxItem()
{
}
void ChartBoxItem::render()
{
// Remember old dirty values, since ChartBarItem::render() will clear them
bool oldPositionDirty = positionDirty;
bool oldColorDirty = colorDirty;
ChartBarItem::render(); // This will create the base node, so no need to check for that.
if (!whiskersNode) {
whiskersGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 10));
whiskersGeometry->setDrawingMode(QSGGeometry::DrawLines);
whiskersGeometry->setLineWidth(static_cast<float>(borderWidth));
whiskersMaterial.reset(new QSGFlatColorMaterial);
whiskersNode.reset(new QSGGeometryNode);
whiskersNode->setGeometry(whiskersGeometry.get());
whiskersNode->setMaterial(whiskersMaterial.get());
node->node->appendChildNode(whiskersNode.get());
// If this is the first time, make sure to update the geometry.
oldPositionDirty = oldColorDirty = true;
}
if (oldColorDirty) {
whiskersMaterial->setColor(borderColor);
whiskersNode->markDirty(QSGNode::DirtyMaterial);
}
if (oldPositionDirty) {
auto vertices = whiskersGeometry->vertexDataAsPoint2D();
double left = rect.left();
double right = rect.right();
double mid = (left + right) / 2.0;
// top bar
setPoint(vertices[0], QPointF(left, max));
setPoint(vertices[1], QPointF(right, max));
// top whisker
setPoint(vertices[2], QPointF(mid, max));
setPoint(vertices[3], QPointF(mid, rect.top()));
// bottom bar
setPoint(vertices[4], QPointF(left, min));
setPoint(vertices[5], QPointF(right, min));
// bottom whisker
setPoint(vertices[6], QPointF(mid, min));
setPoint(vertices[7], QPointF(mid, rect.bottom()));
// median indicator
setPoint(vertices[8], QPointF(left, median));
setPoint(vertices[9], QPointF(right, median));
whiskersNode->markDirty(QSGNode::DirtyGeometry);
}
}
void ChartBoxItem::setBox(const QRectF &rect, double minIn, double maxIn, double medianIn)
{
min = minIn;
max = maxIn;
median = medianIn;
setRect(rect);
}
QRectF ChartBoxItem::getRect() const
{
QRectF res = rect;
res.setTop(min);
res.setBottom(max);
return rect;
}