I'd like to be able to animate multiple (based on the data) arc charts from one percent (angle) to another in D3.js and can draw them fine initially.
However, after much hunting around, I'm stuck with the animation. Below is the code that does the original drawing and then two options for animation to subsequent values. I'm using groups for each Chart Node as I will be adding multiple elements to each.
Option 1 uses standard interpolation which I know doesn't work
properly as the shape is too complex. So the animation doesn't
follow the correct steps and also errors are reported to the console.
Option 2 uses the Arc Tween method, but this just reports errors.
To see each option working, comment out the other one.
Ideally, I'd like to be able to create an arc function to which I can pass the innerRadius, outerRadius and then the endAngle. For at least the endAngle, I want to be able to choose to pass a constant (e.g. 0) or Bound Data (e.g. d.pct).
index.html
<html lang="en">
<head>
<script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
<div id="vis">
</div>
<script src = 'SOarc.js'></script>
</body>
</html>
SOarc.js
data = [
{x:50, y: 250, pct: 0.25},
{x:200, y: 250, pct: 0.50},
{x:350, y: 250, pct: 0.75}]
radialScale = d3.scaleLinear()
.domain([0, 1])
.range([0, 2 * Math.PI]);
svg = d3.select("#vis")
.append('svg')
.attr('width', 500)
.attr('height', 500)
.attr('opacity', 1)
// Join to the data and create a group for each data point so that various chart items (e.g. multiple arcs) can be added
chartNodes = svg
.selectAll('g.chartGroup')
.data(data)
// Position each using transform/ translate with coordinates specified in data
chartNodesEnter = chartNodes
.enter()
.append('g')
.attr("class", "chartGroup")
.attr('transform', (d) => 'translate('+d.x+','+d.y+')');
// Add arcs to as per data
chartNodesEnter.append('path')
.attr("class", "chart1")
.attr('fill', "red")
.attr('d', d3.arc()
.startAngle(0)
.endAngle((d) => radialScale(d.pct))
.innerRadius(50+2) // This is the size of the donut hole
.outerRadius(50+8));
// Now animate to a different endAngle (90% in this example)
// Option 1 - Standard Interpolation - doesn't work with complex shapes
// --------------------------------------------------------------------
// Animate all arcs to 90% - doesn't animate properly as interpolation not correct for this complex shape
// and also throws Error: <path> attribute d: Expected arc flag ('0' or '1') errors for the same reason
svg.selectAll('.chart1')
.transition().duration(3000).delay(0)
.attr('d', d3.arc()
.startAngle(0)
.endAngle(function(d){ return radialScale(0.9)})
.innerRadius(50+2) // This is the size of the donut hole
.outerRadius(50+8)
)
// Option 2 - Tween Interpolation - Produces error
// -----------------------------------------------
// Code from from Mike Bostock's Arc Tween http://bl.ocks.org/mbostock/5100636
// Errors with <path> attribute d: Expected moveto path command ('M' or 'm'), "function(t) {\n …".
var arc = d3.arc()
.innerRadius(50+2)
.outerRadius(50+8)
.startAngle(0);
// Returns a tween for a transition’s "d" attribute, transitioning any selected
// arcs from their current angle to the specified new angle.
function arcTween(newAngle) {
return function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc(d);
};
};
}
// Animate to 90%
svg.selectAll('.chart1')
.transition().duration(3000).delay(0)
.attrTween("d", d => arcTween(radialScale(0.9)) );
Error: <path> attribute d: Expected moveto path command ('M' or 'm'), "function(t) {\n …". # SOarc.js:68
Option 2 is the right way to do this but Mr. Bostock's example is a little much for your simpler use case.
Let's examine the simplest code which achieves your goal:
// create a arc generator with start angle of 0
var arc = d3
.arc()
.innerRadius(50 + 2)
.outerRadius(50 + 8)
.startAngle(0)
.endAngle(0);
svg
.selectAll('.chart1')
.transition()
.duration(3000)
.delay(0)
.attrTween('d', function(d,i) {
// for each chart
// create an interpolator between start angle 0
// and end angle of d.pct
var interpolate = d3.interpolate(0, radialScale(d.pct));
// attrTween is expecting a function to call for every iteration of t
// so let's return such a function
return function(t) {
// assign end angle to interpolated value for t
arc.endAngle(interpolate(t));
// call arc and return intermediate `d` value
return arc();
};
});
Here it is running:
<html lang="en">
<head>
<script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
<div id="vis"></div>
<script>
data = [
{ x: 50, y: 250, pct: 0.25 },
{ x: 200, y: 250, pct: 0.5 },
{ x: 350, y: 250, pct: 0.75 },
];
radialScale = d3
.scaleLinear()
.domain([0, 1])
.range([0, 2 * Math.PI]);
svg = d3
.select('#vis')
.append('svg')
.attr('width', 500)
.attr('height', 500)
.attr('opacity', 1);
// Join to the data and create a group for each data point so that various chart items (e.g. multiple arcs) can be added
chartNodes = svg.selectAll('g.chartGroup').data(data);
// Position each using transform/ translate with coordinates specified in data
chartNodesEnter = chartNodes
.enter()
.append('g')
.attr('class', 'chartGroup')
.attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')');
// Add arcs to as per data
chartNodesEnter
.append('path')
.attr('class', 'chart1')
.attr('fill', 'red')
.attr(
'd',
d3
.arc()
.startAngle(0)
.endAngle((d) => radialScale(d.pct))
.innerRadius(50 + 2) // This is the size of the donut hole
.outerRadius(50 + 8)
);
// Now animate to a different endAngle (90% in this example)
// Option 1 - Standard Interpolation - doesn't work with complex shapes
// --------------------------------------------------------------------
// Animate all arcs to 90% - doesn't animate properly as interpolation not correct for this complex shape
// and also throws Error: <path> attribute d: Expected arc flag ('0' or '1') errors for the same reason
/*
svg
.selectAll('.chart1')
.transition()
.duration(3000)
.delay(0)
.attr(
'd',
d3
.arc()
.startAngle(0)
.endAngle(function (d) {
return radialScale(0.9);
})
.innerRadius(50 + 2) // This is the size of the donut hole
.outerRadius(50 + 8)
);
*/
// Option 2 - Tween Interpolation - Produces error
// -----------------------------------------------
// Code from from Mike Bostock's Arc Tween http://bl.ocks.org/mbostock/5100636
// Errors with <path> attribute d: Expected moveto path command ('M' or 'm'), "function(t) {\n …".
var arc = d3
.arc()
.innerRadius(50 + 2)
.outerRadius(50 + 8)
.startAngle(0)
.endAngle(0);
// Animate to end angle
svg
.selectAll('.chart1')
.transition()
.duration(3000)
.delay(0)
.attrTween('d', function(d,i) {
var interpolate = d3.interpolate(0, radialScale(d.pct));
return function(t) {
arc.endAngle(interpolate(t));
return arc();
};
});
</script>
</body>
</html>
New snippet for comments
Lots of options for variable arcs. The first thing that jumped into my head was to add your radiuses into your data binding and create the arcs like in this snippet.
<html lang="en">
<head>
<script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
<div id="vis"></div>
<script>
data = [
{ x: 50, y: 250, pct: 0.25, inner: 52, outer: 58 },
{ x: 200, y: 250, pct: 0.5, inner: 22, outer: 28 },
{ x: 350, y: 250, pct: 0.75, inner: 82, outer: 88 },
];
radialScale = d3
.scaleLinear()
.domain([0, 1])
.range([0, 2 * Math.PI]);
svg = d3
.select('#vis')
.append('svg')
.attr('width', 500)
.attr('height', 500)
.attr('opacity', 1);
// Join to the data and create a group for each data point so that various chart items (e.g. multiple arcs) can be added
chartNodes = svg.selectAll('g.chartGroup').data(data);
// Position each using transform/ translate with coordinates specified in data
chartNodesEnter = chartNodes
.enter()
.append('g')
.attr('class', 'chartGroup')
.attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')');
// Add arcs to as per data
chartNodesEnter
.append('path')
.attr('class', 'chart1')
.attr('fill', 'red')
.attr(
'd',
d3
.arc()
.startAngle(0)
.endAngle((d) => radialScale(d.pct))
.innerRadius(50 + 2) // This is the size of the donut hole
.outerRadius(50 + 8)
);
// Now animate to a different endAngle (90% in this example)
// Option 1 - Standard Interpolation - doesn't work with complex shapes
// --------------------------------------------------------------------
// Animate all arcs to 90% - doesn't animate properly as interpolation not correct for this complex shape
// and also throws Error: <path> attribute d: Expected arc flag ('0' or '1') errors for the same reason
/*
svg
.selectAll('.chart1')
.transition()
.duration(3000)
.delay(0)
.attr(
'd',
d3
.arc()
.startAngle(0)
.endAngle(function (d) {
return radialScale(0.9);
})
.innerRadius(50 + 2) // This is the size of the donut hole
.outerRadius(50 + 8)
);
*/
// Option 2 - Tween Interpolation - Produces error
// -----------------------------------------------
// Code from from Mike Bostock's Arc Tween http://bl.ocks.org/mbostock/5100636
// Errors with <path> attribute d: Expected moveto path command ('M' or 'm'), "function(t) {\n …".
// Animate to end angle
svg
.selectAll('.chart1')
.transition()
.duration(3000)
.delay(0)
.attrTween('d', function(d,i) {
var interpolate = d3.interpolate(0, radialScale(d.pct));
var arc = d3
.arc()
.innerRadius(d.inner)
.outerRadius(d.outer)
.startAngle(0)
.endAngle(0);
return function(t) {
arc.endAngle(interpolate(t));
return arc();
};
});
</script>
</body>
</html>
Related
I have a .json file with data, and I'd like to make a d3 donut (pie) chart from it. I'm not especially fluent in javascript, and every example I can find either pulls from inline json data or the json file is structured differently than mine (mine is a list of dictionaries; theirs are often single dictionaries). I've been troubleshooting for a few days, and somehow can't land on anything that actually works. Any thoughts/tips?
The example at https://www.d3-graph-gallery.com/graph/donut_label.html uses inline json data to render a donut chart with labels. I've attempted to modify it that code by:
pulling json data from /data/all-facet-digitized.json
pull labels each dictionary's "facet" key ("true" and "false"), and values from each dictionary's "count" key (373977 and 55433).
change the color scale domain to match the facet keys ("true" and "false")
/data/all-facet-digitized.json looks like:
[
{
"count": "55433",
"facet": "true"
},
{
"count": "373977",
"facet": "false"
}
]
Code in the of my html file looks like:
<div id="chart"></div> <!-- div containing the donut chart -->
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
// set the dimensions and margins of the graph
var width = 450
height = 450
margin = 40
// The radius of the pieplot is half the width or half the height (smallest one) minus margin.
var radius = Math.min(width, height) / 2 - margin
// append the svg object to the div called 'chart'
var svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Parse the Data
d3.json("/data/all-facet-digitized.json", function(data) {
// set the color scale
var color = d3.scaleOrdinal()
.domain(["true","false"])
.range(d3.schemeDark2);
// Compute the position of each group on the pie:
var pie = d3.pie()
.sort(null) // Do not sort group by size
.value(function(d) {return d.count; })
var data_ready = pie(d3.entries(data))
// The arc generator
var arc = d3.arc()
.innerRadius(radius * 0.5) // This is the size of the donut hole
.outerRadius(radius * 0.8)
// Another arc that won't be drawn. Just for labels positioning
var outerArc = d3.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9)
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
svg
.selectAll('allSlices')
.data(data_ready)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d){ return(color(d.facet)) })
.attr("stroke", "white")
.style("stroke-width", "2px")
.style("opacity", 0.7)
// Add the polylines between chart and labels:
svg
.selectAll('allPolylines')
.data(data_ready)
.enter()
.append('polyline')
.attr("stroke", "black")
.style("fill", "none")
.attr("stroke-width", 1)
.attr('points', function(d) {
var posA = arc.centroid(d) // line insertion in the slice
var posB = outerArc.centroid(d) // line break: we use the other arc generator that has been built only for that
var posC = outerArc.centroid(d); // Label position = almost the same as posB
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2 // we need the angle to see if the X position will be at the extreme right or extreme left
posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1); // multiply by 1 or -1 to put it on the right or on the left
return [posA, posB, posC]
})
// Add the polylines between chart and labels:
svg
.selectAll('allLabels')
.data(data_ready)
.enter()
.append('text')
.text( function(d) { console.log(d.facet) ; return d.facet} )
.attr('transform', function(d) {
var pos = outerArc.centroid(d);
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
return 'translate(' + pos + ')';
})
.style('text-anchor', function(d) {
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
return (midangle < Math.PI ? 'start' : 'end')
})
})
</script>
My result renders as an empty space:
<div id="chart">
<svg width="450" height="450">
<g transform="translate(225,225)"></g>
</svg>
</div>
The schemeDark2 doens't exist in d3 v4. I've replaced it with schemeCategory10:
var color = d3.scaleOrdinal()
.domain(["true","false"])
.range(d3.schemeCategory10);
Since you have an array of objects, you don't need d3.entries. That takes an object and converts it to an array where each key is an item of the array. But since you already have an array here, you can put it directly in pie():
// Compute the position of each group on the pie:
var pie = d3.pie()
.sort(null) // Do not sort group by size
.value(function(d) {return d.count; })
var data_ready = pie(data)
Now that you've got the data, you can access it on any of the functions: try putting console.log(data_ready) to see what's available. You'll see that the data is bound for each object as the .data property. pie() takes an array and puts it in a format that's convenient to make pie charts with.
Say we want to access the facet property: we would access that as item.data.facet. So in your functions, to access, you can do:
svg
.selectAll('allSlices')
.data(data_ready)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d){ return(color(d.data.facet)) })
<head></head>
<div id="chart"></div> <!-- div containing the donut chart -->
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
// set the dimensions and margins of the graph
var width = 450
height = 450
margin = 40
// The radius of the pieplot is half the width or half the height (smallest one) minus margin.
var radius = Math.min(width, height) / 2 - margin
// append the svg object to the div called 'chart'
var svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Parse the Data
var data = [
{
"count": "55433",
"facet": "true"
},
{
"count": "373977",
"facet": "false"
}
]
// set the color scale
var color = d3.scaleOrdinal()
.domain(["true","false"])
.range(d3.schemeCategory10);
// Compute the position of each group on the pie:
var pie = d3.pie()
.sort(null) // Do not sort group by size
.value(function(d) {return d.count; })
var data_ready = pie(data)
console.log('data_r', data_ready)
// The arc generator
var arc = d3.arc()
.innerRadius(radius * 0.5) // This is the size of the donut hole
.outerRadius(radius * 0.8)
// Another arc that won't be drawn. Just for labels positioning
var outerArc = d3.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9)
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
svg
.selectAll('allSlices')
.data(data_ready)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d){ return(color(d.data.facet)) })
.attr("stroke", "white")
.style("stroke-width", "2px")
.style("opacity", 0.7)
// Add the polylines between chart and labels:
svg
.selectAll('allPolylines')
.data(data_ready)
.enter()
.append('polyline')
.attr("stroke", "black")
.style("fill", "none")
.attr("stroke-width", 1)
.attr('points', function(d) {
var posA = arc.centroid(d) // line insertion in the slice
var posB = outerArc.centroid(d) // line break: we use the other arc generator that has been built only for that
var posC = outerArc.centroid(d); // Label position = almost the same as posB
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2 // we need the angle to see if the X position will be at the extreme right or extreme left
posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1); // multiply by 1 or -1 to put it on the right or on the left
return [posA, posB, posC]
})
// Add the polylines between chart and labels:
svg
.selectAll('allLabels')
.data(data_ready)
.enter()
.append('text')
.text( function(d) { return d.data.facet} )
.attr('transform', function(d) {
var pos = outerArc.centroid(d);
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
return 'translate(' + pos + ')';
})
.style('text-anchor', function(d) {
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
return (midangle < Math.PI ? 'start' : 'end')
})
</script>
Ok, the issues here is that you've completely missed how data_ready is structured after converting the JSON response. You might want to add console.log(data_ready) just after you set data_ready and inspect it in the console for better understanding of the following fixes.
First a color fix:
.attr('fill', function(d){ return(color(d.data.value.facet)) })
Then a data fix:
.value(function(d) {return d.value.count; })
And lastly a label fix:
.text( function(d) { console.log(d.data.key) ; return d.data.value.facet } )
Your script should look like this:
// set the dimensions and margins of the graph
var width = 450
height = 450
margin = 40
// The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin.
var radius = Math.min(width, height) / 2 - margin
// append the svg object to the div called 'my_dataviz'
var svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
d3.json("/data/all-facet-digitized.json", function(data) {
// set the color scale
var color = d3.scaleOrdinal()
.domain(["true","false"])
.range(d3.schemeDark2);
// Compute the position of each group on the pie:
var pie = d3.pie()
.sort(null) // Do not sort group by size
.value(function(d) {return d.value.count; })
var data_ready = pie(d3.entries(data))
// The arc generator
var arc = d3.arc()
.innerRadius(radius * 0.5) // This is the size of the donut hole
.outerRadius(radius * 0.8)
// Another arc that won't be drawn. Just for labels positioning
var outerArc = d3.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9)
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
svg
.selectAll('allSlices')
.data(data_ready)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d){ return(color(d.data.value.facet)) })
.attr("stroke", "white")
.style("stroke-width", "2px")
.style("opacity", 0.7)
// Add the polylines between chart and labels:
svg
.selectAll('allPolylines')
.data(data_ready)
.enter()
.append('polyline')
.attr("stroke", "black")
.style("fill", "none")
.attr("stroke-width", 1)
.attr('points', function(d) {
var posA = arc.centroid(d) // line insertion in the slice
var posB = outerArc.centroid(d) // line break: we use the other arc generator that has been built only for that
var posC = outerArc.centroid(d); // Label position = almost the same as posB
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2 // we need the angle to see if the X position will be at the extreme right or extreme left
posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1); // multiply by 1 or -1 to put it on the right or on the left
return [posA, posB, posC]
})
// Add the polylines between chart and labels:
svg
.selectAll('allLabels')
.data(data_ready)
.enter()
.append('text')
.text( function(d) { console.log(d.data.key) ; return d.data.value.facet } )
.attr('transform', function(d) {
var pos = outerArc.centroid(d);
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
return 'translate(' + pos + ')';
})
.style('text-anchor', function(d) {
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
return (midangle < Math.PI ? 'start' : 'end')
})
})
I'm trying to display labels with its value and tooltip in semi donut pie chart using D3 JS.
I'm not able to display labels and their values at the simultaneously.
And how can I add the tooltip on this chart?
I tried implementing this fiddle.
https://jsfiddle.net/SampathPerOxide/hcvuqjt2/6/
var width = 400;
var height = 300; //this is the double because are showing just the half of the pie
var radius = Math.min(width, height) / 2;
var labelr = radius + 30; // radius for label anchor
//array of colors for the pie (in the same order as the dataset)
var color = d3.scale
.ordinal()
.range(['#2b5eac', '#0dadd3', '#ffea61', '#ff917e', '#ff3e41']);
data = [
{ label: 'CDU', value: 10 },
{ label: 'SPD', value: 15 },
{ label: 'Die Grünen', value: 8 },
{ label: 'Die Mitte', value: 1 },
{ label: 'Frei Wähler', value: 3 }
];
var vis = d3
.select('#chart')
.append('svg') //create the SVG element inside the <body>
.data([data]) //associate our data with the document
.attr('width', width) //set the width and height of our visualization (these will be attributes of the <svg> tag
.attr('height', height)
.append('svg:g') //make a group to hold our pie chart
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); //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
.innerRadius(79)
// .outerRadius(radius);
.outerRadius(radius - 10); // full height semi pie
//.innerRadius(0);
var pie = d3.layout
.pie() //this will create arc data for us given a list of values
.startAngle(-90 * (Math.PI / 180))
.endAngle(90 * (Math.PI / 180))
.padAngle(0.02) // some space between slices
.sort(null) //No! we don't want to order it by size
.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
arcs
.append('svg:text')
.attr('class', 'labels') //add a label to each slice
.attr('fill', 'grey')
.attr('transform', function(d) {
var c = arc.centroid(d),
xp = c[0],
yp = c[1],
// pythagorean theorem for hypotenuse
hp = Math.sqrt(xp * xp + yp * yp);
return 'translate(' + (xp / hp) * labelr + ',' + (yp / hp) * labelr + ')';
})
.attr('text-anchor', 'middle') //center the text on it's origin
.text(function(d, i) {
return data[i].value;
})
.text(function(d, i) {
return data[i].label;
}); //get the label from our original data array
I'm trying to achieve this. https://i.imgur.com/kTXeAXt.png
First of all, if you console.log the data (from .data(pie)) you used for displaying text label and value, you will noticed that label can only be access via d.data.label instead of data[i].label.
{data: {label: "Frei Wähler", value: 3}, value: 3, startAngle: 1.304180706233562, endAngle: 1.5707963267948966, padAngle: 0.02}
Therefore in order to correctly display both label and value, the code should be:
arcs.append("svg:text")
.attr("class", "labels")//add a label to each slice
.attr("fill", "grey")
.attr("transform", function(d) {
var c = arc.centroid(d),
xp = c[0],
yp = c[1],
// pythagorean theorem for hypotenuse
hp = Math.sqrt(xp*xp + yp*yp);
return "translate(" + (xp/hp * labelr) + ',' +
(yp/hp * labelr) + ")";
})
.attr("text-anchor", "middle") //center the text on it's origin
.text(function(d, i) { return d.data.value; })
.text(function(d, i) { return d.data.label; });
How to add tooltip
As for how to create d3 tooltip, it needs a little of css, html and then add d3 event handling.
1) add the following html to your index.html:
<div id="tooltip" class="hidden"><p id="tooltip-data"></p></div>
2) add a little bit css to set the div to position:absolute and hide the tooltip with display:none, and gives it a little bit styling per your preference:
<style>
#tooltip {
position:absolute;
background: #ffffe0;
color: black;
width: 180px;
border-radius: 3px;
box-shadow: 2px 2px 6px rgba(40, 40, 40, 0.5);
}
#tooltip.hidden {
display:none;
}
#tooltip p {
margin: 0px;
padding: 8px;
font-size: 12px;
}
3) We then add mouseover event handler, the idea is when the mouse is over the chart, we will set the top and left properties of the #tooltip css style to where the mouse is, and set the css display property to show the tooltip.
//tooltip
arcs.on("mouseover", function(d) {
d3.select("#tooltip")
.style("left", `${d3.event.clientX}px`)
.style("top", `${d3.event.clientY}px`)
.classed("hidden", false);
d3.select("#tooltip-data")
.html(`Label: ${d.data.label}<br>Value: ${d.data.value}`);
});
arcs.on("mouseout", function(d) {
d3.select("#tooltip")
.classed("hidden", true);
});
selection.text([value])
If a value is specified, sets the text content to the specified value on all selected elements, replacing any existing child elements.
So you are setting the text content with the value and immediately replacing it with the label.
What you can do is return a combined string from the value and label of your datum in a template literal like this:
.text(function(d, i) { return `${data[i].value} - ${data[i].label}`; })
var width = 400;
var height = 300; //this is the double because are showing just the half of the pie
var radius = Math.min(width, height) / 2;
var labelr = radius + 30; // radius for label anchor
//array of colors for the pie (in the same order as the dataset)
var color = d3.scale.ordinal()
.range(['#2b5eac', '#0dadd3', '#ffea61', '#ff917e', '#ff3e41']);
data = [{
label: 'CDU',
value: 10
},
{
label: 'SPD',
value: 15
},
{
label: 'Die Grünen',
value: 8
},
{
label: 'Die Mitte',
value: 1
},
{
label: 'Frei Wähler',
value: 3
}
];
var vis = d3.select("#chart")
.append("svg") //create the SVG element inside the <body>
.data([data]) //associate our data with the document
.attr("width", width) //set the width and height of our visualization (these will be attributes of the <svg> tag
.attr("height", height)
.append("svg:g") //make a group to hold our pie chart
.attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')'); //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
.innerRadius(79)
// .outerRadius(radius);
.outerRadius(radius - 10) // full height semi pie
//.innerRadius(0);
var pie = d3.layout.pie() //this will create arc data for us given a list of values
.startAngle(-90 * (Math.PI / 180))
.endAngle(90 * (Math.PI / 180))
.padAngle(.02) // some space between slices
.sort(null) //No! we don't want to order it by size
.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
arcs.append("svg:text")
.attr("class", "labels") //add a label to each slice
.attr("fill", "grey")
.attr("transform", function(d) {
var c = arc.centroid(d),
xp = c[0],
yp = c[1],
// pythagorean theorem for hypotenuse
hp = Math.sqrt(xp * xp + yp * yp);
return "translate(" + (xp / hp * labelr) + ',' +
(yp / hp * labelr) + ")";
})
.attr("text-anchor", "middle") //center the text on it's origin
.text(function(d, i) {
return `${data[i].value} - ${data[i].label}`;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div id="chart" style="width: 330px;height: 200px;"></div>
Edit:
If you want the two text strings to be on separate lines you would have to append some <tspan> elements and position them.
var width = 400;
var height = 300; //this is the double because are showing just the half of the pie
var radius = Math.min(width, height) / 2;
var labelr = radius + 30; // radius for label anchor
//array of colors for the pie (in the same order as the dataset)
var color = d3.scale.ordinal()
.range(['#2b5eac', '#0dadd3', '#ffea61', '#ff917e', '#ff3e41']);
data = [{
label: 'CDU',
value: 10
},
{
label: 'SPD',
value: 15
},
{
label: 'Die Grünen',
value: 8
},
{
label: 'Die Mitte',
value: 1
},
{
label: 'Frei Wähler',
value: 3
}
];
var vis = d3.select("#chart")
.append("svg") //create the SVG element inside the <body>
.data([data]) //associate our data with the document
.attr("width", width) //set the width and height of our visualization (these will be attributes of the <svg> tag
.attr("height", height)
.append("svg:g") //make a group to hold our pie chart
.attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')'); //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
.innerRadius(79)
// .outerRadius(radius);
.outerRadius(radius - 10) // full height semi pie
//.innerRadius(0);
var pie = d3.layout.pie() //this will create arc data for us given a list of values
.startAngle(-90 * (Math.PI / 180))
.endAngle(90 * (Math.PI / 180))
.padAngle(.02) // some space between slices
.sort(null) //No! we don't want to order it by size
.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
const textEl = arcs.append("svg:text")
.attr("class", "labels") //add a label to each slice
.attr("fill", "grey")
.attr("transform", function(d) {
var c = arc.centroid(d),
xp = c[0],
yp = c[1],
// pythagorean theorem for hypotenuse
hp = Math.sqrt(xp * xp + yp * yp);
return "translate(" + (xp / hp * labelr) + ',' +
(yp / hp * labelr) + ")";
})
.attr("text-anchor", "middle"); //center the text on it's origin
textEl.append('tspan')
.text(function(d, i) {
return data[i].label;
});
textEl.append('tspan')
.text(function(d, i) {
return data[i].value;
})
.attr('x', '0')
.attr('dy', '1.2em');
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div id="chart" style="width: 330px;height: 200px;"></div>
I have this donut chart currently working in an AngularJS app:
But the design mockup says we would like this, note the border-radius property on the green portion of the arc:
How do I add a border-radius to the SVG that d3js outputs, the code I'm currently using looks like this:
let data = [
{
label: 'Data',
count: scope.data
},
{
label: 'Fill',
count: 100 - scope.data
}
];
let width = 60;
let height = 60;
let radius = Math.min(width, height) / 2;
let color = d3.scale
.ordinal()
.range(['#3CC692', '#F3F3F4']);
let selector = '#donut-asset-' + scope.chartId;
d3
.select(selector)
.selectAll('*')
.remove();
let svg = d3
.selectAll(selector)
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr(
'transform',
'translate(' + width / 2 + ',' + height / 2 + ')'
);
let arc = d3.svg
.arc()
.innerRadius(23)
.outerRadius(radius);
let pie = d3.layout
.pie()
.value(function(d) {
return d.count;
})
.sort(null);
let path = svg
.selectAll('path')
.data(pie(data))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data.label);
});
let legend = svg
.selectAll('.legend')
.data(data)
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
return 'translate(' + 0 + ',' + 0 + ')';
});
legend
.append('text')
.attr('x', 1)
.attr('y', 1)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.text(function(d) {
return d.count + '%';
});
};
I know to use cornerRadius but when I do it sets a radius for both arcs, it just needs to exist on the colored one. Any ideas? Thanks in advance for any help!
You can apply a corner radius to a d3 arc which allows rounding on the corners:
let arc = d3.svg
.arc()
.innerRadius(23)
.outerRadius(radius)
.cornerRadius(10);
But, the downside is that all arcs' borders are rounded:
If you apply the cornerRadius to only the darkened arc - the other arc won't fill in the background behind the rounded corners. Instead, we could append a circular arc (full donut) and place the darkened arc on top with rounding (my example doesn't adapt your code, just shows how that it can be done, also with d3v4 which uses d3.arc() rather than d3.svg.arc() ):
var backgroundArc = d3.arc()
.innerRadius(30)
.outerRadius(50)
.startAngle(0)
.endAngle(Math.PI*2);
var mainArc = d3.arc()
.innerRadius(30)
.outerRadius(50)
.cornerRadius(10)
.startAngle(0)
.endAngle(function(d) { return d/100*Math.PI* 2 });
var data = [10,20,30,40,50] // percents.
var svg = d3.select("body").append("svg")
.attr("width", 600)
.attr("height", 200);
var charts = svg.selectAll("g")
.data(data)
.enter()
.append("g")
.attr("transform",function(d,i) {
return "translate("+(i*100+50)+",100)";
});
charts.append("path")
.attr("d", backgroundArc)
.attr("fill","#ccc")
charts.append("path")
.attr("d", mainArc)
.attr("fill","orange")
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Try playing with stroke attributes like:
stroke
stroke-dasharray
stroke-dashoffset
stroke-linecap
stroke-linejoin
stroke-miterlimit
stroke-opacity
stroke-width
And set width of bar to lower values, or 0.
Reference: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
But the better way is to make charts on canvas, because you can draw everything you want. Or to use an library.
I'm looking to create multiple progress circles. (So, draw 3 circles from one data set)
I've been trying to adapt from this example, but as you will see, I've gone seriously wrong somewhere.
First steps I took was to change all the datums to data, as I will want the option to handle the data dynamically. Then, I tried to simplify the code so it was clearer/easier for me to understand. (I'm a d3 newbie!)
And now, I'm not sure what's going on, and was hoping someone could help me get to the end result?
Here is a fiddle and my code;
/*global d3*/
var width = 240,
height = 125,
min = Math.min(width, height),
oRadius = min / 2 * 0.8,
iRadius = min / 2 * 0.85,
color = d3.scale.category20();
var arc = d3.svg.arc()
.outerRadius(oRadius)
.innerRadius(iRadius);
var pie = d3.layout.pie().value(function(d) {
return d;
}).sort(null);
var data = [
[20],
[40],
[60]
];
// draw and append the container
var svg = d3.select("#chart").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("width", width).attr("height", height);
svg.append('g')
.attr('transform', 'translate(75,62.5)')
.append('text').attr('text-anchor', 'middle').text("asdasdasdas")
// enter data and draw pie chart
var path = svg.selectAll("path")
.data(pie)
.enter().append("path")
.attr("class", "piechart")
.attr("fill", function(d, i) {
return color(i);
})
.attr("d", arc)
.each(function(d) {
this._current = d;
})
function render() {
// add transition to new path
svg.selectAll("path")
.data(pie)
.transition()
.duration(1000)
.attrTween("d", arcTween)
// add any new paths
svg.selectAll("path")
.data(pie)
.enter().append("path")
.attr("class", "piechart")
.attr("fill", function(d, i) {
return color(i);
})
.attr("d", arc)
.each(function(d) {
this._current = d;
})
// remove data not being used
svg.selectAll("path")
.data(pie).exit().remove();
}
render();
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
Thanks all!
You have a problem with where you are drawing your separate charts. All you need to do is add a translate to each one so they are in the center of their containers.
Add this after you create the paths :
.attr('transform', 'translate(' + width/2 + ',' +height/2 + ')')
width and height here are the containers dimensions, this will move each chart to the center of their container.
Update fiddle : http://jsfiddle.net/thatOneGuy/dbrehvtz/2/
Obviously, you probably know, if you want to add more values to each pie, just edit the data. For example :
var data = [
[20, 100, 100, 56, 23, 20, 100, 100, 56, 23 ],
[40],
[60]
];
New fiddle : http://jsfiddle.net/thatOneGuy/dbrehvtz/3/
I have a very basic D3 SVG which essentially consists of a couple arcs.
No matter what I use (attr, attrTween, and call) I cannot seem to get the datum via the first argument of the callback--it is always coming back null (I presume it's some kind of parse error, even though the path renders correctly?)
I might be overlooking something basic as I am relatively new to the library...
var el = $('#graph'),
width = 280,
height = 280,
twoPi = Math.PI * 2,
total = 0;
var arc = d3.svg.arc()
.startAngle(0)
.innerRadius(110)
.outerRadius(130),
svg = d3.select('#graph').append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"),
meter = svg.append('g').attr('class', 'progress');
/* Add Meter Background */
meter.append('path')
.attr('class', 'background')
.attr('d', arc.endAngle(twoPi))
.attr('transform', 'rotate(180)');
/* Add in Icon */
meter.append('text')
.attr('text-anchor', 'middle')
.attr('class', 'fa fa-user')
.attr('y',30)
.text('')
/* Create Meter Progress */
var percentage = 0.4,
foreground = meter.append('path').attr('class', 'foreground')
.attr('transform', 'rotate(180)')
.attr('d', arc.endAngle(twoPi*percentage)),
setAngle = function(transition, newAngle) {
transition.attrTween('d', function(d,v,i) {
console.log(d,v,i)
});
/*transition.attrTween('d', function(d) { console.log(this)
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) { d.endAngle = interpolate(t); return arc(d); };
});*/
};
setTimeout(function() {
percentage = 0.8;
foreground.transition().call(setAngle, percentage*twoPi);
},2000);
It's this block of code that seems to be problematic:
transition.attrTween('d', function(d,v,i) {
console.log(d,v,i)
});
Returning:
undefined 0 "M7.959941299845452e-15,-130A130,130 0 0,1 76.4120827980215,105.17220926874317L64.65637775217205,88.99186938124421A110,110 0 0,0 6.735334946023075e-15,-110Z"
I tried using the interpolator to parse the i value as a string since I cannot seem to acquire "d," however that had a parsing error returning a d attribute with multiple NaN.
This all seems very strange seeing as it's a simple path calculated from an arc???
The first argument of basically all callbacks in D3 (d here) is the data element that is bound to the DOM element you're operating on. In your case, no data is bound to anything and therefore d is undefined.
I've updated your jsfiddle here to animate the transition and be more like the pie chart examples. The percentage to show is bound to the path as the datum. Then all you need to do is bind new data and create the tween in the same way as for any of the pie chart examples:
meter.select("path.foreground").datum(percentage)
.transition().delay(2000).duration(750)
.attrTween('d', function(d) {
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc.endAngle(twoPi*interpolate(t))();
};
});