Repository: altair-viz/altair

Issue:
Multiple views in JupyterLab blank with default html renderer
To reproduce:

1. Create an environment with JupyterLab 1.2.6 and altair 4.0.1:

```
conda create -c conda-forge -yn testenv jupyterlab altair pandas
conda activate testenv
```

2. Start JupyterLab:
```
jupyter lab
```
3. Create a new notebook with an example plot:

```
import altair as alt
import numpy as np
import pandas as pd

# Compute x^2 + y^2 across a 2D grid
x, y = np.meshgrid(range(-5, 5), range(-5, 5))
z = x ** 2 + y ** 2

# Convert this grid to columnar data expected by Altair
source = pd.DataFrame({'x': x.ravel(),
                     'y': y.ravel(),
                     'z': z.ravel()})

alt.Chart(source).mark_rect().encode(
    x='x:O',
    y='y:O',
    color='z:Q'
)
```
4. Right-click on the plot and choose "Create New view for output" to open a new jlab tab for the plot. The new tab is blank:
<img width="622" alt="Screen Shot 2020-02-24 at 11 22 23 AM" src="https://user-images.githubusercontent.com/192614/75183729-f70da980-56f7-11ea-8e25-bf11bfda8b9f.png">

I also see the same issue if opening a new view of the notebook (right-clicking on the file's tab, selecting "New View for Notebook"

I think at least part of the problem is that the plot div has a hard-coded id, and ids should be unique on a page, so viewing the same output multiple times can be a problem. There may be other issues with the js not knowing to redraw the plot in the new jlab tab, etc.


Comments:

Patch:
diff --git a/altair/utils/deprecation.py b/altair/utils/deprecation.py
index 766ab212..099795e2 100644
--- a/altair/utils/deprecation.py
+++ b/altair/utils/deprecation.py
@@ -1,12 +1,25 @@
 import warnings
-# import functools
+import functools
 
 
 class AltairDeprecationWarning(UserWarning):
     pass
 
 
-def _deprecated(obj, name=None, message=None):
+def deprecated(message=None):
+    """Decorator to deprecate a function or class.
+
+    Parameters
+    ----------
+    message : string (optional)
+        The deprecation message
+    """
+    def wrapper(obj):
+        return _deprecate(obj, message=message)
+    return wrapper
+
+
+def _deprecate(obj, name=None, message=None):
     """Return a version of a class or function that raises a deprecation warning.
 
     Parameters
@@ -26,7 +39,7 @@ def _deprecated(obj, name=None, message=None):
     Examples
     --------
     >>> class Foo(object): pass
-    >>> OldFoo = _deprecated(Foo, "OldFoo")
+    >>> OldFoo = _deprecate(Foo, "OldFoo")
     >>> f = OldFoo()  # doctest: +SKIP
     AltairDeprecationWarning: alt.OldFoo is deprecated. Use alt.Foo instead.
     """
@@ -36,9 +49,9 @@ def _deprecated(obj, name=None, message=None):
     if isinstance(obj, type):
         return type(name, (obj,),
                     {'__doc__': obj.__doc__,
-                    '__init__': _deprecated(obj.__init__, "__init__", message)})
+                    '__init__': _deprecate(obj.__init__, "__init__", message)})
     elif callable(obj):
-        # @functools.wraps(obj)  # TODO: use this in Py3 only
+        @functools.wraps(obj)
         def new_obj(*args, **kwargs):
             warnings.warn(message, AltairDeprecationWarning)
             return obj(*args, **kwargs)
diff --git a/altair/utils/display.py b/altair/utils/display.py
index 8d7f4370..63b5d1cc 100644
--- a/altair/utils/display.py
+++ b/altair/utils/display.py
@@ -26,10 +26,10 @@ class RendererRegistry(PluginRegistry[RendererType]):
             See https://altair-viz.github.io/getting_started/installation.html
             for more information.
             """),
-        'vegascope': textwrap.dedent(
+        'altair_viewer': textwrap.dedent(
             """
-            To use the 'vegascope' renderer, you must install the vegascope
-            package; see http://github.com/diana-hep/vegascope/
+            To use the 'altair_viewer' renderer, you must install the altair_viewer
+            package; see http://github.com/altair-viz/altair_viewer/
             for more information.
             """),
     }
diff --git a/altair/utils/html.py b/altair/utils/html.py
index 39699c13..6848e8c5 100644
--- a/altair/utils/html.py
+++ b/altair/utils/html.py
@@ -85,7 +85,10 @@ HTML_TEMPLATE_UNIVERSAL = jinja2.Template("""
 <div id="{{ output_div }}"></div>
 <script type="text/javascript">
   (function(spec, embedOpt){
-    const outputDiv = document.getElementById("{{ output_div }}");
+    let outputDiv = document.currentScript.previousElementSibling;
+    if (outputDiv.id !== "{{ output_div }}") {
+      outputDiv = document.getElementById("{{ output_div }}");
+    }
     const paths = {
       "vega": "{{ base_url }}/vega@{{ vega_version }}?noext",
       "vega-lib": "{{ base_url }}/vega-lib?noext",
diff --git a/altair/vegalite/v3/_deprecated.py b/altair/vegalite/v3/_deprecated.py
index 5241d1ed..f25f7fbd 100644
--- a/altair/vegalite/v3/_deprecated.py
+++ b/altair/vegalite/v3/_deprecated.py
@@ -1,19 +1,19 @@
-from ...utils.deprecation import _deprecated
+from ...utils.deprecation import _deprecate
 from . import channels
 
 # Deprecated classes (see https://github.com/altair-viz/altair/issues/1474).
 # TODO: Remove these in Altair 3.2.
-Fillopacity = _deprecated(channels.FillOpacity, 'Fillopacity')
-FillopacityValue = _deprecated(channels.FillOpacityValue, 'FillopacityValue')
-Strokeopacity = _deprecated(channels.StrokeOpacity, 'Strokeopacity')
-StrokeopacityValue = _deprecated(channels.StrokeOpacityValue, 'StrokeopacityValue')
-Strokewidth = _deprecated(channels.StrokeWidth, 'Strokewidth')
-StrokewidthValue = _deprecated(channels.StrokeWidthValue, 'StrokewidthValue')
-Xerror = _deprecated(channels.XError, 'Xerror')
-XerrorValue = _deprecated(channels.XErrorValue, 'XerrorValue')
-Xerror2 = _deprecated(channels.XError2, 'Xerror2')
-Xerror2Value = _deprecated(channels.XError2Value, 'Xerror2Value')
-Yerror = _deprecated(channels.YError, 'Yerror')
-YerrorValue = _deprecated(channels.YErrorValue, 'YerrorValue')
-Yerror2 = _deprecated(channels.YError2, 'Yerror2')
-Yerror2Value = _deprecated(channels.YError2Value, 'Yerror2Value')
+Fillopacity = _deprecate(channels.FillOpacity, 'Fillopacity')
+FillopacityValue = _deprecate(channels.FillOpacityValue, 'FillopacityValue')
+Strokeopacity = _deprecate(channels.StrokeOpacity, 'Strokeopacity')
+StrokeopacityValue = _deprecate(channels.StrokeOpacityValue, 'StrokeopacityValue')
+Strokewidth = _deprecate(channels.StrokeWidth, 'Strokewidth')
+StrokewidthValue = _deprecate(channels.StrokeWidthValue, 'StrokewidthValue')
+Xerror = _deprecate(channels.XError, 'Xerror')
+XerrorValue = _deprecate(channels.XErrorValue, 'XerrorValue')
+Xerror2 = _deprecate(channels.XError2, 'Xerror2')
+Xerror2Value = _deprecate(channels.XError2Value, 'Xerror2Value')
+Yerror = _deprecate(channels.YError, 'Yerror')
+YerrorValue = _deprecate(channels.YErrorValue, 'YerrorValue')
+Yerror2 = _deprecate(channels.YError2, 'Yerror2')
+Yerror2Value = _deprecate(channels.YError2Value, 'Yerror2Value')
diff --git a/altair/vegalite/v4/api.py b/altair/vegalite/v4/api.py
index f99f605f..344ae16b 100644
--- a/altair/vegalite/v4/api.py
+++ b/altair/vegalite/v4/api.py
@@ -1498,6 +1498,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         else:
             display(self)
 
+    @utils.deprecation.deprecated(message="serve() is deprecated. Use show() instead.")
     def serve(self, ip='127.0.0.1', port=8888, n_retries=50, files=None,
               jupyter_warning=True, open_browser=True, http_server=None,
               **kwargs):
@@ -1538,6 +1539,27 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
               files=files, jupyter_warning=jupyter_warning,
               open_browser=open_browser, http_server=http_server)
 
+    def show(self, embed_opt=None, open_browser=None):
+        """Show the chart in an external browser window.
+
+        This requires a recent version of the altair_viewer package.
+
+        Parameters
+        ----------
+        embed_opt : dict (optional)
+            The Vega embed options that control the dispay of the chart.
+        open_browser : bool (optional)
+            Specify whether a browser window should be opened. If not specified,
+            a browser window will be opened only if the server is not already
+            connected to a browser.
+        """
+        try:
+            import altair_viewer
+        except ImportError:
+            raise ValueError("show() method requires the altair_viewer package. "
+                "See http://github.com/altair-viz/altair_viewer")
+        altair_viewer.show(self, embed_opt=embed_opt, open_browser=open_browser)
+
     @utils.use_signature(core.Resolve)
     def _set_resolve(self, **kwargs):
         """Copy the chart and update the resolve property with kwargs"""
diff --git a/doc/user_guide/display_frontends.rst b/doc/user_guide/display_frontends.rst
index 354aa5a8..364756de 100644
--- a/doc/user_guide/display_frontends.rst
+++ b/doc/user_guide/display_frontends.rst
@@ -141,46 +141,26 @@ Examples are:
 - The Hydrogen_ project, which is built on nteract_ and renders Altair charts
   via the ``mimebundle`` renderer.
 
-The Vegascope Renderer
-~~~~~~~~~~~~~~~~~~~~~~
-For other IDEs, a useful companion is the `VegaScope`_ project, which provides
-an Altair renderer that works directly from a Python terminal.
+Altair Viewer
+~~~~~~~~~~~~~
+For non-notebook IDEs, a useful companion is the `Altair Viewer`_ package,
+which provides an Altair renderer that works directly from any Python terminal.
 Start by installing the package::
 
-    $ pip install vegascope
+    $ pip install altair_viewer
 
-Now in your Python script you can enable the vegascope renderer::
+When enabled, this will serve charts via a local HTTP server and automatically open
+a browser window in which to view them, with subsequent charts displayed in the
+same window.
 
-    import altair as alt
-    alt.renderers.enable('vegascope')
-
-    # load a simple dataset as a pandas DataFrame
-    from vega_datasets import data
-    cars = data.cars()
+If you are using an IPython-compatible terminal ``altair_viewer`` can be enabled via
+Altair's standard renderer framework::
 
-    chart = alt.Chart(cars).mark_point().encode(
-        x='Horsepower',
-        y='Miles_per_Gallon',
-        color='Origin',
-    ).interactive()
-
-In an IPython environment, this will automatically trigger vegascope to serve
-the chart in a background process to your web browser, and unlike Altair's
-:meth:`Chart.serve` method, any subsequently created charts will use
-the same server.
-
-If you are in a non-IPython terminal, you can trigger the renderer manually
-using the :meth:`Chart.display` method::
-
-   chart.display()
-
-Built-in ``serve()`` method
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Altair includes a :meth:`Chart.serve` method which will seamlessly convert a
-chart to HTML, start a web server serving that HTML, and open your system's
-default web browser to view it.
+    import altair as alt
+    alt.renderers.enable('altair_viewer')
 
-For example, you can serve a chart to a web browser like this::
+If you prefer to manually trigger chart display, you can use the built-in :meth:`Chart.show`
+method to manually trigger chart display::
 
     import altair as alt
 
@@ -194,10 +174,10 @@ For example, you can serve a chart to a web browser like this::
         color='Origin',
     ).interactive()
 
-    chart.serve()
+    chart.show()
 
-The command will block the Python interpreter, and will have to be canceled with
-``Ctrl-C`` to execute any further code.
+This command will block the Python interpreter until the browser window containing
+the chart is closed.
 
 Manual ``save()`` and display
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -284,11 +264,11 @@ For an example, see `ipyvega`_.
 .. _nteract: https://nteract.io
 .. _nbconvert: https://nbconvert.readthedocs.io/
 .. _nbviewer: https://nbviewer.jupyter.org/
+.. _Altair Viewer: https://github.com/altair-viz/altair_viewer/
 .. _Colab: https://colab.research.google.com
 .. _Hydrogen: https://github.com/nteract/hydrogen
 .. _Jupyter Notebook: https://jupyter-notebook.readthedocs.io/en/stable/
 .. _Vega-Lite: http://vega.github.io/vega-lite
 .. _Vega: https://vega.github.io/vega/
-.. _VegaScope: https://github.com/scikit-hep/vegascope
 .. _VSCode-Python: https://code.visualstudio.com/docs/python/python-tutorial
 .. _Zeppelin: https://zeppelin.apache.org/
\ No newline at end of file
diff --git a/doc/user_guide/faq.rst b/doc/user_guide/faq.rst
index d8a54f20..3ce5224c 100644
--- a/doc/user_guide/faq.rst
+++ b/doc/user_guide/faq.rst
@@ -15,12 +15,12 @@ altair to an environment capable of executing the javascript code that
 turns the JSON specification into a visual chart.
 
 There are extensions included in JupyterLab, Jupyter Notebook, Colab,
-Kaggle kernels, Hydrogen, and nteract that know how to automatically perform
-this rendering (see :ref:`installation` for details).
+Kaggle kernels, VSCode, Hydrogen, and nteract that know how to automatically
+perform this rendering (see :ref:`installation` for details).
 
 For other frontends that don't have vega-lite rendering built-in, it is
-possible to work with Altair charts using either the ``vegascope`` project,
-or the build-in :meth:`Chart.serve` or :meth:`Chart.save` methods.
+possible to work with Altair charts using the build-in :meth:`Chart.show`
+or :meth:`Chart.save` methods.
 For more information on these, see :ref:`display-general`.
 
 .. _faq-no-display:


Test patch:
diff --git a/altair/utils/tests/test_deprecation.py b/altair/utils/tests/test_deprecation.py
index 035d6087..4fac9112 100644
--- a/altair/utils/tests/test_deprecation.py
+++ b/altair/utils/tests/test_deprecation.py
@@ -2,12 +2,22 @@ import pytest
 
 import altair as alt
 from altair.utils import AltairDeprecationWarning
-from altair.utils.deprecation import _deprecated
+from altair.utils.deprecation import _deprecate, deprecated
 
 
 def test_deprecated_class():
-    OldChart = _deprecated(alt.Chart, "OldChart")
+    OldChart = _deprecate(alt.Chart, "OldChart")
     with pytest.warns(AltairDeprecationWarning) as record:
         OldChart()
     assert "alt.OldChart" in record[0].message.args[0]
-    assert "alt.Chart" in record[0].message.args[0]
\ No newline at end of file
+    assert "alt.Chart" in record[0].message.args[0]
+
+
+def test_deprecation_decorator():
+    @deprecated(message="func is deprecated")
+    def func(x):
+        return x + 1
+    with pytest.warns(AltairDeprecationWarning) as record:
+        y = func(1)
+    assert y == 2
+    assert record[0].message.args[0] == "func is deprecated"