Force directed graphs in d3v4 - javascript

I'm experimenting with D3 version 4 force directed graphs and have looked at Jim Vallandingham's tutorial and code as a starting point.
http://vallandingham.me/bubble_chart_v4/
and am attempting to produce an animation similar to the example here from Nathan Yau
https://flowingdata.com/2016/08/23/make-a-moving-bubbles-chart-to-show-clustering-and-distributions/
I've stripped the bubble chart from Jim Vallandingham's code to what I think I need and can display the individual states by changing the index value, but for some reason the code does not want to animate between the different states. I assume the redraw function isn't working. It may be an obvious error or one made through complete ignorance, but if you can help it would be great.
Here's my code:
function bubbleChart() {
var width = 940;
var height = 600;
var center = { x: width / 2, y: height / 3 };
var years = ["0","2008", "2009", "2010"];
var yearCenters = {
2008: { x: width / 3, y: 2 * height / 3 },
2009: { x: width / 2, y: 2 * height / 3 },
2010: { x: 2 * width / 3, y: 2 * height / 3 }
};
// #v4 strength to apply to the position forces
var forceStrength = 0.03;
// These will be set in create_nodes and create_vis
var svg = null;
var bubbles = null;
var nodes = [];
var index= 0;
function charge(d) {
return -Math.pow(d.radius, 2.3) * forceStrength;
}
// Here we create a force layout
var simulation = d3.forceSimulation()
.velocityDecay(0.2)
.force('x', d3.forceX().strength(forceStrength).x(center.x))
.force('y', d3.forceY().strength(forceStrength).y(center.y))
.force('charge', d3.forceManyBody().strength(charge))
.on('tick', ticked);
// #v4 Force starts up automatically, which we don't want as there aren't any nodes yet.
simulation.stop();
// Nice looking colors
var fillColor = d3.scaleOrdinal()
.domain(['low', 'medium', 'high'])
.range(['#d84b2a', '#beccae', '#7aa25c']);
function createNodes(rawData) {
var myNodes = rawData.map(function (d) {
return {
id: d.id,
radius: 5,
value: +d.total_amount,
name: d.grant_title,
org: d.organization,
group: d.group,
year: d.start_year,
x: Math.random() * 900,
y: Math.random() * 800
};
});
// sort them to prevent occlusion of smaller nodes.
myNodes.sort(function (a, b) { return b.value - a.value; });
return myNodes;
}
/*
* Main entry point to the bubble chart.
*/
var chart = function chart(selector, rawData) {
// convert raw data into nodes data
nodes = createNodes(rawData);
// Create a SVG element inside the provided selector
// with desired size.
svg = d3.select(selector)
.append('svg')
.attr('width', width)
.attr('height', height);
// Bind nodes data to what will become DOM elements to represent them.
bubbles = svg.selectAll('.bubble')
.data(nodes, function (d) { return d.id; });
// Create new circle elements each with class `bubble`.
// There will be one circle.bubble for each object in the nodes array.
// Initially, their radius (r attribute) will be 0.
// #v4 Selections are immutable, so lets capture the
// enter selection to apply our transtition to below.
var bubblesE = bubbles.enter().append('circle')
.classed('bubble', true)
.attr('r', 0)
.attr('fill', function (d) { return fillColor(d.group); })
.attr('stroke', function (d) { return d3.rgb(fillColor(d.group)).darker(); })
.attr('stroke-width', 2)
// #v4 Merge the original empty selection and the enter selection
bubbles = bubbles.merge(bubblesE);
// Fancy transition to make bubbles appear, ending with the
// correct radius
bubbles.transition()
.duration(2000)
.attr('r', function (d) { return d.radius; });
// Set the simulation's nodes to our newly created nodes array.
// #v4 Once we set the nodes, the simulation will start running automatically!
simulation.nodes(nodes);
chart.redraw();
};
// Callback function that is called after every tick of the force simulation.
// These x and y values are modified by the force simulation.
function ticked() {
bubbles
.attr('cx', function (d) { return d.x; })
.attr('cy', function (d) { return d.y; });
}
chart.redraw = function (index){
simulation.force('x', d3.forceX().strength(forceStrength).x(nodePosX));
simulation.force('y', d3.forceY().strength(forceStrength).y(nodePosY));
simulation.alpha(1).restart();
}
function nodePosX(d) {
if (+d.year <= +years[index]) {
return yearCenters[d.year].x;
} else {
return center.x;
}
}
function nodePosY(d) {
if (+d.year <= +years[index]) {
return yearCenters[d.year].y;
} else {
return center.y;
}
}
// return the chart function from closure.
return chart;
}
var myBubbleChart = bubbleChart();
myBubbleChart('#vis', data);
for (i=0;i<4;i++){
setInterval(function(){myBubbleChart.redraw(i);}, 100);
}

I misunderstood how to use setInterval to redraw the chart, so it should be as follows:
var i = 0;
setInterval(function(){myBubbleChart.redraw(i++);}, 1000);

Related

instant values for d3 enter selection and transitioned for update

I'm trying to avoid repeating code for the enter and update selections in d3. I have successfully used merge to get the right selection of all items that are present with the new data (after I have used append to add the new ones). Now I want to set some attributes such as r, cy and cx. I can do that on the merged selection but if I put them behind a transition the items that are just appearing animate in from 0,0 and I want them to just appear in the right place.
What I'm hoping for is some way to write all the attribute setting lines into one function and one time pass in the enter() selection and the next time pass in the transitioned merge so that I only have to write the lines for setting cx once. Here's what I have already followed by my guess at pseudo code
const valueCircles = this.chartGroup.selectAll('.valueCircle')
.data(values);
valueCircles.enter()
.append('circle')
.attr('stroke-width', '1px')
.attr('stroke', '#ffffff')
.attr('opacity', '1')
.attr('class', 'dots valueCircle')
.merge(valueCircles)
.transition()
.duration(100)
.attr('r', (d) => {
if (d.y < 0.5 ) {
return 5;
} else {
return 15;
}
})
.attr('cx', (d) => {
return timescale(new Date(d.x));
})
.attr('cy', (d) => {
return valueScale(d.y)
})
This is what I think I want to achieve
function setTheDynamicValues(aSelection) {
aSelection
.attr('cx', (d) => {
return timescale(new Date(d.x));
})
.attr('cy', (d) => {
return valueScale(d.y)
})
}
function updateGraph(newData) {
const valueCircles = this.chartGroup.selectAll('.valueCircle')
.data(newData);
setTheDynamicValues(valueCircles.enter());
setTheDynamicValues(valueCircles.transition().duration(100));
}
Another way of describing this would be to make the duration of the transition 0 for the entering elements and 100 for existing elements so that new ones appear correct and existing ones have a visible transition.
First of all, D3 selections are immutable: that merge of yours is not doing anything. This would be the proper pattern:
//here, pay attention to "let" instead of const
let valueCircles = this.chartGroup.selectAll('.valueCircle')
.data(values);
//our enter selection is called "valuesCirclesEnter"
const valueCirclesEnter = valueCircles.enter()
.append('circle')
etc...
//now, you merge the selections
valueCircles = valueCirclesEnter.merge(valueCircles);
From that point on, valueCircles contains both your updating and entering elements.
Now, back to your question:
An idiomatic D3 indeed has a lot of repetition, and that's a point lots of people complain about D3. But we can avoid some repetition with methods like selection.call. Also, since you want a transition for the update selection but none for the enter selection, you can simply drop the merge.
Here is a basic exemple, using a bit of your code:
const data1 = [{
x: 100,
y: 100
}, {
x: 230,
y: 20
}];
const data2 = [{
x: 10,
y: 10
}, {
x: 50,
y: 120
}, {
x: 190,
y: 100
}, {
x: 140,
y: 30
}, {
x: 270,
y: 140
}];
const svg = d3.select("svg");
draw(data1);
setTimeout(() => draw(data2), 1000);
function draw(values) {
const valueCircles = svg.selectAll('.valueCircle')
.data(values);
valueCircles.enter()
.append('circle')
.attr("class", "valueCircle")
.attr("r", 10)
.call(positionCircles);
valueCircles.transition()
.duration(1000)
.call(positionCircles)
};
function positionCircles(selection) {
selection.attr("cx", d => d.x)
.attr("cy", d => d.y)
}
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg></svg>
In the snippet above, I'm positioning both the enter and update selections using a function called positionCircles; however, while for the enter selection I'm passing a simple selection, for the update selection I'm passing a transitioning selection. But the function is the same:
function positionCircles(selection) {
selection.attr("cx", d => d.x)
.attr("cy", d => d.y)
}

Change order in which data is drawn in d3

I have some data (Nodes) that I need to draw. These nodes can overlap and thus the order in which they are drawn is important (the ones supposed to be displayed on top need to be drawn lastly).
The position and consequently the z-axis of those nodes is subject to change, that is why I tried to model this behavior by using a key, that incoporates the current index of the List the nodes are stored in.
case class Node(id: Int)
def addNodesToSVG = {
val sortedData: List[Node] = ???
val nodesSelection = d3.select("#nodes").selectAll(".node")
.data(sortedData.toJSArray, (n: Node) => {
n.id.toString +
// the position of those nodes may change over time
// that's why we need to include the position in the identifier
sortedData.indexOf(n)
}
nodesSelection.enter().append("g").attr("class", "node") // ...
nodesSelection
.attr("transform", transform) // ...
nodesSelection.exit().remove()
}
Unfortunatly, this does not seem to work as expected.
In theory this is how I thought this is going to work if I just have two nodes (n1 and n2), which are saved in a List(n1, n2)
node key
----- ---
n1 10 // node 1 at position 0
n2 21 // node 2 at position 1
Now if I change the List to List(n2, n1) and call addNodesToSVG again this is what I thought is going to happen:
node key
----- ---
n2 20 // node 1 at position 0
n1 12 // node 2 at position 1
Since these are unknown I thought it will remove (nodesSelection.exit().remove()) the old nodes and draw the 'new' ones in the correct order. This - however - is not happening. Why?
Edit after some more debugging I found out that my exit Selection is always of size 0.
I think id function should be used in a consistent manner -- just because an object changed its position, the result of the id's function on it shouldn't change (which as I see it is the whole point of using it in the first place). The approach I'd take would be making the id function to solely depend on node's id; add a field to data objects that specifies rendering order; sort selection after merging according to that new field.
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<button onclick="sw()">Switch</button>
<script>
var d1 = [{
id: 'a',
z: 1,
fill: 'red',
y: 0
}, {
id: 'b',
z: 2,
fill: 'green',
y: 5
}];
var d2 = [{
id: 'a',
z: 2,
fill: 'red',
y: 5
}, {
id: 'b',
z: 1,
fill: 'green',
y: 0
}]
var current = 0;
var svg = d3.select("body").append("svg")
.attr("width", 100)
.attr("height", 100)
.attr('viewBox', '0 0 10 20');
function render(d) {
var r = svg.selectAll('rect')
.data(d, function(d) { return d.id; });
r.enter()
.append('rect')
.attr('width', 10)
.attr('height', 10)
.attr('fill', function(d) { return d.fill; })
.merge(r)
.sort(function(r1, r2) {
if (r1.z > r2.z) return 1;
if (r1.z < r2.z) return -1;
return 0;
})
.transition()
.attr('y', function(d) { return d.y; });
r.exit().remove();
};
function sw() {
if (current == 0) {
current = 1;
render(d2);
} else {
current = 0;
render(d1);
}
}
render(d1);
</script>
</body>

Split an SVG path lengthwise into multiple colours

I have a tree visualisation in which I am trying to display paths between nodes that represent a distribution with multiple classes. I want to split the path lengthwise into multiple colours to represent the frequency of each distribution.
For example: say we have Class A (red) and Class B (black), that each have a frequency of 50. Then I would like a path that is half red and half black between the nodes. The idea is to represent the relative frequencies of the classes, so the frequencies would be normalised.
My current (naive) attempt is to create a separate path for each class and then use an x-offset. It looks like this.
However, as shown in the image, the lines do not maintain an equal distance for the duration of the path.
The relevant segment of code:
linkGroup.append("path").attr("class", "link")
.attr("d", diagonal)
.style("stroke", "red")
.style("stroke-width", 5)
.attr("transform", function(d) {
return "translate(" + -2.5 + "," + 0.0 + ")"; });
linkGroup.append("path").attr("class", "link")
.attr("d", diagonal)
.style("stroke", "black")
.style("stroke-width", 5)
.attr("transform", function(d) {
return "translate(" + 2.5 + "," + 0.0 + ")"; });
It would be great if anyone has some advice.
Thanks!
A possible solution is to calculate the individual paths and fill with the required color.
Using the library svg-path-properties from geoexamples.com you can calculate properties (x,y,tangent) of a path without creating it first like it is done in this SO answer (this does not calculate the tangent).
The code snippet does it for 2 colors but it can be easy generalized for more.
You specify the colors, percentage and width of the stroke with a dictionary
var duoProp = { color: ["red", "black"], percent: 0.30, width: 15 };
percent is the amount color[0] takes from the stroke width.
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
The pathPoints parameters
path that needs to be stroked, can be generated by d3.line path example from SO answer
var lineGenerator = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveNatural);
var curvePoints = [[0,0],[0,10],[20,30]];
var duoPath = pathPoints(lineGenerator(curvePoints), 10, duoProp);
path length interval at which to sample (unit pixels). Every 10 pixels gives a good approximation
dictionary with the percent and width of the stroke
It returns an array with the paths to be filled, 1 for each color.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/svg-path-properties#0.4.4/build/path-properties.min.js"></script>
</head>
<body>
<svg id="chart" width="350" height="350"></svg>
<script>
var svg = d3.select("#chart");
function pathPoints(path, stepLength, duoProp) {
var props = spp.svgPathProperties(path);
var length = props.getTotalLength();
var tList = d3.range(0, length, stepLength);
tList.push(length);
var tProps = tList.map(d => props.getPropertiesAtLength(d));
var pFactor = percent => (percent - 0.5) * duoProp.width;
tProps.forEach(p => {
p.x0 = p.x - pFactor(0) * p.tangentY;
p.y0 = p.y + pFactor(0) * p.tangentX;
p.xP = p.x - pFactor(duoProp.percent) * p.tangentY;
p.yP = p.y + pFactor(duoProp.percent) * p.tangentX;
p.x1 = p.x - pFactor(1) * p.tangentY;
p.y1 = p.y + pFactor(1) * p.tangentX;
});
var format1d = d3.format(".1f");
var createPath = (forward, backward) => {
var fp = tProps.map(p => forward(p));
var bp = tProps.map(p => backward(p));
bp.reverse();
return 'M' + fp.concat(bp).map(p => `${format1d(p[0])},${format1d(p[1])}`).join(' ') + 'z';
}
return [createPath(p => [p.x0, p.y0], p => [p.xP, p.yP]), createPath(p => [p.xP, p.yP], p => [p.x1, p.y1])]
}
var duoProp = { color: ["red", "black"], percent: 0.30, width: 15 };
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
</script>
</body>
</html>
As a quick follow-up to rioV8's excellent answer, I was able to get their code working but needed to generalise it to work with more than two colours. In case someone else has a similar requirement, here is the code:
function pathPoints(path, stepLength, duoProp) {
// get the properties of the path
var props = spp.svgPathProperties(path);
var length = props.getTotalLength();
// build a list of segments to use as approximation points
var tList = d3.range(0, length, stepLength);
tList.push(length);
var tProps = tList.map(function (d) {
return props.getPropertiesAtLength(d);
});
// incorporate the percentage
var pFactor = function pFactor(percent) {
return (percent - 0.5) * duoProp.width;
};
// for each path segment, calculate offset points
tProps.forEach(function (p) {
// create array to store modified points
p.x_arr = [];
p.y_arr = [];
// calculate offset at 0%
p.x_arr.push(p.x - pFactor(0) * p.tangentY);
p.y_arr.push(p.y + pFactor(0) * p.tangentX);
// calculate offset at each specified percent
duoProp.percents.forEach(function(perc) {
p.x_arr.push(p.x - pFactor(perc) * p.tangentY);
p.y_arr.push(p.y + pFactor(perc) * p.tangentX);
});
// calculate offset at 100%
p.x_arr.push(p.x - pFactor(1) * p.tangentY);
p.y_arr.push(p.y + pFactor(1) * p.tangentX);
});
var format1d = d3.format(".1f");
var createPath = function createPath(forward, backward) {
var fp = tProps.map(function (p) {
return forward(p);
});
var bp = tProps.map(function (p) {
return backward(p);
});
bp.reverse();
return 'M' + fp.concat(bp).map(function (p) {
return format1d(p[0]) + "," + format1d(p[1]);
}).join(' ') + 'z';
};
// create a path for each projected point
var paths = [];
for(var i=0; i <= duoProp.percents.length; i++) {
paths.push(createPath(function (p) { return [p.x_arr[i], p.y_arr[i]]; }, function (p) { return [p.x_arr[i+1], p.y_arr[i+1]]; }));
}
return paths;
}
// generate the line
var duoProp = { color: ["red", "blue", "green"], percents: [0.5, 0.7], width: 15 };
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
Note that the percents array specifies the cumulative percentage of the stroke, not the individual percentages of the width. E.g. in the example above, the red stroke will span 0% to 50% width, the blue stroke 50% to 70% width and the green stroke 70% to 100% width.

filtering data and working with scales - d3js

I have a simple scatterplot in d3js. The aim of the visualization is to fade out points on a selection. This works. Congruent with this, a new trendline should appear only for those selected points as well as an updated slope equation and R2 value. The fading of points and updating of slope equation/R2 values is working on selection. However, the trendline appears to be truncated and not scaled correctly, but I can't figure out why.
Here is a working version.
Following the on.change the following code is executed:
filteredData = filterJSON(data, 'name', value); // gets filtered json data
var x = d3.scaleLinear()
.range([0,width]);
var y = d3.scaleLinear()
.range([height,0]);
var xSeries1 = filteredData.map(function(e) { return e.x; }); // new x values
var ySeries1 = filteredData.map(function(e) { return e.y; }); // new y values
var rsq1 = leastSquares(xSeries1,ySeries1); // calculates r2/slope etc. - see function below
// Add trendline
ptAx1 = d3.min(xSeries1);
ptAy1 = rsq1[0] * d3.min(xSeries1) + rsq1[1];
ptBy1 = d3.min(ySeries1);
ptBx1 = (d3.min(ySeries1) - rsq1[1]) / rsq1[0];
svg.append("line")
.attr("class", "regression")
.attr("x1", x(ptAx1))
.attr("y1", y(ptAy1))
.attr("x2", x(ptBx1))
.attr("y2", y(ptBy1));
// calculate linear regression
function leastSquares(xSeries,ySeries) {
var reduceSumFunc = function(prev, cur) { return prev + cur; };
var xBar = xSeries.reduce(reduceSumFunc) * 1.0 / xSeries.length;
var yBar = ySeries.reduce(reduceSumFunc) * 1.0 / ySeries.length;
var ssXX = xSeries.map(function(d) { return Math.pow(d - xBar, 2); })
.reduce(reduceSumFunc);
var ssYY = ySeries.map(function(d) { return Math.pow(d - yBar, 2); })
.reduce(reduceSumFunc);
var ssXY = xSeries.map(function(d, i) { return (d - xBar) * (ySeries[i] - yBar); })
.reduce(reduceSumFunc);
var slope = ssXY / ssXX;
var intercept = yBar - (xBar * slope);
var rSquare = Math.pow(ssXY, 2) / (ssXX * ssYY);
return [slope, intercept, rSquare];
}
This code works well when all data points (no filtering of data), but doesn't when filtering occurs.
This is all points - trendline ok
This is filtered points - trendline truncated
It looks like you left "min" where you meant "max" in assigning values to ptBy1 and ptBx1
Made this change in your "blockbuilder" and it seemed to work as intended.

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.

Categories

Resources