Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active October 15, 2015 20:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitaku/05a1f298ba13380ae934 to your computer and use it in GitHub Desktop.
Save nitaku/05a1f298ba13380ae934 to your computer and use it in GitHub Desktop.
D3.js dispatch

An example that uses d3.dispatch to keep two visualizations in sync. The code is organized in different modules, following a convention inspired by the MVC pattern.

Use the mouse to hover a bar or a section to see the corresponding datapoint in the other visualization. You can also add or remove datapoints by first clicking on the diagram to enable focus, then using the arrow keys (right to add, left to remove).

window.bar = {}
bar.dispatcher = d3.dispatch('focus','blur')
vis = null
width = null
height = null
x = d3.scale.linear()
color_scale = d3.scale.category10()
bar.init = (v, w, h) ->
vis = v
width = w
height = h
x.range([0, width])
bar.update = (data) ->
x.domain([0, d3.sum(data)])
subbars = vis.selectAll('.subbar')
.data(data)
subbars.enter().append('rect')
.attr
class: 'subbar datapoint'
fill: (d,i) -> color_scale(i)
y: 0
height: height
.on('mouseover', subbar_on_mouseover)
.on('mouseout', subbar_on_mouseout)
subbars
.attr
x: (d,i) -> x(d3.sum(data[0...i]))
width: (d) -> x(d)
subbars.exit()
.remove()
subbar_on_mouseover = (d,i) ->
bar.dispatcher.focus(i)
subbar_on_mouseout = (d,i) ->
bar.dispatcher.blur(i)
bar.focus = (i) ->
vis.select(".subbar:nth-of-type(#{i+1})")
.classed('focus', true)
bar.blur = (i) ->
vis.select(".subbar:nth-of-type(#{i+1})")
.classed('focus', false)
// Generated by CoffeeScript 1.4.0
(function() {
var color_scale, height, subbar_on_mouseout, subbar_on_mouseover, vis, width, x;
window.bar = {};
bar.dispatcher = d3.dispatch('focus', 'blur');
vis = null;
width = null;
height = null;
x = d3.scale.linear();
color_scale = d3.scale.category10();
bar.init = function(v, w, h) {
vis = v;
width = w;
height = h;
return x.range([0, width]);
};
bar.update = function(data) {
var subbars;
x.domain([0, d3.sum(data)]);
subbars = vis.selectAll('.subbar').data(data);
subbars.enter().append('rect').attr({
"class": 'subbar datapoint',
fill: function(d, i) {
return color_scale(i);
},
y: 0,
height: height
}).on('mouseover', subbar_on_mouseover).on('mouseout', subbar_on_mouseout);
subbars.attr({
x: function(d, i) {
return x(d3.sum(data.slice(0, i)));
},
width: function(d) {
return x(d);
}
});
return subbars.exit().remove();
};
subbar_on_mouseover = function(d, i) {
return bar.dispatcher.focus(i);
};
subbar_on_mouseout = function(d, i) {
return bar.dispatcher.blur(i);
};
bar.focus = function(i) {
return vis.select(".subbar:nth-of-type(" + (i + 1) + ")").classed('focus', true);
};
bar.blur = function(i) {
return vis.select(".subbar:nth-of-type(" + (i + 1) + ")").classed('focus', false);
};
}).call(this);
window.bar_chart = {}
bar_chart.dispatcher = d3.dispatch('focus','blur')
vis = null
width = null
height = null
x = d3.scale.ordinal()
y = d3.scale.linear()
color_scale = d3.scale.category10()
bar_chart.init = (v, w, h) ->
vis = v
width = w
height = h
y.range([0, height])
bar_chart.update = (data) ->
x
.domain(d3.range(0, data.length))
.rangeRoundBands([0,width],0.015,0)
y.domain([0, d3.max(data)])
bars = vis.selectAll('.bar')
.data(data)
bars.enter().append('rect')
.attr
class: 'bar datapoint'
fill: (d,i) -> color_scale(i)
'stroke-width': 2
.on('mouseover', bar_on_mouseover)
.on('mouseout', bar_on_mouseout)
bars
.attr
x: (d,i) -> x(i)
y: (d) -> height-y(d)
width: (d) -> x.rangeBand()
height: (d) -> y(d)
bars.exit()
.remove()
bar_on_mouseover = (d,i) ->
bar_chart.dispatcher.focus(i)
bar_on_mouseout = (d,i) ->
bar_chart.dispatcher.blur(i)
bar_chart.focus = (i) ->
vis.select(".bar:nth-of-type(#{i+1})")
.classed('focus', true)
bar_chart.blur = (i) ->
vis.select(".bar:nth-of-type(#{i+1})")
.classed('focus', false)
// Generated by CoffeeScript 1.4.0
(function() {
var bar_on_mouseout, bar_on_mouseover, color_scale, height, vis, width, x, y;
window.bar_chart = {};
bar_chart.dispatcher = d3.dispatch('focus', 'blur');
vis = null;
width = null;
height = null;
x = d3.scale.ordinal();
y = d3.scale.linear();
color_scale = d3.scale.category10();
bar_chart.init = function(v, w, h) {
vis = v;
width = w;
height = h;
return y.range([0, height]);
};
bar_chart.update = function(data) {
var bars;
x.domain(d3.range(0, data.length)).rangeRoundBands([0, width], 0.015, 0);
y.domain([0, d3.max(data)]);
bars = vis.selectAll('.bar').data(data);
bars.enter().append('rect').attr({
"class": 'bar datapoint',
fill: function(d, i) {
return color_scale(i);
},
'stroke-width': 2
}).on('mouseover', bar_on_mouseover).on('mouseout', bar_on_mouseout);
bars.attr({
x: function(d, i) {
return x(i);
},
y: function(d) {
return height - y(d);
},
width: function(d) {
return x.rangeBand();
},
height: function(d) {
return y(d);
}
});
return bars.exit().remove();
};
bar_on_mouseover = function(d, i) {
return bar_chart.dispatcher.focus(i);
};
bar_on_mouseout = function(d, i) {
return bar_chart.dispatcher.blur(i);
};
bar_chart.focus = function(i) {
return vis.select(".bar:nth-of-type(" + (i + 1) + ")").classed('focus', true);
};
bar_chart.blur = function(i) {
return vis.select(".bar:nth-of-type(" + (i + 1) + ")").classed('focus', false);
};
}).call(this);
// Generated by CoffeeScript 1.4.0
(function() {
window.model = {};
model.data = [1, 2, 3, 4, 5];
}).call(this);
PAD = 20
BAR_H = 60
svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height
# Initialize views
bar_g = svg.append('g')
.attr
transform: "translate(#{PAD},#{height-BAR_H})"
bar.init(bar_g, width-2*PAD, BAR_H-PAD)
bar_chart_g = svg.append('g')
.attr
transform: "translate(#{PAD},#{PAD})"
bar_chart.init(bar_chart_g, width-2*PAD, height-BAR_H-2*PAD)
# Connect views to models
model.dispatcher.on 'statechange.bar', bar.update
model.dispatcher.on 'statechange.bar_chart', bar_chart.update
# Initialize models (also triggers the first update of views)
model.init()
## User Interaction
d3.select('body').on 'keydown', () ->
if d3.event.keyCode is 39
model.add_datapoint(Math.ceil(Math.random()*20))
else if d3.event.keyCode is 37
model.remove_datapoint()
# focus on a datapoint
focus = (args...) ->
bar_chart.focus(args...)
bar.focus(args...)
bar_chart.dispatcher.on 'focus', focus
bar.dispatcher.on 'focus', focus
# blur a datapoint
blur = (args...) ->
bar_chart.blur(args...)
bar.blur(args...)
bar_chart.dispatcher.on 'blur', blur
bar.dispatcher.on 'blur', blur
.bar, .subbar {
shape-rendering: crispEdges;
}
.subbar {
stroke: white;
stroke-width: 3px;
}
.datapoint.focus {
fill: black;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3.js dispatch</title>
<script src="http://d3js.org/d3.v3.min.js"></script>
<link rel="stylesheet" href="index.css">
</head>
<body>
<svg width="960px" height="500px"></svg>
<script src="model.js"></script>
<script src="bar_chart.js"></script>
<script src="bar.js"></script>
<script src="index.js"></script>
</body>
</html>
// Generated by CoffeeScript 1.4.0
(function() {
var BAR_H, PAD, bar_chart_g, bar_g, blur, focus, height, svg, width,
__slice = [].slice;
PAD = 20;
BAR_H = 60;
svg = d3.select('svg');
width = svg.node().getBoundingClientRect().width;
height = svg.node().getBoundingClientRect().height;
bar_g = svg.append('g').attr({
transform: "translate(" + PAD + "," + (height - BAR_H) + ")"
});
bar.init(bar_g, width - 2 * PAD, BAR_H - PAD);
bar_chart_g = svg.append('g').attr({
transform: "translate(" + PAD + "," + PAD + ")"
});
bar_chart.init(bar_chart_g, width - 2 * PAD, height - BAR_H - 2 * PAD);
model.dispatcher.on('statechange.bar', bar.update);
model.dispatcher.on('statechange.bar_chart', bar_chart.update);
model.init();
d3.select('body').on('keydown', function() {
if (d3.event.keyCode === 39) {
return model.add_datapoint(Math.ceil(Math.random() * 20));
} else if (d3.event.keyCode === 37) {
return model.remove_datapoint();
}
});
focus = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
bar_chart.focus.apply(bar_chart, args);
return bar.focus.apply(bar, args);
};
bar_chart.dispatcher.on('focus', focus);
bar.dispatcher.on('focus', focus);
blur = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
bar_chart.blur.apply(bar_chart, args);
return bar.blur.apply(bar, args);
};
bar_chart.dispatcher.on('blur', blur);
bar.dispatcher.on('blur', blur);
}).call(this);
window.model = {}
model.dispatcher = d3.dispatch('statechange')
data = null
model.init = () ->
data = [1,2,3,4,5]
model.dispatcher.statechange(data)
model.add_datapoint = (n) ->
data.push n
model.dispatcher.statechange(data)
model.remove_datapoint = (n) ->
data.pop()
model.dispatcher.statechange(data)
// Generated by CoffeeScript 1.4.0
(function() {
var data;
window.model = {};
model.dispatcher = d3.dispatch('statechange');
data = null;
model.init = function() {
data = [1, 2, 3, 4, 5];
return model.dispatcher.statechange(data);
};
model.add_datapoint = function(n) {
data.push(n);
return model.dispatcher.statechange(data);
};
model.remove_datapoint = function(n) {
data.pop();
return model.dispatcher.statechange(data);
};
}).call(this);
// Generated by CoffeeScript 1.4.0
(function() {
window.bar = {};
bar.init = function(group, width, height) {};
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment