/* Reusable d3 force cluster layout for visualizing communities in * large gene networks * * Follows the design pattern suggested by Mike Bostock: * http://bost.ocks.org/mike/chart/ */ (function() { d3.netClust = function() { // declare everything // colors for the communities const colors = { '0': '#27ae60', '1': '#2980b9', '2': '#8e44ad', '3': '#e67e22', '4': '#e74c3c', '5': '#34495e', '6': '#f39c12', '7': '#7f8c8d', '8': '#bdc3c7', }; let selection; let genes = []; let edges = []; let enrichData = []; let normdegree = 1; let clusters = []; const force = d3.forceSimulation(); const zoomba = d3.zoom(); const edgesByCluster = {}; const interClusterEdges = []; const cNodeIds = {}; const degree = {}; const enrich = {}; const eCurrMin = {}; const k = 5; // parameter that we should be able to set const enrichwc = {}; const enrichtitle = {}; const enrichAll = {}; const stopwords = [ '_positive', '_negative', '_process', '_activity', '_pathway', '_from', '_via', '_a', '_an', '_and', '_the', '_in', '_regulation', '_to', '_of', '_response', '_cell', '_signaling', '_stimulus', '_cellular', '_protein', '_localization', '_modification', '_organization', ]; const buff = 5.0; // should be able to set?? const w = 800; // should be able to set const h = 600; // should be able to set // vars for zoom let selected = false; let lockzoom = false; let lockzoomk = 0; // tooltip stuff const tooltip = d3 .select('body') .append('div') .attr('class', 'toolTip'); const clustTip = d3 .select('body') .append('div') .attr('class', 'clustTip'); const clustTip2 = d3 .select('body') .append('div') .attr('class', 'clustTip'); // core of the generation stuff function chart(_selection) { selection = _selection; // make selection global console.info(selection); const svg = selection .append('svg') .attr('width', w) .attr('height', h) .append('g') .attr('transform', `translate(${w / 2},${h / 2})`) .attr('class', 'network'); const forceCollide = d3 .forceCollide() .radius(function(d) { return degree[d.id] / normdegree * 7 + 4; }) .iterations(1); // define cluster centers - temp but will be the last element of cluster clusters = new Array(d3.max(genes, d => parseInt(d.cluster, 10))); genes.forEach(d => { // init case if (clusters[parseInt(d.cluster, 10)] != null) { if (degree[clusters[parseInt(d.cluster, 10)].id] < degree[d.id]) // set the center to be the highest degree elm per cluster clusters[parseInt(d.cluster, 10)] = d; } else { clusters[parseInt(d.cluster, 10)] = d; } }); function forceCluster(alpha) { for ( let i = 0, n = genes.length, node, cluster, k2 = alpha * 1; i < n; i += 1 ) { node = genes[i]; cluster = clusters[node.cluster]; node.vx -= (node.x - cluster.x) * k2; node.vy -= (node.y - cluster.y) * k2; } } // grouping for the cluster path const filtered = genes.filter(d => d.cluster < 99); // REMOVE BUT ONLY 9 CLUSTER COLORS RIGHT NOW const hclusters = d3 .nest() .key(function(d) { return d.cluster; }) .entries(filtered); const groupPath = function(d) { // console.log("ddddddd"); // console.log(d); if (d.values.length === 2) { const arr = d.values.map(i => [i.x, i.y]); arr.push([arr[0][0], arr[0][1]]); return `M${d3.polygonHull(arr).join('L')}Z`; } return `M${d3.polygonHull(d.values.map(i => [i.x, i.y])).join('L')}Z`; }; svg.append('g').attr('class', 'edgeContainer'); const circle = svg .selectAll('circle') .data(genes) .enter() .append('circle') .attr('r', function(d) { return degree[d.id] / normdegree * 7 + 3; }) .attr('fill-opacity', 0.9) .attr('class', function(d) { return `node cluster${d.cluster}`; }) .attr('fill', function(d) { return colors[d.cluster]; }) .attr('stroke', '#fff') .attr('stroke-width', 1.5) .on('mouseover', mouseOver) .on('mouseout', mouseOut) .on('click', clusterSelect); function tick() { circle .attr('cx', function(d) { return d.x; }) .attr('cy', function(d) { return d.y; }); // add the cluster paths svg .selectAll('path') .data(hclusters) .attr('d', groupPath) .enter() .insert('path', 'circle') // .style("fill", function(d){return colors[d.key];}) .style('fill', 'none') .style('stroke', '#00') .style('stroke-width', 40) .style('stroke-linejoin', 'round') .style('opacity', 0.2) .attr('d', groupPath) .attr('class', function(d) { return `pathc${d.key}`; }); } force .force('link', d3.forceLink().id(d => d.id)) .nodes(genes) .force('center', d3.forceCenter()) .force('collide', forceCollide) .force('cluster', forceCluster) .force('gravity', d3.forceManyBody(5)) .force('x', d3.forceX().strength(0.5)) .force('y', d3.forceY().strength(0.5)) .on('tick', tick); // .stop(); // force.force("link") // .links(edges); function zoomed() { if (!lockzoom) svg.attr('transform', d3.event.transform); else svg.attr( 'transform', `translate(${d3.event.transform.x}, ${ d3.event.transform.y }) scale(${lockzoomk})`, ); } zoomba.on('zoom', zoomed); d3 .select('svg') .call(zoomba) .call( zoomba.transform, d3.zoomIdentity.translate(w / 2.5, h / 2.5).scale(0.4), ); } // getter and setter functions chart.genes = function(x) { if (!arguments.length) return genes; genes = x; x.forEach(d => { cNodeIds[d.id] = d.cluster; }); return chart; }; chart.edges = function(x) { if (!arguments.length) return edges; edges = x; x.forEach(edge => { if (!(edge.source in degree)) { degree[edge.source] = 0; } if (!(edge.target in degree)) { degree[edge.target] = 0; } degree[edge.source] += 1; degree[edge.target] += 1; if (cNodeIds[edge.source] === cNodeIds[edge.target]) { // check that the edge is within the cluster if (!edgesByCluster[cNodeIds[edge.source]]) edgesByCluster[cNodeIds[edge.source]] = []; edgesByCluster[cNodeIds[edge.source]].push(edge); } else { interClusterEdges.push(edge); } }); console.info('inter cluster edges'); console.info(interClusterEdges); normdegree = d3.max(d3.values(degree)); return chart; }; chart.enrich = function(x) { if (!arguments.length) return enrichData; x.forEach(d => { if (!enrich[d.cluster]) { enrich[d.cluster] = []; eCurrMin[d.cluster] = 0.0; enrichtitle[d.cluster] = ''; enrichwc[d.cluster] = {}; enrichAll[d.cluster] = []; } if (d.q_value < 0.05) { // if significant count words const words = d.name.split(/\b/); for (let i = 0; i < words.length; i += 1) { if (words[i] !== ' ' && words[i] !== '-') enrichwc[d.cluster][`_${words[i]}`] = (enrichwc[d.cluster][`_${words[i]}`] || 0) + 1; } enrichAll[d.cluster].push(d); } if (enrich[d.cluster].length < k || d.q_value < eCurrMin[d.cluster]) { enrich[d.cluster].push(d); if (enrich[d.cluster].length > k) { // need to take an element off let needle = -1; let newMin = d.q_value; for (let i = 0; i < enrich[d.cluster].length; i += 1) { if (enrich[d.cluster][i].q_value === eCurrMin[d.cluster]) { needle = i; } else { newMin = Math.max(newMin, enrich[d.cluster][i].q_value); } } if (needle === -1) console.error( 'something went horribly wrong during enrichment title calculation!', ); else { enrich[d.cluster].splice(needle, 1); eCurrMin[d.cluster] = newMin; } } eCurrMin[d.cluster] = Math.max(d.q_value, eCurrMin[d.cluster]); } }); // second pass through the data to expand the top seeds Object.keys(enrichwc).forEach(clust => { // sort by word frequency let tmpwc = []; tmpwc = Object.keys(enrichwc[clust]).map(key => ({ name: key, count: enrichwc[clust][key], })); tmpwc.sort((a, b) => b.count - a.count); // get most common word that is not a stop word as seed let seed = ''; let freq = 0; for (let i = 0; i < tmpwc.length; i += 1) { if ($.inArray(tmpwc[i].name, stopwords) === -1) { seed = tmpwc[i].name; freq = tmpwc[i].count; break; } } // console.log("cluster" + clust); // console.log(seed + " " + freq); // expand on either side of seed to get candidate titles const enrichTs = []; // console.log(enrichAll[clust]); Object.keys(enrichAll[clust]).forEach(elm => { const d = enrichAll[clust][elm]; if (d.name.search(seed.substr(1)) !== -1) { // check if term in name // console.log("in substring"); // console.log(d); let words = d.name.split(/\b/); // remove the spaces and the dashes or underscores const clean = []; Object.keys(words).forEach(word => { if ( words[word] !== '-' && words[word] !== ' ' && words[word] !== '_' ) clean.push(words[word]); }); words = clean; // console.log("words after"); // console.log(words); const ind = $.inArray(seed.substr(1), words); if (ind !== -1) { let title = seed.substr(1); let sum = freq; let marker = ind; // console.log("marker: " + marker + " sum: " + sum + " title: " + title); while ( marker < words.length && enrichwc[clust][`_${words[marker + 1]}`] > Math.max(freq - buff, 2) // expand to the right ) { title += ` ${words[marker + 1]}`; if ($.inArray(`_${words[marker + 1]}`, stopwords) !== -1) sum += 1; else sum += enrichwc[clust][`_${words[marker + 1]}`]; marker += 1; } const pre = []; marker = ind; while ( marker > 0 && enrichwc[clust][`_${words[marker - 1]}`] > Math.max(freq - buff, 2) // expand to the left ) { // title += " " + words[marker-1]; if ($.inArray(`_${words[marker - 1]}`, stopwords) !== -1) sum += 1; else sum += enrichwc[clust][`_${words[marker - 1]}`]; marker -= 1; pre.push(words[marker - 1]); } pre.reverse(); title = `${pre.join(' ')} ${title}`; if (title.charAt(0) === ' ') title = title.substr(1); enrichTs.push({ title, count: sum }); } } }); // take top seed enrichTs.sort((a, b) => b.count - a.count); // console.log("enrichTs"); // console.log(enrichTs); enrichtitle[clust] = enrichTs[0].title; }); console.info('enrichment'); console.info(enrichtitle); console.info(enrich); enrichData = enrichAll; return chart; }; function mouseOver(d) { if (!selected) { // console.log("mouseover"); d3.selectAll(`.cluster${d.cluster}`).attr('fill-opacity', 1.0); // label the enrichment let htmltxt = `