Преобразование атрибута SVG Path d в массив точек
Когда я могу создать строку следующим образом:
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))
Теперь я хочу добавить текст ко второй точке этой строкиArray:
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.
Ответы
Ответ 1
Вы можете разбить строку на отдельные команды, разделив строку на символы L
, M
и 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])/);
Это дает последовательность команд, которые используются для отображения пути. Каждая строка будет состоять из символа (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-esque способ доступа к данным, чем обратное проектирование пути. Также возможно более понятным.
Дополнительная информация о элементах пути SVG
Ответ 2
SVGPathElement
API имеет встроенные методы для получения этой информации. Вам не нужно самостоятельно разбирать строку данных.
Поскольку вы сохранили выделение для своей строки в качестве переменной, вы можете легко получить доступ к элементу пути api, используя myLine.node()
, чтобы обратиться к самому элементу path
.
Например:
var pathElement = myLine.node();
Затем вы можете получить доступ к списку команд, используемых для создания пути, путем доступа к свойству pathSegList
:
var pathSegList = pathElement.pathSegList;
Используя свойство length
этого объекта, вы можете легко пропустить его, чтобы получить координаты, связанные с каждым сегментом пути:
for (var i = 0; i < pathSegList.length; i++) {
console.log(pathSegList[i]);
}
Проверяя вывод консоли, вы обнаружите, что каждый сегмент пути имеет свойства для x
и y
, представляющие конечную точку этого сегмента. Для кривых Безье, дуг и т.п. Контрольные точки также задаются как x1
, y1
, x2
и y2
по мере необходимости.
В вашем случае, независимо от того, используете ли вы этот метод или сами разбираете строку, вы столкнетесь с трудностями, потому что вы использовали 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
})
}
EDIT:
Что касается использования исходных данных при добавлении текста... Я предполагаю, что вы хотите добавить метки к точкам, нет необходимости проходить через все проблемы с восстановлением данных. На самом деле реальная проблема заключается в том, что вы никогда не использовали привязку данных, чтобы сделать свой линейный график. Попробуйте привязать данные с помощью метода .datum()
для вашего пути и с помощью метода .data()
для меток. Также вы можете переименовать lineEnter
, так как вы не используете выбор ввода и просто представляете группу. Например:
// 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')
Ответ 3
Немного взломанный, но вы можете использовать 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 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
Я нашел этот вопрос у Google. Мне нужно было просто свойство pathSegList
объекта SVG path
:
var points = pathElement.pathSegList;
Каждая точка выглядит как
y: 57, x: 109, pathSegTypeAsLetter: "L", pathSegType: 4, PATHSEG_UNKNOWN: 0…}
См.
Ответ 5
pathSegList
поддерживается в старых Chrome и удаляется с Chrome 48.
Но Chrome не реализовал новый новый API.
Используйте путь seg polyfill для работы со старым API.
Используйте данные о пути polyfill для работы с new 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 ]
}
]