Repository: bokeh/bokeh

Issue:
[BUG] AjaxDataSource-based glyphs appear incompatible with FactorRange
Bokeh 1.3.4
Python 3.7.2
MacOS and CentOS
Chrome 77.0.3865.90

Summary: Populating data for a categorical plot from an AjaxDataSource fails. Root cause appears to be an attempt to convert data to synthetic coordinates before the data has been retrieved.

Expected behavior: Categorical data can be retrieved with AjaxDataSource and then used as the source.

Observed behavior: Browser error: "Uncaught TypeError: Cannot read property 'length' of undefined
    at FactorRange.synthetic (VM16 bokeh-1.3.4.js:22760)".
This error occurs before the Ajax call finishes.

Example: Adapted from https://github.com/bokeh/bokeh/blob/1.3.4/examples/howto/ajax_source.py. Switching the source to a static ColumnDataSource produces the expected plot. 

```
import numpy as np
from flask import Flask, jsonify, make_response, request

from bokeh.plotting import figure, show
from bokeh.models import AjaxDataSource, CustomJS, ColumnDataSource

# Bokeh related code

adapter = CustomJS(code="""
    const result = {x: [], y: []}
    const pts = cb_data.response.points
    for (i=0; i<pts.length; i++) {
        result.x.push(pts[i][0])
        result.y.push(pts[i][1])
    }
    console.log(result)
    return result
""")

source = AjaxDataSource(data_url='http://localhost:5050/data', adapter=adapter)
# source = ColumnDataSource(data=dict(x=["a","b"], y=[1,2]))

p = figure(x_range=["a","b"], plot_height=300, plot_width=800, background_fill_color="lightgrey",
           title="Streaming Noisy sin(x) via Ajax")
p.circle('x', 'y', source=source)

# Flask related code

app = Flask(__name__)

def crossdomain(f):
    def wrapped_function(*args, **kwargs):
        resp = make_response(f(*args, **kwargs))
        h = resp.headers
        h['Access-Control-Allow-Origin'] = '*'
        h['Access-Control-Allow-Methods'] = "GET, OPTIONS, POST"
        h['Access-Control-Max-Age'] = str(21600)
        requested_headers = request.headers.get('Access-Control-Request-Headers')
        if requested_headers:
            h['Access-Control-Allow-Headers'] = requested_headers
        return resp
    return wrapped_function

x = ["a","b"]
y = [1,2]

@app.route('/data', methods=['GET', 'OPTIONS', 'POST'])
@crossdomain
def data():
    return jsonify(points=list(zip(x,y)))

# show and run

show(p)
app.run(port=5050)

```

#### Stack traceback and/or browser JavaScript console output

#### Screenshots or screencasts of the bug in action



Discussion:
@hypostulate you will need to "prime the pump" by providing a data dict with *empty* columns for the columns you need:
```
source = AjaxDataSource(data_url='http://localhost:5050/data', adapter=adapter, 
                        data=dict(x=[], y=[]))
```
We've tried to make this unnecessary where we can but I don't see what can be done about it in this specific scenario, so I would simply regard that as expected usage. If you would like to submit a PR to add a note about this need to be more explicit when used in conjunction with factor ranges to the `AjaxDataSource` docstring it would be appropriate and appreciated. 

Resolving patch:
diff --git a/bokeh/models/sources.py b/bokeh/models/sources.py
--- a/bokeh/models/sources.py
+++ b/bokeh/models/sources.py
@@ -786,6 +786,10 @@ class AjaxDataSource(RemoteSource):
     callback can be provided to convert the REST response into Bokeh format,
     via the ``adapter`` property of this data source.
 
+    Initial data can be set by specifying the ``data`` property directly.
+    This is necessary when used in conjunction with a ``FactorRange``, even
+    if the columns in `data`` are empty.
+
     A full example can be seen at :bokeh-tree:`examples/howto/ajax_source.py`
 
     '''


Test patch: