Skip to content

Instantly share code, notes, and snippets.

@ensley
Last active March 8, 2017 20:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ensley/09addcd400aafb5fdeb5cefa2b3bdcb6 to your computer and use it in GitHub Desktop.
Save ensley/09addcd400aafb5fdeb5cefa2b3bdcb6 to your computer and use it in GitHub Desktop.
QuickPredict Comparison Charts

A tool for visualizing results of many prediction tests simultaneously. Made during an internship at Lubrizol using Highcharts.js.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>QuickPredict</title>
<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/simplex/bootstrap.min.css'>
<link rel="stylesheet" href="https://rawgit.com/ensley/qp_charts/master/qp_multiple.css">
</head>
<body>
<div class="container">
<div class="page-header">
<h1>QuickPredict Charts</h1>
<button type="button" id="help" class="btn btn-info btn-xs" data-toggle="modal" data-target="infoModal">
<span class="glyphicon glyphicon-question-sign"></span>
</button>
</div>
<div class="row controls">
<div class="col-lg-2 dropdown">
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
Select a test
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="#" data-ref="/data/qlife_mtm.json">MTM Traction</a></li>
<li><a href="#" data-ref="/data/qlife_ehd.json">EHD Film Thickness</a></li>
</ul>
</div>
<div class="col-lg-6">
<div class="row">
<div class="btn-toolbar hide-at-start" role="toolbar">
<button type="button" id="export" class="btn btn-success">Export All...</button>
<button type="button" id="swap" class="btn btn-success">Swap rows/columns</button>
</div>
</div>
<div class="row">
<div class="btn-group" id="xbuttons" data-toggle="buttons"></div>
</div>
</div>
<div class="col-lg-4 text-right">
<!-- <h4>Adjust y axis range:</h4> -->
<form class="form-horizontal hide-at-start">
<div class="form-group">
<label for="ymin" class="col-sm-4 control-label">Y axis min:</label>
<div class="col-sm-8">
<input type="number" step="any" class="form-control" id="ymin">
</div>
</div>
<div class="form-group">
<label for="ymax" class="col-sm-4 control-label">Y axis max:</label>
<div class="col-sm-8">
<input type="number" step="any" class="form-control" id="ymax">
</div>
</div>
<button type="button" id="reset-range" class="btn btn-default">Reset</button>
<button type="button" id="change-range" class="btn btn-primary">Update</button>
</form>
</div>
</div>
<div class="row">
<div class="col-lg-2">
<div class="row hide-at-start">
<h3>Legend</h3>
<div class="media" id="legend"></div>
</div>
</div>
<div class="col-lg-10" id="datatable">
Pick a test from the dropdown.
</div>
</div>
</div>
<div class="modal fade" id="infoModal" tabindex="-1" role="dialog" aria-labelledby="infoModal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="infoModalLabel">Info</h4>
<div class="modal-body">
<p>This is a tool to compare predicted test results for multiple blends.</p>
<p>Each blend is given a color and displayed in the list on the left. Across the top are controls to fine tune the plots. For example, click "Plot srr" to draw the plots with srr on the x axis and the predicted traction result on
the y axis. The other treatment settings, temp and load, are placed on the rows and columns of the plot grid.</p>
<p>Click "Swap rows/columns" to swap which variables are on the rows and which are on the columns. This is sometimes useful to better fit information on the screen.</p>
<p>Hover over a plot to see information about predicted values at a particular point for each blend. The blends are shown in decreasing order in this tooltip.</p>
<p>The range of the y axes can be adjusted. By default the upper and lower limits are set to the minimum and maximum predicted values. However, if one plot has an outlier that makes the other plots difficult to look at, adjust the
range limits and click "Update" to change the view. "Reset" will reset the limits to their default values.</p>
<p>To download a particular plot, click on the icon in its top right corner. To download the entire grid, click "Export All...".</p>
</div>
<div class="modal-footer">
<button class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
<script src="https://code.highcharts.com/4.2.5/highcharts.js" charset="utf-8"></script>
<script src="https://rawgit.com/ensley/qp_charts/master/scripts/exporting.js" charset="utf-8"></script>
<script src="https://rawgit.com/ensley/qp_charts/master/scripts/offline-exporting.js" charset="utf-8"></script>
<script src="https://rawgit.com/ensley/qp_charts/master/scripts/exporting-extension.js" charset="utf-8"></script>
<script src="qp-app.js" charset="utf-8"></script>
</body>
</html>
var app = (function() {
var config = {
'$container' : $('#datatable'),
'$buttons' : $('#xbuttons'),
'filepath' : '/data/qlife_mtm.json',
'yvar' : 'pred',
'seriesvar' : 'blendid',
'nonplots' : [ 'blendid', 'corpid', 'datetime' ]
};
var allData = [];
var originalRange = [];
var xvals = {};
var xNames = [];
var otherXs = [];
var chartArray = [];
var init = function() {
setupHelpButton();
setupDropdown();
setupRange();
setupExport();
};
var setupHelpButton = function() {
$( '#help' ).click( function() {
$( '#infoModal' ).modal();
});
};
var setupDropdown = function() {
$( '.dropdown-menu li a' ).click( function() {
var $this = $( this );
var $clicked = $this.parents( '.dropdown' ).find( '.btn' );
$clicked.html( $this.text() + ' <span class="caret"></span>' );
$clicked.val( $this.data( 'value' ) );
config.filepath = 'https://rawgit.com/ensley/qp_charts/master' + $this.attr( 'data-ref' );
console.log( config );
loadData();
});
};
var setupRange = function() {
$( '#change-range' ).unbind();
$( '#change-range' ).click( function() {
console.log( 'CHANGE RANGE BUTTON CLICKED.' );
console.log( 'CALLING createHtmlTable FROM CHANGE RANGE BUTTON.' );
chartArray = createHtmlTable( config.$container.data() );
});
$( '#reset-range' ).unbind();
$( '#reset-range' ).click( function() {
$( '#ymin' ).val( originalRange[0] );
$( '#ymax' ).val( originalRange[1] );
$( '#change-range' ).click();
});
$( '.hide-at-start' ).css( 'display', 'none' );
};
var setupExport = function() {
$( '#export' ).unbind();
$( '#export' ).click( function() {
Highcharts.exportCharts( chartArray );
});
};
var findYrange = function( data, yvar ) {
var min = Infinity;
var max = -Infinity;
$.each( data, function( index, obj ) {
value = obj[ yvar ];
if( value < min ) {
min = value;
} else if( value > max ) {
max = value;
}
} );
return [ min, max ];
};
var createXvals = function( data ) {
var xvals = {};
$.each( data, function( index, item ) {
$.each( Object.keys( item ), function( i, key ) {
if( key === config.yvar ) return;
if( Object.keys( xvals ).indexOf( key ) < 0 ) {
xvals[key] = [];
}
if( xvals[key].indexOf( item[key] ) < 0) {
xvals[key].push( item[key] );
}
});
});
return xvals;
};
var createXbuttons = function( gridObj ) {
var allXs = $.map( gridObj, function( value ) {
return value;
}).sort();
$.each( allXs, function( index, xname ) {
var isActive = '';
if( xname === gridObj.plot ) {
isActive = ' active';
}
if( xname !== undefined ) {
config.$buttons.append(
$( '<label/>', {
class: 'btn btn-primary' + isActive,
id: 'label-' + xname
} ).append(
$( '<input/>', {
type: 'radio',
id: 'btn-' + xname,
'data-xname': xname
} )
).append( 'Plot ' + xname )
);
}
});
};
function dynamicSort( property ) {
var sortOrder = 1;
if ( property[ 0 ] === "-" ) {
sortOrder = -1;
property = property.substr( 1 );
}
return function ( a, b ) {
var result = ( a[ property ] < b[ property ] ) ? -1 : ( a[ property ] > b[ property ] ) ? 1 : 0;
return result * sortOrder;
};
}
var loadData = function() {
$( '.hide-at-start' ).css( 'display', '' );
console.log( 'LOAD DATA CALLED. FILEPATH: ' + config.filepath );
$.ajax( {
url: config.filepath,
dataType: 'json',
async: true
} ).done( function( data ) {
console.log( 'DATA LOADED.' );
allData = data;
originalRange = findYrange( data, config.yvar );
$( '#ymin' ).val( originalRange[0] );
$( '#ymax' ).val( originalRange[1] );
xvals = createXvals( data );
console.log( 'XVALS CREATED.' );
xNames = $.grep( Object.keys( xvals ), function( value ) {
return config.nonplots.indexOf( value ) === -1;
});
otherXs = $.grep( xNames, function( name ) {
return xNames[0] !== name;
});
var gridObj = {
'row': otherXs[1],
'col': otherXs[0],
'plot': xNames[0]
};
chartArray = createHtmlTable( gridObj );
});
};
var createHtmlTable = function( gridObj ) {
console.log( 'createHtmlTable CALLED.' );
config.$container.html( '' );
config.$buttons.html( '' );
config.$container.data( gridObj );
if( gridObj.row === undefined ) {
chartArray = createSingleRow( gridObj );
} else if( gridObj.col === undefined ) {
chartArray = createSingleCol( gridObj );
} else {
chartArray = createFullTable( gridObj );
}
createXbuttons( gridObj );
initializeButtons( gridObj );
return chartArray;
};
var initializeButtons = function( gridObj ) {
console.log( 'initializeButtons CALLED.' );
var allXs = $.map( gridObj, function( value ) {
return value;
});
console.log( 'INITIALIZING X BUTTONS.' );
$( 'label[id^="label-"]' ).on( 'click', function() {
var $button = $( this )[0].firstChild;
var newPlotvar = $button.dataset.xname;
var newOtherXs = $.grep( allXs, function( name ) {
return newPlotvar !== name;
});
var newGridObj = {
'row': newOtherXs[0],
'col': newOtherXs[1],
'plot': newPlotvar
};
console.log( 'CALLING createHtmlTable FROM X BUTTON.' );
chartArray = createHtmlTable( newGridObj );
});
console.log( 'X BUTTONS INITIALIZED.' );
console.log( 'INITIALIZING SWAP BUTTON.' );
$( '#swap' ).unbind();
$( '#swap' ).click( function() {
console.log( 'SWAP BUTTON CLICKED' );
var newGridObj = {
'row': config.$container.data( 'col' ),
'col': config.$container.data( 'row' ),
'plot': config.$container.data( 'plot' )
};
console.log( 'CALLING createHtmlTable FROM SWAP BUTTON.' );
chartArray = createHtmlTable( newGridObj );
return false;
});
console.log( 'SWAP BUTTON INITIALIZED' );
};
var createFullTable = function( gridObj ) {
chartArray = [];
var columns = xvals[ gridObj.col ];
var rows = xvals[ gridObj.row ];
var $headerFlexbox = $( '<div/>', { class: 'flex-row' } );
$headerFlexbox.append( $( '<div/>', {
class: 'chart-holder row-header'
} ) );
columns.map( function( col ) {
$headerFlexbox.append( $( '<div/>', {
class: 'chart-holder'
} ).html( '<h4>' + gridObj.col + ' : ' + col + '</h4>' ) );
} );
config.$container.append( $headerFlexbox );
rows.map( function( row ) {
var $row = $( '<div/>', {
class: 'flex-row'
} );
$row.append( $( '<div/>', {
class: 'row-header'
} ).html( '<h4>' + gridObj.row + ' : ' + row + '</h4>' ) );
columns.map( function( col ) {
var plotId = [ gridObj.row, row, gridObj.col, col ].join( '' );
$row.append( $( '<div/>', {
id: 'plot-' + plotId,
class: 'chart-holder'
} ) );
} );
config.$container.append( $row );
} );
rows.map( function( row ) {
columns.map( function( col ) {
var chart = drawChart( gridObj, row, col );
chartArray.push( chart );
} );
} );
chartArray.numRows = rows.length;
chartArray.numCols = columns.length;
return chartArray;
};
var createSingleRow = function( gridObj ) {
chartArray = [];
var columns = xvals[ gridObj.col ];
$headerFlexbox = $( '<div/>', { class: 'flex-row' } );
$row = $( '<div/>', { class: 'flex-row' } );
columns.map( function( col ) {
var plotId = [ gridObj.col, col ].join( '' );
$headerFlexbox.append( $( '<div/>', {
class: 'column-header'
} ).html( '<h4>' + gridObj.col + ' : ' + col + '</h4>' ) );
$row.append( $( '<div/>', {
id: 'plot-' + plotId,
class: 'chart-holder'
} ) );
} );
config.$container.append( $headerFlexbox );
config.$container.append( $row );
columns.map( function( col ) {
var chart = drawChart( gridObj, undefined, col );
chartArray.push( chart );
} );
chartArray.numRows = 1;
chartArray.numCols = columns.length;
return chartArray;
};
var createSingleCol = function( gridObj ) {
chartArray = [];
var rows = xvals[ gridObj.row ];
var $headerFlexbox = $( '<div/>', { class: 'flex-column' } );
var $col = $( '<div/>', { class: 'flex-column' } );
rows.map( function( row ) {
var plotId = [ gridObj.row, row ].join( '' );
$headerFlexbox.append( $( '<div/>', {
class: 'row-header'
} ).html( '<h4>' + gridObj.row + ' : ' + row + '</h4>' ) );
$col.append( $( '<div/>', {
id: 'plot-' + plotId,
class: 'chart-holder-column'
} ) );
} );
config.$container.append( $headerFlexbox );
config.$container.append( $col );
rows.map( function( row ) {
var chart = drawChart( gridObj, row, undefined );
chartArray.push( chart );
} );
chartArray.numRows = rows.length;
chartArray.numCols = 1;
return chartArray;
};
var drawChart = function( gridObj, row, col ) {
var cellData = pullCellData( gridObj, row, col );
var plotId = 'plot-' + [ gridObj.row, row, gridObj.col, col ].join( '' );
var setMin = $( '#ymin' ).val();
var setMax = $( '#ymax' ).val();
var titleText = '';
if( gridObj.row === undefined ) {
titleText = gridObj.col + ' = ' + col;
} else if( gridObj.col === undefined ) {
titleText = gridObj.row + ' = ' + row;
} else {
titleText = gridObj.row + ' = ' + row + ', ' + gridObj.col + ' = ' + col;
}
// Options for the chart. Mostly deals with hiding axis labels and text so that it fits better inside a table.
var options = {
exporting: {
chartOptions: {
chart: {
margin: undefined
},
title: {
text: titleText
},
xAxis: {
title: {
text: gridObj.plot
}
}
},
sourceHeight: 400
},
chart: {
renderTo: plotId,
type: 'spline',
backgroundColor: null,
borderWidth: 0,
margin: [ 2, 0, 2, 0 ],
height: 200,
width: undefined,
style: {
overflow: 'visible'
}
},
title: {
text: ''
},
credits: {
enabled: false
},
xAxis: {
title: {
text: ''
}
},
yAxis: {
min: setMin,
max: setMax,
title: {
text: ''
},
labels: {
reserveSpace: true
},
opposite: true
},
legend: {
enabled: false
},
tooltip: {
backgroundColor: 'rgba(255, 255, 255, 0.75)',
borderColor: 'rgba(0, 0, 0, 0.8)',
borderRadius: 20,
borderWidth: 1,
shadow: false,
useHTML: true,
hideDelay: 0,
shared: true,
padding: 0,
positioner: function ( w, h, point ) {
return {
x: point.plotX - w / 2,
y: point.plotY - h
};
},
// Set the information displayed in the tooltip
formatter: function () {
var str = '<h4>' + gridObj.plot + ': ' + this.x + '</h4><table class="table table-condensed" id="tooltip-table"><tr><th>Blend ID</th><th>Prediction</th></tr>';
var pts = this.points.sort( dynamicSort( '-y' ) );
$.each( pts, function ( index, point ) {
str += '<tr><td style="color: ' + point.series.color + '"><b>' + point.series.name + '</b></td><td>' + point.y + '</td></tr>';
} );
str += '</table>';
return str;
}
},
plotOptions: {
series: {
animation: false,
lineWidth: 2,
shadow: false,
states: {
hover: {
lineWidth: 2
}
},
marker: {
radius: 3,
states: {
hover: {
radius: 4
}
}
},
fillOpacity: 0.25
}
},
series: []
};
xvals[ config.seriesvar ].map( function( form ) {
options.series.push( {
name: form,
data: []
} );
} );
$.each( cellData, function( index, obj ) {
var point = {
x: obj[ gridObj.plot ],
y: obj[ config.yvar ]
};
$.each( options.series, function( index, series ) {
if( obj[ config.seriesvar ] === series.name ) {
series.data.push( point );
}
} );
} );
var c = new Highcharts.chart( options, function() {
var $legend = $( '#legend' );
if( $legend.html() !== '' ) return;
$.each( this.series, function( index, series ) {
var $legendEntry = $( '<div/>', { class: 'media' } );
var $legendColorSpot = $( '<div/>', { class: 'media-left' } );
var $legendTextSpot = $( '<div/>', { class: 'media-body' } );
var $newColorDiv = $( '<div/>', { class: 'media-object' } )
.css( {
'background-color': series.color,
'width': '32px',
'height': '32px'
} );
var $newLegendDiv = $( '<h4/>', { class: 'media-heading' } )
.text( series.name );
// Append everything to the legend
$legendColorSpot.append( $newColorDiv );
$legendTextSpot.append( $newLegendDiv );
$legendEntry.append( $legendColorSpot );
$legendEntry.append( $legendTextSpot );
$legend.append( $legendEntry );
} );
} );
return c;
};
var pullCellData = function( gridObj, row, col ) {
var result = [];
$.each( allData, function( index, obj ) {
if( gridObj.row === undefined ) {
if( obj[ gridObj.col ] === col ) {
result.push( obj );
}
} else if( gridObj.col === undefined ) {
if( obj[ gridObj.row ] === row ) {
result.push( obj );
}
} else {
if( obj[ gridObj.row ] === row && obj[ gridObj.col ] === col ) {
result.push( obj );
}
}
} );
return result;
};
return {
init: init
};
})();
$(function() {
app.init();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment