Public
Edited
Jun 24, 2024
1 star
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
filters
Insert cell
Insert cell
Insert cell
Insert cell
accounts = [...new Set(data.map(d=>d.user))]
Insert cell
Insert cell
Insert cell
getHashtagsNetwork = (data, mapFunction, options = {}) => {
const { key, toLowerCase, toRemove, minCount, minWeight } = options;

const graph = new Graph({
multi: false,
allowSelfLoops: false,
type: "undirected"
});

data.map(mapFunction).forEach((d) => {
if (d?.constructor === Array) {
if (toLowerCase) {
d = d.map((d) => d.toLowerCase());
}
d = [...new Set(d)];
} else {
d = [];
}

const thisNetwork = list2network(d);

thisNetwork.nodes.forEach((node) => {
graph.updateNode(node.id, (attr) => {
return {
...attr,
label: node.id,
group: "hashtag",
count: (attr.count || 0) + 1
};
});
});

thisNetwork.edges.forEach((edge) => {
const _update = graph.updateEdge(edge.source, edge.target, (attr) => {
return {
...attr,
weight: (attr.weight || 0) + 1
};
});
});
});

if (toRemove) {
toRemove.split("\n").forEach((id) => {
if (graph.hasNode(id)) {
graph.dropNode(id);
}
});
}

if (minCount) {
graph.forEachNode((node, attributes) => {
if (attributes.count < minCount) {
graph.dropNode(node);
}
});
}

if (minWeight) {
graph.forEachEdge(
(
edge,
attributes,
source,
target,
sourceAttributes,
targetAttributes
) => {
if (attributes.weight < minWeight) {
graph.dropEdge(edge);
}
}
);
}

return graph.export();
}
Insert cell
Insert cell
hashtagsNetworkOptions
Insert cell
Insert cell
networkGraph(hashtagsNetwork, {
type: hashtagsNetwork.options.type,
layoutIterations: 256
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
usersNetwork = getUsersNetwork(
data,
(d) => {
const source = d.user;
const targets = [...new Set(d.usertags.concat(d.mentionedUsersCaption))];
return { source, targets };
},
usersNetworkOptions
)
Insert cell
networkGraph(usersNetwork, {
type: usersNetwork.options.type,
layoutIterations: 1024
})
Insert cell
Insert cell
DOM.download(new Blob([getGexfString(usersNetwork, {type: usersNetwork.options.type})], {type: "application/application/vnd.geometry-explorer"}), "usersNetwork.gexf", "Download usersNetwork.gexf")
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viz_usersHashtagsNetwork = networkGraph(usersHashtagsNetwork, {
type: usersHashtagsNetwork.options.type,
layoutIterations: 256
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
networkGraph(locationHashtagNetwork, {
type: locationHashtagNetwork.options.type,
layoutIterations: 256
})
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
networkGraph = (data, options = {key}) => {
let { height, type, layoutIterations } = options;
if (!height) height = window.screen.availHeight;
if (!type) type = "undireceted";
if (!layoutIterations) layoutIterations = 64;
// The force simulation mutates links and nodes, so create a copy
// so that re-evaluating this cell produces the same result.
const nodes = data.nodes.map((d) => ({ id: d.key, ...d.attributes }));
const links = data.edges.map((d) => ({
id: d.key,
source: d.source,
target: d.target,
...d.attributes
}));

data.spatialized = {
links,
nodes
};

// Specify the color scale.
//const color = d3.scaleOrdinal(d3.schemeCategory10);
const thickness = d3.scaleLinear(
[1, d3.max(links, (d) => d.weight)],
[1, 10]
);
const label_size = 12;
let new_label_size = label_size,
label_size_hover = label_size * 1.3;

// Create a simulation with several forces.
const simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3.forceLink(links).id((d) => d.id)
)
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.force(
"collision",
d3.forceCollide((d) => size(d.count) + 0.5)
)
.stop()
.tick(layoutIterations);
//.on("tick", ticked)
//.on("end", () => { label.attr("display", "block"); });

// Create the SVG container.
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, window.screen.availWidth, height])
.style("background-color", "#fafafa");

const g = svg.append("g");

// Add a line for each link, and a circle for each node.
const link = g
.append("g")
.selectAll()
.data(links)
.join(type === "directed" ? "path" : "line")
.attr("fill", "none")
.attr("stroke-width", (d) => thickness(d.weight))
.attr("stroke", "#999")
.attr("stroke-opacity", 0.25);

const node = g
.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 0.5)
.selectAll()
.data(nodes)
.join("circle")
.attr("r", (d) => {
d.size = size(d.count);
return d.size;
})
.attr("fill", (d) => {
d.color = color(d.group);
return d.color;
})
.style("cursor", "pointer")
.on("mouseenter", function (event) {
const id = event.target.__data__.id;
const connected = [];
const filteredLinks = links.filter(
(e) => e.source.id == id || e.target.id == id
);
filteredLinks.forEach((e) => {
if (connected.indexOf(e.source) < 0) {
connected.push(e.source.id);
}
if (connected.indexOf(e.target) < 0) {
connected.push(e.target.id);
}
});
node.filter((d) => connected.indexOf(d.id) < 0).attr("fill", "white");
link
.attr("stroke-opacity", 0.25)
.filter((e) => e.source.id == id || e.target.id == id)
.attr("stroke-opacity", 1);
label
.style("opacity", 0.25)
.filter((d) => connected.indexOf(d.id) > -1)
.style("opacity", 1)
.attr("fill", "#000")
.filter((d) => d.id === event.target.__data__.id)
.attr("font-weight", "bold")
.attr("font-size", label_size_hover)
.raise();
})
.on("mouseleave", function (event) {
node.attr("fill", (d) => color(d.group));
link.attr("stroke-opacity", 0.25);
label
.style("opacity", 1)
.attr("fill", "#666")
.attr("font-weight", "normal")
.attr("font-size", new_label_size);
})
.on("click", function (event) {
const d = event.target.__data__;
if (d.group === "post") {
window.open(`https://www.instagram.com/p/${d.id}`, "_blank").focus();
} else if (d.group === "account" || d.group === "new account") {
window.open(`https://www.instagram.com/${d.id}`, "_blank").focus();
} else if (d.group === "hashtag") {
window
.open(
`https://www.instagram.com/explore/tags/${d.id.replace(/^#/g, "")}`,
"_blank"
)
.focus();
}
});

const label = g
.append("g")
.selectAll()
.data(nodes)
.join("text")
// .attr("display", "none")
.attr("fill", "#666")
.attr("font-family", "sans-serif")
.attr("font-size", new_label_size)
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.text((d) => d.label);

node.append("title").text((d) => d.id + "-" + d.count);
link.append("title").text((d) => d.id + "-" + d.weight);

ticked();

// Add a drag behavior.
node.call(
d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended)
);

// links are drawn as curved paths between nodes,
// through the intermediate nodes
function positionLink(d) {
var offset = 20;

var midpoint_x = (d.source.x + d.target.x) / 2;
var midpoint_y = (d.source.y + d.target.y) / 2;

var dx = d.target.x - d.source.x;
var dy = d.target.y - d.source.y;

var normalise = Math.sqrt(dx * dx + dy * dy);

var offSetX = midpoint_x + offset * (dy / normalise);
var offSetY = midpoint_y - offset * (dx / normalise);

return (
"M" +
d.source.x +
"," +
d.source.y +
"S" +
offSetX +
"," +
offSetY +
" " +
d.target.x +
"," +
d.target.y
);
}

// Set the position attributes of links and nodes each time the simulation ticks.
function ticked() {
if (type === "directed") {
link.attr("d", positionLink);
} else {
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);
label.attr("x", (d) => d.x).attr("y", (d) => d.y);
}

// Reheat the simulation when drag starts, and fix the subject position.
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}

// Update the subject (dragged node) position during drag.
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}

// Restore the target alpha so the simulation cools after dragging ends.
// Unfix the subject position now that it’s no longer being dragged.
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}

// When this cell is re-run, stop the previous simulation. (This doesn’t
// really matter since the target alpha is zero and the simulation will
// stop naturally, but it’s a good practice.)
invalidation.then(() => simulation.stop());

svg.call(
d3
.zoom()
.extent([
[0, 0],
[width, height]
])
.scaleExtent([0.1, 8])
.on("zoom", zoomed)
);

function zoomed({ transform }) {
g.attr("transform", transform);
new_label_size = label_size / transform.k;
label_size_hover = new_label_size * 1.3;
label.attr("font-size", new_label_size);
}

return svg.node();
}
Insert cell
gexf = require("https://bundle.run/graphology-gexf@0.8.1");
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