Repository: bokeh/bokeh

Issue:
Proposal: remove need for output_file() and display in "native" window
I would like to discuss two related ideas:

1) Remove the need to require specifying an `output_file()`. Having to choose a name (and path) for an html file sits in the way of doing quick interactive plotting IMO. It feels like something of an export mechanism. 

1a) One simple solution is to automatically create an html file in a temp folder. We'd have to do a bit of management to clean up old html docs. 

1b) Alternatively, we could make the Python process serve the html file. This would require substantially more changes and design. This would lean towards making apps, not sure if this is an approach we want to support?.

2) The second proposal is to show the generated web page in a "native" window instead of adding a tab to a browser. I think this would give Bokeh a more interactive/native feel, similar to MPL. Of course, the "native" window would really be a web-browser in disguise, like [XUL](https://en.wikipedia.org/wiki/XUL) or [nwjs](http://nwjs.io/) or [Electron](http://electron.atom.io/). Flexx already has functionality to do this based on XUL (which is available anywhere where Firefox is installed), and Chrome. A similar approach should be possible based on Internet Explorer.



Discussion:

Resolving patch:
diff --git a/bokeh/io.py b/bokeh/io.py
--- a/bokeh/io.py
+++ b/bokeh/io.py
@@ -304,21 +304,24 @@ def _show_with_state(obj, state, browser, new):
     controller = browserlib.get_browser_controller(browser=browser)
 
     comms_handle = None
+    shown = False
 
     if state.notebook:
         comms_handle = _show_notebook_with_state(obj, state)
+        shown = True
 
     elif state.server_enabled:
         _show_server_with_state(obj, state, new, controller)
+        shown = True
 
-    if state.file:
+    if state.file or not shown:
         _show_file_with_state(obj, state, new, controller)
 
     return comms_handle
 
 def _show_file_with_state(obj, state, new, controller):
-    save(obj, state=state)
-    controller.open("file://" + os.path.abspath(state.file['filename']), new=_new_param[new])
+    filename = save(obj, state=state)
+    controller.open("file://" + filename, new=_new_param[new])
 
 def _show_notebook_with_state(obj, state):
     if state.server_enabled:
@@ -342,7 +345,9 @@ def save(obj, filename=None, resources=None, title=None, state=None, validate=Tr
 
     Will fall back to the default output state (or an explicitly provided
     :class:`State` object) for ``filename``, ``resources``, or ``title`` if they
-    are not provided.
+    are not provided. If the filename is not given and not provided via output state,
+    it is derived from the script name (e.g. ``/foo/myplot.py`` will create
+    ``/foo/myplot.html``)
 
     Args:
         obj (Document or model object) : a plot object to save
@@ -362,7 +367,7 @@ def save(obj, filename=None, resources=None, title=None, state=None, validate=Tr
         validate (bool, optional) : True to check integrity of the models
 
     Returns:
-        None
+        filename (str) : the filename where the HTML file is saved.
 
     Raises:
         RuntimeError
@@ -372,30 +377,56 @@ def save(obj, filename=None, resources=None, title=None, state=None, validate=Tr
         state = _state
 
     filename, resources, title = _get_save_args(state, filename, resources, title)
-
     _save_helper(obj, filename, resources, title, validate)
+    return os.path.abspath(filename)
+
+def _detect_filename(ext):
+    """ Detect filename from the name of the script being run. Returns
+    None if the script could not be found (e.g. interactive mode).
+    """
+    import inspect
+    from os.path import isfile, dirname, basename, splitext, join
+    from inspect import currentframe
+    
+    frame = inspect.currentframe()
+    while frame.f_back and frame.f_globals.get('name') != '__main__':
+        frame = frame.f_back
+    
+    filename = frame.f_globals.get('__file__')
+    if filename and isfile(filename):
+        name, _ = splitext(basename(filename))
+        return join(dirname(filename), name + "." + ext)
 
 def _get_save_args(state, filename, resources, title):
+    warn = True
 
     if filename is None and state.file:
         filename = state.file['filename']
 
-    if resources is None and state.file:
-        resources = state.file['resources']
-
-    if title is None and state.file:
-        title = state.file['title']
+    if filename is None:
+        warn = False
+        filename = _detect_filename("html")
 
     if filename is None:
-        raise RuntimeError("save() called but no filename was supplied and output_file(...) was never called, nothing saved")
+        raise RuntimeError("save() called but no filename was supplied or detected, and output_file(...) was never called, nothing saved")
+
+    if resources is None and state.file:
+        resources = state.file['resources']
 
     if resources is None:
-        warnings.warn("save() called but no resources were supplied and output_file(...) was never called, defaulting to resources.CDN")
+        if warn:
+            warnings.warn("save() called but no resources were supplied and output_file(...) was never called, defaulting to resources.CDN")
+
         from .resources import CDN
         resources = CDN
 
+    if title is None and state.file:
+        title = state.file['title']
+
     if title is None:
-        warnings.warn("save() called but no title was supplied and output_file(...) was never called, using default title 'Bokeh Plot'")
+        if warn:
+            warnings.warn("save() called but no title was supplied and output_file(...) was never called, using default title 'Bokeh Plot'")
+
         title = "Bokeh Plot"
 
     return filename, resources, title
diff --git a/examples/plotting/file/choropleth.py b/examples/plotting/file/choropleth.py
--- a/examples/plotting/file/choropleth.py
+++ b/examples/plotting/file/choropleth.py
@@ -1,4 +1,4 @@
-from bokeh.plotting import figure, show, output_file
+from bokeh.plotting import figure, save, show
 from bokeh.sampledata.us_counties import data as counties
 from bokeh.sampledata.us_states import data as states
 from bokeh.sampledata.unemployment import data as unemployment
@@ -37,6 +37,4 @@
 p.patches(state_xs, state_ys, fill_alpha=0.0,
           line_color="#884444", line_width=2, line_alpha=0.3)
 
-output_file("choropleth.html", title="choropleth.py example")
-
-show(p)
+show(p)  # Change to save(p) to save but not show the HTML file
diff --git a/examples/plotting/file/periodic.py b/examples/plotting/file/periodic.py
--- a/examples/plotting/file/periodic.py
+++ b/examples/plotting/file/periodic.py
@@ -1,6 +1,5 @@
-
 from bokeh.models import HoverTool, ColumnDataSource
-from bokeh.plotting import figure, show, output_file
+from bokeh.plotting import figure, save, show
 from bokeh.sampledata.periodic_table import elements
 
 romans = ["I", "II", "III", "IV", "V", "VI", "VII"]
@@ -82,6 +81,4 @@
     ("electronic configuration", "@electronic"),
 ]
 
-output_file("periodic.html", title="periodic.py example")
-
-show(p)
+show(p)  # Change to save(p) to save but not show the HTML file


Test patch:
diff --git a/bokeh/tests/test_io.py b/bokeh/tests/test_io.py
--- a/bokeh/tests/test_io.py
+++ b/bokeh/tests/test_io.py
@@ -332,15 +332,15 @@ def test(self, mock_save, mock_abspath):
         s = io.State()
         s.output_file("foo.html")
         controller = Mock()
-        mock_abspath.return_value = "abspath"
+        mock_save.return_value = "savepath"
 
         io._show_file_with_state("obj", s, "window", controller)
         self._check_func_called(mock_save, ("obj",), {"state": s})
-        self._check_func_called(controller.open, ("file://abspath",), {"new": 1})
+        self._check_func_called(controller.open, ("file://savepath",), {"new": 1})
 
         io._show_file_with_state("obj", s, "tab", controller)
         self._check_func_called(mock_save, ("obj",), {"state": s})
-        self._check_func_called(controller.open, ("file://abspath",), {"new": 2})
+        self._check_func_called(controller.open, ("file://savepath",), {"new": 2})
 
 class Test_ShowNotebookWithState(DefaultStateTester):
 
diff --git a/tests/examples/test_examples.py b/tests/examples/test_examples.py
--- a/tests/examples/test_examples.py
+++ b/tests/examples/test_examples.py
@@ -179,7 +179,7 @@ def _run_example(example, log_file):
     example_path = join(example_dir, example)
 
     code = """\
-filename = '%s'
+__file__ = filename = '%s'
 
 import random
 random.seed(1)