Repository: bokeh/bokeh

Issue:
circle not circular
#### Version Information:

On Mac OS X 10.13.1 (High Sierra)
Python:  2.7.13 |Continuum Analytics, Inc.| (default, Dec 20 2016, 23:05:08) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)]
IPython:  5.3.0
Bokeh:  0.12.10

#### Problem: 

When using `p.circle(...)`, the rendered circle is not perfectly circular.  The `x` dimension seems to render according to the supplied radius argument, but the `y` dimension is visibly short of the specified radius.

#### To reproduce:

The following code creates the screenshot below:

```
    from bokeh.plotting import figure, output_file, show

    output_file("circle_v1.html")

    p = figure(plot_width=400, plot_height=400, match_aspect=True, tools='save')
    p.circle(0, 0, radius=1.0, fill_alpha=0)
    p.line((0, 1), (0, 0))
    p.line((0, -1), (0, 0))
    p.line((0, 0), (0, 1))
    p.line((0, 0), (0, -1))
    #p.rect(0, 0, 2, 2, fill_alpha=0)

    show(p)
```
![bokeh_plot 9](https://user-images.githubusercontent.com/110926/32842217-16f6f878-c9e2-11e7-9e54-9101dd44ae6b.png)

Note: When you uncomment the line beginning with `#p.rect(...` the dimensions of the rendered circle seem fine.  e.g.:

![bokeh_plot 10](https://user-images.githubusercontent.com/110926/32842365-7eb76150-c9e2-11e7-8991-414e1cbbb844.png)

Best,

Travis



Discussion:
> When you uncomment the line beginning with #p.rect(... the dimensions of the rendered circle seem fine.

That's just a coincidence that comes from this particular setup of ranges.
> That's just a coincidence that comes from this particular setup of ranges.

`match_aspect` with data ranges should work in both cases. 

Yeah, I thought that `match_aspect` does something different (acts on frame dimensions).
I think perhaps some glyphs are not reporting their bounds very well, causing `match_aspect` to do the right thing with the wrong information
To be clear, I see the same behavior with or without the match_aspect argument.  (You may already know that ;) )


Perhaps the following observation is also of some help: as soon as you manually adjust the x or y range either by `p=figure(x_range=Range1D())` or `p.x_range.start=-1` and `p.x_range.end=1` it will overwrite what `match_aspect` does. 

I'm still having trouble to understand why `circle` and `line` glyphs align differently on the same axis coordinate system. Even if the data aspect ratio and the screen aspect ratio vary independently, shouldn't it be the same for all glyphs?
@mansenfranzen that is entirely expected. Aspect matching *only* applies to auto ranging a with DataRange1d. If you manually and explicitly override things, Bokeh assumes you know what you are doing and honors the values you set. 

As for the second, do you mean in general? It's because the low level canvas always draws *actual circles* regardless of the aspect ratio (i.e. nothing automatically squeezes it in to an ellipse,which is what a purely mathematical viewpoint would dictate). There is much discussion in old issues please refer to them. 
@bryevdv Thanks for the clarification. I finally got the point that the low level canvas draws circles irrespective of the aspect ratio. It explains the different behavior between `rect` and `circle`. I'm curious to know how other plotting projects employing the low level canvas deal with such challenges. Do you have any experiences on that?
>  I'm curious to know how other plotting projects employing the low level canvas deal with such challenges. Do you have any experiences on that?

I was a bit cursory in my response. It is in fact possible to apply a scale transform to get the canvas to draw ellipses according to aspect at a low level. So the question becomes: is it appropriate to do so? The vast majority of uses of circle are for scatter plots, in which you always want small, actual circles, regardless of aspect, and thus the answer is "probably not really, definitely not in general" The issue really only affects "big" circles, in which case there is `match_aspect` (modulo bugs like this) or the `ellipse` glyph  method should suffice. It's hard to justify adding more complexity to circle for configurable "auto-squashing", which is already the most complicated glyph by far (maybe a surprise but it has many applications and corner cases, this would complicate half a dozen hit testing code paths as an example). 
OK I think I know why this is happening. First note that the following code *does* work:
```
from bokeh.plotting import figure, output_file, show

output_file("circle_v1.html")

p = figure(plot_width=400, plot_height=400, match_aspect=True, tools='save')
p.circle(0, 0, radius=1.0, fill_alpha=0)
p.line((-1, 1), (0, 0))
p.line((0, 0), (1, -1))

show(p) 
```

<img width="349" alt="screen shot 2017-12-20 at 15 48 42" src="https://user-images.githubusercontent.com/1078448/34230312-4b93e880-e59d-11e7-8e78-72b80141511d.png">

The reason for this (I believe) is that the match aspect adjustment is being made too early. The plot canvas collects all the glyph reported bounds, then applies match aspect to all of them separately. Then *later* the data range unions all the reported bounds, but only the ones that are in it's renderers lists. @tvaught It just happens that by choosing to split the two "diameters" into four "radiuses" exposes the issue, because the match aspect is not happening on the aggregated bounds for all the lines. 

So the somewhat good news is that I think things will work in many common situations. The better news is that I think the solution is just to make `DataRange1d` responsible for applying aspect matching computations *after* union the culled bounds, which should not be too difficult (and better anyway)
OK I will push  PR soon. The changes make the original code work as expected. I do think there are some caveats, in particular I think use of `match_aspect` and linked data ranges in multiple plots is probably not going to result in well-defined behavior. At this point I will just add a caution about that since I think trying to make that work would be quite alot of work. 

Resolving patch:
diff --git a/bokeh/models/plots.py b/bokeh/models/plots.py
--- a/bokeh/models/plots.py
+++ b/bokeh/models/plots.py
@@ -656,6 +656,10 @@ def _scale(cls, scale):
         setting only sets the initial plot draw and subsequent resets. It is
         possible for tools (single axis zoom, unconstrained box zoom) to
         change the aspect ratio.
+
+    .. warning::
+        This setting is incompatible with linking dataranges across multiple
+        plots. Doing so may result in undefined behaviour.
     """)
 
     aspect_scale = Float(default=1, help="""


Test patch:
diff --git a/bokehjs/test/models/ranges/data_range1d.coffee b/bokehjs/test/models/ranges/data_range1d.coffee
--- a/bokehjs/test/models/ranges/data_range1d.coffee
+++ b/bokehjs/test/models/ranges/data_range1d.coffee
@@ -335,3 +335,26 @@ describe "datarange1d module", ->
       expect(spy.called).to.be.false
       r.start = 15
       expect(spy.calledOnce).to.be.true
+
+  describe "adjust_bounds_for_aspect", ->
+    it "should preserve y axis when it is larger", ->
+      r = new DataRange1d()
+
+      bds = {minX: 0, maxX: 1, minY: 0, maxY: 2}
+
+      bds = r.adjust_bounds_for_aspect(bds, 4)
+      expect(bds.minX).to.be.equal -3.5
+      expect(bds.maxX).to.be.equal 4.5
+      expect(bds.minY).to.be.equal 0
+      expect(bds.maxY).to.be.equal 2
+
+    it "should preserve x axis when it is larger", ->
+      r = new DataRange1d()
+
+      bds = {minX: 0, maxX: 8, minY: 0, maxY: 1}
+
+      bds = r.adjust_bounds_for_aspect(bds, 4)
+      expect(bds.minX).to.be.equal 0
+      expect(bds.maxX).to.be.equal 8
+      expect(bds.minY).to.be.equal -0.5
+      expect(bds.maxY).to.be.equal 1.5