Public
Edited
Jan 17, 2023
Insert cell
Insert cell
Insert cell
// ECV = Essential Climate Variable
ecvs=[
{name:"aerosol",group:"atmospheric",links:["soil moisture","ozone","land cover","ocean colour","sea surface temperature","cloud","fire"]},
{name:"cloud",group:"atmospheric",links:["soil moisture","aerosol"]},
{name:"carbon\ndioxide",group:"atmospheric",links:["soil moisture","methane","land cover","fire"]},
{name:"methane",group:"atmospheric",links:["soil moisture","carbon dioxide","fire"]},
{name:"ozone",group:"atmospheric",links:["ocean colour","aerosol","fire"]},
{name:"fire",group:"terrestrial",links:["soil moisture","carbon dioxide","methane","ozone","aerosol","land cover","aerosol"]},
{name:"soil\nmoisture",group:"terrestrial",links:["sea level","carbon dioxide","methane","land cover","sea surface temperature","cloud","aerosol","fire"]},
{name:"land\ncover",group:"terrestrial",links:["soil moisture","carbon dioxide","aerosol","fire"]},
{name:"glaciers",group:"terrestrial",links:["glaciers","sea level","land cover"]},
{name:"ice sheets",group:"terrestrial",links:[]}, // No links!?!
{name:"sea\nsurface\ntemperature",group:"oceanic",links:["soil moisture","sea level","ocean colour","cloud","aerosol","sea ice"]},
{name:"ocean colour",group:"oceanic",links:["sea level","ozone","sea surface temperature","aerosol"]},
{name:"sea level",group:"oceanic",links:["soil moisture","glaciers","ocean colour","sea surface temperature","sea ice"]},
{name:"sea ice",group:"oceanic",links:["sea level","sea surface temperature"]}
]
Insert cell
groupcolours=[{oceanic:'#007D8A',terrestrial:'#FF8F1C',atmospheric:'#009CDE'}][0]
Insert cell
Insert cell
//preload=html`<link rel="preload" as="font" href="${fonturl}" type="font/woff2" crossorigin="anonymous">` // Not sure this is needed.
Insert cell
Insert cell
chords = {
document.head.innerHTML+=styling;
const width=512;
const height=512;
var canvas=DOM.canvas(width, height);
var ctx=canvas.getContext('2d');
let frame;

// Precompute label centres
var ecvxys=[];
for (var i=0;i<ecvs.length;i++) {
const ecv=ecvs[i];
const a=(2.0*Math.PI*(1+i))/(ecvs.length+1);
const dx= 0.4*width*Math.sin(a);
const dy= -0.4*height*Math.cos(a);
const x=width*0.5+dx;
const y=height*0.5+dy;
ecvxys.push(
[x,y]
);
}

// Names with "\n" line breaks replaced by " "
var ecvnames=[];
for (var i=0;i<ecvs.length;i++) {
ecvnames.push(ecvs[i].name.replaceAll("\n"," "));
}

// Lookup from ecvname to its index
var ecvindices={};
for (var i=0;i<ecvs.length;i++) {
ecvindices[ecvnames[i]]=i;
}
// Precompute enumerated links
var ecvlinks=[];
for (var i=0;i<ecvs.length;i++) {
var row=[];
console.log(ecvs[i].links);
for (var j=0;j<ecvs[i].links.length;j++) {
row.push(ecvindices[ecvs[i].links[j]]);
}
ecvlinks.push(row);
}
console.log(ecvlinks);
var nearestecv= -2; // -1 means near centre

var ecvstar=
function tick() {
ctx.fillStyle = "white";
ctx.fillRect(0, 0, width, height); // Draw a filled rectangle

//console.log('Tick: nearest is '+(ecvs[nearestecv].name));
for (var i=0;i<ecvs.length;i++) {
const ecv=ecvs[i];

const active=(i==nearestecv);
for (var j=0;j<ecvlinks[i].length;j++) {

const t=ecvlinks[i][j];

const x0=width*0.5+0.8*(ecvxys[i][0]-0.5*width);
const y0=height*0.5+0.8*(ecvxys[i][1]-0.5*height)

const x1=width*0.5+0.8*(ecvxys[t][0]-0.5*width);
const y1=height*0.5+0.8*(ecvxys[t][1]-0.5*height);

if (active || nearestecv==-1) {
var gradient = ctx.createLinearGradient(x0,y0,x1,y1);
gradient.addColorStop(0,groupcolours[ecvs[i].group]);
gradient.addColorStop(1,groupcolours[ecvs[t].group]);
ctx.strokeStyle=gradient;
ctx.lineWidth=2;
} else {
ctx.strokeStyle='#eeeeee44'; // A bit of transparency to make it really faint
ctx.lineWidth=1;
}

ctx.beginPath();
ctx.moveTo(x0,y0);
ctx.quadraticCurveTo(width*0.5,height*0.5,x1,y1);
ctx.stroke();
}

if (active) {
ctx.font='16px notesstyle-boldtfregular';
} else {
ctx.font='14px notesstyle-boldtfregular';
}
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle='#444444'; // Text colour
const lines=ecv.name.split('\n');
if (lines.length==1) {
ctx.fillText(ecv.name,ecvxys[i][0],ecvxys[i][1]);
} else {
const lineHeight=ctx.measureText('M').width*1.25 // Doesn't measure height!
for (var line=0;line<lines.length;line++) {
ctx.fillText(lines[line],ecvxys[i][0],ecvxys[i][1]-lineHeight*(0.5*lines.length-line));
}
}
}

frame = requestAnimationFrame(tick);
}
tick();

function sqr(x) {return x*x;}
canvas.onmousemove = function(e) {
const rect = canvas.getBoundingClientRect();
const ex = e.clientX - rect.left;
const ey = e.clientY - rect.top;
nearestecv= -1;
var nearestd=4.0*Math.sqrt(sqr(ex-0.5*width)+sqr(ey-0.5*height));
for (var i=0;i<ecvs.length;i++) {
const x=ecvxys[i][0];
const y=ecvxys[i][1];
const d=Math.sqrt(sqr(x-ex)+sqr(y-ey));
if (d<nearestd) {
nearestecv=i;
nearestd=d;
}
//console.log('Nearest is '+(ecvs[nearestecv].name));
}
}
invalidation.then(
() => cancelAnimationFrame(frame)
);
return canvas;
}

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