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
This commit is contained in:
Kenneth Lyons 2020-06-28 08:51:34 -07:00 committed by GitHub
parent 96d1ef986f
commit 4110b3e539
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 115 additions and 50 deletions

View File

@ -106,8 +106,9 @@ jobs:
fi fi
conda info conda info
conda install $(qt.bindings) numpy scipy pyopengl h5py six --yes --quiet conda install $(qt.bindings) numpy scipy pyopengl h5py six --yes --quiet
pip install matplotlib
else else
pip install $(qt.bindings) numpy scipy pyopengl h5py six pip install $(qt.bindings) numpy scipy pyopengl h5py six matplotlib
fi fi
pip install pytest pytest-cov coverage pytest-xdist pip install pytest pytest-cov coverage pytest-xdist
if [ $(python.version) == "2.7" ] if [ $(python.version) == "2.7" ]

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from ..Qt import QtGui, QtCore from ..Qt import QtGui, QtCore
from .Exporter import Exporter from .Exporter import Exporter
from .. import PlotItem from .. import PlotItem
@ -31,6 +32,7 @@ publication. Fonts are not vectorized (outlined), and window colors are white.
class MatplotlibExporter(Exporter): class MatplotlibExporter(Exporter):
Name = "Matplotlib Window" Name = "Matplotlib Window"
windows = [] windows = []
def __init__(self, item): def __init__(self, item):
Exporter.__init__(self, item) Exporter.__init__(self, item)
@ -55,58 +57,70 @@ class MatplotlibExporter(Exporter):
ax.xaxis.set_ticks_position('bottom') ax.xaxis.set_ticks_position('bottom')
def export(self, fileName=None): 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): # get labels from the graphic item
mpw = MatplotlibWindow() xlabel = xax.label.toPlainText()
MatplotlibExporter.windows.append(mpw) ylabel = yax.label.toPlainText()
title = self.item.titleLabel.text
stdFont = 'Arial' # if axes use autoSIPrefix, scale the data so mpl doesn't add its own
# scale factor label
fig = mpw.getFigure() xscale = yscale = 1.0
if xax.autoSIPrefix:
# get labels from the graphic item xscale = xax.autoSIPrefixScale
xlabel = self.item.axes['bottom']['item'].label.toPlainText() if yax.autoSIPrefix:
ylabel = self.item.axes['left']['item'].label.toPlainText() yscale = yax.autoSIPrefixScale
title = self.item.titleLabel.text
ax = fig.add_subplot(111, title=title) ax = fig.add_subplot(111, title=title)
ax.clear() ax.clear()
self.cleanAxes(ax) self.cleanAxes(ax)
#ax.grid(True) for item in self.item.curves:
for item in self.item.curves: x, y = item.getData()
x, y = item.getData() x = x * xscale
opts = item.opts y = y * yscale
pen = fn.mkPen(opts['pen'])
if pen.style() == QtCore.Qt.NoPen: opts = item.opts
linestyle = '' pen = fn.mkPen(opts['pen'])
else: if pen.style() == QtCore.Qt.NoPen:
linestyle = '-' linestyle = ''
color = tuple([c/255. for c in fn.colorTuple(pen.color())]) else:
symbol = opts['symbol'] linestyle = '-'
if symbol == 't': color = tuple([c/255. for c in fn.colorTuple(pen.color())])
symbol = '^' symbol = opts['symbol']
symbolPen = fn.mkPen(opts['symbolPen']) if symbol == 't':
symbolBrush = fn.mkBrush(opts['symbolBrush']) symbol = '^'
markeredgecolor = tuple([c/255. for c in fn.colorTuple(symbolPen.color())]) symbolPen = fn.mkPen(opts['symbolPen'])
markerfacecolor = tuple([c/255. for c in fn.colorTuple(symbolBrush.color())]) symbolBrush = fn.mkBrush(opts['symbolBrush'])
markersize = opts['symbolSize'] markeredgecolor = tuple([c/255. for c in fn.colorTuple(symbolPen.color())])
markerfacecolor = tuple([c/255. for c in fn.colorTuple(symbolBrush.color())])
if opts['fillLevel'] is not None and opts['fillBrush'] is not None: markersize = opts['symbolSize']
fillBrush = fn.mkBrush(opts['fillBrush'])
fillcolor = tuple([c/255. for c in fn.colorTuple(fillBrush.color())]) if opts['fillLevel'] is not None and opts['fillBrush'] is not None:
ax.fill_between(x=x, y1=y, y2=opts['fillLevel'], facecolor=fillcolor) fillBrush = fn.mkBrush(opts['fillBrush'])
fillcolor = tuple([c/255. for c in fn.colorTuple(fillBrush.color())])
pl = ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(), ax.fill_between(x=x, y1=y, y2=opts['fillLevel'], facecolor=fillcolor)
linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor,
markersize=markersize) pl = ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(),
xr, yr = self.item.viewRange() linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor,
ax.set_xbound(*xr) markersize=markersize)
ax.set_ybound(*yr)
ax.set_xlabel(xlabel) # place the labels. xr, yr = self.item.viewRange()
ax.set_ylabel(ylabel) ax.set_xbound(xr[0]*xscale, xr[1]*xscale)
mpw.draw() ax.set_ybound(yr[0]*yscale, yr[1]*yscale)
else:
raise Exception("Matplotlib export currently only works with plot items") ax.set_xlabel(xlabel) # place the labels.
ax.set_ylabel(ylabel)
mpw.draw()
MatplotlibExporter.register() MatplotlibExporter.register()

View File

@ -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