File indexing completed on 2024-04-28 05:31:45

0001 /*
0002     SPDX-FileCopyrightText: 2006-2009 John Tapsell <tapsell@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #ifndef KGRAPHICSSIGNALPLOTTER_H
0008 #define KGRAPHICSSIGNALPLOTTER_H
0009 
0010 #include <QColor>
0011 #include <QFont>
0012 #include <QGraphicsWidget>
0013 #include <QList>
0014 #include <QString>
0015 #include <klocalizedstring.h>
0016 
0017 #include "ksignalplotter_export.h"
0018 
0019 class QGraphicsSceneResizeEvent;
0020 class KGraphicsSignalPlotterPrivate;
0021 
0022 // Make sure only declare KLocalizedString once
0023 #ifndef KSIGNALPLOTTER_H
0024 Q_DECLARE_METATYPE(KLocalizedString)
0025 #endif
0026 
0027 /** \class KGraphicsSignalPlotter
0028  * \brief The KGraphicsSignalPlotter graphics widget draws a real time graph of data that updates continually.
0029  *
0030  *  Features include:
0031  *  \li Points are joined by a bezier curve.
0032  *  \li Lines are anti-aliased
0033  *  \li Background can be set as a specified SVG
0034  *  \li The lines can be reordered
0035  *  \li Uses as little memory and CPU as possible
0036  *  \li Graph can be smoothed using the formula (value * 2 + last_value)/3
0037  *  \li Can cope with positive/negative infinity and NaN values.
0038  *
0039  *  Example usage:
0040  *  \code
0041  *    KGraphicsSignalPlotter *s = KGraphicsSignalPlotter(parent);
0042  *    s->addBeam(Qt::blue);
0043  *    s->addBeam(Qt::green);
0044  *    QList<qreal> data;
0045  *    data << 4.0 << 5.0;
0046  *    s->addSample(data);
0047  *  \endcode
0048  *
0049  *  Note that the number of horizontal lines is calculated automatically based on the axis font size, even if the axis labels are not shown.
0050  *
0051  *  Smoothing looks very nice visually and is enabled by default.  It can be disabled with setSmoothGraph().
0052  *
0053  *  \image KSignalPlotter.png  Example KGraphicsSignalPlotter with two beams
0054  */
0055 class KSIGNALPLOTTER_EXPORT KGraphicsSignalPlotter : public QGraphicsWidget
0056 {
0057     Q_OBJECT
0058     Q_PROPERTY(qreal minimumValue READ minimumValue WRITE setMinimumValue)
0059     Q_PROPERTY(qreal maximumValue READ maximumValue WRITE setMaximumValue)
0060     Q_PROPERTY(bool useAutoRange READ useAutoRange WRITE setUseAutoRange)
0061     Q_PROPERTY(KLocalizedString unit READ unit WRITE setUnit)
0062     Q_PROPERTY(qreal scaleDownBy READ scaleDownBy WRITE setScaleDownBy)
0063     Q_PROPERTY(uint horizontalScale READ horizontalScale WRITE setHorizontalScale)
0064     Q_PROPERTY(bool showHorizontalLines READ showHorizontalLines WRITE setShowHorizontalLines)
0065     Q_PROPERTY(bool showVerticalLines READ showVerticalLines WRITE setShowVerticalLines)
0066     Q_PROPERTY(bool verticalLinesScroll READ verticalLinesScroll WRITE setVerticalLinesScroll)
0067     Q_PROPERTY(uint verticalLinesDistance READ verticalLinesDistance WRITE setVerticalLinesDistance)
0068     Q_PROPERTY(bool showAxis READ showAxis WRITE setShowAxis)
0069     Q_PROPERTY(QString svgBackground READ svgBackground WRITE setSvgBackground)
0070     Q_PROPERTY(bool thinFrame READ thinFrame WRITE setThinFrame)
0071     Q_PROPERTY(int maxAxisTextWidth READ maxAxisTextWidth WRITE setMaxAxisTextWidth)
0072     Q_PROPERTY(bool smoothGraph READ smoothGraph WRITE setSmoothGraph)
0073     Q_PROPERTY(bool stackGraph READ stackGraph WRITE setStackGraph)
0074     Q_PROPERTY(int fillOpacity READ fillOpacity WRITE setFillOpacity)
0075 
0076 public:
0077     KGraphicsSignalPlotter(QGraphicsItem *parent = nullptr);
0078     ~KGraphicsSignalPlotter() override;
0079 
0080     /** \brief Add a new line to the graph plotter, with the specified color.
0081      *
0082      *  Note that the order you add the beams in must be the same order that
0083      *  the beam data is given in (Unless you reorder the beams).
0084      *
0085      *  \param color Color of beam - does not have to be unique.
0086      */
0087     void addBeam(const QColor &color);
0088 
0089     /** \brief Add data to the graph, and advance the graph by one time period.
0090      *
0091      *  The data must be given as a list in the same order that the beams were
0092      *  added (or consequently reordered).  If samples.count() != numBeams(),
0093      *  a warning is printed and the data discarded.
0094      *
0095      *  To indicate that no data is available for a given beam, set its value to
0096      *  (non-signalling) NaN.
0097      *
0098      *  For example:
0099      *
0100      *  \code
0101      *    KSignalPlotter *s = KSignalPlotter(parent);
0102      *    s->addBeam(Qt::red);
0103      *    s->addBeam(Qt::green);
0104      *    s->addBeam(Qt::blue);
0105      *    signalPlotter->addSample( QList<qreal>() << std::numeric_limits<qreal>::quiet_NaN() << 1.0/0 << 10.0 );
0106      *  \endcode
0107      *
0108      *  This indicates that no data is available yet for red (so the beam will not be drawn for this section),
0109      *  that's it positive infinity for green, and 10 for blue.
0110      *
0111      *  Infinity is handled by drawing a straight line up to the top or bottom of the display, and does not affect the range.
0112      *  For the above example, the displayed range would now be 0 to 10.
0113      */
0114     void addSample(const QList<qreal> &samples);
0115 
0116     /** \brief Reorder the beams into the order given.
0117      *
0118      * For example:
0119      * \code
0120      *   KSignalPlotter *s = KSignalPlotter(parent);
0121      *   s->addBeam(Qt::blue);
0122      *   s->addBeam(Qt::green);
0123      *   s->addBeam(Qt::red);
0124      *   QList<int> neworder;
0125      *   neworder << 2 << 0 << 1;
0126      *   s->reorderBeams( newOrder);
0127      *   //Now the order is red, blue then green
0128      * \endcode
0129      *
0130      * The size of the \p newOrder list must be equal to the result of numBeams().
0131      * \param newOrder New order of beams.
0132      */
0133     void reorderBeams(const QList<int> &newOrder);
0134 
0135     /** \brief Removes the beam at the specified index.
0136      *
0137      * This causes the graph to be redrawn with the specified beam completely
0138      * removed.
0139      */
0140     void removeBeam(int index);
0141 
0142     /** \brief Get the color of the beam at the specified index.
0143      *
0144      * For example:
0145      * \code
0146      *   KSignalPlotter *s = KSignalPlotter(parent);
0147      *   s->addBeam(Qt::blue);
0148      *   s->addBeam(Qt::green);
0149      *   s->addBeam(Qt::red);
0150      *
0151      *   QColor color = s->beamColor(0);  //returns blue
0152      * \endcode
0153      *
0154      * \sa setBeamColor()
0155      */
0156     QColor beamColor(int index) const;
0157 
0158     /** \brief Set the color of the beam at the specified index.
0159      *
0160      * \sa beamColor()
0161      */
0162     void setBeamColor(int index, const QColor &color);
0163 
0164     /** \brief Returns the number of beams. */
0165     int numBeams() const;
0166 
0167     /** \brief Set the axis units with a localized string.
0168      *
0169      * The localized string must contain a placeholder "%1" which is substituted for the value.
0170      * The plural form (ki18np) can be used if the unit string changes depending on the number (for example
0171      * "1 second", "2 seconds").
0172      *
0173      * For example:
0174      *
0175      * \code
0176      *   KSignalPlotter plotter;
0177      *   plotter.setUnit( ki18ncp("Units", "%1 second", "%1 seconds") );
0178      *   QString formattedString = plotter.valueAsString(3.4); //returns "3.4 seconds"
0179      * \endcode
0180      *
0181      * Typically a new unit would be set when setScaleDownBy is called.
0182      * Note that even the singular should use "%1 second" instead of "1 second", so that a value of -1 works correctly.
0183      *
0184      * \see unit(), setScaleDownBy()
0185      */
0186     void setUnit(const KLocalizedString &unit);
0187 
0188     /** \brief The localizable units used on the vertical axis of the graph.
0189      *
0190      * The returns the localizable string set with setUnit().
0191      *
0192      * Default is the string "%1" - i.e. to just display the number.
0193      *
0194      * \see setUnit
0195      */
0196     KLocalizedString unit() const;
0197 
0198     /** \brief Scale all the values down by the given amount.
0199      *
0200      * This is useful when the data is given in, say, kilobytes, but you set
0201      * the units as megabytes.  Thus you would have to call this with @p value
0202      * set to 1024.  This affects all the data already entered.
0203      *
0204      * Typically this is followed by calling setUnit() to set the display axis
0205      * units.  Default value is 1.
0206      */
0207     void setScaleDownBy(qreal value);
0208 
0209     /** \brief Amount scaled down by.
0210      *
0211      * \sa setScaleDownBy */
0212     qreal scaleDownBy() const;
0213 
0214     /** \brief Set whether to scale the graph automatically beyond the given range.
0215      *
0216      * If true, the range on vertical axis is automatically expanded from the
0217      * data available, expanding beyond the range set by changeRange() if data
0218      * values are outside of this range.
0219      *
0220      * Regardless whether this is set of not, the range of the vertical axis
0221      * will never be less than the range given by maximumValue() and minimumvalue().
0222      *
0223      * \param value Whether to scale beyond the given range. Default is true.
0224      *
0225      * \sa useAutoRange
0226      */
0227     void setUseAutoRange(bool value);
0228 
0229     /** \brief Whether the vertical axis range is set automatically.
0230      */
0231     bool useAutoRange() const;
0232 
0233     /** \brief Change the minimum and maximum values drawn on the graph.
0234      *
0235      *  Note that these values are sanitised.  For example, if you
0236      *  set the minimum as 3, and the maximum as 97, then the graph
0237      *  would be drawn between 0 and 100.  The algorithm to determine
0238      *  this "nice range" attempts to minimize the number of non-zero
0239      *  digits.
0240      *
0241      *  If autoRange() is true, then this range is taking as a 'hint'.
0242      *  The range will never be smaller than the given range, but can grow
0243      *  if there are values larger than the given range.
0244      *
0245      *  This is equivalent to calling
0246      *  \code
0247      *    setMinimumValue(min);
0248      *    setMaximumValue(max);
0249      *  \endcode
0250      *
0251      *  \sa setMinimumValue(), setMaximumValue(), minimumValue(), maximumValue()
0252      */
0253     void changeRange(qreal min, qreal max);
0254 
0255     /** \brief Set the min value hint for the vertical axis.
0256      *
0257      * \sa changeRange(), minimumValue(), setMaximumValue(), maximumValue() */
0258     void setMinimumValue(qreal min);
0259 
0260     /** \brief Get the min value hint for the vertical axis.
0261      *
0262      * \sa changeRange(), minimumValue(), setMaximumValue(), maximumValue() */
0263     qreal minimumValue() const;
0264 
0265     /** \brief Set the max value hint for the vertical axis. *
0266      *
0267      * \sa changeRange(), minimumValue(), setMaximumValue(), maximumValue() */
0268     void setMaximumValue(qreal max);
0269 
0270     /** \brief Get the maximum value hint for the vertical axis.
0271      *
0272      * \sa changeRange(), minimumValue(), setMaximumValue(), maximumValue() */
0273     qreal maximumValue() const;
0274 
0275     /** \brief Get the current maximum value on the y-axis.
0276      *
0277      *  This will never be lower than maximumValue(), and if autoRange() is true,
0278      *  it will be equal or larger (due to rounding up to make it a nice number)
0279      *  than the highest value being shown.
0280      */
0281     qreal currentMaximumRangeValue() const;
0282     /** \brief Get the current minimum value on the y-axis.
0283      *
0284      *  This will never be lower than minimumValue(), and if autoRange() is true,
0285      *  it will be equal or larger (due to rounding up to make it a nice number)
0286      *  than the highest value being shown.
0287      */
0288     qreal currentMinimumRangeValue() const;
0289 
0290     /** \brief Set the number of pixels horizontally between data points.
0291      *  Default is 6. */
0292     void setHorizontalScale(uint scale);
0293     /** \brief The number of pixels horizontally between data points.
0294      *  Default is 6. */
0295     int horizontalScale() const;
0296 
0297     /** \brief Set whether to draw the vertical grid lines.
0298      *  Default is false. */
0299     void setShowVerticalLines(bool value);
0300     /** \brief Whether to draw the vertical grid lines.
0301      *  Default is false. */
0302     bool showVerticalLines() const;
0303 
0304     /** \brief Set the horizontal distance, in pixels, between the vertical grid lines.
0305      *  Must be a distance of 1 or more.
0306      *  Default is 30 pixels. */
0307     void setVerticalLinesDistance(uint distance);
0308     /** \brief The horizontal distance, in pixels, between the vertical grid lines.
0309      *  Default is 30 pixels. */
0310     uint verticalLinesDistance() const;
0311 
0312     /** \brief Set whether the vertical lines move with the data.
0313      *  Default is true. This has no effect is showVerticalLines is false. */
0314     void setVerticalLinesScroll(bool value);
0315     /** \brief Whether the vertical lines move with the data.
0316      *  Default is true. This has no effect is showVerticalLines is false. */
0317     bool verticalLinesScroll() const;
0318 
0319     /** \brief Set whether to draw the horizontal grid lines.
0320      *  Default is true. */
0321     void setShowHorizontalLines(bool value);
0322     /** \brief Whether to draw the horizontal grid lines.
0323      *  Default is true. */
0324     bool showHorizontalLines() const;
0325 
0326     /** \brief The number of decimal places used for the axis labels
0327      *
0328      *  This is calculated based on the current range */
0329     int currentAxisPrecision() const;
0330 
0331     /** \brief Set whether to show the vertical axis labels.
0332      *
0333      * Default is true.
0334      * \sa showAxis(), setAxisFont(), setAxisFontColor(), setMaxAxisTextWidth() */
0335     void setShowAxis(bool show);
0336     /** \brief Whether to show the vertical axis labels.
0337      *
0338      * Default is true.
0339      * \sa setShowAxis(), axisFont(), axisFontColor(), maxAxisTextWidth() */
0340     bool showAxis() const;
0341 
0342     /** \brief Set the filename of the SVG background.
0343      *
0344      * Set to empty (default) to disable again. */
0345     void setSvgBackground(const QString &filename);
0346 
0347     /** \brief The filename of the SVG background. */
0348     QString svgBackground() const;
0349 
0350     /** \brief Return the last value that we have for the given beam index.
0351      *
0352      * \return last value, or 0 if not known. */
0353     qreal lastValue(int index) const;
0354 
0355     /** \brief Return a translated string for the last value at the given index.
0356      *
0357      * Returns, for example,  "34 %" or "100 KB" for the given beam index,
0358      * using the last value set for the beam, using the given precision.
0359      *
0360      * If precision is -1 (the default) then if @p value is greater than 99.5, no decimal figures are shown,
0361      * otherwise if @p value is greater than 0.995, 1 decimal figure is used, otherwise 2.
0362      */
0363     QString lastValueAsString(int index, int precision = -1) const;
0364 
0365     /** \brief Return a translated string for the given value.
0366      *
0367      * Returns, for example, "34 %" or "100 KB" for the given value in unscaled units.
0368      *
0369      * If precision is -1 (the default) then if @p value is greater than 99.5, no decimal figures are shown,
0370      * otherwise if @p value is greater than 0.995, 1 decimal figure is used, otherwise 2.
0371      *
0372      * For example:
0373      * \code
0374      *   KSignalPlotter plotter;
0375      *   plotter.setUnit( ki18ncp("Units", "1 hour", "%1 hours") );
0376      *   plotter.scaleDownBy( 60 ); //The input will be in seconds, and there's 60 seconds in an hour
0377      *   QString formattedString = plotter.valueAsString(150); //returns "2.5 hours"
0378      * \endcode
0379      *
0380      */
0381     QString valueAsString(qreal value, int precision = -1) const;
0382 
0383     /** \brief Set the distance between the left of the widget and the left of the plotting region.
0384      *
0385      *  For example:
0386      *  \code
0387      *      int axisTextWidth = fontMetrics().width(i18nc("Largest axis title", "99999 XXXX"));
0388      *      plotter->setMaxAxisTextWidth(axisTextWidth);
0389      *  \endcode
0390      *
0391      *  If this is 0, the default, then the text will be shown inside the plotting area.
0392      */
0393     void setMaxAxisTextWidth(int maxAxisTextWidth);
0394     /** \brief Get the distance between the left of the widget and the left of the plotting region. */
0395     int maxAxisTextWidth() const;
0396 
0397     /** \brief Set whether to smooth the graph by averaging the points.
0398      *
0399      * This uses the formula:  (value*2 + last_value)/3.
0400      * Default is true. */
0401     void setSmoothGraph(bool smooth);
0402     /** \brief Whether to smooth the graph by averaging the points.
0403      *
0404      * This uses the formula:  (value*2 + last_value)/3.
0405      * Default is true. */
0406     bool smoothGraph() const;
0407 
0408     /** \brief Set whether to stack the beams on top of each other.
0409      *
0410      * Default is false */
0411     void setStackGraph(bool stack);
0412     /** \brief Whether to stack the beams on top of each other.
0413      *
0414      * Default is false */
0415     bool stackGraph() const;
0416 
0417     /** \brief Alpha value for filling the area underneath the graph lines.
0418      *
0419      * Set to 0 to disable filling the graph, and 255 for a solid fill. Default is 20*/
0420     void setFillOpacity(int fill);
0421     /** \brief Alpha value for filling the area underneath the graph lines. */
0422     int fillOpacity() const;
0423 
0424     /* Whether to show a thin line on the left and bottom of the widget, for a slight 3D effect */
0425     bool thinFrame() const;
0426     /* Set whether to show a thin line on the left and bottom of the widget, for a slight 3D effect */
0427     void setThinFrame(bool thinFrame);
0428 
0429 Q_SIGNALS:
0430     /** When the axis has changed this signal is emitted. */
0431     void axisScaleChanged();
0432 
0433 protected:
0434     /* Reimplemented */
0435     void resizeEvent(QGraphicsSceneResizeEvent *event) override;
0436     void paint(QPainter *p, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override;
0437     QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint = QSizeF()) const override;
0438     void changeEvent(QEvent *event) override;
0439     QPainterPath opaqueArea() const override;
0440 
0441 private:
0442     KGraphicsSignalPlotterPrivate *const d;
0443     friend class KGraphicsSignalPlotterPrivate;
0444 };
0445 
0446 #endif