Last active
August 4, 2017 17:52
-
-
Save yymao/e6ab51e81a6effe6e1c8 to your computer and use it in GitHub Desktop.
Plots of Everything vs. Everything: A web interface which shows scatter plots with any two columns in a Google Spreadsheet with Google Charts API.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
if (!isset($_GET['url']) or !preg_match('/(?:\/spreadsheets\/d\/|[?&]key=)([\w-]+)/', $_GET['url'], $matches)) { | |
?> | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Plots of Everything vs. Everything</title> | |
<meta http-equiv="content-type" content="text/html; charset=utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<link rel="stylesheet" href="https://unpkg.com/[email protected]/build/pure-min.css"> | |
<style> | |
div#main{ | |
padding: 1em; | |
max-width: 600px; | |
margin-left: auto; | |
margin-right: auto; | |
} | |
input#url{ | |
width: 100%; | |
} | |
.footer{ | |
font-size: small; | |
margin-top: 42px; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="main"> | |
<center> | |
<h1>Plots of Everything vs. Everything</h1> | |
<form class="pure-form" action="scatter.php" method="get"> | |
<fieldset> | |
<p><input id="url" name="url" required placeholder="Google Spreadsheet URL"></p> | |
<p><button type="submit" class="pure-button pure-button-primary">Show me!</button></p> | |
</fieldset> | |
</form> | |
<p class="footer">Give it a Google Spreadsheet that has multiple columns, and you can pick any two columns to make a scatter plot (so everything vs. everything)! | |
<b>Note</b>: In the spreadsheet, the first column should be row labels, the first row should have column labels, and all other cells should have only numbers.</p> | |
<p class="footer">By <a href="http://web.stanford.edu/~yymao/">Yao-Yuan Mao</a> (2015-2016). Source code <a href="https://gist.github.com/yymao/e6ab51e81a6effe6e1c8">on Gist</a>.</p> | |
</center> | |
</div> | |
</body> | |
</html> | |
<?php | |
die(); } | |
$url = 'https://docs.google.com/spreadsheet'; | |
$url .= (strpos($matches[0], 's/d/')) ? "s/d/${matches[1]}/gviz/tq?" : "/tq?key=${matches[1]}&"; | |
$url .= "headers=1"; | |
if (preg_match('/[?&#](gid=\d+)/', $_GET['url'], $matches)){ | |
$url .= "&${matches[1]}"; | |
} | |
?> | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Plots of Everything vs. Everything</title> | |
<meta http-equiv="content-type" content="text/html; charset=utf-8" /> | |
<link rel="stylesheet" type="text/css" href="style.css" /> | |
<script type="text/javascript" src='//www.google.com/jsapi?autoload={"modules":[{"name":"visualization","version":"1","packages":["corechart"]}]}'></script> | |
<script type="text/javascript" src="//code.jquery.com/jquery-2.2.0.min.js"></script> | |
<script type="text/javascript"> | |
var initialized = false; | |
var nVar, nHalo; | |
var col = 0, row = 1; | |
var data, view, chart; | |
var pData, tauData; | |
var hist, hData, nbin = 15; | |
var selectedHalo, varName; | |
var history = new Array(); | |
var setAxisLimit = false; | |
function handleQueryResponse(response) { | |
if (response.isError()) { | |
alert('Error in query: ' + response.getMessage() + ' ' + response.getDetailedMessage()); | |
return; | |
} | |
data = response.getDataTable(); | |
view = new google.visualization.DataView(data); | |
initializeDoc(); | |
}; | |
function initializeGoogle(){ | |
var query = new google.visualization.Query('<?=$url?>'); | |
query.send(handleQueryResponse); | |
}; | |
function initializeDoc(){ | |
if(!initialized){ | |
initialized = true; | |
return; | |
} | |
nVar = data.getNumberOfColumns()-1; | |
nHalo = data.getNumberOfRows(); | |
selectedHalo = new Array(nHalo); | |
varName = new Array(nVar); | |
var i; | |
for(i=0; i<nVar; i++) varName[i] = data.getColumnLabel(i+1); | |
chart = new google.visualization.ScatterChart(document.getElementById('plot')); | |
google.visualization.events.addListener(chart, 'select', selectHandler); | |
hist = new google.visualization.SteppedAreaChart(document.getElementById('plot-hist')); | |
hData = new google.visualization.DataTable(); | |
hData.addColumn('string', 'edges'); | |
hData.addColumn('number', 'accumulated number'); | |
hData.addRows(nbin+2); | |
var options = "<option>" + varName.join("</option><option>") + "</option>"; | |
$("#selRow").html(options); | |
$("#selCol").html(options); | |
$("#selRow").change(function(){ | |
loadPlot(col, $("#selRow > option:selected").index()); | |
$(this).blur(); | |
}); | |
$("#selCol").change(function(){ | |
loadPlot($("#selCol > option:selected").index(), row); | |
$(this).blur(); | |
}); | |
var dcol = new Array(-1, 0, 1, 0); | |
var drow = new Array(0, -1, 0, 1); | |
$(document).keydown(function(event){ | |
var key = event.which; | |
if(key >= 37 && key < 41){ | |
event.preventDefault(); | |
key -= 37; | |
loadPlot(col+dcol[key], row+drow[key]); | |
} | |
}); | |
$("input.style_checkbox").click(function(){ | |
loadPlot(col, row); | |
}); | |
$("#setLimits").click(function(){ | |
setAxisLimit = true; | |
loadPlot(col, row); | |
}); | |
$("#clearLimits").click(function(){ | |
$("#x_amin").val(''); | |
$("#x_amax").val(''); | |
$("#y_amin").val(''); | |
$("#y_amax").val(''); | |
setAxisLimit = false; | |
loadPlot(col, row); | |
}); | |
$("#selectRange").click(function(){ | |
var xmin = parseFloat($("#xmin").val()); | |
var xmax = parseFloat($("#xmax").val()); | |
var ymin = parseFloat($("#ymin").val()); | |
var ymax = parseFloat($("#ymax").val()); | |
var i, crit = new Array; | |
var crit_x = {column: col+1}, crit_y = {column: row+1}; | |
i = 0; | |
if(!isNaN(xmin)) {crit_x.minValue = xmin; i+=1;} | |
if(!isNaN(xmax)) {crit_x.maxValue = xmax; i+=1;} | |
if(i > 0) crit.push(crit_x); | |
i = 0; | |
if(!isNaN(ymin)) {crit_y.minValue = ymin; i+=1;} | |
if(!isNaN(ymax)) {crit_y.maxValue = ymax; i+=1;} | |
if(i > 0) crit.push(crit_y); | |
var selRows = data.getFilteredRows(crit); | |
for(i=0; i<nHalo; i++){ | |
selectedHalo[i] = (selRows.indexOf(i) >= 0)?true:false; | |
} | |
if(col!=row) drawScatter(); | |
genKey(); | |
}); | |
$("#clearRange").click(function(){ | |
var i; | |
for(i=0; i<nHalo; i++) selectedHalo[i] = false; | |
if(col!=row) drawScatter(); | |
genKey(); | |
$("#xmin").val(''); | |
$("#xmax").val(''); | |
$("#ymin").val(''); | |
$("#ymax").val(''); | |
}); | |
$("#key").click(function(){ | |
$(this).select(); | |
}); | |
$("#goKey").click(function(){ | |
var keys = $("#key").val().split('/'); | |
var num = parseInt(keys[0]); | |
for(i=0; i<nHalo; i++){ | |
selectedHalo[i] = (keys.indexOf(i.toString()) > 0)?true:false; | |
} | |
loadPlot(Math.floor(num/nVar), num%nVar); | |
}); | |
$("#history").change(function(){ | |
var num = parseInt($(this).val()); | |
loadPlot(Math.floor(num/nVar), num%nVar, true); | |
$(this).blur(); | |
}); | |
$('#plot-hist').hide(); | |
var key = window.location.hash.substring(1); | |
if (key.length){ | |
$("#key").val(key); | |
$("#goKey").click(); | |
} | |
else{ | |
loadPlot(0,1,true); | |
} | |
}; | |
function selectHandler() { | |
var selection = chart.getSelection(); | |
var i = selection[0].row; | |
selectedHalo[i] = (selectedHalo[i])?false:true; | |
drawScatter(); | |
genKey(); | |
}; | |
function getCol1(d, i){ | |
return (selectedHalo[i])?(d.getValue(i, row+1)):null | |
}; | |
function getCol2(d, i){ | |
return (!selectedHalo[i])?(d.getValue(i, row+1)):null | |
}; | |
function drawScatter(){ | |
view.setColumns([col+1, {calc:getCol2, type:'number'}, | |
{sourceColumn:0, role:'tooltip'}, {calc:getCol1, type:'number'}, | |
{sourceColumn:0, role:'tooltip'}]); | |
chart.draw(view, {width: 600, height: 600, | |
hAxis: {title: varName[col], | |
viewWindowMode: (setAxisLimit?'explicit':'pretty'), | |
viewWindow: {min: parseFloat($('#x_amin').val()), max:parseFloat($('#x_amax').val())}, | |
logScale: $('#x_log').is(':checked'), | |
//format: ($('#x_log').is(':checked')?'##E#':'####'), | |
direction: ($('#x_rev').is(':checked')?-1:1)}, | |
vAxis: {title: varName[row], | |
viewWindowMode: (setAxisLimit?'explicit':'pretty'), | |
viewWindow: {min: parseFloat($('#y_amin').val()), max:parseFloat($('#y_amax').val())}, | |
logScale: $('#y_log').is(':checked'), | |
//format: ($('#y_log').is(':checked')?'##E#':'####'), | |
direction: ($('#y_rev').is(':checked')?-1:1)}, | |
chartArea: {left:60,top:25,width:520,height:520}, | |
legend: 'none'}); | |
$('#plot').show(); | |
$('#plot-hist').hide(); | |
}; | |
function drawHist(){ | |
var i = 0, j = col+1, n=nbin+2, binsize; | |
var range = data.getColumnRange(j); | |
var h = new Array(n), x = new Array(n); | |
if($('#x_log').is(':checked')){ | |
binsize = (Math.log(range.max) - Math.log(range.min))/nbin; | |
} | |
else{ | |
binsize = (range.max - range.min)/nbin; | |
} | |
for(i=0; i<n; i++){ | |
h[i] = 0; | |
x[i] = range.min + (i-0.5)*binsize; | |
} | |
if($('#x_log').is(':checked')){ | |
for(i=0; i<nHalo; i++){ | |
h[Math.floor((Math.log(data.getValue(i, j))-range.min)/binsize)+1] += 1; | |
} | |
} | |
else{ | |
for(i=0; i<nHalo; i++){ | |
h[Math.floor((data.getValue(i, j)-range.min)/binsize)+1] += 1; | |
} | |
} | |
for(i=0; i<n; i++){ | |
hData.setCell(i, 0, x[i].toPrecision(2)); | |
hData.setCell(i, 1, h[i]); | |
} | |
hist.draw(hData, {width: 600, height: 600, | |
hAxis: {title: varName[col], | |
direction: ($('#x_rev').is(':checked')?-1:1)}, | |
vAxis: {title: 'accumulated number', | |
logScale: $('#y_log').is(':checked'), | |
direction: ($('#y_rev').is(':checked')?-1:1)}, | |
chartArea:{left:60,top:25,width:520,height:520}, | |
legend: 'none'}); | |
$('#plot-hist').show(); | |
$('#plot').hide(); | |
}; | |
function loadPlot(newCol, newRow, fromHistory){ | |
if(newCol >= 0 && newCol < nVar && newRow >= 0 && newRow < nVar){ | |
col = newCol; | |
row = newRow; | |
if(col!=row){ | |
drawScatter(); | |
} | |
else{ | |
drawHist(); | |
} | |
$("#selCol > option").eq(col).attr('selected', 'selected'); | |
$("#selRow > option").eq(row).attr('selected', 'selected'); | |
genKey(); | |
if(!fromHistory) addHistory(); | |
} | |
}; | |
function genKey(){ | |
var key = (col*nVar + row).toString() + '/'; | |
for(i=0; i<nHalo; i++){ | |
key += (selectedHalo[i])?i.toString()+'/':''; | |
} | |
$("#key").val(key); | |
window.location.hash = '#' + key; | |
}; | |
function addHistory(){ | |
var num = (col*nVar + row).toString(); | |
if($("#history > option").eq(0).val() == num) return; | |
var htmlStr = "<option value='" + num | |
+ "'>(" + varName[col] + ", " + varName[row] + ")</option>" | |
+ $("#history").html(); | |
$("#history").html(htmlStr); | |
}; | |
google.setOnLoadCallback(initializeGoogle); | |
$(document).ready(initializeDoc); | |
</script> | |
<style> | |
html | |
{ height: 100%;} | |
* | |
{ padding: 0; | |
margin: 0;} | |
#header, #main | |
{ width: 960px; | |
margin-left: auto; | |
margin-right: auto;} | |
#header | |
{ padding-top: 16px; | |
padding-bottom: 8px; | |
height: 30px;} | |
#main | |
{ height: 600px; | |
padding-bottom: 16px;} | |
#selector, #plot, #plot-hist | |
{ float: left; | |
width: 600px; | |
height: 100%;} | |
#plot, #plot-hist | |
{ background-repeat: no-repeat; | |
background-size: 600px 600px;} | |
#sidebar | |
{ float: right; | |
width: 348px; | |
height: 100%;} | |
#selRow, #selCol | |
{ width: 160px; } | |
#selCol | |
{ margin-right: 24px;} | |
#info p | |
{ padding-top: 10px; | |
font-size: small;} | |
#info input.style_checkbox | |
{ margin-left: 12px; } | |
#info input.range_input | |
{ width: 70px; | |
margin-left: 6px; | |
margin-right: 6px;} | |
#info input.key_input | |
{ margin-left: 6px; | |
width: 160px; } | |
#info button | |
{ margin-left: 24px;} | |
</style> | |
</head> | |
<body> | |
<div id="header"> | |
<div id="selector"> | |
Column (X axis): <select id="selCol"></select> | |
Row (Y axis): <select id="selRow"></select> | |
</div> | |
</div> | |
<div id="main"> | |
<div id="plot-hist">LOADING... Please be patient.</div> | |
<div id="plot"></div> | |
<div id="sidebar"> | |
<div id="info"> | |
<h4>Axis Style</h4> | |
<p>x axis: | |
<input type="checkbox" class="style_checkbox" id="x_log">log scale</input> | |
<input type="checkbox" class="style_checkbox" id="x_rev">reversed</input> | |
</p> | |
<p>y axis: | |
<input type="checkbox" class="style_checkbox" id="y_log">log scale</input> | |
<input type="checkbox" class="style_checkbox" id="y_rev">reversed</input> | |
</p> | |
<p>x limits: <input class="range_input" id="x_amin"></input> to <input class="range_input" id="x_amax"></input><button id="setLimits">Set</button></p> | |
<p>y limits: <input class="range_input" id="y_amin"></input> to <input class="range_input" id="y_amax"></input><button id="clearLimits">Clear</button></p> | |
<p><i>Setting limits is still an experimental function. For now you MUST specify all four numbers to set limits.</i></p> | |
<br><br> | |
<h4>Selection</h4> | |
<p>x range: <input class="range_input" id="xmin"></input> to <input class="range_input" id="xmax"></input><button id="selectRange">Select</button></p> | |
<p>y range: <input class="range_input" id="ymin"></input> to <input class="range_input" id="ymax"></input><button id="clearRange">Unselect</button></p> | |
<br><br> | |
<h4>Jump to</h4> | |
<p>History: <select id="history"></select></p> | |
<p>Key: <input class="key_input" id="key"></input><button id="goKey">Go!</button></p> | |
<br><br> | |
<hr> | |
<p><a href="<?=$_GET['url']?>">Source spreadsheet</a></p> | |
<p><a href="scatter.php">Generate anthoer one!</a></p> | |
<p><i>Interface by <a href="http://web.stanford.edu/~yymao/">Yao-Yuan Mao</a>, | |
with <a href="https://developers.google.com/chart/">Google Charts</a>.<br> | |
Source code <a href="https://gist.github.com/yymao/e6ab51e81a6effe6e1c8">on Gist</a></i>.</p> | |
</div> | |
</div> | |
</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment