-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathsequentialseriesscrollingchartview.cpp
More file actions
271 lines (240 loc) · 8.74 KB
/
Copy pathsequentialseriesscrollingchartview.cpp
File metadata and controls
271 lines (240 loc) · 8.74 KB
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
#include "sequentialseriesscrollingchartview.h"
#include <QLineSeries>
#include <QLogValueAxis>
#include <QValueAxis>
SequentialSeriesScrollingChartView::SequentialSeriesScrollingChartView(
QWidget* const parent)
: QChartView(parent)
// QChartView already creates a chart for us during construction
// (https://github.com/qt/qtcharts/blob/4d0f13651edd605eacf9748ed46185a7d4d47e21/src/charts/qchartview.cpp#L284-L285),
// we only grab its pointer for convenience.
, mChart(this->chart())
, mXAxis(new QValueAxis())
, mYAxisLeft(new QValueAxis())
, mYAxisRight(new QValueAxis())
, mFixedXAxis(nullptr)
, mSeriesLists(2)
, mYAxisScalingMode(YAxisScalingMode::ZeroAnchored)
, mSeriesSpacing(0)
{
mXAxis->setLabelFormat("%d");
static_cast<QValueAxis*>(mYAxisLeft)->setLabelFormat("%d");
static_cast<QValueAxis*>(mYAxisRight)->setLabelFormat("%d");
mXAxis->setTickType(QValueAxis::TicksDynamic);
mXAxis->setTickAnchor(0);
mXAxis->setTickInterval(10);
mChart->legend()->hide();
mChart->addAxis(mXAxis, Qt::AlignBottom);
mChart->addAxis(mYAxisLeft, Qt::AlignLeft);
mChart->addAxis(mYAxisRight, Qt::AlignRight);
}
auto
SequentialSeriesScrollingChartView::xAxis() const -> QValueAxis*
{
return mXAxis;
}
auto
SequentialSeriesScrollingChartView::yAxisLeft() const -> QAbstractAxis*
{
return mYAxisLeft;
}
auto
SequentialSeriesScrollingChartView::yAxisRight() const -> QAbstractAxis*
{
return mYAxisRight;
}
void
SequentialSeriesScrollingChartView::enableLogYAxis()
{
assert(mSeriesLists[0].size() == 0 &&
"enableLogYAxis() must only be called prior to adding datapoints");
assert(mSeriesLists[1].size() == 0 &&
"enableLogYAxis() must only be called prior to adding datapoints");
mChart->removeAxis(mYAxisLeft);
delete mYAxisLeft;
// Use a temporary value with type QLogValueAxis as we're calling
// QLogValueAxis-specific methods below.
auto yAxisLeft = new QLogValueAxis();
mYAxisLeft = yAxisLeft;
mChart->removeAxis(mYAxisRight);
delete mYAxisRight;
// See comment on yAxisLeft;
auto yAxisRight = new QLogValueAxis();
mYAxisRight = yAxisRight;
mChart->addAxis(yAxisLeft, Qt::AlignLeft);
mChart->addAxis(yAxisRight, Qt::AlignRight);
yAxisLeft->setLabelFormat("%d");
yAxisLeft->setBase(10.0);
yAxisLeft->setMinorTickCount(-1);
yAxisLeft->setMax(1000);
yAxisRight->setLabelFormat("%d");
yAxisRight->setBase(10.0);
yAxisRight->setMinorTickCount(-1);
yAxisRight->setMax(1000);
}
void
SequentialSeriesScrollingChartView::enableFixedXAxis()
{
assert(!mFixedXAxis && "enableFixedXAxis() can only be called once");
mXAxis->setVisible(false);
mFixedXAxis = new QValueAxis();
mFixedXAxis->setLabelFormat("%d");
mFixedXAxis->setTickCount(7);
mChart->addAxis(mFixedXAxis, Qt::AlignBottom);
refreshXRange();
}
void
SequentialSeriesScrollingChartView::setSeriesSpacing(const size_t& spacing)
{
assert(mSeriesLists.size() == 0 &&
"setSeriesSpacing() must only be called prior to adding datapoints");
mSeriesSpacing = spacing;
}
void
SequentialSeriesScrollingChartView::setYAxisScalingMode(
const YAxisScalingMode& mode)
{
assert(dynamic_cast<QValueAxis*>(mYAxisLeft) &&
"setYAxisScalingMode must not be called for log Y axes");
assert(dynamic_cast<QValueAxis*>(mYAxisRight) &&
"setYAxisScalingMode must not be called for log Y axes");
mYAxisScalingMode = mode;
}
void
SequentialSeriesScrollingChartView::setTitle(const QString& title)
{
mChart->setTitle(title);
}
void
SequentialSeriesScrollingChartView::setXRange(const qsizetype& range)
{
mXRange = range;
refreshXRange();
}
void
SequentialSeriesScrollingChartView::refreshXRange()
{
const auto maxX = mSeriesLists[0].size() > 0
? std::optional<qsizetype>(static_cast<qsizetype>(
mSeriesLists[0].back()->points().back().rx()))
: std::nullopt;
if (!mFixedXAxis) {
qsizetype ceil = mXRange;
if (maxX.has_value()) {
ceil = std::max(ceil, maxX.value());
}
mXAxis->setRange(ceil - mXRange, ceil);
} else {
mFixedXAxis->setRange(-mXRange, 0);
if (maxX.has_value()) {
mXAxis->setRange(maxX.value() - mXRange, maxX.value());
}
}
}
void
SequentialSeriesScrollingChartView::wipeData()
{
for (auto& seriesList : mSeriesLists) {
for (QLineSeries* series : seriesList) {
mChart->removeSeries(series);
delete series;
}
seriesList.clear();
}
refreshXRange();
}
void
SequentialSeriesScrollingChartView::addDatapoint(const size_t& deviceIndex,
const size_t& seriesIndex,
const qreal& value)
{
assert(deviceIndex < 2 && "only (up to) 2 devices are supported");
assert((seriesIndex + 1 >= mSeriesLists[deviceIndex].size()) &&
"must not append to prior (already completed) series");
// Checking the axis type is a very ugly way of detecting whether we're using
// log axes. TODO: figure out a more elegant solution.
QAbstractAxis* const yAxis = deviceIndex == 0 ? mYAxisLeft : mYAxisRight;
auto const logYAxis = dynamic_cast<QLogValueAxis*>(yAxis);
const QLineSeries* const lastPopulatedSeries =
mSeriesLists[deviceIndex].size() > 0 ? mSeriesLists[deviceIndex].back()
: nullptr;
QLineSeries* series;
while (seriesIndex >= mSeriesLists[deviceIndex].size()) {
// Add in a loop, because the caller could have skipped one or more
// intermediate series. We could make the vector sparse... (which is likely
// to be confusing to manage), or we can just add some empty series which
// makes things easier to reason about.
series = mSeriesLists[deviceIndex].emplace_back(new QLineSeries());
// Ordering is significant: Qt complains (and ignores the request) if
// you try to attach an axis to a new series, if that axis is already
// attached to another series which is attached to a chart, and if that
// new series is not yet attached to the chart. (Translation:
// addSeries(foo), before you foo->attachAxis.)
mChart->addSeries(series);
series->attachAxis(mXAxis);
series->attachAxis(deviceIndex == 0 ? mYAxisLeft : mYAxisRight);
}
series = mSeriesLists[deviceIndex].back();
qsizetype x;
if (series->points().length() > 0) {
x = series->points().back().rx() + 1;
} else {
if (lastPopulatedSeries) {
x = lastPopulatedSeries->points().back().rx() + mSeriesSpacing;
} else {
// First point, ever.
x = 0;
}
}
if (logYAxis != nullptr) {
// Calculated fit-factors can sometimes be < 1.0 (e.g. when measuring a
// null-specimen, in combination with fluctuations in ambient particle
// count: particle counts during the exercise might be higher than during
// the ambient sampling period, resulting a ratio of < 1.0).
// However we can't represent that on a log-scale, so we round up to 1.0
// (QChartView won't even show the points if they are < 1.0.)
series->append(QPointF(x, std::max(value, 1.0)));
} else {
series->append(QPointF(x, value));
}
refreshXRange();
// Perform vertical scaling based on the _current_ series, e.g. current
// exercise, because that's an approximation of what will be interesting for
// the subject.
// TODO: consider scaling based only on the currently visible range: this is
// particularly useful for "live-mode" tests where the current series may
// contain a few hundred points. Re-scaling for every new point is likely
// to be annoying, but maybe we can move the start of the range in chunks?
// (However, given that we're already doing some quantisation below, that's
// maybe NBD?)
qreal minYSeen = std::numeric_limits<qreal>::infinity();
qreal maxYSeen = 0.0;
for (const auto& point : series->points()) {
minYSeen = std::min(minYSeen, point.y());
maxYSeen = std::max(maxYSeen, point.y());
}
if (logYAxis == nullptr) {
switch (mYAxisScalingMode) {
case ZeroAnchored: {
// TODO: configure making this configurable
const qsizetype max = static_cast<qsizetype>(maxYSeen) / 20 * 20 + 20;
yAxis->setRange(0.0, static_cast<qreal>(max));
break;
}
case Floating: {
// TODO: consider making margins configurable.
const qsizetype min =
std::max(static_cast<qsizetype>(0),
static_cast<qsizetype>(minYSeen) / 100 * 100 - 100);
const qsizetype max =
static_cast<qsizetype>(maxYSeen) / 100 * 100 + 200;
yAxis->setRange(static_cast<qreal>(min), static_cast<qreal>(max));
break;
}
}
} else {
// Round up to the next whole log as that seems easiest to read.
auto maxLog = std::log10(maxYSeen);
logYAxis->setMax(std::pow(10.0, std::max(std::floor(maxLog) + 1, 3.0)));
}
}