【问题标题】:How to plot degree on spiral chart using d3.js如何使用 d3.js 在螺旋图上绘制度数
【发布时间】:2021-02-02 02:54:43
【问题描述】:

我有一个正在运行的 Angular 9 应用程序,其中 SVG 有一个螺旋图,其中包含度数的最小值和最大值。 我正在使用 d3.js 在螺旋图上绘制给定的度数。 我写了以下代码:

// min -> min degree, -140 in this example
// max -> max degree, 440 in this example
// currentDegree -> degree value to be ploted, 0 in this example
// svg -> svg containing spiral chart
// circle -> circle to be moved to depict current Degree position in the svg 

void setDegree(min,max,currentDegree, svg, circle) {

    const pathNode = svg.select('path').node();
    const totalPathLength = pathNode.getTotalLength();

    const yDomain = d3.scale.linear().domain([min, max]).range(
        [0, totalPathLength]); 

    const currentPathLength = yDomain(currentDegree); // current path length
    const pathPoint =  pathNode.getPointAtLength(totalPathLength - currentPathLength);

    circle.transition()
        .duration(300)
        .attrTween('cx', () => (t) => pathPoint.x)
        .attrTween('cy', () => (t) => pathPoint.y);
}

上面的代码产生这个输出:

在上图中,0 度略微向右移动,但它应该位于中心,如下图所示:

function setDegree(min, max, currentDegree, svg, circle) {
  const pathNode = svg.select("path").node();
  const totalPathLength = pathNode.getTotalLength();

  const yDomain = d3
    .scaleLinear()
    .domain([min, max])
    .range([0, totalPathLength]);

  const currentPathLength = yDomain(currentDegree); // current path length
  const pathPoint = pathNode.getPointAtLength(
    totalPathLength - currentPathLength
  );

  circle
    .transition()
    .duration(300)
    .attrTween("cx", () => t => pathPoint.x)
    .attrTween("cy", () => t => pathPoint.y);
}

const svg = d3.select("svg");
const circle = d3.select("#cur_pos");
setDegree(-140, 410, 0, svg, circle);
p {
  font-family: Lato;
}

.cls-3 {
  fill: none;
  stroke-width: 10px;
  stroke: #000;
}

.cls-3,
.cls-4,
.cls-5 {
  stroke-miterlimit: 10;
}

.cls-4,
.cls-5 {
  stroke-width: 0.25px;
}

.cls-5 {
  font-size: 60px;
  font-family: ArialMT, Arial;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1031.1 1010.3" preserveAspectRatio="xMinYMin meet">
  <title>Spiral Chart</title>

  <g>
    <g id="Spiral_Chart">
      <g id="Spiral_Path">
        <path
          class="cls-3 svg-range-line"
          d="M881.5,154.9C679.6-47,352.3-47,150.4,154.9c-195.9,195.9-195.9,513.4,0,709.3,189.7,190,497.5,190.1,687.5.4l.4-.4c184.3-184.3,184.3-483,0-667.3h0C659.6,18.1,369.8,18.1,191.1,196.8H191C17.6,370.3,17.6,651.4,191,824.8"
        />
      </g>
      <circle
        id="cur_pos"
        class="cls-4 svg-range-indicator"
        cx="514"
        cy="64.3"
        r="18.5"
      />
    </g>
    <text
      id="Min"
      class="cls-5 svg-text"
      style="text-anchor:start;"
      x="195"
      y="880"
    >
      -140
    </text>
    <text
      id="Max"
      class="cls-5 svg-text"
      style="text-anchor:start;"
      x="885"
      y="210"
    >
      410
    </text>
  </g>
</svg>

【问题讨论】:

  • 你已经让你的范围等于路径的长度,这意味着如果路径不小心出错了,那么零点也会在错误的位置。您能否将您的代码更改为minimal reproducible example,特别是可运行的sn-p?可以在这里,或者如果你愿意,可以通过 jsfiddle/codepen/observablehq。它将帮助您获得更好的答案
  • 在此处添加运行示例:stackblitz.com/edit/angular-ivy-scb3s9

标签: javascript angular d3.js


【解决方案1】:

因为螺旋线的内侧较小,所以如果计算 0 度(或 90 或 -90 度)的长度,就会超出它。那是因为路径的总长度包括了螺旋的外侧部分,它更长,因为它的半径更大。换句话说,如果路径完全是循环的,那么您的逻辑是正确的。但这并不是让你偏离了一点点。

请注意,如果您将currentDegree 更改为 360,它几乎是完美放置的。这又是因为这个半径。


我用过这个很棒的包kld-intersections,它可以计算两个SVG形状的交点。

我首先取圆的中点,然后沿着我想要圆的方向计算一些很长的线。我计算了路径与那条线的交点,然后我得到了一个交点数组。

现在,要知道是使用最近的还是最远的交点,我按到中心的距离对它们进行排序,并检查 360 度在最小角度和所需角度之间拟合的次数。

注意圆心不是完美的,这就是为什么如果你把它改成-140,圆就不会在准确的结束位置。也许您可以对此进行改进,或者 - 如果设计稳定,请手动计算点。

const {
  ShapeInfo,
  Intersection
} = KldIntersections;

function getCentroid(node) {
  const bbox = node.getBBox();
  return {
    x: bbox.x + bbox.width / 2,
    y: bbox.y + bbox.height / 2,
  };
}

function setDegree(min, max, currentDegree, svg, circle) {
  const pathNode = svg.select("path").node();
  const centroid = getCentroid(pathNode);

  const pathInfo = ShapeInfo.path(pathNode.getAttribute("d"));
  // We need to draw a line from the centroid, at the angle we want the
  // circle to have.
  const currentRadian = (currentDegree / 180) * Math.PI - Math.PI / 2;
  const lineEnd = {
    // HACK: small offset so the line is never completely vertical
    x: centroid.x + 1000 * Math.cos(currentRadian) + Math.random() * 0.01,
    y: centroid.y + 1000 * Math.sin(currentRadian),
  };

  indicatorLine
    .attr("x1", centroid.x)
    .attr("y1", centroid.y)
    .attr("x2", lineEnd.x)
    .attr("y2", lineEnd.y);
  const line = ShapeInfo.line([centroid.x, centroid.y], [lineEnd.x, lineEnd.y]);
  const intersections = Intersection.intersect(pathInfo, line).points;

  // Sort the points based on their distance to the centroid
  intersections.forEach(
    p => p.dist = Math.sqrt((p.x - centroid.x) ** 2 + (p.y - centroid.y) ** 2));
  intersections.sort((a, b) => a.dist - b.dist);

  // See which intersection we need.
  // Iteratively go round the circle until we find the correct one
  let i = 0;
  while (min + 360 * (i + 1) <= currentDegree) {
    i++;
  }

  const pathPoint = intersections[i];

  circle
    .attr("cx", pathPoint.x)
    .attr("cy", pathPoint.y);
}

const svg = d3.select("svg");
const indicatorLine = svg.append("line").attr("stroke", "red");
const circle = d3.select("#cur_pos");

setDegree(-140, 410, 0, svg, circle);
d3.select("input").on("change", function() {
  setDegree(-140, 410, +this.value, svg, circle);
});
p {
  font-family: Lato;
}

.cls-3 {
  fill: none;
  stroke-width: 10px;
  stroke: #000;
}

.cls-3,
.cls-4,
.cls-5 {
  stroke-miterlimit: 10;
}

.cls-4,
.cls-5 {
  stroke-width: 0.25px;
}

.cls-5 {
  font-size: 60px;
  font-family: ArialMT, Arial;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://unpkg.com/kld-intersections"></script>

<label>Value</label> <input type="number" value="0" min="-140" max="410"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1031.1 1010.3" preserveAspectRatio="xMinYMin meet">
  <g>
    <g id="Spiral_Chart">
      <g id="Spiral_Path">
        <path
          class="cls-3 svg-range-line"
          d="M881.5,154.9C679.6-47,352.3-47,150.4,154.9c-195.9,195.9-195.9,513.4,0,709.3,189.7,190,497.5,190.1,687.5.4l.4-.4c184.3-184.3,184.3-483,0-667.3h0C659.6,18.1,369.8,18.1,191.1,196.8H191C17.6,370.3,17.6,651.4,191,824.8"
        />
      </g>
      <circle
        id="cur_pos"
        class="cls-4 svg-range-indicator"
        cx="514"
        cy="64.3"
        r="18.5"
      />
    </g>
    <text
      id="Min"
      class="cls-5 svg-text"
      style="text-anchor:start;"
      x="195"
      y="880"
    >
      -140
    </text>
    <text
      id="Max"
      class="cls-5 svg-text"
      style="text-anchor:start;"
      x="885"
      y="210"
    >
      410
    </text>
  </g>
</svg>

【讨论】:

  • 能否在不使用任何第三方包(如 kld-intersections)的情况下实现这一点?有什么方法可以只使用纯 d3.js 来实现这一点吗?
  • 也许如果你使用一些二分搜索算法并查看形状是否重叠,但这很棘手。我不知道有什么办法,恐怕
  • 我尝试了上面的解决方案,它对我有用,我得到零交叉点,从而导致未定义的错误。你有这个运行的例子吗?可能在 stackblitz 或其他地方?
  • 我已将答案更新为与数字输入交互。它在 -140、220 和 410 附近表现得有些奇怪,因为质心不正确。您应该能够在用于创建螺旋的设计程序中找出正确的质心。希望有帮助
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-28
  • 1970-01-01
  • 2017-10-09
  • 2015-01-13
  • 2011-10-13
  • 2013-03-08
  • 1970-01-01
相关资源
最近更新 更多