使用 C3/D3 修复饼图标签重叠

Fix Piechart label overlap using C3/D3

根据我原来的问题 (),我试图将最初为 flot 创建的错误修正应用于 C3 饼图。 原则上它似乎有效,检测到碰撞并且正在移动标签但定位似乎不正确。 这是一些示例代码来显示问题

var columns = ['data11', 'data2', 'data347', 'data40098', 'data777'];
var data = [150, 250, 300, 50, 50];
var colors = ['#0065A3', '#767670', '#D73648', '#7FB2CE', '#00345B'];
var padding = 5;

var chart = c3.generate({
  bindto: d3.select('#chart'),
  data: {
    columns: [
      [columns[0]].concat(data[0])
    ],
    type: 'pie',
  },
  legend: {
    position: 'right',
    show: true
  },
  pie: {
    label: {
      threshold: 0.001,
      format: function(value, ratio, id) {
        return [id, d3.format(",.0f")(value), "[" + d3.format(",.1%")(ratio) + "]"].join(';');
      }
    }
  },
  color: {
    pattern: colors
  },
  onrendered: function() {
    redrawLabelBackgrounds();
  }
});



function addLabelBackground(index) {
  //get label text element
  var textLabel = d3.select(".c3-target-" + columns[index] + " > text");
  //add rect to parent
  var labelNode = textLabel.node();
  if (labelNode /*&& labelNode.innerHTML.length > 0*/ ) {
    var p = d3.select(labelNode.parentNode).insert("rect", "text")
      .style("fill", colors[index]);
  }
}

for (var i = 0; i < columns.length; i++) {
  if (i > 0) {

    setTimeout(function(column) {
      chart.load({
        columns: [
           [columns[column]].concat(data[column]),
        ]
      });
      //chart.data.names(columnNames[column])
      addLabelBackground(column);

    }, (i * 5000 / columns.length), i);
  } else {
    addLabelBackground(i);
  }
}

function redrawLabelBackgrounds() {
  function isOverlapping(pos1, pos2) {
    /*isOverlapping = (x1min < x2max AND x2min < x1max AND y1min < y2max AND y2min < y1max)
           /*
           * x1min = pos1[0][0]
           * x1max = pos1[0][1]
           * y1min = pos1[1][0]
           * y1max = pos1[1][1]
           *
           * x2min = pos2[0][0]
           * x2max = pos2[0][1]
           * y2min = pos2[1][0]
           * y2max = pos2[1][1]
           *
           * isOverlapping = (pos1[0][0] < pos2[0][1] AND pos2[0][0] < pos1[0][1] AND pos1[1][0] < pos2[1][1] AND pos2[1][0] < pos1[1][1])
           */
    return (pos1[0][0] < pos2[0][1] && pos2[0][0] < pos1[0][1] && pos1[1][0] < pos2[1][1] && pos2[1][0] < pos1[1][1]);
  }

  function getAngle(pos) {
    //Q1 && Q4
    if ((pos[0] > 0 && pos[1] >= 0) || (pos[0] > 0 && pos[1] > 0)) {
      return Math.atan(pos[0] / pos[1]);
    }
    //Q2
    else if (pos[0] < 0 && pos[1] >= 0) {
      return Math.atan(pos[0] / pos[1]) + Math.PI;
    }
    //Q3
    else if (pos[0] < 0 && pos[1] <= 0) {
      return Math.atan(pos[0] / pos[1]) - Math.PI;
    }
    // x = 0, y>0
    else if (pos[0] === 0 && pos[1] > 0) {
      return Math.PI / 2;
    }
    // x = 0, y<0
    else if (pos[0] === 0 && pos[1] < 0) {
      return Math.PI / -2;
    }
    // x= 0, y = 0
    else {
      return 0;
    }
  }


  //for all label texts drawn yet
  var labelSelection = d3.select('#chart').selectAll(".c3-chart-arc > text");

  //first put all label nodes in one array
  var allLabels = [];
  labelSelection.each(function() {
    allLabels.push(d3.select(this));
  });
  //then check and modify labels
  labelSelection.each(function(v) {
    // get d3 node
    var label = d3.select(this);
    var labelNode = label.node();
    //check if label is drawn
    if (labelNode) {
      var bbox = labelNode.getBBox();
      var labelTextHeight = bbox.height;
      if (labelNode.childElementCount === 0 && labelNode.innerHTML.length > 0) {
        //build data
        var data = labelNode.innerHTML.split(';');
        label.text("");
        data.forEach(function(i, n) {
          label.append("tspan")
            .text(i)
            .attr("dy", (n === 0) ? 0 : "1.2em")
            .attr("x", 0)
            .attr("text-anchor", "middle");
        }, label);
      }
      //check if element is visible
      if (d3.select(labelNode.parentNode).style("display") !== 'none') {

        //get pos of the label text
        var labelPos = label.attr("transform").match(/-?\d+(\.\d+)?/g);
        if (labelPos && labelPos.length === 2) {
          //get surrounding box of the label
          bbox = labelNode.getBBox();

          // modify the labelPos of the text - check to make sure that the label doesn't overlap one of the other labels
          // check to make sure that the label doesn't overlap one of the other labels - 4.3.2014 - from flot user fix pie_label_ratio on github
          var newRadius = Math.sqrt(labelPos[0] * labelPos[0] + labelPos[1] * labelPos[1]);
          var angle = getAngle(labelPos);
          var labelBottom = (labelPos[1] - bbox.height / 2); //
          var labelLeft = (labelPos[0] - bbox.width / 2); //
          var bCollision = false;
          var labelBox = [
            [labelLeft, labelLeft + bbox.width],
            [labelBottom, labelBottom + bbox.height]
          ];
          var yix = 10; //max label reiterations with collisions
          do {
            for (var i = allLabels.length - 1; i >= 0; i--) {
              if (!labelNode.isEqualNode(allLabels[i].node())) {
                var checkLabelBBox = allLabels[i].node().getBBox();
                var checkLabelPos = allLabels[i].attr("transform").match(/-?\d+(\.\d+)?/g);
                var checkLabelBox = [
                  [(checkLabelPos[0] - checkLabelBBox.width / 2), (checkLabelPos[0] - checkLabelBBox.width / 2) + checkLabelBBox.width],
                  [(checkLabelPos[1] - checkLabelBBox.height / 2), (checkLabelPos[1] - checkLabelBBox.height / 2) + checkLabelBBox.height]
                ];

                while (isOverlapping(labelBox, checkLabelBox)) {
                  newRadius -= 2;
                  if (newRadius < 0.00) {
                    break;
                  }
                  x = Math.round(Math.cos(angle) * newRadius);
                  y = Math.round(Math.sin(angle) * newRadius);
                  labelBottom = (y - bbox.height / 2);
                  labelLeft = (x - bbox.width / 2);
                  labelBox[0][0] = labelLeft;
                  labelBox[0][1] = labelLeft + bbox.width;
                  labelBox[1][0] = labelBottom;
                  labelBox[1][1] = labelBottom + bbox.height;
                  bCollision = true;
                }
                if (bCollision) break;
              }
            }
            if (bCollision) bCollision = false;
            else break;
            yix--;
          }
          while (yix > 0);
          //now apply the potentially corrected positions to the label
          if (labelPos[0] !== (labelLeft + bbox.width / 2) || labelpos[1] !== (labelBottom + bbox.height / 2)) {
            labelPos[0] = labelLeft + bbox.width / 2;
            labelPos[1] = labelBottom + bbox.height / 2;
            label.attr("transform", "translate(" + labelPos[0] + ',' + labelPos[1] + ")");
          }

          //now draw and move the rects
          d3.select(labelNode.parentNode).select("rect")
            .attr("transform", "translate(" + (labelLeft - padding) +
              "," + (labelPos[1] - labelTextHeight / 2 - padding) + ")")
            .attr("width", bbox.width + 2 * padding)
            .attr("height", bbox.height + 2 * padding);
        }
      }
    }
  });
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.9/c3.min.css" rel="stylesheet" />
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.js"></script>
<div id="chart">

</div>

有人知道这里出了什么问题吗?

基本上它是几件事的结合: - 计算角度(搞砸了 arctan ;)) - 检查矩形的重叠(不仅是文本框) - 错误的循环

这现在有效:

var columns = ['data11', 'data2', 'data347', 'data40098', 'data777'];
var data = [150, 250, 300, 50, 50];
var colors = ['#0065A3', '#767670', '#D73648', '#7FB2CE', '#00345B'];
var padding = 5;

var chart = c3.generate({
  bindto: d3.select('#chart'),
  data: {
    columns: [
      [columns[0]].concat(data[0])
    ],
    type: 'pie',
  },
  legend: {
    position: 'right',
    show: true
  },
  pie: {
    label: {
      threshold: 0.001,
      format: function(value, ratio, id) {
        return [id, d3.format(",.0f")(value), "[" + d3.format(",.1%")(ratio) + "]"].join(';');
      }
    }
  },
  color: {
    pattern: colors
  },
  onrendered: function() {
    redrawLabelBackgrounds();
  }
});



function addLabelBackground(index) {
  //get label text element
  var textLabel = d3.select(".c3-target-" + columns[index] + " > text");
  //add rect to parent
  var labelNode = textLabel.node();
  if (labelNode /*&& labelNode.innerHTML.length > 0*/ ) {
    var p = d3.select(labelNode.parentNode).insert("rect", "text")
      .style("fill", colors[index]);
  }
}

for (var i = 0; i < columns.length; i++) {
  if (i > 0) {

    setTimeout(function(column) {
      chart.load({
        columns: [
          [columns[column]].concat(data[column]),
        ]
      });
      //chart.data.names(columnNames[column])
      addLabelBackground(column);

    }, (i * 5000 / columns.length), i);
  } else {
    addLabelBackground(i);
  }
}

function redrawLabelBackgrounds() {
  function isOverlapping(pos1, pos2) {
    /*isOverlapping = (x1min < x2max AND x2min < x1max AND y1min < y2max AND y2min < y1max)
           /*
           * x1min = pos1[0][0]
           * x1max = pos1[0][1]
           * y1min = pos1[1][0]
           * y1max = pos1[1][1]
           *
           * x2min = pos2[0][0]
           * x2max = pos2[0][1]
           * y2min = pos2[1][0]
           * y2max = pos2[1][1]
           *
           * isOverlapping = (pos1[0][0] < pos2[0][1] AND pos2[0][0] < pos1[0][1] AND pos1[1][0] < pos2[1][1] AND pos2[1][0] < pos1[1][1])
           */
    return (pos1[0][0] < pos2[0][1] && pos2[0][0] < pos1[0][1] && pos1[1][0] < pos2[1][1] && pos2[1][0] < pos1[1][1]);
  }

  function getAngle(pos) {
    //Q1
    if ((pos[0] > 0 && pos[1] >= 0)) {
      return Math.atan(pos[1] / pos[0]);
    }
    //Q2 & Q3
    else if (pos[0] < 0) {
      return Math.atan(pos[1] / pos[0]) + Math.PI;
    } //Q4
    else if (pos[0] > 0 && pos[1] < 0) {
      return Math.atan(pos[1] / pos[0]) + 2 * Math.PI;
    }
    // x = 0, y>0
    else if (pos[0] === 0 && pos[1] > 0) {
      return Math.PI / 2;
    }
    // x = 0, y<0
    else if (pos[0] === 0 && pos[1] < 0) {
      return Math.PI / -2;
    }
    // x= 0, y = 0
    else {
      return 0;
    }
  }


  //for all label texts drawn yet
  var labelSelection = d3.select('#chart').selectAll(".c3-chart-arc > text");
  //.filter(function(d){return d.style("display") !== 'none'});

  //first put all label nodes in one array
  var allLabels = [];
  labelSelection.each(function() {
    allLabels.push(d3.select(this));
  });
  //padding of the surrounding rect
  var rectPadding = 1;
  //then check and modify labels
  labelSelection.each(function(v, labelIndex) {
    // get d3 node
    var label = d3.select(this);
    var labelNode = label.node();
    //check if label is drawn
    if (labelNode) {
      var bbox = labelNode.getBBox();
      var labelTextHeight = bbox.height;
      if (labelNode.childElementCount === 0 && labelNode.innerHTML.length > 0) {
        //build data
        var data = labelNode.innerHTML.split(';');
        if (data.length > 1) {
          label.html('')
            .attr("dominant-baseline", "central")
            .attr("text-anchor", "middle");
          data.forEach(function(i, n) {
            label.append("tspan")
              .text(i)
              .attr("dy", (n === 0) ? 0 : "1.2em")
              .attr("x", 0);
          }, label);
        }
      }
      //check if element is visible
      if (d3.select(labelNode.parentNode).style("display") !== 'none') {

        //get pos of the label text
        var labelPos = label.attr("transform").match(/-?\d+(\.\d+)?/g);

        if (labelPos && labelPos.length === 2) {
          labelPos[0] = parseFloat(labelPos[0]);
          labelPos[1] = parseFloat(labelPos[1]);
          //get surrounding box of the label
          bbox = labelNode.getBBox();

          // modify the labelPos of the text - check to make sure that the label doesn't overlap one of the other labels
          // check to make sure that the label doesn't overlap one of the other labels - 4.3.2014 - from flot user fix pie_label_ratio on github
          var oldRadius = Math.sqrt(labelPos[0] * labelPos[0] + labelPos[1] * labelPos[1]);
          var newRadius = oldRadius;
          var angle = getAngle(labelPos);
          var labelBottom = (labelPos[1] - bbox.height / 2); //
          var labelLeft = (labelPos[0] - bbox.width / 2); //
          var bCollision = false;
          var labelBox = [ [ labelLeft - rectPadding, labelLeft + bbox.width + rectPadding ], [ labelBottom-rectPadding, labelBottom + bbox.height + rectPadding ] ];
          var yix = 10; //max label reiterations with collisions
          do {
            for (var i = labelIndex - 1; i >= 0; i--) {
              var checkLabelNode = allLabels[i].node();

              var checkLabelBBox = checkLabelNode.getBBox();
              var checkLabelPos = allLabels[i].attr("transform").match(/-?\d+(\.\d+)?/g);
              if (checkLabelBBox && checkLabelPos) { //element visible
                checkLabelPos[0] = parseFloat(checkLabelPos[0]);
                checkLabelPos[1] = parseFloat(checkLabelPos[1]);
                //box is text bbox + padding from rect
                var checkLabelBox = [
                  [(checkLabelPos[0] - checkLabelBBox.width / 2) - rectPadding, (checkLabelPos[0] + checkLabelBBox.width / 2) + rectPadding],
                  [(checkLabelPos[1] - checkLabelBBox.height / 2) - rectPadding, (checkLabelPos[1] + checkLabelBBox.height / 2) + rectPadding]
                ];

                while (isOverlapping(labelBox, checkLabelBox)) {
                  newRadius -= 2;
                  if (newRadius < 0.00) {
                    bCollision = true;
                    break;
                  }
                  x = Math.round(Math.cos(angle) * newRadius);
                  y = Math.round(Math.sin(angle) * newRadius);
                  labelBottom = (y - bbox.height / 2);
                  labelLeft = (x - bbox.width / 2);
                  labelBox[0][0] = labelLeft;
                  labelBox[0][1] = labelLeft + bbox.width;
                  labelBox[1][0] = labelBottom;
                  labelBox[1][1] = labelBottom + bbox.height;
                }
                if (bCollision) break;
              }

            }
            if (bCollision) bCollision = false;
            else break;
            yix--;
          }
          while (yix > 0);
          //now apply the potentially corrected positions to the label
          if (Math.round(labelPos[0]) !== Math.round(labelLeft + bbox.width / 2) || Math.round(labelPos[1]) !== Math.round(labelBottom + bbox.height / 2)) {
            /*console.log("moving label[" + labelIndex + "][" + labelPos[0] + "," + labelPos[1] + "] to [" + (labelLeft + bbox.width / 2) + "," + (labelBottom + bbox.height / 2) + "] Radius " + oldRadius + "=>" + newRadius + " #" + yix);*/
            labelPos[0] = labelLeft + bbox.width / 2;
            labelPos[1] = labelBottom + bbox.height / 2;
            label.attr("transform", "translate(" + labelPos[0] + ',' + labelPos[1] + ")");
          }

          //now draw and move the rects
          d3.select(labelNode.parentNode).select("rect")
            .attr("transform", "translate(" + (labelLeft - rectPadding) +
              "," + (labelPos[1] - labelTextHeight / 2 - rectPadding) + ")")
            .attr("width", bbox.width + 2 * rectPadding)
            .attr("height", bbox.height + 2 * rectPadding);
        }
      }
    }
  });
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.14/c3.min.css" rel="stylesheet" />
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.14/c3.min.js"></script>
<div id="chart">

</div>

可选待办事项:隐藏最后仍然重叠的标签