updating d3 chart with new data, the old data points not removed - javascript

I am trying to update my d3 bubble chart with new data getting from the backend. However, it seems that the new data gets added to the chart but the old data points are not removed? What is the issue here?
Here is my function:
function updateGeoData(ds, de) {
console.log("hit 233")
const size = d3.scaleLinear()
.domain([1, 100]) // What's in the data
.range([4, 50]) // Size in pixel
let updateData = $.get("/update_geo_data", {"ds":ds, "de":de});
updateData.done(function (result) {
var circles = svg.selectAll("myCircles").data(result, function(d) { return d; });
circles.attr("class", "update");
circles.enter().append("circle")
.merge(circles)
.attr("cx", d => projection([d.long, d.lat])[0])
.attr("cy", d => projection([d.long, d.lat])[1])
.attr("r", d => size(d.count))
.style("fill", "69b3a2")
.attr("stroke", "#c99614")
.attr("stroke-width", 2)
.attr("fill-opacity", .4);
circles.exit().remove();
});
}

This part right here is your problem:
svg.selectAll("myCircles")
myCircles isn't anything so the selection will always be empty, and you will always only append to it.
svg.selectAll("circle") should work as a selection for you. This will select all the circles currently plotted on and enter, update, remove appropriately.

Related

D3.JS Click Event problem, using a svg map

Hi I'm fairly new to the D3 library, and I cannot figure out how to get a click event to work when I'm clicking on the country, I understand the 'd' variable contains the country information but how do I get it to console log on click. I've tried a few methods but cant figure it out. any help appreciated
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
// Map and projection
const projection = d3.geoNaturalEarth1()
.scale(width / 1.5 ) // Lower the num closer the zoom
.translate([200, 700]) // (Horizontal, Vertical)
// Load external data and boot
d3.json("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson").then( function(data) {
// Draw the map
svg.append("g")
.selectAll("path")
.data(data.features)
.join("path")
.attr("fill", "#348C31") // Color Of Country
.attr("d", d3.geoPath().projection(projection))
.style("stroke", "white")// Border Lines
.append("title")
.text(d => console.log(d))
//Want to run on click of country,
})
const $countyPaths = svg.append("g")
.selectAll("path")
.data(data.features)
.join("path")
$countyPaths
.attr("fill", d => color(data.get(d.id)))
.attr("d", path)
.append("title")
$countyPaths.on('click',d=>console.log(d.id))
function buttonClick() {
window.alert("Boom");
}
</script>
d3 provides the .on(event, fx(event, d)) method in order to handle events.
To log the country, you could do then the following:
svg.append("g")
.selectAll("path")
.data(data.features)
.join("path")
.attr("fill", "#348C31") // Color Of Country
.attr("d", d3.geoPath().projection(projection))
.style("stroke", "white")// Border Lines
.on("click", (_, d) => console.log(d))
Note that the first thing that the function from the event listener will receive is the event info, and the second one the data

D3 join() only renders 'update' while doesn't show 'enter' elements

I tried to update an array of circles from the static positions
to dynamic force layout positions.
I tried to apply different colors for the circles within 'update' range
and apply another color to the new circles added from the new data.
however, newly added data nodes are not rendered for some reason.
Can anyone help me?
My update pattern is as below.
1.Initial Data Binding
let svg = d3.select('.graph')
.attr('width',width)
.attr('height',height)
svg.selectAll('circles').data(randoms)
.join('circle')
.attr('cx',d=>Math.random()*600)
.attr('cy',d=>Math.random()*600)
.attr('r',5)
2.Update Data binding
let simulation = d3.forceSimulation(randoms2)
.force('x', d3.forceX().x(function(d,i) {
if(i%2==0){
return 10;
}
else{
return 20
}
}))
.force('collision', d3.forceCollide().radius(function(d) {
return 1
}))
.on('tick',ticked)
function ticked(){
d3.selectAll('circle')
.data(randoms2)
.join( enter =>enter.append('circle')
.attr("fill", "green")
.attr('cx', function(d) {return d.x})
.attr('cy', function(d) {return d.y})
.attr('r',5),
update => update
.attr("fill", "red"),
exit => exit.remove()
)
.transition().duration(100)
.attr('cx', function(d) {return d.x})
.attr('cy', function(d) {return d.y})
}
It only shows the updated circles.
the complete code is in the following link.
https://codepen.io/jotnajoa/pen/xxEYaYV?editors=0001

D3 chart can't update -- enter and exit property of selection both empty

I'm trying to make a scatter plot using a .json file. It will let the user to select which group of data in the json file to be displayed. So I'm trying to use the update pattern.
The following code will make the first drawing, but every time selectGroup() is called(the code is in the html file), nothing got updated. The console.log(selection) did come back with a new array each time, but the enter and exit property of that selection is always empty.
Can anyone help me take a look? Thanks a lot!
var margin = {
top: 30,
right: 40,
bottom: 30,
left: 40
}
var width = 640 - margin.right - margin.left,
height = 360 - margin.top - margin.bottom;
var dataGroup;
var groupNumDefault = "I";
var maxX, maxY;
var svg, xAxis, xScale, yAxis, yScale;
//select and read data by group
function init() {
d3.json("data.json", function (d) {
maxX = d3.max(d, function (d) {
return d.x;
});
maxY = d3.max(d, function (d) {
return d.y;
});
console.log(maxY);
svg = d3.select("svg")
.attr("id", "scatter_plot")
.attr("width", 960)
.attr("height", 500)
.append("g")
.attr("id", "drawing_area")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//x-axis
xScale = d3.scale.linear().range([0, width]).domain([0, maxX]);
xAxis = d3.svg.axis()
.scale(xScale).orient("bottom").ticks(6);
//y-axis
yScale = d3.scale.linear().range([0, height]).domain([maxY, 0]);
yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(6);
svg.append("g")
.attr("class", "x_axis")
.attr("transform", "translate(0," + (height) + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y_axis")
.call(yAxis);
});
selectGroup(groupNumDefault);
}
//update data
function selectGroup(groupNum) {
d3.json("/data.json", function (d) {
dataGroup = d.filter(function (el) {
return el.group == groupNum;
});
console.log(dataGroup);
drawChart(dataGroup);
});
}
//drawing function
function drawChart(data) {
var selection = d3.select("svg").selectAll("circle")
.data(data);
console.log(selection);
selection.enter()
.append("circle")
.attr("class", "dots")
.attr("cx", function (d) {
console.log("updating!");
return xScale(d.x);
})
.attr("cy", function (d) {
return yScale(d.y);
})
.attr("r", function (d) {
return 10;
})
.attr("fill", "red");
selection.exit().remove();
}
init();
The problem here is on two fronts:
Firstly, your lack of a key function in your data() call means data is matched by index (position in data array) by default, which will mean no enter and exit selections if the old and current datasets sent to data() are of the same size. Instead, most (perhaps all) of the data will be put in the update selection when d3 matches by index (first datum in old dataset = first datum in new dataset, second datum in old dataset = second datum in new dataset etc etc)
var selection = d3.select("svg").selectAll("circle")
.data(data);
See: https://bl.ocks.org/mbostock/3808221
Basically, you need your data call adjusted to something like this (if your data has an .id property or anything else that can uniquely identify each datum)
var selection = d3.select("svg").selectAll("circle")
.data(data, function(d) { return d.id; });
This will generate enter() and exit() (and update) selections based on the data's actual contents rather than just their index.
Secondly, not everything the second time round is guaranteed be in the enter or exit selections. Some data may be just an update of existing data and not in either of those selections (in your case it may be intended to be completely new each time). However, given the situation just described above it's pretty much guaranteed most of your data will be in the update selection, some of it by mistake. To show updates you will need to alter the code like this (I'm assuming d3 v3 here, apparently it's slightly different for v4)
selection.enter()
.append("circle")
.attr("class", "dots")
.attr("r", function (d) {
return 10;
})
.attr("fill", "red");
// this new bit is the update selection (which includes the just added enter selection
// now, the syntax is different in v4)
selection // v3 version
// .merge(selection) // v4 version (remove semi-colon off preceding enter statement)
.attr("cx", function (d) {
console.log("updating!");
return xScale(d.x);
})
.attr("cy", function (d) {
return yScale(d.y);
})
selection.exit().remove();
Those two changes should see your visualisation working, unless of course the problem is something as simple as an empty set of data the second time around which would also explain things :-)

Failing to transition in D3 off new data

I can load and display data perfectly once, but any attempt to update seems to fail.
I'm trying to build a map that shows activity. In my data csv I have latitude and logitude and the hour of activity (0-23). I am using this highly useful tutorial on updating D3 with new data. The idea being that I cycle through data by the hour.
I've more-or-less stolen the update function. While the initial call works perfectly, any repeated calls manage to only recolor the already-present data, rather than remove it and add new data. This leads me to believe I am not changing the incoming data.
Is there anything obviously wrong with my update call?
function update(data, x) {
// DATA JOIN
// Join new data to any existing data
var circles = g.selectAll("circle")
.data(data); //, function(d) { return d; });
// UPDATE
// Update old elements as needed.
circles
.style("fill", "green")
.transition()
.duration(750);
// ENTER
// Create new elements
circles.enter().append("circle")
.filter(function(d) { return d.hr == x; })
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("r", 0)
.style("opacity", 0.2)
.style("fill", "blue")
.transition()
.duration(750)
.ease("linear")
.attr("r", function(d) {
return d.radius/2;
});
// EXIT
// Remove old elements
circles.exit()
.style("fill", "red")
.transition()
.duration(1500)
.ease("linear")
.attr("r", 0)
.remove();
}
Run from the command line:
update(csv_data, 1)
update(csv_data, 5)
It seems like you're trying to change the data by the line
filter(function(d) { return d.hr == x; })
but that's only filtering the selection.
What you want to do is filter the data before passing it to your selection which you do with the line
.data(data);
I suggest using Array.filter in the beginning of your update function:
function update(data, x) {
// DATA JOIN
// Join new data to any existing data
// filter the data as per x
data = data.filter(function (d) { return d.hr == x; });
var circles = g.selectAll("circle")
.data(data);
// rest of function
}
Be sure to remove the line where you filter after the append.

D3js - Creating and easily updating a multi-line chart

I've created a little test line chart using D3, but since I am quite new to the library I am not sure what the best way would be to add multiple lines to a chart, at the moment I only have one line displayed in this fiddle.
I would like to display 2 lines on the chart, but I am unsure of how to achieve that without copy pasting code, which I am sure would be very inefficient as I would like to update/animate the graph at regular intervals based on user selection.
Instead of this,
var data = [12345,22345,32345,42345,52345,62345,72345,82345,92345,102345,112345,122345,132345,142345];
I would like to display something like this,
var data = [
[12345,42345,3234,22345,72345,62345,32345,92345,52345,22345], // line one
[1234,4234,3234,2234,7234,6234,3234,9234,5234,2234] // line two
];
Would this be a possibility? If so, what would be the best way to approach this, so that I can easily update/animate the graph when needed?
Note: I am merely trying to learn and to familiarize myself with D3 best practices and the library as a whole. Thanks.
This is possible and reasonable.
There is a tutorial that approaches this at the
D3 Nested Selection Tutorial
which describes nesting of data.
Below is code that I hacked from your fiddle to demonstrate this.
var data = [
[12345,42345,3234,22345,72345,62345,32345,92345,52345,22345],
[1234,4234,3234,2234,7234,6234,3234,9234,5234,2234]
];
var width = 625,
height = 350;
var x = d3.scale.linear()
.domain([0,data[0].length]) // Hack. Computing x-domain from 1st array
.range([0, width]);
var y = d3.scale.linear()
.domain([0,d3.max(data[0])]) // Hack. Computing y-domain from 1st array
.range([height, 0]);
var line = d3.svg.line()
.x(function(d,i) { return x(i); })
.y(function(d) { return y(d); });
var area = d3.svg.area()
.x(line.x())
.y1(line.y())
.y0(y(0));
var svg = d3.select("body").append("svg")
//.datum(data)
.attr("width", width)
.attr("height", height)
//.append("g");
var lines = svg.selectAll( "g" )
.data( data ); // The data here is two arrays
// for each array, create a 'g' line container
var aLineContainer = lines
.enter().append("g");
/*svg.append("path")
.attr("class", "area")
.attr("d", area);*/
aLineContainer.append("path")
.attr("class", "area")
.attr("d", area);
/*svg.append("path")
.attr("class", "line")
.attr("d", line);*/
aLineContainer.append("path")
.attr("class", "line")
.attr("d", line);
/*svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("cx", line.x())
.attr("cy", line.y())
.attr("r", 3.5);*/
// Access the nested data, which are values within the array here
aLineContainer.selectAll(".dot")
.data( function( d, i ) { return d; } ) // This is the nested data call
.enter()
.append("circle")
.attr("class", "dot")
.attr("cx", line.x())
.attr("cy", line.y())
.attr("r", 3.5);
​
One deficiency is that I computed the domain for the x and y axes off the first array, which is a hack, but not pertinent to your question exactly.

Categories