Repository: plotly/plotly.py

Issue:
autosize not working as expected in plotly.offline.plot
The height of plots stored in html via `plotly.offline.plot` is set to 450px when height is not specified (at least in my test).

Example:
```python
import numpy as np
import pandas as pd
import plotly
from plotly import graph_objs as go
from plotly.offline import plot

N = 1000
df = pd.DataFrame({
    't': np.random.random(N),
    'x': np.random.random(N),
    'y': np.random.random(N)
})

traces = [
    go.Scatter3d(
        x=df['x'],
        y=df['y'],
        z=df['t'],
        mode='markers',
        marker=dict(size=1)
    )
]

layout = go.Layout(
    autosize=True,
    scene=go.layout.Scene(
        xaxis=go.layout.scene.XAxis(title='X'),
        yaxis=go.layout.scene.YAxis(title='Y'),
        zaxis=go.layout.scene.ZAxis(title='Time'),
    )
)
fig = go.Figure(data=traces, layout=layout)

filename = f"demo.html"
plotly.offline.plot(fig, filename=filename, auto_open=False)
```

The picture below shows the entire browser page in `Firefox 66.0.3` on `macOS High Sierra 10.13.6`. The file looks the same in `Chrome 73.0.3683.103`. I am using `plotly 3.8.0` on `Python 3.7`.

<img width="2048" alt="Screen Shot 2019-04-16 at 12 04 42 pm" src="https://user-images.githubusercontent.com/1052500/56176933-249db700-6040-11e9-92bf-a1820213a13d.png">




Discussion:
I should add, the size is adjusted correctly when used in Jupyter notebook.
Thanks for the report @marcotama.  Do you know if this worked differently prior to 3.8.0?  I ask because the reponsive resizing logic was updated to use the Plotly.js `responsive` config flag (https://plot.ly/javascript/responsive-fluid-layout/) instead of custom callbacks.

@antoinerg is the `responsive` flag expected to resize the plot vertically as well as horizontally?

I'm having the same issue immediately after updating from `plotly 3.7.0`  to `plotly 3.8.0`, i suspect this is related to #3746
> @antoinerg is the responsive flag expected to resize the plot vertically as well as horizontally?

We're probably hitting a special case: if `plotly.js` is drawing into a DOM element with zero height, it will revert to its default value of 450.

My guess is this could be fixed by sizing the element in the produced HTML document. A good default could be to have it fill the viewport using CSS `width:100vw; height:100vh;`.
Thanks @antoinerg, that sounds very promising.  I'll take a look at the generated HTML and let you know if that's not enough for me to figure out what needs to be done :slightly_smiling_face: 
@jonmmease I confirm the problem was not present in 3.7.1.

Resolving patch:
diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py
--- a/plotly/io/_base_renderers.py
+++ b/plotly/io/_base_renderers.py
@@ -1,3 +1,4 @@
+from __future__ import absolute_import
 import base64
 import json
 import webbrowser
@@ -238,8 +239,8 @@ def __init__(self,
         self.global_init = global_init
         self.requirejs = requirejs
         self.full_html = full_html
-        self.post_script = post_script
         self.animation_opts = animation_opts
+        self.post_script = post_script
 
     def activate(self):
         if self.global_init:
@@ -310,15 +311,50 @@ def to_mimebundle(self, fig_dict):
             include_plotlyjs = True
             include_mathjax = 'cdn'
 
+        # build post script
+        post_script = ["""
+var gd = document.getElementById('{plot_id}');
+var x = new MutationObserver(function (mutations, observer) {{
+        var display = window.getComputedStyle(gd).display;
+        if (!display || display === 'none') {{
+            console.log([gd, 'removed!']);
+            Plotly.purge(gd);
+            observer.disconnect();
+        }}
+}});
+
+// Listen for the removal of the full notebook cells
+var notebookContainer = gd.closest('#notebook-container');
+if (notebookContainer) {{
+    x.observe(notebookContainer, {childList: true});
+}}
+
+// Listen for the clearing of the current output cell
+var outputEl = gd.closest('.output');
+if (outputEl) {{
+    x.observe(outputEl, {childList: true});
+}}
+"""]
+
+        # Add user defined post script
+        if self.post_script:
+            if not isinstance(self.post_script, (list, tuple)):
+                post_script.append(self.post_script)
+            else:
+                post_script.extend(self.post_script)
+
         html = to_html(
             fig_dict,
             config=self.config,
             auto_play=self.auto_play,
             include_plotlyjs=include_plotlyjs,
             include_mathjax=include_mathjax,
-            post_script=self.post_script,
+            post_script=post_script,
             full_html=self.full_html,
-            animation_opts=self.animation_opts)
+            animation_opts=self.animation_opts,
+            default_width='100%',
+            default_height=525,
+        )
 
         return {'text/html': html}
 
@@ -446,17 +482,16 @@ def to_mimebundle(self, fig_dict):
         # having iframe have its own scroll bar.
         iframe_buffer = 20
         layout = fig_dict.get('layout', {})
-        if 'width' in layout:
-            iframe_width = layout['width'] + iframe_buffer
+
+        if layout.get('width', False):
+            iframe_width = str(layout['width'] + iframe_buffer) + 'px'
         else:
-            layout['width'] = 700
-            iframe_width = layout['width'] + iframe_buffer
+            iframe_width = '100%'
 
-        if 'height' in layout:
+        if layout.get('height', False):
             iframe_height = layout['height'] + iframe_buffer
         else:
-            layout['height'] = 450
-            iframe_height = layout['height'] + iframe_buffer
+            iframe_height = str(525 + iframe_buffer) + 'px'
 
         # Build filename using ipython cell number
         ip = IPython.get_ipython()
@@ -477,11 +512,14 @@ def to_mimebundle(self, fig_dict):
                    auto_open=False,
                    post_script=self.post_script,
                    animation_opts=self.animation_opts,
+                   default_width='100%',
+                   default_height=525,
                    validate=False)
 
         # Build IFrame
         iframe_html = """\
 <iframe
+    scrolling="no"
     width="{width}"
     height="{height}"
     src="{src}"
@@ -579,16 +617,17 @@ def __init__(self,
         self.animation_opts = animation_opts
 
     def render(self, fig_dict):
-        renderer = HtmlRenderer(
-            connected=False,
-            full_html=True,
-            requirejs=False,
-            global_init=False,
+        from plotly.io import to_html
+        html = to_html(
+            fig_dict,
             config=self.config,
             auto_play=self.auto_play,
+            include_plotlyjs=True,
+            include_mathjax='cdn',
             post_script=self.post_script,
-            animation_opts=self.animation_opts)
-
-        bundle = renderer.to_mimebundle(fig_dict)
-        html = bundle['text/html']
+            full_html=True,
+            animation_opts=self.animation_opts,
+            default_width='100%',
+            default_height='100%',
+        )
         open_html_in_browser(html, self.using, self.new, self.autoraise)
diff --git a/plotly/io/_html.py b/plotly/io/_html.py
--- a/plotly/io/_html.py
+++ b/plotly/io/_html.py
@@ -31,6 +31,8 @@ def to_html(fig,
             post_script=None,
             full_html=True,
             animation_opts=None,
+            default_width='100%',
+            default_height='100%',
             validate=True):
     """
     Convert a figure to an HTML string representation.
@@ -93,10 +95,10 @@ def to_html(fig,
         If a string that ends in '.js', a script tag is included that
         references the specified path. This approach can be used to point the
         resulting HTML div string to an alternative CDN.
-    post_script: str or None (default None)
-        JavaScript snippet to be included in the resulting div just after
-        plot creation.  The string may include '{plot_id}' placeholders that
-        will then be replaced by the `id` of the div element that the
+    post_script: str or list or None (default None)
+        JavaScript snippet(s) to be included in the resulting div just after
+        plot creation.  The string(s) may include '{plot_id}' placeholders
+        that will then be replaced by the `id` of the div element that the
         plotly.js figure is associated with.  One application for this script
         is to install custom plotly.js event handlers.
     full_html: bool (default True)
@@ -109,6 +111,11 @@ def to_html(fig,
         https://github.com/plotly/plotly.js/blob/master/src/plots/animation_attributes.js
         for available options. Has no effect if the figure does not contain
         frames, or auto_play is False.
+    default_width, default_height: number or str (default '100%')
+        The default figure width/height to use if the provided figure does not
+        specify its own layout.width/layout.height property.  May be
+        specified in pixels as an integer (e.g. 500), or as a css width style
+        string (e.g. '500px', '100%').
     validate: bool (default True)
         True if the figure should be validated before being converted to
         JSON, False otherwise.
@@ -143,25 +150,47 @@ def to_html(fig,
     # ## Serialize figure config ##
     config = _get_jconfig(config)
 
-    # Check whether we should add responsive
-    layout_dict = fig_dict.get('layout', {})
-    if layout_dict.get('width', None) is None:
-        config.setdefault('responsive', True)
+    # Set responsive
+    config.setdefault('responsive', True)
 
     jconfig = json.dumps(config)
 
+    # Get div width/height
+    layout_dict = fig_dict.get('layout', {})
+    div_width = layout_dict.get('width', default_width)
+    div_height = layout_dict.get('height', default_height)
+
+    # Add 'px' suffix to numeric widths
+    try:
+        float(div_width)
+    except (ValueError, TypeError):
+        pass
+    else:
+        div_width = str(div_width) + 'px'
+
+    try:
+        float(div_height)
+    except (ValueError, TypeError):
+        pass
+    else:
+        div_height = str(div_height) + 'px'
+
     # ## Get platform URL ##
     plotly_platform_url = config.get('plotlyServerURL', 'https://plot.ly')
 
     # ## Build script body ##
     # This is the part that actually calls Plotly.js
+
+    # build post script snippet(s)
+    then_post_script = ''
     if post_script:
-        then_post_script = """.then(function(){{
+        if not isinstance(post_script, (list, tuple)):
+            post_script = [post_script]
+        for ps in post_script:
+            then_post_script += """.then(function(){{
                             {post_script}
                         }})""".format(
-            post_script=post_script.replace('{plot_id}', plotdivid))
-    else:
-        then_post_script = ''
+                post_script=ps.replace('{plot_id}', plotdivid))
 
     then_addframes = ''
     then_animate = ''
@@ -274,7 +303,8 @@ def to_html(fig,
 <div>
         {mathjax_script}
         {load_plotlyjs}
-            <div id="{id}" class="plotly-graph-div"></div>
+            <div id="{id}" class="plotly-graph-div" \
+style="height:{height}; width:{width};"></div>
             <script type="text/javascript">
                 {require_start}
                     window.PLOTLYENV=window.PLOTLYENV || {{}};
@@ -286,6 +316,8 @@ def to_html(fig,
         mathjax_script=mathjax_script,
         load_plotlyjs=load_plotlyjs,
         id=plotdivid,
+        width=div_width,
+        height=div_height,
         plotly_platform_url=plotly_platform_url,
         require_start=require_start,
         script=script,
@@ -313,6 +345,8 @@ def write_html(fig,
                full_html=True,
                animation_opts=None,
                validate=True,
+               default_width='100%',
+               default_height='100%',
                auto_open=False):
     """
     Write a figure to an HTML file representation
@@ -393,10 +427,10 @@ def write_html(fig,
         If a string that ends in '.js', a script tag is included that
         references the specified path. This approach can be used to point the
         resulting HTML div string to an alternative CDN.
-    post_script: str or None (default None)
-        JavaScript snippet to be included in the resulting div just after
-        plot creation.  The string may include '{plot_id}' placeholders that
-        will then be replaced by the `id` of the div element that the
+    post_script: str or list or None (default None)
+        JavaScript snippet(s) to be included in the resulting div just after
+        plot creation.  The string(s) may include '{plot_id}' placeholders
+        that will then be replaced by the `id` of the div element that the
         plotly.js figure is associated with.  One application for this script
         is to install custom plotly.js event handlers.
     full_html: bool (default True)
@@ -409,6 +443,11 @@ def write_html(fig,
         https://github.com/plotly/plotly.js/blob/master/src/plots/animation_attributes.js
         for available options. Has no effect if the figure does not contain
         frames, or auto_play is False.
+    default_width, default_height: number or str (default '100%')
+        The default figure width/height to use if the provided figure does not
+        specify its own layout.width/layout.height property.  May be
+        specified in pixels as an integer (e.g. 500), or as a css width style
+        string (e.g. '500px', '100%').
     validate: bool (default True)
         True if the figure should be validated before being converted to
         JSON, False otherwise.
@@ -430,8 +469,11 @@ def write_html(fig,
         include_mathjax=include_mathjax,
         post_script=post_script,
         full_html=full_html,
+        animation_opts=animation_opts,
+        default_width=default_width,
+        default_height=default_height,
         validate=validate,
-        animation_opts=animation_opts)
+    )
 
     # Check if file is a string
     file_is_str = isinstance(file, six.string_types)


Test patch:
diff --git a/plotly/tests/test_core/test_offline/test_offline.py b/plotly/tests/test_core/test_offline/test_offline.py
--- a/plotly/tests/test_core/test_offline/test_offline.py
+++ b/plotly/tests/test_core/test_offline/test_offline.py
@@ -35,10 +35,6 @@
     ]
 }
 
-
-resize_code_strings = ['"responsive": true']
-
-
 PLOTLYJS = plotly.offline.get_plotlyjs()
 
 plotly_config_script = """\
@@ -266,45 +262,6 @@ def test_div_output(self):
         self.assertNotIn('</html>', html)
         self.assertTrue(html.startswith('<div>') and html.endswith('</div>'))
 
-    def test_autoresizing(self):
-
-        # If width or height wasn't specified, then we add a window resizer
-        html = self._read_html(plotly.offline.plot(fig, auto_open=False))
-        for resize_code_string in resize_code_strings:
-            self.assertIn(resize_code_string, html)
-
-        # If width or height was specified, then we don't resize
-        html = self._read_html(plotly.offline.plot({
-            'data': fig['data'],
-            'layout': {
-                'width': 500, 'height': 500
-            }
-        }, auto_open=False))
-        for resize_code_string in resize_code_strings:
-            self.assertNotIn(resize_code_string, html)
-
-    def test_autoresizing_div(self):
-
-        # If width or height wasn't specified, then we add a window resizer
-        for include_plotlyjs in [True, False, 'cdn', 'directory']:
-            html = plotly.offline.plot(fig,
-                                       output_type='div',
-                                       include_plotlyjs=include_plotlyjs)
-
-            for resize_code_string in resize_code_strings:
-                self.assertIn(resize_code_string, html)
-
-        # If width or height was specified, then we don't resize
-        html = plotly.offline.plot({
-            'data': fig['data'],
-            'layout': {
-                'width': 500, 'height': 500
-            }
-        }, output_type='div')
-
-        for resize_code_string in resize_code_strings:
-            self.assertNotIn(resize_code_string, html)
-
     def test_config(self):
         config = dict(linkText='Plotly rocks!',
                       showLink=True,