Published
Edited
Jun 17, 2021
1 fork
95 stars
Insert cell
Insert cell
Insert cell
viewof mapColor = color({
value: "#d9d9d9"
})
Insert cell
function drawChart() {

// button
var markProgress = 0;
let currentSimId = 0;
/////////////////////////////////////////////////////////////////////////
//////////////////////////// Canvas /////////////////////////////////////
/////////////////////////////////////////////////////////////////////////
const wrapper = d3.select("#wrapper")
.append("svg")
.attr("viewBox",[0, 0, 330, 330])
.attr("width", 300)
.attr("height", 300)
const bounds = wrapper.append("g")
.style("transform", `translate(${dimensions.margin.left}px,
${dimensions.margin.top}px)`)
/////////////////////////////////////////////////////////////////////////
//////////////////////////// Element groups //////////////////////////////
/////////////////////////////////////////////////////////////////////////
bounds.append("g").attr("id","sankey")
bounds.append("g").attr("id","particles")
bounds.append("g").attr("id","stock-bars")
bounds.append("g").attr("id","catch-bars")
bounds.append("g").attr("id","catch-text")
bounds.append("g").attr("id","EEZ-labels")
/////////////////////////////////////////////////////////////////////////
//////////////////////////// Scales /////////////////////////////////////
/////////////////////////////////////////////////////////////////////////
const yScale = d3.scaleLinear()
.domain([0,1])
.range([0, dimensions.boundedHeight])
.clamp(true)
const xScale = d3.scaleLinear()
.domain([0,countryIDs.length-1])
.range([0, dimensions.boundedWidth])

const linkLineGenerator = d3.line()
.y((d,i) => i * (dimensions.boundedHeight / 5))
.x((d,i) => i <= 2
? xScale(d[0])
: xScale(d[1])
)
.curve(d3.curveMonotoneX)
const yTransitionProgressScale = d3.scaleLinear()
.domain([0.35, 0.65])
.range([0,1])
.clamp(true)

/////////////////////////////////////////////////////////////////////////
//////////////////////////// Draw Sankey ////////////////////////////////
/////////////////////////////////////////////////////////////////////////
const linkOptions = d3.merge(
//For every EEZ
countryIDs.map(startID => (
//For every fleet
countryIDs.map(endID => (
//Return array
new Array(6).fill([startID, endID])
))
))
)
const links = d3.select("#sankey").selectAll(".category-path")
.data(linkOptions)
.enter().append("path")
.attr("class", "category-path")
.attr("id", d => `EEZ${d[0][0]}`)
.attr("d", linkLineGenerator)
.attr("stroke-width", dimensions.pathWidth)
.attr("opacity", 1);
//List of paths & ids to reference for particle tracing
let linksData = [];
d3.selectAll(".category-path").each(function(d){
let id = `${d[0][0]} > ${d[0][1]}`
linksData.push({id: id, path: this});
})
sankeyLabels(d3.select("#EEZ-labels").append("g").attr("id", "sankey-label-bottom"), dimensions.pathWidth, dimensions.labelHeight, x, dimensions.boundedHeight-20, "sankey-label-bottom")
sankeyLabels(d3.select("#EEZ-labels").append("g").attr("id", "sankey-label-top"), dimensions.pathWidth, dimensions.labelHeight, x, 0, "sankey-label-top")
let pathLength = linksData[0].path.getTotalLength()
/////////////////////////////////////////////////////////////////////////
//////////////////////////// Draw Particles /////////////////////////////
/////////////////////////////////////////////////////////////////////////
let sims = []
//Draw static stock-bar background
d3.select("#stock-bars").selectAll(".static-bar")
.data(EEZstocks)
.join("rect")
.attr("class", "static-bar")
.attr("id", function(d,i){return countryCodes[i]})
.attr("y", d => -reverseY(d.stock))
.attr("x", (d,i) => x(i))
.attr("height", d => reverseY(d.stock))
.attr("width", dimensions.pathWidth)
.attr("rx",2)
.style("fill", "rgba(211, 211, 211,0.6)")
let f2 = d3.format(".4s")
d3.select("#stock-bars")
.append("g").attr("id", "stock-pct")
.selectAll("text")
.data(EEZtonnage2)
.join("text").attr("id", function(d,i){return countryCodes[i]})
.attr("x", (d,i) => x(i) + dimensions.pathWidth/2)
.attr("y", d => d.EEZ == "United Kingdom" ? -reverseYtonn(d.stock) - 24 : -reverseYtonn(d.stock) - 8)
.text(d => Math.round(d.stock/1000))
.style("text-anchor", "middle")
.style("fill", "#C2C2C2")
.style("font-size", 13.5)
.style("font-family", "Lato")
.append("tspan")
.attr("dy","1.2em")
.attr("x",0)
.text(d => `${d.EEZ == "United Kingdom" ? "(k tonnes)" : ""}`)
.style("font-size", 12)
//Draw catch pct text
d3.select("#catch-text")
.selectAll("text")
.data(fleetLandingsPct(simulatedLandings),(d,i) => i)
.enter()
.append("text")
.text(d => f(d[1]))
.attr("x", (d,i) => x(i) + dimensions.pathWidth/2)
.attr("y", d => y(d[0]) + 18)
.style("text-anchor", "middle")
.style("fill", "#C2C2C2")
.style("font-size", 13.5)
.style("font-family", "Lato")
.style("opacity", 0);
//Update markers
function updateMarkers(tick, payoutInterval, elapsed, EEZ, maxSims) {
//If there are still sims to payout, add either 1 or 2 sims to the data store, depdning on how big the EEZ stock is //(payoutInterval)
if (mutable currentSimId < maxSims) {
sims = [
...sims,
...d3.range(payoutInterval < 1 ? 2 : 1).map(() => generateSim(elapsed, EEZ, simulatedLandings, currentSimId))
]
}
//Recompute the stacked bar chart segments
let stackedLandings = stackLandings(simulatedLandings)
.map(d => (d.forEach(v => v.index = d.index), d))
//Re-bind circles to data
const particles = d3.select("#particles").selectAll(".marker-circle")
.data(sims, d => d.id)
//Create circles for new sims
particles.enter().append("circle")
.attr("class", "marker marker-circle")
.attr("r", 2.3)
.style("opacity", 1)
.style("fill", d => colorScale(d.EEZ))
//Remoe spent sims
particles.exit().remove()
const markers = d3.selectAll(".marker")
//Update circle positions
markers.each(function(d){
let path = linksData.filter(l => l.id == d.linkID)[0].path
d.current = ((elapsed - d.startTime) / simSpeed) * path.getTotalLength() * d.speed
d.currentPos = path.getPointAtLength(d.current);
})
markers
.attr("cx", d => d.currentPos.x + d.xJitter)
.attr("cy", d => d.currentPos.y);
//Remove spent sims from data store
sims = sims.filter(d => (
d.currentPos.y < pathLength - dimensions.labelHeight/2
));
/////////////////////////////////////////////////////////////////////////
//////////////////////////// Draw EEZ Stock Bars ////////////////////////
/////////////////////////////////////////////////////////////////////////
d3.select("#stock-bars").selectAll(".shrinking-bar")
.data(EEZstocks)
.join("rect")
.attr("class", "shrinking-bar")
.attr("y", d => -reverseY(d.stock))
.attr("x", (d,i) => x(i))
.attr("height", d => reverseY(d.stock))
.attr("width", dimensions.pathWidth)
.attr("rx",2)
.style("fill", (d,i) => colorScale(i));
/////////////////////////////////////////////////////////////////////////
//////////////////////////// Draw Catch Bars //////////////////////////
/////////////////////////////////////////////////////////////////////////

d3.select("#catch-bars").selectAll("g")
.data(stackedLandings)
.join("g").attr("id", d => `${whiteSpace(d.key)}-segments`)
.selectAll("rect")
.data(v => v)
.join("rect")
.attr("x", (v,i) => x(countryNames.indexOf(v.data.fleet)))
.attr("y", v => y(v[0]))
.attr("height", v => y(v[1]) - y(v[0]))
.attr("width", dimensions.pathWidth)
.style("fill", (d,i) => colorScale((countryIDs.length-1) - d.index))
.attr("rx",3)
d3.select("#catch-text").selectAll("text")
.data(fleetLandingsPct(simulatedLandings), (d,i) => i)
.attr("y", d => y(d[0]) + 18)
.text(d => Math.round(d[1]/1000));
//Update the progress of the ultimate mark, to know when to trigger the next EEZ
markProgress = d3.min(sims,d => d.currentPos.y);
}
/////////////////////////////////////////////////////////////////////////
///////////////// Loops ///////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////

let EEZ = 0
highlightEEZ(countryNames[EEZ])

function loopEEZs() {
//
let particleAnimations = function(){
let maxSims = EEZstocks[EEZ].stock
sims = []; //Clear sims data-store
mutable currentSimId = 0;
let duration = 180;
let payoutInterval = duration / maxSims;
let tick = 0;
markProgress = 0;
//Fade catch pct text in
if (EEZ == 0) {
d3.select("#catch-text").selectAll("text")
.transition().delay(simSpeed).duration(2500).style("opacity", 1);
}

let ticker = d3.timer(
function(elapsed){
tick ++
updateMarkers(tick, payoutInterval, elapsed, EEZ, maxSims)
let circlesComplete = markProgress >= pathLength - (dimensions.labelHeight/2) - 5;
//if all circles are dealt and there are more more EEZs to go, move onto next EEZ
if (EEZstocks[EEZ].stock <= 0 && circlesComplete && EEZ < 8) {

ticker.stop();
setTimeout(function(){loopEEZs(EEZ++)},1000);
// //Highlight focal EEZ labels
// d3.selectAll(".sankey-label-top").select("text").style("fill","#D3D3D3");
// d3.selectAll(".sankey-label-top").select("rect").style("stroke","#D3D3D3");
// let focalLabel = d3.select("#sankey-label-top").select(`#${countryCodes[EEZ]}`)
// focalLabel.select("text").style("fill","grey");
// focalLabel.select("rect").style("stroke","grey");
};
//if all circles are dealt and there are no more EEZs
if (EEZstocks[EEZ].stock <= 0 && circlesComplete && EEZ == 8) {
ticker.stop();
interactions();
}

})
}
//Colour each EEZ feature on iteration, then run particle animations
highlightEEZ(countryNames[EEZ], particleAnimations)

}
loopEEZs()

}
Insert cell
descText = function(EEZ){
d3.select("#EUpct").text(`(${f(EEZstockPct[EEZ])})`);
d3.select("#EEZtonnes").text(`${Math.round(EEZtonnage2[EEZ].stock/1000)}k tonnes`)
// .style("color", colorScale(EEZ));
d3.selectAll(".Nationality").text(Nationalities[EEZ]);
}
Insert cell
descText2 = function(EEZ) {
d3.select("#catchPct").text(f(simulatedLandingsTonnage[EEZ][countryNames[EEZ]]/EEZtonnage2[EEZ].stock));
d3.select("#fleetCount").text(
simulatedLandingsTonnage.filter(d => d.fleet != countryNames[EEZ])
.map(d => d[countryNames[EEZ]])
.filter(v => v > 0)
.length
)
d3.select("#descText2").transition().delay(1000).duration(1500).style("opacity",1);
}
Insert cell
interactions = function(){
let stockBars = d3.select("#stock-bars").selectAll("rect");
let stockBarLengths = d3.selectAll("#stock-bars").selectAll("rect").nodes().map(d => d.getTotalLength());
muteGlobe2()
//Stock-bar overlay
d3.select("#stock-bars").append("g")
.attr("id", "static-bar-overlay-group")
.selectAll(".static-bar-overlay")
.data(EEZstocksStatic)
.join("rect")
.attr("class", ".static-bar-overlay")
.attr("id", function(d,i){return countryCodes[i]})
.attr("y", d => -reverseY(d.stock) - 30)
.attr("x", (d,i) => xScaleOverlay(i))
.attr("height", d => reverseY(d.stock) + 30)
.attr("width", xScaleOverlay.bandwidth())
// .style("stroke","black")
.style("fill","transparent")
//Fade colours back
stockBars.transition().duration(2000)
.style("fill", (d,i) => colorScale(i));
let stockBarOverlay = d3.select("#static-bar-overlay-group")
.selectAll("rect")
stockBarOverlay
.on("mouseover", function(){
let selectedEEZ = d3.select(this).data()[0].EEZ
let EEZindex = countryNames.indexOf(selectedEEZ)
// hoverColor(EEZindex);
d3.select("#descWrapper").style("opacity",1)
descText(EEZindex);
descText2(EEZindex);
//Add colour to tooltip figures
d3.select("#EEZtonnesPct").style("color", colorScale(EEZindex))
d3.select("#catchPct").style("color", colorScale(EEZindex))

d3.select("#stock-pct").select("#" + this.id + "").style("fill", colorScale(EEZindex))
//Mute non-focal EEZs
d3.selectAll(".static-bar:not(#" + this.id + ")").style("fill","rgba(211, 211, 211,0.7)");
d3.selectAll(".sankey-label-top:not(#" + this.id + ")").select("text").style("fill","#D3D3D3");
d3.selectAll(".sankey-label-top:not(#" + this.id + ")").select("rect").style("stroke","#D3D3D3");

//Bring selected EEZ to start of segment array
stackLandings.order(reorderArray(d3.range(countryNames.length), EEZindex));
// d3.selectAll(".stock-pct")
let testData = stackLandings(simulatedLandings).map(d => (d.forEach(v => v.key = d.key), d));

d3.select("#catch-bars").selectAll("g")
.data(testData)
.selectAll("rect")
.data(v => v)
.attr("x", (v,i) => x(countryNames.indexOf(v.data.fleet)))
.attr("y", v => y(v[0]))
.attr("height", v => y(v[1]) - y(v[0]))
.style("fill", function(d) {
if (selectedEEZ == d.key){return colorScale(EEZindex)} else {return "rgba(211, 211, 211,0.6)"};
});
d3.select("#catch-text").selectAll("text")
.data(simulatedLandingsTonnage.map(d => d[selectedEEZ]),(d,i) => i)
.attr("y", d => yTonn(d) + 15)
.text(d => d > 0 ? Math.round(d/1000) : null)
.style("fill", colorScale(EEZindex))
.style("font-size", 12.5)
.style("font-weight", 500);
highlightEEZ(selectedEEZ);
})
.on("mouseleave", function(){
muteGlobe();
stackLandings.order(d3.stackOrderReverse);
d3.select("#descWrapper").style("opacity",0)
d3.select("#stock-pct").selectAll("text").style("fill", "#C2C2C2")
//Un-mute all EEZs
d3.selectAll(".static-bar").style("fill", (d,i) => colorScale(i));
d3.selectAll(".sankey-label-top").select("text").style("fill","grey");
d3.selectAll(".sankey-label-top:not(#" + this.id + ")").select("rect").style("stroke","grey");
let testData = stackLandings(simulatedLandings).map(d => (d.forEach(v => v.key = d.key), d));
d3.select("#catch-bars").selectAll("g")
.data(testData)
.selectAll("rect")
.data(v => v)
.attr("x", (v,i) => x(countryNames.indexOf(v.data.fleet)))
.attr("y", v => y(v[0]))
.attr("height", v => y(v[1]) - y(v[0]))
.style("fill", (d,i) => colorScale(countryNames.indexOf(d.key)))
d3.select("#catch-text").selectAll("text")
.data(fleetLandingsPct(simulatedLandings), (d,i) => i)
.attr("y", d => y(d[0]) + 18)
.text(d => Math.round(d[1]/1000))
.style("fill", "#C2C2C2")
.style("font-size", 13.5)
.style("font-weight", 400);
});



}
Insert cell
simulatedLandingsTonnage.map(d => d["United Kingdom"]).map(d => yTonn(d))
Insert cell
mutable currentSimId = 0
Insert cell
simulatedLandings = simulatedLandingsData('fleet')
Insert cell
simulatedLandingsTonnage = simulatedLandingsData('fleet')
Insert cell
simulatedLandingsTonnage.map(d => Math.round(d["United Kingdom"]/1000))
Insert cell
generateSim = function(elapsed, EEZ, simulatedLandings, currentSimId) {
mutable currentSimId++
const EEZname = countryNames[EEZ]
const fleetProbDist = fleetProbabilities[EEZname]
//Simulate a random draw from the probability distribution
const fleet = d3.bisect(fleetProbDist, Math.random())
const fleetName = countryNames[fleet]
//Deduct the new sim from the EEZ stocks
EEZstocks[EEZ]['stock']--
//Add the new sim to the stacked bar chart dataset
let speed = (Math.random() + 2) / 2.5
setTimeout(function(){simulatedLandings[fleet][EEZname] ++},simSpeed * (1/speed))
setTimeout(function(){simulatedLandingsTonnage[fleet][EEZname] += totalTonnage/numParticles},simSpeed * (1/speed))
return {
id: mutable currentSimId,
EEZ: EEZ,
fleet: fleet,
linkID: `${EEZ} > ${fleet}`,
startTime: elapsed + getRandomNumberInRange(-0.1, 0.1),
xJitter: getRandomNumberInRange(-14, 14),
speed: speed
}
}
Insert cell
simSpeed = 4000;
Insert cell
whiteSpace = function(str){
return str.replace(/\s+/g, '');
}
Insert cell
reorderArray = function(array,value){
//Add selected value to start of array
array.unshift(value)
//Get previous index of this value
let pos = array.lastIndexOf(value)
//Delete
array.splice(pos,1)
return array;
}
Insert cell
Insert cell
Insert cell
fleetLandingsPct = function(landings) {
let data = landings.map(function(d){
let sum = 0
countryNames.map(function(v){
sum += d[v] || 0
});
return sum
});
return data.map(d => [d, d * (totalTonnage/numParticles) ,d/d3.sum(data) || 0.0001])
}
Insert cell
numParticles = 1500
Insert cell
data = FileAttachment("EU_Landings_Flow@1.csv").csv()
Insert cell
countryNames = ["United Kingdom","Ireland","France","Denmark","Sweden","Germany","Spain","Netherlands","Finland"]
Insert cell
countryCodes = ["UK","IRL","FRA","DEN","SWE","GER","SPA","NED","FIN"]
Insert cell
Nationalities = ["UK","Irish","French","Danish","Swedish","German","Spanish","Dutch","Finnish"]
Insert cell
countryIDs = d3.range(countryNames.length)
Insert cell
Insert cell
Insert cell
EEZstockPct = EEZtonnage2.map(d => d.stock / totalTonnage)
Insert cell
totalTonnage = d3.sum(EEZtonnage2, d => d.stock)
Insert cell
Insert cell
EEZstocks = {
let data = []
countryNames.map(function(d){
data.push({EEZ: d, stock: Math.round(EEZlandings[d] * numParticles)})
})
return data
}
Insert cell
EEZstocksStatic = {
let data = []
countryNames.map(function(d){
data.push({EEZ: d, stock: Math.round(EEZlandings[d] * numParticles)})
})
return data
}
Insert cell
EEZgroups = {
//Return an object for each unique EEZ
var EEZgroups = [],
flags = [];
for (var i=0; i < data.length; i++) {
if (flags[data[i]['EEZ']]) continue; //Skip to next iteration if country has already been added
flags[data[i]['EEZ']] = true; //Else flag that country has been added
EEZgroups.push({EEZ: data[i]['EEZ']}); //Push new country to data
}
//Cycle through EEZ countries
EEZgroups.map(function(d){
countryNames.map(function(country) {
d[country] = 0; //Create a key: value pair for each fleet
//Swap value for landings
let EEZ_Data = data.filter(t => t.EEZ == d.EEZ && t.fleet == country)
for (var i=0; i < EEZ_Data.length; i++) {
d[country] = +EEZ_Data[i].landings
}
});
});
//Convert landings to % within EEZ
EEZgroups.map(function(d){
//Calculate the sum of landings for each EEZ
let values = Object.values(d)
values.shift()
let total = d3.sum(values)

let fleet = Object.keys(d)

for (fleet in d) {
if (d[fleet] != [d.EEZ])
d[fleet] = d[fleet] / total
}
});
return EEZgroups
}


Insert cell
function simulatedLandingsData(side) {
let simulatedLandings = []
countryNames.map(function(d){
let data = {};
data[side] = d;
countryNames.map(function(v){
data[v] = 0
})
simulatedLandings.push(data)
});
return simulatedLandings
}
Insert cell
Insert cell
sankeyLabels = function(parent, labelWidth, labelHeight, xScale, yPos, className) {
let labelGroups = parent.selectAll(className)
.data(countryCodes)
.enter().append("g")
.attr("class",className)
.attr("id", d => `${d}`)
.attr("transform", (d,i) => `translate(${xScale(i)},${yPos})`)

labelGroups.append("rect")
.attr("width",labelWidth).attr("height", labelHeight)
.style("fill","white")
.attr("stroke-dasharray","32 20")
.attr("stroke", "grey")
labelGroups.append("text").text(d => d)
.attr("x",labelWidth/2)
.attr("y",10+1)
.attr("alignment-baseline","middle")
.style("text-anchor","middle")
.style("fill","grey")
.style("font-size","13")
.style("font-family","Lato");
}
Insert cell
stackLandings = d3.stack()
.keys(countryNames)
// .order([1,0,2,3,4,5,6,7,8]);
.order(d3.stackOrderReverse);
Insert cell
getRandomNumberInRange = (min, max) => Math.random() * (max - min) + min
Insert cell
getRandomValue = arr => arr[Math.floor(getRandomNumberInRange(0, arr.length))]
Insert cell
hoverColor = function(EEZ){
d3.select("#EUpct")
.style("color",colorScale(EEZ))
}
Insert cell
Insert cell
Insert cell
xScaleOverlay = d3.scaleBand()
.domain(d3.range(9))
.range([-dimensions.pathWidth/2 , dimensions.boundedWidth + dimensions.pathWidth/2])
Insert cell
x = d3.scaleLinear()
.domain(d3.extent(countryIDs))
.range([-dimensions.pathWidth/2, dimensions.boundedWidth - dimensions.pathWidth/2])
Insert cell
y = d3.scaleLinear()
.domain([0, numParticles/2])
.rangeRound([dimensions.boundedHeight,dimensions.boundedHeight+200])
Insert cell
yTonn = d3.scaleLinear()
.domain([0, totalTonnage/2])
.rangeRound([dimensions.boundedHeight,dimensions.boundedHeight+200])
Insert cell
colorScale = d3.scaleLinear()
.domain(d3.extent(countryIDs))
.range(["#B53471", "#12CBC4"])
.interpolate(d3.interpolateHcl)
Insert cell
reverseY = d3.scaleLinear()
.domain([0, numParticles/2])
.rangeRound([0,200]);
Insert cell
reverseYtonn = d3.scaleLinear()
.domain([0, totalTonnage/2])
.rangeRound([0,200]);
Insert cell
f = d3.format(".0%")
Insert cell
Insert cell
import {color} from "@jashkenas/inputs"
Insert cell
import {highlightEEZ, muteGlobe, muteGlobe2, showMap} from "@benjamesdavis/eez-globe"
Insert cell
d3 = require("d3@6")
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