10 changed files with 2280 additions and 0 deletions
@ -0,0 +1,2 @@ |
|||
config |
|||
server.pyc |
@ -0,0 +1,19 @@ |
|||
|
|||
# SQL chart |
|||
Draw chart any SQL queries |
|||
|
|||
|
|||
## What is this? |
|||
A simple web application to visualize data from any arbitrary SQL query. |
|||
|
|||
## How it is working? |
|||
* set `DB_CONNECTION` string either in env variable or in `config` file |
|||
* Start the application by running `run.sh`. Visit 'http://127.0.0.1:8000/index.html' |
|||
* Enter the SQL query in the text box and submit. |
|||
* available columns in the data will be shown in UI. visualize the data by dragging columns into required axis. Also choose requied aggregation function for pivot table |
|||
* Entire state of the graph ( SQL, columns in each axis, aggregation function etc ) is stored in the URL ( in URL hash ) so that we can reload the page without loosing the data. |
|||
|
|||
## Thanks |
|||
* Authors of pivottable ( https://github.com/nicolaskruchten/pivottable ). The UI code is shamelessly copied from one of the examples given in this project. |
|||
* Simple backed api server ( server.js ) is written in https://falconframework.org/. |
|||
* https://plot.ly/javascript/ |
@ -0,0 +1,2 @@ |
|||
|
|||
DB_CONNECTION=postgresql://postgres@localhost/postgres |
@ -0,0 +1,2 @@ |
|||
falcon==1.4.1 |
|||
gunicorn==19.9.0 |
@ -0,0 +1,4 @@ |
|||
#!/usr/bin/env bash |
|||
|
|||
[ -f ./env.sh ] && . ./env.sh && export DB_CONNECTION |
|||
gunicorn server:api |
@ -0,0 +1,27 @@ |
|||
import falcon |
|||
import sqlalchemy |
|||
import json |
|||
import sys |
|||
import datetime |
|||
import os |
|||
|
|||
db_url = os.environ[ 'DB_CONNECTION' ] if 'DB_CONNECTION' in os.environ else 'postgresql://postgres@localhost/postgres' |
|||
eng = sqlalchemy.create_engine(db_url, pool_size=1, pool_recycle=3600) |
|||
conn = eng.connect() |
|||
print "Connected to db" |
|||
|
|||
class QResource: |
|||
def on_get(self, req, resp): |
|||
"""Handles GET requests""" |
|||
sql = req.params['q'] |
|||
result = conn.execute(sql) |
|||
data = [(dict(row.items())) for row in result] |
|||
resp.set_header('Access-Control-Allow-Origin', '*') |
|||
resp.set_header('Access-Control-Allow-Headers', 'Content-Type,Authorization') |
|||
resp.set_header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS') |
|||
resp.set_header('content-type', 'application/json; charset=UTF-8') |
|||
resp.body = json.dumps( data, default=str ) |
|||
|
|||
api = falcon.API() |
|||
api.add_static_route('/', os.path.dirname(os.path.realpath(__file__)) + '/static' ) |
|||
api.add_route('/api/data', QResource()) |
@ -0,0 +1,114 @@ |
|||
.pvtUi { color: #333; } |
|||
|
|||
|
|||
table.pvtTable { |
|||
font-size: 8pt; |
|||
text-align: left; |
|||
border-collapse: collapse; |
|||
} |
|||
table.pvtTable thead tr th, table.pvtTable tbody tr th { |
|||
background-color: #e6EEEE; |
|||
border: 1px solid #CDCDCD; |
|||
font-size: 8pt; |
|||
padding: 5px; |
|||
} |
|||
|
|||
table.pvtTable .pvtColLabel {text-align: center;} |
|||
table.pvtTable .pvtTotalLabel {text-align: right;} |
|||
|
|||
table.pvtTable tbody tr td { |
|||
color: #3D3D3D; |
|||
padding: 5px; |
|||
background-color: #FFF; |
|||
border: 1px solid #CDCDCD; |
|||
vertical-align: top; |
|||
text-align: right; |
|||
} |
|||
|
|||
.pvtTotal, .pvtGrandTotal { font-weight: bold; } |
|||
|
|||
.pvtVals { text-align: center; white-space: nowrap;} |
|||
.pvtRowOrder, .pvtColOrder { |
|||
cursor:pointer; |
|||
width: 15px; |
|||
margin-left: 5px; |
|||
display: inline-block; } |
|||
.pvtAggregator { margin-bottom: 5px ;} |
|||
|
|||
.pvtAxisContainer, .pvtVals { |
|||
border: 1px solid gray; |
|||
background: #EEE; |
|||
padding: 5px; |
|||
min-width: 20px; |
|||
min-height: 20px; |
|||
|
|||
user-select: none; |
|||
-webkit-user-select: none; |
|||
-moz-user-select: none; |
|||
-khtml-user-select: none; |
|||
-ms-user-select: none; |
|||
} |
|||
.pvtAxisContainer li { |
|||
padding: 8px 6px; |
|||
list-style-type: none; |
|||
cursor:move; |
|||
} |
|||
.pvtAxisContainer li.pvtPlaceholder { |
|||
-webkit-border-radius: 5px; |
|||
padding: 3px 15px; |
|||
-moz-border-radius: 5px; |
|||
border-radius: 5px; |
|||
border: 1px dashed #aaa; |
|||
} |
|||
|
|||
.pvtAxisContainer li span.pvtAttr { |
|||
-webkit-text-size-adjust: 100%; |
|||
background: #F3F3F3; |
|||
border: 1px solid #DEDEDE; |
|||
padding: 2px 5px; |
|||
white-space:nowrap; |
|||
-webkit-border-radius: 5px; |
|||
-moz-border-radius: 5px; |
|||
border-radius: 5px; |
|||
} |
|||
|
|||
.pvtTriangle { |
|||
cursor:pointer; |
|||
color: grey; |
|||
} |
|||
|
|||
.pvtHorizList li { display: inline; } |
|||
.pvtVertList { vertical-align: top; } |
|||
|
|||
.pvtFilteredAttribute { font-style: italic } |
|||
|
|||
.pvtFilterBox{ |
|||
z-index: 100; |
|||
width: 300px; |
|||
border: 1px solid gray; |
|||
background-color: #fff; |
|||
position: absolute; |
|||
text-align: center; |
|||
} |
|||
|
|||
.pvtFilterBox h4{ margin: 15px; } |
|||
.pvtFilterBox p { margin: 10px auto; } |
|||
.pvtFilterBox label { font-weight: normal; } |
|||
.pvtFilterBox input[type='checkbox'] { margin-right: 10px; margin-left: 10px; } |
|||
.pvtFilterBox input[type='text'] { width: 230px; } |
|||
.pvtFilterBox .count { color: gray; font-weight: normal; margin-left: 3px;} |
|||
|
|||
.pvtCheckContainer{ |
|||
text-align: left; |
|||
font-size: 14px; |
|||
white-space: nowrap; |
|||
overflow-y: scroll; |
|||
width: 100%; |
|||
max-height: 250px; |
|||
border-top: 1px solid lightgrey; |
|||
border-bottom: 1px solid lightgrey; |
|||
} |
|||
|
|||
.pvtCheckContainer p{ margin: 5px; } |
|||
|
|||
.pvtRendererArea { padding: 5px;} |
@ -0,0 +1,80 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<!-- This file is shamelessly copied from https://github.com/nicolaskruchten/pivottable/blob/master/examples/plotly.html --> |
|||
<title>Pivot Demo</title> |
|||
<!-- external libs from cdnjs --> |
|||
<script src="https://cdn.plot.ly/plotly-basic-latest.min.js"></script> |
|||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script> |
|||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script> |
|||
|
|||
<!-- PivotTable.js libs from ../dist --> |
|||
<link rel="stylesheet" type="text/css" href="./css/pivot.css"> |
|||
<script type="text/javascript" src="./js/pivot.js"></script> |
|||
<script type="text/javascript" src="./js/plotly_renderers.js"></script> |
|||
<style> |
|||
body {font-family: Verdana;} |
|||
</style> |
|||
|
|||
</head> |
|||
<body> |
|||
<form action="" method="get" accept-charset="utf-8" onsubmit="return applySQL()"> |
|||
<label for="">SQL Query</label> |
|||
<textarea name="q" id="txt_sql" rows="6" cols="80" placeholder="Enter SQL query"></textarea> |
|||
<button type="submit">Submit</button> |
|||
</form> |
|||
<script type="text/javascript"> |
|||
// This example adds Plotly chart renderers. |
|||
|
|||
$(function(){ |
|||
|
|||
var derivers = $.pivotUtilities.derivers; |
|||
var renderers = $.extend($.pivotUtilities.renderers, $.pivotUtilities.plotly_renderers); |
|||
var API = '/api/data'; |
|||
var params = document.location.hash.length > 1 ? JSON.parse( decodeURIComponent( document.location.hash.slice(1) ) ) : {}; |
|||
var sql = params.q; |
|||
var conf = params.c || {}; |
|||
$('#txt_sql').val(sql) |
|||
|
|||
function updateUrl(){ |
|||
document.location.hash = JSON.stringify( params ); |
|||
} |
|||
|
|||
window.applySQL = function(){ |
|||
params.q = $('#txt_sql').val(); |
|||
updateUrl(); |
|||
loadData(); |
|||
return false; |
|||
} |
|||
|
|||
function loadData(){ |
|||
var sql = params.q; |
|||
if( !sql ){ |
|||
console.log( 'Empty sql' ); |
|||
return; |
|||
} |
|||
console.log( "SQL", sql ); |
|||
var data = { q: sql }; |
|||
$.getJSON( API, data, function(mps) { |
|||
console.log( mps ); |
|||
$("#output").pivotUI(mps, Object.assign( { |
|||
renderers: renderers, |
|||
rendererName: "Horizontal Stacked Bar Chart", |
|||
rowOrder: "value_a_to_z", colOrder: "value_z_to_a", |
|||
onRefresh: function( config ){ |
|||
var confData = ["aggregatorName", "colOrder", "cols", "rendererName", "rowOrder", "rows", "vals"] |
|||
.reduce( ( acc, v ) => { acc[v] = config[v]; return acc; }, {} ) |
|||
params.c = confData; |
|||
updateUrl(); |
|||
} |
|||
}, conf )); |
|||
}); |
|||
} |
|||
loadData(); |
|||
}); |
|||
</script> |
|||
|
|||
<div id="output" style="margin: 30px;"></div> |
|||
|
|||
</body> |
|||
</html> |
File diff suppressed because it is too large
@ -0,0 +1,191 @@ |
|||
(function() { |
|||
var callWithJQuery; |
|||
|
|||
callWithJQuery = function(pivotModule) { |
|||
if (typeof exports === "object" && typeof module === "object") { |
|||
return pivotModule(require("jquery"), require("plotly.js")); |
|||
} else if (typeof define === "function" && define.amd) { |
|||
return define(["jquery", "plotly.js"], pivotModule); |
|||
} else { |
|||
return pivotModule(jQuery, Plotly); |
|||
} |
|||
}; |
|||
|
|||
callWithJQuery(function($, Plotly) { |
|||
var makePlotlyChart, makePlotlyScatterChart; |
|||
makePlotlyChart = function(traceOptions, layoutOptions, transpose) { |
|||
if (traceOptions == null) { |
|||
traceOptions = {}; |
|||
} |
|||
if (layoutOptions == null) { |
|||
layoutOptions = {}; |
|||
} |
|||
if (transpose == null) { |
|||
transpose = false; |
|||
} |
|||
return function(pivotData, opts) { |
|||
var colKeys, data, datumKeys, defaults, fullAggName, groupByTitle, hAxisTitle, layout, result, rowKeys, titleText, traceKeys; |
|||
defaults = { |
|||
localeStrings: { |
|||
vs: "vs", |
|||
by: "by" |
|||
}, |
|||
plotly: {} |
|||
}; |
|||
opts = $.extend(true, {}, defaults, opts); |
|||
rowKeys = pivotData.getRowKeys(); |
|||
colKeys = pivotData.getColKeys(); |
|||
traceKeys = transpose ? colKeys : rowKeys; |
|||
if (traceKeys.length === 0) { |
|||
traceKeys.push([]); |
|||
} |
|||
datumKeys = transpose ? rowKeys : colKeys; |
|||
if (datumKeys.length === 0) { |
|||
datumKeys.push([]); |
|||
} |
|||
fullAggName = pivotData.aggregatorName; |
|||
if (pivotData.valAttrs.length) { |
|||
fullAggName += "(" + (pivotData.valAttrs.join(", ")) + ")"; |
|||
} |
|||
data = traceKeys.map(function(traceKey) { |
|||
var datumKey, i, labels, len, trace, val, values; |
|||
values = []; |
|||
labels = []; |
|||
for (i = 0, len = datumKeys.length; i < len; i++) { |
|||
datumKey = datumKeys[i]; |
|||
val = parseFloat(pivotData.getAggregator(transpose ? datumKey : traceKey, transpose ? traceKey : datumKey).value()); |
|||
values.push(isFinite(val) ? val : null); |
|||
labels.push(datumKey.join('-') || ' '); |
|||
} |
|||
trace = { |
|||
name: traceKey.join('-') || fullAggName |
|||
}; |
|||
trace.x = transpose ? values : labels; |
|||
trace.y = transpose ? labels : values; |
|||
return $.extend(trace, traceOptions); |
|||
}); |
|||
if (transpose) { |
|||
hAxisTitle = pivotData.rowAttrs.join("-"); |
|||
groupByTitle = pivotData.colAttrs.join("-"); |
|||
} else { |
|||
hAxisTitle = pivotData.colAttrs.join("-"); |
|||
groupByTitle = pivotData.rowAttrs.join("-"); |
|||
} |
|||
titleText = fullAggName; |
|||
if (hAxisTitle !== "") { |
|||
titleText += " " + opts.localeStrings.vs + " " + hAxisTitle; |
|||
} |
|||
if (groupByTitle !== "") { |
|||
titleText += " " + opts.localeStrings.by + " " + groupByTitle; |
|||
} |
|||
layout = { |
|||
title: titleText, |
|||
hovermode: 'closest', |
|||
width: window.innerWidth / 1.4, |
|||
height: window.innerHeight / 1.4 - 50, |
|||
xaxis: { |
|||
title: transpose ? fullAggName : null, |
|||
automargin: true |
|||
}, |
|||
yaxis: { |
|||
title: transpose ? null : fullAggName, |
|||
automargin: true |
|||
} |
|||
}; |
|||
result = $("<div>").appendTo($("body")); |
|||
Plotly.newPlot(result[0], data, $.extend(layout, layoutOptions, opts.plotly)); |
|||
return result.detach(); |
|||
}; |
|||
}; |
|||
makePlotlyScatterChart = function() { |
|||
return function(pivotData, opts) { |
|||
var colKey, colKeys, data, defaults, i, j, layout, len, len1, renderArea, result, rowKey, rowKeys, v; |
|||
defaults = { |
|||
localeStrings: { |
|||
vs: "vs", |
|||
by: "by" |
|||
}, |
|||
plotly: {} |
|||
}; |
|||
opts = $.extend(true, {}, defaults, opts); |
|||
rowKeys = pivotData.getRowKeys(); |
|||
if (rowKeys.length === 0) { |
|||
rowKeys.push([]); |
|||
} |
|||
colKeys = pivotData.getColKeys(); |
|||
if (colKeys.length === 0) { |
|||
colKeys.push([]); |
|||
} |
|||
data = { |
|||
x: [], |
|||
y: [], |
|||
text: [], |
|||
type: 'scatter', |
|||
mode: 'markers' |
|||
}; |
|||
for (i = 0, len = rowKeys.length; i < len; i++) { |
|||
rowKey = rowKeys[i]; |
|||
for (j = 0, len1 = colKeys.length; j < len1; j++) { |
|||
colKey = colKeys[j]; |
|||
v = pivotData.getAggregator(rowKey, colKey).value(); |
|||
if (v != null) { |
|||
data.x.push(colKey.join('-')); |
|||
data.y.push(rowKey.join('-')); |
|||
data.text.push(v); |
|||
} |
|||
} |
|||
} |
|||
layout = { |
|||
title: pivotData.rowAttrs.join("-") + ' vs ' + pivotData.colAttrs.join("-"), |
|||
hovermode: 'closest', |
|||
xaxis: { |
|||
title: pivotData.colAttrs.join('-'), |
|||
domain: [0.1, 1.0] |
|||
}, |
|||
yaxis: { |
|||
title: pivotData.rowAttrs.join('-') |
|||
}, |
|||
width: window.innerWidth / 1.5, |
|||
height: window.innerHeight / 1.4 - 50 |
|||
}; |
|||
renderArea = $("<div>", { |
|||
style: "display:none;" |
|||
}).appendTo($("body")); |
|||
result = $("<div>").appendTo(renderArea); |
|||
Plotly.plot(result[0], [data], $.extend(layout, opts.plotly)); |
|||
result.detach(); |
|||
renderArea.remove(); |
|||
return result; |
|||
}; |
|||
}; |
|||
return $.pivotUtilities.plotly_renderers = { |
|||
"Horizontal Bar Chart": makePlotlyChart({ |
|||
type: 'bar', |
|||
orientation: 'h' |
|||
}, { |
|||
barmode: 'group' |
|||
}, true), |
|||
"Horizontal Stacked Bar Chart": makePlotlyChart({ |
|||
type: 'bar', |
|||
orientation: 'h' |
|||
}, { |
|||
barmode: 'relative' |
|||
}, true), |
|||
"Bar Chart": makePlotlyChart({ |
|||
type: 'bar' |
|||
}, { |
|||
barmode: 'group' |
|||
}), |
|||
"Stacked Bar Chart": makePlotlyChart({ |
|||
type: 'bar' |
|||
}, { |
|||
barmode: 'relative' |
|||
}), |
|||
"Line Chart": makePlotlyChart(), |
|||
"Scatter Chart": makePlotlyScatterChart() |
|||
}; |
|||
}); |
|||
|
|||
}).call(this); |
|||
|
|||
//# sourceMappingURL=plotly_renderers.js.map
|
Loading…
Reference in new issue