UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

308 lines 10.6 kB
import { GestureTool, GestureToolView } from "../gestures/gesture_tool"; import { OnOffButton } from "../on_off_button"; import { BoxAnnotation } from "../../annotations/box_annotation"; import { Range } from "../../ranges/range"; import { logger } from "../../../core/logging"; import { assert, unreachable } from "../../../core/util/assert"; import { isNumber } from "../../../core/util/types"; import { tool_icon_range } from "../../../styles/icons.css"; import { Node } from "../../coordinates/node"; import { Enum } from "../../../core/kinds"; const StartGesture = Enum("pan", "tap", "none"); export class RangeToolView extends GestureToolView { static __name__ = "RangeToolView"; get overlays() { return [...super.overlays, this.model.overlay]; } initialize() { super.initialize(); this.model.update_overlay_from_ranges(); } connect_signals() { super.connect_signals(); const update_overlay = () => this.model.update_overlay_from_ranges(); this.on_transitive_change(this.model.properties.x_range, update_overlay); this.on_transitive_change(this.model.properties.y_range, update_overlay); this.model.overlay.pan.connect(([state, _]) => { if (state == "pan") { this.model.update_ranges_from_overlay(); } else if (state == "pan:end") { const ranges = [this.model.x_range, this.model.y_range].filter((r) => r != null); this.parent.trigger_ranges_update_event(ranges); } }); const { active, x_interaction, y_interaction } = this.model.properties; this.on_change([active, x_interaction, y_interaction], () => { this.model.update_constraints(); }); } _mappers() { const mapper = (units, scale, view, canvas) => { switch (units) { case "canvas": return canvas; case "screen": return view; case "data": return scale; } }; const { overlay } = this.model; const { frame, canvas } = this.plot_view; const { x_scale, y_scale } = frame; const { x_view, y_view } = frame.bbox; const { x_screen, y_screen } = canvas.bbox; return { left: mapper(overlay.left_units, x_scale, x_view, x_screen), right: mapper(overlay.right_units, x_scale, x_view, x_screen), top: mapper(overlay.top_units, y_scale, y_view, y_screen), bottom: mapper(overlay.bottom_units, y_scale, y_view, y_screen), }; } _invert_lrtb({ left, right, top, bottom }) { const lrtb = this._mappers(); const { x_range, y_range } = this.model; const has_x = x_range != null; const has_y = y_range != null; return { left: has_x ? lrtb.left.invert(left) : this.model.nodes.left, right: has_x ? lrtb.right.invert(right) : this.model.nodes.right, top: has_y ? lrtb.top.invert(top) : this.model.nodes.top, bottom: has_y ? lrtb.bottom.invert(bottom) : this.model.nodes.bottom, }; } _compute_limits(curr_point) { const dims = (() => { const { x_range, y_range } = this.model; const has_x = x_range != null; const has_y = y_range != null; if (has_x && has_y) { return "both"; } else if (has_x) { return "width"; } else if (has_y) { return "height"; } else { unreachable(); } })(); assert(this._base_point != null); let base_point = this._base_point; if (this.model.overlay.symmetric) { const [cx, cy] = base_point; const [dx, dy] = curr_point; base_point = [cx - (dx - cx), cy - (dy - cy)]; } const { frame } = this.plot_view; return this.model._get_dim_limits(base_point, curr_point, frame, dims); } _base_point; _tap(ev) { assert(this.model.start_gesture == "tap"); const { sx, sy } = ev; const { frame } = this.plot_view; if (!frame.bbox.contains(sx, sy)) { return; } if (this._base_point == null) { this._base_point = [sx, sy]; } else { this._update_overlay(sx, sy); this._base_point = null; } } _move(ev) { if (this._base_point != null && this.model.start_gesture == "tap") { const { sx, sy } = ev; this._update_overlay(sx, sy); } } _pan_start(ev) { assert(this.model.start_gesture == "pan"); assert(this._base_point == null); const { sx, sy } = ev; const { frame } = this.plot_view; if (!frame.bbox.contains(sx, sy)) { return; } this._base_point = [sx, sy]; } _update_overlay(sx, sy) { const [sxlim, sylim] = this._compute_limits([sx, sy]); const [[left, right], [top, bottom]] = [sxlim, sylim]; this.model.overlay.update(this._invert_lrtb({ left, right, top, bottom })); this.model.update_ranges_from_overlay(); } _pan(ev) { if (this._base_point == null) { return; } const { sx, sy } = ev; this._update_overlay(sx, sy); } _pan_end(ev) { if (this._base_point == null) { return; } const { sx, sy } = ev; this._update_overlay(sx, sy); this._base_point = null; } get _is_selecting() { return this._base_point != null; } _stop() { this._base_point = null; } _keyup(ev) { if (!this.model.active) { return; } if (ev.key == "Escape" && this._is_selecting) { this._stop(); } } } const DEFAULT_RANGE_OVERLAY = () => { return new BoxAnnotation({ syncable: false, level: "overlay", visible: true, editable: true, propagate_hover: true, left: NaN, right: NaN, top: NaN, bottom: NaN, left_limit: Node.frame.left, right_limit: Node.frame.right, top_limit: Node.frame.top, bottom_limit: Node.frame.bottom, fill_color: "lightgrey", fill_alpha: 0.5, line_color: "black", line_alpha: 1.0, line_width: 0.5, line_dash: [2, 2], }); }; export class RangeTool extends GestureTool { static __name__ = "RangeTool"; constructor(attrs) { super(attrs); } static { this.prototype.default_view = RangeToolView; this.define(({ Bool, Ref, Nullable }) => ({ x_range: [Nullable(Ref(Range)), null], y_range: [Nullable(Ref(Range)), null], x_interaction: [Bool, true], y_interaction: [Bool, true], overlay: [Ref(BoxAnnotation), DEFAULT_RANGE_OVERLAY], start_gesture: [StartGesture, "none"], })); this.override({ active: true, }); } initialize() { super.initialize(); this.update_constraints(); } update_constraints() { this.overlay.editable = this.active; const has_x = this.x_range != null && this.x_interaction; const has_y = this.y_range != null && this.y_interaction; if (has_x && has_y) { this.overlay.movable = "both"; this.overlay.resizable = "all"; } else if (has_x) { this.overlay.movable = "x"; this.overlay.resizable = "x"; } else if (has_y) { this.overlay.movable = "y"; this.overlay.resizable = "y"; } else { this.overlay.movable = "none"; this.overlay.resizable = "none"; } const { x_range, y_range } = this; if (x_range != null) { this.overlay.min_width = x_range.min_interval ?? 0; this.overlay.max_width = x_range.max_interval ?? Infinity; } if (y_range != null) { this.overlay.min_height = y_range.min_interval ?? 0; this.overlay.max_height = y_range.max_interval ?? Infinity; } } update_ranges_from_overlay() { const { left, right, top, bottom } = this.overlay; const { x_range, y_range } = this; const affected_plots = new Set(); const xrs = new Map(); const yrs = new Map(); if (x_range != null && this.x_interaction) { assert(isNumber(left) && isNumber(right)); xrs.set(x_range, { start: left, end: right }); for (const plot of x_range.linked_plots) { affected_plots.add(plot); } } if (y_range != null && this.y_interaction) { assert(isNumber(bottom) && isNumber(top)); yrs.set(y_range, { start: bottom, end: top }); for (const plot of y_range.linked_plots) { affected_plots.add(plot); } } if (affected_plots.size == 0) { for (const [range, { start, end }] of [...xrs, ...yrs]) { range.setv({ start, end }); } } else { for (const plot of affected_plots) { plot.update_range({ xrs, yrs }, { panning: true, scrolling: true }); } } } nodes = Node.frame.freeze(); update_overlay_from_ranges() { const { x_range, y_range } = this; const has_x = x_range != null; const has_y = y_range != null; this.overlay.update({ left: has_x ? x_range.start : this.nodes.left, right: has_x ? x_range.end : this.nodes.right, top: has_y ? y_range.end : this.nodes.top, bottom: has_y ? y_range.start : this.nodes.bottom, }); if (!has_x && !has_y) { logger.warn("RangeTool not configured with any Ranges."); this.overlay.clear(); } } tool_name = "Range Tool"; tool_icon = tool_icon_range; get event_type() { switch (this.start_gesture) { case "pan": return "pan"; case "tap": return ["tap", "move"]; case "none": return []; } } default_order = 40; supports_auto() { return true; } tool_button() { return new OnOffButton({ tool: this }); } } //# sourceMappingURL=range_tool.js.map