diff --git a/doc/users/whats_new/2016-04_qt_config-AL.rst b/doc/users/whats_new/2016-04_qt_config-AL.rst
new file mode 100644
index 000000000000..434ec43c7cda
--- /dev/null
+++ b/doc/users/whats_new/2016-04_qt_config-AL.rst
@@ -0,0 +1,14 @@
+Improvements for the Qt figure options editor
+---------------------------------------------
+
+Various usability improvements were implemented for the Qt figure options
+editor, among which:
+- Line style entries are now sorted without duplicates.
+- The colormap and normalization limits can now be set for images.
+- Line edits for floating values now display only as many digits as necessary
+ to avoid precision loss. An important bug was also fixed regarding input
+ validation using Qt5 and a locale where the decimal separator is ",".
+- The axes selector now uses shorter, more user-friendly names for axes, and
+ does not crash if there are no axes.
+- Line and image entries using the default labels ("_lineX", "_imageX") are now
+ sorted numerically even when there are more than 10 entries.
diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py
index 1e50188d14f0..05c4e43265d9 100644
--- a/lib/matplotlib/axes/_base.py
+++ b/lib/matplotlib/axes/_base.py
@@ -1732,6 +1732,8 @@ def add_image(self, image):
Returns the image.
"""
self._set_artist_props(image)
+ if not image.get_label():
+ image.set_label('_image%d' % len(self.images))
self.images.append(image)
image._remove_method = lambda h: self.images.remove(h)
self.stale = True
diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py
index 1b220173c5f6..038073edd6a1 100644
--- a/lib/matplotlib/backends/qt_editor/figureoptions.py
+++ b/lib/matplotlib/backends/qt_editor/figureoptions.py
@@ -13,23 +13,24 @@
import six
import os.path as osp
+import re
+import matplotlib
+from matplotlib import cm, markers, colors as mcolors
import matplotlib.backends.qt_editor.formlayout as formlayout
from matplotlib.backends.qt_compat import QtGui
-from matplotlib import markers
-from matplotlib.colors import colorConverter, rgb2hex
def get_icon(name):
- import matplotlib
basedir = osp.join(matplotlib.rcParams['datapath'], 'images')
return QtGui.QIcon(osp.join(basedir, name))
+
LINESTYLES = {'-': 'Solid',
'--': 'Dashed',
'-.': 'DashDot',
':': 'Dotted',
- 'none': 'None',
+ 'None': 'None',
}
DRAWSTYLES = {'default': 'Default',
@@ -43,8 +44,6 @@ def figure_edit(axes, parent=None):
"""Edit matplotlib figure options"""
sep = (None, None) # separator
- has_curve = len(axes.get_lines()) > 0
-
# Get / General
xmin, xmax = axes.get_xlim()
ymin, ymax = axes.get_ylim()
@@ -69,57 +68,115 @@ def figure_edit(axes, parent=None):
xunits = axes.xaxis.get_units()
yunits = axes.yaxis.get_units()
- if has_curve:
- # Get / Curves
- linedict = {}
- for line in axes.get_lines():
- label = line.get_label()
- if label == '_nolegend_':
- continue
- linedict[label] = line
- curves = []
- linestyles = list(six.iteritems(LINESTYLES))
- drawstyles = list(six.iteritems(DRAWSTYLES))
- markers = list(six.iteritems(MARKERS))
- curvelabels = sorted(linedict.keys())
- for label in curvelabels:
- line = linedict[label]
- color = rgb2hex(colorConverter.to_rgb(line.get_color()))
- ec = rgb2hex(colorConverter.to_rgb(line.get_markeredgecolor()))
- fc = rgb2hex(colorConverter.to_rgb(line.get_markerfacecolor()))
- curvedata = [('Label', label),
- sep,
- (None, 'Line'),
- ('Line Style', [line.get_linestyle()] + linestyles),
- ('Draw Style', [line.get_drawstyle()] + drawstyles),
- ('Width', line.get_linewidth()),
- ('Color', color),
- sep,
- (None, 'Marker'),
- ('Style', [line.get_marker()] + markers),
- ('Size', line.get_markersize()),
- ('Facecolor', fc),
- ('Edgecolor', ec),
- ]
- curves.append([curvedata, label, ""])
-
- # make sure that there is at least one displayed curve
- has_curve = bool(curves)
+ # Sorting for default labels (_lineXXX, _imageXXX).
+ def cmp_key(label):
+ match = re.match(r"(_line|_image)(\d+)", label)
+ if match:
+ return match.group(1), int(match.group(2))
+ else:
+ return label, 0
+
+ # Get / Curves
+ linedict = {}
+ for line in axes.get_lines():
+ label = line.get_label()
+ if label == '_nolegend_':
+ continue
+ linedict[label] = line
+ curves = []
+
+ def prepare_data(d, init):
+ """Prepare entry for FormLayout.
+
+ `d` is a mapping of shorthands to style names (a single style may
+ have multiple shorthands, in particular the shorthands `None`,
+ `"None"`, `"none"` and `""` are synonyms); `init` is one shorthand
+ of the initial style.
+
+ This function returns an list suitable for initializing a
+ FormLayout combobox, namely `[initial_name, (shorthand,
+ style_name), (shorthand, style_name), ...]`.
+ """
+ # Drop duplicate shorthands from dict (by overwriting them during
+ # the dict comprehension).
+ name2short = {name: short for short, name in d.items()}
+ # Convert back to {shorthand: name}.
+ short2name = {short: name for name, short in name2short.items()}
+ # Find the kept shorthand for the style specified by init.
+ canonical_init = name2short[d[init]]
+ # Sort by representation and prepend the initial value.
+ return ([canonical_init] +
+ sorted(short2name.items(),
+ key=lambda short_and_name: short_and_name[1]))
+
+ curvelabels = sorted(linedict, key=cmp_key)
+ for label in curvelabels:
+ line = linedict[label]
+ color = mcolors.to_hex(
+ mcolors.to_rgba(line.get_color(), line.get_alpha()),
+ keep_alpha=True)
+ ec = mcolors.to_hex(line.get_markeredgecolor(), keep_alpha=True)
+ fc = mcolors.to_hex(line.get_markerfacecolor(), keep_alpha=True)
+ curvedata = [
+ ('Label', label),
+ sep,
+ (None, 'Line'),
+ ('Line style', prepare_data(LINESTYLES, line.get_linestyle())),
+ ('Draw style', prepare_data(DRAWSTYLES, line.get_drawstyle())),
+ ('Width', line.get_linewidth()),
+ ('Color (RGBA)', color),
+ sep,
+ (None, 'Marker'),
+ ('Style', prepare_data(MARKERS, line.get_marker())),
+ ('Size', line.get_markersize()),
+ ('Face color (RGBA)', fc),
+ ('Edge color (RGBA)', ec)]
+ curves.append([curvedata, label, ""])
+ # Is there a curve displayed?
+ has_curve = bool(curves)
+
+ # Get / Images
+ imagedict = {}
+ for image in axes.get_images():
+ label = image.get_label()
+ if label == '_nolegend_':
+ continue
+ imagedict[label] = image
+ imagelabels = sorted(imagedict, key=cmp_key)
+ images = []
+ cmaps = [(cmap, name) for name, cmap in sorted(cm.cmap_d.items())]
+ for label in imagelabels:
+ image = imagedict[label]
+ cmap = image.get_cmap()
+ if cmap not in cm.cmap_d.values():
+ cmaps = [(cmap, cmap.name)] + cmaps
+ low, high = image.get_clim()
+ imagedata = [
+ ('Label', label),
+ ('Colormap', [cmap.name] + cmaps),
+ ('Min. value', low),
+ ('Max. value', high)]
+ images.append([imagedata, label, ""])
+ # Is there an image displayed?
+ has_image = bool(images)
datalist = [(general, "Axes", "")]
- if has_curve:
+ if curves:
datalist.append((curves, "Curves", ""))
+ if images:
+ datalist.append((images, "Images", ""))
def apply_callback(data):
"""This function will be called to apply changes"""
- if has_curve:
- general, curves = data
- else:
- general, = data
+ general = data.pop(0)
+ curves = data.pop(0) if has_curve else []
+ images = data.pop(0) if has_image else []
+ if data:
+ raise ValueError("Unexpected field")
# Set / General
- title, xmin, xmax, xlabel, xscale, ymin, ymax, ylabel, yscale, \
- generate_legend = general
+ (title, xmin, xmax, xlabel, xscale, ymin, ymax, ylabel, yscale,
+ generate_legend) = general
if axes.get_xscale() != xscale:
axes.set_xscale(xscale)
@@ -140,26 +197,33 @@ def apply_callback(data):
axes.xaxis._update_axisinfo()
axes.yaxis._update_axisinfo()
- if has_curve:
- # Set / Curves
- for index, curve in enumerate(curves):
- line = linedict[curvelabels[index]]
- label, linestyle, drawstyle, linewidth, color, \
- marker, markersize, markerfacecolor, markeredgecolor \
- = curve
- line.set_label(label)
- line.set_linestyle(linestyle)
- line.set_drawstyle(drawstyle)
- line.set_linewidth(linewidth)
- line.set_color(color)
- if marker is not 'none':
- line.set_marker(marker)
- line.set_markersize(markersize)
- line.set_markerfacecolor(markerfacecolor)
- line.set_markeredgecolor(markeredgecolor)
+ # Set / Curves
+ for index, curve in enumerate(curves):
+ line = linedict[curvelabels[index]]
+ (label, linestyle, drawstyle, linewidth, color, marker, markersize,
+ markerfacecolor, markeredgecolor) = curve
+ line.set_label(label)
+ line.set_linestyle(linestyle)
+ line.set_drawstyle(drawstyle)
+ line.set_linewidth(linewidth)
+ rgba = mcolors.to_rgba(color)
+ line.set_color(rgba[:3])
+ line.set_alpha(rgba[-1])
+ if marker is not 'none':
+ line.set_marker(marker)
+ line.set_markersize(markersize)
+ line.set_markerfacecolor(markerfacecolor)
+ line.set_markeredgecolor(markeredgecolor)
+
+ # Set / Images
+ for index, image_settings in enumerate(images):
+ image = imagedict[imagelabels[index]]
+ label, cmap, low, high = image_settings
+ image.set_label(label)
+ image.set_cmap(cm.get_cmap(cmap))
+ image.set_clim(*sorted([low, high]))
# re-generate legend, if checkbox is checked
-
if generate_legend:
draggable = None
ncol = 1
diff --git a/lib/matplotlib/backends/qt_editor/formlayout.py b/lib/matplotlib/backends/qt_editor/formlayout.py
index a786a6105342..00a1a03a36ee 100644
--- a/lib/matplotlib/backends/qt_editor/formlayout.py
+++ b/lib/matplotlib/backends/qt_editor/formlayout.py
@@ -32,43 +32,33 @@
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
"""
-from __future__ import (absolute_import, division, print_function,
- unicode_literals)
-
-import six
-from six.moves import xrange
# History:
# 1.0.10: added float validator (disable "Ok" and "Apply" button when not valid)
# 1.0.7: added support for "Apply" button
# 1.0.6: code cleaning
+from __future__ import (absolute_import, division, print_function,
+ unicode_literals)
+
__version__ = '1.0.10'
__license__ = __doc__
DEBUG = False
-import sys
-STDERR = sys.stderr
+import copy
+import datetime
+import warnings
-from matplotlib.colors import is_color_like
-from matplotlib.colors import rgb2hex
-from matplotlib.colors import colorConverter
+import six
+from matplotlib import colors as mcolors
from matplotlib.backends.qt_compat import QtGui, QtWidgets, QtCore
-if not hasattr(QtWidgets, 'QFormLayout'):
- raise ImportError("Warning: formlayout requires PyQt4 >v4.3 or PySide")
-import datetime
BLACKLIST = set(["title", "label"])
-def col2hex(color):
- """Convert matplotlib color to hex before passing to Qt"""
- return rgb2hex(colorConverter.to_rgb(color))
-
-
class ColorButton(QtWidgets.QPushButton):
"""
Color choosing push button
@@ -83,7 +73,9 @@ def __init__(self, parent=None):
self._color = QtGui.QColor()
def choose_color(self):
- color = QtWidgets.QColorDialog.getColor(self._color, self.parentWidget(), '')
+ color = QtWidgets.QColorDialog.getColor(
+ self._color, self.parentWidget(), "",
+ QtWidgets.QColorDialog.ShowAlphaChannel)
if color.isValid():
self.set_color(color)
@@ -101,21 +93,17 @@ def set_color(self, color):
color = QtCore.Property(QtGui.QColor, get_color, set_color)
-def col2hex(color):
- """Convert matplotlib color to hex before passing to Qt"""
- return rgb2hex(colorConverter.to_rgb(color))
def to_qcolor(color):
"""Create a QColor from a matplotlib color"""
qcolor = QtGui.QColor()
- color = str(color)
try:
- color = col2hex(color)
+ rgba = mcolors.to_rgba(color)
except ValueError:
- #print('WARNING: ignoring invalid color %r' % color)
+ warnings.warn('Ignoring invalid color %r' % color)
return qcolor # return invalid QColor
- qcolor.setNamedColor(color) # set using hex color
- return qcolor # return valid QColor
+ qcolor.setRgbF(*rgba)
+ return qcolor
class ColorLayout(QtWidgets.QHBoxLayout):
@@ -123,7 +111,8 @@ class ColorLayout(QtWidgets.QHBoxLayout):
def __init__(self, color, parent=None):
QtWidgets.QHBoxLayout.__init__(self)
assert isinstance(color, QtGui.QColor)
- self.lineedit = QtWidgets.QLineEdit(color.name(), parent)
+ self.lineedit = QtWidgets.QLineEdit(
+ mcolors.to_hex(color.getRgbF(), keep_alpha=True), parent)
self.lineedit.editingFinished.connect(self.update_color)
self.addWidget(self.lineedit)
self.colorbtn = ColorButton(parent)
@@ -137,7 +126,7 @@ def update_color(self):
self.colorbtn.color = qcolor # defaults to black if not qcolor.isValid()
def update_text(self, color):
- self.lineedit.setText(color.name())
+ self.lineedit.setText(mcolors.to_hex(color.getRgbF(), keep_alpha=True))
def text(self):
return self.lineedit.text()
@@ -146,7 +135,7 @@ def text(self):
def font_is_installed(font):
"""Check if font is installed"""
return [fam for fam in QtGui.QFontDatabase().families()
- if six.text_type(fam) == font]
+ if six.text_type(fam) == font]
def tuple_to_qfont(tup):
@@ -154,11 +143,11 @@ def tuple_to_qfont(tup):
Create a QFont from tuple:
(family [string], size [int], italic [bool], bold [bool])
"""
- if not isinstance(tup, tuple) or len(tup) != 4 \
- or not font_is_installed(tup[0]) \
- or not isinstance(tup[1], int) \
- or not isinstance(tup[2], bool) \
- or not isinstance(tup[3], bool):
+ if not (isinstance(tup, tuple) and len(tup) == 4
+ and font_is_installed(tup[0])
+ and isinstance(tup[1], int)
+ and isinstance(tup[2], bool)
+ and isinstance(tup[3], bool)):
return None
font = QtGui.QFont()
family, size, italic, bold = tup
@@ -189,7 +178,7 @@ def __init__(self, value, parent=None):
# Font size
self.size = QtWidgets.QComboBox(parent)
self.size.setEditable(True)
- sizelist = list(xrange(6, 12)) + list(xrange(12, 30, 2)) + [36, 48, 72]
+ sizelist = list(range(6, 12)) + list(range(12, 30, 2)) + [36, 48, 72]
size = font.pointSize()
if size not in sizelist:
sizelist.append(size)
@@ -227,8 +216,7 @@ class FormWidget(QtWidgets.QWidget):
update_buttons = QtCore.Signal()
def __init__(self, data, comment="", parent=None):
QtWidgets.QWidget.__init__(self, parent)
- from copy import deepcopy
- self.data = deepcopy(data)
+ self.data = copy.deepcopy(data)
self.widgets = []
self.formlayout = QtWidgets.QFormLayout(self)
if comment:
@@ -264,7 +252,8 @@ def setup(self):
continue
elif tuple_to_qfont(value) is not None:
field = FontLayout(value, self)
- elif label.lower() not in BLACKLIST and is_color_like(value):
+ elif (label.lower() not in BLACKLIST
+ and mcolors.is_color_like(value)):
field = ColorLayout(to_qcolor(value), self)
elif isinstance(value, six.string_types):
field = QtWidgets.QLineEdit(value, self)
@@ -284,8 +273,9 @@ def setup(self):
elif selindex in keys:
selindex = keys.index(selindex)
elif not isinstance(selindex, int):
- print("Warning: '%s' index is invalid (label: "
- "%s, value: %s)" % (selindex, label, value), file=STDERR)
+ warnings.warn(
+ "index '%s' is invalid (label: %s, value: %s)" %
+ (selindex, label, value))
selindex = 0
field.setCurrentIndex(selindex)
elif isinstance(value, bool):
@@ -326,7 +316,8 @@ def get(self):
continue
elif tuple_to_qfont(value) is not None:
value = field.get_font()
- elif isinstance(value, six.string_types) or is_color_like(value):
+ elif (isinstance(value, six.string_types)
+ or mcolors.is_color_like(value)):
value = six.text_type(field.text())
elif isinstance(value, (list, tuple)):
index = int(field.currentIndex())
@@ -431,8 +422,8 @@ def __init__(self, data, title="", comment="",
self.formwidget.setup()
# Button box
- self.bbox = bbox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok
- | QtWidgets.QDialogButtonBox.Cancel)
+ self.bbox = bbox = QtWidgets.QDialogButtonBox(
+ QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
self.formwidget.update_buttons.connect(self.update_buttons)
if self.apply_callback is not None:
apply_btn = bbox.addButton(QtWidgets.QDialogButtonBox.Apply)
@@ -457,7 +448,8 @@ def update_buttons(self):
for field in self.float_fields:
if not is_edit_valid(field):
valid = False
- for btn_type in (QtWidgets.QDialogButtonBox.Ok, QtWidgets.QDialogButtonBox.Apply):
+ for btn_type in (QtWidgets.QDialogButtonBox.Ok,
+ QtWidgets.QDialogButtonBox.Apply):
btn = self.bbox.button(btn_type)
if btn is not None:
btn.setEnabled(valid)
diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py
index 0db136682d9f..eca3fa1fe49b 100644
--- a/lib/matplotlib/cbook.py
+++ b/lib/matplotlib/cbook.py
@@ -1505,6 +1505,13 @@ def issubclass_safe(x, klass):
def safe_masked_invalid(x, copy=False):
x = np.array(x, subok=True, copy=copy)
+ if not x.dtype.isnative:
+ # Note that the argument to `byteswap` is 'inplace',
+ # thus if we have already made a copy, do the byteswap in
+ # place, else make a copy with the byte order swapped.
+ # Be explicit that we are swapping the byte order of the dtype
+ x = x.byteswap(copy).newbyteorder('S')
+
try:
xm = np.ma.masked_invalid(x, copy=False)
xm.shrink_mask()
diff --git a/lib/matplotlib/tests/baseline_images/test_image/imshow_endianess.png b/lib/matplotlib/tests/baseline_images/test_image/imshow_endianess.png
new file mode 100644
index 000000000000..148acf125174
Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_image/imshow_endianess.png differ
diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py
index 1b0022d63e3b..f4e45ae7fd36 100644
--- a/lib/matplotlib/tests/test_axes.py
+++ b/lib/matplotlib/tests/test_axes.py
@@ -4553,6 +4553,7 @@ def test_large_offset():
fig.canvas.draw()
+@cleanup
def test_bar_color_cycle():
ccov = mcolors.colorConverter.to_rgb
fig, ax = plt.subplots()
diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py
index 08d55155d5fa..f2d5fb019010 100644
--- a/lib/matplotlib/tests/test_image.py
+++ b/lib/matplotlib/tests/test_image.py
@@ -715,6 +715,21 @@ def test_mask_image():
ax2.imshow(A, interpolation='nearest')
-if __name__=='__main__':
- import nose
- nose.runmodule(argv=['-s','--with-doctest'], exit=False)
+@image_comparison(baseline_images=['imshow_endianess'],
+ remove_text=True, extensions=['png'])
+def test_imshow_endianess():
+ x = np.arange(10)
+ X, Y = np.meshgrid(x, x)
+ Z = ((X-5)**2 + (Y-5)**2)**0.5
+
+ fig, (ax1, ax2) = plt.subplots(1, 2)
+
+ kwargs = dict(origin="lower", interpolation='nearest',
+ cmap='viridis')
+
+ ax1.imshow(Z.astype('f8'), **kwargs)
+
+
+if __name__ == '__main__':
+ nose.runmodule(argv=['-s', '--with-doctest'], exit=False)