Skip to content

Commit 4a9f3a7

Browse files
committed
Merge pull request matplotlib#465 from jdh2358/sankey2
add sankey module and new demo
2 parents 76acbb6 + 1e86b8a commit 4a9f3a7

File tree

3 files changed

+1353
-188
lines changed

3 files changed

+1353
-188
lines changed

examples/api/sankey_demo.py

Lines changed: 211 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -1,188 +1,211 @@
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

Comments
 (0)