glyph_renderers.coffee | |
---|---|
Setup Bokeh Module | Collections = Continuum.Collections |
MAIN BOKEH CLASSES | |
backbone assumes that valid attrs are any non-null, or non-defined value thats dumb, we only check for undefined, because null is perfectly valid | safebind = Continuum.safebind
HasParent = Continuum.HasParent
BokehView = Continuum.ContinuumView
HasProperties = Continuum.HasProperties
class MetaGlyph
constructor: (@styleprovider, @glyphspec, @attrnames) -> |
| make_glyph: (datapoint) -> |
Returns an object that has properties corresponding all of the named
attributes in | glyph = {}
for attrname in @attrnames
if attrname.indexOf(":") > -1
[attrname, attrtype] = attrname.split(":")
else
attrtype = "number"
unitsname = attrname + "_units"
if not (attrname of @glyphspec) |
The field is absent from the glyph specification.
Use the default field name and check for its existence on the
datapoint. If it doesn't exist, then read the defaults from the
styleprovider. The default value of | if attrname of datapoint
glyph[attrname] = datapoint[attrname]
else
glyph[attrname] = @styleprovider.mget(attrname)
units = @styleprovider.mget(unitsname)
glyph[unitsname] = units ? 'data'
continue
else if _.isNumber(@glyphspec[attrname]) |
The glyph specification provided a number. This is always a default value. | glyph[attrname] = if attrname of datapoint then datapoint[attrname] else @glyphspec[attrname]
units = @styleprovider.mget(unitsname)
glyph[unitsname] = units ? 'data'
continue
else
if _.isString(@glyphspec[attrname]) |
The glyph specification provided a string for this field; how we interpret it depends on the type of the field. For string fields, this becomes the field value. For all others, treat this as customizing the name of the field. | if attrtype == 'string'
default_value = @glyphspec[attrname]
fieldname = attrname
else
default_value = @styleprovider.mget(attrname)
fieldname = @glyphspec[attrname] |
In either case, use the default units | units = @styleprovider.mget(unitsname)
glyph[unitsname] = units ? 'data'
else if _.isObject(@glyphspec[attrname])
obj = @glyphspec[attrname]
fieldname = if obj.field? then obj.field else attrname
default_value = if obj.default? then obj.default else @styleprovider.mget(attrname)
if obj.units?
glyph[unitsname] = obj.units
else
units = @styleprovider.mget(unitsname)
glyph[unitsname] = units ? 'data'
else |
This is an error down here... | console.log("Unknown glyph specification value type.")
continue |
Both string and object glyphspecs share this logic | if fieldname of datapoint
glyph[attrname] = datapoint[fieldname]
else
glyph[attrname] = default_value
return glyph |
class : GlyphRendererView | class GlyphRendererView extends Bokeh.XYRendererView
addSquare: (x, y, size, color) ->
if isNaN(x) or isNaN(y)
reqturn null
@plot_view.ctx.fillStyle = color
@plot_view.ctx.strokeStyle = color
@plot_view.ctx.fillRect(x - size / 2, y - size / 2, size, size)
addCircle: (x, y, size, color, outline_color, alpha) ->
ctx = @plot_view.ctx
if isNaN(x) or isNaN(y)
return null
if not (outline_color?)
outline_color = color
if alpha? and (alpha != ctx.globalAlpha)
old_alpha = ctx.globalAlpha
ctx.globalAlpha = alpha
ctx.fillStyle = color
ctx.strokeStyle = outline_color
ctx.beginPath()
ctx.arc(x, y, size/2, 0, Math.PI*2)
ctx.closePath()
ctx.fill()
ctx.stroke()
if alpha?
ctx.globalAlpha = old_alpha
addRect : (glyph, plot_view, left, right, bottom, top) -> |
Internal method for actually drawing a rectangle glyph contains some visual attributes, and plotview is typically @plotview. | |
TODO: We need to manually flip the Y axis coordinates | bottom = plot_view.viewstate.ypos(bottom)
top = plot_view.viewstate.ypos(top) |
At this point, we have the box boundaries (left, right, bottom, top) in screen space coordinates, and should be ready to draw. | |
In the following, we need to grab the first element of the returned valued b/c getter functions always return (val, units) and we don't care about units for color. | ctx = plot_view.ctx
old_alpha = ctx.globalAlpha
old_linewidth = ctx.lineWidth
ctx.globalAlpha = glyph.alpha
ctx.lineWidth = glyph.outline_width
if glyph.angle != 0 |
TODO: Fix angle handling. We need to translate, then rotate, then reset the ctm. Probably best to do this with save() and restore() b/c there doesn't seem to be a way to read out the current ctm. | if glyph.angle_units == 'deg'
angle = glyph.angle * Math.PI / 180
else
angle = glyph.angle
ctx.rotate(angle)
if glyph.color? and glyph.color != "none"
ctx.fillStyle = glyph.color
ctx.fillRect(left, bottom, right-left, top-bottom)
if glyph.outline_color? and glyph.outline_color != "none"
ctx.strokeStyle = glyph.outline_color
ctx.strokeRect(left, bottom, right-left, top-bottom)
if angle?
ctx.rotate(-angle)
ctx.globalAlpha = old_alpha
ctx.lineWidth = old_linewidth |
method : GlyphRendererView::calc_screen | calc_screen : (glyph, direction, datapoint, mapper) -> |
Parameters
Returns
| |
get dim, first from the glyph, otherwise from the glyph
renderer model. dims can either be strings to specify the field name
| dim = if glyph[direction] then glyph[direction] else @mget(direction)
if _.isArray(dim)
data = datapoint[dim[0]]
data = if dim[1] then dim[1] + data else data
screenoffset = if dim[2] then dim[2] else 0
else
data = datapoint[dim]
screenoffset = 0
if dim == 'x'
screenoffset = screenoffset * @plot_view.viewstate.get('width')
else
screenoffset = screenoffset * @plot_view.viewstate.get('height')
screen = mapper.map_screen(data) + screenoffset
return screen
render_scatter : (glyph, data) ->
datapoint = data[glyph.index]
screenx = @calc_screen(glyph, 'x', datapoint, @xmapper)
screeny = @calc_screen(glyph, 'y', datapoint, @ymapper)
size = if glyph.size then glyph.size else @mget('scatter_size')
color = if glyph.color then glyph.color else @mget('color')
if glyph.type == 'circle'
@addCircle(screenx, screeny, size, color)
if glyph.type == 'square'
@addSquare(screenx, screeny, size, color)
render : ->
screen_glpyhs = []
source = @mget_obj('data_source')
if source.type == "ObjectArrayDataSource"
data = source.get('data')
else if source.type == "ColumnDataSource"
data = source.datapoints()
for glyph in @mget('glyphs')
if glyph.type == 'circle' or glyph.type == 'square'
@render_scatter(glyph, data)
else if glyph.type == 'circles'
@render_circles(glyph, data)
else if glyph.type == 'rects'
@render_rects(glyph, data)
else if glyph.type == 'rectregions'
@render_rectregions(glyph, data)
else if glyph.type == 'line'
@render_line(glyph, data)
else if glyph.type == 'area'
@render_area(glyph, data)
else if glyph.type == 'stacked_lines'
@render_stacked_lines(glyph, data)
else if glyph.type == 'stacked_rects'
@render_stacked_rects(glyph, data)
else if glyph.type == 'boxplots'
@render_boxplots(glyph, data)
render_line : (glyphspec, data) -> |
Fields of the
| metaglyph = new MetaGlyph(this, glyphspec, ['x','y','line_width:string', 'line_color:string', 'alpha'])
ctx = @plot_view.ctx
ctx.save() |
Since we do not allow override of any of the aesthetic parameters from point to point, we just take the values off of the first_glyph. | first_glyph = metaglyph.make_glyph(data[0])
ctx.lineWidth = first_glyph.line_width
ctx.strokeStyle = first_glyph.line_color
ctx.globalAlpha = first_glyph.alpha
for idx in [0..data.length-1]
glyph = metaglyph.make_glyph(data[idx])
if not (glyph.x? and glyph.y?)
continue
if glyph.x_units == 'data'
sx = @plot_view.viewstate.xpos(@xmapper.map_screen(glyph.x))
else
sx = glyph.x
if glyph.y_units == 'data'
sy = @plot_view.viewstate.ypos(@ymapper.map_screen(glyph.y))
else
sy = glyph.y
if idx == 0 |
First glyph, start the path | ctx.beginPath()
ctx.moveTo(sx, sy)
continue
else if isNaN(sx) or isNaN(sy)
ctx.stroke()
ctx.beginPath()
continue
else
ctx.lineTo(sx, sy)
ctx.stroke()
ctx.restore()
render_stacked_lines : (glyphspec, data) -> |
Fields of the | ctx = @plot_view.ctx
ctx.save()
accum = []
for pt in data
accum.push(0)
for yidx in [0..(glyphspec.y.length-1)] |
render the area | ctx.lineWidth = 0
ctx.fillStyle = glyphspec.fills[glyphspec.y[yidx]].fill_color
ctx.globalAlpha = glyphspec.fills[glyphspec.y[yidx]].fill_alpha
for idx in [0..data.length-1]
sx = @plot_view.viewstate.xpos(@xmapper.map_screen(data[idx].x))
sy = @plot_view.viewstate.ypos(@ymapper.map_screen(accum[idx]))
if idx == 0
ctx.beginPath()
ctx.moveTo(sx, sy)
else
ctx.lineTo(sx, sy)
for idx in [(data.length-1)..0]
y = accum[idx] + data[idx][glyphspec.y[yidx]]
sx = @plot_view.viewstate.xpos(@xmapper.map_screen(data[idx].x))
sy = @plot_view.viewstate.ypos(@ymapper.map_screen(y))
accum[idx] = y
ctx.lineTo(sx, sy)
ctx.closePath()
ctx.fill() |
render the line | ctx.lineWidth = glyphspec.lines[glyphspec.y[yidx]].line_width
ctx.strokeStyle = glyphspec.lines[glyphspec.y[yidx]].line_color
ctx.globalAlpha = glyphspec.lines[glyphspec.y[yidx]].line_alpha
for idx in [0..data.length-1]
sx = @plot_view.viewstate.xpos(@xmapper.map_screen(data[idx].x))
sy = @plot_view.viewstate.ypos(@ymapper.map_screen(accum[idx]))
if idx == 0
ctx.beginPath()
ctx.moveTo(sx, sy)
continue
else
ctx.lineTo(sx, sy)
ctx.stroke()
ctx.restore()
render_stacked_rects : (glyphspec, data) -> |
Fields of the | rectdata = []
glyph = {
type: 'rects'
x: 'x'
color:
field: 'color'
}
accum = []
for pt in data
accum.push(0)
width = 0.2 # TODO
for yidx in [0..(glyphspec.y.length-1)]
color = glyphspec.fills[glyphspec.y[yidx]].fill_color
for idx in [0..data.length-1]
x = data[idx].x
height = data[idx][glyphspec.y[yidx]]
y = accum[idx] + height/2
accum[idx] = accum[idx] + height
pt = {}
pt['x'] = x
pt['y'] = y
pt['width'] = width
pt['height'] = height
pt['color'] = color
rectdata.push(pt)
@render_rects(glyph, rectdata)
render_circles : (glyphspec, data) -> |
Fields of the
| metaglyph = new MetaGlyph(this, glyphspec, ["x", "y", "radius", "color:string", "outline_color:string", "outline_width", "alpha"])
@plot_view.ctx.save()
for datapoint in data
glyph = metaglyph.make_glyph(datapoint) |
Instead of calling @calc_screen and supporting offsets, we just bake that logic into the loop here. | sx = @plot_view.viewstate.xpos(if glyph.x_units == 'screen' then glyph.x else @xmapper.map_screen(glyph.x))
sy = @plot_view.viewstate.ypos(if glyph.y_units == 'screen' then glyph.y else @ymapper.map_screen(glyph.y))
if glyph.radius_units == 'data' |
Use of span2bounds is a tiny bit hackish, because it assumes the span is specified as (center,width). In our case, we don't care about center. | [left, right, units] = @_span2bounds(glyph.x, glyph.x_units, glyph.radius, glyph.radius_units)
if units == 'data'
left = @xmapper.map_screen(left)
right = @xmapper.map_screen(right)
size = right - left
else
size = glyph.radius
ctx = @plot_view.ctx
old_linewidth = ctx.lineWidth
ctx.lineWidth = glyph.outline_width
@addCircle(sx, sy, size, glyph.color, glyph.outline_color, glyph.alpha)
ctx.lineWidth = old_linewidth
@plot_view.ctx.restore()
render_area : (glyphspec, data) ->
metaglyph = new MetaGlyph(this, glyphspec, ['x','y','color:string', 'outline_width:string', 'outline_color:string', 'alpha'])
ctx = @plot_view.ctx
ctx.save() |
Since we do not allow override of any of the aesthetic parameters from point to point, we just take the values off of the first_glyph. | first_glyph = metaglyph.make_glyph(data[0])
ctx.fillStyle = first_glyph.color
ctx.lineWidth = first_glyph.outline_width
ctx.strokeStyle = first_glyph.outline_color
ctx.globalAlpha = first_glyph.alpha
for idx in [0..data.length-1]
glyph = metaglyph.make_glyph(data[idx])
if not (glyph.x? and glyph.y?)
continue
if glyph.x_units == 'data'
sx = @plot_view.viewstate.xpos(@xmapper.map_screen(glyph.x))
else
sx = glyph.x
if glyph.y_units == 'data'
sy = @plot_view.viewstate.ypos(@ymapper.map_screen(glyph.y))
else
sy = glyph.y
if idx == 0 |
First glyph, start the path | ctx.beginPath()
ctx.moveTo(sx, sy)
continue
ctx.lineTo(sx, sy)
ctx.closePath()
ctx.fill()
ctx.stroke()
ctx.restore()
render_rects : (glyphspec, data) -> |
A rectangle, specified by a center and width & height. Spatial parameters
For each of these spatial parameters, the full specification in the glyph is an embedded object with the following properties: * field: the name of the field in each data point; this defaults to the name of the spatial parameter itself (e.g. 'height', 'bottom', 'x') * default: a numerical default value to use if the field does not exist on a datapoint. Each spatial parameter also has a global default value (defined in GlyphRenderer::display_defaults) * units: For all parameters except 'angle', this specifies the coordinate space in which to interpret data values, either 'data' (default) or 'screen'. For the 'angle' parameter, this property is either 'deg' or 'rad'. Example: type: 'rects' x: field: 'var1' units: 'data' y: field: 'var2' default: 10 units: screen However, a shorthand can be used if only the field name needs to be specified, or a constant default numerical value is to be used. Example: type: 'rects' width: 'var3' # shorthand for field:'var3' height: 8 # shorthand for default:8, units:'data' If a numerical default value is specified, it can still be overridden on a per-datapoint basis since the GlyphRenderer::display_defaults specify default field names for each of these properties. In the example above, if a datapoint had an additional field named 'height', which is the default field name for the height parameter, then it would override the constant value of 8. For colors and other properties which can accept string values, there is potential ambiguity in the shorthand form: type: 'rects' color: 'red' Does this mean that the default value of type: 'rects' color: field: 'colorfieldname' Other parameters
| |
TODO: This checking for the 'x' or 'left' parameter of the Glyph spec is brittle and behaves poorly; perhaps the user omits these fields altogether? Need a better way to specify this, perhaps an explicit parameter. | params = ['x','y','width','height']
params.push.apply(params, ["angle", "color:string", "outline_color:string","alpha", "outline_width"])
metaglyph = new MetaGlyph(this, glyphspec, params)
@plot_view.ctx.save()
for datapoint in data
glyph = metaglyph.make_glyph(datapoint)
[left,right,h_units] = @_span2bounds(glyph.x, glyph.x_units, glyph.width, glyph.width_units, @xmapper)
if h_units == 'data'
left = @xmapper.map_screen(left)
right = @xmapper.map_screen(right)
[bottom,top,v_units] = @_span2bounds(glyph.y, glyph.y_units, glyph.height, glyph.height_units, @ymapper)
if v_units == 'data'
bottom = @ymapper.map_screen(bottom)
top = @ymapper.map_screen(top)
@addRect(glyph, @plot_view, left, right, bottom, top) |
End per-datapoint loop | |
Done with all drawing, restore the graphics state | @plot_view.ctx.restore()
return # render_rects()
render_rectregions : (glyphspec, data) -> |
Rectangles, specified by their edges (left, right, bottom, top). Spatial parameters
The treatment of these parameters is the same as in render_rects, that is, there is a full object specification in the glyph, with a shorthand if only the field name or a constant numerical value needs to be used. Example: type: 'rectregions' left: 'left' bottom: field: "foo" default: 1.8 units: "data" Other parametersThese are identical to the parameters of the "rects" glyph. | params = ['left', 'right', 'bottom', 'top']
params.push.apply(params, ["angle", "color:string", "outline_color:string","alpha", "outline_width"])
metaglyph = new MetaGlyph(this, glyphspec, params)
@plot_view.ctx.save()
for datapoint in data
glyph = metaglyph.make_glyph(datapoint)
if glyph.left_units == 'data'
left = @xmapper.map_screen(glyph.left)
if glyph.right_units == 'data'
right = @xmapper.map_screen(glyph.right)
if glyph.bottom_units == 'data'
bottom = @ymapper.map_screen(glyph.bottom)
if glyph.top_units == 'data'
top = @ymapper.map_screen(glyph.top)
@addRect(glyph, @plot_view, left, right, bottom, top) |
Done with all drawing, restore the graphics state | @plot_view.ctx.restore()
return # render_rect_regions()
render_boxplots : (glyphspec, data) ->
metaglyph = new MetaGlyph(this, glyphspec, ['x', 'median', 'size', 'q1', 'q3'])
ctx = @plot_view.ctx
ctx.save()
ctx.fillStyle = 'lightblue'
ctx.lineWidth = 1.0
ctx.strokeStyle = 'orange'
ctx.globalAlpha = 1.0
for datapoint in data
glyph = metaglyph.make_glyph(datapoint)
iqr = glyph.q3 - glyph.q1
x = @plot_view.viewstate.xpos(@xmapper.map_screen(glyph.x))
ym = @plot_view.viewstate.ypos(@ymapper.map_screen(glyph.median))
yq1 = @plot_view.viewstate.ypos(@ymapper.map_screen(glyph.q1))
yq3 = @plot_view.viewstate.ypos(@ymapper.map_screen(glyph.q3))
yl = @plot_view.viewstate.ypos(@ymapper.map_screen(glyph.q1 - 1.5*iqr))
yu = @plot_view.viewstate.ypos(@ymapper.map_screen(glyph.q3 + 1.5*iqr))
half_size = glyph.size/2.0
whisker_half_size = half_size*0.8
if glyph.size_units == 'data'
x0 = @plot_view.viewstate.xpos(@xmapper.map_screen(glyph.x-half_size))
x1 = @plot_view.viewstate.xpos(@xmapper.map_screen(glyph.x+half_size))
xw0 = @plot_view.viewstate.xpos(@xmapper.map_screen(glyph.x-whisker_half_size))
xw1 = @plot_view.viewstate.xpos(@xmapper.map_screen(glyph.x+whisker_half_size))
else
x0 = x - half_size
x1 = x + half_size
xw0 = x - whisker_half_size
xw1 = x + whisker_half_size |
upper box | ctx.moveTo(x0, yq3)
ctx.lineTo(x1, yq3)
ctx.lineTo(x1, ym)
ctx.lineTo(x0, ym)
ctx.closePath()
ctx.stroke() |
lower box | ctx.moveTo(x0, yq1)
ctx.lineTo(x1, yq1)
ctx.lineTo(x1, ym)
ctx.lineTo(x0, ym)
ctx.closePath()
ctx.stroke() |
centerline | ctx.moveTo(x0, ym)
ctx.lineTo(x1, ym)
ctx.stroke() |
upper line | ctx.moveTo(x, yq3)
ctx.lineTo(x, yu)
ctx.stroke() |
lower line | ctx.moveTo(x, yq1)
ctx.lineTo(x, yl)
ctx.stroke() |
upper whisker | ctx.moveTo(xw0, yu)
ctx.lineTo(xw1, yu)
ctx.stroke() |
lower whisker | ctx.moveTo(xw0, yl)
ctx.lineTo(xw1, yl)
ctx.stroke()
ctx.restore()
_span2bounds : (center, center_units, span, span_units, mapper) -> |
Given a center value and a span value of potentially different spaces, returns an tuple (min, max, units) normalizing them into the space space ('data' or 'screen'), via the given mapper. NB: The mapper must be able to map from screen to data space. TODO: This function should probably be moved onto the Mappers. | halfspan = span / 2
if center_units == 'data' and span_units == 'data'
return [center-halfspan, center+halfspan, 'data']
else if center_units == 'data' and span_units == 'screen'
center_s = mapper.map_screen(center)
return [center_s-halfspan, center_s+halfspan, 'screen']
else if center_units == 'screen' and span_units == 'data'
center_d = mapper.map_data(center)
return [center_d-halfspan, centerd+halfspan, 'data']
else if center_units == 'screen' and span_units == 'screen'
return [center-halfspan, center+halfspan, 'screen'] |
class : GlyphRenderer | class GlyphRenderer extends HasParent |
Example definition | type : 'GlyphRenderer'
default_view : GlyphRendererView
GlyphRenderer::defaults = _.clone(GlyphRenderer::defaults)
_.extend(GlyphRenderer::defaults,
data_source : null
scatter_size : 3
color : 'black'
)
GlyphRenderer::display_defaults = _.clone(GlyphRenderer::display_defaults)
_.extend(GlyphRenderer::display_defaults, {
radius : 5
radius_units: 'screen'
color : 'gray'
outline_color: 'none'
outline_width: 1
angle_units: 'deg' |
Rects glyph | height: 1
width: 1
top: 1
bottom: 0
left: 0
right: 1
})
class GlyphRenderers extends Continuum.Collection
model : GlyphRenderer
if not Continuum.Collections.GlyphRenderer
Continuum.Collections.GlyphRenderer = new GlyphRenderers
Bokeh.GlyphRendererView = GlyphRendererView
|