UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

324 lines 12.6 kB
import { XYGlyph, XYGlyphView } from "./xy_glyph"; import * as mixins from "../../core/property_mixins"; import * as p from "../../core/properties"; import { UniformScalar, UniformVector } from "../../core/uniforms"; import { Selection } from "../selections/selection"; import { BBox } from "../../core/util/bbox"; import { enumerate } from "../../core/util/iterator"; import { rotate_around, AffineTransform } from "../../core/util/affine"; import { TextBox } from "../../core/graphics"; import { BorderRadius, Padding } from "../common/kinds"; import * as resolve from "../common/resolve"; import { round_rect } from "../common/painting"; import { sqrt, PI } from "../../core/util/math"; class TextAnchorSpec extends p.DataSpec { static __name__ = "TextAnchorSpec"; } class OutlineShapeSpec extends p.DataSpec { static __name__ = "OutlineShapeSpec"; } export class TextView extends XYGlyphView { static __name__ = "TextView"; async _build_labels(text) { return Array.from(text, (value) => { if (value == null) { return null; } else { const text = `${value}`; // TODO: guarantee correct types earlier return new TextBox({ text }); } }); } async _set_lazy_data() { if (this.inherited_text) { this._inherit_attr("labels"); } else { this._define_attr("labels", await this._build_labels(this.text)); } } after_visuals() { super.after_visuals(); const n = this.data_size; const { anchor } = this.base ?? this; const { padding, border_radius } = this.model; const { text_align, text_baseline } = this.visuals.text; if (anchor.is_Scalar() && anchor.value != "auto") { this.anchor_ = new UniformScalar(resolve.anchor(anchor.value), n); } else if (anchor.is_Scalar() && text_align.is_Scalar() && text_baseline.is_Scalar()) { this.anchor_ = new UniformScalar(resolve.text_anchor(anchor.value, text_align.value, text_baseline.value), n); } else { const anchors = new Array(n); for (let i = 0; i < n; i++) { const anchor_i = anchor.get(i); const align_i = text_align.get(i); const baseline_i = text_baseline.get(i); anchors[i] = resolve.text_anchor(anchor_i, align_i, baseline_i); } this.anchor_ = new UniformVector(anchors); } this.padding = resolve.padding(padding); this.border_radius = resolve.border_radius(border_radius); this.swidth = new Float32Array(n); this.sheight = new Float32Array(n); const { left, right, top, bottom } = this.padding; for (const [label, i] of enumerate(this.labels)) { if (label == null) { continue; } label.visuals = this.visuals.text.values(i); label.position = { sx: 0, sy: 0, x_anchor: "left", y_anchor: "top" }; label.align = "auto"; const size = label.size(); const width = left + size.width + right; const height = top + size.height + bottom; this.swidth[i] = width; this.sheight[i] = height; } } _paint(ctx, indices, data) { const { sx, sy, x_offset, y_offset, angle, outline_shape } = { ...this, ...data }; const { text, background_fill, background_hatch, border_line } = this.visuals; const { anchor_: anchor, border_radius, padding } = this; const { labels, swidth, sheight } = this; for (const i of indices) { const sx_i = sx[i] + x_offset.get(i); const sy_i = sy[i] + y_offset.get(i); const angle_i = angle.get(i); const label_i = labels[i]; const shape_i = outline_shape.get(i); if (!isFinite(sx_i + sy_i + angle_i) || label_i == null) { continue; } const swidth_i = swidth[i]; const sheight_i = sheight[i]; const anchor_i = anchor.get(i); const dx_i = anchor_i.x * swidth_i; const dy_i = anchor_i.y * sheight_i; ctx.translate(sx_i, sy_i); ctx.rotate(angle_i); ctx.translate(-dx_i, -dy_i); if (shape_i != "none" && (background_fill.v_doit(i) || background_hatch.v_doit(i) || border_line.v_doit(i))) { const bbox = new BBox({ x: 0, y: 0, width: swidth_i, height: sheight_i }); const visuals = { fill: background_fill, hatch: background_hatch, line: border_line, }; this._paint_shape(ctx, i, shape_i, bbox, visuals, border_radius); } if (text.v_doit(i)) { const { left, top } = padding; ctx.translate(left, top); label_i.visuals = text.values(i); label_i.paint(ctx); ctx.translate(-left, -top); } ctx.translate(dx_i, dy_i); ctx.rotate(-angle_i); ctx.translate(-sx_i, -sy_i); } } _paint_shape(ctx, i, shape, bbox, visuals, border_radius) { ctx.beginPath(); switch (shape) { case "none": { break; } case "box": case "rectangle": { round_rect(ctx, bbox, border_radius); break; } case "square": { const square = (() => { const { x, y, width, height } = bbox; if (width > height) { const dy = (width - height) / 2; return new BBox({ x, y: y - dy, width, height: width }); } else { const dx = (height - width) / 2; return new BBox({ x: x - dx, y, width: height, height }); } })(); round_rect(ctx, square, border_radius); break; } case "circle": { const cx = bbox.x_center; const cy = bbox.y_center; const radius = sqrt(bbox.width ** 2 + bbox.height ** 2) / 2; ctx.arc(cx, cy, radius, 0, 2 * PI, false); break; } case "ellipse": { const cx = bbox.x_center; const cy = bbox.y_center; const rx = bbox.width / 2; const ry = bbox.height / 2; const n = 1.5; const x_0 = rx; const y_0 = ry; const a = sqrt(x_0 ** 2 + x_0 ** (2 / n) * y_0 ** (2 - 2 / n)); const b = sqrt(y_0 ** 2 + y_0 ** (2 / n) * x_0 ** (2 - 2 / n)); ctx.ellipse(cx, cy, a, b, 0, 0, 2 * PI); break; } case "trapezoid": { const { left, right, top, bottom, width } = bbox; const ext = 0.2 * width; ctx.moveTo(left, top); ctx.lineTo(right, top); ctx.lineTo(right + ext, bottom); ctx.lineTo(left - ext, bottom); ctx.closePath(); break; } case "parallelogram": { const { left, right, top, bottom, width } = bbox; const ext = 0.2 * width; ctx.moveTo(left, top); ctx.lineTo(right + ext, top); ctx.lineTo(right, bottom); ctx.lineTo(left - ext, bottom); ctx.closePath(); break; } case "diamond": { const { x_center, y_center, width, height } = bbox; ctx.moveTo(x_center, y_center - height); ctx.lineTo(width + width / 2, y_center); ctx.lineTo(x_center, y_center + height); ctx.lineTo(-width / 2, y_center); ctx.closePath(); break; } case "triangle": { const w = bbox.width; const h = bbox.height; const l = sqrt(3) / 2 * w; const H = h + l; ctx.translate(w / 2, -l); ctx.moveTo(0, 0); ctx.lineTo(H / 2, H); ctx.lineTo(-H / 2, H); ctx.closePath(); ctx.translate(-w / 2, l); break; } } visuals.fill.apply(ctx, i); visuals.hatch.apply(ctx, i); visuals.line.apply(ctx, i); } _hit_point(geometry) { const hit_xy = { x: geometry.sx, y: geometry.sy }; const { sx, sy, x_offset, y_offset, angle, labels } = this; const { anchor_: anchor } = this; const { swidth, sheight } = this; const n = this.data_size; const indices = []; for (let i = 0; i < n; i++) { const sx_i = sx[i] + x_offset.get(i); const sy_i = sy[i] + y_offset.get(i); const angle_i = angle.get(i); const label_i = labels[i]; if (!isFinite(sx_i + sy_i + angle_i) || label_i == null) { continue; } const swidth_i = swidth[i]; const sheight_i = sheight[i]; const anchor_i = anchor.get(i); const dx_i = anchor_i.x * swidth_i; const dy_i = anchor_i.y * sheight_i; const { x, y } = rotate_around(hit_xy, { x: sx_i, y: sy_i }, -angle_i); const left = sx_i - dx_i; const top = sy_i - dy_i; const right = left + swidth_i; const bottom = top + sheight_i; // TODO: consider round corners if (left <= x && x <= right && top <= y && y <= bottom) { indices.push(i); } } return new Selection({ indices }); } rect_i(i) { const { sx, sy, x_offset, y_offset, angle, labels } = this; const { anchor_: anchor } = this; const { swidth, sheight } = this; const sx_i = sx[i] + x_offset.get(i); const sy_i = sy[i] + y_offset.get(i); const angle_i = angle.get(i); const label_i = labels[i]; if (!isFinite(sx_i + sy_i + angle_i) || label_i == null) { return { p0: { x: NaN, y: NaN }, p1: { x: NaN, y: NaN }, p2: { x: NaN, y: NaN }, p3: { x: NaN, y: NaN }, }; } const swidth_i = swidth[i]; const sheight_i = sheight[i]; const anchor_i = anchor.get(i); const dx_i = anchor_i.x * swidth_i; const dy_i = anchor_i.y * sheight_i; const bbox = new BBox({ x: sx_i - dx_i, y: sy_i - dy_i, width: swidth_i, height: sheight_i, }); const { rect } = bbox; if (angle_i == 0) { return rect; } else { const tr = new AffineTransform(); tr.rotate_around(sx_i, sy_i, angle_i); return tr.apply_rect(rect); } } scenterxy(i) { const { p0, p1, p2, p3 } = this.rect_i(i); const sx = (p0.x + p1.x + p2.x + p3.x) / 4; const sy = (p0.y + p1.y + p2.y + p3.y) / 4; return [sx, sy]; } } export class Text extends XYGlyph { static __name__ = "Text"; constructor(attrs) { super(attrs); } static { this.prototype.default_view = TextView; this.mixins([ mixins.TextVector, ["border_", mixins.LineVector], ["background_", mixins.FillVector], ["background_", mixins.HatchVector], ]); this.define(() => ({ text: [p.NullStringSpec, { field: "text" }], angle: [p.AngleSpec, 0], x_offset: [p.NumberSpec, 0], y_offset: [p.NumberSpec, 0], anchor: [TextAnchorSpec, { value: "auto" }], padding: [Padding, 0], border_radius: [BorderRadius, 0], outline_shape: [OutlineShapeSpec, "box"], })); this.override({ border_line_color: null, background_fill_color: null, background_hatch_color: null, }); } } //# sourceMappingURL=text.js.map