summaryrefslogtreecommitdiff
path: root/silx/gui/plot/backends
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2019-05-28 08:16:16 +0200
committerPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2019-05-28 08:16:16 +0200
commita763e5d1b3921b3194f3d4e94ab9de3fbe08bbdd (patch)
tree45d462ed36a5522e9f3b9fde6c4ec4918c2ae8e3 /silx/gui/plot/backends
parentcebdc9244c019224846cb8d2668080fe386a6adc (diff)
New upstream version 0.10.1+dfsg
Diffstat (limited to 'silx/gui/plot/backends')
-rw-r--r--silx/gui/plot/backends/BackendBase.py37
-rw-r--r--silx/gui/plot/backends/BackendMatplotlib.py223
-rw-r--r--silx/gui/plot/backends/BackendOpenGL.py364
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotCurve.py119
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotFrame.py124
-rw-r--r--silx/gui/plot/backends/glutils/GLSupport.py63
6 files changed, 492 insertions, 438 deletions
diff --git a/silx/gui/plot/backends/BackendBase.py b/silx/gui/plot/backends/BackendBase.py
index 7fb8be0..0514c85 100644
--- a/silx/gui/plot/backends/BackendBase.py
+++ b/silx/gui/plot/backends/BackendBase.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-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
@@ -31,7 +31,7 @@ This API is a simplified version of PyMca PlotBackend API.
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "24/04/2018"
+__date__ = "21/12/2018"
import weakref
from ... import qt
@@ -170,7 +170,8 @@ class BackendBase(object):
"""
return legend
- 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):
"""Add an item (i.e. a shape) to the plot.
:param numpy.ndarray x: The X coords of the points of the shape
@@ -182,6 +183,19 @@ class BackendBase(object):
:param bool fill: True to fill the shape
:param bool overlay: True if item is an overlay, False otherwise
:param int z: Layer on which to draw the item
+ :param str linestyle: Style of the line.
+ Only relevant for line markers where X or Y is None.
+ Value in:
+
+ - ' ' no line
+ - '-' solid line
+ - '--' dashed line
+ - '-.' dash-dot line
+ - ':' dotted line
+ :param float linewidth: Width of the line.
+ Only relevant for line markers where X or Y is None.
+ :param str linebgcolor: Background color of the line, e.g., 'blue', 'b',
+ '#FF0000'. It is used to draw dotted line using a second color.
:returns: The handle used by the backend to univocally access the item
"""
return legend
@@ -546,3 +560,20 @@ class BackendBase(object):
This only check status set to axes from the public API
"""
return self._axesDisplayed
+
+ def setForegroundColors(self, foregroundColor, gridColor):
+ """Set foreground and grid colors used to display this widget.
+
+ :param List[float] foregroundColor: RGBA foreground color of the widget
+ :param List[float] gridColor: RGBA grid color of the data view
+ """
+ pass
+
+ def setBackgroundColors(self, backgroundColor, dataBackgroundColor):
+ """Set background colors used to display this widget.
+
+ :param List[float] backgroundColor: RGBA background color of the widget
+ :param Union[Tuple[float],None] dataBackgroundColor:
+ RGBA background color of the data view
+ """
+ pass
diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py
index 3b1d6dd..726a839 100644
--- a/silx/gui/plot/backends/BackendMatplotlib.py
+++ b/silx/gui/plot/backends/BackendMatplotlib.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-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__ = ["V.A. Sole", "T. Vincent, H. Payno"]
__license__ = "MIT"
-__date__ = "01/08/2018"
+__date__ = "21/12/2018"
import logging
@@ -56,12 +56,26 @@ from matplotlib.collections import PathCollection, LineCollection
from matplotlib.ticker import Formatter, ScalarFormatter, Locator
-from ....third_party.modest_image import ModestImage
from . import BackendBase
from .._utils import FLOAT32_MINPOS
from .._utils.dtime_ticklayout import calcTicks, bestFormatString, timestamp
+_PATCH_LINESTYLE = {
+ "-": 'solid',
+ "--": 'dashed',
+ '-.': 'dashdot',
+ ':': 'dotted',
+ '': "solid",
+ None: "solid",
+}
+"""Patches do not uses the same matplotlib syntax"""
+
+
+def normalize_linestyle(linestyle):
+ """Normalize known old-style linestyle, else return the provided value."""
+ return _PATCH_LINESTYLE.get(linestyle, linestyle)
+
class NiceDateLocator(Locator):
"""
@@ -115,7 +129,6 @@ class NiceDateLocator(Locator):
return ticks
-
class NiceAutoDateFormatter(Formatter):
"""
Matplotlib FuncFormatter that is linked to a NiceDateLocator and gives the
@@ -139,7 +152,6 @@ class NiceAutoDateFormatter(Formatter):
else:
return bestFormatString(self.locator.spacing, self.locator.unit)
-
def __call__(self, x, pos=None):
"""Return the format for tick val *x* at position *pos*
Expects x to be a POSIX timestamp (seconds since 1 Jan 1970)
@@ -149,8 +161,6 @@ class NiceAutoDateFormatter(Formatter):
return tickStr
-
-
class _MarkerContainer(Container):
"""Marker artists container supporting draw/remove and text position update
@@ -204,6 +214,57 @@ class _MarkerContainer(Container):
self.text.set_x(xmax)
+class _DoubleColoredLinePatch(matplotlib.patches.Patch):
+ """Matplotlib patch to display any patch using double color."""
+
+ def __init__(self, patch):
+ super(_DoubleColoredLinePatch, self).__init__()
+ self.__patch = patch
+ self.linebgcolor = None
+
+ def __getattr__(self, name):
+ return getattr(self.__patch, name)
+
+ def draw(self, renderer):
+ oldLineStype = self.__patch.get_linestyle()
+ if self.linebgcolor is not None and oldLineStype != "solid":
+ oldLineColor = self.__patch.get_edgecolor()
+ oldHatch = self.__patch.get_hatch()
+ self.__patch.set_linestyle("solid")
+ self.__patch.set_edgecolor(self.linebgcolor)
+ self.__patch.set_hatch(None)
+ self.__patch.draw(renderer)
+ self.__patch.set_linestyle(oldLineStype)
+ self.__patch.set_edgecolor(oldLineColor)
+ self.__patch.set_hatch(oldHatch)
+ self.__patch.draw(renderer)
+
+ def set_transform(self, transform):
+ self.__patch.set_transform(transform)
+
+ def get_path(self):
+ return self.__patch.get_path()
+
+ def contains(self, mouseevent, radius=None):
+ return self.__patch.contains(mouseevent, radius)
+
+ def contains_point(self, point, radius=None):
+ return self.__patch.contains_point(point, radius)
+
+
+class Image(AxesImage):
+ """An AxesImage with a fast path for uint8 RGBA images"""
+
+ def set_data(self, A):
+ A = numpy.array(A, copy=False)
+ if A.ndim != 3 or A.shape[2] != 4 or A.dtype != numpy.uint8:
+ super(Image, self).set_data(A)
+ else:
+ # Call AxesImage.set_data with small data to set attributes
+ super(Image, self).set_data(numpy.zeros((2, 2, 4), dtype=A.dtype))
+ self._A = A # Override stored data
+
+
class BackendMatplotlib(BackendBase.BackendBase):
"""Base class for Matplotlib backend without a FigureCanvas.
@@ -231,6 +292,8 @@ class BackendMatplotlib(BackendBase.BackendBase):
self.ax = self.fig.add_axes([.15, .15, .75, .75], label="left")
self.ax2 = self.ax.twinx()
self.ax2.set_label("right")
+ # Make sure background of Axes is displayed
+ self.ax2.patch.set_visible(True)
# disable the use of offsets
try:
@@ -239,9 +302,9 @@ class BackendMatplotlib(BackendBase.BackendBase):
self.ax2.get_yaxis().get_major_formatter().set_useOffset(False)
self.ax2.get_xaxis().get_major_formatter().set_useOffset(False)
except:
- _logger.warning('Cannot disabled axes offsets in %s ' \
+ _logger.warning('Cannot disabled axes offsets in %s '
% matplotlib.__version__)
-
+
# critical for picking!!!!
self.ax2.set_zorder(0)
self.ax2.set_autoscaley_on(True)
@@ -376,44 +439,13 @@ class BackendMatplotlib(BackendBase.BackendBase):
picker = (selectable or draggable)
- # Debian 7 specific support
- # No transparent colormap with matplotlib < 1.2.0
- # Add support for transparent colormap for uint8 data with
- # colormap with 256 colors, linear norm, [0, 255] range
- if self._matplotlibVersion < _parse_version('1.2.0'):
- if (len(data.shape) == 2 and colormap.getName() is None and
- colormap.getColormapLUT() is not None):
- colors = colormap.getColormapLUT()
- if (colors.shape[-1] == 4 and
- not numpy.all(numpy.equal(colors[3], 255))):
- # This is a transparent colormap
- if (colors.shape == (256, 4) and
- colormap.getNormalization() == 'linear' and
- not colormap.isAutoscale() and
- colormap.getVMin() == 0 and
- colormap.getVMax() == 255 and
- data.dtype == numpy.uint8):
- # Supported case, convert data to RGBA
- data = colors[data.reshape(-1)].reshape(
- data.shape + (4,))
- else:
- _logger.warning(
- 'matplotlib %s does not support transparent '
- 'colormap.', matplotlib.__version__)
-
- if ((height * width) > 5.0e5 and
- origin == (0., 0.) and scale == (1., 1.)):
- imageClass = ModestImage
- else:
- imageClass = AxesImage
-
# All image are shown as RGBA image
- image = imageClass(self.ax,
- label="__IMAGE__" + legend,
- interpolation='nearest',
- picker=picker,
- zorder=z,
- origin='lower')
+ image = Image(self.ax,
+ label="__IMAGE__" + legend,
+ interpolation='nearest',
+ picker=picker,
+ zorder=z,
+ origin='lower')
if alpha < 1:
image.set_alpha(alpha)
@@ -438,40 +470,41 @@ class BackendMatplotlib(BackendBase.BackendBase):
ystep = 1 if scale[1] >= 0. else -1
data = data[::ystep, ::xstep]
- if self._matplotlibVersion < _parse_version('2.1'):
- # matplotlib 1.4.2 do not support float128
- dtype = data.dtype
- if dtype.kind == "f" and dtype.itemsize >= 16:
- _logger.warning("Your matplotlib version do not support "
- "float128. Data converted to float64.")
- data = data.astype(numpy.float64)
-
if data.ndim == 2: # Data image, convert to RGBA image
data = colormap.applyToData(data)
image.set_data(data)
-
self.ax.add_artist(image)
-
return 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):
+ if (linebgcolor is not None and
+ shape not in ('rectangle', 'polygon', 'polylines')):
+ _logger.warning(
+ 'linebgcolor not implemented for %s with matplotlib backend',
+ shape)
xView = numpy.array(x, copy=False)
yView = numpy.array(y, copy=False)
+ linestyle = normalize_linestyle(linestyle)
+
if shape == "line":
item = self.ax.plot(x, y, label=legend, color=color,
- linestyle='-', marker=None)[0]
+ linestyle=linestyle, linewidth=linewidth,
+ marker=None)[0]
elif shape == "hline":
if hasattr(y, "__len__"):
y = y[-1]
- item = self.ax.axhline(y, label=legend, color=color)
+ item = self.ax.axhline(y, label=legend, color=color,
+ linestyle=linestyle, linewidth=linewidth)
elif shape == "vline":
if hasattr(x, "__len__"):
x = x[-1]
- item = self.ax.axvline(x, label=legend, color=color)
+ item = self.ax.axvline(x, label=legend, color=color,
+ linestyle=linestyle, linewidth=linewidth)
elif shape == 'rectangle':
xMin = numpy.nanmin(xView)
@@ -484,10 +517,16 @@ class BackendMatplotlib(BackendBase.BackendBase):
width=w,
height=h,
fill=False,
- color=color)
+ color=color,
+ linestyle=linestyle,
+ linewidth=linewidth)
if fill:
item.set_hatch('.')
+ if linestyle != "solid" and linebgcolor is not None:
+ item = _DoubleColoredLinePatch(item)
+ item.linebgcolor = linebgcolor
+
self.ax.add_patch(item)
elif shape in ('polygon', 'polylines'):
@@ -500,10 +539,16 @@ class BackendMatplotlib(BackendBase.BackendBase):
closed=closed,
fill=False,
label=legend,
- color=color)
+ color=color,
+ linestyle=linestyle,
+ linewidth=linewidth)
if fill and shape == 'polygon':
item.set_hatch('/')
+ if linestyle != "solid" and linebgcolor is not None:
+ item = _DoubleColoredLinePatch(item)
+ item.linebgcolor = linebgcolor
+
self.ax.add_patch(item)
else:
@@ -908,8 +953,56 @@ class BackendMatplotlib(BackendBase.BackendBase):
# remove external margins
self.ax.set_position([0, 0, 1, 1])
self.ax2.set_position([0, 0, 1, 1])
+ self._synchronizeBackgroundColors()
+ self._synchronizeForegroundColors()
self._plot._setDirtyPlot()
+ def _synchronizeBackgroundColors(self):
+ backgroundColor = self._plot.getBackgroundColor().getRgbF()
+
+ dataBackgroundColor = self._plot.getDataBackgroundColor()
+ if dataBackgroundColor.isValid():
+ dataBackgroundColor = dataBackgroundColor.getRgbF()
+ else:
+ dataBackgroundColor = backgroundColor
+
+ if self.ax2.axison:
+ self.fig.patch.set_facecolor(backgroundColor)
+ if self._matplotlibVersion < _parse_version('2'):
+ self.ax2.set_axis_bgcolor(dataBackgroundColor)
+ else:
+ self.ax2.set_facecolor(dataBackgroundColor)
+ else:
+ self.fig.patch.set_facecolor(dataBackgroundColor)
+
+ def _synchronizeForegroundColors(self):
+ foregroundColor = self._plot.getForegroundColor().getRgbF()
+
+ gridColor = self._plot.getGridColor()
+ if gridColor.isValid():
+ gridColor = gridColor.getRgbF()
+ else:
+ gridColor = foregroundColor
+
+ for axes in (self.ax, self.ax2):
+ if axes.axison:
+ axes.spines['bottom'].set_color(foregroundColor)
+ axes.spines['top'].set_color(foregroundColor)
+ axes.spines['right'].set_color(foregroundColor)
+ axes.spines['left'].set_color(foregroundColor)
+ axes.tick_params(axis='x', colors=foregroundColor)
+ axes.tick_params(axis='y', colors=foregroundColor)
+ axes.yaxis.label.set_color(foregroundColor)
+ axes.xaxis.label.set_color(foregroundColor)
+ axes.title.set_color(foregroundColor)
+
+ for line in axes.get_xgridlines():
+ line.set_color(gridColor)
+
+ for line in axes.get_ygridlines():
+ line.set_color(gridColor)
+ # axes.grid().set_markeredgecolor(gridColor)
+
class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
"""QWidget matplotlib backend using a QtAgg canvas.
@@ -1137,3 +1230,9 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
else:
cursor = self._QT_CURSORS[cursor]
FigureCanvasQTAgg.setCursor(self, qt.QCursor(cursor))
+
+ def setBackgroundColors(self, backgroundColor, dataBackgroundColor):
+ self._synchronizeBackgroundColors()
+
+ def setForegroundColors(self, foregroundColor, gridColor):
+ self._synchronizeForegroundColors()
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
diff --git a/silx/gui/plot/backends/glutils/GLPlotCurve.py b/silx/gui/plot/backends/glutils/GLPlotCurve.py
index 12b6bbe..5f8d652 100644
--- a/silx/gui/plot/backends/glutils/GLPlotCurve.py
+++ b/silx/gui/plot/backends/glutils/GLPlotCurve.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
@@ -42,7 +42,7 @@ import numpy
from silx.math.combo import min_max
from ...._glutils import gl
-from ...._glutils import Program, vertexBuffer
+from ...._glutils import Program, vertexBuffer, VertexBufferAttrib
from .GLSupport import buildFillMaskIndices, mat4Identity, mat4Translate
@@ -245,7 +245,7 @@ class _Fill2D(object):
SOLID, DASHED, DASHDOT, DOTTED = '-', '--', '-.', ':'
-class _Lines2D(object):
+class GLLines2D(object):
"""Object rendering curve as a polyline
:param xVboData: X coordinates VBO
@@ -323,6 +323,7 @@ class _Lines2D(object):
/* Dashes: [0, x], [y, z]
Dash period: w */
uniform vec4 dash;
+ uniform vec4 dash2ndColor;
varying float vDist;
varying vec4 vColor;
@@ -330,25 +331,52 @@ class _Lines2D(object):
void main(void) {
float dist = mod(vDist, dash.w);
if ((dist > dash.x && dist < dash.y) || dist > dash.z) {
- discard;
+ if (dash2ndColor.a == 0.) {
+ discard; // Discard full transparent bg color
+ } else {
+ gl_FragColor = dash2ndColor;
+ }
+ } else {
+ gl_FragColor = vColor;
}
- gl_FragColor = vColor;
}
""",
attrib0='xPos')
def __init__(self, xVboData=None, yVboData=None,
colorVboData=None, distVboData=None,
- style=SOLID, color=(0., 0., 0., 1.),
- width=1, dashPeriod=20, drawMode=None,
+ style=SOLID, color=(0., 0., 0., 1.), dash2ndColor=None,
+ width=1, dashPeriod=10., drawMode=None,
offset=(0., 0.)):
+ if (xVboData is not None and
+ not isinstance(xVboData, VertexBufferAttrib)):
+ xVboData = numpy.array(xVboData, copy=False, dtype=numpy.float32)
self.xVboData = xVboData
+
+ if (yVboData is not None and
+ not isinstance(yVboData, VertexBufferAttrib)):
+ yVboData = numpy.array(yVboData, copy=False, dtype=numpy.float32)
self.yVboData = yVboData
+
+ # Compute distances if not given while providing numpy array coordinates
+ if (isinstance(self.xVboData, numpy.ndarray) and
+ isinstance(self.yVboData, numpy.ndarray) and
+ distVboData is None):
+ distVboData = distancesFromArrays(self.xVboData, self.yVboData)
+
+ if (distVboData is not None and
+ not isinstance(distVboData, VertexBufferAttrib)):
+ distVboData = numpy.array(
+ distVboData, copy=False, dtype=numpy.float32)
self.distVboData = distVboData
+
+ if colorVboData is not None:
+ assert isinstance(colorVboData, VertexBufferAttrib)
self.colorVboData = colorVboData
self.useColorVboData = colorVboData is not None
self.color = color
+ self.dash2ndColor = dash2ndColor
self.width = width
self._style = None
self.style = style
@@ -396,29 +424,46 @@ class _Lines2D(object):
gl.glUniform2f(program.uniforms['halfViewportSize'],
0.5 * viewWidth, 0.5 * viewHeight)
+ dashPeriod = self.dashPeriod * self.width
if self.style == DOTTED:
- dash = (0.1 * self.dashPeriod,
- 0.6 * self.dashPeriod,
- 0.7 * self.dashPeriod,
- self.dashPeriod)
+ dash = (0.2 * dashPeriod,
+ 0.5 * dashPeriod,
+ 0.7 * dashPeriod,
+ dashPeriod)
elif self.style == DASHDOT:
- dash = (0.3 * self.dashPeriod,
- 0.5 * self.dashPeriod,
- 0.6 * self.dashPeriod,
- self.dashPeriod)
+ dash = (0.3 * dashPeriod,
+ 0.5 * dashPeriod,
+ 0.6 * dashPeriod,
+ dashPeriod)
else:
- dash = (0.5 * self.dashPeriod,
- self.dashPeriod,
- self.dashPeriod,
- self.dashPeriod)
+ dash = (0.5 * dashPeriod,
+ dashPeriod,
+ dashPeriod,
+ dashPeriod)
gl.glUniform4f(program.uniforms['dash'], *dash)
+ if self.dash2ndColor is None:
+ # Use fully transparent color which gets discarded in shader
+ dash2ndColor = (0., 0., 0., 0.)
+ else:
+ dash2ndColor = self.dash2ndColor
+ gl.glUniform4f(program.uniforms['dash2ndColor'], *dash2ndColor)
+
distAttrib = program.attributes['distance']
gl.glEnableVertexAttribArray(distAttrib)
- self.distVboData.setVertexAttrib(distAttrib)
+ if isinstance(self.distVboData, VertexBufferAttrib):
+ self.distVboData.setVertexAttrib(distAttrib)
+ else:
+ gl.glVertexAttribPointer(distAttrib,
+ 1,
+ gl.GL_FLOAT,
+ False,
+ 0,
+ self.distVboData)
- gl.glEnable(gl.GL_LINE_SMOOTH)
+ if self.width != 1:
+ gl.glEnable(gl.GL_LINE_SMOOTH)
matrix = numpy.dot(matrix,
mat4Translate(*self.offset)).astype(numpy.float32)
@@ -435,11 +480,27 @@ class _Lines2D(object):
xPosAttrib = program.attributes['xPos']
gl.glEnableVertexAttribArray(xPosAttrib)
- self.xVboData.setVertexAttrib(xPosAttrib)
+ if isinstance(self.xVboData, VertexBufferAttrib):
+ self.xVboData.setVertexAttrib(xPosAttrib)
+ else:
+ gl.glVertexAttribPointer(xPosAttrib,
+ 1,
+ gl.GL_FLOAT,
+ False,
+ 0,
+ self.xVboData)
yPosAttrib = program.attributes['yPos']
gl.glEnableVertexAttribArray(yPosAttrib)
- self.yVboData.setVertexAttrib(yPosAttrib)
+ if isinstance(self.yVboData, VertexBufferAttrib):
+ self.yVboData.setVertexAttrib(yPosAttrib)
+ else:
+ gl.glVertexAttribPointer(yPosAttrib,
+ 1,
+ gl.GL_FLOAT,
+ False,
+ 0,
+ self.yVboData)
gl.glLineWidth(self.width)
gl.glDrawArrays(self._drawMode, 0, self.xVboData.size)
@@ -447,7 +508,7 @@ class _Lines2D(object):
gl.glDisable(gl.GL_LINE_SMOOTH)
-def _distancesFromArrays(xData, yData):
+def distancesFromArrays(xData, yData):
"""Returns distances between each points
:param numpy.ndarray xData: X coordinate of points
@@ -711,7 +772,7 @@ class _ErrorBars(object):
This is using its own VBO as opposed to fill/points/lines.
There is no picking on error bars.
- It uses 2 vertices per error bars and uses :class:`_Lines2D` to
+ It uses 2 vertices per error bars and uses :class:`GLLines2D` to
render error bars and :class:`_Points2D` to render the ends.
:param numpy.ndarray xData: X coordinates of the data.
@@ -753,7 +814,7 @@ class _ErrorBars(object):
self._xData, self._yData = None, None
self._xError, self._yError = None, None
- self._lines = _Lines2D(
+ self._lines = GLLines2D(
None, None, color=color, drawMode=gl.GL_LINES, offset=offset)
self._xErrPoints = _Points2D(
None, None, color=color, marker=V_LINE, offset=offset)
@@ -957,7 +1018,7 @@ class GLPlotCurve2D(object):
self.xMin, self.yMin,
offset=self.offset)
- self.lines = _Lines2D()
+ self.lines = GLLines2D()
self.lines.style = lineStyle
self.lines.color = lineColor
self.lines.width = lineWidth
@@ -999,7 +1060,7 @@ class GLPlotCurve2D(object):
@classmethod
def init(cls):
"""OpenGL context initialization"""
- _Lines2D.init()
+ GLLines2D.init()
_Points2D.init()
def prepare(self):
@@ -1007,7 +1068,7 @@ class GLPlotCurve2D(object):
if self.xVboData is None:
xAttrib, yAttrib, cAttrib, dAttrib = None, None, None, None
if self.lineStyle in (DASHED, DASHDOT, DOTTED):
- dists = _distancesFromArrays(self.xData, self.yData)
+ dists = distancesFromArrays(self.xData, self.yData)
if self.colorData is None:
xAttrib, yAttrib, dAttrib = vertexBuffer(
(self.xData, self.yData, dists))
diff --git a/silx/gui/plot/backends/glutils/GLPlotFrame.py b/silx/gui/plot/backends/glutils/GLPlotFrame.py
index 4ad1547..43f6e10 100644
--- a/silx/gui/plot/backends/glutils/GLPlotFrame.py
+++ b/silx/gui/plot/backends/glutils/GLPlotFrame.py
@@ -63,6 +63,7 @@ class PlotAxis(object):
def __init__(self, plot,
tickLength=(0., 0.),
+ foregroundColor=(0., 0., 0., 1.0),
labelAlign=CENTER, labelVAlign=CENTER,
titleAlign=CENTER, titleVAlign=CENTER,
titleRotate=0, titleOffset=(0., 0.)):
@@ -78,6 +79,7 @@ class PlotAxis(object):
self._title = ''
self._tickLength = tickLength
+ self._foregroundColor = foregroundColor
self._labelAlign = labelAlign
self._labelVAlign = labelVAlign
self._titleAlign = titleAlign
@@ -169,6 +171,20 @@ class PlotAxis(object):
plot._dirty()
@property
+ def foregroundColor(self):
+ """Color used for frame and labels"""
+ return self._foregroundColor
+
+ @foregroundColor.setter
+ def foregroundColor(self, color):
+ """Color used for frame and labels"""
+ assert len(color) == 4, \
+ "foregroundColor must have length 4, got {}".format(len(self._foregroundColor))
+ if self._foregroundColor != color:
+ self._foregroundColor = color
+ self._dirtyTicks()
+
+ @property
def ticks(self):
"""Ticks as tuples: ((x, y) in display, dataPos, textLabel)."""
if self._ticks is None:
@@ -192,6 +208,7 @@ class PlotAxis(object):
tickScale = 1.
label = Text2D(text=text,
+ color=self._foregroundColor,
x=xPixel - xTickLength,
y=yPixel - yTickLength,
align=self._labelAlign,
@@ -223,6 +240,7 @@ class PlotAxis(object):
# yOffset -= 3 * yTickLength
axisTitle = Text2D(text=self.title,
+ color=self._foregroundColor,
x=xAxisCenter + xOffset,
y=yAxisCenter + yOffset,
align=self._titleAlign,
@@ -373,15 +391,21 @@ class GLPlotFrame(object):
# Margins used when plot frame is not displayed
_NoDisplayMargins = _Margins(0, 0, 0, 0)
- def __init__(self, margins):
+ def __init__(self, margins, foregroundColor, gridColor):
"""
:param margins: The margins around plot area for axis and labels.
:type margins: dict with 'left', 'right', 'top', 'bottom' keys and
values as ints.
+ :param foregroundColor: color used for the frame and labels.
+ :type foregroundColor: tuple with RGBA values ranging from 0.0 to 1.0
+ :param gridColor: color used for grid lines.
+ :type gridColor: tuple RGBA with RGBA values ranging from 0.0 to 1.0
"""
self._renderResources = None
self._margins = self._Margins(**margins)
+ self._foregroundColor = foregroundColor
+ self._gridColor = gridColor
self.axes = [] # List of PlotAxis to be updated by subclasses
@@ -401,6 +425,36 @@ class GLPlotFrame(object):
GRID_ALL_TICKS = (GRID_MAIN_TICKS + GRID_SUB_TICKS)
@property
+ def foregroundColor(self):
+ """Color used for frame and labels"""
+ return self._foregroundColor
+
+ @foregroundColor.setter
+ def foregroundColor(self, color):
+ """Color used for frame and labels"""
+ assert len(color) == 4, \
+ "foregroundColor must have length 4, got {}".format(len(self._foregroundColor))
+ if self._foregroundColor != color:
+ self._foregroundColor = color
+ for axis in self.axes:
+ axis.foregroundColor = color
+ self._dirty()
+
+ @property
+ def gridColor(self):
+ """Color used for frame and labels"""
+ return self._gridColor
+
+ @gridColor.setter
+ def gridColor(self, color):
+ """Color used for frame and labels"""
+ assert len(color) == 4, \
+ "gridColor must have length 4, got {}".format(len(self._gridColor))
+ if self._gridColor != color:
+ self._gridColor = color
+ self._dirty()
+
+ @property
def displayed(self):
"""Whether axes and their labels are displayed or not (bool)"""
return self._displayed
@@ -522,6 +576,7 @@ class GLPlotFrame(object):
self.margins.right) // 2
yTitle = self.margins.top - self._TICK_LENGTH_IN_PIXELS
labels.append(Text2D(text=self.title,
+ color=self._foregroundColor,
x=xTitle,
y=yTitle,
align=CENTER,
@@ -556,7 +611,7 @@ class GLPlotFrame(object):
gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
matProj.astype(numpy.float32))
- gl.glUniform4f(prog.uniforms['color'], 0., 0., 0., 1.)
+ gl.glUniform4f(prog.uniforms['color'], *self._foregroundColor)
gl.glUniform1f(prog.uniforms['tickFactor'], 0.)
gl.glEnableVertexAttribArray(prog.attributes['position'])
@@ -590,7 +645,7 @@ class GLPlotFrame(object):
gl.glLineWidth(self._LINE_WIDTH)
gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
matProj.astype(numpy.float32))
- gl.glUniform4f(prog.uniforms['color'], 0.7, 0.7, 0.7, 1.)
+ gl.glUniform4f(prog.uniforms['color'], *self._gridColor)
gl.glUniform1f(prog.uniforms['tickFactor'], 0.) # 1/2.) # 1/tickLen
gl.glEnableVertexAttribArray(prog.attributes['position'])
@@ -606,15 +661,21 @@ class GLPlotFrame(object):
# GLPlotFrame2D ###############################################################
class GLPlotFrame2D(GLPlotFrame):
- def __init__(self, margins):
+ def __init__(self, margins, foregroundColor, gridColor):
"""
:param margins: The margins around plot area for axis and labels.
:type margins: dict with 'left', 'right', 'top', 'bottom' keys and
values as ints.
+ :param foregroundColor: color used for the frame and labels.
+ :type foregroundColor: tuple with RGBA values ranging from 0.0 to 1.0
+ :param gridColor: color used for grid lines.
+ :type gridColor: tuple RGBA with RGBA values ranging from 0.0 to 1.0
+
"""
- super(GLPlotFrame2D, self).__init__(margins)
+ super(GLPlotFrame2D, self).__init__(margins, foregroundColor, gridColor)
self.axes.append(PlotAxis(self,
tickLength=(0., -5.),
+ foregroundColor=self._foregroundColor,
labelAlign=CENTER, labelVAlign=TOP,
titleAlign=CENTER, titleVAlign=TOP,
titleRotate=0,
@@ -624,6 +685,7 @@ class GLPlotFrame2D(GLPlotFrame):
self.axes.append(PlotAxis(self,
tickLength=(5., 0.),
+ foregroundColor=self._foregroundColor,
labelAlign=RIGHT, labelVAlign=CENTER,
titleAlign=CENTER, titleVAlign=BOTTOM,
titleRotate=ROTATE_270,
@@ -632,6 +694,7 @@ class GLPlotFrame2D(GLPlotFrame):
self._y2Axis = PlotAxis(self,
tickLength=(-5., 0.),
+ foregroundColor=self._foregroundColor,
labelAlign=LEFT, labelVAlign=CENTER,
titleAlign=CENTER, titleVAlign=TOP,
titleRotate=ROTATE_270,
@@ -825,23 +888,6 @@ class GLPlotFrame2D(GLPlotFrame):
_logger.info('yMax: warning log10(%f)', y2Max)
y2Max = 0.
- # Non-orthogonal axes
- if self.baseVectors != self.DEFAULT_BASE_VECTORS:
- (xx, xy), (yx, yy) = self.baseVectors
- skew_mat = numpy.array(((xx, yx), (xy, yy)))
-
- corners = [(xMin, yMin), (xMin, yMax),
- (xMax, yMin), (xMax, yMax),
- (xMin, y2Min), (xMin, y2Max),
- (xMax, y2Min), (xMax, y2Max)]
-
- corners = numpy.array(
- [numpy.dot(skew_mat, corner) for corner in corners],
- dtype=numpy.float32)
- xMin, xMax = corners[:, 0].min(), corners[:, 0].max()
- yMin, yMax = corners[0:4, 1].min(), corners[0:4, 1].max()
- y2Min, y2Max = corners[4:, 1].min(), corners[4:, 1].max()
-
self._transformedDataRanges = self._DataRanges(
(xMin, xMax), (yMin, yMax), (y2Min, y2Max))
@@ -861,16 +907,6 @@ class GLPlotFrame2D(GLPlotFrame):
mat = mat4Ortho(xMin, xMax, yMax, yMin, 1, -1)
else:
mat = mat4Ortho(xMin, xMax, yMin, yMax, 1, -1)
-
- # Non-orthogonal axes
- if self.baseVectors != self.DEFAULT_BASE_VECTORS:
- (xx, xy), (yx, yy) = self.baseVectors
- mat = numpy.dot(mat, numpy.array((
- (xx, yx, 0., 0.),
- (xy, yy, 0., 0.),
- (0., 0., 1., 0.),
- (0., 0., 0., 1.)), dtype=numpy.float64))
-
self._transformedDataProjMat = mat
return self._transformedDataProjMat
@@ -890,16 +926,6 @@ class GLPlotFrame2D(GLPlotFrame):
mat = mat4Ortho(xMin, xMax, y2Max, y2Min, 1, -1)
else:
mat = mat4Ortho(xMin, xMax, y2Min, y2Max, 1, -1)
-
- # Non-orthogonal axes
- if self.baseVectors != self.DEFAULT_BASE_VECTORS:
- (xx, xy), (yx, yy) = self.baseVectors
- mat = numpy.dot(mat, numpy.matrix((
- (xx, yx, 0., 0.),
- (xy, yy, 0., 0.),
- (0., 0., 1., 0.),
- (0., 0., 0., 1.)), dtype=numpy.float64))
-
self._transformedDataY2ProjMat = mat
return self._transformedDataY2ProjMat
@@ -1114,3 +1140,17 @@ class GLPlotFrame2D(GLPlotFrame):
vertices = numpy.append(vertices, extraVertices, axis=0)
self._renderResources = (vertices, gridVertices, labels)
+
+ @property
+ def foregroundColor(self):
+ """Color used for frame and labels"""
+ return self._foregroundColor
+
+ @foregroundColor.setter
+ def foregroundColor(self, color):
+ """Color used for frame and labels"""
+ assert len(color) == 4, \
+ "foregroundColor must have length 4, got {}".format(len(self._foregroundColor))
+ if self._foregroundColor != color:
+ self._y2Axis.foregroundColor = color
+ GLPlotFrame.foregroundColor.fset(self, color) # call parent property
diff --git a/silx/gui/plot/backends/glutils/GLSupport.py b/silx/gui/plot/backends/glutils/GLSupport.py
index 18c5eb7..da6dffa 100644
--- a/silx/gui/plot/backends/glutils/GLSupport.py
+++ b/silx/gui/plot/backends/glutils/GLSupport.py
@@ -60,16 +60,12 @@ def buildFillMaskIndices(nIndices, dtype=None):
return indices
-class Shape2D(object):
+class FilledShape2D(object):
_NO_HATCH = 0
_HATCH_STEP = 20
- def __init__(self, points, fill='solid', stroke=True,
- fillColor=(0., 0., 0., 1.), strokeColor=(0., 0., 0., 1.),
- strokeClosed=True):
+ def __init__(self, points, style='solid', color=(0., 0., 0., 1.)):
self.vertices = numpy.array(points, dtype=numpy.float32, copy=False)
- self.strokeClosed = strokeClosed
-
self._indices = buildFillMaskIndices(len(self.vertices))
tVertex = numpy.transpose(self.vertices)
@@ -81,28 +77,16 @@ class Shape2D(object):
self._xMin, self._xMax = xMin, xMax
self._yMin, self._yMax = yMin, yMax
- self.fill = fill
- self.fillColor = fillColor
- self.stroke = stroke
- self.strokeColor = strokeColor
-
- @property
- def xMin(self):
- return self._xMin
-
- @property
- def xMax(self):
- return self._xMax
-
- @property
- def yMin(self):
- return self._yMin
+ self.style = style
+ self.color = color
- @property
- def yMax(self):
- return self._yMax
+ def render(self, posAttrib, colorUnif, hatchStepUnif):
+ assert self.style in ('hatch', 'solid')
+ gl.glUniform4f(colorUnif, *self.color)
+ step = self._HATCH_STEP if self.style == 'hatch' else self._NO_HATCH
+ gl.glUniform1i(hatchStepUnif, step)
- def prepareFillMask(self, posAttrib):
+ # Prepare fill mask
gl.glEnableVertexAttribArray(posAttrib)
gl.glVertexAttribPointer(posAttrib,
2,
@@ -126,9 +110,6 @@ class Shape2D(object):
gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
gl.glDepthMask(gl.GL_TRUE)
- def renderFill(self, posAttrib):
- self.prepareFillMask(posAttrib)
-
gl.glVertexAttribPointer(posAttrib,
2,
gl.GL_FLOAT,
@@ -138,30 +119,6 @@ class Shape2D(object):
gl.glDisable(gl.GL_STENCIL_TEST)
- def renderStroke(self, posAttrib):
- gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, self.vertices)
- gl.glLineWidth(1)
- drawMode = gl.GL_LINE_LOOP if self.strokeClosed else gl.GL_LINE_STRIP
- gl.glDrawArrays(drawMode, 0, len(self.vertices))
-
- def render(self, posAttrib, colorUnif, hatchStepUnif):
- assert self.fill in ['hatch', 'solid', None]
- if self.fill is not None:
- gl.glUniform4f(colorUnif, *self.fillColor)
- step = self._HATCH_STEP if self.fill == 'hatch' else self._NO_HATCH
- gl.glUniform1i(hatchStepUnif, step)
- self.renderFill(posAttrib)
-
- if self.stroke:
- gl.glUniform4f(colorUnif, *self.strokeColor)
- gl.glUniform1i(hatchStepUnif, self._NO_HATCH)
- self.renderStroke(posAttrib)
-
# matrix ######################################################################