diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..a1d7019 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "public/lib" +} diff --git a/.gitignore b/.gitignore index 2736604..43f8c66 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ -node_modules +*.iml +.idea .jshintrc -Procfile -.idea/ atlassian-ide-plugin.xml +bower_components +lib +node_modules +tmp +public/lib + diff --git a/README.md b/README.md new file mode 100644 index 0000000..934e37d --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +shopcade_analytics +================== + +Shopcade analytics workspace diff --git a/bin/viewServer.js b/bin/viewServer.js new file mode 100644 index 0000000..a86a65e --- /dev/null +++ b/bin/viewServer.js @@ -0,0 +1,43 @@ +var express = require('express'), + util = require('util'), + path = require('path'), + http = require('http'), + fs = require('fs'), + app = express(), + port = 3001; + +app.use("/", express.static('public')); +// env + +app.set('port', process.env.port || port); +app.set('views', path.join(__dirname, '/public/views')); +app.set('view engine', 'jade'); +//app.use(express.bodyParser()); +app.disable('x-powered-by'); + +/** + * route + */ +app.get('/', function (req, res) { + res.render('overview', {title: 'overview'}); +}); + +app.get('/:page', function (req, res) { + var page = req.params.page; + res.render(page, {title: page}); +}); + +/** + * read fs + */ +fs.readdirSync('public').forEach(function (file) { // iterate through every sub-folder + if (!file.match(/(\.ico$|views)/)) { + app.use(express.static('public/' + file)); + } +}); + +(function () { + http.createServer(app).listen(app.get('port'), function (req, res) { + util.log('View server listening on port ' + port); + }); +})(); diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..1c0253f --- /dev/null +++ b/bower.json @@ -0,0 +1,26 @@ +{ + "name": "analytics", + "version": "0.0.1", + "ignore": [ + ".jshintrc", + "**/*.txt", + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "jquery": "2.1.0", + "bootstrap": "3.1.1", + "d3": "3.3.*", + "crossfilter": "1.2.0", + "nvd3": "1.1.*", + "dcjs": "1.6.0" + }, + "directory": "public/lib", + "authors": [ + "yeqing " + ], + "license": "MIT" +} diff --git a/examples/frontend-test/filters.js b/examples/frontend-test/filters.js new file mode 100644 index 0000000..5c2a175 --- /dev/null +++ b/examples/frontend-test/filters.js @@ -0,0 +1,211 @@ +var FilterModel = Backbone.Model.extend({ + initialize: function () { + console.log('model is created'); + }, + defaults: { + name: 'Filter', + /** + * {: {: }} + */ + condition: null, + change: function () { + var _condition = {}; + _condition[this.get('field')] = {}; + _condition[this.get('field')][this.get('operator')] = this.get('value'); + this.set('condition', _condition); + }, + render: function () { + return this.get('condition'); + }, + reset: function () { + this.set('condition', null); + } + } +}); + +var FilterView = Backbone.View.extend({ + tagName: 'li', + + events: { + 'click button.remove': 'remove', + 'change input,select': 'change' + }, + + initialize: function () { + _.bindAll(this, 'render', 'unrender', 'remove', 'reset', 'change'); + this.model.bind('change', this.change); + this.model.bind('remove', this.unrender); + this.model.bind('reset', this.reset); + + this._create_condition_html = function () { + var _columns = []; + columns.forEach(function (d) { + if (!d.match('dim')) { + _columns.push(d); + } + }); + var $li = $('
  • '); + $li + .append(createSelOpts( + createElem('select', {className: 'field', placeholder: 'field name'}), + _columns + )) + .append(createSelOpts( + createElem('select', {className: 'operator'}), + [ + 'equal to (=)', + 'not equal to (!=)', + 'greater than or equal to (>=)', + 'greater than (>)', + 'smaller than or equal to (<=)', + 'smaller than (<)', + 'between (e.g. 0-10)', + 'contains (regex)' + ], + { + 'equal to (=)': null, + 'not equal to (!=)': '$ne', + 'greater than or equal to (>=)': '$gte', + 'greater than (>)': '$gt', + 'smaller than or equal to (<=)': '$lte', + 'smaller than (<)': '$lt', + 'between (e.g. 0-10)': 'btwn', + 'contains (regex)': '$regex' + } + )) + .append(createElem('input', {className: 'value', placeholder: 'value'})) + .append(createElem('button', {className: 'reset'}, 'reset')) + .append(createElem('button', {className: 'remove'}, 'remove')) +// .append(createElem('div', {className: 'text hidden'})) //debug + return $li.html(); + }; + }, + + render: function () { + $(this.el).append(this._create_condition_html()); + return this; + }, + + unrender: function () { + $(this.el).remove(); + }, + + remove: function () { + this.model.destroy(); + }, + + reset: function () { + $('input', this.el).val(''); + this.model.set('condition', {}); + }, + + /** + * {: {: }} + */ + change: function () { + var field = $('.field', this.el).val(), + operator = $('.operator', this.el).val(), + value = $('.value', this.el).val(); + value = !isNaN(Number(value)) ? Number(value) : value; + var _condition = {}; + _condition[field] = {}; + if (String(operator) == 'null') { // equal to + _condition[field] = value; + } else if (operator == 'btwn') { // between + if (value) { + value = value.split('-'); + if (value.length > 1) { + value.forEach(function (d, i) { + value[i] = !isNaN(Number(value[i].split(' ').join(''))) ? Number(value[i].split(' ').join('')) : value[i]; + }); + _condition[field]['$gte'] = value[0]; + _condition[field]['$lte'] = value[1]; + } + } + } else if (String(operator) != 'null') { + _condition[field][operator] = value; + } else { + //TODO + } + this.model.set('condition', _condition); +// $('div.text', this.el).text(JSON.stringify(this.model.get('condition'))); //debug + } +}); + +var FiltersCollection = Backbone.Collection.extend({ + model: FilterModel +}); + +var FiltersCollectionView = Backbone.View.extend({ + el: '', + + events: { + 'click button#add': 'addItem' + }, + + initialize: function () { + _.bindAll(this, 'render', 'addItem', 'appendItem', 'change'); + this.counter = 0; + this.conditions = {}; + this.getConditions = function () { + if (_size(this.conditions)) { + return this.conditions + } else { + return null; + } + }; + + this.collection = new FiltersCollection(); + this.collection.bind('add', this.appendItem); + this.collection.bind('change', this.change); + this.collection.bind('remove', this.change); + + this.render(); + }, + + render: function () { + var _this = this; + var $div = $('
    ') + .append(createElem('button', {id: 'add'}, 'Add Condition')) + .append(createElem('div', {className: 'text hidden'})) //debug + var $ul = $('
      '); + $(this.el).html($div.append($ul).html()); + _(this.collection.models).each(function (item) { // in case collection is not empty + _this.appendItem(item); + }, this); + }, + + addItem: function () { + this.counter++; + var item = new FilterModel(); + this.collection.add(item); + }, + + appendItem: function (item) { + var itemView = new FilterView({ + model: item + }); + $('ul', this.el).append(itemView.render().el); + }, + + change: function () { +// $('div.text', this.el).text(JSON.stringify(this.output())); //debug + this.conditions = this.output(); + }, + /** + * output conditions object + * @returns {object} + */ + output: function () { + var _conditions = {}; + _(this.collection.models).each(function (item) { + var _condition = item.get('condition'); + for (var key in _condition) { + if (_condition.hasOwnProperty(key)) { + _conditions[key] = _condition[key]; + } + } + }); + return _conditions; + } +}); \ No newline at end of file diff --git a/examples/frontend-test/nvd3.js b/examples/frontend-test/nvd3.js new file mode 100644 index 0000000..afa3d34 --- /dev/null +++ b/examples/frontend-test/nvd3.js @@ -0,0 +1,159 @@ + + +(function() { + + var mainExample, exampleOne, exampleTwo, exampleThree; + + //var colors = d3.scale.category20().range(); + + var test_data = stream_layers(3,20 + Math.random()*50,.1).map(function(data, i) { + return { + key: 'Stream' + i + , values: data + //, color: colors[i] + }; + }); + + + // --------------------------- MAIN EXAMPLE --------------------------------- + + + nv.addGraph(function() { + var chart = nv.models.multiBarChart() + .margin({top: 50, bottom: 30, left: 40, right: 10}); + + chart.xAxis + .tickFormat(d3.format(',f')); + + chart.yAxis + .tickFormat(d3.format(',.1f')); + + d3.select('#mainExample') + .datum(test_data) + .transition().duration(500).call(chart); + + nv.utils.windowResize(chart.update); + + chart.legend.dispatch.on('legendClick.updateExamples', function() { + setTimeout(function() { + exampleOne.update(); + exampleTwo.update(); + exampleThree.update(); + }, 100); + }); + + mainExample = chart; + + return chart; + }); + + + + // --------------------------- EXAMPLE ONE --------------------------------- + + + nv.addGraph(function() { + var chart = nv.models.lineChart() + .showLegend(false) + .margin({top: 10, bottom: 30, left: 40, right: 10}) + .useInteractiveGuideline(true) + ; + + chart.xAxis // chart sub-models (ie. xAxis, yAxis, etc) when accessed directly, return themselves, not the partent chart, so need to chain separately + .tickFormat(d3.format(',r')); + + chart.yAxis + .tickFormat(d3.format(',.1f')); + + d3.select('#exampleOne') + .datum(test_data) + .transition().duration(500) + .call(chart); + + //TODO: Figure out a good way to do this automatically + nv.utils.windowResize(chart.update); + //nv.utils.windowResize(function() { d3.select('#chart1 svg').call(chart) }); + + exampleOne = chart; + + return chart; + }); + + + // --------------------------- EXAMPLE TWO --------------------------------- + + + + nv.addGraph(function() { + var chart = nv.models.stackedAreaChart() + .margin({top: 10, bottom: 30, left: 40, right: 10}) + .showControls(false) + .showLegend(false) + .useInteractiveGuideline(true) + .style('stream'); + + chart.yAxis + .showMaxMin(false) + .tickFormat(d3.format(',.1f')); + + d3.select("#exampleTwo") + .datum(test_data) + .transition().duration(500).call(chart); + + nv.utils.windowResize(chart.update); + + + chart.stacked.dispatch.on('areaClick.updateExamples', function(e) { + setTimeout(function() { + mainExample.update(); + exampleOne.update(); + //exampleTwo.update(); + exampleThree.update(); + }, 100); + }) + + exampleTwo = chart; + + return chart; + }); + + + + // --------------------------- EXAMPLE THREE --------------------------------- + + + nv.addGraph(function() { + var chart = nv.models.stackedAreaChart() + .margin({top: 10, bottom: 30, left: 40, right: 10}) + .showControls(false) + .showLegend(false) + .useInteractiveGuideline(true) + .style('stacked'); + + chart.yAxis + .tickFormat(d3.format(',.1f')); + + d3.select("#exampleThree") + .datum(test_data) + .transition().duration(500).call(chart); + + nv.utils.windowResize(chart.update); + + + chart.stacked.dispatch.on('areaClick.updateExamples', function(e) { + setTimeout(function() { + mainExample.update(); + exampleOne.update(); + exampleTwo.update(); + //exampleThree.update(); + }, 100); + }) + + exampleThree = chart; + + return chart; + }); + + +})(); + diff --git a/package.json b/package.json index 0936e12..ac4e3be 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,28 @@ { - "name": "cube", - "version": "0.2.12", - "description": "A system for analyzing time series data using MongoDB and Node.", + "name": "analytics.shopcade.com", + "version": "0.0.1", + "description": "Realtime analytics based on mongodb inspired from cube", + "private": true, + "homepage": "http://analytics.shopcade.com", "keywords": [ - "time series" + "time series", "realtime", "aggregation", "analytics", "dashboard" ], - "homepage": "http://square.github.com/cube/", - "author": { - "name": "Mike Bostock", - "url": "http://bost.ocks.org/mike" - }, - "repository": { - "type": "git", - "url": "http://github.com/square/cube.git" - }, - "main": "./lib/cube", + "authors": [ + { + "name": "Mircea Danila Dumitrescu", + "url": "http://venatir.com" + }, + { + "name": "Yeqing Zhang", + "url": "http://github.com/tikazyq" + } + ], + "main": "./lib/analytics", "dependencies": { + "jade": "*", + "express": "4.*", + "bower": "1.3.*", "mongodb": "~1.3.18", - "node-static": "0.6.5", "ws": "0.4.31" } -} +} \ No newline at end of file diff --git a/public/css/dashboard.css b/public/css/dashboard.css new file mode 100644 index 0000000..cff65c2 --- /dev/null +++ b/public/css/dashboard.css @@ -0,0 +1,100 @@ +/* + * Base structure + */ + +/* Move down content because we have a fixed navbar that is 50px tall */ +body { + padding-top: 50px; +} + +/* + * Global add-ons + */ + +.sub-header { + padding-bottom: 10px; + border-bottom: 1px solid #eee; +} + +/* + * Sidebar + */ + +/* Hide for mobile, show later */ +.sidebar { + display: none; +} + +@media (min-width: 768px) { + .sidebar { + position: fixed; + top: 51px; + bottom: 0; + left: 0; + z-index: 1000; + display: block; + padding: 20px; + overflow-x: hidden; + overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ + background-color: #f5f5f5; + border-right: 1px solid #eee; + } +} + +/* Sidebar navigation */ +.nav-sidebar { + margin-right: -21px; /* 20px padding + 1px border */ + margin-bottom: 20px; + margin-left: -20px; +} + +.nav-sidebar > li > a { + padding-right: 20px; + padding-left: 20px; +} + +.nav-sidebar > .active > a { + color: #fff; + background-color: #428bca; +} + +/* + * Main content + */ + +.main { + padding: 20px; +} + +@media (min-width: 768px) { + .main { + padding-right: 40px; + padding-left: 40px; + } +} + +.main .page-header { + margin-top: 0; +} + +/* + * Placeholder dashboard ideas + */ + +.placeholders { + margin-bottom: 30px; + text-align: center; +} + +.placeholders h4 { + margin-bottom: 0; +} + +.placeholder { + margin-bottom: 20px; +} + +.placeholder img { + display: inline-block; + border-radius: 50%; +} diff --git a/public/css/dc.css b/public/css/dc.css new file mode 100644 index 0000000..caa12ec --- /dev/null +++ b/public/css/dc.css @@ -0,0 +1,308 @@ +div.dc-chart { + float: left; +} + +.dc-chart rect.bar { + stroke: none; + cursor: pointer; +} + +.dc-chart rect.bar:hover { + fill-opacity: .5; +} + +.dc-chart rect.stack1 { + stroke: none; + fill: red; +} + +.dc-chart rect.stack2 { + stroke: none; + fill: green; +} + +.dc-chart rect.deselected { + stroke: none; + fill: #ccc; +} + +.dc-chart .pie-slice { + fill: white; + font-size: 12px; + cursor: pointer; +} + +.dc-chart .pie-slice.external { + fill: black; +} + +.dc-chart .pie-slice :hover { + fill-opacity: .8; +} + +.dc-chart .pie-slice.highlight { + fill-opacity: .8; +} + +.dc-chart .selected path { + stroke-width: 3; + stroke: #ccc; + fill-opacity: 1; +} + +.dc-chart .deselected path { + stroke: none; + fill-opacity: .5; + fill: #ccc; +} + +.dc-chart .axis path, .axis line { + fill: none; + stroke: #000; + shape-rendering: crispEdges; +} + +.dc-chart .axis text { + font: 10px sans-serif; +} + +.dc-chart .grid-line { + fill: none; + stroke: #ccc; + opacity: .5; + shape-rendering: crispEdges; +} + +.dc-chart .grid-line line { + fill: none; + stroke: #ccc; + opacity: .5; + shape-rendering: crispEdges; +} + +.dc-chart .brush rect.background { + z-index: -999; +} + +.dc-chart .brush rect.extent { + fill: steelblue; + fill-opacity: .125; +} + +.dc-chart .brush .resize path { + fill: #eee; + stroke: #666; +} + +.dc-chart path.line { + fill: none; + stroke-width: 1.5px; +} + +.dc-chart circle.dot { + stroke: none; +} + +.dc-chart g.dc-tooltip path { + fill: none; + stroke: grey; + stroke-opacity: .8; +} + +.dc-chart path.area { + fill-opacity: .3; + stroke: none; +} + +.dc-chart .node { + font-size: 0.7em; + cursor: pointer; +} + +.dc-chart .node :hover { + fill-opacity: .8; +} + +.dc-chart .selected circle { + stroke-width: 3; + stroke: #ccc; + fill-opacity: 1; +} + +.dc-chart .deselected circle { + stroke: none; + fill-opacity: .5; + fill: #ccc; +} + +.dc-chart .bubble { + stroke: none; + fill-opacity: 0.6; +} + +.dc-data-count { + float: right; + margin-top: 15px; + margin-right: 15px; +} + +.dc-data-count .filter-count { + color: #3182bd; + font-weight: bold; +} + +.dc-data-count .total-count { + color: #3182bd; + font-weight: bold; +} + +.dc-data-table { +} + +.dc-chart g.state { + cursor: pointer; +} + +.dc-chart g.state :hover { + fill-opacity: .8; +} + +.dc-chart g.state path { + stroke: white; +} + +.dc-chart g.selected path { +} + +.dc-chart g.deselected path { + fill: grey; +} + +.dc-chart g.selected text { +} + +.dc-chart g.deselected text { + display: none; +} + +.dc-chart g.county path { + stroke: white; + fill: none; +} + +.dc-chart g.debug rect { + fill: blue; + fill-opacity: .2; +} + +.dc-chart g.row rect { + fill-opacity: 0.8; + cursor: pointer; +} + +.dc-chart g.row rect:hover { + fill-opacity: 0.6; +} + +.dc-chart g.row text { + fill: white; + font-size: 12px; + cursor: pointer; +} + +.dc-legend { + font-size: 11px; +} + +.dc-legend-item { + cursor: pointer; +} + +.dc-chart g.axis text { + /* Makes it so the user can't accidentally click and select text that is meant as a label only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10 */ + -o-user-select: none; + user-select: none; + pointer-events: none; +} + +.dc-chart path.highlight { + stroke-width: 3; + fill-opacity: 1; + stroke-opacity: 1; +} + +.dc-chart .highlight { + fill-opacity: 1; + stroke-opacity: 1; +} + +.dc-chart .fadeout { + fill-opacity: 0.2; + stroke-opacity: 0.2; +} + +.dc-chart path.dc-symbol, g.dc-legend-item.fadeout { + fill-opacity: 0.5; + stroke-opacity: 0.5; +} + +.dc-hard .number-display { + float: none; +} + +.dc-chart .box text { + font: 10px sans-serif; + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10 */ + -o-user-select: none; + user-select: none; + pointer-events: none; +} + +.dc-chart .box line, +.dc-chart .box circle { + fill: #fff; + stroke: #000; + stroke-width: 1.5px; +} + +.dc-chart .box rect { + stroke: #000; + stroke-width: 1.5px; +} + +.dc-chart .box .center { + stroke-dasharray: 3, 3; +} + +.dc-chart .box .outlier { + fill: none; + stroke: #ccc; +} + +.dc-chart .box.deselected .box { + fill: #ccc; +} + +.dc-chart .box.deselected { + opacity: .5; +} + +.dc-chart .symbol { + stroke: none; +} + +.dc-chart .heatmap .box-group.deselected rect { + stroke: none; + fill-opacity: .5; + fill: #ccc; +} + +.dc-chart .heatmap g.axis text { + pointer-events: all; + cursor: pointer; +} \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..d2f16b6 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/js/analytics.js b/public/js/analytics.js new file mode 100644 index 0000000..13aa66a --- /dev/null +++ b/public/js/analytics.js @@ -0,0 +1,759 @@ +/** + * create a real time instance for events line chart. + * To make it work, an HTML element with a specific ID needs to be added in the HTML file. + * var rt = new RealTimeEvents(); + * rt.setChartAnchor('#line-chart'); + * rt.init(); + */ +function RealTimeEvents() { + /** + * add chart object + * @param callback + */ + this.addCharts = function (callback) { + nv.addGraph(function () { + _this.chart + .showLegend(false) + .margin({top: 10, bottom: 30, left: 60, right: 60}) + .useInteractiveGuideline(true) + ; + + _this.chart.xAxis // chart sub-models (ie. xAxis, yAxis, etc) when accessed directly, return themselves, not the partent chart, so need to chain separately + .tickFormat(function (d) { + return d3.time.format('%X')(new Date(d)) + }); + + _this.chart.yAxis + .tickFormat(function (d) { + return d3.format('d')(d); + }) + .axisLabel('Count') + ; + return _this.chart; + }); + + if (callback) { + callback(); + } + }; + + /** + * refresh chart + * @param callback + */ + this.refreshChart = function (callback) { + d3.select(_this.chartAnchor + ' svg') + .datum([ + { + key: 'Events', + values: _this.chartData + } + ]) + .transition().duration(1) + .call(_this.chart); + + _this.chart.update(); + + //TODO: Figure out a good way to do this automatically + nv.utils.windowResize(_this.chart.update); + + if (callback) { + callback(); + } + }; + + /** + * refresh chartData + * @param callback + */ + this.refreshChartData = function (callback) { + var timer = _this.timer; + _this.chartData = []; + for (var t = timer.min; t < timer.now; t += 1e3) { + var x = new Date(t); + var y = _this._data[t] ? _this._data[t] : 0; + _this.chartData.push({ + x: x, + y: y + }); + } + + if (callback) { + callback(); + } + }; + + /** + * handle incoming event data + * @param eventData + */ + this.dataHandler = function (eventData) { + var t = eventData['time'] - eventData['time'] % 1e3; + if (typeof _this._data[t] === 'undefined') { + _this._data[t] = 0; + } + _this._data[t]++; + }; + + /** + * reset data + */ + this.resetData = function (callback) { + var i; + for (i in _this._data) { + if (_this._data.hasOwnProperty(i)) { + if (Number(i) < _this.timer.min) { + delete _this._data[i]; + } + } + } + if (callback) { + callback(); + } + }; + + /** + * update timer + * @param [callback] + */ + this.updateTimer = function (callback) { + var timer = _this.timer; + timer.nowReal = new Date().getTime(); + timer.now = timer.min = timer.nowReal - timer.nowReal % 1e3; + timer.min = timer.now - timer.timePeriod; // start of chart + timer.max = timer.now - timer.now % 1e3; + if (callback) { + callback(); + } + }; + + /** + * refresh all + */ + this.refreshAll = function () { + _this.refreshChartData(function () { + if (_this.chartData && _this.chartData.length) { + _this.refreshChart(function () { + _this.resetData(); + }); + } + _this.updateTimer(); + }); + }; + + /** + * init HTML + */ + this.initHtml = function (callback) { + if ($(_this.chartAnchor + ' svg').length == 0) { // create svg + $(_this.chartAnchor) + .append(createElem('div', {className: 'control'})) // for control buttons + .append('') // svg for chart + ; + } + + $(_this.chartAnchor + ' .control') + .append(createElem('span', {className: 'title'}, 'Events')) // title + .append(createElem('button', {className: 'pause'}, 'Pause')) // pause button + .append(createElem('button', {className: 'resume'}, 'Resume')) // resume + ; + + $(_this.chartAnchor + ' button.pause') + .click(function () { + _this.pause(); + }) + ; + $(_this.chartAnchor + ' button.resume') + .click(function () { + _this.resume(); + }) + ; + + if (callback) { + callback(); + } + }; + + /** + * major init + */ + this.init = function () { + _this.initSocket(function () { + _this.initHtml(function () { + _this.addCharts(function () { + setTimeout(function () { + _this.intervalHandle = setInterval(function () { + _this.refreshAll(); + }, _this.refreshFrequency); + }, 1e3); + }); + }) + }); + }; + + /** + * initialize socket + * @param [callback] + */ + this.initSocket = function (callback) { + if (_this.socket != null) { + _this.resetData(function () { + _this._data = {}; + _this.chartData = []; +// _this.refreshAll(); + }); + _this.socket.close(); + } + _this.socket = new WebSocket(_this.socketConnection); + _this.socket.onopen = function () { + console.log("connected to " + _this.socketConnection); + _this.socket.send(JSON.stringify(_this.socketPacket)); + }; + _this.socket.onmessage = function (message) { + var event; + if (message) { + if (message.data) { + event = JSON.parse(message.data); + if (event && event.data) { + _this.dataHandler(event); + } + } + } + }; + _this.socket.onclose = function () { + console.log("closed"); + }; + _this.socket.onerror = function (error) { + console.log("error", error); + }; + if (callback) { + callback(); + } + }; + + /** + * set eventType + * @param eventType + */ + this.setEventType = function (eventType) { + _this.eventType = eventType; + console.log('eventType is set to ' + _this.eventType); + }; + + /** + * set chart html anchor + * @param chartAnchor + */ + this.setChartAnchor = function (chartAnchor) { + if (!chartAnchor.match('^#.*')) { + chartAnchor = '#' + chartAnchor; + } + _this.chartAnchor = chartAnchor; + console.log('chartAnchor is set to ' + _this.chartAnchor); + }; + + /** + * set timePeriod + * @param timePeriod + * @param [callback] + */ + this.setTimePeriod = function (timePeriod, callback) { + _this.timer.timePeriod = timePeriod; + _this._updateSocketPacket(); + console.log('timePeriod is set to ' + _this.timer.timePeriod); + if (callback) { + callback(); + } + }; + + this._updateSocketPacket = function () { + _this.socketPacket = { + type: _this.eventType, + start: new Date(new Date().getTime() - _this.timer.timePeriod) + }; + }; + + /** + * set socketConnection + * @param value + */ + this.setSocketConnection = function (value) { + _this.socketConnection = value; + }; + + /** + * set socketPacket + * @param value + */ + this.setSocketPacket = function (value) { + _this.socketPacket = value; + }; + /** + * set timePeriod + * @param value + */ + this.setsampleDuration = function (value) { + _this.timer.timePeriod = value; + }; + + /** + * pause refresh data + */ + this.pause = function () { + clearInterval(_this.intervalHandle); + }; + + /** + * resume + */ + this.resume = function () { + setTimeout(function () { + _this.intervalHandle = setInterval(function () { + _this.refreshAll(); + }, _this.refreshFrequency); + }, 1e3); + }; + + /** + * test + */ + this.test = function () { + _this.setEventType('type1'); + _this.setChartAnchor('line-chart'); + _this.init(); + }; + + // variables + var _this = this; + this.eventType = 'type1'; // event type + this.chartAnchor = ''; + this.chartData = []; // chartData should be formatted like {x: new Date, y: 130}... + this._data = {}; // temporary data dictionary for aggregation + this.timer = { + timePeriod: 60 * 1e3, // 60 seconds + now: null, + nowReal: null, + min: null, + max: null + }; + this.refreshFrequency = 1e3; + this.chart = nv.models.lineChart(); + this.socket = null; + this.socketConnection = 'ws://localhost:1081/1.0/event/get'; + this.socketPacket = { + type: _this.eventType, + start: new Date(new Date().getTime() - _this.timer.timePeriod) + }; + this.intervalHandle = null; +} + +/** + * create a real time instance for aggregation stack chart. + * @type {RealTimeEvents} + */ +function RealTimeAggregations() { + /** + * add chart objects + * @param callback + */ + this.addCharts = function (callback) { + _this.dimTargetKeys.forEach(function (dimTargetKey) { + var chart = nv.models.stackedAreaChart(); + nv.addGraph(function () { + chart + .showLegend(false) + .margin({top: 10, bottom: 30, left: 60, right: 60}) + .useInteractiveGuideline(true) + ; + + chart.xAxis // chart sub-models (ie. xAxis, yAxis, etc) when accessed directly, return themselves, not the partent chart, so need to chain separately + .tickFormat(function (d) { + return d3.time.format('%X')(new Date(d)) + }); + + chart.yAxis + .tickFormat(function (d) { + return d3.format('d')(d); + }) + .axisLabel('Count') + ; + return chart; + }); + + _this.charts[dimTargetKey] = chart; + }); + + if (callback) { + callback(); + } + }; + + /** + * refresh chart + * @param callback + */ + this.refreshChart = function (callback) { + _this.dimTargetKeys.forEach(function (dimTargetKey) { + var chartId = _this.chartAnchor.replaceAll('#') + '-' + dimTargetKey.replaceAll('.', '-'), + chart = _this.charts[dimTargetKey], + chartData = _this.chartDataList[dimTargetKey]; + d3.select('#' + chartId + ' svg') + .datum(chartData) + .transition().duration(1) + .call(chart); + + chart.update(); + }); + + //TODO: Figure out a good way to do this automatically +// nv.utils.windowResize(_this.chart.update); + + if (callback) { + callback(); + } + }; + + /** + * refresh chartData + * @param callback + */ + this.refreshChartData = function (callback) { + _this.chartDataList = {}; + var ndx = crossfilter(_this._data), + dimTime = ndx.dimension(function (d) { + return new Date(d['time']); + }); +// console.log(grpDimTime.all());//debug + _this.dimTargetKeys.forEach(function (dimTargetKey) { + var chartData = [], // store all data series for a stack chart + dimTarget = ndx.dimension(function (d) { + return d[dimTargetKey]; + }), + dimTargetValsUniq = _this._data.ix(dimTargetKey).unique(); + dimTargetValsUniq.forEach(function (v) { + var t, + tmp = {}; + dimTarget.filter(v); + var values = dimTime.group().reduceSum(function (d) { + return d['count']; + }).all(); // store values for one series in a stack chart + values.forEach(function (d) { + tmp[d.key.getTime()] = d.value; + }); + values = []; + for (t = _this.timer.min; t < _this.timer.now; t += _this.resolution) { + var x = new Date(t), + y = tmp[t] ? tmp[t] : 0; + values.push({ + x: x, + y: y + }) + } + chartData.push({ + key: v, + values: values + }); + }); + dimTarget.filter(null); + _this.chartDataList[dimTargetKey] = chartData; + }); + + if (callback) { + callback(); + } + }; + + /** + * handle incoming event data + * @param aggregationData + */ + this.dataHandler = function (aggregationData) { + _this._data.push(JSON.flatten(aggregationData)); + }; + + /** + * reset data + */ + this.resetData = function (callback) { + var tmp = []; + _this._data.forEach((function (d) { + if (d['time'] >= _this.timer.min) { + tmp.push(d); + } + })); + _this._data = tmp; + if (callback) { + callback(); + } + }; + + /** + * update timer + * @param [callback] + */ + this.updateTimer = function (callback) { + var timer = _this.timer; + timer.nowReal = new Date().getTime(); + timer.now = timer.nowReal - timer.nowReal % _this.resolution; + timer.min = (timer.now - timer.timePeriod) - (timer.now - timer.timePeriod) % _this.resolution; + timer.max = timer.now - timer.now % _this.resolution; + if (callback) { + callback(); + } + }; + + /** + * refresh all + */ + this.refreshAll = function () { + _this.refreshChartData(function () { + _this.refreshChart(function () { + _this.resetData(); + }); + _this.updateTimer(); + }); + }; + + /** + * init HTML + */ + this.initHtml = function (callback) { + var $tmp = $('
      '), + $anchor = $(_this.chartAnchor), + $control = $(createElem('div', {className: 'control'})); // elements in control block + // control block TODO: add listeners to control stack area + var selectorLookup = { + 'data.v1': ['male', 'female', 'all'], + 'data.v2': ['web', 'android', 'web', 'all'], + 'data.v3': ['GB', 'US', 'IN', 'JP', 'all'] + }; + _this.dimTargetKeys.forEach(function (dimTargetKey) { + var selectorId = _this.chartAnchor.replaceAll('#') + '-selector-' + dimTargetKey.replaceAll('.', '-'), + $selector = $(createElem('select', {id: selectorId})); + $selector.append(''); // default option + selectorLookup[dimTargetKey].forEach(function (option) { + $selector.append(createElem('option', {'value': option}, option)); + }); + $control + .append($tmp.clone().append($selector).html()) + ; + }); + $anchor + .append($tmp.clone().append($control).html()) + ; + _this.dimTargetKeys.forEach(function (dimTargetKey) { + var chartId = _this.chartAnchor.replaceAll('#') + '-' + dimTargetKey.replaceAll('.', '-'), + $elem = createElem('div', {id: chartId}); + $($elem) + .append(createElem('svg')) + ; + $anchor + .append($tmp.clone().append($elem).html()) + ; + }); + + if (callback) { + callback(); + } + }; + + /** + * major init + */ + this.init = function () { + _this.initSocket(function () { + _this.initHtml(function () { + _this.addCharts(function () { + setTimeout(function () { + _this.intervalHandle = setInterval(function () { + _this.refreshAll(); + }, _this.refreshFrequency); // refresh frequency + }, 2e3); // initial waiting time + }); + }) + }); + }; + + /** + * initialize socket + * @param [callback] + */ + this.initSocket = function (callback) { + if (_this.socket != null) { + _this.resetData(function () { + _this._data = []; + _this.chartData = []; +// _this.refreshAll(); + }); + _this.socket.close(); + } + _this.socket = new WebSocket(_this.socketConnection); + _this.socket.onopen = function () { + console.log("connected to " + _this.socketConnection); + _this.socket.send(JSON.stringify(_this.socketPacket)); + }; + _this.socket.onmessage = function (message) { + var jsonData; + if (message) { + if (message.data) { + jsonData = JSON.parse(message.data); + if (jsonData && jsonData.data) { + _this.dataHandler(jsonData); + } + } + } + }; + _this.socket.onclose = function () { + console.log("closed"); + }; + _this.socket.onerror = function (error) { + console.log("error", error); + }; + if (callback) { + callback(); + } + }; + + /** + * init listeners + * @param [callback] + */ + this.initListners = function (callback) { + //TODO + }; + + /** + * set eventType + * @param value + */ + this.setAggType = function (value) { + _this.aggType = value; + console.log('aggType is set to ' + _this.aggType); + }; + + /** + * set chart html anchor + * @param chartAnchor + */ + this.setChartAnchor = function (chartAnchor) { + if (!chartAnchor.match('^#.*')) { + chartAnchor = '#' + chartAnchor; + } + _this.chartAnchor = chartAnchor; + console.log('chartAnchor is set to ' + _this.chartAnchor); + }; + + /** + * set timePeriod + * @param timePeriod + * @param [callback] + */ + this.setTimePeriod = function (timePeriod, callback) { + _this.timer.timePeriod = timePeriod; + _this._updateSocketPacket(); + console.log('timePeriod is set to ' + _this.timer.timePeriod); + if (callback) { + callback(); + } + }; + + this._updateSocketPacket = function () { + _this.socketPacket = { + name: _this.aggType, + start: new Date(new Date().getTime() - _this.timer.timePeriod) + }; + }; + + /** + * set socketConnection + * @param value + */ + this.setSocketConnection = function (value) { + _this.socketConnection = value; + }; + + /** + * set socketPacket + * @param value + */ + this.setSocketPacket = function (value) { + _this.socketPacket = value; + }; + /** + * set timePeriod + * @param value + */ + this.setsampleDuration = function (value) { + _this.timer.timePeriod = value; + }; + + /** + * pause refresh data + */ + this.pause = function () { + clearInterval(_this.intervalHandle); + }; + + /** + * resume + */ + this.resume = function () { + setTimeout(function () { + _this.intervalHandle = setInterval(function () { + _this.refreshAll(); + }, _this.refreshFrequency); + }, 1e3); + }; + + /** + * test + */ + this.test = function () { + _this.setAggType('type1'); + _this.setChartAnchor('stack-charts'); + _this.init(); + }; + + // variables + var _this = this; + this.aggType = 'agg1'; + this.chartAnchor = ''; + this.chartDataList = {}; + this._data = []; // temporary data dictionary for aggregation + this.timer = { + timePeriod: 60 * 60 * 1e3, // 60 minutes + now: null, + nowReal: null, + min: null, + max: null + }; + this.resolutionLookup = { + '1m': 60 * 1e3, + '5m': 5 * 60 * 1e3, + '1h': 60 * 60 * 1e3 + }; + this.resolutionName = '1m'; + this.resolution = this.resolutionLookup[this.resolutionName]; + this.refreshFrequency = 5 * 1e3; // refresh every 5 seconds, will be deprecated + this.dimTargetKeys = [ + 'data.v1', + 'data.v2', + 'data.v3' + ]; + this.charts = {}; + this.socket = null; + this.socketConnection = 'ws://localhost:1081/1.0/aggregation/get'; + this.socketPacket = { + name: _this.aggType + '_' + _this.resolutionName, + start: new Date(new Date().getTime() - _this.timer.timePeriod) + }; + this.intervalHandle = null; +} + +function testRealTime() { + rte = new RealTimeEvents(); + rte.test(); + + rta = new RealTimeAggregations(); + rta.test(); +} \ No newline at end of file diff --git a/public/js/util.js b/public/js/util.js new file mode 100644 index 0000000..8077140 --- /dev/null +++ b/public/js/util.js @@ -0,0 +1,336 @@ +/** + * get a column of multi-dimensional array or DataFrame-like array + * @param index + * @param deep + * @returns {Array} + */ +Array.prototype.getColumn = function (index, deep) { + var ret = [], + i; + if (typeof deep === 'undefined') { + deep = 0; + } + if (this[0] instanceof Object) { + this.forEach(function (d) { + if (deep) { + ret.push(getDeepValue(d, index)); + } else { + ret.push(d[index]); + } + }); + } else { + for (i = 0; i < this.getColumnLength(); i++) { + ret.push(this[i][index]); + } + } + return ret; +}; +/** + * index by key + * key elements MUST be unique + * @param key + * @returns {{}} + */ +Array.prototype.indexBy = function (key) { + var ret = {}; + this.forEach(function (d) { + ret[d[key]] = d; + }); + return ret; +}; +/** + * index by key + * similar to indexBy but key elements can be non-unique and elements of return object are array + * @param key + * @return {*} + */ +Array.prototype.indexMultiBy = function (key) { + var ret = {}; + this.forEach(function (d) { + if (ret[d[key]] == null) { + ret[d[key]] = []; + } + ret[d[key]].push(d); + }); + return ret; +}; +/** + * index by key + * key elements MUST be unique + * @param key + * @returns {{}} + */ +Array.prototype.indexBy = function (key) { + var ret = {}; + this.forEach(function (d) { + ret[d[key]] = d; + }); + return ret; +}; +/** + * sortBy + * only applies to arrays like [{a:1,b:2},...] + * may conflict with cubism + * @param key + * @param direction + */ +Array.prototype.sortBy = function (key, direction) { + if (direction == null) { + direction = 'desc'; + } + switch (("" + direction).toLowerCase()) { + case 'desc': + direction = 1; + break; + case 'asc': + direction = -1; + break; + default : + } + var lookup = this.indexBy(key), + ret = [], + indexes = this.getColumn(key)._sort(direction), + i; + for (i = 0; i < indexes.length; i++) { + ret.push(lookup[indexes[i]]); + } + return ret; +}; +/** + * select by indexes + * use indexMultiBy and _.flatten + * only applies to arrays like [{a:1,b:2},...] + * may conflict with cubism + * @param key + * @param [indexes] + */ +Array.prototype.ix = function (key, indexes) { + if (indexes == null) { + return this.getColumn(key); + } + var lookup = this.indexMultiBy(key), + i, + ret = []; + if (!(indexes instanceof Array)) { + indexes = [indexes]; + } + for (i = 0; i < indexes.length; i++) { + if (lookup[indexes[i]] != null) { + ret.push(lookup[indexes[i]]); + } + } + ret = _.flatten(ret); + return ret; +}; +/** + * select columns + * @param keys + */ +Array.prototype.icol = function (keys) { + if (keys == null) { + throw Error('keys should not be null'); + } + if (!(keys instanceof Array)) { + keys = [keys]; + } + var row, + ret = []; + this.forEach(function (d) { + row = {}; + keys.forEach(function (key) { + row[key] = d[key]; + }); + ret.push(row); + }); + return ret; +}; +/** + * sort a simple array + * 1 ascending + * -1 descending + * @param direction + * @returns {Array} + * @private + */ +Array.prototype._sort = function (direction) { + return this.sort(function (a, b) { + switch (direction) { + case 1: + return (a > b) ? 1 : -1; + case 2: + return (a < b) ? 1 : -1; + default : + return 1 + } + }); +}; +/** + * return a unique array + * @returns {Array} + */ +Array.prototype.unique = function () { + var i, + ret = [], + tmp = {}; + for (i = 0; i < this.length; i++) { + tmp[this[i]] = 1; + } + for (i in tmp) { + if (tmp.hasOwnProperty(i)) { + ret.push(i); + } + } + return ret; +}; +/** + * max + * @returns {*} + */ +Array.prototype.max = function () { + var ret; + for (var i = 0; i < this.length; i++) { + if (typeof ret === "undefined") { + ret = this[i]; + } + if (ret < this[i]) { + ret = this[i]; + } + } + return ret; +}; +/** + * min + * @returns {*} + */ +Array.prototype.min = function () { + var ret; + for (var i = 0; i < this.length; i++) { + if (typeof ret === "undefined") { + ret = this[i]; + } + if (ret > this[i]) { + ret = this[i]; + } + } + return ret; +}; +/** + * unflatten + * @param data + * @returns {*} + */ +JSON.unflatten = function (data) { + "use strict"; + if (Object(data) !== data || Array.isArray(data)) + return data; + var regex = /\.?([^.\[\]]+)|\[(\d+)\]/g, + resultholder = {}; + for (var p in data) { + var cur = resultholder, + prop = "", + m; + while (m = regex.exec(p)) { + cur = cur[prop] || (cur[prop] = (m[2] ? [] : {})); + prop = m[2] || m[1]; + } + cur[prop] = data[p]; + } + return resultholder[""] || resultholder; +}; +/** + * + * @param data + * @returns {{}} + */ +JSON.flatten = function (data) { + var result = {}; + + function recurse(cur, prop) { + if (Object(cur) !== cur) { + result[prop] = cur; + } else if (Array.isArray(cur)) { + for (var i = 0, l = cur.length; i < l; i++) + recurse(cur[i], prop + "[" + i + "]"); + if (l == 0) + result[prop] = []; + } else { + var isEmpty = true; + for (var p in cur) { + isEmpty = false; + recurse(cur[p], prop ? prop + "." + p : p); + } + if (isEmpty && prop) + result[prop] = {}; + } + } + + recurse(data, ""); + return result; +}; + +/** + * flatten a dataframe-like array + * require JSON.flatten + * @returns {Array} + */ +Array.prototype.flatten = function () { + var ret = []; + this.forEach(function (d) { + ret.push(JSON.flatten(d)); + }); + return ret; +}; +/** + * return size of an object + * @param obj + * @returns {number} + */ +function size(obj) { + var n = 0, + i; + for (i in obj) { + if (obj.hasOwnProperty(i)) { + n++; + } + } + return n; +} + +/** + * create element + * @param type + * @param [attrs] + * @param [text] + * @returns {HTMLElement} + */ +function createElem(type, attrs, text) { + var elem = document.createElement(type), + attr; + + if (typeof attrs !== "undefined" && attrs instanceof Object) { + for (attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + elem[attr] = attrs[attr]; + } + } + } + + if (typeof text !== 'undefined') { + elem.innerText = text; + } + + return elem; +} + +/** + * replace all elements in a string + * @param oldVal + * @param [newVal] + * @returns {string} + */ +String.prototype.replaceAll = function (oldVal, newVal) { + if (newVal == null) { + newVal = ''; + } + return this.split(oldVal).join(newVal); +}; \ No newline at end of file diff --git a/public/views/404.html b/public/views/404.html new file mode 100644 index 0000000..2ade2e6 --- /dev/null +++ b/public/views/404.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/views/404.jade b/public/views/404.jade new file mode 100644 index 0000000..733ccda --- /dev/null +++ b/public/views/404.jade @@ -0,0 +1,3 @@ +extends layout +block append content + h1 404 Not Found diff --git a/public/views/includes/content.html b/public/views/includes/content.html new file mode 100644 index 0000000..822a6b2 --- /dev/null +++ b/public/views/includes/content.html @@ -0,0 +1 @@ +
      \ No newline at end of file diff --git a/public/views/includes/content.jade b/public/views/includes/content.jade new file mode 100644 index 0000000..30d8ff0 --- /dev/null +++ b/public/views/includes/content.jade @@ -0,0 +1,2 @@ +div(class='col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main') + block content \ No newline at end of file diff --git a/public/views/includes/footer.jade b/public/views/includes/footer.jade new file mode 100644 index 0000000..aa23783 --- /dev/null +++ b/public/views/includes/footer.jade @@ -0,0 +1,11 @@ +//script(src='/lib/requirejs/require.js') +script(src='/lib/jquery/dist/jquery.min.js') +script(src='/lib/bootstrap/dist/js/bootstrap.min.js') +//script(src='/lib/socket.io/lib/socket.io.js') +script(src='/lib/d3/d3.js') +script(src='/lib/crossfilter/crossfilter.min.js') +script(src='/lib/dcjs/dc.min.js') +script(src='/lib/nvd3/nv.d3.min.js') +script(src='/js/util.js') +script(src='/js/analytics.js') +block footer-extra \ No newline at end of file diff --git a/public/views/includes/head.jade b/public/views/includes/head.jade new file mode 100644 index 0000000..4e25f4e --- /dev/null +++ b/public/views/includes/head.jade @@ -0,0 +1,8 @@ +head + title= title + link(rel="stylesheet", href='/lib/bootstrap/dist/css/bootstrap.min.css') + link(rel="stylesheet", href='/lib/bootstrap/dist/css/bootstrap-theme.min.css') + link(rel="stylesheet", href='/lib/nvd3/nv.d3.css') + //link(rel="stylesheet", href='/css/dc.css') + link(rel="stylesheet", href='/css/dashboard.css') + block head-extra \ No newline at end of file diff --git a/public/views/includes/navbar-side.jade b/public/views/includes/navbar-side.jade new file mode 100644 index 0000000..bc9ab26 --- /dev/null +++ b/public/views/includes/navbar-side.jade @@ -0,0 +1,12 @@ +// left side bar +div(id='navbar-side', class='col-sm-3 col-md-2 sidebar') + ul(class='nav nav-sidebar') + li: a(href='/overview') Overview + li: a(href='/test') Test + li: a(href='/report') Reports + li: a(href='/real-time') Real-time + li: a(href='/usage') Usage + li: a(data-toggle='dropdown', href='#') Other + ul(class='dropdown-menu', role='menu') + li: a(href='#') Account + li: a(href='#') Help diff --git a/public/views/includes/navbar-top.jade b/public/views/includes/navbar-top.jade new file mode 100644 index 0000000..89a96d0 --- /dev/null +++ b/public/views/includes/navbar-top.jade @@ -0,0 +1,16 @@ +// top nav bar +div(id='navbar-top', class='navbar navbar-inverse navbar-fixed-top', role='navigation') + div(class='container-fluid') + div(class='navbar-header') + button(type='button', class='navbar-toggle', data-toggle='collapse', data-target='.navbar-collapse') + span(class='sr-only') Toggle navigation + span(class='icon-bar') + a(class='navbar-brand', href='#') Dashboard + div(class='navbar-collapse') + ul(class='nav navbar-nav navbar-right') + li: a(href='#') Dashboard + li: a(href='#') Settings + li: a(href='#') Profile + li: a(href='#') Help + form(class='navbar-form navbar-right') + input(type='text', class='form-control', placeholder='Search...') \ No newline at end of file diff --git a/public/views/index.html b/public/views/index.html new file mode 100644 index 0000000..76209a1 --- /dev/null +++ b/public/views/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/views/index.jade b/public/views/index.jade new file mode 100644 index 0000000..ee0471f --- /dev/null +++ b/public/views/index.jade @@ -0,0 +1,5 @@ +extends layout + +block append content + // main container + div(class='col'): p welcome \ No newline at end of file diff --git a/public/views/layout.html b/public/views/layout.html new file mode 100644 index 0000000..c13accb --- /dev/null +++ b/public/views/layout.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/views/layout.jade b/public/views/layout.jade new file mode 100644 index 0000000..a0bc42f --- /dev/null +++ b/public/views/layout.jade @@ -0,0 +1,10 @@ +doctype html +html(lang="en", ng-app="app") + block vars + include includes/head + body + include includes/navbar-top + div(class='row') + include includes/navbar-side + include includes/content + include includes/footer \ No newline at end of file diff --git a/public/views/overview.html b/public/views/overview.html new file mode 100644 index 0000000..76209a1 --- /dev/null +++ b/public/views/overview.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/views/overview.jade b/public/views/overview.jade new file mode 100644 index 0000000..ee0471f --- /dev/null +++ b/public/views/overview.jade @@ -0,0 +1,5 @@ +extends layout + +block append content + // main container + div(class='col'): p welcome \ No newline at end of file diff --git a/public/views/real-time.html b/public/views/real-time.html new file mode 100644 index 0000000..095060c --- /dev/null +++ b/public/views/real-time.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/views/real-time.jade b/public/views/real-time.jade new file mode 100644 index 0000000..ef0df56 --- /dev/null +++ b/public/views/real-time.jade @@ -0,0 +1,10 @@ +extends layout + +block append content + div(id='line-chart', style='width: 750px;') + div(id='stack-charts', style='width: 750px;') +block append footer-extra + script(type="text/javascript"). + testRealTime() + //script(type="text/javascript"). + // quickDirtyAggregation() \ No newline at end of file diff --git a/public/views/report.html b/public/views/report.html new file mode 100644 index 0000000..6cd86e8 --- /dev/null +++ b/public/views/report.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/views/report.jade b/public/views/report.jade new file mode 100644 index 0000000..da69f26 --- /dev/null +++ b/public/views/report.jade @@ -0,0 +1,11 @@ +extends layout +block append content + div(id='filter', class='horizon', style='width: 750px') + div(id='chart', class='horizon', style='width: 750px') + div(id='dc-line-chart', class='horizon', style='width: 750px') + div(id='dc-range-chart', class='horizon', style='width: 750px') +block append head-extra + link(rel="stylesheet", href='/css/cubism.css') + +block append footer-extra + script(src='/js/analytics.js') diff --git a/public/views/test.html b/public/views/test.html new file mode 100644 index 0000000..acf0559 --- /dev/null +++ b/public/views/test.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/views/test.jade b/public/views/test.jade new file mode 100644 index 0000000..75bf7b4 --- /dev/null +++ b/public/views/test.jade @@ -0,0 +1,18 @@ +extends layout + +block append content + ul + li query + form(action='/api/mongo/query', method='post') + input(name='connectionName', value='localhost', placeholder='connection') + input(name='dbName', value='test', placeholder='database') + input(name='colName', placeholder='collection') + textarea(name='qry', rows='5', cols='50') + input(type='submit') + li aggregate + form(action='/api/mongo/aggregate', method='post') + input(name='connectionName', value='localhost', placeholder='connection') + input(name='dbName', value='test', placeholder='database') + input(name='colName', placeholder='collection') + textarea(name='aggPipe', rows='5', cols='50') + input(type='submit') \ No newline at end of file diff --git a/public/views/usage.html b/public/views/usage.html new file mode 100644 index 0000000..ec6ca66 --- /dev/null +++ b/public/views/usage.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/views/usage.jade b/public/views/usage.jade new file mode 100644 index 0000000..8cb74eb --- /dev/null +++ b/public/views/usage.jade @@ -0,0 +1,5 @@ +extends layout + +block append footer-extra + script(src='/js/usage.js') +block append content