Repository: plotly/plotly.py

Issue:
plotly express scatter with more than ~34 facet rows breaks
I am trying to create subplots for a number of categories. I have approximately 250 categories in my actual data. I have found that this error occurs all the way down to 35 categories, and disappears around 34.

```python
import numpy as np
import pandas as pd
import plotly.express as px
import plotly

# Versions I'm running with
np.__version__ # 1.17.4
pd.__version__ # 0.25.3
plotly.__version__  # 4.4.1


df = pd.DataFrame({
    'x':np.random.rand(10_000), 
    'y':np.random.rand(10_000), 
    'category':np.random.choice(range(35), 10_000)
})

# The following breaks
fig = px.scatter(df, x = 'x', y = 'y', facet_row = 'category')

# Specifying a height seems to change nothing
fig = px.scatter(df, x = 'x', y = 'y', facet_row = 'category', height = 20_000)
```
I get the following error in both cases
```python
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-40-f3e17e288c8e> in <module>
----> 1 fig = px.scatter(df, x = 'x', y = 'y', facet_row = 'category', height = 20_000)

~/miniconda3/lib/python3.7/site-packages/plotly/express/_chart_types.py in scatter(data_frame, x, y, color, symbol, size, hover_name, hover_data, custom_data, text, facet_row, facet_col, facet_col_wrap, error_x, error_x_minus, error_y, error_y_minus, animation_frame, animation_group, category_orders, labels, color_discrete_sequence, color_discrete_map, color_continuous_scale, range_color, color_continuous_midpoint, symbol_sequence, symbol_map, opacity, size_max, marginal_x, marginal_y, trendline, trendline_color_override, log_x, log_y, range_x, range_y, render_mode, title, template, width, height)
     53     mark in 2D space.
     54     """
---> 55     return make_figure(args=locals(), constructor=go.Scatter)
     56 
     57 

~/miniconda3/lib/python3.7/site-packages/plotly/express/_core.py in make_figure(args, constructor, trace_patch, layout_patch)
   1358 
   1359     fig = init_figure(
-> 1360         args, subplot_type, frame_list, nrows, ncols, col_labels, row_labels
   1361     )
   1362 

~/miniconda3/lib/python3.7/site-packages/plotly/express/_core.py in init_figure(args, subplot_type, frame_list, nrows, ncols, col_labels, row_labels)
   1464         row_heights=row_heights,
   1465         column_widths=column_widths,
-> 1466         start_cell="bottom-left",
   1467     )
   1468 

~/miniconda3/lib/python3.7/site-packages/plotly/subplots.py in make_subplots(rows, cols, shared_xaxes, shared_yaxes, start_cell, print_grid, horizontal_spacing, vertical_spacing, subplot_titles, column_widths, row_heights, specs, insets, column_titles, row_titles, x_title, y_title, **kwargs)
    683             secondary_y = spec["secondary_y"]
    684             subplot_refs = _init_subplot(
--> 685                 layout, subplot_type, secondary_y, x_domain, y_domain, max_subplot_ids
    686             )
    687             grid_ref[r][c] = subplot_refs

~/miniconda3/lib/python3.7/site-packages/plotly/subplots.py in _init_subplot(layout, subplot_type, secondary_y, x_domain, y_domain, max_subplot_ids)
   1055     if subplot_type == "xy":
   1056         subplot_refs = _init_subplot_xy(
-> 1057             layout, secondary_y, x_domain, y_domain, max_subplot_ids
   1058         )
   1059     elif subplot_type in _single_subplot_types:

~/miniconda3/lib/python3.7/site-packages/plotly/subplots.py in _init_subplot_xy(layout, secondary_y, x_domain, y_domain, max_subplot_ids)
    919 
    920     layout[xaxis_name] = x_axis
--> 921     layout[yaxis_name] = y_axis
    922 
    923     subplot_refs = [

~/miniconda3/lib/python3.7/site-packages/plotly/basedatatypes.py in __setitem__(self, prop, value)
   4435         if match is None:
   4436             # Set as ordinary property
-> 4437             super(BaseLayoutHierarchyType, self).__setitem__(prop, value)
   4438         else:
   4439             # Set as subplotid property

~/miniconda3/lib/python3.7/site-packages/plotly/basedatatypes.py in __setitem__(self, prop, value)
   3480             # ### Handle compound property ###
   3481             if isinstance(validator, CompoundValidator):
-> 3482                 self._set_compound_prop(prop, value)
   3483 
   3484             # ### Handle compound array property ###

~/miniconda3/lib/python3.7/site-packages/plotly/basedatatypes.py in _set_compound_prop(self, prop, val)
   3834         # ------------
   3835         validator = self._validators.get(prop)
-> 3836         val = validator.validate_coerce(val, skip_invalid=self._skip_invalid)
   3837 
   3838         # Save deep copies of current and new states

~/miniconda3/lib/python3.7/site-packages/_plotly_utils/basevalidators.py in validate_coerce(self, v, skip_invalid)
   2442 
   2443         elif isinstance(v, dict):
-> 2444             v = self.data_class(v, skip_invalid=skip_invalid)
   2445 
   2446         elif isinstance(v, self.data_class):

~/miniconda3/lib/python3.7/site-packages/plotly/graph_objs/layout/__init__.py in __init__(self, arg, anchor, automargin, autorange, calendar, categoryarray, categoryarraysrc, categoryorder, color, constrain, constraintoward, dividercolor, dividerwidth, domain, dtick, exponentformat, fixedrange, gridcolor, gridwidth, hoverformat, layer, linecolor, linewidth, matches, mirror, nticks, overlaying, position, range, rangemode, scaleanchor, scaleratio, separatethousands, showdividers, showexponent, showgrid, showline, showspikes, showticklabels, showtickprefix, showticksuffix, side, spikecolor, spikedash, spikemode, spikesnap, spikethickness, tick0, tickangle, tickcolor, tickfont, tickformat, tickformatstops, tickformatstopdefaults, ticklen, tickmode, tickprefix, ticks, tickson, ticksuffix, ticktext, ticktextsrc, tickvals, tickvalssrc, tickwidth, title, titlefont, type, uirevision, visible, zeroline, zerolinecolor, zerolinewidth, **kwargs)
   3053         self["dividerwidth"] = dividerwidth if dividerwidth is not None else _v
   3054         _v = arg.pop("domain", None)
-> 3055         self["domain"] = domain if domain is not None else _v
   3056         _v = arg.pop("dtick", None)
   3057         self["dtick"] = dtick if dtick is not None else _v

~/miniconda3/lib/python3.7/site-packages/plotly/basedatatypes.py in __setitem__(self, prop, value)
   3488             # ### Handle simple property ###
   3489             else:
-> 3490                 self._set_prop(prop, value)
   3491 
   3492         # Handle non-scalar case

~/miniconda3/lib/python3.7/site-packages/plotly/basedatatypes.py in _set_prop(self, prop, val)
   3775                 return
   3776             else:
-> 3777                 raise err
   3778 
   3779         # val is None

~/miniconda3/lib/python3.7/site-packages/plotly/basedatatypes.py in _set_prop(self, prop, val)
   3770         validator = self._validators.get(prop)
   3771         try:
-> 3772             val = validator.validate_coerce(val)
   3773         except ValueError as err:
   3774             if self._skip_invalid:

~/miniconda3/lib/python3.7/site-packages/_plotly_utils/basevalidators.py in validate_coerce(self, v)
   2170             for i, (el, validator) in enumerate(zip(v, self.item_validators)):
   2171                 # Validate coerce elements
-> 2172                 v[i] = validator.validate_coerce(el)
   2173 
   2174         return v

~/miniconda3/lib/python3.7/site-packages/_plotly_utils/basevalidators.py in validate_coerce(self, v)
    785             if self.has_min_max:
    786                 if not (self.min_val <= v <= self.max_val):
--> 787                     self.raise_invalid_val(v)
    788         return v
    789 

~/miniconda3/lib/python3.7/site-packages/_plotly_utils/basevalidators.py in raise_invalid_val(self, v, inds)
    281                 typ=type_str(v),
    282                 v=repr(v),
--> 283                 valid_clr_desc=self.description(),
    284             )
    285         )

ValueError: 
    Invalid value of type 'builtins.float' received for the 'domain[1]' property of layout.yaxis
        Received value: -0.0005714285714285719

    The 'domain[1]' property is a number and may be specified as:
      - An int or float in the interval [0, 1]
```


Discussion:
Related to #1031, #1216. This bug still does present as of now (I am using plotly 4.4.1).
I get the same error (plotly 4.5.0). Were you able to find a workaround?
Hi @jonmmease , I am using plotly 4.5.1 and am getting this error too as i tried creating a subplot with 63 rows.  it gives an error if the row is bigger than 58 rows. 

Here is the code:
`
fig = make_subplots(rows=63, cols=1, shared_xaxes=True, vertical_spacing=0.02)
size = len(asinhourlydict)
xvalues = ["12am", "1am", "2am","3am","4am","5am","6am","7am","8am","9am","10am","11am","12pm","1pm", "2pm","3pm","4pm","5pm","6pm","7pm","8pm","9pm","10pm","11pm" ]
for key,value in asinhourlydict.items():
    fig.add_trace(go.Bar(x=xvalues,y=value),row=size,col= 1)
    fig.update_yaxes(title_text=key, row=size, col=1)
    size = size -1
fig.write_html('violationbarchart.html', config={ "displayModeBar" : False} )
`

and here is the error:

`
ValueError:
    Invalid value of type 'builtins.float' received for the 'domain[0]' property of layout.yaxis
        Received value: 1.0038095238095237
    The 'domain[0]' property is a number and may be specified as:
      - An int or float in the interval [0, 1]
`

Is there a way to fix this? Or is it my code that have bugs? Thanks in advance! 
@albertsugi-tallridge you're encountering bug https://github.com/plotly/plotly.py/issues/2556 and you can get around it by reducing your `vertical_spacing`
The reason this is breaking down is because Plotly Express' defaults are running afoul of the problem described in #2556... We'll have to solve it within Plotly Express, as we don't expose `vertical_spacing` in the PX API, so PX needs to be smarter about how it sets things.
In Plotly Express you can now (as of version 4.9) avoid this problem by using `facet_row_spacing` and `facet_col_spacing` https://plotly.com/python/facet-plots/#controlling-facet-spacing
I'll close this issue when I finish #2556 and the error message will be clearer :)
Thanks Nicholas! Ill look into it!
Albert SugiantosdasdsadsasdasdAAAAAaAAAAA
Developer
<http://www.tallridge.com/>
Direct 847.504.17XX
Fax 847.504.1700
Toll Free 877.776.3644
*Northbrook*
722 Landwehr Rd
Northbrook, IL 60062
*Chicago*
20 N Wacker Dr, Ste 1200
Chicago, IL 60606


On Thu, Jul 16, 2020 at 8:52 AM Nicolas Kruchten <notifications@github.com>
wrote:

> I'll close this issue when I finish #2556
> <https://github.com/plotly/plotly.py/issues/2556> and the error message
> will be clearer :)
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/plotly/plotly.py/issues/2026#issuecomment-659426675>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AOQ73TYBMS6TK5ZK52SEEHTR34AZPANCNFSM4KCGCDYA>
> .
>


Resolving patch:
diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py
--- a/packages/python/plotly/plotly/express/_core.py
+++ b/packages/python/plotly/plotly/express/_core.py
@@ -2120,22 +2120,41 @@ def init_figure(args, subplot_type, frame_list, nrows, ncols, col_labels, row_la
             for j in range(ncols):
                 subplot_labels[i * ncols + j] = col_labels[(nrows - 1 - i) * ncols + j]
 
+    def _spacing_error_translator(e, direction, facet_arg):
+        """
+        Translates the spacing errors thrown by the underlying make_subplots
+        routine into one that describes an argument adjustable through px.
+        """
+        if ("%s spacing" % (direction,)) in e.args[0]:
+            e.args = (
+                e.args[0]
+                + """
+Use the {facet_arg} argument to adjust this spacing.""".format(
+                    facet_arg=facet_arg
+                ),
+            )
+            raise e
+
     # Create figure with subplots
-    fig = make_subplots(
-        rows=nrows,
-        cols=ncols,
-        specs=specs,
-        shared_xaxes="all",
-        shared_yaxes="all",
-        row_titles=[] if facet_col_wrap else list(reversed(row_labels)),
-        column_titles=[] if facet_col_wrap else col_labels,
-        subplot_titles=subplot_labels if facet_col_wrap else [],
-        horizontal_spacing=horizontal_spacing,
-        vertical_spacing=vertical_spacing,
-        row_heights=row_heights,
-        column_widths=column_widths,
-        start_cell="bottom-left",
-    )
+    try:
+        fig = make_subplots(
+            rows=nrows,
+            cols=ncols,
+            specs=specs,
+            shared_xaxes="all",
+            shared_yaxes="all",
+            row_titles=[] if facet_col_wrap else list(reversed(row_labels)),
+            column_titles=[] if facet_col_wrap else col_labels,
+            subplot_titles=subplot_labels if facet_col_wrap else [],
+            horizontal_spacing=horizontal_spacing,
+            vertical_spacing=vertical_spacing,
+            row_heights=row_heights,
+            column_widths=column_widths,
+            start_cell="bottom-left",
+        )
+    except ValueError as e:
+        _spacing_error_translator(e, "Horizontal", "facet_col_spacing")
+        _spacing_error_translator(e, "Vertical", "facet_row_spacing")
 
     # Remove explicit font size of row/col titles so template can take over
     for annot in fig.layout.annotations:
diff --git a/packages/python/plotly/plotly/subplots.py b/packages/python/plotly/plotly/subplots.py
--- a/packages/python/plotly/plotly/subplots.py
+++ b/packages/python/plotly/plotly/subplots.py
@@ -530,12 +530,32 @@ def _checks(item, defaults):
             )
         )
 
+    def _check_hv_spacing(dimsize, spacing, name, dimvarname, dimname):
+        if spacing < 0 or spacing > 1:
+            raise ValueError("%s spacing must be between 0 and 1." % (name,))
+        if dimsize <= 1:
+            return
+        max_spacing = 1.0 / float(dimsize - 1)
+        if spacing > max_spacing:
+            raise ValueError(
+                """{name} spacing cannot be greater than (1 / ({dimvarname} - 1)) = {max_spacing:f}.
+The resulting plot would have {dimsize} {dimname} ({dimvarname}={dimsize}).""".format(
+                    dimvarname=dimvarname,
+                    name=name,
+                    dimname=dimname,
+                    max_spacing=max_spacing,
+                    dimsize=dimsize,
+                )
+            )
+
     # ### horizontal_spacing ###
     if horizontal_spacing is None:
         if has_secondary_y:
             horizontal_spacing = 0.4 / cols
         else:
             horizontal_spacing = 0.2 / cols
+    # check horizontal_spacing can be satisfied:
+    _check_hv_spacing(cols, horizontal_spacing, "Horizontal", "cols", "columns")
 
     # ### vertical_spacing ###
     if vertical_spacing is None:
@@ -543,6 +563,8 @@ def _checks(item, defaults):
             vertical_spacing = 0.5 / rows
         else:
             vertical_spacing = 0.3 / rows
+    # check vertical_spacing can be satisfied:
+    _check_hv_spacing(rows, vertical_spacing, "Vertical", "rows", "rows")
 
     # ### subplot titles ###
     if subplot_titles is None:


Test patch:
diff --git a/packages/python/plotly/plotly/tests/test_core/test_px/test_facets.py b/packages/python/plotly/plotly/tests/test_core/test_px/test_facets.py
--- a/packages/python/plotly/plotly/tests/test_core/test_px/test_facets.py
+++ b/packages/python/plotly/plotly/tests/test_core/test_px/test_facets.py
@@ -1,5 +1,9 @@
+import plotly
+import pandas as pd
 import plotly.express as px
 from pytest import approx
+import pytest
+import random
 
 
 def test_facets():
@@ -41,3 +45,45 @@ def test_facets():
     )
     assert fig.layout.xaxis4.domain[0] - fig.layout.xaxis.domain[1] == approx(0.09)
     assert fig.layout.yaxis4.domain[0] - fig.layout.yaxis.domain[1] == approx(0.08)
+
+
+@pytest.fixture
+def bad_facet_spacing_df():
+    NROWS = 101
+    NDATA = 1000
+    categories = [n % NROWS for n in range(NDATA)]
+    df = pd.DataFrame(
+        {
+            "x": [random.random() for _ in range(NDATA)],
+            "y": [random.random() for _ in range(NDATA)],
+            "category": categories,
+        }
+    )
+    return df
+
+
+def test_bad_facet_spacing_eror(bad_facet_spacing_df):
+    df = bad_facet_spacing_df
+    with pytest.raises(
+        ValueError, match="Use the facet_row_spacing argument to adjust this spacing\."
+    ):
+        fig = px.scatter(
+            df, x="x", y="y", facet_row="category", facet_row_spacing=0.01001
+        )
+    with pytest.raises(
+        ValueError, match="Use the facet_col_spacing argument to adjust this spacing\."
+    ):
+        fig = px.scatter(
+            df, x="x", y="y", facet_col="category", facet_col_spacing=0.01001
+        )
+    # Check error is not raised when the spacing is OK
+    try:
+        fig = px.scatter(df, x="x", y="y", facet_row="category", facet_row_spacing=0.01)
+    except ValueError:
+        # Error shouldn't be raised, so fail if it is
+        assert False
+    try:
+        fig = px.scatter(df, x="x", y="y", facet_col="category", facet_col_spacing=0.01)
+    except ValueError:
+        # Error shouldn't be raised, so fail if it is
+        assert False
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
--- 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
@@ -1,6 +1,7 @@
 from __future__ import absolute_import
 
 from unittest import TestCase
+import pytest
 from plotly.graph_objs import (
     Annotation,
     Annotations,
@@ -1934,3 +1935,80 @@ def test_if_passed_figure(self):
         fig2sp = subplots.make_subplots(2, 2)
         assert id(fig2sp) != id(figsp)
         assert fig2sp.layout == figsp.layout
+
+
+def test_make_subplots_spacing_error():
+    # check exception describing maximum value for horizontal_spacing or
+    # vertical_spacing is raised when spacing exceeds that value
+    for match in [
+        (
+            "^%s spacing cannot be greater than \(1 / \(%s - 1\)\) = %f."
+            % ("Vertical", "rows", 1.0 / 50.0)
+        ).replace(".", "\."),
+        "The resulting plot would have 51 rows \(rows=51\)\.$",
+    ]:
+        with pytest.raises(
+            ValueError, match=match,
+        ):
+            fig = subplots.make_subplots(51, 1, vertical_spacing=0.0201)
+    for match in [
+        (
+            "^%s spacing cannot be greater than \(1 / \(%s - 1\)\) = %f."
+            % ("Horizontal", "cols", 1.0 / 50.0)
+        ).replace(".", "\."),
+        "The resulting plot would have 51 columns \(cols=51\)\.$",
+    ]:
+        with pytest.raises(
+            ValueError, match=match,
+        ):
+            fig = subplots.make_subplots(1, 51, horizontal_spacing=0.0201)
+    # Check it's not raised when it's not beyond the maximum
+    try:
+        fig = subplots.make_subplots(51, 1, vertical_spacing=0.0200)
+    except ValueError:
+        # This shouldn't happen so we assert False to force failure
+        assert False
+    try:
+        fig = subplots.make_subplots(1, 51, horizontal_spacing=0.0200)
+    except ValueError:
+        # This shouldn't happen so we assert False to force failure
+        assert False
+    # make sure any value between 0 and 1 works for horizontal_spacing if cols is 1
+    try:
+        fig = subplots.make_subplots(1, 1, horizontal_spacing=0)
+    except ValueError:
+        # This shouldn't happen so we assert False to force failure
+        assert False
+    try:
+        fig = subplots.make_subplots(1, 1, horizontal_spacing=1)
+    except ValueError:
+        # This shouldn't happen so we assert False to force failure
+        assert False
+    # make sure any value between 0 and 1 works for horizontal_spacing if cols is 1
+    try:
+        fig = subplots.make_subplots(1, 1, horizontal_spacing=0)
+    except ValueError:
+        # This shouldn't happen so we assert False to force failure
+        assert False
+    # make sure any value between 0 and 1 works for horizontal_spacing if cols is 1
+    try:
+        fig = subplots.make_subplots(1, 1, horizontal_spacing=1)
+    except ValueError:
+        # This shouldn't happen so we assert False to force failure
+        assert False
+    with pytest.raises(
+        ValueError, match="^Horizontal spacing must be between 0 and 1\.$"
+    ):
+        fig = subplots.make_subplots(1, 1, horizontal_spacing=-0.01)
+    with pytest.raises(
+        ValueError, match="^Horizontal spacing must be between 0 and 1\.$"
+    ):
+        fig = subplots.make_subplots(1, 1, horizontal_spacing=1.01)
+    with pytest.raises(
+        ValueError, match="^Vertical spacing must be between 0 and 1\.$"
+    ):
+        fig = subplots.make_subplots(1, 1, vertical_spacing=-0.01)
+    with pytest.raises(
+        ValueError, match="^Vertical spacing must be between 0 and 1\.$"
+    ):
+        fig = subplots.make_subplots(1, 1, vertical_spacing=1.01)