HBW Destination Choice Model
Production & Attraction Pairs by Model
Code
dc = {
const modT = transpose(dist_sum_mod);
const obsT = transpose(dist_sum_obs);
return modT.map(m => {
const o = obsT.find(d =>
d.p_DistLrg === m.p_DistLrg &&
d.a_DistLrg === m.a_DistLrg &&
d.Source === m.Source
);
if (!o || o.total_trips === 0) return null;
return {
p_DistLrg: m.p_DistLrg,
a_DistLrg: m.a_DistLrg,
Source: m.Source,
tmeMod: m.total_trips,
tmeObs: o.total_trips,
tmeErrorPct: (m.total_trips - o.total_trips) / o.total_trips
};
}).filter(d => d !== null);
}
// Source Selection
viewof vSource = Inputs.select(
Array.from(new Set(dc.map(d => d.Source))).sort(),
{ label: "Model Source:" }
)
// Filtered data based on selection
dc_filtered = dc.filter(d => d.Source === vSource)
// Shared Max for the Scatter Plot axes
chartMax = d3.max(dc_filtered, d => Math.max(d.tmeObs, d.tmeMod))Code
// CHART 1a: Model vs Observed Trips
Plot.plot({
grid: true,
width: 460,
height: 380,
marginRight: 50,
caption: html`<h4>1a. Model vs Observed Trips</h4>`,
x: { label: "Observed Trips", domain: [0, chartMax] },
y: { label: "Modeled Trips", domain: [0, chartMax] },
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(dc_filtered, {
x: "tmeObs",
y: "tmeMod",
r: 3,
fill: "rgb(80, 116, 230)",
fillOpacity: 0.5,
tip: true,
title: (d) =>
`District: ${d.p_DistLrg} to ${d.a_DistLrg}\n` +
`Observed: ${d3.format(",.0f")(d.tmeObs)}\n` +
`Modeled: ${d3.format(",.0f")(d.tmeMod)}`
}),
Plot.linearRegressionY(dc_filtered, {
x: "tmeObs", y: "tmeMod",
stroke: "rgb(80, 116, 230)", strokeDasharray: "4 4"
})
]
})Code
// CHART 1b: Percent Error
Plot.plot({
grid: true,
width: 460,
height: 380,
caption: html`<h4>1b. Model vs Observed Percent Error</h4>`,
x: { label: "Observed Trips", domain: [0, chartMax] },
y: { label: "Percent Error", domain: [-2, 2], tickFormat: d3.format(".0%") },
marks: [
Plot.ruleY([0], { stroke: "#000", strokeWidth: 1.5 }),
Plot.dot(dc_filtered, {
x: "tmeObs",
y: "tmeErrorPct",
r: 3,
fill: "rgb(80, 116, 230)",
fillOpacity: 0.5,
tip: true,
title: (d) =>
`District: ${d.p_DistLrg} to ${d.a_DistLrg}\n` +
`Observed: ${d3.format(",.0f")(d.tmeObs)}\n` +
`Error: ${d3.format("+.1%")(d.tmeErrorPct)}`
}),
Plot.line([[0, 1.0], [500, 1.0], [500, 0.5], [1000, 0.5], [1000, 0.25], [chartMax, 0.25]],
{ stroke: "gray", strokeWidth: 1.5, strokeDasharray: "2 2", curve: "step-after" }),
Plot.line([[0, -1.0], [500, -1.0], [500, -0.5], [1000, -0.5], [1000, -0.25], [chartMax, -0.25]],
{ stroke: "gray", strokeWidth: 1.5, strokeDasharray: "2 2", curve: "step-after" })
]
})Production & Attraction Pairs by Auto Ownership & Income
Code
dc_2 = {
const modT2 = transpose(dist_sum_mod2);
const obsT2 = transpose(dist_sum_obs2);
return modT2.map(m => {
const o = obsT2.find(d =>
d.p_DistLrg === m.p_DistLrg &&
d.a_DistLrg === m.a_DistLrg &&
d.segment === m.segment // Match on the new segment column
);
if (!o || o.total_trips === 0) return null;
// Define whether this segment belongs to the Income view or Vehicle view
const isIncome = ["Low Income", "High Income"].includes(m.segment);
const viewCategory = isIncome ? "Income" : "Vehicles";
return {
p_DistLrg: m.p_DistLrg,
a_DistLrg: m.a_DistLrg,
segment: m.segment,
viewCat: viewCategory,
Source: m.Source,
tmeMod: m.total_trips,
tmeObs: o.total_trips,
tmeErrorPct: (m.total_trips - o.total_trips) / o.total_trips
};
}).filter(d => d !== null);
}
// Stable sorted facet dimension list for columns
source_vals_2 = ["Gravity", "Destination Choice"]Code
Code
dc_2_filtered = dc_2.filter(d => d.viewCat === selected_view_2)
// Set dynamic row domains based on the dropdown
dynamic_row_domains = selected_view_2 === "Income"
? ["Low Income", "High Income"]
: ["0 Vehicle", "1 Vehicle", "2+ Vehicles"]
// Generate a stack of individual plots using Flexbox to put the title on the right
html`<div style="display: flex; flex-direction: column; gap: 30px;">
${dynamic_row_domains.map(segment => {
// 1. Filter data exclusively for this specific row (segment)
const row_data = dc_2_filtered.filter(d => d.segment === segment);
// 2. Calculate a localized max for this segment's axes
const row_max = (d3.max(row_data, d => Math.max(d.tmeObs, d.tmeMod)) || 1) * 1.05;
// 3. Render the plot without the caption
const plotElement = Plot.plot({
grid: true,
width: 850, // Slightly reduced to make room for the right-side label
height: 350,
marginRight: 40,
marginLeft: 110,
marginTop: 50,
marginBottom: 50,
style: { fontSize: "15px" },
fx: { label: "Model Source", domain: source_vals_2, padding: 0.1 },
x: { label: "Observed Trips →", domain: [0, row_max], labelOffset: 40 },
y: { label: `↑ Modeled Trips`, domain: [0, row_max], labelOffset: 55 },
marks: [
// 45-degree reference line
Plot.link(
source_vals_2.map(s => ({ s })),
{
x1: 0, y1: 0,
x2: row_max, y2: row_max,
fx: "s",
stroke: "gray", strokeOpacity: 0.7, strokeWidth: 1.5
}
),
// Data points
Plot.dot(row_data, {
x: "tmeObs",
y: "tmeMod",
fx: "Source",
r: 3,
fill: "rgb(80, 116, 230)",
fillOpacity: 0.5,
tip: true,
title: d =>
`District: ${d.p_DistLrg} → ${d.a_DistLrg}\n` +
`Source: ${d.Source} | Segment: ${d.segment}\n` +
`Observed: ${d3.format(",.0f")(d.tmeObs)}\n` +
`Modeled: ${d3.format(",.0f")(d.tmeMod)}`
}),
// Forced-through-origin regression line
Plot.link(
source_vals_2.map(s => {
const subset = row_data.filter(d => d.Source === s);
const denom = d3.sum(subset, d => d.tmeObs ** 2);
const slope = denom === 0 ? 0 : d3.sum(subset, d => d.tmeObs * d.tmeMod) / denom;
return { s, slope };
}),
{
x1: 0, y1: 0,
x2: row_max,
y2: d => d.slope * row_max,
fx: "s",
stroke: "rgb(80, 116, 230)",
strokeDasharray: "4 4",
strokeWidth: 2
}
)
]
});
// 4. Wrap the plot and the new right-side label in a Flexbox container
return html`<div style="display: flex; align-items: center; justify-content: flex-start;">
<div>${plotElement}</div>
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
margin-left: 20px;
font-weight: 600;
font-size: 16px;
color: #333;
text-align: center;">
${segment}
</div>
</div>`;
})}
</div>`