Skip to content

Instantly share code, notes, and snippets.

@nbremer
Last active January 15, 2023 14:27
Show Gist options
  • Save nbremer/864b11eb83aac3a1f6a2 to your computer and use it in GitHub Desktop.
Save nbremer/864b11eb83aac3a1f6a2 to your computer and use it in GitHub Desktop.
Data based orientations in SVG Gradients - Final example - Avenger Movie Collaborations
height: 1000
license: mit

This Chord diagram shows the final result of the example used in my blog on Data-based orientations for gradients in a d3.js Chord Diagram

What you see here are movie collaborations between the Avengers in the MCU (up until & including Thor Ragnarok). Because collaborations are symmetrical, the chords are all as thick at the beginnen as at the end. Therefore, I wanted each chord to represent both the Avengers that it connects through a gradient. Because all the chords run in different directions, I had to set up the orientation of each gradient by using the chord.chords() dataset

In this end result I've made the Avenger names wrap around the arcs (as explained in another blog on Placing Texts on Arcs with D3.js ) and added a simple tooltip on a hover

You can find the steps leading up to this one here

/* ========================================================================
* Bootstrap: popover.js v3.1.1
* http://getbootstrap.com/javascript/#popovers
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// POPOVER PUBLIC CLASS DEFINITION
// ===============================
var Popover = function (element, options) {
this.init('popover', element, options)
}
if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, {
placement: 'right',
trigger: 'click',
content: '',
template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
})
// NOTE: POPOVER EXTENDS tooltip.js
// ================================
Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype)
Popover.prototype.constructor = Popover
Popover.prototype.getDefaults = function () {
return Popover.DEFAULTS
}
Popover.prototype.setContent = function () {
var $tip = this.tip()
var title = this.getTitle()
var content = this.getContent()
$tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
$tip.find('.popover-content')[ // we use append for html objects to maintain js events
this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'
](content)
$tip.removeClass('fade top bottom left right in')
// IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do
// this manually by checking the contents.
if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide()
}
Popover.prototype.hasContent = function () {
return this.getTitle() || this.getContent()
}
Popover.prototype.getContent = function () {
var $e = this.$element
var o = this.options
return $e.attr('data-content')
|| (typeof o.content == 'function' ?
o.content.call($e[0]) :
o.content)
}
Popover.prototype.arrow = function () {
return this.$arrow = this.$arrow || this.tip().find('.arrow')
}
Popover.prototype.tip = function () {
if (!this.$tip) this.$tip = $(this.options.template)
return this.$tip
}
// POPOVER PLUGIN DEFINITION
// =========================
var old = $.fn.popover
$.fn.popover = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.popover')
var options = typeof option == 'object' && option
if (!data && option == 'destroy') return
if (!data) $this.data('bs.popover', (data = new Popover(this, options)))
if (typeof option == 'string') data[option]()
})
}
$.fn.popover.Constructor = Popover
// POPOVER NO CONFLICT
// ===================
$.fn.popover.noConflict = function () {
$.fn.popover = old
return this
}
}(jQuery);
/* ========================================================================
* Bootstrap: tooltip.js v3.1.1
* http://getbootstrap.com/javascript/#tooltip
* Inspired by the original jQuery.tipsy by Jason Frame
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// TOOLTIP PUBLIC CLASS DEFINITION
// ===============================
var Tooltip = function (element, options) {
this.type =
this.options =
this.enabled =
this.timeout =
this.hoverState =
this.$measure =
this.$element = null
this.init('tooltip', element, options)
}
Tooltip.DEFAULTS = {
animation: true,
mouseOffset: 0,
measure: false,
followMouse: false,
placement: 'top',
selector: false,
template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
trigger: 'hover focus',
title: '',
delay: 0,
html: false,
container: false
}
Tooltip.prototype.init = function (type, element, options) {
this.enabled = true
this.type = type
this.$element = $(element)
this.options = this.getOptions(options)
if (this.options.measure) {
this.$measure = $(this.options.measure)
}
var triggers = this.options.trigger.split(' ')
for (var i = triggers.length; i--;) {
var trigger = triggers[i]
if (trigger == 'click') {
this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
} else if (trigger != 'manual') {
var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin'
var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'
this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
}
}
this.options.selector ?
(this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
this.fixTitle()
}
Tooltip.prototype.getDefaults = function () {
return Tooltip.DEFAULTS
}
Tooltip.prototype.getOptions = function (options) {
options = $.extend({}, this.getDefaults(), this.$element.data(), options)
if (options.delay && typeof options.delay == 'number') {
options.delay = {
show: options.delay,
hide: options.delay
}
}
return options
}
Tooltip.prototype.getDelegateOptions = function () {
var options = {}
var defaults = this.getDefaults()
this._options && $.each(this._options, function (key, value) {
if (defaults[key] != value) options[key] = value
})
return options
}
Tooltip.prototype.enter = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
clearTimeout(self.timeout)
self.hoverState = 'in'
if (!self.options.delay || !self.options.delay.show) return self.show()
self.timeout = setTimeout(function () {
if (self.hoverState == 'in') self.show()
}, self.options.delay.show)
}
Tooltip.prototype.leave = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
clearTimeout(self.timeout)
self.hoverState = 'out'
if (!self.options.delay || !self.options.delay.hide) return self.hide()
self.timeout = setTimeout(function () {
if (self.hoverState == 'out') self.hide()
}, self.options.delay.hide)
}
Tooltip.prototype.show = function () {
var e = $.Event('show.bs.' + this.type)
if (this.hasContent() && this.enabled) {
this.$element.trigger(e)
if (e.isDefaultPrevented()) return
var that = this;
var $tip = this.tip()
this.setContent()
if (this.options.animation) $tip.addClass('fade')
var placement = typeof this.options.placement == 'function' ?
this.options.placement.call(this, $tip[0], this.$element[0]) :
this.options.placement
var autoToken = /\s?auto?\s?/i
var autoPlace = autoToken.test(placement)
if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
$tip
.detach()
.css({ top: 0, left: 0, display: 'block' })
.addClass(placement)
this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
var pos = this.getPosition()
var actualWidth = $tip[0].offsetWidth
var actualHeight = $tip[0].offsetHeight
if (autoPlace) {
placement = this.getAutoPlace($tip, placement, pos, actualWidth, actualHeight)
}
var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
this.applyPlacement(calculatedOffset, placement)
this.hoverState = null
var complete = function() {
that.$element.trigger('shown.bs.' + that.type)
}
if (this.options.followMouse) {
$(this.options.container).on('mousemove', $.proxy(this.follow, this));
}
$.support.transition && this.$tip.hasClass('fade') ?
$tip
.one($.support.transition.end, complete)
.emulateTransitionEnd(150) :
complete()
}
}
Tooltip.prototype.getAutoPlace = function($tip, placement, pos, actualWidth, actualHeight) {
var $parent = this.$element.parent()
var orgPlacement = placement
var docScroll = document.documentElement.scrollTop || document.body.scrollTop
var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth()
var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight()
var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left
placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' :
placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' :
placement == 'right' && pos.left + actualWidth > parentWidth ? 'left' :
placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' :
placement
$tip
.removeClass(orgPlacement)
.addClass(placement)
return placement
}
Tooltip.prototype.follow = function(e) {
var $tip = this.tip()
var pos = this.getPosition()
var actualWidth = $tip[0].offsetWidth
var actualHeight = $tip[0].offsetHeight
var placement = typeof this.options.placement == 'function' ?
this.options.placement.call(this, $tip[0], this.$element[0]) :
this.options.placement
var autoToken = /\s?auto?\s?/i
var autoPlace = autoToken.test(placement)
if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
pos.left = e.pageX + this.options.mouseOffset;
pos.top = e.pageY + this.options.mouseOffset;
var placement = this.getAutoPlace($tip, placement, pos, actualWidth, actualHeight)
var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight, e)
this.applyPlacement(calculatedOffset, placement)
}
Tooltip.prototype.applyPlacement = function (offset, placement) {
var replace
var $tip = this.tip()
var width = $tip[0].offsetWidth
var height = $tip[0].offsetHeight
// manually read margins because getBoundingClientRect includes difference
var marginTop = parseInt($tip.css('margin-top'), 10)
var marginLeft = parseInt($tip.css('margin-left'), 10)
// we must check for NaN for ie 8/9
if (isNaN(marginTop)) marginTop = 0
if (isNaN(marginLeft)) marginLeft = 0
offset.top = offset.top + marginTop
offset.left = offset.left + marginLeft
// $.fn.offset doesn't round pixel values
// so we use setOffset directly with our own function B-0
$.offset.setOffset($tip[0], $.extend({
using: function (props) {
$tip.css({
top: Math.round(props.top),
left: Math.round(props.left)
})
}
}, offset), 0)
$tip.addClass('in')
// check to see if placing tip in new offset caused the tip to resize itself
var actualWidth = $tip[0].offsetWidth
var actualHeight = $tip[0].offsetHeight
if (placement == 'top' && actualHeight != height) {
replace = true
offset.top = offset.top + height - actualHeight
}
if (/bottom|top/.test(placement)) {
var delta = 0
if (offset.left < 0) {
delta = offset.left * -2
offset.left = 0
$tip.offset(offset)
actualWidth = $tip[0].offsetWidth
actualHeight = $tip[0].offsetHeight
}
this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
} else {
this.replaceArrow(actualHeight - height, actualHeight, 'top')
}
if (replace) $tip.offset(offset)
}
Tooltip.prototype.replaceArrow = function (delta, dimension, position) {
if(this.options.mouseOffset > 0 && position === 'top') {
this.arrow().css(position, this.options.mouseOffset + 10 + 'px')
return;
}
this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '')
}
Tooltip.prototype.setContent = function () {
var $tip = this.tip()
var title = this.getTitle()
$tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
$tip.removeClass('fade in top bottom left right')
}
Tooltip.prototype.hide = function () {
var that = this
var $tip = this.tip()
var e = $.Event('hide.bs.' + this.type)
function complete() {
if (that.hoverState != 'in') $tip.detach()
that.$element.trigger('hidden.bs.' + that.type)
}
$(this.options.container).off('mousemove');
this.$element.trigger(e)
if (e.isDefaultPrevented()) return
$tip.removeClass('in')
$.support.transition && this.$tip.hasClass('fade') ?
$tip
.one($.support.transition.end, complete)
.emulateTransitionEnd(150) :
complete()
this.hoverState = null
return this
}
Tooltip.prototype.fixTitle = function () {
var $e = this.$element
if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
$e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
}
}
Tooltip.prototype.hasContent = function () {
return this.getTitle()
}
Tooltip.prototype.getPosition = function () {
var el = this.$element[0]
if(this.$element.prop('tagName') == 'AREA') {
var position = this.$element.attr('coords').split(',');
var x = parseInt(position[0], 0) + parseInt(this.$measure.offset().left, 0);
var y = parseInt(position[1], 0) + parseInt(this.$measure.offset().top, 0);
return {bottom: 0, height: el.offsetWidth, left: x, right: 0, top: y, width: el.offsetWidth}
}
return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
width: el.offsetWidth,
height: el.offsetHeight
}, this.$element.offset())
}
Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight, follow) {
if (typeof follow !== 'undefined') {
return placement == 'bottom' ? { top: follow.pageY + this.options.mouseOffset, left: follow.pageX - (actualWidth / 2) } :
placement == 'top' ? { top: follow.pageY - actualHeight - this.options.mouseOffset, left: follow.pageX - (actualWidth / 2) } :
placement == 'left' ? { top: follow.pageY - this.options.mouseOffset, left: follow.pageX - (actualWidth + this.options.mouseOffset) } :
{ top: follow.pageY - this.options.mouseOffset, left: follow.pageX + this.options.mouseOffset }
}
return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
{ top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
}
Tooltip.prototype.getTitle = function () {
var title
var $e = this.$element
var o = this.options
title = $e.attr('data-original-title')
|| (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
return title
}
Tooltip.prototype.tip = function () {
return this.$tip = this.$tip || $(this.options.template)
}
Tooltip.prototype.arrow = function () {
return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')
}
Tooltip.prototype.validate = function () {
if (!this.$element[0].parentNode) {
this.hide()
this.$element = null
this.options = null
}
}
Tooltip.prototype.enable = function () {
this.enabled = true
}
Tooltip.prototype.disable = function () {
this.enabled = false
}
Tooltip.prototype.toggleEnabled = function () {
this.enabled = !this.enabled
}
Tooltip.prototype.toggle = function (e) {
var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this
self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
}
Tooltip.prototype.destroy = function () {
clearTimeout(this.timeout)
this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
}
// TOOLTIP PLUGIN DEFINITION
// =========================
var old = $.fn.tooltip
$.fn.tooltip = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.tooltip')
var options = typeof option == 'object' && option
if (!data && option == 'destroy') return
if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
if (typeof option == 'string') data[option]()
})
}
$.fn.tooltip.Constructor = Tooltip
// TOOLTIP NO CONFLICT
// ===================
$.fn.tooltip.noConflict = function () {
$.fn.tooltip = old
return this
}
}(jQuery);
////////////////////////////////////////////////////////////
//////////// Custom Chord Layout Function //////////////////
/////// Places the Chords in the visually best order ///////
///////////////// to reduce overlap ////////////////////////
////////////////////////////////////////////////////////////
//////// Slightly adjusted by Nadieh Bremer ////////////////
//////////////// VisualCinnamon.com ////////////////////////
////////////////////////////////////////////////////////////
////// Original from the d3.layout.chord() function ////////
///////////////// from the d3.js library ///////////////////
//////////////// Created by Mike Bostock ///////////////////
////////////////////////////////////////////////////////////
customChordLayout = function() {
var ε = 1e-6, ε2 = ε * ε, π = Math.PI, τ = 2 * π, τε = τ - ε, halfπ = π / 2, d3_radians = π / 180, d3_degrees = 180 / π;
var chord = {}, chords, groups, matrix, n, padding = 0, sortGroups, sortSubgroups, sortChords;
function relayout() {
var subgroups = {}, groupSums = [], groupIndex = d3.range(n), subgroupIndex = [], k, x, x0, i, j;
var numSeq;
chords = [];
groups = [];
k = 0, i = -1;
while (++i < n) {
x = 0, j = -1, numSeq = [];
while (++j < n) {
x += matrix[i][j];
}
groupSums.push(x);
//////////////////////////////////////
////////////// New part //////////////
//////////////////////////////////////
for(var m = 0; m < n; m++) {
numSeq[m] = (n+(i-1)-m)%n;
}
subgroupIndex.push(numSeq);
//////////////////////////////////////
////////// End new part /////////////
//////////////////////////////////////
k += x;
}//while
k = (τ - padding * n) / k;
x = 0, i = -1;
while (++i < n) {
x0 = x, j = -1;
while (++j < n) {
var di = groupIndex[i], dj = subgroupIndex[di][j], v = matrix[di][dj], a0 = x, a1 = x += v * k;
subgroups[di + "-" + dj] = {
index: di,
subindex: dj,
startAngle: a0,
endAngle: a1,
value: v
};
}//while
groups[di] = {
index: di,
startAngle: x0,
endAngle: x,
value: (x - x0) / k
};
x += padding;
}//while
i = -1;
while (++i < n) {
j = i - 1;
while (++j < n) {
var source = subgroups[i + "-" + j], target = subgroups[j + "-" + i];
if (source.value || target.value) {
chords.push(source.value < target.value ? {
source: target,
target: source
} : {
source: source,
target: target
});
}//if
}//while
}//while
if (sortChords) resort();
}//function relayout
function resort() {
chords.sort(function(a, b) {
return sortChords((a.source.value + a.target.value) / 2, (b.source.value + b.target.value) / 2);
});
}
chord.matrix = function(x) {
if (!arguments.length) return matrix;
n = (matrix = x) && matrix.length;
chords = groups = null;
return chord;
};
chord.padding = function(x) {
if (!arguments.length) return padding;
padding = x;
chords = groups = null;
return chord;
};
chord.sortGroups = function(x) {
if (!arguments.length) return sortGroups;
sortGroups = x;
chords = groups = null;
return chord;
};
chord.sortSubgroups = function(x) {
if (!arguments.length) return sortSubgroups;
sortSubgroups = x;
chords = null;
return chord;
};
chord.sortChords = function(x) {
if (!arguments.length) return sortChords;
sortChords = x;
if (chords) resort();
return chord;
};
chord.chords = function() {
if (!chords) relayout();
return chords;
};
chord.groups = function() {
if (!groups) relayout();
return groups;
};
return chord;
};
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Collaborations between MCU Avengers</title>
<!-- D3.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<!-- Google Fonts -->
<link href='https://fonts.googleapis.com/css?family=Bangers' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Oswald' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Lato:400,900' rel='stylesheet' type='text/css'>
<!-- jQuery -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<!-- Bootstrap -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<!-- Two scripts to make the Bootstrap tooltip follow the mouse movement
taken from https://github.com/ghophp/bootstrap-popover-move -->
<script src="bootstrap.tooltip.js"></script>
<script src="bootstrap.popover.js"></script>
<script src="d3.layout.chord.sort.js"></script>
<style>
body {
font-size: 14px;
font-family: 'Lato', sans-serif;
text-align: left;
color: #757575;
cursor: default;
}
.title {
margin-top: 20px;
margin-bottom: 10px;
margin-left: 20px;
font-size:32px;
font-family: 'Oswald', sans-serif;
color: #2B2B2B;
}
.texts {
margin-left: 20px;
margin-right: 20px;
line-height: 140%;
}
.credit {
color: #9E9E9E;
font-size: 10px;
margin-bottom: 0.5em;
}
.notes {
color: #9E9E9E;
font-size: 10px;
}
.popover {
pointer-events: none;
}
#chart{
font-size: 16px;
font-family: 'Bangers', sans-serif;
text-align: center;
fill: #2B2B2B;
}
@media (min-width: 600px) {
#chart{
font-size: 20px;
}
}
</style>
</head>
<body>
<div class = "title">Movie collaborations between Avengers in the MCU</div>
<div class = "explanation texts">
<p style="margin-bottom: 0em;">The visualization below shows how often the original Avengers have appeared in MCU movies together since Iron Man
in 2008 and counting up to Thor: Ragnarok scheduled in 2017. The thicker a line, the more movies in which both characters appeared.</p>
<p style="margin-top: 0.5em;">Because all six have appeared in both Avengers movies, the minimum number of co-appearances is 2.
Iron Man and Black Widow are the Avengers who have appeared most often together, with 5 movies.</p></div>
<div id = "chart"></div>
<div class="credit texts">Created by Nadieh Bremer | <a href="http://www.visualcinnamon.com/">VisualCinnamon.com</a></div>
<div class = "notes texts">
The 12 MCU movies taken into account are: Iron Man, Iron Man 2, Iron Man 3, Captain America: The First Avenger, Captain America: The Winter Soldier,
Captain America: Civil War, Thor, Thor: The Dark World, Thor: Ragnarok, The Incredible Hulk, The Avengers & Avengers: Age of Ultron</div>
<script src="script.js"></script>
</body>
</html>
////////////////////////////////////////////////////////////
//////////////////////// Set-Up ////////////////////////////
////////////////////////////////////////////////////////////
var margin = {left:20, top:20, right:20, bottom:20},
width = Math.min(window.innerWidth, 700) - margin.left - margin.right,
height = Math.min(window.innerWidth, 700) - margin.top - margin.bottom,
innerRadius = Math.min(width, height) * .39,
outerRadius = innerRadius * 1.1;
var Names = ["Black Widow","Captain America","Hawkeye","the Hulk","Iron Man","Thor"],
colors = ["#301E1E", "#083E77", "#342350", "#567235", "#8B161C", "#DF7C00"],
opacityDefault = 0.8;
var matrix = [
[0,4,3,2,5,2], //Black Widow
[4,0,3,2,4,3], //Captain America
[3,3,0,2,3,3], //Hawkeye
[2,2,2,0,3,3], //The Hulk
[5,4,3,3,0,2], //Iron Man
[2,3,3,3,2,0], //Thor
];
////////////////////////////////////////////////////////////
/////////// Create scale and layout functions //////////////
////////////////////////////////////////////////////////////
var colors = d3.scale.ordinal()
.domain(d3.range(Names.length))
.range(colors);
//A "custom" d3 chord function that automatically sorts the order of the chords in such a manner to reduce overlap
var chord = customChordLayout()
.padding(.15)
.sortChords(d3.descending) //which chord should be shown on top when chords cross. Now the biggest chord is at the bottom
.matrix(matrix);
var arc = d3.svg.arc()
.innerRadius(innerRadius*1.01)
.outerRadius(outerRadius);
var path = d3.svg.chord()
.radius(innerRadius);
////////////////////////////////////////////////////////////
////////////////////// Create SVG //////////////////////////
////////////////////////////////////////////////////////////
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + (width/2 + margin.left) + "," + (height/2 + margin.top) + ")");
////////////////////////////////////////////////////////////
/////////////// Create the gradient fills //////////////////
////////////////////////////////////////////////////////////
//Function to create the id for each chord gradient
function getGradID(d){ return "linkGrad-" + d.source.index + "-" + d.target.index; }
//Create the gradients definitions for each chord
var grads = svg.append("defs").selectAll("linearGradient")
.data(chord.chords())
.enter().append("linearGradient")
.attr("id", getGradID)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", function(d,i) { return innerRadius * Math.cos((d.source.endAngle-d.source.startAngle)/2 + d.source.startAngle - Math.PI/2); })
.attr("y1", function(d,i) { return innerRadius * Math.sin((d.source.endAngle-d.source.startAngle)/2 + d.source.startAngle - Math.PI/2); })
.attr("x2", function(d,i) { return innerRadius * Math.cos((d.target.endAngle-d.target.startAngle)/2 + d.target.startAngle - Math.PI/2); })
.attr("y2", function(d,i) { return innerRadius * Math.sin((d.target.endAngle-d.target.startAngle)/2 + d.target.startAngle - Math.PI/2); })
//Set the starting color (at 0%)
grads.append("stop")
.attr("offset", "0%")
.attr("stop-color", function(d){ return colors(d.source.index); });
//Set the ending color (at 100%)
grads.append("stop")
.attr("offset", "100%")
.attr("stop-color", function(d){ return colors(d.target.index); });
////////////////////////////////////////////////////////////
////////////////// Draw outer Arcs /////////////////////////
////////////////////////////////////////////////////////////
var outerArcs = svg.selectAll("g.group")
.data(chord.groups)
.enter().append("g")
.attr("class", "group")
.on("mouseover", fade(.1))
.on("mouseout", fade(opacityDefault));
outerArcs.append("path")
.style("fill", function(d) { return colors(d.index); })
.attr("d", arc)
.each(function(d,i) {
//Search pattern for everything between the start and the first capital L
var firstArcSection = /(^.+?)L/;
//Grab everything up to the first Line statement
var newArc = firstArcSection.exec( d3.select(this).attr("d") )[1];
//Replace all the comma's so that IE can handle it
newArc = newArc.replace(/,/g , " ");
//If the end angle lies beyond a quarter of a circle (90 degrees or pi/2)
//flip the end and start position
if (d.endAngle > 90*Math.PI/180 & d.startAngle < 270*Math.PI/180) {
var startLoc = /M(.*?)A/, //Everything between the first capital M and first capital A
middleLoc = /A(.*?)0 0 1/, //Everything between the first capital A and 0 0 1
endLoc = /0 0 1 (.*?)$/; //Everything between the first 0 0 1 and the end of the string (denoted by $)
//Flip the direction of the arc by switching the start en end point (and sweep flag)
//of those elements that are below the horizontal line
var newStart = endLoc.exec( newArc )[1];
var newEnd = startLoc.exec( newArc )[1];
var middleSec = middleLoc.exec( newArc )[1];
//Build up the new arc notation, set the sweep-flag to 0
newArc = "M" + newStart + "A" + middleSec + "0 0 0 " + newEnd;
}//if
//Create a new invisible arc that the text can flow along
svg.append("path")
.attr("class", "hiddenArcs")
.attr("id", "arc"+i)
.attr("d", newArc)
.style("fill", "none");
});
////////////////////////////////////////////////////////////
////////////////// Append Names ////////////////////////////
////////////////////////////////////////////////////////////
//Append the label names on the outside
outerArcs.append("text")
.attr("class", "titles")
.attr("dy", function(d,i) { return (d.endAngle > 90*Math.PI/180 & d.startAngle < 270*Math.PI/180 ? 25 : -16); })
.append("textPath")
.attr("startOffset","50%")
.style("text-anchor","middle")
.attr("xlink:href",function(d,i){return "#arc"+i;})
.text(function(d,i){ return Names[i]; });
////////////////////////////////////////////////////////////
////////////////// Draw inner chords ///////////////////////
////////////////////////////////////////////////////////////
svg.selectAll("path.chord")
.data(chord.chords)
.enter().append("path")
.attr("class", "chord")
.style("fill", function(d){ return "url(#" + getGradID(d) + ")"; })
.style("opacity", opacityDefault)
.attr("d", path)
.on("mouseover", mouseoverChord)
.on("mouseout", mouseoutChord);
////////////////////////////////////////////////////////////
////////////////// Extra Functions /////////////////////////
////////////////////////////////////////////////////////////
//Returns an event handler for fading a given chord group.
function fade(opacity) {
return function(d,i) {
svg.selectAll("path.chord")
.filter(function(d) { return d.source.index !== i && d.target.index !== i; })
.transition()
.style("opacity", opacity);
};
}//fade
//Highlight hovered over chord
function mouseoverChord(d,i) {
//Decrease opacity to all
svg.selectAll("path.chord")
.transition()
.style("opacity", 0.1);
//Show hovered over chord with full opacity
d3.select(this)
.transition()
.style("opacity", 1);
//Define and show the tooltip over the mouse location
$(this).popover({
placement: 'auto top',
container: 'body',
mouseOffset: 10,
followMouse: true,
trigger: 'hover',
html : true,
content: function() {
return "<p style='font-size: 11px; text-align: center;'><span style='font-weight:900'>" + Names[d.source.index] +
"</span> and <span style='font-weight:900'>" + Names[d.target.index] +
"</span> appeared together in <span style='font-weight:900'>" + d.source.value + "</span> movies </p>"; }
});
$(this).popover('show');
}//mouseoverChord
//Bring all chords back to default opacity
function mouseoutChord(d) {
//Hide the tooltip
$('.popover').each(function() {
$(this).remove();
});
//Set opacity back to default for all
svg.selectAll("path.chord")
.transition()
.style("opacity", opacityDefault);
}//function mouseoutChord
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment