Skip to content

Instantly share code, notes, and snippets.

@cpapazian
Last active November 17, 2015 06:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cpapazian/6228888157c39be85e9c to your computer and use it in GitHub Desktop.
Save cpapazian/6228888157c39be85e9c to your computer and use it in GitHub Desktop.
React/D3 Histogram

React + D3

Demonstration of React + D3 integration. Features include

  • React component that wraps an arbitrary D3 chart closure.
  • App component that manages state (selected stock tickers, data) and passes data as props to D3 wrapper component.
  • A histogram of various random die rolls
  • A legend
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")
<html>
<head></head>
<style>
body {
font: 10px sans-serif;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
.bar rect {
fill: steelblue;
shape-rendering: crispEdges;
}
.bar text {
fill: #fff;
}
</style>
<body>
<div id="app">errp!</div>
</body>
<script src="http://underscorejs.org/underscore.js"></script>
<script src="https://fb.me/react-0.14.2.js"></script>
<script src="https://fb.me/react-dom-0.14.2.js"></script>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="http://coffeescript.org/extras/coffee-script.js"></script>
<script type="text/coffeescript" src="app.coffee"></script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment