|
1 | | -#!/usr/bin/env python |
2 | | - |
3 | | -__author__ = "Yannick Copin <[email protected]>" |
4 | | -__version__ = "Time-stamp: <10/02/2010 16:49 [email protected]>" |
5 | | - |
6 | | -import numpy as N |
7 | | - |
8 | | -def sankey(ax, |
9 | | - outputs=[100.], outlabels=None, |
10 | | - inputs=[100.], inlabels='', |
11 | | - dx=40, dy=10, outangle=45, w=3, inangle=30, offset=2, **kwargs): |
12 | | - """Draw a Sankey diagram. |
13 | | -
|
14 | | - outputs: array of outputs, should sum up to 100% |
15 | | - outlabels: output labels (same length as outputs), |
16 | | - or None (use default labels) or '' (no labels) |
17 | | - inputs and inlabels: similar for inputs |
18 | | - dx: horizontal elongation |
19 | | - dy: vertical elongation |
20 | | - outangle: output arrow angle [deg] |
21 | | - w: output arrow shoulder |
22 | | - inangle: input dip angle |
23 | | - offset: text offset |
24 | | - **kwargs: propagated to Patch (e.g. fill=False) |
25 | | -
|
26 | | - Return (patch,[intexts,outtexts]).""" |
27 | | - |
28 | | - import matplotlib.patches as mpatches |
29 | | - from matplotlib.path import Path |
30 | | - |
31 | | - outs = N.absolute(outputs) |
32 | | - outsigns = N.sign(outputs) |
33 | | - outsigns[-1] = 0 # Last output |
34 | | - |
35 | | - ins = N.absolute(inputs) |
36 | | - insigns = N.sign(inputs) |
37 | | - insigns[0] = 0 # First input |
38 | | - |
39 | | - assert sum(outs)==100, "Outputs don't sum up to 100%" |
40 | | - assert sum(ins)==100, "Inputs don't sum up to 100%" |
41 | | - |
42 | | - def add_output(path, loss, sign=1): |
43 | | - h = (loss/2+w)*N.tan(outangle/180.*N.pi) # Arrow tip height |
44 | | - move,(x,y) = path[-1] # Use last point as reference |
45 | | - if sign==0: # Final loss (horizontal) |
46 | | - path.extend([(Path.LINETO,[x+dx,y]), |
47 | | - (Path.LINETO,[x+dx,y+w]), |
48 | | - (Path.LINETO,[x+dx+h,y-loss/2]), # Tip |
49 | | - (Path.LINETO,[x+dx,y-loss-w]), |
50 | | - (Path.LINETO,[x+dx,y-loss])]) |
51 | | - outtips.append((sign,path[-3][1])) |
52 | | - else: # Intermediate loss (vertical) |
53 | | - path.extend([(Path.CURVE4,[x+dx/2,y]), |
54 | | - (Path.CURVE4,[x+dx,y]), |
55 | | - (Path.CURVE4,[x+dx,y+sign*dy]), |
56 | | - (Path.LINETO,[x+dx-w,y+sign*dy]), |
57 | | - (Path.LINETO,[x+dx+loss/2,y+sign*(dy+h)]), # Tip |
58 | | - (Path.LINETO,[x+dx+loss+w,y+sign*dy]), |
59 | | - (Path.LINETO,[x+dx+loss,y+sign*dy]), |
60 | | - (Path.CURVE3,[x+dx+loss,y-sign*loss]), |
61 | | - (Path.CURVE3,[x+dx/2+loss,y-sign*loss])]) |
62 | | - outtips.append((sign,path[-5][1])) |
63 | | - |
64 | | - def add_input(path, gain, sign=1): |
65 | | - h = (gain/2)*N.tan(inangle/180.*N.pi) # Dip depth |
66 | | - move,(x,y) = path[-1] # Use last point as reference |
67 | | - if sign==0: # First gain (horizontal) |
68 | | - path.extend([(Path.LINETO,[x-dx,y]), |
69 | | - (Path.LINETO,[x-dx+h,y+gain/2]), # Dip |
70 | | - (Path.LINETO,[x-dx,y+gain])]) |
71 | | - xd,yd = path[-2][1] # Dip position |
72 | | - indips.append((sign,[xd-h,yd])) |
73 | | - else: # Intermediate gain (vertical) |
74 | | - path.extend([(Path.CURVE4,[x-dx/2,y]), |
75 | | - (Path.CURVE4,[x-dx,y]), |
76 | | - (Path.CURVE4,[x-dx,y+sign*dy]), |
77 | | - (Path.LINETO,[x-dx-gain/2,y+sign*(dy-h)]), # Dip |
78 | | - (Path.LINETO,[x-dx-gain,y+sign*dy]), |
79 | | - (Path.CURVE3,[x-dx-gain,y-sign*gain]), |
80 | | - (Path.CURVE3,[x-dx/2-gain,y-sign*gain])]) |
81 | | - xd,yd = path[-4][1] # Dip position |
82 | | - indips.append((sign,[xd,yd+sign*h])) |
83 | | - |
84 | | - outtips = [] # Output arrow tip dir. and positions |
85 | | - urpath = [(Path.MOVETO,[0,100])] # 1st point of upper right path |
86 | | - lrpath = [(Path.LINETO,[0,0])] # 1st point of lower right path |
87 | | - for loss,sign in zip(outs,outsigns): |
88 | | - add_output(sign>=0 and urpath or lrpath, loss, sign=sign) |
89 | | - |
90 | | - indips = [] # Input arrow tip dir. and positions |
91 | | - llpath = [(Path.LINETO,[0,0])] # 1st point of lower left path |
92 | | - ulpath = [(Path.MOVETO,[0,100])] # 1st point of upper left path |
93 | | - for gain,sign in zip(ins,insigns)[::-1]: |
94 | | - add_input(sign<=0 and llpath or ulpath, gain, sign=sign) |
95 | | - |
96 | | - def revert(path): |
97 | | - """A path is not just revertable by path[::-1] because of Bezier |
98 | | - curves.""" |
99 | | - rpath = [] |
100 | | - nextmove = Path.LINETO |
101 | | - for move,pos in path[::-1]: |
102 | | - rpath.append((nextmove,pos)) |
103 | | - nextmove = move |
104 | | - return rpath |
105 | | - |
106 | | - # Concatenate subpathes in correct order |
107 | | - path = urpath + revert(lrpath) + llpath + revert(ulpath) |
108 | | - |
109 | | - codes,verts = zip(*path) |
110 | | - verts = N.array(verts) |
111 | | - |
112 | | - # Path patch |
113 | | - path = Path(verts,codes) |
114 | | - patch = mpatches.PathPatch(path, **kwargs) |
115 | | - ax.add_patch(patch) |
116 | | - |
117 | | - if False: # DEBUG |
118 | | - print "urpath", urpath |
119 | | - print "lrpath", revert(lrpath) |
120 | | - print "llpath", llpath |
121 | | - print "ulpath", revert(ulpath) |
122 | | - |
123 | | - xs,ys = zip(*verts) |
124 | | - ax.plot(xs,ys,'go-') |
125 | | - |
126 | | - # Labels |
127 | | - |
128 | | - def set_labels(labels,values): |
129 | | - """Set or check labels according to values.""" |
130 | | - if labels=='': # No labels |
131 | | - return labels |
132 | | - elif labels is None: # Default labels |
133 | | - return [ '%2d%%' % val for val in values ] |
134 | | - else: |
135 | | - assert len(labels)==len(values) |
136 | | - return labels |
137 | | - |
138 | | - def put_labels(labels,positions,output=True): |
139 | | - """Put labels to positions.""" |
140 | | - texts = [] |
141 | | - lbls = output and labels or labels[::-1] |
142 | | - for i,label in enumerate(lbls): |
143 | | - s,(x,y) = positions[i] # Label direction and position |
144 | | - if s==0: |
145 | | - t = ax.text(x+offset,y,label, |
146 | | - ha=output and 'left' or 'right', va='center') |
147 | | - elif s>0: |
148 | | - t = ax.text(x,y+offset,label, ha='center', va='bottom') |
149 | | - else: |
150 | | - t = ax.text(x,y-offset,label, ha='center', va='top') |
151 | | - texts.append(t) |
152 | | - return texts |
153 | | - |
154 | | - outlabels = set_labels(outlabels, outs) |
155 | | - outtexts = put_labels(outlabels, outtips, output=True) |
156 | | - |
157 | | - inlabels = set_labels(inlabels, ins) |
158 | | - intexts = put_labels(inlabels, indips, output=False) |
159 | | - |
160 | | - # Axes management |
161 | | - ax.set_xlim(verts[:,0].min()-dx, verts[:,0].max()+dx) |
162 | | - ax.set_ylim(verts[:,1].min()-dy, verts[:,1].max()+dy) |
163 | | - ax.set_aspect('equal', adjustable='datalim') |
164 | | - |
165 | | - return patch,[intexts,outtexts] |
166 | | - |
167 | | -if __name__=='__main__': |
168 | | - |
169 | | - import matplotlib.pyplot as P |
170 | | - |
171 | | - outputs = [10.,-20.,5.,15.,-10.,40.] |
172 | | - outlabels = ['First','Second','Third','Fourth','Fifth','Hurray!'] |
173 | | - outlabels = [ s+'\n%d%%' % abs(l) for l,s in zip(outputs,outlabels) ] |
174 | | - |
175 | | - inputs = [60.,-25.,15.] |
176 | | - |
177 | | - fig = P.figure() |
178 | | - ax = fig.add_subplot(1,1,1, xticks=[],yticks=[], |
179 | | - title="Sankey diagram" |
180 | | - ) |
181 | | - |
182 | | - patch,(intexts,outtexts) = sankey(ax, outputs=outputs, outlabels=outlabels, |
183 | | - inputs=inputs, inlabels=None, |
184 | | - fc='g', alpha=0.2) |
185 | | - outtexts[1].set_color('r') |
186 | | - outtexts[-1].set_fontweight('bold') |
187 | | - |
188 | | - P.show() |
| 1 | +"""Demonstrate the Sankey class. |
| 2 | +""" |
| 3 | +import numpy as np |
| 4 | +import matplotlib.pyplot as plt |
| 5 | + |
| 6 | +from matplotlib.sankey import Sankey |
| 7 | +from itertools import cycle |
| 8 | + |
| 9 | + |
| 10 | +"""Demonstrate the Sankey class. |
| 11 | +""" |
| 12 | +import matplotlib.pyplot as plt |
| 13 | +from itertools import cycle |
| 14 | + |
| 15 | +# Example 1 -- Mostly defaults |
| 16 | +# This demonstrates how to create a simple diagram by implicitly calling the |
| 17 | +# Sankey.add() method and by appending finish() to the call to the class. |
| 18 | +Sankey(flows=[0.25, 0.15, 0.60, -0.20, -0.15, -0.05, -0.50, -0.10], |
| 19 | + labels=['', '', '', 'First', 'Second', 'Third', 'Fourth', 'Fifth'], |
| 20 | + orientations=[-1, 1, 0, 1, 1, 1, 0, -1]).finish() |
| 21 | +plt.title("The default settings produce a diagram like this.") |
| 22 | +# Notice: |
| 23 | +# 1. Axes weren't provided when Sankey() was instantiated, so they were |
| 24 | +# created automatically. |
| 25 | +# 2. The scale argument wasn't necessary since the data was already |
| 26 | +# normalized. |
| 27 | +# 3. By default, the lengths of the paths are justified. |
| 28 | + |
| 29 | +# Example 2 |
| 30 | +# This demonstrates: |
| 31 | +# 1. Setting one path longer than the others |
| 32 | +# 2. Placing a label in the middle of the diagram |
| 33 | +# 3. Using the the scale argument to normalize the flows |
| 34 | +# 4. Implicitly passing keyword arguments to PathPatch() |
| 35 | +# 5. Changing the angle of the arrow heads |
| 36 | +# 6. Changing the offset between the tips of the paths and their labels |
| 37 | +# 7. Formatting the numbers in the path labels and the associated unit |
| 38 | +# 8. Changing the appearance of the patch and the labels after the figure |
| 39 | +# is created |
| 40 | +fig = plt.figure() |
| 41 | +ax = fig.add_subplot(1, 1, 1, xticks=[], yticks=[], |
| 42 | + title="Flow Diagram of a Widget") |
| 43 | +sankey = Sankey(ax=ax, scale=0.01, offset=0.2, head_angle=180, |
| 44 | + format='%.0f', unit='%') |
| 45 | +sankey.add(flows=[25, 0, 60, -10, -20, -5, -15, -10, -40], |
| 46 | + labels = ['', '', '', 'First', 'Second', 'Third', 'Fourth', |
| 47 | + 'Fifth', 'Hurray!'], |
| 48 | + orientations=[-1, 1, 0, 1, 1, 1, -1, -1, 0], |
| 49 | + pathlengths = [0.25, 0.25, 0.25, 0.25, 0.25, 0.6, 0.25, 0.25, |
| 50 | + 0.25], |
| 51 | + patchlabel="Widget\nA", |
| 52 | + alpha=0.2, lw=2.0) # Arguments to matplotlib.patches.PathPatch() |
| 53 | +diagrams = sankey.finish() |
| 54 | +diagrams[0].patch.set_facecolor('#37c959') |
| 55 | +diagrams[0].texts[-1].set_color('r') |
| 56 | +diagrams[0].text.set_fontweight('bold') |
| 57 | +# Without namedtuple: |
| 58 | +#diagrams[0][0].set_facecolor('#37c959') |
| 59 | +#diagrams[0][5][-1].set_color('r') |
| 60 | +#diagrams[0][4].set_fontweight('bold') |
| 61 | +# Notice: |
| 62 | +# 1. Since the sum of the flows isn't zero, the width of the trunk isn't |
| 63 | +# uniform. A message is given in the terminal window. |
| 64 | +# 2. The second flow doesn't appear because its value is zero. A messsage |
| 65 | +# is given in the terminal window. |
| 66 | + |
| 67 | +# Example 3 |
| 68 | +# This demonstrates: |
| 69 | +# 1. Connecting two systems |
| 70 | +# 2. Turning off the labels of the quantities |
| 71 | +# 3. Adding a legend |
| 72 | +fig = plt.figure() |
| 73 | +ax = fig.add_subplot(1, 1, 1, xticks=[], yticks=[], title="Two Systems") |
| 74 | +flows = [0.25, 0.15, 0.60, -0.10, -0.05, -0.25, -0.15, -0.10, -0.35] |
| 75 | +sankey = Sankey(ax=ax, unit=None) |
| 76 | +sankey.add(flows=flows, label='one', |
| 77 | + orientations=[-1, 1, 0, 1, 1, 1, -1, -1, 0]) |
| 78 | +sankey.add(flows=[-0.25, 0.15, 0.1], fc='#37c959', label='two', |
| 79 | + orientations=[-1, -1, -1], prior=0, connect=(0, 0)) |
| 80 | +diagrams = sankey.finish() |
| 81 | +diagrams[-1].patch.set_hatch('/') |
| 82 | +# Without namedtuple: |
| 83 | +#diagrams[-1][0].set_hatch('/') |
| 84 | + |
| 85 | +plt.legend(loc='best') |
| 86 | +# Notice that only one connection is specified, but the systems form a |
| 87 | +# circuit since: (1) the lengths of the paths are justified and (2) the |
| 88 | +# orientation and ordering of the flows is mirrored. |
| 89 | + |
| 90 | +# Example 4 |
| 91 | +# This tests a long chain of connections. |
| 92 | +links_per_side = 6 |
| 93 | +def side(sankey, n=1): |
| 94 | + prior = len(sankey.diagrams) |
| 95 | + colors = cycle(['orange', 'b', 'g', 'r', 'c', 'm', 'y']) |
| 96 | + for i in range(0, 2*n, 2): |
| 97 | + sankey.add(flows=[1, -1], orientations=[-1, -1], |
| 98 | + patchlabel=str(prior+i), facecolor=colors.next(), |
| 99 | + prior=prior+i-1, connect=(1, 0), alpha=0.5) |
| 100 | + sankey.add(flows=[1, -1], orientations=[1, 1], |
| 101 | + patchlabel=str(prior+i+1), facecolor=colors.next(), |
| 102 | + prior=prior+i, connect=(1, 0), alpha=0.5) |
| 103 | +def corner(sankey): |
| 104 | + prior = len(sankey.diagrams) |
| 105 | + sankey.add(flows=[1, -1], orientations=[0, 1], |
| 106 | + patchlabel=str(prior), facecolor='k', |
| 107 | + prior=prior-1, connect=(1, 0), alpha=0.5) |
| 108 | +fig = plt.figure() |
| 109 | +ax = fig.add_subplot(1, 1, 1, xticks=[], yticks=[], |
| 110 | + title="Why would you want to do this?" \ |
| 111 | + "\n(But you could.)") |
| 112 | +sankey = Sankey(ax=ax, unit=None) |
| 113 | +sankey.add(flows=[1, -1], orientations=[0, 1], |
| 114 | + patchlabel="0", facecolor='k', |
| 115 | + rotation=45) |
| 116 | +side(sankey, n=links_per_side) |
| 117 | +corner(sankey) |
| 118 | +side(sankey, n=links_per_side) |
| 119 | +corner(sankey) |
| 120 | +side(sankey, n=links_per_side) |
| 121 | +corner(sankey) |
| 122 | +side(sankey, n=links_per_side) |
| 123 | +sankey.finish() |
| 124 | +# Notice: |
| 125 | +# 1. The alignment doesn't drift significantly (if at all; with 16007 |
| 126 | +# subdiagrams there is still closure). |
| 127 | +# 2. The first diagram is rotated 45 degrees, so all other diagrams are |
| 128 | +# rotated accordingly. |
| 129 | + |
| 130 | +# Example 5 |
| 131 | +# This demonstrates a practical example -- a Rankine power cycle. |
| 132 | +fig = plt.figure(figsize=(8, 12)) |
| 133 | +ax = fig.add_subplot(1, 1, 1, xticks=[], yticks=[], |
| 134 | + title="Rankine Power Cycle: Example 8.6 from Moran and Shapiro\n" |
| 135 | + + "\x22Fundamentals of Engineering Thermodynamics\x22, 6th ed., 2008") |
| 136 | +Hdot = np.array([260.431, 35.078, 180.794, 221.115, 22.700, |
| 137 | + 142.361, 10.193, 10.210, 43.670, 44.312, |
| 138 | + 68.631, 10.758, 10.758, 0.017, 0.642, |
| 139 | + 232.121, 44.559, 100.613, 132.168])*1.0e6 # W |
| 140 | +sankey = Sankey(ax=ax, format='%.3G', unit='W', gap=0.5, scale=1.0/Hdot[0]) |
| 141 | +# Shared copy: |
| 142 | +#Hdot = [260.431, 35.078, 180.794, 221.115, 22.700, |
| 143 | +# 142.361, 10.193, 10.210, 43.670, 44.312, |
| 144 | +# 68.631, 10.758, 10.758, 0.017, 0.642, |
| 145 | +# 232.121, 44.559, 100.613, 132.168] # MW |
| 146 | +#sankey = Sankey(ax=ax, format='%.3G', unit=' MW', gap=0.5, scale=1.0/Hdot[0]) |
| 147 | +sankey.add(patchlabel='\n\nPump 1', rotation=90, facecolor='#37c959', |
| 148 | + flows=[Hdot[13], Hdot[6], -Hdot[7]], |
| 149 | + labels=['Shaft power', '', None], |
| 150 | + pathlengths=[0.4, 0.883, 0.25], |
| 151 | + orientations=[1, -1, 0]) |
| 152 | +sankey.add(patchlabel='\n\nOpen\nheater', facecolor='#37c959', |
| 153 | + flows=[Hdot[11], Hdot[7], Hdot[4], -Hdot[8]], |
| 154 | + labels=[None, '', None, None], |
| 155 | + pathlengths=[0.25, 0.25, 1.93, 0.25], |
| 156 | + orientations=[1, 0, -1, 0], prior=0, connect=(2, 1)) |
| 157 | +sankey.add(patchlabel='\n\nPump 2', facecolor='#37c959', |
| 158 | + flows=[Hdot[14], Hdot[8], -Hdot[9]], |
| 159 | + labels=['Shaft power', '', None], |
| 160 | + pathlengths=[0.4, 0.25, 0.25], |
| 161 | + orientations=[1, 0, 0], prior=1, connect=(3, 1)) |
| 162 | +sankey.add(patchlabel='Closed\nheater', trunklength=2.914, fc='#37c959', |
| 163 | + flows=[Hdot[9], Hdot[1], -Hdot[11], -Hdot[10]], |
| 164 | + pathlengths=[0.25, 1.543, 0.25, 0.25], |
| 165 | + labels=['', '', None, None], |
| 166 | + orientations=[0, -1, 1, -1], prior=2, connect=(2, 0)) |
| 167 | +sankey.add(patchlabel='Trap', facecolor='#37c959', trunklength=5.102, |
| 168 | + flows=[Hdot[11], -Hdot[12]], |
| 169 | + labels=['\n', None], |
| 170 | + pathlengths=[1.0, 1.01], |
| 171 | + orientations=[1, 1], prior=3, connect=(2, 0)) |
| 172 | +sankey.add(patchlabel='Steam\ngenerator', facecolor='#ff5555', |
| 173 | + flows=[Hdot[15], Hdot[10], Hdot[2], -Hdot[3], -Hdot[0]], |
| 174 | + labels=['Heat rate', '', '', None, None], |
| 175 | + pathlengths=0.25, |
| 176 | + orientations=[1, 0, -1, -1, -1], prior=3, connect=(3, 1)) |
| 177 | +sankey.add(patchlabel='\n\n\nTurbine 1', facecolor='#37c959', |
| 178 | + flows=[Hdot[0], -Hdot[16], -Hdot[1], -Hdot[2]], |
| 179 | + labels=['', None, None, None], |
| 180 | + pathlengths=[0.25, 0.153, 1.543, 0.25], |
| 181 | + orientations=[0, 1, -1, -1], prior=5, connect=(4, 0)) |
| 182 | +sankey.add(patchlabel='\n\n\nReheat', facecolor='#37c959', |
| 183 | + flows=[Hdot[2], -Hdot[2]], |
| 184 | + labels=[None, None], |
| 185 | + pathlengths=[0.725, 0.25], |
| 186 | + orientations=[-1, 0], prior=6, connect=(3, 0)) |
| 187 | +sankey.add(patchlabel='Turbine 2', trunklength=3.212, facecolor='#37c959', |
| 188 | + flows=[Hdot[3], Hdot[16], -Hdot[5], -Hdot[4], -Hdot[17]], |
| 189 | + labels=[None, 'Shaft power', None, '', 'Shaft power'], |
| 190 | + pathlengths=[0.751, 0.15, 0.25, 1.93, 0.25], |
| 191 | + orientations=[0, -1, 0, -1, 1], prior=6, connect=(1, 1)) |
| 192 | +sankey.add(patchlabel='Condenser', facecolor='#58b1fa', trunklength=1.764, |
| 193 | + flows=[Hdot[5], -Hdot[18], -Hdot[6]], |
| 194 | + labels=['', 'Heat rate', None], |
| 195 | + pathlengths=[0.45, 0.25, 0.883], |
| 196 | + orientations=[-1, 1, 0], prior=8, connect=(2, 0)) |
| 197 | +diagrams = sankey.finish() |
| 198 | +for diagram in diagrams: |
| 199 | + diagram.text.set_fontweight('bold') |
| 200 | + diagram.text.set_fontsize('10') |
| 201 | + for text in diagram.texts: |
| 202 | + # Without namedtuple: |
| 203 | + #diagram[4].set_fontweight('bold') |
| 204 | + #diagram[4].set_fontsize('10') |
| 205 | + #for text in diagram[5]: |
| 206 | + text.set_fontsize('10') |
| 207 | +# Notice that the explicit connections are handled automatically, but the |
| 208 | +# implicit ones currently are not. The lengths of the paths and the trunks |
| 209 | +# must be adjusted manually, and that is a bit tricky. |
| 210 | + |
| 211 | +plt.show() |
0 commit comments