简体   繁体   English

d3 - 嵌套饼图的标签放置

[英]d3 - label placement for a nested pie chart

I'd like to place labels similar to what the image below illustrates. 我想放置与下图所示类似的标签。 This might be a 2 questions in 1 sort of thing, sorry for that. 对于这种情况,这可能是2个问题,对不起。

在此输入图像描述

Tried 2 different approaches. 尝试了两种不同的方法。

An inappropriate one, starting from a circle, draw each segment independently and rely on the way data's sorted and a property labelled parent to identify a segment within a chunk (main/bigger segment). 不合适的一个,从一个圆圈开始,独立地绘制每个段,并依赖于数据的排序方式和标记parent的属性来标识块(主/更大段)中的段。 This way, I can't easily place labels according to the main segment's place in the circle and it does not feel natural datawise. 这样,我就不能根据主要部分在圆圈中的位置轻松放置标签,并且数据感觉不自然。

https://jsfiddle.net/raven0us/c2jtsv4m/ https://jsfiddle.net/raven0us/c2jtsv4m/

A more appropriate one, have chunks (main segments) and inner chunks as children, this way, I can use centroid and place labels accordingly. 一个更合适的一个,有块(主要部分)和内部块作为孩子,这样,我可以使用centroid并相应地放置标签。 Moreover, things seem natural, but I can't figure out how to draw multiple inner segments within the main segment so it looks like the chart in my previous attempt. 事情似乎很自然,但我无法弄清楚如何在主要细分中绘制多个内部细分,因此它看起来像我之前尝试的图表。

https://jsfiddle.net/raven0us/1v9mtdjL/ https://jsfiddle.net/raven0us/1v9mtdjL/

Data is mocked at the beginning of each script, console.log(data) before the colors array to see the exact structure of the data that I want to illustrate. 在每个脚本的开头console.log(data) ,在colors数组之前console.log(data)以查看我想要说明的数据的确切结构。

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. 我在https://bl.ocks.org/ialarmedalien/1e453ed9b148be442f50e06ad7eb3759上放了一个演示,所以你可以在那里看到数据输入。

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');

I am not sure If I understand the question properly. 我不确定我是否理解这个问题。 But this is too long to be crammed into a comment so I wrote an answer, maybe it solves the problem. 但这太长了,不能插入评论,所以我写了一个答案,也许它解决了问题。

The stated problem for the first method is: 第一种方法的陈述问题是:

This way, I can't easily place labels according to the main segment's place in the circle 这样,我就不能根据主要部分在圆圈中的位置轻松放置标签

The label placement code is: 标签放置代码是:

labels.selectAll("text")
        .data(keys)
        .enter()
        .append("text")
        .style("text-anchor", "middle")
        .style("font-weight", "bold")
//LABEL PLACEMENT CODE
            .attr("x", (d, i) => {
                return barScale(config.max * 1.2) * Math.cos(segmentSlice * i - Math.PI / 2);
            })
            .attr("y", (d, i) => {
                return barScale(config.max * 1.2) * Math.sin(segmentSlice * i - Math.PI / 2);
            })

This places the lables along big segment dividing lines. 这将标签放在大段分割线上。 We have a total of 12 segments each spanning 30 degrees. 我们总共有12个段,每个段跨越30度。 Each large segment has 6 sub-segments each one spanning 5 degrees. 每个大段有6个子段,每个子段跨越5度。 So it seems you only need to rotate your labels 15 degrees (3 sub-segment span) to place them like the picture in the question. 因此,您似乎只需要将标签旋转15度(3个子段跨度),将它们放置在问题中的图片中。

First convert 15 degrees to radians: 首先将15度转换为弧度:

15 * PI / 180 = 0.261799

Then add the above value to the label placement code: 然后将以上值添加到标签放置代码:

.attr("x", (d, i) => {
    return barScale(config.max * 1.2) * 
        Math.cos(segmentSlice * i - Math.PI / 2 + 0.261799); //HERE
    }).attr("y", (d, i) => {
        return barScale(config.max * 1.2) * 
           Math.sin(segmentSlice * i - Math.PI / 2 + 0.261799); //AND HERE
                })

Here is the updated fiddle: https://jsfiddle.net/fha19jtm/ 这是更新的小提琴: https//jsfiddle.net/fha19jtm/

And all the labels are placed like the given picture. 并且所有标签都像给定的图片一样放置。 The data property can also be used to change the rotation angle based on the combination of large/small segments. data属性还可用于根据大/小段的组合来改变旋转角度。 This way, the placement of each label can be fine-tuend to the desired amount. 这样,每个标签的放置可以精细到所需的量。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM