Public
Edited
Oct 22, 2020
1 star
Insert cell
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, totalWidth, totalHeight])
.style('background','white');
// Setup D3 beeswarm simulation (a force layout with year and position constraints)
const simulation = d3.forceSimulation(nodes_JSON)
.force("x", d3.forceX(function(d) {
return xScale(d.Year);
}).strength(1))
.force("y", d3.forceY(function(d) {
return yScale(d.Xposition);
}))
.force("collide", d3.forceCollide(function(d){
return radius(d.citations+1)
}))
.stop();
simulation.tick(120);
////////////////////////////
// ADD NODES TO THE PAGE
const circles = svg.append('g').selectAll(null)
.data(nodes_JSON)
.enter()
.append("circle")
.attr("id", function(d){
return "p_"+d.ID
})
.attr("year", function(d){
return d.Year
})
.attr("r", function(d){
return radius(d.citations)
})
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})

// style them
.attr("fill", function(d) { if (d.citations > 0){ return "RebeccaPurple" } else {return "LightGrey"} })
.style("stroke", function(d) { if (d.citations > 0){ return "none" } else {return "RebeccaPurple"} })
.style("opacity","0.8")

// add mouse over event listeners
.on("mouseover", function(d) {
onMouseOver(d)
})
.on("mouseout", function(d) {
onMouseOut(d)
});
function onMouseOver(d){
let circle = document.getElementById("p_"+d.ID)
circle.style.stroke = "none";
circle.setAttribute("fill", "MidnightBlue");
tooltip.transition()
.duration(200)
.style("opacity", 1)
tooltip.html("<b>"+ d.title + "</b><br/>" + d.creator + ", "+ d.Year +"<br/><br/>" + d.citations + " citations");
//showLinks(d);
}


function onMouseOut(d){
let circle = document.getElementById("p_"+d.ID)
if (d.citations > 0) {
circle.setAttribute("fill", "RebeccaPurple");
}
else {
circle.style.stroke = "RebeccaPurple";
circle.setAttribute("fill", "LightGrey");
}
tooltip.transition()
.duration(500)
.style("opacity", 0);
//hideLinks(d);
}
///////////////////////////
// LEGEND
const legend = svg.append("foreignObject")
.attr("width",width)
.attr("height",height)
.style("pointer-events","none")
.append("xhtml:div")
.attr("class", "legend")
.style("opacity", 1)
.style("position", "absolute")
.style("width", "360px")
.style("top", (margins + 70)+"px")
.style("left", (margins)+"px")
.style("padding", "10px")
.style("font", "10px sans-serif")
.style("color","grey")
.style("line-height", "175%")
.style("background", "none")
.style("border", "none")
.style("border-radius", "4px")
.style("pointer-events", "none");

legend.html("<span style='height:6px; width:6px; border-radius:50%; background-color:LightGrey; border:1px solid RebeccaPurple; display:inline-block; margin-left:5px; margin-right:9px; margin-bottom:1px;'></span>Articles with no citations<br><span style='height:7px; width:7px; border-radius:50%; background-color:RebeccaPurple; border:1px solid RebeccaPurple; display:inline-block; margin-left: 4px; margin-right:9px; margin-bottom:1px;'></span>Articles with one citation<br><span style='height:15px; width:15px; border-radius:50%; background-color:RebeccaPurple; border:1px solid RebeccaPurple; display:inline-block; margin-right:5px; margin-bottom:-4px;'></span>Articles with many citations");
///////////////////////////
// TOOLTIP
const tooltip = svg.append("foreignObject")
.attr("width",width)
.attr("height",height)
.style("pointer-events","none")
.append("xhtml:div")
.attr("class", "tooltip")
.style("opacity", 0)
.style("position", "absolute")
.style("width", "360px")
//.style("height", "28px")
.style("top", (margins + 70)+"px")
.style("left", (margins)+"px")
.style("padding", "10px")
.style("font", "16px sans-serif")
.style("line-height", "125%")
.style("vertical-align","middle")
.style("background", "white")
.style("border", "solid 1px RebeccaPurple")
.style("border-opacity","0.5")
.style("border-radius", "2px")
.style("pointer-events", "none");
///////////////////////
// ADD X-AXIS
const x_axis = d3.axisBottom()
.scale(xScale)
.tickFormat(d3.format("d"));
svg.append("g")
.attr("transform", "translate(0, "+(totalHeight-margins)+")")
.call(x_axis);

// ADD TITLE
svg.append("text")
.attr("x", margins)
.attr("y", margins)
.attr("text-anchor", "left")
.style("font", "18px sans-serif")
.text("How many articles about the future of work are published each year?");

// ADD SUB-TITLE
svg.append("text")
.attr("x", margins)
.attr("y", margins + 30)
.attr("text-anchor", "left")
.style("font", "14px sans-serif")
.text("And which ones are cited the most?");

// COUNT TOTAL PUBLICATIONS FOR EACH YEAR AND GET Y-POS FOR ANNOTATION
// initialize an array of annotations for each year
let annotations = [];
for (let i=startYear; i<=endYear; i++) {
annotations.push({"year":i, "pubs":0, "topY":totalHeight})
}
// iterate through data.nodes to count pubs per year and find the highest Y position
for (let i=0; i<nodes_JSON.length; i++) {
if (nodes_JSON[i].Year >= startYear && nodes_JSON[i].Year <= endYear) {
for (let j=0; j<annotations.length; j++) {
if (annotations[j].year == nodes_JSON[i].Year) {
annotations[j].pubs++
if (nodes_JSON[i].y < annotations[j].topY) annotations[j].topY = nodes_JSON[i].y
}
}
}
}
// ADD LABELS - COUNT FOR EACH COLUMN
svg.selectAll("text.annotations")
.data(annotations)
.enter()
.append("text")
.classed('annotations', true)
.attr("x", function(d) { return xScale(d.year)})
.attr("y", function(d) { return d.topY - 20})
.style("text-anchor","middle")
.style("font", "10px sans-serif")
.style("fill","grey")
.text(function(d){ return d.pubs });

return svg.node();
}
Insert cell
md`Set parameters`
Insert cell
startYear = 2000;
Insert cell
endYear = 2020;
Insert cell
maxPapers = 150; // this really should be done by looking at the data
Insert cell
{
// FILTER OUT ANY NODES BEFORE STARTYEAR AND AFTER ENDYEAR
for(let i = 0; i < nodes_JSON.length; i++){
if ( Number(nodes_JSON[i].Year) < startYear || Number(nodes_JSON[i].Year) > endYear ) {
nodes_JSON.splice(i, 1);
i--;
}
}
}
Insert cell
{
// FILTER OUT ANY EDGES THAT POINT TO NODES THAT AREN'T GOING TO BE DRAWN
for (let i=0; i<edges_JSON.length; i++) {
if ( isValid(edges_JSON[i].Source) && isValid(edges_JSON[i].Target) ) {
// do nothing
}
else {
edges_JSON.splice(i, i);
i--;
}
}
}

Insert cell
// CHECK TO SEE IF A NODE THAT IS CALLED FOR IS VALID
function isValid(id) {
let answer = false;
for(let i = 0; i < nodes_JSON.length; i++){
if (id == nodes_JSON[i].ID) answer = true;
}
return answer
}
Insert cell
md`D3 helper functions to set scales`
Insert cell
xScale = d3.scaleLinear()
.domain([startYear, endYear]).nice()
.range([margins, totalWidth-margins]);
Insert cell
yScale = d3.scaleLinear()
.domain([0,maxPapers]).nice()
.range([totalHeight-(margins*1.5), margins]);
Insert cell
radius = d3.scaleSqrt()
.domain([0,maxPapers])
.range([2,20]);
Insert cell
/*
This link stuff all needs refactoring to work here
////////////////////////////
// ADD EDGES TO THE PAGE
var link = svg.append("g")
.selectAll("path")
.data(edges_JSON)
.enter().append("path")
.attr("class", "link")
.attr("stroke-width", "1")
.attr("stroke", "MidnightBlue")
.attr("stroke-dasharray", "none")
.attr("opacity","0") // make invisible to begin
.attr("fill","none")
.attr("source", function(d) { return "p_" + d.Source })
.attr("target", function(d) { return "p_" + d.Target })

link.attr("d", function(d) {
var dx = parseFloat(findNodeById(d.Target).x) - parseFloat(findNodeById(d.Source).x),
dy = parseFloat(findNodeById(d.Target).y) - parseFloat(findNodeById(d.Source).y),
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + findNodeById(d.Source).x + "," + findNodeById(d.Source).y + "A" + dr + "," + dr + " 0 0,1 " + findNodeById(d.Target).x + "," + findNodeById(d.Target).y;
});
// helper function to get data from the right node
function findNodeById(id) {
// get it from the data
for (let i=0; i<data.nodes.length; i++) {
if (nodes_JS[i].ID == id) {
return data.nodes[i];
}
}
}
*/

//console.log(annotations)
/*
// show links associated with selected node
function showLinks(d) {
let allLinks = document.getElementsByClassName("link");
for (let i=0; i < allLinks.length; i++){
let source = allLinks[i].getAttribute("source")
let target = allLinks[i].getAttribute("target")
// show sources -- papers that cite the selected paper
if (source == "p_"+d.ID) {
allLinks[i].setAttribute("opacity","1");
}
// show targets -- papers listed in the selected paper's reference section
if (target == "p_"+d.ID) {
allLinks[i].setAttribute("stroke","grey");
allLinks[i].setAttribute("opacity","1");
allLinks[i].setAttribute("stroke-dasharray", ("2, 2"))
}
}
}
// hide all links
function hideLinks(d) {
let allLinks = document.getElementsByClassName("link");
for (let i=0; i < allLinks.length; i++){
allLinks[i].setAttribute("stroke","MidnightBlue");
allLinks[i].setAttribute("opacity","0");
allLinks[i].setAttribute("stroke-dasharray", "none")
}
}
*/

Insert cell
Insert cell
nodes_temp = FileAttachment("FOW Nodes 2019-11-20.csv").text()
Insert cell
nodes_JSON = d3.csvParse(nodes_temp, d3.autoType)
Insert cell
edges_temp = FileAttachment("FOW Edges 2019-11-14.csv").text()
Insert cell
edges_JSON = d3.csvParse(edges_temp, d3.autoType)
Insert cell
md`Setup page layout parameters`
Insert cell
height = 600
Insert cell
width = 900
Insert cell
margins = 50
Insert cell
totalWidth = width + (margins * 2)
Insert cell
totalHeight = height + (margins * 2)
Insert cell
md`Load D3 Library`
Insert cell
d3 = require("d3@5")
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