[ Launch: Funnel ] 6704206 by rowoot
[ Launch: Funnel ] 6704203 by rowoot
[ Launch: Funnel ] 6588330 by jfsiii
-
-
Save michealbenedict/6704206 to your computer and use it in GitHub Desktop.
Funnel
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{"description":"Funnel","endpoint":"","display":"svg","public":true,"require":[],"fileconfigs":{"inlet.js":{"default":true,"vim":false,"emacs":false,"fontSize":12},"_.md":{"default":true,"vim":false,"emacs":false,"fontSize":12},"config.json":{"default":true,"vim":false,"emacs":false,"fontSize":12},"inlet.css":{"default":true,"vim":false,"emacs":false,"fontSize":12}},"fullscreen":false,"play":false,"loop":false,"restart":false,"autoinit":true,"pause":true,"loop_type":"pingpong","bv":false,"nclones":15,"clone_opacity":0.4,"duration":3000,"ease":"linear","dt":0.01,"thumbnail":"http://i.imgur.com/yFqd5zd.png"} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
svg { | |
width: 100%; | |
height: 100%; | |
} | |
.funnel-background { | |
fill: white; | |
} | |
.funnel-label, .funnel-value, | |
.funnel-cohort-label, .funnel-cohort-value, | |
.derived-column-value, .derived-column-change { | |
text-anchor: middle; | |
font-weight: bold; | |
font-size: 12pt; | |
} | |
.funnel-label, .funnel-value, | |
.funnel-cohort-label, .funnel-cohort-value { | |
fill: white; | |
} | |
.funnel-value { | |
font-size: 18pt; | |
} | |
.funnel-cohort-label { | |
fill: #cccccc; | |
} | |
.funnel-part-active circle { | |
fill: #00ACEE; | |
} | |
.funnel-part-outcome .funnel-part-cohort { | |
display: none; | |
} | |
.funnel-part-active .funnel-part-cohort { | |
display: initial; | |
} | |
.funnel-part-cohort circle { | |
fill: #333333; | |
} | |
.derived-column-value, .derived-column-change { | |
fill: #555555; | |
} | |
.arrow { | |
stroke: #00ACEE; | |
stroke-width: 0.20em; | |
marker-end: url(#arrowhead) | |
} | |
#arrowhead { | |
fill: #00ACEE; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Circle data set | |
var funnelColumnsData = [ | |
{ | |
nodes: [{ | |
r: 75, | |
label: 'Column A', | |
value: '12,345', | |
color: '#00ACEE', | |
nodes: [ | |
{ | |
label: "Your Stuff", | |
value: '1,234', | |
r: 34, | |
// `gap: 0` will make circles touch | |
// `gap: 10` puts 10px space between them | |
gap: 17, | |
// an explicit distance, regardless of radius, between two circles | |
// overrides `gap` setting | |
// distance: 131, | |
color: '#3333333', | |
rotation: d3.functor(-243) | |
} | |
] | |
}] | |
}, { | |
nodes: [{ | |
r: 150, | |
label: 'Column B', | |
value: '34,567,890', | |
color: '#00ACEE', | |
nodes: [ | |
{ | |
label: "Things from your stuff", | |
lines: ["Things from", "your stuff"], | |
value: '1,234', | |
r: 34, | |
// `gap: 0` will make circles touch | |
// `gap: 10` puts 10px space between them | |
gap: 17, | |
// an explicit distance, regardless of radius, between two circles | |
// overrides `gap` setting | |
// distance: 131, | |
color: '#00acee', | |
rotation: d3.functor(-243) | |
} | |
] | |
}] | |
}, { | |
isOutcome: true, | |
nodes: [{ | |
r: 66, | |
label: 'Outcome 1', | |
value: '12,345', | |
color: '#ccc', | |
nodes: [ | |
{ | |
label: "Clicks", | |
value: '1,234', | |
r: 25, | |
// `gap: 0` will make circles touch | |
// `gap: 10` puts 10px space between them | |
gap: 17, | |
// an explicit distance, regardless of radius, between two circles | |
// overrides `gap` setting | |
// distance: 131, | |
color: '#00acee', | |
rotation: d3.functor(-243) | |
} | |
] | |
}, { | |
r: 66, | |
label: 'Outcome 2', | |
value: '10,234', | |
color: '#ccc', | |
isActive: false, | |
nodes: [ | |
{ | |
label: "Things from your stuff", | |
lines: ["Things from", "your stuff"], | |
value: '1,234', | |
r: 30, | |
// `gap: 0` will make circles touch | |
// `gap: 10` puts 10px space between them | |
gap: 20, | |
// an explicit distance, regardless of radius, between two circles | |
// overrides `gap` setting | |
// distance: 131, | |
color: '#00acee', | |
rotation: d3.functor(132) | |
} | |
] | |
}, { | |
r: 66, | |
label: 'Outcome 3', | |
value: '9,012', | |
color: '#ccc', | |
isActive: true, | |
nodes: [ | |
{ | |
label: "RTs", | |
lines: ["More", "by you"], | |
value: '1,234', | |
r: 25, | |
// `gap: 0` will make circles touch | |
// `gap: 10` puts 10px space between them | |
gap: 17, | |
// an explicit distance, regardless of radius, between two circles | |
// overrides `gap` setting | |
// distance: 131, | |
color: '#00acee', | |
rotation: d3.functor(-243) | |
} | |
] | |
}] | |
}]; | |
var svg = d3.select('svg'); | |
var svgWidth = parseInt(svg.style('width'), 10); | |
var svgHeight = parseInt(svg.style('height'), 10); | |
// Apply the background color | |
svg.append('rect') | |
.attr({ | |
'class': 'funnel-background', | |
width: svgWidth, | |
height: svgHeight | |
}) | |
// definitions (only the arrowhead, atm) | |
svg.append('defs').selectAll('marker') | |
.data(['arrowhead']) | |
.enter() | |
.append('marker') | |
.attr({ | |
id: String, | |
viewBox: '0 -5 10 10', | |
refX: 8, | |
refY: 0, | |
markerWidth: 5, | |
markerHeight: 5, | |
orient: 'auto' | |
}) | |
.append('path') | |
.attr('d', 'M0,-5L10,0L0,5'); | |
// create the columns for each funnel part | |
var numCols = funnelColumnsData.length; | |
var colWidth = (svgWidth / numCols); | |
var colCenterX = colWidth / 2; | |
var colCenterY = svgHeight / 2; | |
// Generate the circles before the derived text | |
// so that the text will appear "above" them if they collide | |
var funnelColumns = svg.selectAll('g.column-funnel') | |
.data(funnelColumnsData, function (d, i) { | |
return d.nodes[0].label; | |
}) | |
.enter() | |
.append('g') | |
.attr({ | |
'class': 'column-funnel', | |
transform: funnelColumnTransform | |
}) | |
// iterate through the columns | |
// using `each` instead of continuing with the enter() chain | |
// because determining the rotation for the outcomes circles | |
// requires the index of both column and group | |
// e.g. col[2] and outcome[1] | |
funnelColumns.each(createFunnelParts); | |
// generate the derived columns | |
createDerivedColumns(); | |
////////////////////////////////////////////// | |
// lib functions | |
////////////////////////////////////////////// | |
function createFunnelParts(partData, partIndex) { | |
// create the column containers | |
var part = d3.select(this) | |
.selectAll('g.funnel-part') | |
.data(function (d, i) { | |
// we want the active outcome to appear "above" the others | |
// however, SVG doesn't have z-index so the last DOM node is "above" its siblings | |
// sort the nodes so that the active outcome is last | |
d.nodes.sort(activeChildrenLast); | |
d.nodes.forEach(generateRotationAccessors, d); | |
return d.nodes; | |
}, function (d, i) { | |
return d.label; | |
}) | |
part | |
.enter() | |
.append('g') | |
.classed('funnel-part', true) | |
.classed('funnel-part-outcome', partData.isOutcome) | |
.classed('funnel-part-active', pluck('isActive')) | |
.attr('transform', function (d, i){ | |
return d.transform(d, i); | |
}); | |
part | |
.each(addFunnelCircle) | |
.each(addFunnelText) | |
.each(createCohortParts) | |
} | |
function createCohortParts(d, i) { | |
if (!d.nodes) { | |
return; | |
} | |
var part = d3.select(this) | |
.selectAll('g.funnel-part-cohort') | |
.data(function (d, i) { | |
d.nodes.forEach(generateRotationAccessors, d); | |
return d.nodes; | |
}, function (d, i) { | |
return d.label; | |
}) | |
part | |
.enter() | |
.append('g') | |
.attr('class', 'funnel-part-cohort') | |
.attr('transform', function (d, i){ | |
return d.transform(d, i); | |
}); | |
part | |
.each(addFunnelCircle) | |
.each(addCohortText) | |
} | |
function addFunnelCircle() { | |
// append the circle | |
var circle = d3.select(this) | |
.append('circle') | |
.attr('r', pluck('r')) | |
.attr('fill', pluck('color')); | |
return circle; | |
} | |
function addFunnelText() { | |
var part = d3.select(this); | |
// Create the text for the labels | |
part | |
.append('text') | |
.attr({ | |
'class': 'funnel-label' | |
}) | |
.text(pluck('label')); | |
// Create the text for the values | |
part | |
.append('text') | |
.attr({ | |
'class': 'funnel-value', | |
dy: '1em' | |
}) | |
.text(pluck('value')) | |
return part | |
} | |
function addCohortText(d) { | |
var part = d3.select(this); | |
// Create the text for the values | |
part | |
.append('text') | |
.attr({ | |
'class': 'funnel-cohort-value', | |
dy: '0.5em' | |
}) | |
.text(pluck('value')) | |
// Create the text for the labels | |
var lines; | |
if (d.lines) { | |
lines = d.lines; | |
} else { | |
lines = [d.label] | |
} | |
for (var i = 0; i < lines.length; i++) { | |
var line = lines[i]; | |
var dy = i + 0.5 + 'em'; | |
part | |
.append('text') | |
.attr({ | |
'class': 'funnel-cohort-label', | |
dy: dy, | |
'transform': 'translate(' + (d.r * 2.5) + ')' | |
}) | |
.text(line); | |
} | |
return part | |
} | |
function createDerivedColumns() { | |
var lineGenerator = d3.svg.line() | |
.x(pluck('x')) | |
.y(pluck('y')) | |
.interpolate('linear'); | |
var derivedTextAttrs = { | |
length: derivedColumnWidth, | |
transform: function (d, i) { | |
var width = d3.select(this).attr('length'); | |
return 'translate(' + width/2 + ',0)'; | |
} | |
}; | |
// create the derived column data | |
var derivedColumnData = d3.range(0, numCols - 1).map(function (val, i) { | |
// we're not storing any info for the derived columns, atm | |
// but let's maintain the "data" part of Data Driven Documents | |
return { | |
// trying to force the data to be recognized as `enter`ing | |
key: (Date.now() + Math.random()).toString().replace('.', '') | |
}; | |
}); | |
var derivedCol = svg.selectAll('g.column-derived') | |
.data(derivedColumnData, function (d, i) { | |
return d.key; | |
}) | |
var derivedEnter = derivedCol | |
.enter() | |
.append('g') | |
.attr({ | |
'class': 'column-derived', | |
'transform': derivedColumnTransform | |
}) | |
// add the arrow | |
derivedEnter | |
.append('path') | |
.attr({ | |
'class': 'arrow', | |
d: function (d, i) { | |
return lineGenerator(lineData(d, i)); | |
} | |
}) | |
// text for the label above the line | |
derivedEnter | |
.append('text') | |
.attr(derivedTextAttrs) | |
.attr({ | |
'class': 'derived-column-value', | |
dy: '-1em' | |
}) | |
.text('Stuff between') | |
// text for the values below the line | |
derivedEnter | |
.append('text') | |
.attr(derivedTextAttrs) | |
.attr({ | |
'class': 'derived-column-change', | |
dy: '1.5em' | |
}) | |
.text(function (d, i) { | |
return ['columns', i+1, 'and', i+2].join(' ') | |
}) | |
} | |
function funnelColumnTransform(d, i) { | |
// translate in the column amount to reach the left column edge | |
var x = (colWidth * i); | |
// translate in some more to reach the center of the column | |
x += colCenterX; | |
var y = colCenterY; | |
return 'translate(' + x + ',' + y + ')'; | |
} | |
function derivedColumnPosition(d, i) { | |
var x0 = (colWidth * i) + colCenterX + funnelColumnsData[i].nodes[0].r; | |
var x1 = (colWidth * (i + 1)) + colCenterX - funnelColumnsData[i + 1].nodes[0].r; | |
return { | |
x0: x0, | |
x1: x1 | |
}; | |
} | |
function derivedColumnWidth(d, i) { | |
var x = derivedColumnPosition(d, i); | |
return x.x1 - x.x0 | |
} | |
function derivedColumnTransform(d, i) { | |
var x = derivedColumnPosition(d, i).x0; | |
var y = colCenterY; | |
return 'translate(' + x + ',' + y + ')'; | |
} | |
function lineData(d, i) { | |
var coords = [ | |
{ | |
x: 0, | |
y: 0 | |
}, { | |
x: derivedColumnWidth(d, i), | |
y: 0 | |
} | |
]; | |
return coords; | |
} | |
function activeChildrenLast(a, b) { | |
var aNum = +(a.isActive) || 0; | |
var bNum = +(b.isActive) || 0; | |
return aNum > bNum; | |
} | |
function pluck(key) { | |
return function keyAccessor(o) { | |
return o[key]; | |
}; | |
} | |
function deg2rad(deg) { | |
return deg * (Math.PI/180); | |
} | |
function translateDistanceAtAngle(r, angle) { | |
var x, y; | |
if (!angle || angle == 0) { | |
x = 0; | |
y = 0; | |
} else { | |
x = r * Math.cos(deg2rad(angle)); | |
y = r * Math.sin(deg2rad(angle)) | |
} | |
var transform = d3.transform('translate('+ x +','+ y +')'); | |
return transform; | |
} | |
function generateRotationAccessors(node,nodeIndex,nodes){ | |
// create a list of rotation angles for the circles | |
var slice = 360 / nodes.length | |
var rotations = d3.range(slice, 360, slice); | |
var rootNode = this; | |
if (!node.rotation) { | |
node.rotation = function rotation(d, i) { | |
return rotations[i]; | |
}; | |
} | |
if (rootNode.isOutcome) { | |
// siblings equally spaced around the same point | |
node.transform = function outcomeTransform(d, i) { | |
var distance = d.r; | |
// 0 is E (on a compass), we want it to be W | |
var angle = (d.rotation(d, i) || 0) - 180; | |
var transform = translateDistanceAtAngle(distance, angle); | |
// we don't want it centered on the column edge | |
if (d.isActive) { | |
transform.translate[0] += distance; | |
} else { | |
// move the inactive outcomes a bit more | |
transform.translate[0] += distance * 1.1; | |
} | |
return transform.toString(); | |
} | |
} else { | |
// offset d.distance OR combined length of both radiuses | |
node.transform = function(d, i) { | |
var planetRadius = (rootNode.r || 0); | |
var satelliteRadius = d.r || 25; | |
var gap = d.gap || 0; | |
var distance = d.distance || (planetRadius + satelliteRadius + gap); | |
var angle = d.rotation(d, i); | |
var transform = translateDistanceAtAngle(distance, angle); | |
return transform.toString(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment