Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ColumnarDataSource.inspection_policy #13439

Open
wants to merge 1 commit into
base: branch-3.5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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