Skip to content

Instantly share code, notes, and snippets.

@vijithassar
Last active November 12, 2016 22:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vijithassar/2b1bf939d3c5a4ae2a824f3973364363 to your computer and use it in GitHub Desktop.
Save vijithassar/2b1bf939d3c5a4ae2a824f3973364363 to your computer and use it in GitHub Desktop.
d3-parent example

Demonstration of d3-parent, a plugin that makes it easier to navigate around hierarchical selections and adds a more stable API around parentNode.

(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-selection')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-selection'], factory) :
(factory((global.d3 = global.d3 || {}),global.d3));
}(this, function (exports,d3Selection) { 'use strict';
var all_parents;
var direct_parent;
var closest_parent;
var single_parent;
var iterator;
var selector_to_nodes;
var parent;
// match all parents with linear iteration
all_parents = function(node, candidates) {
var candidate,
results,
i,
ilength;
results = [];
for (i = 0, ilength = candidates.length; i < ilength; i++) {
candidate = candidates[i];
if (candidate.contains(node)) {
results.push(candidate);
}
}
return results;
};
// return the immediate parentNode
direct_parent = function(node) {
if (node && node.parentNode) {
return node.parentNode;
} else {
return null;
}
};
// match by iterating upward through the DOM. this is an
// expensive operation and should be avoided when another
// method will suffice.
closest_parent = function(node, candidates) {
var results,
match,
end;
results = [];
// iterate upward from starting node
while (!match && !end && node.parentNode) {
// reassign node to its own parent
node = node.parentNode;
end = !node.parentNode;
// test selection for current iterator node
match = candidates.indexOf(node) !== -1;
}
// if there is a match, add it to the results
if (match) {
results.push(node);
}
return results;
};
// find exactly one parent node, optimizing iteration when possible
single_parent = function(node, candidates) {
var results,
result;
// try linear search first
results = all_parents(node, candidates);
// if linear search matches multiple candidates,
// retry hierarchically to find the single closest parent
if (results.length && results.length > 1) {
results = closest_parent(node, candidates);
}
// if the results are in array format, take the first
if (results.length > 0) {
result = results[0];
} else {
result = results;
}
return result;
};
// reformat a multidimensional array of nodes as a d3 selection
// optionally inserting a processing function along the way
iterator = function(input_selection, processor) {
var output_selection,
group,
node,
parent,
i,
ilength,
j,
jlength;
// create a new selection and copy the existing nodes
output_selection = d3Selection.selection();
output_selection._parents = input_selection._parents;
// loop through groups
for (i = 0, ilength = input_selection._groups.length; i < ilength; i++) {
group = input_selection._groups[i];
if (typeof output_selection._groups[i] === 'undefined') {
output_selection._groups[i] = [];
}
// loop through nodes
for (j = 0, jlength = group.length; j < jlength; j++) {
// process nodes
node = group[j];
if (node) {
parent = processor(node);
output_selection._groups[i][j] = parent;
}
}
}
return output_selection;
};
// fetch an array of matching nodes for a DOM selector string
selector_to_nodes = function(selector) {
var node_list,
nodes;
if (!selector) {
console.error('missing DOM selector string');
return;
}
node_list = document.querySelectorAll(selector);
// convert to array
nodes = Array.prototype.slice.call(node_list);
return nodes;
};
// public function to select single parent matching a selector
parent = function(selector) {
var candidates,
processor,
results;
if (!selector) {
processor = function(node) {
return direct_parent(node);
};
} else {
candidates = selector_to_nodes(selector);
processor = function(node) {
return single_parent(node, candidates);
};
}
results = iterator(this, processor);
return results;
};
var parent$1 = parent;
d3Selection.selection.prototype.parent = parent$1
exports.selection = d3Selection.selection;
exports.select = d3Selection.select;
Object.defineProperty(exports, '__esModule', { value: true });
}));
<html>
<head>
<title>d3-parent example</title>
</head>
<body>
<div class="target"></div>
<script type="text/javascript" src="https://d3js.org/d3.v4.min.js"></script>
<script type="text/javascript" src="./d3-parent.js"></script>
<script type="text/javascript" src="./magnetic-poetry.js"></script>
</body>
</html>
(function(d3) {
'use strict'
var svg,
wrapper,
height,
width,
increment,
x_increment,
y_increment,
highlight,
text,
menu,
sentence_count,
switcher,
mode,
modes,
drag,
move,
nest,
create_menu,
magnetic_poetry
// configuration values
width = 960
height = 480
increment = 15
x_increment = increment
y_increment = x_increment * 2
highlight = 'red'
modes = ['character', 'word', 'sentence', 'text']
mode = modes[0]
// behaviors
move = function() {
var transition,
target,
node,
nodes,
index,
word_offset
if (!mode) {
mode = modes[0]
}
node = this
target = d3.select(node).parent('g.' + mode)
nodes = d3.select(node)
.parent('g.sentence')
.selectAll('g.character')
.nodes()
index = nodes.indexOf(node)
word_offset = mode === 'word' ? index * x_increment * -1 : 0
target.transition().ease(d3.easeLinear).attr('transform', function() {
var parent,
container,
coordinates,
x,
y
if (mode === 'text') {
parent = 'wrapper'
} else {
parent = modes[modes.indexOf(mode) + 1]
}
container = d3.select(this).parent('g.' + parent).node()
coordinates = d3.mouse(container)
x = coordinates[0] + word_offset
y = coordinates[1]
return 'translate(' + x + ',' + y + ')'
})
}
drag = d3.drag().on('drag.text', move)
// set up dom
svg = d3.select('body')
.append('svg:svg')
.attr('height', height)
.attr('width', width)
wrapper = svg.append('g')
.classed('wrapper', true)
.attr('transform', 'translate(' + increment + ',' + increment + ')')
text = wrapper.append('g').classed('text', true)
// use a monospaced font to simplify position calculations
text
.style('font-family', 'Courier')
.style('text-anchor', 'middle')
.style('font-size', increment * 1.5)
.attr('transform', 'translate(' + increment * 5 + ',' + increment * 5 + ')')
menu = wrapper.append('g').classed('menu', true)
// toggle the menu options
switcher = function() {
svg.select('g.options').selectAll('text')
.on('mouseenter', function(d, i) {
svg.select('g.options').selectAll('text')
.style('fill', 'black')
d3.select(this)
.style('fill', highlight)
mode = d
})
}
// menu to toggle between operational modes
create_menu = function() {
var instruction,
options,
option,
label
menu = wrapper.append('g').classed('menu', true)
instruction = menu.append('g').classed('instruction', true)
instruction.append('text').text('move:')
options = menu.append('g').classed('options', true)
options.attr('transform', 'translate(' + increment + ',' + increment + ')')
option = options.selectAll('g.option')
.data(modes)
.enter()
.append('g')
.attr('class', function(d) {
return ['option', d].join(' ')
})
option.attr('transform', function(d, i) {
return 'translate(0,' + i * increment + ')'
})
label = option
.append('text')
label
.text(function(d) {
return d === 'text' ? 'all ' + d : d + 's'
})
options.selectAll('g.option.' + mode)
.style('fill', highlight)
}
// convert text into deeply nested arrays which can be bound
nest = function(sentences) {
var nested_sentences
nested_sentences = sentences.map(function(sentence) {
var words,
nested_words
words = sentence.split(' ')
nested_words = words.map(function(word) {
var characters
characters = word.split('')
return characters
})
return nested_words
})
return nested_sentences
}
// wrapper function to execute all text content
magnetic_poetry = function(sentences) {
var nested_sentences,
word_count,
character_count,
sentence,
word,
character,
handle
nested_sentences = nest(sentences)
sentence_count = d3.local()
word_count = d3.local()
character_count = d3.local()
sentence = text.selectAll('g.sentence')
.data(nested_sentences)
.enter()
.append('g')
.classed('sentence', true)
sentence.attr('transform', function(d, i) {
var y
y = i * y_increment
return 'translate(0,' + y + ')'
})
sentence.each(function(d, i) {
sentence_count.set(this, i)
character_count.set(this, 0)
})
word = sentence.selectAll('g.word')
.data(function(d) {
return d
})
.enter()
.append('g')
.classed('word', true)
word.each(function(d, i) {
word_count.set(this, i)
})
character = word.selectAll('g.character')
.data(function(d) {
return d
})
.enter()
.append('g')
.classed('character', true)
character.each(function(d, i, a, b) {
var sentence_node,
word_node,
current
sentence_node = d3.select(this).parent('.sentence').node()
word_node = d3.select(this).parent('.word').node()
current = character_count.get(sentence_node)
character_count.set(sentence_node, current + 1)
d3.select(this).attr('transform', function(d, i) {
var x_offset,
x
x_offset = character_count.get(sentence_node) + word_count.get(word_node)
x = x_offset * x_increment
return 'translate(' + x + ',0)'
})
})
character
.append('text')
.text(function(d) {
return d
})
handle = character.append('rect')
handle
.attr('height', function() {
return this.parentNode.getBoundingClientRect().height
})
.attr('width', function() {
return this.parentNode.getBoundingClientRect().width
})
.attr('x', x_increment * -1 * 0.5)
.attr('y', y_increment * -1 * 0.5)
.style('opacity', 0.0001)
character
.style('cursor', 'pointer')
.call(drag)
}
// fetch data file
d3.json('./sentences.json', function(error, results) {
if (error) {
console.error('one or more data files could not be found')
}
// render
create_menu()
switcher()
magnetic_poetry(results)
})
}).call(this, d3)
[
"It's like raaaaiiiiaaaaiiin on your wedding day",
"It's a freeeeee riiiiiide when you've already paid",
"It's the good advice that you just didn't take",
"Who would have thought, it figures"
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment