Demand Modification
Create, edit, and delete demand points and pops (commuters)
The Demand API lets mods modify demand data at runtime: add new demand points, move or resize existing ones, and create or remove pops (the commuters that travel between them). Use it to build scenario mods, simulate population growth, or add demand to custom cities after load.
For read-only access to demand data, use `gameState.getDemandData()`. To customize when pops commute, see Commute Timing.
Data Model#
Demand is made of two linked structures:
- Demand points — locations on the map with
residents(people living there) andjobs(people working there). These render as the demand bubbles. - Pops — groups of commuters. Each pop has a
size(number of people), aresidenceId(the demand point it lives at), and ajobId(the demand point it works at). Pops are what actually ride your network.
The API keeps them consistent automatically: adding a pop increments its residence point's residents and its job point's jobs by the pop's size, and links the pop to both points. Removing reverses it. A point's demand bubble and the commute simulation both update after every call, and `hooks.onDemandChange` fires.
A point's `jobs`/`residents` numbers do not create commuters. They are display values — they size the demand bubble and feed the stats panels, nothing else. Every person in the simulation is a pop with both a home and a workplace. If you give a point 1,000 jobs but no pops reference it as their jobId, zero people will ever travel there. To actually populate a workplace, add pops whose jobId is your point and whose residenceId says where those workers live — existing residential points from getDemandData(), or points you create. The counts then update automatically.
All methods return a result object with success and, on failure, an error string — they never throw.
Demand editing is blocked during active daily challenges (it would be cheating). Calls return { success: false } until the challenge is completed or submitted.
Add a Demand Point#
const api = window.SubwayBuilderAPI;
const result = api.demand.addDemandPoint({
location: [-73.985, 40.748], // [lng, lat]
});
console.log(result); // { success: true, pointId: 'mod-point-a1b2c3d4' }New points start empty — no residents, no jobs, no pops. Attach pops to give them demand.
You can pass your own id (it must not collide with an existing point):
api.demand.addDemandPoint({ id: 'my-mod-stadium', location: [-73.99, 40.75] });Special id prefixes: pops whose job point id starts with AIR_ get airport commute timing (flattened, mirrored across the day), and UNI_ gets student timing. Use these prefixes deliberately if you want that behavior.
Add Pops#
addPops takes an array and is all-or-nothing: every entry is validated before any is applied, so a typo can't half-apply a batch.
const home = api.demand.addDemandPoint({ location: [-73.95, 40.78] });
const work = api.demand.addDemandPoint({ location: [-74.01, 40.71] });
const result = api.demand.addPops([
{ residenceId: home.pointId, jobId: work.pointId, size: 400 },
{ residenceId: home.pointId, jobId: work.pointId, size: 200 },
]);
console.log(result); // { success: true, popIds: ['mod-pop-…', 'mod-pop-…'] }Each entry:
| Field | Required | Description |
|---|---|---|
residenceId | yes | Demand point the pop lives at. Must exist. |
jobId | yes | Demand point the pop works at. Must exist (may equal residenceId). |
size | no | People in this pop. Default 200, matching built-in city data. |
drivingSeconds | no | Drive time residence → job. Estimated when omitted. |
drivingDistance | no | Drive distance in meters. Estimated when omitted. |
New pops get commute departure times assigned through the same pipeline as city load, so they respect the current commute time ranges and participate in the next commute simulation like any built-in pop. They also persist in save files.
Driving estimates: built-in cities precompute real road-network driving stats offline. When you omit drivingSeconds/drivingDistance, the game estimates them from straight-line distance (×1.3 road circuity, ~40 km/h average speed). That's good enough for mode choice in most cases, but if your mod has better data — for example from `map.queryRoute` — pass it explicitly:
const route = await api.map.queryRoute(cityCode, homeLocation, workLocation);
api.demand.addPops([{
residenceId: home.pointId,
jobId: work.pointId,
drivingSeconds: route.drivingSeconds,
drivingDistance: route.drivingDistance,
}]);Update a Demand Point#
// Move a point
api.demand.updateDemandPoint('my-mod-stadium', { location: [-73.98, 40.755] });
// Override the displayed resident/job counts
api.demand.updateDemandPoint('my-mod-stadium', { residents: 0, jobs: 15000 });residents and jobs are normally maintained automatically as you add and remove pops — only override them if you deliberately want the bubble to diverge from the attached pops. Remember: these numbers are display values only. Overriding jobs to 15,000 does not create 15,000 workers — see the note in Data Model. Use `addPops` for that.
Seeing Your Changes on the Map#
Demand bubbles only render while a demand view is open (the Demand panel in the Analyze category, or a demand point / pop details panel). Two more things to know when checking your work visually:
- Homes vs Jobs view: in Homes view, bubbles are sized by
residents; in Jobs view, byjobs. A point with only residents draws nothing in Jobs view, and vice versa. - While a demand point is selected, every other point that has no simulated travelers to or from the selection is hidden. Freshly added pops haven't been through a commute simulation yet, so new points can be invisible while a selection is active — clear the selection to see them.
Update a Pop#
Change where an existing pop lives (residenceId), works (jobId), and/or how many people it represents (size) — any combination:
// A neighborhood shrinks: this pop now represents 80 people instead of 200
api.demand.updatePop(popId, { size: 80 });
// A company relocates: everyone who worked there now commutes to the new point
api.demand.updatePop(popId, { jobId: 'new-office-park' });
// Full move
api.demand.updatePop(popId, { residenceId: 'suburb-point', jobId: 'downtown-point', size: 350 });Endpoint links and resident/job counts adjust automatically, including size changes (the delta is applied to both endpoints). When the home or workplace changes, the pop's driving stats are re-estimated for the new trip and its stale route geometry and last-commute record are cleared — it gets fresh ones on the next commute simulation. New points must exist before you reference them.
Update Many Pops at Once#
Every updatePop call re-clones the demand maps and re-renders the demand layer, so calling it in a loop over hundreds of pops causes a noticeable hang. Use updatePops instead — it takes a list of updates (each entry is the updatePop shape plus an id), applies them all against a single clone, and commits once:
// Recession event: shrink every pop commuting downtown, in one commit
const layoffs = downtownPopIds.map((id) => ({ id, size: 100 }));
const result = api.demand.updatePops(layoffs);
console.log(result); // { success: true, updatedCount: 1500 }Entries apply in order, so a later entry sees earlier entries' changes (duplicate ids compound). The batch is all-or-nothing: if any entry is invalid, nothing is applied and error names the offending entry. One updatePops call fires onDemandChange once, no matter how many entries it has — thousands of updates complete in a few milliseconds.
Remove Pops#
const result = api.demand.removePops(['mod-pop-abc', 'mod-pop-def']);
console.log(result); // { success: true, removedCount: 2 }Unknown ids are skipped (they count toward neither failure nor removedCount). Endpoint links and counts update automatically.
Remove a Demand Point#
const result = api.demand.removeDemandPoint('my-mod-stadium');
console.log(result); // { success: true, removedPopCount: 3 }Removing a point cascades: every pop that lives or works at the point is removed with it, and each removed pop is unlinked from its other endpoint (counts decremented there too). removedPopCount reports how many pops went with the point.
Works With Existing Cities#
The ids of built-in demand points come from city data — grab them from `getDemandData()` and mix freely with your own:
api.hooks.onCityLoad((cityCode) => {
const demand = api.gameState.getDemandData();
// Double the commuters flowing into the ten biggest job centers
const topJobs = [...demand.points.values()]
.sort((a, b) => b.jobs - a.jobs)
.slice(0, 10);
for (const point of topJobs) {
const feeders = point.popIds
.map((id) => demand.popsMap.get(id))
.filter((pop) => pop && pop.jobId === point.id)
.slice(0, 5);
api.demand.addPops(
feeders.map((pop) => ({
residenceId: pop.residenceId,
jobId: pop.jobId,
size: pop.size,
}))
);
}
});Method Reference#
| Method | Returns |
|---|---|
addDemandPoint({ id?, location }) | { success, pointId?, error? } |
updateDemandPoint(id, { location?, residents?, jobs? }) | { success, error? } |
removeDemandPoint(id) | { success, removedPopCount?, error? } |
addPops([{ residenceId, jobId, size?, drivingSeconds?, drivingDistance? }]) | { success, popIds?, error? } |
updatePop(id, { residenceId?, jobId?, size? }) | { success, error? } |
updatePops([{ id, residenceId?, jobId?, size? }]) | { success, updatedCount?, error? } |
removePops(popIds) | { success, removedCount?, error? } |