- Version 10.0.0
- Calibration - C22
- 2b HBW Destination Choice Model (C22)
HBW Destination Choice Model
Production & Attractions 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,
// Added Percent Error calculation for the second chart
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: [
// Fan Reference Lines (+/- 10% increments)
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
}),
// Labels for Fan Lines
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
}),
// Data Points
Plot.dot(dc_filtered, {
x: "tmeObs",
y: "tmeMod",
r: 3, // Slightly larger dots are easier to hover
fill: "rgb(80, 116, 230)",
fillOpacity: 0.5,
// Add Tooltip data here:
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)}`
}),
// Regression Line
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,
// Add Tooltip data here:
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)}`
}),
// The Validation "Staircase" (Targets)
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 & Attractions by Vehicle Ownership & Income Category
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.veh_inc === m.veh_inc
);
if (!o || o.total_trips === 0) return null;
return {
p_DistLrg: m.p_DistLrg,
a_DistLrg: m.a_DistLrg,
veh_inc: m.veh_inc,
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);
}
// 2. STABLE SELECTIONS
// Get the list of incomes ONCE from the master list
vincs_list = Array.from(new Set(dc_2.map(d => d.veh_inc))).sort()
// Model Type Selection
viewof vSource_2 = Inputs.select(
["Destination Choice", "Gravity"],
{ label: "Model Type:", value: "Destination Choice" }
)
// Vehicle Income Selection (Uses the stable vincs_list)
viewof vVehInc_2 = Inputs.select(
vincs_list,
{ label: "Vehicle-Income:", value: vincs_list[0] }
)
// 3. FINAL FILTERING
// This is the only part that reacts to the dropdowns
dc_filtered_2 = dc_2.filter(d => d.Source === vSource_2 && d.veh_inc === vVehInc_2)
// Shared Max for the Scatter Plot axes
chartMax_2 = d3.max(dc_filtered_2, d => Math.max(d.tmeObs, d.tmeMod))Code
// CHART 2a: Model vs Observed Trips (by Vehicle Income)
Plot.plot({
grid: true,
width: 460,
height: 380,
marginRight: 50,
caption: html`<h4>2a. Model vs Observed Trips (Income Group)</h4>`,
x: { label: "Observed Trips", domain: [0, chartMax_2] },
y: { label: "Modeled Trips", domain: [0, chartMax_2] },
marks: [
// Fan Reference Lines
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_2 : chartMax_2 / k),
y2: (k) => (k <= 1 ? chartMax_2 * k : chartMax_2),
strokeOpacity: (k) => k === 1 ? 1 : 0.2,
stroke: "gray",
strokeWidth: (k) => k === 1 ? 2 : 1
}),
// Labels for Fan Lines
Plot.text([0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4], {
x: (k) => (k <= 1 ? chartMax_2 : chartMax_2 / k),
y: (k) => (k <= 1 ? chartMax_2 * k : chartMax_2),
text: (k) => k === 1 ? "Equal" : d3.format("+.0%")(k - 1),
textAnchor: "start", dx: 6, fill: "gray", fontSize: 10
}),
// Data Points
Plot.dot(dc_filtered_2, {
x: "tmeObs",
y: "tmeMod",
r: 3, // Slightly larger dots are easier to hover
fill: "rgb(80, 116, 230)",
fillOpacity: 0.5,
// Add Tooltip data here:
tip: true,
title: (d) =>
`District: ${d.p_DistLrg} to ${d.a_DistLrg}\n` +
`Income/Veh: ${d.veh_inc}\n` +
`Observed: ${d3.format(",.0f")(d.tmeObs)}\n` +
`Modeled: ${d3.format(",.0f")(d.tmeMod)}`
}),
// Regression Line
Plot.linearRegressionY(dc_filtered_2, {
x: "tmeObs", y: "tmeMod",
stroke: "rgb(80, 116, 230)", strokeDasharray: "4 4"
})
]
})Code
// CHART 2b: Percent Error (by Vehicle Income)
Plot.plot({
grid: true,
width: 460,
height: 380,
caption: html`<h4>2b. Model vs Observed Percent Error (Income Group)</h4>`,
x: { label: "Observed Trips", domain: [0, chartMax_2] },
y: { label: "Percent Error", domain: [-2, 2], tickFormat: d3.format(".0%") },
marks: [
Plot.ruleY([0], { stroke: "#000", strokeWidth: 1.5 }),
Plot.dot(dc_filtered_2, {
x: "tmeObs",
y: "tmeErrorPct",
r: 3,
fill: "rgb(80, 116, 230)",
fillOpacity: 0.5,
// Add Tooltip data here:
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)}`
}),
// The Validation "Staircase" (Targets)
Plot.line([[0, 1.0], [500, 1.0], [500, 0.5], [1000, 0.5], [1000, 0.25], [chartMax_2, 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_2, -0.25]],
{ stroke: "gray", strokeWidth: 1.5, strokeDasharray: "2 2", curve: "step-after" })
]
})