@bokeh/bokehjs
Version:
Interactive, novel data visualization
246 lines • 10.5 kB
JavaScript
import { GestureTool, GestureToolView } from "./gesture_tool";
import { Modifiers, satisfies_modifiers, print_modifiers } from "./common";
import { DataRenderer } from "../../renderers/data_renderer";
import { CompositeScale } from "../../scales/composite_scale";
import { GroupBy } from "../../misc/group_by";
import { clamp } from "../../../core/util/math";
import { scale_range } from "../../../core/util/zoom";
import { Dimensions } from "../../../core/enums";
import { logger } from "../../../core/logging";
import { tool_icon_wheel_zoom } from "../../../styles/icons.css";
import { Enum, List, Ref, Or, Auto } from "../../../core/kinds";
const ZoomTogether = Enum("none", "cross", "all");
const Renderers = Or(List(Ref(DataRenderer)), Auto);
export class WheelZoomToolView extends GestureToolView {
static __name__ = "WheelZoomToolView";
_scroll(ev) {
const { modifiers } = this.model;
if (!satisfies_modifiers(modifiers, ev.modifiers)) {
this.plot_view.notify_about(`use ${print_modifiers(modifiers)} + scroll to zoom`);
return false;
}
const { sx, sy, delta } = ev;
this.zoom(sx, sy, delta);
return true;
}
_pinch(ev) {
const { sx, sy, scale } = ev;
const delta = scale >= 1 ? (scale - 1) * 20.0 : -20.0 / scale;
this.zoom(sx, sy, delta);
}
zoom(sx, sy, delta) {
const axis_view = this.plot_view.axis_views.find((view) => view.bbox.contains(sx, sy));
if (axis_view != null && !this.model.zoom_on_axis) {
return;
}
const { frame } = this.plot_view;
if (axis_view == null && !frame.bbox.contains(sx, sy)) {
return;
}
const [x_frame_scales_, y_frame_scales_] = (() => {
const x_frame = [...frame.x_scales.values()];
const y_frame = [...frame.y_scales.values()];
if (axis_view == null) {
return [x_frame, y_frame];
}
else {
const { zoom_together } = this.model;
if (zoom_together == "all") {
if (axis_view.dimension == 0) {
return [x_frame, []];
}
else {
return [[], y_frame];
}
}
else {
const { x_scale, y_scale } = axis_view.coordinates;
switch (zoom_together) {
case "cross": {
return [[x_scale], [y_scale]];
}
case "none": {
if (axis_view.dimension == 0) {
return [[x_scale], []];
}
else {
return [[], [y_scale]];
}
}
}
}
}
})();
const data_renderers = (() => {
const { renderers } = this.model;
const data_renderers = new Set(renderers != "auto" ? renderers : this.plot_view.model.data_renderers);
if (!this.model.hit_test) {
return data_renderers;
}
else {
const collected_renderers = new Set();
const hit_renderers = new Set();
for (const renderer of data_renderers) {
if (renderer.coordinates == null) {
collected_renderers.add(renderer);
continue;
}
const geometry = (() => {
switch (this.model.hit_test_mode) {
case "point": return { type: "point", sx, sy };
case "hline": return { type: "span", sx, sy, direction: "v" };
case "vline": return { type: "span", sx, sy, direction: "h" };
}
})();
const rv = this.plot_view.views.get_one(renderer);
const did_hit = rv.hit_test(geometry);
if (did_hit != null && !did_hit.is_empty()) {
hit_renderers.add(rv.model);
}
}
if (hit_renderers.size != 0) {
const { hit_test_behavior } = this.model;
if (hit_test_behavior == "only_hit") {
for (const hit of hit_renderers) {
collected_renderers.add(hit);
}
}
else {
for (const group of hit_test_behavior.query_groups(hit_renderers, data_renderers)) {
for (const renderer of group) {
if (renderer instanceof DataRenderer && data_renderers.has(renderer)) {
collected_renderers.add(renderer);
}
}
}
}
}
return [...collected_renderers];
}
})();
const x_frame_scales = new Set(x_frame_scales_);
const y_frame_scales = new Set(y_frame_scales_);
const x_renderer_scales = new Set();
const y_renderer_scales = new Set();
for (const renderer of data_renderers) {
if (renderer.coordinates == null) {
continue;
}
const rv = this.plot_view.views.get_one(renderer);
const { x_scale, y_scale } = rv.coordinates;
if (x_scale instanceof CompositeScale) {
if (x_frame_scales.has(x_scale.target_scale)) {
x_renderer_scales.add(x_scale);
}
}
if (y_scale instanceof CompositeScale) {
if (y_frame_scales.has(y_scale.target_scale)) {
y_renderer_scales.add(y_scale);
}
}
}
const [x_all_scales, y_all_scales] = (() => {
if (this.model.renderers == "auto") {
return [
new Set([...x_frame_scales, ...x_renderer_scales]),
new Set([...y_frame_scales, ...y_renderer_scales]),
];
}
else {
return [
x_renderer_scales,
y_renderer_scales,
];
}
})();
const subcoord = { x: false, y: false };
const traverse = (scale, dim) => {
const { level } = this.model;
for (let i = 0; i < level; i++) {
if (scale instanceof CompositeScale) {
subcoord[dim] = true;
scale = scale.source_scale;
}
else {
logger.warn(`can't reach sub-coordinate level ${level} for ${scale} in ${dim} dimension; stopped at ${i}`);
break;
}
}
if (scale instanceof CompositeScale) {
return scale.target_scale;
}
else {
return scale;
}
};
const x_scales = new Set();
const y_scales = new Set();
for (const x_scale of x_all_scales) {
x_scales.add(traverse(x_scale, "x"));
}
for (const y_scale of y_all_scales) {
y_scales.add(traverse(y_scale, "y"));
}
const center = (() => {
const x = subcoord.x ? null : sx;
const y = subcoord.y ? null : sy;
if (axis_view != null) {
return axis_view.dimension == 0 ? { x, y: null } : { x: null, y };
}
else {
return { x, y };
}
})();
// restrict to axis configured in tool's dimensions property and if
// zoom origin is inside of frame range/domain
const dims = this.model.dimensions;
const x_axis = dims == "width" || dims == "both";
const y_axis = dims == "height" || dims == "both";
const { x_target, y_target } = frame;
// The interval scaling functions assume factor < 1 (otherwise range
// start and end can get "flipped"), so clamp to [-0.95, 0.95] here
const factor = clamp(this.model.speed * delta, -0.95, 0.95);
const zoom_info = scale_range(x_scales, y_scales, x_target, y_target, factor, x_axis, y_axis, center);
this.plot_view.state.push("wheel_zoom", { range: zoom_info });
const { maintain_focus } = this.model;
this.plot_view.update_range(zoom_info, { scrolling: true, maintain_focus });
this.model.document?.interactive_start(this.plot_view.model, () => this.plot_view.trigger_ranges_update_event());
}
}
export class WheelZoomTool extends GestureTool {
static __name__ = "WheelZoomTool";
constructor(attrs) {
super(attrs);
}
static {
this.prototype.default_view = WheelZoomToolView;
this.define(({ Bool, Float, NonNegative, Int, Ref, Or }) => ({
dimensions: [Dimensions, "both"],
renderers: [Renderers, "auto"],
level: [NonNegative(Int), 0],
hit_test: [Bool, false],
hit_test_mode: [Enum("point", "hline", "vline"), "point"],
hit_test_behavior: [Or(Ref(GroupBy), Enum("only_hit")), "only_hit"],
maintain_focus: [Bool, true],
zoom_on_axis: [Bool, true],
zoom_together: [ZoomTogether, "all"],
speed: [Float, 1 / 600],
modifiers: [Modifiers, {}],
}));
this.register_alias("wheel_zoom", () => new WheelZoomTool({ dimensions: "both" }));
this.register_alias("xwheel_zoom", () => new WheelZoomTool({ dimensions: "width" }));
this.register_alias("ywheel_zoom", () => new WheelZoomTool({ dimensions: "height" }));
}
tool_name = "Wheel Zoom";
tool_icon = tool_icon_wheel_zoom;
event_type = "scroll";
default_order = 10;
get tooltip() {
return this._get_dim_tooltip(this.dimensions);
}
supports_auto() {
const { alt, ctrl, shift } = this.modifiers;
return alt != null || ctrl != null || shift != null;
}
}
//# sourceMappingURL=wheel_zoom_tool.js.map