File indexing completed on 2024-04-28 15:29:30

0001 /*  -*- C++ -*-
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2003 Jason Harris <kstars@30doradus.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "kplotwidget.h"
0009 
0010 #include <math.h>
0011 
0012 #include <QHash>
0013 #include <QHelpEvent>
0014 #include <QPainter>
0015 #include <QToolTip>
0016 #include <QtAlgorithms>
0017 
0018 #include "kplotaxis.h"
0019 #include "kplotobject.h"
0020 #include "kplotpoint.h"
0021 
0022 #define XPADDING 20
0023 #define YPADDING 20
0024 #define BIGTICKSIZE 10
0025 #define SMALLTICKSIZE 4
0026 #define TICKOFFSET 0
0027 
0028 class Q_DECL_HIDDEN KPlotWidget::Private
0029 {
0030 public:
0031     Private(KPlotWidget *qq)
0032         : q(qq)
0033         , cBackground(Qt::black)
0034         , cForeground(Qt::white)
0035         , cGrid(Qt::gray)
0036         , showGrid(false)
0037         , showObjectToolTip(true)
0038         , useAntialias(false)
0039         , autoDelete(true)
0040     {
0041         // create the axes and setting their default properties
0042         KPlotAxis *leftAxis = new KPlotAxis();
0043         leftAxis->setTickLabelsShown(true);
0044         axes.insert(LeftAxis, leftAxis);
0045         KPlotAxis *bottomAxis = new KPlotAxis();
0046         bottomAxis->setTickLabelsShown(true);
0047         axes.insert(BottomAxis, bottomAxis);
0048         KPlotAxis *rightAxis = new KPlotAxis();
0049         axes.insert(RightAxis, rightAxis);
0050         KPlotAxis *topAxis = new KPlotAxis();
0051         axes.insert(TopAxis, topAxis);
0052     }
0053 
0054     ~Private()
0055     {
0056         if (autoDelete) {
0057             qDeleteAll(objectList);
0058         }
0059         qDeleteAll(axes);
0060     }
0061 
0062     KPlotWidget *q;
0063 
0064     void calcDataRectLimits(double x1, double x2, double y1, double y2);
0065     /**
0066      * @return a value indicating how well the given rectangle is
0067      * avoiding masked regions in the plot.  A higher returned value
0068      * indicates that the rectangle is intersecting a larger portion
0069      * of the masked region, or a portion of the masked region which
0070      * is weighted higher.
0071      * @param r The rectangle to be tested
0072      */
0073     float rectCost(const QRectF &r) const;
0074 
0075     // Colors
0076     QColor cBackground, cForeground, cGrid;
0077     // draw options
0078     bool showGrid;
0079     bool showObjectToolTip;
0080     bool useAntialias;
0081     bool autoDelete;
0082     // padding
0083     int leftPadding, rightPadding, topPadding, bottomPadding;
0084     // hashmap with the axes we have
0085     QHash<Axis, KPlotAxis *> axes;
0086     // List of KPlotObjects
0087     QList<KPlotObject *> objectList;
0088     // Limits of the plot area in data units
0089     QRectF dataRect, secondDataRect;
0090     // Limits of the plot area in pixel units
0091     QRect pixRect;
0092     // Array holding the mask of "used" regions of the plot
0093     QImage plotMask;
0094 };
0095 
0096 KPlotWidget::KPlotWidget(QWidget *parent)
0097     : QFrame(parent)
0098     , d(new Private(this))
0099 {
0100     setAttribute(Qt::WA_OpaquePaintEvent);
0101     setAttribute(Qt::WA_NoSystemBackground);
0102 
0103     d->secondDataRect = QRectF(); // default: no secondary data rect
0104     // sets the default limits
0105     d->calcDataRectLimits(0.0, 1.0, 0.0, 1.0);
0106 
0107     setDefaultPaddings();
0108 }
0109 
0110 KPlotWidget::~KPlotWidget()
0111 {
0112     delete d;
0113 }
0114 
0115 QSize KPlotWidget::minimumSizeHint() const
0116 {
0117     return QSize(150, 150);
0118 }
0119 
0120 QSize KPlotWidget::sizeHint() const
0121 {
0122     return size();
0123 }
0124 
0125 void KPlotWidget::setLimits(double x1, double x2, double y1, double y2)
0126 {
0127     d->calcDataRectLimits(x1, x2, y1, y2);
0128     update();
0129 }
0130 
0131 void KPlotWidget::Private::calcDataRectLimits(double x1, double x2, double y1, double y2)
0132 {
0133     double XA1;
0134     double XA2;
0135     double YA1;
0136     double YA2;
0137     if (x2 < x1) {
0138         XA1 = x2;
0139         XA2 = x1;
0140     } else {
0141         XA1 = x1;
0142         XA2 = x2;
0143     }
0144     if (y2 < y1) {
0145         YA1 = y2;
0146         YA2 = y1;
0147     } else {
0148         YA1 = y1;
0149         YA2 = y2;
0150     }
0151 
0152     if (XA2 == XA1) {
0153         // qWarning() << "x1 and x2 cannot be equal. Setting x2 = x1 + 1.0";
0154         XA2 = XA1 + 1.0;
0155     }
0156     if (YA2 == YA1) {
0157         // qWarning() << "y1 and y2 cannot be equal. Setting y2 = y1 + 1.0";
0158         YA2 = YA1 + 1.0;
0159     }
0160     dataRect = QRectF(XA1, YA1, XA2 - XA1, YA2 - YA1);
0161 
0162     q->axis(LeftAxis)->setTickMarks(dataRect.y(), dataRect.height());
0163     q->axis(BottomAxis)->setTickMarks(dataRect.x(), dataRect.width());
0164 
0165     if (secondDataRect.isNull()) {
0166         q->axis(RightAxis)->setTickMarks(dataRect.y(), dataRect.height());
0167         q->axis(TopAxis)->setTickMarks(dataRect.x(), dataRect.width());
0168     }
0169 }
0170 
0171 void KPlotWidget::setSecondaryLimits(double x1, double x2, double y1, double y2)
0172 {
0173     double XA1;
0174     double XA2;
0175     double YA1;
0176     double YA2;
0177     if (x2 < x1) {
0178         XA1 = x2;
0179         XA2 = x1;
0180     } else {
0181         XA1 = x1;
0182         XA2 = x2;
0183     }
0184     if (y2 < y1) {
0185         YA1 = y2;
0186         YA2 = y1;
0187     } else {
0188         YA1 = y1;
0189         YA2 = y2;
0190     }
0191 
0192     if (XA2 == XA1) {
0193         // qWarning() << "x1 and x2 cannot be equal. Setting x2 = x1 + 1.0";
0194         XA2 = XA1 + 1.0;
0195     }
0196     if (YA2 == YA1) {
0197         // qWarning() << "y1 and y2 cannot be equal. Setting y2 = y1 + 1.0";
0198         YA2 = YA1 + 1.0;
0199     }
0200     d->secondDataRect = QRectF(XA1, YA1, XA2 - XA1, YA2 - YA1);
0201 
0202     axis(RightAxis)->setTickMarks(d->secondDataRect.y(), d->secondDataRect.height());
0203     axis(TopAxis)->setTickMarks(d->secondDataRect.x(), d->secondDataRect.width());
0204 
0205     update();
0206 }
0207 
0208 void KPlotWidget::clearSecondaryLimits()
0209 {
0210     d->secondDataRect = QRectF();
0211     axis(RightAxis)->setTickMarks(d->dataRect.y(), d->dataRect.height());
0212     axis(TopAxis)->setTickMarks(d->dataRect.x(), d->dataRect.width());
0213 
0214     update();
0215 }
0216 
0217 QRectF KPlotWidget::dataRect() const
0218 {
0219     return d->dataRect;
0220 }
0221 
0222 QRectF KPlotWidget::secondaryDataRect() const
0223 {
0224     return d->secondDataRect;
0225 }
0226 
0227 void KPlotWidget::addPlotObject(KPlotObject *object)
0228 {
0229     // skip null pointers
0230     if (!object) {
0231         return;
0232     }
0233     d->objectList.append(object);
0234     update();
0235 }
0236 
0237 void KPlotWidget::addPlotObjects(const QList<KPlotObject *> &objects)
0238 {
0239     bool addedsome = false;
0240     for (KPlotObject *o : objects) {
0241         if (!o) {
0242             continue;
0243         }
0244 
0245         d->objectList.append(o);
0246         addedsome = true;
0247     }
0248     if (addedsome) {
0249         update();
0250     }
0251 }
0252 
0253 QList<KPlotObject *> KPlotWidget::plotObjects() const
0254 {
0255     return d->objectList;
0256 }
0257 
0258 void KPlotWidget::setAutoDeletePlotObjects(bool autoDelete)
0259 {
0260     d->autoDelete = autoDelete;
0261 }
0262 
0263 void KPlotWidget::removeAllPlotObjects()
0264 {
0265     if (d->objectList.isEmpty()) {
0266         return;
0267     }
0268 
0269     if (d->autoDelete) {
0270         qDeleteAll(d->objectList);
0271     }
0272     d->objectList.clear();
0273     update();
0274 }
0275 
0276 void KPlotWidget::resetPlotMask()
0277 {
0278     d->plotMask = QImage(pixRect().size(), QImage::Format_ARGB32);
0279     QColor fillColor = Qt::black;
0280     fillColor.setAlpha(128);
0281     d->plotMask.fill(fillColor.rgb());
0282 }
0283 
0284 void KPlotWidget::resetPlot()
0285 {
0286     if (d->autoDelete) {
0287         qDeleteAll(d->objectList);
0288     }
0289     d->objectList.clear();
0290     clearSecondaryLimits();
0291     d->calcDataRectLimits(0.0, 1.0, 0.0, 1.0);
0292     KPlotAxis *a = axis(RightAxis);
0293     a->setLabel(QString());
0294     a->setTickLabelsShown(false);
0295     a = axis(TopAxis);
0296     a->setLabel(QString());
0297     a->setTickLabelsShown(false);
0298     axis(KPlotWidget::LeftAxis)->setLabel(QString());
0299     axis(KPlotWidget::BottomAxis)->setLabel(QString());
0300     resetPlotMask();
0301 }
0302 
0303 void KPlotWidget::replacePlotObject(int i, KPlotObject *o)
0304 {
0305     // skip null pointers and invalid indexes
0306     if (!o || i < 0 || i >= d->objectList.count()) {
0307         return;
0308     }
0309     if (d->objectList.at(i) == o) {
0310         return;
0311     }
0312     if (d->autoDelete) {
0313         delete d->objectList.at(i);
0314     }
0315     d->objectList.replace(i, o);
0316     update();
0317 }
0318 
0319 QColor KPlotWidget::backgroundColor() const
0320 {
0321     return d->cBackground;
0322 }
0323 
0324 QColor KPlotWidget::foregroundColor() const
0325 {
0326     return d->cForeground;
0327 }
0328 
0329 QColor KPlotWidget::gridColor() const
0330 {
0331     return d->cGrid;
0332 }
0333 
0334 void KPlotWidget::setBackgroundColor(const QColor &bg)
0335 {
0336     d->cBackground = bg;
0337     update();
0338 }
0339 
0340 void KPlotWidget::setForegroundColor(const QColor &fg)
0341 {
0342     d->cForeground = fg;
0343     update();
0344 }
0345 
0346 void KPlotWidget::setGridColor(const QColor &gc)
0347 {
0348     d->cGrid = gc;
0349     update();
0350 }
0351 
0352 bool KPlotWidget::isGridShown() const
0353 {
0354     return d->showGrid;
0355 }
0356 
0357 bool KPlotWidget::isObjectToolTipShown() const
0358 {
0359     return d->showObjectToolTip;
0360 }
0361 
0362 bool KPlotWidget::antialiasing() const
0363 {
0364     return d->useAntialias;
0365 }
0366 
0367 void KPlotWidget::setAntialiasing(bool b)
0368 {
0369     d->useAntialias = b;
0370     update();
0371 }
0372 
0373 void KPlotWidget::setShowGrid(bool show)
0374 {
0375     d->showGrid = show;
0376     update();
0377 }
0378 
0379 void KPlotWidget::setObjectToolTipShown(bool show)
0380 {
0381     d->showObjectToolTip = show;
0382 }
0383 
0384 KPlotAxis *KPlotWidget::axis(Axis type)
0385 {
0386     QHash<Axis, KPlotAxis *>::Iterator it = d->axes.find(type);
0387     return it != d->axes.end() ? it.value() : nullptr;
0388 }
0389 
0390 const KPlotAxis *KPlotWidget::axis(Axis type) const
0391 {
0392     QHash<Axis, KPlotAxis *>::ConstIterator it = d->axes.constFind(type);
0393     return it != d->axes.constEnd() ? it.value() : nullptr;
0394 }
0395 
0396 QRect KPlotWidget::pixRect() const
0397 {
0398     return d->pixRect;
0399 }
0400 
0401 QList<KPlotPoint *> KPlotWidget::pointsUnderPoint(const QPoint &p) const
0402 {
0403     QList<KPlotPoint *> pts;
0404     for (const KPlotObject *po : std::as_const(d->objectList)) {
0405         const auto pointsList = po->points();
0406         for (KPlotPoint *pp : pointsList) {
0407             if ((p - mapToWidget(pp->position()).toPoint()).manhattanLength() <= 4) {
0408                 pts << pp;
0409             }
0410         }
0411     }
0412 
0413     return pts;
0414 }
0415 
0416 bool KPlotWidget::event(QEvent *e)
0417 {
0418     if (e->type() == QEvent::ToolTip) {
0419         if (d->showObjectToolTip) {
0420             QHelpEvent *he = static_cast<QHelpEvent *>(e);
0421             QList<KPlotPoint *> pts = pointsUnderPoint(he->pos() - QPoint(leftPadding(), topPadding()) - contentsRect().topLeft());
0422             if (!pts.isEmpty()) {
0423                 QToolTip::showText(he->globalPos(), pts.front()->label(), this);
0424             }
0425         }
0426         e->accept();
0427         return true;
0428     } else {
0429         return QFrame::event(e);
0430     }
0431 }
0432 
0433 void KPlotWidget::resizeEvent(QResizeEvent *e)
0434 {
0435     QFrame::resizeEvent(e);
0436     setPixRect();
0437     resetPlotMask();
0438 }
0439 
0440 void KPlotWidget::setPixRect()
0441 {
0442     int newWidth = contentsRect().width() - leftPadding() - rightPadding();
0443     int newHeight = contentsRect().height() - topPadding() - bottomPadding();
0444     // PixRect starts at (0,0) because we will translate by leftPadding(), topPadding()
0445     d->pixRect = QRect(0, 0, newWidth, newHeight);
0446 }
0447 
0448 QPointF KPlotWidget::mapToWidget(const QPointF &p) const
0449 {
0450     float px = d->pixRect.left() + d->pixRect.width() * (p.x() - d->dataRect.x()) / d->dataRect.width();
0451     float py = d->pixRect.top() + d->pixRect.height() * (d->dataRect.y() + d->dataRect.height() - p.y()) / d->dataRect.height();
0452     return QPointF(px, py);
0453 }
0454 
0455 void KPlotWidget::maskRect(const QRectF &rf, float fvalue)
0456 {
0457     QRect r = rf.toRect().intersected(d->pixRect);
0458     int value = int(fvalue);
0459     QColor newColor;
0460     for (int ix = r.left(); ix < r.right(); ++ix) {
0461         for (int iy = r.top(); iy < r.bottom(); ++iy) {
0462             newColor = QColor(d->plotMask.pixel(ix, iy));
0463             newColor.setAlpha(200);
0464             newColor.setRed(qMin(newColor.red() + value, 255));
0465             d->plotMask.setPixel(ix, iy, newColor.rgba());
0466         }
0467     }
0468 }
0469 
0470 void KPlotWidget::maskAlongLine(const QPointF &p1, const QPointF &p2, float fvalue)
0471 {
0472     if (!d->pixRect.contains(p1.toPoint()) && !d->pixRect.contains(p2.toPoint())) {
0473         return;
0474     }
0475 
0476     int value = int(fvalue);
0477 
0478     // Determine slope and zeropoint of line
0479     double m = (p2.y() - p1.y()) / (p2.x() - p1.x());
0480     double y0 = p1.y() - m * p1.x();
0481     QColor newColor;
0482 
0483     // Mask each pixel along the line joining p1 and p2
0484     if (m > 1.0 || m < -1.0) { // step in y-direction
0485         int y1 = int(p1.y());
0486         int y2 = int(p2.y());
0487         if (y1 > y2) {
0488             y1 = int(p2.y());
0489             y2 = int(p1.y());
0490         }
0491 
0492         for (int y = y1; y <= y2; ++y) {
0493             int x = int((y - y0) / m);
0494             if (d->pixRect.contains(x, y)) {
0495                 newColor = QColor(d->plotMask.pixel(x, y));
0496                 newColor.setAlpha(100);
0497                 newColor.setRed(qMin(newColor.red() + value, 255));
0498                 d->plotMask.setPixel(x, y, newColor.rgba());
0499             }
0500         }
0501 
0502     } else { // step in x-direction
0503         int x1 = int(p1.x());
0504         int x2 = int(p2.x());
0505         if (x1 > x2) {
0506             x1 = int(p2.x());
0507             x2 = int(p1.x());
0508         }
0509 
0510         for (int x = x1; x <= x2; ++x) {
0511             int y = int(y0 + m * x);
0512             if (d->pixRect.contains(x, y)) {
0513                 newColor = QColor(d->plotMask.pixel(x, y));
0514                 newColor.setAlpha(100);
0515                 newColor.setRed(qMin(newColor.red() + value, 255));
0516                 d->plotMask.setPixel(x, y, newColor.rgba());
0517             }
0518         }
0519     }
0520 }
0521 
0522 // Determine optimal placement for a text label for point pp.  We want
0523 // the label to be near point pp, but we don't want it to overlap with
0524 // other labels or plot elements.  We will use a "downhill simplex"
0525 // algorithm to find a label position that minimizes the pixel values
0526 // in the plotMask image over the label's rect().  The sum of pixel
0527 // values in the label's rect is the "cost" of placing the label there.
0528 //
0529 // Because a downhill simplex follows the local gradient to find low
0530 // values, it can get stuck in local minima.  To mitigate this, we will
0531 // iteratively attempt each of the initial path offset directions (up,
0532 // down, right, left) in the order of increasing cost at each location.
0533 void KPlotWidget::placeLabel(QPainter *painter, KPlotPoint *pp)
0534 {
0535     int textFlags = Qt::TextSingleLine | Qt::AlignCenter;
0536 
0537     QPointF pos = mapToWidget(pp->position());
0538     if (!d->pixRect.contains(pos.toPoint())) {
0539         return;
0540     }
0541 
0542     QFontMetricsF fm(painter->font(), painter->device());
0543     QRectF bestRect = fm.boundingRect(QRectF(pos.x(), pos.y(), 1, 1), textFlags, pp->label());
0544     float xStep = 0.5 * bestRect.width();
0545     float yStep = 0.5 * bestRect.height();
0546     float maxCost = 0.05 * bestRect.width() * bestRect.height();
0547     float bestCost = d->rectCost(bestRect);
0548 
0549     // We will travel along a path defined by the maximum decrease in
0550     // the cost at each step.  If this path takes us to a local minimum
0551     // whose cost exceeds maxCost, then we will restart at the
0552     // beginning and select the next-best path.  The indices of
0553     // already-tried paths are stored in the TriedPathIndex list.
0554     //
0555     // If we try all four first-step paths and still don't get below
0556     // maxCost, then we'll adopt the local minimum position with the
0557     // best cost (designated as bestBadCost).
0558     int iter = 0;
0559     QList<int> TriedPathIndex;
0560     float bestBadCost = 10000;
0561     QRectF bestBadRect;
0562 
0563     // needed to halt iteration from inside the switch
0564     bool flagStop = false;
0565 
0566     while (bestCost > maxCost) {
0567         // Displace the label up, down, left, right; determine which
0568         // step provides the lowest cost
0569         QRectF upRect = bestRect;
0570         upRect.moveTop(upRect.top() + yStep);
0571         float upCost = d->rectCost(upRect);
0572         QRectF downRect = bestRect;
0573         downRect.moveTop(downRect.top() - yStep);
0574         float downCost = d->rectCost(downRect);
0575         QRectF leftRect = bestRect;
0576         leftRect.moveLeft(leftRect.left() - xStep);
0577         float leftCost = d->rectCost(leftRect);
0578         QRectF rightRect = bestRect;
0579         rightRect.moveLeft(rightRect.left() + xStep);
0580         float rightCost = d->rectCost(rightRect);
0581 
0582         // which direction leads to the lowest cost?
0583         QList<float> costList;
0584         costList << upCost << downCost << leftCost << rightCost;
0585         int imin = -1;
0586         for (int i = 0; i < costList.size(); ++i) {
0587             if (iter == 0 && TriedPathIndex.contains(i)) {
0588                 continue; // Skip this first-step path, we already tried it!
0589             }
0590 
0591             // If this first-step path doesn't improve the cost,
0592             // skip this direction from now on
0593             if (iter == 0 && costList[i] >= bestCost) {
0594                 TriedPathIndex.append(i);
0595                 continue;
0596             }
0597 
0598             if (costList[i] < bestCost && (imin < 0 || costList[i] < costList[imin])) {
0599                 imin = i;
0600             }
0601         }
0602 
0603         // Make a note that we've tried the current first-step path
0604         if (iter == 0 && imin >= 0) {
0605             TriedPathIndex.append(imin);
0606         }
0607 
0608         // Adopt the step that produced the best cost
0609         switch (imin) {
0610         case 0: // up
0611             bestRect.moveTop(upRect.top());
0612             bestCost = upCost;
0613             break;
0614         case 1: // down
0615             bestRect.moveTop(downRect.top());
0616             bestCost = downCost;
0617             break;
0618         case 2: // left
0619             bestRect.moveLeft(leftRect.left());
0620             bestCost = leftCost;
0621             break;
0622         case 3: // right
0623             bestRect.moveLeft(rightRect.left());
0624             bestCost = rightCost;
0625             break;
0626         case -1: // no lower cost found!
0627             // We hit a local minimum.  Keep the best of these as bestBadRect
0628             if (bestCost < bestBadCost) {
0629                 bestBadCost = bestCost;
0630                 bestBadRect = bestRect;
0631             }
0632 
0633             // If all of the first-step paths have now been searched, we'll
0634             // have to adopt the bestBadRect
0635             if (TriedPathIndex.size() == 4) {
0636                 bestRect = bestBadRect;
0637                 flagStop = true; // halt iteration
0638                 break;
0639             }
0640 
0641             // If we haven't yet tried all of the first-step paths, start over
0642             if (TriedPathIndex.size() < 4) {
0643                 iter = -1; // anticipating the ++iter below
0644                 bestRect = fm.boundingRect(QRectF(pos.x(), pos.y(), 1, 1), textFlags, pp->label());
0645                 bestCost = d->rectCost(bestRect);
0646             }
0647             break;
0648         }
0649 
0650         // Halt iteration, because we've tried all directions and
0651         // haven't gotten below maxCost (we'll adopt the best
0652         // local minimum found)
0653         if (flagStop) {
0654             break;
0655         }
0656 
0657         ++iter;
0658     }
0659 
0660     painter->drawText(bestRect, textFlags, pp->label());
0661 
0662     // Is a line needed to connect the label to the point?
0663     float deltax = pos.x() - bestRect.center().x();
0664     float deltay = pos.y() - bestRect.center().y();
0665     float rbest = sqrt(deltax * deltax + deltay * deltay);
0666     if (rbest > 20.0) {
0667         // Draw a rectangle around the label
0668         painter->setBrush(QBrush());
0669         // QPen pen = painter->pen();
0670         // pen.setStyle( Qt::DotLine );
0671         // painter->setPen( pen );
0672         painter->drawRoundedRect(bestRect, 25, 25, Qt::RelativeSize);
0673 
0674         // Now connect the label to the point with a line.
0675         // The line is drawn from the center of the near edge of the rectangle
0676         float xline = bestRect.center().x();
0677         if (bestRect.left() > pos.x()) {
0678             xline = bestRect.left();
0679         }
0680         if (bestRect.right() < pos.x()) {
0681             xline = bestRect.right();
0682         }
0683 
0684         float yline = bestRect.center().y();
0685         if (bestRect.top() > pos.y()) {
0686             yline = bestRect.top();
0687         }
0688         if (bestRect.bottom() < pos.y()) {
0689             yline = bestRect.bottom();
0690         }
0691 
0692         painter->drawLine(QPointF(xline, yline), pos);
0693     }
0694 
0695     // Mask the label's rectangle so other labels won't overlap it.
0696     maskRect(bestRect);
0697 }
0698 
0699 float KPlotWidget::Private::rectCost(const QRectF &r) const
0700 {
0701     if (!plotMask.rect().contains(r.toRect())) {
0702         return 10000.;
0703     }
0704 
0705     // Compute sum of mask values in the rect r
0706     QImage subMask = plotMask.copy(r.toRect());
0707     int cost = 0;
0708     for (int ix = 0; ix < subMask.width(); ++ix) {
0709         for (int iy = 0; iy < subMask.height(); ++iy) {
0710             cost += QColor(subMask.pixel(ix, iy)).red();
0711         }
0712     }
0713 
0714     return float(cost);
0715 }
0716 
0717 void KPlotWidget::paintEvent(QPaintEvent *e)
0718 {
0719     // let QFrame draw its default stuff (like the frame)
0720     QFrame::paintEvent(e);
0721     QPainter p;
0722 
0723     p.begin(this);
0724     p.setRenderHint(QPainter::Antialiasing, d->useAntialias);
0725     p.fillRect(rect(), backgroundColor());
0726     p.translate(leftPadding() + 0.5, topPadding() + 0.5);
0727 
0728     setPixRect();
0729     p.setClipRect(d->pixRect);
0730     p.setClipping(true);
0731 
0732     resetPlotMask();
0733 
0734     for (KPlotObject *po : std::as_const(d->objectList)) {
0735         po->draw(&p, this);
0736     }
0737 
0738     // DEBUG: Draw the plot mask
0739     //    p.drawImage( 0, 0, d->plotMask );
0740 
0741     p.setClipping(false);
0742     drawAxes(&p);
0743 
0744     p.end();
0745 }
0746 
0747 void KPlotWidget::drawAxes(QPainter *p)
0748 {
0749     if (d->showGrid) {
0750         p->setPen(gridColor());
0751 
0752         // Grid lines are placed at locations of primary axes' major tickmarks
0753         // vertical grid lines
0754         const QList<double> majMarks = axis(BottomAxis)->majorTickMarks();
0755         for (const double xx : majMarks) {
0756             double px = d->pixRect.width() * (xx - d->dataRect.x()) / d->dataRect.width();
0757             p->drawLine(QPointF(px, 0.0), QPointF(px, double(d->pixRect.height())));
0758         }
0759         // horizontal grid lines
0760         const QList<double> leftTickMarks = axis(LeftAxis)->majorTickMarks();
0761         for (const double yy : leftTickMarks) {
0762             double py = d->pixRect.height() * (1.0 - (yy - d->dataRect.y()) / d->dataRect.height());
0763             p->drawLine(QPointF(0.0, py), QPointF(double(d->pixRect.width()), py));
0764         }
0765     }
0766 
0767     p->setPen(foregroundColor());
0768     p->setBrush(Qt::NoBrush);
0769 
0770     // set small font for tick labels
0771     QFont f = p->font();
0772     int s = f.pointSize();
0773     f.setPointSize(s - 2);
0774     p->setFont(f);
0775 
0776     /*** BottomAxis ***/
0777     KPlotAxis *a = axis(BottomAxis);
0778     if (a->isVisible()) {
0779         // Draw axis line
0780         p->drawLine(0, d->pixRect.height(), d->pixRect.width(), d->pixRect.height());
0781 
0782         // Draw major tickmarks
0783         const QList<double> majMarks = a->majorTickMarks();
0784         for (const double xx : majMarks) {
0785             double px = d->pixRect.width() * (xx - d->dataRect.x()) / d->dataRect.width();
0786             if (px > 0 && px < d->pixRect.width()) {
0787                 p->drawLine(QPointF(px, double(d->pixRect.height() - TICKOFFSET)), //
0788                             QPointF(px, double(d->pixRect.height() - BIGTICKSIZE - TICKOFFSET)));
0789 
0790                 // Draw ticklabel
0791                 if (a->areTickLabelsShown()) {
0792                     QRect r(int(px) - BIGTICKSIZE, d->pixRect.height() + BIGTICKSIZE, 2 * BIGTICKSIZE, BIGTICKSIZE);
0793                     p->drawText(r, Qt::AlignCenter | Qt::TextDontClip, a->tickLabel(xx));
0794                 }
0795             }
0796         }
0797 
0798         // Draw minor tickmarks
0799         const QList<double> minTickMarks = a->minorTickMarks();
0800         for (const double xx : minTickMarks) {
0801             double px = d->pixRect.width() * (xx - d->dataRect.x()) / d->dataRect.width();
0802             if (px > 0 && px < d->pixRect.width()) {
0803                 p->drawLine(QPointF(px, double(d->pixRect.height() - TICKOFFSET)), //
0804                             QPointF(px, double(d->pixRect.height() - SMALLTICKSIZE - TICKOFFSET)));
0805             }
0806         }
0807 
0808         // Draw BottomAxis Label
0809         if (!a->label().isEmpty()) {
0810             QRect r(0, d->pixRect.height() + 2 * YPADDING, d->pixRect.width(), YPADDING);
0811             p->drawText(r, Qt::AlignCenter, a->label());
0812         }
0813     } // End of BottomAxis
0814 
0815     /*** LeftAxis ***/
0816     a = axis(LeftAxis);
0817     if (a->isVisible()) {
0818         // Draw axis line
0819         p->drawLine(0, 0, 0, d->pixRect.height());
0820 
0821         // Draw major tickmarks
0822         const QList<double> majMarks = a->majorTickMarks();
0823         for (const double yy : majMarks) {
0824             double py = d->pixRect.height() * (1.0 - (yy - d->dataRect.y()) / d->dataRect.height());
0825             if (py > 0 && py < d->pixRect.height()) {
0826                 p->drawLine(QPointF(TICKOFFSET, py), QPointF(double(TICKOFFSET + BIGTICKSIZE), py));
0827 
0828                 // Draw ticklabel
0829                 if (a->areTickLabelsShown()) {
0830                     QRect r(-2 * BIGTICKSIZE - SMALLTICKSIZE, int(py) - SMALLTICKSIZE, 2 * BIGTICKSIZE, 2 * SMALLTICKSIZE);
0831                     p->drawText(r, Qt::AlignRight | Qt::AlignVCenter | Qt::TextDontClip, a->tickLabel(yy));
0832                 }
0833             }
0834         }
0835 
0836         // Draw minor tickmarks
0837         const QList<double> minTickMarks = a->minorTickMarks();
0838         for (const double yy : minTickMarks) {
0839             double py = d->pixRect.height() * (1.0 - (yy - d->dataRect.y()) / d->dataRect.height());
0840             if (py > 0 && py < d->pixRect.height()) {
0841                 p->drawLine(QPointF(TICKOFFSET, py), QPointF(double(TICKOFFSET + SMALLTICKSIZE), py));
0842             }
0843         }
0844 
0845         // Draw LeftAxis Label.  We need to draw the text sideways.
0846         if (!a->label().isEmpty()) {
0847             // store current painter translation/rotation state
0848             p->save();
0849 
0850             // translate coord sys to left corner of axis label rectangle, then rotate 90 degrees.
0851             p->translate(-3 * XPADDING, d->pixRect.height());
0852             p->rotate(-90.0);
0853 
0854             QRect r(0, 0, d->pixRect.height(), XPADDING);
0855             p->drawText(r, Qt::AlignCenter, a->label()); // draw the label, now that we are sideways
0856 
0857             p->restore(); // restore translation/rotation state
0858         }
0859     } // End of LeftAxis
0860 
0861     // Prepare for top and right axes; we may need the secondary data rect
0862     double x0 = d->dataRect.x();
0863     double y0 = d->dataRect.y();
0864     double dw = d->dataRect.width();
0865     double dh = d->dataRect.height();
0866     if (secondaryDataRect().isValid()) {
0867         x0 = secondaryDataRect().x();
0868         y0 = secondaryDataRect().y();
0869         dw = secondaryDataRect().width();
0870         dh = secondaryDataRect().height();
0871     }
0872 
0873     /*** TopAxis ***/
0874     a = axis(TopAxis);
0875     if (a->isVisible()) {
0876         // Draw axis line
0877         p->drawLine(0, 0, d->pixRect.width(), 0);
0878 
0879         // Draw major tickmarks
0880         const QList<double> majMarks = a->majorTickMarks();
0881         for (const double xx : majMarks) {
0882             double px = d->pixRect.width() * (xx - x0) / dw;
0883             if (px > 0 && px < d->pixRect.width()) {
0884                 p->drawLine(QPointF(px, TICKOFFSET), QPointF(px, double(BIGTICKSIZE + TICKOFFSET)));
0885 
0886                 // Draw ticklabel
0887                 if (a->areTickLabelsShown()) {
0888                     QRect r(int(px) - BIGTICKSIZE, (int)-1.5 * BIGTICKSIZE, 2 * BIGTICKSIZE, BIGTICKSIZE);
0889                     p->drawText(r, Qt::AlignCenter | Qt::TextDontClip, a->tickLabel(xx));
0890                 }
0891             }
0892         }
0893 
0894         // Draw minor tickmarks
0895         const QList<double> minMarks = a->minorTickMarks();
0896         for (const double xx : minMarks) {
0897             double px = d->pixRect.width() * (xx - x0) / dw;
0898             if (px > 0 && px < d->pixRect.width()) {
0899                 p->drawLine(QPointF(px, TICKOFFSET), QPointF(px, double(SMALLTICKSIZE + TICKOFFSET)));
0900             }
0901         }
0902 
0903         // Draw TopAxis Label
0904         if (!a->label().isEmpty()) {
0905             QRect r(0, 0 - 3 * YPADDING, d->pixRect.width(), YPADDING);
0906             p->drawText(r, Qt::AlignCenter, a->label());
0907         }
0908     } // End of TopAxis
0909 
0910     /*** RightAxis ***/
0911     a = axis(RightAxis);
0912     if (a->isVisible()) {
0913         // Draw axis line
0914         p->drawLine(d->pixRect.width(), 0, d->pixRect.width(), d->pixRect.height());
0915 
0916         // Draw major tickmarks
0917         const QList<double> majMarks = a->majorTickMarks();
0918         for (const double yy : majMarks) {
0919             double py = d->pixRect.height() * (1.0 - (yy - y0) / dh);
0920             if (py > 0 && py < d->pixRect.height()) {
0921                 p->drawLine(QPointF(double(d->pixRect.width() - TICKOFFSET), py), //
0922                             QPointF(double(d->pixRect.width() - TICKOFFSET - BIGTICKSIZE), py));
0923 
0924                 // Draw ticklabel
0925                 if (a->areTickLabelsShown()) {
0926                     QRect r(d->pixRect.width() + SMALLTICKSIZE, int(py) - SMALLTICKSIZE, 2 * BIGTICKSIZE, 2 * SMALLTICKSIZE);
0927                     p->drawText(r, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextDontClip, a->tickLabel(yy));
0928                 }
0929             }
0930         }
0931 
0932         // Draw minor tickmarks
0933         const QList<double> minMarks = a->minorTickMarks();
0934         for (const double yy : minMarks) {
0935             double py = d->pixRect.height() * (1.0 - (yy - y0) / dh);
0936             if (py > 0 && py < d->pixRect.height()) {
0937                 p->drawLine(QPointF(double(d->pixRect.width() - 0.0), py), QPointF(double(d->pixRect.width() - 0.0 - SMALLTICKSIZE), py));
0938             }
0939         }
0940 
0941         // Draw RightAxis Label.  We need to draw the text sideways.
0942         if (!a->label().isEmpty()) {
0943             // store current painter translation/rotation state
0944             p->save();
0945 
0946             // translate coord sys to left corner of axis label rectangle, then rotate 90 degrees.
0947             p->translate(d->pixRect.width() + 2 * XPADDING, d->pixRect.height());
0948             p->rotate(-90.0);
0949 
0950             QRect r(0, 0, d->pixRect.height(), XPADDING);
0951             p->drawText(r, Qt::AlignCenter, a->label()); // draw the label, now that we are sideways
0952 
0953             p->restore(); // restore translation/rotation state
0954         }
0955     } // End of RightAxis
0956 }
0957 
0958 int KPlotWidget::leftPadding() const
0959 {
0960     if (d->leftPadding >= 0) {
0961         return d->leftPadding;
0962     }
0963     const KPlotAxis *a = axis(LeftAxis);
0964     if (a && a->isVisible() && a->areTickLabelsShown()) {
0965         return !a->label().isEmpty() ? 3 * XPADDING : 2 * XPADDING;
0966     }
0967     return XPADDING;
0968 }
0969 
0970 int KPlotWidget::rightPadding() const
0971 {
0972     if (d->rightPadding >= 0) {
0973         return d->rightPadding;
0974     }
0975     const KPlotAxis *a = axis(RightAxis);
0976     if (a && a->isVisible() && a->areTickLabelsShown()) {
0977         return !a->label().isEmpty() ? 3 * XPADDING : 2 * XPADDING;
0978     }
0979     return XPADDING;
0980 }
0981 
0982 int KPlotWidget::topPadding() const
0983 {
0984     if (d->topPadding >= 0) {
0985         return d->topPadding;
0986     }
0987     const KPlotAxis *a = axis(TopAxis);
0988     if (a && a->isVisible() && a->areTickLabelsShown()) {
0989         return !a->label().isEmpty() ? 3 * YPADDING : 2 * YPADDING;
0990     }
0991     return YPADDING;
0992 }
0993 
0994 int KPlotWidget::bottomPadding() const
0995 {
0996     if (d->bottomPadding >= 0) {
0997         return d->bottomPadding;
0998     }
0999     const KPlotAxis *a = axis(BottomAxis);
1000     if (a && a->isVisible() && a->areTickLabelsShown()) {
1001         return !a->label().isEmpty() ? 3 * YPADDING : 2 * YPADDING;
1002     }
1003     return YPADDING;
1004 }
1005 
1006 void KPlotWidget::setLeftPadding(int padding)
1007 {
1008     d->leftPadding = padding;
1009 }
1010 
1011 void KPlotWidget::setRightPadding(int padding)
1012 {
1013     d->rightPadding = padding;
1014 }
1015 
1016 void KPlotWidget::setTopPadding(int padding)
1017 {
1018     d->topPadding = padding;
1019 }
1020 
1021 void KPlotWidget::setBottomPadding(int padding)
1022 {
1023     d->bottomPadding = padding;
1024 }
1025 
1026 void KPlotWidget::setDefaultPaddings()
1027 {
1028     d->leftPadding = -1;
1029     d->rightPadding = -1;
1030     d->topPadding = -1;
1031     d->bottomPadding = -1;
1032 }
1033 
1034 #include "moc_kplotwidget.cpp"