d3.js nested data update line plot in html table - javascript

I am trying to create a dashboard with d3. I am putting bullet plots and sparklines into a table. I have been able to put the bullet plots in without any trouble, but I can't get the paths for the line plots. My data is nested and the below appends an svg and path correctly, but the path is empty. I have tried multiple update patterns and I do not see why I can't get any data for the path with the below.
Any help or suggestions is appreciated. Thanks.
data = [{data:{data1:[{index:1, value:5},{index:2, value:9}]}}]
sparkScaleY = d3.scaleLinear()
.domain([0,10])
.range([10,0])
sparkScaleX = d3.scaleLinear()
.domain([0,10])
.range([0,15])
line = d3.line()
.x(function(d) { return sparkScaleX (d.index); })
.y(function(d) { return sparkScaleY (d.value); });
rows = d3.select("tbody")
.selectAll("tr")
.data(data)
.enter()
.append("tr")
spark = rows.append('td')
.append('svg')
.attr("class", "spark-svg")
.attr("width", 15)
.attr("height", 10)
.append('path')
spark
.attr("class", "spark-path")
function update(data){
sparkSvg = d3.selectAll(".spark-svg")
.data(data)
sparkPath = sparkSvg.selectAll('.spark-path')
.data(function(d){return [d.data.data1]})
.enter()
.append('path')
.classed("new", true)
sparkPath.attr('d', line)
sparkPath
.attr('d', line)
.classed("new",false)
sparkPath.exit().remove()
}
update(data)
<table>
<tbody>
</tbody>
</table>
<script src="https://d3js.org/d3.v5.min.js">
To clarify, the number of rows, columns, svgs, and paths remain constant between updates.

If the table structure is constant (same number of rows, columns, svgs and sparklines(paths)), then we can simplify your code a bit. In your example you are updating, entering, and exiting in the update function. We don't need to do this if we aren't entering or exiting anything once the table is created.
You already add the rows, columns, svg, and path once prior to the update function, so all we need in the update function is an update selection - which let us simplify that a fair bit:
// Add rows once, same as before:
rows = d3.select("tbody")
.selectAll("tr")
.data(data)
.enter()
.append("tr")
// Add sparkline cell,svg, and path once, same as before:
var spark = rows.append('td')
.append('svg')
.attr("class", "spark-svg")
.attr("width", 15)
.attr("height", 10)
.append('path')
.attr("class", "spark-path")
// Only update, no need for entering/exiting if table structure doesn't change, only data:
function update(data){
spark // use existing selection
.data(data) // assign new data
.attr('d', function(d) {
return line(d.data.data1); // draw new data
})
}
This looks like:
data = [{data:{data1:[{index:1, value:5},{index:4, value:9},{index:7,value:4},{index:10,value:8}]}}]
sparkScaleY = d3.scaleLinear()
.domain([0,10])
.range([10,0])
sparkScaleX = d3.scaleLinear()
.domain([0,10])
.range([0,15])
line = d3.line()
.x(function(d) { return sparkScaleX (d.index); })
.y(function(d) { return sparkScaleY (d.value); });
// Add rows once:
rows = d3.select("tbody")
.selectAll("tr")
.data(data)
.enter()
.append("tr")
// Add title cell:
rows.append("td")
.text(function(d) {
return Object.keys(d.data)[0];
})
// Add sparkline cell,svg, and path once:
var spark = rows.append('td')
.append('svg')
.attr("class", "spark-svg")
.attr("width", 15)
.attr("height", 10)
.append('path')
.attr("class", "spark-path")
// Only update, no need for entering/exiting if table structure doesn't change, only data:
function update(data){
spark // use the existing selection
.data(data)
.attr('d', function(d) {
return line(d.data.data1);
})
}
update(data)
path {
stroke-width: 2;
stroke:black;
fill: none;
}
<script src="https://d3js.org/d3.v5.min.js"></script><table>
<tbody>
</tbody>
</table>
And with some dynamic data
var data = generate();
sparkScaleY = d3.scaleLinear()
.domain([0,10])
.range([10,0])
sparkScaleX = d3.scaleLinear()
.domain([0,10])
.range([0,15])
line = d3.line()
.x(function(d) { return sparkScaleX (d.index); })
.y(function(d) { return sparkScaleY (d.value); });
// Add rows once:
rows = d3.select("tbody")
.selectAll("tr")
.data(data)
.enter()
.append("tr")
// Add title cell:
rows.append("td")
.text(function(d) {
return d.name;
})
// Add sparkline cell,svg, and path once:
var spark = rows.append('td')
.append('svg')
.attr("class", "spark-svg")
.attr("width", 15)
.attr("height", 10)
.append('path')
.attr("class", "spark-path")
var spark2 = rows.append('td')
.append('svg')
.attr("class", "spark-svg")
.attr("width", 15)
.attr("height", 10)
.append('path')
.attr("class", "spark-path2")
.style("stroke","steelblue");
// Only update, no need for entering/exiting if table structure doesn't change, only data:
function update(data){
spark
.data(data)
.transition()
.attr('d', function(d) {
return line(d.spark1);
})
spark2
.data(data)
.transition()
.attr('d', function(d) {
return line(d.spark2);
})
}
update(generate());
d3.interval(function(elapsed) {
update(generate());
}, 2000);
function generate() {
return d3.range(10).map(function(d) {
return {
name: "row"+d,
spark1: d3.range(10).map(function(d) {
return {index: d, value: Math.random()*10}
}),
spark2: d3.range(10).map(function(d) {
return {index: d, value: Math.random()*10}
})
}
})
}
path {
stroke-width: 2;
stroke:black;
fill: none;
}
<script src="https://d3js.org/d3.v5.min.js"></script><table>
<tbody>
</tbody>
</table>

Related

Conditional Bar Chart with D3

I have a D3 visualization with a map and a bar chart. I am trying to get the bar chart to change depending on which circle on the map is clicked. Not sure how to do this. I have a function in my bar_chart.js file named update(newData) and a few extra arrays for the different circles on the map. Here is the link to the bl.ocks for the map and bar char.
js code for map
var myData = [21, 3, 5, 21, 15];
//Width and height
var w = 200;
var h = 125;
var yScale = null;
function draw(initialData) {
var xScale = d3.scale.ordinal()
.domain(d3.range(initialData.length))
.rangeRoundBands([0, w], 0.05);
yScale = d3.scale.linear()
.domain([0, d3.max(initialData)])
.range([0, h]);
//Create SVG element
var svg = d3.select("#chart")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(initialData)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return h - yScale(d);
})
.attr("width", xScale.rangeBand())
.attr("height", function(d) {
return yScale(d);
})
.attr("fill", "steelblue");
svg.selectAll("text")
.data(initialData)
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
return h - yScale(d) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
}
draw(myData);
//update function
function update(newData) {
yScale.domain([0, d3.max(newData)]);
var rects = d3.select("#chart svg")
.selectAll("rect")
.data(newData);
// enter selection
rects
.enter().append("rect");
// update selection
rects
.transition()
.duration(300)
.attr("y", function(d) {
return h - yScale(d);
})
.attr("height", function(d) {
return yScale(d);
})
// exit selection
rects
.exit().remove();
var texts = d3.select("#chart svg")
.selectAll("text")
.data(newData);
// enter selection
texts
.enter().append("rect");
// update selection
texts
.transition()
.duration(300)
.attr("y", function(d) {
return h - yScale(d) + 14;
})
.text(function(d) {
return d;
})
// exit selection
texts
.exit().remove();
}
var mk = [10,17,20,14,8];
var cn = [18,4,9,20,15];
var nd = [5,12,7,15,21];
d3.select("#update").on("click", function() { update(newData); });
You have to incorporate the barchart data in your cities.csv file.
In the on-click handler of cities.csv where you show the tooltip you have to transform the data from the CSV into an array and call the bar chart update() method with this array.
One way of doing is to replace the , from the bar chart data with another char and split the string and convert the parts to numbers.
var cityData = d.barchart.split('#').map(Number);
update(cityData);
You also have to set the attributes of the new rects and texts of the bar chart. And the x-position will change if the number of bars change.

D3 - Load in and display data, row by row with set delay

I am trying to use time series data to plot bubbles on a map. What I would like to do is slowly plot these bubbles based on their date rather than all at once.
Something similar to:
http://www.nytimes.com/interactive/2014/upshot/mapping-the-spread-of-drought-across-the-us.html?_r=0
Here is some sample data:
date count code,country,lat,lon,counter
1/28/16 3 AND,Andorra,42.5,1.516667,0.577121255
1/29/16,146,ARE,United Arab Emirates,24.46666667,54.366667,2.264352856
1/30/16,13,AFG,Afghanistan,34.51666667,69.183333,1.213943352
Example of D3 Map
I have already looked at MB's tutorials on Path Transitions, Udacity's course on D3, and many questions on Stack Overflow.
I have previously tried using setInterval and setTimeout but most of the examples were with multiple data files. I would like to use one datafile line by line.
Code:
var width = 960,
height = 500;
var projection = d3.geo.mercator()
.center([0, 5 ])
.scale(200)
.rotate([-180,0]);
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([0, 0])
.html(function(d) {
return "<strong>Frequency:</strong> <span style='color:white'>" + d.count + "</span>"+ "<br/>" + d.country;
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var path = d3.geo.path()
.projection(projection);
var g = svg.append("g");
// load and display the World
d3.json("world-110m2.json", function(error, topology) {
svg.call(tip)
// load and display the cities
d3.csv("cities2_or.csv", function(error, data) {
max = d3.max(data, function(d)
{return +d.counter})
coloring = d3.scale.linear()
.domain([0, max])
.range(["blue", "green"])
radiusing = d3.scale.linear()
.domain([0, max])
//.domain([0, 100])
.range([2, 30])
g.selectAll("circle")
.data(data)
.enter()
.append("circle")
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.style("r", function(d){
return radiusing(+d.counter)
;})
//.style("opacity", .5)
.style("fill", function(d){
return coloring(+d.counter);
})
});
Thanks for any help
I took #adilapapaya suggestions and tried using the delay function but I am only able to plot the first point.
Instead of the g.selectAll that I was using above I have replaced it with the following. This however only plots the first point in my csv file and then stops.
g.append("circle")
.data(data)
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.style("r", 20)
.transition()
.duration(1)
.delay(function(d, i) { return i*1; })
.style("r",30)
.style("fill","green");
Thanks to Andrew Guy who helped find the JS fiddle example for me. Worked perfectly. My issue was not using the enter before hand. Here is the final code. Please respond if anyone has any questions.
var width = 960,
height = 500;
var projection = d3.geo.mercator()
.center([0, 5 ])
.scale(200)
.rotate([-180,0]);
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([0, 0])
.html(function(d) {
return "<strong>Frequency:</strong> <span style='color:white'>" + d.count + "</span>"+ "<br/>" + d.country;
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var path = d3.geo.path()
.projection(projection);
var g = svg.append("g");
// load and display the World
d3.json("world-110m2.json", function(error, topology) {
svg.call(tip)
// load and display the cities
d3.csv("cities2_or.csv", function(error, data) {
max = d3.max(data, function(d)
{return +d.counter})
coloring = d3.scale.linear()
.domain([0, max])
.range(["blue", "green"])
radiusing = d3.scale.linear()
.domain([0, max])
//.domain([0, 100])
.range([2, 30])
g.selectAll("circle")
.data(data)
.enter()
.append("circle")
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
// .style("r", function(d){
// return radiusing(+d.counter)
// ;})
//.style("opacity", .5)
.style("fill", function(d){
return coloring(+d.counter);})
.style("r",0)
.transition()
.duration(500)
.delay(function(d, i) { return i*200; })
.style("r",30);
});
g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries)
.geometries)
.enter()
.append("path")
.attr("d", path)
});
// zoom and pan
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll("circle")
.attr("d", path.projection(projection));
g.selectAll("path")
.attr("d", path.projection(projection));
});
svg.call(zoom)

d3 nested data - individual graphs - setting y.domain?

I am trying to turn some nested data into 8 individual line charts (one chart for each key). So far I am creating one graph per svg, however I am having issues with the y-domain - specifically setting the y-domain for each graph.
currently:
var line = d3.svg.line()
.x(function (d) { return x(d.date); })
.y(function (d) { return y(d.app); })
x.domain(d3.extent([parseDate("2003.0"), parseDate("2014.0")]));
y.domain(d3.extent([0,20000]));
var data2 = d3.nest()
.key(function(d) { return d.race; })
.entries(data);
var svgContainer = d3.selectAll("body")
.data(data2)
.enter()
.append("svg")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.append("path")
.data(data2)
.attr('id', function(d) { return d.key ;})
.attr('class', 'line')
.attr('opacity', .8)
.attr('d', function(d) { return line(d.values); });
I see parts of some lines in the svg's, but most are cut off. Any suggestions? I'm not sure if the paths are correct either.
Data:
{ key: "1", values: 0: ['app' : 50000, year: '2003'], 1: ['app': 20000, year: '2004'],
key: "2" values: 0: ['app' : 40000, year: '2003'], 1: ['app' 50000, year: '2004']
etc...}
Modified d3 using a different X scale and Y scale domain for each selection
var svgContainer = d3.select("body").selectAll(".line")
.data(data2)
.enter()
.append("svg")
.attr("transform", "translate(" + margin.left + "," + margin.top+ ")")
.append("g")
.each(function(d, i){
var eachRace = d.values;
console.log(eachRace);
var svg = d3.select(this);
var yMax = d3.max(eachRace, function(d) { return d.app; });
var yScale = d3.scale.linear().domain([0, yMax]).range([height/8, 0]);
var yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(5);
var line = d3.svg.line()
.x(function (d) { return x(d.date); })
.y(function (d) { return yScale(d.app); })
svg.append("path")
.attr("id", function(d) { return d.key ;})
.attr('class', 'line')
.attr('opacity', .8)
.attr('d', function(d) { return line(d.values); })
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
})
You can apply extent directly to your data like so:
yScale.domain(d3.extent(data, function(d) { return d.app; }));
This will give you the extent of all the data. If you need to get the extent of portions of the data, such as in your case of one category vs. another, you need to get the extent of the result of some filtering function. You should look into either d3.filter or write your own within extent(). So you'd probably want to make the return value contingent on d.key matching your current key, however you are storing that.

Update pattern with path line selections

I am trying to use the enter(), update(), exit() pattern for a line chart, and I'm not getting my lines to appear properly.
A fiddle example. http://jsfiddle.net/wy6h1jcg/
THey do show up in the dom, but have no x or y values (though they are styled)
My svg is already created as follows:
var chart = d3.select("#charts")
.append("svg")
chart
.attr("width", attributes.chartsWidth)
.attr("height", attributes.chartsHeight);
I want to create a new object for my update binding, as follows:
var thechart = chart.selectAll("path.line").data(data, function(d){return d.x_axis} )
thechart.enter()
.append("path")
thechart.transition().duration(100).attr('d', line).attr("class", "line");
But this is no good.
Note, this does work (but can't be used for our update):
chart.append("path")
.datum(data, function(d){return d.x_axis})
.attr("class", "line")
.attr("d", line);
One other note:
I have a separate function that binds data for creating another chart on the svg.
var thechart = chart.selectAll("rect")
.data(data, function(d){return d.x_axis});
thechart.enter()
.append("rect")
.attr("class","bars")
Could these two bindings be interacting?
This is the update logic I ended on, still a closured pattern:
function updateScatterChart(chartUpdate) {
var wxz = (wx * 37) + c;
var x = d3.scale.linear()
.range([c, wxz]);
var y = d3.scale.linear()
.range([h, hTopMargin]);
var line = d3.svg.line()
.x(function(d) { return x(+d.x_axis); })
.y(function(d) { return y(+d.percent); }).interpolate("basis");
if (lastUpdateLine != chartUpdate) {
console.log('updating..')
d3.csv("./data/multiline.csv", function(dataset) {
console.log(chartUpdate);
var data2 = dataset.filter(function(d){return d.type == chartUpdate});
x.domain(d3.extent(data2, function(d) { return +d.x_axis; }));
y.domain([0, d3.max(data2, function(d) { return +d.percent; })]);
var thechart2 = chart.selectAll("path.line").data(data2, function(d){return d.neighborhood;});
thechart2.enter()
.append("svg:path")
.attr("class", "line")
.attr("d", line(data2))
thechart2.transition()
.duration(800)
.attr("d", line(data2))
.style("opacity", (chartUpdate == 'remove') ? 0 : 1 )
thechart2.exit()
.transition()
.duration(400)
.remove();
})
}
lastUpdateLine = chartUpdate;
}

D3 updating bar chart with new data set cause update issue

Well the rendering of bar chart works fine with default given data. The problem occurs on the button click which should also cause the get of new data set. Updating the x-axis y-axis works well but the rendering data causes problems.
First Ill try to remove all the previously added rects and then add the new data set. But all the new rect elements gets added into wrong place, because there is no reference to old rects.
Here is the code and the redraw is in the end of code.
http://jsfiddle.net/staar2/wBNWK/9/
var data = JSON.parse('[{"hour":0,"time":147},{"hour":1,"time":0},{"hour":2,"time":74},{"hour":3,"time":141},{"hour":4,"time":137},{"hour":5,"time":210},{"hour":6,"time":71},{"hour":7,"time":73},{"hour":8,"time":0},{"hour":9,"time":68},{"hour":10,"time":70},{"hour":11,"time":0},{"hour":12,"time":147},{"hour":13,"time":0},{"hour":14,"time":0},{"hour":15,"time":69},{"hour":16,"time":67},{"hour":17,"time":67},{"hour":18,"time":66},{"hour":19,"time":0},{"hour":20,"time":0},{"hour":21,"time":66},{"hour":22,"time":210},{"hour":23,"time":0}] ');
var w = 15,
h = 80;
var xScale = d3.scale.linear()
.domain([0, 1])
.range([0, w]);
var yScale = d3.scale.linear()
.domain([0, d3.max(data, function (d) {
return d.time;
})])
.rangeRound([5, h]);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.ticks(5);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
var chart = d3.select("#viz")
.append("svg")
.attr("class", "chart")
.attr("width", w * data.length - 1)
.attr("height", h);
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", function(d, i) {
return xScale(i) - 0.5;
})
.attr("y", function(d) {
return h - yScale(d.time) - 0.5;
})
.attr("width", w)
.attr("height", function(d) {
return yScale(d.time);
});
chart.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function(d) {
if (d.time > 10) {
return Math.round(d.time);
}
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "#FFF")
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return xScale(i) + w / 2;
})
.attr("y", function(d) {
return h - yScale(d.time) - 0.5 + 10;
});
chart.append("g")
.attr("transform", "translate(0," + (h) + ")")
.call(xAxis);
function redraw() {
// This the part where the incoming data set also changes, which means the update to x-axis y-axis, labels
yScale.domain([0, d3.max(data, function (d) {
return d.time;
})]);
var bars = d3.selectAll("rect")
.data(data, function (d) {
return d.hour;
});
bars
.transition()
.duration(500)
.attr("x", w) // <-- Exit stage left
.remove();
d3.selectAll("rect") // This is actually empty
.data(data, function (d) {
return d.hour;
})
.enter()
.append("rect")
.attr("x", function(d, i) {
console.log(d, d.day, xScale(d.day));
return xScale(d.day);
})
.attr("y", function(d) {
return yScale(d.time);
})
.attr("width", w)
.attr("height", function (d) {
return h - yScale(d.time);
});
}
d3.select("button").on("click", function() {
console.log('Clicked');
redraw();
});
Agree with Sam (although there were a few more issues, like using remove() without exit(), etc.) and I am putting this out because I was playing with it as I was cleaning the code and applying the update pattern. Here is the FIDDLE with changes in code I made. I only changed the first few data points but this should get you going.
var data2 = JSON.parse('[{"hour":0,"time":153},{"hour":1,"time":10},{"hour":2,"time":35},{"hour":3,"time":150},
UPDATE: per request, adding logic to consider an update with new data. UPDATED FIDDLE.
Since you're binding the same data to bars, the enter selection is empty. Once you remove the existing bars, you append a new bar for each data point in the enter selection - which again is empty. If you had different data, the bars should append.
If you haven't read through it already, the general update pattern is a great resource for understanding this sort of thing.

Categories