From e077c13bafd0d139dd2816acdad86b10c38b7a48 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 1 Jul 2016 23:36:51 -0400 Subject: [PATCH 01/12] FIX: handle non-native endian images In cbook.safe_mask_invalid also ensure that the data is in native byte order. close #6671 closes #6394 --- lib/matplotlib/cbook.py | 7 ++++++ .../test_image/imshow_endianess.png | Bin 0 -> 5320 bytes lib/matplotlib/tests/test_image.py | 21 +++++++++++++++--- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_image/imshow_endianess.png diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 8fef27ac0165..ab0cc8edec27 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 0000000000000000000000000000000000000000..148acf1251743f8b2f5a1d6916854c31f0bcc76c GIT binary patch literal 5320 zcmeHLSyU5Q0xgghTp&75tF)RpdPeN=ptPtUTU?NcG+|^6s0cwt5NL=_!kP})FgC5C zk-?2!$uxnWNehG}1Tfkv3Q0hguq6>$0whI*kOUHwOa;1s`pi#{%p5Y!B)?MdZ+Mk529WgYU?Ls|R41?CR_5 z?$b(^b+-pu6KCw361zc@y{DOeh1U5qE;4(D;Nl=kEFK^Y*Rr!tN8s`Ja_GNIi+bIl z!fvmb1putrybk~_tu-+PfS)Z^0Ki6%OVG2gOw9n`hm{)vU|%|50sv zFr2B;4>J3=eZY>_+fD5zOKrCCmnUNLRmD!^n-~fUxfYC4_FtB_a}>#$Sw~9JUwoDGhbfBtMd$6KI;?lGS`P3pcuRWi?!CJ$*?laloNR5LTuHoCYrL^q4 zfOVs+qud?K4MDU-wq&aNvUYQz1C5^K`UiPSwg6(c@JO?NCXRs{MCc^TY2#fcwQ5tU*ows78l@YD0fAd9#x0tiOhz zn3||0+r?_pdEaO)&UPrZrP=9{iY8V)s+46&k0tR5Iz0`lP2T*!h~T}TUi0AEIKP6@ z+eAgi31qcWnz5J6sw6l%$vNistqD3ZwNOzcC{xgAdo($%32H0Z6<%#6%h_p1__;r8 z4P~~f^W`!lTF!!h$yzT?E}1`tA7zq&N}@EtKO)x9iQryixCCJJV`scms9lgIKlZ|@mdCBEFUXm`}HSSD3couN(dpA13jMq%x5%|&Tv3DC( zpB^t>)lK~ zG5eb%Nq}12IXqV1_>dz^3gJ;S2%kd|ZheDwo`V4y2@wTm-kN~HM4E>^TUE{<)tpjU zKcaoHzPWEe#|e2zB37K3oz_O8r1{KW0Qi~#F8WDG#u|dad?_vgY;4-mo^hAVBy0+{ zVJUQo4q+1xmk>1|itwVoD*JWZAzjhDE5(*jZy<%LuP$wf7MEl)MkT3aaIMNT7;e(1WD*!X-y5Y z@nOy~b4VCtqGI9*qPpxW5HWJD@^XTj46aW-lI@*lJ13%3{V9;omoH!2a1ei6cpmaL2_OO2WkLFliGu@S9QhC+G^o=n>mM7Hf#9bXEsAdEaqey6f~m z)gz8eZrZeTGY||3y19z9JUA`Dg~VR%gAh;lyzWp-Cj}zt^}VXCE&uQoQ3eLL9sjsx z%$(w`vUeEk5@j_$gsmCggzuRWH6I{|-UcI`a08ivMzL+HMYu~qh&w!DQoo;(9bU@} z>#`K(Gvu+qhxpch+O{&c0-STuc1B|OmffKEZ>dS+!SZN=D0w_Eou>_s!4gD0FX%@5 z)K8_3u&H6=?h@ULK3`*JaiC7#m|Kc3RXSvNf{=_rWZ@g7l7*i(n~&Ax+%O<@kuCtu3XCu^k~U)WA-q>&XsX zScQ~W1f@`@b}P;7Fp52+7V4kRJd=NT2yRXna^*phzb|zf5qP_1aq4~yMmN~g(;`S> zmmNjM6JtiCbSvc8W8v{YYfUx>RI(;`?*$l)Elgt!(&TX%6!SA{BbBYUVc*wBlk5#@ zi@K8QzbpodRDH($Exfk@JVDHr5RyWt)$*+(Eh7!5@lksVx+o2O#r$HUf8'), **kwargs) + + +if __name__ == '__main__': + nose.runmodule(argv=['-s', '--with-doctest'], exit=False) From 3c2ea5a606725428e6b54cb73dad5725ea9e3724 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 9 Jul 2016 13:19:58 -0400 Subject: [PATCH 02/12] TST: add missing decorator This maybe the source of flaky tests. --- lib/matplotlib/tests/test_axes.py | 1 + 1 file changed, 1 insertion(+) 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() From f7aa7b36e7ff84e30677e8da9fa48e898e7d8419 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 9 Jul 2016 14:43:33 -0400 Subject: [PATCH 03/12] Merge pull request #6381 from anntzer/figure-options-canonical-none-linestyle Fix canonical name for "None" linestyle in qtconfig gui --- lib/matplotlib/backends/qt_editor/figureoptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index 1b220173c5f6..75abf9ddeeda 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -29,7 +29,7 @@ def get_icon(name): '--': 'Dashed', '-.': 'DashDot', ':': 'Dotted', - 'none': 'None', + 'None': 'None', } DRAWSTYLES = {'default': 'Default', From 44ce103457bd4df125bdab905144e0afc943c11d Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 28 Oct 2015 16:05:38 -0700 Subject: [PATCH 04/12] Sort and uniquify style entries in figure options. Fixes the first two points of #5341. --- .../backends/qt_editor/figureoptions.py | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index 75abf9ddeeda..d434bb43ecf8 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -78,29 +78,38 @@ def figure_edit(axes, parent=None): continue linedict[label] = line curves = [] - linestyles = list(six.iteritems(LINESTYLES)) - drawstyles = list(six.iteritems(DRAWSTYLES)) - markers = list(six.iteritems(MARKERS)) + + def prepare_data(d, init): + """Prepare entry for FormLayout. + """ + # List items in dict, dropping duplicate values, sorting by values. + kvs = [(k, v) for v, k in + sorted({v: k for k, v in d.items()}.items())] + # Find the unique kept key with the same value as the init value. + canonical_init, = ({k for k, v in d.items() if v == d[init]}. + intersection(k for k, v in kvs)) + return [canonical_init] + kvs + 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), - ] + 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', color), + sep, + (None, 'Marker'), + ('Style', prepare_data(MARKERS, line.get_marker())), + ('Size', line.get_markersize()), + ('Facecolor', fc), + ('Edgecolor', ec)] curves.append([curvedata, label, ""]) # make sure that there is at least one displayed curve From 68b42d971f47ca28b8233f111491a141c5260d84 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 12 Nov 2015 09:35:47 -0800 Subject: [PATCH 05/12] Clarify the implementation of prepare_data. --- .../backends/qt_editor/figureoptions.py | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index d434bb43ecf8..c72c48852150 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -81,14 +81,27 @@ def figure_edit(axes, parent=None): 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), ...]`. """ - # List items in dict, dropping duplicate values, sorting by values. - kvs = [(k, v) for v, k in - sorted({v: k for k, v in d.items()}.items())] - # Find the unique kept key with the same value as the init value. - canonical_init, = ({k for k, v in d.items() if v == d[init]}. - intersection(k for k, v in kvs)) - return [canonical_init] + kvs + # 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.keys()) for label in curvelabels: From 240d24b15e29e1ec4f9689e51725fb01d4ba124d Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 12 Nov 2015 01:24:12 -0800 Subject: [PATCH 06/12] Set image cmap from figure options. This patch allows the user to interactively set an image's colormap from the axes and lines (and now images) parameters editor (in the Qt backend). Colormaps are listed from those registered with matplotlib.cm. Colorbars seem to be handled properly. --- lib/matplotlib/axes/_base.py | 2 + .../backends/qt_editor/figureoptions.py | 198 ++++++++++-------- 2 files changed, 114 insertions(+), 86 deletions(-) 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 c72c48852150..37668f4f961a 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -16,7 +16,7 @@ import matplotlib.backends.qt_editor.formlayout as formlayout from matplotlib.backends.qt_compat import QtGui -from matplotlib import markers +from matplotlib import cm, markers from matplotlib.colors import colorConverter, rgb2hex @@ -43,8 +43,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,79 +67,103 @@ 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 = [] - - 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.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', prepare_data(LINESTYLES, line.get_linestyle())), - ('Draw Style', prepare_data(DRAWSTYLES, line.get_drawstyle())), - ('Width', line.get_linewidth()), - ('Color', color), - sep, - (None, 'Marker'), - ('Style', prepare_data(MARKERS, line.get_marker())), - ('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) + # 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.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', prepare_data(LINESTYLES, line.get_linestyle())), + ('Draw Style', prepare_data(DRAWSTYLES, line.get_drawstyle())), + ('Width', line.get_linewidth()), + ('Color', color), + sep, + (None, 'Marker'), + ('Style', prepare_data(MARKERS, line.get_marker())), + ('Size', line.get_markersize()), + ('Facecolor', fc), + ('Edgecolor', 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) + 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: + cmaps = [(cmap, cmap.name)] + cmaps + imagedata = [ + ('Label', label), + ('Colormap', [cmap.name] + cmaps) + ] + 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) @@ -162,26 +184,30 @@ 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) + 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 / Images + for index, image_settings in enumerate(images): + image = imagedict[imagelabels[index]] + label, cmap = image_settings + image.set_label(label) + image.set_cmap(cm.get_cmap(cmap)) # re-generate legend, if checkbox is checked - if generate_legend: draggable = None ncol = 1 From cf4b2196a06742c2d1cdea756b5f11274b7bd62c Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 28 Apr 2016 10:57:45 -0700 Subject: [PATCH 07/12] Avoid duplicate cmap in image options. The previous implementation was incorrect and would yield a double entry in the cmap combobox. --- lib/matplotlib/backends/qt_editor/figureoptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index 37668f4f961a..b83e6a9b394c 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -137,7 +137,7 @@ def prepare_data(d, init): for label in imagelabels: image = imagedict[label] cmap = image.get_cmap() - if cmap not in cm.cmap_d: + if cmap not in cm.cmap_d.values(): cmaps = [(cmap, cmap.name)] + cmaps imagedata = [ ('Label', label), From 21174b57cb7b8f636a4ec80150030ced8823ed8f Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 28 Apr 2016 11:39:51 -0700 Subject: [PATCH 08/12] Allow setting image clims in Qt options editor. Currently, `(cmin, cmax)` is swapped if `cmin > cmax` because it's not easy to impose the constraint using formlayout. --- lib/matplotlib/backends/qt_editor/figureoptions.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index b83e6a9b394c..344db5d9abb3 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -139,10 +139,12 @@ def prepare_data(d, init): 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) - ] + ('Colormap', [cmap.name] + cmaps), + ('Min. value', low), + ('Max. value', high)] images.append([imagedata, label, ""]) # Is there an image displayed? has_image = bool(images) @@ -203,9 +205,10 @@ def apply_callback(data): # Set / Images for index, image_settings in enumerate(images): image = imagedict[imagelabels[index]] - label, cmap = image_settings + 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: From 6d9b4a5ae7685ac244f799f9391c0403227d63e9 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 4 May 2016 17:37:46 -0700 Subject: [PATCH 09/12] Sort default labels numerically in Qt editor. --- lib/matplotlib/backends/qt_editor/figureoptions.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index 344db5d9abb3..fb146418fd5f 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -13,6 +13,7 @@ import six import os.path as osp +import re import matplotlib.backends.qt_editor.formlayout as formlayout from matplotlib.backends.qt_compat import QtGui @@ -67,6 +68,14 @@ def figure_edit(axes, parent=None): xunits = axes.xaxis.get_units() yunits = axes.yaxis.get_units() + # 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(): @@ -100,7 +109,7 @@ def prepare_data(d, init): sorted(short2name.items(), key=lambda short_and_name: short_and_name[1])) - curvelabels = sorted(linedict.keys()) + curvelabels = sorted(linedict, key=cmp_key) for label in curvelabels: line = linedict[label] color = rgb2hex(colorConverter.to_rgb(line.get_color())) @@ -131,7 +140,7 @@ def prepare_data(d, init): if label == '_nolegend_': continue imagedict[label] = image - imagelabels = sorted(imagedict) + imagelabels = sorted(imagedict, key=cmp_key) images = [] cmaps = [(cmap, name) for name, cmap in sorted(cm.cmap_d.items())] for label in imagelabels: From 5d179baf3f454a152687a86188af84fc332dd16c Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 5 May 2016 11:23:19 -0700 Subject: [PATCH 10/12] DOC: add whats_new for qt configuration editor. --- doc/users/whats_new/2016-04_qt_config-AL.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 doc/users/whats_new/2016-04_qt_config-AL.rst 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. From 46dd6891ae326256dfee10267beeaa9240ecf911 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 4 May 2016 17:52:34 -0700 Subject: [PATCH 11/12] Minor cleanups for FormLayout. --- .../backends/qt_editor/formlayout.py | 62 +++++++++---------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/lib/matplotlib/backends/qt_editor/formlayout.py b/lib/matplotlib/backends/qt_editor/formlayout.py index a786a6105342..34395a5f1e33 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 six -from matplotlib.colors import is_color_like -from matplotlib.colors import rgb2hex -from matplotlib.colors import colorConverter +import copy +import datetime +import warnings +from matplotlib.colors import colorConverter, is_color_like, rgb2hex 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,8 @@ 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(), '') if color.isValid(): self.set_color(color) @@ -101,10 +92,12 @@ 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() @@ -112,7 +105,7 @@ def to_qcolor(color): try: color = col2hex(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 @@ -146,7 +139,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 +147,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 +182,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 +220,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: @@ -284,8 +276,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): @@ -431,8 +424,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 +450,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) From c077c565dade84378196da5cfb5d0a050164f9dc Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 4 May 2016 19:11:29 -0700 Subject: [PATCH 12/12] Qt editor alpha handling. --- .../backends/qt_editor/figureoptions.py | 28 +++++++++------- .../backends/qt_editor/formlayout.py | 32 +++++++++---------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index fb146418fd5f..038073edd6a1 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -15,17 +15,17 @@ 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 cm, 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', @@ -112,23 +112,25 @@ def prepare_data(d, init): curvelabels = sorted(linedict, key=cmp_key) 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())) + 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())), + ('Line style', prepare_data(LINESTYLES, line.get_linestyle())), + ('Draw style', prepare_data(DRAWSTYLES, line.get_drawstyle())), ('Width', line.get_linewidth()), - ('Color', color), + ('Color (RGBA)', color), sep, (None, 'Marker'), ('Style', prepare_data(MARKERS, line.get_marker())), ('Size', line.get_markersize()), - ('Facecolor', fc), - ('Edgecolor', ec)] + ('Face color (RGBA)', fc), + ('Edge color (RGBA)', ec)] curves.append([curvedata, label, ""]) # Is there a curve displayed? has_curve = bool(curves) @@ -204,7 +206,9 @@ def apply_callback(data): line.set_linestyle(linestyle) line.set_drawstyle(drawstyle) line.set_linewidth(linewidth) - line.set_color(color) + 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) diff --git a/lib/matplotlib/backends/qt_editor/formlayout.py b/lib/matplotlib/backends/qt_editor/formlayout.py index 34395a5f1e33..00a1a03a36ee 100644 --- a/lib/matplotlib/backends/qt_editor/formlayout.py +++ b/lib/matplotlib/backends/qt_editor/formlayout.py @@ -46,13 +46,13 @@ DEBUG = False -import six - import copy import datetime import warnings -from matplotlib.colors import colorConverter, is_color_like, rgb2hex +import six + +from matplotlib import colors as mcolors from matplotlib.backends.qt_compat import QtGui, QtWidgets, QtCore @@ -74,7 +74,8 @@ def __init__(self, parent=None): def choose_color(self): color = QtWidgets.QColorDialog.getColor( - self._color, self.parentWidget(), '') + self._color, self.parentWidget(), "", + QtWidgets.QColorDialog.ShowAlphaChannel) if color.isValid(): self.set_color(color) @@ -93,22 +94,16 @@ 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: 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): @@ -116,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) @@ -130,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() @@ -256,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) @@ -319,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())