Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active July 28, 2016 00:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nitaku/d1c32b2e77120f560061 to your computer and use it in GitHub Desktop.
Save nitaku/d1c32b2e77120f560061 to your computer and use it in GitHub Desktop.
Arc Diagram II

A new take on a previous example about arc diagrams, showing character patterns in an Italian tongue-twister.

As in Wattenberg's original definition, only essential matching pairs are shown as arcs, to reduce visual clutter. Clicking on an arc highlights all the pairs having the same subsequence of characters.

Whitespaces are ignored by using a character replacement technique prior to the application of the matching algorithm.

Data is computed offline with a python tool by Oliver Mader, which implements a different version of Wattenberg's original algorithm.

sequence = 'Sul tagliere gli agli taglia, non tagliare la tovaglia: la tovaglia non è aglio e tagliarla è un grave sbaglio'
matches = [{'length': 8, 'source': 46, 'subsequence': 'tovaglia', 'target': 59},
{'length': 7, 'source': 34, 'subsequence': 'tagliar', 'target': 82},
{'length': 6, 'source': 22, 'subsequence': 'taglia', 'target': 34},
{'length': 5, 'source': 74, 'subsequence': 'aglio', 'target': 105},
{'length': 5, 'source': 4, 'subsequence': 'tagli', 'target': 22},
{'length': 5, 'source': 35, 'subsequence': 'aglia', 'target': 49},
{'length': 5, 'source': 62, 'subsequence': 'aglia', 'target': 83},
{'length': 4, 'source': 5, 'subsequence': 'agli', 'target': 17},
{'length': 4, 'source': 17, 'subsequence': 'agli', 'target': 23},
{'length': 4, 'source': 62, 'subsequence': 'agli', 'target': 74},
{'length': 4, 'source': 74, 'subsequence': 'agli', 'target': 83},
{'length': 4, 'source': 83, 'subsequence': 'agli', 'target': 105},
{'length': 3, 'source': 30, 'subsequence': 'non', 'target': 68},
{'length': 3, 'source': 6, 'subsequence': 'gli', 'target': 13},
{'length': 3, 'source': 13, 'subsequence': 'gli', 'target': 18},
{'length': 2, 'source': 43, 'subsequence': 'la', 'target': 56},
{'length': 2, 'source': 56, 'subsequence': 'la', 'target': 89},
{'length': 2, 'source': 10, 'subsequence': 're', 'target': 40}]
svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height
# translate the viewBox to have (0,0) at the center of the vis
svg
.attr
viewBox: "#{-width/2} #{-height+60} #{width} #{height}"
width -= 60
x = d3.scale.ordinal()
.domain([0..sequence.length])
.rangeBands([-width/2,width/2])
characters = svg.selectAll('.character')
.data(sequence)
characters.enter().append('text')
.text (d) -> d
.attr
class: 'character'
x: (d,i) -> x(i) + x.rangeBand()/2
dy: '1em'
arcs = svg.selectAll('.arc')
.data(matches.filter (m) -> m.length > 1)
enter_arcs = arcs.enter().append('path')
.attr
class: 'arc'
d: (match) ->
start_left = x(match.source)
start_right = x(match.source + match.length)
end_left = x(match.target)
end_right = x(match.target + match.length)
big_radius = (end_right-start_left)/2
small_radius = (end_left-start_right)/2
return "M#{start_left} 0 A#{big_radius} #{big_radius} 0 1 1 #{end_right} 0 L#{end_left} 0 A#{small_radius} #{small_radius} 0 1 0 #{start_right} 0 z"
# subsequence selection
subsequence = svg.append 'text'
.attr
class: 'subsequence'
y: -350
enter_arcs.on 'click', (d1) ->
subsequence
.text d1.subsequence
svg.selectAll('.arc')
.classed 'selected', (d2) -> d1.subsequence is d2.subsequence
.classed 'dimmed', (d2) -> d1.subsequence isnt d2.subsequence
d3.event.stopPropagation()
svg.on 'click', () ->
subsequence
.text ''
svg.selectAll('.arc')
.classed 'selected', false
.classed 'dimmed', false
body, html {
padding: 0;
margin: 0;
height: 100%;
}
svg {
width: 100%;
height: 100%;
background: white;
}
.character {
text-anchor: middle;
font-size: 16px;
font-family: Georgia;
}
.arc {
fill-opacity: 0.2;
cursor: pointer;
}
.arc.selected {
fill: steelblue;
fill-opacity: 0.4;
}
.arc.dimmed {
fill-opacity: 0.07;
}
.subsequence {
fill: steelblue;
text-anchor: middle;
font-size: 40px;
font-family: Georgia;
text-transform: uppercase;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Arc Diagram II</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<svg></svg>
<script src="index.js"></script>
</body>
</html>
// Generated by CoffeeScript 1.10.0
(function() {
var arcs, characters, enter_arcs, height, j, matches, ref, results, sequence, subsequence, svg, width, x;
sequence = 'Sul tagliere gli agli taglia, non tagliare la tovaglia: la tovaglia non è aglio e tagliarla è un grave sbaglio';
matches = [
{
'length': 8,
'source': 46,
'subsequence': 'tovaglia',
'target': 59
}, {
'length': 7,
'source': 34,
'subsequence': 'tagliar',
'target': 82
}, {
'length': 6,
'source': 22,
'subsequence': 'taglia',
'target': 34
}, {
'length': 5,
'source': 74,
'subsequence': 'aglio',
'target': 105
}, {
'length': 5,
'source': 4,
'subsequence': 'tagli',
'target': 22
}, {
'length': 5,
'source': 35,
'subsequence': 'aglia',
'target': 49
}, {
'length': 5,
'source': 62,
'subsequence': 'aglia',
'target': 83
}, {
'length': 4,
'source': 5,
'subsequence': 'agli',
'target': 17
}, {
'length': 4,
'source': 17,
'subsequence': 'agli',
'target': 23
}, {
'length': 4,
'source': 62,
'subsequence': 'agli',
'target': 74
}, {
'length': 4,
'source': 74,
'subsequence': 'agli',
'target': 83
}, {
'length': 4,
'source': 83,
'subsequence': 'agli',
'target': 105
}, {
'length': 3,
'source': 30,
'subsequence': 'non',
'target': 68
}, {
'length': 3,
'source': 6,
'subsequence': 'gli',
'target': 13
}, {
'length': 3,
'source': 13,
'subsequence': 'gli',
'target': 18
}, {
'length': 2,
'source': 43,
'subsequence': 'la',
'target': 56
}, {
'length': 2,
'source': 56,
'subsequence': 'la',
'target': 89
}, {
'length': 2,
'source': 10,
'subsequence': 're',
'target': 40
}
];
svg = d3.select('svg');
width = svg.node().getBoundingClientRect().width;
height = svg.node().getBoundingClientRect().height;
svg.attr({
viewBox: (-width / 2) + " " + (-height + 60) + " " + width + " " + height
});
width -= 60;
x = d3.scale.ordinal().domain((function() {
results = [];
for (var j = 0, ref = sequence.length; 0 <= ref ? j <= ref : j >= ref; 0 <= ref ? j++ : j--){ results.push(j); }
return results;
}).apply(this)).rangeBands([-width / 2, width / 2]);
characters = svg.selectAll('.character').data(sequence);
characters.enter().append('text').text(function(d) {
return d;
}).attr({
"class": 'character',
x: function(d, i) {
return x(i) + x.rangeBand() / 2;
},
dy: '1em'
});
arcs = svg.selectAll('.arc').data(matches.filter(function(m) {
return m.length > 1;
}));
enter_arcs = arcs.enter().append('path').attr({
"class": 'arc',
d: function(match) {
var big_radius, end_left, end_right, small_radius, start_left, start_right;
start_left = x(match.source);
start_right = x(match.source + match.length);
end_left = x(match.target);
end_right = x(match.target + match.length);
big_radius = (end_right - start_left) / 2;
small_radius = (end_left - start_right) / 2;
return "M" + start_left + " 0 A" + big_radius + " " + big_radius + " 0 1 1 " + end_right + " 0 L" + end_left + " 0 A" + small_radius + " " + small_radius + " 0 1 0 " + start_right + " 0 z";
}
});
subsequence = svg.append('text').attr({
"class": 'subsequence',
y: -350
});
enter_arcs.on('click', function(d1) {
subsequence.text(d1.subsequence);
svg.selectAll('.arc').classed('selected', function(d2) {
return d1.subsequence === d2.subsequence;
}).classed('dimmed', function(d2) {
return d1.subsequence !== d2.subsequence;
});
return d3.event.stopPropagation();
});
svg.on('click', function() {
subsequence.text('');
return svg.selectAll('.arc').classed('selected', false).classed('dimmed', false);
});
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment