What happened when people called Toronto Central Intake
See what happened when people called Toronto Central Intake because they needed somewhere safe to stay, including referrals and people still without a place at 4 a.m.
Author
Miriam Marling
Central Intake is Toronto’s 24-hour shelter phone line. People call when they need a place to stay. Staff may give a caller a shelter location. Street outreach workers can also help people get a space.
We rebuild three charts from the City’s page. We leave out the City’s fourth chart because its public data file is missing Bridging & Triage Programs. That makes the total too low. See daily shelter use on Beds and Spaces Today and see the missing numbers on the Central Intake data check.
Choose Match the City’s chart to see April 2024 onward. Choose Pick dates to go back to November 2020. The technical checkbox adds a range around each monthly average.
data =FileAttachment("../data/central_intake.json").json()
MONTH_ABB = ["","Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]// City replication range: April 2024 onward (matches the City's published table)cityRange = data.monthly.filter(d => d.year>2024|| (d.year===2024&& d.month>=4)).sort((a, b) => a.year*100+ a.month- (b.year*100+ b.month))// Ordered domain for city replication x-axisxDomain = cityRange.map(d => d.month_label)// Year-start labels only — one tick mark per calendar year, no rotationxYearLabels = (() => {const seen =newSet()const first = [] xDomain.forEach(l => {const yr = l.slice(-4)if (!seen.has(yr)) { seen.add(yr); first.push(l) } })return first // ["Apr 2024", "Jan 2025", "Jan 2026"]})()xTickFormat = l => xYearLabels.includes(l) ? l.slice(-4) :""// All available months for the range-selector dropdowns (full dataset)allMonthOpts = data.monthly.map(d => ({key:`${d.year}-${String(d.month).padStart(2,"0")}`,label: d.month_label })).sort((a, b) => a.key.localeCompare(b.key))
// Unified control bar — radio, range dropdowns (conditional), SE checkbox.// All state is exposed as controls.viewMode / .startKey / .endKey / .showSE.viewof controls = {const jan2025 = allMonthOpts.find(o => o.key==="2025-01") ?? allMonthOpts[0]const last = allMonthOpts.at(-1)// --- individual inputs ---const modeInput = Inputs.radio( ["Match the City's chart","Pick dates"], {value:"Match the City's chart"} )const startInput = Inputs.select(allMonthOpts, {label:"From",format: o => o.label,value: jan2025 })const endInput = Inputs.select(allMonthOpts, {label:"To",format: o => o.label,value: last })const seInput = Inputs.checkbox( ["Show technical range lines"], {value: []} // unchecked by default )// Range dropdowns sit in a flex span; hidden until Custom range is chosenconst rangeSpan =html`<span style="display:none;gap:12px;align-items:flex-end">${startInput}${endInput} </span>`// Assemble the full control barconst bar =html`<div class="bq-control-bar">${modeInput}${rangeSpan} <div>${seInput} <small class="bq-hint"> The lines show ±2 standard errors around each monthly average. </small> </div> </div>`// Compute current state as a plain objectconst getValue = () => ({viewMode: modeInput.value,startKey: startInput.value.key,endKey: endInput.value.key,showSE: seInput.value.length>0 }) bar.value=getValue()// Re-evaluate on any input change; toggle range visibility on mode changeconst fire = () => { rangeSpan.style.display= modeInput.value==="Pick dates"?"inline-flex":"none" bar.value=getValue() bar.dispatchEvent(newEvent("input", {bubbles:true})) } modeInput.addEventListener("input", fire) startInput.addEventListener("input", fire) endInput.addEventListener("input", fire) seInput.addEventListener("input", fire)return bar}
activeRange = {if (controls.viewMode==="Match the City's chart") return cityRangeconst {startKey, endKey} = controlsif (startKey > endKey) return [] // guard: start after endreturn data.monthly.filter(d => {const k =`${d.year}-${String(d.month).padStart(2,"0")}`return k >= startKey && k <= endKey }).sort((a, b) => a.year*100+ a.month- (b.year*100+ b.month))}// Ordered x domain for the current viewactiveDomain = activeRange.map(d => d.month_label)// Year-start ticks for the active range — first occurrence of each yearactiveYearTicks = (() => {const seen =newSet(), first = [] activeDomain.forEach(l => {const yr = l.slice(-4)if (!seen.has(yr)) { seen.add(yr); first.push(l) } })return first})()// x-axis: year-only ticks in both modes, no rotation.// Avoids label overlap regardless of range width or screen size.activeX = controls.viewMode==="Match the City's chart"? {domain: xDomain,ticks: xYearLabels,tickSize:10,tickFormat: xTickFormat,tickRotate:0,label:null }: {domain: activeDomain,ticks: activeYearTicks,tickSize:10,tickFormat: l => l.slice(-4),// "Jan 2025" -> "2025"tickRotate:0,label:null }activeMarginBottom =40
Tap or hover any bar to see the month, year, and average value.
// Chart 1 — Calls referred to a shelter space (wrap-up Code 1A){if (activeRange.length===0)returnhtml`<p class="bq-no-data"> ⚠ "From" month must be before "To" month.</p>`return Plot.plot({title:"Calls given a shelter referral", width,marginTop:40,marginBottom: activeMarginBottom,marginLeft:55,x: activeX,y: {grid:true,label:"Average each day"},marks: [ Plot.barY(activeRange, {x:"month_label",y:"referred_mean",fill:"#5BA75B",tip:true }),// Capless ±2 SE error bars — only rendered when SE toggle is on...(controls.showSE? [Plot.ruleX(activeRange, {x:"month_label",y1: d => d.referred_se!=null? d.referred_mean-2* d.referred_se:null,y2: d => d.referred_se!=null? d.referred_mean+2* d.referred_se:null,stroke:getComputedStyle(document.body).getPropertyValue("--bq-chart-stroke"),strokeWidth:1.5 })] : []) ] })}
// Chart 2 — Unmatched individual callers (Service Queue data){if (activeRange.length===0)returnhtml`<p class="bq-no-data"> ⚠ "From" month must be before "To" month.</p>`return Plot.plot({title:"People or couples still without a place at 4 a.m.", width,marginTop:40,marginBottom: activeMarginBottom,marginLeft:55,x: activeX,y: {grid:true,label:"Average each day"},marks: [ Plot.barY(activeRange, {x:"month_label",y:"unmatched_mean",fill:"#FF2D55",tip:true }),...(controls.showSE? [Plot.ruleX(activeRange, {x:"month_label",y1: d => d.unmatched_se!=null? d.unmatched_mean-2* d.unmatched_se:null,y2: d => d.unmatched_se!=null? d.unmatched_mean+2* d.unmatched_se:null,stroke:getComputedStyle(document.body).getPropertyValue("--bq-chart-stroke"),strokeWidth:1.5 })] : []) ] })}
// Chart 3 — Total calls handled (all wrap-up codes combined){if (activeRange.length===0)returnhtml`<p class="bq-no-data"> ⚠ "From" month must be before "To" month.</p>`return Plot.plot({title:"Calls handled by staff", width,marginTop:40,marginBottom: activeMarginBottom,marginLeft:55,x: activeX,y: {grid:true,label:"Average each day"},marks: [ Plot.barY(activeRange, {x:"month_label",y:"handled_mean",fill:"#4A90D9",tip:true }),...(controls.showSE? [Plot.ruleX(activeRange, {x:"month_label",y1: d => d.handled_se!=null? d.handled_mean-2* d.handled_se:null,y2: d => d.handled_se!=null? d.handled_mean+2* d.handled_se:null,stroke:getComputedStyle(document.body).getPropertyValue("--bq-chart-stroke"),strokeWidth:1.5 })] : []) ] })}
This table shows the same monthly averages as the City’s page. BonQuery calculates them from the City’s daily public data.
thStyle = (extra ="") =>`border-bottom:2px solid var(--bq-chart-grid);padding:6px 10px;text-align:center;`+`background:var(--bq-bg-accent);${extra}`html`<table style="width:100%;border-collapse:collapse;font-size:0.9em"> <thead> <tr> <th style="${thStyle()}">Year</th> <th style="${thStyle()}">Month</th> <th colspan="3" style="${thStyle()}">Average daily</th> </tr> <tr> <th style="${thStyle()}"></th> <th style="${thStyle()}"></th> <th style="${thStyle()}">Calls given a shelter referral [1]</th> <th style="${thStyle()}">People or couples still without a place [2]</th> <th style="${thStyle()}">Calls handled by staff [3]</th> </tr> </thead> <tbody>${cityRange.map((d, i) => {// Jan rows mark a new year — same shade as headers; boldconst bg = d.month===1?"var(--bq-bg-accent)": (i %2===0?"var(--bq-bg)":"var(--bq-bg-alt)")const fw = d.month===1?"font-weight:600;":""const td = () =>`padding:5px 10px;border-bottom:1px solid var(--bq-border);`+`text-align:center;background:${bg};${fw}`returnhtml`<tr> <td style="${td()}">${d.year}</td> <td style="${td()}">${MONTH_ABB[d.month]}</td> <td style="${td()}">${d.referred_mean!=null? d.referred_mean.toFixed(1) :"—"} </td> <td style="${td()}">${d.unmatched_mean!=null? d.unmatched_mean.toFixed(1) :"—"} </td> <td style="${td()}">${d.handled_mean!=null?Math.round(d.handled_mean).toLocaleString() :"—"} </td> </tr>` })} </tbody></table>`
[1] Calls marked as referred to a sleeping or resting space.
[2] People or couples still without a shelter place at 4 a.m. A couple counts once.
[3] All calls marked as handled by staff.
Notes on the Data
The City publishes two public files for these charts. One counts phone calls. The other counts people or couples still without a shelter place at 4 a.m. Those are different things, so the numbers should not be added together.
In the call file, staff choose a code for what happened on each call. A referral code means staff gave the caller a sleeping or resting place to try. It does not prove that the person got into that space.
Data source
All numbers come from the City of Toronto’s Central Intake Calls public data. The City updates it monthly. BonQuery calculates each monthly average from the daily numbers.
Need a safe place or support now?These contacts serve Toronto unless another area is named. BonQuery is an independent data project, not a service provider.
Besoin d'un endroit sûr ou de soutien maintenant?Ces services desservent Toronto, sauf si une autre région est indiquée. BonQuery est un projet de données indépendant, pas un fournisseur de services.