|
# Base class with features common to all charts |
|
window.Chart = Backbone.D3View.extend |
|
initialize: (conf) -> |
|
# store current pixel width and height |
|
@width = @el.getBoundingClientRect().width |
|
@height = @el.getBoundingClientRect().height |
|
|
|
@d3el.classed 'chart', true |
|
|
|
# A basic bar chart |
|
window.BarChart = Chart.extend |
|
initialize: (conf) -> |
|
Chart.prototype.initialize.call(this, conf) |
|
|
|
@d3el.classed 'bar_chart', true |
|
|
|
# horizontal is the default orientation |
|
@orientation = if conf.orientation? then conf.orientation else 'horizontal' |
|
|
|
# key function is used to identify data, defaults to array index |
|
@key = if conf.key? then conf.key else (d,i) -> i |
|
# value accessor (defaults to d itself) |
|
@value = if conf.value? then conf.value else (d) -> d |
|
|
|
# this is the default configuration for scales |
|
@scales_config = |
|
size: |
|
type: d3.scale.linear |
|
range: (data) -> [0, if @orientation is 'horizontal' then @width else @height] |
|
domain: (data) -> [0, d3.max(data, @value)] |
|
position: |
|
type: d3.scale.ordinal |
|
range: (data) -> [0, if @orientation is 'horizontal' then @height else @width] |
|
domain: (data) -> data.map @key |
|
color: |
|
type: d3.scale.category10 |
|
range: (data) -> @scales.color.range() # keeps the same range to support d3 default categorical scales as types |
|
domain: (data) -> data.map @key |
|
|
|
# mix in user-defined scales |
|
if conf.scales? |
|
Object.keys(conf.scales).forEach (s) => |
|
Object.keys(conf.scales[s]).forEach (p) => |
|
@scales_config[s][p] = conf.scales[s][p] |
|
|
|
# upcast to functions |
|
Object.keys(@scales_config).forEach (s) => |
|
Object.keys(@scales_config[s]).forEach (p) => |
|
@scales_config[s][p] = d3.functor(@scales_config[s][p]) |
|
|
|
# instantiate scales |
|
@scales = |
|
size: @scales_config.size.type() |
|
position: @scales_config.position.type() |
|
color: @scales_config.color.type() |
|
|
|
# tooltip - default: none |
|
if conf.tooltip? |
|
@tooltip = conf.tooltip |
|
|
|
# react to changes in data |
|
@listenTo @model, 'change:data', @render |
|
# react to changes in focus |
|
@listenTo @model, 'change:focus', @update_focus |
|
|
|
@render() |
|
|
|
# a focused item could already be set |
|
@update_focus() |
|
|
|
render: () -> |
|
data = @model.attributes.data |
|
|
|
# update scales |
|
# FIXME always updating could be heavy to do |
|
@update_scales(data) |
|
|
|
# update axes |
|
size_axis = d3.svg.axis() |
|
.scale(@scales.size) |
|
.tickSize(if @orientation is 'horizontal' then @height else @width) |
|
.orient(if @orientation is 'horizontal' then 'bottom' else 'right') |
|
|
|
@d3el.append 'g' |
|
.attr |
|
class: 'axis' |
|
.call size_axis |
|
|
|
# enter / update / exit |
|
bars = @d3el.selectAll '.bar' |
|
.data data, @key |
|
|
|
enter_bars = bars.enter().append 'rect' |
|
.attr |
|
class: 'bar' |
|
x: 0 |
|
.on 'mouseover', (d,i) => |
|
@model.set |
|
focus: @key(d,i) |
|
.on 'mouseout', () => |
|
@model.set |
|
focus: null |
|
|
|
if @orientation is 'horizontal' |
|
bars |
|
.attr |
|
y: (d,i) => @scales.position( @key(d,i) ) |
|
width: (d,i) => @scales.size( @value(d,i) ) |
|
height: @scales.position.rangeBand() |
|
else |
|
bars |
|
.attr |
|
x: (d,i) => @scales.position( @key(d,i) ) |
|
height: (d,i) => @scales.size( @value(d,i) ) |
|
y: (d,i) => @height - @scales.size( @value(d,i) ) |
|
width: @scales.position.rangeBand() |
|
|
|
bars |
|
.attr |
|
fill: (d,i) => @scales.color( @key(d,i) ) |
|
|
|
bars.exit() |
|
.remove() |
|
|
|
# add tooltips, if defined |
|
if @tooltip? |
|
enter_bars.append 'title' |
|
|
|
bars.select 'title' |
|
.text @tooltip |
|
|
|
update_focus: () -> |
|
@d3el.selectAll '.bar' |
|
.classed 'focus', (d,i) => @key(d,i) is @model.attributes.focus |
|
|
|
# reconfigure all the scales according to data |
|
update_scales: (data) -> |
|
@scales |
|
.size |
|
.domain( @scales_config.size.domain.call(this, data) ) |
|
.range( @scales_config.size.range.call(this, data) ) |
|
|
|
@scales |
|
.position |
|
.domain( @scales_config.position.domain.call(this, data) ) |
|
.rangeRoundBands( @scales_config.position.range.call(this, data), 0.05 ) |
|
@scales |
|
.color |
|
.domain( @scales_config.color.domain.call(this, data) ) |
|
.range( @scales_config.color.range.call(this, data) ) |
|
|