aboutsummaryrefslogtreecommitdiffstats
path: root/stats/legend.cpp
blob: d0a5c19b13f7a374115b13e2130902a5d8d77304 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// SPDX-License-Identifier: GPL-2.0
#include "legend.h"
#include "statscolors.h"
#include "zvalues.h"

#include <cmath>
#include <QFontMetrics>
#include <QPen>

static const double legendBorderSize = 2.0;
static const double legendBoxBorderSize = 1.0;
static const double legendBoxBorderRadius = 4.0;	// radius of rounded corners
static const double legendBoxScale = 0.8;		// 1.0: text-height of the used font
static const double legendInternalBorderSize = 2.0;
static const QColor legendColor(0x00, 0x8e, 0xcc, 192); // Note: fourth argument is opacity
static const QColor legendBorderColor(Qt::black);

Legend::Legend(StatsView &view, const std::vector<QString> &names) :
	ChartRectItem(view, ChartZValue::Legend,
		      QPen(legendBorderColor, legendBorderSize), QBrush(legendColor), legendBoxBorderRadius),
	displayedItems(0), width(0.0), height(0.0),
	font(QFont()),	// Make configurable
	posInitialized(false)
{
	entries.reserve(names.size());
	QFontMetrics fm(font);
	fontHeight = fm.height();
	int idx = 0;
	for (const QString &name: names)
		entries.emplace_back(name, idx++, (int)names.size(), fm);
}

Legend::Entry::Entry(const QString &name, int idx, int numBins, const QFontMetrics &fm) :
	name(name),
	rectBrush(QBrush(binColor(idx, numBins)))
{
	width = fm.height() + 2.0 * legendBoxBorderSize + fm.size(Qt::TextSingleLine, name).width();
}

void Legend::hide()
{
	ChartRectItem::resize(QSizeF(1,1));
	img->fill(Qt::transparent);
}

void Legend::resize()
{
	if (entries.empty())
		return hide();

	QSizeF size = sceneSize();

	// Silly heuristics: make the legend at most half as high and half as wide as the chart.
	// Not sure if that makes sense - this might need some optimization.
	int maxRows = static_cast<int>(size.height() / 2.0 - 2.0 * legendInternalBorderSize) / fontHeight;
	if (maxRows <= 0)
		return hide();
	int numColumns = ((int)entries.size() - 1) / maxRows + 1;
	int numRows = ((int)entries.size() - 1) / numColumns + 1;

	double x = legendInternalBorderSize;
	displayedItems = 0;
	for (int col = 0; col < numColumns; ++col) {
		double y = legendInternalBorderSize;
		double nextX = x;

		for (int row = 0; row < numRows; ++row) {
			int idx = col * numRows + row;
			if (idx >= (int)entries.size())
				break;
			entries[idx].pos = QPointF(x, y);
			nextX = std::max(nextX, x + entries[idx].width);
			y += fontHeight;
			++displayedItems;
		}
		x = nextX;
		width = nextX;
		if (width >= size.width() / 2.0) // More than half the chart-width -> give up
			break;
	}
	width += legendInternalBorderSize;
	height = 2 * legendInternalBorderSize + numRows * fontHeight;

	ChartRectItem::resize(QSizeF(width, height));

	// Paint rectangles
	painter->setPen(QPen(legendBorderColor, legendBoxBorderSize));
	for (int i = 0; i < displayedItems; ++i) {
		QPointF itemPos = entries[i].pos;
		painter->setBrush(entries[i].rectBrush);
		QRectF rect(itemPos, QSizeF(fontHeight, fontHeight));
		// Decrease box size by legendBoxScale factor
		double delta = fontHeight * (1.0 - legendBoxScale) / 2.0;
		rect = rect.adjusted(delta, delta, -delta, -delta);
		painter->drawRect(rect);
	}

	// Paint labels
	painter->setPen(darkLabelColor); // QPainter uses pen not brush for text!
	painter->setFont(font);
	for (int i = 0; i < displayedItems; ++i) {
		QPointF itemPos = entries[i].pos;
		itemPos.rx() += fontHeight + 2.0 * legendBoxBorderSize;
		QRectF rect(itemPos, QSizeF(entries[i].width, fontHeight));
		painter->drawText(rect, entries[i].name);
	}

	if (!posInitialized) {
		// At first, place in top right corner
		setPos(QPointF(size.width() - width - 10.0, 10.0));
		posInitialized = true;
	} else {
		// Try to keep relative position with what it was before
		setPos(QPointF(size.width() * centerPos.x() - width / 2.0,
			       size.height() * centerPos.y() - height / 2.0));
	}
}

void Legend::setPos(QPointF newPos)
{
	// Round the position to integers or horrible artifacts appear (at least on desktop)
	QPointF posInt(round(newPos.x()), round(newPos.y()));

	// Make sure that the center is inside the drawing area,
	// so that the user can't lose the legend.
	QSizeF size = sceneSize();
	if (size.width() < 1.0 || size.height() < 1.0)
		return;
	double widthHalf = floor(width / 2.0);
	double heightHalf = floor(height / 2.0);
	QPointF sanitizedPos(std::clamp(posInt.x(), -widthHalf, size.width() - widthHalf - 1.0),
			     std::clamp(posInt.y(), -heightHalf, size.height() - heightHalf - 1.0));

	// Set position
	ChartRectItem::setPos(sanitizedPos);

	// Remember relative position of center for next time
	QPointF centerPosAbsolute(sanitizedPos.x() + width / 2.0,
				  sanitizedPos.y() + height / 2.0);
	centerPos = QPointF(centerPosAbsolute.x() / size.width(),
			    centerPosAbsolute.y() / size.height());
}