UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

200 lines 6.68 kB
import { Annotation, AnnotationView } from "./annotation"; import * as mixins from "../../core/property_mixins"; import { CoordinateUnits, Dimension } from "../../core/enums"; import { dist_to_segment } from "../../core/hittest"; import { Signal } from "../../core/signaling"; import { assert } from "../../core/util/assert"; const EDGE_TOLERANCE = 2.5; class Line { p0; p1; static __name__ = "Line"; constructor(p0, p1) { this.p0 = p0; this.p1 = p1; } clone() { return new Line({ ...this.p0 }, { ...this.p1 }); } hit_test(pt, tolerance = EDGE_TOLERANCE) { return dist_to_segment(pt, this.p0, this.p1) < tolerance; } translate(dx, dy) { const { p0, p1 } = this; const dp0 = { x: p0.x + dx, y: p0.y + dy }; const dp1 = { x: p1.x + dx, y: p1.y + dy }; return new Line(dp0, dp1); } } export class SpanView extends AnnotationView { static __name__ = "SpanView"; line; connect_signals() { super.connect_signals(); this.connect(this.model.change, () => this.plot_view.request_paint(this)); } _paint(ctx) { const { location, location_units } = this.model; if (location == null) { return; } function compute(value, units, scale, view, canvas) { switch (units) { case "canvas": return canvas.compute(value); case "screen": return view.compute(value); case "data": return scale.compute(value); } } const { frame, canvas } = this.plot_view; const { x_scale, y_scale } = this.coordinates; let height, sleft, stop, width; if (this.model.dimension == "width") { stop = compute(location, location_units, y_scale, frame.bbox.yview, canvas.bbox.y_screen); sleft = frame.bbox.left; width = frame.bbox.width; height = this.model.line_width; } else { stop = frame.bbox.top; sleft = compute(location, location_units, x_scale, frame.bbox.xview, canvas.bbox.y_screen); width = this.model.line_width; height = frame.bbox.height; } const p0 = { x: sleft, y: stop }; const p1 = { x: sleft + width, y: stop + height }; this.line = new Line(p0, p1); const { _is_hovered, visuals } = this; const line = _is_hovered && visuals.hover_line.doit ? visuals.hover_line : visuals.line; ctx.save(); ctx.beginPath(); this.visuals.line.set_value(ctx); ctx.moveTo(sleft, stop); if (this.model.dimension == "width") { ctx.lineTo(sleft + width, stop); } else { ctx.lineTo(sleft, stop + height); } line.apply(ctx); ctx.restore(); } interactive_hit(sx, sy) { if (!this.model.visible || !this.model.editable) { return false; } return this._hit_test(sx, sy) != null; } _hit_test(sx, sy) { const tolerance = Math.max(EDGE_TOLERANCE, this.model.line_width / 2); return this.line.hit_test({ x: sx, y: sy }, tolerance) ? "edge" : null; } _can_hit(_target) { return true; } _pan_state = null; on_pan_start(ev) { if (this.model.visible && this.model.editable) { const { sx, sy } = ev; const target = this._hit_test(sx, sy); if (target != null && this._can_hit(target)) { this._pan_state = { line: this.line.clone(), target, }; this.model.pan.emit(["pan:start", ev.modifiers]); return true; } } return false; } on_pan(ev) { assert(this._pan_state != null); function invert(sv, units, scale, view, canvas) { switch (units) { case "canvas": return canvas.invert(sv); case "screen": return view.invert(sv); case "data": return scale.invert(sv); } } const sloc = (() => { const { dx, dy } = ev; const { line } = this._pan_state; if (this.model.dimension == "width") { return line.translate(0, dy).p0.y; } else { return line.translate(dx, 0).p0.x; } })(); const loc = (() => { const { location_units } = this.model; const { frame, canvas } = this.plot_view; const { x_scale, y_scale } = this.coordinates; if (this.model.dimension == "width") { return invert(sloc, location_units, y_scale, frame.bbox.yview, canvas.bbox.y_screen); } else { return invert(sloc, location_units, x_scale, frame.bbox.xview, canvas.bbox.y_screen); } })(); this.model.location = loc; this.model.pan.emit(["pan", ev.modifiers]); } on_pan_end(ev) { this._pan_state = null; this.model.pan.emit(["pan:end", ev.modifiers]); } get _has_hover() { const { hover_line } = this.visuals; return hover_line.doit; } _is_hovered = false; on_enter(_ev) { const { _has_hover } = this; if (_has_hover) { this._is_hovered = true; this.request_paint(); } return _has_hover; } on_move(_ev) { } on_leave(_ev) { if (this._has_hover) { this._is_hovered = false; this.request_paint(); } } cursor(sx, sy) { const target = this._pan_state?.target ?? this._hit_test(sx, sy); if (target == null || !this._can_hit(target)) { return null; } return this.model.dimension == "width" ? "ns-resize" : "ew-resize"; } } export class Span extends Annotation { static __name__ = "Span"; constructor(attrs) { super(attrs); } static { this.prototype.default_view = SpanView; this.mixins([ mixins.Line, ["hover_", mixins.Line], ]); this.define(({ Bool, Float, Nullable }) => ({ location: [Nullable(Float), null], location_units: [CoordinateUnits, "data"], dimension: [Dimension, "width"], editable: [Bool, false], })); this.override({ line_color: "black", hover_line_color: null, hover_line_alpha: 0.3, }); } pan = new Signal(this, "pan"); } //# sourceMappingURL=span.js.map