@quartic/bokehjs
Version:
Interactive, novel data visualization
419 lines (351 loc) • 13.9 kB
text/coffeescript
import * as Backbone from "./backbone"
import {logger} from "./logging"
import * as property_mixins from "./property_mixins"
import * as refs from "./util/refs"
import * as p from "./properties"
import {uniqueId} from "./util/string"
import {max} from "./util/array"
import {extend, values, clone, isEmpty} from "./util/object"
import {isString, isObject, isArray} from "./util/types"
export class HasProps extends Backbone.Model
props: {}
mixins: []
@define: (object) ->
for name, prop of object
do (name, prop) =>
if this.prototype.props[name]?
throw new Error("attempted to redefine property '#{this.name}.#{name}'")
if this.prototype[name]?
throw new Error("attempted to redefine attribute '#{this.name}.#{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: () -> value = this.getv(name); return value
set: (value) -> this.setv(name, value); return this
}, {
configurable: false
enumerable: true
})
[type, default_value, internal] = prop
refined_prop = {
type: type
default_value: default_value
internal: internal ? false
}
props = clone(this.prototype.props)
props[name] = refined_prop
this.prototype.props = props
@internal: (object) ->
_object = {}
for name, prop of object
do (name, prop) =>
[type, default_value] = prop
_object[name] = [type, default_value, true]
@define(_object)
@mixin: (names...) ->
@define(property_mixins.create(names))
mixins = this.prototype.mixins.concat(names)
this.prototype.mixins = mixins
@mixins: (names) -> @mixin(names...)
@override: (name_or_object, default_value) ->
if isString(name_or_object)
object = {}
object[name] = default_value
else
object = name_or_object
for name, default_value of object
do (name, default_value) =>
value = this.prototype.props[name]
if not value?
throw new Error("attempted to override nonexistent '#{this.name}.#{name}'")
props = clone(this.prototype.props)
props[name] = extend({}, value, { default_value: default_value })
this.prototype.props = props
@define {
id: [ p.Any ]
}
toString: () -> "#{@type}(#{@id})"
constructor : (attributes, options) ->
@document = null
## straight from backbone.js
attrs = attributes || {}
if not options
options = {}
this.attributes = {}
@properties = {}
for name, {type, default_value} of @props
if not type?
throw new Error("undefined property type for #{@type}.#{name}")
@properties[name] = new type({obj: @, attr: name, default_value: default_value})
# Bokeh specific
this._set_after_defaults = {}
this.setv(attrs, options)
# this is maintained by backbone ("changes since the last
# setv()") and probably isn't relevant to us
this.changed = {}
## bokeh custom constructor code
# setting up data structures for properties
@_computed = {}
# auto generating ID
if not attrs.id?
this.id = uniqueId(this.type)
# 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 not options.defer_initialization
this.initialize.apply(this, arguments)
initialize: (options) ->
# This is necessary because the initial creation of properties relies on
# model.get which is not usable at that point yet in the constructor. This
# initializer is called when deferred initialization happens for all models
# and insures that the Bokeh properties are initialized from Backbone
# attributes in a consistent way.
#
# TODO (bev) split property creation up into two parts so that only the
# portion of init that can be done happens in HasProps constructor and so
# that subsequent updates do not duplicate that setup work.
for name, prop of @properties
prop.update()
if prop.spec.transform
@listenTo(prop.spec.transform, "change", () -> @trigger('transformchange', this))
setv: (key, value, options) ->
# backbones set function supports 2 call signatures, either a dictionary of
# key value pairs, and then options, or one key, one value, and then options.
# replicating that logic here
if isObject(key) or key == null
attrs = key
options = value
else
attrs = {}
attrs[key] = value
for own key, val of attrs
prop_name = key
if not @props[prop_name]?
throw new Error("property #{@type}.#{prop_name} wasn't declared")
if not (options? and options.defaults)
@_set_after_defaults[key] = true
if not isEmpty(attrs)
old = {}
for key, value of attrs
old[key] = @getv(key)
super(attrs, options)
if not options?.silent?
for key, value of attrs
@_tell_document_about_change(key, old[key], @getv(key), options)
add_dependencies: (prop_name, object, fields) ->
# * prop_name - name of property
# * object - object on which dependencies reside
# * fields - attributes on that object
if not isArray(fields)
fields = [fields]
prop_spec = @_computed[prop_name]
prop_spec.dependencies = prop_spec.dependencies.concat(
obj: object
fields: fields
)
# bind depdencies to change dep callback
for fld in fields
@listenTo(object, "change:" + fld, prop_spec['callbacks']['changedep'])
define_computed_property: (prop_name, getter, use_cache=true) ->
# #### Parameters
# * prop_name: name of property
# * getter: function, calculates computed value, takes no arguments
# * use_cache: whether to cache or not
# #### Returns
# * prop_spec: specification of the property, with the getter,
if @props[prop_name]?
#throw new Error(
console.log("attempted to redefine existing property #{@type}.#{prop_name}")
if @_computed[prop_name]?
throw new Error("attempted to redefine existing computed property #{@type}.#{prop_name}")
changedep = () =>
@trigger('changedep:' + prop_name)
propchange = () =>
firechange = true
if prop_spec['use_cache']
old_val = prop_spec.cache
prop_spec.cache = undefined
new_val = @_get_computed(prop_name)
firechange = new_val != old_val
if firechange
@trigger('change:' + prop_name, this, @_get_computed(prop_name))
@trigger('change', this)
prop_spec =
'getter': getter,
'dependencies': [],
'use_cache': use_cache
'callbacks':
changedep: changedep
propchange: propchange
@_computed[prop_name] = prop_spec
# bind propchange callback to change dep event
@listenTo(this, "changedep:#{prop_name}", prop_spec['callbacks']['propchange'])
return prop_spec
set: (key, value, options) ->
logger.warn("HasProps.set('prop_name', value) is deprecated, use HasProps.prop_name = value instead")
return @setv(key, value, options)
get: (prop_name) ->
logger.warn("HasProps.get('prop_name') is deprecated, use HasProps.prop_name instead")
return @getv(prop_name)
getv: (prop_name) ->
if not @props[prop_name]?
throw new Error("property #{@type}.#{prop_name} wasn't declared")
else
return super(prop_name)
_get_computed: (prop_name) ->
prop_spec = @_computed[prop_name]
if not prop_spec?
throw new Error("computed property #{@type}.#{prop_name} wasn't declared")
if prop_spec.use_cache and prop_spec.cache
return prop_spec.cache
else
getter = prop_spec.getter
computed = getter.apply(this, [prop_name])
if prop_spec.use_cache
prop_spec.cache = computed
return computed
ref: () -> refs.create_ref(@)
# we only keep the subtype so we match Python;
# only Python cares about this
set_subtype: (subtype) ->
@_subtype = subtype
attribute_is_serializable: (attr) ->
prop = @props[attr]
if not prop?
throw new Error("#{@type}.attribute_is_serializable('#{attr}'): #{attr} wasn't declared")
else
return not prop.internal
# dict of attributes that should be serialized to the server. We
# sometimes stick things in attributes that aren't part of the
# Document's models, subtypes that do that have to remove their
# extra attributes here.
serializable_attributes: () ->
attrs = {}
for name, value of @attributes
if @attribute_is_serializable(name)
attrs[name] = value
return attrs
@_value_to_json: (key, value, optional_parent_object) ->
if value instanceof HasProps
value.ref()
else if isArray(value)
ref_array = []
for v, i in value
ref_array.push(HasProps._value_to_json(i, v, value))
ref_array
else if isObject(value)
ref_obj = {}
for own subkey of value
ref_obj[subkey] = HasProps._value_to_json(subkey, value[subkey], value)
ref_obj
else
value
# Convert attributes to "shallow" JSON (values which are themselves models
# are included as just references)
# TODO (havocp) can this just be toJSON (from Backbone / JSON.stingify?)
# backbone will have implemented a toJSON already that we may need to override
# optional value_to_json is for test to override with a "deep" version to replace the
# standard "shallow" HasProps._value_to_json
attributes_as_json: (include_defaults=true, value_to_json=HasProps._value_to_json) ->
attrs = {}
for own key, value of @serializable_attributes()
if include_defaults
attrs[key] = value
else if key of @_set_after_defaults
attrs[key] = value
value_to_json("attributes", attrs, @)
# this is like _value_record_references but expects to find refs
# instead of models, and takes a doc to look up the refs in
@_json_record_references: (doc, v, result, recurse) ->
if not v?
;
else if refs.is_ref(v)
if v.id not of result
model = doc.get_model_by_id(v.id)
HasProps._value_record_references(model, result, recurse)
else if isArray(v)
for elem in v
HasProps._json_record_references(doc, elem, result, recurse)
else if isObject(v)
for own k, elem of v
HasProps._json_record_references(doc, elem, result, recurse)
# add all references from 'v' to 'result', if recurse
# is true then descend into refs, if false only
# descend into non-refs
@_value_record_references: (v, result, recurse) ->
if not v?
;
else if v instanceof HasProps
if v.id not of result
result[v.id] = v
if recurse
immediate = v._immediate_references()
for obj in immediate
HasProps._value_record_references(obj, result, true) # true=recurse
else if v.buffer instanceof ArrayBuffer
else if isArray(v)
for elem in v
HasProps._value_record_references(elem, result, recurse)
else if isObject(v)
for own k, elem of v
HasProps._value_record_references(elem, result, recurse)
# Get models that are immediately referenced by our properties
# (do not recurse, do not include ourselves)
_immediate_references: () ->
result = {}
attrs = @serializable_attributes()
for key of attrs
value = attrs[key]
HasProps._value_record_references(value, result, false) # false = no recurse
values(result)
references: () ->
references = {}
HasProps._value_record_references(this, references, true)
return values(references)
attach_document: (doc) ->
# This should only be called by the Document implementation to set the document field
if @document != null and @document != doc
throw new Error("models must be owned by only a single document")
@document = doc
if @_doc_attached?
@_doc_attached()
detach_document: () ->
# This should only be called by the Document implementation to unset the document field
@document = null
_tell_document_about_change: (attr, old, new_, options) ->
if not @attribute_is_serializable(attr)
return
if @document != null
new_refs = {}
HasProps._value_record_references(new_, new_refs, false)
old_refs = {}
HasProps._value_record_references(old, old_refs, false)
need_invalidate = false
for new_id, new_ref of new_refs
if new_id not of old_refs
need_invalidate = true
break
if not need_invalidate
for old_id, old_ref of old_refs
if old_id not of new_refs
need_invalidate = true
break
if need_invalidate
@document._invalidate_all_models()
@document._notify_change(@, attr, old, new_, options)
materialize_dataspecs: (source) ->
# Note: this should be moved to a function separate from HasProps
data = {}
for name, prop of @properties
if not prop.dataspec
continue
# this skips optional properties like radius for circles
if (prop.optional || false) and prop.spec.value == null and (name not of @_set_after_defaults)
continue
data["_#{name}"] = prop.array(source)
# the shapes are indexed by the column name, but when we materialize the dataspec, we should
# store under the canonical field name, e.g. _image_shape, even if the column name is "foo"
if prop.spec.field? and prop.spec.field of source._shapes
data["_#{name}_shape"] = source._shapes[prop.spec.field]
if prop instanceof p.Distance
data["max_#{name}"] = max(data["_#{name}"])
return data