I have made a map which shows some circles and on click, the lines are shown. The map is overlayed on a leaflet basemap.
The map works fine. Here is how I projected the map:
d3.csv("going.csv", function(data) {
going = data;
d3.json("lor_migration.geojson", function(json) {
//Projection
transform = d3.geo.transform({
point: projectPoint
});
path = d3.geo.path().projection(transform);
function projectPoint(x, y) {
var point = map.latLngToLayerPoint(new L.LatLng(y, x));
this.stream.point(point.x, point.y);
}
//Bind data and create one path per GeoJSON feature
var feature = g.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("id", function(d) {
return d.properties.state;
})
.attr("d", path)
.attr("stroke-width", 0.5)
.attr("fill-opacity", "0.5")
.style("stroke", "#666")
.style("fill", "#fff");
map.on("viewreset", reset);
reset();
//Reset function
function reset() {
var bounds = path.bounds(json);
topLeft = bounds[0],
bottomRight = bounds[1];
svg.attr("width", bottomRight[0] - topLeft[0])
.attr("height", bottomRight[1] - topLeft[1])
.style("left", topLeft[0] + "px")
.style("top", topLeft[1] + "px");
g.attr("transform", "translate(" + -topLeft[0] + "," + -topLeft[1] + ")");
feature.attr("d", path);
}
Here is how the circles are drawn on the map:
//Create circles for net migration
var circles = g.selectAll("circle")
.data(json.features)
.enter().append("circle")
.attr("cx", function(d) {
d.centroid = path.centroid(d);
return d.centroid[0];
})
.attr("cy", function(d) {
return d.centroid[1];
})
.attr("r", function(d) { //circle size
console.log("Value: " + value);
var diff = d.properties.total_imm - d.properties.total_emm;
return circleSize((Math.abs(diff)/Math.PI));
})
.attr("class", "circ")
.attr("id", function(d) {
return d.abbrev;
})
.attr("fill", function(d) { //circle color
var diff = d.properties.total_imm - d.properties.total_emm;
if (diff > 0) {
return "#65a89d";
}
else {
return "#a96a46";
}
})
.attr("fill-opacity", "0.5")
.attr("stroke", "#fff")
.attr("stroke-weight", "0.5")
.on("mouseover", function(d) {
return toolOver(d, this);
})
.on("mousemove", function(d) {
var m = d3.mouse(this);
mx = m[0];
my = m[1];
return toolMove(mx, my, d);
})
.on("mouseout", function(d) {
return toolOut(d, this);
})
.on("click", function(d) {
clicked(d)
});
Here is the onclick function for the lines:
function clicked(selected) {
var selname = selected.id;
var homex = selected.centroid[0];
var homey = selected.centroid[1];
g.selectAll(".goingline")
.attr("stroke-dasharray", 0)
.remove()
g.selectAll(".goingline")
.data(going)
.enter().append("path")
.attr("class", "goingline")
.attr("d", function(d, i) {
var finalval = coming[i][selname] - going[i][selname];
come = coming[i][selname];
go = going[i][selname];
try {
var theState = d3.select("#" + d.abbrev).datum();
if (!isNaN(finalval)) {
var startx = theState.centroid[0];
var starty = theState.centroid[1];
if (finalval > 0)
return "M" + startx + "," + starty + " Q" + (startx + homex) / 2 + " " + (starty + homey) / 1.5 + " " + homex + " " + homey;
else
return "M" + homex + "," + homey + " Q" + (startx + homex) / 2 + " " + (starty + homey) / 5 + " " + startx + " " + starty;
}
}
catch (err) {
console.log(err)
console.log('No datum found for ' + d.abbrev)
}
})
.call(transition)
.attr("stroke-width", function(d, i) {
var finalval = coming[i][selname] - going[i][selname];
return 0.75//(Math.abs(finalval));
})
.attr("stroke", function(d, i) {
var finalval = coming[i][selname] - going[i][selname];
if (finalval > 0) {
return "#65a89d";
}
else {
return "#a96a46";
}
})
.attr("fill", "none")
.attr("opacity", 0.5)
.attr("stroke-linecap", "round")
.on("mouseover", function(d) {
return toolOver2(d, this);
})
.on("mousemove", function(d, i) {
var m = d3.mouse(this);
mx = m[0];
my = m[1];
return toolMove2(mx, my, selname, d.state, coming[i][selname], going[i][selname]);
})
.on("mouseout", function(d) {
return toolOut2(d, this);
});
}
The problem is the projection does not work for circles and the lines. Can you please suggest a solution.
Thanks
EDITED:
So this is an attempt I made on trying to fix the issue: There are still some issues
//Create SVG element
var svg = d3.select(map.getPanes().overlayPane)
.append("svg")
var g = svg.append("g").attr("class", "leaflet-zoom-hide");
var svgCircles = svg.append("g").attr("class", "leaflet-zoom-hide");
var coming, going;
var by_name = {};
//Define the coming and going of csv
d3.csv("coming.csv", function(data) {
coming = data;
});
d3.csv("going.csv", function(data) {
going = data;
d3.json("lor_migration.geojson", function(json) {
//Projection
transform = d3.geo.transform({
point: projectPoint
});
path = d3.geo.path().projection(transform);
/* json.features.forEach(function(d) {
d.LatLng = new L.LatLng(d.geometry.coordinates[1], d.geometry.coordinates[0]);
});*/
function projectPoint(x, y) {
var point = map.latLngToLayerPoint(new L.LatLng(y, x));
this.stream.point(point.x, point.y);
}
for (var i = 0; i < data.length; i++) {
by_name[ data[i].state ] = {};
for (var propt in data[i]) {
by_name[ data[i].state ][propt] = +data[i][propt];
}
by_name[ data[i].state ].abbrev = data[i].abbrev;
by_name[ data[i].state ].state = data[i].state;
}
//Find the corresponding state inside the GeoJSON
json.features.forEach( function (j) {
var jsonState = j.properties.name;
if ( by_name[jsonState] ) {
j.properties.state = by_name[jsonState].state;
j.id = by_name[jsonState].state;
j.abbrev = by_name[jsonState].abbrev;
j.properties.total_imm = by_name[jsonState].total_imm;
j.properties.total_emm = by_name[jsonState].total_emm;
}
else {
console.log('No data for ' + jsonState)
}
})
//Bind data and create one path per GeoJSON feature
var feature = g.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("id", function(d) {
return d.properties.state;
})
.attr("d", path)
.attr("stroke-width", 0.5)
.attr("fill-opacity", "0.5")
.style("stroke", "#666")
.style("fill", "#fff");
//Net migration circles
var circles = svgCircles.selectAll("circle")
.data(json.features)
.enter().append("circle")
.attr("cx", function(d) {
d.centroid = path.centroid(d);
return d.centroid[0];
})
.attr("cy", function(d) {
return d.centroid[1];
})
.attr("r", function(d) { //circle size
console.log("Value: " + value);
var diff = d.properties.total_imm - d.properties.total_emm;
// if (diff > 200){
return circleSize((Math.abs(diff)/Math.PI));
// }
})
.attr("class", "circ")
.attr("id", function(d) {
return d.abbrev;
})
.attr("fill", function(d) { //circle color
var diff = d.properties.total_imm - d.properties.total_emm;
if (diff > 0) {
return "#65a89d";
}
else {
return "#a96a46";
}
})
.attr("fill-opacity", "0.5")
.attr("stroke", "#fff")
.attr("stroke-weight", "0.5")
.on("mouseover", function(d) {
return toolOver(d, this);
})
.on("mousemove", function(d) {
var m = d3.mouse(this);
mx = m[0];
my = m[1];
return toolMove(mx, my, d);
})
.on("mouseout", function(d) {
return toolOut(d, this);
})
.on("click", function(d) {
clicked(d)
});
map.on("viewreset", reset);
reset();
//Reset function
function reset() {
var bounds = path.bounds(json);
topLeft = bounds[0],
bottomRight = bounds[1];
svg.attr("width", bottomRight[0] - topLeft[0])
.attr("height", bottomRight[1] - topLeft[1])
.style("left", topLeft[0] + "px")
.style("top", topLeft[1] + "px");
g.attr("transform", "translate(" + -topLeft[0] + "," + -topLeft[1] + ")");
feature.attr("d", path);
circles.attr("cx", function(d) {
return map.latLngToLayerPoint(d.LatLng).x;
});
circles.attr("cy", function(d) {
return map.latLngToLayerPoint(d.LatLng).y;
});
svgCircles.attr("transform", "translate(" + -topLeft[0] + "," + -topLeft[1] + ")");
}
});
});
The problem is if I try to use this function, then nothing shows:
json.features.forEach(function(d) {
d.LatLng = new L.LatLng(d.geometry.coordinates[1],
d.geometry.coordinates[0]);
});
Also I am getting the error:
TypeError: t is undefined, can't access property "lat" of it
Help please
So I fixed the circles at least. The problem was I was not returning the correct attributes. Should'Ve returned the centroids and not log lat. Here is the updated version of my reset function:
//Reset function
function reset() {
//Defining Bounds
var bounds = path.bounds(json);
topLeft = bounds[0],
bottomRight = bounds[1];
svg.attr("width", bottomRight[0] - topLeft[0])
.attr("height", bottomRight[1] - topLeft[1])
.style("left", topLeft[0] + "px")
.style("top", topLeft[1] + "px");
//Recalculating Projections for Json map
g.attr("transform", "translate(" + -topLeft[0] + "," + -topLeft[1] + ")");
feature.attr("d", path);
//Recalculating Projections for Circles
circles.attr("cx", function(d) {
d.centroid = path.centroid(d);
return d.centroid[0];
});
circles.attr("cy", function(d) {
return d.centroid[1];
});
g.attr("transform", "translate(" + -topLeft[0] + "," + -topLeft[1] + ")");
circles.attr("d", path);
}
I still need to figure out the projection of the lines. Any suggestions on that?
Related
Hope someone can help with my first D3 visualisation. I've got two datasets that I've loaded in as separate selections of group elements. I'm trying to simulate forces on the bubbles however I can only get one of the groups to function at once. I have a feeling it's something to do with calling d3.forceSimulation multiple times but I can't figure out a workaround. Any help appreciated! (Apologies for sloppy coding, not been in the game long! Github repo: https://github.com/skemp456/D3)
Visualisation (middle group (features) doesn't work)
function createBubbleChart(error, features, dummydata) {
var features_f = features.map(function(d) { return d.Feature});
var features_t = d3.set(dummydata.map(function(tech) { return tech.Feature; }));
var importance = {'Tertiary':30,'Secondary':45,'Critical':60},
//importanceExtent = d3.extent(importance.values),
importanceScaleX,
importanceScaleY;
var categories = d3.set(features.map(function(d) { return d.Category}));
var catColourScale = d3.scaleOrdinal()
.domain(categories.values())
.range(["#1092E1", "#E13610", "#11B40C"])
var featureColourScale = d3.scaleOrdinal(d3.schemeCategory10)
.domain(features.values());
var columns = d3.set(['Technology - Networks',
'Technology - Apps & Devices and Security & Privacy',
'Internal to the Community',
'Legislation, Standards, Economic, Political, Social']);
var timeScales = d3.set(['Now', 'Now - 6 months', '6 Months - 1 Year', '1 Year - 3 Years', '3 Years +'])
var impact = dummydata.map(function(tech) { return tech.Impact; }),
impactExtent = d3.extent(impact),
impactScaleX,
impactScaleY;
var width = 1200,
height = 800;
var svg,
circles,
f_circles,
t_circles,
circleSize = {min: 30, max:60},
clusterPadding = 6
var circleRadiusScale_t = d3.scaleSqrt()
.domain(impactExtent)
.range([circleSize.min, circleSize.max]);
var circleRadiusScale_f = d3.scaleSqrt()
.domain(importance)
.range([circleSize.min, circleSize.max])
var tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.style("max-width", "300px")
.style("color", "white")
.style("padding", "8px")
.style("background-color", "rgba(0, 0, 0, 0.75)")
.style("border-radius", "6px")
.style("font", "12px sans-serif")
.text("tooltip");
var forces,
forceSimulation,
forceSimulation2;
//Call functions here
createSVG();
createFCircles();
createTCircles();
createForces();
createForceSimulation();
addGroupingListeners();
/////////////////////////////////
//---------Functions-----------//
/////////////////////////////////
//Function to create the simple vector graphic space
function createSVG() {
svg = d3.select("#bubble-chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("class", "bubble");
}
//Function to determine if a html element is checked
function isChecked(elementID) {
return d3.select(elementID).property("checked");
}
//Create feature circles
function createFCircles() {
f_circles = svg.selectAll("f_circle")
.data(features)
.enter()
.append("g")
.attr("class", "f_circle")
f_circles
.append("circle")
.attr("cursor", "pointer")
.attr("r", function(d) { return importance[d.Importance]; })
.style("fill", function(d) {return catColourScale(d.Category);})
.on("mouseover", function(d, i) {
d3.select(this).transition()
.ease(d3.easeElastic)
.duration("500")
.attr("r", importance[d.Importance]+20);
d3.select("#clipCircle"+i+" circle").transition()
.ease(d3.easeCubicOut)
.duration("200")
.attr("r", importance[d.Importance]+10);
d3.select("#text"+i).transition()
.ease(d3.easeCubicOut)
.duration("200")
.attr("y", 12)
.attr("font-size", 32)
.attr("fill", "#333");
})
.on("mouseout", function(d, i) {
d3.select(this).transition()
.ease(d3.easeQuad)
.delay("10")
.duration("200")
.attr("r", importance[d.Importance]);
d3.select("#clipCircle"+i+" circle").transition()
.ease(d3.easeQuad)
.delay("10")
.duration("200")
.attr("r", 0);
d3.select("#text"+i).transition()
.ease(d3.easeCubicOut)
.duration("400")
.delay("10")
.attr("y", 7)
.attr("font-size", 20)
.attr("fill", "#FFF");
});
f_circles.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.style("pointer-events", "none")
.text(function(d){ return d.Feature; });
}
//Create Technology Circles
function createTCircles() {
t_circles = svg.selectAll("t_circle")
.data(dummydata)
.enter()
.append("g")
.attr("class", "t_circle")
t_circles
.append("circle")
.attr("cursor", "pointer")
.attr("r", function(d) {return circleRadiusScale_t(d.Impact); })
.style("fill", function(d) {return featureColourScale(d.Feature);})
.on("mouseover", function(d, i) {
d3.select(this).transition()
.ease(d3.easeElastic)
.duration("500")
.attr("r", function(d) {return circleRadiusScale_t(d.Impact)+10; });
d3.select("#clipCircle"+i+" circle").transition()
.ease(d3.easeCubicOut)
.duration("200")
.attr("r", function(d) {return circleRadiusScale_t(10); });
d3.select("#text"+i).transition()
.ease(d3.easeCubicOut)
.duration("200")
.attr("y", 12)
.attr("font-size", 32)
.attr("fill", "#333");
tooltip.text(d.Description);
tooltip.style("visibility", "visible");
})
.on("mousemove", function() {
return tooltip.style("top", (d3.event.pageY-10)+"px").style("left",(d3.event.pageX+10)+"px");
})
.on("mouseout", function(d, i) {
d3.select(this).transition()
.ease(d3.easeQuad)
.delay("10")
.duration("200")
.attr("r", function(d) {return circleRadiusScale_t(d.Impact); });
d3.select("#clipCircle"+i+" circle").transition()
.ease(d3.easeQuad)
.delay("10")
.duration("200")
.attr("r", 0);
d3.select("#text"+i).transition()
.ease(d3.easeCubicOut)
.duration("400")
.delay("10")
.attr("y", 7)
.attr("font-size", 20)
.attr("fill", "#FFF");
return tooltip.style("visibility", "hidden");
});
t_circles.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.style("pointer-events", "none")
.text(function(d){ return d.Technology; });
}
function createForces() {
var forceStrength = 0.15;
forces = {
features: createFeaturesForces(),
cases: createCasesForces(),
timeScales: createTimeScalesForces()
};
function createFeaturesForces() {
return {
x: d3.forceX(featureForceX).strength(forceStrength),
y: d3.forceY(featureForceY).strength(forceStrength)
};
function featureForceX(d) {
if ("Importance" in d) {
return (width /2);
}else{
return (width + 1000);
}
}
function featureForceY(d) {
if ("Importance" in d) {
return (height /2);
}else{
return (height /2);
}
}
}
function createCasesForces() {
return {
x: d3.forceX(casesForceX).strength(forceStrength),
y: d3.forceY(casesForceY).strength(forceStrength)
};
function casesForceX(d) {
if ("Technology" in d) {
return (width /2);
}else{
return (width-1000);
}
}
function casesForceY(d) {
if ("Technology" in d) {
return (height /2);
}else{
return (height / 2);
}
}
}
function createTimeScalesForces() {
var columnNamesDomain = columns.values();
var timeScalesDomain = timeScales.values();
var scaledTimeMargin = circleSize.max;
timeScaleX = d3.scaleBand()
.domain(columnNamesDomain)
.range([scaledTimeMargin, width - scaledTimeMargin*2]);
timeScaleY = d3.scaleBand()
.domain(timeScalesDomain)
.range([height - scaledTimeMargin, scaledTimeMargin*2]);
var centerCirclesInScaleBandOffset = timeScaleX.bandwidth() / 2;
return {
x: d3.forceX(function(d) {
if ("Technology" in d){
return timeScaleX(d.Column) + centerCirclesInScaleBandOffset;
}else{
return (0 - width/2);
}
}).strength(forceStrength),
y: d3.forceY(function(d) {
if("Technology" in d){
return timeScaleY(d.Timescale);
}else{
return (0 - height/2);
}
}).strength(forceStrength)
};
}
}
function createForceSimulation() {
//Create forces for cases
forceSimulation_t = d3.forceSimulation();
forceSimulation_t
.force("x", forces.features.x)
.force("y", forces.features.y)
.force("collide", d3.forceCollide(forceCollide));
forceSimulation_t
.nodes(dummydata)
.on("tick", function() {
t_circles
.attr("transform", function(d) {return "translate(" + d.x + "," + d.y + ")"});
});
//Create forces for features
forceSimulation_f = d3.forceSimulation();
forceSimulation_f
.force("x", forces.features.x)
.force("y", forces.features.y)
.force("collide", d3.forceCollide(forceCollide));
forceSimulation_f
.nodes(features)
.on("tick", function() {
f_circles
.attr("transform", function(d) {return "translate(" + d.x + "," + d.y + ")"});
});
}
function forceCollide(d) {
if ("Technology" in d){
return circleRadiusScale_t(d.Impact)+2;
}else{
return importance[d.Importance]+2;
}
}
function featureGrouping() {
return isChecked("#Features");
}
function casesGrouping() {
return isChecked("#Cases")
}
function timeScaleGrouping() {
return isChecked("#Time-Scales");
}
function addGroupingListeners() {
addListener("#Features", forces.features);
addListener("#Cases", forces.cases);
addListener("#Time-Scales", forces.timeScales);
function addListener(selector, forces) {
d3.select(selector).on("click", function() {
updateForces_t(forces);
updateForces_f(forces);
toggleTimeScaleAxis(timeScaleGrouping());
//toggleFeaturesKey();
});
}
function updateForces_t(forces) {
forceSimulation_t
.force("x", forces.x)
.force("y", forces.y)
.force("collide", d3.forceCollide(forceCollide))
.alphaTarget(0.5)
.restart();
}
function updateForces_f() {
forceSimulation_f
.force("x", forces.x)
.force("y", forces.y)
.force("collide", d3.forceCollide(forceCollide))
.alphaTarget(0.5)
.restart();
}
function toggleTimeScaleAxis(showAxes) {
var onScreenXOffset = 100,
offScreenXOffset = -40;
var onScreenYOffset = 120,
offScreenYOffset = 100;
if (d3.select(".x-axis").empty()) {
createAxes();
}
var xAxis = d3.select(".x-axis"),
yAxis = d3.select(".y-axis");
if (showAxes) {
translateAxis(xAxis, "translate(20," + (height - onScreenYOffset) + ")");
translateAxis(yAxis, "translate(" + onScreenXOffset + ",-50)");
} else {
translateAxis(xAxis, "translate(0," + (height + offScreenYOffset) + ")");
translateAxis(yAxis, "translate(" + offScreenXOffset + ",0)");
}
function createAxes() {
var numberOfTicks = 10,
tickFormat = ".0s";
var xAxis = d3.axisBottom(timeScaleX)
.ticks(numberOfTicks, tickFormat);
svg.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + (height + offScreenYOffset) + ")")
.call(xAxis)
.selectAll(".tick text")
.attr("font-size", "16px");
var yAxis = d3.axisLeft(timeScaleY)
.ticks(numberOfTicks, tickFormat);
svg.append("g")
.attr("class", "y-axis")
.attr("transform", "translate(" + offScreenXOffset + ",0)")
.call(yAxis);
}
function translateAxis(axis, translation) {
axis
.transition()
.duration(100)
.attr("transform", translation);
}
}
}
}
You must update the transform of the enclosing g in the force tick not the cx and cy.
t_circles
.attr("transform", function(d) {return `translate(${d.x},${d.y})`;})
// .attr("cx", function(d) {return d.x;})
// .attr("cy", function(d) {return d.y;});
You must also add the features objects to the force simulation, now only the Case nodes are animated and updated.
Set a new alpha value to start the force again, otherwise the sim will never end.
function updateForces(forces) {
forceSimulation
.force("x", forces.x)
.force("y", forces.y)
.force("collide", d3.forceCollide(forceCollide))
// .alphaTarget(0.5)
.alpha(0.5)
.restart();
}
Edit
The root cause of the immobile nodes is the fact that you are not allowed to share forces between simulations. Reason: a force keeps track of the nodes used in the simulation. You are allowed to share the accessor functions.
Also the position is changed for some cases.
function createForces() {
var forceStrength = 0.15;
forces = {
features: createFeaturesForces(),
cases: createCasesForces(),
timeScales: createTimeScalesForces()
};
function createFeaturesForces() {
return {
x: d3.forceX(featurePositionX).strength(forceStrength),
y: d3.forceY(featurePositionY).strength(forceStrength),
xpos: featurePositionX,
ypos: featurePositionY
};
function featurePositionX(d) {
if ("Importance" in d) {
return (width /2);
}
return (width + 1000);
}
function featurePositionY(d) {
if ("Importance" in d) {
return (height /2);
}
return (height /2);
}
}
function createCasesForces() {
return {
x: d3.forceX(casesPositionX).strength(forceStrength),
y: d3.forceY(casesPositionY).strength(forceStrength),
xpos: casesPositionX,
ypos: casesPositionY
};
function casesPositionX(d) {
if ("Technology" in d) {
return (width /2);
}
return (0 - 1000);
}
function casesPositionY(d) {
if ("Technology" in d) {
return (height /2);
}
return (height / 2);
}
}
function createTimeScalesForces() {
var columnNamesDomain = columns.values();
var timeScalesDomain = timeScales.values();
var scaledTimeMargin = circleSize.max;
timeScaleX = d3.scaleBand()
.domain(columnNamesDomain)
.range([scaledTimeMargin, width - scaledTimeMargin*2]);
timeScaleY = d3.scaleBand()
.domain(timeScalesDomain)
.range([height - scaledTimeMargin, scaledTimeMargin*2]);
var centerCirclesInScaleBandOffset = timeScaleX.bandwidth() / 2;
return {
x: d3.forceX().strength(forceStrength),
y: d3.forceY().strength(forceStrength),
xpos: timePositionX,
ypos: timePositionY
};
function timePositionX(d) {
if ("Technology" in d){
return timeScaleX(d.Column) + centerCirclesInScaleBandOffset;
}
return (0 - width/2);
}
function timePositionY(d) {
if("Technology" in d){
return timeScaleY(d.Timescale);
}
return height/2;
}
}
}
function createForceSimulation() {
//Create forces for cases
forceSimulation_t = d3.forceSimulation();
forceSimulation_t
.force("x", forces.cases.x)
.force("y", forces.cases.y)
.force("collide", d3.forceCollide(forceCollide));
forceSimulation_t
.nodes(dummydata)
.on("tick", function() {
t_circles
.attr("transform", function(d) {return "translate(" + d.x + "," + d.y + ")"});
});
// we use the wrong position functions so update
updateForces(forceSimulation_t, forces.features);
//Create forces for features
forceSimulation_f = d3.forceSimulation();
forceSimulation_f
.force("x", forces.features.x)
.force("y", forces.features.y)
.force("collide", d3.forceCollide(forceCollide));
forceSimulation_f
.nodes(features)
.on("tick", function() {
f_circles
.attr("transform", function(d) {return "translate(" + d.x + "," + d.y + ")"});
});
}
function forceCollide(d) {
if ("Technology" in d){
return circleRadiusScale_t(d.Impact)+2;
}else{
return importance[d.Importance]+2;
}
}
function featureGrouping() {
return isChecked("#Features");
}
function casesGrouping() {
return isChecked("#Cases")
}
function timeScaleGrouping() {
return isChecked("#Time-Scales");
}
function updateForces(forceSim, forces) {
forceSim.force("x").x(forces.xpos);
forceSim.force("y").y(forces.ypos);
forceSim.alpha(0.5).restart();
}
function addGroupingListeners() {
addListener("#Features", forces.features);
addListener("#Cases", forces.cases);
addListener("#Time-Scales", forces.timeScales);
function addListener(selector, forces) {
d3.select(selector).on("click", function() {
updateForces(forceSimulation_t, forces);
updateForces(forceSimulation_f, forces);
toggleTimeScaleAxis(timeScaleGrouping());
//toggleFeaturesKey();
});
}
function toggleTimeScaleAxis(showAxes) {
var onScreenXOffset = 100,
offScreenXOffset = -40;
var onScreenYOffset = 120,
offScreenYOffset = 100;
if (d3.select(".x-axis").empty()) {
createAxes();
}
var xAxis = d3.select(".x-axis"),
yAxis = d3.select(".y-axis");
if (showAxes) {
translateAxis(xAxis, "translate(20," + (height - onScreenYOffset) + ")");
translateAxis(yAxis, "translate(" + onScreenXOffset + ",-50)");
} else {
translateAxis(xAxis, "translate(0," + (height + offScreenYOffset) + ")");
translateAxis(yAxis, "translate(" + offScreenXOffset + ",0)");
}
function createAxes() {
var numberOfTicks = 10,
tickFormat = ".0s";
var xAxis = d3.axisBottom(timeScaleX)
.ticks(numberOfTicks, tickFormat);
svg.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + (height + offScreenYOffset) + ")")
.call(xAxis)
.selectAll(".tick text")
.attr("font-size", "16px");
var yAxis = d3.axisLeft(timeScaleY)
.ticks(numberOfTicks, tickFormat);
svg.append("g")
.attr("class", "y-axis")
.attr("transform", "translate(" + offScreenXOffset + ",0)")
.call(yAxis);
}
function translateAxis(axis, translation) {
axis
.transition()
.duration(100)
.attr("transform", translation);
}
}
}
I am working on a city to city migration visualization and I am stuck with a problem... I want to display my map on top of the leaflet basemap. The problem is that the leaflet map zoom in and out but the SVG created is static. I tried implementing a lot of examples but I am stuck... Here is my code:
var map = L.map('map', {
center: [52.52, 13.5],
zoom: 11,
zoomControl: true
});
mapLink = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMapcontributors'
}).addTo(map);
// Add an SVG element to Leaflet’s overlay pane
var w = 950;
var h = 600;
var centered;
var formatC = d3.format(",.0f");
var formatD = d3.format("+,.0f");
var immin, immax, exmin, exmax;
var colors = ["#EDF8FB", "#41083E"];
var immdomain = [24431, 537148];
var emmdomain = [20056, 566986];
var circleSize = d3.scale.linear().range([0, 25000]).domain([0, 137175]);
var lineSize = d3.scale.linear().range([2, 25]).domain([0, 35000]);
var fillcolor = d3.scale.linear().range(colors).domain(immdomain);
//Define path generator
var path = d3.geo.path()
//Create SVG element
var svg = d3.select(map.getPanes().overlayPane)
.append("svg")
.attr("width", w)
.attr("height", h)
.style("class", "background");
var fp = d3.format(".1f");
var g = svg.append("g").attr("class", "leaflet-zoom-hide");
// This is the function that transforms D3 svg output to the correct leaflet projection
function projectPoint(x, y) {
var point = map.latLngToLayerPoint(new L.LatLng(y, x));
this.stream.point(point.x, point.y);
}
//initialize html tooltip
var tooltip = d3.select("#maincontainer")
.append("div")
.attr("id", "tt")
.style("z-index", "10")
.style("position", "absolute")
.style("visibility", "hidden");
var tooltip2 = d3.select("#maincontainer")
.append("div")
.attr("id", "tt2")
.style("z-index", "10")
.style("position", "absolute")
.style("visibility", "hidden");
var coming, going;
d3.csv("coming.csv", function(data) {
coming = data;
});
d3.csv("going.csv", function(data) {
going = data;
d3.json("test.json", function(json) {
var bounds = d3.geo.bounds(json)
bottomLeft = bounds[0],
topRight = bounds[1],
rotLong = -(topRight[0]+bottomLeft[0])/2;
center = [(topRight[0]+bottomLeft[0])/2+rotLong, (topRight[1]+bottomLeft[1])/2],
//default scale projection
projection = d3.geo.albers()
.parallels([bottomLeft[1],topRight[1]])
.rotate([rotLong,0,0])
.translate([w/2,h/2])
.center(center),
bottomLeftPx = projection(bottomLeft),
topRightPx = projection(topRight),
scaleFactor = 1.00*Math.min(w/(topRightPx[0]-bottomLeftPx[0]), h/(-topRightPx[1]+bottomLeftPx[1])),
projection = d3.geo.albers()
.parallels([bottomLeft[1],topRight[1]])
.rotate([rotLong,0,0])
.translate([w/2,h/2])
.scale(scaleFactor*0.975*1000)
.center(center);
path = d3.geo.path()
.projection(projection);
for (var i = 0; i < data.length; i++) {
var dataName = data[i].state;
var tempObj = {};
for (var propt in data[i]) {
var valz = parseFloat(data[i][propt]);
tempObj[propt] = valz;
}
//Find the corresponding state inside the GeoJSON
for (var j = 0; j < json.features.length; j++) {
var jsonState = json.features[j].properties.name;
if (dataName == jsonState) {
matched = true;
json.features[j].properties.state = dataName;
json.features[j].id = dataName;
json.features[j].abbrev = data[i].abbrev;
json.features[j].ind = i;
for (var propt in tempObj) {
if (!isNaN(tempObj[propt])) {
json.features[j].properties[propt] = tempObj[propt];
}
}
break;
}
}
}
//Bind data and create one path per GeoJSON feature
g.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("class", "state")
.attr("id", function(d) {
return d.properties.state;
})
.attr("d", path)
.attr("stroke-width", 0.5)
.style("stroke", "#666")
.style("fill", "#fff");
g.selectAll("circle")
.data(json.features)
.enter().append("circle")
.attr("cx", function(d) {
var centname = d.properties.name;
var ctroid;
ctroid = path.centroid(d)[0];
return ctroid;
})
.attr("cy", function(d) {
var centname = d.properties.name;
var ctroid;
ctroid = path.centroid(d)[1];
return ctroid;
})
.attr("r", function(d) {
var diff = d.properties.total_imm - d.properties.total_emm;
return circleSize(Math.sqrt(Math.abs(diff) / Math.PI));
})
.attr("class", "circ")
.attr("id", function(d) {
return d.abbrev;
})
.attr("fill", function(d) {
var diff = d.properties.total_imm - d.properties.total_emm;
if (diff > 0) {
return "#65a89d";
}
else {
return "#a96a46";
}
})
.attr("fill-opacity", "0.5")
.attr("stroke", "#fff")
.attr("stroke-weight", "0.5")
.on("mouseover", function(d) {
return toolOver(d, this);
})
.on("mousemove", function(d) {
var m = d3.mouse(this);
mx = m[0];
my = m[1];
return toolMove(mx, my, d);
})
.on("mouseout", function(d) {
return toolOut(d, this);
})
.on("click", function(d) {
clicked(d)
});
});
});
function toolOver(v, thepath) {
d3.select(thepath).style({
"fill-opacity": "0.7",
"cursor": "pointer"
});
return tooltip.style("visibility", "visible");
};
function toolOut(m, thepath) {
d3.select(thepath).style({
"fill-opacity": "0.5",
"cursor": ""
});
return tooltip.style("visibility", "hidden");
};
function toolMove(mx, my, data) {
if (mx < 120) {
mx = 120
};
if (my < 40) {
my = 40
};
return tooltip.style("top", my + -140 + "px").style("left", mx - 120 + "px").html("<div id='tipContainer'><div id='tipLocation'><b>" + data.id + "</b></div><div id='tipKey'>Migration in: <b>" + formatC(data.properties.total_imm) + "</b><br>Migration out: <b>" + formatC(data.properties.total_emm) + "</b><br>Net migration: <b>" + formatC((data.properties.total_imm - data.properties.total_emm)) + "</b></div><div class='tipClear'></div> </div>");
};
function toolOver2(v, thepath) {
d3.select(thepath).style({
"opacity": "1",
"cursor": "pointer"
});
return tooltip2.style("visibility", "visible");
};
function toolOut2(m, thepath) {
d3.select(thepath).style({
"opacity": "0.5",
"cursor": ""
});
return tooltip2.style("visibility", "hidden");
};
function toolMove2(mx, my, home, end, v1, v2) {
var diff = v1 - v2;
if (mx < 120) {
mx = 120
};
if (my < 40) {
my = 40
};
return tooltip2.style("top", my + -140 + "px").style("left", mx - 120 + "px").html("<div id='tipContainer2'><div id='tipLocation'><b>" + home + "/" + end + "</b></div><div id='tipKey2'>Migration, " + home + " to " + end + ": <b>" + formatC(v2) + "</b><br>Migration, " + end + " to " + home + ": <b>" + formatC(v1) + "</b><br>Net change, " + home + ": <b>" + formatD(v1 - v2) + "</b></div><div class='tipClear'></div> </div>");
};
function clicked(selected) {
var selname = selected.id;
var homex = path.centroid(selected)[0];
var homey = path.centroid(selected)[1];
g.selectAll(".goingline")
.attr("stroke-dasharray", 0)
.remove()
g.selectAll(".goingline")
.data(going)
.enter().append("path")
.attr("class", "goingline")
.attr("d", function(d, i) {
var abb = d.abbrev;
var finalval = coming[i][selname] - going[i][selname];
var theState = d3.select("#" + abb);
if (!isNaN(finalval)) {
var startx = path.centroid(theState[0][0].__data__)[0];
var starty = path.centroid(theState[0][0].__data__)[1];
if (finalval > 0) {
return "M" + startx + "," + starty + " Q" + (startx + homex) / 2 + " " + (starty + homey) / 1.5 + " " + homex + " " + homey;
}
else {
return "M" + homex + "," + homey + " Q" + (startx + homex) / 2 + " " + (starty + homey) / 2.5 + " " + startx + " " + starty;
}
}
})
.call(transition)
.attr("stroke-width", function(d, i) {
var finalval = coming[i][selname] - going[i][selname];
return lineSize(parseFloat(Math.abs(finalval)));
})
.attr("stroke", function(d, i) {
var finalval = coming[i][selname] - going[i][selname];
if (finalval > 0) {
return "#65a89d";
}
else {
return "#a96a46";
}
})
.attr("fill", "none")
.attr("opacity", 0.5)
.attr("stroke-linecap", "round")
.on("mouseover", function(d){
return toolOver2(d, this);
})
.on("mousemove", function(d, i){
var m = d3.mouse(this);
mx = m[0];
my = m[1];
return toolMove2(mx, my, selname, d.state, coming[i][selname], going[i][selname]);
})
.on("mouseout", function(d) {
return toolOut2(d, this);
});
}
function transition(path) {
path.transition()
.duration(1500)
.attrTween("stroke-dasharray", tweenDash);
}
function tweenDash() {
var l = this.getTotalLength(),
i = d3.interpolateString("0," + l, l + "," + l);
return function(t) {
return i(t);
};
}
I am still very new to d3 js. I am sure my problem is not a big one but really need help on this.
I have prototype code I am working with here:
jsfiddle
The example shows how to add a segment when new data is added but not how to remove it again when the data changes [back]. I am fairly new to d3 and don't quite get the exit() function yet...
if you reverse the initial and second dataset you will see that the grapes segment is never removed. Thanks in advance!
any help would be great!
The update code: (my final chart needs to update on a timer when data changes)
var arcs = arc_grp.selectAll("path")
.data(donut(data));
arcs.enter()
.append("path")
.attr("stroke", "white")
.attr("stroke-width", 0.8)
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { return this.current = d; });
var sliceLabel = label_group.selectAll("text")
.data(donut(data));
sliceLabel.enter()
.append("text")
.attr("class", "arcLabel")
.attr("transform", function(d) { return "translate(" + (arc.centroid(d)) +
")"; })
.attr("text-anchor", "middle")
.style("fill-opacity", function(d) {
if (d.value === 0) { return 1e-6; }
else { return 1; }
})
.text(function(d) { return d.data.label; });
};
To remove elements without data, you have to use exit(), which...
Returns the exit selection: existing DOM elements in the selection for which no new datum was found.
So, inside your updateChart function, you need an exit selection for both paths and texts:
var newArcs = arcs.data(donut(dataset));
newArcs.exit().remove();
var newSlices = sliceLabel.data(donut(dataset));
newSlices.exit().remove();
Here is your updated code:
// Setup all the constants
var duration = 500;
var width = 500
var height = 300
var radius = Math.floor(Math.min(width / 2, height / 2) * 0.9);
var colors = ["#d62728", "#ff9900", "#004963", "#3497D3"];
// Test Data
var d2 = [{
label: 'apples',
value: 20
}, {
label: 'oranges',
value: 50
}, {
label: 'pears',
value: 100
}];
var d1 = [{
label: 'apples',
value: 100
}, {
label: 'oranges',
value: 20
}, {
label: 'pears',
value: 20
}, {
label: 'grapes',
value: 20
}];
// Set the initial data
var data = d1
var updateChart = function(dataset) {
var newArcs = arcs.data(donut(dataset));
newArcs.exit().remove();
newArcs.enter()
.append("path")
.attr("stroke", "white")
.attr("stroke-width", 0.8)
.attr("fill", function(d, i) {
return color(i);
})
.attr("d", arc);
newArcs.transition()
.duration(duration)
.attrTween("d", arcTween);
var newSlices = sliceLabel.data(donut(dataset));
newSlices.exit().remove();
newSlices.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + (arc.centroid(d)) + ")";
})
.style("fill-opacity", function(d) {
if (d.value === 0) {
return 1e-6;
} else {
return 1;
}
});
sliceLabel.data(donut(dataset)).enter()
.append("text")
.attr("class", "arcLabel")
.attr("transform", function(d) {
return "translate(" + (arc.centroid(d)) + ")";
})
.attr("text-anchor", "middle")
.style("fill-opacity", function(d) {
if (d.value === 0) {
return 1e-6;
} else {
return 1;
}
})
.text(function(d) {
return d.data.label;
});
};
var color = d3.scale.category20();
var donut = d3.layout.pie()
.sort(null)
.value(function(d) {
return d.value;
});
var arc = d3.svg.arc()
.innerRadius(radius * .4)
.outerRadius(radius);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var arc_grp = svg.append("g")
.attr("class", "arcGrp")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")");
var label_group = svg.append("g")
.attr("class", "lblGroup")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")");
var arcs = arc_grp.selectAll("path")
.data(donut(data));
arcs.enter()
.append("path")
.attr("stroke", "white")
.attr("stroke-width", 0.8)
.attr("fill", function(d, i) {
return color(i);
})
.attr("d", arc)
.each(function(d) {
return this.current = d;
});
var sliceLabel = label_group.selectAll("text")
.data(donut(data));
sliceLabel.enter()
.append("text")
.attr("class", "arcLabel")
.attr("transform", function(d) {
return "translate(" + (arc.centroid(d)) + ")";
})
.attr("text-anchor", "middle")
.style("fill-opacity", function(d) {
if (d.value === 0) {
return 1e-6;
} else {
return 1;
}
})
.text(function(d) {
return d.data.label;
});
// Update the data
setInterval(function(model) {
return updateChart(d2);
}, 2000);
// Tween Function
var arcTween = function(a) {
var i = d3.interpolate(this.current, a);
this.current = i(0);
return function(t) {
return arc(i(t));
};
};
.arcLabel {
font: 10px sans-serif;
fill: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I have created a bubble chart with zoom feature on JSfiddle
var r = 500,
h = 500,
format = d3.format(",d"),
fill = d3.scale.category20c();
var bubble = d3.layout.pack().sort(null).size([r,h]);
var vis = d3.select("#chart").append("svg")
.attr("class", "bubble")
.call(d3.behavior.zoom().on("zoom", redraw))
.append("g").attr("class", "group2")
d3.json("cantGetRidOfThis", function() {
var node = vis.selectAll("g.node")
.data(bubble.nodes(flat).filter(function(d) {return !d.children;}))
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {return "translate(" + d.x + "," + d.y + ")";});
node.append("title").text(function(d) {return d.className + ": " + format(d.value);});
node.append("circle")
.attr("r", function(d) {return d.r;})
.attr("class", "nodecircle")
.style("fill", '#ff4719')
.attr("data-classname", function(d) {return d.className;});
node.append("text")
.attr("text-anchor", "middle")
.attr("class", "nodetext")
.attr("data-classname", function(d) {return d.className;})
.attr("style", function(d) {return "font-size:" + d.r/5;})
.attr("data-classname", function(d) {return d.className;})
.each(function(d, i) {
var nm = d.className;
var arr = nm.replace(/[\(\)\\/,-]/g, " ").replace(/\s+/g, " ").split(" "),arrlength = (arr.length > 7) ? 8 : arr.length;
d3.select(this).attr('y',"-" + (arrlength/2) + "em");
//if text is over 7 words then ellipse the 8th
for(var n = 0; n < arrlength; n++) {
var tsp = d3.select(this).append('tspan').attr("x", "0").attr("dy", "1em").attr("data-classname", nm);
if(n === 7) {
tsp.text("...");
} else {
tsp.text(arr[n]);
}
}
});
});
function clickOnCircleFunc(el){
var selection = el.target.__data__.className;
//bubble select
$('.nodecircle').each(function (id, v) {
var $this = $(this),d_nm = $this.attr('data-classname');
if (d_nm === selection && $this.attr('data-selected') !== 'y') {
$this.attr('data-selected','y').css('fill', '#3182bd');
}
else {
$this.attr('data-selected','n').css('fill', '#ff4719');
}
});
}
function redraw() {
vis.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
}
Above code is working fine with all what needed but one thing which am not able to figure out is to populate the chart in centre of the div id="chart".Currently chart is displayed on left .
How can the chart be populate in centre of the div
I need to draw a tree with d3.js that has two "roots" and that is "force-directed" The json of this graph (very simple, it's just a trial) is:
{
"nodes":
[
{"name":"node1",
"children":[
{"name":"node2", "children":[{"name":"node3"}, {"name":"node4"}]},
{"name":"node5", "children":[{"name":"node6", "children":[{"name":"node8"}]}, {"name":"node7"}]}
]},
{"name":"node9",
"children":[
{"name":"node2", "children":[]}
]}
]
}
I tried to use the force layout, but it doesn't seem to work. So I thought that maybe I'm using the wrong layout for my goal, but I can't figure out which layout I should use.
My code is:
var width = 1200,
height = 800,
graph;
var force = d3.layout.force()
.linkDistance(100)
.charge(-120)
.gravity(.05)
.size([width, height])
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("svg:defs").append("svg:marker")
.attr("id", "arrowhead")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 30)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
d3.json("dirGraph.json", function(error, json) {
graph = json;
for(var i=0; i<graph.nodes.length; i++)
update(graph.nodes[i]);
})
function update(root) {
var nodes = flatten(root),
links = d3.layout.tree().links(nodes);
force
.nodes(nodes)
.links(links)
.start();
node = node.data(nodes, function(d){return d.id;});
node.exit().remove();
var nodeEnter = node.enter().append("svg:g")
.attr("id", function(d){return d.id;})
.attr("class", "node")
.on("click", click)
.call(force.drag);
nodeEnter.append("circle").attr("r", 20);
nodeEnter.append("text").attr("dy", ".35em").text(function(d){return d.name;});
node.select("circle").style("fill", color);
link = link.data(links, function(d){return d.target.id;});
link.exit().remove();
link.enter().insert("line", ".node").attr("class", "link")
.attr("x1", function(d){return d.source.x;})
.attr("y1", function(d){return d.source.y;})
.attr("x2", function(d){return d.target.x;})
.attr("y2", function(d){return d.target.y;})
.attr("marker-end", "url(#arrowhead)");
}
function tick() {
link.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
function color(d) {
return d._children ? "#3182bd" // collapsed package
: "#c6dbef"; // expanded package
}
function flatten(root) {
var nodes = [], i = 0;
function recurse(node) {
if(node.children) node.children.forEach(recurse);
if(!node.id) node.id = ++i;
nodes.push(node);
}
recurse(root);
return nodes;
}
function click(d) {
if (d3.event.defaultPrevented) return; // ignore drag
if(d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update();
}
My question is: should I use a different layout or should I use the force layout in a different way?