instant values for d3 enter selection and transitioned for update - javascript

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

Related

Go back to previous children order after calling .raise()

I'm building a network with d3js (basically nodes and links)
When I mouseouver one node, I want to highlight the associated links and make them top of parentNode to make them really visible
On mouseover, I've made something like this
// get links to highlight given the selected node
var highlightLinks = nodeLinks(id);
lines.each(function(index){
// if index is in selected links
if (highlightLinks.include(index)){
// highlight link with yellow color
this.setAttribute('style',"stroke:#ffc900")
// raise the link on top of parent node
d3.select(this).raise()
}
else {
// else grey
this.setAttribute('style',"stroke:#1B1B1B")
}
}
It's working fine with the first mouseouver. But when I get out, my lines aren't ordered the same way anymore, and the if clause is highlighting random links for other mouseovers
I think the solution should be to re-order back the links when exiting mouseover, but I can't find a way to do this. I've stored the movedIndex as an array
What I would like :
(state 1) My links before first mouseover : [0,1,2,3,4,5]
(action 1) Enter mouseover highlighting 1 and 2, raising them, stored the movedIndex = [1,2]
(state 2) My links after mouseover : [0,3,4,5,1,2]
(action 2) Exit mouseover highlighting 1 and 2, do some magic with movedIndex to "unraise" them
(state 3) My links should be again : [0,1,2,3,4,5]
Provided you're not changing your data, the easiest alternative is selection.order(), which:
Re-inserts elements into the document such that the document order of each group matches the selection order.
Thus, all you need in your mouseout event is this:
lines.order();
Here's a simple demo (using v7):
const svg = d3.select("svg");
const circles = svg.selectAll(null)
.data(d3.range(0, 1.2, 0.2))
.enter()
.append("circle")
.attr("r", 40)
.attr("cy", 50)
.attr("cx", d => 80 + 240 * d)
.style("fill", d3.interpolateTurbo);
circles.on("mouseover", event => d3.select(event.currentTarget).raise())
.on("mouseout", () => circles.order());
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg width="400" height="100"></svg>
Calling d3.lower() in reverse order will restore the original order after d3.raise(). See reorderItems as an example:
const colors = ['red', 'green', 'blue', 'yellow', 'orange'];
const g = d3.select('g');
const angleUnit = Math.PI * 2 / colors.length;
const reorderItems = () => {
for (let index = colors.length - 1; index >= 0; index--)
g.select(`circle[item-index='${index}']`).lower();
}
const createItem = (color, index) => {
const angle = angleUnit * index;
const x = 50 * Math.sin(angle);
const y = 50 * -Math.cos(angle);
g.append('circle')
.attr('cx', x)
.attr('cy', y)
.attr('r', 40)
.attr('item-index', index)
.style('fill', color)
.on('mouseenter', function() {
d3.select(this).raise();
})
.on('mouseleave', reorderItems);
}
colors.forEach(createItem);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="200" height="200">
<g transform="translate(100, 100)" />
</svg>

Is there a way to change C3 legend item tiles to be icons?

I want my bar graph to have different coloured versions of the same icon for the legend tiles. I am currently using font-awesome 5 for text in my HTML document but I do not mind using other icon libraries if I must (as long as it's free). At the moment my graph has squares for the legend tiles as that is the default. In font-awesome the icon I want is class="fa fa-bar-chart". The class for the legend tile is called .c3-legend-item-tile
I tried code from Use Font Awesome Icons in CSS but the code there didn't help change the legend tiles. Neither did Using Font Awesome icon for bullet points, with a single list item element
I don't want my icons to float above the bar chart like the example from Adding icons to bar charts made using c3js I just want the tiles to change from the default square to an icon.
https://jsfiddle.net/SharonM/k49gbs13/25/ has a rough example of what I've tried. (Labels show what I want the actual tiles to look like but I do not actually want labels)
.c3-chart-text .c3-text {
font-family: 'FontAwesome';
}
.c3-legend-item-tile::before {
font-family: 'FontAwesome';
content: '\uf080'!important;
font-weight: 900!important;
margin: 0 5px 0 -15px!important;
}
I've tried before and after and also just the class itself.
Update:
I also tried:
d3.select(string).insert('div', '.chart').attr('class', 'legend').selectAll('span')
.data(this.allIDs)
.enter().append('span')
.attr('data-id', function (id) { return id; })
.html(function (id) { return '<i class="fa fa-bar-chart" aria-hidden="true"> </i>'+ id; })
.each(function (id) {
d3.select(this).style('background-color', chart.color(id));
})
.on('mouseover', function (id) {
chart.focus(id);
})
.on('mouseout', function (id) {
chart.revert();
})
.on('click', function (id) {
chart.toggle(id);
});
where string is the name of my container class but that did not do what I wanted at all. It created new 'legends' on the side with the icon I wanted but that bypassed my onclick checks when toggling which I could re-implement in this function but it just looks really bad. I'd rather the original little square was replaced by the icon.
Version for C3 < 0.7
It turns out the way to do this and keep the colours is to do stuff to the .c3-legend-item-tile(s). Since they're rectangles you can't add text to them but what you can do is use masks and patterns on them to give the impression of text.
My first attempt just replaced the fill style of the c3-legend-item-tiles with a pattern, where the pattern was the text character needed. However, this removed the colour and just showed them as black - not very handy
It turns out what you can do though is add a mask separately from the fill style and reuse the pattern within there --> How to change color of SVG pattern on usage? . Here we set the mask to be a rectangle, which in turn uses the pattern as a fill, and hey presto the 'icons' appear in the right colour as the fill style per tile stays as it was...
https://jsfiddle.net/j2596x0u/
var chart = c3.generate({
data: {
columns: [
['data1', 30, -200, -100, 400, 150, 250],
['data2', -50, 150, -150, 150, -50, -150],
['data3', -100, 100, -40, 100, -150, -50]
],
type: 'bar',
labels: {
// format: function (v, id, i, j) { return "Default Format"; },
format: {
data1: function (v, id, i, j) { return "\uf080"; },
data2: function (v, id, i, j) { return "\uf080"; },
data3: function (v, id, i, j) { return "\uf080"; },
}
}
},
grid: {
y: {
lines: [{value: 0}]
}
},
onrendered: function () {
d3.selectAll(".c3-legend-item-tile").attr("mask", "url(#iconMask)");
}
});
d3.select("svg defs").append("mask")
.attr("id", "iconMask")
.attr("x", 0)
.attr("y", 0)
.attr("width", 1)
.attr("height", 1)
.attr("maskContentUnits", "objectBoundingBox")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 1)
.attr("height", 1)
.style("fill", "url(#iconPattern)")
;
d3.select("svg defs").append("pattern")
.attr("id", "iconPattern")
.attr("x", 0)
.attr("y", 0)
.attr("width", 1)
.attr("height", 1)
.style("fill", "#fff")
.attr("patternContentUnits", "objectBoundingBox")
.append("text")
.attr("x", "0.2em")
.attr("dy", "1em")
.attr("font-size", 0.8)
.attr ("font-family", "FontAwesome")
.text("\uf080")
;
I won't pretend it was simple, I had to use objectBoundingBox and normalise everything in the masks and patterns to between 0 and 1 to get them to show up in the right place from the example I found, and the font-size was trial and error. But, yeh, it's doable.
C3 0.7+
Ha, it turns out the mask didn't work simply because the dom considers the lines to have zero height even though it is drawn with 10px thickness. If I change the y2 attribute so the line is technically a diagonal there's enough space for the text mask and pattern to render:
For c3 versions where .c3-legend-item-tile's are lines rather than rects
onrendered: function () {
d3.selectAll(".c3-legend-item-tile").attr("mask", "url(#iconMask)")
.each (function (d,i) {
var d3this = d3.select(this);
d3this.attr("y2", +d3this.attr("y1") + 10);
})
;
}
https://jsfiddle.net/u7coz2p5/3/

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>

Force directed graphs in d3v4

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

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