UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

285 lines 11.5 kB
import { DataRange1d } from "../ranges/data_range1d"; import { logger } from "../../core/logging"; export class RangeManager { parent; static __name__ = "RangeManager"; constructor(parent) { this.parent = parent; } get frame() { return this.parent.frame; } warn_initial_ranges = true; invalidate_dataranges = true; update(range_info, options = {}) { const panning = options.panning ?? false; const scrolling = options.scrolling ?? false; const maintain_focus = options.maintain_focus ?? false; const range_state = new Map(); for (const [range, interval] of range_info.xrs) { range_state.set(range, interval); } for (const [range, interval] of range_info.yrs) { range_state.set(range, interval); } if (scrolling && maintain_focus) { this._update_ranges_together(range_state); // apply interval bounds while keeping aspect } this._update_ranges_individually(range_state, { panning, scrolling, maintain_focus }); } ranges() { const x_ranges = new Set(); const y_ranges = new Set(); for (const range of this.frame.x_ranges.values()) { x_ranges.add(range); } for (const range of this.frame.y_ranges.values()) { y_ranges.add(range); } for (const renderer of this.parent.model.data_renderers) { const { coordinates } = renderer; if (coordinates != null) { x_ranges.add(coordinates.x_source); y_ranges.add(coordinates.y_source); } } return { x_ranges: [...x_ranges], y_ranges: [...y_ranges], }; } reset() { const { x_ranges, y_ranges } = this.ranges(); for (const range of x_ranges) { range.reset(); } for (const range of y_ranges) { range.reset(); } this.update_dataranges(); } _update_dataranges(frame) { // Update any DataRange1ds here const bounds = new Map(); const log_bounds = new Map(); let calculate_log_bounds = false; for (const [, xr] of frame.x_ranges) { if (xr instanceof DataRange1d && xr.scale_hint == "log") { calculate_log_bounds = true; } } for (const [, yr] of frame.y_ranges) { if (yr instanceof DataRange1d && yr.scale_hint == "log") { calculate_log_bounds = true; } } for (const renderer of this.parent.auto_ranged_renderers) { const bds = renderer.bounds(this.parent.model.window_axis); bounds.set(renderer.model, bds); if (calculate_log_bounds) { const log_bds = renderer.log_bounds(); log_bounds.set(renderer.model, log_bds); } } let follow_enabled = false; let has_bounds = false; //const {width, height} = frame.bbox const width = frame.x_target.span; const height = frame.y_target.span; let r; if (this.parent.model.match_aspect !== false && width != 0 && height != 0) { r = (1 / this.parent.model.aspect_scale) * (width / height); } for (const [, xr] of frame.x_ranges) { if (xr instanceof DataRange1d) { const bounds_to_use = xr.scale_hint == "log" ? log_bounds : bounds; xr.update(bounds_to_use, 0, this.parent, r); if (xr.follow != null) { follow_enabled = true; } } if (xr.bounds != null) { has_bounds = true; } } for (const [, yr] of frame.y_ranges) { if (yr instanceof DataRange1d) { const bounds_to_use = yr.scale_hint == "log" ? log_bounds : bounds; yr.update(bounds_to_use, 1, this.parent, r); if (yr.follow != null) { follow_enabled = true; } } if (yr.bounds != null) { has_bounds = true; } } if (follow_enabled && has_bounds) { logger.warn("Follow enabled so bounds are unset."); for (const [, xr] of frame.x_ranges) { xr.bounds = null; } for (const [, yr] of frame.y_ranges) { yr.bounds = null; } } } update_dataranges() { this._update_dataranges(this.frame); for (const renderer of this.parent.auto_ranged_renderers) { const { coordinates } = renderer.model; if (coordinates != null) { this._update_dataranges(coordinates); } } if (this.compute_initial() != null) { this.invalidate_dataranges = false; } } compute_initial() { // check for good values for ranges before setting initial range let good_vals = true; const { x_ranges, y_ranges } = this.frame; const xrs = new Map(); const yrs = new Map(); for (const [, range] of x_ranges) { const { start, end } = range; if (isNaN(start + end)) { good_vals = false; break; } xrs.set(range, { start, end }); } if (good_vals) { for (const [, range] of y_ranges) { const { start, end } = range; if (isNaN(start + end)) { good_vals = false; break; } yrs.set(range, { start, end }); } } if (good_vals) { return { xrs, yrs }; } else { if (this.warn_initial_ranges) { logger.warn("could not set initial ranges"); } return null; } } _update_ranges_together(range_state) { // Get weight needed to scale the diff of the range to honor interval limits let weight = 1.0; for (const [rng, range_info] of range_state) { weight = Math.min(weight, this._get_weight_to_constrain_interval(rng, range_info)); } // Apply shared weight to all ranges if (weight < 1) { for (const [rng, range_info] of range_state) { range_info.start = weight * range_info.start + (1 - weight) * rng.start; range_info.end = weight * range_info.end + (1 - weight) * rng.end; } } } _update_ranges_individually(range_state, options) { const { panning, scrolling, maintain_focus } = options; let hit_bound = false; for (const [rng, range_info] of range_state) { // Limit range interval first. Note that for scroll events, // the interval has already been limited for all ranges simultaneously if (!scrolling || maintain_focus) { const weight = this._get_weight_to_constrain_interval(rng, range_info); if (weight < 1) { range_info.start = weight * range_info.start + (1 - weight) * rng.start; range_info.end = weight * range_info.end + (1 - weight) * rng.end; } } // Prevent range from going outside limits // Also ensure that range keeps the same delta when panning/scrolling if (rng.bounds != null) { const [min, max] = rng.computed_bounds; // Make sure the "new_interval" isn't larger than the distance between the bounds, otherwise // the bound could be ignored, see issue #14568 const new_interval = Math.min(Math.abs(range_info.end - range_info.start), Math.abs(max - min)); if (rng.is_reversed) { if (min > range_info.end) { hit_bound = true; range_info.end = min; if (panning || scrolling) { range_info.start = min + new_interval; } } if (max < range_info.start) { hit_bound = true; range_info.start = max; if (panning || scrolling) { range_info.end = max - new_interval; } } } else { if (min > range_info.start) { hit_bound = true; range_info.start = min; if (panning || scrolling) { range_info.end = min + new_interval; } } if (max < range_info.end) { hit_bound = true; range_info.end = max; if (panning || scrolling) { range_info.start = max - new_interval; } } } } } // Cancel the event when hitting a bound while scrolling. This ensures that // the scroll-zoom tool maintains its focus position. Setting `maintain_focus` // to false results in a more "gliding" behavior, allowing one to // zoom out more smoothly, at the cost of losing the focus position. if (scrolling && hit_bound && maintain_focus) { return; } for (const [rng, range_info] of range_state) { rng.have_updated_interactively = true; if (rng.start != range_info.start || rng.end != range_info.end) { rng.setv(range_info); } } } _get_weight_to_constrain_interval(rng, range_info) { // Get the weight by which a range-update can be applied // to still honor the interval limits (including the implicit // max interval imposed by the bounds) const { min_interval } = rng; let { max_interval } = rng; // Express bounds as a max_interval. By doing this, the application of // bounds and interval limits can be applied independent from each-other. if (rng.bounds != null && rng.bounds != "auto") { // check `auto` for type-checking purpose const [min, max] = rng.bounds; if (min != null && max != null) { const max_interval2 = Math.abs(max - min); max_interval = max_interval != null ? Math.min(max_interval, max_interval2) : max_interval2; } } let weight = 1.0; if (min_interval != null || max_interval != null) { const old_interval = Math.abs(rng.end - rng.start); const new_interval = Math.abs(range_info.end - range_info.start); if (min_interval != null && min_interval > 0 && new_interval < min_interval) { weight = (old_interval - min_interval) / (old_interval - new_interval); } if (max_interval != null && max_interval > 0 && new_interval > max_interval) { weight = (max_interval - old_interval) / (new_interval - old_interval); } weight = Math.max(0.0, Math.min(1.0, weight)); } return weight; } } //# sourceMappingURL=range_manager.js.map