UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

286 lines 9.9 kB
import { GestureTool, GestureToolView } from "./gesture_tool"; import { BoxAnnotation } from "../../annotations/box_annotation"; import { MenuItem } from "../../ui/menus"; import { Dimensions, BoxOrigin } from "../../../core/enums"; import * as icons from "../../../styles/icons.css"; export class BoxZoomToolView extends GestureToolView { static __name__ = "BoxZoomToolView"; get overlays() { return [...super.overlays, this.model.overlay]; } _base_point = null; _match_aspect([bx, by], [cx, cy], frame) { // aspect ratio of plot frame const a = frame.bbox.aspect; const hend = frame.bbox.h_range.end; const hstart = frame.bbox.h_range.start; const vend = frame.bbox.v_range.end; const vstart = frame.bbox.v_range.start; // current aspect of cursor-defined box let vw = Math.abs(bx - cx); let vh = Math.abs(by - cy); const va = vh == 0 ? 0 : vw / vh; const [xmod] = va >= a ? [1, va / a] : [a / va, 1]; // OK the code blocks below merit some explanation. They do: // // compute left/right, pin to frame if necessary // compute top/bottom (based on new left/right), pin to frame if necessary // recompute left/right (based on top/bottom), in case top/bottom were pinned // bx is left let left; let right; if (bx <= cx) { left = bx; right = bx + vw * xmod; if (right > hend) { right = hend; } // bx is right } else { right = bx; left = bx - vw * xmod; if (left < hstart) { left = hstart; } } vw = Math.abs(right - left); // by is bottom let top; let bottom; if (by <= cy) { bottom = by; top = by + vw / a; if (top > vend) { top = vend; } // by is top } else { top = by; bottom = by - vw / a; if (bottom < vstart) { bottom = vstart; } } vh = Math.abs(top - bottom); if (bx <= cx) { // bx is left right = bx + a * vh; } else { // bx is right left = bx - a * vh; } return [[left, right], [bottom, top]]; } _get_dimensions(base_point, curr_point) { const { dimensions } = this.model; if (dimensions == "auto") { const [bx, by] = base_point; const [cx, cy] = curr_point; const dx = Math.abs(bx - cx); const dy = Math.abs(by - cy); const tol_d = 15; const tol_aspect_ratio = 3; if (dx < tol_d && dy > tol_d && dy > tol_aspect_ratio * dx) { return "height"; } else if (dx > tol_d && dy < tol_d && dx > tol_aspect_ratio * dy) { return "width"; } else { return "both"; } } else { return dimensions; } } _compute_limits(base_point, curr_point) { const { frame } = this.plot_view; if (this.model.origin == "center") { const [cx, cy] = base_point; const [dx, dy] = curr_point; base_point = [cx - (dx - cx), cy - (dy - cy)]; } const dims = this._get_dimensions(base_point, curr_point); if (this.model.match_aspect && dims == "both") { return this._match_aspect(base_point, curr_point, frame); } else { return this.model._get_dim_limits(base_point, curr_point, frame, dims); } } _pan_start(ev) { const { sx, sy } = ev; if (this.plot_view.frame.bbox.contains(sx, sy)) { this._base_point = [sx, sy]; } } _pan(ev) { if (this._base_point == null) { return; } const [sxlim, sylim] = this._compute_limits(this._base_point, [ev.sx, ev.sy]); const dims = this._get_dimensions(this._base_point, [ev.sx, ev.sy]); const { line_width } = this.model.overlay; const [[left, right], [top, bottom]] = this.model._compute_overlay_limits(sxlim, sylim, dims, line_width); this.model.overlay.update({ left, right, top, bottom }); } _pan_end(ev) { if (this._base_point == null) { return; } const [sx, sy] = this._compute_limits(this._base_point, [ev.sx, ev.sy]); this._update(sx, sy); this._stop(); } _stop() { this.model.overlay.clear(); this._base_point = null; } _keydown(ev) { if (ev.key == "Escape") { this._stop(); } } _doubletap(_ev) { const { state } = this.plot_view; if (state.peek()?.type == "box_zoom") { state.undo(); } } _update([sx0, sx1], [sy0, sy1]) { // If the viewing window is too small, no-op: it is likely that the user did // not intend to make this box zoom and instead was trying to cancel out of the // zoom, a la matplotlib's ToolZoom. Like matplotlib, set the threshold at 5 pixels. if (Math.abs(sx1 - sx0) <= 5 || Math.abs(sy1 - sy0) <= 5) { return; } const { x_scales, y_scales } = this.plot_view.frame; const xrs = new Map(); for (const [, scale] of x_scales) { const [start, end] = scale.r_invert(sx0, sx1); xrs.set(scale.source_range, { start, end }); } const yrs = new Map(); for (const [, scale] of y_scales) { const [start, end] = scale.r_invert(sy0, sy1); yrs.set(scale.source_range, { start, end }); } const zoom_info = { xrs, yrs }; this.plot_view.state.push("box_zoom", { range: zoom_info }); this.plot_view.update_range(zoom_info); this.plot_view.trigger_ranges_update_event(); } } const DEFAULT_BOX_OVERLAY = () => { return new BoxAnnotation({ syncable: false, level: "overlay", visible: false, editable: false, left: NaN, right: NaN, top: NaN, bottom: NaN, top_units: "canvas", left_units: "canvas", bottom_units: "canvas", right_units: "canvas", fill_color: "lightgrey", fill_alpha: 0.5, line_color: "black", line_alpha: 1.0, line_width: 2, line_dash: [4, 4], }); }; export class BoxZoomTool extends GestureTool { static __name__ = "BoxZoomTool"; constructor(attrs) { super(attrs); } static { this.prototype.default_view = BoxZoomToolView; this.define(({ Bool, Ref, Or, Auto }) => ({ dimensions: [Or(Dimensions, Auto), "auto"], overlay: [Ref(BoxAnnotation), DEFAULT_BOX_OVERLAY], match_aspect: [Bool, false], origin: [BoxOrigin, "corner"], })); this.register_alias("box_zoom", () => new BoxZoomTool({ dimensions: "both" })); this.register_alias("xbox_zoom", () => new BoxZoomTool({ dimensions: "width" })); this.register_alias("ybox_zoom", () => new BoxZoomTool({ dimensions: "height" })); this.register_alias("auto_box_zoom", () => new BoxZoomTool({ dimensions: "auto" })); } tool_name = "Box Zoom"; event_type = ["pan", "doubletap"]; get event_role() { return "pan"; } default_order = 20; get computed_icon() { const icon = super.computed_icon; if (icon != null) { return icon; } else { switch (this.dimensions) { case "both": return `.${icons.tool_icon_box_zoom}`; case "width": return `.${icons.tool_icon_x_box_zoom}`; case "height": return `.${icons.tool_icon_y_box_zoom}`; case "auto": return `.${icons.tool_icon_auto_box_zoom}`; } } } get tooltip() { return this._get_dim_tooltip(this.dimensions); } get menu() { return [ new MenuItem({ icon: `.${icons.tool_icon_box_zoom}`, label: "XY mode", tooltip: "Box zoom in both dimensions", checked: () => this.dimensions == "both", action: () => { this.dimensions = "both"; this.active = true; }, }), new MenuItem({ icon: `.${icons.tool_icon_x_box_zoom}`, label: "X-only", tooltip: "Box zoom in x-dimension", checked: () => this.dimensions == "width", action: () => { this.dimensions = "width"; this.active = true; }, }), new MenuItem({ icon: `.${icons.tool_icon_y_box_zoom}`, label: "Y-only", tooltip: "Box zoom in y-dimension", checked: () => this.dimensions == "height", action: () => { this.dimensions = "height"; this.active = true; }, }), new MenuItem({ icon: `.${icons.tool_icon_auto_box_zoom}`, label: "Auto mode", tooltip: "Automatic mode (box zoom in x, y or both dimensions, depending on the mouse gesture)", checked: () => this.dimensions == "auto", action: () => { this.dimensions = "auto"; this.active = true; }, }), ]; } } //# sourceMappingURL=box_zoom_tool.js.map