I am plotting points on a UK map using D3 off a live data stream. When the data points exceed 10,000 the browser becomes sluggish and the animation is no longer smooth. So I modify the dataPoints array to keep only the last 5000 points.
However when I modify the dataPoints the first time using splice() D3 stops rendering any new points. The old points gradually disappear (due to a transition) but there are no new points. I am not sure what I am doing wrong here.
I have simulated the problem by loading data of a CSV as well storing it in memory and plotting them at a rate of 1 point every 100ms. Once the number of dots goes above 10 I splice to retain the last 5 points. I see the same behaviour. Can someone review the code and let me know what I am doing wrong?
Setup and the plotting function:
var width = 960,
height = 1160;
var dataPoints = []
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height);
var projection = d3.geo.albers()
.center([0, 55.4])
.rotate([4.4, 0])
.parallels([40, 70])
.scale(5000)
.translate([width / 2, height / 2]);
function renderPoints() {
var points = svg.selectAll("circle")
.data(dataPoints)
points.enter()
.append("circle")
.attr("cx", function (d) {
prj = projection([d.longitude, d.latitude])
return prj[0];
})
.attr("cy", function (d) {
prj = projection([d.longitude, d.latitude])
return prj[1];
})
.attr("r", "4px")
.attr("fill", "blue")
.attr("fill-opacity", ".4")
.transition()
.delay(5000)
.attr("r", "0px")
}
/* JavaScript goes here. */
d3.json("uk.json", function(error, uk) {
if (error) return console.error(error);
console.log(uk);
var subunits = topojson.feature(uk, uk.objects.subunits);
var path = d3.geo.path()
.projection(projection);
svg.selectAll(".subunit")
.data(subunits.features)
.enter().append("path")
.attr("class", function(d) { return "subunit " + d.id })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a,b) {return a!== b && a.id !== 'IRL';}))
.attr("d", path)
.attr("class", "subunit-boundary")
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a,b) {return a=== b && a.id === 'IRL';}))
.attr("d", path)
.attr("class", "subunit-boundary IRL")
svg.selectAll(".place-label")
.attr("x", function(d) { return d.geometry.coordinates[0] > -1 ? 6 : -6; })
.style("text-anchor", function(d) { return d.geometry.coordinates[0] > -1 ? "start": "end"; });
svg.selectAll(".subunit-label")
.data(topojson.feature(uk, uk.objects.subunits).features)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.id })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; })
// function applyProjection(d) {
// console.log(d);
// prj = projection(d)
// console.log(prj);
// return prj;
// }
lon = -4.6
lat = 55.45
dataPoints.push([lon,lat])
renderPoints()
});
Function to cleanup old points
var cleanupDataPoints = function() {
num_of_elements = dataPoints.length
console.log("Pre:" + num_of_elements)
if(num_of_elements > 10) {
dataPoints = dataPoints.splice(-5, 5)
}
console.log("Post:" + dataPoints.length)
}
Loading data from CSV and plotting at a throttled rate
var bufferedData = null
var ptr = 0
var renderNext = function() {
d = bufferedData[ptr]
console.log(d)
dataPoints.push(d)
ptr++;
renderPoints()
cleanupDataPoints()
if(ptr < bufferedData.length)
setTimeout(renderNext, 100)
}
d3.csv('test.csv', function (error, data) {
bufferedData = data
console.log(data)
setTimeout(renderNext, 100)
})
In the lines
points = svg.selectAll("circle")
.data(dataPoints)
points.enter() (...)
d3 maps each element in dataPoints (indexed from 0 to 5000) to the circle elements (of which there should be 5000 eventually). So from its point of view, there is no enter'ing data: there are enough circles to hold all your points.
To make sure that the same data point is mapped to the same html element after it changed index in its array, you need to use an id field of some sort attached to each of your data point, and tell d3 to use this id to map the data to elements, instead of their index.
points = svg.selectAll("circle")
.data(dataPoints, function(d){return d.id})
If the coordinates are a good identifier for your point, you can directly use:
points = svg.selectAll("circle")
.data(dataPoints, function(d){return d.longitude+" "+d.latitude})
See https://github.com/mbostock/d3/wiki/Selections#data for more details.
Related
I created this chart using D3 V5. Also, I have attached the sample data on the fiddle you can view by clicking here.
I've included the tick function code block which appends new domains for x and y scales and line/data on the path to slide left:
When the tick function executes, the line sort of rebuilds which makes it look like it bounces.
How can it be smooth, without a bounce at all when it rebuilds the line?
var tr = d3
.transition()
.duration(obj.tick.duration)
.ease(d3.easeLinear);
function tick() {
return setInterval(function() {
var newData = [];
var tickFunction = obj.tick.fnTickData;
if (tickFunction !== undefined && typeof tickFunction === "function") {
newData = tickFunction();
for (var i = 0; i < newData.length; i++) {
obj.data.push(newData[i]);
}
}
if (newData.length > 0) {
var newMaxDate, newMinDate, newDomainX;
if (isKeyXDate) {
newMaxDate = new Date(
Math.max.apply(
null,
obj.data.map(function(e) {
return new Date(e[obj.dataKeys.keyX]);
})
)
);
newMinDate = new Date(
Math.min.apply(
null,
obj.data.map(function(e) {
return new Date(e[obj.dataKeys.keyX]);
})
)
);
newDomainX = [newMinDate, newMaxDate];
} else {
newDomainX = [
d3.min(obj.data, function(d) {
return d[obj.dataKeys.keyX];
}),
d3.max(obj.data, function(d) {
return d[obj.dataKeys.keyX];
})
];
}
// update the domains
//x.domain([newMinDate, newMaxDate]);
if (obj.tick.updateXDomain) {
newDomainX = obj.tick.updateXDomain;
}
x.domain(newDomainX);
if (obj.tick.updateYDomain) {
y.domain(obj.tick.updateYDomain);
}
path.attr("transform", null);
// slide the line left
if (obj.area.allowArea) {
areaPath.attr("transform", null);
areaPath
.transition()
.transition(tr)
.attr("d", area);
}
path
.transition()
.transition(tr)
.attr("d", line);
svg
.selectAll(".x")
.transition()
.transition(tr)
.call(x.axis);
svg
.selectAll(".y")
.transition()
.transition(tr)
.call(y.axis);
// pop the old data point off the front
obj.data.shift();
}
}, obj.tick.tickDelay);
}
this.interval = tick();
That bounce is actually the expected result when you transition the d attribute, which is just a string.
There are several solutions here. Without refactoring your code too much, a simple one is using the pathTween function written by Mike Bostock in this bl.ocks: https://bl.ocks.org/mbostock/3916621. Here, I'm changing it a little bit so you can pass the datum, like this:
path.transition()
.transition(tr)
.attrTween("d", function(d) {
var self = this;
var thisd = line(d);
return pathTween(thisd, 1, self)()
})
Here is the forked plunker: https://plnkr.co/edit/aAqpdSb9JozwHsErpqa9?p=preview
As Gerardo notes, transitioning the d attribute of the path won't work very well unless you modfiy the approach. Here's a simple example of the sort of wiggle/bouncing that will arise if simply updating the d attribute of the path:
Pᴏɪɴᴛs ᴛʀᴀɴsɪᴛɪᴏɴɪɴɢ ᴀᴄʀᴏss sᴄʀᴇᴇɴ, ᴡɪᴛʜ ᴘᴀᴛʜ ᴛʀᴀɴsɪᴛɪᴏɴɪɴɢ ғʀᴏᴍ ᴏɴᴇ ᴅᴀᴛᴀ sᴇᴛ ᴛᴏ ᴛʜᴇ ɴᴇxᴛ.
The above behavior is noted by Mike Bostock in a short piece here, and here's a snippet reproducing the above animation:
var n = 10;
var data = d3.range(n).map(function(d) {
return {x: d, y:Math.random() }
})
var x = d3.scaleLinear()
.domain(d3.extent(data, function(d) { return d.x; }))
.range([10,490])
var y = d3.scaleLinear()
.range([290,10]);
var line = d3.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); })
var svg = d3.select("body")
.append("svg")
.attr("width",500)
.attr("height", 400)
.append("g");
var path = svg.append("path")
.datum(data)
.attr("d", line);
var points = svg.selectAll("circle")
.data(data, function(d) { return d.x; })
.enter()
.append("circle")
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.attr("r", 5);
function tick() {
var transition = d3.transition()
.duration(1000);
var newPoint = {x:n++, y: Math.random() };
data.shift()
data.push(newPoint);
x.domain(d3.extent(data,function(d) { return d.x; }))
points = svg.selectAll("circle").data(data, function(d) { return d.x; })
points.exit()
.transition(transition)
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.remove();
points.enter().append("circle")
.attr("cx", function(d) { return x(d.x)+30; })
.attr("cy", function(d) { return y(d.y); })
.merge(points)
.transition(transition)
.attr("cx", function(d) { return x(d.x); })
.attr("r", 5);
path.datum(data)
.transition(transition)
.attr("d", line)
.on("end", tick);
}
tick();
path {
fill: none;
stroke: black;
stroke-width: 2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
One solution to this wiggle/bounce is:
add an additional point(s) to the data,
redraw the line with the recently added to data array
find out the next extent of the data
transition the line to the left
update the scale and transition the axis
remove the first data point(s)
This is also proposed in Mike's article that I've linked to. Here would be a basic implementation with your code:
I've avoided a setInterval function by recursively calling the function at the end of the last transition:
function slide() {
// Stop any ongoing transitions:
d3.selectAll().interrupt();
// A transition:
var transition = d3.transition()
.duration(2000)
.ease(d3.easeLinear)
// 1. add an additional point(s) to the data
var newData = obj.tick.fnTickData();
obj.data.push(...newData);
// 2. redraw the line with the recently added to data array
path.datum(obj.data)
areaPath.datum(obj.data)
// Redraw the graph, without the translate, with less data:
path.attr("transform","translate(0,0)")
.attr("d", line)
areaPath.attr("transform","translate(0,0)")
.attr("d", area)
// 3. find out the next extent of the data
// Assuming data is in chronological order:
var min = obj.data[newData.length][obj.dataKeys.keyX];
var max = obj.data[obj.data.length-1][obj.dataKeys.keyX];
// 4. transition the line to the left
path.datum(obj.data)
.transition(transition)
.attr("transform", "translate("+(-x(new Date(min)))+",0)");
areaPath.datum(obj.data)
.transition(transition)
.attr("transform", "translate("+(-x(new Date(min)))+",0)");
// 5. update the scale and transition the axis
x.domain([new Date(min),new Date(max)])
// Update the xAxis:
svg.selectAll('.x')
.transition(transition)
.call(x.axis)
.on("end",slide); // Trigger a new transition at the end.
// 6. remove the first data point(s)
obj.data.splice(0,newData.length)
}
slide();
Here's an updated plunkr.
I made a map of a state that takes 3 different data sets(2 csv and 1 json) and pumps out a map of the state, with population data per country, and a circle on each major city.
My issue is when I run the code, 2 separate svg elements are created.
If I define the var svg=d3.select() outside the first d3.csv() function, the first svg element on the DOM is blank, and the second SVG elemnt gets the correct map.
If I place the svg=d3.select() inside the first d3.csv() function, both SVG elemnts get the map.
I cannot figure out why or where the second SVG is coming from, or why the code is running twice
The below code has the var svg=d3... inside the d3.csv... Everything on the map works, I removed a lot of filtering to make it easier to read, but I can add the code if you think I need to
var w = 960;
var h = 500;
//define the projection
var projection=d3.geoAlbers()
.translate([w/2, h/2])
.scale([1000]);
//Define path generator, using the Albers USA projection
var path = d3.geoPath()
.projection(projection);
//Create SVG element
//Load in GeoJson Data
var color=d3.scaleQuantize()
.range(['rgb(66,146,198)','rgb(33,113,181)','rgb(8,81,156)','rgb(8,48,107)'])
//load the migration data, which will fill the states
d3.csv("http://127.0.0.1:8000/whyleave/migrations.csv").then(function(data){
color.domain([
d3.min(data, function(d) {return d.exemptions;}),
d3.max(data, function(d) {return d.exemptions;})
]);
data=data.filter(function(d){
return d.State==stateab;})
d3.json("http://127.0.0.1:8000/whyleave/data.json").then(function(json){
var ga=json.features.filter(function(feature){
if (feature.properties.STATE == statenum)
return feature.properties.STATE
})
var state = {
"type": "FeatureCollection",
"features": ga
}
projection.scale(1).translate([0,0])
var b = path.bounds(state),
s = .95 / Math.max((b[1][0] - b[0][0]) / w, (b[1][1] - b[0][1]) / h),
t = [(w - s * (b[1][0] + b[0][0])) / 2, (h - s * (b[1][1] + b[0][1])) / 2];
projection
.scale(s)
.translate(t);
var svg = d3.select("#map")
.append("svg")
.attr("width", w)
.attr("height", h);
//Bind data and create one path per GeoJSON feature
svg.selectAll("path")
.data(state.features)
.enter()
.append('path')
.attr("class", "nation")
.attr("d", path)
.style("stroke", "#fff")
.style("stroke-width", "1")
.style("fill", function(d){
//get data value
var value=d.properties.value;
if (value){ return color(value);}
else{return "rgb(198,219,239)";}
});
d3.csv("http://127.0.0.1:8000/whyleave/cities.csv").then(function(city){
city=city.filter(function(d){
return d.state_id==stateab & d.population > 250000;})
svg.selectAll("circle")
.data(city)
.enter()
.append("circle")
.attr("cx", function(d){
return projection([d.lng, d.lat])[0];
})
.attr("cy", function(d){
return projection([d.lng, d.lat])[1];
})
.attr("r", "5")
.style("fill", "yellow")
.style("stroke", "gray")
.style("stroke-width", 0.25)
.style("opacity", 0.75);
svg.selectAll("text")
.data(city)
.enter()
.append("text")
.attr('class', 'label')
.attr("x", function(d){
return projection([d.lng, d.lat])[0];
})
.attr("y", function(d){
return projection([d.lng, d.lat])[1];})
.text(function(d){
return d.city;
})
.attr("fill", "red");
});
});});
I put the script lines on the html after the body, when I loaded them in the body everything worked fine
I have a d3 pie chart. It reads data from a json file (in this example it's just an array variable).
Some of the key names are not very helpful, so I am trying to write a map function to pass some different strings, but other key names are fine, so I want to skip over those or return them as normal.
If a key name is not declared my function returns undefined. Also the names are not being added to the legend.
I do know all of the key names, but I would rather continue over the ones I have not defined as they can be returned as they are, if this is possible (I thought an else/if with a continue condition). This code would be more useful if new keys are added to the data.
I have tested a version with all key names declared, it stops throwing an undefined variable, but is not appending the new key names to the legend.
There is a full jsfiddle here: https://jsfiddle.net/lharby/ugweordj/6/
Here is my js function (truncated).
var w = 400,
h = 400,
r = 180,
inner = 70,
color = d3.scale.category20c();
data = [{"label":"OOS", "value":194},
{"label":"TH10", "value":567},
{"label":"OK", "value":1314},
{"label":"KO", "value":793},
{"label":"Spark", "value":1929}];
mapData = data.map(function(d){
if(d.label == 'OOS'){
return 'Out of scope';
}else if(d.label == 'TH10'){
return 'Threshold 10';
}else{
// I don't care about the other keys, they can map as they are.
continue;
}
});
var total = d3.sum(data, function(d) {
return d3.sum(d3.values(d));
});
var vis = d3.select("#chart")
.append("svg:svg")
.data([data])
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + r * 1.1 + "," + r * 1.1 + ")")
var arc = d3.svg.arc()
.innerRadius(inner)
.outerRadius(r);
var pie = d3.layout.pie()
.value(function(d) { return d.value; });
var arcs = vis.selectAll("g.slice")
.data(pie)
.enter()
///
};
arcs.append("svg:path")
.attr("fill", function(d, i) { return color(i); } )
.attr("d", arc);
var legend = d3.select("#chart").append("svg")
.attr("class", "legend")
.attr("width", r)
.attr("height", r * 2)
.selectAll("g")
.data(mapData) // returning mapData up until undefined variables
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i) { return color(i); });
legend.append("text")
.attr("x", 24)
.attr("y", 9)
.attr("dy", ".35em")
.text(function(d) { return d.label; }); // should this point to a reference in mapData?
The HTML is a simple element with an id of #chart.
You can use .forEach() to iterate over your data and change just the labels of interest while keeping all others unchanged:
data = [{"label":"OOS", "value":194},
{"label":"TH10", "value":567},
{"label":"OK", "value":1314},
{"label":"KO", "value":793},
{"label":"Spark", "value":1929}];
data.forEach(d => {
if(d.label == 'OOS'){
d.label = 'Out of scope';
} else if(d.label == 'TH10'){
d.label = 'Threshold 10';
}
});
console.log(data);
You cannot skip elements using map. However, you can skip them using reduce:
var data = [
{"label":"OOS", "value":194},
{"label":"TH10", "value":567},
{"label":"OK", "value":1314},
{"label":"KO", "value":793},
{"label":"Spark", "value":1929}
];
var mapData = data.reduce(function(result, d) {
if (d.label == 'OOS') {
result.push('Out of scope');
} else if (d.label == 'TH10') {
result.push('Threshold 10');
};
return result;
}, []);
console.log(mapData)
EDIT: after your comment your desired outcome is clearer. You ca do this using map, just return the property's value:
var data = [{"label":"OOS", "value":194},
{"label":"TH10", "value":567},
{"label":"OK", "value":1314},
{"label":"KO", "value":793},
{"label":"Spark", "value":1929}];
var mapData = data.map(function(d) {
if (d.label == 'OOS') {
return 'Out of scope';
} else if (d.label == 'TH10') {
return 'Threshold 10';
} else {
return d.label
}
});
console.log(mapData);
I'm trying to plot circles from data in my csv file, but the circles are not appearing on the svg canvas. I believe the problem stems from how I load in the data (it gets loaded as an array of objects), but I'm not quite sure how to figure out what to do next.
Based off this tutorial: https://www.dashingd3js.com/svg-text-element
D3.js code:
var circleData = d3.csv("files/data.csv", function (error, data) {
data.forEach(function (d) {
d['KCComment'] = +d['KCComment'];
d['pscoreResult'] = +d['pscoreResult'];
d['r'] = +d['r'];
});
console.log(data);
});
var svg = d3.select("body").append("svg")
.attr("width", 480)
.attr("height", 480);
var circles = svg.selectAll("circle")
.data(circleData)
.enter()
.append("circle");
var circleAttributes = circles
.attr("cx", function (d) { return d.KCComment; })
.attr("cy", function (d) { return d.pscoreResult; })
.attr("r", function (d) { return d.r; })
.style("fill", "green");
var text = svg.selectAll("text")
.data(circleData)
.enter()
.append("text");
var textLabels = text
.attr("x", function(d) { return d.KCComment; })
.attr("y", function(d) { return d.pscoreResult; })
.text(function (d) { return "( " + d.KCComment + ", " + d.pscoreResult + " )"; })
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "red");
What the CSV looks like:
fmname, fmtype, KCComment, pscoreResult, r
test1, type1, 7.1, 8, 39
test2, type2, 1.2, 3, 12
You should have the circle-drawing code within the d3.csv function's callback, so it's only processed when the data is available.
d3.csv("data.csv", function (error, circleData) {
circleData.forEach(function (d) {
d['KCComment'] = +d['KCComment'];
d['pscoreResult'] = +d['pscoreResult'];
d['r'] = +d['r'];
});
console.log(circleData);
// Do the SVG drawing stuff
...
// Finished
});
Also note that instead of setting var circleData = d3.csv(... you should just define it in the callback function.
Here's a plunker with the working code: http://embed.plnkr.co/fzBX0o/preview
You'll be able to see a number of further issues now: both circles are overlapping and only one quarter is visible. That's because your KCComment and pscoreResult values used to define the circles' cx and cy are too small. Try multiplying them up so that the circles move right and down and are a bit more visible! Same is true of the text locations, but I'll leave those problems for you to solve
I'm trying to make a map exactly like this example ( http://bost.ocks.org/mike/map/ ) except focused on Australia and New Zealand.
I've followed the instructions but the dots for the places don't render on my map.
This is how I'm generating my data:
ogr2ogr -f GeoJSON -where "adm0_a3 IN ('AUS', 'NZL')" subunits.json ne_10m_admin_0_map_subunits.shp
ogr2ogr -f GeoJSON -where "(iso_a2 = 'AU' OR iso_a2 = 'NZ') AND SCALERANK < 8" places.json ne_10m_populated_places.shp
topojson --id-property su_a3 -p name=NAME -p name -o aus.json subunits.json places.json
Here is the code I've got so far: http://bashsolutions.com.au/australia.html
The map shows up but the dots for the cities are not displaying. What am I doing wrong?
EDIT: So this isn't very clear just with the big long error so here's the actual code:
<script>
var width = 960,
height = 1160;
//var subunits = topojson.object(aus, aus.objects.subunitsAUS);
var projection = d3.geo.mercator()
//.center([0,0])
.center([180,-40])
.scale(400)
//.translate([width / 2, height / 2])
.precision(.1);
var path = d3.geo.path()
.projection(projection)
.pointRadius(2);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("aus.json", function(error, aus) {
svg.selectAll(".subunit")
.data(topojson.object(aus, aus.objects.subunits).geometries)
.enter().append("path")
.attr("class", function(d) { return "subunit " + d.id; })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(aus, aus.objects.subunits, function(a,b) { return a !== b; }))
.attr("d", path)
.attr("class", "subunit-boundary");
svg.append("path")
.datum(topojson.mesh(aus, aus.objects.subunits, function(a,b) { return a == b; }))
.attr("d", path)
.attr("class", "subunit-boundary External");
/* This is the failing bit */
svg.append("path")
.datum(topojson.object(aus, aus.objects.places))
.attr("class", "place")
.attr("d", path);
/* End of failing bit */
/*
svg.selectAll(".place-label")
.data(topojson.object(aus, aus.objects.places).geometries)
.enter().append("text")
.attr("class", "place-label")
.attr("transform", function(d) { return "translate(" + projection(d.coordinates) + ")"; })
.attr("x", function(d) { return d.coordinates[0] > -1 ? 6 : -6; })
.attr("dy", ".35em")
.style("text-anchor", function(d) { return d.coordinates[0] > -1 ? "start" : "end"; })
.text(function(d) { return d.properties.name; });
*/
});
When you plot the outline you need to remove the TKL (Tokelau) data points.
svg.append("path")
.datum(topojson.mesh(aus, aus.objects.subunits, function(a, b) {
return a === b && a.id !=="TKL" }))
.attr("d", path)
.attr("class", "subunit-boundary External");
I'm still researching why this creates the error, but adding that condition to the mesh function filter seems to fix things.
I found away around this issue that solves the problem but it still doesn't explain why it was failing in the first place.
Here is the fix: http://bashsolutions.com.au/australia2.html
This chunk of code replaces the failing bit above:
svg.selectAll(".place")
.data(topojson.object(aus, aus.objects.places).geometries)
.enter().append("circle")
.attr("d", path)
.attr("transform", function(d) { return "translate(" + projection(d.coordinates) + ")"; })
.attr("r", 2)
.attr("class", "place");
So this gets around it by doing something similar to the labels bit (which is commented out above) but drawing a circle instead of text.
But I'm still not sure what was wrong with the above bit considering it's the same as Mike Bostock's example (apart from the data).