Stacked-to-Grouped Bars Transition - d3.js

This example of a stacked-to-grouped bar transition is beautiful. However, it's functioning off of a random number generator and for the life of me, I cannot figure how to replace that with my own data set.
http://bl.ocks.org/mbostock/3943967
How do get this stacked-to-grouped bar transition to import and work with a .csv file instead of the random data generator?
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font: 14px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
form {
position: absolute;
right: 10px;
top: 10px;
}
</style>
<form>
<label><input type="radio" name="mode" value="grouped"> Grouped</label>
<label><input type="radio" name="mode" value="stacked" checked> Stacked</label>
</form>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 1000 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var stack = d3.layout.stack(),
layers = stack(d3.range(n).map(function() { return bumpLayer(m, .1); })),
yGroupMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y; }); }),
yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); });
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var y = d3.scale.linear()
.rangeRound([height, 0]);
var color = d3.scale.ordinal()
.range(["#aa0000", "#ffff66", "#99ff99", "#00aa00"]);
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(0)
.tickPadding(6)
.orient("bottom");
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("data.csv", function(error, data) {
color.domain(d3.keys(data[0]).filter(function(key) { return key !== "Year"; }));
data.forEach(function(d) {
var y0 = 0;
d.power = color.domain().map(function(name) { return {name: name, y0: y0, y1: y0 += +d[name]}; });
d.total = d.power[d.power.length - 1].y1;
});
data.sort(function(b, a) { return b.total - a.total; });
x.domain(data.map(function(d) { return d.Year; }));
y.domain([0, d3.max(data, function(d) { return d.total; })]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("y", -8)
.attr("x", 40)
.attr("dy", "0em")
.style("text-anchor", "end")
.text("Power (Mw)");
var year = svg.selectAll(".year")
.data(data)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) { return "translate(" + x(d.Year) + ",0)"; });
year.selectAll("rect")
.data(function(d) { return d.power; })
.enter().append("rect")
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.y1); })
.attr("height", function(d) { return y(d.y0) - y(d.y1); })
.style("fill", function(d) { return color(d.name); });
var legend = svg.selectAll(".legend")
.data(color.domain().slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 800)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 770)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "begin")
.text(function(d) { return d; });
});
rect.transition()
.delay(function(d, i) { return i * 10; })
.attr("y", function(d) { return y(d.y0 + d.y); })
.attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); });
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
d3.selectAll("input").on("change", change);
var timeout = setTimeout(function() {
d3.select("input[value=\"grouped\"]").property("checked", true).each(change);
}, 2000);
function change() {
clearTimeout(timeout);
if (this.value === "grouped") transitionGrouped();
else transitionStacked();
}
function transitionGrouped() {
y.domain([0, yGroupMax]);
rect.transition()
.duration(500)
.delay(function(d, i) { return i * 10; })
.attr("x", function(d, i, j) { return x(d.x) + x.rangeBand() / n * j; })
.attr("width", x.rangeBand() / n)
.transition()
.attr("y", function(d) { return y(d.y); })
.attr("height", function(d) { return height - y(d.y); });
}
function transitionStacked() {
y.domain([0, yStackMax]);
rect.transition()
.duration(500)
.delay(function(d, i) { return i * 10; })
.attr("y", function(d) { return y(d.y0 + d.y); })
.attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); })
.transition()
.attr("x", function(d) { return x(d.x); })
.attr("width", x.rangeBand());
}
</script>

I think you might be missing a level in your data. It looks like data in your example is an array of objects. But in Mike's example he's using an array of arrays of objects:
[
[{x:, y:}, {x:, y:},...],
[{x:, y:}, {x:, y:},...],
...
]
The first level in the array is the "layers", which represent the number of bars in each stack or group. There is a <g> element created per layer that will contain all 58 bars in that layer.
The second level in the array represents the bars themselves within each layer.
You are probably going to have a difficult time representing this structure as a CSV. You might be better off storing the data on the server as JSON. If you need to use CSV for some reason you'll need to figure out how to represent the inner array without using commas. One option is to add a column called "layer" and assign a layer index value to each row/object that can be used to turn the flat CSV data into a nested array on the client.

Old question, but been working on the same problem; hope this helps someone. Really just expanding on what Scott responded with below. Mike Bostock, this is some amazing work BTW.
1) Comment out the current layers array that is generated using the test generator functions (you can also delete/comment-out the test generator functions)
2) Here is a simple example of what a new nested layers array would look like:
layers = [
[
{"x":0,"y":1.5,"y0":0}
],
[
{"x":0,"y":1.5,"y0":1.5}
]
];
3) Whatever data you use, you still need to someone populate the n [# of layers/columns per sample period] and m [number of samples total OR number of periods] variables to match the array(s) that you loaded into layers
4) y0 governs the difference between the array[0] and array[1] for the stacking view - haven't quite figured out how function(d) returns the values needed for y0 so working around it with my source data for now
5) As you start adding new periods or what Mike calls "samples" in the comments, you group each "layer" in the separate period array (which is equal to the number of layers you are plotting). Notice how y0 is just the aggregation of all the prior y coordinates. Example for n = 4, m=2:
layers = [
[
{"x":0,"y":0.5,"y0":0},
{"x":1,"y":1.5,"y0":0}
],
[
{"x":0,"y":2.5,"y0":0.5},
{"x":1,"y":1.5,"y0":1.5}
],
[
{"x":0,"y":0.5,"y0":3.0},
{"x":1,"y":1.5,"y0":3.0}
],
[
{"x":0,"y":2.5,"y0":3.5},
{"x":1,"y":1.5,"y0":4.5}
]
];

Let us assume this is your json data:
[
{"a":"uno", "b":11, "c":21},
{"a":"duo", "b":12, "c":22},
{"a":"tre", "b":13, "c":23}
]
The structure of array layers is then as follows:
[
[
{x:"uno", y:11, y0:0},
{x:"duo", y:12, y0:0},
{x:"tre", y:13, y0:0}
],
[
{x:"uno", y:21, y0:11},
{x:"duo", y:22, y0:12},
{x:"tre", y:23, y0:13}
]
}
The two inner arrays correspond to the two layers in the graph.
To properly transform the data from json to stack, you need to go layer by layer. It the code below, cats is an array containing the names of the groups or categories ["uno", "duo", "tre"] and m is their number(3); vals is an array of layer labels ["b", "c"] and n is their number (2). The function readLayer gets called twice, once per layer.
var stack = d3.layout.stack(),
layers = stack(d3.range(n).map(function(i) { return readLayer(m,i);})),
yGroupMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y;});}),
yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y;});});
function readLayer(m,r){
var a = [], i;
for (i = 0; i < m; ++i) { a[i] = data[i][vals[r]]; }
return a.map(function(d, i) { return {x: cats[i], y: a[i]};});
};
Please note that the value y0 is not returned by readLayer; it gets created in the stack function.

Related

Smooth scrolling brush with scaleband like string labels [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 3 years ago.
Improve this question
The problem: Using scaleband does not pan smoothly using brush.
https://shanegibney.github.io/D3-v4-Bar-chart-Scaleband-with-Brush/
Here is an example of smooth panning with dates on x-axis.
https://shanegibney.github.io/d3-Bar-Chart-Pan-And-Zoom/
But this only works when the x-domain is continuous.
Using a linearscale, I would like to replace the values on the x-axis with the string in data d.name. The issue is that when zooming new ticks are created in between the value. For example the values 1 and 2 when zoomed will produce values and tciks for 1, 1.5, 2 and 2.5 etc. This means that the names do not match up anymore.
This could be solved with two things;
Prevent new ticks appearing when zooming in. I don't know how to do this.
Replace each label on the x-axis ticks with d.name from the data
These are my attempts to do this,
var xAxis = d3.axisBottom(x)
.tickFormat(function(d, i) {
label = data[i].name;
local.set(this, data[i].name);
return "test";
return data[i].name;
return label;
return i;
return d;
});
None of which work.
The Plunker
https://plnkr.co/edit/zbWu6AV8MNO9N8m0byre
Inside the tickFormat function, check if the numeric tick corresponds to any value in the data array. If it does, return the name, otherwise return nothing:
var xAxis = d3.axisBottom(x)
.tickFormat(function(d) {
var found = data.find(function(e){
return e.num === d
});
return found ? found.name : null;
})
Here is the code with that change:
<!DOCTYPE html>
<meta charset="utf-8">
<style type="text/css">
body {
font-family: avenir next, sans-serif;
font-size: 12px;
}
.zoom {
cursor: move;
fill: none;
pointer-events: all;
}
.axis {
stroke-width: 0.5px;
stroke: #888;
font: 10px avenir next, sans-serif;
}
.axis>path {
stroke: #888;
}
</style>
<body>
<div id="totalDistance">
</div>
</body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
/* Adapted from: https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172 */
var data = [{
"name": "A",
"date": "2016-11-09 11:15",
"num": "1",
"distance": "1900"
},
{
"name": "B",
"date": "2016-11-10 10:40",
"num": "2",
"distance": "1500"
},
{
"name": "C",
"date": "2016-11-11 16:45",
"num": "3",
"distance": "2500"
},
{
"name": "D",
"date": "2016-11-12 12:48",
"num": "4",
"distance": "2300"
},
{
"name": "E",
"date": "2016-11-15 20:00",
"num": "5",
"distance": "2000"
}
];
var margin = {
top: 20,
right: 20,
bottom: 90,
left: 50
},
margin2 = {
top: 230,
right: 20,
bottom: 30,
left: 50
},
width = 960 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom,
height2 = 300 - margin2.top - margin2.bottom;
var parseTime = d3.timeParse("%Y-%m-%d %H:%M");
var local = d3.local();
var x = d3.scaleLinear().range([0, width]),
x2 = d3.scaleLinear().range([0, width]),
y = d3.scaleLinear().range([height, 0]),
y2 = d3.scaleLinear().range([height2, 0]);
// dur = d3.scaleLinear().range([0, 12]);
var tickLabels = ['a', 'b', 'c', 'd'];
var newData = [];
data.forEach(function(e) {
newData.push({
"name": e.name
});
});
//var xAxis = d3.axisBottom(x).tickSize(0);
var xAxis2 = d3.axisBottom(x2).tickSize(0)
.tickFormat(function(d) {
var found = data.find(function(e) {
return e.num === d
});
return found ? found.name : null;
})
yAxis = d3.axisLeft(y).tickSize(0);
var xAxis = d3.axisBottom(x)
.tickFormat(function(d) {
var found = data.find(function(e) {
return e.num === d
});
if (!found) {
d3.select(this.parentNode).select("line").remove()
}
return found ? found.name : null;
})
// var xAxis = d3.axisBottom(x)
// .tickFormat(function(d, i) {
// label = data[i].name;
// local.set(this, data[i].name);
// return "test";
// return data[i].name;
// return label;
// return i;
// return d;
// });
var brush = d3.brushX()
.extent([
[0, 0],
[width, height2]
])
.on("start brush end", brushed);
var zoom = d3.zoom()
.scaleExtent([1, 10])
.translateExtent([
[0, 0],
[width, height]
])
.extent([
[0, 0],
[width, height]
])
.on("zoom", zoomed);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var focus = svg.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var context = svg.append("g")
.attr("class", "context")
.attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
// d3.json("data.json", function(error, data) {
// if (error) throw error;
var parseTime = d3.timeParse("%Y-%m-%d %H:%M");
var mouseoverTime = d3.timeFormat("%a %e %b %Y %H:%M");
var minTime = d3.timeFormat("%b%e, %Y");
var parseDate = d3.timeParse("%b %Y");
data.forEach(function(d) {
d.distance = +d.distance;
d.num = +d.num;
return d;
},
function(error, data) {
if (error) throw error;
});
var total = 0;
data.forEach(function(d) {
total = d.distance + total;
});
var minDate = d3.min(data, function(d) {
return d.num;
});
var xMin = d3.min(data, function(d) {
return d.num;
});
var yMax = Math.max(20, d3.max(data, function(d) {
return d.distance;
}));
x.domain([xMin, d3.max(data, function(d) {
return d.num;
})]);
y.domain([0, yMax]);
x2.domain(x.domain());
y2.domain(y.domain());
var rects = focus.append("g");
rects.attr("clip-path", "url(#clip)");
rects.selectAll("rects")
.data(data)
.enter().append("rect")
.style("fill", function(d) {
return "#8DA5ED";
})
.attr("class", "rects")
.attr("x", function(d) {
return x(d.num);
})
.attr("y", function(d) {
return y(d.distance);
})
.attr("width", function(d) {
return 10;
})
.attr("height", function(d) {
return height - y(d.distance);
});
focus.append("g")
.attr("class", "axis x-axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
focus.append("g")
.attr("class", "axis axis--y")
.call(yAxis);
// Summary Stats
focus.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x", 0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Distance in meters");
svg.append("text")
.attr("transform",
"translate(" + ((width + margin.right + margin.left) / 2) + " ," +
(height + margin.top + margin.bottom) + ")")
.style("text-anchor", "middle")
.text("Date");
svg.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
var rects = context.append("g");
rects.attr("clip-path", "url(#clip)");
rects.selectAll("rects")
.data(data)
.enter().append("rect")
.style("fill", function(d) {
return "#8DA5ED";
})
.attr("class", "rects")
.attr("x", function(d) {
return x2(d.num);
})
.attr("y", function(d) {
return y2(d.distance);
})
.attr("width", function(d) {
return 10;
})
.attr("height", function(d) {
return height2 - y2(d.distance);
});
context.append("g")
.attr("class", "axis x-axis")
.attr("transform", "translate(0," + height2 + ")")
.call(xAxis2);
context.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, x.range());
// });
function brushed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
var s = d3.event.selection || x2.range();
x.domain(s.map(x2.invert, x2));
focus.selectAll(".rects")
.attr("x", function(d) {
return x(d.num);
})
.attr("y", function(d) {
return y(d.distance);
})
.attr("width", function(d) {
return 10;
})
.attr("height", function(d) {
return height - y(d.distance);
});
focus.select(".x-axis").call(xAxis);
svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
.scale(width / (s[1] - s[0]))
.translate(-s[0], 0));
var e = d3.event.selection;
var selectedrects = focus.selectAll('.rects').filter(function() {
var xValue = this.getAttribute('x');
return e[0] <= xValue && xValue <= e[1];
});
}
function zoomed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
var t = d3.event.transform;
x.domain(t.rescaleX(x2).domain());
focus.selectAll(".rects")
.attr("x", function(d) {
return x(d.num);
})
.attr("y", function(d) {
return y(d.distance);
})
.attr("width", function(d) {
return 10;
})
.attr("height", function(d) {
return height - y(d.distance);
});
focus.select(".x-axis").call(xAxis);
context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
}
</script>
PS: this is clearly a XY Problem: disguising a linear scale as an categorical scale, as you're doing here, may not be the correct solution. Instead of that, you should find out how to brush the band scale itself.

How to assign id to the bar chart

How can I assign id base on its name?
the link of the screenshot of the console is down below.
Thanks
serie.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("class","bar")
.attr("x", function(d) { return x(d.data.Company); })
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return y(d[0]) - y(d[1]); })
.attr("width", x.bandwidth())
.attr("id",function(d,i){
return "id"+ d.name;
})
I discourage assigning ids to every bar as ids need to be unique. Its better to have a class instead. But as requested, I have posted a fiddle below. Hover on the bars to get the id. And you also said you need the id for animating so I've also added some animations when the chart loads (If that's what you meant by animation).
// Setup svg using Bostock's margin convention
var margin = {
top: 20,
right: 160,
bottom: 35,
left: 30
};
var width = 700 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
/* Data in strings like it would be if imported from a csv */
var data = [{
mystate: "Intuit",
name: "Asian",
y0: 53,
y1: 1824,
value: "1771"
}, {
mystate: "Intuit",
name: "Latino",
y0: 2707,
y1: 1231,
value: "1771"
}, {
mystate: "Intuit",
name: "Black_or_African_American",
y0: 2060,
y1: 1824,
value: "1771"
}, {
mystate: "Intuit",
name: "Caucasian",
y0: 355,
y1: 1024,
value: "1771"
}];
// Transpose the data into layers
var dataset = d3.layout.stack()(['y0', 'y1'].map(function(values) {
return data.map(function(d) {
return {
x: d.name,
y: +d[values],
};
});
}));
// Set x, y and colors
var x = d3.scale.ordinal()
.domain(dataset[0].map(function(d) {
return d.x;
}))
.rangeRoundBands([10, width - 10], 0.02);
var y = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) {
return d3.max(d, function(d) {
return d.y0 + d.y;
});
})])
.range([height, 0]);
var colors = ["b33040", "#d25c4d", "#f2b447", "#d9d574"];
// Define and draw axes
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.tickSize(-width, 0, 0)
.tickFormat(function(d) {
return d
});
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Create groups for each series, rects for each segment
var groups = svg.selectAll("g.cost")
.data(dataset)
.enter().append("g")
.attr("class", "cost")
.style("fill", function(d, i) {
return colors[i];
});
var rect = groups.selectAll("rect")
.data(function(d) {
return d;
})
.enter()
.append("rect")
.attr("x", function(d) {
return x(d.x);
})
.attr("y", function(d) {
return height;
})
.attr("height", function(d) {
return 0;
})
.attr("width", x.rangeBand())
.attr('id', function(d) {
return 'id' + d.x;
})
.on("mouseover", function() {
console.log(d3.select(this).attr('id'));
tooltip.style("display", null);
})
.on("mouseout", function() {
tooltip.style("display", "none");
})
.on("mousemove", function(d) {
var xPosition = d3.mouse(this)[0] - 15;
var yPosition = d3.mouse(this)[1] - 25;
tooltip.attr("transform", "translate(" + xPosition + "," + yPosition + ")");
tooltip.select("text").text(d.y);
}).transition().duration(1000)
.delay(function(d, i) {
return i * 300;
})
.attr("height", function(d) {
return y(d.y0) - y(d.y0 + d.y);
}).attr("y", function(d) {
return y(d.y0 + d.y);
});
// Prep the tooltip bits, initial display is hidden
var tooltip = svg.append("g")
.attr("class", "tooltip")
.style("display", "none");
tooltip.append("rect")
.attr("width", 30)
.attr("height", 20)
.attr("fill", "white")
.style("opacity", 0.5);
tooltip.append("text")
.attr("x", 15)
.attr("dy", "1.2em")
.style("text-anchor", "middle")
.attr("font-size", "12px")
.attr("font-weight", "bold");
svg {
font: 10px sans-serif;
shape-rendering: crispEdges;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
}
path.domain {
stroke: none;
}
.y .tick line {
stroke: #ddd;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.3.13/d3.min.js"></script>
Now coming back to your code, when you transform the data by using d3.layout.stack() there is no property called name in your new data (I suppose) and hence your ids are idundefined

Resize the SVGs according to windows size

Here's how's the D3.js look currently
What I want to achieve is that, when I resize the windows four tables inside needs resize accordingly. No new tables are added.
Currently it just keep adding new tables inside. How to correct this behavior?
The content of 1.json
[
[[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]],
[[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]],
[[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]],
[[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]]
]
The content of D3.js:
<!DOCTYPE html>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.8/d3.min.js" type="text/JavaScript"></script>
<!--script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.9/d3.js" type="text/JavaScript"></script-->
<style>
rect {
stroke: #9A8B7A;
stroke-width: 1px;
fill: #CF7D1C;
}
svg{
width: 50%;
height: 50%;
}
</style>
<body>
</body>
<script>
function draw(){
d3.json("array_data/1.json", function(data){
for (i=0; i<data.length; ++i) {
main(data[i]);
}
})
}
function main(dataset){
var local = d3.local();
var svg = d3.select("body").append("svg"),
bBox = svg.node().getBoundingClientRect(),
margin = {top: 20, right: 20, bottom: 30, left: 40},
width = bBox.width - margin.left - margin.right,
height = bBox.height - margin.top - margin.bottom;
var x = d3.scaleBand().rangeRound([0, width]);
var y = d3.scaleBand().rangeRound([0, height]);
y.domain(dataset.map(function(d,i) { return i; }));
var maxChildLength= d3.max(dataset, function(d) { return d.length; });
var xArr=Array.apply(null, {length: maxChildLength}).map(Function.call, Number);
x.domain(xArr);
var maxNum = d3.max(dataset, function(array) {
return d3.max(array);
});
var color=d3.scaleLinear().domain([0,maxNum]).range([0,1]);
svg.append("g")
.selectAll("g")
.data(dataset)//use top-level data to join g
.enter()
.append("g")
.selectAll("rect")
.data(function(d, i) {//for each <g>, use the second-level data (return d) to join rect
local.set(this, i);//this is the <g> parent
return d;
})
.enter()
.append("rect")
.attr("x", function(d, i, j) {
// return (i * 20) + 40;
return x(i);
})
.attr("y", function(d) {
// return (local.get(this) * 20) + 40;
return y(local.get(this));
})
//.attr("width",20)
.attr("width", x.bandwidth())
.attr("height", y.bandwidth())
.attr("fill-opacity",function(d){console.log(color(+d));return color(+d);})
svg.append("g")
.selectAll("g")
.data(dataset)
.enter()
.append("g")
.selectAll("text")
.data(function(d, i) {
local.set(this, i)
return d;
})
.enter()
.append("text")
.text(function(d, i, j) {
return d;
})
.attr("x", function(d, i, j) {
// return (i * 20) + 40;
return x(i);
})
.attr("y", function(d) {
return y(local.get(this));
//return (local.get(this) * 20) + 40;
})
.attr("dx", x.bandwidth()/2)
.attr("dy", y.bandwidth()/2)
.attr("dominant-baseline", "central")//vertical - http://bl.ocks.org/eweitnauer/7325338
.attr("text-anchor", "middle")//horizontal - https://bl.ocks.org/emmasaunders/0016ee0a2cab25a643ee9bd4855d3464
.attr("font-family", "sans-serif")
.attr("font-size", "20px");
svg.append("g")
.append("text")
.attr("x", width/2)
.attr("y", height)
.attr("dominant-baseline", "text-before-edge")
.style("text-anchor", "middle")
//.attr("transform", "translate("+width/2+"," + height+ ")")
.text("Units sold");
}
draw();
window.addEventListener("resize", draw);
</script>
Using your method, you'll need to clear out the HTML first, then redraw. So, at the beginning of your main() function:
var element = d3.select('body');
element.innerHTML = '';
svg = d3.select(element).append('svg');
There are other methods for resizing (viewport, having a resize function), but this fits your code as it exists now.

Making a grouped bar chart when my groups are varied sizes?

I am trying to make a bar chart out of some grouped data. This is dummy data, but the structure is basically the same. The data: election results includes a bunch of candidates, organized into the districts they were running in, and the total vote count:
district,candidate,votes
Dist 1,Leticia Putte,3580
Dist 2,David Barron,1620
Dist 2,John Higginson,339
Dist 2,Walter Bannister,2866
[...]
I'd like to create a bar or column chart (either, honestly, though my end goal is horizontal) that groups the candidates by district.
Mike Bostock has an excellent demo but I'm having trouble translating it intelligently for my purposes. I started to tease it out at https://jsfiddle.net/97ur6cwt/6/ but my data is organized somewhat differently -- instead of rows, by group, I have a column that sets the category. And there might be just one candidate or there might be a few candidates.
Can I group items if the groups aren't the same size?
My answer is similar to #GerardoFurtado but instead I use a d3.nest to build a domain per district. This removes the need for hardcoding values and cleans it up a bit:
y0.domain(data.map(function(d) { return d.district; }));
var districtD = d3.nest()
.key(function(d) { return d.district; })
.rollup(function(d){
return d3.scale.ordinal()
.domain(d.map(function(c){return c.candidate}))
.rangeRoundBands([0, y0.rangeBand()], pad);
}).map(data);
districtD becomes a map of domains for your y-axis which you use when placing the rects:
svg.selectAll("bar")
.data(data)
.enter().append("rect")
.style("fill", function(d,i) {
return color(d.district);
})
.attr("x", 0)
.attr("y", function(d) { return y0(d.district) + districtD[d.district](d.candidate); })
.attr("height", function(d){
return districtD[d.district].rangeBand();
})
.attr("width", function(d) {
return x(d.votes);
});
I'm off to a meeting but the next step is to clean up the axis and get the candidate names on there.
Full running code:
var url = "https://gist.githubusercontent.com/amandabee/edf73bc0bbe131435c952f5ed47524a6/raw/99febb9971f76e36af06f1b99913fcaa645ecb3e/election.csv"
var m = {top: 10, right: 10, bottom: 50, left: 110},
w = 800 - m.left - m.right,
h = 500 - m.top - m.bottom,
pad = .1;
var x = d3.scale.linear().range([0, w]);
y0 = d3.scale.ordinal().rangeRoundBands([0, h], pad);
var color = d3.scale.category20c();
var yAxis = d3.svg.axis()
.scale(y0)
.orient("left");
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5)
.tickFormat(d3.format("$,.0f"));
var svg = d3.select("#chart").append("svg")
.attr("width", w + m.right + m.left + 100)
.attr("height", h + m.top + m.bottom)
.append("g")
.attr("transform",
"translate(" + m.left + "," + m.top + ")");
// This moves the SVG over by m.left(110)
// and down by m.top (10)
d3.csv(url, function(error, data) {
data.forEach(function(d) {
d.votes = +d.votes;
});
y0.domain(data.map(function(d) { return d.district; }));
districtD = d3.nest()
.key(function(d) { return d.district; })
.rollup(function(d){
console.log(d);
return d3.scale.ordinal()
.domain(d.map(function(c){return c.candidate}))
.rangeRoundBands([0, y0.rangeBand()], pad);
})
.map(data);
x.domain([0, d3.max(data, function(d) {
return d.votes;
})]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "middle");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text");
svg.selectAll("bar")
.data(data)
.enter().append("rect")
.style("fill", function(d,i) {
return color(d.district);
})
.attr("x", 0)
.attr("y", function(d) { return y0(d.district) + districtD[d.district](d.candidate); })
.attr("height", function(d){
return districtD[d.district].rangeBand();
})
.attr("width", function(d) {
return x(d.votes);
});
svg.selectAll(".label")
.data(data)
.enter().append("text")
.text(function(d) {
return (d.votes);
})
.attr("text-anchor", "start")
.attr("x", function(d) { return x(d.votes)})
.attr("y", function(d) { return y0(d.district) + districtD[d.district](d.candidate) + districtD[d.district].rangeBand()/2;})
.attr("class", "axis");
});
.axis {
font: 10px sans-serif;
}
.axis path, .axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="chart"></div>
An alternate version which sizes the bars the same and scales the outer domain appropriately:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<style>
.label {
font: 10px sans-serif;
}
.axis {
font: 11px sans-serif;
font-weight: bold;
}
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<div id="chart"></div>
<script>
var url = "https://gist.githubusercontent.com/amandabee/edf73bc0bbe131435c952f5ed47524a6/raw/99febb9971f76e36af06f1b99913fcaa645ecb3e/election.csv"
var m = {
top: 10,
right: 10,
bottom: 50,
left: 110
},
w = 800 - m.left - m.right,
h = 500 - m.top - m.bottom,
pad = .1, padPixel = 5;
var x = d3.scale.linear().range([0, w]);
var y0 = d3.scale.ordinal();
var color = d3.scale.category20c();
var yAxis = d3.svg.axis()
.scale(y0)
.orient("left");
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5)
.tickFormat(d3.format("$,.0f"));
var svg = d3.select("#chart").append("svg")
.attr("width", w + m.right + m.left + 100)
.attr("height", h + m.top + m.bottom)
.append("g")
.attr("transform",
"translate(" + m.left + "," + m.top + ")");
// This moves the SVG over by m.left(110)
// and down by m.top (10)
d3.csv(url, function(error, data) {
data.forEach(function(d) {
d.votes = +d.votes;
});
var barHeight = h / data.length;
y0.domain(data.map(function(d) {
return d.district;
}));
var y0Range = [0];
districtD = d3.nest()
.key(function(d) {
return d.district;
})
.rollup(function(d) {
var barSpace = (barHeight * d.length);
y0Range.push(y0Range[y0Range.length - 1] + barSpace);
return d3.scale.ordinal()
.domain(d.map(function(c) {
return c.candidate
}))
.rangeRoundBands([0, barSpace], pad);
})
.map(data);
y0.range(y0Range);
x.domain([0, d3.max(data, function(d) {
return d.votes;
})]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "middle");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text");
svg.selectAll("bar")
.data(data)
.enter().append("rect")
.style("fill", function(d, i) {
return color(d.district);
})
.attr("x", 0)
.attr("y", function(d) {
return y0(d.district) + districtD[d.district](d.candidate);
})
.attr("height", function(d) {
return districtD[d.district].rangeBand();
})
.attr("width", function(d) {
return x(d.votes);
});
var ls = svg.selectAll(".labels")
.data(data)
.enter().append("g");
ls.append("text")
.text(function(d) {
return (d.votes);
})
.attr("text-anchor", "start")
.attr("x", function(d) {
return x(d.votes)
})
.attr("y", function(d) {
return y0(d.district) + districtD[d.district](d.candidate) + districtD[d.district].rangeBand() / 2;
})
.attr("class", "label");
ls.append("text")
.text(function(d) {
return (d.candidate);
})
.attr("text-anchor", "end")
.attr("x", -2)
.attr("y", function(d) {
return y0(d.district) + districtD[d.district](d.candidate) + districtD[d.district].rangeBand() / 2;
})
.style("alignment-baseline", "middle")
.attr("class", "label");
});
</script>
</body>
</html>
This is a partial solution: https://jsfiddle.net/hb13oe4v/
The main problem here is creating a scale for each group with a variable domain. Unlike Bostock's example, you don't have the same amount of bars(candidates) for each group(districts).
So, I had to do a workaround. First, I nested the data in the most trivial way:
var nested = d3.nest()
.key(function(d) { return d.district; })
.entries(data);
And then created the groups accordingly:
var district = svg.selectAll(".district")
.data(nested)
.enter()
.append("g")
.attr("transform", function(d) { return "translate(0," + y(d.key) + ")"; });
As I couldn't create an y1 (x1 in Bostock's example) scale, I had to hardcode the height of the bars (which is inherently bad). Also, for centring the bars in each group, I created this crazy math, that puts one bar in the center, the next under, the next above, the next under and so on:
.attr("y", function(d, i) {
if( i % 2 == 0){ return (y.rangeBand()/2 - 10) + (i/2 + 0.5) * 10}
else { return (y.rangeBand()/2 - 10) - (i/2) * 10}
})
Of course, all this can be avoided and coded way more elegantly if we could set a variable scale for each group.

Adding traces to d3.js animated bubble chart

I'm trying to build an animated time series chart which shows a 'trace' or snail trail following the moving dot. I have been trying to integrate KoGor's http://bl.ocks.org/KoGor/8163022 but haven't had luck- I think the problem lies in tweenDash() - The original function was designed for a single trace- this one has one per company.
Attached below is a working example- the time series scrubbing and movable data labels work, just not the trace aspect.
Thanks,
RL
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.10/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<body bgcolor="#000000">
<title>BPS</title>
<style>
#import url(style.css);
#chart {
margin-left: -40px;
height: 506px;
display:inline;
}
#buffer {
width: 100px;
height:506px;
float:left;
}
text {
font: 10px sans-serif;
color: #ffffff;
}
.dot {
stroke: #000;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.label {
fill: #777;
}
.year.label {
font: 900 125px "Helvetica Neue";
fill: #ddd;
}
.year.label.active {
fill: #aaa;
}
.overlay {
fill: none;
pointer-events: all;
cursor: ew-resize;
}
</style>
<div>
<div id="buffer"></div><div id="chart"></div>
</div>
<script src="d3.v3.min.js"></script>
<script>
var source = '[{"name":"ABCD","AUM":[[2010,1000.6],[2011,1200.6],[2012,1300.1],[2013,1400.5],[2014,1600.0]],"AUA":[[2010,3000.6],[2011,3300.2],[2012,4000.0],[2013,4500.8],[2014,6000.3]],"marketPercentage":[[2010,40.4],[2011,39.7],[2012,38.5],[2013,37.1],[2014,36.5]],"fill":[[2010,0],[2011,-1],[2012,-1],[2013,-1],[2014,-1]],"xOffset":[[2010,5],[2011,5],[2012,5],[2013,5],[2014,5]],"yOffset":[[2010,-30],[2011,-20],[2012,-20],[2013,-20],[2014,-10]]},{"name":"EFGH","AUM":[[2010,32.8],[2011,43.2],[2012,58.3],[2013,78.8],[2014,92]],"AUA":[[2010,327.3],[2011,439.3],[2012,547.0],[2013,710.0],[2014,824.0]],"marketPercentage":[[2010,1.0],[2011,1.2],[2012,1.5],[2013,1.8],[2014,1.9]],"fill":[[2010,0],[2011,1],[2012,1],[2013,1],[2014,1]],"xOffset":[[2010,5],[2011,5],[2012,5],[2013,5],[2014,5]],"yOffset":[[2010,-10],[2011,-10],[2012,-10],[2013,-10],[2014,-10]]},{"name":"HIJK","AUM":[[2010,0.1],[2011,0.5],[2012,1.2],[2013,2.4],[2014,2.6]],"AUA":[[2010,159.6],[2011,176.7],[2012,199.9],[2013,235.1],[2014,269.0]],"marketPercentage":[[2010,0.1],[2011,0.1],[2012,0.1],[2013,0.1],[2014,0.1]],"fill":[[2010,0],[2011,0],[2012,0],[2013,1],[2014,1]],"xOffset":[[2010,5],[2011,5],[2012,5],[2013,5],[2014,5]],"yOffset":[[2010,-10],[2011,-10],[2012,-10],[2013,-10],[2014,-10]]}]';
// Various accessors that specify the four dimensions of data to visualize.
function x(d) { return d.AUM; }
function y(d) { return d.AUA; }
function xo(d) {return d.xOffset; }
function yo(d) {return d.yOffset; }
function radius(d) { return d.marketPercentage; }
function key(d) { return d.name; }
// Chart dimensions.
var margin = {top: 19.5, right: 19.5, bottom: 19.5, left: 39.5},
width = 960 - margin.right,
height = 500 - margin.top - margin.bottom;
// Various scales. These domains make assumptions of data, naturally.
var xScale = d3.scale.linear().domain([0, 2000]).range([0, width]),
yScale = d3.scale.linear().domain([0, 5000]).range([height, 0]),
radiusScale = d3.scale.sqrt().domain([0, 500]).range([0, 40]),
colorScale = d3.scale.category10();
// The x & y axes.
var xAxis = d3.svg.axis().orient("bottom").scale(xScale).ticks(12, d3.format(",d")),
yAxis = d3.svg.axis().scale(yScale).orient("left");
// Create the SVG container and set the origin.
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Add the x-axis.
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.style("fill", "#FFFFFF")
.call(xAxis);
// Add the y-axis.
svg.append("g")
.attr("class", "y axis")
.style("fill", "#FFFFFF")
.call(yAxis);
// Add an x-axis label.
svg.append("text")
.attr("class", "x label")
.attr("text-anchor", "end")
.style("fill", "#FFFFFF")
.attr("x", width)
.attr("y", height - 6);
//.text("income per capita, inflation-adjusted (dollars)");
// Add a y-axis label.
svg.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.attr("y", 6)
.attr("dy", ".75em")
.style("fill", "#FFFFFF")
.attr("transform", "rotate(-90)")
// .text("life expectancy (years)")
;
// Add the year label; the value is set on transition.
var label = svg.append("text")
.attr("class", "year label")
.attr("text-anchor", "end")
.attr("y", height - 24)
.attr("x", width)
.text(2010);
//d3.json("investments_v04ANON.json", function(companies) {
companies = JSON.parse(source)
// A bisector since many company's data is sparsely-defined.
var bisect = d3.bisector(function(d) { return d[0]; });
// Add a dot per company. Initialize the data at 2010, and set the colors.
var dot = svg.append("g")
.attr("class", "dots")
.selectAll(".dot")
.data(interpolateData(2010))
.enter().append("circle")
.attr("class", "dot")
// .style("fill", function(d) { return colorScale(color(d)); })
.style("fill", function(d) {return colorScale(interpolateData(2010)) })
.call(position)
.sort(order);
var lineTraces = svg.append("path")
.attr("class", "lineTrace")
.selectAll(".traces")
.attr("stroke-width", 2)
.attr("stroke", "grey")
.data(interpolateData(2010));
//yields a mouseover label - "title" precludes need for separate mouseover event.
// dot.append("title")
// .text(function(d) { return d.name; });
//.text(function(d) {return d.AUM});
var theLabel = svg.append("g")
.attr("class", "texts")
.selectAll(".theLabel")
.data(interpolateData(2010))
.enter().append("text")
.attr("class", "text")
.text("hey")
.call(position2);
// Add an overlay for the year label.
var box = label.node().getBBox();
var overlay = svg.append("rect")
.attr("class", "overlay")
.attr("x", box.x)
.attr("y", box.y)
.attr("width", box.width)
.attr("height", box.height)
.on("mouseover", enableInteraction);
// Start a transition that interpolates the data based on year.
svg.transition()
.duration(30000)
.ease("linear")
.tween("year", tweenYear)
.attrTween("stroke-dasharray", tweenDash)
.each("end", enableInteraction);
// Positions the dots based on data.
function position(dot) {
dot .attr("cx", function(d) { return xScale(x(d)); })
.attr("cy", function(d) { return yScale(y(d)); })
.attr("r", function(d) { return radiusScale(radius(d)); })
.style("fill", function(d) {return d.fill>0 ? "green" : "red"} );//{return d.fill});
}
//function from: http://bl.ocks.org/KoGor/8163022
function tweenDash() {
var i = d3.interpolateString("0," + 5, 5 + "," + 5); // interpolation of stroke-dasharray style attr
// var l = path.node().getTotalLength();
// var i = d3.interpolateString("0," + l, l + "," + l); // interpolation of stroke-dasharray style attr
return function(t) {
var marker = d3.select(".dots");
// var p = path.node().getPointAtLength(t * l);
var p = lineTraces.node().getPointAtLength(t * 5);
marker.attr("transform", "translate(" + p.x + "," + p.y + ")");//move marker
return i(t);
}
}
function position2(theLabel) {
theLabel.attr("x", function(d) { return xScale(x(d)) + xo(d); })
.attr("y", function(d) { return yScale(y(d)) + yo(d); })
.attr("text-anchor", "end")
.style("fill", "#FFFFFF")
.text(function(d) { return d.name + ": AUM:" + Math.round(d.AUM) + ", AUA: " + Math.round(d.AUA) });//{return d.fill});
}
// Defines a sort order so that the smallest dots are drawn on top.
function order(a, b) {
return radius(b) - radius(a);
}
// After the transition finishes, you can mouseover to change the year.
function enableInteraction() {
var yearScale = d3.scale.linear()
.domain([2010, 2014])
.range([box.x + 10, box.x + box.width - 10])
.clamp(true);
// Cancel the current transition, if any.
svg.transition().duration(0);
overlay
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("mousemove", mousemove)
.on("touchmove", mousemove);
function mouseover() {
label.classed("active", true);
}
function mouseout() {
label.classed("active", true);
label.classed("active", false);
}
function mousemove() {
displayYear(yearScale.invert(d3.mouse(this)[0]));
}
}
// Tweens the entire chart by first tweening the year, and then the data.
// For the interpolated data, the dots and label are redrawn.
function tweenYear() {
var year = d3.interpolateNumber(2010, 2014);
return function(t) { displayYear(year(t)); };
}
// Updates the display to show the specified year.
function displayYear(year) {
dot.data(interpolateData(year), key).call(position).sort(order);
theLabel.data(interpolateData(year), key).call(position2).sort(order);
label.text(Math.round(year));
}
// Interpolates the dataset for the given (fractional) year.
function interpolateData(year) {
return companies.map(function(d) {
return {
// name: d.name + ": AUM:" + interpolateValues(d.AUM, year) + ", AUA: " + interpolateValues(d.AUA, year),
// name: d.name + ": AUM:" + d.AUM + ", AUA: " + d.AUA,
// name: interpolateValues(d.AUM, year),
name: d.name,
AUM: interpolateValues(d.AUM, year),
marketPercentage: interpolateValues(d.marketPercentage, year),
AUA: interpolateValues(d.AUA, year),
fill: interpolateValues(d.fill, year),
xOffset: interpolateValues(d.xOffset, year),
yOffset: interpolateValues(d.yOffset, year)
};
});
}
// Finds (and possibly interpolates) the value for the specified year.
function interpolateValues(values, year) {
var i = bisect.left(values, year, 0, values.length - 1),
a = values[i];
if (i > 0) {
var b = values[i - 1],
t = (year - a[0]) / (b[0] - a[0]);
return a[1] * (1 - t) + b[1] * t;
}
return a[1];
};
//});
</script>
Mark- the second version you built works very well. I'm now trying to address the individual line segments. I've added an attribute 'toggleSwitch' but the below code runs 1x and captures only the initial state of the object.
var lineTraces = svg.append("g")
.selectAll(".traces")
.data([0,1,2,4,5,6,7,8,9,10,11,12])
.enter()
.append("path")
.attr("stroke-width", 2)
.attr("stroke", "grey")
.attr("class", "lineTrace")
.attr("d", line)
.each(function(d,i){
d3.select(this)
.datum([someData[i]])
.attr("nothing", function(i) {console.log(i[0])})
.attr("d", line)
.style("stroke-dasharray", function(i) {return (i[0]["toggleSwitch"]<0 ? "0,0": "3,3")})
});
console log, one per object:
Object { name: "TheName", Impact: 120, bubbleSize: 30.4, YoY: 11, toggleSwitch: 0, xOffset: 5, yOffset: -30 }
The example you linked to had a pre-established path and then attrTweened the "stroke-dasharray" on it. Your first problem is that you need to establish that path for each company. Then you can tween it.
// set up a line to create the path
var line = d3.svg.line()
.x(function(d) { return xScale(x(d)); })
.y(function(d) { return yScale(y(d)); })
.interpolate("basis");
// for each company add the path
var lineTraces = svg.append("g")
.selectAll(".traces")
.attr("fill","red")
.data([0,1,2]) // 3 companies
.enter()
.append("path")
.attr("stroke-width", 2)
.attr("stroke", "grey")
.attr("class", "lineTrace")
.each(function(d,i){
// get the line data and add path
var lineData = [interpolateData(2010)[i],interpolateData(2011)[i],
interpolateData(2012)[i],interpolateData(2013)[i],interpolateData(2014)[i]];
d3.select(this)
.datum(lineData)
.attr("d", line);
});
Now set up the transitions on each path:
lineTraces.each(function(){
var path = d3.select(this);
path.transition()
.duration(30000)
.ease("linear")
.attrTween("stroke-dasharray", tweenDash)
});
Where tweenDash is:
function tweenDash() {
var l = lineTraces.node().getTotalLength();
var i = d3.interpolateString("0," + l, l + "," + l); // interpolation of stroke-dasharray style attr
return function(t) {
var p = lineTraces.node().getPointAtLength(t);
return i(t);
}
}
Here's an example.
You'll see it's not perfect, the timings are off. If I get a bit more time, I'll try and come back and straighten it out.
EDITS
Gave this some thought last night and it dawned on me that there's an easier, more succinct way to add the trace. Instead of pre-defining the path and then attrTweening the "stroke-dasharray", just build the path as you go:
var someData = interpolateData(2010);
// add the paths like before
var lineTraces = svg.append("g")
.selectAll(".traces")
.data([0,1,2])
.enter()
.append("path")
.attr("stroke-width", 2)
.attr("stroke", "grey")
.attr("class", "lineTrace")
.attr("d", line)
.each(function(d,i){
d3.select(this)
.datum([someData[i]])
.attr("d", line);
});
// Tweens the entire chart by first tweening the year, and then the data.
// For the interpolated data, the dots and label are redrawn.
function tweenYear() {
var year = d3.interpolateNumber(2010, 2014);
// added "addTrace" function
return function(t) { addTrace(year(t)); displayYear(year(t)); };
}
// append the data and draw the path
function addTrace(year){
var thisData = interpolateData(year);
lineTraces.each(function(d,i){
var trace = d3.select(this);
trace.datum().push(thisData[i]);
trace.attr("d", line);
});
}
This produces much better results.

Resources