diff --git a/bokeh/models/glyph.py b/bokeh/models/glyph.py index dc3f3c391c4..0c1a50c73b1 100644 --- a/bokeh/models/glyph.py +++ b/bokeh/models/glyph.py @@ -66,19 +66,21 @@ def parameters(cls): ''' arg_params = [] + no_more_defaults = False - for arg in cls._args: + for arg in reversed(cls._args): descriptor = cls.lookup(arg) default = descriptor.class_default(cls) + if default is None: + no_more_defaults = True param = Parameter( name=arg, kind=Parameter.POSITIONAL_OR_KEYWORD, - - # for positional arg properties, default=None means no default - default=Parameter.empty if default is None else default + # For positional arg properties, default=None means no default. + default=Parameter.empty if no_more_defaults else default ) typ = descriptor.property._sphinx_type() - arg_params.append((param, typ, descriptor.__doc__)) + arg_params.insert(0, (param, typ, descriptor.__doc__)) # these are not really useful, and should also really be private, just skip them omissions = {'js_event_callbacks', 'js_property_callbacks', 'subscribed_events'} diff --git a/bokeh/models/glyphs.py b/bokeh/models/glyphs.py index dfed4492bc1..ddbe97cbd6b 100644 --- a/bokeh/models/glyphs.py +++ b/bokeh/models/glyphs.py @@ -71,6 +71,7 @@ String, StringSpec, ) +from ..core.property.dataspec import field from ..core.property_mixins import ( FillProps, HatchProps, @@ -144,11 +145,11 @@ class AnnularWedge(XYGlyph, LineGlyph, FillGlyph): # functions derived from this class _args = ('x', 'y', 'inner_radius', 'outer_radius', 'start_angle', 'end_angle', 'direction') - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates of the center of the annular wedges. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates of the center of the annular wedges. """) @@ -191,11 +192,11 @@ class Annulus(XYGlyph, LineGlyph, FillGlyph): # functions derived from this class _args = ('x', 'y', 'inner_radius', 'outer_radius') - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates of the center of the annuli. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates of the center of the annuli. """) @@ -226,11 +227,11 @@ class Arc(XYGlyph, LineGlyph): # functions derived from this class _args = ('x', 'y', 'radius', 'start_angle', 'end_angle', 'direction') - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates of the center of the arcs. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates of the center of the arcs. """) @@ -267,37 +268,37 @@ class Bezier(LineGlyph): # a canonical order for positional args that can be used for any # functions derived from this class - _args = ('x0', 'y0', 'x1', 'y1', 'cx0', 'cy0', 'cx1', 'cy1') + _args = ("x0", "y0", "x1", "y1", "cx0", "cy0", "cx1", "cy1") - x0 = NumberSpec(help=""" + x0 = NumberSpec(default=field("x0"), help=""" The x-coordinates of the starting points. """) - y0 = NumberSpec(help=""" + y0 = NumberSpec(default=field("y0"), help=""" The y-coordinates of the starting points. """) - x1 = NumberSpec(help=""" + x1 = NumberSpec(default=field("x1"), help=""" The x-coordinates of the ending points. """) - y1 = NumberSpec(help=""" + y1 = NumberSpec(default=field("y1"), help=""" The y-coordinates of the ending points. """) - cx0 = NumberSpec(help=""" + cx0 = NumberSpec(default=field("cx0"), help=""" The x-coordinates of first control points. """) - cy0 = NumberSpec(help=""" + cy0 = NumberSpec(default=field("cy0"), help=""" The y-coordinates of first control points. """) - cx1 = NumberSpec(help=""" + cx1 = NumberSpec(default=field("cx1"), help=""" The x-coordinates of second control points. """) - cy1 = NumberSpec(help=""" + cy1 = NumberSpec(default=field("cy1"), help=""" The y-coordinates of second control points. """) @@ -316,11 +317,11 @@ class Ellipse(XYGlyph, LineGlyph, FillGlyph): # functions derived from this class _args = ('x', 'y', 'width', 'height', 'angle') - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates of the centers of the ellipses. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates of the centers of the ellipses. """) @@ -356,15 +357,15 @@ class HArea(FillGlyph, HatchGlyph, LineGlyph): # functions derived from this class _args = ('x1', 'x2', 'y') - x1 = NumberSpec(help=""" + x1 = NumberSpec(default=field("x1"), help=""" The x-coordinates for the points of one side of the area. """) - x2 = NumberSpec(help=""" + x2 = NumberSpec(default=field("x2"), help=""" The x-coordinates for the points of the other side of the area. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates for the points of the area. """) @@ -388,7 +389,7 @@ class HBar(LineGlyph, FillGlyph, HatchGlyph): # functions derived from this class _args = ('y', 'height', 'right', 'left') - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates of the centers of the horizontal bars. """) @@ -511,11 +512,11 @@ def __init__(self, **kwargs): The arrays of scalar data for the images to be colormapped. """) - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates to locate the image anchors. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates to locate the image anchors. """) @@ -572,11 +573,11 @@ class ImageRGBA(XYGlyph): The arrays of RGBA data for the images. """) - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates to locate the image anchors. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates to locate the image anchors. """) @@ -631,11 +632,11 @@ class ImageURL(XYGlyph): the client. """) - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates to locate the image anchors. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates to locate the image anchors. """) @@ -698,11 +699,11 @@ class Line(ConnectedXYGlyph, LineGlyph): __example__ = "examples/reference/models/Line.py" - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates for the points of the line. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates for the points of the line. """) @@ -724,11 +725,11 @@ class MultiLine(LineGlyph): # functions derived from this class _args = ('xs', 'ys') - xs = NumberSpec(help=""" + xs = NumberSpec(default=field("xs"), help=""" The x-coordinates for all the lines, given as a "list of lists". """) - ys = NumberSpec(help=""" + ys = NumberSpec(default=field("ys"), help=""" The y-coordinates for all the lines, given as a "list of lists". """) @@ -754,7 +755,7 @@ class MultiPolygons(LineGlyph, FillGlyph, HatchGlyph): # functions derived from this class _args = ('xs', 'ys') - xs = NumberSpec(help=""" + xs = NumberSpec(default=field("xs"), help=""" The x-coordinates for all the patches, given as a nested list. .. note:: @@ -763,7 +764,7 @@ class MultiPolygons(LineGlyph, FillGlyph, HatchGlyph): one exterior ring optionally followed by ``m`` interior rings (holes). """) - ys = NumberSpec(help=""" + ys = NumberSpec(default=field("ys"), help=""" The y-coordinates for all the patches, given as a "list of lists". .. note:: @@ -803,11 +804,11 @@ def __init__(self, **kwargs): # functions derived from this class _args = ('x', 'y', 'width', 'height', 'angle') - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates of the centers of the ovals. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates of the centers of the ovals. """) @@ -845,7 +846,7 @@ class Patch(ConnectedXYGlyph, LineGlyph, FillGlyph, HatchGlyph): # functions derived from this class _args = ('x', 'y') - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates for the points of the patch. .. note:: @@ -854,7 +855,7 @@ class Patch(ConnectedXYGlyph, LineGlyph, FillGlyph, HatchGlyph): values in the sequence. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates for the points of the patch. .. note:: @@ -892,7 +893,7 @@ class Patches(LineGlyph, FillGlyph, HatchGlyph): # functions derived from this class _args = ('xs', 'ys') - xs = NumberSpec(help=""" + xs = NumberSpec(default=field("xs"), help=""" The x-coordinates for all the patches, given as a "list of lists". .. note:: @@ -901,7 +902,7 @@ class Patches(LineGlyph, FillGlyph, HatchGlyph): values in the sublists. """) - ys = NumberSpec(help=""" + ys = NumberSpec(default=field("ys"), help=""" The y-coordinates for all the patches, given as a "list of lists". .. note:: @@ -970,29 +971,29 @@ class Quadratic(LineGlyph): # a canonical order for positional args that can be used for any # functions derived from this class - _args = ('x0', 'y0', 'x1', 'y1', 'cx', 'cy') + _args = ("x0", "y0", "x1", "y1", "cx", "cy") - x0 = NumberSpec(help=""" + x0 = NumberSpec(default=field("x0"), help=""" The x-coordinates of the starting points. """) - y0 = NumberSpec(help=""" + y0 = NumberSpec(default=field("y0"), help=""" The y-coordinates of the starting points. """) - x1 = NumberSpec(help=""" + x1 = NumberSpec(default=field("x1"), help=""" The x-coordinates of the ending points. """) - y1 = NumberSpec(help=""" + y1 = NumberSpec(default=field("y1"), help=""" The y-coordinates of the ending points. """) - cx = NumberSpec(help=""" + cx = NumberSpec(default=field("cx"), help=""" The x-coordinates of the control points. """) - cy = NumberSpec(help=""" + cy = NumberSpec(default=field("cy"), help=""" The y-coordinates of the control points. """) @@ -1011,11 +1012,11 @@ class Ray(XYGlyph, LineGlyph): # functions derived from this class _args = ('x', 'y', 'length', 'angle') - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates to start the rays. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates to start the rays. """) @@ -1043,11 +1044,11 @@ class Rect(XYGlyph, LineGlyph, FillGlyph): # functions derived from this class _args = ('x', 'y', 'width', 'height', 'angle', 'dilate') - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates of the centers of the rectangles. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates of the centers of the rectangles. """) @@ -1091,19 +1092,19 @@ class Segment(LineGlyph): # functions derived from this class _args = ('x0', 'y0', 'x1', 'y1') - x0 = NumberSpec(help=""" + x0 = NumberSpec(default=field("x0"), help=""" The x-coordinates of the starting points. """) - y0 = NumberSpec(help=""" + y0 = NumberSpec(default=field("y0"), help=""" The y-coordinates of the starting points. """) - x1 = NumberSpec(help=""" + x1 = NumberSpec(default=field("x1"), help=""" The x-coordinates of the ending points. """) - y1 = NumberSpec(help=""" + y1 = NumberSpec(default=field("y1"), help=""" The y-coordinates of the ending points. """) @@ -1128,11 +1129,11 @@ class Step(XYGlyph, LineGlyph): # functions derived from this class _args = ('x', 'y') - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates for the steps. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates for the steps. """) @@ -1160,11 +1161,11 @@ class Text(XYGlyph, TextGlyph): # functions derived from this class _args = ('x', 'y', 'text', 'angle', 'x_offset', 'y_offset') - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates to locate the text anchors. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates to locate the text anchors. """) @@ -1206,15 +1207,15 @@ class VArea(FillGlyph, HatchGlyph): # functions derived from this class _args = ('x', 'y1', 'y2') - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates for the points of the area. """) - y1 = NumberSpec(help=""" + y1 = NumberSpec(default=field("y1"), help=""" The y-coordinates for the points of one side of the area. """) - y2 = NumberSpec(help=""" + y2 = NumberSpec(default=field("y2"), help=""" The y-coordinates for the points of the other side of the area. """) @@ -1237,7 +1238,7 @@ class VBar(LineGlyph, FillGlyph, HatchGlyph): # functions derived from this class _args = ('x', 'width', 'top', 'bottom') - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates of the centers of the vertical bars. """) @@ -1276,11 +1277,11 @@ class Wedge(XYGlyph, LineGlyph, FillGlyph): # functions derived from this class _args = ('x', 'y', 'radius', 'start_angle', 'end_angle', 'direction') - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-coordinates of the points of the wedges. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-coordinates of the points of the wedges. """) diff --git a/bokeh/models/markers.py b/bokeh/models/markers.py index d85eb566456..6668d050e91 100644 --- a/bokeh/models/markers.py +++ b/bokeh/models/markers.py @@ -70,6 +70,7 @@ NumberSpec, ScreenDistanceSpec, ) +from ..core.property.dataspec import field from ..core.property_mixins import FillProps, LineProps from .glyph import FillGlyph, LineGlyph, XYGlyph @@ -130,11 +131,11 @@ class Marker(XYGlyph, LineGlyph, FillGlyph): # functions derived from this class _args = ('x', 'y', 'size', 'angle') - x = NumberSpec(help=""" + x = NumberSpec(default=field("x"), help=""" The x-axis coordinates for the center of the markers. """) - y = NumberSpec(help=""" + y = NumberSpec(default=field("y"), help=""" The y-axis coordinates for the center of the markers. """) diff --git a/bokehjs/src/lib/core/has_props.ts b/bokehjs/src/lib/core/has_props.ts index 9acb90cbdf2..f6cecbea61d 100644 --- a/bokehjs/src/lib/core/has_props.ts +++ b/bokehjs/src/lib/core/has_props.ts @@ -1,7 +1,7 @@ //import {logger} from "./logging" import {View} from "./view" import {Class} from "./class" -import {Arrayable, Attrs, PlainObject} from "./types" +import {Arrayable, Attrs} from "./types" import {Signal0, Signal, Signalable, ISignalable} from "./signaling" import {Struct, Ref, is_ref} from "./util/refs" import * as p from "./properties" @@ -69,8 +69,8 @@ export abstract class HasProps extends Signalable() implements Equals, Printable return __module__ != null ? `${__module__}.${__name__}` : __name__ } - get [Symbol.toStringTag](): string { - return this.constructor.__name__ + static get [Symbol.toStringTag](): string { + return this.__name__ } static init_HasProps(): void { @@ -100,16 +100,14 @@ export abstract class HasProps extends Signalable() implements Equals, Printable return undefined else if (isFunction(default_value)) return default_value + else if (isArray(default_value)) + return () => copy(default_value) + else if (isPlainObject(default_value)) + return () => clone(default_value) else if (!isObject(default_value)) return () => default_value - else { - //logger.warn(`${this.prototype.type}.${attr} uses unwrapped non-primitive default value`) - - if (isArray(default_value)) - return () => copy(default_value) - else - return () => clone(default_value as PlainObject) - } + else + throw new Error(`${default_value} must be explicitly wrapped in a function`) } // TODO: don't use Partial<>, but exclude inherited properties diff --git a/bokehjs/src/lib/core/properties.ts b/bokehjs/src/lib/core/properties.ts index 0f609ec41e3..f9aad64be8b 100644 --- a/bokehjs/src/lib/core/properties.ts +++ b/bokehjs/src/lib/core/properties.ts @@ -1,11 +1,12 @@ import {Signal0} from "./signaling" +import {logger} from "./logging" import type {HasProps} from "./has_props" import * as enums from "./enums" -import {Arrayable, NumberArray} from "./types" +import {Arrayable, NumberArray, ColorArray} from "./types" import * as types from "./types" import {includes, repeat} from "./util/array" import {map} from "./util/arrayable" -import {is_color} from "./util/color" +import {is_color, color2rgba, encode_rgba} from "./util/color" import {isBoolean, isNumber, isString, isArray, isPlainObject} from "./util/types" import {Factor/*, OffsetFactor*/} from "../models/ranges/factor_range" import {ColumnarDataSource} from "../models/sources/columnar_data_source" @@ -368,25 +369,35 @@ export abstract class VectorSpec = Vector> extends Pro } array(source: ColumnarDataSource): Arrayable { - let ret: any + let array: Arrayable + + const length = source.get_length() ?? 1 if (this.spec.field != null) { - ret = this.normalize(source.get_column(this.spec.field)) - if (ret == null) - throw new Error(`attempted to retrieve property array for nonexistent field '${this.spec.field}'`) + const column = source.get_column(this.spec.field) + if (column != null) + array = this.normalize(column) + else { + logger.warn(`attempted to retrieve property array for nonexistent field '${this.spec.field}'`) + const missing = new NumberArray(length) + missing.fill(NaN) + array = missing + } } else if (this.spec.expr != null) { - ret = this.normalize(this.spec.expr.v_compute(source)) + array = this.normalize(this.spec.expr.v_compute(source)) } else { - let length = source.get_length() - if (length == null) - length = 1 const value = this.value(false) // don't apply any spec transform - ret = repeat(value, length) + if (isNumber(value)) { + const values = new NumberArray(length) + values.fill(value) + array = values + } else + array = repeat(value, length) } if (this.spec.transform != null) - ret = this.spec.transform.v_compute(ret) - return ret + array = this.spec.transform.v_compute(array) + return array } } @@ -422,6 +433,23 @@ export abstract class NumberUnitsSpec extends UnitsSpec { } } +export abstract class BaseCoordinateSpec extends DataSpec { + readonly dimension: "x" | "y" +} + +export abstract class CoordinateSpec extends BaseCoordinateSpec {} +export abstract class CoordinateSeqSpec extends BaseCoordinateSpec | Arrayable> {} +export abstract class CoordinateSeqSeqSeqSpec extends BaseCoordinateSpec {} + +export class XCoordinateSpec extends CoordinateSpec { readonly dimension = "x" } +export class YCoordinateSpec extends CoordinateSpec { readonly dimension = "y" } + +export class XCoordinateSeqSpec extends CoordinateSeqSpec { readonly dimension = "x" } +export class YCoordinateSeqSpec extends CoordinateSeqSpec { readonly dimension = "y" } + +export class XCoordinateSeqSeqSeqSpec extends CoordinateSeqSeqSeqSpec { readonly dimension = "x" } +export class YCoordinateSeqSeqSeqSpec extends CoordinateSeqSeqSeqSpec { readonly dimension = "y" } + export class AngleSpec extends NumberUnitsSpec { get default_units(): enums.AngleUnits { return "rad" as "rad" } get valid_units(): enums.AngleUnits[] { return enums.AngleUnits } @@ -451,11 +479,19 @@ export class NumberSpec extends DataSpec { } } -export class CoordinateSpec extends DataSpec {} - -export class CoordinateSeqSpec extends DataSpec {} - -export class ColorSpec extends DataSpec {} +export class ColorSpec extends DataSpec { + array(source: ColumnarDataSource): ColorArray { + const colors = super.array(source) + const n = colors.length + const array = new ColorArray(n) + for (let i = 0; i < n; i++) { + const color = colors[i] as types.Color | null + const rgba = color2rgba(color) + array[i] = encode_rgba(rgba) + } + return array + } +} export class FontSizeSpec extends DataSpec {} diff --git a/bokehjs/src/lib/core/types.ts b/bokehjs/src/lib/core/types.ts index 7d79473ea0f..70a4d4410bd 100644 --- a/bokehjs/src/lib/core/types.ts +++ b/bokehjs/src/lib/core/types.ts @@ -7,6 +7,62 @@ export {TypedArray} from "./util/ndarray" export type NumberArray = Float64Array export const NumberArray = Float64Array +export type ColorArray = Uint32Array +export const ColorArray = Uint32Array + +import {equals, Equals, Comparator} from "core/util/eq" + +export class RaggedArray implements Equals { + + static [Symbol.toStringTag] = "RaggedArray" + + constructor(readonly offsets: Uint32Array, readonly array: NumberArray) {} + + [equals](that: this, cmp: Comparator): boolean { + return cmp.arrays(this.offsets, that.offsets) && cmp.arrays(this.array, that.array) + } + + get length(): number { + return this.offsets.length + } + + clone(): RaggedArray { + return new RaggedArray(new Uint32Array(this.offsets), new NumberArray(this.array)) + } + + static from(items: number[][]): RaggedArray { + const n = items.length + const offsets = new Uint32Array(n) + let offset = 0 + for (let i = 0; i < n; i++) { + const length = items[i].length + offsets[i] = offset + offset += length + } + const array = new NumberArray(offset) + for (let i = 0; i < n; i++) { + array.set(items[i], offsets[i]) + } + return new RaggedArray(offsets, array) + } + + *[Symbol.iterator](): IterableIterator { + const {offsets, length} = this + for (let i = 0; i < length; i++) { + yield this.array.subarray(offsets[i], offsets[i + 1]) + } + } + + get(i: number): NumberArray { + const {offsets} = this + return this.array.subarray(offsets[i], offsets[i + 1]) + } + + set(i: number, array: ArrayLike): void { + this.array.set(array, this.offsets[i]) + } +} + export type Arrayable = { readonly length: number [n: number]: T diff --git a/bokehjs/src/lib/core/util/canvas.ts b/bokehjs/src/lib/core/util/canvas.ts index 27839b7acc6..41f19b759f1 100644 --- a/bokehjs/src/lib/core/util/canvas.ts +++ b/bokehjs/src/lib/core/util/canvas.ts @@ -1,33 +1,16 @@ export type Context2d = { - setLineDashOffset(offset: number): void - getLineDashOffset(): number setImageSmoothingEnabled(value: boolean): void getImageSmoothingEnabled(): boolean measureText(text: string): TextMetrics & {ascent: number} + lineDash: number[] } & CanvasRenderingContext2D function fixup_line_dash(ctx: any): void { - if (!ctx.setLineDash) { - ctx.setLineDash = (dash: any): void => { - ctx.mozDash = dash - ctx.webkitLineDash = dash - } - } - if (!ctx.getLineDash) { - ctx.getLineDash = (): any => { - return ctx.mozDash - } - } -} - -function fixup_line_dash_offset(ctx: any): void { - ctx.setLineDashOffset = (offset: number): void => { - ctx.lineDashOffset = offset - ctx.mozDashOffset = offset - ctx.webkitLineDashOffset = offset - } - ctx.getLineDashOffset = (): number => { - return ctx.mozDashOffset + if (typeof ctx.lineDash === "undefined") { + Object.defineProperty(ctx, "lineDash", { + get: () => ctx.getLineDash(), + set: (segments: number[]) => ctx.setLineDash(segments), + }) } } @@ -92,7 +75,6 @@ function fixup_ellipse(ctx: any): void { export function fixup_ctx(ctx: any): void { fixup_line_dash(ctx) - fixup_line_dash_offset(ctx) fixup_image_smoothing(ctx) fixup_measure_text(ctx) fixup_ellipse(ctx) diff --git a/bokehjs/src/lib/core/util/color.ts b/bokehjs/src/lib/core/util/color.ts index c743ee42d2c..151e7e55c09 100644 --- a/bokehjs/src/lib/core/util/color.ts +++ b/bokehjs/src/lib/core/util/color.ts @@ -33,9 +33,22 @@ export function color2hex(color: string): string { return color } +// each component is in [0, 1] range export type RGBA = [number, number, number, number] -export function color2rgba(color: string, alpha: number = 1.0): RGBA { +export function encode_rgba([r, g, b, a]: RGBA): number { + return (r*255 | 0) << 24 | (g*255 | 0) << 16 | (b*255 | 0) << 8 | (a*255 | 0) +} + +export function decode_rgba(rgba: number): RGBA { + const r = ((rgba >> 24) & 0xff) / 255 + const g = ((rgba >> 16) & 0xff) / 255 + const b = ((rgba >> 8) & 0xff) / 255 + const a = ((rgba >> 0) & 0xff) / 255 + return [r, g, b, a] +} + +export function color2rgba(color: string | null, alpha: number = 1.0): RGBA { if (!color) // NaN, null, '', etc. return [0, 0, 0, 0] // transparent // Convert to hex and then to clean version of 6 or 8 chars @@ -54,15 +67,6 @@ export function color2rgba(color: string, alpha: number = 1.0): RGBA { return rgba.slice(0, 4) as RGBA } -export function color2css(color: string, alpha: number = 1.0): string { - if (alpha == 1.0) - return color - else { - const [r, g, b, a] = color2rgba(color, alpha) - return `rgba(${r*255},${g*255},${b*255},${a})` - } -} - export function valid_rgb(value: string): boolean { let params: {start: string, len: number, alpha: boolean} switch (value.substring(0, 4)) { diff --git a/bokehjs/src/lib/core/util/projections.ts b/bokehjs/src/lib/core/util/projections.ts index 6a6370a505a..aa8bd14d16a 100644 --- a/bokehjs/src/lib/core/util/projections.ts +++ b/bokehjs/src/lib/core/util/projections.ts @@ -46,17 +46,35 @@ export function in_bounds(value: number, dimension: LatLon): boolean { return min < value && value < max } +export namespace inplace { + export function project_xy(x: Arrayable, y: Arrayable, merc_x?: Arrayable, merc_y?: Arrayable): void { + const n = min(x.length, y.length) + merc_x = merc_x ?? x + merc_y = merc_y ?? y + for (let i = 0; i < n; i++) { + const xi = x[i] + const yi = y[i] + const [merc_xi, merc_yi] = wgs84_mercator.compute(xi, yi) + merc_x[i] = merc_xi + merc_y[i] = merc_yi + } + } + + export function project_xsys(xs: Arrayable[], ys: Arrayable[], merc_xs?: Arrayable[], merc_ys?: Arrayable[]): void { + const n = min(xs.length, ys.length) + merc_xs = merc_xs ?? xs + merc_ys = merc_ys ?? ys + for (let i = 0; i < n; i++) { + project_xy(xs[i], ys[i], merc_xs[i], merc_ys[i]) + } + } +} + export function project_xy(x: Arrayable, y: Arrayable): [NumberArray, NumberArray] { const n = min(x.length, y.length) const merc_x = new NumberArray(n) const merc_y = new NumberArray(n) - for (let i = 0; i < n; i++) { - const xi = x[i] - const yi = y[i] - const [merc_xi, merc_yi] = wgs84_mercator.compute(xi, yi) - merc_x[i] = merc_xi - merc_y[i] = merc_yi - } + inplace.project_xy(x, y, merc_x, merc_y) return [merc_x, merc_y] } diff --git a/bokehjs/src/lib/core/visuals.ts b/bokehjs/src/lib/core/visuals.ts index c51b40ca410..55c86015269 100644 --- a/bokehjs/src/lib/core/visuals.ts +++ b/bokehjs/src/lib/core/visuals.ts @@ -1,14 +1,21 @@ import * as mixins from "./property_mixins" import * as p from "./properties" -import {color2css} from "./util/color" +import {color2rgba, decode_rgba} from "./util/color" import {Context2d} from "./util/canvas" import {Class} from "./class" -import {Arrayable} from "./types" -import {map} from "./util/arrayable" +import {Arrayable, Color} from "./types" +import {isString} from "./util/types" +import {subselect} from "./util/arrayable" import {LineJoin, LineCap, FontStyle, TextAlign, TextBaseline} from "./enums" import {HasProps} from "./has_props" import {ColumnarDataSource} from "models/sources/columnar_data_source" +import {Texture} from "models/textures/texture" + +function color2css(color: Color | number, alpha: number): string { + const [r, g, b, a] = isString(color) ? color2rgba(color) : decode_rgba(color) + return `rgba(${r*255}, ${g*255}, ${b*255}, ${a == 1.0 ? alpha : a})` +} function _horz(ctx: Context2d, h: number, h2: number): void { ctx.moveTo(0, h2+0.5) @@ -38,8 +45,7 @@ function _get_canvas(size: number): HTMLCanvasElement { return canvas } -export type Color = string -function create_hatch_canvas(hatch_pattern: mixins.HatchPattern, hatch_color: Color, hatch_scale: number, hatch_weight: number): HTMLCanvasElement { +function create_hatch_canvas(hatch_pattern: mixins.HatchPattern, hatch_color: Color, hatch_alpha: number, hatch_scale: number, hatch_weight: number): HTMLCanvasElement { const h = hatch_scale const h2 = h / 2 const h4 = h2 / 2 @@ -47,8 +53,8 @@ function create_hatch_canvas(hatch_pattern: mixins.HatchPattern, hatch_color: Co const canvas = _get_canvas(hatch_scale) const ctx = canvas.getContext("2d")! as Context2d - ctx.strokeStyle = hatch_color - ctx.lineCap="square" + ctx.strokeStyle = color2css(hatch_color, hatch_alpha) + ctx.lineCap = "square" ctx.fillStyle = hatch_color ctx.lineWidth = hatch_weight @@ -225,7 +231,7 @@ export abstract class ContextProperties { get_array(attr: string): Arrayable { const array = this.cache[attr + "_array"] as Arrayable if (this.all_indices != null) { - return map(this.all_indices, (i) => array[i]) + return subselect(array, this.all_indices) } else { return array } @@ -252,13 +258,15 @@ export class Line extends ContextProperties { readonly line_dash_offset: p.Number set_value(ctx: Context2d): void { - ctx.strokeStyle = this.line_color.value() - ctx.globalAlpha = this.line_alpha.value() - ctx.lineWidth = this.line_width.value() - ctx.lineJoin = this.line_join.value() - ctx.lineCap = this.line_cap.value() - ctx.setLineDash(this.line_dash.value()) - ctx.setLineDashOffset(this.line_dash_offset.value()) + const color = this.line_color.value() + const alpha = this.line_alpha.value() + + ctx.strokeStyle = color2css(color, alpha) + ctx.lineWidth = this.line_width.value() + ctx.lineJoin = this.line_join.value() + ctx.lineCap = this.line_cap.value() + ctx.lineDash = this.line_dash.value() + ctx.lineDashOffset = this.line_dash_offset.value() } get doit(): boolean { @@ -268,26 +276,20 @@ export class Line extends ContextProperties { } protected _set_vectorize(ctx: Context2d, i: number): void { - this.cache_select("line_color", i) - ctx.strokeStyle = this.cache.line_color - - this.cache_select("line_alpha", i) - ctx.globalAlpha = this.cache.line_alpha - - this.cache_select("line_width", i) - ctx.lineWidth = this.cache.line_width - - this.cache_select("line_join", i) - ctx.lineJoin = this.cache.line_join - - this.cache_select("line_cap", i) - ctx.lineCap = this.cache.line_cap - - this.cache_select("line_dash", i) - ctx.setLineDash(this.cache.line_dash) - - this.cache_select("line_dash_offset", i) - ctx.setLineDashOffset(this.cache.line_dash_offset) + const color = this.cache_select("line_color", i) + const alpha = this.cache_select("line_alpha", i) + const width = this.cache_select("line_width", i) + const join = this.cache_select("line_join", i) + const cap = this.cache_select("line_cap", i) + const dash = this.cache_select("line_dash", i) + const offset = this.cache_select("line_dash_offset", i) + + ctx.strokeStyle = color2css(color, alpha) + ctx.lineWidth = width + ctx.lineJoin = join + ctx.lineCap = cap + ctx.lineDash = dash + ctx.lineDashOffset = offset } color_value(): string { @@ -303,8 +305,10 @@ export class Fill extends ContextProperties { readonly fill_alpha: p.NumberSpec set_value(ctx: Context2d): void { - ctx.fillStyle = this.fill_color.value() - ctx.globalAlpha = this.fill_alpha.value() + const color = this.fill_color.value() + const alpha = this.fill_alpha.value() + + ctx.fillStyle = color2css(color, alpha) } get doit(): boolean { @@ -313,11 +317,10 @@ export class Fill extends ContextProperties { } protected _set_vectorize(ctx: Context2d, i: number): void { - this.cache_select("fill_color", i) - ctx.fillStyle = this.cache.fill_color + const color = this.cache_select("fill_color", i) + const alpha = this.cache_select("fill_alpha", i) - this.cache_select("fill_alpha", i) - ctx.globalAlpha = this.cache.fill_alpha + ctx.fillStyle = color2css(color, alpha) } color_value(): string { @@ -338,17 +341,19 @@ export class Hatch extends ContextProperties { cache_select(name: string, i: number): any { let value: any if (name == "pattern") { - this.cache_select("hatch_color", i) - this.cache_select("hatch_scale", i) - this.cache_select("hatch_pattern", i) - this.cache_select("hatch_weight", i) - const {hatch_color, hatch_scale, hatch_pattern, hatch_weight, hatch_extra} = this.cache - if (hatch_extra != null && hatch_extra.hasOwnProperty(hatch_pattern)) { - const custom = hatch_extra[hatch_pattern] - this.cache.pattern = custom.get_pattern(hatch_color, hatch_scale, hatch_weight) + const color = this.cache_select("hatch_color", i) + const alpha = this.cache_select("hatch_alpha", i) + const scale = this.cache_select("hatch_scale", i) + const pattern = this.cache_select("hatch_pattern", i) + const weight = this.cache_select("hatch_weight", i) + + const {hatch_extra} = this.cache + if (hatch_extra != null && hatch_extra.hasOwnProperty(pattern)) { + const custom: Texture = hatch_extra[pattern] + this.cache.pattern = custom.get_pattern(color, alpha, scale, weight) } else { this.cache.pattern = (ctx: Context2d) => { - const canvas = create_hatch_canvas(hatch_pattern, hatch_color, hatch_scale, hatch_weight) + const canvas = create_hatch_canvas(pattern, color, alpha, scale, weight) return ctx.createPattern(canvas, 'repeat')! } } @@ -392,9 +397,6 @@ export class Hatch extends ContextProperties { protected _set_vectorize(ctx: Context2d, i: number): void { this.cache_select("pattern", i) ctx.fillStyle = this.cache.pattern(ctx) - - this.cache_select("hatch_alpha", i) - ctx.globalAlpha = this.cache.hatch_alpha } color_value(): string { @@ -446,9 +448,11 @@ export class Text extends ContextProperties { } set_value(ctx: Context2d): void { + const color = this.text_color.value() + const alpha = this.text_alpha.value() + + ctx.fillStyle = color2css(color, alpha) ctx.font = this.font_value() - ctx.fillStyle = this.text_color.value() - ctx.globalAlpha = this.text_alpha.value() ctx.textAlign = this.text_align.value() ctx.textBaseline = this.text_baseline.value() } @@ -459,20 +463,16 @@ export class Text extends ContextProperties { } protected _set_vectorize(ctx: Context2d, i: number): void { - this.cache_select("font", i) - ctx.font = this.cache.font - - this.cache_select("text_color", i) - ctx.fillStyle = this.cache.text_color - - this.cache_select("text_alpha", i) - ctx.globalAlpha = this.cache.text_alpha - - this.cache_select("text_align", i) - ctx.textAlign = this.cache.text_align - - this.cache_select("text_baseline", i) - ctx.textBaseline = this.cache.text_baseline + const color = this.cache_select("text_color", i) + const alpha = this.cache_select("text_alpha", i) + const font = this.cache_select("font", i) + const align = this.cache_select("text_align", i) + const baseline = this.cache_select("text_baseline", i) + + ctx.fillStyle = color2css(color, alpha) + ctx.font = font + ctx.textAlign = align + ctx.textBaseline = baseline } } diff --git a/bokehjs/src/lib/models/glyphs/bezier.ts b/bokehjs/src/lib/models/glyphs/bezier.ts index fcc6a0cab7b..9f7c61ab1ec 100644 --- a/bokehjs/src/lib/models/glyphs/bezier.ts +++ b/bokehjs/src/lib/models/glyphs/bezier.ts @@ -5,6 +5,7 @@ import {SpatialIndex} from "core/util/spatial" import {Context2d} from "core/util/canvas" import {Glyph, GlyphView, GlyphData} from "./glyph" import {generic_line_legend} from "./utils" +import {inplace} from "core/util/projections" import * as p from "core/properties" // algorithm adapted from http://stackoverflow.com/a/14429749/3406693 @@ -101,6 +102,11 @@ export class BezierView extends GlyphView { model: Bezier visuals: Bezier.Visuals + protected _project_data(): void { + inplace.project_xy(this._x0, this._y0) + inplace.project_xy(this._x1, this._y1) + } + protected _index_data(index: SpatialIndex): void { const {data_size} = this @@ -173,7 +179,16 @@ export class Bezier extends Glyph { static init_Bezier(): void { this.prototype.default_view = BezierView - this.coords([['x0', 'y0'], ['x1', 'y1'], ['cx0', 'cy0'], ['cx1', 'cy1']]) + this.define({ + x0: [ p.XCoordinateSpec, {field: "x0"} ], + y0: [ p.YCoordinateSpec, {field: "y0"} ], + x1: [ p.XCoordinateSpec, {field: "x1"} ], + y1: [ p.YCoordinateSpec, {field: "y1"} ], + cx0: [ p.XCoordinateSpec, {field: "cx0"} ], + cy0: [ p.YCoordinateSpec, {field: "cy0"} ], + cx1: [ p.XCoordinateSpec, {field: "cx1"} ], + cy1: [ p.YCoordinateSpec, {field: "cy1"} ], + }) this.mixins(LineVector) } } diff --git a/bokehjs/src/lib/models/glyphs/glyph.ts b/bokehjs/src/lib/models/glyphs/glyph.ts index 69a9cb3cfb1..c4de8c32a9d 100644 --- a/bokehjs/src/lib/models/glyphs/glyph.ts +++ b/bokehjs/src/lib/models/glyphs/glyph.ts @@ -1,7 +1,6 @@ import {HitTestResult} from "core/hittest" import * as p from "core/properties" import * as bbox from "core/util/bbox" -import * as proj from "core/util/projections" import * as visuals from "core/visuals" import * as geometry from "core/geometry" import {Context2d} from "core/util/canvas" @@ -9,12 +8,9 @@ import {View} from "core/view" import {Model} from "../../model" import {Anchor} from "core/enums" import {logger} from "core/logging" -import {Arrayable, Rect, NumberArray} from "core/types" -import {map, subselect} from "core/util/arrayable" -import {extend} from "core/util/object" -import {isArray, isTypedArray} from "core/util/types" +import {Arrayable, Rect, NumberArray, RaggedArray} from "core/types" +import {map, max, subselect} from "core/util/arrayable" import {SpatialIndex} from "core/util/spatial" -import {LineView} from "./line" import {Scale} from "../scales/scale" import {FactorRange} from "../ranges/factor_range" import {Selection} from "../selections/selection" @@ -238,97 +234,53 @@ export abstract class GlyphView extends View { return new Selection({indices}) } + protected _project_data(): void {} + set_data(source: ColumnarDataSource, indices: number[], indices_to_update: number[] | null): void { - let data = this.model.materialize_dataspecs(source) - - if (indices && !(this instanceof LineView)) { - const data_subset: {[key: string]: any} = {} - for (const k in data) { - const v = data[k] - if (k.charAt(0) === '_') - data_subset[k] = subselect(v as Arrayable, indices) - else - data_subset[k] = v + const {x_range, y_range} = this.renderer.scope + + this._data_size = source.get_length() ?? 1 + + for (const prop of this.model) { + if (!(prop instanceof p.VectorSpec)) + continue + + // this skips optional properties like radius for circles + if (prop.optional && prop.spec.value == null && !prop.dirty) + continue + + const name = prop.attr + let array = subselect(prop.array(source), indices) + + if (prop instanceof p.BaseCoordinateSpec) { + const range = prop.dimension == "x" ? x_range : y_range + if (range instanceof FactorRange) { + if (prop instanceof p.CoordinateSpec) { + array = range.v_synthetic(array as any) + } else if (prop instanceof p.CoordinateSeqSpec) { + for (let i = 0; i < array.length; i++) { + array[i] = range.v_synthetic(array[i] as any) + } + } + } + + if (prop instanceof p.CoordinateSeqSpec) { + array = RaggedArray.from(array as any) as any + } + } else if (prop instanceof p.DistanceSpec) { + (this as any)[`max_${name}`] = max(array as any) } - data = data_subset - } - let data_size = Infinity - for (const k in data) { - if (k.charAt(0) === '_') - data_size = Math.min(data_size, (data[k] as any).length) + (this as any)[`_${name}`] = array } - if (data_size != Infinity) - this._data_size = data_size - else - this._data_size = 0 // XXX: this only happens in degenerate unit tests - - const self = this as any - extend(self, data) - // TODO (bev) Should really probably delegate computing projected - // coordinates to glyphs, instead of centralizing here in one place. if (this.renderer.plot_view.model.use_map) { - if (self._x != null) - [self._x, self._y] = proj.project_xy(self._x, self._y) - - if (self._xs != null) - [self._xs, self._ys] = proj.project_xsys(self._xs, self._ys) - - if (self._x0 != null) - [self._x0, self._y0] = proj.project_xy(self._x0, self._y0) - - if (self._x1 != null) - [self._x1, self._y1] = proj.project_xy(self._x1, self._y1) - } - - function num_array(array: Arrayable): NumberArray { - if (array instanceof NumberArray) - return array - else - return new NumberArray(array) - } - - // if we have any coordinates that are categorical, convert them to - // synthetic coords here - const xr = this.renderer.scope.x_range - const yr = this.renderer.scope.y_range - - // XXX: MultiPolygons is a special case of special cases - if (this.model.type != "MultiPolygons") { - for (let [xname, yname] of this.model._coords) { - xname = `_${xname}` - yname = `_${yname}` - - // TODO (bev) more robust detection of multi-glyph case - // hand multi glyph case - if (self._xs != null) { - if (xr instanceof FactorRange) - self[xname] = map(self[xname], (arr: any) => xr.v_synthetic(arr)) - else - self[xname] = map(self[xname], num_array) - if (yr instanceof FactorRange) - self[yname] = map(self[yname], (arr: any) => yr.v_synthetic(arr)) - else - self[yname] = map(self[yname], num_array) - } else { - // hand standard glyph case - if (xr instanceof FactorRange) - self[xname] = xr.v_synthetic(self[xname]) - else - self[xname] = num_array(self[xname]) - if (yr instanceof FactorRange) - self[yname] = yr.v_synthetic(self[yname]) - else - self[yname] = num_array(self[yname]) - } - } + this._project_data() } - if (this.glglyph != null) - this.glglyph.set_data_changed(self._x.length) + this.glglyph?.set_data_changed() - this._set_data(indices_to_update) //TODO doesn't take subset indices into account + this._set_data(indices_to_update) // TODO doesn't take subset indices into account this.index_data() } @@ -359,29 +311,21 @@ export abstract class GlyphView extends View { protected _mask_data?(): number[] map_data(): void { - // TODO: if using gl, skip this (when is this called?) - // map all the coordinate fields const self = this as any - for (let [xname, yname] of this.model._coords) { - const sxname = `s${xname}` - const syname = `s${yname}` - xname = `_${xname}` - yname = `_${yname}` - - if (self[xname] != null && (isArray(self[xname][0]) || isTypedArray(self[xname][0]))) { - const n = self[xname].length - - self[sxname] = new Array(n) - self[syname] = new Array(n) - - for (let i = 0; i < n; i++) { - const [sx, sy] = this.renderer.scope.map_to_screen(self[xname][i], self[yname][i]) - self[sxname][i] = sx - self[syname][i] = sy + const {x_scale, y_scale} = this.renderer.scope + for (const prop of this.model) { + if (prop instanceof p.BaseCoordinateSpec) { + const scale = prop.dimension == "x" ? x_scale : y_scale + let array = self[`_${prop.attr}`] as NumberArray | RaggedArray + if (array instanceof RaggedArray) { + const screen = scale.v_compute(array.array) + array = new RaggedArray(array.offsets, screen) + } else { + array = scale.v_compute(array) } - } else - [self[sxname], self[syname]] = this.renderer.scope.map_to_screen(self[xname], self[yname]) + (this as any)[`s${prop.attr}`] = array + } } this._map_data() @@ -405,26 +349,9 @@ export abstract class Glyph extends Model { properties: Glyph.Props __view_type__: GlyphView - /* prototype */ _coords: [string, string][] - constructor(attrs?: Partial) { super(attrs) } - static init_Glyph(): void { - this.prototype._coords = [] - } - - static coords(coords: [string, string][]): void { - const _coords = this.prototype._coords.concat(coords) - this.prototype._coords = _coords - - const result: any = {} - for (const [x, y] of coords) { - result[x] = [ p.CoordinateSpec ] - result[y] = [ p.CoordinateSpec ] - } - - this.define(result) - } + static init_Glyph(): void {} } diff --git a/bokehjs/src/lib/models/glyphs/harea.ts b/bokehjs/src/lib/models/glyphs/harea.ts index b5a56e88e06..ea3feacd0d8 100644 --- a/bokehjs/src/lib/models/glyphs/harea.ts +++ b/bokehjs/src/lib/models/glyphs/harea.ts @@ -124,9 +124,9 @@ export class HArea extends Area { this.prototype.default_view = HAreaView this.define({ - x1: [ p.CoordinateSpec ], - x2: [ p.CoordinateSpec ], - y: [ p.CoordinateSpec ], + x1: [ p.XCoordinateSpec, {field: "x1"} ], + x2: [ p.XCoordinateSpec, {field: "x2"} ], + y: [ p.YCoordinateSpec, {field: "y"} ], }) } } diff --git a/bokehjs/src/lib/models/glyphs/hbar.ts b/bokehjs/src/lib/models/glyphs/hbar.ts index 46be189d3e5..736c9e6b27c 100644 --- a/bokehjs/src/lib/models/glyphs/hbar.ts +++ b/bokehjs/src/lib/models/glyphs/hbar.ts @@ -82,11 +82,11 @@ export class HBar extends Box { static init_HBar(): void { this.prototype.default_view = HBarView - this.coords([['left', 'y']]) this.define({ - height: [ p.NumberSpec ], - right: [ p.CoordinateSpec ], + left: [ p.XCoordinateSpec, {value: 0} ], + y: [ p.YCoordinateSpec, {field: "y"} ], + height: [ p.NumberSpec, {value: 1} ], + right: [ p.XCoordinateSpec, {field: "right"} ], }) - this.override({ left: 0 }) } } diff --git a/bokehjs/src/lib/models/glyphs/hex_tile.ts b/bokehjs/src/lib/models/glyphs/hex_tile.ts index 0c1b52f4c95..112d9dd7f98 100644 --- a/bokehjs/src/lib/models/glyphs/hex_tile.ts +++ b/bokehjs/src/lib/models/glyphs/hex_tile.ts @@ -9,10 +9,13 @@ import {Context2d} from "core/util/canvas" import {SpatialIndex} from "core/util/spatial" import {Line, Fill} from "core/visuals" import {HexTileOrientation} from "core/enums" +import {inplace} from "core/util/projections" import {generic_area_legend} from "./utils" import {Selection} from "../selections/selection" +export type Vertices = [number, number, number, number, number, number] + export interface HexTileData extends GlyphData { _q: NumberArray _r: NumberArray @@ -25,8 +28,8 @@ export interface HexTileData extends GlyphData { sx: NumberArray sy: NumberArray - svx: number[] - svy: number[] + svx: Vertices + svy: Vertices ssize: number } @@ -43,29 +46,32 @@ export class HexTileView extends GlyphView { return [scx, scy] } - protected _set_data(): void { const n = this._q.length - const size = this.model.size - const aspect_scale = this.model.aspect_scale + const {orientation, size, aspect_scale} = this.model this._x = new NumberArray(n) this._y = new NumberArray(n) - if (this.model.orientation == "pointytop") { + const sqrt3 = Math.sqrt(3) + if (orientation == "pointytop") { for (let i = 0; i < n; i++) { - this._x[i] = size * Math.sqrt(3) * (this._q[i] + this._r[i]/2) / aspect_scale + this._x[i] = size * sqrt3 * (this._q[i] + this._r[i]/2) / aspect_scale this._y[i] = -size * 3/2 * this._r[i] } } else { for (let i = 0; i < n; i++) { this._x[i] = size * 3/2 * this._q[i] - this._y[i] = -size * Math.sqrt(3) * (this._r[i] + this._q[i]/2) * aspect_scale + this._y[i] = -size * sqrt3 * (this._r[i] + this._q[i]/2) * aspect_scale } } } + protected _project_data(): void { + inplace.project_xy(this._x, this._y) + } + protected _index_data(index: SpatialIndex): void { let ysize = this.model.size let xsize = Math.sqrt(3)*ysize/2 @@ -96,7 +102,7 @@ export class HexTileView extends GlyphView { ;[this.svx, this.svy] = this._get_unscaled_vertices() } - protected _get_unscaled_vertices(): [number[], number[]] { + protected _get_unscaled_vertices(): [[number, number, number, number, number, number], [number, number, number, number, number, number]] { const size = this.model.size const aspect_scale = this.model.aspect_scale @@ -108,8 +114,8 @@ export class HexTileView extends GlyphView { const h = Math.sqrt(3)/2*Math.abs(hscale.compute(0) - hscale.compute(size)) / aspect_scale // assumes linear scale const r2 = r/2.0 - const svx = [0, -h, -h, 0, h, h ] - const svy = [r, r2, -r2, -r, -r2, r2] + const svx: Vertices = [0, -h, -h, 0, h, h ] + const svy: Vertices = [r, r2, -r2, -r, -r2, r2] return [svx, svy] } else { @@ -120,8 +126,8 @@ export class HexTileView extends GlyphView { const h = Math.sqrt(3)/2*Math.abs(hscale.compute(0) - hscale.compute(size)) * aspect_scale // assumes linear scale const r2 = r/2.0 - const svx = [r, r2, -r2, -r, -r2, r2] - const svy = [0, -h, -h, 0, h, h ] + const svx: Vertices = [r, r2, -r2, -r, -r2, r2] + const svy: Vertices = [0, -h, -h, 0, h, h ] return [svx, svy] } @@ -232,14 +238,15 @@ export class HexTile extends Glyph { static init_HexTile(): void { this.prototype.default_view = HexTileView - this.coords([['r', 'q']]) this.mixins([LineVector, FillVector]) this.define({ + r: [ p.NumberSpec ], + q: [ p.NumberSpec ], size: [ p.Number, 1.0 ], aspect_scale: [ p.Number, 1.0 ], scale: [ p.NumberSpec, 1.0 ], orientation: [ p.HexTileOrientation, "pointytop" ], }) - this.override({ line_color: null }) + this.override({line_color: null}) } } diff --git a/bokehjs/src/lib/models/glyphs/image_url.ts b/bokehjs/src/lib/models/glyphs/image_url.ts index e191bbbbfee..21538a05e6b 100644 --- a/bokehjs/src/lib/models/glyphs/image_url.ts +++ b/bokehjs/src/lib/models/glyphs/image_url.ts @@ -76,13 +76,11 @@ export class ImageURLView extends XYGlyphView { const n = this._x.length - const xs = new Array(w_data ? 2*n : n) - const ys = new Array(h_data ? 2*n : n) + const xs = new NumberArray(w_data ? 2*n : n) + const ys = new NumberArray(h_data ? 2*n : n) - for (let i = 0; i < n; i++) { - xs[i] = this._x[i] - ys[i] = this._y[i] - } + xs.set(this._x, 0) + ys.set(this._y, 0) // if the width/height are in screen units, don't try to include them in bounds if (w_data) { diff --git a/bokehjs/src/lib/models/glyphs/multi_line.ts b/bokehjs/src/lib/models/glyphs/multi_line.ts index 1a4023629e9..0c945f12354 100644 --- a/bokehjs/src/lib/models/glyphs/multi_line.ts +++ b/bokehjs/src/lib/models/glyphs/multi_line.ts @@ -1,8 +1,9 @@ import {SpatialIndex} from "core/util/spatial" +import {inplace} from "core/util/projections" import {PointGeometry, SpanGeometry} from "core/geometry" import {LineVector} from "core/property_mixins" import {Line} from "core/visuals" -import {Arrayable, Rect, NumberArray} from "core/types" +import {Rect, RaggedArray} from "core/types" import * as hittest from "core/hittest" import * as p from "core/properties" import {minmax} from "core/util/arrayable" @@ -13,11 +14,11 @@ import {generic_line_legend, line_interpolation} from "./utils" import {Selection} from "../selections/selection" export interface MultiLineData extends GlyphData { - _xs: NumberArray[] - _ys: NumberArray[] + _xs: RaggedArray + _ys: RaggedArray - sxs: NumberArray[] - sys: NumberArray[] + sxs: RaggedArray + sys: RaggedArray } export interface MultiLineView extends MultiLineData {} @@ -26,18 +27,22 @@ export class MultiLineView extends GlyphView { model: MultiLine visuals: MultiLine.Visuals + protected _project_data(): void { + inplace.project_xy(this._xs.array, this._ys.array) + } + protected _index_data(index: SpatialIndex): void { const {data_size} = this for (let i = 0; i < data_size; i++) { - const xsi = this._xs[i] - if (xsi == null || xsi.length == 0) { // XXX: null?, so include in types + const xsi = this._xs.get(i) + if (xsi.length == 0) { index.add_empty() continue } - const ysi = this._ys[i] - if (ysi == null || ysi.length == 0) { // XXX: null?, so include in types + const ysi = this._ys.get(i) + if (ysi.length == 0) { index.add_empty() continue } @@ -51,7 +56,8 @@ export class MultiLineView extends GlyphView { protected _render(ctx: Context2d, indices: number[], {sxs, sys}: MultiLineData): void { for (const i of indices) { - const [sx, sy] = [sxs[i], sys[i]] + const sx = sxs.get(i) + const sy = sys.get(i) this.visuals.line.set_vectorize(ctx, i) for (let j = 0, end = sx.length; j < end; j++) { @@ -78,10 +84,13 @@ export class MultiLineView extends GlyphView { for (let i = 0, end = this.sxs.length; i < end; i++) { const threshold = Math.max(2, this.visuals.line.cache_select('line_width', i) / 2) + const sxsi = this.sxs.get(i) + const sysi = this.sys.get(i) + let points: number[] | null = null - for (let j = 0, endj = this.sxs[i].length-1; j < endj; j++) { - const p0 = {x: this.sxs[i][j], y: this.sys[i][j] } - const p1 = {x: this.sxs[i][j+1], y: this.sys[i][j+1]} + for (let j = 0, endj = sxsi.length - 1; j < endj; j++) { + const p0 = {x: sxsi[j], y: sysi[j] } + const p1 = {x: sxsi[j+1], y: sysi[j+1]} const dist = hittest.dist_to_segment(point, p0, p1) if (dist < threshold && dist < shortest) { shortest = dist @@ -103,20 +112,21 @@ export class MultiLineView extends GlyphView { const {sx, sy} = geometry let val: number - let values: Arrayable> - if (geometry.direction === 'v') { + let vs: RaggedArray + if (geometry.direction == 'v') { val = this.renderer.yscale.invert(sy) - values = this._ys + vs = this._ys } else { val = this.renderer.xscale.invert(sx) - values = this._xs + vs = this._xs } const hits: Map = new Map() - for (let i = 0, end = values.length; i < end; i++) { + for (let i = 0, end = vs.length; i < end; i++) { + const vsi = vs.get(i) const points: number[] = [] - for (let j = 0, endj = values[i].length-1; j < endj; j++) { - if (values[i][j] <= val && val <= values[i][j+1]) + for (let j = 0, endj = vsi.length - 1; j < endj; j++) { + if (vsi[j] <= val && val <= vsi[j + 1]) points.push(j) } if (points.length > 0) { @@ -131,7 +141,12 @@ export class MultiLineView extends GlyphView { } get_interpolation_hit(i: number, point_i: number, geometry: PointGeometry | SpanGeometry): [number, number] { - const [x2, y2, x3, y3] = [this._xs[i][point_i], this._ys[i][point_i], this._xs[i][point_i+1], this._ys[i][point_i+1]] + const xsi = this._xs.get(i) + const ysi = this._ys.get(i) + const x2 = xsi[point_i] + const y2 = ysi[point_i] + const x3 = xsi[point_i + 1] + const y3 = ysi[point_i + 1] return line_interpolation(this.renderer, geometry, x2, y2, x3, y3) } @@ -170,7 +185,10 @@ export class MultiLine extends Glyph { static init_MultiLine(): void { this.prototype.default_view = MultiLineView - this.coords([['xs', 'ys']]) + this.define({ + xs: [ p.XCoordinateSeqSpec, {field: "xs"} ], + ys: [ p.YCoordinateSeqSpec, {field: "ys"} ], + }) this.mixins(LineVector) } } diff --git a/bokehjs/src/lib/models/glyphs/multi_polygons.ts b/bokehjs/src/lib/models/glyphs/multi_polygons.ts index a2f6458f685..e30c2a6f1f2 100644 --- a/bokehjs/src/lib/models/glyphs/multi_polygons.ts +++ b/bokehjs/src/lib/models/glyphs/multi_polygons.ts @@ -11,7 +11,6 @@ import {Line, Fill, Hatch} from "core/visuals" import * as hittest from "core/hittest" import * as p from "core/properties" import {Selection} from "../selections/selection" -import {isArray, isTypedArray} from "core/util/types" import {unreachable} from "core/util/assert" export interface MultiPolygonsData extends GlyphData { @@ -30,6 +29,10 @@ export class MultiPolygonsView extends GlyphView { protected _hole_index: SpatialIndex + protected _project_data(): void { + // TODO + } + protected _index_data(index: SpatialIndex): void { const {min, max} = Math const {data_size} = this @@ -132,7 +135,7 @@ export class MultiPolygonsView extends GlyphView { }) } - protected _inner_loop(ctx: Context2d, sx: Arrayable>>, sy: Arrayable>>): void { + protected _inner_loop(ctx: Context2d, sx: NumberArray[][], sy: NumberArray[][]): void { ctx.beginPath() for (let j = 0, endj = sx.length; j < endj; j++) { for (let k = 0, endk = sx[j].length; k < endk; k++) { @@ -284,34 +287,21 @@ export class MultiPolygonsView extends GlyphView { } map_data(): void { - const self = this as any - - for (let [xname, yname] of this.model._coords) { - const sxname = `s${xname}` - const syname = `s${yname}` - xname = `_${xname}` - yname = `_${yname}` - - if (self[xname] != null && (isArray(self[xname][0]) || isTypedArray(self[xname][0]))) { - const ni = self[xname].length - - self[sxname] = new Array(ni) - self[syname] = new Array(ni) - - for (let i = 0; i < ni; i++) { - const nj = self[xname][i].length - self[sxname][i] = new Array(nj) - self[syname][i] = new Array(nj) - for (let j = 0; j < nj; j++) { - const nk = self[xname][i][j].length - self[sxname][i][j] = new Array(nk) - self[syname][i][j] = new Array(nk) - for (let k = 0; k < nk; k++) { - const [sx, sy] = this.renderer.scope.map_to_screen(self[xname][i][j][k], self[yname][i][j][k]) - self[sxname][i][j][k] = sx - self[syname][i][j][k] = sy - } - } + const n_i = this._xs.length + this.sxs = new Array(n_i) + this.sys = new Array(n_i) + for (let i = 0; i < n_i; i++) { + const n_j = this._xs[i].length + this.sxs[i] = new Array(n_j) + this.sys[i] = new Array(n_j) + for (let j = 0; j < n_j; j++) { + const n_k = this._xs[i][j].length + this.sxs[i][j] = new Array(n_k) + this.sys[i][j] = new Array(n_k) + for (let k = 0; k < n_k; k++) { + const [sx, sy] = this.renderer.scope.map_to_screen(this._xs[i][j][k], this._ys[i][j][k]) + this.sxs[i][j][k] = sx + this.sys[i][j][k] = sy } } } @@ -326,8 +316,8 @@ export namespace MultiPolygons { export type Attrs = p.AttrsOf export type Props = Glyph.Props & { - xs: p.CoordinateSeqSpec - ys: p.CoordinateSeqSpec + xs: p.CoordinateSeqSeqSeqSpec + ys: p.CoordinateSeqSeqSeqSpec } & Mixins export type Mixins = LineVector & FillVector & HatchVector @@ -348,7 +338,10 @@ export class MultiPolygons extends Glyph { static init_MultiPolygons(): void { this.prototype.default_view = MultiPolygonsView - this.coords([['xs', 'ys']]) + this.define({ + xs: [ p.XCoordinateSeqSeqSeqSpec, {field: "xs"} ], + ys: [ p.YCoordinateSeqSeqSeqSpec, {field: "ys"} ], + }) this.mixins([LineVector, FillVector, HatchVector]) } } diff --git a/bokehjs/src/lib/models/glyphs/patches.ts b/bokehjs/src/lib/models/glyphs/patches.ts index 393f34dc748..07e28f8d260 100644 --- a/bokehjs/src/lib/models/glyphs/patches.ts +++ b/bokehjs/src/lib/models/glyphs/patches.ts @@ -2,7 +2,7 @@ import {SpatialIndex} from "core/util/spatial" import {Glyph, GlyphView, GlyphData} from "./glyph" import {generic_area_legend} from "./utils" import {minmax, sum} from "core/util/arrayable" -import {Arrayable, Rect, NumberArray} from "core/types" +import {Arrayable, Rect, RaggedArray} from "core/types" import {PointGeometry, RectGeometry} from "core/geometry" import {Context2d} from "core/util/canvas" import {LineVector, FillVector, HatchVector} from "core/property_mixins" @@ -11,13 +11,14 @@ import * as hittest from "core/hittest" import * as p from "core/properties" import {Selection} from "../selections/selection" import {unreachable} from "core/util/assert" +import {inplace} from "core/util/projections" export interface PatchesData extends GlyphData { - _xs: NumberArray[] - _ys: NumberArray[] + _xs: RaggedArray + _ys: RaggedArray - sxs: NumberArray[] - sys: NumberArray[] + sxs: RaggedArray + sys: RaggedArray } export interface PatchesView extends PatchesData {} @@ -26,12 +27,16 @@ export class PatchesView extends GlyphView { model: Patches visuals: Patches.Visuals + protected _project_data(): void { + inplace.project_xy(this._xs.array, this._ys.array) + } + protected _index_data(index: SpatialIndex): void { const {data_size} = this for (let i = 0; i < data_size; i++) { - const xsi = this._xs[i] - const ysi = this._ys[i] + const xsi = this._xs.get(i) + const ysi = this._ys.get(i) if (xsi.length == 0) index.add_empty() @@ -77,7 +82,8 @@ export class PatchesView extends GlyphView { protected _render(ctx: Context2d, indices: number[], {sxs, sys}: PatchesData): void { for (const i of indices) { - const [sx, sy] = [sxs[i], sys[i]] + const sx = sxs.get(i) + const sy = sys.get(i) if (this.visuals.fill.doit) { this.visuals.fill.set_vectorize(ctx, i) @@ -105,8 +111,8 @@ export class PatchesView extends GlyphView { for (let i = 0, end = candidates.length; i < end; i++) { const index = candidates[i] - const sxss = this.sxs[index] - const syss = this.sys[index] + const sxss = this.sxs.get(index) + const syss = this.sys.get(index) let hit = true for (let j = 0, endj = sxss.length; j < endj; j++) { const sx = sxss[j] @@ -136,8 +142,8 @@ export class PatchesView extends GlyphView { for (let i = 0, end = candidates.length; i < end; i++) { const index = candidates[i] - const sxsi = this.sxs[index] - const sysi = this.sys[index] + const sxsi = this.sxs.get(index) + const sysi = this.sys.get(index) const n = sxsi.length for (let k = 0, j = 0;; j++) { @@ -163,8 +169,8 @@ export class PatchesView extends GlyphView { } scenterxy(i: number, sx: number, sy: number): [number, number] { - const sxsi = this.sxs[i] - const sysi = this.sys[i] + const sxsi = this.sxs.get(i) + const sysi = this.sys.get(i) const n = sxsi.length let has_nan = false @@ -226,7 +232,10 @@ export class Patches extends Glyph { static init_Patches(): void { this.prototype.default_view = PatchesView - this.coords([['xs', 'ys']]) + this.define({ + xs: [ p.XCoordinateSeqSpec, {field: "xs"} ], + ys: [ p.YCoordinateSeqSpec, {field: "ys"} ], + }) this.mixins([LineVector, FillVector, HatchVector]) } } diff --git a/bokehjs/src/lib/models/glyphs/quad.ts b/bokehjs/src/lib/models/glyphs/quad.ts index 36bfa852682..76effba038c 100644 --- a/bokehjs/src/lib/models/glyphs/quad.ts +++ b/bokehjs/src/lib/models/glyphs/quad.ts @@ -61,6 +61,11 @@ export class Quad extends Box { static init_Quad(): void { this.prototype.default_view = QuadView - this.coords([['right', 'bottom'], ['left', 'top']]) + this.define({ + right: [ p.XCoordinateSpec, {field: "right"} ], + bottom: [ p.YCoordinateSpec, {field: "bottom"} ], + left: [ p.XCoordinateSpec, {field: "left"} ], + top: [ p.YCoordinateSpec, {field: "top"} ], + }) } } diff --git a/bokehjs/src/lib/models/glyphs/quadratic.ts b/bokehjs/src/lib/models/glyphs/quadratic.ts index 5eadf7db29c..d778ea5a2a2 100644 --- a/bokehjs/src/lib/models/glyphs/quadratic.ts +++ b/bokehjs/src/lib/models/glyphs/quadratic.ts @@ -2,6 +2,7 @@ import {LineVector} from "core/property_mixins" import {Line} from "core/visuals" import {Rect, NumberArray} from "core/types" import {SpatialIndex} from "core/util/spatial" +import {inplace} from "core/util/projections" import {Context2d} from "core/util/canvas" import {Glyph, GlyphView, GlyphData} from "./glyph" import {generic_line_legend} from "./utils" @@ -49,6 +50,11 @@ export class QuadraticView extends GlyphView { model: Quadratic visuals: Quadratic.Visuals + protected _project_data(): void { + inplace.project_xy(this._x0, this._y0) + inplace.project_xy(this._x1, this._y1) + } + protected _index_data(index: SpatialIndex): void { const {data_size} = this @@ -119,7 +125,14 @@ export class Quadratic extends Glyph { static init_Quadratic(): void { this.prototype.default_view = QuadraticView - this.coords([['x0', 'y0'], ['x1', 'y1'], ['cx', 'cy']]) + this.define({ + x0: [ p.XCoordinateSpec, {field: "x0"} ], + y0: [ p.YCoordinateSpec, {field: "y0"} ], + x1: [ p.XCoordinateSpec, {field: "x1"} ], + y1: [ p.YCoordinateSpec, {field: "y1"} ], + cx: [ p.XCoordinateSpec, {field: "cx"} ], + cy: [ p.YCoordinateSpec, {field: "cy"} ], + }) this.mixins(LineVector) } } diff --git a/bokehjs/src/lib/models/glyphs/segment.ts b/bokehjs/src/lib/models/glyphs/segment.ts index f41ca1615f6..f1696ffd34b 100644 --- a/bokehjs/src/lib/models/glyphs/segment.ts +++ b/bokehjs/src/lib/models/glyphs/segment.ts @@ -5,6 +5,7 @@ import {LineVector} from "core/property_mixins" import {Line} from "core/visuals" import {Arrayable, Rect, NumberArray} from "core/types" import {SpatialIndex} from "core/util/spatial" +import {inplace} from "core/util/projections" import {Context2d} from "core/util/canvas" import {Glyph, GlyphView, GlyphData} from "./glyph" import {generic_line_legend} from "./utils" @@ -28,6 +29,11 @@ export class SegmentView extends GlyphView { model: Segment visuals: Segment.Visuals + protected _project_data(): void { + inplace.project_xy(this._x0, this._y0) + inplace.project_xy(this._x1, this._y1) + } + protected _index_data(index: SpatialIndex): void { const {min, max} = Math const {data_size} = this @@ -168,7 +174,12 @@ export class Segment extends Glyph { static init_Segment(): void { this.prototype.default_view = SegmentView - this.coords([['x0', 'y0'], ['x1', 'y1']]) + this.define({ + x0: [ p.XCoordinateSpec, {field: "x0"} ], + y0: [ p.YCoordinateSpec, {field: "y0"} ], + x1: [ p.XCoordinateSpec, {field: "x1"} ], + y1: [ p.YCoordinateSpec, {field: "y1"} ], + }) this.mixins(LineVector) } } diff --git a/bokehjs/src/lib/models/glyphs/varea.ts b/bokehjs/src/lib/models/glyphs/varea.ts index ac795220c43..a06b7eb3303 100644 --- a/bokehjs/src/lib/models/glyphs/varea.ts +++ b/bokehjs/src/lib/models/glyphs/varea.ts @@ -124,9 +124,9 @@ export class VArea extends Area { this.prototype.default_view = VAreaView this.define({ - x: [ p.CoordinateSpec ], - y1: [ p.CoordinateSpec ], - y2: [ p.CoordinateSpec ], + x: [ p.XCoordinateSpec, {field: "x"} ], + y1: [ p.YCoordinateSpec, {field: "y1"} ], + y2: [ p.YCoordinateSpec, {field: "y2"} ], }) } } diff --git a/bokehjs/src/lib/models/glyphs/vbar.ts b/bokehjs/src/lib/models/glyphs/vbar.ts index 117f2242166..530d8aaa3d0 100644 --- a/bokehjs/src/lib/models/glyphs/vbar.ts +++ b/bokehjs/src/lib/models/glyphs/vbar.ts @@ -82,13 +82,11 @@ export class VBar extends Box { static init_VBar(): void { this.prototype.default_view = VBarView - this.coords([['x', 'bottom']]) this.define({ - width: [ p.NumberSpec ], - top: [ p.CoordinateSpec ], - }) - this.override({ - bottom: 0, + x: [ p.XCoordinateSpec, {field: "x"} ], + bottom: [ p.YCoordinateSpec, {value: 0} ], + width: [ p.NumberSpec, {value: 1} ], + top: [ p.YCoordinateSpec, {field: "top"} ], }) } } diff --git a/bokehjs/src/lib/models/glyphs/webgl/base.ts b/bokehjs/src/lib/models/glyphs/webgl/base.ts index 2d898c1154b..0e6916b6703 100644 --- a/bokehjs/src/lib/models/glyphs/webgl/base.ts +++ b/bokehjs/src/lib/models/glyphs/webgl/base.ts @@ -1,7 +1,7 @@ // This module implements the Base GL Glyph and some utilities import {Program, VertexBuffer} from "./utils" import {Arrayable} from "core/types" -import {color2rgba} from "core/util/color" +import {color2rgba, encode_rgba, decode_rgba, RGBA} from "core/util/color" import {Context2d} from "core/util/canvas" import {logger} from "core/logging" import {GlyphView} from "../glyph" @@ -19,9 +19,10 @@ export abstract class BaseGLGlyph { protected abstract init(): void - set_data_changed(n: number): void { - if (n != this.nvertices) { - this.nvertices = n + set_data_changed(): void { + const {data_size} = this.glyph + if (data_size != this.nvertices) { + this.nvertices = data_size this.size_changed = true } @@ -89,14 +90,6 @@ export function line_width(width: number): number { return width } -export function fill_array_with_float(n: number, val: number): Float32Array { - const a = new Float32Array(n) - for (let i = 0, end = n; i < end; i++) { - a[i] = val - } - return a -} - export function fill_array_with_vec(n: number, m: number, val: Arrayable): Float32Array { const a = new Float32Array(n*m) for (let i = 0; i < n; i++) { @@ -107,7 +100,7 @@ export function fill_array_with_vec(n: number, m: number, val: Arrayable return a } -export function visual_prop_is_singular(visual: any, propname: string): boolean { +export function is_singular(visual: any, propname: string): boolean { // This touches the internals of the visual, so we limit use in this function // See renderer.ts:cache_select() for similar code return visual[propname].spec.value !== undefined @@ -119,7 +112,7 @@ export function attach_float(prog: Program, vbo: VertexBuffer & {used?: boolean} if (!visual.doit) { vbo.used = false prog.set_attribute(att_name, 'float', [0]) - } else if (visual_prop_is_singular(visual, name)) { + } else if (is_singular(visual, name)) { vbo.used = false prog.set_attribute(att_name, 'float', [visual[name].value()]) } else { @@ -135,7 +128,7 @@ export function attach_color(prog: Program, vbo: VertexBuffer & {used?: boolean} // Attach the color attribute to the program. If there's just one color, // then use this single color for all vertices (no VBO). Otherwise we // create an array and upload that to the VBO, which we attahce to the prog. - let rgba + let rgba: RGBA const m = 4 const colorname = prefix + '_color' const alphaname = prefix + '_alpha' @@ -144,40 +137,40 @@ export function attach_color(prog: Program, vbo: VertexBuffer & {used?: boolean} // Don't draw (draw transparent) vbo.used = false prog.set_attribute(att_name, 'vec4', [0, 0, 0, 0]) - } else if (visual_prop_is_singular(visual, colorname) && visual_prop_is_singular(visual, alphaname)) { + } else if (is_singular(visual, colorname) && is_singular(visual, alphaname)) { // Nice and simple; both color and alpha are singular vbo.used = false rgba = color2rgba(visual[colorname].value(), visual[alphaname].value()) prog.set_attribute(att_name, 'vec4', rgba) } else { // Use vbo; we need an array for both the color and the alpha - let alphas, colors vbo.used = true - // Get array of colors - if (visual_prop_is_singular(visual, colorname)) { - colors = ((() => { - const result = [] - for (let i = 0, end = n; i < end; i++) { - result.push(visual[colorname].value()) - } - return result - })()) - } else { + + let colors: Arrayable + if (is_singular(visual, colorname)) { + const val = encode_rgba(color2rgba(visual[colorname].value())) + const array = new Uint32Array(n) + array.fill(val) + colors = array + } else colors = visual.get_array(colorname) - } - // Get array of alphas - if (visual_prop_is_singular(visual, alphaname)) { - alphas = fill_array_with_float(n, visual[alphaname].value()) - } else { + + let alphas: Arrayable + if (is_singular(visual, alphaname)) { + const val = visual[alphaname].value() + const array = new Float32Array(n) + array.fill(val) + alphas = array + } else alphas = visual.get_array(alphaname) - } + // Create array of rgbs const a = new Float32Array(n*m) for (let i = 0, end = n; i < end; i++) { - rgba = color2rgba(colors[i], alphas[i]) - for (let j = 0, endj = m; j < endj; j++) { - a[(i*m)+j] = rgba[j] - } + const rgba = decode_rgba(colors[i]) + if (rgba[3] == 1.0) + rgba[3] = alphas[i] + a.set(rgba, i*m) } // Attach vbo vbo.set_size(n*m*4) diff --git a/bokehjs/src/lib/models/glyphs/xy_glyph.ts b/bokehjs/src/lib/models/glyphs/xy_glyph.ts index 609d2e41977..a0223010dd5 100644 --- a/bokehjs/src/lib/models/glyphs/xy_glyph.ts +++ b/bokehjs/src/lib/models/glyphs/xy_glyph.ts @@ -1,5 +1,6 @@ import {NumberArray} from "core/types" import {SpatialIndex} from "core/util/spatial" +import {inplace} from "core/util/projections" import * as p from "core/properties" import {Glyph, GlyphView, GlyphData} from "./glyph" @@ -17,6 +18,10 @@ export abstract class XYGlyphView extends GlyphView { model: XYGlyph visuals: XYGlyph.Visuals + protected _project_data(): void { + inplace.project_xy(this._x, this._y) + } + protected _index_data(index: SpatialIndex): void { const {data_size} = this @@ -58,6 +63,9 @@ export abstract class XYGlyph extends Glyph { } static init_XYGlyph(): void { - this.coords([['x', 'y']]) + this.define({ + x: [ p.XCoordinateSpec, {field: "x"} ], + y: [ p.YCoordinateSpec, {field: "y"} ], + }) } } diff --git a/bokehjs/src/lib/models/graphs/layout_provider.ts b/bokehjs/src/lib/models/graphs/layout_provider.ts index 9d129f5d863..c3ae32bbf7c 100644 --- a/bokehjs/src/lib/models/graphs/layout_provider.ts +++ b/bokehjs/src/lib/models/graphs/layout_provider.ts @@ -1,5 +1,6 @@ import {Model} from "../../model" import {ColumnarDataSource} from "../sources/columnar_data_source" +import {Arrayable, NumberArray} from "core/types" import * as p from "core/properties" export namespace LayoutProvider { @@ -17,7 +18,7 @@ export abstract class LayoutProvider extends Model { super(attrs) } - abstract get_node_coordinates(graph_source: ColumnarDataSource): [number[], number[]] + abstract get_node_coordinates(graph_source: ColumnarDataSource): [NumberArray, NumberArray] - abstract get_edge_coordinates(graph_source: ColumnarDataSource): [[number, number][], [number, number][]] + abstract get_edge_coordinates(graph_source: ColumnarDataSource): [Arrayable[], Arrayable[]] } diff --git a/bokehjs/src/lib/models/graphs/static_layout_provider.ts b/bokehjs/src/lib/models/graphs/static_layout_provider.ts index d5f5dbb2896..812ffeb782b 100644 --- a/bokehjs/src/lib/models/graphs/static_layout_provider.ts +++ b/bokehjs/src/lib/models/graphs/static_layout_provider.ts @@ -1,6 +1,7 @@ import {LayoutProvider} from "./layout_provider" import {ColumnarDataSource} from "../sources/columnar_data_source" -import * as p from "../../core/properties" +import {Arrayable, NumberArray} from "core/types" +import * as p from "core/properties" export namespace StaticLayoutProvider { export type Attrs = p.AttrsOf @@ -25,36 +26,41 @@ export class StaticLayoutProvider extends LayoutProvider { }) } - get_node_coordinates(node_source: ColumnarDataSource): [number[], number[]] { - const xs: number[] = [] - const ys: number[] = [] + get_node_coordinates(node_source: ColumnarDataSource): [NumberArray, NumberArray] { const index = node_source.data.index - for (let i = 0, end = index.length; i < end; i++) { + const n = index.length + const xs = new NumberArray(n) + const ys = new NumberArray(n) + for (let i = 0; i < n; i++) { const point = this.graph_layout[index[i]] - const [x, y] = point != null ? point : [NaN, NaN] - xs.push(x) - ys.push(y) + const [x, y] = point ?? [NaN, NaN] + xs[i] = x + ys[i] = y } return [xs, ys] } - get_edge_coordinates(edge_source: ColumnarDataSource): [[number, number][], [number, number][]] { - const xs: [number, number][] = [] - const ys: [number, number][] = [] + get_edge_coordinates(edge_source: ColumnarDataSource): [Arrayable[], Arrayable[]] { const starts = edge_source.data.start const ends = edge_source.data.end - const has_paths = (edge_source.data.xs != null) && (edge_source.data.ys != null) - for (let i = 0, endi = starts.length; i < endi; i++) { - const in_layout = (this.graph_layout[starts[i]] != null) && (this.graph_layout[ends[i]] != null) + const n = starts.length + const xs: number[][] = [] + const ys: number[][] = [] + const has_paths = edge_source.data.xs != null && edge_source.data.ys != null + for (let i = 0; i < n; i++) { + const in_layout = this.graph_layout[starts[i]] != null && this.graph_layout[ends[i]] != null if (has_paths && in_layout) { xs.push(edge_source.data.xs[i]) ys.push(edge_source.data.ys[i]) } else { - let end, start - if (in_layout) - [start, end] = [this.graph_layout[starts[i]], this.graph_layout[ends[i]]] - else - [start, end] = [[NaN, NaN], [NaN, NaN]] + let start, end + if (in_layout) { + start = this.graph_layout[starts[i]] + end = this.graph_layout[ends[i]] + } else { + start = [NaN, NaN] + end = [NaN, NaN] + } xs.push([start[0], end[0]]) ys.push([start[1], end[1]]) } diff --git a/bokehjs/src/lib/models/renderers/graph_renderer.ts b/bokehjs/src/lib/models/renderers/graph_renderer.ts index ec634612d48..9894d5711d4 100644 --- a/bokehjs/src/lib/models/renderers/graph_renderer.ts +++ b/bokehjs/src/lib/models/renderers/graph_renderer.ts @@ -3,73 +3,87 @@ import {GlyphRenderer, GlyphRendererView} from "./glyph_renderer" import {LayoutProvider} from "../graphs/layout_provider" import {GraphHitTestPolicy, NodesOnly} from "../graphs/graph_hit_test_policy" import * as p from "core/properties" -import {build_views, remove_views} from "core/build_views" +import {build_view} from "core/build_views" import {SelectionManager} from "core/selection_manager" +import {XYGlyph} from "../glyphs/xy_glyph" +import {MultiLine} from "../glyphs/multi_line" +import {ColumnarDataSource} from "../sources/columnar_data_source" +import {Arrayable} from "core/types" +import {assert} from "core/util/assert" export class GraphRendererView extends DataRendererView { model: GraphRenderer - node_view: GlyphRendererView edge_view: GlyphRendererView - - protected _renderer_views: Map - - initialize(): void { - super.initialize() - this._renderer_views = new Map() - } + node_view: GlyphRendererView async lazy_initialize(): Promise { - [this.node_view, this.edge_view] = await build_views(this._renderer_views, [ - this.model.node_renderer, - this.model.edge_renderer, - ], {parent: this.parent}) - - this.set_data() - } - - remove(): void { - remove_views(this._renderer_views) - super.remove() - } - - connect_signals(): void { - super.connect_signals() + await super.lazy_initialize() - this.connect(this.model.layout_provider.change, () => this.set_data()) - this.connect(this.model.node_renderer.data_source._select, () => this.set_data()) - this.connect(this.model.node_renderer.data_source.inspect, () => this.set_data()) - this.connect(this.model.node_renderer.data_source.change, () => this.set_data()) - this.connect(this.model.edge_renderer.data_source._select, () => this.set_data()) - this.connect(this.model.edge_renderer.data_source.inspect, () => this.set_data()) - this.connect(this.model.edge_renderer.data_source.change, () => this.set_data()) + const graph = this.model - const {x_ranges, y_ranges} = this.plot_view.frame + // TODO: replace this with bi-variate transforms + let xs_ys: [Arrayable[], Arrayable[]] | null = null + let x_y: [Arrayable, Arrayable] | null = null - for (const [, range] of x_ranges) { - this.connect(range.change, () => this.set_data()) + const xs_expr = { + v_compute(source: ColumnarDataSource) { + assert(xs_ys == null) + const [xs] = xs_ys = graph.layout_provider.get_edge_coordinates(source) + return xs + }, + } + const ys_expr = { + v_compute(_source: ColumnarDataSource) { + assert(xs_ys != null) + const [, ys] = xs_ys + xs_ys = null + return ys + }, } - for (const [, range] of y_ranges) { - this.connect(range.change, () => this.set_data()) + const x_expr = { + v_compute(source: ColumnarDataSource) { + assert(x_y == null) + const [x] = x_y = graph.layout_provider.get_node_coordinates(source) + return x + }, + } + const y_expr = { + v_compute(_source: ColumnarDataSource) { + assert(x_y != null) + const [, y] = x_y + x_y = null + return y + }, } - } - set_data(request_render: boolean = true): void { - // XXX - const node_glyph: any = this.node_view.glyph - ;[node_glyph._x, node_glyph._y] = - this.model.layout_provider.get_node_coordinates(this.model.node_renderer.data_source) as any + const {edge_renderer, node_renderer} = this.model - const edge_glyph: any = this.edge_view.glyph - ;[edge_glyph._xs, edge_glyph._ys] = - this.model.layout_provider.get_edge_coordinates(this.model.edge_renderer.data_source) as any + edge_renderer.glyph.xs = {expr: xs_expr} + edge_renderer.glyph.ys = {expr: ys_expr} - node_glyph.index_data() - edge_glyph.index_data() + node_renderer.glyph.x = {expr: x_expr} + node_renderer.glyph.y = {expr: y_expr} - if (request_render) + const {parent} = this + this.edge_view = await build_view(edge_renderer, {parent}) + this.node_view = await build_view(node_renderer, {parent}) + } + + connect_signals(): void { + super.connect_signals() + this.connect(this.model.layout_provider.change, () => { + this.edge_view.set_data(false) + this.node_view.set_data(false) this.request_render() + }) + } + + remove(): void { + this.edge_view.remove() + this.node_view.remove() + super.remove() } protected _render(): void { @@ -83,8 +97,8 @@ export namespace GraphRenderer { export type Props = DataRenderer.Props & { layout_provider: p.Property - node_renderer: p.Property - edge_renderer: p.Property + node_renderer: p.Property + edge_renderer: p.Property selection_policy: p.Property inspection_policy: p.Property } @@ -104,11 +118,11 @@ export class GraphRenderer extends DataRenderer { this.prototype.default_view = GraphRendererView this.define({ - layout_provider: [ p.Instance ], - node_renderer: [ p.Instance ], - edge_renderer: [ p.Instance ], - selection_policy: [ p.Instance, () => new NodesOnly() ], - inspection_policy: [ p.Instance, () => new NodesOnly() ], + layout_provider: [ p.Instance ], + node_renderer: [ p.Instance ], + edge_renderer: [ p.Instance ], + selection_policy: [ p.Instance, () => new NodesOnly() ], + inspection_policy: [ p.Instance, () => new NodesOnly() ], }) } diff --git a/bokehjs/src/lib/models/textures/texture.ts b/bokehjs/src/lib/models/textures/texture.ts index d0384b06293..dde2505870f 100644 --- a/bokehjs/src/lib/models/textures/texture.ts +++ b/bokehjs/src/lib/models/textures/texture.ts @@ -1,7 +1,8 @@ import {Model} from "../../model" -import {TextureRepetition} from 'core/enums' +import {Color} from "core/types" +import {TextureRepetition} from "core/enums" import * as p from "core/properties" -import {Context2d} from 'core/util/canvas' +import {Context2d} from "core/util/canvas" export namespace Texture { export type Attrs = p.AttrsOf @@ -26,10 +27,9 @@ export abstract class Texture extends Model { }) } - abstract get_pattern(color: any, alpha: number, scale: number, weight: number): (ctx: Context2d) => CanvasPattern | null + abstract get_pattern(color: Color, alpha: number, scale: number, weight: number): (ctx: Context2d) => CanvasPattern | null onload(defer_func: () => void): void { defer_func() } - } diff --git a/bokehjs/test/baselines/linux/Bug__in_issue_#9879__disallows_to_change_FactorRange_to_a_lower_dimension_with_a_different_number_of_factors.png b/bokehjs/test/baselines/linux/Bug__in_issue_#9879__disallows_to_change_FactorRange_to_a_lower_dimension_with_a_different_number_of_factors.png index b824d7e3231..5266239c88d 100644 Binary files a/bokehjs/test/baselines/linux/Bug__in_issue_#9879__disallows_to_change_FactorRange_to_a_lower_dimension_with_a_different_number_of_factors.png and b/bokehjs/test/baselines/linux/Bug__in_issue_#9879__disallows_to_change_FactorRange_to_a_lower_dimension_with_a_different_number_of_factors.png differ diff --git a/bokehjs/test/baselines/macos/Bug__in_issue_#9879__disallows_to_change_FactorRange_to_a_lower_dimension_with_a_different_number_of_factors.png b/bokehjs/test/baselines/macos/Bug__in_issue_#9879__disallows_to_change_FactorRange_to_a_lower_dimension_with_a_different_number_of_factors.png index 3fa5db2a679..2af22d984e5 100644 Binary files a/bokehjs/test/baselines/macos/Bug__in_issue_#9879__disallows_to_change_FactorRange_to_a_lower_dimension_with_a_different_number_of_factors.png and b/bokehjs/test/baselines/macos/Bug__in_issue_#9879__disallows_to_change_FactorRange_to_a_lower_dimension_with_a_different_number_of_factors.png differ diff --git a/bokehjs/test/baselines/windows/Bug__in_issue_#9879__disallows_to_change_FactorRange_to_a_lower_dimension_with_a_different_number_of_factors.png b/bokehjs/test/baselines/windows/Bug__in_issue_#9879__disallows_to_change_FactorRange_to_a_lower_dimension_with_a_different_number_of_factors.png index 10e48741722..aca27fc21d5 100644 Binary files a/bokehjs/test/baselines/windows/Bug__in_issue_#9879__disallows_to_change_FactorRange_to_a_lower_dimension_with_a_different_number_of_factors.png and b/bokehjs/test/baselines/windows/Bug__in_issue_#9879__disallows_to_change_FactorRange_to_a_lower_dimension_with_a_different_number_of_factors.png differ diff --git a/bokehjs/test/integration/regressions.ts b/bokehjs/test/integration/regressions.ts index 68661e3087a..5e465c78337 100644 --- a/bokehjs/test/integration/regressions.ts +++ b/bokehjs/test/integration/regressions.ts @@ -25,7 +25,7 @@ describe("Bug", () => { y_range: new DataRange1d(), }) const source = new ColumnDataSource({data: {x: [["a", "b"], ["b", "c"]], y: [1, 2]}}) - p.vbar({x: {field: "x"}, top: {field: "y"}, source}) + p.vbar({x: {field: "x"}, top: {field: "y"}, width: 0.1, source}) const {view} = await display(p, [250, 250]) source.data = {x: ["a"], y: [1]} @@ -52,7 +52,7 @@ describe("Bug", () => { }) describe("in issue #9703", () => { - it.allowing(6)("disallows ImageURL glyph to set anchor and angle at the same time", async () => { + it.allowing(7)("disallows ImageURL glyph to set anchor and angle at the same time", async () => { const p = fig([300, 300], {x_range: [-1, 10], y_range: [-1, 10]}) const svg = `\ diff --git a/bokehjs/test/unit/core/properties.ts b/bokehjs/test/unit/core/properties.ts index aa575751f6d..996b45a9773 100644 --- a/bokehjs/test/unit/core/properties.ts +++ b/bokehjs/test/unit/core/properties.ts @@ -91,8 +91,8 @@ class Some extends HasProps { angle_spec: [ p.AngleSpec ], boolean_spec: [ p.BooleanSpec ], color_spec: [ p.ColorSpec ], - coordinate_spec: [ p.CoordinateSpec ], - coordinate_seq_spec: [ p.CoordinateSeqSpec ], + coordinate_spec: [ p.XCoordinateSpec ], + coordinate_seq_spec: [ p.XCoordinateSeqSpec ], distance_spec: [ p.DistanceSpec ], font_size_spec: [ p.FontSizeSpec ], marker_spec: [ p.MarkerSpec ], @@ -279,13 +279,11 @@ describe("properties module", () => { }) it("should throw an Error otherwise", () => { - function fn(): void { - const source = new ColumnDataSource({data: {}}) - const obj = new Some({number_spec: {field: "foo"}}) - const prop = obj.properties.number_spec - prop.array(source) - } - expect(fn).to.throw(Error, /attempted to retrieve property array for nonexistent field 'foo'/) + const source = new ColumnDataSource({data: {bar: [1, 2, 3]}}) + const obj = new Some({number_spec: {field: "foo"}}) + const prop = obj.properties.number_spec + const arr = prop.array(source) + expect(arr).to.be.equal(new Float64Array([NaN, NaN, NaN])) }) it("should apply a spec transform to a field", () => { diff --git a/bokehjs/test/unit/core/visuals.ts b/bokehjs/test/unit/core/visuals.ts index b4aa283ae91..7142be8328b 100644 --- a/bokehjs/test/unit/core/visuals.ts +++ b/bokehjs/test/unit/core/visuals.ts @@ -2,6 +2,7 @@ import {expect} from "assertions" import {create_glyph_renderer_view} from "../models/glyphs/glyph_utils" import {Fill, Line, Text, Visuals} from "@bokehjs/core/visuals" +import {Context2d} from "@bokehjs/core/util/canvas" import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source" import {CDSView} from "@bokehjs/models/sources/cds_view" import {IndexFilter} from "@bokehjs/models/filters/index_filter" @@ -11,20 +12,19 @@ import * as text_glyph from "@bokehjs/models/glyphs/text" describe("Fill", () => { describe("set_value", () => { - for (const [attr, spec, value] of [ - ['fillStyle', 'fill_color', 'red'], - ['globalAlpha', 'fill_alpha', 0.5], - ]) { - it(`should set canvas context ${attr} value from ${spec} spec value`, () =>{ - const ctx = {} as any - const attrs = {} as any - attrs[spec] = value - const model = new Circle(attrs) - const fill = new Fill(model) - fill.set_value(ctx) - expect(ctx[attr]).to.be.equal(value) - }) - } + it("should set canvas context attributes", () => { + const attrs = { + fill_color: "red", + fill_alpha: 0.5, + } + const model = new Circle(attrs) + const fill = new Fill(model) + + const ctx = {} as Context2d + fill.set_value(ctx) + + expect(ctx.fillStyle).to.be.equal("rgba(255, 0, 0, 0.5)") + }) }) describe("doit", () => { @@ -34,12 +34,14 @@ describe("Fill", () => { const fill = new Fill(model) expect(fill.doit).to.be.false }) + it("should be false if fill_alpha is 0", () => { const attrs = {fill_alpha: {value: 0}, fill_color: {value: "red"}} const model = new Circle(attrs) const fill = new Fill(model) expect(fill.doit).to.be.false }) + it("should be true otherwise", () => { const attrs = {fill_alpha: {value: 1}, fill_color: {value: "red"}} const model = new Circle(attrs) @@ -47,37 +49,36 @@ describe("Fill", () => { expect(fill.doit).to.be.true }) }) - }) describe("Line", () => { describe("set_value", () => { - for (const [attr, spec, value] of [ - ['strokeStyle', 'line_color', 'red'], - ['globalAlpha', 'line_alpha', 0.5], - ['lineWidth', 'line_width', 2], - ['lineJoin', 'line_join', 'miter'], - ['lineCap', 'line_cap', 'butt'], - ['lineDashOffset', 'line_dash_offset', 2], - - // TS complains destructuring this, just test (redundantly) by hand below - //['lineDash', 'line_dash', [1,2]], - ]) { - it(`should set canvas context ${attr} value from ${spec} spec value`, () =>{ - const ctx = {} as any - ctx.setLineDash = function(x: number) { ctx.lineDash = x } - ctx.setLineDashOffset = function(x: number) { ctx.lineDashOffset = x } - const attrs = {line_dash: [1, 2]} as any - attrs[spec] = value - const model = new Circle(attrs) - const line = new Line(model) - line.set_value(ctx) - expect(ctx[attr]).to.be.equal(value) - expect(ctx.lineDash).to.be.equal([1, 2]) - }) - } + it("should set canvas context attributes", () =>{ + const attrs = { + line_color: "red", + line_alpha: 0.5, + line_width: 2, + line_join: "miter" as "miter", + line_cap: "butt" as "butt", + line_dash: [1, 2], + line_dash_offset: 2, + } + const model = new Circle(attrs) + const line = new Line(model) + + const ctx = {} as Context2d + line.set_value(ctx) + + expect(ctx.strokeStyle).to.be.equal("rgba(255, 0, 0, 0.5)") + expect(ctx.lineWidth).to.be.equal(2) + expect(ctx.lineJoin).to.be.equal("miter") + expect(ctx.lineCap).to.be.equal("butt") + expect(ctx.lineDash).to.be.equal([1, 2]) + expect(ctx.lineDashOffset).to.be.equal(2) + }) }) + describe("doit", () => { it("should be false if line_color is null", () => { const attrs = {line_alpha: {value: 1}, line_color: {value: null}, line_width: {value: 1}} @@ -85,18 +86,21 @@ describe("Line", () => { const line = new Line(model) expect(line.doit).to.be.false }) + it("should be false if line_width is 0", () => { const attrs = {line_alpha: {value: 1}, line_color: {value: "red"}, line_width: {value: 0}} const model = new Circle(attrs) const line = new Line(model) expect(line.doit).to.be.false }) + it("should be false if line_alpha is 0", () => { const attrs = {line_alpha: {value: 0}, line_color: {value: "red"}, line_width: {value: 1}} const model = new Circle(attrs) const line = new Line(model) expect(line.doit).to.be.false }) + it("should be true otherwise", () => { const attrs = {line_alpha: {value: 1}, line_color: {value: "red"}, line_width: {value: 1}} const model = new Circle(attrs) @@ -104,31 +108,34 @@ describe("Line", () => { expect(line.doit).to.be.true }) }) - }) describe("Text", () => { describe("set_value", () => { - for (const [attr, spec, value] of [ - // "font" handled below - ['fillStyle', 'text_color', 'red'], - ['globalAlpha', 'text_alpha', '0.5'], - ['textAlign', 'text_align', 'center'], - ['textBaseline', 'text_baseline', 'bottom'], - ]) { - it(`should set canvas context ${attr} value from ${spec} spec value`, () =>{ - const ctx = {} as any - const attrs = {text_font: 'times', text_font_size: "16px", text_font_style: "bold"} as any - attrs[spec] = value - const model = new text_glyph.Text(attrs) - const text = new Text(model) - text.set_value(ctx) - expect(ctx[attr]).to.be.equal(value) - expect(ctx.font).to.be.equal("bold 16px times") - }) - } + it("should set canvas context attributes", () => { + const attrs = { + text_font: "times", + text_font_size: "16px", + text_font_style: "bold" as "bold", + text_color: "red", + text_alpha: 0.5, + text_align: "center" as "center", + text_baseline: "bottom" as "bottom", + } + const model = new text_glyph.Text(attrs) + const text = new Text(model) + + const ctx = {} as Context2d + text.set_value(ctx) + + expect(ctx.fillStyle).to.be.equal("rgba(255, 0, 0, 0.5)") + expect(ctx.textAlign).to.be.equal("center") + expect(ctx.textBaseline).to.be.equal("bottom") + expect(ctx.font).to.be.equal("bold 16px times") + }) }) + describe("doit", () => { it("should be false if text_color is null", () => { const attrs = {text_alpha: {value: 1}, text_color: {value: null}} @@ -136,12 +143,14 @@ describe("Text", () => { const text = new Text(model) expect(text.doit).to.be.false }) + it("should be false if text_alpha is 0", () => { const attrs = {text_alpha: {value: 0}, text_color: {value: "red"}} const model = new text_glyph.Text(attrs) const text = new Text(model) expect(text.doit).to.be.false }) + it("should be true otherwise", () => { const attrs = {text_alpha: {value: 1}, text_color: {value: "red"}} const model = new text_glyph.Text(attrs) @@ -149,28 +158,27 @@ describe("Text", () => { expect(text.doit).to.be.true }) }) - }) describe("Visuals", () => { it("should set the correct visual values when values are vectorized", () => { - const source = new ColumnDataSource({data: {fill_alpha: [0, 0.5, 1]}}) - const attrs = {fill_alpha: {field: "fill_alpha"}} + const source = new ColumnDataSource({data: {fill_color: ["red", "green", "blue"], fill_alpha: [0, 0.5, 1]}}) + const attrs = {fill_color: {field: "fill_color"}, fill_alpha: {field: "fill_alpha"}} const circle = new Circle(attrs) const visuals = new Visuals(circle) as Visuals & {fill: Fill} visuals.warm_cache(source) - const ctx = {} as any + const ctx = {} as Context2d visuals.fill.set_vectorize(ctx, 1) - expect(ctx.globalAlpha).to.be.equal(0.5) + expect(ctx.fillStyle).to.be.equal("rgba(0, 128, 0, 0.5)") }) it("should set the correct visual values when values are vectorized and all_indices is set", () => { - const source = new ColumnDataSource({data: {fill_alpha: [0, 0.5, 1]}}) - const attrs = {fill_alpha: {field: "fill_alpha"}} + const source = new ColumnDataSource({data: {fill_color: ["red", "green", "blue"], fill_alpha: [0, 0.5, 1]}}) + const attrs = {fill_color: {field: "fill_color"}, fill_alpha: {field: "fill_alpha"}} const circle = new Circle(attrs) const visuals = new Visuals(circle) as Visuals & {fill: Fill} @@ -178,27 +186,27 @@ describe("Visuals", () => { visuals.warm_cache(source) visuals.set_all_indices([1, 2]) - const ctx = {} as any + const ctx = {} as Context2d visuals.fill.set_vectorize(ctx, 1) - expect(ctx.globalAlpha).to.be.equal(1) + expect(ctx.fillStyle).to.be.equal("rgba(0, 0, 255, 1)") }) describe("interacting with GlyphViews", () => { it("set_all_indices should be called by the glyph view", async () => { - const attrs = {fill_alpha: {field: "fill_alpha"}} + const attrs = {fill_color: {field: "fill_color"}, fill_alpha: {field: "fill_alpha"}} const circle = new Circle(attrs) - const renderer_view = await create_glyph_renderer_view(circle, {fill_alpha: [0, 0.5, 1]}) + const renderer_view = await create_glyph_renderer_view(circle, {fill_color: ["red", "green", "blue"], fill_alpha: [0, 0.5, 1]}) const filter = new IndexFilter({indices: [1, 2]}) renderer_view.model.view = new CDSView({source: renderer_view.model.data_source, filters: [filter]}) //need to manually set_data because signals for renderer aren't connected by create_glyph_view util renderer_view.set_data() - const ctx = {} as any + const ctx = {} as Context2d (renderer_view.glyph as CircleView).visuals.fill.set_vectorize(ctx, 1) - expect(ctx.globalAlpha).to.be.equal(1) + expect(ctx.fillStyle).to.be.equal("rgba(0, 0, 255, 1)") }) }) }) diff --git a/bokehjs/test/unit/models/glyphs/glyph.ts b/bokehjs/test/unit/models/glyphs/glyph.ts index 3a55f9bb4f8..b943d27c10e 100644 --- a/bokehjs/test/unit/models/glyphs/glyph.ts +++ b/bokehjs/test/unit/models/glyphs/glyph.ts @@ -18,7 +18,9 @@ describe("glyph module", () => { class SomeGlyphView extends GlyphView { model: SomeGlyph - protected _index_data(_index: SpatialIndex): void {} + protected _index_data(index: SpatialIndex): void { + index.add_empty() + } protected _render(_ctx: Context2d, _indices: number[], {}: object): void {} diff --git a/bokehjs/test/unit/models/glyphs/image_url.ts b/bokehjs/test/unit/models/glyphs/image_url.ts index 7ea81d44d37..93a3d616368 100644 --- a/bokehjs/test/unit/models/glyphs/image_url.ts +++ b/bokehjs/test/unit/models/glyphs/image_url.ts @@ -42,6 +42,8 @@ describe("ImageURL module", () => { it("`_map_data` should correctly map data if w and h units are 'data'", async () => { // ImageURLView._map_data is called by ImageURLView.map_data const image_url = new ImageURL() + image_url.x = 0 + image_url.y = 0 image_url.w = 17 image_url.h = 19 @@ -55,6 +57,8 @@ describe("ImageURL module", () => { it("`_map_data` should correctly map data if w and h units are 'screen'", async () => { // ImageURLView._map_data is called by ImageURLView.map_data const image_url = new ImageURL() + image_url.x = 0 + image_url.y = 0 image_url.w = 1 image_url.h = 2 image_url.properties.w.units = "screen" @@ -70,6 +74,8 @@ describe("ImageURL module", () => { it("`_map_data` should map data to NaN if w and h are null, 'data' units", async () => { // if sw, sh are NaN, then the image width or height are used during render const image_url = new ImageURL() + image_url.x = 0 + image_url.y = 0 image_url.w = null as any // XXX image_url.h = null as any // XXX @@ -82,6 +88,8 @@ describe("ImageURL module", () => { it("`_map_data` should map data to NaN if w and h are null, 'screen' units", async () => { const image_url = new ImageURL() + image_url.x = 0 + image_url.y = 0 image_url.w = null as any // XXX image_url.h = null as any // XXX image_url.properties.w.units = "screen" diff --git a/bokehjs/test/unit/models/graphs/graph_hit_test_policy.ts b/bokehjs/test/unit/models/graphs/graph_hit_test_policy.ts index 738ac9494f9..cffffdecb0a 100644 --- a/bokehjs/test/unit/models/graphs/graph_hit_test_policy.ts +++ b/bokehjs/test/unit/models/graphs/graph_hit_test_policy.ts @@ -15,15 +15,19 @@ import {ColumnarDataSource} from "@bokehjs/models/sources/columnar_data_source" import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source" import {Document} from "@bokehjs/document" import {build_view} from "@bokehjs/core/build_views" +import {Arrayable, NumberArray} from "@bokehjs/core/types" +import {repeat} from "@bokehjs/core/util/array" class TrivialLayoutProvider extends LayoutProvider { - get_node_coordinates(_graph_source: ColumnarDataSource): [number[], number[]] { - return [[], []] + get_node_coordinates(graph_source: ColumnarDataSource): [NumberArray, NumberArray] { + const n = graph_source.get_length() ?? 1 + return [new NumberArray(n), new NumberArray(n)] } - get_edge_coordinates(_graph_source: ColumnarDataSource): [[number, number][], [number, number][]] { - return [[], []] + get_edge_coordinates(graph_source: ColumnarDataSource): [Arrayable[], Arrayable[]] { + const n = graph_source.get_length() ?? 1 + return [repeat([], n), repeat([], n)] } } @@ -54,10 +58,12 @@ describe("GraphHitTestPolicy", () => { data: { start: [10, 10, 30], end: [20, 30, 20], + xs: [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + ys: [[1, 2, 3], [4, 5, 6], [7, 8, 9]], }, }) - const node_renderer = new GlyphRenderer({data_source: node_source, glyph: new Circle()}) - const edge_renderer = new GlyphRenderer({data_source: edge_source, glyph: new MultiLine()}) + const node_renderer = new GlyphRenderer({data_source: node_source, glyph: new Circle()}) as GlyphRenderer & {glyph: Circle} + const edge_renderer = new GlyphRenderer({data_source: edge_source, glyph: new MultiLine()}) as GlyphRenderer & {glyph: MultiLine} gr = new GraphRenderer({ node_renderer, diff --git a/bokehjs/test/unit/models/graphs/static_layout_provider.ts b/bokehjs/test/unit/models/graphs/static_layout_provider.ts index 45e64eaec0e..a399c93e585 100644 --- a/bokehjs/test/unit/models/graphs/static_layout_provider.ts +++ b/bokehjs/test/unit/models/graphs/static_layout_provider.ts @@ -2,6 +2,7 @@ import {expect} from "assertions" import {StaticLayoutProvider} from "@bokehjs/models/graphs/static_layout_provider" import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source" +import {NumberArray} from "@bokehjs/core/types" describe("StaticLayoutProvider", () => { @@ -29,8 +30,8 @@ describe("StaticLayoutProvider", () => { node_source.data.index = [0, 1, 2, 3] const [xs, ys] = layout_provider.get_node_coordinates(node_source) - expect(xs).to.be.equal([-1, 0, 1, 0]) - expect(ys).to.be.equal([0, 1, 0, -1]) + expect(xs).to.be.equal(new NumberArray([-1, 0, 1, 0])) + expect(ys).to.be.equal(new NumberArray([0, 1, 0, -1])) }) it("should return NaNs if coords don't exist", () => { @@ -38,8 +39,8 @@ describe("StaticLayoutProvider", () => { node_source.data.index = [4, 5, 6] const [xs, ys] = layout_provider.get_node_coordinates(node_source) - expect(xs).to.be.equal([NaN, NaN, NaN]) - expect(ys).to.be.equal([NaN, NaN, NaN]) + expect(xs).to.be.equal(new NumberArray([NaN, NaN, NaN])) + expect(ys).to.be.equal(new NumberArray([NaN, NaN, NaN])) }) }) @@ -51,20 +52,20 @@ describe("StaticLayoutProvider", () => { edge_source.data.end = [1, 2, 3] const [xs, ys] = layout_provider.get_edge_coordinates(edge_source) - expect(xs).to.be.equal([ [ -1, 0 ], [ -1, 1 ], [ -1, 0 ] ]) - expect(ys).to.be.equal([ [ 0, 1 ], [ 0, 0 ], [ 0, -1 ] ]) + expect(xs).to.be.equal([[-1, 0], [-1, 1], [-1, 0]]) + expect(ys).to.be.equal([[0, 1], [0, 0], [0, -1]]) }) it("should return explicit edge coords if exist", () => { const edge_source = new ColumnDataSource() edge_source.data.start = [0, 0, 0] edge_source.data.end = [1, 2, 3] - edge_source.data.xs = [ [ -1, -0.5, 0 ], [ -1, 0, 1 ], [ -1, -0.5, 0 ] ] - edge_source.data.ys = [ [ 0, 0.5, 1 ], [ 0, 0, 0 ], [ 0, -0.5, -1 ] ] + edge_source.data.xs = [[-1, -0.5, 0], [-1, 0, 1], [-1, -0.5, 0]] + edge_source.data.ys = [[0, 0.5, 1], [0, 0, 0], [0, -0.5, -1]] const [xs, ys] = layout_provider.get_edge_coordinates(edge_source) - expect(xs).to.be.equal([ [ -1, -0.5, 0 ], [ -1, 0, 1 ], [ -1, -0.5, 0 ] ] as any) // XXX: number[] instead of [number, number] - expect(ys).to.be.equal([ [ 0, 0.5, 1 ], [ 0, 0, 0 ], [ 0, -0.5, -1 ] ] as any) // XXX: number[] instead of [number, number] + expect(xs).to.be.equal([[-1, -0.5, 0], [-1, 0, 1], [-1, -0.5, 0]]) + expect(ys).to.be.equal([[0, 0.5, 1], [0, 0, 0], [0, -0.5, -1]]) }) it("should return NaNs if coords don't exist", () => { @@ -73,20 +74,20 @@ describe("StaticLayoutProvider", () => { edge_source.data.end = [5, 6, 7] const [xs, ys] = layout_provider.get_edge_coordinates(edge_source) - expect(xs).to.be.equal([ [ NaN, NaN ], [ NaN, NaN ], [ NaN, NaN ] ]) - expect(ys).to.be.equal([ [ NaN, NaN ], [ NaN, NaN ], [ NaN, NaN ] ]) + expect(xs).to.be.equal([[NaN, NaN], [NaN, NaN], [NaN, NaN]]) + expect(ys).to.be.equal([[NaN, NaN], [NaN, NaN], [NaN, NaN]]) }) it("should not return explicit edge coords if coords don't exist", () => { const edge_source = new ColumnDataSource() edge_source.data.start = [4, 4, 4] edge_source.data.end = [5, 6, 7] - edge_source.data.xs = [ [ -1, -0.5, 0 ], [ -1, 0, 1 ], [ -1, -0.5, 0 ] ] - edge_source.data.ys = [ [ 0, 0.5, 1 ], [ 0, 0, 0 ], [ 0, -0.5, -1 ] ] + edge_source.data.xs = [[-1, -0.5, 0], [-1, 0, 1], [-1, -0.5, 0]] + edge_source.data.ys = [[0, 0.5, 1], [0, 0, 0], [0, -0.5, -1]] const [xs, ys] = layout_provider.get_edge_coordinates(edge_source) - expect(xs).to.be.equal([ [ NaN, NaN ], [ NaN, NaN ], [ NaN, NaN ] ]) - expect(ys).to.be.equal([ [ NaN, NaN ], [ NaN, NaN ], [ NaN, NaN ] ]) + expect(xs).to.be.equal([[NaN, NaN], [NaN, NaN], [NaN, NaN]]) + expect(ys).to.be.equal([[NaN, NaN], [NaN, NaN], [NaN, NaN]]) }) }) }) diff --git a/tests/unit/bokeh/io/test_export.py b/tests/unit/bokeh/io/test_export.py index d6a575fe94b..c017ec74c79 100644 --- a/tests/unit/bokeh/io/test_export.py +++ b/tests/unit/bokeh/io/test_export.py @@ -18,6 +18,9 @@ import re from typing import Tuple +# External imports +from flaky import flaky + # Bokeh imports from bokeh.core.validation import silenced from bokeh.core.validation.warnings import MISSING_RENDERERS @@ -50,6 +53,7 @@ def webdriver(request): # Dev API #----------------------------------------------------------------------------- +@flaky(max_runs=10) @pytest.mark.selenium @pytest.mark.parametrize("dimensions", [(14, 14), (44, 44), (144, 144), (444, 444), (1444, 1444)]) def test_get_screenshot_as_png(webdriver, dimensions: Tuple[int, int]) -> None: @@ -74,6 +78,7 @@ def test_get_screenshot_as_png(webdriver, dimensions: Tuple[int, int]) -> None: assert data == b"\x00\xff\x00\xff"*width*height +@flaky(max_runs=10) @pytest.mark.selenium @pytest.mark.parametrize("dimensions", [(14, 14), (44, 44), (144, 144), (444, 444), (1444, 1444)]) def test_get_screenshot_as_png_with_glyph(webdriver, dimensions: Tuple[int, int]) -> None: @@ -107,6 +112,7 @@ def test_get_screenshot_as_png_with_glyph(webdriver, dimensions: Tuple[int, int] expected_count = w*h - 2*b*(w + h) + 4*b**2 assert count == expected_count +@flaky(max_runs=10) @pytest.mark.selenium def test_get_screenshot_as_png_with_unicode_minified(webdriver) -> None: p = figure(title="유니 코드 지원을위한 작은 테스트") @@ -116,6 +122,7 @@ def test_get_screenshot_as_png_with_unicode_minified(webdriver) -> None: assert len(png.tobytes()) > 0 +@flaky(max_runs=10) @pytest.mark.selenium def test_get_screenshot_as_png_with_unicode_unminified(webdriver) -> None: p = figure(title="유니 코드 지원을위한 작은 테스트") @@ -125,6 +132,7 @@ def test_get_screenshot_as_png_with_unicode_unminified(webdriver) -> None: assert len(png.tobytes()) > 0 +@flaky(max_runs=10) @pytest.mark.selenium def test_get_svgs_no_svg_present() -> None: layout = Plot(x_range=Range1d(), y_range=Range1d(), @@ -135,6 +143,7 @@ def test_get_svgs_no_svg_present() -> None: assert svgs == [] +@flaky(max_runs=10) @pytest.mark.selenium def test_get_svgs_with_svg_present(webdriver) -> None: @@ -156,7 +165,7 @@ def fix_ids(svg): '' '' '' - '' + '' '' '' '' diff --git a/tests/unit/bokeh/models/_util_models.py b/tests/unit/bokeh/models/_util_models.py index 412dc9fb392..613681175fd 100644 --- a/tests/unit/bokeh/models/_util_models.py +++ b/tests/unit/bokeh/models/_util_models.py @@ -20,6 +20,7 @@ # Bokeh imports from bokeh.core.enums import LineCap, LineJoin from bokeh.core.enums import NamedColor as Color +from bokeh.core.property.dataspec import field # Module under test # isort:skip @@ -89,8 +90,8 @@ def check_text_properties(model, prefix="", font_size='16px', baseline='bottom', assert getattr(model, prefix + "text_baseline") == baseline def check_marker_properties(marker): - assert marker.x is None - assert marker.y is None + assert marker.x == field("x") + assert marker.y == field("y") assert marker.size == 4 #----------------------------------------------------------------------------- diff --git a/tests/unit/bokeh/models/test_glyphs.py b/tests/unit/bokeh/models/test_glyphs.py index 6dacb225abe..5bc31ee39e1 100644 --- a/tests/unit/bokeh/models/test_glyphs.py +++ b/tests/unit/bokeh/models/test_glyphs.py @@ -46,6 +46,7 @@ ) from bokeh.core.enums import NamedColor as Color from bokeh.core.enums import TextAlign, TextBaseline +from bokeh.core.property.dataspec import field from bokeh.models.glyphs import ( Asterisk, CircleCross, @@ -84,7 +85,6 @@ VBar, Wedge) - #----------------------------------------------------------------------------- # Setup #----------------------------------------------------------------------------- @@ -101,8 +101,8 @@ def test_AnnularWedge() -> None: glyph = AnnularWedge() - assert glyph.x is None - assert glyph.y is None + assert glyph.x == field("x") + assert glyph.y == field("y") assert glyph.inner_radius is None assert glyph.outer_radius is None assert glyph.start_angle is None @@ -127,8 +127,8 @@ def test_AnnularWedge() -> None: def test_Annulus() -> None: glyph = Annulus() - assert glyph.x is None - assert glyph.y is None + assert glyph.x == field("x") + assert glyph.y == field("y") assert glyph.inner_radius is None assert glyph.outer_radius is None check_fill_properties(glyph) @@ -145,8 +145,8 @@ def test_Annulus() -> None: def test_Arc() -> None: glyph = Arc() - assert glyph.x is None - assert glyph.y is None + assert glyph.x == field("x") + assert glyph.y == field("y") assert glyph.radius is None assert glyph.start_angle is None assert glyph.end_angle is None @@ -167,14 +167,14 @@ def test_Arc() -> None: def test_Bezier() -> None: glyph = Bezier() - assert glyph.x0 is None - assert glyph.y0 is None - assert glyph.x1 is None - assert glyph.y1 is None - assert glyph.cx0 is None - assert glyph.cy0 is None - assert glyph.cx1 is None - assert glyph.cy1 is None + assert glyph.x0 == field("x0") + assert glyph.y0 == field("y0") + assert glyph.x1 == field("x1") + assert glyph.y1 == field("y1") + assert glyph.cx0 == field("cx0") + assert glyph.cy0 == field("cy0") + assert glyph.cx1 == field("cx1") + assert glyph.cy1 == field("cy1") check_line_properties(glyph) check_properties_existence(glyph, [ "x0", @@ -190,9 +190,9 @@ def test_Bezier() -> None: def test_HArea() -> None: glyph = HArea() - assert glyph.y is None - assert glyph.x1 is None - assert glyph.x2 is None + assert glyph.y == field("y") + assert glyph.x1 == field("x1") + assert glyph.x2 == field("x2") check_fill_properties(glyph) check_hatch_properties(glyph) check_properties_existence(glyph, [ @@ -204,7 +204,7 @@ def test_HArea() -> None: def test_HBar() -> None: glyph = HBar() - assert glyph.y is None + assert glyph.y == field("y") assert glyph.height is None assert glyph.left == 0 assert glyph.right is None @@ -222,8 +222,8 @@ def test_HBar() -> None: def test_Image() -> None: glyph = Image() assert glyph.image is None - assert glyph.x is None - assert glyph.y is None + assert glyph.x == field("x") + assert glyph.y == field("y") assert glyph.dw is None assert glyph.dh is None assert glyph.dilate is False @@ -244,8 +244,8 @@ def test_Image() -> None: def test_ImageRGBA() -> None: glyph = ImageRGBA() assert glyph.image is None - assert glyph.x is None - assert glyph.y is None + assert glyph.x == field("x") + assert glyph.y == field("y") assert glyph.dw is None assert glyph.dh is None assert glyph.dilate is False @@ -265,8 +265,8 @@ def test_ImageRGBA() -> None: def test_ImageURL() -> None: glyph = ImageURL() assert glyph.url is None - assert glyph.x is None - assert glyph.y is None + assert glyph.x == field("x") + assert glyph.y == field("y") assert glyph.w is None assert glyph.h is None assert glyph.angle == 0 @@ -295,8 +295,8 @@ def test_ImageURL() -> None: def test_Line() -> None: glyph = Line() - assert glyph.x is None - assert glyph.y is None + assert glyph.x == field("x") + assert glyph.y == field("y") check_line_properties(glyph) check_properties_existence(glyph, [ "x", @@ -306,8 +306,8 @@ def test_Line() -> None: def test_MultiLine() -> None: glyph = MultiLine() - assert glyph.xs is None - assert glyph.ys is None + assert glyph.xs == field("xs") + assert glyph.ys == field("ys") check_line_properties(glyph) check_properties_existence(glyph, [ "xs", @@ -317,8 +317,8 @@ def test_MultiLine() -> None: def test_MultiPolygons() -> None: glyph = MultiPolygons() - assert glyph.xs is None - assert glyph.ys is None + assert glyph.xs == field("xs") + assert glyph.ys == field("ys") check_fill_properties(glyph) check_hatch_properties(glyph) check_line_properties(glyph) @@ -330,8 +330,8 @@ def test_MultiPolygons() -> None: def test_Oval() -> None: glyph = Oval() - assert glyph.x is None - assert glyph.y is None + assert glyph.x == field("x") + assert glyph.y == field("y") assert glyph.width is None assert glyph.height is None assert glyph.angle == 0 @@ -351,8 +351,8 @@ def test_Oval() -> None: def test_Patch() -> None: glyph = Patch() - assert glyph.x is None - assert glyph.y is None + assert glyph.x == field("x") + assert glyph.y == field("y") check_fill_properties(glyph) check_hatch_properties(glyph) check_line_properties(glyph) @@ -364,8 +364,8 @@ def test_Patch() -> None: def test_Patches() -> None: glyph = Patches() - assert glyph.xs is None - assert glyph.ys is None + assert glyph.xs == field("xs") + assert glyph.ys == field("ys") check_fill_properties(glyph) check_hatch_properties(glyph) check_line_properties(glyph) @@ -394,12 +394,12 @@ def test_Quad() -> None: def test_Quadratic() -> None: glyph = Quadratic() - assert glyph.x0 is None - assert glyph.y0 is None - assert glyph.x1 is None - assert glyph.y1 is None - assert glyph.cx is None - assert glyph.cy is None + assert glyph.x0 == field("x0") + assert glyph.y0 == field("y0") + assert glyph.x1 == field("x1") + assert glyph.y1 == field("y1") + assert glyph.cx == field("cx") + assert glyph.cy == field("cy") check_line_properties(glyph) check_properties_existence(glyph, [ "x0", @@ -413,8 +413,8 @@ def test_Quadratic() -> None: def test_Ray() -> None: glyph = Ray() - assert glyph.x is None - assert glyph.y is None + assert glyph.x == field("x") + assert glyph.y == field("y") assert glyph.angle is None assert glyph.length is None check_line_properties(glyph) @@ -430,8 +430,8 @@ def test_Ray() -> None: def test_Rect() -> None: glyph = Rect() - assert glyph.x is None - assert glyph.y is None + assert glyph.x == field("x") + assert glyph.y == field("y") assert glyph.width is None assert glyph.height is None assert glyph.angle == 0 @@ -453,10 +453,10 @@ def test_Rect() -> None: def test_Segment() -> None: glyph = Segment() - assert glyph.x0 is None - assert glyph.y0 is None - assert glyph.x1 is None - assert glyph.y1 is None + assert glyph.x0 == field("x0") + assert glyph.y0 == field("y0") + assert glyph.x1 == field("x1") + assert glyph.y1 == field("y1") check_line_properties(glyph) check_properties_existence(glyph, [ "x0", @@ -468,8 +468,8 @@ def test_Segment() -> None: def test_Step() -> None: glyph = Step() - assert glyph.x is None - assert glyph.y is None + assert glyph.x == field("x") + assert glyph.y == field("y") assert glyph.mode == "before" check_line_properties(glyph) check_properties_existence(glyph, [ @@ -481,8 +481,8 @@ def test_Step() -> None: def test_Text() -> None: glyph = Text() - assert glyph.x is None - assert glyph.y is None + assert glyph.x == field("x") + assert glyph.y == field("y") assert glyph.text == "text" assert glyph.angle == 0 check_text_properties(glyph) @@ -499,9 +499,9 @@ def test_Text() -> None: def test_VArea() -> None: glyph = VArea() - assert glyph.x is None - assert glyph.y1 is None - assert glyph.y2 is None + assert glyph.x == field("x") + assert glyph.y1 == field("y1") + assert glyph.y2 == field("y2") check_fill_properties(glyph) check_hatch_properties(glyph) check_properties_existence(glyph, [ @@ -513,7 +513,7 @@ def test_VArea() -> None: def test_VBar() -> None: glyph = VBar() - assert glyph.x is None + assert glyph.x == field("x") assert glyph.width is None assert glyph.top is None assert glyph.bottom == 0 @@ -530,8 +530,8 @@ def test_VBar() -> None: def test_Wedge() -> None: glyph = Wedge() - assert glyph.x is None - assert glyph.y is None + assert glyph.x == field("x") + assert glyph.y == field("y") assert glyph.radius is None assert glyph.start_angle is None assert glyph.end_angle is None