Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Fix sankey.py pep8 and py3 compatibility #973

Merged
merged 3 commits into from

4 participants

Christoph Gohlke Phil Elson Michael Droettboom Benjamin Root
Christoph Gohlke

No description provided.

Phil Elson
Collaborator

I've gone through these changes and they are fine. I have made similar stylistic improvements in the past, but we do need to be careful in making large scale changes to files whilst we have the v1.1.x branch in a non-frozen state (see #910).

This gets my +1.

Christoph Gohlke

That's a good point (making large scale changes). This could wait until after 1.1.1.

Phil Elson
Collaborator

This could wait until after 1.1.1.

Lets do that. That is a whole other issue which I intend to bring up in the next couple of days :-)

Michael Droettboom
Owner

I agree about holding off on this until the 1.1.1 release.

It might also be nice, if possible, to separate out the actual fixes for Python 3 from the general PEP8 stuff.

Benjamin Root
Collaborator

Now that v1.1.1 is out, would it be safe to get this merged in?

Phil Elson
Collaborator

@WeatherGod: If your happy with the changes too, then I would say nothing.

Benjamin Root
Collaborator

ok, merging....

Benjamin Root WeatherGod merged commit 11567bb into from
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
22 examples/api/sankey_demo_links.py
View
@@ -1,31 +1,35 @@
"""Demonstrate/test the Sankey class by producing a long chain of connections.
"""
-import numpy as np
-import matplotlib.pyplot as plt
-from matplotlib.sankey import Sankey
from itertools import cycle
+import matplotlib.pyplot as plt
+from matplotlib.sankey import Sankey
+
links_per_side = 6
+
+
def side(sankey, n=1):
- """Generate a side chain.
- """
+ """Generate a side chain."""
prior = len(sankey.diagrams)
colors = cycle(['orange', 'b', 'g', 'r', 'c', 'm', 'y'])
for i in range(0, 2*n, 2):
sankey.add(flows=[1, -1], orientations=[-1, -1],
- patchlabel=str(prior+i), facecolor=colors.next(),
+ patchlabel=str(prior+i), facecolor=next(colors),
prior=prior+i-1, connect=(1, 0), alpha=0.5)
sankey.add(flows=[1, -1], orientations=[1, 1],
- patchlabel=str(prior+i+1), facecolor=colors.next(),
+ patchlabel=str(prior+i+1), facecolor=next(colors),
prior=prior+i, connect=(1, 0), alpha=0.5)
+
+
def corner(sankey):
- """Generate a corner link.
- """
+ """Generate a corner link."""
prior = len(sankey.diagrams)
sankey.add(flows=[1, -1], orientations=[0, 1],
patchlabel=str(prior), facecolor='k',
prior=prior-1, connect=(1, 0), alpha=0.5)
+
+
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, xticks=[], yticks=[],
title="Why would you want to do this?\n(But you could.)")
200 examples/api/sankey_demo_old.py
View
@@ -7,149 +7,149 @@
import numpy as np
+
def sankey(ax,
outputs=[100.], outlabels=None,
inputs=[100.], inlabels='',
dx=40, dy=10, outangle=45, w=3, inangle=30, offset=2, **kwargs):
"""Draw a Sankey diagram.
-outputs: array of outputs, should sum up to 100%
-outlabels: output labels (same length as outputs),
-or None (use default labels) or '' (no labels)
-inputs and inlabels: similar for inputs
-dx: horizontal elongation
-dy: vertical elongation
-outangle: output arrow angle [deg]
-w: output arrow shoulder
-inangle: input dip angle
-offset: text offset
-**kwargs: propagated to Patch (e.g. fill=False)
-
-Return (patch,[intexts,outtexts])."""
-
+ outputs: array of outputs, should sum up to 100%
+ outlabels: output labels (same length as outputs),
+ or None (use default labels) or '' (no labels)
+ inputs and inlabels: similar for inputs
+ dx: horizontal elongation
+ dy: vertical elongation
+ outangle: output arrow angle [deg]
+ w: output arrow shoulder
+ inangle: input dip angle
+ offset: text offset
+ **kwargs: propagated to Patch (e.g. fill=False)
+
+ Return (patch,[intexts,outtexts]).
+ """
import matplotlib.patches as mpatches
from matplotlib.path import Path
outs = np.absolute(outputs)
outsigns = np.sign(outputs)
- outsigns[-1] = 0 # Last output
+ outsigns[-1] = 0 # Last output
ins = np.absolute(inputs)
insigns = np.sign(inputs)
- insigns[0] = 0 # First input
+ insigns[0] = 0 # First input
- assert sum(outs)==100, "Outputs don't sum up to 100%"
- assert sum(ins)==100, "Inputs don't sum up to 100%"
+ assert sum(outs) == 100, "Outputs don't sum up to 100%"
+ assert sum(ins) == 100, "Inputs don't sum up to 100%"
def add_output(path, loss, sign=1):
- h = (loss/2+w)*np.tan(outangle/180.*np.pi) # Arrow tip height
- move,(x,y) = path[-1] # Use last point as reference
- if sign==0: # Final loss (horizontal)
- path.extend([(Path.LINETO,[x+dx,y]),
- (Path.LINETO,[x+dx,y+w]),
- (Path.LINETO,[x+dx+h,y-loss/2]), # Tip
- (Path.LINETO,[x+dx,y-loss-w]),
- (Path.LINETO,[x+dx,y-loss])])
- outtips.append((sign,path[-3][1]))
- else: # Intermediate loss (vertical)
- path.extend([(Path.CURVE4,[x+dx/2,y]),
- (Path.CURVE4,[x+dx,y]),
- (Path.CURVE4,[x+dx,y+sign*dy]),
- (Path.LINETO,[x+dx-w,y+sign*dy]),
- (Path.LINETO,[x+dx+loss/2,y+sign*(dy+h)]), # Tip
- (Path.LINETO,[x+dx+loss+w,y+sign*dy]),
- (Path.LINETO,[x+dx+loss,y+sign*dy]),
- (Path.CURVE3,[x+dx+loss,y-sign*loss]),
- (Path.CURVE3,[x+dx/2+loss,y-sign*loss])])
- outtips.append((sign,path[-5][1]))
+ h = (loss/2 + w)*np.tan(outangle/180. * np.pi) # Arrow tip height
+ move, (x, y) = path[-1] # Use last point as reference
+ if sign == 0: # Final loss (horizontal)
+ path.extend([(Path.LINETO, [x+dx, y]),
+ (Path.LINETO, [x+dx, y+w]),
+ (Path.LINETO, [x+dx+h, y-loss/2]), # Tip
+ (Path.LINETO, [x+dx, y-loss-w]),
+ (Path.LINETO, [x+dx, y-loss])])
+ outtips.append((sign, path[-3][1]))
+ else: # Intermediate loss (vertical)
+ path.extend([(Path.CURVE4, [x+dx/2, y]),
+ (Path.CURVE4, [x+dx, y]),
+ (Path.CURVE4, [x+dx, y+sign*dy]),
+ (Path.LINETO, [x+dx-w, y+sign*dy]),
+ (Path.LINETO, [x+dx+loss/2, y+sign*(dy+h)]), # Tip
+ (Path.LINETO, [x+dx+loss+w, y+sign*dy]),
+ (Path.LINETO, [x+dx+loss, y+sign*dy]),
+ (Path.CURVE3, [x+dx+loss, y-sign*loss]),
+ (Path.CURVE3, [x+dx/2+loss, y-sign*loss])])
+ outtips.append((sign, path[-5][1]))
def add_input(path, gain, sign=1):
- h = (gain/2)*np.tan(inangle/180.*np.pi) # Dip depth
- move,(x,y) = path[-1] # Use last point as reference
- if sign==0: # First gain (horizontal)
- path.extend([(Path.LINETO,[x-dx,y]),
- (Path.LINETO,[x-dx+h,y+gain/2]), # Dip
- (Path.LINETO,[x-dx,y+gain])])
- xd,yd = path[-2][1] # Dip position
- indips.append((sign,[xd-h,yd]))
- else: # Intermediate gain (vertical)
- path.extend([(Path.CURVE4,[x-dx/2,y]),
- (Path.CURVE4,[x-dx,y]),
- (Path.CURVE4,[x-dx,y+sign*dy]),
- (Path.LINETO,[x-dx-gain/2,y+sign*(dy-h)]), # Dip
- (Path.LINETO,[x-dx-gain,y+sign*dy]),
- (Path.CURVE3,[x-dx-gain,y-sign*gain]),
- (Path.CURVE3,[x-dx/2-gain,y-sign*gain])])
- xd,yd = path[-4][1] # Dip position
- indips.append((sign,[xd,yd+sign*h]))
-
- outtips = [] # Output arrow tip dir. and positions
- urpath = [(Path.MOVETO,[0,100])] # 1st point of upper right path
- lrpath = [(Path.LINETO,[0,0])] # 1st point of lower right path
- for loss,sign in zip(outs,outsigns):
+ h = (gain/2)*np.tan(inangle/180. * np.pi) # Dip depth
+ move, (x, y) = path[-1] # Use last point as reference
+ if sign == 0: # First gain (horizontal)
+ path.extend([(Path.LINETO, [x-dx, y]),
+ (Path.LINETO, [x-dx+h, y+gain/2]), # Dip
+ (Path.LINETO, [x-dx, y+gain])])
+ xd, yd = path[-2][1] # Dip position
+ indips.append((sign, [xd-h, yd]))
+ else: # Intermediate gain (vertical)
+ path.extend([(Path.CURVE4, [x-dx/2, y]),
+ (Path.CURVE4, [x-dx, y]),
+ (Path.CURVE4, [x-dx, y+sign*dy]),
+ (Path.LINETO, [x-dx-gain/2, y+sign*(dy-h)]), # Dip
+ (Path.LINETO, [x-dx-gain, y+sign*dy]),
+ (Path.CURVE3, [x-dx-gain, y-sign*gain]),
+ (Path.CURVE3, [x-dx/2-gain, y-sign*gain])])
+ xd, yd = path[-4][1] # Dip position
+ indips.append((sign, [xd, yd+sign*h]))
+
+ outtips = [] # Output arrow tip dir. and positions
+ urpath = [(Path.MOVETO, [0, 100])] # 1st point of upper right path
+ lrpath = [(Path.LINETO, [0, 0])] # 1st point of lower right path
+ for loss, sign in zip(outs, outsigns):
add_output(sign>=0 and urpath or lrpath, loss, sign=sign)
- indips = [] # Input arrow tip dir. and positions
- llpath = [(Path.LINETO,[0,0])] # 1st point of lower left path
- ulpath = [(Path.MOVETO,[0,100])] # 1st point of upper left path
- for gain,sign in zip(ins,insigns)[::-1]:
+ indips = [] # Input arrow tip dir. and positions
+ llpath = [(Path.LINETO, [0, 0])] # 1st point of lower left path
+ ulpath = [(Path.MOVETO, [0, 100])] # 1st point of upper left path
+ for gain, sign in reversed(list(zip(ins, insigns))):
add_input(sign<=0 and llpath or ulpath, gain, sign=sign)
def revert(path):
"""A path is not just revertable by path[::-1] because of Bezier
-curves."""
+ curves."""
rpath = []
nextmove = Path.LINETO
- for move,pos in path[::-1]:
- rpath.append((nextmove,pos))
+ for move, pos in path[::-1]:
+ rpath.append((nextmove, pos))
nextmove = move
return rpath
# Concatenate subpathes in correct order
path = urpath + revert(lrpath) + llpath + revert(ulpath)
- codes,verts = zip(*path)
+ codes, verts = zip(*path)
verts = np.array(verts)
# Path patch
- path = Path(verts,codes)
+ path = Path(verts, codes)
patch = mpatches.PathPatch(path, **kwargs)
ax.add_patch(patch)
- if False: # DEBUG
+ if False: # DEBUG
print("urpath", urpath)
print("lrpath", revert(lrpath))
print("llpath", llpath)
print("ulpath", revert(ulpath))
-
- xs,ys = zip(*verts)
- ax.plot(xs,ys,'go-')
+ xs, ys = zip(*verts)
+ ax.plot(xs, ys, 'go-')
# Labels
- def set_labels(labels,values):
+ def set_labels(labels, values):
"""Set or check labels according to values."""
- if labels=='': # No labels
+ if labels == '': # No labels
return labels
- elif labels is None: # Default labels
- return [ '%2d%%' % val for val in values ]
+ elif labels is None: # Default labels
+ return ['%2d%%' % val for val in values]
else:
- assert len(labels)==len(values)
+ assert len(labels) == len(values)
return labels
- def put_labels(labels,positions,output=True):
+ def put_labels(labels, positions, output=True):
"""Put labels to positions."""
texts = []
lbls = output and labels or labels[::-1]
- for i,label in enumerate(lbls):
- s,(x,y) = positions[i] # Label direction and position
- if s==0:
- t = ax.text(x+offset,y,label,
+ for i, label in enumerate(lbls):
+ s, (x, y) = positions[i] # Label direction and position
+ if s == 0:
+ t = ax.text(x+offset, y, label,
ha=output and 'left' or 'right', va='center')
- elif s>0:
- t = ax.text(x,y+offset,label, ha='center', va='bottom')
+ elif s > 0:
+ t = ax.text(x, y+offset, label, ha='center', va='bottom')
else:
- t = ax.text(x,y-offset,label, ha='center', va='top')
+ t = ax.text(x, y-offset, label, ha='center', va='top')
texts.append(t)
return texts
@@ -160,32 +160,30 @@ def put_labels(labels,positions,output=True):
intexts = put_labels(inlabels, indips, output=False)
# Axes management
- ax.set_xlim(verts[:,0].min()-dx, verts[:,0].max()+dx)
- ax.set_ylim(verts[:,1].min()-dy, verts[:,1].max()+dy)
+ ax.set_xlim(verts[:, 0].min()-dx, verts[:, 0].max()+dx)
+ ax.set_ylim(verts[:, 1].min()-dy, verts[:, 1].max()+dy)
ax.set_aspect('equal', adjustable='datalim')
- return patch,[intexts,outtexts]
+ return patch, [intexts, outtexts]
+
if __name__=='__main__':
import matplotlib.pyplot as plt
- outputs = [10.,-20.,5.,15.,-10.,40.]
- outlabels = ['First','Second','Third','Fourth','Fifth','Hurray!']
- outlabels = [ s+'\n%d%%' % abs(l) for l,s in zip(outputs,outlabels) ]
+ outputs = [10., -20., 5., 15., -10., 40.]
+ outlabels = ['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Hurray!']
+ outlabels = [s+'\n%d%%' % abs(l) for l, s in zip(outputs, outlabels)]
- inputs = [60.,-25.,15.]
+ inputs = [60., -25., 15.]
fig = plt.figure()
- ax = fig.add_subplot(1,1,1, xticks=[],yticks=[],
- title="Sankey diagram"
- )
+ ax = fig.add_subplot(1, 1, 1, xticks=[], yticks=[], title="Sankey diagram")
- patch,(intexts,outtexts) = sankey(ax, outputs=outputs, outlabels=outlabels,
- inputs=inputs, inlabels=None,
- fc='g', alpha=0.2)
+ patch, (intexts, outtexts) = sankey(ax, outputs=outputs,
+ outlabels=outlabels, inputs=inputs,
+ inlabels=None, fc='g', alpha=0.2)
outtexts[1].set_color('r')
outtexts[-1].set_fontweight('bold')
plt.show()
-
429 lib/matplotlib/sankey.py
View
@@ -16,16 +16,17 @@
# --Provided handling for cases where the total of the inputs isn't 100
# Now, the default layout is based on the assumption that the inputs sum to
# 1. A scaling parameter can be used in other cases.
-# --The call structure was changed to be more explicit about layout, including
-# the length of the trunk, length of the paths, gap between the paths, and
-# the margin around the diagram.
+# --The call structure was changed to be more explicit about layout,
+# including the length of the trunk, length of the paths, gap between the
+# paths, and the margin around the diagram.
# --Allowed the lengths of paths to be adjusted individually, with an option
# to automatically justify them
# --The call structure was changed to make the specification of path
# orientation more flexible. Flows are passed through one array, with
-# inputs being positive and outputs being negative. An orientation argument
-# specifies the direction of the arrows. The "main" inputs/outputs are now
-# specified via an orientation of 0, and there may be several of each.
+# inputs being positive and outputs being negative. An orientation
+# argument specifies the direction of the arrows. The "main"
+# inputs/outputs are now specified via an orientation of 0, and there may
+# be several of each.
# --Added assertions to catch common calling errors
# -Added the physical unit as a string argument to be used in the labels, so
# that the values of the flows can usually be applied automatically
@@ -34,7 +35,6 @@
# --Allowed the diagram to be rotated
import numpy as np
-import warnings
from matplotlib.cbook import iterable, Bunch
from matplotlib.path import Path
@@ -61,7 +61,7 @@ class Sankey:
`Wikipedia (6/1/2011) <http://en.wikipedia.org/wiki/Sankey_diagram>`_
"""
- def _arc(self, quadrant=0, cw=True, radius=1, center=(0,0)):
+ def _arc(self, quadrant=0, cw=True, radius=1, center=(0, 0)):
"""
Return the codes and vertices for a rotated, scaled, and translated
90 degree arc.
@@ -94,23 +94,25 @@ def _arc(self, quadrant=0, cw=True, radius=1, center=(0,0)):
[7.07106781e-01, 7.07106781e-01],
[5.19642327e-01, 8.94571235e-01],
[2.65114773e-01, 1.00000000e+00],
- #[6.12303177e-17, 1.00000000e+00]]) # Insignificant
+ # Insignificant
+ #[6.12303177e-17, 1.00000000e+00]])
[0.00000000e+00, 1.00000000e+00]])
if quadrant == 0 or quadrant == 2:
if cw:
vertices = ARC_VERTICES
else:
- vertices = ARC_VERTICES[:,::-1] # Swap x and y.
+ vertices = ARC_VERTICES[:, ::-1] # Swap x and y.
elif quadrant == 1 or quadrant == 3:
# Negate x.
if cw:
# Swap x and y.
- vertices = np.column_stack((-ARC_VERTICES[:,1],
- ARC_VERTICES[:,0]))
+ vertices = np.column_stack((-ARC_VERTICES[:, 1],
+ ARC_VERTICES[:, 0]))
else:
- vertices = np.column_stack((-ARC_VERTICES[:,0],
- ARC_VERTICES[:,1]))
- if quadrant > 1: radius = -radius # Rotate 180 deg.
+ vertices = np.column_stack((-ARC_VERTICES[:, 0],
+ ARC_VERTICES[:, 1]))
+ if quadrant > 1:
+ radius = -radius # Rotate 180 deg.
return zip(ARC_CODES, radius*vertices +
np.tile(center, (ARC_VERTICES.shape[0], 1)))
@@ -121,7 +123,7 @@ def _add_input(self, path, angle, flow, length):
if angle is None:
return [0, 0], [0, 0]
else:
- (x, y) = path[-1][1] # Use the last point as a reference.
+ x, y = path[-1][1] # Use the last point as a reference.
dipdepth = (flow / 2) * self.pitch
if angle == RIGHT:
x -= length
@@ -131,16 +133,21 @@ def _add_input(self, path, angle, flow, length):
(Path.LINETO, [x, y + flow]),
(Path.LINETO, [x+self.gap, y + flow])])
label_location = [dip[0] - self.offset, dip[1]]
- else: # Vertical
+ else: # Vertical
x -= self.gap
- if angle == UP: sign = 1
- else: sign = -1
+ if angle == UP:
+ sign = 1
+ else:
+ sign = -1
dip = [x - flow / 2, y - sign * (length - dipdepth)]
- if angle == DOWN: quadrant = 2
- else: quadrant = 1
+ if angle == DOWN:
+ quadrant = 2
+ else:
+ quadrant = 1
- if self.radius: # Inner arc isn't needed if inner radius is zero
+ # Inner arc isn't needed if inner radius is zero
+ if self.radius:
path.extend(self._arc(quadrant=quadrant,
cw=angle==UP,
radius=self.radius,
@@ -170,7 +177,7 @@ def _add_output(self, path, angle, flow, length):
if angle is None:
return [0, 0], [0, 0]
else:
- (x, y) = path[-1][1] # Use the last point as a reference.
+ x, y = path[-1][1] # Use the last point as a reference.
tipheight = (self.shoulder - flow / 2) * self.pitch
if angle == RIGHT:
x += length
@@ -182,17 +189,20 @@ def _add_output(self, path, angle, flow, length):
(Path.LINETO, [x, y + flow]),
(Path.LINETO, [x-self.gap, y + flow])])
label_location = [tip[0] + self.offset, tip[1]]
- else: # Vertical
+ else: # Vertical
x += self.gap
- if angle == UP: sign = 1
- else: sign = -1
+ if angle == UP:
+ sign = 1
+ else:
+ sign = -1
tip = [x - flow / 2.0, y + sign * (length + tipheight)]
if angle == UP:
quadrant = 3
else:
quadrant = 0
- if self.radius: # Inner arc isn't needed if inner radius is zero
+ # Inner arc isn't needed if inner radius is zero
+ if self.radius:
path.extend(self._arc(quadrant=quadrant,
cw=angle==UP,
radius=self.radius,
@@ -223,7 +233,7 @@ def _revert(self, path, first_action=Path.LINETO):
"""
reverse_path = []
next_code = first_action
- for code,position in path[::-1]:
+ for code, position in path[::-1]:
reverse_path.append((next_code, position))
next_code = code
return reverse_path
@@ -235,9 +245,9 @@ def _revert(self, path, first_action=Path.LINETO):
#return path
@docstring.dedent_interpd
- def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
- labels='', trunklength=1.0, pathlengths=0.25, prior=None,
- connect=(0,0), rotation=0, **kwargs):
+ def add(self, patchlabel='', flows=np.array([1.0, -1.0]),
+ orientations=[0, 0], labels='', trunklength=1.0, pathlengths=0.25,
+ prior=None, connect=(0, 0), rotation=0, **kwargs):
"""
Add a simple Sankey diagram with flows at the same hierarchical level.
@@ -245,44 +255,44 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
Optional keyword arguments:
- =============== ==========================================================
+ =============== ===================================================
Keyword Description
- =============== ==========================================================
+ =============== ===================================================
*patchlabel* label to be placed at the center of the diagram
Note: *label* (not *patchlabel*) will be passed to
- the patch through ``**kwargs`` and can be used to create
- an entry in the legend.
+ the patch through ``**kwargs`` and can be used to
+ create an entry in the legend.
*flows* array of flow values
By convention, inputs are positive and outputs are
negative.
*orientations* list of orientations of the paths
- Valid values are 1 (from/to the top), 0 (from/to the
- left or right), or -1 (from/to the bottom). If
+ Valid values are 1 (from/to the top), 0 (from/to
+ the left or right), or -1 (from/to the bottom). If
*orientations* == 0, inputs will break in from the
left and outputs will break away to the right.
*labels* list of specifications of the labels for the flows
Each value may be None (no labels), '' (just label
the quantities), or a labeling string. If a single
value is provided, it will be applied to all flows.
- If an entry is a non-empty string, then the quantity
- for the corresponding flow will be shown below the
- string. However, if the *unit* of the main diagram
- is None, then quantities are never shown, regardless
- of the value of this argument.
+ If an entry is a non-empty string, then the
+ quantity for the corresponding flow will be shown
+ below the string. However, if the *unit* of the
+ main diagram is None, then quantities are never
+ shown, regardless of the value of this argument.
*trunklength* length between the bases of the input and output
groups
*pathlengths* list of lengths of the arrows before break-in or
after break-away
If a single value is given, then it will be applied
to the first (inside) paths on the top and bottom,
- and the length of all other arrows will be justified
- accordingly. The *pathlengths* are not applied to
- the horizontal inputs and outputs.
+ and the length of all other arrows will be
+ justified accordingly. The *pathlengths* are not
+ applied to the horizontal inputs and outputs.
*prior* index of the prior diagram to which this diagram
should be connected
- *connect* a (prior, this) tuple indexing the flow of the prior
- diagram and the flow of this diagram which should be
- connected
+ *connect* a (prior, this) tuple indexing the flow of the
+ prior diagram and the flow of this diagram which
+ should be connected
If this is the first diagram or *prior* is None,
*connect* will be ignored.
*rotation* angle of rotation of the diagram [deg]
@@ -292,7 +302,7 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
will be rotated accordingly (e.g., if *rotation*
== 90, an *orientations* entry of 1 means to/from
the left).
- =============== ==========================================================
+ =============== ===================================================
Valid kwargs are :meth:`matplotlib.patches.PathPatch` arguments:
@@ -304,10 +314,10 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
The indexing parameters (*prior* and *connect*) are zero-based.
- The flows are placed along the top of the diagram from the inside out in
- order of their index within the *flows* list or array. They are placed
- along the sides of the diagram from the top down and along the bottom
- from the outside in.
+ The flows are placed along the top of the diagram from the inside out
+ in order of their index within the *flows* list or array. They are
+ placed along the sides of the diagram from the top down and along the
+ bottom from the outside in.
If the the sum of the inputs and outputs is nonzero, the discrepancy
will appear as a cubic Bezier curve along the top and bottom edges of
@@ -319,68 +329,70 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
"""
# Check and preprocess the arguments.
flows = np.array(flows)
- n = flows.shape[0] # Number of flows
+ n = flows.shape[0] # Number of flows
if rotation == None:
rotation = 0
else:
- rotation /= 90.0 # In the code below, angles are expressed in deg/90
- assert len(orientations) == n, ("orientations and flows must have the "
- "same length.\norientations has length "
- "%d, but flows has length %d."
- % len(orientations), n)
- if getattr(labels, '__iter__', False):
- # iterable() isn't used because it would give True if labels is a string
- assert len(labels) == n, ("If labels is a list, then labels and "
- "flows must have the same length.\n"
- "labels has length %d, but flows has "
- "length %d." % len(labels), n)
+ # In the code below, angles are expressed in deg/90
+ rotation /= 90.0
+ assert len(orientations) == n, (
+ "orientations and flows must have the same length.\n"
+ "orientations has length %d, but flows has length %d."
+ % (len(orientations), n))
+ if labels != '' and getattr(labels, '__iter__', False):
+ # iterable() isn't used because it would give True if labels is a
+ # string
+ assert len(labels) == n, (
+ "If labels is a list, then labels and flows must have the "
+ "same length.\nlabels has length %d, but flows has length %d."
+ % (len(labels), n))
else:
labels = [labels]*n
- assert trunklength >= 0, ("trunklength is negative.\nThis isn't "
- "allowed, because it would cause poor "
- "layout.")
+ assert trunklength >= 0, (
+ "trunklength is negative.\nThis isn't allowed, because it would "
+ "cause poor layout.")
if np.absolute(np.sum(flows)) > self.tolerance:
- verbose.report("The sum of the flows is nonzero (%f).\nIs the "
- "system not at steady state?" % np.sum(flows),
- 'helpful')
+ verbose.report(
+ "The sum of the flows is nonzero (%f).\nIs the "
+ "system not at steady state?" % np.sum(flows), 'helpful')
scaled_flows = self.scale*flows
gain = sum(max(flow, 0) for flow in scaled_flows)
loss = sum(min(flow, 0) for flow in scaled_flows)
if not (0.5 <= gain <= 2.0):
- verbose.report("The scaled sum of the inputs is %f.\nThis may "
- "cause poor layout.\nConsider changing the scale so "
- "that the scaled sum is approximately 1.0." % gain,
- 'helpful')
+ verbose.report(
+ "The scaled sum of the inputs is %f.\nThis may "
+ "cause poor layout.\nConsider changing the scale so"
+ " that the scaled sum is approximately 1.0." % gain, 'helpful')
if not (-2.0 <= loss <= -0.5):
- verbose.report("The scaled sum of the outputs is %f.\nThis may "
- "cause poor layout.\nConsider changing the scale so "
- "that the scaled sum is approximately 1.0." % gain,
- 'helpful')
+ verbose.report(
+ "The scaled sum of the outputs is %f.\nThis may "
+ "cause poor layout.\nConsider changing the scale so"
+ " that the scaled sum is approximately 1.0." % gain, 'helpful')
if prior is not None:
assert prior >= 0, "The index of the prior diagram is negative."
- assert min(connect) >= 0, ("At least one of the connection indices "
- "is negative.")
- assert prior < len(self.diagrams), ("The index of the prior "
- "diagram is %d, but there are "
- "only %d other diagrams.\nThe "
- "index is zero-based." % prior,
- len(self.diagrams))
- assert connect[0] < len(self.diagrams[prior].flows), \
- ("The connection index to the source diagram is %d, but "
- "that diagram has only %d flows.\nThe index is zero-based."
- % connect[0], len(self.diagrams[prior].flows))
- assert connect[1] < n, ("The connection index to this diagram is "
- "%d, but this diagram has only %d flows.\n"
- "The index is zero-based." % connect[1], n)
- assert self.diagrams[prior].angles[connect[0]] is not None, \
- ("The connection cannot be made. Check that the magnitude "
- "of flow %d of diagram %d is greater than or equal to the "
- "specified tolerance." % connect[0], prior)
- flow_error = self.diagrams[prior].flows[connect[0]] \
- + flows[connect[1]]
- assert abs(flow_error) < self.tolerance, \
- ("The scaled sum of the connected flows is %f, which is not "
- "within the tolerance (%f)." % flow_error, self.tolerance)
+ assert min(connect) >= 0, (
+ "At least one of the connection indices is negative.")
+ assert prior < len(self.diagrams), (
+ "The index of the prior diagram is %d, but there are "
+ "only %d other diagrams.\nThe index is zero-based."
+ % (prior, len(self.diagrams)))
+ assert connect[0] < len(self.diagrams[prior].flows), (
+ "The connection index to the source diagram is %d, but "
+ "that diagram has only %d flows.\nThe index is zero-based."
+ % (connect[0], len(self.diagrams[prior].flows)))
+ assert connect[1] < n, (
+ "The connection index to this diagram is %d, but this diagram"
+ "has only %d flows.\n The index is zero-based."
+ % (connect[1], n))
+ assert self.diagrams[prior].angles[connect[0]] is not None, (
+ "The connection cannot be made. Check that the magnitude "
+ "of flow %d of diagram %d is greater than or equal to the "
+ "specified tolerance." % (connect[0], prior))
+ flow_error = (self.diagrams[prior].flows[connect[0]] +
+ flows[connect[1]])
+ assert abs(flow_error) < self.tolerance, (
+ "The scaled sum of the connected flows is %f, which is not "
+ "within the tolerance (%f)." % (flow_error, self.tolerance))
# Determine if the flows are inputs.
are_inputs = [None]*n
@@ -390,10 +402,11 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
elif flow <= -self.tolerance:
are_inputs[i] = False
else:
- verbose.report("The magnitude of flow %d (%f) is below the "
- "tolerance (%f).\nIt will not be shown, and it "
- "cannot be used in a connection." % (i, flow,
- self.tolerance), 'helpful')
+ verbose.report(
+ "The magnitude of flow %d (%f) is below the "
+ "tolerance (%f).\nIt will not be shown, and it "
+ "cannot be used in a connection."
+ % (i, flow, self.tolerance), 'helpful')
# Determine the angles of the arrows (before rotation).
angles = [None]*n
@@ -401,14 +414,16 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
if orient == 1:
if is_input:
angles[i] = DOWN
- elif is_input == False: # Be specific since is_input can be None.
+ elif is_input == False:
+ # Be specific since is_input can be None.
angles[i] = UP
elif orient == 0:
if is_input is not None:
angles[i] = RIGHT
else:
- assert orient == -1, ("The value of orientations[%d] is %d, "
- "but it must be -1, 0, or 1." % i, orient)
+ assert orient == -1, (
+ "The value of orientations[%d] is %d, "
+ "but it must be -1, 0, or 1." % (i, orient))
if is_input:
angles[i] = UP
elif is_input == False:
@@ -416,11 +431,10 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
# Justify the lengths of the paths.
if iterable(pathlengths):
- assert len(pathlengths) == n, ("If pathlengths is a list, then "
- "pathlengths and flows must have "
- "the same length.\npathlengths has "
- "length %d, but flows has length %d."
- % len(pathlengths), n)
+ assert len(pathlengths) == n, (
+ "If pathlengths is a list, then pathlengths and flows must "
+ "have the same length.\npathlengths has length %d, but flows "
+ "has length %d." % (len(pathlengths), n))
else: # Make pathlengths into a list.
urlength = pathlengths
ullength = pathlengths
@@ -430,18 +444,18 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
pathlengths = [d.get(angle, 0) for angle in angles]
# Determine the lengths of the top-side arrows
# from the middle outwards.
- for i, (angle, is_input, flow) \
- in enumerate(zip(angles, are_inputs, scaled_flows)):
+ for i, (angle, is_input, flow) in enumerate(zip(angles, are_inputs,
+ scaled_flows)):
if angle == DOWN and is_input:
pathlengths[i] = ullength
ullength += flow
elif angle == UP and not is_input:
pathlengths[i] = urlength
- urlength -= flow # Flow is negative for outputs.
+ urlength -= flow # Flow is negative for outputs.
# Determine the lengths of the bottom-side arrows
# from the middle outwards.
- for i, (angle, is_input, flow) \
- in enumerate(zip(angles, are_inputs, scaled_flows)[::-1]):
+ for i, (angle, is_input, flow) in enumerate(reversed(zip(
+ angles, are_inputs, scaled_flows))):
if angle == UP and is_input:
pathlengths[n-i-1] = lllength
lllength += flow
@@ -451,9 +465,8 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
# Determine the lengths of the left-side arrows
# from the bottom upwards.
has_left_input = False
- for i, (angle, is_input, spec) \
- in enumerate(zip(angles, are_inputs, zip(scaled_flows,
- pathlengths))[::-1]):
+ for i, (angle, is_input, spec) in enumerate(reversed(zip(
+ angles, are_inputs, zip(scaled_flows, pathlengths)))):
if angle == RIGHT:
if is_input:
if has_left_input:
@@ -463,9 +476,8 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
# Determine the lengths of the right-side arrows
# from the top downwards.
has_right_output = False
- for i, (angle, is_input, spec) \
- in enumerate(zip(angles, are_inputs, zip(scaled_flows,
- pathlengths))):
+ for i, (angle, is_input, spec) in enumerate(zip(
+ angles, are_inputs, zip(scaled_flows, pathlengths))):
if angle == RIGHT:
if not is_input:
if has_right_output:
@@ -475,7 +487,7 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
# Begin the subpaths, and smooth the transition if the sum of the flows
# is nonzero.
- urpath = [(Path.MOVETO, [(self.gap - trunklength / 2.0), # Upper right
+ urpath = [(Path.MOVETO, [(self.gap - trunklength / 2.0), # Upper right
gain / 2.0]),
(Path.LINETO, [(self.gap - trunklength / 2.0) / 2.0,
gain / 2.0]),
@@ -487,7 +499,7 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
-loss / 2.0]),
(Path.LINETO, [(trunklength / 2.0 - self.gap),
-loss / 2.0])]
- llpath = [(Path.LINETO, [(trunklength / 2.0 - self.gap), # Lower left
+ llpath = [(Path.LINETO, [(trunklength / 2.0 - self.gap), # Lower left
loss / 2.0]),
(Path.LINETO, [(trunklength / 2.0 - self.gap) / 2.0,
loss / 2.0]),
@@ -499,41 +511,36 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
-gain / 2.0]),
(Path.LINETO, [(self.gap - trunklength / 2.0),
-gain / 2.0])]
- lrpath = [(Path.LINETO, [(trunklength / 2.0 - self.gap), # Lower right
+ lrpath = [(Path.LINETO, [(trunklength / 2.0 - self.gap), # Lower right
loss / 2.0])]
- ulpath = [(Path.LINETO, [self.gap - trunklength / 2.0, # Upper left
+ ulpath = [(Path.LINETO, [self.gap - trunklength / 2.0, # Upper left
gain / 2.0])]
# Add the subpaths and assign the locations of the tips and labels.
- tips = np.zeros((n,2))
- label_locations = np.zeros((n,2))
+ tips = np.zeros((n, 2))
+ label_locations = np.zeros((n, 2))
# Add the top-side inputs and outputs from the middle outwards.
- for i, (angle, is_input, spec) \
- in enumerate(zip(angles, are_inputs,
- zip(scaled_flows, pathlengths))):
+ for i, (angle, is_input, spec) in enumerate(zip(
+ angles, are_inputs, zip(scaled_flows, pathlengths))):
if angle == DOWN and is_input:
- tips[i,:], label_locations[i,:] = self._add_input(ulpath, angle,
- *spec)
+ tips[i, :], label_locations[i, :] = self._add_input(
+ ulpath, angle, *spec)
elif angle == UP and not is_input:
- tips[i,:], label_locations[i,:] = self._add_output(urpath,
- angle, *spec)
+ tips[i, :], label_locations[i, :] = self._add_output(
+ urpath, angle, *spec)
# Add the bottom-side inputs and outputs from the middle outwards.
- for i, (angle, is_input, spec) \
- in enumerate(zip(angles, are_inputs,
- zip(scaled_flows, pathlengths))[::-1]):
+ for i, (angle, is_input, spec) in enumerate(reversed(zip(
+ angles, are_inputs, zip(scaled_flows, pathlengths)))):
if angle == UP and is_input:
- (tips[n-i-1,:],
- label_locations[n-i-1,:]) = self._add_input(llpath, angle,
- *spec)
+ tips[n-i-1, :], label_locations[n-i-1, :] = self._add_input(
+ llpath, angle, *spec)
elif angle == DOWN and not is_input:
- (tips[n-i-1,:],
- label_locations[n-i-1,:]) = self._add_output(lrpath, angle,
- *spec)
+ tips[n-i-1, :], label_locations[n-i-1, :] = self._add_output(
+ lrpath, angle, *spec)
# Add the left-side inputs from the bottom upwards.
has_left_input = False
- for i, (angle, is_input, spec) \
- in enumerate(zip(angles, are_inputs,
- zip(scaled_flows, pathlengths))[::-1]):
+ for i, (angle, is_input, spec) in enumerate(reversed(zip(
+ angles, are_inputs, zip(scaled_flows, pathlengths)))):
if angle == RIGHT and is_input:
if not has_left_input:
# Make sure the lower path extends
@@ -542,14 +549,12 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
llpath.append((Path.LINETO, [ulpath[-1][1][0],
llpath[-1][1][1]]))
has_left_input = True
- (tips[n-i-1,:],
- label_locations[n-i-1,:]) = self._add_input(llpath, angle,
- *spec)
+ tips[n-i-1, :], label_locations[n-i-1, :] = self._add_input(
+ llpath, angle, *spec)
# Add the right-side outputs from the top downwards.
has_right_output = False
- for i, (angle, is_input, spec) \
- in enumerate(zip(angles, are_inputs,
- zip(scaled_flows, pathlengths))):
+ for i, (angle, is_input, spec) in enumerate(zip(
+ angles, are_inputs, zip(scaled_flows, pathlengths))):
if angle == RIGHT and not is_input:
if not has_right_output:
# Make sure the upper path extends
@@ -558,8 +563,8 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
urpath.append((Path.LINETO, [lrpath[-1][1][0],
urpath[-1][1][1]]))
has_right_output = True
- (tips[i,:],
- label_locations[i,:]) = self._add_output(urpath, angle, *spec)
+ tips[i, :], label_locations[i, :] = self._add_output(
+ urpath, angle, *spec)
# Trim any hanging vertices.
if not has_left_input:
ulpath.pop()
@@ -575,12 +580,15 @@ def add(self, patchlabel='', flows=np.array([1.0,-1.0]), orientations=[0,0],
# Create a patch with the Sankey outline.
codes, vertices = zip(*path)
vertices = np.array(vertices)
+
def _get_angle(a, r):
- if a is None: return None
- else: return a + r
+ if a is None:
+ return None
+ else:
+ return a + r
if prior is None:
- if rotation != 0: # By default, none of this is needed.
+ if rotation != 0: # By default, none of this is needed.
angles = [_get_angle(angle, rotation) for angle in angles]
rotate = Affine2D().rotate_deg(rotation*90).transform_point
tips = rotate(tips)
@@ -600,7 +608,7 @@ def _get_angle(a, r):
vertices = translate(rotate(vertices))
kwds = dict(s=patchlabel, ha='center', va='center')
text = self.ax.text(*offset, **kwds)
- if False: # Debug
+ if False: # Debug
print "llpath\n", llpath
print "ulpath\n", self._revert(ulpath)
print "urpath\n", urpath
@@ -609,9 +617,8 @@ def _get_angle(a, r):
self.ax.plot(xs, ys, 'go-')
patch = PathPatch(Path(vertices, codes),
fc=kwargs.pop('fc', kwargs.pop('facecolor',
- '#bfd1d4')), # Custom defaults
- lw=kwargs.pop('lw', kwargs.pop('linewidth',
- 0.5)),
+ '#bfd1d4')), # Custom defaults
+ lw=kwargs.pop('lw', kwargs.pop('linewidth', 0.5)),
**kwargs)
self.ax.add_patch(patch)
@@ -626,8 +633,10 @@ def _get_angle(a, r):
labels[i] += quantity
texts = []
for i, (label, location) in enumerate(zip(labels, label_locations)):
- if label: s = label
- else: s = ''
+ if label:
+ s = label
+ else:
+ s = ''
texts.append(self.ax.text(x=location[0], y=location[1],
s=s,
ha='center', va='center'))
@@ -636,20 +645,24 @@ def _get_angle(a, r):
# user wants to provide labels later.
# Expand the size of the diagram if necessary.
- self.extent = (min(np.min(vertices[:,0]), np.min(label_locations[:,0]),
+ self.extent = (min(np.min(vertices[:, 0]),
+ np.min(label_locations[:, 0]),
self.extent[0]),
- max(np.max(vertices[:,0]), np.max(label_locations[:,0]),
+ max(np.max(vertices[:, 0]),
+ np.max(label_locations[:, 0]),
self.extent[1]),
- min(np.min(vertices[:,1]), np.min(label_locations[:,1]),
+ min(np.min(vertices[:, 1]),
+ np.min(label_locations[:, 1]),
self.extent[2]),
- max(np.max(vertices[:,1]), np.max(label_locations[:,1]),
+ max(np.max(vertices[:, 1]),
+ np.max(label_locations[:, 1]),
self.extent[3]))
# Include both vertices _and_ label locations in the extents; there are
# where either could determine the margins (e.g., arrow shoulders).
# Add this diagram as a subdiagram.
self.diagrams.append(Bunch(patch=patch, flows=flows, angles=angles,
- tips=tips, text=text, texts=texts))
+ tips=tips, text=text, texts=texts))
# Allow a daisy-chained call structure (see docstring for the class).
return self
@@ -662,32 +675,32 @@ def finish(self):
Return value is a list of subdiagrams represented with the following
fields:
- =============== =====================================================
+ =============== ===================================================
Field Description
- =============== =====================================================
+ =============== ===================================================
*patch* Sankey outline (an instance of
:class:`~maplotlib.patches.PathPatch`)
*flows* values of the flows (positive for input, negative
for output)
*angles* list of angles of the arrows [deg/90]
- For example, if the diagram has not been rotated, an
- input to the top side will have an angle of 3
- (DOWN), and an output from the top side will have an
- angle of 1 (UP). If a flow has been skipped
+ For example, if the diagram has not been rotated,
+ an input to the top side will have an angle of 3
+ (DOWN), and an output from the top side will have
+ an angle of 1 (UP). If a flow has been skipped
(because its magnitude is less than *tolerance*),
then its angle will be None.
- *tips* array in which each row is an [x, y] pair indicating
- the positions of the tips (or "dips") of the flow
- paths
+ *tips* array in which each row is an [x, y] pair
+ indicating the positions of the tips (or "dips") of
+ the flow paths
If the magnitude of a flow is less the *tolerance*
for the instance of :class:`Sankey`, the flow is
skipped and its tip will be at the center of the
diagram.
*text* :class:`~matplotlib.text.Text` instance for the
label of the diagram
- *texts* list of :class:`~matplotlib.text.Text` instances for
- the labels of flows
- =============== =====================================================
+ *texts* list of :class:`~matplotlib.text.Text` instances
+ for the labels of flows
+ =============== ===================================================
.. seealso::
@@ -708,19 +721,19 @@ def __init__(self, ax=None, scale=1.0, unit='', format='%G', gap=0.25,
Optional keyword arguments:
- =============== ====================================================
+ =============== ===================================================
Field Description
- =============== ====================================================
+ =============== ===================================================
*ax* axes onto which the data should be plotted
If *ax* isn't provided, new axes will be created.
*scale* scaling factor for the flows
*scale* sizes the width of the paths in order to
maintain proper layout. The same scale is applied
- to all subdiagrams. The value should be chosen such
- that the product of the scale and the sum of the
- inputs is approximately 1.0 (and the product of the
- scale and the sum of the outputs is approximately
- -1.0).
+ to all subdiagrams. The value should be chosen
+ such that the product of the scale and the sum of
+ the inputs is approximately 1.0 (and the product of
+ the scale and the sum of the outputs is
+ approximately -1.0).
*unit* string representing the physical unit associated
with the flow quantities
If *unit* is None, then none of the quantities are
@@ -728,8 +741,8 @@ def __init__(self, ax=None, scale=1.0, unit='', format='%G', gap=0.25,
*format* a Python number formatting string to be used in
labeling the flow as a quantity (i.e., a number
times a unit, where the unit is given)
- *gap* space between paths that break in/break away to/from
- the top or bottom
+ *gap* space between paths that break in/break away
+ to/from the top or bottom
*radius* inner radius of the vertical paths
*shoulder* size of the shoulders of output arrowS
*offset* text offset (from the dip or tip of the arrow)
@@ -741,14 +754,14 @@ def __init__(self, ax=None, scale=1.0, unit='', format='%G', gap=0.25,
flows
The magnitude of the sum of connected flows cannot
be greater than *tolerance*.
- =============== ====================================================
+ =============== ===================================================
The optional arguments listed above are applied to all subdiagrams so
that there is consistent alignment and formatting.
- If :class:`Sankey` is instantiated with any keyword arguments other than
- those explicitly listed above (``**kwargs``), they will be passed to
- :meth:`add`, which will create the first subdiagram.
+ If :class:`Sankey` is instantiated with any keyword arguments other
+ than those explicitly listed above (``**kwargs``), they will be passed
+ to :meth:`add`, which will create the first subdiagram.
In order to draw a complex Sankey diagram, create an instance of
:class:`Sankey` by calling it without any kwargs::
@@ -781,16 +794,18 @@ def __init__(self, ax=None, scale=1.0, unit='', format='%G', gap=0.25,
.. plot:: mpl_examples/api/sankey_demo_basics.py
"""
# Check the arguments.
- assert gap >= 0, ("The gap is negative.\nThis isn't allowed because it "
- "would cause the paths to overlap.")
- assert radius <= gap, ("The inner radius is greater than the path "
- "spacing.\nThis isn't allowed because it would "
- "cause the paths to overlap.")
- assert head_angle >= 0, ("The angle is negative.\nThis isn't allowed "
- "because it would cause inputs to look like "
- "outputs and vice versa.")
- assert tolerance >= 0, ("The tolerance is negative.\nIt must be a "
- "magnitude.")
+ assert gap >= 0, (
+ "The gap is negative.\nThis isn't allowed because it "
+ "would cause the paths to overlap.")
+ assert radius <= gap, (
+ "The inner radius is greater than the path spacing.\n"
+ "This isn't allowed because it would cause the paths to overlap.")
+ assert head_angle >= 0, (
+ "The angle is negative.\nThis isn't allowed "
+ "because it would cause inputs to look like "
+ "outputs and vice versa.")
+ assert tolerance >= 0, (
+ "The tolerance is negative.\nIt must be a magnitude.")
# Create axes if necessary.
if ax is None:
Something went wrong with that request. Please try again.