Public
Edited
Nov 8, 2024
2 forks
8 stars
Insert cell
Insert cell
url = "https://data.cityofnewyork.us/resource/mwzb-yiwb.json"
Insert cell
Inputs.table(tableData)
Insert cell
tableData = {
let data = [], res, i =0;
const MAX_PAGES = 100,
ROWS_PER_PAGE = 50000;

while (i++<MAX_PAGES) {
res = await fetch(`https://data.cityofnewyork.us/resource/mwzb-yiwb.json?$limit=${ROWS_PER_PAGE}&$offset=${data.length}&publication_date=20190207`).then(res=> res.json());
if (!res || res.length===0) break;
data = data.concat(res);
yield data;
}
yield data;
}
Insert cell
Insert cell
treeData = d3.group(
tableData.filter(d => d.current_modified_budget_amount > 0), // only consider accounts with budget > 0
d => d.agency_name.trim(),
d => d.object_class_name.trim()
)
Insert cell
Insert cell
hierarchyData = {
// Function to reorganize the nodes for the viz
const extractName = d => {
console.log(d);
d.name = d.children ? d.data[0] || "" : d.data.object_code_name;
d.name = d.name.toLowerCase();
d.id = (d.parent ? d.parent.id + "." : "") + d.name;
};

return d3
.hierarchy(treeData)
.eachBefore(extractName)
.sum(d => +d.current_modified_budget_amount)
.sort((a, b) => b.value - a.value);
}
Insert cell
Insert cell
laidoutData = {
const treemapLayout = d3.treemap()
.tile(d3.treemapSquarify)
.size([width, width*3/4])
.round(true)
.paddingInner(1);
return treemapLayout(hierarchyData);
}
Insert cell
Insert cell
format = d3.format("$,.2s")
Insert cell
color = d3.scaleOrdinal(d3.schemePastel1)
Insert cell
{
const treemapData = laidoutData;
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, (width * 3) / 4])
.style("font", "10px sans-serif");

const leaf = svg
.selectAll("g.leaf")
.data(treemapData.leaves().filter(d => d.x1 - d.x0 > 0.5)) //exclude very small rectangles
.join("g")
.attr("class", "leaf")
.attr("transform", d => `translate(${d.x0},${d.y0})`);

leaf.append("title").text(
d =>
`${d
.ancestors()
.reverse()
.map(d => d.name)
.join("/")}\n${format(d.value)}`
);

leaf
.append("rect")
.attr("id", d => (d.leafUid = DOM.uid("leaf")).id)
.attr("fill", d => {
while (d.depth > 1) d = d.parent;
return color(d.data[0]);
})
.attr("fill-opacity", 0.6)
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0);

leaf
.filter(d => d.x1 - d.x0 > 30) // Don't do text on very small nodes
.call(leaf => {
leaf
.append("clipPath")
.attr("id", d => (d.clipUid = DOM.uid("clip")).id)
.append("use")
.attr("xlink:href", d => d.leafUid.href);

leaf
.append("text")
.attr("clip-path", d => d.clipUid)
.selectAll("tspan")
.data(d =>
d
.ancestors()
.reverse()
.slice(1)
.map(d => d.name)
.concat(format(d.value))
)
.join("tspan")
.attr("x", 3)
.attr(
"y",
(d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + 1.1 + i * 0.9}em`
)
.attr("fill-opacity", (d, i, nodes) =>
i === nodes.length - 1 ? 0.7 : null
)
.text(d => d);
});

const innerNodes = svg
.selectAll("g.inner")
.data(
treemapData
.descendants()
.filter(d => d.children && d.depth <= 2 && d.x1 - d.x0 > 20)
)
.join("g")
.attr("class", "inner")
.style("pointer-events", "none")
.style("cursor", "none")
.attr("transform", d => `translate(${d.x0},${d.y0})`);

innerNodes
.append("rect")
.style("fill", "none")
.style("stroke", "#333")
.attr("id", d => (d.leafUid = DOM.uid("leaf")).id)
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0);

innerNodes
.append("clipPath")
.attr("id", d => (d.clipUid = DOM.uid("clip")).id)
.append("use")
.attr("xlink:href", d => d.leafUid.href);

innerNodes
.append("text")
.attr("clip-path", d => d.clipUid)
.attr("y", d => (d.depth === 1 ? 30 : (d.y1 - d.y0) / 2))
.attr("x", d => (d.x1 - d.x0) / 2)
.style("text-anchor", "middle")
.style("font-size", "3em")
.attr("fill-opacity", 0.3)
.style("stroke", "white")
.style("stroke-width", d => (d.depth === 1 ? 1 : 0.5))
.text(d => d.name);

return svg.node();
}
Insert cell
d3 = require("d3@6")
Insert cell
Insert cell
// {
// // Vega lite doesn't like pointers in the data
// const simplifiedData = laidoutData.leaves().map(d => {
// const node = {
// x0:d.x0,
// y0:d.y0,
// x1:d.x1,
// y1:d.y1,
// name: d.data.name,
// value: d.value
// };
// // Find the highest parent key
// let eldestParent = d;
// while (eldestParent.depth > 1) {
// eldestParent = eldestParent.parent;
// }
// node.parentKey = eldestParent.data.key;
// return node;
// }).filter(d => d.x1 - d.x0>1);
// console.log(laidoutData.leaves());
// console.log("simple", simplifiedData);

// const chart = vl.markRect({tooltip: {data:true}})
// .data(simplifiedData)
// .width( width)
// .height(width/2)
// .encode(
// vl.x().fieldQ("x0").axis(null).scale({zero:false}),
// vl.y().fieldQ("y0").sort("descending").axis(null).scale({zero:false}),
// vl.x2().fieldQ("x1"),
// vl.y2().fieldQ("y1"),
// vl.color().fieldN("parentKey"),
// vl.tooltip(["name", "value", "parentKey"])
// );
// return chart
// .render();
// }
Insert cell
import {vl} from "@vega/vega-lite-api"
Insert cell
function cloneTree(node) {
const clonedNode = Object.create(Object.getPrototypeOf(node));
clonedNode.prototype = node.prototype;
if (node.children) node.children = node.children.map(cloneTree);
return clonedNode;
}
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