doc-dna
Version:
dependencies graph of a the CreativeWork of a JSON-LD document using schema.org context
407 lines (343 loc) • 11.7 kB
JavaScript
var d3 = require('d3')
, clone = require('clone')
, url = require('url');
//avoid to load the whole schema.org ontology in memory
var TYPES = require('./data/data.json');
/**
* Adapted from https://github.com/fzaninotto/DependencyWheel
*/
function graph (options) {
var width = 400
, margin = 80
, paddingText = 26
, printType = false
, padding = 0.06
, colors = 'material';
var cols = {
'material': {
dataset: ['#aeea00', '#c6ff00', '#eeff41', '#f4ff81'], //Lime
code: ['#fdd835', '#ffeb3b', '#ffee58', '#fff176'], //Yellow
article: ['#ff8a65', '#ffab91', '#ffccbc', '#fbe9e7'], //Deep Orange
image: ['#ce93d8', '#e1bee7', '#f3e5f5', '#ba68c8'], //Purple
audio: ['#2baf2b', '#42bd41', '#72d572', '#a3e9a4'], //Green
video: ['#6889ff', '#a6baff', '#4d69ff', '#4d73ff'], //Blue
other: ['#7986cb', '#9fa8da', 'c5cae9', '#e8eaf6'] //Indigo
},
'cynthia': {
dataset: ["#fff7f3","#fde0dd","#fcc5c0","#fa9fb5","#f768a1","#dd3497","#ae017e","#7a0177","#49006a"], //RdPu
code: ["#ffffcc","#ffeda0","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#bd0026","#800026"], //YlOrRd
article: ["#ffffff","#f0f0f0","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525","#000000"], //Greys
image: ["#fff7fb","#ece7f2","#d0d1e6","#a6bddb","#74a9cf","#3690c0","#0570b0","#045a8d","#023858"], //PuBu
audio: ["#ffffe5","#f7fcb9","#d9f0a3","#addd8e","#78c679","#41ab5d","#238443","#006837","#004529"], //YlGn
video: ["#fff5f0","#fee0d2","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#a50f15","#67000d"], //Reds
other: ["#fcfbfd","#efedf5","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#54278f","#3f007d"], //Purples
}
};
function mapSchemaType(type){
for (var key in TYPES) {
if (~TYPES[key].indexOf(type)){
return key;
}
}
return 'other';
};
function chart(selection) {
selection.each(function(data, selectionIndex) {
var matrix = data.matrix;
var labels = data.labels;
var radius = width / 2 - margin;
// create the layout
var chord = d3.layout.chord()
.padding(padding)
.sortSubgroups(d3.descending);
// Select the svg element, if it exists.
var svg = d3.select(this).selectAll("svg").data([data]);
// Otherwise, create the skeletal chart.
var gEnter = svg.enter().append("svg:svg")
.attr("width", width)
.attr("height", width)
.append("g")
.attr("transform", "translate(" + (width / 2) + "," + (width / 2) + ")");
if (!matrix || !matrix.length) {
return;
}
var arc = d3.svg.arc()
.innerRadius(radius)
.outerRadius(radius *1.1);
var fill = function(d) {
var label = labels[d.index];
return cols[colors][mapSchemaType(label.type)][d.index % cols[colors][mapSchemaType(label.type)].length]
};
// Returns an event handler for fading a given chord group.
var fade = function(opacity) {
return function(g, i) {
svg.selectAll(".chord")
.filter(function(d) {
return d.source.index != i && d.target.index != i;
})
.transition()
.style("opacity", opacity);
var groups = [];
svg.selectAll(".chord")
.each(function(d) {
if (d.source.index == i) {
groups.push(d.target.index);
}
if (d.target.index == i) {
groups.push(d.source.index);
}
});
groups.push(i);
var length = groups.length;
svg.selectAll('.group')
.filter(function(d) {
for (var i = 0; i < length; i++) {
if(groups[i] == d.index) return false;
}
return true;
})
.transition()
.style("opacity", opacity);
};
};
chord.matrix(matrix);
var rootGroup = chord.groups()[0];
var rotation = - (rootGroup.endAngle - rootGroup.startAngle) / 2 * (180 / Math.PI);
var g = gEnter.selectAll("g.group")
.data(chord.groups)
.enter().append("svg:g")
.attr("class", "group")
.attr("transform", function(d) {
return "rotate(" + rotation + ")";
});
g.append("svg:path")
.style("fill", fill)
.style("stroke", fill)
.attr("d", arc)
.on("mouseover", fade(0.1))
.on("mouseout", fade(1));
g.append("svg:text")
.each(function(d) { d.angle = (d.startAngle + d.endAngle) / 2; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
.attr("transform", function(d) {
return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" +
"translate(" + (radius + paddingText) + ")" +
(d.angle > Math.PI ? "rotate(180)" : "");
})
.text(function(d) {
if(printType){
return labels[d.index].type;
}else {
if(labels[d.index].name.length>11){
return labels[d.index].name.slice(0,4) + '...' + labels[d.index].name.slice(labels[d.index].name.length-4,labels[d.index].name.length);
} else {
return labels[d.index].name;
}
}
})
.on("mouseover", function(d, i){
fade(0.1)(d, i);
var label = labels[d.index];
var col = cols[colors][mapSchemaType(label.type)][d.index % cols[colors][mapSchemaType(label.type)].length]
var el = selection[0][selectionIndex];
var event
if (window.CustomEvent) {
event = new CustomEvent('show', {detail: {id:labels[d.index].name, color: col}});
} else {
event = document.createEvent('CustomEvent');
event.initCustomEvent('show', true, true, {detail: {id:labels[d.index].name, color: col}});
}
el.dispatchEvent(event);
})
.on("mouseout", function(d, i){
var el = selection[0][selectionIndex];
var event
if (window.CustomEvent) {
event = new CustomEvent('hide', {detail: {id:labels[d.index].name}});
} else {
event = document.createEvent('CustomEvent');
event.initCustomEvent('hide', true, true, {detail: {id:labels[d.index].name}});
}
el.dispatchEvent(event);
fade(1)(d, i);
});
gEnter.selectAll("path.chord")
.data(chord.chords)
.enter().append("svg:path")
.attr("class", "chord")
.style("stroke", function(d) { return d3.rgb(fill(d.source)).darker(); })
.style("fill", function(d) { return fill(d.source); })
.attr("d", d3.svg.chord().radius(radius))
.attr("transform", function(d) {
return "rotate(" + rotation + ")";
})
.style("opacity", 1);
});
};
chart.width = function(value) {
if (!arguments.length) return width;
width = value;
return chart;
};
chart.margin = function(value) {
if (!arguments.length) return margin;
margin = value;
return chart;
};
chart.padding = function(value) {
if (!arguments.length) return padding;
padding = value;
return chart;
};
chart.paddingText = function(value) {
if (!arguments.length) return paddingText;
paddingText = value;
return chart;
};
chart.printType = function() {
printType = true;
return chart;
};
chart.colors = function(value) {
if (!arguments.length) return colors;
colors = value;
return chart;
};
return chart;
};
function compute(cdoc, opts){
function _intersect(list1, list2){
for (var i=0; i<list1.length; i++){
for (var j=0; j<list2.length; j++){
if(list1[i] === list2[j]) return true;
}
}
return false;
};
function _forEachNode(doc, callback){
for (var prop in doc) {
if (prop === '@context' || !doc.hasOwnProperty(prop)) continue;
if (Array.isArray(doc[prop])) {
for (var i=0; i<doc[prop].length; i++) {
if (typeof doc[prop][i] === 'object') {
callback( prop, doc[prop][i]);
_forEachNode(doc[prop][i], callback);
}
}
} else if (typeof doc[prop] === 'object') {
callback(prop, doc[prop]);
_forEachNode(doc[prop], callback);
}
}
};
function _setIds(cdoc, env) {
env = env || {counter: 0};
if (!('@id' in cdoc) ) {
var id = '_:n' + env.counter++;
cdoc['@id'] = id;
}
//traverse
_forEachNode(cdoc, function(prop, node){
_setIds(node, env);
});
return cdoc;
};
//make sure that all node have an @id
cdoc = _setIds(clone(cdoc));
var entries = {};
var refs = {};
var hasDeps = false;
//properties making a node a "downloadable"
var dprops = ['encoding', 'distribution', 'codeRepository', 'downloadUrl', 'installUrl'];
function _getEntry(node){
//is the node a downloadable ?
if (_intersect(Object.keys(node), dprops)) {
refs[node['@id']] = node;
entries[node['@id']] = {
name: node.name || node['@id'],
type: node['@type'] || 'CreativeWork',
deps: []
};
}
};
//unnest
_getEntry(cdoc)
_forEachNode(cdoc, function(prop, node){
_getEntry(node);
});
var keys = Object.keys(entries);
function _getUrlOfReverseProp(x){
if (!x) return;
x = Array.isArray(x)? x: [x];
return x.map(function(y){
return (typeof y === 'string') ? y : y['@id'];
});
};
//fill deps
keys.forEach(function(key){
var node = refs[key];
var entry = entries[key];
var parts;
if (node.hasPart) {
parts = Array.isArray(node.hasPart)? node.hasPart: [entry.hasPart];
}
entry.deps = (entry.deps).concat(
node.isBasedOnUrl || [],
node.requirements || [],
_getUrlOfReverseProp(node.sourceCode) || [],
(parts || []).map(function(x){
return (typeof x === 'string')? x : x['@id'];
})
);
//target products depends on source code
if (node.targetProduct) {
var targetProducts = Array.isArray(node.targetProduct)? node.targetProduct: [node.targetProduct];
targetProducts.forEach(function(tp){
var id = (typeof x === 'string')? tp : tp['@id'];
if (id in entries) {
entries[id].deps.push(node['@id']);
hasDeps = true;
}
});
}
//isPartOf: is x isPartOf y then y depends on x
(_getUrlOfReverseProp(node.isPartOf) || []).forEach(function(id){
if (id in entries) {
entries[id].deps.push(node['@id']);
hasDeps = true;
}
});
//only keep within document links
entry.deps.filter(function(x){
return ~keys.indexOf(x);
});
if (entry.deps.length) {
hasDeps = true;
}
});
var matrix = [];
keys.forEach(function(xkey, i){
var x = entries[xkey];
var row = Array.apply(null, new Array(keys.length)).map(Number.prototype.valueOf,0);
if (!hasDeps) {
row[i] = 1;
} else {
keys.forEach(function(ykey, j){
var y = entries[ykey];
if (y.deps.indexOf(xkey) !== -1) {
row[j] = 1;
}
});
}
matrix.push(row);
});
return {
labels: keys.map(function(x) {return {name: entries[x].name, type: entries[x].type}}),
matrix: matrix
};
};
if (typeof module !== 'undefined' && module.exports){
exports.graph = graph;
exports.compute = compute;
}