diff --git a/examples/user_interfaces/svg_histogram.py b/examples/user_interfaces/svg_histogram.py new file mode 100755 index 000000000000..8763bbec2f17 --- /dev/null +++ b/examples/user_interfaces/svg_histogram.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +#-*- encoding:utf-8 -*- + +""" +Demonstrate how to create an interactive histogram, in which bars +are hidden or shown by cliking on legend markers. + +The interactivity is encoded in ecmascript and inserted in the SVG code +in a post-processing step. To render the image, open it in a web +browser. SVG is supported in most web browsers used by Linux and OSX +users. Windows IE9 supports SVG, but earlier versions do not. + +__author__="david.huard@gmail.com" + +""" + +import numpy as np +import matplotlib.pyplot as plt +import xml.etree.ElementTree as ET +from StringIO import StringIO + +plt.rcParams['svg.embed_char_paths'] = 'none' + +# Apparently, this `register_namespace` method works only with +# python 2.7 and up and is necessary to avoid garbling the XML name +# space with ns0. +ET.register_namespace("","http://www.w3.org/2000/svg") + + +def python2js(d): + """Return a string representation of a python dictionary in + ecmascript object syntax.""" + + objs = [] + for key, value in d.items(): + objs.append( key + ':' + str(value) ) + + return '{' + ', '.join(objs) + '}' + + +# --- Create histogram, legend and title --- +plt.figure() +r = np.random.randn(100) +r1 = r + 1 +labels = ['Rabbits', 'Frogs'] +H = plt.hist([r,r1], label=labels) +containers = H[-1] +leg = plt.legend(frameon=False) +plt.title("""From a web browser, click on the legend +marker to toggle the corresponding histogram.""") + + +# --- Add ids to the svg objects we'll modify + +hist_patches = {} +for ic, c in enumerate(containers): + hist_patches['hist_%d'%ic] = [] + for il, element in enumerate(c): + element.set_gid('hist_%d_patch_%d'%(ic, il)) + hist_patches['hist_%d'%ic].append('hist_%d_patch_%d'%(ic,il)) + +# Set ids for the legend patches +for i, t in enumerate(leg.get_patches()): + t.set_gid('leg_patch_%d'%i) + +# Save SVG in a fake file object. +f = StringIO() +plt.savefig(f, format="svg") + +# Create XML tree from the SVG file. +tree, xmlid = ET.XMLID(f.getvalue()) + + +# --- Add interactivity --- + +# Add attributes to the patch objects. +for i, t in enumerate(leg.get_patches()): + el = xmlid['leg_patch_%d'%i] + el.set('cursor', 'pointer') + el.set('opacity', '1.0') + el.set('onclick', "toggle_element(evt, 'hist_%d')"%i) + +# Create script defining the function `toggle_element`. +# We create a global variable `container` that stores the patches id +# belonging to each histogram. Then a function "toggle_element" sets the +# visibility attribute of all patches of each histogram and the opacity +# of the marker itself. + +script = """ + +"""%python2js(hist_patches) + +# Insert the script and save to file. +tree.insert(0, ET.XML(script)) + +ET.ElementTree(tree).write("svg_histogram.svg") + + + diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 53e7346e2d70..661a7bcb221f 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -279,6 +279,8 @@ def __init__(self, width, height, svgwriter, basename=None): self._write_default_style() def finalize(self): + self._write_clips() + self._write_hatches() self._write_svgfonts() self.writer.close(self._start_id) @@ -321,26 +323,35 @@ def _get_hatch(self, gc, rgbFace): """ Create a new hatch pattern """ - writer = self.writer - HATCH_SIZE = 72 dictkey = (gc.get_hatch(), rgbFace, gc.get_rgb()) oid = self._hatchd.get(dictkey) if oid is None: oid = self._make_id('h', dictkey) - writer.start('defs') + self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, gc.get_rgb()), oid) + else: + _, oid = oid + return oid + + def _write_hatches(self): + if not len(self._hatchd): + return + HATCH_SIZE = 72 + writer = self.writer + writer.start('defs') + for ((path, face, stroke), oid) in self._hatchd.values(): writer.start( 'pattern', id=oid, patternUnits="userSpaceOnUse", x="0", y="0", width=str(HATCH_SIZE), height=str(HATCH_SIZE)) path_data = self._convert_path( - gc.get_hatch_path(), + path, Affine2D().scale(HATCH_SIZE).scale(1.0, -1.0).translate(0, HATCH_SIZE), simplify=False) - if rgbFace is None: + if face is None: fill = 'none' else: - fill = rgb2hex(rgbFace) + fill = rgb2hex(face) writer.element( 'rect', x="0", y="0", width=str(HATCH_SIZE+1), height=str(HATCH_SIZE+1), @@ -349,17 +360,15 @@ def _get_hatch(self, gc, rgbFace): 'path', d=path_data, style=generate_css({ - 'fill': rgb2hex(gc.get_rgb()), - 'stroke': rgb2hex(gc.get_rgb()), + 'fill': rgb2hex(stroke), + 'stroke': rgb2hex(stroke), 'stroke-width': str(1.0), 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter' }) ) writer.end('pattern') - writer.end('defs') - self._hatchd[dictkey] = oid - return oid + writer.end('defs') def _get_style(self, gc, rgbFace): """ @@ -409,22 +418,34 @@ def _get_clip(self, gc): else: return None - oid = self._clipd.get(dictkey) - if oid is None: - writer = self.writer + clip = self._clipd.get(dictkey) + if clip is None: oid = self._make_id('p', dictkey) - writer.start('defs') - writer.start('clipPath', id=oid) if clippath is not None: + self._clipd[dictkey] = ((clippath, clippath_trans), oid) + else: + self._clipd[dictkey] = (dictkey, oid) + else: + clip, oid = clip + return oid + + def _write_clips(self): + if not len(self._clipd): + return + writer = self.writer + writer.start('defs') + for clip, oid in self._clipd.values(): + writer.start('clipPath', id=oid) + if len(clip) == 2: + clippath, clippath_trans = clip path_data = self._convert_path(clippath, clippath_trans, simplify=False) writer.element('path', d=path_data) else: + x, y, w, h = clip writer.element('rect', x=str(x), y=str(y), width=str(w), height=str(h)) writer.end('clipPath') - writer.end('defs') - self._clipd[dictkey] = oid - return oid - + writer.end('defs') + def _write_svgfonts(self): if not rcParams['svg.fonttype'] == 'svgfont': return