Repository: plotly/plotly.py
Pull request #885 (2017-11-24 21:50:30)

Subplot proportions
Want to add way to specify column proportions and row proportions in the domains of subplots, like `go.Table`.

(Note that this in service of a [Sparkline](https://plotly.slack.com/files/U07MQ8SJV/F7Z4B2M16/image.png) FigureFactory I'm working on)

So far I've added `column_width` as another accepted kwargs for `plotly.tools.make_subplots()`. The idea is that you specify a ratio of the columns (eg. [4, 7, 7, 2]) and the domains are changed proportionally.

cc @etpinard since you originally wrote most of this code
@chriddyp any comments would be helpful

note: I changed a couple of the tests as some of the domain values were off by the last floating point due to changing the number of +'s and x's in computing the main grid. Personally I don't think it's much to worry about.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7bafcf57..0a0cdb762 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 All notable changes to this project will be documented in this file.
 This project adheres to [Semantic Versioning](http://semver.org/).
 
+## [Unreleased] - TBA
+### Added
+-`column_width` and `row_width` parameters for `plotly.tools.make_subplots`. Call `help(plotly.tools.make_subplots)` for documentation.
+
 ## [2.2.2] - 2017-11-23
 ### Added
 - bullet chart figure factory. Call `help(plotly.figure_factory.create_bullet)` for examples and how to get started making bullet charts with the API.
diff --git a/plotly/tools.py b/plotly/tools.py
index ba6aae75c..b24c39183 100644
--- a/plotly/tools.py
+++ b/plotly/tools.py
@@ -772,6 +772,32 @@ def make_subplots(rows=1, cols=1,
                   in fraction of cell height
             * h (float or 'to_end', default='to_end') inset height
                   in fraction of cell height ('to_end': to cell top edge)
+
+    column_width (kwarg, list of numbers)
+        Column_width specifications
+
+        - Functions similarly to `column_width` of `plotly.graph_objs.Table`.
+          Specify a list that contains numbers where the amount of numbers in
+          the list is equal to `cols`.
+
+        - The numbers in the list indicate the proportions that each column
+          domains take across the full horizontal domain excluding padding.
+
+        - For example, if columns_width=[3, 1], horizontal_spacing=0, and
+          cols=2, the domains for each column would be [0. 0.75] and [0.75, 1]
+
+    row_width (kwargs, list of numbers)
+        Row_width specifications
+
+        - Functions similarly to `column_width`. Specify a list that contains
+          numbers where the amount of numbers in the list is equal to `rows`.
+
+        - The numbers in the list indicate the proportions that each row
+          domains take along the full vertical domain excluding padding.
+
+        - For example, if row_width=[3, 1], vertical_spacing=0, and
+          cols=2, the domains for each row from top to botton would be
+          [0. 0.75] and [0.75, 1]
     """
     # TODO: protected until #282
     from plotly.graph_objs import graph_objs
@@ -807,7 +833,8 @@ def make_subplots(rows=1, cols=1,
 
     # Throw exception if non-valid kwarg is sent
     VALID_KWARGS = ['horizontal_spacing', 'vertical_spacing',
-                    'specs', 'insets', 'subplot_titles']
+                    'specs', 'insets', 'subplot_titles', 'column_width',
+                    'row_width']
     for key in kwargs.keys():
         if key not in VALID_KWARGS:
             raise Exception("Invalid keyword argument: '{0}'".format(key))
@@ -907,9 +934,47 @@ def make_subplots(rows=1, cols=1,
         )
         _check_keys_and_fill('insets', insets, INSET_defaults)
 
-    # Set width & height of each subplot cell (excluding padding)
-    width = (1. - horizontal_spacing * (cols - 1)) / cols
-    height = (1. - vertical_spacing * (rows - 1)) / rows
+    # set heights (with 'column_width')
+    try:
+        column_width = kwargs['column_width']
+        if not isinstance(column_width, list) or len(column_width) != cols:
+            raise Exception(
+                "Keyword argument 'column_width' must be a list with {} "
+                "numbers in it, the number of subplot cols.".format(cols)
+            )
+    except KeyError:
+        column_width = None
+
+    if column_width:
+        cum_sum = float(sum(column_width))
+        widths = []
+        for w in column_width:
+            widths.append(
+                (1. - horizontal_spacing * (cols - 1)) * (w / cum_sum)
+            )
+    else:
+        widths = [(1. - horizontal_spacing * (cols - 1)) / cols] * cols
+
+    # set widths (with 'row_width')
+    try:
+        row_width = kwargs['row_width']
+        if not isinstance(row_width, list) or len(row_width) != rows:
+            raise Exception(
+                "Keyword argument 'row_width' must be a list with {} "
+                "numbers in it, the number of subplot rows.".format(rows)
+            )
+    except KeyError:
+        row_width = None
+
+    if row_width:
+        cum_sum = float(sum(row_width))
+        heights = []
+        for h in row_width:
+            heights.append(
+                (1. - vertical_spacing * (rows - 1)) * (h / cum_sum)
+            )
+    else:
+        heights = [(1. - vertical_spacing * (rows - 1)) / rows] * rows
 
     # Built row/col sequence using 'row_dir' and 'col_dir'
     COL_DIR = START_CELL['col_dir']
@@ -918,10 +983,14 @@ def make_subplots(rows=1, cols=1,
     row_seq = range(rows)[::ROW_DIR]
 
     # [grid] Build subplot grid (coord tuple of cell)
-    grid = [[((width + horizontal_spacing) * c,
-              (height + vertical_spacing) * r)
-            for c in col_seq]
-            for r in row_seq]
+    grid = [
+        [
+            (
+                (sum(widths[:c]) + c * horizontal_spacing),
+                (sum(heights[:r]) + r * vertical_spacing)
+            ) for c in col_seq
+        ] for r in row_seq
+    ]
 
     # [grid_ref] Initialize the grid and insets' axis-reference lists
     grid_ref = [[None for c in range(cols)] for r in range(rows)]
@@ -1040,16 +1109,16 @@ def make_subplots(rows=1, cols=1,
 
             # Get x domain using grid and colspan
             x_s = grid[r][c][0] + spec['l']
-            x_e = grid[r][c_spanned][0] + width - spec['r']
+            x_e = grid[r][c_spanned][0] + widths[c] - spec['r']
             x_domain = [x_s, x_e]
 
             # Get y domain (dep. on row_dir) using grid & r_spanned
             if ROW_DIR > 0:
                 y_s = grid[r][c][1] + spec['b']
-                y_e = grid[r_spanned][c][1] + height - spec['t']
+                y_e = grid[r_spanned][c][1] + heights[-1 - r] - spec['t']
             else:
                 y_s = grid[r_spanned][c][1] + spec['b']
-                y_e = grid[r][c][1] + height - spec['t']
+                y_e = grid[r][c][1] + heights[-1 - r] - spec['t']
             y_domain = [y_s, y_e]
 
             if spec['is_3d']:
@@ -1108,19 +1177,19 @@ def make_subplots(rows=1, cols=1,
                                 "Note: the starting cell is (1, 1)")
 
             # Get inset x domain using grid
-            x_s = grid[r][c][0] + inset['l'] * width
+            x_s = grid[r][c][0] + inset['l'] * widths[c]
             if inset['w'] == 'to_end':
-                x_e = grid[r][c][0] + width
+                x_e = grid[r][c][0] + widths[c]
             else:
-                x_e = x_s + inset['w'] * width
+                x_e = x_s + inset['w'] * widths[c]
             x_domain = [x_s, x_e]
 
             # Get inset y domain using grid
-            y_s = grid[r][c][1] + inset['b'] * height
+            y_s = grid[r][c][1] + inset['b'] * heights[-1 - r]
             if inset['h'] == 'to_end':
-                y_e = grid[r][c][1] + height
+                y_e = grid[r][c][1] + heights[-1 - r]
             else:
-                y_e = y_s + inset['h'] * height
+                y_e = y_s + inset['h'] * heights[-1 - r]
             y_domain = [y_s, y_e]
 
             if inset['is_3d']:


Test patch:
diff --git a/plotly/tests/test_core/test_tools/test_make_subplots.py b/plotly/tests/test_core/test_tools/test_make_subplots.py
index 69722667f..70916540b 100644
--- a/plotly/tests/test_core/test_tools/test_make_subplots.py
+++ b/plotly/tests/test_core/test_tools/test_make_subplots.py
@@ -83,6 +83,22 @@ class TestMakeSubplots(TestCase):
         with self.assertRaises(Exception):
             tls.make_subplots(insets=([{'cell': (1, 0)}]))
 
+    def test_column_width_not_list(self):
+        with self.assertRaises(Exception):
+            tls.make_subplots(rows=2, cols=2, column_width='not gonna work')
+
+    def test_column_width_not_list_of_correct_numbers(self):
+        with self.assertRaises(Exception):
+            tls.make_subplots(rows=2, cols=2, column_width=[0])
+
+    def test_row_width_not_list(self):
+        with self.assertRaises(Exception):
+            tls.make_subplots(rows=2, cols=2, row_width='not gonna work')
+
+    def test_row_width_not_list_of_correct_numbers(self):
+        with self.assertRaises(Exception):
+            tls.make_subplots(rows=2, cols=2, row_width=[1])
+
     def test_single_plot(self):
         expected = Figure(
             data=Data(),
@@ -193,7 +209,7 @@ class TestMakeSubplots(TestCase):
                     anchor='y12'
                 ),
                 xaxis13=XAxis(
-                    domain=[0.7346938775510203, 0.8530612244897958],
+                    domain=[0.7346938775510204, 0.8530612244897959],
                     anchor='y13'
                 ),
                 xaxis14=XAxis(
@@ -225,7 +241,7 @@ class TestMakeSubplots(TestCase):
                     anchor='y2'
                 ),
                 xaxis20=XAxis(
-                    domain=[0.7346938775510203, 0.8530612244897958],
+                    domain=[0.7346938775510204, 0.8530612244897959],
                     anchor='y20'
                 ),
                 xaxis21=XAxis(
@@ -253,7 +269,7 @@ class TestMakeSubplots(TestCase):
                     anchor='y26'
                 ),
                 xaxis27=XAxis(
-                    domain=[0.7346938775510203, 0.8530612244897958],
+                    domain=[0.7346938775510204, 0.8530612244897959],
                     anchor='y27'
                 ),
                 xaxis28=XAxis(
@@ -273,7 +289,7 @@ class TestMakeSubplots(TestCase):
                     anchor='y5'
                 ),
                 xaxis6=XAxis(
-                    domain=[0.7346938775510203, 0.8530612244897958],
+                    domain=[0.7346938775510204, 0.8530612244897959],
                     anchor='y6'
                 ),
                 xaxis7=XAxis(
@@ -289,7 +305,7 @@ class TestMakeSubplots(TestCase):
                     anchor='y9'
                 ),
                 yaxis1=YAxis(
-                    domain=[0.8062499999999999, 0.9999999999999999],
+                    domain=[0.80625, 1.0],
                     anchor='x1'
                 ),
                 yaxis10=YAxis(
@@ -333,7 +349,7 @@ class TestMakeSubplots(TestCase):
                     anchor='x19'
                 ),
                 yaxis2=YAxis(
-                    domain=[0.8062499999999999, 0.9999999999999999],
+                    domain=[0.80625, 1.0],
                     anchor='x2'
                 ),
                 yaxis20=YAxis(
@@ -373,23 +389,23 @@ class TestMakeSubplots(TestCase):
                     anchor='x28'
                 ),
                 yaxis3=YAxis(
-                    domain=[0.8062499999999999, 0.9999999999999999],
+                    domain=[0.80625, 1.0],
                     anchor='x3'
                 ),
                 yaxis4=YAxis(
-                    domain=[0.8062499999999999, 0.9999999999999999],
+                    domain=[0.80625, 1.0],
                     anchor='x4'
                 ),
                 yaxis5=YAxis(
-                    domain=[0.8062499999999999, 0.9999999999999999],
+                    domain=[0.80625, 1.0],
                     anchor='x5'
                 ),
                 yaxis6=YAxis(
-                    domain=[0.8062499999999999, 0.9999999999999999],
+                    domain=[0.80625, 1.0],
                     anchor='x6'
                 ),
                 yaxis7=YAxis(
-                    domain=[0.8062499999999999, 0.9999999999999999],
+                    domain=[0.80625, 1.0],
                     anchor='x7'
                 ),
                 yaxis8=YAxis(
@@ -426,7 +442,7 @@ class TestMakeSubplots(TestCase):
                     anchor='y12'
                 ),
                 xaxis13=XAxis(
-                    domain=[0.7346938775510203, 0.8530612244897958],
+                    domain=[0.7346938775510204, 0.8530612244897959],
                     anchor='y13'
                 ),
                 xaxis14=XAxis(
@@ -458,7 +474,7 @@ class TestMakeSubplots(TestCase):
                     anchor='y2'
                 ),
                 xaxis20=XAxis(
-                    domain=[0.7346938775510203, 0.8530612244897958],
+                    domain=[0.7346938775510204, 0.8530612244897959],
                     anchor='y20'
                 ),
                 xaxis21=XAxis(
@@ -486,7 +502,7 @@ class TestMakeSubplots(TestCase):
                     anchor='y26'
                 ),
                 xaxis27=XAxis(
-                    domain=[0.7346938775510203, 0.8530612244897958],
+                    domain=[0.7346938775510204, 0.8530612244897959],
                     anchor='y27'
                 ),
                 xaxis28=XAxis(
@@ -506,7 +522,7 @@ class TestMakeSubplots(TestCase):
                     anchor='y5'
                 ),
                 xaxis6=XAxis(
-                    domain=[0.7346938775510203, 0.8530612244897958],
+                    domain=[0.7346938775510204, 0.8530612244897959],
                     anchor='y6'
                 ),
                 xaxis7=XAxis(
@@ -578,31 +594,31 @@ class TestMakeSubplots(TestCase):
                     anchor='x21'
                 ),
                 yaxis22=YAxis(
-                    domain=[0.8062499999999999, 0.9999999999999999],
+                    domain=[0.80625, 1.0],
                     anchor='x22'
                 ),
                 yaxis23=YAxis(
-                    domain=[0.8062499999999999, 0.9999999999999999],
+                    domain=[0.80625, 1.0],
                     anchor='x23'
                 ),
                 yaxis24=YAxis(
-                    domain=[0.8062499999999999, 0.9999999999999999],
+                    domain=[0.80625, 1.0],
                     anchor='x24'
                 ),
                 yaxis25=YAxis(
-                    domain=[0.8062499999999999, 0.9999999999999999],
+                    domain=[0.80625, 1.0],
                     anchor='x25'
                 ),
                 yaxis26=YAxis(
-                    domain=[0.8062499999999999, 0.9999999999999999],
+                    domain=[0.80625, 1.0],
                     anchor='x26'
                 ),
                 yaxis27=YAxis(
-                    domain=[0.8062499999999999, 0.9999999999999999],
+                    domain=[0.80625, 1.0],
                     anchor='x27'
                 ),
                 yaxis28=YAxis(
-                    domain=[0.8062499999999999, 0.9999999999999999],
+                    domain=[0.80625, 1.0],
                     anchor='x28'
                 ),
                 yaxis3=YAxis(
@@ -1308,7 +1324,7 @@ class TestMakeSubplots(TestCase):
                 xaxis4=XAxis(
                     domain=[0.55, 1.0],
                     anchor='free',
-                    position=0.636
+                    position=0.6359999999999999
                 ),
                 xaxis5=XAxis(
                     domain=[0.0, 0.45],
@@ -1337,7 +1353,7 @@ class TestMakeSubplots(TestCase):
                     anchor='x1'
                 ),
                 yaxis2=YAxis(
-                    domain=[0.636, 0.788],
+                    domain=[0.6359999999999999, 0.7879999999999999],
                     anchor='x3'
                 ),
                 yaxis3=YAxis(