Quantcast
Channel: User i alarmed alien - Stack Overflow
Viewing all articles
Browse latest Browse all 42

Answer by i alarmed alien for d3 - label placement for a nested pie chart

$
0
0

The layout that you have already is dependent on your data being uniform, which doesn't happen in the real world, so I found a data set and used it to create a pie chart that doesn't require perfect data.

It's a mix of the first and second charts. I have added copious comments to the code so please look through and check that you understand what is happening. I've put a demo at https://bl.ocks.org/ialarmedalien/1e453ed9b148be442f50e06ad7eb3759, so you can see the data input there.

function chart(id) {  // this reads in the CSV file  d3.csv('morley3.csv').then( data => {    // this massages the data I'm using into a more suitable form for your chart    // we have 12 runs with 6 experiments in each.    // each datum is of the form     // { Run: <number>, Expt: <number>, Speed: <number> }    const filteredData = data        .filter( d => d.Run < 13 )        .map( d => { return { Run: +d.Run, Expt: +d.Expt, Speed: +d.Speed } } )    // set up the chart    const width = 800,    height = 800,    radius = Math.min(height, width) * 0.5 - 100,    // how far away from the chart the labels should be    labelOffset = 10,    svg = d3.select(id).append("svg")        .attr("width", width)        .attr("height", height),    g = svg.append("g")        .attr("transform", `translate(${width/2}, ${height/2})`),    // this will be used to generate the pie segments    arc = d3.arc()      .outerRadius(radius)      .innerRadius(0),    // group the data by the run number    // this results in 12 groups of six experiments    // the nested data has the form    // [ { key: <run #>, values: [{ Run: 1, Expt: 1, Speed: 958 }, { Run: 1, Expt: 2, Speed: 869 } ... ],    //   { key: 2, values: [{ Run: 2, Expt: 1, Speed: 987 },{ Run: 2, Expt: 2, Speed: 809 } ... ],    // etc.    nested = d3.nest()      .key( d => +d.Run )      .entries(filteredData),    chunkSize = nested[0].values.length,    // d3.pie() is the pie chart generator    pie = d3.pie()      // the size of each slice will be the sum of all the Speed values for each run      .value( d => d3.sum( d.values, function (e) { return e.Speed } ) )      // sort by run #      .sort( (a,b) => a.key - b.key )      (nested)    // bind the data to the DOM. Add a `g` for each run    const runs = g.selectAll(".run")      .data(pie, d => d.key )      .enter()      .append("g")      .classed('run', true)      .each( d => {        // run the pie generator on the children        // d.data.values is all the experiments in the run, or in pie terms,        // all the experiments in this piece of the pie. We're going to use         // `startAngle` and `endAngle` to specify that we're only generating        // part of the pie. The values for `startAngle` and `endAngle` come        // from using the pie chart generator on the run data.        d.children = d3.pie()        .value( e => e.Speed )        .sort( (a,b) => a.Expt - b.Expt )        .startAngle( d.startAngle )        .endAngle( d.endAngle )        ( d.data.values )      })    // we want to label each run (rather than every single segment), so    // the labels get added next.    runs.append('text')      .classed('label', true)      // if the midpoint of the segment is on the right of the pie, set the      // text anchor to be at the start. If it is on the left, set the text anchor      // to the end.      .attr('text-anchor', d => {        d.midPt = (0.5 * (d.startAngle + d.endAngle))        return d.midPt < Math.PI ? 'start' : 'end'      } )      // to calculate the position of the label, I've taken the mid point of the      // start and end angles for the segment. I've then used d3.pointRadial to      // convert the angle (in radians) and the distance from the centre of       // the circle/pie (pie radius + labelOffset) into cartesian coordinates.      // d3.pointRadial returns [x, y] coordinates      .attr('x', d => d3.pointRadial( d.midPt, radius + labelOffset )[0] )      .attr('y', d => d3.pointRadial( d.midPt, radius + labelOffset )[1] )      // If the segment is in the upper half of the pie, move the text up a bit      // so that the label doesn't encroach on the pie itself      .attr('dy', d => {        let dy = 0.35;        if ( d.midPt < 0.5 * Math.PI || d.midPt > 1.5 * Math.PI ) {          dy -= 3.0;        }        return dy +'em'      })      .text( d => {        return 'Run '+ d.data.key +', experiments 1 - 6'      })      .call(wrap, 50)    // now we can get on to generating the sub segments within each main segment.    // add another g for each experiment         const expts = runs.selectAll('.expt')      // we already have the data bound to the DOM, but we want the d.children,      // which has the layout information from the pie chart generator      .data( d => d.children )      .enter()      .append('g')      .classed('expt', true)    // add the paths for each sub-segment    expts.append('path')      .classed('speed-segment', true)      .attr('d', arc)    // I simplified this slightly to use one of the built-in d3 colour schemes    // my data was already numeric so it was easy to use the run # as the colour      .attr('fill', (d,i) => {        const c = i / chunkSize,        color = d3.rgb( d3.schemeSet3[ d.data.Run - 1 ] );        return c < 1 ? color.brighter(c*0.5) : color;      })      // add a title element that appears when mousing over the segment      .append('title')      .text(d => 'Run '+ d.data.Run +', experiment '+ d.data.Expt +', speed: '+ d.data.Speed )    // add the lines    expts.append('line')      .attr('y2', radius)      // assign a class to each line so we can control the stroke, etc., using css      .attr('class', d => {        return 'run-'+ d.data.Run +' expt-'+ d.data.Expt      })      // convert the angle from radians to degrees      .attr("transform", d => {        return "rotate("+ (180 + d.endAngle * 180 / Math.PI) +")";      });    function wrap(text, width) {        text.each(function () {            let text = d3.select(this),                words = text.text().split(/\s+/).reverse(),                word,                line = [],                lineNumber = 0,                lineHeight = 1.2, // ems                tfrm = text.attr('transform')                y = text.attr("y"),                x = text.attr("x"),                dy = parseFloat(text.attr("dy")),                tspan = text.text(null).append("tspan")                .attr("x", x)                .attr("y", y)                .attr("dy", dy +"em");            while (word = words.pop()) {                line.push(word);                tspan.text(line.join(""));                if (tspan.node().getComputedTextLength() > width) {                    line.pop();                    tspan.text(line.join(""));                    line = [word];                    tspan = text.append("tspan")                    .attr("x", x)                    .attr("y", y)                    .attr("dy", ++lineNumber * lineHeight + dy +"em")                        .text(word);                }            }        });    }    return svg;  })}chart('#chart');

Viewing all articles
Browse latest Browse all 42

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>