Last active
June 9, 2023 00:43
-
-
Save djfan/b9594f33af0f588c6ca1cb138b712bee to your computer and use it in GitHub Desktop.
hahah
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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