angular directive d3 stuck when hidden on mobile - javascript

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.

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!

Selecting SVG with D3

I have the following function:
function myfunc(d) {
var svg = d3.select('.map-wrap svg');
console.log('svg is:');
console.log(svg);
// interesting stuff happens later ...
}
I call that function on a mouseover event. Here is code where myfunc is supposed to be called.
myChart.width(800)
.height(800)
.dimension(xDim)
.group(xDimGrp)
.projection(projection)
.colors(quantize)
.colorDomain(quantize.domain())
.colorCalculator(function (d) { return d ? getColorClass(myChart.colors()(d)) : '#ccc'; })
.overlayGeoJson(map.features, 'states', function (d) {
return d.properties.state;
}).on('mouseover', myfunc);
When I print out svg I expect to see this:
instead, I see the following:
I see 0: null instead of 0: svg, why is this happening? How can I select the SVG in a way that will give me what is shown in first picture?
.map-wrap is like so:
It was as Cyril said, after debugging better, I realised that it was indeed being called before the creation of the svg element.

groups.exit(...).watchTransition is not a function

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.

D3 - How to call an array for a tooltip

I am trying to make it so that the name of each item shows up in the tooltip when you hover. I am sure there is a straightforward answer to this, but I am new to D3 so I am not sure what it is.
Example here: http://www.chloesilver.ca/favouritethings/object/
You can see that when you hover, some crazy code shows up so I obviously did it wrong.
In the D3 script, I did this:
$('svg circle').tipsy({
gravity: 'w',
html: true,
title: function() {
var o = colors.domain;
return o;
}
});
Where I am trying to call a specific domain label that was specified previously in the code. I was able to do this with a CSV, but the sticky bit here is that all the information is held within the script inside the HTML document.
Delete the code for the tooltips and after line 236: .call(force.drag) add the following:
.on("mouseover", function(d) {
$(this).tipsy({
gravity: 'w',
html: true,
title: function() {
return d.name;
}
})
});
You should now see the name of each item as a tooltip.

Legend toggling for d3.js pie chart

I'm working on a d3.js application.
In this example I am trying to toggle the slices when the user clicks on the legend components. It will initially take the complete data as its source, but if there is a previous manipulated data source will use that as a base. I've tried to hook into the toggling functionality as the legend is manipulated. I would prefer to separate the functionality - but wasn't sure how else to know if the slice is to be active or not.
Its not working as expected though, especially when trying to handle multiple active/non active slices.
http://jsfiddle.net/Qh9X5/3282/
onLegendClick: function(dt, i){
//_toggle rectangle in legend
var completeData = jQuery.extend(true, [], methods.currentDataSet);
newDataSet = completeData;
if(methods.manipulatedData){
newDataSet = methods.manipulatedData;
}
d3.selectAll('rect')
.data([dt], function(d) {
return d.data.label;
})
.style("fill-opacity", function(d, j) {
var isActive = Math.abs(1-d3.select(this).style("fill-opacity"));
if(isActive){
newDataSet[j].total = completeData[j].total;
}else{
newDataSet[j].total = 0;
}
return isActive;
});
//animate slices
methods.animateSlices(newDataSet);
//stash manipulated data
methods.manipulatedData = newDataSet;
}
Here is the onLegendClick function.
I am toggling the opacity of the inner fill on the rectangle when the user clicks.
I've tried to modify the value of the data accordingly - although it is not handling multiple toggles.
http://jsfiddle.net/Qh9X5/3324/
Ideally if the user tries to deactivate all slices, I would want it to reset the chart and restore all the slices in the process. Would be keen to streamline the code and maybe separate the logic and presentation layer for the styling of the rectangles in the legend.
line 234
onLegendClick: function(dt, i){
//_toggle rectangle in legend
var completeData = jQuery.extend(true, [], methods.currentDataSet);
newDataSet = completeData;
if(methods.manipulatedData){
newDataSet = methods.manipulatedData;
}
d3.selectAll('rect')
.data([dt], function(d) {
return d.data.label;
})
.style("fill-opacity", function(d, j) {
var isActive = Math.abs(1-d3.select(this).style("fill-opacity"));
if(isActive){
newDataSet[j].total = completeData[j].total;
}else{
newDataSet[j].total = 0;
}
return isActive;
});
//animate slices
methods.animateSlices(newDataSet);
//stash manipulated data
methods.manipulatedData = newDataSet;
}

Categories

Resources