web-dev-qa-db-fra.com

Convertir l'attribut SVG Path d en un tableau de points

Quand je peux créer une ligne comme suit:

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

Maintenant, je veux ajouter un texte au deuxième point de ce 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;

});

Sortie de la ligne 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

Je peux obtenir les données de chemin au format chaîne. Puis-je reconvertir ces données en tableau lineData? Ou, existe-t-il un autre moyen simple de régénérer ou d'obtenir le lineData lors de l'ajout d'un texte?

Veuillez vous référer à ce JSFiddle .

15
ankakusu

Vous pouvez couper la ligne en commandes individuelles en divisant la chaîne sur les caractères L, M et C:

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

Cela donne la séquence de commandes qui sont utilisées pour rendre le chemin. Chacun sera une chaîne composée d'un caractère (L, M ou C) suivi d'un tas de nombres séparés par des virgules. Ils ressembleront à ceci:

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

Cela décrit une courbe en trois points, [66,66], [83,83] et [99,99]. Vous pouvez les traiter en tableaux de paires de points avec une autre commande split et une boucle, contenues dans une carte:

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

Cela renverra un tableau contenant chaque commande sous la forme d'un tableau de tableaux de longueur 2, chacun étant une paire de coordonnées (x, y) pour un point dans la partie correspondante du chemin.

Vous pouvez également modifier la fonction dans map pour renvoyer un objet contenant à la fois le type de commande et les points du tableau.

[~ # ~] modifier [~ # ~] : si vous voulez pouvoir accéder à lineData, vous pouvez l'ajouter comme données à un groupe, puis ajoutez le chemin d'accès au groupe et le texte au groupe.

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

Ce serait une manière plus d3-esque d'accéder aux données que de rétroconcevoir le chemin. Aussi probablement plus compréhensible.

Plus d'informations sur les éléments de chemin SVG

7
ckersch

L'API SVGPathElement possède des méthodes intégrées pour obtenir ces informations. Vous n'avez pas besoin d'analyser la chaîne de données vous-même.

Puisque vous avez stocké une sélection pour votre ligne sous forme de variable, vous pouvez facilement accéder à l'api de l'élément path en utilisant myLine.node() pour faire référence à l'élément path lui-même.

Par exemple:

var pathElement = myLine.node();

Ensuite, vous pouvez accéder à la liste des commandes utilisées pour construire le chemin en accédant à la propriété pathSegList:

var pathSegList = pathElement.pathSegList;

En utilisant la propriété length de cet objet, vous pouvez facilement le parcourir pour obtenir les coordonnées associées à chaque segment de chemin:

for (var i = 0; i < pathSegList.length; i++) {
  console.log(pathSegList[i]);
}

En inspectant la sortie de la console, vous constaterez que chaque segment de chemin possède des propriétés pour x et y représentant l'extrémité de ce segment. Pour les courbes de Bézier, les arcs, etc., les points de contrôle sont également indiqués comme x1, y1, x2 Et y2 Selon les besoins.

Dans votre cas, que vous utilisiez cette méthode ou que vous choisissiez d'analyser la chaîne vous-même, vous rencontrerez des difficultés car vous avez utilisé interpolate('basis') pour votre interpolation de ligne. Par conséquent, le générateur de ligne génère 6 commandes (dans votre cas spécifique) plutôt que 4, et leurs points d'extrémité ne correspondent pas toujours aux points d'origine dans les données. Si vous utilisez interpolate('linear') vous pourrez reconstruire le jeu de données d'origine, car l'interpolation linéaire a une correspondance un à un avec la sortie des données de chemin.

En supposant que vous avez utilisé une interpolation linéaire, la reconstruction de l'ensemble de données d'origine pourrait se faire comme suit:

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

MODIFIER:

En ce qui concerne l'utilisation des données d'origine lors de l'ajout de texte ... Je suppose que vous cherchez à ajouter des étiquettes aux points, il n'est pas nécessaire de passer par tous les problèmes de reconstruction des données. En fait, le vrai problème est que vous n'avez jamais utilisé la liaison de données en premier lieu pour créer votre graphique linéaire. Essayez de lier les données à l'aide de la méthode .datum() pour votre chemin d'accès et à l'aide de la méthode .data() pour les étiquettes. Vous pouvez également renommer lineEnter car vous n'utilisez pas de sélection d'entrée et qu'elle représente simplement un groupe. Par exemple:

// 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')
24
jshanley

pathSegList est pris en charge dans l'ancien Chrome et est supprimé depuis Chrome 48.
Mais Chrome n'a pas implémenté la nouvelle API .

Utilisez polyfill segment seg pour travailler avec l'ancienne API.

Utilisez polyfill de données de chemin pour travailler avec nouvelle API . C'est recommandé.

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 ]
    }
]
3
cuixiping

J'ai trouvé cette question par Google. Ce dont j'avais besoin était simplement la propriété pathSegList d'un objet SVG path:

var points = pathElement.pathSegList;

Chaque point ressemble

y: 57, x: 109, pathSegTypeAsLetter: "L", pathSegType: 4, PATHSEG_UNKNOWN: 0…}

Voir

2
Martin Thoma

Un peu hacky, mais vous pouvez utiliser animateMotion pour animer un objet (par exemple un rect ou un cercle) le long du chemin, puis échantillonner la position x/y de l'objet. Vous devrez faire un tas de choix (par exemple, à quelle vitesse animez-vous l'objet, à quelle vitesse échantillonnez-vous la position x/y, etc.). Vous pouvez également exécuter ce processus plusieurs fois et prendre une sorte de moyenne ou de médiane.

Code complet (voyez-le en action: 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>
2
Alok

J'ai utilisé cela avec succès pour rendre une liste de points x, y:

https://shinao.github.io/PathToPoints/

Le code est plus que ce que je pourrais intégrer dans cette zone de texte, mais voici probablement un bon début: https://github.com/Shinao/PathToPoints/blob/master/js/pathtopoints.js#L209

0
Eran W