UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

549 lines 19.1 kB
import { default_resolver } from "../base"; import { version as js_version } from "../version"; import { logger } from "../core/logging"; import { HasProps } from "../core/has_props"; import { ModelResolver } from "../core/resolvers"; import { Serializer } from "../core/serialization"; import { Deserializer } from "../core/serialization/deserializer"; import { Version } from "../core/util/version"; import { Signal0 } from "../core/signaling"; import { isString } from "../core/util/types"; import { equals, is_equal } from "../core/util/eq"; import { copy } from "../core/util/array"; import { entries, dict } from "../core/util/object"; import * as sets from "../core/util/set"; import { execute } from "../core/util/callbacks"; import { assert } from "../core/util/assert"; import { Model } from "../model"; import { decode_def } from "./defs"; import { ModelEvent } from "../core/bokeh_events"; import { DocumentReady, LODStart, LODEnd } from "../core/bokeh_events"; import { DocumentEventBatch, RootRemovedEvent, TitleChangedEvent, MessageSentEvent, RootAddedEvent } from "./events"; Deserializer.register("model", decode_def); // Dispatches events to the subscribed models export class EventManager { document; static __name__ = "EventManager"; subscribed_models = new Set(); constructor(document) { this.document = document; } send_event(bokeh_event) { if (bokeh_event.publish) { const event = new MessageSentEvent(this.document, "bokeh_event", bokeh_event); this.document._trigger_on_change(event); } this.document._trigger_on_event(bokeh_event); } trigger(event) { for (const model of this.subscribed_models) { if (event.origin != null && event.origin != model) { continue; } model._process_event(event); } } } export const documents = []; export const DEFAULT_TITLE = "Bokeh Application"; // This class should match the API of the Python Document class // as much as possible. export class Document { static __name__ = "Document"; /** @experimental */ views_manager; event_manager; idle; _init_timestamp; _resolver; _title; _roots; _all_models; _new_models; _all_models_freeze_count; _callbacks; _document_callbacks; _message_callbacks; _idle_roots; _interactive_timestamp; _interactive_plot; _interactive_finalize; _recompute_timeout; constructor(options = {}) { documents.push(this); this._init_timestamp = Date.now(); this._resolver = options.resolver ?? new ModelResolver(default_resolver); this._title = DEFAULT_TITLE; this._roots = []; this._all_models = new Map(); this._new_models = new Set(); this._all_models_freeze_count = 0; this._callbacks = new Map(); this._document_callbacks = new Map(); this._message_callbacks = new Map(); this.event_manager = new EventManager(this); this.idle = new Signal0(this, "idle"); this._idle_roots = new WeakSet(); this._interactive_timestamp = null; this._interactive_plot = null; this._recompute_timeout = options.recompute_timeout ?? 30_000; /* 30s */ if (options.roots != null) { this._add_roots(...options.roots); } this.on_message("bokeh_event", (event) => { assert(event instanceof ModelEvent); this.event_manager.trigger(event); }); } [equals](that, _cmp) { return this == that; } get all_models() { return new Set(this._all_models.values()); } get is_idle() { // TODO: models without views, e.g. data models for (const root of this._roots) { if (!this._idle_roots.has(root)) { return false; } } return true; } notify_idle(model) { this._idle_roots.add(model); if (this.is_idle) { logger.info(`document idle at ${Date.now() - this._init_timestamp} ms`); this.event_manager.send_event(new DocumentReady()); this.idle.emit(); } } clear() { this._push_all_models_freeze(); try { while (this._roots.length > 0) { this.remove_root(this._roots[0]); } } finally { this._pop_all_models_freeze(); } } interactive_start(plot, finalize = null) { if (this._interactive_plot == null) { this._interactive_plot = plot; this._interactive_plot.trigger_event(new LODStart()); } this._interactive_finalize = finalize; this._interactive_timestamp = Date.now(); } interactive_stop() { if (this._interactive_plot != null) { this._interactive_plot.trigger_event(new LODEnd()); if (this._interactive_finalize != null) { this._interactive_finalize(); } } this._interactive_plot = null; this._interactive_timestamp = null; this._interactive_finalize = null; } interactive_duration() { if (this._interactive_timestamp == null) { return -1; } else { return Date.now() - this._interactive_timestamp; } } destructively_move(dest_doc) { if (dest_doc === this) { throw new Error("Attempted to overwrite a document with itself"); } dest_doc.clear(); // we have to remove ALL roots before adding any // to the new doc or else models referenced from multiple // roots could be in both docs at once, which isn't allowed. const roots = copy(this._roots); this.clear(); for (const root of roots) { if (root.document != null) { throw new Error(`Somehow we didn't detach ${root}`); } } if (this._all_models.size != 0) { throw new Error(`this._all_models still had stuff in it: ${this._all_models}`); } for (const root of roots) { dest_doc.add_root(root); } dest_doc.set_title(this._title); } _hold_models_freeze = false; _push_all_models_freeze() { if (this._hold_models_freeze) { return; } this._all_models_freeze_count += 1; } _pop_all_models_freeze() { if (this._hold_models_freeze) { return; } this._all_models_freeze_count -= 1; if (this._all_models_freeze_count === 0) { this._recompute_all_models(); } } _recompute_timer = null; _cancel_recompute_all_models() { if (this._recompute_timer != null) { clearTimeout(this._recompute_timer); this._recompute_timer = null; } } _schedule_recompute_all_models() { const timeout = this._recompute_timeout; if (isNaN(timeout) || timeout <= 0) { this._recompute_all_models(); } else if (isFinite(timeout)) { this._cancel_recompute_all_models(); this._recompute_timer = setTimeout(() => { this._recompute_all_models(); }, timeout); } } _recompute_all_models() { this._cancel_recompute_all_models(); let new_all_models_set = new Set(); for (const r of this._roots) { new_all_models_set = sets.union(new_all_models_set, r.references()); } const old_all_models_set = new Set(this._all_models.values()); const to_detach = sets.difference(old_all_models_set, new_all_models_set); const to_attach = sets.difference(new_all_models_set, old_all_models_set); const recomputed = new Map(); for (const model of new_all_models_set) { recomputed.set(model.id, model); } for (const d of to_detach) { d.detach_document(); } for (const model of to_attach) { model.attach_document(this); this._new_models.add(model); } this._all_models = recomputed; } partially_update_all_models(value) { const refs = new Set(); HasProps._value_record_references(value, refs, { recursive: false }); for (const ref of refs) { if (!this._all_models.has(ref.id)) { ref.attach_document(this); this._new_models.add(ref); this._all_models.set(ref.id, ref); } } this._schedule_recompute_all_models(); } roots() { return this._roots; } _add_roots(...models) { models = models.filter((model) => !this._roots.includes(model)); if (models.length == 0) { return false; } this._push_all_models_freeze(); try { this._roots.push(...models); } finally { this._pop_all_models_freeze(); } return true; } _remove_root(model) { const i = this._roots.indexOf(model); if (i < 0) { return false; } this._push_all_models_freeze(); try { this._roots.splice(i, 1); } finally { this._pop_all_models_freeze(); } return true; } _set_title(title) { const new_title = title != this._title; if (new_title) { this._title = title; } return new_title; } add_root(model, { sync } = {}) { if (this._add_roots(model)) { const event = new RootAddedEvent(this, model); event.sync = sync ?? true; this._trigger_on_change(event); } } remove_root(model, { sync } = {}) { if (this._remove_root(model)) { const event = new RootRemovedEvent(this, model); event.sync = sync ?? true; this._trigger_on_change(event); } } set_title(title, { sync } = {}) { if (this._set_title(title)) { const event = new TitleChangedEvent(this, title); event.sync = sync ?? true; this._trigger_on_change(event); } } title() { return this._title; } get_model_by_id(model_id) { return this._all_models.get(model_id) ?? null; } get_model_by_name(name) { const found = []; for (const model of this._all_models.values()) { if (model instanceof Model && model.name == name) { found.push(model); } } switch (found.length) { case 0: return null; case 1: return found[0]; default: throw new Error(`Multiple models are named '${name}'`); } } on_message(msg_type, callback) { const message_callbacks = this._message_callbacks.get(msg_type); if (message_callbacks == null) { this._message_callbacks.set(msg_type, new Set([callback])); } else { message_callbacks.add(callback); } } remove_on_message(msg_type, callback) { this._message_callbacks.get(msg_type)?.delete(callback); } _trigger_on_message(msg_type, msg_data) { const message_callbacks = this._message_callbacks.get(msg_type); if (message_callbacks != null) { for (const cb of message_callbacks) { cb(msg_data); } } } on_change(callback, allow_batches = false) { if (!this._callbacks.has(callback)) { this._callbacks.set(callback, allow_batches); } } remove_on_change(callback) { this._callbacks.delete(callback); } _trigger_on_change(event) { for (const [callback, allow_batches] of this._callbacks) { if (!allow_batches && event instanceof DocumentEventBatch) { for (const ev of event.events) { callback(ev); } } else { callback(event); // TODO } } } _trigger_on_event(event) { const callbacks = this._document_callbacks.get(event.event_name); if (callbacks != null) { for (const callback of callbacks) { void execute(callback, this, event); } } } on_event(event, ...callbacks) { const name = isString(event) ? event : event.prototype.event_name; const existing = this._document_callbacks.get(name) ?? []; const added = callbacks; this._document_callbacks.set(name, [...existing, ...added]); } to_json_string(include_defaults = true) { return JSON.stringify(this.to_json(include_defaults)); } to_json(include_defaults = true) { const serializer = new Serializer({ include_defaults }); const roots = serializer.encode(this._roots); return { version: js_version, title: this._title, roots, }; } static from_json_string(s, events) { const json = JSON.parse(s); return Document.from_json(json, events); } static _handle_version(json) { if (json.version == null) { logger.warn("'version' field is missing"); } const py_version = json.version ?? "0.0.0"; const py_ver = Version.from(py_version); const js_ver = Version.from(js_version); const message = `new document using Bokeh ${py_version} and BokehJS ${js_version}`; if (is_equal(py_ver, js_ver)) { logger.debug(message); } else { logger.warn(`Bokeh/BokehJS version mismatch: ${message}`); } } static from_json(doc_json, events) { logger.debug("Creating Document from JSON"); Document._handle_version(doc_json); const resolver = new ModelResolver(default_resolver); if (doc_json.defs != null) { const deserializer = new Deserializer(resolver); deserializer.decode(doc_json.defs); } const doc = new Document({ resolver }); doc._push_all_models_freeze(); const listener = (event) => events?.push(event); doc.on_change(listener, true); const deserializer = new Deserializer(resolver, doc._all_models, (obj) => obj.attach_document(doc)); const roots = deserializer.decode(doc_json.roots); const callbacks = (() => { if (doc_json.callbacks != null) { return deserializer.decode(doc_json.callbacks); } else { return {}; } })(); doc.remove_on_change(listener); for (const [event, event_callbacks] of entries(callbacks)) { doc.on_event(event, ...event_callbacks); } for (const root of roots) { doc.add_root(root); } if (doc_json.title != null) { doc.set_title(doc_json.title); } doc._pop_all_models_freeze(); return doc; } replace_with_json(json) { const replacement = Document.from_json(json); replacement.destructively_move(this); } create_json_patch(events) { for (const event of events) { if (event.document != this) { throw new Error("Cannot create a patch using events from a different document"); } } const references = new Map(); for (const model of this._all_models.values()) { if (!this._new_models.has(model)) { references.set(model, model.ref()); } } const serializer = new Serializer({ references, binary: true }); const patch = { events: serializer.encode(events) }; this._new_models.clear(); return patch; } apply_json_patch(patch, buffers = new Map()) { const { _hold_models_freeze } = this; this._hold_models_freeze = true; try { this._apply_json_patch(patch, buffers); } finally { this._hold_models_freeze = _hold_models_freeze; } this._schedule_recompute_all_models(); } _apply_json_patch(patch, buffers = new Map()) { const finalize = (obj) => { obj.attach_document(this); this._new_models.add(obj); this._all_models.set(obj.id, obj); }; const deserializer = new Deserializer(this._resolver, this._all_models, finalize); const events = deserializer.decode(patch.events, buffers); for (const event of events) { switch (event.kind) { case "MessageSent": { const { msg_type, msg_data } = event; this._trigger_on_message(msg_type, msg_data); break; } case "ModelChanged": { const { model, attr, new: value } = event; model.setv({ [attr]: value }, { sync: false }); break; } case "ColumnDataChanged": { const { model, attr, data, cols } = event; if (cols != null) { const new_data = dict(data); const current_data = dict(model.property(attr).get_value()); for (const [name, column] of current_data) { if (!new_data.has(name)) { new_data.set(name, column); } } } model.setv({ data }, { sync: false, check_eq: false }); break; } case "ColumnsStreamed": { const { model, attr, data, rollover } = event; const prop = model.property(attr); model.stream_to(prop, data, rollover, { sync: false }); break; } case "ColumnsPatched": { const { model, attr, patches } = event; const prop = model.property(attr); model.patch_to(prop, patches, { sync: false }); break; } case "RootAdded": { this.add_root(event.model, { sync: false }); break; } case "RootRemoved": { this.remove_root(event.model, { sync: false }); break; } case "TitleChanged": { this.set_title(event.title, { sync: false }); break; } default: { throw new Error(`unknown patch event type '${event.kind}'`); // XXX } } } } } //# sourceMappingURL=document.js.map