@bokeh/bokehjs
Version:
Interactive, novel data visualization
308 lines • 10.6 kB
JavaScript
import { GestureTool, GestureToolView } from "../gestures/gesture_tool";
import { OnOffButton } from "../on_off_button";
import { BoxAnnotation } from "../../annotations/box_annotation";
import { Range } from "../../ranges/range";
import { logger } from "../../../core/logging";
import { assert, unreachable } from "../../../core/util/assert";
import { isNumber } from "../../../core/util/types";
import { tool_icon_range } from "../../../styles/icons.css";
import { Node } from "../../coordinates/node";
import { Enum } from "../../../core/kinds";
const StartGesture = Enum("pan", "tap", "none");
export class RangeToolView extends GestureToolView {
static __name__ = "RangeToolView";
get overlays() {
return [...super.overlays, this.model.overlay];
}
initialize() {
super.initialize();
this.model.update_overlay_from_ranges();
}
connect_signals() {
super.connect_signals();
const update_overlay = () => this.model.update_overlay_from_ranges();
this.on_transitive_change(this.model.properties.x_range, update_overlay);
this.on_transitive_change(this.model.properties.y_range, update_overlay);
this.model.overlay.pan.connect(([state, _]) => {
if (state == "pan") {
this.model.update_ranges_from_overlay();
}
else if (state == "pan:end") {
const ranges = [this.model.x_range, this.model.y_range].filter((r) => r != null);
this.parent.trigger_ranges_update_event(ranges);
}
});
const { active, x_interaction, y_interaction } = this.model.properties;
this.on_change([active, x_interaction, y_interaction], () => {
this.model.update_constraints();
});
}
_mappers() {
const mapper = (units, scale, view, canvas) => {
switch (units) {
case "canvas": return canvas;
case "screen": return view;
case "data": return scale;
}
};
const { overlay } = this.model;
const { frame, canvas } = this.plot_view;
const { x_scale, y_scale } = frame;
const { x_view, y_view } = frame.bbox;
const { x_screen, y_screen } = canvas.bbox;
return {
left: mapper(overlay.left_units, x_scale, x_view, x_screen),
right: mapper(overlay.right_units, x_scale, x_view, x_screen),
top: mapper(overlay.top_units, y_scale, y_view, y_screen),
bottom: mapper(overlay.bottom_units, y_scale, y_view, y_screen),
};
}
_invert_lrtb({ left, right, top, bottom }) {
const lrtb = this._mappers();
const { x_range, y_range } = this.model;
const has_x = x_range != null;
const has_y = y_range != null;
return {
left: has_x ? lrtb.left.invert(left) : this.model.nodes.left,
right: has_x ? lrtb.right.invert(right) : this.model.nodes.right,
top: has_y ? lrtb.top.invert(top) : this.model.nodes.top,
bottom: has_y ? lrtb.bottom.invert(bottom) : this.model.nodes.bottom,
};
}
_compute_limits(curr_point) {
const dims = (() => {
const { x_range, y_range } = this.model;
const has_x = x_range != null;
const has_y = y_range != null;
if (has_x && has_y) {
return "both";
}
else if (has_x) {
return "width";
}
else if (has_y) {
return "height";
}
else {
unreachable();
}
})();
assert(this._base_point != null);
let base_point = this._base_point;
if (this.model.overlay.symmetric) {
const [cx, cy] = base_point;
const [dx, dy] = curr_point;
base_point = [cx - (dx - cx), cy - (dy - cy)];
}
const { frame } = this.plot_view;
return this.model._get_dim_limits(base_point, curr_point, frame, dims);
}
_base_point;
_tap(ev) {
assert(this.model.start_gesture == "tap");
const { sx, sy } = ev;
const { frame } = this.plot_view;
if (!frame.bbox.contains(sx, sy)) {
return;
}
if (this._base_point == null) {
this._base_point = [sx, sy];
}
else {
this._update_overlay(sx, sy);
this._base_point = null;
}
}
_move(ev) {
if (this._base_point != null && this.model.start_gesture == "tap") {
const { sx, sy } = ev;
this._update_overlay(sx, sy);
}
}
_pan_start(ev) {
assert(this.model.start_gesture == "pan");
assert(this._base_point == null);
const { sx, sy } = ev;
const { frame } = this.plot_view;
if (!frame.bbox.contains(sx, sy)) {
return;
}
this._base_point = [sx, sy];
}
_update_overlay(sx, sy) {
const [sxlim, sylim] = this._compute_limits([sx, sy]);
const [[left, right], [top, bottom]] = [sxlim, sylim];
this.model.overlay.update(this._invert_lrtb({ left, right, top, bottom }));
this.model.update_ranges_from_overlay();
}
_pan(ev) {
if (this._base_point == null) {
return;
}
const { sx, sy } = ev;
this._update_overlay(sx, sy);
}
_pan_end(ev) {
if (this._base_point == null) {
return;
}
const { sx, sy } = ev;
this._update_overlay(sx, sy);
this._base_point = null;
}
get _is_selecting() {
return this._base_point != null;
}
_stop() {
this._base_point = null;
}
_keyup(ev) {
if (!this.model.active) {
return;
}
if (ev.key == "Escape" && this._is_selecting) {
this._stop();
}
}
}
const DEFAULT_RANGE_OVERLAY = () => {
return new BoxAnnotation({
syncable: false,
level: "overlay",
visible: true,
editable: true,
propagate_hover: true,
left: NaN,
right: NaN,
top: NaN,
bottom: NaN,
left_limit: Node.frame.left,
right_limit: Node.frame.right,
top_limit: Node.frame.top,
bottom_limit: Node.frame.bottom,
fill_color: "lightgrey",
fill_alpha: 0.5,
line_color: "black",
line_alpha: 1.0,
line_width: 0.5,
line_dash: [2, 2],
});
};
export class RangeTool extends GestureTool {
static __name__ = "RangeTool";
constructor(attrs) {
super(attrs);
}
static {
this.prototype.default_view = RangeToolView;
this.define(({ Bool, Ref, Nullable }) => ({
x_range: [Nullable(Ref(Range)), null],
y_range: [Nullable(Ref(Range)), null],
x_interaction: [Bool, true],
y_interaction: [Bool, true],
overlay: [Ref(BoxAnnotation), DEFAULT_RANGE_OVERLAY],
start_gesture: [StartGesture, "none"],
}));
this.override({
active: true,
});
}
initialize() {
super.initialize();
this.update_constraints();
}
update_constraints() {
this.overlay.editable = this.active;
const has_x = this.x_range != null && this.x_interaction;
const has_y = this.y_range != null && this.y_interaction;
if (has_x && has_y) {
this.overlay.movable = "both";
this.overlay.resizable = "all";
}
else if (has_x) {
this.overlay.movable = "x";
this.overlay.resizable = "x";
}
else if (has_y) {
this.overlay.movable = "y";
this.overlay.resizable = "y";
}
else {
this.overlay.movable = "none";
this.overlay.resizable = "none";
}
const { x_range, y_range } = this;
if (x_range != null) {
this.overlay.min_width = x_range.min_interval ?? 0;
this.overlay.max_width = x_range.max_interval ?? Infinity;
}
if (y_range != null) {
this.overlay.min_height = y_range.min_interval ?? 0;
this.overlay.max_height = y_range.max_interval ?? Infinity;
}
}
update_ranges_from_overlay() {
const { left, right, top, bottom } = this.overlay;
const { x_range, y_range } = this;
const affected_plots = new Set();
const xrs = new Map();
const yrs = new Map();
if (x_range != null && this.x_interaction) {
assert(isNumber(left) && isNumber(right));
xrs.set(x_range, { start: left, end: right });
for (const plot of x_range.linked_plots) {
affected_plots.add(plot);
}
}
if (y_range != null && this.y_interaction) {
assert(isNumber(bottom) && isNumber(top));
yrs.set(y_range, { start: bottom, end: top });
for (const plot of y_range.linked_plots) {
affected_plots.add(plot);
}
}
if (affected_plots.size == 0) {
for (const [range, { start, end }] of [...xrs, ...yrs]) {
range.setv({ start, end });
}
}
else {
for (const plot of affected_plots) {
plot.update_range({ xrs, yrs }, { panning: true, scrolling: true });
}
}
}
nodes = Node.frame.freeze();
update_overlay_from_ranges() {
const { x_range, y_range } = this;
const has_x = x_range != null;
const has_y = y_range != null;
this.overlay.update({
left: has_x ? x_range.start : this.nodes.left,
right: has_x ? x_range.end : this.nodes.right,
top: has_y ? y_range.end : this.nodes.top,
bottom: has_y ? y_range.start : this.nodes.bottom,
});
if (!has_x && !has_y) {
logger.warn("RangeTool not configured with any Ranges.");
this.overlay.clear();
}
}
tool_name = "Range Tool";
tool_icon = tool_icon_range;
get event_type() {
switch (this.start_gesture) {
case "pan": return "pan";
case "tap": return ["tap", "move"];
case "none": return [];
}
}
default_order = 40;
supports_auto() {
return true;
}
tool_button() {
return new OnOffButton({ tool: this });
}
}
//# sourceMappingURL=range_tool.js.map