From 4110b3e5399b4c1ea83cc6f0041e850d6966a922 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 28 Jun 2020 08:51:34 -0700 Subject: [PATCH] Handle axis SI prefix scaling in MatplotlibExporter (#1282) * Handle axis SI prefix scaling in MatplotlibExporter * Added some MatplotlibExporter tests and added matplotlib to CI deps * Install mpl with pip instead of conda * Cleanup --- azure-test-template.yml | 3 +- pyqtgraph/exporters/Matplotlib.py | 112 +++++++++++-------- pyqtgraph/exporters/tests/test_matplotlib.py | 50 +++++++++ 3 files changed, 115 insertions(+), 50 deletions(-) create mode 100644 pyqtgraph/exporters/tests/test_matplotlib.py diff --git a/azure-test-template.yml b/azure-test-template.yml index e1d4e177..070281bb 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -106,8 +106,9 @@ jobs: fi conda info conda install $(qt.bindings) numpy scipy pyopengl h5py six --yes --quiet + pip install matplotlib else - pip install $(qt.bindings) numpy scipy pyopengl h5py six + pip install $(qt.bindings) numpy scipy pyopengl h5py six matplotlib fi pip install pytest pytest-cov coverage pytest-xdist if [ $(python.version) == "2.7" ] diff --git a/pyqtgraph/exporters/Matplotlib.py b/pyqtgraph/exporters/Matplotlib.py index dedc2b87..b02b3161 100644 --- a/pyqtgraph/exporters/Matplotlib.py +++ b/pyqtgraph/exporters/Matplotlib.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ..Qt import QtGui, QtCore from .Exporter import Exporter from .. import PlotItem @@ -31,6 +32,7 @@ publication. Fonts are not vectorized (outlined), and window colors are white. class MatplotlibExporter(Exporter): Name = "Matplotlib Window" windows = [] + def __init__(self, item): Exporter.__init__(self, item) @@ -55,58 +57,70 @@ class MatplotlibExporter(Exporter): ax.xaxis.set_ticks_position('bottom') def export(self, fileName=None): + if not isinstance(self.item, PlotItem): + raise Exception("MatplotlibExporter currently only works with PlotItem") + + mpw = MatplotlibWindow() + MatplotlibExporter.windows.append(mpw) + + fig = mpw.getFigure() + + xax = self.item.getAxis('bottom') + yax = self.item.getAxis('left') - if isinstance(self.item, PlotItem): - mpw = MatplotlibWindow() - MatplotlibExporter.windows.append(mpw) + # get labels from the graphic item + xlabel = xax.label.toPlainText() + ylabel = yax.label.toPlainText() + title = self.item.titleLabel.text - stdFont = 'Arial' - - fig = mpw.getFigure() - - # get labels from the graphic item - xlabel = self.item.axes['bottom']['item'].label.toPlainText() - ylabel = self.item.axes['left']['item'].label.toPlainText() - title = self.item.titleLabel.text + # if axes use autoSIPrefix, scale the data so mpl doesn't add its own + # scale factor label + xscale = yscale = 1.0 + if xax.autoSIPrefix: + xscale = xax.autoSIPrefixScale + if yax.autoSIPrefix: + yscale = yax.autoSIPrefixScale - ax = fig.add_subplot(111, title=title) - ax.clear() - self.cleanAxes(ax) - #ax.grid(True) - for item in self.item.curves: - x, y = item.getData() - opts = item.opts - pen = fn.mkPen(opts['pen']) - if pen.style() == QtCore.Qt.NoPen: - linestyle = '' - else: - linestyle = '-' - color = tuple([c/255. for c in fn.colorTuple(pen.color())]) - symbol = opts['symbol'] - if symbol == 't': - symbol = '^' - symbolPen = fn.mkPen(opts['symbolPen']) - symbolBrush = fn.mkBrush(opts['symbolBrush']) - markeredgecolor = tuple([c/255. for c in fn.colorTuple(symbolPen.color())]) - markerfacecolor = tuple([c/255. for c in fn.colorTuple(symbolBrush.color())]) - markersize = opts['symbolSize'] - - if opts['fillLevel'] is not None and opts['fillBrush'] is not None: - fillBrush = fn.mkBrush(opts['fillBrush']) - fillcolor = tuple([c/255. for c in fn.colorTuple(fillBrush.color())]) - ax.fill_between(x=x, y1=y, y2=opts['fillLevel'], facecolor=fillcolor) - - pl = ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(), - linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor, - markersize=markersize) - xr, yr = self.item.viewRange() - ax.set_xbound(*xr) - ax.set_ybound(*yr) - ax.set_xlabel(xlabel) # place the labels. - ax.set_ylabel(ylabel) - mpw.draw() - else: - raise Exception("Matplotlib export currently only works with plot items") + ax = fig.add_subplot(111, title=title) + ax.clear() + self.cleanAxes(ax) + for item in self.item.curves: + x, y = item.getData() + x = x * xscale + y = y * yscale + + opts = item.opts + pen = fn.mkPen(opts['pen']) + if pen.style() == QtCore.Qt.NoPen: + linestyle = '' + else: + linestyle = '-' + color = tuple([c/255. for c in fn.colorTuple(pen.color())]) + symbol = opts['symbol'] + if symbol == 't': + symbol = '^' + symbolPen = fn.mkPen(opts['symbolPen']) + symbolBrush = fn.mkBrush(opts['symbolBrush']) + markeredgecolor = tuple([c/255. for c in fn.colorTuple(symbolPen.color())]) + markerfacecolor = tuple([c/255. for c in fn.colorTuple(symbolBrush.color())]) + markersize = opts['symbolSize'] + + if opts['fillLevel'] is not None and opts['fillBrush'] is not None: + fillBrush = fn.mkBrush(opts['fillBrush']) + fillcolor = tuple([c/255. for c in fn.colorTuple(fillBrush.color())]) + ax.fill_between(x=x, y1=y, y2=opts['fillLevel'], facecolor=fillcolor) + + pl = ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(), + linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor, + markersize=markersize) + + xr, yr = self.item.viewRange() + ax.set_xbound(xr[0]*xscale, xr[1]*xscale) + ax.set_ybound(yr[0]*yscale, yr[1]*yscale) + + ax.set_xlabel(xlabel) # place the labels. + ax.set_ylabel(ylabel) + mpw.draw() MatplotlibExporter.register() diff --git a/pyqtgraph/exporters/tests/test_matplotlib.py b/pyqtgraph/exporters/tests/test_matplotlib.py new file mode 100644 index 00000000..172394c4 --- /dev/null +++ b/pyqtgraph/exporters/tests/test_matplotlib.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +import pytest +import pyqtgraph as pg +from pyqtgraph.exporters import MatplotlibExporter +pytest.importorskip("matplotlib") + +app = pg.mkQApp() + + +def test_MatplotlibExporter(): + plt = pg.plot() + + # curve item + plt.plot([0, 1, 2], [0, 1, 2]) + # scatter item + plt.plot([0, 1, 2], [1, 2, 3], pen=None, symbolBrush='r') + # curve + scatter + plt.plot([0, 1, 2], [2, 3, 4], pen='k', symbolBrush='r') + + exp = MatplotlibExporter(plt.getPlotItem()) + exp.export() + + +def test_MatplotlibExporter_nonplotitem(): + # attempting to export something other than a PlotItem raises an exception + plt = pg.plot() + plt.plot([0, 1, 2], [2, 3, 4]) + exp = MatplotlibExporter(plt.getPlotItem().getViewBox()) + with pytest.raises(Exception): + exp.export() + + +@pytest.mark.parametrize('scale', [1e10, 1e-9]) +def test_MatplotlibExporter_siscale(scale): + # coarse test to verify that plot data is scaled before export when + # autoSIPrefix is in effect (so mpl doesn't add its own multiplier label) + plt = pg.plot([0, 1, 2], [(i+1)*scale for i in range(3)]) + # set the label so autoSIPrefix works + plt.setLabel('left', 'magnitude') + exp = MatplotlibExporter(plt.getPlotItem()) + exp.export() + + mpw = MatplotlibExporter.windows[-1] + fig = mpw.getFigure() + ymin, ymax = fig.axes[0].get_ylim() + + if scale < 1: + assert ymax > scale + else: + assert ymax < scale