|
r$ = React.createElement |
|
|
|
D3Histogram = () -> |
|
xScale = null |
|
yScale = null |
|
data = [] |
|
series = [] |
|
color = [] |
|
|
|
graph = (el) -> |
|
x1 = d3.scale.ordinal() |
|
.domain(series) |
|
.rangeRoundBands([0, xScale.rangeBand()]); |
|
|
|
binUpdate = el.selectAll(".bin").data(data) |
|
binEnter = binUpdate.enter() |
|
.append("g") |
|
.attr("class", "bin") |
|
.attr("transform", (d) -> "translate(#{xScale(d.bin)},0)") |
|
|
|
binUpdate.attr("transform", (d) -> "translate(#{xScale(d.bin)},0)") |
|
|
|
barUpdate = binUpdate.selectAll(".bar").data( (d) -> series.map (s) -> {x: s, y: d[s]} ) |
|
barEnter = barUpdate.enter().append("g").attr("class", "bar") |
|
|
|
barEnter.append("rect") |
|
.attr("width", x1.rangeBand()) |
|
.attr("x", (d) -> x1(d.x)) |
|
.attr("y", (d) -> yScale(d.y)) |
|
.attr("height", (d) -> yScale.range()[0] - yScale(d.y)) |
|
.style("fill", (d) -> color(d.x)) |
|
|
|
barUpdate.selectAll("rect") |
|
.attr("width", x1.rangeBand()) |
|
.attr("x", (d) -> x1(d.x)) |
|
.attr("y", (d) -> yScale(d.y)) |
|
.attr("height", (d) -> yScale.range()[0] - yScale(d.y)) |
|
.style("fill", (d) -> color(d.x)) |
|
|
|
graph.xScale = (val) -> |
|
if arguments.length == 0 |
|
xScale |
|
else |
|
xScale = val |
|
line = d3.svg.line() |
|
.x (d) -> xScale(d.x) |
|
.y (d) -> yScale(d.y) |
|
@ |
|
|
|
graph.yScale = (val) -> |
|
if arguments.length == 0 |
|
yScale |
|
else |
|
yScale = val |
|
line = d3.svg.line() |
|
.x (d) -> xScale(d.x) |
|
.y (d) -> yScale(d.y) |
|
@ |
|
|
|
graph.series = (val) -> |
|
if arguments.length == 0 |
|
series |
|
else |
|
series = val |
|
@ |
|
|
|
graph.data = (val) -> |
|
if arguments.length == 0 |
|
data |
|
else |
|
data = val |
|
@ |
|
|
|
graph.color = (val) -> |
|
if arguments.length == 0 |
|
color |
|
else |
|
color = val |
|
@ |
|
|
|
graph |
|
|
|
Legend = () -> |
|
data = [] |
|
width = 0 |
|
color = [] |
|
|
|
graph = (el) -> |
|
legendUpdate = el.selectAll(".legend").data(data.slice().reverse()) |
|
legendEnter = legendUpdate.enter().append("g").attr("class", "legend") |
|
.attr("transform", (d, i) -> "translate(0, #{i*20})") |
|
|
|
legendUpdate.attr("transform", (d, i) -> "translate(0, #{i*20})") |
|
|
|
legendEnter.append("rect") |
|
legendUpdate.selectAll("rect") |
|
.attr("x", width - 18) |
|
.attr("width", 18) |
|
.attr("height", 18) |
|
.style("fill", color); |
|
|
|
legendEnter.append("text") |
|
legendUpdate.selectAll("text") |
|
.attr("x", width - 24) |
|
.attr("y", 9) |
|
.attr("dy", ".35em") |
|
.style("text-anchor", "end") |
|
.text((d) -> d) |
|
|
|
graph.width = (val) -> |
|
if arguments.length == 0 |
|
width |
|
else |
|
width = val |
|
@ |
|
|
|
graph.data = (val) -> |
|
if arguments.length == 0 |
|
data |
|
else |
|
data = val |
|
@ |
|
|
|
graph.color = (val) -> |
|
if arguments.length == 0 |
|
color |
|
else |
|
color = val |
|
@ |
|
|
|
graph |
|
|
|
App = React.createClass |
|
displayName: "App" |
|
|
|
getInitialState: -> |
|
width: 600 |
|
height: 400 |
|
margin: { top: 20, right: 40, bottom: 40, left: 60 } |
|
data: @getData() |
|
selected: {} |
|
|
|
rolls: [ |
|
[6, 3], |
|
[6, 2], |
|
[4, 5] |
|
] |
|
|
|
bins: [1..21] |
|
|
|
getSeries: -> @rolls.map ([s,c]) -> "#{c}d#{s}" |
|
|
|
getData: -> |
|
dice = (sides, count=1) -> |
|
if count == 1 |
|
1 + Math.floor(Math.random() * sides) |
|
else |
|
dice(sides) + dice(sides, count-1) |
|
|
|
return _.object @getSeries(), @rolls.map( ([s,c]) -> [0..100000].map () -> dice(s,c) ) |
|
|
|
componentDidMount: -> |
|
@handleResize() |
|
window.addEventListener("resize", @handleResize) |
|
|
|
componentWillUnmount: -> |
|
window.removeEventListener("resize", @handleResize) |
|
|
|
handleResize: -> |
|
@setState |
|
width: ReactDOM.findDOMNode(@).offsetWidth #- @state.margin.left - @state.margin.right |
|
height: ReactDOM.findDOMNode(@).offsetHeight #- @state.margin.top - @state.margin.bottom |
|
|
|
|
|
parseData: -> |
|
histogram = d3.layout.histogram().bins(@bins) |
|
|
|
data = _.chain(@state.data) |
|
.pairs() |
|
.map ([name, values]) -> [name, histogram(values)] |
|
.map ([name, values]) -> _.map values, (d) -> _.object ["bin", name], [d.x, d.y] |
|
.reduce (left, right) -> _.chain(left).zip(right).map( ([l, r]) -> _.extend {}, l, r ).value() |
|
.value() |
|
|
|
return data |
|
|
|
xScale: -> |
|
width = @state.width - @state.margin.left - @state.margin.right |
|
data = @parseData() |
|
|
|
d3.scale.ordinal() |
|
.domain @bins |
|
.rangeRoundBands([0, width], .1) |
|
|
|
yScale: -> |
|
height = @state.height - @state.margin.top - @state.margin.bottom |
|
data = @parseData() |
|
|
|
d3.scale.linear() |
|
.domain [0, d3.max data, (d) -> d3.max(_.chain(d).omit("bin").values().value())] |
|
.range [height, 0] |
|
|
|
render: -> |
|
series = @getSeries() |
|
data = @parseData() |
|
xScale = @xScale() |
|
yScale = @yScale() |
|
color = d3.scale.category10() |
|
|
|
r$ "div", |
|
{} |
|
r$ "svg", |
|
className: "chart bar-chart" |
|
width: @state.width |
|
height: @state.height |
|
r$ "g", |
|
ref: "g-chart" |
|
className: "chart" |
|
transform: "translate(#{@state.margin.left}, #{@state.margin.top})" |
|
r$ D3Component, |
|
className: "graph" |
|
chart: D3Histogram |
|
xScale: xScale |
|
yScale: yScale |
|
data: data |
|
series: series |
|
color: color |
|
r$ D3Component, |
|
className: "x axis" |
|
chart: d3.svg.axis, |
|
attrs: |
|
transform: "translate(0, #{@state.height - @state.margin.top - @state.margin.bottom})" |
|
scale: xScale |
|
orient: "bottom" |
|
r$ D3Component, |
|
className: "y axis" |
|
chart: d3.svg.axis |
|
key: "y-axis" |
|
scale: yScale |
|
orient: "left" |
|
r$ D3Component, |
|
className: "legend" |
|
chart: Legend |
|
data: series |
|
width: @state.width - @state.margin.left - @state.margin.right |
|
color: color |
|
|
|
D3Component = React.createClass |
|
displayName: "D3Component" |
|
|
|
propTypes: |
|
chart: React.PropTypes.func.isRequired |
|
|
|
componentWillMount: -> |
|
@chart = @props.chart() |
|
|
|
componentDidMount: -> |
|
if @updateChart(@chart) |
|
@chart @selectNode() |
|
|
|
componentDidUpdate: (prevProps, prevState) -> |
|
if @updateChart(@chart, prevProps) |
|
@chart @selectNode() |
|
|
|
componentWillUnmount: () -> |
|
_.chain(@props) |
|
.keys() |
|
.filter (key) -> key.indexOf("on") == 0 |
|
.each (key) => |
|
event_name = "#{key.charAt(2).toLowerCase()}#{key.substr(3)}" |
|
@chart.on event_name, null |
|
|
|
updateChart: (chart, prevProps={}) -> |
|
_.chain(@props) |
|
.omit "chart", "className", "attrs", "key" |
|
.pairs() |
|
.reject ([key, value]) -> prevProps[key] == value |
|
.each ([key, value]) -> |
|
if key.indexOf("on") == 0 |
|
event_name = "#{key.charAt(2).toLowerCase()}#{key.substr(3)}" |
|
chart.on event_name, value |
|
else if typeof chart[key] is 'function' |
|
chart[key] value |
|
else |
|
console.warn "#{key} is not an attribute of chart object; skipping" |
|
return |
|
|
|
.isEmpty() |
|
.negate() |
|
.value() |
|
|
|
selectNode: -> |
|
d3.select(ReactDOM.findDOMNode(@)) |
|
|
|
render: () -> |
|
r$ "g", |
|
_.extend {}, @props.attrs, className: @props.className |
|
|
|
ReactDOM.render r$(App), document.getElementById("app") |