cfpb-chart-builder
Advanced tools
Comparing version 0.0.9 to 1.0.0
{ | ||
"name": "cfpb-chart-builder", | ||
"version": "0.0.9", | ||
"description": "A package to build charts to CFPB style", | ||
"main": "index.js", | ||
"version": "1.0.0", | ||
"description": "Charts for the Consumer Financial Protection Bureau", | ||
"main": "src/index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
"build": "npm run js && npm run css && npm run minify", | ||
"js": "browserify src/index.js -o dist/cfpb-chart-builder.js", | ||
"css": "lessc src/styles.less dist/cfpb-chart-builder.css", | ||
"minify": "browserify src/index.js -t uglifyify | uglifyjs -c -o dist/cfpb-chart-builder.min.js && lessc --clean-css src/styles.less dist/cfpb-chart-builder.min.css", | ||
"watch": "watchify src/index.js -o dist/cfpb-chart-builder.js -v -d", | ||
"start": "http-server . -p 8088", | ||
"browser-tests": "CI_ENVIRONMENT=true node test/browser-tests.js", | ||
"test": "npm run build && npm run browser-tests", | ||
"preinstall": "git config merge.ours.driver true" | ||
}, | ||
@@ -13,3 +21,3 @@ "repository": { | ||
}, | ||
"author": "", | ||
"author": "Consumer Financial Protection Bureau", | ||
"license": "CC0-1.0", | ||
@@ -21,4 +29,18 @@ "bugs": { | ||
"dependencies": { | ||
"d3": "4.2.7" | ||
"highcharts": "5.0.7", | ||
"papaparse": "4.1.2", | ||
"xdr": "^0.5.3" | ||
}, | ||
"devDependencies": { | ||
"browserify": "^13.3.0", | ||
"http-server": "^0.9.0", | ||
"less": "^2.7.2", | ||
"less-plugin-clean-css": "^1.5.1", | ||
"request": "^2.79.0", | ||
"sauce-connect-launcher": "^1.2.0", | ||
"static-server": "^2.0.4", | ||
"uglify-js": "^2.7.5", | ||
"uglifyify": "^3.0.4", | ||
"watchify": "^3.8.0" | ||
} | ||
} |
@@ -1,60 +0,69 @@ | ||
# CFPB Chart Builder | ||
# CFPB Chart Builder [![Build Status](https://travis-ci.org/cfpb/cfpb-chart-builder.svg?branch=master)](https://travis-ci.org/cfpb/cfpb-chart-builder) | ||
This package builds charts to CFPB styles. | ||
[![Sauce Test Status](https://saucelabs.com/browser-matrix/cct-sauce.svg)](https://saucelabs.com/u/cct-sauce) | ||
Charts for the [Consumer Financial Protection Bureau](https://cfpb.github.io/). | ||
**Screenshot**: Example chart screenshots coming soon. | ||
## Usage | ||
![](https://raw.githubusercontent.com/cfpb/open-source-project-template/master/screenshot.png) | ||
Add a `div` with a class of `cfpb-chart` and the following data attributes to your page: | ||
``` | ||
<div class="cfpb-chart" | ||
data-chart-type="line" | ||
data-chart-title="Number of Originations (in millions)" | ||
data-chart-description="Auto loan originations decreased in 2016." | ||
data-chart-color="green" | ||
data-chart-metadata="Number of Loans" | ||
data-chart-source="consumer-credit-trends/auto-loans.csv"> | ||
Auto loan originations decreased in 2016. | ||
</div> | ||
``` | ||
## Dependencies | ||
Add this library to your page: | ||
Describe any dependencies that must be installed for this software to work. | ||
This includes programming languages, databases or other storage mechanisms, build tools, frameworks, and so forth. | ||
If specific versions of other software are required, or known not to work, call that out. | ||
``` | ||
<script type="text/javascript" src="dist/cfpb-chart-builder.min.js"></script> | ||
``` | ||
## Installation | ||
It'll generate a chart for you: | ||
Detailed instructions on how to install, configure, and get the project running. | ||
This should be frequently tested to ensure reliability. Alternatively, link to | ||
a separate [INSTALL](INSTALL.md) document. | ||
![Screenshot](screenshot.png) | ||
It can also do column charts and maps. | ||
## Configuration | ||
If the software is configurable, describe it in detail, either here or in other documentation to which you link. | ||
TBD | ||
## Usage | ||
## Contributing | ||
Show users how to use the software. | ||
Be specific. | ||
Use appropriate formatting when showing code snippets. | ||
1. Clone this repository. | ||
1. `npm install` | ||
1. `npm run watch` to bundle the JS and output it to `dist/`. | ||
1. `npm start` to serve the test directory. | ||
1. Open `http://localhost:8088/test` in a browser to see the test page with a dozen random charts on it. | ||
1. Whenever a JS file in `src/` is edited, the JS will be rebundled. Refresh the page. | ||
## How to test the software | ||
Bonus: Visit `http://localhost:8088/test/all-charts.html` to see *all* the CCT charts. | ||
If the software includes automated tests, detail how to run those tests. | ||
Helpful commands: | ||
## Known issues | ||
- `npm run build` - Bundle and minify all assets into the `dist/` directory. | ||
- `npm run watch` - Bundle JS files whenever they're changed. | ||
- `npm start` - Start a local server to demo the charts at `http://localhost:8088/test`. | ||
- `npm test` - Run the test charts through Sauce Labs. | ||
Document any known significant shortcomings with the software. | ||
## Testing | ||
## Getting help | ||
Sauce Labs is used to test the charts in IE 8 through 10. | ||
An [Open Sauce](https://saucelabs.com/open-source) account has been created for this repo. | ||
The `curl` command below will grab the credentials for you. | ||
Instruct users how to get help with this software; this might include links to an issue tracker, wiki, mailing list, etc. | ||
1. `curl -o test/config.json https://GHE/raw/gist/contolini/504ea71f6a19c74090c7a150aff60421/raw/b3850abab5466af62406d3f0d7a3da05f7f92124/config.json` | ||
1. `npm test` | ||
**Example** | ||
The browser tests will take several minutes to run. | ||
The test script simply loads `http://localhost:8088/test` in IE VMs and reports any `window` errors. | ||
If you have questions, concerns, bug reports, etc, please file an issue in this repository's Issue Tracker. | ||
## Getting involved | ||
This section should detail why people should get involved and describe key areas you are | ||
currently focusing on; e.g., trying to get feedback on features, fixing certain bugs, building | ||
important pieces, etc. | ||
General instructions on _how_ to contribute should be stated with a link to [CONTRIBUTING](CONTRIBUTING.md). | ||
---- | ||
## Open source licensing info | ||
@@ -64,12 +73,1 @@ 1. [TERMS](TERMS.md) | ||
3. [CFPB Source Code Policy](https://github.com/cfpb/source-code-policy/) | ||
---- | ||
## Credits and references | ||
1. These charts are generated using [d3.js](d3js.org). | ||
1. The following Mike Bostock d3 examples were instrumental to the development of this code: | ||
- [Line chart](http://bl.ocks.org/mbostock/3883245) example using the latest v4 of d3 | ||
- [Bar chart](https://bl.ocks.org/d3noob/bdf28027e0ce70bd132edc64f1dd7ea4) example using v4 | ||
- [State grid](http://bl.ocks.org/mbostock/29cc3cc4078091fd2115) |
'use strict'; | ||
var d3 = require( 'd3' ); | ||
var CFPBChart = require( './CFPBChart' ); | ||
var Highcharts = require( 'highcharts/highstock' ); | ||
BarChart.prototype = new CFPBChart(); | ||
BarChart.prototype.constructor = BarChart; | ||
Highcharts.setOptions({ | ||
lang: { | ||
rangeSelectorZoom: '', | ||
thousandsSep: ',' | ||
} | ||
}); | ||
var yAxisUnit; | ||
var months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', | ||
'July', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec' ]; | ||
function BarChart( properties ) { | ||
this.selector = properties.selector; | ||
this.data = properties.data; | ||
this.type = 'BarChart'; | ||
this.labels = properties.labels || {}; | ||
yAxisUnit = properties.yAxisUnit || ''; | ||
function BarChart( props ) { | ||
// var i = props.data.categories.length - 7, | ||
// projected = props.data.categories[i], | ||
// projDate = new Date( projected ), | ||
// projDateText = months[projDate.getMonth()] + ' ' + projDate.getFullYear(); | ||
this.drawGraph = function( options ) { | ||
var data = this.data; | ||
var projDateText = 'April 2016'; | ||
// variables from options | ||
var baseWidth = options.baseWidth || 200, | ||
baseHeight = options.baseHeight || 100, | ||
paddingDecimal = options.paddingDecimal || .1, | ||
margin = options.margin || {top: 20, right: 20, bottom: 20, left: 20}; | ||
var options = { | ||
title: { | ||
text: props.title | ||
}, | ||
description: props.description, | ||
credits: false, | ||
rangeSelector : { | ||
inputEnabled:false | ||
}, | ||
chart: { | ||
width: 650, | ||
height: 500 | ||
}, | ||
legend: { | ||
enabled: false | ||
}, | ||
plotOptions: { | ||
column: { | ||
pointPadding: 0, | ||
borderWidth: 1, | ||
groupPadding: 0, | ||
shadow: false, | ||
grouping: false | ||
} | ||
}, | ||
xAxis: { | ||
tickInterval: 12, | ||
// labels: { | ||
// autoRotation: false, | ||
// formatter: function() { | ||
// var date = new Date( this.value ); | ||
// return months[ date.getMonth() ] + ' ' + date.getFullYear(); | ||
// }, | ||
// style: { | ||
// 'color': '#75787b', | ||
// 'font-size': '16px' | ||
// }, | ||
// y: 30 | ||
// } | ||
// plotLines: [{ | ||
// className: 'bar-chart_projected-line', | ||
// color: '#75787b', | ||
// label: { | ||
// align: 'left', | ||
// rotation: 0, | ||
// style: { | ||
// 'color': '#75787b', | ||
// 'font-size': '16px' | ||
// }, | ||
// text: 'Values after ' + projDateText + ' are projected', | ||
// textAlign: 'right', | ||
// // useHTML: true, | ||
// y: -30 | ||
// }, | ||
// width: 2, | ||
// value: props.data.length - 6.5 | ||
// }] | ||
}, | ||
yAxis: { | ||
opposite: false, | ||
title: { | ||
text: 'Year-over-year change (%)', | ||
style: { | ||
'color': '#75787b', | ||
'font-size': '16px' | ||
} | ||
} | ||
}, | ||
navigator: { | ||
maskFill: 'rgba(0, 0, 0, 0.05)', | ||
handles: { | ||
backgroundColor: '#fff', | ||
borderColor: '#000' | ||
}, | ||
series: { | ||
color: '#addc91', | ||
lineWidth: 2 | ||
} | ||
}, | ||
series: [ { | ||
type: 'column', | ||
data: props.data, | ||
name: 'Year-over-year change (%)', | ||
tooltip: { | ||
valueDecimals: 2 | ||
}, | ||
zoneAxis: 'x', | ||
zones: [ | ||
{ | ||
color: '#2CB34A', | ||
value: Date.UTC( 2016, 4, 1 ) | ||
}, | ||
{ | ||
color: '#addc91', | ||
} | ||
] | ||
} ] | ||
} | ||
// calculated variables | ||
var width = baseWidth - margin.left - margin.right, | ||
height = baseHeight - margin.top - margin.bottom; | ||
// @todo - add time interval handling | ||
var x = d3.scaleBand() | ||
.range( [ 0, width ] ) | ||
.padding( paddingDecimal ); | ||
var y = d3.scaleLinear() | ||
.range( [ height, 0 ] ); | ||
var svg = d3.select( this.selector ) | ||
.append( 'svg' ) | ||
.attr( 'width', width + margin.left + margin.right) | ||
.attr( 'height', height + margin.top + margin.bottom) | ||
.append( 'g' ) | ||
.attr( 'transform', | ||
'translate( ' + margin.left + ',' + margin.top + ' )' ); | ||
data.forEach( function( d ) { | ||
d.label = d.label; | ||
d.amount = +d.amount; | ||
} ); | ||
var ymin = d3.min( data, function(d) { return d.amount } ), | ||
ymax = d3.max( data, function(d) { return d.amount; } ); | ||
// If the graph displays values below zero, find new bounds by | ||
// rounding to the absolute greatest factor of 10 and set that | ||
// as the min and max | ||
if ( ymin < 0 ) { | ||
var top = Math.ceil( Math.abs( ymax ) / 10 ) * 10, | ||
bottom = Math.ceil( Math.abs( ymin ) / 10 ) * 10, | ||
max = Math.max( top, bottom ); | ||
ymax = max; | ||
ymin = -1 * max; | ||
} | ||
x.domain( data.map( function( d ) { return d.label; } ) ); | ||
y.domain( [ Math.min( 0, ymin ), ymax ] ); | ||
// x-axis | ||
var xAxis = svg.append( 'g' ) | ||
.attr( 'class', 'x axis' ) | ||
.attr( 'transform', 'translate( 0,' + Math.max( y( 0 ), y( ymin ) ) + ')' ) | ||
.call( | ||
d3.axisBottom( x ) | ||
.tickValues( x.domain().filter( | ||
function( d, i ) { | ||
return !( i % 12 ); | ||
} ) | ||
) | ||
.tickFormat( function( d, i ) { return ' '} ) | ||
); | ||
// @todo - this must be customizable! | ||
xAxis.selectAll( 'g' ) | ||
.append( 'text' ) | ||
.style( 'text-anchor', 'middle' ) | ||
.attr( 'y', 25 ) | ||
.text( function( d ) { return 'Jan' } ) | ||
.attr( 'width', width / 15 ); | ||
xAxis.selectAll( 'g' ) | ||
.append( 'text' ) | ||
.style( 'text-anchor', 'middle' ) | ||
.attr( 'y', 45 ) | ||
.text( function( d ) { return d.substr( 0, 4 ) } ) | ||
.attr( 'width', width / 15 ); | ||
// Determine y-axis tick interval | ||
var yTickInterval = 10; | ||
if ( ymax > 130 ) { | ||
yTickInterval = 20; | ||
} | ||
// y-axis | ||
svg.append( 'g' ) | ||
.attr( 'class', 'y axis') | ||
.call( | ||
d3.axisLeft( y ) | ||
.ticks( ( ymax - ymin ) / yTickInterval ) | ||
.tickSize( -width ) | ||
.tickFormat( function( d ) { | ||
if ( ymax <= 40 || d % ( 2 * yTickInterval ) === 0 ) { | ||
return d + yAxisUnit; | ||
} | ||
} ) | ||
) | ||
.selectAll( 'text' ) | ||
.attr( 'dy', '.25em' ); | ||
// y-axis label | ||
svg.append( 'text' ) | ||
.attr( 'transform', 'rotate(-90)' ) | ||
.attr( 'text-anchor', 'middle' ) | ||
.attr( 'x', -1 * ( height + ymin ) / 2 ) | ||
.attr( 'y', -50 ) | ||
.attr( 'class', 'y-axis-label' ) | ||
.style( 'font-size', '1em' ) | ||
.text( this.labels.yAxisLabel || '' ); | ||
svg.selectAll( 'bar' ) | ||
.data( data ) | ||
.enter().append( 'rect' ) | ||
.attr( 'class', 'bar' ) | ||
.attr( 'x', function(d) { return x( d.label ); }) | ||
.attr( 'width' , x.bandwidth() ) | ||
.attr( 'y', function( d ) { return y( Math.max( 0, d.amount ) ); }) | ||
.attr( 'height', | ||
function( d ) { | ||
return Math.abs( y( d.amount ) - y( 0 ) ); | ||
} ) | ||
.attr( 'fill', '#2CB34A' ); | ||
return { | ||
chart: svg, | ||
x: x, | ||
y: y | ||
} | ||
} | ||
Highcharts.stockChart( props.selector, options); | ||
} | ||
module.exports = BarChart; |
'use strict'; | ||
var d3 = require( 'd3' ); | ||
var CFPBChart = require( './CFPBChart' ); | ||
var getMonth = d3.utcFormat( '%b' ); | ||
var getYear = d3.utcFormat( '%Y' ); | ||
var Highcharts = require( 'highcharts/highstock' ); | ||
LineChart.prototype = new CFPBChart(); | ||
LineChart.prototype.constructor = LineChart; | ||
var lineSets = [], | ||
rawData = [], | ||
yAxisTickFactor, | ||
yAxisLabel, | ||
yAxisUnit, | ||
labels = {}; | ||
function sortByDateAscending( a, b ) { | ||
return a.x - b.x; | ||
} | ||
function findNewMax( max ) { | ||
var newMax = 0; | ||
if ( max < 50 ) { | ||
newMax = 5 * Math.ceil( max / 5 ); | ||
} else if ( max > 100 && max < 500 ) { | ||
newMax = 50 * Math.ceil( max / 50 ); | ||
} else { | ||
var pow = max.toString().length - 1; | ||
newMax = Math.pow( 10, pow ) * Math.ceil( max / Math.pow( 10, pow ) ); | ||
Highcharts.setOptions({ | ||
lang: { | ||
rangeSelectorZoom: '', | ||
thousandsSep: ',' | ||
} | ||
}); | ||
return newMax; | ||
} | ||
function tickThinker( ymin, ymax, factor ) { | ||
var acceptable = [ .5, .25, .2, .1, .05, .025, .01, | ||
1, 2, 5, 10, 15, 20, 25, 50, 100, 200 ]; | ||
// var divisors = [ .5, .25, .2, .1, .05, .025, .01 ]; | ||
var range = Math.ceil( ymax - ymin ); | ||
var count = Math.floor( range / factor ); | ||
var coeff = range / ( factor * count ); | ||
for (var count = 10; count >= 5; count-- ) { | ||
coeff = range / ( factor * count ); | ||
if ( range % count === 0 && acceptable.indexOf( coeff ) !== -1 ) { | ||
break; | ||
} | ||
/** | ||
* _getTickValue - Convert the data point's unit to M or B. | ||
* | ||
* @param {int} value Data point's value | ||
* @return {int} Data point's value over million or billion. | ||
*/ | ||
function _getTickValue( value ) { | ||
// If it's 0 or borked data gets passed in, return it. | ||
if ( !value ) { | ||
return value; | ||
} | ||
var array = []; | ||
for ( var x = 0; x <= count; x++ ) { | ||
array.push( x * coeff * factor ); | ||
if ( value % 1000000000 < value ) { | ||
return value / 1000000000 + 'B'; | ||
} | ||
return { | ||
valueArray: array, | ||
coefficient: coeff | ||
}; | ||
return value / 1000000 + 'M'; | ||
} | ||
function labelToString( number, multiplier ) { | ||
var label = number.toString(); | ||
if ( multiplier < 1 ) { | ||
if ( label.length > 4 ) { | ||
label = label.substr( 0, 4 ); | ||
if ( label.substr( 3, 1 ) === '0' ) { | ||
label = label.substr( 0, 3 ); | ||
function LineChart( props ) { | ||
var options = { | ||
title: { | ||
text: props.title | ||
}, | ||
description: props.description, | ||
credits: false, | ||
rangeSelector : { | ||
inputEnabled:false | ||
}, | ||
plotOptions: { | ||
series: { | ||
states: { | ||
hover: { | ||
enabled: false | ||
} | ||
} | ||
} | ||
} | ||
} else if ( label.indexOf('.') !== -1 ) { | ||
var decPart = label.split('.')[1]; | ||
var intPart = label.split('.')[0]; | ||
decPart = decPart.substr( 0, 1 ) | ||
label = intPart + '.' + decPart; | ||
}, | ||
navigator: { | ||
maskFill: 'rgba(0, 0, 0, 0.05)', | ||
handles: { | ||
backgroundColor: '#fff', | ||
borderColor: '#000' | ||
}, | ||
series: { | ||
color: '#addc91', | ||
lineWidth: 2 | ||
} | ||
}, | ||
chart: { | ||
width: 650, | ||
height: 500 | ||
}, | ||
xAxis: { | ||
startOnTick: true, | ||
type: 'datetime', | ||
dateTimeLabelFormats: { | ||
day: '%b %Y' | ||
}, | ||
tickInterval: 60 * 60 * 24 * 365 * 1000 // one year in ms | ||
}, | ||
yAxis: { | ||
opposite: false, | ||
className: 'axis-label', | ||
title: { | ||
text: props.title | ||
}, | ||
labels: { | ||
formatter: function () { | ||
return _getTickValue( this.value ); | ||
} | ||
} | ||
}, | ||
series: [ | ||
{ | ||
name: 'Unadjusted', | ||
data: props.data.unadjusted, | ||
color: '#addc91', | ||
lineWidth: 3, | ||
tooltip: { | ||
valueDecimals: 0 | ||
} | ||
}, | ||
{ | ||
name: 'Seasonally Adjusted', | ||
data: props.data.adjusted, | ||
color: '#20aa3f', | ||
lineWidth: 3, | ||
tooltip: { | ||
valueDecimals: 0 | ||
} | ||
} | ||
] | ||
} | ||
return label; | ||
} | ||
Highcharts.stockChart( props.selector, options ); | ||
function LineChart( properties ) { | ||
this.selector = properties.selector; | ||
this.type = 'LineChart'; | ||
this.data = {}; | ||
rawData = properties.data; | ||
labels = properties.labels || {}; | ||
lineSets = properties.lineSets || undefined; | ||
yAxisTickFactor = properties.yAxisTickFactor || 1; | ||
yAxisLabel = properties.labels.yAxisLabel || ''; | ||
yAxisUnit = properties.labels.yAxisUnit || ''; | ||
this.drawGraph = function( options ) { | ||
var data = this.data = this.getDataBySets(); | ||
// variables from options | ||
var baseWidth = options.baseWidth || 200, | ||
baseHeight = options.baseHeight || 100, | ||
paddingDecimal = options.paddingDecimal || .1, | ||
margin = options.margin || {top: 20, right: 20, bottom: 20, left: 20}; | ||
// calculated variables | ||
var width = baseWidth - margin.left - margin.right, | ||
height = baseHeight - margin.top - margin.bottom; | ||
// @todo: the x-axis is not always time intervals | ||
var x = d3.scaleTime() | ||
.range( [ 0, width ] ); | ||
var y = d3.scaleLinear() | ||
.range( [ height, 0 ] ); | ||
var xmin = d3.min( rawData, function(d) { return d.x } ), | ||
xmax = d3.max( rawData, function(d) { return d.x; } ), | ||
ymin = d3.min( rawData, function(d) { return d.y } ), | ||
ymax = d3.max( rawData, function(d) { return d.y; } ); | ||
// ymin should be 0 or less | ||
ymin = Math.min( ymin, 0 ); | ||
// ymax should be "niced" (made into a nice round number) | ||
ymax = findNewMax( ymax ); | ||
// check if the yAxisTickFactor is ideal | ||
var bestTickFactors = tickThinker( ymin, ymax, yAxisTickFactor ); | ||
var tickMultiplier = bestTickFactors.coefficient; | ||
var tickValueArray = bestTickFactors.valueArray; | ||
x.domain( [ xmin, xmax ] ); | ||
y.domain( [ ymin, ymax ] ); | ||
var svg = d3.select( this.selector ) | ||
.append( 'svg' ) | ||
.attr( 'width', width + margin.left + margin.right) | ||
.attr( 'height', height + margin.top + margin.bottom) | ||
.append( 'g' ) | ||
.attr( 'transform', | ||
'translate( ' + margin.left + ',' + margin.top + ' )' ); | ||
// line function | ||
var line = d3.line() | ||
.x( function( d ) { | ||
return x( d.x ); | ||
} ) | ||
.y( function( d ) { | ||
return Math.floor( y( d.y ) ); | ||
} ); | ||
// Add the X Axis | ||
var xAxis = svg.append('g') | ||
.classed('axis axis__x', true) | ||
.attr('transform', 'translate(0,' + height + ')') | ||
.call( d3.axisBottom( x ) | ||
.tickFormat( function( d ) { return ''; } ) | ||
); | ||
xAxis.selectAll( 'g' ) | ||
.append( 'text' ) | ||
.style( 'text-anchor', 'middle' ) | ||
.attr( 'y', 25 ) | ||
.text( function( d) { return getMonth( d ); } ) | ||
.attr( 'width', width / 15 ); | ||
xAxis.selectAll( 'g' ) | ||
.append( 'text' ) | ||
.style( 'text-anchor', 'middle' ) | ||
.attr( 'y', 45 ) | ||
.text( function( d ) { return getYear( d ); } ) | ||
.attr( 'width', width / 15 ); | ||
// Add the Y Axis | ||
var yAxis = d3.axisLeft( y ); | ||
yAxis.tickValues( tickValueArray ) | ||
yAxis.tickFormat( function( d ) { | ||
var label = d / yAxisTickFactor; | ||
label = labelToString( label, tickMultiplier ); | ||
return label + yAxisUnit; | ||
} ) | ||
yAxis.tickSize( -width ); | ||
svg.append( 'g' ) | ||
.classed( 'axis axis__y', true ) | ||
.call( yAxis ); | ||
// Iterate all lines: | ||
for ( var key in data ) { | ||
svg.append( 'path' ) | ||
.attr( 'd', line( data[key] ) ) | ||
.classed( lineSets[key].classes, true); | ||
} | ||
// text label for the y axis | ||
svg.append( 'text' ) | ||
.classed( 'axis-label' , true) | ||
.attr( 'transform', 'rotate(-90)' ) | ||
.attr( 'text-anchor', 'end' ) | ||
.attr( 'x', -20 ) | ||
.attr( 'y', -60 ) | ||
.text( labels.yAxisLabel ); | ||
// add the legend | ||
var legendPositions = [ | ||
[ -70, -65 ], | ||
[ -70, -45 ], | ||
[ width / 4, -55 ], | ||
[ width / 4, -35 ] | ||
]; | ||
for ( var key in lineSets ) { | ||
if ( lineSets[key].showInLegend !== false ) { | ||
var pos = legendPositions[0]; | ||
svg.append( 'line' ) | ||
.classed( lineSets[key].classes, true) | ||
.style( 'stroke-width', '10px' ) | ||
.attr( 'x1', pos[0] ) | ||
.attr( 'x2', pos[0] + 10 ) | ||
.attr( 'y1', pos[1] ) | ||
.attr( 'y2', pos[1] ); | ||
svg.append( 'text' ) | ||
.attr( 'text-anchor', 'start' ) | ||
.attr( 'x', pos[0] + 20 ) | ||
.attr( 'y', pos[1] + 5 ) | ||
.attr( 'class', 'gray-text' ) | ||
.text( lineSets[key].legendLabel || key ); | ||
// Drop the first position so the next entry will | ||
// use the next position, etc | ||
legendPositions = legendPositions.splice( 1 ); | ||
} | ||
} | ||
return { | ||
chart: svg, | ||
x: x, | ||
y: y | ||
}; | ||
}; | ||
this.getDataBySets = function() { | ||
var obj = {}; | ||
// create an object property for each set | ||
for ( var key in lineSets ) { | ||
obj[key] = []; | ||
} | ||
for (var x = 0; x < rawData.length; x++ ) { | ||
obj[rawData[x].set].push( rawData[x] ); | ||
} | ||
for ( var key in lineSets ) { | ||
obj[key] = obj[key].sort( sortByDateAscending ); | ||
} | ||
return obj; | ||
} | ||
} | ||
module.exports = LineChart; |
'use strict'; | ||
var d3 = require( 'd3' ); | ||
var CFPBChart = require( './CFPBChart' ); | ||
var stateCoords = require( '../utils/state-tile-coords' ); | ||
var fillByValue = require( '../utils/fill-by-value' ); | ||
var valueGrid, | ||
legendLabels; | ||
var Highcharts = require('highcharts/highmaps'); | ||
TileMap.prototype = new CFPBChart(); | ||
TileMap.prototype.constructor = TileMap; | ||
Highcharts.setOptions({ | ||
lang: { | ||
thousandsSep: ',' | ||
} | ||
}); | ||
function TileMap( properties ) { | ||
this.selector = properties.selector; | ||
this.data = properties.data; | ||
this.type = 'TileMap'; | ||
valueGrid = properties.valueGrid || []; | ||
legendLabels = properties.legendLabels || []; | ||
function TileMap(props) { | ||
var props = props || {}; | ||
this.drawGraph = function( options ) { | ||
var data = this.data; | ||
var options = { | ||
title: { | ||
text: props.title | ||
}, | ||
chart: { | ||
width: 650, | ||
height: 500 | ||
}, | ||
description: props.description, | ||
credits: false, | ||
legend: { | ||
enabled: false | ||
}, | ||
tooltip: { | ||
borderColor: 'rgb(117, 120, 123)', | ||
formatter: function() { | ||
return this.point.tooltip; | ||
} | ||
}, | ||
series: [{ | ||
type: 'map', | ||
borderColor: 'rgb(117, 120, 123)', | ||
states: { | ||
hover: { | ||
brightness: 0, | ||
borderColor: '#000' | ||
} | ||
}, | ||
// borderWidth: 0.2, | ||
dataLabels: { | ||
enabled: true, | ||
color: '#FFFFFF', | ||
format: '{point.name}<br />{point.value}%', | ||
style: '' | ||
}, | ||
name: props.title, | ||
data: props.data | ||
}] | ||
}; | ||
// variables from options | ||
var baseWidth = options.baseWidth || 200, | ||
baseHeight = options.baseHeight || 100, | ||
paddingDecimal = options.paddingDecimal || .1, | ||
margin = options.margin || {top: 20, right: 20, bottom: 20, left: 20}; | ||
// calculated variables | ||
var width = baseWidth - margin.left - margin.right, | ||
height = baseHeight - margin.top - margin.bottom, | ||
tileGutterWidth = Math.floor( width / 11 ), | ||
tileWidth = Math.floor( tileGutterWidth - 2 - tileGutterWidth / 20 ); | ||
var svg = d3.select( this.selector ) | ||
.append( 'svg' ) | ||
.attr( 'width', width + margin.left + margin.right) | ||
.attr( 'height', height + margin.top + margin.bottom) | ||
.append( 'g' ) | ||
.classed( 'tiles' , true ) | ||
.attr( 'transform', | ||
'translate( ' + margin.left + ',' + margin.top + ' )' ); | ||
// Add legend group | ||
svg.append( 'g' ) | ||
.classed( 'legend' , true ); | ||
var legend = svg.selectAll('.legend'); | ||
var tiles = svg.selectAll( '.tiles' ) | ||
.data( data ) | ||
.enter(); | ||
tiles.append( 'rect' ) | ||
.filter( function( d ) { return stateCoords( d.state ) !== false; } ) | ||
.attr( 'x', function( d ) { | ||
return stateCoords( d.state )[0] * tileGutterWidth; | ||
} ) | ||
.attr( 'y', function( d ) { | ||
return stateCoords(d.state)[1] * tileGutterWidth + 60; | ||
} ) | ||
.attr( 'width', tileWidth ) | ||
.attr( 'height', tileWidth ) | ||
.style( 'fill', function( d ) { | ||
return fillByValue( d.value, valueGrid ) | ||
} ) | ||
.style( 'stroke', '#75787B'); | ||
tiles.append( 'text' ) | ||
.filter( function( d ) { return stateCoords( d.state ) !== false; } ) | ||
.attr( 'x', function( d ) { | ||
var x = stateCoords(d.state)[0] * tileGutterWidth; | ||
x += .5 * tileWidth; | ||
return x; | ||
} ) | ||
.attr( 'y', function( d ) { | ||
var y = stateCoords(d.state)[1] * tileGutterWidth + 60; | ||
y += tileWidth * .4; | ||
return y; | ||
} ) | ||
.attr( 'width', tileWidth ) | ||
.attr( 'height', tileWidth ) | ||
.attr( 'class', 'state-abbreviation' ) | ||
.style( 'font-size', tileWidth * .25 + 'px' ) | ||
.style( 'text-anchor', 'middle' ) | ||
.text( function( d ) { return d.state; } ); | ||
tiles.append( 'text' ) | ||
.filter( function( d ) { return stateCoords( d.state ) !== false; } ) | ||
.attr( 'x', function( d ) { | ||
var x = stateCoords(d.state)[0] * tileGutterWidth; | ||
x += .5 * tileWidth; | ||
return x; | ||
} ) | ||
.attr( 'y', function( d ) { | ||
var y = stateCoords(d.state)[1] * tileGutterWidth + 60; | ||
y += tileWidth * .8; | ||
return y; | ||
} ) | ||
.attr( 'width', tileWidth ) | ||
.attr( 'height', tileWidth ) | ||
.style( 'font-size', tileWidth * .25 + 'px' ) | ||
.style( 'text-anchor', 'middle' ) | ||
.text( function( d ) { | ||
var val = Math.round( d.value * 100 ); | ||
return val + '%'; | ||
} ); | ||
// draw the legend, rectangles | ||
for ( var x = 0; x < valueGrid.length; x++ ) { | ||
legend.append( 'rect' ) | ||
.attr( 'x', x * tileWidth + 15 ) | ||
.attr( 'y', 0 ) | ||
.attr( 'width', tileWidth ) | ||
.attr( 'height', 10 ) | ||
.attr( 'fill', valueGrid[x].fillColor ) | ||
.style( 'stroke', '#75787B'); | ||
} | ||
// draw the legend, text labels | ||
for ( var x = 0; x < legendLabels.length; x++ ) { | ||
legend.append( 'text' ) | ||
.attr( 'x', x * tileWidth + 15 ) | ||
.attr( 'y', 20 + tileWidth * .125 ) | ||
.attr( 'width', tileWidth ) | ||
.attr( 'height', 30 ) | ||
.style( 'font-size', tileWidth * .25 + 'px' ) | ||
.style( 'text-anchor', 'middle' ) | ||
.style( 'fill', '#75787B') | ||
.text( legendLabels[x] ) | ||
} | ||
return svg; | ||
} | ||
Highcharts.mapChart(props.selector, options); | ||
} | ||
module.exports = TileMap; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Install scripts
Supply chain riskInstall scripts are run when the package is installed. The majority of malware in npm is hidden in install scripts.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
1132248
30
7783
0
3
10
73
1
12
4
+ Addedhighcharts@5.0.7
+ Addedpapaparse@4.1.2
+ Addedxdr@^0.5.3
+ Addedhighcharts@5.0.7(transitive)
+ Addedpapaparse@4.1.2(transitive)
+ Addedxdr@0.5.3(transitive)
- Removedd3@4.2.7
- Removedcommander@2.20.3(transitive)
- Removedd3@4.2.7(transitive)
- Removedd3-array@1.0.1(transitive)
- Removedd3-axis@1.0.3(transitive)
- Removedd3-brush@1.0.3(transitive)
- Removedd3-chord@1.0.2(transitive)
- Removedd3-collection@1.0.1(transitive)
- Removedd3-color@1.0.1(transitive)
- Removedd3-dispatch@1.0.1(transitive)
- Removedd3-drag@1.0.1(transitive)
- Removedd3-dsv@1.0.3(transitive)
- Removedd3-ease@1.0.1(transitive)
- Removedd3-force@1.0.3(transitive)
- Removedd3-format@1.0.2(transitive)
- Removedd3-geo@1.2.6(transitive)
- Removedd3-hierarchy@1.0.2(transitive)
- Removedd3-interpolate@1.1.1(transitive)
- Removedd3-path@1.0.2(transitive)
- Removedd3-polygon@1.0.1(transitive)
- Removedd3-quadtree@1.0.1(transitive)
- Removedd3-queue@3.0.3(transitive)
- Removedd3-random@1.0.1(transitive)
- Removedd3-request@1.0.2(transitive)
- Removedd3-scale@1.0.3(transitive)
- Removedd3-selection@1.0.2(transitive)
- Removedd3-shape@1.0.3(transitive)
- Removedd3-time@1.0.4(transitive)
- Removedd3-time-format@2.0.2(transitive)
- Removedd3-timer@1.0.3(transitive)
- Removedd3-transition@1.0.2(transitive)
- Removedd3-voronoi@1.0.2(transitive)
- Removedd3-zoom@1.0.3(transitive)
- Removediconv-lite@0.4.24(transitive)
- Removedrw@1.3.3(transitive)
- Removedsafer-buffer@2.1.2(transitive)
- Removedxmlhttprequest@1.8.0(transitive)