Append d3 svg to current ng-repeat element - javascript

I would like to append a d3.js pie chart to each li elements generated with ng-repeat.
<ol>
<li ng-repeat="h in hashtags | orderBy:predicate:reverse | limitTo: limit">
<div class="hashtag">
<a ng-click="showTweetsForHashtag(h)">#{{h.Hashtag}}</a>
</div>
<div class="frequency">
{{h.Frequency}} times
</div>
<div class="engagement">
{{h.Engagement}}
<pie-chart data="h" on-click="showTweetsForHashtag(item)"></pie-chart>
</div>
</li>
</ol>
My $scope.hashtag is an array of objects containing hashtags engagement properties :
[{
"Favorites": 0,
"Frequency": 1,
"Hashtag": "19h30",
"Replies": 0,
"Retweets": 1,
"Engagement":2,
"tweetId": 615850479952785400
}, {
"Favorites": 0,
"Frequency": 1,
"Hashtag": "80s",
"Replies": 0,
"Retweets": 2,
"Engagement":2,
"tweetId": [
616521677275533300,
617319253738393600
]
}{
"Favorites": 1,
"Frequency": 1,
"Hashtag": "AloeBlacc",
"Replies": 0,
"Retweets": 1,
"Engagement":2,
"tweetId": 617309488572420100
}, {
"Favorites": 2,
"Frequency": 1,
"Hashtag": "Alpes",
"Replies": 0,
"Retweets": 1,
"Engagement":3,
"tweetId": 615481266348146700
}]
Tanks to the ng-repeat, each time I call the pie-chart directive, I only pass one h object :
{
"Favorites": 2,
"Frequency": 1,
"Hashtag": "Alpes",
"Replies": 0,
"Retweets": 1,
"Engagement":3,
"tweetId": 615481266348146700
}
Which I then manually "map" into that format :
var mapped = [{
"label": "Retweets",
"value": data.Retweets
}, {
"label": "Favorites",
"value": data.Favorites
}, {
"label": "Replies",
"value": data.Replies
}];
In the end, I would like my directive to append the pie to the current <div class="pie_chart"></div> (which is generated in the directive template) with the mapped data of the current h object that has been passed. But as ocket-san mentionned d3.select(someElement) only matches the first element in the DOM.
Here is my directive :
.directive('pieChart', ['d3', function(d3) {
return {
restrict: 'E',
scope: {
data: '=',
onClick: '&'
},
template: '<div class="pie_chart"></div>',
link: function(scope, iElement, iAttrs) {
// watch for data changes and re-render
scope.$watch('data', function(newVals, oldVals) {
if (newVals) {
scope.render(newVals);
}
}, true);
scope.render = function(data) {
var w = 50, //width
h = 50, //height
r = data.Engagement / 3, // adapt radius to engagement value
color = d3.scale.ordinal().range(["#77b255", "#ffac33", "#07c"]); //custom range of colors
// map data to to be used by pie chart directive
var mapped = [{
"label": "Retweets",
"value": data.Retweets
}, {
"label": "Favorites",
"value": data.Favorites
}, {
"label": "Replies",
"value": data.Replies
}];
data = mapped;
// Courtesy of https://gist.github.com/enjalot/1203641
var vis = d3.select(".pie_chart")
.append("svg:svg") //create the SVG element inside the <body>
.data([data]) //associate our data with the document
.attr("width", w) //set the width and height of our visualization (these will be attributes of the <svg> tag
.attr("height", h)
.append("svg:g") //make a group to hold our pie chart
.attr("transform", "translate(" + r + "," + r + ")") //move the center of the pie chart from 0, 0 to radius, radius
var arc = d3.svg.arc() //this will create <path> elements for us using arc data
.outerRadius(r);
var pie = d3.layout.pie() //this will create arc data for us given a list of values
.value(function(d) {
return d.value;
}); //we must tell it out to access the value of each element in our data array
var arcs = vis.selectAll("g.slice") //this selects all <g> elements with class slice (there aren't any yet)
.data(pie) //associate the generated pie data (an array of arcs, each having startAngle, endAngle and value properties)
.enter() //this will create <g> elements for every "extra" data element that should be associated with a selection. The result is creating a <g> for every object in the data array
.append("svg:g") //create a group to hold each slice (we will have a <path> and a <text> element associated with each slice)
.attr("class", "slice"); //allow us to style things in the slices (like text)
arcs.append("svg:path")
.attr("fill", function(d, i) {
return color(i);
}) //set the color for each slice to be chosen from the color function defined above
.attr("d", arc); //this creates the actual SVG path using the associated data (pie) with the arc drawing function
};
}
}
}]);
The problem is that the instruction
var vis = d3.select(".pie_chart")
.append("svg:svg")
Appends all the pie charts to the first div with the pie_chart class.
I tried changing it to d3.select(iElement) (…) but it didn't work.
Any suggestions ?
Thanks in advance !
Q.
You can see the current output there :
http://i61.tinypic.com/wqqc0z.png

The problem is that d3.select('.pie_chart') selects the first element matching such class in the body, not within your directive template. To achieve this, you should use the element object provided within the link function. In your case:
var vis = d3.select(element[0]).select(".pie_chart").append("svg")...
I have created a simplified fiddle trying to show this.
Hope it helps.

When we using Angularjs and d3js together we’ll need to make updating the d3.select('body') selection to be relative to the directive using
d3.select(element[0]) instead of the entire DOM. The reason we have to use element[0] instead of just element is because element “is” a jQuery
wrapped selection and not an ordinary DOM object. Doing element[0] gives us just the plain old DOM element. (I say “is” in quotes because it’s technically a jqlite wrapped DOM element. jqlite is essentially a slimmed down version of jQuery.)
So you need to update your Code to:
.directive('pieChart', ['d3', function(d3) {
return {
restrict: 'E',
scope: {
data: '=',
onClick: '&'
},
template: '<div class="pie_chart"></div>',
link: function(scope, iElement, iAttrs) {
// watch for data changes and re-render
scope.$watch('data', function(newVals, oldVals) {
if (newVals) {
scope.render(newVals);
}
}, true);
scope.render = function(data) {
var w = 50, //width
h = 50, //height
r = data.Engagement / 3, // adapt radius to engagement value
color = d3.scale.ordinal().range(["#77b255", "#ffac33", "#07c"]); //custom range of colors
// map data to to be used by pie chart directive
var mapped = [{
"label": "Retweets",
"value": data.Retweets
}, {
"label": "Favorites",
"value": data.Favorites
}, {
"label": "Replies",
"value": data.Replies
}];
data = mapped;
// Courtesy of https://gist.github.com/enjalot/1203641
//Part need Update
var vis = d3.select(iElement[0])
.append("svg:svg") //create the SVG element inside the <body>
.data([data]) //associate our data with the document
.attr("width", w) //set the width and height of our visualization (these will be attributes of the <svg> tag
.attr("height", h)
.append("svg:g") //make a group to hold our pie chart
.attr("transform", "translate(" + r + "," + r + ")") //move the center of the pie chart from 0, 0 to radius, radius
var arc = d3.svg.arc() //this will create <path> elements for us using arc data
.outerRadius(r);
var pie = d3.layout.pie() //this will create arc data for us given a list of values
.value(function(d) {
return d.value;
}); //we must tell it out to access the value of each element in our data array
var arcs = vis.selectAll("g.slice") //this selects all <g> elements with class slice (there aren't any yet)
.data(pie) //associate the generated pie data (an array of arcs, each having startAngle, endAngle and value properties)
.enter() //this will create <g> elements for every "extra" data element that should be associated with a selection. The result is creating a <g> for every object in the data array
.append("svg:g") //create a group to hold each slice (we will have a <path> and a <text> element associated with each slice)
.attr("class", "slice"); //allow us to style things in the slices (like text)
arcs.append("svg:path")
.attr("fill", function(d, i) {
return color(i);
}) //set the color for each slice to be chosen from the color function defined above
.attr("d", arc); //this creates the actual SVG path using the associated data (pie) with the arc drawing function
};
}
}
}]);
When you update your code the directive('pieChart') function dynamically will select <pie-chart/> tag. if you have specific class, update your code to:
var vis = d3.select(iElement[0]).select(".pie_chart")
Update 1
You need to add $index to ng-repeat because:
What Angular is telling us is that every element in an ng-repeat needs to be unique. However,
we can tell Angular to use the elements index within the array instead to determine uniqueness by adding track by $index.
<ol>
<li ng-repeat="h in hashtags track by $index" | orderBy:predicate:reverse | limitTo: limit">
<div class="hashtag">
<a ng-click="showTweetsForHashtag(h)">#{{h.Hashtag}}</a>
</div>
<div class="frequency">
{{h.Frequency}} times
</div>
<div class="engagement">
{{h.Engagement}}
<pie-chart data="h" on-click="showTweetsForHashtag(item)"></pie-chart>
</div>
</li>
</ol>

I found the answers here to be incorrect in my case.
Jarandaf - was the closest however my solution was to remove the class selector.
and just use the below code:
d3.select(element[0]).append('svg')

d3.select("element") always selects the first element it finds. For example: suppose you have the following html structure:
<body>
<p></p>
<p></p>
<p></p>
</body>
and you would code: d3.select("p").append("svg"), the result is going to be
<body>
<p>
<svg></svg>
</p>
<p></p>
<p></p>
</body>
You need to use d3.selectAll(element), which will give you a d3 selection with all the items that fit the selector.
edit:
Ok, so i think your final html structure could look something like this:
<ol>
<li ng-repeat="h in hashtags | orderBy:predicate:reverse | limitTo: limit">
<div class="hashtag">
<a ng-click="showTweetsForHashtag(h)">#{{h.Hashtag}}</a>
</div>
<div class="frequency">
{{h.Frequency}} times
</div>
<div class="engagement">
{{h.Engagement}}
<div id="pie_chart">
<svg> your piechart goes here</svg>
</div>
</div>
</li>
<li ng-repeat="h in hashtags | orderBy:predicate:reverse | limitTo: limit">
<div class="hashtag">
<a ng-click="showTweetsForHashtag(h)">#{{h.Hashtag}}</a>
</div>
<div class="frequency">
{{h.Frequency}} times
</div>
<div class="engagement">
{{h.Engagement}}
<div id="pie_chart">
<svg> another piechart goes here</svg>
</div>
</div>
</li>
</ol>
so suppose that html structure already exists without the tag (its because i dont know anything about angular or directives :-) ) and you want to append the svg tag and append an tag to every div with class "pie_chart", you need to do it as following:
var piecharts = d3.selectAll(".pie_chart").append("svg");
The result will be an html structure like above.
If this is not what you want, then I am sorry, I think i totally misunderstood the question :-)

Thank you Gabriel for you answer !
In the meantime, I found a workaround (it may not be a the prettiest, but it works !)
Directive :
.directive('pieChart', ['d3', function(d3) {
return {
restrict: 'E',
scope: {
data: '=',
max: '#',
item: '#',
onClick: '&'
},
template: '<div class="pie_chart"></div>',
link: function(scope, iElement, iAttrs) {
// watch for data changes and re-render
scope.$watch('data', function(newVals, oldVals) {
if (newVals) {
scope.render(newVals);
}
}, true);
scope.render = function(data) {
// Courtesy of https://gist.github.com/enjalot/1203641
var vis = d3.selectAll(".pie_chart")
.each(function(d, i) {
if (scope.item == i) {
var w = 50, //width
h = 50, //height
normalized = 50 * (data.Engagement) / (scope.max),
r = normalized/2, // adapt radius to engagement value
color = d3.scale.ordinal().range(["#77b255", "#ffac33", "#07c"]); //custom range of colors
// map data to to be used by pie chart directive
var mapped = [{
"label": "Retweets",
"value": data.Retweets
}, {
"label": "Favorites",
"value": data.Favorites
}, {
"label": "Replies",
"value": data.Replies
}];
var vis = d3.select(this)
.append("svg:svg") //create the SVG element inside the template
.data([mapped]) //associate our data with the document
.attr("width", w) //set the width and height of our visualization (these will be attributes of the <svg> tag
.attr("height", h)
.append("svg:g") //make a group to hold our pie chart
.attr("transform", "translate(" + (w/2) + "," + (h/2) + ")") //move the center of the pie chart from 0, 0 to radius, radius
.on("click", function(d, i){
return scope.onClick({item: data});
});
var arc = d3.svg.arc() //this will create <path> elements for us using arc data
.outerRadius(r);
var pie = d3.layout.pie() //this will create arc data for us given a list of values
.value(function(d) {
return d.value;
}); //we must tell it out to access the value of each element in our data array
var arcs = vis.selectAll("g.slice") //this selects all <g> elements with class slice (there aren't any yet)
.data(pie) //associate the generated pie data (an array of arcs, each having startAngle, endAngle and value properties)
.enter() //this will create <g> elements for every "extra" data element that should be associated with a selection. The result is creating a <g> for every object in the data array
.append("svg:g") //create a group to hold each slice (we will have a <path> and a <text> element associated with each slice)
.attr("class", "slice"); //allow us to style things in the slices (like text)
arcs.append("svg:path")
.attr("fill", function(d, i) {
return color(i);
}) //set the color for each slice to be chosen from the color function defined above
.attr("d", arc); //this creates the actual SVG path using the associated data (pie) with the arc drawing function
}
})
};
}
}
}])
HTML
<ol>
<li ng-repeat="h in hashtags | orderBy:predicate:reverse | limitTo: limit">
<div class="hashtag">
<a ng-click="showTweetsForHashtag(h)">
#{{h.Hashtag}}
</a>
</div>
<div class="frequency">
{{h.Frequency}} times
</div>
<div class="engagement">
<pie-chart data="h" max="{{hashtagMaxEngagement}}" item="{{$index}}" on-click="showTweetsForHashtag(item)">
</pie-chart>
</div>
</li>
</ol>
Thanks everyone for your help !
Q.

Related

Data passed to angular d3 directive not arriving in time

I am doing a mongodb query and passing results to an angular directive that contains d3 chart logic. I am using a controller to pass myData to scope, and can print it in HTML just fine, but the directive does not load - I think myData is not resolved in time because the query is not finished yet.
This has been discussed in other threads, but very specific to problems that I find difficult to apply in this context.
I am generally wondering how I should approach this problem. I am pretty new to Angular and d3, so I would appreciate any guidance.
The HTML. Here "lsk" can be accessed nicely, and "myData" can also be accessed directly in the html. However, the directive does not load.
<div ng-hide="editMode" class="container">
<div>
<p="lsk.value1">Verdi1: {{ lsk.value1 }} </p>
<p="lsk.value2">Verdi2: {{ lsk.value2 }} </p>
<p="lsk.value3">Verdi3: {{ lsk.value3 }} </p>
<p>
<button style="margin-top:20px" class="btn btn-default" ng-click="toggleEdit()">Edit</button>
<a style="margin-top:20px" class="btn btn-default" href="#/">Back</a>
</p>
</div>
</div>
<div>MyData: {{ myData }} Length: {{ myData.length }}</div>
<div>
<bars-chart chart-data="myData"></bars-chart>
</div>
The controller (extract):
.controller("EditLskController", function($scope, $routeParams, Lsks) {
Lsks.getLsk($routeParams.lskId).then(function(doc) {
//This works, lsk can be referenced in my form.
$scope.lsk = doc.data;
//This does not work for the d3 chart, but the
//values can be referenced in a div separately.
$scope.myData = [doc.data.value1,doc.data.value2,doc.data.value3];
}, function(response) {
alert(response);
});
//By uncommenting here I get static data that does render the d3 directive
// $scope.myData = [1,10,30,40,60, 80, 20,50];
})
The service (extract):
.service("Lsks", function($http) {
this.getLsks = function() {
return $http.get("/lsks").
then(function(response) {
return response;
}, function(response) {
alert("Error finding lsks.");
});
}
this.getLsk = function(lskId) {
var url = "/lsks/" + lskId;
return $http.get(url).
then(function(response) {
return response;
}, function(response) {
alert("Error finding this lsk.");
});
}
The directive is taken from this tutorial: http://odiseo.net/angularjs/proper-use-of-d3-js-with-angular-directives. It works fine.
//camel cased directive name
//in your HTML, this will be named as bars-chart
.directive('barsChart', function ($parse) {
//explicitly creating a directive definition variable
//this may look verbose but is good for clarification purposes
//in real life you'd want to simply return the object {...}
var directiveDefinitionObject = {
//We restrict its use to an element
//as usually <bars-chart> is semantically
//more understandable
restrict: 'E',
//this is important,
//we don't want to overwrite our directive declaration
//in the HTML mark-up
replace: false,
//our data source would be an array
//passed thru chart-data attribute
scope: {data: '=chartData'},
link: function (scope, element, attrs) {
//in D3, any selection[0] contains the group
//selection[0][0] is the DOM node
//but we won't need that this time
var chart = d3.select(element[0]);
//to our original directive markup bars-chart
//we add a div with out chart stling and bind each
//data entry to the chart
chart.append("div").attr("class", "chart")
.selectAll('div')
.data(scope.data).enter().append("div")
.transition().ease("elastic")
.style("width", function(d) { return d + "%"; })
.text(function(d) { return d + "%"; });
//a little of magic: setting it's width based
//on the data value (d)
//and text all with a smooth transition
}
};
return directiveDefinitionObject;
})
Indeed, $scope.myData will be undefined when the directive loads, since the service response isn't available yet, so the datareference in your directive scope will be undefined as well, so basically you're passing nothing to the d3.data() method.
There are two approaches I can think of, first would be setting a $watch on the data inside the directive and wait for the value to change.
scope.$watch('data', function(newVal, oldVal) {
if (newVal !== oldVal) {
// Render your chart here
}
});
That would render your chart every time the bound value of datachanges.
The other workaround is emitting and event once the Lsks service is resolved and pass the data in the event, but since you already have a binding in your $scope, better use that.
This is a continued exploration of errors, while the original problem was solved and the answer accepted.
The error message received
I get this initially when the directive is loaded. I am assuming it is because of the asyncronous call.
I will post my code below - it is most likely not very good. I am trying to understand how to use watchers and directives. The challenge is that I am trying to display a d3 graph before I get the data from the DB.
//camel cased directive name
//in your HTML, this will be named as bars-chart
.directive('bulletChart', function($parse) {
var directiveDefinitionObject = {
restrict: 'E',
replace: false,
//our data source will be
//passed thru bullet-data attribute
scope: {
bdata: '=bulletData'
},
link: function(scope, element, attrs) {
var margin = {
top: 5,
right: 40,
bottom: 20,
left: 120
},
width = 800 - margin.left - margin.right,
height = 50 - margin.top - margin.bottom;
var chart = d3.bullet()
.width(width)
.height(height);
//TESTING - I need some listeners on the bdata value
scope.$watch('bdata', function(newValue, oldValue) {
if ( newValue !== oldValue ) {
console.log('bdata has new value');
} else {
console.log('bdata did not change');
}
//TESTING - added this To avoid undefined-errors on first time rendering
if (!newValue) return;
var svg = d3.select(element[0]).selectAll("svg")
.data(scope.bdata)
.enter().append("svg")
.attr("class", "bullet")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(chart);
var title = svg.append("g")
.style("text-anchor", "end")
.attr("transform", "translate(-6," + height / 2 + ")");
title.append("text")
.attr("class", "title")
.text(function(d) {
return d.title;
});
}, true); //for deep dive something
//TESTING - I want a transition when I change a value, but
//I probably cannot add a second watcher like this. There
//must be some colission.
scope.$watch('bdata', function(newValue, oldValue) {
d3.select("body").selectAll("svg")
.datum(function(d, i) {
d.ranges = scope.data[i].ranges;
d.measures = scope[i].measures;
d.markers = scope[i].markers;
return d;
})
.call(chart.duration(1000));
}, true);
}
};
return directiveDefinitionObject;
})
If this is too little information, or too messy, I understand. I will probably in that case clean this up and prepare a new question.
I am generally finding it challenging to work with a d3 graph, getting data from a DB, and changing values in the chart based on the DB data. For example I want different ranges in these bullet charts to be dynamic, calculated based on some values in the DB.
I am basing the code on this tutorial for bullet charts: https://bl.ocks.org/mbostock/4061961
Thanks!

D3:reflect data changes on visualization keeping the same array size. Trying to represent a sort algorithm visually

I'm trying to represent a selection sort visually with d3 to show some students and I'm having problems updating the data once the positions swap(I will add the transitions and delays once it's working). The positional attributes don't seem to be working as well, I don't know why, any ideas?. The codepen is here:
HTML:
<div id="canvas">
</div>
CSS:
rect{
border:1px solid black;
}
JS:
function selectionSort(array,svg){
//where the index will position
var positioningIndex=0;
var aux;
var minIndex;
var minVal=Number.MAX_VALUE;
while(positioningIndex<array.length){
//get the index of the mínimum
for(var i=positioningIndex;i<array.length;i++){
if(array[i]<minVal){
minIndex=i;
minVal=array[i];
}
}
//swap the mínimum to the positioningIndex
aux=array[minIndex];
array[minIndex]=array[positioningIndex];
array[positioningIndex]=aux;
//update visualization
svg.selectAll("rect").data(array);
minVal=Number.MAX_VALUE;
++positioningIndex;
}
return array;
}
var dataSet=[10,7,8,44];
var svg=d3.select("#canvas").selectAll("rect")
.append("svg")
.attr("width", 960)
.attr("height", 500);
var rect=svg.data(dataSet)
.enter()
.append("rect");
rect.text(function(el){
return el;
})
.attr("width", 30)
.attr("height", 30)
.attr("x", function(d, i) {
return i*5;
})
.attr("y", 30);
.style("color","green");
array=selectionSort(dataSet,svg);
You've got a lot of mixing up of html and svg elements going on there
First off, your svg element isn't getting appended:
var svg=d3.select("#canvas").selectAll("rect") // <- selectAll("rect") is causing problems
.append("svg")
No existing rect elements at the start means no svg's getting appended (do you mean to add one for each rect?) Edit: and in fact that one error is the cause of everything that happens afterwards - the selectAll("rect") needs moved to the line where elements are added to the svg - not on the line where the svg itself is added -->
var rect=svg.selectAll("rect").data(dataSet) // rect should really be g
.enter()
.append("rect");
Secondly, and because of the above error, the elements called 'rect' that are added (and added directly to the #canvas id div) aren't svg:rect objects - they're just html elements with the name 'rect' - see Is there a way to create your own html tag in HTML5?. The browser just treats them as inline elements, so none of your x's or y's make a difference they just line up one after the other
Finally, if this was svg you wouldn't be able to add text directly to a rect, you'd need to use a group (g) element and add both rect and text elements to that to keep them associated, and style("transform", translate(x,y)) the group element to move them around.
var g=svg.selectAll("g").data(dataSet) // <-- now changed from rect
.enter()
.append("g") // <-- and here
.attr ("transform", function(d,i) {
return "translate("+(i*35)+" 30)";
})
;
// texts n rects added here
g.append("text").text(function(el){
return el;
})
.attr("dy", "1em")
g.append("rect")
.attr("width", 30)
.attr("height", 30)
;
See http://codepen.io/anon/pen/bwJWEa?editors=1111

d3 + drawing svgs + incorrectly overwriting

this i my jsfiddle that shows 2 piecharts which gives me something like this in the html view:
<svg width="220" height="220">
<svg width="220" height="220">
this is my fiddle where I am trying to insert an svg before the 2 piecharts, but I am not writing it correctly, 1 of the pie charts gets over written.
Can anyone advise how I can have all 3 svgs showing?
All my code as it is in the 2nd fiddle
var width = $(document).width()-50 || 960,
height = $(document).height()-50 ||500;
//attaches/appends a svg tag to a body tag with a width and height
/* COMMENT IN */
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
data=[[10, 5],[32, 24]]
z = d3.scale.category20c();
//attaches/appends a svg tag to a body tag with a width and height
var svg2 = d3.select("body").selectAll("svg")
.data(data)
.enter().append("svg:svg")
.attr("width", 220)
.attr("height", 220)
.append("svg:g")
.attr("transform", "translate(" + 100 + "," + 100 + ")")
svg2.selectAll("path")
.data(d3.layout.pie())
.enter().append("svg:path")
.attr("d", d3.svg.arc()
.innerRadius(10)
.outerRadius(100))
.style("fill", function(d, i) { return z(i); });
NOTE: Some further background is that the first svg is a map that I have working. And What I am trying to do is create a layer on top of this which will be piecharts, in this case just 2, but you have to start somewhere :)
Your interpretation of the results is slightly wrong. The first pie chart doesn't get overwritten, it won't get rendered in the first place. In the commented in version you are appending the svg for your map, which works fine and can easily be identified by the width and height values assigned to it.
When trying to append the SVGs for your pie charts, you are selecting all SVGs in the body by doing d3.select("body").selectAll("svg"). This selection will include the previously appended SVG. You are then binding your data to the selection which will compute the data join. Since you are binding an array of data without any key function the join will be based on the index. Thus, the existing SVG will correspond to the first element in the data array and will be put to the update selection. As your code only works on the enter selection the first pie chart will never be drawn because it doesn't belong to this selection. The second one is output as expected.
One way to work around this is to use a CSS marker class for the pie charts. That way you could limit the second selection to the SVGs having the marker class:
var svg2 = d3.select("body").selectAll("svg.pie") // select with class "pie"
.data(data)
.enter().append("svg:svg")
.attr("width", 220)
.attr("height", 220)
.classed("pie", true) // set marker class for all pie charts
.append("svg:g")
.attr("transform", "translate(" + 100 + "," + 100 + ")")
Because no SVG is present having class pie, all your data will end up in the enter selection rendering an SVG containing a pie chart for every data element as expected. I have updated your JSFiddle to show the effect.

How to update/overwrite map and legend content using d3

I've put together a choropleth map using d3, helped by examples written by Mike Bostock. I'm new to d3 (and HTML, JavaScript, CSS to be honest).
I've got as far as creating the map and the legend, and being able to switch between different data sets. The map and source code can be viewed here on bl.ocks.org
Glasgow Index of Deprivation 2012
The problem I'm having now is working out how to replace the map and legend content when switching between the different datasets. As you can see at the moment, when a different dataset is selected, the content is simply added on top of the existing content.
I've tried following the advice given by Stephen Spann in this answer, and the code he provided in a working fiddle. But to no avail.
As I understand, I should add the g append to the svg variable in the beginning like so...
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
Then select it when updating like so...
var appending = svg.selectAll("g")
.attr("class", "S12000046_geo")
.data(topojson.feature(glasgowdep, glasgowdep.objects.S12000046_geo).features);
// add new elements
appending.enter().append("path");
// update existing elements
appending.style("fill",
function (d) {
return color(choro[d.id]);
})
.style("stroke", "#cfcfcf")
.attr("d", path)
// rollover functionality to display tool tips
.on("mouseover", function (d) {
tip.show(d)
d3.select(this)
.transition().duration(200)
.style("fill", "red");
})
.on("mouseout", function () {
tip.hide()
d3.select(this)
.transition().duration(200)
.style("fill",
function (d) {
return color(choro[d.id]);
});
})
// build the map legend
var legend = d3.select('#legend')
.append('ul')
.attr('class', 'list-inline');
var keys = legend.selectAll('li.key')
.data(color.range());
var legend_items = ["Low", "", "", "", "", "", "", "", "High"];
keys.enter().append('li')
.attr('class', 'key')
.style('border-top-color', String)
.text(function (d, i) {
return legend_items[i];
});
// remove old elements
appending.exit().remove();
A solution could be the following: at your code in http://bl.ocks.org/niallmackenzie/8a763afd14e195154e63 try adding the following line just before you build the map legend (line 220 in index.html):
d3.select('#legend').selectAll('ul').remove();
Every time you update your data, you empty first the #legend.
Thanks to the advice from Lars and the solution proposed by nipro, the following works. By adding the following code just above the section that builds the legend, the legend is emptied first before it gets updated:
d3.select('#legend')
.selectAll('ul')
.remove();
// build the map legend
var legend = d3.select('#legend')
...
And by doing the same for the main map, we can first empty the map before updating it:
d3.select("g")
.selectAll("path")
.remove();
// build the choropleth map
var appending = svg.selectAll("g")
...
The full working updated code can been seen on bl.ocks.org here.

Display original and updated data of the bar chart

I am trying to modify the following code found on one of the sites to display both the original and updated data in the graph. I want the updated data be in different color and still show the original data and see the change. Can anyone point me the error.
<title>d3 example</title>
<style>
.original{
fill: rgb(7, 130, 180);
}
.updated{
fill: rgb(7,200,200);
}
</style>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" src="http://d3js.org/d3.v2.min.js"></script>
<script type="text/javascript">
// Suppose there is currently one div with id "d3TutoGraphContainer" in the DOM
// We append a 600x300 empty SVG container in the div
var chart = d3.select("#d3TutoGraphContainer").append("svg").attr("width", "600").attr("height", "300");
// Create the bar chart which consists of ten SVG rectangles, one for each piece of data
var rects = chart.selectAll('rect').data([1 ,4, 5, 6, 24, 8, 12, 1, 1, 20])
.enter().append('rect')
.attr("stroke", "none")
//.attr("fill", "rgb(7, 130, 180)")
.attr('class','original')
.attr("x", 0)
.attr("y", function(d, i) { return 25 * i; } )
.attr("width", function(d) { return 20 * d; } )
.attr("height", "20");
// Transition on click managed by jQuery
rects.on('click', function() {
// Generate randomly a data set with 10 elements
var newData = [1,2,3,4];
//for (var i=0; i<10; i+=1) { newData.push(Math.floor(24 * Math.random())); }
var newRects = d3.selectAll('rects.original');
newRects.data(newData)
.transition().duration(2000).delay(200)
.attr("width", function(d) { return d * 20; } )
//.attr("fill", newColor);
.attr('class','updated');
});
</script>
I want to know if I can get control of the original data using d3.selectAll('rects.original')
If you select "rects.original" and bind data to it, you create a join with an Update, Exit and Enter selection. Im not sure i fully understand what you are trying to achieve, but if you want new data to be drawn independently of the old rects and data, you have to create a new selection for it:
var newRects = chart.selectAll("rect.new")
.data(newData)
.enter()
(...)
and draw it.
Beware that overlapping in SVG means that underlying Elements will not be displayed anymore.
Im not sure what you mean by "getting control over the original Data", it is still bound to the selection you bound it to. If you want to modify it, you have to modify the data, rebind it, and then apply a transition on the update selection.

Categories

Resources