Skip to content

Instantly share code, notes, and snippets.

@derryl
Created July 9, 2019 03:44
Show Gist options
  • Save derryl/38b19220d9b3862f4249bdcf5a432d06 to your computer and use it in GitHub Desktop.
Save derryl/38b19220d9b3862f4249bdcf5a432d06 to your computer and use it in GitHub Desktop.
Hugo Challenge - Derryl
<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> &mdash; 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>
// ----------------
// 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> &mdash; 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 )
})();
@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