UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

661 lines 26.4 kB
import { UIGestures } from "./ui_gestures"; import { Signal, Signal0 } from "./signaling"; import { offset_bbox } from "./dom"; import * as events from "./bokeh_events"; import { getDeltaY } from "./util/wheel"; import { reversed, is_empty } from "./util/array"; import { isObject, isBoolean } from "./util/types"; export function is_Tapable(obj) { return isObject(obj) && "on_tap" in obj; } export function is_Moveable(obj) { return isObject(obj) && "on_enter" in obj && "on_move" in obj && "on_leave" in obj; } export function is_Pannable(obj) { return isObject(obj) && "on_pan_start" in obj && "on_pan" in obj && "on_pan_end" in obj; } export function is_Pinchable(obj) { return isObject(obj) && "on_pinch_start" in obj && "on_pinch" in obj && "on_pinch_end" in obj; } export function is_Rotatable(obj) { return isObject(obj) && "on_rotate_start" in obj && "on_rotate" in obj && "on_rotate_end" in obj; } export function is_Scrollable(obj) { return isObject(obj) && "on_scroll" in obj; } export function is_Keyable(obj) { return isObject(obj) && "on_keydown" in obj && "on_keyup" in obj; } export class UIEventBus { canvas_view; static __name__ = "UIEventBus"; pan_start = new Signal(this, "pan:start"); pan = new Signal(this, "pan"); pan_end = new Signal(this, "pan:end"); pinch_start = new Signal(this, "pinch:start"); pinch = new Signal(this, "pinch"); pinch_end = new Signal(this, "pinch:end"); rotate_start = new Signal(this, "rotate:start"); rotate = new Signal(this, "rotate"); rotate_end = new Signal(this, "rotate:end"); tap = new Signal(this, "tap"); doubletap = new Signal(this, "doubletap"); press = new Signal(this, "press"); pressup = new Signal(this, "pressup"); move_enter = new Signal(this, "move:enter"); move = new Signal(this, "move"); move_exit = new Signal(this, "move:exit"); scroll = new Signal(this, "scroll"); keydown = new Signal(this, "keydown"); keyup = new Signal(this, "keyup"); focus = new Signal0(this, "focus"); blur = new Signal0(this, "blur"); hit_area; ui_gestures; constructor(canvas_view) { this.canvas_view = canvas_view; this.hit_area = canvas_view.events_el; this.on_tap = this.on_tap.bind(this); this.on_doubletap = this.on_doubletap.bind(this); this.on_press = this.on_press.bind(this); this.on_pressup = this.on_pressup.bind(this); this.on_enter = this.on_enter.bind(this); this.on_move = this.on_move.bind(this); this.on_leave = this.on_leave.bind(this); this.on_pan_start = this.on_pan_start.bind(this); this.on_pan = this.on_pan.bind(this); this.on_pan_end = this.on_pan_end.bind(this); this.on_pinch_start = this.on_pinch_start.bind(this); this.on_pinch = this.on_pinch.bind(this); this.on_pinch_end = this.on_pinch_end.bind(this); this.on_rotate_start = this.on_rotate_start.bind(this); this.on_rotate = this.on_rotate.bind(this); this.on_rotate_end = this.on_rotate_end.bind(this); this.on_context_menu = this.on_context_menu.bind(this); this.on_mouse_wheel = this.on_mouse_wheel.bind(this); this.on_key_down = this.on_key_down.bind(this); this.on_key_up = this.on_key_up.bind(this); this.on_focus = this.on_focus.bind(this); this.on_blur = this.on_blur.bind(this); this.ui_gestures = new UIGestures(this.hit_area, this, { must_be_target: true }); this.ui_gestures.connect_signals(); this.hit_area.addEventListener("contextmenu", this.on_context_menu); this.hit_area.addEventListener("wheel", this.on_mouse_wheel); this.hit_area.addEventListener("focus", this.on_focus); this.hit_area.addEventListener("blur", this.on_blur); this.hit_area.addEventListener("keydown", this.on_key_down); this.hit_area.addEventListener("keyup", this.on_key_up); } remove() { this.ui_gestures.remove(); this.hit_area.removeEventListener("contextmenu", this.on_context_menu); this.hit_area.removeEventListener("wheel", this.on_mouse_wheel); document.removeEventListener("keydown", this.on_key_down); document.removeEventListener("keyup", this.on_key_up); } _tools = new Map(); register_tool(tool_view) { const { model: tool } = tool_view; if (this._tools.has(tool)) { throw new Error(`${tool} already registered`); } else { this._tools.set(tool, tool_view); } } hit_test_renderers(plot_view, sx, sy) { const collected = []; for (const view of reversed(plot_view.all_renderer_views)) { if (view.interactive_hit?.(sx, sy) ?? false) { collected.push(view); } } return collected; } set_cursor(cursor) { this.hit_area.style.cursor = cursor ?? "default"; } hit_test_frame(plot_view, sx, sy) { return plot_view.frame.bbox.contains(sx, sy); } hit_test_plot(sx, sy) { // TODO: z-index for (const plot_view of this.canvas_view.plot_views) { if (plot_view.bbox.relative() /*XXX*/.contains(sx, sy)) { return plot_view; } } return null; } _prev_move = null; _curr_pan = null; _curr_pinch = null; _curr_rotate = null; _trigger(signal, e) { if (!this.hit_area.isConnected) { return; } const { sx, sy, native: srcEvent } = e; const plot_view = this.hit_test_plot(sx, sy); const curr_view = plot_view; const relativize_event = (_plot_view) => { const [rel_sx, rel_sy] = [sx, sy]; // plot_view.layout.bbox.relativize(sx, sy) return { ...e, sx: rel_sx, sy: rel_sy }; }; if (e.type == "pan_start" || e.type == "pan" || e.type == "pan_end") { let pan_view; if (e.type == "pan_start" && curr_view != null) { this._curr_pan = { plot_view: curr_view }; pan_view = curr_view; } else if (e.type == "pan" && this._curr_pan != null) { pan_view = this._curr_pan.plot_view; } else if (e.type == "pan_end" && this._curr_pan != null) { pan_view = this._curr_pan.plot_view; this._curr_pan = null; } else { pan_view = null; } if (pan_view != null) { const event = relativize_event(pan_view); this.__trigger(pan_view, signal, event, srcEvent); } } else if (e.type == "pinch_start" || e.type == "pinch" || e.type == "pinch_end") { let pinch_view; if (e.type == "pinch_start" && curr_view != null) { this._curr_pinch = { plot_view: curr_view }; pinch_view = curr_view; } else if (e.type == "pinch" && this._curr_pinch != null) { pinch_view = this._curr_pinch.plot_view; } else if (e.type == "pinch_end" && this._curr_pinch != null) { pinch_view = this._curr_pinch.plot_view; this._curr_pinch = null; } else { pinch_view = null; } if (pinch_view != null) { const event = relativize_event(pinch_view); this.__trigger(pinch_view, signal, event, srcEvent); } } else if (e.type == "rotate_start" || e.type == "rotate" || e.type == "rotate_end") { let rotate_view; if (e.type == "rotate_start" && curr_view != null) { this._curr_rotate = { plot_view: curr_view }; rotate_view = curr_view; } else if (e.type == "rotate" && this._curr_rotate != null) { rotate_view = this._curr_rotate.plot_view; } else if (e.type == "rotate_end" && this._curr_rotate != null) { rotate_view = this._curr_rotate.plot_view; this._curr_rotate = null; } else { rotate_view = null; } if (rotate_view != null) { const event = relativize_event(rotate_view); this.__trigger(rotate_view, signal, event, srcEvent); } } else if (e.type == "enter" || e.type == "move" || e.type == "leave") { const prev_view = this._prev_move?.plot_view; if (prev_view != null && (e.type == "leave" || prev_view != curr_view)) { const { sx, sy } = relativize_event(prev_view); this.__trigger(prev_view, this.move_exit, { type: "leave", sx, sy, modifiers: { shift: false, ctrl: false, alt: false }, native: srcEvent }, srcEvent); } if (curr_view != null && (e.type == "enter" || prev_view != curr_view)) { const { sx, sy } = relativize_event(curr_view); this.__trigger(curr_view, this.move_enter, { type: "enter", sx, sy, modifiers: { shift: false, ctrl: false, alt: false }, native: srcEvent }, srcEvent); } if (curr_view != null && e.type == "move") { const event = relativize_event(curr_view); this.__trigger(curr_view, signal, event, srcEvent); } this._prev_move = { sx, sy, plot_view: curr_view }; } else { if (curr_view != null) { const event = relativize_event(curr_view); this.__trigger(curr_view, signal, event, srcEvent); } } } _current_interactive_tool_view = null; _current_pan_view = null; _current_pinch_view = null; _current_rotate_view = null; _current_move_views = []; __trigger(plot_view, signal, e, srcEvent) { const gestures = plot_view.model.toolbar.gestures; const event_type = signal.name; const base_type = event_type.split(":")[0]; const views = this.hit_test_renderers(plot_view, e.sx, e.sy); if (base_type == "pan") { const event = e; if (this._current_pan_view == null) { if (event_type == "pan:start") { for (const view of views) { if (!is_Pannable(view)) { continue; } if (view.on_pan_start(event)) { this._current_pan_view = view; srcEvent.preventDefault(); return; } } } } else { if (event_type == "pan") { this._current_pan_view.on_pan(event); } else if (event_type == "pan:end") { this._current_pan_view.on_pan_end(event); this.set_cursor(this._current_pan_view.cursor(event.sx, event.sy)); this._current_pan_view = null; } srcEvent.preventDefault(); return; } } else if (base_type == "pinch") { const event = e; if (this._current_pinch_view == null) { if (event_type == "pinch:start") { for (const view of views) { if (!is_Pinchable(view)) { continue; } if (view.on_pinch_start(event)) { this._current_pinch_view = view; srcEvent.preventDefault(); return; } } } } else { if (event_type == "pinch") { this._current_pinch_view.on_pinch(event); } else if (event_type == "pinch:end") { this._current_pinch_view.on_pinch_end(event); this._current_pinch_view = null; } srcEvent.preventDefault(); return; } } else if (base_type == "rotate") { const event = e; if (this._current_rotate_view == null) { if (event_type == "rotate:start") { for (const view of views) { if (!is_Rotatable(view)) { continue; } if (view.on_rotate_start(event)) { this._current_rotate_view = view; srcEvent.preventDefault(); return; } } } } else { if (event_type == "rotate") { this._current_rotate_view.on_rotate(event); } else if (event_type == "rotate:end") { this._current_rotate_view.on_rotate_end(event); this._current_rotate_view = null; } srcEvent.preventDefault(); return; } } else if (base_type == "move") { const event = e; const new_views = new Set(views); const current_views = new Set(this._current_move_views); this._current_move_views = []; for (const view of current_views) { if (!new_views.has(view)) { current_views.delete(view); view.on_leave(event); } } for (const view of views) { if (!is_Moveable(view)) { continue; } if (!current_views.has(view)) { if (view.on_enter(e)) { this._current_move_views.push(view); } } else { this._current_move_views.push(view); view.on_move(event); } } } function get_tool_view(tool_like) { if (tool_like != null) { return plot_view.tool_views.get(tool_like.underlying) ?? null; } else { return null; } } const top_view = views.at(0); switch (base_type) { case "move": { const active_gesture = gestures.move.active; if (active_gesture != null) { this.trigger(signal, e, active_gesture); } const active_inspectors = plot_view.model.toolbar.inspectors.filter(t => t.active); const cursor = (() => { const current_view = this._current_interactive_tool_view ?? this._current_pan_view ?? this._current_pinch_view ?? this._current_rotate_view ?? this._current_move_views.at(0) ?? top_view ?? get_tool_view(active_gesture); if (current_view != null) { const cursor = current_view.cursor(e.sx, e.sy); if (cursor != null) { return cursor; } } if (this.hit_test_frame(plot_view, e.sx, e.sy) && !is_empty(active_inspectors)) { // the event happened on the plot frame but off a renderer return "crosshair"; } return null; })(); this.set_cursor(cursor); active_inspectors.map((inspector) => this.trigger(signal, e, inspector)); break; } case "tap": { const path = srcEvent.composedPath(); if (path.length != 0 && path[0] != this.hit_area) { return; // don't trigger bokeh events } top_view?.on_hit?.(e.sx, e.sy); if (this.hit_test_frame(plot_view, e.sx, e.sy)) { const active_gesture = gestures.tap.active; if (active_gesture != null) { this.trigger(signal, e, active_gesture); } } break; } case "doubletap": { if (this.hit_test_frame(plot_view, e.sx, e.sy)) { const active_gesture = gestures.doubletap.active ?? gestures.tap.active; if (active_gesture != null) { this.trigger(signal, e, active_gesture); } } break; } case "press": { if (this.hit_test_frame(plot_view, e.sx, e.sy)) { const active_gesture = gestures.press.active ?? gestures.tap.active; if (active_gesture != null) { this.trigger(signal, e, active_gesture); } } break; } case "pinch": { const active_gesture = gestures.pinch.active ?? gestures.scroll.active; if (active_gesture != null) { if (this.trigger(signal, e, active_gesture)) { srcEvent.preventDefault(); srcEvent.stopPropagation(); } } break; } case "scroll": { const active_gesture = gestures.scroll.active; if (active_gesture != null) { if (this.trigger(signal, e, active_gesture)) { srcEvent.preventDefault(); srcEvent.stopPropagation(); } } break; } case "pan": { const active_gesture = gestures.pan.active; const active_pan_view = get_tool_view(active_gesture); if (active_pan_view != null) { switch (event_type) { case "pan:start": { this._current_interactive_tool_view = active_pan_view; break; } case "pan:end": { this._current_interactive_tool_view = null; break; } } if (this.trigger(signal, e, active_gesture)) { srcEvent.preventDefault(); srcEvent.stopPropagation(); } const cursor = active_pan_view.cursor(e.sx, e.sy); this.set_cursor(cursor); } break; } default: { const active_gesture = gestures[base_type].active; if (active_gesture != null) { this.trigger(signal, e, active_gesture); } } } this._trigger_bokeh_event(plot_view, e); } trigger(signal, e, tool = null) { const emit = (tool) => { const tool_view = this._tools.get(tool); if (tool_view == null) { return false; } const fn = (() => { switch (signal) { case this.pan_start: return tool_view._pan_start; case this.pan: return tool_view._pan; case this.pan_end: return tool_view._pan_end; case this.pinch_start: return tool_view._pinch_start; case this.pinch: return tool_view._pinch; case this.pinch_end: return tool_view._pinch_end; case this.rotate_start: return tool_view._rotate_start; case this.rotate: return tool_view._rotate; case this.rotate_end: return tool_view._rotate_end; case this.move_enter: return tool_view._move_enter; case this.move: return tool_view._move; case this.move_exit: return tool_view._move_exit; case this.tap: return tool_view._tap; case this.doubletap: return tool_view._doubletap; case this.press: return tool_view._press; case this.pressup: return tool_view._pressup; case this.scroll: return tool_view._scroll; case this.keydown: return tool_view._keydown; case this.keyup: return tool_view._keyup; default: return null; } })(); if (fn == null) { return false; } const val = fn.bind(tool_view)(e); if (isBoolean(val)) { return val; } else { return true; } }; if (tool != null) { return emit(tool); } else { let result = false; for (const tool of this._tools.keys()) { // don't conflate these lines because of short circuiting nature of ||= operator const emitted = emit(tool); result ||= emitted; } return result; } } /*protected*/ _trigger_bokeh_event(plot_view, ev) { const bokeh_event = (() => { const { sx, sy, modifiers } = ev; const x = plot_view.frame.x_scale.invert(sx); const y = plot_view.frame.y_scale.invert(sy); switch (ev.type) { case "wheel": return new events.MouseWheel(sx, sy, x, y, ev.delta, modifiers); case "enter": return new events.MouseEnter(sx, sy, x, y, modifiers); case "move": return new events.MouseMove(sx, sy, x, y, modifiers); case "leave": return new events.MouseLeave(sx, sy, x, y, modifiers); case "tap": return new events.Tap(sx, sy, x, y, modifiers); case "double_tap": return new events.DoubleTap(sx, sy, x, y, modifiers); case "press": return new events.Press(sx, sy, x, y, modifiers); case "press_up": return new events.PressUp(sx, sy, x, y, modifiers); case "pan_start": return new events.PanStart(sx, sy, x, y, modifiers); case "pan": return new events.Pan(sx, sy, x, y, ev.dx, ev.dy, modifiers); case "pan_end": return new events.PanEnd(sx, sy, x, y, modifiers); case "pinch_start": return new events.PinchStart(sx, sy, x, y, modifiers); case "pinch": return new events.Pinch(sx, sy, x, y, ev.scale, modifiers); case "pinch_end": return new events.PinchEnd(sx, sy, x, y, modifiers); case "rotate_start": return new events.RotateStart(sx, sy, x, y, modifiers); case "rotate": return new events.Rotate(sx, sy, x, y, ev.rotation, modifiers); case "rotate_end": return new events.RotateEnd(sx, sy, x, y, modifiers); default: return null; } })(); if (bokeh_event != null) { plot_view.model.trigger_event(bokeh_event); } } _get_sxy(event) { const { pageX, pageY } = event; const { left, top } = offset_bbox(this.hit_area); return { sx: pageX - left, sy: pageY - top, }; } _get_modifiers(event) { return { shift: event.shiftKey, ctrl: event.ctrlKey, alt: event.altKey, }; } _scroll_event(event) { return { type: event.type, ...this._get_sxy(event), delta: getDeltaY(event), modifiers: this._get_modifiers(event), native: event, }; } _key_event(event) { return { type: event.type, key: event.key, modifiers: this._get_modifiers(event), native: event, }; } on_tap(event) { this._trigger(this.tap, event); } on_doubletap(event) { this._trigger(this.doubletap, event); } on_press(event) { this._trigger(this.press, event); } on_pressup(event) { this._trigger(this.pressup, event); } on_enter(event) { this._trigger(this.move_enter, event); } on_move(event) { this._trigger(this.move, event); } on_leave(event) { this._trigger(this.move_exit, event); } on_pan_start(event) { this._trigger(this.pan_start, event); } on_pan(event) { this._trigger(this.pan, event); } on_pan_end(event) { this._trigger(this.pan_end, event); } on_pinch_start(event) { this._trigger(this.pinch_start, event); } on_pinch(event) { this._trigger(this.pinch, event); } on_pinch_end(event) { this._trigger(this.pinch_end, event); } on_rotate_start(event) { this._trigger(this.rotate_start, event); } on_rotate(event) { this._trigger(this.rotate, event); } on_rotate_end(event) { this._trigger(this.rotate_end, event); } on_mouse_wheel(event) { this._trigger(this.scroll, this._scroll_event(event)); } on_context_menu(_event) { // TODO } on_key_down(event) { // NOTE: keyup event triggered unconditionally this.trigger(this.keydown, this._key_event(event)); } on_key_up(event) { // NOTE: keyup event triggered unconditionally this.trigger(this.keyup, this._key_event(event)); } on_focus() { this.focus.emit(); } on_blur() { this.blur.emit(); } } //# sourceMappingURL=ui_events.js.map