D3 Multiple Instances of wForceSimulation - d3.js

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);
}
}
}

Related

Projections for different layers d3js

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?

How can I refactor a d3 pie to accept more or less data points?

I have a project that almost works the way I want. When a smaller dataset is added, slices are removed. It fails when a larger dataset is added. The space for the arc is added but no label or color is added for it.
This is my enter() code:
newArcs.enter()
.append("path")
.attr("stroke", "white")
.attr("stroke-width", 0.8)
.attr("fill", function(d, i) {
return color(i);
})
.attr("d", arc);
What am I doing wrong?
I've fixed the code such that it works now:
// Tween Function
var arcTween = function(a) {
var i = d3.interpolate(this.current || {}, a);
this.current = i(0);
return function(t) {
return arc(i(t));
};
};
// 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) {
arcs = arcs.data(donut(dataset), function(d) { return d.data.label });
arcs.exit().remove();
arcs.enter()
.append("path")
.attr("stroke", "white")
.attr("stroke-width", 0.8)
.attr("fill", function(d, i) {
return color(i);
})
.attr("d", arc);
arcs.transition()
.duration(duration)
.attrTween("d", arcTween);
sliceLabel = sliceLabel.data(donut(dataset), function(d) { return d.data.label });
sliceLabel.exit().remove();
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;
});
sliceLabel.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;
}
});
};
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");
var sliceLabel = label_group.selectAll("text");
updateChart(data);
// returns random integer between min and max number
function getRand() {
var min = 1,
max = 2;
var res = Math.floor(Math.random() * (max - min + 1) + min);
//console.log(res);
return res;
}
// Update the data
setInterval(function(model) {
var r = getRand();
return updateChart(r == 1 ? d1 : d2);
}, 2000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

D3 dashboard chart data bind from json or csv

I am new to D3 charts
i am using D3 dashboard chart following link
http://bl.ocks.org/diethardsteiner/3287802
here the datas are given by variables.
I don't want to get the value from variable and i want to get the value from json file.
Before the data are stored in a variable.
################ FORMATS ##################
-------------------------------------------
*/
var formatAsPercentage = d3.format("%"),
formatAsPercentage1Dec = d3.format(".1%"),
formatAsInteger = d3.format(","),
fsec = d3.time.format("%S s"),
fmin = d3.time.format("%M m"),
fhou = d3.time.format("%H h"),
fwee = d3.time.format("%a"),
fdat = d3.time.format("%d d"),
fmon = d3.time.format("%b")
;
/*
############# PIE CHART ###################
-------------------------------------------
*/
function dsPieChart(){
var dataset = [
{category: "ACC", measure: 0.30},
{category: "B56", measure: 0.25},
{category: "MAB", measure: 0.15},
]
;
var width = 400,
height = 400,
outerRadius = Math.min(width, height) / 2,
innerRadius = outerRadius * .999,
innerRadiusFinal = outerRadius * .5,
innerRadiusFinal3 = outerRadius* .45,
color = d3.scale.category20()
;
var vis = d3.select("#pieChart")
.append("svg:svg")
.data([dataset])
.attr("width", width)
.attr("height", height)
.append("svg:g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")")
;
var arc = d3.svg.arc()
.outerRadius(outerRadius).innerRadius(innerRadius);
var arcFinal = d3.svg.arc().innerRadius(innerRadiusFinal).outerRadius(outerRadius);
var arcFinal3 = d3.svg.arc().innerRadius(innerRadiusFinal3).outerRadius(outerRadius);
var pie = d3.layout.pie()
.value(function(d) { return d.measure; });
var arcs = vis.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice")
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", up)
;
arcs.append("svg:path")
.attr("fill", function(d, i) { return color(i); } )
.attr("d", arc)
.append("svg:title")
.text(function(d) { return d.data.category + ": " + formatAsPercentage(d.data.measure); });
d3.selectAll("g.slice").selectAll("path").transition()
.duration(750)
.delay(10)
.attr("d", arcFinal )
;
arcs.filter(function(d) { return d.endAngle - d.startAngle > .2; })
.append("svg:text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.attr("transform", function(d) { return "translate(" + arcFinal.centroid(d) + ")rotate(" + angle(d) + ")"; })
.text(function(d) { return d.data.category; })
;
function angle(d) {
var a = (d.startAngle + d.endAngle) * 90 / Math.PI - 90;
return a > 90 ? a - 180 : a;
}
// Pie chart title
vis.append("svg:text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text("Building")
.attr("class","title")
;
function mouseover() {
d3.select(this).select("path").transition()
.duration(750)
.attr("d", arcFinal3)
;
}
function mouseout() {
d3.select(this).select("path").transition()
.duration(750)
.attr("d", arcFinal)
;
}
function up(d, i) {
updateBarChart(d.data.category, color(i));
updateLineChart(d.data.category, color(i));
}
}
dsPieChart();
now i am trying to get the data by folowing way.
I dont know this is correct way or not can anyone help me
################ FORMATS ##################
-------------------------------------------
*/
var formatAsPercentage = d3.format("%"),
formatAsPercentage1Dec = d3.format(".1%"),
formatAsInteger = d3.format(","),
fsec = d3.time.format("%S s"),
fmin = d3.time.format("%M m"),
fhou = d3.time.format("%H h"),
fwee = d3.time.format("%a"),
fdat = d3.time.format("%d d"),
fmon = d3.time.format("%b")
;
/*
############# PIE CHART ###################
-------------------------------------------
*/
function dsPieChart(){
var width = 400,
height = 400,
outerRadius = Math.min(width, height) / 2,
innerRadius = outerRadius * .999,
// for animation
innerRadiusFinal = outerRadius * .5,
innerRadiusFinal3 = outerRadius* .45,
color = d3.scale.category20() //builtin range of colors
;
});
var vis = d3.select("#pieChart")
.append("svg")
d3.json("readme.json", function(error, root) {
if (error) throw error;
.data([root])
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")")
})
;
var arc = d3.svg.arc()
.outerRadius(outerRadius).innerRadius(innerRadius);
var arcFinal = d3.svg.arc().innerRadius(innerRadiusFinal).outerRadius(outerRadius);
var arcFinal3 = d3.svg.arc().innerRadius(innerRadiusFinal3).outerRadius(outerRadius);
var pie = d3.layout.pie()
.value(function(d) { return d.measure; });
var arcs = vis.selectAll("g.slice")
.data(pie)
.enter()
.append("g")
.attr("class", "slice")
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", up)
;
arcs.append("path")
.attr("fill", function(d, i) { return color(i); } )
.attr("d", arc)
.append("title")
.text(function(d) { return d.data.category + ": " + formatAsPercentage(d.data.measure); });
d3.selectAll("g.slice").selectAll("path").transition()
.duration(750)
.delay(10)
.attr("d", arcFinal )
;
arcs.filter(function(d) { return d.endAngle - d.startAngle > .2; })
.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.attr("transform", function(d) { return "translate(" + arcFinal.centroid(d) + ")rotate(" + angle(d) + ")"; })
.text(function(d) { return d.data.category; })
;
function angle(d) {
var a = (d.startAngle + d.endAngle) * 90 / Math.PI - 90;
return a > 90 ? a - 180 : a;
}
vis.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text("Building")
.attr("class","title")
;
function mouseover() {
d3.select(this).select("path").transition()
.duration(750)
.attr("d", arcFinal3)
;
}
function mouseout() {
d3.select(this).select("path").transition()
.duration(750)
.attr("d", arcFinal)
;
}
function up(d, i) {
updateBarChart(d.data.category, color(i));
updateLineChart(d.data.category, color(i));
}
}
dsPieChart();
can any one give a correct solution
Thanks,
Vinoth
Just create a JSON file with this structure:
[{
"category": "ACC",
"measure": 0.30
}, {
"category": "B56",
"measure": 0.25
}, {
"category": "MAB",
"measure": 0.15
}]
And use d3.json:
d3.json("data.json", function(dataset){
//code here
});
Here is a plunker showing it: https://plnkr.co/edit/OUjPNY3W2aXXCSxvm4tZ?p=preview

Removing segments from a d3 animated pie

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>

D3 V4 Rect bind data in stacked bar chart

In Normalized stacked bar I am trying to bind data in all rect in a bar but wrong value is passed. I adopted my code from this example and made it horizontal. Below is my code and I have created a plunker as well. In .text function entire object is passed. Can someone help me where I am going wrong
var svg = d3.select("svg"),
margin = {
top: 20,
right: 60,
bottom: 30,
left: 40
},
/*width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,*/
width = 120,
height = 120,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var y = d3.scaleBand()
.rangeRound([0, width])
.padding(0.1)
.align(0.1);
var x = d3.scaleLinear()
.rangeRound([height, 0]);
var z = d3.scaleOrdinal()
.range(['#02CA22', '#FB5652', '#FFB005']);
var stack = d3.stack()
.offset(d3.stackOffsetExpand);
d3.csv("data.csv", type, function (error, data) {
if (error) throw error;
/*data.sort(function(a, b) {
return b[data.columns[1]] / b.total - a[data.columns[1]] / a.total;
});*/
y.domain(data.map(function (d) {
return d.State;
}));
z.domain(data.columns.slice(1));
var serie = g.selectAll(".serie")
.data(stack.keys(data.columns.slice(1))(data))
.enter().append("g")
.attr("class", "serie")
.attr("fill", function (d) {
return z(d.key);
});
var rect = serie.selectAll("rect")
.data(function (d) {
return d;
}).enter();
rect.append("rect")
.attr("y", function (d) {
return y(d.data.State);
})
.attr("x", function (d) {
return x(d[1]);
})
.attr("width", function (d) {
return x(d[0]) - x(d[1]);
})
.attr("height", y.bandwidth());
rect.append("text")
.text(function (d) {
console.log('d');
console.log(d);
console.log(d.data.key);
return 'val';
})
.attr("y", function (d) { return y(d.data.State) + y.bandwidth() / 2; })
.attr("x", function (d) {
return x(d[1]);
});
/* g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(2, "%"));*/
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y));
var legend = serie.append("g")
.attr("class", "legend")
.attr("transform", function (d) {
var d = d[0];
return "translate(" + ((x(d[0]) + x(d[1])) / 2) + ", " + (y(d.data.State) - y.bandwidth()) + ")";
});
/*legend.append("line")
.attr("y1", 5)
.attr("x1", 15)
.attr("x2", 15)
.attr("y2", 12)
.attr("stroke", "#000");
legend.append("text")
.attr("x", 9)
.attr("dy", "0.35em")
.attr("fill", "#000")
.style("font", "10px sans-serif")
.text(function (d) {
return d.key;
}); */
});
function type(d, i, columns) {
var t;
for (i = 1, t = 0; i < columns.length; ++i) t += d[columns[i]] = +d[columns[i]];
d.total = t;
return d;
}
I think the best way to do this is to modify your subselection data-binding to include that information:
var rect = serie.selectAll("rect")
.data(function (d) {
// return all the data you need as flat as possible
var rv = d.map(function(da){
return {p: da, key: d.key, state: da.data.State}
});
return rv;
}).enter();
The text is then available as:
rect.append("text")
.text(function (d) {
return d.key;
})
.attr("y", function (d) { return y(d.state) + y.bandwidth() / 2; })
.attr("x", function (d) {
return x(d.p[1]);
});
Updated Plunker.

Resources