Skip to content

Instantly share code, notes, and snippets.

@cloudshapes
Last active December 19, 2015 06:09
Show Gist options
  • Save cloudshapes/5909708 to your computer and use it in GitHub Desktop.
Save cloudshapes/5909708 to your computer and use it in GitHub Desktop.
AttrTween, Transitions and MV* in Reusable D3

AttrTween, Transitions and MV* in Reusable D3

An example of using attrTween & transitions whilst writing reusable D3.

The code uses three instances of a data module to hold onto and manipulate the data used to drive the patterns, and one instance of a "manager", which handles the creation and manipulation (i.e. the classic enter, update, exit) of the actual visual pattern.

Full write-up, split across two blog posts, at "Using AttrTween, Transitions and MV*" on the Safari Books Online Blog.

<!doctype html>
<html lang="en">
<head lang=en>
<meta charset="utf-8">
<title>AttrTween, Transitions and MV* in Reusable D3</title>
<style>
svg {
background: #eee;
}
path {
fill: none;
stroke-width: 3px;
}
circle {
stroke: #fff;
stroke-width: 3px;
}
text {
font-family: Verdana, sans-serif;
font-size: 0.8em;
font-weight: bold;
}
</style>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<script>
//////////////// CONSTANTS ////////////////////
var C_SVG_WIDTH=1200;
var C_SVG_HEIGHT=900;
var C_GENERAL_TRANSITION_TIME = 2000;
var C_CIRCLE_MIN_RADIUS = 8;
var C_CIRCLE_RADIUS_RANGE = 25;
var C_TRANSITION_DELAY = 500;
var C_MINIMUM_DURATION = 2000;
var C_DURATION_RANGE = 3000;
var C_MAX_COLOR_RANGE = 19;
///////////////////////////////////// patternData Module: START //////////////
d3.cloudshapes = {};
d3.cloudshapes.patternData = function module() {
var id, pattern, patternData, line_tension, line_interpolation, transition_ease;
var path_color, circle_color, text_color, duration;
var circle_radius, reverse_flag;
var exports = function(_id, _line_tension, _line_interpolation){
id = _id;
line_tension = _line_tension;
line_interpolation = _line_interpolation;
patternData = d3.map();
};
// Specific pattern data setters:
exports.set_spiral_data = function(centerX, centerY, radius, sides, coils, rotation, spiral_text) {
var t_map = d3.map();
t_map.set('centerX', centerX);
t_map.set('centerY', centerY);
t_map.set('radius', radius);
t_map.set('sides', sides);
t_map.set('coils', coils);
t_map.set('rotation', rotation);
t_map.set('text', spiral_text);
patternData.set('spiral', t_map);
}
exports.set_sine_data = function(startX, startY, width, height, nPoints, sine_text) {
var t_map = d3.map();
startX = startX - (width/2);
t_map.set('startX', startX);
t_map.set('startY', startY);
t_map.set('width', width);
t_map.set('height', height);
t_map.set('nPoints', nPoints);
t_map.set('text', sine_text);
patternData.set('sine', t_map);
}
exports.set_circle_data = function(centerX, centerY, radius, nPoints, circle_text) {
var t_map = d3.map();
t_map.set('centerX', centerX);
t_map.set('centerY', centerY);
t_map.set('radius', radius);
t_map.set('nPoints', nPoints);
t_map.set('text', circle_text);
patternData.set('circle', t_map);
}
exports.get_pattern_points = function() {
var t_ret_points = undefined;
var t_pattern_data = patternData.get(pattern);
switch(pattern) {
case 'spiral':
t_ret_points = exports.createSpiral(t_pattern_data.get('centerX'), t_pattern_data.get('centerY'), t_pattern_data.get('radius'), t_pattern_data.get('sides'), t_pattern_data.get('coils'), t_pattern_data.get('rotation'));
break;
case 'sine':
t_ret_points = exports.createSineWave(t_pattern_data.get('startX'), t_pattern_data.get('startY'), t_pattern_data.get('width'), t_pattern_data.get('height'), t_pattern_data.get('nPoints'));
break;
case 'circle':
t_ret_points = exports.createCircle(t_pattern_data.get('centerX'), t_pattern_data.get('centerY'), t_pattern_data.get('radius'), t_pattern_data.get('nPoints'));
break;
}
if (reverse_flag==true)
t_ret_points.reverse();
return t_ret_points;
}
exports.get_pattern_text = function() {
var t_ret_points = undefined;
var t_pattern_data = patternData.get(pattern);
return t_pattern_data.get('text');
}
// General Getters / Setters - there are many.
exports.id = function(_id) {
if (!arguments.length) return id;
id = _id;
return this;
};
exports.pattern = function(_pattern) {
if (!arguments.length) return pattern;
pattern = _pattern;
return this;
};
exports.patternData = function(_patternData) {
if (!arguments.length) return patternData;
patternData = _patternData;
return this;
};
exports.line_tension = function(_line_tension) {
if (!arguments.length) return line_tension;
line_tension = _line_tension;
return this;
};
exports.line_interpolation = function(_line_interpolation) {
if (!arguments.length) return line_interpolation;
line_interpolation = _line_interpolation;
return this;
};
exports.transition_ease = function(_transition_ease) {
if (!arguments.length) return transition_ease;
transition_ease = _transition_ease;
return this;
};
exports.path_color = function(_path_color) {
if (!arguments.length) return path_color;
path_color = _path_color;
return this;
};
exports.circle_color = function(_circle_color) {
if (!arguments.length) return circle_color;
circle_color = _circle_color;
return this;
};
exports.text_color = function(_text_color) {
if (!arguments.length) return text_color;
text_color = _text_color;
return this;
};
exports.duration = function(_duration) {
if (!arguments.length) return duration;
duration = _duration;
return this;
};
exports.circle_radius = function(_circle_radius) {
if (!arguments.length) return circle_radius;
circle_radius = _circle_radius;
return this;
};
exports.reverse_flag = function(_reverse_flag) {
if (!arguments.length) return reverse_flag;
reverse_flag = _reverse_flag;
return this;
};
// Functions to generate sets of points:
// SPIRAL CODE SHAMELESSLY LIFTED FROM: http://www.emoticode.net/actionscript-3/dynamically-draw-a-logarithmic-spiral.html?raw
//
//
// centerX-- X origin of the spiral.
// centerY-- Y origin of the spiral.
// radius--- Distance from origin to outer arm.
// sides---- Number of points or sides along the spiral's arm.
// coils---- Number of coils or full rotations. (Positive numbers spin clockwise, negative numbers spin counter-clockwise)
// rotation- Overall rotation of the spiral. ('0'=no rotation, '1'=360 degrees, '180/360'=180 degrees)
//
exports.createSpiral = function(centerX, centerY, radius, sides, coils, rotation){
var ret_points = [];
// How far to rotate around center for each side.
var aroundStep = coils/sides;// 0 to 1 based.
//
// Convert aroundStep to radians.
var aroundRadians = aroundStep * 2 * Math.PI;
//
// Convert rotation to radians.
rotation *= 2 * Math.PI;
//
// For every side, step around and away from center.
for(var i=1; i<=sides; i++){
//
// How far away from center
var away = Math.pow(radius, i/sides);
//
// How far around the center.
var around = i * aroundRadians + rotation;
//
// Convert 'around' and 'away' to X and Y.
var x = centerX + Math.cos(around) * away;
var y = centerY + Math.sin(around) * away;
//
// Now that you know it, do it.
ret_points.push([x,y]);
}
return ret_points;
};
// createCircle ...
exports.createCircle=function(centerX, centerY, radius, nPoints){
var ret_points = [];
var t_step = (Math.PI*2)/nPoints;
var t_rad = 0;
for (var i=0; i <= nPoints; i++) {
var t_x = (Math.cos(t_rad) * radius) + centerX;
var t_y = (Math.sin(t_rad) * radius) + centerY;
ret_points.push([t_x,t_y]);
t_rad += t_step;
}
return ret_points;
}
// createSineWave
exports.createSineWave=function(startX, startY, width, height, nPoints){
var ret_points = [];
// Algorithm to draw circle, see Prince Charles etc ...
var t_step = width / nPoints;
var t_rad_step = (Math.PI*2)/nPoints;
t_x = startX;
t_rad = 0;
for (var i=0; i <= nPoints; i++) {
t_y = (Math.sin(t_rad) * height) + startY;
ret_points.push([t_x,t_y]);
t_rad += t_rad_step;
t_x += t_step;
}
return ret_points;
}
return exports;
};
///////////////////////////////////// patternData Module: END //////////////
///////////////////////////////////// patternManager Component: STARTS //////////////
d3.cloudshapes.patternManager = function module() {
var targetSvg;
var data;
var dispatch = d3.dispatch('transitionComplete', 'allTransitionsComplete', 'all_enter_update_exit_TransitionsComplete');
var nTransitionsComplete;
var nenter_update_exit_TransitionsComplete;
function exports(_selection, _data) {
data = _data;
_selection.each(function() {
if (!targetSvg) {
targetSvg = d3.select(this);
}
nenter_update_exit_TransitionsComplete = 0;
// 'g' elements. One 'g' per pattern:
// First, create one 'g' per pattern:
var enter_gelements = svg.selectAll("g.pattern")
.data(data, function(d) { return d.id();})
.enter()
.append("g")
.classed("pattern", true)
.attr('id', function(d) { return d.id();})
.style({opacity: 0});
// Update g.pattern:
var update_gelements = svg.selectAll("g.pattern")
.data(data, function(d) { return d.id();})
.attr('id', function(d) { return d.id();})
.transition().delay(C_TRANSITION_DELAY).duration(C_GENERAL_TRANSITION_TIME).style({opacity: 1})
.each("end", function() { exports.enter_update_exit_transactionComplete(); });
// Exit g.pattern:
svg.selectAll("g.pattern")
.data(data, function(d) { return d.id();})
.exit()
.transition().delay(C_TRANSITION_DELAY).duration(C_GENERAL_TRANSITION_TIME).style({opacity: 0}).remove()
.each("end", function() { exports.enter_update_exit_transactionComplete(); });
//////////// Enter() code: ///////////
enter_gelements
.append("path")
.attr("d", function(d) {
var t_line_tension = d.line_tension();
var t_line_interpolation = d.line_interpolation();
var t_line_creator = d3.svg.line()
.tension(t_line_tension)
.interpolate(t_line_interpolation)
.x(function(d) { return d[0]; })
.y(function(d) { return d[1]; });
var t_points = d.get_pattern_points();
var t_ret_value = t_line_creator(t_points);
t_points.length = 0;
return t_ret_value;
})
// Add an 'inner' 'g' element - to hold the circle and the text.
var enter_inner_g_elements = enter_gelements
.append("g")
.classed("inner", true)
.attr("transform", function(d,i) {
var t_points = d.get_pattern_points();
var t_ret_string = "translate(" + t_points[0][0] + "," + t_points[0][1] + ")";
t_points.length = 0;
return t_ret_string;
});
// Add the circle to the 'inner' 'g'element:
enter_inner_g_elements
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
// Add the text to the 'inner' 'g' element:
enter_inner_g_elements
.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("fill", function(d) { return d.text_color(); })
.text(function(d) {
var t_text = d.get_pattern_text();
var t_d = new Date();
var t_curr_hour = t_d.getHours();
var t_curr_min = t_d.getMinutes();
var t_curr_sec = t_d.getSeconds();
return t_text + " (" + t_curr_hour + ":" + t_curr_min + ":" + t_curr_sec + ")";
} )
.attr("d", function(d) {
var t_this_text_element = d3.select(this);
var t_bbox = t_this_text_element.node().getBBox();
t_this_text_element.attr('x', (-t_bbox.width/2));
return d;
})
enter_gelements.transition().delay(C_TRANSITION_DELAY).duration(C_GENERAL_TRANSITION_TIME).style({opacity: 1})
.each("end", function() { exports.enter_update_exit_transactionComplete(); });
//////////// Update() code: ///////////
update_gelements.select("path")
.attr("d", function(d) {
var t_line_tension = d.line_tension();
var t_line_interpolation = d.line_interpolation();
var t_line_creator = d3.svg.line()
.tension(t_line_tension)
.interpolate(t_line_interpolation)
.x(function(d) { return d[0]; })
.y(function(d) { return d[1]; });
var t_points = d.get_pattern_points();
var t_ret_value = t_line_creator(t_points);
t_points.length = 0;
return t_ret_value;
})
.style("stroke", function(d) { return d.path_color(); })
// Here - need to translate to start position of the path ...:
update_gelements.select("g.inner")
.attr("transform", function(d,i) {
var t_points = d.get_pattern_points();
var t_ret_string = "translate(" + t_points[0][0] + "," + t_points[0][1] + ")";
t_points.length = 0;
return t_ret_string;
});
// Update circle:
update_gelements.select("g.inner").select("circle")
.attr("r", function(d) { return d.circle_radius();} )
.style("fill", function(d) { return d.circle_color(); });
// Update text:
update_gelements.select("g.inner").select("text")
.attr("fill", function(d) { return d.text_color(); })
.text(function(d) {
var t_text = d.get_pattern_text();
var t_d = new Date();
var t_curr_hour = t_d.getHours();
var t_curr_min = t_d.getMinutes();
var t_curr_sec = t_d.getSeconds();
return t_text + " (" + t_curr_hour + ":" + t_curr_min + ":" + t_curr_sec + ")";
} )
.attr("d", function(d) {
var t_this_text_element = d3.select(this);
var t_bbox = t_this_text_element.node().getBBox();
t_this_text_element.attr('x', (-t_bbox.width/2));
return d;
})
exports.on('transitionComplete', function(){exports.transitionsAllComplete();});
})
exports.enter_update_exit_transactionComplete = function() {
nenter_update_exit_TransitionsComplete++;
if (nenter_update_exit_TransitionsComplete == data.length) {
// Ready to go, so send event.
dispatch.all_enter_update_exit_TransitionsComplete();
}
}
exports.transitionsAllComplete = function() {
nTransitionsComplete++;
if (nTransitionsComplete == data.length) {
dispatch.allTransitionsComplete();
}
}
exports.transition = function() {
nTransitionsComplete=0;
var t_gelements = targetSvg.selectAll("g.pattern").selectAll("g.inner");
t_gelements
.attr("d", function(d) {
var t_ease_param = d.transition_ease();
var t_ease_function = d3.ease(t_ease_param);
d3.select(this).transition()
.ease(t_ease_function)
.duration(function(d) { return d.duration(); })
.delay(C_TRANSITION_DELAY)
.attrTween("transform", exports.translateAlong() )
.each("end", function() {dispatch.transitionComplete();});
return d;
})
}
exports.translateAlong = function() {
return function(d, i, a) {
var t_path = d3.select(this.parentNode).select("path");
return function(t) {
var t_path_node = t_path.node();
var l = t_path_node.getTotalLength();
var p = t_path_node.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
}
d3.rebind(exports, dispatch, "on");
return exports;
};
///////////////////////////////////// patternManager Component: Ends //////////////
///////////////////// Code that actually uses the module and the component:
// cached_data: cache of pattern data that we want to use. Acts as a backup, a repository from which we
// periodically take copies of pattern data.
var cached_data = [];
// live_data: the data that the patternManager actually uses.
// So this is an array that fluctuates between having a fully copy of cached_data, and having a
// copy of cached_data minus one entry.
var live_data = [];
// Create three items of pattern data, each in their own instance of the patternData module:
var t_pattern1 = d3.cloudshapes.patternData();
t_pattern1(1, 0.5, "cardinal");
t_pattern1.set_spiral_data(250, 300, 240, 1200, 24, 0.65, "Spiral 1");
t_pattern1.set_circle_data(150,300, 100, 100, "Circle 1");
t_pattern1.set_sine_data(150,300,300,200, 100, "Sine 1");
var t_pattern2 = d3.cloudshapes.patternData();
t_pattern2(2, 0.5, "cardinal");
t_pattern2.set_spiral_data(550,300,240,1200,24,0.65,"Spiral 2");
t_pattern2.set_circle_data(450,300, 100, 100, "Circle 2");
t_pattern2.set_sine_data(450,300,300,200, 100, "Sine 2");
var t_pattern3 = d3.cloudshapes.patternData();
t_pattern3(3, 0.5, "cardinal");
t_pattern3.set_spiral_data(850,300,240,1200,24,0.65, "Spiral 3");
t_pattern3.set_circle_data(750,300, 100, 100, "Circle 3");
t_pattern3.set_sine_data(750,300,300,200, 100, "Sine 3");
// Stuff them in the cached_data array.
cached_data.push(t_pattern1);
cached_data.push(t_pattern2);
cached_data.push(t_pattern3);
var dispatch = d3.dispatch('patternManagerTransitionComplete');
// Initialise the main SVG:
var svg = d3.select("body").append("svg")
.attr("width", C_SVG_WIDTH)
.attr("height", C_SVG_HEIGHT);
// Setup instance of patternManager:
var patternManager = d3.cloudshapes.patternManager();
// Once all of enter, update, and exit transitions are complete, then
// kick off the main transitions.
patternManager.on('all_enter_update_exit_TransitionsComplete', function() {
patternManager.transition();
});
// Once the main transitions are all complete, dispatch an event so we're
// definitely not calling the patternManager from within patternManager code.
patternManager.on('allTransitionsComplete', function() {
dispatch.patternManagerTransitionComplete();
});
var t_ease = ['linear-in-out', 'cubic-in-out', 'bounce-in-out', 'back-in-out', 'sin-in-out', 'quad-in-out'];
var t_patterns = ['circle', 'spiral', 'sine'];
// At this point, we know all the main transitions are complete,
// so fiddle with / change / randomise data, and kick off the main transitions again:
dispatch.on('patternManagerTransitionComplete', function(){
// First up, make sure live_data contains a full copy of cached_data.
for (var i=0; i<cached_data.length; i++) {
if (live_data.length==0) {
live_data.push(cached_data[i]);
} else {
var t_found = false;
for (var j=0;j<live_data.length;j++) {
if (cached_data[i].id() == live_data[j].id()) {
t_found = true;
}
}
if (t_found == false) {
live_data.push(cached_data[i]);
}
}
}
// Occasionally, randomly delete one entry from live_data.
var t_delete_or_not = Math.round(Math.random());
if (t_delete_or_not==1) {
var t_index_to_delete = Math.floor(Math.random()*live_data.length);
live_data.splice(t_index_to_delete, 1);
}
// Next up: randomly change various bits'n'bobs in each visual pattern.
var t_path_color_list = d3.scale.category20();
var t_circle_color_list = d3.scale.category20b();
var t_text_color_list = d3.scale.category20();
var t_patterns_length = t_patterns.length;
for (var i=0; i < live_data.length; i++) {
var t_pattern_index = Math.floor(Math.random() * t_patterns_length);
var t_pattern = t_patterns[t_pattern_index];
live_data[i].pattern(t_pattern);
var t_cr_radius = C_CIRCLE_MIN_RADIUS + Math.floor(Math.random() * C_CIRCLE_RADIUS_RANGE);
live_data[i].circle_radius(t_cr_radius);
var t_ease_setting = Math.floor(Math.random() * t_ease.length);
live_data[i].transition_ease(t_ease[t_ease_setting]);
var t_path_color_index = Math.floor(Math.random() * C_MAX_COLOR_RANGE);
var t_path_color = t_path_color_list.range()[t_path_color_index];
live_data[i].path_color(t_path_color);
var t_circle_index = Math.floor(Math.random() * C_MAX_COLOR_RANGE);
var t_circle_color = t_circle_color_list.range()[t_circle_index];
live_data[i].circle_color(t_circle_color);
var t_text_index = Math.floor(Math.random() * C_MAX_COLOR_RANGE);
var t_text_color = t_text_color_list.range()[t_text_index];
live_data[i].text_color(t_text_color);
var t_duration = C_MINIMUM_DURATION + Math.floor(Math.random() * C_DURATION_RANGE);
live_data[i].duration(t_duration);
live_data[i].reverse_flag(true);
if (Math.round(Math.random())==1)
live_data[i].reverse_flag(false);
}
// Actually call the patternManager, render the patterns specified in live_data.
svg.call(patternManager, live_data);
});
// Kick things off:
dispatch.patternManagerTransitionComplete();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment