如何正确排列圆的标签?

huangapple go评论65阅读模式
英文:

How to properly arrange labels of a circle?

问题

我有一个如下所示的圆:

如何正确排列圆的标签?

正如您可能注意到的,从0到9的标签与外圆的距离与其他标签不同。我的期望是:

  1. 如何使标签与外圆的距离相同?
  2. 如何安排标签,使它们位于从中心出发的线的中间,而不是在其侧面?

您可以在这里找到我的代码:https://jsfiddle.net/ao4xwpnk/

我尝试更改我的代码的这一部分,以使标签对称,但没有成功。

let featureData = features.map((f, i) => {
  let angle = (Math.PI / 2) + (2 * Math.PI * i / features.length);
  let isLeftHalf = angle < Math.PI; // 检查角度是否在圆的左半部分

  let value = isLeftHalf ? 14.5 : 10.5; // 调整左右两侧标签的值

  return {
    "name": f,
    "angle": angle,
    "line_coord": angleToCoordinate(angle, 180),
    "label_coord": angleToCoordinate(angle, value)
  };
});
英文:

I have a circle like the following:

如何正确排列圆的标签?

As you might notice, labels from 0 to 9 have different proximity to the outer circle in comparison to other labels. My expectations are:

  1. How can I have same distance of a label to the outer circle?
  2. How can I arrange labels so that they are in the middle of the line that comes from center, and not in the side of it?

You can find my code here: https://jsfiddle.net/ao4xwpnk/

I tried to change this part of my code to have symmetry of labels, but it did not work.

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-html -->

  let featureData = features.map((f, i) =&gt; {
    let angle = (Math.PI / 2) + (2 * Math.PI * i / features.length);
    let isLeftHalf = angle &lt; Math.PI; // Check if the angle is in the left half of the circle

    let value = isLeftHalf ? 14.5 : 10.5; // Adjust the value for labels in left and right halves

    return {
      &quot;name&quot;: f,
      &quot;angle&quot;: angle,
      &quot;line_coord&quot;: angleToCoordinate(angle, 180),
      &quot;label_coord&quot;: angleToCoordinate(angle, value)
    };
  });

<!-- end snippet -->

答案1

得分: 2

使用text元素上的text-anchordy属性将有所帮助:

// 绘制轴标签
svg.selectAll(".axislabel")
  .data(featureData)
  .join(
    enter => enter.append("text")
      .attr("x", d => d.label_coord.x)
      .attr("y", d => d.label_coord.y)
      .attr("text-anchor", "middle")     // <-- 在这里
      .attr("dy", "0.35em")              // <-- 以及在这里
      .text(d => d.name)
  )

text-anchor用于水平定位:

渲染的字符被对齐,使得文本字符串的中间位于当前文本位置。

这解决了您尝试使用isLeftHalf来解决的问题。

dy属性用于垂直定位。值0.35em是一个约定 - 可以参考这个答案,其中有趣地说:

使用dy=0.35em可以帮助在字体大小不同的情况下垂直居中文本。

我理解这个值考虑了字体设计中下行线的平均值,例如,对于字母"p"和"g",'尾巴'大致占据了高度的35%,因此您通过这个值将标签向下移动,以与坐标轴对齐。可以参考这里

使用这个更新后的代码:

// 代码示例

希望这对您有所帮助。

英文:

Using text-anchor and dy attributes on the text element will help:

// draw axis label
svg.selectAll(&quot;.axislabel&quot;)
  .data(featureData)
  .join(
    enter =&gt; enter.append(&quot;text&quot;)
      .attr(&quot;x&quot;, d =&gt; d.label_coord.x)
      .attr(&quot;y&quot;, d =&gt; d.label_coord.y)
      .attr(&quot;text-anchor&quot;, &quot;middle&quot;)     // &lt;-- here
      .attr(&quot;dy&quot;, &quot;0.35em&quot;)              // &lt;-- and here
      .text(d =&gt; d.name)

text-anchor works for the horizontal placing:

> The rendered characters are aligned such that the middle of the text
> string is at the current text position.

Which solves the problem for you that you are trying to use isLeftHalf for.

The dy attribute works for the vertical placing. The value of 0.35em is a convention - see this answer which interestingly says:

> Using dy=0.35em can help vertically centre text regardless of font
> size

My read on it is that this value accounts for an average of the descenders in the font design e.g. for p and g the 'tail' is roughly 35% of the height and you are therefore pushing the label 'down' by that to line it up with the axes. See here for example.

Your code with this update:

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

let data = [];
    let features = [0, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; // labels on the circle
    //generate the data
    for (var i = 0; i &lt; 2; i++) {
      var point = {}
      //each feature will be a random number from 1-9
      features.forEach(f =&gt; point[f] = 1 + Math.random() * 162);
      data.push(point);
    }

    let width = 600;
    let height = 600;
    let svg = d3.select(&quot;body&quot;).append(&quot;svg&quot;)
      .attr(&quot;width&quot;, width)
      .attr(&quot;height&quot;, height);

    //adding radial scale
    let radialScale = d3.scaleLinear()
      .domain([0, 180])
      .range([0, 250]);

    //adding ticks
    let ticks = [20, 40, 60, 80, 100, 120, 140, 160, 180]; // 9 circles

    //adding circles
    svg.selectAll(&quot;circle&quot;)
      .data(ticks)
      .join(
        enter =&gt; enter.append(&quot;circle&quot;)
          .attr(&quot;cx&quot;, width / 2)
          .attr(&quot;cy&quot;, height / 2)
          .attr(&quot;fill&quot;, &quot;none&quot;)
          .attr(&quot;stroke&quot;, &quot;gray&quot;)
          .attr(&quot;r&quot;, d =&gt; radialScale(d))
          .attr(&quot;stroke-width&quot;, 1) // Set the same stroke-width for all circles
      );

    //adding white background circle to remove filling in the center
    svg.append(&quot;circle&quot;)
      .attr(&quot;cx&quot;, width / 2)
      .attr(&quot;cy&quot;, height / 2)
      .attr(&quot;fill&quot;, &quot;white&quot;)
      .attr(&quot;stroke&quot;, &quot;gray&quot;)
      .attr(&quot;r&quot;, radialScale(20))
      .attr(&quot;stroke-width&quot;, 1); // Set the same stroke-width for the center circle

    //adding text labels
    svg.selectAll(&quot;.ticklabel&quot;)
      .data(ticks)
      .join(
        enter =&gt; enter.append(&quot;text&quot;)
          .attr(&quot;class&quot;, &quot;ticklabel&quot;)
          .attr(&quot;x&quot;, width / 2 + 5)
          .attr(&quot;y&quot;, d =&gt; height / 2 - radialScale(d))
          .text(d =&gt; d.toString())
      );

    //Plotting the Axes
    //map an angle and value into SVG
    function angleToCoordinate(angle, value) {
      let x = Math.cos(angle) * radialScale(value);
      let y = Math.sin(angle) * radialScale(value);
      return { &quot;x&quot;: width / 2 + x, &quot;y&quot;: height / 2 - y };
    }

    let featureData = features.map((f, i) =&gt; {
      let angle = (Math.PI / 2) + (2 * Math.PI * i / features.length);
      let value = i &lt; 9 ? 10.5 : 12; // Adjust the value for labels 13 to 21 to 12
      return {
        &quot;name&quot;: f,
        &quot;angle&quot;: angle,
        &quot;line_coord&quot;: angleToCoordinate(angle, 180),
        &quot;label_coord&quot;: angleToCoordinate(angle, 190) // changin 190 allows me to adjust labels proximity to the circle!
      };
    });

    // draw axis line
    svg.selectAll(&quot;line&quot;)
      .data(featureData)
      .join(
        enter =&gt; enter.append(&quot;line&quot;)
          .attr(&quot;x1&quot;, width / 2)
          .attr(&quot;y1&quot;, height / 2)
          .attr(&quot;x2&quot;, d =&gt; d.line_coord.x)
          .attr(&quot;y2&quot;, d =&gt; d.line_coord.y)
          .attr(&quot;stroke&quot;, &quot;black&quot;)
          .attr(&quot;stroke-width&quot;, 1) // Set the same stroke-width for all axis lines
      );

    // draw axis label
    svg.selectAll(&quot;.axislabel&quot;)
      .data(featureData)
      .join(
        enter =&gt; enter.append(&quot;text&quot;)
          .attr(&quot;x&quot;, d =&gt; d.label_coord.x)
          .attr(&quot;y&quot;, d =&gt; d.label_coord.y)
          .attr(&quot;text-anchor&quot;, &quot;middle&quot;)
          .attr(&quot;dy&quot;, &quot;0.35em&quot;)
          .text(d =&gt; d.name)
      );

    //draw shapes for actual data
    let line = d3.line()
      .x(d =&gt; d.x)
      .y(d =&gt; d.y);

    let area = d3.area()
      .x(d =&gt; d.x)
      .y0(d =&gt; d.y)
      .y1(height / 2)
      .curve(d3.curveLinear);

    let colors = [&quot;red&quot;, &quot;green&quot;, &quot;navy&quot;];

    //helper function to iterate through fields in each data point
    function getPathCoordinates(data_point) {
      let coordinates = [];
      for (var i = 0; i &lt; features.length; i++) {
        let ft_name = features[i];
        let angle = (Math.PI / 2) + (2 * Math.PI * i / features.length);
        coordinates.push({
          x: width / 2 + Math.cos(angle) * radialScale(data_point[ft_name]),
          y: height / 2 - Math.sin(angle) * radialScale(data_point[ft_name])
        });
      }
      return coordinates;
    }

    // Modify the path elements to separate red and green data
    svg.selectAll(&quot;.path-red&quot;)
      .data([data[0]]) // Data for the red line
      .join(
        enter =&gt; enter.append(&quot;path&quot;)
          .datum(d =&gt; getPathCoordinates(d))
          .attr(&quot;class&quot;, &quot;path-red&quot;)
          .attr(&quot;d&quot;, line)
          .attr(&quot;stroke-width&quot;, 3)
          .attr(&quot;stroke&quot;, &quot;red&quot;)
          .attr(&quot;fill&quot;, &quot;none&quot;) // No fill for the red line
      );

    svg.selectAll(&quot;.path-green&quot;)
      .data([data[1]]) // Data for the green line
      .join(
        enter =&gt; enter.append(&quot;path&quot;)
          .datum(d =&gt; getPathCoordinates(d))
          .attr(&quot;class&quot;, &quot;path-green&quot;)
          .attr(&quot;d&quot;, line)
          .attr(&quot;stroke-width&quot;, 3)
          .attr(&quot;stroke&quot;, &quot;green&quot;)
          .attr(&quot;fill&quot;, &quot;none&quot;) // No fill for the green line
      );

    // Fill the area between the red and green lines
    let areaFillData = [
      ...getPathCoordinates(data[0]),
      ...getPathCoordinates(data[1]).reverse()
    ];

    svg.append(&quot;path&quot;)
      .datum(areaFillData)
      .attr(&quot;d&quot;, area)
      .attr(&quot;stroke-width&quot;, 0)
      .attr(&quot;fill&quot;, &quot;blue&quot;) // Fill the area between the red and green lines with blue color
      .attr(&quot;fill-opacity&quot;, 0.3); // You can adjust the opacity as you like

<!-- language: lang-html -->

&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.0/d3.min.js&quot;&gt;&lt;/script&gt;

<!-- end snippet -->

huangapple
  • 本文由 发表于 2023年7月3日 17:55:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/76603658.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定