diff --git a/_src/_data.json b/_src/_data.json index f09cb460..a8ee5e94 100644 --- a/_src/_data.json +++ b/_src/_data.json @@ -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" } } diff --git a/_src/_who-we-are.jade b/_src/_who-we-are.jade index 03b8ed45..ac635d2b 100644 --- a/_src/_who-we-are.jade +++ b/_src/_who-we-are.jade @@ -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]) + diff --git a/_src/budget-per-capita.jade b/_src/budget-per-capita.jade new file mode 100644 index 00000000..199f5843 --- /dev/null +++ b/_src/budget-per-capita.jade @@ -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') + diff --git a/_src/budget-visuals.jade b/_src/budget-visuals.jade index cba8becb..bcf99804 100644 --- a/_src/budget-visuals.jade +++ b/_src/budget-visuals.jade @@ -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") diff --git a/_src/css/_okc-per-capita.scss b/_src/css/_okc-per-capita.scss new file mode 100644 index 00000000..81e5837f --- /dev/null +++ b/_src/css/_okc-per-capita.scss @@ -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; +} diff --git a/_src/css/main.scss b/_src/css/main.scss index edf2188b..1d2f3682 100644 --- a/_src/css/main.scss +++ b/_src/css/main.scss @@ -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; @@ -102,7 +104,7 @@ footer { .row.visualizations { h3 { text-align: center; - } + } img { height: 214px; margin: 0 auto 15px; diff --git a/_src/data/population.json b/_src/data/population.json new file mode 100644 index 00000000..0b2e64c5 --- /dev/null +++ b/_src/data/population.json @@ -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" + } + +] diff --git a/_src/images/budget-per-capita-thumb.png b/_src/images/budget-per-capita-thumb.png new file mode 100644 index 00000000..46975477 Binary files /dev/null and b/_src/images/budget-per-capita-thumb.png differ diff --git a/_src/js/okc-per-capita.js b/_src/js/okc-per-capita.js new file mode 100644 index 00000000..0c1c55b2 --- /dev/null +++ b/_src/js/okc-per-capita.js @@ -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 = $("
"); + rowDiv.className = "o-row"; + rowDiv.id = element.L2; + + spanMeasure = $(""); + spanMeasure.append("$" + element.program_per_capita + ""); + + spanDetail = $(""); + spanDetail.append("

" + element.agency + "

"); + spanDetail.append("

" + element.program + "

"); + spanDetail.append("

Total: $" + element.program_total + "

"); + + 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); +})($);