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;
}
Related
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>
I’ve created a line graph in D3. To ensure the line doesn’t overlap the y-axis, I have altered the range of the x-axis. As a result of this, there is a gap between the x-axis and the y-axis which I am trying to fill with another line.
The rest of the graph uses the D3 update pattern. However, when I try to use the pattern on this simple line, two path elements are drawn (one on top of the other). I have tried numerous solutions to correct this issue but I’m not having any luck. Does anyone have any suggestions?
The code below is what is drawing two of the same path elements
var xAxisLineData = [
{ x: margins.left , y: height - margins.bottom + 0.5 },
{ x: margins.left + 40, y: height - margins.bottom + 0.5 }];
var xAxisLine = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var update = vis.selectAll(".xAxisLine")
.data(xAxisLineData);
var enter = update.enter()
.append("path")
.attr("d", xAxisLine(xAxisLineData))
.attr("class", "xAxisLine")
.attr("stroke", "black");
Your problem is here:
var update = vis.selectAll(".xAxisLine")
.data(xAxisLineData);
this is a null selection, assuming there are no elements with the class xAxisLine, which means that using .enter().append() will append one element for each item in the xAxisLineData array.
You want to append one path per set of points representing a line, not one path for each in a set of points representing a line.
You really just want one line to be drawn, so you could do:
.data([xAxisLineData]);
or, place all the points in an array when defining xAxisLineData
Now you are passing a data array to the selection that contains one item: an array of points - as opposed to many items representing individual points. As the data array has one item, and your selection is empty, using .enter().append() will append one element:
var svg = d3.select("body").append("svg").attr("width",500).attr("height",400);
var lineData = [{x:100,y:100},{x:200,y:200}]
var xAxisLine = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var colors = ["steelblue","orange"];
var line = svg.selectAll(null)
.data([lineData])
.enter()
.append("path")
.attr("d", xAxisLine(lineData))
.attr("class", "xAxisLine")
.attr("stroke-width", function(d,i) { return (1-i) * 10 + 10; })
.attr("stroke", function(d,i) { return colors[i]; });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Compare without using an array to hold all the data points:
var svg = d3.select("body").append("svg").attr("width",500).attr("height",400);
var lineData = [{x:100,y:100},{x:200,y:200}]
var xAxisLine = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var colors = ["steelblue","orange"];
var line = svg.selectAll(null)
.data(lineData)
.enter()
.append("path")
.attr("d", xAxisLine(lineData))
.attr("class", "xAxisLine")
.attr("stroke-width", function(d,i) { return (1-i) * 10 + 10; })
.attr("stroke", function(d,i) { return colors[i]; });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
But, we can make one last change. Since each item in the data array is bound to the element, we can reference the datum, not the data array xAxisLineData, which would make adding multiple lines much easier:
.attr("d", function(d) { return xAxisLine(d) })
Note in the demo below that the variable xAxisLineData is defined as an array of arrays of points, or an array of multiple lines.
var svg = d3.select("body").append("svg").attr("width",500).attr("height",400);
var lineData = [[{x:100,y:100},{x:200,y:200}],[{x:150,y:150},{x:260,y:150}]]
var xAxisLine = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var colors = ["steelblue","orange"];
var line = svg.selectAll(null)
.data(lineData)
.enter()
.append("path")
.attr("d", function(d) { return xAxisLine(d) }) // use the element's datum
.attr("class", "xAxisLine")
.attr("stroke-width", function(d,i) { return (1-i) * 10 + 10; })
.attr("stroke", function(d,i) { return colors[i]; });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
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)
I have a scatter plot with a line of best fit. I want to draw vertical lines (residual error lines) from each data point to the regression line. How can I do this?
If possible, I would like to use a transition to rotate the line of best fit and have the error lines expand or contract as the line of best rotates. Any help would be greatly appreciated!
Edit:
As pointed out, I incorrectly said the data for my line was contained within "data"; however, the line is being drawn using an array called lin_reg_results that is structured as follows:
var lin_reg_results = [{'x':3.4,'y':4.6},{'x':3.6,'y',2.4}...]
lin_reg_results data points were created by performing a linear regression server side and then passing the results back with ajax.
var x = d3.scale.linear()
.domain(d3.extent(data, function(d) { return d[x_val]; }))
.range([0, width]);
var y = d3.scale.linear()
.domain(d3.extent(data, function(d) { return d[y_val]; }))
// PLOT THE DATA POINTS
svg.selectAll(".dot")
.data(data)
.enter()
.append("circle")
.attr("class", "dot")
.attr("transform", "translate("+50+"," + 10 + ")")
.attr("r", 2.5)
.attr("cx", function(d) { return x(d[x_val]); })
.attr("cy", function(d) { return y(d[y_val]); })
.style("fill", function(d) { return color(d[z_val]); });
// DRAW THE PATH
var x = d3.scale.linear()
.domain(d3.extent(data, function(d) { return d['x']; }))
.range([0, width]);
var y = d3.scale.linear()
.domain(d3.extent(data, function(d) { return d['y']; }))
var line = d3.svg.line()
.x(function(d) { return x(d['x']); })
.y(function(d) { return y(d['y']); })
.interpolate("linear");
svg.append("path")
.datum(lin_reg_results)
.attr("transform", "translate("+50+"," + 10 + ")")
.attr("id","regression_line")
.attr("class", "line")
.attr("d", line);
You need to append a line for each data point from the coordinates of that point to where it meets the line. The code would look something like the following (with some code adapted from this question):
d3.selectAll("line")
.data(data)
.enter()
.append("line")
.attr("x1", function(d) { return x(d[x_val]); })
.attr("x2", function(d) { return x(d[x_val]); })
.attr("y1", function(d) { return y(d[y_val]); })
.attr("y2", function(d) {
var line = d3.select("#regression_line").node(),
x = x(d[x_val]);
function getXY(len) {
return line.getPointAtLength(len);
}
var curlen = 0;
while (getXY(curlen).x < x) { curlen += 0.01; }
return getXY(curlen).y;
});
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.