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) + +