Repository: bokeh/bokeh

Issue:
Confusing multi_line error message
I got this error message (“RuntimeError: 
Supplying a user-defined data source AND iterable values to glyph methods is
not possibe. Either: Pass all data directly as literals: p.circe(x=a_list, y=an_array, ...)
Or, put all data in a ColumnDataSource and pass column names: source = ColumnDataSource(data=dict(x=a_list, y=an_array)) p.circe(x='x', y='y', source=source, ...)”) when I tried to run the code below. 

However, the error message doesn't seem to be helpful. The problem is from line_color. I should have defined the line color in the ColumnDataSource and then referenced the line color in the multi_line function. So it would be helpful if the error message points out the line_color issue. Also, there is a typo 'circe' in the error message.  

Thanks! 



**Code with the error message:**
```
ys = [row[1].as_matrix() for row in DF.iloc[:,1:].iterrows()]
num_lines = len(DF)
xs = [DF.columns[1:].values]*num_lines

source = ColumnDataSource(data=dict(
    x = xs,
    y=  ys
))

p = figure(
           x_range = DF.columns[1:].values.tolist(),
           plot_width=600, plot_height=300)
p.multi_line(
    xs='x', 
    ys='y',
    source=source,
    line_color=Viridis256[0:num_lines],
    line_width=2
)
show(p)

```



**Correct code:**
```
ys = [row[1].as_matrix() for row in DF.iloc[:,1:].iterrows()]
num_lines = len(DF)
xs = [DF.columns[1:].values]*num_lines

source = ColumnDataSource(data=dict(
    x = xs,
    y=  ys,
    color = Viridis256[0:num_lines]
))

p = figure(
           x_range = DF.columns[1:].values.tolist(),
           plot_width=600, plot_height=300)
p.multi_line(
    xs='x', 
    ys='y',
    source=source,
    line_color='color',
    line_width=2
)
show(p)
```


Discussion:
@syhooper The error message is just meant to be illustrative of the *kind* of usage that is correct, which is also why the "..." are there. I'm not sure what else or more we can do here though if you have a specific suggestion for different wording, please share it so we can consider it.
I can't quite see how to interpret the "...".  Why does line_color have to be a column name here, while line_width doesn't?  Is the problem that Viridis256 is a list, and it would have been fine as "red"?  If so maybe the error message could say that all properties provided either need to be scalars (like "red") or else strings specifying a column in the CDS?
> I can't quite see how to interpret the "..."

I'm genuinely surprised by this. What other meaning could there be besides "extra irrelevant stuff has been omitted"?

> all properties provided either need to be scalars (like "red") or else strings specifying a column in the CDS?

That's not a true statement. This situation does not apply to all properties, only to DataSpecs, which can either be single scalar values, or columns of data. And then, when providing any columns of data for a DataSpec property, the situation is:

Either: 

* All values have to be literal sequences passed to the glyph function, with no `source` given, OR
* All values have to be string names that reference a column in a CDS, and a `source` has to be given

You can't mix and match the two possibilities. i.e. *"Supplying a user-defined data source AND iterable values to glyph methods is not possible."* The current message is an attempt to balance giving enough information to act vs veering off into the weeds with a 15 line message about details most users won't be familiar with. 

I guess, not to put to fine a point on it, I disagree that the message is not helpful, since in this case it led the user to the correct code, which is all it needs to do.

But again, happy to consider concrete suggestions for different wording. 
Actually, it didn't!  Sophia and I both stared at that message, and we agreed that we had done everything it said, yet it was still complaining.  I certainly had no idea that "Viridis256[0:num_lines]" was the culprit. I think Philipp helped her figure that out, and I suggested she file this issue because we both found the current message was *not* pointing us in the right direction.  So I'm giving feedback that the current message did not make it clear what to do, at least not to me. I think if there were an example of some optional value that also needs to be updated, then it would be clear to look for those.
>  I think if there were an example of some optional value that also needs to be updated, then it would be clear to look for those.

I'm not really sure what you are suggesting. What I would appreciate is a concrete sentence or paragraph, actual specific text, to replace what is there. I think the current message is good, so I am definitely not qualified to author a replacement. 

Edit: 

>  I certainly had no idea that "Viridis256[0:num_lines]" was the culprit. 

Can you elaborate? The message says it's not possible to mix list literals, and column names. `"x"` is a column name, and `Viridis256[0:num_lines]` is a list. I'm not trying to be obtuse. I genuinely don't know how to make this message better than it is, which is why I am trying to solicit concrete suggestions for replacements, rather than general suggestions for how it might be improved. 
Personally I also find the message informative, but that's probably because I was aware of this restriction and was therefore immediately able to point out the issue to @syhooper. Would it not be possible to specifically point out the dataspecs that were constructed incorrectly, i.e. leave the current error as an illustrative example and then add something like:

```
Expected xs and line_color to reference fields in the data source.
```
> that were constructed incorrectly

That assumes the correct solution is to put everything in the source that is passed, but not passing a source at all could also be an option. 
"Expected xs and line_color to reference fields in the data source (or else do not pass a source)"?
```
In [1]: from bokeh.plotting import figure, ColumnDataSource
   ...: p = figure()
   ...: source = ColumnDataSource(data=dict(x=[1,2,3]))
   ...: p.circle('x', [4,5,5], line_color=["red", "green", "blue"], source=source)
y True
line_color True
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-1-5197055b149c> in <module>
      2 p = figure()
      3 source = ColumnDataSource(data=dict(x=[1,2,3]))
----> 4 p.circle('x', [4,5,5], line_color=["red", "green", "blue"], source=source)

fakesource in circle(self, x, y, **kwargs)

~/work/bokeh/bokeh/plotting/helpers.py in func(self, **kwargs)
    756         incompatible_literal_spec_values += _process_sequence_literals(glyphclass, glyph_ca, source, is_user_source)
    757         if incompatible_literal_spec_values:
--> 758             raise RuntimeError(_GLYPH_SOURCE_MSG % nice_join(incompatible_literal_spec_values, conjuction="and"))
    759
    760         # handle the nonselection glyph, we always set one

RuntimeError:

Expected y and line_color to reference fields in the supplied data source.

When a 'source' argument is passed to a glyph method, values that are sequences
(like lists or arrays) must come from references to data columns in the source.

For instance, as an example:

    source = ColumnDataSource(data=dict(x=a_list, y=an_array))

    p.circle(x='x', y='y', source=source, ...) # pass column names and a source

Alternatively, *all* data sequences amy be provided as literals as long as a
source is *not* provided:

    p.circle(x=a_list, y=an_array, ...)  # pass actual sequences and no source

```

@syhooper @jbednar @philippjfr ?
Perfect, after amy->may.  Thanks for seeing this through.
Looks perfect to me.
Awesome! Thanks so much!

Resolving patch:
diff --git a/bokeh/plotting/helpers.py b/bokeh/plotting/helpers.py
--- a/bokeh/plotting/helpers.py
+++ b/bokeh/plotting/helpers.py
@@ -256,37 +256,47 @@ def _get_legend_item_label(kwargs):
 
 
 _GLYPH_SOURCE_MSG = """
-Supplying a user-defined data source AND iterable values to glyph methods is
-not possibe. Either:
 
-Pass all data directly as literals:
+Expected %s to reference fields in the supplied data source.
 
-    p.circe(x=a_list, y=an_array, ...)
+When a 'source' argument is passed to a glyph method, values that are sequences
+(like lists or arrays) must come from references to data columns in the source.
 
-Or, put all data in a ColumnDataSource and pass column names:
+For instance, as an example:
 
     source = ColumnDataSource(data=dict(x=a_list, y=an_array))
-    p.circe(x='x', y='y', source=source, ...)
+
+    p.circle(x='x', y='y', source=source, ...) # pass column names and a source
+
+Alternatively, *all* data sequences may be provided as literals as long as a
+source is *not* provided:
+
+    p.circle(x=a_list, y=an_array, ...)  # pass actual sequences and no source
 
 """
 
 
 def _process_sequence_literals(glyphclass, kwargs, source, is_user_source):
+    incompatible_literal_spec_values = []
     dataspecs = glyphclass.dataspecs_with_props()
     for var, val in kwargs.items():
 
         # ignore things that are not iterable
         if not isinstance(val, Iterable):
             continue
+
         # pass dicts (i.e., values or fields) on as-is
         if isinstance(val, dict):
             continue
+
         # let any non-dataspecs do their own validation (e.g., line_dash properties)
         if var not in dataspecs:
             continue
+
         # strings sequences are handled by the dataspec as-is
         if isinstance(val, string_types):
             continue
+
         # similarly colorspecs handle color tuple sequences as-is
         if (isinstance(dataspecs[var].property, ColorSpec) and isinstance(val, tuple)):
             continue
@@ -295,10 +305,12 @@ def _process_sequence_literals(glyphclass, kwargs, source, is_user_source):
             raise RuntimeError("Columns need to be 1D (%s is not)" % var)
 
         if is_user_source:
-            raise RuntimeError(_GLYPH_SOURCE_MSG)
+            incompatible_literal_spec_values.append(var)
+        else:
+            source.add(val, name=var)
+            kwargs[var] = var
 
-        source.add(val, name=var)
-        kwargs[var] = var
+    return incompatible_literal_spec_values
 
 
 def _make_glyph(glyphclass, kws, extra):
@@ -738,8 +750,11 @@ def func(self, **kwargs):
 
         # handle the main glyph, need to process literals
         glyph_ca = _pop_colors_and_alpha(glyphclass, kwargs)
-        _process_sequence_literals(glyphclass, kwargs, source, is_user_source)
-        _process_sequence_literals(glyphclass, glyph_ca, source, is_user_source)
+        incompatible_literal_spec_values = []
+        incompatible_literal_spec_values += _process_sequence_literals(glyphclass, kwargs, source, is_user_source)
+        incompatible_literal_spec_values += _process_sequence_literals(glyphclass, glyph_ca, source, is_user_source)
+        if incompatible_literal_spec_values:
+            raise RuntimeError(_GLYPH_SOURCE_MSG % nice_join(incompatible_literal_spec_values, conjuction="and"))
 
         # handle the nonselection glyph, we always set one
         nsglyph_ca = _pop_colors_and_alpha(glyphclass, kwargs, prefix='nonselection_', default_alpha=0.1)
diff --git a/bokeh/util/string.py b/bokeh/util/string.py
--- a/bokeh/util/string.py
+++ b/bokeh/util/string.py
@@ -79,13 +79,15 @@ def indent(text, n=2, ch=" "):
     padding = ch * n
     return "\n".join(padding+line for line in text.split("\n"))
 
-def nice_join(seq, sep=", "):
+def nice_join(seq, sep=", ", conjuction="or"):
     ''' Join together sequences of strings into English-friendly phrases using
     the conjunction ``or`` when appropriate.
 
     Args:
         seq (seq[str]) : a sequence of strings to nicely join
         sep (str, optional) : a sequence delimiter to use (default: ", ")
+        conjunction (str or None, optional) : a conjuction to use for the last
+            two items, or None to reproduce basic join behaviour (default: "or")
 
     Returns:
         a joined string
@@ -97,10 +99,10 @@ def nice_join(seq, sep=", "):
     '''
     seq = [str(x) for x in seq]
 
-    if len(seq) <= 1:
+    if len(seq) <= 1 or conjuction is None:
         return sep.join(seq)
     else:
-        return "%s or %s" % (sep.join(seq[:-1]), seq[-1])
+        return "%s %s %s" % (sep.join(seq[:-1]), conjuction, seq[-1])
 
 def snakify(name, sep='_'):
     ''' Convert CamelCase to snake_case. '''


Test patch:
diff --git a/bokeh/plotting/tests/test_figure.py b/bokeh/plotting/tests/test_figure.py
--- a/bokeh/plotting/tests/test_figure.py
+++ b/bokeh/plotting/tests/test_figure.py
@@ -1,4 +1,7 @@
 from __future__ import absolute_import
+
+import re
+
 import pytest
 
 from bokeh.core.enums import MarkerType
@@ -184,6 +187,20 @@ def test_columnsource_auto_conversion_from_pandas(self, pd):
         df = pd.DataFrame({'x': [1, 2, 3], 'y': [2, 3, 4]})
         p.circle(x='x', y='y', source=df)
 
+    def test_glyph_method_errors_on_sequence_literals_with_source(self):
+        p = bpf.figure()
+        source = ColumnDataSource({'x': [1, 2, 3], 'y': [2, 3, 4]})
+
+        with pytest.raises(RuntimeError, match=r"Expected y to reference fields in the supplied data source."):
+            p.circle(x='x', y=[1,2,3], source=source)
+        with pytest.raises(RuntimeError, match=r"Expected y and line_color to reference fields in the supplied data source."):
+            p.circle(x='x', y=[1,2,3], line_color=["red", "green", "blue"], source=source)
+        with pytest.raises(RuntimeError) as e:
+            p.circle(x='x', y=[1,2,3], color=["red", "green", "blue"], source=source)
+        m = re.search (r"Expected y, (.+) and (.+) to reference fields in the supplied data source.", str(e.value))
+        assert m is not None
+        assert set(m.groups()) == set(["fill_color", "line_color"])
+
 class TestMarkers(object):
 
     @pytest.mark.parametrize('marker', list(MarkerType))
diff --git a/bokeh/util/tests/test_string.py b/bokeh/util/tests/test_string.py
--- a/bokeh/util/tests/test_string.py
+++ b/bokeh/util/tests/test_string.py
@@ -59,6 +59,32 @@ def test_with_n(self):
     def test_with_ch(self):
         assert bus.indent(self.TEXT, ch="-") == "--some text\n--to indent\n--  goes here"
 
+class Test_nice_join(object):
+
+    def test_default(self):
+        assert bus.nice_join(["one"]) == "one"
+        assert bus.nice_join(["one", "two"]) == "one or two"
+        assert bus.nice_join(["one", "two", "three"]) == "one, two or three"
+        assert bus.nice_join(["one", "two", "three", "four"]) == "one, two, three or four"
+
+    def test_string_conjunction(self):
+        assert bus.nice_join(["one"], conjuction="and") == "one"
+        assert bus.nice_join(["one", "two"], conjuction="and") == "one and two"
+        assert bus.nice_join(["one", "two", "three"], conjuction="and") == "one, two and three"
+        assert bus.nice_join(["one", "two", "three", "four"], conjuction="and") == "one, two, three and four"
+
+    def test_None_conjunction(self):
+        assert bus.nice_join(["one"], conjuction=None) == "one"
+        assert bus.nice_join(["one", "two"], conjuction=None) == "one, two"
+        assert bus.nice_join(["one", "two", "three"], conjuction=None) == "one, two, three"
+        assert bus.nice_join(["one", "two", "three", "four"], conjuction=None) == "one, two, three, four"
+
+    def test_sep(self):
+        assert bus.nice_join(["one"], sep='; ') == "one"
+        assert bus.nice_join(["one", "two"], sep='; ') == "one or two"
+        assert bus.nice_join(["one", "two", "three"], sep='; ') == "one; two or three"
+        assert bus.nice_join(["one", "two", "three", "four"], sep="; ") == "one; two; three or four"
+
 def test_snakify():
     assert bus.snakify("MyClassName") == "my_class_name"
     assert bus.snakify("My1Class23Name456") == "my1_class23_name456"