├── .gitignore ├── lib └── cube │ ├── client │ ├── end.js │ ├── semicolon.js │ ├── start.js │ ├── cube.js │ ├── palette.css │ ├── board.css │ ├── visualizer.html │ ├── squares.js │ ├── header.js │ ├── body.css │ ├── palette.js │ ├── piece-text.js │ ├── piece.css │ ├── piece-sum.js │ ├── board.js │ ├── piece-area.js │ └── piece.js │ ├── server │ ├── evaluator.js │ ├── types.js │ ├── collector.js │ ├── reduces.js │ ├── emitter.js │ ├── tiers.js │ ├── collectd.js │ ├── endpoint.js │ ├── server.js │ ├── event.js │ ├── event-expression.peg │ ├── visualizer.js │ ├── metric-expression.peg │ └── metric.js │ └── index.js ├── .npmignore ├── examples └── emitter │ ├── dji │ ├── dji-config.js │ └── dji.js │ └── random │ ├── random-config.js │ └── random.js ├── bin ├── collector-config.js ├── evaluator-config.js ├── collector.js └── evaluator.js ├── schema ├── schema-drop.js └── schema-create.js ├── Makefile ├── LICENSE ├── package.json ├── README.md └── test ├── types-test.js ├── collector-test.js ├── test.js ├── endpoint-test.js ├── reduces-test.js ├── metric-test.js ├── event-expression-test.js ├── metric-expression-test.js └── tiers-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /lib/cube/client/end.js: -------------------------------------------------------------------------------- 1 | })(); 2 | -------------------------------------------------------------------------------- /lib/cube/client/semicolon.js: -------------------------------------------------------------------------------- 1 | ; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | -------------------------------------------------------------------------------- /lib/cube/client/start.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | -------------------------------------------------------------------------------- /lib/cube/client/cube.js: -------------------------------------------------------------------------------- 1 | cube = {version: "0.0.1"}; 2 | 3 | var cube_time = d3.time.format.iso; 4 | -------------------------------------------------------------------------------- /examples/emitter/dji/dji-config.js: -------------------------------------------------------------------------------- 1 | // Default configuration for development. 2 | module.exports = { 3 | "http-host": "127.0.0.1", 4 | "http-port": 1080 5 | }; 6 | -------------------------------------------------------------------------------- /examples/emitter/random/random-config.js: -------------------------------------------------------------------------------- 1 | // Default configuration for development. 2 | module.exports = { 3 | "http-host": "127.0.0.1", 4 | "http-port": 1080 5 | }; 6 | -------------------------------------------------------------------------------- /lib/cube/client/palette.css: -------------------------------------------------------------------------------- 1 | .palette rect { 2 | fill: #eee; 3 | stroke: #999; 4 | } 5 | 6 | .palette rect:hover { 7 | fill: #ddd; 8 | } 9 | 10 | .palette text { 11 | pointer-events: none; 12 | } 13 | -------------------------------------------------------------------------------- /lib/cube/client/board.css: -------------------------------------------------------------------------------- 1 | .squares rect { 2 | fill: none; 3 | stroke: #ddd; 4 | } 5 | 6 | .squares rect.black { 7 | fill: #fafafa; 8 | } 9 | 10 | .squares rect.shadow { 11 | fill: #e7e7e7; 12 | } 13 | -------------------------------------------------------------------------------- /bin/collector-config.js: -------------------------------------------------------------------------------- 1 | // Default configuration for development. 2 | module.exports = { 3 | "mongo-host": "127.0.0.1", 4 | "mongo-port": 27017, 5 | "mongo-database": "cube_development", 6 | "http-port": 1080 7 | }; 8 | -------------------------------------------------------------------------------- /bin/evaluator-config.js: -------------------------------------------------------------------------------- 1 | // Default configuration for development. 2 | module.exports = { 3 | "mongo-host": "127.0.0.1", 4 | "mongo-port": 27017, 5 | "mongo-database": "cube_development", 6 | "http-port": 1081 7 | }; 8 | -------------------------------------------------------------------------------- /schema/schema-drop.js: -------------------------------------------------------------------------------- 1 | db.boards.drop(); 2 | 3 | ["random", "stock", "collectd_df", "collectd_load", "collectd_interface", "collectd_memory"].forEach(function(type) { 4 | db[type + "_events"].drop(); 5 | db[type + "_metrics"].drop(); 6 | }); 7 | -------------------------------------------------------------------------------- /bin/collector.js: -------------------------------------------------------------------------------- 1 | var options = require("./collector-config"), 2 | cube = require("../"), 3 | server = cube.server(options); 4 | 5 | server.register = function(db, endpoints) { 6 | cube.collector.register(db, endpoints); 7 | }; 8 | 9 | server.start(); 10 | -------------------------------------------------------------------------------- /lib/cube/server/evaluator.js: -------------------------------------------------------------------------------- 1 | var endpoint = require("./endpoint"); 2 | 3 | exports.register = function(db, endpoints) { 4 | endpoints.ws.push( 5 | endpoint.exact("/1.0/event/get", require("./event").getter(db)), 6 | endpoint.exact("/1.0/metric/get", require("./metric").getter(db)) 7 | ); 8 | }; 9 | -------------------------------------------------------------------------------- /bin/evaluator.js: -------------------------------------------------------------------------------- 1 | var options = require("./evaluator-config"), 2 | cube = require("../"), 3 | server = cube.server(options); 4 | 5 | server.register = function(db, endpoints) { 6 | cube.evaluator.register(db, endpoints); 7 | cube.visualizer.register(db, endpoints); 8 | }; 9 | 10 | server.start(); 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | JS_TESTER = ./node_modules/vows/bin/vows 2 | PEG_COMPILER = ./node_modules/pegjs/bin/pegjs 3 | 4 | .PHONY: test 5 | 6 | %.js: %.peg Makefile 7 | $(PEG_COMPILER) < $< > $@ 8 | 9 | all: \ 10 | lib/cube/server/event-expression.js \ 11 | lib/cube/server/metric-expression.js 12 | 13 | test: all 14 | @$(JS_TESTER) 15 | -------------------------------------------------------------------------------- /lib/cube/index.js: -------------------------------------------------------------------------------- 1 | exports.version = "0.0.9"; 2 | exports.emitter = require("./server/emitter"); 3 | exports.server = require("./server/server"); 4 | exports.collector = require("./server/collector"); 5 | exports.evaluator = require("./server/evaluator"); 6 | exports.visualizer = require("./server/visualizer"); 7 | exports.endpoint = require("./server/endpoint"); 8 | -------------------------------------------------------------------------------- /schema/schema-create.js: -------------------------------------------------------------------------------- 1 | db.createCollection("boards"); 2 | 3 | ["random", "stock", "collectd_df", "collectd_load", "collectd_interface", "collectd_memory"].forEach(function(type) { 4 | var event = type + "_events", metric = type + "_metrics"; 5 | db.createCollection(event); 6 | db[event].ensureIndex({t: 1}); 7 | db.createCollection(metric, {capped: true, size: 1e6, autoIndexId: false}); 8 | db[metric].ensureIndex({e: 1, l: 1, t: 1, g: 1}, {unique: true}); 9 | db[metric].ensureIndex({i: 1, e: 1, l: 1, t: 1}); 10 | db[metric].ensureIndex({i: 1, l: 1, t: 1}); 11 | }); 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011 Square, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cube", 3 | "version": "0.0.14", 4 | "description": "A system for time series visualization using MongoDB, Node and D3.", 5 | "keywords": ["time series", "visualization"], 6 | "homepage": "http://square.github.com/cube/", 7 | "author": {"name": "Mike Bostock", "url": "http://bost.ocks.org/mike"}, 8 | "repository": {"type": "git", "url": "http://github.com/square/cube.git"}, 9 | "main": "./lib/cube", 10 | "dependencies": { 11 | "d3": "2.6.0", 12 | "mongodb": "0.9.7-1.3", 13 | "pegjs": "0.6.2", 14 | "vows": "0.5.11", 15 | "websocket": "1.0.2", 16 | "websocket-server": "1.4.04" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/cube/server/types.js: -------------------------------------------------------------------------------- 1 | // Much like db.collection, but caches the result for both events and metrics. 2 | // Also, this is synchronous, since we are opening a collection unsafely. 3 | module.exports = function(db) { 4 | var collections = {}; 5 | return function(type) { 6 | var collection = collections[type]; 7 | if (!collection) { 8 | collection = collections[type] = {}; 9 | db.collection(type + "_events", function(error, events) { 10 | collection.events = events; 11 | }); 12 | db.collection(type + "_metrics", function(error, metrics) { 13 | collection.metrics = metrics; 14 | }); 15 | } 16 | return collection; 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /lib/cube/client/visualizer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cube 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/emitter/dji/dji.js: -------------------------------------------------------------------------------- 1 | var util = require("util"), 2 | emitter = require("../../../lib/cube/server/emitter"), 3 | options = require("./dji-config"); 4 | 5 | // Connect to websocket. 6 | util.log("starting websocket client"); 7 | var client = emitter().open(options["http-host"], options["http-port"]); 8 | 9 | // Emit stock data. 10 | readline(function(line, i) { 11 | if (i) { 12 | var fields = line.split(","); 13 | client.send({ 14 | type: "stock", 15 | time: new Date(fields[0]), 16 | data: { 17 | open: +fields[1], 18 | high: +fields[2], 19 | low: +fields[3], 20 | close: +fields[4], 21 | volume: +fields[5] 22 | } 23 | }); 24 | } 25 | }); 26 | 27 | function readline(callback) { 28 | var stdin = process.openStdin(), line = "", i = -1; 29 | stdin.setEncoding("utf8"); 30 | stdin.on("data", function(string) { 31 | var lines = string.split("\n"); 32 | lines[0] = line + lines[0]; 33 | line = lines.pop(); 34 | lines.forEach(function(line) { callback(line, ++i); }); 35 | }); 36 | stdin.on("end", function() { 37 | util.log("stopping websocket client"); 38 | client.close(); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cube 2 | __ 3 | /\ \ 4 | ___ __ __\ \ \____ ____ 5 | / ___\/\ \/\ \\ \ __ \ / __ \ 6 | /\ \__/\ \ \_\ \\ \ \_\ \/\ __/ 7 | \ \____\\ \____/ \ \____/\ \____\ 8 | \/____/ \/___/ \/___/ \/____/ 9 | 10 | See for an introduction. 11 | See for documentation. 12 | 13 | ## Thank You 14 | 15 | Cube is built with the following open-source systems and libraries: 16 | 17 | * [D3.js](http://mbostock.github.com/d3/) 18 | * [MongoDB](http://www.mongodb.org/) 19 | * [node-mongodb-native](/christkv/node-mongodb-native) 20 | * [Node.js](http://nodejs.org/) 21 | * [PEG.js](http://pegjs.majda.cz/) 22 | * [Vows](http://vowsjs.org/) 23 | * [websocket](/Worlize/WebSocket-Node) 24 | * [websocket-server](/miksago/node-websocket-server) 25 | 26 | ## Contributing 27 | 28 | We'd love for you to participate in the development of Cube. Before we can accept your pull request, please sign our [Individual Contributor License Agreement](https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1). It's a short form that covers our bases and makes sure you're eligible to contribute. Thank you! 29 | -------------------------------------------------------------------------------- /lib/cube/server/collector.js: -------------------------------------------------------------------------------- 1 | var endpoint = require("./endpoint"), 2 | util = require("util"); 3 | 4 | exports.register = function(db, endpoints) { 5 | var putter = require("./event").putter(db); 6 | endpoints.ws.push( 7 | endpoint.exact("/1.0/event/put", putter) 8 | ); 9 | endpoints.http.push( 10 | endpoint.exact("POST", "/1.0/event/put", post(putter)), 11 | endpoint.exact("POST", "/collectd", require("./collectd").putter(putter)) 12 | ); 13 | }; 14 | 15 | function post(putter) { 16 | return function(request, response) { 17 | var content = ""; 18 | request.on("data", function(chunk) { 19 | content += chunk; 20 | }); 21 | request.on("end", function() { 22 | try { 23 | JSON.parse(content).forEach(putter); 24 | } catch (e) { 25 | util.log(e); 26 | response.writeHead(400, { 27 | "Content-Type": "application/json", 28 | "Access-Control-Allow-Origin": "*" 29 | }); 30 | return response.end("{\"status\":400}"); 31 | } 32 | response.writeHead(200, { 33 | "Content-Type": "application/json", 34 | "Access-Control-Allow-Origin": "*" 35 | }); 36 | response.end("{\"status\":200}"); 37 | }); 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /test/types-test.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | assert = require("assert"), 3 | test = require("./test"), 4 | types = require("../lib/cube/server/types"), 5 | mongodb = require("mongodb"); 6 | 7 | var suite = vows.describe("types"); 8 | 9 | suite.addBatch(test.batch({ 10 | topic: function(test) { 11 | return types(test.db); 12 | }, 13 | 14 | "types": { 15 | "returns collection cache for a given database": function(types) { 16 | assert.equal(typeof types, "function"); 17 | }, 18 | "each typed collection has events and metrics": function(types) { 19 | var collection = types("random"), 20 | keys = []; 21 | for (var key in collection) { 22 | keys.push(key); 23 | } 24 | keys.sort(); 25 | assert.deepEqual(keys, ["events", "metrics"]); 26 | assert.isTrue(collection.events instanceof mongodb.Collection); 27 | assert.isTrue(collection.metrics instanceof mongodb.Collection); 28 | assert.equal(collection.events.collectionName, "random_events"); 29 | assert.equal(collection.metrics.collectionName, "random_metrics"); 30 | }, 31 | "memoizes cached collections": function(types) { 32 | assert.strictEqual(types("random"), types("random")); 33 | } 34 | } 35 | })); 36 | 37 | suite.export(module); 38 | -------------------------------------------------------------------------------- /lib/cube/client/squares.js: -------------------------------------------------------------------------------- 1 | cube.squares = function(board) { 2 | var squares = {}; 3 | 4 | var g = document.createElementNS(d3.ns.prefix.svg, "g"); 5 | 6 | d3.select(g) 7 | .attr("class", "squares"); 8 | 9 | board 10 | .on("size", resize) 11 | .on("squareSize", resize) 12 | .on("squareRadius", resize); 13 | 14 | function resize() { 15 | var boardSize = board.size(), 16 | squareSize = board.squareSize(), 17 | squareRadius = board.squareRadius(); 18 | 19 | var square = d3.select(g).selectAll(".square") 20 | .data(d3.range(boardSize[0] * boardSize[1]) 21 | .map(function(d) {return { x: d % boardSize[0], y: d / boardSize[0] | 0}; })); 22 | 23 | square.enter().append("svg:rect") 24 | .attr("class", "square"); 25 | 26 | square 27 | .attr("rx", squareRadius) 28 | .attr("ry", squareRadius) 29 | .attr("class", function(d, i) { return (i - d.y & 1 ? "black" : "white") + " square"; }) 30 | .attr("x", function(d) { return d.x * squareSize; }) 31 | .attr("y", function(d) { return d.y * squareSize; }) 32 | .attr("width", squareSize) 33 | .attr("height", squareSize); 34 | 35 | square.exit().remove(); 36 | } 37 | 38 | squares.node = function() { 39 | return g; 40 | }; 41 | 42 | resize(); 43 | 44 | return squares; 45 | }; 46 | -------------------------------------------------------------------------------- /examples/emitter/random/random.js: -------------------------------------------------------------------------------- 1 | var util = require("util"), 2 | cube = require("../../../"), 3 | options = require("./random-config"), 4 | count = 0, 5 | batch = 10, 6 | hour = 60 * 60 * 1000, 7 | start = Date.now(), 8 | offset = -Math.abs(random()) * 24 * hour; 9 | 10 | // Connect to websocket. 11 | util.log("starting websocket client"); 12 | var client = cube.emitter().open(options["http-host"], options["http-port"]); 13 | 14 | // Emit random values. 15 | var interval = setInterval(function() { 16 | for (var i = -1; ++i < batch;) { 17 | client.send({ 18 | type: "random", 19 | time: new Date(Date.now() + random() * 2 * hour + offset), 20 | data: {random: (offset & 1 ? 1 : -1) * Math.random()} 21 | }); 22 | count++; 23 | } 24 | var duration = Date.now() - start; 25 | console.log(count + " events in " + duration + " ms: " + Math.round(1000 * count / duration) + " sps"); 26 | }, 10); 27 | 28 | // Display stats on shutdown. 29 | process.on("SIGINT", function() { 30 | console.log("stopping websocket client"); 31 | client.close(); 32 | clearInterval(interval); 33 | }); 34 | 35 | // Sample from a normal distribution with mean 0, stddev 1. 36 | function random() { 37 | var x = 0, y = 0, r; 38 | do { 39 | x = Math.random() * 2 - 1; 40 | y = Math.random() * 2 - 1; 41 | r = x * x + y * y; 42 | } while (!r || r > 1); 43 | return x * Math.sqrt(-2 * Math.log(r) / r); 44 | } 45 | -------------------------------------------------------------------------------- /lib/cube/server/reduces.js: -------------------------------------------------------------------------------- 1 | var reduces = module.exports = { 2 | 3 | sum: function(values) { 4 | var i = -1, n = values.length, sum = 0; 5 | while (++i < n) sum += values[i]; 6 | return sum; 7 | }, 8 | 9 | min: function(values) { 10 | var i = -1, n = values.length, min = Infinity, value; 11 | while (++i < n) if ((value = values[i]) < min) min = value; 12 | return min; 13 | }, 14 | 15 | max: function(values) { 16 | var i = -1, n = values.length, max = -Infinity, value; 17 | while (++i < n) if ((value = values[i]) > max) max = value; 18 | return max; 19 | }, 20 | 21 | distinct: function(values) { 22 | var map = {}, count = 0, i = -1, n = values.length, value; 23 | while (++i < n) if (!((value = values[i]) in map)) map[value] = ++count; 24 | return count; 25 | }, 26 | 27 | median: function(values) { 28 | return quantile(values.sort(ascending), .5); 29 | } 30 | 31 | }; 32 | 33 | // These metrics have well-defined values for the empty set. 34 | reduces.sum.empty = 0; 35 | reduces.distinct.empty = 0; 36 | 37 | // These metrics can be computed using pyramidal aggregation. 38 | reduces.sum.pyramidal = true; 39 | reduces.min.pyramidal = true; 40 | reduces.max.pyramidal = true; 41 | 42 | function ascending(a, b) { 43 | return a - b; 44 | } 45 | 46 | function quantile(values, q) { 47 | var i = 1 + q * (values.length - 1), 48 | j = ~~i, 49 | h = i - j, 50 | a = values[j - 1]; 51 | return h ? a + h * (values[j] - a) : a; 52 | } 53 | -------------------------------------------------------------------------------- /lib/cube/client/header.js: -------------------------------------------------------------------------------- 1 | cube.header = function(board) { 2 | var header = {}; 3 | 4 | var div = document.createElement("div"); 5 | 6 | var selection = d3.select(div) 7 | .attr("class", "header"); 8 | 9 | var left = selection.append("div") 10 | .attr("class", "left"); 11 | 12 | left.append("a") 13 | .attr("href", "/" + board.id) 14 | .append("button") 15 | .text("View"); 16 | 17 | left.append("a") 18 | .attr("href", "/" + board.id + "/edit") 19 | .append("button") 20 | .text("Edit"); 21 | 22 | var viewers = selection.append("div") 23 | .attr("class", "right"); 24 | 25 | board.on("view", function(e) { 26 | viewers.text(e.count > 1 ? e.count - 1 + " other" + (e.count > 2 ? "s" : "") + " viewing" : null); 27 | }); 28 | 29 | header.node = function() { 30 | return div; 31 | }; 32 | 33 | if (mode == "view") { 34 | var shown = false; 35 | 36 | d3.select(window) 37 | .on("mouseout", mouseout) 38 | .on("mousemove", mousemove); 39 | 40 | function show(show) { 41 | if (show != shown) { 42 | d3.select(div.parentNode).transition() 43 | .style("top", ((shown = show) ? 0 : -60) + "px"); 44 | } 45 | } 46 | 47 | function mouseout() { 48 | if (d3.event.relatedTarget == null) show(false); 49 | } 50 | 51 | function mousemove() { 52 | if (d3.event.pageY > 120) show(false); 53 | else if (d3.event.pageY < 60) show(true); 54 | } 55 | } 56 | 57 | return header; 58 | }; 59 | -------------------------------------------------------------------------------- /lib/cube/client/body.css: -------------------------------------------------------------------------------- 1 | body { 2 | overflow-x: hidden; 3 | overflow-y: scroll; 4 | margin-top: 50px; 5 | } 6 | 7 | body, button { 8 | font: 14px "Helvetica Neue"; 9 | } 10 | 11 | #header { 12 | display: block; 13 | position: fixed; 14 | top: 0; 15 | left: 0; 16 | width: 100%; 17 | height: 40px; 18 | background: #222; 19 | background: -webkit-gradient(linear,left top,left bottom,from(#333),to(#111)); 20 | color: #fff; 21 | -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.5); 22 | z-index: 1; 23 | } 24 | 25 | .view #header { 26 | top: -60px; 27 | } 28 | 29 | .header { 30 | height: 40px; 31 | line-height: 40px; 32 | width: 1281px; 33 | } 34 | 35 | .header, #board { 36 | display: block; 37 | margin: auto; 38 | position: relative; 39 | } 40 | 41 | #board { 42 | width: 1299px; 43 | } 44 | 45 | .left { 46 | float: left; 47 | } 48 | 49 | .right { 50 | float: right; 51 | } 52 | 53 | .right button { 54 | margin: 0 0 0 4px; 55 | } 56 | 57 | svg { 58 | display: block; 59 | } 60 | 61 | button { 62 | background: #333; 63 | background: -webkit-gradient(linear,left top,left bottom,from(#444),to(#222)); 64 | color: #fff; 65 | cursor: pointer; 66 | height: 30px; 67 | width: 113px; 68 | margin: 0 4px 0 0; 69 | padding: 0; 70 | border: solid 1px #000; 71 | border-radius: 4px; 72 | } 73 | 74 | .view { 75 | margin-top: -60px; 76 | overflow: hidden; 77 | } 78 | 79 | .view .squares { 80 | display: none; 81 | } 82 | 83 | @media screen and (min-width: 1920px) { 84 | .view { 85 | -webkit-transform: scale(1.4,1.4)translate(0px,110px); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/cube/server/emitter.js: -------------------------------------------------------------------------------- 1 | var util = require("util"), 2 | websocket = require("websocket"); 3 | 4 | module.exports = function() { 5 | var emitter = {}, 6 | queue = [], 7 | url, 8 | socket, 9 | timeout; 10 | 11 | function close() { 12 | if (socket) { 13 | util.log("closing socket"); 14 | socket.close(); 15 | socket = null; 16 | } 17 | } 18 | 19 | function closeWhenDone() { 20 | if (socket) { 21 | if (!socket.bytesWaitingToFlush) close(); 22 | else setTimeout(closeWhenDone, 1000); 23 | } 24 | } 25 | 26 | function open() { 27 | timeout = 0; 28 | close(); 29 | util.log("opening socket: " + url); 30 | var client = new websocket.client(); 31 | client.on("connect", function(connection) { socket = connection; flush(); }); 32 | client.on("connectFailed", reopen); 33 | client.connect(url); 34 | } 35 | 36 | function reopen() { 37 | if (!timeout) { 38 | util.log("reopening soon"); 39 | timeout = setTimeout(open, 1000); 40 | } 41 | } 42 | 43 | function flush() { 44 | var event; 45 | while (event = queue.pop()) { 46 | try { 47 | socket.sendUTF(JSON.stringify(event)); 48 | } catch (e) { 49 | util.log(e.stack); 50 | reopen(); 51 | return queue.push(event); 52 | } 53 | } 54 | } 55 | 56 | emitter.open = function(host, port) { 57 | url = "ws://" + host + ":" + port + "/1.0/event/put"; 58 | open(); 59 | return emitter; 60 | }; 61 | 62 | emitter.send = function(event) { 63 | queue.push(event); 64 | if (socket) flush(); 65 | return emitter; 66 | }; 67 | 68 | emitter.close = function() { 69 | closeWhenDone(); 70 | return emitter; 71 | }; 72 | 73 | return emitter; 74 | }; 75 | -------------------------------------------------------------------------------- /lib/cube/client/palette.js: -------------------------------------------------------------------------------- 1 | cube.palette = function(board) { 2 | var palette = {}; 3 | 4 | var g = document.createElementNS(d3.ns.prefix.svg, "g"); 5 | 6 | var type = d3.select(g) 7 | .attr("class", "palette") 8 | .selectAll(".piece-type") 9 | .data(d3.entries(cube.piece.type)) 10 | .enter().append("svg:g") 11 | .attr("class", "piece-type") 12 | .on("mousedown", mousedown); 13 | 14 | type.append("svg:rect"); 15 | 16 | type.append("svg:text") 17 | .attr("dy", ".35em") 18 | .attr("text-anchor", "middle") 19 | .text(function(d) { return d.key; }); 20 | 21 | board 22 | .on("squareSize", resize) 23 | .on("squareRadius", resize); 24 | 25 | function resize() { 26 | var size = board.squareSize(), 27 | radius = board.squareRadius(); 28 | 29 | type 30 | .attr("transform", function(d, i) { return "translate(" + (i * size + size / 2) + "," + (size / 2) + ")"; }) 31 | .select("rect") 32 | .attr("x", -size / 2) 33 | .attr("y", -size / 2) 34 | .attr("width", size) 35 | .attr("height", size) 36 | .attr("rx", radius) 37 | .attr("ry", radius); 38 | } 39 | 40 | function mousedown(d) { 41 | var piece = board.add(d.value), 42 | pieceSize = piece.size(), 43 | squareSize = board.squareSize(), 44 | mouse = d3.svg.mouse(g); 45 | 46 | piece.position([ 47 | mouse[0] / squareSize - pieceSize[0] / 2, 48 | mouse[1] / squareSize - pieceSize[1] / 2 - 1.5 49 | ]); 50 | 51 | // Simulate mousedown on the piece to start dragging. 52 | var div = d3.select(piece.node()); 53 | div.each(div.on("mousedown.piece")); 54 | } 55 | 56 | palette.node = function() { 57 | return g; 58 | }; 59 | 60 | resize(); 61 | 62 | return palette; 63 | }; 64 | -------------------------------------------------------------------------------- /lib/cube/client/piece-text.js: -------------------------------------------------------------------------------- 1 | cube.piece.type.text = function(board) { 2 | var timeout; 3 | 4 | var text = cube.piece(board) 5 | .on("size", resize) 6 | .on("serialize", serialize) 7 | .on("deserialize", deserialize); 8 | 9 | var div = d3.select(text.node()) 10 | .classed("text", true); 11 | 12 | if (mode == "edit") { 13 | div.append("h3") 14 | .attr("class", "title") 15 | .text("Text Label"); 16 | 17 | var content = div.append("textarea") 18 | .attr("class", "content") 19 | .attr("placeholder", "text content…") 20 | .on("keyup.text", textchange) 21 | .on("focus.text", text.focus) 22 | .on("blur.text", text.blur); 23 | } 24 | 25 | function resize() { 26 | var transition = text.transition(), 27 | innerSize = text.innerSize(); 28 | 29 | if (mode == "edit") { 30 | transition.select(".content") 31 | .style("width", innerSize[0] - 12 + "px") 32 | .style("height", innerSize[1] - 34 + "px"); 33 | } else { 34 | transition 35 | .style("font-size", innerSize[0] / 12 + "px") 36 | .style("margin-top", innerSize[1] / 2 + innerSize[0] / 5 - innerSize[0] / 12 + "px"); 37 | } 38 | } 39 | 40 | function textchange() { 41 | if (timeout) clearTimeout(timeout); 42 | timeout = setTimeout(text.edit, 750); 43 | } 44 | 45 | function serialize(json) { 46 | json.type = "text"; 47 | json.content = content.property("value"); 48 | } 49 | 50 | function deserialize(json) { 51 | if (mode == "edit") { 52 | content.property("value", json.content); 53 | } else { 54 | div.text(json.content); 55 | } 56 | } 57 | 58 | text.copy = function() { 59 | return board.add(cube.piece.type.text); 60 | }; 61 | 62 | resize(); 63 | 64 | return text; 65 | }; 66 | -------------------------------------------------------------------------------- /lib/cube/server/tiers.js: -------------------------------------------------------------------------------- 1 | var tiers = module.exports = {}; 2 | 3 | var second = 1000, 4 | second20 = 20 * second, 5 | minute = 60 * second, 6 | minute5 = 5 * minute, 7 | hour = 60 * minute, 8 | day = 24 * hour, 9 | week = 7 * day, 10 | month = 30 * day, 11 | year = 365 * day; 12 | 13 | tiers[second20] = { 14 | key: second20, 15 | floor: function(d) { return new Date(Math.floor(d / second20) * second20); }, 16 | ceil: tier_ceil, 17 | step: function(d) { return new Date(+d + second20); } 18 | }; 19 | 20 | tiers[minute5] = { 21 | key: minute5, 22 | floor: function(d) { return new Date(Math.floor(d / minute5) * minute5); }, 23 | ceil: tier_ceil, 24 | step: function(d) { return new Date(+d + minute5); }, 25 | next: tiers[second20], 26 | size: function() { return 15; } 27 | }; 28 | 29 | tiers[hour] = { 30 | key: hour, 31 | floor: function(d) { return new Date(Math.floor(d / hour) * hour); }, 32 | ceil: tier_ceil, 33 | step: function(d) { return new Date(+d + hour); }, 34 | next: tiers[minute5], 35 | size: function() { return 12; } 36 | }; 37 | 38 | tiers[day] = { 39 | key: day, 40 | floor: function(d) { return new Date(Math.floor(d / day) * day); }, 41 | ceil: tier_ceil, 42 | step: function(d) { return new Date(+d + day); }, 43 | next: tiers[hour], 44 | size: function() { return 24; } 45 | }; 46 | 47 | tiers[week] = { 48 | key: week, 49 | floor: function(d) { (d = new Date(Math.floor(d / day) * day)).setUTCDate(d.getUTCDate() - d.getUTCDay()); return d; }, 50 | ceil: tier_ceil, 51 | step: function(d) { return new Date(+d + week); }, 52 | next: tiers[day], 53 | size: function() { return 7; } 54 | }; 55 | 56 | tiers[month] = { 57 | key: month, 58 | floor: function(d) { return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1)); }, 59 | ceil: tier_ceil, 60 | step: function(d) { (d = new Date(d)).setUTCMonth(d.getUTCMonth() + 1); return d; }, 61 | next: tiers[day], 62 | size: function(d) { return 32 - new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 32)).getUTCDate(); } 63 | }; 64 | 65 | function tier_ceil(date) { 66 | return this.step(this.floor(new Date(date - 1))); 67 | } 68 | -------------------------------------------------------------------------------- /test/collector-test.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | assert = require("assert"), 3 | cube = require("../"), 4 | test = require("./test"); 5 | 6 | var suite = vows.describe("collector"); 7 | 8 | var port = ++test.port, server = cube.server({ 9 | "mongo-host": "localhost", 10 | "mongo-port": 27017, 11 | "mongo-database": "cube_test", 12 | "http-port": port 13 | }); 14 | 15 | var obj = { type: "test" 16 | , time: (new Date).toISOString() 17 | , data: { foo: "bar" } 18 | }; 19 | var arr = [obj]; 20 | var num = 42; 21 | 22 | server.register = cube.collector.register; 23 | 24 | server.start(); 25 | 26 | suite.addBatch(test.batch({ 27 | "POST /event/put with invalid JSON": { 28 | topic: test.request({method: "POST", port: port, path: "/1.0/event/put"}, "This ain't JSON.\n"), 29 | "responds with status 400": function(response) { 30 | assert.equal(response.statusCode, 400); 31 | assert.deepEqual(JSON.parse(response.body), {status: 400}); 32 | } 33 | } 34 | })); 35 | 36 | suite.addBatch(test.batch({ 37 | "POST /event/put with a JSON object": { 38 | topic: test.request({method: "POST", port: port, path: "/1.0/event/put"}, JSON.stringify(obj)), 39 | "responds with status 400": function(response) { 40 | assert.equal(response.statusCode, 400); 41 | assert.deepEqual(JSON.parse(response.body), {status: 400}); 42 | } 43 | } 44 | })); 45 | 46 | suite.addBatch(test.batch({ 47 | "POST /event/put with a JSON array": { 48 | topic: test.request({method: "POST", port: port, path: "/1.0/event/put"}, JSON.stringify(arr)), 49 | "responds with status 200": function(response) { 50 | assert.equal(response.statusCode, 200); 51 | assert.deepEqual(JSON.parse(response.body), {status: 200}); 52 | } 53 | } 54 | })); 55 | 56 | suite.addBatch(test.batch({ 57 | "POST /event/put with a JSON number": { 58 | topic: test.request({method: "POST", port: port, path: "/1.0/event/put"}, JSON.stringify(num)), 59 | "responds with status 400": function(response) { 60 | assert.equal(response.statusCode, 400); 61 | assert.deepEqual(JSON.parse(response.body), {status: 400}); 62 | } 63 | } 64 | })); 65 | 66 | suite.export(module); 67 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var mongodb = require("mongodb"), 2 | assert = require("assert"), 3 | util = require("util"), 4 | http = require("http"); 5 | 6 | exports.port = 1083; 7 | 8 | exports.batch = function(batch) { 9 | return { 10 | "": { 11 | topic: function() { 12 | var client = new mongodb.Server("localhost", 27017); 13 | db = new mongodb.Db("cube_test", client), 14 | cb = this.callback; 15 | db.open(function(error) { 16 | var collectionsRemaining = 2; 17 | 18 | db.dropCollection("test_events", function(error) { 19 | db.createCollection("test_events", function(error, events) { 20 | events.ensureIndex({t: 1}, collectionReady); 21 | }); 22 | }); 23 | 24 | db.dropCollection("test_metrics", function(error) { 25 | db.createCollection("test_metrics", {capped: true, size: 1e6, autoIndexId: false}, function(error, metrics) { 26 | var indexesRemaining = 3; 27 | metrics.ensureIndex({e: 1, l: 1, t: 1, g: 1}, {unique: true}, indexReady); 28 | metrics.ensureIndex({i: 1, e: 1, l: 1, t: 1}, indexReady); 29 | metrics.ensureIndex({i: 1, l: 1, t: 1}, indexReady); 30 | function indexReady() { 31 | if (!--indexesRemaining) { 32 | collectionReady(); 33 | } 34 | } 35 | }); 36 | }); 37 | 38 | function collectionReady() { 39 | if (!--collectionsRemaining) { 40 | cb(null, {client: client, db: db}); 41 | } 42 | } 43 | }); 44 | }, 45 | "": batch, 46 | teardown: function(test) { 47 | if (test.client.isConnected()) { 48 | test.client.close(); 49 | } 50 | } 51 | } 52 | }; 53 | }; 54 | 55 | exports.request = function(options, data) { 56 | return function() { 57 | var cb = this.callback; 58 | 59 | options.host = "localhost"; 60 | 61 | var request = http.request(options, function(response) { 62 | response.body = ""; 63 | response.setEncoding("utf8"); 64 | response.on("data", function(chunk) { response.body += chunk; }); 65 | response.on("end", function() { cb(null, response); }); 66 | }); 67 | 68 | request.on("error", function(e) { cb(e, null); }); 69 | 70 | if (data && data.length > 0) request.write(data); 71 | request.end(); 72 | }; 73 | }; 74 | 75 | // Disable logging for tests. 76 | util.log = function() {}; 77 | -------------------------------------------------------------------------------- /lib/cube/server/collectd.js: -------------------------------------------------------------------------------- 1 | exports.putter = function(putter) { 2 | var values = {}, 3 | queue = [], 4 | flushInterval, 5 | flushDelay = 5000; 6 | 7 | function store(value, i, event, name) { 8 | var v1 = value.values[i]; 9 | switch (value.dstypes[i]) { 10 | case "gauge": { 11 | event[name] = v1; 12 | break; 13 | } 14 | case "derive": { 15 | var k = value.host 16 | + "/" + value.plugin + "/" + value.plugin_instance 17 | + "/" + value.type + "/" + value.type_instance 18 | + "/" + name; 19 | event[name] = k in values 20 | ? -(values[k] - (values[k] = v1)) 21 | : (values[k] = v1, 0); 22 | break; 23 | } 24 | } 25 | } 26 | 27 | flushInterval = setInterval(function() { 28 | var hosts = {}, 29 | latest = Date.now() - 2 * flushDelay, // to coalesce 30 | retries = []; 31 | 32 | queue.forEach(function(value) { 33 | if (value.time > latest) { 34 | retries.push(value); 35 | } else { 36 | var host = hosts[value.host] || (hosts[value.host] = {}), 37 | event = host[value.time] || (host[value.time] = {}); 38 | event = event[value.plugin] || (event[value.plugin] = {host: value.host}); 39 | if (value.plugin_instance) event = event[value.plugin_instance] || (event[value.plugin_instance] = {}); 40 | if (value.type != value.plugin) event = event[value.type] || (event[value.type] = {}); 41 | if (value.values.length == 1) store(value, 0, event, value.type_instance); 42 | else value.values.forEach(function(d, i) { store(value, i, event, value.dsnames[i]); }); 43 | } 44 | }); 45 | 46 | queue = retries; 47 | 48 | for (var host in hosts) { 49 | for (var time in hosts[host]) { 50 | for (var type in hosts[host][time]) { 51 | putter({ 52 | type: "collectd_" + type, 53 | time: new Date(+time), 54 | data: hosts[host][time][type] 55 | }); 56 | } 57 | } 58 | } 59 | }, flushDelay); 60 | 61 | return function(request, response) { 62 | var content = ""; 63 | request.on("data", function(chunk) { 64 | content += chunk; 65 | }); 66 | request.on("end", function() { 67 | JSON.parse(content).forEach(function(value) { 68 | value.time = Math.round(value.time / 1073741824) * 1000; 69 | queue.push(value); 70 | }); 71 | response.writeHead(200); 72 | response.end(); 73 | }); 74 | }; 75 | }; 76 | 77 | -------------------------------------------------------------------------------- /lib/cube/server/endpoint.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"), 2 | url = require("url"), 3 | path = require("path"), 4 | cube = require("../"); 5 | 6 | exports.re = re; 7 | exports.exact = exact; 8 | exports.file = file; 9 | 10 | var types = { 11 | html: "text/html;charset=utf-8", 12 | css: "text/css;charset=utf-8", 13 | js: "text/javascript;charset=utf-8", 14 | json: "application/json;charset=utf-8", 15 | png: "image/png" 16 | }; 17 | 18 | function exact(method, path, dispatch) { 19 | return { 20 | match: arguments.length < 3 21 | ? (dispatch = path, path = method, function(p) { return p == path; }) 22 | : function(p, m) { return m == method && p == path; }, 23 | dispatch: dispatch 24 | }; 25 | } 26 | 27 | function re(re, dispatch) { 28 | return { 29 | match: function(p) { return re.test(p); }, 30 | dispatch: dispatch 31 | }; 32 | } 33 | 34 | function file() { 35 | var files = Array.prototype.slice.call(arguments), 36 | type = types[files[0].substring(files[0].lastIndexOf(".") + 1)]; 37 | return function(request, response) { 38 | var modified = -Infinity, 39 | size = 0, 40 | n = files.length; 41 | 42 | files.forEach(function(file) { 43 | fs.stat(file, function(error, stats) { 44 | if (error) throw fiveohoh(request, response), error; 45 | size += stats.size; 46 | var time = new Date(stats.mtime); 47 | if (time > modified) modified = time; 48 | if (!--n) respond(); 49 | }); 50 | }); 51 | 52 | function respond() { 53 | var status = modified <= new Date(request.headers["if-modified-since"]) ? 304 : 200, 54 | hasBody = status === 200 && request.method !== "HEAD"; 55 | 56 | var headers = { 57 | "Content-Type": type, 58 | "Date": new Date().toUTCString(), 59 | "Last-Modified": modified.toUTCString() 60 | }; 61 | 62 | if (hasBody) { 63 | headers["Content-Length"] = size; 64 | response.writeHead(status, headers); 65 | read(0); 66 | } else { 67 | response.writeHead(status, headers); 68 | response.end(); 69 | } 70 | } 71 | 72 | function read(i) { 73 | fs.readFile(files[i], function(error, data) { 74 | if (error) throw fiveohoh(request, response), error; 75 | response.write(data); 76 | if (i < files.length - 1) read(i + 1); 77 | else response.end(); 78 | }); 79 | } 80 | }; 81 | }; 82 | 83 | function fiveohoh(request, response) { 84 | response.writeHead(500, {"Content-Type": "text/plain"}); 85 | response.end("500 Server Error"); 86 | } 87 | -------------------------------------------------------------------------------- /lib/cube/client/piece.css: -------------------------------------------------------------------------------- 1 | .piece { 2 | position: absolute; 3 | text-shadow: 0 1px 1px #fff; 4 | font-size: 12px; 5 | } 6 | 7 | .view .piece { 8 | font: 10px sans-serif; 9 | padding: 4px 4px 3px 3px; 10 | } 11 | 12 | .edit .piece { 13 | background: #eee; 14 | border: solid 2px #ccc; 15 | border-radius: 4px; 16 | margin: -2px 0 0 -2px; 17 | } 18 | 19 | .piece:focus, .piece.active { 20 | box-shadow: 0 4px 8px rgba(0,0,0,.3); 21 | outline-style: none; 22 | border-color: #999; 23 | background-color: #e7e7e7; 24 | } 25 | 26 | .piece h3 { 27 | pointer-events: none; 28 | font-size: 14px; 29 | margin: 4px; 30 | } 31 | 32 | .piece textarea, .piece input { 33 | margin-left: 4px; 34 | height: 20px; 35 | border-radius: 4px; 36 | border: solid 1px #ccc; 37 | resize: none; 38 | } 39 | 40 | .piece .time { 41 | margin-top: 1px; 42 | margin-left: 4px; 43 | line-height: 20px; 44 | } 45 | 46 | .piece input { 47 | height: 16px; 48 | width: 88px; 49 | } 50 | 51 | .piece select { 52 | margin-right: 4px; 53 | float: right; 54 | height: 20px; 55 | border-radius: 4px; 56 | border: solid 1px #ccc; 57 | } 58 | 59 | .view .piece.sum, .view .piece.text { 60 | text-align: right; 61 | } 62 | 63 | .resize { 64 | position: absolute; 65 | padding: 6px; 66 | } 67 | 68 | .resize.n { 69 | top: -8px; 70 | left: -8px; 71 | width: 100%; 72 | cursor: ns-resize; 73 | } 74 | 75 | .resize.e { 76 | top: -8px; 77 | right: -8px; 78 | height: 100%; 79 | cursor: ew-resize; 80 | } 81 | 82 | .resize.s { 83 | bottom: -8px; 84 | left: -8px; 85 | width: 100%; 86 | cursor: ns-resize; 87 | } 88 | 89 | .resize.w { 90 | top: -8px; 91 | left: -8px; 92 | height: 100%; 93 | cursor: ew-resize; 94 | } 95 | 96 | .resize.nw { 97 | top: -8px; 98 | left: -8px; 99 | cursor: nwse-resize; 100 | } 101 | 102 | .resize.ne { 103 | top: -8px; 104 | right: -8px; 105 | cursor: nesw-resize; 106 | } 107 | 108 | .resize.se { 109 | bottom: -8px; 110 | right: -8px; 111 | cursor: nwse-resize; 112 | } 113 | 114 | .resize.sw { 115 | bottom: -8px; 116 | left: -8px; 117 | cursor: nesw-resize; 118 | } 119 | 120 | .area path.area { 121 | fill: #e7e7e7; 122 | } 123 | 124 | .area path.line { 125 | fill: none; 126 | stroke: #666; 127 | } 128 | 129 | .axis { 130 | shape-rendering: crispEdges; 131 | } 132 | 133 | .axis .domain { 134 | fill: none; 135 | } 136 | 137 | .x.axis .domain { 138 | stroke: #000; 139 | } 140 | 141 | .x.axis line { 142 | stroke: #fff; 143 | } 144 | 145 | .x.axis .minor { 146 | stroke-opacity: .5; 147 | } 148 | 149 | .y.axis line { 150 | stroke: #eee; 151 | } 152 | -------------------------------------------------------------------------------- /test/endpoint-test.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | assert = require("assert"), 3 | http = require("http"), 4 | test = require("./test"), 5 | endpoint = require("../lib/cube/server/endpoint"); 6 | 7 | var suite = vows.describe("endpoint"); 8 | 9 | var file = "lib/cube/client/semicolon.js", 10 | port = ++test.port, 11 | server = http.createServer(endpoint.file(file, file)); 12 | 13 | server.listen(port, "127.0.0.1"); 14 | 15 | suite.addBatch({ 16 | "file": { 17 | "GET": { 18 | topic: test.request({method: "GET", port: port}), 19 | "the status should be 200": function(response) { 20 | assert.equal(response.statusCode, 200); 21 | }, 22 | "the expected headers should be set": function(response) { 23 | assert.equal(response.headers["content-type"], "text/javascript;charset=utf-8"); 24 | assert.equal(response.headers["content-length"], 2); 25 | assert.ok(new Date(response.headers["date"]) > Date.UTC(2011, 0, 1)); 26 | assert.ok(new Date(response.headers["last-modified"]) > Date.UTC(2011, 0, 1)); 27 | }, 28 | "the expected content should be returned": function(response) { 29 | assert.equal(response.body, ";;"); 30 | } 31 | }, 32 | "GET If-Modified-Since": { 33 | topic: test.request({method: "GET", port: port, headers: {"if-modified-since": new Date(2101, 0, 1).toUTCString()}}), 34 | "the status should be 304": function(response) { 35 | assert.equal(response.statusCode, 304); 36 | }, 37 | "the expected headers should be set": function(response) { 38 | assert.equal(response.headers["content-type"], "text/javascript;charset=utf-8"); 39 | assert.ok(!("Content-Length" in response.headers)); 40 | assert.ok(new Date(response.headers["date"]) > Date.UTC(2011, 0, 1)); 41 | assert.ok(new Date(response.headers["last-modified"]) > Date.UTC(2011, 0, 1)); 42 | }, 43 | "no content should be returned": function(response) { 44 | assert.equal(response.body, ""); 45 | } 46 | }, 47 | "HEAD": { 48 | topic: test.request({method: "HEAD", port: port, headers: {"if-modified-since": new Date(2001, 0, 1).toUTCString()}}), 49 | "the status should be 200": function(response) { 50 | assert.equal(response.statusCode, 200); 51 | }, 52 | "the expected headers should be set": function(response) { 53 | assert.equal(response.headers["content-type"], "text/javascript;charset=utf-8"); 54 | assert.ok(!("Content-Length" in response.headers)); 55 | assert.ok(new Date(response.headers["date"]) > Date.UTC(2011, 0, 1)); 56 | assert.ok(new Date(response.headers["last-modified"]) > Date.UTC(2011, 0, 1)); 57 | }, 58 | "no content should be returned": function(response) { 59 | assert.equal(response.body, ""); 60 | } 61 | } 62 | } 63 | }); 64 | 65 | suite.export(module); 66 | -------------------------------------------------------------------------------- /lib/cube/client/piece-sum.js: -------------------------------------------------------------------------------- 1 | cube.piece.type.sum = function(board) { 2 | var timeout, 3 | socket, 4 | data = 0, 5 | format = d3.format(".2s"); 6 | 7 | var sum = cube.piece(board) 8 | .on("size", resize) 9 | .on("serialize", serialize) 10 | .on("deserialize", deserialize); 11 | 12 | var div = d3.select(sum.node()) 13 | .classed("sum", true); 14 | 15 | if (mode == "edit") { 16 | div.append("h3") 17 | .attr("class", "title") 18 | .text("Rolling Sum"); 19 | 20 | var query = div.append("textarea") 21 | .attr("class", "query") 22 | .attr("placeholder", "query expression…") 23 | .on("keyup.sum", querychange) 24 | .on("focus.sum", sum.focus) 25 | .on("blur.sum", sum.blur); 26 | 27 | var time = div.append("div") 28 | .attr("class", "time") 29 | .text("Time Range:"); 30 | 31 | time.append("input"); 32 | 33 | time.append("select").selectAll("option") 34 | .data([ 35 | {description: "Seconds @ 20", value: 2e4}, 36 | {description: "Minutes @ 5", value: 3e5}, 37 | {description: "Hours", value: 36e5}, 38 | {description: "Days", value: 864e5}, 39 | {description: "Weeks", value: 6048e5}, 40 | {description: "Months", value: 2592e6} 41 | ]) 42 | .enter().append("option") 43 | .property("selected", function(d, i) { return i == 1; }) 44 | .attr("value", cube_piece_areaValue) 45 | .text(function(d) { return d.description; }); 46 | 47 | time.selectAll("input,select") 48 | .on("change.sum", sum.edit) 49 | .on("focus.sum", sum.focus) 50 | .on("blur.sum", sum.blur) 51 | } 52 | 53 | function resize() { 54 | var innerSize = sum.innerSize(), 55 | transition = sum.transition(); 56 | 57 | if (mode == "edit") { 58 | transition.select(".query") 59 | .style("width", innerSize[0] - 12 + "px") 60 | .style("height", innerSize[1] - 58 + "px"); 61 | 62 | transition.select(".time select") 63 | .style("width", innerSize[0] - 174 + "px"); 64 | } else { 65 | transition 66 | .style("font-size", innerSize[0] / 5 + "px") 67 | .style("line-height", innerSize[1] + "px") 68 | .text(format(data)); 69 | } 70 | } 71 | 72 | function redraw() { 73 | div.text(format(data)); 74 | return true; 75 | } 76 | 77 | function querychange() { 78 | if (timeout) clearTimeout(timeout); 79 | timeout = setTimeout(sum.edit, 750); 80 | } 81 | 82 | function serialize(json) { 83 | var step = +time.select("select").property("value"), 84 | range = time.select("input").property("value") * cube_piece_areaMultipler(step); 85 | json.type = "sum"; 86 | json.query = query.property("value"); 87 | json.time = {range: range, step: step}; 88 | } 89 | 90 | function deserialize(json) { 91 | if (!json.time.range) json.time = {range: json.time, step: 3e5}; 92 | if (mode == "edit") { 93 | query.property("value", json.query); 94 | time.select("input").property("value", json.time.range / cube_piece_areaMultipler(json.time.step)); 95 | time.select("select").property("value", json.time.step); 96 | } else { 97 | var dt = json.time.step, 98 | t1 = new Date(Math.floor(Date.now() / dt) * dt), 99 | t0 = new Date(t1 - json.time.range); 100 | 101 | data = 0; 102 | 103 | if (timeout) timeout = clearTimeout(timeout); 104 | if (socket) socket.close(); 105 | socket = new WebSocket("ws://" + location.host + "/1.0/metric/get"); 106 | socket.onopen = load; 107 | socket.onmessage = store; 108 | 109 | function load() { 110 | socket.send(JSON.stringify({ 111 | expression: json.query, 112 | start: cube_time(t0), 113 | stop: cube_time(t1), 114 | step: dt 115 | })); 116 | timeout = setTimeout(function() { 117 | deserialize(json); 118 | }, t1 - Date.now() + dt + 4500 + 1000 * Math.random()); 119 | } 120 | 121 | function store(message) { 122 | data += JSON.parse(message.data).value; 123 | d3.timer(redraw); 124 | } 125 | } 126 | } 127 | 128 | sum.copy = function() { 129 | return board.add(cube.piece.type.sum); 130 | }; 131 | 132 | resize(); 133 | 134 | return sum; 135 | }; 136 | -------------------------------------------------------------------------------- /test/reduces-test.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | assert = require("assert"), 3 | reduces = require("../lib/cube/server/reduces"); 4 | 5 | var suite = vows.describe("reduces"); 6 | 7 | suite.addBatch({ 8 | "reduces": { 9 | "contains exactly the expected reduces": function() { 10 | var keys = []; 11 | for (var key in reduces) { 12 | keys.push(key); 13 | } 14 | keys.sort(); 15 | assert.deepEqual(keys, ["distinct", "max", "median", "min", "sum"]); 16 | } 17 | }, 18 | 19 | "distinct": { 20 | topic: function() { 21 | return reduces.distinct; 22 | }, 23 | "empty is zero": function(reduce) { 24 | assert.strictEqual(reduce.empty, 0); 25 | }, 26 | "is not pyramidal": function(reduce) { 27 | assert.isTrue(!reduce.pyramidal); 28 | }, 29 | "returns the number of distinct values": function(reduce) { 30 | assert.equal(reduce([1, 2, 3, 2, 1]), 3); 31 | }, 32 | "determines uniqueness based on string coercion": function(reduce) { 33 | assert.equal(reduce([{}, {}, {}]), 1); 34 | assert.equal(reduce([{}, "[object Object]", new Object]), 1); 35 | assert.equal(reduce([new Number(1), 1, "1"]), 1); 36 | assert.equal(reduce([new Number(1), 2, "3", "2", 3, new Number(1)]), 3); 37 | assert.equal(reduce([{toString: function() { return 1; }}, 1, 2]), 2); 38 | } 39 | }, 40 | 41 | "max": { 42 | topic: function() { 43 | return reduces.max; 44 | }, 45 | "empty is undefined": function(reduce) { 46 | assert.strictEqual(reduce.empty, undefined); 47 | }, 48 | "is pyramidal": function(reduce) { 49 | assert.isTrue(reduce.pyramidal); 50 | }, 51 | "returns the maximum value": function(reduce) { 52 | assert.equal(reduce([1, 2, 3, 2, 1]), 3); 53 | }, 54 | "ignores undefined and NaN": function(reduce) { 55 | assert.equal(reduce([1, NaN, 3, undefined, null]), 3); 56 | }, 57 | "compares using natural order": function(reduce) { 58 | assert.equal(reduce([2, 10, 3]), 10); 59 | assert.equal(reduce(["2", "10", "3"]), "3"); 60 | assert.equal(reduce(["2", "10", 3]), 3); // "2" < "10", "10" < 3 61 | assert.equal(reduce([3, "2", "10"]), "10"); // "2" < 3, 3 < "10" 62 | }, 63 | "returns the first of equal values": function(reduce) { 64 | assert.strictEqual(reduce([1, new Number(1)]), 1); 65 | } 66 | }, 67 | 68 | "median": { 69 | topic: function() { 70 | return reduces.median; 71 | }, 72 | "empty is undefined": function(reduce) { 73 | assert.strictEqual(reduce.empty, undefined); 74 | }, 75 | "is not pyramidal": function(reduce) { 76 | assert.isTrue(!reduce.pyramidal); 77 | }, 78 | "returns the median value": function(reduce) { 79 | assert.equal(reduce([1, 2, 3, 2, 1]), 2); 80 | assert.equal(reduce([1, 2, 4, 2, 1, 4, 4, 4]), 3); 81 | }, 82 | "sorts input in-place": function(reduce) { 83 | var values = [1, 2, 3, 2, 1]; 84 | reduce(values); 85 | assert.deepEqual(values, [1, 1, 2, 2, 3]); 86 | }, 87 | "ignores undefined and NaN": function(reduce) { 88 | assert.equal(reduce([1, NaN, 3, undefined, 0]), 0); 89 | } 90 | }, 91 | 92 | "min": { 93 | topic: function() { 94 | return reduces.min; 95 | }, 96 | "empty is undefined": function(reduce) { 97 | assert.strictEqual(reduce.empty, undefined); 98 | }, 99 | "is pyramidal": function(reduce) { 100 | assert.isTrue(reduce.pyramidal); 101 | }, 102 | "returns the minimum value": function(reduce) { 103 | assert.equal(reduce([1, 2, 3, 2, 1]), 1); 104 | }, 105 | "ignores undefined and NaN": function(reduce) { 106 | assert.equal(reduce([1, NaN, 3, undefined, 0]), 0); 107 | }, 108 | "compares using natural order": function(reduce) { 109 | assert.equal(reduce([2, 10, 3]), 2); 110 | assert.equal(reduce(["2", "10", 3]), 3); // "2" > "10", 3 > "2" 111 | assert.equal(reduce([3, "2", "10"]), "10"); // 3 > "2", "2" > "10" 112 | }, 113 | "returns the first of equal values": function(reduce) { 114 | assert.strictEqual(reduce([1, new Number(1)]), 1); 115 | } 116 | }, 117 | 118 | "sum": { 119 | topic: function() { 120 | return reduces.sum; 121 | }, 122 | "empty is zero": function(reduce) { 123 | assert.strictEqual(reduce.empty, 0); 124 | }, 125 | "is pyramidal": function(reduce) { 126 | assert.isTrue(reduce.pyramidal); 127 | }, 128 | "returns the sum of values": function(reduce) { 129 | assert.equal(reduce([1, 2, 3, 2, 1]), 9); 130 | assert.equal(reduce([1, 2, 4, 2, 1, 4, 4, 4]), 22); 131 | }, 132 | "does not ignore undefined and NaN": function(reduce) { 133 | assert.isNaN(reduce([1, NaN, 3, undefined, 0])); 134 | } 135 | } 136 | }); 137 | 138 | suite.export(module); 139 | -------------------------------------------------------------------------------- /lib/cube/server/server.js: -------------------------------------------------------------------------------- 1 | var util = require("util"), 2 | url = require("url"), 3 | http = require("http"), 4 | websocket = require("websocket"), 5 | websprocket = require("websocket-server"), 6 | mongodb = require("mongodb"); 7 | 8 | // Don't crash on errors. 9 | process.on("uncaughtException", function(error) { 10 | util.log(error.stack); 11 | }); 12 | 13 | // And then this happened: 14 | websprocket.Connection = require("../../../node_modules/websocket-server/lib/ws/connection"); 15 | 16 | // Configuration for WebSocket requests. 17 | var wsOptions = { 18 | maxReceivedFrameSize: 0x10000, 19 | maxReceivedMessageSize: 0x100000, 20 | fragmentOutgoingMessages: true, 21 | fragmentationThreshold: 0x4000, 22 | keepalive: true, 23 | keepaliveInterval: 20000, 24 | assembleFragments: true, 25 | disableNagleAlgorithm: true, 26 | closeTimeout: 5000 27 | }; 28 | 29 | module.exports = function(options) { 30 | var server = {}, 31 | primary = http.createServer(), 32 | secondary = websprocket.createServer(), 33 | endpoints = {ws: [], http: []}, 34 | mongo = new mongodb.Server(options["mongo-host"], options["mongo-port"]), 35 | db = new mongodb.Db(options["mongo-database"], mongo), 36 | id = 0; 37 | 38 | secondary.server = primary; 39 | 40 | // Register primary WebSocket listener with fallback. 41 | primary.on("upgrade", function(request, socket, head) { 42 | if ("sec-websocket-version" in request.headers) { 43 | request = new websocket.request(socket, request, wsOptions); 44 | request.readHandshake(); 45 | connect(request.accept(request.requestedProtocols[0], request.origin), request.httpRequest); 46 | } else if (request.method === "GET" 47 | && /^websocket$/i.test(request.headers.upgrade) 48 | && /^upgrade$/i.test(request.headers.connection)) { 49 | new websprocket.Connection(secondary.manager, secondary.options, request, socket, head); 50 | } 51 | }); 52 | 53 | // Register secondary WebSocket listener. 54 | secondary.on("connection", function(connection) { 55 | connection.socket = connection._socket; 56 | connection.remoteAddress = connection.socket.remoteAddress; 57 | connection.sendUTF = connection.send; 58 | connect(connection, connection._req); 59 | }); 60 | 61 | function connect(connection, request) { 62 | util.log(connection.remoteAddress + " " + request.url); 63 | 64 | // Forward messages to the appropriate endpoint, or close the connection. 65 | for (var i = -1, n = endpoints.ws.length, e; ++i < n;) { 66 | if ((e = endpoints.ws[i]).match(request.url)) { 67 | 68 | function callback(response) { 69 | if (connection.socket.writable) { 70 | connection.sendUTF(JSON.stringify(response)); 71 | } 72 | } 73 | 74 | callback.id = ++id; 75 | 76 | // Listen for close events. 77 | if (e.dispatch.close) { 78 | connection.on("close", function() { 79 | interval = clearInterval(interval); 80 | e.dispatch.close(callback); 81 | }); 82 | 83 | // Unfortunately, it looks like there is a bug in websocket-server (or 84 | // somewhere else) where close events are not emitted if the socket is 85 | // closed very shortly after it is opened. So we do an additional 86 | // check using an interval to verify that the socket is still open. 87 | var interval = setInterval(function() { 88 | if (!connection.socket.writable) { 89 | interval = clearInterval(interval); 90 | connection.close(); 91 | } 92 | }, 5000); 93 | } 94 | 95 | return connection.on("message", function(request) { 96 | e.dispatch(JSON.parse(request.utf8Data || request), callback); 97 | }); 98 | } 99 | } 100 | 101 | connection.close(); 102 | } 103 | 104 | // Register HTTP listener. 105 | primary.on("request", function(request, response) { 106 | var u = url.parse(request.url); 107 | util.log(request.connection.remoteAddress + " " + u.pathname); 108 | 109 | // Forward messages to the appropriate endpoint, or 404. 110 | for (var i = -1, n = endpoints.http.length, e; ++i < n;) { 111 | if ((e = endpoints.http[i]).match(u.pathname, request.method)) { 112 | return e.dispatch(request, response); 113 | } 114 | } 115 | 116 | response.writeHead(404, {"Content-Type": "text/plain"}); 117 | response.end("404 Not Found"); 118 | }); 119 | 120 | server.start = function() { 121 | // Connect to mongodb. 122 | util.log("starting mongodb client"); 123 | db.open(function(error) { 124 | if (error) throw error; 125 | server.register(db, endpoints); 126 | }); 127 | 128 | // Start the server! 129 | util.log("starting http server on port " + options["http-port"]); 130 | primary.listen(options["http-port"]); 131 | }; 132 | 133 | return server; 134 | }; 135 | -------------------------------------------------------------------------------- /lib/cube/server/event.js: -------------------------------------------------------------------------------- 1 | // TODO report failures? 2 | // TODO include the event._id (and define a JSON encoding for ObjectId?) 3 | // TODO allow the event time to change when updating (fix invalidation) 4 | // TODO fix race condition between cache invalidation and metric computation 5 | 6 | var util = require("util"), 7 | mongodb = require("mongodb"), 8 | parser = require("./event-expression"), 9 | tiers = require("./tiers"), 10 | types = require("./types"); 11 | 12 | var type_re = /^[a-z][a-zA-Z0-9_]+$/, 13 | invalidate = {$set: {i: true}}, 14 | multi = {multi: true}, 15 | event_options = {sort: {t: -1}, batchSize: 1000}, 16 | type_options = {safe: true}; 17 | 18 | exports.putter = function(db) { 19 | var collection = types(db), 20 | eventsByType = {}, 21 | flushInterval, 22 | flushTypes = {}, 23 | flushDelay = 5000; 24 | 25 | function endpoint(request) { 26 | var time = new Date(request.time), 27 | type = request.type, 28 | events = eventsByType[type]; 29 | 30 | // Validate the date and type. 31 | if (!type_re.test(type)) return util.log("invalid type: " + request.type); 32 | if (isNaN(time)) return util.log("invalid time: " + request.time); 33 | 34 | // If this is a known event type, save immediately. 35 | if (events) return save(events); 36 | 37 | // Otherwise, verify that the collection exists before saving. 38 | db.collection(type + "_events", type_options, function(error, events) { 39 | if (error) return util.log("unknown type: " + type); 40 | save(eventsByType[type] = events); 41 | }); 42 | 43 | // Create and save the event object. 44 | function save(events) { 45 | var event = {t: time, d: request.data}; 46 | 47 | // If an id is specified, promote it to Mongo's primary key. 48 | if ("id" in request) event._id = request.id; 49 | 50 | events.save(event); 51 | 52 | // Queue invalidation of metrics for this type. 53 | var times = flushTypes[type] || (flushTypes[type] = [time, time]); 54 | if (time < times[0]) times[0] = time; 55 | if (time > times[1]) times[1] = time; 56 | } 57 | } 58 | 59 | // Invalidate cached metrics. 60 | endpoint.flush = function() { 61 | var types = []; 62 | for (var type in flushTypes) { 63 | var metrics = collection(type).metrics, 64 | times = flushTypes[type]; 65 | types.push(type); 66 | for (var tier in tiers) { 67 | var floor = tiers[tier].floor; 68 | metrics.update({ 69 | i: false, 70 | l: +tier, 71 | t: { 72 | $gte: floor(times[0]), 73 | $lte: floor(times[1]) 74 | } 75 | }, invalidate, multi); 76 | } 77 | } 78 | if (types.length) util.log("flush " + types.join(", ")); 79 | flushTypes = {}; 80 | }; 81 | 82 | flushInterval = setInterval(endpoint.flush, flushDelay); 83 | 84 | return endpoint; 85 | }; 86 | 87 | exports.getter = function(db) { 88 | var collection = types(db), 89 | streamDelay = 5000; 90 | 91 | function getter(request, callback) { 92 | var stream = !("stop" in request), 93 | start = new Date(request.start), 94 | stop = stream ? new Date(Date.now() - streamDelay) : new Date(request.stop); 95 | 96 | // Validate the dates. 97 | if (isNaN(start)) return util.log("invalid start: " + request.start); 98 | if (isNaN(stop)) return util.log("invalid stop: " + request.stop); 99 | 100 | // Parse the expression. 101 | var expression; 102 | try { 103 | expression = parser.parse(request.expression); 104 | } catch (error) { 105 | return util.log("invalid expression: " + error); 106 | } 107 | 108 | // Copy any expression filters into the query object. 109 | var filter = {t: {$gte: start, $lt: stop}}; 110 | expression.filter(filter); 111 | 112 | // Request any needed fields. 113 | var fields = {t: 1}; 114 | expression.fields(fields); 115 | 116 | // Query for the desired events. 117 | function query() { 118 | collection(expression.type).events.find(filter, fields, event_options, function(error, cursor) { 119 | if (error) throw error; 120 | cursor.each(function(error, event) { 121 | if (callback.closed) return cursor.close(); 122 | if (error) throw error; 123 | if (event) callback({ 124 | time: event.t, 125 | data: event.d 126 | }); 127 | }); 128 | }); 129 | } 130 | 131 | query(); 132 | 133 | // While streaming, periodically poll for new results. 134 | if (stream) { 135 | stream = setInterval(function() { 136 | if (callback.closed) return clearInterval(stream); 137 | filter.t.$gte = stop; 138 | filter.t.$lt = stop = new Date(Date.now() - streamDelay); 139 | query(); 140 | }, streamDelay); 141 | } 142 | } 143 | 144 | getter.close = function(callback) { 145 | callback.closed = true; 146 | }; 147 | 148 | return getter; 149 | }; 150 | -------------------------------------------------------------------------------- /lib/cube/server/event-expression.peg: -------------------------------------------------------------------------------- 1 | // TODO allow null literals (requires fixing pegjs) 2 | 3 | { 4 | var filterEqual = function(o, k, v) { o[k] = v; }, 5 | filterGreater = filter("$gt"), 6 | filterGreaterOrEqual = filter("$gte"), 7 | filterLess = filter("$lt"), 8 | filterLessOrEqual = filter("$lte"), 9 | filterNotEqual = filter("$ne"), 10 | filterRegularExpression = filter("$regex"), 11 | filterIn = filter("$in"), 12 | exists = {$exists: true}; 13 | 14 | function noop() {} 15 | 16 | function filter(op) { 17 | return function(o, k, v) { 18 | var f = o[k]; 19 | switch (typeof f) { 20 | case "undefined": o[k] = f = {}; // continue 21 | case "object": f[op] = v; break; 22 | // otherwise, observe the existing equals (literal) filter 23 | } 24 | }; 25 | } 26 | 27 | function arrayAccessor(name) { 28 | name = new String(name); 29 | name.array = true; 30 | return name; 31 | } 32 | 33 | function objectAccessor(name) { 34 | return name; 35 | } 36 | 37 | function compoundFields(type, head, tail) { 38 | var n = tail.length; 39 | return { 40 | type: type, 41 | exists: function(o) { 42 | var i = -1; 43 | head.exists(o); 44 | while (++i < n) tail[i][3].exists(o); 45 | }, 46 | fields: function(o) { 47 | var i = -1; 48 | head.fields(o); 49 | while (++i < n) tail[i][3].fields(o); 50 | } 51 | }; 52 | } 53 | 54 | function member(head, tail) { 55 | var fields = ["d", head].concat(tail), 56 | shortName = fields.filter(function(d) { return !d.array; }).join("."), 57 | longName = fields.join("."), 58 | i = -1, 59 | n = fields.length; 60 | return { 61 | field: longName, 62 | exists: function(o) { 63 | if (!(shortName in o)) { 64 | o[shortName] = exists; 65 | } 66 | }, 67 | fields: function(o) { 68 | o[shortName] = 1; 69 | } 70 | }; 71 | } 72 | 73 | } 74 | 75 | start 76 | = _ expression:event_expression _ { return expression; } 77 | 78 | event_expression 79 | = value:event_value_expression filters:(_ "." _ event_filter_expression)* 80 | { 81 | value.filter = function(filter) { 82 | var i = -1, n = filters.length; 83 | while (++i < n) filters[i][3](filter); 84 | value.exists(filter); 85 | }; 86 | return value; 87 | } 88 | 89 | event_filter_expression 90 | = op:filter_operator _ "(" _ member:event_member_expression _ "," _ value:literal _ ")" { return function(o) { op(o, member.field, value); }; } 91 | 92 | event_value_expression 93 | = type:identifier _ "(" _ head:event_member_expression tail:(_ "," _ event_member_expression)* _ ")" { return compoundFields(type, head, tail); } 94 | / type:identifier { return {type: type, exists: noop, fields: noop}; } 95 | 96 | event_member_expression 97 | = head:identifier tail:( 98 | _ "[" _ name:number _ "]" { return arrayAccessor(name); } 99 | / _ "." _ name:identifier { return objectAccessor(name); } 100 | )* { return member(head, tail); } 101 | 102 | filter_operator 103 | = "eq" { return filterEqual; } 104 | / "gt" { return filterGreater; } 105 | / "ge" { return filterGreaterOrEqual; } 106 | / "lt" { return filterLess; } 107 | / "le" { return filterLessOrEqual; } 108 | / "ne" { return filterNotEqual; } 109 | / "re" { return filterRegularExpression; } 110 | / "in" { return filterIn; } 111 | 112 | identifier 113 | = first:[a-zA-Z_$] rest:[a-zA-Z0-9_$]* { return first + rest.join(""); } 114 | 115 | literal 116 | = array_literal 117 | / string 118 | / number 119 | / "true" { return true; } 120 | / "false" { return false; } 121 | 122 | array_literal 123 | = "[" _ first:literal rest:(_ "," _ literal)* _ "]" { return [first].concat(rest.map(function(d) { return d[3]; })); } 124 | / "[" _ "]" { return []; } 125 | 126 | string "string" 127 | = '"' chars:double_string_char* '"' { return chars.join(""); } 128 | / "'" chars:single_string_char* "'" { return chars.join(""); } 129 | 130 | double_string_char 131 | = !('"' / "\\") char_:. { return char_; } 132 | / "\\" sequence:escape_sequence { return sequence; } 133 | 134 | single_string_char 135 | = !("'" / "\\") char_:. { return char_; } 136 | / "\\" sequence:escape_sequence { return sequence; } 137 | 138 | escape_sequence 139 | = character_escape_sequence 140 | / "0" !digit { return "\0"; } 141 | / hex_escape_sequence 142 | / unicode_escape_sequence 143 | 144 | character_escape_sequence 145 | = single_escape_character 146 | / non_escape_character 147 | 148 | single_escape_character 149 | = char_:['"\\bfnrtv] { return char_.replace("b", "\b").replace("f", "\f").replace("n", "\n").replace("r", "\r").replace("t", "\t").replace("v", "\x0B"); } 150 | 151 | non_escape_character 152 | = !escape_character char_:. { return char_; } 153 | 154 | escape_character 155 | = single_escape_character 156 | / digit 157 | / "x" 158 | / "u" 159 | 160 | hex_escape_sequence 161 | = "x" h1:hex_digit h2:hex_digit { return String.fromCharCode(+("0x" + h1 + h2)); } 162 | 163 | unicode_escape_sequence 164 | = "u" h1:hex_digit h2:hex_digit h3:hex_digit h4:hex_digit { return String.fromCharCode(+("0x" + h1 + h2 + h3 + h4)); } 165 | 166 | number "number" 167 | = "-" _ number:number { return -number; } 168 | / int_:int frac:frac exp:exp { return +(int_ + frac + exp); } 169 | / int_:int frac:frac { return +(int_ + frac); } 170 | / int_:int exp:exp { return +(int_ + exp); } 171 | / frac:frac { return +frac; } 172 | / int_:int { return +int_; } 173 | 174 | int 175 | = digit19:digit19 digits:digits { return digit19 + digits; } 176 | / digit:digit 177 | 178 | frac 179 | = "." digits:digits { return "." + digits; } 180 | 181 | exp 182 | = e:e digits:digits { return e + digits; } 183 | 184 | digits 185 | = digits:digit+ { return digits.join(""); } 186 | 187 | e 188 | = e:[eE] sign:[+-]? { return e + sign; } 189 | 190 | digit 191 | = [0-9] 192 | 193 | digit19 194 | = [1-9] 195 | 196 | hex_digit 197 | = [0-9a-fA-F] 198 | 199 | _ "whitespace" 200 | = whitespace* 201 | 202 | whitespace 203 | = [ \t\n\r] 204 | -------------------------------------------------------------------------------- /lib/cube/client/board.js: -------------------------------------------------------------------------------- 1 | cube.board = function(url, id) { 2 | var board = {id: cube_board_formatId(id)}, 3 | socket, 4 | interval, 5 | pieceId = 0, 6 | palette, 7 | squares, 8 | pieces = [], 9 | size = [32, 18], // in number of squares 10 | squareSize = 40, // in pixels 11 | squareRadius = 4, // in pixels 12 | padding = 9.5; // half-pixel for crisp strokes 13 | 14 | var event = d3.dispatch( 15 | "size", 16 | "squareSize", 17 | "squareRadius", 18 | "padding", 19 | "view" 20 | ); 21 | 22 | var svg = document.createElementNS(d3.ns.prefix.svg, "svg"); 23 | 24 | d3.select(svg) 25 | .attr("class", "board"); 26 | 27 | event.on("size.board", resize); 28 | event.on("squareSize.board", resize); 29 | event.on("squareRadius.board", resize); 30 | event.on("padding.board", resize); 31 | 32 | function message(message) { 33 | var e = JSON.parse(message.data); 34 | switch (e.type) { 35 | case "view": { 36 | event.view.call(board, e); 37 | break; 38 | } 39 | case "add": { 40 | var piece = board.add(cube.piece.type[e.piece.type]) 41 | .fromJSON(e.piece) 42 | .on("move.board", move); 43 | 44 | d3.select(piece.node()) 45 | .style("opacity", 1e-6) 46 | .transition() 47 | .duration(500) 48 | .style("opacity", 1); 49 | 50 | pieceId = Math.max(pieceId, piece.id = e.piece.id); 51 | break; 52 | } 53 | case "edit": { 54 | pieces.some(function(piece) { 55 | if (piece.id == e.piece.id) { 56 | piece 57 | .on("move.board", null) 58 | .transition(d3.transition().duration(500)) 59 | .fromJSON(e.piece); 60 | 61 | // Renable events after transition starts. 62 | d3.timer(function() { piece.on("move.board", move); }, 250); 63 | return true; 64 | } 65 | }); 66 | break; 67 | } 68 | case "move": { 69 | pieces.some(function(piece) { 70 | if (piece.id == e.piece.id) { 71 | piece 72 | .on("move.board", null) 73 | .transition(d3.transition().duration(500)) 74 | .size(e.piece.size) 75 | .position(e.piece.position); 76 | 77 | // Bring to front. 78 | svg.parentNode.appendChild(piece.node()); 79 | 80 | // Renable events after transition starts. 81 | d3.timer(function() { piece.on("move.board", move); }, 250); 82 | return true; 83 | } 84 | }); 85 | break; 86 | } 87 | case "remove": { 88 | pieces.some(function(piece) { 89 | if (piece.id == e.piece.id) { 90 | board.remove(piece, true); 91 | return true; 92 | } 93 | }); 94 | break; 95 | } 96 | } 97 | } 98 | 99 | function reopen() { 100 | if (socket) { 101 | pieces.slice().forEach(function(piece) { board.remove(piece, true); }); 102 | socket.close(); 103 | } 104 | socket = new WebSocket(url); 105 | socket.onopen = load; 106 | socket.onmessage = message; 107 | if (!interval) interval = setInterval(ping, 5000); 108 | } 109 | 110 | function load() { 111 | if (id && socket && socket.readyState == 1) { 112 | socket.send(JSON.stringify({type: "load", id: id})); 113 | } 114 | } 115 | 116 | function ping() { 117 | if (socket.readyState == 1) { 118 | socket.send(JSON.stringify({type: "ping", id: id})); 119 | } else if (socket.readyState > 1) { 120 | reopen(); 121 | } 122 | } 123 | 124 | // A one-time listener to send an add event on mouseup. 125 | function add() { 126 | socket.send(JSON.stringify({type: "add", id: id, piece: this})); 127 | this.on("move.board", move); 128 | } 129 | 130 | function move() { 131 | socket.send(JSON.stringify({type: "move", id: id, piece: this})); 132 | } 133 | 134 | function edit() { 135 | socket.send(JSON.stringify({type: "edit", id: id, piece: this})); 136 | } 137 | 138 | function resize() { 139 | d3.select(svg) 140 | .attr("width", size[0] * squareSize + 2 * padding) 141 | .attr("height", (size[1] + 2) * squareSize + 2 * padding); 142 | 143 | d3.select(palette.node()) 144 | .attr("transform", "translate(" + padding + "," + padding + ")"); 145 | 146 | d3.select(squares.node()) 147 | .attr("transform", "translate(" + padding + "," + (1.5 * squareSize + padding) + ")"); 148 | } 149 | 150 | board.node = function() { 151 | return svg; 152 | }; 153 | 154 | board.on = function(type, listener) { 155 | event.on(type, listener); 156 | return board; 157 | }; 158 | 159 | board.size = function(x) { 160 | if (!arguments.length) return size; 161 | event.size.call(board, size = x); 162 | return board; 163 | }; 164 | 165 | board.squareSize = function(x) { 166 | if (!arguments.length) return squareSize; 167 | event.squareSize.call(board, squareSize = x); 168 | return board; 169 | }; 170 | 171 | board.squareRadius = function(x) { 172 | if (!arguments.length) return squareRadius; 173 | event.squareRadius.call(board, squareRadius = x); 174 | return board; 175 | }; 176 | 177 | board.padding = function(x) { 178 | if (!arguments.length) return padding; 179 | event.padding.call(board, padding = x); 180 | return board; 181 | }; 182 | 183 | board.add = function(type) { 184 | var piece = type(board); 185 | piece.id = ++pieceId; 186 | piece.on("move.board", add).on("edit.board", edit); 187 | svg.parentNode.appendChild(piece.node()); 188 | pieces.push(piece); 189 | return piece; 190 | }; 191 | 192 | board.remove = function(piece, silent) { 193 | piece.on("move.board", null).on("edit.board", null); 194 | if (silent) { 195 | d3.select(piece.node()) 196 | .style("opacity", 1) 197 | .transition() 198 | .duration(500) 199 | .style("opacity", 1e-6) 200 | .remove(); 201 | } else { 202 | socket.send(JSON.stringify({type: "remove", id: id, piece: {id: piece.id}})); 203 | svg.parentNode.removeChild(piece.node()); 204 | } 205 | pieces.splice(pieces.indexOf(piece), 1); 206 | return piece; 207 | }; 208 | 209 | board.toJSON = function() { 210 | return {id: id, size: size, pieces: pieces}; 211 | }; 212 | 213 | svg.appendChild((palette = cube.palette(board)).node()); 214 | svg.appendChild((squares = cube.squares(board)).node()); 215 | resize(); 216 | reopen(); 217 | 218 | return board; 219 | }; 220 | 221 | function cube_board_formatId(id) { 222 | id = id.toString(36); 223 | if (id.length < 6) id = new Array(7 - id.length).join("0") + id; 224 | return id; 225 | } 226 | -------------------------------------------------------------------------------- /lib/cube/server/visualizer.js: -------------------------------------------------------------------------------- 1 | var url = require("url"), 2 | path = require("path"), 3 | endpoint = require("./endpoint"); 4 | 5 | exports.register = function(db, endpoints) { 6 | endpoints.ws.push( 7 | endpoint.exact("/board", viewBoard(db)) 8 | ); 9 | endpoints.http.push( 10 | endpoint.exact("/", createBoard(db)), 11 | endpoint.re(/^\/[0-9][0-9a-z]{5}(\/edit)?$/, loadBoard(db)), 12 | endpoint.exact("/cube.js", endpoint.file( 13 | resolve("start.js"), 14 | resolve("cube.js"), 15 | resolve("piece.js"), 16 | resolve("piece-area.js"), 17 | resolve("piece-sum.js"), 18 | resolve("piece-text.js"), 19 | resolve("palette.js"), 20 | resolve("squares.js"), 21 | resolve("board.js"), 22 | resolve("header.js"), 23 | resolve("end.js") 24 | )), 25 | endpoint.exact("/cube.css", endpoint.file( 26 | resolve("body.css"), 27 | resolve("palette.css"), 28 | resolve("board.css"), 29 | resolve("piece.css") 30 | )), 31 | endpoint.exact("/d3/d3.js", endpoint.file( 32 | resolve("../../../node_modules/d3/d3.min.js"), 33 | resolve("semicolon.js"), 34 | resolve("../../../node_modules/d3/d3.geo.min.js"), 35 | resolve("semicolon.js"), 36 | resolve("../../../node_modules/d3/d3.geom.min.js"), 37 | resolve("semicolon.js"), 38 | resolve("../../../node_modules/d3/d3.layout.min.js"), 39 | resolve("semicolon.js"), 40 | resolve("../../../node_modules/d3/d3.time.min.js") 41 | )) 42 | ); 43 | }; 44 | 45 | function createBoard(db) { 46 | var boards, max = parseInt("9zzzzy", 36); 47 | 48 | db.collection("boards", function(error, collection) { 49 | boards = collection; 50 | }); 51 | 52 | return function random(request, response) { 53 | var id = (Math.random() * max | 0) + 1; 54 | boards.insert({_id: id}, {safe: true}, function(error) { 55 | if (error) { 56 | if (/^E11000/.test(error.message)) return random(request, response); // duplicate 57 | response.writeHead(500, {"Content-Type": "text/plain"}); 58 | response.end("500 Server Error"); 59 | } else { 60 | id = id.toString(36); 61 | if (id.length < 6) id = new Array(7 - id.length).join("0") + id; 62 | response.writeHead(302, {"Location": "http://" + request.headers["host"] + "/" + id + "/edit"}); 63 | response.end(); 64 | } 65 | }); 66 | }; 67 | } 68 | 69 | function loadBoard(db) { 70 | var boards, 71 | file = endpoint.file(resolve("visualizer.html")); 72 | 73 | db.collection("boards", function(error, collection) { 74 | boards = collection; 75 | }); 76 | 77 | return function random(request, response) { 78 | var id = parseInt(url.parse(request.url).pathname.substring(1), "36"); 79 | boards.findOne({_id: id}, function(error, object) { 80 | if (object == null) { 81 | response.writeHead(404, {"Content-Type": "text/plain"}); 82 | response.end("404 Not Found"); 83 | } else { 84 | file(request, response); 85 | } 86 | }); 87 | }; 88 | } 89 | 90 | function viewBoard(db) { 91 | var boards, 92 | boardsByCallback = {}, 93 | callbacksByBoard = {}; 94 | 95 | db.collection("boards", function(error, collection) { 96 | boards = collection; 97 | }); 98 | 99 | function dispatch(request, callback) { 100 | switch (request.type) { 101 | case "load": load(request, callback); break; 102 | case "add": add(request, callback); break; 103 | case "edit": case "move": move(request, callback); break; 104 | case "remove": remove(request, callback); break; 105 | default: callback({type: "error", status: 400}); break; 106 | } 107 | } 108 | 109 | function add(request, callback) { 110 | var boardId = request.id, 111 | callbacks = callbacksByBoard[boardId].filter(function(c) { return c.id != callback.id; }); 112 | boards.update({_id: boardId}, {$push: {pieces: request.piece}}); 113 | if (callbacks.length) emit(callbacks, {type: "add", piece: request.piece}); 114 | } 115 | 116 | function move(request, callback) { 117 | var boardId = request.id, 118 | callbacks = callbacksByBoard[boardId].filter(function(c) { return c.id != callback.id; }); 119 | boards.update({_id: boardId, "pieces.id": request.piece.id}, {$set: {"pieces.$": request.piece}}); 120 | if (callbacks.length) emit(callbacks, {type: request.type, piece: request.piece}); 121 | } 122 | 123 | function remove(request, callback) { 124 | var boardId = request.id, 125 | callbacks = callbacksByBoard[boardId].filter(function(c) { return c.id != callback.id; }); 126 | boards.update({_id: boardId}, {$pull: {pieces: {id: request.piece.id}}}); 127 | if (callbacks.length) emit(callbacks, {type: "remove", piece: {id: request.piece.id}}); 128 | } 129 | 130 | function load(request, callback) { 131 | var boardId = boardsByCallback[callback.id], 132 | callbacks; 133 | 134 | // If callback was previously viewing to a different board, remove it. 135 | if (boardId) { 136 | callbacks = callbacksByBoard[boardId]; 137 | callbacks.splice(callbacks.indexOf(callback), 1); 138 | if (callbacks.length) emit(callbacks, {type: "view", count: callbacks.length}); 139 | else delete callbacksByBoard[boardId]; 140 | } 141 | 142 | // Register that we are now viewing the new board. 143 | boardsByCallback[callback.id] = boardId = request.id; 144 | 145 | // If this board has other viewers, notify them. 146 | if (boardId in callbacksByBoard) { 147 | callbacks = callbacksByBoard[boardId]; 148 | callbacks.push(callback); 149 | emit(callbacks, {type: "view", count: callbacks.length}); 150 | } else { 151 | callbacks = callbacksByBoard[boardId] = [callback]; 152 | } 153 | 154 | // Asynchronously load the requested board. 155 | boards.findOne({_id: boardId}, function(error, board) { 156 | if (board != null) { 157 | if (board.pieces) board.pieces.forEach(function(piece) { 158 | callback({type: "add", piece: piece}); 159 | }); 160 | } else { 161 | callback({type: "error", status: 404}); 162 | } 163 | }); 164 | } 165 | 166 | dispatch.close = function(callback) { 167 | var boardId = boardsByCallback[callback.id], 168 | callbacks; 169 | 170 | // If callback was viewing, remove it. 171 | if (boardId) { 172 | callbacks = callbacksByBoard[boardId]; 173 | callbacks.splice(callbacks.indexOf(callback), 1); 174 | if (callbacks.length) emit(callbacks, {type: "view", count: callbacks.length}); 175 | else delete callbacksByBoard[boardId]; 176 | delete boardsByCallback[callback.id]; 177 | } 178 | }; 179 | 180 | return dispatch; 181 | } 182 | 183 | function resolve(file) { 184 | return path.join(__dirname, "../client", file); 185 | } 186 | 187 | function emit(callbacks, event) { 188 | callbacks.forEach(function(callback) { 189 | callback(event); 190 | }); 191 | } 192 | -------------------------------------------------------------------------------- /test/metric-test.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | assert = require("assert"), 3 | test = require("./test"), 4 | event = require("../lib/cube/server/event"), 5 | metric = require("../lib/cube/server/metric"); 6 | 7 | var suite = vows.describe("metric"); 8 | 9 | var steps = { 10 | 3e5: function(date, n) { return new Date((Math.floor(date / 3e5) + n) * 3e5); }, 11 | 36e5: function(date, n) { return new Date((Math.floor(date / 36e5) + n) * 36e5); }, 12 | 864e5: function(date, n) { return new Date((Math.floor(date / 864e5) + n) * 864e5); }, 13 | 6048e5: function(date, n) { (date = steps[864e5](date, n * 7)).setUTCDate(date.getUTCDate() - date.getUTCDay()); return date; }, 14 | 2592e6: function(date, n) { return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + n, 1)); } 15 | }; 16 | 17 | steps[3e5].description = "5-minute"; 18 | steps[36e5].description = "1-hour"; 19 | steps[864e5].description = "1-day"; 20 | steps[6048e5].description = "1-week"; 21 | steps[2592e6].description = "1-month"; 22 | 23 | suite.addBatch(test.batch({ 24 | topic: function(test) { 25 | var emit = event.putter(test.db); 26 | for (var i = 0; i < 2500; i++) { 27 | emit({ 28 | type: "test", 29 | time: new Date(Date.UTC(2011, 6, 18, 0, Math.sqrt(i) - 10)).toISOString(), 30 | data: {i: i} 31 | }); 32 | } 33 | return metric.getter(test.db); 34 | }, 35 | 36 | "unary expression": metricTest({ 37 | expression: "sum(test)", 38 | start: "2011-07-17T23:47:00.000Z", 39 | stop: "2011-07-18T00:50:00.000Z", 40 | }, { 41 | 3e5: [0, 17, 65, 143, 175, 225, 275, 325, 375, 425, 475, 0, 0], 42 | 36e5: [82, 2418], 43 | 864e5: [82, 2418], 44 | 6048e5: [2500], 45 | 2592e6: [2500] 46 | } 47 | ), 48 | 49 | "unary expression with data accessor": metricTest({ 50 | expression: "sum(test(i))", 51 | start: "2011-07-17T23:47:00.000Z", 52 | stop: "2011-07-18T00:50:00.000Z" 53 | }, { 54 | 3e5: [0, 136, 3185, 21879, 54600, 115200, 209550, 345150, 529500, 770100, 1074450, 0, 0], 55 | 36e5: [3321, 3120429], 56 | 864e5: [3321, 3120429], 57 | 6048e5: [3123750], 58 | 2592e6: [3123750] 59 | } 60 | ), 61 | 62 | "unary expression with compound data accessor": metricTest({ 63 | expression: "sum(test(i / 100))", 64 | start: "2011-07-17T23:47:00.000Z", 65 | stop: "2011-07-18T00:50:00.000Z" 66 | }, { 67 | 3e5: [0, 1.36, 31.85, 218.79, 546, 1152, 2095.5, 3451.5, 5295, 7701, 10744.5, 0, 0], 68 | 36e5: [33.21, 31204.29], 69 | 864e5: [33.21, 31204.29], 70 | 6048e5: [31237.5], 71 | 2592e6: [31237.5] 72 | } 73 | ), 74 | 75 | "compound expression": metricTest({ 76 | expression: "max(test(i)) - min(test(i))", 77 | start: "2011-07-17T23:47:00.000Z", 78 | stop: "2011-07-18T00:50:00.000Z", 79 | }, { 80 | 3e5: [NaN, 16, 64, 142, 174, 224, 274, 324, 374, 424, 474, NaN, NaN], 81 | 36e5: [81, 2417], 82 | 864e5: [81, 2417], 83 | 6048e5: [2499], 84 | 2592e6: [2499] 85 | } 86 | ), 87 | 88 | "non-pyramidal expression": metricTest({ 89 | expression: "distinct(test(i))", 90 | start: "2011-07-17T23:47:00.000Z", 91 | stop: "2011-07-18T00:50:00.000Z", 92 | }, { 93 | 3e5: [0, 17, 65, 143, 175, 225, 275, 325, 375, 425, 475, 0, 0], 94 | 36e5: [82, 2418], 95 | 864e5: [82, 2418], 96 | 6048e5: [2500], 97 | 2592e6: [2500] 98 | } 99 | ), 100 | 101 | "compound pyramidal and non-pyramidal expression": metricTest({ 102 | expression: "sum(test(i)) - median(test(i))", 103 | start: "2011-07-17T23:47:00.000Z", 104 | stop: "2011-07-18T00:50:00.000Z", 105 | }, { 106 | 3e5: [NaN, 128, 3136, 21726, 54288, 114688, 208788, 344088, 528088, 768288, 1072188, NaN, NaN], 107 | 36e5: [3280.5, 3119138.5], 108 | 864e5: [3280.5, 3119138.5], 109 | 6048e5: [3122500.5], 110 | 2592e6: [3122500.5] 111 | } 112 | ), 113 | 114 | "compound with constant expression": metricTest({ 115 | expression: "-1 + sum(test)", 116 | start: "2011-07-17T23:47:00.000Z", 117 | stop: "2011-07-18T00:50:00.000Z", 118 | }, { 119 | 3e5: [-1, 16, 64, 142, 174, 224, 274, 324, 374, 424, 474, -1, -1], 120 | 36e5: [81, 2417], 121 | 864e5: [81, 2417], 122 | 6048e5: [2499], 123 | 2592e6: [2499] 124 | } 125 | ) 126 | })); 127 | 128 | suite.export(module); 129 | 130 | function metricTest(request, expected) { 131 | var t = {}, k; 132 | for (k in expected) t["at " + steps[k].description + " intervals"] = testStep(k, expected[k]); 133 | return t; 134 | 135 | function testStep(step, expected) { 136 | var t = testStepDepth(0, step, expected); 137 | t["(cached)"] = testStepDepth(1, step, expected); 138 | return t; 139 | } 140 | 141 | function testStepDepth(depth, step, expected) { 142 | var start = new Date(request.start), 143 | stop = new Date(request.stop); 144 | 145 | var test = { 146 | topic: function() { 147 | var actual = [], 148 | total = expected.length, 149 | timeout = setTimeout(function() { cb("Time's up!"); }, 10000), 150 | cb = this.callback, 151 | req = Object.create(request); 152 | req.step = step; 153 | arguments[depth](req, function(response) { 154 | if (actual.push(response) >= total) { 155 | clearTimeout(timeout); 156 | cb(null, actual.sort(function(a, b) { return a.time - b.time; })); 157 | } 158 | }); 159 | } 160 | }; 161 | 162 | test[request.expression] = function(actual) { 163 | 164 | // rounds down the start time (inclusive) 165 | var floor = steps[step](start, 0); 166 | assert.deepEqual(actual[0].time, floor); 167 | 168 | // rounds up the stop time (exclusive) 169 | var ceil = steps[step](stop, 0); 170 | if (!(ceil - stop)) ceil = steps[step](stop, -1); 171 | assert.deepEqual(actual[actual.length - 1].time, ceil); 172 | 173 | // formats UTC time in ISO 8601 174 | actual.forEach(function(d) { 175 | assert.instanceOf(d.time, Date); 176 | assert.match(JSON.stringify(d.time), /[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:00.000Z/); 177 | }); 178 | 179 | // returns exactly one value per time 180 | var i = 0, n = actual.length, t = actual[0].time; 181 | while (++i < n) assert.isTrue(t < (t = actual[i].time)); 182 | 183 | // each metric defines only time and value properties 184 | actual.forEach(function(d) { 185 | assert.deepEqual(Object.keys(d), ["time", "value"]); 186 | }); 187 | 188 | // returns the expected times 189 | var floor = steps[step], 190 | time = floor(start, 0), 191 | times = []; 192 | while (time < stop) { 193 | times.push(time); 194 | time = floor(time, 1); 195 | } 196 | assert.deepEqual(actual.map(function(d) { return d.time; }), times); 197 | 198 | // returns the expected values 199 | expected.forEach(function(value, i) { 200 | if (Math.abs(actual[i].value - value) > 1e-6) { 201 | assert.fail(actual.map(function(d) { return d.value; }), expected, "expected {expected}, got {actual} at " + actual[i].time.toISOString()); 202 | } 203 | }); 204 | 205 | }; 206 | 207 | return test; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /test/event-expression-test.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | assert = require("assert"), 3 | parser = require("../lib/cube/server/event-expression"); 4 | 5 | var suite = vows.describe("event-expression"); 6 | 7 | suite.addBatch({ 8 | 9 | "a simple event expression, test": { 10 | topic: parser.parse("test"), 11 | "has the expected event type, test": function(e) { 12 | assert.equal(e.type, "test"); 13 | }, 14 | "does not load any event data fields": function(e) { 15 | var fields = {}; 16 | e.fields(fields); 17 | assert.deepEqual(fields, {}); 18 | }, 19 | "does not apply any event data filters": function(e) { 20 | var filter = {}; 21 | e.filter(filter); 22 | assert.deepEqual(filter, {}); 23 | } 24 | }, 25 | 26 | "an expression with a single field": { 27 | topic: parser.parse("test(i)"), 28 | "loads the specified event data field": function(e) { 29 | var fields = {}; 30 | e.fields(fields); 31 | assert.deepEqual(fields, {"d.i": 1}); 32 | }, 33 | "ignores events that do not have the specified field": function(e) { 34 | var filter = {}; 35 | e.filter(filter); 36 | assert.deepEqual(filter, {"d.i": {$exists: true}}); 37 | } 38 | }, 39 | 40 | "an expression with multiple fields": { 41 | topic: parser.parse("test(i, j)"), 42 | "loads the specified event data fields": function(e) { 43 | var fields = {}; 44 | e.fields(fields); 45 | assert.deepEqual(fields, {"d.i": 1, "d.j": 1}); 46 | }, 47 | "ignores events that do not have the specified fields": function(e) { 48 | var filter = {}; 49 | e.filter(filter); 50 | assert.deepEqual(filter, {"d.i": {$exists: true}, "d.j": {$exists: true}}); 51 | } 52 | }, 53 | 54 | "an expression with a filter on the requested field": { 55 | topic: parser.parse("test(i).gt(i, 42)"), 56 | "loads the specified event data fields": function(e) { 57 | var fields = {}; 58 | e.fields(fields); 59 | assert.deepEqual(fields, {"d.i": 1}); 60 | }, 61 | "only filters using the explicit filter; existence is implied": function(e) { 62 | var filter = {}; 63 | e.filter(filter); 64 | assert.deepEqual(filter, {"d.i": {$gt: 42}}); 65 | } 66 | }, 67 | 68 | "an expression with filters on different fields": { 69 | topic: parser.parse("test.gt(i, 42).eq(j, \"foo\")"), 70 | "does not load fields that are only filtered": function(e) { 71 | var fields = {}; 72 | e.fields(fields); 73 | assert.deepEqual(fields, {}); 74 | }, 75 | "has the expected filters on each specified field": function(e) { 76 | var filter = {}; 77 | e.filter(filter); 78 | assert.deepEqual(filter, {"d.i": {$gt: 42}, "d.j": "foo"}); 79 | } 80 | }, 81 | 82 | "an expression with multiple filters on the same field": { 83 | topic: parser.parse("test.gt(i, 42).le(i, 52)"), 84 | "combines multiple filters on the specified field": function(e) { 85 | var filter = {}; 86 | e.filter(filter); 87 | assert.deepEqual(filter, {"d.i": {$gt: 42, $lte: 52}}); 88 | } 89 | }, 90 | 91 | "an expression with range and exact filters on the same field": { 92 | topic: parser.parse("test.gt(i, 42).eq(i, 52)"), 93 | "ignores range filters, taking only the exact filter": function(e) { 94 | var filter = {}; 95 | e.filter(filter); 96 | assert.deepEqual(filter, {"d.i": 52}); 97 | } 98 | }, 99 | 100 | "an expression with exact and range filters on the same field": { 101 | topic: parser.parse("test.eq(i, 52).gt(i, 42)"), 102 | "ignores range filters, taking only the exact filter": function(e) { 103 | var filter = {}; 104 | e.filter(filter); 105 | assert.deepEqual(filter, {"d.i": 52}); 106 | } 107 | }, 108 | 109 | "an expression with an object data accessor": { 110 | topic: parser.parse("test(i.j)"), 111 | "loads the specified event data field": function(e) { 112 | var fields = {}; 113 | e.fields(fields); 114 | assert.deepEqual(fields, {"d.i.j": 1}); 115 | }, 116 | "ignores events that do not have the specified field": function(e) { 117 | var filter = {}; 118 | e.filter(filter); 119 | assert.deepEqual(filter, {"d.i.j": {$exists: true}}); 120 | } 121 | }, 122 | 123 | "an expression with an array data accessor": { 124 | topic: parser.parse("test(i[0])"), 125 | "loads the specified event data field": function(e) { 126 | var fields = {}; 127 | e.fields(fields); 128 | assert.deepEqual(fields, {"d.i": 1}); 129 | }, 130 | "ignores events that do not have the specified field": function(e) { 131 | var filter = {}; 132 | e.filter(filter); 133 | assert.deepEqual(filter, {"d.i": {$exists: true}}); 134 | } 135 | }, 136 | 137 | "an expression with an elaborate data accessor": { 138 | topic: parser.parse("test(i.j[0].k)"), 139 | "loads the specified event data field": function(e) { 140 | var fields = {}; 141 | e.fields(fields); 142 | assert.deepEqual(fields, {"d.i.j.k": 1}); 143 | }, 144 | "ignores events that do not have the specified field": function(e) { 145 | var filter = {}; 146 | e.filter(filter); 147 | assert.deepEqual(filter, {"d.i.j.k": {$exists: true}}); 148 | } 149 | }, 150 | 151 | "an expression with an elaborate filter": { 152 | topic: parser.parse("test.gt(i.j[0].k, 42)"), 153 | "does not load fields that are only filtered": function(e) { 154 | var fields = {}; 155 | e.fields(fields); 156 | assert.deepEqual(fields, {}); 157 | }, 158 | "has the expected filter": function(e) { 159 | var filter = {}; 160 | e.filter(filter); 161 | assert.deepEqual(filter, {"d.i.j.0.k": {$gt: 42}}); 162 | } 163 | }, 164 | 165 | "filters": { 166 | "the eq filter results in a simple query filter": function() { 167 | var filter = {}; 168 | parser.parse("test.eq(i, 42)").filter(filter); 169 | assert.deepEqual(filter, {"d.i": 42}); 170 | }, 171 | "the gt filter results in a $gt query filter": function(e) { 172 | var filter = {}; 173 | parser.parse("test.gt(i, 42)").filter(filter); 174 | assert.deepEqual(filter, {"d.i": {$gt: 42}}); 175 | }, 176 | "the ge filter results in a $gte query filter": function(e) { 177 | var filter = {}; 178 | parser.parse("test.ge(i, 42)").filter(filter); 179 | assert.deepEqual(filter, {"d.i": {$gte: 42}}); 180 | }, 181 | "the lt filter results in an $lt query filter": function(e) { 182 | var filter = {}; 183 | parser.parse("test.lt(i, 42)").filter(filter); 184 | assert.deepEqual(filter, {"d.i": {$lt: 42}}); 185 | }, 186 | "the le filter results in an $lte query filter": function(e) { 187 | var filter = {}; 188 | parser.parse("test.le(i, 42)").filter(filter); 189 | assert.deepEqual(filter, {"d.i": {$lte: 42}}); 190 | }, 191 | "the ne filter results in an $ne query filter": function(e) { 192 | var filter = {}; 193 | parser.parse("test.ne(i, 42)").filter(filter); 194 | assert.deepEqual(filter, {"d.i": {$ne: 42}}); 195 | }, 196 | "the re filter results in a $regex query filter": function(e) { 197 | var filter = {}; 198 | parser.parse("test.re(i, \"foo\")").filter(filter); 199 | assert.deepEqual(filter, {"d.i": {$regex: "foo"}}); 200 | }, 201 | "the in filter results in a $in query filter": function(e) { 202 | var filter = {}; 203 | parser.parse("test.in(i, [\"foo\", 42])").filter(filter); 204 | assert.deepEqual(filter, {"d.i": {$in: ["foo", 42]}}); 205 | } 206 | } 207 | 208 | }); 209 | 210 | suite.export(module); 211 | -------------------------------------------------------------------------------- /lib/cube/client/piece-area.js: -------------------------------------------------------------------------------- 1 | cube.piece.type.area = function(board) { 2 | var timeout, 3 | data = [], 4 | dt0; 5 | 6 | var area = cube.piece(board) 7 | .on("size", resize) 8 | .on("serialize", serialize) 9 | .on("deserialize", deserialize); 10 | 11 | var div = d3.select(area.node()) 12 | .classed("area", true); 13 | 14 | if (mode == "edit") { 15 | div.append("h3") 16 | .attr("class", "title") 17 | .text("Area Chart"); 18 | 19 | var query = div.append("textarea") 20 | .attr("class", "query") 21 | .attr("placeholder", "query expression…") 22 | .on("keyup.area", querychange) 23 | .on("focus.area", area.focus) 24 | .on("blur.area", area.blur); 25 | 26 | var time = div.append("div") 27 | .attr("class", "time") 28 | .text("Time Range:"); 29 | 30 | time.append("input"); 31 | 32 | time.append("select").selectAll("option") 33 | .data([ 34 | {description: "Seconds @ 20", value: 2e4}, 35 | {description: "Minutes @ 5", value: 3e5}, 36 | {description: "Hours", value: 36e5}, 37 | {description: "Days", value: 864e5}, 38 | {description: "Weeks", value: 6048e5}, 39 | {description: "Months", value: 2592e6} 40 | ]) 41 | .enter().append("option") 42 | .property("selected", function(d, i) { return i == 1; }) 43 | .attr("value", cube_piece_areaValue) 44 | .text(function(d) { return d.description; }); 45 | 46 | time.selectAll("input,select") 47 | .on("change.area", area.edit) 48 | .on("focus.area", area.focus) 49 | .on("blur.area", area.blur) 50 | } else { 51 | var m = [6, 40, 14, 10], // top, right, bottom, left margins 52 | socket; 53 | 54 | var svg = div.append("svg:svg"); 55 | 56 | var x = d3.time.scale(), 57 | y = d3.scale.linear(), 58 | xAxis = d3.svg.axis().scale(x).orient("bottom").tickSubdivide(true), 59 | yAxis = d3.svg.axis().scale(y).orient("right"); 60 | 61 | var a = d3.svg.area() 62 | .interpolate("step-after") 63 | .x(function(d) { return x(d.time); }) 64 | .y0(function(d) { return y(0); }) 65 | .y1(function(d) { return y(d.value); }); 66 | 67 | var l = d3.svg.line() 68 | .interpolate("step-after") 69 | .x(function(d) { return x(d.time); }) 70 | .y(function(d) { return y(d.value); }); 71 | 72 | var g = svg.append("svg:g") 73 | .attr("transform", "translate(" + m[3] + "," + m[0] + ")"); 74 | 75 | g.append("svg:g").attr("class", "y axis").call(yAxis); 76 | g.append("svg:path").attr("class", "area"); 77 | g.append("svg:g").attr("class", "x axis").call(xAxis); 78 | g.append("svg:path").attr("class", "line"); 79 | } 80 | 81 | function resize() { 82 | var transition = area.transition(); 83 | 84 | if (mode == "edit") { 85 | var innerSize = area.innerSize(); 86 | 87 | transition.select(".query") 88 | .style("width", innerSize[0] - 12 + "px") 89 | .style("height", innerSize[1] - 58 + "px"); 90 | 91 | transition.select(".time select") 92 | .style("width", innerSize[0] - 174 + "px"); 93 | 94 | } else { 95 | var z = board.squareSize(), 96 | w = area.size()[0] * z - m[1] - m[3], 97 | h = area.size()[1] * z - m[0] - m[2]; 98 | 99 | x.range([0, w]); 100 | y.range([h, 0]); 101 | 102 | // Adjust the ticks based on the current chart dimensions. 103 | xAxis.ticks(w / 80).tickSize(-h, 0); 104 | yAxis.ticks(h / 25).tickSize(-w, 0); 105 | 106 | transition.select("svg") 107 | .attr("width", w + m[1] + m[3]) 108 | .attr("height", h + m[0] + m[2]); 109 | 110 | transition.select(".area") 111 | .attr("d", a(data)); 112 | 113 | transition.select(".x.axis") 114 | .attr("transform", "translate(0," + h + ")") 115 | .call(xAxis) 116 | .select("path") 117 | .attr("transform", "translate(0," + (y(0) - h) + ")"); 118 | 119 | transition.select(".y.axis") 120 | .attr("transform", "translate(" + w + ",0)") 121 | .call(yAxis); 122 | 123 | transition.select(".line") 124 | .attr("d", l(data)); 125 | } 126 | } 127 | 128 | function redraw() { 129 | if (data.length > 1) data[data.length - 1].value = data[data.length - 2].value; 130 | 131 | var z = board.squareSize(), 132 | h = area.size()[1] * z - m[0] - m[2], 133 | min = d3.min(data, cube_piece_areaValue), 134 | max = d3.max(data, cube_piece_areaValue); 135 | 136 | if ((min < 0) && (max < 0)) max = 0; 137 | else if ((min > 0) && (max > 0)) min = 0; 138 | y.domain([min, max]).nice(); 139 | 140 | div.select(".area").attr("d", a(data)); 141 | div.select(".y.axis").call(yAxis.tickFormat(cube_piece_format(y.domain()))); 142 | div.select(".x.axis").call(xAxis).select("path").attr("transform", "translate(0," + (y(0) - h) + ")"); 143 | div.select(".line").attr("d", l(data)); 144 | return true; 145 | } 146 | 147 | function querychange() { 148 | if (timeout) clearTimeout(timeout); 149 | timeout = setTimeout(area.edit, 750); 150 | } 151 | 152 | function serialize(json) { 153 | var step = +time.select("select").property("value"), 154 | range = time.select("input").property("value") * cube_piece_areaMultipler(step); 155 | json.type = "area"; 156 | json.query = query.property("value"); 157 | json.time = {range: range, step: step}; 158 | } 159 | 160 | function deserialize(json) { 161 | if (mode == "edit") { 162 | query.property("value", json.query); 163 | time.select("input").property("value", json.time.range / cube_piece_areaMultipler(json.time.step)); 164 | time.select("select").property("value", json.time.step); 165 | } else { 166 | var dt1 = json.time.step, 167 | t1 = new Date(Math.floor(Date.now() / dt1) * dt1), 168 | t0 = new Date(t1 - json.time.range), 169 | d0 = x.domain(), 170 | d1 = [t0, t1]; 171 | 172 | if (dt0 != dt1) { 173 | data = []; 174 | dt0 = dt1; 175 | } 176 | 177 | if (d0 != d1 + "") { 178 | x.domain(d1); 179 | resize(); 180 | var times = data.map(cube_piece_areaTime); 181 | data = data.slice(d3.bisectLeft(times, t0), d3.bisectLeft(times, t1)); 182 | data.push({time: t1, value: 0}); 183 | } 184 | 185 | if (timeout) timeout = clearTimeout(timeout); 186 | if (socket) socket.close(); 187 | socket = new WebSocket("ws://" + location.host + "/1.0/metric/get"); 188 | socket.onopen = load; 189 | socket.onmessage = store; 190 | 191 | function load() { 192 | timeout = setTimeout(function() { 193 | socket.send(JSON.stringify({ 194 | expression: json.query, 195 | start: cube_time(t0), 196 | stop: cube_time(t1), 197 | step: dt1 198 | })); 199 | timeout = setTimeout(function() { 200 | deserialize(json); 201 | }, t1 - Date.now() + dt1 + 4500 + 1000 * Math.random()); 202 | }, 500); 203 | } 204 | 205 | // TODO use a skip list to insert more efficiently 206 | // TODO compute contiguous segments on the fly 207 | function store(message) { 208 | var d = JSON.parse(message.data), 209 | i = d3.bisectLeft(data.map(cube_piece_areaTime), d.time = cube_time.parse(d.time)); 210 | if (i < 0 || data[i].time - d.time) { 211 | if (d.value != null) { 212 | data.splice(i, 0, d); 213 | } 214 | } else if (d.value == null) { 215 | data.splice(i, 1); 216 | } else { 217 | data[i] = d; 218 | } 219 | d3.timer(redraw); 220 | } 221 | } 222 | } 223 | 224 | area.copy = function() { 225 | return board.add(cube.piece.type.area); 226 | }; 227 | 228 | resize(); 229 | 230 | return area; 231 | }; 232 | 233 | function cube_piece_areaTime(d) { 234 | return d.time; 235 | } 236 | 237 | function cube_piece_areaValue(d) { 238 | return d.value; 239 | } 240 | 241 | var cube_piece_formatNumber = d3.format(".2r"); 242 | 243 | function cube_piece_areaMultipler(step) { 244 | return step / (step === 2e4 ? 20 245 | : step === 3e5 ? 5 246 | : 1); 247 | } 248 | 249 | function cube_piece_format(domain) { 250 | var prefix = d3.formatPrefix(Math.max(-domain[0], domain[1]), 2); 251 | return function(value) { 252 | return cube_piece_formatNumber(value * prefix.scale) + prefix.symbol; 253 | }; 254 | } 255 | -------------------------------------------------------------------------------- /lib/cube/client/piece.js: -------------------------------------------------------------------------------- 1 | cube.piece = function(board) { 2 | var piece = {}, 3 | size = [8, 3], 4 | position = [0, 0], 5 | padding = 4; 6 | 7 | var event = d3.dispatch( 8 | "position", 9 | "size", 10 | "move", 11 | "edit", 12 | "serialize", 13 | "deserialize" 14 | ); 15 | 16 | var div = document.createElement("div"), 17 | selection = d3.select(div), 18 | transition = selection; 19 | 20 | var selection = d3.select(div) 21 | .attr("class", "piece"); 22 | 23 | if (mode == "edit") { 24 | selection 25 | .attr("tabindex", 1) 26 | .on("keydown.piece", keydown) 27 | .on("mousedown.piece", mousedrag) 28 | .selectAll(".resize") 29 | .data(["n", "e", "s", "w", "nw", "ne", "se", "sw"]) 30 | .enter().append("div") 31 | .attr("class", function(d) { return "resize " + d; }) 32 | .on("mousedown.piece", mouseresize); 33 | 34 | d3.select(window) 35 | .on("keydown.piece", cube_piece_keydown) 36 | .on("mousemove.piece", cube_piece_mousemove) 37 | .on("mouseup.piece", cube_piece_mouseup); 38 | } 39 | 40 | board 41 | .on("padding.piece", resize) 42 | .on("squareSize.piece", resize); 43 | 44 | event.on("position.piece", resize); 45 | event.on("size.piece", resize); 46 | event.on("deserialize.piece", deserialize); 47 | 48 | function resize() { 49 | var squareSize = board.squareSize(), 50 | boardPadding = board.padding() | 0; 51 | 52 | piece.transition() 53 | .style("left", (padding + boardPadding + position[0] * squareSize) + "px") 54 | .style("top", (padding + boardPadding + (1.5 + position[1]) * squareSize) + "px") 55 | .style("width", size[0] * squareSize - 2 * padding + 1 + "px") 56 | .style("height", size[1] * squareSize - 2 * padding + 1 + "px"); 57 | } 58 | 59 | function deserialize(x) { 60 | piece 61 | .size(x.size) 62 | .position(x.position); 63 | } 64 | 65 | function keydown() { 66 | if (d3.event.target !== this) return d3.event.stopPropagation(); 67 | if (cube_piece_dragPiece) return; 68 | if (d3.event.keyCode === 8) { 69 | board.remove(piece); 70 | d3.event.preventDefault(); 71 | } 72 | } 73 | 74 | piece.node = function() { 75 | return div; 76 | }; 77 | 78 | piece.on = function(type, listener) { 79 | event.on(type, listener); 80 | return piece; 81 | }; 82 | 83 | piece.size = function(x) { 84 | if (!arguments.length) return size; 85 | event.size.call(piece, size = x); 86 | return piece; 87 | }; 88 | 89 | piece.innerSize = function() { 90 | var squareSize = board.squareSize(); 91 | return [ 92 | size[0] * squareSize - 2 * padding, 93 | size[1] * squareSize - 2 * padding 94 | ]; 95 | }; 96 | 97 | piece.position = function(x) { 98 | if (!arguments.length) return position; 99 | event.position.call(piece, position = x); 100 | return piece; 101 | }; 102 | 103 | piece.toJSON = function() { 104 | var x = {id: piece.id, size: size, position: position}; 105 | event.serialize.call(piece, x); 106 | return x; 107 | }; 108 | 109 | piece.fromJSON = function(x) { 110 | event.deserialize.call(piece, x); 111 | return piece; 112 | }; 113 | 114 | piece.edit = function() { 115 | event.edit.call(piece); 116 | return piece; 117 | }; 118 | 119 | function mousedrag() { 120 | if (/^(INPUT|TEXTAREA|SELECT)$/.test(d3.event.target.tagName)) return; 121 | if (d3.event.target === this && d3.event.altKey) { 122 | cube_piece_dragPiece = piece.copy().fromJSON(piece.toJSON()); 123 | cube_piece_dragPiece.node().focus(); 124 | } else { 125 | d3.select(this).transition(); // cancel transition, if any 126 | this.parentNode.appendChild(this).focus(); 127 | cube_piece_dragPiece = piece; 128 | } 129 | cube_piece_dragOrigin = [d3.event.pageX, d3.event.pageY]; 130 | cube_piece_dragPosition = position.slice(); 131 | cube_piece_dragSize = size.slice(); 132 | cube_piece_dragBoard = board; 133 | cube_piece_mousemove(); 134 | } 135 | 136 | function mouseresize(d) { 137 | cube_piece_dragResize = d; 138 | } 139 | 140 | piece.transition = function(x) { 141 | if (!arguments.length) return transition; 142 | if (x == null) { 143 | transition = selection; 144 | } else { 145 | transition = x.select(function() { return div; }); 146 | d3.timer(function() { 147 | event.move.call(piece); 148 | return transition = selection; 149 | }); 150 | } 151 | return piece; 152 | }; 153 | 154 | piece.focus = function() { 155 | selection.classed("active", true); 156 | return piece; 157 | }; 158 | 159 | piece.blur = function() { 160 | selection.classed("active", false); 161 | return piece; 162 | }; 163 | 164 | resize(); 165 | 166 | return piece; 167 | }; 168 | 169 | cube.piece.type = {}; 170 | 171 | var cube_piece_dragPiece, 172 | cube_piece_dragBoard, 173 | cube_piece_dragOrigin, 174 | cube_piece_dragPosition, 175 | cube_piece_dragSize, 176 | cube_piece_dragResize; 177 | 178 | function cube_piece_mousePosition() { 179 | var squareSize = cube_piece_dragBoard.squareSize(); 180 | return cube_piece_dragResize ? [ 181 | cube_piece_dragPosition[0] + /w$/.test(cube_piece_dragResize) * Math.min(cube_piece_dragSize[0] - 5, (d3.event.pageX - cube_piece_dragOrigin[0]) / squareSize), 182 | cube_piece_dragPosition[1] + /^n/.test(cube_piece_dragResize) * Math.min(cube_piece_dragSize[1] - 3, (d3.event.pageY - cube_piece_dragOrigin[1]) / squareSize) 183 | ] : [ 184 | cube_piece_dragPosition[0] + (d3.event.pageX - cube_piece_dragOrigin[0]) / squareSize, 185 | cube_piece_dragPosition[1] + (d3.event.pageY - cube_piece_dragOrigin[1]) / squareSize 186 | ]; 187 | } 188 | 189 | function cube_piece_mouseSize() { 190 | var squareSize = cube_piece_dragBoard.squareSize(); 191 | return cube_piece_dragResize ? [ 192 | Math.max(5, cube_piece_dragSize[0] + (/e$/.test(cube_piece_dragResize) ? 1 : /w$/.test(cube_piece_dragResize) ? -1 : 0) * (d3.event.pageX - cube_piece_dragOrigin[0]) / squareSize), 193 | Math.max(3, cube_piece_dragSize[1] + (/^s/.test(cube_piece_dragResize) ? 1 : /^n/.test(cube_piece_dragResize) ? -1 : 0) * (d3.event.pageY - cube_piece_dragOrigin[1]) / squareSize) 194 | ] : cube_piece_dragSize; 195 | } 196 | 197 | function cube_piece_clamp(position, size) { 198 | var boardSize = cube_piece_dragBoard.size(); 199 | if (cube_piece_dragResize) { 200 | if (/e$/.test(cube_piece_dragResize)) { 201 | size[0] = Math.max(0, Math.min(boardSize[0] - position[0], Math.round(size[0]))); 202 | } else if (/w$/.test(cube_piece_dragResize)) { 203 | size[0] = Math.round(size[0] + position[0] - (position[0] = Math.max(0, Math.min(boardSize[0], Math.round(position[0]))))); 204 | } 205 | if (/^s/.test(cube_piece_dragResize)) { 206 | size[1] = Math.max(0, Math.min(boardSize[1] - position[1], Math.round(size[1]))); 207 | } else if (/^n/.test(cube_piece_dragResize)) { 208 | size[1] = Math.round(size[1] + position[1] - (position[1] = Math.max(0, Math.min(boardSize[1], Math.round(position[1]))))); 209 | } 210 | } else { 211 | position[0] = Math.max(0, Math.min(boardSize[0] - size[0], Math.round(position[0]))); 212 | position[1] = Math.max(0, Math.min(boardSize[1] - size[1], Math.round(position[1]))); 213 | } 214 | } 215 | 216 | function cube_piece_mousemove() { 217 | if (cube_piece_dragPiece) { 218 | var position = cube_piece_mousePosition(), 219 | size = cube_piece_mouseSize(); 220 | 221 | cube_piece_dragPiece 222 | .position(position.slice()) 223 | .size(size.slice()); 224 | 225 | cube_piece_clamp(position, size); 226 | 227 | var x0 = position[0], 228 | y0 = position[1], 229 | x1 = x0 + size[0], 230 | y1 = y0 + size[1]; 231 | 232 | d3.select(cube_piece_dragBoard.node()).selectAll(".squares rect") 233 | .classed("shadow", function(d, i) { return d.x >= x0 && d.x < x1 && d.y >= y0 && d.y < y1; }); 234 | 235 | d3.event.preventDefault(); 236 | } 237 | } 238 | 239 | function cube_piece_mouseup() { 240 | if (cube_piece_dragPiece) { 241 | var position = cube_piece_mousePosition(), 242 | size = cube_piece_mouseSize(); 243 | 244 | cube_piece_clamp(position, size); 245 | 246 | d3.select(cube_piece_dragBoard.node()).selectAll(".squares rect") 247 | .classed("shadow", false); 248 | 249 | cube_piece_dragPiece 250 | .transition(d3.transition().ease("elastic").duration(500)) 251 | .position(position) 252 | .size(size); 253 | 254 | cube_piece_dragPiece = 255 | cube_piece_dragBoard = 256 | cube_piece_dragEvent = 257 | cube_piece_dragOrigin = 258 | cube_piece_dragPosition = 259 | cube_piece_dragSize = 260 | cube_piece_dragResize = null; 261 | d3.event.preventDefault(); 262 | } 263 | } 264 | 265 | // Disable delete as the back key, since we use it to delete pieces. 266 | function cube_piece_keydown() { 267 | if (d3.event.keyCode == 8) { 268 | d3.event.preventDefault(); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /lib/cube/server/metric-expression.peg: -------------------------------------------------------------------------------- 1 | // TODO allow null literals (requires fixing pegjs) 2 | // TODO allow multiple group dimensions (requires compound group key) 3 | 4 | { 5 | var filterEqual = function(o, k, v) { o[k] = v; }, 6 | filterGreater = filter("$gt"), 7 | filterGreaterOrEqual = filter("$gte"), 8 | filterLess = filter("$lt"), 9 | filterLessOrEqual = filter("$lte"), 10 | filterNotEqual = filter("$ne"), 11 | filterRegularExpression = filter("$regex"), 12 | filterIn = filter("$in"), 13 | exists = {$exists: true}; 14 | 15 | function add(a, b) { return a + b; } 16 | function subtract(a, b) { return a - b; } 17 | function multiply(a, b) { return a * b; } 18 | function divide(a, b) { return a / b; } 19 | 20 | function one() { return 1; } 21 | function noop() {} 22 | 23 | function filter(op) { 24 | return function(o, k, v) { 25 | var f = o[k]; 26 | switch (typeof f) { 27 | case "undefined": o[k] = f = {}; // continue 28 | case "object": f[op] = v; break; 29 | // otherwise, observe the existing equals (literal) filter 30 | } 31 | }; 32 | } 33 | 34 | function arrayAccessor(name) { 35 | name = new String(name); 36 | name.array = true; 37 | return name; 38 | } 39 | 40 | function objectAccessor(name) { 41 | return name; 42 | } 43 | 44 | function compoundMetric(head, tail) { 45 | var i = -1, 46 | n = tail.length, 47 | t, 48 | e = head; 49 | while (++i < n) { 50 | t = tail[i]; 51 | e = {left: e, op: t[1], right: t[3]}; 52 | if (!i) head = e; 53 | } 54 | return head; 55 | } 56 | 57 | function compoundValue(head, tail) { 58 | var n = tail.length; 59 | return { 60 | exists: function(o) { 61 | var i = -1; 62 | head.exists(o); 63 | while (++i < n) tail[i][3].exists(o); 64 | }, 65 | fields: function(o) { 66 | var i = -1; 67 | head.fields(o); 68 | while (++i < n) tail[i][3].fields(o); 69 | }, 70 | value: function(o) { 71 | var v = head.value(o), 72 | i = -1, 73 | t; 74 | while (++i < n) v = (t = tail[i])[1](v, t[3].value(o)); 75 | return v; 76 | } 77 | }; 78 | } 79 | 80 | function member(head, tail) { 81 | var fields = ["d", head].concat(tail), 82 | shortName = fields.filter(function(d) { return !d.array; }).join("."), 83 | longName = fields.join("."), 84 | i = -1, 85 | n = fields.length; 86 | return { 87 | field: longName, 88 | exists: function(o) { 89 | if (!(shortName in o)) { 90 | o[shortName] = exists; 91 | } 92 | }, 93 | fields: function(o) { 94 | o[shortName] = 1; 95 | }, 96 | value: function(o) { 97 | var i = -1; 98 | while (++i < n) { 99 | o = o[fields[i]]; 100 | } 101 | return o; 102 | } 103 | }; 104 | } 105 | } 106 | 107 | start 108 | = _ expression:metric_additive_expression _ { return expression; } 109 | 110 | metric_additive_expression 111 | = head:metric_multiplicative_expression tail:(_ additive_operator _ metric_additive_expression)* { return compoundMetric(head, tail); } 112 | 113 | metric_multiplicative_expression 114 | = head:metric_unary_expression tail:(_ multiplicative_operator _ metric_multiplicative_expression)* { return compoundMetric(head, tail); } 115 | 116 | metric_unary_expression 117 | = "-" _ expression:metric_unary_expression { var value = expression.value; expression.value = function(o) { return -value(o); }; return expression; } 118 | / metric_primary_expression 119 | 120 | metric_primary_expression 121 | = reduce:identifier _ "(" _ event:event_expression _ ")" { event.reduce = reduce; event.source = input.substring(savedPos1, pos); return event; } 122 | / value:number { return {value: function() { return value; }}; } 123 | / "(" _ expression:metric_additive_expression _ ")" { return expression; } 124 | 125 | event_expression 126 | = value:event_value_expression filters:(_ "." _ event_filter_expression)* group:(_ "." _ event_group_expression)? 127 | { 128 | value.filter = function(filter) { 129 | var i = -1, n = filters.length; 130 | while (++i < n) filters[i][3](filter); 131 | value.exists(filter); 132 | }; 133 | if (group) value.group = group[3]; 134 | return value; 135 | } 136 | 137 | event_group_expression 138 | = "group" _ "(" _ member:event_member_expression _ ")" { return member; } 139 | / "groups" _ "(" _ member:event_member_expression _ ")" { member.multi = true; return member; } 140 | 141 | event_filter_expression 142 | = op:filter_operator _ "(" _ member:event_member_expression _ "," _ value:literal _ ")" { return function(o) { op(o, member.field, value); }; } 143 | 144 | event_value_expression 145 | = type:identifier _ "(" _ value:event_additive_expression _ ")" { value.type = type; return value; } 146 | / type:identifier { return {type: type, value: one, exists: noop, fields: noop}; } 147 | 148 | event_additive_expression 149 | = head:event_multiplicative_expression tail:(_ additive_operator _ event_additive_expression)* { return compoundValue(head, tail); } 150 | 151 | event_multiplicative_expression 152 | = head:event_unary_expression tail:(_ multiplicative_operator _ event_multiplicative_expression)* { return compoundValue(head, tail); } 153 | 154 | event_unary_expression 155 | = event_primary_expression 156 | / "-" _ unary:event_unary_expression { return {value: function(o) { return -unary.value(o); }, exists: unary.exists, fields: unary.fields}; } 157 | 158 | event_primary_expression 159 | = event_member_expression 160 | / number:number { return {value: function() { return number; }, exists: noop, fields: noop}; } 161 | / "(" _ expression:event_additive_expression _ ")" { return expression; } 162 | 163 | event_member_expression 164 | = head:identifier tail:( 165 | _ "[" _ name:number _ "]" { return arrayAccessor(name); } 166 | / _ "." _ name:identifier { return objectAccessor(name); } 167 | )* { return member(head, tail); } 168 | 169 | additive_operator 170 | = "+" { return add; } 171 | / "-" { return subtract; } 172 | 173 | multiplicative_operator 174 | = "*" { return multiply; } 175 | / "/" { return divide; } 176 | 177 | filter_operator 178 | = "eq" { return filterEqual; } 179 | / "gt" { return filterGreater; } 180 | / "ge" { return filterGreaterOrEqual; } 181 | / "lt" { return filterLess; } 182 | / "le" { return filterLessOrEqual; } 183 | / "ne" { return filterNotEqual; } 184 | / "re" { return filterRegularExpression; } 185 | / "in" { return filterIn; } 186 | 187 | identifier 188 | = first:[a-zA-Z_$] rest:[a-zA-Z0-9_$]* { return first + rest.join(""); } 189 | 190 | literal 191 | = array_literal 192 | / string 193 | / number 194 | / "true" { return true; } 195 | / "false" { return false; } 196 | 197 | array_literal 198 | = "[" _ first:literal rest:(_ "," _ literal)* _ "]" { return [first].concat(rest.map(function(d) { return d[3]; })); } 199 | / "[" _ "]" { return []; } 200 | 201 | string "string" 202 | = '"' chars:double_string_char* '"' { return chars.join(""); } 203 | / "'" chars:single_string_char* "'" { return chars.join(""); } 204 | 205 | double_string_char 206 | = !('"' / "\\") char_:. { return char_; } 207 | / "\\" sequence:escape_sequence { return sequence; } 208 | 209 | single_string_char 210 | = !("'" / "\\") char_:. { return char_; } 211 | / "\\" sequence:escape_sequence { return sequence; } 212 | 213 | escape_sequence 214 | = character_escape_sequence 215 | / "0" !digit { return "\0"; } 216 | / hex_escape_sequence 217 | / unicode_escape_sequence 218 | 219 | character_escape_sequence 220 | = single_escape_character 221 | / non_escape_character 222 | 223 | single_escape_character 224 | = char_:['"\\bfnrtv] { return char_.replace("b", "\b").replace("f", "\f").replace("n", "\n").replace("r", "\r").replace("t", "\t").replace("v", "\x0B"); } 225 | 226 | non_escape_character 227 | = !escape_character char_:. { return char_; } 228 | 229 | escape_character 230 | = single_escape_character 231 | / digit 232 | / "x" 233 | / "u" 234 | 235 | hex_escape_sequence 236 | = "x" h1:hex_digit h2:hex_digit { return String.fromCharCode(+("0x" + h1 + h2)); } 237 | 238 | unicode_escape_sequence 239 | = "u" h1:hex_digit h2:hex_digit h3:hex_digit h4:hex_digit { return String.fromCharCode(+("0x" + h1 + h2 + h3 + h4)); } 240 | 241 | number "number" 242 | = "-" _ number:number { return -number; } 243 | / int_:int frac:frac exp:exp { return +(int_ + frac + exp); } 244 | / int_:int frac:frac { return +(int_ + frac); } 245 | / int_:int exp:exp { return +(int_ + exp); } 246 | / frac:frac { return +frac; } 247 | / int_:int { return +int_; } 248 | 249 | int 250 | = digit19:digit19 digits:digits { return digit19 + digits; } 251 | / digit:digit 252 | 253 | frac 254 | = "." digits:digits { return "." + digits; } 255 | 256 | exp 257 | = e:e digits:digits { return e + digits; } 258 | 259 | digits 260 | = digits:digit+ { return digits.join(""); } 261 | 262 | e 263 | = e:[eE] sign:[+-]? { return e + sign; } 264 | 265 | digit 266 | = [0-9] 267 | 268 | digit19 269 | = [1-9] 270 | 271 | hex_digit 272 | = [0-9a-fA-F] 273 | 274 | _ "whitespace" 275 | = whitespace* 276 | 277 | whitespace 278 | = [ \t\n\r] 279 | -------------------------------------------------------------------------------- /lib/cube/server/metric.js: -------------------------------------------------------------------------------- 1 | var util = require("util"), 2 | parser = require("./metric-expression"), 3 | tiers = require("./tiers"), 4 | types = require("./types"), 5 | reduces = require("./reduces"); 6 | 7 | var metric_fields = {t: 1, g: 1, v: 1}, 8 | metric_options = {sort: {t: 1}, batchSize: 1000}, 9 | event_options = {sort: {t: 1}, batchSize: 1000}, 10 | update_options = {upsert: true}; 11 | 12 | // Query for metrics. 13 | // TODO use expression ids, and record how often expressions are queried? 14 | exports.getter = function(db) { 15 | var collection = types(db), 16 | Double = db.bson_serializer.Double; 17 | 18 | // Computes the metric for the given expression for the time interval from 19 | // start (inclusive) to stop (exclusive). The time granularity is determined 20 | // by the specified tier, such as daily or hourly. The callback is invoked 21 | // repeatedly for each metric value, being passed two arguments: the time and 22 | // the value. The values may be out of order due to partial cache hits. 23 | function measure(expression, start, stop, tier, callback) { 24 | (expression.op ? binary : expression.type ? unary : constant)(expression, start, stop, tier, callback); 25 | } 26 | 27 | // Computes a constant expression; 28 | function constant(expression, start, stop, tier, callback) { 29 | var value = expression.value(); 30 | while (start < stop) { 31 | callback(start, value); 32 | start = tier.step(start); 33 | } 34 | } 35 | 36 | // Computes a unary (primary) expression. 37 | function unary(expression, start, stop, tier, callback) { 38 | var type = collection(expression.type), 39 | map = expression.value, 40 | group = expression.group, 41 | reduce = reduces[expression.reduce], 42 | filter = {t: {}}, 43 | fields = {t: 1}; 44 | 45 | if (!reduce) return util.log("no such reduce: " + expression.reduce); 46 | 47 | // Prepare for grouping. 48 | if (group) { 49 | delete fields.t; 50 | var group_options = {sort: {}}; 51 | group.fields(group_options.sort); 52 | group.fields(fields); 53 | } 54 | 55 | // Copy any expression filters into the query object. 56 | expression.filter(filter); 57 | 58 | // Request any needed fields. 59 | expression.fields(fields); 60 | 61 | find(start, stop, tier, reduce.pyramidal && tier.next, callback); 62 | 63 | // The metric is computed recursively, reusing the above variables. 64 | function find(start, stop, tier, pyramidal, callback) { 65 | var compute = group ? (group.multi ? computeGroups : computeGroup) 66 | : pyramidal ? computePyramidal : computeFlat; 67 | 68 | // Query for the desired metric in the cache. 69 | type.metrics.find({ 70 | i: false, 71 | e: expression.source, 72 | l: tier.key, 73 | t: { 74 | $gte: start, 75 | $lt: stop 76 | } 77 | }, metric_fields, metric_options, foundMetrics); 78 | 79 | // Immediately report back whatever we have. If any values are missing, 80 | // merge them into contiguous intervals and asynchronously compute them. 81 | function foundMetrics(error, cursor) { 82 | if (error) throw error; 83 | var time = start; 84 | cursor.each(function(error, row) { 85 | if (error) throw error; 86 | if (row) { 87 | callback(row.t, row.v, row.g); 88 | if (time < row.t) compute(time, row.t); 89 | time = tier.step(row.t); 90 | } else { 91 | if (time < stop) compute(time, stop); 92 | } 93 | }); 94 | } 95 | 96 | // Process each bin separately, sorting by group. 97 | function computeGroup(start, stop) { 98 | var next = tier.step(start); 99 | filter.t.$gte = start; 100 | filter.t.$lt = next; 101 | type.events.find(filter, fields, group_options, function(error, cursor) { 102 | if (error) throw error; 103 | var k0, values; 104 | cursor.nextObject(function(error, row) { 105 | if (error) throw error; 106 | if (!row) return; 107 | k0 = group.value(row); 108 | values = [map(row)]; 109 | cursor.each(function(error, row) { 110 | if (error) throw error; 111 | if (row) { 112 | var k1 = group.value(row); 113 | if (k0 != k1) { 114 | saveGroup(start, values.length ? reduce(values) : reduce.empty, k0); 115 | k0 = k1; 116 | values = [map(row)]; 117 | } else { 118 | values.push(map(row)); 119 | } 120 | } else { 121 | saveGroup(start, values.length ? reduce(values) : reduce.empty, k0); 122 | if (next < stop) computeGroup(next, stop); 123 | } 124 | }); 125 | }); 126 | }); 127 | } 128 | 129 | // Process each bin separately, loading it entirely into memory. 130 | function computeGroups(start, stop) { 131 | var next = tier.step(start); 132 | filter.t.$gte = start; 133 | filter.t.$lt = next; 134 | type.events.find(filter, fields, event_options, function(error, cursor) { 135 | if (error) throw error; 136 | var groups = {}; 137 | cursor.each(function(error, row) { 138 | if (error) throw error; 139 | 140 | if (!row) { 141 | for (var key in groups) saveGroup(start, reduce(groups[key]), key); 142 | return; 143 | } 144 | 145 | var keys = group.value(row), value = map(row); 146 | if (keys && keys.forEach) { 147 | var i = -1, n = keys.length; 148 | while (++i < n) storeGroup(keys[i], value); 149 | } else if (keys != null) { 150 | storeGroup(keys, value); 151 | } 152 | 153 | if (next < stop) computeGroup(next, stop); 154 | }); 155 | 156 | function storeGroup(key, value) { 157 | var values = groups[key]; 158 | if (values) values.push(value); 159 | else groups[key] = [value]; 160 | } 161 | }); 162 | } 163 | 164 | // Group metrics from the next tier. 165 | function computePyramidal(start, stop) { 166 | var bins = {}; 167 | find(start, stop, tier.next, false, function(time, value) { 168 | var bin = bins[time = tier.floor(time)] || (bins[time] = {size: tier.size(time), values: []}); 169 | if (bin.values.push(value) === bin.size) { 170 | save(time, reduce(bin.values)); 171 | delete bins[time]; 172 | } 173 | }); 174 | } 175 | 176 | // Group raw events. Unlike the pyramidal computation, here we can control 177 | // the order in which rows are returned from the database. Thus, we know 178 | // when we've seen all of the events for a given time interval. 179 | function computeFlat(start, stop) { 180 | filter.t.$gte = start; 181 | filter.t.$lt = stop; 182 | type.events.find(filter, fields, event_options, function(error, cursor) { 183 | if (error) throw error; 184 | var time = start, values = []; 185 | cursor.each(function(error, row) { 186 | if (error) throw error; 187 | if (row) { 188 | var then = tier.floor(row.t); 189 | if (time < then) { 190 | save(time, values.length ? reduce(values) : reduce.empty); 191 | while ((time = tier.step(time)) < then) callback(time, reduce.empty); 192 | values = [map(row)]; 193 | } else { 194 | values.push(map(row)); 195 | } 196 | } else { 197 | save(time, values.length ? reduce(values) : reduce.empty); 198 | while ((time = tier.step(time)) < stop) callback(time, reduce.empty); 199 | } 200 | }); 201 | }); 202 | } 203 | 204 | function saveGroup(time, value, group) { 205 | callback(time, value, group); 206 | if (value) type.metrics.update({ 207 | e: expression.source, 208 | l: tier.key, 209 | t: time, 210 | g: group 211 | }, { 212 | $set: { 213 | i: false, 214 | v: new Double(value) 215 | } 216 | }, update_options); 217 | } 218 | 219 | function save(time, value) { 220 | callback(time, value); 221 | if (value) type.metrics.update({ 222 | e: expression.source, 223 | l: tier.key, 224 | t: time 225 | }, { 226 | $set: { 227 | i: false, 228 | v: new Double(value) 229 | } 230 | }, update_options); 231 | } 232 | } 233 | } 234 | 235 | // Computes a binary expression by merging two subexpressions. 236 | function binary(expression, start, stop, tier, callback) { 237 | var left = {}, right = {}; 238 | 239 | measure(expression.left, start, stop, tier, function(t, l) { 240 | if (t in right) { 241 | callback(t, expression.op(l, right[t])); 242 | delete right[t]; 243 | } else { 244 | left[t] = l; 245 | } 246 | }); 247 | 248 | measure(expression.right, start, stop, tier, function(t, r) { 249 | if (t in left) { 250 | callback(t, expression.op(left[t], r)); 251 | delete left[t]; 252 | } else { 253 | right[t] = r; 254 | } 255 | }); 256 | } 257 | 258 | return function(request, callback) { 259 | 260 | // Validate the dates. 261 | var start = new Date(request.start), 262 | stop = new Date(request.stop), 263 | id = request.id; 264 | if (isNaN(start)) return util.log("invalid start: " + request.start); 265 | if (isNaN(stop)) return util.log("invalid stop: " + request.stop); 266 | 267 | // Parse the expression. 268 | // TODO store expression as JSON object, or compute canonical form 269 | var expression; 270 | try { 271 | expression = parser.parse(request.expression); 272 | } catch (error) { 273 | return util.log("invalid expression: " + error); 274 | } 275 | 276 | // Round start and stop to the appropriate time step. 277 | var tier = tiers[request.step]; 278 | if (!tier) return util.log("invalid step: " + request.step); 279 | start = tier.floor(start); 280 | stop = tier.ceil(stop); 281 | 282 | function callbackGroupId(time, value, group) { 283 | callback({time: time, group: group, value: value, id: id}); 284 | } 285 | 286 | function callbackGroup(time, value, group) { 287 | callback({time: time, group: group, value: value}); 288 | } 289 | 290 | function callbackValueId(time, value) { 291 | callback({time: time, value: value, id: id}); 292 | } 293 | 294 | function callbackValue(time, value) { 295 | callback({time: time, value: value}); 296 | } 297 | 298 | // Find or compute the desired metric. 299 | measure(expression, start, stop, tier, expression.group 300 | ? ("id" in request ? callbackGroupId : callbackGroup) 301 | : ("id" in request ? callbackValueId : callbackValue)); 302 | }; 303 | }; 304 | -------------------------------------------------------------------------------- /test/metric-expression-test.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | assert = require("assert"), 3 | parser = require("../lib/cube/server/metric-expression"); 4 | 5 | var suite = vows.describe("metric-expression"); 6 | 7 | suite.addBatch({ 8 | 9 | "a simple unary expression, sum(test)": { 10 | topic: parser.parse("sum(test)"), 11 | "is unary (has no associated binary operator)": function(e) { 12 | assert.isUndefined(e.op); 13 | }, 14 | "has the expected event type, test": function(e) { 15 | assert.equal(e.type, "test"); 16 | }, 17 | "does not load any event data fields": function(e) { 18 | var fields = {}; 19 | e.fields(fields); 20 | assert.deepEqual(fields, {}); 21 | }, 22 | "does not apply any event data filters": function(e) { 23 | var filter = {}; 24 | e.filter(filter); 25 | assert.deepEqual(filter, {}); 26 | }, 27 | "has the expected reduce": function(e) { 28 | assert.equal(e.reduce, "sum"); 29 | }, 30 | "has the expected source": function(e) { 31 | assert.equal(e.source, "sum(test)"); 32 | }, 33 | "returns the value one for any event": function(e) { 34 | assert.equal(e.value(), 1); 35 | } 36 | }, 37 | 38 | "an expression with a simple data accessor": { 39 | topic: parser.parse("sum(test(i))"), 40 | "loads the specified event data field": function(e) { 41 | var fields = {}; 42 | e.fields(fields); 43 | assert.deepEqual(fields, {"d.i": 1}); 44 | }, 45 | "ignores events that do not have the specified field": function(e) { 46 | var filter = {}; 47 | e.filter(filter); 48 | assert.deepEqual(filter, {"d.i": {$exists: true}}); 49 | }, 50 | "returns the specified field value": function(e) { 51 | assert.equal(e.value({d: {i: 42}}), 42); 52 | assert.equal(e.value({d: {i: -1}}), -1); 53 | } 54 | }, 55 | 56 | "an expression with a compound data accessor": { 57 | topic: parser.parse("sum(test(i + i * i - 2))"), 58 | "loads the specified event data field": function(e) { 59 | var fields = {}; 60 | e.fields(fields); 61 | assert.deepEqual(fields, {"d.i": 1}); 62 | }, 63 | "ignores events that do not have the specified field": function(e) { 64 | var filter = {}; 65 | e.filter(filter); 66 | assert.deepEqual(filter, {"d.i": {$exists: true}}); 67 | }, 68 | "computes the specified value expression": function(e) { 69 | assert.equal(e.value({d: {i: 42}}), 1804); 70 | assert.equal(e.value({d: {i: -1}}), -2); 71 | } 72 | }, 73 | 74 | "an expression with a data accessor and a filter on the same field": { 75 | topic: parser.parse("sum(test(i).gt(i, 42))"), 76 | "loads the specified event data field": function(e) { 77 | var fields = {}; 78 | e.fields(fields); 79 | assert.deepEqual(fields, {"d.i": 1}); 80 | }, 81 | "only filters using the explicit filter; existence is implied": function(e) { 82 | var filter = {}; 83 | e.filter(filter); 84 | assert.deepEqual(filter, {"d.i": {$gt: 42}}); 85 | } 86 | }, 87 | 88 | "an expression with filters on different fields": { 89 | topic: parser.parse("sum(test.gt(i, 42).eq(j, \"foo\"))"), 90 | "does not load fields that are only filtered": function(e) { 91 | var fields = {}; 92 | e.fields(fields); 93 | assert.deepEqual(fields, {}); 94 | }, 95 | "has the expected filters on each specified field": function(e) { 96 | var filter = {}; 97 | e.filter(filter); 98 | assert.deepEqual(filter, {"d.i": {$gt: 42}, "d.j": "foo"}); 99 | } 100 | }, 101 | 102 | "an expression with an object data accessor": { 103 | topic: parser.parse("sum(test(i.j))"), 104 | "loads the specified event data field": function(e) { 105 | var fields = {}; 106 | e.fields(fields); 107 | assert.deepEqual(fields, {"d.i.j": 1}); 108 | }, 109 | "ignores events that do not have the specified field": function(e) { 110 | var filter = {}; 111 | e.filter(filter); 112 | assert.deepEqual(filter, {"d.i.j": {$exists: true}}); 113 | }, 114 | "computes the specified value expression": function(e) { 115 | assert.equal(e.value({d: {i: {j: 42}}}), 42); 116 | assert.equal(e.value({d: {i: {j: -1}}}), -1); 117 | } 118 | }, 119 | 120 | "an expression with an array data accessor": { 121 | topic: parser.parse("sum(test(i[0]))"), 122 | "loads the specified event data field": function(e) { 123 | var fields = {}; 124 | e.fields(fields); 125 | assert.deepEqual(fields, {"d.i": 1}); 126 | }, 127 | "ignores events that do not have the specified field": function(e) { 128 | var filter = {}; 129 | e.filter(filter); 130 | assert.deepEqual(filter, {"d.i": {$exists: true}}); 131 | }, 132 | "computes the specified value expression": function(e) { 133 | assert.equal(e.value({d: {i: [42]}}), 42); 134 | assert.equal(e.value({d: {i: [-1]}}), -1); 135 | } 136 | }, 137 | 138 | "an expression with an elaborate data accessor": { 139 | topic: parser.parse("sum(test(i.j[0].k))"), 140 | "loads the specified event data field": function(e) { 141 | var fields = {}; 142 | e.fields(fields); 143 | assert.deepEqual(fields, {"d.i.j.k": 1}); 144 | }, 145 | "ignores events that do not have the specified field": function(e) { 146 | var filter = {}; 147 | e.filter(filter); 148 | assert.deepEqual(filter, {"d.i.j.k": {$exists: true}}); 149 | }, 150 | "computes the specified value expression": function(e) { 151 | assert.equal(e.value({d: {i: {j: [{k: 42}]}}}), 42); 152 | assert.equal(e.value({d: {i: {j: [{k: -1}]}}}), -1); 153 | } 154 | }, 155 | 156 | "a compound expression": { 157 | topic: parser.parse("sum(foo(2)) + sum(bar(3))"), 158 | "is compound (has an associated binary operator)": function(e) { 159 | assert.equal(e.op.name, "add"); 160 | }, 161 | "has the expected left expression": function(e) { 162 | var filter = {}, fields = {}; 163 | e.left.filter(filter); 164 | e.left.fields(fields); 165 | assert.deepEqual(filter, {}); 166 | assert.deepEqual(fields, {}); 167 | assert.equal(e.left.type, "foo"); 168 | assert.equal(e.left.reduce, "sum"); 169 | assert.equal(e.left.value(), 2); 170 | }, 171 | "has the expected right expression": function(e) { 172 | var filter = {}, fields = {}; 173 | e.right.filter(filter); 174 | e.right.fields(fields); 175 | assert.deepEqual(filter, {}); 176 | assert.deepEqual(fields, {}); 177 | assert.equal(e.right.type, "bar"); 178 | assert.equal(e.right.reduce, "sum"); 179 | assert.equal(e.right.value(), 3); 180 | }, 181 | "computes the specified value expression": function(e) { 182 | assert.equal(e.op(2, 3), 5) 183 | } 184 | }, 185 | 186 | "a negated unary expression": { 187 | topic: parser.parse("-sum(foo)"), 188 | "negates the specified value expression": function(e) { 189 | assert.equal(e.value(), -1) 190 | } 191 | }, 192 | 193 | "constant expressions": { 194 | topic: parser.parse("-4"), 195 | "has a constant value": function(e) { 196 | assert.equal(e.value(), -4) 197 | } 198 | }, 199 | 200 | "a grouped expression": { 201 | topic: parser.parse("sum(test(bar).group(foo))"), 202 | "has the expected expression": function(e) { 203 | var filter = {}, fields = {}, group = {}; 204 | e.filter(filter); 205 | e.fields(fields); 206 | e.group.fields(group); 207 | assert.deepEqual(filter, {"d.bar": {$exists: true}}); 208 | assert.deepEqual(fields, {"d.bar": 1}); 209 | assert.deepEqual(group, {"d.foo": 1}); 210 | assert.equal(e.type, "test"); 211 | assert.equal(e.reduce, "sum"); 212 | assert.equal(e.value({d: {bar: 42}}), 42); 213 | assert.equal(e.group.field, "d.foo"); 214 | assert.isTrue(!e.group.multi); 215 | } 216 | }, 217 | 218 | "a multi-grouped (labeled) expression": { 219 | topic: parser.parse("sum(test(bar).groups(foo))"), 220 | "has the expected expression": function(e) { 221 | var filter = {}, fields = {}, group = {}; 222 | e.filter(filter); 223 | e.fields(fields); 224 | e.group.fields(group); 225 | assert.deepEqual(filter, {"d.bar": {$exists: true}}); 226 | assert.deepEqual(fields, {"d.bar": 1}); 227 | assert.deepEqual(group, {"d.foo": 1}); 228 | assert.equal(e.type, "test"); 229 | assert.equal(e.reduce, "sum"); 230 | assert.equal(e.value({d: {bar: 42}}), 42); 231 | assert.equal(e.group.field, "d.foo"); 232 | assert.isTrue(e.group.multi); 233 | } 234 | }, 235 | 236 | "filters": { 237 | "multiple filters on the same field are combined": function() { 238 | var filter = {}; 239 | parser.parse("sum(test.gt(i, 42).le(i, 52))").filter(filter); 240 | assert.deepEqual(filter, {"d.i": {$gt: 42, $lte: 52}}); 241 | }, 242 | "given range and exact filters, range filters are ignored": function() { 243 | var filter = {}; 244 | parser.parse("sum(test.gt(i, 42).eq(i, 52))").filter(filter); 245 | assert.deepEqual(filter, {"d.i": 52}); 246 | }, 247 | "given exact and range filters, range filters are ignored": function() { 248 | var filter = {}; 249 | parser.parse("sum(test.eq(i, 52).gt(i, 42))").filter(filter); 250 | assert.deepEqual(filter, {"d.i": 52}); 251 | }, 252 | "the eq filter results in a simple query filter": function() { 253 | var filter = {}; 254 | parser.parse("sum(test.eq(i, 42))").filter(filter); 255 | assert.deepEqual(filter, {"d.i": 42}); 256 | }, 257 | "the gt filter results in a $gt query filter": function(e) { 258 | var filter = {}; 259 | parser.parse("sum(test.gt(i, 42))").filter(filter); 260 | assert.deepEqual(filter, {"d.i": {$gt: 42}}); 261 | }, 262 | "the ge filter results in a $gte query filter": function(e) { 263 | var filter = {}; 264 | parser.parse("sum(test.ge(i, 42))").filter(filter); 265 | assert.deepEqual(filter, {"d.i": {$gte: 42}}); 266 | }, 267 | "the lt filter results in an $lt query filter": function(e) { 268 | var filter = {}; 269 | parser.parse("sum(test.lt(i, 42))").filter(filter); 270 | assert.deepEqual(filter, {"d.i": {$lt: 42}}); 271 | }, 272 | "the le filter results in an $lte query filter": function(e) { 273 | var filter = {}; 274 | parser.parse("sum(test.le(i, 42))").filter(filter); 275 | assert.deepEqual(filter, {"d.i": {$lte: 42}}); 276 | }, 277 | "the ne filter results in an $ne query filter": function(e) { 278 | var filter = {}; 279 | parser.parse("sum(test.ne(i, 42))").filter(filter); 280 | assert.deepEqual(filter, {"d.i": {$ne: 42}}); 281 | }, 282 | "the re filter results in a $regex query filter": function(e) { 283 | var filter = {}; 284 | parser.parse("sum(test.re(i, \"foo\"))").filter(filter); 285 | assert.deepEqual(filter, {"d.i": {$regex: "foo"}}); 286 | }, 287 | "the in filter results in a $in query filter": function(e) { 288 | var filter = {}; 289 | parser.parse("sum(test.in(i, [\"foo\", 42]))").filter(filter); 290 | assert.deepEqual(filter, {"d.i": {$in: ["foo", 42]}}); 291 | } 292 | } 293 | 294 | }); 295 | 296 | suite.export(module); 297 | -------------------------------------------------------------------------------- /test/tiers-test.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | assert = require("assert"), 3 | tiers = require("../lib/cube/server/tiers"); 4 | 5 | var suite = vows.describe("tiers"); 6 | 7 | suite.addBatch({ 8 | 9 | "tiers": { 10 | "contains exactly the expected tiers": function() { 11 | var keys = []; 12 | for (var key in tiers) { 13 | keys.push(+key); 14 | } 15 | keys.sort(function(a, b) { return a - b; }); 16 | assert.deepEqual(keys, [2e4, 3e5, 36e5, 864e5, 6048e5, 2592e6]); 17 | } 18 | }, 19 | 20 | "second20": { 21 | topic: tiers[2e4], 22 | "has the key 2e4": function(tier) { 23 | assert.strictEqual(tier.key, 2e4); 24 | }, 25 | "next is undefined": function(tier) { 26 | assert.isUndefined(tier.next); 27 | }, 28 | "size is undefined": function(tier) { 29 | assert.isUndefined(tier.size); 30 | }, 31 | 32 | "floor": { 33 | "rounds down to 20-seconds": function(tier) { 34 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 00, 20)), utc(2011, 08, 02, 12, 00, 20)); 35 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 00, 21)), utc(2011, 08, 02, 12, 00, 20)); 36 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 00, 23)), utc(2011, 08, 02, 12, 00, 20)); 37 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 00, 39)), utc(2011, 08, 02, 12, 00, 20)); 38 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 00, 40)), utc(2011, 08, 02, 12, 00, 40)); 39 | }, 40 | "does not modify the passed-in date": function(tier) { 41 | var date = utc(2011, 08, 02, 12, 00, 21); 42 | assert.deepEqual(tier.floor(date), utc(2011, 08, 02, 12, 00, 20)); 43 | assert.deepEqual(date, utc(2011, 08, 02, 12, 00, 21)); 44 | } 45 | }, 46 | 47 | "ceil": { 48 | "rounds up to 5-minutes": function(tier) { 49 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 00, 20)), utc(2011, 08, 02, 12, 00, 20)); 50 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 00, 21)), utc(2011, 08, 02, 12, 00, 40)); 51 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 00, 23)), utc(2011, 08, 02, 12, 00, 40)); 52 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 00, 39)), utc(2011, 08, 02, 12, 00, 40)); 53 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 00, 40)), utc(2011, 08, 02, 12, 00, 40)); 54 | }, 55 | "does not modified the specified date": function(tier) { 56 | var date = utc(2011, 08, 02, 12, 00, 21); 57 | assert.deepEqual(tier.ceil(date), utc(2011, 08, 02, 12, 00, 40)); 58 | assert.deepEqual(date, utc(2011, 08, 02, 12, 00, 21)); 59 | } 60 | }, 61 | 62 | "step": { 63 | "increments time by twenty minutes": function(tier) { 64 | var date = utc(2011, 08, 02, 23, 59, 20); 65 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 02, 23, 59, 40)); 66 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 03, 00, 00, 00)); 67 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 03, 00, 00, 20)); 68 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 03, 00, 00, 40)); 69 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 03, 00, 01, 00)); 70 | }, 71 | "does not round the specified date": function(tier) { 72 | assert.deepEqual(tier.step(utc(2011, 08, 02, 12, 21, 23)), utc(2011, 08, 02, 12, 21, 43)); 73 | }, 74 | "does not modify the specified date": function(tier) { 75 | var date = utc(2011, 08, 02, 12, 20, 00); 76 | assert.deepEqual(tier.step(date), utc(2011, 08, 02, 12, 20, 20)); 77 | assert.deepEqual(date, utc(2011, 08, 02, 12, 20, 00)); 78 | } 79 | } 80 | }, 81 | "minute5": { 82 | topic: tiers[3e5], 83 | "has the key 3e5": function(tier) { 84 | assert.strictEqual(tier.key, 3e5); 85 | }, 86 | "next is the 20-second tier": function(tier) { 87 | assert.equal(tier.next, tiers[2e4]); 88 | }, 89 | "size is 15": function(tier) { 90 | assert.strictEqual(tier.size(), 15); 91 | }, 92 | 93 | "floor": { 94 | "rounds down to 5-minutes": function(tier) { 95 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 20, 00)), utc(2011, 08, 02, 12, 20)); 96 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 20, 01)), utc(2011, 08, 02, 12, 20)); 97 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 21, 00)), utc(2011, 08, 02, 12, 20)); 98 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 23, 00)), utc(2011, 08, 02, 12, 20)); 99 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 24, 59)), utc(2011, 08, 02, 12, 20)); 100 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 25, 00)), utc(2011, 08, 02, 12, 25)); 101 | }, 102 | "does not modify the passed-in date": function(tier) { 103 | var date = utc(2011, 08, 02, 12, 21); 104 | assert.deepEqual(tier.floor(date), utc(2011, 08, 02, 12, 20)); 105 | assert.deepEqual(date, utc(2011, 08, 02, 12, 21)); 106 | } 107 | }, 108 | 109 | "ceil": { 110 | "rounds up to 5-minutes": function(tier) { 111 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 20, 00)), utc(2011, 08, 02, 12, 20)); 112 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 20, 01)), utc(2011, 08, 02, 12, 25)); 113 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 21, 00)), utc(2011, 08, 02, 12, 25)); 114 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 23, 00)), utc(2011, 08, 02, 12, 25)); 115 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 24, 59)), utc(2011, 08, 02, 12, 25)); 116 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 25, 00)), utc(2011, 08, 02, 12, 25)); 117 | }, 118 | "does not modified the specified date": function(tier) { 119 | var date = utc(2011, 08, 02, 12, 21, 00); 120 | assert.deepEqual(tier.ceil(date), utc(2011, 08, 02, 12, 25)); 121 | assert.deepEqual(date, utc(2011, 08, 02, 12, 21)); 122 | } 123 | }, 124 | 125 | "step": { 126 | "increments time by five minutes": function(tier) { 127 | var date = utc(2011, 08, 02, 23, 45, 00); 128 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 02, 23, 50)); 129 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 02, 23, 55)); 130 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 03, 00, 00)); 131 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 03, 00, 05)); 132 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 03, 00, 10)); 133 | }, 134 | "does not round the specified date": function(tier) { 135 | assert.deepEqual(tier.step(utc(2011, 08, 02, 12, 21, 23)), utc(2011, 08, 02, 12, 26, 23)); 136 | }, 137 | "does not modify the specified date": function(tier) { 138 | var date = utc(2011, 08, 02, 12, 20, 00); 139 | assert.deepEqual(tier.step(date), utc(2011, 08, 02, 12, 25)); 140 | assert.deepEqual(date, utc(2011, 08, 02, 12, 20)); 141 | } 142 | } 143 | }, 144 | 145 | "hour": { 146 | topic: tiers[36e5], 147 | "has the key 36e5": function(tier) { 148 | assert.strictEqual(tier.key, 36e5); 149 | }, 150 | "next is the 5-minute tier": function(tier) { 151 | assert.equal(tier.next, tiers[3e5]); 152 | }, 153 | "size is 12": function(tier) { 154 | assert.strictEqual(tier.size(), 12); 155 | }, 156 | 157 | "floor": { 158 | "rounds down to hours": function(tier) { 159 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 00, 00)), utc(2011, 08, 02, 12, 00)); 160 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 00, 01)), utc(2011, 08, 02, 12, 00)); 161 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 21, 00)), utc(2011, 08, 02, 12, 00)); 162 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 59, 59)), utc(2011, 08, 02, 12, 00)); 163 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 13, 00, 00)), utc(2011, 08, 02, 13, 00)); 164 | }, 165 | "does not modify the passed-in date": function(tier) { 166 | var date = utc(2011, 08, 02, 12, 21); 167 | assert.deepEqual(tier.floor(date), utc(2011, 08, 02, 12, 00)); 168 | assert.deepEqual(date, utc(2011, 08, 02, 12, 21)); 169 | } 170 | }, 171 | 172 | "ceil": { 173 | "rounds up to hours": function(tier) { 174 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 00, 00)), utc(2011, 08, 02, 12, 00)); 175 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 00, 01)), utc(2011, 08, 02, 13, 00)); 176 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 21, 00)), utc(2011, 08, 02, 13, 00)); 177 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 59, 59)), utc(2011, 08, 02, 13, 00)); 178 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 13, 00, 00)), utc(2011, 08, 02, 13, 00)); 179 | }, 180 | "does not modified the specified date": function(tier) { 181 | var date = utc(2011, 08, 02, 12, 21, 00); 182 | assert.deepEqual(tier.ceil(date), utc(2011, 08, 02, 13, 00)); 183 | assert.deepEqual(date, utc(2011, 08, 02, 12, 21)); 184 | } 185 | }, 186 | 187 | "step": { 188 | "increments time by one hour": function(tier) { 189 | var date = utc(2011, 08, 02, 22, 00, 00); 190 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 02, 23, 00)); 191 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 03, 00, 00)); 192 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 03, 01, 00)); 193 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 03, 02, 00)); 194 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 03, 03, 00)); 195 | }, 196 | "does not round the specified date": function(tier) { 197 | assert.deepEqual(tier.step(utc(2011, 08, 02, 12, 21, 23)), utc(2011, 08, 02, 13, 21, 23)); 198 | }, 199 | "does not modify the specified date": function(tier) { 200 | var date = utc(2011, 08, 02, 12, 00, 00); 201 | assert.deepEqual(tier.step(date), utc(2011, 08, 02, 13, 00)); 202 | assert.deepEqual(date, utc(2011, 08, 02, 12, 00)); 203 | } 204 | } 205 | }, 206 | 207 | "day": { 208 | topic: tiers[864e5], 209 | "has the key 864e5": function(tier) { 210 | assert.strictEqual(tier.key, 864e5); 211 | }, 212 | "next is the one-hour tier": function(tier) { 213 | assert.equal(tier.next, tiers[36e5]); 214 | }, 215 | "size is 24": function(tier) { 216 | assert.strictEqual(tier.size(), 24); 217 | }, 218 | 219 | "floor": { 220 | "rounds down to days": function(tier) { 221 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 00, 00, 00)), utc(2011, 08, 02, 00, 00)); 222 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 00, 00, 01)), utc(2011, 08, 02, 00, 00)); 223 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 12, 21, 00)), utc(2011, 08, 02, 00, 00)); 224 | assert.deepEqual(tier.floor(utc(2011, 08, 02, 23, 59, 59)), utc(2011, 08, 02, 00, 00)); 225 | assert.deepEqual(tier.floor(utc(2011, 08, 03, 00, 00, 00)), utc(2011, 08, 03, 00, 00)); 226 | }, 227 | "does not modify the passed-in date": function(tier) { 228 | var date = utc(2011, 08, 02, 12, 21); 229 | assert.deepEqual(tier.floor(date), utc(2011, 08, 02, 00, 00)); 230 | assert.deepEqual(date, utc(2011, 08, 02, 12, 21)); 231 | } 232 | }, 233 | 234 | "ceil": { 235 | "rounds up to days": function(tier) { 236 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 00, 00, 00)), utc(2011, 08, 02, 00, 00)); 237 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 00, 00, 01)), utc(2011, 08, 03, 00, 00)); 238 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 12, 21, 00)), utc(2011, 08, 03, 00, 00)); 239 | assert.deepEqual(tier.ceil(utc(2011, 08, 02, 23, 59, 59)), utc(2011, 08, 03, 00, 00)); 240 | assert.deepEqual(tier.ceil(utc(2011, 08, 03, 00, 00, 00)), utc(2011, 08, 03, 00, 00)); 241 | }, 242 | "does not modified the specified date": function(tier) { 243 | var date = utc(2011, 08, 02, 12, 21, 00); 244 | assert.deepEqual(tier.ceil(date), utc(2011, 08, 03, 00, 00)); 245 | assert.deepEqual(date, utc(2011, 08, 02, 12, 21)); 246 | } 247 | }, 248 | 249 | "step": { 250 | "increments time by one day": function(tier) { 251 | var date = utc(2011, 08, 02, 00, 00, 00); 252 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 03, 00, 00)); 253 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 04, 00, 00)); 254 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 05, 00, 00)); 255 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 06, 00, 00)); 256 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 07, 00, 00)); 257 | }, 258 | "does not round the specified date": function(tier) { 259 | assert.deepEqual(tier.step(utc(2011, 08, 02, 12, 21, 23)), utc(2011, 08, 03, 12, 21, 23)); 260 | }, 261 | "does not modify the specified date": function(tier) { 262 | var date = utc(2011, 08, 02, 00, 00, 00); 263 | assert.deepEqual(tier.step(date), utc(2011, 08, 03, 00, 00)); 264 | assert.deepEqual(date, utc(2011, 08, 02, 00, 00)); 265 | } 266 | } 267 | }, 268 | 269 | "week": { 270 | topic: tiers[6048e5], 271 | "has the key 6048e5": function(tier) { 272 | assert.strictEqual(tier.key, 6048e5); 273 | }, 274 | "next is the one-day tier": function(tier) { 275 | assert.equal(tier.next, tiers[864e5]); 276 | }, 277 | "size is 7": function(tier) { 278 | assert.strictEqual(tier.size(), 7); 279 | }, 280 | 281 | "floor": { 282 | "rounds down to weeks": function(tier) { 283 | assert.deepEqual(tier.floor(utc(2011, 08, 04, 00, 00, 00)), utc(2011, 08, 04, 00, 00)); 284 | assert.deepEqual(tier.floor(utc(2011, 08, 04, 00, 00, 01)), utc(2011, 08, 04, 00, 00)); 285 | assert.deepEqual(tier.floor(utc(2011, 08, 04, 12, 21, 00)), utc(2011, 08, 04, 00, 00)); 286 | assert.deepEqual(tier.floor(utc(2011, 08, 10, 23, 59, 59)), utc(2011, 08, 04, 00, 00)); 287 | assert.deepEqual(tier.floor(utc(2011, 08, 11, 00, 00, 00)), utc(2011, 08, 11, 00, 00)); 288 | }, 289 | "does not modify the passed-in date": function(tier) { 290 | var date = utc(2011, 08, 04, 12, 21); 291 | assert.deepEqual(tier.floor(date), utc(2011, 08, 04, 00, 00)); 292 | assert.deepEqual(date, utc(2011, 08, 04, 12, 21)); 293 | } 294 | }, 295 | 296 | "ceil": { 297 | "rounds up to weeks": function(tier) { 298 | assert.deepEqual(tier.ceil(utc(2011, 08, 04, 00, 00, 00)), utc(2011, 08, 04, 00, 00)); 299 | assert.deepEqual(tier.ceil(utc(2011, 08, 04, 00, 00, 01)), utc(2011, 08, 11, 00, 00)); 300 | assert.deepEqual(tier.ceil(utc(2011, 08, 04, 12, 21, 00)), utc(2011, 08, 11, 00, 00)); 301 | assert.deepEqual(tier.ceil(utc(2011, 08, 10, 23, 59, 59)), utc(2011, 08, 11, 00, 00)); 302 | assert.deepEqual(tier.ceil(utc(2011, 08, 11, 00, 00, 00)), utc(2011, 08, 11, 00, 00)); 303 | }, 304 | "does not modified the specified date": function(tier) { 305 | var date = utc(2011, 08, 02, 12, 21, 00); 306 | assert.deepEqual(tier.ceil(date), utc(2011, 08, 04, 00, 00)); 307 | assert.deepEqual(date, utc(2011, 08, 02, 12, 21)); 308 | } 309 | }, 310 | 311 | "step": { 312 | "increments time by one week": function(tier) { 313 | var date = utc(2011, 08, 04, 00, 00, 00); 314 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 11, 00, 00)); 315 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 18, 00, 00)); 316 | assert.deepEqual(date = tier.step(date), utc(2011, 08, 25, 00, 00)); 317 | assert.deepEqual(date = tier.step(date), utc(2011, 09, 02, 00, 00)); 318 | assert.deepEqual(date = tier.step(date), utc(2011, 09, 09, 00, 00)); 319 | }, 320 | "does not round the specified date": function(tier) { 321 | assert.deepEqual(tier.step(utc(2011, 08, 02, 12, 21, 23)), utc(2011, 08, 09, 12, 21, 23)); 322 | }, 323 | "does not modify the specified date": function(tier) { 324 | var date = utc(2011, 08, 04, 00, 00, 00); 325 | assert.deepEqual(tier.step(date), utc(2011, 08, 11, 00, 00)); 326 | assert.deepEqual(date, utc(2011, 08, 04, 00, 00)); 327 | } 328 | } 329 | }, 330 | 331 | "month": { 332 | topic: tiers[2592e6], 333 | "has the key 2592e6": function(tier) { 334 | assert.strictEqual(tier.key, 2592e6); 335 | }, 336 | "next is the one-day tier": function(tier) { 337 | assert.equal(tier.next, tiers[864e5]); 338 | }, 339 | "size is number of days in a month": function(tier) { 340 | assert.strictEqual(tier.size(utc(2011, 00, 01)), 31); 341 | assert.strictEqual(tier.size(utc(2011, 01, 01)), 28); 342 | }, 343 | 344 | "floor": { 345 | "rounds down to months": function(tier) { 346 | assert.deepEqual(tier.floor(utc(2011, 08, 01, 00, 00, 00)), utc(2011, 08, 01, 00, 00)); 347 | assert.deepEqual(tier.floor(utc(2011, 08, 01, 00, 00, 01)), utc(2011, 08, 01, 00, 00)); 348 | assert.deepEqual(tier.floor(utc(2011, 08, 04, 12, 21, 00)), utc(2011, 08, 01, 00, 00)); 349 | assert.deepEqual(tier.floor(utc(2011, 08, 29, 23, 59, 59)), utc(2011, 08, 01, 00, 00)); 350 | assert.deepEqual(tier.floor(utc(2011, 09, 01, 00, 00, 00)), utc(2011, 09, 01, 00, 00)); 351 | }, 352 | "does not modify the passed-in date": function(tier) { 353 | var date = utc(2011, 08, 04, 12, 21); 354 | assert.deepEqual(tier.floor(date), utc(2011, 08, 01, 00, 00)); 355 | assert.deepEqual(date, utc(2011, 08, 04, 12, 21)); 356 | } 357 | }, 358 | 359 | "ceil": { 360 | "rounds up to weeks": function(tier) { 361 | assert.deepEqual(tier.ceil(utc(2011, 08, 01, 00, 00, 00)), utc(2011, 08, 01, 00, 00)); 362 | assert.deepEqual(tier.ceil(utc(2011, 08, 01, 00, 00, 01)), utc(2011, 09, 01, 00, 00)); 363 | assert.deepEqual(tier.ceil(utc(2011, 08, 04, 12, 21, 00)), utc(2011, 09, 01, 00, 00)); 364 | assert.deepEqual(tier.ceil(utc(2011, 08, 29, 23, 59, 59)), utc(2011, 09, 01, 00, 00)); 365 | assert.deepEqual(tier.ceil(utc(2011, 09, 01, 00, 00, 00)), utc(2011, 09, 01, 00, 00)); 366 | }, 367 | "does not modified the specified date": function(tier) { 368 | var date = utc(2011, 08, 02, 12, 21, 00); 369 | assert.deepEqual(tier.ceil(date), utc(2011, 09, 01, 00, 00)); 370 | assert.deepEqual(date, utc(2011, 08, 02, 12, 21)); 371 | } 372 | }, 373 | 374 | "step": { 375 | "increments time by one month": function(tier) { 376 | var date = utc(2011, 08, 01, 00, 00, 00); 377 | assert.deepEqual(date = tier.step(date), utc(2011, 09, 01, 00, 00)); 378 | assert.deepEqual(date = tier.step(date), utc(2011, 10, 01, 00, 00)); 379 | assert.deepEqual(date = tier.step(date), utc(2011, 11, 01, 00, 00)); 380 | assert.deepEqual(date = tier.step(date), utc(2012, 00, 01, 00, 00)); 381 | assert.deepEqual(date = tier.step(date), utc(2012, 01, 01, 00, 00)); 382 | }, 383 | "does not round the specified date": function(tier) { 384 | assert.deepEqual(tier.step(utc(2011, 01, 02, 12, 21, 23)), utc(2011, 02, 02, 12, 21, 23)); 385 | assert.deepEqual(tier.step(utc(2011, 08, 02, 12, 21, 23)), utc(2011, 09, 02, 12, 21, 23)); 386 | }, 387 | "does not modify the specified date": function(tier) { 388 | var date = utc(2011, 08, 01, 00, 00, 00); 389 | assert.deepEqual(tier.step(date), utc(2011, 09, 01, 00, 00)); 390 | assert.deepEqual(date, utc(2011, 08, 01, 00, 00)); 391 | } 392 | } 393 | } 394 | 395 | }); 396 | 397 | function utc(year, month, day, hours, minutes, seconds) { 398 | return new Date(Date.UTC(year, month, day, hours || 0, minutes || 0, seconds || 0)); 399 | } 400 | 401 | suite.export(module); 402 | --------------------------------------------------------------------------------