Convert SVG Path d attribute to a array of points - javascript

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.

The SVGPathElement API has built-in methods for getting this info. You do not need to parse the data-string yourself.
Since you stored a selection for your line as a variable, you can easily access the path element's api using myLine.node() to refer to the path element itself.
For example:
var pathElement = myLine.node();
Then you can access the list of commands used to construct the path by accessing the pathSegList property:
var pathSegList = pathElement.pathSegList;
Using the length property of this object, you can easily loop through it to get the coordinates associated with each path segment:
for (var i = 0; i < pathSegList.length; i++) {
console.log(pathSegList[i]);
}
Inspecting the console output, you will find that each path segment has properties for x and y representing the endpoint of that segment. For bezier curves, arcs, and the like, the control points are also given as x1, y1, x2, and y2 as necessary.
In your case, regardless of whether you use this method or choose to parse the string yourself, you will run into difficulties because you used interpolate('basis') for your line interpolation. Therefore, the line generator outputs 6 commands (in your specific case) rather than 4, and their endpoints do not always correspond to the original points in the data. If you use interpolate('linear') you will be able to reconstruct the original dataset, since the linear interpolation has a one-to-one correspondence with the path data output.
Assuming you used linear interpolation, reconstructing the original dataset could be done as follows:
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:
As far as using the original data when appending text... I'm assuming you are looking to append labels to the points, there's no need to go through all the trouble of reconstructing the data. In fact the real issue is that you never used data-binding in the first place to make your line graph. Try binding the data using the .datum() method for your path, and using the .data() method for the labels. Also you might want to rename lineEnter since you're not using an enter selection and it simply represents a group. For example:
// 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')

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

A little hacky, but you can use animateMotion to animate an object (e.g. a rect or a circle) along the path and then sample the x/y position of the object. You will have to make a bunch of choices (e.g. how fast do you animated the object, how fast do you sample the x/y position, etc.). You could also run this process multiple times and take some kind of average or median.
Full code (see it in 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>

pathSegList is supported in old Chrome and is removed since Chrome 48.
But Chrome has not implemented the new API.
Use path seg polyfill to work with old API.
Use path data polyfill to work with new API. It's recommended.
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 ]
}
]

I found this question by Google. What I needed was simply the pathSegList property of a SVG path object:
var points = pathElement.pathSegList;
Every point looks like
y: 57, x: 109, pathSegTypeAsLetter: "L", pathSegType: 4, PATHSEG_UNKNOWN: 0…}
See
www.w3.org/TR/SVG/paths
https://msdn.microsoft.com/en-us/library/ie/ff971976(v=vs.85).aspx

I've successfully used this to render a list of x,y points:
https://shinao.github.io/PathToPoints/
Code is more than what I could fit into this textbox, but here is probably a good start:
https://github.com/Shinao/PathToPoints/blob/master/js/pathtopoints.js#L209

Expanding on #cuixiping answer: getPathData() also includes a normalization option:
getPathData({normalize:true}) that will convert
relative and shorthand commands to use only M, L, C and z.
So you don't have to worry about highly optimized/minified d strings (containing relative commands, shorthands etc).
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>

Related

d3 how to make single stacked column chart

I'm trying to get d3 to draw a single stacked bar chart, like so:
<svg width = '500' height= '100'>
<rect x='0' y='0' width='224' height='30' fill='green'/>
<rect x='224' y='0' width='84' height='30' fill='blue'/>
<rect x='308' y='0' width='29' height='30' fill='darkgray'/>
<rect x='337' y='0' width='3' height='30' fill='#606060'/>
</svg>
As you can see, the x position starts at zero, then each subsequent x position is equal to the sum of the preceding widths.
So I'm trying to get d3 to draw something like that from an array called datavars:
var datavars = [224, 84, 29, 3];
... and to ensure that d3 assigns the correct width-value to the correct x-value, I created these variables:
var prev_width0 = 0;
var prev_width1 = datavars[0];
var prev_width2 = datavars[0] + datavars[1];
var prev_width3 = datavars[0] + datavars[1] + datavars[2];
... and define the x value like so:
//create SVG element
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h);
svg.selectAll('rect')
.data(datavars)
.enter()
.append('rect')
.attr('width', function(d){
return d;})
.attr('x',function(d, i){
return 'prev_width'+i; })
.attr('y',0)
.attr('height', 30);
As you might have guessed, the strongly-indented function (based on the sum of the preceding widths, and defined in the prev_width variables) returns a string for each iteration (prev_width0, prev_width1, etc.) and not the values I thought I defined when I created the prev_width variables.
I'm obviously defining the variables incorrectly. Any idea how I can do this properly?
JavaScript does not interpret the string "datavars1" as the variable datavars1. To interpret a string as a JavaScript variable you can use eval(). So change: return 'prev_width'+i; }) to return eval('prev_width'+i); }). Although a better idea might be to use an array instead such as:
var prev_width = [0,
datavars[0],
datavars[0] + datavars[1],
datavars[0] + datavars[1] + datavars[2]
];
...
.attr('x',function(d, i){
return prev_width[i]; })
Fiddle Example
You can do a similar thing with the colors:
var colors = ['green','blue','darkgray','#606060'];
...
.attr('fill', function(d, i){ return colors[i]; })
Bar chart with color
You can also create a single array of objects to store the color, width, etc.
A more scalable idea would be to get rid of the array prev_width and have a function that can take the sum up to a point. Such as:
function sum(array, start, end) {
var total = 0;
for(var i=start; i<end; i++) total += array[i];
return total;
}
Then you can do:
...
.attr('x',function(d, i){
return sum(datavars, 0, i); })
Example using summation
here is a snippet (approached a little differently by me)
//this function deals with pairing finding x for each width in the array and add them to a new array called x_and_width_values
//example :[[0,224],[84,224],[29,308],[3,337]]
// ^ ^ ^ ^ ^ ^ ^ ^
// x width x width .............
function combinations(prev,current,index,arr){
// This method focus on accummulating results by reference previous and current element
//for the first and second element we need specifically add this
if(index==1){
x_and_width_values.push([arr[index-1],0]);
x_and_width_values.push([arr[index],prev])
}
//other elements we use index as follows;
else{
x_and_width_values.push([arr[index],prev])
}
return prev+current
}
//after creating an array with all [x,width] combination, we map it to colors to get out final array
//example :[[[0,224],'green'],[[84,224],'blue'],[29,308],[[3,337],'darkgray']
function colors_with_combination(e, i) {
return [x_and_width_values[i], colors[i]];
}
//*******************acutal beef og code***************
var x_and_width_values=[];
//this link is needed to creation of svg elements
var link="http://www.w3.org/2000/svg"
//all your widths
var all_widths=[224,84,29,3];
//all your colors
var colors=['green','blue','darkgray','#606060'];
//sort your width array to be on the safe side
all_widths.sort(function(a,b){return b-a});
//find all width and x combination
all_widths.reduce(combinations);
//map width and x values to color values
var all = x_and_width_values.map(colors_with_combination);
//create a loop for object creation
for(var i=0;i<all.length;++i){
var rect=document.createElementNS(link,'rect');
rect.setAttributeNS(null,'x',all[i][0][1])
rect.setAttributeNS(null,'width',all[i][0][0]);
rect.setAttributeNS(null,'fill',all[i][1]);
rect.setAttributeNS(null,'height','30');
rect.setAttributeNS(null,'y','0');
document.getElementById('svg').appendChild(rect);;
}
<svg id='svg' width = '500' height= '100'>
</svg>

How to use series.stack = false in dimple.js to suppress aggregation; different symbols for each series

I am trying to suppress aggregation of data for given months using dimple.js' s3.stacked = false toggle, but it won't do so. How to do aggregation suppression properly in dimple.js? The code below should not add up the profit for 05/2013, at least that is what I want to achieve.
I tried to use "Profit" in s3 = chart.addSeries("Profit in each unit", dimple.plot.bubble, [x,y3]); instead of "Profit in each unit" and then there is no aggregation, but the bubbles change colors across the s3 series. How to achieve one color across the "Profit in each unit" bubbles and no aggregation?
Also, is it possible to use different symbols for each series, like squares, diamonds, etc. besides bubbles?
var svg = dimple.newSvg("body", 800, 400);
var data = [
{"Month":"01/2013", "Revenue":2000, "Profit":2000, "Units":4},
{"Month":"02/2013", "Revenue":3201, "Profit":2000, "Units":3},
{"Month":"03/2013", "Revenue":1940, "Profit":14000, "Units":5},
{"Month":"04/2013", "Revenue":2500, "Profit":3200, "Units":1},
{"Month":"05/2013", "Revenue":800, "Profit":1200, "Units":4},
{"Month":"05/2013", "Revenue":800, "Profit":5300, "Units":4}
];
var chart = new dimple.chart(svg, data);
chart.setBounds(60,20,680,330);
var x = chart.addCategoryAxis("x", "Month");
var y1 = chart.addMeasureAxis("y", "Revenue");
var y2 = chart.addMeasureAxis("y", "Units");
var y3 = chart.addMeasureAxis(y1, "Profit");
chart.addSeries("Sales Units", dimple.plot.bubble, [x,y2]);
chart.addSeries("Sales Revenue", dimple.plot.bubble, [x,y1]);
s3 = chart.addSeries("Profit in each unit", dimple.plot.bubble, [x,y3]);
s3.stacked = false;
chart.assignColor("Sales Units", "white", "red", 0.5);
chart.assignColor("Sales Revenue", "#FF00FF");
chart.assignColor("Profit in each unit", "green");
x.dateParseFormat = "%m/%Y";
x.addOrderRule("Date");
chart.draw();
You were close to the answer, you do need to specify the profit in the first parameter of addSeries but that parameter can take an array and only the last element of the array defines colour, so you can include your label there:
s3 = chart.addSeries(["Profit", "Profit in each unit"], dimple.plot.bubble, [x,y3]);
The stacked property is about positioning of the elements rather than aggregation of the dataset, so won't help you here.
Dimple doesn't support other shapes with its provided plot methods, however you could make your own and provide it to the second parameter of addSeries. To do a complete series plotter you should take a copy of the bubble plot source (https://github.com/PMSI-AlignAlytics/dimple/blob/master/src/objects/plot/bubble.js) and modify it to draw whatever you like. However this can get tricky.
If you don't care about certain features (e.g. tooltips, animation, repeated draws etc), you can cut them out and it reduces the code to something very minimal. Here's the most basic plotter for drawing a star:
var myCustomPlotter = {
stacked: false,
grouped: false,
supportedAxes: ["x", "y"],
draw: function (chart, series, duration) {
chart._group
.selectAll(".my-series")
.data(series._positionData)
.enter()
.append("path")
// Path Generated at http://www.smiffysplace.com/stars.html
.attr("d", "M 0 10 L 11.756 16.180 L 9.511 3.090 L 19.021 -6.180 L 5.878 -8.090 L 0 -20 L -5.878 -8.090 L -19.021 -6.180 L -9.511 3.090 L -11.756 16.180 L 0 10")
.attr("transform", function (d) {
return "translate(" +
dimple._helpers.cx(d, chart, series) + "," +
dimple._helpers.cy(d, chart, series) + ")";
})
.style("fill", "yellow")
.style("stroke", "orange");
}
};
http://jsbin.com/mafegu/6/edit?js,output

SVG - Follow rotated path

I'm trying to have a circle follow an elliptical path that is rotated, using Snap.svg, but I've ran into a problem.
Using the getPointAtLength only returns the point of the path that is not rotated, so when I rotate the path, the circle will still follow the original path, before rotation.
var firstOrbit = s.path("M250,400a150,75 0 1,0 300,0a150,75 0 1,0 -300,0")
.attr({stroke: "#000", strokeWidth:"5", fill: "transparent"});
var len = firstOrbit.getTotalLength();
firstOrbit = firstOrbit.transform('r75,400,400');
var circleA = s.circle(0,0,10);
var startingPointA = Snap.path.getPointAtLength(firstOrbit, 0);
Snap.animate(0, len, function(value){
var movePoint = firstOrbit.getPointAtLength(value);
circleA.attr({ cx: movePoint.x, cy: movePoint.y });
}, 2000, mina.linear);
The problem is, that the ellipse rotates, but the path that circleA follows does not, so it follows a wrong path. Is there an easy way to fix this, or do I have to calculate the difference at each step of the animation?
You have two possible solutions for this issue. Either you will have to put the <path> and <circle> inside a <g> group and apply transformations to the group Or find actual point on path and then find the transformed point by applying the transformation matrix.
Solution 1
var len = firstOrbit.getTotalLength();
var circleA = s.circle(0,0,10);
var group = s.g(firstOrbit,circleA);
group = group.transform('r75,400,400');
JSFiddle
Solution 2
var pt = s.node.createSVGPoint();
Snap.animate(0, len, function(value){
var movePoint = firstOrbit.getPointAtLength(value);
pt.x = movePoint.x;
pt.y = movePoint.y;
movePoint = pt.matrixTransform(firstOrbit.node.getCTM());
circleA.attr({ cx: movePoint.x, cy: movePoint.y });
}, 2000, mina.linear);
JSFiddle
Put both the circle and the path inside a group. Apply the transform/rotation to the group (instead of the path) so that both the circle and the path inherit the same transform.
.

Combining Parent and Nested Data with d3.js

I have a data structure like this (assume that the data structure is non-negotiable):
data = {
segments : [
{x : 20, size : 10, colors : ['#ff0000','#00ff00']},
{x : 40, size : 20, colors : ['#0000ff','#000000']}
]};
Using the d3.js javascript library, I'd like to draw four rectangles, one for each color in both colors arrays. Information from each entry in the segments array is used to draw the rectangles corresponding to each color in its color array. E.g., The red and green rectangles will have a width and height of 10. The resulting html should look like this:
<div id="container">
<svg width="200" height="200">
<g>
<rect x="20" y="20" width="10" height="10" fill="#ff0000"></rect>
<rect x="30" y="30" width="10" height="10" fill="#00ff00"></rect>
</g>
<g>
<rect x="40" y="40" width="20" height="20" fill="#0000ff"></rect>
<rect x="60" y="60" width="20" height="20" fill="#000000"></rect>
</g>
</svg>
</div>
I've come up with some code that accomplishes this, but I found the part about using data from two different levels of nesting in data to be confusing, and I feel that there might be a more idiomatic way to accomplish the same with d3.js. Here's the code (full example at http://jsbin.com/welcome/39650/edit):
function pos(d,i) { return d.x + (i * d.size); } // rect position
function size(d,i) { return d.size; } // rect size
function f(d,i) { return d.color; } // rect color
// add the top-level svg element and size it
vis = d3
.select('#container')
.append('svg')
.attr('width',200)
.attr('height',200);
// add the nested svg elements
var nested = vis
.selectAll('g')
.data(data.segments)
.enter()
.append('g');
// Add a rectangle for each color
nested
.selectAll('rect')
.data(function(d) {
// **** ATTENTION ****
// Is there a more idiomatic, d3-ish way to approach this?
var expanded = [];
for(var i = 0; i < d.colors.length; i++) {
expanded.push({
color : d.colors[i],
x : d.x
size : d.size });
}
return expanded;
})
.enter()
.append('rect')
.attr('x',pos)
.attr('y',pos)
.attr('width',size)
.attr('height',size)
.attr('fill',f);
Is there a better and/or more idiomatic way to access data from two different levels of nesting in a data structure using d3.js?
Edit
Here's the solution I came up with, thanks to meetamit's answer for the closure idea, and using more idiomatic d3.js indentation thanks to nautat's answer:
$(function() {
var
vis = null,
width = 200,
height = 200,
data = {
segments : [
{x : 20, y : 0, size : 10, colors : ['#ff0000','#00ff00']},
{x : 40, y : 0, size : 20, colors : ['#0000ff','#000000']}
]
};
// set the color
function f(d,i) {return d;}
// set the position
function pos(segment) {
return function(d,i) {
return segment.x + (i * segment.size);
};
}
// set the size
function size(segment) {
return function() {
return segment.size;
};
}
// add the top-level svg element and size it
vis = d3.select('#container').append('svg')
.attr('width',width)
.attr('height',height);
// add the nested svg elements
var nested = vis
.selectAll('g')
.data(data.segments)
.enter().append('g');
// Add a rectangle for each color. Size of rectangles is determined
// by the "parent" data object.
nested
.each(function(segment, i) {
var
ps = pos(segment),
sz = size(segment);
var colors = d3.select(this)
.selectAll('rect')
.data(segment.colors)
.enter().append('rect')
.attr('x', ps)
.attr('y',ps)
.attr('width', sz)
.attr('height',sz)
.attr('fill', f);
});
});
Here's the full working example: http://jsbin.com/welcome/42885/edit
You can use closures
var nested = vis
.selectAll('g')
.data(data.segments);
nested.enter()
.append('g')
.each(function(segment, i) {
var colors = d3.select(this)
.selectAll('rect')
.data(segment.colors);
colors.enter()
.append('rect')
.attr('x', function(color, j) { return pos(segment, j); })
// OR: .attr('x', function(color, j) { return segment.x + (j * segment.size); })
.attr('width', function(color, j) { return size(segment); })
.attr('fill', String);
});
You could do something like the following to restructure your data:
newdata = data.segments.map(function(s) {
return s.colors.map(function(d) {
var o = this; // clone 'this' in some manner, for example:
o = ["x", "size"].reduce(function(obj, k) { return(obj[k] = o[k], obj); }, {});
return (o.color = d, o);
}, s);
});
This will transform your input data into:
// newdata:
[
[
{"size":10,"x":20,"color":"#ff0000"},
{"size":10,"x":20,"color":"#00ff00"}],
[
{"size":20,"x":40,"color":"#0000ff"},
{"size":20,"x":40,"color":"#000000"}
]
]
which then can be used in the standard nested data selection pattern:
var nested = vis.selectAll('g')
.data(newdata)
.enter().append('g');
nested.selectAll('rect')
.data(function(d) { return d; })
.enter().append('rect')
.attr('x',pos)
.attr('y',pos)
.attr('width',size)
.attr('height',size)
.attr('fill',f);
BTW, if you'd like to be more d3-idiomatic, I would change the indentation style a bit for the chained methods. Mike proposed to use half indentation every time the selection changes. This helps to make it very clear what selection you are working on. For example in the last code; the variable nested refers to the enter() selection. See the 'selections' chapter in: http://bost.ocks.org/mike/d3/workshop/
I would try to flatten the colors before you actually start creating the elements. If changes to the data occur I would then update this flattened data structure and redraw. The flattened data needs to be stored somewhere to make real d3 transitions possible.
Here is a longer example that worked for me. Yon can see it in action here.
Here is the code:
var data = {
segments : [
{x : 20, size : 10, colors : ['#ff0000','#00ff00']},
{x : 40, size : 20, colors : ['#0000ff','#000000']}
]
};
function pos(d,i) { return d.x + (i * d.size); } // rect position
function size(d,i) { return d.size; } // rect size
function f(d,i) { return d.color; } // rect color
function flatten(data) {
// converts the .colors to a ._colors list
data.segments.forEach( function(s,i) {
var list = s._colors = s._colors || [];
s.colors.forEach( function(c,j) {
var obj = list[j] = list[j] || {}
obj.color = c
obj.x = s.x
obj.size = s.size
});
});
}
function changeRect(chain) {
return chain
.transition()
.attr('x',pos)
.attr('y',pos)
.attr('width',size)
.attr('height',size)
.attr('fill',f)
.style('fill-opacity', 0.5)
}
vis = d3
.select('#container')
.append('svg')
.attr('width',200)
.attr('height',200);
// add the top-level svg element and size it
function update(){
flatten(data);
// add the nested svg elements
var all = vis.selectAll('g')
.data(data.segments)
all.enter().append('g');
all.exit().remove();
// Add a rectangle for each color
var rect = all.selectAll('rect')
.data(function (d) { return d._colors; }, function(d){return d.color;})
changeRect( rect.enter().append('rect') )
changeRect( rect )
rect.exit().remove()
}
function changeLater(time) {
setTimeout(function(){
var ds = data.segments
ds[0].x = 10 + Math.random() * 100;
ds[0].size = 10 + Math.random() * 100;
ds[1].x = 10 + Math.random() * 100;
ds[1].size = 10 + Math.random() * 100;
if(time == 500) ds[0].colors.push("orange")
if(time == 1000) ds[1].colors.push("purple")
if(time == 1500) ds[1].colors.push("yellow")
update()
}, time)
}
update()
changeLater(500)
changeLater(1000)
changeLater(1500)
Important here is the flatten function which does the data conversion and stores/reuses the result as _colors property in the parent data element. Another important line is;
.data(function (d) { return d._colors; }, function(d){return d.color;})
which specifies where to get the data (first parameter) AND what the unique id for each data element is (second parameter). This helps identifying existing colors for transitions, etc.

Adding labels to a pv.Layout.Stack (streamgraph) in Protovis

I'm working with the Protovis library to do a streamgraph of data. I want to label the different layers with the "words" array. I can't seem to get the words to line up how I'd like. I want them to be inserted where the graph is the largest for that particular layer, similar to this site:
http://mbostock.github.com/protovis/ex/jobs.html
var words = [
"tasty","delicious","yum","scrumpious","dry"];
var data = [
[23,52,6,3,16,35,24,12,35,119,2,5,65,33,81,61,55,122,3,19,2,5,65,33,81,61,55,122,3,19,54,72,85,119,23,52,6,3,16,35],
[43,2,46,78,46,25,54,72,85,119,23,52,6,3,16,35,24,12,35,119,23,52,6,3,16,35,24,12,35,119,2,5,65,33,81,61,55,122,3,19],
[2,5,65,33,81,61,55,122,3,19,54,72,85,119,23,52,6,3,16,35,2,5,65,33,81,1,5,12,95,14,12,8,84,115,15,27,6,31,6,35],
[2,5,6,3,1,6,5,12,32,191,142,22,75,139,27,32,26,13,161,35,21,52,64,35,21,61,55,123,5,142,54,58,8,11,53,2,64,3,16,35],
[2,5,65,33,81,61,55,122,3,19,54,72,85,119,23,52,6,3,16,35,2,5,65,33,81,61,55,123,5,142,54,58,8,11,53,2,64,3,16,35]];
var w = 800,
h = 300,
x = pv.Scale.linear(0, 40).range(0, w),
y = pv.Scale.linear(0, 600).range(0, h);
var vis = new pv.Panel()
.canvas('streamgraph')
.width(w)
.height(h);
vis.add(pv.Layout.Stack)
.layers(data)
.order("inside-out")
.offset("wiggle")
.x(x.by(pv.index))
.y(y)
.layer.add(pv.Area)
.fillStyle(pv.ramp("#aad", "#556").by(Math.random))
.strokeStyle(function () { this.fillStyle().alpha(.5) });
vis.render();
Try this:
vis.add(pv.Layout.Stack)
.layers(data)
.order("inside-out")
.offset("wiggle")
.x(x.by(pv.index))
.y(y)
.layer.add(pv.Area)
.fillStyle(pv.ramp("#aad", "#556").by(Math.random))
.strokeStyle(function () { this.fillStyle().alpha(.5) })
// this is new code:
.anchor("center").add(pv.Label)
.def("max", function(d) {return pv.max.index(d)})
.visible(function() {return this.index == this.max() })
.text(function(d, p) {return words[this.parent.index]});
Basically this adds a whole bunch of labels to your areas, But then only makes them visible at the index where the value is the maximum, by defining a function max on the series. I adapted this code from the code in the link you sent.

Categories

Resources