├── .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 |
--------------------------------------------------------------------------------