@bokeh/bokehjs
Version:
Interactive, novel data visualization
1,151 lines (1,149 loc) • 44.5 kB
JavaScript
import { CartesianFrame } from "../canvas/cartesian_frame";
import { CanvasPanel } from "../canvas/canvas_panel";
import { Canvas } from "../canvas/canvas";
import { RendererView } from "../renderers/renderer";
import { CompositeRendererView } from "../renderers/composite_renderer";
import { ToolProxy } from "../tools/tool_proxy";
import { ToolMenu } from "../tools/tool_menu";
import { LayoutDOMView } from "../layouts/layout_dom";
import { Annotation, AnnotationView } from "../annotations/annotation";
import { Title } from "../annotations/title";
import { AxisView } from "../axes/axis";
import { ToolbarPanel } from "../annotations/toolbar_panel";
import { is_auto_ranged } from "../ranges/data_range1d";
import { Panel } from "../ui/panel";
import { Div } from "../dom/elements";
import { Reset } from "../../core/bokeh_events";
import { build_views, remove_views } from "../../core/build_views";
import { Visuals } from "../../core/visuals";
import { logger } from "../../core/logging";
import { RangesUpdate } from "../../core/bokeh_events";
import { Signal0 } from "../../core/signaling";
import { throttle } from "../../core/util/throttle";
import { isBoolean, isArray, isString } from "../../core/util/types";
import { copy, reversed } from "../../core/util/array";
import { flat_map } from "../../core/util/iterator";
import { CanvasLayer } from "../../core/util/canvas";
import { HStack, VStack, NodeLayout } from "../../core/layout/alignments";
import { BorderLayout } from "../../core/layout/border";
import { Row, Column } from "../../core/layout/grid";
import { SidePanel } from "../../core/layout/side_panel";
import { BBox } from "../../core/util/bbox";
import { parse_css_font_size } from "../../core/util/text";
import { RangeManager } from "./range_manager";
import { StateManager } from "./state_manager";
import { settings } from "../../core/settings";
import { InlineStyleSheet, px } from "../../core/dom";
import { Node } from "../coordinates/node";
import * as plots_css from "../../styles/plots.css";
import * as canvas_css from "../../styles/canvas.css";
import * as attribution_css from "../../styles/attribution.css";
const { max } = Math;
export class PlotView extends LayoutDOMView {
static __name__ = "PlotView";
visuals;
_top_panel;
_bottom_panel;
_left_panel;
_right_panel;
top_panel;
bottom_panel;
left_panel;
right_panel;
_frame;
frame_view;
get frame() {
return this.frame_view;
}
_canvas;
canvas_view;
get canvas() {
return this.canvas_view;
}
_render_count = 0;
repainted = new Signal0(this, "repainted");
_computed_style = new InlineStyleSheet("", "computed");
stylesheets() {
return [...super.stylesheets(), plots_css.default, this._computed_style];
}
_title;
_toolbar;
_attribution;
_notifications;
get toolbar_panel() {
return this._toolbar != null ? this.views.find_one(this._toolbar) : null;
}
_outer_bbox = new BBox();
_inner_bbox = new BBox();
_needs_paint = true;
_invalidated_painters = new Set();
_invalidate_all = true;
_state_manager;
_range_manager;
get state() {
return this._state_manager;
}
set invalidate_dataranges(value) {
this._range_manager.invalidate_dataranges = value;
}
lod_started;
_initial_state;
throttled_paint;
computed_renderers = [];
get computed_renderer_views() {
return this
.computed_renderers
.map((r) => this.renderer_views.get(r))
.filter((rv) => rv != null); // TODO race condition again
}
get all_renderer_views() {
const collected = [];
for (const rv of this.computed_renderer_views) {
collected.push(rv);
if (rv instanceof CompositeRendererView) {
collected.push(...rv.computed_renderer_views);
}
}
return collected;
}
get auto_ranged_renderers() {
return this.computed_renderer_views.filter(is_auto_ranged);
}
get base_font_size() {
const font_size = getComputedStyle(this.el).fontSize;
const result = parse_css_font_size(font_size);
if (result != null) {
const { value, unit } = result;
if (unit == "px") {
return value;
}
}
return null;
}
/*protected*/ renderer_views = new Map();
/*protected*/ tool_views = new Map();
*children() {
yield* super.children();
yield* this.renderer_views.values();
yield* this.tool_views.values();
}
get child_models() {
return [];
}
_is_paused = 0;
get is_paused() {
return this._is_paused != 0;
}
pause() {
this._is_paused += 1;
}
unpause(no_render = false) {
this._is_paused = max(this._is_paused - 1, 0);
if (!this.is_paused && !no_render) {
this.request_repaint();
}
}
_needs_notify = false;
notify_finished_after_paint() {
this._needs_notify = true;
}
request_repaint() {
this.request_paint();
}
request_paint(...to_invalidate) {
this.invalidate_painters(...to_invalidate);
this.schedule_paint();
}
invalidate_painters(...to_invalidate) {
if (to_invalidate.length == 0) {
this._invalidate_all = true;
return;
}
for (const item of to_invalidate) {
const view = (() => {
if (item instanceof RendererView) {
return item;
}
else {
return this.views.get_one(item);
}
})();
this._invalidated_painters.add(view);
}
}
schedule_paint() {
if (!this.is_paused) {
this._await_ready(this.throttled_paint());
}
}
request_layout() {
this.request_repaint();
}
reset() {
if (this.model.reset_policy == "standard") {
this.state.clear();
this.reset_range();
this.reset_selection();
}
this.model.trigger_event(new Reset());
}
remove() {
remove_views(this.renderer_views);
remove_views(this.tool_views);
super.remove();
}
_provide_context_menu() {
return new ToolMenu({ toolbar: this.model.toolbar });
}
get_context_menu(xy) {
const { x, y } = xy;
for (const rv of reversed([...this.renderer_views.values()])) {
if (rv.context_menu != null && rv.interactive_hit?.(x, y) == true) {
return rv.context_menu;
}
}
return super.get_context_menu(xy);
}
initialize() {
this.pause();
super.initialize();
this.lod_started = false;
this.visuals = new Visuals(this);
this._initial_state = {
selection: new Map(), // XXX: initial selection?
};
this._top_panel = new CanvasPanel({ place: "above" });
this._bottom_panel = new CanvasPanel({ place: "below" });
this._left_panel = new CanvasPanel({ place: "left" });
this._right_panel = new CanvasPanel({ place: "right" });
this._frame = new CartesianFrame({
place: "center",
x_scale: this.model.x_scale,
y_scale: this.model.y_scale,
x_range: this.model.x_range,
y_range: this.model.y_range,
extra_x_ranges: this.model.extra_x_ranges,
extra_y_ranges: this.model.extra_y_ranges,
extra_x_scales: this.model.extra_x_scales,
extra_y_scales: this.model.extra_y_scales,
aspect_scale: this.model.aspect_scale,
match_aspect: this.model.match_aspect,
});
this._range_manager = new RangeManager(this);
this._state_manager = new StateManager(this, this._initial_state);
this.throttled_paint = throttle(() => {
if (!this.is_destroyed) {
this.repaint();
}
}, 1000 / 60);
const { title_location, title } = this.model;
if (title_location != null && title != null) {
this._title = title instanceof Title ? title : new Title({ text: title });
}
const { toolbar_location, toolbar_inner, toolbar } = this.model;
if (toolbar_location != null) {
this._toolbar = new ToolbarPanel({ toolbar });
toolbar.location = toolbar_location;
toolbar.inner = toolbar_inner;
}
const { hidpi, output_backend } = this.model;
this._canvas = new Canvas({ hidpi, output_backend });
this._attribution = new Panel({
position: new Node({ target: "frame", symbol: "bottom_right" }),
anchor: "bottom_right",
elements: [],
css_variables: {
"--max-width": new Node({ target: "frame", symbol: "width" }),
},
stylesheets: [attribution_css.default],
});
this._notifications = new Panel({
position: new Node({ target: this.model, symbol: "top_center" }),
anchor: "top_center",
elements: [],
stylesheets: [`
:host {
display: flex;
flex-direction: column;
gap: 1em;
width: max-content;
max-width: 80%;
}
:host:empty {
display: none;
}
:host > div {
padding: 0.5em;
border: 1px solid gray;
border-radius: 0.5em;
opacity: 0.8;
}
`],
});
}
get elements() {
return [
this._canvas,
this._frame,
this._top_panel,
this._bottom_panel,
this._left_panel,
this._right_panel,
this._attribution,
this._notifications,
...super.elements,
];
}
async lazy_initialize() {
await super.lazy_initialize();
this.canvas_view = this._element_views.get(this._canvas);
this.canvas_view.plot_views = [this];
this.frame_view = this._element_views.get(this._frame);
this.top_panel = this._element_views.get(this._top_panel);
this.bottom_panel = this._element_views.get(this._bottom_panel);
this.left_panel = this._element_views.get(this._left_panel);
this.right_panel = this._element_views.get(this._right_panel);
await this.build_tool_views();
await this.build_renderer_views();
this._range_manager.update_dataranges();
this._update_touch_action(); // active_changed emits too early, so update manually the first time
}
box_sizing() {
const { width_policy, height_policy, ...sizing } = super.box_sizing();
const { frame_width, frame_height } = this.model;
return {
...sizing,
width_policy: frame_width != null && width_policy == "auto" ? "fit" : width_policy,
height_policy: frame_height != null && height_policy == "auto" ? "fit" : height_policy,
};
}
_intrinsic_display() {
return { inner: this.model.flow_mode, outer: "grid" };
}
_update_layout() {
super._update_layout();
// TODO: invalidating all should imply "needs paint"
this._invalidate_all = true;
this._needs_paint = true;
const layout = new BorderLayout();
const { frame_align } = this.model;
layout.aligns = (() => {
if (isBoolean(frame_align)) {
return { left: frame_align, right: frame_align, top: frame_align, bottom: frame_align };
}
else {
const { left = true, right = true, top = true, bottom = true } = frame_align;
return { left, right, top, bottom };
}
})();
layout.set_sizing({ width_policy: "max", height_policy: "max" });
if (this.visuals.outline_line.doit) {
const width = this.visuals.outline_line.line_width.get_value();
layout.center_border_width = width;
}
const outer_above = copy(this.model.above);
const outer_below = copy(this.model.below);
const outer_left = copy(this.model.left);
const outer_right = copy(this.model.right);
const inner_above = [];
const inner_below = [];
const inner_left = [];
const inner_right = [];
const get_side = (side, inner = false) => {
switch (side) {
case "above": return inner ? inner_above : outer_above;
case "below": return inner ? inner_below : outer_below;
case "left": return inner ? inner_left : outer_left;
case "right": return inner ? inner_right : outer_right;
}
};
const { title_location } = this.model;
if (title_location != null && this._title != null) {
get_side(title_location).push(this._title);
}
if (this._toolbar != null) {
const { location } = this._toolbar.toolbar;
if (!this.model.toolbar_inner) {
const panels = get_side(location);
let push_toolbar = true;
if (this.model.toolbar_sticky) {
for (let i = 0; i < panels.length; i++) {
const panel = panels[i];
if (panel instanceof Title) {
if (location == "above" || location == "below") {
panels[i] = [panel, this._toolbar];
}
else {
panels[i] = [this._toolbar, panel];
}
push_toolbar = false;
break;
}
}
}
if (push_toolbar) {
panels.push(this._toolbar);
}
}
else {
const panels = get_side(location, true);
panels.push(this._toolbar);
}
}
const set_layout = (side, model) => {
const view = this.views.get_one(model);
view.panel = new SidePanel(side);
view.update_layout?.();
return view.layout;
};
const set_layouts = (side, panels) => {
const horizontal = side == "above" || side == "below";
const layouts = [];
for (const panel of panels) {
if (isArray(panel)) {
const items = panel.map((subpanel) => {
const item = set_layout(side, subpanel);
if (item == null) {
return undefined;
}
if (subpanel instanceof ToolbarPanel) {
const dim = horizontal ? "width_policy" : "height_policy";
item.set_sizing({ ...item.sizing, [dim]: "min" });
}
return item;
}).filter((item) => item != null);
let layout;
if (horizontal) {
layout = new Row(items);
layout.set_sizing({ width_policy: "max", height_policy: "min" });
}
else {
layout = new Column(items);
layout.set_sizing({ width_policy: "min", height_policy: "max" });
}
layout.absolute = true;
layouts.push(layout);
}
else {
const layout = set_layout(side, panel);
if (layout != null) {
layouts.push(layout);
}
}
}
return layouts;
};
const min_border = this.model.min_border ?? 0;
layout.min_border = {
left: this.model.min_border_left ?? min_border,
top: this.model.min_border_top ?? min_border,
right: this.model.min_border_right ?? min_border,
bottom: this.model.min_border_bottom ?? min_border,
};
const center_panel = new NodeLayout();
const top_panel = new VStack();
const bottom_panel = new VStack();
const left_panel = new HStack();
const right_panel = new HStack();
const inner_top_panel = new VStack();
const inner_bottom_panel = new VStack();
const inner_left_panel = new HStack();
const inner_right_panel = new HStack();
center_panel.absolute = true;
top_panel.absolute = true;
bottom_panel.absolute = true;
left_panel.absolute = true;
right_panel.absolute = true;
inner_top_panel.absolute = true;
inner_bottom_panel.absolute = true;
inner_left_panel.absolute = true;
inner_right_panel.absolute = true;
center_panel.children =
this.model.center.filter((obj) => {
return obj instanceof Annotation;
}).map((model) => {
const view = this.views.get_one(model);
view.update_layout?.();
return view.layout;
}).filter((layout) => layout != null);
const { frame_width, frame_height } = this.model;
center_panel.set_sizing({
...(frame_width != null ? { width_policy: "fixed", width: frame_width } : { width_policy: "fit" }),
...(frame_height != null ? { height_policy: "fixed", height: frame_height } : { height_policy: "fit" }),
});
center_panel.on_resize((bbox) => this.frame.set_geometry(bbox));
top_panel.on_resize((bbox) => this.top_panel.set_geometry(bbox));
bottom_panel.on_resize((bbox) => this.bottom_panel.set_geometry(bbox));
left_panel.on_resize((bbox) => this.left_panel.set_geometry(bbox));
right_panel.on_resize((bbox) => this.right_panel.set_geometry(bbox));
top_panel.children = reversed(set_layouts("above", outer_above));
bottom_panel.children = set_layouts("below", outer_below);
left_panel.children = reversed(set_layouts("left", outer_left));
right_panel.children = set_layouts("right", outer_right);
inner_top_panel.children = set_layouts("above", inner_above);
inner_bottom_panel.children = set_layouts("below", inner_below);
inner_left_panel.children = set_layouts("left", inner_left);
inner_right_panel.children = set_layouts("right", inner_right);
top_panel.set_sizing({ width_policy: "fit", height_policy: "min" /*, min_height: layout.min_border.top*/ });
bottom_panel.set_sizing({ width_policy: "fit", height_policy: "min" /*, min_height: layout.min_width.bottom*/ });
left_panel.set_sizing({ width_policy: "min", height_policy: "fit" /*, min_width: layout.min_width.left*/ });
right_panel.set_sizing({ width_policy: "min", height_policy: "fit" /*, min_width: layout.min_width.right*/ });
inner_top_panel.set_sizing({ width_policy: "fit", height_policy: "min" });
inner_bottom_panel.set_sizing({ width_policy: "fit", height_policy: "min" });
inner_left_panel.set_sizing({ width_policy: "min", height_policy: "fit" });
inner_right_panel.set_sizing({ width_policy: "min", height_policy: "fit" });
layout.center_panel = center_panel;
layout.top_panel = top_panel;
layout.bottom_panel = bottom_panel;
layout.left_panel = left_panel;
layout.right_panel = right_panel;
if (inner_top_panel.children.length != 0) {
layout.inner_top_panel = inner_top_panel;
}
if (inner_bottom_panel.children.length != 0) {
layout.inner_bottom_panel = inner_bottom_panel;
}
if (inner_left_panel.children.length != 0) {
layout.inner_left_panel = inner_left_panel;
}
if (inner_right_panel.children.length != 0) {
layout.inner_right_panel = inner_right_panel;
}
this.layout = layout;
const above_els = this.views.select(this.model.above).map((view) => view.el);
const below_els = this.views.select(this.model.below).map((view) => view.el);
const left_els = this.views.select(this.model.left).map((view) => view.el);
const right_els = this.views.select(this.model.right).map((view) => view.el);
const center_els = this.views.select(this.model.center).map((view) => view.el);
const renderer_els = this.views.select(this.model.renderers).map((view) => view.el);
this.top_panel.shadow_el.append(...reversed(above_els));
this.bottom_panel.shadow_el.append(...below_els);
this.left_panel.shadow_el.append(...reversed(left_els));
this.right_panel.shadow_el.append(...right_els);
this.frame.shadow_el.append(...renderer_els, ...center_els);
}
_measure_layout() {
const { frame_width, frame_height } = this.model;
const frame = {
width: frame_width == null ? "1fr" : px(frame_width),
height: frame_height == null ? "1fr" : px(frame_height),
};
const { layout } = this;
const top = layout.top_panel.measure({ width: Infinity, height: Infinity });
const bottom = layout.bottom_panel.measure({ width: Infinity, height: Infinity });
const left = layout.left_panel.measure({ width: Infinity, height: Infinity });
const right = layout.right_panel.measure({ width: Infinity, height: Infinity });
const top_height = max(top.height, layout.min_border.top);
const bottom_height = max(bottom.height, layout.min_border.bottom);
const left_width = max(left.width, layout.min_border.left);
const right_width = max(right.width, layout.min_border.right);
this._computed_style.replace(`
:host {
grid-template-rows: ${top_height}px ${frame.height} ${bottom_height}px;
grid-template-columns: ${left_width}px ${frame.width} ${right_width}px;
}
`);
}
get axis_views() {
const views = [];
for (const [, renderer_view] of this.renderer_views) {
if (renderer_view instanceof AxisView) {
views.push(renderer_view);
}
}
return views;
}
update_range(range_info, options) {
this.pause();
this._range_manager.update(range_info, options);
this.unpause();
}
reset_range() {
this.pause();
this._range_manager.reset();
this.unpause();
this.trigger_ranges_update_event();
}
trigger_ranges_update_event(extra_ranges = []) {
/**
* Emits `RangesUpdate` event on all plots linked by all
* ranges managed by this plot's range manager and linked
* by additional context dependent ranges (`extra_ranges`).
*/
const { x_ranges, y_ranges } = this._range_manager.ranges();
const ranges = [...x_ranges, ...y_ranges, ...extra_ranges];
const linked_plots = new Set(ranges.flatMap((r) => [...r.linked_plots]));
for (const plot_view of linked_plots) {
const { x_range, y_range } = plot_view.model;
const event = new RangesUpdate(x_range.start, x_range.end, y_range.start, y_range.end);
plot_view.model.trigger_event(event);
}
}
get_selection() {
const selection = new Map();
for (const renderer of this.model.data_renderers) {
const { selected } = renderer.selection_manager.source;
selection.set(renderer, selected);
}
return selection;
}
update_selection(selections) {
for (const renderer of this.model.data_renderers) {
const ds = renderer.selection_manager.source;
if (selections != null) {
const selection = selections.get(renderer);
if (selection != null) {
ds.selected.update(selection, true);
}
}
else {
ds.selection_manager.clear();
}
}
}
reset_selection() {
this.update_selection(null);
}
_invalidate_layout_if_needed() {
const needs_layout = (() => {
for (const panel of this.model.side_panels) {
const view = this.renderer_views.get(panel);
if (view.layout?.has_size_changed() ?? false) {
this.invalidate_painters(view);
return true;
}
}
return false;
})();
if (needs_layout) {
this.compute_layout();
}
}
*_compute_renderers() {
const { above, below, left, right, center, renderers } = this.model;
yield* renderers;
yield* above;
yield* below;
yield* left;
yield* right;
yield* center;
if (this._title != null) {
yield this._title;
}
if (this._toolbar != null) {
yield this._toolbar;
}
for (const [, view] of this.tool_views) {
yield* view.overlays;
}
}
_update_attribution() {
const attribution = [
...this.model.attribution,
...this.computed_renderer_views.map((rv) => rv.attribution),
].filter((rv) => rv != 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, " ")
}
async _build_renderers() {
this.computed_renderers = [...this._compute_renderers()];
const result = await build_views(this.renderer_views, this.computed_renderers, { parent: this });
this._update_attribution();
return result;
}
async _update_renderers() {
const { created } = await this._build_renderers();
const created_renderers = new Set(created);
// First remove and then either reattach existing renderers or render and
// attach new renderers, so that the order of children is consistent, while
// avoiding expensive re-rendering of existing views.
for (const renderer_view of this.renderer_views.values()) {
renderer_view.el.remove();
}
for (const renderer_view of this.renderer_views.values()) {
const is_new = created_renderers.has(renderer_view);
const target = renderer_view.rendering_target();
if (is_new) {
renderer_view.render_to(target);
}
else {
target.append(renderer_view.el);
}
}
this.r_after_render();
}
async build_renderer_views() {
await this._build_renderers();
}
async build_tool_views() {
const tool_models = flat_map(this.model.toolbar.tools, (item) => item instanceof ToolProxy ? item.tools : [item]);
const { created } = await build_views(this.tool_views, [...tool_models], { parent: this });
created.map((tool_view) => this.canvas_view.ui_event_bus.register_tool(tool_view));
}
connect_signals() {
super.connect_signals();
const { x_range, y_range, x_scale, y_scale, extra_x_ranges, extra_y_ranges, extra_x_scales, extra_y_scales, aspect_scale, match_aspect, } = this.model.properties;
this.on_change([
x_range, y_range,
x_scale, y_scale,
extra_x_ranges, extra_y_ranges,
extra_x_scales, extra_y_scales,
aspect_scale, match_aspect,
], () => {
const { x_range, y_range, x_scale, y_scale, extra_x_ranges, extra_y_ranges, extra_x_scales, extra_y_scales, aspect_scale, match_aspect, } = this.model;
this._frame.setv({
x_range, y_range,
x_scale, y_scale,
extra_x_ranges, extra_y_ranges,
extra_x_scales, extra_y_scales,
aspect_scale, match_aspect,
});
});
const { above, below, left, right, center, renderers } = this.model.properties;
const panels = [above, below, left, right, center];
this.on_change(renderers, async () => {
await this._update_renderers();
});
this.on_change(panels, async () => {
await this._update_renderers();
this.invalidate_layout();
});
this.connect(this.model.toolbar.properties.tools.change, async () => {
await this.build_tool_views();
await this._update_renderers();
});
const { x_ranges, y_ranges } = this.frame;
for (const [, range] of x_ranges) {
this.connect(range.change, () => {
this.request_repaint();
});
}
for (const [, range] of y_ranges) {
this.connect(range.change, () => {
this.request_repaint();
});
}
this.connect(this.model.change, () => this.request_repaint());
this.connect(this.model.reset, () => this.reset());
const { toolbar_location } = this.model.properties;
this.on_change(toolbar_location, async () => {
const { toolbar_location } = this.model;
if (this._toolbar != null) {
if (toolbar_location != null) {
this._toolbar.toolbar.location = toolbar_location;
}
else {
this._toolbar = undefined;
await this._update_renderers();
}
}
else {
if (toolbar_location != null) {
const { toolbar, toolbar_inner } = this.model;
this._toolbar = new ToolbarPanel({ toolbar });
toolbar.location = toolbar_location;
toolbar.inner = toolbar_inner;
await this._update_renderers();
}
}
this.invalidate_layout();
});
const { hold_render } = this.model.properties;
this.on_change(hold_render, () => {
if (!this.model.hold_render) {
this.request_repaint();
}
});
this.model.toolbar.active_changed.connect(() => this._update_touch_action());
if (visualViewport != null) {
visualViewport.addEventListener("resize", () => {
if (this.canvas.resize()) {
this.request_repaint();
}
});
}
}
_update_touch_action() {
const { toolbar } = this.model;
let has_pan = false;
let has_scroll = false;
for (const tool of toolbar.tools) {
if (tool.active) {
const { event_types } = tool;
if (event_types.includes("pan")) {
has_pan = true;
}
if (event_types.includes("scroll")) {
has_scroll = true;
}
if (has_pan && has_scroll) {
break;
}
}
}
const touch_action = (() => {
if (!has_pan && !has_scroll) {
return "auto";
}
else if (!has_pan) {
return "pan-x pan-y";
}
else if (!has_scroll) {
return "pinch-zoom"; // scroll implies pinch where applicable
}
else {
return "none";
}
})();
this.canvas.touch_action.replace(`
.${canvas_css.events} {
touch-action: ${touch_action};
}
`);
}
has_finished() {
if (!super.has_finished()) {
return false;
}
if (this.model.visible) {
for (const [, renderer_view] of this.renderer_views) {
if (!renderer_view.has_finished()) {
return false;
}
}
}
return true;
}
_after_layout() {
super._after_layout();
this.unpause(true);
const left = this.layout.left_panel.bbox;
const right = this.layout.right_panel.bbox;
const center = this.layout.center_panel.bbox;
const top = this.layout.top_panel.bbox;
const bottom = this.layout.bottom_panel.bbox;
const { bbox } = this;
const top_height = top.bottom;
const bottom_height = bbox.height - bottom.top;
const left_width = left.right;
const right_width = bbox.width - right.left;
// TODO: don't replace here; inject stylesheet?
this.canvas.parent_style.replace(`
.bk-layer.bk-events {
display: grid;
grid-template-areas:
". above . "
"left center right"
". below . ";
grid-template-rows: ${px(top_height)} ${px(center.height)} ${px(bottom_height)};
grid-template-columns: ${px(left_width)} ${px(center.width)} ${px(right_width)};
}
`);
for (const [, child_view] of this.renderer_views) {
if (child_view instanceof AnnotationView) {
child_view.after_layout?.();
}
}
this.model.setv({
inner_width: Math.round(this.frame.bbox.width),
inner_height: Math.round(this.frame.bbox.height),
outer_width: Math.round(this.bbox.width),
outer_height: Math.round(this.bbox.height),
}, { no_change: true });
if (this.model.match_aspect) {
this.pause();
this._range_manager.update_dataranges();
this.unpause(true);
}
if (!this._outer_bbox.equals(this.bbox)) {
this.canvas_view.resize(); // XXX temporary hack
this._outer_bbox = this.bbox;
this._invalidate_all = true;
this._needs_paint = true;
}
const { inner_bbox } = this.layout;
if (!this._inner_bbox.equals(inner_bbox)) {
this._inner_bbox = inner_bbox;
this._invalidate_all = true;
this._needs_paint = true;
}
if (this._needs_paint) {
// XXX: can't be this.request_paint(), because it would trigger back-and-forth
// layout recomputing feedback loop between plots. Plots are also much more
// responsive this way, especially in interactive mode.
this.paint();
}
}
render() {
super.render();
for (const renderer_view of this.computed_renderer_views) {
const target = renderer_view.rendering_target();
renderer_view.render_to(target);
}
}
repaint() {
this._invalidate_layout_if_needed();
this.paint();
}
paint() {
if (this.is_paused || this.model.hold_render) {
return;
}
if (this.is_displayed) {
logger.trace(`${this.toString()}.paint()`);
this._actual_paint();
}
else {
// This is possibly the first render cycle, but plot isn't displayed,
// so all renderers have to be manually marked as finished, because
// their `render()` method didn't run.
for (const renderer_view of this.computed_renderer_views) {
renderer_view.force_finished();
}
}
if (this._needs_notify) {
this._needs_notify = false;
this.notify_finished();
}
}
_actual_paint() {
logger.trace(`${this.toString()}._actual_paint ${this._render_count} start`);
const { document } = this.model;
if (document != null) {
const interactive_duration = document.interactive_duration();
if (interactive_duration >= 0 && interactive_duration < this.model.lod_interval) {
setTimeout(() => {
if (document.interactive_duration() > this.model.lod_timeout) {
document.interactive_stop();
}
this.request_repaint(); // TODO: this.schedule_paint()
}, this.model.lod_timeout);
}
else {
document.interactive_stop();
}
}
if (this._range_manager.invalidate_dataranges || this.model.window_axis != "none") {
this._range_manager.update_dataranges();
this._invalidate_layout_if_needed();
}
let do_primary = false;
let do_overlays = false;
if (this._invalidate_all) {
do_primary = true;
do_overlays = true;
}
else {
for (const painter of this._invalidated_painters) {
const { level } = painter.model;
if (level != "overlay") {
do_primary = true;
}
else {
do_overlays = true;
}
if (do_primary && do_overlays) {
break;
}
}
}
this._invalidated_painters.clear();
this._invalidate_all = false;
if (do_primary) {
const { primary } = this.canvas_view;
const ctx = primary.prepare();
this._paint_primary(ctx);
primary.finish();
}
if (do_overlays || settings.wireframe) {
const { overlays } = this.canvas_view;
const ctx = overlays.prepare();
this._paint_overlays(ctx);
overlays.finish();
}
if (this._initial_state.range == null) {
this._initial_state.range = this._range_manager.compute_initial() ?? undefined;
}
for (const element_view of this.element_views) {
element_view.reposition();
}
this._needs_paint = false;
this.repainted.emit();
logger.trace(`${this.toString()}._actual_paint ${this._render_count} end`);
this._render_count++;
}
_paint_primary(ctx) {
const frame_box = this.frame.bbox;
this.canvas_view.prepare_webgl(frame_box);
this._paint_empty(ctx, frame_box);
this._paint_outline(ctx, frame_box);
this._paint_levels(ctx, "image", frame_box, true);
this._paint_levels(ctx, "underlay", frame_box, true);
this._paint_levels(ctx, "glyph", frame_box, true);
this._paint_levels(ctx, "guide", frame_box, false);
this._paint_levels(ctx, "annotation", frame_box, false);
}
_paint_overlays(ctx) {
const frame_box = this.frame.bbox;
this._paint_levels(ctx, "overlay", frame_box, false);
if (settings.wireframe) {
this.paint_layout(ctx, this.layout);
}
}
_paint_levels(ctx, level, clip_box, global_clip) {
for (const renderer_view of this.computed_renderer_views) {
if (renderer_view.model.level != level) {
continue;
}
ctx.save();
if (global_clip || renderer_view.needs_clip) {
ctx.beginPath();
ctx.rect(...clip_box.args);
ctx.clip();
}
renderer_view.paint(ctx);
ctx.restore();
if (renderer_view.has_webgl) {
this.canvas_view.blit_webgl(ctx);
}
}
}
paint_layout(ctx, layout) {
const { x, y, width, height } = layout.bbox;
ctx.strokeStyle = "blue";
ctx.strokeRect(x, y, width, height);
for (const child of layout) {
ctx.save();
if (!layout.absolute) {
ctx.translate(x, y);
}
this.paint_layout(ctx, child);
ctx.restore();
}
}
_paint_empty(ctx, frame_box) {
const canvas_box = this.bbox.relative();
const [cx, cy, cw, ch] = canvas_box.args;
const [fx, fy, fw, fh] = frame_box.args;
if (this.visuals.border_fill.doit || this.visuals.border_hatch.doit) {
ctx.save();
ctx.beginPath();
ctx.rect(cx, cy, cw, ch);
ctx.rect(fx, fy, fw, fh);
ctx.clip("evenodd");
ctx.beginPath();
ctx.rect(cx, cy, cw, ch);
this.visuals.border_fill.apply(ctx);
this.visuals.border_hatch.apply(ctx);
ctx.restore();
}
if (this.visuals.background_fill.doit || this.visuals.background_hatch.doit) {
ctx.beginPath();
ctx.rect(fx, fy, fw, fh);
this.visuals.background_fill.apply(ctx);
this.visuals.background_hatch.apply(ctx);
}
}
_paint_outline(ctx, frame_box) {
if (this.visuals.outline_line.doit) {
ctx.save();
this.visuals.outline_line.set_value(ctx);
let [x0, y0, w, h] = frame_box.args;
// XXX: shrink outline region by 1px to make right and bottom lines visible
// if they are on the edge of the canvas.
if (x0 + w == this.bbox.width) {
w -= 1;
}
if (y0 + h == this.bbox.height) {
h -= 1;
}
ctx.strokeRect(x0, y0, w, h);
ctx.restore();
}
}
_force_paint = false;
get is_forcing_paint() {
return this._force_paint;
}
force_paint(fn) {
try {
this._force_paint = true;
fn();
}
finally {
this._force_paint = false;
}
}
export(type = "auto", hidpi = true) {
const output_backend = (() => {
switch (type) {
case "auto": return this.canvas_view.model.output_backend;
case "png": return "canvas";
case "svg": return "svg";
}
})();
const composite = new CanvasLayer(output_backend, hidpi);
const { width, height } = this.bbox;
composite.resize(width, height);
if (width != 0 && height != 0) {
this.force_paint(() => {
const ctx = composite.prepare();
this._paint_primary(ctx);
this._paint_overlays(ctx);
composite.finish();
});
}
return composite;
}
resolve_frame() {
return this.frame;
}
resolve_canvas() {
return this.canvas;
}
resolve_plot() {
return this;
}
resolve_xy(coord) {
const { x, y } = coord;
const sx = this.frame.x_scale.compute(x);
const sy = this.frame.y_scale.compute(y);
if (this.frame.bbox.contains(sx, sy)) {
return { x: sx, y: sy };
}
else {
return { x: NaN, y: NaN };
}
}
resolve_indexed(coord) {
const { index: i, renderer } = coord;
const rv = this.views.find_one(renderer);
if (rv != null && rv.has_finished()) {
const [sx, sy] = rv.glyph.scenterxy(i, NaN, NaN);
if (this.frame.bbox.contains(sx, sy)) {
return { x: sx, y: sy };
}
}
return { x: NaN, y: NaN };
}
_messages = new Map();
notify_about(message) {
if (this._messages.has(message)) {
return;
}
const el = new Div({ children: [message] });
const timer = setTimeout(() => {
this._messages.delete(message);
this._notifications.elements = this._notifications.elements.filter((item) => item != el);
}, 2000);
this._messages.set(message, timer);
this._notifications.elements = [...this._notifications.elements, el];
logger.info(message);
}
serializable_children() {
// TODO temporarily remove CanvasPanel views to reduce baseline noise
return super.serializable_children().filter((view) => view.model instanceof CartesianFrame || !(view.model instanceof CanvasPanel));
}
}
//# sourceMappingURL=plot_canvas.js.map