rowTemplate = new Map(
daily_occ.filter(r => r.date === latestOpenDate).map(r => [r.key, r])
)
// La page Web de la Ville réutilise une seule ligne là où les données ouvertes
// en comptent deux : une ligne sommaire et une ligne de détail. Le moissonnage
// ne conserve que la première occurrence, donc une ligne doit servir aux deux.
cityKeyAliases = ({
singles_shelter_total: ["singles_shelter_total", "singles_shelter"],
allied_total: ["allied_total", "allied_summ"],
iso: ["iso", "iso_summ"]
})
// The City sometimes prints a section total with only an individuals count and
// leaves the rest of the row blank, as it did for Allied Services on Aug 6.
// Where every component row is present, rebuild the missing columns from them.
// The identity holds exactly on every date the City published both, so this
// reproduces the City's own arithmetic rather than inventing a figure.
//
// Done here and not in the scrape on purpose: audit_city_scrape.py reads
// city_daily_table.json as evidence of what the City actually published, and
// a derived value written into that archive would show up in the daily audit
// as though the City had published it.
cityTotalComponents = ({ allied_total: ["respites", "dropin"] })
function cityDerivedTotals(rows) {
const byKey = new Map(rows.map(r => [r.key, r]))
const out = new Map()
for (const [total, parts] of Object.entries(cityTotalComponents)) {
const t = byKey.get(total)
if (!t || t.city_occ !== null) continue // already published in full
const ps = parts.map(k => byKey.get(k))
if (ps.some(p => !p || p.city_occ === null || p.city_unocc === null
|| p.city_cap === null)) continue
const sum = f => ps.reduce((a, p) => a + p[f], 0)
const occ = sum("city_occ"), cap = sum("city_cap")
out.set(total, {
city_occ: occ,
city_unocc: sum("city_unocc"),
city_cap: cap,
city_rate: cap > 0 ? Math.round(1000 * occ / cap) / 10 : null
})
}
return out
}
function cityRowsFor(date) {
const rows = (city_table.entries ?? {})[date]
if (!rows) return []
const patch = cityDerivedTotals(rows)
return rows.flatMap(row => {
const keys = cityKeyAliases[row.key] ?? [row.key]
return keys.map(key => {
const t = rowTemplate.get(key)
return {
date,
key,
label: t ? t.label : row.label,
indent: t ? t.indent : 0,
is_total: t ? t.is_total : false,
col_type: t ? t.col_type : row.col_type,
ind: row.city_ind,
occ: (patch.get(row.key) ?? row).city_occ,
unocc: (patch.get(row.key) ?? row).city_unocc,
cap: (patch.get(row.key) ?? row).city_cap,
rate: (patch.get(row.key) ?? row).city_rate,
cap_funding: null, // la page Web de la Ville ne la publie pas
source: "city"
}
})
})
}
dayRows = {
const open = daily_occ.filter(r => r.date === selectedDate)
if (open.length) return open.map(r => ({...r, source: "open"}))
return cityRowsFor(selectedDate)
}
selectedSource = dayRows.length > 0 ? dayRows[0].source : "open"
function n(v) { return v !== null && v !== undefined ? v.toLocaleString("fr-CA") : "—" }
function pct(v) { return v !== null && v !== undefined ? v.toFixed(1) + " %" : "—" }
function offlineCell(r) {
if (r.cap === null || r.cap === undefined) return `<td></td>`
if (r.cap_funding === null || r.cap_funding === undefined) return `<td></td>`
const offline = r.cap_funding - r.cap
if (offline <= 0) return `<td class="bq-num">${offline.toLocaleString("fr-CA")}</td>`
return `<td class="bq-num bq-danger">${offline.toLocaleString("fr-CA")}</td>`
}
function tableRow(r) {
const pad = (r.indent ?? 0) * 16
const cls = r.is_total ? "bq-row-total" : "bq-row"
const bld = r.is_total ? " bq-bold" : ""
if (r.col_type === "summary") {
return `<tr class="${cls}">
<td style="padding-left:${pad}px" class="${bld}">${r.label}</td>
<td class="bq-num${bld}">${n(r.ind)}</td>
<td></td><td></td><td></td><td></td><td></td>
</tr>`
}
return `<tr class="${cls}">
<td style="padding-left:${pad}px" class="${bld}">${r.label}</td>
<td class="bq-num${bld}">${n(r.ind)}</td>
<td class="bq-num">${n(r.occ)}</td>
<td class="bq-num">${n(r.unocc)}</td>
<td class="bq-num">${n(r.cap)}</td>
${offlineCell(r)}
<td class="bq-num">${pct(r.rate)}</td>
</tr>`
}
function sectionHeader(label, col1, col2, col3, col4) {
return `<tr class="bq-th">
<td>${label}</td>
<td class="bq-num">Personnes</td>
<td class="bq-num">${col1}</td>
<td class="bq-num">${col2}</td>
<td class="bq-num">${col3}</td>
<td class="bq-num">${col4}</td>
<td class="bq-num">Taux d'occupation</td>
</tr>`
}
function summaryHeader() {
return `<tr class="bq-th">
<td>Résumé de l'occupation</td>
<td class="bq-num">Personnes</td>
<td colspan="5"></td>
</tr>`
}
dailyTable = {
const s = (key) => dayRows.find(r => r.key === key)
const allShelterInd = s("all_shelter") ? s("all_shelter").ind : null
const btEntry = bt_data && bt_data.entries ? bt_data.entries[selectedDate] : null
const btInd = btEntry ? btEntry.bridging_triage : null
const btStart = bt_data && bt_data.first_captured ? bt_data.first_captured : "la date de début de la collecte"
const totalInd = (allShelterInd !== null && btInd !== null) ? allShelterInd + btInd : allShelterInd
const totalAccomRow = `<tr class="bq-row-total bq-bold">
<td>Total des personnes hébergées †</td>
<td class="bq-num">${n(totalInd)}</td>
<td></td><td></td><td></td><td></td><td></td>
</tr>`
const bridgingRow = btInd !== null
? `<tr class="bq-row-alt">
<td style="padding-left:26px">Programmes de passerelle et de triage</td>
<td class="bq-num">${n(btInd)}</td>
<td></td><td></td><td></td><td></td><td></td>
</tr>`
: `<tr class="bq-row-na">
<td style="padding-left:26px">Programmes de passerelle et de triage</td>
<td class="bq-num" style="font-size:0.85em">Non disponible pour cette date</td>
<td></td><td></td><td></td><td></td><td></td>
</tr>`
const summaryKeys = ["all_shelter","room_based","singles_sector","singles_shelter","allied_summ","temp_summ","iso_summ"]
const roomKeys = ["fam_total","fam_emerg","fam_trans_r","fam_hotel","sng_hotel"]
const bedKeys = ["singles_total","emerg_total","mix_emerg","men_emerg","wom_emerg","yth_emerg",
"trans_total","mix_trans","fam_trans_b","men_trans","wom_trans","yth_trans"]
const alliedKeys = ["allied_total","respites","dropin"]
return html`
<div style="overflow-x:auto;margin:0 0 1rem">
<table class="bq-table">
${summaryHeader()}
${totalAccomRow}
${bridgingRow}
${summaryKeys.map(k => s(k) ? tableRow(s(k)) : "").join("")}
${sectionHeader("Programmes de refuge, base chambre","Chambres occupées","Chambres inoccupées","Capacité réelle (chambres)","Chambres hors service")}
${roomKeys.map(k => s(k) ? tableRow(s(k)) : "").join("")}
${sectionHeader("Programmes de refuge, base lit","Lits occupés","Lits inoccupés","Capacité réelle (lits)","Lits hors service")}
${bedKeys.map(k => s(k) ? tableRow(s(k)) : "").join("")}
${sectionHeader("Services complémentaires","Espaces occupés","Espaces inoccupés","Capacité réelle (espaces)","Espaces hors service")}
${alliedKeys.map(k => s(k) ? tableRow(s(k)) : "").join("")}
<tr class="bq-th">
<td>Programmes temporaires, base lit/espace</td>
<td class="bq-num">Personnes</td>
<td class="bq-num">Lits/espaces occupés</td>
<td class="bq-num">Inoccupés</td>
<td class="bq-num">Capacité réelle</td>
<td class="bq-num">Lits/espaces hors service</td>
<td class="bq-num">Taux d'occupation</td>
</tr>
${s("temp_resp") ? tableRow(s("temp_resp")) : ""}
${sectionHeader("Programmes temporaires, base chambre/unité","Chambres occupées","Chambres inoccupées","Capacité réelle (chambres)","Chambres hors service")}
${s("hotels") ? tableRow(s("hotels")) : ""}
${sectionHeader("Programmes d'isolement/rétablissement temporaires","Chambres occupées","Chambres inoccupées","Capacité réelle (chambres)","Chambres hors service")}
${s("iso") ? tableRow(s("iso")) : ""}
</table>
<p class="bq-footnote">
† Total des personnes hébergées = Tous les programmes de refuge + Programmes de
passerelle et de triage. Les chiffres des Programmes de passerelle et de triage ne
sont pas publiés dans les données ouvertes de la Ville de Toronto (CKAN). BonQuery
les collecte quotidiennement depuis la page du recensement des refuges de la Ville
lorsque la Ville publie le tableau ; les données sont disponibles depuis ${btStart}.
Les jours où la Ville n'a pas encore publié (fins de semaine, jours fériés ou délais
de publication), la ligne Passerelle et triage affichera
« Non disponible pour cette date » et le Total reflètera uniquement Tous les
programmes de refuge.
</p>
</div>`
}