Repository: plotly/plotly.py
Pull request #4153 (2023-04-06 19:41:43)

Adding some rounding and error messaging for subplots
<!--
Please uncomment this block and take a look at this checklist if your PR is making substantial changes to **documentation**/impacts files in the `doc` directory. Check all that apply to your PR, and leave the rest unchecked to discuss with your reviewer! Not all boxes must be checked for every PR :)

If your PR modifies code of the `plotly` package, we have a different checklist
below :-).

### Documentation PR

- [ ] I've [seen the `doc/README.md` file](https://github.com/plotly/plotly.py/blob/master/doc/README.md)
- [ ] This change runs in the current version of Plotly on PyPI and targets the `doc-prod` branch OR it targets the `master` branch
- [ ] If this PR modifies the first example in a page or adds a new one, it is a `px` example if at all possible
- [ ] Every new/modified example has a descriptive title and motivating sentence or paragraph
- [ ] Every new/modified example is independently runnable
- [ ] Every new/modified example is optimized for short line count	and focuses on the Plotly/visualization-related aspects of the example rather than the computation required to produce the data being visualized
- [ ] Meaningful/relatable datasets are used for all new examples instead of randomly-generated data where possible
- [ ] The random seed is set if using randomly-generated data in new/modified examples
- [ ] New/modified remote datasets are loaded from https://plotly.github.io/datasets and added to https://github.com/plotly/datasets
- [ ] Large computations are avoided in the new/modified examples in favour of loading remote datasets that represent the output of such computations
- [ ] Imports are `plotly.graph_objects as go` / `plotly.express as px` / `plotly.io as pio`
- [ ] Data frames are always called `df`
- [ ] `fig = <something>` call is high up in each new/modified example (either `px.<something>` or `make_subplots` or `go.Figure`)
- [ ] Liberal use is made of `fig.add_*` and `fig.update_*` rather than `go.Figure(data=..., layout=...)` in every new/modified example
- [ ] Specific adders and updaters like `fig.add_shape` and `fig.update_xaxes` are used instead of big `fig.update_layout` calls in every new/modified example
- [ ] `fig.show()` is at the end of each new/modified example
- [ ] `plotly.plot()` and `plotly.iplot()` are not used in any new/modified example
- [ ] Hex codes for colors are not used in any new/modified example in favour of [these nice ones](https://github.com/plotly/plotly.py/issues/2192)
-->

## Code PR

- [x] I have read through the [contributing notes](https://github.com/plotly/plotly.py/blob/master/contributing.md) and understand the structure of the package. In particular, if my PR modifies code of `plotly.graph_objects`, my modifications concern the `codegen` files and not generated files.
- [x] I have added tests (if submitting a new feature or correcting a bug) or
  modified existing tests.
- [ ] For a new feature, I have added documentation examples in an existing or
  new tutorial notebook (please see the doc checklist as well).
- [x] I have added a CHANGELOG entry if fixing/changing/adding anything substantial.
- [ ] For a new feature or a change in behaviour, I have updated the relevant docstrings in the code to describe the feature or behaviour (please see the doc checklist as well).

-->



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78a996637..880774085 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 
 ### Fixed
   - Fixed another compatibility issue with Pandas 2.0, just affecting `px.*(line_close=True)` [[#4190](https://github.com/plotly/plotly.py/pull/4190)]
+  - Added some rounding to the `make_subplots` function to handle situations where the user-input specs cause the domain to exceed 1 by small amounts https://github.com/plotly/plotly.py/pull/4153
 
 ## [5.14.1] - 2023-04-05
 
diff --git a/packages/python/plotly/plotly/_subplots.py b/packages/python/plotly/plotly/_subplots.py
index 24ec630e0..f2606f49e 100644
--- a/packages/python/plotly/plotly/_subplots.py
+++ b/packages/python/plotly/plotly/_subplots.py
@@ -671,7 +671,6 @@ The row_titles argument to make_subplots must be a list or tuple
     # Loop through specs -- (r, c) <-> (row, col)
     for r, spec_row in enumerate(specs):
         for c, spec in enumerate(spec_row):
-
             if spec is None:  # skip over None cells
                 continue
 
@@ -702,6 +701,46 @@ The row_titles argument to make_subplots must be a list or tuple
             else:
                 y_s = grid[r_spanned][c][1] + spec["b"]
                 y_e = grid[r][c][1] + heights[-1 - r] - spec["t"]
+
+            if y_s < 0.0:
+                # round for values very close to one
+                # handles some floating point errors
+                if y_s > -0.01:
+                    y_s = 0.0
+                else:
+                    raise Exception(
+                        "A combination of the 'b' values, heights, and "
+                        "number of subplots too large for this subplot grid."
+                    )
+            if y_s > 1.0:
+                # round for values very close to one
+                # handles some floating point errors
+                if y_s < 1.01:
+                    y_s = 1.0
+                else:
+                    raise Exception(
+                        "A combination of the 'b' values, heights, and "
+                        "number of subplots too large for this subplot grid."
+                    )
+
+            if y_e < 0.0:
+                if y_e > -0.01:
+                    y_e = 0.0
+                else:
+                    raise Exception(
+                        "A combination of the 't' values, heights, and "
+                        "number of subplots too large for this subplot grid."
+                    )
+
+            if y_e > 1.0:
+                if y_e < 1.01:
+                    y_e = 1.0
+                else:
+                    raise Exception(
+                        "A combination of the 't' values, heights, and "
+                        "number of subplots too large for this subplot grid."
+                    )
+
             y_domain = [y_s, y_e]
 
             list_of_domains.append(x_domain)
@@ -726,7 +765,6 @@ The row_titles argument to make_subplots must be a list or tuple
     insets_ref = [None for inset in range(len(insets))] if insets else None
     if insets:
         for i_inset, inset in enumerate(insets):
-
             r = inset["cell"][0] - 1
             c = inset["cell"][1] - 1
 
@@ -1052,7 +1090,6 @@ def _subplot_type_for_trace_type(trace_type):
 
 
 def _validate_coerce_subplot_type(subplot_type):
-
     # Lowercase subplot_type
     orig_subplot_type = subplot_type
     subplot_type = subplot_type.lower()
@@ -1200,7 +1237,6 @@ def _build_subplot_title_annotations(
 
 
 def _build_grid_str(specs, grid_ref, insets, insets_ref, row_seq):
-
     # Compute rows and columns
     rows = len(specs)
     cols = len(specs[0])
@@ -1257,7 +1293,6 @@ def _build_grid_str(specs, grid_ref, insets, insets_ref, row_seq):
     # Loop through specs, fill in _tmp
     for r, spec_row in enumerate(specs):
         for c, spec in enumerate(spec_row):
-
             ref = grid_ref[r][c]
             if ref is None:
                 if _tmp[r][c] == "":
@@ -1339,7 +1374,6 @@ def _build_grid_str(specs, grid_ref, insets, insets_ref, row_seq):
 
 
 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)"
@@ -1461,7 +1495,6 @@ Unexpected subplot type with layout_keys of {}""".format(
 
 
 def _get_subplot_ref_for_trace(trace):
-
     if "domain" in trace:
         return SubplotRef(
             subplot_type="domain",


Test patch:
diff --git a/packages/python/plotly/plotly/tests/test_core/test_subplots/test_make_subplots.py b/packages/python/plotly/plotly/tests/test_core/test_subplots/test_make_subplots.py
index 7c7a1a9da..28ad18f19 100644
--- a/packages/python/plotly/plotly/tests/test_core/test_subplots/test_make_subplots.py
+++ b/packages/python/plotly/plotly/tests/test_core/test_subplots/test_make_subplots.py
@@ -660,6 +660,22 @@ class TestMakeSubplots(TestCase):
         )
         self.assertEqual(fig.to_plotly_json(), expected.to_plotly_json())
 
+    def test_specs_rounding_rounds_down(self):
+        n_subplots = 8
+        padding_size = 0.2
+
+        specs = []
+        for _ in range(n_subplots):
+            specs.append([{"b": padding_size / 2.0, "t": padding_size / 2.0}])
+
+        fig = subplots.make_subplots(rows=n_subplots, specs=specs)
+        self.assertTrue(
+            all(
+                fig.layout[f"yaxis{i if i > 1 else ''}"]["domain"][0] <= 1.0
+                for i in range(1, n_subplots + 1)
+            )
+        )
+
     def test_specs_padding_bottom_left(self):
         expected = Figure(
             data=Data(),
@@ -1592,7 +1608,6 @@ class TestMakeSubplots(TestCase):
         )
 
     def test_row_width_and_column_width(self):
-
         expected = Figure(
             {
                 "data": [],
@@ -1680,7 +1695,6 @@ class TestMakeSubplots(TestCase):
         self.assertEqual(fig.to_plotly_json(), expected.to_plotly_json())
 
     def test_row_width_and_shared_yaxes(self):
-
         expected = Figure(
             {
                 "data": [],