# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2014-2021 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ############################################################################*/
"""OpenGL Plot backend."""

from __future__ import division

__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "21/12/2018"

import logging
import weakref

import numpy

from .. import items
from .._utils import FLOAT32_MINPOS
from . import BackendBase
from ... import colors
from ... import qt

from ..._glutils import gl
from ... import _glutils as glu
from . import glutils
from .glutils.PlotImageFile import saveImageToFile

_logger = logging.getLogger(__name__)


# TODO idea: BackendQtMixIn class to share code between mpl and gl
# TODO check if OpenGL is available
# TODO make an off-screen mesa backend

# Content #####################################################################

class _ShapeItem(dict):
    def __init__(self, x, y, shape, color, fill, overlay,
                 linestyle, linewidth, linebgcolor):
        super(_ShapeItem, self).__init__()

        if shape not in ('polygon', 'rectangle', 'line',
                         'vline', 'hline', 'polylines'):
            raise NotImplementedError("Unsupported shape {0}".format(shape))

        x = numpy.array(x, copy=False)
        y = numpy.array(y, copy=False)

        if shape == 'rectangle':
            xMin, xMax = x
            x = numpy.array((xMin, xMin, xMax, xMax))
            yMin, yMax = y
            y = numpy.array((yMin, yMax, yMax, yMin))

        # Ignore fill for polylines to mimic matplotlib
        fill = fill if shape != 'polylines' else False

        self.update({
            'shape': shape,
            'color': colors.rgba(color),
            'fill': 'hatch' if fill else None,
            'x': x,
            'y': y,
            'linestyle': linestyle,
            'linewidth': linewidth,
            'linebgcolor': linebgcolor,
        })


class _MarkerItem(dict):
    def __init__(self, x, y, text, color,
                 symbol, linestyle, linewidth, constraint, yaxis):
        super(_MarkerItem, self).__init__()

        if symbol is None:
            symbol = '+'

        # Apply constraint to provided position
        isConstraint = (constraint is not None and
                        x is not None and y is not None)
        if isConstraint:
            x, y = constraint(x, y)

        self.update({
            'x': x,
            'y': y,
            'text': text,
            'color': colors.rgba(color),
            'constraint': constraint if isConstraint else None,
            'symbol': symbol,
            'linestyle': linestyle,
            'linewidth': linewidth,
            'yaxis': yaxis,
        })


# shaders #####################################################################

_baseVertShd = """
    attribute vec2 position;
    uniform mat4 matrix;
    uniform bvec2 isLog;

    const float oneOverLog10 = 0.43429448190325176;

    void main(void) {
        vec2 posTransformed = position;
        if (isLog.x) {
            posTransformed.x = oneOverLog10 * log(position.x);
        }
        if (isLog.y) {
            posTransformed.y = oneOverLog10 * log(position.y);
        }
        gl_Position = matrix * vec4(posTransformed, 0.0, 1.0);
    }
    """

_baseFragShd = """
    uniform vec4 color;
    uniform int hatchStep;
    uniform float tickLen;

    void main(void) {
        if (tickLen != 0.) {
            if (mod((gl_FragCoord.x + gl_FragCoord.y) / tickLen, 2.) < 1.) {
                gl_FragColor = color;
            } else {
                discard;
            }
        } else if (hatchStep == 0 ||
            mod(gl_FragCoord.x - gl_FragCoord.y, float(hatchStep)) == 0.) {
            gl_FragColor = color;
        } else {
            discard;
        }
    }
    """

_texVertShd = """
   attribute vec2 position;
   attribute vec2 texCoords;
   uniform mat4 matrix;

   varying vec2 coords;

   void main(void) {
        gl_Position = matrix * vec4(position, 0.0, 1.0);
        coords = texCoords;
   }
   """

_texFragShd = """
    uniform sampler2D tex;

    varying vec2 coords;

    void main(void) {
        gl_FragColor = texture2D(tex, coords);
        gl_FragColor.a = 1.0;
    }
    """

# BackendOpenGL ###############################################################


class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
    """OpenGL-based Plot backend.

    WARNINGS:
    Unless stated otherwise, this API is NOT thread-safe and MUST be
    called from the main thread.
    When numpy arrays are passed as arguments to the API (through
    :func:`addCurve` and :func:`addImage`), they are copied only if
    required.
    So, the caller should not modify these arrays afterwards.
    """

    _sigPostRedisplay = qt.Signal()
    """Signal handling automatic asynchronous replot"""

    def __init__(self, plot, parent=None, f=qt.Qt.WindowFlags()):
        glu.OpenGLWidget.__init__(self, parent,
                                  alphaBufferSize=8,
                                  depthBufferSize=0,
                                  stencilBufferSize=0,
                                  version=(2, 1),
                                  f=f)
        BackendBase.BackendBase.__init__(self, plot, parent)

        self._backgroundColor = 1., 1., 1., 1.
        self._dataBackgroundColor = 1., 1., 1., 1.

        self.matScreenProj = glutils.mat4Identity()

        self._progBase = glu.Program(
            _baseVertShd, _baseFragShd, attrib0='position')
        self._progTex = glu.Program(
            _texVertShd, _texFragShd, attrib0='position')
        self._plotFBOs = weakref.WeakKeyDictionary()

        self._keepDataAspectRatio = False

        self._crosshairCursor = None
        self._mousePosInPixels = None

        self._glGarbageCollector = []

        self._plotFrame = glutils.GLPlotFrame2D(
            foregroundColor=(0., 0., 0., 1.),
            gridColor=(.7, .7, .7, 1.),
            marginRatios=(.15, .1, .1, .15))
        self._plotFrame.size = (  # Init size with size int
            int(self.getDevicePixelRatio() * 640),
            int(self.getDevicePixelRatio() * 480))

        # Make postRedisplay asynchronous using Qt signal
        self._sigPostRedisplay.connect(
            super(BackendOpenGL, self).postRedisplay,
            qt.Qt.QueuedConnection)

        self.setAutoFillBackground(False)
        self.setMouseTracking(True)

    # QWidget

    _MOUSE_BTNS = {1: 'left', 2: 'right', 4: 'middle'}

    def sizeHint(self):
        return qt.QSize(8 * 80, 6 * 80)  # Mimic MatplotlibBackend

    def mousePressEvent(self, event):
        if event.button() not in self._MOUSE_BTNS:
            return super(BackendOpenGL, self).mousePressEvent(event)
        self._plot.onMousePress(
            event.x(), event.y(), self._MOUSE_BTNS[event.button()])
        event.accept()

    def mouseMoveEvent(self, event):
        qtPos = event.x(), event.y()

        previousMousePosInPixels = self._mousePosInPixels
        if qtPos == self._mouseInPlotArea(*qtPos):
            devicePixelRatio = self.getDevicePixelRatio()
            devicePos = qtPos[0] * devicePixelRatio, qtPos[1] * devicePixelRatio
            self._mousePosInPixels = devicePos  # Mouse in plot area
        else:
            self._mousePosInPixels = None  # Mouse outside plot area

        if (self._crosshairCursor is not None and
                previousMousePosInPixels != self._mousePosInPixels):
            # Avoid replot when cursor remains outside plot area
            self._plot._setDirtyPlot(overlayOnly=True)

        self._plot.onMouseMove(*qtPos)
        event.accept()

    def mouseReleaseEvent(self, event):
        if event.button() not in self._MOUSE_BTNS:
            return super(BackendOpenGL, self).mouseReleaseEvent(event)
        self._plot.onMouseRelease(
            event.x(), event.y(), self._MOUSE_BTNS[event.button()])
        event.accept()

    def wheelEvent(self, event):
        if hasattr(event, 'angleDelta'):  # Qt 5
            delta = event.angleDelta().y()
        else:  # Qt 4 support
            delta = event.delta()
        angleInDegrees = delta / 8.
        self._plot.onMouseWheel(event.x(), event.y(), angleInDegrees)
        event.accept()

    def leaveEvent(self, _):
        self._plot.onMouseLeaveWidget()

    # OpenGLWidget API

    def initializeGL(self):
        gl.testGL()

        gl.glClearStencil(0)

        gl.glEnable(gl.GL_BLEND)
        # gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
        gl.glBlendFuncSeparate(gl.GL_SRC_ALPHA,
                               gl.GL_ONE_MINUS_SRC_ALPHA,
                               gl.GL_ONE,
                               gl.GL_ONE)

        # For lines
        gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)

        # For points
        gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE)  # OpenGL 2
        gl.glEnable(gl.GL_POINT_SPRITE)  # OpenGL 2
        # gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)

    def _paintDirectGL(self):
        self._renderPlotAreaGL()
        self._plotFrame.render()
        self._renderOverlayGL()

    def _paintFBOGL(self):
        context = glu.Context.getCurrent()
        plotFBOTex = self._plotFBOs.get(context)
        if (self._plot._getDirtyPlot() or self._plotFrame.isDirty or
                plotFBOTex is None):
            self._plotVertices = (
                # Vertex coordinates
                numpy.array(((-1., -1.), (1., -1.), (-1., 1.), (1., 1.)),
                             dtype=numpy.float32),
                 # Texture coordinates
                 numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)),
                             dtype=numpy.float32))
            if plotFBOTex is None or \
               plotFBOTex.shape[1] != self._plotFrame.size[0] or \
               plotFBOTex.shape[0] != self._plotFrame.size[1]:
                if plotFBOTex is not None:
                    plotFBOTex.discard()
                plotFBOTex = glu.FramebufferTexture(
                    gl.GL_RGBA,
                    shape=(self._plotFrame.size[1],
                           self._plotFrame.size[0]),
                    minFilter=gl.GL_NEAREST,
                    magFilter=gl.GL_NEAREST,
                    wrap=(gl.GL_CLAMP_TO_EDGE,
                          gl.GL_CLAMP_TO_EDGE))
                self._plotFBOs[context] = plotFBOTex

            with plotFBOTex:
                gl.glClearColor(*self._backgroundColor)
                gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_STENCIL_BUFFER_BIT)
                self._renderPlotAreaGL()
                self._plotFrame.render()

        # Render plot in screen coords
        gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])

        self._progTex.use()
        texUnit = 0

        gl.glUniform1i(self._progTex.uniforms['tex'], texUnit)
        gl.glUniformMatrix4fv(self._progTex.uniforms['matrix'], 1, gl.GL_TRUE,
                              glutils.mat4Identity().astype(numpy.float32))

        gl.glEnableVertexAttribArray(self._progTex.attributes['position'])
        gl.glVertexAttribPointer(self._progTex.attributes['position'],
                                 2,
                                 gl.GL_FLOAT,
                                 gl.GL_FALSE,
                                 0,
                                 self._plotVertices[0])

        gl.glEnableVertexAttribArray(self._progTex.attributes['texCoords'])
        gl.glVertexAttribPointer(self._progTex.attributes['texCoords'],
                                 2,
                                 gl.GL_FLOAT,
                                 gl.GL_FALSE,
                                 0,
                                 self._plotVertices[1])

        with plotFBOTex.texture:
            gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._plotVertices[0]))

        self._renderOverlayGL()

    def paintGL(self):
        with glu.Context.current(self.context()):
            # Release OpenGL resources
            for item in self._glGarbageCollector:
                item.discard()
            self._glGarbageCollector = []

            gl.glClearColor(*self._backgroundColor)
            gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_STENCIL_BUFFER_BIT)

            # Check if window is large enough
            if self._plotFrame.plotSize <= (2, 2):
                return

            # Sync plot frame with window
            self._plotFrame.devicePixelRatio = self.getDevicePixelRatio()
            # self._paintDirectGL()
            self._paintFBOGL()

    def _renderItems(self, overlay=False):
        """Render items according to :class:`PlotWidget` order

        Note: Scissor test should already be set.

        :param bool overlay:
            False (the default) to render item that are not overlays.
            True to render items that are overlays.
        """
        # Values that are often used
        plotWidth, plotHeight = self._plotFrame.plotSize
        isXLog = self._plotFrame.xAxis.isLog
        isYLog = self._plotFrame.yAxis.isLog
        isYInverted = self._plotFrame.isYAxisInverted

        # Used by marker rendering
        labels = []
        pixelOffset = 3

        context = glutils.RenderContext(
            isXLog=isXLog, isYLog=isYLog, dpi=self.getDotsPerInch())

        for plotItem in self.getItemsFromBackToFront(
                condition=lambda i: i.isVisible() and i.isOverlay() == overlay):
            if plotItem._backendRenderer is None:
                continue

            item = plotItem._backendRenderer

            if isinstance(item, glutils.GLPlotItem):  # Render data items
                gl.glViewport(self._plotFrame.margins.left,
                              self._plotFrame.margins.bottom,
                              plotWidth, plotHeight)
                # Set matrix
                if item.yaxis == 'right':
                    context.matrix = self._plotFrame.transformedDataY2ProjMat
                else:
                    context.matrix = self._plotFrame.transformedDataProjMat
                item.render(context)

            elif isinstance(item, _ShapeItem):  # Render shape items
                gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])

                if ((isXLog and numpy.min(item['x']) < FLOAT32_MINPOS) or
                        (isYLog and numpy.min(item['y']) < FLOAT32_MINPOS)):
                    # Ignore items <= 0. on log axes
                    continue

                if item['shape'] == 'hline':
                    width = self._plotFrame.size[0]
                    _, yPixel = self._plotFrame.dataToPixel(
                        0.5 * sum(self._plotFrame.dataRanges[0]),
                        item['y'],
                        axis='left')
                    subShapes = [numpy.array(((0., yPixel), (width, yPixel)),
                                             dtype=numpy.float32)]

                elif item['shape'] == 'vline':
                    xPixel, _ = self._plotFrame.dataToPixel(
                        item['x'],
                        0.5 * sum(self._plotFrame.dataRanges[1]),
                        axis='left')
                    height = self._plotFrame.size[1]
                    subShapes = [numpy.array(((xPixel, 0), (xPixel, height)),
                                             dtype=numpy.float32)]

                else:
                    # Split sub-shapes at not finite values
                    splits = numpy.nonzero(numpy.logical_not(numpy.logical_and(
                        numpy.isfinite(item['x']), numpy.isfinite(item['y']))))[0]
                    splits = numpy.concatenate(([-1], splits, [len(item['x'])]))
                    subShapes = []
                    for begin, end in zip(splits[:-1] + 1, splits[1:]):
                        if end > begin:
                            subShapes.append(numpy.array([
                                self._plotFrame.dataToPixel(x, y, axis='left')
                                for (x, y) in zip(item['x'][begin:end], item['y'][begin:end])]))

                for points in subShapes:  # Draw each sub-shape
                    # Draw the fill
                    if (item['fill'] is not None and
                            item['shape'] not in ('hline', 'vline')):
                        self._progBase.use()
                        gl.glUniformMatrix4fv(
                            self._progBase.uniforms['matrix'], 1, gl.GL_TRUE,
                            self.matScreenProj.astype(numpy.float32))
                        gl.glUniform2i(self._progBase.uniforms['isLog'], False, False)
                        gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)

                        shape2D = glutils.FilledShape2D(
                            points, style=item['fill'], color=item['color'])
                        shape2D.render(
                            posAttrib=self._progBase.attributes['position'],
                            colorUnif=self._progBase.uniforms['color'],
                            hatchStepUnif=self._progBase.uniforms['hatchStep'])

                    # Draw the stroke
                    if item['linestyle'] not in ('', ' ', None):
                        if item['shape'] != 'polylines':
                            # close the polyline
                            points = numpy.append(points,
                                                  numpy.atleast_2d(points[0]), axis=0)

                        lines = glutils.GLLines2D(
                            points[:, 0], points[:, 1],
                            style=item['linestyle'],
                            color=item['color'],
                            dash2ndColor=item['linebgcolor'],
                            width=item['linewidth'])
                        context.matrix = self.matScreenProj
                        lines.render(context)

            elif isinstance(item, _MarkerItem):
                gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])

                xCoord, yCoord, yAxis = item['x'], item['y'], item['yaxis']

                if ((isXLog and xCoord is not None and xCoord <= 0) or
                        (isYLog and yCoord is not None and yCoord <= 0)):
                    # Do not render markers with negative coords on log axis
                    continue

                if xCoord is None or yCoord is None:
                    if xCoord is None:  # Horizontal line in data space
                        pixelPos = self._plotFrame.dataToPixel(
                            0.5 * sum(self._plotFrame.dataRanges[0]),
                            yCoord,
                            axis=yAxis)

                        if item['text'] is not None:
                            x = self._plotFrame.size[0] - \
                                self._plotFrame.margins.right - pixelOffset
                            y = pixelPos[1] - pixelOffset
                            label = glutils.Text2D(
                                item['text'], x, y,
                                color=item['color'],
                                bgColor=(1., 1., 1., 0.5),
                                align=glutils.RIGHT,
                                valign=glutils.BOTTOM,
                                devicePixelRatio=self.getDevicePixelRatio())
                            labels.append(label)

                        width = self._plotFrame.size[0]
                        lines = glutils.GLLines2D(
                            (0, width), (pixelPos[1], pixelPos[1]),
                            style=item['linestyle'],
                            color=item['color'],
                            width=item['linewidth'])
                        context.matrix = self.matScreenProj
                        lines.render(context)

                    else:  # yCoord is None: vertical line in data space
                        yRange = self._plotFrame.dataRanges[1 if yAxis == 'left' else 2]
                        pixelPos = self._plotFrame.dataToPixel(
                            xCoord, 0.5 * sum(yRange), axis=yAxis)

                        if item['text'] is not None:
                            x = pixelPos[0] + pixelOffset
                            y = self._plotFrame.margins.top + pixelOffset
                            label = glutils.Text2D(
                                item['text'], x, y,
                                color=item['color'],
                                bgColor=(1., 1., 1., 0.5),
                                align=glutils.LEFT,
                                valign=glutils.TOP,
                                devicePixelRatio=self.getDevicePixelRatio())
                            labels.append(label)

                        height = self._plotFrame.size[1]
                        lines = glutils.GLLines2D(
                            (pixelPos[0], pixelPos[0]), (0, height),
                            style=item['linestyle'],
                            color=item['color'],
                            width=item['linewidth'])
                        context.matrix = self.matScreenProj
                        lines.render(context)

                else:
                    xmin, xmax = self._plot.getXAxis().getLimits()
                    ymin, ymax = self._plot.getYAxis(axis=yAxis).getLimits()
                    if not xmin < xCoord < xmax or not ymin < yCoord < ymax:
                        # Do not render markers outside visible plot area
                        continue
                    pixelPos = self._plotFrame.dataToPixel(
                        xCoord, yCoord, axis=yAxis)

                    if isYInverted:
                        valign = glutils.BOTTOM
                        vPixelOffset = -pixelOffset
                    else:
                        valign = glutils.TOP
                        vPixelOffset = pixelOffset

                    if item['text'] is not None:
                        x = pixelPos[0] + pixelOffset
                        y = pixelPos[1] + vPixelOffset
                        label = glutils.Text2D(
                            item['text'], x, y,
                            color=item['color'],
                            bgColor=(1., 1., 1., 0.5),
                            align=glutils.LEFT,
                            valign=valign,
                            devicePixelRatio=self.getDevicePixelRatio())
                        labels.append(label)

                    # For now simple implementation: using a curve for each marker
                    # Should pack all markers to a single set of points
                    markerCurve = glutils.GLPlotCurve2D(
                        numpy.array((pixelPos[0],), dtype=numpy.float64),
                        numpy.array((pixelPos[1],), dtype=numpy.float64),
                        marker=item['symbol'],
                        markerColor=item['color'],
                        markerSize=11)

                    context = glutils.RenderContext(
                        matrix=self.matScreenProj,
                        isXLog=False,
                        isYLog=False,
                        dpi=self.getDotsPerInch())
                    markerCurve.render(context)
                    markerCurve.discard()

            else:
                _logger.error('Unsupported item: %s', str(item))
                continue

        # Render marker labels
        gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
        for label in labels:
            label.render(self.matScreenProj)

    def _renderOverlayGL(self):
        """Render overlay layer: overlay items and crosshair."""
        plotWidth, plotHeight = self._plotFrame.plotSize

        # Scissor to plot area
        gl.glScissor(self._plotFrame.margins.left,
                     self._plotFrame.margins.bottom,
                     plotWidth, plotHeight)
        gl.glEnable(gl.GL_SCISSOR_TEST)

        self._renderItems(overlay=True)

        # Render crosshair cursor
        if self._crosshairCursor is not None and self._mousePosInPixels is not None:
            self._progBase.use()
            gl.glUniform2i(self._progBase.uniforms['isLog'], False, False)
            gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
            posAttrib = self._progBase.attributes['position']
            matrixUnif = self._progBase.uniforms['matrix']
            colorUnif = self._progBase.uniforms['color']
            hatchStepUnif = self._progBase.uniforms['hatchStep']

            gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])

            gl.glUniformMatrix4fv(matrixUnif, 1, gl.GL_TRUE,
                                  self.matScreenProj.astype(numpy.float32))

            color, lineWidth = self._crosshairCursor
            gl.glUniform4f(colorUnif, *color)
            gl.glUniform1i(hatchStepUnif, 0)

            xPixel, yPixel = self._mousePosInPixels
            xPixel, yPixel = xPixel + 0.5, yPixel + 0.5
            vertices = numpy.array(((0., yPixel),
                                    (self._plotFrame.size[0], yPixel),
                                    (xPixel, 0.),
                                    (xPixel, self._plotFrame.size[1])),
                                   dtype=numpy.float32)

            gl.glEnableVertexAttribArray(posAttrib)
            gl.glVertexAttribPointer(posAttrib,
                                     2,
                                     gl.GL_FLOAT,
                                     gl.GL_FALSE,
                                     0, vertices)
            gl.glLineWidth(lineWidth)
            gl.glDrawArrays(gl.GL_LINES, 0, len(vertices))

        gl.glDisable(gl.GL_SCISSOR_TEST)

    def _renderPlotAreaGL(self):
        """Render base layer of plot area.

        It renders the background, grid and items except overlays
        """
        plotWidth, plotHeight = self._plotFrame.plotSize

        gl.glScissor(self._plotFrame.margins.left,
                     self._plotFrame.margins.bottom,
                     plotWidth, plotHeight)
        gl.glEnable(gl.GL_SCISSOR_TEST)

        if self._dataBackgroundColor != self._backgroundColor:
            gl.glClearColor(*self._dataBackgroundColor)
            gl.glClear(gl.GL_COLOR_BUFFER_BIT)

        self._plotFrame.renderGrid()

        # Matrix
        trBounds = self._plotFrame.transformedDataRanges
        if trBounds.x[0] != trBounds.x[1] and trBounds.y[0] != trBounds.y[1]:
            # Do rendering of items
            self._renderItems(overlay=False)

        gl.glDisable(gl.GL_SCISSOR_TEST)

    def resizeGL(self, width, height):
        if width == 0 or height == 0:  # Do not resize
            return

        self._plotFrame.size = (
            int(self.getDevicePixelRatio() * width),
            int(self.getDevicePixelRatio() * height))

        self.matScreenProj = glutils.mat4Ortho(
            0, self._plotFrame.size[0],
            self._plotFrame.size[1], 0,
            1, -1)

        # Store current ranges
        previousXRange = self.getGraphXLimits()
        previousYRange = self.getGraphYLimits(axis='left')
        previousYRightRange = self.getGraphYLimits(axis='right')

        (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = \
            self._plotFrame.dataRanges
        self.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)

        # If plot range has changed, then emit signal
        if previousXRange != self.getGraphXLimits():
            self._plot.getXAxis()._emitLimitsChanged()
        if previousYRange != self.getGraphYLimits(axis='left'):
            self._plot.getYAxis(axis='left')._emitLimitsChanged()
        if previousYRightRange != self.getGraphYLimits(axis='right'):
            self._plot.getYAxis(axis='right')._emitLimitsChanged()

    # Add methods

    @staticmethod
    def _castArrayTo(v):
        """Returns best floating type to cast the array to.

        :param numpy.ndarray v: Array to cast
        :rtype: numpy.dtype
        :raise ValueError: If dtype is not supported
        """
        if numpy.issubdtype(v.dtype, numpy.floating):
            return numpy.float32 if v.itemsize <= 4 else numpy.float64
        elif numpy.issubdtype(v.dtype, numpy.integer):
            return numpy.float32 if v.itemsize <= 2 else numpy.float64
        else:
            raise ValueError('Unsupported data type')

    def addCurve(self, x, y,
                 color, symbol, linewidth, linestyle,
                 yaxis,
                 xerror, yerror,
                 fill, alpha, symbolsize, baseline):
        for parameter in (x, y, color, symbol, linewidth, linestyle,
                          yaxis, fill, symbolsize):
            assert parameter is not None
        assert yaxis in ('left', 'right')

        # Convert input data
        x = numpy.array(x, copy=False)
        y = numpy.array(y, copy=False)

        # Check if float32 is enough
        if (self._castArrayTo(x) is numpy.float32 and
                self._castArrayTo(y) is numpy.float32):
            dtype = numpy.float32
        else:
            dtype = numpy.float64

        x = numpy.array(x, dtype=dtype, copy=False, order='C')
        y = numpy.array(y, dtype=dtype, copy=False, order='C')

        # Convert errors to float32
        if xerror is not None:
            xerror = numpy.array(
                xerror, dtype=numpy.float32, copy=False, order='C')
        if yerror is not None:
            yerror = numpy.array(
                yerror, dtype=numpy.float32, copy=False, order='C')

        # Handle axes log scale: convert data

        if self._plotFrame.xAxis.isLog:
            logX = numpy.log10(x)

            if xerror is not None:
                # Transform xerror so that
                # log10(x) +/- xerror' = log10(x +/- xerror)
                if hasattr(xerror, 'shape') and len(xerror.shape) == 2:
                    xErrorMinus, xErrorPlus = xerror[0], xerror[1]
                else:
                    xErrorMinus, xErrorPlus = xerror, xerror
                with numpy.errstate(divide='ignore', invalid='ignore'):
                    # Ignore divide by zero, invalid value encountered in log10
                    xErrorMinus = logX - numpy.log10(x - xErrorMinus)
                xErrorPlus = numpy.log10(x + xErrorPlus) - logX
                xerror = numpy.array((xErrorMinus, xErrorPlus),
                                     dtype=numpy.float32)

            x = logX

        isYLog = (yaxis == 'left' and self._plotFrame.yAxis.isLog) or (
            yaxis == 'right' and self._plotFrame.y2Axis.isLog)

        if isYLog:
            logY = numpy.log10(y)

            if yerror is not None:
                # Transform yerror so that
                # log10(y) +/- yerror' = log10(y +/- yerror)
                if hasattr(yerror, 'shape') and len(yerror.shape) == 2:
                    yErrorMinus, yErrorPlus = yerror[0], yerror[1]
                else:
                    yErrorMinus, yErrorPlus = yerror, yerror
                with numpy.errstate(divide='ignore', invalid='ignore'):
                    # Ignore divide by zero, invalid value encountered in log10
                    yErrorMinus = logY - numpy.log10(y - yErrorMinus)
                yErrorPlus = numpy.log10(y + yErrorPlus) - logY
                yerror = numpy.array((yErrorMinus, yErrorPlus),
                                     dtype=numpy.float32)

            y = logY

        # TODO check if need more filtering of error (e.g., clip to positive)

        # TODO check and improve this
        if (len(color) == 4 and
                type(color[3]) in [type(1), numpy.uint8, numpy.int8]):
            color = numpy.array(color, dtype=numpy.float32) / 255.

        if isinstance(color, numpy.ndarray) and color.ndim == 2:
            colorArray = color
            color = None
        else:
            colorArray = None
            color = colors.rgba(color)

        if alpha < 1.:  # Apply image transparency
            if colorArray is not None and colorArray.shape[1] == 4:
                # multiply alpha channel
                colorArray[:, 3] = colorArray[:, 3] * alpha
            if color is not None:
                color = color[0], color[1], color[2], color[3] * alpha

        fillColor = None
        if fill is True:
            fillColor = color
        curve = glutils.GLPlotCurve2D(
            x, y, colorArray,
            xError=xerror,
            yError=yerror,
            lineStyle=linestyle,
            lineColor=color,
            lineWidth=linewidth,
            marker=symbol,
            markerColor=color,
            markerSize=symbolsize,
            fillColor=fillColor,
            baseline=baseline,
            isYLog=isYLog)
        curve.yaxis = 'left' if yaxis is None else yaxis

        if yaxis == "right":
            self._plotFrame.isY2Axis = True

        return curve

    def addImage(self, data,
                 origin, scale,
                 colormap, alpha):
        for parameter in (data, origin, scale):
            assert parameter is not None

        if data.ndim == 2:
            # Ensure array is contiguous and eventually convert its type
            dtypes = [dtype for dtype in (
                numpy.float32, numpy.float16, numpy.uint8, numpy.uint16)
                if glu.isSupportedGLType(dtype)]
            if data.dtype in dtypes:
                data = numpy.array(data, copy=False, order='C')
            else:
                _logger.info(
                    'addImage: Convert %s data to float32', str(data.dtype))
                data = numpy.array(data, dtype=numpy.float32, order='C')

            normalization = colormap.getNormalization()
            if normalization in glutils.GLPlotColormap.SUPPORTED_NORMALIZATIONS:
                # Fast path applying colormap on the GPU
                cmapRange = colormap.getColormapRange(data=data)
                colormapLut = colormap.getNColors(nbColors=256)
                gamma = colormap.getGammaNormalizationParameter()
                nanColor = colors.rgba(colormap.getNaNColor())

                image = glutils.GLPlotColormap(
                    data,
                    origin,
                    scale,
                    colormapLut,
                    normalization,
                    gamma,
                    cmapRange,
                    alpha,
                    nanColor)

            else:  # Fallback applying colormap on CPU
                rgba = colormap.applyToData(data)
                image = glutils.GLPlotRGBAImage(rgba, origin, scale, alpha)

        elif len(data.shape) == 3:
            # For RGB, RGBA data
            assert data.shape[2] in (3, 4)

            if numpy.issubdtype(data.dtype, numpy.floating):
                data = numpy.array(data, dtype=numpy.float32, copy=False)
            elif data.dtype in [numpy.uint8, numpy.uint16]:
                pass
            elif numpy.issubdtype(data.dtype, numpy.integer):
                data = numpy.array(data, dtype=numpy.uint8, copy=False)
            else:
                raise ValueError('Unsupported data type')

            image = glutils.GLPlotRGBAImage(data, origin, scale, alpha)

        else:
            raise RuntimeError("Unsupported data shape {0}".format(data.shape))

        # TODO is this needed?
        if self._plotFrame.xAxis.isLog and image.xMin <= 0.:
            raise RuntimeError(
                'Cannot add image with X <= 0 with X axis log scale')
        if self._plotFrame.yAxis.isLog and image.yMin <= 0.:
            raise RuntimeError(
                'Cannot add image with Y <= 0 with Y axis log scale')

        return image

    def addTriangles(self, x, y, triangles,
                     color, alpha):
        # Handle axes log scale: convert data
        if self._plotFrame.xAxis.isLog:
            x = numpy.log10(x)
        if self._plotFrame.yAxis.isLog:
            y = numpy.log10(y)

        triangles = glutils.GLPlotTriangles(x, y, color, triangles, alpha)

        return triangles

    def addShape(self, x, y, shape, color, fill, overlay,
                 linestyle, linewidth, linebgcolor):
        x = numpy.array(x, copy=False)
        y = numpy.array(y, copy=False)

        # TODO is this needed?
        if self._plotFrame.xAxis.isLog and x.min() <= 0.:
            raise RuntimeError(
                'Cannot add item with X <= 0 with X axis log scale')
        if self._plotFrame.yAxis.isLog and y.min() <= 0.:
            raise RuntimeError(
                'Cannot add item with Y <= 0 with Y axis log scale')

        return _ShapeItem(x, y, shape, color, fill, overlay,
                          linestyle, linewidth, linebgcolor)

    def addMarker(self, x, y, text, color,
                  symbol, linestyle, linewidth, constraint, yaxis):
        return _MarkerItem(x, y, text, color,
                           symbol, linestyle, linewidth, constraint, yaxis)

    # Remove methods

    def remove(self, item):
        if isinstance(item, glutils.GLPlotItem):
            if item.yaxis == 'right':
                # Check if some curves remains on the right Y axis
                y2AxisItems = (item for item in self._plot.getItems()
                               if isinstance(item, items.YAxisMixIn) and
                               item.getYAxis() == 'right')
                self._plotFrame.isY2Axis = next(y2AxisItems, None) is not None

            if item.isInitialized():
                self._glGarbageCollector.append(item)

        elif isinstance(item, (_MarkerItem, _ShapeItem)):
            pass  # No-op

        else:
            _logger.error('Unsupported item: %s', str(item))

    # Interaction methods

    _QT_CURSORS = {
        BackendBase.CURSOR_DEFAULT: qt.Qt.ArrowCursor,
        BackendBase.CURSOR_POINTING: qt.Qt.PointingHandCursor,
        BackendBase.CURSOR_SIZE_HOR: qt.Qt.SizeHorCursor,
        BackendBase.CURSOR_SIZE_VER: qt.Qt.SizeVerCursor,
        BackendBase.CURSOR_SIZE_ALL: qt.Qt.SizeAllCursor,
    }

    def setGraphCursorShape(self, cursor):
        if cursor is None:
            super(BackendOpenGL, self).unsetCursor()
        else:
            cursor = self._QT_CURSORS[cursor]
            super(BackendOpenGL, self).setCursor(qt.QCursor(cursor))

    def setGraphCursor(self, flag, color, linewidth, linestyle):
        if linestyle != '-':
            _logger.warning(
                "BackendOpenGL.setGraphCursor linestyle parameter ignored")

        if flag:
            color = colors.rgba(color)
            crosshairCursor = color, linewidth
        else:
            crosshairCursor = None

        if crosshairCursor != self._crosshairCursor:
            self._crosshairCursor = crosshairCursor

    _PICK_OFFSET = 3  # Offset in pixel used for picking

    def _mouseInPlotArea(self, x, y):
        """Returns closest visible position in the plot.

        This is performed in Qt widget pixel, not device pixel.

        :param float x: X coordinate in Qt widget pixel
        :param float y: Y coordinate in Qt widget pixel
        :return: (x, y) closest point in the plot.
        :rtype: List[float]
        """
        left, top, width, height = self.getPlotBoundsInPixels()
        return (numpy.clip(x, left, left + width - 1),  # TODO -1?
                numpy.clip(y, top, top + height - 1))

    def __pickCurves(self, item, x, y):
        """Perform picking on a curve item.

        :param GLPlotCurve2D item:
        :param float x: X position of the mouse in widget coordinates
        :param float y: Y position of the mouse in widget coordinates
        :return: List of indices of picked points or None if not picked
        :rtype: Union[List[int],None]
        """
        offset = self._PICK_OFFSET
        if item.marker is not None:
            # Convert markerSize from points to qt pixels
            qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio()
            size = item.markerSize / 72. * qtDpi
            offset = max(size / 2., offset)
        if item.lineStyle is not None:
            # Convert line width from points to qt pixels
            qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio()
            lineWidth = item.lineWidth / 72. * qtDpi
            offset = max(lineWidth / 2., offset)

        inAreaPos = self._mouseInPlotArea(x - offset, y - offset)
        dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1],
                                         axis=item.yaxis, check=True)
        if dataPos is None:
            return None
        xPick0, yPick0 = dataPos

        inAreaPos = self._mouseInPlotArea(x + offset, y + offset)
        dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1],
                                         axis=item.yaxis, check=True)
        if dataPos is None:
            return None
        xPick1, yPick1 = dataPos

        if xPick0 < xPick1:
            xPickMin, xPickMax = xPick0, xPick1
        else:
            xPickMin, xPickMax = xPick1, xPick0

        if yPick0 < yPick1:
            yPickMin, yPickMax = yPick0, yPick1
        else:
            yPickMin, yPickMax = yPick1, yPick0

        # Apply log scale if axis is log
        if self._plotFrame.xAxis.isLog:
            xPickMin = numpy.log10(xPickMin)
            xPickMax = numpy.log10(xPickMax)

        if (item.yaxis == 'left' and self._plotFrame.yAxis.isLog) or (
                item.yaxis == 'right' and self._plotFrame.y2Axis.isLog):
            yPickMin = numpy.log10(yPickMin)
            yPickMax = numpy.log10(yPickMax)

        return item.pick(xPickMin, yPickMin,
                         xPickMax, yPickMax)

    def pickItem(self, x, y, item):
        # Picking is performed in Qt widget pixels not device pixels
        dataPos = self._plot.pixelToData(x, y, axis='left', check=True)
        if dataPos is None:
            return None  # Outside plot area

        if item is None:
            _logger.error("No item provided for picking")
            return None

        # Pick markers
        if isinstance(item, _MarkerItem):
            yaxis = item['yaxis']
            pixelPos = self._plot.dataToPixel(
                item['x'], item['y'], axis=yaxis, check=False)
            if pixelPos is None:
                return None  # negative coord on a log axis

            if item['x'] is None:  # Horizontal line
                pt1 = self._plot.pixelToData(
                    x, y - self._PICK_OFFSET, axis=yaxis, check=False)
                pt2 = self._plot.pixelToData(
                    x, y + self._PICK_OFFSET, axis=yaxis, check=False)
                isPicked = (min(pt1[1], pt2[1]) <= item['y'] <=
                            max(pt1[1], pt2[1]))

            elif item['y'] is None:  # Vertical line
                pt1 = self._plot.pixelToData(
                    x - self._PICK_OFFSET, y, axis=yaxis, check=False)
                pt2 = self._plot.pixelToData(
                    x + self._PICK_OFFSET, y, axis=yaxis, check=False)
                isPicked = (min(pt1[0], pt2[0]) <= item['x'] <=
                            max(pt1[0], pt2[0]))

            else:
                isPicked = (
                    numpy.fabs(x - pixelPos[0]) <= self._PICK_OFFSET and
                    numpy.fabs(y - pixelPos[1]) <= self._PICK_OFFSET)

            return (0,) if isPicked else None

        # Pick image, curve, triangles
        elif isinstance(item, glutils.GLPlotItem):
            if isinstance(item, glutils.GLPlotCurve2D):
                return self.__pickCurves(item, x, y)
            else:
                return item.pick(*dataPos)  # Might be None

    # Update curve

    def setCurveColor(self, curve, color):
        pass  # TODO

    # Misc.

    def getWidgetHandle(self):
        return self

    def postRedisplay(self):
        self._sigPostRedisplay.emit()

    def replot(self):
        self.update()  # async redraw
        # self.repaint()  # immediate redraw

    def saveGraph(self, fileName, fileFormat, dpi):
        if dpi is not None:
            _logger.warning("saveGraph ignores dpi parameter")

        if fileFormat not in ['png', 'ppm', 'svg', 'tiff']:
            raise NotImplementedError('Unsupported format: %s' % fileFormat)

        if not self.isValid():
            _logger.error('OpenGL 2.1 not available, cannot save OpenGL image')
            width, height = self._plotFrame.size
            data = numpy.zeros((height, width, 3), dtype=numpy.uint8)
        else:
            self.makeCurrent()

            data = numpy.empty(
                (self._plotFrame.size[1], self._plotFrame.size[0], 3),
                dtype=numpy.uint8, order='C')

            context = self.context()
            framebufferTexture = self._plotFBOs.get(context)
            if framebufferTexture is None:
                # Fallback, supports direct rendering mode: _paintDirectGL
                # might have issues as it can read on-screen framebuffer
                fboName = self.defaultFramebufferObject()
                width, height = self._plotFrame.size
            else:
                fboName = framebufferTexture.name
                height, width = framebufferTexture.shape

            previousFramebuffer = gl.glGetInteger(gl.GL_FRAMEBUFFER_BINDING)
            gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fboName)
            gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
            gl.glReadPixels(0, 0, width, height,
                            gl.GL_RGB, gl.GL_UNSIGNED_BYTE, data)
            gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, previousFramebuffer)

            # glReadPixels gives bottom to top,
            # while images are stored as top to bottom
            data = numpy.flipud(data)

        # fileName is either a file-like object or a str
        saveImageToFile(data, fileName, fileFormat)

    # Graph labels

    def setGraphTitle(self, title):
        self._plotFrame.title = title

    def setGraphXLabel(self, label):
        self._plotFrame.xAxis.title = label

    def setGraphYLabel(self, label, axis):
        if axis == 'left':
            self._plotFrame.yAxis.title = label
        else:  # right axis
            self._plotFrame.y2Axis.title = label

    # Graph limits

    def _setDataRanges(self, xlim=None, ylim=None, y2lim=None):
        """Set the visible range of data in the plot frame.

        This clips the ranges to possible values (takes care of float32
        range + positive range for log).
        This also takes care of non-orthogonal axes.

        This should be moved to PlotFrame.
        """
        # Update axes range with a clipped range if too wide
        self._plotFrame.setDataRanges(xlim, ylim, y2lim)

    def _ensureAspectRatio(self, keepDim=None):
        """Update plot bounds in order to keep aspect ratio.

        Warning: keepDim on right Y axis is not implemented !

        :param str keepDim: The dimension to maintain: 'x', 'y' or None.
            If None (the default), the dimension with the largest range.
        """
        plotWidth, plotHeight = self._plotFrame.plotSize
        if plotWidth <= 2 or plotHeight <= 2:
            return

        if keepDim is None:
            ranges = self._plot.getDataRange()
            if (ranges.y is not None and
                ranges.x is not None and
                (ranges.y[1] - ranges.y[0]) != 0.):
                dataRatio = (ranges.x[1] - ranges.x[0]) / float(ranges.y[1] - ranges.y[0])
                plotRatio = plotWidth / float(plotHeight)  # Test != 0 before

                keepDim = 'x' if dataRatio > plotRatio else 'y'
            else:  # Limit case
                keepDim = 'x'

        (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = \
            self._plotFrame.dataRanges
        if keepDim == 'y':
            dataW = (yMax - yMin) * plotWidth / float(plotHeight)
            xCenter = 0.5 * (xMin + xMax)
            xMin = xCenter - 0.5 * dataW
            xMax = xCenter + 0.5 * dataW
        elif keepDim == 'x':
            dataH = (xMax - xMin) * plotHeight / float(plotWidth)
            yCenter = 0.5 * (yMin + yMax)
            yMin = yCenter - 0.5 * dataH
            yMax = yCenter + 0.5 * dataH
            y2Center = 0.5 * (y2Min + y2Max)
            y2Min = y2Center - 0.5 * dataH
            y2Max = y2Center + 0.5 * dataH
        else:
            raise RuntimeError('Unsupported dimension to keep: %s' % keepDim)

        # Update plot frame bounds
        self._setDataRanges(xlim=(xMin, xMax),
                            ylim=(yMin, yMax),
                            y2lim=(y2Min, y2Max))

    def _setPlotBounds(self, xRange=None, yRange=None, y2Range=None,
                       keepDim=None):
        # Update axes range with a clipped range if too wide
        self._setDataRanges(xlim=xRange,
                            ylim=yRange,
                            y2lim=y2Range)

        # Keep data aspect ratio
        if self.isKeepDataAspectRatio():
            self._ensureAspectRatio(keepDim)

    def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None):
        assert xmin < xmax
        assert ymin < ymax

        if y2min is None or y2max is None:
            y2Range = None
        else:
            assert y2min < y2max
            y2Range = y2min, y2max
        self._setPlotBounds((xmin, xmax), (ymin, ymax), y2Range)

    def getGraphXLimits(self):
        return self._plotFrame.dataRanges.x

    def setGraphXLimits(self, xmin, xmax):
        assert xmin < xmax
        self._setPlotBounds(xRange=(xmin, xmax), keepDim='x')

    def getGraphYLimits(self, axis):
        assert axis in ("left", "right")
        if axis == "left":
            return self._plotFrame.dataRanges.y
        else:
            return self._plotFrame.dataRanges.y2

    def setGraphYLimits(self, ymin, ymax, axis):
        assert ymin < ymax
        assert axis in ("left", "right")

        if axis == "left":
            self._setPlotBounds(yRange=(ymin, ymax), keepDim='y')
        else:
            self._setPlotBounds(y2Range=(ymin, ymax), keepDim='y')

    # Graph axes

    def getXAxisTimeZone(self):
        return self._plotFrame.xAxis.timeZone

    def setXAxisTimeZone(self, tz):
        self._plotFrame.xAxis.timeZone = tz

    def isXAxisTimeSeries(self):
        return self._plotFrame.xAxis.isTimeSeries

    def setXAxisTimeSeries(self, isTimeSeries):
        self._plotFrame.xAxis.isTimeSeries = isTimeSeries

    def setXAxisLogarithmic(self, flag):
        if flag != self._plotFrame.xAxis.isLog:
            if flag and self._keepDataAspectRatio:
                _logger.warning(
                    "KeepDataAspectRatio is ignored with log axes")

            self._plotFrame.xAxis.isLog = flag

    def setYAxisLogarithmic(self, flag):
        if (flag != self._plotFrame.yAxis.isLog or
                flag != self._plotFrame.y2Axis.isLog):
            if flag and self._keepDataAspectRatio:
                _logger.warning(
                    "KeepDataAspectRatio is ignored with log axes")

            self._plotFrame.yAxis.isLog = flag
            self._plotFrame.y2Axis.isLog = flag

    def setYAxisInverted(self, flag):
        if flag != self._plotFrame.isYAxisInverted:
            self._plotFrame.isYAxisInverted = flag

    def isYAxisInverted(self):
        return self._plotFrame.isYAxisInverted

    def isKeepDataAspectRatio(self):
        if self._plotFrame.xAxis.isLog or self._plotFrame.yAxis.isLog:
            return False
        else:
            return self._keepDataAspectRatio

    def setKeepDataAspectRatio(self, flag):
        if flag and (self._plotFrame.xAxis.isLog or
                     self._plotFrame.yAxis.isLog):
            _logger.warning("KeepDataAspectRatio is ignored with log axes")

        self._keepDataAspectRatio = flag

    def setGraphGrid(self, which):
        assert which in (None, 'major', 'both')
        self._plotFrame.grid = which is not None  # TODO True grid support

    # Data <-> Pixel coordinates conversion

    def dataToPixel(self, x, y, axis):
        result = self._plotFrame.dataToPixel(x, y, axis)
        if result is None:
            return None
        else:
            devicePixelRatio = self.getDevicePixelRatio()
            return tuple(value/devicePixelRatio for value in result)

    def pixelToData(self, x, y, axis):
        devicePixelRatio = self.getDevicePixelRatio()
        return self._plotFrame.pixelToData(
            x * devicePixelRatio, y * devicePixelRatio, axis)

    def getPlotBoundsInPixels(self):
        devicePixelRatio = self.getDevicePixelRatio()
        return tuple(int(value / devicePixelRatio)
            for value in self._plotFrame.plotOrigin + self._plotFrame.plotSize)

    def setAxesMargins(self, left: float, top: float, right: float, bottom: float):
        self._plotFrame.marginRatios = left, top, right, bottom

    def setForegroundColors(self, foregroundColor, gridColor):
        self._plotFrame.foregroundColor = foregroundColor
        self._plotFrame.gridColor = gridColor

    def setBackgroundColors(self, backgroundColor, dataBackgroundColor):
        self._backgroundColor = backgroundColor
        self._dataBackgroundColor = dataBackgroundColor
