Skip to content

Commit

Permalink
Add support for ColumnarDataSource.inspection_policy
Browse files Browse the repository at this point in the history
  • Loading branch information
mattpap committed Apr 12, 2024
1 parent 5e0fe2b commit 7608549
Show file tree
Hide file tree
Showing 19 changed files with 382 additions and 191 deletions.
71 changes: 43 additions & 28 deletions bokehjs/src/lib/core/selection_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {ColumnarDataSource} from "models/sources/columnar_data_source"
import type {DataRenderer, DataRendererView} from "models/renderers/data_renderer"
import type {GlyphRendererView} from "models/renderers/glyph_renderer"
import type {GraphRendererView} from "models/renderers/graph_renderer"
import {logger} from "core/logging"

// XXX: this is needed to cut circular dependency between this, models/renderers/* and models/sources/*
function is_GlyphRendererView(renderer_view: DataRendererView): renderer_view is GlyphRendererView {
Expand All @@ -19,49 +20,63 @@ export class SelectionManager {

inspectors: Map<DataRenderer, Selection> = new Map()

select(renderer_views: DataRendererView[], geometry: Geometry, final: boolean, mode: SelectionMode = "replace"): boolean {
// divide renderers into glyph_renderers or graph_renderers
select(renderer_views: DataRendererView[], geometry: Geometry, final: boolean = true, mode: SelectionMode = "replace"): boolean {
const glyph_renderer_views: GlyphRendererView[] = []
const graph_renderer_views: GraphRendererView[] = []
for (const r of renderer_views) {
if (is_GlyphRendererView(r)) {
glyph_renderer_views.push(r)
} else if (is_GraphRendererView(r)) {
graph_renderer_views.push(r)

for (const rv of renderer_views) {
if (is_GlyphRendererView(rv)) {
glyph_renderer_views.push(rv)
} else if (is_GraphRendererView(rv)) {
graph_renderer_views.push(rv)
} else {
logger.warn(`selection of ${rv.model} is not supported`)
}
}

let did_hit = false

// graph renderer case
for (const r of graph_renderer_views) {
const hit_test_result = r.model.selection_policy.hit_test(geometry, r)
did_hit = did_hit || r.model.selection_policy.do_selection(hit_test_result, r.model, final, mode)
}
// glyph renderers
if (glyph_renderer_views.length > 0) {
const hit_test_result = this.source.selection_policy.hit_test(geometry, glyph_renderer_views)
did_hit = did_hit || this.source.selection_policy.do_selection(hit_test_result, this.source, final, mode)
const {selection_policy} = this.source
const hit_test_result = selection_policy.hit_test(geometry, glyph_renderer_views)
did_hit ||= selection_policy.do_selection(hit_test_result, this.source, final, mode)
}

for (const rv of graph_renderer_views) {
const {selection_policy} = rv.model
const hit_test_result = selection_policy.hit_test(geometry, rv)
did_hit ||= selection_policy.do_selection(hit_test_result, rv.model, final, mode)
}

return did_hit
}

inspect(renderer_view: DataRendererView, geometry: Geometry): boolean {
let did_hit = false
inspect(renderer_views: DataRendererView[], geometry: Geometry, final: boolean = true, mode: SelectionMode = "replace"): boolean {
const glyph_renderer_views: GlyphRendererView[] = []
const graph_renderer_views: GraphRendererView[] = []

if (is_GlyphRendererView(renderer_view)) {
const hit_test_result = renderer_view.hit_test(geometry)
if (hit_test_result != null) {
did_hit = !hit_test_result.is_empty()
const inspection = this.get_or_create_inspector(renderer_view.model)
inspection.update(hit_test_result, true, "replace")
this.source.setv({inspected: inspection}, {silent: true})
this.source.inspect.emit([renderer_view.model, {geometry}])
for (const rv of renderer_views) {
if (is_GlyphRendererView(rv)) {
glyph_renderer_views.push(rv)
} else if (is_GraphRendererView(rv)) {
graph_renderer_views.push(rv)
} else {
logger.warn(`inspection of ${rv.model} is not supported`)
}
} else if (is_GraphRendererView(renderer_view)) {
const hit_test_result = renderer_view.model.inspection_policy.hit_test(geometry, renderer_view)
did_hit = renderer_view.model.inspection_policy.do_inspection(hit_test_result, geometry, renderer_view, false, "replace")
}

let did_hit = false

if (glyph_renderer_views.length > 0) {
const {inspection_policy} = this.source
const hit_test_result = inspection_policy.hit_test(geometry, glyph_renderer_views)
did_hit ||= inspection_policy.do_inspection(hit_test_result, this.source, final, mode, glyph_renderer_views, geometry)
}

for (const rv of graph_renderer_views) {
const {inspection_policy} = rv.model
const hit_test_result = inspection_policy.hit_test(geometry, rv)
did_hit ||= inspection_policy.do_inspection(hit_test_result, geometry, rv, final, mode)
}

return did_hit
Expand Down
10 changes: 5 additions & 5 deletions bokehjs/src/lib/models/graphs/graph_hit_test_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class EdgesOnly extends GraphHitTestPolicy {

// silently set inspected attr to avoid triggering data_source.change event and rerender
graph_view.edge_view.model.data_source.setv({inspected: edge_inspection}, {silent: true})
graph_view.edge_view.model.data_source.inspect.emit([graph_view.edge_view.model, {geometry}])
graph_view.edge_view.model.data_source.inspect.emit([[graph_view.edge_view.model], {geometry}])

return !edge_inspection.is_empty()
}
Expand Down Expand Up @@ -139,7 +139,7 @@ export class NodesOnly extends GraphHitTestPolicy {

// silently set inspected attr to avoid triggering data_source.change event and rerender
graph_view.node_view.model.data_source.setv({inspected: node_inspection}, {silent: true})
graph_view.node_view.model.data_source.inspect.emit([graph_view.node_view.model, {geometry}])
graph_view.node_view.model.data_source.inspect.emit([[graph_view.node_view.model], {geometry}])

return !node_inspection.is_empty()
}
Expand Down Expand Up @@ -228,7 +228,7 @@ export class NodesAndLinkedEdges extends GraphHitTestPolicy {

//silently set inspected attr to avoid triggering data_source.change event and rerender
graph_view.edge_view.model.data_source.setv({inspected: edge_inspection}, {silent: true})
graph_view.node_view.model.data_source.inspect.emit([graph_view.node_view.model, {geometry}])
graph_view.node_view.model.data_source.inspect.emit([[graph_view.node_view.model], {geometry}])

return !node_inspection.is_empty()
}
Expand Down Expand Up @@ -308,7 +308,7 @@ export class EdgesAndLinkedNodes extends GraphHitTestPolicy {

// silently set inspected attr to avoid triggering data_source.change event and rerender
graph_view.node_view.model.data_source.setv({inspected: node_inspection}, {silent: true})
graph_view.edge_view.model.data_source.inspect.emit([graph_view.edge_view.model, {geometry}])
graph_view.edge_view.model.data_source.inspect.emit([[graph_view.edge_view.model], {geometry}])

return !edge_inspection.is_empty()
}
Expand Down Expand Up @@ -401,7 +401,7 @@ export class NodesAndAdjacentNodes extends GraphHitTestPolicy {
graph_view.node_view.model.data_source.setv({inspected: node_inspection}, {silent: true})
}

graph_view.node_view.model.data_source.inspect.emit([graph_view.node_view.model, {geometry}])
graph_view.node_view.model.data_source.inspect.emit([[graph_view.node_view.model], {geometry}])
return !node_inspection.is_empty()
}
}
9 changes: 9 additions & 0 deletions bokehjs/src/lib/models/plots/plot_canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,15 @@ export class PlotView extends LayoutDOMView implements Renderable {
return view
}

get_renderer_view<T extends Renderer>(renderer: T): T["__view_type__"] {
const view = this.renderer_view(renderer)
if (view != null) {
return view
} else {
throw new Error(`can't find view for ${renderer}`)
}
}

get auto_ranged_renderers(): (RendererView & AutoRanged)[] {
return this.model.renderers.map((r) => this.renderer_view(r)!).filter(is_auto_ranged)
}
Expand Down
17 changes: 15 additions & 2 deletions bokehjs/src/lib/models/selections/interaction_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,28 @@ export abstract class SelectionPolicy extends Model {

abstract hit_test(geometry: Geometry, renderer_views: GlyphRendererView[]): HitTestResult

do_selection(hit_test_result: HitTestResult, source: ColumnarDataSource, final: boolean, mode: SelectionMode): boolean {
do_selection(hit_test_result: HitTestResult, source: ColumnarDataSource, final: boolean, mode: SelectionMode/*, renderer_views: GlyphRendererView[], geometry: Geometry*/): boolean {
if (hit_test_result == null) {
return false
} else {
source.selected.update(hit_test_result, final, mode)
source._select.emit()
source._select.emit() // [renderer_views, {geometry}])
return !source.selected.is_empty()
}
}

do_inspection(
hit_test_result: HitTestResult, source: ColumnarDataSource, final: boolean,
mode: SelectionMode, renderer_views: GlyphRendererView[], geometry: Geometry,
): boolean {
if (hit_test_result == null) {
return false
} else {
source.inspected.update(hit_test_result, final, mode)
source.inspect.emit([renderer_views.map((rv) => rv.model), {geometry}])
return !source.inspected.is_empty()
}
}
}

export class IntersectRenderers extends SelectionPolicy {
Expand Down
6 changes: 4 additions & 2 deletions bokehjs/src/lib/models/sources/columnar_data_source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export namespace ColumnarDataSource {
data: p.Property<Data> // XXX: this is hack!!!
default_values: p.Property<Dict<unknown>>
selection_policy: p.Property<SelectionPolicy>
inspection_policy: p.Property<SelectionPolicy>
inspected: p.Property<Selection>
}
}
Expand All @@ -50,7 +51,7 @@ export abstract class ColumnarDataSource extends DataSource {
}

_select: Signal0<this>
inspect: Signal<[GlyphRenderer, {geometry: Geometry}], this>
inspect: Signal<[GlyphRenderer[], {geometry: Geometry}], this>

readonly selection_manager = new SelectionManager(this)

Expand All @@ -62,10 +63,11 @@ export abstract class ColumnarDataSource extends DataSource {
this.define<ColumnarDataSource.Props>(({Ref, Dict, Unknown}) => ({
default_values: [ Dict(Unknown), {} ],
selection_policy: [ Ref(SelectionPolicy), () => new UnionRenderers() ],
inspection_policy: [ Ref(SelectionPolicy), () => new UnionRenderers() ],
}))

this.internal<ColumnarDataSource.Props>(({AnyRef}) => ({
inspected: [ AnyRef(), () => new Selection() ],
inspected: [ AnyRef(), () => new Selection() ],
}))
}

Expand Down
10 changes: 6 additions & 4 deletions bokehjs/src/lib/models/tools/gestures/select_tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {GestureTool, GestureToolView} from "./gesture_tool"
import {GlyphRenderer} from "../../renderers/glyph_renderer"
import {GraphRenderer} from "../../renderers/graph_renderer"
import {DataRenderer} from "../../renderers/data_renderer"
import type {DataSource} from "../../sources/data_source"
import type {ColumnarDataSource} from "../../sources/columnar_data_source"
import {compute_renderers} from "../../util"
import type * as p from "core/properties"
import type {KeyEvent, KeyModifiers} from "core/ui_events"
Expand All @@ -13,6 +13,7 @@ import {Signal0} from "core/signaling"
import type {MenuItem} from "core/util/menus"
import {unreachable} from "core/util/assert"
import {uniq} from "core/util/array"
import {logger} from "core/logging"

export abstract class SelectToolView extends GestureToolView {
declare model: SelectTool
Expand All @@ -29,16 +30,17 @@ export abstract class SelectToolView extends GestureToolView {
return compute_renderers(renderers, all_renderers)
}

_computed_renderers_by_data_source(): Map<DataSource, DataRenderer[]> {
const renderers_by_source: Map<DataSource, DataRenderer[]> = new Map()
_computed_renderers_by_data_source(): Map<ColumnarDataSource, DataRenderer[]> {
const renderers_by_source: Map<ColumnarDataSource, DataRenderer[]> = new Map()

for (const r of this.computed_renderers) {
let source: DataSource
let source: ColumnarDataSource
if (r instanceof GlyphRenderer) {
source = r.data_source
} else if (r instanceof GraphRenderer) {
source = r.node_renderer.data_source
} else {
logger.warn(`${r} is not supported in this context`)
continue
}

Expand Down
57 changes: 31 additions & 26 deletions bokehjs/src/lib/models/tools/gestures/tap_tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {DataRendererView} from "../../renderers/data_renderer"
import {tool_icon_tap_select} from "styles/icons.css"

export type TapToolCallback = CallbackLike1<TapTool, {
geometries: PointGeometry & {x: number, y: number}
geometries: (PointGeometry & {x: number, y: number})[]
source: ColumnarDataSource
event: {
modifiers?: KeyModifiers
Expand Down Expand Up @@ -53,52 +53,57 @@ export class TapToolView extends SelectToolView {
this._clear_other_overlays()

const geometry: PointGeometry = {type: "point", sx, sy}
if (this.model.behavior == "select") {
this._select(geometry, true, this._select_mode(ev.modifiers))
} else {
this._inspect(geometry, ev.modifiers)
const mode = this._select_mode(ev.modifiers)

switch (this.model.behavior) {
case "select": {
this._select(geometry, true, mode, ev.modifiers)
break
}
case "inspect": {
this._inspect(geometry, true, mode, ev.modifiers)
break
}
}
}

protected _select(geometry: PointGeometry, final: boolean, mode: SelectionMode): void {
protected _select(geometry: PointGeometry, final: boolean, mode: SelectionMode, modifiers?: KeyModifiers): void {
const renderers_by_source = this._computed_renderers_by_data_source()

for (const [, renderers] of renderers_by_source) {
const sm = renderers[0].get_selection_manager()
const r_views = renderers.map((r) => this.plot_view.renderer_view(r)).filter(non_null)
const did_hit = sm.select(r_views, geometry, final, mode)
for (const [source, renderers] of renderers_by_source) {
const renderer_views = renderers.map((r) => this.plot_view.renderer_view(r)).filter(non_null)
const did_hit = source.selection_manager.select(renderer_views, geometry, final, mode)
if (did_hit) {
const [rv] = r_views
this._emit_callback(rv, geometry, sm.source)
this._emit_callback(renderer_views, geometry, source, modifiers)
}
}

this._emit_selection_event(geometry)
this.plot_view.state.push("tap", {selection: this.plot_view.get_selection()})
}

protected _inspect(geometry: PointGeometry, modifiers?: KeyModifiers): void {
for (const r of this.computed_renderers) {
const rv = this.plot_view.renderer_view(r)
if (rv == null) {
continue
}
protected _inspect(geometry: PointGeometry, final: boolean, mode: SelectionMode, modifiers?: KeyModifiers): void {
const renderers_by_source = this._computed_renderers_by_data_source()

const sm = r.get_selection_manager()
const did_hit = sm.inspect(rv, geometry)
for (const [source, renderers] of renderers_by_source) {
const renderer_views = renderers.map((r) => this.plot_view.renderer_view(r)).filter(non_null)
const did_hit = source.selection_manager.inspect(renderer_views, geometry, final, mode)
if (did_hit) {
this._emit_callback(rv, geometry, sm.source, modifiers)
this._emit_callback(renderer_views, geometry, source, modifiers)
}
}
}

protected _emit_callback(rv: DataRendererView, geometry: PointGeometry, source: ColumnarDataSource, modifiers?: KeyModifiers): void {
protected _emit_callback(renderer_views: DataRendererView[], geometry: PointGeometry, source: ColumnarDataSource, modifiers?: KeyModifiers): void {
const {callback} = this.model
if (callback != null) {
const x = rv.coordinates.x_scale.invert(geometry.sx)
const y = rv.coordinates.y_scale.invert(geometry.sy)
if (callback != null && renderer_views.length != 0) {
const geometries = renderer_views.map((rv) => {
const x = rv.coordinates.x_scale.invert(geometry.sx)
const y = rv.coordinates.y_scale.invert(geometry.sy)
return {...geometry, x, y}
})
const data = {
geometries: {...geometry, x, y},
geometries,
source,
event: {modifiers},
}
Expand Down

0 comments on commit 7608549

Please sign in to comment.