mirror of
https://github.com/harish2704/sql-chart.git
synced 2026-09-10 06:21:07 +00:00
Initial commit
This commit is contained in:
@@ -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>
|
||||||
+1839
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
Reference in New Issue
Block a user