Skip to content

Instantly share code, notes, and snippets.

@christophe-g
Last active February 23, 2016 14:50
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 christophe-g/23616ee1a7125032d557 to your computer and use it in GitHub Desktop.
Save christophe-g/23616ee1a7125032d557 to your computer and use it in GitHub Desktop.
! function() {
function t(t) {
return function(e, i) {
e = d3.hsl(e), i = d3.hsl(i);
var r = (e.h + 120) * a,
h = (i.h + 120) * a - r,
s = e.s,
l = i.s - s,
o = e.l,
u = i.l - o;
return isNaN(l) && (l = 0, s = isNaN(s) ? i.s : s), isNaN(h) && (h = 0, r = isNaN(r) ? i.h : r),
function(a) {
var e = r + h * a,
i = Math.pow(o + u * a, t),
c = (s + l * a) * i * (1 - i);
return "#" + n(i + c * (-.14861 * Math.cos(e) + 1.78277 * Math.sin(e))) + n(i + c * (-.29227 * Math.cos(e) - .90649 * Math.sin(e))) + n(i + c * 1.97294 * Math.cos(e))
}
}
}
function n(t) {
var n = (t = 0 >= t ? 0 : t >= 1 ? 255 : 0 | 255 * t).toString(16);
return 16 > t ? "0" + n : n
}
var a = Math.PI / 180;
d3.scale.cubehelix = function() {
return d3.scale.linear().range([d3.hsl(300, .5, 0), d3.hsl(-240, .5, 1)]).interpolate(d3.interpolateCubehelix)
}, d3.interpolateCubehelix = t(1), d3.interpolateCubehelix.gamma = t
}();
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define('d3-foci', ['exports'], factory) :
factory((global.d3_foci = {}));
}(this, function (exports) { 'use strict';
/**
* getSet creates a getter/setter function for a re-usable D3.js component.
*
* @method getSet
* @param {string} option - the name of the object in the string you want agetter/setter for.
* @param {function} component - the D3 component this getter/setter relates to.
*
* @return {mixed} The value of the option or the component.
*/
function getSet(option, component) {
return function(_) {
if (! arguments.length) {
return this[option];
}
this[option] = _;
return component;
};
}
function applier(component, options) {
for (var key in options) {
if(component[key] && (typeof component[key] == "function")) {
component[key](options[key]);
}
}
return component;
}
function binder(component, options) {
for (var key in options) {
if(!component[key]) {
component[key] = getSet(key, component).bind(options);
}
}
}
// apply a d3.fore layout with foci on venn area center to set foci
// d3.layout.venn.packCircles looks prettier.
function force(layout, data, links) {
var force = layout.packer()
if (!force) {
force = d3.layout.force();
binder(force, {
padding: 3,
maxRadius: 8,
collider: true,
ticker: null,
ender : null,
starter : null
});
}
var packingConfig = layout.packingConfig(),
size = layout.size(),
sets = layout.sets(),
padding = force.padding(), // separation between nodes
maxRadius = force.maxRadius(),
collider = force.collider;
// foci = d3.map({}, function(d) {
// return d.__key__
// });
// layout.sets().forEach(function(set) {
// foci.set(set.__key__, set.center);
// })
applier(force, packingConfig)
.nodes(data)
.links(links || [])
.gravity(0)
.charge(0)
.size(size)
.on('start.__packer__', init)
.on('tick.__packer__', tick)
var ender ;
if(ender = force.ender()) {
force.on('end.__packer__', ender)
}
function init(e) {
// if(layout.__ended__) {
data.forEach(function(d) {
var center = sets.get(d.__setKey__);
center = center.center || center;
d.x = d.x ? d.x * 1 : center.x;
d.y = d.y ? d.y * 1 : center.y;
})
var starter ;
if(starter = force.starter()) {
starter(layout)
}
}
function tick(e) {
var ticker;
data
.forEach(gravity(.2 * e.alpha))
if (collider) {
data
.forEach(collide(.5))
}
if (ticker = force.ticker()) {
ticker(layout)
}
}
// Move nodes toward cluster focus.
function gravity(alpha) {
return function(d) {
var center = sets.get(d.__setKey__);
center = center.center || center;
d.y += (center.y - d.y) * alpha;
d.x += (center.x - d.x) * alpha;
};
}
// Resolve collisions between nodes.
function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
return function(d) {
var r = d.r + maxRadius + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.r + quad.point.r + (d.__setKey__ !== quad.point.__setKey__) * padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
return force;
}
function foci() {
// d3.layout.foci = function() {
var opts = {
sets: null,
links: [],
chargeFactor : -70,
iterationLength: 120,
setsAccessor: setsAccessorFn,
setsSize: setsSize,
packingStragegy: force,
packingConfig: {
value: valueFn,
},
fociConfig: {
linkStrength: 0.01,
gravity: 0.1,
},
size: [1, 1]
};
var event = d3.dispatch("start", "tick", "end");
// fociEvent = d3.dispatch("tick");
var packer,
centers ,
nodeLinks,
nodes;
// var circles,
// nodes,
// centres;
var fociForce = d3.layout.force().charge(function(d){
return opts.chargeFactor * d.size
}),
// var fociForce = d3.layout.force(),
fociLinks;
// Build simple getter and setter Functions
binder(foci, opts);
//The layout function
function foci(data) {
if (!arguments.length) return nodes;
nodes = compute(data);
return foci;
}
function runForce() {
event.start({type: "start"});
var n = foci.iterationLength(),
sets = foci.sets();
packer.stop();
//run the simulation n times
fociForce.start();
for (var i = n * n; i > 0; --i) fociForce.tick();
fociForce.stop();
sets.forEach(function(k, set) {
centers.push(set.center = {
x: set.x,
y: set.y
})
});
packer.start();
event.end({type: "end"});
}
function compute(data) {
var sets,
setsValues,
// layout = foci.layoutFunction(),
packingStragegy = foci.packingStragegy();
// foci.__ended__ = false;
centers = [];
nodeLinks = [];
fociLinks = [];
sets = extractSets(data);
setsValues = sets.values();
applier(fociForce, opts.fociConfig);
fociForce
.size(foci.size())
.nodes(setsValues)
.links(fociLinks)
packer = packingStragegy(foci, data, foci.links());
// Use a timeout to allow the rest of the page to load first.
setTimeout(function() {
// Run the layout a fixed number of times.
// The ideal number of times scales with graph complexity.
// Of course, don't run too long—you'll hang the page!
runForce();
}, 10);
// solution = layout(setsValues);
console.info("data: ", data)
console.info("sets: ", sets)
console.info("links: ", fociLinks)
return data
}
// loop over data and build the set so that they comply with https://github.com/benfred/foci.js
/*
from data = [
{"set":["A"],"name":"node_0"},
{"set":["B"],"name":"node_1"},
{"set":["B","A"],"name":"node_2"}
{"set":["B","A"],"name":"node_3"}
]
to sets = [
{sets: ['A'], size: 1, nodes : ['node_0'], __key__: 'A'},
{sets: ['B'], size: 1, nodes : ['node_1'],__key__: 'A'},
{sets: ['A','B'], size: 2, nodes ['node_2', 'node_3'], __key__: 'A,B'}
];
links : [
{source: 'A', target: 'A,B', weight : 1}
{source: 'B', target: 'A,B', weight : 1}
]
*/
function extractSets(data) {
var oldSets = foci.sets() ,
sets = d3.map({}, function(d) {
return d.__key__
}),
individualSets = d3.map(),
accessor = foci.setsAccessor(),
size = foci.setsSize(),
set,
s,
key,
i,
n = data.length;
//reset fociLink s
for (i = -1; ++i < n;) {
set = accessor(data[i]);
if (set.length) {
key = set.sort().join(','); //so taht we have the same key as in https://github.com/benfred/foci.js
set.forEach(function(val) {
if (s = individualSets.get(val)) {
s.size += 1;
// s.nodes.push([data[i]]);
} else {
individualSets.set(val, {
__key__: val,
size: 1,
sets: [val],
nodes: []
// nodes: [data[i]]
})
}
});
data[i].__setKey__ = key;
if (s = sets.get(key)) {
s.size++;
s.nodes.push(data[i]);
} else {
sets.set(key, {
__key__: key,
sets: set,
size: 1,
nodes: [data[i]]
});
}
}
}
individualSets.forEach(function(k, v) {
if (!sets.get(k)) {
sets.set(k, v);
}
});
// reset the size for each set.
sets.forEach(function(k, v) {
v.size = size(v.size);
if ((n = v.sets.length) && n != 1) {
for (i = -1; ++i < n;) {
fociLinks.push({
source: sets.get(v.sets[i]),
target: sets.get(k)
})
}
}
if(oldSets && (set = oldSets.get(k))){
v.center = set.center;
v.x = set.x;
v.y = set.y;
}
v.nodes.forEach(function(n){
i = v.sets.length;
v.sets.forEach(function(v){
nodeLinks.push({source: sets.get(v), target: n, size: i});
})
})
})
// sets = sets.values();
foci.sets(sets);
return sets;
}
function setsSize(size) {
return size;
}
// data accessors
function setsAccessorFn(d) {
return d.set || [];
}
function valueFn(d) {
return d.value;
}
foci.packingConfig = function(_) {
var config = opts.packingConfig;
if (!arguments.length) {
return config;
}
for (var k in _) {
config[k] = _[k]
}
if (packer) {
applier(packer, _)
}
return foci;
};
foci.fociConfig = function(_) {
var config = opts.fociConfig;
if (!arguments.length) {
return config;
}
for (var k in _) {
config[k] = _[k]
}
if (packer) {
applier(fociForce, _)
}
return foci;
};
foci.force = function() {
return fociForce;
}
foci.packer = function() {
return packer;
}
foci.fociLinks = function() {
return fociLinks;
}
foci.centers = function() {
return centers;
}
foci.nodeLinks = function() {
return nodeLinks;
}
foci.start = function() {
runForce();
// fociForce.start()
}
foci.nodes = foci;
// return foci;
// d3.rebind(fociForce, endFoci, 'on');
return d3.rebind(foci, event, "on");
};
var version = "0.0.1";
exports.version = version;
exports.foci = foci;
}));(function(target, export_name, name) {
if (target) {
target[name] = this[export_name] && this[export_name][name];
for (var k in this[export_name]) {
if (k != name) {
target[name][k] = this[export_name][k];
}
}
delete this[export_name];
}
}(this.d3 && this.d3.layout, 'd3_foci', 'foci'));
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Venn Diagram - pack layout</title>
<script src="//d3js.org/d3.v3.min.js"></script>
<!-- <script src="https://raw.githubusercontent.com/christophe-g/vennLayout/master/vennLayout.js"></script> -->
<!-- <script type="text/javascript" src="../d3.js"></script> -->
<script type="text/javascript" src="cubehelix.js"></script>
<script type="text/javascript" src="d3-foci.js"></script>
<style>
#venn {
margin-left: 250px;
margin-top: -200px;
}
</style>
</head>
<body>
<form>
<div id="inputs">
<p>
<label for="dataLength">Number of Nodes</label>
<input type="number" min="10" step="10" max="600" name="dataLength" id="dataLength" value="" />
</p>
<p>
<label for="setLength">Number of Circles</label>
<input min="2" max="8" type="number" name="setLength" id="setLength" value="" />
</p>
<p>
<label for="circleOpacity">opacity for Circle</label>
<input min="0.1" max="1" step ="0.1" type="range" name="circleOpacity" id="circleOpacity" value="" />
</p>
</div>
<div id="force">
<p>
<label for="linkDistance">link distance</label>
<input type="number" name="linkDistance" id="linkDistance" value="" />
</p>
<p>
<label for="linkStrength">link strength</label>
<input type="number" max="1" min="0" step="0.1" name="linkStrength" id="linkStrength" value="" />
</p>
<!-- <p>
<label for="charge">charge</label>
<input type="number" name="charge" id="charge" value="" />
</p> -->
<p>
<label for="gravity">gravity</label>
<input type="number" max="1" min="0" step="0.1" name="gravity" id="gravity" value="" />
</p>
<p>
<label for="theta">theta</label>
<input type="number" name="theta" id="theta" value="" />
</p>
</div>
</form>
<svg id="venn"></svg>
<script type="text/javascript" src="script.js"></script>
</body>
</html>
(function test() {
var width = 600,
height = 600,
colors = d3.scale.cubehelix()
.domain([0, .5, 1])
.range([
d3.hsl(-100, 0.75, 0.35),
d3.hsl(80, 1.50, 0.80),
d3.hsl(260, 0.75, 0.35)
]);
var setChar = 'ABCDEFGHIJKLMN',
charFn = i => setChar[i],
setLength = 4,
sets = d3.range(setLength).map(function(d, i) {
return setChar[i]
})
var opts = {
dataLength: 120,
setLength: setLength,
duration: 800,
circleOpacity: 0.4,
innerOpacity: 0.2
};
var forceOpts = {
linkDistance: 10,
linkStrength: 2,
// charge: -30,
gravity: 0.1,
theta: 1
}
function resetColorScale() {
var l = layout.sets().values().length;
colors = colors.domain([0, l / 2, l])
}
// Build simple getter and setter Functions
for (var key in opts) {
test[key] = getSet(key, test).bind(opts);
}
for (var key in forceOpts) {
test[key] = getSet(key, test).bind(forceOpts);
}
function getSet(option, component) {
return function(_) {
if (!arguments.length) {
return this[option];
}
this[option] = _;
return component;
};
}
function refreshInput() {
var sel = d3.select(this),
name = sel.attr("name"),
value = sel.property("value")
test[name](value);
if (name == 'dataLength' || name == 'setLength') {
if (name == 'setLength') {
globalData = [] // we reshuffle everything
}
return refresh(generateData())
}
refresh();
}
function refreshForce() {
var sel = d3.select(this),
name = sel.attr("name"),
value = sel.property("value")
test[name](value);
if (layout) {
var force = layout.force();
if (force[name]) {
force[name](value);
layout.start();
// force.start().on('end.force', function() {
// refresh()
// })
// refresh();
}
}
}
function binder(obj) {
return function() {
var sel = d3.select(this),
name = sel.attr("name");
if (obj[name]) {
sel.property("value", obj[name]())
}
}
}
//set input value accorging to options and handle change of input
d3.selectAll('#inputs input')
.each(binder(test))
.on('input', refreshInput)
d3.selectAll('#force input')
.each(binder(test))
.on('input', refreshForce)
var layout = d3.layout.foci()
.size([width, height])
var svg = d3.select('svg')
.attr('width', width)
.attr('height', height),
isFirstLayout = true;
var linkNodeContainer = svg.append("g")
.attr("class", "link-node-container")
var svgDefs = svg.append("defs")
var globalData = [],
generator = 0;
function generateData() {
var dataLength = test.dataLength(),
setLength = test.setLength(),
diff = dataLength - globalData.length;
if (diff > 0) {
globalData = globalData.concat(d3.range(diff).map((d, i) => {
var l = Math.floor((Math.random() * setLength / 2) + 1),
set = [],
c,
i;
for (i = -1; ++i < l;) {
c = charFn(Math.floor((Math.random() * setLength)));
if (set.indexOf(c) == -1) {
set.push(c)
}
}
return {
set: set,
r: 8,
name: 'node_' + generator++
}
}))
} else {
globalData.splice(0, -diff);
}
return globalData;
}
function refresh(data) {
if (data) {
// we recalculate the layout for new data only
layout.nodes(data)
resetColorScale();
}
var setData = layout.sets().values();
var vennArea = svg.selectAll("g.venn-area")
.data(setData, function(d) {
return d.__key__;
});
var vennEnter = vennArea.enter()
.append('g')
.attr("class", function(d) {
return "venn-area venn-" +
(d.sets.length == 1 ? "circle" : "intersection");
})
.attr('fill', function(d, i) {
return colors(i)
})
vennEnter.append('circle')
.attr('class', 'venn-area-path')
.attr('r', 25)
// .call(layout.force().drag)
vennEnter.append('circle')
.attr('class', 'inner')
.attr('fill', 'grey');
vennEnter.append("text")
.attr("class", "label")
.attr("text-anchor", "middle")
.attr("dy", ".35em")
var link = linkNodeContainer.selectAll('.link')
.data(layout.fociLinks());
link.enter().append('line')
.attr('class', 'link')
// .attr('stroke', "#AAA")
.attr('stroke', function(d) {
return "url(#" + d.source.__key__ + '__' + d.target.__key__ + ")"
})
.attr('stroke-dasharray', "10,10")
.attr("stroke-width", 5);
link.exit().remove()
var linkNode = linkNodeContainer.selectAll('.link-node')
.data(layout.nodeLinks());
linkNode.enter().append('line')
.attr('class', 'link-node')
.attr('stroke', "#bbb")
.attr("stroke-width", 1);
linkNode.exit().remove()
var vennAreaLabel = vennArea.selectAll("text.label").data(function(d) {
return [d];
}).text(function(d) {
return d.__key__;
})
var vennAreaCircle = vennArea.selectAll('circle.venn-area-path').data(function(d) {
return [d];
}).text(function(d) {
return d.__key__;
})
.attr('opacity', test.circleOpacity())
layout.on('end', function() {
vennAreaLabel
.attr("x", function(d) {
// return d.x;
return d.center.x
})
.attr("y", function(d) {
// return d.y
return d.center.y
});
vennAreaCircle.attr("cx", function(d) {
// return d.x;
return d.center.x
})
.attr("cy", function(d) {
// return d.y;
return d.center.y
});
link
.attr('x1', function(d) {
return d.source.x;
})
.attr('y1', function(d) {
return d.source.y;
})
.attr('x2', function(d) {
return d.target.x;
})
.attr('y2', function(d) {
return d.target.y;
});
svgDefs.selectAll('.grad').remove()
var grad = svgDefs.selectAll('.grad')
.data(layout.fociLinks());
var gradEnter = grad.enter()
.append('linearGradient')
.attr({
id: function(d) {
return d.source.__key__ + '__' + d.target.__key__
},
x1: '0%',
x1: '0%',
x2: '100%',
y2: '0%',
})
gradEnter.append('stop')
.attr({
offset: '0%',
'stop-color': function(d) {
return colors(setData.indexOf(d.source.x < d.target.x ? d.source : d.target))
}
})
gradEnter.append('stop')
.attr({
offset: '100%',
'stop-color': function(d) {
return colors(setData.indexOf(d.source.x <= d.target.x ? d.target : d.source))
// return colors(setData.indexOf(d.target))
}
})
})
vennArea.exit().transition()
.duration(test.duration())
// .attrTween('d', function(d) {
// return d.d
// })
.remove()
// need this so that nodes always on top
var circleContainer = svg.selectAll("g.venn-circle-container")
.data(layout.sets().values(), function(d) {
return d.__key__;
});
circleContainer.enter()
.append('g')
.attr("class", "venn-circle-container")
.attr('fill', function(d, i) {
return colors(i)
});
circleContainer.exit().remove();
// need this so that nodes always on top
var circleContainer = svg.selectAll("g.venn-circle-container")
.data(layout.sets().values(), function(d) {
return d.__key__;
});
circleContainer.enter()
.append('g')
.attr("class", "venn-circle-container")
.attr('fill', function(d, i) {
return colors(i)
});
circleContainer.exit().remove();
var points = circleContainer.selectAll("circle.node")
.data(function(d) {
return d.nodes
}, function(d) {
return d.name
})
var pointsEnter = points.enter()
.append('circle')
.attr('r', 0)
.attr('class', 'node')
.call(layout.packer().drag)
points.transition()
.duration(isFirstLayout ? 0 : test.duration())
.attr('r', function(d) {
return d.r
})
points.exit().transition()
.attr('r', 0)
.remove()
isFirstLayout = false;
// var foci = layout.force()
// foci.nodes()
//set the force ticker
layout.packingConfig({
ticker: function() {
points.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
linkNode
.attr('x1', function(d) {
return d.source.x;
})
.attr('y1', function(d) {
return d.source.y;
})
.attr('x2', function(d) {
return d.target.x;
})
.attr('y2', function(d) {
return d.target.y;
});
}
})
//start the force layout
// var packer = layout.packer();
// packer.on('tick', function() {
// points.attr("cx", function(d) {
// return d.x
// })
// .attr("cy", function(d) {
// return d.y
// })
// }).start()
// layout.start()
return test
}
return refresh(generateData())
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment