@quartic/bokehjs
Version:
Interactive, novel data visualization
258 lines (216 loc) • 9.34 kB
text/coffeescript
import {Model} from "../../model"
import {empty} from "core/dom"
import * as p from "core/properties"
import {GE, EQ, Strength, Variable} from "core/layout/solver"
import {build_views} from "core/build_views"
import {BokehView} from "core/bokeh_view"
import {logger} from "core/logging"
import {extend} from "core/util/object"
export class LayoutDOMView extends BokehView
initialize: (options) ->
super(options)
# Provides a hook so document can measure
@el.setAttribute("id", "modelid_#{@model.id}")
@el.classList.add("bk-layout-#{@model.sizing_mode}")
if @model.css_classes?
for cls in @model.css_classes
@el.classList.add(cls)
@child_views = {}
# init_solver = false becuase we only need to init solver on subsequent
# children change. build_child_views calls bind_bokeh_events
@build_child_views(false)
build_child_views: (init_solver=true) ->
@unbind_bokeh_events()
if init_solver
# TODO (bird) Can't we put the call to invalidate_all_models in _init_solver
# surely its document's problem to know how to init a solver. Also _init_solver
# probably shouldn't be a private method if we're using it here.
@model.document._invalidate_all_models()
@model.document._init_solver()
children = @model.get_layoutable_children()
@child_views = {}
build_views(@child_views, children)
empty(@el)
for child in children
# Look-up the child_view in @child_views and then append We can't just
# read from @child_views because then we don't get guaranteed ordering.
# Which is a problem in non-box layouts.
child_view = @child_views[child.id]
@el.appendChild(child_view.el)
@bind_bokeh_events()
unbind_bokeh_events: () ->
@stopListening()
for id, view of @child_views
view.stopListening()
view.unbind_bokeh_events?()
bind_bokeh_events: () ->
@listenTo(@model, 'change', @render)
if @model.sizing_mode == 'fixed'
@listenToOnce(@model.document.solver(), 'resize', () => @render())
else
@listenTo(@model.document.solver(), 'resize', () => @render())
# Note: `sizing_mode` update is not supported because changing the
# sizing_mode mode necessitates stripping out all the relevant constraints
# from solver and re-adding the new correct ones. We don't currently have
# a machinery for this. Other things with a similar problem are axes and
# title.
sizing_mode_msg = "Changing sizing_mode after initialization is not currently supported."
@listenTo(@model, 'change:sizing_mode', () -> logger.warn(sizing_mode_msg))
render: () ->
#logger.debug("#{@model} _dom_left: #{@model._dom_left._value}, _dom_top: #{@model._dom_top._value}")
#logger.debug("#{@model} _top: #{@model._top._value}, _right: #{@model._right._value}, _bottom: #{@model._bottom._value}, _left: #{@model._left._value}")
#logger.debug("#{@model} _width: #{@model._width._value}, _height: #{@model._height._value}")
#logger.debug("#{@model} _width_minus_right: #{@model._width_minus_right._value}, _height_minus_bottom: #{@model._height_minus_bottom._value}")
s = @model.document.solver()
if @model.sizing_mode is 'fixed'
# If the width or height is unset:
# - compute it from children
# - but then save for future use
# (for some reason widget boxes keep shrinking if you keep computing
# but this is more efficient and appropriate for fixed anyway).
if @model.width?
width = @model.width
else
width = @get_width()
@model.width = width
if @model.height?
height = @model.height
else
height = @get_height()
@model.height = height
s.suggest_value(@model._width, width)
s.suggest_value(@model._height, height)
s.update_variables()
@el.style.width = "#{width}px"
@el.style.height = "#{height}px"
if @model.sizing_mode is 'scale_width'
height = @get_height()
s.suggest_value(@model._height, height)
s.update_variables()
@el.style.width = "#{@model._width._value}px"
@el.style.height = "#{@model._height._value}px"
if @model.sizing_mode is 'scale_height'
width = @get_width()
s.suggest_value(@model._width, width)
s.update_variables()
@el.style.width = "#{@model._width._value}px"
@el.style.height = "#{@model._height._value}px"
if @model.sizing_mode is 'stretch_both'
@el.style.position = 'absolute'
@el.style.left = "#{@model._dom_left._value}px"
@el.style.top = "#{@model._dom_top._value}px"
@el.style.width = "#{@model._width._value}px"
@el.style.height = "#{@model._height._value}px"
get_height: () ->
# Subclasses should implement this to explain
# what their height should be in sizing_mode mode.
return null
get_width: () ->
# Subclasses should implement this to explain
# what their width should be in sizing_mode mode.
return null
export class LayoutDOM extends Model
type: "LayoutDOM"
initialize: (attrs, options) ->
super(attrs, options)
@_width = new Variable("_width #{@id}")
@_height = new Variable("_height #{@id}")
# These are the COORDINATES of the four plot sides
@_left = new Variable("_left #{@id}")
@_right = new Variable("_right #{@id}")
@_top = new Variable("_top #{@id}")
@_bottom = new Variable("_bottom #{@id}")
# This is the dom position
@_dom_top = new Variable("_dom_top #{@id}")
@_dom_left = new Variable("_dom_left #{@id}")
# This is the distance from the side of the right and bottom,
@_width_minus_right = new Variable("_width_minus_right #{@id}")
@_height_minus_bottom = new Variable("_height_minus_bottom #{@id}")
# Whitespace variables
@_whitespace_top = new Variable()
@_whitespace_bottom = new Variable()
@_whitespace_left = new Variable()
@_whitespace_right = new Variable()
get_constraints: () ->
constraints = []
# Make sure things dont squeeze out of their bounding box
constraints.push(GE(@_dom_left))
constraints.push(GE(@_dom_top))
# Plot has to be inside the width/height
constraints.push(GE(@_left))
constraints.push(GE(@_width, [-1, @_right]))
constraints.push(GE(@_top))
constraints.push(GE(@_height, [-1, @_bottom]))
## Declare computed constraints
constraints.push(EQ(@_width_minus_right, [-1, @_width], @_right))
constraints.push(EQ(@_height_minus_bottom, [-1, @_height], @_bottom))
return constraints
get_layoutable_children: () ->
[]
get_edit_variables: () ->
edit_variables = []
if @sizing_mode is 'fixed'
edit_variables.push({edit_variable: @_height, strength: Strength.strong})
edit_variables.push({edit_variable: @_width, strength: Strength.strong})
if @sizing_mode is 'scale_width'
edit_variables.push({edit_variable: @_height, strength: Strength.strong})
if @sizing_mode is 'scale_height'
edit_variables.push({edit_variable: @_width, strength: Strength.strong})
return edit_variables
get_constrained_variables: () ->
# THE FOLLOWING ARE OPTIONAL VARS THAT
# YOU COULD ADD INTO SUBCLASSES
#
# # When this widget is on the edge of a box visually,
# # align these variables down that edge. Right/bottom
# # are an inset from the edge.
# 'on-edge-align-top' : @_top
# 'on-edge-align-bottom' : @_height_minus_bottom
# 'on-edge-align-left' : @_left
# 'on-edge-align-right' : @_width_minus_right
# # When this widget is in a box cell with the same "arity
# # path" as a widget in another cell, align these variables
# # between the two box cells. Right/bottom are an inset from
# # the edge.
# 'box-cell-align-top' : @_top
# 'box-cell-align-bottom': @_height_minus_bottom
# 'box-cell-align-left' : @_left
# 'box-cell-align-right' : @_width_minus_right
# # When this widget is in a box, make these the same distance
# # apart in every widget. Right/bottom are inset from the edge.
# 'box-equal-size-top' : @_top
# 'box-equal-size-bottom': @_height_minus_bottom
# 'box-equal-size-left' : @_left
# 'box-equal-size-right' : @_width_minus_right
constrained_variables = {
'origin-x': @_dom_left
'origin-y': @_dom_top
'whitespace-top' : @_whitespace_top
'whitespace-bottom' : @_whitespace_bottom
'whitespace-left' : @_whitespace_left
'whitespace-right' : @_whitespace_right
}
if @sizing_mode is 'stretch_both'
constrained_variables = extend(constrained_variables, {
'width': @_width
'height': @_height
})
if @sizing_mode is 'scale_width'
constrained_variables = extend(constrained_variables, {
'width': @_width
})
if @sizing_mode is 'scale_height'
constrained_variables = extend(constrained_variables, {
'height': @_height
})
return constrained_variables
@define {
height: [ p.Number ]
width: [ p.Number ]
disabled: [ p.Bool, false ]
sizing_mode: [ p.SizingMode, "fixed" ]
css_classes: [ p.Array ]
}
@internal {
layoutable: [ p.Bool, true ]
}