@quartic/bokehjs
Version:
Interactive, novel data visualization
480 lines (396 loc) • 18.5 kB
text/coffeescript
import * as sprintf from "sprintf"
import {Document} from "../document"
import * as embed from "../embed"
import {BOKEH_ROOT} from "../embed"
import * as models from "./models"
import {div} from "../core/dom"
import {startsWith} from "../core/util/string"
import {isEqual} from "../core/util/eq"
import {any, all} from "../core/util/array"
import {extend, clone} from "../core/util/object"
import {isNumber, isString, isArray} from "../core/util/types"
_default_tooltips = [
["index", "$index"],
["data (x, y)", "($x, $y)"],
["canvas (x, y)", "($sx, $sy)"],
]
_default_tools = "pan,wheel_zoom,box_zoom,save,reset,help"
_known_tools = {
pan: (plot) -> new models.PanTool(plot: plot, dimensions: 'both')
xpan: (plot) -> new models.PanTool(plot: plot, dimensions: 'width')
ypan: (plot) -> new models.PanTool(plot: plot, dimensions: 'height')
wheel_zoom: (plot) -> new models.WheelZoomTool(plot: plot, dimensions: 'both')
xwheel_zoom: (plot) -> new models.WheelZoomTool(plot: plot, dimensions: 'width')
ywheel_zoom: (plot) -> new models.WheelZoomTool(plot: plot, dimensions: 'height')
zoom_in: (plot) -> new models.ZoomInTool(plot: plot, dimensions: 'both')
xzoom_in: (plot) -> new models.ZoomInTool(plot: plot, dimensions: 'width')
yzoom_in: (plot) -> new models.ZoomInTool(plot: plot, dimensions: 'height')
zoom_out: (plot) -> new models.ZoomOutTool(plot: plot, dimensions: 'both')
xzoom_out: (plot) -> new models.ZoomOutTool(plot: plot, dimensions: 'width')
yzoom_out: (plot) -> new models.ZoomOutTool(plot: plot, dimensions: 'height')
resize: (plot) -> new models.ResizeTool(plot: plot)
click: (plot) -> new models.TapTool(plot: plot, behavior: "inspect")
tap: (plot) -> new models.TapTool(plot: plot)
crosshair: (plot) -> new models.CrosshairTool(plot: plot)
box_select: (plot) -> new models.BoxSelectTool(plot: plot)
xbox_select: (plot) -> new models.BoxSelectTool(plot: plot, dimensions: 'width')
ybox_select: (plot) -> new models.BoxSelectTool(plot: plot, dimensions: 'height')
poly_select: (plot) -> new models.PolySelectTool(plot: plot)
lasso_select: (plot) -> new models.LassoSelectTool(plot: plot)
box_zoom: (plot) -> new models.BoxZoomTool(plot: plot, dimensions: 'both')
xbox_zoom: (plot) -> new models.BoxZoomTool(plot: plot, dimensions: 'width')
ybox_zoom: (plot) -> new models.BoxZoomTool(plot: plot, dimensions: 'height')
hover: (plot) -> new models.HoverTool(plot: plot, tooltips: _default_tooltips)
save: (plot) -> new models.SaveTool(plot: plot)
previewsave: (plot) -> new models.SaveTool(plot: plot)
undo: (plot) -> new models.UndoTool(plot: plot)
redo: (plot) -> new models.RedoTool(plot: plot)
reset: (plot) -> new models.ResetTool(plot: plot)
help: (plot) -> new models.HelpTool(plot: plot)
}
_with_default = (value, default_value) ->
if value == undefined then default_value else value
export class Figure extends models.Plot
constructor: (attributes={}, options={}) ->
attrs = clone(attributes)
tools = _with_default(attrs.tools, _default_tools)
delete attrs.tools
attrs.x_range =
attrs.y_range =
x_axis_type = if attrs.x_axis_type == undefined then "auto" else attrs.x_axis_type
y_axis_type = if attrs.y_axis_type == undefined then "auto" else attrs.y_axis_type
delete attrs.x_axis_type
delete attrs.y_axis_type
x_minor_ticks = attrs.x_minor_ticks ? "auto"
y_minor_ticks = attrs.y_minor_ticks ? "auto"
delete attrs.x_minor_ticks
delete attrs.y_minor_ticks
x_axis_location = attrs.x_axis_location ? "below"
y_axis_location = attrs.y_axis_location ? "left"
delete attrs.x_axis_location
delete attrs.y_axis_location
x_axis_label = attrs.x_axis_label ? ""
y_axis_label = attrs.y_axis_label ? ""
delete attrs.x_axis_label
delete attrs.y_axis_label
if attrs.width != undefined
if attrs.plot_width == undefined
attrs.plot_width = attrs.width
else
throw new Error("both 'width' and 'plot_width' can't be given at the same time")
delete attrs.width
if attrs.height != undefined
if attrs.plot_height == undefined
attrs.plot_height = attrs.height
else
throw new Error("both 'height' and 'plot_height' can't be given at the same time")
delete attrs.height
super(attrs, options)
= new models.Legend({plot: this, items: []})
Object.defineProperty this.prototype, "xgrid", {
get: () -> .filter((r) -> r instanceof models.Grid and r.dimension == 0)[0] # TODO
}
Object.defineProperty this.prototype, "ygrid", {
get: () -> .filter((r) -> r instanceof models.Grid and r.dimension == 1)[0] # TODO
}
Object.defineProperty this.prototype, "xaxis", {
get: () -> .concat().filter((r) -> r instanceof models.Axis)[0] # TODO
}
Object.defineProperty this.prototype, "yaxis", {
get: () -> .concat().filter((r) -> r instanceof models.Axis)[0] # TODO
}
annular_wedge: (args...) ->
annulus: (args...) ->
arc: (args...) ->
bezier: (args...) ->
circle: (args...) ->
ellipse: (args...) ->
image: (args...) ->
image_rgba: (args...) ->
image_url: (args...) ->
line: (args...) ->
multi_line: (args...) ->
oval: (args...) ->
patch: (args...) ->
patches: (args...) ->
quad: (args...) ->
quadratic: (args...) ->
ray: (args...) ->
rect: (args...) ->
segment: (args...) ->
text: (args...) ->
wedge: (args...) ->
asterisk: (args...) ->
circle_cross: (args...) ->
circle_x: (args...) ->
cross: (args...) ->
diamond: (args...) ->
diamond_cross: (args...) ->
inverted_triangle: (args...) ->
square: (args...) ->
square_cross: (args...) ->
square_x: (args...) ->
triangle: (args...) ->
x: (args...) ->
_vectorable: [
"fill_color", "fill_alpha",
"line_color", "line_alpha", "line_width",
"text_color", "text_alpha", "text_font_size",
]
_default_color: "#1f77b4"
_default_alpha: 1.0
_pop_colors_and_alpha: (cls, attrs, prefix="", default_color=, default_alpha=) ->
result = {}
color = _with_default(attrs[prefix + "color"], default_color)
alpha = _with_default(attrs[prefix + "alpha"], default_alpha)
delete attrs[prefix + "color"]
delete attrs[prefix + "alpha"]
_update_with = (name, default_value) ->
if cls.prototype.props[name]?
result[name] = _with_default(attrs[prefix + name], default_value)
delete attrs[prefix + name]
_update_with("fill_color", color)
_update_with("line_color", color)
_update_with("text_color", "black")
_update_with("fill_alpha", alpha)
_update_with("line_alpha", alpha)
_update_with("text_alpha", alpha)
return result
_find_uniq_name: (data, name) ->
i = 1
while true
new_name = "#{name}__#{i}"
if data[new_name]?
i += 1
else
return new_name
_fixup_values: (cls, data, attrs) ->
for name, value of attrs
do (name, value) =>
prop = cls.prototype.props[name]
if prop?
if prop.type.prototype.dataspec
if value?
if isArray(value)
if data[name]?
if data[name] != value
field =
data[field] = value
else
field = name
else
field = name
data[field] = value
attrs[name] = { field: field }
else if isNumber(value) or isString(value) # or Date?
attrs[name] = { value: value }
_glyph: (cls, params, args) ->
params = params.split(",")
if args.length == 1
[attrs] = args
attrs = clone(attrs)
else
[args..., opts] = args
attrs = clone(opts)
for param, i in params
do (param, i) ->
attrs[param] = args[i]
legend =
delete attrs.legend
has_sglyph = any(Object.keys(attrs), (key) -> startsWith(key, "selection_"))
has_hglyph = any(Object.keys(attrs), (key) -> startsWith(key, "hover_"))
glyph_ca =
nsglyph_ca =
sglyph_ca = if has_sglyph then else {}
hglyph_ca = if has_hglyph then else {}
source = attrs.source ? new models.ColumnDataSource()
data = clone(source.data)
delete attrs.source
source.data = data
_make_glyph = (cls, attrs, extra_attrs) =>
new cls(extend({}, attrs, extra_attrs))
glyph = _make_glyph(cls, attrs, glyph_ca)
nsglyph = _make_glyph(cls, attrs, nsglyph_ca)
sglyph = if has_sglyph then _make_glyph(cls, attrs, sglyph_ca) else null
hglyph = if has_hglyph then _make_glyph(cls, attrs, hglyph_ca) else null
glyph_renderer = new models.GlyphRenderer({
data_source: source
glyph: glyph
nonselection_glyph: nsglyph
selection_glyph: sglyph
hover_glyph: hglyph
})
if legend?
return glyph_renderer
_marker: (cls, args) ->
return
_get_range: (range) ->
if not range?
return new models.DataRange1d()
if range instanceof models.Range
return range
if isArray(range)
if all(range, isString)
return new models.FactorRange({factors: range})
if range.length == 2
return new models.Range1d({start: range[0], end: range[1]})
_process_guides: (dim, axis_type, axis_location, minor_ticks, axis_label) ->
range = if dim == 0 then else
axiscls =
if axiscls?
if axiscls == models.LogAxis
if dim == 0
= 'log'
else
= 'log'
axis = new axiscls()
if axis.ticker instanceof models.ContinuousTicker
axis.ticker.num_minor_ticks =
if axis_label.length != 0
axis.axis_label = axis_label
grid = new models.Grid({dimension: dim, ticker: axis.ticker})
_get_axis_class: (axis_type, range) ->
if not axis_type?
return null
if axis_type == "linear"
return models.LinearAxis
if axis_type == "log"
return models.LogAxis
if axis_type == "datetime"
return models.DatetimeAxis
if axis_type == "auto"
if range instanceof models.FactorRange
return models.CategoricalAxis
else
# TODO: return models.DatetimeAxis (Date type)
return models.LinearAxis
_get_num_minor_ticks: (axis_class, num_minor_ticks) ->
if isNumber(num_minor_ticks)
if num_minor_ticks <= 1
throw new Error("num_minor_ticks must be > 1")
return num_minor_ticks
if not num_minor_ticks?
return 0
if num_minor_ticks == 'auto'
if axis_class == models.LogAxis
return 10
return 5
_process_tools: (tools) ->
if isString(tools)
tools = tools.split(/\s*,\s*/)
objs = for tool in tools
if isString(tool)
_known_tools[tool](this)
else
tool
return objs
_process_legend: (legend, source) ->
legend_item_label = null
if legend?
if isString(legend)
legend_item_label = { value: legend }
if source? and source.column_names?
if legend in source.column_names
legend_item_label = { field: legend }
else
legend_item_label = legend
return legend_item_label
_update_legend: (legend_item_label, glyph_renderer) ->
added = false
for item in .items
if isEqual(item.label, legend_item_label)
if item.label.value?
item.renderers.push(glyph_renderer)
added = true
break
if item.label.field? and glyph_renderer.data_source == item.renderers[0].data_source
item.renderers.push(glyph_renderer)
added = true
break
if not added
new_item = new models.LegendItem({ label: legend_item_label, renderers: [glyph_renderer] })
.items.push(new_item)
export figure = (attributes={}, options={}) ->
new Figure(attributes, options)
export show = (obj, target) ->
multiple = isArray(obj)
doc = new Document()
if not multiple
doc.add_root(obj)
else
for _obj in obj
doc.add_root(_obj)
if not target?
element = document.body
else if isString(target)
element = document.querySelector(target)
if not element?
throw new Error("'#{target}' selector didn't match any elements")
else if target instanceof HTMLElement
element = target
else if $? and target instanceof $
element = target[0]
else
throw new Error("target should be HTMLElement, string selector, $ or null")
root = div({class: BOKEH_ROOT})
element.appendChild(root)
views = embed.add_document_standalone(doc, root)
if not multiple
return views[obj.id]
else
return views
export color = (r, g, b) -> sprintf("#%02x%02x%02x", r, g, b)
export gridplot = (children, options={}) ->
toolbar_location = if options.toolbar_location == undefined then 'above' else options.toolbar_location
sizing_mode = if options.sizing_mode == undefined then 'fixed' else options.sizing_mode
toolbar_sizing_mode = if options.sizing_mode == 'fixed' then 'scale_width' else sizing_mode
tools = []
rows = []
for row in children
row_tools = []
row_children = []
for item in row
if item instanceof models.Plot
row_tools = row_tools.concat(item.toolbar.tools)
item.toolbar_location = null
if item == null
for neighbor in row
if neighbor instanceof models.Plot
break
item = new models.Spacer({width: neighbor.plot_width, height: neighbor.plot_height})
if item instanceof models.LayoutDOM
item.sizing_mode = sizing_mode
row_children.push(item)
else
throw new Error("only LayoutDOM items can be inserted into Grid")
tools = tools.concat(row_tools)
row = new models.Row({children: row_children, sizing_mode: sizing_mode})
rows.push(row)
grid = new models.Column({children: rows, sizing_mode: sizing_mode})
layout = if toolbar_location
toolbar = new models.ToolbarBox({tools: tools, sizing_mode: toolbar_sizing_mode, toolbar_location: toolbar_location})
switch toolbar_location
when 'above'
new models.Column({children: [toolbar, grid], sizing_mode: sizing_mode})
when 'below'
new models.Column({children: [grid, toolbar], sizing_mode: sizing_mode})
when 'left'
new models.Row({children: [toolbar, grid], sizing_mode: sizing_mode})
when 'right'
new models.Row({children: [grid, toolbar], sizing_mode: sizing_mode})
else
grid
return layout