UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

570 lines 20 kB
import { Signal0, Signal, Signalable } from "./signaling"; import { may_have_refs } from "./util/refs"; import * as p from "./properties"; import * as k from "./kinds"; import { assert } from "./util/assert"; import { unique_id } from "./util/string"; import { keys, values, entries, extend, is_empty, dict } from "./util/object"; import { isObject, isIterable, isPlainObject, isArray, isFunction, isPrimitive } from "./util/types"; import { serialize } from "./serialization"; import { DocumentEventBatch, ModelChangedEvent, ColumnsPatchedEvent, ColumnsStreamedEvent } from "../document/events"; import { equals, is_equal } from "./util/eq"; import { pretty } from "./util/pretty"; import { clone, Cloner } from "./util/cloneable"; import * as kinds from "./kinds"; import { isExpr } from "./vectorization"; import { stream_to_columns, patch_to_columns } from "./patching"; const _qualified_names = new WeakMap(); export class HasProps extends Signalable() { id; get is_syncable() { return true; } get type() { return this.constructor.__qualified__; } static __name__; static __module__; static get __qualified__() { let qualified = _qualified_names.get(this); if (qualified == null) { const { __module__, __name__ } = this; qualified = __module__ != null ? `${__module__}.${__name__}` : __name__; _qualified_names.set(this, qualified); } return qualified; } static set __qualified__(qualified) { _qualified_names.set(this, qualified); } get [Symbol.toStringTag]() { return this.constructor.__qualified__; } static { this.prototype._props = {}; this.prototype._mixins = []; } static _fix_default(default_value, _attr) { if (default_value === undefined || default_value === p.unset) { return () => p.unset; } else if (isFunction(default_value)) { return default_value; } else if (isPrimitive(default_value)) { return () => default_value; } else { const cloner = new Cloner(); return () => cloner.clone(default_value); } } // TODO: don't use Partial<>, but exclude inherited properties static define(obj) { for (const [name, prop] of entries(isFunction(obj) ? obj(kinds) : obj)) { if (name in this.prototype._props) { throw new Error(`attempted to redefine property '${this.prototype.type}.${name}'`); } if (name in this.prototype) { throw new Error(`attempted to redefine attribute '${this.prototype.type}.${name}'`); } Object.defineProperty(this.prototype, name, { // XXX: don't use tail calls in getters/setters due to https://bugs.webkit.org/show_bug.cgi?id=164306 get() { const value = this.properties[name].get_value(); return value; }, set(value) { this.setv({ [name]: value }); return this; }, configurable: false, enumerable: true, }); const [type, default_value, options = {}] = prop; const refined_prop = { type, default_value: this._fix_default(default_value, name), options, }; this.prototype._props = { ...this.prototype._props, [name]: refined_prop, }; } } static internal(obj) { const _object = {}; for (const [name, prop] of entries(isFunction(obj) ? obj(kinds) : obj)) { const [type, default_value, options = {}] = prop; _object[name] = [type, default_value, { ...options, internal: true }]; } this.define(_object); } static mixins(defs) { function rename(prefix, mixin) { const result = {}; for (const [name, prop] of entries(mixin)) { result[prefix + name] = prop; } return result; } const mixin_defs = {}; const mixins = []; for (const def of isArray(defs) ? defs : [defs]) { if (isArray(def)) { const [prefix, mixin] = def; extend(mixin_defs, rename(prefix, mixin)); mixins.push([prefix, mixin]); } else { const mixin = def; extend(mixin_defs, mixin); mixins.push(["", mixin]); } } this.define(mixin_defs); this.prototype._mixins = [...this.prototype._mixins, ...mixins]; } static override(obj) { for (const [name, prop] of entries(obj)) { const default_value = this._fix_default(prop, name); if (!(name in this.prototype._props)) { throw new Error(`attempted to override nonexistent '${this.prototype.type}.${name}'`); } const value = this.prototype._props[name]; const props = { ...this.prototype._props }; props[name] = { ...value, default_value }; this.prototype._props = props; } } static override_options(obj) { for (const [name, options] of entries(obj)) { if (!(name in this.prototype._props)) { throw new Error(`attempted to override nonexistent '${this.prototype.type}.${name}'`); } const current = this.prototype._props[name]; const props = { ...this.prototype._props, [name]: { ...current, options: { ...current.options, ...options } }, }; this.prototype._props = props; } } static toString() { return this.__qualified__; } toString() { return `${this.type}(${this.id})`; } document = null; destroyed = new Signal0(this, "destroyed"); change = new Signal0(this, "change"); transformchange = new Signal0(this, "transformchange"); exprchange = new Signal0(this, "exprchange"); streaming = new Signal0(this, "streaming"); patching = new Signal(this, "patching"); properties = {}; property(name) { if (name in this.properties) { return this.properties[name]; } else { throw new Error(`unknown property ${this.type}.${name}`); } } /** * Gets values of all set properties. */ get attributes() { const attrs = {}; for (const prop of this) { if (!prop.is_unset) { attrs[prop.attr] = prop.get_value(); } } return attrs; } /** * Gets values of all set and dirty (modified) properties. */ get dirty_attributes() { const attrs = {}; for (const prop of this) { if (!prop.is_unset && prop.dirty) { attrs[prop.attr] = prop.get_value(); } } return attrs; } [clone](cloner) { const attrs = new Map(); for (const prop of this) { if (prop.dirty) { attrs.set(prop.attr, cloner.clone(prop.get_value())); } } return new this.constructor(attrs); } [equals](that, cmp) { for (const p0 of this) { const p1 = that.property(p0.attr); if (!cmp.eq(p0.get_value(), p1.get_value())) { return false; } } return true; } [pretty](printer) { const T = printer.token; const items = []; for (const prop of this) { if (prop.dirty) { const value = prop.get_value(); items.push(`${prop.attr}${T(":")} ${printer.to_string(value)}`); } } const cls = this.constructor.__qualified__; return `${cls}${T("(")}${T("{")}${items.join(`${T(",")} `)}${T("}")}${T(")")}`; } [serialize](serializer) { const ref = this.ref(); serializer.add_ref(this, ref); const attributes = {}; for (const prop of this) { if (prop.syncable && (serializer.include_defaults || prop.dirty) && !(prop.readonly && prop.is_unset)) { const value = prop.get_value(); attributes[prop.attr] = serializer.encode(value); } } const { type: name, id } = this; const rep = { type: "object", name, id }; return is_empty(attributes) ? rep : { ...rep, attributes }; } constructor(attrs = {}) { super(); const deferred = isPlainObject(attrs) && "id" in attrs; this.id = deferred ? attrs.id : unique_id(); for (const [name, { type, default_value, options }] of entries(this._props)) { let property; if (type instanceof p.PropertyAlias) { const property = this.properties[type.attr]; if (typeof property === "undefined") { throw new Error(`can't resolve ${type.attr} before ${name} to create an alias`); } Object.defineProperty(this.properties, name, { get: () => property, configurable: false, enumerable: false, }); } else { if (type instanceof k.Kind) { property = new p.PrimitiveProperty(this, name, type, default_value, options); } else { property = new type(this, name, k.Any, default_value, options); } this.properties[name] = property; } } // allowing us to defer initialization when loading many models // when loading a bunch of models, we want to do initialization as a second pass // because other objects that this one depends on might not be loaded yet if (deferred) { assert(keys(attrs).length == 1, "'id' cannot be used together with property initializers"); } else { this.initialize_props(attrs); this.finalize(); this.connect_signals(); } } initialize_props(vals) { const vals_proxy = dict(vals); const visited = new Set(); for (const prop of this) { const val = vals_proxy.get(prop.attr); prop.initialize(val); visited.add(prop.attr); } for (const [attr, val] of vals_proxy) { if (!visited.has(attr)) { // either throws for unknown properties or updates aliased properties this.property(attr).set_value(val); } } } finalize() { this.initialize(); } initialize() { } assert_initialized() { for (const prop of this) { if (prop.syncable && !prop.readonly) { prop.get_value(); } } } connect_signals() { for (const prop of this) { if (!(prop instanceof p.VectorSpec || prop instanceof p.ScalarSpec)) { continue; } if (prop.is_unset) { continue; } const value = prop.get_value(); if (value.transform != null) { this.connect(value.transform.change, () => this.transformchange.emit()); } if (isExpr(value)) { this.connect(value.expr.change, () => this.exprchange.emit()); } } } disconnect_signals() { Signal.disconnect_receiver(this); } destroy() { this.disconnect_signals(); this.destroyed.emit(); } // Create a new model with exact attribute values to this one, but new identity. clone(attrs) { const cloner = new Cloner(); const that = cloner.clone(this); if (attrs != null) { that.setv(attrs); } return that; } _watchers = new WeakMap(); _clear_watchers() { this._watchers = new WeakMap(); } changed_for(obj) { const changed = this._watchers.get(obj); this._watchers.set(obj, false); return changed ?? true; } _pending = false; _changing = false; // Set a hash of model attributes on the object, firing `"change"`. This is // the core primitive operation of a model, updating the data and notifying // anyone who needs to know about the change in state. The heart of the beast. _setv(changes, options) { // Extract attributes and options. const check_eq = options.check_eq; const changed = new Set(); const changing = this._changing; this._changing = true; for (const [prop, value] of changes) { if (check_eq === false || prop.is_unset || !is_equal(prop.get_value(), value)) { prop.set_value(value); changed.add(prop); } } // Trigger all relevant attribute changes. if (changed.size > 0) { this._clear_watchers(); this._pending = true; } for (const prop of changed) { prop.change.emit(); } // You might be wondering why there's a `while` loop here. Changes can // be recursively nested within `"change"` events. if (!changing) { if (!(options.no_change ?? false)) { while (this._pending) { this._pending = false; this.change.emit(); } } this._pending = false; this._changing = false; } return changed; } setv(changed_attrs, options = {}) { const changes = entries(changed_attrs); if (changes.length == 0) { return; } if (options.silent ?? false) { this._clear_watchers(); for (const [attr, value] of changes) { this.properties[attr].set_value(value); } return; } const changed = new Map(); const previous = new Map(); for (const [attr, value] of changes) { const prop = this.properties[attr]; changed.set(prop, value); previous.set(prop, prop.is_unset ? undefined : prop.get_value()); } const updated = this._setv(changed, options); const { document } = this; if (document != null) { const changed = []; for (const [prop, value] of previous) { if (updated.has(prop)) { changed.push([prop, value, prop.get_value()]); } } for (const [prop, _, new_value] of changed) { if (prop.may_have_refs) { document.partially_update_all_models(new_value); break; } } const sync = options.sync ?? true; this._push_changes(changed, sync); } } ref() { return { id: this.id }; } *[Symbol.iterator]() { yield* values(this.properties); } *syncable_properties() { for (const prop of this) { if (prop.syncable) { yield prop; } } } *own_properties() { const self = Object.getPrototypeOf(this); const base = Object.getPrototypeOf(self); const exclude = new Set(keys(base._props)); for (const prop of this) { if (!exclude.has(prop.attr)) { yield prop; } } } // add all references from 'v' to 'result', if recurse // is true then descend into refs, if false only // descend into non-refs static _value_record_references(value, refs, options) { if (!isObject(value) || !may_have_refs(value)) { return; } const { recursive } = options; if (value instanceof HasProps) { if (!refs.has(value)) { refs.add(value); if (recursive) { for (const prop of value.syncable_properties()) { if (!prop.is_unset && prop.may_have_refs) { const value = prop.get_value(); HasProps._value_record_references(value, refs, { recursive }); } } } } } else if (isIterable(value)) { for (const elem of value) { HasProps._value_record_references(elem, refs, { recursive }); } } else if (isPlainObject(value)) { for (const elem of values(value)) { HasProps._value_record_references(elem, refs, { recursive }); } } } static references(value, options) { const refs = new Set(); HasProps._value_record_references(value, refs, options); return refs; } references() { return HasProps.references(this, { recursive: true }); } _doc_attached() { } _doc_detached() { } attach_document(doc) { // This should only be called by the Document implementation to set the document field if (this.document != null) { if (this.document == doc) { return; } else { throw new Error("models must be owned by only a single document"); } } this.document = doc; this._doc_attached(); } detach_document() { // This should only be called by the Document implementation to unset the document field if (this.document != null) { this._doc_detached(); this.document = null; } } _push_changes(changes, sync) { if (!this.is_syncable) { return; } const { document } = this; if (document == null) { return; } const events = []; for (const [prop, , new_value] of changes) { if (prop.syncable) { const event = new ModelChangedEvent(document, this, prop.attr, new_value); event.sync = sync; events.push(event); } } if (events.length != 0) { let event; if (events.length == 1) { [event] = events; } else { event = new DocumentEventBatch(document, events); } document._trigger_on_change(event); } } on_change(properties, fn) { for (const property of isArray(properties) ? properties : [properties]) { this.connect(property.change, fn); } } stream_to(prop, new_data, rollover, { sync } = {}) { const data = prop.get_value(); stream_to_columns(data, new_data, rollover); this._clear_watchers(); prop.set_value(data); this.streaming.emit(); if (this.document != null) { const event = new ColumnsStreamedEvent(this.document, this, prop.attr, new_data, rollover); event.sync = sync ?? true; this.document._trigger_on_change(event); } } patch_to(prop, patches, { sync } = {}) { const data = prop.get_value(); const patched = patch_to_columns(data, patches); this._clear_watchers(); prop.set_value(data); this.patching.emit([...patched]); if (this.document != null) { const event = new ColumnsPatchedEvent(this.document, this, prop.attr, patches); event.sync = sync ?? true; this.document._trigger_on_change(event); } } } //# sourceMappingURL=has_props.js.map