Repository: plotly/plotly.py

Issue:
fig.set_subplots
If we could call `set_subplots` on a pre-existing, sufficiently-empty figure, we could avoid a separate import just for `make_subplots` and have a smaller entry-point footprint .


Discussion:

Resolving patch:
diff --git a/packages/python/plotly/plotly/basedatatypes.py b/packages/python/plotly/plotly/basedatatypes.py
--- a/packages/python/plotly/plotly/basedatatypes.py
+++ b/packages/python/plotly/plotly/basedatatypes.py
@@ -14,6 +14,7 @@
 from .optional_imports import get_module
 
 from . import shapeannotation
+from . import subplots
 
 # Create Undefined sentinel value
 #   - Setting a property to None removes any existing value
@@ -3926,6 +3927,22 @@ def _subplot_not_empty(self, xref, yref, selector="all"):
                 )
         return ret
 
+    def set_subplots(self, rows=None, cols=None, **make_subplots_args):
+        """
+        Add subplots to this figure. If the figure already contains subplots,
+        then this throws an error. Accepts any keyword arguments that
+        plotly.subplots.make_subplots accepts.
+        """
+        # rows, cols provided so that this can be called like
+        # fig.set_subplots(2,3), say
+        if rows is not None:
+            make_subplots_args["rows"] = rows
+        if cols is not None:
+            make_subplots_args["cols"] = cols
+        if self._has_subplots():
+            raise ValueError("This figure already has subplots.")
+        return subplots.make_subplots(figure=self, **make_subplots_args)
+
 
 class BasePlotlyType(object):
     """
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
@@ -60,6 +60,7 @@ def make_subplots(
     row_titles=None,
     x_title=None,
     y_title=None,
+    figure=None,
     **kwargs
 ):
     """
@@ -226,7 +227,15 @@ def make_subplots(
 
     y_title: str or None (default None)
         Title to place to the left of the left column of subplots,
-       centered vertically
+        centered vertically
+
+    figure: go.Figure or None (default None)
+        If None, a new go.Figure instance will be created and its axes will be
+        populated with those corresponding to the requested subplot geometry and
+        this new figure will be returned.
+        If a go.Figure instance, the axes will be added to the
+        layout of this figure and this figure will be returned. If the figure
+        already contains axes, they will be overwritten.
 
     Examples
     --------
@@ -809,13 +818,15 @@ def _checks(item, defaults):
         print(grid_str)
 
     # Build resulting figure
-    fig = go.Figure(layout=layout)
+    if figure is None:
+        figure = go.Figure()
+    figure.update_layout(layout)
 
     # Attach subplot grid info to the figure
-    fig.__dict__["_grid_ref"] = grid_ref
-    fig.__dict__["_grid_str"] = grid_str
+    figure.__dict__["_grid_ref"] = grid_ref
+    figure.__dict__["_grid_str"] = grid_str
 
-    return fig
+    return figure
 
 
 def _configure_shared_axes(layout, grid_ref, specs, x_or_y, shared, row_dir):


Test patch:
diff --git a/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_figure.py b/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_figure.py
--- a/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_figure.py
+++ b/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_figure.py
@@ -1,7 +1,9 @@
 from __future__ import absolute_import
 
 import plotly.graph_objects as go
+from plotly.subplots import make_subplots
 from plotly.tests.utils import TestCaseNoTemplate
+import pytest
 
 
 class FigureTest(TestCaseNoTemplate):
@@ -199,3 +201,21 @@ def test_update_overwrite_data(self):
             fig.to_plotly_json()["data"],
             [{"type": "scatter", "y": [1, 3, 2], "line": {"color": "yellow"}}],
         )
+
+
+def test_set_subplots():
+    # Test that it works the same as make_subplots for a simple call
+    fig0 = go.Figure()
+    fig0_sp = make_subplots(2, 2)
+    fig0.set_subplots(2, 2)
+    assert fig0.layout == fig0_sp.layout
+    # Test that it accepts the same arguments as make_subplots
+    fig1 = go.Figure()
+    fig1.set_subplots(rows=2, cols=2, horizontal_spacing=0.25, vertical_spacing=0.1)
+    fig1_sp = make_subplots(
+        rows=2, cols=2, horizontal_spacing=0.25, vertical_spacing=0.1
+    )
+    assert fig1.layout == fig1_sp.layout
+    # Test that calling on a figure that already has subplots throws an error.
+    with pytest.raises(ValueError, match=r"^This figure already has subplots\.$"):
+        fig1.set_subplots(2, 3)
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
@@ -1924,3 +1924,13 @@ def test_secondary_y_subplots(self):
         expected.update_traces(uid=None)
 
         self.assertEqual(fig.to_plotly_json(), expected.to_plotly_json())
+
+    def test_if_passed_figure(self):
+        # assert it returns the same figure it was passed
+        fig = Figure()
+        figsp = subplots.make_subplots(2, 2, figure=fig)
+        assert id(fig) == id(figsp)
+        # assert the layout is the same when it returns its own figure
+        fig2sp = subplots.make_subplots(2, 2)
+        assert id(fig2sp) != id(figsp)
+        assert fig2sp.layout == figsp.layout