├── MANIFEST.in ├── .gitignore ├── LICENSE ├── setup.py ├── tests └── nestedsetrect_test.py ├── README.rst └── matplotlib_subsets └── __init__.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | build/ 3 | dist/ 4 | *.pyc 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Johannes Buchner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | version = '1.0' 4 | long_description = '' 5 | try: 6 | long_description = open("README.rst").read() 7 | except: 8 | pass 9 | 10 | setuptools.setup( 11 | name='matplotlib-subsets', 12 | version=version, 13 | description="Functions for plotting area-proportional hierarchical subset diagrams in matplotlib.", 14 | long_description=long_description, 15 | classifiers=[ # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers 16 | 'Development Status :: 4 - Beta', 17 | 'Intended Audience :: Science/Research', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Operating System :: OS Independent', 20 | 'Programming Language :: Python :: 2', 21 | 'Topic :: Scientific/Engineering :: Visualization' 22 | ], 23 | platforms=['Platform Independent'], 24 | keywords='matplotlib plotting charts venn-diagrams subset tree hierarchy', 25 | author='Johannes Buchner', 26 | author_email='buchner.johannes@gmx.at', 27 | url='https://github.com/JohannesBuchner/matplotlib-subsets', 28 | license='MIT', 29 | packages=['matplotlib_subsets'], 30 | zip_safe=True, 31 | install_requires=['matplotlib', 'numpy', 'scipy'], 32 | ) 33 | 34 | -------------------------------------------------------------------------------- /tests/nestedsetrect_test.py: -------------------------------------------------------------------------------- 1 | from matplotlib_subsets import * 2 | 3 | def test_nested_example1(): 4 | sets = [ 5 | set(list('ABCDEFGH')), 6 | set(list('DEFG')), 7 | set(list('E')), 8 | ] 9 | setsizes = [len(s) for s in sets] 10 | nestedsets_rectangles(setsizes, labels = [ 11 | r'$\mathbf{%s}$ ($%d$)' % (string.ascii_uppercase[i], len(s)) 12 | for i, s in enumerate(sets)]) 13 | plt.savefig('example_nested.pdf', bbox_inches='tight') 14 | plt.close() 15 | 16 | def test_tree_example1(): 17 | tree = ((120, '100', None), [ 18 | ((50, 'A50', None), []), 19 | ((50, 'B50', None), []) 20 | ]) 21 | 22 | treesets_rectangles(tree) 23 | plt.savefig('example_tree.pdf', bbox_inches='tight') 24 | plt.close() 25 | 26 | def test_tree_example2(): 27 | tree = ((120, '100', None), 28 | [((50, 'A50', None), 29 | [((20, 'AX20', None), [((8, 'AXM8', None), [((4, 'AXM4', None), [((2, 'AXM2', None), [])])]), ((8, 'AXN8', None), [])]), 30 | ((20, 'AY20', None), [])]), 31 | ((50, 'B50', None), [((5, 'Bt', None), [])]*5) 32 | ]) 33 | 34 | plt.figure(figsize=(7,7)) 35 | treesets_rectangles(tree) 36 | plt.savefig('example_tree2.pdf', bbox_inches='tight') 37 | plt.close() 38 | 39 | if __name__ == '__main__': 40 | test_nested_example1() 41 | test_tree_example1() 42 | test_tree_example2() 43 | 44 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================================================== 2 | Hierarchical subset diagram plotting for Python/Matplotlib 3 | ============================================================== 4 | 5 | Routines for plotting area-weighted diagrams of subsets and subsets of subsets. 6 | This consistutes an extension to Venn diagrams in some sense (hierarchy), while a limitation 7 | in another (subsets only). 8 | 9 | Installation 10 | ------------ 11 | 12 | The simplest way to install the package is via ``easy_install`` or ``pip``:: 13 | 14 | $ easy_install matplotlib-subsets 15 | 16 | Dependencies 17 | ------------ 18 | 19 | - ``numpy``, ``scipy``, ``matplotlib``. 20 | 21 | Usage 22 | ----- 23 | The package provides the function: ``treesets_rectangles``. 24 | 25 | It takes a tree, where each node is defined as ((number-of-items-contained, 26 | label, dictionary-of-plotting-attributes), [child nodes...]). 27 | 28 | For example:: 29 | 30 | tree = ((120, '120', None), [ 31 | ((50, 'A50', None), []), 32 | ((50, 'B50', None), []) 33 | ]) 34 | 35 | treesets_rectangles(tree) 36 | plt.savefig('example_tree.pdf', bbox_inches='tight') 37 | plt.close() 38 | 39 | Here, the node '120' is of size 120. It has two subsets, 'A50' and 'B50', each of size 50. 40 | No additional plotting attributes are given (e.g. the color of rectangles is chosen automatically). 41 | 42 | See also 43 | -------- 44 | 45 | * Report issues and submit fixes at Github: https://github.com/JohannesBuchner/matplotlib-subsets 46 | * Venn diagram package: https://github.com/konstantint/matplotlib-venn 47 | 48 | -------------------------------------------------------------------------------- /matplotlib_subsets/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Subset diagram plotting routines. 3 | 4 | Copyright 2013, Johannes Buchner. 5 | 6 | Licensed under MIT license. 7 | ''' 8 | import numpy 9 | import warnings 10 | import string 11 | 12 | import matplotlib.pyplot as plt 13 | import matplotlib.transforms as mtransforms 14 | from matplotlib.patches import FancyBboxPatch 15 | import itertools 16 | import scipy.stats 17 | 18 | default_attrs = [dict(ec=c, fc='None', alpha=0.7) for c in ['black', 'blue', 'red', 'green', 'magenta']] 19 | default_attrs = itertools.cycle(default_attrs) 20 | 21 | def plot_box(bbox, label='', attrs={}): 22 | ((xmin, ymin), (xmax, ymax)) = bbox.get_points() 23 | if attrs is None: 24 | attrs = default_attrs.next() 25 | print 'picked attrs:', attrs 26 | 27 | ax = plt.gca() 28 | print 'plotting %s at' % label, ((xmin, ymin), (xmax, ymax)) 29 | p_fancy = FancyBboxPatch((xmin, ymin), 30 | xmax - xmin, ymax - ymin, 31 | boxstyle="round,pad=0.0, rounding_size=0.02", 32 | **attrs) 33 | ax.add_patch(p_fancy) 34 | ax.text(xmin + 0.01, ymax - 0.01, label, 35 | horizontalalignment='left', 36 | verticalalignment='top', 37 | ) 38 | 39 | def nodesets_rectangles((node, children), bbox): 40 | # plot what the children make out in the bbox 41 | nodesize, nodelabel, nodeattrs = node 42 | 43 | nchildren = len(children) 44 | 45 | print 'node', node 46 | print ' has %d children:' % nchildren 47 | for i, c in enumerate(children): 48 | print ' %i: %s' % (i, c) 49 | ((xmin0, ymin0), (xmax0, ymax0)) = bbox.get_points() 50 | deltay = ymax0 - ymin0 51 | deltax = xmax0 - xmin0 52 | if nchildren > 0: 53 | nsizechildren = sum([size for (size, label, attrs), grandchildren in children]) 54 | empty = 1 - nsizechildren * 1. / nodesize 55 | fbuffer = (empty)**0.5 56 | yratio = deltay * (nsizechildren * 1. / nodesize) / deltax 57 | #yratio = deltax / deltax * (nsizechildren * 1. / nodesize) 58 | 59 | #padding = arearatio**0.5 / 10 60 | #shorteningratio = 61 | #fbuffer = 0 62 | print 'yratio %s:' % str(node), yratio 63 | print 'deltax, deltay:', deltax, deltay 64 | for child, grandchildren in children: 65 | ((xmin, ymin), (xmax, ymax)) = bbox.get_points() 66 | 67 | size, label, attrs = child 68 | arearatio = size * 1. / nodesize 69 | print 'arearatio of child:', arearatio 70 | if arearatio == 0: 71 | # skip empty children 72 | continue 73 | 74 | if nchildren == 1: 75 | # update borders, proportional to area 76 | print 'single subset: arearatio', arearatio 77 | # along a beta distribution; cdf is area 78 | a, b = 10, 1 79 | rv = scipy.stats.beta(a, b) 80 | fx = rv.ppf(arearatio) 81 | fy = arearatio / fx 82 | print 'fx, fy:', fx, fy 83 | # add padding if possible 84 | ypad = min(fy*0.02, 1 - fy) 85 | xpad = min(fx*0.02, 1 - fx) 86 | 87 | ymax = ymax - deltay * (1 - (fy + ypad)) 88 | xmin = xmin + deltax * (1 - (fx + xpad)) 89 | ymin = ymin + deltay * ypad 90 | xmax = xmax - deltax * xpad 91 | else: 92 | # update borders, split dependent on xratio 93 | # we prefer to split in y (because label text flows in x) 94 | # but split if ratio is too extreme 95 | if yratio > 0.4: # split in y 96 | print 'splitting in y: starting box', ((xmin0, ymin0), (xmax0, ymax0)) 97 | #ymin, ymax = ymax0 - deltay * arearatio + 0.*deltay * fbuffer / 2, ymax0 98 | #ymax0, ymin0 = ymax0 - deltay * arearatio - 0.*deltay * fbuffer / 2, ymin0 99 | ymin, ymax = ymin0, ymin0 + deltay * arearatio - 1.*deltay * fbuffer / 40 100 | ymax0, ymin0 = ymax0, ymin0 + deltay * arearatio + 1.*deltay * fbuffer / 40 101 | #ymax0 = ymax0 - deltay * arearatio - deltay * fbuffer / 2 102 | #ymin = ymax0 + deltay * fbuffer / 2 103 | #xmax, xmin = xmax0, xmin0 104 | print 'splitting in y: child box', ((xmin, ymin), (xmax, ymax)) 105 | print 'splitting in y: remaining box', ((xmin0, ymin0), (xmax0, ymax0)) 106 | else: 107 | print 'splitting in x: starting box', ((xmin0, ymin0), (xmax0, ymax0)) 108 | #xmin, xmax = xmin0, xmin0 + deltay * arearatio - 1.*deltax * fbuffer / 80 109 | xmin, xmax = xmax0 - deltax * arearatio + deltax * fbuffer / 40, xmax0 110 | xmax0, xmin0 = xmax0 - deltax * arearatio - deltax * fbuffer / 40, xmin0 111 | 112 | print 'splitting in x: child box', ((xmin, ymin), (xmax, ymax)) 113 | print 'splitting in x: remaining box', ((xmin0, ymin0), (xmax0, ymax0)) 114 | # recurse 115 | childbox = mtransforms.Bbox(((xmin, ymin), (xmax, ymax))) 116 | nodesets_rectangles((child, grandchildren), childbox) 117 | plot_box(bbox=childbox, attrs=attrs, label=label) 118 | 119 | def treesets_rectangles(tree): 120 | ((xmin, xmax), (ymin, ymax)) = [(0, 1), (0, 1)] 121 | superset = None 122 | ax = plt.gca() 123 | ax.set_aspect(1.) 124 | ax.set_xlim(-0.1, 1.1) 125 | ax.set_ylim(-0.1, 1.1) 126 | ax.set_xticks([]) 127 | ax.set_yticks([]) 128 | plt.axis('off') 129 | # start with plotting root node 130 | root, children = tree 131 | size, label, attrs = root 132 | assert size > 0 133 | rootbox = mtransforms.Bbox([[xmin, ymin], [xmax, ymax]]) 134 | nodesets_rectangles((root, children), rootbox) 135 | plot_box(bbox=rootbox, attrs=attrs, label=label) 136 | 137 | def nestedsets_rectangles(setsizes, labels = None, attrs = None): 138 | nsets = len(setsizes) 139 | if labels is None: 140 | labels = list(string.ascii_uppercase)[:nsets] 141 | if attrs is None: 142 | attrs = [default_attrs.next() for i in range(nsets)] 143 | #itertools.cycle([dict(ec=c, fc='None', alpha=0.7) 144 | # for c in ['black', 'blue', 'red', 'green', 'magenta']]) 145 | 146 | tree = [] 147 | for node in list(zip(setsizes, labels, attrs))[::-1]: 148 | tree = [[node, tree]] 149 | treesets_rectangles(tree[0]) 150 | 151 | 152 | 153 | --------------------------------------------------------------------------------