From 2eb3a3a7d228f757d84826cec448fc10f8084508 Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Mon, 23 Jul 2018 15:23:19 +0200 Subject: [PATCH 01/11] Basic matplotlib output for scatterplots --- Orange/widgets/io.py | 30 ++++++ Orange/widgets/tests/test_io.py | 39 +++++++- Orange/widgets/utils/matplotlib_export.py | 108 ++++++++++++++++++++++ requirements-gui.txt | 1 + 4 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 Orange/widgets/utils/matplotlib_export.py diff --git a/Orange/widgets/io.py b/Orange/widgets/io.py index 3c0231fff58..df111237123 100644 --- a/Orange/widgets/io.py +++ b/Orange/widgets/io.py @@ -9,6 +9,7 @@ ) from Orange.data.io import FileFormat +from Orange.widgets.utils.matplotlib_export import scene_code # Importing WebviewWidget can fail if neither QWebKit (old, deprecated) nor # QWebEngine (bleeding-edge, hard to install) are available @@ -181,6 +182,35 @@ def write_image(cls, filename, scene): super().write_image(filename, scene) +class MatplotlibFormat(FileFormat): + EXTENSIONS = ('.py',) + DESCRIPTION = 'Python Code (with Matplotlib)' + PRIORITY = 300 + + @classmethod + def write_image(cls, filename, scene): + code = scene_code(scene) + "\n\nplt.show()" + with open(filename, "wt") as f: + f.write(code) + + @classmethod + def write(cls, filename, scene): + if type(scene) == dict: + scene = scene['scene'] + cls.write_image(filename, scene) + + +class MatplotlibPDFFormat(MatplotlibFormat): + EXTENSIONS = ('.matplotlib.pdf',) # file formats with same extension are not supported + DESCRIPTION = 'Portable Document Format (from Matplotlib)' + PRIORITY = 200 + + @classmethod + def write_image(cls, filename, scene): + code = scene_code(scene) + "\n\nplt.savefig({})".format(repr(filename)) + exec(code, {}) # will generate a pdf + + if hasattr(QtGui, "QPdfWriter"): class PdfFormat(ImgFormat): EXTENSIONS = ('.pdf', ) diff --git a/Orange/widgets/tests/test_io.py b/Orange/widgets/tests/test_io.py index d21498f5d17..10a42bd6c74 100644 --- a/Orange/widgets/tests/test_io.py +++ b/Orange/widgets/tests/test_io.py @@ -1,11 +1,18 @@ import os import tempfile import unittest +from unittest.mock import patch from AnyQt.QtWidgets import QGraphicsScene, QGraphicsRectItem -from Orange.widgets.tests.base import GuiTest + +import Orange +from Orange.tests import named_file +from Orange.widgets.tests.base import GuiTest, WidgetTest from Orange.widgets import io as imgio +from Orange.widgets.io import MatplotlibFormat, MatplotlibPDFFormat +from Orange.widgets.visualize.owscatterplot import OWScatterPlot + @unittest.skipUnless(hasattr(imgio, "PdfFormat"), "QPdfWriter not available") class TestIO(GuiTest): @@ -18,3 +25,33 @@ def test_pdf(self): imgio.PdfFormat.write_image(fname, sc) finally: os.unlink(fname) + + +class TestMatplotlib(WidgetTest): + + def test_python(self): + iris = Orange.data.Table("iris") + self.widget = self.create_widget(OWScatterPlot) + self.send_signal(OWScatterPlot.Inputs.data, iris[::10]) + with named_file("", suffix=".py") as fname: + with patch("Orange.widgets.utils.filedialogs.open_filename_dialog_save", + lambda *x: (fname, MatplotlibFormat, None)): + self.widget.save_graph() + with open(fname, "rt") as f: + code = f.read() + self.assertIn("plt.show()", code) + self.assertIn("plt.scatter", code) + # test if the runs + exec(code.replace("plt.show()", ""), {}) + + def test_pdf(self): + iris = Orange.data.Table("iris") + self.widget = self.create_widget(OWScatterPlot) + self.send_signal(OWScatterPlot.Inputs.data, iris[::10]) + with named_file("", suffix=".pdf") as fname: + with patch("Orange.widgets.utils.filedialogs.open_filename_dialog_save", + lambda *x: (fname, MatplotlibPDFFormat, None)): + self.widget.save_graph() + with open(fname, "rb") as f: + code = f.read() + self.assertTrue(code.startswith(b"%PDF")) diff --git a/Orange/widgets/utils/matplotlib_export.py b/Orange/widgets/utils/matplotlib_export.py new file mode 100644 index 00000000000..d6deb16f8f8 --- /dev/null +++ b/Orange/widgets/utils/matplotlib_export.py @@ -0,0 +1,108 @@ +from itertools import chain + +import numpy as np + +from pyqtgraph.graphicsItems.ScatterPlotItem import ScatterPlotItem + + +def numpy_repr(a): + """ A numpy repr without summarization """ + opts = np.get_printoptions() + try: + np.set_printoptions(threshold=10**10) + return repr(a) + finally: + np.set_printoptions(**opts) + + +def scatterplot_code(scatterplot_item): + x = scatterplot_item.data['x'] + y = scatterplot_item.data['y'] + sizes = scatterplot_item.data["size"] + + code = [] + + code.append("# data") + code.append("x = {}".format(numpy_repr(x))) + code.append("y = {}".format(numpy_repr(y))) + + code.append("# style") + code.append("sizes = {}".format(numpy_repr(sizes))) + + def colortuple(color): + return color.redF(), color.greenF(), color.blueF(), color.alphaF() + + edgecolors = np.array([colortuple(a.color()) for a in scatterplot_item.data["pen"]]) + facecolors = np.array([colortuple(a.color()) for a in scatterplot_item.data["brush"]]) + + code.append("edgecolors = {}".format(numpy_repr(edgecolors))) + code.append("facecolors = {}".format(numpy_repr(facecolors))) + + # possible_markers for scatterplot are in .graph.CurveSymbols + def matplotlib_marker(m): + if m == "t": + return "^" + elif m == "t2": + return ">" + elif m == "t3": + return "<" + elif m == "star": + return "*" + elif m == "+": + return "P" + elif m == "x": + return "X" + return m + + # TODO labels are missing + + # each marker requires one call to matplotlib's scatter! + markers = np.array([matplotlib_marker(m) for m in scatterplot_item.data["symbol"]]) + for m in set(markers): + indices = np.where(markers == m)[0] + if np.all(indices == np.arange(x.shape[0])): + # indices are unused + code.append("plt.scatter(x=x, y=y, s=sizes**2/4, marker={},".format(repr(m))) + code.append(" facecolors=facecolors, edgecolors=edgecolors)") + else: + code.append("indices = {}".format(numpy_repr(indices))) + code.append("plt.scatter(x=x[indices], y=y[indices], s=sizes[indices]**2/4, " + "marker={},".format(repr(m))) + code.append(" facecolors=facecolors[indices], " + "edgecolors=edgecolors[indices])") + + return "\n".join(code) + + +def scene_code(scene): + + code = [] + + code.append("import matplotlib.pyplot as plt") + code.append("from numpy import array") + + code.append("") + code.append("plt.clf()") + + code.append("") + + for item in scene.items: + if isinstance(item, ScatterPlotItem): + code.append(scatterplot_code(item)) + + # TODO currently does not work for graphs without axes and for multiple axes! + for position, set_ticks, set_label in [("bottom", "plt.xticks", "plt.xlabel"), + ("left", "plt.yticks", "plt.ylabel")]: + axis = scene.getAxis(position) + code.append("{}({})".format(set_label, repr(str(axis.labelText)))) + + # textual tick labels + if axis._tickLevels is not None: + major_minor = list(chain(*axis._tickLevels)) + locs = [a[0] for a in major_minor] + labels = [a[1] for a in major_minor] + code.append("{}({}, {})".format(set_ticks, locs, repr(labels))) + + return "\n".join(code) + + diff --git a/requirements-gui.txt b/requirements-gui.txt index 3c2bc76f1bc..29a2386a39c 100644 --- a/requirements-gui.txt +++ b/requirements-gui.txt @@ -2,6 +2,7 @@ AnyQt>=0.0.8 pyqtgraph>=0.10.0 +matplotlib>=2.0.0 # For add-ons' descriptions docutils From b5ef61f4f357d2e91b6d16ea049ec345ae7d6afb Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Mon, 23 Jul 2018 16:48:47 +0200 Subject: [PATCH 02/11] Matplotlib export: use line widths This fixes the selection output. --- Orange/widgets/utils/matplotlib_export.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Orange/widgets/utils/matplotlib_export.py b/Orange/widgets/utils/matplotlib_export.py index d6deb16f8f8..a078d4c2591 100644 --- a/Orange/widgets/utils/matplotlib_export.py +++ b/Orange/widgets/utils/matplotlib_export.py @@ -3,6 +3,7 @@ import numpy as np from pyqtgraph.graphicsItems.ScatterPlotItem import ScatterPlotItem +from AnyQt.QtCore import Qt def numpy_repr(a): @@ -34,9 +35,15 @@ def colortuple(color): edgecolors = np.array([colortuple(a.color()) for a in scatterplot_item.data["pen"]]) facecolors = np.array([colortuple(a.color()) for a in scatterplot_item.data["brush"]]) + linewidths = np.array([a.widthF() for a in scatterplot_item.data["pen"]]) + + pen_style = [a.style() for a in scatterplot_item.data["pen"]] + no_pen = [s == Qt.NoPen for s in pen_style] + linewidths[np.nonzero(no_pen)[0]] = 0 code.append("edgecolors = {}".format(numpy_repr(edgecolors))) code.append("facecolors = {}".format(numpy_repr(facecolors))) + code.append("linewidths = {}".format(numpy_repr(linewidths))) # possible_markers for scatterplot are in .graph.CurveSymbols def matplotlib_marker(m): @@ -63,13 +70,15 @@ def matplotlib_marker(m): if np.all(indices == np.arange(x.shape[0])): # indices are unused code.append("plt.scatter(x=x, y=y, s=sizes**2/4, marker={},".format(repr(m))) - code.append(" facecolors=facecolors, edgecolors=edgecolors)") + code.append(" facecolors=facecolors, edgecolors=edgecolors,") + code.append(" linewidths=linewidths)") else: code.append("indices = {}".format(numpy_repr(indices))) code.append("plt.scatter(x=x[indices], y=y[indices], s=sizes[indices]**2/4, " "marker={},".format(repr(m))) code.append(" facecolors=facecolors[indices], " - "edgecolors=edgecolors[indices])") + "edgecolors=edgecolors[indices],") + code.append(" linewidths=linewidths[indices])") return "\n".join(code) From 90c90f3f9676823f845c5e79004007fac89cde67 Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Mon, 23 Jul 2018 17:07:40 +0200 Subject: [PATCH 03/11] Matplotlib export: exit early if scatterplot is transparent --- Orange/widgets/utils/matplotlib_export.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Orange/widgets/utils/matplotlib_export.py b/Orange/widgets/utils/matplotlib_export.py index a078d4c2591..351c6f6e734 100644 --- a/Orange/widgets/utils/matplotlib_export.py +++ b/Orange/widgets/utils/matplotlib_export.py @@ -39,7 +39,15 @@ def colortuple(color): pen_style = [a.style() for a in scatterplot_item.data["pen"]] no_pen = [s == Qt.NoPen for s in pen_style] - linewidths[np.nonzero(no_pen)[0]] = 0 + edgecolors[:, 3][np.nonzero(no_pen)[0]] = 0 # set alpha channel to zero + + brush_style = [a.style() for a in scatterplot_item.data["brush"]] + no_brush = [s == Qt.NoBrush for s in brush_style] + facecolors[:, 3][np.nonzero(no_brush)[0]] = 0 # set alpha channel to zero + + # return early if the scatterplot is all transparent + if not any(edgecolors[:, 3] > 0) and not any(facecolors[:, 3] > 0): + return "" code.append("edgecolors = {}".format(numpy_repr(edgecolors))) code.append("facecolors = {}".format(numpy_repr(facecolors))) From 842d3b61a9cb0dcd84e73365d252414aea64af73 Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Tue, 24 Jul 2018 16:33:24 +0200 Subject: [PATCH 04/11] Matplotlib export: compress array into single elements if possible --- Orange/widgets/utils/matplotlib_export.py | 54 ++++++++++++++++++----- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/Orange/widgets/utils/matplotlib_export.py b/Orange/widgets/utils/matplotlib_export.py index 351c6f6e734..fa25d8408f3 100644 --- a/Orange/widgets/utils/matplotlib_export.py +++ b/Orange/widgets/utils/matplotlib_export.py @@ -16,6 +16,29 @@ def numpy_repr(a): np.set_printoptions(**opts) +def compress_if_all_same(l): + s = set(l) + return s.pop() if len(s) == 1 else l + + +def is_sequence_not_string(a): + if isinstance(a, str): + return False + try: + iter(a) + return True + except TypeError: + pass + return False + + +def code_with_indices(data, data_name, indices, indices_name): + if is_sequence_not_string(data) and indices is not None: + return data_name + "[" + indices_name + "]" + else: + return data_name + + def scatterplot_code(scatterplot_item): x = scatterplot_item.data['x'] y = scatterplot_item.data['y'] @@ -28,6 +51,7 @@ def scatterplot_code(scatterplot_item): code.append("y = {}".format(numpy_repr(y))) code.append("# style") + sizes = compress_if_all_same(sizes) code.append("sizes = {}".format(numpy_repr(sizes))) def colortuple(color): @@ -51,6 +75,7 @@ def colortuple(color): code.append("edgecolors = {}".format(numpy_repr(edgecolors))) code.append("facecolors = {}".format(numpy_repr(facecolors))) + linewidths = compress_if_all_same(linewidths) code.append("linewidths = {}".format(numpy_repr(linewidths))) # possible_markers for scatterplot are in .graph.CurveSymbols @@ -76,17 +101,24 @@ def matplotlib_marker(m): for m in set(markers): indices = np.where(markers == m)[0] if np.all(indices == np.arange(x.shape[0])): - # indices are unused - code.append("plt.scatter(x=x, y=y, s=sizes**2/4, marker={},".format(repr(m))) - code.append(" facecolors=facecolors, edgecolors=edgecolors,") - code.append(" linewidths=linewidths)") - else: + indices = None + if indices is not None: code.append("indices = {}".format(numpy_repr(indices))) - code.append("plt.scatter(x=x[indices], y=y[indices], s=sizes[indices]**2/4, " - "marker={},".format(repr(m))) - code.append(" facecolors=facecolors[indices], " - "edgecolors=edgecolors[indices],") - code.append(" linewidths=linewidths[indices])") + + def indexed(data, data_name, indices=indices): + return code_with_indices(data, data_name, indices, "indices") + + code.append("plt.scatter(x={}, y={}, s={}**2/4, marker={},\n" + " facecolors={}, edgecolors={},\n" + " linewidths={})" + .format(indexed(x, "x"), + indexed(y, "y"), + indexed(sizes, "sizes"), + repr(m), + indexed(facecolors, "facecolors"), + indexed(edgecolors, "edgecolors"), + indexed(linewidths, "linewidths") + )) return "\n".join(code) @@ -121,5 +153,3 @@ def scene_code(scene): code.append("{}({}, {})".format(set_ticks, locs, repr(labels))) return "\n".join(code) - - From 6d26744ad0171994980198c72f929668232430be Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Wed, 25 Jul 2018 14:27:42 +0200 Subject: [PATCH 05/11] Matplotlib export: test skipping empty graph --- .../widgets/tests/test_matplotlib_export.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Orange/widgets/tests/test_matplotlib_export.py diff --git a/Orange/widgets/tests/test_matplotlib_export.py b/Orange/widgets/tests/test_matplotlib_export.py new file mode 100644 index 00000000000..420d3387142 --- /dev/null +++ b/Orange/widgets/tests/test_matplotlib_export.py @@ -0,0 +1,26 @@ +import pyqtgraph as pg + +from AnyQt.QtCore import QRectF + +import Orange + +from Orange.widgets.tests.base import WidgetTest +from Orange.widgets.utils.matplotlib_export import scatterplot_code +from Orange.widgets.visualize.owscatterplot import OWScatterPlot + + +class TestScatterPlot(WidgetTest): + + def test_owscatterplot_ignore_empty(self): + iris = Orange.data.Table("iris") + self.widget = self.create_widget(OWScatterPlot) + self.send_signal(OWScatterPlot.Inputs.data, iris[::10]) + code = scatterplot_code(self.widget.graph.scatterplot_item) + self.assertIn("plt.scatter", code) + # tbe selected graph has to generate nothing + code = scatterplot_code(self.widget.graph.scatterplot_item_sel) + self.assertEqual(code, "") + # for a selection the selected graph has to be non-empty + self.widget.graph.select_by_rectangle(QRectF(4, 3, 3, 1)) + code = scatterplot_code(self.widget.graph.scatterplot_item_sel) + self.assertIn("plt.scatter", code) From ececcc6f6d27a2e86497abde47b17a99cf898e43 Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Wed, 25 Jul 2018 17:27:52 +0200 Subject: [PATCH 06/11] Matplotlib export: support undefined colors and sizes --- .../widgets/tests/test_matplotlib_export.py | 17 ++++++ Orange/widgets/utils/matplotlib_export.py | 54 ++++++++++++++----- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/Orange/widgets/tests/test_matplotlib_export.py b/Orange/widgets/tests/test_matplotlib_export.py index 420d3387142..334f7d1ae35 100644 --- a/Orange/widgets/tests/test_matplotlib_export.py +++ b/Orange/widgets/tests/test_matplotlib_export.py @@ -9,6 +9,13 @@ from Orange.widgets.visualize.owscatterplot import OWScatterPlot +def add_intro(a): + r = "import matplotlib.pyplot as plt\n" + \ + "from numpy import array\n" + \ + "plt.clf()" + return r + a + + class TestScatterPlot(WidgetTest): def test_owscatterplot_ignore_empty(self): @@ -24,3 +31,13 @@ def test_owscatterplot_ignore_empty(self): self.widget.graph.select_by_rectangle(QRectF(4, 3, 3, 1)) code = scatterplot_code(self.widget.graph.scatterplot_item_sel) self.assertIn("plt.scatter", code) + exec(add_intro(code), {}) + + def test_scatterplot_simple(self): + plotWidget = pg.PlotWidget(background="w") + scatterplot = pg.ScatterPlotItem() + scatterplot.setData(x=[1, 2, 3], y=[3, 2, 1]) + plotWidget.addItem(scatterplot) + code = scatterplot_code(scatterplot) + self.assertIn("plt.scatter", code) + exec(add_intro(code), {}) diff --git a/Orange/widgets/utils/matplotlib_export.py b/Orange/widgets/utils/matplotlib_export.py index fa25d8408f3..7d47a588edc 100644 --- a/Orange/widgets/utils/matplotlib_export.py +++ b/Orange/widgets/utils/matplotlib_export.py @@ -4,6 +4,7 @@ from pyqtgraph.graphicsItems.ScatterPlotItem import ScatterPlotItem from AnyQt.QtCore import Qt +from AnyQt.QtGui import QPen, QBrush def numpy_repr(a): @@ -52,27 +53,52 @@ def scatterplot_code(scatterplot_item): code.append("# style") sizes = compress_if_all_same(sizes) + if sizes == -1: + sizes = None code.append("sizes = {}".format(numpy_repr(sizes))) - def colortuple(color): - return color.redF(), color.greenF(), color.blueF(), color.alphaF() + def colortuple(pen): + if isinstance(pen, (QPen, QBrush)): + color = pen.color() + return color.redF(), color.greenF(), color.blueF(), color.alphaF() + return pen - edgecolors = np.array([colortuple(a.color()) for a in scatterplot_item.data["pen"]]) - facecolors = np.array([colortuple(a.color()) for a in scatterplot_item.data["brush"]]) - linewidths = np.array([a.widthF() for a in scatterplot_item.data["pen"]]) + def width(pen): + if isinstance(pen, QPen): + return pen.widthF() + return pen - pen_style = [a.style() for a in scatterplot_item.data["pen"]] - no_pen = [s == Qt.NoPen for s in pen_style] - edgecolors[:, 3][np.nonzero(no_pen)[0]] = 0 # set alpha channel to zero + linewidths = np.array([width(a) for a in scatterplot_item.data["pen"]]) - brush_style = [a.style() for a in scatterplot_item.data["brush"]] - no_brush = [s == Qt.NoBrush for s in brush_style] - facecolors[:, 3][np.nonzero(no_brush)[0]] = 0 # set alpha channel to zero + def shown(a): + if isinstance(a, (QPen, QBrush)): + s = a.style() + if s == Qt.NoPen or s == Qt.NoBrush or a.color().alpha() == 0: + return False + return True + + shown_edge = [shown(a) for a in scatterplot_item.data["pen"]] + shown_brush = [shown(a) for a in scatterplot_item.data["brush"]] # return early if the scatterplot is all transparent - if not any(edgecolors[:, 3] > 0) and not any(facecolors[:, 3] > 0): + if not any(shown_edge) and not any(shown_brush): return "" + def do_colors(data_column, show): + colors = [colortuple(a) for a in data_column] + if all(a is None for a in colors): + colors = None + else: + # replace None values with blue colors + colors = np.array([((0, 0, 1, 1) if a is None else a) + for a in colors]) + # set alpha for hidden (Qt.NoPen, Qt.NoBrush) elements to zero + colors[:, 3][np.array(show) == 0] = 0 + return colors + + edgecolors = do_colors(scatterplot_item.data["pen"], shown_edge) + facecolors = do_colors(scatterplot_item.data["brush"], shown_brush) + code.append("edgecolors = {}".format(numpy_repr(edgecolors))) code.append("facecolors = {}".format(numpy_repr(facecolors))) linewidths = compress_if_all_same(linewidths) @@ -108,12 +134,12 @@ def matplotlib_marker(m): def indexed(data, data_name, indices=indices): return code_with_indices(data, data_name, indices, "indices") - code.append("plt.scatter(x={}, y={}, s={}**2/4, marker={},\n" + code.append("plt.scatter(x={}, y={}, s={}, marker={},\n" " facecolors={}, edgecolors={},\n" " linewidths={})" .format(indexed(x, "x"), indexed(y, "y"), - indexed(sizes, "sizes"), + (indexed(sizes, "sizes") + "**2/4") if sizes is not None else "sizes", repr(m), indexed(facecolors, "facecolors"), indexed(edgecolors, "edgecolors"), From 8dbf90ac586e3d2463d6b71795a447a48b23e5a0 Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Thu, 26 Jul 2018 16:10:39 +0200 Subject: [PATCH 07/11] Matplotlib export: compress colors --- Orange/widgets/utils/matplotlib_export.py | 42 ++++++++++++++++++----- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/Orange/widgets/utils/matplotlib_export.py b/Orange/widgets/utils/matplotlib_export.py index 7d47a588edc..967a0fa7c88 100644 --- a/Orange/widgets/utils/matplotlib_export.py +++ b/Orange/widgets/utils/matplotlib_export.py @@ -2,6 +2,7 @@ import numpy as np +from matplotlib.colors import to_hex from pyqtgraph.graphicsItems.ScatterPlotItem import ScatterPlotItem from AnyQt.QtCore import Qt from AnyQt.QtGui import QPen, QBrush @@ -40,6 +41,18 @@ def code_with_indices(data, data_name, indices, indices_name): return data_name +def index_per_different(l): + different = [] + different_ind = {} + index = [] + for e in l: + if e not in different_ind: + different_ind[e] = len(different) + different.append(e) + index.append(different_ind[e]) + return different, index + + def scatterplot_code(scatterplot_item): x = scatterplot_item.data['x'] y = scatterplot_item.data['y'] @@ -84,23 +97,34 @@ def shown(a): if not any(shown_edge) and not any(shown_brush): return "" - def do_colors(data_column, show): + def do_colors(code, data_column, show, name): colors = [colortuple(a) for a in data_column] if all(a is None for a in colors): - colors = None + colors, index = None, None else: # replace None values with blue colors colors = np.array([((0, 0, 1, 1) if a is None else a) for a in colors]) # set alpha for hidden (Qt.NoPen, Qt.NoBrush) elements to zero colors[:, 3][np.array(show) == 0] = 0 - return colors + # shorter color names for printout + colors = [to_hex(c, keep_alpha=True) for c in colors] + colors, index = index_per_different(colors) + + code.append("{} = {}".format(name, repr(colors))) + if index is not None: + code.append("{}_index = {}".format(name, repr(index))) + + decompresssed_code = name + if index is not None: + decompresssed_code = "array({})[{}_index]".format(name, name) + colors = np.array(colors)[index] + + return colors, decompresssed_code - edgecolors = do_colors(scatterplot_item.data["pen"], shown_edge) - facecolors = do_colors(scatterplot_item.data["brush"], shown_brush) + edgecolors, edgecolors_code = do_colors(code, scatterplot_item.data["pen"], shown_edge, "edgecolors") + facecolors, facecolors_code = do_colors(code, scatterplot_item.data["brush"], shown_brush, "facecolors") - code.append("edgecolors = {}".format(numpy_repr(edgecolors))) - code.append("facecolors = {}".format(numpy_repr(facecolors))) linewidths = compress_if_all_same(linewidths) code.append("linewidths = {}".format(numpy_repr(linewidths))) @@ -141,8 +165,8 @@ def indexed(data, data_name, indices=indices): indexed(y, "y"), (indexed(sizes, "sizes") + "**2/4") if sizes is not None else "sizes", repr(m), - indexed(facecolors, "facecolors"), - indexed(edgecolors, "edgecolors"), + indexed(facecolors, facecolors_code), + indexed(edgecolors, edgecolors_code), indexed(linewidths, "linewidths") )) From 74b9b4c7d02208a3d448f96eeeedcc101142fbef Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Thu, 26 Jul 2018 16:39:22 +0200 Subject: [PATCH 08/11] Matplotlib export: add Matplotlib exporters only to scatter plot --- Orange/widgets/io.py | 5 +++-- Orange/widgets/visualize/owscatterplot.py | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Orange/widgets/io.py b/Orange/widgets/io.py index df111237123..390c5f393db 100644 --- a/Orange/widgets/io.py +++ b/Orange/widgets/io.py @@ -182,7 +182,8 @@ def write_image(cls, filename, scene): super().write_image(filename, scene) -class MatplotlibFormat(FileFormat): +class MatplotlibFormat: + # not registered as a FileFormat as it only works with scatter plot EXTENSIONS = ('.py',) DESCRIPTION = 'Python Code (with Matplotlib)' PRIORITY = 300 @@ -201,7 +202,7 @@ def write(cls, filename, scene): class MatplotlibPDFFormat(MatplotlibFormat): - EXTENSIONS = ('.matplotlib.pdf',) # file formats with same extension are not supported + EXTENSIONS = ('.pdf',) DESCRIPTION = 'Portable Document Format (from Matplotlib)' PRIORITY = 200 diff --git a/Orange/widgets/visualize/owscatterplot.py b/Orange/widgets/visualize/owscatterplot.py index 1310186c45a..9a665be68f4 100644 --- a/Orange/widgets/visualize/owscatterplot.py +++ b/Orange/widgets/visualize/owscatterplot.py @@ -15,6 +15,7 @@ from Orange.preprocess.score import ReliefF, RReliefF from Orange.widgets import gui from Orange.widgets import report +from Orange.widgets.io import MatplotlibFormat, MatplotlibPDFFormat from Orange.widgets.settings import \ DomainContextHandler, Setting, ContextSetting, SettingProvider from Orange.widgets.utils.itemmodels import DomainModel @@ -211,6 +212,12 @@ def __init__(self): self.graph.zoom_actions(self) + # manually register Matplotlib file writers + self.graph_writers = self.graph_writers.copy() + for w in [MatplotlibFormat, MatplotlibPDFFormat]: + for ext in w.EXTENSIONS: + self.graph_writers[ext] = w + def keyPressEvent(self, event): super().keyPressEvent(event) self.graph.update_tooltip(event.modifiers()) From 921a1e6accd5ec6b4a9ea621674b6698afeacffa Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Thu, 26 Jul 2018 16:21:22 +0200 Subject: [PATCH 09/11] Matplotlib export: avoid numpy repr because of version differences --- Orange/widgets/utils/matplotlib_export.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Orange/widgets/utils/matplotlib_export.py b/Orange/widgets/utils/matplotlib_export.py index 967a0fa7c88..f82a07feb62 100644 --- a/Orange/widgets/utils/matplotlib_export.py +++ b/Orange/widgets/utils/matplotlib_export.py @@ -11,6 +11,10 @@ def numpy_repr(a): """ A numpy repr without summarization """ opts = np.get_printoptions() + # avoid numpy repr as it changes between versions + # TODO handle numpy repr differences + if isinstance(a, np.ndarray): + return "array(" + repr(list(a)) + ")" try: np.set_printoptions(threshold=10**10) return repr(a) @@ -18,6 +22,12 @@ def numpy_repr(a): np.set_printoptions(**opts) +def numpy_repr_int(a): + # avoid numpy repr as it changes between versions + # TODO handle numpy repr differences + return "array(" + repr(list(a)) + ", dtype='int')" + + def compress_if_all_same(l): s = set(l) return s.pop() if len(s) == 1 else l @@ -113,7 +123,7 @@ def do_colors(code, data_column, show, name): code.append("{} = {}".format(name, repr(colors))) if index is not None: - code.append("{}_index = {}".format(name, repr(index))) + code.append("{}_index = {}".format(name, numpy_repr_int(index))) decompresssed_code = name if index is not None: @@ -153,7 +163,7 @@ def matplotlib_marker(m): if np.all(indices == np.arange(x.shape[0])): indices = None if indices is not None: - code.append("indices = {}".format(numpy_repr(indices))) + code.append("indices = {}".format(numpy_repr_int(indices))) def indexed(data, data_name, indices=indices): return code_with_indices(data, data_name, indices, "indices") From f4a8a4f74a137d204a1d97539d8132d72c500b0e Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Fri, 24 Aug 2018 13:47:42 +0200 Subject: [PATCH 10/11] TestComputeCD: do not mock matplotlib --- Orange/tests/test_evaluation_scoring.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/Orange/tests/test_evaluation_scoring.py b/Orange/tests/test_evaluation_scoring.py index 724955716fb..2c18a4d518e 100644 --- a/Orange/tests/test_evaluation_scoring.py +++ b/Orange/tests/test_evaluation_scoring.py @@ -1,10 +1,7 @@ # Test methods with long descriptive names can omit docstrings # pylint: disable=missing-docstring -import sys -import contextlib import unittest -from unittest.mock import Mock import numpy as np from Orange.data import DiscreteVariable, ContinuousVariable, Domain @@ -337,23 +334,9 @@ def test_compute_CD(self): cd = scoring.compute_CD(avranks, 30, test="bonferroni-dunn") np.testing.assert_almost_equal(cd, 0.798) - @contextlib.contextmanager - def mock_module(name): - if not name in sys.modules: - try: - sys.modules[name] = Mock() - yield - finally: - del sys.modules[name] - else: - yield - # Do what you will, just don't crash - with mock_module("matplotlib"), \ - mock_module("matplotlib.pyplot"), \ - mock_module("matplotlib.backends.backend_agg"): - scoring.graph_ranks(avranks, "abcd", cd) - scoring.graph_ranks(avranks, "abcd", cd, cdmethod=0) + scoring.graph_ranks(avranks, "abcd", cd) + scoring.graph_ranks(avranks, "abcd", cd, cdmethod=0) class TestLogLoss(unittest.TestCase): From a02d74dc2ae3abcf908dc66d393dde53740f366e Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Fri, 24 Aug 2018 14:02:55 +0200 Subject: [PATCH 11/11] Add matplotlib to package specifications --- conda-recipe/meta.yaml | 1 + scripts/macos/requirements.txt | 1 + scripts/windows/specs/PY34-win32.txt | 1 + 3 files changed, 3 insertions(+) diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index fde79d76a3b..f3a4de6b67b 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -50,6 +50,7 @@ requirements: - python.app # [osx] - commonmark - serverfiles + - matplotlib >=2.0.0 test: # Python imports diff --git a/scripts/macos/requirements.txt b/scripts/macos/requirements.txt index 3c30cb232af..d4a524d3bcf 100644 --- a/scripts/macos/requirements.txt +++ b/scripts/macos/requirements.txt @@ -20,3 +20,4 @@ sip==4.19.3 six==1.10.0 xlrd==1.0.0 CommonMark==0.7.3 +matplotlib==2.2.3 diff --git a/scripts/windows/specs/PY34-win32.txt b/scripts/windows/specs/PY34-win32.txt index 3188d7b02c4..21bd13c1357 100644 --- a/scripts/windows/specs/PY34-win32.txt +++ b/scripts/windows/specs/PY34-win32.txt @@ -21,3 +21,4 @@ pip==9.0.1 pyqtgraph==0.10.0 six==1.10.0 xlrd==1.0.0 +matplotlib==2.2.3