diff --git a/README.md b/README.md
index 667d48af..0ac3bf78 100644
--- a/README.md
+++ b/README.md
@@ -53,3 +53,19 @@ npx simple-git-hooks
```
If linting fails, the commit will be blocked until issues are resolved.
+
+## Toronto data conversion
+
+The Toronto Sankey data is generated via `data/toronto/scripts/convert_toronto_sankey.py`. The
+script supports both the original dataset (`legacy`) and the new 2024 operating budget dataset:
+
+```bash
+# Legacy dataset (writes to data/toronto)
+python3 data/toronto/scripts/convert_toronto_sankey.py --dataset legacy
+
+# 2024 Operating Budget dataset (writes to data/toronto-operating)
+python3 data/toronto/scripts/convert_toronto_sankey.py --dataset operating
+```
+
+Use `--output-dir` to override the destination or pass custom spreadsheet paths if the files live
+elsewhere.
diff --git a/data/british-columbia/sankey.json b/data/british-columbia/sankey.json
index cab05294..b0bee43f 100644
--- a/data/british-columbia/sankey.json
+++ b/data/british-columbia/sankey.json
@@ -38,7 +38,7 @@
},
{
"name": "Legislative Assembly \u2192 Adjustment of Prior Year Accrual",
- "amount": -3e-05
+ "amount": -3e-5
}
]
},
@@ -112,7 +112,7 @@
},
{
"name": "Office of the Premier \u2192 Local Government (Transferred from Ministry of Municipal Affairs)",
- "amount": 1.62e-07
+ "amount": 1.62e-7
}
]
},
@@ -300,7 +300,7 @@
},
{
"name": "Ministry of Citizens Services \u2192 Services to Citizens and Businesses - BC Registry Services",
- "amount": 1e-06
+ "amount": 1e-6
},
{
"name": "Ministry of Citizens Services \u2192 Office of the Chief Information Officer",
@@ -487,7 +487,7 @@
},
{
"name": "Ministry of Energy and Climate Solutions \u2192 Adjustment of Prior Year Accrual",
- "amount": -1.1466e-05
+ "amount": -1.1466e-5
}
]
},
@@ -621,7 +621,7 @@
},
{
"name": "Ministry of Finance \u2192 Executive and Support Services(Transferred from Jobs, Economic Development and Innovation)",
- "amount": 4.3325e-05
+ "amount": 4.3325e-5
},
{
"name": "Ministry of Finance \u2192 Executive and Support Services(Transferred from Mental Health and Addictions)",
@@ -848,7 +848,7 @@
},
{
"name": "Ministry of Housing and Municipal Affairs \u2192 Adjustment of Prior Year Accrual",
- "amount": -5.2397e-05
+ "amount": -5.2397e-5
}
]
},
@@ -992,7 +992,7 @@
},
{
"name": "Ministry of Labour \u2192 WorkSafeBC Funded Services",
- "amount": 1e-06
+ "amount": 1e-6
},
{
"name": "Ministry of Labour \u2192 Labour Policy and Legislation",
@@ -1008,7 +1008,7 @@
},
{
"name": "Ministry of Labour \u2192 Adjustment of Prior Year Accrual",
- "amount": -1.0496e-05
+ "amount": -1.0496e-5
}
]
},
@@ -1189,7 +1189,7 @@
},
{
"name": "Ministry of Public Safety and Solicitor General \u2192 Liquor Regulation - Liquor Regulation",
- "amount": 1e-06
+ "amount": 1e-6
},
{
"name": "Ministry of Public Safety and Solicitor General \u2192 Cannabis Regulation - Cannabis Regulation",
@@ -1201,7 +1201,7 @@
},
{
"name": "Ministry of Public Safety and Solicitor General \u2192 Gaming Policy and Enforcement - Distribution of Gaming Proceeds",
- "amount": 1e-06
+ "amount": 1e-6
},
{
"name": "Ministry of Public Safety and Solicitor General \u2192 Cannabis, Consumer Protection and Corporate Policy - Cannabis, Consumer Protection and Corporate Policy",
@@ -1412,7 +1412,7 @@
},
{
"name": "Ministry of Transportation and Transit \u2192 Commercial Transportation Regulation - Container Trucking Commissioner",
- "amount": 5.5905e-05
+ "amount": 5.5905e-5
},
{
"name": "Ministry of Transportation and Transit \u2192 Commercial Transportation Regulation - Passenger Transportation Branch",
@@ -1506,11 +1506,11 @@
},
{
"name": "Management of Public Funds and Debt \u2192 Adjustment of Prior Year Accrual - Adjustment of Prior Year Accrual",
- "amount": -7.5e-07
+ "amount": -7.5e-7
}
]
},
- {
+ {
"name": "Other Appropriations",
"children": [
{
@@ -1609,7 +1609,7 @@
},
{
"name": "Other Appropriations \u2192 Adjustment of Prior Year Accrual",
- "amount": -0.304940840
+ "amount": -0.30494084
}
]
}
@@ -1769,4 +1769,4 @@
}
]
}
-}
\ No newline at end of file
+}
diff --git a/data/toronto-operating/sankey.json b/data/toronto-operating/sankey.json
new file mode 100644
index 00000000..7da4e374
--- /dev/null
+++ b/data/toronto-operating/sankey.json
@@ -0,0 +1,728 @@
+{
+ "total": 11.67877,
+ "spending": 15.704151000000001,
+ "revenue": 11.67877,
+ "spending_data": {
+ "name": "Spending",
+ "children": [
+ {
+ "name": "Community and Social Services",
+ "children": [
+ {
+ "name": "Community and Social Services \u2192 Toronto Employment & Social Services",
+ "amount": 1.2445709999999999
+ },
+ {
+ "name": "Community and Social Services \u2192 Children's Services",
+ "amount": 1.066048
+ },
+ {
+ "name": "Community and Social Services \u2192 Toronto Shelter and Support Services",
+ "amount": 0.843995
+ },
+ {
+ "name": "Community and Social Services \u2192 Fire Services",
+ "amount": 0.579616
+ },
+ {
+ "name": "Community and Social Services \u2192 Parks, Forestry & Recreation",
+ "amount": 0.5377029999999999
+ },
+ {
+ "name": "Community and Social Services \u2192 Seniors Services and Long-Term Care",
+ "amount": 0.380527
+ },
+ {
+ "name": "Community and Social Services \u2192 Toronto Paramedic Services",
+ "amount": 0.325914
+ },
+ {
+ "name": "Community and Social Services \u2192 Social Development, Finance & Administration",
+ "amount": 0.12800999999999998
+ },
+ {
+ "name": "Community and Social Services \u2192 Economic Development and Culture",
+ "amount": 0.10205299999999999
+ },
+ {
+ "name": "Community and Social Services \u2192 Court Services",
+ "amount": 0.037442
+ }
+ ]
+ },
+ {
+ "name": "Agencies",
+ "children": [
+ {
+ "name": "Agencies \u2192 Toronto Transit Commission - Wheel Trans",
+ "amount": 2.3824810000000003
+ },
+ {
+ "name": "Agencies \u2192 Toronto Police Service Board",
+ "amount": 1.3911900000000001
+ },
+ {
+ "name": "Agencies \u2192 Exhibition Place",
+ "amount": 0.254511
+ },
+ {
+ "name": "Agencies \u2192 Toronto Public Library",
+ "amount": 0.248052
+ },
+ {
+ "name": "Agencies \u2192 Toronto Police Service",
+ "amount": 0.167917
+ },
+ {
+ "name": "Agencies \u2192 Heritage Toronto",
+ "amount": 0.067217
+ },
+ {
+ "name": "Agencies \u2192 Sankofa Square",
+ "amount": 0.06547
+ },
+ {
+ "name": "Agencies \u2192 Toronto Zoo",
+ "amount": 0.03982
+ },
+ {
+ "name": "Agencies \u2192 Toronto & Region Conservation Authority",
+ "amount": 0.018463
+ },
+ {
+ "name": "Agencies \u2192 Toronto Public Health",
+ "amount": 0.013326000000000001
+ },
+ {
+ "name": "Agencies \u2192 Toronto Transit Commission - Conventional",
+ "amount": 0.011594
+ },
+ {
+ "name": "Agencies \u2192 CreateTO",
+ "amount": 0.0038309999999999998
+ },
+ {
+ "name": "Agencies \u2192 To Live",
+ "amount": 0.001335
+ }
+ ]
+ },
+ {
+ "name": "Corporate Accounts",
+ "children": [
+ {
+ "name": "Corporate Accounts \u2192 Debt Charges",
+ "amount": 0.89362
+ },
+ {
+ "name": "Corporate Accounts \u2192 Capital from Current",
+ "amount": 0.286742
+ },
+ {
+ "name": "Corporate Accounts \u2192 Technology Sustainment",
+ "amount": 0.021297
+ },
+ {
+ "name": "Corporate Accounts \u2192 Association of Community Centres",
+ "amount": 0.011885999999999999
+ },
+ {
+ "name": "Corporate Accounts \u2192 Arena Boards of Management",
+ "amount": 0.011106999999999999
+ }
+ ]
+ },
+ {
+ "name": "Non-Levy Operation",
+ "children": [
+ {
+ "name": "Non-Levy Operation \u2192 Toronto Water",
+ "amount": 0.503995
+ },
+ {
+ "name": "Non-Levy Operation \u2192 Solid Waste Management Services",
+ "amount": 0.403488
+ },
+ {
+ "name": "Non-Levy Operation \u2192 Toronto Parking Authority",
+ "amount": 0.130011
+ }
+ ]
+ },
+ {
+ "name": "Development & Growth Services",
+ "children": [
+ {
+ "name": "Development & Growth Services \u2192 Housing Secretariat",
+ "amount": 0.7475109999999999
+ },
+ {
+ "name": "Development & Growth Services \u2192 City Planning",
+ "amount": 0.07324599999999999
+ },
+ {
+ "name": "Development & Growth Services \u2192 Toronto Building",
+ "amount": 0.067258
+ },
+ {
+ "name": "Development & Growth Services \u2192 Development Review",
+ "amount": 0.00828
+ }
+ ]
+ },
+ {
+ "name": "Infrastructure Services",
+ "children": [
+ {
+ "name": "Infrastructure Services \u2192 Transportation Services",
+ "amount": 0.500696
+ },
+ {
+ "name": "Infrastructure Services \u2192 Engineering & Construction Services",
+ "amount": 0.085084
+ },
+ {
+ "name": "Infrastructure Services \u2192 Municipal Licensing & Standards",
+ "amount": 0.076939
+ },
+ {
+ "name": "Infrastructure Services \u2192 Policy, Planning, Finance & Administration",
+ "amount": 0.022785
+ },
+ {
+ "name": "Infrastructure Services \u2192 Transit Expansion",
+ "amount": 0.007979
+ },
+ {
+ "name": "Infrastructure Services \u2192 Toronto Emergency Management",
+ "amount": 0.006112
+ }
+ ]
+ },
+ {
+ "name": "Non-Program Expenditures",
+ "children": [
+ {
+ "name": "Non-Program Expenditures \u2192 Programs Funded from Reserve Funds",
+ "amount": 0.16631100000000001
+ },
+ {
+ "name": "Non-Program Expenditures \u2192 Other Corporate Expenditures",
+ "amount": 0.117756
+ },
+ {
+ "name": "Non-Program Expenditures \u2192 Funding of Employee Related Liabilities",
+ "amount": 0.083065
+ },
+ {
+ "name": "Non-Program Expenditures \u2192 Solid Waste Management Services Rebate",
+ "amount": 0.075371
+ },
+ {
+ "name": "Non-Program Expenditures \u2192 Parking Tag Enforcement & Operations Exp",
+ "amount": 0.07024899999999999
+ },
+ {
+ "name": "Non-Program Expenditures \u2192 Insurance Contributions",
+ "amount": 0.052411
+ },
+ {
+ "name": "Non-Program Expenditures \u2192 Assessment Function (MPAC)",
+ "amount": 0.047291
+ },
+ {
+ "name": "Non-Program Expenditures \u2192 Tax Increment Equivalent Grants (TIEG)",
+ "amount": 0.040055
+ },
+ {
+ "name": "Non-Program Expenditures \u2192 Tax Deficiencies / Write Offs",
+ "amount": 0.0312
+ },
+ {
+ "name": "Non-Program Expenditures \u2192 Tax Increment Funding (TIF)",
+ "amount": 0.007231
+ },
+ {
+ "name": "Non-Program Expenditures \u2192 Heritage Property Taxes Rebate",
+ "amount": 0.0006360000000000001
+ }
+ ]
+ },
+ {
+ "name": "Corporate Services",
+ "children": [
+ {
+ "name": "Corporate Services \u2192 Corporate Real Estate Management",
+ "amount": 0.22578299999999998
+ },
+ {
+ "name": "Corporate Services \u2192 Technology Services",
+ "amount": 0.174141
+ },
+ {
+ "name": "Corporate Services \u2192 Fleet Services",
+ "amount": 0.079066
+ },
+ {
+ "name": "Corporate Services \u2192 Office of the Chief Information Security Officer",
+ "amount": 0.031236999999999997
+ },
+ {
+ "name": "Corporate Services \u2192 Customer Experience",
+ "amount": 0.024392
+ },
+ {
+ "name": "Corporate Services \u2192 Environment & Climate",
+ "amount": 0.019162
+ }
+ ]
+ },
+ {
+ "name": "City Building Fund (CBF)",
+ "children": [
+ {
+ "name": "City Building Fund (CBF) \u2192 City Building Fund (CBF)",
+ "amount": 0.314096
+ }
+ ]
+ },
+ {
+ "name": "Other City Programs",
+ "children": [
+ {
+ "name": "Other City Programs \u2192 Legal Services",
+ "amount": 0.067054
+ },
+ {
+ "name": "Other City Programs \u2192 City Clerk's Office",
+ "amount": 0.053473999999999994
+ },
+ {
+ "name": "Other City Programs \u2192 Mayor's Office",
+ "amount": 0.003055
+ }
+ ]
+ },
+ {
+ "name": "Finance and Treasury Services",
+ "children": [
+ {
+ "name": "Finance and Treasury Services \u2192 Financial Operations & Control",
+ "amount": 0.067048
+ },
+ {
+ "name": "Finance and Treasury Services \u2192 Office of the Chief Financial Officer and Treasurer",
+ "amount": 0.045003
+ }
+ ]
+ },
+ {
+ "name": "City Manager",
+ "children": [
+ {
+ "name": "City Manager \u2192 City Manager's Office (Excluding FIFA)",
+ "amount": 0.08591700000000001
+ }
+ ]
+ },
+ {
+ "name": "Special Levy for Scarborough Subway",
+ "children": [
+ {
+ "name": "Special Levy for Scarborough Subway \u2192 Special Levy for Scarborough Subway",
+ "amount": 0.040699
+ }
+ ]
+ },
+ {
+ "name": "Accountability Offices",
+ "children": [
+ {
+ "name": "Accountability Offices \u2192 Auditor General's Office",
+ "amount": 0.007781
+ },
+ {
+ "name": "Accountability Offices \u2192 Office of the Ombudsman",
+ "amount": 0.0038399999999999997
+ },
+ {
+ "name": "Accountability Offices \u2192 Office of the Lobbyist Registrar",
+ "amount": 0.0009310000000000001
+ },
+ {
+ "name": "Accountability Offices \u2192 Integrity Commissioner's Office",
+ "amount": 0.0007740000000000001
+ }
+ ]
+ }
+ ]
+ },
+ "revenue_data": {
+ "name": "Revenue",
+ "children": [
+ {
+ "name": "Community and Social Services",
+ "children": [
+ {
+ "name": "Community and Social Services \u2192 Toronto Employment & Social Services",
+ "amount": 1.162375
+ },
+ {
+ "name": "Community and Social Services \u2192 Children's Services",
+ "amount": 0.9726699999999999
+ },
+ {
+ "name": "Community and Social Services \u2192 Toronto Shelter and Support Services",
+ "amount": 0.614602
+ },
+ {
+ "name": "Community and Social Services \u2192 Seniors Services and Long-Term Care",
+ "amount": 0.31587
+ },
+ {
+ "name": "Community and Social Services \u2192 Toronto Paramedic Services",
+ "amount": 0.213976
+ },
+ {
+ "name": "Community and Social Services \u2192 Parks, Forestry & Recreation",
+ "amount": 0.17311600000000002
+ },
+ {
+ "name": "Community and Social Services \u2192 Court Services",
+ "amount": 0.108185
+ },
+ {
+ "name": "Community and Social Services \u2192 Fire Services",
+ "amount": 0.029585999999999998
+ },
+ {
+ "name": "Community and Social Services \u2192 Economic Development and Culture",
+ "amount": 0.018039000000000003
+ },
+ {
+ "name": "Community and Social Services \u2192 Social Development, Finance & Administration",
+ "amount": 0.01718
+ }
+ ]
+ },
+ {
+ "name": "Non-Program Revenues",
+ "children": [
+ {
+ "name": "Non-Program Revenues \u2192 Municipal Land Transfer Tax",
+ "amount": 0.879752
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Interest/Investment Earnings",
+ "amount": 0.39
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Other Corporate Revenues",
+ "amount": 0.232131
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Parking Tag Enforcement & Operations Rev",
+ "amount": 0.134032
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Municipal Accommodation Tax (MAT)",
+ "amount": 0.108
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Payments in Lieu of Taxes",
+ "amount": 0.106597
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Provincial Revenue",
+ "amount": 0.0916
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Dividend Income",
+ "amount": 0.08394
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Supplementary Taxes",
+ "amount": 0.06
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Vacant Home Tax",
+ "amount": 0.055
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Tax Penalty Revenue",
+ "amount": 0.0525
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Casino Woodbine Revenues",
+ "amount": 0.0272
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Parking Authority Revenues",
+ "amount": 0.025678
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Administrative Support Recoveries - Water",
+ "amount": 0.018973
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Administrative Support Recoveries - Health & EMS",
+ "amount": 0.011855000000000001
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Third Party Sign Tax",
+ "amount": 0.010352
+ },
+ {
+ "name": "Non-Program Revenues \u2192 Other Tax Revenues",
+ "amount": 0.010043
+ }
+ ]
+ },
+ {
+ "name": "Non-Levy Operation",
+ "children": [
+ {
+ "name": "Non-Levy Operation \u2192 Toronto Water",
+ "amount": 1.5532260000000002
+ },
+ {
+ "name": "Non-Levy Operation \u2192 Solid Waste Management Services",
+ "amount": 0.42586900000000005
+ },
+ {
+ "name": "Non-Levy Operation \u2192 Toronto Parking Authority",
+ "amount": 0.17113
+ }
+ ]
+ },
+ {
+ "name": "Agencies",
+ "children": [
+ {
+ "name": "Agencies \u2192 Toronto Transit Commission - Conventional",
+ "amount": 1.2761300000000002
+ },
+ {
+ "name": "Agencies \u2192 Toronto Police Service",
+ "amount": 0.19488999999999998
+ },
+ {
+ "name": "Agencies \u2192 Toronto Public Health",
+ "amount": 0.180506
+ },
+ {
+ "name": "Agencies \u2192 Exhibition Place",
+ "amount": 0.068217
+ },
+ {
+ "name": "Agencies \u2192 Toronto Zoo",
+ "amount": 0.051572
+ },
+ {
+ "name": "Agencies \u2192 To Live",
+ "amount": 0.03359
+ },
+ {
+ "name": "Agencies \u2192 Toronto Public Library",
+ "amount": 0.020797999999999997
+ },
+ {
+ "name": "Agencies \u2192 CreateTO",
+ "amount": 0.018463
+ },
+ {
+ "name": "Agencies \u2192 Toronto Transit Commission - Wheel Trans",
+ "amount": 0.008468
+ },
+ {
+ "name": "Agencies \u2192 Toronto & Region Conservation Authority",
+ "amount": 0.005743000000000001
+ },
+ {
+ "name": "Agencies \u2192 Sankofa Square",
+ "amount": 0.0023290000000000003
+ },
+ {
+ "name": "Agencies \u2192 Toronto Police Service Board",
+ "amount": 0.000853
+ },
+ {
+ "name": "Agencies \u2192 Heritage Toronto",
+ "amount": 0.000696
+ }
+ ]
+ },
+ {
+ "name": "Development & Growth Services",
+ "children": [
+ {
+ "name": "Development & Growth Services \u2192 Housing Secretariat",
+ "amount": 0.258788
+ },
+ {
+ "name": "Development & Growth Services \u2192 Toronto Building",
+ "amount": 0.099429
+ },
+ {
+ "name": "Development & Growth Services \u2192 City Planning",
+ "amount": 0.05158
+ },
+ {
+ "name": "Development & Growth Services \u2192 Development Review",
+ "amount": 0.00828
+ }
+ ]
+ },
+ {
+ "name": "Infrastructure Services",
+ "children": [
+ {
+ "name": "Infrastructure Services \u2192 Transportation Services",
+ "amount": 0.220585
+ },
+ {
+ "name": "Infrastructure Services \u2192 Engineering & Construction Services",
+ "amount": 0.079664
+ },
+ {
+ "name": "Infrastructure Services \u2192 Municipal Licensing & Standards",
+ "amount": 0.058732
+ },
+ {
+ "name": "Infrastructure Services \u2192 Policy, Planning, Finance & Administration",
+ "amount": 0.017036000000000003
+ },
+ {
+ "name": "Infrastructure Services \u2192 Transit Expansion",
+ "amount": 0.009922
+ },
+ {
+ "name": "Infrastructure Services \u2192 Toronto Emergency Management",
+ "amount": 0.001307
+ }
+ ]
+ },
+ {
+ "name": "Capital & Corporate Financing",
+ "children": [
+ {
+ "name": "Capital & Corporate Financing \u2192 Sustainment Debt Charges",
+ "amount": 0.167781
+ },
+ {
+ "name": "Capital & Corporate Financing \u2192 Capital from Current Technology",
+ "amount": 0.15
+ }
+ ]
+ },
+ {
+ "name": "Non-Program Expenditures",
+ "children": [
+ {
+ "name": "Non-Program Expenditures \u2192 Programs Funded from Reserve Funds",
+ "amount": 0.16631100000000001
+ },
+ {
+ "name": "Non-Program Expenditures \u2192 Other Corporate Expenditures",
+ "amount": 0.11699599999999999
+ }
+ ]
+ },
+ {
+ "name": "Corporate Services",
+ "children": [
+ {
+ "name": "Corporate Services \u2192 Corporate Real Estate Management",
+ "amount": 0.102509
+ },
+ {
+ "name": "Corporate Services \u2192 Technology Services",
+ "amount": 0.048427
+ },
+ {
+ "name": "Corporate Services \u2192 Fleet Services",
+ "amount": 0.043323
+ },
+ {
+ "name": "Corporate Services \u2192 Customer Experience",
+ "amount": 0.009594
+ },
+ {
+ "name": "Corporate Services \u2192 Environment & Climate",
+ "amount": 0.004221
+ },
+ {
+ "name": "Corporate Services \u2192 Office of the Chief Information Security Officer",
+ "amount": 0.00041799999999999997
+ }
+ ]
+ },
+ {
+ "name": "Finance and Treasury Services",
+ "children": [
+ {
+ "name": "Finance and Treasury Services \u2192 Financial Operations & Control",
+ "amount": 0.042066
+ },
+ {
+ "name": "Finance and Treasury Services \u2192 Office of the Chief Financial Officer and Treasurer",
+ "amount": 0.016367
+ }
+ ]
+ },
+ {
+ "name": "Other City Programs",
+ "children": [
+ {
+ "name": "Other City Programs \u2192 Legal Services",
+ "amount": 0.024591000000000002
+ },
+ {
+ "name": "Other City Programs \u2192 City Clerk's Office",
+ "amount": 0.016242999999999997
+ },
+ {
+ "name": "Other City Programs \u2192 City Council",
+ "amount": 0.000424
+ }
+ ]
+ },
+ {
+ "name": "City Manager",
+ "children": [
+ {
+ "name": "City Manager \u2192 City Manager's Office (Excluding FIFA)",
+ "amount": 0.016877
+ }
+ ]
+ },
+ {
+ "name": "Corporate Accounts",
+ "children": [
+ {
+ "name": "Corporate Accounts \u2192 Arena Boards of Management",
+ "amount": 0.011182000000000001
+ },
+ {
+ "name": "Corporate Accounts \u2192 Association of Community Centres",
+ "amount": 0.000332
+ }
+ ]
+ },
+ {
+ "name": "Accountability Offices",
+ "children": [
+ {
+ "name": "Accountability Offices \u2192 Integrity Commissioner's Office",
+ "amount": 5.3e-5
+ }
+ ]
+ }
+ ]
+ },
+ "population": 2930000,
+ "budget_balance": -4.025381000000001,
+ "per_capita_spending": 5360,
+ "property_tax_per_capita": null,
+ "property_tax_revenue": 0.0
+}
diff --git a/data/toronto-operating/summary.json b/data/toronto-operating/summary.json
new file mode 100644
index 00000000..977225f9
--- /dev/null
+++ b/data/toronto-operating/summary.json
@@ -0,0 +1,133 @@
+{
+ "name": "Toronto Operating Budget",
+ "financialYear": "2024 Operating Budget",
+ "source": "https://docs.google.com/spreadsheets/d/1nbUIUaV75xoTXwj6MvtV3pw4vmw1yb-b/edit?usp=sharing",
+ "totalProvincialSpending": 15.704151000000001,
+ "totalProvincialSpendingFormatted": "$15.7B",
+ "totalEmployees": 44000,
+ "netDebt": null,
+ "totalDebt": null,
+ "debtInterest": null,
+ "population": 2930000,
+ "budgetBalance": -4.025381000000001,
+ "budgetBalanceFormatted": "$4B",
+ "perCapitaSpending": 5360,
+ "propertyTaxPerCapita": null,
+ "propertyTaxRevenue": 0.0,
+ "propertyTaxRevenueFormatted": "$0M",
+ "ministries": [
+ {
+ "name": "Community and Social Services",
+ "slug": "community-and-social-services",
+ "totalSpending": 5.2458789999999995,
+ "totalSpendingFormatted": "$5.2B",
+ "percentage": 33.404410082404326,
+ "percentageFormatted": "33.4%"
+ },
+ {
+ "name": "Agencies",
+ "slug": "agencies",
+ "totalSpending": 4.6652070000000005,
+ "totalSpendingFormatted": "$4.7B",
+ "percentage": 29.706839930410755,
+ "percentageFormatted": "29.7%"
+ },
+ {
+ "name": "Corporate Accounts",
+ "slug": "corporate-accounts",
+ "totalSpending": 1.2246519999999999,
+ "totalSpendingFormatted": "$1.2B",
+ "percentage": 7.79826938750143,
+ "percentageFormatted": "7.8%"
+ },
+ {
+ "name": "Non-Levy Operation",
+ "slug": "non-levy-operation",
+ "totalSpending": 1.0374940000000001,
+ "totalSpendingFormatted": "$1B",
+ "percentage": 6.606495314519073,
+ "percentageFormatted": "6.6%"
+ },
+ {
+ "name": "Development & Growth Services",
+ "slug": "development-and-growth-services",
+ "totalSpending": 0.896295,
+ "totalSpendingFormatted": "$896M",
+ "percentage": 5.707376349093943,
+ "percentageFormatted": "5.7%"
+ },
+ {
+ "name": "Infrastructure Services",
+ "slug": "infrastructure-services",
+ "totalSpending": 0.6995950000000001,
+ "totalSpendingFormatted": "$700M",
+ "percentage": 4.4548412709480445,
+ "percentageFormatted": "4.5%"
+ },
+ {
+ "name": "Non-Program Expenditures",
+ "slug": "non-program-expenditures",
+ "totalSpending": 0.6915759999999999,
+ "totalSpendingFormatted": "$692M",
+ "percentage": 4.403778338606141,
+ "percentageFormatted": "4.4%"
+ },
+ {
+ "name": "Corporate Services",
+ "slug": "corporate-services",
+ "totalSpending": 0.5537809999999999,
+ "totalSpendingFormatted": "$554M",
+ "percentage": 3.526335170872974,
+ "percentageFormatted": "3.5%"
+ },
+ {
+ "name": "City Building Fund (CBF)",
+ "slug": "city-building-fund-cbf",
+ "totalSpending": 0.314096,
+ "totalSpendingFormatted": "$314M",
+ "percentage": 2.0000826533061224,
+ "percentageFormatted": "2.0%"
+ },
+ {
+ "name": "Other City Programs",
+ "slug": "other-city-programs",
+ "totalSpending": 0.123583,
+ "totalSpendingFormatted": "$124M",
+ "percentage": 0.7869448020462868,
+ "percentageFormatted": "0.8%"
+ },
+ {
+ "name": "Finance and Treasury Services",
+ "slug": "finance-and-treasury-services",
+ "totalSpending": 0.112051,
+ "totalSpendingFormatted": "$112M",
+ "percentage": 0.7135119880087754,
+ "percentageFormatted": "0.7%"
+ },
+ {
+ "name": "City Manager",
+ "slug": "city-manager",
+ "totalSpending": 0.08591700000000001,
+ "totalSpendingFormatted": "$86M",
+ "percentage": 0.5470973884548105,
+ "percentageFormatted": "0.5%"
+ },
+ {
+ "name": "Special Levy for Scarborough Subway",
+ "slug": "special-levy-for-scarborough-subway",
+ "totalSpending": 0.040699,
+ "totalSpendingFormatted": "$41M",
+ "percentage": 0.25916077857376685,
+ "percentageFormatted": "0.3%"
+ },
+ {
+ "name": "Accountability Offices",
+ "slug": "accountability-offices",
+ "totalSpending": 0.013326,
+ "totalSpendingFormatted": "$13M",
+ "percentage": 0.08485654525354475,
+ "percentageFormatted": "0.1%"
+ }
+ ],
+ "generatedAt": "2025-11-11T19:19:32Z"
+}
diff --git a/data/toronto/2024 City of Toronto Budget Summary (Operating).xlsx b/data/toronto/2024 City of Toronto Budget Summary (Operating).xlsx
new file mode 100644
index 00000000..69c63979
Binary files /dev/null and b/data/toronto/2024 City of Toronto Budget Summary (Operating).xlsx differ
diff --git a/data/toronto/scripts/convert_toronto_sankey.py b/data/toronto/scripts/convert_toronto_sankey.py
index 52054a9f..abb9d255 100644
--- a/data/toronto/scripts/convert_toronto_sankey.py
+++ b/data/toronto/scripts/convert_toronto_sankey.py
@@ -1,21 +1,25 @@
#!/usr/bin/env python3
"""Convert Toronto municipal finances from Excel to Sankey JSON format."""
+import argparse
import json
-import os
import re
from datetime import datetime
-from typing import Any, Dict
+from pathlib import Path
+from typing import Any, Dict, Tuple, Union
import pandas as pd
+REPO_ROOT = Path(__file__).resolve().parents[3]
-def parse_sankeymatic_txt(filepath: str) -> Dict[str, Any]:
+
+def parse_sankeymatic_txt(filepath: Union[str, Path]) -> Dict[str, Any]:
"""
Parse the sankeymatic.txt file to extract the hierarchical structure.
Returns a dict with 'revenue' and 'spending' tier-1 categories and their totals.
"""
- with open(filepath, 'r') as f:
+ path = Path(filepath)
+ with path.open('r') as f:
content = f.read()
result = {
@@ -114,12 +118,13 @@ def parse_sankeymatic_txt(filepath: str) -> Dict[str, Any]:
return result
-def load_excel_data(filepath: str) -> Dict[str, pd.DataFrame]:
+def load_excel_data(filepath: Union[str, Path]) -> Dict[str, pd.DataFrame]:
"""Load all relevant sheets from the Excel file."""
+ path = Path(filepath)
return {
- 'income_tier2': pd.read_excel(filepath, sheet_name='Income Tier 2'),
- 'expense_tier2': pd.read_excel(filepath, sheet_name='Expense Tier 2'),
- 'expense_tier3': pd.read_excel(filepath, sheet_name='Expesnse Tier 3')
+ 'income_tier2': pd.read_excel(path, sheet_name='Income Tier 2'),
+ 'expense_tier2': pd.read_excel(path, sheet_name='Expense Tier 2'),
+ 'expense_tier3': pd.read_excel(path, sheet_name='Expesnse Tier 3')
}
@@ -276,6 +281,85 @@ def build_spending_structure(sankey_data: Dict, excel_data: Dict) -> Dict[str, A
return spending_root
+CATEGORY_RENAMES = {
+ 'Non Levy Operation': 'Non-Levy Operation',
+ 'Non Levy Operation': 'Non-Levy Operation',
+ 'Non Levy Operation': 'Non-Levy Operation',
+ 'Non Program Revenues': 'Non-Program Revenues',
+ 'Non Program Revenues': 'Non-Program Revenues',
+}
+
+
+def normalize_category_name(value: Any) -> str:
+ if not isinstance(value, str):
+ return str(value).strip()
+ normalized = value.replace('\xa0', ' ').strip()
+ normalized = re.sub(r'\s+', ' ', normalized)
+ return CATEGORY_RENAMES.get(normalized, normalized)
+
+
+def prepare_operating_dataframe(
+ df: pd.DataFrame,
+ value_column: str,
+ *,
+ drop_revenue_rows: bool = False,
+) -> pd.DataFrame:
+ working = df.copy()
+ working = working.dropna(subset=['Name', 'Category'])
+ working[value_column] = pd.to_numeric(working[value_column], errors='coerce')
+ working = working.dropna(subset=[value_column])
+ working['Category'] = working['Category'].apply(normalize_category_name)
+ working['Name'] = working['Name'].astype(str).str.strip()
+ if drop_revenue_rows:
+ revenue_mask = working['Category'].str.contains('revenue', case=False, na=False)
+ working = working[~revenue_mask]
+ working = working[working['Name'] != '-']
+ working = working.rename(columns={value_column: 'value_m'})
+ return working[['Category', 'Name', 'value_m']]
+
+
+def build_hierarchy_from_dataframe(df: pd.DataFrame, root_name: str) -> Dict[str, Any]:
+ if df.empty:
+ return {'name': root_name, 'children': []}
+
+ grouped_nodes = []
+ for category, group in df.groupby('Category'):
+ group_sorted = group.sort_values('value_m', ascending=False)
+ children = [
+ {
+ 'name': f"{category} → {row['Name']}",
+ 'amount': float(row['value_m']) / 1000,
+ }
+ for _, row in group_sorted.iterrows()
+ ]
+ total = float(group['value_m'].sum())
+ grouped_nodes.append((category, total, children))
+
+ grouped_nodes.sort(key=lambda item: item[1], reverse=True)
+
+ return {
+ 'name': root_name,
+ 'children': [
+ {'name': category, 'children': children}
+ for category, _, children in grouped_nodes
+ ],
+ }
+
+
+def build_operating_revenue_structure(revenue_df: pd.DataFrame) -> Tuple[Dict[str, Any], float]:
+ prepared = prepare_operating_dataframe(revenue_df, '2024 ($Ms)')
+ structure = build_hierarchy_from_dataframe(prepared, 'Revenue')
+ total_billion = prepared['value_m'].sum() / 1000
+ return structure, total_billion
+
+
+def build_operating_spending_structure(expense_df: pd.DataFrame) -> Tuple[Dict[str, Any], float]:
+ prepared = prepare_operating_dataframe(expense_df, '2024 ($M)', drop_revenue_rows=True)
+ structure = build_hierarchy_from_dataframe(prepared, 'Spending')
+ total_billion = prepared['value_m'].sum() / 1000
+ return structure, total_billion
+
+
def calculate_totals(sankey_data: Dict) -> Dict[str, float]:
"""Calculate total revenue and spending from tier-1 items."""
revenue_total = sum(item['amount'] for item in sankey_data['revenue_tier1']) / 1000
@@ -381,23 +465,30 @@ def generate_summary(
return summary
+def load_operating_excel_data(filepath: Union[str, Path]) -> Dict[str, pd.DataFrame]:
+ path = Path(filepath)
+ return {
+ 'revenues': pd.read_excel(path, sheet_name='RevenuesT2_Sankey'),
+ 'expenses': pd.read_excel(path, sheet_name='ExpensesT2_Sankey'),
+ }
-def main():
- """Main conversion function."""
- print("Starting Toronto sankey conversion...")
- # Parse sankeymatic.txt
+def process_legacy_dataset(
+ *,
+ sankeymatic_file: Path,
+ excel_path: Path,
+ output_dir: Path,
+) -> None:
+ print("Starting Toronto legacy sankey conversion...")
print("Parsing sankeymatic.txt...")
- sankey_data = parse_sankeymatic_txt('2024_sankeymatic.txt')
+ sankey_data = parse_sankeymatic_txt(sankeymatic_file)
print(f" Found {len(sankey_data['revenue_tier1'])} revenue tier-1 categories")
print(f" Found {len(sankey_data['spending_tier1'])} spending tier-1 categories")
- # Load Excel data
print("\nLoading Excel data...")
- excel_data = load_excel_data('City_of_Toronto_2024_Actuals - Cleaned.xlsx')
+ excel_data = load_excel_data(excel_path)
print(" Excel data loaded successfully")
- # Build structures
print("\nBuilding revenue structure...")
revenue_data = build_revenue_structure(sankey_data, excel_data)
print(f" Built {len(revenue_data['children'])} revenue categories")
@@ -406,7 +497,6 @@ def main():
spending_data = build_spending_structure(sankey_data, excel_data)
print(f" Built {len(spending_data['children'])} spending categories")
- # Calculate totals
print("\nCalculating totals...")
totals = calculate_totals(sankey_data)
print(f" Total: ${totals['total']:.3f}B")
@@ -419,7 +509,6 @@ def main():
jurisdiction_name = 'Toronto'
financial_year = '2024'
- # Build final JSON structure
final_json = {
'total': totals['total'],
'spending': totals['spending'],
@@ -447,14 +536,98 @@ def main():
'property_tax_revenue': property_tax_total,
})
- # Create output directory
- import os
- os.makedirs('data/toronto', exist_ok=True)
+ output_dir.mkdir(parents=True, exist_ok=True)
+ sankey_path = output_dir / 'sankey.json'
+ print(f"\nSaving to {sankey_path}...")
+ with sankey_path.open('w') as f:
+ json.dump(final_json, f, indent=2)
+
+ summary = generate_summary(
+ totals,
+ spending_data,
+ revenue_data,
+ population=population,
+ total_employees=total_employees,
+ source_url=source_url,
+ name=jurisdiction_name,
+ financial_year=financial_year,
+ )
+
+ summary_path = output_dir / 'summary.json'
+ print(f"Saving to {summary_path}...")
+ with summary_path.open('w') as f:
+ json.dump(summary, f, indent=2)
+
+ print("✓ Legacy conversion complete!")
+ print(f"Output saved to: {sankey_path}")
+ print(f"File size: {sankey_path.stat().st_size / 1024:.1f} KB")
+
+
+def process_operating_dataset(
+ *,
+ excel_path: Path,
+ output_dir: Path,
+) -> None:
+ print("Starting Toronto operating-budget conversion...")
+ print("Loading operating budget Excel data...")
+ excel_data = load_operating_excel_data(excel_path)
+ print(" Excel data loaded successfully")
+
+ print("\nBuilding revenue structure from operating data...")
+ revenue_data, revenue_total = build_operating_revenue_structure(excel_data['revenues'])
+ print(f" Built {len(revenue_data['children'])} revenue categories")
+
+ print("\nBuilding spending structure from operating data...")
+ spending_data, spending_total = build_operating_spending_structure(excel_data['expenses'])
+ print(f" Built {len(spending_data['children'])} spending categories")
+
+ totals = {
+ 'total': revenue_total,
+ 'revenue': revenue_total,
+ 'spending': spending_total,
+ }
+
+ print("\nCalculated totals from operating budget:")
+ print(f" Revenue: ${revenue_total:.3f}B")
+ print(f" Spending: ${spending_total:.3f}B")
+
+ population = 2_930_000
+ total_employees = 44_000
+ source_url = 'https://docs.google.com/spreadsheets/d/1nbUIUaV75xoTXwj6MvtV3pw4vmw1yb-b/edit?usp=sharing'
+ jurisdiction_name = 'Toronto Operating Budget'
+ financial_year = '2024 Operating Budget'
- # Save to file
- output_path = 'data/toronto/sankey.json'
- print(f"\nSaving to {output_path}...")
- with open(output_path, 'w') as f:
+ final_json = {
+ 'total': totals['total'],
+ 'spending': totals['spending'],
+ 'revenue': totals['revenue'],
+ 'spending_data': spending_data,
+ 'revenue_data': revenue_data
+ }
+
+ budget_balance = totals['revenue'] - totals['spending']
+ property_tax_node = next(
+ (child for child in revenue_data['children'] if child['name'] == 'Property taxes & taxation from other governments'),
+ None,
+ )
+ property_tax_total = sum_node(property_tax_node) if property_tax_node else 0.0
+ per_capita_spending = (totals['spending'] * 1_000_000_000) / population
+ property_tax_per_capita = (
+ (property_tax_total * 1_000_000_000) / population if property_tax_total else None
+ )
+
+ final_json.update({
+ 'population': population,
+ 'budget_balance': budget_balance,
+ 'per_capita_spending': round(per_capita_spending) if per_capita_spending else None,
+ 'property_tax_per_capita': round(property_tax_per_capita) if property_tax_per_capita else None,
+ 'property_tax_revenue': property_tax_total,
+ })
+
+ output_dir.mkdir(parents=True, exist_ok=True)
+ sankey_path = output_dir / 'sankey.json'
+ print(f"\nSaving to {sankey_path}...")
+ with sankey_path.open('w') as f:
json.dump(final_json, f, indent=2)
summary = generate_summary(
@@ -468,14 +641,67 @@ def main():
financial_year=financial_year,
)
- summary_path = 'data/toronto/summary.json'
+ summary_path = output_dir / 'summary.json'
print(f"Saving to {summary_path}...")
- with open(summary_path, 'w') as f:
+ with summary_path.open('w') as f:
json.dump(summary, f, indent=2)
- print("✓ Conversion complete!")
- print(f"\nOutput saved to: {output_path}")
- print(f"File size: {os.path.getsize(output_path) / 1024:.1f} KB")
+ print("✓ Operating budget conversion complete!")
+ print(f"Output saved to: {sankey_path}")
+ print(f"File size: {sankey_path.stat().st_size / 1024:.1f} KB")
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Convert Toronto financial data into Sankey JSON.")
+ parser.add_argument(
+ '--dataset',
+ choices=['legacy', 'operating'],
+ default='legacy',
+ help="Select which dataset to process (default: legacy).",
+ )
+ parser.add_argument(
+ '--output-dir',
+ type=Path,
+ help="Override the output directory (defaults to data/toronto or data/toronto-operating).",
+ )
+ parser.add_argument(
+ '--sankeymatic-file',
+ type=Path,
+ default=REPO_ROOT / 'tmp' / '2024_sankeymatic.txt',
+ help="Path to the sankeymatic.txt file for the legacy dataset.",
+ )
+ parser.add_argument(
+ '--legacy-excel',
+ type=Path,
+ default=REPO_ROOT / 'tmp' / 'City_of_Toronto_2024_Actuals - Cleaned.xlsx',
+ help="Path to the legacy Excel file.",
+ )
+ parser.add_argument(
+ '--operating-excel',
+ type=Path,
+ default=REPO_ROOT / 'data' / 'toronto' / '2024 City of Toronto Budget Summary (Operating).xlsx',
+ help="Path to the operating budget Excel file.",
+ )
+
+ args = parser.parse_args()
+
+ if args.output_dir:
+ output_dir = args.output_dir
+ else:
+ default_subdir = 'toronto' if args.dataset == 'legacy' else 'toronto-operating'
+ output_dir = REPO_ROOT / 'data' / default_subdir
+
+ if args.dataset == 'legacy':
+ process_legacy_dataset(
+ sankeymatic_file=args.sankeymatic_file,
+ excel_path=args.legacy_excel,
+ output_dir=output_dir,
+ )
+ else:
+ process_operating_dataset(
+ excel_path=args.operating_excel,
+ output_dir=output_dir,
+ )
if __name__ == '__main__':
diff --git a/data/toronto/summary.json b/data/toronto/summary.json
index c2a93d0f..1f127422 100644
--- a/data/toronto/summary.json
+++ b/data/toronto/summary.json
@@ -89,5 +89,5 @@
"percentageFormatted": "1.4%"
}
],
- "generatedAt": "2025-10-21T22:57:13Z"
+ "generatedAt": "2025-11-11T19:19:43Z"
}
diff --git a/src/components/MainLayout/index.tsx b/src/components/MainLayout/index.tsx
index 71b739f4..53525d73 100644
--- a/src/components/MainLayout/index.tsx
+++ b/src/components/MainLayout/index.tsx
@@ -121,6 +121,14 @@ export const MainLayout = ({ children }: { children: React.ReactNode }) => {
Budget
+
+
+ Toronto (Operating)
+
+