Skip to content
This repository has been archived by the owner on Jan 28, 2023. It is now read-only.

Issue #41: Budget Spending Per Citizen #137

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions _src/_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,9 @@
"budget-radar": {
"title": "Budget Radar | Open Budget: OKC",
"slug": "budget-radar"
},
"budget-per-capita": {
"title": "OKC 2017 FY Budget Per Citizen | Open Budget: OKC",
"slug": "budget-per-capita"
}
}
2 changes: 2 additions & 0 deletions _src/_who-we-are.jade
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ div
li Alex Ayon (#[a(href='https://github.com/alex-code4okc') github])
li James England (#[a(href='https://twitter.com/JEinOKC') twitter]/#[a(href='https://github.com/JEinOKC') github])
li Daniel Ashcraft (#[a(href='https://github.com/dashcraft') github])
li Brent Lightsey (#[a(href='https://github.com/brentlightsey') github])

12 changes: 12 additions & 0 deletions _src/budget-per-capita.jade
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.container
h1 2017 FY OKC Budget Per Citizen
#list-container
// divs to be added here

// style elements
link(href='https://fonts.googleapis.com/css?family=Lato', rel='stylesheet')
script(src='https://code.jquery.com/jquery-3.1.1.min.js', integrity='sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=', crossorigin='anonymous')
script(src='/js/okc-per-capita.js', type='application/javascript', charset='utf-8')
//if IE
script(src='http://html5shiv.googlecode.com/svn/trunk/html5.js')

3 changes: 3 additions & 0 deletions _src/budget-visuals.jade
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@
h3 FY2016 Budget Overview
img.img-responsive(src="/images/fy2016-tree-thumb.png")
.col-md-4
a(href="/budget-per-capita.html")
h3 FY2017 Budget Per Citizen
img.img-responsive(src="/images/budget-per-capita-thumb.png")
64 changes: 64 additions & 0 deletions _src/css/_okc-per-capita.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//colors
$color_alto_approx: #ddd;
$color_smalt_blue_approx: #5c828a;
$color_mountain_mist_approx: #999;
$color_pink_swan_approx: #bbb;

//fonts
$font_0: Lato;
$font_1: sans-serif;

body {
font-family: $font_0, $font_1;
}
div {
display: block;
}
p {
margin: 0 0 1em 0;
&.o-l1 {
text-transform: uppercase;
font-size: 12px;
color: $color_smalt_blue_approx;
margin-bottom: 0;
font-weight: 700;
}
&.o-l2 {
font-size: 22px;
margin-bottom: 3px;
text-transform: capitalize;
}
&.o-total {
font-size: 18px;
margin-bottom: 0;
color: $color_mountain_mist_approx;
}
}
.o-row {
display: block;
span {
display: inline-block;
}
.o-measure {
width: 70%;
text-align: right;
margin-bottom: 10px;
font-weight: 300;
position: relative;
}
.o-detail {
width: 28%;
margin-left: 1%;
}
}
.o-value {
border-bottom: 1px solid $color_alto_approx;
}
.o-cash {
color: $color_pink_swan_approx;
font-size: 50%;
vertical-align: top;
top: 0.4em;
position: relative;
margin-right: 2px;
}
4 changes: 3 additions & 1 deletion _src/css/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
@import "treemap"; // D3 Treemap
@import "flow"; // D3 Sankey diagram
@import "okc-budget-tree";
@import "okc-per-capita";

html {
height: 100%;
overflow-y: scroll;
Expand Down Expand Up @@ -102,7 +104,7 @@ footer {
.row.visualizations {
h3 {
text-align: center;
}
}
img {
height: 214px;
margin: 0 auto 15px;
Expand Down
18 changes: 18 additions & 0 deletions _src/data/population.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"year": 2015,
"city-population": "631346",
"metro-population": "1358452"
},
{
"year": 2016,
"city-population": "631346",
"metro-population": "1358452"
},
{
"year": 2017,
"city-population": "631346",
"metro-population": "1358452"
}

]
Binary file added _src/images/budget-per-capita-thumb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
181 changes: 181 additions & 0 deletions _src/js/okc-per-capita.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/* Takes in a 2-level hierarchical set of data, and renders a series of divs
showing all of the measures sized proportinally to each other based on their
value compared to the grand total. This ensures that the largest spending item
will have the largest font.
*/
;/* global $ */
(function($){
// Credit: https://remysharp.com/2010/07/21/throttling-function-calls
function debounce(fn, delay) {
var timer = null;
return function () {
var context = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}

function getRootElement() {
return $("#list-container");
}

function getResizeElements(rootElement) {
return rootElement.find(".o-measure");
}

function readData(callback) {
$.when($.getJSON("./data/fy2017/c4okc_fy2017.json"),
$.getJSON("./data/population.json"))
.done(function(budgetData, populationData) {
callback(budgetData[0], populationData[0]);
})
.fail(function (jqxhr, textStatus, error) {
output("Had a problem getting the data: " + error);
});
}

// master function to begin after data retrieval
function processData(budgetData, populationData) {
var aggregated = aggregateData(budgetData);
var perCapita = calculatePerCapita(aggregated, populationData);
var sorted = sortData(perCapita);
var rootElement = getRootElement();
rootElement = renderList(rootElement, sorted);
resizeList();
}

// Create a new list that reduces the data into totals based on given keys
function aggregateData(data){
aggregated = data.reduce(function(acc,val){
var key = val.agency+"-"+val.program;
// break here, what's happening?
if (!acc.hasOwnProperty(key)){
acc[key] =
{
"agency": val.agency,
"program": val.program,
"program_total": 0
};
}
acc[key]["program_total"] += Number(val.value);
return acc;
}, {});
// convert single object to array
aggArray = [];
for(var key in aggregated) {
aggArray.push(aggregated[key]);
}

return aggArray;
}

// Calculates the per capita value of each program total
function calculatePerCapita(budgetData, populationData) {
// expects aggregated budget data with "program_total" attribute
// find the metro population, assume 2017
var metroPopObject = $.grep(populationData, function(e){return e.year == 2017});
var metroPop = Number(metroPopObject[0]["metro-population"]);

var perCapitaData = budgetData.map(function (e) {
var programTotal = Number(e["program_total"]);
var programPerCapita = programTotal / metroPop;

return {
"agency": e["agency"],
"program": e["program"],
"program_total": e["program_total"].toLocaleString(),
"program_per_capita": programPerCapita.toLocaleString(undefined,
{ maximumFractionDigits: 2, minimumFractionDigits: 2})
};
});

return perCapitaData;
}

// assumes the data is already structured with L1, L2 & measure
function sortData(data) {
var sortedData = data.sort(function(a, b) {
// sort only by program total
return b.program_per_capita - a.program_per_capita; // descending order
});

return sortedData;
}

//Render the elements in the data as a series of divs with the 'o-row' class applied
function renderList(rootElement, data) {
data.forEach(function (element) {
rowDiv = $("<div class='o-row'></div>");
rowDiv.className = "o-row";
rowDiv.id = element.L2;

spanMeasure = $("<span class='o-measure'></span>");
spanMeasure.append("<span class='o-cash'>$</span");
spanMeasure.append("<span class='o-value'>" + element.program_per_capita + "</span>");

spanDetail = $("<span class='o-detail'></span>");
spanDetail.append("<p class='o-l1'>" + element.agency + "</p>");
spanDetail.append("<p class='o-l2'>" + element.program + "</p>");
spanDetail.append("<p class='o-total'>Total: $" + element.program_total + "</p>");

rowDiv.append(spanMeasure);
rowDiv.append(spanDetail);
rootElement.append(rowDiv);
});

return rootElement;
}

// Resize the list based on window size
function resizeList() {
rootElement = getRootElement();
elementsToResize = getResizeElements(rootElement);

var maxWidth = 1800;
var defaultScaler = 22.5; //scaler - multiplication factor for fonts
var minScaler = 4.2;


var newWidth = Math.min($(window).width(), maxWidth); // sets max for width calc
var scaler = Math.max(defaultScaler * newWidth / maxWidth, minScaler); // min scale

elementsToResize.each(function(k,v) {
var valSpan = $(v).children('.o-value')[0];
var value = $(valSpan).text();
var fontSize = getFontSize(value, scaler);
$(v).css('font-size', fontSize + 'px');
});

}

function getFontSize(val, scaler) {
var minSize = 18; // minimum font size
var pc = String(val);
var str = pc.replace(',', ''); // "100.01"
var val = Number(str); // 100.01
var roundNum = Math.round(val);
var periodCount = (str.match(/\./g) || []).length;
var numeralCount = (str.match(/[0-9]/g) || []).length;
var nonNumerals = Math.floor((String(roundNum).length-1) / 3) + periodCount; // count of periods and commas

var size = Math.sqrt((val) / (.7*( (.56 * numeralCount) + (.27*nonNumerals) ))); // font size function
var fontSize = scaler * size;

return Math.max(fontSize, minSize);

}

// helpers
function output(message) {
alert(message);
}

// resize fonts when window resizes
$(window).resize(debounce(resizeList, 250));

// find root element
// TODO: make root element dynamic
readData(processData);
})($);