将SVG路径d属性转换为点的数组

23

当我能够创建以下行:

var lineData = [{ "x": 50, "y": 50 }, {"x": 100,"y": 100}, {"x": 150,"y": 150}, {"x": 200, "y": 200}];
var lineFunction = d3.svg.line()
   .x(function(d) { return d.x; })
   .y(function(d) { return d.y; })
   .interpolate("basis");
var myLine = lineEnter.append("path")
   .attr("d", lineFunction(lineData))

现在我想在这个lineArray的第二个点上添加一段文字:

lineEnter.append("text").text("Yaprak").attr("y", function(d){ 
console.log(d); // This is null
console.log("MyLine");
console.log(myLine.attr("d")) // This is the string given below, unfortunately as a String
// return lineData[1].x
return 10;

} );

console.log(myLine.attr("d"))行的输出:

M50,50L58.33333333333332,58.33333333333332C66.66666666666666,66.66666666666666,83.33333333333331,83.33333333333331,99.99999999999999,99.99999999999999C116.66666666666666,116.66666666666666,133.33333333333331,133.33333333333331,150,150C166.66666666666666,166.66666666666666,183.33333333333331,183.33333333333331,191.66666666666663,191.66666666666663L200,200

我可以以字符串格式获取路径数据。 我能否将此数据转换回lineData数组? 或者,在附加文本时是否有其他简单的方法来重新生成或获取lineData?

请参考此JSFiddle

7个回答

35
SVGPathElement API提供了内置方法来获取此信息。您不需要自己解析数据字符串。
由于您将选择线路存储为变量,因此可以使用myLine.node()轻松访问路径元素的API,以引用path元素本身。
例如:
var pathElement = myLine.node();

您可以通过访问pathSegList属性来访问用于构建路径的命令列表:

var pathSegList = pathElement.pathSegList;
使用此对象的length 属性,您可以轻松地循环遍历它,以获取与每个路径段相关联的坐标:
for (var i = 0; i < pathSegList.length; i++) {
  console.log(pathSegList[i]);
}
检查控制台输出,你会发现每个路径段都有代表该段终点的xy属性。对于贝塞尔曲线、弧线等,控制点也会按需要给出x1y1x2y2
在你的情况下,无论你使用此方法还是选择自己解析字符串,都会遇到困难,因为你使用了interpolate('basis')进行线性插值。因此,线条生成器输出6个命令(在你的特定情况下),而不是4个,并且它们的端点并不总是对应于数据中的原始点。如果你使用interpolate('linear'),你将能够重构原始数据集,因为线性插值与路径数据输出具有一一对应的关系。
假设你使用了线性插值,则可以按以下方式重构原始数据集:
var pathSegList = myLine.node().pathSegList;

var restoredDataset = [];

// loop through segments, adding each endpoint to the restored dataset
for (var i = 0; i < pathSegList.length; i++) {
  restoredDataset.push({
    "x": pathSegList[i].x,
    "y": pathSegList[i].y
  })
}

编辑:

如果你想在数据后追加文本来添加标签,那么并不需要费力地重建数据。实际上,真正的问题在于你一开始从未使用数据绑定来制作折线图。尝试使用 .datum() 方法为路径绑定数据,使用 .data() 方法为标签绑定数据。此外,你可能还想重新命名 lineEnter,因为你没有使用 enter 选择器,这个变量只是表示一个分组。例如:

// THIS USED TO BE CALLED `lineEnter`
var lineGroup = svgContainer.append("g");

var myLine = lineGroup.append("path")
    // HERE IS WHERE YOU BIND THE DATA FOR THE PATH
    .datum(lineData)
    // NOW YOU SIMPLY CALL `lineFunction` AND THE BOUND DATA IS USED AUTOMATICALLY
    .attr("d", lineFunction)
    .attr("stroke", "blue")
    .attr("stroke-width", 2)
    .attr("fill", "none");

// FOR THE LABELS, CREATE AN EMPTY SELECTION
var myLabels = lineGroup.selectAll('.label')
    // FILTER THE LINE DATA SINCE YOU ONLY WANT THE SECOND POINT
    .data(lineData.filter(function(d,i) {return i === 1;})
    // APPEND A TEXT ELEMENT FOR EACH ELEMENT IN THE ENTER SELECTION
    .enter().append('text')
    // NOW YOU CAN USE THE DATA TO SET THE POSITION OF THE TEXT
    .attr('x', function(d) {return d.x;})
    .attr('y', function(d) {return d.y;})
    // FINALLY, ADD THE TEXT ITSELF
    .text('Yaprak')

5
这在大多数浏览器上还有效吗?我相当确定在 Chrome 48 上看到了 .pathSegListundefined - qwwqwwq
2
@qwwqwwq pathSegList已被弃用。请参考此答案获取替代方案:http://stackoverflow.com/a/34359059/1525769 - jshanley
5
他们弃用了它(从而破坏了所有使用它的应用程序),因为在较新的SVG标准中提议了一种新的API作为替代品。 当然,许多月过去了,Chrome仍然没有任何优化的API来获取路径段,无论是旧的还是新的。 在此处查看火焰派对:https://bugs.chromium.org/p/chromium/issues/detail?id=539385 - Tobia
请原谅我的幼稚,但是 getPathData 会返回一个点数组,对吧,MCVHZ 的东西。但是如果 SVG 路径被压缩了,例如 h 值没有 y,那么你如何找到 x/y 值呢?你需要循环遍历数组并找到最接近的前一个 y 值吗? - Marais Rossouw

12
你可以通过在字符串上分割LMC字符来将行拆分为单独的命令:
var str = "M50,50L58.33333333333332,58.33333333333332C66.66666666666666,
  66.66666666666666,83.33333333333331,83.33333333333331,
  99.99999999999999,99.99999999999999C116.66666666666666,116.66666666666666,
  133.33333333333331,133.33333333333331,150,150C166.66666666666666,
  166.66666666666666,183.33333333333331,183.33333333333331,191.66666666666663,
  191.66666666666663L200,200"

var commands = str.split(/(?=[LMC])/);

这提供了用于呈现路径的命令序列。每个命令将由一个字符(L、M或C)后跟一堆用逗号分隔的数字组成的字符串组成。它们看起来会像这样:
"C66.66666666666666,66.66666666666666,83.33333333333331,
83.33333333333331,99.99999999999999,99.99999999999999"

这描述了通过三个点[66,66]、[83,83]和[99,99]的曲线。您可以使用另一个split命令和包含在映射中的循环将它们处理成一对点的数组:

var pointArrays = commands.map(function(d){
    var pointsArray = d.slice(1, d.length).split(',');
    var pairsArray = [];
    for(var i = 0; i < pointsArray.length; i += 2){
        pairsArray.push([+pointsArray[i], +pointsArray[i+1]]);
    }
    return pairsArray;
});

这将返回一个数组,其中包含每个命令作为长度为2的数组的一部分,每个数组都是对应路径部分中点的(x,y)坐标对。
您也可以修改map函数中的函数,以返回包含命令类型和数组中点的对象。
编辑: 如果您想要访问lineData,可以将其作为数据添加到组中,然后将路径和文本附加到该组。
var group = d3.selectAll('g').data([lineData])
  .append('g');

var myLine = group.append('path')
  .attr('d', function(d){ return lineFunction(d); });

var myText = group.append('text')
  .attr('text', function(d){ return 'x = ' + d[1][0]; });

这将是一种更像d3的方式来访问数据,而不是逆向工程路径。这样可能会更容易理解。 有关SVG路径元素的更多信息

1
这将是有问题的,因为“C”命令后面的一些点是控制点,而不是实际通过该线的点。使用路径元素的pathSegList属性,您可以确定路径数据中哪些点实际上代表输出线上的点。请参见我发布的答案以获得澄清。 - jshanley
我添加了一种获取数据的方法,无需担心解析路径数据。 - ckersch
请注意,此解决方案不考虑 hv 命令,这将仅提供单个 xy 参数而不是一对。 - polarblau
请注意,当输入中的命令之间有空格时,该注释也会失败。 - MrYellow

6

pathSegList 在旧版 Chrome 中得到支持,但自 Chrome 48 版本以后被移除。
但是 Chrome 还没有实现 新的 API

使用 path seg polyfill 来使用旧版 API。

使用 path data polyfill 来使用新的 API推荐使用该方式。

var path = myLine.node();
//Be sure you have added the pathdata polyfill to your page before use getPathData
var pathdata = path.getPathData();
console.log(pathdata);
//you will get an Array object contains all path data details
//like this:
[
    {
        "type": "M",
        "values": [ 50, 50 ]
    },
    {
        "type": "L",
        "values": [ 58.33333333333332, 58.33333333333332 ]
    },
    {
        "type": "C",
        "values": [ 66.66666666666666, 66.66666666666666, 83.33333333333331, 83.33333333333331, 99.99999999999999, 99.99999999999999 ]
    },
    {
        "type": "C",
        "values": [ 116.66666666666666, 116.66666666666666, 133.33333333333331, 133.33333333333331, 150, 150 ]
    },
    {
        "type": "C",
        "values": [ 166.66666666666666, 166.66666666666666, 183.33333333333331, 183.33333333333331, 191.66666666666663, 191.66666666666663 ]
    },
    {
        "type": "L",
        "values": [ 200, 200 ]
    }
]

5
有一种比较巧妙的方法,可以使用animateMotion来沿着路径动画化一个对象(例如矩形或圆形),然后采样该对象的x/y位置。您需要做出许多选择(例如,您要将对象动画化多快,您要多快地采样x/y位置等)。您还可以多次运行此过程并取某种平均值或中位数。
完整代码(在此处查看演示:http://jsfiddle.net/mqmkc7xz/
<html>
  <body>
    <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
      <path id="mypath"
      style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
      d="m 70,67 15,0 c 0,0 -7.659111,-14.20627 -10.920116,-27.28889 -3.261005,-13.08262 9.431756,-13.85172 6.297362,-15.57166 -3.134394,-1.71994 -7.526366,-1.75636 -2.404447,-3.77842 3.016991,-1.19107 9.623655,-5.44678 0.801482,-9.67404 C 76.821958,10 70,10 70,10"
      />
    </svg>
    <div id="points"></div>
    <script>
    /**
     * Converts a path into an array of points.
     *
     * Uses animateMotion and setInterval to "steal" the points from the path.
     * It's very hacky and I have no idea how well it works.
     *
     * @param SVGPathElement  path to convert
     * @param int             approximate number of points to read
     * @param callback        gets called once the data is ready
     */
    function PathToPoints(path, resolution, onDone) {
      var ctx = {};
      ctx.resolution = resolution;
      ctx.onDone = onDone;
      ctx.points = [];
      ctx.interval = null;

      // Walk up nodes until we find the root svg node
      var svg = path;
      while (!(svg instanceof SVGSVGElement)) {
        svg = svg.parentElement;
      }
      // Create a rect, which will be used to trace the path

      var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
      ctx.rect = rect;
      svg.appendChild(rect);

      var motion = document.createElementNS("http://www.w3.org/2000/svg", "animateMotion");
      motion.setAttribute("path", path.getAttribute("d"));
      motion.setAttribute("begin", "0");
      motion.setAttribute("dur", "3"); // TODO: set this to some larger value, e.g. 10 seconds?
      motion.setAttribute("repeatCount", "1");
      motion.onbegin = PathToPoints.beginRecording.bind(this, ctx);
      motion.onend = PathToPoints.stopRecording.bind(this, ctx);

      // Add rect
      rect.appendChild(motion);
    }

    PathToPoints.beginRecording = function(ctx) {
      var m = ctx.rect.getScreenCTM();
      ctx.points.push({x: m.e, y: m.f});
      ctx.interval = setInterval(PathToPoints.recordPosition.bind(this, ctx), 1000*3/ctx.resolution);
    }

    PathToPoints.stopRecording = function(ctx) {
      clearInterval(ctx.interval);

      // Remove the rect
      ctx.rect.remove();

      ctx.onDone(ctx.points);
    }

    PathToPoints.recordPosition = function(ctx) {
      var m = ctx.rect.getScreenCTM();
      ctx.points.push({x: m.e, y: m.f});
    }
    PathToPoints(mypath, 100, function(p){points.textContent = JSON.stringify(p)});
    </script>
  </body>
</html>

4

2

1
这个库似乎不仅仅是将路径转换为点,它还有很多其他功能。附:按 y 键将提交哈希放入 URL 中,这样如果文件更改,您的链接仍将有效。 - MrYellow

0

@cuixiping的回答的基础上,getPathData()还包括一个规范化选项:

getPathData({normalize:true})将转换相对和简写命令,仅使用MLCz

因此,您不必担心高度优化/缩小的d字符串(包含相对命令、简写等)。

let pathData = path1.getPathData({ normalize: true });
let lineData = pathDataToPoints(pathData);
pointsOut.value=JSON.stringify(lineData, null, '\t')

/**
* create point array 
* from path data
**/
function pathDataToPoints(pathData) {
  let points = [];
  pathData.forEach((com) => {
    let values = com.values;
    let valuesL = values.length;
    // the last 2 coordinates represent a segments end point
    if (valuesL) {
      let p = { x: values[valuesL - 2], y: values[valuesL - 1] };
      points.push(p);
    }
  });
  return points;
}

/**
* render points from array
* just for illustration
**/
renderPoints(svg, lineData);

function renderPoints(svg, points) {
  points.forEach(point=>{
    renderPoint(svg, point);
  })
}

function renderPoint(svg, coords, fill = "red", r = "2") {
  if (Array.isArray(coords)) {
    coords = {
      x: coords[0],
      y: coords[1]
    };
  }
  let marker = `<circle cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
      <title>${coords.x} ${coords.y}</title></circle>`;
  svg.insertAdjacentHTML("beforeend", marker);
}
svg{
  width:20em;
  border:1px solid red;
  overflow:visible;
}

path{
  stroke:#000;
  stroke-width:1
}

textarea{
width:100%;
min-height:20em
}
<svg id="svg" viewBox='0 0 250 250'>
  <path id="path1" d="M50 50l8.33 8.33c8.33 8.33 25 25 41.67 41.67s33.33 33.33 50 50s33.33 33.33 41.67 41.67l8.33 8.33" stroke="#000" />
</svg>

<h3>Points</h3>
<textarea id="pointsOut"></textarea>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.4/path-data-polyfill.min.js"></script>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接