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