Created
July 9, 2019 03:44
-
-
Save derryl/38b19220d9b3862f4249bdcf5a432d06 to your computer and use it in GitHub Desktop.
Hugo Challenge - Derryl
This file contains hidden or 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
<div class="container"> | |
<h1>U.S. Poverty Data</h1> | |
<div class="section"> | |
<h2>At a glance</h2> | |
<div class="cards flexrow" id="data-cards"> | |
<div class="card"> | |
<!-- <h4>Highest Population in Poverty</h4> --> | |
<div class="content flexrow" id="highest-absolute-state"> | |
<h1 class="display">CA</h1> | |
<p>California has <b>2,300,000 people living in poverty</b> — the highest in the nation.</p> | |
</div> | |
</div> | |
<div class="card"> | |
<!-- <h4>Highest % in Poverty</h4> --> | |
<div class="content flexrow" id="highest-percent-state"> | |
<h1 class="display">AL</h1> | |
<p>Alabama has the <b>highest poverty rate,</b> with <b>27.22%</b> of its inhabitants living below the poverty line.</p> | |
</div> | |
</div> | |
<div class="card"> | |
<!-- <h4>Most Representative Youth Population</h4> --> | |
<div class="content flexrow" id="most-representative-youth"> | |
<h1 class="display">KS</h1> | |
<p></p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="section"> | |
<h2>All data</h2> | |
<!-- Table for display information about each state --> | |
<table id="data-table" class="header-fixed"> | |
<!-- Heading row --> | |
<thead> | |
<tr> | |
<th class="sorted"><!-- State --></th> | |
<th>Population</th> | |
<th># in Poverty</th> | |
<th>% in Poverty</th> | |
<th>0 to 17 in Poverty</th> | |
</tr> | |
</thead> | |
<!-- Data (populated from JavaScript) --> | |
<tbody id="statesData"></tbody> | |
</table> | |
</div> | |
</div> |
This file contains hidden or 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
// ---------------- | |
// High-level notes | |
// ---------------- | |
// In the interest of time, I've organized this entire thing | |
// as a large singleton IIFE. Here are some notes on what I'd | |
// improve/change, if this were a longer-term project: | |
// - Utility logic (sorting, calculating, DOM manipulation) | |
// would get pulled in from a separate component. | |
// - Rendering in general could be much DRY-er, and follow a | |
// more predictable pattern for custom transformation of data, | |
// as well as providing events for user interaction on the DOM. | |
// - `Cards` and `Table` components would get pulled in as deps | |
// by a central `App` component, rather than created anonymously | |
// (without a retained reference) by renderData(). | |
(function() { | |
// Shorthand for `console.log`. Used for development. | |
const log = console.log.bind( console ); | |
// Mapping of U.S. state codes to their full names. | |
const stateNameByCode = {"AK": "Alaska", "AL": "Alabama", "AR": "Arkansas", "AZ": "Arizona", "CA": "California", "CO": "Colorado", "CT": "Connecticut", "DC": "District of Columbia", "DE": "Delaware", "FL": "Florida", "GA": "Georgia", "HI": "Hawaii", "IA": "Iowa", "ID": "Idaho", "IL": "Illinois", "IN": "Indiana", "KS": "Kansas", "KY": "Kentucky", "LA": "Louisiana", "MA": "Massachusetts", "MD": "Maryland", "ME": "Maine", "MI": "Michigan", "MN": "Minnesota", "MO": "Missouri", "MS": "Mississippi", "MT": "Montana", "NC": "North Carolina", "ND": "North Dakota", "NE": "Nebraska", "NH": "New Hampshire", "NJ": "New Jersey", "NM": "New Mexico", "NV": "Nevada", "NY": "New York", "OH": "Ohio", "OK": "Oklahoma", "OR": "Oregon", "PA": "Pennsylvania", "RI": "Rhode Island", "SC": "South Carolina", "SD": "South Dakota", "TN": "Tennessee", "TX": "Texas", "UT": "Utah", "VA": "Virginia", "VT": "Vermont", "WA": "Washington", "WI": "Wisconsin", "WV": "West Virginia", "WY": "Wyoming"}; | |
// ------------------------------------------------------------- | |
// HELPER FUNCTIONS | |
// ------------------------------------------------------------- | |
// Returns a copy of the provided object Array, sorted by a given key. | |
function sortBy( arr, k ) { | |
return arr.slice().sort( ( a, b ) => { | |
let A = a[k], | |
B = b[k]; | |
if (A < B) return 1; | |
if (A > B) return -1; | |
return 0; | |
}) | |
} | |
// DEPRECATED: I decided to simply overwrite existing content when rendering. For larger datasets, this decision would present performance issues. | |
//-- Accepts a DOM Node. Returns the Node with all of its children removed (empty). | |
//-- function removeChildren( node ) { | |
//-- if ( node && node instanceof Node ) { | |
//-- while( node.firstChild ) { | |
//-- node.removeChild( node.firstChild ) | |
//-- } | |
//-- } | |
//-- return node; | |
//-- } | |
// Accepts two Numbers. Returns the percentage value of subtotal, rounded to 2 decimals. | |
function getPercent( subtotal, total ) { | |
return Math.round( subtotal / total * 10000) / 100; | |
} | |
// Accepts an Array of objects and a key containing the desired data point. | |
// Returns the average value of that key, across all objects. | |
function getMean( arr, key ) { | |
// Ignore array items that do not possess the desired value, to avoid a mis-count. | |
arr = arr.slice().filter( item => { | |
return (item[key] && typeof item[key] === "number") | |
}); | |
return arr | |
.map( item => { return item[key] }) | |
.reduce((a, b) => { return a + b }, 0) / arr.length; | |
} | |
// Accepts a mean youth population (Number) and an Array of state objects. | |
// Returns the state (as an Object) whose youth population is nearest to national average. | |
// Note: This function runs in O(n), as it checks every single state. | |
// Sorting our input by `age0to17` ahead of time would improve the runtime | |
// of the function... but we'd merely have shifted the work elsewhere. | |
// Overall speed would remain roughly the same. | |
function getNearestToMean( states, mean ) { | |
let smallestDiff = 99999999999999, | |
nearestState; | |
states.forEach( state => { | |
let diff = Math.abs( state['age0to17'] - mean ); | |
if ( diff < smallestDiff ) { | |
nearestState = state; | |
smallestDiff = diff + 0; | |
} | |
}); | |
return nearestState; | |
} | |
// Accepts a value. Returns the same value. | |
// If the value is a Number, commas will be inserted for readability. | |
function prettify( text ) { | |
let casted = parseInt( text, 10 ); | |
return( isNaN( casted ) || typeof casted !== "number" ) ? text : casted.toLocaleString(); | |
} | |
// Accepts two Objects. Returns a de-duplicated Array of keys found in one or both Objects. | |
function getUniqueKeys( first, second ) { | |
// TODO: Modify function to accept arbitrary number of inputs, rather than two. | |
return Object.keys( first ).concat(Object.keys( second )) | |
.filter((key, pos, self) => { | |
return self.indexOf( key ) === pos; | |
}) | |
.sort(); | |
} | |
// ------------------------------------------------------------- | |
// APP LOGIC | |
// ------------------------------------------------------------- | |
// Accepts an Array of endpoints and returns an Array of the resulting data, as a Promise. | |
// Usage: fetchData([ thing1, thing2, ... ]).then( doSomething ) | |
// *Note: Receiving function must be aware of ordering. This is a design flaw!* | |
function fetchData( endpoints ) { | |
// Extracts JSON from a Response object. | |
let getJson = response => { | |
return response.json(); | |
} | |
// Fetch each endpoint (async) | |
return Promise.all( Array.prototype.map.call( endpoints, endpoint => { | |
return fetch( endpoint ); | |
// Extract JSON from each response (async) | |
})).then( responses => { | |
return Promise.all( Array.prototype.map.call( responses, getJson )).then( data => { | |
return data; | |
}) | |
}) | |
} | |
// Accepts an Array of data objects, with known order. (Important!) | |
// Returns a merged dataset for use by the UI. | |
function collateData( data ) { | |
const povertyData = data[0], | |
populations = data[1]; | |
// Get a list of all states present in the dataset. | |
const stateNames = getUniqueKeys( povertyData, populations ); | |
// Merge and return the two sets, along with certain values derived from both. | |
return Array.prototype.map.call( stateNames, state => { | |
let population = populations[state], | |
poverty = povertyData[state]; | |
return { | |
name: stateNameByCode[ state ], | |
code: state, | |
population: population || "--", | |
poverty_total: poverty.total, | |
poverty_perc: (population) ? getPercent(poverty.total, population) : "--", | |
age0to17: poverty.age0to17 | |
} | |
}) | |
} | |
// ------------------------------------------------------------- | |
// RENDERING | |
// ------------------------------------------------------------- | |
// Render the poverty data | |
function renderData( states ) { | |
renderTable( states ); | |
renderCards( states ); | |
} | |
// Accepts a vector of state data, and renders the cards at top of page. | |
function renderCards( data ) { | |
const states = data.slice(), | |
meanPovertyRate = getMean( states, 'poverty_perc'), | |
meanYouthPopulation = getMean( states, 'age0to17' ); | |
// First card shows the state with highest absolute # in poverty. | |
const c1 = sortBy( states, 'poverty_total' ).slice(0,1)[0]; | |
// Second card shows the state with highest percentage in poverty. | |
const c2 = sortBy( states, 'poverty_perc' ).slice(0,1)[0]; | |
c2.mean = meanPovertyRate; | |
// Third card shows the state whose youth population is nearest to national average. | |
const c3 = getNearestToMean( states, meanYouthPopulation ); | |
c3.mean = meanYouthPopulation; | |
renderCard( 'highest-absolute-state', c1, getFirstCardText ); | |
renderCard( 'highest-percent-state', c2, getSecondCardText ); | |
renderCard( 'most-representative-youth', c3, getThirdCardText ); | |
} | |
// Accepts a state Object, and returns HTML content for display. | |
function getFirstCardText( state ) { | |
return `<h1 class="display">${ state.code }</h1> | |
<p>${ state.name } has <b>${ prettify(state.poverty_total) } | |
people living in poverty</b> — the highest in the nation.</p>`; | |
} | |
// Accepts a state Object, and returns HTML content for display. | |
function getSecondCardText( state ) { | |
return `<h1 class="display">${ state.code }</h1> | |
<p>${ state.name } has the <b>highest poverty rate,</b> | |
with <b>${ state.poverty_perc }%</b> of its inhabitants living below | |
the poverty line. The national average is ${ prettify(state.mean) }%.</p>`; | |
} | |
// Accepts a state Object, and returns HTML content for display. | |
function getThirdCardText( state ) { | |
return `<h1 class="display">${ state.code }</h1> | |
<p>${ state.name } has ${ prettify(state.age0to17) } <b>youth living in poverty</b> | |
(aged 0-17), which most closely resembles the national average of ${ prettify(state.mean) }.</p>`; | |
} | |
// Given an element ID, state data, and rendering function, | |
// inserts the final result into the appropriate DOM element. | |
function renderCard( elementID, stateData, renderFn ) { | |
const card = document.getElementById( elementID ); | |
card.innerHTML = renderFn( stateData ); | |
return card; | |
} | |
// Accepts a collated dataset of U.S. states, and displays them in table format. | |
// (Optional: A second argument to denote sort order. This feature | |
// would make it trivial to implement clickable table headers.) | |
function renderTable( states, sortKey ) { | |
// Grab the body of the data for appending rows to | |
const table = document.getElementById( "statesData" ); | |
// Sort the data. If no sort order is specified, | |
// then use the total # in poverty (descending). | |
states = sortBy( states, sortKey || 'poverty_total' ); | |
// Append each state as a row in the table | |
states.forEach( state => { | |
table.appendChild( getTableRow( state, sortKey ) ); | |
}); | |
return states; | |
} | |
// Helper function for returning a specific HTML td element | |
// for use in a row. | |
function getTableField( fieldValue ) { | |
const field = document.createElement( "td" ); | |
// const fieldText = document.createTextNode( fieldValue ); | |
// field.appendChild( fieldText ); | |
field.innerHTML = fieldValue; | |
return field; | |
} | |
// Helper function for retrieving an HTML element defining | |
// a state in an HTML table. | |
function getTableRow( state, sortKey ) { | |
// Generate a table row for this state's data. | |
const row = document.createElement( "tr" ); | |
// Produce an array of content for the table row, | |
// transforming numbers into "prettified" strings. | |
const fields = [ | |
state['name'], | |
prettify( state['population'] ), | |
prettify( state['poverty_total'] ), | |
state['poverty_perc'], | |
prettify( state['age0to17'] ) | |
]; | |
// Create table cells based on the data. | |
fields.forEach( text => { | |
row.appendChild( getTableField( text ) ); | |
}); | |
return row; | |
} | |
// ------------------------------------------------------------- | |
// THE "APP" (see notes above) | |
// ------------------------------------------------------------- | |
fetchData([ | |
"//static.withhugo.com/tests/data/poverty.json", | |
"//static.withhugo.com/tests/data/population.json" | |
]) | |
.then( collateData ) | |
.then( renderData ) | |
})(); |
This file contains hidden or 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
@import "//fonts.googleapis.com/css?family=Roboto:300,700"; | |
* { | |
box-sizing: border-box; | |
} | |
/* BASE, TYPOGRAPHY, etc. */ | |
body { | |
font-family: "Roboto", Arial, sans-serif; | |
font-size: 67.5%; | |
line-height: 1.4; | |
padding: 0; | |
margin: 0; | |
color: #333; | |
display: flex; | |
flex-direction: column; | |
} | |
h1, h2, h3, h4, h5, p { | |
margin-top: 0; | |
} | |
h1 { | |
font-size: 2rem; | |
} | |
b, .bold { | |
font-weight: 700; | |
} | |
h1.display { | |
font-size: 2rem; | |
margin-bottom: .5rem; | |
} | |
/* LAYOUT */ | |
.flexrow { | |
display: flex; | |
flex-direction: row; | |
} | |
.container { | |
display: flex; | |
flex-direction: column; | |
padding: 2rem; | |
background: #f3f3f3; | |
} | |
.section { | |
margin-bottom: 1rem; | |
} | |
.card, table { | |
border-radius: 5px; | |
background: #fff; | |
box-shadow: 0 1px 3px rgba(0,0,0,0.2); | |
} | |
/* CARDS */ | |
.cards { | |
margin-bottom: 1rem; | |
} | |
.card { | |
padding: 1rem; | |
flex: 1 1 auto; | |
position: relative; | |
display: flex; | |
flex-direction: row; | |
justify-content: stretch; | |
} | |
.card + .card { | |
margin-left: 1rem; | |
} | |
.card .content { | |
align-items: center; | |
} | |
.card h1, .card p { | |
margin: 0; | |
} | |
.card p { | |
max-width: 250px; | |
margin-left: 1rem; | |
font-size: .85rem; | |
} | |
/* TABLES */ | |
table { | |
width: 100%; | |
font-size: 1rem; | |
border-collapse: separate; | |
border-spacing: 0; | |
font-size: .9rem; | |
} | |
th, td { | |
text-align: left; | |
padding: .65rem 1rem .5rem; | |
font-weight: normal; | |
margin: 0; | |
} | |
th { | |
border-bottom: 1px solid #ddd; | |
font-weight: bold; | |
/*background: #ddd;*/ | |
/*cursor: pointer;*/ | |
} | |
th:first-child { | |
border-top-left-radius: 5px; | |
} | |
th:last-child { | |
border-top-right-radius: 5px; | |
} | |
td { | |
border-bottom: 1px solid #eee; | |
} | |
tr:hover td { | |
background: #f3f3f3; | |
} | |
tr:last-child td { | |
border: none; | |
} | |
td:first-child { | |
font-weight: bold; | |
} | |
/*th.sorted, td.sorted { | |
font-weight: bold; | |
background: #f3f3f3; | |
} | |
th.sorted { | |
border-color: black; | |
} | |
th:hover:not(.sorted) { | |
background: #f8f8f8; | |
border-color: #aaa; | |
}*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment