UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

292 lines 9.1 kB
import { Annotation, AnnotationView } from "./annotation"; import { auto_ranged } from "../ranges/data_range1d"; import * as mixins from "../../core/property_mixins"; import { CoordinateUnits } from "../../core/enums"; import { point_in_poly, dist_to_segment } from "../../core/hittest"; import { Signal } from "../../core/signaling"; import { BBox, empty } from "../../core/util/bbox"; import { minmax2 } from "../../core/util/arrayable"; import { assert } from "../../core/util/assert"; class Polygon { xs; ys; static __name__ = "Polygon"; constructor(xs = [], ys = []) { this.xs = xs; this.ys = ys; assert(xs.length == ys.length); } clone() { return new Polygon(this.xs.slice(), this.ys.slice()); } [Symbol.iterator]() { return this.nodes(); } *nodes() { const { xs, ys, n } = this; for (let i = 0; i < n; i++) { yield [xs[i], ys[i], i]; } } *edges() { const { xs, ys, n } = this; for (let i = 1; i < n; i++) { const p0 = { x: xs[i - 1], y: ys[i - 1] }; const p1 = { x: xs[i], y: ys[i] }; yield [p0, p1, i - 1]; } if (n >= 3) { const p0 = { x: xs[n - 1], y: ys[n - 1] }; const p1 = { x: xs[0], y: ys[0] }; yield [p0, p1, n - 1]; } } contains(x, y) { return point_in_poly(x, y, this.xs, this.ys); } get bbox() { const [x0, x1, y0, y1] = minmax2(this.xs, this.ys); return new BBox({ x0, x1, y0, y1 }); } get n() { return this.xs.length; } translate(dx, dy, ...i) { const poly = this.clone(); const { xs, ys, n } = poly; if (i.length != 0) { for (const j of i) { const k = j % n; xs[k] += dx; ys[k] += dy; } } else { for (let i = 0; i < n; i++) { xs[i] += dx; ys[i] += dy; } } return poly; } } export class PolyAnnotationView extends AnnotationView { static __name__ = "PolyAnnotationView"; poly = new Polygon(); get bbox() { return this.poly.bbox; } connect_signals() { super.connect_signals(); this.connect(this.model.change, () => this.request_paint()); } [auto_ranged] = true; bounds() { const { xs_units, ys_units } = this.model; if (xs_units == "data" && ys_units == "data") { const { xs, ys } = this.model; const [x0, x1, y0, y1] = minmax2(xs, ys); return { x0, x1, y0, y1 }; } else { return empty(); } } log_bounds() { return empty(); } _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 { x: mapper(overlay.xs_units, x_scale, x_view, x_screen), y: mapper(overlay.ys_units, y_scale, y_view, y_screen), }; } _paint(ctx) { const { xs, ys } = this.model; assert(xs.length == ys.length); this.poly = (() => { const { x, y } = this._mappers(); return new Polygon(x.v_compute(xs), y.v_compute(ys)); })(); ctx.beginPath(); for (const [sx, sy] of this.poly) { ctx.lineTo(sx, sy); } const { _is_hovered, visuals } = this; const fill = _is_hovered && visuals.hover_fill.doit ? visuals.hover_fill : visuals.fill; const hatch = _is_hovered && visuals.hover_hatch.doit ? visuals.hover_hatch : visuals.hatch; const line = _is_hovered && visuals.hover_line.doit ? visuals.hover_line : visuals.line; if (this.poly.n >= 3) { ctx.closePath(); fill.apply(ctx); hatch.apply(ctx); } line.apply(ctx); } interactive_hit(sx, sy) { if (!this.model.visible || !this.model.editable) { return false; } return this.poly.contains(sx, sy); } _hit_test(sx, sy) { const { abs } = Math; const EDGE_TOLERANCE = 2.5; const tolerance = Math.max(EDGE_TOLERANCE, this.model.line_width / 2); for (const [px, py, i] of this.poly) { if (abs(px - sx) < tolerance && abs(py - sy) < tolerance) { return { type: "node", i }; } } const spt = { x: sx, y: sy }; let j = null; let dist = Infinity; for (const [p0, p1, i] of this.poly.edges()) { const d = dist_to_segment(spt, p0, p1); if (d < tolerance && d < dist) { dist = d; j = i; } } if (j != null) { return { type: "edge", i: j }; } if (this.poly.contains(sx, sy)) { return { type: "area" }; } return 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 = { poly: this.poly.clone(), target, }; this.model.pan.emit(["pan:start", ev.modifiers]); return true; } } return false; } on_pan(ev) { assert(this._pan_state != null); const spoly = (() => { const { poly, target } = this._pan_state; const { dx, dy } = ev; switch (target.type) { case "node": { const { i } = target; return poly.translate(dx, dy, i); } case "edge": { const { i } = target; return poly.translate(dx, dy, i, i + 1); } case "area": { return poly.translate(dx, dy); } } })(); const { x, y } = this._mappers(); const xs = x.v_invert(spoly.xs); const ys = y.v_invert(spoly.ys); this.model.update({ xs, ys }); 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, hover_fill, hover_hatch } = this.visuals; return hover_line.doit || hover_fill.doit || hover_hatch.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; } switch (target.type) { case "node": return "move"; case "edge": return "move"; case "area": return "move"; } } } export class PolyAnnotation extends Annotation { static __name__ = "PolyAnnotation"; constructor(attrs) { super(attrs); } static { this.prototype.default_view = PolyAnnotationView; this.mixins([ mixins.Line, mixins.Fill, mixins.Hatch, ["hover_", mixins.Line], ["hover_", mixins.Fill], ["hover_", mixins.Hatch], ]); this.define(({ Bool, Float, Arrayable }) => ({ xs: [Arrayable(Float), []], ys: [Arrayable(Float), []], xs_units: [CoordinateUnits, "data"], ys_units: [CoordinateUnits, "data"], editable: [Bool, false], })); this.override({ fill_color: "#fff9ba", fill_alpha: 0.4, line_color: "#cccccc", line_alpha: 0.3, hover_fill_color: null, hover_fill_alpha: 0.4, hover_line_color: null, hover_line_alpha: 0.3, }); } pan = new Signal(this, "pan"); update({ xs, ys }) { this.setv({ xs: xs.slice(), ys: ys.slice(), visible: true }); } clear() { this.setv({ xs: [], ys: [], visible: false }); } } //# sourceMappingURL=poly_annotation.js.map