Published unlisted
Edited
May 11, 2022
Insert cell
# Relationship of the top Hashtags and Ads Categories
Insert cell
chart = {
var nodeId = d => d.id, // given d in nodes, returns a unique identifier (string)
nodeGroup = d => d.group, // given d in nodes, returns an (ordinal) value for color
nodeGroups, // an array of ordinal values representing the node groups
nodeFill = "currentColor", // node stroke fill (if not using a group color encoding)
nodeStroke = "#fff", // node stroke color
nodeStrokeWidth = l => l.value, // node stroke width, in pixels
nodeStrokeOpacity = 1, // node stroke opacity
nodeRadius = 40, // node radius, in pixels
nodeStrength,
linkSource = ({source}) => source, // given d in links, returns a node identifier string
linkTarget = ({target}) => target, // given d in links, returns a node identifier string
linkStroke = "#999", // link stroke color
linkStrokeOpacity = 0.6, // link stroke opacity
linkStrokeWidth = 1.5, // given d in links, returns a stroke width in pixels
linkStrokeLinecap = "round", // link stroke linecap
linkStrength,
colors = d3.schemeTableau10, // an array of color strings, for the node groups
height = 1000, // outer height, in pixels

paths,
groups,
groupIds,
scaleFactor = 1.5,
polygon,
centroid,
valueline = d3.line()
.x(function(d) { return d[0]; })
.y(function(d) { return d[1]; })
.curve(d3.curveCatmullRomClosed)

const nodes = data.nodes;
const links = data.links;
// Construct the scales.
// Manual add of d3.schemetableau10 and
const color = d3.scaleOrdinal(["#4e79a7", "#f28e2c", "#e15759", "#76b7b2", "#59a14f", "#edc949", "#af7aa1", "#ff9da7", "#9c755f", "#bab0ab", "#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f", "#e5c494", "#b3b3b3"]);

function nodeRadiusfunc(d) {
if (d.title) {
return nodeRadius;
}
return nodeRadius * 3/5;
}
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(function (l) {
if (l.inner_categ) {
return 50;
}
return 300;
}))
.force("center", d3.forceCenter().id)
.force("collision", d3.forceCollide().radius(nodeRadiusfunc))
.force("charge", d3.forceManyBody())
.on("tick", ticked);

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");

svg
.append("text")
.text("Ads").attr("x", -width / 4 - 50).attr("y", -height/2 + 50)
.style("font-size","50px")
.style("text-align", "center");
svg
.append("text")
.text("Hashtags").attr("x", width / 4 - 100).attr("y", -height/2 + 50)
.style("font-size","50px")
.style("text-align", "center");

svg.append('line')
.style("stroke", "black")
.style("stroke-width", 5)
.attr("x1", 0)
.attr("y1", height/2)
.attr("x2", 0)
.attr("y2", -height/2);

// create groups, links and nodes
groups = svg.append('g').attr('class', 'groups');

const link = svg.append("g")
.attr("stroke", typeof linkStroke !== "function" ? linkStroke : null)
.attr("stroke-opacity", linkStrokeOpacity)
.attr("stroke-width", typeof linkStrokeWidth !== "function" ? linkStrokeWidth : null)
.attr("stroke-linecap", linkStrokeLinecap)
.selectAll("line")
.data(links)
.join("line");

const node = svg.append('g')
.attr('class', 'nodes')
.selectAll('g')
.data(nodes)
.enter().append("g")
.call(drag(simulation));
const circle = node
.append('circle')
.attr('r', nodeRadiusfunc)
.attr('fill', function(d) { return color(d.group % 100); })
.attr("opacity", function(d) {
if (d.group == 3 || d.group == 0) {
return 1;
}
var other_count = 0;
if (d.group > 100) {
other_count = nodes.filter(function(n) { return (+n.group == d.group - 100); }).length;
}
if (d.group < 100) {
other_count = nodes.filter(function(n) { return (+n.group == d.group + 100); }).length;
}

if (other_count == 0) {
return 0.3
}
return 1;
});

var labels = node
.append("text")
.text(function(d) { return d.id; })
.attr("text-anchor", "middle")
.attr("x", 0)
.attr("y", 5)
.call(wrap, 10);

// count members of each group. Groups with less
// than 3 member will not be considered (creating
// a convex hull need 3 points at least)
groupIds = Array.from(new Set(nodes.map(function(n) { return +n.group; })),
function(groupId) {
return {
groupId : groupId,
count : nodes.filter(function(n) { return +n.group == groupId; }).length
};
}).filter((group) => (group.count > 2)).map(group => group.groupId);

paths = groups.selectAll('.path_placeholder')
.data(groupIds, function(d) { return +d; })
.enter()
.append('g')
.attr('class', 'path_placeholder')
.append('path')
.attr('stroke', function(d) { return color(d % 100); })
.attr('fill', function(d) { return color(d % 100); })
.attr('opacity', 0);

paths
.transition()
.duration(2000)
.attr('opacity', 1);

// add interaction to the groups
groups.selectAll('.path_placeholder')
.call(d3.drag()
.on('start', group_dragstarted)
.on('drag', group_dragged)
.on('end', group_dragended)
);

simulation
.nodes(nodes)
.on('tick', ticked)
.force('link')
.links(links);

function ticked() {
node
.attr('cx', function(d) {
d.x = Math.min(Math.max(-width/2 + (nodeRadius * 1.2), d.x), width/2 - (nodeRadius * 1.2));
if (d.ad !== undefined) {
if (d.ad) {
// Stay on the left
d.x = Math.min(-(nodeRadius * 1.2), d.x)
} else {
// Stay on the right
d.x = Math.max((nodeRadius * 1.2), d.x)
}
}
return d.x;
})
.attr('cy', function(d) {
d.y = Math.min(Math.max(-height/2 + 150, d.y), height/2 - (nodeRadius * 1.2));
return d.y;
})
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
link
.attr('x1', function(d) { return d.source.x; })
.attr('y1', function(d) { return d.source.y; })
.attr('x2', function(d) { return d.target.x; })
.attr('y2', function(d) { return d.target.y; });
updateGroups();
}

// select nodes of the group, retrieve its positions
// and return the convex hull of the specified points
// (3 points as minimum, otherwise returns null)
var polygonGenerator = function(groupId) {
var node_coords = node
.filter(function(d) { return d.group == groupId; })
.data()
.map(function(d) {
return [d.x, d.y];
});
return d3.polygonHull(node_coords);
};
function updateGroups() {
groupIds.forEach(function(groupId) {
var path = paths.filter(function(d) { return d == groupId;})
.attr('transform', 'scale(1) translate(0,0)')
.attr('d', function(d) {
polygon = polygonGenerator(d);
centroid = d3.polygonCentroid(polygon);
if (isNaN(centroid[0]) || isNaN(centroid[1])) {
return;
}
// to scale the shape properly around its points:
// move the 'g' element to the centroid point, translate
// all the path around the center of the 'g' and then
// we can scale the 'g' element properly
return valueline(
polygon.map(function(point) {
return [ point[0] - centroid[0], point[1] - centroid[1] ];
})
);
});
if (isNaN(centroid[0]) || isNaN(centroid[1])) {
return;
}
d3.select(path.node().parentNode).attr('transform', 'translate(' + centroid[0] + ',' + (centroid[1]) + ') scale(' + scaleFactor + ')');
});
}
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
// drag groups
function group_dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d3.select(this).select('path').style('stroke-width', 3);
}
function group_dragged(event) {
node
.filter(function(d) { return d.group == event.subject; })
.each(function(d) {
d.x += event.dx;
d.y += event.dy;
})
}
function group_dragended(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d3.select(this).select('path').style('stroke-width', 1);
}

function wrap(text, width) {
text.each(function () {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
x = text.attr("x"),
y = text.attr("y"),
dy = 0; //parseFloat(text.attr("dy")),
if (words.length == 1) {
return;
}
var tspan = text.text(null)
.append("tspan")
.attr("x", x)
.attr("y", y)
.attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.text().length > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan")
.attr("x", x)
.attr("y", y)
.attr("dy", ++lineNumber * lineHeight + dy + "em")
.text(word);
}
}
text.selectAll("tspan").attr("y", y - (2 * (lineNumber * lineHeight + dy)));
});
}

return svg.node();
}
Insert cell
<style>

body {
font-family: sans-serif, Arial;
font-size: 12px;
font-weight: bold;
}
.links line {
stroke: #999;
stroke-opacity: 0.6;
}

.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}

path {
fill-opacity: .1;
stroke-opacity: 1;
}

</style>

Insert cell
data = FileAttachment("hashtags-ads_relation@4.json").json();
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