recharts icon indicating copy to clipboard operation
recharts copied to clipboard

PieChart - labels are overlapping when distance between them is really small

Open wkwiatek opened this issue 9 years ago • 44 comments

For some big numbers in pair with rather small ones, labels are overlapping other as there's no sufficient space for all of them.

Here is the demo: http://jsfiddle.net/qzk61f5h/

And this is how it looks like: image

I think that library should handle this case properly - maybe by showing them stacked up at the top of another?

wkwiatek avatar Jan 31 '17 13:01 wkwiatek

Same issue here selection_00120170814 51 02

stephenlacy avatar Feb 08 '17 21:02 stephenlacy

Is there any progress on fixing this issue?

Seems like a pretty big bug.

I have also run into this issue and I'm trying to find a way around it, but with no success.

wmg481 avatar Mar 03 '17 21:03 wmg481

Very big issue for me using this library.

Otoris avatar Aug 20 '17 21:08 Otoris

Would be nice to have an automated solution, but in the mean time I worked around by using Legend instead and had coloured pie slices, added some paddingAngle to make small slices obvious and added minAngle to also make the slice visible. You an tweak the angles, but it gives an idea.

Not bothering to create jsfiddle because they always seem broken by the time I view ... the relevant code snippets below ...

// Static data, or you could have a list of available colours and map your data to contain different fill colours
const myData = [
    {
      name: 'Name A',
      value: 9999,
      fill: '#908ce2',
    },
    {
      name: 'Name B',
      value: 5000,
      fill: '#2fe18a',
    },
    {
      name: 'Name C',
      value: 2,
      fill: '#ef8a88',
    },
    {
      name: 'Name D',
      value: 1,
      fill: '#8fcddd',
    },
  ];

...

<ResponsiveContainer width="60%" minHeight={400}>
  <PieChart>
    <Pie data={myData} dataKey="value" nameKey="name" fill="#8884d8" legendType="circle" paddingAngle={1} minAngle={1} />
    <Tooltip/>
    <Legend/>
  </PieChart>
</ResponsiveContainer>

mbyrne00 avatar Nov 28 '17 22:11 mbyrne00

Has anyone been able find a workaround to this problem?

zaunermax avatar Feb 12 '18 10:02 zaunermax

I have a problem with this too. I tried to use custom label, but i can't create algorithm to calc positions for labels...=(

TotallWAR avatar Apr 20 '18 10:04 TotallWAR

capture d ecran 2018-05-23 a 14 17 57

Highcharts handle this smoothly... It should be great to have a workaround for this issue. https://www.highcharts.com/demo/pie-basic

mrkpatchaa avatar May 23 '18 14:05 mrkpatchaa

I'm having the same problem.

Does anyone have a solution?

I also had issues placing custom label text, but I am using the following solution for that as well as some logic for translating the labels because the built in locations were not working:

const offsetLabel = (name, x, y) => {
  let offsetX
  let offsetY
  if (name === '100%') return [22, 5]
  if (x <= 145) {
    offsetX = 14
  } else {
    offsetX = 22
  }
  if (y <= 105) {
    offsetY = 11
  } else {
    offsetY = 5
  }
  return [offsetX, offsetY]
}

const renderLabelContent = props => {
  const { name, x, y, midAngle } = props
  const [offsetX, offsetY] = offsetLabel(name, x, y)
  return (
    <g transform={`translate(${x}, ${y})`} textAnchor={(midAngle < -90 || midAngle >= 90) ? 'end' : 'start'}>
      <text x={offsetX} y={offsetY}>{name}</text>
    </g>
  )
}

I am passing the function renderLabelContent to the attribute label below:

<ReactPieChart width={200} height={220}>
        <Pie
          startAngle={90}
          endAngle={450}
          innerRadius={innerRadius || 0}
          data={data}
          paddingAngle={paddingAngle}
          stroke={stroke}
          label={renderLabelContent}
          labelLine={false}
          dataKey='value'
        />
      </ReactPieChart>

zgallagher08 avatar Jan 11 '19 17:01 zgallagher08

We ended up creating a custom label function that just returns NULL for values that are less than a configured percent of the total. Since the value is shown in a tooltip on hover, this was the cleanest solution we could think of without trying to calculate positions. What also helped was being sure to sort the values such that the smallest always appear at "3 'o clock" to minimize the possibility of horizontal overlap.

image 2019-03-04 at 12 33 54 pm

That being said, this is certainly not ideal and would love to know if there's actual interest in getting a real fix for the library.

blambillotte avatar Mar 05 '19 21:03 blambillotte

+1 for the issue, any update?

7hibault avatar Mar 26 '19 15:03 7hibault

Thanks to @zgallagher08 initial comment, I came with following code, which pretty much provides the same output as @rmkpatchaa shown :

renderCustomizedLabel({
    cx, cy, midAngle, innerRadius, outerRadius, value, color, startAngle, endAngle}) {
    const RADIAN = Math.PI / 180;
    const diffAngle = endAngle - startAngle;
    const delta = ((360-diffAngle)/15)-1;
    const radius = innerRadius + (outerRadius - innerRadius);
    const x = cx + (radius+delta) * Math.cos(-midAngle * RADIAN);
    const y = cy + (radius+(delta*delta)) * Math.sin(-midAngle * RADIAN);
    return (
      <text x={x} y={y} fill={color} textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central" fontWeight="normal">
        {value}
      </text>
    );
  };
  renderCustomizedLabelLine(props){
    let { cx, cy, midAngle, innerRadius, outerRadius, color, startAngle, endAngle } = props;
    const RADIAN = Math.PI / 180;
    const diffAngle = endAngle - startAngle;
    const radius = innerRadius + (outerRadius - innerRadius);
    let path='';
    for(let i=0;i<((360-diffAngle)/15);i++){
      path += `${(cx + (radius+i) * Math.cos(-midAngle * RADIAN))},${(cy + (radius+i*i) * Math.sin(-midAngle * RADIAN))} `
    }
    return (
      <polyline points={path} stroke={color} fill="none" />
    );
  }

image

abishek28007 avatar Jun 03 '19 14:06 abishek28007

Any update? Currently can't use this lib with this bug and @abishek28007 solution doesn't render correctly for us.

joe-akers-douglas-otm avatar Oct 09 '19 10:10 joe-akers-douglas-otm

@abishek28007 After doing some changes to your customized label function it's working fine for me. I'm a bit confused about where to set the customized line function to the pie chart ??

senelithperera avatar Oct 16 '19 17:10 senelithperera

@SenelithPerera

<Pie labelLine={renderCustomizedLabelLine}
        label={renderCustomizedLabel}
>

abishek28007 avatar Oct 16 '19 17:10 abishek28007

@abishek28007 Thanks for getting back to me, but unfortunately your solution didn't fix my problem 100% so I moved to temporary solution, which is showing the labels like this (custom active Index) , as specified in this

Screenshot from 2019-10-17 10-49-26

We need a concrete solution for showing labels in pie charts without having the overlapping issue !!

senelithperera avatar Oct 17 '19 05:10 senelithperera

@SenelithPerera , can you post the modification that you made in my code, would help me to come up with a fix for this.

abishek28007 avatar Oct 17 '19 05:10 abishek28007

Any update on this? It's a pretty serious issue for quite an ordinary and common use case and hasn't been solved for a while now. Is it being worked on?

pgularski avatar Nov 21 '19 14:11 pgularski

<Pie isAnimationActive={false} isUpdateAnimationActive={true} data={props.data} dataKey="value" nameKey="name" labelLine={({ cx, cy, midAngle, innerRadius, outerRadius, value, index }) => { const RADIAN = Math.PI / 180;

                    // eslint-disable-next-line
                    let radius1 = 20 + innerRadius + (outerRadius - innerRadius);

                    let radius2 = innerRadius + (outerRadius - innerRadius);
                    // eslint-disable-next-line
                    let x2 = cx + radius1 * Math.cos(-midAngle * RADIAN);
                    // eslint-disable-next-line
                    let y2 = cy + radius1 * Math.sin(-midAngle * RADIAN);

                    let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN);
                    // eslint-disable-next-line
                    let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN);

                    if (value<30){
                        return null;
                    }

                    return(
                        <line x1={x1} y1={y1} x2={x2} y2={y2} stroke="#ccc" strokeWidth={1}>

                        </line>
                    )
                }}
                label={({
                    cx,
                    cy,
                    midAngle,
                    innerRadius,
                    outerRadius,
                    value,
                    index
                }) => {
                    const RADIAN = Math.PI / 180;
                    
                    // eslint-disable-next-line
                    let radius = 20 + innerRadius + (outerRadius - innerRadius);
                    // eslint-disable-next-line
                    let x = cx + radius * Math.cos(-midAngle * RADIAN);
                    // eslint-disable-next-line
                    let y = cy + radius * Math.sin(-midAngle * RADIAN);
                    if (value<30){
                        return null;
                    }
                    return (
                        <text
                            x={x}
                            y={y}
                            fill="#000"
                            fontWeight="300"
                            fontSize="13px"
                            fontFamily="'Source Sans Pro', 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'"
                            textAnchor={x > cx ? "start" : "end"}
                            dominantBaseline="central"
                        >
                            {props.data[index].name} {value}
                                                  </text>
                    );
                }}
                cx="50%"
                cy="50%"
                outerRadius="84%"
                innerRadius="40%"
                fill="#8884d8" >
                    {/* <LabelList dataKey="name" position="outside" /> */}
                    <Label value={100} position="center" fontSize={24} fontWeight={400}></Label>
            </Pie>

avinashsingh953 avatar Dec 12 '19 09:12 avinashsingh953

Any update on this? It's a pretty serious issue for quite an ordinary and common use case and hasn't been solved for a while now. Is it being worked on?

Use This <Pie isAnimationActive={false} isUpdateAnimationActive={true} data={props.data} dataKey="value" nameKey="name" labelLine={({ cx, cy, midAngle, innerRadius, outerRadius, value, index }) => { const RADIAN = Math.PI / 180;

                    // eslint-disable-next-line
                    let radius1 = 20 + innerRadius + (outerRadius - innerRadius);

                    let radius2 = innerRadius + (outerRadius - innerRadius);
                    // eslint-disable-next-line
                    let x2 = cx + radius1 * Math.cos(-midAngle * RADIAN);
                    // eslint-disable-next-line
                    let y2 = cy + radius1 * Math.sin(-midAngle * RADIAN);

                    let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN);
                    // eslint-disable-next-line
                    let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN);

                    if (value<30){
                        return null;
                    }

                    return(
                        <line x1={x1} y1={y1} x2={x2} y2={y2} stroke="#ccc" strokeWidth={1}>

                        </line>
                    )
                }}
                label={({
                    cx,
                    cy,
                    midAngle,
                    innerRadius,
                    outerRadius,
                    value,
                    index
                }) => {
                    const RADIAN = Math.PI / 180;
                    
                    // eslint-disable-next-line
                    let radius = 20 + innerRadius + (outerRadius - innerRadius);
                    // eslint-disable-next-line
                    let x = cx + radius * Math.cos(-midAngle * RADIAN);
                    // eslint-disable-next-line
                    let y = cy + radius * Math.sin(-midAngle * RADIAN);
                    if (value<30){
                        return null;
                    }
                    return (
                        <text
                            x={x}
                            y={y}
                            fill="#000"
                            fontWeight="300"
                            fontSize="13px"
                            fontFamily="'Source Sans Pro', 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'"
                            textAnchor={x > cx ? "start" : "end"}
                            dominantBaseline="central"
                        >
                            {props.data[index].name} {value}
                                                  </text>
                    );
                }}
                cx="50%"
                cy="50%"
                outerRadius="84%"
                innerRadius="40%"
                fill="#8884d8" >
                    {/* <LabelList dataKey="name" position="outside" /> */}
                    <Label value={100} position="center" fontSize={24} fontWeight={400}></Label>
            </Pie>

avinashsingh953 avatar Dec 12 '19 09:12 avinashsingh953

@avinashsingh953 I assume your solution won't render label for value < 30

abishek28007 avatar Dec 12 '19 16:12 abishek28007

@avinashsingh953 I assume your solution won't render label for value < 30

Yes But you can adjust the value or calculate the angle for which labels can be hidden

avinashsingh953 avatar Dec 13 '19 04:12 avinashsingh953

@abishek28007 - what is the significance or reason for the number 15 in working out the delta variable in your solution? Thanks

lxm7 avatar Jan 28 '20 12:01 lxm7

@lxm7 15 and -1 are some arbitrary value which worked my use case, depending upon the use case they may change. I am trying to generalise it, any suggestion is appreciated.

abishek28007 avatar Jan 28 '20 16:01 abishek28007

I think this not only affects pie charts but also stacked bar charts and line charts.

MZanggl avatar May 18 '20 01:05 MZanggl

I’d like to bump this issue. Any solid solution would be appreciated

jbyerline avatar Jun 17 '21 05:06 jbyerline

Hi. We are facing the same issue as well where the labels are getting overlapped. Any fix for this? Thanks.

image

tvvignesh avatar Aug 25 '21 11:08 tvvignesh

We've been looking for a solution for some time; even considered implementing something ourselves, possibly using things like d3-labeler. All for naught; we finally changed the UI and implemented the information via a legend. Seems to me like the only reasonable solution at this point.

JESii avatar Sep 24 '21 21:09 JESii

any update on this?

laclance avatar Apr 16 '22 14:04 laclance

Automatically placing an arbitrary amount of labels for arbitrarily thin slices of a Pie is not reasonable.

Instead we could extend the label feature:

  • to not show the label for slices smaller than X percent (possibly configurable) https://github.com/recharts/recharts/issues/490#issuecomment-469865402
  • to add an option to only show the label on hover https://github.com/recharts/recharts/issues/490#issuecomment-543007483

nikolasrieble avatar Dec 09 '22 13:12 nikolasrieble

Seems reasonable, @nikolasrieble ... and maybe a combination of the two: show the labels that can fit, and then add hover for the others. Because for our use case, we definitely want the users to see the percentage. Using a legend as we did turned out to work great: see it all at a glance, no matter how small.

JESii avatar Dec 09 '22 15:12 JESii