Related
I also posted in the pyqtgraph forum here.
My overall goal is to have several clickable regions overlaid on an image, and if the plot boundary of any region is clicked I get a signal with the ID of that region. Something like this:
If I use only one PlotDataItem with nan-separated curves then each boundary sends the same signal. However, using a separate PlotDataItem for each boundary makes the application extremely sluggish.
I ended up subclassing ScatterPlotItem and rewriting the pointsAt function, which does what I want. The problem now is I can't figure out the appropriate way to change the ScatterPlotItem's boundingRect. Am I on the right approach? Is there a better way of doing this?
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
class CustScatter(pg.ScatterPlotItem):
def pointsAt(self, pos: QtCore.QPointF):
"""
The default implementation only checks a square around each spot. However, this is not
precise enough for my needs. It also triggers when clicking *inside* the spot boundary,
which I don't want.
"""
pts = []
for spot in self.points(): # type: pg.SpotItem
symb = QtGui.QPainterPath(spot.symbol())
symb.translate(spot.pos())
stroker = QtGui.QPainterPathStroker()
mousePath = stroker.createStroke(symb)
# Only trigger when clicking a boundary, not the inside of the shape
if mousePath.contains(pos):
pts.append(spot)
return pts[::-1]
"""Make some sample data"""
tri = np.array([[0,2.3,0,1,4,5,0], [0,4,4,8,8,3,0]]).T
tris = []
xyLocs = []
datas = []
for ii in np.arange(0, 16, 5):
curTri = tri + ii
tris.append(curTri)
xyLocs.append(curTri.min(0))
datas.append(ii)
def ptsClicked(item, pts):
print(f'ID {pts[0].data()} Clicked!')
"""Logic for making spot shapes from a list of (x,y) vertices"""
def makeSymbol(verts: np.ndarray):
outSymbol = QtGui.QPainterPath()
symPath = pg.arrayToQPath(*verts.T)
outSymbol.addPath(symPath)
# From pyqtgraph.examples for plotting text
br = outSymbol.boundingRect()
tr = QtGui.QTransform()
tr.translate(-br.x(), -br.y())
outSymbol = tr.map(outSymbol)
return outSymbol
app = pg.mkQApp()
pg.setConfigOption('background', 'w')
symbs = []
for xyLoc, tri in zip(xyLocs, tris):
symbs.append(makeSymbol(tri))
"""Create the scatterplot"""
xyLocs = np.vstack(xyLocs)
tri2 = pg.PlotDataItem()
scat = CustScatter(*xyLocs.T, symbol=symbs, data=datas, connect='finite',
pxMode=False, brush=None, pen=pg.mkPen(width=5), size=1)
scat.sigClicked.connect(ptsClicked)
# Now each 'point' is one of the triangles, hopefully
"""Construct GUI window"""
w = pg.PlotWindow()
w.plotItem.addItem(scat)
plt: pg.PlotItem = w.plotItem
plt.showGrid(True, True, 1)
w.show()
app.exec()
Solved! It turns out unless you specify otherwise, the boundingRect of each symbol in the dataset is assumed to be 1 and that the spot size is the limiting factor. After overriding measureSpotSizes as well my solution works:
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
class CustScatter(pg.ScatterPlotItem):
def pointsAt(self, pos: QtCore.QPointF):
"""
The default implementation only checks a square around each spot. However, this is not
precise enough for my needs. It also triggers when clicking *inside* the spot boundary,
which I don't want.
"""
pts = []
for spot in self.points(): # type: pg.SpotItem
symb = QtGui.QPainterPath(spot.symbol())
symb.translate(spot.pos())
stroker = QtGui.QPainterPathStroker()
mousePath = stroker.createStroke(symb)
# Only trigger when clicking a boundary, not the inside of the shape
if mousePath.contains(pos):
pts.append(spot)
return pts[::-1]
def measureSpotSizes(self, dataSet):
"""
Override the method so that it takes symbol size into account
"""
for rec in dataSet:
## keep track of the maximum spot size and pixel size
symbol, size, pen, brush = self.getSpotOpts(rec)
br = symbol.boundingRect()
size = max(br.width(), br.height())*2
width = 0
pxWidth = 0
if self.opts['pxMode']:
pxWidth = size + pen.widthF()
else:
width = size
if pen.isCosmetic():
pxWidth += pen.widthF()
else:
width += pen.widthF()
self._maxSpotWidth = max(self._maxSpotWidth, width)
self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth)
self.bounds = [None, None]
"""Make some sample data"""
tri = np.array([[0,2.3,0,1,4,5,0], [0,4,4,8,8,3,0]]).T
tris = []
xyLocs = []
datas = []
for ii in np.arange(0, 16, 5):
curTri = tri + ii
tris.append(curTri)
xyLocs.append(curTri.min(0))
datas.append(ii)
def ptsClicked(item, pts):
print(f'ID {pts[0].data()} Clicked!')
"""Logic for making spot shapes from a list of (x,y) vertices"""
def makeSymbol(verts: np.ndarray):
plotVerts = verts - verts.min(0, keepdims=True)
symPath = pg.arrayToQPath(*plotVerts.T)
return symPath
app = pg.mkQApp()
pg.setConfigOption('background', 'd')
symbs = []
for xyLoc, tri in zip(xyLocs, tris):
symbs.append(makeSymbol(tri))
"""Create the scatterplot"""
xyLocs = np.vstack(xyLocs)
tri2 = pg.PlotDataItem()
scat = CustScatter(*xyLocs.T, symbol=symbs, data=datas, connect='finite',
pxMode=False, brush=None, pen=pg.mkPen(width=5), size=1)
scat.sigClicked.connect(ptsClicked)
# Now each 'point' is one of the triangles, hopefully
"""Construct GUI window"""
w = pg.PlotWindow()
w.plotItem.addItem(scat)
plt: pg.PlotItem = w.plotItem
plt.showGrid(True, True, 1)
w.show()
app.exec()
I am writing a PyQt5 application that uses OpenGL to slice an STL into images and displays them as it goes. The application works fine when invoked from the command line (Windows 10, Python 3.7.1, PyInstaller 3.5), but when I try packaging it with pyinstaller it crashes with an error:
Traceback (most recent call last):
File "app_qt.py", line 45, in initializeGL
self.gl = self.context().versionFunctions()
ModuleNotFoundError: No module named 'PyQt5._QOpenGLFunctions_4_1_Core'`
The call comes from a QtGui.QOpenGLWindow object. I have followed the suggestions in this answer to no avail. I have tried importing PyQt5 directly, and adding it to the hidden imports in the .spec file as well for pyinstaller.
Since the application runs fine when invoked normally (i.e. python slicer_gui.py) I am inclined to believe pyinstaller is neglecting to add an import to the package somewhere.
Here is the full code. The GUI is simple (pun intended):
import PySimpleGUI as sg
import app_pyopengl
# define GUI layout
layout = [
[sg.Text('STL to Slice', size=(16, 1)), sg.Input(), sg.FileBrowse()],
[sg.Text('Layer Thickness (um) ', size=(16, 1)), sg.InputText('10')],
[sg.Submit(button_text="Slice"), sg.Cancel(button_text='Quit')]
]
# name the window
window = sg.Window('PyQT STL Slicer').Layout(layout)
# loop until user quits
while True:
button, values = window.Read() # read all values in the window
if button == "Quit":
exit()
if button == "Slice":
thickness = float(values[1]) / 1000 # convert um to mm
filename = values[0]
app_pyopengl.main(filename, thickness)
Here is the slicing application:
import sys
import os
import shutil
from ctypes import c_float, c_uint, sizeof
from PyQt5 import QtGui, QtCore, QtWidgets
from stl import mesh
import numpy as np
from printer import printer
GLfloat = c_float
GLuint = c_uint
EPSILON = 0.00001
SCR_WIDTH = 640
SCR_HEIGHT = int(SCR_WIDTH * printer.height / printer.width)
class Window(QtGui.QOpenGLWindow):
def __init__(self,
stlFilename,
layerThickness,
sliceSavePath,
*args,
**kwargs):
super().__init__(*args, **kwargs)
self.setTitle('STL Slicer')
self.vertVAO, self.vertVBO = 0, 0
self.maskVAO, self.maskVBO = 0, 0
self.numOfVerts = 0
self.bounds = dict()
self.totalThickness = 0.
self.currentLayer = 0
self.height = 0
self.stlFilename = stlFilename
self.layerThickness = layerThickness
self.sliceSavePath = sliceSavePath
def initializeGL(self):
self.gl = self.context().versionFunctions()
self.shaderProg = QtGui.QOpenGLShaderProgram()
self.shaderProg.create()
self.shaderProg.addShaderFromSourceFile(
QtGui.QOpenGLShader.Vertex, 'shaders/slice.vert')
self.shaderProg.addShaderFromSourceFile(
QtGui.QOpenGLShader.Fragment, 'shaders/slice.frag')
self.shaderProg.link()
self.loadMesh()
self.proj = QtGui.QMatrix4x4()
self.proj.setToIdentity()
self.proj.ortho(0, printer.width*printer.pixel,
0, printer.height*printer.pixel,
-self.totalThickness, self.totalThickness)
self.model = QtGui.QMatrix4x4()
self.model.setToIdentity()
self.model.translate(0, 0, self.totalThickness+EPSILON)
self.sliceFbo = QtGui.QOpenGLFramebufferObject(
printer.width,
printer.height
)
self.sliceFbo.setAttachment(
QtGui.QOpenGLFramebufferObject.CombinedDepthStencil
)
def loadMesh(self):
# Get information about our mesh
ourMesh = mesh.Mesh.from_file(self.stlFilename)
self.numOfVerts = ourMesh.vectors.shape[0] * 3
self.bounds = {
'xmin': np.min(ourMesh.vectors[:,:,0]),
'xmax': np.max(ourMesh.vectors[:,:,0]),
'ymin': np.min(ourMesh.vectors[:,:,1]),
'ymax': np.max(ourMesh.vectors[:,:,1]),
'zmin': np.min(ourMesh.vectors[:,:,2]),
'zmax': np.max(ourMesh.vectors[:,:,2])
}
self.totalThickness = self.bounds['zmax'] - self.bounds['zmin']
#######################################
# make VAO for drawing our mesh
self.vertVAO = QtGui.QOpenGLVertexArrayObject()
self.vertVAO.create()
self.vertVAO.bind()
self.vertVBO = QtGui.QOpenGLBuffer(QtGui.QOpenGLBuffer.VertexBuffer)
self.vertVBO.create()
self.vertVBO.bind()
self.vertVBO.setUsagePattern(QtGui.QOpenGLBuffer.StaticDraw)
data = ourMesh.vectors.astype(GLfloat).tostring()
self.vertVBO.allocate(data, len(data))
self.gl.glVertexAttribPointer(0, 3, self.gl.GL_FLOAT,
self.gl.GL_FALSE, 3*sizeof(GLfloat), 0)
self.gl.glEnableVertexAttribArray(0)
self.vertVBO.release()
self.vertVAO.release()
#######################################
# a mask vertex array for stencil buffer to subtract
maskVert = np.array(
[[0, 0, 0],
[printer.width*printer.pixel, 0, 0],
[printer.width*printer.pixel, printer.height*printer.pixel, 0],
[0, 0, 0],
[printer.width*printer.pixel, printer.height*printer.pixel, 0],
[0, printer.height*printer.pixel, 0]], dtype=GLfloat
)
#######################################
# make VAO for drawing mask
self.maskVAO = QtGui.QOpenGLVertexArrayObject()
self.maskVAO.create()
self.maskVAO.bind()
self.maskVBO = QtGui.QOpenGLBuffer(QtGui.QOpenGLBuffer.VertexBuffer)
self.maskVBO.create()
self.maskVBO.bind()
self.maskVBO.setUsagePattern(QtGui.QOpenGLBuffer.StaticDraw)
data = maskVert.tostring()
self.maskVBO.allocate(data, len(data))
self.gl.glVertexAttribPointer(0, 3, self.gl.GL_FLOAT,
self.gl.GL_FALSE, 3*sizeof(GLfloat), 0)
self.gl.glEnableVertexAttribArray(0)
self.maskVBO.release()
self.maskVAO.release()
#######################################
def paintGL(self):
if self.height >= self.totalThickness-EPSILON:
sys.exit()
else:
self.height += self.layerThickness
self.currentLayer += 1
self.draw()
self.renderSlice()
self.update()
def draw(self):
self.gl.glViewport(0, 0, self.size().width(), self.size().height())
self.gl.glEnable(self.gl.GL_STENCIL_TEST)
self.gl.glClearColor(0., 0., 0., 1.)
self.gl.glClear(self.gl.GL_COLOR_BUFFER_BIT | self.gl.GL_STENCIL_BUFFER_BIT)
self.vertVAO.bind()
self.shaderProg.bind()
self.model.translate(0, 0, -self.layerThickness)
self.shaderProg.setUniformValue('proj', self.proj)
self.shaderProg.setUniformValue('model', self.model)
self.gl.glEnable(self.gl.GL_CULL_FACE)
self.gl.glCullFace(self.gl.GL_FRONT)
self.gl.glStencilFunc(self.gl.GL_ALWAYS, 0, 0xFF)
self.gl.glStencilOp(self.gl.GL_KEEP, self.gl.GL_KEEP, self.gl.GL_INCR)
self.gl.glDrawArrays(self.gl.GL_TRIANGLES, 0, self.numOfVerts)
self.gl.glCullFace(self.gl.GL_BACK)
self.gl.glStencilOp(self.gl.GL_KEEP, self.gl.GL_KEEP, self.gl.GL_DECR)
self.gl.glDrawArrays(self.gl.GL_TRIANGLES, 0, self.numOfVerts)
self.gl.glDisable(self.gl.GL_CULL_FACE)
self.gl.glClear(self.gl.GL_COLOR_BUFFER_BIT)
self.maskVAO.bind()
self.gl.glStencilFunc(self.gl.GL_NOTEQUAL, 0, 0xFF)
self.gl.glStencilOp(self.gl.GL_KEEP, self.gl.GL_KEEP, self.gl.GL_KEEP)
self.gl.glDrawArrays(self.gl.GL_TRIANGLES, 0, 6)
self.gl.glDisable(self.gl.GL_STENCIL_TEST)
self.shaderProg.release()
def renderSlice(self):
self.sliceFbo.bind()
self.gl.glViewport(0, 0, printer.width, printer.height)
self.gl.glEnable(self.gl.GL_STENCIL_TEST)
self.gl.glClearColor(0., 0., 0., 1.)
self.gl.glClear(self.gl.GL_COLOR_BUFFER_BIT | self.gl.GL_STENCIL_BUFFER_BIT)
self.vertVAO.bind()
self.shaderProg.bind()
self.shaderProg.setUniformValue('proj', self.proj)
self.shaderProg.setUniformValue('model', self.model)
self.gl.glEnable(self.gl.GL_CULL_FACE)
self.gl.glCullFace(self.gl.GL_FRONT)
self.gl.glStencilFunc(self.gl.GL_ALWAYS, 0, 0xFF)
self.gl.glStencilOp(self.gl.GL_KEEP, self.gl.GL_KEEP, self.gl.GL_INCR)
self.gl.glDrawArrays(self.gl.GL_TRIANGLES, 0, self.numOfVerts)
self.gl.glCullFace(self.gl.GL_BACK)
self.gl.glStencilOp(self.gl.GL_KEEP, self.gl.GL_KEEP, self.gl.GL_DECR)
self.gl.glDrawArrays(self.gl.GL_TRIANGLES, 0, self.numOfVerts)
self.gl.glDisable(self.gl.GL_CULL_FACE)
self.gl.glClear(self.gl.GL_COLOR_BUFFER_BIT)
self.maskVAO.bind()
self.gl.glStencilFunc(self.gl.GL_NOTEQUAL, 0, 0xFF)
self.gl.glStencilOp(self.gl.GL_KEEP, self.gl.GL_KEEP, self.gl.GL_KEEP)
self.gl.glDrawArrays(self.gl.GL_TRIANGLES, 0, 6)
self.gl.glDisable(self.gl.GL_STENCIL_TEST)
image = self.sliceFbo.toImage()
# makes a QComboBox for different Image Format,
# namely Format_Mono, Format_MonoLSB, and Format_Grayscale8
image = image.convertToFormat(QtGui.QImage.Format_Grayscale8)
image.save(os.path.join(self.sliceSavePath,
'out{:04d}.png'.format(self.currentLayer)))
self.sliceFbo.release()
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Escape:
sys.exit()
event.accept()
def main(stlFilename, layerThickness):
temp = os.path.dirname(stlFilename)
sliceSavePath = os.path.join(temp, 'slices')
# remove old slices folder if there is one, and make a new empty one
if os.path.exists(sliceSavePath):
shutil.rmtree(sliceSavePath) # using shutil here avoids permissions errors
if not os.path.exists(sliceSavePath):
os.mkdir(sliceSavePath)
# Set format here, otherwise it throws error
# `QCocoaGLContext: Falling back to unshared context.`
# on Mac when use QOpenGLWidgets
# https://doc.qt.io/qt-5/qopenglwidget.html#details last paragraph
format = QtGui.QSurfaceFormat()
format.setRenderableType(QtGui.QSurfaceFormat.OpenGL)
format.setProfile(QtGui.QSurfaceFormat.CoreProfile)
format.setVersion(4, 1)
format.setStencilBufferSize(8)
QtGui.QSurfaceFormat.setDefaultFormat(format)
app = QtWidgets.QApplication(sys.argv)
window = Window(stlFilename, layerThickness, sliceSavePath)
window.resize(SCR_WIDTH, SCR_HEIGHT)
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main(sys.argv[1], float(sys.argv[2]))
I'm using reportlab to generate reports. I can define the creation process in four steps: 1) get the data via API, 2) filter the data, 3) generate the graphics with matplotlib and 4) insert information in PDF with reportlab.
I found in this (thanks Patrick Maupin!) and in this (thanks Larry Meyn!) answer a flowable matplotlib for ReportLab. I made some changes and I copy below the part of the code that interests:
import os
from matplotlib import pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
from reportlab.platypus import Paragraph, SimpleDocTemplate, Flowable
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfgen import canvas
from pdfrw import PdfReader, PdfDict
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
try:
from cStringIO import StringIO as BytesIO
except ImportError:
from io import BytesIO
styles = getSampleStyleSheet()
style = styles['Normal']
#Info if you want to run the script
div = ['may/18', 'jun/18', 'jul/18', 'aug/18', 'sep/18', 'oct/18', 'nov/18', \
'dec/18', 'jan/19', 'feb/19', 'mar/19', 'apr/19']
mes = [0, 15, 149, 0, 59, 0, 0, 101, 45, 25, 751.07, 5358.3]
acc = [0, 15, 164, 164, 223, 223, 223, 324, 369, 394, 1145.07, 6503.37]
class PdfImage(Flowable):
"""
Generates a reportlab image flowable for matplotlib figures. It is initialized
with either a matplotlib figure or a pointer to a list of pagexobj objects and
an index for the pagexobj to be used.
"""
def __init__(self, fig=None, width=200, height=200, cache=None, cacheindex=0):
self.img_width = width
self.img_height = height
if fig is None and cache is None:
raise ValueError("Either 'fig' or 'cache' must be provided")
if fig is not None:
imgdata = BytesIO()
fig.savefig(imgdata, format='pdf')
imgdata.seek(0)
page, = PdfReader(imgdata).pages
image = pagexobj(page)
self.img_data = image
else:
self.img_data = None
self.cache = cache
self.cacheindex = cacheindex
def wrap(self, width, height):
return self.img_width, self.img_height
def drawOn(self, canv, x, y, _sW=0):
if _sW > 0 and hasattr(self, 'hAlign'):
a = self.hAlign
if a in ('CENTER', 'CENTRE', TA_CENTER):
x += 0.5*_sW
elif a in ('RIGHT', TA_RIGHT):
x += _sW
elif a not in ('LEFT', TA_LEFT):
raise ValueError("Bad hAlign value " + str(a))
canv.saveState()
if self.img_data is not None:
img = self.img_data
else:
img = self.cache[self.cacheindex]
if isinstance(img, PdfDict):
xscale = self.img_width / img.BBox[2]
yscale = self.img_height / img.BBox[3]
canv.translate(x, y)
canv.scale(xscale, yscale)
canv.doForm(makerl(canv, img))
else:
canv.drawImage(img, x, y, self.img_width, self.img_height)
canv.restoreState()
class PdfImageCache(object):
"""
Saves matplotlib figures to a temporary multi-page PDF file using the 'savefig'
method. When closed the images are extracted and saved to the attribute 'cache'.
The temporary PDF file is then deleted. The 'savefig' returns a PdfImage object
with a pointer to the 'cache' list and an index for the figure. Use of this
cache reduces duplicated resources in the reportlab generated PDF file.
Use is similar to matplotlib's PdfPages object. When not used as a context
manager, the 'close()' method must be explictly called before the reportlab
document is built.
"""
def __init__(self):
self.pdftempfile = '_temporary_pdf_image_cache_.pdf'
self.pdf = PdfPages(self.pdftempfile)
self.cache = []
self.count = 0
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
def close(self, *args):
self.pdf.close()
pages = PdfReader(self.pdftempfile).pages
pages = [pagexobj(x) for x in pages]
self.cache.extend(pages)
os.remove(self.pdftempfile)
def savefig(self, fig, width=200, height=200):
self.pdf.savefig(fig)
index = self.count
self.count += 1
return PdfImage(width=width, height=height, cache=self.cache, cacheindex=index)
def make_report_cached_figs(outfn):
"""
Makes a dummy report with nfig matplotlib plots using PdfImageCache
to reduce PDF file size.
"""
doc = SimpleDocTemplate(outfn)
style = styles["Normal"]
story = []
with PdfImageCache() as pdfcache:
fig = plt.figure(figsize=(5, 5))
plt.bar(div, acc, width = 0.8, color = 'silver', label = 'Year')
plt.bar(div, mes, width = 0.4, color = 'k', label = 'Month', alpha = 0.5)
plt.legend()
plt.xticks(rotation=60)
plt.close()
img0 = pdfcache.savefig(fig, width=185, height=135)
img0.drawOn(doc, 0, 100)
story.append(img0)
doc.build(story)
make_report_cached_figs("Test.pdf")
My problem is in the drawOn method, more specifically in the first argument. In the above code I get the following error:
AttributeError: 'SimpleDocTemplate' object has no attribute 'saveState'
If I replace doc = SimpleDocTemplate(outfn) with doc = canvas.Canvas(outfn) (I know that if I'm going to use canvas I should make some other minor changes) I get another error:
img = self.cache[self.cacheindex]
IndexError: list index out of range
I have searched for information in the official documentation and in the source code but have not been successful. So, how do I set the position of the image? (using the drawOn method or not)
I'm using matplotlib and PyQt5 in a GUI application. To plot my data I use the "FigureCanvasQTAgg" and add the "NavigationToolbar2QT" to be able to modify and save my plots. It works, but I was wondering if there are more advanved Toolbars that for example allow changing the font size of the titel and/or label? Here is what I use atm:
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
self.figure = plt.figure()
self.ax = self.figure.add_subplot(111)
self.canvas = FigureCanvas(self.figure)
self.toolbar = NavigationToolbar(self.canvas)
The available "Figure options" look like this:
Options I'm looking for are:
font size of the title
font size of axis-label
options for the legend like position, font size, style
Probably I'm not the first one looking for these options, so I guess that somebody coded such an advanced toolbar already, but I couldn't find anything and thought it's worth asking here before I try to code it on my own and (probably) waste a lot of time.
The figure options qt dialog is defined in
https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/backends/qt_editor/figureoptions.py
You may copy that code to a new file, say myfigureoptions.py and make the changes you want to. Then monkey-patch it into the original.
The following would add a title fontsize field.
# Copyright © 2009 Pierre Raybaut
# Licensed under the terms of the MIT License
# see the mpl licenses directory for a copy of the license
# Modified to add a title fontsize
"""Module that provides a GUI-based editor for matplotlib's figure options."""
import os.path
import re
import matplotlib
from matplotlib import cm, colors as mcolors, markers, image as mimage
import matplotlib.backends.qt_editor.formlayout as formlayout
from matplotlib.backends.qt_compat import QtGui
def get_icon(name):
basedir = os.path.join(matplotlib.rcParams['datapath'], 'images')
return QtGui.QIcon(os.path.join(basedir, name))
LINESTYLES = {'-': 'Solid',
'--': 'Dashed',
'-.': 'DashDot',
':': 'Dotted',
'None': 'None',
}
DRAWSTYLES = {
'default': 'Default',
'steps-pre': 'Steps (Pre)', 'steps': 'Steps (Pre)',
'steps-mid': 'Steps (Mid)',
'steps-post': 'Steps (Post)'}
MARKERS = markers.MarkerStyle.markers
def figure_edit(axes, parent=None):
"""Edit matplotlib figure options"""
sep = (None, None) # separator
# Get / General
# Cast to builtin floats as they have nicer reprs.
xmin, xmax = map(float, axes.get_xlim())
ymin, ymax = map(float, axes.get_ylim())
general = [('Title', axes.get_title()),
('Title Fontsize', axes.title.get_fontsize()), # <------------- HERE
sep,
(None, "<b>X-Axis</b>"),
('Left', xmin), ('Right', xmax),
('Label', axes.get_xlabel()),
('Scale', [axes.get_xscale(), 'linear', 'log', 'logit']),
sep,
(None, "<b>Y-Axis</b>"),
('Bottom', ymin), ('Top', ymax),
('Label', axes.get_ylabel()),
('Scale', [axes.get_yscale(), 'linear', 'log', 'logit']),
sep,
('(Re-)Generate automatic legend', False),
]
# Save the unit data
xconverter = axes.xaxis.converter
yconverter = axes.yaxis.converter
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():
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), ...]`.
"""
if init not in d:
d = {**d, init: str(init)}
# 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(
mcolors.to_rgba(line.get_markeredgecolor(), line.get_alpha()),
keep_alpha=True)
fc = mcolors.to_hex(
mcolors.to_rgba(line.get_markerfacecolor(), line.get_alpha()),
keep_alpha=True)
curvedata = [
('Label', label),
sep,
(None, '<b>Line</b>'),
('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, '<b>Marker</b>'),
('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),
('Interpolation',
[image.get_interpolation()]
+ [(name, name) for name in sorted(mimage.interpolations_names)])]
images.append([imagedata, label, ""])
# Is there an image displayed?
has_image = bool(images)
datalist = [(general, "Axes", "")]
if curves:
datalist.append((curves, "Curves", ""))
if images:
datalist.append((images, "Images", ""))
def apply_callback(data):
"""This function will be called to apply changes"""
orig_xlim = axes.get_xlim()
orig_ylim = axes.get_ylim()
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, titlefontsize, xmin, xmax, xlabel, xscale, # <------------- HERE
ymin, ymax, ylabel, yscale, generate_legend) = general
if axes.get_xscale() != xscale:
axes.set_xscale(xscale)
if axes.get_yscale() != yscale:
axes.set_yscale(yscale)
axes.set_title(title)
axes.title.set_fontsize(titlefontsize) # <------------- HERE
axes.set_xlim(xmin, xmax)
axes.set_xlabel(xlabel)
axes.set_ylim(ymin, ymax)
axes.set_ylabel(ylabel)
# Restore the unit data
axes.xaxis.converter = xconverter
axes.yaxis.converter = yconverter
axes.xaxis.set_units(xunits)
axes.yaxis.set_units(yunits)
axes.xaxis._update_axisinfo()
axes.yaxis._update_axisinfo()
# 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_alpha(None)
line.set_color(rgba)
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, interpolation = image_settings
image.set_label(label)
image.set_cmap(cm.get_cmap(cmap))
image.set_clim(*sorted([low, high]))
image.set_interpolation(interpolation)
# re-generate legend, if checkbox is checked
if generate_legend:
draggable = None
ncol = 1
if axes.legend_ is not None:
old_legend = axes.get_legend()
draggable = old_legend._draggable is not None
ncol = old_legend._ncol
new_legend = axes.legend(ncol=ncol)
if new_legend:
new_legend.set_draggable(draggable)
# Redraw
figure = axes.get_figure()
figure.canvas.draw()
if not (axes.get_xlim() == orig_xlim and axes.get_ylim() == orig_ylim):
figure.canvas.toolbar.push_current()
data = formlayout.fedit(datalist, title="Figure options", parent=parent,
icon=get_icon('qt4_editor_options.svg'),
apply=apply_callback)
if data is not None:
apply_callback(data)
# Monkey-patch original figureoptions
from matplotlib.backends.qt_editor import figureoptions # <------------- HERE
figureoptions.figure_edit = figure_edit
Use it as
import matplotlib.pyplot as plt
import myfigureoptions
fig, ax = plt.subplots()
ax.plot([1,2])
ax.set_title("My Title")
plt.show()
When clicking the figure options dialog you now have a title font size field.
Based on the answer of ImportanceOfBeingErnes I started to modify the matplotlib figure options in the way I need them for my app. I removed the left/right and up/down limits for the X-Axis and Y-Axis because I already got these options implemented in my GUI, but I added another tab for various legend options. This his how the figure options look now:
And here is the code of the current version (I had to access some private variables because I couldn't find the corresponding get-functions and also the variable naming might not be the best. Please feel free to revise my code):
# Copyright © 2009 Pierre Raybaut
# Licensed under the terms of the MIT License
# see the mpl licenses directory for a copy of the license
# Modified to add a title fontsize
"""Module that provides a GUI-based editor for matplotlib's figure options."""
import os.path
import re
import matplotlib
from matplotlib import cm, colors as mcolors, markers, image as mimage
import matplotlib.backends.qt_editor.formlayout as formlayout
from matplotlib.backends.qt_compat import QtGui
def get_icon(name):
basedir = os.path.join(matplotlib.rcParams['datapath'], 'images')
return QtGui.QIcon(os.path.join(basedir, name))
LINESTYLES = {'-': 'Solid',
'--': 'Dashed',
'-.': 'DashDot',
':': 'Dotted',
'None': 'None',
}
DRAWSTYLES = {
'default': 'Default',
'steps-pre': 'Steps (Pre)', 'steps': 'Steps (Pre)',
'steps-mid': 'Steps (Mid)',
'steps-post': 'Steps (Post)'}
MARKERS = markers.MarkerStyle.markers
def figure_edit(axes, parent=None):
"""Edit matplotlib figure options"""
sep = (None, None) # separator
# Get / General
# Cast to builtin floats as they have nicer reprs.
xmin, xmax = map(float, axes.get_xlim())
ymin, ymax = map(float, axes.get_ylim())
if 'labelsize' in axes.xaxis._major_tick_kw:
_ticksize = int(axes.xaxis._major_tick_kw['labelsize'])
else:
_ticksize = 15
general = [(None, "<b>Figure Title</b>"),
('Title', axes.get_title()),
('Font Size', int(axes.title.get_fontsize())),
sep,
(None, "<b>Axes settings</b>"),
('Label Size', int(axes.xaxis.label.get_fontsize())),
('Tick Size', _ticksize),
('Show grid', axes.xaxis._gridOnMajor),
sep,
(None, "<b>X-Axis</b>"),
('Label', axes.get_xlabel()),
('Scale', [axes.get_xscale(), 'linear', 'log', 'logit']),
sep,
(None, "<b>Y-Axis</b>"),
('Label', axes.get_ylabel()),
('Scale', [axes.get_yscale(), 'linear', 'log', 'logit'])
]
if axes.legend_ is not None:
old_legend = axes.get_legend()
_draggable = old_legend._draggable is not None
_ncol = old_legend._ncol
_fontsize = int(old_legend._fontsize)
_frameon = old_legend._drawFrame
_shadow = old_legend.shadow
_fancybox = type(old_legend.legendPatch.get_boxstyle()) == matplotlib.patches.BoxStyle.Round
_framealpha = old_legend.get_frame().get_alpha()
else:
_draggable = False
_ncol = 1
_fontsize = 15
_frameon = True
_shadow = True
_fancybox = True
_framealpha = 0.5
legend = [('Draggable', _draggable),
('columns', _ncol),
('Font Size', _fontsize),
('Frame', _frameon),
('Shadow', _shadow),
('FancyBox', _fancybox),
('Alpha', _framealpha)
]
# Save the unit data
xconverter = axes.xaxis.converter
yconverter = axes.yaxis.converter
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():
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), ...]`.
"""
if init not in d:
d = {**d, init: str(init)}
# 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(
mcolors.to_rgba(line.get_markeredgecolor(), line.get_alpha()),
keep_alpha=True)
fc = mcolors.to_hex(
mcolors.to_rgba(line.get_markerfacecolor(), line.get_alpha()),
keep_alpha=True)
curvedata = [
('Label', label),
sep,
(None, '<b>Line</b>'),
('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, '<b>Marker</b>'),
('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),
('Interpolation',
[image.get_interpolation()]
+ [(name, name) for name in sorted(mimage.interpolations_names)])]
images.append([imagedata, label, ""])
# Is there an image displayed?
has_image = bool(images)
datalist = [(general, "Axes", ""), (legend, "Legend", "")]
if curves:
datalist.append((curves, "Curves", ""))
if images:
datalist.append((images, "Images", ""))
def apply_callback(data):
"""This function will be called to apply changes"""
general = data.pop(0)
legend = 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, titlesize, labelsize, ticksize, grid, xlabel, xscale,
ylabel, yscale) = general
if axes.get_xscale() != xscale:
axes.set_xscale(xscale)
if axes.get_yscale() != yscale:
axes.set_yscale(yscale)
axes.set_title(title)
axes.title.set_fontsize(titlesize)
axes.set_xlabel(xlabel)
axes.xaxis.label.set_size(labelsize)
axes.xaxis.set_tick_params(labelsize=ticksize)
axes.set_ylabel(ylabel)
axes.yaxis.label.set_size(labelsize)
axes.yaxis.set_tick_params(labelsize=ticksize)
axes.grid(grid)
# Restore the unit data
axes.xaxis.converter = xconverter
axes.yaxis.converter = yconverter
axes.xaxis.set_units(xunits)
axes.yaxis.set_units(yunits)
axes.xaxis._update_axisinfo()
axes.yaxis._update_axisinfo()
# Set / Legend
(leg_draggable, leg_ncol, leg_fontsize, leg_frameon, leg_shadow,
leg_fancybox, leg_framealpha, ) = legend
new_legend = axes.legend(ncol=leg_ncol,
fontsize=float(leg_fontsize),
frameon=leg_frameon,
shadow=leg_shadow,
framealpha=leg_framealpha,
fancybox=leg_fancybox)
new_legend.set_draggable(leg_draggable)
# 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_alpha(None)
line.set_color(rgba)
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, interpolation = image_settings
image.set_label(label)
image.set_cmap(cm.get_cmap(cmap))
image.set_clim(*sorted([low, high]))
image.set_interpolation(interpolation)
# Redraw
figure = axes.get_figure()
figure.canvas.draw()
data = formlayout.fedit(datalist, title="Figure options", parent=parent,
icon=get_icon('qt4_editor_options.svg'),
apply=apply_callback)
if data is not None:
apply_callback(data)
# Monkey-patch original figureoptions
from matplotlib.backends.qt_editor import figureoptions
figureoptions.figure_edit = figure_edit
Using the minimal example below, the line plot of a large (some 110k points) plot I get (with python 2.7, numpy 1.5.1, chaco/enable/traits 4.3.0) is this:
However, that is bizarre, because it is a line plot, and there shouldn't be any filled areas in there? Especially since the data is sawtooth-ish signal? It's as if there is a line at y~=37XX, above which there is color filling?! But sure enough, if I zoom into an area, I get the rendering I expect - without the unexpected fill:
Is this a bug - or is there something I'm doing wrong? I tried to use use_downsampling, but it makes no difference...
The test code:
import numpy as np
import numpy.random as npr
from pprint import pprint
from traits.api import HasTraits, Instance
from chaco.api import Plot, ArrayPlotData, VPlotContainer
from traitsui.api import View, Item
from enable.component_editor import ComponentEditor
from chaco.tools.api import PanTool, BetterSelectingZoom
tlen = 112607
alr = npr.randint(0, 4000, tlen)
tx = np.arange(0.0, 30.0-0.00001, 30.0/tlen)
ty = np.arange(0, tlen, 1) % 10000 + alr
pprint(len(ty))
class ChacoTest(HasTraits):
container = Instance(VPlotContainer)
traits_view = View(
Item('container', editor=ComponentEditor(), show_label=False),
width=800, height=500, resizable=True,
title="Chaco Test"
)
def __init__(self):
super(ChacoTest, self).__init__()
pprint(ty)
self.plotdata = ArrayPlotData(x = tx, y = ty)
self.plotobj = Plot(self.plotdata)
self.plotA = self.plotobj.plot(("x", "y"), type="line", color=(0,0.99,0), spacing=0, padding=0, alpha=0.7, use_downsampling=True)
self.container = VPlotContainer(self.plotobj, spacing=5, padding=5, bgcolor="lightgray")
#~ container.add(plot)
self.plotobj.tools.append(PanTool(self.plotobj))
self.plotobj.overlays.append(BetterSelectingZoom(self.plotobj))
if __name__ == "__main__":
ChacoTest().configure_traits()
I am able to reproduce the error and talking with John Wiggins (maintainer of Enable), it is a bug in kiva (which chaco uses to paint on the screen):
https://github.com/enthought/enable
The good news is that this is a bug in one of the kiva backend that you can use. So to go around the issue, you can run your script choosing a different backend:
ETS_TOOLKIT=qt4.qpainter python <NAME OF YOUR SCRIPT>
if you use qpainter or quartz, the plot looks (on my machine) as expected. If you choose qt4.image (the Agg backend), you will reproduce the issue. Unfortunately, the Agg backend is the default one. To change that, you can set the ETS_TOOLKIT environment variable to that value:
export ETS_TOOLKIT=qt4.qpainter
The bad news is that fixing this isn't going to be an easy task. Please feel free to report the bug in github (again https://github.com/enthought/enable) if you want to be involved in this. If you don't, I will log it in the next couple of days. Thanks for reporting it!
Just a note - I found this:
[Enthought-Dev] is chaco faster than matplotlib
I recall reading somewhere that you are expected to implement the
_downsample method because the optimal algorithm depends on the type
of data you're collecting.
And as I couldn't find any examples with _downsample implementation other than decimated_plot.py referred in that post, which isn't standalone - I tried and built a standalone example, included below.
The example basically has messed up drag and zoom, (plot disappears if you go out of range, or stretches upon a drag move) - and it starts zoomed in; but it is possible to zoom it out in the range shown in the OP - and then it displays the exact same plot rendering problem. So downsampling isn't the solution per se, so this is likely a bug?
import numpy as np
import numpy.random as npr
from pprint import pprint
from traits.api import HasTraits, Instance
from chaco.api import Plot, ArrayPlotData, VPlotContainer
from traitsui.api import View, Item
from enable.component_editor import ComponentEditor
from chaco.tools.api import PanTool, BetterSelectingZoom
#
from chaco.api import BaseXYPlot, LinearMapper, AbstractPlotData
from enable.api import black_color_trait, LineStyle
from traits.api import Float, Enum, Int, Str, Trait, Event, Property, Array, cached_property, Bool, Dict
from chaco.abstract_mapper import AbstractMapper
from chaco.abstract_data_source import AbstractDataSource
from chaco.array_data_source import ArrayDataSource
from chaco.data_range_1d import DataRange1D
tlen = 112607
alr = npr.randint(0, 4000, tlen)
tx = np.arange(0.0, 30.0-0.00001, 30.0/tlen)
ty = np.arange(0, tlen, 1) % 10000 + alr
pprint(len(ty))
class ChacoTest(HasTraits):
container = Instance(VPlotContainer)
traits_view = View(
Item('container', editor=ComponentEditor(), show_label=False),
width=800, height=500, resizable=True,
title="Chaco Test"
)
downsampling_cutoff = Int(4)
def __init__(self):
super(ChacoTest, self).__init__()
pprint(ty)
self.plotdata = ArrayPlotData(x = tx, y = ty)
self.plotobj = TimeSeriesPlot(self.plotdata)
self.plotobj.setplotranges("x", "y")
self.container = VPlotContainer(self.plotobj, spacing=5, padding=5, bgcolor="lightgray")
self.plotobj.tools.append(PanTool(self.plotobj))
self.plotobj.overlays.append(BetterSelectingZoom(self.plotobj))
# decimate from:
# https://bitbucket.org/mjrosen/neurobehavior/raw/097ef3719d1263a8b303d29c31ab71b6e792ab04/cns/widgets/views/decimated_plot.py
def decimate(data, screen_width, downsampling_cutoff=4, mode='extremes'):
data_width = data.shape[-1]
downsample = np.floor((data_width/screen_width)/4.)
if downsample > downsampling_cutoff:
return globals()['decimate_'+mode](data, downsample)
else:
return data
def decimate_extremes(data, downsample):
last_dim = data.ndim
offset = data.shape[-1] % downsample
if data.ndim == 2:
shape = (len(data), -1, downsample)
else:
shape = (-1, downsample)
data = data[..., offset:].reshape(shape).copy()
data_min = data.min(last_dim)
data_max = data.max(last_dim)
return data_min, data_max
def decimate_mean(data, downsample):
offset = len(data) % downsample
if data.ndim == 2:
shape = (-1, downsample, data.shape[-1])
else:
shape = (-1, downsample)
data = data[offset:].reshape(shape).copy()
return data.mean(1)
# based on class from decimated_plot.py, also
# neurobehavior/cns/chaco_exts/timeseries_plot.py ;
# + some other code from chaco
class TimeSeriesPlot(BaseXYPlot):
color = black_color_trait
line_width = Float(1.0)
line_style = LineStyle
reference = Enum('most_recent', 'trigger')
traits_view = View("color#", "line_width")
downsampling_cutoff = Int(100)
signal_trait = "updated"
decimate_mode = Str('extremes')
ch_index = Trait(None, Int, None)
# Mapping of data names from self.data to their respective datasources.
datasources = Dict(Str, Instance(AbstractDataSource))
index_mapper = Instance(AbstractMapper)
value_mapper = Instance(AbstractMapper)
def __init__(self, data=None, **kwargs):
super(TimeSeriesPlot, self).__init__(**kwargs)
self._index_mapper_changed(None, self.index_mapper)
self.setplotdata(data)
self._plot_ui_info = None
return
def setplotdata(self, data):
if data is not None:
if isinstance(data, AbstractPlotData):
self.data = data
elif type(data) in (ndarray, tuple, list):
self.data = ArrayPlotData(data)
else:
raise ValueError, "Don't know how to create PlotData for data" \
"of type " + str(type(data))
def setplotranges(self, index_name, value_name):
self.index_name = index_name
self.value_name = value_name
index = self._get_or_create_datasource(index_name)
value = self._get_or_create_datasource(value_name)
if not(self.index_mapper):
imap = LinearMapper()#(range=self.index_range)
self.index_mapper = imap
if not(self.value_mapper):
vmap = LinearMapper()#(range=self.value_range)
self.value_mapper = vmap
if not(self.index_range): self.index_range = DataRange1D() # calls index_mapper
if not(self.value_range): self.value_range = DataRange1D()
self.index_range.add(index) # calls index_mapper!
self.value_range.add(value)
# now do it (right?):
self.index_mapper = LinearMapper(range=self.index_range)
self.value_mapper = LinearMapper(range=self.value_range)
def _get_or_create_datasource(self, name):
if name not in self.datasources:
data = self.data.get_data(name)
if type(data) in (list, tuple):
data = array(data)
if isinstance(data, np.ndarray):
if len(data.shape) == 1:
ds = ArrayDataSource(data, sort_order="none")
elif len(data.shape) == 2:
ds = ImageData(data=data, value_depth=1)
elif len(data.shape) == 3:
if data.shape[2] in (3,4):
ds = ImageData(data=data, value_depth=int(data.shape[2]))
else:
raise ValueError("Unhandled array shape in creating new plot: " \
+ str(data.shape))
elif isinstance(data, AbstractDataSource):
ds = data
else:
raise ValueError("Couldn't create datasource for data of type " + \
str(type(data)))
self.datasources[name] = ds
return self.datasources[name]
def get_screen_points(self):
self._gather_points()
return self._downsample()
def _data_changed(self):
self.invalidate_draw()
self._cache_valid = False
self._screen_cache_valid = False
self.request_redraw()
def _gather_points(self):
if not self._cache_valid:
range = self.index_mapper.range
#if self.reference == 'most_recent':
# values, t_lb, t_ub = self.get_recent_range(range.low, range.high)
#else:
# values, t_lb, t_ub = self.get_range(range.low, range.high, -1)
values, t_lb, t_ub = self.data[self.value_name][range.low:range.high], range.low, range.high
#if self.ch_index is None:
# self._cached_data = values
#else:
# #self._cached_data = values[:,self.ch_index]
self._cached_data = values
self._cached_data_bounds = t_lb, t_ub
self._cache_valid = True
self._screen_cache_valid = False
def _downsample(self):
if not self._screen_cache_valid:
val_pts = self._cached_data
screen_min, screen_max = self.index_mapper.screen_bounds
screen_width = screen_max-screen_min
values = decimate(val_pts, screen_width, self.downsampling_cutoff,
self.decimate_mode)
if type(values) == type(()):
n = len(values[0])
s_val_min = self.value_mapper.map_screen(values[0])
s_val_max = self.value_mapper.map_screen(values[1])
self._cached_screen_data = s_val_min, s_val_max
else:
s_val_pts = self.value_mapper.map_screen(values)
self._cached_screen_data = s_val_pts
n = len(values)
t = np.linspace(*self._cached_data_bounds, num=n)
t_screen = self.index_mapper.map_screen(t)
self._cached_screen_index = t_screen
self._screen_cache_valid = True
return [self._cached_screen_index, self._cached_screen_data]
def _render(self, gc, points):
idx, val = points
if len(idx) == 0:
return
gc.save_state()
gc.set_antialias(True)
gc.clip_to_rect(self.x, self.y, self.width, self.height)
gc.set_stroke_color(self.color_)
gc.set_line_width(self.line_width)
#gc.set_line_width(5)
gc.begin_path()
#if len(val) == 2:
if type(val) == type(()):
starts = np.column_stack((idx, val[0]))
ends = np.column_stack((idx, val[1]))
gc.line_set(starts, ends)
else:
gc.lines(np.column_stack((idx, val)))
gc.stroke_path()
self._draw_default_axes(gc)
gc.restore_state()
if __name__ == "__main__":
ChacoTest().configure_traits()