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 * 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
95KPlotWidget::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
109KPlotWidget::~KPlotWidget() = default;
110
111QSize KPlotWidget::minimumSizeHint() const
112{
113 return QSize(150, 150);
114}
115
116QSize KPlotWidget::sizeHint() const
117{
118 return size();
119}
120
121void KPlotWidget::setLimits(double x1, double x2, double y1, double y2)
122{
123 d->calcDataRectLimits(x1, x2, y1, y2);
124 update();
125}
126
127void 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
167void 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
204void 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
213QRectF KPlotWidget::dataRect() const
214{
215 return d->dataRect;
216}
217
218QRectF KPlotWidget::secondaryDataRect() const
219{
220 return d->secondDataRect;
221}
222
223void 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
233void 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
249QList<KPlotObject *> KPlotWidget::plotObjects() const
250{
251 return d->objectList;
252}
253
254void KPlotWidget::setAutoDeletePlotObjects(bool autoDelete)
255{
256 d->autoDelete = autoDelete;
257}
258
259void 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
272void 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
280void 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
299void 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
315QColor KPlotWidget::backgroundColor() const
316{
317 return d->cBackground;
318}
319
320QColor KPlotWidget::foregroundColor() const
321{
322 return d->cForeground;
323}
324
325QColor KPlotWidget::gridColor() const
326{
327 return d->cGrid;
328}
329
330void KPlotWidget::setBackgroundColor(const QColor &bg)
331{
332 d->cBackground = bg;
333 update();
334}
335
336void KPlotWidget::setForegroundColor(const QColor &fg)
337{
338 d->cForeground = fg;
339 update();
340}
341
342void KPlotWidget::setGridColor(const QColor &gc)
343{
344 d->cGrid = gc;
345 update();
346}
347
348bool KPlotWidget::isGridShown() const
349{
350 return d->showGrid;
351}
352
353bool KPlotWidget::isObjectToolTipShown() const
354{
355 return d->showObjectToolTip;
356}
357
358bool KPlotWidget::antialiasing() const
359{
360 return d->useAntialias;
361}
362
363void KPlotWidget::setAntialiasing(bool b)
364{
365 d->useAntialias = b;
366 update();
367}
368
369void KPlotWidget::setShowGrid(bool show)
370{
371 d->showGrid = show;
372 update();
373}
374
375void KPlotWidget::setObjectToolTipShown(bool show)
376{
377 d->showObjectToolTip = show;
378}
379
380KPlotAxis *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
386const 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
392QRect KPlotWidget::pixRect() const
393{
394 return d->pixRect;
395}
396
397QList<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
412bool 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
429void KPlotWidget::resizeEvent(QResizeEvent *e)
430{
431 QFrame::resizeEvent(event: e);
432 setPixRect();
433 resetPlotMask();
434}
435
436void 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
444QPointF 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
451void 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
466void 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.
529void 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
695float 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
713void 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
743void 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
954int 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
966int 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
978int 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
990int 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
1002void KPlotWidget::setLeftPadding(int padding)
1003{
1004 d->leftPadding = padding;
1005}
1006
1007void KPlotWidget::setRightPadding(int padding)
1008{
1009 d->rightPadding = padding;
1010}
1011
1012void KPlotWidget::setTopPadding(int padding)
1013{
1014 d->topPadding = padding;
1015}
1016
1017void KPlotWidget::setBottomPadding(int padding)
1018{
1019 d->bottomPadding = padding;
1020}
1021
1022void 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

source code of kplotting/src/kplotwidget.cpp