wg-backend-django/dell-env/lib/python3.11/site-packages/plotly/_subplots.py

1537 lines
51 KiB
Python
Raw Normal View History

2023-10-30 03:40:43 -04:00
# 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