Repository: bokeh/bokeh

Issue:
state and check min phantomjs version
Hello!
I have an issue with export_png function.

Using Jupyter Notebook (v.5.0.0-8734a01), running on Python 3.6.2 |Anaconda custom (64-bit)|, Bokeh (v.0.12.13).

When I run this code:
```
#Langevin vs. Nose-Hoover
p = figure(x_axis_label='Radius of compression, nm', y_axis_label='Pressure, atm', 
           plot_width=800, plot_height=600,
           x_range = (9.6, 20.4), y_range = (-20, 520))
plot = output_file('lan_nh.html', title="lan_nh")
p.xaxis.axis_label_text_font_size = "22pt"
p.xaxis.major_label_text_font_size = "18pt"
p.yaxis.axis_label_text_font_size = "22pt"
p.yaxis.major_label_text_font_size = "18pt"


files = ['VelocityVerlet_Langevin/dsDNA_4000bp_mM50/push/0.1/dsDNA_4000bp_mM50_pressure_push.dat', 
         '4000bp_concentration/dsDNA_4000bp_mM50/push_10k_steps/dsDNA_4000bp_mM50_pressure_push.dat',
         '4000bp_concentration/dsDNA_4000bp_mM50/push_100k_steps/dsDNA_4000bp_mM50_pressure_push.dat']
labels = ['Langevin',
          'Nose-Hoover 10 M steps',
          'Nose-Hoover 100 M steps']
colors = inferno(len(labels)+1)

for fname,l,c in zip(files, labels, colors):
    fname = path + fname
    radius = [float(i.split('\t')[1].strip()) for i in open(fname)]
    pressure = [float(i.split('\t')[2].strip())*1.6*9.87 for i in open(fname)]
    p.line(radius, pressure, color = c, legend=l, line_width=2)
p.legend.label_text_font_size = "18pt"
show(p)
export_png(p, filename='/home/yagafarova/lan_nh.png')
```

I get this message:
```
{"errorMessage":"'undefined' is not an object (evaluating 'window.Bokeh.documents')","request":{"headers":{"Accept":"application/json","Accept-Encoding":"identity","Connection":"close","Content-Length":"349","Content-Type":"application/json;charset=UTF-8","Host":"127.0.0.1:45104","User-Agent":"Python-urllib/3.6"},"httpVersion":"1.1","method":"POST","post":"{\"script\": \"\\n// add private window prop to check that render is complete\\nwindow._bokeh_render_complete = false;\\nfunction done() {\\n  window._bokeh_render_complete = true;\\n}\\n\\nvar doc = window.Bokeh.documents[0];\\n\\nif (doc.is_idle)\\n  done();\\nelse\\n  doc.idle.connect(done);\\n\", \"args\": [], \"sessionId\": \"5992e420-0115-11e8-a323-57bffbd25446\"}","url":"/execute","urlParsed":{"anchor":"","query":"","file":"execute","directory":"/","path":"/execute","relative":"/execute","port":"","host":"","password":"","user":"","userInfo":"","authority":"","protocol":"","source":"/execute","queryKey":{},"chunks":["execute"]},"urlOriginal":"/session/5992e420-0115-11e8-a323-57bffbd25446/execute"}}
```

Does anyone know, how to solve it?


Discussion:
Do you have all the additional dependencies needed for export listed here:

https://bokeh.pydata.org/en/latest/docs/user_guide/export.html#additional-dependencies

installed? If so what versions?
> 'undefined' is not an object (evaluating 'window.Bokeh.documents')"

I suspect that this is a secondary error to the real one that has something to do with phantomjs. @yagafarova, does this code work without `expor_png()` in any web browser? Unfortunately this code is incomplete, so I can't reproduce this myself.
Yep, I have:

phantomjs                 1.9.7
selenium                  3.0.2
pillow                    5.0.0
> phantomjs 1.9.7

That's pretty old. Try the most recent 2.1.1.
I believe another use had an issue with < 2.0 so we should probably update the docs to reflect that (this issue could be for that task)
Updating the package helped. Thanks!
Going to leave this open as a task to update the docs. 
Will also try to add a phantomjs version check so a clear runtime error can be issued. 

Resolving patch:
diff --git a/bokeh/util/dependencies.py b/bokeh/util/dependencies.py
--- a/bokeh/util/dependencies.py
+++ b/bokeh/util/dependencies.py
@@ -6,6 +6,8 @@
 import shutil
 from subprocess import Popen, PIPE
 
+from packaging.version import Version as V
+
 from ..settings import settings
 
 logger = logging.getLogger(__name__)
@@ -52,8 +54,17 @@ def import_required(mod_name, error_msg):
     except ImportError:
         raise RuntimeError(error_msg)
 
-def detect_phantomjs():
-    '''Detect if PhantomJS is avaiable in PATH.'''
+def detect_phantomjs(version='2.1'):
+    ''' Detect if PhantomJS is avaiable in PATH, at a minimum version.
+
+    Args:
+        version (str, optional) :
+            Required minimum version for PhantomJS (mostly for testing)
+
+    Returns:
+        str, path to PhantomJS
+
+    '''
     if settings.phantomjs_path() is not None:
         phantomjs_path = settings.phantomjs_path()
     else:
@@ -66,8 +77,18 @@ def detect_phantomjs():
     try:
         proc = Popen([phantomjs_path, "--version"], stdout=PIPE, stderr=PIPE)
         proc.wait()
+        out = proc.communicate()
+
+        if len(out[1]) > 0:
+            raise RuntimeError('Error encountered in PhantomJS detection: %r' % out[1].decode('utf8'))
+
+        required = V(version)
+        installed = V(out[0].decode('utf8'))
+        if installed < required:
+            raise RuntimeError('PhantomJS version to old. Version>=%s required, installed: %s' % (required, installed))
 
     except OSError:
         raise RuntimeError('PhantomJS is not present in PATH. Try "conda install phantomjs" or \
             "npm install -g phantomjs-prebuilt"')
+
     return phantomjs_path
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -92,6 +92,7 @@
     'python-dateutil >=2.1',
     'Jinja2 >=2.7',
     'numpy >=1.7.1',
+    'packaging >=16.8',
     'tornado >=4.3',
 ]
 


Test patch:
diff --git a/bokeh/util/tests/test_dependencies.py b/bokeh/util/tests/test_dependencies.py
--- a/bokeh/util/tests/test_dependencies.py
+++ b/bokeh/util/tests/test_dependencies.py
@@ -1,24 +1,38 @@
 import pytest
 import bokeh.util.dependencies as dep
 
-def test_optional_success():
-    assert dep.import_optional('sys') is not None
+class Test_detect_phantomjs(object):
 
-def test_optional_fail():
-    assert dep.import_optional('bleepbloop') is None
+    def test_detect_phantomjs_success(self):
+        assert dep.detect_phantomjs() is not None
 
-def test_required_success():
-    assert dep.import_required('sys', 'yep') is not None
+    def test_detect_phantomjs_bad_path(self, monkeypatch):
+        monkeypatch.setenv("BOKEH_PHANTOMJS_PATH", "bad_path")
+        with pytest.raises(RuntimeError):
+            dep.detect_phantomjs()
 
-def test_required_fail():
-    with pytest.raises(RuntimeError) as excinfo:
-        dep.import_required('bleepbloop', 'nope')
-    assert 'nope' in str(excinfo.value)
+    def test_detect_phantomjs_bad_version(self):
+        with pytest.raises(RuntimeError) as e:
+            dep.detect_phantomjs('10.1')
+        assert str(e).endswith("PhantomJS version to old. Version>=10.1 required, installed: 2.1.1")
 
-def test_detect_phantomjs_success():
-    assert dep.detect_phantomjs() is not None
+    def test_detect_phantomjs_default_required_version(self):
+        assert dep.detect_phantomjs.__defaults__ == ('2.1',)
 
-def test_detect_phantomjs_fail(monkeypatch):
-    monkeypatch.setenv("BOKEH_PHANTOMJS_PATH", "bad_path")
-    with pytest.raises(RuntimeError):
-        dep.detect_phantomjs()
+class Test_import_optional(object):
+
+    def test_success(self):
+        assert dep.import_optional('sys') is not None
+
+    def test_fail(self):
+        assert dep.import_optional('bleepbloop') is None
+
+class Test_import_required(object):
+
+    def test_success(self):
+        assert dep.import_required('sys', 'yep') is not None
+
+    def test_fail(self):
+        with pytest.raises(RuntimeError) as excinfo:
+            dep.import_required('bleepbloop', 'nope')
+        assert 'nope' in str(excinfo.value)