1 | /* -*- C++ -*- |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 2003 Jason Harris <kstars@30doradus.org> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.0-or-later |
6 | */ |
7 | |
8 | #include "kplotwidget.h" |
9 | |
10 | #include <math.h> |
11 | |
12 | #include <QHash> |
13 | #include <QHelpEvent> |
14 | #include <QPainter> |
15 | #include <QToolTip> |
16 | #include <QtAlgorithms> |
17 | |
18 | #include "kplotaxis.h" |
19 | #include "kplotobject.h" |
20 | #include "kplotpoint.h" |
21 | |
22 | #define XPADDING 20 |
23 | #define YPADDING 20 |
24 | #define BIGTICKSIZE 10 |
25 | #define SMALLTICKSIZE 4 |
26 | #define TICKOFFSET 0 |
27 | |
28 | class Q_DECL_HIDDEN KPlotWidget::Private |
29 | { |
30 | public: |
31 | Private(KPlotWidget *qq) |
32 | : q(qq) |
33 | , cBackground(Qt::black) |
34 | , cForeground(Qt::white) |
35 | , cGrid(Qt::gray) |
36 | , showGrid(false) |
37 | , showObjectToolTip(true) |
38 | , useAntialias(false) |
39 | , autoDelete(true) |
40 | { |
41 | // create the axes and setting their default properties |
42 | KPlotAxis *leftAxis = new KPlotAxis(); |
43 | leftAxis->setTickLabelsShown(true); |
44 | axes.insert(key: LeftAxis, value: leftAxis); |
45 | KPlotAxis *bottomAxis = new KPlotAxis(); |
46 | bottomAxis->setTickLabelsShown(true); |
47 | axes.insert(key: BottomAxis, value: bottomAxis); |
48 | KPlotAxis *rightAxis = new KPlotAxis(); |
49 | axes.insert(key: RightAxis, value: rightAxis); |
50 | KPlotAxis *topAxis = new KPlotAxis(); |
51 | axes.insert(key: TopAxis, value: topAxis); |
52 | } |
53 | |
54 | ~Private() |
55 | { |
56 | if (autoDelete) { |
57 | qDeleteAll(c: objectList); |
58 | } |
59 | qDeleteAll(c: axes); |
60 | } |
61 | |
62 | KPlotWidget *q; |
63 | |
64 | void calcDataRectLimits(double x1, double x2, double y1, double y2); |
65 | /* |
66 | * Returns a value indicating how well the given rectangle is |
67 | * avoiding masked regions in the plot. A higher returned value |
68 | * indicates that the rectangle is intersecting a larger portion |
69 | * of the masked region, or a portion of the masked region which |
70 | * is weighted higher. |
71 | */ |
72 | float rectCost(const QRectF &r) const; |
73 | |
74 | // Colors |
75 | QColor cBackground, cForeground, cGrid; |
76 | // draw options |
77 | bool showGrid; |
78 | bool showObjectToolTip; |
79 | bool useAntialias; |
80 | bool autoDelete; |
81 | // padding |
82 | int leftPadding, rightPadding, topPadding, bottomPadding; |
83 | // hashmap with the axes we have |
84 | QHash<Axis, KPlotAxis *> axes; |
85 | // List of KPlotObjects |
86 | QList<KPlotObject *> objectList; |
87 | // Limits of the plot area in data units |
88 | QRectF dataRect, secondDataRect; |
89 | // Limits of the plot area in pixel units |
90 | QRect pixRect; |
91 | // Array holding the mask of "used" regions of the plot |
92 | QImage plotMask; |
93 | }; |
94 | |
95 | KPlotWidget::KPlotWidget(QWidget *parent) |
96 | : QFrame(parent) |
97 | , d(new Private(this)) |
98 | { |
99 | setAttribute(Qt::WA_OpaquePaintEvent); |
100 | setAttribute(Qt::WA_NoSystemBackground); |
101 | |
102 | d->secondDataRect = QRectF(); // default: no secondary data rect |
103 | // sets the default limits |
104 | d->calcDataRectLimits(x1: 0.0, x2: 1.0, y1: 0.0, y2: 1.0); |
105 | |
106 | setDefaultPaddings(); |
107 | } |
108 | |
109 | KPlotWidget::~KPlotWidget() = default; |
110 | |
111 | QSize KPlotWidget::minimumSizeHint() const |
112 | { |
113 | return QSize(150, 150); |
114 | } |
115 | |
116 | QSize KPlotWidget::sizeHint() const |
117 | { |
118 | return size(); |
119 | } |
120 | |
121 | void KPlotWidget::setLimits(double x1, double x2, double y1, double y2) |
122 | { |
123 | d->calcDataRectLimits(x1, x2, y1, y2); |
124 | update(); |
125 | } |
126 | |
127 | void KPlotWidget::Private::calcDataRectLimits(double x1, double x2, double y1, double y2) |
128 | { |
129 | double XA1; |
130 | double XA2; |
131 | double YA1; |
132 | double YA2; |
133 | if (x2 < x1) { |
134 | XA1 = x2; |
135 | XA2 = x1; |
136 | } else { |
137 | XA1 = x1; |
138 | XA2 = x2; |
139 | } |
140 | if (y2 < y1) { |
141 | YA1 = y2; |
142 | YA2 = y1; |
143 | } else { |
144 | YA1 = y1; |
145 | YA2 = y2; |
146 | } |
147 | |
148 | if (XA2 == XA1) { |
149 | // qWarning() << "x1 and x2 cannot be equal. Setting x2 = x1 + 1.0"; |
150 | XA2 = XA1 + 1.0; |
151 | } |
152 | if (YA2 == YA1) { |
153 | // qWarning() << "y1 and y2 cannot be equal. Setting y2 = y1 + 1.0"; |
154 | YA2 = YA1 + 1.0; |
155 | } |
156 | dataRect = QRectF(XA1, YA1, XA2 - XA1, YA2 - YA1); |
157 | |
158 | q->axis(type: LeftAxis)->setTickMarks(x0: dataRect.y(), length: dataRect.height()); |
159 | q->axis(type: BottomAxis)->setTickMarks(x0: dataRect.x(), length: dataRect.width()); |
160 | |
161 | if (secondDataRect.isNull()) { |
162 | q->axis(type: RightAxis)->setTickMarks(x0: dataRect.y(), length: dataRect.height()); |
163 | q->axis(type: TopAxis)->setTickMarks(x0: dataRect.x(), length: dataRect.width()); |
164 | } |
165 | } |
166 | |
167 | void KPlotWidget::setSecondaryLimits(double x1, double x2, double y1, double y2) |
168 | { |
169 | double XA1; |
170 | double XA2; |
171 | double YA1; |
172 | double YA2; |
173 | if (x2 < x1) { |
174 | XA1 = x2; |
175 | XA2 = x1; |
176 | } else { |
177 | XA1 = x1; |
178 | XA2 = x2; |
179 | } |
180 | if (y2 < y1) { |
181 | YA1 = y2; |
182 | YA2 = y1; |
183 | } else { |
184 | YA1 = y1; |
185 | YA2 = y2; |
186 | } |
187 | |
188 | if (XA2 == XA1) { |
189 | // qWarning() << "x1 and x2 cannot be equal. Setting x2 = x1 + 1.0"; |
190 | XA2 = XA1 + 1.0; |
191 | } |
192 | if (YA2 == YA1) { |
193 | // qWarning() << "y1 and y2 cannot be equal. Setting y2 = y1 + 1.0"; |
194 | YA2 = YA1 + 1.0; |
195 | } |
196 | d->secondDataRect = QRectF(XA1, YA1, XA2 - XA1, YA2 - YA1); |
197 | |
198 | axis(type: RightAxis)->setTickMarks(x0: d->secondDataRect.y(), length: d->secondDataRect.height()); |
199 | axis(type: TopAxis)->setTickMarks(x0: d->secondDataRect.x(), length: d->secondDataRect.width()); |
200 | |
201 | update(); |
202 | } |
203 | |
204 | void KPlotWidget::clearSecondaryLimits() |
205 | { |
206 | d->secondDataRect = QRectF(); |
207 | axis(type: RightAxis)->setTickMarks(x0: d->dataRect.y(), length: d->dataRect.height()); |
208 | axis(type: TopAxis)->setTickMarks(x0: d->dataRect.x(), length: d->dataRect.width()); |
209 | |
210 | update(); |
211 | } |
212 | |
213 | QRectF KPlotWidget::dataRect() const |
214 | { |
215 | return d->dataRect; |
216 | } |
217 | |
218 | QRectF KPlotWidget::secondaryDataRect() const |
219 | { |
220 | return d->secondDataRect; |
221 | } |
222 | |
223 | void KPlotWidget::addPlotObject(KPlotObject *object) |
224 | { |
225 | // skip null pointers |
226 | if (!object) { |
227 | return; |
228 | } |
229 | d->objectList.append(t: object); |
230 | update(); |
231 | } |
232 | |
233 | void KPlotWidget::addPlotObjects(const QList<KPlotObject *> &objects) |
234 | { |
235 | bool addedsome = false; |
236 | for (KPlotObject *o : objects) { |
237 | if (!o) { |
238 | continue; |
239 | } |
240 | |
241 | d->objectList.append(t: o); |
242 | addedsome = true; |
243 | } |
244 | if (addedsome) { |
245 | update(); |
246 | } |
247 | } |
248 | |
249 | QList<KPlotObject *> KPlotWidget::plotObjects() const |
250 | { |
251 | return d->objectList; |
252 | } |
253 | |
254 | void KPlotWidget::setAutoDeletePlotObjects(bool autoDelete) |
255 | { |
256 | d->autoDelete = autoDelete; |
257 | } |
258 | |
259 | void KPlotWidget::removeAllPlotObjects() |
260 | { |
261 | if (d->objectList.isEmpty()) { |
262 | return; |
263 | } |
264 | |
265 | if (d->autoDelete) { |
266 | qDeleteAll(c: d->objectList); |
267 | } |
268 | d->objectList.clear(); |
269 | update(); |
270 | } |
271 | |
272 | void KPlotWidget::resetPlotMask() |
273 | { |
274 | d->plotMask = QImage(pixRect().size(), QImage::Format_ARGB32); |
275 | QColor fillColor = Qt::black; |
276 | fillColor.setAlpha(128); |
277 | d->plotMask.fill(pixel: fillColor.rgb()); |
278 | } |
279 | |
280 | void KPlotWidget::resetPlot() |
281 | { |
282 | if (d->autoDelete) { |
283 | qDeleteAll(c: d->objectList); |
284 | } |
285 | d->objectList.clear(); |
286 | clearSecondaryLimits(); |
287 | d->calcDataRectLimits(x1: 0.0, x2: 1.0, y1: 0.0, y2: 1.0); |
288 | KPlotAxis *a = axis(type: RightAxis); |
289 | a->setLabel(QString()); |
290 | a->setTickLabelsShown(false); |
291 | a = axis(type: TopAxis); |
292 | a->setLabel(QString()); |
293 | a->setTickLabelsShown(false); |
294 | axis(type: KPlotWidget::LeftAxis)->setLabel(QString()); |
295 | axis(type: KPlotWidget::BottomAxis)->setLabel(QString()); |
296 | resetPlotMask(); |
297 | } |
298 | |
299 | void KPlotWidget::replacePlotObject(int i, KPlotObject *o) |
300 | { |
301 | // skip null pointers and invalid indexes |
302 | if (!o || i < 0 || i >= d->objectList.count()) { |
303 | return; |
304 | } |
305 | if (d->objectList.at(i) == o) { |
306 | return; |
307 | } |
308 | if (d->autoDelete) { |
309 | delete d->objectList.at(i); |
310 | } |
311 | d->objectList.replace(i, t: o); |
312 | update(); |
313 | } |
314 | |
315 | QColor KPlotWidget::backgroundColor() const |
316 | { |
317 | return d->cBackground; |
318 | } |
319 | |
320 | QColor KPlotWidget::foregroundColor() const |
321 | { |
322 | return d->cForeground; |
323 | } |
324 | |
325 | QColor KPlotWidget::gridColor() const |
326 | { |
327 | return d->cGrid; |
328 | } |
329 | |
330 | void KPlotWidget::setBackgroundColor(const QColor &bg) |
331 | { |
332 | d->cBackground = bg; |
333 | update(); |
334 | } |
335 | |
336 | void KPlotWidget::setForegroundColor(const QColor &fg) |
337 | { |
338 | d->cForeground = fg; |
339 | update(); |
340 | } |
341 | |
342 | void KPlotWidget::setGridColor(const QColor &gc) |
343 | { |
344 | d->cGrid = gc; |
345 | update(); |
346 | } |
347 | |
348 | bool KPlotWidget::isGridShown() const |
349 | { |
350 | return d->showGrid; |
351 | } |
352 | |
353 | bool KPlotWidget::isObjectToolTipShown() const |
354 | { |
355 | return d->showObjectToolTip; |
356 | } |
357 | |
358 | bool KPlotWidget::antialiasing() const |
359 | { |
360 | return d->useAntialias; |
361 | } |
362 | |
363 | void KPlotWidget::setAntialiasing(bool b) |
364 | { |
365 | d->useAntialias = b; |
366 | update(); |
367 | } |
368 | |
369 | void KPlotWidget::setShowGrid(bool show) |
370 | { |
371 | d->showGrid = show; |
372 | update(); |
373 | } |
374 | |
375 | void KPlotWidget::setObjectToolTipShown(bool show) |
376 | { |
377 | d->showObjectToolTip = show; |
378 | } |
379 | |
380 | KPlotAxis *KPlotWidget::axis(Axis type) |
381 | { |
382 | QHash<Axis, KPlotAxis *>::Iterator it = d->axes.find(key: type); |
383 | return it != d->axes.end() ? it.value() : nullptr; |
384 | } |
385 | |
386 | const KPlotAxis *KPlotWidget::axis(Axis type) const |
387 | { |
388 | QHash<Axis, KPlotAxis *>::ConstIterator it = d->axes.constFind(key: type); |
389 | return it != d->axes.constEnd() ? it.value() : nullptr; |
390 | } |
391 | |
392 | QRect KPlotWidget::pixRect() const |
393 | { |
394 | return d->pixRect; |
395 | } |
396 | |
397 | QList<KPlotPoint *> KPlotWidget::pointsUnderPoint(const QPoint &p) const |
398 | { |
399 | QList<KPlotPoint *> pts; |
400 | for (const KPlotObject *po : std::as_const(t&: d->objectList)) { |
401 | const auto pointsList = po->points(); |
402 | for (KPlotPoint *pp : pointsList) { |
403 | if ((p - mapToWidget(p: pp->position()).toPoint()).manhattanLength() <= 4) { |
404 | pts << pp; |
405 | } |
406 | } |
407 | } |
408 | |
409 | return pts; |
410 | } |
411 | |
412 | bool KPlotWidget::event(QEvent *e) |
413 | { |
414 | if (e->type() == QEvent::ToolTip) { |
415 | if (d->showObjectToolTip) { |
416 | QHelpEvent *he = static_cast<QHelpEvent *>(e); |
417 | QList<KPlotPoint *> pts = pointsUnderPoint(p: he->pos() - QPoint(leftPadding(), topPadding()) - contentsRect().topLeft()); |
418 | if (!pts.isEmpty()) { |
419 | QToolTip::showText(pos: he->globalPos(), text: pts.front()->label(), w: this); |
420 | } |
421 | } |
422 | e->accept(); |
423 | return true; |
424 | } else { |
425 | return QFrame::event(e); |
426 | } |
427 | } |
428 | |
429 | void KPlotWidget::resizeEvent(QResizeEvent *e) |
430 | { |
431 | QFrame::resizeEvent(event: e); |
432 | setPixRect(); |
433 | resetPlotMask(); |
434 | } |
435 | |
436 | void KPlotWidget::setPixRect() |
437 | { |
438 | int newWidth = contentsRect().width() - leftPadding() - rightPadding(); |
439 | int newHeight = contentsRect().height() - topPadding() - bottomPadding(); |
440 | // PixRect starts at (0,0) because we will translate by leftPadding(), topPadding() |
441 | d->pixRect = QRect(0, 0, newWidth, newHeight); |
442 | } |
443 | |
444 | QPointF KPlotWidget::mapToWidget(const QPointF &p) const |
445 | { |
446 | float px = d->pixRect.left() + d->pixRect.width() * (p.x() - d->dataRect.x()) / d->dataRect.width(); |
447 | float py = d->pixRect.top() + d->pixRect.height() * (d->dataRect.y() + d->dataRect.height() - p.y()) / d->dataRect.height(); |
448 | return QPointF(px, py); |
449 | } |
450 | |
451 | void KPlotWidget::maskRect(const QRectF &rf, float fvalue) |
452 | { |
453 | QRect r = rf.toRect().intersected(other: d->pixRect); |
454 | int value = int(fvalue); |
455 | QColor newColor; |
456 | for (int ix = r.left(); ix < r.right(); ++ix) { |
457 | for (int iy = r.top(); iy < r.bottom(); ++iy) { |
458 | newColor = QColor(d->plotMask.pixel(x: ix, y: iy)); |
459 | newColor.setAlpha(200); |
460 | newColor.setRed(qMin(a: newColor.red() + value, b: 255)); |
461 | d->plotMask.setPixel(x: ix, y: iy, index_or_rgb: newColor.rgba()); |
462 | } |
463 | } |
464 | } |
465 | |
466 | void KPlotWidget::maskAlongLine(const QPointF &p1, const QPointF &p2, float fvalue) |
467 | { |
468 | if (!d->pixRect.contains(p: p1.toPoint()) && !d->pixRect.contains(p: p2.toPoint())) { |
469 | return; |
470 | } |
471 | |
472 | int value = int(fvalue); |
473 | |
474 | // Determine slope and zeropoint of line |
475 | double m = (p2.y() - p1.y()) / (p2.x() - p1.x()); |
476 | double y0 = p1.y() - m * p1.x(); |
477 | QColor newColor; |
478 | |
479 | // Mask each pixel along the line joining p1 and p2 |
480 | if (m > 1.0 || m < -1.0) { // step in y-direction |
481 | int y1 = int(p1.y()); |
482 | int y2 = int(p2.y()); |
483 | if (y1 > y2) { |
484 | y1 = int(p2.y()); |
485 | y2 = int(p1.y()); |
486 | } |
487 | |
488 | for (int y = y1; y <= y2; ++y) { |
489 | int x = int((y - y0) / m); |
490 | if (d->pixRect.contains(ax: x, ay: y)) { |
491 | newColor = QColor(d->plotMask.pixel(x, y)); |
492 | newColor.setAlpha(100); |
493 | newColor.setRed(qMin(a: newColor.red() + value, b: 255)); |
494 | d->plotMask.setPixel(x, y, index_or_rgb: newColor.rgba()); |
495 | } |
496 | } |
497 | |
498 | } else { // step in x-direction |
499 | int x1 = int(p1.x()); |
500 | int x2 = int(p2.x()); |
501 | if (x1 > x2) { |
502 | x1 = int(p2.x()); |
503 | x2 = int(p1.x()); |
504 | } |
505 | |
506 | for (int x = x1; x <= x2; ++x) { |
507 | int y = int(y0 + m * x); |
508 | if (d->pixRect.contains(ax: x, ay: y)) { |
509 | newColor = QColor(d->plotMask.pixel(x, y)); |
510 | newColor.setAlpha(100); |
511 | newColor.setRed(qMin(a: newColor.red() + value, b: 255)); |
512 | d->plotMask.setPixel(x, y, index_or_rgb: newColor.rgba()); |
513 | } |
514 | } |
515 | } |
516 | } |
517 | |
518 | // Determine optimal placement for a text label for point pp. We want |
519 | // the label to be near point pp, but we don't want it to overlap with |
520 | // other labels or plot elements. We will use a "downhill simplex" |
521 | // algorithm to find a label position that minimizes the pixel values |
522 | // in the plotMask image over the label's rect(). The sum of pixel |
523 | // values in the label's rect is the "cost" of placing the label there. |
524 | // |
525 | // Because a downhill simplex follows the local gradient to find low |
526 | // values, it can get stuck in local minima. To mitigate this, we will |
527 | // iteratively attempt each of the initial path offset directions (up, |
528 | // down, right, left) in the order of increasing cost at each location. |
529 | void KPlotWidget::placeLabel(QPainter *painter, KPlotPoint *pp) |
530 | { |
531 | int textFlags = Qt::TextSingleLine | Qt::AlignCenter; |
532 | |
533 | QPointF pos = mapToWidget(p: pp->position()); |
534 | if (!d->pixRect.contains(p: pos.toPoint())) { |
535 | return; |
536 | } |
537 | |
538 | QFontMetricsF fm(painter->font(), painter->device()); |
539 | QRectF bestRect = fm.boundingRect(r: QRectF(pos.x(), pos.y(), 1, 1), flags: textFlags, string: pp->label()); |
540 | float xStep = 0.5 * bestRect.width(); |
541 | float yStep = 0.5 * bestRect.height(); |
542 | float maxCost = 0.05 * bestRect.width() * bestRect.height(); |
543 | float bestCost = d->rectCost(r: bestRect); |
544 | |
545 | // We will travel along a path defined by the maximum decrease in |
546 | // the cost at each step. If this path takes us to a local minimum |
547 | // whose cost exceeds maxCost, then we will restart at the |
548 | // beginning and select the next-best path. The indices of |
549 | // already-tried paths are stored in the TriedPathIndex list. |
550 | // |
551 | // If we try all four first-step paths and still don't get below |
552 | // maxCost, then we'll adopt the local minimum position with the |
553 | // best cost (designated as bestBadCost). |
554 | int iter = 0; |
555 | QList<int> TriedPathIndex; |
556 | float bestBadCost = 10000; |
557 | QRectF bestBadRect; |
558 | |
559 | // needed to halt iteration from inside the switch |
560 | bool flagStop = false; |
561 | |
562 | while (bestCost > maxCost) { |
563 | // Displace the label up, down, left, right; determine which |
564 | // step provides the lowest cost |
565 | QRectF upRect = bestRect; |
566 | upRect.moveTop(pos: upRect.top() + yStep); |
567 | float upCost = d->rectCost(r: upRect); |
568 | QRectF downRect = bestRect; |
569 | downRect.moveTop(pos: downRect.top() - yStep); |
570 | float downCost = d->rectCost(r: downRect); |
571 | QRectF leftRect = bestRect; |
572 | leftRect.moveLeft(pos: leftRect.left() - xStep); |
573 | float leftCost = d->rectCost(r: leftRect); |
574 | QRectF rightRect = bestRect; |
575 | rightRect.moveLeft(pos: rightRect.left() + xStep); |
576 | float rightCost = d->rectCost(r: rightRect); |
577 | |
578 | // which direction leads to the lowest cost? |
579 | QList<float> costList; |
580 | costList << upCost << downCost << leftCost << rightCost; |
581 | int imin = -1; |
582 | for (int i = 0; i < costList.size(); ++i) { |
583 | if (iter == 0 && TriedPathIndex.contains(t: i)) { |
584 | continue; // Skip this first-step path, we already tried it! |
585 | } |
586 | |
587 | // If this first-step path doesn't improve the cost, |
588 | // skip this direction from now on |
589 | if (iter == 0 && costList[i] >= bestCost) { |
590 | TriedPathIndex.append(t: i); |
591 | continue; |
592 | } |
593 | |
594 | if (costList[i] < bestCost && (imin < 0 || costList[i] < costList[imin])) { |
595 | imin = i; |
596 | } |
597 | } |
598 | |
599 | // Make a note that we've tried the current first-step path |
600 | if (iter == 0 && imin >= 0) { |
601 | TriedPathIndex.append(t: imin); |
602 | } |
603 | |
604 | // Adopt the step that produced the best cost |
605 | switch (imin) { |
606 | case 0: // up |
607 | bestRect.moveTop(pos: upRect.top()); |
608 | bestCost = upCost; |
609 | break; |
610 | case 1: // down |
611 | bestRect.moveTop(pos: downRect.top()); |
612 | bestCost = downCost; |
613 | break; |
614 | case 2: // left |
615 | bestRect.moveLeft(pos: leftRect.left()); |
616 | bestCost = leftCost; |
617 | break; |
618 | case 3: // right |
619 | bestRect.moveLeft(pos: rightRect.left()); |
620 | bestCost = rightCost; |
621 | break; |
622 | case -1: // no lower cost found! |
623 | // We hit a local minimum. Keep the best of these as bestBadRect |
624 | if (bestCost < bestBadCost) { |
625 | bestBadCost = bestCost; |
626 | bestBadRect = bestRect; |
627 | } |
628 | |
629 | // If all of the first-step paths have now been searched, we'll |
630 | // have to adopt the bestBadRect |
631 | if (TriedPathIndex.size() == 4) { |
632 | bestRect = bestBadRect; |
633 | flagStop = true; // halt iteration |
634 | break; |
635 | } |
636 | |
637 | // If we haven't yet tried all of the first-step paths, start over |
638 | if (TriedPathIndex.size() < 4) { |
639 | iter = -1; // anticipating the ++iter below |
640 | bestRect = fm.boundingRect(r: QRectF(pos.x(), pos.y(), 1, 1), flags: textFlags, string: pp->label()); |
641 | bestCost = d->rectCost(r: bestRect); |
642 | } |
643 | break; |
644 | } |
645 | |
646 | // Halt iteration, because we've tried all directions and |
647 | // haven't gotten below maxCost (we'll adopt the best |
648 | // local minimum found) |
649 | if (flagStop) { |
650 | break; |
651 | } |
652 | |
653 | ++iter; |
654 | } |
655 | |
656 | painter->drawText(r: bestRect, flags: textFlags, text: pp->label()); |
657 | |
658 | // Is a line needed to connect the label to the point? |
659 | float deltax = pos.x() - bestRect.center().x(); |
660 | float deltay = pos.y() - bestRect.center().y(); |
661 | float rbest = sqrt(x: deltax * deltax + deltay * deltay); |
662 | if (rbest > 20.0) { |
663 | // Draw a rectangle around the label |
664 | painter->setBrush(QBrush()); |
665 | // QPen pen = painter->pen(); |
666 | // pen.setStyle( Qt::DotLine ); |
667 | // painter->setPen( pen ); |
668 | painter->drawRoundedRect(rect: bestRect, xRadius: 25, yRadius: 25, mode: Qt::RelativeSize); |
669 | |
670 | // Now connect the label to the point with a line. |
671 | // The line is drawn from the center of the near edge of the rectangle |
672 | float xline = bestRect.center().x(); |
673 | if (bestRect.left() > pos.x()) { |
674 | xline = bestRect.left(); |
675 | } |
676 | if (bestRect.right() < pos.x()) { |
677 | xline = bestRect.right(); |
678 | } |
679 | |
680 | float yline = bestRect.center().y(); |
681 | if (bestRect.top() > pos.y()) { |
682 | yline = bestRect.top(); |
683 | } |
684 | if (bestRect.bottom() < pos.y()) { |
685 | yline = bestRect.bottom(); |
686 | } |
687 | |
688 | painter->drawLine(p1: QPointF(xline, yline), p2: pos); |
689 | } |
690 | |
691 | // Mask the label's rectangle so other labels won't overlap it. |
692 | maskRect(rf: bestRect); |
693 | } |
694 | |
695 | float KPlotWidget::Private::rectCost(const QRectF &r) const |
696 | { |
697 | if (!plotMask.rect().contains(r: r.toRect())) { |
698 | return 10000.; |
699 | } |
700 | |
701 | // Compute sum of mask values in the rect r |
702 | QImage subMask = plotMask.copy(rect: r.toRect()); |
703 | int cost = 0; |
704 | for (int ix = 0; ix < subMask.width(); ++ix) { |
705 | for (int iy = 0; iy < subMask.height(); ++iy) { |
706 | cost += QColor(subMask.pixel(x: ix, y: iy)).red(); |
707 | } |
708 | } |
709 | |
710 | return float(cost); |
711 | } |
712 | |
713 | void KPlotWidget::paintEvent(QPaintEvent *e) |
714 | { |
715 | // let QFrame draw its default stuff (like the frame) |
716 | QFrame::paintEvent(e); |
717 | QPainter p; |
718 | |
719 | p.begin(this); |
720 | p.setRenderHint(hint: QPainter::Antialiasing, on: d->useAntialias); |
721 | p.fillRect(rect(), color: backgroundColor()); |
722 | p.translate(dx: leftPadding() + 0.5, dy: topPadding() + 0.5); |
723 | |
724 | setPixRect(); |
725 | p.setClipRect(d->pixRect); |
726 | p.setClipping(true); |
727 | |
728 | resetPlotMask(); |
729 | |
730 | for (KPlotObject *po : std::as_const(t&: d->objectList)) { |
731 | po->draw(p: &p, pw: this); |
732 | } |
733 | |
734 | // DEBUG: Draw the plot mask |
735 | // p.drawImage( 0, 0, d->plotMask ); |
736 | |
737 | p.setClipping(false); |
738 | drawAxes(p: &p); |
739 | |
740 | p.end(); |
741 | } |
742 | |
743 | void KPlotWidget::drawAxes(QPainter *p) |
744 | { |
745 | if (d->showGrid) { |
746 | p->setPen(gridColor()); |
747 | |
748 | // Grid lines are placed at locations of primary axes' major tickmarks |
749 | // vertical grid lines |
750 | const QList<double> majMarks = axis(type: BottomAxis)->majorTickMarks(); |
751 | for (const double xx : majMarks) { |
752 | double px = d->pixRect.width() * (xx - d->dataRect.x()) / d->dataRect.width(); |
753 | p->drawLine(p1: QPointF(px, 0.0), p2: QPointF(px, double(d->pixRect.height()))); |
754 | } |
755 | // horizontal grid lines |
756 | const QList<double> leftTickMarks = axis(type: LeftAxis)->majorTickMarks(); |
757 | for (const double yy : leftTickMarks) { |
758 | double py = d->pixRect.height() * (1.0 - (yy - d->dataRect.y()) / d->dataRect.height()); |
759 | p->drawLine(p1: QPointF(0.0, py), p2: QPointF(double(d->pixRect.width()), py)); |
760 | } |
761 | } |
762 | |
763 | p->setPen(foregroundColor()); |
764 | p->setBrush(Qt::NoBrush); |
765 | |
766 | // set small font for tick labels |
767 | QFont f = p->font(); |
768 | int s = f.pointSize(); |
769 | f.setPointSize(s - 2); |
770 | p->setFont(f); |
771 | |
772 | /* BottomAxis */ |
773 | KPlotAxis *a = axis(type: BottomAxis); |
774 | if (a->isVisible()) { |
775 | // Draw axis line |
776 | p->drawLine(x1: 0, y1: d->pixRect.height(), x2: d->pixRect.width(), y2: d->pixRect.height()); |
777 | |
778 | // Draw major tickmarks |
779 | const QList<double> majMarks = a->majorTickMarks(); |
780 | for (const double xx : majMarks) { |
781 | double px = d->pixRect.width() * (xx - d->dataRect.x()) / d->dataRect.width(); |
782 | if (px > 0 && px < d->pixRect.width()) { |
783 | p->drawLine(p1: QPointF(px, double(d->pixRect.height() - TICKOFFSET)), // |
784 | p2: QPointF(px, double(d->pixRect.height() - BIGTICKSIZE - TICKOFFSET))); |
785 | |
786 | // Draw ticklabel |
787 | if (a->areTickLabelsShown()) { |
788 | QRect r(int(px) - BIGTICKSIZE, d->pixRect.height() + BIGTICKSIZE, 2 * BIGTICKSIZE, BIGTICKSIZE); |
789 | p->drawText(r, flags: Qt::AlignCenter | Qt::TextDontClip, text: a->tickLabel(value: xx)); |
790 | } |
791 | } |
792 | } |
793 | |
794 | // Draw minor tickmarks |
795 | const QList<double> minTickMarks = a->minorTickMarks(); |
796 | for (const double xx : minTickMarks) { |
797 | double px = d->pixRect.width() * (xx - d->dataRect.x()) / d->dataRect.width(); |
798 | if (px > 0 && px < d->pixRect.width()) { |
799 | p->drawLine(p1: QPointF(px, double(d->pixRect.height() - TICKOFFSET)), // |
800 | p2: QPointF(px, double(d->pixRect.height() - SMALLTICKSIZE - TICKOFFSET))); |
801 | } |
802 | } |
803 | |
804 | // Draw BottomAxis Label |
805 | if (!a->label().isEmpty()) { |
806 | QRect r(0, d->pixRect.height() + 2 * YPADDING, d->pixRect.width(), YPADDING); |
807 | p->drawText(r, flags: Qt::AlignCenter, text: a->label()); |
808 | } |
809 | } // End of BottomAxis |
810 | |
811 | /* LeftAxis */ |
812 | a = axis(type: LeftAxis); |
813 | if (a->isVisible()) { |
814 | // Draw axis line |
815 | p->drawLine(x1: 0, y1: 0, x2: 0, y2: d->pixRect.height()); |
816 | |
817 | // Draw major tickmarks |
818 | const QList<double> majMarks = a->majorTickMarks(); |
819 | for (const double yy : majMarks) { |
820 | double py = d->pixRect.height() * (1.0 - (yy - d->dataRect.y()) / d->dataRect.height()); |
821 | if (py > 0 && py < d->pixRect.height()) { |
822 | p->drawLine(p1: QPointF(TICKOFFSET, py), p2: QPointF(double(TICKOFFSET + BIGTICKSIZE), py)); |
823 | |
824 | // Draw ticklabel |
825 | if (a->areTickLabelsShown()) { |
826 | QRect r(-2 * BIGTICKSIZE - SMALLTICKSIZE, int(py) - SMALLTICKSIZE, 2 * BIGTICKSIZE, 2 * SMALLTICKSIZE); |
827 | p->drawText(r, flags: Qt::AlignRight | Qt::AlignVCenter | Qt::TextDontClip, text: a->tickLabel(value: yy)); |
828 | } |
829 | } |
830 | } |
831 | |
832 | // Draw minor tickmarks |
833 | const QList<double> minTickMarks = a->minorTickMarks(); |
834 | for (const double yy : minTickMarks) { |
835 | double py = d->pixRect.height() * (1.0 - (yy - d->dataRect.y()) / d->dataRect.height()); |
836 | if (py > 0 && py < d->pixRect.height()) { |
837 | p->drawLine(p1: QPointF(TICKOFFSET, py), p2: QPointF(double(TICKOFFSET + SMALLTICKSIZE), py)); |
838 | } |
839 | } |
840 | |
841 | // Draw LeftAxis Label. We need to draw the text sideways. |
842 | if (!a->label().isEmpty()) { |
843 | // store current painter translation/rotation state |
844 | p->save(); |
845 | |
846 | // translate coord sys to left corner of axis label rectangle, then rotate 90 degrees. |
847 | p->translate(dx: -3 * XPADDING, dy: d->pixRect.height()); |
848 | p->rotate(a: -90.0); |
849 | |
850 | QRect r(0, 0, d->pixRect.height(), XPADDING); |
851 | p->drawText(r, flags: Qt::AlignCenter, text: a->label()); // draw the label, now that we are sideways |
852 | |
853 | p->restore(); // restore translation/rotation state |
854 | } |
855 | } // End of LeftAxis |
856 | |
857 | // Prepare for top and right axes; we may need the secondary data rect |
858 | double x0 = d->dataRect.x(); |
859 | double y0 = d->dataRect.y(); |
860 | double dw = d->dataRect.width(); |
861 | double dh = d->dataRect.height(); |
862 | if (secondaryDataRect().isValid()) { |
863 | x0 = secondaryDataRect().x(); |
864 | y0 = secondaryDataRect().y(); |
865 | dw = secondaryDataRect().width(); |
866 | dh = secondaryDataRect().height(); |
867 | } |
868 | |
869 | /* TopAxis */ |
870 | a = axis(type: TopAxis); |
871 | if (a->isVisible()) { |
872 | // Draw axis line |
873 | p->drawLine(x1: 0, y1: 0, x2: d->pixRect.width(), y2: 0); |
874 | |
875 | // Draw major tickmarks |
876 | const QList<double> majMarks = a->majorTickMarks(); |
877 | for (const double xx : majMarks) { |
878 | double px = d->pixRect.width() * (xx - x0) / dw; |
879 | if (px > 0 && px < d->pixRect.width()) { |
880 | p->drawLine(p1: QPointF(px, TICKOFFSET), p2: QPointF(px, double(BIGTICKSIZE + TICKOFFSET))); |
881 | |
882 | // Draw ticklabel |
883 | if (a->areTickLabelsShown()) { |
884 | QRect r(int(px) - BIGTICKSIZE, (int)-1.5 * BIGTICKSIZE, 2 * BIGTICKSIZE, BIGTICKSIZE); |
885 | p->drawText(r, flags: Qt::AlignCenter | Qt::TextDontClip, text: a->tickLabel(value: xx)); |
886 | } |
887 | } |
888 | } |
889 | |
890 | // Draw minor tickmarks |
891 | const QList<double> minMarks = a->minorTickMarks(); |
892 | for (const double xx : minMarks) { |
893 | double px = d->pixRect.width() * (xx - x0) / dw; |
894 | if (px > 0 && px < d->pixRect.width()) { |
895 | p->drawLine(p1: QPointF(px, TICKOFFSET), p2: QPointF(px, double(SMALLTICKSIZE + TICKOFFSET))); |
896 | } |
897 | } |
898 | |
899 | // Draw TopAxis Label |
900 | if (!a->label().isEmpty()) { |
901 | QRect r(0, 0 - 3 * YPADDING, d->pixRect.width(), YPADDING); |
902 | p->drawText(r, flags: Qt::AlignCenter, text: a->label()); |
903 | } |
904 | } // End of TopAxis |
905 | |
906 | /* RightAxis */ |
907 | a = axis(type: RightAxis); |
908 | if (a->isVisible()) { |
909 | // Draw axis line |
910 | p->drawLine(x1: d->pixRect.width(), y1: 0, x2: d->pixRect.width(), y2: d->pixRect.height()); |
911 | |
912 | // Draw major tickmarks |
913 | const QList<double> majMarks = a->majorTickMarks(); |
914 | for (const double yy : majMarks) { |
915 | double py = d->pixRect.height() * (1.0 - (yy - y0) / dh); |
916 | if (py > 0 && py < d->pixRect.height()) { |
917 | p->drawLine(p1: QPointF(double(d->pixRect.width() - TICKOFFSET), py), // |
918 | p2: QPointF(double(d->pixRect.width() - TICKOFFSET - BIGTICKSIZE), py)); |
919 | |
920 | // Draw ticklabel |
921 | if (a->areTickLabelsShown()) { |
922 | QRect r(d->pixRect.width() + SMALLTICKSIZE, int(py) - SMALLTICKSIZE, 2 * BIGTICKSIZE, 2 * SMALLTICKSIZE); |
923 | p->drawText(r, flags: Qt::AlignLeft | Qt::AlignVCenter | Qt::TextDontClip, text: a->tickLabel(value: yy)); |
924 | } |
925 | } |
926 | } |
927 | |
928 | // Draw minor tickmarks |
929 | const QList<double> minMarks = a->minorTickMarks(); |
930 | for (const double yy : minMarks) { |
931 | double py = d->pixRect.height() * (1.0 - (yy - y0) / dh); |
932 | if (py > 0 && py < d->pixRect.height()) { |
933 | p->drawLine(p1: QPointF(double(d->pixRect.width() - 0.0), py), p2: QPointF(double(d->pixRect.width() - 0.0 - SMALLTICKSIZE), py)); |
934 | } |
935 | } |
936 | |
937 | // Draw RightAxis Label. We need to draw the text sideways. |
938 | if (!a->label().isEmpty()) { |
939 | // store current painter translation/rotation state |
940 | p->save(); |
941 | |
942 | // translate coord sys to left corner of axis label rectangle, then rotate 90 degrees. |
943 | p->translate(dx: d->pixRect.width() + 2 * XPADDING, dy: d->pixRect.height()); |
944 | p->rotate(a: -90.0); |
945 | |
946 | QRect r(0, 0, d->pixRect.height(), XPADDING); |
947 | p->drawText(r, flags: Qt::AlignCenter, text: a->label()); // draw the label, now that we are sideways |
948 | |
949 | p->restore(); // restore translation/rotation state |
950 | } |
951 | } // End of RightAxis |
952 | } |
953 | |
954 | int KPlotWidget::leftPadding() const |
955 | { |
956 | if (d->leftPadding >= 0) { |
957 | return d->leftPadding; |
958 | } |
959 | const KPlotAxis *a = axis(type: LeftAxis); |
960 | if (a && a->isVisible() && a->areTickLabelsShown()) { |
961 | return !a->label().isEmpty() ? 3 * XPADDING : 2 * XPADDING; |
962 | } |
963 | return XPADDING; |
964 | } |
965 | |
966 | int KPlotWidget::rightPadding() const |
967 | { |
968 | if (d->rightPadding >= 0) { |
969 | return d->rightPadding; |
970 | } |
971 | const KPlotAxis *a = axis(type: RightAxis); |
972 | if (a && a->isVisible() && a->areTickLabelsShown()) { |
973 | return !a->label().isEmpty() ? 3 * XPADDING : 2 * XPADDING; |
974 | } |
975 | return XPADDING; |
976 | } |
977 | |
978 | int KPlotWidget::topPadding() const |
979 | { |
980 | if (d->topPadding >= 0) { |
981 | return d->topPadding; |
982 | } |
983 | const KPlotAxis *a = axis(type: TopAxis); |
984 | if (a && a->isVisible() && a->areTickLabelsShown()) { |
985 | return !a->label().isEmpty() ? 3 * YPADDING : 2 * YPADDING; |
986 | } |
987 | return YPADDING; |
988 | } |
989 | |
990 | int KPlotWidget::bottomPadding() const |
991 | { |
992 | if (d->bottomPadding >= 0) { |
993 | return d->bottomPadding; |
994 | } |
995 | const KPlotAxis *a = axis(type: BottomAxis); |
996 | if (a && a->isVisible() && a->areTickLabelsShown()) { |
997 | return !a->label().isEmpty() ? 3 * YPADDING : 2 * YPADDING; |
998 | } |
999 | return YPADDING; |
1000 | } |
1001 | |
1002 | void KPlotWidget::setLeftPadding(int padding) |
1003 | { |
1004 | d->leftPadding = padding; |
1005 | } |
1006 | |
1007 | void KPlotWidget::setRightPadding(int padding) |
1008 | { |
1009 | d->rightPadding = padding; |
1010 | } |
1011 | |
1012 | void KPlotWidget::setTopPadding(int padding) |
1013 | { |
1014 | d->topPadding = padding; |
1015 | } |
1016 | |
1017 | void KPlotWidget::setBottomPadding(int padding) |
1018 | { |
1019 | d->bottomPadding = padding; |
1020 | } |
1021 | |
1022 | void KPlotWidget::setDefaultPaddings() |
1023 | { |
1024 | d->leftPadding = -1; |
1025 | d->rightPadding = -1; |
1026 | d->topPadding = -1; |
1027 | d->bottomPadding = -1; |
1028 | } |
1029 | |
1030 | #include "moc_kplotwidget.cpp" |
1031 | |