From b3a5a6924899d5d5404aacdd658fb39d510511d7 Mon Sep 17 00:00:00 2001 From: Andrew Schechtman-Rook Date: Fri, 17 Sep 2021 19:31:07 -0400 Subject: [PATCH] Intermediate fix for the pick (#49) * added ptplot.ts to manifest, added nodejs to envs * actually kind of have circle-based pick working * pick seems to be all the way working * cleaned up the ts file --- MANIFEST.in | 1 + environment.yml | 1 + environment_minimum_requirements.yml | 1 + ptplot/pick.py | 26 +-- ptplot/pick.ts | 249 +++++---------------------- setup.py | 6 +- 6 files changed, 49 insertions(+), 235 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index af4b77a..4782b3a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include versioneer.py include ptplot/_version.py +include ptplot/pick.ts include environment.yml include environment_minimum_requirements.yml diff --git a/environment.yml b/environment.yml index c0f4d96..43ee0c5 100644 --- a/environment.yml +++ b/environment.yml @@ -7,6 +7,7 @@ dependencies: - bokeh - flake8 - mypy + - nodejs - notebook - numpy - pandas diff --git a/environment_minimum_requirements.yml b/environment_minimum_requirements.yml index 5cf28e4..084a011 100644 --- a/environment_minimum_requirements.yml +++ b/environment_minimum_requirements.yml @@ -7,6 +7,7 @@ dependencies: - bokeh==2.3.3 - flake8==3.8.4 - mypy==0.910 + - nodejs==16.4.1 - notebook==6.2.0 - numpy==1.19.5 - pandas==1.2.0 diff --git a/ptplot/pick.py b/ptplot/pick.py index 56ca035..9c7b03a 100644 --- a/ptplot/pick.py +++ b/ptplot/pick.py @@ -1,30 +1,10 @@ -from bokeh.models.glyph import LineGlyph, FillGlyph +from bokeh.models.glyphs import Circle from bokeh.core.property.dataspec import field -from bokeh.core.properties import AngleSpec, Include, NullDistanceSpec, NumberSpec -from bokeh.core.property_mixins import LineProps, FillProps +from bokeh.core.properties import AngleSpec -class Pick(LineGlyph, FillGlyph): +class Pick(Circle): __implementation__ = "pick.ts" _args = ("x", "y", "rot") - radius = NullDistanceSpec() - x = NumberSpec(default=field("x")) - y = NumberSpec(default=field("y")) rot = AngleSpec(default=field("rot")) - - line_props = Include( - LineProps, - use_prefix=False, - help=""" - The {prop} values for the Pick. - """, - ) - - fill_props = Include( - FillProps, - use_prefix=False, - help=""" - The {prop} values for the Pick. - """, - ) diff --git a/ptplot/pick.ts b/ptplot/pick.ts index 34a7856..a533253 100644 --- a/ptplot/pick.ts +++ b/ptplot/pick.ts @@ -1,96 +1,27 @@ -//All of this adapted from the bokeh bezier glyph -import {FillVector, LineVector} from "core/property_mixins" -import * as visuals from "core/visuals" -import {Rect, FloatArray, ScreenArray} from "core/types" -import {SpatialIndex} from "core/util/spatial" -import {Context2d} from "core/util/canvas" -import {Glyph, GlyphView, GlyphData} from "models/glyphs/glyph" -import {inplace} from "core/util/projections" import * as p from "core/properties" +import {Circle, CircleView, CircleData} from "models/glyphs/circle" +import {Context2d} from "core/util/canvas" -// algorithm adapted from http://stackoverflow.com/a/14429749/3406693 -function _cbb(x0: number, y0: number, - x1: number, y1: number, - x2: number, y2: number, - x3: number, y3: number): [number, number, number, number] { - const tvalues: number[] = [] - const bounds: [number[], number[]] = [[], []] - - for (let i = 0; i <= 2; i++) { - let a, b, c - if (i === 0) { - b = ((6 * x0) - (12 * x1)) + (6 * x2) - a = (((-3 * x0) + (9 * x1)) - (9 * x2)) + (3 * x3) - c = (3 * x1) - (3 * x0) - } else { - b = ((6 * y0) - (12 * y1)) + (6 * y2) - a = (((-3 * y0) + (9 * y1)) - (9 * y2)) + (3 * y3) - c = (3 * y1) - (3 * y0) - } - - if (Math.abs(a) < 1e-12) { // Numerical robustness - if (Math.abs(b) < 1e-12) // Numerical robustness - continue - const t = -c / b - if (0 < t && t < 1) - tvalues.push(t) - continue - } - - const b2ac = (b * b) - (4 * c * a) - const sqrtb2ac = Math.sqrt(b2ac) - - if (b2ac < 0) - continue - - const t1 = (-b + sqrtb2ac) / (2 * a) - if (0 < t1 && t1 < 1) - tvalues.push(t1) - - const t2 = (-b - sqrtb2ac) / (2 * a) - if (0 < t2 && t2 < 1) - tvalues.push(t2) - } - - let j = tvalues.length - const jlen = j - while (j--) { - const t = tvalues[j] - const mt = 1 - t - const x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3) - bounds[0][j] = x - const y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3) - bounds[1][j] = y - } - bounds[0][jlen] = x0 - bounds[1][jlen] = y0 - bounds[0][jlen + 1] = x3 - bounds[1][jlen + 1] = y3 +function _convert_to_bezier(x: number, y: number, radius: number, + rot: number): [number, number, number, number, + number, number] { + //0 degrees is pointing down (in data space), rotation is clockwise + const [xy0_offset, cx_offset, cy_offset] = _generate_offsets(radius) - return [ - Math.min(...bounds[0]), - Math.max(...bounds[1]), - Math.max(...bounds[0]), - Math.min(...bounds[1]), - ] -} -function _convert_to_bezier(x: number, y: number, - rot: number, xy0_offset: number, - cx_offset: number, cy_offset: number): [number, number, number, number, - number, number] { const cosine = -Math.cos(rot * Math.PI / 180) const sine = Math.sin(rot * Math.PI / 180) const x0 = x - xy0_offset * sine - const y0 = y - xy0_offset * cosine + const y0 = y + xy0_offset * cosine const cx0 = x - cx_offset * cosine + cy_offset * sine const cx1 = x + cx_offset * cosine + cy_offset * sine - const cy0 = y + cx_offset * sine + cy_offset * cosine - const cy1 = y - cx_offset * sine + cy_offset * cosine + const cy0 = y - cx_offset * sine - cy_offset * cosine + const cy1 = y + cx_offset * sine - cy_offset * cosine return [x0, y0, cx0, cx1, cy0, cy1] } + function _generate_offsets(radius: number): [number, number, number] { //Empirically these values basically "work" to make a pick with the same //visual radius as a circle @@ -101,115 +32,38 @@ function _generate_offsets(radius: number): [number, number, number] { return [xy0_offset, cx_offset, cy_offset] } -export type PickData = GlyphData & p.UniformsOf & { - radius: p.UniformScalar - _x: FloatArray - _y: FloatArray - rot: p.UniformVector - _x0: FloatArray - _y0: FloatArray - _x1: FloatArray - _y1: FloatArray - _cx0: FloatArray - _cy0: FloatArray - _cx1: FloatArray - _cy1: FloatArray - sx0: ScreenArray - sy0: ScreenArray - sx1: ScreenArray - sy1: ScreenArray - scx0: ScreenArray - scy0: ScreenArray - scx1: ScreenArray - scy1: ScreenArray +export type PickData = CircleData & { + rot: p.UniformVector } export interface PickView extends PickData {} -export class PickView extends GlyphView { +export class PickView extends CircleView { model: Pick visuals: Pick.Visuals - protected _project_data(): void { - inplace.project_xy(this._x, this._y) - inplace.project_xy(this._x0, this._y0) - inplace.project_xy(this._x1, this._y1) - } + protected _render(ctx: Context2d, indices: number[], data?: PickData): void { + const {sx, sy, sradius} = data ?? this - protected _set_data(): void { const rot = this.rot.array - const [xy0_offset, cx_offset, cy_offset] = _generate_offsets(this.radius.value) - const length = this._x.length - this._x0 = new Float64Array(length) - this._y0 = new Float64Array(length) - this._cx0 = new Float64Array(length) - this._cy0 = new Float64Array(length) - this._cx1 = new Float64Array(length) - this._cy1 = new Float64Array(length) - for (let i = 0; i < length; i++) { - [this._x0[i], this._y0[i], this._cx0[i], this._cx1[i], this._cy0[i], this._cy1[i]] = _convert_to_bezier( - this._x[i], this._y[i], rot[i], xy0_offset, cx_offset, cy_offset - ) - } - this._x1 = this._x0 - this._y1 = this._y0 - } - - protected _map_data(): void { - const {x_scale, y_scale} = this.renderer.coordinates - - this.sx0 = this.sx1 = x_scale.v_compute(this._x0) - this.sy0 = this.sy1 = y_scale.v_compute(this._y0) - this.scx0 = x_scale.v_compute(this._cx0) - this.scx1 = x_scale.v_compute(this._cx1) - this.scy0 = y_scale.v_compute(this._cy0) - this.scy1 = y_scale.v_compute(this._cy1) - - } - - protected _index_data(index: SpatialIndex): void { - const {data_size, _x0, _y0, _x1, _y1, _cx0, _cy0, _cx1, _cy1} = this - - for (let i = 0; i < data_size; i++) { - const x0_i = _x0[i] - const y0_i = _y0[i] - const x1_i = _x1[i] - const y1_i = _y1[i] - const cx0_i = _cx0[i] - const cy0_i = _cy0[i] - const cx1_i = _cx1[i] - const cy1_i = _cy1[i] - - if (isNaN(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, x1_i, y1_i, cx0_i, cy0_i, cx1_i, cy1_i) - index.add(x0, y0, x1, y1) - } - } - } - - protected _render(ctx: Context2d, indices: number[], data? : PickData): void { - const {sx0, sy0, sx1, sy1, scx0, scy0, scx1, scy1} = data ?? this - for (const i of indices) { - const sx0_i = sx0[i] - const sy0_i = sy0[i] - const sx1_i = sx1[i] - const sy1_i = sy1[i] - const scx0_i = scx0[i] - const scy0_i = scy0[i] - const scx1_i = scx1[i] - const scy1_i = scy1[i] + const sx_i = sx[i] + const sy_i = sy[i] + const rot_i = rot[i] + const sradius_i = sradius[i] - if (!isFinite(sx0_i + sy0_i + sx1_i + sy1_i + scx0_i + scy0_i + scx1_i + scy1_i)) + if (!isFinite(sx_i + sy_i + sradius_i)) continue + const [x0, y0, cx0, cx1, cy0, cy1] = _convert_to_bezier( + sx_i, sy_i, sradius_i, rot_i + ) + ctx.beginPath() - ctx.moveTo(sx0_i, sy0_i) - ctx.bezierCurveTo(scx0_i, scy0_i, scx1_i, scy1_i, sx1_i, sy1_i) + ctx.moveTo(x0, y0) + ctx.bezierCurveTo(cx0, cy0, cx1, cy1, x0, y0) if (this.visuals.line.doit) { this.visuals.line.set_vectorize(ctx, i) @@ -219,52 +73,30 @@ export class PickView extends GlyphView { this.visuals.fill.set_vectorize(ctx, i) ctx.fill() } + if (this.visuals.hatch.doit) { + this.visuals.hatch.set_vectorize(ctx, i) + ctx.fill() + } } } - - draw_legend_for_index(ctx: Context2d, {x0, y0, x1, y1}: Rect, index: number): void { - const len = index + 1 - - let sx0 = new Float64Array(len) - let sy0 = new Float64Array(len) - let scx0 = new Float64Array(len) - let scx1 = new Float64Array(len) - let scy0 = new Float64Array(len) - let scy1 = new Float64Array(len) - const [xy0_offset, cx_offset, cy_offset] = _generate_offsets( - Math.min(Math.abs(x1 - x0), Math.abs(y1 - y0)) * 0.3 - ); - [sx0[index], sy0[index], scx0[index], scx1[index], scy0[index], scy1[index]] = _convert_to_bezier( - (x0 + x1) / 2, (y0 + y1) / 2, 0, xy0_offset, cx_offset, cy_offset - ); - const sx1 = sx0; - const sy1 = sy0; - this._render(ctx, [index], {sx0, sy0, sx1, sy1, scx0, scx1, scy0, scy1} as any) - } - - scenterxy(): [number, number] { - throw new Error(`${this}.scenterxy() is not implemented`) - } } export namespace Pick { export type Attrs = p.AttrsOf - export type Props = Glyph.Props & { - x: p.CoordinateSpec - y: p.CoordinateSpec + export type Props = Circle.Props & { rot: p.AngleSpec - radius: p.NullDistanceSpec - } & Mixins + } - export type Mixins = LineVector & FillVector + export type Mixins = Circle.Mixins + + export type Visuals = Circle.Visuals - export type Visuals = Glyph.Visuals & {line: visuals.LineVector, fill: visuals.FillVector} } export interface Pick extends Pick.Attrs {} -export class Pick extends Glyph { +export class Pick extends Circle { properties: Pick.Props __view_type__: PickView @@ -276,11 +108,8 @@ export class Pick extends Glyph { this.prototype.default_view = PickView this.define(({}) => ({ - x: [ p.XCoordinateSpec, {field: "x"} ], - y: [ p.YCoordinateSpec, {field: "y"} ], - rot: [ p.AngleSpec, {field: "rot"} ], - radius: [p.NullDistanceSpec, 1] + rot: [ p.AngleSpec, {field: "rot"} ] })) - this.mixins([FillVector, LineVector]) + } } \ No newline at end of file diff --git a/setup.py b/setup.py index e77366f..2afc900 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,8 @@ extras = { 'dev': [ 'black','notebook', 'flake8', 'mypy', 'pytest', 'pytest-cov', 'tox' - ] + ], + 'no_pip_package': ['nodejs'] } requirements, extra_requirements = parse_envs.parse_conda_envs( @@ -20,13 +21,14 @@ "environment.yml", optional_packages=extras ) +extra_requirements.pop('no_pip_package', None) setup( author='Andrew Schechtman-Rook', author_email='footballastronomer@gmail.com', python_requires='>=3.7', classifiers=[ - 'Development Status :: 2 - Pre-Alpha', + 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Natural Language :: English',