Merge pull request woocommerce/woocommerce-admin#350 from woocommerce/fix/chart-design-feedback

Merging this.

@LevinMedia please continue to post additional issues or comments on the master thread woocommerce/woocommerce-admin#164
This commit is contained in:
Robert Elliott 2018-09-07 14:54:58 +02:00 committed by GitHub
commit 74514a2b48
9 changed files with 241 additions and 281 deletions

View File

@ -97,6 +97,7 @@ class D3Chart extends Component {
tooltipFormat,
type,
xFormat,
x2Format,
yFormat,
} = this.props;
const { width } = this.state;
@ -128,6 +129,7 @@ class D3Chart extends Component {
uniqueKeys,
width: calculatedWidth,
xFormat: d3TimeFormat( xFormat ),
x2Format: d3TimeFormat( x2Format ),
xGroupScale: getXGroupScale( orderedKeys, xScale ),
xLineScale,
xScale,
@ -210,6 +212,10 @@ D3Chart.propTypes = {
* A datetime formatting string, passed to d3TimeFormat.
*/
xFormat: PropTypes.string,
/**
* A datetime formatting string, passed to d3TimeFormat.
*/
x2Format: PropTypes.string,
/**
* A number formatting string, passed to d3Format.
*/
@ -229,6 +235,7 @@ D3Chart.defaultProps = {
type: 'line',
width: 600,
xFormat: '%Y-%m-%d',
x2Format: '',
yFormat: '.3s',
};

View File

@ -84,16 +84,24 @@ class Chart extends Component {
...d,
visible: d.key === event.target.id ? ! d.visible : d.visible,
} ) );
this.setState( {
orderedKeys,
visibleData: this.getVisibleData( data, orderedKeys ),
} );
const copyEvent = { ...event }; // can't pass a synthetic event into the hover handler
this.setState(
{
orderedKeys,
visibleData: this.getVisibleData( data, orderedKeys ),
},
() => {
this.handleLegendHover( copyEvent );
}
);
}
handleLegendHover( event ) {
const hoverTarget = this.state.orderedKeys.filter( d => d.key === event.target.id )[ 0 ];
this.setState( {
orderedKeys: this.state.orderedKeys.map( d => {
const enterFocus = d.key === event.target.id ? true : false;
let enterFocus = d.key === event.target.id ? true : false;
enterFocus = ! hoverTarget.visible ? true : enterFocus;
return {
...d,
focus: event.type === 'mouseleave' || event.type === 'blur' ? true : enterFocus,
@ -135,8 +143,8 @@ class Chart extends Component {
);
const margin = {
bottom: 50,
left: 50,
right: 10,
left: 80,
right: 30,
top: 0,
};
return (
@ -162,6 +170,7 @@ class Chart extends Component {
type={ this.props.type }
width={ chartDirection === 'row' ? width - 320 : width }
xFormat={ this.props.xFormat }
x2Format={ this.props.x2Format }
yFormat={ this.props.yFormat }
/>
</div>
@ -192,6 +201,10 @@ Chart.propTypes = {
* A datetime formatting string, passed to d3TimeFormat.
*/
xFormat: PropTypes.string,
/**
* A datetime formatting string, passed to d3TimeFormat.
*/
x2Format: PropTypes.string,
/**
* A number formatting string, passed to d3Format.
*/
@ -201,8 +214,9 @@ Chart.propTypes = {
Chart.defaultProps = {
data: [],
tooltipFormat: '%Y-%m-%d',
xFormat: '%Y-%m-%d',
yFormat: '.3s',
xFormat: '%d',
x2Format: '%b %Y',
yFormat: '$.3s',
};
export default Chart;

View File

@ -39,7 +39,9 @@ class Legend extends Component {
>
{ data.map( row => (
<li
className="woocommerce-legend__item"
className={ classNames( 'woocommerce-legend__item', {
'woocommerce-legend__item-checked': row.visible,
} ) }
key={ row.key }
id={ row.key }
onMouseEnter={ handleLegendHover }

View File

@ -5,16 +5,18 @@
justify-content: flex-start;
align-items: flex-start;
margin: -$gap;
border-top: 1px solid $core-grey-light-200;
border-top: 1px solid $core-grey-light-700;
.woocommerce-chart__header {
min-height: 50px;
border-top: 1px solid $core-grey-light-200;
border-top: 1px solid $core-grey-light-700;
border-bottom: 1px solid $core-grey-light-700;
display: flex;
flex-wrap: nowrap;
flex-direction: row;
justify-content: flex-start;
align-items: center;
width: 100%;
.woocommerce-chart__title {
height: 18px;
@ -23,6 +25,7 @@
font-weight: 600;
line-height: 18px;
margin-left: $gap;
margin-right: $gap;
}
}
@ -45,41 +48,41 @@
&.woocommerce-legend__direction-column {
height: 100%;
min-height: none;
border-right: none;
margin-bottom: $gap;
}
}
}
.key-colour {
width: 10px;
height: 10px;
margin-right: 8px;
border-radius: 2px;
}
.key-key {
margin-right: 6px;
font-weight: 600;
}
svg {
overflow: visible;
}
.tooltip {
border: 1px solid $core-grey-light-700;
position: absolute;
display: none;
min-width: 80px;
min-width: 324px;
height: auto;
background-color: $white;
text-align: left;
padding: 6px;
box-shadow: 0 4px 8px 0 $gray-darken-30, 0 6px 20px 0 $gray-darken-30;
padding: 17px;
box-shadow: 0 3px 20px 0 rgba(18, 24, 30, 0.1), 0 1px 3px 0 rgba(18, 24, 30, 0.1);
flex-direction: column;
flex-wrap: nowrap;
justify-content: flex-start;
h4 {
text-align: center;
text-align: left;
line-height: 18px;
width: 100%;
margin: 0 auto;
text-transform: uppercase;
font-size: 11px;
color: $core-grey-dark-100;
opacity: 0.6;
margin-top: 0;
}
ul {
padding-left: 7px;
list-style: none;
margin-bottom: 2px;
margin-top: 2px;
@ -91,6 +94,31 @@
flex-wrap: nowrap;
justify-content: flex-start;
align-items: center;
&.key-row {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
.key-container {
width: 100%;
min-width: 100px;
.key-color {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 8px;
}
.key-key {
margin-right: 6px;
}
}
.key-value {
font-weight: 600;
}
}
}
}
}
@ -100,17 +128,40 @@
}
}
.grid {
line {
stroke: $core-grey-light-200;
stroke-width: 1;
shape-rendering: crispEdges;
.tick {
line {
stroke: $core-grey-light-500;
stroke-width: 1;
shape-rendering: crispEdges;
}
&:first-child {
line {
stroke: $core-grey-dark-500;
}
}
&:last-child {
line {
opacity: 0;
}
}
}
}
.tick {
padding-top: 10px;
stroke-width: 1;
}
.axis-month {
.tick {
opacity: 0;
&:first-child {
opacity: 1;
}
}
}
.y-axis {
text-anchor: start;
&.tick {
&text {
fill: $core-grey-dark-500;
@ -119,7 +170,7 @@
}
line {
&.focus-grid {
stroke: $core-grey-light-200;
stroke: $core-grey-light-700;
stroke-width: 1px;
}
}
@ -153,14 +204,18 @@
&.woocommerce-legend__direction-column {
flex-direction: column;
border-right: 1px solid $core-grey-light-200;
border-top: 1px solid $core-grey-light-200;
border-right: 1px solid $core-grey-light-700;
height: 300px;
min-width: 320px;
li {
height: 32px;
margin: 0 17px;
margin: 0;
padding: 0;
button {
height: 32px;
padding: 0 17px;
}
&:first-child {
margin-top: 17px;
@ -172,9 +227,12 @@
flex-direction: row;
li {
margin: 0 0 0 30px;
padding: 0;
margin: 0;
button {
padding: 0 17px;
.woocommerce-legend__item-container {
height: 50px;
align-items: center;
@ -192,6 +250,14 @@
}
li {
&.woocommerce-legend__item {
button {
&:hover {
background-color: $core-grey-light-100;
}
}
}
button {
background-color: $white;
display: inline-flex;

View File

@ -157,7 +157,7 @@ describe( 'getXLineScale', () => {
describe( 'getYMax', () => {
it( 'calculate the correct maximum y value', () => {
expect( testYMax ).toEqual( 14139347 );
expect( testYMax ).toEqual( 15000000 );
} );
} );

View File

@ -4,10 +4,10 @@
* External dependencies
*/
import { findIndex } from 'lodash';
import { findIndex, get } from 'lodash';
import { max as d3Max } from 'd3-array';
import { axisBottom as d3AxisBottom, axisLeft as d3AxisLeft } from 'd3-axis';
import { format as d3Format } from 'd3-format';
import { format as d3Format, formatDefaultLocale as d3FormatDefaultLocale } from 'd3-format';
import {
scaleBand as d3ScaleBand,
scaleLinear as d3ScaleLinear,
@ -16,9 +16,23 @@ import {
import { mouse as d3Mouse, select as d3Select } from 'd3-selection';
import { line as d3Line } from 'd3-shape';
import { utcParse as d3UTCParse } from 'd3-time-format';
/**
* Internal dependencies
*/
import { formatCurrency } from 'lib/currency';
export const parseDate = d3UTCParse( '%Y-%m-%dT%H:%M:%S' );
function decodeSymbol( str ) {
return str.replace( /&#(\d+);/g, ( match, dec ) => String.fromCharCode( dec ) );
}
d3FormatDefaultLocale( {
decimal: '.',
thousands: ',',
grouping: [ 3 ],
currency: [ decodeSymbol( get( wcSettings, 'currency.symbol', '' ) ), '' ],
} );
/**
* Describes `getUniqueKeys`
* @param {array} data - The chart component's `data` prop.
@ -130,12 +144,15 @@ export const getXLineScale = ( uniqueDates, width ) =>
.rangeRound( [ 0, width ] );
/**
* Describes getYMax
* Describes and rounds the maximum y value to the nearest thousadn, ten-thousand, million etc.
* @param {array} lineData - from `getLineData`
* @returns {number} the maximum value in the timeseries multiplied by 4/3
*/
export const getYMax = lineData =>
Math.round( 4 / 3 * d3Max( lineData, d => d3Max( d.values.map( date => date.value ) ) ) );
export const getYMax = lineData => {
const yMax = 4 / 3 * d3Max( lineData, d => d3Max( d.values.map( date => date.value ) ) );
const pow3Y = Math.pow( 10, ( ( Math.log( yMax ) * Math.LOG10E + 1 ) | 0 ) - 2 ) * 3;
return Math.ceil( yMax / pow3Y ) * pow3Y;
};
/**
* Describes getYScale
@ -217,6 +234,32 @@ export const drawAxis = ( node, params ) => {
.tickFormat( d => params.xFormat( d instanceof Date ? d : new Date( d ) ) )
);
node
.append( 'g' )
.attr( 'class', 'axis axis-month' )
.attr( 'transform', `translate(3, ${ params.height + 20 })` )
.call(
d3AxisBottom( xScale )
.tickValues( params.uniqueDates.map( d => ( params.type === 'line' ? new Date( d ) : d ) ) )
.tickFormat( d => params.x2Format( d instanceof Date ? d : new Date( d ) ) )
)
.call( g => g.select( '.domain' ).remove() );
node
.selectAll( '.axis-month .tick text' )
.style( 'font-size', `${ Math.round( params.scale * 10 ) }px` );
node
.append( 'g' )
.attr( 'class', 'pipes' )
.attr( 'transform', `translate(0, ${ params.height })` )
.call(
d3AxisBottom( xScale )
.tickValues( params.uniqueDates.map( d => ( params.type === 'line' ? new Date( d ) : d ) ) )
.tickSize( 5 )
.tickFormat( '' )
);
node
.append( 'g' )
.attr( 'class', 'grid' )
@ -224,7 +267,7 @@ export const drawAxis = ( node, params ) => {
.call(
d3AxisLeft( params.yScale )
.tickValues( yGrids )
.tickSize( -params.width - params.margin.left )
.tickSize( -params.width - params.margin.left - params.margin.right )
.tickFormat( '' )
)
.call( g => g.select( '.domain' ).remove() );
@ -232,10 +275,12 @@ export const drawAxis = ( node, params ) => {
node
.append( 'g' )
.attr( 'class', 'axis y-axis' )
.attr( 'transform', 'translate(-50, 0)' )
.attr( 'text-anchor', 'start' )
.call(
d3AxisLeft( params.yTickOffset )
.tickValues( yGrids )
.tickFormat( d => ( d !== 0 ? d3Format( params.yFormat )( d ) : 0 ) )
.tickFormat( d => d3Format( params.yFormat )( d !== 0 ? d : 0 ) )
);
node
@ -253,14 +298,16 @@ export const drawAxis = ( node, params ) => {
const showTooltip = ( node, params, d ) => {
const chartCoords = node.node().getBoundingClientRect();
let [ xPosition, yPosition ] = d3Mouse( node.node() );
xPosition = xPosition > chartCoords.width - 200 ? xPosition - 200 : xPosition + 20;
xPosition = xPosition > chartCoords.width - 340 ? xPosition - 340 : xPosition + 100;
yPosition = yPosition > chartCoords.height - 150 ? yPosition - 200 : yPosition + 20;
const keys = params.orderedKeys.filter( row => row.visible ).map(
row => `
<li>
<span class="key-colour" style="background-color:${ getColor( row.key, params ) }"></span>
<span class="key-key">${ row.key }:</span>
<span class="key-value">${ d3Format( ',.0f' )( d[ row.key ] ) }</span>
<li class="key-row">
<div class="key-container">
<span class="key-color" style="background-color:${ getColor( row.key, params ) }"></span>
<span class="key-key">${ row.key }:</span>
</div>
<span class="key-value">${ formatCurrency( d[ row.key ] ) }</span>
</li>
`
);
@ -268,7 +315,7 @@ const showTooltip = ( node, params, d ) => {
params.tooltip
.style( 'left', xPosition + 'px' )
.style( 'top', yPosition + 'px' )
.style( 'display', 'inline-block' ).html( `
.style( 'display', 'flex' ).html( `
<div>
<h4>${ params.tooltipFormat( d.date instanceof Date ? d.date : new Date( d.date ) ) }</h4>
<ul>
@ -289,7 +336,7 @@ const handleMouseOutBarChart = ( d, i, nodes, params ) => {
d3Select( nodes[ i ].parentNode )
.select( '.barfocus' )
.attr( 'opacity', '0' );
params.tooltip.style( 'display', 'none' );
params.tooltip.style( 'display', 'flex' );
};
const handleMouseOverLineChart = ( d, i, nodes, node, data, params ) => {
@ -307,6 +354,48 @@ const handleMouseOutLineChart = ( d, i, nodes, params ) => {
};
export const drawLines = ( node, data, params ) => {
const series = node
.append( 'g' )
.attr( 'class', 'lines' )
.selectAll( '.line-g' )
.data( params.lineData.filter( d => d.visible ) )
.enter()
.append( 'g' )
.attr( 'class', 'line-g' );
series
.append( 'path' )
.attr( 'fill', 'none' )
.attr( 'stroke-width', 3 )
.attr( 'stroke-linejoin', 'round' )
.attr( 'stroke-linecap', 'round' )
.attr( 'stroke', d => getColor( d.key, params ) )
.style( 'opacity', d => {
const opacity = d.focus ? 1 : 0.1;
return d.visible ? opacity : 0;
} )
.attr( 'd', d => params.line( d.values ) );
series
.selectAll( 'circle' )
.data( ( d, i ) => d.values.map( row => ( { ...row, i, visible: d.visible, key: d.key } ) ) )
.enter()
.append( 'circle' )
.attr( 'r', 6 )
.attr( 'fill', d => getColor( d.key, params ) )
.attr( 'stroke', '#fff' )
.attr( 'stroke-width', 3 )
.style( 'opacity', d => {
const opacity = d.focus ? 1 : 0.1;
return d.visible ? opacity : 0;
} )
.attr( 'cx', d => params.xLineScale( new Date( d.date ) ) )
.attr( 'cy', d => params.yScale( d.value ) )
.on( 'mouseover', ( d, i, nodes ) =>
handleMouseOverLineChart( d, i, nodes, node, data, params )
)
.on( 'mouseout', ( d, i, nodes ) => handleMouseOutLineChart( d, i, nodes, params ) );
const focus = node
.append( 'g' )
.attr( 'class', 'focusspaces' )
@ -337,44 +426,6 @@ export const drawLines = ( node, data, params ) => {
handleMouseOverLineChart( d, i, nodes, node, data, params )
)
.on( 'mouseout', ( d, i, nodes ) => handleMouseOutLineChart( d, i, nodes, params ) );
const series = node
.append( 'g' )
.attr( 'class', 'lines' )
.selectAll( '.line-g' )
.data( params.lineData.filter( d => d.visible ) )
.enter()
.append( 'g' )
.attr( 'class', 'line-g' );
series
.append( 'path' )
.attr( 'fill', 'none' )
.attr( 'stroke-width', 3 )
.attr( 'stroke-linejoin', 'round' )
.attr( 'stroke-linecap', 'round' )
.attr( 'stroke', d => getColor( d.key, params ) )
.style( 'opacity', d => {
const opacity = d.focus ? 1 : 0.1;
return d.visible ? opacity : 0;
} )
.attr( 'd', d => params.line( d.values ) );
series
.selectAll( 'circle' )
.data( ( d, i ) => d.values.map( row => ( { ...row, i, visible: d.visible, key: d.key } ) ) )
.enter()
.append( 'circle' )
.attr( 'r', 3.5 )
.attr( 'fill', '#fff' )
.attr( 'stroke', d => getColor( d.key, params ) )
.attr( 'stroke-width', 3 )
.style( 'opacity', d => {
const opacity = d.focus ? 1 : 0.1;
return d.visible ? opacity : 0;
} )
.attr( 'cx', d => params.xLineScale( new Date( d.date ) ) )
.attr( 'cy', d => params.yScale( d.value ) );
};
export const drawBars = ( node, data, params ) => {
@ -418,7 +469,11 @@ export const drawBars = ( node, data, params ) => {
.style( 'opacity', d => {
const opacity = d.focus ? 1 : 0.1;
return d.visible ? opacity : 0;
} );
} )
.on( 'mouseover', ( d, i, nodes ) =>
handleMouseOverBarChart( d, i, nodes, node, data, params )
)
.on( 'mouseout', ( d, i, nodes ) => handleMouseOutBarChart( d, i, nodes, params ) );
barGroup
.append( 'rect' )

View File

@ -9,7 +9,7 @@ import { Component, Fragment } from '@wordpress/element';
* Internal dependencies
*/
import './style.scss';
import ChartExample from 'components/chart/example-hour';
import ChartExample from 'components/chart/example';
import Header from 'layout/header';
import StorePerformance from './store-performance';
import TopSellingProducts from './top-selling-products';

View File

@ -14,7 +14,6 @@ import { get, isNaN } from 'lodash';
*/
export function formatCurrency( number, currency ) {
const locale = wcSettings.siteLocale || 'en-US'; // Default so we don't break.
// default to wcSettings if currency is not passed in
if ( ! currency ) {
currency = get( wcSettings, 'currency.code', 'USD' );

View File

@ -3240,15 +3240,6 @@
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
"dev": true
},
"boom": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
"integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
"dev": true,
"requires": {
"hoek": "2.x.x"
}
},
"boxen": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz",
@ -4446,15 +4437,6 @@
"which": "^1.2.9"
}
},
"cryptiles": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
"integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=",
"dev": true,
"requires": {
"boom": "2.x.x"
}
},
"crypto-browserify": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@ -8208,18 +8190,6 @@
"minimalistic-assert": "^1.0.1"
}
},
"hawk": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
"integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=",
"dev": true,
"requires": {
"boom": "2.x.x",
"cryptiles": "2.x.x",
"hoek": "2.x.x",
"sntp": "1.x.x"
}
},
"history": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz",
@ -8244,12 +8214,6 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
"hoek": {
"version": "2.16.3",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
"integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
"dev": true
},
"hoist-non-react-statics": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
@ -11060,15 +11024,6 @@
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
"dev": true
},
"json-stable-stringify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
"integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
"dev": true,
"requires": {
"jsonify": "~0.0.0"
}
},
"json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@ -11096,12 +11051,6 @@
"graceful-fs": "^4.1.6"
}
},
"jsonify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
"dev": true
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@ -12503,9 +12452,9 @@
}
},
"node-gyp": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.7.0.tgz",
"integrity": "sha512-qDQE/Ft9xXP6zphwx4sD0t+VhwV7yFaloMpfbL2QnnDZcyaiakWlLdtFGGQfTAwpFHdpbRhRxVhIHN1OKAjgbg==",
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
"integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==",
"dev": true,
"requires": {
"fstream": "^1.0.0",
@ -12515,135 +12464,18 @@
"nopt": "2 || 3",
"npmlog": "0 || 1 || 2 || 3 || 4",
"osenv": "0",
"request": ">=2.9.0 <2.82.0",
"request": "^2.87.0",
"rimraf": "2",
"semver": "~5.3.0",
"tar": "^2.0.0",
"which": "1"
},
"dependencies": {
"ajv": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz",
"integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=",
"dev": true,
"requires": {
"co": "^4.6.0",
"json-stable-stringify": "^1.0.1"
}
},
"assert-plus": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
"integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=",
"dev": true
},
"aws-sign2": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
"integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=",
"dev": true
},
"form-data": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz",
"integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.5",
"mime-types": "^2.1.12"
}
},
"har-schema": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz",
"integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=",
"dev": true
},
"har-validator": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz",
"integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=",
"dev": true,
"requires": {
"ajv": "^4.9.1",
"har-schema": "^1.0.5"
}
},
"http-signature": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
"integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
"dev": true,
"requires": {
"assert-plus": "^0.2.0",
"jsprim": "^1.2.2",
"sshpk": "^1.7.0"
}
},
"performance-now": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
"integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=",
"dev": true
},
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
},
"qs": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz",
"integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=",
"dev": true
},
"request": {
"version": "2.81.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz",
"integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=",
"dev": true,
"requires": {
"aws-sign2": "~0.6.0",
"aws4": "^1.2.1",
"caseless": "~0.12.0",
"combined-stream": "~1.0.5",
"extend": "~3.0.0",
"forever-agent": "~0.6.1",
"form-data": "~2.1.1",
"har-validator": "~4.2.1",
"hawk": "~3.1.3",
"http-signature": "~1.1.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.7",
"oauth-sign": "~0.8.1",
"performance-now": "^0.2.0",
"qs": "~6.4.0",
"safe-buffer": "^5.0.1",
"stringstream": "~0.0.4",
"tough-cookie": "~2.3.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.0.0"
}
},
"semver": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
"dev": true
},
"tough-cookie": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
"integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==",
"dev": true,
"requires": {
"punycode": "^1.4.1"
}
}
}
},
@ -17958,15 +17790,6 @@
}
}
},
"sntp": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
"integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=",
"dev": true,
"requires": {
"hoek": "2.x.x"
}
},
"sort-keys": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
@ -18245,12 +18068,6 @@
"safe-buffer": "~5.1.0"
}
},
"stringstream": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz",
"integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==",
"dev": true
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",