Skip to content

Instantly share code, notes, and snippets.

@yymao
Last active August 4, 2017 17: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 yymao/e6ab51e81a6effe6e1c8 to your computer and use it in GitHub Desktop.
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.
<?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/purecss@1.0.0/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