Kantha Girish
Kantha Girish

Reputation: 67

Python matplotlib: Error while saving scatter plot in svg format

I am trying to create a scatter plot with the color for each point based on the value in y-dimension and the tooltip for each point based on the value of x-axis. I was in need of creating tooltips for each point on mouseover event, which I was able to achieve. I would like to save this plot to a svg file with events so that the svg file can be viewed in a browser with tooltip.

I get the following error when I try to save it,

Traceback (most recent call last):


File "./altered_tooltip.py", line 160, in <module>
    example.plot()
  File "./altered_tooltip.py", line 70, in plot
    pl.savefig(f, format="svg")
  File "/usr/lib/pymodules/python2.7/matplotlib/pyplot.py", line 561, in savefig
    return fig.savefig(*args, **kwargs)
  File "/usr/lib/pymodules/python2.7/matplotlib/figure.py", line 1421, in savefig
    self.canvas.print_figure(*args, **kwargs)
  File "/usr/lib/pymodules/python2.7/matplotlib/backend_bases.py", line 2220, in print_figure
    **kwargs)
  File "/usr/lib/pymodules/python2.7/matplotlib/backend_bases.py", line 1978, in print_svg
    return svg.print_svg(*args, **kwargs)
  File "/usr/lib/pymodules/python2.7/matplotlib/backends/backend_svg.py", line 1157, in print_svg
    return self._print_svg(filename, svgwriter, fh_to_close, **kwargs)
  File "/usr/lib/pymodules/python2.7/matplotlib/backends/backend_svg.py", line 1185, in _print_svg
    self.figure.draw(renderer)
  File "/usr/lib/pymodules/python2.7/matplotlib/artist.py", line 55, in draw_wrapper
    draw(artist, renderer, *args, **kwargs)
  File "/usr/lib/pymodules/python2.7/matplotlib/figure.py", line 1034, in draw
    func(*args)
  File "/usr/lib/pymodules/python2.7/matplotlib/artist.py", line 55, in draw_wrapper
    draw(artist, renderer, *args, **kwargs)
  File "/usr/lib/pymodules/python2.7/matplotlib/axes.py", line 2086, in draw
    a.draw(renderer)
  File "/usr/lib/pymodules/python2.7/matplotlib/artist.py", line 55, in draw_wrapper
    draw(artist, renderer, *args, **kwargs)
  File "/usr/lib/pymodules/python2.7/matplotlib/collections.py", line 718, in draw
    return Collection.draw(self, renderer)
  File "/usr/lib/pymodules/python2.7/matplotlib/artist.py", line 55, in draw_wrapper
    draw(artist, renderer, *args, **kwargs)
  File "/usr/lib/pymodules/python2.7/matplotlib/collections.py", line 250, in draw
    renderer.open_group(self.__class__.__name__, self.get_gid())
  File "/usr/lib/pymodules/python2.7/matplotlib/backends/backend_svg.py", line 516, in open_group
    self.writer.start('g', id=gid)
  File "/usr/lib/pymodules/python2.7/matplotlib/backends/backend_svg.py", line 141, in start
    v = escape_attrib(v)
  File "/usr/lib/pymodules/python2.7/matplotlib/backends/backend_svg.py", line 80, in escape_attrib
    s = s.replace(u"&", u"&amp;")
AttributeError: 'list' object has no attribute 'replace'

The code that I am running is

from numpy import *
import pylab as pl
import matplotlib.colors as mcolors
import xml.etree.ElementTree as ET
from StringIO import StringIO

ET.register_namespace("","http://www.w3.org/2000/svg")

class wxToolTipExample(object):

    def __init__(self, plot_data):
        self.figure = pl.figure()
        self.axis = pl.axes()
        self.ax = self.figure.add_subplot(111)

        self.tooltip.SetTip() calls
        self.dataX = plot_data[:,0]
        self.dataY = plot_data[:,1]
        self.annotes = []
        self.pathCol = None #to hold the scatter object

    def plot(self):

        for idx in arange(self.dataX.size):
            # create a tuple of co-ordinates
            tup = (self.dataX[idx], self.dataY[idx])

            # create annotation with tooltip
            annotation = self.axis.annotate("Column %s" % int(self.dataX[idx]),
                xy=tup, xycoords='data',
                xytext=(+10, +30), textcoords='offset points',
                #horizontalalignment="right",
                arrowprops=dict(arrowstyle="->",
                                connectionstyle="arc3,rad=0.2"),
                #bbox=dict(boxstyle="round", facecolor="w", 
                 #         edgecolor="0.0", alpha=0.0)
                )

            # by default, disable the annotation visibility
            annotation.set_visible(False)

            # append the annotation object and co-ords tuple to the list
            self.annotes.append([tup, annotation])

        self.figure.canvas.mpl_connect('motion_notify_event', self._onMotion)
        c_map = self.WGrYR()
        self.pathCol = self.axis.scatter(self.dataX, self.dataY, c=self.dataY, marker='s', s=40, cmap=c_map)

        # Set id for the annotations
        for i, t in enumerate(self.axis.texts):
            t.set_gid('tooltip_%d'%i)

        # Set id for the points on the scatter plot
        points = ['point_{0}'.format(ii) for ii in arange(1, self.dataX.size+1)]
        self.pathCol.set_gid(points)

        f = StringIO()
        #pl.show()
        pl.savefig(f, format="svg")
        """
        # Create XML tree from the SVG file.
        tree, xmlid = ET.XMLID(f.getvalue())
        tree.set('onload', 'init(evt)')

        # Hide the tooltips
        for i, t in enumerate(self.axis.texts):
            ele = xmlid['tooltip_%d'%i]
            ele.set('visibility','hidden')

        # assign mouseover and mouseout events
        for p in points:
            ele = xmlid[p]
            ele.set('onmouseover', "ShowTooltip(this)")
            ele.set('onmouseout', "HideTooltip(this)")

        script = self.getSvgScript()

        # Insert the script at the top of the file and save it.
        tree.insert(0, ET.XML(script))
        ET.ElementTree(tree).write('svg_tooltip.svg')
        """
    def getSvgScript(self):
        return """
            <script type="text/ecmascript">
            <![CDATA[

            function init(evt) {
                if ( window.svgDocument == null ) {
                    svgDocument = evt.target.ownerDocument;
                    }
                }

            function ShowTooltip(obj) {
                var cur = obj.id.slice(-1);

                var tip = svgDocument.getElementById('tooltip_' + cur);
                tip.setAttribute('visibility',"visible")
                }

            function HideTooltip(obj) {
                var cur = obj.id.slice(-1);
                var tip = svgDocument.getElementById('tooltip_' + cur);
                tip.setAttribute('visibility',"hidden")
                }

            ]]>
            </script>
            """

    def _onMotion(self, event):
        visibility_changed = False
        for point, annotation in self.annotes:
            if event.xdata != None and event.ydata != None: # mouse is inside the axes
                should_be_visible = abs(point[0]-event.xdata) < 0.2 and abs(point[1]-event.ydata) < 0.05

                if should_be_visible != annotation.get_visible():
                    visibility_changed = True
                    annotation.set_visible(should_be_visible)

        if visibility_changed:        
            pl.draw()

    def WGrYR(self):
        c = mcolors.ColorConverter().to_rgb
        seq = [c('white'), c('grey'), 0.33, c('grey'), c('yellow'), 0.66, c('yellow'), c('red')]

        seq = [(None,) * 3, 0.0] + list(seq) + [1.0, (None,) * 3]
        cdict = {'red': [], 'green': [], 'blue': []}
        for i, item in enumerate(seq):
            if isinstance(item, float):
                r1, g1, b1 = seq[i - 1]
                r2, g2, b2 = seq[i + 1]
                cdict['red'].append([item, r1, r2])
                cdict['green'].append([item, g1, g2])
                cdict['blue'].append([item, b1, b2])
        #print cdict
        return mcolors.LinearSegmentedColormap('Custom_WGrYR', cdict)

ET.register_namespace("","http://www.w3.org/2000/svg")

# test column heat for nodes
n_cols = 5
plot_data = zeros((n_cols,2))

# generate column numbers and random heat values to test
plot_data[:,0] = asarray(arange(1, n_cols+1))#.reshape(n_cols,1)
plot_data[:,1] = random.rand(n_cols,1).reshape(n_cols,)
example = wxToolTipExample(plot_data)
example.plot()

Where am I going wrong?

Upvotes: 2

Views: 891

Answers (1)

Jarek Pi&#243;rkowski
Jarek Pi&#243;rkowski

Reputation: 648

Your code looks like a work in progress that hasn't been completely cleaned up. (I had to comment out self.tooltip.SetTip() calls to get it to run.) But here is the cause of your immediate issue with the exception you note, and how I found it:

On my machine, I edited backend_svg.py function start() to add a print(extra) and then I run your code. As a result of your lines:

  points = ['point_{0}'.format(ii) for ii in arange(1, self.dataX.size+1)]
  self.pathCol.set_gid(points)

the matplotlib backend attempts to create an SVG <g> node with the ID: ['point_1', 'point_2', 'point_3', 'point_4', 'point_5']. This is a list, rather than a valid string, so s.replace() fails.

Ultimately you must change your code so that set_gid() only receives string parameters. The simplest way to do that is to change the two lines above to simply:

  self.pathCol.set_gid('point_1')

but then you don't get IDs for individual points in the generated SVG. You could also remove the self.pathCol.set_gid line altogether (pathCol it will be rendered to SVG as <g id="PathCollection_1"> and points will also not have IDs).

It appears that it is not simple to assign SVG IDs to individual points/vertices contained within a pathCollection. If you need to do that, you might need to come up with another way to plot them -- if I understand the problem correctly, you'd need to plot individual points rather than a path.

Upvotes: 1

Related Questions