summaryrefslogtreecommitdiff
path: root/silx/gui/plot/backends/BackendMatplotlib.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/backends/BackendMatplotlib.py')
-rw-r--r--silx/gui/plot/backends/BackendMatplotlib.py223
1 files changed, 161 insertions, 62 deletions
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()