Public
Edited
Aug 14, 2023
Insert cell
Insert cell
chart = {
const root = pack(data);
let focus = root;
let view;
// Specify locations, sizes, and nicknames for certain nodes:
const nodeLocations = Object({
"":[], // Super Centers
"Chicago-Naperville-Elgin, IL-IN-WI": [width*0.2, 450, 2*Math.sqrt(194205/Math.PI), "Chicago, IL"],
"Philadelphia-Camden-Wilmington, PA-NJ-DE-MD": [width*0.5, 450, 2*Math.sqrt(191468/Math.PI), "Philadelphia, PA"],
"Atlanta-Sandy Springs-Roswell, GA": [width*0.77, 450, 2*Math.sqrt(116120/Math.PI), "Atlanta, GA"],
"":[], // Major Centers
"Cleveland-Elyria, OH":[175, 1050, 2*Math.sqrt(33648/Math.PI), "Cleveland, OH"],
//"Cincinnati, OH-KY-IN":[175, 1050, 2*Math.sqrt(66849/Math.PI)],
//"Columbus, OH":[175, 1050, 2*Math.sqrt(67437/Math.PI)],
"Tampa-St. Petersburg-Clearwater, FL":[475, 1050, 2*Math.sqrt(53959/Math.PI), "Tampa Bay, FL"],
"Denver-Aurora-Lakewood, CO":[775, 1050, 2*Math.sqrt(47342/Math.PI), "Denver, CO"],
// "Denver-Aurora-Lakewood, CO":[175, 1050, 2*Math.sqrt(47342/Math.PI), "Denver, CO"],
// "Syracuse, NY":[475, 1050, 2*Math.sqrt(37044/Math.PI)],
// "Virginia Beach-Norfolk-Newport News, VA-NC":[775, 1050, 2*Math.sqrt(37606/Math.PI), "Hampton Roads, VA-NC"],
// "Tampa-St. Petersburg-Clearwater, FL":[1200, 1000, 2*Math.sqrt(53959/Math.PI)],
"":[], // Minor Centers
"Montgomery, AL":[175, 1460, 2*Math.sqrt(9474/Math.PI)],
"Chattanooga, TN-GA":[475, 1460, 2*Math.sqrt(12979/Math.PI), "Chattanooga, TN"],
"Green Bay, WI":[775, 1460, 2*Math.sqrt(5562/Math.PI)],
// "Salem, OR":[1200, 1000, 2*Math.sqrt(7717/Math.PI)],
"":[], // Multi-College Towns
"Santa Maria-Santa Barbara, CA":[1200, 1050, 2*Math.sqrt(26676/Math.PI), "Santa Barbara, CA"],
"Durham-Chapel Hill, NC":[1500, 1050, 2*Math.sqrt(44848/Math.PI)],
"Lubbock, TX":[1800, 1050, 2*Math.sqrt(33540/Math.PI)],
// "Madison, WI":[1200, 1600, 2*Math.sqrt(47388/Math.PI)],
// "Blacksburg-Christiansburg-Radford, VA":[1800, 1600, 2*Math.sqrt(41135/Math.PI)],
// "Tallahassee, FL":[1500, 1600, 2*Math.sqrt(41135/Math.PI)],
"":[], // Strong College Towns
"Lafayette-West Lafayette, IN": [1200, 1500, 2*Math.sqrt(35106/Math.PI), "Lafayette, IN"],
"Tucson, AZ": [1500, 1500, 2*Math.sqrt(33777/Math.PI)],
"Corvallis, OR": [1800, 1500, 2*Math.sqrt(18094/Math.PI)],
// "Athens-Clarke County, GA": [1200, 2000, 2*Math.sqrt(33445/Math.PI), "Athens, GA"],
"":[], // Weak College Towns
"Topeka, KS": [1200,1860,2*Math.sqrt(3958/Math.PI)],
"Amarillo, TX": [1500,1860,2*Math.sqrt(4853/Math.PI)],
"Lake Charles, LA": [1800,1860,2*Math.sqrt(5276/Math.PI)],
// "Florence-Muscle Shoals, AL": [1200,2300,2*Math.sqrt(4967/Math.PI), "Florence, AL"],
})
const heiacronyms = Object({
"": "", // Philadelphia
/*
"University of Pennsylvania": "UPenn",
"Temple University": "Temple",
"University of Delaware": "UDelaware",
"Drexel University": "Drexel",
"Rowan University": "Rowan",
"West Chester University of Pennsylvania": "West Chester",
"Villanova University": "Villanova",
"": "", // Atlanta
"Georgia State University": "Georgia State",
"Kennesaw State University": "Kennesaw State",
"Georgia Institute of Technology-Main Campus": "Georgia Tech",
"Emory University": "Emory",
"University of West Georgia": "W Georgia",
"Georgia Gwinnett College": "Gwinnett",
"Clark Atlanta University": "Clark Atl",
"Clayton State University": "Clayton",
"Gordon State College": "Gordon",
"": "", // San Francisco
"University of California-Berkeley": "UC Berkeley",
"University of Georgia": "UGeorgia",
"": "", // Syracuse
"Syracuse University": "Syracuse University",
"SUNY College at Oswego": "SUNYOswego",
"Colgate University": "Colgate",
"Le Moyne College": "LeMoyne",
"Morrisville State College": "Morrisv",
"SUNY College of Environmental Science and Forestry": "ESF",
"Upstate Medical University": "UMU",
"Cazenovia College": "Caz",
"": "", // Denver
"Metropolitan State University of Denver": "Metro State",
"University of Colorado Denver/Anschutz Medical Campus": "UColorado",
"University of Denver": "UDenver",
"Colorado School of Mines": "CO Mines",
"Regis University": "Regis",
"Community College of Denver": "CCD",
"Red Rocks Community College": "RRCC",
"Johnson & Wales University-Denver": "J&W",
"Colorado Christian University": "",
"Denver Seminary": "",
"Iliff School of Theology": "",
"": "", // Tampa FL
"University of South Florida-Main Campus": "U South Florida",
"The University of Tampa": "U Tampa",
"St Petersburg College": "St Petersburg",
"Saint Leo University": "St Leo",
"Pasco-Hernando State College": "PH State",
"University of South Florida-St Petersburg": "USF",
"Eckerd College": "Eckherd",
"Florida College": "",
"Trinity College of Florida": "",
"": "", // Multi-College Towns
"University of California-Santa Barbara": "UC Santa Barbara",
"Texas Tech University": "Texas Tech",
"University of Florida": "University of Florida",
"Santa Fe College": "Santa Fe",
// Strong College Towns
"Purdue University-Main Campus": "Purdue University",
"University of Arizona": "University of Arizona",
"Oregon State University": "Oregon State",
"University of Georgia": "University of Georgia",
// Weak College Towns
"Washburn University": "Washburn",
"West Texas A & M University": "W TX A&M",
"McNeese State University": "McNeese St",
"University of North Alabama": "U N AL",
*/
})
// Move all nodes that have a specified location above to their desinations:
for (let node of root.descendants()) {
if (nodeLocations[node.data.name]) {
let dest = nodeLocations[node.data.name];
move_circle_group(node, dest[0], dest[1]);
size_circle_group(node, dest[2]);
}
}
function shoulddisplay(d) {
if (nodeLocations.hasOwnProperty(d.data.name)) {
return true
} else if (d.parent) {
return shoulddisplay(d.parent) // recursive loop
} else {
return false
}
}

// Alters the radii of the largest circles so that they are proportional by number of students
// calculate_new_radii(root.children)
// Spreads the second-largest circles out within the largest circles
// spread_child_circles(root.children)
// Sets up the svg
const svg = d3.create("svg")
.attr("xmlns", "http://www.w3.org/2000/svg")
.attr("xmlns:xlink", "http://www.w3.org/1999/xlink")
.attr("viewBox", `0 0 ${width} ${height}`)
// .attr("viewBox", `-${width / 2} -${height / 2} ${width} ${height}`)
.style("display", "block")
.style("margin", "0 -14px")
.style("background", "white")
.style("cursor", "pointer")
// .on("click", () => zoom(root)); // event handler, f(x) () is an empty function with no parameters
// Creates the groups and circles within them
const node = svg.append("g")
.selectAll("circle")
.data(root.descendants().slice(1)) // all of the descendants of the root, except the root slice(1) start at index 1 instead of index 0
.join("circle") // let me tell you what
.style("display", d => shoulddisplay(d) ? "inline" : "none")
.attr("fill", d => d.children ? depth_color(d.depth) : item_color(d.data) ) // does the data point have children? if yes, color according to depth, otherwise color it according to the color function below
.attr("stroke", d => d.children ? "#C2CBD3" : null ) // does the data point have children? if yes, outline
.attr("stroke-width", 0.7) // does the data point have children? if yes, set width, if not, set width to zero (helps deal with mouseover and mouseout issues with the HEIs, but breaks the mouseover for the HEIs)
// can I define a different mouseover and mouseout action for the HEIs at the smallest level?
// .attr("fill-opacity", d => d.parent === root ? .5 : 1) // makes second highest level circles transparent
// .attr("pointer-events", d => !d.children ? "none" : null)
.on("mouseover", function(d) {
if (d.children) {
d3.select(this).attr("stroke", "#687186");
} else {
d3.select(this).attr("stroke", "white");
}
}) // data and index(when you care about order)
// .on("mouseout", function() { d3.select(this).attr("stroke", "#C2CBD3"); }) // set this to same as color above
.on("mouseout", function(d) {
d3.select(this).attr("stroke", d => d.children ? "#C2CBD3" : null);
}) // data and index(when you care about order)
// .on("click", d => focus !== d && d.children && (zoom(d), d3.event.stopPropagation()));
// Adds a title element for mouseover text
svg.selectAll("circle").join("circle").append("title")
//.text(d => d.data.name + "\nStudents: " + Math.floor(d.value).toLocaleString());
.text(function(d){
if (d.parent.data.name == "top") {
return d.data.name + "\nStudents: " + Math.floor(d.value).toLocaleString()
} else if (d.parent.parent.data.name == "top"){
return d.data.name + "\nStudents: " + Math.floor(d.value).toLocaleString() + "\n" + d.parent.data.name
} else {
return d.data.name + "\nStudents: " + Math.floor(d.value).toLocaleString() + "\n" + d.parent.data.name + "\n" + d.parent.parent.data.name
}
})

// Adds paths around circles for labels
const edge_paths = svg.append("g")
.selectAll("path")
.data(root.descendants().slice(1))
.join("path")
.attr("stroke", "none")
.attr("fill", "none")
.attr("id", d => "path_" + d.data.name)
.attr("d", d => path_data_for_edge_of_circle(d.x, d.y, d.r*1.02));
// Adds labels
const edge_text = svg.append("g").append("text")
.style("font", "28px 'Gill Sans', 'Gill Sans MT', sans-serif")
.attr("text-anchor", "middle")
.selectAll("textPath")
.data(root.descendants().slice(1))
.join("textPath")
.style("display", d => nodeLocations.hasOwnProperty(d.data.name) ? "inline" : "none")
.attr("startOffset", "50%")
.attr("xlink:href", d => "#path_" + d.data.name)
.text(d => nodeLocations.hasOwnProperty(d.data.name) && nodeLocations[d.data.name].length >= 4 ? nodeLocations[d.data.name][3] : d.data.name)
// Original label code, as separate objects
const label = svg.append("g")
.style("font", "12px 'Gill Sans', 'Gill Sans MT', sans-serif")
.style("fill", "white")
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle") // vertical orientation
.selectAll("text")
.data(root.descendants())
.join("text")
.style("fill-opacity", d => heiacronyms.hasOwnProperty(d.data.name) ? 1 : 0)
.style("display", d => heiacronyms.hasOwnProperty(d.data.name) ? "inline" : "none")
.text(d => heiacronyms[d.data.name])
;

// Make a group for the title, subtitle, legend
const headergroup = svg.append("g") // creates a group, appends to svg, stores as variable
// function add_text(container, text, left, top, fontsize, fontweight, anchor)
const supc = add_text(headergroup,"Super Centers",(width*0.5),(height*0.05),40,"","middle")
const majc = add_text(headergroup,"Major Centers",475,870,40,"","middle")
const minc = add_text(headergroup,"Minor Centers",475,1325,40,"","middle")
const mct = add_text(headergroup,"Multi-College Towns",1500,870,40,"","middle")
const sct = add_text(headergroup,"Strong College Towns",1500,1325,40,"","middle")
const wct = add_text(headergroup,"Weak College Towns",1500,1750,40,"","middle")
// const subtitletext = "Higher education institutions within metros within type"
// Add legend
// add_legend(container,title,cccolor,x_position_relative_to_center,y_position_relative_to_center)
// add_legend2col(container, title, colorkey, numcolumns, columnwidth, left, top)
const legendtitle = "Carnegie Class of Higher Education Institutions"
const legend = add_legend2col(headergroup,legendtitle,cccolor,2,400,100,1725)
// Add credit
const credittext = "Ehlenz & Mawhorter 2020"
const credit = add_text(svg,credittext,(width*0.98),(height*0.98),20,undefined,"end")

function zoomTo(v) {
const k = 1.00*((width) / v[2]); // multiplying k by 0.75 zooms out a bit

view = v;

label.attr("transform", d => `translate(${(d.x - v[0]) * k},${(d.y - v[1]) * k})`);
edge_paths.attr("d", d => path_data_for_edge_of_circle((d.x - v[0])*k, (d.y - v[1])*k, (d.r2 ? d.r2 : d.r) * k+7));
node.attr("transform", d => `translate(${(d.x - v[0]) * k},${(d.y - v[1]) * k})`);
node.attr("r", d => (d.r2 ? d.r2 : d.r) * k); // uses r2 if it's available ? is if statement, values that are undefined are false, : acts as else
}

zoomTo([0, 0, width]);
function zoom(d) {
const focus0 = focus;

focus = d;
// legend.style("display", focus === root ? "block" : "none" );
const transition = svg.transition()
.duration(d3.event.altKey ? 7500 : 750)
.tween("zoom", d => {
const i = d3.interpolateZoom(view, [focus.x, focus.y, (focus.r2 ? focus.r2 : focus.r) * 2]);
return t => zoomTo(i(t));
});

headergroup.transition(transition)
.style("fill-opacity", root === focus ? 1 : 0)
.on("start", function() { if (root === focus) this.style.display = "inline"; })
.on("end", function() { if (root !== focus) this.style.display = "none"; });
/* // want to say the parent is the focus, and the parent of the parent is the root (and is &&)
edge_text
.filter(function(d) { return d.parent === focus || this.style.display === "inline"; })
.transition(transition)
.style("fill-opacity", d => d.parent === focus ? 1 : 0)
.on("start", function(d) { if (d.parent === focus) this.style.display = "inline"; })
.on("end", function(d) { if (d.parent !== focus) this.style.display = "none"; });
*/
}
return svg.node();

}


Insert cell
Insert cell
function move_circle_group(node, x, y) {
// compute how far each circle needs to move
let delta_x = x - node.x;
let delta_y = y - node.y;
// move all descendants in the hierarchy
let targets = node.descendants();
for (let desc of targets) {
desc.x += delta_x; // += is the old value plus the difference
desc.y += delta_y;
}
}
Insert cell
Insert cell
function size_circle_group(node, size) {
// compute how far each circle needs to move
let sizeratio = size/(2 * node.r); // ratio of old to new diameter
// move all descendants in the hierarchy
let targets = node.descendants();
for (let desc of targets) {
desc.r *= sizeratio;
let rel_x = desc.x - node.x
let rel_y = desc.y - node.y
desc.x = node.x + (rel_x * sizeratio);
desc.y = node.y + (rel_y * sizeratio);
}
}
Insert cell
Insert cell
function determine_student_area_ratios(nodes) {
// compute the existing ratio of students to area
for (let node of nodes) {
node.studentarearatio = node.value / (Math.PI * node.r * node.r)
}
}
Insert cell
function find_min_student_area_ratio(nodes) {
// map allows us to create an array of these values from each individual node in the list
let ratios = nodes.map(node => node.studentarearatio)
return Math.min(...ratios) // ... turns an array into a series of arguments to a function
}
Insert cell
function calculate_new_radii(nodes){
determine_student_area_ratios(nodes)
let minratio = find_min_student_area_ratio(nodes)
for(let node of nodes) {
node.r2 = Math.sqrt(node.value / (Math.PI * minratio))
console.log(node.r, node.r2)
}
}
Insert cell
Insert cell
// you must call calculate_new_radii on the nodes first
function spread_child_circles(nodes) {
for(let node of nodes) {
let adjust_ratio = node.r2/node.r
spread_circles(node.children, adjust_ratio)
}
}
Insert cell
function spread_circles(nodes, spread_factor) {
for(let node of nodes) {
let dx = node.x - node.parent.x
let dy = node.y - node.parent.y
let newx = node.parent.x + (dx * spread_factor)
let newy = node.parent.y + (dy * spread_factor)
let movex = newx - node.x
let movey = newy - node.y
for (let desc of node.descendants()) {
desc.x += movex
desc.y += movey
}
}
}
Insert cell
Insert cell
table = d3.csvParse(await FileAttachment("type_cbsa13_hei.csv").text())
Insert cell
function fetch_or_create_category(list_of_categories, target_name) {
for (let category of list_of_categories) {
if (category["name"] == target_name) {
return category; // this ends the code if there is already that category in the list, and returns that category
}
}
let result = {"name": target_name, "children": [] };
list_of_categories.push(result);
return result;
}
Insert cell
function hierarchize(csv_data) {
let all_types = [];
let result = {"name":"top", "children": all_types};
for (let row of csv_data) {
let type = row["lpl6names"];
let torder = row["lpl6sort"];
let msa = row["NMCBSA"];
let hei = row["instnm"];
let students = row["eftloc"];
let cclass = row["cclass"];
if (type=="") {
continue;
}
let gtype = fetch_or_create_category(all_types, type);
let gmsa = fetch_or_create_category(gtype["children"], msa);
let onehei = {"name": hei, "value": students, "cclass": cclass};
gmsa["children"].push(onehei);
}
return result;
}
Insert cell
data = hierarchize(table)
Insert cell
Insert cell
pack = data => d3.pack()
.size([width, height])
.padding(3)
(d3.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value))
Insert cell
Insert cell
width = 1950 // 6.5 inches x 300px/in
Insert cell
height = 2050 // 8 inches x 300px/in
Insert cell
Insert cell
format = d3.format(",d")
Insert cell
Insert cell
dcolor = ["white", "#F7F6F3", "#DDD9CC"] // #EAE7DF #E2DED3
Insert cell
function depth_color(depth) {
if (depth >= 0 && depth < dcolor.length) {
return dcolor[depth];
} else {
return "white";
}
}

Insert cell
color = d3.scaleLinear()
.domain([0, 4])
.range(["#EDE8E2","#685E47"])
// .range(["#C7C1AA","#544B36"])
// .range(["hsl(152,80%,80%)", "hsl(228,30%,40%)"])
.interpolate(d3.interpolateHcl)
Insert cell
Insert cell
cccolor = Object({
"Doctoral Universities":"#242D53",
"Master's Universities":"#3D6775",
"Special Focus Institutions":"#E6763E",
"Baccalaureate Colleges":"#638F64",
"Baccalaureate/Associate's Colleges":"#EDAD6D",
// "Tribal Colleges":"#D2C4D6"
})
Insert cell
function item_color(data) {
if (cccolor[data["cclass"]]) {
return cccolor[data["cclass"]];
} else {
return "white";
}
}
Insert cell
Object.keys(cccolor)
Insert cell
Insert cell
function add_text(container, text, left, top, fontsize, fontweight, anchor) {
if (fontsize == undefined) {fontsize = 12;}
if (fontweight == undefined) {fontweight = "";}
if (anchor == undefined) {anchor = "start";}
const result = container.append("text")
.text(text)
.style("font", fontweight+" "+fontsize+"px 'Gill Sans', 'Gill Sans MT', sans-serif")
.attr("text-anchor", anchor) // left/right orientation, start, middle, or end
.attr("dominant-baseline", "top") // vertical orientation
.attr("x", left)
.attr("y", top);
return result;
}
Insert cell
Insert cell
swatches({
color: d3.scaleOrdinal(["Doctoral Universities", "Master's Universities", "Baccalaureate Colleges", "Baccalaureate/Associate's Colleges", "Special Focus Institutions"], ["#32414c", "#2A585C", "#C96947", "#ECB764", "#CED19A"]),
// columns: "180px"
})
Insert cell
Insert cell
legend_test = {
const svg = d3.create("svg")
.attr("viewBox", "0 0 600 180") // minx, miny, width, height
.style("display", "block")
.style("margin", "0 -14px")
.style("background", "white")
.style("cursor", "pointer")
add_legend(svg,"Carnegie Class of Higher Education Institutions",cccolor,0,0)
return svg.node();
}
Insert cell
function add_legend(container, title, colorkey, left, top) {
let gap = 35 // vertical start of legend items
let lead = 25 // vertical spacing between legend items
const header = container.append("text")
.text(title)
.style("font", "bold 16px 'Gill Sans', 'Gill Sans MT', sans-serif")
.attr("text-anchor", "start") // left/right orientation
.attr("dominant-baseline", "central") // vertical orientation
.attr("x", left + 3)
.attr("y", top + 10);
// Creates the groups and circles within them
const node = container.append("g")
.selectAll("circle")
.data(Object.keys(colorkey)) //
.join("circle") //
.attr("fill", datum => colorkey[datum] ) //
.attr("cx", left + 10) // x position
.attr("cy", (datum,index) => top + gap + index*lead) // two-parameter function for y position
.attr("r", 7); // radius

// Labels are a separate object
const label = container.append("g")
.style("font", "16px 'Gill Sans', 'Gill Sans MT', sans-serif")
.attr("text-anchor", "start") // left/right orientation
.attr("dominant-baseline", "central") // vertical orientation
.selectAll("text")
.data(Object.keys(colorkey)) //
.join("text")
.text(datum => datum)
.attr("x", left + 20)
.attr("y", (datum,index) => top + gap + index*lead);
return container.node();
}
Insert cell
legend_test2col = {
const svg = d3.create("svg")
.attr("viewBox", "0 0 600 180") // minx, miny, width, height
.style("display", "block")
.style("margin", "0 -14px")
.style("background", "white")
.style("cursor", "pointer")
add_legend2col(svg,"Carnegie Class of Higher Education Institutions",cccolor,2,200,0,0)
return svg.node();
}
Insert cell
function add_legend2col(container, title, colorkey, numcolumns, columnwidth, left, top) {
let gap = 55 // vertical start of legend items
let lead = 40 // vertical spacing between legend items
let nitems = Object.keys(colorkey).length
let itemspercolumn = Math.ceil(nitems/numcolumns)
function itemcolumn(index) {
return Math.floor(index/itemspercolumn)
}
function item_x(index) {
let col = itemcolumn(index)
return col*columnwidth
}
function item_y(index) {
let col = itemcolumn(index)
let itemsbefore = col*itemspercolumn
let indexincolumn = index - itemsbefore
return indexincolumn * lead
}
const legendgroup = container.append("g")
.classed("legend", true)
const header = legendgroup.append("text")
.text(title)
.style("font", "40px 'Gill Sans', 'Gill Sans MT', sans-serif")
.attr("text-anchor", "start") // left/right orientation
.attr("dominant-baseline", "central") // vertical orientation
.attr("x", left + 3)
.attr("y", top + 10);
// creates the groups and circles within them
const node = legendgroup.append("g")
.selectAll("circle")
.data(Object.keys(colorkey)) //
.join("circle") //
.attr("fill", datum => colorkey[datum] ) //
.attr("cx", (datum,index) => left + 15 + item_x(index)) // x position
.attr("cy", (datum,index) => top + gap + item_y(index)) // two-parameter function for y position
.attr("r", 12); // radius

// labels are a separate object
const label = legendgroup.append("g")
.style("font", "28px 'Gill Sans', 'Gill Sans MT', sans-serif")
.attr("text-anchor", "start") // left/right orientation
.attr("dominant-baseline", "central") // vertical orientation
.selectAll("text")
.data(Object.keys(colorkey)) //
.join("text")
.text(datum => datum)
.attr("x", (datum,index) => left + 35 + item_x(index))
.attr("y", (datum,index) => top + gap + item_y(index));
return legendgroup;
}
Insert cell
function entity(character) {
return `&#${character.charCodeAt(0).toString()};`;
}
Insert cell
Insert cell
// Returns an SVG path data string that moves to the 9:00 position on the edge of
// the specified circle and then travels clockwise around the edge of the circle until
// it reaches the 6:00 position.

// Documentation for path specification
// https://svgwg.org/svg2-draft/paths.html#DProperty
// MDN: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#Path_commands
// Diagram of elliptical arcs:
// https://svgwg.org/svg2-draft/images/paths/arcs02.svg
function path_data_for_edge_of_circle(x, y, r) {
const start = 0.52*Math.PI
const end = 0.48*Math.PI
const xstart = x + r*Math.cos(start)
const ystart = y + r*Math.sin(start)
const xend = x + r*Math.cos(end)
const yend = y + r*Math.sin(end)
return "M " + xstart + "," + ystart + " A " + r + "," + r + " 0,1,1 " + xend + "," + yend;
}
Insert cell
Insert cell
d3 = require("d3@5")
Insert cell
md`
# To do:
- *Add a legend
- *Disable zoom to the HEIs
- *Move the types
- *Label the types along the edge of the circles
- *Remove the metro and hei labels
- *Change colors
- *Create custom tooltip
- *Give different padding to different levels
- *Figure out how to embed into Wordpress site
- Change color of circle highlighter
- *Review order of tooltip text
- *Make the highest level circles proportional
`
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