From 549a34a0d0cc72a2618b4e8b9b0d7e52490dd2b8 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 10 May 2019 07:44:37 -0400 Subject: [PATCH 01/18] secondary_y WIP --- plotly/basedatatypes.py | 23 +++++++-- plotly/subplots.py | 100 ++++++++++++++++++++++++++++++++++------ 2 files changed, 103 insertions(+), 20 deletions(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index cd8cd7ea909..c2d1341371c 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -796,23 +796,32 @@ def update_traces( return self def _select_layout_subplots_by_prefix( - self, prefix, selector=None, row=None, col=None): + self, prefix, selector=None, row=None, col=None, secondary_y=None): """ Helper called by code generated select_* methods """ if row is not None or col is not None: # Build mapping from container keys ('xaxis2', 'scene4', etc.) - # to row/col pairs + # to (row, col, secondary_y triplets) grid_ref = self._validate_get_grid_ref() container_to_row_col = {} for r, subplot_row in enumerate(grid_ref): for c, ref in enumerate(subplot_row): if ref is None: continue + + # collect primary keys for layout_key in ref['layout_keys']: if layout_key.startswith(prefix): - container_to_row_col[layout_key] = r + 1, c + 1 + container_to_row_col[layout_key] = ( + r + 1, c + 1, False) + + # collection secondary keys + for layout_key in ref.get('secondary_layout_keys', ()): + if layout_key.startswith(prefix): + container_to_row_col[layout_key] = ( + r + 1, c + 1, True) else: container_to_row_col = None @@ -831,6 +840,10 @@ def _select_layout_subplots_by_prefix( container_to_row_col.get(k, (None, None))[1] != col): # col specified and this is not a match continue + elif (secondary_y is not None and + container_to_row_col.get( + k, (None, None, None))[2] != secondary_y): + continue # Filter by selector if not self._selector_matches(self.layout[k], selector): @@ -1494,13 +1507,13 @@ def append_trace(self, trace, row, col): self.add_trace(trace=trace, row=row, col=col) - def _set_trace_grid_position(self, trace, row, col): + def _set_trace_grid_position(self, trace, row, col, secondary_y=False): grid_ref = self._validate_get_grid_ref() from _plotly_future_ import _future_flags if 'v4_subplots' in _future_flags: return _set_trace_grid_reference( - trace, self.layout, grid_ref, row, col) + trace, grid_ref, row, col, secondary_y) if row <= 0: raise Exception("Row value is out of range. " diff --git a/plotly/subplots.py b/plotly/subplots.py index eb0718619b5..26c0a75db50 100644 --- a/plotly/subplots.py +++ b/plotly/subplots.py @@ -27,7 +27,8 @@ # Named tuple to hold an xaxis/yaxis pair that represent a single subplot -SubplotXY = collections.namedtuple('SubplotXY', ('xaxis', 'yaxis')) +SubplotXY = collections.namedtuple('SubplotXY', + ('xaxis', 'yaxis', 'secondary_yaxis')) SubplotDomain = collections.namedtuple('SubplotDomain', ('x', 'y')) @@ -155,6 +156,9 @@ def make_subplots( - 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 @@ -461,14 +465,16 @@ def _checks(item, defaults): rows=rows, cols=cols, typ=type(specs), val=repr(specs) )) - # For backward compatibility, convert is_3d flag to type='scene' kwarg 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, @@ -478,6 +484,15 @@ def _checks(item, defaults): ) _check_keys_and_fill('specs', specs, spec_defaults) + # Validate secondary_y + for row in specs: + for spec in row: + 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 = [] @@ -612,8 +627,10 @@ def _checks(item, defaults): # ### construct subplot container ### subplot_type = spec['type'] + secondary_y = spec['secondary_y'] grid_ref_element = _init_subplot( - layout, subplot_type, x_domain, y_domain, max_subplot_ids) + layout, subplot_type, secondary_y, + x_domain, y_domain, max_subplot_ids) grid_ref[r][c] = grid_ref_element _configure_shared_axes(layout, grid_ref, specs, 'x', shared_xaxes, row_dir) @@ -842,7 +859,7 @@ def update_axis_matches(first_axis_id, ref, spec, remove_label): def _init_subplot_xy( - layout, x_domain, y_domain, max_subplot_ids=None + 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() @@ -873,6 +890,21 @@ def _init_subplot_xy( 'trace_kwargs': {'xaxis': x_label, 'yaxis': y_label} } + if secondary_y: + y_cnt += 1 + secondary_y_label = "y{cnt}".format(cnt=y_cnt) + ref_element['secondary_trace_kwargs'] = { + 'xaxis': x_label, 'yaxis': secondary_y_label + } + + secondary_yaxis_name = 'yaxis{cnt}'.format( + cnt=y_cnt if y_cnt > 1 else '') + ref_element['secondary_layout_keys'] = ( + xaxis_name, secondary_yaxis_name) + + secondary_y_axis = {'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 @@ -966,7 +998,8 @@ def _validate_coerce_subplot_type(subplot_type): def _init_subplot( - layout, subplot_type, x_domain, y_domain, max_subplot_ids=None + layout, subplot_type, secondary_y, + x_domain, y_domain, max_subplot_ids=None ): # Normalize subplot type subplot_type = _validate_coerce_subplot_type(subplot_type) @@ -982,7 +1015,7 @@ def _init_subplot( if subplot_type == 'xy': ref_element = _init_subplot_xy( - layout, x_domain, y_domain, max_subplot_ids + layout, secondary_y, x_domain, y_domain, max_subplot_ids ) elif subplot_type in _single_subplot_types: ref_element = _init_subplot_single( @@ -1217,7 +1250,7 @@ def _pad(s, cell_len=cell_len): return grid_str -def _set_trace_grid_reference(trace, layout, grid_ref, row, col): +def _set_trace_grid_reference(trace, grid_ref, row, col, secondary_y): if row <= 0: raise Exception("Row value is out of range. " "Note: the starting cell is (1, 1)") @@ -1238,7 +1271,18 @@ def _set_trace_grid_reference(trace, layout, grid_ref, row, col): col=col )) - for k in ref['trace_kwargs']: + if secondary_y: + if 'secondary_trace_kwargs' not in ref: + 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 = ref['secondary_trace_kwargs'] + else: + trace_kwargs = ref['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}' @@ -1311,13 +1355,21 @@ def _get_grid_subplot(fig, row, col): elif len(layout_keys) == 2: return SubplotXY( xaxis=fig.layout[layout_keys[0]], - yaxis=fig.layout[layout_keys[1]]) + yaxis=fig.layout[layout_keys[1]], + secondary_yaxis=None + ) + elif len(layout_keys) == 3: + return SubplotXY( + xaxis=fig.layout[layout_keys[0]], + yaxis=fig.layout[layout_keys[1]], + secondary_yaxis=fig.layout[layout_keys[2]], + ) else: raise ValueError(""" Unexpected subplot type with layout_keys of {}""".format(layout_keys)) -def _get_subplot_ref_for_trace(trace): +def _get_subplot_ref_for_trace(trace, secondary_to_primary_y_axes): if 'domain' in trace: return { @@ -1331,11 +1383,29 @@ def _get_subplot_ref_for_trace(trace): xaxis_name = 'xaxis' + trace.xaxis[1:] if trace.xaxis else 'xaxis' yaxis_name = 'yaxis' + trace.yaxis[1:] if trace.yaxis else 'yaxis' - return { - 'subplot_type': 'xy', - 'layout_keys': (xaxis_name, yaxis_name), - 'trace_kwargs': {'xaxis': trace.xaxis, 'yaxis': trace.yaxis} - } + # Check whether this yaxis is secondary yaxis + if yaxis_name in secondary_to_primary_y_axes: + secondary_yaxis_name = yaxis_name + yaxis_name = secondary_to_primary_y_axes[secondary_yaxis_name] + y_label = 'y' + yaxis_name[5:] + secondary_y_label = 'y' + secondary_yaxis_name[5:] + return { + 'subplot_type': 'xy', + 'layout_keys': (xaxis_name, yaxis_name), + 'trace_kwargs': { + 'xaxis': trace.xaxis, 'yaxis': y_label, + }, + 'secondary_trace_kwargs': { + 'xaxis': trace.xaxis, 'yaxis': secondary_y_label + }, + 'secondary_layout_keys': (xaxis_name, secondary_yaxis_name), + } + else: + return { + 'subplot_type': 'xy', + 'layout_keys': (xaxis_name, yaxis_name), + 'trace_kwargs': {'xaxis': trace.xaxis, 'yaxis': trace.yaxis} + } elif 'geo' in trace: return { 'subplot_type': 'geo', From b5a51185a2088c5d28b669b78def2a7b14eb0d6c Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 11 May 2019 12:54:58 -0400 Subject: [PATCH 02/18] Initial implementation of secondary y-axis support in make_subplots --- codegen/datatypes.py | 15 +- codegen/figure.py | 125 ++++++++-- plotly/basedatatypes.py | 181 ++++++++++---- plotly/graph_objs/_figure.py | 364 ++++++++++++++++++++++++++--- plotly/graph_objs/_figurewidget.py | 364 ++++++++++++++++++++++++++--- plotly/subplots.py | 225 +++++++++--------- 6 files changed, 1015 insertions(+), 259 deletions(-) diff --git a/codegen/datatypes.py b/codegen/datatypes.py index 01777086735..5b541625605 100644 --- a/codegen/datatypes.py +++ b/codegen/datatypes.py @@ -491,11 +491,16 @@ def add_docstring(buffer, node, header, prepend_extras=(), append_extras=()): # Write any append extras for p, v in append_extras: - v_wrapped = '\n'.join(textwrap.wrap( - v, - width=79-12, - initial_indent=' ' * 12, - subsequent_indent=' ' * 12)) + if '\n' in v: + # If v contains newlines then assume it's already wrapped as + # desired + v_wrapped = v + else: + v_wrapped = '\n'.join(textwrap.wrap( + v, + width=79-12, + initial_indent=' ' * 12, + subsequent_indent=' ' * 12)) buffer.write(f""" {p} {v_wrapped}""") diff --git a/codegen/figure.py b/codegen/figure.py index d60f2c7b762..e4cf108e860 100644 --- a/codegen/figure.py +++ b/codegen/figure.py @@ -107,28 +107,72 @@ def __init__(self, data=None, layout=None, # ### add_trace methods for each trace type ### for trace_node in trace_nodes: + include_secondary_y = bool([ + d for d in trace_node.child_datatypes + if d.name_property == 'yaxis' + ]) + if include_secondary_y: + secondary_y_1 = ', secondary_y=None' + secondary_y_2 = ', secondary_y=secondary_y' + secondary_y_docstring = f""" + secondary_y: boolean or None (default None) + * If True, only select yaxis objects associated with the secondary + y-axis of the subplot. + * If False, only select yaxis objects associated with the primary + y-axis of the subplot. + * If None (the default), do not filter yaxis objects based on + a secondary y-axis condition. + + To select yaxis objects by secondary y-axis, the Figure must + have been created using plotly.subplots.make_subplots. See + the docstring for the specs argument to make_subplots for more + info on creating subplots with secondary y-axes.""" + else: + secondary_y_1 = '' + secondary_y_2 = '' + secondary_y_docstring = '' + # #### Function signature #### buffer.write(f""" def add_{trace_node.plotly_name}(self""") # #### Function params#### + param_extras = ['row', 'col'] + if include_secondary_y: + param_extras.append('secondary_y') add_constructor_params(buffer, trace_node.child_datatypes, - append_extras=['row', 'col']) + append_extras=param_extras) # #### Docstring #### header = f"Add a new {trace_node.name_datatype_class} trace" - extras = (('row : int or None (default)', - 'Subplot row index (starting from 1) for the trace to be ' - 'added. Only valid if figure was created using ' - '`plotly.tools.make_subplots`'), - ('col : int or None (default)', - 'Subplot col index (starting from 1) for the trace to be ' - 'added. Only valid if figure was created using ' - '`plotly.tools.make_subplots`')) - - add_docstring(buffer, trace_node, header, append_extras=extras) + doc_extras = [( + 'row : int or None (default)', + 'Subplot row index (starting from 1) for the trace to be ' + 'added. Only valid if figure was created using ' + '`plotly.tools.make_subplots`'), + ('col : int or None (default)', + 'Subplot col index (starting from 1) for the trace to be ' + 'added. Only valid if figure was created using ' + '`plotly.tools.make_subplots`')] + + if include_secondary_y: + doc_extras.append( + ('secondary_y: boolean or None (default None)', """\ + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info.\ +""") + ) + + add_docstring(buffer, trace_node, header, append_extras=doc_extras) # #### Function body #### buffer.write(f""" @@ -143,8 +187,14 @@ def add_{trace_node.plotly_name}(self""") buffer.write(f""" **kwargs)""") + if include_secondary_y: + secondary_y_kwarg = ', secondary_y=secondary_y' + else: + secondary_y_kwarg = '' + buffer.write(f""" - return self.add_trace(new_trace, row=row, col=col)""") + return self.add_trace( + new_trace, row=row, col=col{secondary_y_kwarg})""") # update layout subplots # ---------------------- @@ -152,9 +202,32 @@ def add_{trace_node.plotly_name}(self""") for subplot_node in subplot_nodes: singular_name = subplot_node.name_property plural_name = inflect_eng.plural_noun(singular_name) + + if singular_name == 'yaxis': + secondary_y_1 = ', secondary_y=None' + secondary_y_2 = ', secondary_y=secondary_y' + secondary_y_docstring = f""" + secondary_y: boolean or None (default None) + * If True, only select yaxis objects associated with the secondary + y-axis of the subplot. + * If False, only select yaxis objects associated with the primary + y-axis of the subplot. + * If None (the default), do not filter yaxis objects based on + a secondary y-axis condition. + + To select yaxis objects by secondary y-axis, the Figure must + have been created using plotly.subplots.make_subplots. See + the docstring for the specs argument to make_subplots for more + info on creating subplots with secondary y-axes.""" + else: + secondary_y_1 = '' + secondary_y_2 = '' + secondary_y_docstring = '' + buffer.write(f""" - def select_{plural_name}(self, selector=None, row=None, col=None): + def select_{plural_name}( + self, selector=None, row=None, col=None{secondary_y_1}): \"\"\" Select {singular_name} subplot objects from a particular subplot cell and/or {singular_name} subplot objects that satisfy custom selection @@ -172,8 +245,8 @@ def select_{plural_name}(self, selector=None, row=None, col=None): Subplot row and column index of {singular_name} objects to select. To select {singular_name} objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. - If None (the default), all {singular_name} objects are selected. - + If None (the default), all {singular_name} objects are selected.\ +{secondary_y_docstring} Returns ------- generator @@ -184,9 +257,10 @@ def select_{plural_name}(self, selector=None, row=None, col=None): _validate_v4_subplots('select_{plural_name}') return self._select_layout_subplots_by_prefix( - '{singular_name}', selector, row, col) + '{singular_name}', selector, row, col{secondary_y_2}) - def for_each_{singular_name}(self, fn, selector=None, row=None, col=None): + def for_each_{singular_name}( + self, fn, selector=None, row=None, col=None{secondary_y_1}): \"\"\" Apply a function to all {singular_name} objects that satisfy the specified selection criteria @@ -205,21 +279,25 @@ def for_each_{singular_name}(self, fn, selector=None, row=None, col=None): Subplot row and column index of {singular_name} objects to select. To select {singular_name} objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. - If None (the default), all {singular_name} objects are selected. - + If None (the default), all {singular_name} objects are selected.\ +{secondary_y_docstring} Returns ------- self Returns the Figure object that the method was called on \"\"\" for obj in self.select_{plural_name}( - selector=selector, row=row, col=col): + selector=selector, row=row, col=col{secondary_y_2}): fn(obj) return self def update_{plural_name}( - self, patch=None, selector=None, row=None, col=None, **kwargs): + self, + patch=None, + selector=None, + row=None, col=None{secondary_y_1}, + **kwargs): \"\"\" Perform a property update operation on all {singular_name} objects that satisfy the specified selection criteria @@ -239,7 +317,8 @@ def update_{plural_name}( Subplot row and column index of {singular_name} objects to select. To select {singular_name} objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. - If None (the default), all {singular_name} objects are selected. + If None (the default), all {singular_name} objects are selected.\ +{secondary_y_docstring} **kwargs Additional property updates to apply to each selected {singular_name} object. If a property is specified in @@ -251,7 +330,7 @@ def update_{plural_name}( Returns the Figure object that the method was called on \"\"\" for obj in self.select_{plural_name}( - selector=selector, row=row, col=col): + selector=selector, row=row, col=col{secondary_y_2}): obj.update(patch, **kwargs) return self""") diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index c2d1341371c..87fdd580726 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -639,7 +639,8 @@ def data(self, new_data): for trace_ind, trace in enumerate(self._data_objs): trace._trace_ind = trace_ind - def select_traces(self, selector=None, row=None, col=None): + def select_traces( + self, selector=None, row=None, col=None, secondary_y=None): """ Select traces from a particular subplot cell and/or traces that satisfy custom selection criteria. @@ -657,7 +658,18 @@ def select_traces(self, selector=None, row=None, col=None): To select traces by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all traces are selected. - + secondary_y: boolean or None (default None) + * If True, only select traces associated with the secondary + y-axis of the subplot. + * If False, only select traces associated with the primary + y-axis of the subplot. + * If None (the default), do not filter traces based on secondary + y-axis. + + To select traces by secondary y-axis, the Figure must have been + created using plotly.subplots.make_subplots. See the docstring + for the specs argument to make_subplots for more info on + creating subplots with secondary y-axes. Returns ------- generator @@ -667,20 +679,40 @@ def select_traces(self, selector=None, row=None, col=None): if not selector: selector = {} - if row is not None or col is not None: + if row is not None or col is not None or secondary_y is not None: _validate_v4_subplots('select_traces') grid_ref = self._validate_get_grid_ref() filter_by_subplot = True - if row is None: + if row is None and col is not None: # All rows for column - grid_subplot_refs = [ref_row[col-1] for ref_row in grid_ref] - elif col is None: + grid_subplot_ref_tuples = [ + ref_row[col-1] for ref_row in grid_ref + ] + elif col is None and row is not None: # All columns for row - grid_subplot_refs = grid_ref[row-1] - else: + grid_subplot_ref_tuples = grid_ref[row-1] + elif col is not None and row is not None: # Single grid cell - grid_subplot_refs = [grid_ref[row-1][col-1]] + grid_subplot_ref_tuples = [grid_ref[row-1][col-1]] + else: + # row and col are None, secondary_y not None + grid_subplot_ref_tuples = [ + refs + for refs_row in grid_ref + for refs in refs_row + ] + + # Collect list of subplot refs, taking secondary_y into account + grid_subplot_refs = [] + for refs in grid_subplot_ref_tuples: + if not refs: + continue + if secondary_y is not True: + grid_subplot_refs.append(refs[0]) + + if secondary_y is not False and len(refs) > 1: + grid_subplot_refs.append(refs[1]) else: filter_by_subplot = False @@ -728,7 +760,8 @@ def _selector_matches(obj, selector): return True - def for_each_trace(self, fn, selector=None, row=None, col=None): + def for_each_trace( + self, fn, selector=None, row=None, col=None, secondary_y=None): """ Apply a function to all traces that satisfy the specified selection criteria @@ -748,19 +781,38 @@ def for_each_trace(self, fn, selector=None, row=None, col=None): To select traces by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all traces are selected. - + secondary_y: boolean or None (default None) + * If True, only select traces associated with the secondary + y-axis of the subplot. + * If False, only select traces associated with the primary + y-axis of the subplot. + * If None (the default), do not filter traces based on secondary + y-axis. + + To select traces by secondary y-axis, the Figure must have been + created using plotly.subplots.make_subplots. See the docstring + for the specs argument to make_subplots for more info on + creating subplots with secondary y-axes. Returns ------- self Returns the Figure object that the method was called on """ - for trace in self.select_traces(selector=selector, row=row, col=col): + for trace in self.select_traces( + selector=selector, row=row, col=col, secondary_y=secondary_y): fn(trace) return self def update_traces( - self, patch=None, selector=None, row=None, col=None, **kwargs): + self, + patch=None, + selector=None, + row=None, + col=None, + secondary_y=None, + **kwargs + ): """ Perform a property update operation on all traces that satisfy the specified selection criteria @@ -781,6 +833,18 @@ def update_traces( To select traces by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all traces are selected. + secondary_y: boolean or None (default None) + * If True, only select traces associated with the secondary + y-axis of the subplot. + * If False, only select traces associated with the primary + y-axis of the subplot. + * If None (the default), do not filter traces based on secondary + y-axis. + + To select traces by secondary y-axis, the Figure must have been + created using plotly.subplots.make_subplots. See the docstring + for the specs argument to make_subplots for more info on + creating subplots with secondary y-axes. **kwargs Additional property updates to apply to each selected trace. If a property is specified in both patch and in **kwargs then the @@ -791,37 +855,40 @@ def update_traces( self Returns the Figure object that the method was called on """ - for trace in self.select_traces(selector=selector, row=row, col=col): + for trace in self.select_traces( + selector=selector, row=row, col=col, secondary_y=secondary_y): trace.update(patch, **kwargs) return self def _select_layout_subplots_by_prefix( - self, prefix, selector=None, row=None, col=None, secondary_y=None): + self, + prefix, + selector=None, + row=None, + col=None, + secondary_y=None + ): """ Helper called by code generated select_* methods """ - if row is not None or col is not None: + if row is not None or col is not None or secondary_y is not None: # Build mapping from container keys ('xaxis2', 'scene4', etc.) # to (row, col, secondary_y triplets) grid_ref = self._validate_get_grid_ref() container_to_row_col = {} for r, subplot_row in enumerate(grid_ref): - for c, ref in enumerate(subplot_row): - if ref is None: + for c, subplot_refs in enumerate(subplot_row): + if not subplot_refs: continue # collect primary keys - for layout_key in ref['layout_keys']: - if layout_key.startswith(prefix): - container_to_row_col[layout_key] = ( - r + 1, c + 1, False) - - # collection secondary keys - for layout_key in ref.get('secondary_layout_keys', ()): - if layout_key.startswith(prefix): - container_to_row_col[layout_key] = ( - r + 1, c + 1, True) + for i, subplot_ref in enumerate(subplot_refs): + for layout_key in subplot_ref.layout_keys: + if layout_key.startswith(prefix): + is_secondary_y = i == 1 + container_to_row_col[layout_key] = ( + r + 1, c + 1, is_secondary_y) else: container_to_row_col = None @@ -1281,7 +1348,7 @@ def _validate_rows_cols(name, n, vals): else: BaseFigure._raise_invalid_rows_cols(name=name, n=n, invalid=vals) - def add_trace(self, trace, row=None, col=None): + def add_trace(self, trace, row=None, col=None, secondary_y=None): """ Add a trace to the figure @@ -1299,15 +1366,26 @@ def add_trace(self, trace, row=None, col=None): - All remaining properties are passed to the constructor of the specified trace type. - row : int or None (default) + row : int or None (default None) Subplot row index (starting from 1) for the trace to be added. Only valid if figure was created using - `plotly.tools.make_subplots` - col : int or None (default) + `plotly.subplots.make_subplots` + col : int or None (default None) Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using - `plotly.tools.make_subplots` - + `plotly.subplots.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + * The trace argument is a 2D cartesian trace + (scatter, bar, etc.) Returns ------- BaseTraceType @@ -1350,12 +1428,14 @@ def add_trace(self, trace, row=None, col=None): 'Received col parameter but not row.\n' 'row and col must be specified together') - return self.add_traces(data=[trace], - rows=[row] if row is not None else None, - cols=[col] if col is not None else None - )[0] + return self.add_traces( + data=[trace], + rows=[row] if row is not None else None, + cols=[col] if col is not None else None, + secondary_ys=[secondary_y] if secondary_y is not None else None + )[0] - def add_traces(self, data, rows=None, cols=None): + def add_traces(self, data, rows=None, cols=None, secondary_ys=None): """ Add traces to the figure @@ -1383,6 +1463,9 @@ def add_traces(self, data, rows=None, cols=None): List of subplot column indexes (starting from 1) for the traces to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_ys: None or list[boolean] (default None) + List of secondary_y booleans for traces to be added. See the + docstring for `add_trace` for more info. Returns ------- @@ -1432,10 +1515,22 @@ def add_traces(self, data, rows=None, cols=None): 'Received cols parameter but not rows.\n' 'rows and cols must be specified together') + # Process secondary_ys defaults + if secondary_ys is not None and rows is None: + # Default rows/cols to 1s if secondary_ys specified but not rows + # or cols + rows = [1] * len(secondary_ys) + cols = rows + elif secondary_ys is None and rows is not None: + # Default secondary_ys to Nones if secondary_ys is not specified + # but not rows and cols are specified + secondary_ys = [None] * len(rows) + # Apply rows / cols if rows is not None: - for trace, row, col in zip(data, rows, cols): - self._set_trace_grid_position(trace, row, col) + for trace, row, col, secondary_y in \ + zip(data, rows, cols, secondary_ys): + self._set_trace_grid_position(trace, row, col, secondary_y) # Make deep copy of trace data (Optimize later if needed) new_traces_data = [deepcopy(trace._props) for trace in data] @@ -1557,7 +1652,7 @@ def _validate_get_grid_ref(self): "to create the figure with a subplot grid.") return grid_ref - def get_subplot(self, row, col): + def get_subplot(self, row, col, secondary_y=False): """ Return an object representing the subplot at the specified row and column. May only be used on Figures created using @@ -1587,7 +1682,7 @@ def get_subplot(self, row, col): - xaxis: plotly.graph_objs.layout.XAxis instance for subplot - yaxis: plotly.graph_objs.layout.YAxis instance for subplot """ - return _get_grid_subplot(self, row, col) + return _get_grid_subplot(self, row, col, secondary_y) # Child property operations # ------------------------- diff --git a/plotly/graph_objs/_figure.py b/plotly/graph_objs/_figure.py index 8cc8ef26242..bb1a7d6e40e 100644 --- a/plotly/graph_objs/_figure.py +++ b/plotly/graph_objs/_figure.py @@ -721,6 +721,7 @@ def add_bar( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -971,6 +972,17 @@ def add_bar( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -1038,7 +1050,9 @@ def add_bar( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_barpolar( self, @@ -1365,6 +1379,7 @@ def add_box( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -1600,6 +1615,17 @@ def add_box( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -1657,7 +1683,9 @@ def add_box( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_candlestick( self, @@ -1700,6 +1728,7 @@ def add_candlestick( yaxis=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -1853,6 +1882,17 @@ def add_candlestick( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -1898,7 +1938,9 @@ def add_candlestick( yaxis=yaxis, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_carpet( self, @@ -1937,6 +1979,7 @@ def add_carpet( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -2081,6 +2124,17 @@ def add_carpet( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -2122,7 +2176,9 @@ def add_carpet( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_choropleth( self, @@ -2785,6 +2841,7 @@ def add_contour( zsrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -3031,6 +3088,17 @@ def add_contour( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -3093,7 +3161,9 @@ def add_contour( zsrc=zsrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_contourcarpet( self, @@ -3148,6 +3218,7 @@ def add_contourcarpet( zsrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -3360,6 +3431,17 @@ def add_contourcarpet( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -3417,7 +3499,9 @@ def add_contourcarpet( zsrc=zsrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_heatmap( self, @@ -3473,6 +3557,7 @@ def add_heatmap( zsrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -3706,6 +3791,17 @@ def add_heatmap( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -3764,7 +3860,9 @@ def add_heatmap( zsrc=zsrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_heatmapgl( self, @@ -3809,6 +3907,7 @@ def add_heatmapgl( zsrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -3983,6 +4082,17 @@ def add_heatmapgl( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -4030,7 +4140,9 @@ def add_heatmapgl( zsrc=zsrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_histogram( self, @@ -4083,6 +4195,7 @@ def add_histogram( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -4317,6 +4430,17 @@ def add_histogram( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -4372,7 +4496,9 @@ def add_histogram( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_histogram2d( self, @@ -4425,6 +4551,7 @@ def add_histogram2d( zsrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -4669,6 +4796,17 @@ def add_histogram2d( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -4724,7 +4862,9 @@ def add_histogram2d( zsrc=zsrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_histogram2dcontour( self, @@ -4780,6 +4920,7 @@ def add_histogram2dcontour( zsrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -5042,6 +5183,17 @@ def add_histogram2dcontour( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -5100,7 +5252,9 @@ def add_histogram2dcontour( zsrc=zsrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_isosurface( self, @@ -5875,6 +6029,7 @@ def add_ohlc( yaxis=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -6027,6 +6182,17 @@ def add_ohlc( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -6072,7 +6238,9 @@ def add_ohlc( yaxis=yaxis, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_parcats( self, @@ -6717,6 +6885,7 @@ def add_pointcloud( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -6863,6 +7032,17 @@ def add_pointcloud( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -6903,7 +7083,9 @@ def add_pointcloud( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_sankey( self, @@ -7131,6 +7313,7 @@ def add_scatter( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -7428,6 +7611,17 @@ def add_scatter( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -7493,7 +7687,9 @@ def add_scatter( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_scatter3d( self, @@ -7836,6 +8032,7 @@ def add_scattercarpet( yaxis=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -8038,6 +8235,17 @@ def add_scattercarpet( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -8087,7 +8295,9 @@ def add_scattercarpet( yaxis=yaxis, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_scattergeo( self, @@ -8431,6 +8641,7 @@ def add_scattergl( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -8655,6 +8866,17 @@ def add_scattergl( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -8710,7 +8932,9 @@ def add_scattergl( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_scattermapbox( self, @@ -11289,6 +11513,7 @@ def add_violin( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -11548,6 +11773,17 @@ def add_violin( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -11607,7 +11843,9 @@ def add_violin( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_volume( self, @@ -12003,6 +12241,7 @@ def add_waterfall( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -12239,6 +12478,17 @@ def add_waterfall( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -12300,7 +12550,9 @@ def add_waterfall( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def select_geos(self, selector=None, row=None, col=None): """ @@ -12321,7 +12573,6 @@ def select_geos(self, selector=None, row=None, col=None): To select geo objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all geo objects are selected. - Returns ------- generator @@ -12355,7 +12606,6 @@ def for_each_geo(self, fn, selector=None, row=None, col=None): To select geo objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all geo objects are selected. - Returns ------- self @@ -12423,7 +12673,6 @@ def select_mapboxes(self, selector=None, row=None, col=None): To select mapbox objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all mapbox objects are selected. - Returns ------- generator @@ -12457,7 +12706,6 @@ def for_each_mapbox(self, fn, selector=None, row=None, col=None): To select mapbox objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all mapbox objects are selected. - Returns ------- self @@ -12525,7 +12773,6 @@ def select_polars(self, selector=None, row=None, col=None): To select polar objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all polar objects are selected. - Returns ------- generator @@ -12559,7 +12806,6 @@ def for_each_polar(self, fn, selector=None, row=None, col=None): To select polar objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all polar objects are selected. - Returns ------- self @@ -12627,7 +12873,6 @@ def select_scenes(self, selector=None, row=None, col=None): To select scene objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all scene objects are selected. - Returns ------- generator @@ -12661,7 +12906,6 @@ def for_each_scene(self, fn, selector=None, row=None, col=None): To select scene objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all scene objects are selected. - Returns ------- self @@ -12729,7 +12973,6 @@ def select_ternaries(self, selector=None, row=None, col=None): To select ternary objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all ternary objects are selected. - Returns ------- generator @@ -12763,7 +13006,6 @@ def for_each_ternary(self, fn, selector=None, row=None, col=None): To select ternary objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all ternary objects are selected. - Returns ------- self @@ -12831,7 +13073,6 @@ def select_xaxes(self, selector=None, row=None, col=None): To select xaxis objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all xaxis objects are selected. - Returns ------- generator @@ -12865,7 +13106,6 @@ def for_each_xaxis(self, fn, selector=None, row=None, col=None): To select xaxis objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all xaxis objects are selected. - Returns ------- self @@ -12914,7 +13154,9 @@ def update_xaxes( return self - def select_yaxes(self, selector=None, row=None, col=None): + def select_yaxes( + self, selector=None, row=None, col=None, secondary_y=None + ): """ Select yaxis subplot objects from a particular subplot cell and/or yaxis subplot objects that satisfy custom selection @@ -12933,7 +13175,18 @@ def select_yaxes(self, selector=None, row=None, col=None): To select yaxis objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all yaxis objects are selected. - + secondary_y: boolean or None (default None) + * If True, only select yaxis objects associated with the secondary + y-axis of the subplot. + * If False, only select yaxis objects associated with the primary + y-axis of the subplot. + * If None (the default), do not filter yaxis objects based on + a secondary y-axis condition. + + To select yaxis objects by secondary y-axis, the Figure must + have been created using plotly.subplots.make_subplots. See + the docstring for the specs argument to make_subplots for more + info on creating subplots with secondary y-axes. Returns ------- generator @@ -12944,10 +13197,12 @@ def select_yaxes(self, selector=None, row=None, col=None): _validate_v4_subplots('select_yaxes') return self._select_layout_subplots_by_prefix( - 'yaxis', selector, row, col + 'yaxis', selector, row, col, secondary_y=secondary_y ) - def for_each_yaxis(self, fn, selector=None, row=None, col=None): + def for_each_yaxis( + self, fn, selector=None, row=None, col=None, secondary_y=None + ): """ Apply a function to all yaxis objects that satisfy the specified selection criteria @@ -12967,19 +13222,38 @@ def for_each_yaxis(self, fn, selector=None, row=None, col=None): To select yaxis objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all yaxis objects are selected. - + secondary_y: boolean or None (default None) + * If True, only select yaxis objects associated with the secondary + y-axis of the subplot. + * If False, only select yaxis objects associated with the primary + y-axis of the subplot. + * If None (the default), do not filter yaxis objects based on + a secondary y-axis condition. + + To select yaxis objects by secondary y-axis, the Figure must + have been created using plotly.subplots.make_subplots. See + the docstring for the specs argument to make_subplots for more + info on creating subplots with secondary y-axes. Returns ------- self Returns the Figure object that the method was called on """ - for obj in self.select_yaxes(selector=selector, row=row, col=col): + for obj in self.select_yaxes( + selector=selector, row=row, col=col, secondary_y=secondary_y + ): fn(obj) return self def update_yaxes( - self, patch=None, selector=None, row=None, col=None, **kwargs + self, + patch=None, + selector=None, + row=None, + col=None, + secondary_y=None, + **kwargs ): """ Perform a property update operation on all yaxis objects @@ -13001,6 +13275,18 @@ def update_yaxes( To select yaxis objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all yaxis objects are selected. + secondary_y: boolean or None (default None) + * If True, only select yaxis objects associated with the secondary + y-axis of the subplot. + * If False, only select yaxis objects associated with the primary + y-axis of the subplot. + * If None (the default), do not filter yaxis objects based on + a secondary y-axis condition. + + To select yaxis objects by secondary y-axis, the Figure must + have been created using plotly.subplots.make_subplots. See + the docstring for the specs argument to make_subplots for more + info on creating subplots with secondary y-axes. **kwargs Additional property updates to apply to each selected yaxis object. If a property is specified in @@ -13011,7 +13297,9 @@ def update_yaxes( self Returns the Figure object that the method was called on """ - for obj in self.select_yaxes(selector=selector, row=row, col=col): + for obj in self.select_yaxes( + selector=selector, row=row, col=col, secondary_y=secondary_y + ): obj.update(patch, **kwargs) return self diff --git a/plotly/graph_objs/_figurewidget.py b/plotly/graph_objs/_figurewidget.py index 71698ae0740..66e20df3f34 100644 --- a/plotly/graph_objs/_figurewidget.py +++ b/plotly/graph_objs/_figurewidget.py @@ -721,6 +721,7 @@ def add_bar( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -971,6 +972,17 @@ def add_bar( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -1038,7 +1050,9 @@ def add_bar( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_barpolar( self, @@ -1365,6 +1379,7 @@ def add_box( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -1600,6 +1615,17 @@ def add_box( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -1657,7 +1683,9 @@ def add_box( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_candlestick( self, @@ -1700,6 +1728,7 @@ def add_candlestick( yaxis=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -1853,6 +1882,17 @@ def add_candlestick( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -1898,7 +1938,9 @@ def add_candlestick( yaxis=yaxis, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_carpet( self, @@ -1937,6 +1979,7 @@ def add_carpet( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -2081,6 +2124,17 @@ def add_carpet( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -2122,7 +2176,9 @@ def add_carpet( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_choropleth( self, @@ -2785,6 +2841,7 @@ def add_contour( zsrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -3031,6 +3088,17 @@ def add_contour( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -3093,7 +3161,9 @@ def add_contour( zsrc=zsrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_contourcarpet( self, @@ -3148,6 +3218,7 @@ def add_contourcarpet( zsrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -3360,6 +3431,17 @@ def add_contourcarpet( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -3417,7 +3499,9 @@ def add_contourcarpet( zsrc=zsrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_heatmap( self, @@ -3473,6 +3557,7 @@ def add_heatmap( zsrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -3706,6 +3791,17 @@ def add_heatmap( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -3764,7 +3860,9 @@ def add_heatmap( zsrc=zsrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_heatmapgl( self, @@ -3809,6 +3907,7 @@ def add_heatmapgl( zsrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -3983,6 +4082,17 @@ def add_heatmapgl( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -4030,7 +4140,9 @@ def add_heatmapgl( zsrc=zsrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_histogram( self, @@ -4083,6 +4195,7 @@ def add_histogram( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -4317,6 +4430,17 @@ def add_histogram( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -4372,7 +4496,9 @@ def add_histogram( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_histogram2d( self, @@ -4425,6 +4551,7 @@ def add_histogram2d( zsrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -4669,6 +4796,17 @@ def add_histogram2d( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -4724,7 +4862,9 @@ def add_histogram2d( zsrc=zsrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_histogram2dcontour( self, @@ -4780,6 +4920,7 @@ def add_histogram2dcontour( zsrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -5042,6 +5183,17 @@ def add_histogram2dcontour( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -5100,7 +5252,9 @@ def add_histogram2dcontour( zsrc=zsrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_isosurface( self, @@ -5875,6 +6029,7 @@ def add_ohlc( yaxis=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -6027,6 +6182,17 @@ def add_ohlc( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -6072,7 +6238,9 @@ def add_ohlc( yaxis=yaxis, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_parcats( self, @@ -6717,6 +6885,7 @@ def add_pointcloud( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -6863,6 +7032,17 @@ def add_pointcloud( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -6903,7 +7083,9 @@ def add_pointcloud( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_sankey( self, @@ -7131,6 +7313,7 @@ def add_scatter( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -7428,6 +7611,17 @@ def add_scatter( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -7493,7 +7687,9 @@ def add_scatter( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_scatter3d( self, @@ -7836,6 +8032,7 @@ def add_scattercarpet( yaxis=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -8038,6 +8235,17 @@ def add_scattercarpet( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -8087,7 +8295,9 @@ def add_scattercarpet( yaxis=yaxis, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_scattergeo( self, @@ -8431,6 +8641,7 @@ def add_scattergl( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -8655,6 +8866,17 @@ def add_scattergl( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -8710,7 +8932,9 @@ def add_scattergl( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_scattermapbox( self, @@ -11289,6 +11513,7 @@ def add_violin( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -11548,6 +11773,17 @@ def add_violin( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -11607,7 +11843,9 @@ def add_violin( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def add_volume( self, @@ -12003,6 +12241,7 @@ def add_waterfall( ysrc=None, row=None, col=None, + secondary_y=None, **kwargs ): """ @@ -12239,6 +12478,17 @@ def add_waterfall( Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots` + secondary_y: boolean or None (default None) + If True, associate this trace with the secondary y-axis of the + subplot at the specified row and col. Only valid if all of the + following conditions are satisfied: + * The figure was created using `plotly.subplots.make_subplots`. + * The row and col arguments are not None + * The subplot at the specified row and col has type xy + (which is the default) and secondary_y True. These + properties are specified in the specs argument to + make_subplots. See the make_subplots docstring for more info. + Returns ------- @@ -12300,7 +12550,9 @@ def add_waterfall( ysrc=ysrc, **kwargs ) - return self.add_trace(new_trace, row=row, col=col) + return self.add_trace( + new_trace, row=row, col=col, secondary_y=secondary_y + ) def select_geos(self, selector=None, row=None, col=None): """ @@ -12321,7 +12573,6 @@ def select_geos(self, selector=None, row=None, col=None): To select geo objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all geo objects are selected. - Returns ------- generator @@ -12355,7 +12606,6 @@ def for_each_geo(self, fn, selector=None, row=None, col=None): To select geo objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all geo objects are selected. - Returns ------- self @@ -12423,7 +12673,6 @@ def select_mapboxes(self, selector=None, row=None, col=None): To select mapbox objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all mapbox objects are selected. - Returns ------- generator @@ -12457,7 +12706,6 @@ def for_each_mapbox(self, fn, selector=None, row=None, col=None): To select mapbox objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all mapbox objects are selected. - Returns ------- self @@ -12525,7 +12773,6 @@ def select_polars(self, selector=None, row=None, col=None): To select polar objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all polar objects are selected. - Returns ------- generator @@ -12559,7 +12806,6 @@ def for_each_polar(self, fn, selector=None, row=None, col=None): To select polar objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all polar objects are selected. - Returns ------- self @@ -12627,7 +12873,6 @@ def select_scenes(self, selector=None, row=None, col=None): To select scene objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all scene objects are selected. - Returns ------- generator @@ -12661,7 +12906,6 @@ def for_each_scene(self, fn, selector=None, row=None, col=None): To select scene objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all scene objects are selected. - Returns ------- self @@ -12729,7 +12973,6 @@ def select_ternaries(self, selector=None, row=None, col=None): To select ternary objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all ternary objects are selected. - Returns ------- generator @@ -12763,7 +13006,6 @@ def for_each_ternary(self, fn, selector=None, row=None, col=None): To select ternary objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all ternary objects are selected. - Returns ------- self @@ -12831,7 +13073,6 @@ def select_xaxes(self, selector=None, row=None, col=None): To select xaxis objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all xaxis objects are selected. - Returns ------- generator @@ -12865,7 +13106,6 @@ def for_each_xaxis(self, fn, selector=None, row=None, col=None): To select xaxis objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all xaxis objects are selected. - Returns ------- self @@ -12914,7 +13154,9 @@ def update_xaxes( return self - def select_yaxes(self, selector=None, row=None, col=None): + def select_yaxes( + self, selector=None, row=None, col=None, secondary_y=None + ): """ Select yaxis subplot objects from a particular subplot cell and/or yaxis subplot objects that satisfy custom selection @@ -12933,7 +13175,18 @@ def select_yaxes(self, selector=None, row=None, col=None): To select yaxis objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all yaxis objects are selected. - + secondary_y: boolean or None (default None) + * If True, only select yaxis objects associated with the secondary + y-axis of the subplot. + * If False, only select yaxis objects associated with the primary + y-axis of the subplot. + * If None (the default), do not filter yaxis objects based on + a secondary y-axis condition. + + To select yaxis objects by secondary y-axis, the Figure must + have been created using plotly.subplots.make_subplots. See + the docstring for the specs argument to make_subplots for more + info on creating subplots with secondary y-axes. Returns ------- generator @@ -12944,10 +13197,12 @@ def select_yaxes(self, selector=None, row=None, col=None): _validate_v4_subplots('select_yaxes') return self._select_layout_subplots_by_prefix( - 'yaxis', selector, row, col + 'yaxis', selector, row, col, secondary_y=secondary_y ) - def for_each_yaxis(self, fn, selector=None, row=None, col=None): + def for_each_yaxis( + self, fn, selector=None, row=None, col=None, secondary_y=None + ): """ Apply a function to all yaxis objects that satisfy the specified selection criteria @@ -12967,19 +13222,38 @@ def for_each_yaxis(self, fn, selector=None, row=None, col=None): To select yaxis objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all yaxis objects are selected. - + secondary_y: boolean or None (default None) + * If True, only select yaxis objects associated with the secondary + y-axis of the subplot. + * If False, only select yaxis objects associated with the primary + y-axis of the subplot. + * If None (the default), do not filter yaxis objects based on + a secondary y-axis condition. + + To select yaxis objects by secondary y-axis, the Figure must + have been created using plotly.subplots.make_subplots. See + the docstring for the specs argument to make_subplots for more + info on creating subplots with secondary y-axes. Returns ------- self Returns the Figure object that the method was called on """ - for obj in self.select_yaxes(selector=selector, row=row, col=col): + for obj in self.select_yaxes( + selector=selector, row=row, col=col, secondary_y=secondary_y + ): fn(obj) return self def update_yaxes( - self, patch=None, selector=None, row=None, col=None, **kwargs + self, + patch=None, + selector=None, + row=None, + col=None, + secondary_y=None, + **kwargs ): """ Perform a property update operation on all yaxis objects @@ -13001,6 +13275,18 @@ def update_yaxes( To select yaxis objects by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all yaxis objects are selected. + secondary_y: boolean or None (default None) + * If True, only select yaxis objects associated with the secondary + y-axis of the subplot. + * If False, only select yaxis objects associated with the primary + y-axis of the subplot. + * If None (the default), do not filter yaxis objects based on + a secondary y-axis condition. + + To select yaxis objects by secondary y-axis, the Figure must + have been created using plotly.subplots.make_subplots. See + the docstring for the specs argument to make_subplots for more + info on creating subplots with secondary y-axes. **kwargs Additional property updates to apply to each selected yaxis object. If a property is specified in @@ -13011,7 +13297,9 @@ def update_yaxes( self Returns the Figure object that the method was called on """ - for obj in self.select_yaxes(selector=selector, row=row, col=col): + for obj in self.select_yaxes( + selector=selector, row=row, col=col, secondary_y=secondary_y + ): obj.update(patch, **kwargs) return self diff --git a/plotly/subplots.py b/plotly/subplots.py index 26c0a75db50..21c8feed065 100644 --- a/plotly/subplots.py +++ b/plotly/subplots.py @@ -28,9 +28,12 @@ # Named tuple to hold an xaxis/yaxis pair that represent a single subplot SubplotXY = collections.namedtuple('SubplotXY', - ('xaxis', 'yaxis', 'secondary_yaxis')) + ('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 @@ -628,10 +631,10 @@ def _checks(item, defaults): # ### construct subplot container ### subplot_type = spec['type'] secondary_y = spec['secondary_y'] - grid_ref_element = _init_subplot( + subplot_refs = _init_subplot( layout, subplot_type, secondary_y, x_domain, y_domain, max_subplot_ids) - grid_ref[r][c] = grid_ref_element + 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) @@ -675,10 +678,11 @@ def _checks(item, defaults): subplot_type = inset['type'] - inset_ref_element = _init_subplot( - layout, subplot_type, x_domain, y_domain, max_subplot_ids) + subplot_refs = _init_subplot( + layout, subplot_type, False, + x_domain, y_domain, max_subplot_ids) - insets_ref[i_inset] = inset_ref_element + insets_ref[i_inset] = subplot_refs # Build grid_str # This is the message printed when print_grid=True @@ -800,8 +804,8 @@ def _configure_shared_axes(layout, grid_ref, specs, x_or_y, shared, row_dir): else: rows_iter = range(rows) - def update_axis_matches(first_axis_id, ref, spec, remove_label): - if ref is None: + 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': @@ -809,12 +813,12 @@ def update_axis_matches(first_axis_id, ref, spec, remove_label): else: span = spec['rowspan'] - if ref['subplot_type'] == 'xy' and span == 1: + if subplot_ref.subplot_type == 'xy' and span == 1: if first_axis_id is None: - first_axis_name = ref['layout_keys'][layout_key_ind] + first_axis_name = subplot_ref.layout_keys[layout_key_ind] first_axis_id = first_axis_name.replace('axis', '') else: - axis_name = ref['layout_keys'][layout_key_ind] + 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: @@ -827,26 +831,26 @@ def update_axis_matches(first_axis_id, ref, spec, remove_label): first_axis_id = None ok_to_remove_label = x_or_y == 'x' for r in rows_iter: - ref = grid_ref[r][c] + subplot_ref = grid_ref[r][c][0] spec = specs[r][c] first_axis_id = update_axis_matches( - first_axis_id, ref, spec, ok_to_remove_label) + 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): - ref = grid_ref[r][c] + subplot_ref = grid_ref[r][c][0] spec = specs[r][c] first_axis_id = update_axis_matches( - first_axis_id, ref, spec, ok_to_remove_label) + 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): - ref = grid_ref[r][c] + subplot_ref = grid_ref[r][c][0] spec = specs[r][c] if x_or_y == 'y': @@ -855,7 +859,7 @@ def update_axis_matches(first_axis_id, ref, spec, remove_label): ok_to_remove_label = ri > 0 if row_dir > 0 else r < rows - 1 first_axis_id = update_axis_matches( - first_axis_id, ref, spec, ok_to_remove_label) + first_axis_id, subplot_ref, spec, ok_to_remove_label) def _init_subplot_xy( @@ -884,32 +888,36 @@ def _init_subplot_xy( layout[xaxis_name] = x_axis layout[yaxis_name] = y_axis - ref_element = { - 'subplot_type': 'xy', - 'layout_keys': (xaxis_name, yaxis_name), - 'trace_kwargs': {'xaxis': x_label, 'yaxis': y_label} - } + 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_y_label = "y{cnt}".format(cnt=y_cnt) - ref_element['secondary_trace_kwargs'] = { - 'xaxis': x_label, 'yaxis': secondary_y_label - } - secondary_yaxis_name = 'yaxis{cnt}'.format( cnt=y_cnt if y_cnt > 1 else '') - ref_element['secondary_layout_keys'] = ( - xaxis_name, secondary_yaxis_name) + 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} + )) - secondary_y_axis = {'overlaying': y_label, 'side': 'right'} + # 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 ref_element + return tuple(subplot_refs) def _init_subplot_single( @@ -930,26 +938,28 @@ def _init_subplot_single( if subplot_type in _subplot_prop_named_subplot else subplot_type) - ref_element = { - 'subplot_type': subplot_type, - 'layout_keys': (label,), - 'trace_kwargs': {trace_key: label}} + 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 ref_element + return (subplot_ref,) def _init_subplot_domain(x_domain, y_domain): # No change to layout since domain traces are labeled individually - ref_element = { - 'subplot_type': 'domain', - 'layout_keys': (), - 'trace_kwargs': { - 'domain': {'x': tuple(x_domain), 'y': tuple(y_domain)}}} + subplot_ref = SubplotRef( + subplot_type='domain', + layout_keys=(), + trace_kwargs={ + 'domain': {'x': tuple(x_domain), 'y': tuple(y_domain)}} + ) - return ref_element + return (subplot_ref,) def _subplot_type_for_trace_type(trace_type): @@ -1014,20 +1024,20 @@ def _init_subplot( y_domain = [max(0.0, y_domain[0]), min(1.0, y_domain[1])] if subplot_type == 'xy': - ref_element = _init_subplot_xy( + subplot_refs = _init_subplot_xy( layout, secondary_y, x_domain, y_domain, max_subplot_ids ) elif subplot_type in _single_subplot_types: - ref_element = _init_subplot_single( + subplot_refs = _init_subplot_single( layout, subplot_type, x_domain, y_domain, max_subplot_ids ) elif subplot_type == 'domain': - ref_element = _init_subplot_domain(x_domain, y_domain) + subplot_refs = _init_subplot_domain(x_domain, y_domain) else: raise ValueError('Unsupported subplot type: {}' .format(repr(subplot_type))) - return ref_element + return subplot_refs def _get_cartesian_label(x_or_y, r, c, cnt): @@ -1162,8 +1172,14 @@ def _build_grid_str(specs, grid_ref, insets, insets_ref, row_seq): _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, ref): - ref_str = ','.join(ref['layout_keys']) + 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) return '({r},{c}) {ref}'.format( r=r + 1, c=c + 1, @@ -1243,8 +1259,8 @@ def _pad(s, cell_len=cell_len): ref = grid_ref[r][c] grid_str += ( - s_str + ','.join(insets_ref[i_inset]['layout_keys']) + e_str + - ' over ' + + s_str + ','.join(insets_ref[i_inset][0].layout_keys) + + e_str + ' over ' + s_str + _get_cell_str(r, c, ref) + e_str + '\n' ) return grid_str @@ -1258,13 +1274,13 @@ def _set_trace_grid_reference(trace, grid_ref, row, col, secondary_y): raise Exception("Col value is out of range. " "Note: the starting cell is (1, 1)") try: - ref = grid_ref[row - 1][col - 1] + 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 ref is None: + if not subplot_refs: raise ValueError(""" No subplot specified at grid position ({row}, {col})""".format( row=row, @@ -1272,15 +1288,15 @@ def _set_trace_grid_reference(trace, grid_ref, row, col, secondary_y): )) if secondary_y: - if 'secondary_trace_kwargs' not in ref: + 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 = ref['secondary_trace_kwargs'] + trace_kwargs = subplot_refs[1].trace_kwargs else: - trace_kwargs = ref['trace_kwargs'] + trace_kwargs = subplot_refs[0].trace_kwargs for k in trace_kwargs: if k not in trace: @@ -1291,16 +1307,16 @@ def _set_trace_grid_reference(trace, grid_ref, row, col, secondary_y): See the docstring for the specs argument to plotly.subplots.make_subplot for more information on subplot types""".format( typ=trace.type, - subplot_type=ref['subplot_type'], + subplot_type=subplot_refs[0].subplot_type, row=row, col=col )) # Update trace reference - trace.update(ref['trace_kwargs']) + trace.update(trace_kwargs) -def _get_grid_subplot(fig, row, col): +def _get_grid_subplot(fig, row, col, secondary_y=False): # Make sure we're in future subplots mode from _plotly_future_ import _future_flags if 'v4_subplots' not in _future_flags: @@ -1343,88 +1359,73 @@ def _get_grid_subplot(fig, row, col): val=repr(col) )) - ref = fig._grid_ref[row - 1][col - 1] - if ref is None: + subplot_refs = fig._grid_ref[row - 1][col - 1] + if not subplot_refs: return None - layout_keys = ref['layout_keys'] + 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(**ref['trace_kwargs']['domain']) + 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]], - secondary_yaxis=None - ) - elif len(layout_keys) == 3: - return SubplotXY( - xaxis=fig.layout[layout_keys[0]], - yaxis=fig.layout[layout_keys[1]], - secondary_yaxis=fig.layout[layout_keys[2]], - ) + 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, secondary_to_primary_y_axes): +def _get_subplot_ref_for_trace(trace): if 'domain' in trace: - return { - 'subplot_type': 'domain', - 'layout_keys': (), - 'trace_kwargs': { + return SubplotRef( + subplot_type='domain', + layout_keys=(), + trace_kwargs={ 'domain': {'x': trace.domain.x, - 'y': trace.domain.y}}} + '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' - # Check whether this yaxis is secondary yaxis - if yaxis_name in secondary_to_primary_y_axes: - secondary_yaxis_name = yaxis_name - yaxis_name = secondary_to_primary_y_axes[secondary_yaxis_name] - y_label = 'y' + yaxis_name[5:] - secondary_y_label = 'y' + secondary_yaxis_name[5:] - return { - 'subplot_type': 'xy', - 'layout_keys': (xaxis_name, yaxis_name), - 'trace_kwargs': { - 'xaxis': trace.xaxis, 'yaxis': y_label, - }, - 'secondary_trace_kwargs': { - 'xaxis': trace.xaxis, 'yaxis': secondary_y_label - }, - 'secondary_layout_keys': (xaxis_name, secondary_yaxis_name), - } - else: - return { - 'subplot_type': 'xy', - 'layout_keys': (xaxis_name, yaxis_name), - 'trace_kwargs': {'xaxis': trace.xaxis, 'yaxis': trace.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 { - 'subplot_type': 'geo', - 'layout_keys': (trace.geo,), - 'trace_kwargs': {'geo': trace.geo}} + return SubplotRef( + subplot_type='geo', + layout_keys=(trace.geo,), + trace_kwargs={'geo': trace.geo} + ) elif 'scene' in trace: - return { - 'subplot_type': 'scene', - 'layout_keys': (trace.scene,), - 'trace_kwargs': {'scene': trace.scene}} + 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 { - 'subplot_type': t, - 'layout_keys': (trace.subplot,), - 'trace_kwargs': {'subplot': trace.subplot}} + return SubplotRef( + subplot_type=t, + layout_keys=(trace.subplot,), + trace_kwargs={'subplot': trace.subplot} + ) except ValueError: pass From f66f73a70d12d7a4a7d2e8e97c9eb18f5bbb2a39 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 12 May 2019 06:09:46 -0400 Subject: [PATCH 03/18] Added initial secondary_y make_subplots tests --- .../test_subplots/test_make_subplots.py | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/plotly/tests/test_core/test_subplots/test_make_subplots.py b/plotly/tests/test_core/test_subplots/test_make_subplots.py index a6f70239bce..53d572e08a1 100644 --- a/plotly/tests/test_core/test_subplots/test_make_subplots.py +++ b/plotly/tests/test_core/test_subplots/test_make_subplots.py @@ -5,6 +5,7 @@ from plotly.graph_objs import (Annotation, Annotations, Data, Figure, Font, Layout, layout, Scene, XAxis, YAxis) import plotly.tools as tls +from plotly import subplots class TestMakeSubplots(TestCase): @@ -2170,4 +2171,47 @@ def test_row_width_and_shared_yaxes(self): self.assertEqual(fig.to_plotly_json(), expected.to_plotly_json()) - # def test_row_width_and_shared_yaxes(self): + def test_secondary_y(self): + fig = subplots.make_subplots( + rows=1, cols=1, specs=[[{'secondary_y': True}]]) + + expected = Figure({ + 'data': [], + 'layout': {'xaxis': {'anchor': 'y', 'domain': [0.0, 1.0]}, + 'yaxis': {'anchor': 'x', 'domain': [0.0, 1.0]}, + 'yaxis2': {'anchor': 'x', + 'overlaying': 'y', + 'side': 'right'}} + }) + + self.assertEqual(fig.to_plotly_json(), expected.to_plotly_json()) + + def test_secondary_y_traces(self): + fig = subplots.make_subplots( + rows=1, cols=1, specs=[[{'secondary_y': True}]]) + + fig.add_scatter(y=[1, 3, 2], name='First', row=1, col=1) + fig.add_scatter( + y=[2, 1, 3], name='second', row=1, col=1, secondary_y=True) + fig.update_traces(uid=None) + + expected = Figure({ + 'data': [{'name': 'First', + 'type': 'scatter', + 'y': [1, 3, 2], + 'xaxis': 'x', + 'yaxis': 'y'}, + {'name': 'second', + 'type': 'scatter', + 'y': [2, 1, 3], + 'xaxis': 'x', + 'yaxis': 'y2'}], + 'layout': {'xaxis': {'anchor': 'y', 'domain': [0.0, 1.0]}, + 'yaxis': {'anchor': 'x', 'domain': [0.0, 1.0]}, + 'yaxis2': {'anchor': 'x', + 'overlaying': 'y', + 'side': 'right'}} + }) + expected.update_traces(uid=None) + + self.assertEqual(fig.to_plotly_json(), expected.to_plotly_json()) From 0546456e99dd7d63d6fa1243a269dce0ac546ed2 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 13 May 2019 09:34:59 -0400 Subject: [PATCH 04/18] get_subplot tests --- .../test_subplots/test_get_subplot.py | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 plotly/tests/test_core/test_subplots/test_get_subplot.py diff --git a/plotly/tests/test_core/test_subplots/test_get_subplot.py b/plotly/tests/test_core/test_subplots/test_get_subplot.py new file mode 100644 index 00000000000..9b576df8e0d --- /dev/null +++ b/plotly/tests/test_core/test_subplots/test_get_subplot.py @@ -0,0 +1,145 @@ +from __future__ import absolute_import +from _plotly_future_ import _future_flags + +from unittest import TestCase +from plotly.graph_objs import Figure +from plotly import subplots +import plotly.graph_objs as go +from plotly.subplots import SubplotXY, SubplotDomain + + +class TestGetSubplot(TestCase): + def setUp(self): + # Use v4_subplots mode + _future_flags.add('v4_subplots') + + def tearDown(self): + _future_flags.remove('v4_subplots') + + def test_get_subplot(self): + # Make Figure with subplot types + fig = subplots.make_subplots( + rows=4, + cols=2, + specs=[[{}, {'secondary_y': True}], + [{'type': 'polar'}, {'type': 'ternary'}], + [{'type': 'scene'}, {'type': 'geo'}], + [{'type': 'domain', 'colspan': 2}, None]] + ) + + fig.add_scatter(y=[2, 1, 3], row=1, col=1) + fig.add_scatter(y=[2, 1, 3], row=1, col=2) + fig.add_scatter(y=[1, 3, 2], row=1, col=2, secondary_y=True) + fig.add_trace(go.Scatterpolar(r=[2, 1, 3], theta=[20, 50, 125]), row=2, + col=1) + fig.add_traces([go.Scatterternary(a=[.2, .1, .3], b=[.4, .6, .5])], + rows=[2], cols=[2]) + fig.add_scatter3d(x=[2, 0, 1], y=[0, 1, 0], z=[0, 1, 2], mode='lines', + row=3, col=1) + fig.add_scattergeo(lat=[0, 40], lon=[10, 5], mode='lines', row=3, + col=2) + fig.add_parcats( + dimensions=[ + {'values': ['A', 'A', 'B', 'A', 'B']}, + {'values': ['a', 'a', 'a', 'b', 'b']}, + ], row=4, col=1) + + fig.update_traces(uid=None) + fig.update(layout_height=1200) + + # Check + expected = Figure({ + 'data': [ + {'type': 'scatter', 'xaxis': 'x', + 'y': [2, 1, 3], 'yaxis': 'y'}, + {'type': 'scatter', 'xaxis': 'x2', + 'y': [2, 1, 3], 'yaxis': 'y2'}, + {'type': 'scatter', 'xaxis': 'x2', + 'y': [1, 3, 2], 'yaxis': 'y3'}, + {'r': [2, 1, 3], 'subplot': 'polar', + 'theta': [20, 50, 125], 'type': 'scatterpolar'}, + {'a': [0.2, 0.1, 0.3], 'b': [0.4, 0.6, 0.5], + 'subplot': 'ternary', 'type': 'scatterternary'}, + {'mode': 'lines', 'scene': 'scene', 'type': 'scatter3d', + 'x': [2, 0, 1], 'y': [0, 1, 0], 'z': [0, 1, 2]}, + {'geo': 'geo', 'lat': [0, 40], 'lon': [10, 5], + 'mode': 'lines', 'type': 'scattergeo'}, + {'dimensions': [{'values': ['A', 'A', 'B', 'A', 'B']}, + {'values': ['a', 'a', 'a', 'b', 'b']}], + 'domain': {'x': [0.0, 1.0], 'y': [0.0, 0.19375]}, + 'type': 'parcats'}], + 'layout': { + 'geo': {'domain': {'x': [0.55, 1.0], + 'y': [0.26875, 0.4625]}}, + 'height': 1200, + 'polar': {'domain': {'x': [0.0, 0.45], + 'y': [0.5375, 0.73125]}}, + 'scene': {'domain': {'x': [0.0, 0.45], + 'y': [0.26875, 0.4625]}}, + 'ternary': {'domain': {'x': [0.55, 1.0], + 'y': [0.5375, 0.73125]}}, + 'xaxis': {'anchor': 'y', 'domain': [0.0, 0.45]}, + 'xaxis2': {'anchor': 'y2', 'domain': [0.55, 1.0]}, + 'yaxis': {'anchor': 'x', 'domain': [0.80625, 1.0]}, + 'yaxis2': {'anchor': 'x2', 'domain': [0.80625, 1.0]}, + 'yaxis3': {'anchor': 'x2', + 'overlaying': 'y2', + 'side': 'right'}} + }) + + expected.update_traces(uid=None) + + # Make sure we have expected starting figure + self.assertEqual(fig, expected) + + # (1, 1) + subplot = fig.get_subplot(1, 1) + self.assertEqual( + subplot, SubplotXY( + xaxis=fig.layout.xaxis, yaxis=fig.layout.yaxis)) + + # (1, 2) Primary + subplot = fig.get_subplot(1, 2) + self.assertEqual( + subplot, SubplotXY( + xaxis=fig.layout.xaxis2, yaxis=fig.layout.yaxis2)) + + # (1, 2) Primary + subplot = fig.get_subplot(1, 2, secondary_y=True) + self.assertEqual( + subplot, SubplotXY( + xaxis=fig.layout.xaxis2, yaxis=fig.layout.yaxis3)) + + # (2, 1) + subplot = fig.get_subplot(2, 1) + self.assertEqual( + subplot, fig.layout.polar) + + # (2, 2) + subplot = fig.get_subplot(2, 2) + self.assertEqual( + subplot, fig.layout.ternary) + + # (3, 1) + subplot = fig.get_subplot(3, 1) + self.assertEqual( + subplot, fig.layout.scene) + + # (3, 2) + subplot = fig.get_subplot(3, 2) + self.assertEqual( + subplot, fig.layout.geo) + + # (4, 1) + subplot = fig.get_subplot(4, 1) + domain = fig.data[-1].domain + self.assertEqual( + subplot, SubplotDomain(x=domain.x, y=domain.y)) + + def test_get_subplot_out_of_bounds(self): + fig = subplots.make_subplots(rows=4, cols=2) + + self.assertRaises(ValueError, lambda: fig.get_subplot(0, 1)) + self.assertRaises(ValueError, lambda: fig.get_subplot(5, 1)) + self.assertRaises(ValueError, lambda: fig.get_subplot(1, 0)) + self.assertRaises(ValueError, lambda: fig.get_subplot(1, 3)) From f898d34254a2350c5c03e47bfbd4d288fb5c17e1 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 13 May 2019 09:36:51 -0400 Subject: [PATCH 05/18] added test secondary y subplot test --- .../test_subplots/test_make_subplots.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/plotly/tests/test_core/test_subplots/test_make_subplots.py b/plotly/tests/test_core/test_subplots/test_make_subplots.py index 53d572e08a1..1dec1671fff 100644 --- a/plotly/tests/test_core/test_subplots/test_make_subplots.py +++ b/plotly/tests/test_core/test_subplots/test_make_subplots.py @@ -2215,3 +2215,73 @@ def test_secondary_y_traces(self): expected.update_traces(uid=None) self.assertEqual(fig.to_plotly_json(), expected.to_plotly_json()) + + def test_secondary_y_subplots(self): + fig = subplots.make_subplots( + rows=2, + cols=2, + specs=[[{'secondary_y': True}, {'secondary_y': True}], + [{'secondary_y': True}, {'secondary_y': True}]] + ) + + fig.add_scatter(y=[1, 3, 2], name='First', row=1, col=1) + fig.add_scatter( + y=[2, 1, 3], name='Second', row=1, col=1, secondary_y=True) + + fig.add_scatter(y=[4, 3, 2], name='Third', row=1, col=2) + fig.add_scatter( + y=[8, 1, 3], name='Forth', row=1, col=2, secondary_y=True) + + fig.add_scatter(y=[0, 2, 4], name='Fifth', row=2, col=1) + fig.add_scatter( + y=[2, 1, 3], name='Sixth', row=2, col=1, secondary_y=True) + + fig.add_scatter(y=[2, 4, 0], name='Fifth', row=2, col=2) + fig.add_scatter( + y=[2, 3, 6], name='Sixth', row=2, col=2, secondary_y=True) + + fig.update_traces(uid=None) + + expected = Figure({ + 'data': [ + {'name': 'First', 'type': 'scatter', + 'xaxis': 'x', 'y': [1, 3, 2], 'yaxis': 'y'}, + {'name': 'Second', 'type': 'scatter', + 'xaxis': 'x', 'y': [2, 1, 3], 'yaxis': 'y2'}, + {'name': 'Third', 'type': 'scatter', + 'xaxis': 'x2', 'y': [4, 3, 2], 'yaxis': 'y3'}, + {'name': 'Forth', 'type': 'scatter', + 'xaxis': 'x2', 'y': [8, 1, 3], 'yaxis': 'y4'}, + {'name': 'Fifth', 'type': 'scatter', + 'xaxis': 'x3', 'y': [0, 2, 4], 'yaxis': 'y5'}, + {'name': 'Sixth', 'type': 'scatter', + 'xaxis': 'x3', 'y': [2, 1, 3], 'yaxis': 'y6'}, + {'name': 'Fifth', 'type': 'scatter', + 'xaxis': 'x4', 'y': [2, 4, 0], 'yaxis': 'y7'}, + {'name': 'Sixth', 'type': 'scatter', + 'xaxis': 'x4', 'y': [2, 3, 6], 'yaxis': 'y8'}], + 'layout': { + 'xaxis': {'anchor': 'y', 'domain': [0.0, 0.45]}, + 'xaxis2': {'anchor': 'y3', 'domain': [0.55, 1.0]}, + 'xaxis3': {'anchor': 'y5', 'domain': [0.0, 0.45]}, + 'xaxis4': {'anchor': 'y7', 'domain': [0.55, 1.0]}, + 'yaxis': {'anchor': 'x', 'domain': [0.575, 1.0]}, + 'yaxis2': {'anchor': 'x', + 'overlaying': 'y', 'side': 'right'}, + 'yaxis3': {'anchor': 'x2', + 'domain': [0.575, 1.0]}, + 'yaxis4': {'anchor': 'x2', + 'overlaying': 'y3', 'side': 'right'}, + 'yaxis5': {'anchor': 'x3', + 'domain': [0.0, 0.425]}, + 'yaxis6': {'anchor': 'x3', + 'overlaying': 'y5', 'side': 'right'}, + 'yaxis7': {'anchor': 'x4', + 'domain': [0.0, 0.425]}, + 'yaxis8': {'anchor': 'x4', + 'overlaying': 'y7', 'side': 'right'}} + }) + + expected.update_traces(uid=None) + + self.assertEqual(fig.to_plotly_json(), expected.to_plotly_json()) From 0eb76279200c2dc0a06d0f6edaa6a5e99501ca7b Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 13 May 2019 09:37:25 -0400 Subject: [PATCH 06/18] Updated secondary_y docstring --- plotly/basedatatypes.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 87fdd580726..0bd71df329f 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -1664,7 +1664,13 @@ def get_subplot(self, row, col, secondary_y=False): 1-based index of subplot row col: int 1-based index of subplot column - + secondary_y: bool + If True, select the subplot that consists of the x-axis and the + secondary y-axis at the specified row/col. Only valid if the + subplot at row/col is an 2D cartesian subplot that was created + with a secondary y-axis. See the docstring for the specs argument + to make_subplots for more info on creating a subplot with a + secondary y-axis. Returns ------- subplot From 5e896a93bc2a9461ea57838f82f2f3b4db12f560 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 13 May 2019 10:13:40 -0400 Subject: [PATCH 07/18] secondary_y update_* tests --- .../test_subplots/test_get_subplot.py | 4 +- .../test_update_subplots.py | 58 ++++++++++++++++--- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/plotly/tests/test_core/test_subplots/test_get_subplot.py b/plotly/tests/test_core/test_subplots/test_get_subplot.py index 9b576df8e0d..3596f4f4afb 100644 --- a/plotly/tests/test_core/test_subplots/test_get_subplot.py +++ b/plotly/tests/test_core/test_subplots/test_get_subplot.py @@ -30,8 +30,8 @@ def test_get_subplot(self): fig.add_scatter(y=[2, 1, 3], row=1, col=1) fig.add_scatter(y=[2, 1, 3], row=1, col=2) fig.add_scatter(y=[1, 3, 2], row=1, col=2, secondary_y=True) - fig.add_trace(go.Scatterpolar(r=[2, 1, 3], theta=[20, 50, 125]), row=2, - col=1) + fig.add_trace(go.Scatterpolar( + r=[2, 1, 3], theta=[20, 50, 125]), row=2, col=1) fig.add_traces([go.Scatterternary(a=[.2, .1, .3], b=[.4, .6, .5])], rows=[2], cols=[2]) fig.add_scatter3d(x=[2, 0, 1], y=[0, 1, 0], z=[0, 1, 2], mode='lines', diff --git a/plotly/tests/test_core/test_update_objects/test_update_subplots.py b/plotly/tests/test_core/test_update_objects/test_update_subplots.py index 7ad874021e4..bea73c2d3a6 100644 --- a/plotly/tests/test_core/test_update_objects/test_update_subplots.py +++ b/plotly/tests/test_core/test_update_objects/test_update_subplots.py @@ -18,7 +18,9 @@ def setUp(self): rows=3, cols=3, specs=[[{}, {'type': 'scene'}, {}], - [{}, {'type': 'polar'}, {'type': 'polar'}], + [{'secondary_y': True}, + {'type': 'polar'}, + {'type': 'polar'}], [{'type': 'xy', 'colspan': 2}, None, {'type': 'ternary'}]] ).update(layout={'height': 800}) @@ -50,15 +52,22 @@ def tearDown(self): def assert_select_subplots( self, subplot_type, subplots_name, expected_nums, - selector=None, row=None, col=None, test_no_grid=False): + selector=None, row=None, col=None, secondary_y=None, + test_no_grid=False): select_fn = getattr(Figure, 'select_' + subplots_name) for_each_fn = getattr(Figure, 'for_each_' + subplot_type) + if secondary_y is not None: + sec_y_args = dict(secondary_y=secondary_y) + else: + sec_y_args = {} + def check_select(fig): # Check select_* subplots = list( - select_fn(fig, selector=selector, row=row, col=col)) + select_fn(fig, selector=selector, + row=row, col=col, **sec_y_args)) expected_keys = [ subplot_type + (str(cnt) if cnt > 1 else '') for cnt in expected_nums @@ -73,8 +82,7 @@ def check_select(fig): subplots = [] res = for_each_fn( fig, lambda obj: subplots.append(obj), - selector=selector, row=row, col=col - ) + selector=selector, row=row, col=col, **sec_y_args) self.assertIs(res, fig) @@ -92,7 +100,7 @@ def test_select_by_type(self): 'xaxis', 'xaxes', [1, 2, 3, 4], test_no_grid=True) self.assert_select_subplots( - 'yaxis', 'yaxes', [1, 2, 3, 4], test_no_grid=True) + 'yaxis', 'yaxes', [1, 2, 3, 4, 5], test_no_grid=True) self.assert_select_subplots( 'scene', 'scenes', [1], test_no_grid=True) @@ -127,6 +135,19 @@ def test_select_by_type_and_grid(self): self.assert_select_subplots( 'xaxis', 'xaxes', [], row=2, col=2) + def test_select_by_secondary_y(self): + self.assert_select_subplots( + 'yaxis', 'yaxes', [4], secondary_y=True) + + self.assert_select_subplots( + 'yaxis', 'yaxes', [1, 2, 3, 5], secondary_y=False) + + self.assert_select_subplots( + 'yaxis', 'yaxes', [4], col=1, secondary_y=True) + + self.assert_select_subplots( + 'yaxis', 'yaxes', [], col=3, secondary_y=True) + def test_select_by_type_and_selector(self): # xaxis self.assert_select_subplots( @@ -288,10 +309,16 @@ def test_select_by_type_and_grid_and_selector(self): def assert_update_subplots( self, subplot_type, subplots_name, expected_nums, patch=None, - selector=None, row=None, col=None, test_no_grid=False, **kwargs): + selector=None, row=None, col=None, secondary_y=None, + test_no_grid=False, **kwargs): update_fn = getattr(Figure, 'update_' + subplots_name) + if secondary_y is not None: + secy_kwargs = dict(secondary_y=secondary_y) + else: + secy_kwargs = {} + def check_update(fig): # Copy input figure so that we don't modify it @@ -300,7 +327,9 @@ def check_update(fig): # perform update_* update_res = update_fn( - fig, patch, selector=selector, row=row, col=col, **kwargs) + fig, patch, selector=selector, row=row, col=col, + **dict(kwargs, **secy_kwargs)) + self.assertIs(update_res, fig) # Build expected layout keys @@ -331,7 +360,7 @@ def test_update_by_type(self): test_no_grid=True) self.assert_update_subplots( - 'yaxis', 'yaxes', [1, 2, 3, 4], + 'yaxis', 'yaxes', [1, 2, 3, 4, 5], {'range': [5, 10]}, test_no_grid=True) @@ -395,6 +424,17 @@ def test_update_by_type_and_grid(self): row=2, col=3) + def test_update_by_secondary_y(self): + self.assert_update_subplots( + 'yaxis', 'yaxes', [4], + {'range': [5, 10]}, + secondary_y=True) + + self.assert_update_subplots( + 'yaxis', 'yaxes', [1, 2, 3, 5], + {'range': [5, 10]}, + secondary_y=False) + def test_update_by_type_and_grid_and_selector(self): # xaxis self.assert_update_subplots( From 3c9b191c1983578fdc5be268117547acae38381e Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 13 May 2019 10:28:29 -0400 Subject: [PATCH 08/18] secondary_y update/select_traces tests --- .../test_update_objects/test_update_traces.py | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/plotly/tests/test_core/test_update_objects/test_update_traces.py b/plotly/tests/test_core/test_update_objects/test_update_traces.py index 70e7a5280d7..fc48168785d 100644 --- a/plotly/tests/test_core/test_update_objects/test_update_traces.py +++ b/plotly/tests/test_core/test_update_objects/test_update_traces.py @@ -16,7 +16,7 @@ def setUp(self): rows=3, cols=2, specs=[[{}, {'type': 'scene'}], - [{}, {'type': 'polar'}], + [{'secondary_y': True}, {'type': 'polar'}], [{'type': 'domain', 'colspan': 2}, None]] ).update(layout={'height': 800}) @@ -104,6 +104,17 @@ def setUp(self): col=1 ) + # data[9], (2, 1) with secondary_y + fig.add_scatter( + mode='lines', + y=[1, 2, 0], + line={'color': 'purple'}, + name='C', + row=2, + col=1, + secondary_y=True + ) + self.fig = fig self.fig_no_grid = go.Figure(self.fig.to_dict()) @@ -112,10 +123,13 @@ def tearDown(self): # select_traces and for_each_trace # -------------------------------- - def assert_select_traces(self, expected_inds, selector=None, row=None, col=None, test_no_grid=False): + def assert_select_traces( + self, expected_inds, selector=None, + row=None, col=None, secondary_y=None, test_no_grid=False): + # Select traces on figure initialized with make_subplots trace_generator = self.fig.select_traces( - selector=selector, row=row, col=col) + selector=selector, row=row, col=col, secondary_y=secondary_y) self.assertTrue(inspect.isgenerator(trace_generator)) trace_list = list(trace_generator) @@ -124,7 +138,7 @@ def assert_select_traces(self, expected_inds, selector=None, row=None, col=None, # Select traces on figure not containing subplot info if test_no_grid: trace_generator = self.fig_no_grid.select_traces( - selector=selector, row=row, col=col) + selector=selector, row=row, col=col, secondary_y=secondary_y) trace_list = list(trace_generator) self.assertEqual(trace_list, [self.fig_no_grid.data[i] for i in expected_inds]) @@ -135,6 +149,7 @@ def assert_select_traces(self, expected_inds, selector=None, row=None, col=None, selector=selector, row=row, col=col, + secondary_y=secondary_y ) self.assertIs(for_each_res, self.fig) @@ -143,7 +158,7 @@ def assert_select_traces(self, expected_inds, selector=None, row=None, col=None, def test_select_by_type(self): self.assert_select_traces( - [0, 2], selector={'type': 'scatter'}, test_no_grid=True) + [0, 2, 9], selector={'type': 'scatter'}, test_no_grid=True) self.assert_select_traces( [1], selector={'type': 'bar'}, test_no_grid=True) self.assert_select_traces( @@ -160,25 +175,30 @@ def test_select_by_type(self): def test_select_by_grid(self): # Row and column self.assert_select_traces([0, 1], row=1, col=1) - self.assert_select_traces([2, 3], row=2, col=1) + self.assert_select_traces([2, 3, 9], row=2, col=1) self.assert_select_traces([4, 5], row=1, col=2) self.assert_select_traces([6, 7], row=2, col=2) self.assert_select_traces([8], row=3, col=1) # Row only self.assert_select_traces([0, 1, 4, 5], row=1) - self.assert_select_traces([2, 3, 6, 7], row=2) + self.assert_select_traces([2, 3, 6, 7, 9], row=2) self.assert_select_traces([8], row=3) # Col only - self.assert_select_traces([0, 1, 2, 3, 8], col=1) + self.assert_select_traces([0, 1, 2, 3, 8, 9], col=1) self.assert_select_traces([4, 5, 6, 7], col=2) + def test_select_by_secondary_y(self): + self.assert_select_traces([2, 3, 9], row=2, col=1) + self.assert_select_traces([2, 3], row=2, col=1, secondary_y=False) + self.assert_select_traces([9], row=2, col=1, secondary_y=True) + def test_select_by_property_across_trace_types(self): self.assert_select_traces( [0, 4, 6], selector={'mode': 'markers'}, test_no_grid=True) self.assert_select_traces( - [2, 5, 7], selector={'mode': 'lines'}, test_no_grid=True) + [2, 5, 7, 9], selector={'mode': 'lines'}, test_no_grid=True) self.assert_select_traces( [0, 4], selector={'marker': {'color': 'green', 'size': 10}}, @@ -191,7 +211,7 @@ def test_select_by_property_across_trace_types(self): self.assert_select_traces( [0, 4, 6], selector={'marker.color': 'green'}, test_no_grid=True) self.assert_select_traces( - [2, 5, 8], selector={'line.color': 'purple'}, test_no_grid=True) + [2, 5, 8, 9], selector={'line.color': 'purple'}, test_no_grid=True) def test_select_property_and_grid(self): # (1, 1) @@ -202,7 +222,7 @@ def test_select_property_and_grid(self): # (2, 1) self.assert_select_traces( - [2], selector={'mode': 'lines'}, row=2, col=1) + [2, 9], selector={'mode': 'lines'}, row=2, col=1) # (1, 2) self.assert_select_traces( @@ -233,7 +253,7 @@ def test_for_each_trace_lowercase_names(self): # test update_traces # ------------------ def assert_update_traces(self, expected_inds, patch=None, selector=None, - row=None, col=None, **kwargs): + row=None, col=None, secondary_y=None, **kwargs): # Save off original figure fig_orig = copy.deepcopy(self.fig) for trace1, trace2 in zip(fig_orig.data, self.fig.data): @@ -241,7 +261,8 @@ def assert_update_traces(self, expected_inds, patch=None, selector=None, # Perform update update_res = self.fig.update_traces( - patch, selector=selector, row=row, col=col, **kwargs + patch, selector=selector, row=row, col=col, + secondary_y=secondary_y, **kwargs ) # Check chaining support @@ -260,10 +281,10 @@ def assert_update_traces(self, expected_inds, patch=None, selector=None, self.assertEqual(t_orig, t) def test_update_traces_by_type(self): - self.assert_update_traces([0, 2], {'visible': 'legendonly'}, + self.assert_update_traces([0, 2, 9], {'visible': 'legendonly'}, selector={'type': 'scatter'}) - self.assert_update_traces([0, 2], + self.assert_update_traces([0, 2, 9], selector={'type': 'scatter'}, visible=False) @@ -321,3 +342,6 @@ def test_update_traces_by_grid_and_selector(self): self.assert_update_traces([6], {'marker.size': 6}, selector={'marker.color': 'green'}, row=2, col=2) + + self.assert_update_traces([9], {'marker.size': 6}, + col=1, secondary_y=True) From f754a0be01cecf64d105f8ccfac5e8ccd5603225 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 14 May 2019 18:14:37 -0400 Subject: [PATCH 09/18] Add more right margin when secondary y-axis is present --- plotly/subplots.py | 92 ++++++++++--------- .../test_subplots/test_get_subplot.py | 14 +-- .../test_subplots/test_make_subplots.py | 12 +-- 3 files changed, 64 insertions(+), 54 deletions(-) diff --git a/plotly/subplots.py b/plotly/subplots.py index 21c8feed065..1d4cc3c2043 100644 --- a/plotly/subplots.py +++ b/plotly/subplots.py @@ -373,47 +373,6 @@ def make_subplots( if not subplot_titles: subplot_titles = [""] * rows * cols - # ### column_widths ### - if 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(cols), val=repr(column_widths))) - - # ### row_heights ### - if row_heights is None: - heights = [(1. - 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. - 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(cols), val=repr(row_heights))) - # ### Helper to validate coerce elements of lists of dictionaries ### def _check_keys_and_fill(name, arg, defaults): def _checks(item, defaults): @@ -488,8 +447,11 @@ def _checks(item, defaults): _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}' @@ -546,6 +508,54 @@ def _checks(item, defaults): val=repr(val) )) + # ### 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(cols), val=repr(column_widths))) + + # ### row_heights ### + if row_heights is None: + heights = [(1. - 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. - 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(cols), val=repr(row_heights))) + # ### column_titles / row_titles ### if column_titles and not isinstance(column_titles, (list, tuple)): raise ValueError(""" diff --git a/plotly/tests/test_core/test_subplots/test_get_subplot.py b/plotly/tests/test_core/test_subplots/test_get_subplot.py index 3596f4f4afb..ed883c5ef85 100644 --- a/plotly/tests/test_core/test_subplots/test_get_subplot.py +++ b/plotly/tests/test_core/test_subplots/test_get_subplot.py @@ -66,20 +66,20 @@ def test_get_subplot(self): 'mode': 'lines', 'type': 'scattergeo'}, {'dimensions': [{'values': ['A', 'A', 'B', 'A', 'B']}, {'values': ['a', 'a', 'a', 'b', 'b']}], - 'domain': {'x': [0.0, 1.0], 'y': [0.0, 0.19375]}, + 'domain': {'x': [0.0, 0.94], 'y': [0.0, 0.19375]}, 'type': 'parcats'}], 'layout': { - 'geo': {'domain': {'x': [0.55, 1.0], + 'geo': {'domain': {'x': [0.52, 0.94], 'y': [0.26875, 0.4625]}}, 'height': 1200, - 'polar': {'domain': {'x': [0.0, 0.45], + 'polar': {'domain': {'x': [0.0, 0.42], 'y': [0.5375, 0.73125]}}, - 'scene': {'domain': {'x': [0.0, 0.45], + 'scene': {'domain': {'x': [0.0, 0.42], 'y': [0.26875, 0.4625]}}, - 'ternary': {'domain': {'x': [0.55, 1.0], + 'ternary': {'domain': {'x': [0.52, 0.94], 'y': [0.5375, 0.73125]}}, - 'xaxis': {'anchor': 'y', 'domain': [0.0, 0.45]}, - 'xaxis2': {'anchor': 'y2', 'domain': [0.55, 1.0]}, + 'xaxis': {'anchor': 'y', 'domain': [0.0, 0.42]}, + 'xaxis2': {'anchor': 'y2', 'domain': [0.52, 0.94]}, 'yaxis': {'anchor': 'x', 'domain': [0.80625, 1.0]}, 'yaxis2': {'anchor': 'x2', 'domain': [0.80625, 1.0]}, 'yaxis3': {'anchor': 'x2', diff --git a/plotly/tests/test_core/test_subplots/test_make_subplots.py b/plotly/tests/test_core/test_subplots/test_make_subplots.py index 1dec1671fff..b99c4de67e2 100644 --- a/plotly/tests/test_core/test_subplots/test_make_subplots.py +++ b/plotly/tests/test_core/test_subplots/test_make_subplots.py @@ -2177,7 +2177,7 @@ def test_secondary_y(self): expected = Figure({ 'data': [], - 'layout': {'xaxis': {'anchor': 'y', 'domain': [0.0, 1.0]}, + 'layout': {'xaxis': {'anchor': 'y', 'domain': [0.0, 0.94]}, 'yaxis': {'anchor': 'x', 'domain': [0.0, 1.0]}, 'yaxis2': {'anchor': 'x', 'overlaying': 'y', @@ -2206,7 +2206,7 @@ def test_secondary_y_traces(self): 'y': [2, 1, 3], 'xaxis': 'x', 'yaxis': 'y2'}], - 'layout': {'xaxis': {'anchor': 'y', 'domain': [0.0, 1.0]}, + 'layout': {'xaxis': {'anchor': 'y', 'domain': [0.0, 0.94]}, 'yaxis': {'anchor': 'x', 'domain': [0.0, 1.0]}, 'yaxis2': {'anchor': 'x', 'overlaying': 'y', @@ -2261,10 +2261,10 @@ def test_secondary_y_subplots(self): {'name': 'Sixth', 'type': 'scatter', 'xaxis': 'x4', 'y': [2, 3, 6], 'yaxis': 'y8'}], 'layout': { - 'xaxis': {'anchor': 'y', 'domain': [0.0, 0.45]}, - 'xaxis2': {'anchor': 'y3', 'domain': [0.55, 1.0]}, - 'xaxis3': {'anchor': 'y5', 'domain': [0.0, 0.45]}, - 'xaxis4': {'anchor': 'y7', 'domain': [0.55, 1.0]}, + 'xaxis': {'anchor': 'y', 'domain': [0.0, 0.42]}, + 'xaxis2': {'anchor': 'y3', 'domain': [0.52, 0.94]}, + 'xaxis3': {'anchor': 'y5', 'domain': [0.0, 0.42]}, + 'xaxis4': {'anchor': 'y7', 'domain': [0.52, 0.94]}, 'yaxis': {'anchor': 'x', 'domain': [0.575, 1.0]}, 'yaxis2': {'anchor': 'x', 'overlaying': 'y', 'side': 'right'}, From cfee91284eec3a9bdd628f4f754b19fe86ce3680 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 15 May 2019 07:00:58 -0400 Subject: [PATCH 10/18] Add more default horizontal spacing when secondary y-axis is present --- plotly/subplots.py | 34 +++++++++++-------- .../test_subplots/test_get_subplot.py | 19 +++++++---- .../test_subplots/test_make_subplots.py | 10 +++--- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/plotly/subplots.py b/plotly/subplots.py index 1d4cc3c2043..ce81bbfc8bc 100644 --- a/plotly/subplots.py +++ b/plotly/subplots.py @@ -358,21 +358,6 @@ def make_subplots( Received value of type {typ}: {val}""".format( typ=type(start_cell), val=repr(start_cell))) - # ### horizontal_spacing ### - if horizontal_spacing is None: - horizontal_spacing = 0.2 / cols - - # ### vertical_spacing ### - if vertical_spacing is None: - if subplot_titles: - vertical_spacing = 0.5 / rows - else: - vertical_spacing = 0.3 / rows - - # ### subplot titles ### - if not subplot_titles: - subplot_titles = [""] * rows * cols - # ### Helper to validate coerce elements of lists of dictionaries ### def _check_keys_and_fill(name, arg, defaults): def _checks(item, defaults): @@ -508,6 +493,24 @@ def _checks(item, defaults): val=repr(val) )) + # ### horizontal_spacing ### + if horizontal_spacing is None: + if has_secondary_y: + horizontal_spacing = 0.4 / cols + else: + horizontal_spacing = 0.2 / cols + + # ### vertical_spacing ### + if vertical_spacing is None: + if subplot_titles: + vertical_spacing = 0.5 / rows + else: + vertical_spacing = 0.3 / rows + + # ### subplot titles ### + if not subplot_titles: + subplot_titles = [""] * rows * cols + # ### column_widths ### if has_secondary_y: # Add room for secondary y-axis title @@ -517,6 +520,7 @@ def _checks(item, defaults): max_width = 0.98 else: max_width = 1.0 + if column_widths is None: widths = [(max_width - horizontal_spacing * ( cols - 1)) / cols] * cols diff --git a/plotly/tests/test_core/test_subplots/test_get_subplot.py b/plotly/tests/test_core/test_subplots/test_get_subplot.py index ed883c5ef85..412657e40aa 100644 --- a/plotly/tests/test_core/test_subplots/test_get_subplot.py +++ b/plotly/tests/test_core/test_subplots/test_get_subplot.py @@ -66,20 +66,25 @@ def test_get_subplot(self): 'mode': 'lines', 'type': 'scattergeo'}, {'dimensions': [{'values': ['A', 'A', 'B', 'A', 'B']}, {'values': ['a', 'a', 'a', 'b', 'b']}], - 'domain': {'x': [0.0, 0.94], 'y': [0.0, 0.19375]}, + 'domain': {'x': [0.0, 0.9400000000000001], + 'y': [0.0, 0.19375]}, 'type': 'parcats'}], 'layout': { - 'geo': {'domain': {'x': [0.52, 0.94], + 'geo': {'domain': {'x': [0.5700000000000001, + 0.9400000000000001], 'y': [0.26875, 0.4625]}}, 'height': 1200, - 'polar': {'domain': {'x': [0.0, 0.42], + 'polar': {'domain': {'x': [0.0, 0.37], 'y': [0.5375, 0.73125]}}, - 'scene': {'domain': {'x': [0.0, 0.42], + 'scene': {'domain': {'x': [0.0, 0.37], 'y': [0.26875, 0.4625]}}, - 'ternary': {'domain': {'x': [0.52, 0.94], + 'ternary': {'domain': {'x': [0.5700000000000001, + 0.9400000000000001], 'y': [0.5375, 0.73125]}}, - 'xaxis': {'anchor': 'y', 'domain': [0.0, 0.42]}, - 'xaxis2': {'anchor': 'y2', 'domain': [0.52, 0.94]}, + 'xaxis': {'anchor': 'y', 'domain': [0.0, 0.37]}, + 'xaxis2': {'anchor': 'y2', + 'domain': [0.5700000000000001, + 0.9400000000000001]}, 'yaxis': {'anchor': 'x', 'domain': [0.80625, 1.0]}, 'yaxis2': {'anchor': 'x2', 'domain': [0.80625, 1.0]}, 'yaxis3': {'anchor': 'x2', diff --git a/plotly/tests/test_core/test_subplots/test_make_subplots.py b/plotly/tests/test_core/test_subplots/test_make_subplots.py index b99c4de67e2..9d97184cd98 100644 --- a/plotly/tests/test_core/test_subplots/test_make_subplots.py +++ b/plotly/tests/test_core/test_subplots/test_make_subplots.py @@ -2261,10 +2261,12 @@ def test_secondary_y_subplots(self): {'name': 'Sixth', 'type': 'scatter', 'xaxis': 'x4', 'y': [2, 3, 6], 'yaxis': 'y8'}], 'layout': { - 'xaxis': {'anchor': 'y', 'domain': [0.0, 0.42]}, - 'xaxis2': {'anchor': 'y3', 'domain': [0.52, 0.94]}, - 'xaxis3': {'anchor': 'y5', 'domain': [0.0, 0.42]}, - 'xaxis4': {'anchor': 'y7', 'domain': [0.52, 0.94]}, + 'xaxis': {'anchor': 'y', 'domain': [0.0, 0.37]}, + 'xaxis2': {'anchor': 'y3', + 'domain': [0.5700000000000001, 0.9400000000000001]}, + 'xaxis3': {'anchor': 'y5', 'domain': [0.0, 0.37]}, + 'xaxis4': {'anchor': 'y7', + 'domain': [0.5700000000000001, 0.9400000000000001]}, 'yaxis': {'anchor': 'x', 'domain': [0.575, 1.0]}, 'yaxis2': {'anchor': 'x', 'overlaying': 'y', 'side': 'right'}, From b32c5ef1149b32d380c40d8662f8c2ae498ec137 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 19 May 2019 19:12:12 -0400 Subject: [PATCH 11/18] Fix error messages --- plotly/subplots.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plotly/subplots.py b/plotly/subplots.py index ce81bbfc8bc..816bcaf7cee 100644 --- a/plotly/subplots.py +++ b/plotly/subplots.py @@ -538,7 +538,7 @@ def _checks(item, defaults): 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(cols), val=repr(column_widths))) + cols=cols, typ=type(column_widths), val=repr(column_widths))) # ### row_heights ### if row_heights is None: @@ -558,7 +558,7 @@ def _checks(item, defaults): 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(cols), val=repr(row_heights))) + rows=rows, typ=type(row_heights), val=repr(row_heights))) # ### column_titles / row_titles ### if column_titles and not isinstance(column_titles, (list, tuple)): From 257052b4e699f3b8699598ef7f2b7a0b75582c63 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 19 May 2019 19:12:47 -0400 Subject: [PATCH 12/18] Guard against emtpy subplot when setting up matching axes --- plotly/subplots.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plotly/subplots.py b/plotly/subplots.py index 816bcaf7cee..8f7dcc97a00 100644 --- a/plotly/subplots.py +++ b/plotly/subplots.py @@ -845,6 +845,8 @@ def update_axis_matches(first_axis_id, subplot_ref, spec, remove_label): 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( @@ -855,6 +857,8 @@ def update_axis_matches(first_axis_id, subplot_ref, spec, remove_label): 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( @@ -864,6 +868,8 @@ def update_axis_matches(first_axis_id, subplot_ref, spec, remove_label): 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] From a837a18fbe50480152e9c92aa454da5f31f0cedd Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 19 May 2019 19:13:04 -0400 Subject: [PATCH 13/18] Default secondary_y to False --- plotly/subplots.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plotly/subplots.py b/plotly/subplots.py index 8f7dcc97a00..f902f152b21 100644 --- a/plotly/subplots.py +++ b/plotly/subplots.py @@ -1072,7 +1072,6 @@ def _build_subplot_title_annotations( 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: @@ -1286,7 +1285,7 @@ def _pad(s, cell_len=cell_len): return grid_str -def _set_trace_grid_reference(trace, grid_ref, row, col, secondary_y): +def _set_trace_grid_reference(trace, 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)") From 5a1a2b06bafdebe84d85ee863db474aff360755c Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 19 May 2019 19:21:36 -0400 Subject: [PATCH 14/18] Add layout arg back to _set_trace_grid_reference for compatibility --- plotly/basedatatypes.py | 5 +++-- plotly/subplots.py | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 0bd71df329f..823a1171138 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -1602,13 +1602,14 @@ def append_trace(self, trace, row, col): self.add_trace(trace=trace, row=row, col=col) - def _set_trace_grid_position(self, trace, row, col, secondary_y=False): + def _set_trace_grid_position( + self, trace, row, col, secondary_y=False): grid_ref = self._validate_get_grid_ref() from _plotly_future_ import _future_flags if 'v4_subplots' in _future_flags: return _set_trace_grid_reference( - trace, grid_ref, row, col, secondary_y) + trace, self.layout, grid_ref, row, col, secondary_y) if row <= 0: raise Exception("Row value is out of range. " diff --git a/plotly/subplots.py b/plotly/subplots.py index f902f152b21..88a8a663017 100644 --- a/plotly/subplots.py +++ b/plotly/subplots.py @@ -1285,7 +1285,9 @@ def _pad(s, cell_len=cell_len): return grid_str -def _set_trace_grid_reference(trace, grid_ref, row, col, secondary_y=False): +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)") From 7710f4c5174c8f8946208c7daf7d56bfcc58b24c Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 25 May 2019 06:43:53 -0400 Subject: [PATCH 15/18] Fix comment --- plotly/basedatatypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 823a1171138..3dbfe992ee5 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -874,7 +874,7 @@ def _select_layout_subplots_by_prefix( if row is not None or col is not None or secondary_y is not None: # Build mapping from container keys ('xaxis2', 'scene4', etc.) - # to (row, col, secondary_y triplets) + # to (row, col, secondary_y) triplets grid_ref = self._validate_get_grid_ref() container_to_row_col = {} for r, subplot_row in enumerate(grid_ref): From 0a04d51eb2a704307958c11bcbc008498322081d Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 25 May 2019 06:47:13 -0400 Subject: [PATCH 16/18] Make default value a triplet for consistency --- plotly/basedatatypes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 3dbfe992ee5..3144689218f 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -900,11 +900,13 @@ def _select_layout_subplots_by_prefix( # Filter by row/col if (row is not None and - container_to_row_col.get(k, (None, None))[0] != row): + container_to_row_col.get( + k, (None, None, None))[0] != row): # row specified and this is not a match continue elif (col is not None and - container_to_row_col.get(k, (None, None))[1] != col): + container_to_row_col.get( + k, (None, None, None))[1] != col): # col specified and this is not a match continue elif (secondary_y is not None and From d2eee1379192e51be4cd3808c50817045dd5c4a9 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 25 May 2019 08:25:04 -0400 Subject: [PATCH 17/18] Replace yaxis2->y2 in print_grid --- plotly/subplots.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plotly/subplots.py b/plotly/subplots.py index 88a8a663017..65e2b957698 100644 --- a/plotly/subplots.py +++ b/plotly/subplots.py @@ -1199,6 +1199,9 @@ def _get_cell_str(r, c, subplot_refs): }) 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, @@ -1277,8 +1280,13 @@ def _pad(s, cell_len=cell_len): 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 + ','.join(insets_ref[i_inset][0].layout_keys) + s_str + subplot_labels_str + e_str + ' over ' + s_str + _get_cell_str(r, c, ref) + e_str + '\n' ) From 31e8e8922ba5bc1cbf91be5379d5f76672d0b91c Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 25 May 2019 08:34:32 -0400 Subject: [PATCH 18/18] Dedent error message --- plotly/subplots.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plotly/subplots.py b/plotly/subplots.py index 65e2b957698..dcb94138b03 100644 --- a/plotly/subplots.py +++ b/plotly/subplots.py @@ -1350,12 +1350,12 @@ def _get_grid_subplot(fig, row, col, secondary_y=False): from _plotly_future_ import _future_flags if 'v4_subplots' not in _future_flags: raise ValueError(""" - plotly.subplots.get_subplots may only be used in the - v4_subplots _plotly_future_ mode. To try it out, run +plotly.subplots.get_subplots may only be used in the +v4_subplots _plotly_future_ mode. To try it out, run - >>> from _plotly_future_ import v4_subplots +>>> from _plotly_future_ import v4_subplots - before importing plotly. +before importing plotly. """) try: