summaryrefslogtreecommitdiff
path: root/silx/gui/plot/backends/BackendOpenGL.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/backends/BackendOpenGL.py')
-rw-r--r--silx/gui/plot/backends/BackendOpenGL.py364
1 files changed, 115 insertions, 249 deletions
diff --git a/silx/gui/plot/backends/BackendOpenGL.py b/silx/gui/plot/backends/BackendOpenGL.py
index 9e2cb73..e33d03c 100644
--- a/silx/gui/plot/backends/BackendOpenGL.py
+++ b/silx/gui/plot/backends/BackendOpenGL.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2019 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
@@ -28,7 +28,7 @@ from __future__ import division
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "01/08/2018"
+__date__ = "21/12/2018"
from collections import OrderedDict, namedtuple
from ctypes import c_void_p
@@ -44,10 +44,11 @@ from ... import qt
from ..._glutils import gl
from ... import _glutils as glu
from .glutils import (
+ GLLines2D,
GLPlotCurve2D, GLPlotColormap, GLPlotRGBAImage, GLPlotFrame2D,
mat4Ortho, mat4Identity,
LEFT, RIGHT, BOTTOM, TOP,
- Text2D, Shape2D)
+ Text2D, FilledShape2D)
from .glutils.PlotImageFile import saveImageToFile
_logger = logging.getLogger(__name__)
@@ -338,6 +339,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
f=f)
BackendBase.BackendBase.__init__(self, plot, parent)
+ self._backgroundColor = 1., 1., 1., 1.
+ self._dataBackgroundColor = 1., 1., 1., 1.
+
self.matScreenProj = mat4Identity()
self._progBase = glu.Program(
@@ -357,6 +361,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._glGarbageCollector = []
self._plotFrame = GLPlotFrame2D(
+ foregroundColor=(0., 0., 0., 1.),
+ gridColor=(.7, .7, .7, 1.),
margins={'left': 100, 'right': 50, 'top': 50, 'bottom': 50})
# Make postRedisplay asynchronous using Qt signal
@@ -432,7 +438,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def initializeGL(self):
gl.testGL()
- gl.glClearColor(1., 1., 1., 1.)
gl.glClearStencil(0)
gl.glEnable(gl.GL_BLEND)
@@ -482,6 +487,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
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()
@@ -530,6 +536,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
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
@@ -543,100 +550,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
glu.setGLContextGetter()
_current_context = None
- def _nonOrthoAxesLineMarkerPrimitives(self, marker, pixelOffset):
- """Generates the vertices and label for a line marker.
-
- :param dict marker: Description of a line marker
- :param int pixelOffset: Offset of text from borders in pixels
- :return: Line vertices and Text label or None
- :rtype: 2-tuple (2x2 numpy.array of float, Text2D)
- """
- label, vertices = None, None
-
- xCoord, yCoord = marker['x'], marker['y']
- assert xCoord is None or yCoord is None # Specific to line markers
-
- # Get plot corners in data coords
- plotLeft, plotTop, plotWidth, plotHeight = self.getPlotBoundsInPixels()
-
- corners = [(plotLeft, plotTop),
- (plotLeft, plotTop + plotHeight),
- (plotLeft + plotWidth, plotTop + plotHeight),
- (plotLeft + plotWidth, plotTop)]
- corners = numpy.array([self.pixelToData(x, y, axis='left', check=False)
- for (x, y) in corners])
-
- borders = {
- 'right': (corners[3], corners[2]),
- 'top': (corners[0], corners[3]),
- 'bottom': (corners[2], corners[1]),
- 'left': (corners[1], corners[0])
- }
-
- textLayouts = { # align, valign, offsets
- 'right': (RIGHT, BOTTOM, (-1., -1.)),
- 'top': (LEFT, TOP, (1., 1.)),
- 'bottom': (LEFT, BOTTOM, (1., -1.)),
- 'left': (LEFT, BOTTOM, (1., -1.))
- }
-
- if xCoord is None: # Horizontal line in data space
- if marker['text'] is not None:
- # Find intersection of hline with borders in data
- # Order is important as it stops at first intersection
- for border_name in ('right', 'top', 'bottom', 'left'):
- (x0, y0), (x1, y1) = borders[border_name]
-
- if min(y0, y1) <= yCoord < max(y0, y1):
- xIntersect = (yCoord - y0) * (x1 - x0) / (y1 - y0) + x0
-
- # Add text label
- pixelPos = self.dataToPixel(
- xIntersect, yCoord, axis='left', check=False)
-
- align, valign, offsets = textLayouts[border_name]
-
- x = pixelPos[0] + offsets[0] * pixelOffset
- y = pixelPos[1] + offsets[1] * pixelOffset
- label = Text2D(marker['text'], x, y,
- color=marker['color'],
- bgColor=(1., 1., 1., 0.5),
- align=align, valign=valign)
- break # Stop at first intersection
-
- xMin, xMax = corners[:, 0].min(), corners[:, 0].max()
- vertices = numpy.array(
- ((xMin, yCoord), (xMax, yCoord)), dtype=numpy.float32)
-
- else: # yCoord is None: vertical line in data space
- if marker['text'] is not None:
- # Find intersection of hline with borders in data
- # Order is important as it stops at first intersection
- for border_name in ('top', 'bottom', 'right', 'left'):
- (x0, y0), (x1, y1) = borders[border_name]
- if min(x0, x1) <= xCoord < max(x0, x1):
- yIntersect = (xCoord - x0) * (y1 - y0) / (x1 - x0) + y0
-
- # Add text label
- pixelPos = self.dataToPixel(
- xCoord, yIntersect, axis='left', check=False)
-
- align, valign, offsets = textLayouts[border_name]
-
- x = pixelPos[0] + offsets[0] * pixelOffset
- y = pixelPos[1] + offsets[1] * pixelOffset
- label = Text2D(marker['text'], x, y,
- color=marker['color'],
- bgColor=(1., 1., 1., 0.5),
- align=align, valign=valign)
- break # Stop at first intersection
-
- yMin, yMax = corners[:, 1].min(), corners[:, 1].max()
- vertices = numpy.array(
- ((xCoord, yMin), (xCoord, yMax)), dtype=numpy.float32)
-
- return vertices, label
-
def _renderMarkersGL(self):
if len(self._markers) == 0:
return
@@ -651,16 +564,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
- # Prepare vertical and horizontal markers rendering
- 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.glUniform1i(self._progBase.uniforms['hatchStep'], 0)
- gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
- posAttrib = self._progBase.attributes['position']
-
labels = []
pixelOffset = 3
@@ -677,59 +580,43 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
continue
if xCoord is None or yCoord is None:
- if not self.isDefaultBaseVectors(): # Non-orthogonal axes
- vertices, label = self._nonOrthoAxesLineMarkerPrimitives(
- marker, pixelOffset)
- if label is not None:
- labels.append(label)
+ pixelPos = self.dataToPixel(
+ xCoord, yCoord, axis='left', check=False)
- else: # Orthogonal axes
- pixelPos = self.dataToPixel(
- xCoord, yCoord, axis='left', check=False)
-
- if xCoord is None: # Horizontal line in data space
- if marker['text'] is not None:
- x = self._plotFrame.size[0] - \
- self._plotFrame.margins.right - pixelOffset
- y = pixelPos[1] - pixelOffset
- label = Text2D(marker['text'], x, y,
- color=marker['color'],
- bgColor=(1., 1., 1., 0.5),
- align=RIGHT, valign=BOTTOM)
- labels.append(label)
-
- width = self._plotFrame.size[0]
- vertices = numpy.array(((0, pixelPos[1]),
- (width, pixelPos[1])),
- dtype=numpy.float32)
-
- else: # yCoord is None: vertical line in data space
- if marker['text'] is not None:
- x = pixelPos[0] + pixelOffset
- y = self._plotFrame.margins.top + pixelOffset
- label = Text2D(marker['text'], x, y,
- color=marker['color'],
- bgColor=(1., 1., 1., 0.5),
- align=LEFT, valign=TOP)
- labels.append(label)
-
- height = self._plotFrame.size[1]
- vertices = numpy.array(((pixelPos[0], 0),
- (pixelPos[0], height)),
- dtype=numpy.float32)
+ if xCoord is None: # Horizontal line in data space
+ if marker['text'] is not None:
+ x = self._plotFrame.size[0] - \
+ self._plotFrame.margins.right - pixelOffset
+ y = pixelPos[1] - pixelOffset
+ label = Text2D(marker['text'], x, y,
+ color=marker['color'],
+ bgColor=(1., 1., 1., 0.5),
+ align=RIGHT, valign=BOTTOM)
+ labels.append(label)
- self._progBase.use()
- gl.glUniform4f(self._progBase.uniforms['color'],
- *marker['color'])
+ width = self._plotFrame.size[0]
+ lines = GLLines2D((0, width), (pixelPos[1], pixelPos[1]),
+ style=marker['linestyle'],
+ color=marker['color'],
+ width=marker['linewidth'])
+ lines.render(self.matScreenProj)
+
+ else: # yCoord is None: vertical line in data space
+ if marker['text'] is not None:
+ x = pixelPos[0] + pixelOffset
+ y = self._plotFrame.margins.top + pixelOffset
+ label = Text2D(marker['text'], x, y,
+ color=marker['color'],
+ bgColor=(1., 1., 1., 0.5),
+ align=LEFT, valign=TOP)
+ labels.append(label)
- gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, vertices)
- gl.glLineWidth(1)
- gl.glDrawArrays(gl.GL_LINES, 0, len(vertices))
+ height = self._plotFrame.size[1]
+ lines = GLLines2D((pixelPos[0], pixelPos[0]), (0, height),
+ style=marker['linestyle'],
+ color=marker['color'],
+ width=marker['linewidth'])
+ lines.render(self.matScreenProj)
else:
pixelPos = self.dataToPixel(
@@ -820,13 +707,17 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def _renderPlotAreaGL(self):
plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
- self._plotFrame.renderGrid()
-
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] or \
@@ -853,32 +744,61 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# Render Items
gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
- 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.)
-
for item in self._items.values():
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
- closed = item['shape'] != 'polylines'
- points = [self.dataToPixel(x, y, axis='left', check=False)
- for (x, y) in zip(item['x'], item['y'])]
- shape2D = Shape2D(points,
- fill=item['fill'],
- fillColor=item['color'],
- stroke=True,
- strokeColor=item['color'],
- strokeClosed=closed)
+ if item['shape'] == 'hline':
+ width = self._plotFrame.size[0]
+ _, yPixel = self.dataToPixel(
+ None, item['y'], axis='left', check=False)
+ points = numpy.array(((0., yPixel), (width, yPixel)),
+ dtype=numpy.float32)
- posAttrib = self._progBase.attributes['position']
- colorUnif = self._progBase.uniforms['color']
- hatchStepUnif = self._progBase.uniforms['hatchStep']
- shape2D.render(posAttrib, colorUnif, hatchStepUnif)
+ elif item['shape'] == 'vline':
+ xPixel, _ = self.dataToPixel(
+ item['x'], None, axis='left', check=False)
+ height = self._plotFrame.size[1]
+ points = numpy.array(((xPixel, 0), (xPixel, height)),
+ dtype=numpy.float32)
+
+ else:
+ points = numpy.array([
+ self.dataToPixel(x, y, axis='left', check=False)
+ for (x, y) in zip(item['x'], item['y'])])
+
+ # 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 = 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 = GLLines2D(points[:, 0], points[:, 1],
+ style=item['linestyle'],
+ color=item['color'],
+ dash2ndColor=item['linebgcolor'],
+ width=item['linewidth'])
+ lines.render(self.matScreenProj)
gl.glDisable(gl.GL_SCISSOR_TEST)
@@ -1123,7 +1043,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return legend, 'image'
- def addItem(self, x, y, legend, shape, color, fill, overlay, z):
+ def addItem(self, x, y, legend, shape, color, fill, overlay, z,
+ linestyle, linewidth, linebgcolor):
# TODO handle overlay
if shape not in ('polygon', 'rectangle', 'line',
'vline', 'hline', 'polylines'):
@@ -1154,7 +1075,10 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
'color': colors.rgba(color),
'fill': 'hatch' if fill else None,
'x': x,
- 'y': y
+ 'y': y,
+ 'linestyle': linestyle,
+ 'linewidth': linewidth,
+ 'linebgcolor': linebgcolor,
}
return legend, 'item'
@@ -1166,10 +1090,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if symbol is None:
symbol = '+'
- if linestyle != '-' or linewidth != 1:
- _logger.warning(
- 'OpenGL backend does not support marker line style and width.')
-
behaviors = set()
if selectable:
behaviors.add('selectable')
@@ -1191,6 +1111,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
'behaviors': behaviors,
'constraint': constraint if isConstraint else None,
'symbol': symbol,
+ 'linestyle': linestyle,
+ 'linewidth': linewidth,
}
return legend, 'marker'
@@ -1441,37 +1363,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if label:
_logger.warning('Right axis label not implemented')
- # Non orthogonal axes
-
- def setBaseVectors(self, x=(1., 0.), y=(0., 1.)):
- """Set base vectors.
-
- Useful for non-orthogonal axes.
- If an axis is in log scale, skew is applied to log transformed values.
-
- Base vector does not work well with log axes, to investi
- """
- if x != (1., 0.) and y != (0., 1.):
- if self._plotFrame.xAxis.isLog:
- _logger.warning("setBaseVectors disables X axis logarithmic.")
- self.setXAxisLogarithmic(False)
- if self._plotFrame.yAxis.isLog:
- _logger.warning("setBaseVectors disables Y axis logarithmic.")
- self.setYAxisLogarithmic(False)
-
- if self.isKeepDataAspectRatio():
- _logger.warning("setBaseVectors disables keepDataAspectRatio.")
- self.keepDataAspectRatio(False)
-
- self._plotFrame.baseVectors = x, y
-
- def getBaseVectors(self):
- return self._plotFrame.baseVectors
-
- def isDefaultBaseVectors(self):
- return self._plotFrame.baseVectors == \
- self._plotFrame.DEFAULT_BASE_VECTORS
-
# Graph limits
def _setDataRanges(self, xlim=None, ylim=None, y2lim=None):
@@ -1486,26 +1377,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# Update axes range with a clipped range if too wide
self._plotFrame.setDataRanges(xlim, ylim, y2lim)
- if not self.isDefaultBaseVectors():
- # Update axes range with axes bounds in data coords
- plotLeft, plotTop, plotWidth, plotHeight = \
- self.getPlotBoundsInPixels()
-
- self._plotFrame.xAxis.dataRange = sorted([
- self.pixelToData(x, y, axis='left', check=False)[0]
- for (x, y) in ((plotLeft, plotTop + plotHeight),
- (plotLeft + plotWidth, plotTop + plotHeight))])
-
- self._plotFrame.yAxis.dataRange = sorted([
- self.pixelToData(x, y, axis='left', check=False)[1]
- for (x, y) in ((plotLeft, plotTop + plotHeight),
- (plotLeft, plotTop))])
-
- self._plotFrame.y2Axis.dataRange = sorted([
- self.pixelToData(x, y, axis='right', check=False)[1]
- for (x, y) in ((plotLeft + plotWidth, plotTop + plotHeight),
- (plotLeft + plotWidth, plotTop))])
-
def _ensureAspectRatio(self, keepDim=None):
"""Update plot bounds in order to keep aspect ratio.
@@ -1619,11 +1490,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
_logger.warning(
"KeepDataAspectRatio is ignored with log axes")
- if flag and not self.isDefaultBaseVectors():
- _logger.warning(
- "setXAxisLogarithmic ignored because baseVectors are set")
- return
-
self._plotFrame.xAxis.isLog = flag
def setYAxisLogarithmic(self, flag):
@@ -1633,11 +1499,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
_logger.warning(
"KeepDataAspectRatio is ignored with log axes")
- if flag and not self.isDefaultBaseVectors():
- _logger.warning(
- "setYAxisLogarithmic ignored because baseVectors are set")
- return
-
self._plotFrame.yAxis.isLog = flag
self._plotFrame.y2Axis.isLog = flag
@@ -1658,9 +1519,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if flag and (self._plotFrame.xAxis.isLog or
self._plotFrame.yAxis.isLog):
_logger.warning("KeepDataAspectRatio is ignored with log axes")
- if flag and not self.isDefaultBaseVectors():
- _logger.warning(
- "keepDataAspectRatio ignored because baseVectors are set")
self._keepDataAspectRatio = flag
@@ -1723,3 +1581,11 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def setAxesDisplayed(self, displayed):
BackendBase.BackendBase.setAxesDisplayed(self, displayed)
self._plotFrame.displayed = displayed
+
+ def setForegroundColors(self, foregroundColor, gridColor):
+ self._plotFrame.foregroundColor = foregroundColor
+ self._plotFrame.gridColor = gridColor
+
+ def setBackgroundColors(self, backgroundColor, dataBackgroundColor):
+ self._backgroundColor = backgroundColor
+ self._dataBackgroundColor = dataBackgroundColor