class dragnet {
constructor(opts) {
this.element = opts.element;
this.options = opts.options;
this.data = opts.data;
this.meta_data = opts.meta_data;
this.sizeVar = opts.sizeVar
this.draw();
}
draw() {
let that = this;
const width_share = 0.82
this.width = (width * width_share) - 10;
this.side_width = width - this.width;
this.height = 750;
this.margin = this.options.margin;
this.element.innerHTML = '';
d3.select(this.element).style('overflow', 'hidden');
const main_div = d3.select(this.element)
.append('div')
.attr('width', this.width)
.attr('height', this.height)
.style('float', 'left');
main_div.append('div')
.style('width', '100px')
.style('background-color', 'steelblue')
.style('color', 'white')
.style('text-align', 'center')
.html("RESET")
.on('click', clearSummary);
const side_div = d3.select(this.element).append('div').attr('width', this.side_width).style('float', 'right');
this.svg = main_div.append('svg').attr("viewBox", [0, 0, this.width, this.height]);
this.svg.attr('width', this.width);
this.svg.attr('height', this.height);
this.svg.style('top', this.margin.top);
this.plot = this.svg.append('g')
.attr('transform','translate('+this.width/2+','+this.height/2+')');
this.chart = this.plot.append('g');
this.tooltip = d3.select(this.element).append("div").attr("class", "toolTip");
this.toolTipBody = this.tooltip
.append('div')
.attr('class', 'toolTipBody');
const community = this.findCommunity(this.createAgentNodes(this.data), this.createAgentEdges(this.data));
this.community = community;
side_div.append('div');
const select_options = Object.values(community).unique();
side_div.append('div').append('h5').text("Most Frequent Keywords");
this.svg_side = side_div.append('div').append('svg').attr('width', this.side_width).attr('height', 225 + 'px');
this.side_plot_keys = this.svg_side.append('g');
side_div.append('div').append('h5').text("Most Frequent Description");
this.svg_side_desc = side_div.append('div').append('svg').attr('width', this.side_width ).attr('height', 225 + 'px');
this.side_plot_desc = this.svg_side_desc.append('g');
side_div.append('div').append('h5').text("Most Connected COs");
this.svg_side_comp = side_div.append('div').append('svg').attr('width', this.side_width ).attr('height', 225 + 'px');
this.side_plot_comp = this.svg_side_comp.append('g');
const nodes_data = this.createAgentNodes(this.data);
const nodes = nodes_data.map(function(d) { return Object.create(d) });
this.nodes = nodes;
this.nodes_index = nodes_data;
const links = this.createAgentEdges(this.data).map(function(d) { return Object.create(d) });
const color = d3.scaleOrdinal(
d3.schemeCategory10.concat( d3.schemeDark2 ).concat( d3.schemeTableau10 ).concat( d3.schemePaired )
);
this.simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody())
.force('collision', d3.forceCollide() )
.force("x", d3.forceX() )
.force("y", d3.forceY() );
const link = this.chart.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.5)
.selectAll("line")
.data(links)
.join("line")
.attr("stroke-width", d => Math.sqrt(d.value))
;
const linkedByIndex = {};
links.forEach(d => {
linkedByIndex[`${d.source.index},${d.target.index}`] = 1;
});
const node = this.chart.append("g")
.attr("stroke", "none")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", function(d) {
d.weight = link.filter(function(l) {
return l.source.index == d.index || l.target.index == d.index
}).size();
var minRadius = 4;
return Math.min(minRadius + (d.weight * 2), 12);
})
.attr("fill", d => color(community[d.id] ) )
.call(drag(this.simulation))
.on("click", hoverOn)
.on('dblclick', summarise);
this.simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
});
this.svg.call(d3.zoom()
.extent([[0, 0], [width, this.height]])
.scaleExtent([0.5, 8])
.on("zoom", zoomed))
.on("dblclick.zoom", null);
function zoomed({transform}) {
node.attr("transform", transform);
link.attr("transform", transform);
}
function clearSummary(){
d3.selectAll('select').property('value', '');
that.chart.selectAll('circle')
.style( 'opacity', 1)
//.style( 'stroke', 'none')
//.style( 'r', 8 )
;
that.chart.selectAll('line')
.style( 'stroke-opacity', 0.5);
side_div.selectAll('rect').remove();
side_div.selectAll('text').remove();
that.tooltip
.style('display', 'none')
}
function summarise(){
var value = d3.select(this).node().nodeName == "SELECT" ? d3.select(this).property("value") : d3.select(this).data().map(d => community[d.id] );
const community_selected = Object.entries(community).filter(d => d[1] == value);
const community_nodes = community_selected.map(d => d[0]);
const community_data = that.meta_data.filter(d => community_nodes.indexOf(d.CompanyName) > -1);
if(value == '') {
clearSummary();
} else {
that.chart.selectAll('circle')
.style( 'opacity', d => community_nodes.indexOf(d.id) > -1 ? 1 : 0.2);
const selected_nodes = that.chart.selectAll('circle')
.data()
.filter(d => community_nodes.indexOf(d.id) > -1 )
.map( d => d.index ) ;
that.chart.selectAll('line')
.style( 'stroke-opacity', function(d) {
return selected_nodes.indexOf(d.source.index) > -1 && selected_nodes.indexOf(d.target.index) > -1 ? 0.5 : 0.01;
});
const selected_edges = that.chart.selectAll('line').data()
.filter( function(d) {
return selected_nodes.indexOf(d.source.index) > -1 && selected_nodes.indexOf(d.target.index) > -1
});
const selected_desc = that.chart.selectAll('line').data()
.filter( function(d) {
return selected_nodes.indexOf(d.source.index) > -1 && selected_nodes.indexOf(d.target.index) > -1
}).map(d => d.desc_link);
const selected_keywords = that.chart.selectAll('line').data()
.filter( function(d) {
return selected_nodes.indexOf(d.source.index) > -1 && selected_nodes.indexOf(d.target.index) > -1
}).map(d => d.keyword_link);
that.buildSummary(that.side_plot_keys, that.side_plot_desc, that.side_plot_comp, selected_keywords, selected_edges,selected_desc);
}
}
function isConnected(a, b) {
return linkedByIndex[`${a.index},${b.index}`] || linkedByIndex[`${b.index},${a.index}`] || a.index === b.index;
}
function hoverOff(){
d3.select(this)
//.attr('r', 8)
.attr('stroke', 'none');
node.style("opacity", 1);
link.style('stroke-opacity', 0.5);
}
function hoverOn(){
d3.select(this)
//.attr('r', 10)
.attr('stroke', 'white');
let selected_node = d3.select(this).data()[0];
let connectedNodeIds = links
.filter(x => x.source.id == selected_node.id || x.target.id == selected_node.id)
.map(x => x.source.id == selected_node.id ? x.target.id : x.source.id);
node
.style("opacity", function(c) {
if (connectedNodeIds.indexOf(c.id) > -1 || c.id == selected_node.id) return 1;
else return 0.1;
});
link.style('stroke-opacity', o => ( (connectedNodeIds.indexOf(o.source.id)>-1 && connectedNodeIds.indexOf(o.target.id) >-1) || o.source === selected_node || o.target === selected_node ? 0.5 : 0.01));
const nodes_name = nodes.filter( d => d.index === selected_node.index).map( d => d.id)[0];
const nodes_meta_data = that.meta_data.filter(function(d){ return d.CompanyName.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"") == nodes_name.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"")});
const print_data = nodes_meta_data.map(function(d) {
return{
'Name': d.CompanyName,
'Website': '<a href="https://' + d.Website + '" target="_blank">' + d.Website + '</a>' ,
'Business Status': d.BusinessStatus,
'Keywords': d.Keywords,
'Description': d.Description
}});
that.tooltip
.style('display', 'inline-block')
.style('opacity', 1)
.style("left",0)
.style("top", 0);
that.toolTipBody
.html(function() {
var y_text = []
print_data.forEach(function(item){
Object.keys(item).forEach(function(key) {
y_text.push('<span class="tool-text"><strong>' + key + ':</strong> ' + item[key] + '</span><br>');
});
});
return y_text.join(' ');
})
const selected_nodes = node
.filter(c => connectedNodeIds.indexOf(c.id) > -1 || c.id == selected_node.id )
.data()
.map(d => d.index) ;
const clique_nodes = node
.filter(c => connectedNodeIds.indexOf(c.id) > -1 || c.id == selected_node.id )
.data()
.map(d => d.id) ;
const clique_meta_data = that.meta_data.filter( d=> clique_nodes.indexOf(d.CompanyName) > -1 );
let selected_desc = that.chart.selectAll('line').data()
.filter(x => clique_nodes.indexOf(x.source.id) > -1 || clique_nodes.indexOf(x.target.id) > -1)
.map(d => d.desc_link);
const selected_keywords = that.chart.selectAll('line').data()
.filter(x => clique_nodes.indexOf(x.source.id) > -1 || clique_nodes.indexOf(x.target.id) > -1 )
.map(d => d.keyword_link);
let selected_edges = that.chart.selectAll('line').data()
.filter(x => clique_nodes.indexOf(x.source.id) > -1 || clique_nodes.indexOf(x.target.id) > -1 );
that.buildSummary(that.side_plot_keys, that.side_plot_desc, that.side_plot_comp, selected_keywords, selected_edges,selected_desc);
}
}
createAgentNodes(data){
const node_a = d3.map( data, (d) => d.node_a.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"") ).unique();
const node_b = d3.map( data, (d) => d.node_b.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"") ).unique();
const nodes = node_a.concat(node_b).unique().map((str, index) => ({ id: str, index: index + 1 }));
return nodes;
}
createAgentEdges(data){
data.forEach(function(d){
d.source = d.node_a.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"");
d.target = d.node_b.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"");
d.value = d.keyword_link.split(",").length;
})
return data;
}
findCommunity(node_data, edge_data) {
const nodes = node_data.map(({id}) => ({id}));
const edges = edge_data.map(({source, target, value}) => ({source, target, value}))
const nodeData = nodes.map(function (d) {return d.id});
const linkData = edges.map(function (d) {return {source: d.source, target: d.target, weight: d.value}; });
const community = jLouvain()
.nodes(nodeData)
.edges(linkData);
const result = community();
return(result);
}
buildSummary(plotArea, descArea, compArea, data, link_data, desc_data){
let that = this;
const format_percent = d3.format(".1%");
let comp_count_words = link_data.map( d => Array.from( new Set ([d.source.id, d.target.id ]) ) ).flat();
let comp_count_map = comp_count_words.reduce((acc, e) => acc.set(e, (acc.get(e) || 0) + 1), new Map());
//let comp_count = Array.from(comp_count_map).length;
let comp_count = link_data.length;
const key_words = data.map( d => d.split(",").map(d => d.trim() )).flat();
const top_key_words_map = key_words.reduce((acc, e) => acc.set(e, (acc.get(e) || 0) + 1), new Map());
const top_key_words = Array.from(top_key_words_map)
.filter( d=> d[1] / +comp_count >= 0.05 )
.sort((a,b) => a[1] - b[1])
.slice(-6);
const key_y = d3.scaleBand()
.domain( top_key_words.map(d => d[0]).reverse() )
.range([10,225])
.padding(0.1);
const key_x = d3.scaleLinear()
.domain([0, d3.max(top_key_words, d => d[1])]).nice()
.range([0, that.side_width - 40]);
const bars = plotArea.selectAll("rect")
.data(top_key_words);
bars.exit().remove();
bars.enter().append("rect")
.attr("class", "bar")
.style('fill', 'steelblue')
.attr("width", 0 )
.attr("y", function(d) { return key_y(d[0]); })
.attr("height", key_y.bandwidth() - 15)
.transition().ease(d3.easeQuad)
.duration(800)
.attr("width", function(d) {return key_x(d[1]); } );
bars.transition().ease(d3.easeQuad)
.duration(800)
.attr("height", key_y.bandwidth() - 15)
.attr("width", function(d) {return key_x(d[1]); } )
.attr("y", function(d) { return key_y(d[0]); });
const labels = plotArea.selectAll(".label")
.data(top_key_words);
labels.exit().remove();
labels
.enter()
.append('text')
.attr('class', 'label')
.attr("x",0)
.attr("y", d=> key_y(d[0]) )
.attr('dy', '-.15em')
.attr("text-anchor", "start")
.attr('font-size', '12px')
.text(d => d[0] );
labels
.attr("x",0)
.attr("y", d=> key_y(d[0]))
.text(d =>d[0])
const freq = plotArea.selectAll(".freq")
.data(top_key_words);
freq.exit().remove();
freq
.enter()
.append('text')
.attr('class', 'freq')
.style('fill', 'gray')
.attr("y", d=> key_y(d[0])+15 )
.attr('dx', '.35em')
.attr('dy', '.15em')
.attr("text-anchor", "start")
.attr('font-size', '12px')
.text(d => format_percent( +d[1] / +comp_count ) )
.transition().ease(d3.easeQuad)
.duration(800)
.attr("x", function(d) {return key_x(d[1]); } );
freq
.attr("y", d=> key_y(d[0]) +15)
.text(d => format_percent( +d[1] / +comp_count ) )
.transition().ease(d3.easeQuad)
.duration(800)
.attr("x", function(d) {return key_x(d[1]); } )
let desc_words = desc_data.map( d => d.split(",").map(d => d.trim() )).flat();
let top_desc_words_map = desc_words.reduce((acc, e) => acc.set(e, (acc.get(e) || 0) + 1), new Map());
let top_desc_words = Array.from(top_desc_words_map)
.filter( d=> d[1] / +comp_count >= 0.05 )
.sort((a,b) => a[1] - b[1]).slice(-6);
let desc_y = d3.scaleBand()
.domain( top_desc_words.map(d => d[0]).reverse() )
.range([10,225])
.padding(0.1);
let desc_x = d3.scaleLinear()
.domain([0, d3.max(top_desc_words, d => d[1])]).nice()
.range([0, that.side_width - 40]);
let bars_desc = descArea.selectAll("rect")
.data(top_desc_words);
bars_desc.exit().remove();
bars_desc.enter().append("rect")
.attr("class", "bar")
.style('fill', 'orange')
.attr("width", 0 )
.attr("y", function(d) { return desc_y(d[0]); })
.attr("height", desc_y.bandwidth() - 15)
.transition().ease(d3.easeQuad)
.duration(800)
.attr("width", function(d) {return desc_x(d[1]); } );
bars_desc.transition().ease(d3.easeQuad)
.duration(800)
.attr("height", desc_y.bandwidth() - 15)
.attr("width", function(d) {return desc_x(d[1]); } )
.attr("y", function(d) { return desc_y(d[0]); });
const labels_desc = descArea.selectAll(".label")
.data(top_desc_words);
labels_desc.exit().remove();
labels_desc
.enter()
.append('text')
.attr('class', 'label')
.attr("x",0)
.attr("y", d=> desc_y(d[0]) )
.attr('dy', '-.15em')
.attr("text-anchor", "start")
.attr('font-size', '12px')
.text(d =>d[0]);
labels_desc
.attr("x",0)
.attr("y", d=> desc_y(d[0]))
.text(d =>d[0])
const freq_desc = descArea.selectAll(".freq")
.data(top_desc_words);
freq_desc.exit().remove();
freq_desc
.enter()
.append('text')
.attr('class', 'freq')
.style('fill', 'gray')
.attr("y", d=> desc_y(d[0])+15 )
.attr('dx', '.35em')
.attr('dy', '.15em')
.attr("text-anchor", "start")
.attr('font-size', '12px')
.text(d => format_percent( +d[1] / +comp_count ) )
.transition().ease(d3.easeQuad)
.duration(800)
.attr("x", function(d) {return desc_x(d[1]); } );
freq_desc
.attr("y", d=> desc_y(d[0]) +15)
.text(d => format_percent( +d[1] / +comp_count ) )
.transition().ease(d3.easeQuad)
.duration(800)
.attr("x", function(d) {return desc_x(d[1]); } )
let comp_words = link_data.map( d => Array.from( new Set ([d.source.id, d.target.id ]) ) ).flat();
let top_comp_words_map = comp_words.reduce((acc, e) => acc.set(e, (acc.get(e) || 0) + 1), new Map());
let top_comp_words = Array.from(top_comp_words_map)
.filter( d=> d[1] / +comp_count >= 0.05 )
.sort((a,b) => a[1] - b[1]).slice(-6);
let comp_y = d3.scaleBand()
.domain( top_comp_words.map(d => d[0]).reverse() )
.range([10,225])
.padding(0.1);
let comp_x = d3.scaleLinear()
.domain([0, d3.max(top_comp_words, d => d[1])]).nice()
.range([0, that.side_width - 40]);
const bars_comp = compArea.selectAll("rect")
.data(top_comp_words);
bars_comp.exit().remove();
bars_comp.enter().append("rect")
.attr("class", "bar")
.style('fill', 'green')
.attr("width", 0 )
.attr("y", function(d) { return comp_y(d[0]); })
.attr("height", comp_y.bandwidth() - 15)
.transition().ease(d3.easeQuad)
.duration(800)
.attr("width", function(d) {return comp_x(d[1]); } );
bars_comp.transition().ease(d3.easeQuad)
.duration(800)
.attr("height", comp_y.bandwidth() - 15)
.attr("width", function(d) {return comp_x(d[1]); } )
.attr("y", function(d) { return comp_y(d[0]); });
const labels_comp = compArea.selectAll(".label")
.data(top_comp_words);
labels_comp.exit().remove();
labels_comp
.enter()
.append('text')
.attr('class', 'label')
.attr("x",0)
.attr("y", d=> comp_y(d[0]) )
.attr('dy', '-.15em')
.attr("text-anchor", "start")
.attr('font-size', '12px')
.text(d =>d[0]);
labels_comp
.attr("x",0)
.attr("y", d=> comp_y(d[0]))
.text(d =>d[0])
const freq_comp = compArea.selectAll(".freq")
.data(top_comp_words);
freq_comp.exit().remove();
freq_comp
.enter()
.append('text')
.attr('class', 'freq')
.style('fill', 'gray')
.attr("y", d=> comp_y(d[0])+15 )
.attr('dx', '.35em')
.attr('dy', '.15em')
.attr("text-anchor", "start")
.attr('font-size', '12px')
.text(d => format_percent( +d[1] / +comp_count ) )
.transition().ease(d3.easeQuad)
.duration(800)
.attr("x", function(d) {return comp_x(d[1]); } );
freq_comp
.attr("y", d=> comp_y(d[0]) +15)
.text(d => format_percent( +d[1] / +comp_count ) )
.transition().ease(d3.easeQuad)
.duration(800)
.attr("x", function(d) {return comp_x(d[1]); } )
}
highlightNode(node){
let that = this;
const nodes_name = node;
if(node == '') {
this.chart.selectAll('circle')
//.style( 'r', 8 )
.attr( 'classed', false );
that.tooltip
.style('display', 'none')
} else {
const node_index = this.chart.selectAll( 'circle' ).data().filter( d => d.id.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"") == node.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"") ).map( d => d.index );
this.chart.selectAll('circle')
.attr( 'class', d => d.index == node_index ? 'halo' : 'no-halo' );
const node_to_front = this.chart.selectAll('circle')
.filter(d => d.index == node_index).node();
node_to_front.parentElement.appendChild(node_to_front);
const nodes_meta_data = that.meta_data.filter(function(d){ return d.CompanyName.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"") == nodes_name.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"")});
const print_data = nodes_meta_data.map(function(d) {return{
'Name': d.CompanyName,
'Website': '<a href="https://' + d.Website + '" target="_blank">' + d.Website + '</a>' ,
'Business Status': d.BusinessStatus,
'Keywords': d.Keywords,
'Description': d.Description
}});
that.tooltip
.style('display', 'inline-block')
.style('opacity', 1)
.style("left",0)
.style("top", 0);
that.toolTipBody
.html(function() {
var y_text = []
print_data.forEach(function(item){
Object.keys(item).forEach(function(key) {
y_text.push('<span class="tool-text"><strong>' + key + ':</strong> ' + item[key] + '</span><br>');
});
});
return y_text.join(' ');
})
}
}
}