ankakusu ankakusu - 7 months ago 543
Javascript Question

Convert SVG Path d attribute to a array of points

When I can create a line as follows:

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))


Now I want to add a text to the second point of this 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;


} );

Output of the line
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


I can get the path data in string format. Can I convert this data back to lineData array? Or, is there any other and simple way to regenerate or get the lineData when appending a text?

Please refer to this JSFiddle.

Answer

You can break the line into individual commands by splitting the string on the L, M, and C characters:

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])/);

This gives the sequence of commands that are used to render the path. Each will be a string comprised of a character (L, M, or C) followed by a bunch of numbers separated by commas. They will look something like this:

"C66.66666666666666,66.66666666666666,83.33333333333331,
83.33333333333331,99.99999999999999,99.99999999999999"

That describes a curve through three points, [66,66], [83,83], and [99,99]. You can process these into arrays of pairs points with another split command and a loop, contained in a map:

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;
});

This will return an array containing each command as an array of length-2 arrays, each of which is an (x,y) coordinate pair for a point in the corresponding part of the path.

You could also modify the function in map to return object that contain both the command type and the points in the array.

EDIT: If you want to be able to access lineData, you can add it as data to a group, and then append the path to the group, and the text to the group.

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]; });

This would be a more d3-esque way of accessing the data than reverse-engineering the path. Also probably more understandable.

More info on SVG path elements