From c22a5d30227127965dd01f1051b7504fa04a95e6 Mon Sep 17 00:00:00 2001 From: Mircea Danila Dumitrescu Date: Fri, 16 May 2014 11:22:48 +0100 Subject: [PATCH] Added yeqings project --- .bowerrc | 3 + .gitignore | 11 +- README.md | 4 + bin/viewServer.js | 43 ++ bower.json | 26 + examples/frontend-test/filters.js | 211 +++++++ examples/frontend-test/nvd3.js | 159 ++++++ package.json | 37 +- public/css/dashboard.css | 100 ++++ public/css/dc.css | 308 ++++++++++ public/favicon.ico | Bin 0 -> 14276 bytes public/js/analytics.js | 759 +++++++++++++++++++++++++ public/js/util.js | 336 +++++++++++ public/views/404.html | 1 + public/views/404.jade | 3 + public/views/includes/content.html | 1 + public/views/includes/content.jade | 2 + public/views/includes/footer.jade | 11 + public/views/includes/head.jade | 8 + public/views/includes/navbar-side.jade | 12 + public/views/includes/navbar-top.jade | 16 + public/views/index.html | 1 + public/views/index.jade | 5 + public/views/layout.html | 1 + public/views/layout.jade | 10 + public/views/overview.html | 1 + public/views/overview.jade | 5 + public/views/real-time.html | 1 + public/views/real-time.jade | 10 + public/views/report.html | 1 + public/views/report.jade | 11 + public/views/test.html | 1 + public/views/test.jade | 18 + public/views/usage.html | 1 + public/views/usage.jade | 5 + 35 files changed, 2103 insertions(+), 19 deletions(-) create mode 100644 .bowerrc create mode 100644 README.md create mode 100644 bin/viewServer.js create mode 100644 bower.json create mode 100644 examples/frontend-test/filters.js create mode 100644 examples/frontend-test/nvd3.js create mode 100644 public/css/dashboard.css create mode 100644 public/css/dc.css create mode 100644 public/favicon.ico create mode 100644 public/js/analytics.js create mode 100644 public/js/util.js create mode 100644 public/views/404.html create mode 100644 public/views/404.jade create mode 100644 public/views/includes/content.html create mode 100644 public/views/includes/content.jade create mode 100644 public/views/includes/footer.jade create mode 100644 public/views/includes/head.jade create mode 100644 public/views/includes/navbar-side.jade create mode 100644 public/views/includes/navbar-top.jade create mode 100644 public/views/index.html create mode 100644 public/views/index.jade create mode 100644 public/views/layout.html create mode 100644 public/views/layout.jade create mode 100644 public/views/overview.html create mode 100644 public/views/overview.jade create mode 100644 public/views/real-time.html create mode 100644 public/views/real-time.jade create mode 100644 public/views/report.html create mode 100644 public/views/report.jade create mode 100644 public/views/test.html create mode 100644 public/views/test.jade create mode 100644 public/views/usage.html create mode 100644 public/views/usage.jade 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 0000000000000000000000000000000000000000..d2f16b682cc80568e2a7627c74be66941d3eb971 GIT binary patch literal 14276 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 AM*si- literal 0 HcmV?d00001 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