forked from codeforokc/openbudgetokc
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Creating a per citizen view of spending. Related to Issue codeforokc#41…
…. Adding thumbnail image and updating contributors page.
- Loading branch information
Brent Lightsey
authored and
Brent Lightsey
committed
Mar 15, 2017
1 parent
281f71e
commit ca5eb60
Showing
9 changed files
with
274 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
.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='http://benalman.com/code/projects/jquery-throttle-debounce/jquery.ba-throttle-debounce.js', type='application/javascript') | ||
script(src='/js/okc-per-capita.js', type='application/javascript') | ||
//if IE | ||
script(src='http://html5shiv.googlecode.com/svn/trunk/html5.js') | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
|
||
] |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
/* 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. | ||
*/ | ||
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); | ||
} | ||
// run on load | ||
$(function () { | ||
// find root element | ||
// TODO: make root element dynamic | ||
readData(processData); | ||
// resize fonts when window resizes | ||
$(window).resize($.debounce(250, resizeList)); | ||
}); |