Repository: bokeh/bokeh

Issue:
Line plots jaggy when overplotting noisy data with `line_join='miter'` 
## versions
- bokeh: `0.12.16`
- python: 
  ```
  3.6.5 (default, Apr  5 2018, 23:36:52) 
  [GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.1)]
  ```
- OS: `Darwin Kernel Version 17.5.0: Fri Apr 13 19:32:32 PDT 2018; root:xnu-4570.51.2~1/RELEASE_X86_64`
- browser: Chrome `Version 66.0.3359.181 (Official Build) (64-bit)`

## Description of expected behavior and the observed behavior

When plotting noisy time series with low screen resolution but high sampling rate (overplotting), I get a sort of "noise amplification" effect, which is apparently linked to the way in which `line_join='miter'` is implemented.

## Complete, minimal, self-contained example code that reproduces the issue

```
from bokeh.plotting import figure
from bokeh.io import output_notebook, show
from bokeh.layouts import gridplot

output_notebook(verbose=True, notebook_type='jupyter')
import numpy as np

fs = 10**4
x = np.linspace(0, 1, num=fs)
# signal
y = x**2
# gaussian noise
wgn = np.random.randn(*x.shape)
# noisy signal, should be visually indistinguibile from original one
a = 1e-3
yn = y + a*wgn

p1 = figure(title="miter",plot_width=450, plot_height=250)
p2 = figure(title="bevel",plot_width=450, plot_height=250,
            x_range=p1.x_range, y_range=p1.y_range,)

p1.line(x, yn, line_join='miter')
p1.line(x, y, color='red')
p2.line(x, yn, line_join='bevel')
p2.line(x, y, color='red')

grid = gridplot([[p1,p2],])
show(grid)
```

I get the following images:

![bokeh_plot](https://user-images.githubusercontent.com/6403904/40886991-e5609730-6741-11e8-8e7b-70df88f4d4b1.png)
![bokeh_plot 1](https://user-images.githubusercontent.com/6403904/40886996-ec33c0f0-6741-11e8-9865-19e7963065da.png)

Here you can see that with `line_join='miter'` the noise appears much greater as it really is: the correct representation is obtained with `line_join='bevel'`. This effect disappears when zooming in:

![bokeh_plot 2](https://user-images.githubusercontent.com/6403904/40887046-bae0b85e-6742-11e8-9185-b5800db406d8.png)
![bokeh_plot 3](https://user-images.githubusercontent.com/6403904/40887047-c0bfdb38-6742-11e8-87c4-442928a9ec68.png)

## Discussion

I know that the correct way of representing data with high sample rate with respect to screen resolution should be via datashader and friends, however sometimes a simpler approach is more desirable (here I have about 20 data points per pixel, which should be acceptable, imho).

The bokeh default (`line_join='miter'`) gives a visually incorrect result, which was for me at a first glance incomprehensible until I traced it down to the miter joint thing.

I'm not sure if this is a bug or a feature (high frequency noise makes the miter joint overshoot the data envelope) but the result is surprising. May be the `line_join='bevel'` or `line_join='round'` should be the default.


Discussion:
Thanks for the detailed report. Bokeh does not do any actual rendering itself. This is purely a function of the browser's HTML canvas implementation. It may look different on different browsers or even different versions off the same  browser. Unfortunately that being the case there is nothing we can do if a browser itself is buggy in some way, except possibly remove the option. We could certainly consider a PR to change the default, we just need to validate how the other option performs under normal usage with common browsers. Would you be able to submit a small PR to evaluate? It would entail changing the line props mixin on the python side and on the bokehjs side. 
The other option is to leave it but provide documentation support that gives guidance for this usage scenario. 
@bryevdv thanks for the prompt reply.

Some thoughts:
1. Reading the HTML living standard at the [line styles paragraph](https://html.spec.whatwg.org/multipage/canvas.html#line-styles) it seems that this is a browser bug: the [`miterLimit`](https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-miterlimit) is not honoured in edge cases.
1. The bug is present also in Safari and Firefox.
1. For small line widths the difference between *miter* and *bevel* line join is almost irrelevant, so that choosing the latter as a default seems a sensible choice.

I will try to submit a PR, but not immediately, since I do not have much spare time and I'm pretty new to bokeh's core and the bokehjs javascript library.

Fortunately explicitly stating `line_join='bevel'` is an effective mitigation for now, so maybe resolving this issue is not so urgent.
I've noticed this problem in other cases, e.g. Bokeh-based [TriMesh plots](https://holoviews.org/reference/elements/bokeh/TriMesh.html) where lines meet at a narrow angle:

![image](https://user-images.githubusercontent.com/1695496/40922542-109b5c46-67d8-11e8-8980-4f75dd4ed99d.png)
![image](https://user-images.githubusercontent.com/1695496/40922558-1a46ed64-67d8-11e8-87bc-aaa01e535c05.png)

This [link from TeX](https://tex.stackexchange.com/questions/203090/why-is-a-path-overflowing-in-a-tikz-picture) seems to show the underlying problem well.  Mitered edges can look nicer when done properly, but given that they don't seem to be implemented with the appropriate limits in practice, I think the safest thing to do is to make bevel or round the default in Bokeh.
@jbednar do you want to submit a small PR? Probably better if someone in @bokeh/dev does so that the image diffs run. AFAICT the only changes are adding `default="bevel"` here:

https://github.com/bokeh/bokeh/blob/6fc249ad4432e8acac44fab2582d000dbc579ae6/bokeh/core/property_mixins.py#L101

And changing the default from `"miter"` to `"bevel"` here:

https://github.com/bokeh/bokeh/blob/9acd0ef3cf777b9c8a60e0af9f026d799831d16f/bokehjs/src/lib/core/property_mixins.ts#L73

Would also need a line in the release notes. 
Sure, I'll try that and see how it goes.
OK, I'll wait for the PR by @jbednar thanks.

Resolving patch:
diff --git a/bokeh/command/subcommands/serve.py b/bokeh/command/subcommands/serve.py
--- a/bokeh/command/subcommands/serve.py
+++ b/bokeh/command/subcommands/serve.py
@@ -467,7 +467,7 @@ class Serve(Subcommand):
         ('--num-procs', dict(
             metavar='N',
             action='store',
-            help="Number of worker processes for an app. Default to one. Using "
+            help="Number of worker processes for an app. Using "
                  "0 will autodetect number of cores (defaults to 1)",
             default=1,
             type=int,
diff --git a/bokeh/core/property_mixins.py b/bokeh/core/property_mixins.py
--- a/bokeh/core/property_mixins.py
+++ b/bokeh/core/property_mixins.py
@@ -98,7 +98,7 @@ class ScalarFillProps(HasProps):
 """
 
 class BaseLineProps(HasProps):
-    line_join = Enum(LineJoin, help="""
+    line_join = Enum(LineJoin, default='bevel', help="""
     How path segments should be joined together.
 
     Acceptable values are:


Test patch:
diff --git a/bokeh/command/subcommands/tests/test_serve.py b/bokeh/command/subcommands/tests/test_serve.py
--- a/bokeh/command/subcommands/tests/test_serve.py
+++ b/bokeh/command/subcommands/tests/test_serve.py
@@ -167,7 +167,7 @@ def test_args():
         ('--num-procs', dict(
              metavar='N',
              action='store',
-             help="Number of worker processes for an app. Default to one. Using "
+             help="Number of worker processes for an app. Using "
                   "0 will autodetect number of cores (defaults to 1)",
              default=1,
              type=int,
diff --git a/bokeh/models/tests/utils/property_utils.py b/bokeh/models/tests/utils/property_utils.py
--- a/bokeh/models/tests/utils/property_utils.py
+++ b/bokeh/models/tests/utils/property_utils.py
@@ -33,7 +33,7 @@ def check_line_properties(model, prefix="", line_color=Color.black, line_width=1
     assert getattr(model, prefix + "line_color") == line_color
     assert getattr(model, prefix + "line_width") == line_width
     assert getattr(model, prefix + "line_alpha") == line_alpha
-    assert getattr(model, prefix + "line_join") == LineJoin.miter
+    assert getattr(model, prefix + "line_join") == LineJoin.bevel
     assert getattr(model, prefix + "line_cap") == LineCap.butt
     assert getattr(model, prefix + "line_dash") == []
     assert getattr(model, prefix + "line_dash_offset") == 0