1537 lines
51 KiB
Python
1537 lines
51 KiB
Python
# Constants
|
|
# ---------
|
|
# Subplot types that are each individually positioned with a domain
|
|
#
|
|
# Each of these subplot types has a `domain` property with `x`/`y`
|
|
# properties.
|
|
# Note that this set does not contain `xaxis`/`yaxis` because these behave a
|
|
# little differently.
|
|
import collections
|
|
|
|
_single_subplot_types = {"scene", "geo", "polar", "ternary", "mapbox"}
|
|
_subplot_types = set.union(_single_subplot_types, {"xy", "domain"})
|
|
|
|
# For most subplot types, a trace is associated with a particular subplot
|
|
# using a trace property with a name that matches the subplot type. For
|
|
# example, a `scatter3d.scene` property set to `'scene2'` associates a
|
|
# scatter3d trace with the second `scene` subplot in the figure.
|
|
#
|
|
# There are a few subplot types that don't follow this pattern, and instead
|
|
# the trace property is just named `subplot`. For example setting
|
|
# the `scatterpolar.subplot` property to `polar3` associates the scatterpolar
|
|
# trace with the third polar subplot in the figure
|
|
_subplot_prop_named_subplot = {"polar", "ternary", "mapbox"}
|
|
|
|
|
|
# Named tuple to hold an xaxis/yaxis pair that represent a single subplot
|
|
SubplotXY = collections.namedtuple("SubplotXY", ("xaxis", "yaxis"))
|
|
SubplotDomain = collections.namedtuple("SubplotDomain", ("x", "y"))
|
|
|
|
SubplotRef = collections.namedtuple(
|
|
"SubplotRef", ("subplot_type", "layout_keys", "trace_kwargs")
|
|
)
|
|
|
|
|
|
def _get_initial_max_subplot_ids():
|
|
max_subplot_ids = {subplot_type: 0 for subplot_type in _single_subplot_types}
|
|
max_subplot_ids["xaxis"] = 0
|
|
max_subplot_ids["yaxis"] = 0
|
|
return max_subplot_ids
|
|
|
|
|
|
def make_subplots(
|
|
rows=1,
|
|
cols=1,
|
|
shared_xaxes=False,
|
|
shared_yaxes=False,
|
|
start_cell="top-left",
|
|
print_grid=False,
|
|
horizontal_spacing=None,
|
|
vertical_spacing=None,
|
|
subplot_titles=None,
|
|
column_widths=None,
|
|
row_heights=None,
|
|
specs=None,
|
|
insets=None,
|
|
column_titles=None,
|
|
row_titles=None,
|
|
x_title=None,
|
|
y_title=None,
|
|
figure=None,
|
|
**kwargs,
|
|
):
|
|
"""
|
|
Return an instance of plotly.graph_objs.Figure with predefined subplots
|
|
configured in 'layout'.
|
|
|
|
Parameters
|
|
----------
|
|
rows: int (default 1)
|
|
Number of rows in the subplot grid. Must be greater than zero.
|
|
|
|
cols: int (default 1)
|
|
Number of columns in the subplot grid. Must be greater than zero.
|
|
|
|
shared_xaxes: boolean or str (default False)
|
|
Assign shared (linked) x-axes for 2D cartesian subplots
|
|
|
|
- True or 'columns': Share axes among subplots in the same column
|
|
- 'rows': Share axes among subplots in the same row
|
|
- 'all': Share axes across all subplots in the grid.
|
|
|
|
shared_yaxes: boolean or str (default False)
|
|
Assign shared (linked) y-axes for 2D cartesian subplots
|
|
|
|
- 'columns': Share axes among subplots in the same column
|
|
- True or 'rows': Share axes among subplots in the same row
|
|
- 'all': Share axes across all subplots in the grid.
|
|
|
|
start_cell: 'bottom-left' or 'top-left' (default 'top-left')
|
|
Choose the starting cell in the subplot grid used to set the
|
|
domains_grid of the subplots.
|
|
|
|
- 'top-left': Subplots are numbered with (1, 1) in the top
|
|
left corner
|
|
- 'bottom-left': Subplots are numbererd with (1, 1) in the bottom
|
|
left corner
|
|
|
|
print_grid: boolean (default True):
|
|
If True, prints a string representation of the plot grid. Grid may
|
|
also be printed using the `Figure.print_grid()` method on the
|
|
resulting figure.
|
|
|
|
horizontal_spacing: float (default 0.2 / cols)
|
|
Space between subplot columns in normalized plot coordinates. Must be
|
|
a float between 0 and 1.
|
|
|
|
Applies to all columns (use 'specs' subplot-dependents spacing)
|
|
|
|
vertical_spacing: float (default 0.3 / rows)
|
|
Space between subplot rows in normalized plot coordinates. Must be
|
|
a float between 0 and 1.
|
|
|
|
Applies to all rows (use 'specs' subplot-dependents spacing)
|
|
|
|
subplot_titles: list of str or None (default None)
|
|
Title of each subplot as a list in row-major ordering.
|
|
|
|
Empty strings ("") can be included in the list if no subplot title
|
|
is desired in that space so that the titles are properly indexed.
|
|
|
|
specs: list of lists of dict or None (default None)
|
|
Per subplot specifications of subplot type, row/column spanning, and
|
|
spacing.
|
|
|
|
ex1: specs=[[{}, {}], [{'colspan': 2}, None]]
|
|
|
|
ex2: specs=[[{'rowspan': 2}, {}], [None, {}]]
|
|
|
|
- Indices of the outer list correspond to subplot grid rows
|
|
starting from the top, if start_cell='top-left',
|
|
or bottom, if start_cell='bottom-left'.
|
|
The number of rows in 'specs' must be equal to 'rows'.
|
|
|
|
- Indices of the inner lists correspond to subplot grid columns
|
|
starting from the left. The number of columns in 'specs'
|
|
must be equal to 'cols'.
|
|
|
|
- Each item in the 'specs' list corresponds to one subplot
|
|
in a subplot grid. (N.B. The subplot grid has exactly 'rows'
|
|
times 'cols' cells.)
|
|
|
|
- Use None for a blank a subplot cell (or to move past a col/row span).
|
|
|
|
- Note that specs[0][0] has the specs of the 'start_cell' subplot.
|
|
|
|
- Each item in 'specs' is a dictionary.
|
|
The available keys are:
|
|
* type (string, default 'xy'): Subplot type. One of
|
|
- 'xy': 2D Cartesian subplot type for scatter, bar, etc.
|
|
- 'scene': 3D Cartesian subplot for scatter3d, cone, etc.
|
|
- 'polar': Polar subplot for scatterpolar, barpolar, etc.
|
|
- 'ternary': Ternary subplot for scatterternary
|
|
- 'mapbox': Mapbox subplot for scattermapbox
|
|
- 'domain': Subplot type for traces that are individually
|
|
positioned. pie, parcoords, parcats, etc.
|
|
- trace type: A trace type which will be used to determine
|
|
the appropriate subplot type for that trace
|
|
|
|
* secondary_y (bool, default False): If True, create a secondary
|
|
y-axis positioned on the right side of the subplot. Only valid
|
|
if type='xy'.
|
|
* colspan (int, default 1): number of subplot columns
|
|
for this subplot to span.
|
|
* rowspan (int, default 1): number of subplot rows
|
|
for this subplot to span.
|
|
* l (float, default 0.0): padding left of cell
|
|
* r (float, default 0.0): padding right of cell
|
|
* t (float, default 0.0): padding right of cell
|
|
* b (float, default 0.0): padding bottom of cell
|
|
|
|
- Note: Use 'horizontal_spacing' and 'vertical_spacing' to adjust
|
|
the spacing in between the subplots.
|
|
|
|
insets: list of dict or None (default None):
|
|
Inset specifications. Insets are subplots that overlay grid subplots
|
|
|
|
- Each item in 'insets' is a dictionary.
|
|
The available keys are:
|
|
|
|
* cell (tuple, default=(1,1)): (row, col) index of the
|
|
subplot cell to overlay inset axes onto.
|
|
* type (string, default 'xy'): Subplot type
|
|
* l (float, default=0.0): padding left of inset
|
|
in fraction of cell width
|
|
* w (float or 'to_end', default='to_end') inset width
|
|
in fraction of cell width ('to_end': to cell right edge)
|
|
* b (float, default=0.0): padding bottom of inset
|
|
in fraction of cell height
|
|
* h (float or 'to_end', default='to_end') inset height
|
|
in fraction of cell height ('to_end': to cell top edge)
|
|
|
|
column_widths: list of numbers or None (default None)
|
|
list of length `cols` of the relative widths of each column of suplots.
|
|
Values are normalized internally and used to distribute overall width
|
|
of the figure (excluding padding) among the columns.
|
|
|
|
For backward compatibility, may also be specified using the
|
|
`column_width` keyword argument.
|
|
|
|
row_heights: list of numbers or None (default None)
|
|
list of length `rows` of the relative heights of each row of subplots.
|
|
If start_cell='top-left' then row heights are applied top to bottom.
|
|
Otherwise, if start_cell='bottom-left' then row heights are applied
|
|
bottom to top.
|
|
|
|
For backward compatibility, may also be specified using the
|
|
`row_width` kwarg. If specified as `row_width`, then the width values
|
|
are applied from bottom to top regardless of the value of start_cell.
|
|
This matches the legacy behavior of the `row_width` argument.
|
|
|
|
column_titles: list of str or None (default None)
|
|
list of length `cols` of titles to place above the top subplot in
|
|
each column.
|
|
|
|
row_titles: list of str or None (default None)
|
|
list of length `rows` of titles to place on the right side of each
|
|
row of subplots. If start_cell='top-left' then row titles are
|
|
applied top to bottom. Otherwise, if start_cell='bottom-left' then
|
|
row titles are applied bottom to top.
|
|
|
|
x_title: str or None (default None)
|
|
Title to place below the bottom row of subplots,
|
|
centered horizontally
|
|
|
|
y_title: str or None (default None)
|
|
Title to place to the left of the left column of subplots,
|
|
centered vertically
|
|
|
|
figure: go.Figure or None (default None)
|
|
If None, a new go.Figure instance will be created and its axes will be
|
|
populated with those corresponding to the requested subplot geometry and
|
|
this new figure will be returned.
|
|
If a go.Figure instance, the axes will be added to the
|
|
layout of this figure and this figure will be returned. If the figure
|
|
already contains axes, they will be overwritten.
|
|
|
|
Examples
|
|
--------
|
|
|
|
Example 1:
|
|
|
|
>>> # Stack two subplots vertically, and add a scatter trace to each
|
|
>>> from plotly.subplots import make_subplots
|
|
>>> import plotly.graph_objects as go
|
|
>>> fig = make_subplots(rows=2)
|
|
|
|
This is the format of your plot grid:
|
|
[ (1,1) xaxis1,yaxis1 ]
|
|
[ (2,1) xaxis2,yaxis2 ]
|
|
|
|
>>> fig.add_scatter(y=[2, 1, 3], row=1, col=1) # doctest: +ELLIPSIS
|
|
Figure(...)
|
|
>>> fig.add_scatter(y=[1, 3, 2], row=2, col=1) # doctest: +ELLIPSIS
|
|
Figure(...)
|
|
|
|
or see Figure.append_trace
|
|
|
|
Example 2:
|
|
|
|
>>> # Stack a scatter plot
|
|
>>> fig = make_subplots(rows=2, shared_xaxes=True)
|
|
|
|
This is the format of your plot grid:
|
|
[ (1,1) xaxis1,yaxis1 ]
|
|
[ (2,1) xaxis2,yaxis2 ]
|
|
|
|
>>> fig.add_scatter(y=[2, 1, 3], row=1, col=1) # doctest: +ELLIPSIS
|
|
Figure(...)
|
|
>>> fig.add_scatter(y=[1, 3, 2], row=2, col=1) # doctest: +ELLIPSIS
|
|
Figure(...)
|
|
|
|
Example 3:
|
|
|
|
>>> # irregular subplot layout (more examples below under 'specs')
|
|
>>> fig = make_subplots(rows=2, cols=2,
|
|
... specs=[[{}, {}],
|
|
... [{'colspan': 2}, None]])
|
|
|
|
This is the format of your plot grid:
|
|
[ (1,1) xaxis1,yaxis1 ] [ (1,2) xaxis2,yaxis2 ]
|
|
[ (2,1) xaxis3,yaxis3 - ]
|
|
|
|
>>> fig.add_trace(go.Scatter(x=[1,2,3], y=[2,1,2]), row=1, col=1) # doctest: +ELLIPSIS
|
|
Figure(...)
|
|
>>> fig.add_trace(go.Scatter(x=[1,2,3], y=[2,1,2]), row=1, col=2) # doctest: +ELLIPSIS
|
|
Figure(...)
|
|
>>> fig.add_trace(go.Scatter(x=[1,2,3], y=[2,1,2]), row=2, col=1) # doctest: +ELLIPSIS
|
|
Figure(...)
|
|
|
|
Example 4:
|
|
|
|
>>> # insets
|
|
>>> fig = make_subplots(insets=[{'cell': (1,1), 'l': 0.7, 'b': 0.3}])
|
|
|
|
This is the format of your plot grid:
|
|
[ (1,1) xaxis1,yaxis1 ]
|
|
|
|
With insets:
|
|
[ xaxis2,yaxis2 ] over [ (1,1) xaxis1,yaxis1 ]
|
|
|
|
>>> fig.add_scatter(x=[1,2,3], y=[2,1,1]) # doctest: +ELLIPSIS
|
|
Figure(...)
|
|
>>> fig.add_scatter(x=[1,2,3], y=[2,1,2], xaxis='x2', yaxis='y2') # doctest: +ELLIPSIS
|
|
Figure(...)
|
|
|
|
Example 5:
|
|
|
|
>>> # include subplot titles
|
|
>>> fig = make_subplots(rows=2, subplot_titles=('Plot 1','Plot 2'))
|
|
|
|
This is the format of your plot grid:
|
|
[ (1,1) x1,y1 ]
|
|
[ (2,1) x2,y2 ]
|
|
|
|
>>> fig.add_scatter(x=[1,2,3], y=[2,1,2], row=1, col=1) # doctest: +ELLIPSIS
|
|
Figure(...)
|
|
>>> fig.add_bar(x=[1,2,3], y=[2,1,2], row=2, col=1) # doctest: +ELLIPSIS
|
|
Figure(...)
|
|
|
|
Example 6:
|
|
|
|
Subplot with mixed subplot types
|
|
|
|
>>> fig = make_subplots(rows=2, cols=2,
|
|
... specs=[[{'type': 'xy'}, {'type': 'polar'}],
|
|
... [{'type': 'scene'}, {'type': 'ternary'}]])
|
|
|
|
>>> fig.add_traces(
|
|
... [go.Scatter(y=[2, 3, 1]),
|
|
... go.Scatterpolar(r=[1, 3, 2], theta=[0, 45, 90]),
|
|
... go.Scatter3d(x=[1, 2, 1], y=[2, 3, 1], z=[0, 3, 5]),
|
|
... go.Scatterternary(a=[0.1, 0.2, 0.1],
|
|
... b=[0.2, 0.3, 0.1],
|
|
... c=[0.7, 0.5, 0.8])],
|
|
... rows=[1, 1, 2, 2],
|
|
... cols=[1, 2, 1, 2]) # doctest: +ELLIPSIS
|
|
Figure(...)
|
|
"""
|
|
|
|
import plotly.graph_objs as go
|
|
|
|
# Handle backward compatibility
|
|
# -----------------------------
|
|
use_legacy_row_heights_order = "row_width" in kwargs
|
|
row_heights = kwargs.pop("row_width", row_heights)
|
|
column_widths = kwargs.pop("column_width", column_widths)
|
|
|
|
if kwargs:
|
|
raise TypeError(
|
|
"make_subplots() got unexpected keyword argument(s): {}".format(
|
|
list(kwargs)
|
|
)
|
|
)
|
|
|
|
# Validate coerce inputs
|
|
# ----------------------
|
|
# ### rows ###
|
|
if not isinstance(rows, int) or rows <= 0:
|
|
raise ValueError(
|
|
"""
|
|
The 'rows' argument to make_suplots must be an int greater than 0.
|
|
Received value of type {typ}: {val}""".format(
|
|
typ=type(rows), val=repr(rows)
|
|
)
|
|
)
|
|
|
|
# ### cols ###
|
|
if not isinstance(cols, int) or cols <= 0:
|
|
raise ValueError(
|
|
"""
|
|
The 'cols' argument to make_suplots must be an int greater than 0.
|
|
Received value of type {typ}: {val}""".format(
|
|
typ=type(cols), val=repr(cols)
|
|
)
|
|
)
|
|
|
|
# ### start_cell ###
|
|
if start_cell == "bottom-left":
|
|
col_dir = 1
|
|
row_dir = 1
|
|
elif start_cell == "top-left":
|
|
col_dir = 1
|
|
row_dir = -1
|
|
else:
|
|
raise ValueError(
|
|
"""
|
|
The 'start_cell` argument to make_subplots must be one of \
|
|
['bottom-left', 'top-left']
|
|
Received value of type {typ}: {val}""".format(
|
|
typ=type(start_cell), val=repr(start_cell)
|
|
)
|
|
)
|
|
|
|
# ### Helper to validate coerce elements of lists of dictionaries ###
|
|
def _check_keys_and_fill(name, arg, defaults):
|
|
def _checks(item, defaults):
|
|
if item is None:
|
|
return
|
|
if not isinstance(item, dict):
|
|
raise ValueError(
|
|
"""
|
|
Elements of the '{name}' argument to make_suplots must be dictionaries \
|
|
or None.
|
|
Received value of type {typ}: {val}""".format(
|
|
name=name, typ=type(item), val=repr(item)
|
|
)
|
|
)
|
|
|
|
for k in item:
|
|
if k not in defaults:
|
|
raise ValueError(
|
|
"""
|
|
Invalid key specified in an element of the '{name}' argument to \
|
|
make_subplots: {k}
|
|
Valid keys include: {valid_keys}""".format(
|
|
k=repr(k), name=name, valid_keys=repr(list(defaults))
|
|
)
|
|
)
|
|
for k, v in defaults.items():
|
|
item.setdefault(k, v)
|
|
|
|
for arg_i in arg:
|
|
if isinstance(arg_i, (list, tuple)):
|
|
# 2D list
|
|
for arg_ii in arg_i:
|
|
_checks(arg_ii, defaults)
|
|
elif isinstance(arg_i, dict):
|
|
# 1D list
|
|
_checks(arg_i, defaults)
|
|
|
|
# ### specs ###
|
|
if specs is None:
|
|
specs = [[{} for c in range(cols)] for r in range(rows)]
|
|
elif not (
|
|
isinstance(specs, (list, tuple))
|
|
and specs
|
|
and all(isinstance(row, (list, tuple)) for row in specs)
|
|
and len(specs) == rows
|
|
and all(len(row) == cols for row in specs)
|
|
and all(all(v is None or isinstance(v, dict) for v in row) for row in specs)
|
|
):
|
|
raise ValueError(
|
|
"""
|
|
The 'specs' argument to make_subplots must be a 2D list of dictionaries with \
|
|
dimensions ({rows} x {cols}).
|
|
Received value of type {typ}: {val}""".format(
|
|
rows=rows, cols=cols, typ=type(specs), val=repr(specs)
|
|
)
|
|
)
|
|
|
|
for row in specs:
|
|
for spec in row:
|
|
# For backward compatibility,
|
|
# convert is_3d flag to type='scene' kwarg
|
|
if spec and spec.pop("is_3d", None):
|
|
spec["type"] = "scene"
|
|
|
|
spec_defaults = dict(
|
|
type="xy", secondary_y=False, colspan=1, rowspan=1, l=0.0, r=0.0, b=0.0, t=0.0
|
|
)
|
|
_check_keys_and_fill("specs", specs, spec_defaults)
|
|
|
|
# Validate secondary_y
|
|
has_secondary_y = False
|
|
for row in specs:
|
|
for spec in row:
|
|
if spec is not None:
|
|
has_secondary_y = has_secondary_y or spec["secondary_y"]
|
|
if spec and spec["type"] != "xy" and spec["secondary_y"]:
|
|
raise ValueError(
|
|
"""
|
|
The 'secondary_y' spec property is not supported for subplot of type '{s_typ}'
|
|
'secondary_y' is only supported for subplots of type 'xy'
|
|
""".format(
|
|
s_typ=spec["type"]
|
|
)
|
|
)
|
|
|
|
# ### insets ###
|
|
if insets is None or insets is False:
|
|
insets = []
|
|
elif not (
|
|
isinstance(insets, (list, tuple)) and all(isinstance(v, dict) for v in insets)
|
|
):
|
|
raise ValueError(
|
|
"""
|
|
The 'insets' argument to make_suplots must be a list of dictionaries.
|
|
Received value of type {typ}: {val}""".format(
|
|
typ=type(insets), val=repr(insets)
|
|
)
|
|
)
|
|
|
|
if insets:
|
|
for inset in insets:
|
|
if inset and inset.pop("is_3d", None):
|
|
inset["type"] = "scene"
|
|
|
|
inset_defaults = dict(
|
|
cell=(1, 1), type="xy", l=0.0, w="to_end", b=0.0, h="to_end"
|
|
)
|
|
_check_keys_and_fill("insets", insets, inset_defaults)
|
|
|
|
# ### shared_xaxes / shared_yaxes
|
|
valid_shared_vals = [None, True, False, "rows", "columns", "all"]
|
|
shared_err_msg = """
|
|
The {arg} argument to make_subplots must be one of: {valid_vals}
|
|
Received value of type {typ}: {val}"""
|
|
|
|
if shared_xaxes not in valid_shared_vals:
|
|
val = shared_xaxes
|
|
raise ValueError(
|
|
shared_err_msg.format(
|
|
arg="shared_xaxes",
|
|
valid_vals=valid_shared_vals,
|
|
typ=type(val),
|
|
val=repr(val),
|
|
)
|
|
)
|
|
if shared_yaxes not in valid_shared_vals:
|
|
val = shared_yaxes
|
|
raise ValueError(
|
|
shared_err_msg.format(
|
|
arg="shared_yaxes",
|
|
valid_vals=valid_shared_vals,
|
|
typ=type(val),
|
|
val=repr(val),
|
|
)
|
|
)
|
|
|
|
def _check_hv_spacing(dimsize, spacing, name, dimvarname, dimname):
|
|
if spacing < 0 or spacing > 1:
|
|
raise ValueError("%s spacing must be between 0 and 1." % (name,))
|
|
if dimsize <= 1:
|
|
return
|
|
max_spacing = 1.0 / float(dimsize - 1)
|
|
if spacing > max_spacing:
|
|
raise ValueError(
|
|
"""{name} spacing cannot be greater than (1 / ({dimvarname} - 1)) = {max_spacing:f}.
|
|
The resulting plot would have {dimsize} {dimname} ({dimvarname}={dimsize}).""".format(
|
|
dimvarname=dimvarname,
|
|
name=name,
|
|
dimname=dimname,
|
|
max_spacing=max_spacing,
|
|
dimsize=dimsize,
|
|
)
|
|
)
|
|
|
|
# ### horizontal_spacing ###
|
|
if horizontal_spacing is None:
|
|
if has_secondary_y:
|
|
horizontal_spacing = 0.4 / cols
|
|
else:
|
|
horizontal_spacing = 0.2 / cols
|
|
# check horizontal_spacing can be satisfied:
|
|
_check_hv_spacing(cols, horizontal_spacing, "Horizontal", "cols", "columns")
|
|
|
|
# ### vertical_spacing ###
|
|
if vertical_spacing is None:
|
|
if subplot_titles is not None:
|
|
vertical_spacing = 0.5 / rows
|
|
else:
|
|
vertical_spacing = 0.3 / rows
|
|
# check vertical_spacing can be satisfied:
|
|
_check_hv_spacing(rows, vertical_spacing, "Vertical", "rows", "rows")
|
|
|
|
# ### subplot titles ###
|
|
if subplot_titles is None:
|
|
subplot_titles = [""] * rows * cols
|
|
|
|
# ### column_widths ###
|
|
if has_secondary_y:
|
|
# Add room for secondary y-axis title
|
|
max_width = 0.94
|
|
elif row_titles:
|
|
# Add a little breathing room between row labels and legend
|
|
max_width = 0.98
|
|
else:
|
|
max_width = 1.0
|
|
|
|
if column_widths is None:
|
|
widths = [(max_width - horizontal_spacing * (cols - 1)) / cols] * cols
|
|
elif isinstance(column_widths, (list, tuple)) and len(column_widths) == cols:
|
|
cum_sum = float(sum(column_widths))
|
|
widths = []
|
|
for w in column_widths:
|
|
widths.append((max_width - horizontal_spacing * (cols - 1)) * (w / cum_sum))
|
|
else:
|
|
raise ValueError(
|
|
"""
|
|
The 'column_widths' argument to make_suplots must be a list of numbers of \
|
|
length {cols}.
|
|
Received value of type {typ}: {val}""".format(
|
|
cols=cols, typ=type(column_widths), val=repr(column_widths)
|
|
)
|
|
)
|
|
|
|
# ### row_heights ###
|
|
if row_heights is None:
|
|
heights = [(1.0 - vertical_spacing * (rows - 1)) / rows] * rows
|
|
elif isinstance(row_heights, (list, tuple)) and len(row_heights) == rows:
|
|
cum_sum = float(sum(row_heights))
|
|
heights = []
|
|
for h in row_heights:
|
|
heights.append((1.0 - vertical_spacing * (rows - 1)) * (h / cum_sum))
|
|
if row_dir < 0 and not use_legacy_row_heights_order:
|
|
heights = list(reversed(heights))
|
|
else:
|
|
raise ValueError(
|
|
"""
|
|
The 'row_heights' argument to make_suplots must be a list of numbers of \
|
|
length {rows}.
|
|
Received value of type {typ}: {val}""".format(
|
|
rows=rows, typ=type(row_heights), val=repr(row_heights)
|
|
)
|
|
)
|
|
|
|
# ### column_titles / row_titles ###
|
|
if column_titles and not isinstance(column_titles, (list, tuple)):
|
|
raise ValueError(
|
|
"""
|
|
The column_titles argument to make_subplots must be a list or tuple
|
|
Received value of type {typ}: {val}""".format(
|
|
typ=type(column_titles), val=repr(column_titles)
|
|
)
|
|
)
|
|
|
|
if row_titles and not isinstance(row_titles, (list, tuple)):
|
|
raise ValueError(
|
|
"""
|
|
The row_titles argument to make_subplots must be a list or tuple
|
|
Received value of type {typ}: {val}""".format(
|
|
typ=type(row_titles), val=repr(row_titles)
|
|
)
|
|
)
|
|
|
|
# Init layout
|
|
# -----------
|
|
layout = go.Layout()
|
|
|
|
# Build grid reference
|
|
# --------------------
|
|
# Built row/col sequence using 'row_dir' and 'col_dir'
|
|
col_seq = range(cols)[::col_dir]
|
|
row_seq = range(rows)[::row_dir]
|
|
|
|
# Build 2D array of tuples of the start x and start y coordinate of each
|
|
# subplot
|
|
grid = [
|
|
[
|
|
(
|
|
(sum(widths[:c]) + c * horizontal_spacing),
|
|
(sum(heights[:r]) + r * vertical_spacing),
|
|
)
|
|
for c in col_seq
|
|
]
|
|
for r in row_seq
|
|
]
|
|
|
|
domains_grid = [[None for _ in range(cols)] for _ in range(rows)]
|
|
|
|
# Initialize subplot reference lists for the grid and insets
|
|
grid_ref = [[None for c in range(cols)] for r in range(rows)]
|
|
|
|
list_of_domains = [] # added for subplot titles
|
|
|
|
max_subplot_ids = _get_initial_max_subplot_ids()
|
|
|
|
# Loop through specs -- (r, c) <-> (row, col)
|
|
for r, spec_row in enumerate(specs):
|
|
for c, spec in enumerate(spec_row):
|
|
if spec is None: # skip over None cells
|
|
continue
|
|
|
|
# ### Compute x and y domain for subplot ###
|
|
c_spanned = c + spec["colspan"] - 1 # get spanned c
|
|
r_spanned = r + spec["rowspan"] - 1 # get spanned r
|
|
|
|
# Throw exception if 'colspan' | 'rowspan' is too large for grid
|
|
if c_spanned >= cols:
|
|
raise Exception(
|
|
"Some 'colspan' value is too large for " "this subplot grid."
|
|
)
|
|
if r_spanned >= rows:
|
|
raise Exception(
|
|
"Some 'rowspan' value is too large for " "this subplot grid."
|
|
)
|
|
|
|
# Get x domain using grid and colspan
|
|
x_s = grid[r][c][0] + spec["l"]
|
|
|
|
x_e = grid[r][c_spanned][0] + widths[c_spanned] - spec["r"]
|
|
x_domain = [x_s, x_e]
|
|
|
|
# Get y domain (dep. on row_dir) using grid & r_spanned
|
|
if row_dir > 0:
|
|
y_s = grid[r][c][1] + spec["b"]
|
|
y_e = grid[r_spanned][c][1] + heights[r_spanned] - spec["t"]
|
|
else:
|
|
y_s = grid[r_spanned][c][1] + spec["b"]
|
|
y_e = grid[r][c][1] + heights[-1 - r] - spec["t"]
|
|
|
|
if y_s < 0.0:
|
|
# round for values very close to one
|
|
# handles some floating point errors
|
|
if y_s > -0.01:
|
|
y_s = 0.0
|
|
else:
|
|
raise Exception(
|
|
"A combination of the 'b' values, heights, and "
|
|
"number of subplots too large for this subplot grid."
|
|
)
|
|
if y_s > 1.0:
|
|
# round for values very close to one
|
|
# handles some floating point errors
|
|
if y_s < 1.01:
|
|
y_s = 1.0
|
|
else:
|
|
raise Exception(
|
|
"A combination of the 'b' values, heights, and "
|
|
"number of subplots too large for this subplot grid."
|
|
)
|
|
|
|
if y_e < 0.0:
|
|
if y_e > -0.01:
|
|
y_e = 0.0
|
|
else:
|
|
raise Exception(
|
|
"A combination of the 't' values, heights, and "
|
|
"number of subplots too large for this subplot grid."
|
|
)
|
|
|
|
if y_e > 1.0:
|
|
if y_e < 1.01:
|
|
y_e = 1.0
|
|
else:
|
|
raise Exception(
|
|
"A combination of the 't' values, heights, and "
|
|
"number of subplots too large for this subplot grid."
|
|
)
|
|
|
|
y_domain = [y_s, y_e]
|
|
|
|
list_of_domains.append(x_domain)
|
|
list_of_domains.append(y_domain)
|
|
|
|
domains_grid[r][c] = [x_domain, y_domain]
|
|
|
|
# ### construct subplot container ###
|
|
subplot_type = spec["type"]
|
|
secondary_y = spec["secondary_y"]
|
|
subplot_refs = _init_subplot(
|
|
layout, subplot_type, secondary_y, x_domain, y_domain, max_subplot_ids
|
|
)
|
|
grid_ref[r][c] = subplot_refs
|
|
|
|
_configure_shared_axes(layout, grid_ref, specs, "x", shared_xaxes, row_dir)
|
|
_configure_shared_axes(layout, grid_ref, specs, "y", shared_yaxes, row_dir)
|
|
|
|
# Build inset reference
|
|
# ---------------------
|
|
# Loop through insets
|
|
insets_ref = [None for inset in range(len(insets))] if insets else None
|
|
if insets:
|
|
for i_inset, inset in enumerate(insets):
|
|
r = inset["cell"][0] - 1
|
|
c = inset["cell"][1] - 1
|
|
|
|
# Throw exception if r | c is out of range
|
|
if not (0 <= r < rows):
|
|
raise Exception(
|
|
"Some 'cell' row value is out of range. "
|
|
"Note: the starting cell is (1, 1)"
|
|
)
|
|
if not (0 <= c < cols):
|
|
raise Exception(
|
|
"Some 'cell' col value is out of range. "
|
|
"Note: the starting cell is (1, 1)"
|
|
)
|
|
|
|
# Get inset x domain using grid
|
|
x_s = grid[r][c][0] + inset["l"] * widths[c]
|
|
if inset["w"] == "to_end":
|
|
x_e = grid[r][c][0] + widths[c]
|
|
else:
|
|
x_e = x_s + inset["w"] * widths[c]
|
|
x_domain = [x_s, x_e]
|
|
|
|
# Get inset y domain using grid
|
|
y_s = grid[r][c][1] + inset["b"] * heights[-1 - r]
|
|
if inset["h"] == "to_end":
|
|
y_e = grid[r][c][1] + heights[-1 - r]
|
|
else:
|
|
y_e = y_s + inset["h"] * heights[-1 - r]
|
|
y_domain = [y_s, y_e]
|
|
|
|
list_of_domains.append(x_domain)
|
|
list_of_domains.append(y_domain)
|
|
|
|
subplot_type = inset["type"]
|
|
|
|
subplot_refs = _init_subplot(
|
|
layout, subplot_type, False, x_domain, y_domain, max_subplot_ids
|
|
)
|
|
|
|
insets_ref[i_inset] = subplot_refs
|
|
|
|
# Build grid_str
|
|
# This is the message printed when print_grid=True
|
|
grid_str = _build_grid_str(specs, grid_ref, insets, insets_ref, row_seq)
|
|
|
|
# Add subplot titles
|
|
plot_title_annotations = _build_subplot_title_annotations(
|
|
subplot_titles, list_of_domains
|
|
)
|
|
|
|
layout["annotations"] = plot_title_annotations
|
|
|
|
# Add column titles
|
|
if column_titles:
|
|
domains_list = []
|
|
if row_dir > 0:
|
|
for c in range(cols):
|
|
domain_pair = domains_grid[-1][c]
|
|
if domain_pair:
|
|
domains_list.extend(domain_pair)
|
|
else:
|
|
for c in range(cols):
|
|
domain_pair = domains_grid[0][c]
|
|
if domain_pair:
|
|
domains_list.extend(domain_pair)
|
|
|
|
# Add subplot titles
|
|
column_title_annotations = _build_subplot_title_annotations(
|
|
column_titles, domains_list
|
|
)
|
|
|
|
layout["annotations"] += tuple(column_title_annotations)
|
|
|
|
if row_titles:
|
|
domains_list = []
|
|
for r in range(rows):
|
|
domain_pair = domains_grid[r][-1]
|
|
if domain_pair:
|
|
domains_list.extend(domain_pair)
|
|
|
|
# Add subplot titles
|
|
column_title_annotations = _build_subplot_title_annotations(
|
|
row_titles, domains_list, title_edge="right"
|
|
)
|
|
|
|
layout["annotations"] += tuple(column_title_annotations)
|
|
|
|
if x_title:
|
|
domains_list = [(0, max_width), (0, 1)]
|
|
|
|
# Add subplot titles
|
|
column_title_annotations = _build_subplot_title_annotations(
|
|
[x_title], domains_list, title_edge="bottom", offset=30
|
|
)
|
|
|
|
layout["annotations"] += tuple(column_title_annotations)
|
|
|
|
if y_title:
|
|
domains_list = [(0, 1), (0, 1)]
|
|
|
|
# Add subplot titles
|
|
column_title_annotations = _build_subplot_title_annotations(
|
|
[y_title], domains_list, title_edge="left", offset=40
|
|
)
|
|
|
|
layout["annotations"] += tuple(column_title_annotations)
|
|
|
|
# Handle displaying grid information
|
|
if print_grid:
|
|
print(grid_str)
|
|
|
|
# Build resulting figure
|
|
if figure is None:
|
|
figure = go.Figure()
|
|
figure.update_layout(layout)
|
|
|
|
# Attach subplot grid info to the figure
|
|
figure.__dict__["_grid_ref"] = grid_ref
|
|
figure.__dict__["_grid_str"] = grid_str
|
|
|
|
return figure
|
|
|
|
|
|
def _configure_shared_axes(layout, grid_ref, specs, x_or_y, shared, row_dir):
|
|
rows = len(grid_ref)
|
|
cols = len(grid_ref[0])
|
|
|
|
layout_key_ind = ["x", "y"].index(x_or_y)
|
|
|
|
if row_dir < 0:
|
|
rows_iter = range(rows - 1, -1, -1)
|
|
else:
|
|
rows_iter = range(rows)
|
|
|
|
def update_axis_matches(first_axis_id, subplot_ref, spec, remove_label):
|
|
if subplot_ref is None:
|
|
return first_axis_id
|
|
|
|
if x_or_y == "x":
|
|
span = spec["colspan"]
|
|
else:
|
|
span = spec["rowspan"]
|
|
|
|
if subplot_ref.subplot_type == "xy" and span == 1:
|
|
if first_axis_id is None:
|
|
first_axis_name = subplot_ref.layout_keys[layout_key_ind]
|
|
first_axis_id = first_axis_name.replace("axis", "")
|
|
else:
|
|
axis_name = subplot_ref.layout_keys[layout_key_ind]
|
|
axis_to_match = layout[axis_name]
|
|
axis_to_match.matches = first_axis_id
|
|
if remove_label:
|
|
axis_to_match.showticklabels = False
|
|
|
|
return first_axis_id
|
|
|
|
if shared == "columns" or (x_or_y == "x" and shared is True):
|
|
for c in range(cols):
|
|
first_axis_id = None
|
|
ok_to_remove_label = x_or_y == "x"
|
|
for r in rows_iter:
|
|
if not grid_ref[r][c]:
|
|
continue
|
|
subplot_ref = grid_ref[r][c][0]
|
|
spec = specs[r][c]
|
|
first_axis_id = update_axis_matches(
|
|
first_axis_id, subplot_ref, spec, ok_to_remove_label
|
|
)
|
|
|
|
elif shared == "rows" or (x_or_y == "y" and shared is True):
|
|
for r in rows_iter:
|
|
first_axis_id = None
|
|
ok_to_remove_label = x_or_y == "y"
|
|
for c in range(cols):
|
|
if not grid_ref[r][c]:
|
|
continue
|
|
subplot_ref = grid_ref[r][c][0]
|
|
spec = specs[r][c]
|
|
first_axis_id = update_axis_matches(
|
|
first_axis_id, subplot_ref, spec, ok_to_remove_label
|
|
)
|
|
|
|
elif shared == "all":
|
|
first_axis_id = None
|
|
for c in range(cols):
|
|
for ri, r in enumerate(rows_iter):
|
|
if not grid_ref[r][c]:
|
|
continue
|
|
subplot_ref = grid_ref[r][c][0]
|
|
spec = specs[r][c]
|
|
|
|
if x_or_y == "y":
|
|
ok_to_remove_label = c > 0
|
|
else:
|
|
ok_to_remove_label = ri > 0 if row_dir > 0 else r < rows - 1
|
|
|
|
first_axis_id = update_axis_matches(
|
|
first_axis_id, subplot_ref, spec, ok_to_remove_label
|
|
)
|
|
|
|
|
|
def _init_subplot_xy(layout, secondary_y, x_domain, y_domain, max_subplot_ids=None):
|
|
if max_subplot_ids is None:
|
|
max_subplot_ids = _get_initial_max_subplot_ids()
|
|
|
|
# Get axis label and anchor
|
|
x_cnt = max_subplot_ids["xaxis"] + 1
|
|
y_cnt = max_subplot_ids["yaxis"] + 1
|
|
|
|
# Compute x/y labels (the values of trace.xaxis/trace.yaxis
|
|
x_label = "x{cnt}".format(cnt=x_cnt if x_cnt > 1 else "")
|
|
y_label = "y{cnt}".format(cnt=y_cnt if y_cnt > 1 else "")
|
|
|
|
# Anchor x and y axes to each other
|
|
x_anchor, y_anchor = y_label, x_label
|
|
|
|
# Build layout.xaxis/layout.yaxis containers
|
|
xaxis_name = "xaxis{cnt}".format(cnt=x_cnt if x_cnt > 1 else "")
|
|
yaxis_name = "yaxis{cnt}".format(cnt=y_cnt if y_cnt > 1 else "")
|
|
x_axis = {"domain": x_domain, "anchor": x_anchor}
|
|
y_axis = {"domain": y_domain, "anchor": y_anchor}
|
|
|
|
layout[xaxis_name] = x_axis
|
|
layout[yaxis_name] = y_axis
|
|
|
|
subplot_refs = [
|
|
SubplotRef(
|
|
subplot_type="xy",
|
|
layout_keys=(xaxis_name, yaxis_name),
|
|
trace_kwargs={"xaxis": x_label, "yaxis": y_label},
|
|
)
|
|
]
|
|
|
|
if secondary_y:
|
|
y_cnt += 1
|
|
secondary_yaxis_name = "yaxis{cnt}".format(cnt=y_cnt if y_cnt > 1 else "")
|
|
secondary_y_label = "y{cnt}".format(cnt=y_cnt)
|
|
|
|
# Add secondary y-axis to subplot reference
|
|
subplot_refs.append(
|
|
SubplotRef(
|
|
subplot_type="xy",
|
|
layout_keys=(xaxis_name, secondary_yaxis_name),
|
|
trace_kwargs={"xaxis": x_label, "yaxis": secondary_y_label},
|
|
)
|
|
)
|
|
|
|
# Add secondary y axis to layout
|
|
secondary_y_axis = {"anchor": y_anchor, "overlaying": y_label, "side": "right"}
|
|
layout[secondary_yaxis_name] = secondary_y_axis
|
|
|
|
# increment max_subplot_ids
|
|
max_subplot_ids["xaxis"] = x_cnt
|
|
max_subplot_ids["yaxis"] = y_cnt
|
|
|
|
return tuple(subplot_refs)
|
|
|
|
|
|
def _init_subplot_single(
|
|
layout, subplot_type, x_domain, y_domain, max_subplot_ids=None
|
|
):
|
|
if max_subplot_ids is None:
|
|
max_subplot_ids = _get_initial_max_subplot_ids()
|
|
|
|
# Add scene to layout
|
|
cnt = max_subplot_ids[subplot_type] + 1
|
|
label = "{subplot_type}{cnt}".format(
|
|
subplot_type=subplot_type, cnt=cnt if cnt > 1 else ""
|
|
)
|
|
scene = dict(domain={"x": x_domain, "y": y_domain})
|
|
layout[label] = scene
|
|
|
|
trace_key = (
|
|
"subplot" if subplot_type in _subplot_prop_named_subplot else subplot_type
|
|
)
|
|
|
|
subplot_ref = SubplotRef(
|
|
subplot_type=subplot_type, layout_keys=(label,), trace_kwargs={trace_key: label}
|
|
)
|
|
|
|
# increment max_subplot_id
|
|
max_subplot_ids[subplot_type] = cnt
|
|
|
|
return (subplot_ref,)
|
|
|
|
|
|
def _init_subplot_domain(x_domain, y_domain):
|
|
# No change to layout since domain traces are labeled individually
|
|
subplot_ref = SubplotRef(
|
|
subplot_type="domain",
|
|
layout_keys=(),
|
|
trace_kwargs={"domain": {"x": tuple(x_domain), "y": tuple(y_domain)}},
|
|
)
|
|
|
|
return (subplot_ref,)
|
|
|
|
|
|
def _subplot_type_for_trace_type(trace_type):
|
|
from plotly.validators import DataValidator
|
|
|
|
trace_validator = DataValidator()
|
|
if trace_type in trace_validator.class_strs_map:
|
|
# subplot_type is a trace name, find the subplot type for trace
|
|
trace = trace_validator.validate_coerce([{"type": trace_type}])[0]
|
|
if "domain" in trace:
|
|
return "domain"
|
|
elif "xaxis" in trace and "yaxis" in trace:
|
|
return "xy"
|
|
elif "geo" in trace:
|
|
return "geo"
|
|
elif "scene" in trace:
|
|
return "scene"
|
|
elif "subplot" in trace:
|
|
for t in _subplot_prop_named_subplot:
|
|
try:
|
|
trace.subplot = t
|
|
return t
|
|
except ValueError:
|
|
pass
|
|
|
|
return None
|
|
|
|
|
|
def _validate_coerce_subplot_type(subplot_type):
|
|
# Lowercase subplot_type
|
|
orig_subplot_type = subplot_type
|
|
subplot_type = subplot_type.lower()
|
|
|
|
# Check if it's a named subplot type
|
|
if subplot_type in _subplot_types:
|
|
return subplot_type
|
|
|
|
# Try to determine subplot type for trace
|
|
subplot_type = _subplot_type_for_trace_type(subplot_type)
|
|
|
|
if subplot_type is None:
|
|
raise ValueError("Unsupported subplot type: {}".format(repr(orig_subplot_type)))
|
|
else:
|
|
return subplot_type
|
|
|
|
|
|
def _init_subplot(
|
|
layout, subplot_type, secondary_y, x_domain, y_domain, max_subplot_ids=None
|
|
):
|
|
# Normalize subplot type
|
|
subplot_type = _validate_coerce_subplot_type(subplot_type)
|
|
|
|
if max_subplot_ids is None:
|
|
max_subplot_ids = _get_initial_max_subplot_ids()
|
|
|
|
# Clamp domain elements between [0, 1].
|
|
# This is only needed to combat numerical precision errors
|
|
# See GH1031
|
|
x_domain = [max(0.0, x_domain[0]), min(1.0, x_domain[1])]
|
|
y_domain = [max(0.0, y_domain[0]), min(1.0, y_domain[1])]
|
|
|
|
if subplot_type == "xy":
|
|
subplot_refs = _init_subplot_xy(
|
|
layout, secondary_y, x_domain, y_domain, max_subplot_ids
|
|
)
|
|
elif subplot_type in _single_subplot_types:
|
|
subplot_refs = _init_subplot_single(
|
|
layout, subplot_type, x_domain, y_domain, max_subplot_ids
|
|
)
|
|
elif subplot_type == "domain":
|
|
subplot_refs = _init_subplot_domain(x_domain, y_domain)
|
|
else:
|
|
raise ValueError("Unsupported subplot type: {}".format(repr(subplot_type)))
|
|
|
|
return subplot_refs
|
|
|
|
|
|
def _get_cartesian_label(x_or_y, r, c, cnt):
|
|
# Default label (given strictly by cnt)
|
|
label = "{x_or_y}{cnt}".format(x_or_y=x_or_y, cnt=cnt)
|
|
return label
|
|
|
|
|
|
def _build_subplot_title_annotations(
|
|
subplot_titles, list_of_domains, title_edge="top", offset=0
|
|
):
|
|
# If shared_axes is False (default) use list_of_domains
|
|
# This is used for insets and irregular layouts
|
|
# if not shared_xaxes and not shared_yaxes:
|
|
x_dom = list_of_domains[::2]
|
|
y_dom = list_of_domains[1::2]
|
|
subtitle_pos_x = []
|
|
subtitle_pos_y = []
|
|
|
|
if title_edge == "top":
|
|
text_angle = 0
|
|
xanchor = "center"
|
|
yanchor = "bottom"
|
|
|
|
for x_domains in x_dom:
|
|
subtitle_pos_x.append(sum(x_domains) / 2.0)
|
|
for y_domains in y_dom:
|
|
subtitle_pos_y.append(y_domains[1])
|
|
|
|
yshift = offset
|
|
xshift = 0
|
|
elif title_edge == "bottom":
|
|
text_angle = 0
|
|
xanchor = "center"
|
|
yanchor = "top"
|
|
|
|
for x_domains in x_dom:
|
|
subtitle_pos_x.append(sum(x_domains) / 2.0)
|
|
for y_domains in y_dom:
|
|
subtitle_pos_y.append(y_domains[0])
|
|
|
|
yshift = -offset
|
|
xshift = 0
|
|
elif title_edge == "right":
|
|
text_angle = 90
|
|
xanchor = "left"
|
|
yanchor = "middle"
|
|
|
|
for x_domains in x_dom:
|
|
subtitle_pos_x.append(x_domains[1])
|
|
for y_domains in y_dom:
|
|
subtitle_pos_y.append(sum(y_domains) / 2.0)
|
|
|
|
yshift = 0
|
|
xshift = offset
|
|
elif title_edge == "left":
|
|
text_angle = -90
|
|
xanchor = "right"
|
|
yanchor = "middle"
|
|
|
|
for x_domains in x_dom:
|
|
subtitle_pos_x.append(x_domains[0])
|
|
for y_domains in y_dom:
|
|
subtitle_pos_y.append(sum(y_domains) / 2.0)
|
|
|
|
yshift = 0
|
|
xshift = -offset
|
|
else:
|
|
raise ValueError("Invalid annotation edge '{edge}'".format(edge=title_edge))
|
|
|
|
plot_titles = []
|
|
for index in range(len(subplot_titles)):
|
|
if not subplot_titles[index] or index >= len(subtitle_pos_y):
|
|
pass
|
|
else:
|
|
annot = {
|
|
"y": subtitle_pos_y[index],
|
|
"xref": "paper",
|
|
"x": subtitle_pos_x[index],
|
|
"yref": "paper",
|
|
"text": subplot_titles[index],
|
|
"showarrow": False,
|
|
"font": dict(size=16),
|
|
"xanchor": xanchor,
|
|
"yanchor": yanchor,
|
|
}
|
|
|
|
if xshift != 0:
|
|
annot["xshift"] = xshift
|
|
|
|
if yshift != 0:
|
|
annot["yshift"] = yshift
|
|
|
|
if text_angle != 0:
|
|
annot["textangle"] = text_angle
|
|
|
|
plot_titles.append(annot)
|
|
return plot_titles
|
|
|
|
|
|
def _build_grid_str(specs, grid_ref, insets, insets_ref, row_seq):
|
|
# Compute rows and columns
|
|
rows = len(specs)
|
|
cols = len(specs[0])
|
|
|
|
# Initialize constants
|
|
sp = " " # space between cell
|
|
s_str = "[ " # cell start string
|
|
e_str = " ]" # cell end string
|
|
|
|
s_top = "⎡ " # U+23A1
|
|
s_mid = "⎢ " # U+23A2
|
|
s_bot = "⎣ " # U+23A3
|
|
|
|
e_top = " ⎤" # U+23A4
|
|
e_mid = " ⎟" # U+239F
|
|
e_bot = " ⎦" # U+23A6
|
|
|
|
colspan_str = " -" # colspan string
|
|
rowspan_str = " :" # rowspan string
|
|
empty_str = " (empty) " # empty cell string
|
|
# Init grid_str with intro message
|
|
grid_str = "This is the format of your plot grid:\n"
|
|
|
|
# Init tmp list of lists of strings (sorta like 'grid_ref' but w/ strings)
|
|
_tmp = [["" for c in range(cols)] for r in range(rows)]
|
|
|
|
# Define cell string as function of (r, c) and grid_ref
|
|
def _get_cell_str(r, c, subplot_refs):
|
|
layout_keys = sorted({k for ref in subplot_refs for k in ref.layout_keys})
|
|
|
|
ref_str = ",".join(layout_keys)
|
|
|
|
# Replace yaxis2 -> y2
|
|
ref_str = ref_str.replace("axis", "")
|
|
return "({r},{c}) {ref}".format(r=r + 1, c=c + 1, ref=ref_str)
|
|
|
|
# Find max len of _cell_str, add define a padding function
|
|
cell_len = (
|
|
max(
|
|
[
|
|
len(_get_cell_str(r, c, ref))
|
|
for r, row_ref in enumerate(grid_ref)
|
|
for c, ref in enumerate(row_ref)
|
|
if ref
|
|
]
|
|
)
|
|
+ len(s_str)
|
|
+ len(e_str)
|
|
)
|
|
|
|
def _pad(s, cell_len=cell_len):
|
|
return " " * (cell_len - len(s))
|
|
|
|
# Loop through specs, fill in _tmp
|
|
for r, spec_row in enumerate(specs):
|
|
for c, spec in enumerate(spec_row):
|
|
ref = grid_ref[r][c]
|
|
if ref is None:
|
|
if _tmp[r][c] == "":
|
|
_tmp[r][c] = empty_str + _pad(empty_str)
|
|
continue
|
|
|
|
if spec["rowspan"] > 1:
|
|
cell_str = s_top + _get_cell_str(r, c, ref)
|
|
else:
|
|
cell_str = s_str + _get_cell_str(r, c, ref)
|
|
|
|
if spec["colspan"] > 1:
|
|
for cc in range(1, spec["colspan"] - 1):
|
|
_tmp[r][c + cc] = colspan_str + _pad(colspan_str)
|
|
|
|
if spec["rowspan"] > 1:
|
|
_tmp[r][c + spec["colspan"] - 1] = (
|
|
colspan_str + _pad(colspan_str + e_str)
|
|
) + e_top
|
|
else:
|
|
_tmp[r][c + spec["colspan"] - 1] = (
|
|
colspan_str + _pad(colspan_str + e_str)
|
|
) + e_str
|
|
else:
|
|
padding = " " * (cell_len - len(cell_str) - 2)
|
|
if spec["rowspan"] > 1:
|
|
cell_str += padding + e_top
|
|
else:
|
|
cell_str += padding + e_str
|
|
|
|
if spec["rowspan"] > 1:
|
|
for cc in range(spec["colspan"]):
|
|
for rr in range(1, spec["rowspan"]):
|
|
row_str = rowspan_str + _pad(rowspan_str)
|
|
if cc == 0:
|
|
if rr < spec["rowspan"] - 1:
|
|
row_str = s_mid + row_str[2:]
|
|
else:
|
|
row_str = s_bot + row_str[2:]
|
|
|
|
if cc == spec["colspan"] - 1:
|
|
if rr < spec["rowspan"] - 1:
|
|
row_str = row_str[:-2] + e_mid
|
|
else:
|
|
row_str = row_str[:-2] + e_bot
|
|
|
|
_tmp[r + rr][c + cc] = row_str
|
|
|
|
_tmp[r][c] = cell_str + _pad(cell_str)
|
|
|
|
# Append grid_str using data from _tmp in the correct order
|
|
for r in row_seq[::-1]:
|
|
grid_str += sp.join(_tmp[r]) + "\n"
|
|
|
|
# Append grid_str to include insets info
|
|
if insets:
|
|
grid_str += "\nWith insets:\n"
|
|
for i_inset, inset in enumerate(insets):
|
|
r = inset["cell"][0] - 1
|
|
c = inset["cell"][1] - 1
|
|
ref = grid_ref[r][c]
|
|
|
|
subplot_labels_str = ",".join(insets_ref[i_inset][0].layout_keys)
|
|
|
|
# Replace, e.g., yaxis2 -> y2
|
|
subplot_labels_str = subplot_labels_str.replace("axis", "")
|
|
|
|
grid_str += (
|
|
s_str
|
|
+ subplot_labels_str
|
|
+ e_str
|
|
+ " over "
|
|
+ s_str
|
|
+ _get_cell_str(r, c, ref)
|
|
+ e_str
|
|
+ "\n"
|
|
)
|
|
return grid_str
|
|
|
|
|
|
def _set_trace_grid_reference(trace, layout, grid_ref, row, col, secondary_y=False):
|
|
if row <= 0:
|
|
raise Exception(
|
|
"Row value is out of range. " "Note: the starting cell is (1, 1)"
|
|
)
|
|
if col <= 0:
|
|
raise Exception(
|
|
"Col value is out of range. " "Note: the starting cell is (1, 1)"
|
|
)
|
|
try:
|
|
subplot_refs = grid_ref[row - 1][col - 1]
|
|
except IndexError:
|
|
raise Exception(
|
|
"The (row, col) pair sent is out of "
|
|
"range. Use Figure.print_grid to view the "
|
|
"subplot grid. "
|
|
)
|
|
|
|
if not subplot_refs:
|
|
raise ValueError(
|
|
"""
|
|
No subplot specified at grid position ({row}, {col})""".format(
|
|
row=row, col=col
|
|
)
|
|
)
|
|
|
|
if secondary_y:
|
|
if len(subplot_refs) < 2:
|
|
raise ValueError(
|
|
"""
|
|
Subplot with type '{subplot_type}' at grid position ({row}, {col}) was not
|
|
created with the secondary_y spec property set to True. See the docstring
|
|
for the specs argument to plotly.subplots.make_subplots for more information.
|
|
"""
|
|
)
|
|
trace_kwargs = subplot_refs[1].trace_kwargs
|
|
else:
|
|
trace_kwargs = subplot_refs[0].trace_kwargs
|
|
|
|
for k in trace_kwargs:
|
|
if k not in trace:
|
|
raise ValueError(
|
|
"""\
|
|
Trace type '{typ}' is not compatible with subplot type '{subplot_type}'
|
|
at grid position ({row}, {col})
|
|
|
|
See the docstring for the specs argument to plotly.subplots.make_subplots
|
|
for more information on subplot types""".format(
|
|
typ=trace.type,
|
|
subplot_type=subplot_refs[0].subplot_type,
|
|
row=row,
|
|
col=col,
|
|
)
|
|
)
|
|
|
|
# Update trace reference
|
|
trace.update(trace_kwargs)
|
|
|
|
|
|
def _get_grid_subplot(fig, row, col, secondary_y=False):
|
|
try:
|
|
grid_ref = fig._grid_ref
|
|
except AttributeError:
|
|
raise Exception(
|
|
"In order to reference traces by row and column, "
|
|
"you must first use "
|
|
"plotly.tools.make_subplots "
|
|
"to create the figure with a subplot grid."
|
|
)
|
|
|
|
rows = len(grid_ref)
|
|
cols = len(grid_ref[0])
|
|
|
|
# Validate row
|
|
if not isinstance(row, int) or row < 1 or rows < row:
|
|
raise ValueError(
|
|
"""
|
|
The row argument to get_subplot must be an integer where 1 <= row <= {rows}
|
|
Received value of type {typ}: {val}""".format(
|
|
rows=rows, typ=type(row), val=repr(row)
|
|
)
|
|
)
|
|
|
|
if not isinstance(col, int) or col < 1 or cols < col:
|
|
raise ValueError(
|
|
"""
|
|
The col argument to get_subplot must be an integer where 1 <= row <= {cols}
|
|
Received value of type {typ}: {val}""".format(
|
|
cols=cols, typ=type(col), val=repr(col)
|
|
)
|
|
)
|
|
|
|
subplot_refs = fig._grid_ref[row - 1][col - 1]
|
|
if not subplot_refs:
|
|
return None
|
|
|
|
if secondary_y:
|
|
if len(subplot_refs) > 1:
|
|
layout_keys = subplot_refs[1].layout_keys
|
|
else:
|
|
return None
|
|
else:
|
|
layout_keys = subplot_refs[0].layout_keys
|
|
|
|
if len(layout_keys) == 0:
|
|
return SubplotDomain(**subplot_refs[0].trace_kwargs["domain"])
|
|
elif len(layout_keys) == 1:
|
|
return fig.layout[layout_keys[0]]
|
|
elif len(layout_keys) == 2:
|
|
return SubplotXY(
|
|
xaxis=fig.layout[layout_keys[0]], yaxis=fig.layout[layout_keys[1]]
|
|
)
|
|
else:
|
|
raise ValueError(
|
|
"""
|
|
Unexpected subplot type with layout_keys of {}""".format(
|
|
layout_keys
|
|
)
|
|
)
|
|
|
|
|
|
def _get_subplot_ref_for_trace(trace):
|
|
if "domain" in trace:
|
|
return SubplotRef(
|
|
subplot_type="domain",
|
|
layout_keys=(),
|
|
trace_kwargs={"domain": {"x": trace.domain.x, "y": trace.domain.y}},
|
|
)
|
|
|
|
elif "xaxis" in trace and "yaxis" in trace:
|
|
xaxis_name = "xaxis" + trace.xaxis[1:] if trace.xaxis else "xaxis"
|
|
yaxis_name = "yaxis" + trace.yaxis[1:] if trace.yaxis else "yaxis"
|
|
|
|
return SubplotRef(
|
|
subplot_type="xy",
|
|
layout_keys=(xaxis_name, yaxis_name),
|
|
trace_kwargs={"xaxis": trace.xaxis, "yaxis": trace.yaxis},
|
|
)
|
|
elif "geo" in trace:
|
|
return SubplotRef(
|
|
subplot_type="geo",
|
|
layout_keys=(trace.geo,),
|
|
trace_kwargs={"geo": trace.geo},
|
|
)
|
|
elif "scene" in trace:
|
|
return SubplotRef(
|
|
subplot_type="scene",
|
|
layout_keys=(trace.scene,),
|
|
trace_kwargs={"scene": trace.scene},
|
|
)
|
|
elif "subplot" in trace:
|
|
for t in _subplot_prop_named_subplot:
|
|
try:
|
|
validator = trace._get_prop_validator("subplot")
|
|
validator.validate_coerce(t)
|
|
return SubplotRef(
|
|
subplot_type=t,
|
|
layout_keys=(trace.subplot,),
|
|
trace_kwargs={"subplot": trace.subplot},
|
|
)
|
|
except ValueError:
|
|
pass
|
|
|
|
return None
|