I've placed several pie charts on a map and want to adjust their size based on a corresponding value from a csv-file ("Total", in this example). But no matter how I adjust the radius, the pies won't show. Is there something important I missed?
My code so far:
d3.csv("Bevoelkerung-Altersstruktur-2010-Summe.csv", function drawPies (data) {
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return +d});
var arc = d3.svg.arc()
.innerRadius(0)
.outerRadius(function(d) {
return d.Total; });
var pies = svg.selectAll('.pie')
.data(data)
.enter()
.append('g')
.attr('class', 'pie')
.attr("transform", function(d) {
return "translate(" + projection([d.lon, d.lat])[0] + "," + projection([d.lon, d.lat])[1] + ")";
});
var color = d3.scale.ordinal()
.range(["#98abc5", "#7b6888", "#a05d56", "#d0743c",])
.domain(d3.range(0,4));
pies.selectAll('.slice')
.data(function(d){
return pie([d.Group1, d.Group2, d.Group3, d.Group4]); })
.enter()
.append('path')
.attr('d', arc)
.style('fill', function(d,i){
return color(i);
});
Here is the link to the complete code.
I could not run in a correct way your code so I moved a few things to get it working under a plnkr.
// You had all the async calls to remote data files nested which I
// recommend not doing. I separated your GeoJSON rendering and your
// pie rendering into two distinct functions.
// Start GeoJSON rendering
d3.csv("Jugendarbeitslosigkeit.csv", function(data) {
//Load in GeoJSON data
d3.json("PolenReg2.json", function(json) {
data.forEach(function(d, i) {
// ...more code
// This is a tricky part
// Since we separated the polygon and pie rendering
// and the polygon calls will take longer due to size
// the group containing the polygons will be rendered
// last, thus rendering the group after your pie group.
// This will make your pies stay behind the polygon paths
// that's why we use the insert. In order to position
// the polygons layer below the pies.
svg
.insert('g', ':first-child')
// ... more code
// End GeoJSON rendering
// Start Pie rendering
d3.csv("Bevoelkerung-Altersstruktur-2010-Summe.csv", function(err, data) {
// Set our large pie function
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
return +d.key;
});
// ... more code
// End Pie rendering
The important part is here:
var pies = svg
.append('g') // Add a group with the class 'big-pies'
.attr('class', 'big-pies')
.selectAll('.pie') // Join data to create groups by each polygon (as far as I understand)
.data(data)
.enter()
.append('g')
.attr('class', 'pie')
.attr("transform", function(d) {
var proj = projection([d.lon, d.lat]);
return "translate(" + proj[0] + "," + proj[1] + ")";
})
.selectAll('.slice') // Start pie - data join
.data(function(d) {
// set slice data with additional total value
// so that we can use this value at the attr d
// function
return pie([{
key: d.Kinder,
tot: d.total
}, {
key: d.Jugendliche,
tot: d.total
}, {
key: d.Erwachsene,
tot: d.total
}, {
key: d.Rentner,
tot: d.total
}]);
})
.enter()
.append('path')
.attr('d', function(d, i) {
// return the arc function with outer radius increased by the total value
return d3.svg.arc().innerRadius(0).outerRadius(d.data.tot * 2).call(d, d)
})
.style('fill', function(d, i) {
return c10(i);
});
Plnkr: https://plnkr.co/edit/CwiFnNmfIleo5zZ6BseW?p=preview
Related
I have a line chart (or, more properly a connected scatterplot) where I plot pies/donuts around the points. So, there is a data set that specifies the date and mood for plotting the points along with two other parameters, pos and neg, providing the values to go into the pie chart. The overall data that describes the points is called plotPoints.
That all works great, but what I still would like to do is to set the radius of the pie to be a function of the sum of pos + neg.
When I plot out the points, I can access all of the data with a function(d). In each pie, however, function(d) returns the data about the slices, one at a time. d contains the data for the current slice, not the overall data. For each pie, the first time arc is called it has the frequency for the first pie slice and the second time it is called it has the frequency for the second pie slice.
How do I refer to the current plotPoints properties when drawing the arc so that I can change the radius of the pie/donut to represent the sum of plotPoints[i].pos + plotPoints[i].neg?
The relevant code looks like this:
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(8);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d; });
var p = moodChart.selectAll(".pieContainer")
.data(plotPoints).enter()
.append("g")
.attr("class","pieContainer")
.attr("transform", function(d,i) {return "translate(" + (x(d.date)) + "," + (y(d.mood)) + ")"});
p.append("title")
.text(function(d) { return shortDateFormat(d.date) +", " + d.mood.toFixed(2) });
var g = p.selectAll(".arc")
.data(function (d) {return pie([d.neg, d.pos])})
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d,i) { return i==0 ? "brown" : "green"; });
It's tough to answer this authoritatively without a bit more code/data to look at but in this situation I usually stash the needed variables in my data-bindings so they are available later:
var g = p.selectAll(".arc")
.data(function (d) {
var total = d.neg + d.pos,
pie_data = pie([d.neg, d.pos]),
point_arc = d3.svg.arc()
.outerRadius((total * radius) - 10) //<-- set radius based on total
.innerRadius((total * radius) - 8);
pie_data.forEach(function(d1){
d1.data.arc = point_arc; //<-- stash the arc for this point in our data bindings
});
return pie_data;
});
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", function(d){
return d.data.arc
})
.style("fill", function(d,i) { return i==0 ? "brown" : "green"; });
The data
I've got a .csv which has data of the following format:
city, industry, X, Non-X, latitude, longitude, Total
The goal
I've made a map of the country, and have created some g elements over each capital (using the latitude and longitude data). Within each g element, I'd like to create a pie chart, showing the proportions of X and Non-X. I'd like the radius of each pie to be a function of Total.
The code
Skipping over creating the map, etc, here's my function to create the pie charts (scroll down; a bit long):
function drawPies(csv) {
var radius = d3.scale.sqrt()
.domain([1, d3.max(csv, function(d) {
return d.Total;
})])
.range([2, 25]);
var color = d3.scale.ordinal();
var arc = d3.svg.arc();
var pie = d3.layout.pie();
var circleLayer = d3.select("svg")
.append("g")
.attr("class", "circleLayer");
var pies = circleLayer.selectAll("g")
.data(csv)
.enter()
.append("g")
.attr("id", function(d) { return d.city; })
.attr("class", "capitals")
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.attr("transform", function(d) {
return "translate(" + projection([d.lon, d.lat])[0] + "," + projection([d.lon, d.lat])[1] + ")";
});
arc.outerRadius(function (d) { return radius(d.Total); })
.innerRadius(0);
pies.append("text")
.text("Hi");
pies.append("path")
.attr("d", arc) //this is not working...
.style("fill", function(d) { return color(d['X']); });
}
pies.append("text") works fine; the text elements appear at the right lat and long. Something's wrong with my arc variable; and I think it must be to do with how I've defined .outerRadius(). The path elements are created, but I'm getting NaN (and, hence, no pies). Here's what the HTML looks like from an example g element for each pie chart:
<path d="MNaN,NaNA4.859251238796309,4.859251238796309 0 1,1 NaN,NaNL0,0Z">
I suspect it's something with how I'm binding the data, and how I'm passing the data into arc.outerRadius(). I've tried re-binding the data to each pies element; I've also tried binding .data(d3.layout.pie()) (following Mike Bostock's example here), but that just doesn't generate d altogether.
What I'd like to do, is have d['X'] and d['Non-X'] be used to generate each pie's arc.outerRadius() (I think) - any guidance would be much appreciated!
You need a nested selection:
var arc = d3.svg.arc().innerRadius(0).outerRadius(20)
pie = d3.layout.pie()
.value(function(d){ return d });
var pies = svg.selectAll('.pie') //<-- this is each row
.data(data)
.enter()
.append('g')
.attr('class', 'pie')
...
pies.selectAll('.slice')
.data(function(d){
return pie([d.X, d.NonX]); //<-- this is each slice
})
.enter()
.append('path')
.attr('class', 'slice')
.attr('d', arc);
Working code:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
</head>
<body>
<script>
var data = [{
X: Math.random(),
NonX: Math.random(),
Total: Math.random() * 10
},{
X: Math.random(),
NonX: Math.random(),
Total: Math.random() * 10
},{
X: Math.random(),
NonX: Math.random(),
Total: Math.random() * 10
},{
X: Math.random(),
NonX: Math.random(),
Total: Math.random() * 10
}];
data.forEach(function(d){
d.pieVals = [d.X, d.NonX];
})
var svg = d3.select('body')
.append('svg')
.attr('width', 500)
.attr('height', 500);
var arc = d3.svg.arc().innerRadius(0).outerRadius(20)
pie = d3.layout.pie()
.value(function(d){ return d }),
color = d3.scale.category10();
var pies = svg.selectAll('.pie')
.data(data)
.enter()
.append('g')
.attr('class', 'pie')
.attr('transform',function(d,i){
return 'translate(' + 20 + ',' + (i + 1) * 40 + ')';
})
pies.selectAll('.slice')
.data(function(d){
return pie([d.X, d.NonX]);
})
.enter()
.append('path')
.attr('d', arc)
.style('fill', function(d,i){
return color(i);
});
</script>
</body>
</html>
I like dcjs, http://bl.ocks.org/d3noob/6584483 but the problem is I see no labels anywhere for the line chart (Events Per Hour). Is it possible to add a label that shows up just above the data point, or even better, within a circular dot at the tip of each data point?
I attempted to apply the concepts in the pull request and came up with:
function getLayers(chart){
var chartBody = chart.chartBodyG();
var layersList = chartBody.selectAll('g.label-list');
if (layersList.empty()) {
layersList = chartBody.append('g').attr('class', 'label-list');
}
var layers = layersList.data(chart.data());
return layers;
}
function addDataLabelToLineChart(chart){
var LABEL_FONTSIZE = 50;
var LABEL_PADDING = -19;
var layers = getLayers(chart);
layers.each(function (d, layerIndex) {
var layer = d3.select(this);
var labels = layer.selectAll('text.lineLabel')
.data(d.values, dc.pluck('x'));
labels.enter()
.append('text')
.attr('class', 'lineLabel')
.attr('text-anchor', 'middle')
.attr('x', function (d) {
return dc.utils.safeNumber(chart.x()(d.x));
})
.attr('y', function (d) {
var y = chart.y()(d.y + d.y0) - LABEL_PADDING;
return dc.utils.safeNumber(y);
})
.attr('fill', 'white')
.style('font-size', LABEL_FONTSIZE + "px")
.text(function (d) {
return chart.label()(d);
});
dc.transition(labels.exit(), chart.transitionDuration())
.attr('height', 0)
.remove();
});
}
I changed the "layers" to be a new group rather than using the existing "stack-list" group so that it would be added after the data points and therefore render on top of them.
Here is a fiddle of this hack: https://jsfiddle.net/bsx0vmok/
I'm attempting to draw a graph/network in which node contents should be filled with a third party library. So far, I was able to draw the graph with d3 with a container () for each node to be filled later on. My problem is that the containers seem not to yet exist when there are referred for drawing. Adding an onload event doesn't work either, but using a onclick event shows that everything is in place to work and most probably drawing starts before the DOM elements are actually created.
What is the best practice to make sure d3 generated DOM elements are created before starting assigning them content? The third party JS library I use only requires the div id to draw content in.
From arrays of nodes and relationships return by Neo4j, I build a graph with d3 as follow. Images to be displayed in nodes are stored in the neo4j data base as Base64 strings.
var width = 500, height = 500;
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var col = obj.columns;
var data = obj.data;
var nodes = obj.data[0][0].nodes;
var nodeMap = {};
nodes.forEach(function(x) { nodeMap[x.label] = x; });
var edges = obj.data[0][0].edges;
var links = edges.map(function(x) {
return { source: nodeMap[x.source], target: nodeMap[x.target], value: x.value };
});
var svg = d3.select("#graph").append("svg")
.attr("width", width)
.attr("height", height)
.attr("pointer-events", "all")
.append('g')
.call(d3.behavior.zoom().on("zoom", redraw))
.append('g');
var force = d3.layout.force()
.gravity(.3)
.distance(150)
.charge(-4000)
.size([width, height]);
var drag = force.drag()
.on("dragstart", dragstart);
force
.nodes(nodes)
.links(links)
.friction(0.8)
.start();
var link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("svg:g")
.attr("class", "node")
.on("dblclick", dblclick)
.call(force.drag);
node.append("circle")
.attr("r", 50);
node.append("image")
// display structure in nodes
.attr("xlink:href", function(d){
if (d.imgB64) {
return 'data:image/png;base64, ' + d.imgB64 ;
}
})
.attr("x", -40)
.attr("y", -40)
.attr("width", 80)
.attr("height", 80);
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
That code works fine. Hence, what I'm attempting to do is to replace the Base64 images with canvas drawings generated by a third party library.
This library works as follow:
var myCanvas = newCanvasViewer(id, data);
This function knows how to generate an image from the given data and puts the result in the DOM element, if the element already exists in the DOM, or generate a new canvas element in the DOM, directly in but unfortunately erasing any other existing DOM elements.
So, I changed the d3 above code, replacing the node.append('image') block with:
node.append("canvas")
.attr("id", function(d) {
var cnv = newCanvasViewer(d.name, d.data);
return d.name;
});
This obviously doesn't work, canvas objects are displayed but not in the nodes, probably because the DOM element doesn't exist yet when calling newCanvasViewer. Furthermore, the d3 graph is overwritten.
When setting an onclick function calling newCanvasViewer, the drawing shows up within the nodes on click.
node.append("circle")
.attr("r", 50)
.attr("onclick", function(d) {
return "newCanvasViewer('"+d.name+"', '"+d.data+"')";
});
node.append("canvas")
.attr("id", function(d) {
return d.name;
});
Since I would like each node to display its canvas drawing from start, I was wondering when to call the newCanvasViewer function? I guess an oncreate function at each node would make it, but doesn't exist.
Would a call back function work? Should I call it once d3 is finished with the whole network drawing?
In order to be more comprehensive, here is the HTML and javascript code with callback function to attempt drawing canvas content once d3 d3 is done drawing. I'm still stuck, the canvas sample is actually displayed but no canvas content show in nodes, probably due to timing between generating canvas containers and using them.
HTML:
<body onload="init(); cypherQuery()">
<div id="graph" align="left" valign="top"></div>
<div id="sample">
<canvas id="mol" style="width: 160px; height: 160px;"></canvas>
</div>
</body>
Javascript (in the HTML header):
var xmlhttp;
function init() {
xmlhttp = new XMLHttpRequest();
}
function cypherQuery() {
// perform neo4j query, extract JSON and call the network drawing (d3).
// ...
var obj = JSON.parse(xmlhttp.responseText);
drawGraph(obj, drawCanvas);
// this is just to show that canvasViewer does the
// job if the <canvas id='...'> element exists in DOM
var nodes = obj.data[0][0].nodes;
var cnv = newCanvasViewer("sample", nodes[0].data);
}
function drawGraph(obj, drawCanvas) {
// see code above from...
var width = 500, height = 500;
// ... to
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
function redraw() {
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
// node.attr("font-size", (nodeFontSize / d3.event.scale) + "px");
}
function dblclick(d) {
d3.select(this).classed("fixed", d.fixed = false);
}
if (callback && typeof(callback) === "function") {
callback(nodes);
}
}
This is probably the simplest graph possible to create using d3js. And yet I am struggling.
The graph runs everything given to it in enter() and exit(). But everything in ENTER + UPDATE is completely ignored. WHY?
// Setup dimensions
var width = 200,
height = 200,
radius = Math.min(width, height) / 2,
// Setup a color function with 20 colors to use in the graph
color = d3.scale.category20(),
// Configure pie container
arc = d3.svg.arc().outerRadius(radius - 10).innerRadius(0), // Define the arc element
svg = d3.select(".pie").append("svg:svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"),
// This is the layout manager for the pieGraph
pie = d3.layout.pie()
.sort(null)
.value(function (d) {
return d.answers;
}),
// Allow two groups in the container. One overlapping the other, just to make sure that
// text labels never get hidden below pie arcs.
graphGroup = svg.append("svg:g").attr("class", "graphGroup"),
textGroup = svg.append("svg:g").attr("class", "labelGroup");
// Data is loaded upon user interaction. On angular $scope update, refresh graph...
$scope.$watch('answers', function (data) {
// === DATA ENTER ===================
var g = graphGroup.selectAll("path.arc").data(pie(data)),
gEnter = g.enter()
.append("path")
.attr("d", arc)
.attr("class", "arc"),
t = textGroup.selectAll("text.label").data(data),
tEnter = t.enter()
.append("text")
.attr("class", "label")
.attr("dy", ".35em")
.style("text-anchor", "middle");
// === ENTER + UPDATE ================
g.select("path.arc")
.attr("id", function (d) {
return d.data.id + "_" + d.data.selection;
})
.attr("fill", function (d, i) {
return color(i);
})
.transition().duration(750).attrTween("d", function (d) {
var i = d3.interpolate(this._current, d);
this._current = i(0);
return function (t) {
return arc(i(t));
};
});
t.select("text.label")
.attr("transform", function (d) {
return "translate(" + arc.centroid(d) + ")";
})
.text(function (d) {
return d.data.opt;
});
// === EXIT ==========================
g.exit().remove();
t.exit().remove();
});
This one example of the json structure given to the update function as "data":
[{"selection":"0","opt":"1-2 timer","answers":"7"},
{"selection":"1","opt":"3-4 timer","answers":"13"},
{"selection":"2","opt":"5-6 timer","answers":"5"},
{"selection":"3","opt":"7-8 timer","answers":"8"},
{"selection":"4","opt":"9-10 timer","answers":"7"},
{"selection":"5","opt":"11 timer eller mer","answers":"11"},
{"selection":"255","opt":"Blank","answers":"8"}]
You don't need the additional .select() to access the update selection. This will in fact return empty selections in your case, which means that nothing happens. To make it work, simply get rid of the additional .select() and do
g.attr("id", function (d) {
return d.data.id + "_" + d.data.selection;
})
// etc
t.attr("transform", function (d) {
return "translate(" + arc.centroid(d) + ")";
})
// etc