Skip to content

Instantly share code, notes, and snippets.

@djfan
Last active June 9, 2023 00:43
Show Gist options
  • Save djfan/b9594f33af0f588c6ca1cb138b712bee to your computer and use it in GitHub Desktop.
Save djfan/b9594f33af0f588c6ca1cb138b712bee to your computer and use it in GitHub Desktop.
hahah
groupid id size related
0 Psychology 1 3 ['Education', 'Sociology', 'Personal Development']
1 Finance 1.1 4 ['Personal Development', 'Business', 'Business', 'Philosophy']
2 Sociology 1.2 3 ['Psychology', 'Political Science', 'International Relations']
3 Education 1.1.1 1 ['Psychology']
4 Biology 1.1.2 2 ['Anthropology', 'History']
5 Management 1.1.3 2 ['Business', 'Leadership']
6 History 1.2.1 4 ['Biography', 'Anthropology', 'Political Science', 'Biology']
7 Innovation 8 2 ['Business', 'Entrepreneurship']
8 Personal Development 8.1 5 ['Business', 'Finance', 'Psychology', 'Philosophy', 'Business']
9 Political Science 8.2 4 ['International Relations', 'Sociology', 'History', 'Biography']
10 Leadership 8.3 4 ['Business', 'Business', 'Management', 'Entrepreneurship']
11 Biography 8.2.1 2 ['History', 'Political Science']
12 International Relations 8.2.2 2 ['Political Science', 'Sociology']
13 Philosophy 9 4 ['Personal Development', 'Business', 'Business', 'Finance']
14 Business 10 13 ['Entrepreneurship', 'Leadership', 'Entrepreneurship', 'Personal Development', 'Finance', 'Leadership', 'Innovation', 'Management', 'Personal Development', 'Philosophy', 'Entrepreneurship', 'Philosophy', 'Finance']
15 Entrepreneurship 10.1 5 ['Business', 'Business', 'Business', 'Innovation', 'Leadership']
16 Anthropology 11 2 ['History', 'Biology']
<!DOCTYPE html>
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<h1 class="inGreen">PKN</h1>
<style>
.inGreen { color: green; }
</style>
<body>
<div id="d3_div" height=200 width=450></div>
<div id="d3_div2" height=10 width=1800></div>
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
// new bubble chart instance
let myBubbleChart = bubbleChart();
function display(data) {
myBubbleChart('#d3_div', data,
{label: d => d.id,
value: d => d.cnt,
group: d => d.group
});
}
// d3.csv('./book.csv').then(display);
let myBubbleChart2 = BubbleChart();
function display2(data) {
myBubbleChart2('#d3_div2', data,
{
name: d => d.groupid,
label: d => `${d.groupid}\n\n${d.size}`, //[...d.id.split(".").pop().split(/(?=[A-Z][a-z])/g), d.value.toLocaleString("en")].join("\n"),
value: d => d.size,//value,
group: d => d.id.split(".")[0],
title: d => `${d.groupid}\n${d.size}`,
link: d => `https://thelinkingknowledge.com/resources/${d.groupid.replace(/\ /g, "%20").toLowerCase()}`,
related: d => d.related,
width: 2000,
height:1000
});
}
d3.csv('./book.csv').then(display2);
// d3.csv('./flare.csv').then(display2)
function BubbleChart() {
let chart = function chart(selector, data, {
name = ([x]) => x, // alias for label
label = name, // given d in data, returns text to display on the bubble
value = ([, y]) => y, // given d in data, returns a quantitative size
group, // given d in data, returns a categorical value for color
title, // given d in data, returns text to show on hover
related, //related topics
link, // given a node d, its link (if any)
linkTarget = "_blank", // the target attribute for links, if any
width = 640, // outer width, in pixels
height = width, // outer height, in pixels
padding = 3, // padding between circles
margin = 1, // default margins
marginTop = margin, // top margin, in pixels
marginRight = margin, // right margin, in pixels
marginBottom = margin, // bottom margin, in pixels
marginLeft = margin, // left margin, in pixels
groups, // array of group names (the domain of the color scale)
colors = d3.schemeTableau10, // an array of colors (for groups)
fill = "#ccc", // a static fill color, if no group channel is specified
fillOpacity = 0.7, // the fill opacity of the bubbles
stroke, // a static stroke around the bubbles
strokeWidth, // the stroke width around the bubbles, if any
strokeOpacity, // the stroke opacity around the bubbles, if any
} = {}) {
// Compute the values.
console.log(data.length);
const D = d3.map(data, d => d);
const V = d3.map(data, value);
// const N = d3.map(data, name);
const G = group == null ? null : d3.map(data, group);
const I = d3.range(V.length).filter(i => V[i] > 0);
// Unique the groups.
if (G && groups === undefined) groups = I.map(i => G[i]);
groups = G && new d3.InternSet(groups);
// Construct scales.
const color = G && d3.scaleOrdinal(groups, colors);
// Compute labels and titles.
const L = label == null ? null : d3.map(data, label);
const N = name == null ? null : d3.map(data, name);
const T = title === undefined ? L : title == null ? null : d3.map(data, title);
const R = related == null ? null : d3.map(data, related);
// Compute layout: create a 1-deep hierarchy, and pack it.
const root = d3.pack()
.size([width - marginLeft - marginRight, height - marginTop - marginBottom])
.padding(padding)
(d3.hierarchy({children: I})
.sum(i => V[i]));
const svg = d3.select(selector)
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-marginLeft, -marginTop, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.attr("fill", "currentColor")
.attr("font-size", 15)
.attr("font-family", "Gill Sans")
.attr("text-anchor", "middle");
const leaf = svg.selectAll("a")
.data(root.leaves())
.join("a")
.attr("xlink:href", link == null ? null : (d, i) => link(D[d.data], i, data))
.attr("target", link == null ? null : linkTarget)
.attr("transform", d => `translate(${d.x},${d.y})`)
.attr("name", d => D[d.data].groupid);
function mouseover(d) {
// console.log("mouseover");
d3.selectAll("circle")
.filter(function(c) {
//console.log("d", d3.select(d).attr("related"));
let array = JSON.parse(d3.select(d).attr("related").replace(/'/g, '"')); //console.log(array);
// console.log(N[c.data], array);
return (array.includes(N[c.data]) && N[c.data] != d3.select(d).attr("name"));
})
.attr("fill-opacity", 0.1)
.attr("stroke-opacity", 0.1)
.attr("r", 0.1);
// console.log("mouseover2");
}
function mouseout(d) {
d3.selectAll("circle")
.attr("fill-opacity", 0.7)
.attr("stroke-opacity", 1)
.attr("r", d => d.r);
}
leaf.append("circle")
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.attr("fill", G ? d => color(G[d.data]) : fill == null ? "none" : fill)
.attr("fill-opacity", fillOpacity)
.attr("r", d => d.r)
.attr("name", d => N[d.data])
.attr("related", d => R[d.data])
.on('mouseover', function (d, i) {
// console.log(d3.select(d));
d3.select(this).transition()
.duration('500')
.attr('opacity', '.7');
mouseover(this);})
.on('mouseout', function (d, i) {
d3.select(this).transition()
.duration('500')
.attr('opacity', '1');
mouseout(d3.select(this));});
if (T) {
leaf.append("title")
.text(d => T[d.data]);
}
if (L) {
// A unique identifier for clip paths (to avoid conflicts).
const uid = `O-${Math.random().toString(16).slice(2)}`;
leaf.append("clipPath")
.attr("id", d => `${uid}-clip-${d.data}`)
.append("circle")
.attr("r", d => d.r)
.attr("name", d => d.name);
leaf.append("text")
.attr("clip-path", d => `url(${new URL(`#${uid}-clip-${d.data}`, location)})`)
.selectAll("tspan")
.data(d => `${L[d.data]}`.split(/\n/g))
.join("tspan")
.attr("x", 0)
.attr("y", (d, i, D) => `${i - D.length / 2 + 0.85}em`)
.attr("fill-opacity", (d, i, D) => i === D.length - 1 ? 0.7 : null)
.text(d => d);
}
// let leaf2 = svg.selectAll("a")
// .data(root.leaves())
// .join("a")
// .attr("xlink:href", link == null ? null : (d, i) => link(D[d.data], i, data))
// .attr("target", link == null ? null : linkTarget)
// .attr("transform", d => `translate(${d.x},${d.y})`)
// .attr("name", d => D[d.data].groupid)
// .on("mousemove", d => {console.log("source: " + this.id);});
// leaf.selectAll("circle")
// .on("mousemove", d => {
// console.log("source: " + d.name + ", target: " + d.target)
// })
}
return chart;
}
function bubbleChart() {
const width = 940;
const height = 500;
// location to centre the bubbles
const centre = { x: width/2, y: height/2 };
// strength to apply to the position forces
const forceStrength = 0.03;
// these will be set in createNodes and chart functions
let svg = null;
let bubbles = null;
let labels = null;
let nodes = [];
// charge is dependent on size of the bubble, so bigger towards the middle
function charge(d) {
return Math.pow(d.radius, 2.0) * 0.01
}
// create a force simulation and add forces to it
const simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody().strength(charge))
.force('center', d3.forceCenter(centre.x, centre.y))
.force('x', d3.forceX().strength(forceStrength).x(centre.x))
.force('y', d3.forceY().strength(forceStrength).y(centre.y))
.force('collision', d3.forceCollide().radius(d => d.radius + 1));
// force simulation starts up automatically, which we don't want as there aren't any nodes yet
simulation.stop();
// set up colour scale
const fillColour = d3.scaleOrdinal()
.domain(["1", "2", "3", "5", "99"])
.range(["#0074D9", "#7FDBFF", "#39CCCC", "#3D9970", "#AAAAAA"]);
// data manipulation function takes raw data from csv and converts it into an array of node objects
// each node will store data and visualisation values to draw a bubble
// rawData is expected to be an array of data objects, read in d3.csv
// function returns the new node array, with a node for each element in the rawData input
function createNodes(rawData) {
// use max size in the data as the max in the scale's domain
// note we have to ensure that size is a number
const maxSize = d3.max(rawData, d => +d.size);
// size bubbles based on area
const radiusScale = d3.scaleSqrt()
.domain([0, maxSize])
.range([0, 80])
// use map() to convert raw data into node data
const myNodes = rawData.map(d => ({
...d,
radius: radiusScale(+d.size),
size: +d.size,
x: Math.random() * 900,
y: Math.random() * 800
}))
return myNodes;
}
// main entry point to bubble chart, returned by parent closure
// prepares rawData for visualisation and adds an svg element to the provided selector and starts the visualisation process
let chart = function chart(selector, rawData) {
// convert raw data into nodes data
nodes = createNodes(rawData);
// create svg element inside provided selector
svg = d3.select(selector)
.append('svg')
.attr('width', width)
.attr('height', height)
// bind nodes data to circle elements
const elements = svg.selectAll('.bubble')
.data(nodes, d => d.id)
.enter()
.append('g')
bubbles = elements
.append('circle')
.classed('bubble', true)
.attr('r', d => d.radius)
.attr('fill', d => fillColour(d.groupid))
// labels
labels = elements
.append('text')
.attr('dy', '.3em')
.style('text-anchor', 'middle')
.style('font-size', 10)
.text(d => d.id)
// set simulation's nodes to our newly created nodes array
// simulation starts running automatically once nodes are set
simulation.nodes(nodes)
.on('tick', ticked)
.restart();
}
// callback function called after every tick of the force simulation
// here we do the actual repositioning of the circles based on current x and y value of their bound node data
// x and y values are modified by the force simulation
function ticked() {
bubbles
.attr('cx', d => d.x)
.attr('cy', d => d.y)
labels
.attr('x', d => d.x)
.attr('y', d => d.y)
}
// return chart function from closure
return chart;
}
</script>
<div id="d3_div3" height=200 width=450></div>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment