Public
Edited
Sep 15, 2024
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chartSlope_v4 = {
const svg = d3.create('svg')
.attr('height', height)
.attr('width', width)
.style('font-family', 'sans-serif')
.style('font-size', 12)

const threshhold = 70; // Values 70 and higher can be considered "within the donut"
// TODO: Maybe have gradient of colors within red and green?

const green = '#689F38'
const red = '#B71C1C'
const gray = 'lightgray'
const c = d3.scaleOrdinal()
.domain([true, false, null]) // Green = we are in the safe zone (percentage >= threshhold = true)
.range([green, red, gray]);
const planet_outer_radius = (Math.min(width, height) / 2) ;
const planet_inner_radius = planet_outer_radius * 0.65;
const donut_outer_radius = planet_inner_radius;
const donut_inner_radius = donut_outer_radius * 0.6;
const donut_center_radius = (donut_outer_radius + donut_inner_radius) / 2;
const human_outer_radius = donut_inner_radius;
const human_inner_radius = human_outer_radius * 0.0; // TODO: Choose something that looks ok

// ------------ Green inside-the-donut static stuff ------------
const stroke_width = 14;
const donut_donut = svg.append('g')
.attr('transform', `translate(${width/2},${height/2})`)
// Center ring
donut_donut.append("circle")
.attr('r', donut_center_radius)
.attr('fill', 'transparent')
.attr('stroke', 'transparent')
.attr('stroke-width', (donut_outer_radius - donut_inner_radius));

// "Ecological Ceiling" ring
const ceilingRadius = donut_outer_radius - stroke_width/2;
const ceilingStroke = donut_donut.append("circle")
.attr('id', 'ecological_ceiling')
.attr('r', ceilingRadius)
.attr('fill', 'transparent')
.attr('stroke', green)
.attr('stroke-width', stroke_width);
const labelFontSize = 12;
const labelHeight = labelFontSize * 0.7;
const ceilingTextRadius = ceilingRadius - 0.5 * labelHeight;
donut_donut.append("path")
.attr('id', 'eco_ceiling_curve')
.attr('d', `M-${ceilingTextRadius},0 A${ceilingTextRadius},${ceilingTextRadius} 0 0,1 ${ceilingTextRadius},0`)
.attr('fill', 'none')
.attr('opacity', 0.3)
donut_donut.append("text")
.attr('font-family', 'sans-serif')
.attr('font-size', labelFontSize)
.attr('fill', 'white')
.append("textPath")
.attr('xlink:href', '#eco_ceiling_curve')
.text("ECOLOGICAL CEILING")
.attr('text-anchor', 'middle')
.attr('startOffset', '50%')

// "Social Foundation" ring
const socialRadius = donut_inner_radius + stroke_width/2;
const socialStroke = donut_donut.append("circle")
.attr('id', 'social_foundation')
.attr('r', socialRadius)
.attr('fill', 'transparent') //hotpink for testing
.attr('stroke', green)
.attr('stroke-width', stroke_width);
const socialTextRadius = socialRadius - 0.5 * labelHeight;
donut_donut.append("path")
.attr('id', 'social_foundation_curve')
.attr('d', `M-${socialTextRadius},0 A${socialTextRadius},${socialTextRadius} 0 0,1 ${socialTextRadius},0`)
.attr('fill', 'transparent') //hotpink for testing
.attr('opacity', 0.3)
donut_donut.append("text")
.attr('font-family', 'sans-serif')
.attr('font-size', labelFontSize)
.attr('fill', 'white')
.append("textPath")
.attr('xlink:href', '#social_foundation_curve')
.text("SOCIAL FOUNDATION")
.attr('text-anchor', 'middle')
.attr('startOffset', '50%')

// ----------- Outer & Inner Data Driven Stuff -----------
const planet_outsideDonut_scale = d3.scaleLinear()
.domain([threshhold-1, 0])
.range([planet_inner_radius, planet_outer_radius]);
const human_outsideDonut_scale = d3.scaleLinear()
.domain([0, threshhold])
.range([human_inner_radius, human_outer_radius - 1]);

const planet_insideDonut_arc = d3.arc()
.innerRadius(donut_center_radius + 1)
.outerRadius(donut_outer_radius - stroke_width + 1) // TODO: sketchy pixel math?
.padRadius(0.5 * planet_outer_radius)
.padAngle(2/(0.65 * planet_outer_radius))
.cornerRadius(0)
const planet_outsideDonut_arc = d3.arc()
.innerRadius(planet_inner_radius)
.outerRadius(d => planet_outsideDonut_scale(d.data.percentage === null ? 0 : d.data.percentage))
.padRadius(0.5 * planet_outer_radius)
.padAngle(2/(0.65 * planet_outer_radius))
.cornerRadius(0)

const human_insideDonut_arc = d3.arc()
.innerRadius(donut_inner_radius + stroke_width - 1) // TODO: sketchy pixel math?
.outerRadius(donut_center_radius - 1)
.padRadius(0.5 * planet_outer_radius)
.padAngle(2/(0.65 * planet_outer_radius)) // TODO: How does this padAngle relate to the pie padAngle?
.cornerRadius(0)
const human_outsideDonut_arc = d3.arc()
.innerRadius(d => human_outsideDonut_scale(d.data.percentage === null? 0 : d.data.percentage))
.outerRadius(human_outer_radius)
.padRadius(0.5 * planet_outer_radius)
.padAngle(2/(0.65 * planet_outer_radius)) // TODO: How does this padAngle relate to the pie padAngle?
.cornerRadius(0)

const planet_pie = d3.pie()
.padAngle(0.00)
.sort(null)
.value(1) // Optional: value() sets value accessor that returns a numeric value for a given datum.
// Make this constant to make the arc length constant and arc height variable?

const human_pie = d3.pie()
.padAngle(0.00)
.sort(null)
.value(1)

// TODO: Standardize on terminology. "Outer/Inner" or "Planetary/Human" or "Ecological/Social" or "Ceiling/Foundation"
const planet_data = data_new.filter(d => d.inner_or_outer == "Outer");
const human_data = data_new.filter(d => d.inner_or_outer == "Inner");
const planet_arcs = planet_pie(planet_data);
const human_arcs = human_pie(human_data);
const planet_donut = svg.append('g')
.attr('transform', `translate(${width/2},${height/2})`)
const human_donut = svg.append('g')
.attr('transform', `translate(${width/2},${height/2})`)

// Attempt 2
planet_donut.selectAll('path')
.data(planet_arcs)
.join('path')
.attr('fill', d => c(d.data.percentage === null ? null : d.data.percentage >= threshhold))
.attr('d', d => d.data.percentage === null ? planet_outsideDonut_arc(d) :
d.data.percentage >= threshhold ? planet_insideDonut_arc(d) : planet_outsideDonut_arc(d))
.append('title')
.text(d => `Ecological: ${d.data.category} at ${d.data.percentage}%`);
// TODO: Better hover tooltip text
// TODO: allow hovering on the text

human_donut.selectAll('path')
.data(human_arcs)
.join('path')
.attr('fill', d => c(d.data.percentage === null ? null : d.data.percentage >= threshhold))
.attr('d', d => d.data.percentage === null ? human_outsideDonut_arc(d) :
d.data.percentage >= threshhold ? human_insideDonut_arc(d) : human_outsideDonut_arc(d))
.append('title')
.text(d => `Social: ${d.data.category} at ${d.data.percentage}%`);
// TODO: Better hover tooltip text
// TODO: allow hovering on the text, and maybe hovering even outside the mark boundaries?

// ----------------------- Text Labels for "Ecological Ceiling" items -----------------------
const planet_default_text_arc = d3.arc()
.innerRadius(planet_inner_radius)
.outerRadius(planet_outer_radius)
.padRadius(0.5 * planet_outer_radius)
.padAngle(2/(0.65 * planet_outer_radius))
.cornerRadius(0)
planet_donut.append('g')
.attr('font-family', 'sans-serif')
.attr('font-size', 12)
.attr('text-anchor', 'middle')
.selectAll('text')
.data(planet_arcs)
.join('text')
.attr('transform', d => `translate(${planet_default_text_arc.centroid(d)})`)
.call(text => text.append('tspan')
.attr('y', '-0.4em')
.attr('font-weight', 'bold')
.text(d => d.data.category)) // TODO: uhh is this right?
.call(text => text.filter(d => (d.endAngle - d.startAngle) > 0.25).append('tspan')
.attr('x', 0)
.attr('y', '0.7em')
.attr('fill-opacity', 0.7)
.text(d => d.data.percentage === null ? "no data" : d.data.percentage.toLocaleString() + "%"));

// ----------------------- Text Labels for "Social Foundation" items -----------------------
const human_text_radius = donut_inner_radius + stroke_width + 3; // TODO: sketchy pixel math
const human_text_arc = d3.arc()
.innerRadius(human_text_radius)
.outerRadius(human_text_radius)
const human_donut_text = svg.append('g')
.attr('transform', `translate(${width/2},${height/2})`)
human_donut_text.selectAll('path')
.data(human_arcs)
.join('path')
.attr('id', function(d,i) {return "humanTextArc_"+i;})
.attr('fill', 'none')
.attr('d', human_text_arc)
human_donut_text.append('g')
.attr('font-family', 'sans-serif')
.attr('font-size', 12)
.selectAll('text')
.data(human_arcs)
.join('text')
.append('textPath')
.attr('xlink:href', function(d,i){return "#humanTextArc_"+i;})
.attr('text-anchor', 'middle')
.attr('startOffset', '25%')
.call(text => text.append('tspan')
.attr('font-weight', 'bold')
.text(d => d.data.category + " " +
(d.data.percentage === null ? "no data" : d.data.percentage.toLocaleString() + "%")))
// .call(text => text.filter(d => (d.endAngle - d.startAngle) > 0.25).append('tspan')
// .attr('x', 0)
// .attr('y', '0.7em')
// .attr('fill-opacity', 0.7)
// .text(d => d.data.percentage === null ? "no data" : d.data.percentage.toLocaleString() + "%"));
// TODO: How to get newlines on this? Ugh would you have to just make yet another arc to put it on?
return svg.node()
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof sheetInput = Inputs.text({label: "Link to your google sheet"})
Insert cell
data_new = {
const getCsvUrl = url => {
url = new URL(url);
const id = url.pathname.split("/")[3]
const gid = new URLSearchParams(url.hash.slice(1)).get("gid") || 0;
return `https://docs.google.com/spreadsheets/d/${id}/export?format=csv&gid=${gid}`
};

try {
const data = await d3.csv(getCsvUrl(sheetInput), d3.autoType);
return data;
}
catch (error) {
console.error("Error: Are you sure this is a valid google doc with public viewing permissions?");
return null;
}

// return d3.csv(getCsvUrl("https://docs.google.com/spreadsheets/d/1CCFffe9HGeQidmh3Fuf-TZltFqDP8xq3RwHBPKZUGvA/edit#gid=0"), d3.autoType)
// data_new = d3.csvParse(await FileAttachment("d3-donut-sample.csv").text(), d3.autoType)
}
Insert cell
getCsvUrl = url => {
url = new URL(url);
const id = url.pathname.split("/")[3]
const gid = new URLSearchParams(url.hash.slice(1)).get("gid") || 0;
return `https://docs.google.com/spreadsheets/d/${id}/export?format=csv&gid=${gid}`
};
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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