Circles keep accumulating and are not merging correctly - d3.js

I am trying to merge these circles but I keep getting a graph of accumulating circles as opposed to circles moving across the graph?
What am I missing?
I have attached the code below. This function is called updatechart. It corresponds to a slider. So whenever I move the slider across the screen. I corresponding year it lands on is where the updated circle data should move.
var filteredyears = d3.nest()
.key(function(d) {
if(year === d.year){
return d;
}
}).entries(globaldataset);
var circled = svg.selectAll('.countries')
.data(filteredyears[1].values);
var circledEnter = circled.enter()
circled.merge(circledEnter);
circledEnter.append("circle").attr("cx", function(d) {
return xScale(d.gdpPercap);
})
.attr("cy", function(d) {
return yScale(d.lifeExp);
})
.attr('transform', "translate("+[40,30]+")")
.attr( 'r', function(d) {
return rScale(d.population) / 100})
.style("fill", function(d) {
if(d.continent == 'Asia'){
return '#fc5a74';
} else if (d.continent == 'Europe') {
return '#fee633';
} else if (d.continent == 'Africa') {
return '#24d5e8';
} else if (d.continent == 'Americas') {
return '#82e92d';
} else if (d.continent == 'Oceania') {
return '#fc5a74';
}
})
.style("stroke", "black");
circled.exit().remove();

You have a couple of issues using the merge() method, which is indeed quite hard to understand initially.
First, you have to reassign your selection:
circled = circled.merge(circledEnter);
Now, from this point on, apply the changes to circled, not to circledEnter:
circled.attr("//etc...
Besides that, your exit selection won't work, since you're calling it on the merged selection. Put it before the merge.
Finally, append goes to the circledEnter selection, before merging, as well as all attributes that don't change.
Here is a very basic demo showing it:
var svg = d3.select("svg"),
color = d3.scaleOrdinal(d3.schemeCategory10);
render();
function render() {
var data = d3.range(~~(1 + Math.random() * 9));
var circles = svg.selectAll("circle")
.data(data);
circles.exit().remove();
var circlesEnter = circles.enter()
.append("circle")
.attr("r", 5)
.attr("fill", function(d) {
return color(d);
});
circles = circlesEnter.merge(circles);
circles.attr("cx", function() {
return 5 + Math.random() * 290
})
.attr("cy", function() {
return 5 + Math.random() * 140
});
}
d3.interval(render, 1000);
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

Related

Force bubbles to left in D3.js

I am trying in many ways to create a bubble chart by forcing the left of the page based on the value. The highest value bubble should be in the upper left corner. As other approaches didn't work, I looked for an example in which the bubbles are forcing to the center of the page and now I'm trying to adjust to the left, the result I need is the same as the example image above.
My code:
function bubbleChart() {
var width = 940;
var height = 600;
var center = {
x: width / 2,
y: height / 2
};
var left = {
x: 0,
y: 300
};
var forceStrength = 0.03;
var svg = null;
var bubbles = null;
var nodes = [];
function charge(d) {
return -Math.pow(d.radius, 2.0) * forceStrength;
}
var simulation = d3.forceSimulation()
.velocityDecay(0.2)
.force('x', d3.forceX().strength(forceStrength).x(left.x))
.force('y', d3.forceY().strength(forceStrength).y(left.y))
.force('charge', d3.forceManyBody().strength(charge))
.on('tick', ticked);
simulation.stop();
var fillColor = d3.scaleOrdinal()
.domain(['low', 'medium', 'high'])
.range(['#d84b2a', '#beccae', '#7aa25c']);
function createNodes(rawData) {
var maxAmount = d3.max(rawData, function(d) {
return +d.total_amount;
});
var radiusScale = d3.scalePow()
.exponent(0.5)
.range([2, 85])
.domain([0, maxAmount]);
var myNodes = rawData.map(function(d) {
return {
id: d.id,
radius: radiusScale(+d.total_amount),
value: +d.total_amount,
name: d.grant_title,
org: d.organization,
group: d.group,
year: d.start_year,
x: Math.random() * 900,
y: Math.random() * 800
};
});
myNodes.sort(function(a, b) {
return b.value - a.value;
});
return myNodes;
}
var chart = function chart(selector, rawData) {
nodes = createNodes(rawData);
svg = d3.select(selector)
.append('svg')
.attr('width', width)
.attr('height', height);
bubbles = svg.selectAll('.bubble')
.data(nodes, function(d) {
return d.id;
});
var bubblesE = bubbles.enter().append('circle')
.classed('bubble', true)
.attr('r', 0)
.attr('fill', function(d) {
return fillColor(d.group);
})
.attr('stroke', function(d) {
return d3.rgb(fillColor(d.group)).darker();
})
.attr('stroke-width', 2)
.on('mouseover', showDetail)
.on('mouseout', hideDetail);
bubbles = bubbles.merge(bubblesE);
bubbles.transition()
.duration(2000)
.attr('r', function(d) {
return d.radius;
});
simulation.nodes(nodes);
groupBubbles();
};
function ticked() {
bubbles
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
}
function nodeYearPos(d) {
return yearCenters[d.year].x;
}
function groupBubbles() {
hideYearTitles();
// #v4 Reset the 'x' force to draw the bubbles to the center.
simulation.force('x', d3.forceX().strength(forceStrength).x(center.x));
// #v4 We can reset the alpha value and restart the simulation
simulation.alpha(1).restart();
}
function splitBubbles() {
showYearTitles();
// #v4 Reset the 'x' force to draw the bubbles to their year centers
simulation.force('x', d3.forceX().strength(forceStrength).x(nodeYearPos));
// #v4 We can reset the alpha value and restart the simulation
simulation.alpha(1).restart();
}
function hideYearTitles() {
svg.selectAll('.year').remove();
}
function showYearTitles() {
// Another way to do this would be to create
// the year texts once and then just hide them.
var yearsData = d3.keys(yearsTitleX);
var years = svg.selectAll('.year')
.data(yearsData);
years.enter().append('text')
.attr('class', 'year')
.attr('x', function(d) {
return yearsTitleX[d];
})
.attr('y', 40)
.attr('text-anchor', 'middle')
.text(function(d) {
return d;
});
}
function showDetail(d) {
// change outline to indicate hover state.
d3.select(this).attr('stroke', 'black');
var content = '';
tooltip.showTooltip(content, d3.event);
}
function hideDetail(d) {
// reset outline
d3.select(this)
.attr('stroke', d3.rgb(fillColor(d.group)).darker());
tooltip.hideTooltip();
}
This is my complete code on Vizhub:
https://vizhub.com/barbosa-renan/2864204410d54af5a5c402cdfdd9959d?edit=files&file=index.js
Note:
In other attempts I tried to use .domain () and .range but in this scenario it didn't work because when I have bubbles with a very high value and others with a very low value they are very distant.

d3.js click and apply zoom and pan to distribute points located inside a targeted division to the triggered subdivisions

Based on the response and example made by Andrew Reid, I produced this
pen code here points_in_subdivisons: on clicking on areas(Germany) on the screen
We want to offer a smooth animation from one close-up on the map to another
by using ZOOM OUT, PAN, ZOOM IN.
I have many divisions(countries) on Country level and then many sub-divisions(regions) inside each country .
Many points scattered across all divisions (countries) on my example mainly above Germany.
when I have to click on a targeted division(country) I must get only the points which correspond to this targeted division(country) that I have just clicked on
That means when the zoom of the subdivision(regions) is triggered(when the click is
made),
the code should take all the points that exist already only inside the
contours of the targeted divison(country) (that have just been clicked on) and points
enclosed-in should scatter in their corresponding subdivisions(regions).
To achieve this functionality and
based on Michael Rovinsky comment:
in the function manipulate(), the code is able to filter and extract only points that are embedded inside the targeted and triggered subdivisions(regions) and exclude markers those that are outside.
Inside function redraw() the enter exit pattern works well .
var svg = d3.select("svg");
width = 960;
height = 500;
var dataArray = [];
var mydataArray= [];
var projection = d3.geoMercator();
var baseProjection = d3.geoMercator();
var path = d3.geoPath().projection(projection);
var gBackground = svg.append("g"); // appended first
var gProvince = svg.append("g");
var gDataPoints = svg.append("g"); // appended second
var ttooltip = d3.select("body").append("div")
.attr("class", "ttooltip");
var csvPath="https://dl.dropbox.com/s/rb9trt4zy87ezi3/lonlat.csv?dl=0";
d3.csv(csvPath, function(error, data) {
if (error) throw error;
d3.json("https://gist.githubusercontent.com/rveciana/5919944/raw/2fef6be25d39ebeb3bead3933b2c9380497ddff4/nuts0.json", function(error, nuts0) {
if (error) throw error;
d3.json("https://gist.githubusercontent.com/rveciana/5919944/raw/2fef6be25d39ebeb3bead3933b2c9380497ddff4/nuts2.json", function(error, nuts2) {
if (error) throw error;
// convert topojson back to geojson
var countries = topojson.feature(nuts0, nuts0.objects.nuts0);
var regions = topojson.feature(nuts2, nuts2.objects.nuts2);
baseProjection.fitSize([width,height],regions);
projection.fitSize([width,height],regions);
var color = d3.scaleLinear().range(["steelblue","darkblue"]).domain([0,countries.features.length]);
var regionColor = d3.scaleLinear().range(["orange","red"]);
baseProjection.fitSize([width,height],countries);
projection.fitSize([width,height],countries);
var featureCollectionCountries = { "type":"FeatureCollection", "features": countries.features };
gBackground
.attr("class", "country")
.selectAll("path")
.data(countries.features)
.enter()
.append("path")
.attr("fill",function(d,i) { return color(i); })
.attr("opacity",0.7)
.attr("d", path)
.style("stroke","black")
.style("stroke-width",0)
.on("mouseover", function() {
d3.select(this)
.style("stroke-width",1)
.raise();
})
.on("mouseout", function(d,i) {
d3.select(this)
.style("stroke-width", 0 );
})
///// now zoom in when clicked and show subdivisions:
.on("click", function(d) {
// remove all other subdivisions:
d3.selectAll(".region")
.remove();
// add new features:
var features = regions.features.filter(function(feature) { return feature.properties.nuts_id.substring(0,2) == d.properties.nuts_id; });
regionColor.domain([0,features.length])
gProvince.selectAll(null)
.data(features)
.enter()
.append("path")
.attr("class","region")
.attr("fill", function(d,i) { return regionColor(i) })
.attr("d", path)
.style("stroke","black")
.style("stroke-width",0)
.on("click", function() {
zoom(projection,baseProjection);
d3.selectAll(".subdivision")
.remove();
})
.on("mouseover", function() {
d3.select(this)
.style("stroke-width",1)
.raise();
})
.on("mouseout", function(d,i) {
d3.select(this)
.style("stroke-width", 0 );
})
.raise()
// zoom to selected features:
var featureCollection = { "type":"FeatureCollection", "features": features }
manipulate(data,features);
redraw(featureCollection);
var endProjection = d3.geoMercator();
zoom(projection,endProjection.fitExtent([[50,50],[width-50,height-50]],featureCollection));
});
dataArray = data;
redraw(featureCollectionCountries);
});
});
});
function zoom(startProjection,endProjection,middleProjection) {
if(!middleProjection) {
d3.selectAll("path")
.transition()
.attrTween("d", function(d) {
var s = d3.interpolate(startProjection.scale(), endProjection.scale());
var x = d3.interpolate(startProjection.translate()[0], endProjection.translate()[0]);
var y = d3.interpolate(startProjection.translate()[1], endProjection.translate()[1]);
return function(t) {
projection
.scale(s(t))
.translate([x(t),y(t)])
path.projection(projection);
return path(d);
}
})
.duration(1000);
}
else {
d3.selectAll("path")
.transition()
.attrTween("d", function(d) {
var s1 = d3.interpolate(startProjection.scale(),middleProjection.scale());
var s2 = d3.interpolate(middleProjection.scale(),endProjection.scale());
var x = d3.interpolate(startProjection.translate()[0], endProjection.translate()[0]);
var y = d3.interpolate(startProjection.translate()[1], endProjection.translate()[1]);
function s(t) {
if (t < 0.5) return s1; return s2;
}
return function(t) {
projection
.translate([x(t),y(t)])
.scale(s(t)(t))
path.projection(projection);
return path(d);
}
})
.duration(1500);
}
}
function redraw(featureCollection,type) {
var mapG = d3.select('svg g.country');
d3.selectAll('circle')
.remove();
let grp = gDataPoints
.attr("class", "circle")
.selectAll("circle")
.data(dataArray,function(d) { return d.NOM; })
let grpEnter = grp.enter()
let group = grpEnter
group.append("circle")
.attr('fill', 'rgba(135, 5, 151, 125)')
.attr('stroke', 'black')
.each(function(d) {
if (d.lon === null ) return;
if (isNaN(d.lon ))return;
if (d.lat === null) return;
if (isNaN(d.lat ))return;
var pos = projection([parseFloat(d.lon), parseFloat(d.lat)]);
d.cx = pos[0];
d.cy = pos[1];
})
.attr("cx", function(d) {
return d.cx;
})
.attr("cy", function(d) {
return d.cy;
})
.attr("r",0.5)
.on("mouseover", showTooltip)
.on("mouseout", hideTooltip)
.on('mousemove', function(d) {
var xPos = d3.mouse(this)[0] - 15;
var yPos = d3.mouse(this)[1] - 55;
ttooltip.attr('transform', 'translate(' + xPos + ',' + yPos + ')');
ttooltip.style('opacity', 1);
var html = "<span>" + d.lon+ "</span>, <span>" + d.lat + "</span>";
ttooltip.html(html);
});
// Setup each circle with a transition, each transition working on transform attribute,
// and using the translateFn
group
.transition()
.duration(2000)
.attrTween("transform",function(d) {
return mapG._groups[0][0] != null ? recenter(featureCollection): null;
});
group.exit().remove() // exit > remove > g
}
function recenter(featureCollection) {
console.log('recentering');
};
function manipulate(data,features){
dataArray= [];
mydataArray =[];
data.forEach(function(ddd)
{
features.forEach(function(feature)
{
var polygoneOriginal =feature;
var points = [parseFloat(ddd.lon), parseFloat(ddd.lat)];
var isIn = d3.geoContains(polygoneOriginal, points);
if(isIn)
{
var element = ddd;
mydataArray.pushIfNotExist(element, function(e) {
return e.lat === element.lat && e.lon === element.lon ;
});
}
});
});
if(mydataArray.length>0)
{
var columnsArray= ["lon","lat"];
dataArray=mydataArray;
dataArray.columns = columnsArray;
}
}
function showTooltip(d) {
var html = "<span>" + d.lon+ "</span>, <span>" + d.lat + "</span>";
ttooltip.html(html);
ttooltip
.style("left", window.pageXOffset + d3.event.x + 12 + "px")
.style("top", window.pageYOffset + d3.event.y + 12 + "px")
.transition()
.style("opacity", 1);
return d3.select(this).attr('fill', 'rgba(103, 65, 114, 0.8)');
}
function hideTooltip() {
ttooltip
.transition()
.style("opacity", 0);
return d3.select(this).attr('fill', 'rgba(103, 65, 114, 0.5)');
}
// check if an element exists in array using a comparer function
// comparer : function(currentElement)
Array.prototype.inArray = function(comparer) {
for(var i=0; i < this.length; i++) {
if(comparer(this[i])) return true;
}
return false;
};
// adds an element to the array if it does not already exist using a comparer
// function
Array.prototype.pushIfNotExist = function(element, comparer) {
if (!this.inArray(comparer)) {
this.push(element);
}
};
My Question is the following : How to make the Zooming (for points circle) to work adequately:
right now, on a map upon click the x y points not scale.
They are rendered as circles in background and I would like them to move with the map.
That means How to apply the same animation zoom (when subdivisions are triggered by click on a division) in order to those points inside the targeted subdivision follow in transition and move with the map and we could see circles points clearly distributed adequately in each correct corresponding subdivisions?
update
Andrew Reid described here How To accomplish a smooth zoom using d3.js
so following his hints.
I added the following instructions in redraw() function
var mapG = d3.select('svg g.country');
group
.transition()
.duration(2000)
.attrTween("transform",function(d) {
return mapG._groups[0][0] != null ? recenter(): null;
});
AND then we should add the code to the The function that should actually do the moving recenter(featureCollection) function to
function recenter(featureCollection) {
// TO ADD CODE TO BE IMPLEMENTED HERE
};
Thank You very much for your cooperation,participation and help !
1- To generate first iteration click on Region equal country
//GENERATE FIRST MAP
dataArray = data;
redraw();
2- To generate counties for example on click on region, we should first set startprojection and endprojection in zoom function and then trigger redraw of circles
//zoom to selected provinces features:
var countiesFeatureCollection = { "type":"FeatureCollection", "features": countiesFeatures }
//manipulate counties And Redraw
manipulateCounties(data,countiesFeatures);
baseProjection.fitExtent([[50,50],[width-50,height-50]],countiesFeatureCollection);
projection.fitExtent([[50,50],[width-50,height-50]],countiesFeatureCollection);
redraw(countiesFeatureCollection,"counties");
if ( projection.translate().toString() === baseProjection.translate().toString() && projection.scale() === baseProjection.scale() )
{
zoom(baseProjection,projection.fitExtent([[50,50],[width-50,height-50]],countiesFeatureCollection));
}
else
{
var endProjection = d3.geoMercator();
zoom(projection,endProjection.fitExtent([[50,50],[width-50,height-50]],countiesFeatureCollection));
}
3-the same thing should be applied to communities
var endProjection = d3.geoMercator();
endProjection.fitExtent([[50,50],[width-50,height-50]],communesfeatureCollection);
projection.fitExtent([[50,50],[width-50,height-50]],communesfeatureCollection);
redraw(communesfeatureCollection,"communes");
if ( projection.translate().toString() === projectioncommune.translate().toString() && projection.scale() === projectioncommune.scale()){
zoom(projectioncommune,projection.fitExtent([[50,50],[width-50,height-50]],communesfeatureCollection));
}
else {
var endProjection = d3.geoMercator();
zoom(projection,endProjection.fitExtent([[50,50],[width-50,height-50]],communesfeatureCollection));
}
4- Then reinitialise to go to first step 1 by
// start rendering points again
baseProjection.fitSize([width,height],regions);
projection.fitSize([width,height],regions);
//GENERATE AGAIN THE FIRST MAP
dataArray = data;
redraw();
zoom(projection,baseProjection);
ATTACHED WORKING PEN

Conditionally change background image of D3 circles

I am trying to conditionally change the background of some circles created with D3 but I am unable to get the if else logic to correctly pick out the correct picture. The code goes right to the final else statement for the default.gif. I don't see any errors in the console. All the images are in the same directory as the html file.
var diameter = 500, //max size of the bubbles
format = d3.format(",d"),
color = d3.scaleOrdinal(d3.schemeCategory20c); //color category
var bubble = d3.pack()
.size([diameter, diameter])
.padding(1.5);
var svg = d3.select("body")
.append("svg")
.attr("width", diameter)
.attr("height", diameter)
.attr("class", "bubble");
d3.csv("fruit.csv", function(error, data){
if (error) throw error;
//convert numerical values from strings to numbers
data = data.map(function(d){ d.value = +d["Amount"]; return d; });
//Sets up a hierarchy of data object
var root = d3.hierarchy({children:data})
.sum(function(d) { return d.value; })
.sort(function(a, b) { return b.value - a.value; });
//Once we have hierarchal data, run bubble generator
bubble(root);
//setup the chart
var bubbles = svg.selectAll(".bubble")
.data(root.children)
.enter();
//create the bubbles
bubbles.append("circle")
.attr("class", "circle")
.attr("r", function(d){ return d.r; })
.attr("cx", function(d){ return d.x; })
.attr("cy", function(d){ return d.y; })
.append("defs")
.append("pattern")
.append("image")
.attr("xlink:href", function(d) {
if ( d.Fruit == "Apple") {
return "apple.jpg";
}
else if (d.Fruit == "Pear") {
return "pear.jpg"
}
else if (d.Fruit == "Banana") {
return "banana.jpg";
}
else if (d.Fruit == "Strawberry") {
return "strawberry.jpg";
}
else if (d.Fruit == "Grapes") {
return "grapes.jpg";
}
else { return "default.gif"; }
});
//.style("fill", function(d) { return color(d.value); });
//format the text for each bubble
bubbles.append("text")
.attr("x", function(d){ return d.x; })
.attr("y", function(d){ return d.y + 5; })
.attr("text-anchor", "middle")
.text(function(d){ return d.data["Fruit"]; })
.style("fill","white")
.style("font-family", "Helvetica Neue, Helvetica, Arial, san-serif")
.style("font-size", "12px");
});
The csv file contains the following data:
Fruit,Amount
Apple,32
Pear,13
Banana,25
Grapes,29
Strawberry,36
As I said in my comment, I'm surprised that you're seeing anything as the circles' background, since your code for creating/referencing the patterns is not correct. For a quick explanation, check my answer here.
However, I'll address only your main question here, which is the if statement.
The problem is that you're using d3.hierarchy to create your data array. Because of that, all data are inside node.data. According the API:
The returned node and each descendant has the following properties:
node.data - the associated data, as specified to the constructor.
Therefore, for your conditionals, instead of:
d.Fruit == "Apple"
It should be:
d.data.Fruit == "Apple"
And the same for d.data.Fruit == "Pear", d.data.Fruit == "Banana" etc...

How to animate the radius and the arc length of a pie chart in d3 (at the same time)?

I figured out how to change arc length of a pie chart with new values using d3 following Bostock's Pie Chart Update, but I'm having a hard time figuring out how to also animate the radius of the pic chart at the same time.
This a snippet of my current code:
pie_inner.value(function(d) {
return d.toward === toward || toward === "both" ? d.spent : 0;
});
path_inner
.data(function(d) {
return pie_inner(d.data).map(function(m) {
m.r = d.r;
return m;
})
})
.transition()
.duration(750)
.attrTween("d", function(d) {
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
arc_inner.innerRadius(d.r - thickness);
arc_inner.outerRadius(d.r);
return arc_inner(interpolate(t));
};
});
With this code the arc length animates to the new values nicely but the radius "jumps" to the new value.
I see that this code animates both the arc length and the radius but it does it in sequence as opposed to the same time.
I was setting the radius using the function outerRadius(), but I figured out if I just set the property, then it works:
pie_inner.value(function(d) {
return d.toward === toward || toward === "both" ? d.spent : 0;
});
path_inner
.data(function(d) {
return pie_inner(d.data).map(function(m) {
m.r = d.r;
return m;
})
})
.transition()
.duration(750)
.attrTween("d", function(d) {
d.innerRadius = d.r - thickness;
d.outerRadius = d.r;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc_inner(interpolate(t));
};
});

d3.js - Smooth transition on arc draw for pie chart

I have put together a pie chart in d3 and am using a transition and delay for when each of the arcs are getting drawn.
Code Snippet:
var path = g.selectAll("path")
.data(pie(dataset.apples))
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.transition()
.delay(function(d, i) {
return i * 100;
})
.duration(500)
.attr("d", arc);
Working fiddle
I'd like to achieve a smoother transition so that each arc appears to grow in sequence from one to the other as they are drawn until, rather than just appear immediately like they do now.
Can someone give me some help?
Thanks
I can suggest 2 ways to do this animation:
Animation 1 done with the help of delay here one slice grows and then next slice will grow.
//the tween will do the animation as end angle increase over time
var path = g.selectAll("path")
.data(pie(dataset.apples))
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.transition()
.delay(function(d, i) {
return i * 800;
})
.attrTween('d', function(d) {
var i = d3.interpolate(d.startAngle+0.1, d.endAngle);
return function(t) {
d.endAngle = i(t);
return arc(d);
}
});
working example here
Animation 2 Each slice grows #same time done with duration
var path = g.selectAll("path")
.data(pie(dataset.apples))
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.transition()
.duration(function(d, i) {
return i * 800;
})
.attrTween('d', function(d) {
var i = d3.interpolate(d.startAngle+0.1, d.endAngle);
return function(t) {
d.endAngle = i(t);
return arc(d);
}
});
Working code here
Hope this helps!

Resources