UNPKG

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
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; }