Skip to content

Commit

Permalink
Add support for screen coordinates to renderers
Browse files Browse the repository at this point in the history
  • Loading branch information
mattpap committed May 13, 2024
1 parent bbe0b0b commit ae96e8c
Show file tree
Hide file tree
Showing 17 changed files with 207 additions and 94 deletions.
2 changes: 2 additions & 0 deletions bokehjs/src/lib/core/enums.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {Enum} from "./kinds"

export {Auto} from "./kinds"

export const Align = Enum("start", "center", "end")
export type Align = typeof Align["__type__"]

Expand Down
4 changes: 3 additions & 1 deletion bokehjs/src/lib/core/kinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -654,9 +654,11 @@ export const Positive = <BaseType extends number>(base_type: Kind<BaseType>) =>
export const Percent = new Kinds.Percent()
export const Alpha = Percent
export const Color = new Kinds.Color()
export const Auto = Enum("auto")
export const CSSLength = new Kinds.CSSLength()

export const Auto = Enum("auto")
export type Auto = typeof Auto["__type__"]

export const FontSize = Str
export const Font = Str
export const Angle = Float
Expand Down
4 changes: 4 additions & 0 deletions bokehjs/src/lib/core/view_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ abstract class AbstractViewQuery {
find_all_by_id(id: string): View[] {
return [...this.find_by_id(id)]
}

collect<T extends HasProps>(models: T[]): ViewOf<T>[] {
return models.map((model) => this.get_one(model))
}
}

export class ViewQuery extends AbstractViewQuery {
Expand Down
84 changes: 54 additions & 30 deletions bokehjs/src/lib/models/coordinates/coordinate_mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,42 @@ import {CompositeScale} from "../scales/composite_scale"
import {Range} from "../ranges/range"
import {DataRange1d} from "../ranges/data_range1d"
import {FactorRange} from "../ranges/factor_range"
import type {CartesianFrameView} from "../canvas/cartesian_frame"
import {assert} from "core/util/assert"
import type {Auto} from "core/enums"
import type * as p from "core/properties"

export type CoordinateSource = {
x_scale: Scale
y_scale: Scale
}

export class CoordinateTransform {
readonly x_source: Range
readonly y_source: Range

readonly x_scale: Scale
readonly y_scale: Scale

readonly x_source: Range
readonly y_source: Range
readonly x_target: Range
readonly y_target: Range

readonly ranges: readonly [Range, Range]
readonly scales: readonly [Scale, Scale]

readonly x_ranges: Map<string, Range>
readonly y_ranges: Map<string, Range>

constructor(x_scale: Scale, y_scale: Scale) {
this.x_scale = x_scale
this.y_scale = y_scale
this.x_source = this.x_scale.source_range
this.y_source = this.y_scale.source_range
this.x_target = this.x_scale.target_range
this.y_target = this.y_scale.target_range
this.ranges = [this.x_source, this.y_source]
this.scales = [this.x_scale, this.y_scale]
this.x_ranges = new Map([["default", this.x_source]])
this.y_ranges = new Map([["default", this.y_source]])
}

map_to_screen(xs: Arrayable<number>, ys: Arrayable<number>): [ScreenArray, ScreenArray] {
Expand All @@ -41,6 +57,18 @@ export class CoordinateTransform {
const ys = this.y_scale.v_invert(sys)
return [xs, ys]
}

compose(onto: CoordinateSource): CoordinateTransform {
const x_scale = new CompositeScale({
source_scale: this.x_scale, source_range: this.x_scale.source_range,
target_scale: onto.x_scale, target_range: onto.x_scale.target_range,
})
const y_scale = new CompositeScale({
source_scale: this.y_scale, source_range: this.y_scale.source_range,
target_scale: onto.y_scale, target_range: onto.y_scale.target_range,
})
return new CoordinateTransform(x_scale, y_scale)
}
}

export namespace CoordinateMapping {
Expand All @@ -51,8 +79,9 @@ export namespace CoordinateMapping {
y_source: p.Property<Range>
x_scale: p.Property<Scale>
y_scale: p.Property<Scale>
x_target: p.Property<Range>
y_target: p.Property<Range>
x_target: p.Property<Range | Auto>
y_target: p.Property<Range | Auto>
target: p.Property</*CoordinateSource |*/ "frame" | null>
}
}

Expand All @@ -66,24 +95,17 @@ export class CoordinateMapping extends Model {
}

static {
this.define<CoordinateMapping.Props>(({Ref}) => ({
this.define<CoordinateMapping.Props>(({Auto, Enum, Ref, Or, Nullable}) => ({
x_source: [ Ref(Range), () => new DataRange1d() ],
y_source: [ Ref(Range), () => new DataRange1d() ],
x_scale: [ Ref(Scale), () => new LinearScale() ],
y_scale: [ Ref(Scale), () => new LinearScale() ],
x_target: [ Ref(Range) ],
y_target: [ Ref(Range) ],
x_target: [ Or(Ref(Range), Auto), "auto" ],
y_target: [ Or(Ref(Range), Auto), "auto" ],
target: [ Nullable(Enum("frame")), "frame" ],
}))
}

get x_ranges(): Map<string, Range> {
return new Map([["default", this.x_source]])
}

get y_ranges(): Map<string, Range> {
return new Map([["default", this.y_source]])
}

private _get_scale(range: Range, scale: Scale, target: Range): Scale {
const factor_range = range instanceof FactorRange
const categorical_scale = scale instanceof CategoricalScale
Expand All @@ -101,22 +123,24 @@ export class CoordinateMapping extends Model {
return derived_scale
}

get_transform(frame: CartesianFrameView): CoordinateTransform {
const {x_source, x_scale, x_target} = this
const x_source_scale = this._get_scale(x_source, x_scale, x_target)
get_transform(target?: CoordinateSource): CoordinateTransform {
const {x_source, y_source} = this
const {x_scale: in_x_scale, y_scale: in_y_scale} = this
let {x_target, y_target} = this

const {y_source, y_scale, y_target} = this
const y_source_scale = this._get_scale(y_source, y_scale, y_target)
if (x_target == "auto") {
assert(target != null)
x_target = target.x_scale.target_range
}
if (y_target == "auto") {
assert(target != null)
y_target = target.y_scale.target_range
}

const xscale = new CompositeScale({
source_scale: x_source_scale, source_range: x_source_scale.source_range,
target_scale: frame.x_scale, target_range: frame.x_target,
})
const yscale = new CompositeScale({
source_scale: y_source_scale, source_range: y_source_scale.source_range,
target_scale: frame.y_scale, target_range: frame.y_target,
})
const x_scale = this._get_scale(x_source, in_x_scale, x_target)
const y_scale = this._get_scale(y_source, in_y_scale, y_target)

return new CoordinateTransform(xscale, yscale)
const transform = new CoordinateTransform(x_scale, y_scale)
return target != null ? transform.compose(target) : transform
}
}
5 changes: 0 additions & 5 deletions bokehjs/src/lib/models/plots/plot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import type {Glyph} from "../glyphs/glyph"
import type {ColumnarDataSource} from "../sources/columnar_data_source"
import {ColumnDataSource} from "../sources/column_data_source"
import {Renderer} from "../renderers/renderer"
import {DataRenderer} from "../renderers/data_renderer"
import {GlyphRenderer} from "../renderers/glyph_renderer"
import type {ToolAliases} from "../tools/tool"
import {Tool} from "../tools/tool"
Expand Down Expand Up @@ -227,10 +226,6 @@ export class Plot extends LayoutDOM {
del(this.center)
}

get data_renderers(): DataRenderer[] {
return this.renderers.filter((r): r is DataRenderer => r instanceof DataRenderer)
}

add_renderers(...renderers: Renderer[]): void {
this.renderers = [...this.renderers, ...renderers]
}
Expand Down
31 changes: 22 additions & 9 deletions bokehjs/src/lib/models/plots/plot_canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {Canvas} from "../canvas/canvas"
import type {Renderer} from "../renderers/renderer"
import {RendererView} from "../renderers/renderer"
import type {DataRenderer} from "../renderers/data_renderer"
import {DataRendererView} from "../renderers/data_renderer"
import type {Range} from "../ranges/range"
import type {Tool} from "../tools/tool"
import {ToolProxy} from "../tools/tool_proxy"
Expand Down Expand Up @@ -36,7 +37,7 @@ import type {Side, RenderLevel} from "core/enums"
import type {View} from "core/view"
import {Signal0} from "core/signaling"
import {throttle} from "core/util/throttle"
import {isBoolean, isArray, isString, isNotNull} from "core/util/types"
import {isBoolean, isArray, isString, non_null} from "core/util/types"
import {copy, reversed} from "core/util/array"
import {flat_map} from "core/util/iterator"
import type {Context2d} from "core/util/canvas"
Expand Down Expand Up @@ -125,14 +126,24 @@ export class PlotView extends LayoutDOMView implements Paintable {

protected throttled_paint: () => void

computed_renderers: Renderer[] = []
private _computed_renderers: Renderer[] = []

get computed_renderer_views(): RendererView[] {
return this.computed_renderers.map((r) => this.renderer_views.get(r)).filter(isNotNull) // TODO race condition again
return this._computed_renderers.map((r) => this.renderer_views.get(r)).filter(non_null) // TODO race condition again
}

get data_renderers(): DataRendererView[] {
return this
.computed_renderer_views
.filter((rv): rv is DataRendererView => rv instanceof DataRendererView)
}

get frame_renderers(): RendererView[] {
return this.computed_renderer_views.filter((rv) => rv.is_frame_renderer)
}

get auto_ranged_renderers(): (RendererView & AutoRanged)[] {
return this.computed_renderer_views.filter(is_auto_ranged)
return this.frame_renderers.filter(is_auto_ranged)
}

get base_font_size(): number | null {
Expand Down Expand Up @@ -662,15 +673,17 @@ export class PlotView extends LayoutDOMView implements Paintable {

get_selection(): Map<DataRenderer, Selection> {
const selection = new Map<DataRenderer, Selection>()
for (const renderer of this.model.data_renderers) {
for (const renderer_view of this.data_renderers) {
const renderer = renderer_view.model
const {selected} = renderer.selection_manager.source
selection.set(renderer, selected)
}
return selection
}

update_selection(selections: Map<DataRenderer, Selection> | null): void {
for (const renderer of this.model.data_renderers) {
for (const renderer_view of this.data_renderers) {
const renderer = renderer_view.model
const ds = renderer.selection_manager.source
if (selections != null) {
const selection = selections.get(renderer)
Expand Down Expand Up @@ -731,15 +744,15 @@ export class PlotView extends LayoutDOMView implements Paintable {
const attribution = [
...this.model.attribution,
...this.computed_renderer_views.map((rv) => rv.attribution),
].filter(isNotNull)
].filter(non_null)
const elements = attribution.map((attrib) => isString(attrib) ? new Div({children: [attrib]}) : attrib)
this._attribution.elements = elements
// TODO this._attribution.title = contents_el.textContent!.replace(/\s*\n\s*/g, " ")
}

protected async _build_renderers(): Promise<BuildResult<Renderer>> {
this.computed_renderers = [...this._compute_renderers()]
const result = await build_views(this.renderer_views, this.computed_renderers, {parent: this})
this._computed_renderers = [...this._compute_renderers()]
const result = await build_views(this.renderer_views, this._computed_renderers, {parent: this})
this._update_attribution()
return result
}
Expand Down
19 changes: 9 additions & 10 deletions bokehjs/src/lib/models/plots/range_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {Range} from "../ranges/range"
import type {Bounds} from "../ranges/data_range1d"
import {DataRange1d} from "../ranges/data_range1d"
import type {CartesianFrameView} from "../canvas/cartesian_frame"
import type {CoordinateMapping} from "../coordinates/coordinate_mapping"
import type {CoordinateTransform} from "../coordinates/coordinate_mapping"
import type {Dimensions} from "../ranges/auto_ranged"
import type {PlotView} from "./plot_canvas"
import type {Interval, Rect} from "core/types"
Expand Down Expand Up @@ -55,11 +55,10 @@ export class RangeManager {
for (const range of this.frame.y_ranges.values()) {
y_ranges.add(range)
}
for (const renderer of this.parent.model.data_renderers) {
const {coordinates} = renderer
if (coordinates != null) {
x_ranges.add(coordinates.x_source)
y_ranges.add(coordinates.y_source)
for (const renderer_view of this.parent.data_renderers) {
if (renderer_view.is_subcoordinate_renderer) {
x_ranges.add(renderer_view.coordinates.x_source)
y_ranges.add(renderer_view.coordinates.y_source)
}
}

Expand All @@ -80,7 +79,7 @@ export class RangeManager {
this.update_dataranges()
}

protected _update_dataranges(frame: CartesianFrameView | CoordinateMapping): void {
protected _update_dataranges(frame: CartesianFrameView | CoordinateTransform): void {
// Update any DataRange1ds here
const bounds: Bounds = new Map()
const log_bounds: Bounds = new Map()
Expand Down Expand Up @@ -170,10 +169,10 @@ export class RangeManager {
update_dataranges(): void {
this._update_dataranges(this.frame)

for (const renderer of this.parent.auto_ranged_renderers) {
const {coordinates} = renderer.model
for (const renderer_view of this.parent.auto_ranged_renderers) {
const {coordinates} = renderer_view.model
if (coordinates != null) {
this._update_dataranges(coordinates)
this._update_dataranges(renderer_view.coordinates)
}
}

Expand Down
19 changes: 15 additions & 4 deletions bokehjs/src/lib/models/renderers/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,29 @@ export abstract class RendererView extends StyledElementView implements visuals.
this.connect(this.plot_view.frame.model.change, () => delete this._coordinates)
}

get is_frame_renderer(): boolean {
const {coordinates} = this.model
return coordinates == null || coordinates.target == "frame"
}

get is_subcoordinate_renderer(): boolean {
const {coordinates} = this.model
return coordinates != null && coordinates.target == "frame"
}

protected _initialize_coordinates(): CoordinateTransform {
if (this._custom_coordinates != null) {
return this._custom_coordinates
}
const {coordinates} = this.model
const {frame} = this.plot_view
const {frame_view} = this.plot_view
if (coordinates != null) {
return coordinates.get_transform(frame)
const target = coordinates.target == "frame" ? frame_view : undefined
return coordinates.get_transform(target)
} else {
const {x_range_name, y_range_name} = this.model
const x_scale = frame.x_scales.get(x_range_name)
const y_scale = frame.y_scales.get(y_range_name)
const x_scale = frame_view.x_scales.get(x_range_name)
const y_scale = frame_view.y_scales.get(y_range_name)
assert(x_scale != null, `missing '${x_range_name}' range`)
assert(y_scale != null, `missing '${y_range_name}' range`)
return new CoordinateTransform(x_scale, y_scale)
Expand Down
10 changes: 4 additions & 6 deletions bokehjs/src/lib/models/tools/actions/zoom_base_tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,13 @@ export abstract class ZoomBaseToolView extends PlotActionToolView {
const x_scales = [...x_frame_scales.values()]
const y_scales = [...y_frame_scales.values()]

const data_renderers = renderers != "auto" ? renderers : this.plot_view.model.data_renderers
const data_renderers = renderers != "auto" ? this.plot_view.views.collect(renderers) : this.plot_view.data_renderers

for (const renderer of data_renderers) {
if (renderer.coordinates == null) {
for (const renderer_view of data_renderers) {
if (!renderer_view.is_subcoordinate_renderer) {
continue
}

const rv = this.plot_view.views.get_one(renderer)

const process = (scale: Scale, dim: "x" | "y") => {
const {level} = this.model
for (let i = 0; i < level; i++) {
Expand All @@ -78,7 +76,7 @@ export abstract class ZoomBaseToolView extends PlotActionToolView {
}
}

const {x_scale, y_scale} = rv.coordinates
const {x_scale, y_scale} = renderer_view.coordinates
x_scales.push(process(x_scale, "x"))
y_scales.push(process(y_scale, "y"))
}
Expand Down
2 changes: 1 addition & 1 deletion bokehjs/src/lib/models/tools/gestures/select_tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export abstract class SelectToolView extends GestureToolView {

get computed_renderers(): DataRenderer[] {
const {renderers} = this.model
const all_renderers = this.plot_view.model.data_renderers
const all_renderers = this.plot_view.data_renderers.map((rv) => rv.model)
return compute_renderers(renderers, all_renderers)
}

Expand Down

0 comments on commit ae96e8c

Please sign in to comment.