UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

475 lines 19.5 kB
import { DataRenderer, DataRendererView } from "./data_renderer"; import { LineView } from "../glyphs/line"; import { PatchView } from "../glyphs/patch"; import { HAreaStepView } from "../glyphs/harea_step"; import { HAreaView } from "../glyphs/harea"; import { VAreaStepView } from "../glyphs/varea_step"; import { VAreaView } from "../glyphs/varea"; import { Glyph } from "../glyphs/glyph"; import { ColumnarDataSource } from "../sources/columnar_data_source"; import { CDSView } from "../sources/cds_view"; import { Indices } from "../../core/types"; import { filter } from "../../core/util/arrayable"; import { extend, clone } from "../../core/util/object"; import { build_view } from "../../core/build_views"; import { is_equal } from "../../core/util/eq"; import { FactorRange } from "../ranges/factor_range"; import { Decoration } from "../graphics/decoration"; const selection_defaults = { fill: {}, line: {}, }; const decimated_defaults = { fill: { fill_alpha: 0.3, fill_color: "grey" }, line: { line_alpha: 0.3, line_color: "grey" }, }; const nonselection_defaults = { fill: { fill_alpha: 0.2 }, line: {}, }; const muted_defaults = { fill: { fill_alpha: 0.2 }, line: {}, }; export class GlyphRendererView extends DataRendererView { static __name__ = "GlyphRendererView"; cds_view; glyph; selection_glyph; nonselection_glyph; hover_glyph; muted_glyph; decimated_glyph; get glyph_view() { return this.glyph; } children_views() { const _hover_glyph = this.hover_glyph != null ? [this.hover_glyph] : []; return [...super.children_views(), this.cds_view, this.glyph, this.selection_glyph, this.nonselection_glyph, ..._hover_glyph, this.muted_glyph, this.decimated_glyph]; } all_indices; decimated; last_dtrender; get data_source() { return this.model.properties.data_source; } async lazy_initialize() { await super.lazy_initialize(); this.cds_view = await build_view(this.model.view, { parent: this }); const base_glyph = this.model.glyph; this.glyph = await this.build_glyph_view(base_glyph); const has_fill = "fill" in this.glyph.visuals; const has_line = "line" in this.glyph.visuals; const glyph_attrs = { ...base_glyph.attributes }; delete glyph_attrs.id; function mk_glyph(defaults) { const attrs = clone(glyph_attrs); if (has_fill) { extend(attrs, defaults.fill); } if (has_line) { extend(attrs, defaults.line); } return new base_glyph.constructor(attrs); } function glyph_from_mode(defaults, glyph) { if (glyph instanceof Glyph) { return glyph; } else if (glyph == "auto") { return mk_glyph(defaults); } return mk_glyph({ fill: {}, line: {} }); } let { selection_glyph, nonselection_glyph, hover_glyph, muted_glyph } = this.model; selection_glyph = glyph_from_mode(selection_defaults, selection_glyph); this.selection_glyph = await this.build_glyph_view(selection_glyph); nonselection_glyph = glyph_from_mode(nonselection_defaults, nonselection_glyph); this.nonselection_glyph = await this.build_glyph_view(nonselection_glyph); if (hover_glyph != null) { this.hover_glyph = await this.build_glyph_view(hover_glyph); } muted_glyph = glyph_from_mode(muted_defaults, muted_glyph); this.muted_glyph = await this.build_glyph_view(muted_glyph); const decimated_glyph = glyph_from_mode(decimated_defaults, "auto"); this.decimated_glyph = await this.build_glyph_view(decimated_glyph); this.selection_glyph.set_base(this.glyph); this.nonselection_glyph.set_base(this.glyph); this.hover_glyph?.set_base(this.glyph); this.muted_glyph.set_base(this.glyph); this.decimated_glyph.set_base(this.glyph); await this.set_data(); } async build_glyph_view(glyph) { return build_view(glyph, { parent: this }); } remove() { this.cds_view.remove(); this.glyph.remove(); this.selection_glyph.remove(); this.nonselection_glyph.remove(); this.hover_glyph?.remove(); this.muted_glyph.remove(); this.decimated_glyph.remove(); super.remove(); } _previous_inspected; connect_signals() { super.connect_signals(); const render = () => this.request_paint(); const update = () => this.update_data(); this.connect(this.model.change, render); this.connect(this.glyph.model.change, update); this.connect(this.selection_glyph.model.change, update); this.connect(this.nonselection_glyph.model.change, update); if (this.hover_glyph != null) { this.connect(this.hover_glyph.model.change, update); } this.connect(this.muted_glyph.model.change, update); this.connect(this.decimated_glyph.model.change, update); this.connect(this.model.data_source.change, update); this.connect(this.model.data_source.streaming, update); this.connect(this.model.data_source.patching, (indices) => this.update_data(indices)); this.connect(this.model.data_source.selected.change, render); this.connect(this.model.data_source._select, render); if (this.hover_glyph != null) { this.connect(this.model.data_source.inspect, () => { // XXX: hoping for the best, assuming no in-place mutation const { inspected } = this.model.data_source; const current_inspected = { indices: inspected.indices, line_indices: inspected.line_indices, multiline_indices: inspected.multiline_indices, image_indices: inspected.image_indices, selected_glyphs: inspected.selected_glyphs, }; if (!is_equal(this._previous_inspected, current_inspected)) { this._previous_inspected = current_inspected; render(); } }); } this.connect(this.model.properties.view.change, async () => { this.cds_view.remove(); this.cds_view = await build_view(this.model.view, { parent: this }); await update(); }); this.connect(this.model.view.properties.indices.change, update); this.connect(this.model.view.properties.masked.change, async () => await this.set_visuals()); this.connect(this.model.properties.visible.change, () => this.plot_view.invalidate_dataranges = true); const { x_ranges, y_ranges } = this.plot_view.frame; for (const [, range] of x_ranges) { if (range instanceof FactorRange) { this.connect(range.invalidate_synthetic, update); } } for (const [, range] of y_ranges) { if (range instanceof FactorRange) { this.connect(range.invalidate_synthetic, update); } } const { transformchange, exprchange } = this.model.glyph; this.connect(transformchange, update); this.connect(exprchange, update); } _update_masked_indices() { const masked = this.glyph.mask_data(); this.model.view.masked = masked; return masked; } async update_data(indices) { await this.set_data(indices); this.request_paint(); } // in case of partial updates like patching, the list of indices that actually // changed may be passed as the "indices" parameter to afford any optional optimizations async set_data(indices) { const source = this.model.data_source; this.all_indices = this.model.view.indices; const { all_indices } = this; await this.glyph.set_data(source, all_indices, indices); await this.decimated_glyph.set_data(source, all_indices, indices); await this.selection_glyph.set_data(source, all_indices, indices); await this.nonselection_glyph.set_data(source, all_indices, indices); await this.hover_glyph?.set_data(source, all_indices, indices); await this.muted_glyph.set_data(source, all_indices, indices); await this.set_visuals(); this._update_masked_indices(); const { lod_factor } = this.plot_model; const n = this.all_indices.count; this.decimated = new Indices(n); for (let i = 0; i < n; i += lod_factor) { this.decimated.set(i); } this.plot_view.invalidate_dataranges = true; } async set_visuals() { const source = this.model.data_source; const { all_indices } = this; this.glyph.set_visuals(source, all_indices); this.glyph.after_visuals(); await this.glyph.after_lazy_visuals(); this.decimated_glyph.set_visuals(source, all_indices); this.decimated_glyph.after_visuals(); await this.decimated_glyph.after_lazy_visuals(); this.selection_glyph.set_visuals(source, all_indices); this.selection_glyph.after_visuals(); await this.selection_glyph.after_lazy_visuals(); this.nonselection_glyph.set_visuals(source, all_indices); this.nonselection_glyph.after_visuals(); await this.nonselection_glyph.after_lazy_visuals(); this.hover_glyph?.set_visuals(source, all_indices); this.hover_glyph?.after_visuals(); await this.hover_glyph?.after_lazy_visuals(); this.muted_glyph.set_visuals(source, all_indices); this.muted_glyph.after_visuals(); await this.muted_glyph.after_lazy_visuals(); } map_data() { this.glyph.map_data(); this.decimated_glyph.map_data(); this.selection_glyph.map_data(); this.nonselection_glyph.map_data(); this.hover_glyph?.map_data(); this.muted_glyph.map_data(); } get bbox() { return this.glyph.bbox; } get has_webgl() { return this.glyph.has_webgl(); } _paint(ctx) { const { has_webgl } = this; this.map_data(); // all_indices is in full data space, indices is converted to subset space by mask_data (that may use the spatial index) const all_indices = this.all_indices.ones(); let indices = this._update_masked_indices().ones(); // selected is in full set space const { selected } = this.model.data_source; const selected_full_indices = (() => { if (selected.is_empty()) { return []; } else { if (this.glyph instanceof LineView && selected.selected_glyph === this.glyph.model) { return this.model.view.convert_indices_from_subset(indices); } else { return selected.indices; } } })(); // inspected is in full set space const { inspected } = this.model.data_source; this._previous_inspected = { indices: inspected.indices, line_indices: inspected.line_indices, multiline_indices: inspected.multiline_indices, image_indices: inspected.image_indices, selected_glyphs: inspected.selected_glyphs, }; const inspected_full_indices = new Set((() => { if (inspected.is_empty()) { return []; } else { if (inspected.selected_glyph != null) { return this.model.view.convert_indices_from_subset(indices); } else if (inspected.indices.length > 0) { return inspected.indices; } else { // TODO: return inspected.multiline_indices.keys() return Object.keys(inspected.multiline_indices).map((i) => parseInt(i)); } } })()); // inspected is transformed to subset space const inspected_subset_indices = (() => { if (inspected_full_indices.size === 0) { return []; } return filter(indices, (i) => inspected_full_indices.has(all_indices[i])); })(); const { lod_threshold } = this.plot_model; let glyph; let nonselection_glyph; let selection_glyph; if ((this.model.document != null ? this.model.document.interactive_duration() > 0 : false) && !has_webgl && lod_threshold != null && all_indices.length > lod_threshold) { // Render decimated during interaction if too many elements and not using GL indices = [...this.decimated]; glyph = this.decimated_glyph; nonselection_glyph = this.decimated_glyph; selection_glyph = this.selection_glyph; } else { glyph = this.model.muted ? this.muted_glyph : this.glyph; nonselection_glyph = this.nonselection_glyph; selection_glyph = this.selection_glyph; } if (this.hover_glyph != null && inspected_subset_indices.length != 0) { // TODO: keep working on Indices instead of converting back and forth const set = new Set(indices); for (const i of inspected_subset_indices) { set.delete(i); } indices = [...set]; } ctx.save(); // Render with no selection if (selected_full_indices.length == 0) { if (this.glyph instanceof LineView) { if (this.hover_glyph != null && inspected_subset_indices.length != 0) { this.hover_glyph.paint(ctx, this.model.view.convert_indices_from_subset(inspected_subset_indices)); } else { glyph.paint(ctx, all_indices); } } else if (this.glyph instanceof PatchView || this.glyph instanceof HAreaView || this.glyph instanceof VAreaView || this.glyph instanceof VAreaStepView || this.glyph instanceof HAreaStepView) { if (inspected.selected_glyphs.length == 0 || this.hover_glyph == null) { glyph.paint(ctx, all_indices); } else { for (const sglyph of inspected.selected_glyphs) { if (sglyph == this.glyph.model) { this.hover_glyph.paint(ctx, all_indices); } } } } else { glyph.paint(ctx, indices); if (this.hover_glyph != null && inspected_subset_indices.length != 0) { this.hover_glyph.paint(ctx, inspected_subset_indices); } } // Render with selection } else { // reset the selection mask const selected_mask = new Set(selected_full_indices); // intersect/different selection with render mask const selected_subset_indices = new Array(); const nonselected_subset_indices = new Array(); // now, selected is changed to subset space, except for Line glyph if (this.glyph instanceof LineView) { for (const i of all_indices) { if (selected_mask.has(i)) { selected_subset_indices.push(i); } else { nonselected_subset_indices.push(i); } } } else { for (const i of indices) { if (selected_mask.has(all_indices[i])) { selected_subset_indices.push(i); } else { nonselected_subset_indices.push(i); } } } nonselection_glyph.paint(ctx, nonselected_subset_indices); selection_glyph.paint(ctx, selected_subset_indices); if (this.hover_glyph != null) { if (this.glyph instanceof LineView) { this.hover_glyph.paint(ctx, this.model.view.convert_indices_from_subset(inspected_subset_indices)); } else { this.hover_glyph.paint(ctx, inspected_subset_indices); } } } ctx.restore(); } get_reference_point(field, value) { return this._get_reference_point(field, value) ?? 0; // fall back to first index } _get_reference_point(field, value) { if (field != null) { const array = this.model.data_source.get_column(field); if (array != null) { return this.model.view.get_reference_point(array, value); } } return null; } draw_legend(ctx, x0, x1, y0, y1, field, label, index) { if (this.glyph.data_size == 0) { return; } const subset_index = (() => { if (index == null) { return this.get_reference_point(field, label); } else { return this.model.view.get_subset_index(index); } })(); if (subset_index != null) { this.glyph.draw_legend_for_index(ctx, { x0, x1, y0, y1 }, subset_index); } } hit_test(geometry) { if (!this.model.visible) { return null; } const hit_test_result = this.glyph.hit_test(geometry); // glyphs that don't have hit-testing implemented will return null if (hit_test_result == null) { return null; } return this.model.view.convert_selection_from_subset(hit_test_result); } } export class GlyphRenderer extends DataRenderer { static __name__ = "GlyphRenderer"; constructor(attrs) { super(attrs); } static { this.prototype.default_view = GlyphRendererView; this.define(({ Bool, Auto, Or, Ref, Null, Nullable }) => ({ data_source: [Ref(ColumnarDataSource)], view: [Ref(CDSView), () => new CDSView()], glyph: [Ref(Glyph)], hover_glyph: [Nullable(Ref(Glyph)), null], nonselection_glyph: [Or(Ref(Glyph), Auto, Null), "auto"], selection_glyph: [Or(Ref(Glyph), Auto, Null), "auto"], muted_glyph: [Or(Ref(Glyph), Auto, Null), "auto"], muted: [Bool, false], })); } get_selection_manager() { return this.data_source.selection_manager; } add_decoration(marking, node) { const decoration = new Decoration({ marking, node }); const glyphs = [ this.glyph, this.selection_glyph, this.nonselection_glyph, this.hover_glyph, this.muted_glyph, ]; for (const glyph of glyphs) { if (glyph instanceof Glyph) { glyph.decorations = [...glyph.decorations, decoration]; } } return decoration; } } //# sourceMappingURL=glyph_renderer.js.map