【问题标题】:trying to create a donut chart with labels inside the curve using d3.js尝试使用 d3.js 在曲线内创建带有标签的圆环图
【发布时间】:2021-05-06 09:36:06
【问题描述】:

我正在尝试弄清楚如何将文本放入圆弧内并自动调整标签的字体大小以适应圆弧,以免发生文本溢出。

我正在使用 D3.js 创建一个形状,然后尝试将文本放在圆弧上并调整我的值,我将文本放入甜甜圈形状中。

问题是文本没有从适当的位置开始,我也希望文本集中。

以下是我一直在使用的代码和当前输出:

function wrap(text, width) {
  let lineNumbers = 1;

  text.each(function () {
    let text = d3.select(this),
      line = [],
      lineNumbers = 1,
      lineNumber = 0,
      words = text.text().split(/\s+/).reverse();
    words2 = text.text().split(/\s+/).reverse();
    console.log("text", text);

    while ((word = words.pop())) {
      line.push(word);
      current_line = line.join(" ");
      console.log("current_line", current_line.length);
      if (current_line.length > width) {
        line.pop();
        current_line = line.join(" ");
        line = [word];
        lineNumbers += 1;
      }
    }
    console.log("lineNumbers", lineNumbers);
    append_line = [];
    (lineHeight = 1), // ems
      (x = text.attr("x")),
      (y = text.attr("y")),
      (dy = 1), //parseFloat(text.attr("dy")),
      (tspan = text
        .text(null)
        .append("tspan")
        .attr("x", x)
        .attr("y", y)
        .attr("dy", dy + "em"));
    while ((word = words2.pop())) {
      line = 0;
      append_line.push(word);
      tspan.text(append_line.join(" "));
      if (tspan.node().getComputedTextLength() > width) {
        append_line.pop();
        line = 1;
        tspan.text(append_line.join(" "));
        append_line = [word];
        if (line == 0) {
          tspan = text
            .append("tspan")
            .attr("x", x)
            .attr("y", y)
            .attr("dy", ++lineNumber * lineHeight + dy + "em")
            .attr("dx", -9 + "em")
            .text(word);
        }
        if (line == 1) {
          tspan = text
            .append("tspan")
            .attr("x", x)
            .attr("y", y)
            .attr("dy", ++lineNumber * lineHeight + dy + "em")
            .attr("dx", -10 + "em")
            .text(word);
        }
      }
    }
  });
}
let screenWidth = window.innerWidth;

let margin = { left: 20, top: 20, right: 20, bottom: 20 },
  width = Math.min(screenWidth, 500) - margin.left - margin.right,
  height = Math.min(screenWidth, 500) - margin.top - margin.bottom;

let svg = d3
  .select("#chart")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("class", "wrapper")
  .attr(
    "transform",
    "translate(" +
      (width / 2 + margin.left) +
      "," +
      (height / 2 + margin.top) +
      ")"
  );

//////////////////////////////////////////////////////////////
///////////////////// Data &  Scales /////////////////////////
//////////////////////////////////////////////////////////////

//Some random data
let donutData = [
  { name: "Antelope sjvadknk saoindosa savasa slahslas sasas", value: 15 },
  { name: "Bear fhfxhxfhxgxhg hgxhx", value: 9 },
  { name: "Cheetah", value: 19 },
  { name: "Dolphin", value: 12 },
  { name: "Elephant", value: 14 },
  { name: "Flamingo", value: 21 },
  { name: "Giraffe", value: 18 },
  { name: "Other", value: 8 },
];

//Create a color scale
let colorScale = d3.scale
  .linear()
  .domain([1, 3.5, 6])
  .range(["#2c7bb6", "#ffffbf", "#d7191c"])
  .interpolate(d3.interpolateHcl);

//Create an arc function
let arc = d3.svg
  .arc()
  .innerRadius((width * 0.75) / 2)
  .outerRadius((width * 0.75) / 2 + 30);

//Turn the pie chart 90 degrees counter clockwise, so it starts at the left
let pie = d3.layout
  .pie()
  .startAngle((-90 * Math.PI) / 180)
  .endAngle((-90 * Math.PI) / 180 + 2 * Math.PI)
  .value(function (d) {
    return d.value;
  })
  .padAngle(0.01)
  .sort(null);

//////////////////////////////////////////////////////////////
//////////////////// Create Donut Chart //////////////////////
//////////////////////////////////////////////////////////////

//Create the donut slices and also the invisible arcs for the text
svg
  .selectAll(".donutArcs")
  .data(pie(donutData))
  .enter()
  .append("path")
  .attr("class", "donutArcs")
  .attr("d", arc)
  .style("fill", function (d, i) {
    if (i === 7) return "#CCCCCC";
    //Other
    else return colorScale(i);
  })
  .each(function (d, i) {
    //Search pattern for everything between the start and the first capital L
    let firstArcSection = /(^.+?)L/;

    //Grab everything up to the first Line statement
    let newArc = firstArcSection.exec(d3.select(this).attr("d"))[1];
    //Replace all the comma's so that IE can handle it
    newArc = newArc.replace(/,/g, " ");

    //If the end angle lies beyond a quarter of a circle (90 degrees or pi/2)
    //flip the end and start position
    if (d.endAngle > (90 * Math.PI) / 180) {
      let startLoc = /M(.*?)A/, //Everything between the first capital M and first capital A
        middleLoc = /A(.*?)0 0 1/, //Everything between the first capital A and 0 0 1
        endLoc = /0 0 1 (.*?)$/; //Everything between the first 0 0 1 and the end of the string (denoted by $)
      //Flip the direction of the arc by switching the start en end point (and sweep flag)
      //of those elements that are below the horizontal line
      let newStart = endLoc.exec(newArc)[1];
      let newEnd = startLoc.exec(newArc)[1];
      let middleSec = middleLoc.exec(newArc)[1];

      //Build up the new arc notation, set the sweep-flag to 0
      newArc = "M" + newStart + "A" + middleSec + "0 0 0 " + newEnd;
    } //if

    //Create a new invisible arc that the text can flow along
    svg
      .append("path")
      .attr("class", "hiddenDonutArcs")
      .attr("id", "donutArc" + i)
      .attr("d", newArc)
      .style("fill", "none");
  });

//Append the label names on the outside
svg
  .selectAll(".donutText")
  .data(pie(donutData))
  .enter()
  .append("text")
  .attr("class", "donutText")
  //Move the labels below the arcs for those slices with an end angle greater than 90 degrees
  .attr("dy", 20)
  .append("textPath")
  .attr("startOffset", "50%")
  .style("text-anchor", "middle")
  .attr("xlink:href", function (d, i) {
    return "#donutArc" + i;
  })
  .attr("font-size", function (d, i) {
    return 10;
  })
  .text(function (d) {
    return d.data.name;
  })
  .call(wrap, 100);

任何帮助都会非常好,提前感谢:)

【问题讨论】:

  • 有些文本在弧内,有些在弧外?是故意的吗?
  • 目前实际上是有意为之,但所有内容都可以在拱门内移动,如果发生文本溢出,则应为多行

标签: javascript html css d3.js


【解决方案1】:

这是我通过拆分长标签并计算每个部分的文本路径来解决问题的尝试:

const data = [
  {value: 30, text: 'First', color: 'red'},
  {value: 40, text: 'Second', color: 'green'},
  {value: 60, text: 'Third', color: 'blue'},
  {value: 50, text: 'A very very very very very very very very long text', color: 'yellow'},
];


const splitLongString = (str, count) => {
  const partLength = Math.round(str.length / count);
  const words = str.split(' ');
  const parts = [];
  str.split(' ').forEach(part => {
  if (!parts.length) {
    parts.push(part);
  }
  else {
    const last = parts[parts.length - 1];
    if (parts[parts.length - 1].length >= partLength)
    parts.push(part);
  else  
    parts[parts.length - 1] += ' ' + part;
  }
});
return parts;
};

const svg = d3.select('svg');
const width = parseInt(svg.attr('width'));
const height = parseInt(svg.attr('height'));

const margin = 10;
const arcWidth = 50;
const radius = Math.min(width/2 - margin, height/2 - margin) - arcWidth / 2;
const center = {x: width / 2, y: height / 2};

let anglePos = 0;
const angleOffset = 0.025;

const sum = data.reduce((s, {value}) => s + value, 0);
data.forEach(({value, text, color}, index) => {
    const angle = Math.PI * 2 * value / sum;
  const startAngle = anglePos + angleOffset;
  anglePos += angle;
  const endAngle = anglePos - angleOffset;
  const start = {
    x: center.x + radius * Math.sin(startAngle),
    y: center.y + radius * -Math.cos(startAngle),
  };
  const end = {
    x: center.x + radius * Math.sin(endAngle),
    y: center.y + radius * -Math.cos(endAngle),
  };
  const flags = value / sum >= 0.5 ? '1 1 1' : '0 0 1';
  const pathId = `my-pie-chart-path-${index}`;
  const path = svg.append('path')
    .attr('id', pathId)
    .attr('d', `M ${start.x},${start.y} A ${radius},${radius} ${flags} ${end.x},${end.y}`)
    .style('stroke', color)
    .style('fill', 'none')
    .style('stroke-width', arcWidth);
    
  const len = path.node().getTotalLength();
  
  const textElement = svg.append('text')
    .text(text)
    .attr('dy', 0)
    .attr('text-anchor', 'middle');
  const width = textElement.node().getBBox().width;  
  let texts = [text];
  if (width > len)
    texts = splitLongString(text, Math.ceil(width / len));
        
  textElement.text(null);
  
  // const midAngle = anglePos - angle / 2;
  
  texts.forEach((t, i) => {
    const textPathId = `my-pie-chart-path-${index}-${i}`;
    const textRadius = radius - i * 12;
    const textStart = {
    x: center.x + textRadius * Math.sin(startAngle),
    y: center.y + textRadius * -Math.cos(startAngle),
  };
  const textEnd = {
    x: center.x + textRadius * Math.sin(endAngle),
    y: center.y + textRadius * -Math.cos(endAngle),
  };

  const path = svg.append('path')
    .attr('id', textPathId)
    .attr('d', `M ${textStart.x},${textStart.y} A ${textRadius},${textRadius} ${flags} ${textEnd.x},${textEnd.y}`)
    .style('stroke', 'none')
    .style('fill', 'none');
  
  textElement.append('textPath')
    .text(t)
    .attr('startOffset', (endAngle - startAngle) * textRadius / 2)
    .attr('href', `#${textPathId}`)
  });

});
text {
  font-family: Calibri;
  font-size: 12px;
  font-weight: bold;
  fill: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>

<svg width="300" height="300"></svg>

【讨论】:

  • 谢谢你的答案,它很好地打破了线条,除了我正在寻找的东西是如果文本无法放入容器中,那么字体大小应该自动调整大小
  • 简而言之,只要有文本溢出,字体大小应动态更改以适应容器
  • 我认为在同一个图表上使用不同字体大小的文本不是一个好主意(从 UI 角度来看)。您可以做的是不仅在空格上拆分长词,也可以对超过一定限制的文本使用省略号(...)
  • 那是真的,但问题是我要创建的图表可以有多达 40 个左右的除法,因此用户至少可以了解它是什么……我一直在寻找改变如果区域不够,字体大小会动态变化
  • 40 条弧线从视觉上看太多了。我怀疑饼图对于这些数字来说是一个很好的可视化模型......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-07-04
  • 1970-01-01
  • 1970-01-01
  • 2017-08-27
  • 1970-01-01
  • 2014-01-19
相关资源
最近更新 更多