Skip to content

Commit caa63c9

Browse files
committed
Merge pull request #432 from mdboom/svg_references
Svg references
2 parents 7d79a3b + 62d3de5 commit caa63c9

File tree

2 files changed

+172
-20
lines changed

2 files changed

+172
-20
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env python
2+
#-*- encoding:utf-8 -*-
3+
4+
"""
5+
Demonstrate how to create an interactive histogram, in which bars
6+
are hidden or shown by cliking on legend markers.
7+
8+
The interactivity is encoded in ecmascript and inserted in the SVG code
9+
in a post-processing step. To render the image, open it in a web
10+
browser. SVG is supported in most web browsers used by Linux and OSX
11+
users. Windows IE9 supports SVG, but earlier versions do not.
12+
13+
__author__="david.huard@gmail.com"
14+
15+
"""
16+
17+
import numpy as np
18+
import matplotlib.pyplot as plt
19+
import xml.etree.ElementTree as ET
20+
from StringIO import StringIO
21+
22+
plt.rcParams['svg.embed_char_paths'] = 'none'
23+
24+
# Apparently, this `register_namespace` method works only with
25+
# python 2.7 and up and is necessary to avoid garbling the XML name
26+
# space with ns0.
27+
ET.register_namespace("","http://www.w3.org/2000/svg")
28+
29+
30+
def python2js(d):
31+
"""Return a string representation of a python dictionary in
32+
ecmascript object syntax."""
33+
34+
objs = []
35+
for key, value in d.items():
36+
objs.append( key + ':' + str(value) )
37+
38+
return '{' + ', '.join(objs) + '}'
39+
40+
41+
# --- Create histogram, legend and title ---
42+
plt.figure()
43+
r = np.random.randn(100)
44+
r1 = r + 1
45+
labels = ['Rabbits', 'Frogs']
46+
H = plt.hist([r,r1], label=labels)
47+
containers = H[-1]
48+
leg = plt.legend(frameon=False)
49+
plt.title("""From a web browser, click on the legend
50+
marker to toggle the corresponding histogram.""")
51+
52+
53+
# --- Add ids to the svg objects we'll modify
54+
55+
hist_patches = {}
56+
for ic, c in enumerate(containers):
57+
hist_patches['hist_%d'%ic] = []
58+
for il, element in enumerate(c):
59+
element.set_gid('hist_%d_patch_%d'%(ic, il))
60+
hist_patches['hist_%d'%ic].append('hist_%d_patch_%d'%(ic,il))
61+
62+
# Set ids for the legend patches
63+
for i, t in enumerate(leg.get_patches()):
64+
t.set_gid('leg_patch_%d'%i)
65+
66+
# Save SVG in a fake file object.
67+
f = StringIO()
68+
plt.savefig(f, format="svg")
69+
70+
# Create XML tree from the SVG file.
71+
tree, xmlid = ET.XMLID(f.getvalue())
72+
73+
74+
# --- Add interactivity ---
75+
76+
# Add attributes to the patch objects.
77+
for i, t in enumerate(leg.get_patches()):
78+
el = xmlid['leg_patch_%d'%i]
79+
el.set('cursor', 'pointer')
80+
el.set('opacity', '1.0')
81+
el.set('onclick', "toggle_element(evt, 'hist_%d')"%i)
82+
83+
# Create script defining the function `toggle_element`.
84+
# We create a global variable `container` that stores the patches id
85+
# belonging to each histogram. Then a function "toggle_element" sets the
86+
# visibility attribute of all patches of each histogram and the opacity
87+
# of the marker itself.
88+
89+
script = """
90+
<script type="text/ecmascript">
91+
<![CDATA[
92+
var container = %s
93+
94+
function toggle_element(evt, element) {
95+
96+
var names = container[element]
97+
var el, state;
98+
99+
state = evt.target.getAttribute("opacity") == 1.0 ||
100+
evt.target.getAttribute("opacity") == null;
101+
102+
if (state) {
103+
evt.target.setAttribute("opacity", 0.5);
104+
105+
for (var i=0; i < names.length; i++) {
106+
el = document.getElementById(names[i]);
107+
el.setAttribute("visibility","hidden");
108+
}
109+
}
110+
111+
else {
112+
evt.target.setAttribute("opacity", 1);
113+
114+
for (var i=0; i < names.length; i++) {
115+
el = document.getElementById(names[i]);
116+
el.setAttribute("visibility","visible");
117+
}
118+
119+
};
120+
};
121+
]]>
122+
</script>
123+
"""%python2js(hist_patches)
124+
125+
# Insert the script and save to file.
126+
tree.insert(0, ET.XML(script))
127+
128+
ET.ElementTree(tree).write("svg_histogram.svg")
129+
130+
131+

lib/matplotlib/backends/backend_svg.py

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ def __init__(self, width, height, svgwriter, basename=None):
279279
self._write_default_style()
280280

281281
def finalize(self):
282+
self._write_clips()
283+
self._write_hatches()
282284
self._write_svgfonts()
283285
self.writer.close(self._start_id)
284286

@@ -321,26 +323,35 @@ def _get_hatch(self, gc, rgbFace):
321323
"""
322324
Create a new hatch pattern
323325
"""
324-
writer = self.writer
325-
HATCH_SIZE = 72
326326
dictkey = (gc.get_hatch(), rgbFace, gc.get_rgb())
327327
oid = self._hatchd.get(dictkey)
328328
if oid is None:
329329
oid = self._make_id('h', dictkey)
330-
writer.start('defs')
330+
self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, gc.get_rgb()), oid)
331+
else:
332+
_, oid = oid
333+
return oid
334+
335+
def _write_hatches(self):
336+
if not len(self._hatchd):
337+
return
338+
HATCH_SIZE = 72
339+
writer = self.writer
340+
writer.start('defs')
341+
for ((path, face, stroke), oid) in self._hatchd.values():
331342
writer.start(
332343
'pattern',
333344
id=oid,
334345
patternUnits="userSpaceOnUse",
335346
x="0", y="0", width=str(HATCH_SIZE), height=str(HATCH_SIZE))
336347
path_data = self._convert_path(
337-
gc.get_hatch_path(),
348+
path,
338349
Affine2D().scale(HATCH_SIZE).scale(1.0, -1.0).translate(0, HATCH_SIZE),
339350
simplify=False)
340-
if rgbFace is None:
351+
if face is None:
341352
fill = 'none'
342353
else:
343-
fill = rgb2hex(rgbFace)
354+
fill = rgb2hex(face)
344355
writer.element(
345356
'rect',
346357
x="0", y="0", width=str(HATCH_SIZE+1), height=str(HATCH_SIZE+1),
@@ -349,17 +360,15 @@ def _get_hatch(self, gc, rgbFace):
349360
'path',
350361
d=path_data,
351362
style=generate_css({
352-
'fill': rgb2hex(gc.get_rgb()),
353-
'stroke': rgb2hex(gc.get_rgb()),
363+
'fill': rgb2hex(stroke),
364+
'stroke': rgb2hex(stroke),
354365
'stroke-width': str(1.0),
355366
'stroke-linecap': 'butt',
356367
'stroke-linejoin': 'miter'
357368
})
358369
)
359370
writer.end('pattern')
360-
writer.end('defs')
361-
self._hatchd[dictkey] = oid
362-
return oid
371+
writer.end('defs')
363372

364373
def _get_style(self, gc, rgbFace):
365374
"""
@@ -409,22 +418,34 @@ def _get_clip(self, gc):
409418
else:
410419
return None
411420

412-
oid = self._clipd.get(dictkey)
413-
if oid is None:
414-
writer = self.writer
421+
clip = self._clipd.get(dictkey)
422+
if clip is None:
415423
oid = self._make_id('p', dictkey)
416-
writer.start('defs')
417-
writer.start('clipPath', id=oid)
418424
if clippath is not None:
425+
self._clipd[dictkey] = ((clippath, clippath_trans), oid)
426+
else:
427+
self._clipd[dictkey] = (dictkey, oid)
428+
else:
429+
clip, oid = clip
430+
return oid
431+
432+
def _write_clips(self):
433+
if not len(self._clipd):
434+
return
435+
writer = self.writer
436+
writer.start('defs')
437+
for clip, oid in self._clipd.values():
438+
writer.start('clipPath', id=oid)
439+
if len(clip) == 2:
440+
clippath, clippath_trans = clip
419441
path_data = self._convert_path(clippath, clippath_trans, simplify=False)
420442
writer.element('path', d=path_data)
421443
else:
444+
x, y, w, h = clip
422445
writer.element('rect', x=str(x), y=str(y), width=str(w), height=str(h))
423446
writer.end('clipPath')
424-
writer.end('defs')
425-
self._clipd[dictkey] = oid
426-
return oid
427-
447+
writer.end('defs')
448+
428449
def _write_svgfonts(self):
429450
if not rcParams['svg.fonttype'] == 'svgfont':
430451
return

0 commit comments

Comments
 (0)