Public
Edited
Oct 24, 2022
1 fork
Insert cell
md`# Humanitarian Aid Recipients`
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
//colors used
var background_color = "#f2f3f4"
var highlight_color = "#D79922"
var outline_color = "#4056A1"
var center_circle_color = "#C5CBE3"
var font_color = '#303C6C'

var fontsize = d3.scaleSqrt()
.domain([0, 150])
.range([5, 20])
.clamp(true)
const root = packWithMapChildren(data_total); // B) Final approach //https://observablehq.com/d/ea342ae19603b9a0

let focus = root;
console.log(root)
let view;

var current_title = "Click to Zoom"
const svg = d3.create("svg")
.attr("viewBox", `-${width / 2} -${height /(2)} ${width} ${height}`)
.style("display", "block")
.style("margin", "0 0px")
.style("background", background_color)
//.style("cursor", "pointer")
.on("mouseover", (event) => (svgMouseOut(event, root)))

// tooltip + styling
const toolTip = d3.select("body")
.append("div")
.attr("class", "toolTip")
.style("position", "absolute")
.style("display", "none")
.style("border-radius", "1px")
.style("width", width/3)
.style("height", "auto")
.style("background", background_color)
.style("padding", "0px")
.style("border", "1px solid #DDD")
.style("font-size", "1.1rem")
.style("text-align", "left")

//https://stackoverflow.com/questions/149055/how-to-format-numbers-as-currency-strings
// Create our number formatter.
var formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
notation: 'compact',
// These options are needed to round to whole numbers if that's what you want.
//minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
//maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501)
});

var startTime;
var endTime;
const node = svg.append("g")
.selectAll("circle")
//.data(root)
.data(root.descendants().slice(1))
.join("circle")
.attr("fill", d => d.children ? background_color : toTitleCase(String(d.data[0])) == toTitleCase(String(recipientdrop)) ? highlight_color : center_circle_color)
.attr("stroke", d=> d.children ? highlight_color : toTitleCase(String(d.data[0])) == toTitleCase(String(recipientdrop)) ? highlight_color : outline_color)
.attr("stroke-width", 1)
// .on('touchdown', function() { startTime = new Date(); })
// .on('touchup',function(event, d) {
// endTime = new Date();
// (((endTime - startTime) > 1000) && focus !== d) ? (zoom(d), event.stopPropagation()) : console.log(d)})
//event.stopPropagation stops the mouseover
//being used to trigger a background event
.on("mouseover", (event, d) => (leafMouseOver(event, d), event.stopPropagation()))
.on("mouseout", (event, d) => (leafMouseOut(event, d), event.stopPropagation()))
.on("click", (event, d) => focus !== d ? (zoom(d)) : console.log(d))
.on("touchstart", (event, d) => focus !== d ? (leafMouseOver(event, d)) : console.log(d))
.on("touchend", (event, d) => focus !== d ? (leafMouseOut(event, d), zoom(d)) : console.log(d))



// show tooltip on mousehover
function leafMouseOver(event, d){
d3.selectAll('.toolTip').style("display", "none" )
d3.select(event.target).attr("stroke-width", 10)
d3.select(event.target).attr("stroke", highlight_color)
var formated_money =parseFloat(d.value)*parseFloat(dd3)/root.value
var formated_money = formatter.format(formated_money)
var raw_money = formatter.format(parseFloat(d.value))
var descendents = d.ancestors().map(d => (d.data[0])).join(' -> ')
toolTip
.style("left", (width - d3.select(event.target).attr("cx")) < width/3 ? event.pageX - width/3 + "px": event.pageX + "px")
.style("top", event.pageY + 10 + "px")
.style("display", "block")
.html(`
<b>${toTitleCase(String(d.data[0]))}</b> <br>
Your taxes provide : <strong> ${formated_money} </strong><br>
Total money : <strong> ${raw_money} </strong><br>
Click to zoom in`)
}
function svgMouseOut(event, d){
d3.selectAll('.toolTip').style("display", "none" )
d3.selectAll('.title_desc').style("display", "none" )
}
function leafMouseOut(event, d){
d3.select(event.target).attr("stroke", d=> d.children ? highlight_color : d.data[0] == recipientdrop ? highlight_color : outline_color)
d3.select(event.target).attr("stroke-width", d=> d.data[0] == recipientdrop ? 3 : 1)
//toolTip.style("display", "none" );
}


const label = svg.append("g")
.style("font", "10px sans-serif")
.attr("text-anchor", "middle")
.selectAll("text")
.data(root.descendants())
.join("text")
.style("fill-opacity", d => d.parent === root ? 1 : 0)
.style("display", d => d.parent === root ? "inline" : "none")
.text(d => toTitleCase(String(d.data[0])).substring(0, 30) + "...")
// .on('touchdown', function() { startTime = new Date(); })
// .on('touchup',function(event, d) {
// endTime = new Date();
// (((endTime - startTime) > 1000) && focus !== d) ? (zoom(d), event.stopPropagation()) : console.log(d)})
// .on("click", (event, d) => focus !== d ? (zoom(d), event.stopPropagation()) : console.log(d))

const node_international_affairs = node
//.attr("test", d => console.log(d.descendants().map(d => (d.data[0]))))
.filter(function(d, i) { return d.data[0] == "International Affairs: Budget Function" })
.filter(function(d, i) { return i == 0 })

console.log(node_international_affairs )
if (first_item === recipientdrop) {
zoom(root);
}

else {
node_international_affairs
.each(function(d) {
zoom(d); // d is datum
temp_highlight()
})
}


function temp_highlight(){

node_international_affairs
.each(function(d) {
zoom(d) // d is datum
})
console.log(recipientdrop)
const node_selected = node
//.attr("test", d => console.log(d.descendants().map(d => (d.data[0]))))
.filter(function(d, i) { return d.data[0] == recipientdrop })

console.log(node_selected)
node_selected
.attr("stroke-width", 20)
.attr('stroke-opacity', .8)
}
function zoomTo(v) {
console.log(v)
const k = width / v[2];
view = v;
label.attr("transform", d => d == focus ? `translate(-${0}, -${height * .9 /(2)})` : `translate(${(d.x - v[0]) * k},${(d.y - v[1]) * k})`)
.attr("font-size", d => fontsize(d.r * k))
.style("fill-opacity", d =>d.parent == focus ? 1 : d == focus && d != root ? .9 : 0)
.style("display", d => fontsize(d.r * k) < 10 ? "none": (d.parent == focus | d == focus) ? "inline" : "none")
node.attr("transform", d => `translate(${(d.x - v[0]) * k},${(d.y - v[1]) * k})`);
node.attr("r", d => d.r * k)

}

function zoom(d) {
console.log("zoom")
console.log(d)
const focus0 = focus;
if (d != focus && d.height == 0){
d = d.parent.parent
}
if (d != focus && d.height == 1){
d = d.parent
}

// if (d != focus && d.depth != 1) {
// if (d.parent){
// if (d.parent.children.length == 1){
// d = d.parent
// if (d.parent){
// if (d.parent.children.length == 1){
// d = d.parent
// if (d.parent){
// if (d.parent.children.length == 1){
// d = d.parent
// }
// }
// }
// }
// }
// }}
focus = d;
console.log(focus)
current_title = focus.data[0]
console.log(current_title)
if (focus0 !=focus | focus == root){
zoomTo([focus.x, focus.y, focus.r * 2])}
}



var title1 = svg.append("text")
.attr("x", width / 2 - 100)//`-${width / 2} - 200`)
.attr("y", -height /2 + 100 )//`-${height / 2} - 200`)
.attr("text-anchor", "middle")
.style("cursor", "pointer")
.style("font-size", "18px")
// .on("click", (event, d) => (temp_highlight(), event.stopPropagation()) )
.attr('font-weight', 500)
.style("display", d => recipientdrop != first_item ? "inline" : "none")
.text("Highlight Selected")
.on("mouseover", (event, d) => (d3.select(event.target).style("font-weight", "bold"), temp_highlight(), event.stopPropagation()) )
.on("mouseout", (event, d) => d3.select(event.target).style("font-weight", "normal"))
var title2 = svg.append("text")
.attr("x", width / 2 - 100)//`-${width / 2} - 200`)
.attr("y", -height /2 + 50 )//`-${height / 2} - 200`)
.attr("text-anchor", "middle")
.style("cursor", "pointer")
.style("font-size", "18px")
// .on("click", (event, d) => (zoom(root), event.stopPropagation()) )
.attr('font-weight', 600)
.text("Zoom Out")
.on("mouseover", (event, d) => (d3.select(event.target).style("font-weight", 900), zoom(root), event.stopPropagation()))
.on("mouseout", (event, d) => d3.select(event.target).style("font-weight", 600))

return svg.node();
}
Insert cell
first_item = "Select One"
Insert cell
// current_order = {
// const root = packWithMapChildren(data_total); // B) Final approach //https://observablehq.com/d/ea342ae19603b9a0

// console.log(root)
// //get ancestor of selected groups
// var desc = []

// function onlyUnique(value, index, self) {
// return self.indexOf(value) === index;
// }

// root.each(function(node) {
// if (node.data[0] == recipientdrop)
// {desc.push(node.ancestors().map(d => (d.data[0])))
// }

// });
// desc = desc.filter(onlyUnique);

// //get first common ancestor
// var i = 1
// var min_name = desc.map(d => d[i])
// while (min_name.length > 1 | i == desc[0].length) {
// min_name = desc.map(d => d[i])
// min_name = min_name.filter(onlyUnique);
// i++
// }
// var current_order = desc[0].slice(i - 1, desc[0].length -1).reverse().join(' -> ')
// return current_order}
Insert cell
function onlyUnique(value, index, self) {
return self.indexOf(value) === index;
}
Insert cell
function toTitleCase(str) {
return str.replace(
/\w\S*/g,
function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
}
).replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"");
}
Insert cell
// usaMapCoordinatesDemo = md` ---
// ## ${current_order}`
Insert cell
// min_name = {
// const root = packWithMapChildren(data_total); // B) Final approach //https://observablehq.com/d/ea342ae19603b9a0

// console.log(root)
// //get ancestor of selected groups
// var desc = []



// root.each(function(node) {
// if (node.data[0] == recipientdrop)
// {desc.push(node.ancestors().map(d => (d.data[0])))
// }

// });
// desc = desc.filter(onlyUnique);

// //get first common ancestor
// var i = 1
// var min_name = desc.map(d => d[i])
// while (min_name.length > 1 | i == desc[0].length) {
// min_name = desc.map(d => d[i])
// min_name = min_name.filter(onlyUnique);
// i++
// }
// return min_name}
Insert cell
// chart2 = {
// //colors used
// var background_color = "#f2f3f4"
// var highlight_color = "#D79922"
// var outline_color = "#4056A1"
// var center_circle_color = "#C5CBE3"
// var font_color = '#303C6C'


// var fontsize = d3.scaleSqrt()
// .domain([0, 150])
// .range([1, 20])
// .clamp(true)
// const root = packWithMapChildren(data_total); // B) Final approach //https://observablehq.com/d/ea342ae19603b9a0

// let focus = root;
// console.log(root)
// let view;
// function toTitleCase(str) {
// return str.replace(
// /\w\S*/g,
// function(txt) {
// return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
// }
// );
// }
// const svg = d3.create("svg")
// .attr("viewBox", `-${width / 2} -${height / 2} ${width} ${height}`)
// .style("display", "block")
// .style("margin", "0 -14px")
// .style("background", background_color)
// //.style("cursor", "pointer")
// .on("click", (event) => zoom(root))
// .on("mouseover", (event) => svgMouseOut(event, root))

// // tooltip + styling
// const toolTip = d3.select("body")
// .append("div")
// .attr("class", "toolTip")
// .style("position", "absolute")
// .style("display", "none")
// .style("border-radius", "1px")
// .style("width", "200px")
// .style("height", "auto")
// .style("background", background_color)
// .style("padding", "0px")
// .style("border", "1px solid #DDD")
// .style("font-size", ".9rem")
// .style("text-align", "left")

// //https://stackoverflow.com/questions/149055/how-to-format-numbers-as-currency-strings
// // Create our number formatter.
// var formatter = new Intl.NumberFormat('en-US', {
// style: 'currency',
// currency: 'USD',
// notation: 'compact',
// // These options are needed to round to whole numbers if that's what you want.
// //minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
// //maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501)
// });

// const node = svg.append("g")
// .selectAll("circle")
// //.data(root)
// .data(root.descendants().slice(1))
// .join("circle")
// .attr("fill", d => d.children ? background_color : toTitleCase(String(d.data[0])) == toTitleCase(String(recipientdrop)) ? highlight_color : d.data[0] == min_name[0] ? highlight_color: center_circle_color)
// .attr("stroke", d=> d.children ? highlight_color : toTitleCase(String(d.data[0])) == toTitleCase(String(recipientdrop)) ? highlight_color : d.data[0] == min_name[0] ? highlight_color: outline_color)
// .attr("stroke-width", d => d.data[0] == min_name[0] ? 10: 1)
// .on("click", (event, d) => focus !== d ? (zoom(d), event.stopPropagation()) : console.log(d))
// //event.stopPropagation stops the mouseover
// //being used to trigger a background event
// .on("mouseover", (event, d) => (leafMouseOver(event, d), event.stopPropagation()))
// .on("mouseout", (event, d) => (leafMouseOut(event, d), event.stopPropagation()))

// // show tooltip on mousehover
// function leafMouseOver(event, d){
// d3.selectAll('.toolTip').style("display", "none" )
// d3.select(event.target).attr("stroke-width", 10)
// d3.select(event.target).attr("stroke", highlight_color)
// var formated_money =parseFloat(d.value)*parseFloat(dd3)/root.value
// var formated_money = formatter.format(formated_money)
// var raw_money = formatter.format(parseFloat(d.value))
// toolTip.style("left", (width - event.pageX) < 400 ? event.pageX - 250 + "px": event.pageX + 50 + "px")
// .style("top", event.pageY + "px")
// .style("display", "block")
// .html(`
// <b>${toTitleCase(String(d.data[0]))}</b> <br>
// Your taxes provide : <strong> ${formated_money} </strong><br>
// Total money : <strong> ${raw_money} </strong><br>
// Double click to zoom into this circle`)
// }
// function svgMouseOut(event, d){
// d3.selectAll('.toolTip').style("display", "none" )
// }
// function leafMouseOut(event, d){
// d3.select(event.target).attr("stroke", d=> d.children ? highlight_color : d.data[0] == recipientdrop ? highlight_color : outline_color)
// d3.select(event.target).attr("stroke-width", d=> d.data[0] == recipientdrop ? 3 : 1)
// //toolTip.style("display", "none" );
// }


// const label = svg.append("g")
// .style("font", "10px sans-serif")
// .attr("text-anchor", "middle")
// .selectAll("text")
// .data(root.descendants())
// .join("text")
// .style("fill-opacity", d => d.parent === root ? 1 : 0)
// .style("display", d => d.parent === root ? "inline" : "none")
// .text(d => toTitleCase(String(d.data[0])).substring(0, 30) + "...")
// .on("click", (event, d) => focus !== d ? (zoom(d), event.stopPropagation()) : console.log(d))
// zoom(root)
// function zoomTo(v) {
// console.log(v)
// const k = width / v[2];
// view = v;
// label.attr("transform", d => `translate(${(d.x - v[0]) * k},${(d.y - v[1]) * k})`)
// .attr("font-size", d => fontsize(d.r * k))
// .style("fill-opacity", d =>d.parent == focus ? 1 : 0)
// .style("display", d => fontsize(d.r * k) < 10 ? "none": d.parent == focus ? "inline" : "none")
// node.attr("transform", d => `translate(${(d.x - v[0]) * k},${(d.y - v[1]) * k})`);
// node.attr("r", d => d.r * k);
// }

// function zoom(d) {
// const focus0 = focus;
// if (!d.children) {
// console.log("children")
// console.log(d.children)
// d = d.parent
// console.log(d)
// }
// focus = d;
// zoomTo([focus.x, focus.y, focus.r * 2])

// }


// return svg.node();
// }
Insert cell
Insert cell
csv_breakdown_awards = FileAttachment("FY2021P01-P12_All_FA_AccountBreakdownByAward_2022-01-05_H17M06S49_1.csv").csv().then(
function(data) {
data.forEach(function(d) {
d.value = d.transaction_obligated_amount,
d.budget_function = "International Affairs" + ": Budget Function",
d.owning_agency_name = d.owning_agency_name + ": Agency",
d.federal_account_name = d.federal_account_name + ": Account",
d.budget_subfunction = d.budget_subfunction + ": Budget Subfunction",
d.recipient_name = d.recipient_duns == "71550813" ? "World Food Program"
: d.recipient_duns == "" ? "No Recipient Given: Award Funds"
: toTitleCase(d.recipient_name)
});
return data;
}
)
Insert cell
//by federal_account get the sum of the transaction obligated amount spend on awards so can subtract this from total amount per federal account
subtract_all_awards = Array.from(
d3.rollup(
csv_breakdown_awards,
D => d3.sum(D, d => d["transaction_obligated_amount"]) ,
d => d.federal_account_symbol,
)).flatMap(([key, lvalues]) => {
return {federal_account_symbol: key, transaction_obligated_amount: lvalues};
});

Insert cell
//sum of account balances for all acounts
csv_account_balances_unsummed = FileAttachment("FY2021P01-P12_All_FA_AccountBalances_2022-01-05_H17M11S14_1.csv").csv().then(
function(data) {
data.forEach(function(d) {
d.budget_function = d.budget_function + ": Budget Function",
d.owning_agency_name = d.owning_agency_name + ": Agency",
d.federal_account_name = d.federal_account_name + ": Account",
d.budget_subfunction = d.budget_subfunction + ": Budget Subfunction"
});
return data;
}
)
Insert cell
//to make sure each row represents a federal account, sum by federal account
// there are five examples where the federal account is split into two rows because it has two different reporting agencies
csv_account_balances = {
const data = d3.rollup(
csv_account_balances_unsummed,
D => d3.sum(D, d => d["obligations_incurred"]),
d => d.federal_account_symbol,
)
const full_data = d3.group(csv_account_balances_unsummed, d => d.federal_account_symbol);
return Array.from(data).flatMap(([key, lvalues]) => {
return {federal_account_symbol: key, federal_account_name: full_data.get(key)[0].federal_account_name, obligations_incurred: lvalues, owning_agency_name: full_data.get(key)[0].owning_agency_name, budget_subfunction: full_data.get(key)[0].budget_subfunction, budget_function: full_data.get(key)[0].budget_function};
})
}
Insert cell
//sum of account balances for all acounts, with sum awards for international affairs subtracted, resulting in recipients "federal_acount_name" for federal acounts not in international affaris, and "non-awards" for international affairs account obligations minus award obligations
sum_all_accounts = {
const left = d3.group(csv_account_balances, d => d.federal_account_symbol);
const right = d3.group(subtract_all_awards, d => d.federal_account_symbol);
return Array.from(left).flatMap(([key, lvalues]) => {
//use "none" as placeholder for no match in awards summary
// left join uisng https://talk.observablehq.com/t/how-to-left-join-csv-files/2604
return d3.cross(lvalues, right.get(key) || ["none"], (lvalue, rvalue) => {
return {federal_account_symbol: lvalue.federal_account_symbol, federal_account_name: lvalue.federal_account_name, owning_agency_name: lvalue.owning_agency_name, budget_subfunction: lvalue.budget_subfunction, budget_function: lvalue.budget_function, value: rvalue.transaction_obligated_amount? Number(lvalue.obligations_incurred - rvalue.transaction_obligated_amount): Number(lvalue.obligations_incurred), recipient_name: rvalue.transaction_obligated_amount? "Non-Award Funds": lvalue.federal_account_name};
});
});
}
Insert cell
//add together so rows are either the federal account total funds (for non international) or funds minus awards (for none international), or rows are awards
data_total = csv_breakdown_awards.concat(sum_all_accounts).filter(d => d["value"] > 0)
// .objects() // Uncomment to return an array of objects
Insert cell

recip_list_initial = recipient_account_sum.filter(d => d["value"] > 1000000).map(d => d.recipient_name).sort()
Insert cell
recip_list = recip_list_initial.filter(function(item) {
return item !== "No Recipient Given Award Funds"
})
Insert cell
//thanks to https://observablehq.com/d/ea342ae19603b9a0

keysToGroupBy = groupByPriority === "Agency->Account" ? [d => d.budget_function, d => d.budget_subfunction, d =>d.owning_agency_name, d=> d.federal_account_name, d => d.recipient_name] : [d => d.budget_function, d => d.budget_subfunction, d=> d.federal_account_name, d =>d.owning_agency_name, d => d.recipient_name];
Insert cell
recipient_account_sum = {
const data = d3.rollup(
csv_breakdown_awards,
D => d3.sum(D, d => d["transaction_obligated_amount"]),
d => toTitleCase(d.recipient_name),
)
return Array.from(data).flatMap(([key, lvalues]) => {
return {recipient_name: key, value: lvalues};
})
}
Insert cell
//thanks to https://observablehq.com/d/ea342ae19603b9a0 for data wrangling and grouping option

hierarchyWithMapChildren = d3.hierarchy(d3.rollup(data_total, D => d3.sum(D, d => d["value"]) , ...keysToGroupBy))
.sum(([, value]) => value)
.sort((a, b) => b.value - a.value)
Insert cell
packWithMapChildren = data_total => d3.pack()
.size([width - 2, height - 2])
.padding(0.2)
(hierarchyWithMapChildren)
Insert cell
width = 932
Insert cell
height = width
Insert cell
import {select} from "@jashkenas/inputs"
Insert cell
format = d3.format(",d")
Insert cell
nameForRootNode = "Platform"
Insert cell
color = d3.scaleLinear()
.domain([0, 5])
.range(["hsl(152,80%,80%)", "hsl(228,30%,40%)"])
.interpolate(d3.interpolateHcl)
Insert cell
d3 = require("d3@6")
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