Newbie here. I am building a D3 bar-chart, and was able to animate the bar height; however, I have a text field next to each bar that shows the values, I was unable to make the text count up as the bar height grows.
I am using the .text attribute with I think where the problem is:
g.selectAll(".myText")
.transition()
.text(function(d){return (d.m6+"%")}) //code for counting from previous d.m6 value?
.attr("transform", function(d) { ...code for moving text location...)
.duration(700)
.ease(d3.easeLinear);
Any help will be greatly appreciated
Transitioning plain text will just result in the end value being displayed on transition start (after any delay):
For each selected element, sets the text content to the specified target value when the transition starts. .... Text is not interpolated by default because it is usually undesirable. (docs).
Here's an example of this at work:
var svg = d3.select("body")
.append("svg");
var text = svg.append("text")
.attr("x", 50)
.attr("y", 50)
.text(1);
text.transition()
.text(1000)
.duration(1000)
.delay(1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
However, you can interpolate text, the most canonical method would be to use transition.tween() (documentation). Though you could use .attrTween or even d3-timer. There are also a few other options out there but they are less straight forward.
A tween takes two arguments. The first argument is the name of the property being modified. The second argument is a tween value which returns a function of the form:
return function(t) { interpolator(t); })
Where t is a number between 0 and 1 representing the progress of the transition (0 = start, 1 = end), and interpolator is some function for interpolating a transition value for any point in the transition. With d3 this might be a d3-interpolator.
var svg = d3.select("body")
.append("svg");
var text = svg.append("text")
.attr("x", 50)
.attr("y", 50)
.text(1);
text.transition()
.tween("text", function() {
var selection = d3.select(this); // selection of node being transitioned
var start = d3.select(this).text(); // start value prior to transition
var end = 1000; // specified end value
var interpolator = d3.interpolateNumber(start,end); // d3 interpolator
return function(t) { selection.text(Math.round(interpolator(t))); }; // return value
})
.duration(10000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>
I've just used rounding to keep the number formatted, otherwise the decimal points get pretty obnoxious. You could always apply some actual formatting.
I created an heatmap using this example and this data:
NAME,YEAR,M1,M2
A,2000,20,5
B,2000,30,1
C,2000,,10
D,2000,,88
E,2000,,21
F,2000,84,3
G,2000,,64
A,2001,44,48
B,2001,15,51
C,2001,20,5
D,2001,95,2
E,2001,82,9
F,2001,,77
G,2001,3,80
A,2002,8,99
B,2002,92,52
C,2002,62,
D,2002,41,
E,2002,66,
F,2002,21,21
G,2002,62,4
A,2003,2,5
B,2003,89,78
C,2003,9,
D,2003,7,9
E,2003,2,45
F,2003,92,58
G,2003,2,14
A,2004,2,55
B,2004,89,58
C,2004,9,55
D,2004,7,59
E,2004,2,70
F,2004,92,
G,2004,2,
Now I would like to add to the right of the heatmap a sparkline for each row, so there must be a sparkline associated with A, to B, etc.
And I wish they were positioned right next to each other.
To make the sparklines I saw this example.
This is the result: PLUNKER.
As you can see, I can't get the data correctly from the data.csv file to create the sparklines. Also I don't know how to place them in the correct position.
I tried this way but without success.
var sparkSvg = d3.select("#container-sparkline")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.data(dataNest)
.enter()
.append("path")
.attr("class", "sparkline-path")
.attr("d", function(d) {
console.log("i");
console.log(d);
});
Also I'm not sure using two div is the correct way to put a chart near another chart.
Anyone would know how to help me?
Approach:
I've created a sparkline for every name in data set with values on x axis as a year and y as a m2 value from data set. For the demo purposes I've hardcoded number of years to 5 so x axis have only 5 values, but that can be computed with some additional script based on input data.
I've also added tome padding for sparkline container so they're aligned with the heatmap
Code:
As you can see in the plunker I've introduced a function to group data by name, so for each name we have an array with objects:
var groupBy = function(array, key) {
return array.reduce(function(a, v) {
(a[v[key]] = a[v[key]] || []).push(v);
return a;
}, {});
};
// data grouped by name
var groupedData = groupBy(data, 'name');
Since we assumed for demo purposes that X axis has fixed number of values we need to find max value for Y axis to properly scale charts. To do that I reduce array of values to get only m2 values and find a max number whthin that array:
var maxYvalue = Math.max(...data.map(function(d){return Number(d.m2)}));
Now we can create scales for the sparklines
var x = d3.scaleLinear().domain([0, 4]).range([0, 60]);
var y = d3.scaleLinear().domain([0, maxYvalue]).range([2, itemSize-2 ]);
I'm assuming that chart have width of 60px and height of itemSize, I also introduce 2px of vertical padding so its easier to read those sparklines being on next to each-other.
Now we can define d3.line(as you already did in your plunker) which we'll use fro rendering sparklines .
var line = d3.line()
.x(function(d, i) { return x(i); })
.y(function(d) { return y(d); })
And last step is to render sparklines inside "#container-sparkline" container. To do that we can iterate over every array in groupedData and render sparkline for each name:
// for each name render sparkline
Object.keys(groupedData).forEach(function(key){
const sparkData = groupedData[key].map(function(datum){
return Number(datum['m2']);
})
var sparkSvg = d3.select("#container-sparkline")
.append("svg")
.attr("width", "100%")
.attr("height", itemSize-1)
.append("path")
.attr("class", "sparkline-path")
.attr("d", line(sparkData));
})
I've also slightly changed styles for #container-sparkline and added borders for sparkline svg's. I hope this is what you've asked for.
Here you can find your plunker with my changes
http://plnkr.co/edit/9vUFI76Ghieq4yZID5B7?p=preview
I'm very new to d3, so goal #1 was to show the chart. This works with the following code:
const line = d3.line()
.x(d => this.x(d.x))
.y(d => this.y(d.y));
console.log('data', JSON.stringify(data), 'color', color);
// data [{"x":"2017-07-01T04:00:00.000Z","y":81.2},{"x":"2017-08-01T04:00:00.000Z","y":79.6},{"x":"2017-09-01T04:00:00.000Z","y":79.4},{"x":"2017-10-01T04:00:00.000Z","y":80.6},{"x":"2017-11-01T04:00:00.000Z","y":80},{"x":"2017-12-01T05:00:00.000Z","y":76}] color blue
g.append('path')
.datum(data)
.attr('class', 'line')
.attr('stroke', color)
.attr('d', line);
With this code, anytime I run this method again, I get a new line, which is expected since I'm appending. I want to update only the stroke and d attributes when I have new data and/or color, I replace the code after the console.log with:
const lines = g.selectAll('.line').data(data);
lines.enter()
.append('path')
.attr('class', 'line');
lines
.attr('stroke', color)
.attr('d', line);
I don't see the line anymore, not at first, not after updates.
I'm including a codepen with the code in this question: https://codepen.io/anon/pen/BJaVNM?editors=0010
Thanks again for your help!
The proper data join would be:
// give data an array, each part of the array is an array representing a line (ie path)
let lines = g.selectAll('.line').data([data]); //<-- notice array
// you have lines entering, .merge this back to the update selection
lines = lines.enter()
.append('path')
.attr('class', 'line')
.merge(lines);
// lines variable is now enter + update
lines
.attr('stroke', color)
.attr('d', line);
Updated codepen
This D3 js example shows all the code to produce a multi-line graph that can be toggled. Each line in the graph includes dots for existing data points.
While the lines can be toggled on/off, the dots stay stagnant. I would like for the toggle to work for both turning on/off the line & the dots that are associated with the same line.
I suspect that the svg.append("text") is the part that requires code update to also enable the dots to be turned on/off along with the line.
Here is the existing code snipet that turns on/off the line graph, but it doesn't turn on/off the dots.
svg.append("text")
.attr("x", (legendSpace/2)+i*legendSpace) // space legend
.attr("y", height + (margin.bottom/2)+ 5)
.attr("class", "legend") // style the legend
.style("font-size","15px") // Change the font size
.style("font-weight", "bold") // Change the font to bold
.style("text-anchor", "middle") // center the legend
.style("fill", function() { // Add the colours dynamically
return d.color = color(d.key); })
.on("click", function(){
// Determine if current line is visible
var active = d.active ? false : true,
newOpacity = active ? 0 : 1;
// Hide or show the elements based on the ID
d3.select("#tag"+d.key.replace(/\s+/g, ''))
.transition().duration(100)
.style("opacity", newOpacity);
// Update whether or not the elements are active
d.active = active;
})
.text(d.key);
Please help.
IDs are unique. You cannot set the same ID for several different DOM elements.
Solution: set classes instead.
For the lines:
.attr("class", 'tag'+d.key.replace(/\s+/g, ''))
And for the circles:
.attr("class", d=>'tag'+d.symbol.replace(/\s+/g, ''))
Then, get the class on the click event (use selectAll, not select):
d3.selectAll(".tag"+d.key.replace(/\s+/g, ''))
here is the updated fiddle: http://jsfiddle.net/gx4zc8tq/
I'm trying to make a plot that shows density estimations for two different distributions simultaneously, like this:
The data is in two columns of a CSV file. I've modified code from Mike Bostock's block on kernel density estimation, and have managed to make a plot that does what I want, but only if I manually specify the two separate density plots -- see this JSFiddle, particularly beginning at line 66:
svg.append("path")
.datum(kde(cola))
.attr("class", "area")
.attr("d", area)
.style("fill", "#a6cee3");
svg.append("path")
.datum(kde(colb))
.attr("class", "area")
.attr("d", area)
.style("fill", "#b2df8a");
I've tried various incantations with map() to try to get the data into a single object that can be used to set the color of each density area according to the color domain, e.g.:
var columns = color.domain().map(function(column) {
return {
column: column,
values: data.map(function(d) {
return {kde: kde(d[column])};
})
};
});
I don't have a great grasp of what map() does, but this definitely does not work.
How can I structure my data to make this plot in a less brittle way?
To make this generic and remove column dependency first prepare your data:
var newData = {};
// Columns should be numeric
data.forEach(function(d) {
//iterate over all the keys
d3.keys(d).forEach(function(col){
if (!newData[col])
newData[col] = [];//create an array if not present.
newData[col].push(+d[col])
});
});
Now newData will hold the data like this
{
a:[123, 23, 45 ...],
b: [34,567, 45, ...]
}
Next make the color domain like this:
var color = d3.scale.category10()
.domain(d3.keys(newData))//this will return the columns
.range(["#a6cee3", "#b2df8a"]);
Finally make your area chart like this:
d3.keys(newData).forEach(function(d){
svg.append("path")
.datum(kde(newData[d]))
.attr("class", "area")
.attr("d", area)
.style("fill", color(d));
})
So now the code will have no dependency over the column names and its generic.
working code here