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 Oct 13, 2023
1 parent 06a410c commit 77cf1cf
Show file tree
Hide file tree
Showing 17 changed files with 271 additions and 133 deletions.
79 changes: 48 additions & 31 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,57 +20,73 @@ 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
}

clear(rview?: DataRendererView): void {
clear(rv?: DataRendererView): void {
this.source.selected.clear()
if (rview != null)
this.get_or_create_inspector(rview.model).clear()
if (rv != null) {
this.get_or_create_inspector(rv.model).clear()
}
}

get_or_create_inspector(renderer: DataRenderer): Selection {
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 @@ -86,7 +86,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 @@ -132,7 +132,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 @@ -210,7 +210,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 @@ -283,7 +283,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 @@ -366,7 +366,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 @@ -124,6 +124,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 @@ -23,6 +23,7 @@ export namespace ColumnarDataSource {
export type Props = DataSource.Props & {
data: p.Property<{[key: string]: Arrayable}> // XXX: this is hack!!!
selection_policy: p.Property<SelectionPolicy>
inspection_policy: p.Property<SelectionPolicy>
inspected: p.Property<Selection>
}
}
Expand All @@ -46,7 +47,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 @@ -57,10 +58,11 @@ export abstract class ColumnarDataSource extends DataSource {
static {
this.define<ColumnarDataSource.Props>(({Ref}) => ({
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
17 changes: 10 additions & 7 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 @@ -12,6 +12,7 @@ import type {Geometry} from "core/geometry"
import {Signal0} from "core/signaling"
import type {MenuItem} from "core/util/menus"
import {unreachable} from "core/util/assert"
import {logger} from "core/logging"

export abstract class SelectToolView extends GestureToolView {
declare model: SelectTool
Expand All @@ -27,17 +28,19 @@ 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
if (r instanceof GlyphRenderer)
let source: ColumnarDataSource
if (r instanceof GlyphRenderer) {
source = r.data_source
else if (r instanceof GraphRenderer)
} else if (r instanceof GraphRenderer) {
source = r.node_renderer.data_source
else
} else {
logger.warn(`${r} is not supported in this context`)
continue
}

const renderers = renderers_by_source.get(source) ?? []
renderers_by_source.set(source, [...renderers, r])
Expand Down

0 comments on commit 77cf1cf

Please sign in to comment.