@bokeh/bokehjs
Version:
Interactive, novel data visualization
661 lines • 26.4 kB
JavaScript
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