{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n# SVG Histogram\n\nDemonstrate how to create an interactive histogram, in which bars\nare hidden or shown by clicking on legend markers.\n\nThe interactivity is encoded in ecmascript (javascript) and inserted in\nthe SVG code in a post-processing step. To render the image, open it in\na web browser. SVG is supported in most web browsers used by Linux and\nmacOS users. Windows IE9 supports SVG, but earlier versions do not.\n\n## Notes\nThe matplotlib backend lets us assign ids to each object. This is the\nmechanism used here to relate matplotlib objects created in python and\nthe corresponding SVG constructs that are parsed in the second step.\nWhile flexible, ids are cumbersome to use for large collection of\nobjects. Two mechanisms could be used to simplify things:\n\n* systematic grouping of objects into SVG tags,\n* assigning classes to each SVG object according to its origin.\n\nFor example, instead of modifying the properties of each individual bar,\nthe bars from the `~.pyplot.hist` function could either be grouped in\na PatchCollection, or be assigned a class=\"hist_##\" attribute.\n\nCSS could also be used more extensively to replace repetitive markup\nthroughout the generated SVG.\n\nAuthor: david.huard@gmail.com\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "from io import BytesIO\nimport json\nimport xml.etree.ElementTree as ET\n\nimport matplotlib.pyplot as plt\nimport numpy as np\n\nplt.rcParams['svg.fonttype'] = 'none'\n\n# Apparently, this `register_namespace` method is necessary to avoid garbling\n# the XML namespace with ns0.\nET.register_namespace(\"\", \"http://www.w3.org/2000/svg\")\n\n# Fixing random state for reproducibility\nnp.random.seed(19680801)\n\n# --- Create histogram, legend and title ---\nplt.figure()\nr = np.random.randn(100)\nr1 = r + 1\nlabels = ['Rabbits', 'Frogs']\nH = plt.hist([r, r1], label=labels)\ncontainers = H[-1]\nleg = plt.legend(frameon=False)\nplt.title(\"From a web browser, click on the legend\\n\"\n \"marker to toggle the corresponding histogram.\")\n\n\n# --- Add ids to the svg objects we'll modify\n\nhist_patches = {}\nfor ic, c in enumerate(containers):\n hist_patches[f'hist_{ic}'] = []\n for il, element in enumerate(c):\n element.set_gid(f'hist_{ic}_patch_{il}')\n hist_patches[f'hist_{ic}'].append(f'hist_{ic}_patch_{il}')\n\n# Set ids for the legend patches\nfor i, t in enumerate(leg.get_patches()):\n t.set_gid(f'leg_patch_{i}')\n\n# Set ids for the text patches\nfor i, t in enumerate(leg.get_texts()):\n t.set_gid(f'leg_text_{i}')\n\n# Save SVG in a fake file object.\nf = BytesIO()\nplt.savefig(f, format=\"svg\")\n\n# Create XML tree from the SVG file.\ntree, xmlid = ET.XMLID(f.getvalue())\n\n\n# --- Add interactivity ---\n\n# Add attributes to the patch objects.\nfor i, t in enumerate(leg.get_patches()):\n el = xmlid[f'leg_patch_{i}']\n el.set('cursor', 'pointer')\n el.set('onclick', \"toggle_hist(this)\")\n\n# Add attributes to the text objects.\nfor i, t in enumerate(leg.get_texts()):\n el = xmlid[f'leg_text_{i}']\n el.set('cursor', 'pointer')\n el.set('onclick', \"toggle_hist(this)\")\n\n# Create script defining the function `toggle_hist`.\n# We create a global variable `container` that stores the patches id\n# belonging to each histogram. Then a function \"toggle_element\" sets the\n# visibility attribute of all patches of each histogram and the opacity\n# of the marker itself.\n\nscript = \"\"\"\n\n\"\"\" % json.dumps(hist_patches)\n\n# Add a transition effect\ncss = tree.find('.//{http://www.w3.org/2000/svg}style')\ncss.text = css.text + \"g {-webkit-transition:opacity 0.4s ease-out;\" + \\\n \"-moz-transition:opacity 0.4s ease-out;}\"\n\n# Insert the script and save to file.\ntree.insert(0, ET.XML(script))\n\nET.ElementTree(tree).write(\"svg_histogram.svg\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.2" } }, "nbformat": 4, "nbformat_minor": 0 }