Published
Edited
Jun 18, 2021
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
html`
<style>
@-webkit-keyframes svg_pulse {
0% {
stroke-width: 2;
stroke: #00FF00;
z-index: 999;
}
25% {
stroke-width: 4;
stroke: #00FF00;
}

50% {
stroke-width: 6;
stroke: #00FF00;
}
75% {
stroke-width: 4;
stroke: #00FF00;
}
100% {
stroke-width: 2;
stroke: #00FF00;
}
}

.halo {
-webkit-animation: svg_pulse 2s ease;
-webkit-animation-iteration-count: infinite;
}
.no-halo{
opacity: 0.9;
}
.toolTip {
position: relative;
display: none;
min-width: 125px;
height: auto;
background: none repeat scroll 0 0 #DCDCDC;
backgroud-color: #F0F8FF;
border-radius: 2px;
line-height: 1.3;
box-shadow: 0px 3px 9px rgba(0, 0, 0, .15);
}
.toolTipBody {
min-width: 120px;
min-height: 35px;
margin-top: 10px;
margin-left: 5px;
padding-right: 5px;
padding-left: 5px;
text-align: left;
}
.tool-text{
padding-bottom: 10px;
display: inline-block;
}
* { box-sizing: border-box; }
body {
font: 16px Arial;
}
.autocomplete {
/*the container must be positioned relative:*/
position: relative;
display: inline-block;
}
input {
border: 1px solid transparent;
background-color: #f1f1f1;
padding: 10px;
font-size: 16px;
}
input[type=text] {
background-color: #f1f1f1;
width: 35%;
}
input[type=submit] {
background-color: DodgerBlue;
color: #fff;
}
.autocomplete-items {
position: absolute;
border: 1px solid #d4d4d4;
border-bottom: none;
border-top: none;
z-index: 99;
/*position the autocomplete items to be the same width as the container:*/
top: 100%;
left: 0;
right: 0;
}
.autocomplete-items div {
padding: 10px;
cursor: pointer;
background-color: #fff;
border-bottom: 1px solid #d4d4d4;
}
.autocomplete-items div:hover {
/*when hovering an item:*/
background-color: #e9e9e9;
}
.autocomplete-active {
/*when navigating through the items using the arrow keys:*/
background-color: DodgerBlue !important;
color: #ffffff;
}
</style>
`
Insert cell
Insert cell
Insert cell
class dragnet {
constructor(opts) {
//pass chart elements
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;
//define dimensions
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;
//set up parent element and SVG
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(' ');
})
}
}
}



Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more