d3.js pie-chart is truncated - angularjs

I've tried to copy a working pie-chart code from a simple single HTML file to an angular directive.
Loading chart using my directive works but the chart is truncated.
The code is a bit long so I attached a codepen demonstration
JS code:
var app = angular.module('chartApp', []);
app.controller('SalesController', ['$scope', function($scope){
$scope.salesData=[
{label: 'aa',value: 54},
{label: 'bb',value: 26},
{label: 'cc',value: 20}
];
}]);
app.directive('pieChart', function ($window) {
return {
restrict: 'EA',
template: "<svg></svg>",
link: function (scope, element, attr, ctrl) {
// render graph only when data avaliable
var data = scope[attr.chartData];
var w = 300,
h = 300 ;
var r = 150;
var d3 = $window.d3;
var color = d3.scale.category20(); //builtin range of colors
var svg = d3.select(element.find('svg')[0]);
svg.data([data])
.append('svg') //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 = svg.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
arcs.append("svg:text") //add a label to each slice
.attr("transform", function(d) { //set the label's origin to the center of the arc
// we have to make sure to set these before calling arc.centroid
d.innerRadius = 0;
d.outerRadius = r;
return "translate(" + arc.centroid(d) + ")"; //this gives us a pair of coordinates like [50, 50]
})
.attr("text-anchor", "middle")
.style("font-size","20px")//center the text on it's origin
.text(function(d, i) { return data[i].label; }); //get the label from our original data array
}
};
});
HTML:
<div ng-app="chartApp" ng-controller="SalesController">
<h1>Chart</h1>
<div pie-chart chart-data="salesData"></div>
</div>
I've tried to add a padding and margin as shown here but issue is still persist.
Any ideas?

There are several issues with your code:
Do not use a template: <svg></svg>, because later on you insert another svg in it. Simple remove that line, so that the pie chart will be inserted in your current div.
One you removed the first svg element, change the selector from var svg = d3.select(element.find('svg')[0]) to var svg = d3.select(element[0]).
And lastly, you need to chain your svg definition, so that it will point the the object that already has the translated g in it. Now you define the svg. Then you attach to it a g element, and later you attach another g.slice element, that is not placed inside the first g.
Changes at point 3 are as follows:
// From
var svg = d3.select(element[0]);
svg.data([data]) ...
// To
var svg = d3.select(element[0])
.data([data])
.append('svg') ...
Here is a fork of your codepen with a working example.
Good luck!

Related

TypeError: this.setAttribute is not a function

I'm new to d3 and reactjs. I was trying to make a bar chart with following Rect.js componenet
I've tried to debug right before // enter-update but with no avail.
import React, { Component } from 'react'
import * as d3 from 'd3';
const dataset = [14, 68, 24500, 430, 19, 1000, 5555];
const width = 600;
const height = 400;
export default class Rect extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
}
componentDidMount() {
this.renderRect();
}
componentDidUpdate() {
this.renderRect();
}
renderRect() {
this.container = d3.select(this.refs.container);
// draw refs rectangle
this.rects = this.container.selectAll("bar")
.data(dataset)
.enter();
// exit
this.rects.exit().remove();
// enter-update
this.rects = this.rects.enter()
.append("rect")
.merge(this.rects)
.attr("y", d => 24500 - d)
}
render() {
return (
<svg ref="container" width={width} height={height}>
</svg>
)
}
}
The error message is TypeError: this.setAttribute is not a function; When I debug, I found the error only appears when I have attr chained after merge(this.rects) but I don't understand why.
Here is a minimal example of the issue:
var rect = container.selectAll("rect")
.data([1,2,3])
.enter(); // return a selection of entered placeholders
var merge = rect.enter() // return a selection of entered placeholders
.append("rect") // return a selection of appended nodes
.merge(rect) // merge appended nodes with entered placeholders.
.attr("someAttribute",0); // set an attribute for both appended nodes and entered placeholders.
The issue is straightforward: the merged selection isn't what you think it is.
The selection rect is a selection of placeholder nodes created by .enter() - not actual nodes in the DOM. They have a few methods, but no method for setting attributes. This is why the above code, or your code, will produce the error: this.setAttribute is not a function, as setAttribute is not a method of an entered placeholder (see the source here).
Instead we just drop the first .enter() statement. Which will also lead to the behavior you want, as we probably don't want rect to be an enter selection, since we do exit and update after. Instead we want rect to be a selection of existing rect elements, not placeholders. Modifying the above we get to account for this change we get:
var rect = container.selectAll("rect")
.data([1,2,3])
rect.enter().append("rect").merge(rect)...
rect.exit()....
Or with your code:
// draw refs rectangle
this.rects = this.container.selectAll("bar")
.data(dataset)
// No enter here
// exit
this.rects.exit().remove();
// enter-update
this.rects = this.rects.enter()
.append("rect")
.merge(this.rects)
.attr("y", d => 24500 - d)

Convert SVG to image in PNG

i am converting angular nvd3 chart to svg using html2canvas and canvg plugings but when i convert pie chart to png then i looks same as chart but when i convert line chart or area chart then its background goes to black and some circle drown on image.
My code is
var svgElements = $("#container").find('svg');
//replace all svgs with a temp canvas
svgElements.each(function () {
var canvas, xml;
// canvg doesn't cope very well with em font sizes so find the calculated size in pixels and replace it in the element.
$.each($(this).find('[style*=em]'), function (index, el) {
$(this).css('font-size', getStyle(el, 'font-size'));
});
canvas = document.createElement("canvas");
canvas.className = "screenShotTempCanvas";
//convert SVG into a XML string
xml = (new XMLSerializer()).serializeToString(this);
// Removing the name space as IE throws an error
xml = xml.replace(/xmlns=\"http:\/\/www\.w3\.org\/2000\/svg\"/, '');
//draw the SVG onto a canvas
canvg(canvas, xml);
$(canvas).insertAfter(this);
//hide the SVG element
////this.className = "tempHide";
$(this).attr('class', 'tempHide');
$(this).hide();
});
html2canvas($("#container"), {
onrendered: function (canvas) {
var a = document.createElement("a");
a.download = "Dashboard.png";
a.href = canvas.toDataURL("image/png");
a.click();
var imgData = canvas.toDataURL('image/png');
var doc = new jsPDF('p', 'mm','a4');
var width = doc.internal.pageSize.width;
var height = doc.internal.pageSize.height;
doc.addImage(imgData, 'PNG', 0, 0, width, height);
doc.save('Dashboard.pdf');
}
});
$("#container").find('.screenShotTempCanvas').remove();
$("#container").find('.tempHide').show().removeClass('tempHide');
Help me guys.
Thanks In Advance
Your svg elements are being styled by the external stylesheet nv.d3.min.css .
canvg seems unable to access external style sheets, so you need to append it directly in your svg node.
To do so, if your style sheet is hosted on the same domain as your scripts, you can do something like :
var sheets = document.styleSheets;
var styleStr = '';
Array.prototype.forEach.call(sheets, function(sheet){
try{ // we need a try-catch block for external stylesheets that could be there...
styleStr += Array.prototype.reduce.call(sheet.cssRules, function(a, b){
return a + b.cssText; // just concatenate all our cssRules' text
}, "");
}
catch(e){console.log(e);}
});
// create our svg nodes that will hold all these rules
var defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
var style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
style.innerHTML = styleStr;
defs.appendChild(style);
// now append it in your svg node
thesvg[0].insertBefore(defs, thesvg[0].firstElementChild);
So now you can call the XMLSerializer, and canvg will be happy.
(note that this is not only a canvg limitation, the same applies for every way to draw an svg on a canvas).
Forked plunkr, where I copied the nv.d3.min.css's content to a same-origin style.css.
Very late to the conversation but I just wanted to add that the solution as described by Kaiido, put very simply, is to embed the styles into the SVG document directly.
In order to do this, you manipulate the DOM to make the SVG element look like this:
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100" version="1.1">
<defs>
<style>
.rectangleStyle{
width:200px;
height:100px;
stroke:black;
stroke-width: 6;
fill: green;
}
</style>
</defs>
<rect class="rectangleStyle"/>
</svg>

When zooming, circle gets 'dragged' from original position

I'm using mapbox.js and map.css to create a map with a simple circle drawn on it, and nothing else. (working jsfiddle) On the example that I did without angular, the zoom works as it should (meaning, it stays on the same position when zooming).
When I integrated with the example and I zoom, the circle gets dragged slightly to the top left when zooming out and to the bottom right when I zoom in, in the end returning to the original position when the zoom stops.
On the angular project I'm using require to load the scripts to the page and the view that has the map is not the first one that is loaded. I am not using a directive for leaflet, just using the files. The code is exactly the same from the example to the angular project, the only difference is that I am not using a script tag in the html file as I was in the example (since the code has been moved to the controller).
I wanted the behaviour to be the same as it was in the example but at this stage I don't know what might be causing it besides the fact that is not the view that is first being loaded.
Here's the view code:
<div id="leafletMap" style="width: 500px; height: 300px;"></div>
Here's the code in my controller:
/* globals L, $ */
define(['angular'], function (angular) {
'use strict';
/**
* #ngdoc function
* #name rmsPortalApp.controller:ViewMapCtrl
* #description
* # ViewMapCtrl
* Controller of the rmsPortalApp
*/
angular.module('rmsPortalApp.controllers.ViewMapCtrl', [])
.controller('ViewMapCtrl', function ($scope, Data) {
var ownerCircleLayer;
L.Map = L.Map.extend({
openPopup: function (popup) {
// this.closePopup(); // just comment this
this._popup = popup;
return this.addLayer(popup).fire('popupopen', {
popup: this._popup
});
}
});
var map = L.map('leafletMap', {
touchZoom: true,
dragging: true
}).setView([39.678, -8.229], 6);
/* Initialize the SVG layer */
//map._initPathRoot();
/* SVG from the map object */
//var svg = d3.select("#map").select("svg");
var southWest = [85.05, -180.01];
var northEast = [-85.06, 180.05];
var bounds = L.latLngBounds(southWest, northEast);
map.setMaxBounds(bounds);
L.tileLayer('http://api.tiles.mapbox.com/v4/bmpgp.010fd6a5/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoiYm1wZ3AiLCJhIjoiOWY4NGYwN2VjZDg0MGI1ZjdmMWI3ZjdlNGNmY2NmNmQifQ.FZ5cr4mO3iDKVkx9zz4Nkg', {
attribution: '© Powered By CGI',
minZoom: 3,
maxZoom: 18,
id: 'bmpgp.010fd6a5',
accessToken: 'pk.eyJ1IjoiYm1wZ3AiLCJhIjoiOWY4NGYwN2VjZDg0MGI1ZjdmMWI3ZjdlNGNmY2NmNmQifQ.FZ5cr4mO3iDKVkx9zz4Nkg'
}).addTo(map);
setOwnerCircle()
function setOwnerCircle() {
ownerCircleLayer = new L.layerGroup();
var ownerCircle = L.circle([39, -8], 3000, {
color: 'red',
opacity: 1,
weigth: 1,
fillOpacity: 0.0,
className: 'leaflet-zoom-animated'
});
ownerCircleLayer.addLayer(ownerCircle);
var circleBounds = ownerCircle.getBounds();
// Image popup
var endLat = circleBounds.getCenter().lat - 0.10000;
var endLon = circleBounds.getEast();
map.addLayer(ownerCircleLayer);
addedOwnerCircle = true;
}
})
});
EDIT:
I have tried putting it in the first view that is being loaded, it still happens.
Found the reason why it was happening:
When double checking the actual mapbox.js and mapbox.css I noticed that my css was wrong, upon replacing it with the right mapbox.css it stopped doing the dragging. Thank you for the help regardless.

How do I read mouse position in angularjs when a click is made?

I want to get the in-element (x, y) position of a ng-clicked element. Using clientX and clientY gets transformed by the page scroll. Using pageX and pageY is not scroll-dependent, but it is absolute. Those four properties belong to the $event object, available inside the ng-click directive. How would I calculate the in-element x and y?
In jQuery I'd do something like:
$("#something").click(function(e){
var parentOffset = $(this).parent().offset();
//or $(this).offset(); if you really just want the current element's offset
var relX = e.pageX - parentOffset.left;
var relY = e.pageY - parentOffset.top;
});
But now, I want to do it in angularjs (i.e. inside a controller, not inside a postLink function). What is a good way to do it? Is it safe to do the same but using angular.element? Or is there another way of calculating the values I need?
Edit Clarifying: In a controller, I'd try:
$scope.onClick = function($event) {
//currently I can...
var pageX = $event.pageX;
var pageY = $event.pageY;
};
But I need to transform those coordinates to clicked-element related coordinates, since those elements are absolute. Consider an appropiate HTML:
<div ng-click="onClick($event)">Nestor Kirchner</div>
this should get you started:
$scope.doClick = function(event){
var x = event.x;
var y = event.y;
var offsetX = event.offsetX;
var offsetY = event.offsetY;
// you have lots of things to try here, not sure what you want to calculate
console.log(event, x, y, offsetX, offsetY);
};
and HTML:
<div ng-click="doClick($event)"></div>
Please clarify what you are trying to calculate? Some examples?
Sample code: http://plnkr.co/edit/TnhEliSUgChCaMN4gmYn?p=preview
It will not work in mozilla firefox.
For mozilla to work use,
var offsetX = event.originalEvent.layerX;
var offsetY = event.originalEvent.layerY;

D3 with AngularJs, Multiple charts on same page with different data based on common key

My requirement is as follows can any one suggest good references for code in D3 with Angular js.
In a single page I need to include multiple graphs (it could be line, bar, pie ), or it could be combination of any of these. Each graph should get json data from Database using Ajax request for particular customer id. The json data for each graph is different but to get the data each graph uses same customer id in query to Database. The customer dropdown appears on the top of the page, when I change the customers all these graphs have repopulated with changed customer data.
I am facing problem to put all the bit and peaces together to make this design, using multiple controllers each controller having different graph, and sharing the data using factory service, passing customer id to the data query of each of these controllers, etc. Can any one point some references with code for me to have best design to start?
I followed this tutorial to put my d3 charts inside a directive: http://www.ng-newsletter.com/posts/d3-on-angular.html
After your chart is in a directive you can treat it like any other angular directive and easily reuse it with different inputs parameters.
This is the structure i have used so far and has worked for me
DATA RETRIEVING SERVICE
|
|
-------------CONTROLLER------------
| | |
TRANSLATOR 1 TRANSLATOR2 TRANSLATOR3
| | |
| | |
DIRECTIVE 1 DIRECTIVE 2 DIRECTIVE 3
A SIMPLE DIRECTIVE
this assumes that the data us passed to the data attr of the directive as a json string in the format of
[{value:x,legend:y}]
so in your template just need to do something like
<div pie-chart data={{Data}}></div>
and your controller should have a Data variable in its scope with the formated data in it.
.directive('pieChart',function(CommService,$compile,$timeout){
return{
restrict:'A',
replace:false,
scope:{
gdata:'#data'
},
template:'<div id="piecontainer_{{$id}}"></div>',
link:function(scope,element,attrs){
var data=[];
/*data=[{value:x,legend:y}]*/
function Draw(data){
var i,arr=[];
for(i=0;i<data.length;i++){
arr.push(data[i].legend);
}
var color = d3.scale.ordinal().domain(arr)
.range(scope.colors||["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
element.children().remove();
var width = $(element).width();
var height =$(element).height()||width;
var radius = Math.min(width, height) / 2;
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(10);
var pie = d3.layout.pie()
.sort(null)
.value(function(d){return d.value;});
var node=element[0];
var svg = d3.select(node).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var g = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc ");
var path=g.append("path")
.attr("d", arc)
.style("fill", function(d) { return color(d.data.legend); })
.style("stroke","#3592ac")
.style("stroke-width","5px")
.style("position","absolute");
}
scope.$watch('data', function (value) {
Draw(value);
});
}
};
})

Resources