@bokeh/bokehjs
Version:
Interactive, novel data visualization
200 lines • 6.68 kB
JavaScript
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