@quartic/bokehjs
Version:
Interactive, novel data visualization
401 lines (339 loc) • 11.6 kB
text/coffeescript
import {SidePanel} from "core/layout/side_panel"
import {GuideRenderer} from "../renderers/guide_renderer"
import {RendererView} from "../renderers/renderer"
import {GE} from "core/layout/solver"
import {logger} from "core/logging"
import * as p from "core/properties"
import {isString, isArray} from "core/util/types"
export class AxisView extends RendererView
initialize: (options) ->
super(options)
= .x_range_name
= .y_range_name
render: () ->
if .visible == false
return
ctx = .canvas_view.ctx
ctx.save()
ctx.restore()
bind_bokeh_events: () ->
_get_size: () ->
return + +
_draw_rule: (ctx) ->
if not .axis_line.doit
return
[x, y] = coords = .rule_coords
[sx, sy] = .map_to_screen(x, y, , )
[nx, ny] = .normals
[xoff, yoff] = .offsets
.axis_line.set_value(ctx)
ctx.beginPath()
ctx.moveTo(Math.round(sx[0]+nx*xoff), Math.round(sy[0]+ny*yoff))
for i in [1...sx.length]
ctx.lineTo(Math.round(sx[i]+nx*xoff), Math.round(sy[i]+ny*yoff))
ctx.stroke()
_draw_major_ticks: (ctx) ->
if not .major_tick_line.doit
return
coords = .tick_coords
[x, y] = coords.major
[sx, sy] = .map_to_screen(x, y, , )
[nx, ny] = .normals
[xoff, yoff] = .offsets
tin = .major_tick_in
tout = .major_tick_out
.major_tick_line.set_value(ctx)
for i in [0...sx.length]
ctx.beginPath()
ctx.moveTo(Math.round(sx[i]+nx*tout+nx*xoff), Math.round(sy[i]+ny*tout+ny*yoff))
ctx.lineTo(Math.round(sx[i]-nx*tin+nx*xoff), Math.round(sy[i]-ny*tin+ny*yoff))
ctx.stroke()
_draw_minor_ticks: (ctx) ->
if not .minor_tick_line.doit
return
coords = .tick_coords
[x, y] = coords.minor
[sx, sy] = .map_to_screen(x, y, , )
[nx, ny] = .normals
[xoff, yoff] = .offsets
tin = .minor_tick_in
tout = .minor_tick_out
.minor_tick_line.set_value(ctx)
for i in [0...sx.length]
ctx.beginPath()
ctx.moveTo(Math.round(sx[i]+nx*tout+nx*xoff), Math.round(sy[i]+ny*tout+ny*yoff))
ctx.lineTo(Math.round(sx[i]-nx*tin+nx*xoff), Math.round(sy[i]-ny*tin+ny*yoff))
ctx.stroke()
_draw_major_labels: (ctx) ->
coords = .tick_coords
[x, y] = coords.major
[sx, sy] = .map_to_screen(x, y, , )
[nx, ny] = .normals
[xoff, yoff] = .offsets
dim = .dimension
side = .panel_side
orient = .major_label_orientation
if isString(orient)
angle = .panel.get_label_angle_heuristic(orient)
else
angle = -orient
standoff = + .major_label_standoff
labels = .formatter.doFormat(coords.major[dim], .loc)
.major_label_text.set_value(ctx)
.panel.apply_label_text_heuristics(ctx, orient)
for i in [0...sx.length]
if angle
ctx.translate(sx[i]+nx*standoff+nx*xoff, sy[i]+ny*standoff+ny*yoff)
ctx.rotate(angle)
ctx.fillText(labels[i], 0, 0)
ctx.rotate(-angle)
ctx.translate(-sx[i]-nx*standoff+nx*xoff, -sy[i]-ny*standoff+ny*yoff)
else
ctx.fillText(labels[i], Math.round(sx[i]+nx*standoff+nx*xoff), Math.round(sy[i]+ny*standoff+ny*yoff))
_draw_axis_label: (ctx) ->
label = .axis_label
if not label?
return
[x, y] = .rule_coords
[sx, sy] = .map_to_screen(x, y, , )
[nx, ny] = .normals
[xoff, yoff] = .offsets
side = .panel_side
orient = 'parallel'
angle = .panel.get_label_angle_heuristic(orient)
standoff = ( + + .axis_label_standoff)
sx = (sx[0] + sx[sx.length-1])/2
sy = (sy[0] + sy[sy.length-1])/2
.axis_label_text.set_value(ctx)
.panel.apply_label_text_heuristics(ctx, orient)
x = sx+nx*standoff+nx*xoff
y = sy+ny*standoff+ny*yoff
if isNaN(x) or isNaN(y)
return
if angle
ctx.translate(x, y)
ctx.rotate(angle)
ctx.fillText(label, 0, 0)
ctx.rotate(-angle)
ctx.translate(-x, -y)
else
ctx.fillText(label, x, y)
_tick_extent: () ->
return .major_tick_out
_tick_label_extent: () ->
extent = 0
ctx = .canvas_view.ctx
dim = .dimension
coords = .tick_coords.major
side = .panel_side
orient = .major_label_orientation
labels = .formatter.doFormat(coords[dim], .loc)
.major_label_text.set_value(ctx)
if isString(orient)
hscale = 1
angle = .panel.get_label_angle_heuristic(orient)
else
hscale = 2
angle = -orient
angle = Math.abs(angle)
c = Math.cos(angle)
s = Math.sin(angle)
if side == "above" or side == "below"
wfactor = s
hfactor = c
else
wfactor = c
hfactor = s
for i in [0...labels.length]
if not labels[i]?
continue
w = ctx.measureText(labels[i]).width * 1.1
h = ctx.measureText(labels[i]).ascent * 0.9
val = w*wfactor + (h/hscale)*hfactor
if val > extent
extent = val
if extent > 0
extent += .major_label_standoff
return extent
_axis_label_extent: () ->
extent = 0
side = .panel_side
axis_label = .axis_label
orient = 'parallel'
ctx = .canvas_view.ctx
.axis_label_text.set_value(ctx)
angle = Math.abs(.panel.get_label_angle_heuristic(orient))
c = Math.cos(angle)
s = Math.sin(angle)
if axis_label
extent += .axis_label_standoff
.axis_label_text.set_value(ctx)
w = ctx.measureText(axis_label).width * 1.1
h = ctx.measureText(axis_label).ascent * 0.9
if side == "above" or side == "below"
extent += w*s + h*c
else
extent += w*c + h*s
return extent
export class Axis extends GuideRenderer
default_view: AxisView
type: 'Axis'
[
'line:axis_',
'line:major_tick_',
'line:minor_tick_',
'text:major_label_',
'text:axis_label_'
]
{
bounds: [ p.Any, 'auto' ] # TODO (bev)
ticker: [ p.Instance, null ]
formatter: [ p.Instance, null ]
x_range_name: [ p.String, 'default' ]
y_range_name: [ p.String, 'default' ]
axis_label: [ p.String, '' ]
axis_label_standoff: [ p.Int, 5 ]
major_label_standoff: [ p.Int, 5 ]
major_label_orientation: [ p.Any, "horizontal" ] # TODO: p.Orientation | p.Number
major_tick_in: [ p.Number, 2 ]
major_tick_out: [ p.Number, 6 ]
minor_tick_in: [ p.Number, 0 ]
minor_tick_out: [ p.Number, 4 ]
}
{
axis_line_color: 'black'
major_tick_line_color: 'black'
minor_tick_line_color: 'black'
major_label_text_font_size: "8pt"
major_label_text_align: "center"
major_label_text_baseline: "alphabetic"
axis_label_text_font_size: "10pt"
axis_label_text_font_style: "italic"
}
{
panel_side: [ p.Any ]
}
initialize: (attrs, options)->
super(attrs, options)
{
computed_bounds: () ->
rule_coords: () ->
tick_coords: () ->
ranges: () ->
normals: () -> ._normals
dimension: () -> ._dim
offsets: () ->
loc: () ->
}
add_panel: (side) ->
= new SidePanel({side: side})
.attach_document()
= side
_offsets: () ->
side =
[xoff, yoff] = [0, 0]
frame = .plot_canvas.frame
switch side
when "below"
yoff = Math.abs(.top - frame.bottom)
when "above"
yoff = Math.abs(.bottom - frame.top)
when "right"
xoff = Math.abs(.left - frame.right)
when "left"
xoff = Math.abs(.right - frame.left)
return [xoff, yoff]
_ranges: () ->
i =
j = (i + 1) % 2
frame = .plot_canvas.frame
ranges = [
frame.x_ranges[],
frame.y_ranges[]
]
return [ranges[i], ranges[j]]
_computed_bounds: () ->
[range, cross_range] =
user_bounds = ? 'auto'
range_bounds = [range.min, range.max]
if user_bounds == 'auto'
return range_bounds
if isArray(user_bounds)
if Math.abs(user_bounds[0]-user_bounds[1]) >
Math.abs(range_bounds[0]-range_bounds[1])
start = Math.max(Math.min(user_bounds[0], user_bounds[1]),
range_bounds[0])
end = Math.min(Math.max(user_bounds[0], user_bounds[1]),
range_bounds[1])
else
start = Math.min(user_bounds[0], user_bounds[1])
end = Math.max(user_bounds[0], user_bounds[1])
return [start, end]
logger.error("user bounds '#{ user_bounds }' not understood")
return null
_rule_coords: () ->
i =
j = (i + 1) % 2
[range, cross_range] =
[start, end] =
xs = new Array(2)
ys = new Array(2)
coords = [xs, ys]
coords[i][0] = Math.max(start, range.min)
coords[i][1] = Math.min(end, range.max)
if coords[i][0] > coords[i][1]
coords[i][0] = coords[i][1] = NaN
coords[j][0] =
coords[j][1] =
return coords
_tick_coords: () ->
i =
j = (i + 1) % 2
[range, cross_range] =
[start, end] =
ticks = .get_ticks(start, end, range, , {})
majors = ticks.major
minors = ticks.minor
xs = []
ys = []
coords = [xs, ys]
minor_xs = []
minor_ys = []
minor_coords = [minor_xs, minor_ys]
if range.type == "FactorRange"
for ii in [0...majors.length]
coords[i].push(majors[ii])
coords[j].push()
else
[range_min, range_max] = [range.min, range.max]
for ii in [0...majors.length]
if majors[ii] < range_min or majors[ii] > range_max
continue
coords[i].push(majors[ii])
coords[j].push()
for ii in [0...minors.length]
if minors[ii] < range_min or minors[ii] > range_max
continue
minor_coords[i].push(minors[ii])
minor_coords[j].push()
return {
"major": coords,
"minor": minor_coords
}
_get_loc: () ->
[range, cross_range] =
cstart = cross_range.start
cend = cross_range.end
side =
return switch side
when 'left', 'below' then cross_range.start
when 'right', 'above' then cross_range.end