Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for glyph decorations/markers #13619

Open
wants to merge 1 commit into
base: branch-3.5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions bokehjs/src/lib/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export const GeneratorFunction: GeneratorFunctionConstructor = Object.getPrototypeOf(function* () {}).constructor
export const AsyncGeneratorFunction: AsyncGeneratorFunctionConstructor = Object.getPrototypeOf(async function* () {}).constructor

export type int = number

export type uint8 = number
export type uint16 = number
export type uint32 = number
Expand Down
100 changes: 100 additions & 0 deletions bokehjs/src/lib/core/util/curves.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type {Rect} from "../types"
import {qbb, cbb} from "./algorithms"

const {atan2} = Math

abstract class ParametricCurve {

abstract evaluate(t: number): [number, number]
abstract derivative(t: number): [number, number]

tangent(t: number) {
const [dx, dy] = this.derivative(t)
return atan2(dy, dx)
}

abstract bounding_box(): Rect
}

export class QuadraticBezier extends ParametricCurve {

constructor(
readonly x0: number, readonly y0: number,
readonly x1: number, readonly y1: number,
readonly cx0: number, readonly cy0: number,
) {
super()
}

protected _evaluate(t: number, v0: number, v1: number, v2: number) {
return (1 - t)**2*v0 + 2*(1 - t)*t*v1 + t**2*v2
}

evaluate(t: number): [number, number] {
const {x0, cx0, x1} = this
const {y0, cy0, y1} = this
const x = this._evaluate(t, x0, cx0, x1)
const y = this._evaluate(t, y0, cy0, y1)
return [x, y]
}

protected _derivative(t: number, v0: number, v1: number, v2: number) {
return 2*(1 - t)*(v1 - v0) + 2*t*(v2 - v1)
}

derivative(t: number): [number, number] {
const {x0, cx0, x1} = this
const {y0, cy0, y1} = this
const dx = this._derivative(t, x0, cx0, x1)
const dy = this._derivative(t, y0, cy0, y1)
return [dx, dy]
}

bounding_box(): Rect {
const {x0, cx0, x1} = this
const {y0, cy0, y1} = this
return qbb(x0, y0, cx0, cy0, x1, y1)
}
}

export class CubicBezier extends ParametricCurve {

constructor(
readonly x0: number, readonly y0: number,
readonly x1: number, readonly y1: number,
readonly cx0: number, readonly cy0: number,
readonly cx1: number, readonly cy1: number,
) {
super()
}

protected _evaluate(t: number, v0: number, v1: number, v2: number, v3: number) {
return (1 - t)**3*v0 + 3*(1 - t)**2*t*v1 + 3*(1 - t)*t**2*v2 + t**3*v3
}

evaluate(t: number): [number, number] {
const {x0, cx0, cx1, x1} = this
const {y0, cy0, cy1, y1} = this
const x = this._evaluate(t, x0, cx0, cx1, x1)
const y = this._evaluate(t, y0, cy0, cy1, y1)
return [x, y]
}

protected _derivative(t: number, v0: number, v1: number, v2: number, v3: number) {
return 3*(1 - t)**2*(v1 - v0) + 6*(1 - t)*t*(v2 - v1) + 3*t**2*(v3 - v2)
}

derivative(t: number): [number, number] {
const {x0, cx0, cx1, x1} = this
const {y0, cy0, cy1, y1} = this
const dx = this._derivative(t, x0, cx0, cx1, x1)
const dy = this._derivative(t, y0, cy0, cy1, y1)
return [dx, dy]
}

bounding_box(): Rect {
const {x0, cx0, cx1, x1} = this
const {y0, cy0, cy1, y1} = this
return cbb(x0, y0, cx0, cy0, cx1, cy1, x1, y1)
}
}
12 changes: 10 additions & 2 deletions bokehjs/src/lib/core/util/math.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type {AngleUnits, Direction} from "../enums"
import {isObject} from "./types"
import {assert} from "./assert"
import type {XY} from "./bbox"

const {PI, abs, sign} = Math
const {PI, abs, sign, sin, cos} = Math
export {PI}

export function angle_norm(angle: number): number {
if (angle == 0) {
Expand Down Expand Up @@ -41,7 +43,13 @@ export function randomIn(min: number, max?: number): number {
return min + Math.floor(Math.random()*(max - min + 1))
}

export function atan2(start: [number, number], end: [number, number]): number {
export function to_cartesian(radius: number, angle: number): XY {
const x = radius*cos(angle)
const y = radius*sin(angle)
return {x, y}
}

export function slope(start: [number, number], end: [number, number]): number {
/*
* Calculate the angle between a line containing start and end points (composed
* of [x, y] arrays) and the positive x-axis.
Expand Down
4 changes: 2 additions & 2 deletions bokehjs/src/lib/models/annotations/arrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {IterViews} from "core/build_views"
import {build_view} from "core/build_views"
import {Indices} from "core/types"
import * as p from "core/properties"
import {atan2} from "core/util/math"
import {slope} from "core/util/math"

export class ArrowView extends DataAnnotationView {
declare model: Arrow
Expand Down Expand Up @@ -125,7 +125,7 @@ export class ArrowView extends DataAnnotationView {

for (let i = 0; i < n; i++) {
// arrow head runs orthogonal to arrow body (???)
angles[i] = Math.PI/2 + atan2([sx_start[i], sy_start[i]], [sx_end[i], sy_end[i]])
angles[i] = Math.PI/2 + slope([sx_start[i], sy_start[i]], [sx_end[i], sy_end[i]])
}
}

Expand Down
6 changes: 3 additions & 3 deletions bokehjs/src/lib/models/annotations/label.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {TextAnnotation, TextAnnotationView} from "./text_annotation"
import {compute_angle, invert_angle, atan2} from "core/util/math"
import {compute_angle, invert_angle, slope} from "core/util/math"
import type {CoordinateMapper} from "core/util/bbox"
import {CoordinateUnits, AngleUnits, Direction} from "core/enums"
import type * as p from "core/properties"
Expand Down Expand Up @@ -122,8 +122,8 @@ export class LabelView extends TextAnnotationView implements Pannable {
const {angle, base} = this._pan_state
const {origin} = this

const angle0 = atan2([origin.sx, origin.sy], [base.sx, base.sy])
const angle1 = atan2([origin.sx, origin.sy], [base.sx + dx, base.sy + dy])
const angle0 = slope([origin.sx, origin.sy], [base.sx, base.sy])
const angle1 = slope([origin.sx, origin.sy], [base.sx + dx, base.sy + dy])

const da = angle1 - angle0
const na = angle + da
Expand Down
41 changes: 25 additions & 16 deletions bokehjs/src/lib/models/glyphs/arc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {to_screen} from "core/types"
import {Direction} from "core/enums"
import * as p from "core/properties"
import type {Context2d} from "core/util/canvas"
import {to_cartesian, PI} from "core/util/math"

export interface ArcView extends Arc.Data {}

Expand Down Expand Up @@ -43,36 +44,44 @@ export class ArcView extends XYGlyphView {
const start_angle_i = start_angle.get(i)
const end_angle_i = end_angle.get(i)

if (!isFinite(sx_i + sy_i + sradius_i + start_angle_i + end_angle_i))
if (!isFinite(sx_i + sy_i + sradius_i + start_angle_i + end_angle_i)) {
continue

this._render_decorations(ctx, i, sx_i, sy_i, sradius_i, start_angle_i, end_angle_i, anticlock)
}

ctx.beginPath()
ctx.arc(sx_i, sy_i, sradius_i, start_angle_i, end_angle_i, anticlock)

this.visuals.line.apply(ctx, i)

this._render_decorations(ctx, i, sx_i, sy_i, sradius_i, start_angle_i, end_angle_i, anticlock)
}
}

protected _render_decorations(ctx: Context2d, i: number, sx: number, sy: number, sradius: number,
start_angle: number, end_angle: number, _anticlock: boolean): void {

const {sin, cos, PI} = Math

for (const decoration of this.decorations.values()) {
ctx.save()

if (decoration.model.node == "start") {
const x = sradius*cos(start_angle) + sx
const y = sradius*sin(start_angle) + sy
ctx.translate(x, y)
ctx.rotate(start_angle + PI)
} else if (decoration.model.node == "end") {
const x = sradius*Math.cos(end_angle) + sx
const y = sradius*Math.sin(end_angle) + sy
ctx.translate(x, y)
ctx.rotate(end_angle)
switch (decoration.model.node) {
case "start": {
const {x, y} = to_cartesian(sradius, start_angle)
ctx.translate(sx + x, sy + y)
ctx.rotate(start_angle + PI)
break
}
case "middle": {
const angle = (start_angle + end_angle)/2
const {x, y} = to_cartesian(sradius, angle)
ctx.translate(sx + x, sy + y)
ctx.rotate(angle)
break
}
case "end": {
const {x, y} = to_cartesian(sradius, end_angle)
ctx.translate(sx + x, sy + y)
ctx.rotate(end_angle)
break
}
}

decoration.marking.render(ctx, i)
Expand Down
73 changes: 59 additions & 14 deletions bokehjs/src/lib/models/glyphs/bezier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import type {Context2d} from "core/util/canvas"
import {Glyph, GlyphView} from "./glyph"
import {generic_line_vector_legend} from "./utils"
import {inplace} from "core/util/projections"
import {cbb} from "core/util/algorithms"
import * as p from "core/properties"
import {PI} from "core/util/math"
import {CubicBezier} from "core/util/curves"

export interface BezierView extends Bezier.Data {}

Expand All @@ -33,19 +34,17 @@ export class BezierView extends GlyphView {
const cx1_i = cx1[i]
const cy1_i = cy1[i]

if (!isFinite(x0_i + x1_i + y0_i + y1_i + cx0_i + cy0_i + cx1_i + cy1_i))
if (!isFinite(x0_i + x1_i + y0_i + y1_i + cx0_i + cy0_i + cx1_i + cy1_i)) {
index.add_empty()
else {
const {x0, y0, x1, y1} = cbb(x0_i, y0_i, cx0_i, cy0_i, cx1_i, cy1_i, x1_i, y1_i)
} else {
const curve = new CubicBezier(x0_i, y0_i, x1_i, y1_i, cx0_i, cy0_i, cx1_i, cy1_i)
const {x0, y0, x1, y1} = curve.bounding_box()
index.add_rect(x0, y0, x1, y1)
}
}
}

protected _render(ctx: Context2d, indices: number[], data?: Bezier.Data): void {
if (!this.visuals.line.doit)
return

const {sx0, sy0, sx1, sy1, scx0, scy0, scx1, scy1} = {...this, ...data}

for (const i of indices) {
Expand All @@ -58,23 +57,69 @@ export class BezierView extends GlyphView {
const scx1_i = scx1[i]
const scy1_i = scy1[i]

if (!isFinite(sx0_i + sy0_i + sx1_i + sy1_i + scx0_i + scy0_i + scx1_i + scy1_i))
if (!isFinite(sx0_i + sy0_i + sx1_i + sy1_i + scx0_i + scy0_i + scx1_i + scy1_i)) {
continue
}

if (this.visuals.line.doit) {
ctx.beginPath()
ctx.moveTo(sx0_i, sy0_i)
ctx.bezierCurveTo(scx0_i, scy0_i, scx1_i, scy1_i, sx1_i, sy1_i)
this.visuals.line.apply(ctx, i)
}

ctx.beginPath()
ctx.moveTo(sx0_i, sy0_i)
ctx.bezierCurveTo(scx0_i, scy0_i, scx1_i, scy1_i, sx1_i, sy1_i)
if (this.has_decorations) {
const curve = new CubicBezier(sx0_i, sy0_i, sx1_i, sy1_i, scx0_i, scy0_i, scx1_i, scy1_i)
this._render_decorations(ctx, i, curve)
}
}
}

this.visuals.line.apply(ctx, i)
protected _render_decorations(ctx: Context2d, i: number, curve: CubicBezier): void {
for (const decoration of this.decorations.values()) {
const {sx, sy, angle} = (() => {
switch (decoration.model.node) {
case "start": {
const {x0: sx, y0: sy} = curve
const angle = curve.tangent(0.0) + PI/2 + PI
return {sx, sy, angle}
}
case "middle": {
const [sx, sy] = curve.evaluate(0.5)
const angle = curve.tangent(0.5) + PI/2
return {sx, sy, angle}
}
case "end": {
const {x1: sx, y1: sy} = curve
const angle = curve.tangent(1.0) + PI/2
return {sx, sy, angle}
}
}
})()

ctx.translate(sx, sy)
ctx.rotate(angle)
decoration.marking.render(ctx, i)
ctx.rotate(-angle)
ctx.translate(-sx, -sy)
}
}

override draw_legend_for_index(ctx: Context2d, bbox: Rect, index: number): void {
generic_line_vector_legend(this.visuals, ctx, bbox, index)
}

scenterxy(): [number, number] {
throw new Error(`${this}.scenterxy() is not implemented`)
scenterxy(i: number): [number, number] {
const sx0_i = this.sx0[i]
const sy0_i = this.sy0[i]
const sx1_i = this.sx1[i]
const sy1_i = this.sy1[i]
const scx0_i = this.scx0[i]
const scy0_i = this.scy0[i]
const scx1_i = this.scx1[i]
const scy1_i = this.scy1[i]
const curve = new CubicBezier(sx0_i, sy0_i, sx1_i, sy1_i, scx0_i, scy0_i, scx1_i, scy1_i)
return curve.evaluate(0.5)
}
}

Expand Down
3 changes: 2 additions & 1 deletion bokehjs/src/lib/models/glyphs/circle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ export class CircleView extends XYGlyphView {
const sy_i = sy[i]
const sradius_i = sradius[i]

if (!isFinite(sx_i + sy_i + sradius_i))
if (!isFinite(sx_i + sy_i + sradius_i)) {
continue
}

ctx.beginPath()
ctx.arc(sx_i, sy_i, sradius_i, 0, 2*Math.PI, false)
Expand Down
4 changes: 4 additions & 0 deletions bokehjs/src/lib/models/glyphs/glyph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,10 @@ export abstract class GlyphView extends View {
}
}

get has_decorations(): boolean {
return this.decorations.size != 0
}

protected _set_data(_indices: number[] | null): void {}
protected async _set_lazy_data(_indices: number[] | null): Promise<void> {}

Expand Down