@bokeh/bokehjs
Version:
Interactive, novel data visualization
282 lines • 9.68 kB
JavaScript
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]];
}
_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 = (() => {
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;
}
})();
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 [[left, right], [top, bottom]] = this._compute_limits(this._base_point, [ev.sx, ev.sy]);
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