From be8c49df639bbe55d68ade5eb37f84d3bb6ac0ba Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Balusu Date: Tue, 24 Mar 2026 22:34:05 -0400 Subject: [PATCH 1/3] Fix partial autorange when range contains None elements When setting `range=[value, None]` or `range=[None, value]` on an axis, plotly.js should use partial autoranging (autorange="max" or "min"). However, plotly.js impliedEdits mechanism sets autorange=false whenever range is specified, which prevents partial autoranging from working. This fix explicitly sets the autorange property to the appropriate value when range contains None elements: - range=[value, None] -> autorange="max" - range=[None, value] -> autorange="min" - range=[None, None] -> autorange=True The fix handles all code paths: update_yaxes/update_xaxes, update_layout, and direct property assignment. When the user explicitly provides an autorange value in the same update call, it takes precedence. Fixes #5536 --- plotly/basedatatypes.py | 49 ++++++++++++++ .../test_update_subplots.py | 66 +++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 1384e08d543..b7194919a50 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -3991,6 +3991,35 @@ def _perform_update(plotly_obj, update_obj, overwrite=False): # Assign non-compound value plotly_obj[key] = val + # Handle partial autorange for axis range with None elements + # ----------------------------------------------------------- + # When 'range' is set with None elements on an axis object that + # has 'autorange', set autorange to the appropriate value so + # that plotly.js partial autoranging works correctly. + # Without this, plotly.js impliedEdits sets autorange=false when + # range is specified, preventing partial autorange behavior. + if ( + "range" in update_obj + and "autorange" in plotly_obj._valid_props + ): + if "autorange" in update_obj: + # User explicitly provided autorange in the same update. + # Re-apply it to ensure it takes precedence over any + # implicit autorange set by the __setitem__ handler + # for 'range'. + plotly_obj["autorange"] = update_obj["autorange"] + else: + range_val = update_obj["range"] + if isinstance(range_val, (list, tuple)) and len(range_val) == 2: + has_null_lower = range_val[0] is None + has_null_upper = range_val[1] is None + if has_null_lower and has_null_upper: + plotly_obj["autorange"] = True + elif has_null_lower: + plotly_obj["autorange"] = "min" + elif has_null_upper: + plotly_obj["autorange"] = "max" + elif isinstance(plotly_obj, tuple): if len(update_obj) == 0: # Nothing to do @@ -4930,6 +4959,26 @@ def __setitem__(self, prop, value): # ### Handle simple property ### else: self._set_prop(prop, value) + + # Handle partial autorange when 'range' is set directly + # with None elements on an axis object. + # When range contains a None (null) element, plotly.js + # should use partial autoranging, but its impliedEdits + # mechanism sets autorange=false when range is specified. + # We counteract this by explicitly setting autorange. + if prop == "range" and "autorange" in self._valid_props: + if ( + isinstance(value, (list, tuple)) + and len(value) == 2 + ): + has_null_lower = value[0] is None + has_null_upper = value[1] is None + if has_null_lower and has_null_upper: + self._set_prop("autorange", True) + elif has_null_lower: + self._set_prop("autorange", "min") + elif has_null_upper: + self._set_prop("autorange", "max") else: # Make sure properties dict is initialized self._init_props() diff --git a/tests/test_core/test_update_objects/test_update_subplots.py b/tests/test_core/test_update_objects/test_update_subplots.py index 159b995ca40..71b25840f43 100644 --- a/tests/test_core/test_update_objects/test_update_subplots.py +++ b/tests/test_core/test_update_objects/test_update_subplots.py @@ -599,3 +599,69 @@ def test_update_subplot_overwrite(self): fig.layout.xaxis.to_plotly_json(), {"title": {"font": {"family": "Courier"}}}, ) + + +class TestPartialAutorange(TestCase): + """Tests for automatic autorange setting when range contains None. + + When a user sets range=[value, None] or range=[None, value], plotly.js + should use partial autoranging. However, plotly.js impliedEdits sets + autorange=false when range is specified. We counteract this by + explicitly setting autorange to the appropriate value. + + See: https://github.com/plotly/plotly.py/issues/5536 + """ + + def test_range_upper_none_sets_autorange_max(self): + fig = go.Figure() + fig.add_trace(go.Scatter(y=[0, 6])) + fig.update_yaxes(range=[0, None]) + self.assertEqual(fig.layout.yaxis.autorange, "max") + + def test_range_lower_none_sets_autorange_min(self): + fig = go.Figure() + fig.add_trace(go.Scatter(y=[0, 6])) + fig.update_yaxes(range=[None, 5]) + self.assertEqual(fig.layout.yaxis.autorange, "min") + + def test_range_both_none_sets_autorange_true(self): + fig = go.Figure() + fig.add_trace(go.Scatter(y=[0, 6])) + fig.update_yaxes(range=[None, None]) + self.assertEqual(fig.layout.yaxis.autorange, True) + + def test_range_no_none_does_not_set_autorange(self): + fig = go.Figure() + fig.add_trace(go.Scatter(y=[0, 6])) + fig.update_yaxes(range=[0, 10]) + self.assertIsNone(fig.layout.yaxis.autorange) + + def test_xaxis_range_with_none(self): + fig = go.Figure() + fig.add_trace(go.Scatter(y=[0, 6])) + fig.update_xaxes(range=[None, 5]) + self.assertEqual(fig.layout.xaxis.autorange, "min") + + def test_explicit_autorange_overrides_implicit(self): + fig = go.Figure() + fig.add_trace(go.Scatter(y=[0, 6])) + fig.update_yaxes(range=[0, None], autorange="max reversed") + self.assertEqual(fig.layout.yaxis.autorange, "max reversed") + + def test_explicit_autorange_dict_order_preserved(self): + fig = go.Figure() + fig.add_trace(go.Scatter(y=[0, 6])) + fig.update_yaxes(**{"autorange": "max reversed", "range": [0, None]}) + self.assertEqual(fig.layout.yaxis.autorange, "max reversed") + + def test_direct_property_assignment(self): + fig = go.Figure() + fig.add_trace(go.Scatter(y=[0, 6])) + fig.layout.yaxis.range = [0, None] + self.assertEqual(fig.layout.yaxis.autorange, "max") + + def test_update_layout_yaxis_range(self): + fig = go.Figure() + fig.add_trace(go.Scatter(y=[0, 6])) + fig.update_layout(yaxis_range=[None, 5]) + self.assertEqual(fig.layout.yaxis.autorange, "min") From 5a03e24468ce3fb35a76c23bb290b8091403678f Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Balusu Date: Wed, 25 Mar 2026 09:25:42 -0400 Subject: [PATCH 2/3] Fix ruff formatting in basedatatypes.py Apply ruff format to fix code style issues flagged by CI: - Collapse multi-line if conditions onto single lines where they fit - Fix lambda expression formatting in layout_keys_filters --- plotly/basedatatypes.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index b7194919a50..360202063c0 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -1447,10 +1447,12 @@ def _select_layout_subplots_by_prefix( layout_keys_filters = [ lambda k: k.startswith(prefix) and self.layout[k] is not None, - lambda k: row is None - or container_to_row_col.get(k, (None, None, None))[0] == row, - lambda k: col is None - or container_to_row_col.get(k, (None, None, None))[1] == col, + lambda k: ( + row is None or container_to_row_col.get(k, (None, None, None))[0] == row + ), + lambda k: ( + col is None or container_to_row_col.get(k, (None, None, None))[1] == col + ), lambda k: ( secondary_y is None or container_to_row_col.get(k, (None, None, None))[2] == secondary_y @@ -3998,10 +4000,7 @@ def _perform_update(plotly_obj, update_obj, overwrite=False): # that plotly.js partial autoranging works correctly. # Without this, plotly.js impliedEdits sets autorange=false when # range is specified, preventing partial autorange behavior. - if ( - "range" in update_obj - and "autorange" in plotly_obj._valid_props - ): + if "range" in update_obj and "autorange" in plotly_obj._valid_props: if "autorange" in update_obj: # User explicitly provided autorange in the same update. # Re-apply it to ensure it takes precedence over any @@ -4967,10 +4966,7 @@ def __setitem__(self, prop, value): # mechanism sets autorange=false when range is specified. # We counteract this by explicitly setting autorange. if prop == "range" and "autorange" in self._valid_props: - if ( - isinstance(value, (list, tuple)) - and len(value) == 2 - ): + if isinstance(value, (list, tuple)) and len(value) == 2: has_null_lower = value[0] is None has_null_upper = value[1] is None if has_null_lower and has_null_upper: From e73c2a02f43ebcabe2e9c2d1ef46c10d6b7bcd6a Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Balusu Date: Wed, 25 Mar 2026 12:45:19 -0400 Subject: [PATCH 3/3] Retrigger CI: previous failure was unrelated kaleido infra issue The test_optional_py-3.13 CI job failed due to a BrowserDepsError in test_kaleido (missing Chromium system dependencies), not due to any code change in this PR. Retriggering CI with empty commit.