Household Disaggregation and Vehicle Ownership
Household Disaggregation
Modeled vs. Observed
Code
Code
viewof vGeoLevel = Inputs.select(["CO_FIPS", "PUMA"],
{ label: "Geography Level:", value: "CO_FIPS" })
viewof vHHSIZE = Inputs.select(hhsize_list,
{ label: "Household Size:", value: hhsize_list[0] })
viewof vINC = Inputs.select(inc_list,
{ label: "Income Level:", value: inc_list[0] })
viewof vWRKS = Inputs.select(wrks_list,
{ label: "Workers:", value: wrks_list[0] })Code
viewof vXAxis = Inputs.select(["Modeled", "PUMS", "HTS"],
{ label: "X Axis:", value: "PUMS" })
viewof vYAxis = Inputs.select(
["Modeled", "PUMS", "HTS"].filter(d => d !== vXAxis),
{
label: "Y Axis:",
value: ["Modeled", "PUMS", "HTS"].find(d => d !== vXAxis)
}
)
viewof vDisplay = Inputs.radio(["Households", "Percentages"],
{ label: "Display:", value: "Percentages" })Code
hh_filtered = hh_data.filter(d =>
(vHHSIZE === "Total" || d.HHSIZE === vHHSIZE) &&
(vINC === "Total" || d.INC === vINC) &&
(vWRKS === "Total" || d.WRKS === vWRKS)
)
hh_agg = {
const grouped = d3.groups(hh_filtered, d => d[vGeoLevel]);
const rows = grouped.map(([geo_id, records]) => {
const mod = d3.sum(records, d => d.Modeled_Count);
const pums = d3.sum(records, d => d.Observed_Count);
const hts = d3.sum(records, d => d.HTS_Count);
return {
geo_id,
Modeled_Count: mod, Observed_Count: pums, HTS_Count: hts,
Mod_vs_PUMS: pums > 0 ? (mod - pums) / pums : 0,
Mod_vs_HTS: hts > 0 ? (mod - hts) / hts : 0,
PUMS_vs_Mod: mod > 0 ? (pums - mod) / mod : 0,
HTS_vs_Mod: mod > 0 ? (hts - mod) / mod : 0,
PUMS_vs_HTS: hts > 0 ? (pums - hts) / hts : 0,
HTS_vs_PUMS: pums > 0 ? (hts - pums) / pums : 0,
};
});
// Each source normalised by its own total — pcts sum to 100 % across geographies
const totMod = d3.sum(rows, d => d.Modeled_Count);
const totPums = d3.sum(rows, d => d.Observed_Count);
const totHts = d3.sum(rows, d => d.HTS_Count);
return rows.map(r => ({
...r,
Modeled_Pct: totMod > 0 ? r.Modeled_Count / totMod : 0,
Observed_Pct: totPums > 0 ? r.Observed_Count / totPums : 0,
HTS_Pct: totHts > 0 ? r.HTS_Count / totHts : 0,
}));
}
// --- GeoJSON boundary merge for HH map ---
map_features_hh = {
const geo_lookup = {
"CO_FIPS": { geo: geo_county_hh, prop: "CO_FIPS" },
"PUMA": { geo: geo_puma_hh, prop: "PUMA" }
};
const { geo: base_geo, prop: name_prop } = geo_lookup[vGeoLevel];
const features = base_geo.features.map(f => {
const prop_name = f.properties[name_prop];
// Weak equality (==) or String conversion handles int vs string mismatches
const data_row = hh_agg.find(d => String(d.geo_id) === String(prop_name))
|| { geo_id: prop_name, Modeled_Count: 0, Observed_Count: 0, HTS_Count: 0,
Modeled_Pct: 0, Observed_Pct: 0, HTS_Pct: 0,
Mod_vs_PUMS: null, Mod_vs_HTS: null, PUMS_vs_Mod: null,
HTS_vs_Mod: null, PUMS_vs_HTS: null, HTS_vs_PUMS: null };
return { ...f, properties: { ...f.properties, ...data_row } };
});
return { type: "FeatureCollection", features: features };
}
hh_xy_cfg = {
const fm = {
"PUMS": { field: "Observed_Count", pctField: "Observed_Pct", label: "Observed (PUMS)", color: "steelblue" },
"HTS": { field: "HTS_Count", pctField: "HTS_Pct", label: "Observed (HTS)" , color: "#2ca02c" },
"Modeled": { field: "Modeled_Count", pctField: "Modeled_Pct", label: "Modeled", color: "orange" }
};
const errMap = {
"PUMS|Modeled": "Mod_vs_PUMS", "PUMS|HTS": "HTS_vs_PUMS",
"HTS|Modeled": "Mod_vs_HTS", "HTS|PUMS": "PUMS_vs_HTS",
"Modeled|PUMS": "PUMS_vs_Mod", "Modeled|HTS": "HTS_vs_Mod"
};
return { xCfg: fm[vXAxis], yCfg: fm[vYAxis], errField: errMap[`${vXAxis}|${vYAxis}`] };
}
// Picks raw-count vs pct fields/formats based on the display toggle
hh_vf = vDisplay === "Percentages"
? { xf: hh_xy_cfg.xCfg.pctField, yf: hh_xy_cfg.yCfg.pctField,
fmt: d3.format(".1%"), tickFmt: d3.format(".0%"), axSuffix: " (%)" }
: { xf: hh_xy_cfg.xCfg.field, yf: hh_xy_cfg.yCfg.field,
fmt: d3.format(",.0f"), tickFmt: undefined, axSuffix: "" }
chartMax = d3.max(hh_agg, d => Math.max(d[hh_vf.xf], d[hh_vf.yf])) * 1.05Code
Plot.plot({
grid: true,
width: 460,
height: 380,
marginRight: 50,
caption: html`<h4>${hh_xy_cfg.yCfg.label} vs. ${hh_xy_cfg.xCfg.label}</h4>`,
x: { label: `${hh_xy_cfg.xCfg.label}${hh_vf.axSuffix} →`, domain: [0, chartMax],
tickFormat: hh_vf.tickFmt },
y: { label: `↑ ${hh_xy_cfg.yCfg.label}${hh_vf.axSuffix}`, domain: [0, chartMax],
tickFormat: hh_vf.tickFmt },
marks: [
Plot.link([0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4], {
x1: 0, y1: 0,
x2: (k) => (k <= 1 ? chartMax : chartMax / k),
y2: (k) => (k <= 1 ? chartMax * k : chartMax),
strokeOpacity: (k) => k === 1 ? 1 : 0.2,
stroke: "gray",
strokeWidth: (k) => k === 1 ? 2 : 1
}),
Plot.text([0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4], {
x: (k) => (k <= 1 ? chartMax : chartMax / k),
y: (k) => (k <= 1 ? chartMax * k : chartMax),
text: (k) => k === 1 ? "Equal" : d3.format("+.0%")(k - 1),
textAnchor: "start", dx: 6, fill: "gray", fontSize: 10
}),
Plot.dot(hh_agg, {
x: hh_vf.xf, y: hh_vf.yf,
r: 4, fill: hh_xy_cfg.yCfg.color, fillOpacity: 0.6,
tip: true,
title: (d) =>
`${vGeoLevel}: ${d.geo_id}\n` +
`${hh_xy_cfg.xCfg.label}: ${hh_vf.fmt(d[hh_vf.xf])}\n` +
`${hh_xy_cfg.yCfg.label}: ${hh_vf.fmt(d[hh_vf.yf])}`
}),
Plot.linearRegressionY(hh_agg, {
x: hh_vf.xf, y: hh_vf.yf,
stroke: hh_xy_cfg.yCfg.color, strokeDasharray: "4 4"
})
]
})Code
Plot.plot({
grid: true,
width: 460,
height: 380,
caption: html`<h4>${hh_xy_cfg.xCfg.label} as Reference — % Error</h4>`,
x: { label: `${hh_xy_cfg.xCfg.label}${hh_vf.axSuffix} →`, domain: [0, chartMax],
tickFormat: hh_vf.tickFmt },
y: { label: "↑ Percent Error", domain: [-1.5, 1.5], tickFormat: d3.format(".0%") },
marks: [
Plot.ruleY([0], { stroke: "#000", strokeWidth: 1.5 }),
Plot.dot(hh_agg, {
x: hh_vf.xf, y: hh_xy_cfg.errField,
r: 4, fill: hh_xy_cfg.yCfg.color, fillOpacity: 0.6,
tip: true,
title: (d) =>
`${vGeoLevel}: ${d.geo_id}\n` +
`${hh_xy_cfg.xCfg.label}: ${hh_vf.fmt(d[hh_vf.xf])}\n` +
`Error (${hh_xy_cfg.yCfg.label}): ${d3.format("+.1%")(d[hh_xy_cfg.errField])}`
})
]
})Code
{
// Guard prevents duplicate <link> tags on reactive re-runs
if (!document.getElementById("leaflet-css")) {
const link = document.createElement("link");
link.id = "leaflet-css";
link.rel = "stylesheet";
link.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css";
document.head.appendChild(link);
}
const L = await require("leaflet@1.9.4");
const container = html`<div style="height:560px; width:100%; border-radius:6px; overflow:hidden;"></div>`;
yield container;
const map = L.map(container, {zoomControl: true});
L.tileLayer(
"https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png",
{
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors ' +
'© <a href="https://carto.com/attributions">CARTO</a>',
subdomains: "abcd",
maxZoom: 19,
}
).addTo(map);
const colorScale = d3.scaleDiverging(d3.interpolateRdBu).domain([0.5, 0, -0.5]);
function getColor(pct) {
if (pct === null || pct === undefined) return "#cccccc";
return colorScale(Math.max(-0.5, Math.min(0.5, pct)));
}
let geojsonLayer = L.geoJSON(map_features_hh, {
style: feature => ({
fillColor: getColor(feature.properties[hh_xy_cfg.errField]),
fillOpacity: 0.72,
color: "#ffffff",
weight: 1,
}),
onEachFeature: (feature, layer) => {
const p = feature.properties;
const err = p[hh_xy_cfg.errField];
const noData = err === null || err === undefined;
layer.bindPopup(
`<b>${vGeoLevel} ${p.geo_id}</b><br>` +
`<i style="font-size:10px;color:#666">${hh_xy_cfg.yCfg.label} vs. ${hh_xy_cfg.xCfg.label}</i><br>` +
`Pct Error: ${noData ? "N/A" : (err * 100).toFixed(1) + "%"}<br>` +
`Modeled: ${vDisplay === "Percentages" ? d3.format(".1%")(p.Modeled_Pct) : d3.format(",.0f")(p.Modeled_Count)}<br>` +
`Observed (PUMS): ${vDisplay === "Percentages" ? d3.format(".1%")(p.Observed_Pct) : d3.format(",.0f")(p.Observed_Count)}<br>` +
`Observed (HTS): ${vDisplay === "Percentages" ? d3.format(".1%")(p.HTS_Pct) : d3.format(",.0f")(p.HTS_Count)}`
);
layer.on("mouseover", () => layer.setStyle({fillOpacity: 0.95, weight: 2}));
layer.on("mouseout", () => geojsonLayer.resetStyle(layer));
}
}).addTo(map);
if (geojsonLayer.getBounds().isValid()) {
map.fitBounds(geojsonLayer.getBounds(), {padding: [10, 10]});
}
// Ensure map draws correctly inside a hidden tab
const resizeObserver = new ResizeObserver(() => {
setTimeout(() => {
map.invalidateSize();
if (geojsonLayer.getBounds().isValid()) {
map.fitBounds(geojsonLayer.getBounds(), {padding: [10, 10]});
}
}, 100);
});
resizeObserver.observe(container);
invalidation.then(() => { map.remove(); resizeObserver.disconnect(); });
// Legend
const legend = L.control({position: "bottomright"});
legend.onAdd = () => {
const stops = [-0.5, -0.25, 0, 0.25, 0.5];
const labels = ["≤ −50%", "−25%", "0%", "+25%", "≥ +50%"];
const swatches = stops.map((v, i) =>
`<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">` +
`<span style="display:inline-block;width:14px;height:14px;border-radius:2px;flex-shrink:0;` +
`background:${getColor(v)};border:1px solid rgba(0,0,0,0.15);"></span>` +
`<span style="font-size:11px;">${labels[i]}</span>` +
`</div>`
).join("");
const div = L.DomUtil.create("div");
div.innerHTML =
`<div style="background:white;padding:10px 12px;border-radius:6px;` +
`box-shadow:0 1px 5px rgba(0,0,0,0.25);line-height:1.5;">` +
`<div style="font-size:12px;font-weight:600;margin-bottom:4px;">Percent Error</div>` +
`<div style="font-size:10px;color:#666;margin-bottom:6px;">` +
`Red = Underpredict | Blue = Overpredict` +
`</div>` +
swatches +
`</div>`;
return div;
};
legend.addTo(map);
}Code
hh_cnt_fmt = vDisplay === "Percentages" ? d3.format(".1%") : d3.format(",.0f")
viewof hh_summary_table = Inputs.table(hh_agg, {
columns: [
"geo_id",
vDisplay === "Percentages" ? "Modeled_Pct" : "Modeled_Count",
vDisplay === "Percentages" ? "Observed_Pct" : "Observed_Count",
vDisplay === "Percentages" ? "HTS_Pct" : "HTS_Count",
"Mod_vs_PUMS", "Mod_vs_HTS", "PUMS_vs_HTS"
],
header: {
geo_id: "Geo ID",
Modeled_Count: "Modeled", Modeled_Pct: "Modeled (%)",
Observed_Count: "PUMS", Observed_Pct: "PUMS (%)",
HTS_Count: "HTS", HTS_Pct: "HTS (%)",
Mod_vs_PUMS: "Mod vs PUMS",
Mod_vs_HTS: "Mod vs HTS",
PUMS_vs_HTS: "PUMS vs HTS"
},
format: {
Modeled_Count: hh_cnt_fmt, Modeled_Pct: hh_cnt_fmt,
Observed_Count: hh_cnt_fmt, Observed_Pct: hh_cnt_fmt,
HTS_Count: hh_cnt_fmt, HTS_Pct: hh_cnt_fmt,
Mod_vs_PUMS: d3.format("+.1%"),
Mod_vs_HTS: d3.format("+.1%"),
PUMS_vs_HTS: d3.format("+.1%")
},
sort: "Mod_vs_PUMS",
reverse: true,
rows: 15,
width: "100%"
})