Skip to content

Commit

Permalink
Merge pull request #106 from cfpb/bar-graphs
Browse files Browse the repository at this point in the history
Display school and national data in bar graphs
  • Loading branch information
niqjohnson committed Feb 22, 2016
2 parents 077b60d + 793ea4e commit 6570a31
Show file tree
Hide file tree
Showing 7 changed files with 368 additions and 33 deletions.
55 changes: 31 additions & 24 deletions paying_for_college/templates/worksheet.html
Original file line number Diff line number Diff line change
Expand Up @@ -1684,18 +1684,21 @@ <h4 class="metric_heading">
Percentage of full-time students who
graduate from a college or university
</p>
<div class="bar-graph">
<div class="bar-graph"
data-metric="gradRate"
data-national-metric="completionRateMedian"
data-incoming-format="decimal-percent"
data-graph-min="0"
data-graph-max="1">
<div class="bar-graph_bar"></div>
<div class="bar-graph_point
bar-graph_point__you u-clearfix">
<div class="bar-graph_label">
Your school
This school
</div>
<div class="bar-graph_line"></div>
<div class="bar-graph_value">
<div class="bar-graph_number">
92%
</div>
<div class="bar-graph_number"></div>
</div>
</div>
<div class="bar-graph_point
Expand All @@ -1705,9 +1708,7 @@ <h4 class="metric_heading">
</div>
<div class="bar-graph_line"></div>
<div class="bar-graph_value">
<div class="bar-graph_number">
86%
</div>
<div class="bar-graph_number"></div>
</div>
</div>
</div>
Expand Down Expand Up @@ -1755,17 +1756,24 @@ <h4 class="metric_heading">
taxes) after graduating from your
program
</p>
<div class="bar-graph">
<div class="bar-graph"
data-metric="medianSalary"
data-national-metric="earningsMedian"
data-incoming-format="currency"
data-graph-min="0"
data-graph-max="100000">
<div class="bar-graph_top-label">
$100,000
</div>
<div class="bar-graph_bar"></div>
<div class="bar-graph_point
bar-graph_point__you u-clearfix">
<div class="bar-graph_label">
Your school
This school
</div>
<div class="bar-graph_line"></div>
<div class="bar-graph_value">
<div class="bar-graph_number">
$26,000
</div>
<div class="bar-graph_number"></div>
</div>
</div>
<div class="bar-graph_point
Expand All @@ -1775,9 +1783,7 @@ <h4 class="metric_heading">
</div>
<div class="bar-graph_line"></div>
<div class="bar-graph_value">
<div class="bar-graph_number">
$34,000
</div>
<div class="bar-graph_number"></div>
</div>
</div>
</div>
Expand Down Expand Up @@ -2149,18 +2155,21 @@ <h4 class="metric_heading">
Percentage of students who default on
loans after entering repayment
</p>
<div class="bar-graph">
<div class="bar-graph"
data-metric="defaultRate"
data-national-metric="loanDefaultRate"
data-incoming-format="decimal-percent"
data-graph-min="0"
data-graph-max="1">
<div class="bar-graph_bar"></div>
<div class="bar-graph_point
bar-graph_point__you u-clearfix">
<div class="bar-graph_label">
Your school
This school
</div>
<div class="bar-graph_line"></div>
<div class="bar-graph_value">
<div class="bar-graph_number">
72%
</div>
<div class="bar-graph_number"></div>
</div>
</div>
<div class="bar-graph_point
Expand All @@ -2170,9 +2179,7 @@ <h4 class="metric_heading">
</div>
<div class="bar-graph_line"></div>
<div class="bar-graph_value">
<div class="bar-graph_number">
32%
</div>
<div class="bar-graph_number"></div>
</div>
</div>
</div>
Expand Down
34 changes: 34 additions & 0 deletions src/disclosures/css/disclosures.less
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,8 @@
@bar-graph-you-font-size: 18px;
@bar-graph-average-font-size: 16px;
@bar-graph-line-overhang: 6px;
@bar-graph-top-label-width: 70px;
@bar-graph-top-label-height: 20px;
// A little cheat to make the line look vertically centered
@bar-graph-line-top-cheat: 3px;

Expand Down Expand Up @@ -1056,6 +1058,17 @@
});
}

&_top-label {
width: @bar-graph-top-label-width;
margin-left: -(@bar-graph-top-label-width - @bar-graph-width) / 2 - @bar-graph-line-overhang;
position: absolute;
top: unit(-@bar-graph-top-label-height / @small-font-size-px, em);
left: 50%;
z-index: 100;
font-size: unit(@small-font-size-px / @base-font-size-px, em);
text-align: center;
}

&_point__you {
.webfont-medium();
font-size: unit(@bar-graph-you-font-size / @base-font-size-px, em);
Expand All @@ -1076,6 +1089,27 @@
top: unit( (@bar-graph-average-font-size + @bar-graph-line-top-cheat) / 2, px);
}
}

&__equal &_point__average {
display: none;
}

&__missing-you &_point__you {
display: none;
}

&__missing-average &_point__average {
display: none;
}

&__high-point &_top-label {
display: none;
}

// Median salary graph needs room above it for the top-label
&[data-metric="medianSalary"] {
border-top: @bar-graph-top-label-height solid transparent;
}
}

/* ==========================================================================
Expand Down
2 changes: 1 addition & 1 deletion src/disclosures/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ var app = {
financialModel.init( resp );
financialView.init();
// Placeholder to set bar graphs
metricView.demo();
metricView.init();
linksView.init();
} );
}
Expand Down
159 changes: 151 additions & 8 deletions src/disclosures/js/views/metric-view.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,158 @@
'use strict';

var schoolModel = require( '../models/school-model' );
var formatUSD = require( 'format-usd' );

var metricView = {

// Placeholder to set bar graphs
demo: function() {
$( '.graduation-rate .bar-graph_point__you' ).css( 'top', '5px' );
$( '.graduation-rate .bar-graph_point__average' ).css( 'top', '30px' );
$( '.average-salary .bar-graph_point__you' ).css( 'top', '80px' );
$( '.average-salary .bar-graph_point__average' ).css( 'top', '40px' );
$( '.loan-default-rates .bar-graph_point__you' ).css( 'top', '25px' );
$( '.loan-default-rates .bar-graph_point__average' ).css( 'top', '70px' );
/**
* Initiates the object
*/
init: function() {
var $graphs = $( '.bar-graph' ),
schoolValues = schoolModel.values,
nationalValues = window.nationalData || {};
this.initGraphs( $graphs, schoolValues, nationalValues );
},

/**
* Calculates the CSS bottom positions of each point on a bar graph
* @param {number} minValue Bottom point of a graph
* @param {number} maxValue Top point of a graph
* @param {number} graphHeight Height of the graph
* @param {number} schoolValue Value reported by the school
* @param {number} nationalValue Average national value
* @returns {object} Object with CSS bottom positions for each point
*/
calculateBottoms: function( minValue, maxValue, graphHeight, schoolValue, nationalValue ) {
var bottoms = {},
// Lines fall off the bottom of the graph if they sit right at the base
bottomOffset = 20;
bottoms.school = ( graphHeight - bottomOffset ) / ( maxValue - minValue ) * ( schoolValue - minValue ) + bottomOffset;
bottoms.national = ( graphHeight - bottomOffset ) / ( maxValue - minValue ) * ( nationalValue - minValue ) + bottomOffset;
return bottoms;
},

/**
* Formats a raw number for display
* @param {string} valueType Type of value to format (percent or currency)
* @param {number|string} rawValue Value to format
* @returns {boolean|string} False if rawValue is not a number, a formatted
* string otherwise
*/
formatValue: function( valueType, rawValue ) {
var formattedValue = rawValue;
if ( isNaN( rawValue ) ) {
return false;
}
if ( valueType === 'decimal-percent' ) {
formattedValue = Math.round( rawValue * 100 ).toString() + '%';
}
if ( valueType === 'currency' ) {
formattedValue = formatUSD( rawValue, { decimalPlaces: 0 } );
}
return formattedValue;
},

/**
* Fixes overlapping points on a bar graph
* @param {object} $graph jQuery object of the graph containing the points
* @param {string} schoolAverageFormatted Text of the graph's school point
* @param {string} nationalAverageFormatted Text of the graph's school point
* @param {object} $schoolPoint jQuery object of the graph's school point
* @param {object} $nationalPoint jQuery object of the graph's national point
*/
fixOverlap: function( $graph, schoolAverageFormatted, nationalAverageFormatted, $schoolPoint, $nationalPoint ) {
var schoolPointHeight = $schoolPoint.find( '.bar-graph_label' ).height(),
schoolPointTop = $schoolPoint.position().top,
nationalPointHeight = $nationalPoint.find( '.bar-graph_label' ).height(),
nationalPointTop = $nationalPoint.position().top,
$higherPoint = schoolPointTop > nationalPointTop ? $nationalPoint : $schoolPoint,
$higherPointLabels = $higherPoint.find( '.bar-graph_label, .bar-graph_value' ),
$lowerPoint = schoolPointTop > nationalPointTop ? $schoolPoint : $nationalPoint,
// nationalPointHeight is the smaller and gives just the right offset
offset = nationalPointHeight - Math.abs( schoolPointTop - nationalPointTop );
// If the values are equal, handle the display with CSS only
if ( schoolAverageFormatted === nationalAverageFormatted ) {
$graph.addClass( 'bar-graph__equal' );
return;
}
// If the points partially overlap, move the higher point's labels up
if ( nationalPointTop <= schoolPointTop + schoolPointHeight && nationalPointTop + nationalPointHeight >= schoolPointTop ) {
$higherPointLabels.css( {
'padding-bottom': offset,
'top': -offset
} );
$lowerPoint.css( 'z-index', 100 );
}
},

/**
* Sets text of each point on a bar graph (or a class if a point is missing)
* @param {object} $graph jQuery object of the graph containing the points
* @param {string} schoolAverageFormatted Text of the graph's school point
* @param {string} nationalAverageFormatted Text of the graph's school point
*/
setGraphValues: function( $graph, schoolAverageFormatted, nationalAverageFormatted ) {
var $schoolPointNumber = $graph.find( '.bar-graph_point__you .bar-graph_number' ),
$nationalPointNumber = $graph.find( '.bar-graph_point__average .bar-graph_number' );
if ( schoolAverageFormatted ) {
$schoolPointNumber.text( schoolAverageFormatted );
} else {
$graph.addClass( 'bar-graph__missing-you' );
}
if ( nationalAverageFormatted ) {
$nationalPointNumber.text( nationalAverageFormatted );
} else {
$graph.addClass( 'bar-graph__missing-average' );
}
},

/**
* Sets the position of each point on a bar graph
* @param {object} $graph jQuery object of the graph containing the points
* @param {number} schoolAverage Value reported by the school
* @param {number} nationalAverage Average national value
* @param {object} $schoolPoint jQuery object of the graph's school point
* @param {object} $nationalPoint jQuery object of the graph's national point
*/
setGraphPositions: function( $graph, schoolAverage, nationalAverage, $schoolPoint, $nationalPoint ) {
var graphHeight = $graph.height(),
minValue = $graph.attr( 'data-graph-min' ),
maxValue = $graph.attr( 'data-graph-max' ),
bottoms = this.calculateBottoms( minValue, maxValue, graphHeight, schoolAverage, nationalAverage );
// A few outlier schools have very high average salaries, so we need to
// prevent those values from falling off the top of the graph
if ( schoolAverage > maxValue ) {
bottoms.school = graphHeight;
$graph.addClass( 'bar-graph__high-point' );
}
$schoolPoint.css( 'bottom', bottoms.school );
$nationalPoint.css( 'bottom', bottoms.national );
},

/**
* Initializes all metrics with bar graphs
* @param {object} $graphs jQuery object of all graphs on the page
* @param {object} schoolValues Values reported by the school
* @param {object} nationalValues National average values
*/
initGraphs: function( $graphs, schoolValues, nationalValues ) {
$graphs.each( function() {
var $graph = $( this ),
metricKey = $graph.attr( 'data-metric' ),
nationalKey = $graph.attr( 'data-national-metric' ),
graphFormat = $graph.attr( 'data-incoming-format' ),
schoolAverage = parseFloat( schoolValues[metricKey] ),
schoolAverageFormatted = metricView.formatValue( graphFormat, schoolAverage ),
nationalAverage = parseFloat( nationalValues[nationalKey] ),
nationalAverageFormatted = metricView.formatValue( graphFormat, nationalAverage ),
$schoolPoint = $graph.find( '.bar-graph_point__you' ),
$nationalPoint = $graph.find( '.bar-graph_point__average' );
metricView.setGraphValues( $graph, schoolAverageFormatted, nationalAverageFormatted );
metricView.setGraphPositions( $graph, schoolAverage, nationalAverage, $schoolPoint, $nationalPoint );
metricView.fixOverlap( $graph, schoolAverageFormatted, nationalAverageFormatted, $schoolPoint, $nationalPoint );
} );
}

};
Expand Down
26 changes: 26 additions & 0 deletions test/functional/dd-school-data-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,30 @@ fdescribe( 'The dynamic financial aid disclosure', function() {
expect( page.jobRate.getText() ).toEqual( '18' );
} );

it( 'should graph graudation rates', function() {
page.confirmVerification();
expect( page.schoolGradRatePoint.getCssValue( 'bottom' ) ).toEqual( '60.7px' );
expect( page.schoolGradRateValue.getText() ).toEqual( '37%' );
expect( page.nationalGradRatePoint.getCssValue( 'bottom' ) ).toEqual( '57.323px' );
// Checking for z-index lets us know an overlap is being handled correctly
expect( page.nationalGradRatePoint.getCssValue( 'z-index' ) ).toEqual( '100' );
expect( page.nationalGradRateValue.getText() ).toEqual( '34%' );
} );

it( 'should graph average salary', function() {
page.confirmVerification();
expect( page.schoolSalaryPoint.getCssValue( 'bottom' ) ).toEqual( '45.3px' );
expect( page.schoolSalaryValue.getText() ).toEqual( '$23,000' );
expect( page.nationalSalaryPoint.getCssValue( 'bottom' ) ).toEqual( '54.188px' );
expect( page.nationalSalaryValue.getText() ).toEqual( '$31,080' );
} );

it( 'should graph loan default rates', function() {
page.confirmVerification();
expect( page.schoolDefaultRatePoint.getCssValue( 'bottom' ) ).toEqual( '80.5px' );
expect( page.schoolDefaultRateValue.getText() ).toEqual( '55%' );
expect( page.nationalDefaultRatePoint.getCssValue( 'bottom' ) ).toEqual( '35.07px' );
expect( page.nationalDefaultRateValue.getText() ).toEqual( '14%' );
} );

} );
Loading

0 comments on commit 6570a31

Please sign in to comment.