UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

290 lines 10.5 kB
import { DataRange } from "./data_range"; import { PaddingUnits, StartEnd } from "../../core/enums"; import { flat_map } from "../../core/util/iterator"; import { clamp } from "../../core/util/math"; import { logger } from "../../core/logging"; import * as bbox from "../../core/util/bbox"; import { compute_renderers } from "../util"; export const auto_ranged = Symbol("auto_ranged"); export function is_auto_ranged(r) { return auto_ranged in r; } export class DataRange1d extends DataRange { static __name__ = "DataRange1d"; constructor(attrs) { super(attrs); } static { this.define(({ Bool, Float, Nullable }) => ({ range_padding: [Float, 0.1], range_padding_units: [PaddingUnits, "percent"], flipped: [Bool, false], follow: [Nullable(StartEnd), null], follow_interval: [Nullable(Float), null], default_span: [Float, 2.0], only_visible: [Bool, false], })); this.internal(({ Enum }) => ({ scale_hint: [Enum("log", "auto"), "auto"], })); } _initial_start; _initial_end; _initial_range_padding; _initial_range_padding_units; _initial_follow; _initial_follow_interval; _initial_default_span; _plot_bounds; have_updated_interactively = false; initialize() { super.initialize(); this._initial_start = isNaN(this.start) ? null : this.start; this._initial_end = isNaN(this.end) ? null : this.end; this._initial_range_padding = this.range_padding; this._initial_range_padding_units = this.range_padding_units; this._initial_follow = this.follow; this._initial_follow_interval = this.follow_interval; this._initial_default_span = this.default_span; this._plot_bounds = new Map(); } connect_signals() { super.connect_signals(); const { range_padding, range_padding_units, flipped, follow, follow_interval, default_span, only_visible, } = this.properties; this.on_change([ range_padding, range_padding_units, flipped, follow, follow_interval, default_span, only_visible, ], () => this._invalidate_dataranges()); } _invalidate_dataranges() { this.have_updated_interactively = false; for (const plot of this.linked_plots) { plot.invalidate_dataranges = true; plot.request_repaint(); } } get min() { return Math.min(this.start, this.end); } get max() { return Math.max(this.start, this.end); } computed_renderers() { // TODO (bev) check that renderers actually configured with this range const { renderers } = this; const all_renderers = flat_map(this.linked_plots, (plot) => plot.auto_ranged_renderers.map((r) => r.model)); return compute_renderers(renderers.length == 0 ? "auto" : renderers, [...all_renderers]); } /*protected*/ _compute_plot_bounds(renderers, bounds) { let result = bbox.empty(); for (const r of renderers) { const rect = bounds.get(r); if (rect != null && (r.visible || !this.only_visible)) { result = bbox.union(result, rect); } } return result; } adjust_bounds_for_aspect(bounds, ratio) { const result = bbox.empty(); let width = bounds.x1 - bounds.x0; if (width <= 0) { width = 1.0; } let height = bounds.y1 - bounds.y0; if (height <= 0) { height = 1.0; } const xcenter = 0.5 * (bounds.x1 + bounds.x0); const ycenter = 0.5 * (bounds.y1 + bounds.y0); if (width < ratio * height) { width = ratio * height; } else { height = width / ratio; } result.x1 = xcenter + 0.5 * width; result.x0 = xcenter - 0.5 * width; result.y1 = ycenter + 0.5 * height; result.y0 = ycenter - 0.5 * height; return result; } /*protected*/ _compute_min_max(plot_bounds, dimension) { let overall = bbox.empty(); for (const [plot, rect] of plot_bounds) { if (plot.model.visible) { overall = bbox.union(overall, rect); } } let min, max; if (dimension == 0) { [min, max] = [overall.x0, overall.x1]; } else { [min, max] = [overall.y0, overall.y1]; } return [min, max]; } /*protected*/ _compute_range(min, max) { const { range_padding } = this; const min_interval = this.min_interval ?? 0; const max_interval = this.max_interval ?? Infinity; let start, end; if (this._initial_start != null) { min = this._initial_start; } if (this._initial_end != null) { max = this._initial_end; } if (this.scale_hint == "log") { if (isNaN(min) || !isFinite(min) || min <= 0) { if (isNaN(max) || !isFinite(max) || max <= 0) { min = 0.1; } else { min = max / 100; } logger.warn(`could not determine minimum data value for log axis, DataRange1d using value ${min}`); } if (isNaN(max) || !isFinite(max) || max <= 0) { if (isNaN(min) || !isFinite(min) || min <= 0) { max = 10; } else { max = min * 100; } logger.warn(`could not determine maximum data value for log axis, DataRange1d using value ${max}`); } let center, span; if (max == min) { span = this.default_span + 0.001; center = Math.log10(min); } else { let log_min, log_max; if (this.range_padding_units == "percent") { log_min = Math.log10(min); log_max = Math.log10(max); span = (log_max - log_min) * (1 + range_padding); } else { log_min = Math.log10(min - range_padding); log_max = Math.log10(max + range_padding); span = log_max - log_min; } center = (log_min + log_max) / 2.0; } span = clamp(span, min_interval, max_interval); start = 10 ** (center - span / 2.0); end = 10 ** (center + span / 2.0); } else { let span; if (max == min) { span = this.default_span; } else { if (this.range_padding_units == "percent") { span = (max - min) * (1 + range_padding); } else { span = (max - min) + 2 * range_padding; } } span = clamp(span, min_interval, max_interval); const center = (max + min) / 2.0; start = center - span / 2.0; end = center + span / 2.0; } let follow_sign = +1; if (this.flipped) { [start, end] = [end, start]; follow_sign = -1; } const follow_interval = this.follow_interval; if (follow_interval != null && Math.abs(start - end) > follow_interval) { if (this.follow == "start") { end = start + follow_sign * follow_interval; } else if (this.follow == "end") { start = end - follow_sign * follow_interval; } } return [start, end]; } update(bounds, dimension, plot, ratio) { if (this.have_updated_interactively) { return; } const renderers = this.computed_renderers(); // update the raw data bounds for all renderers we care about let total_bounds = this._compute_plot_bounds(renderers, bounds); if (ratio != null) { total_bounds = this.adjust_bounds_for_aspect(total_bounds, ratio); } this._plot_bounds.set(plot, total_bounds); // compute the min/mix for our specified dimension const [min, max] = this._compute_min_max(this._plot_bounds.entries(), dimension); // derive start, end from bounds and data range config let [start, end] = this._compute_range(min, max); if (this._initial_start != null) { if (this.scale_hint == "log") { if (this._initial_start > 0) { start = this._initial_start; } } else { start = this._initial_start; } } if (this._initial_end != null) { if (this.scale_hint == "log") { if (this._initial_end > 0) { end = this._initial_end; } } else { end = this._initial_end; } } let needs_emit = false; if (this.bounds == "auto") { this._computed_bounds = [start, end]; needs_emit = true; } // only trigger updates when there are changes const [_start, _end] = [this.start, this.end]; if (start != _start || end != _end) { const new_range = {}; if (start != _start) { new_range.start = start; } if (end != _end) { new_range.end = end; } this.setv(new_range); needs_emit = false; } if (needs_emit) { this.change.emit(); } } reset() { this.have_updated_interactively = false; // change events silenced as PlotView.update_dataranges triggers property callbacks this.setv({ range_padding: this._initial_range_padding, range_padding_units: this._initial_range_padding_units, follow: this._initial_follow, follow_interval: this._initial_follow_interval, default_span: this._initial_default_span, }, { silent: true }); this.change.emit(); } } //# sourceMappingURL=data_range1d.js.map