<commit_before>/*******************************************************************************

Licensed to the OpenCOR team under one or more contributor license agreements.
See the NOTICE.txt file distributed with this work for additional information
regarding copyright ownership. The OpenCOR team licenses this file to you under
the Apache License, Version 2.0 (the "License"); you may not use this file
except in compliance with the License. You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.

*******************************************************************************/

//==============================================================================
// Single cell view graph panel plot widget
//==============================================================================

#include "cellmlfileruntime.h"
#include "singlecellviewgraphpanelplotwidget.h"

//==============================================================================

#include <QAction>
#include <QApplication>
#include <QClipboard>
#include <QCursor>
#include <QDesktopWidget>
#include <QLocale>
#include <QMenu>
#include <QMouseEvent>
#include <QPainter>
#include <QWidget>

//==============================================================================

#include <QtNumeric>

//==============================================================================

#include <qnamespace.h>

//==============================================================================

#include <float.h>

//==============================================================================

#include "qwt_painter.h"
#include "qwt_plot_canvas.h"
#include "qwt_plot_curve.h"
#include "qwt_plot_directpainter.h"
#include "qwt_plot_grid.h"
#include "qwt_plot_layout.h"
#include "qwt_plot_renderer.h"
#include "qwt_scale_div.h"
#include "qwt_scale_engine.h"

//==============================================================================

#include "ui_singlecellviewgraphpanelplotwidget.h"

//==============================================================================

namespace OpenCOR {
namespace SingleCellView {

//==============================================================================

SingleCellViewGraphPanelPlotGraph::SingleCellViewGraphPanelPlotGraph(CellMLSupport::CellmlFileRuntimeParameter *pParameterX,
                                                                     CellMLSupport::CellmlFileRuntimeParameter *pParameterY) :
    QwtPlotCurve(),
    mSelected(true),
    mFileName(QString()),
    mParameterX(pParameterX),
    mParameterY(pParameterY)
{
    // Customise ourselves a bit

    setPen(QPen(Qt::darkBlue));
    setRenderHint(QwtPlotItem::RenderAntialiased);
}

//==============================================================================

bool SingleCellViewGraphPanelPlotGraph::isValid() const
{
    // Return whether we are valid

    return   !mFileName.isEmpty()
           && mParameterX && mParameterY;
}

//==============================================================================

bool SingleCellViewGraphPanelPlotGraph::isSelected() const
{
    // Return whether we are selected

    return mSelected;
}

//==============================================================================

void SingleCellViewGraphPanelPlotGraph::setSelected(const bool &pSelected)
{
    // Set our selected state

    mSelected = pSelected;
}

//==============================================================================

QString SingleCellViewGraphPanelPlotGraph::fileName() const
{
    // Return our file name

    return mFileName;
}

//==============================================================================

void SingleCellViewGraphPanelPlotGraph::setFileName(const QString &pFileName)
{
    // Set our file name

    mFileName = pFileName;
}

//==============================================================================

CellMLSupport::CellmlFileRuntimeParameter * SingleCellViewGraphPanelPlotGraph::parameterX() const
{
    // Return our parameter X

    return mParameterX;
}

//==============================================================================

void SingleCellViewGraphPanelPlotGraph::setParameterX(CellMLSupport::CellmlFileRuntimeParameter *pParameterX)
{
    // Set our parameter X

    mParameterX = pParameterX;
}

//==============================================================================

CellMLSupport::CellmlFileRuntimeParameter * SingleCellViewGraphPanelPlotGraph::parameterY() const
{
    // Return our parameter Y

    return mParameterY;
}

//==============================================================================

void SingleCellViewGraphPanelPlotGraph::setParameterY(CellMLSupport::CellmlFileRuntimeParameter *pParameterY)
{
    // Set our parameter Y

    mParameterY = pParameterY;
}

//==============================================================================

SingleCellViewGraphPanelPlotOverlayWidget::SingleCellViewGraphPanelPlotOverlayWidget(SingleCellViewGraphPanelPlotWidget *pParent) :
    QWidget(pParent),
    mOwner(pParent),
    mAction(None),
    mOriginPoint(QPointF()),
    mEndPoint(QPointF())
{
    setAttribute(Qt::WA_NoSystemBackground);
    setAttribute(Qt::WA_TransparentForMouseEvents);

    setFocusPolicy(Qt::NoFocus);
}

//==============================================================================

void SingleCellViewGraphPanelPlotOverlayWidget::paintEvent(QPaintEvent *pEvent)
{
    // Accept the event

    pEvent->accept();

    // Check whether an action is to be carried out

    if (mAction == None)
        return;

    // Paint the overlay, if any is needed

    QPainter painter(this);

    QRectF plotLayoutCanvasRect = mOwner->plotLayout()->canvasRect();

    painter.translate(plotLayoutCanvasRect.x(), plotLayoutCanvasRect.y());

    switch (mAction) {
    case ShowCoordinates: {
        // Draw the two dashed lines that show the coordinates, using a dark
        // cyan pen

        QPen pen = painter.pen();
        QColor backgroundColor = Qt::darkCyan;

        backgroundColor.setAlphaF(0.69);

        pen.setColor(backgroundColor);
        pen.setStyle(Qt::DashLine);

        painter.setPen(pen);

        QPointF coordinates = QPointF(mOwner->canvasMap(QwtPlot::xBottom).transform(mOriginPoint.x()),
                                      mOwner->canvasMap(QwtPlot::yLeft).transform(mOriginPoint.y()));

        painter.drawLine(0.0, coordinates.y(),
                         plotLayoutCanvasRect.width(), coordinates.y());
        painter.drawLine(coordinates.x(), 0.0,
                         coordinates.x(), plotLayoutCanvasRect.height());

        // Draw the coordinates

        drawCoordinates(&painter, mOriginPoint, coordinates,
                        backgroundColor, Qt::white);

        break;
    }
    case ZoomRegion: {
        // Retrieve the coordinates of the region to be zoomed

        QRectF zoomRegionRect = zoomRegion();

        // Now, draw the region to be zoomed

        QColor penColor = Qt::darkRed;
        QColor brushColor = Qt::yellow;

        penColor.setAlphaF(0.69);
        brushColor.setAlphaF(0.19);

        painter.setPen(penColor);

        QwtScaleMap canvasMapX = mOwner->canvasMap(QwtPlot::xBottom);
        QwtScaleMap canvasMapY = mOwner->canvasMap(QwtPlot::yLeft);

        QPointF topLeftCoordinates = QPointF(canvasMapX.transform(zoomRegionRect.left()),
                                             canvasMapY.transform(zoomRegionRect.top()));
        QPointF bottomRightCoordinates = QPointF(canvasMapX.transform(zoomRegionRect.right()),
                                                 canvasMapY.transform(zoomRegionRect.bottom()));

        QRectF unmappedZoomRegionRect = QRectF(topLeftCoordinates.x(),
                                               topLeftCoordinates.y(),
                                               bottomRightCoordinates.x()-topLeftCoordinates.x(),
                                               bottomRightCoordinates.y()-topLeftCoordinates.y());

        painter.fillRect(unmappedZoomRegionRect, brushColor);
        painter.drawRect(unmappedZoomRegionRect);

        // Draw the two sets of coordinates

        drawCoordinates(&painter, zoomRegionRect.topLeft(), topLeftCoordinates,
                        penColor, Qt::white, BottomRight, false);
        drawCoordinates(&painter, zoomRegionRect.bottomRight(), bottomRightCoordinates,
                        penColor, Qt::white, TopLeft, false);

        break;
    }
    default:
        // None

        ;
    }
}

//==============================================================================

void SingleCellViewGraphPanelPlotOverlayWidget::reset()
{
    // Reset ourselves

    setAction(None);
}

//==============================================================================

void SingleCellViewGraphPanelPlotOverlayWidget::setAction(const Action &pAction)
{
    // Set our action

    mAction = pAction;

    repaint();
}

//==============================================================================

void SingleCellViewGraphPanelPlotOverlayWidget::setOriginPoint(const QPointF &pOriginPoint)
{
    // Set our point of origin

    mOriginPoint = pOriginPoint;

    repaint();
}

//==============================================================================

void SingleCellViewGraphPanelPlotOverlayWidget::setEndPoint(const QPointF &pEndPoint)
{
    // Set our end point

    mEndPoint = pEndPoint;

    repaint();
}

//==============================================================================

void SingleCellViewGraphPanelPlotOverlayWidget::drawCoordinates(QPainter *pPainter,
                                                                const QPointF &pCoordinates,
                                                                const QPointF &pCoordinatesPosition,
                                                                const QColor &pBackgroundColor,
                                                                const QColor &pForegroundColor,
                                                                const Location &pLocation,
                                                                const bool &pCanMoveLocation)
{
    // Retrieve the size of coordinates as they will appear on the screen,
    // which means using the same font as the one used for the axes

    pPainter->setFont(mOwner->axisFont(QwtPlot::xBottom));

    QString coords = QString("(%1, %2)").arg(QString::number(pCoordinates.x(), 'g', 15),
                                             QString::number(pCoordinates.y(), 'g', 15));
    QRect desktopGeometry = qApp->desktop()->availableGeometry();
    QRectF coordsRect = pPainter->boundingRect(QRectF(0.0, 0.0, desktopGeometry.width(), desktopGeometry.height()), coords);

    // Determine where the coordinates and its background should be drawn

    switch (pLocation) {
    case TopLeft:
        coordsRect.moveTo(pCoordinatesPosition.x()-coordsRect.right()-1.0,
                          pCoordinatesPosition.y()-coordsRect.bottom()-1.0);

        break;
    case TopRight:
        coordsRect.moveTo(pCoordinatesPosition.x()+2.0,
                          pCoordinatesPosition.y()-coordsRect.bottom()-1.0);

        break;
    case BottomLeft:
        coordsRect.moveTo(pCoordinatesPosition.x()-coordsRect.right()-1.0,
                          pCoordinatesPosition.y()+2.0);

        break;
    case BottomRight:
        coordsRect.moveTo(pCoordinatesPosition.x()+2.0,
                          pCoordinatesPosition.y()+2.0);

        break;
    }

    if (pCanMoveLocation) {
        if (coordsRect.top() < 0.0)
            coordsRect.moveTop(pCoordinatesPosition.y()+2.0);

        if (coordsRect.left() < 0.0)
            coordsRect.moveLeft(pCoordinatesPosition.x()+2.0);

        QRectF plotLayoutCanvasRect = mOwner->plotLayout()->canvasRect();

        if (coordsRect.bottom() > plotLayoutCanvasRect.height())
            coordsRect.moveTop(pCoordinatesPosition.y()-coordsRect.height()-1.0);

        if (coordsRect.right() > plotLayoutCanvasRect.width())
            coordsRect.moveLeft(pCoordinatesPosition.x()-coordsRect.width()-1.0);
    }

    // Draw a filled rectangle to act as the background of the coordinates
    // we are to show

    pPainter->fillRect(coordsRect, pBackgroundColor);

    // Draw the text for the coordinates, using a white pen

    QPen pen = pPainter->pen();

    pen.setColor(pForegroundColor);

    pPainter->setPen(pen);

    pPainter->drawText(coordsRect, coords);
}

//==============================================================================

static const double MinZoomFactor =     1.0;
static const double MaxZoomFactor = 32768.0;
// Note: ideally, we would be able to zoom in as much as we want, but in
//       practice this may cause problems with QwtPlot, so we limit how much we
//       can zoom in...

//==============================================================================

QRectF SingleCellViewGraphPanelPlotOverlayWidget::zoomRegion() const
{
    // Return the region to be zoomed based on our origin and end points
    // Note: by default, we assume that we are already fully zoomed in in both
    //       directions...

    double xMin = mOwner->localMinX();
    double xMax = mOwner->localMaxX();
    double yMin = mOwner->localMinY();
    double yMax = mOwner->localMaxY();

    if (mOwner->zoomFactorX() < MaxZoomFactor) {
        xMin = qMin(mOriginPoint.x(), mEndPoint.x());
        xMax = qMax(mOriginPoint.x(), mEndPoint.x());
    }

    if (mOwner->zoomFactorY() < MaxZoomFactor) {
        yMin = qMin(mOriginPoint.y(), mEndPoint.y());
        yMax = qMax(mOriginPoint.y(), mEndPoint.y());
    }

    return QRectF(xMin, yMax, xMax-xMin, yMin-yMax);
}

//==============================================================================

QwtText SingleCellViewGraphPanelPlotScaleDraw::label(double pValue) const
{
    if (qFuzzyCompare(pValue+1.0, 1.0))
        pValue = 0.0;

    return QLocale().toString(pValue, 'g', 15);
}

//==============================================================================

static const double MinAxis =    0.0;
static const double MaxAxis = 1000.0;

//==============================================================================

SingleCellViewGraphPanelPlotWidget::SingleCellViewGraphPanelPlotWidget(QWidget *pParent) :
    QwtPlot(pParent),
    CommonWidget(pParent),
    mGui(new Ui::SingleCellViewGraphPanelPlotWidget),
    mGraphs(QList<SingleCellViewGraphPanelPlotGraph *>()),
    mAction(None),
    mOriginPoint(QPointF()),
    mMinX(MinAxis),
    mMaxX(MaxAxis),
    mMinY(MinAxis),
    mMaxY(MaxAxis),
    mNeedMinX(0.0),
    mNeedMaxX(0.0),
    mNeedMinY(0.0),
    mNeedMaxY(0.0),
    mZoomFactorX(MinZoomFactor),
    mZoomFactorY(MinZoomFactor),
    mNeedContextMenu(false)
{
    // Set up the GUI

    mGui->setupUi(this);

    // Get ourselves a direct painter

    mDirectPainter = new QwtPlotDirectPainter(this);

    // Speedup painting on X11 systems
    // Note: this can only be done on X11 systems...

    if (QwtPainter::isX11GraphicsSystem())
        canvas()->setAttribute(Qt::WA_PaintOnScreen, true);

    // Customise ourselves a bit

    setCanvasBackground(Qt::white);

    setAxisScaleDraw(QwtPlot::xBottom, new SingleCellViewGraphPanelPlotScaleDraw());
    setAxisScaleDraw(QwtPlot::yLeft, new SingleCellViewGraphPanelPlotScaleDraw());

    // We don't want a frame around ourselves

    qobject_cast<QwtPlotCanvas *>(canvas())->setFrameShape(QFrame::NoFrame);

    // Set our axes' local minimum/maximum values

    setLocalMinMaxX(mMinX, mMaxX);
    setLocalMinMaxY(mMinY, mMaxY);

    // Attach a grid to ourselves

    QwtPlotGrid *grid = new QwtPlotGrid();

    grid->setMajorPen(Qt::gray, 0, Qt::DotLine);

    grid->attach(this);

    // Create our overlay widget

    mOverlayWidget = new SingleCellViewGraphPanelPlotOverlayWidget(this);

    // Create our context menu

    mContextMenu = new QMenu(this);

    mContextMenu->addAction(mGui->actionCopy);
    mContextMenu->addSeparator();
    mContextMenu->addAction(mGui->actionZoomIn);
    mContextMenu->addAction(mGui->actionZoomOut);
    mContextMenu->addSeparator();
    mContextMenu->addAction(mGui->actionResetZoom);
}

//==============================================================================

SingleCellViewGraphPanelPlotWidget::~SingleCellViewGraphPanelPlotWidget()
{
    // Delete some internal objects

    delete mDirectPainter;

    foreach (SingleCellViewGraphPanelPlotGraph *graph, mGraphs)
        delete graph;
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::retranslateUi()
{
    // Retranslate our GUI

    mGui->retranslateUi(this);
}

//==============================================================================

bool SingleCellViewGraphPanelPlotWidget::eventFilter(QObject *pObject,
                                                     QEvent *pEvent)
{
    // Default handling of the event

    bool res = QwtPlot::eventFilter(pObject, pEvent);

    // We want to handle a double mouse click, but for some reasons to override
    // mouseDoubleClickEvent() doesn't work, so...

    if (pEvent->type() == QEvent::MouseButtonDblClick)
        handleMouseDoubleClickEvent(static_cast<QMouseEvent *>(pEvent));

    // We are all done, so...

    return res;
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::handleMouseDoubleClickEvent(QMouseEvent *pEvent)
{
    // Reset the zoom level, in case we double-clicked using the left mouse
    // button with no modifiers

    if (   (pEvent->button() == Qt::LeftButton)
        && (pEvent->modifiers() == Qt::NoModifier))
        on_actionResetZoom_triggered();
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::setAxes(const double &pMinX,
                                                 const double &pMaxX,
                                                 const double &pMinY,
                                                 const double &pMaxY)
{
    // Set our needed minimum/maximum X/Y values

    mNeedMinX = pMinX;
    mNeedMaxX = pMaxX;
    mNeedMinY = pMinY;
    mNeedMaxY = pMaxY;

    // Effectively update our axes by trying to set them

    setLocalAxes(0.0, 0.0, 0.0, 0.0, true, true, false, true);
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::minX() const
{
    // Return our minimum X value

    return mMinX;
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::maxX() const
{
    // Return our maximum X value

    return mMaxX;
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::setMinMaxX(const double &pMinX,
                                                    const double &pMaxX)
{
    // Set our minimum/maximum X values

    mMinX = pMinX;
    mMaxX = pMaxX;

    // Make sure that our zoom factors are up-to-date

    updateZoomFactors();
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::minY() const
{
    // Return our minimum Y value

    return mMinY;
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::maxY() const
{
    // Return our maximum Y value

    return mMaxY;
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::setMinMaxY(const double &pMinY,
                                                    const double &pMaxY)
{
    // Set our minimum/maximum Y values

    mMinY = pMinY;
    mMaxY = pMaxY;

    // Make sure that our zoom factors are up-to-date

    updateZoomFactors();
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::updateZoomFactors()
{
    // Update our zoom factors
    // Note: ideally, we wouldn't keep track of them, but say that you are fully
    //       zoomed in, then to pan the plotting area might result in the 'new'
    //       zoom factors not being exactly the same as the 'old' ones (typical
    //       issue when using floating numbers). This, in turn, might result in
    //       the user being wrongly allowed to zoom in, so...

    mZoomFactorX = (mMaxX-mMinX)/(localMaxX()-localMinX());
    mZoomFactorY = (mMaxY-mMinY)/(localMaxY()-localMinY());

    // Update the enabled status of our actions

    mGui->actionZoomIn->setEnabled((mZoomFactorX < MaxZoomFactor) || (mZoomFactorY < MaxZoomFactor));
    mGui->actionZoomOut->setEnabled((mZoomFactorX > MinZoomFactor) || (mZoomFactorY > MinZoomFactor));

    mGui->actionResetZoom->setEnabled((mZoomFactorX != MinZoomFactor) || (mZoomFactorY != MinZoomFactor));
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::setLocalAxis(const int &pAxis,
                                                      const double &pMin,
                                                      const double &pMax)
{
    // Set our local axis
    // Note #1: to use setAxisScale() on its own is not sufficient unless we
    //          were to replot ourselves immediately after, but we don't want to
    //          do that, so instead we also use setAxisScaleDiv() to make sure
    //          that our local axis is indeed taken into account (i.e. we can
    //          retrieve them using localMinX(), localMaxX(), localMinY() and
    //          localMaxY()). Also, we must call setAxisScaleDiv() before
    //          setAxisScale() to make sure that the axis data is not considered
    //          as valid which is important when it comes to plotting
    //          ourselves...
    // Note #2: the way QwtPlot create ticks for an axis means that it cannot
    //          handle an axis that would go from -DBL_MAX to DBL_MAX, even
    //          though it would be a valid axis, so we check that the axis fits
    //          within what we know works fine with QwtPlot...

    static const double DBL_MAX_AXIS = 0.3*DBL_MAX;

    setAxisScaleDiv(pAxis, QwtScaleDiv(qMax(-DBL_MAX_AXIS, pMin),
                                       qMin( DBL_MAX_AXIS, pMax)));
    setAxisScale(pAxis, qMax(-DBL_MAX_AXIS, pMin),
                        qMin( DBL_MAX_AXIS, pMax));

    // Make sure that our zoom factors are up-to-date

    updateZoomFactors();
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::checkLocalAxisValues(const int &pAxis,
                                                              double &pMin,
                                                              double &pMax)
{
    // Make sure that the minimum/maximum values of our local axis have a valid
    // zoom factor

    double min = (pAxis == QwtPlot::xBottom)?mMinX:mMinY;
    double max = (pAxis == QwtPlot::xBottom)?mMaxX:mMaxY;
    double zoomFactor = (max-min)/(pMax-pMin);

    if (zoomFactor < MinZoomFactor) {
        // The local axis' zoom factor is too small, so reset our
        // minimum/maximum values

        pMin = min;
        pMax = max;
    } else if (zoomFactor > MaxZoomFactor) {
        // The local axis' zoom factor is too big, so reset our minimum/maximum
        // values

        double length = (max-min)/MaxZoomFactor;

        pMin = qMax(min, 0.5*(pMin+pMax-length));
        pMax = qMin(max, pMin+length);
        pMin = pMax-length;
        // Note: the last statement is in case pMax has been set to max, in
        //       which case we need to update pMin...
    }
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::localMinX() const
{
    // Return our local minimum X value

    return axisScaleDiv(QwtPlot::xBottom).lowerBound();
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::localMaxX() const
{
    // Return our local maximum X value

    return axisScaleDiv(QwtPlot::xBottom).upperBound();
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::setLocalMinMaxX(const double &pLocalMinX,
                                                         const double &pLocalMaxX)
{
    // Set our local minimum X value

    double locMinX = pLocalMinX;
    double locMaxX = pLocalMaxX;

    checkLocalAxisValues(QwtPlot::xBottom, locMinX, locMaxX);

    setLocalAxis(QwtPlot::xBottom, locMinX, locMaxX);
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::localMinY() const
{
    // Return our local minimum Y value

    return axisScaleDiv(QwtPlot::yLeft).lowerBound();
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::localMaxY() const
{
    // Return our local maximum Y value

    return axisScaleDiv(QwtPlot::yLeft).upperBound();
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::zoomFactorX() const
{
    // Return our zoom factor for the X axis

    return mZoomFactorX;
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::zoomFactorY() const
{
    // Return our zoom factor for the Y axis

    return mZoomFactorY;
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::setLocalMinMaxY(const double &pLocalMinY,
                                                         const double &pLocalMaxY)
{
    // Set our local minimum Y value

    double locMinY = pLocalMinY;
    double locMaxY = pLocalMaxY;

    checkLocalAxisValues(QwtPlot::yLeft, locMinY, locMaxY);

    setLocalAxis(QwtPlot::yLeft, locMinY, locMaxY);
}

//==============================================================================

QList<SingleCellViewGraphPanelPlotGraph *> SingleCellViewGraphPanelPlotWidget::graphs() const
{
    // Return all our graphs

    return mGraphs;
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::checkAnyAxesValues(double &pMinX,
                                                            double &pMaxX,
                                                            double &pMinY,
                                                            double &pMaxY)
{
    // Make sure that the minimum/maximum values of our axes have finite values

    if (!qIsFinite(pMinX))
        pMinX = -DBL_MAX;

    if (!qIsFinite(pMaxX))
        pMaxX = DBL_MAX;

    if (!qIsFinite(pMinY))
        pMinY = -DBL_MAX;

    if (!qIsFinite(pMaxY))
        pMaxY = DBL_MAX;
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::setLocalAxes(const double &pLocalMinX,
                                                      const double &pLocalMaxX,
                                                      const double &pLocalMinY,
                                                      const double &pLocalMaxY,
                                                      const bool &pCanReplot,
                                                      const bool &pForceMinMaxValues,
                                                      const bool &pUpdateMinMaxValues,
                                                      const bool &pResetMinMaxValues)
{
    // Update our axes

    double oldLocalMinX = localMinX();
    double oldLocalMaxX = localMaxX();
    double oldLocalMinY = localMinY();
    double oldLocalMaxY = localMaxY();

    double newLocalMinX = pLocalMinX;
    double newLocalMaxX = pLocalMaxX;
    double newLocalMinY = pLocalMinY;
    double newLocalMaxY = pLocalMaxY;

    // Retrieve the bounding rectangle for all our graphs (but only for those
    // that have some data)

    QRectF boundingRect = QRectF();

    foreach (SingleCellViewGraphPanelPlotGraph *graph, mGraphs)
        if (graph->isValid() && graph->isSelected() && graph->dataSize())
            boundingRect |= graph->boundingRect();

    // Take into account the needed minimum/maximum values for our X and Y axes,
    // if any, or use MinAxis/MaxAxis if we have null bounding rectangle

    bool isBoundingRectNull = boundingRect.isNull();
    bool needMinMaxX = mNeedMinX != mNeedMaxX;
    bool needMinMaxY = mNeedMinY != mNeedMaxY;

    if (needMinMaxX) {
        boundingRect.setLeft(isBoundingRectNull?mNeedMinX:qMin(boundingRect.left(), mNeedMinX));
        boundingRect.setRight(isBoundingRectNull?mNeedMaxX:qMax(boundingRect.right(), mNeedMaxX));
    } else if (isBoundingRectNull) {
        boundingRect.setLeft(MinAxis);
        boundingRect.setRight(MaxAxis);
    }

    if (needMinMaxY) {
        boundingRect.setTop(isBoundingRectNull?mNeedMinY:qMin(boundingRect.top(), mNeedMinY));
        boundingRect.setBottom(isBoundingRectNull?mNeedMaxY:qMax(boundingRect.bottom(), mNeedMaxY));
    } else if (isBoundingRectNull) {
        boundingRect.setTop(MinAxis);
        boundingRect.setBottom(MaxAxis);
    }

    // Update the minimum/maximum values of our axes, should we have retrieved a
    // non-null bounding rectangle

    if (boundingRect.isValid()) {
        // Optimise our bounding rectangle by first retrieving the
        // minimum/maximum values of our axes<commit_msg>Some work towards zooming out more (#240).<commit_after>/*******************************************************************************

Licensed to the OpenCOR team under one or more contributor license agreements.
See the NOTICE.txt file distributed with this work for additional information
regarding copyright ownership. The OpenCOR team licenses this file to you under
the Apache License, Version 2.0 (the "License"); you may not use this file
except in compliance with the License. You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.

*******************************************************************************/

//==============================================================================
// Single cell view graph panel plot widget
//==============================================================================

#include "cellmlfileruntime.h"
#include "singlecellviewgraphpanelplotwidget.h"

//==============================================================================

#include <QAction>
#include <QApplication>
#include <QClipboard>
#include <QCursor>
#include <QDesktopWidget>
#include <QLocale>
#include <QMenu>
#include <QMouseEvent>
#include <QPainter>
#include <QWidget>

//==============================================================================

#include <QtNumeric>

//==============================================================================

#include <qnamespace.h>

//==============================================================================

#include <float.h>

//==============================================================================

#include "qwt_painter.h"
#include "qwt_plot_canvas.h"
#include "qwt_plot_curve.h"
#include "qwt_plot_directpainter.h"
#include "qwt_plot_grid.h"
#include "qwt_plot_layout.h"
#include "qwt_plot_renderer.h"
#include "qwt_scale_div.h"
#include "qwt_scale_engine.h"

//==============================================================================

#include "ui_singlecellviewgraphpanelplotwidget.h"

//==============================================================================

namespace OpenCOR {
namespace SingleCellView {

//==============================================================================

SingleCellViewGraphPanelPlotGraph::SingleCellViewGraphPanelPlotGraph(CellMLSupport::CellmlFileRuntimeParameter *pParameterX,
                                                                     CellMLSupport::CellmlFileRuntimeParameter *pParameterY) :
    QwtPlotCurve(),
    mSelected(true),
    mFileName(QString()),
    mParameterX(pParameterX),
    mParameterY(pParameterY)
{
    // Customise ourselves a bit

    setPen(QPen(Qt::darkBlue));
    setRenderHint(QwtPlotItem::RenderAntialiased);
}

//==============================================================================

bool SingleCellViewGraphPanelPlotGraph::isValid() const
{
    // Return whether we are valid

    return   !mFileName.isEmpty()
           && mParameterX && mParameterY;
}

//==============================================================================

bool SingleCellViewGraphPanelPlotGraph::isSelected() const
{
    // Return whether we are selected

    return mSelected;
}

//==============================================================================

void SingleCellViewGraphPanelPlotGraph::setSelected(const bool &pSelected)
{
    // Set our selected state

    mSelected = pSelected;
}

//==============================================================================

QString SingleCellViewGraphPanelPlotGraph::fileName() const
{
    // Return our file name

    return mFileName;
}

//==============================================================================

void SingleCellViewGraphPanelPlotGraph::setFileName(const QString &pFileName)
{
    // Set our file name

    mFileName = pFileName;
}

//==============================================================================

CellMLSupport::CellmlFileRuntimeParameter * SingleCellViewGraphPanelPlotGraph::parameterX() const
{
    // Return our parameter X

    return mParameterX;
}

//==============================================================================

void SingleCellViewGraphPanelPlotGraph::setParameterX(CellMLSupport::CellmlFileRuntimeParameter *pParameterX)
{
    // Set our parameter X

    mParameterX = pParameterX;
}

//==============================================================================

CellMLSupport::CellmlFileRuntimeParameter * SingleCellViewGraphPanelPlotGraph::parameterY() const
{
    // Return our parameter Y

    return mParameterY;
}

//==============================================================================

void SingleCellViewGraphPanelPlotGraph::setParameterY(CellMLSupport::CellmlFileRuntimeParameter *pParameterY)
{
    // Set our parameter Y

    mParameterY = pParameterY;
}

//==============================================================================

SingleCellViewGraphPanelPlotOverlayWidget::SingleCellViewGraphPanelPlotOverlayWidget(SingleCellViewGraphPanelPlotWidget *pParent) :
    QWidget(pParent),
    mOwner(pParent),
    mAction(None),
    mOriginPoint(QPointF()),
    mEndPoint(QPointF())
{
    setAttribute(Qt::WA_NoSystemBackground);
    setAttribute(Qt::WA_TransparentForMouseEvents);

    setFocusPolicy(Qt::NoFocus);
}

//==============================================================================

void SingleCellViewGraphPanelPlotOverlayWidget::paintEvent(QPaintEvent *pEvent)
{
    // Accept the event

    pEvent->accept();

    // Check whether an action is to be carried out

    if (mAction == None)
        return;

    // Paint the overlay, if any is needed

    QPainter painter(this);

    QRectF plotLayoutCanvasRect = mOwner->plotLayout()->canvasRect();

    painter.translate(plotLayoutCanvasRect.x(), plotLayoutCanvasRect.y());

    switch (mAction) {
    case ShowCoordinates: {
        // Draw the two dashed lines that show the coordinates, using a dark
        // cyan pen

        QPen pen = painter.pen();
        QColor backgroundColor = Qt::darkCyan;

        backgroundColor.setAlphaF(0.69);

        pen.setColor(backgroundColor);
        pen.setStyle(Qt::DashLine);

        painter.setPen(pen);

        QPointF coordinates = QPointF(mOwner->canvasMap(QwtPlot::xBottom).transform(mOriginPoint.x()),
                                      mOwner->canvasMap(QwtPlot::yLeft).transform(mOriginPoint.y()));

        painter.drawLine(0.0, coordinates.y(),
                         plotLayoutCanvasRect.width(), coordinates.y());
        painter.drawLine(coordinates.x(), 0.0,
                         coordinates.x(), plotLayoutCanvasRect.height());

        // Draw the coordinates

        drawCoordinates(&painter, mOriginPoint, coordinates,
                        backgroundColor, Qt::white);

        break;
    }
    case ZoomRegion: {
        // Retrieve the coordinates of the region to be zoomed

        QRectF zoomRegionRect = zoomRegion();

        // Now, draw the region to be zoomed

        QColor penColor = Qt::darkRed;
        QColor brushColor = Qt::yellow;

        penColor.setAlphaF(0.69);
        brushColor.setAlphaF(0.19);

        painter.setPen(penColor);

        QwtScaleMap canvasMapX = mOwner->canvasMap(QwtPlot::xBottom);
        QwtScaleMap canvasMapY = mOwner->canvasMap(QwtPlot::yLeft);

        QPointF topLeftCoordinates = QPointF(canvasMapX.transform(zoomRegionRect.left()),
                                             canvasMapY.transform(zoomRegionRect.top()));
        QPointF bottomRightCoordinates = QPointF(canvasMapX.transform(zoomRegionRect.right()),
                                                 canvasMapY.transform(zoomRegionRect.bottom()));

        QRectF unmappedZoomRegionRect = QRectF(topLeftCoordinates.x(),
                                               topLeftCoordinates.y(),
                                               bottomRightCoordinates.x()-topLeftCoordinates.x(),
                                               bottomRightCoordinates.y()-topLeftCoordinates.y());

        painter.fillRect(unmappedZoomRegionRect, brushColor);
        painter.drawRect(unmappedZoomRegionRect);

        // Draw the two sets of coordinates

        drawCoordinates(&painter, zoomRegionRect.topLeft(), topLeftCoordinates,
                        penColor, Qt::white, BottomRight, false);
        drawCoordinates(&painter, zoomRegionRect.bottomRight(), bottomRightCoordinates,
                        penColor, Qt::white, TopLeft, false);

        break;
    }
    default:
        // None

        ;
    }
}

//==============================================================================

void SingleCellViewGraphPanelPlotOverlayWidget::reset()
{
    // Reset ourselves

    setAction(None);
}

//==============================================================================

void SingleCellViewGraphPanelPlotOverlayWidget::setAction(const Action &pAction)
{
    // Set our action

    mAction = pAction;

    repaint();
}

//==============================================================================

void SingleCellViewGraphPanelPlotOverlayWidget::setOriginPoint(const QPointF &pOriginPoint)
{
    // Set our point of origin

    mOriginPoint = pOriginPoint;

    repaint();
}

//==============================================================================

void SingleCellViewGraphPanelPlotOverlayWidget::setEndPoint(const QPointF &pEndPoint)
{
    // Set our end point

    mEndPoint = pEndPoint;

    repaint();
}

//==============================================================================

void SingleCellViewGraphPanelPlotOverlayWidget::drawCoordinates(QPainter *pPainter,
                                                                const QPointF &pCoordinates,
                                                                const QPointF &pCoordinatesPosition,
                                                                const QColor &pBackgroundColor,
                                                                const QColor &pForegroundColor,
                                                                const Location &pLocation,
                                                                const bool &pCanMoveLocation)
{
    // Retrieve the size of coordinates as they will appear on the screen,
    // which means using the same font as the one used for the axes

    pPainter->setFont(mOwner->axisFont(QwtPlot::xBottom));

    QString coords = QString("(%1, %2)").arg(QString::number(pCoordinates.x(), 'g', 15),
                                             QString::number(pCoordinates.y(), 'g', 15));
    QRect desktopGeometry = qApp->desktop()->availableGeometry();
    QRectF coordsRect = pPainter->boundingRect(QRectF(0.0, 0.0, desktopGeometry.width(), desktopGeometry.height()), coords);

    // Determine where the coordinates and its background should be drawn

    switch (pLocation) {
    case TopLeft:
        coordsRect.moveTo(pCoordinatesPosition.x()-coordsRect.right()-1.0,
                          pCoordinatesPosition.y()-coordsRect.bottom()-1.0);

        break;
    case TopRight:
        coordsRect.moveTo(pCoordinatesPosition.x()+2.0,
                          pCoordinatesPosition.y()-coordsRect.bottom()-1.0);

        break;
    case BottomLeft:
        coordsRect.moveTo(pCoordinatesPosition.x()-coordsRect.right()-1.0,
                          pCoordinatesPosition.y()+2.0);

        break;
    case BottomRight:
        coordsRect.moveTo(pCoordinatesPosition.x()+2.0,
                          pCoordinatesPosition.y()+2.0);

        break;
    }

    if (pCanMoveLocation) {
        if (coordsRect.top() < 0.0)
            coordsRect.moveTop(pCoordinatesPosition.y()+2.0);

        if (coordsRect.left() < 0.0)
            coordsRect.moveLeft(pCoordinatesPosition.x()+2.0);

        QRectF plotLayoutCanvasRect = mOwner->plotLayout()->canvasRect();

        if (coordsRect.bottom() > plotLayoutCanvasRect.height())
            coordsRect.moveTop(pCoordinatesPosition.y()-coordsRect.height()-1.0);

        if (coordsRect.right() > plotLayoutCanvasRect.width())
            coordsRect.moveLeft(pCoordinatesPosition.x()-coordsRect.width()-1.0);
    }

    // Draw a filled rectangle to act as the background of the coordinates
    // we are to show

    pPainter->fillRect(coordsRect, pBackgroundColor);

    // Draw the text for the coordinates, using a white pen

    QPen pen = pPainter->pen();

    pen.setColor(pForegroundColor);

    pPainter->setPen(pen);

    pPainter->drawText(coordsRect, coords);
}

//==============================================================================

static const double MinZoomFactor =     1.0;
static const double MaxZoomFactor = 32768.0;
// Note: ideally, we would be able to zoom in as much as we want, but in
//       practice this may cause problems with QwtPlot, so we limit how much we
//       can zoom in...

//==============================================================================

QRectF SingleCellViewGraphPanelPlotOverlayWidget::zoomRegion() const
{
    // Return the region to be zoomed based on our origin and end points
    // Note: by default, we assume that we are already fully zoomed in in both
    //       directions...

    double xMin = mOwner->localMinX();
    double xMax = mOwner->localMaxX();
    double yMin = mOwner->localMinY();
    double yMax = mOwner->localMaxY();

    if (mOwner->zoomFactorX() < MaxZoomFactor) {
        xMin = qMin(mOriginPoint.x(), mEndPoint.x());
        xMax = qMax(mOriginPoint.x(), mEndPoint.x());
    }

    if (mOwner->zoomFactorY() < MaxZoomFactor) {
        yMin = qMin(mOriginPoint.y(), mEndPoint.y());
        yMax = qMax(mOriginPoint.y(), mEndPoint.y());
    }

    return QRectF(xMin, yMax, xMax-xMin, yMin-yMax);
}

//==============================================================================

QwtText SingleCellViewGraphPanelPlotScaleDraw::label(double pValue) const
{
    if (qFuzzyCompare(pValue+1.0, 1.0))
        pValue = 0.0;

    return QLocale().toString(pValue, 'g', 15);
}

//==============================================================================

static const double DblMaxAxis = 0.3*DBL_MAX;

static const double MinAxis = -DblMaxAxis;
static const double MaxAxis =  DblMaxAxis;

static const double DefMinAxis =    0.0;
static const double DefMaxAxis = 1000.0;

//==============================================================================

SingleCellViewGraphPanelPlotWidget::SingleCellViewGraphPanelPlotWidget(QWidget *pParent) :
    QwtPlot(pParent),
    CommonWidget(pParent),
    mGui(new Ui::SingleCellViewGraphPanelPlotWidget),
    mGraphs(QList<SingleCellViewGraphPanelPlotGraph *>()),
    mAction(None),
    mOriginPoint(QPointF()),
    mMinX(DefMinAxis),
    mMaxX(DefMaxAxis),
    mMinY(DefMinAxis),
    mMaxY(DefMaxAxis),
    mNeedMinX(0.0),
    mNeedMaxX(0.0),
    mNeedMinY(0.0),
    mNeedMaxY(0.0),
    mZoomFactorX(MinZoomFactor),
    mZoomFactorY(MinZoomFactor),
    mNeedContextMenu(false)
{
    // Set up the GUI

    mGui->setupUi(this);

    // Get ourselves a direct painter

    mDirectPainter = new QwtPlotDirectPainter(this);

    // Speedup painting on X11 systems
    // Note: this can only be done on X11 systems...

    if (QwtPainter::isX11GraphicsSystem())
        canvas()->setAttribute(Qt::WA_PaintOnScreen, true);

    // Customise ourselves a bit

    setCanvasBackground(Qt::white);

    setAxisScaleDraw(QwtPlot::xBottom, new SingleCellViewGraphPanelPlotScaleDraw());
    setAxisScaleDraw(QwtPlot::yLeft, new SingleCellViewGraphPanelPlotScaleDraw());

    // We don't want a frame around ourselves

    qobject_cast<QwtPlotCanvas *>(canvas())->setFrameShape(QFrame::NoFrame);

    // Set our axes' local minimum/maximum values

    setLocalMinMaxX(DefMinAxis, DefMaxAxis);
    setLocalMinMaxY(DefMinAxis, DefMaxAxis);

    // Attach a grid to ourselves

    QwtPlotGrid *grid = new QwtPlotGrid();

    grid->setMajorPen(Qt::gray, 0, Qt::DotLine);

    grid->attach(this);

    // Create our overlay widget

    mOverlayWidget = new SingleCellViewGraphPanelPlotOverlayWidget(this);

    // Create our context menu

    mContextMenu = new QMenu(this);

    mContextMenu->addAction(mGui->actionCopy);
    mContextMenu->addSeparator();
    mContextMenu->addAction(mGui->actionZoomIn);
    mContextMenu->addAction(mGui->actionZoomOut);
    mContextMenu->addSeparator();
    mContextMenu->addAction(mGui->actionResetZoom);
}

//==============================================================================

SingleCellViewGraphPanelPlotWidget::~SingleCellViewGraphPanelPlotWidget()
{
    // Delete some internal objects

    delete mDirectPainter;

    foreach (SingleCellViewGraphPanelPlotGraph *graph, mGraphs)
        delete graph;
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::retranslateUi()
{
    // Retranslate our GUI

    mGui->retranslateUi(this);
}

//==============================================================================

bool SingleCellViewGraphPanelPlotWidget::eventFilter(QObject *pObject,
                                                     QEvent *pEvent)
{
    // Default handling of the event

    bool res = QwtPlot::eventFilter(pObject, pEvent);

    // We want to handle a double mouse click, but for some reasons to override
    // mouseDoubleClickEvent() doesn't work, so...

    if (pEvent->type() == QEvent::MouseButtonDblClick)
        handleMouseDoubleClickEvent(static_cast<QMouseEvent *>(pEvent));

    // We are all done, so...

    return res;
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::handleMouseDoubleClickEvent(QMouseEvent *pEvent)
{
    // Reset the zoom level, in case we double-clicked using the left mouse
    // button with no modifiers

    if (   (pEvent->button() == Qt::LeftButton)
        && (pEvent->modifiers() == Qt::NoModifier))
        on_actionResetZoom_triggered();
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::setAxes(const double &pMinX,
                                                 const double &pMaxX,
                                                 const double &pMinY,
                                                 const double &pMaxY)
{
    // Set our needed minimum/maximum X/Y values

    mNeedMinX = pMinX;
    mNeedMaxX = pMaxX;
    mNeedMinY = pMinY;
    mNeedMaxY = pMaxY;

    // Effectively update our axes by trying to set them

    setLocalAxes(0.0, 0.0, 0.0, 0.0, true, true, false, true);
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::minX() const
{
    // Return our minimum X value

    return mMinX;
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::maxX() const
{
    // Return our maximum X value

    return mMaxX;
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::setMinMaxX(const double &pMinX,
                                                    const double &pMaxX)
{
    // Set our minimum/maximum X values

    mMinX = pMinX;
    mMaxX = pMaxX;

    // Make sure that our zoom factors are up-to-date

    updateZoomFactors();
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::minY() const
{
    // Return our minimum Y value

    return mMinY;
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::maxY() const
{
    // Return our maximum Y value

    return mMaxY;
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::setMinMaxY(const double &pMinY,
                                                    const double &pMaxY)
{
    // Set our minimum/maximum Y values

    mMinY = pMinY;
    mMaxY = pMaxY;

    // Make sure that our zoom factors are up-to-date

    updateZoomFactors();
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::updateZoomFactors()
{
    // Update our zoom factors
    // Note: ideally, we wouldn't keep track of them, but say that you are fully
    //       zoomed in, then to pan the plotting area might result in the 'new'
    //       zoom factors not being exactly the same as the 'old' ones (typical
    //       issue when using floating numbers). This, in turn, might result in
    //       the user being wrongly allowed to zoom in, so...

    mZoomFactorX = (mMaxX-mMinX)/(localMaxX()-localMinX());
    mZoomFactorY = (mMaxY-mMinY)/(localMaxY()-localMinY());

    // Update the enabled status of our actions

    mGui->actionZoomIn->setEnabled((mZoomFactorX < MaxZoomFactor) || (mZoomFactorY < MaxZoomFactor));
    mGui->actionZoomOut->setEnabled((mZoomFactorX > MinZoomFactor) || (mZoomFactorY > MinZoomFactor));

    mGui->actionResetZoom->setEnabled((mZoomFactorX != MinZoomFactor) || (mZoomFactorY != MinZoomFactor));
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::setLocalAxis(const int &pAxis,
                                                      const double &pMin,
                                                      const double &pMax)
{
    // Set our local axis
    // Note #1: to use setAxisScale() on its own is not sufficient unless we
    //          were to replot ourselves immediately after, but we don't want to
    //          do that, so instead we also use setAxisScaleDiv() to make sure
    //          that our local axis is indeed taken into account (i.e. we can
    //          retrieve them using localMinX(), localMaxX(), localMinY() and
    //          localMaxY()). Also, we must call setAxisScaleDiv() before
    //          setAxisScale() to make sure that the axis data is not considered
    //          as valid which is important when it comes to plotting
    //          ourselves...
    // Note #2: the way QwtPlot create ticks for an axis means that it cannot
    //          handle an axis that would go from -DBL_MAX to DBL_MAX, even
    //          though it would be a valid axis, so we check that the axis fits
    //          within what we know works fine with QwtPlot...

    setAxisScaleDiv(pAxis, QwtScaleDiv(qMax(-DblMaxAxis, pMin),
                                       qMin( DblMaxAxis, pMax)));
    setAxisScale(pAxis, qMax(-DblMaxAxis, pMin),
                        qMin( DblMaxAxis, pMax));

    // Make sure that our zoom factors are up-to-date

    updateZoomFactors();
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::checkLocalAxisValues(const int &pAxis,
                                                              double &pMin,
                                                              double &pMax)
{
    // Make sure that the minimum/maximum values of our local axis have a valid
    // zoom factor

    double min = (pAxis == QwtPlot::xBottom)?mMinX:mMinY;
    double max = (pAxis == QwtPlot::xBottom)?mMaxX:mMaxY;
    double zoomFactor = (max-min)/(pMax-pMin);

    if (zoomFactor < MinZoomFactor) {
        // The local axis' zoom factor is too small, so reset our
        // minimum/maximum values

        pMin = min;
        pMax = max;
    } else if (zoomFactor > MaxZoomFactor) {
        // The local axis' zoom factor is too big, so reset our minimum/maximum
        // values

        double length = (max-min)/MaxZoomFactor;

        pMin = qMax(min, 0.5*(pMin+pMax-length));
        pMax = qMin(max, pMin+length);
        pMin = pMax-length;
        // Note: the last statement is in case pMax has been set to max, in
        //       which case we need to update pMin...
    }
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::localMinX() const
{
    // Return our local minimum X value

    return axisScaleDiv(QwtPlot::xBottom).lowerBound();
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::localMaxX() const
{
    // Return our local maximum X value

    return axisScaleDiv(QwtPlot::xBottom).upperBound();
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::setLocalMinMaxX(const double &pLocalMinX,
                                                         const double &pLocalMaxX)
{
    // Set our local minimum X value

    double locMinX = pLocalMinX;
    double locMaxX = pLocalMaxX;

    checkLocalAxisValues(QwtPlot::xBottom, locMinX, locMaxX);

    setLocalAxis(QwtPlot::xBottom, locMinX, locMaxX);
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::localMinY() const
{
    // Return our local minimum Y value

    return axisScaleDiv(QwtPlot::yLeft).lowerBound();
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::localMaxY() const
{
    // Return our local maximum Y value

    return axisScaleDiv(QwtPlot::yLeft).upperBound();
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::zoomFactorX() const
{
    // Return our zoom factor for the X axis

    return mZoomFactorX;
}

//==============================================================================

double SingleCellViewGraphPanelPlotWidget::zoomFactorY() const
{
    // Return our zoom factor for the Y axis

    return mZoomFactorY;
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::setLocalMinMaxY(const double &pLocalMinY,
                                                         const double &pLocalMaxY)
{
    // Set our local minimum Y value

    double locMinY = pLocalMinY;
    double locMaxY = pLocalMaxY;

    checkLocalAxisValues(QwtPlot::yLeft, locMinY, locMaxY);

    setLocalAxis(QwtPlot::yLeft, locMinY, locMaxY);
}

//==============================================================================

QList<SingleCellViewGraphPanelPlotGraph *> SingleCellViewGraphPanelPlotWidget::graphs() const
{
    // Return all our graphs

    return mGraphs;
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::checkAnyAxesValues(double &pMinX,
                                                            double &pMaxX,
                                                            double &pMinY,
                                                            double &pMaxY)
{
    // Make sure that the minimum/maximum values of our axes have finite values

    if (!qIsFinite(pMinX))
        pMinX = -DBL_MAX;

    if (!qIsFinite(pMaxX))
        pMaxX = DBL_MAX;

    if (!qIsFinite(pMinY))
        pMinY = -DBL_MAX;

    if (!qIsFinite(pMaxY))
        pMaxY = DBL_MAX;
}

//==============================================================================

void SingleCellViewGraphPanelPlotWidget::setLocalAxes(const double &pLocalMinX,
                                                      const double &pLocalMaxX,
                                                      const double &pLocalMinY,
                                                      const double &pLocalMaxY,
                                                      const bool &pCanReplot,
                                                      const bool &pForceMinMaxValues,
                                                      const bool &pUpdateMinMaxValues,
                                                      const bool &pResetMinMaxValues)
{
    // Update our axes

    double oldLocalMinX = localMinX();
    double oldLocalMaxX = localMaxX();
    double oldLocalMinY = localMinY();
    double oldLocalMaxY = localMaxY();

    double newLocalMinX = pLocalMinX;
    double newLocalMaxX = pLocalMaxX;
    double newLocalMinY = pLocalMinY;
    double newLocalMaxY = pLocalMaxY;

    // Retrieve the bounding rectangle for all our graphs (but only for those
    // that have some data)

    QRectF boundingRect = QRectF();

    foreach (SingleCellViewGraphPanelPlotGraph *graph, mGraphs)
        if (graph->isValid() && graph->isSelected() && graph->dataSize())
            boundingRect |= graph->boundingRect();

    // Take into account the needed minimum/maximum values for our X and Y axes,
    // if any, or use DefMinAxis/DefMaxAxis if we have null bounding rectangle

    bool isBoundingRectNull = boundingRect.isNull();
    bool needMinMaxX = mNeedMinX != mNeedMaxX;
    bool needMinMaxY = mNeedMinY != mNeedMaxY;

    if (needMinMaxX) {
        boundingRect.setLeft(isBoundingRectNull?mNeedMinX:qMin(boundingRect.left(), mNeedMinX));
        boundingRect.setRight(isBoundingRectNull?mNeedMaxX:qMax(boundingRect.right(), mNeedMaxX));
    } else if (isBoundingRectNull) {
        boundingRect.setLeft(DefMinAxis);
        boundingRect.setRight(DefMaxAxis);
    }

    if (needMinMaxY) {
        boundingRect.setTop(isBoundingRectNull?mNeedMinY:qMin(boundingRect.top(), mNeedMinY));
        boundingRect.setBottom(isBoundingRectNull?mNeedMaxY:qMax(boundingRect.bottom(), mNeedMaxY));
    } else if (isBoundingRectNull) {
        boundingRect.setTop(DefMinAxis);
        boundingRect.setBottom(DefMaxAxis);
    }

    // Update the minimum/maximum values of our axes, should we have retrieved a
    // non-null bounding rectangle

    if (boundingRect.isValid()) {
        // Optimise our bounding rectangle by first retrieving the
        // minimum/maximum values of our axes
{'fraction_non_alphanumeric': 0.23251621013629747, 'fraction_numerical': 0.0035893873230117774, 'mean_word_length': 3.05833389273008, 'pattern_counts': {'":': 0, '<': 51, '<?xml version=': 0, '>': 153, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 146, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1119590', 'n_tokens_mistral': 15938, 'n_tokens_neox': 14801, 'n_words': 3613}