summaryrefslogtreecommitdiff
path: root/silx/gui/plot/backends
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/backends')
-rwxr-xr-x[-rw-r--r--]silx/gui/plot/backends/BackendBase.py87
-rwxr-xr-x[-rw-r--r--]silx/gui/plot/backends/BackendMatplotlib.py519
-rwxr-xr-x[-rw-r--r--]silx/gui/plot/backends/BackendOpenGL.py990
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotCurve.py165
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotImage.py4
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotTriangles.py9
6 files changed, 894 insertions, 880 deletions
diff --git a/silx/gui/plot/backends/BackendBase.py b/silx/gui/plot/backends/BackendBase.py
index af37543..75d999b 100644..100755
--- a/silx/gui/plot/backends/BackendBase.py
+++ b/silx/gui/plot/backends/BackendBase.py
@@ -97,16 +97,15 @@ class BackendBase(object):
# Add methods
- def addCurve(self, x, y, legend,
+ def addCurve(self, x, y,
color, symbol, linewidth, linestyle,
yaxis,
- xerror, yerror, z, selectable,
- fill, alpha, symbolsize):
+ xerror, yerror, z,
+ fill, alpha, symbolsize, baseline):
"""Add a 1D curve given by x an y to the graph.
:param numpy.ndarray x: The data corresponding to the x axis
:param numpy.ndarray y: The data corresponding to the y axis
- :param str legend: The legend to be associated to the curve
:param color: color(s) to be used
:type color: string ("#RRGGBB") or (npoints, 4) unsigned byte array or
one of the predefined color names defined in colors.py
@@ -136,24 +135,21 @@ class BackendBase(object):
:param yerror: Values with the uncertainties on the y values
:type yerror: numpy.ndarray or None
:param int z: Layer on which to draw the cuve
- :param bool selectable: indicate if the curve can be selected
:param bool fill: True to fill the curve, False otherwise
:param float alpha: Curve opacity, as a float in [0., 1.]
:param float symbolsize: Size of the symbol (if any) drawn
at each (x, y) position.
:returns: The handle used by the backend to univocally access the curve
"""
- return legend
+ return object()
- def addImage(self, data, legend,
+ def addImage(self, data,
origin, scale, z,
- selectable, draggable,
colormap, alpha):
"""Add an image to the plot.
:param numpy.ndarray data: (nrows, ncolumns) data or
(nrows, ncolumns, RGBA) ubyte array
- :param str legend: The legend to be associated to the image
:param origin: (origin X, origin Y) of the data.
Default: (0., 0.)
:type origin: 2-tuple of float
@@ -161,39 +157,34 @@ class BackendBase(object):
Default: (1., 1.)
:type scale: 2-tuple of float
:param int z: Layer on which to draw the image
- :param bool selectable: indicate if the image can be selected
- :param bool draggable: indicate if the image can be moved
:param ~silx.gui.colors.Colormap colormap: Colormap object to use.
Ignored if data is RGB(A).
:param float alpha: Opacity of the image, as a float in range [0, 1].
:returns: The handle used by the backend to univocally access the image
"""
- return legend
+ return object()
- def addTriangles(self, x, y, triangles, legend,
- color, z, selectable, alpha):
+ def addTriangles(self, x, y, triangles,
+ color, z, alpha):
"""Add a set of triangles.
:param numpy.ndarray x: The data corresponding to the x axis
:param numpy.ndarray y: The data corresponding to the y axis
:param numpy.ndarray triangles: The indices to make triangles
as a (Ntriangle, 3) array
- :param str legend: The legend to be associated to the curve
:param numpy.ndarray color: color(s) as (npoints, 4) array
:param int z: Layer on which to draw the cuve
- :param bool selectable: indicate if the curve can be selected
:param float alpha: Opacity as a float in [0., 1.]
:returns: The triangles' unique identifier used by the backend
"""
- return legend
+ return object()
- def addItem(self, x, y, legend, shape, color, fill, overlay, z,
+ def addItem(self, x, y, 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
:param numpy.ndarray y: The Y coords of the points of the shape
- :param str legend: The legend to be associated to the item
:param str shape: Type of item to be drawn in
hline, polygon, rectangle, vline, polylines
:param str color: Color of the item
@@ -215,22 +206,18 @@ class BackendBase(object):
'#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
+ return object()
- def addMarker(self, x, y, legend, text, color,
- selectable, draggable,
- symbol, linestyle, linewidth, constraint):
+ def addMarker(self, x, y, text, color,
+ symbol, linestyle, linewidth, constraint, yaxis):
"""Add a point, vertical line or horizontal line marker to the plot.
:param float x: Horizontal position of the marker in graph coordinates.
If None, the marker is a horizontal line.
:param float y: Vertical position of the marker in graph coordinates.
If None, the marker is a vertical line.
- :param str legend: Legend associated to the marker
:param str text: Text associated to the marker (or None for no text)
:param str color: Color to be used for instance 'blue', 'b', '#FF0000'
- :param bool selectable: indicate if the marker can be selected
- :param bool draggable: indicate if the marker can be moved
:param str symbol: Symbol representing the marker.
Only relevant for point markers where X and Y are not None.
Value in:
@@ -257,13 +244,13 @@ class BackendBase(object):
dragging operations or None for no filter.
This function is called each time a marker is
moved.
- This parameter is only used if draggable is True.
:type constraint: None or a callable that takes the coordinates of
the current cursor position in the plot as input
and that returns the filtered coordinates.
+ :param str yaxis: The Y axis this marker belongs to in: 'left', 'right'
:return: Handle used by the backend to univocally access the marker
"""
- return legend
+ return object()
# Remove methods
@@ -307,22 +294,40 @@ class BackendBase(object):
"""
pass
- def pickItems(self, x, y, kinds):
- """Get a list of items at a pixel position.
+ def getItemsFromBackToFront(self, condition=None):
+ """Returns the list of plot items order as rendered by the backend.
+
+ This is the order used for rendering.
+ By default, it takes into account overlays, z value and order of addition of items,
+ but backends can override it.
+
+ :param callable condition:
+ Callable taking an item as input and returning False for items to skip.
+ If None (default), no item is skipped.
+ :rtype: List[~silx.gui.plot.items.Item]
+ """
+ # Sort items: Overlays first, then others
+ # and in each category ordered by z and then by order of addition
+ # as content keeps this order.
+ content = self._plot.getItems()
+ if condition is not None:
+ content = [item for item in content if condition(item)]
+
+ return sorted(
+ content,
+ key=lambda i: ((1 if i.isOverlay() else 0), i.getZValue()))
+
+ def pickItem(self, x, y, item):
+ """Return picked indices if any, or None.
:param float x: The x pixel coord where to pick.
:param float y: The y pixel coord where to pick.
- :param List[str] kind: List of item kinds to pick.
- Supported kinds: 'marker', 'curve', 'image'.
- :return: All picked items from back to front.
- One dict per item,
- with 'kind' key in 'curve', 'marker', 'image';
- 'legend' key, the item legend.
- and for curves, 'xdata' and 'ydata' keys storing picked
- position on the curve.
- :rtype: list of dict
- """
- return []
+ :param item: A backend item created with add* methods.
+ :return: None if item was not picked, else returns
+ picked indices information.
+ :rtype: Union[None,List]
+ """
+ return None
# Update curve
diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py
index 7739329..2336494 100644..100755
--- a/silx/gui/plot/backends/BackendMatplotlib.py
+++ b/silx/gui/plot/backends/BackendMatplotlib.py
@@ -56,12 +56,13 @@ from matplotlib.collections import PathCollection, LineCollection
from matplotlib.ticker import Formatter, ScalarFormatter, Locator
from matplotlib.tri import Triangulation
from matplotlib.collections import TriMesh
+from matplotlib import path as mpath
from . import BackendBase
+from .. import items
from .._utils import FLOAT32_MINPOS
from .._utils.dtime_ticklayout import calcTicks, bestFormatString, timestamp
-
_PATCH_LINESTYLE = {
"-": 'solid',
"--": 'dashed',
@@ -72,11 +73,54 @@ _PATCH_LINESTYLE = {
}
"""Patches do not uses the same matplotlib syntax"""
+_MARKER_PATHS = {}
+"""Store cached extra marker paths"""
+
+_SPECIAL_MARKERS = {
+ 'tickleft': 0,
+ 'tickright': 1,
+ 'tickup': 2,
+ 'tickdown': 3,
+ 'caretleft': 4,
+ 'caretright': 5,
+ 'caretup': 6,
+ 'caretdown': 7,
+}
+
def normalize_linestyle(linestyle):
"""Normalize known old-style linestyle, else return the provided value."""
return _PATCH_LINESTYLE.get(linestyle, linestyle)
+def get_path_from_symbol(symbol):
+ """Get the path representation of a symbol, else None if
+ it is not provided.
+
+ :param str symbol: Symbol description used by silx
+ :rtype: Union[None,matplotlib.path.Path]
+ """
+ if symbol == u'\u2665':
+ path = _MARKER_PATHS.get(symbol, None)
+ if path is not None:
+ return path
+ vertices = numpy.array([
+ [0,-99],
+ [31,-73], [47,-55], [55,-46],
+ [63,-37], [94,-2], [94,33],
+ [94,69], [71,89], [47,89],
+ [24,89], [8,74], [0,58],
+ [-8,74], [-24,89], [-47,89],
+ [-71,89], [-94,69], [-94,33],
+ [-94,-2], [-63,-37], [-55,-46],
+ [-47,-55], [-31,-73], [0,-99],
+ [0,-99]])
+ codes = [mpath.Path.CURVE4] * len(vertices)
+ codes[0] = mpath.Path.MOVETO
+ codes[-1] = mpath.Path.CLOSEPOLY
+ path = mpath.Path(vertices, codes)
+ _MARKER_PATHS[symbol] = path
+ return path
+ return None
class NiceDateLocator(Locator):
"""
@@ -162,7 +206,53 @@ class NiceAutoDateFormatter(Formatter):
return tickStr
-class _MarkerContainer(Container):
+class _PickableContainer(Container):
+ """Artists container with a :meth:`contains` method"""
+
+ def __init__(self, *args, **kwargs):
+ Container.__init__(self, *args, **kwargs)
+ self.__zorder = None
+
+ @property
+ def axes(self):
+ """Mimin Artist.axes"""
+ for child in self.get_children():
+ if hasattr(child, 'axes'):
+ return child.axes
+ return None
+
+ def draw(self, *args, **kwargs):
+ """artist-like draw to broadcast draw to children"""
+ for child in self.get_children():
+ child.draw(*args, **kwargs)
+
+ def get_zorder(self):
+ """Mimic Artist.get_zorder"""
+ return self.__zorder
+
+ def set_zorder(self, z):
+ """Mimic Artist.set_zorder to broadcast to children"""
+ if z != self.__zorder:
+ self.__zorder = z
+ for child in self.get_children():
+ child.set_zorder(z)
+
+ def contains(self, mouseevent):
+ """Mimic Artist.contains, and call it on all children.
+
+ :param mouseevent:
+ :return: Picking status and associated information as a dict
+ :rtype: (bool,dict)
+ """
+ # Goes through children from front to back and return first picked one.
+ for child in reversed(self.get_children()):
+ picked, info = child.contains(mouseevent)
+ if picked:
+ return picked, info
+ return False, {}
+
+
+class _MarkerContainer(_PickableContainer):
"""Marker artists container supporting draw/remove and text position update
:param artists:
@@ -173,13 +263,14 @@ class _MarkerContainer(Container):
:param y: Y coordinate of the marker (None for vertical lines)
"""
- def __init__(self, artists, x, y):
+ def __init__(self, artists, x, y, yAxis):
self.line = artists[0]
self.text = artists[1] if len(artists) > 1 else None
self.x = x
self.y = y
+ self.yAxis = yAxis
- Container.__init__(self, artists)
+ _PickableContainer.__init__(self, artists)
def draw(self, *args, **kwargs):
"""artist-like draw to broadcast draw to line and text"""
@@ -214,6 +305,15 @@ class _MarkerContainer(Container):
xmax -= 0.005 * delta
self.text.set_x(xmax)
+ def contains(self, mouseevent):
+ """Mimic Artist.contains, and call it on the line Artist.
+
+ :param mouseevent:
+ :return: Picking status and associated information as a dict
+ :rtype: (bool,dict)
+ """
+ return self.line.contains(mouseevent)
+
class _DoubleColoredLinePatch(matplotlib.patches.Patch):
"""Matplotlib patch to display any patch using double color."""
@@ -294,7 +394,11 @@ class BackendMatplotlib(BackendBase.BackendBase):
self.ax2 = self.ax.twinx()
self.ax2.set_label("right")
# Make sure background of Axes is displayed
- self.ax2.patch.set_visible(True)
+ self.ax2.patch.set_visible(False)
+ self.ax.patch.set_visible(True)
+
+ # Set axis zorder=0.5 so grid is displayed at 0.5
+ self.ax.set_axisbelow(True)
# disable the use of offsets
try:
@@ -306,10 +410,8 @@ class BackendMatplotlib(BackendBase.BackendBase):
_logger.warning('Cannot disabled axes offsets in %s '
% matplotlib.__version__)
- # critical for picking!!!!
- self.ax2.set_zorder(0)
self.ax2.set_autoscaley_on(True)
- self.ax.set_zorder(1)
+
# this works but the figure color is left
if self._matplotlibVersion < _parse_version('2'):
self.ax.set_axis_bgcolor('none')
@@ -317,7 +419,6 @@ class BackendMatplotlib(BackendBase.BackendBase):
self.ax.set_facecolor('none')
self.fig.sca(self.ax)
- self._overlays = set()
self._background = None
self._colormaps = {}
@@ -327,15 +428,67 @@ class BackendMatplotlib(BackendBase.BackendBase):
self._enableAxis('right', False)
self._isXAxisTimeSeries = False
+ def getItemsFromBackToFront(self, condition=None):
+ """Order as BackendBase + take into account matplotlib Axes structure"""
+ def axesOrder(item):
+ if item.isOverlay():
+ return 2
+ elif isinstance(item, items.YAxisMixIn) and item.getYAxis() == 'right':
+ return 1
+ else:
+ return 0
+
+ return sorted(
+ BackendBase.BackendBase.getItemsFromBackToFront(
+ self, condition=condition),
+ key=axesOrder)
+
+ def _overlayItems(self):
+ """Generator of backend renderer for overlay items"""
+ for item in self._plot.getItems():
+ if (item.isOverlay() and
+ item.isVisible() and
+ item._backendRenderer is not None):
+ yield item._backendRenderer
+
+ def _hasOverlays(self):
+ """Returns whether there is an overlay layer or not.
+
+ The overlay layers contains overlay items and the crosshair.
+
+ :rtype: bool
+ """
+ if self._graphCursor:
+ return True # There is the crosshair
+
+ for item in self._overlayItems():
+ return True # There is at least one overlay item
+ return False
+
# Add methods
- def addCurve(self, x, y, legend,
+ def _getMarkerFromSymbol(self, symbol):
+ """Returns a marker that can be displayed by matplotlib.
+
+ :param str symbol: A symbol description used by silx
+ :rtype: Union[str,int,matplotlib.path.Path]
+ """
+ path = get_path_from_symbol(symbol)
+ if path is not None:
+ return path
+ num = _SPECIAL_MARKERS.get(symbol, None)
+ if num is not None:
+ return num
+ # This symbol must be supported by matplotlib
+ return symbol
+
+ def addCurve(self, x, y,
color, symbol, linewidth, linestyle,
yaxis,
- xerror, yerror, z, selectable,
- fill, alpha, symbolsize):
- for parameter in (x, y, legend, color, symbol, linewidth, linestyle,
- yaxis, z, selectable, fill, alpha, symbolsize):
+ xerror, yerror, z,
+ fill, alpha, symbolsize, baseline):
+ for parameter in (x, y, color, symbol, linewidth, linestyle,
+ yaxis, z, fill, alpha, symbolsize):
assert parameter is not None
assert yaxis in ('left', 'right')
@@ -349,7 +502,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
else:
axes = self.ax
- picker = 3 if selectable else None
+ picker = 3
artists = [] # All the artists composing the curve
@@ -368,7 +521,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
yerror.shape[1] == 1):
yerror = numpy.ravel(yerror)
- errorbars = axes.errorbar(x, y, label=legend,
+ errorbars = axes.errorbar(x, y,
xerr=xerror, yerr=yerror,
linestyle=' ', color=errorbarColor)
artists += list(errorbars.get_children())
@@ -383,7 +536,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
if linestyle not in ["", " ", None]:
# scatter plot with an actual line ...
# we need to assign a color ...
- curveList = axes.plot(x, y, label=legend,
+ curveList = axes.plot(x, y,
linestyle=linestyle,
color=actualColor[0],
linewidth=linewidth,
@@ -391,21 +544,24 @@ class BackendMatplotlib(BackendBase.BackendBase):
marker=None)
artists += list(curveList)
+ marker = self._getMarkerFromSymbol(symbol)
scatter = axes.scatter(x, y,
- label=legend,
color=actualColor,
- marker=symbol,
+ marker=marker,
picker=picker,
s=symbolsize**2)
artists.append(scatter)
if fill:
+ if baseline is None:
+ _baseline = FLOAT32_MINPOS
+ else:
+ _baseline = baseline
artists.append(axes.fill_between(
- x, FLOAT32_MINPOS, y, facecolor=actualColor[0], linestyle=''))
+ x, _baseline, y, facecolor=actualColor[0], linestyle=''))
else: # Curve
curveList = axes.plot(x, y,
- label=legend,
linestyle=linestyle,
color=color,
linewidth=linewidth,
@@ -415,40 +571,35 @@ class BackendMatplotlib(BackendBase.BackendBase):
artists += list(curveList)
if fill:
+ if baseline is None:
+ _baseline = FLOAT32_MINPOS
+ else:
+ _baseline = baseline
artists.append(
- axes.fill_between(x, FLOAT32_MINPOS, y, facecolor=color))
+ axes.fill_between(x, _baseline, y, facecolor=color))
for artist in artists:
- artist.set_zorder(z)
if alpha < 1:
artist.set_alpha(alpha)
- return Container(artists)
+ return _PickableContainer(artists)
- def addImage(self, data, legend,
- origin, scale, z,
- selectable, draggable,
- colormap, alpha):
+ def addImage(self, data, origin, scale, z, colormap, alpha):
# Non-uniform image
# http://wiki.scipy.org/Cookbook/Histograms
# Non-linear axes
# http://stackoverflow.com/questions/11488800/non-linear-axes-for-imshow-in-matplotlib
- for parameter in (data, legend, origin, scale, z,
- selectable, draggable):
+ for parameter in (data, origin, scale, z):
assert parameter is not None
origin = float(origin[0]), float(origin[1])
scale = float(scale[0]), float(scale[1])
height, width = data.shape[0:2]
- picker = (selectable or draggable)
-
# All image are shown as RGBA image
image = Image(self.ax,
- label="__IMAGE__" + legend,
interpolation='nearest',
- picker=picker,
- zorder=z,
+ picker=True,
origin='lower')
if alpha < 1:
@@ -481,15 +632,10 @@ class BackendMatplotlib(BackendBase.BackendBase):
self.ax.add_artist(image)
return image
- def addTriangles(self, x, y, triangles, legend,
- color, z, selectable, alpha):
- for parameter in (x, y, triangles, legend, color,
- z, selectable, alpha):
+ def addTriangles(self, x, y, triangles, color, z, alpha):
+ for parameter in (x, y, triangles, color, z, alpha):
assert parameter is not None
- # 0 enables picking on filled triangle
- picker = 0 if selectable else None
-
color = numpy.array(color, copy=False)
assert color.ndim == 2 and len(color) == len(x)
@@ -498,16 +644,14 @@ class BackendMatplotlib(BackendBase.BackendBase):
collection = TriMesh(
Triangulation(x, y, triangles),
- label=legend,
alpha=alpha,
- picker=picker,
- zorder=z)
+ picker=0) # 0 enables picking on filled triangle
collection.set_color(color)
self.ax.add_collection(collection)
return collection
- def addItem(self, x, y, legend, shape, color, fill, overlay, z,
+ def addItem(self, x, y, shape, color, fill, overlay, z,
linestyle, linewidth, linebgcolor):
if (linebgcolor is not None and
shape not in ('rectangle', 'polygon', 'polylines')):
@@ -520,20 +664,20 @@ class BackendMatplotlib(BackendBase.BackendBase):
linestyle = normalize_linestyle(linestyle)
if shape == "line":
- item = self.ax.plot(x, y, label=legend, color=color,
+ item = self.ax.plot(x, y, color=color,
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, 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, color=color,
linestyle=linestyle, linewidth=linewidth)
elif shape == 'rectangle':
@@ -568,7 +712,6 @@ class BackendMatplotlib(BackendBase.BackendBase):
item = Polygon(points,
closed=closed,
fill=False,
- label=legend,
color=color,
linestyle=linestyle,
linewidth=linewidth)
@@ -584,30 +727,32 @@ class BackendMatplotlib(BackendBase.BackendBase):
else:
raise NotImplementedError("Unsupported item shape %s" % shape)
- item.set_zorder(z)
-
if overlay:
item.set_animated(True)
- self._overlays.add(item)
return item
- def addMarker(self, x, y, legend, text, color,
- selectable, draggable,
- symbol, linestyle, linewidth, constraint):
- legend = "__MARKER__" + legend
-
+ def addMarker(self, x, y, text, color,
+ symbol, linestyle, linewidth, constraint, yaxis):
textArtist = None
xmin, xmax = self.getGraphXLimits()
- ymin, ymax = self.getGraphYLimits(axis='left')
+ ymin, ymax = self.getGraphYLimits(axis=yaxis)
+
+ if yaxis == 'left':
+ ax = self.ax
+ elif yaxis == 'right':
+ ax = self.ax2
+ else:
+ assert(False)
+ marker = self._getMarkerFromSymbol(symbol)
if x is not None and y is not None:
- line = self.ax.plot(x, y, label=legend,
- linestyle=" ",
- color=color,
- marker=symbol,
- markersize=10.)[-1]
+ line = ax.plot(x, y,
+ linestyle=" ",
+ color=color,
+ marker=marker,
+ markersize=10.)[-1]
if text is not None:
if symbol is None:
@@ -616,43 +761,40 @@ class BackendMatplotlib(BackendBase.BackendBase):
valign = 'top'
text = " " + text
- textArtist = self.ax.text(x, y, text,
- color=color,
- horizontalalignment='left',
- verticalalignment=valign)
+ textArtist = ax.text(x, y, text,
+ color=color,
+ horizontalalignment='left',
+ verticalalignment=valign)
elif x is not None:
- line = self.ax.axvline(x,
- label=legend,
- color=color,
- linewidth=linewidth,
- linestyle=linestyle)
+ line = ax.axvline(x,
+ color=color,
+ linewidth=linewidth,
+ linestyle=linestyle)
if text is not None:
# Y position will be updated in updateMarkerText call
- textArtist = self.ax.text(x, 1., " " + text,
- color=color,
- horizontalalignment='left',
- verticalalignment='top')
+ textArtist = ax.text(x, 1., " " + text,
+ color=color,
+ horizontalalignment='left',
+ verticalalignment='top')
elif y is not None:
- line = self.ax.axhline(y,
- label=legend,
- color=color,
- linewidth=linewidth,
- linestyle=linestyle)
+ line = ax.axhline(y,
+ color=color,
+ linewidth=linewidth,
+ linestyle=linestyle)
if text is not None:
# X position will be updated in updateMarkerText call
- textArtist = self.ax.text(1., y, " " + text,
- color=color,
- horizontalalignment='right',
- verticalalignment='top')
+ textArtist = ax.text(1., y, " " + text,
+ color=color,
+ horizontalalignment='right',
+ verticalalignment='top')
else:
raise RuntimeError('A marker must at least have one coordinate')
- if selectable or draggable:
- line.set_picker(5)
+ line.set_picker(5)
# All markers are overlays
line.set_animated(True)
@@ -660,24 +802,25 @@ class BackendMatplotlib(BackendBase.BackendBase):
textArtist.set_animated(True)
artists = [line] if textArtist is None else [line, textArtist]
- container = _MarkerContainer(artists, x, y)
+ container = _MarkerContainer(artists, x, y, yaxis)
container.updateMarkerText(xmin, xmax, ymin, ymax)
- self._overlays.add(container)
return container
def _updateMarkers(self):
xmin, xmax = self.ax.get_xbound()
- ymin, ymax = self.ax.get_ybound()
- for item in self._overlays:
+ ymin1, ymax1 = self.ax.get_ybound()
+ ymin2, ymax2 = self.ax2.get_ybound()
+ for item in self._overlayItems():
if isinstance(item, _MarkerContainer):
- item.updateMarkerText(xmin, xmax, ymin, ymax)
+ if item.yAxis == 'left':
+ item.updateMarkerText(xmin, xmax, ymin1, ymax1)
+ else:
+ item.updateMarkerText(xmin, xmax, ymin2, ymax2)
# Remove methods
def remove(self, item):
- # Warning: It also needs to remove extra stuff if added as for markers
- self._overlays.discard(item)
try:
item.remove()
except ValueError:
@@ -699,7 +842,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
self._graphCursor = lineh, linev
else:
- if self._graphCursor is not None:
+ if self._graphCursor:
lineh, linev = self._graphCursor
lineh.remove()
linev.remove()
@@ -746,7 +889,37 @@ class BackendMatplotlib(BackendBase.BackendBase):
if not self.ax2.lines:
self._enableAxis('right', False)
+ def _drawOverlays(self):
+ """Draw overlays if any."""
+ def condition(item):
+ return (item.isVisible() and
+ item._backendRenderer is not None and
+ item.isOverlay())
+
+ for item in self.getItemsFromBackToFront(condition=condition):
+ if (isinstance(item, items.YAxisMixIn) and
+ item.getYAxis() == 'right'):
+ axes = self.ax2
+ else:
+ axes = self.ax
+ axes.draw_artist(item._backendRenderer)
+
+ for item in self._graphCursor:
+ self.ax.draw_artist(item)
+
+ def updateZOrder(self):
+ """Reorder all items with z order from 0 to 1"""
+ items = self.getItemsFromBackToFront(
+ lambda item: item.isVisible() and item._backendRenderer is not None)
+ count = len(items)
+ for index, item in enumerate(items):
+ zorder = 1. + index / count
+ if zorder != item._backendRenderer.get_zorder():
+ item._backendRenderer.set_zorder(zorder)
+
def saveGraph(self, fileName, fileFormat, dpi):
+ self.updateZOrder()
+
# fileName can be also a StringIO or file instance
if dpi is not None:
self.fig.savefig(fileName, format=fileFormat, dpi=dpi)
@@ -788,7 +961,9 @@ class BackendMatplotlib(BackendBase.BackendBase):
def getGraphXLimits(self):
if self._dirtyLimits and self.isKeepDataAspectRatio():
- self.replot() # makes sure we get the right limits
+ self.ax.apply_aspect()
+ self.ax2.apply_aspect()
+ self._dirtyLimits = False
return self.ax.get_xbound()
def setGraphXLimits(self, xmin, xmax):
@@ -804,7 +979,9 @@ class BackendMatplotlib(BackendBase.BackendBase):
return None
if self._dirtyLimits and self.isKeepDataAspectRatio():
- self.replot() # makes sure we get the right limits
+ self.ax.apply_aspect()
+ self.ax2.apply_aspect()
+ self._dirtyLimits = False
return ax.get_ybound()
@@ -917,13 +1094,16 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Data <-> Pixel coordinates conversion
- def _mplQtYAxisCoordConversion(self, y):
+ def _mplQtYAxisCoordConversion(self, y, asint=True):
"""Qt origin (top) to/from matplotlib origin (bottom) conversion.
+ :param y:
+ :param bool asint: True to cast to int, False to keep as float
+
:rtype: float
"""
- height = self.fig.get_window_extent().height
- return height - y
+ value = self.fig.get_window_extent().height - y
+ return int(value) if asint else value
def dataToPixel(self, x, y, axis):
ax = self.ax2 if axis == "right" else self.ax
@@ -932,35 +1112,27 @@ class BackendMatplotlib(BackendBase.BackendBase):
xPixel, yPixel = pixels.T
# Convert from matplotlib origin (bottom) to Qt origin (top)
- yPixel = self._mplQtYAxisCoordConversion(yPixel)
+ yPixel = self._mplQtYAxisCoordConversion(yPixel, asint=False)
return xPixel, yPixel
- def pixelToData(self, x, y, axis, check):
+ def pixelToData(self, x, y, axis):
ax = self.ax2 if axis == "right" else self.ax
# Convert from Qt origin (top) to matplotlib origin (bottom)
- y = self._mplQtYAxisCoordConversion(y)
+ y = self._mplQtYAxisCoordConversion(y, asint=False)
inv = ax.transData.inverted()
x, y = inv.transform_point((x, y))
-
- if check:
- xmin, xmax = self.getGraphXLimits()
- ymin, ymax = self.getGraphYLimits(axis=axis)
-
- if x > xmax or x < xmin or y > ymax or y < ymin:
- return None # (x, y) is out of plot area
-
return x, y
def getPlotBoundsInPixels(self):
bbox = self.ax.get_window_extent()
# Warning this is not returning int...
- return (bbox.xmin,
- self._mplQtYAxisCoordConversion(bbox.ymax),
- bbox.width,
- bbox.height)
+ return (int(bbox.xmin),
+ self._mplQtYAxisCoordConversion(bbox.ymax, asint=True),
+ int(bbox.width),
+ int(bbox.height))
def setAxesDisplayed(self, displayed):
"""Display or not the axes.
@@ -996,12 +1168,12 @@ class BackendMatplotlib(BackendBase.BackendBase):
else:
dataBackgroundColor = backgroundColor
- if self.ax2.axison:
+ if self.ax.axison:
self.fig.patch.set_facecolor(backgroundColor)
if self._matplotlibVersion < _parse_version('2'):
- self.ax2.set_axis_bgcolor(dataBackgroundColor)
+ self.ax.set_axis_bgcolor(dataBackgroundColor)
else:
- self.ax2.set_facecolor(dataBackgroundColor)
+ self.ax.set_facecolor(dataBackgroundColor)
else:
self.fig.patch.set_facecolor(dataBackgroundColor)
@@ -1033,6 +1205,12 @@ class BackendMatplotlib(BackendBase.BackendBase):
line.set_color(gridColor)
# axes.grid().set_markeredgecolor(gridColor)
+ def setBackgroundColors(self, backgroundColor, dataBackgroundColor):
+ self._synchronizeBackgroundColors()
+
+ def setForegroundColors(self, foregroundColor, gridColor):
+ self._synchronizeForegroundColors()
+
class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
"""QWidget matplotlib backend using a QtAgg canvas.
@@ -1079,14 +1257,16 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
_MPL_TO_PLOT_BUTTONS = {1: 'left', 2: 'middle', 3: 'right'}
def _onMousePress(self, event):
- self._plot.onMousePress(
- event.x, self._mplQtYAxisCoordConversion(event.y),
- self._MPL_TO_PLOT_BUTTONS[event.button])
+ button = self._MPL_TO_PLOT_BUTTONS.get(event.button, None)
+ if button is not None:
+ self._plot.onMousePress(
+ event.x, self._mplQtYAxisCoordConversion(event.y),
+ button)
def _onMouseMove(self, event):
if self._graphCursor:
lineh, linev = self._graphCursor
- if event.inaxes != self.ax and lineh.get_visible():
+ if event.inaxes not in (self.ax, self.ax2) and lineh.get_visible():
lineh.set_visible(False)
linev.set_visible(False)
self._plot._setDirtyPlot(overlayOnly=True)
@@ -1102,9 +1282,11 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
event.x, self._mplQtYAxisCoordConversion(event.y))
def _onMouseRelease(self, event):
- self._plot.onMouseRelease(
- event.x, self._mplQtYAxisCoordConversion(event.y),
- self._MPL_TO_PLOT_BUTTONS[event.button])
+ button = self._MPL_TO_PLOT_BUTTONS.get(event.button, None)
+ if button is not None:
+ self._plot.onMouseRelease(
+ event.x, self._mplQtYAxisCoordConversion(event.y),
+ button)
def _onMouseWheel(self, event):
self._plot.onMouseWheel(
@@ -1116,58 +1298,31 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
# picking
- def _onPick(self, event):
- # TODO not very nice and fragile, find a better way?
- # Make a selection according to kind
- if self._picked is None:
- _logger.error('Internal picking error')
- return
+ def pickItem(self, x, y, item):
+ mouseEvent = MouseEvent(
+ 'button_press_event', self, x, self._mplQtYAxisCoordConversion(y))
+ mouseEvent.inaxes = item.axes
+ picked, info = item.contains(mouseEvent)
- label = event.artist.get_label()
- if label.startswith('__MARKER__'):
- self._picked.append({'kind': 'marker', 'legend': label[10:]})
-
- elif label.startswith('__IMAGE__'):
- self._picked.append({'kind': 'image', 'legend': label[9:]})
+ if not picked:
+ return None
- elif isinstance(event.artist, TriMesh):
+ elif isinstance(item, TriMesh):
# Convert selected triangle to data point indices
- triangulation = event.artist._triangulation
- indices = triangulation.get_masked_triangles()[event.ind[0]]
+ triangulation = item._triangulation
+ indices = triangulation.get_masked_triangles()[info['ind'][0]]
# Sort picked triangle points by distance to mouse
# from furthest to closest to put closest point last
# This is to be somewhat consistent with last scatter point
# being the top one.
- dists = ((triangulation.x[indices] - event.mouseevent.xdata) ** 2 +
- (triangulation.y[indices] - event.mouseevent.ydata) ** 2)
- indices = indices[numpy.flip(numpy.argsort(dists))]
+ xdata, ydata = self.pixelToData(x, y, axis='left')
+ dists = ((triangulation.x[indices] - xdata) ** 2 +
+ (triangulation.y[indices] - ydata) ** 2)
+ return indices[numpy.flip(numpy.argsort(dists), axis=0)]
- self._picked.append({'kind': 'curve', 'legend': label,
- 'indices': indices})
-
- else: # it's a curve, item have no picker for now
- if not isinstance(event.artist, (PathCollection, Line2D)):
- _logger.info('Unsupported artist, ignored')
- return
-
- self._picked.append({'kind': 'curve', 'legend': label,
- 'indices': event.ind})
-
- def pickItems(self, x, y, kinds):
- self._picked = []
-
- # Weird way to do an explicit picking: Simulate a button press event
- mouseEvent = MouseEvent('button_press_event',
- self, x, self._mplQtYAxisCoordConversion(y))
- cid = self.mpl_connect('pick_event', self._onPick)
- self.fig.pick(mouseEvent)
- self.mpl_disconnect(cid)
-
- picked = [p for p in self._picked if p['kind'] in kinds]
- self._picked = None
-
- return picked
+ else: # Returns indices if any
+ return info.get('ind', ())
# replot control
@@ -1177,22 +1332,10 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
self.ax.get_xbound(), self.ax.get_ybound(), self.ax2.get_ybound())
FigureCanvasQTAgg.resizeEvent(self, event)
- if self.isKeepDataAspectRatio() or self._overlays or self._graphCursor:
+ if self.isKeepDataAspectRatio() or self._hasOverlays():
# This is needed with matplotlib 1.5.x and 2.0.x
self._plot._setDirtyPlot()
- def _drawOverlays(self):
- """Draw overlays if any."""
- if self._overlays or self._graphCursor:
- # There is some overlays or crosshair
-
- # This assume that items are only on left/bottom Axes
- for item in self._overlays:
- self.ax.draw_artist(item)
-
- for item in self._graphCursor:
- self.ax.draw_artist(item)
-
def draw(self):
"""Overload draw
@@ -1201,6 +1344,8 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
This is directly called by matplotlib for widget resize.
"""
+ self.updateZOrder()
+
# Starting with mpl 2.1.0, toggling autoscale raises a ValueError
# in some situations. See #1081, #1136, #1163,
if self._matplotlibVersion >= _parse_version("2.0.0"):
@@ -1213,7 +1358,7 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
else:
FigureCanvasQTAgg.draw(self)
- if self._overlays or self._graphCursor:
+ if self._hasOverlays():
# Save background
self._background = self.copy_from_bbox(self.fig.bbox)
else:
@@ -1257,7 +1402,7 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
if (_parse_version('1.5') <= self._matplotlibVersion < _parse_version('2.1') and
not hasattr(self, '_firstReplot')):
self._firstReplot = False
- if self._overlays or self._graphCursor:
+ if self._hasOverlays():
qt.QTimer.singleShot(0, self.draw) # Request async draw
# cursor
@@ -1276,9 +1421,3 @@ 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 0420aa9..27f3894 100644..100755
--- a/silx/gui/plot/backends/BackendOpenGL.py
+++ b/silx/gui/plot/backends/BackendOpenGL.py
@@ -30,13 +30,13 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "21/12/2018"
-from collections import OrderedDict, namedtuple
import logging
import warnings
import weakref
import numpy
+from .. import items
from .._utils import FLOAT32_MINPOS
from . import BackendBase
from ... import colors
@@ -59,186 +59,66 @@ _logger = logging.getLogger(__name__)
# TODO check if OpenGL is available
# TODO make an off-screen mesa backend
-# Bounds ######################################################################
-
-class Range(namedtuple('Range', ('min_', 'max_'))):
- """Describes a 1D range"""
-
- @property
- def range_(self):
- return self.max_ - self.min_
-
- @property
- def center(self):
- return 0.5 * (self.min_ + self.max_)
-
-
-class Bounds(object):
- """Describes plot bounds with 2 y axis"""
-
- def __init__(self, xMin, xMax, yMin, yMax, y2Min, y2Max):
- self._xAxis = Range(xMin, xMax)
- self._yAxis = Range(yMin, yMax)
- self._y2Axis = Range(y2Min, y2Max)
-
- def __repr__(self):
- return "x: %s, y: %s, y2: %s" % (repr(self._xAxis),
- repr(self._yAxis),
- repr(self._y2Axis))
-
- @property
- def xAxis(self):
- return self._xAxis
-
- @property
- def yAxis(self):
- return self._yAxis
-
- @property
- def y2Axis(self):
- return self._y2Axis
-
-
# Content #####################################################################
-class PlotDataContent(object):
- """Manage plot data content: images and curves.
-
- This class is only meant to work with _OpenGLPlotCanvas.
- """
-
- _PRIMITIVE_TYPES = 'curve', 'image', 'triangles'
-
- def __init__(self):
- self._primitives = OrderedDict() # For images and curves
-
- def add(self, primitive):
- """Add a curve or image to the content dictionary.
-
- This function generates the key in the dict from the primitive.
-
- :param primitive: The primitive to add.
- :type primitive: Instance of GLPlotCurve2D, GLPlotColormap,
- GLPlotRGBAImage.
- """
- if isinstance(primitive, GLPlotCurve2D):
- primitiveType = 'curve'
- elif isinstance(primitive, (GLPlotColormap, GLPlotRGBAImage)):
- primitiveType = 'image'
- elif isinstance(primitive, GLPlotTriangles):
- primitiveType = 'triangles'
- else:
- raise RuntimeError('Unsupported object type: %s', primitive)
-
- key = primitiveType, primitive.info['legend']
- self._primitives[key] = primitive
+class _ShapeItem(dict):
+ def __init__(self, x, y, shape, color, fill, overlay, z,
+ linestyle, linewidth, linebgcolor):
+ super(_ShapeItem, self).__init__()
- def get(self, primitiveType, legend):
- """Get the corresponding primitive of given type with given legend.
+ if shape not in ('polygon', 'rectangle', 'line',
+ 'vline', 'hline', 'polylines'):
+ raise NotImplementedError("Unsupported shape {0}".format(shape))
- :param str primitiveType: Type of primitive ('curve' or 'image').
- :param str legend: The legend of the primitive to retrieve.
- :return: The corresponding curve or None if no such curve.
- """
- assert primitiveType in self._PRIMITIVE_TYPES
- return self._primitives.get((primitiveType, legend))
+ x = numpy.array(x, copy=False)
+ y = numpy.array(y, copy=False)
- def pop(self, primitiveType, key):
- """Pop the corresponding curve or return None if no such curve.
+ if shape == 'rectangle':
+ xMin, xMax = x
+ x = numpy.array((xMin, xMin, xMax, xMax))
+ yMin, yMax = y
+ y = numpy.array((yMin, yMax, yMax, yMin))
- :param str primitiveType:
- :param str key:
- :return:
- """
- assert primitiveType in self._PRIMITIVE_TYPES
- return self._primitives.pop((primitiveType, key), None)
+ # Ignore fill for polylines to mimic matplotlib
+ fill = fill if shape != 'polylines' else False
- def zOrderedPrimitives(self, reverse=False):
- """List of primitives sorted according to their z order.
+ self.update({
+ 'shape': shape,
+ 'color': colors.rgba(color),
+ 'fill': 'hatch' if fill else None,
+ 'x': x,
+ 'y': y,
+ 'linestyle': linestyle,
+ 'linewidth': linewidth,
+ 'linebgcolor': linebgcolor,
+ })
- It is a stable sort (as sorted):
- Original order is preserved when key is the same.
- :param bool reverse: Ascending (True, default) or descending (False).
- """
- return sorted(self._primitives.values(),
- key=lambda primitive: primitive.info['zOrder'],
- reverse=reverse)
-
- def primitives(self):
- """Iterator over all primitives."""
- return self._primitives.values()
-
- def primitiveKeys(self, primitiveType):
- """Iterator over primitives of a specific type."""
- assert primitiveType in self._PRIMITIVE_TYPES
- for type_, key in self._primitives.keys():
- if type_ == primitiveType:
- yield key
-
- def getBounds(self, xPositive=False, yPositive=False):
- """Bounds of the data.
-
- Can return strictly positive bounds (for log scale).
- In this case, curves are clipped to their smaller positive value
- and images with negative min are ignored.
-
- :param bool xPositive: True to get strictly positive range.
- :param bool yPositive: True to get strictly positive range.
- :return: The range of data for x, y and y2, or default (1., 100.)
- if no range found for one dimension.
- :rtype: Bounds
- """
- xMin, yMin, y2Min = float('inf'), float('inf'), float('inf')
- xMax = 0. if xPositive else -float('inf')
- if yPositive:
- yMax, y2Max = 0., 0.
- else:
- yMax, y2Max = -float('inf'), -float('inf')
-
- for item in self._primitives.values():
- # To support curve <= 0. and log and bypass images:
- # If positive only, uses x|yMinPos if available
- # and bypass other data with negative min bounds
- if xPositive:
- itemXMin = getattr(item, 'xMinPos', item.xMin)
- if itemXMin is None or itemXMin < FLOAT32_MINPOS:
- continue
- else:
- itemXMin = item.xMin
+class _MarkerItem(dict):
+ def __init__(self, x, y, text, color,
+ symbol, linestyle, linewidth, constraint, yaxis):
+ super(_MarkerItem, self).__init__()
- if yPositive:
- itemYMin = getattr(item, 'yMinPos', item.yMin)
- if itemYMin is None or itemYMin < FLOAT32_MINPOS:
- continue
- else:
- itemYMin = item.yMin
-
- if itemXMin < xMin:
- xMin = itemXMin
- if item.xMax > xMax:
- xMax = item.xMax
-
- if item.info.get('yAxis') == 'right':
- if itemYMin < y2Min:
- y2Min = itemYMin
- if item.yMax > y2Max:
- y2Max = item.yMax
- else:
- if itemYMin < yMin:
- yMin = itemYMin
- if item.yMax > yMax:
- yMax = item.yMax
+ if symbol is None:
+ symbol = '+'
- # One of the limit has not been updated, return default range
- if xMin >= xMax:
- xMin, xMax = 1., 100.
- if yMin >= yMax:
- yMin, yMax = 1., 100.
- if y2Min >= y2Max:
- y2Min, y2Max = 1., 100.
+ # 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)
- return Bounds(xMin, xMax, yMin, yMax, y2Min, y2Max)
+ 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 #####################################################################
@@ -350,9 +230,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._crosshairCursor = None
self._mousePosInPixels = None
- self._markers = OrderedDict()
- self._items = OrderedDict()
- self._plotContent = PlotDataContent() # For images and curves
self._glGarbageCollector = []
self._plotFrame = GLPlotFrame2D(
@@ -457,7 +334,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def _paintDirectGL(self):
self._renderPlotAreaGL()
self._plotFrame.render()
- self._renderMarkersGL()
self._renderOverlayGL()
def _paintFBOGL(self):
@@ -522,7 +398,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
with plotFBOTex.texture:
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._plotVertices[0]))
- self._renderMarkersGL()
self._renderOverlayGL()
def paintGL(self):
@@ -543,120 +418,203 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# self._paintDirectGL()
self._paintFBOGL()
- def _renderMarkersGL(self):
- if len(self._markers) == 0:
- return
+ def _renderItems(self, overlay=False):
+ """Render items according to :class:`PlotWidget` order
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
+ Note: Scissor test should already be set.
- # Render in plot area
- gl.glScissor(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
- gl.glEnable(gl.GL_SCISSOR_TEST)
-
- gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
+ :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.getPlotBoundsInPixels()[2:]
+ isXLog = self._plotFrame.xAxis.isLog
+ isYLog = self._plotFrame.yAxis.isLog
+ # Used by marker rendering
labels = []
pixelOffset = 3
- for marker in self._markers.values():
- xCoord, yCoord = marker['x'], marker['y']
-
- if ((self._plotFrame.xAxis.isLog and
- xCoord is not None and
- xCoord <= 0) or
- (self._plotFrame.yAxis.isLog and
- yCoord is not None and
- yCoord <= 0)):
- # Do not render markers with negative coords on log axis
+ for plotItem in self.getItemsFromBackToFront(
+ condition=lambda i: i.isVisible() and i.isOverlay() == overlay):
+ if plotItem._backendRenderer is None:
continue
- if xCoord is None or yCoord is None:
- 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)
+ item = plotItem._backendRenderer
+
+ if isinstance(item, (GLPlotCurve2D,
+ GLPlotColormap,
+ GLPlotRGBAImage,
+ GLPlotTriangles)): # Render data items
+ gl.glViewport(self._plotFrame.margins.left,
+ self._plotFrame.margins.bottom,
+ plotWidth, plotHeight)
+ if isinstance(item, GLPlotCurve2D) and item.info.get('yAxis') == 'right':
+ item.render(self._plotFrame.transformedDataY2ProjMat,
+ isXLog, isYLog)
+ else:
+ item.render(self._plotFrame.transformedDataProjMat,
+ isXLog, isYLog)
+
+ 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]
- lines = GLLines2D((0, width), (pixelPos[1], pixelPos[1]),
- style=marker['linestyle'],
- color=marker['color'],
- width=marker['linewidth'])
+ _, yPixel = self._plot.dataToPixel(
+ None, item['y'], axis='left', check=False)
+ points = numpy.array(((0., yPixel), (width, yPixel)),
+ dtype=numpy.float32)
+
+ elif item['shape'] == 'vline':
+ xPixel, _ = self._plot.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._plot.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)
- else: # yCoord is None: vertical line in data space
- if marker['text'] is not None:
+ 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:
+ pixelPos = self._plot.dataToPixel(
+ xCoord, yCoord, axis=yAxis, check=False)
+
+ if xCoord is None: # Horizontal line in data space
+ if item['text'] is not None:
+ x = self._plotFrame.size[0] - \
+ self._plotFrame.margins.right - pixelOffset
+ y = pixelPos[1] - pixelOffset
+ label = Text2D(item['text'], x, y,
+ color=item['color'],
+ bgColor=(1., 1., 1., 0.5),
+ align=RIGHT, valign=BOTTOM)
+ labels.append(label)
+
+ width = self._plotFrame.size[0]
+ lines = GLLines2D((0, width), (pixelPos[1], pixelPos[1]),
+ style=item['linestyle'],
+ color=item['color'],
+ width=item['linewidth'])
+ lines.render(self.matScreenProj)
+
+ else: # yCoord is None: vertical line in data space
+ if item['text'] is not None:
+ x = pixelPos[0] + pixelOffset
+ y = self._plotFrame.margins.top + pixelOffset
+ label = Text2D(item['text'], x, y,
+ color=item['color'],
+ bgColor=(1., 1., 1., 0.5),
+ align=LEFT, valign=TOP)
+ labels.append(label)
+
+ height = self._plotFrame.size[1]
+ lines = GLLines2D((pixelPos[0], pixelPos[0]), (0, height),
+ style=item['linestyle'],
+ color=item['color'],
+ width=item['linewidth'])
+ lines.render(self.matScreenProj)
+
+ else:
+ pixelPos = self._plot.dataToPixel(
+ xCoord, yCoord, axis=yAxis, check=True)
+ if pixelPos is None:
+ # Do not render markers outside visible plot area
+ continue
+
+ if item['text'] is not None:
x = pixelPos[0] + pixelOffset
- y = self._plotFrame.margins.top + pixelOffset
- label = Text2D(marker['text'], x, y,
- color=marker['color'],
+ y = pixelPos[1] + pixelOffset
+ label = Text2D(item['text'], x, y,
+ color=item['color'],
bgColor=(1., 1., 1., 0.5),
align=LEFT, valign=TOP)
labels.append(label)
- 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)
+ # For now simple implementation: using a curve for each marker
+ # Should pack all markers to a single set of points
+ markerCurve = GLPlotCurve2D(
+ numpy.array((pixelPos[0],), dtype=numpy.float64),
+ numpy.array((pixelPos[1],), dtype=numpy.float64),
+ marker=item['symbol'],
+ markerColor=item['color'],
+ markerSize=11)
+ markerCurve.render(self.matScreenProj, False, False)
+ markerCurve.discard()
else:
- pixelPos = self.dataToPixel(
- xCoord, yCoord, axis='left', check=True)
- if pixelPos is None:
- # Do not render markers outside visible plot area
- continue
-
- if marker['text'] is not None:
- x = pixelPos[0] + pixelOffset
- y = pixelPos[1] + pixelOffset
- label = Text2D(marker['text'], x, y,
- color=marker['color'],
- bgColor=(1., 1., 1., 0.5),
- align=LEFT, valign=TOP)
- labels.append(label)
-
- # For now simple implementation: using a curve for each marker
- # Should pack all markers to a single set of points
- markerCurve = GLPlotCurve2D(
- numpy.array((pixelPos[0],), dtype=numpy.float64),
- numpy.array((pixelPos[1],), dtype=numpy.float64),
- marker=marker['symbol'],
- markerColor=marker['color'],
- markerSize=11)
- markerCurve.render(self.matScreenProj, False, False)
- markerCurve.discard()
-
- gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
+ _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)
- gl.glDisable(gl.GL_SCISSOR_TEST)
-
def _renderOverlayGL(self):
- # Render crosshair cursor
- if self._crosshairCursor is not None:
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
+ """Render overlay layer: overlay items and crosshair."""
+ plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
+
+ # Scissor to plot area
+ gl.glScissor(self._plotFrame.margins.left,
+ self._plotFrame.margins.bottom,
+ plotWidth, plotHeight)
+ gl.glEnable(gl.GL_SCISSOR_TEST)
- # 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.)
@@ -665,39 +623,39 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
colorUnif = self._progBase.uniforms['color']
hatchStepUnif = self._progBase.uniforms['hatchStep']
- # Render crosshair cursor in screen frame but with scissor
- if (self._crosshairCursor is not None and
- self._mousePosInPixels is not None):
- 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)
+ 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.getPlotBoundsInPixels()[2:]
gl.glScissor(self._plotFrame.margins.left,
@@ -713,85 +671,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# Matrix
trBounds = self._plotFrame.transformedDataRanges
- if trBounds.x[0] == trBounds.x[1] or \
- trBounds.y[0] == trBounds.y[1]:
- return
-
- isXLog = self._plotFrame.xAxis.isLog
- isYLog = self._plotFrame.yAxis.isLog
-
- gl.glViewport(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
-
- # Render images and curves
- # sorted is stable: original order is preserved when key is the same
- for item in self._plotContent.zOrderedPrimitives():
- if item.info.get('yAxis') == 'right':
- item.render(self._plotFrame.transformedDataY2ProjMat,
- isXLog, isYLog)
- else:
- item.render(self._plotFrame.transformedDataProjMat,
- isXLog, isYLog)
-
- # Render Items
- gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
-
- 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
-
- 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)
-
- 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)
+ 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)
@@ -841,13 +723,13 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
else:
raise ValueError('Unsupported data type')
- def addCurve(self, x, y, legend,
+ def addCurve(self, x, y,
color, symbol, linewidth, linestyle,
yaxis,
- xerror, yerror, z, selectable,
- fill, alpha, symbolsize):
- for parameter in (x, y, legend, color, symbol, linewidth, linestyle,
- yaxis, z, selectable, fill, symbolsize):
+ xerror, yerror, z,
+ fill, alpha, symbolsize, baseline):
+ for parameter in (x, y, color, symbol, linewidth, linestyle,
+ yaxis, z, fill, symbolsize):
assert parameter is not None
assert yaxis in ('left', 'right')
@@ -939,10 +821,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if color is not None:
color = color[0], color[1], color[2], color[3] * alpha
- behaviors = set()
- if selectable:
- behaviors.add('selectable')
-
+ fillColor = None
+ if fill is True:
+ fillColor = color
curve = GLPlotCurve2D(x, y, colorArray,
xError=xerror,
yError=yerror,
@@ -952,36 +833,24 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
marker=symbol,
markerColor=color,
markerSize=symbolsize,
- fillColor=color if fill else None,
+ fillColor=fillColor,
+ baseline=baseline,
isYLog=isYLog)
curve.info = {
- 'legend': legend,
- 'zOrder': z,
- 'behaviors': behaviors,
'yAxis': 'left' if yaxis is None else yaxis,
}
if yaxis == "right":
self._plotFrame.isY2Axis = True
- self._plotContent.add(curve)
+ return curve
- return legend, 'curve'
-
- def addImage(self, data, legend,
+ def addImage(self, data,
origin, scale, z,
- selectable, draggable,
colormap, alpha):
- for parameter in (data, legend, origin, scale, z,
- selectable, draggable):
+ for parameter in (data, origin, scale, z):
assert parameter is not None
- behaviors = set()
- if selectable:
- behaviors.add('selectable')
- if draggable:
- behaviors.add('draggable')
-
if data.ndim == 2:
# Ensure array is contiguous and eventually convert its type
if data.dtype in (numpy.float32, numpy.uint8, numpy.uint16):
@@ -1002,12 +871,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
colormapIsLog,
cmapRange,
alpha)
- image.info = {
- 'legend': legend,
- 'zOrder': z,
- 'behaviors': behaviors
- }
- self._plotContent.add(image)
elif len(data.shape) == 3:
# For RGB, RGBA data
@@ -1022,29 +885,21 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
image = GLPlotRGBAImage(data, origin, scale, alpha)
- image.info = {
- 'legend': legend,
- 'zOrder': z,
- 'behaviors': behaviors
- }
-
- 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')
-
- self._plotContent.add(image)
-
else:
raise RuntimeError("Unsupported data shape {0}".format(data.shape))
- return legend, 'image'
+ # 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')
- def addTriangles(self, x, y, triangles, legend,
- color, z, selectable, alpha):
+ return image
+ def addTriangles(self, x, y, triangles,
+ color, z, alpha):
# Handle axes log scale: convert data
if self._plotFrame.xAxis.isLog:
x = numpy.log10(x)
@@ -1052,31 +907,14 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
y = numpy.log10(y)
triangles = GLPlotTriangles(x, y, color, triangles, alpha)
- triangles.info = {
- 'legend': legend,
- 'zOrder': z,
- 'behaviors': set(['selectable']) if selectable else set(),
- }
- self._plotContent.add(triangles)
- return legend, 'triangles'
+ return triangles
- def addItem(self, x, y, legend, shape, color, fill, overlay, z,
+ def addItem(self, x, y, shape, color, fill, overlay, z,
linestyle, linewidth, linebgcolor):
- # TODO handle overlay
- 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))
-
# TODO is this needed?
if self._plotFrame.xAxis.isLog and x.min() <= 0.:
raise RuntimeError(
@@ -1085,84 +923,35 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
raise RuntimeError(
'Cannot add item with Y <= 0 with Y axis log scale')
- # Ignore fill for polylines to mimic matplotlib
- fill = fill if shape != 'polylines' else False
-
- self._items[legend] = {
- 'shape': shape,
- 'color': colors.rgba(color),
- 'fill': 'hatch' if fill else None,
- 'x': x,
- 'y': y,
- 'linestyle': linestyle,
- 'linewidth': linewidth,
- 'linebgcolor': linebgcolor,
- }
-
- return legend, 'item'
-
- def addMarker(self, x, y, legend, text, color,
- selectable, draggable,
- symbol, linestyle, linewidth, constraint):
-
- if symbol is None:
- symbol = '+'
-
- behaviors = set()
- if selectable:
- behaviors.add('selectable')
- if draggable:
- behaviors.add('draggable')
-
- # Apply constraint to provided position
- isConstraint = (draggable and constraint is not None and
- x is not None and y is not None)
- if isConstraint:
- x, y = constraint(x, y)
-
- self._markers[legend] = {
- 'x': x,
- 'y': y,
- 'legend': legend,
- 'text': text,
- 'color': colors.rgba(color),
- 'behaviors': behaviors,
- 'constraint': constraint if isConstraint else None,
- 'symbol': symbol,
- 'linestyle': linestyle,
- 'linewidth': linewidth,
- }
+ return _ShapeItem(x, y, shape, color, fill, overlay, z,
+ linestyle, linewidth, linebgcolor)
- return legend, 'marker'
+ 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):
- legend, kind = item
-
- if kind == 'curve':
- curve = self._plotContent.pop('curve', legend)
- if curve is not None:
+ if isinstance(item, (GLPlotCurve2D,
+ GLPlotColormap,
+ GLPlotRGBAImage,
+ GLPlotTriangles)):
+ if isinstance(item, GLPlotCurve2D):
# Check if some curves remains on the right Y axis
- y2AxisItems = (item for item in self._plotContent.primitives()
- if item.info.get('yAxis', 'left') == 'right')
+ 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
- self._glGarbageCollector.append(curve)
-
- elif kind in ('image', 'triangles'):
- item = self._plotContent.pop(kind, legend)
- if item is not None:
- self._glGarbageCollector.append(item)
+ self._glGarbageCollector.append(item)
- elif kind == 'marker':
- self._markers.pop(legend, False)
-
- elif kind == 'item':
- self._items.pop(legend, False)
+ elif isinstance(item, (_MarkerItem, _ShapeItem)):
+ pass # No-op
else:
- _logger.error('Unsupported kind: %s', str(kind))
+ _logger.error('Unsupported item: %s', str(item))
# Interaction methods
@@ -1212,8 +1001,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
: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
- :rtype: List[int]
+ :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:
@@ -1224,17 +1013,17 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
yAxis = item.info['yAxis']
inAreaPos = self._mouseInPlotArea(x - offset, y - offset)
- dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1],
- axis=yAxis, check=True)
+ dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1],
+ axis=yAxis, check=True)
if dataPos is None:
- return []
+ return None
xPick0, yPick0 = dataPos
inAreaPos = self._mouseInPlotArea(x + offset, y + offset)
- dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1],
- axis=yAxis, check=True)
+ dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1],
+ axis=yAxis, check=True)
if dataPos is None:
- return []
+ return None
xPick1, yPick1 = dataPos
if xPick0 < xPick1:
@@ -1260,69 +1049,58 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return item.pick(xPickMin, yPickMin,
xPickMax, yPickMax)
- def pickItems(self, x, y, kinds):
- picked = []
-
- dataPos = self.pixelToData(x, y, axis='left', check=True)
- if dataPos is not None:
- # Pick markers
- if 'marker' in kinds:
- for marker in reversed(list(self._markers.values())):
- pixelPos = self.dataToPixel(
- marker['x'], marker['y'], axis='left', check=False)
- if pixelPos is None: # negative coord on a log axis
- continue
+ def pickItem(self, x, y, item):
+ 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]))
- if marker['x'] is None: # Horizontal line
- pt1 = self.pixelToData(
- x, y - self._PICK_OFFSET, axis='left', check=False)
- pt2 = self.pixelToData(
- x, y + self._PICK_OFFSET, axis='left', check=False)
- isPicked = (min(pt1[1], pt2[1]) <= marker['y'] <=
- max(pt1[1], pt2[1]))
-
- elif marker['y'] is None: # Vertical line
- pt1 = self.pixelToData(
- x - self._PICK_OFFSET, y, axis='left', check=False)
- pt2 = self.pixelToData(
- x + self._PICK_OFFSET, y, axis='left', check=False)
- isPicked = (min(pt1[0], pt2[0]) <= marker['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)
-
- if isPicked:
- picked.append(dict(kind='marker',
- legend=marker['legend']))
-
- # Pick image and curves
- if 'image' in kinds or 'curve' in kinds:
- for item in self._plotContent.zOrderedPrimitives(reverse=True):
- if ('image' in kinds and
- isinstance(item, (GLPlotColormap, GLPlotRGBAImage))):
- pickedPos = item.pick(*dataPos)
- if pickedPos is not None:
- picked.append(dict(kind='image',
- legend=item.info['legend']))
-
- elif 'curve' in kinds:
- if isinstance(item, GLPlotCurve2D):
- pickedIndices = self.__pickCurves(item, x, y)
- if pickedIndices:
- picked.append(dict(kind='curve',
- legend=item.info['legend'],
- indices=pickedIndices))
-
- elif isinstance(item, GLPlotTriangles):
- pickedIndices = item.pick(*dataPos)
- if pickedIndices:
- picked.append(dict(kind='curve',
- legend=item.info['legend'],
- indices=pickedIndices))
- return picked
+ 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, (GLPlotCurve2D,
+ GLPlotColormap,
+ GLPlotRGBAImage,
+ GLPlotTriangles)):
+ if isinstance(item, (GLPlotColormap, GLPlotRGBAImage, GLPlotTriangles)):
+ return item.pick(*dataPos) # Might be None
+
+ elif isinstance(item, GLPlotCurve2D):
+ return self.__pickCurves(item, x, y)
+ else:
+ return None
# Update curve
@@ -1426,12 +1204,11 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return
if keepDim is None:
- dataBounds = self._plotContent.getBounds(
- self._plotFrame.xAxis.isLog, self._plotFrame.yAxis.isLog)
- if dataBounds.yAxis.range_ != 0.:
- dataRatio = dataBounds.xAxis.range_
- dataRatio /= float(dataBounds.yAxis.range_)
-
+ 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'
@@ -1564,51 +1341,10 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# Data <-> Pixel coordinates conversion
- def dataToPixel(self, x, y, axis, check=False):
- assert axis in ('left', 'right')
-
- if x is None or y is None:
- dataBounds = self._plotContent.getBounds(
- self._plotFrame.xAxis.isLog, self._plotFrame.yAxis.isLog)
-
- if x is None:
- x = dataBounds.xAxis.center
-
- if y is None:
- if axis == 'left':
- y = dataBounds.yAxis.center
- else:
- y = dataBounds.y2Axis.center
-
- result = self._plotFrame.dataToPixel(x, y, axis)
-
- if check and result is not None:
- xPixel, yPixel = result
- width, height = self._plotFrame.size
- if (xPixel < self._plotFrame.margins.left or
- xPixel > (width - self._plotFrame.margins.right) or
- yPixel < self._plotFrame.margins.top or
- yPixel > height - self._plotFrame.margins.bottom):
- return None # (x, y) is out of plot area
-
- return result
-
- def pixelToData(self, x, y, axis, check):
- assert axis in ("left", "right")
-
- if x is None:
- x = self._plotFrame.size[0] / 2.
- if y is None:
- y = self._plotFrame.size[1] / 2.
-
- if check and (x < self._plotFrame.margins.left or
- x > (self._plotFrame.size[0] -
- self._plotFrame.margins.right) or
- y < self._plotFrame.margins.top or
- y > (self._plotFrame.size[1] -
- self._plotFrame.margins.bottom)):
- return None # (x, y) is out of plot area
+ def dataToPixel(self, x, y, axis):
+ return self._plotFrame.dataToPixel(x, y, axis)
+ def pixelToData(self, x, y, axis):
return self._plotFrame.pixelToData(x, y, axis)
def getPlotBoundsInPixels(self):
diff --git a/silx/gui/plot/backends/glutils/GLPlotCurve.py b/silx/gui/plot/backends/glutils/GLPlotCurve.py
index 5f8d652..3a0ebac 100644
--- a/silx/gui/plot/backends/glutils/GLPlotCurve.py
+++ b/silx/gui/plot/backends/glutils/GLPlotCurve.py
@@ -132,8 +132,7 @@ class _Fill2D(object):
self.xData is not None and self.yData is not None):
# Get slices of not NaN values longer than 1 element
- isnan = numpy.logical_or(numpy.isnan(self.xData),
- numpy.isnan(self.yData))
+ isnan = numpy.logical_or(numpy.isnan(self.xData), numpy.isnan(self.yData))
notnan = numpy.logical_not(isnan)
start = numpy.where(numpy.logical_and(isnan[:-1], notnan[1:]))[0] + 1
if notnan[0]:
@@ -147,22 +146,25 @@ class _Fill2D(object):
# Number of points: slice + 2 * leading and trailing points
# Twice leading and trailing points to produce degenerated triangles
- nbPoints = numpy.sum(numpy.diff(slices, axis=1)) + 4 * len(slices)
+ nbPoints = numpy.sum(numpy.diff(slices, axis=1)) * 2 + 4 * len(slices)
points = numpy.empty((nbPoints, 2), dtype=numpy.float32)
offset = 0
+ # invert baseline for filling
+ new_y_data = numpy.append(self.yData, self.baseline)
for start, end in slices:
# Duplicate first point for connecting degenerated triangle
- points[offset:offset+2] = self.xData[start], self.baseline
+ points[offset:offset+2] = self.xData[start], new_y_data[start]
# 2nd point of the polygon is last point
- points[offset+2] = self.xData[end-1], self.baseline
+ points[offset+2] = self.xData[start], self.baseline[start]
- # Add all points from the data
- indices = start + buildFillMaskIndices(end - start)
+ indices = numpy.append(numpy.arange(start, end),
+ numpy.arange(len(self.xData) + end-1, len(self.xData) + start-1, -1))
+ indices = indices[buildFillMaskIndices(len(indices))]
- points[offset+3:offset+3+len(indices), 0] = self.xData[indices]
- points[offset+3:offset+3+len(indices), 1] = self.yData[indices]
+ points[offset+3:offset+3+len(indices), 0] = self.xData[indices % len(self.xData)]
+ points[offset+3:offset+3+len(indices), 1] = new_y_data[indices]
# Duplicate last point for connecting degenerated triangle
points[offset+3+len(indices)] = points[offset+3+len(indices)-1]
@@ -526,7 +528,16 @@ def distancesFromArrays(xData, yData):
DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK = \
'd', 'o', 's', '+', 'x', '.', ',', '*'
-H_LINE, V_LINE = '_', '|'
+H_LINE, V_LINE, HEART = '_', '|', u'\u2665'
+
+TICK_LEFT = "tickleft"
+TICK_RIGHT = "tickright"
+TICK_UP = "tickup"
+TICK_DOWN = "tickdown"
+CARET_LEFT = "caretleft"
+CARET_RIGHT = "caretright"
+CARET_UP = "caretup"
+CARET_DOWN = "caretdown"
class _Points2D(object):
@@ -542,7 +553,8 @@ class _Points2D(object):
"""
MARKERS = (DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK,
- H_LINE, V_LINE)
+ H_LINE, V_LINE, HEART, TICK_LEFT, TICK_RIGHT, TICK_UP, TICK_DOWN,
+ CARET_LEFT, CARET_RIGHT, CARET_UP, CARET_DOWN)
"""List of supported markers"""
_VERTEX_SHADER = """
@@ -640,7 +652,110 @@ class _Points2D(object):
return 0.0;
}
}
- """
+ """,
+ HEART: """
+ float alphaSymbol(vec2 coord, float size) {
+ coord = (coord - 0.5) * 2.;
+ coord *= 0.75;
+ coord.y += 0.25;
+ float a = atan(coord.x,-coord.y)/3.141593;
+ float r = length(coord);
+ float h = abs(a);
+ float d = (13.0*h - 22.0*h*h + 10.0*h*h*h)/(6.0-5.0*h);
+ float res = clamp(r-d, 0., 1.);
+ // antialiasing
+ res = smoothstep(0.1, 0.001, res);
+ return res;
+ }
+ """,
+ TICK_LEFT: """
+ float alphaSymbol(vec2 coord, float size) {
+ coord = size * (coord - 0.5);
+ float dy = abs(coord.y);
+ if (dy < 0.5 && coord.x < 0.5) {
+ return 1.0;
+ } else {
+ return 0.0;
+ }
+ }
+ """,
+ TICK_RIGHT: """
+ float alphaSymbol(vec2 coord, float size) {
+ coord = size * (coord - 0.5);
+ float dy = abs(coord.y);
+ if (dy < 0.5 && coord.x > -0.5) {
+ return 1.0;
+ } else {
+ return 0.0;
+ }
+ }
+ """,
+ TICK_UP: """
+ float alphaSymbol(vec2 coord, float size) {
+ coord = size * (coord - 0.5);
+ float dx = abs(coord.x);
+ if (dx < 0.5 && coord.y < 0.5) {
+ return 1.0;
+ } else {
+ return 0.0;
+ }
+ }
+ """,
+ TICK_DOWN: """
+ float alphaSymbol(vec2 coord, float size) {
+ coord = size * (coord - 0.5);
+ float dx = abs(coord.x);
+ if (dx < 0.5 && coord.y > -0.5) {
+ return 1.0;
+ } else {
+ return 0.0;
+ }
+ }
+ """,
+ CARET_LEFT: """
+ float alphaSymbol(vec2 coord, float size) {
+ coord = size * (coord - 0.5);
+ float d = abs(coord.x) - abs(coord.y);
+ if (d >= -0.1 && coord.x > 0.5) {
+ return smoothstep(-0.1, 0.1, d);
+ } else {
+ return 0.0;
+ }
+ }
+ """,
+ CARET_RIGHT: """
+ float alphaSymbol(vec2 coord, float size) {
+ coord = size * (coord - 0.5);
+ float d = abs(coord.x) - abs(coord.y);
+ if (d >= -0.1 && coord.x < 0.5) {
+ return smoothstep(-0.1, 0.1, d);
+ } else {
+ return 0.0;
+ }
+ }
+ """,
+ CARET_UP: """
+ float alphaSymbol(vec2 coord, float size) {
+ coord = size * (coord - 0.5);
+ float d = abs(coord.y) - abs(coord.x);
+ if (d >= -0.1 && coord.y > 0.5) {
+ return smoothstep(-0.1, 0.1, d);
+ } else {
+ return 0.0;
+ }
+ }
+ """,
+ CARET_DOWN: """
+ float alphaSymbol(vec2 coord, float size) {
+ coord = size * (coord - 0.5);
+ float d = abs(coord.y) - abs(coord.x);
+ if (d >= -0.1 && coord.y < 0.5) {
+ return smoothstep(-0.1, 0.1, d);
+ } else {
+ return 0.0;
+ }
+ }
+ """,
}
_FRAGMENT_SHADER_TEMPLATE = """
@@ -964,6 +1079,7 @@ class GLPlotCurve2D(object):
markerColor=(0., 0., 0., 1.),
markerSize=7,
fillColor=None,
+ baseline=None,
isYLog=False):
self.colorData = colorData
@@ -1003,11 +1119,28 @@ class GLPlotCurve2D(object):
self.offset = 0., 0.
self.xData = xData
self.yData = yData
-
if fillColor is not None:
+ def deduce_baseline(baseline):
+ if baseline is None:
+ _baseline = 0
+ else:
+ _baseline = baseline
+ if not isinstance(_baseline, numpy.ndarray):
+ _baseline = numpy.repeat(_baseline,
+ len(self.xData))
+ if isYLog is True:
+ with warnings.catch_warnings(): # Ignore NaN comparison warnings
+ warnings.simplefilter('ignore',
+ category=RuntimeWarning)
+ log_val = numpy.log10(_baseline)
+ _baseline = numpy.where(_baseline>0.0, log_val, -38)
+ return _baseline
+
+ _baseline = deduce_baseline(baseline)
+
# Use different baseline depending of Y log scale
self.fill = _Fill2D(self.xData, self.yData,
- baseline=-38 if isYLog else 0,
+ baseline=_baseline,
color=fillColor,
offset=self.offset)
else:
@@ -1129,7 +1262,7 @@ class GLPlotCurve2D(object):
and the segment [i-1, i] is not tested for picking.
:return: The indices of the picked data
- :rtype: list of int
+ :rtype: Union[List[int],None]
"""
if (self.marker is None and self.lineStyle is None) or \
self.xMin > xPickMax or xPickMin > self.xMax or \
@@ -1209,4 +1342,4 @@ class GLPlotCurve2D(object):
(self.yData >= yPickMin) &
(self.yData <= yPickMax))[0].tolist()
- return indices
+ return tuple(indices) if len(indices) > 0 else None
diff --git a/silx/gui/plot/backends/glutils/GLPlotImage.py b/silx/gui/plot/backends/glutils/GLPlotImage.py
index 6f3c487..5d79023 100644
--- a/silx/gui/plot/backends/glutils/GLPlotImage.py
+++ b/silx/gui/plot/backends/glutils/GLPlotImage.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
@@ -56,7 +56,7 @@ class _GLPlotData2D(object):
sx, sy = self.scale
col = int((x - ox) / sx)
row = int((y - oy) / sy)
- return col, row
+ return ((row, col),)
else:
return None
diff --git a/silx/gui/plot/backends/glutils/GLPlotTriangles.py b/silx/gui/plot/backends/glutils/GLPlotTriangles.py
index c756749..7aeb5ab 100644
--- a/silx/gui/plot/backends/glutils/GLPlotTriangles.py
+++ b/silx/gui/plot/backends/glutils/GLPlotTriangles.py
@@ -114,11 +114,12 @@ class GLPlotTriangles(object):
:param float x: X coordinates in plot data frame
:param float y: Y coordinates in plot data frame
:return: List of picked data point indices
- :rtype: numpy.ndarray
+ :rtype: Union[List[int],None]
"""
if (x < self.xMin or x > self.xMax or
y < self.yMin or y > self.yMax):
- return ()
+ return None
+
xPts, yPts = self.__x_y_color[:2]
if self.__picking_triangles is None:
self.__picking_triangles = numpy.zeros(
@@ -135,9 +136,9 @@ class GLPlotTriangles(object):
# Sorted from furthest to closest point
dists = (xPts[indices] - x) ** 2 + (yPts[indices] - y) ** 2
- indices = indices[numpy.flip(numpy.argsort(dists))]
+ indices = indices[numpy.flip(numpy.argsort(dists), axis=0)]
- return tuple(indices)
+ return tuple(indices) if len(indices) > 0 else None
def discard(self):
"""Release resources on the GPU"""