I have a simple directive that renders a d3 svg word cloud.
The directive should be visible only on desktop, so I remove it on mobile with bootstrap class: "xs-hidden"
the problem is the browser tab gets stuck as angular probably looks for the directive but cant find it.
I altered and added the render method of the directive scope,
the tab gets stuck when we call
layout.start();
only when the div is hidden with "hidden-xs"
** tab gets stuck means - it simply shows "loading" the page is loaded but the page is stuck, I cant refresh, I need to close the tab.
some code:
link: function(scope, element, attrs) {
//stuff....
scope.render(['word', 'word2']);
scope.render = function(words) {
if (!words || !words.length) return;
var maxSize = d3.max(words, function(d) { return d.count; });
var minSize = d3.min(words, function(d) { return d.count; });
var fontScale = d3.scale.linear()
.domain([minSize, maxSize])
.range([16, 40]);
var layout = d3.layout.cloud()
.size([svgWidth, 350])
.words(words.map(function(d) {
return {text: d.word, size: d.count};
}))
.padding(10)
.rotate(function() { return 0; })
.font("Arvo")
.fontSize(function(d) { return fontScale(d.size) })
.fontWeight(700)
.spiral('rectangular')
.on("end", draw);
//when this is called the tab get stuck and must be closed
layout.start();
function draw(words) {
//dont care
}
}
Have you checked if your element where word cloud needs to be rendered is present in DOM or not ?
The class you mentioned has:
.hidden-xs {
display: none !important;
}
So element may not be in DOM when you try to render the word cloud.
I have a multi-bar chart in which I've assigned a click event to the bars. This works fine until a user changes the chart type from grouped to stacked, at which point I've discovered that I need to reassign the onClick handler. This all seems to work correctly.
The problem is that after my click handler runs, whether or not the user has changed the chart type yet previously, attempting to change the chart type will result in a "groups.exit(...).watchTransition is not a function" JS error.
Chart definition:
nv.addGraph(function() {
// Defining the chart itself
var chart = nv.models.multiBarHorizontalChart()
.x(function(d) { return d.label })
.y(function(d) { return d.value })
.margin({top: 30, right: 20, bottom: 50, left: 275})
.showValues(true) //Show bar value next to each bar.
.tooltips(true) //Show tooltips on hover.
.valueFormat(d3.format('$,.2f'))
.groupSpacing(0.5)
.showControls(true); //Allow user to switch between "Grouped" and "Stacked" mode.
chart.yAxis
.tickFormat(d3.format('$,.2f'));
d3.select('#chart2 svg')
.datum(barData)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
},
function(){
// Set the click handler. This part works fine, but the onclick handler goes away after changing the chart type and thus needs redefined below.
// PROBLEM POINT: once this code is run, the user can no longer change the chart type. They just keep getting "groups.exit(...).watchTransition is not a function"
d3.selectAll(".nv-bar").on('click',
function(e){
var canName = e.label.split('(');
var canName = $.trim(canName[0]);
var searchTerm = canName + ' ' + e.key;
var detUrl = "/details.cfm?canName=" + encodeURIComponent(canName) + "&searchTerm=" + encodeURIComponent(searchTerm);
$("#detailsDiv").html("Loading...");
$("#detailsDiv").load(detUrl);
location.href = "#details";
});
// If I try to redefine the bar click handler in the radio button's "click" event it overwrites the built in JS used to change the chart type, so instead
// I handle it onMouseUp.
d3.selectAll(".nv-series").on('mouseup',
function(e){
setTimeout(function(){
// Just running this directly on mouseUp doesn't work. Apparently the chart needs time to load first. So we do it 100ms later, which works fine.
d3.selectAll(".nv-bar").on('click',
function(e){
var canName = e.label.split('(');
var canName = $.trim(canName[0]);
var searchTerm = canName + ' ' + e.key;
var detUrl = "/details.cfm?canName=" + encodeURIComponent(canName) + "&searchTerm=" + encodeURIComponent(searchTerm);
$("#detailsDiv").html("Loading...");
$("#detailsDiv").load(detUrl);
location.href = "#details";
});
}, 100);
});
});
watchTransition is defined by nvd3 on D3's selection prototype, if you have nv.d3.js loaded in the browser, you should be able to step with the debugger into the following code before, any chart is rendered:
d3.selection.prototype.watchTransition = function(renderWatch){
var args = [this].concat([].slice.call(arguments, 1));
return renderWatch.transition.apply(renderWatch, args);
};
I had the same issue. The reason was that I was using webpack, which bundled D3 inside my application, so the D3 that was used to draw the chart was not the D3 that NVD3 visits to add the function to the prototype. So if you are using webpack or browserify make sure to exclude D3 and add it only as reference script.
We solved this issue by downgrading to d3 3.4.4, as advised by this comment.
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.
I've setup an example to demonstrate the issue's I've encountered:
To be brief, I'm using d3 to render a map of the united states. I'm appending relevant data attributes to handle click events and identify which state was clicked.
On click events the following is preformed:
I'm grabbing the US County topojson file (which contains ALL US
Counties).
Removing irrelevant counties from the data and rendering them on the
map of the state that was clicked.
I can't seem to figure out what is going on behind the scenes that is causing some of the counties to be drawn while others are ignored.
When I log the data that is returned from the filtered list, I'm displaying the accurate number of counties, but they are only partially drawn on the map. Some states don't return any. Pennsylvania and Texas partially work.
I've checked the data and the comparison operations, but I'm thinking this may have to do with arcs properties being mismatched.
If I utilize the Counties JSON file to render the entire map of the united states they are all present.
If anyone can help shed some light on what might be happening that would be great.
svg {
fill:#cccccc;
height:100%;
width:100%;
}
.subunit{
outline:#000000;
stroke:#FFFFFF;
stroke-width: 1px;
}
.subunit:hover{
fill:#ffffff;
stroke:#FFFFFF;
stroke-width="10";
}
<body>
<script src="http://www.cleanandgreenfuels.org/jquery-1.11.3.min.js"></script>
<script src="http://www.cleanandgreenfuels.org/d3.v3.min.js"></script>
<script src="http://www.cleanandgreenfuels.org/topojson.v1.min.js"></script>
<script>
var width = window.innerWidth;
height = window.innerHeight;
var projection = d3.geo.albers()
.scale(1500)
.translate([width / 2, height / 2]);
//d3.geo.transverseMercator()
//.rotate([77 + 45 / 60, -39 - 20 / 60]);
//.rotate([77+45/60,-40-10/60])
//.scale(500)
//.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width+"px")
.attr("height", height+"px");
d3.json("http://www.cleanandgreenfuels.org/usstates2.json.php", function(error, us, init) {
//svg.append("path")
// .datum(topojson.feature(us, us.objects.counties))
//.attr("d", path);
function init(){
$( document ).ready(function() {
$('.subunit').on('click',function(){
var stateid = $(this).attr("data-stateid");
function clearStates(stateid){
d3.json("http://www.cleanandgreenfuels.org/uscounties2.json.php", function(error, us) {
console.log(us);
console.log("DATA CLICKED ID "+stateid);
test = jQuery.grep(us.objects.counties.geometries, function(n){
return (n.properties.stateid == stateid);
});
us.objects.counties.geometries = test;
console.log(test.length);
console.log(us.objects.counties.geometries.length);
var test = topojson.feature(us, us.objects.counties).features;
console.log(test);
console.log(test.length);
svg.selectAll(".subunit")
.data(test)
.enter().append("path")
.attr("class", function(d) { return "subunit"; })
.attr("d", path)
.attr("data-countyid", function(r){ return r.id; });
});
}
clearStates(stateid);
});
});
}
svg.selectAll(".subunit")
.data(topojson.feature(us, us.objects.us).features)
.enter().append("path")
.attr("class", function(d) { return "subunit"; })
.attr("d", path)
.attr("data-stateid", function(r){ return r.id; });
init();
});
</script>
</body>
It appears as if I was attempting to utilize some outdated features, using topojson.mesh and .datum() to add the new data has resolved this issue, but has introduced a new error.
Now it appears as if the polygons that are rendered must be in sequence to be drawn properly this way.
I think the data going in should be cleaned up to optimize the way d3 is designed to function, but I'd still like to know more about how it is rendering this information that is obtained from the dataset.
function clearStates(stateid){
d3.json("http://www.cleanandgreenfuels.org/uscounties2.json.php", function(error, us) {
console.log(us);
console.log("DATA CLICKED ID "+stateid);
test = jQuery.grep(us.objects.counties.geometries, function(n){
return (n.properties.stateid == stateid);
});
us.objects.counties.geometries = test;
console.log(test.length);
console.log(us.objects.counties.geometries.length);
**var test = topojson.mesh(us, us.objects.counties);**
console.log(test);
console.log(test.length);
**svg.append("path")
.datum(test)
.attr("class", function(d) { return "subunit"; })
.attr("d", path)
.attr("data-countyid", function(r){ return r.id; });**
});
}
So I have an angular application that is using ui-router. Say I want to display a bar graph inside div.wtflike so:
index.html
<div ui-view ></div>
state1.html
<div class="wtf"></div>
js:
var dataset = [5,3,2,2,10];
d3.select('.wtf').selectAll('div')
.data(dataset)
.enter()
.append('div')
.attr('class', 'bar')
.style('height', function (d) {
return d * 5 + 'px';
});
Does the js have to be in a controller? if so how to I make sure the document is ready?
You could go with custom directives. It gives access to the DOM without junking up your controller.
app.directive('bindDeeThreeStuff', function($window){
var d3 = $window.d3;
return function(scope, elem, attrs) {
var d3Elem = d3.select(elem[0]);
scope.$watch(attrs.bindDeeThreeStuff, function(val) {
d3Elem.selectAll('.bar').data(val)
.enter()
.append('div')
.attr('class', 'bar')
.style('height', function(d) {
return d * 5
});
});
};
});
And in your view:
<div bind-dee-three-stuff="myData"></div>
and in your controller:
$scope.myData = [10,20,30,40,50];
Edit: Example
Here's a quick example of a simple directive that binds data with d3.