cubism-rails 1.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +25 -0
- data/Rakefile +1 -0
- data/cubism-rails.gemspec +21 -0
- data/lib/cubism-rails/version.rb +5 -0
- data/lib/cubism-rails.rb +8 -0
- data/vendor/assets/javascripts/cubism.js +1045 -0
- data/vendor/assets/javascripts/cubism.min.js +1 -0
- metadata +77 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Vlad Gorodetsky
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# cubism-rails
|
2
|
+
|
3
|
+
Cubism.js is a [D3](http://mbostock.github.com/d3/) plugin for visualizing time series. Use Cubism to construct better realtime dashboards, pulling data from [Graphite](/square/cubism/wiki/Graphite), [Cube](/square/cubism/wiki/Cube) and other sources. Cubism is available under the [Apache License](/square/cubism/blob/master/LICENSE).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add the following to your gemfile:
|
8
|
+
|
9
|
+
gem 'cubism-rails'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Add the following directive to your JavaScript manifest file (application.js):
|
16
|
+
|
17
|
+
//= require cubism
|
18
|
+
|
19
|
+
## Contributing
|
20
|
+
|
21
|
+
1. Fork it
|
22
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
23
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
24
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
25
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'cubism-rails/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "cubism-rails"
|
8
|
+
gem.version = Cubism::Rails::VERSION
|
9
|
+
gem.authors = ["Vlad Gorodetsky"]
|
10
|
+
gem.email = ["v@gor.io"]
|
11
|
+
gem.description = %q{Cubism.js: A JavaScript library for time series visualization.}
|
12
|
+
gem.summary = %q{Gemified cubism.js asset for Rails}
|
13
|
+
gem.homepage = "http://github.com/bai/cubism-rails"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency "railties", ">= 3.0", "< 5.0"
|
21
|
+
end
|
data/lib/cubism-rails.rb
ADDED
@@ -0,0 +1,1045 @@
|
|
1
|
+
(function(exports){
|
2
|
+
var cubism = exports.cubism = {version: "1.2.2"};
|
3
|
+
var cubism_id = 0;
|
4
|
+
function cubism_identity(d) { return d; }
|
5
|
+
cubism.option = function(name, defaultValue) {
|
6
|
+
var values = cubism.options(name);
|
7
|
+
return values.length ? values[0] : defaultValue;
|
8
|
+
};
|
9
|
+
|
10
|
+
cubism.options = function(name, defaultValues) {
|
11
|
+
var options = location.search.substring(1).split("&"),
|
12
|
+
values = [],
|
13
|
+
i = -1,
|
14
|
+
n = options.length,
|
15
|
+
o;
|
16
|
+
while (++i < n) {
|
17
|
+
if ((o = options[i].split("="))[0] == name) {
|
18
|
+
values.push(decodeURIComponent(o[1]));
|
19
|
+
}
|
20
|
+
}
|
21
|
+
return values.length || arguments.length < 2 ? values : defaultValues;
|
22
|
+
};
|
23
|
+
cubism.context = function() {
|
24
|
+
var context = new cubism_context,
|
25
|
+
step = 1e4, // ten seconds, in milliseconds
|
26
|
+
size = 1440, // four hours at ten seconds, in pixels
|
27
|
+
start0, stop0, // the start and stop for the previous change event
|
28
|
+
start1, stop1, // the start and stop for the next prepare event
|
29
|
+
serverDelay = 5e3,
|
30
|
+
clientDelay = 5e3,
|
31
|
+
event = d3.dispatch("prepare", "beforechange", "change", "focus"),
|
32
|
+
scale = context.scale = d3.time.scale().range([0, size]),
|
33
|
+
timeout,
|
34
|
+
focus;
|
35
|
+
|
36
|
+
function update() {
|
37
|
+
var now = Date.now();
|
38
|
+
stop0 = new Date(Math.floor((now - serverDelay - clientDelay) / step) * step);
|
39
|
+
start0 = new Date(stop0 - size * step);
|
40
|
+
stop1 = new Date(Math.floor((now - serverDelay) / step) * step);
|
41
|
+
start1 = new Date(stop1 - size * step);
|
42
|
+
scale.domain([start0, stop0]);
|
43
|
+
return context;
|
44
|
+
}
|
45
|
+
|
46
|
+
context.start = function() {
|
47
|
+
if (timeout) clearTimeout(timeout);
|
48
|
+
var delay = +stop1 + serverDelay - Date.now();
|
49
|
+
|
50
|
+
// If we're too late for the first prepare event, skip it.
|
51
|
+
if (delay < clientDelay) delay += step;
|
52
|
+
|
53
|
+
timeout = setTimeout(function prepare() {
|
54
|
+
stop1 = new Date(Math.floor((Date.now() - serverDelay) / step) * step);
|
55
|
+
start1 = new Date(stop1 - size * step);
|
56
|
+
event.prepare.call(context, start1, stop1);
|
57
|
+
|
58
|
+
setTimeout(function() {
|
59
|
+
scale.domain([start0 = start1, stop0 = stop1]);
|
60
|
+
event.beforechange.call(context, start1, stop1);
|
61
|
+
event.change.call(context, start1, stop1);
|
62
|
+
event.focus.call(context, focus);
|
63
|
+
}, clientDelay);
|
64
|
+
|
65
|
+
timeout = setTimeout(prepare, step);
|
66
|
+
}, delay);
|
67
|
+
return context;
|
68
|
+
};
|
69
|
+
|
70
|
+
context.stop = function() {
|
71
|
+
timeout = clearTimeout(timeout);
|
72
|
+
return context;
|
73
|
+
};
|
74
|
+
|
75
|
+
timeout = setTimeout(context.start, 10);
|
76
|
+
|
77
|
+
// Set or get the step interval in milliseconds.
|
78
|
+
// Defaults to ten seconds.
|
79
|
+
context.step = function(_) {
|
80
|
+
if (!arguments.length) return step;
|
81
|
+
step = +_;
|
82
|
+
return update();
|
83
|
+
};
|
84
|
+
|
85
|
+
// Set or get the context size (the count of metric values).
|
86
|
+
// Defaults to 1440 (four hours at ten seconds).
|
87
|
+
context.size = function(_) {
|
88
|
+
if (!arguments.length) return size;
|
89
|
+
scale.range([0, size = +_]);
|
90
|
+
return update();
|
91
|
+
};
|
92
|
+
|
93
|
+
// The server delay is the amount of time we wait for the server to compute a
|
94
|
+
// metric. This delay may result from clock skew or from delays collecting
|
95
|
+
// metrics from various hosts. Defaults to 4 seconds.
|
96
|
+
context.serverDelay = function(_) {
|
97
|
+
if (!arguments.length) return serverDelay;
|
98
|
+
serverDelay = +_;
|
99
|
+
return update();
|
100
|
+
};
|
101
|
+
|
102
|
+
// The client delay is the amount of additional time we wait to fetch those
|
103
|
+
// metrics from the server. The client and server delay combined represent the
|
104
|
+
// age of the most recent displayed metric. Defaults to 1 second.
|
105
|
+
context.clientDelay = function(_) {
|
106
|
+
if (!arguments.length) return clientDelay;
|
107
|
+
clientDelay = +_;
|
108
|
+
return update();
|
109
|
+
};
|
110
|
+
|
111
|
+
// Sets the focus to the specified index, and dispatches a "focus" event.
|
112
|
+
context.focus = function(i) {
|
113
|
+
event.focus.call(context, focus = i);
|
114
|
+
return context;
|
115
|
+
};
|
116
|
+
|
117
|
+
// Add, remove or get listeners for events.
|
118
|
+
context.on = function(type, listener) {
|
119
|
+
if (arguments.length < 2) return event.on(type);
|
120
|
+
|
121
|
+
event.on(type, listener);
|
122
|
+
|
123
|
+
// Notify the listener of the current start and stop time, as appropriate.
|
124
|
+
// This way, metrics can make requests for data immediately,
|
125
|
+
// and likewise the axis can display itself synchronously.
|
126
|
+
if (listener != null) {
|
127
|
+
if (/^prepare(\.|$)/.test(type)) listener.call(context, start1, stop1);
|
128
|
+
if (/^beforechange(\.|$)/.test(type)) listener.call(context, start0, stop0);
|
129
|
+
if (/^change(\.|$)/.test(type)) listener.call(context, start0, stop0);
|
130
|
+
if (/^focus(\.|$)/.test(type)) listener.call(context, focus);
|
131
|
+
}
|
132
|
+
|
133
|
+
return context;
|
134
|
+
};
|
135
|
+
|
136
|
+
d3.select(window).on("keydown.context-" + ++cubism_id, function() {
|
137
|
+
switch (!d3.event.metaKey && d3.event.keyCode) {
|
138
|
+
case 37: // left
|
139
|
+
if (focus == null) focus = size - 1;
|
140
|
+
if (focus > 0) context.focus(--focus);
|
141
|
+
break;
|
142
|
+
case 39: // right
|
143
|
+
if (focus == null) focus = size - 2;
|
144
|
+
if (focus < size - 1) context.focus(++focus);
|
145
|
+
break;
|
146
|
+
default: return;
|
147
|
+
}
|
148
|
+
d3.event.preventDefault();
|
149
|
+
});
|
150
|
+
|
151
|
+
return update();
|
152
|
+
};
|
153
|
+
|
154
|
+
function cubism_context() {}
|
155
|
+
|
156
|
+
var cubism_contextPrototype = cubism.context.prototype = cubism_context.prototype;
|
157
|
+
|
158
|
+
cubism_contextPrototype.constant = function(value) {
|
159
|
+
return new cubism_metricConstant(this, +value);
|
160
|
+
};
|
161
|
+
cubism_contextPrototype.cube = function(host) {
|
162
|
+
if (!arguments.length) host = "";
|
163
|
+
var source = {},
|
164
|
+
context = this;
|
165
|
+
|
166
|
+
source.metric = function(expression) {
|
167
|
+
return context.metric(function(start, stop, step, callback) {
|
168
|
+
d3.json(host + "/1.0/metric"
|
169
|
+
+ "?expression=" + encodeURIComponent(expression)
|
170
|
+
+ "&start=" + cubism_cubeFormatDate(start)
|
171
|
+
+ "&stop=" + cubism_cubeFormatDate(stop)
|
172
|
+
+ "&step=" + step, function(data) {
|
173
|
+
if (!data) return callback(new Error("unable to load data"));
|
174
|
+
callback(null, data.map(function(d) { return d.value; }));
|
175
|
+
});
|
176
|
+
}, expression += "");
|
177
|
+
};
|
178
|
+
|
179
|
+
// Returns the Cube host.
|
180
|
+
source.toString = function() {
|
181
|
+
return host;
|
182
|
+
};
|
183
|
+
|
184
|
+
return source;
|
185
|
+
};
|
186
|
+
|
187
|
+
var cubism_cubeFormatDate = d3.time.format.iso;
|
188
|
+
cubism_contextPrototype.graphite = function(host) {
|
189
|
+
if (!arguments.length) host = "";
|
190
|
+
var source = {},
|
191
|
+
context = this;
|
192
|
+
|
193
|
+
source.metric = function(expression) {
|
194
|
+
var sum = "sum";
|
195
|
+
|
196
|
+
var metric = context.metric(function(start, stop, step, callback) {
|
197
|
+
var target = expression;
|
198
|
+
|
199
|
+
// Apply the summarize, if necessary.
|
200
|
+
if (step !== 1e4) target = "summarize(" + target + ",'"
|
201
|
+
+ (!(step % 36e5) ? step / 36e5 + "hour" : !(step % 6e4) ? step / 6e4 + "min" : step / 1e3 + "sec")
|
202
|
+
+ "','" + sum + "')";
|
203
|
+
|
204
|
+
d3.text(host + "/render?format=raw"
|
205
|
+
+ "&target=" + encodeURIComponent("alias(" + target + ",'')")
|
206
|
+
+ "&from=" + cubism_graphiteFormatDate(start - 2 * step) // off-by-two?
|
207
|
+
+ "&until=" + cubism_graphiteFormatDate(stop - 1000), function(text) {
|
208
|
+
if (!text) return callback(new Error("unable to load data"));
|
209
|
+
callback(null, cubism_graphiteParse(text));
|
210
|
+
});
|
211
|
+
}, expression += "");
|
212
|
+
|
213
|
+
metric.summarize = function(_) {
|
214
|
+
sum = _;
|
215
|
+
return metric;
|
216
|
+
};
|
217
|
+
|
218
|
+
return metric;
|
219
|
+
};
|
220
|
+
|
221
|
+
source.find = function(pattern, callback) {
|
222
|
+
d3.json(host + "/metrics/find?format=completer"
|
223
|
+
+ "&query=" + encodeURIComponent(pattern), function(result) {
|
224
|
+
if (!result) return callback(new Error("unable to find metrics"));
|
225
|
+
callback(null, result.metrics.map(function(d) { return d.path; }));
|
226
|
+
});
|
227
|
+
};
|
228
|
+
|
229
|
+
// Returns the graphite host.
|
230
|
+
source.toString = function() {
|
231
|
+
return host;
|
232
|
+
};
|
233
|
+
|
234
|
+
return source;
|
235
|
+
};
|
236
|
+
|
237
|
+
// Graphite understands seconds since UNIX epoch.
|
238
|
+
function cubism_graphiteFormatDate(time) {
|
239
|
+
return Math.floor(time / 1000);
|
240
|
+
}
|
241
|
+
|
242
|
+
// Helper method for parsing graphite's raw format.
|
243
|
+
function cubism_graphiteParse(text) {
|
244
|
+
var i = text.indexOf("|"),
|
245
|
+
meta = text.substring(0, i),
|
246
|
+
c = meta.lastIndexOf(","),
|
247
|
+
b = meta.lastIndexOf(",", c - 1),
|
248
|
+
a = meta.lastIndexOf(",", b - 1),
|
249
|
+
start = meta.substring(a + 1, b) * 1000,
|
250
|
+
step = meta.substring(c + 1) * 1000;
|
251
|
+
return text
|
252
|
+
.substring(i + 1)
|
253
|
+
.split(",")
|
254
|
+
.slice(1) // the first value is always None?
|
255
|
+
.map(function(d) { return +d; });
|
256
|
+
}
|
257
|
+
function cubism_metric(context) {
|
258
|
+
if (!(context instanceof cubism_context)) throw new Error("invalid context");
|
259
|
+
this.context = context;
|
260
|
+
}
|
261
|
+
|
262
|
+
var cubism_metricPrototype = cubism_metric.prototype;
|
263
|
+
|
264
|
+
cubism.metric = cubism_metric;
|
265
|
+
|
266
|
+
cubism_metricPrototype.valueAt = function() {
|
267
|
+
return NaN;
|
268
|
+
};
|
269
|
+
|
270
|
+
cubism_metricPrototype.alias = function(name) {
|
271
|
+
this.toString = function() { return name; };
|
272
|
+
return this;
|
273
|
+
};
|
274
|
+
|
275
|
+
cubism_metricPrototype.extent = function() {
|
276
|
+
var i = 0,
|
277
|
+
n = this.context.size(),
|
278
|
+
value,
|
279
|
+
min = Infinity,
|
280
|
+
max = -Infinity;
|
281
|
+
while (++i < n) {
|
282
|
+
value = this.valueAt(i);
|
283
|
+
if (value < min) min = value;
|
284
|
+
if (value > max) max = value;
|
285
|
+
}
|
286
|
+
return [min, max];
|
287
|
+
};
|
288
|
+
|
289
|
+
cubism_metricPrototype.on = function(type, listener) {
|
290
|
+
return arguments.length < 2 ? null : this;
|
291
|
+
};
|
292
|
+
|
293
|
+
cubism_metricPrototype.shift = function() {
|
294
|
+
return this;
|
295
|
+
};
|
296
|
+
|
297
|
+
cubism_metricPrototype.on = function() {
|
298
|
+
return arguments.length < 2 ? null : this;
|
299
|
+
};
|
300
|
+
|
301
|
+
cubism_contextPrototype.metric = function(request, name) {
|
302
|
+
var context = this,
|
303
|
+
metric = new cubism_metric(context),
|
304
|
+
id = ".metric-" + ++cubism_id,
|
305
|
+
start = -Infinity,
|
306
|
+
stop,
|
307
|
+
step = context.step(),
|
308
|
+
size = context.size(),
|
309
|
+
values = [],
|
310
|
+
event = d3.dispatch("change"),
|
311
|
+
listening = 0,
|
312
|
+
fetching;
|
313
|
+
|
314
|
+
// Prefetch new data into a temporary array.
|
315
|
+
function prepare(start1, stop) {
|
316
|
+
var steps = Math.min(size, Math.round((start1 - start) / step));
|
317
|
+
if (!steps || fetching) return; // already fetched, or fetching!
|
318
|
+
fetching = true;
|
319
|
+
steps = Math.min(size, steps + cubism_metricOverlap);
|
320
|
+
var start0 = new Date(stop - steps * step);
|
321
|
+
request(start0, stop, step, function(error, data) {
|
322
|
+
fetching = false;
|
323
|
+
if (error) return console.warn(error);
|
324
|
+
var i = isFinite(start) ? Math.round((start0 - start) / step) : 0;
|
325
|
+
for (var j = 0, m = data.length; j < m; ++j) values[j + i] = data[j];
|
326
|
+
event.change.call(metric, start, stop);
|
327
|
+
});
|
328
|
+
}
|
329
|
+
|
330
|
+
// When the context changes, switch to the new data, ready-or-not!
|
331
|
+
function beforechange(start1, stop1) {
|
332
|
+
if (!isFinite(start)) start = start1;
|
333
|
+
values.splice(0, Math.max(0, Math.min(size, Math.round((start1 - start) / step))));
|
334
|
+
start = start1;
|
335
|
+
stop = stop1;
|
336
|
+
}
|
337
|
+
|
338
|
+
//
|
339
|
+
metric.valueAt = function(i) {
|
340
|
+
return values[i];
|
341
|
+
};
|
342
|
+
|
343
|
+
//
|
344
|
+
metric.shift = function(offset) {
|
345
|
+
return context.metric(cubism_metricShift(request, +offset));
|
346
|
+
};
|
347
|
+
|
348
|
+
//
|
349
|
+
metric.on = function(type, listener) {
|
350
|
+
if (!arguments.length) return event.on(type);
|
351
|
+
|
352
|
+
// If there are no listeners, then stop listening to the context,
|
353
|
+
// and avoid unnecessary fetches.
|
354
|
+
if (listener == null) {
|
355
|
+
if (event.on(type) != null && --listening == 0) {
|
356
|
+
context.on("prepare" + id, null).on("beforechange" + id, null);
|
357
|
+
}
|
358
|
+
} else {
|
359
|
+
if (event.on(type) == null && ++listening == 1) {
|
360
|
+
context.on("prepare" + id, prepare).on("beforechange" + id, beforechange);
|
361
|
+
}
|
362
|
+
}
|
363
|
+
|
364
|
+
event.on(type, listener);
|
365
|
+
|
366
|
+
// Notify the listener of the current start and stop time, as appropriate.
|
367
|
+
// This way, charts can display synchronous metrics immediately.
|
368
|
+
if (listener != null) {
|
369
|
+
if (/^change(\.|$)/.test(type)) listener.call(context, start, stop);
|
370
|
+
}
|
371
|
+
|
372
|
+
return metric;
|
373
|
+
};
|
374
|
+
|
375
|
+
//
|
376
|
+
if (arguments.length > 1) metric.toString = function() {
|
377
|
+
return name;
|
378
|
+
};
|
379
|
+
|
380
|
+
return metric;
|
381
|
+
};
|
382
|
+
|
383
|
+
// Number of metric to refetch each period, in case of lag.
|
384
|
+
var cubism_metricOverlap = 6;
|
385
|
+
|
386
|
+
// Wraps the specified request implementation, and shifts time by the given offset.
|
387
|
+
function cubism_metricShift(request, offset) {
|
388
|
+
return function(start, stop, step, callback) {
|
389
|
+
request(new Date(+start + offset), new Date(+stop + offset), step, callback);
|
390
|
+
};
|
391
|
+
}
|
392
|
+
function cubism_metricConstant(context, value) {
|
393
|
+
cubism_metric.call(this, context);
|
394
|
+
value = +value;
|
395
|
+
var name = value + "";
|
396
|
+
this.valueOf = function() { return value; };
|
397
|
+
this.toString = function() { return name; };
|
398
|
+
}
|
399
|
+
|
400
|
+
var cubism_metricConstantPrototype = cubism_metricConstant.prototype = Object.create(cubism_metric.prototype);
|
401
|
+
|
402
|
+
cubism_metricConstantPrototype.valueAt = function() {
|
403
|
+
return +this;
|
404
|
+
};
|
405
|
+
|
406
|
+
cubism_metricConstantPrototype.extent = function() {
|
407
|
+
return [+this, +this];
|
408
|
+
};
|
409
|
+
function cubism_metricOperator(name, operate) {
|
410
|
+
|
411
|
+
function cubism_metricOperator(left, right) {
|
412
|
+
if (!(right instanceof cubism_metric)) right = new cubism_metricConstant(left.context, right);
|
413
|
+
else if (left.context !== right.context) throw new Error("mismatch context");
|
414
|
+
cubism_metric.call(this, left.context);
|
415
|
+
this.left = left;
|
416
|
+
this.right = right;
|
417
|
+
this.toString = function() { return left + " " + name + " " + right; };
|
418
|
+
}
|
419
|
+
|
420
|
+
var cubism_metricOperatorPrototype = cubism_metricOperator.prototype = Object.create(cubism_metric.prototype);
|
421
|
+
|
422
|
+
cubism_metricOperatorPrototype.valueAt = function(i) {
|
423
|
+
return operate(this.left.valueAt(i), this.right.valueAt(i));
|
424
|
+
};
|
425
|
+
|
426
|
+
cubism_metricOperatorPrototype.shift = function(offset) {
|
427
|
+
return new cubism_metricOperator(this.left.shift(offset), this.right.shift(offset));
|
428
|
+
};
|
429
|
+
|
430
|
+
cubism_metricOperatorPrototype.on = function(type, listener) {
|
431
|
+
if (arguments.length < 2) return this.left.on(type);
|
432
|
+
this.left.on(type, listener);
|
433
|
+
this.right.on(type, listener);
|
434
|
+
return this;
|
435
|
+
};
|
436
|
+
|
437
|
+
return function(right) {
|
438
|
+
return new cubism_metricOperator(this, right);
|
439
|
+
};
|
440
|
+
}
|
441
|
+
|
442
|
+
cubism_metricPrototype.add = cubism_metricOperator("+", function(left, right) {
|
443
|
+
return left + right;
|
444
|
+
});
|
445
|
+
|
446
|
+
cubism_metricPrototype.subtract = cubism_metricOperator("-", function(left, right) {
|
447
|
+
return left - right;
|
448
|
+
});
|
449
|
+
|
450
|
+
cubism_metricPrototype.multiply = cubism_metricOperator("*", function(left, right) {
|
451
|
+
return left * right;
|
452
|
+
});
|
453
|
+
|
454
|
+
cubism_metricPrototype.divide = cubism_metricOperator("/", function(left, right) {
|
455
|
+
return left / right;
|
456
|
+
});
|
457
|
+
cubism_contextPrototype.horizon = function() {
|
458
|
+
var context = this,
|
459
|
+
mode = "offset",
|
460
|
+
buffer = document.createElement("canvas"),
|
461
|
+
width = buffer.width = context.size(),
|
462
|
+
height = buffer.height = 30,
|
463
|
+
scale = d3.scale.linear().interpolate(d3.interpolateRound),
|
464
|
+
metric = cubism_identity,
|
465
|
+
extent = null,
|
466
|
+
title = cubism_identity,
|
467
|
+
format = d3.format(".2s"),
|
468
|
+
colors = ["#08519c","#3182bd","#6baed6","#bdd7e7","#bae4b3","#74c476","#31a354","#006d2c"];
|
469
|
+
|
470
|
+
function horizon(selection) {
|
471
|
+
|
472
|
+
selection
|
473
|
+
.on("mousemove.horizon", function() { context.focus(Math.round(d3.mouse(this)[0])); })
|
474
|
+
.on("mouseout.horizon", function() { context.focus(null); });
|
475
|
+
|
476
|
+
selection.append("canvas")
|
477
|
+
.attr("width", width)
|
478
|
+
.attr("height", height);
|
479
|
+
|
480
|
+
selection.append("span")
|
481
|
+
.attr("class", "title")
|
482
|
+
.text(title);
|
483
|
+
|
484
|
+
selection.append("span")
|
485
|
+
.attr("class", "value");
|
486
|
+
|
487
|
+
selection.each(function(d, i) {
|
488
|
+
var that = this,
|
489
|
+
id = ++cubism_id,
|
490
|
+
metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric,
|
491
|
+
colors_ = typeof colors === "function" ? colors.call(that, d, i) : colors,
|
492
|
+
extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent,
|
493
|
+
start = -Infinity,
|
494
|
+
step = context.step(),
|
495
|
+
canvas = d3.select(that).select("canvas"),
|
496
|
+
span = d3.select(that).select(".value"),
|
497
|
+
max_,
|
498
|
+
m = colors_.length >> 1,
|
499
|
+
ready;
|
500
|
+
|
501
|
+
canvas.datum({id: id, metric: metric_});
|
502
|
+
canvas = canvas.node().getContext("2d");
|
503
|
+
|
504
|
+
function change(start1, stop) {
|
505
|
+
canvas.save();
|
506
|
+
|
507
|
+
// compute the new extent and ready flag
|
508
|
+
var extent = metric_.extent();
|
509
|
+
ready = extent.every(isFinite);
|
510
|
+
if (extent_ != null) extent = extent_;
|
511
|
+
|
512
|
+
// if this is an update (with no extent change), copy old values!
|
513
|
+
var i0 = 0, max = Math.max(-extent[0], extent[1]);
|
514
|
+
if (this === context) {
|
515
|
+
if (max == max_) {
|
516
|
+
i0 = width - cubism_metricOverlap;
|
517
|
+
var dx = (start1 - start) / step;
|
518
|
+
if (dx < width) {
|
519
|
+
var canvas0 = buffer.getContext("2d");
|
520
|
+
canvas0.clearRect(0, 0, width, height);
|
521
|
+
canvas0.drawImage(canvas.canvas, dx, 0, width - dx, height, 0, 0, width - dx, height);
|
522
|
+
canvas.clearRect(0, 0, width, height);
|
523
|
+
canvas.drawImage(canvas0.canvas, 0, 0);
|
524
|
+
}
|
525
|
+
}
|
526
|
+
start = start1;
|
527
|
+
}
|
528
|
+
|
529
|
+
// update the domain
|
530
|
+
scale.domain([0, max_ = max]);
|
531
|
+
|
532
|
+
// clear for the new data
|
533
|
+
canvas.clearRect(i0, 0, width - i0, height);
|
534
|
+
|
535
|
+
// record whether there are negative values to display
|
536
|
+
var negative;
|
537
|
+
|
538
|
+
// positive bands
|
539
|
+
for (var j = 0; j < m; ++j) {
|
540
|
+
canvas.fillStyle = colors_[m + j];
|
541
|
+
|
542
|
+
// Adjust the range based on the current band index.
|
543
|
+
var y0 = (j - m + 1) * height;
|
544
|
+
scale.range([m * height + y0, y0]);
|
545
|
+
y0 = scale(0);
|
546
|
+
|
547
|
+
for (var i = i0, n = width, y1; i < n; ++i) {
|
548
|
+
y1 = metric_.valueAt(i);
|
549
|
+
if (y1 <= 0) { negative = true; continue; }
|
550
|
+
canvas.fillRect(i, y1 = scale(y1), 1, y0 - y1);
|
551
|
+
}
|
552
|
+
}
|
553
|
+
|
554
|
+
if (negative) {
|
555
|
+
// enable offset mode
|
556
|
+
if (mode === "offset") {
|
557
|
+
canvas.translate(0, height);
|
558
|
+
canvas.scale(1, -1);
|
559
|
+
}
|
560
|
+
|
561
|
+
// negative bands
|
562
|
+
for (var j = 0; j < m; ++j) {
|
563
|
+
canvas.fillStyle = colors_[m - 1 - j];
|
564
|
+
|
565
|
+
// Adjust the range based on the current band index.
|
566
|
+
var y0 = (j - m + 1) * height;
|
567
|
+
scale.range([m * height + y0, y0]);
|
568
|
+
y0 = scale(0);
|
569
|
+
|
570
|
+
for (var i = i0, n = width, y1; i < n; ++i) {
|
571
|
+
y1 = metric_.valueAt(i);
|
572
|
+
if (y1 >= 0) continue;
|
573
|
+
canvas.fillRect(i, scale(-y1), 1, y0 - scale(-y1));
|
574
|
+
}
|
575
|
+
}
|
576
|
+
}
|
577
|
+
|
578
|
+
canvas.restore();
|
579
|
+
}
|
580
|
+
|
581
|
+
function focus(i) {
|
582
|
+
if (i == null) i = width - 1;
|
583
|
+
var value = metric_.valueAt(i);
|
584
|
+
span.datum(value).text(isNaN(value) ? null : format);
|
585
|
+
}
|
586
|
+
|
587
|
+
// Update the chart when the context changes.
|
588
|
+
context.on("change.horizon-" + id, change);
|
589
|
+
context.on("focus.horizon-" + id, focus);
|
590
|
+
|
591
|
+
// Display the first metric change immediately,
|
592
|
+
// but defer subsequent updates to the canvas change.
|
593
|
+
// Note that someone still needs to listen to the metric,
|
594
|
+
// so that it continues to update automatically.
|
595
|
+
metric_.on("change.horizon-" + id, function(start, stop) {
|
596
|
+
change(start, stop), focus();
|
597
|
+
if (ready) metric_.on("change.horizon-" + id, cubism_identity);
|
598
|
+
});
|
599
|
+
});
|
600
|
+
}
|
601
|
+
|
602
|
+
horizon.remove = function(selection) {
|
603
|
+
|
604
|
+
selection
|
605
|
+
.on("mousemove.horizon", null)
|
606
|
+
.on("mouseout.horizon", null);
|
607
|
+
|
608
|
+
selection.selectAll("canvas")
|
609
|
+
.each(remove)
|
610
|
+
.remove();
|
611
|
+
|
612
|
+
selection.selectAll(".title,.value")
|
613
|
+
.remove();
|
614
|
+
|
615
|
+
function remove(d) {
|
616
|
+
d.metric.on("change.horizon-" + d.id, null);
|
617
|
+
context.on("change.horizon-" + d.id, null);
|
618
|
+
context.on("focus.horizon-" + d.id, null);
|
619
|
+
}
|
620
|
+
};
|
621
|
+
|
622
|
+
horizon.mode = function(_) {
|
623
|
+
if (!arguments.length) return mode;
|
624
|
+
mode = _ + "";
|
625
|
+
return horizon;
|
626
|
+
};
|
627
|
+
|
628
|
+
horizon.height = function(_) {
|
629
|
+
if (!arguments.length) return height;
|
630
|
+
buffer.height = height = +_;
|
631
|
+
return horizon;
|
632
|
+
};
|
633
|
+
|
634
|
+
horizon.metric = function(_) {
|
635
|
+
if (!arguments.length) return metric;
|
636
|
+
metric = _;
|
637
|
+
return horizon;
|
638
|
+
};
|
639
|
+
|
640
|
+
horizon.scale = function(_) {
|
641
|
+
if (!arguments.length) return scale;
|
642
|
+
scale = _;
|
643
|
+
return horizon;
|
644
|
+
};
|
645
|
+
|
646
|
+
horizon.extent = function(_) {
|
647
|
+
if (!arguments.length) return extent;
|
648
|
+
extent = _;
|
649
|
+
return horizon;
|
650
|
+
};
|
651
|
+
|
652
|
+
horizon.title = function(_) {
|
653
|
+
if (!arguments.length) return title;
|
654
|
+
title = _;
|
655
|
+
return horizon;
|
656
|
+
};
|
657
|
+
|
658
|
+
horizon.format = function(_) {
|
659
|
+
if (!arguments.length) return format;
|
660
|
+
format = _;
|
661
|
+
return horizon;
|
662
|
+
};
|
663
|
+
|
664
|
+
horizon.colors = function(_) {
|
665
|
+
if (!arguments.length) return colors;
|
666
|
+
colors = _;
|
667
|
+
return horizon;
|
668
|
+
};
|
669
|
+
|
670
|
+
return horizon;
|
671
|
+
};
|
672
|
+
cubism_contextPrototype.comparison = function() {
|
673
|
+
var context = this,
|
674
|
+
width = context.size(),
|
675
|
+
height = 120,
|
676
|
+
scale = d3.scale.linear().interpolate(d3.interpolateRound),
|
677
|
+
primary = function(d) { return d[0]; },
|
678
|
+
secondary = function(d) { return d[1]; },
|
679
|
+
extent = null,
|
680
|
+
title = cubism_identity,
|
681
|
+
formatPrimary = cubism_comparisonPrimaryFormat,
|
682
|
+
formatChange = cubism_comparisonChangeFormat,
|
683
|
+
colors = ["#9ecae1", "#225b84", "#a1d99b", "#22723a"],
|
684
|
+
strokeWidth = 1.5;
|
685
|
+
|
686
|
+
function comparison(selection) {
|
687
|
+
|
688
|
+
selection
|
689
|
+
.on("mousemove.comparison", function() { context.focus(Math.round(d3.mouse(this)[0])); })
|
690
|
+
.on("mouseout.comparison", function() { context.focus(null); });
|
691
|
+
|
692
|
+
selection.append("canvas")
|
693
|
+
.attr("width", width)
|
694
|
+
.attr("height", height);
|
695
|
+
|
696
|
+
selection.append("span")
|
697
|
+
.attr("class", "title")
|
698
|
+
.text(title);
|
699
|
+
|
700
|
+
selection.append("span")
|
701
|
+
.attr("class", "value primary");
|
702
|
+
|
703
|
+
selection.append("span")
|
704
|
+
.attr("class", "value change");
|
705
|
+
|
706
|
+
selection.each(function(d, i) {
|
707
|
+
var that = this,
|
708
|
+
id = ++cubism_id,
|
709
|
+
primary_ = typeof primary === "function" ? primary.call(that, d, i) : primary,
|
710
|
+
secondary_ = typeof secondary === "function" ? secondary.call(that, d, i) : secondary,
|
711
|
+
extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent,
|
712
|
+
div = d3.select(that),
|
713
|
+
canvas = div.select("canvas"),
|
714
|
+
spanPrimary = div.select(".value.primary"),
|
715
|
+
spanChange = div.select(".value.change"),
|
716
|
+
ready;
|
717
|
+
|
718
|
+
canvas.datum({id: id, primary: primary_, secondary: secondary_});
|
719
|
+
canvas = canvas.node().getContext("2d");
|
720
|
+
|
721
|
+
function change(start, stop) {
|
722
|
+
canvas.save();
|
723
|
+
canvas.clearRect(0, 0, width, height);
|
724
|
+
|
725
|
+
// update the scale
|
726
|
+
var primaryExtent = primary_.extent(),
|
727
|
+
secondaryExtent = secondary_.extent(),
|
728
|
+
extent = extent_ == null ? primaryExtent : extent_;
|
729
|
+
scale.domain(extent).range([height, 0]);
|
730
|
+
ready = primaryExtent.concat(secondaryExtent).every(isFinite);
|
731
|
+
|
732
|
+
// consistent overplotting
|
733
|
+
var round = start / context.step() & 1
|
734
|
+
? cubism_comparisonRoundOdd
|
735
|
+
: cubism_comparisonRoundEven;
|
736
|
+
|
737
|
+
// positive changes
|
738
|
+
canvas.fillStyle = colors[2];
|
739
|
+
for (var i = 0, n = width; i < n; ++i) {
|
740
|
+
var y0 = scale(primary_.valueAt(i)),
|
741
|
+
y1 = scale(secondary_.valueAt(i));
|
742
|
+
if (y0 < y1) canvas.fillRect(round(i), y0, 1, y1 - y0);
|
743
|
+
}
|
744
|
+
|
745
|
+
// negative changes
|
746
|
+
canvas.fillStyle = colors[0];
|
747
|
+
for (i = 0; i < n; ++i) {
|
748
|
+
var y0 = scale(primary_.valueAt(i)),
|
749
|
+
y1 = scale(secondary_.valueAt(i));
|
750
|
+
if (y0 > y1) canvas.fillRect(round(i), y1, 1, y0 - y1);
|
751
|
+
}
|
752
|
+
|
753
|
+
// positive values
|
754
|
+
canvas.fillStyle = colors[3];
|
755
|
+
for (i = 0; i < n; ++i) {
|
756
|
+
var y0 = scale(primary_.valueAt(i)),
|
757
|
+
y1 = scale(secondary_.valueAt(i));
|
758
|
+
if (y0 <= y1) canvas.fillRect(round(i), y0, 1, strokeWidth);
|
759
|
+
}
|
760
|
+
|
761
|
+
// negative values
|
762
|
+
canvas.fillStyle = colors[1];
|
763
|
+
for (i = 0; i < n; ++i) {
|
764
|
+
var y0 = scale(primary_.valueAt(i)),
|
765
|
+
y1 = scale(secondary_.valueAt(i));
|
766
|
+
if (y0 > y1) canvas.fillRect(round(i), y0 - strokeWidth, 1, strokeWidth);
|
767
|
+
}
|
768
|
+
|
769
|
+
canvas.restore();
|
770
|
+
}
|
771
|
+
|
772
|
+
function focus(i) {
|
773
|
+
if (i == null) i = width - 1;
|
774
|
+
var valuePrimary = primary_.valueAt(i),
|
775
|
+
valueSecondary = secondary_.valueAt(i),
|
776
|
+
valueChange = (valuePrimary - valueSecondary) / valueSecondary;
|
777
|
+
|
778
|
+
spanPrimary
|
779
|
+
.datum(valuePrimary)
|
780
|
+
.text(isNaN(valuePrimary) ? null : formatPrimary);
|
781
|
+
|
782
|
+
spanChange
|
783
|
+
.datum(valueChange)
|
784
|
+
.text(isNaN(valueChange) ? null : formatChange)
|
785
|
+
.attr("class", "value change " + (valueChange > 0 ? "positive" : valueChange < 0 ? "negative" : ""));
|
786
|
+
}
|
787
|
+
|
788
|
+
// Display the first primary change immediately,
|
789
|
+
// but defer subsequent updates to the context change.
|
790
|
+
// Note that someone still needs to listen to the metric,
|
791
|
+
// so that it continues to update automatically.
|
792
|
+
primary_.on("change.comparison-" + id, firstChange);
|
793
|
+
secondary_.on("change.comparison-" + id, firstChange);
|
794
|
+
function firstChange(start, stop) {
|
795
|
+
change(start, stop), focus();
|
796
|
+
if (ready) {
|
797
|
+
primary_.on("change.comparison-" + id, cubism_identity);
|
798
|
+
secondary_.on("change.comparison-" + id, cubism_identity);
|
799
|
+
}
|
800
|
+
}
|
801
|
+
|
802
|
+
// Update the chart when the context changes.
|
803
|
+
context.on("change.comparison-" + id, change);
|
804
|
+
context.on("focus.comparison-" + id, focus);
|
805
|
+
});
|
806
|
+
}
|
807
|
+
|
808
|
+
comparison.remove = function(selection) {
|
809
|
+
|
810
|
+
selection
|
811
|
+
.on("mousemove.comparison", null)
|
812
|
+
.on("mouseout.comparison", null);
|
813
|
+
|
814
|
+
selection.selectAll("canvas")
|
815
|
+
.each(remove)
|
816
|
+
.remove();
|
817
|
+
|
818
|
+
selection.selectAll(".title,.value")
|
819
|
+
.remove();
|
820
|
+
|
821
|
+
function remove(d) {
|
822
|
+
d.primary.on("change.comparison-" + d.id, null);
|
823
|
+
d.secondary.on("change.comparison-" + d.id, null);
|
824
|
+
context.on("change.comparison-" + d.id, null);
|
825
|
+
context.on("focus.comparison-" + d.id, null);
|
826
|
+
}
|
827
|
+
};
|
828
|
+
|
829
|
+
comparison.height = function(_) {
|
830
|
+
if (!arguments.length) return height;
|
831
|
+
height = +_;
|
832
|
+
return comparison;
|
833
|
+
};
|
834
|
+
|
835
|
+
comparison.primary = function(_) {
|
836
|
+
if (!arguments.length) return primary;
|
837
|
+
primary = _;
|
838
|
+
return comparison;
|
839
|
+
};
|
840
|
+
|
841
|
+
comparison.secondary = function(_) {
|
842
|
+
if (!arguments.length) return secondary;
|
843
|
+
secondary = _;
|
844
|
+
return comparison;
|
845
|
+
};
|
846
|
+
|
847
|
+
comparison.scale = function(_) {
|
848
|
+
if (!arguments.length) return scale;
|
849
|
+
scale = _;
|
850
|
+
return comparison;
|
851
|
+
};
|
852
|
+
|
853
|
+
comparison.extent = function(_) {
|
854
|
+
if (!arguments.length) return extent;
|
855
|
+
extent = _;
|
856
|
+
return comparison;
|
857
|
+
};
|
858
|
+
|
859
|
+
comparison.title = function(_) {
|
860
|
+
if (!arguments.length) return title;
|
861
|
+
title = _;
|
862
|
+
return comparison;
|
863
|
+
};
|
864
|
+
|
865
|
+
comparison.formatPrimary = function(_) {
|
866
|
+
if (!arguments.length) return formatPrimary;
|
867
|
+
formatPrimary = _;
|
868
|
+
return comparison;
|
869
|
+
};
|
870
|
+
|
871
|
+
comparison.formatChange = function(_) {
|
872
|
+
if (!arguments.length) return formatChange;
|
873
|
+
formatChange = _;
|
874
|
+
return comparison;
|
875
|
+
};
|
876
|
+
|
877
|
+
comparison.colors = function(_) {
|
878
|
+
if (!arguments.length) return colors;
|
879
|
+
colors = _;
|
880
|
+
return comparison;
|
881
|
+
};
|
882
|
+
|
883
|
+
comparison.strokeWidth = function(_) {
|
884
|
+
if (!arguments.length) return strokeWidth;
|
885
|
+
strokeWidth = _;
|
886
|
+
return comparison;
|
887
|
+
};
|
888
|
+
|
889
|
+
return comparison;
|
890
|
+
};
|
891
|
+
|
892
|
+
var cubism_comparisonPrimaryFormat = d3.format(".2s"),
|
893
|
+
cubism_comparisonChangeFormat = d3.format("+.0%");
|
894
|
+
|
895
|
+
function cubism_comparisonRoundEven(i) {
|
896
|
+
return i & 0xfffffe;
|
897
|
+
}
|
898
|
+
|
899
|
+
function cubism_comparisonRoundOdd(i) {
|
900
|
+
return ((i + 1) & 0xfffffe) - 1;
|
901
|
+
}
|
902
|
+
cubism_contextPrototype.axis = function() {
|
903
|
+
var context = this,
|
904
|
+
scale = context.scale,
|
905
|
+
axis_ = d3.svg.axis().scale(scale);
|
906
|
+
|
907
|
+
var format = context.step() < 6e4 ? cubism_axisFormatSeconds
|
908
|
+
: context.step() < 864e5 ? cubism_axisFormatMinutes
|
909
|
+
: cubism_axisFormatDays;
|
910
|
+
|
911
|
+
function axis(selection) {
|
912
|
+
var id = ++cubism_id,
|
913
|
+
tick;
|
914
|
+
|
915
|
+
var g = selection.append("svg")
|
916
|
+
.datum({id: id})
|
917
|
+
.attr("width", context.size())
|
918
|
+
.attr("height", Math.max(28, -axis.tickSize()))
|
919
|
+
.append("g")
|
920
|
+
.attr("transform", "translate(0," + (axis_.orient() === "top" ? 27 : 4) + ")")
|
921
|
+
.call(axis_);
|
922
|
+
|
923
|
+
context.on("change.axis-" + id, function() {
|
924
|
+
g.call(axis_);
|
925
|
+
if (!tick) tick = d3.select(g.node().appendChild(g.selectAll("text").node().cloneNode(true)))
|
926
|
+
.style("display", "none")
|
927
|
+
.text(null);
|
928
|
+
});
|
929
|
+
|
930
|
+
context.on("focus.axis-" + id, function(i) {
|
931
|
+
if (tick) {
|
932
|
+
if (i == null) {
|
933
|
+
tick.style("display", "none");
|
934
|
+
g.selectAll("text").style("fill-opacity", null);
|
935
|
+
} else {
|
936
|
+
tick.style("display", null).attr("x", i).text(format(scale.invert(i)));
|
937
|
+
var dx = tick.node().getComputedTextLength() + 6;
|
938
|
+
g.selectAll("text").style("fill-opacity", function(d) { return Math.abs(scale(d) - i) < dx ? 0 : 1; });
|
939
|
+
}
|
940
|
+
}
|
941
|
+
});
|
942
|
+
}
|
943
|
+
|
944
|
+
axis.remove = function(selection) {
|
945
|
+
|
946
|
+
selection.selectAll("svg")
|
947
|
+
.each(remove)
|
948
|
+
.remove();
|
949
|
+
|
950
|
+
function remove(d) {
|
951
|
+
context.on("change.axis-" + d.id, null);
|
952
|
+
context.on("focus.axis-" + d.id, null);
|
953
|
+
}
|
954
|
+
};
|
955
|
+
|
956
|
+
return d3.rebind(axis, axis_,
|
957
|
+
"orient",
|
958
|
+
"ticks",
|
959
|
+
"tickSubdivide",
|
960
|
+
"tickSize",
|
961
|
+
"tickPadding",
|
962
|
+
"tickFormat");
|
963
|
+
};
|
964
|
+
|
965
|
+
var cubism_axisFormatSeconds = d3.time.format("%I:%M:%S %p"),
|
966
|
+
cubism_axisFormatMinutes = d3.time.format("%I:%M %p"),
|
967
|
+
cubism_axisFormatDays = d3.time.format("%B %d");
|
968
|
+
cubism_contextPrototype.rule = function() {
|
969
|
+
var context = this,
|
970
|
+
metric = cubism_identity;
|
971
|
+
|
972
|
+
function rule(selection) {
|
973
|
+
var id = ++cubism_id;
|
974
|
+
|
975
|
+
var line = selection.append("div")
|
976
|
+
.datum({id: id})
|
977
|
+
.attr("class", "line")
|
978
|
+
.call(cubism_ruleStyle);
|
979
|
+
|
980
|
+
selection.each(function(d, i) {
|
981
|
+
var that = this,
|
982
|
+
id = ++cubism_id,
|
983
|
+
metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric;
|
984
|
+
|
985
|
+
if (!metric_) return;
|
986
|
+
|
987
|
+
function change(start, stop) {
|
988
|
+
var values = [];
|
989
|
+
|
990
|
+
for (var i = 0, n = context.size(); i < n; ++i) {
|
991
|
+
if (metric_.valueAt(i)) {
|
992
|
+
values.push(i);
|
993
|
+
}
|
994
|
+
}
|
995
|
+
|
996
|
+
var lines = selection.selectAll(".metric").data(values);
|
997
|
+
lines.exit().remove();
|
998
|
+
lines.enter().append("div").attr("class", "metric line").call(cubism_ruleStyle);
|
999
|
+
lines.style("left", cubism_ruleLeft);
|
1000
|
+
}
|
1001
|
+
|
1002
|
+
context.on("change.rule-" + id, change);
|
1003
|
+
metric_.on("change.rule-" + id, change);
|
1004
|
+
});
|
1005
|
+
|
1006
|
+
context.on("focus.rule-" + id, function(i) {
|
1007
|
+
line.datum(i)
|
1008
|
+
.style("display", i == null ? "none" : null)
|
1009
|
+
.style("left", i == null ? null : cubism_ruleLeft);
|
1010
|
+
});
|
1011
|
+
}
|
1012
|
+
|
1013
|
+
rule.remove = function(selection) {
|
1014
|
+
|
1015
|
+
selection.selectAll(".line")
|
1016
|
+
.each(remove)
|
1017
|
+
.remove();
|
1018
|
+
|
1019
|
+
function remove(d) {
|
1020
|
+
context.on("focus.rule-" + d.id, null);
|
1021
|
+
}
|
1022
|
+
};
|
1023
|
+
|
1024
|
+
rule.metric = function(_) {
|
1025
|
+
if (!arguments.length) return metric;
|
1026
|
+
metric = _;
|
1027
|
+
return rule;
|
1028
|
+
};
|
1029
|
+
|
1030
|
+
return rule;
|
1031
|
+
};
|
1032
|
+
|
1033
|
+
function cubism_ruleStyle(line) {
|
1034
|
+
line
|
1035
|
+
.style("position", "absolute")
|
1036
|
+
.style("top", 0)
|
1037
|
+
.style("bottom", 0)
|
1038
|
+
.style("width", "1px")
|
1039
|
+
.style("pointer-events", "none");
|
1040
|
+
}
|
1041
|
+
|
1042
|
+
function cubism_ruleLeft(i) {
|
1043
|
+
return i + "px";
|
1044
|
+
}
|
1045
|
+
})(this);
|
@@ -0,0 +1 @@
|
|
1
|
+
(function(a){function d(a){return a}function e(){}function h(a){return Math.floor(a/1e3)}function i(a){var b=a.indexOf("|"),c=a.substring(0,b),d=c.lastIndexOf(","),e=c.lastIndexOf(",",d-1),f=c.lastIndexOf(",",e-1),g=c.substring(f+1,e)*1e3,h=c.substring(d+1)*1e3;return a.substring(b+1).split(",").slice(1).map(function(a){return+a})}function j(a){if(!(a instanceof e))throw new Error("invalid context");this.context=a}function m(a,b){return function(c,d,e,f){a(new Date(+c+b),new Date(+d+b),e,f)}}function n(a,b){j.call(this,a),b=+b;var c=b+"";this.valueOf=function(){return b},this.toString=function(){return c}}function p(a,b){function c(b,c){if(c instanceof j){if(b.context!==c.context)throw new Error("mismatch context")}else c=new n(b.context,c);j.call(this,b.context),this.left=b,this.right=c,this.toString=function(){return b+" "+a+" "+c}}var d=c.prototype=Object.create(j.prototype);return d.valueAt=function(a){return b(this.left.valueAt(a),this.right.valueAt(a))},d.shift=function(a){return new c(this.left.shift(a),this.right.shift(a))},d.on=function(a,b){return arguments.length<2?this.left.on(a):(this.left.on(a,b),this.right.on(a,b),this)},function(a){return new c(this,a)}}function s(a){return a&16777214}function t(a){return(a+1&16777214)-1}function x(a){a.style("position","absolute").style("top",0).style("bottom",0).style("width","1px").style("pointer-events","none")}function y(a){return a+"px"}var b=a.cubism={version:"1.2.2"},c=0;b.option=function(a,c){var d=b.options(a);return d.length?d[0]:c},b.options=function(a,b){var c=location.search.substring(1).split("&"),d=[],e=-1,f=c.length,g;while(++e<f)(g=c[e].split("="))[0]==a&&d.push(decodeURIComponent(g[1]));return d.length||arguments.length<2?d:b},b.context=function(){function p(){var c=Date.now();return g=new Date(Math.floor((c-j-k)/b)*b),f=new Date(g-d*b),i=new Date(Math.floor((c-j)/b)*b),h=new Date(i-d*b),m.domain([f,g]),a}var a=new e,b=1e4,d=1440,f,g,h,i,j=5e3,k=5e3,l=d3.dispatch("prepare","beforechange","change","focus"),m=a.scale=d3.time.scale().range([0,d]),n,o;return a.start=function(){n&&clearTimeout(n);var c=+i+j-Date.now();return c<k&&(c+=b),n=setTimeout(function e(){i=new Date(Math.floor((Date.now()-j)/b)*b),h=new Date(i-d*b),l.prepare.call(a,h,i),setTimeout(function(){m.domain([f=h,g=i]),l.beforechange.call(a,h,i),l.change.call(a,h,i),l.focus.call(a,o)},k),n=setTimeout(e,b)},c),a},a.stop=function(){return n=clearTimeout(n),a},n=setTimeout(a.start,10),a.step=function(a){return arguments.length?(b=+a,p()):b},a.size=function(a){return arguments.length?(m.range([0,d=+a]),p()):d},a.serverDelay=function(a){return arguments.length?(j=+a,p()):j},a.clientDelay=function(a){return arguments.length?(k=+a,p()):k},a.focus=function(b){return l.focus.call(a,o=b),a},a.on=function(b,c){return arguments.length<2?l.on(b):(l.on(b,c),c!=null&&(/^prepare(\.|$)/.test(b)&&c.call(a,h,i),/^beforechange(\.|$)/.test(b)&&c.call(a,f,g),/^change(\.|$)/.test(b)&&c.call(a,f,g),/^focus(\.|$)/.test(b)&&c.call(a,o)),a)},d3.select(window).on("keydown.context-"+ ++c,function(){switch(!d3.event.metaKey&&d3.event.keyCode){case 37:o==null&&(o=d-1),o>0&&a.focus(--o);break;case 39:o==null&&(o=d-2),o<d-1&&a.focus(++o);break;default:return}d3.event.preventDefault()}),p()};var f=b.context.prototype=e.prototype;f.constant=function(a){return new n(this,+a)},f.cube=function(a){arguments.length||(a="");var b={},c=this;return b.metric=function(b){return c.metric(function(c,d,e,f){d3.json(a+"/1.0/metric"+"?expression="+encodeURIComponent(b)+"&start="+g(c)+"&stop="+g(d)+"&step="+e,function(a){if(!a)return f(new Error("unable to load data"));f(null,a.map(function(a){return a.value}))})},b+="")},b.toString=function(){return a},b};var g=d3.time.format.iso;f.graphite=function(a){arguments.length||(a="");var b={},c=this;return b.metric=function(b){var d="sum",e=c.metric(function(c,e,f,g){var j=b;f!==1e4&&(j="summarize("+j+",'"+(f%36e5?f%6e4?f/1e3+"sec":f/6e4+"min":f/36e5+"hour")+"','"+d+"')"),d3.text(a+"/render?format=raw"+"&target="+encodeURIComponent("alias("+j+",'')")+"&from="+h(c-2*f)+"&until="+h(e-1e3),function(a){if(!a)return g(new Error("unable to load data"));g(null,i(a))})},b+="");return e.summarize=function(a){return d=a,e},e},b.find=function(b,c){d3.json(a+"/metrics/find?format=completer"+"&query="+encodeURIComponent(b),function(a){if(!a)return c(new Error("unable to find metrics"));c(null,a.metrics.map(function(a){return a.path}))})},b.toString=function(){return a},b};var k=j.prototype;b.metric=j,k.valueAt=function(){return NaN},k.alias=function(a){return this.toString=function(){return a},this},k.extent=function(){var a=0,b=this.context.size(),c,d=Infinity,e=-Infinity;while(++a<b)c=this.valueAt(a),c<d&&(d=c),c>e&&(e=c);return[d,e]},k.on=function(a,b){return arguments.length<2?null:this},k.shift=function(){return this},k.on=function(){return arguments.length<2?null:this},f.metric=function(a,b){function r(b,c){var d=Math.min(k,Math.round((b-g)/i));if(!d||q)return;q=!0,d=Math.min(k,d+l);var f=new Date(c-d*i);a(f,c,i,function(a,b){q=!1;if(a)return console.warn(a);var d=isFinite(g)?Math.round((f-g)/i):0;for(var h=0,j=b.length;h<j;++h)n[h+d]=b[h];o.change.call(e,g,c)})}function s(a,b){isFinite(g)||(g=a),n.splice(0,Math.max(0,Math.min(k,Math.round((a-g)/i)))),g=a,h=b}var d=this,e=new j(d),f=".metric-"+ ++c,g=-Infinity,h,i=d.step(),k=d.size(),n=[],o=d3.dispatch("change"),p=0,q;return e.valueAt=function(a){return n[a]},e.shift=function(b){return d.metric(m(a,+b))},e.on=function(a,b){return arguments.length?(b==null?o.on(a)!=null&&--p==0&&d.on("prepare"+f,null).on("beforechange"+f,null):o.on(a)==null&&++p==1&&d.on("prepare"+f,r).on("beforechange"+f,s),o.on(a,b),b!=null&&/^change(\.|$)/.test(a)&&b.call(d,g,h),e):o.on(a)},arguments.length>1&&(e.toString=function(){return b}),e};var l=6,o=n.prototype=Object.create(j.prototype);o.valueAt=function(){return+this},o.extent=function(){return[+this,+this]},k.add=p("+",function(a,b){return a+b}),k.subtract=p("-",function(a,b){return a-b}),k.multiply=p("*",function(a,b){return a*b}),k.divide=p("/",function(a,b){return a/b}),f.horizon=function(){function o(o){o.on("mousemove.horizon",function(){a.focus(Math.round(d3.mouse(this)[0]))}).on("mouseout.horizon",function(){a.focus(null)}),o.append("canvas").attr("width",f).attr("height",g),o.append("span").attr("class","title").text(k),o.append("span").attr("class","value"),o.each(function(k,o){function B(c,d){w.save();var i=r.extent();A=i.every(isFinite),t!=null&&(i=t);var j=0,k=Math.max(-i[0],i[1]);if(this===a){if(k==y){j=f-l;var m=(c-u)/v;if(m<f){var n=e.getContext("2d");n.clearRect(0,0,f,g),n.drawImage(w.canvas,m,0,f-m,g,0,0,f-m,g),w.clearRect(0,0,f,g),w.drawImage(n.canvas,0,0)}}u=c}h.domain([0,y=k]),w.clearRect(j,0,f-j,g);var o;for(var p=0;p<z;++p){w.fillStyle=s[z+p];var q=(p-z+1)*g;h.range([z*g+q,q]),q=h(0);for(var x=j,B=f,C;x<B;++x){C=r.valueAt(x);if(C<=0){o=!0;continue}w.fillRect(x,C=h(C),1,q-C)}}if(o){b==="offset"&&(w.translate(0,g),w.scale(1,-1));for(var p=0;p<z;++p){w.fillStyle=s[z-1-p];var q=(p-z+1)*g;h.range([z*g+q,q]),q=h(0);for(var x=j,B=f,C;x<B;++x){C=r.valueAt(x);if(C>=0)continue;w.fillRect(x,h(-C),1,q-h(-C))}}}w.restore()}function C(a){a==null&&(a=f-1);var b=r.valueAt(a);x.datum(b).text(isNaN(b)?null:m)}var p=this,q=++c,r=typeof i=="function"?i.call(p,k,o):i,s=typeof n=="function"?n.call(p,k,o):n,t=typeof j=="function"?j.call(p,k,o):j,u=-Infinity,v=a.step(),w=d3.select(p).select("canvas"),x=d3.select(p).select(".value"),y,z=s.length>>1,A;w.datum({id:q,metric:r}),w=w.node().getContext("2d"),a.on("change.horizon-"+q,B),a.on("focus.horizon-"+q,C),r.on("change.horizon-"+q,function(a,b){B(a,b),C(),A&&r.on("change.horizon-"+q,d)})})}var a=this,b="offset",e=document.createElement("canvas"),f=e.width=a.size(),g=e.height=30,h=d3.scale.linear().interpolate(d3.interpolateRound),i=d,j=null,k=d,m=d3.format(".2s"),n=["#08519c","#3182bd","#6baed6","#bdd7e7","#bae4b3","#74c476","#31a354","#006d2c"];return o.remove=function(b){function c(b){b.metric.on("change.horizon-"+b.id,null),a.on("change.horizon-"+b.id,null),a.on("focus.horizon-"+b.id,null)}b.on("mousemove.horizon",null).on("mouseout.horizon",null),b.selectAll("canvas").each(c).remove(),b.selectAll(".title,.value").remove()},o.mode=function(a){return arguments.length?(b=a+"",o):b},o.height=function(a){return arguments.length?(e.height=g=+a,o):g},o.metric=function(a){return arguments.length?(i=a,o):i},o.scale=function(a){return arguments.length?(h=a,o):h},o.extent=function(a){return arguments.length?(j=a,o):j},o.title=function(a){return arguments.length?(k=a,o):k},o.format=function(a){return arguments.length?(m=a,o):m},o.colors=function(a){return arguments.length?(n=a,o):n},o},f.comparison=function(){function o(o){o.on("mousemove.comparison",function(){a.focus(Math.round(d3.mouse(this)[0]))}).on("mouseout.comparison",function(){a.focus(null)}),o.append("canvas").attr("width",b).attr("height",e),o.append("span").attr("class","title").text(j),o.append("span").attr("class","value primary"),o.append("span").attr("class","value change"),o.each(function(j,o){function B(c,d){x.save(),x.clearRect(0,0,b,e);var g=r.extent(),h=u.extent(),i=v==null?g:v;f.domain(i).range([e,0]),A=g.concat(h).every(isFinite);var j=c/a.step()&1?t:s;x.fillStyle=m[2];for(var k=0,l=b;k<l;++k){var o=f(r.valueAt(k)),p=f(u.valueAt(k));o<p&&x.fillRect(j(k),o,1,p-o)}x.fillStyle=m[0];for(k=0;k<l;++k){var o=f(r.valueAt(k)),p=f(u.valueAt(k));o>p&&x.fillRect(j(k),p,1,o-p)}x.fillStyle=m[3];for(k=0;k<l;++k){var o=f(r.valueAt(k)),p=f(u.valueAt(k));o<=p&&x.fillRect(j(k),o,1,n)}x.fillStyle=m[1];for(k=0;k<l;++k){var o=f(r.valueAt(k)),p=f(u.valueAt(k));o>p&&x.fillRect(j(k),o-n,1,n)}x.restore()}function C(a){a==null&&(a=b-1);var c=r.valueAt(a),d=u.valueAt(a),e=(c-d)/d;y.datum(c).text(isNaN(c)?null:k),z.datum(e).text(isNaN(e)?null:l).attr("class","value change "+(e>0?"positive":e<0?"negative":""))}function D(a,b){B(a,b),C(),A&&(r.on("change.comparison-"+q,d),u.on("change.comparison-"+q,d))}var p=this,q=++c,r=typeof g=="function"?g.call(p,j,o):g,u=typeof h=="function"?h.call(p,j,o):h,v=typeof i=="function"?i.call(p,j,o):i,w=d3.select(p),x=w.select("canvas"),y=w.select(".value.primary"),z=w.select(".value.change"),A;x.datum({id:q,primary:r,secondary:u}),x=x.node().getContext("2d"),r.on("change.comparison-"+q,D),u.on("change.comparison-"+q,D),a.on("change.comparison-"+q,B),a.on("focus.comparison-"+q,C)})}var a=this,b=a.size(),e=120,f=d3.scale.linear().interpolate(d3.interpolateRound),g=function(a){return a[0]},h=function(a){return a[1]},i=null,j=d,k=q,l=r,m=["#9ecae1","#225b84","#a1d99b","#22723a"],n=1.5;return o.remove=function(b){function c(b){b.primary.on("change.comparison-"+b.id,null),b.secondary.on("change.comparison-"+b.id,null),a.on("change.comparison-"+b.id,null),a.on("focus.comparison-"+b.id,null)}b.on("mousemove.comparison",null).on("mouseout.comparison",null),b.selectAll("canvas").each(c).remove(),b.selectAll(".title,.value").remove()},o.height=function(a){return arguments.length?(e=+a,o):e},o.primary=function(a){return arguments.length?(g=a,o):g},o.secondary=function(a){return arguments.length?(h=a,o):h},o.scale=function(a){return arguments.length?(f=a,o):f},o.extent=function(a){return arguments.length?(i=a,o):i},o.title=function(a){return arguments.length?(j=a,o):j},o.formatPrimary=function(a){return arguments.length?(k=a,o):k},o.formatChange=function(a){return arguments.length?(l=a,o):l},o.colors=function(a){return arguments.length?(m=a,o):m},o.strokeWidth=function(a){return arguments.length?(n=a,o):n},o};var q=d3.format(".2s"),r=d3.format("+.0%");f.axis=function(){function f(g){var h=++c,i,j=g.append("svg").datum({id:h}).attr("width",a.size()).attr("height",Math.max(28,-f.tickSize())).append("g").attr("transform","translate(0,"+(d.orient()==="top"?27:4)+")").call(d);a.on("change.axis-"+h,function(){j.call(d),i||(i=d3.select(j.node().appendChild(j.selectAll("text").node().cloneNode(!0))).style("display","none").text(null))}),a.on("focus.axis-"+h,function(a){if(i)if(a==null)i.style("display","none"),j.selectAll("text").style("fill-opacity",null);else{i.style("display",null).attr("x",a).text(e(b.invert(a)));var c=i.node().getComputedTextLength()+6;j.selectAll("text").style("fill-opacity",function(d){return Math.abs(b(d)-a)<c?0:1})}})}var a=this,b=a.scale,d=d3.svg.axis().scale(b),e=a.step()<6e4?u:a.step()<864e5?v:w;return f.remove=function(b){function c(b){a.on("change.axis-"+b.id,null),a.on("focus.axis-"+b.id,null)}b.selectAll("svg").each(c).remove()},d3.rebind(f,d,"orient","ticks","tickSubdivide","tickSize","tickPadding","tickFormat")};var u=d3.time.format("%I:%M:%S %p"),v=d3.time.format("%I:%M %p"),w=d3.time.format("%B %d");f.rule=function(){function e(d){var e=++c,f=d.append("div").datum({id:e}).attr("class","line").call(x);d.each(function(e,f){function j(b,c){var e=[];for(var f=0,g=a.size();f<g;++f)i.valueAt(f)&&e.push(f);var h=d.selectAll(".metric").data(e);h.exit().remove(),h.enter().append("div").attr("class","metric line").call(x),h.style("left",y)}var g=this,h=++c,i=typeof b=="function"?b.call(g,e,f):b;if(!i)return;a.on("change.rule-"+h,j),i.on("change.rule-"+h,j)}),a.on("focus.rule-"+e,function(a){f.datum(a).style("display",a==null?"none":null).style("left",a==null?null:y)})}var a=this,b=d;return e.remove=function(b){function c(b){a.on("focus.rule-"+b.id,null)}b.selectAll(".line").each(c).remove()},e.metric=function(a){return arguments.length?(b=a,e):b},e}})(this);
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cubism-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.2.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Vlad Gorodetsky
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-06 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: railties
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '3.0'
|
22
|
+
- - <
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: '5.0'
|
25
|
+
type: :runtime
|
26
|
+
prerelease: false
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3.0'
|
33
|
+
- - <
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '5.0'
|
36
|
+
description: ! 'Cubism.js: A JavaScript library for time series visualization.'
|
37
|
+
email:
|
38
|
+
- v@gor.io
|
39
|
+
executables: []
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
files:
|
43
|
+
- .gitignore
|
44
|
+
- Gemfile
|
45
|
+
- LICENSE
|
46
|
+
- README.md
|
47
|
+
- Rakefile
|
48
|
+
- cubism-rails.gemspec
|
49
|
+
- lib/cubism-rails.rb
|
50
|
+
- lib/cubism-rails/version.rb
|
51
|
+
- vendor/assets/javascripts/cubism.js
|
52
|
+
- vendor/assets/javascripts/cubism.min.js
|
53
|
+
homepage: http://github.com/bai/cubism-rails
|
54
|
+
licenses: []
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options: []
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ! '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
requirements: []
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 1.8.24
|
74
|
+
signing_key:
|
75
|
+
specification_version: 3
|
76
|
+
summary: Gemified cubism.js asset for Rails
|
77
|
+
test_files: []
|