Repository: bokeh/bokeh

Issue:
Lasso select not working on patches
I tried to use lasso and box selection with patches, but was unable to get them. To understand the desired behavior try click selection instead. See example here ( http://nbviewer.ipython.org/github/jakirkham/bokeh_examples/blob/other_selects/Bokeh_Example_12.ipynb ). This example may be worth including, as well. See the discussion about a related goal here ( https://groups.google.com/a/continuum.io/forum/#!topic/bokeh/d1d37KkGyTs ).



Discussion:
Any thoughts on this? Would be nice to have lasso selection work on patches, but it seems it doesn't.

To provide a non-trivial example of how this would be applied, we use Bokeh to generate interactive HTML plots from the results of processed Calcium imaging data from different regions of the brain for researchers to share amongst themselves, post on webpages, and share via other publications. Here's an [example]( http://dudmanlab.org/html/HiContrastForJohn_proj.html ) of such a figure. Regions of Interests (ROIs) are drawn as patches on a projection of the raw data. Selections of each of these shows a corresponding trace in the neighboring plot.

While the shift+click selection of ROIs works fine in some cases, researchers often like to compare collections of ROIs in the same area to look at relative spiking times. In these cases, it would be very helpful to have the lasso tool work on patches. This would allow them to look at groups of ROIs easily. Also lasso selection can be very handy when trying to pick up some of the smaller ROIs. While lasso seems to work well on other objects like circles, it seems to still not work on patches, which is a bit unfortunate.

Is there any chance we can get lasso selection of patches fixed?
Well, it's not really a matter of fixing, exactly. It has yet to be implemented in the first place. The selection/hit-testing code paths were designed so that capability could be added incrementally, as there was demand for it. Additionally I was also somewhat reticent for things like `Patches` because there are "policy" type questions, and its not clear to me what the "right" answers are. E.g., box selection on a patch: does the entire patch have to be contained in the box? Or does any intersection at all count for a selection? (or maybe that's a configurable property of `Patches`)

Anyway, adding selection support for `Patches` just requires adding one method for each type of selection geometry you want to work:

* `_hit_rect` support hit-testing patches against boxes
*  `_hit_poly` support hit testing patches against polygons (i.e. lasso tool)

Since the changes are completely self-contained and isolated to individual methods, this is pretty much the ideal task for new contributors. 

The hit test functions accept a `geometry` which describes the hit-test geometry (e.g. a point, a rect, a polygon) and returns a result that provides the indices of which glyphs were "hit".  As an example, `Patches` already implements `_hit_point` so that patches can be selected by mouse click or finger tap:

```
  _hit_point: (geometry) ->
    [vx, vy] = [geometry.vx, geometry.vy]
    sx = @renderer.plot_view.canvas.vx_to_sx(vx)
    sy = @renderer.plot_view.canvas.vy_to_sy(vy)

    x = @renderer.xmapper.map_from_target(vx, true)
    y = @renderer.ymapper.map_from_target(vy, true)

    candidates = (x.i for x in @index.search({minX: x, minY: y, maxX: x, maxY: y}))

    hits = []
    for i in [0...candidates.length]
      idx = candidates[i]
      sxs = @renderer.sxss[idx]
      sys = @renderer.syss[idx]
      for j in [0...sxs.length]
        if hittest.point_in_poly(sx, sy, sxs[j], sys[j])
          hits.push(idx)

    result = hittest.create_hit_test_result()
    result['1d'].indices = hits
    return result
```
This function could be modified to implement patch-vs-rect hit testing or patch-vs-poly testing. To keep things simple to start, the `@index.seach` optimization could be skipped, and the method could just loop over all the patches testing every one for geometry collision.

A good first step would be to find some suitably-licensed JS or coffeescript for:

* rect vs poly collision detection
* poly vs poly collision detection

I'm assuming now that "any intersection at all counts" (not "must be completely contained inside") is the desired behavior. 
Just to be concrete the `geometry` objects for `rect` and `poly` look like:
```
    geometry = {
      type: 'rect'
      # view coordinates of the box
      vx0: vx0
      vx1: vx1
      vy0: vy0
      vy1: vy1
    }
```
and 
```
    geometry = {
      type: 'poly'
      # lists of view coordinates of the polygon
      vx: vx
      vy: vy
    }
```
where "view coordinates" are screen/pixel space coordinates, except the origin is at the lower left (instead of the upper left like canvas normally has)
> Well, it's not really a matter of fixing, exactly. It has yet to be implemented in the first place.

Fair enough. So feature request then. 😄

> Additionally I was also somewhat reticent for things like `Patches` because there are "policy" type questions, and its not clear to me what the "right" answers are. E.g., box selection on a patch: does the entire patch have to be contained in the box? Or does any intersection at all count for a selection? (or maybe that's a configurable property of `Patches`)

Yep, you're right. It gets a bit hairy.

> I'm assuming now that "any intersection at all counts" (not "must be completely contained inside") is the desired behavior.

I guess my naive first approach would be much simpler than even this. Basically check if the centroid of the patch is included in the selected region. It is admittedly not perfect, but it is a very approachable first step. Are the centroids for patches already stored?
However, if the collision approach is more desirable, here is an MIT-licensed [library]( https://github.com/jriecken/sat-js ) in JavaScript. 
Oh, that's an interesting idea. They are not stored, but are available to be computed:

https://github.com/bokeh/bokeh/blob/master/bokehjs/src/coffee/models/glyphs/patches.coffee#L146-L177

This is used to snap hover tooltips to the "center" when requested. 

Note, all of this gets more complicated when we start taking about patches with holes or patches with disjoint sub-pieces
I know this is two years old, but since it's recently been added to the short-term milestone, I thought I'd add that this feature would be pretty useful for exploratory geo-analysis where you could have a facet grid with linked-brushing of polygons. 

For example, a toy example could look like this: 
```
import numpy as np
from scipy.spatial import Voronoi, voronoi_plot_2d
import shapely
from shapely.geometry import Polygon
import geopandas as gpd
import matplotlib.pyplot as plt
import random

from bokeh.io import output_file, show
from bokeh.models import GeoJSONDataSource, CDSView, BooleanFilter
from bokeh.plotting import figure
from bokeh.transform import linear_cmap
from bokeh.palettes import Blues
from bokeh.layouts import gridplot

np.random.seed(1)
%matplotlib inline

#Create random polygons using Voronoi
points = np.random.random((50, 2))
vor = Voronoi(points)
fig = voronoi_plot_2d(vor)
plt.show()
```
![image](https://user-images.githubusercontent.com/4526926/48271550-79efef80-e434-11e8-9ecb-b1bbc8b53c56.png)
```
#Create a geo-dataframe with the polygons clipped to 0,0-1,1
lines = [
    shapely.geometry.LineString(vor.vertices[line])
    for line in vor.ridge_vertices
    if -1 not in line
]
polygons = [poly for poly in shapely.ops.polygonize(lines)]

bb = Polygon([(0,0), (0,1), (1,1), (1,0)])
df = gpd.GeoDataFrame(columns=['majority_status', 'majority_gender'], geometry=polygons)
df['geometry'] = df['geometry'].intersection(bb)
df.plot()
```
![image](https://user-images.githubusercontent.com/4526926/48271722-dbb05980-e434-11e8-9fad-479575685877.png)
```
#Add some facets
status = ['Single', 'Married']
gender = ['Female', 'Male']

df['majority_status'] = df['majority_status'].apply(lambda x:random.choice(status))
df['majority_gender'] = df['majority_gender'].apply(lambda x:random.choice(gender))
df['score'] = np.random.random((df.shape[0], 1))

def facet_tuples(df, col1, col2):
    t1 = (col1, df[col1].unique())
    t2 = (col2, df[col2].unique())
    
    return [(t1[0], a, t2[0], b) for a in t1[1] for b in t2[1]]

#Create Bokeh plots and layout
mapper = linear_cmap(field_name='score', palette=Blues[9], low=min(df.score), high=max(df.score))
geo_source = GeoJSONDataSource(geojson=df.to_json())  

def make_plot(bokeh_source, data, row_name, row_value, column_name, column_value):

    tools = ['wheel_zoom', 'hover', 'box_select', 'tap', 'reset']
    
    TOOLTIPS = [("score", "@score")]
        
    p = figure(match_aspect=True, width=500, height=300, title=f'{row_name}: {row_value}, {column_name}: {column_value}',
               tools=tools, tooltips=TOOLTIPS)
    
    p.title.align = 'center'
    p.xaxis.visible, p.xgrid.visible, p.yaxis.visible, p.ygrid.visible = (False,)*4

    #unfiltered background
    p.patches(xs='xs', ys='ys', line_color='black', line_width=0.2, color='gray', fill_alpha=0.2, source=bokeh_source)
    
    #filtered subset of data to show with color mapper
    booleans = [True if (row[row_name] == row_value) & (row[column_name] == column_value) else False for index, row in df.iterrows()]
    view = CDSView(source=bokeh_source, filters=[BooleanFilter(booleans)])
    
    p.patches(xs='xs', ys='ys', line_color='black', line_width=0.2, color=mapper, source=bokeh_source, view=view)
    
    return p

plots = []

for subset in facet_tuples(df, 'majority_status', 'majority_gender'):
    plots.append(make_plot(geo_source, df, *subset))

layout = gridplot(plots, ncols=2, toolbar_location='right')

show(layout)
```
![image](https://user-images.githubusercontent.com/4526926/48271951-68f3ae00-e435-11e8-9c5d-7f7639e89c36.png)

In this example, Tap Tool will work fine, with all 4 polygons highlighted, but it would be very nice to be able to lasso / box select whole regions of interest.
To @bryevdv  [comment request](https://discourse.bokeh.org/t/box-lasso-select-tool-with-patches/3713/2) :
A 'hit' should in my opinion be greedy and relate to any patch the selection rect / polygon / lasso comes in touch with - similar to Qgis. Autocad also behaves this way with one added functionallity: rectangular selections from left to right are 'greedy', i.e. they select any geometry they touch on. Right to left selections will only include geometries that are fully set within the selection tool's boundaries. This, I think, is a nice to have but I would start with the basic greedy behaviour.

I am no developer so no in a position of contributing to it's development - apologies - happy to help with any other UI input.

Resolving patch:
diff --git a/bokeh/models/glyphs.py b/bokeh/models/glyphs.py
--- a/bokeh/models/glyphs.py
+++ b/bokeh/models/glyphs.py
@@ -733,6 +733,10 @@ class MultiPolygons(Glyph):
     Modeled on geoJSON - the data for the ``MultiPolygons`` glyph is
     different in that the vector of values is not a vector of scalars.
     Rather, it is a "list of lists of lists of lists".
+
+    During box selection only multi-polygons entirely contained in the
+    selection box will be included.
+
     '''
 
     __example__ = "examples/reference/models/MultiPolygons.py"
@@ -864,6 +868,9 @@ class Patches(Glyph):
     The data for the ``Patches`` glyph is different in that the vector of
     values is not a vector of scalars. Rather, it is a "list of lists".
 
+    During box selection only patches entirely contained in the
+    selection box will be included.
+
     '''
 
     __example__ = "examples/reference/models/Patches.py"


Test patch:
diff --git a/bokehjs/test/unit/models/glyphs/index.ts b/bokehjs/test/unit/models/glyphs/index.ts
--- a/bokehjs/test/unit/models/glyphs/index.ts
+++ b/bokehjs/test/unit/models/glyphs/index.ts
@@ -2,5 +2,7 @@ import "./glyph"
 import "./image"
 import "./image_rgba"
 import "./image_url"
+import "./multi_polygons"
+import "./patches"
 import "./ray"
 import "./rect"
diff --git a/bokehjs/test/unit/models/glyphs/multi_polygons.ts b/bokehjs/test/unit/models/glyphs/multi_polygons.ts
new file mode 100644
--- /dev/null
+++ b/bokehjs/test/unit/models/glyphs/multi_polygons.ts
@@ -0,0 +1,50 @@
+import {expect} from "chai"
+
+import {create_glyph_view, set_scales} from "./glyph_utils"
+import {MultiPolygons, MultiPolygonsView} from "@bokehjs/models/glyphs/multi_polygons"
+import {Geometry} from "@bokehjs/core/geometry"
+import {assert} from "@bokehjs/core/util/assert"
+
+describe("Glyph (using  as a concrete Glyph)", () => {
+
+  describe("GlyphView", () => {
+    it("should hit test rects for containment", async () => {
+      const data = {xs: [[[[0, 10, 5]]], [[[5, 10, 10, 5]]]], ys: [[[[0, 0, 10]]], [[[10, 10, 20, 20]]]]}
+      const glyph = new MultiPolygons({
+        xs: {field: "xs"},
+        ys: {field: "ys"},
+      })
+
+      const glyph_view = (await create_glyph_view(glyph, data) as MultiPolygonsView)
+      set_scales(glyph_view, "linear")
+      glyph_view.map_data()
+
+      const geometry1: Geometry = { type: "rect", sx0: -1, sy0: 201, sx1: 21, sy1: 179}
+      const geometry2: Geometry = { type: "rect", sx0: 1,  sy0: 200, sx1: 20, sy1: 180}
+      const geometry3: Geometry = { type: "rect", sx0: 9,  sy0: 181, sx1: 21, sy1: 159}
+      const geometry4: Geometry = { type: "rect", sx0: 10, sy0: 180, sx1: 19, sy1: 165}
+      const geometry5: Geometry = { type: "rect", sx0: 5,  sy0: 190, sx1: 15, sy1: 170}
+      const geometry6: Geometry = { type: "rect", sx0: -1, sy0: 201, sx1: 21, sy1: 159}
+
+      const result1 = glyph_view.hit_test(geometry1)
+      const result2 = glyph_view.hit_test(geometry2)
+      const result3 = glyph_view.hit_test(geometry3)
+      const result4 = glyph_view.hit_test(geometry4)
+      const result5 = glyph_view.hit_test(geometry5)
+      const result6 = glyph_view.hit_test(geometry6)
+
+      assert(result1 != null)
+      expect(result1.indices).to.be.deep.equal([0])
+      assert(result2 != null)
+      expect(result2.indices).to.be.deep.equal([])
+      assert(result3 != null)
+      expect(result3.indices).to.be.deep.equal([1])
+      assert(result4 != null)
+      expect(result4.indices).to.be.deep.equal([])
+      assert(result5 != null)
+      expect(result5.indices).to.be.deep.equal([])
+      assert(result6 != null)
+      expect(result6.indices).to.be.deep.equal([0, 1])
+    })
+  })
+})
diff --git a/bokehjs/test/unit/models/glyphs/patches.ts b/bokehjs/test/unit/models/glyphs/patches.ts
new file mode 100644
--- /dev/null
+++ b/bokehjs/test/unit/models/glyphs/patches.ts
@@ -0,0 +1,50 @@
+import {expect} from "chai"
+
+import {create_glyph_view, set_scales} from "./glyph_utils"
+import {Patches, PatchesView} from "@bokehjs/models/glyphs/patches"
+import {Geometry} from "@bokehjs/core/geometry"
+import {assert} from "@bokehjs/core/util/assert"
+
+describe("Glyph (using  as a concrete Glyph)", () => {
+
+  describe("GlyphView", () => {
+    it("should hit test rects for containment", async () => {
+      const data = {xs: [[0, 10, 5], [5, 10, 10, 5]], ys: [[0, 0, 10], [10, 10, 20, 20]]}
+      const glyph = new Patches({
+        xs: {field: "xs"},
+        ys: {field: "ys"},
+      })
+
+      const glyph_view = (await create_glyph_view(glyph, data) as PatchesView)
+      set_scales(glyph_view, "linear")
+      glyph_view.map_data()
+
+      const geometry1: Geometry = { type: "rect", sx0: -1, sy0: 201, sx1: 21, sy1: 179}
+      const geometry2: Geometry = { type: "rect", sx0: 1,  sy0: 200, sx1: 20, sy1: 180}
+      const geometry3: Geometry = { type: "rect", sx0: 9,  sy0: 181, sx1: 21, sy1: 159}
+      const geometry4: Geometry = { type: "rect", sx0: 10, sy0: 180, sx1: 19, sy1: 165}
+      const geometry5: Geometry = { type: "rect", sx0: 5,  sy0: 190, sx1: 15, sy1: 170}
+      const geometry6: Geometry = { type: "rect", sx0: -1, sy0: 201, sx1: 21, sy1: 159}
+
+      const result1 = glyph_view.hit_test(geometry1)
+      const result2 = glyph_view.hit_test(geometry2)
+      const result3 = glyph_view.hit_test(geometry3)
+      const result4 = glyph_view.hit_test(geometry4)
+      const result5 = glyph_view.hit_test(geometry5)
+      const result6 = glyph_view.hit_test(geometry6)
+
+      assert(result1 != null)
+      expect(result1.indices).to.be.deep.equal([0])
+      assert(result2 != null)
+      expect(result2.indices).to.be.deep.equal([])
+      assert(result3 != null)
+      expect(result3.indices).to.be.deep.equal([1])
+      assert(result4 != null)
+      expect(result4.indices).to.be.deep.equal([])
+      assert(result5 != null)
+      expect(result5.indices).to.be.deep.equal([])
+      assert(result6 != null)
+      expect(result6.indices).to.be.deep.equal([0, 1])
+    })
+  })
+})