grunt-phantomas
Version:
Grunt plugin for phantomas
813 lines (686 loc) • 25 kB
JavaScript
/* global window, document, d3 */
/*
* grunt-phantomas
* https://github.com/stefanjudis/grunt-phantomas
*
* Copyright (c) 2013 stefan judis
* Licensed under the MIT license.
*/
;( function( d3, window, document ) {
/**
* Helper functions
*/
/**
* Attach eventlistener to given event
*
* THX to John Resig
* http://ejohn.org/projects/flexible-javascript-events/
*
* @param {Object} obj dom element
* @param {String} type event type
* @param {Function} fn event listener
*/
function addEvent( obj, type, fn ) {
if ( obj.attachEvent ) {
obj[ 'e' + type + fn ] = fn;
obj[ type + fn ] = function() {
obj[ 'e' + type + fn ]( window.event );
};
obj.attachEvent( 'on'+type, obj[ type + fn ] );
} else {
obj.addEventListener( type, fn, false );
}
}
/**
* Get parent of dom element with
* given class
*
* @param {Object} el element
* @param {String} className className
* @return {Object} parent element with given class
*/
function getParent( el, className ) {
var parent = null;
var p = el.parentNode;
while ( p !== null ) {
var o = p;
if ( o.classList.contains( className ) ) {
parent = o;
break;
}
p = o.parentNode;
}
return parent; // returns an Array []
}
/**
* draw the fancy line chart
*
* @param {Array} data data
* @param {String} metric metric
* @param {String} type median|max|min|...
* @param {Number|undefined} lastRuns number of last displayed runs
*/
function drawLineChart( data, metric, type, lastRuns ) {
// Helper functions on top of 8-)
/**
* Draw one particalur circle
*
* @param {Object} datum datum
*/
function drawCircle( datum ) {
circleContainer.datum( datum )
.append( 'circle' )
.attr( 'class', function( d ) {
if ( assertionValue ) {
if ( assertionValue.type === '>' ) {
return ( d.value[ type ] > assertionValue.value ) ?
'lineChart--circle failed' :
'lineChart--circle';
} else {
return ( d.value[ type ] < assertionValue.value ) ?
'lineChart--circle failed' :
'lineChart--circle';
}
}
return 'lineChart--circle';
} )
.attr( 'r', 4 )
.attr(
'cx',
function( d ) {
return x( d.date ) + detailWidth / 2;
}
)
.attr(
'cy',
function( d ) {
return y( d.value[ type ] );
}
)
.attr(
'data-average',
function( d ) {
return d.value.average;
}
)
.attr(
'data-timestamp',
function( d ) {
return +d.date;
}
)
.attr(
'data-max',
function( d ) {
return d.value.max;
}
)
.attr(
'data-median',
function( d ) {
return d.value.median;
}
)
.attr(
'data-metric',
function() {
return metric;
}
)
.attr(
'data-min',
function( d ) {
return d.value.min;
}
)
.attr(
'data-sum',
function( d ) {
return d.value.sum;
}
)
.on( 'mouseenter', function() {
d3.select( this )
.attr(
'class',
function( d ) {
if ( assertionValue ) {
if ( assertionValue.type === '>' ) {
return ( assertionValue && d.value[ type ] > assertionValue.value ) ?
'lineChart--circle highlighted failed' :
'lineChart--circle highlighted';
} else {
return ( assertionValue && d.value[ type ] < assertionValue.value ) ?
'lineChart--circle highlighted failed' :
'lineChart--circle highlighted';
}
}
return 'lineChart--circle highlighted';
}
)
.attr( 'r', 6 );
} )
.on( 'mouseout', function() {
d3.select( this )
.attr(
'class',
function( d ) {
if ( assertionValue ) {
if ( assertionValue.type === '>' ) {
return ( d.value[ type ] > assertionValue.value ) ?
'lineChart--circle failed' :
'lineChart--circle';
} else {
return ( d.value[ type ] < assertionValue.value ) ?
'lineChart--circle failed' :
'lineChart--circle';
}
}
return 'lineChart--circle';
}
)
.attr( 'r', 4 );
} );
}
/**
* Container function to iterate of given
* data and draw a circle for each data set
*
* @param {Array} data data
*/
function drawCircles( data ) {
if ( !circleContainer ) {
circleContainer = svg.append( 'g' );
}
circleContainer.selectAll( 'circle' ).remove();
data.forEach( drawCircle );
}
/**
* Redraw all elements that are effected
* by zooming and translating
*/
function redraw() {
svg.select( '.lineChart--xAxis' ).call( xAxis )
.selectAll( 'text' )
.attr( 'transform', 'rotate(45)' )
.style( 'text-anchor', 'start' );
svg.select( '.lineChart--xAxisTicks' ).call( xAxisTicks );
svg.select( '.p--lineChart--area' ).attr( 'd', area );
svg.select( '.p--lineChart--areaLine' ).attr( 'd', line );
drawCircles( data );
}
/**
* Zoom
*/
function zoomed() {
redraw();
// show reset button
svg.select( '.p--lineChart--reset' )
.attr( 'class', 'p--lineChart--reset active' );
svg.select( '.p--lineChart--resetText' )
.attr( 'class', 'p--lineChart--resetText active' );
}
/**
* Reset zoom
*/
function unZoomed() {
zoom.translate( [ 0, 0 ] ).scale( 1 );
redraw();
// hide reset button
svg.select( '.p--lineChart--reset' )
.attr( 'class', 'p--lineChart--reset' );
svg.select( '.p--lineChart--resetText' )
.attr( 'class', 'p--lineChart--resetText' );
}
// get the assertion value
// out of this huge set of data
var assertionValue = null;
if ( data[ data.length - 1 ].assertions[ metric ] ) {
assertionValue = data[ data.length - 1 ].assertions[ metric ];
}
// data manipulation first
// remove all the stuff that
// is not needed for this chart
data = data.reduce( function( newData, datum ) {
if ( datum.metrics[ metric ] ) {
newData.push( {
date : new Date( datum.timestamp ),
value : datum.metrics[ metric ]
} );
}
return newData;
}, [] );
// TODO code duplication check how you can avoid that
var containerEl = document.getElementById( 'graph--' + metric ),
width = containerEl.clientWidth,
height = width * 0.4,
margin = {
top : 20,
bottom : 60,
},
detailWidth = 115,
container = d3.select( containerEl ),
svg = container.select( '.p--graphs--svg' )
.attr( 'width', width )
.attr( 'height', height + margin.top + margin.bottom )
.attr( 'class', 'p--graphs--svg is-initialized' ),
x = d3.time.scale().range( [ 0, width - detailWidth ] ),
xAxis = d3.svg.axis().scale( x )
.ticks ( 8 )
.tickSize( -height ),
xAxisTicks = d3.svg.axis().scale( x )
.ticks( 16 )
.tickSize( -height )
.tickFormat( '' ),
y = d3.scale.linear().range( [ height, margin.top ] ),
yAxisTicks = d3.svg.axis().scale( y )
.ticks( 12 )
.tickSize( width )
.tickFormat( '' )
.orient( 'right' ),
area = d3.svg.area()
.interpolate( 'linear' )
.x( function( d ) { return x( d.date ) + detailWidth / 2; } )
.y0( height )
.y1( function( d ) { return y( d.value[ type ] ); } ),
line = d3.svg.line()
.interpolate( 'linear' )
.x( function( d ) { return x( d.date ) + detailWidth / 2; } )
.y( function( d ) { return y( d.value[ type ] ); } ),
loader = container.select( '.p--graphs--loading' ),
assertionGroup,
circleContainer,
zoom;
// Compute x-positions for the minimum and maximum date
lastRuns = lastRuns || 10;
var startIndex = ( data.length >= lastRuns ) ? data.length - lastRuns : 0;
x.domain( [ data[ startIndex ].date, data[ data.length - 1 ].date ] );
// hacky hacky hacky :(
y.domain( [
0,
d3.max( data, function( d ) {
if ( d.value ) {
return ( !assertionValue || d.value[ type ] > assertionValue.value ) ?
d.value[ type ] :
assertionValue.value;
} else {
return 0;
}
} )
] );
// hide loading spinner
loader.attr( 'class', 'p--graphs--loading' );
// clean up time... :)
if ( !svg.empty() ) {
svg.selectAll( 'g, path, rect, text, line' ).remove();
}
svg.append( 'g' )
.attr( 'class', 'lineChart--xAxisTicks' )
.attr( 'transform', 'translate(' + detailWidth / 2 + ',' + height + ')' )
.call( xAxisTicks );
svg.append( 'g' )
.attr( 'class', 'lineChart--xAxis' )
.attr( 'transform', 'translate(' + detailWidth / 2 + ',' + ( height + 5 ) + ')' )
.call( xAxis )
.selectAll( 'text' )
.attr( 'transform', 'rotate(45)' )
.style( 'text-anchor', 'start' );
svg.append( 'g' )
.attr( 'class', 'lineChart--yAxisTicks' )
.call( yAxisTicks );
// add assertion graphics
if ( assertionValue !== null ) {
assertionGroup = svg.append( 'g' )
.attr( 'transform', 'translate( 0,' + y( assertionValue.value ) + ')' )
.attr( 'class', 'p--lineChart--assertion' );
if ( assertionValue.type === '>' ) {
assertionGroup.append( 'rect' )
.attr( 'class', 'p--lineChart--assertionBox' )
.attr( 'x', 0 )
.attr( 'y', -y( assertionValue.value ) )
.attr( 'width', width )
.attr( 'height', y( assertionValue.value ) )
.attr( 'fill', 'rgba( 255, 0, 0, 0.5 )' );
} else {
assertionGroup.append( 'rect' )
.attr( 'class', 'p--lineChart--assertionBox' )
.attr( 'x', 0 )
.attr( 'y', 0 )
.attr( 'width', width )
.attr( 'height', height - y( assertionValue.value ) )
.attr( 'fill', 'rgba( 255, 0, 0, 0.5 )' );
}
assertionGroup.append( 'line' )
.attr( 'x1', 0 )
.attr( 'y1', 0 )
.attr( 'x2', width )
.attr( 'y2', 0 )
.attr( 'class', 'p--lineChart--assertion' );
assertionGroup.append( 'rect' )
.attr( 'class', 'p--lineChart--assertionTextBg' )
.attr( 'width', 50 )
.attr( 'height', 20 )
.attr( 'x', 0 )
.attr( 'y', - 10 );
assertionGroup.append( 'text' )
.attr( 'x', 25 )
.attr( 'y', 4 )
.text( assertionValue.type + ' ' + assertionValue.value );
}
// Add the area path.
svg.append( 'path' )
.datum( data )
.attr( 'class', 'p--lineChart--area' )
.attr( 'd', area );
// Add the line path.
svg.append( 'path' )
.datum( data )
.attr( 'class', 'p--lineChart--areaLine' )
.attr( 'd', line );
// configure zoom
zoom = d3.behavior.zoom()
.x( x )
.scaleExtent( [ 0, 100 ] )
.on( 'zoom', zoomed );
// set up zoom pane
svg.append( 'rect' )
.attr( 'class', 'p--lineChart--pane' )
.attr( 'width', width )
.attr( 'height', height )
.call( zoom );
drawCircles( data );
// set up reset button
svg.append( 'rect' )
.attr( 'class', 'p--lineChart--reset' )
.attr( 'width', 77 )
.attr( 'height', 23 )
.attr( 'x', width - 77 )
.attr( 'y', 2 )
.on( 'click', unZoomed );
svg.append( 'text' )
.attr( 'class', 'p--lineChart--resetText' )
.attr( 'x', width - 38 )
.attr( 'y', 17 )
.text( 'reset' )
.on( 'click', unZoomed );
}
/**
* Append a new detail box to circle
*
* @param {Object} circle svg circle
*/
function appendDetailBoxForCircle( circle ) {
removeDetailBoxForCircle( circle );
var bBox = circle.getBBox();
var detailBox = document.createElement( 'div' );
var listContainer = getParent( circle, 'p--graphs--graph' );
detailBox.innerHTML =
'<dl>' +
'<dt>Average:</dt>' +
'<dd>' + circle.attributes.getNamedItem( 'data-average' ).value + '</dd>' +
'<dt>Max:</dt>' +
'<dd>' + circle.attributes.getNamedItem( 'data-max' ).value + '</dd>' +
'<dt>Median:</dt>' +
'<dd>' + circle.attributes.getNamedItem( 'data-median' ).value + '</dd>' +
'<dt>Min:</dt>' +
'<dd>' + circle.attributes.getNamedItem( 'data-min' ).value + '</dd>' +
'<dt>Sum:</dt>' +
'<dd>' + circle.attributes.getNamedItem( 'data-sum' ).value + '</dd>' +
'</dl>';
// radius need to be substracted
// TODO think of cleaner solution
detailBox.style.left = ( bBox.x - 71 ) + 'px';
detailBox.style.top = ( bBox.y - 75 ) + 'px';
detailBox.classList.add( 'p--graphs--detailBox' );
listContainer.appendChild( detailBox );
}
/**
* Remove detail box
*
* @param {Object} circle svg circle
*/
function removeDetailBoxForCircle( circle ) {
var listContainer = getParent( circle, 'p--graphs--graph' );
var detailBox = listContainer.querySelector( '.p--graphs--detailBox' );
if ( detailBox ) {
listContainer.removeChild( detailBox );
}
}
/**
* Attach circle events on graph list
* -> event delegation for the win
*/
function attachCircleEvents() {
var mainContainer = document.getElementsByTagName( 'main' )[ 0 ];
addEvent( mainContainer, 'mouseover', function( event ) {
if ( event.target.tagName === 'circle' ) {
appendDetailBoxForCircle( event.target );
highlightTableRow( event.target );
}
} );
addEvent( mainContainer, 'mouseout', function( event ) {
if ( event.target.tagName === 'circle' ) {
removeDetailBoxForCircle( event.target );
unhighlightTableRow( event.target );
}
} );
}
/**
* Attach click events on graph list
* -> event delegation for the win
*/
function attachClickEvents() {
var body = document.querySelector( 'body' );
var headerHeight = document.getElementsByTagName( 'header' )[ 0 ]
.getBoundingClientRect().height;
var overlay = document.getElementById( 'p--modal__overlay' );
var closeButton = document.getElementById( 'p--modal__close' );
addEvent( body, 'click', function( event ) {
if ( event.target.classList.contains( 'js-expand' ) ) {
document.getElementById(
'p--table--container--' +
event.target.attributes.getNamedItem( 'data-metric' ).value
).classList.toggle( 'expanded' );
}
if ( event.target.classList.contains( 'js-offenders' ) ) {
overlay.style.display = 'block';
overlay.style.opacity = 0.5;
closeButton.style.display = 'block';
document.getElementById(
'offender--' +
event.target.attributes.getNamedItem( 'data-metric' ).value
).classList.toggle( 'in-modal' );
}
if ( event.target === overlay || event.target === closeButton) {
overlay.style.opacity = 0;
overlay.style.display = 'none';
closeButton.style.display = 'none';
document.querySelector( '.in-modal' ).classList.toggle( 'in-modal' );
}
if ( event.target.classList.contains( 'js-scroll' ) ) {
event.preventDefault();
var yPosition = 0;
var element = document.getElementById(
event.target.href.split( '#' )[ 1 ]
);
if ( element.offsetParent ) {
do {
yPosition += element.offsetTop;
} while ( element = element.offsetParent );
}
// console.log( document.getElementById( event.target.href.split( '#' )[ 1 ] ).offsetTop );
window.scrollTo(
0,
yPosition - headerHeight - 20
);
}
} );
}
/**
* Attach hover event to body and listen
* for description button hovers
* to show description
*/
function attachDescriptionEvents () {
var body = document.querySelector( 'body' );
addEvent( body, 'mouseover', function( event ) {
if (
event.target.tagName === 'A' &&
event.target.classList.contains( 'active' ) &&
(
event.target.classList.contains( 'p--graphs--descriptionBtn' ) ||
event.target.classList.contains( 'p--graphs--warningBtn' ) ||
event.target.classList.contains( 'p--graphs--experimentalBtn' )
)
) {
var target = document.getElementById(
event.target.href.split( '#' )[ 1 ]
);
target.removeAttribute( 'hidden' );
event.preventDefault();
}
} );
addEvent( body, 'mouseout', function( event ) {
if (
event.target.tagName === 'A' &&
event.target.classList.contains( 'active' ) &&
(
event.target.classList.contains( 'p--graphs--descriptionBtn' ) ||
event.target.classList.contains( 'p--graphs--warningBtn' ) ||
event.target.classList.contains( 'p--graphs--experimentalBtn' )
)
) {
var target = document.getElementById(
event.target.href.split( '#' )[ 1 ]
);
target.setAttribute( 'hidden', 'hidden' );
event.preventDefault();
}
} );
addEvent( body, 'click', function( event ) {
if (
event.target.tagName === 'A' &&
(
event.target.classList.contains( 'p--graphs--descriptionBtn' ) ||
event.target.classList.contains( 'p--graphs--warningBtn' ) ||
event.target.classList.contains( 'p--graphs--experimentalBtn' )
)
) {
event.preventDefault();
}
} );
}
/**
* Attach mouse hover events on body
* -> event delegation for the win
*/
function attachHeaderEvents() {
var body = document.querySelector( 'body' );
var container = document.getElementById( 'p--header--notification' );
addEvent( body, 'mouseover', function( event ) {
if ( event.target.classList.contains( 'js-warning' ) ) {
container.innerHTML = event.target.innerHTML;
}
} );
}
/**
* Attach event to select box to rerender
* graphs depending on chosen tyoe
*/
function attachLastRunsChangeEvent() {
var switcher = document.getElementById( 'p--switcher--lastRuns' );
addEvent( switcher, 'change', function( event ) {
var currentMetric = document.getElementById( 'p--switcher--metrics' ).value;
drawLineCharts( window.results, currentMetric, +event.target.value );
} );
}
/**
* Attach event to select box to rerender
* graphs depending on chosen tyoe
*/
function attachMetricChangeEvent() {
var switcher = document.getElementById( 'p--switcher--metrics' );
addEvent( switcher, 'change', function( event ) {
var currentLastRuns = +document.getElementById( 'p--switcher--lastRuns' ).value;
drawLineCharts( window.results, event.target.value, currentLastRuns );
} );
}
/**
* Attach events to document
*/
function attachEventListeners() {
attachCircleEvents();
attachClickEvents();
attachDescriptionEvents();
attachHeaderEvents();
attachMetricChangeEvent();
attachLastRunsChangeEvent();
}
/**
* Highlight table row if particular
* graph bullet if hovered
*
* @param {Object} target target
*/
function unhighlightTableRow( target ) {
var row = document.querySelectorAll(
'#' + target.attributes.getNamedItem( 'data-metric' ).value +
'--row--' +
target.attributes.getNamedItem( 'data-timestamp' ).value
);
if ( row.length ) {
row[ 0 ].classList.remove( 'active' );
}
}
/**
* Unhighlight table row if particular
* graph bullet is left
*
* @param {Object} target target
*/
function highlightTableRow( target ) {
var metric = target.attributes.getNamedItem( 'data-metric' ).value;
var row = document.getElementById(
metric +
'--row--' +
target.attributes.getNamedItem( 'data-timestamp' ).value
);
var scrollContainer = document.getElementById(
'p--table--container--' + metric
);
if ( row && scrollContainer ) {
scrollContainer.scrollTop = row.offsetTop;
row.classList.add( 'active' );
}
}
/**
* KICK OFF FOR GRAPH POWER
* *******************************
* Check all metrics if numeric values are
* included and initialize all graphs for it
*
* @param {Array} data data
* @param {String|undefined} type type of displayed data
* @parem {Number|undefined} lastRuns number of last displayed runs
*/
function drawLineCharts( data, type, lastRuns ) {
var lastMetric = data[ data.length - 1 ];
var loaders = document.querySelectorAll( '.p--graphs--loading' );
for ( var i = 0; i < loaders.length; ++i ) {
loaders[ i ].classList.add( 'is-active' );
}
type = type || 'median';
for ( var metric in lastMetric.metrics ) {
if (
lastMetric.metrics[ metric ] &&
typeof lastMetric.metrics[ metric ].median === 'number' &&
metric !== 'timestamp' &&
document.getElementById( 'graph--' + metric )
) {
setTimeout( drawLineChart.bind( null, data, metric, type, lastRuns ), 250 );
}
}
}
drawLineCharts( window.results );
attachEventListeners();
} )( d3, window, document );