BonQuery
  • Home
  • Dashboards
  • Left in the Cold
  • Data & Methods
    • About the Data
    • Daily Data Sources
    • Central Intake Validation
  • About
  • FR

Calls Asking for Shelter

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-axis
xDomain = cityRange.map(d => d.month_label)

// Year-start labels only — one tick mark per calendar year, no rotation
xYearLabels = (() => {
  const seen = new Set()
  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 chosen
  const rangeSpan = html`<span style="display:none;gap:12px;align-items:flex-end">
    ${startInput}${endInput}
  </span>`

  // Assemble the full control bar
  const 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 object
  const 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 change
  const fire = () => {
    rangeSpan.style.display =
      modeInput.value === "Pick dates" ? "inline-flex" : "none"
    bar.value = getValue()
    bar.dispatchEvent(new Event("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 cityRange

  const {startKey, endKey} = controls
  if (startKey > endKey) return []   // guard: start after end

  return 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 view
activeDomain = activeRange.map(d => d.month_label)

// Year-start ticks for the active range — first occurrence of each year
activeYearTicks = (() => {
  const seen = new Set(), 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)
    return html`<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)
    return html`<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)
    return html`<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; bold
      const 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}`
      return html`<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.

Contains information licensed under the Open Government Licence - Toronto.

Back to top
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.
Assaulted Women's HelplineFree and confidential, anywhere in Ontario, 24/7 1-866-863-0511 Young people: Kids Help PhoneAges 5 to 29, confidential, 24/7. Call, or text CONNECT to 686868. 1-800-668-6868 Thinking about suicide, or worried about someone?Call or text 988 anywhere in Canada, 24/7. 988 Toronto emergency shelter line416-338-4766 Toronto shelter line, toll-free1-877-338-3398 Toronto mental-health crisis support211 Toronto street outreach311 Toronto 311 from outside city limits416-392-2489 City services TTY relay711 Immediate danger in Canada911 Toronto warming-centre status
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.
Ligne d'aide aux femmes victimes de violenceGratuite et confidentielle, partout en Ontario, 24 h/24 et 7 j/7 1-866-863-0511 Jeunes : Jeunesse, J'écouteDe 5 à 29 ans, service confidentiel 24 h/24 et 7 j/7. Appelez ou textez le 686868. 1-800-668-6868 Vous pensez au suicide ou vous vous inquiétez pour quelqu'un?Appelez ou textez le 988 partout au Canada, 24 h/24 et 7 j/7. 988 Ligne des refuges d'urgence de Toronto416-338-4766 Ligne des refuges de Toronto, sans frais1-877-338-3398 Soutien en cas de crise de santé mentale à Toronto211 Intervention de rue à Toronto311 Toronto 311 depuis l'extérieur de la ville416-392-2489 Service de relais ATS de la Ville711 Danger immédiat au Canada911 État des centres de réchauffement de Toronto
 

Independent public-interest analysis of Toronto’s shelter system.
© 2026 Miriam Marling · BonQuery