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.
Related
I want to populate the data inside the parent graph node, that is I want to add nested svg on click event using d3.js.
I have tried following code but the output is looking very different I want to show the graph inside the after on click event of first svg node.
var svg = d3.select("body").append("svg")
.attr("width", 600)
.attr("height", 400)
svg.append('rect')
.attr('class', 'content shape')
.attr("width", 20)
.attr("height", 20)
.attr("stroke", "black")
.attr("stroke-width", 1)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
})
.on("click", createMap);
var createMap (mapData){
if(mapdata.data){
d3.select(this).attr("width", 200)
.attr("height", 200)
.call(function(d){
var nodeMap = getGeneratedmap(mapdata.data);
return nodeMap
}
}
}
var getGeneratedmap(mapdata){
var generatedmapSVG = d3.select("#subnodeMap").append("svg")
....
....(logic for map generation)
....
return generatedmapSvg
}
<body>
<div id="mainMap">
<svg></svg>
</div>
<div id="subnodeMap"></div>
</body>
Please guide me and give the proper way to solve the above issue. Thanks in advance.
Your first SVG, the var svg is being appended to <body>. The second SVG is being appended to the div with ID #subnodemap, so you are not actually nesting the first SVG inside the second. You probably want to do something like:
var generatedmapSVG = svg.append("svg")
That's about as much as I can help you without a working example in Codepen or similar, hope it gets you towards a solution.
I want to implement a list on right-click of a data node. In order to do so I came across d3-context-menu plugin of d3.js. The problem I am facing is that the div element is getting appened outside the body tag.
I have never seen such an issue before.
I am following the plugin example given here:
http://plnkr.co/edit/hAx36JQhb0RsvVn7TomS?p=preview
This is the link to the library documentation:
https://github.com/patorjk/d3-context-menu
I have no clue why it is behaving in such manner. My code structure looks like this :
eventGroup = focusClip.selectAll(".event").data(data);
// Enter phase ---
eventGroupEnter = eventGroup.enter().append("svg");
eventGroupEnter.append("rect");
eventGroupEnter.append("circle");
eventGroupEnter.append("text");
// Event Group
eventGroup
.attr("class", "event")
.attr("x", function(d) {
return parseInt(x(d.time)) - 10;
}) // offset for the bg and center of dot
.attr("y", function(d) {
return parseInt(y(d.plotY));
})
.attr("width", function(d) {
return parseInt((d.label.length / 2)) + 60 + "em";
})
.attr("height", "20");
// Background
eventGroup.select("rect")
.attr("x", 0) // removes the "<rect> attribute x: Expected length, 'NaN'" Error
.attr("y", 4)
.attr("width", "100%")
.attr("height", "12")
.attr("fill", "url(#event-bg)");
menu = [{
title: "Item #1"
}];
// Dot
eventGroup.select("circle")
.attr("class", "dot")
.attr("r", 4)
.attr("cx", 10)
.attr("cy", 10)
.attr("fill", function(d) {
return d.evtColor ? d.evtColor : "#229ae5";
})
.attr("stroke", function(d) {
return d.evtColor ? d.evtColor : "#229ae5";
})
.attr("stroke-width", 2)
.on("contextmenu", d3.contextMenu(menu, function() {
console.log("Quick! Before the menu appears!");
}))
.on("mouseenter", tooltip.mouseover)
.on("mouseleave", tooltip.mouseout)
.on("click", annotateBox.click);
In order to explain it well I am adding the image of the chart:
The right click event is being called on the "dot" part of the event. Why would div element get appended outside the body?
This seems to be by design. If you look at the source code of that plugin, you'll see:
d3.selectAll('.d3-context-menu').data([1])
.enter()
.append('div')
.attr('class', 'd3-context-menu');
Since selectAll is called on the root, the div will be appended to the <html>, not to the <body>.
So, the author either did this intentionally or she/he forgot that d3.selectAll is different from selection.selectAll.
Here is a basic demo, click "Run code snippet", open your browser's dev tools and inspect the snippet window.
d3.selectAll("foo")
.data([1])
.enter()
.append("div")
.attr("class", "test")
<script src="https://d3js.org/d3.v3.min.js"></script>
You're gonna see this:
<html>
<head>...</head>
<body>...</body>
<div class="test"></div>
</html>
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!
I have been using D3 to create fancy animated charts, and the examples are great. However, I'm trying to do something seemingly a lot more basic, and having trouble - binding data to a simple list of DIVs.
I set up enter() to initialize elements at opacity 0, transition() to fade them in, and exit() to fade them out and remove them. enter() and exit() seem to be working fine - however, when an update contains an existing element already in the list, it seems to get partially removed - the containing DIV remains, but the contents disappear. I can't understand why the contents of the element would get changed in this way.
My code is as follows:
var data = [...];
sorted = data.sort(function(a, b) { return d3.descending(a.id, b.id); });
var tweet = tweetsBox
.selectAll('div')
.data(sorted, function(d) { return d.id; });
var enterDiv = tweet.enter()
.append("div")
.attr("class", "tweetdiv")
.style("opacity", 0);
enterDiv.append("div")
.attr("class", "username")
.text(function(d) { return "#" + d.username });
enterDiv.append("div")
.attr("class", "displayname")
.text(function(d) { return d.displayname });
enterDiv.append("div")
.attr("class", "date")
.text(function(d) { return d.date });
enterDiv.append("div")
.attr("class", "text")
.text(function(d) { return d.text });
tweet.transition()
.delay(200)
.style("opacity", 1);
tweet.exit()
.transition()
.duration(200)
.style("opacity", 0)
.remove();
I also set up a jsFiddle here demonstrating the issue.
The problem is that you're selecting the divs you created, but create more than one div per data element. When updating, d3 tries to match the data to the nested divs. As you're already assigning a special class to the top-level divs, the fix is very simple. Replace
.selectAll('div')
with
.selectAll('.tweetdiv')
Ok so I have the following code example where I have circles in an svg element. Each circle has a click event and I'm trying to animate the circle that was clicked. Currently all circles animate because I'm referring to the bubble object. What I want is to refer to the clicked object its self and not the other ones:
var data_items=[100,200,300];
var svg = d3.select("#chart")
.append("svg").attr("width", 800).attr("height", 600);
var g = svg.selectAll(".bubbleContainer")
.data(data_items)
.enter().append("g")
.attr("class","bubbleContainer");
var bubble = g.append("circle")
.attr("class","bubble")
.attr("cx", function(d) {
return d;
})
.attr("cy", function(d) {
return d
})
.attr("r", function(d) {
return d/2
})
.on("click",function(d){
bubble
.transition()
.duration(1000)
.attr("r",1000)
})
Any help is much appreciated
Thanks!
What Lars Kotthoff wrote would work. Alternatively – and I'm not sure which is more idiomatic:
Inside the click handler, the this context refers to the clicked DOM element.
So the following would do it too:
.on("click",function(d){
d3.select(this)
.transition()
.duration(1000)
.attr("r",1000)
});
You can use d3.event.target to access the element that is being clicked in the event handler. See for example this jsfiddle.