UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

246 lines 10.5 kB
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