diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..622d14a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +config +server.pyc diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..5eacb3c --- /dev/null +++ b/Readme.md @@ -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/ \ No newline at end of file diff --git a/config.sample b/config.sample new file mode 100644 index 0000000..e3d59a3 --- /dev/null +++ b/config.sample @@ -0,0 +1,2 @@ + +DB_CONNECTION=postgresql://postgres@localhost/postgres diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ee1f55f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +falcon==1.4.1 +gunicorn==19.9.0 diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..ee83456 --- /dev/null +++ b/run.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +[ -f ./env.sh ] && . ./env.sh && export DB_CONNECTION +gunicorn server:api diff --git a/server.py b/server.py new file mode 100644 index 0000000..8516162 --- /dev/null +++ b/server.py @@ -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()) diff --git a/static/css/pivot.css b/static/css/pivot.css new file mode 100644 index 0000000..4df17e8 --- /dev/null +++ b/static/css/pivot.css @@ -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;} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..7fb0aed --- /dev/null +++ b/static/index.html @@ -0,0 +1,80 @@ + + +
").html(opts.localeStrings.tooMany)); + } else { + if (values.length > 5) { + controls = $("
").appendTo(valueList); + sorter = getSort(opts.sorters, attr); + placeholder = opts.localeStrings.filterResults; + $("", { + type: "text" + }).appendTo(controls).attr({ + placeholder: placeholder, + "class": "pvtSearch" + }).bind("keyup", function() { + var accept, accept_gen, filter; + filter = $(this).val().toLowerCase().trim(); + accept_gen = function(prefix, accepted) { + return function(v) { + var real_filter, ref1; + real_filter = filter.substring(prefix.length).trim(); + if (real_filter.length === 0) { + return true; + } + return ref1 = Math.sign(sorter(v.toLowerCase(), real_filter)), indexOf.call(accepted, ref1) >= 0; + }; + }; + accept = filter.indexOf(">=") === 0 ? accept_gen(">=", [1, 0]) : filter.indexOf("<=") === 0 ? accept_gen("<=", [-1, 0]) : filter.indexOf(">") === 0 ? accept_gen(">", [1]) : filter.indexOf("<") === 0 ? accept_gen("<", [-1]) : filter.indexOf("~") === 0 ? function(v) { + if (filter.substring(1).trim().length === 0) { + return true; + } + return v.toLowerCase().match(filter.substring(1)); + } : function(v) { + return v.toLowerCase().indexOf(filter) !== -1; + }; + return valueList.find('.pvtCheckContainer p label span.value').each(function() { + if (accept($(this).text())) { + return $(this).parent().parent().show(); + } else { + return $(this).parent().parent().hide(); + } + }); + }); + controls.append($("")); + $("", { + type: "button" + }).appendTo(controls).html(opts.localeStrings.selectAll).bind("click", function() { + valueList.find("input:visible:not(:checked)").prop("checked", true).toggleClass("changed"); + return false; + }); + $("", { + type: "button" + }).appendTo(controls).html(opts.localeStrings.selectNone).bind("click", function() { + valueList.find("input:visible:checked").prop("checked", false).toggleClass("changed"); + return false; + }); + } + checkContainer = $("").addClass("pvtCheckContainer").appendTo(valueList); + ref1 = values.sort(getSort(opts.sorters, attr)); + for (n = 0, len2 = ref1.length; n < len2; n++) { + value = ref1[n]; + valueCount = attrValues[attr][value]; + filterItem = $(""); + filterItemExcluded = false; + if (opts.inclusions[attr]) { + filterItemExcluded = (indexOf.call(opts.inclusions[attr], value) < 0); + } else if (opts.exclusions[attr]) { + filterItemExcluded = (indexOf.call(opts.exclusions[attr], value) >= 0); + } + hasExcludedItem || (hasExcludedItem = filterItemExcluded); + $("").attr("type", "checkbox").addClass('pvtFilter').attr("checked", !filterItemExcluded).data("filter", [attr, value]).appendTo(filterItem).bind("change", function() { + return $(this).toggleClass("changed"); + }); + filterItem.append($("").addClass("value").text(value)); + filterItem.append($("").addClass("count").text("(" + valueCount + ")")); + checkContainer.append($("").append(filterItem)); + } + } + closeFilterBox = function() { + if (valueList.find("[type='checkbox']").length > valueList.find("[type='checkbox']:checked").length) { + attrElem.addClass("pvtFilteredAttribute"); + } else { + attrElem.removeClass("pvtFilteredAttribute"); + } + valueList.find('.pvtSearch').val(''); + valueList.find('.pvtCheckContainer p').show(); + return valueList.hide(); + }; + finalButtons = $("").appendTo(valueList); + if (values.length <= opts.menuLimit) { + $("", { + type: "button" + }).text(opts.localeStrings.apply).appendTo(finalButtons).bind("click", function() { + if (valueList.find(".changed").removeClass("changed").length) { + refresh(); + } + return closeFilterBox(); + }); + } + $("", { + type: "button" + }).text(opts.localeStrings.cancel).appendTo(finalButtons).bind("click", function() { + valueList.find(".changed:checked").removeClass("changed").prop("checked", false); + valueList.find(".changed:not(:checked)").removeClass("changed").prop("checked", true); + return closeFilterBox(); + }); + triangleLink = $("").addClass('pvtTriangle').html(" ▾").bind("click", function(e) { + var left, ref2, top; + ref2 = $(e.currentTarget).position(), left = ref2.left, top = ref2.top; + return valueList.css({ + left: left + 10, + top: top + 10 + }).show(); + }); + attrElem = $("").addClass("axis_" + i).append($("").addClass('pvtAttr').text(attr).data("attrName", attr).append(triangleLink)); + if (hasExcludedItem) { + attrElem.addClass('pvtFilteredAttribute'); + } + return unused.append(attrElem).append(valueList); + }; + for (i in shownInDragDrop) { + if (!hasProp.call(shownInDragDrop, i)) continue; + attr = shownInDragDrop[i]; + fn1(attr); + } + tr1 = $("").appendTo(uiTable); + aggregator = $("").addClass('pvtAggregator').bind("change", function() { + return refresh(); + }); + ref1 = opts.aggregators; + for (x in ref1) { + if (!hasProp.call(ref1, x)) continue; + aggregator.append($("").val(x).html(x)); + } + ordering = { + key_a_to_z: { + rowSymbol: "↕", + colSymbol: "↔", + next: "value_a_to_z" + }, + value_a_to_z: { + rowSymbol: "↓", + colSymbol: "→", + next: "value_z_to_a" + }, + value_z_to_a: { + rowSymbol: "↑", + colSymbol: "←", + next: "key_a_to_z" + } + }; + rowOrderArrow = $("", { + role: "button" + }).addClass("pvtRowOrder").data("order", opts.rowOrder).html(ordering[opts.rowOrder].rowSymbol).bind("click", function() { + $(this).data("order", ordering[$(this).data("order")].next); + $(this).html(ordering[$(this).data("order")].rowSymbol); + return refresh(); + }); + colOrderArrow = $("", { + role: "button" + }).addClass("pvtColOrder").data("order", opts.colOrder).html(ordering[opts.colOrder].colSymbol).bind("click", function() { + $(this).data("order", ordering[$(this).data("order")].next); + $(this).html(ordering[$(this).data("order")].colSymbol); + return refresh(); + }); + $("").addClass('pvtVals pvtUiCell').appendTo(tr1).append(aggregator).append(rowOrderArrow).append(colOrderArrow).append($("")); + $("").addClass('pvtAxisContainer pvtHorizList pvtCols pvtUiCell').appendTo(tr1); + tr2 = $("").appendTo(uiTable); + tr2.append($("").addClass('pvtAxisContainer pvtRows pvtUiCell').attr("valign", "top")); + pivotTable = $("").attr("valign", "top").addClass('pvtRendererArea').appendTo(tr2); + if (opts.unusedAttrsVertical === true || unusedAttrsVerticalAutoOverride) { + uiTable.find('tr:nth-child(1)').prepend(rendererControl); + uiTable.find('tr:nth-child(2)').prepend(unused); + } else { + uiTable.prepend($("").append(rendererControl).append(unused)); + } + this.html(uiTable); + ref2 = opts.cols; + for (n = 0, len2 = ref2.length; n < len2; n++) { + x = ref2[n]; + this.find(".pvtCols").append(this.find(".axis_" + ($.inArray(x, shownInDragDrop)))); + } + ref3 = opts.rows; + for (o = 0, len3 = ref3.length; o < len3; o++) { + x = ref3[o]; + this.find(".pvtRows").append(this.find(".axis_" + ($.inArray(x, shownInDragDrop)))); + } + if (opts.aggregatorName != null) { + this.find(".pvtAggregator").val(opts.aggregatorName); + } + if (opts.rendererName != null) { + this.find(".pvtRenderer").val(opts.rendererName); + } + if (!opts.showUI) { + this.find(".pvtUiCell").hide(); + } + initialRender = true; + refreshDelayed = (function(_this) { + return function() { + var exclusions, inclusions, len4, newDropdown, numInputsToProcess, pivotUIOptions, pvtVals, ref4, ref5, subopts, t, u, unusedAttrsContainer, vals; + subopts = { + derivedAttributes: opts.derivedAttributes, + localeStrings: opts.localeStrings, + rendererOptions: opts.rendererOptions, + sorters: opts.sorters, + cols: [], + rows: [], + dataClass: opts.dataClass + }; + numInputsToProcess = (ref4 = opts.aggregators[aggregator.val()]([])().numInputs) != null ? ref4 : 0; + vals = []; + _this.find(".pvtRows li span.pvtAttr").each(function() { + return subopts.rows.push($(this).data("attrName")); + }); + _this.find(".pvtCols li span.pvtAttr").each(function() { + return subopts.cols.push($(this).data("attrName")); + }); + _this.find(".pvtVals select.pvtAttrDropdown").each(function() { + if (numInputsToProcess === 0) { + return $(this).remove(); + } else { + numInputsToProcess--; + if ($(this).val() !== "") { + return vals.push($(this).val()); + } + } + }); + if (numInputsToProcess !== 0) { + pvtVals = _this.find(".pvtVals"); + for (x = t = 0, ref5 = numInputsToProcess; 0 <= ref5 ? t < ref5 : t > ref5; x = 0 <= ref5 ? ++t : --t) { + newDropdown = $("").addClass('pvtAttrDropdown').append($("")).bind("change", function() { + return refresh(); + }); + for (u = 0, len4 = shownInAggregators.length; u < len4; u++) { + attr = shownInAggregators[u]; + newDropdown.append($("").val(attr).text(attr)); + } + pvtVals.append(newDropdown); + } + } + if (initialRender) { + vals = opts.vals; + i = 0; + _this.find(".pvtVals select.pvtAttrDropdown").each(function() { + $(this).val(vals[i]); + return i++; + }); + initialRender = false; + } + subopts.aggregatorName = aggregator.val(); + subopts.vals = vals; + subopts.aggregator = opts.aggregators[aggregator.val()](vals); + subopts.renderer = opts.renderers[renderer.val()]; + subopts.rowOrder = rowOrderArrow.data("order"); + subopts.colOrder = colOrderArrow.data("order"); + exclusions = {}; + _this.find('input.pvtFilter').not(':checked').each(function() { + var filter; + filter = $(this).data("filter"); + if (exclusions[filter[0]] != null) { + return exclusions[filter[0]].push(filter[1]); + } else { + return exclusions[filter[0]] = [filter[1]]; + } + }); + inclusions = {}; + _this.find('input.pvtFilter:checked').each(function() { + var filter; + filter = $(this).data("filter"); + if (exclusions[filter[0]] != null) { + if (inclusions[filter[0]] != null) { + return inclusions[filter[0]].push(filter[1]); + } else { + return inclusions[filter[0]] = [filter[1]]; + } + } + }); + subopts.filter = function(record) { + var excludedItems, k, ref6, ref7; + if (!opts.filter(record)) { + return false; + } + for (k in exclusions) { + excludedItems = exclusions[k]; + if (ref6 = "" + ((ref7 = record[k]) != null ? ref7 : 'null'), indexOf.call(excludedItems, ref6) >= 0) { + return false; + } + } + return true; + }; + pivotTable.pivot(materializedInput, subopts); + pivotUIOptions = $.extend({}, opts, { + cols: subopts.cols, + rows: subopts.rows, + colOrder: subopts.colOrder, + rowOrder: subopts.rowOrder, + vals: vals, + exclusions: exclusions, + inclusions: inclusions, + inclusionsInfo: inclusions, + aggregatorName: aggregator.val(), + rendererName: renderer.val() + }); + _this.data("pivotUIOptions", pivotUIOptions); + if (opts.autoSortUnusedAttrs) { + unusedAttrsContainer = _this.find("td.pvtUnused.pvtAxisContainer"); + $(unusedAttrsContainer).children("li").sort(function(a, b) { + return naturalSort($(a).text(), $(b).text()); + }).appendTo(unusedAttrsContainer); + } + pivotTable.css("opacity", 1); + if (opts.onRefresh != null) { + return opts.onRefresh(pivotUIOptions); + } + }; + })(this); + refresh = (function(_this) { + return function() { + pivotTable.css("opacity", 0.5); + return setTimeout(refreshDelayed, 10); + }; + })(this); + refresh(); + this.find(".pvtAxisContainer").sortable({ + update: function(e, ui) { + if (ui.sender == null) { + return refresh(); + } + }, + connectWith: this.find(".pvtAxisContainer"), + items: 'li', + placeholder: 'pvtPlaceholder' + }); + } catch (error) { + e = error; + if (typeof console !== "undefined" && console !== null) { + console.error(e.stack); + } + this.html(opts.localeStrings.uiRenderError); + } + return this; + }; + + /* + Heatmap post-processing + */ + $.fn.heatmap = function(scope, opts) { + var colorScaleGenerator, heatmapper, i, j, l, n, numCols, numRows, ref, ref1, ref2; + if (scope == null) { + scope = "heatmap"; + } + numRows = this.data("numrows"); + numCols = this.data("numcols"); + colorScaleGenerator = opts != null ? (ref = opts.heatmap) != null ? ref.colorScaleGenerator : void 0 : void 0; + if (colorScaleGenerator == null) { + colorScaleGenerator = function(values) { + var max, min; + min = Math.min.apply(Math, values); + max = Math.max.apply(Math, values); + return function(x) { + var nonRed; + nonRed = 255 - Math.round(255 * (x - min) / (max - min)); + return "rgb(255," + nonRed + "," + nonRed + ")"; + }; + }; + } + heatmapper = (function(_this) { + return function(scope) { + var colorScale, forEachCell, values; + forEachCell = function(f) { + return _this.find(scope).each(function() { + var x; + x = $(this).data("value"); + if ((x != null) && isFinite(x)) { + return f(x, $(this)); + } + }); + }; + values = []; + forEachCell(function(x) { + return values.push(x); + }); + colorScale = colorScaleGenerator(values); + return forEachCell(function(x, elem) { + return elem.css("background-color", colorScale(x)); + }); + }; + })(this); + switch (scope) { + case "heatmap": + heatmapper(".pvtVal"); + break; + case "rowheatmap": + for (i = l = 0, ref1 = numRows; 0 <= ref1 ? l < ref1 : l > ref1; i = 0 <= ref1 ? ++l : --l) { + heatmapper(".pvtVal.row" + i); + } + break; + case "colheatmap": + for (j = n = 0, ref2 = numCols; 0 <= ref2 ? n < ref2 : n > ref2; j = 0 <= ref2 ? ++n : --n) { + heatmapper(".pvtVal.col" + j); + } + } + heatmapper(".pvtTotal.rowTotal"); + heatmapper(".pvtTotal.colTotal"); + return this; + }; + + /* + Barchart post-processing + */ + return $.fn.barchart = function(opts) { + var barcharter, i, l, numCols, numRows, ref; + numRows = this.data("numrows"); + numCols = this.data("numcols"); + barcharter = (function(_this) { + return function(scope) { + var forEachCell, max, min, range, scaler, values; + forEachCell = function(f) { + return _this.find(scope).each(function() { + var x; + x = $(this).data("value"); + if ((x != null) && isFinite(x)) { + return f(x, $(this)); + } + }); + }; + values = []; + forEachCell(function(x) { + return values.push(x); + }); + max = Math.max.apply(Math, values); + if (max < 0) { + max = 0; + } + range = max; + min = Math.min.apply(Math, values); + if (min < 0) { + range = max - min; + } + scaler = function(x) { + return 100 * x / (1.4 * range); + }; + return forEachCell(function(x, elem) { + var bBase, bgColor, text, wrapper; + text = elem.text(); + wrapper = $("").css({ + "position": "relative", + "height": "55px" + }); + bgColor = "gray"; + bBase = 0; + if (min < 0) { + bBase = scaler(-min); + } + if (x < 0) { + bBase += scaler(x); + bgColor = "darkred"; + x = -x; + } + wrapper.append($("").css({ + "position": "absolute", + "bottom": bBase + "%", + "left": 0, + "right": 0, + "height": scaler(x) + "%", + "background-color": bgColor + })); + wrapper.append($("").text(text).css({ + "position": "relative", + "padding-left": "5px", + "padding-right": "5px" + })); + return elem.css({ + "padding": 0, + "padding-top": "5px", + "text-align": "center" + }).html(wrapper); + }); + }; + })(this); + for (i = l = 0, ref = numRows; 0 <= ref ? l < ref : l > ref; i = 0 <= ref ? ++l : --l) { + barcharter(".pvtVal.row" + i); + } + barcharter(".pvtTotal.colTotal"); + return this; + }; + }); + +}).call(this); + +//# sourceMappingURL=pivot.js.map diff --git a/static/js/plotly_renderers.js b/static/js/plotly_renderers.js new file mode 100644 index 0000000..566b4b3 --- /dev/null +++ b/static/js/plotly_renderers.js @@ -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 = $("").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 = $("", { + style: "display:none;" + }).appendTo($("body")); + result = $("").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
").append(filterItem)); + } + } + closeFilterBox = function() { + if (valueList.find("[type='checkbox']").length > valueList.find("[type='checkbox']:checked").length) { + attrElem.addClass("pvtFilteredAttribute"); + } else { + attrElem.removeClass("pvtFilteredAttribute"); + } + valueList.find('.pvtSearch').val(''); + valueList.find('.pvtCheckContainer p').show(); + return valueList.hide(); + }; + finalButtons = $("
").appendTo(valueList); + if (values.length <= opts.menuLimit) { + $("", { + type: "button" + }).text(opts.localeStrings.apply).appendTo(finalButtons).bind("click", function() { + if (valueList.find(".changed").removeClass("changed").length) { + refresh(); + } + return closeFilterBox(); + }); + } + $("", { + type: "button" + }).text(opts.localeStrings.cancel).appendTo(finalButtons).bind("click", function() { + valueList.find(".changed:checked").removeClass("changed").prop("checked", false); + valueList.find(".changed:not(:checked)").removeClass("changed").prop("checked", true); + return closeFilterBox(); + }); + triangleLink = $("").addClass('pvtTriangle').html(" ▾").bind("click", function(e) { + var left, ref2, top; + ref2 = $(e.currentTarget).position(), left = ref2.left, top = ref2.top; + return valueList.css({ + left: left + 10, + top: top + 10 + }).show(); + }); + attrElem = $("").addClass("axis_" + i).append($("").addClass('pvtAttr').text(attr).data("attrName", attr).append(triangleLink)); + if (hasExcludedItem) { + attrElem.addClass('pvtFilteredAttribute'); + } + return unused.append(attrElem).append(valueList); + }; + for (i in shownInDragDrop) { + if (!hasProp.call(shownInDragDrop, i)) continue; + attr = shownInDragDrop[i]; + fn1(attr); + } + tr1 = $("").appendTo(uiTable); + aggregator = $("").addClass('pvtAggregator').bind("change", function() { + return refresh(); + }); + ref1 = opts.aggregators; + for (x in ref1) { + if (!hasProp.call(ref1, x)) continue; + aggregator.append($("").val(x).html(x)); + } + ordering = { + key_a_to_z: { + rowSymbol: "↕", + colSymbol: "↔", + next: "value_a_to_z" + }, + value_a_to_z: { + rowSymbol: "↓", + colSymbol: "→", + next: "value_z_to_a" + }, + value_z_to_a: { + rowSymbol: "↑", + colSymbol: "←", + next: "key_a_to_z" + } + }; + rowOrderArrow = $("", { + role: "button" + }).addClass("pvtRowOrder").data("order", opts.rowOrder).html(ordering[opts.rowOrder].rowSymbol).bind("click", function() { + $(this).data("order", ordering[$(this).data("order")].next); + $(this).html(ordering[$(this).data("order")].rowSymbol); + return refresh(); + }); + colOrderArrow = $("", { + role: "button" + }).addClass("pvtColOrder").data("order", opts.colOrder).html(ordering[opts.colOrder].colSymbol).bind("click", function() { + $(this).data("order", ordering[$(this).data("order")].next); + $(this).html(ordering[$(this).data("order")].colSymbol); + return refresh(); + }); + $("").addClass('pvtVals pvtUiCell').appendTo(tr1).append(aggregator).append(rowOrderArrow).append(colOrderArrow).append($("")); + $("").addClass('pvtAxisContainer pvtHorizList pvtCols pvtUiCell').appendTo(tr1); + tr2 = $("").appendTo(uiTable); + tr2.append($("").addClass('pvtAxisContainer pvtRows pvtUiCell').attr("valign", "top")); + pivotTable = $("").attr("valign", "top").addClass('pvtRendererArea').appendTo(tr2); + if (opts.unusedAttrsVertical === true || unusedAttrsVerticalAutoOverride) { + uiTable.find('tr:nth-child(1)').prepend(rendererControl); + uiTable.find('tr:nth-child(2)').prepend(unused); + } else { + uiTable.prepend($("").append(rendererControl).append(unused)); + } + this.html(uiTable); + ref2 = opts.cols; + for (n = 0, len2 = ref2.length; n < len2; n++) { + x = ref2[n]; + this.find(".pvtCols").append(this.find(".axis_" + ($.inArray(x, shownInDragDrop)))); + } + ref3 = opts.rows; + for (o = 0, len3 = ref3.length; o < len3; o++) { + x = ref3[o]; + this.find(".pvtRows").append(this.find(".axis_" + ($.inArray(x, shownInDragDrop)))); + } + if (opts.aggregatorName != null) { + this.find(".pvtAggregator").val(opts.aggregatorName); + } + if (opts.rendererName != null) { + this.find(".pvtRenderer").val(opts.rendererName); + } + if (!opts.showUI) { + this.find(".pvtUiCell").hide(); + } + initialRender = true; + refreshDelayed = (function(_this) { + return function() { + var exclusions, inclusions, len4, newDropdown, numInputsToProcess, pivotUIOptions, pvtVals, ref4, ref5, subopts, t, u, unusedAttrsContainer, vals; + subopts = { + derivedAttributes: opts.derivedAttributes, + localeStrings: opts.localeStrings, + rendererOptions: opts.rendererOptions, + sorters: opts.sorters, + cols: [], + rows: [], + dataClass: opts.dataClass + }; + numInputsToProcess = (ref4 = opts.aggregators[aggregator.val()]([])().numInputs) != null ? ref4 : 0; + vals = []; + _this.find(".pvtRows li span.pvtAttr").each(function() { + return subopts.rows.push($(this).data("attrName")); + }); + _this.find(".pvtCols li span.pvtAttr").each(function() { + return subopts.cols.push($(this).data("attrName")); + }); + _this.find(".pvtVals select.pvtAttrDropdown").each(function() { + if (numInputsToProcess === 0) { + return $(this).remove(); + } else { + numInputsToProcess--; + if ($(this).val() !== "") { + return vals.push($(this).val()); + } + } + }); + if (numInputsToProcess !== 0) { + pvtVals = _this.find(".pvtVals"); + for (x = t = 0, ref5 = numInputsToProcess; 0 <= ref5 ? t < ref5 : t > ref5; x = 0 <= ref5 ? ++t : --t) { + newDropdown = $("").addClass('pvtAttrDropdown').append($("")).bind("change", function() { + return refresh(); + }); + for (u = 0, len4 = shownInAggregators.length; u < len4; u++) { + attr = shownInAggregators[u]; + newDropdown.append($("").val(attr).text(attr)); + } + pvtVals.append(newDropdown); + } + } + if (initialRender) { + vals = opts.vals; + i = 0; + _this.find(".pvtVals select.pvtAttrDropdown").each(function() { + $(this).val(vals[i]); + return i++; + }); + initialRender = false; + } + subopts.aggregatorName = aggregator.val(); + subopts.vals = vals; + subopts.aggregator = opts.aggregators[aggregator.val()](vals); + subopts.renderer = opts.renderers[renderer.val()]; + subopts.rowOrder = rowOrderArrow.data("order"); + subopts.colOrder = colOrderArrow.data("order"); + exclusions = {}; + _this.find('input.pvtFilter').not(':checked').each(function() { + var filter; + filter = $(this).data("filter"); + if (exclusions[filter[0]] != null) { + return exclusions[filter[0]].push(filter[1]); + } else { + return exclusions[filter[0]] = [filter[1]]; + } + }); + inclusions = {}; + _this.find('input.pvtFilter:checked').each(function() { + var filter; + filter = $(this).data("filter"); + if (exclusions[filter[0]] != null) { + if (inclusions[filter[0]] != null) { + return inclusions[filter[0]].push(filter[1]); + } else { + return inclusions[filter[0]] = [filter[1]]; + } + } + }); + subopts.filter = function(record) { + var excludedItems, k, ref6, ref7; + if (!opts.filter(record)) { + return false; + } + for (k in exclusions) { + excludedItems = exclusions[k]; + if (ref6 = "" + ((ref7 = record[k]) != null ? ref7 : 'null'), indexOf.call(excludedItems, ref6) >= 0) { + return false; + } + } + return true; + }; + pivotTable.pivot(materializedInput, subopts); + pivotUIOptions = $.extend({}, opts, { + cols: subopts.cols, + rows: subopts.rows, + colOrder: subopts.colOrder, + rowOrder: subopts.rowOrder, + vals: vals, + exclusions: exclusions, + inclusions: inclusions, + inclusionsInfo: inclusions, + aggregatorName: aggregator.val(), + rendererName: renderer.val() + }); + _this.data("pivotUIOptions", pivotUIOptions); + if (opts.autoSortUnusedAttrs) { + unusedAttrsContainer = _this.find("td.pvtUnused.pvtAxisContainer"); + $(unusedAttrsContainer).children("li").sort(function(a, b) { + return naturalSort($(a).text(), $(b).text()); + }).appendTo(unusedAttrsContainer); + } + pivotTable.css("opacity", 1); + if (opts.onRefresh != null) { + return opts.onRefresh(pivotUIOptions); + } + }; + })(this); + refresh = (function(_this) { + return function() { + pivotTable.css("opacity", 0.5); + return setTimeout(refreshDelayed, 10); + }; + })(this); + refresh(); + this.find(".pvtAxisContainer").sortable({ + update: function(e, ui) { + if (ui.sender == null) { + return refresh(); + } + }, + connectWith: this.find(".pvtAxisContainer"), + items: 'li', + placeholder: 'pvtPlaceholder' + }); + } catch (error) { + e = error; + if (typeof console !== "undefined" && console !== null) { + console.error(e.stack); + } + this.html(opts.localeStrings.uiRenderError); + } + return this; + }; + + /* + Heatmap post-processing + */ + $.fn.heatmap = function(scope, opts) { + var colorScaleGenerator, heatmapper, i, j, l, n, numCols, numRows, ref, ref1, ref2; + if (scope == null) { + scope = "heatmap"; + } + numRows = this.data("numrows"); + numCols = this.data("numcols"); + colorScaleGenerator = opts != null ? (ref = opts.heatmap) != null ? ref.colorScaleGenerator : void 0 : void 0; + if (colorScaleGenerator == null) { + colorScaleGenerator = function(values) { + var max, min; + min = Math.min.apply(Math, values); + max = Math.max.apply(Math, values); + return function(x) { + var nonRed; + nonRed = 255 - Math.round(255 * (x - min) / (max - min)); + return "rgb(255," + nonRed + "," + nonRed + ")"; + }; + }; + } + heatmapper = (function(_this) { + return function(scope) { + var colorScale, forEachCell, values; + forEachCell = function(f) { + return _this.find(scope).each(function() { + var x; + x = $(this).data("value"); + if ((x != null) && isFinite(x)) { + return f(x, $(this)); + } + }); + }; + values = []; + forEachCell(function(x) { + return values.push(x); + }); + colorScale = colorScaleGenerator(values); + return forEachCell(function(x, elem) { + return elem.css("background-color", colorScale(x)); + }); + }; + })(this); + switch (scope) { + case "heatmap": + heatmapper(".pvtVal"); + break; + case "rowheatmap": + for (i = l = 0, ref1 = numRows; 0 <= ref1 ? l < ref1 : l > ref1; i = 0 <= ref1 ? ++l : --l) { + heatmapper(".pvtVal.row" + i); + } + break; + case "colheatmap": + for (j = n = 0, ref2 = numCols; 0 <= ref2 ? n < ref2 : n > ref2; j = 0 <= ref2 ? ++n : --n) { + heatmapper(".pvtVal.col" + j); + } + } + heatmapper(".pvtTotal.rowTotal"); + heatmapper(".pvtTotal.colTotal"); + return this; + }; + + /* + Barchart post-processing + */ + return $.fn.barchart = function(opts) { + var barcharter, i, l, numCols, numRows, ref; + numRows = this.data("numrows"); + numCols = this.data("numcols"); + barcharter = (function(_this) { + return function(scope) { + var forEachCell, max, min, range, scaler, values; + forEachCell = function(f) { + return _this.find(scope).each(function() { + var x; + x = $(this).data("value"); + if ((x != null) && isFinite(x)) { + return f(x, $(this)); + } + }); + }; + values = []; + forEachCell(function(x) { + return values.push(x); + }); + max = Math.max.apply(Math, values); + if (max < 0) { + max = 0; + } + range = max; + min = Math.min.apply(Math, values); + if (min < 0) { + range = max - min; + } + scaler = function(x) { + return 100 * x / (1.4 * range); + }; + return forEachCell(function(x, elem) { + var bBase, bgColor, text, wrapper; + text = elem.text(); + wrapper = $("").css({ + "position": "relative", + "height": "55px" + }); + bgColor = "gray"; + bBase = 0; + if (min < 0) { + bBase = scaler(-min); + } + if (x < 0) { + bBase += scaler(x); + bgColor = "darkred"; + x = -x; + } + wrapper.append($("").css({ + "position": "absolute", + "bottom": bBase + "%", + "left": 0, + "right": 0, + "height": scaler(x) + "%", + "background-color": bgColor + })); + wrapper.append($("").text(text).css({ + "position": "relative", + "padding-left": "5px", + "padding-right": "5px" + })); + return elem.css({ + "padding": 0, + "padding-top": "5px", + "text-align": "center" + }).html(wrapper); + }); + }; + })(this); + for (i = l = 0, ref = numRows; 0 <= ref ? l < ref : l > ref; i = 0 <= ref ? ++l : --l) { + barcharter(".pvtVal.row" + i); + } + barcharter(".pvtTotal.colTotal"); + return this; + }; + }); + +}).call(this); + +//# sourceMappingURL=pivot.js.map diff --git a/static/js/plotly_renderers.js b/static/js/plotly_renderers.js new file mode 100644 index 0000000..566b4b3 --- /dev/null +++ b/static/js/plotly_renderers.js @@ -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 = $("").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 = $("", { + style: "display:none;" + }).appendTo($("body")); + result = $("").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