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
28class Q_DECL_HIDDEN KPlotWidget::Private
29{
30public:
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
96KPlotWidget::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
110KPlotWidget::~KPlotWidget() = default;
111
112QSize KPlotWidget::minimumSizeHint() const
113{
114 return QSize(150, 150);
115}
116
117QSize KPlotWidget::sizeHint() const
118{
119 return size();
120}
121
122void KPlotWidget::setLimits(double x1, double x2, double y1, double y2)
123{
124 d->calcDataRectLimits(x1, x2, y1, y2);
125 update();
126}
127
128void 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
168void 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
205void 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
214QRectF KPlotWidget::dataRect() const
215{
216 return d->dataRect;
217}
218
219QRectF KPlotWidget::secondaryDataRect() const
220{
221 return d->secondDataRect;
222}
223
224void 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
234void 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
250QList<KPlotObject *> KPlotWidget::plotObjects() const
251{
252 return d->objectList;
253}
254
255void KPlotWidget::setAutoDeletePlotObjects(bool autoDelete)
256{
257 d->autoDelete = autoDelete;
258}
259
260void 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
273void 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
281void 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
300void 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
316QColor KPlotWidget::backgroundColor() const
317{
318 return d->cBackground;
319}
320
321QColor KPlotWidget::foregroundColor() const
322{
323 return d->cForeground;
324}
325
326QColor KPlotWidget::gridColor() const
327{
328 return d->cGrid;
329}
330
331void KPlotWidget::setBackgroundColor(const QColor &bg)
332{
333 d->cBackground = bg;
334 update();
335}
336
337void KPlotWidget::setForegroundColor(const QColor &fg)
338{
339 d->cForeground = fg;
340 update();
341}
342
343void KPlotWidget::setGridColor(const QColor &gc)
344{
345 d->cGrid = gc;
346 update();
347}
348
349bool KPlotWidget::isGridShown() const
350{
351 return d->showGrid;
352}
353
354bool KPlotWidget::isObjectToolTipShown() const
355{
356 return d->showObjectToolTip;
357}
358
359bool KPlotWidget::antialiasing() const
360{
361 return d->useAntialias;
362}
363
364void KPlotWidget::setAntialiasing(bool b)
365{
366 d->useAntialias = b;
367 update();
368}
369
370void KPlotWidget::setShowGrid(bool show)
371{
372 d->showGrid = show;
373 update();
374}
375
376void KPlotWidget::setObjectToolTipShown(bool show)
377{
378 d->showObjectToolTip = show;
379}
380
381KPlotAxis *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
387const 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
393QRect KPlotWidget::pixRect() const
394{
395 return d->pixRect;
396}
397
398QList<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
413bool 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
430void KPlotWidget::resizeEvent(QResizeEvent *e)
431{
432 QFrame::resizeEvent(event: e);
433 setPixRect();
434 resetPlotMask();
435}
436
437void 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
445QPointF 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
452void 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
467void 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.
530void 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
696float 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
714void 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
744void 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
955int 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
967int 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
979int 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
991int 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
1003void KPlotWidget::setLeftPadding(int padding)
1004{
1005 d->leftPadding = padding;
1006}
1007
1008void KPlotWidget::setRightPadding(int padding)
1009{
1010 d->rightPadding = padding;
1011}
1012
1013void KPlotWidget::setTopPadding(int padding)
1014{
1015 d->topPadding = padding;
1016}
1017
1018void KPlotWidget::setBottomPadding(int padding)
1019{
1020 d->bottomPadding = padding;
1021}
1022
1023void 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

source code of kplotting/src/kplotwidget.cpp