├── .gitignore ├── .npmignore ├── LICENSE ├── Makefile ├── README.md ├── cubism.v1.js ├── cubism.v1.min.js ├── docs ├── API-Reference.md ├── Axis.md ├── Comparison.md ├── Context.md ├── Cube.md ├── Cubism.md ├── Ganglia.md ├── Graphite.md ├── Home.md ├── Horizon.md ├── Librato.md ├── Metric.md ├── Rule.md ├── Tutorials.md ├── comparison.png └── horizon.png ├── index.js ├── package.json └── src ├── axis.js ├── comparison.js ├── context.js ├── cube.js ├── cubism.js ├── gangliaWeb.js ├── graphite.js ├── horizon.js ├── id.js ├── identity.js ├── librato.js ├── metric-constant.js ├── metric-operator.js ├── metric.js ├── option.js ├── package.js └── rule.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | demo 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | test/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | JS_TESTER = ./node_modules/vows/bin/vows 2 | JS_COMPILER = ./node_modules/uglify-js/bin/uglifyjs 3 | 4 | .PHONY: test 5 | 6 | all: cubism.v1.min.js package.json 7 | 8 | cubism.v1.js: \ 9 | src/cubism.js \ 10 | src/id.js \ 11 | src/identity.js \ 12 | src/option.js \ 13 | src/context.js \ 14 | src/cube.js \ 15 | src/librato.js \ 16 | src/graphite.js \ 17 | src/gangliaWeb.js \ 18 | src/metric.js \ 19 | src/metric-constant.js \ 20 | src/metric-operator.js \ 21 | src/horizon.js \ 22 | src/comparison.js \ 23 | src/axis.js \ 24 | src/rule.js \ 25 | Makefile 26 | 27 | %.min.js: %.js Makefile 28 | @rm -f $@ 29 | $(JS_COMPILER) < $< > $@ 30 | 31 | %.js: 32 | @rm -f $@ 33 | @echo '(function(exports){' > $@ 34 | cat $(filter %.js,$^) >> $@ 35 | @echo '})(this);' >> $@ 36 | @chmod a-w $@ 37 | 38 | package.json: cubism.v1.js src/package.js 39 | @rm -f $@ 40 | node src/package.js > $@ 41 | @chmod a-w $@ 42 | 43 | clean: 44 | rm -f cubism.v1.js cubism.v1.min.js package.json 45 | 46 | test: all 47 | @$(JS_TESTER) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cubism.js 2 | 3 | Cubism.js is a [D3](http://d3js.org) plugin for visualizing time series. Use Cubism to construct better realtime dashboards, pulling data from [Graphite](https://github.com/square/cubism/wiki/Graphite), [Cube](https://github.com/square/cubism/wiki/Cube) and other sources. Cubism is available under the [Apache License](LICENSE). 4 | 5 | Want to learn more? [See the wiki.](https://github.com/square/cubism/wiki) 6 | -------------------------------------------------------------------------------- /cubism.v1.js: -------------------------------------------------------------------------------- 1 | (function(exports){ 2 | var cubism = exports.cubism = {version: "1.6.0"}; 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 | /* librato (http://dev.librato.com/v1/post/metrics) source 189 | * If you want to see an example of how to use this source, check: https://gist.github.com/drio/5792680 190 | */ 191 | cubism_contextPrototype.librato = function(user, token) { 192 | var source = {}, 193 | context = this; 194 | auth_string = "Basic " + btoa(user + ":" + token); 195 | avail_rsts = [ 1, 60, 900, 3600 ]; 196 | 197 | /* Given a step, find the best librato resolution to use. 198 | * 199 | * Example: 200 | * 201 | * (s) : cubism step 202 | * 203 | * avail_rsts 1 --------------- 60 --------------- 900 ---------------- 3600 204 | * | (s) | 205 | * | | 206 | * [low_res top_res] 207 | * 208 | * return: low_res (60) 209 | */ 210 | function find_ideal_librato_resolution(step) { 211 | var highest_res = avail_rsts[0], 212 | lowest_res = avail_rsts[avail_rsts.length]; // high and lowest available resolution from librato 213 | 214 | /* If step is outside the highest or lowest librato resolution, pick them and we are done */ 215 | if (step >= lowest_res) 216 | return lowest_res; 217 | 218 | if (step <= highest_res) 219 | return highest_res; 220 | 221 | /* If not, find in what resolution interval the step lands. */ 222 | var iof, top_res, i; 223 | for (i=step; i<=lowest_res; i++) { 224 | iof = avail_rsts.indexOf(i); 225 | if (iof > -1) { 226 | top_res = avail_rsts[iof]; 227 | break; 228 | } 229 | } 230 | 231 | var low_res; 232 | for (i=step; i>=highest_res; i--) { 233 | iof = avail_rsts.indexOf(i); 234 | if (iof > -1) { 235 | low_res = avail_rsts[iof]; 236 | break; 237 | } 238 | } 239 | 240 | /* What's the closest librato resolution given the step ? */ 241 | return ((top_res-step) < (step-low_res)) ? top_res : low_res; 242 | } 243 | 244 | function find_librato_resolution(sdate, edate, step) { 245 | var i_size = edate - sdate, // interval size 246 | month = 2419200, 247 | week = 604800, 248 | two_days = 172800, 249 | ideal_res; 250 | 251 | if (i_size > month) 252 | return 3600; 253 | 254 | ideal_res = find_ideal_librato_resolution(step); 255 | 256 | /* 257 | * Now we have the ideal resolution, but due to the retention policies at librato, maybe we have 258 | * to use a higher resolution. 259 | * http://support.metrics.librato.com/knowledgebase/articles/66838-understanding-metrics-roll-ups-retention-and-grap 260 | */ 261 | if (i_size > week && ideal_res < 900) 262 | return 900; 263 | else if (i_size > two_days && ideal_res < 60) 264 | return 60; 265 | else 266 | return ideal_res; 267 | } 268 | 269 | /* All the logic to query the librato API is here */ 270 | var librato_request = function(composite) { 271 | var url_prefix = "https://metrics-api.librato.com/v1/metrics"; 272 | 273 | function make_url(sdate, edate, step) { 274 | var params = "compose=" + composite + 275 | "&start_time=" + sdate + 276 | "&end_time=" + edate + 277 | "&resolution=" + find_librato_resolution(sdate, edate, step); 278 | return url_prefix + "?" + params; 279 | } 280 | 281 | /* 282 | * We are most likely not going to get the same number of measurements 283 | * cubism expects for a particular context: We have to perform down/up 284 | * sampling 285 | */ 286 | function down_up_sampling(isdate, iedate, step, librato_mm) { 287 | var av = []; 288 | 289 | for (i=isdate; i<=iedate; i+=step) { 290 | var int_mes = []; 291 | while (librato_mm.length && librato_mm[0].measure_time <= i) { 292 | int_mes.push(librato_mm.shift().value); 293 | } 294 | 295 | var v; 296 | if (int_mes.length) { /* Compute the average */ 297 | v = int_mes.reduce(function(a, b) { return a + b }) / int_mes.length; 298 | } else { /* No librato values on interval */ 299 | v = (av.length) ? av[av.length-1] : 0; 300 | } 301 | av.push(v); 302 | } 303 | 304 | return av; 305 | } 306 | 307 | request = {}; 308 | 309 | request.fire = function(isdate, iedate, step, callback_done) { 310 | var a_values = []; /* Store partial values from librato */ 311 | 312 | /* 313 | * Librato has a limit in the number of measurements we get back in a request (100). 314 | * We recursively perform requests to the API to ensure we have all the data points 315 | * for the interval we are working on. 316 | */ 317 | function actual_request(full_url) { 318 | d3.json(full_url) 319 | .header("X-Requested-With", "XMLHttpRequest") 320 | .header("Authorization", auth_string) 321 | .header("Librato-User-Agent", 'cubism/' + cubism.version) 322 | .get(function (error, data) { /* Callback; data available */ 323 | if (!error) { 324 | if (data.measurements.length === 0) { 325 | return 326 | } 327 | data.measurements[0].series.forEach(function(o) { a_values.push(o); }); 328 | 329 | var still_more_values = 'query' in data && 'next_time' in data.query; 330 | if (still_more_values) { 331 | actual_request(make_url(data.query.next_time, iedate, step)); 332 | } else { 333 | var a_adjusted = down_up_sampling(isdate, iedate, step, a_values); 334 | callback_done(a_adjusted); 335 | } 336 | } 337 | }); 338 | } 339 | 340 | actual_request(make_url(isdate, iedate, step)); 341 | }; 342 | 343 | return request; 344 | }; 345 | 346 | /* 347 | * The user will use this method to create a cubism source (librato in this case) 348 | * and call .metric() as necessary to create metrics. 349 | */ 350 | source.metric = function(m_composite) { 351 | return context.metric(function(start, stop, step, callback) { 352 | /* All the librato logic is here; .fire() retrieves the metrics' data */ 353 | librato_request(m_composite) 354 | .fire(cubism_libratoFormatDate(start), 355 | cubism_libratoFormatDate(stop), 356 | cubism_libratoFormatDate(step), 357 | function(a_values) { callback(null, a_values); }); 358 | 359 | }, m_composite += ""); 360 | }; 361 | 362 | /* This is not used when the source is librato */ 363 | source.toString = function() { 364 | return "librato"; 365 | }; 366 | 367 | return source; 368 | }; 369 | 370 | var cubism_libratoFormatDate = function(time) { 371 | return Math.floor(time / 1000); 372 | }; 373 | cubism_contextPrototype.graphite = function(host) { 374 | if (!arguments.length) host = ""; 375 | var source = {}, 376 | context = this; 377 | 378 | source.metric = function(expression) { 379 | var sum = "sum"; 380 | 381 | var metric = context.metric(function(start, stop, step, callback) { 382 | var target = expression; 383 | 384 | // Apply the summarize, if necessary. 385 | if (step !== 1e4) target = "summarize(" + target + ",'" 386 | + (!(step % 36e5) ? step / 36e5 + "hour" : !(step % 6e4) ? step / 6e4 + "min" : step / 1e3 + "sec") 387 | + "','" + sum + "')"; 388 | 389 | d3.text(host + "/render?format=raw" 390 | + "&target=" + encodeURIComponent("alias(" + target + ",'')") 391 | + "&from=" + cubism_graphiteFormatDate(start - 2 * step) // off-by-two? 392 | + "&until=" + cubism_graphiteFormatDate(stop - 1000), function(text) { 393 | if (!text) return callback(new Error("unable to load data")); 394 | callback(null, cubism_graphiteParse(text)); 395 | }); 396 | }, expression += ""); 397 | 398 | metric.summarize = function(_) { 399 | sum = _; 400 | return metric; 401 | }; 402 | 403 | return metric; 404 | }; 405 | 406 | source.find = function(pattern, callback) { 407 | d3.json(host + "/metrics/find?format=completer" 408 | + "&query=" + encodeURIComponent(pattern), function(result) { 409 | if (!result) return callback(new Error("unable to find metrics")); 410 | callback(null, result.metrics.map(function(d) { return d.path; })); 411 | }); 412 | }; 413 | 414 | // Returns the graphite host. 415 | source.toString = function() { 416 | return host; 417 | }; 418 | 419 | return source; 420 | }; 421 | 422 | // Graphite understands seconds since UNIX epoch. 423 | function cubism_graphiteFormatDate(time) { 424 | return Math.floor(time / 1000); 425 | } 426 | 427 | // Helper method for parsing graphite's raw format. 428 | function cubism_graphiteParse(text) { 429 | var i = text.indexOf("|"), 430 | meta = text.substring(0, i), 431 | c = meta.lastIndexOf(","), 432 | b = meta.lastIndexOf(",", c - 1), 433 | a = meta.lastIndexOf(",", b - 1), 434 | start = meta.substring(a + 1, b) * 1000, 435 | step = meta.substring(c + 1) * 1000; 436 | return text 437 | .substring(i + 1) 438 | .split(",") 439 | .slice(1) // the first value is always None? 440 | .map(function(d) { return +d; }); 441 | } 442 | cubism_contextPrototype.gangliaWeb = function(config) { 443 | var host = '', 444 | uriPathPrefix = '/ganglia2/'; 445 | 446 | if (arguments.length) { 447 | if (config.host) { 448 | host = config.host; 449 | } 450 | 451 | if (config.uriPathPrefix) { 452 | uriPathPrefix = config.uriPathPrefix; 453 | 454 | /* Add leading and trailing slashes, as appropriate. */ 455 | if( uriPathPrefix[0] != '/' ) { 456 | uriPathPrefix = '/' + uriPathPrefix; 457 | } 458 | 459 | if( uriPathPrefix[uriPathPrefix.length - 1] != '/' ) { 460 | uriPathPrefix += '/'; 461 | } 462 | } 463 | } 464 | 465 | var source = {}, 466 | context = this; 467 | 468 | source.metric = function(metricInfo) { 469 | 470 | /* Store the members from metricInfo into local variables. */ 471 | var clusterName = metricInfo.clusterName, 472 | metricName = metricInfo.metricName, 473 | hostName = metricInfo.hostName, 474 | isReport = metricInfo.isReport || false, 475 | titleGenerator = metricInfo.titleGenerator || 476 | /* Reasonable (not necessarily pretty) default for titleGenerator. */ 477 | function(unusedMetricInfo) { 478 | /* unusedMetricInfo is, well, unused in this default case. */ 479 | return ('clusterName:' + clusterName + 480 | ' metricName:' + metricName + 481 | (hostName ? ' hostName:' + hostName : '')); 482 | }, 483 | onChangeCallback = metricInfo.onChangeCallback; 484 | 485 | /* Default to plain, simple metrics. */ 486 | var metricKeyName = isReport ? 'g' : 'm'; 487 | 488 | var gangliaWebMetric = context.metric(function(start, stop, step, callback) { 489 | 490 | function constructGangliaWebRequestQueryParams() { 491 | return ('c=' + clusterName + 492 | '&' + metricKeyName + '=' + metricName + 493 | (hostName ? '&h=' + hostName : '') + 494 | '&cs=' + start/1000 + '&ce=' + stop/1000 + '&step=' + step/1000 + '&graphlot=1'); 495 | } 496 | 497 | d3.json(host + uriPathPrefix + 'graph.php?' + constructGangliaWebRequestQueryParams(), 498 | function(result) { 499 | if( !result ) { 500 | return callback(new Error("Unable to fetch GangliaWeb data")); 501 | } 502 | 503 | callback(null, result[0].data); 504 | }); 505 | 506 | }, titleGenerator(metricInfo)); 507 | 508 | gangliaWebMetric.toString = function() { 509 | return titleGenerator(metricInfo); 510 | }; 511 | 512 | /* Allow users to run their custom code each time a gangliaWebMetric changes. 513 | * 514 | * TODO Consider abstracting away the naked Cubism call, and instead exposing 515 | * a callback that takes in the values array (maybe alongwith the original 516 | * start and stop 'naked' parameters), since it's handy to have the entire 517 | * dataset at your disposal (and users will likely implement onChangeCallback 518 | * primarily to get at this dataset). 519 | */ 520 | if (onChangeCallback) { 521 | gangliaWebMetric.on('change', onChangeCallback); 522 | } 523 | 524 | return gangliaWebMetric; 525 | }; 526 | 527 | // Returns the gangliaWeb host + uriPathPrefix. 528 | source.toString = function() { 529 | return host + uriPathPrefix; 530 | }; 531 | 532 | return source; 533 | }; 534 | 535 | function cubism_metric(context) { 536 | if (!(context instanceof cubism_context)) throw new Error("invalid context"); 537 | this.context = context; 538 | } 539 | 540 | var cubism_metricPrototype = cubism_metric.prototype; 541 | 542 | cubism.metric = cubism_metric; 543 | 544 | cubism_metricPrototype.valueAt = function() { 545 | return NaN; 546 | }; 547 | 548 | cubism_metricPrototype.alias = function(name) { 549 | this.toString = function() { return name; }; 550 | return this; 551 | }; 552 | 553 | cubism_metricPrototype.extent = function() { 554 | var i = 0, 555 | n = this.context.size(), 556 | value, 557 | min = Infinity, 558 | max = -Infinity; 559 | while (++i < n) { 560 | value = this.valueAt(i); 561 | if (value < min) min = value; 562 | if (value > max) max = value; 563 | } 564 | return [min, max]; 565 | }; 566 | 567 | cubism_metricPrototype.on = function(type, listener) { 568 | return arguments.length < 2 ? null : this; 569 | }; 570 | 571 | cubism_metricPrototype.shift = function() { 572 | return this; 573 | }; 574 | 575 | cubism_metricPrototype.on = function() { 576 | return arguments.length < 2 ? null : this; 577 | }; 578 | 579 | cubism_contextPrototype.metric = function(request, name) { 580 | var context = this, 581 | metric = new cubism_metric(context), 582 | id = ".metric-" + ++cubism_id, 583 | start = -Infinity, 584 | stop, 585 | step = context.step(), 586 | size = context.size(), 587 | values = [], 588 | event = d3.dispatch("change"), 589 | listening = 0, 590 | fetching; 591 | 592 | // Prefetch new data into a temporary array. 593 | function prepare(start1, stop) { 594 | var steps = Math.min(size, Math.round((start1 - start) / step)); 595 | if (!steps || fetching) return; // already fetched, or fetching! 596 | fetching = true; 597 | steps = Math.min(size, steps + cubism_metricOverlap); 598 | var start0 = new Date(stop - steps * step); 599 | request(start0, stop, step, function(error, data) { 600 | fetching = false; 601 | if (error) return console.warn(error); 602 | var i = isFinite(start) ? Math.round((start0 - start) / step) : 0; 603 | for (var j = 0, m = data.length; j < m; ++j) values[j + i] = data[j]; 604 | event.change.call(metric, start, stop); 605 | }); 606 | } 607 | 608 | // When the context changes, switch to the new data, ready-or-not! 609 | function beforechange(start1, stop1) { 610 | if (!isFinite(start)) start = start1; 611 | values.splice(0, Math.max(0, Math.min(size, Math.round((start1 - start) / step)))); 612 | start = start1; 613 | stop = stop1; 614 | } 615 | 616 | // 617 | metric.valueAt = function(i) { 618 | return values[i]; 619 | }; 620 | 621 | // 622 | metric.shift = function(offset) { 623 | return context.metric(cubism_metricShift(request, +offset)); 624 | }; 625 | 626 | // 627 | metric.on = function(type, listener) { 628 | if (!arguments.length) return event.on(type); 629 | 630 | // If there are no listeners, then stop listening to the context, 631 | // and avoid unnecessary fetches. 632 | if (listener == null) { 633 | if (event.on(type) != null && --listening == 0) { 634 | context.on("prepare" + id, null).on("beforechange" + id, null); 635 | } 636 | } else { 637 | if (event.on(type) == null && ++listening == 1) { 638 | context.on("prepare" + id, prepare).on("beforechange" + id, beforechange); 639 | } 640 | } 641 | 642 | event.on(type, listener); 643 | 644 | // Notify the listener of the current start and stop time, as appropriate. 645 | // This way, charts can display synchronous metrics immediately. 646 | if (listener != null) { 647 | if (/^change(\.|$)/.test(type)) listener.call(context, start, stop); 648 | } 649 | 650 | return metric; 651 | }; 652 | 653 | // 654 | if (arguments.length > 1) metric.toString = function() { 655 | return name; 656 | }; 657 | 658 | return metric; 659 | }; 660 | 661 | // Number of metric to refetch each period, in case of lag. 662 | var cubism_metricOverlap = 6; 663 | 664 | // Wraps the specified request implementation, and shifts time by the given offset. 665 | function cubism_metricShift(request, offset) { 666 | return function(start, stop, step, callback) { 667 | request(new Date(+start + offset), new Date(+stop + offset), step, callback); 668 | }; 669 | } 670 | function cubism_metricConstant(context, value) { 671 | cubism_metric.call(this, context); 672 | value = +value; 673 | var name = value + ""; 674 | this.valueOf = function() { return value; }; 675 | this.toString = function() { return name; }; 676 | } 677 | 678 | var cubism_metricConstantPrototype = cubism_metricConstant.prototype = Object.create(cubism_metric.prototype); 679 | 680 | cubism_metricConstantPrototype.valueAt = function() { 681 | return +this; 682 | }; 683 | 684 | cubism_metricConstantPrototype.extent = function() { 685 | return [+this, +this]; 686 | }; 687 | function cubism_metricOperator(name, operate) { 688 | 689 | function cubism_metricOperator(left, right) { 690 | if (!(right instanceof cubism_metric)) right = new cubism_metricConstant(left.context, right); 691 | else if (left.context !== right.context) throw new Error("mismatch context"); 692 | cubism_metric.call(this, left.context); 693 | this.left = left; 694 | this.right = right; 695 | this.toString = function() { return left + " " + name + " " + right; }; 696 | } 697 | 698 | var cubism_metricOperatorPrototype = cubism_metricOperator.prototype = Object.create(cubism_metric.prototype); 699 | 700 | cubism_metricOperatorPrototype.valueAt = function(i) { 701 | return operate(this.left.valueAt(i), this.right.valueAt(i)); 702 | }; 703 | 704 | cubism_metricOperatorPrototype.shift = function(offset) { 705 | return new cubism_metricOperator(this.left.shift(offset), this.right.shift(offset)); 706 | }; 707 | 708 | cubism_metricOperatorPrototype.on = function(type, listener) { 709 | if (arguments.length < 2) return this.left.on(type); 710 | this.left.on(type, listener); 711 | this.right.on(type, listener); 712 | return this; 713 | }; 714 | 715 | return function(right) { 716 | return new cubism_metricOperator(this, right); 717 | }; 718 | } 719 | 720 | cubism_metricPrototype.add = cubism_metricOperator("+", function(left, right) { 721 | return left + right; 722 | }); 723 | 724 | cubism_metricPrototype.subtract = cubism_metricOperator("-", function(left, right) { 725 | return left - right; 726 | }); 727 | 728 | cubism_metricPrototype.multiply = cubism_metricOperator("*", function(left, right) { 729 | return left * right; 730 | }); 731 | 732 | cubism_metricPrototype.divide = cubism_metricOperator("/", function(left, right) { 733 | return left / right; 734 | }); 735 | cubism_contextPrototype.horizon = function() { 736 | var context = this, 737 | mode = "offset", 738 | buffer = document.createElement("canvas"), 739 | width = buffer.width = context.size(), 740 | height = buffer.height = 30, 741 | scale = d3.scale.linear().interpolate(d3.interpolateRound), 742 | metric = cubism_identity, 743 | extent = null, 744 | title = cubism_identity, 745 | format = d3.format(".2s"), 746 | colors = ["#08519c","#3182bd","#6baed6","#bdd7e7","#bae4b3","#74c476","#31a354","#006d2c"]; 747 | 748 | function horizon(selection) { 749 | 750 | selection 751 | .on("mousemove.horizon", function() { context.focus(Math.round(d3.mouse(this)[0])); }) 752 | .on("mouseout.horizon", function() { context.focus(null); }); 753 | 754 | selection.append("canvas") 755 | .attr("width", width) 756 | .attr("height", height); 757 | 758 | selection.append("span") 759 | .attr("class", "title") 760 | .text(title); 761 | 762 | selection.append("span") 763 | .attr("class", "value"); 764 | 765 | selection.each(function(d, i) { 766 | var that = this, 767 | id = ++cubism_id, 768 | metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric, 769 | colors_ = typeof colors === "function" ? colors.call(that, d, i) : colors, 770 | extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent, 771 | start = -Infinity, 772 | step = context.step(), 773 | canvas = d3.select(that).select("canvas"), 774 | span = d3.select(that).select(".value"), 775 | max_, 776 | m = colors_.length >> 1, 777 | ready; 778 | 779 | canvas.datum({id: id, metric: metric_}); 780 | canvas = canvas.node().getContext("2d"); 781 | 782 | function change(start1, stop) { 783 | canvas.save(); 784 | 785 | // compute the new extent and ready flag 786 | var extent = metric_.extent(); 787 | ready = extent.every(isFinite); 788 | if (extent_ != null) extent = extent_; 789 | 790 | // if this is an update (with no extent change), copy old values! 791 | var i0 = 0, max = Math.max(-extent[0], extent[1]); 792 | if (this === context) { 793 | if (max == max_) { 794 | i0 = width - cubism_metricOverlap; 795 | var dx = (start1 - start) / step; 796 | if (dx < width) { 797 | var canvas0 = buffer.getContext("2d"); 798 | canvas0.clearRect(0, 0, width, height); 799 | canvas0.drawImage(canvas.canvas, dx, 0, width - dx, height, 0, 0, width - dx, height); 800 | canvas.clearRect(0, 0, width, height); 801 | canvas.drawImage(canvas0.canvas, 0, 0); 802 | } 803 | } 804 | start = start1; 805 | } 806 | 807 | // update the domain 808 | scale.domain([0, max_ = max]); 809 | 810 | // clear for the new data 811 | canvas.clearRect(i0, 0, width - i0, height); 812 | 813 | // record whether there are negative values to display 814 | var negative; 815 | 816 | // positive bands 817 | for (var j = 0; j < m; ++j) { 818 | canvas.fillStyle = colors_[m + j]; 819 | 820 | // Adjust the range based on the current band index. 821 | var y0 = (j - m + 1) * height; 822 | scale.range([m * height + y0, y0]); 823 | y0 = scale(0); 824 | 825 | for (var i = i0, n = width, y1; i < n; ++i) { 826 | y1 = metric_.valueAt(i); 827 | if (y1 <= 0) { negative = true; continue; } 828 | if (y1 === undefined) continue; 829 | canvas.fillRect(i, y1 = scale(y1), 1, y0 - y1); 830 | } 831 | } 832 | 833 | if (negative) { 834 | // enable offset mode 835 | if (mode === "offset") { 836 | canvas.translate(0, height); 837 | canvas.scale(1, -1); 838 | } 839 | 840 | // negative bands 841 | for (var j = 0; j < m; ++j) { 842 | canvas.fillStyle = colors_[m - 1 - j]; 843 | 844 | // Adjust the range based on the current band index. 845 | var y0 = (j - m + 1) * height; 846 | scale.range([m * height + y0, y0]); 847 | y0 = scale(0); 848 | 849 | for (var i = i0, n = width, y1; i < n; ++i) { 850 | y1 = metric_.valueAt(i); 851 | if (y1 >= 0) continue; 852 | canvas.fillRect(i, scale(-y1), 1, y0 - scale(-y1)); 853 | } 854 | } 855 | } 856 | 857 | canvas.restore(); 858 | } 859 | 860 | function focus(i) { 861 | if (i == null) i = width - 1; 862 | var value = metric_.valueAt(i); 863 | span.datum(value).text(isNaN(value) ? null : format); 864 | } 865 | 866 | // Update the chart when the context changes. 867 | context.on("change.horizon-" + id, change); 868 | context.on("focus.horizon-" + id, focus); 869 | 870 | // Display the first metric change immediately, 871 | // but defer subsequent updates to the canvas change. 872 | // Note that someone still needs to listen to the metric, 873 | // so that it continues to update automatically. 874 | metric_.on("change.horizon-" + id, function(start, stop) { 875 | change(start, stop), focus(); 876 | if (ready) metric_.on("change.horizon-" + id, cubism_identity); 877 | }); 878 | }); 879 | } 880 | 881 | horizon.remove = function(selection) { 882 | 883 | selection 884 | .on("mousemove.horizon", null) 885 | .on("mouseout.horizon", null); 886 | 887 | selection.selectAll("canvas") 888 | .each(remove) 889 | .remove(); 890 | 891 | selection.selectAll(".title,.value") 892 | .remove(); 893 | 894 | function remove(d) { 895 | d.metric.on("change.horizon-" + d.id, null); 896 | context.on("change.horizon-" + d.id, null); 897 | context.on("focus.horizon-" + d.id, null); 898 | } 899 | }; 900 | 901 | horizon.mode = function(_) { 902 | if (!arguments.length) return mode; 903 | mode = _ + ""; 904 | return horizon; 905 | }; 906 | 907 | horizon.height = function(_) { 908 | if (!arguments.length) return height; 909 | buffer.height = height = +_; 910 | return horizon; 911 | }; 912 | 913 | horizon.metric = function(_) { 914 | if (!arguments.length) return metric; 915 | metric = _; 916 | return horizon; 917 | }; 918 | 919 | horizon.scale = function(_) { 920 | if (!arguments.length) return scale; 921 | scale = _; 922 | return horizon; 923 | }; 924 | 925 | horizon.extent = function(_) { 926 | if (!arguments.length) return extent; 927 | extent = _; 928 | return horizon; 929 | }; 930 | 931 | horizon.title = function(_) { 932 | if (!arguments.length) return title; 933 | title = _; 934 | return horizon; 935 | }; 936 | 937 | horizon.format = function(_) { 938 | if (!arguments.length) return format; 939 | format = _; 940 | return horizon; 941 | }; 942 | 943 | horizon.colors = function(_) { 944 | if (!arguments.length) return colors; 945 | colors = _; 946 | return horizon; 947 | }; 948 | 949 | return horizon; 950 | }; 951 | cubism_contextPrototype.comparison = function() { 952 | var context = this, 953 | width = context.size(), 954 | height = 120, 955 | scale = d3.scale.linear().interpolate(d3.interpolateRound), 956 | primary = function(d) { return d[0]; }, 957 | secondary = function(d) { return d[1]; }, 958 | extent = null, 959 | title = cubism_identity, 960 | formatPrimary = cubism_comparisonPrimaryFormat, 961 | formatChange = cubism_comparisonChangeFormat, 962 | colors = ["#9ecae1", "#225b84", "#a1d99b", "#22723a"], 963 | strokeWidth = 1.5; 964 | 965 | function comparison(selection) { 966 | 967 | selection 968 | .on("mousemove.comparison", function() { context.focus(Math.round(d3.mouse(this)[0])); }) 969 | .on("mouseout.comparison", function() { context.focus(null); }); 970 | 971 | selection.append("canvas") 972 | .attr("width", width) 973 | .attr("height", height); 974 | 975 | selection.append("span") 976 | .attr("class", "title") 977 | .text(title); 978 | 979 | selection.append("span") 980 | .attr("class", "value primary"); 981 | 982 | selection.append("span") 983 | .attr("class", "value change"); 984 | 985 | selection.each(function(d, i) { 986 | var that = this, 987 | id = ++cubism_id, 988 | primary_ = typeof primary === "function" ? primary.call(that, d, i) : primary, 989 | secondary_ = typeof secondary === "function" ? secondary.call(that, d, i) : secondary, 990 | extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent, 991 | div = d3.select(that), 992 | canvas = div.select("canvas"), 993 | spanPrimary = div.select(".value.primary"), 994 | spanChange = div.select(".value.change"), 995 | ready; 996 | 997 | canvas.datum({id: id, primary: primary_, secondary: secondary_}); 998 | canvas = canvas.node().getContext("2d"); 999 | 1000 | function change(start, stop) { 1001 | canvas.save(); 1002 | canvas.clearRect(0, 0, width, height); 1003 | 1004 | // update the scale 1005 | var primaryExtent = primary_.extent(), 1006 | secondaryExtent = secondary_.extent(), 1007 | extent = extent_ == null ? primaryExtent : extent_; 1008 | scale.domain(extent).range([height, 0]); 1009 | ready = primaryExtent.concat(secondaryExtent).every(isFinite); 1010 | 1011 | // consistent overplotting 1012 | var round = start / context.step() & 1 1013 | ? cubism_comparisonRoundOdd 1014 | : cubism_comparisonRoundEven; 1015 | 1016 | // positive changes 1017 | canvas.fillStyle = colors[2]; 1018 | for (var i = 0, n = width; i < n; ++i) { 1019 | var y0 = scale(primary_.valueAt(i)), 1020 | y1 = scale(secondary_.valueAt(i)); 1021 | if (y0 < y1) canvas.fillRect(round(i), y0, 1, y1 - y0); 1022 | } 1023 | 1024 | // negative changes 1025 | canvas.fillStyle = colors[0]; 1026 | for (i = 0; i < n; ++i) { 1027 | var y0 = scale(primary_.valueAt(i)), 1028 | y1 = scale(secondary_.valueAt(i)); 1029 | if (y0 > y1) canvas.fillRect(round(i), y1, 1, y0 - y1); 1030 | } 1031 | 1032 | // positive values 1033 | canvas.fillStyle = colors[3]; 1034 | for (i = 0; i < n; ++i) { 1035 | var y0 = scale(primary_.valueAt(i)), 1036 | y1 = scale(secondary_.valueAt(i)); 1037 | if (y0 <= y1) canvas.fillRect(round(i), y0, 1, strokeWidth); 1038 | } 1039 | 1040 | // negative values 1041 | canvas.fillStyle = colors[1]; 1042 | for (i = 0; i < n; ++i) { 1043 | var y0 = scale(primary_.valueAt(i)), 1044 | y1 = scale(secondary_.valueAt(i)); 1045 | if (y0 > y1) canvas.fillRect(round(i), y0 - strokeWidth, 1, strokeWidth); 1046 | } 1047 | 1048 | canvas.restore(); 1049 | } 1050 | 1051 | function focus(i) { 1052 | if (i == null) i = width - 1; 1053 | var valuePrimary = primary_.valueAt(i), 1054 | valueSecondary = secondary_.valueAt(i), 1055 | valueChange = (valuePrimary - valueSecondary) / valueSecondary; 1056 | 1057 | spanPrimary 1058 | .datum(valuePrimary) 1059 | .text(isNaN(valuePrimary) ? null : formatPrimary); 1060 | 1061 | spanChange 1062 | .datum(valueChange) 1063 | .text(isNaN(valueChange) ? null : formatChange) 1064 | .attr("class", "value change " + (valueChange > 0 ? "positive" : valueChange < 0 ? "negative" : "")); 1065 | } 1066 | 1067 | // Display the first primary change immediately, 1068 | // but defer subsequent updates to the context change. 1069 | // Note that someone still needs to listen to the metric, 1070 | // so that it continues to update automatically. 1071 | primary_.on("change.comparison-" + id, firstChange); 1072 | secondary_.on("change.comparison-" + id, firstChange); 1073 | function firstChange(start, stop) { 1074 | change(start, stop), focus(); 1075 | if (ready) { 1076 | primary_.on("change.comparison-" + id, cubism_identity); 1077 | secondary_.on("change.comparison-" + id, cubism_identity); 1078 | } 1079 | } 1080 | 1081 | // Update the chart when the context changes. 1082 | context.on("change.comparison-" + id, change); 1083 | context.on("focus.comparison-" + id, focus); 1084 | }); 1085 | } 1086 | 1087 | comparison.remove = function(selection) { 1088 | 1089 | selection 1090 | .on("mousemove.comparison", null) 1091 | .on("mouseout.comparison", null); 1092 | 1093 | selection.selectAll("canvas") 1094 | .each(remove) 1095 | .remove(); 1096 | 1097 | selection.selectAll(".title,.value") 1098 | .remove(); 1099 | 1100 | function remove(d) { 1101 | d.primary.on("change.comparison-" + d.id, null); 1102 | d.secondary.on("change.comparison-" + d.id, null); 1103 | context.on("change.comparison-" + d.id, null); 1104 | context.on("focus.comparison-" + d.id, null); 1105 | } 1106 | }; 1107 | 1108 | comparison.height = function(_) { 1109 | if (!arguments.length) return height; 1110 | height = +_; 1111 | return comparison; 1112 | }; 1113 | 1114 | comparison.primary = function(_) { 1115 | if (!arguments.length) return primary; 1116 | primary = _; 1117 | return comparison; 1118 | }; 1119 | 1120 | comparison.secondary = function(_) { 1121 | if (!arguments.length) return secondary; 1122 | secondary = _; 1123 | return comparison; 1124 | }; 1125 | 1126 | comparison.scale = function(_) { 1127 | if (!arguments.length) return scale; 1128 | scale = _; 1129 | return comparison; 1130 | }; 1131 | 1132 | comparison.extent = function(_) { 1133 | if (!arguments.length) return extent; 1134 | extent = _; 1135 | return comparison; 1136 | }; 1137 | 1138 | comparison.title = function(_) { 1139 | if (!arguments.length) return title; 1140 | title = _; 1141 | return comparison; 1142 | }; 1143 | 1144 | comparison.formatPrimary = function(_) { 1145 | if (!arguments.length) return formatPrimary; 1146 | formatPrimary = _; 1147 | return comparison; 1148 | }; 1149 | 1150 | comparison.formatChange = function(_) { 1151 | if (!arguments.length) return formatChange; 1152 | formatChange = _; 1153 | return comparison; 1154 | }; 1155 | 1156 | comparison.colors = function(_) { 1157 | if (!arguments.length) return colors; 1158 | colors = _; 1159 | return comparison; 1160 | }; 1161 | 1162 | comparison.strokeWidth = function(_) { 1163 | if (!arguments.length) return strokeWidth; 1164 | strokeWidth = _; 1165 | return comparison; 1166 | }; 1167 | 1168 | return comparison; 1169 | }; 1170 | 1171 | var cubism_comparisonPrimaryFormat = d3.format(".2s"), 1172 | cubism_comparisonChangeFormat = d3.format("+.0%"); 1173 | 1174 | function cubism_comparisonRoundEven(i) { 1175 | return i & 0xfffffe; 1176 | } 1177 | 1178 | function cubism_comparisonRoundOdd(i) { 1179 | return ((i + 1) & 0xfffffe) - 1; 1180 | } 1181 | cubism_contextPrototype.axis = function() { 1182 | var context = this, 1183 | scale = context.scale, 1184 | axis_ = d3.svg.axis().scale(scale); 1185 | 1186 | var formatDefault = context.step() < 6e4 ? cubism_axisFormatSeconds 1187 | : context.step() < 864e5 ? cubism_axisFormatMinutes 1188 | : cubism_axisFormatDays; 1189 | var format = formatDefault; 1190 | 1191 | function axis(selection) { 1192 | var id = ++cubism_id, 1193 | tick; 1194 | 1195 | var g = selection.append("svg") 1196 | .datum({id: id}) 1197 | .attr("width", context.size()) 1198 | .attr("height", Math.max(28, -axis.tickSize())) 1199 | .append("g") 1200 | .attr("transform", "translate(0," + (axis_.orient() === "top" ? 27 : 4) + ")") 1201 | .call(axis_); 1202 | 1203 | context.on("change.axis-" + id, function() { 1204 | g.call(axis_); 1205 | if (!tick) tick = d3.select(g.node().appendChild(g.selectAll("text").node().cloneNode(true))) 1206 | .style("display", "none") 1207 | .text(null); 1208 | }); 1209 | 1210 | context.on("focus.axis-" + id, function(i) { 1211 | if (tick) { 1212 | if (i == null) { 1213 | tick.style("display", "none"); 1214 | g.selectAll("text").style("fill-opacity", null); 1215 | } else { 1216 | tick.style("display", null).attr("x", i).text(format(scale.invert(i))); 1217 | var dx = tick.node().getComputedTextLength() + 6; 1218 | g.selectAll("text").style("fill-opacity", function(d) { return Math.abs(scale(d) - i) < dx ? 0 : 1; }); 1219 | } 1220 | } 1221 | }); 1222 | } 1223 | 1224 | axis.remove = function(selection) { 1225 | 1226 | selection.selectAll("svg") 1227 | .each(remove) 1228 | .remove(); 1229 | 1230 | function remove(d) { 1231 | context.on("change.axis-" + d.id, null); 1232 | context.on("focus.axis-" + d.id, null); 1233 | } 1234 | }; 1235 | 1236 | axis.focusFormat = function(_) { 1237 | if (!arguments.length) return format == formatDefault ? null : _; 1238 | format = _ == null ? formatDefault : _; 1239 | return axis; 1240 | }; 1241 | 1242 | return d3.rebind(axis, axis_, 1243 | "orient", 1244 | "ticks", 1245 | "tickSubdivide", 1246 | "tickSize", 1247 | "tickPadding", 1248 | "tickFormat"); 1249 | }; 1250 | 1251 | var cubism_axisFormatSeconds = d3.time.format("%I:%M:%S %p"), 1252 | cubism_axisFormatMinutes = d3.time.format("%I:%M %p"), 1253 | cubism_axisFormatDays = d3.time.format("%B %d"); 1254 | cubism_contextPrototype.rule = function() { 1255 | var context = this, 1256 | metric = cubism_identity; 1257 | 1258 | function rule(selection) { 1259 | var id = ++cubism_id; 1260 | 1261 | var line = selection.append("div") 1262 | .datum({id: id}) 1263 | .attr("class", "line") 1264 | .call(cubism_ruleStyle); 1265 | 1266 | selection.each(function(d, i) { 1267 | var that = this, 1268 | id = ++cubism_id, 1269 | metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric; 1270 | 1271 | if (!metric_) return; 1272 | 1273 | function change(start, stop) { 1274 | var values = []; 1275 | 1276 | for (var i = 0, n = context.size(); i < n; ++i) { 1277 | if (metric_.valueAt(i)) { 1278 | values.push(i); 1279 | } 1280 | } 1281 | 1282 | var lines = selection.selectAll(".metric").data(values); 1283 | lines.exit().remove(); 1284 | lines.enter().append("div").attr("class", "metric line").call(cubism_ruleStyle); 1285 | lines.style("left", cubism_ruleLeft); 1286 | } 1287 | 1288 | context.on("change.rule-" + id, change); 1289 | metric_.on("change.rule-" + id, change); 1290 | }); 1291 | 1292 | context.on("focus.rule-" + id, function(i) { 1293 | line.datum(i) 1294 | .style("display", i == null ? "none" : null) 1295 | .style("left", i == null ? null : cubism_ruleLeft); 1296 | }); 1297 | } 1298 | 1299 | rule.remove = function(selection) { 1300 | 1301 | selection.selectAll(".line") 1302 | .each(remove) 1303 | .remove(); 1304 | 1305 | function remove(d) { 1306 | context.on("focus.rule-" + d.id, null); 1307 | } 1308 | }; 1309 | 1310 | rule.metric = function(_) { 1311 | if (!arguments.length) return metric; 1312 | metric = _; 1313 | return rule; 1314 | }; 1315 | 1316 | return rule; 1317 | }; 1318 | 1319 | function cubism_ruleStyle(line) { 1320 | line 1321 | .style("position", "absolute") 1322 | .style("top", 0) 1323 | .style("bottom", 0) 1324 | .style("width", "1px") 1325 | .style("pointer-events", "none"); 1326 | } 1327 | 1328 | function cubism_ruleLeft(i) { 1329 | return i + "px"; 1330 | } 1331 | })(this); 1332 | -------------------------------------------------------------------------------- /cubism.v1.min.js: -------------------------------------------------------------------------------- 1 | (function(a){function d(a){return a}function e(){}function j(a){return Math.floor(a/1e3)}function k(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 l(a){if(!(a instanceof e))throw new Error("invalid context");this.context=a}function o(a,b){return function(c,d,e,f){a(new Date(+c+b),new Date(+d+b),e,f)}}function p(a,b){l.call(this,a),b=+b;var c=b+"";this.valueOf=function(){return b},this.toString=function(){return c}}function r(a,b){function c(b,c){if(c instanceof l){if(b.context!==c.context)throw new Error("mismatch context")}else c=new p(b.context,c);l.call(this,b.context),this.left=b,this.right=c,this.toString=function(){return b+" "+a+" "+c}}var d=c.prototype=Object.create(l.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 u(a){return a&16777214}function v(a){return(a+1&16777214)-1}function z(a){a.style("position","absolute").style("top",0).style("bottom",0).style("width","1px").style("pointer-events","none")}function A(a){return a+"px"}var b=a.cubism={version:"1.6.0"},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(++e0&&a.focus(--o);break;case 39:o==null&&(o=d-2),o=c)return c;if(a<=b)return b;var d,e,f;for(f=a;f<=c;f++){d=avail_rsts.indexOf(f);if(d>-1){e=avail_rsts[d];break}}var g;for(f=a;f>=b;f--){d=avail_rsts.indexOf(f);if(d>-1){g=avail_rsts[d];break}}return e-ae?3600:(i=f(c),d>g&&i<900?900:d>h&&i<60?60:i)}var d={},e=this;auth_string="Basic "+btoa(a+":"+c),avail_rsts=[1,60,900,3600];var j=function(a){function d(b,d,e){var f="compose="+a+"&start_time="+b+"&end_time="+d+"&resolution="+g(b,d,e);return c+"?"+f}function e(a,b,c,d){var e=[];for(i=a;i<=b;i+=c){var f=[];while(d.length&&d[0].measure_time<=i)f.push(d.shift().value);var g;f.length?g=f.reduce(function(a,b){return a+b})/f.length:g=e.length?e[e.length-1]:0,e.push(g)}return e}var c="https://metrics-api.librato.com/v1/metrics";return request={},request.fire=function(a,c,f,g){function i(j){d3.json(j).header("X-Requested-With","XMLHttpRequest").header("Authorization",auth_string).header("Librato-User-Agent","cubism/"+b.version).get(function(b,j){if(!b){if(j.measurements.length===0)return;j.measurements[0].series.forEach(function(a){h.push(a)});var k="query"in j&&"next_time"in j.query;if(k)i(d(j.query.next_time,c,f));else{var l=e(a,c,f,h);g(l)}}})}var h=[];i(d(a,c,f))},request};return d.metric=function(a){return e.metric(function(b,c,d,e){j(a).fire(h(b),h(c),h(d),function(a){e(null,a)})},a+="")},d.toString=function(){return"librato"},d};var h=function(a){return Math.floor(a/1e3)};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 h=b;f!==1e4&&(h="summarize("+h+",'"+(f%36e5?f%6e4?f/1e3+"sec":f/6e4+"min":f/36e5+"hour")+"','"+d+"')"),d3.text(a+"/render?format=raw"+"&target="+encodeURIComponent("alias("+h+",'')")+"&from="+j(c-2*f)+"&until="+j(e-1e3),function(a){if(!a)return g(new Error("unable to load data"));g(null,k(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},f.gangliaWeb=function(a){var b="",c="/ganglia2/";arguments.length&&(a.host&&(b=a.host),a.uriPathPrefix&&(c=a.uriPathPrefix,c[0]!="/"&&(c="/"+c),c[c.length-1]!="/"&&(c+="/")));var d={},e=this;return d.metric=function(a){var d=a.clusterName,f=a.metricName,g=a.hostName,h=a.isReport||!1,i=a.titleGenerator||function(a){return"clusterName:"+d+" metricName:"+f+(g?" hostName:"+g:"")},j=a.onChangeCallback,k=h?"g":"m",l=e.metric(function(a,e,h,i){function j(){return"c="+d+"&"+k+"="+f+(g?"&h="+g:"")+"&cs="+a/1e3+"&ce="+e/1e3+"&step="+h/1e3+"&graphlot=1"}d3.json(b+c+"graph.php?"+j(),function(a){if(!a)return i(new Error("Unable to fetch GangliaWeb data"));i(null,a[0].data)})},i(a));return l.toString=function(){return i(a)},j&&l.on("change",j),l},d.toString=function(){return b+c},d};var m=l.prototype;b.metric=l,m.valueAt=function(){return NaN},m.alias=function(a){return this.toString=function(){return a},this},m.extent=function(){var a=0,b=this.context.size(),c,d=Infinity,e=-Infinity;while(++ae&&(e=c);return[d,e]},m.on=function(a,b){return arguments.length<2?null:this},m.shift=function(){return this},m.on=function(){return arguments.length<2?null:this},f.metric=function(a,b){function r(b,c){var d=Math.min(j,Math.round((b-g)/i));if(!d||q)return;q=!0,d=Math.min(j,d+n);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;h1&&(e.toString=function(){return b}),e};var n=6,q=p.prototype=Object.create(l.prototype);q.valueAt=function(){return+this},q.extent=function(){return[+this,+this]},m.add=r("+",function(a,b){return a+b}),m.subtract=r("-",function(a,b){return a-b}),m.multiply=r("*",function(a,b){return a*b}),m.divide=r("/",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-n;var l=(c-u)/v;if(l=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:l)}var p=this,q=++c,r=typeof i=="function"?i.call(p,k,o):i,s=typeof m=="function"?m.call(p,k,o):m,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,l=d3.format(".2s"),m=["#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?(l=a,o):l},o.colors=function(a){return arguments.length?(m=a,o):m},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=s.extent(),i=t==null?g:t;f.domain(i).range([e,0]),A=g.concat(h).every(isFinite);var j=c/a.step()&1?v:u;x.fillStyle=m[2];for(var k=0,l=b;kp&&x.fillRect(j(k),p,1,o-p)}x.fillStyle=m[3];for(k=0;kp&&x.fillRect(j(k),o-n,1,n)}x.restore()}function C(a){a==null&&(a=b-1);var c=r.valueAt(a),d=s.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),s.on("change.comparison-"+q,d))}var p=this,q=++c,r=typeof g=="function"?g.call(p,j,o):g,s=typeof h=="function"?h.call(p,j,o):h,t=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:s}),x=x.node().getContext("2d"),r.on("change.comparison-"+q,D),s.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=s,l=t,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 s=d3.format(".2s"),t=d3.format("+.0%");f.axis=function(){function g(e){var h=++c,i,j=e.append("svg").datum({id:h}).attr("width",a.size()).attr("height",Math.max(28,-g.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(f(b.invert(a)));var c=i.node().getComputedTextLength()+6;j.selectAll("text").style("fill-opacity",function(d){return Math.abs(b(d)-a) [Wiki](Home) ▸ API Reference 2 | 3 | Everything in Cubism is scoped under the `cubism` namespace. To get started, see [cubism.context](Cubism#wiki-context). 4 | 5 | ## [cubism](Cubism) 6 | 7 | * [cubism.context](Cubism#wiki-context) - create a new context. 8 | * [cubism.option](Cubism#wiki-option) - parse the query string for an optional parameter. 9 | * [cubism.options](Cubism#wiki-options) - parse the query string for optional parameters. 10 | * [cubism.version](Cubism#wiki-version) - determine the current semantic version number. 11 | 12 | ## [context](Context) 13 | 14 | * [context.step](Context#wiki-step) - get or set the context step in milliseconds. 15 | * [context.size](Context#wiki-size) - get or set the context size in number of values. 16 | * [context.serverDelay](Context#wiki-serverDelay) - get or set the context server-side delay in milliseconds. 17 | * [context.clientDelay](Context#wiki-clientDelay) - get or set the context client-side delay in milliseconds. 18 | * [context.on](Context#wiki-on) - add, get or remove a listener for context events. 19 | * [context.graphite](Context#wiki-graphite) - create a source for Graphite metrics. 20 | * [context.cube](Context#wiki-cube) - create a source for Cube metrics. 21 | * [context.librato](Context#wiki-librato) - create a source for Librato metrics. 22 | * [context.constant](Context#wiki-constant) - create a constant-value metric. 23 | * [context.horizon](Context#wiki-horizon) - create a horizon chart. 24 | * [context.comparison](Context#wiki-comparison) - create a comparison chart. 25 | * [context.axis](Context#wiki-axis) - create an axis. 26 | * [context.rule](Context#wiki-rule) - create a rule. 27 | * [context.focus](Context#wiki-focus) - focus the specified index (for mousemove interaction). 28 | * [context.scale](Context#wiki-scale) - get the x-scale. 29 | * [context.stop](Context#wiki-stop) - stop the context, pausing any events. 30 | * [context.start](Context#wiki-start) - restart the context. 31 | * [context.metric](Context#wiki-metric) - define a new metric implementation. 32 | 33 | ## [ganglia](Ganglia) 34 | 35 | * [gangliaWeb.metric](Ganglia#wiki-metric) - create a Ganglia metric. 36 | * [gangliaWeb.toString](Ganglia#wiki-metric) - returns title of the metric. 37 | 38 | ## [graphite](Graphite) 39 | 40 | * [graphite.metric](Graphite#wiki-metric) - create a Graphite metric. 41 | * [graphite.find](Graphite#wiki-find) - query the Graphite server to find metrics. 42 | * [graphite.toString](Graphite#wiki-toString) - get the Graphite host URL. 43 | 44 | ## [cube](Cube) 45 | 46 | * [cube.metric](Cube#wiki-metric) - create a Cube metric. 47 | * [cube.toString](Cube#wiki-toString) - get the Cube host URL. 48 | 49 | ## [librato](Librato) 50 | 51 | * [librato.metric](Librato#wiki-metric) - create a Librato metric. 52 | 53 | ## [metric](Metric) 54 | 55 | * [metric.valueAt](Metric#wiki-valueAt) - get the value of the metric at the given index. 56 | * [metric.extent](Metric#wiki-extent) - get the minimum and maximum metric value. 57 | * [metric.add](Metric#wiki-add) - add another metric or constant to this metric. 58 | * [metric.subtract](Metric#wiki-subtract) - subtract another metric or constant from this metric. 59 | * [metric.multiply](Metric#wiki-multiply) - multiply this metric by another metric or constant. 60 | * [metric.divide](Metric#wiki-divide) - divide this metric by another metric or constant. 61 | * [metric.shift](Metric#wiki-shift) - time-shift this metric. 62 | * [metric.on](Metric#wiki-on) - add, get or remove a listener for "change" events. 63 | * [metric.context](Metric#wiki-context) - get the metric's parent context. 64 | * [metric.toString](Metric#wiki-toString) - get the metric's associated expression, if any. 65 | 66 | ## [horizon](Horizon) 67 | 68 | * [horizon](Horizon#wiki-_horizon) - apply the horizon chart to a D3 selection. 69 | * [horizon.mode](Horizon#wiki-mode) - get or set the horizon mode ("offset" or "mirror"). 70 | * [horizon.height](Horizon#wiki-height) - get or set the chart height in pixels. 71 | * [horizon.metric](Horizon#wiki-metric) - get or set the chart metric. 72 | * [horizon.scale](Horizon#wiki-scale) - get or set the y-scale. 73 | * [horizon.extent](Horizon#wiki-extent) - get or set the chart extent (if not automatic). 74 | * [horizon.title](Horizon#wiki-title) - get or set the chart title. 75 | * [horizon.format](Horizon#wiki-format) - get or set the chart's value format function. 76 | * [horizon.colors](Horizon#wiki-colors) - get or set the horizon layer colors. 77 | * [horizon.remove](Horizon#wiki-remove) - remove the horizon chart from a D3 selection. 78 | 79 | ## [comparison](Comparison) 80 | 81 | * [comparison](Comparison#wiki-_comparison) - apply the comparison chart to a D3 selection. 82 | * [comparison.height](Comparison#wiki-height) - get or set the chart height in pixels. 83 | * [comparison.primary](Comparison#wiki-primary) - get or set the primary metric. 84 | * [comparison.secondary](Comparison#wiki-secondary) - get or set the comparison metric. 85 | * [comparison.scale](Comparison#wiki-scale) - get or set the y-scale. 86 | * [comparison.extent](Comparison#wiki-extent) - get or set the chart extent (if not automatic). 87 | * [comparison.title](Comparison#wiki-title) - get or set the chart title. 88 | * [comparison.formatPrimary](Comparison#wiki-formatPrimary) - get or set the primary value format function. 89 | * [comparison.formatChange](Comparison#wiki-formatChange) - get or set the percentage change format function. 90 | * [comparison.colors](Comparison#wiki-colors) - get or set the comparison colors (positive and negative). 91 | * [comparison.stroke](Comparison#wiki-stroke) - get or set the primary metric's stroke color. 92 | * [comparison.strokeWidth](Comparison#wiki-strokeWidth) - get or set the primary metric's stroke width. 93 | * [comparison.fill](Comparison#wiki-fill) - get or set the primary metric's fill color. 94 | * [comparison.remove](Comparison#wiki-remove) - remove the chart from a D3 selection. 95 | 96 | ## [axis](Axis) 97 | 98 | * [axis](Axis#wiki-_axis) - apply an axis generator to a D3 selection. 99 | * [axis.orient](Axis#wiki-orient) - get or set the axis orientation. 100 | * [axis.ticks](Axis#wiki-ticks) - control how ticks are generated for the axis. 101 | * [axis.tickSubdivide](Axis#wiki-tickSubdivide) - optionally subdivide ticks uniformly. 102 | * [axis.tickSize](Axis#wiki-tickSize) - specify the size of major, minor and end ticks. 103 | * [axis.tickPadding](Axis#wiki-tickPadding) - specify padding between ticks and tick labels. 104 | * [axis.tickFormat](Axis#wiki-tickFormat) - override the tick formatting for labels. 105 | * [axis.remove](Axis#wiki-remove) - remove the axis from a D3 selection. 106 | 107 | ## [rule](Rule) 108 | 109 | * [rule](Rule#wiki-_rule) - apply a rule generator to a D3 selection. 110 | * [rule.metric](Rule#wiki-metric) - generate rules at each non-zero value for the given metric. 111 | * [rule.remove](Rule#wiki-remove) - remove the rule from a D3 selection. -------------------------------------------------------------------------------- /docs/Axis.md: -------------------------------------------------------------------------------- 1 | > [Wiki](Home) ▸ [API Reference](API-Reference) ▸ Axis 2 | 3 | A thin wrapper on top of D3’s [d3.svg.axis](/mbostock/d3/wiki/SVG-Axes) component that automatically updates whenever the associated [context changes](Context#wiki-on). To create an axis, first create a [context](Context). For example: 4 | 5 | ```js 6 | var context = cubism.context(), // a default context 7 | axis = context.axis(); // a default axis 8 | ``` 9 | 10 | # axis(selection) 11 | 12 | Apply the axis to a [D3 selection](/mbostock/d3/wiki/Selections) or [transition](/mbostock/d3/wiki/Transitions) containing one or more SVG element. For example: 13 | 14 | ```js 15 | d3.select("body").append("svg") 16 | .attr("class", "axis") 17 | .attr("width", 1440) 18 | .attr("height", 30) 19 | .append("g") 20 | .attr("transform", "translate(0,30)") 21 | .call(axis); 22 | ``` 23 | 24 | To add 20px of padding on both sides, you might say: 25 | 26 | ```js 27 | d3.select("body").append("svg") 28 | .attr("class", "axis") 29 | .attr("width", 1480) 30 | .attr("height", 30) 31 | .style("margin-left", "-20px") 32 | .append("g") 33 | .attr("transform", "translate(20,30)") 34 | .call(axis); 35 | ``` 36 | 37 | If you want to customize the display of the axis further, you can post-process the axis display by selecting and modifying elements after the context’s change event. For example, to use left-aligned tick labels: 38 | 39 | ```js 40 | d3.select("body").append("svg") 41 | .attr("class", "axis") 42 | .attr("width", 1920) 43 | .attr("height", 1080) 44 | .call(axis) 45 | .call(function(svg) { 46 | context.on("change.axis", function() { 47 | svg.selectAll("text") 48 | .attr("x", 10) 49 | .attr("y", 10) 50 | .attr("dy", ".71em") 51 | .attr("text-anchor", "start"); 52 | }); 53 | }); 54 | ``` 55 | 56 | # axis.orient([orientation]) 57 | 58 | Get or set the axis orientation. If orientation is specified, sets the axis orientation and returns the axis. If orientation is not specified, returns the current orientation, which defaults to "bottom". Valid values are "top", "bottom", "left" and "right". For a vertical axis, specify "left" or "right"; for a horizontal axis, specify "top" or "bottom". 59 | 60 | # axis.ticks([arguments…]) 61 | 62 | Get or set the arguments that are passed to the underlying scale’s tick function. The specified arguments are passed to scale.ticks to compute the tick values. For [quantitative scales](/mbostock/d3/wiki/Quantitative-Scales#wiki-linear_ticks), specify the desired tick count such as `axis.ticks(20)`. For [time scales](/mbostock/d3/wiki/Time-Scales#wiki-ticks), you can pass in a count or a function, such as `axis.ticks(d3.time.minutes, 15)`. 63 | 64 | The arguments are also passed to the scale’s tickFormat method to generate the default tick format. Thus, for [log scales](/mbostock/d3/wiki/Quantitative-Scales#wiki-log_tickFormat), you might specify both a count and a tick format function. For example: 65 | 66 | ```js 67 | axis.ticks(20, function(d) { return "$" + d.toFixed(2); }); 68 | ``` 69 | 70 | # axis.tickSubdivide([count]) 71 | 72 | Get or set the tick subdivision count. If count is specified, sets the number of uniform subdivisions to make between major tick marks and returns the axis. If count is not specified, returns the current subdivision count which defaults to zero. 73 | 74 | # axis.tickSize([major[‍[, minor], end]‍]) 75 | 76 | Get or set the size of major, minor and end ticks. The end ticks are determined by the associated scale's domain extent, and are part of the generated path domain rather than a tick line. Note that the end ticks may be close or even overlapping with the first or last tick. An end tick size of 0 suppresses end ticks. For example: 77 | 78 | ```js 79 | axis.tickSize(6); // sets the major, minor and end to 6 80 | axis.tickSize(6, 0); // sets major and minor to 6, end to 0 81 | axis.tickSize(6, 3, 0); // sets major to 6, minor to 3, and end to 0 82 | ``` 83 | 84 | # axis.tickPadding([padding]) 85 | 86 | Set or get the padding between ticks and tick labels. If padding is specified, sets the padding to the specified value in pixels and returns the axis. If padding is not specified, returns the current padding which defaults to 3 pixels. 87 | 88 | # axis.tickFormat([format]) 89 | 90 | Set or get the tick value formatter for labels. If format is specified, sets the format to the specified function and returns the axis. If format is not specified, returns the current format function, which defaults to null. A null format indicates that the scale's default formatter should be used, which is generated by calling [scale.tickFormat](/mbostock/d3/wiki/Quantitative-Scales#wiki-linear_tickFormat). In this case, the arguments specified by [ticks](#wiki-ticks) are likewise passed to scale.tickFormat. 91 | 92 | See [d3.format](/mbostock/d3/wiki/Formatting#wiki-d3_format) for help creating formatters. For example, `axis.tickFormat(d3.format(",.0f"))` will display integers with comma-grouping for thousands. 93 | 94 | # axis.remove(selection) 95 | 96 | Removes the axis from a [D3 selection](/mbostock/d3/wiki/Selections), and removes any associated listeners. This method only removes the contents added by the axis itself; typically, you also want to call [remove](/mbostock/d3/wiki/Selections#wiki-remove) on the selection. For example: 97 | 98 | ```js 99 | d3.select(".axis") 100 | .call(axis.remove) 101 | .remove(); 102 | ``` 103 | 104 | Requires that the elements in the selection were previously bound to this axis. -------------------------------------------------------------------------------- /docs/Comparison.md: -------------------------------------------------------------------------------- 1 | > [Wiki](Home) ▸ [API Reference](API-Reference) ▸ Comparison 2 | 3 | ![Comparison Chart](comparison.png) 4 | 5 | A comparison chart, showing a primary metric in the context of a secondary metric. To create a comparison chart, first create a [context](Context). For example: 6 | 7 | ```js 8 | var context = cubism.context(), // a default context 9 | comparison = context.comparison(); // a default comparison chart 10 | ``` 11 | 12 | Next you'll need at least two metrics to visualize, which typically requires one or more source, such as [Graphite](Graphite) or [Cube](Cube): 13 | 14 | ```js 15 | var cube = context.cube("http://cube.example.com"), 16 | primary = cube.metric("sum(request)"), 17 | secondary = primary.shift(-7 * 24 * 60 * 60 * 1000); 18 | ``` 19 | 20 | # comparison(selection) 21 | 22 | Apply the comparison chart to a [D3 selection](/mbostock/d3/wiki/Selections). The default primary and secondary metrics accessor assume that each element's data is a two-element array of metrics. Thus, by binding a two-dimensional array of metrics to a selection, you can create one or more comparison charts: 23 | 24 | ```js 25 | d3.select("body").selectAll(".comparison") 26 | .data([[primary, secondary]]) 27 | .enter().append("div") 28 | .attr("class", "comparison") 29 | .call(comparison); 30 | ``` 31 | 32 | # comparison.height([height]) 33 | 34 | Get or set the chart height in pixels. If height is specified, sets the chart height to the specified value in pixels and returns the comparison chart. If height is not specified, returns the current height, which defaults to 120 pixels. 35 | 36 | # comparison.primary([metric]) 37 | 38 | Get or set the primary chart metric. If metric is specified, sets the primary metric accessor and returns the chart. The metric may be specified either as a single [metric](Metric), or as a function that returns a metric. If metric is not specified, returns the current primary metric accessor, which defaults to `function(d) { return d[0]; }` (so that you can bind the selection to a two-dimensional array of metrics, as demonstrated above). When the metric is specified as an accessor function, it is invoked for each element in the selection, being passed the bound data (`d`) and index (`i`). 39 | 40 | # comparison.secondary([metric]) 41 | 42 | Get or set the secondary chart metric. If metric is specified, sets the secondary metric accessor and returns the chart. The metric may be specified either as a single [metric](Metric), or as a function that returns a metric. If metric is not specified, returns the current secondary metric accessor, which defaults to `function(d) { return d[1]; }` (so that you can bind the selection to a two-dimensional array of metrics, as demonstrated above). When the metric is specified as an accessor function, it is invoked for each element in the selection, being passed the bound data (`d`) and index (`i`). 43 | 44 | # comparison.scale([scale]) 45 | 46 | Get or set the chart y-scale. If scale is specified, sets the chart y-scale to the specified value and returns the chart. If scale is not specified, returns the current y-scale which defaults to a [linear scale](/mbostock/d3/wiki/Quantitative-Scales#wiki-linear) with [rounding](/mbostock/d3/wiki/Quantitative-Scales#wiki-linear_rangeRound). For example, this method can be used to apply a [square-root](/mbostock/d3/wiki/Quantitative-Scales#wiki-sqrt) transform. 47 | 48 | # comparison.extent([extent]) 49 | 50 | Get or set the chart extent (if not automatic). If extent is specified, sets the chart extent accessor and returns the chart. The extent may be specified either as an array of two numbers, or as a function that returns such an array. If extent is not specified, returns the current extent accessor, which defaults to null. When the extent is specified as an accessor function, it is invoked for each element in the selection, being passed the bound data (`d`) and index (`i`). If the extent is null, it will be computed automatically via [metric.extent](Metric#wiki-extent) for the primary metric. 51 | 52 | # comparison.title([title]) 53 | 54 | Get or set the chart title. If title is specified, sets the chart title accessor and returns the chart. The title may be specified either as a string, or as a function that returns a string. If title is not specified, returns the current title accessor, which defaults to the identity function. When the title is specified as an accessor function, it is invoked for each element in the selection, being passed the bound data (`d`) and index (`i`). If the title is null, no title will be displayed. 55 | 56 | # comparison.formatPrimary([format]) 57 | 58 | Get or set the chart's primary value format function. If format is specified, sets the chart primary value formatter and returns the chart. If format is not specified, returns the current formatter, which defaults to `d3.format(".2s")`; see [d3.format](/mbostock/d3/wiki/Formatting#wiki-d3_format) for details. 59 | 60 | # comparison.formatChange([format]) 61 | 62 | Get or set the chart's change format function. If format is specified, sets the chart change formatter and returns the chart. If format is not specified, returns the current change formatter, which defaults to `d3.format("+.0%")`; see [d3.format](/mbostock/d3/wiki/Formatting#wiki-d3_format) for details. The change formatter is passed (primaryValue - secondaryValue) / secondaryValue and is typically used to display percentage change. 63 | 64 | # comparison.colors([colors]) 65 | 66 | Get or set the comparison colors. If colors is specified, sets the negative and positive colors to the specified four-element array of colors and returns the chart. If colors is not specified, returns the current four-element array of colors, which defaults to #9ecae1 #225b84 #a1d99b #22723a. These colors are designed for a light background. For colors against a dark background, try #3182bd #add8e6 #31a354 #90ee90. 67 | 68 | # comparison.strokeWidth([width]) 69 | 70 | Get or set the primary metric stroke width, in pixels. If width is specified, sets the stroke width in pixels and returns the chart. If width is not specified, returns the current stroke width, which defaults to 1.5 pixels. 71 | 72 | # comparison.remove(selection) 73 | 74 | Removes the comparison chart from a [D3 selection](/mbostock/d3/wiki/Selections), and removes any of the chart's associated listeners. This method only removes the contents added by the chart itself; typically, you also want to call [remove](/mbostock/d3/wiki/Selections#wiki-remove) on the selection. For example: 75 | 76 | ```js 77 | d3.select(".comparison") 78 | .call(comparison.remove) 79 | .remove(); 80 | ``` 81 | 82 | Requires that the elements in the selection were previously bound to this chart. -------------------------------------------------------------------------------- /docs/Context.md: -------------------------------------------------------------------------------- 1 | > [Wiki](Home) ▸ [API Reference](API-Reference) ▸ Context 2 | 3 | A context keeps track of the metric resolution (the [step](#wiki-step), in milliseconds) and the number of metric values to fetch and display (the [size](#wiki-size)). Contexts are the root object of any Cubism dashboard, and are used to create charts and metrics, while keeping them in-sync. 4 | 5 | To create a default context, say: 6 | 7 | ```js 8 | var context = cubism.context(); 9 | ``` 10 | 11 | To create a custom context, you might say: 12 | 13 | ```js 14 | var context = cubism.context() 15 | .serverDelay(30 * 1000) // allow 30 seconds of collection lag 16 | .step(5 * 60 * 1000) // five minutes per value 17 | .size(1920); // fetch 1920 values (1080p) 18 | ``` 19 | 20 | Contexts are required to create sources (such as [Cube](#wiki-cube) and [Graphite](#wiki-graphite)), which are in turn required to create [metrics](Metric). Contexts are also required to create charts (such as [horizon charts](#wiki-horizon) and [comparison charts](#wiki-comparison)). 21 | 22 | # context.step([step]) 23 | 24 | Get or set the context step in milliseconds. If step is specified, sets the new context step and returns the context; if step is not specified, returns the current step. The step defaults to ten seconds (1e4). Note: the step cannot be changed after the context is initialized, which occurs shortly after creation via a brief timeout. 25 | 26 | # context.size([size]) 27 | 28 | Get or set the context size in number of values. If size is specified, sets the new context size and returns the context; if size is not specified, returns the current size. The size defaults to 1440 (four hours at the default step of ten seconds). Note: the size cannot be changed after the context is initialized, which occurs shortly after creation via a brief timeout. 29 | 30 | # context.serverDelay([delay]) 31 | 32 | Get or set the context server-side delay in milliseconds. If delay is specified, sets the new context server delay and returns the context; if delay is not specified, returns the current server delay. The server delay defaults to five seconds (5e3). The server delay is the amount of time the context waits for the server to compute or collect metrics. This delay may result from clock skew (either between the client and server, or between the server and the hosts generating metrics) or from delays collecting metrics from various hosts. 33 | 34 | # context.clientDelay([delay]) 35 | 36 | Get or set the context client-side delay in milliseconds. If delay is specified, sets the new context client delay and returns the context; if delay is not specified, returns the current client delay. The client delay defaults to five seconds (5e3). The client delay is the amount of additional time the context waits to fetch metrics from the server. The client and server delay combined represent the age of the most recent displayed metric. The client delay exists so that the charts can be redrawn concurrently, rather than redrawing each chart as the associated metric arrives; this reduces the distracting effect of many charts updating simultaneously. Note: the client delay need only consider the expected delay when incrementally fetching the next metric value, not the (typically much more expensive) initial load. 37 | 38 | # context.graphite(url) 39 | 40 | Create a source for [Graphite metrics](Graphite). 41 | 42 | # context.cube(url) 43 | 44 | Create a source for [Cube metrics](Cube). 45 | 46 | # context.librato(user, token) 47 | 48 | Create a source for [Librato metrics](Librato). 49 | 50 | # context.constant(value) 51 | 52 | Create a constant-value [metric](Metric). The specified value is coerced to a number. 53 | 54 | # context.horizon() 55 | 56 | Create a [horizon chart](Horizon). 57 | 58 | # context.comparison() 59 | 60 | Create a [comparison chart](Comparison). 61 | 62 | # context.axis() 63 | 64 | Create an [axis](Axis). 65 | 66 | # context.rule() 67 | 68 | Create a [rule](Rule). 69 | 70 | # context.scale 71 | 72 | The context's x-scale; a [d3.time.scale](/mbostock/d3/wiki/Time-Scales). The domain of the scale is updated automatically immediately before a "change" event is dispatched. The range is likewise set automatically based on the context [size](#wiki-size). 73 | 74 | # context.on(type[, listener]) 75 | 76 | Add, get or remove a listener for context events. This method is typically used only by other Cubism components, but can be used if you want to perform other actions concurrently when new metrics are displayed (such as custom visualizations). The following types of events are supported: 77 | 78 | * change events are dispatched at the time new metrics should be displayed. This event is used, for example, by charts to render the new values. Listeners are passed two arguments: the start time (a Date, inclusive) and the stop time (a Date, exclusive). The `this` context of the listener is the context. Note that the stop time will be slightly before the current time (now) based on the [server delay](#wiki-serverDelay) plus the [client delay](#wiki-clientDelay). For example, if the combined delay is five seconds, and the step interval is ten seconds, then change events will be dispatched at :05 seconds past the minute, :15 seconds, :25 seconds, etc. 79 | 80 | * beforechange events are dispatched immediately prior to change events; otherwise, they are identical to change events. Listeners are passed two arguments: the start time (a Date, inclusive) and the stop time (a Date, exclusive). The `this` context of the listener is the context. This event is typically used by metrics to shift cached values. 81 | 82 | * prepare events are dispatched some time before change events (and before beforechange events), typically to pre-fetch new metric values. Listeners are passed two arguments: the start time (a Date, inclusive) and the stop time (a Date, exclusive). The `this` context of the listener is the context. Note that the stop time will be slightly before the current time (now) based on the [server delay](#wiki-serverDelay). For example, if the server delay is four seconds, and the step interval is ten seconds, then prepare events will be dispatched at :04 seconds past the minute, :14 seconds, :24 seconds, etc. 83 | 84 | * focus events are dispatched to coordinate interaction with a particular value. Typically, this event is dispatched in response to a mousemove event on a particular chart. The listener is passed one argument: the focus index, which is a value between 0 (inclusive) and the context size (exclusive). A null value indicates that no value should be focused; often this is interpreted as the latest value (size - 1). 85 | 86 | This method follows the same API as D3's [dispatch.on](/mbostock/d3/wiki/Internals#wiki-dispatch_on). If listener is specified and non-null, sets the callback function for events of the specified type and returns the context; any existing listener for the same type will be replaced. If listener is specified and null, clears the callback function for events of the specified type (if any) and returns the context. If listener is not specified, returns the current callback function, if any. The type can be further qualified with a namespace so that multiple listeners can receive the same events; for example, you might use "beforechange.foo" and "beforechange.bar" to register two listeners for beforechange events. 87 | 88 | Given an element with the id "update-time", here's how you might display the most recent update time: 89 | 90 | ```js 91 | context.on("change", function(start, stop) { 92 | d3.select("#update-time").text("Last updated at " + stop + "."); 93 | }); 94 | ``` 95 | 96 | # context.focus(index) 97 | 98 | Sets the focus index and returns the context. This method also dispatches a "focus" event to all registered listeners. This method is typically called by chart implementations to indicate that the user is interested in a particular time. The index should range from 0 (inclusive) to the context size (exclusive), or have the value null indicating that no particular time is focused. 99 | 100 | For example, if you wanted to reposition the horizon charts' value labels on focus, you might say: 101 | 102 | ```js 103 | context.on("focus", function(i) { 104 | d3.selectAll(".value").style("right", i == null ? null : context.size() - i + "px"); 105 | }); 106 | ``` 107 | 108 | # context.stop() 109 | 110 | Pause the context, stopping any events from being dispatched. Returns the context. This method can be used to dispose of a context, if it is no longer needed. 111 | 112 | # context.start() 113 | 114 | Resume the context, if it was previously paused. Returns the context. 115 | 116 | # context.metric(request[, name]) 117 | 118 | Returns a new metric using the specified request function. If a name is specified, the returned metric's toString function will return the specified name. This method can be used to define a new data source, in case you don't want to use one of the built-in data sources such as [[Graphite]] or [[Cube]]. The request function will be invoked with the start time (a Date), the stop time (another Date), the step interval (a number in milliseconds) and the callback function for when results are available. For example, to implement a metric of random values: 119 | 120 | ```js 121 | context.metric(function(start, stop, step, callback) { 122 | var values = []; 123 | 124 | // convert start & stop to milliseconds 125 | start = +start; 126 | stop = +stop; 127 | 128 | while (start < stop) { 129 | start += step; 130 | values.push(Math.random()); 131 | } 132 | 133 | callback(null, values); 134 | }); 135 | ``` 136 | 137 | The result callback takes two arguments, in Node.js convention: an error (which should be null if there was no error, or an Exception if there was), and an array of values (numbers). If some of the data is undefined, the corresponding slots in the array should be NaN. 138 | 139 | For another example, see how the built-in [Cube source](/square/cubism/blob/master/src/cube.js#L7-16) is implemented. -------------------------------------------------------------------------------- /docs/Cube.md: -------------------------------------------------------------------------------- 1 | > [Wiki](Home) ▸ [API Reference](API-Reference) ▸ Cube 2 | 3 | A source for [Cube](http://square.github.io/cube/) metrics. To create a source, first create a [context](Context). Then, use [context.cube](Context#wiki-cube) to specify the URL of the Cube evaluator. For example: 4 | 5 | ```js 6 | var context = cubism.context(), // a default context 7 | cube = context.cube("http://cube.example.com"); 8 | ``` 9 | 10 | # cube.metric(expression) 11 | 12 | Creates a new [metric](Metric) for the given Cube expression. For example, if you were using Cube to collect "request" events, you could query the number of request events by saying: 13 | 14 | ```js 15 | var requests = cube.metric("sum(request)"); 16 | ``` 17 | 18 | For more information on metric expressions, see [Cube's documentation](/square/cube/wiki/Queries). 19 | 20 | # cube.toString() 21 | 22 | Returns the URL of the Cube server; the first argument to the [constructor](#wiki-cube). -------------------------------------------------------------------------------- /docs/Cubism.md: -------------------------------------------------------------------------------- 1 | > [Wiki](Home) ▸ [API Reference](API-Reference) ▸ Cubism 2 | 3 | The Cubism namespace. 4 | 5 | # cubism.context() 6 | 7 | Create a new [context](Context), which specifies the resolution and duration of metrics to visualize. For example, you might say: 8 | 9 | ```js 10 | var context = cubism.context() 11 | .serverDelay(30 * 1000) // allow 30 seconds of collection lag 12 | .step(5 * 60 * 1000) // five minutes per value 13 | .size(1920); // fetch 1920 values (1080p) 14 | ``` 15 | 16 | Contexts are required to create sources (so as to talk to [Cube](Cube) or [Graphite](Graphite)), which are in turn required to create [metrics](Metric). Contexts are also required to create charts (such as [horizon charts](Horizon) and [comparison charts](Comparison)). Contexts keep everything in-sync. 17 | 18 | # cubism.option(name[, value]) 19 | 20 | Parses the query string (`location.search`), returning the value of the query parameter with the specified name. If no matching parameter is found, then the default value is returned; if no default value is specified, returns undefined. For example: 21 | 22 | ```js 23 | var filter = cubism.option("filter", "hosts.foo*"); 24 | ``` 25 | 26 | Given the query string "?filter=hosts.bar*", the returned value is "hosts.bar*"; however, given no query string, the default value "hosts.foo*" is returned. This method can be used to make configurable dashboards, often in conjunction with [graphite.find](Graphite#wiki-find). 27 | 28 | # cubism.options(name[, values]) 29 | 30 | Parses the query string (`location.search`), returning the values of any query parameters with the specified name. If no matching parameter is found, then the default values are returned; if no default values are specified, returns the empty array. For example: 31 | 32 | ```js 33 | var filters = cubism.options("filter", ["foo*"]); 34 | ``` 35 | 36 | Given the query string "?filter=bar\*&filter=foo\*", the returned value is ["bar\*", "foo\*"]; however, given no query string, the default value ["foo\*"] is returned. This method can be used to make configurable dashboards, often in conjunction with [graphite.find](Graphite#wiki-find). 37 | 38 | # cubism.version 39 | 40 | The [semantic version](http://semver.org/), which is a string of the form "X.Y.Z". X is the major version number, Y is the minor version number, and Z is the patch version number. -------------------------------------------------------------------------------- /docs/Ganglia.md: -------------------------------------------------------------------------------- 1 | > [Wiki](Home) ▸ [API Reference](API-Reference) ▸ Ganglia 2 | 3 | A source for [Ganglia](http://ganglia.info/) metrics. To create a source, first create a [context](Context). Then, use [context.graphite](Context#wiki-ganglia) to specify the URL and path to the Ganglia Web installation server. : 4 | 5 | ```js 6 | var context = cubism.context() 7 | .serverDelay(15 * 1000) // allow 15 seconds of collection lag 8 | .step(15000) // fifteen seconds per value 9 | .size(1440); // fetch 1440 values (720p) 10 | var ganglia = context.gangliaWeb( {"host": 'https://ganglia.domain.com', "uriPathPrefix": '/ganglia/'} ); 11 | ``` 12 | After you create the context add some metrics e.g. 13 | 14 | # gangliaWeb.metric 15 | 16 | Creates a new [metric](Metric) for a given Ganglia metric you need to specify clusterName, hostName, metricName and whether it's a report (boolean) 17 | 18 | ```js 19 | var load_metrics = [ 20 | ganglia.metric( { "clusterName": "MYCLUSTE", "hostName": "web1", "metricName": "load_one", "isReport": false} ).alias("web load"), 21 | ganglia.metric( { "clusterName": "MYCLUSTE", "hostName": "web2", "metricName": "load_one", "isReport": false} ).alias("web load") 22 | ]; 23 | ``` 24 | 25 | After that you just need to add some colors to use and append the metrics into the DOM 26 | 27 | ```js 28 | var horizon = context.horizon().colors(["#08519c", "#*82bd", "#6baed6", "#fee6ce", "#fdae6b", "#e6550d" ]); 29 | d3.select("body").selectAll(".axis") 30 | .data(["top", "bottom"]) 31 | .enter().append("div").attr("class", "fluid-row") 32 | .attr("class", function(d) { return d + " axis"; }) 33 | .each(function(d) { d3.select(this).call(context.axis().ticks(12).orient(d)); }); 34 | d3.select("body").append("div") 35 | .attr("class", "rule") 36 | .call(context.rule()); 37 | d3.select("body").selectAll(".horizon") 38 | .data(load_metrics) 39 | .enter().insert("div", ".bottom") 40 | .attr("class", "horizon").call(horizon.extent([0, 32])); 41 | context.on("focus", function(i) { 42 | d3.selectAll(".value").style("right", i == null ? null : context.size() - 1 - i + "px"); 43 | }); 44 | ``` 45 | 46 | Please note the **horizon.extent([0, 32])**. Those are minimum and maximum values for your metric. Choose those carefully. 47 | 48 | # gangliaWeb.toString() 49 | 50 | Returns the title of the Ganglia metric [constructor](#wiki-ganglia). -------------------------------------------------------------------------------- /docs/Graphite.md: -------------------------------------------------------------------------------- 1 | > [Wiki](Home) ▸ [API Reference](API-Reference) ▸ Graphite 2 | 3 | A source for [Graphite](http://graphite.wikidot.com/) metrics. To create a source, first create a [context](Context). Then, use [context.graphite](Context#wiki-graphite) to specify the URL of the Graphite server. For example: 4 | 5 | ```js 6 | var context = cubism.context(), // a default context 7 | graphite = context.graphite("http://graphite.example.com"); 8 | ``` 9 | 10 | # graphite.metric(expression) 11 | 12 | Creates a new [metric](Metric) for the given Graphite expression. For example, if you were using Graphite in conjunction with [Collectd](http://collectd.org/)'s [CPU plugin](http://collectd.org/wiki/index.php/Plugin:CPU), you could monitor the CPU utilization of the machine "foo" by saying: 13 | 14 | ```js 15 | var foo = graphite.metric("sumSeries(nonNegativeDerivative(exclude(hosts.foo.cpu.*.*,'idle')))"); 16 | ``` 17 | 18 | For more information on metric expressions, see Graphite's documentation on the [target parameter](http://graphite.readthedocs.org/en/latest/render_api.html#target). 19 | 20 | When the step is 10 seconds, the metric is sent to Graphite as-is; for other step intervals, the metric will be summarized automatically using Graphite's [summarize function](http://graphite.readthedocs.org/en/1.0/functions.html#graphite.render.functions.summarize). The default summation function is "sum", but you can change this using the returned metric's summarize function. For example, to summarize using average: 21 | 22 | ```js 23 | var foo = graphite.metric("foo").summarize("avg"); 24 | ``` 25 | 26 | # graphite.find(pattern, callback) 27 | 28 | Queries the Graphite server to look for metrics that match the specified pattern, invoking the specified callback when the results are available. The callback is passed two arguments: an error, if any, and an array of string results. For example, to see which hosts have CPU metrics available, you might say: 29 | 30 | ```js 31 | graphite.find("hosts.*.cpu.0", function(error, results) { 32 | console.log(results); // ["hosts.foo.cpu.0.", "hosts.bar.cpu.0.", etc.] 33 | }); 34 | ``` 35 | 36 | # graphite.toString() 37 | 38 | Returns the URL of the Graphite server; the first argument to the [constructor](#wiki-graphite). -------------------------------------------------------------------------------- /docs/Home.md: -------------------------------------------------------------------------------- 1 | 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). 2 | 3 | ## Resources 4 | 5 | * [Introduction](http://square.github.io/cubism/) 6 | * [API Reference](API-Reference) 7 | * [Support](http://stackoverflow.com/questions/tagged/cubism.js) 8 | * [Tutorials and Talks](Tutorials) 9 | * [JS courses](https://skillcombo.com/topic/javascript/) 10 | 11 | 12 | ## Contributing 13 | 14 | Want to add support for a new backend or visualization? We'd love for you to participate in the development of Cubism. Before we can accept your pull request, please sign our [Individual Contributor License Agreement][1]. It's a short form that covers our bases and makes sure you're eligible to contribute. Thank you! 15 | 16 | [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 -------------------------------------------------------------------------------- /docs/Horizon.md: -------------------------------------------------------------------------------- 1 | > [Wiki](Home) ▸ [API Reference](API-Reference) ▸ Horizon 2 | 3 | ![Horizon Chart](horizon.png) 4 | 5 | A horizon chart. To create a [horizon chart](http://vis.berkeley.edu/papers/horizon/), first create a [context](Context). For example: 6 | 7 | ```js 8 | var context = cubism.context(), // a default context 9 | horizon = context.horizon(); // a default horizon chart 10 | ``` 11 | 12 | Next you'll need an array of metrics to visualize, which typically requires one or more source, such as [Graphite](Graphite) or [Cube](Cube): 13 | 14 | ```js 15 | var cube = context.cube("http://cube.example.com"); 16 | 17 | var metrics = [ 18 | cube.metric("sum(request.eq(language,'en'))"), 19 | cube.metric("sum(request.eq(language,'es'))"), 20 | cube.metric("sum(request.eq(language,'de'))"), 21 | cube.metric("sum(request.eq(language,'fr'))") 22 | ]; 23 | ``` 24 | 25 | Equivalently, you could use `cube.metric` as the [metric accessor](#wiki-metric), and simplify the definition of the metrics as an array of strings: 26 | 27 | ```js 28 | var horizon = context.horizon() 29 | .metric(cube.metric); 30 | 31 | var metrics = [ 32 | "sum(request.eq(language,'en'))", 33 | "sum(request.eq(language,'es'))", 34 | "sum(request.eq(language,'de'))", 35 | "sum(request.eq(language,'fr'))" 36 | ]; 37 | ``` 38 | 39 | Or you could go one step further and use an array of languages: 40 | 41 | ```js 42 | var horizon = context.horizon() 43 | .metric(function(d) { return cube.metric("sum(request.eq(language,'" + d + "'))"); }); 44 | 45 | var metrics = [ 46 | "en", 47 | "es", 48 | "de", 49 | "fr" 50 | ]; 51 | ``` 52 | 53 | # horizon(selection) 54 | 55 | Apply the horizon chart to a [D3 selection](/mbostock/d3/wiki/Selections). By binding the metrics to a selection, you can create a horizon chart per metric: 56 | 57 | ```js 58 | d3.select("body").selectAll(".horizon") 59 | .data(metrics) 60 | .enter().append("div") 61 | .attr("class", "horizon") 62 | .call(horizon); 63 | ``` 64 | 65 | # horizon.mode([mode]) 66 | 67 | Get or set the horizon mode, which controls how negative values are displayed. If mode is specified, sets the mode and returns the horizon chart. If mode is not specified, returns the current mode. Defaults to "offset". The following modes are supported, as illustrated in this diagram: 68 | 69 | ![horizon modes](http://vis.berkeley.edu/papers/horizon/construction.png) 70 | 71 | * offset mode translates negative values up, so they descend from the top edge of the chart. 72 | 73 | * mirror mode inverts negative values, equivalent to taking the absolute value. 74 | 75 | # horizon.height([pixels]) 76 | 77 | Get or set the chart height in pixels. If height is specified, sets the chart height to the specified value in pixels and returns the horizon chart. If height is not specified, returns the current height, which defaults to 30 pixels. 78 | 79 | # horizon.metric([metric]) 80 | 81 | Get or set the chart metric accessor. If metric is specified, sets the chart metric accessor and returns the horizon chart. The metric may be specified either as a single [metric](Metric), or as a function that returns a metric. If metric is not specified, returns the current metric accessor, which defaults to the identity function (so that you can bind the selection to an array of metrics, as demonstrated above). When the metric is specified as an accessor function, it is invoked for each element in the selection, being passed the bound data (`d`) and index (`i`). 82 | 83 | # horizon.scale([scale]) 84 | 85 | Get or set the chart y-scale. If scale is specified, sets the chart y-scale to the specified value and returns the chart. If scale is not specified, returns the current y-scale which defaults to a [linear scale](/mbostock/d3/wiki/Quantitative-Scales#wiki-linear) with [rounding](/mbostock/d3/wiki/Quantitative-Scales#wiki-linear_rangeRound). For example, this method can be used to apply a [square-root](/mbostock/d3/wiki/Quantitative-Scales#wiki-sqrt) transform. 86 | 87 | # horizon.extent([extent]) 88 | 89 | Get or set the chart extent (if not automatic). If extent is specified, sets the chart extent accessor and returns the horizon chart. The extent may be specified either as an array of two numbers, or as a function that returns such an array. If extent is not specified, returns the current extent accessor, which defaults to null. When the extent is specified as an accessor function, it is invoked for each element in the selection, being passed the bound data (`d`) and index (`i`). If the extent is null, it will be computed automatically via [metric.extent](Metric#wiki-extent). 90 | 91 | # horizon.title([title]) 92 | 93 | Get or set the chart title. If title is specified, sets the chart title accessor and returns the horizon chart. The title may be specified either as a string, or as a function that returns a string. If title is not specified, returns the current title accessor, which defaults to the identity function. When the title is specified as an accessor function, it is invoked for each element in the selection, being passed the bound data (`d`) and index (`i`). If the title is null, no title will be displayed. 94 | 95 | # horizon.format([format]) 96 | 97 | Get or set the chart's value format function. If format is specified, sets the chart value formatter and returns the horizon chart. If format is not specified, returns the current formatter, which defaults to `d3.format(".2s")`; see [d3.format](/mbostock/d3/wiki/Formatting#wiki-d3_format) for details. 98 | 99 | # horizon.colors([colors]) 100 | 101 | Get or set the horizon layer colors. If colors is specified, sets the horizon layer colors to the specified array of colors and returns the horizon chart. If colors is not specified, returns the current array of colors, which defaults to #08519c #3182bd #6baed6 #bdd7e7 #bae4b3 #74c476 #31a354 #006d2c. The array of colors must have an even number of elements; color.length / 2 determines the number of bands. The first half of the array lists the colors used for the negative bands, and the second half lists the colors for the positive bands. 102 | 103 | # horizon.remove(selection) 104 | 105 | Removes the horizon chart from a [D3 selection](/mbostock/d3/wiki/Selections), and removes any of the chart's associated listeners. This method only removes the contents added by the horizon chart itself; typically, you also want to call [remove](/mbostock/d3/wiki/Selections#wiki-remove) on the selection. For example: 106 | 107 | ```js 108 | d3.select(".horizon") 109 | .call(horizon.remove) 110 | .remove(); 111 | ``` 112 | 113 | Requires that the elements in the selection were previously bound to this chart. -------------------------------------------------------------------------------- /docs/Librato.md: -------------------------------------------------------------------------------- 1 | > [Wiki](Home) ▸ [API Reference](API-Reference) ▸ Librato 2 | 3 | A source for [Librato](https://metrics.librato.com/sign_in) metrics. To create a source, first create a [context](Context). Then, use [context.librato](Context#wiki-cube) to specify your librato credentials. For example: 4 | 5 | ```js 6 | var context = cubism.context(), // a default context 7 | librato = context.librato("foo@gmail.com", "8585ae5c30d55ddef4"); 8 | ``` 9 | 10 | # librato.metric(metric name, source) 11 | 12 | Creates a new [metric](Metric) for the given librato [composite metric query](http://support.metrics.librato.com/knowledgebase/articles/337431-composite-metrics-language-specification). For example: 13 | 14 | ```js 15 | var metrics = librato.metric("sum(series(\"hgsc_cpu_used\", \"ardmore\"))"); 16 | ``` 17 | 18 | -------------------------------------------------------------------------------- /docs/Metric.md: -------------------------------------------------------------------------------- 1 | > [Wiki](Home) ▸ [API Reference](API-Reference) ▸ Metric 2 | 3 | A metric; a mechanism for fetching time-series data. To create a metric, you need a context and a source, such as [Cube](Cube) or [Graphite](Graphite). For example: 4 | 5 | ```js 6 | var context = cubism.context(), // a default context 7 | cube = context.cube("http://cube.example.com"), // a Cube source 8 | requests = cube.metric("sum(request)"); // count request events 9 | ``` 10 | 11 | # metric.add(operand) 12 | 13 | Derive a new metric by adding this metric to another metric or constant. If operand is a metric, returns a new metric that represents this metric plus operand. Otherwise, operand is implicitly converted to a [constant](Context#wiki-constant). 14 | 15 | # metric.subtract(operand) 16 | 17 | Derive a new metric by subtracting another metric or constant from this metric. If operand is a metric, returns a new metric that represents this metric minus operand. Otherwise, operand is implicitly converted to a [constant](Context#wiki-constant). 18 | 19 | # metric.multiply(operand) 20 | 21 | Derive a new metric by multiplying this metric by another metric or constant. If operand is a metric, returns a new metric that represents this metric times operand. Otherwise, operand is implicitly converted to a [constant](Context#wiki-constant). 22 | 23 | # metric.divide(operand) 24 | 25 | Derive a new metric by dividing this metric by another metric or constant. If operand is a metric, returns a new metric that represents this metric divided by operand. Otherwise, operand is implicitly converted to a [constant](Context#wiki-constant). 26 | 27 | For example, if the context step interval is five minutes, you can convert a count metric to a per-second rate by dividing by 300 (5 * 60): 28 | 29 | ```js 30 | var requestsPerSecond = cube.metric("sum(request)").divide(5 * 60); 31 | ``` 32 | 33 | Or, more generally: 34 | 35 | ```js 36 | var requestsPerSecond = cube.metric("sum(request)").divide(context.step() / 1000); 37 | ``` 38 | 39 | # metric.shift(offset) 40 | 41 | Derive a new metric by time-shifting this metric by the specified offset in milliseconds. Unless you have a time machine, the offset should be negative. 42 | 43 | For example, to compare the number of requests this week to the number of requests last week, you might say: 44 | 45 | ```js 46 | var requestsThisWeek = cube.metric("sum(request)"), 47 | requestsLastWeek = requestsThisWeek.shift(-7 * 24 * 60 * 60 * 1000), 48 | changeInRequests = requestsThisWeek.subtract(requestsLastWeek); 49 | ``` 50 | 51 | # metric.valueAt(index) 52 | 53 | Returns the value of the metric at the given index. This method is typically used only by Cubism's chart components to visualize the fetch values. The index is a nonnegative integer ranging from 0, indicating the context's start time (the first argument from the context's change event, inclusive), to [size](Context#wiki-size) - 1, indicate the context's stop time (the second argument, exclusive). 54 | 55 | # metric.extent() 56 | 57 | Returns the minimum and maximum metric value as a two-element array, [min, max]. This method is typically used only by Cubism's chart components to visualize the fetch values. If metric data is unavailable, returns [-Infinity, Infinity]. 58 | 59 | # metric.on(type[, listener]) 60 | 61 | Add, get or remove a listener for "change" events. This method is typically used only by other Cubism components, but can be used if you want to perform other actions concurrently when new metric values are available (such as custom processing). One type of event is currently supported: 62 | 63 | * change events are dispatched at the time new metric values are available. This event is used, for example, by charts to render the new values. Listeners are passed two arguments: the start time (a Date, inclusive) and the stop time (a Date, exclusive), based on the original [context change](Context#wiki-on) event. The `this` context of the listener is the metric. 64 | 65 | This method follows the same API as D3's [dispatch.on](/mbostock/d3/wiki/Internals#wiki-dispatch_on). If listener is specified and non-null, sets the callback function for events of the specified type and returns the metric; any existing listener for the same type will be replaced. If listener is specified and null, clears the callback function for events of the specified type (if any) and returns the metric. If listener is not specified, returns the current callback function, if any. The type can be further qualified with a namespace so that multiple listeners can receive the same events; for example, you might use "change.foo" and "change.bar" to register two listeners for change events. 66 | 67 | Note: metrics only fetch new data if at least one listener is receiving change events. Typically, this is the chart visualizing the metric. 68 | 69 | # metric.context 70 | 71 | The metric's parent [context](Context). 72 | 73 | # metric.toString() 74 | 75 | Returns the metric's associated expression, if any. For example, for Graphite and Cube metrics this is the first argument to the constructor. -------------------------------------------------------------------------------- /docs/Rule.md: -------------------------------------------------------------------------------- 1 | > [Wiki](Home) ▸ [API Reference](API-Reference) ▸ Rule 2 | 3 | A thin vertical line that updates on mousemove. This is typically used to indicate the context's [focus value](Context#wiki-focus). To create a rule, first create a [context](Context). For example: 4 | 5 | ```js 6 | var context = cubism.context(), // a default context 7 | rule = context.rule(); // a rule 8 | ``` 9 | 10 | # rule(selection) 11 | 12 | Apply the rule to a [D3 selection](/mbostock/d3/wiki/Selections) or [transition](/mbostock/d3/wiki/Transitions) containing one or more SVG element. For example: 13 | 14 | ```js 15 | d3.select("body").append("div") 16 | .attr("class", "rule") 17 | .call(rule); 18 | ``` 19 | 20 | # rule.metric([metric]) 21 | 22 | If *metric* is specified, sets this rule’s associated metric and returns the rule. If *metric* is not specified, returns the rule’s current metric which defaults to null. If a metric is associated with the rule, then a rule will be displayed at each non-zero value of the metric; otherwise, only a single rule will be displayed on mouseover at the mouse location. Binding a metric to a rule is a useful way to overlay events on a time-series, such as deploys. 23 | 24 | # rule.remove(selection) 25 | 26 | Removes the rule from a [D3 selection](/mbostock/d3/wiki/Selections), and removes any associated listeners. This method only removes the contents added by the rule itself; typically, you also want to call [remove](/mbostock/d3/wiki/Selections#wiki-remove) on the selection. For example: 27 | 28 | ```js 29 | d3.select(".rule") 30 | .call(rule.remove) 31 | .remove(); 32 | ``` 33 | 34 | Requires that the elements in the selection were previously bound to this rule. -------------------------------------------------------------------------------- /docs/Tutorials.md: -------------------------------------------------------------------------------- 1 | > [Wiki](Home) ▸ Tutorials 2 | 3 | Please feel free to add links to your work! 4 | 5 | ### Specific Techniques 6 | 7 | * [Move focus more than 1 unit with the keyboard](http://cnnr.roon.io/move-more-than-1-unit-with-cubism-js) - Connor Montgomery 8 | 9 | ## Talks and Videos 10 | 11 | * [Time Series Visualization with Cubism.js](https://www.youtube.com/watch?v=QHpfzvjD8pk) ([Slides](http://bost.ocks.org/mike/cubism/intro/))
SF Metrics Meetup, May 2012. 12 | 13 | ## Examples and Demos 14 | 15 | * [A horizon chart of stock prices](http://bost.ocks.org/mike/cubism/intro/demo-stocks.html) from the [Time Series Visualization with Cubism.js talk](#intro-talk) above. 16 | * [Horizon Charts of TwitchPlaysPokemon Inputs](http://tppstats.herokuapp.com/) demo. 17 | * [Discrete cubism.js](http://bl.ocks.org/patrickthompson/4d508eb3b8feac90762e/) (e.g. web events, etc.) - Patrick Thompson -------------------------------------------------------------------------------- /docs/comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/cubism/9fc7a5320cfa4316c37730a0ed8901a12a3238e8/docs/comparison.png -------------------------------------------------------------------------------- /docs/horizon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/cubism/9fc7a5320cfa4316c37730a0ed8901a12a3238e8/docs/horizon.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Sorry for polluting your global namespace! 2 | d3 = require("d3"); 3 | 4 | module.exports = require("./cubism.v1").cubism; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cubism", 3 | "version": "1.6.0", 4 | "description": "A JavaScript library for time series visualization.", 5 | "keywords": [ 6 | "time series", 7 | "visualization", 8 | "d3" 9 | ], 10 | "homepage": "http://square.github.com/cubism/", 11 | "author": { 12 | "name": "Mike Bostock", 13 | "url": "http://bost.ocks.org/mike" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "http://github.com/square/cubism.git" 18 | }, 19 | "main": "./index.js", 20 | "dependencies": { 21 | "d3": "3.x" 22 | }, 23 | "devDependencies": { 24 | "vows": "0.6.1", 25 | "uglify-js": "2.6.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/axis.js: -------------------------------------------------------------------------------- 1 | cubism_contextPrototype.axis = function() { 2 | var context = this, 3 | scale = context.scale, 4 | axis_ = d3.svg.axis().scale(scale); 5 | 6 | var formatDefault = context.step() < 6e4 ? cubism_axisFormatSeconds 7 | : context.step() < 864e5 ? cubism_axisFormatMinutes 8 | : cubism_axisFormatDays; 9 | var format = formatDefault; 10 | 11 | function axis(selection) { 12 | var id = ++cubism_id, 13 | tick; 14 | 15 | var g = selection.append("svg") 16 | .datum({id: id}) 17 | .attr("width", context.size()) 18 | .attr("height", Math.max(28, -axis.tickSize())) 19 | .append("g") 20 | .attr("transform", "translate(0," + (axis_.orient() === "top" ? 27 : 4) + ")") 21 | .call(axis_); 22 | 23 | context.on("change.axis-" + id, function() { 24 | g.call(axis_); 25 | if (!tick) tick = d3.select(g.node().appendChild(g.selectAll("text").node().cloneNode(true))) 26 | .style("display", "none") 27 | .text(null); 28 | }); 29 | 30 | context.on("focus.axis-" + id, function(i) { 31 | if (tick) { 32 | if (i == null) { 33 | tick.style("display", "none"); 34 | g.selectAll("text").style("fill-opacity", null); 35 | } else { 36 | tick.style("display", null).attr("x", i).text(format(scale.invert(i))); 37 | var dx = tick.node().getComputedTextLength() + 6; 38 | g.selectAll("text").style("fill-opacity", function(d) { return Math.abs(scale(d) - i) < dx ? 0 : 1; }); 39 | } 40 | } 41 | }); 42 | } 43 | 44 | axis.remove = function(selection) { 45 | 46 | selection.selectAll("svg") 47 | .each(remove) 48 | .remove(); 49 | 50 | function remove(d) { 51 | context.on("change.axis-" + d.id, null); 52 | context.on("focus.axis-" + d.id, null); 53 | } 54 | }; 55 | 56 | axis.focusFormat = function(_) { 57 | if (!arguments.length) return format == formatDefault ? null : _; 58 | format = _ == null ? formatDefault : _; 59 | return axis; 60 | }; 61 | 62 | return d3.rebind(axis, axis_, 63 | "orient", 64 | "ticks", 65 | "tickSubdivide", 66 | "tickSize", 67 | "tickPadding", 68 | "tickFormat"); 69 | }; 70 | 71 | var cubism_axisFormatSeconds = d3.time.format("%I:%M:%S %p"), 72 | cubism_axisFormatMinutes = d3.time.format("%I:%M %p"), 73 | cubism_axisFormatDays = d3.time.format("%B %d"); 74 | -------------------------------------------------------------------------------- /src/comparison.js: -------------------------------------------------------------------------------- 1 | cubism_contextPrototype.comparison = function() { 2 | var context = this, 3 | width = context.size(), 4 | height = 120, 5 | scale = d3.scale.linear().interpolate(d3.interpolateRound), 6 | primary = function(d) { return d[0]; }, 7 | secondary = function(d) { return d[1]; }, 8 | extent = null, 9 | title = cubism_identity, 10 | formatPrimary = cubism_comparisonPrimaryFormat, 11 | formatChange = cubism_comparisonChangeFormat, 12 | colors = ["#9ecae1", "#225b84", "#a1d99b", "#22723a"], 13 | strokeWidth = 1.5; 14 | 15 | function comparison(selection) { 16 | 17 | selection 18 | .on("mousemove.comparison", function() { context.focus(Math.round(d3.mouse(this)[0])); }) 19 | .on("mouseout.comparison", function() { context.focus(null); }); 20 | 21 | selection.append("canvas") 22 | .attr("width", width) 23 | .attr("height", height); 24 | 25 | selection.append("span") 26 | .attr("class", "title") 27 | .text(title); 28 | 29 | selection.append("span") 30 | .attr("class", "value primary"); 31 | 32 | selection.append("span") 33 | .attr("class", "value change"); 34 | 35 | selection.each(function(d, i) { 36 | var that = this, 37 | id = ++cubism_id, 38 | primary_ = typeof primary === "function" ? primary.call(that, d, i) : primary, 39 | secondary_ = typeof secondary === "function" ? secondary.call(that, d, i) : secondary, 40 | extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent, 41 | div = d3.select(that), 42 | canvas = div.select("canvas"), 43 | spanPrimary = div.select(".value.primary"), 44 | spanChange = div.select(".value.change"), 45 | ready; 46 | 47 | canvas.datum({id: id, primary: primary_, secondary: secondary_}); 48 | canvas = canvas.node().getContext("2d"); 49 | 50 | function change(start, stop) { 51 | canvas.save(); 52 | canvas.clearRect(0, 0, width, height); 53 | 54 | // update the scale 55 | var primaryExtent = primary_.extent(), 56 | secondaryExtent = secondary_.extent(), 57 | extent = extent_ == null ? primaryExtent : extent_; 58 | scale.domain(extent).range([height, 0]); 59 | ready = primaryExtent.concat(secondaryExtent).every(isFinite); 60 | 61 | // consistent overplotting 62 | var round = start / context.step() & 1 63 | ? cubism_comparisonRoundOdd 64 | : cubism_comparisonRoundEven; 65 | 66 | // positive changes 67 | canvas.fillStyle = colors[2]; 68 | for (var i = 0, n = width; i < n; ++i) { 69 | var y0 = scale(primary_.valueAt(i)), 70 | y1 = scale(secondary_.valueAt(i)); 71 | if (y0 < y1) canvas.fillRect(round(i), y0, 1, y1 - y0); 72 | } 73 | 74 | // negative changes 75 | canvas.fillStyle = colors[0]; 76 | for (i = 0; i < n; ++i) { 77 | var y0 = scale(primary_.valueAt(i)), 78 | y1 = scale(secondary_.valueAt(i)); 79 | if (y0 > y1) canvas.fillRect(round(i), y1, 1, y0 - y1); 80 | } 81 | 82 | // positive values 83 | canvas.fillStyle = colors[3]; 84 | for (i = 0; i < n; ++i) { 85 | var y0 = scale(primary_.valueAt(i)), 86 | y1 = scale(secondary_.valueAt(i)); 87 | if (y0 <= y1) canvas.fillRect(round(i), y0, 1, strokeWidth); 88 | } 89 | 90 | // negative values 91 | canvas.fillStyle = colors[1]; 92 | for (i = 0; i < n; ++i) { 93 | var y0 = scale(primary_.valueAt(i)), 94 | y1 = scale(secondary_.valueAt(i)); 95 | if (y0 > y1) canvas.fillRect(round(i), y0 - strokeWidth, 1, strokeWidth); 96 | } 97 | 98 | canvas.restore(); 99 | } 100 | 101 | function focus(i) { 102 | if (i == null) i = width - 1; 103 | var valuePrimary = primary_.valueAt(i), 104 | valueSecondary = secondary_.valueAt(i), 105 | valueChange = (valuePrimary - valueSecondary) / valueSecondary; 106 | 107 | spanPrimary 108 | .datum(valuePrimary) 109 | .text(isNaN(valuePrimary) ? null : formatPrimary); 110 | 111 | spanChange 112 | .datum(valueChange) 113 | .text(isNaN(valueChange) ? null : formatChange) 114 | .attr("class", "value change " + (valueChange > 0 ? "positive" : valueChange < 0 ? "negative" : "")); 115 | } 116 | 117 | // Display the first primary change immediately, 118 | // but defer subsequent updates to the context change. 119 | // Note that someone still needs to listen to the metric, 120 | // so that it continues to update automatically. 121 | primary_.on("change.comparison-" + id, firstChange); 122 | secondary_.on("change.comparison-" + id, firstChange); 123 | function firstChange(start, stop) { 124 | change(start, stop), focus(); 125 | if (ready) { 126 | primary_.on("change.comparison-" + id, cubism_identity); 127 | secondary_.on("change.comparison-" + id, cubism_identity); 128 | } 129 | } 130 | 131 | // Update the chart when the context changes. 132 | context.on("change.comparison-" + id, change); 133 | context.on("focus.comparison-" + id, focus); 134 | }); 135 | } 136 | 137 | comparison.remove = function(selection) { 138 | 139 | selection 140 | .on("mousemove.comparison", null) 141 | .on("mouseout.comparison", null); 142 | 143 | selection.selectAll("canvas") 144 | .each(remove) 145 | .remove(); 146 | 147 | selection.selectAll(".title,.value") 148 | .remove(); 149 | 150 | function remove(d) { 151 | d.primary.on("change.comparison-" + d.id, null); 152 | d.secondary.on("change.comparison-" + d.id, null); 153 | context.on("change.comparison-" + d.id, null); 154 | context.on("focus.comparison-" + d.id, null); 155 | } 156 | }; 157 | 158 | comparison.height = function(_) { 159 | if (!arguments.length) return height; 160 | height = +_; 161 | return comparison; 162 | }; 163 | 164 | comparison.primary = function(_) { 165 | if (!arguments.length) return primary; 166 | primary = _; 167 | return comparison; 168 | }; 169 | 170 | comparison.secondary = function(_) { 171 | if (!arguments.length) return secondary; 172 | secondary = _; 173 | return comparison; 174 | }; 175 | 176 | comparison.scale = function(_) { 177 | if (!arguments.length) return scale; 178 | scale = _; 179 | return comparison; 180 | }; 181 | 182 | comparison.extent = function(_) { 183 | if (!arguments.length) return extent; 184 | extent = _; 185 | return comparison; 186 | }; 187 | 188 | comparison.title = function(_) { 189 | if (!arguments.length) return title; 190 | title = _; 191 | return comparison; 192 | }; 193 | 194 | comparison.formatPrimary = function(_) { 195 | if (!arguments.length) return formatPrimary; 196 | formatPrimary = _; 197 | return comparison; 198 | }; 199 | 200 | comparison.formatChange = function(_) { 201 | if (!arguments.length) return formatChange; 202 | formatChange = _; 203 | return comparison; 204 | }; 205 | 206 | comparison.colors = function(_) { 207 | if (!arguments.length) return colors; 208 | colors = _; 209 | return comparison; 210 | }; 211 | 212 | comparison.strokeWidth = function(_) { 213 | if (!arguments.length) return strokeWidth; 214 | strokeWidth = _; 215 | return comparison; 216 | }; 217 | 218 | return comparison; 219 | }; 220 | 221 | var cubism_comparisonPrimaryFormat = d3.format(".2s"), 222 | cubism_comparisonChangeFormat = d3.format("+.0%"); 223 | 224 | function cubism_comparisonRoundEven(i) { 225 | return i & 0xfffffe; 226 | } 227 | 228 | function cubism_comparisonRoundOdd(i) { 229 | return ((i + 1) & 0xfffffe) - 1; 230 | } 231 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | cubism.context = function() { 2 | var context = new cubism_context, 3 | step = 1e4, // ten seconds, in milliseconds 4 | size = 1440, // four hours at ten seconds, in pixels 5 | start0, stop0, // the start and stop for the previous change event 6 | start1, stop1, // the start and stop for the next prepare event 7 | serverDelay = 5e3, 8 | clientDelay = 5e3, 9 | event = d3.dispatch("prepare", "beforechange", "change", "focus"), 10 | scale = context.scale = d3.time.scale().range([0, size]), 11 | timeout, 12 | focus; 13 | 14 | function update() { 15 | var now = Date.now(); 16 | stop0 = new Date(Math.floor((now - serverDelay - clientDelay) / step) * step); 17 | start0 = new Date(stop0 - size * step); 18 | stop1 = new Date(Math.floor((now - serverDelay) / step) * step); 19 | start1 = new Date(stop1 - size * step); 20 | scale.domain([start0, stop0]); 21 | return context; 22 | } 23 | 24 | context.start = function() { 25 | if (timeout) clearTimeout(timeout); 26 | var delay = +stop1 + serverDelay - Date.now(); 27 | 28 | // If we're too late for the first prepare event, skip it. 29 | if (delay < clientDelay) delay += step; 30 | 31 | timeout = setTimeout(function prepare() { 32 | stop1 = new Date(Math.floor((Date.now() - serverDelay) / step) * step); 33 | start1 = new Date(stop1 - size * step); 34 | event.prepare.call(context, start1, stop1); 35 | 36 | setTimeout(function() { 37 | scale.domain([start0 = start1, stop0 = stop1]); 38 | event.beforechange.call(context, start1, stop1); 39 | event.change.call(context, start1, stop1); 40 | event.focus.call(context, focus); 41 | }, clientDelay); 42 | 43 | timeout = setTimeout(prepare, step); 44 | }, delay); 45 | return context; 46 | }; 47 | 48 | context.stop = function() { 49 | timeout = clearTimeout(timeout); 50 | return context; 51 | }; 52 | 53 | timeout = setTimeout(context.start, 10); 54 | 55 | // Set or get the step interval in milliseconds. 56 | // Defaults to ten seconds. 57 | context.step = function(_) { 58 | if (!arguments.length) return step; 59 | step = +_; 60 | return update(); 61 | }; 62 | 63 | // Set or get the context size (the count of metric values). 64 | // Defaults to 1440 (four hours at ten seconds). 65 | context.size = function(_) { 66 | if (!arguments.length) return size; 67 | scale.range([0, size = +_]); 68 | return update(); 69 | }; 70 | 71 | // The server delay is the amount of time we wait for the server to compute a 72 | // metric. This delay may result from clock skew or from delays collecting 73 | // metrics from various hosts. Defaults to 4 seconds. 74 | context.serverDelay = function(_) { 75 | if (!arguments.length) return serverDelay; 76 | serverDelay = +_; 77 | return update(); 78 | }; 79 | 80 | // The client delay is the amount of additional time we wait to fetch those 81 | // metrics from the server. The client and server delay combined represent the 82 | // age of the most recent displayed metric. Defaults to 1 second. 83 | context.clientDelay = function(_) { 84 | if (!arguments.length) return clientDelay; 85 | clientDelay = +_; 86 | return update(); 87 | }; 88 | 89 | // Sets the focus to the specified index, and dispatches a "focus" event. 90 | context.focus = function(i) { 91 | event.focus.call(context, focus = i); 92 | return context; 93 | }; 94 | 95 | // Add, remove or get listeners for events. 96 | context.on = function(type, listener) { 97 | if (arguments.length < 2) return event.on(type); 98 | 99 | event.on(type, listener); 100 | 101 | // Notify the listener of the current start and stop time, as appropriate. 102 | // This way, metrics can make requests for data immediately, 103 | // and likewise the axis can display itself synchronously. 104 | if (listener != null) { 105 | if (/^prepare(\.|$)/.test(type)) listener.call(context, start1, stop1); 106 | if (/^beforechange(\.|$)/.test(type)) listener.call(context, start0, stop0); 107 | if (/^change(\.|$)/.test(type)) listener.call(context, start0, stop0); 108 | if (/^focus(\.|$)/.test(type)) listener.call(context, focus); 109 | } 110 | 111 | return context; 112 | }; 113 | 114 | d3.select(window).on("keydown.context-" + ++cubism_id, function() { 115 | switch (!d3.event.metaKey && d3.event.keyCode) { 116 | case 37: // left 117 | if (focus == null) focus = size - 1; 118 | if (focus > 0) context.focus(--focus); 119 | break; 120 | case 39: // right 121 | if (focus == null) focus = size - 2; 122 | if (focus < size - 1) context.focus(++focus); 123 | break; 124 | default: return; 125 | } 126 | d3.event.preventDefault(); 127 | }); 128 | 129 | return update(); 130 | }; 131 | 132 | function cubism_context() {} 133 | 134 | var cubism_contextPrototype = cubism.context.prototype = cubism_context.prototype; 135 | 136 | cubism_contextPrototype.constant = function(value) { 137 | return new cubism_metricConstant(this, +value); 138 | }; 139 | -------------------------------------------------------------------------------- /src/cube.js: -------------------------------------------------------------------------------- 1 | cubism_contextPrototype.cube = function(host) { 2 | if (!arguments.length) host = ""; 3 | var source = {}, 4 | context = this; 5 | 6 | source.metric = function(expression) { 7 | return context.metric(function(start, stop, step, callback) { 8 | d3.json(host + "/1.0/metric" 9 | + "?expression=" + encodeURIComponent(expression) 10 | + "&start=" + cubism_cubeFormatDate(start) 11 | + "&stop=" + cubism_cubeFormatDate(stop) 12 | + "&step=" + step, function(data) { 13 | if (!data) return callback(new Error("unable to load data")); 14 | callback(null, data.map(function(d) { return d.value; })); 15 | }); 16 | }, expression += ""); 17 | }; 18 | 19 | // Returns the Cube host. 20 | source.toString = function() { 21 | return host; 22 | }; 23 | 24 | return source; 25 | }; 26 | 27 | var cubism_cubeFormatDate = d3.time.format.iso; 28 | -------------------------------------------------------------------------------- /src/cubism.js: -------------------------------------------------------------------------------- 1 | var cubism = exports.cubism = {version: "1.6.0"}; 2 | -------------------------------------------------------------------------------- /src/gangliaWeb.js: -------------------------------------------------------------------------------- 1 | cubism_contextPrototype.gangliaWeb = function(config) { 2 | var host = '', 3 | uriPathPrefix = '/ganglia2/'; 4 | 5 | if (arguments.length) { 6 | if (config.host) { 7 | host = config.host; 8 | } 9 | 10 | if (config.uriPathPrefix) { 11 | uriPathPrefix = config.uriPathPrefix; 12 | 13 | /* Add leading and trailing slashes, as appropriate. */ 14 | if( uriPathPrefix[0] != '/' ) { 15 | uriPathPrefix = '/' + uriPathPrefix; 16 | } 17 | 18 | if( uriPathPrefix[uriPathPrefix.length - 1] != '/' ) { 19 | uriPathPrefix += '/'; 20 | } 21 | } 22 | } 23 | 24 | var source = {}, 25 | context = this; 26 | 27 | source.metric = function(metricInfo) { 28 | 29 | /* Store the members from metricInfo into local variables. */ 30 | var clusterName = metricInfo.clusterName, 31 | metricName = metricInfo.metricName, 32 | hostName = metricInfo.hostName, 33 | isReport = metricInfo.isReport || false, 34 | titleGenerator = metricInfo.titleGenerator || 35 | /* Reasonable (not necessarily pretty) default for titleGenerator. */ 36 | function(unusedMetricInfo) { 37 | /* unusedMetricInfo is, well, unused in this default case. */ 38 | return ('clusterName:' + clusterName + 39 | ' metricName:' + metricName + 40 | (hostName ? ' hostName:' + hostName : '')); 41 | }, 42 | onChangeCallback = metricInfo.onChangeCallback; 43 | 44 | /* Default to plain, simple metrics. */ 45 | var metricKeyName = isReport ? 'g' : 'm'; 46 | 47 | var gangliaWebMetric = context.metric(function(start, stop, step, callback) { 48 | 49 | function constructGangliaWebRequestQueryParams() { 50 | return ('c=' + clusterName + 51 | '&' + metricKeyName + '=' + metricName + 52 | (hostName ? '&h=' + hostName : '') + 53 | '&cs=' + start/1000 + '&ce=' + stop/1000 + '&step=' + step/1000 + '&graphlot=1'); 54 | } 55 | 56 | d3.json(host + uriPathPrefix + 'graph.php?' + constructGangliaWebRequestQueryParams(), 57 | function(result) { 58 | if( !result ) { 59 | return callback(new Error("Unable to fetch GangliaWeb data")); 60 | } 61 | 62 | callback(null, result[0].data); 63 | }); 64 | 65 | }, titleGenerator(metricInfo)); 66 | 67 | gangliaWebMetric.toString = function() { 68 | return titleGenerator(metricInfo); 69 | }; 70 | 71 | /* Allow users to run their custom code each time a gangliaWebMetric changes. 72 | * 73 | * TODO Consider abstracting away the naked Cubism call, and instead exposing 74 | * a callback that takes in the values array (maybe alongwith the original 75 | * start and stop 'naked' parameters), since it's handy to have the entire 76 | * dataset at your disposal (and users will likely implement onChangeCallback 77 | * primarily to get at this dataset). 78 | */ 79 | if (onChangeCallback) { 80 | gangliaWebMetric.on('change', onChangeCallback); 81 | } 82 | 83 | return gangliaWebMetric; 84 | }; 85 | 86 | // Returns the gangliaWeb host + uriPathPrefix. 87 | source.toString = function() { 88 | return host + uriPathPrefix; 89 | }; 90 | 91 | return source; 92 | }; 93 | 94 | -------------------------------------------------------------------------------- /src/graphite.js: -------------------------------------------------------------------------------- 1 | cubism_contextPrototype.graphite = function(host) { 2 | if (!arguments.length) host = ""; 3 | var source = {}, 4 | context = this; 5 | 6 | source.metric = function(expression) { 7 | var sum = "sum"; 8 | 9 | var metric = context.metric(function(start, stop, step, callback) { 10 | var target = expression; 11 | 12 | // Apply the summarize, if necessary. 13 | if (step !== 1e4) target = "summarize(" + target + ",'" 14 | + (!(step % 36e5) ? step / 36e5 + "hour" : !(step % 6e4) ? step / 6e4 + "min" : step / 1e3 + "sec") 15 | + "','" + sum + "')"; 16 | 17 | d3.text(host + "/render?format=raw" 18 | + "&target=" + encodeURIComponent("alias(" + target + ",'')") 19 | + "&from=" + cubism_graphiteFormatDate(start - 2 * step) // off-by-two? 20 | + "&until=" + cubism_graphiteFormatDate(stop - 1000), function(text) { 21 | if (!text) return callback(new Error("unable to load data")); 22 | callback(null, cubism_graphiteParse(text)); 23 | }); 24 | }, expression += ""); 25 | 26 | metric.summarize = function(_) { 27 | sum = _; 28 | return metric; 29 | }; 30 | 31 | return metric; 32 | }; 33 | 34 | source.find = function(pattern, callback) { 35 | d3.json(host + "/metrics/find?format=completer" 36 | + "&query=" + encodeURIComponent(pattern), function(result) { 37 | if (!result) return callback(new Error("unable to find metrics")); 38 | callback(null, result.metrics.map(function(d) { return d.path; })); 39 | }); 40 | }; 41 | 42 | // Returns the graphite host. 43 | source.toString = function() { 44 | return host; 45 | }; 46 | 47 | return source; 48 | }; 49 | 50 | // Graphite understands seconds since UNIX epoch. 51 | function cubism_graphiteFormatDate(time) { 52 | return Math.floor(time / 1000); 53 | } 54 | 55 | // Helper method for parsing graphite's raw format. 56 | function cubism_graphiteParse(text) { 57 | var i = text.indexOf("|"), 58 | meta = text.substring(0, i), 59 | c = meta.lastIndexOf(","), 60 | b = meta.lastIndexOf(",", c - 1), 61 | a = meta.lastIndexOf(",", b - 1), 62 | start = meta.substring(a + 1, b) * 1000, 63 | step = meta.substring(c + 1) * 1000; 64 | return text 65 | .substring(i + 1) 66 | .split(",") 67 | .slice(1) // the first value is always None? 68 | .map(function(d) { return +d; }); 69 | } 70 | -------------------------------------------------------------------------------- /src/horizon.js: -------------------------------------------------------------------------------- 1 | cubism_contextPrototype.horizon = function() { 2 | var context = this, 3 | mode = "offset", 4 | buffer = document.createElement("canvas"), 5 | width = buffer.width = context.size(), 6 | height = buffer.height = 30, 7 | scale = d3.scale.linear().interpolate(d3.interpolateRound), 8 | metric = cubism_identity, 9 | extent = null, 10 | title = cubism_identity, 11 | format = d3.format(".2s"), 12 | colors = ["#08519c","#3182bd","#6baed6","#bdd7e7","#bae4b3","#74c476","#31a354","#006d2c"]; 13 | 14 | function horizon(selection) { 15 | 16 | selection 17 | .on("mousemove.horizon", function() { context.focus(Math.round(d3.mouse(this)[0])); }) 18 | .on("mouseout.horizon", function() { context.focus(null); }); 19 | 20 | selection.append("canvas") 21 | .attr("width", width) 22 | .attr("height", height); 23 | 24 | selection.append("span") 25 | .attr("class", "title") 26 | .text(title); 27 | 28 | selection.append("span") 29 | .attr("class", "value"); 30 | 31 | selection.each(function(d, i) { 32 | var that = this, 33 | id = ++cubism_id, 34 | metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric, 35 | colors_ = typeof colors === "function" ? colors.call(that, d, i) : colors, 36 | extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent, 37 | start = -Infinity, 38 | step = context.step(), 39 | canvas = d3.select(that).select("canvas"), 40 | span = d3.select(that).select(".value"), 41 | max_, 42 | m = colors_.length >> 1, 43 | ready; 44 | 45 | canvas.datum({id: id, metric: metric_}); 46 | canvas = canvas.node().getContext("2d"); 47 | 48 | function change(start1, stop) { 49 | canvas.save(); 50 | 51 | // compute the new extent and ready flag 52 | var extent = metric_.extent(); 53 | ready = extent.every(isFinite); 54 | if (extent_ != null) extent = extent_; 55 | 56 | // if this is an update (with no extent change), copy old values! 57 | var i0 = 0, max = Math.max(-extent[0], extent[1]); 58 | if (this === context) { 59 | if (max == max_) { 60 | i0 = width - cubism_metricOverlap; 61 | var dx = (start1 - start) / step; 62 | if (dx < width) { 63 | var canvas0 = buffer.getContext("2d"); 64 | canvas0.clearRect(0, 0, width, height); 65 | canvas0.drawImage(canvas.canvas, dx, 0, width - dx, height, 0, 0, width - dx, height); 66 | canvas.clearRect(0, 0, width, height); 67 | canvas.drawImage(canvas0.canvas, 0, 0); 68 | } 69 | } 70 | start = start1; 71 | } 72 | 73 | // update the domain 74 | scale.domain([0, max_ = max]); 75 | 76 | // clear for the new data 77 | canvas.clearRect(i0, 0, width - i0, height); 78 | 79 | // record whether there are negative values to display 80 | var negative; 81 | 82 | // positive bands 83 | for (var j = 0; j < m; ++j) { 84 | canvas.fillStyle = colors_[m + j]; 85 | 86 | // Adjust the range based on the current band index. 87 | var y0 = (j - m + 1) * height; 88 | scale.range([m * height + y0, y0]); 89 | y0 = scale(0); 90 | 91 | for (var i = i0, n = width, y1; i < n; ++i) { 92 | y1 = metric_.valueAt(i); 93 | if (y1 <= 0) { negative = true; continue; } 94 | if (y1 === undefined) continue; 95 | canvas.fillRect(i, y1 = scale(y1), 1, y0 - y1); 96 | } 97 | } 98 | 99 | if (negative) { 100 | // enable offset mode 101 | if (mode === "offset") { 102 | canvas.translate(0, height); 103 | canvas.scale(1, -1); 104 | } 105 | 106 | // negative bands 107 | for (var j = 0; j < m; ++j) { 108 | canvas.fillStyle = colors_[m - 1 - j]; 109 | 110 | // Adjust the range based on the current band index. 111 | var y0 = (j - m + 1) * height; 112 | scale.range([m * height + y0, y0]); 113 | y0 = scale(0); 114 | 115 | for (var i = i0, n = width, y1; i < n; ++i) { 116 | y1 = metric_.valueAt(i); 117 | if (y1 >= 0) continue; 118 | canvas.fillRect(i, scale(-y1), 1, y0 - scale(-y1)); 119 | } 120 | } 121 | } 122 | 123 | canvas.restore(); 124 | } 125 | 126 | function focus(i) { 127 | if (i == null) i = width - 1; 128 | var value = metric_.valueAt(i); 129 | span.datum(value).text(isNaN(value) ? null : format); 130 | } 131 | 132 | // Update the chart when the context changes. 133 | context.on("change.horizon-" + id, change); 134 | context.on("focus.horizon-" + id, focus); 135 | 136 | // Display the first metric change immediately, 137 | // but defer subsequent updates to the canvas change. 138 | // Note that someone still needs to listen to the metric, 139 | // so that it continues to update automatically. 140 | metric_.on("change.horizon-" + id, function(start, stop) { 141 | change(start, stop), focus(); 142 | if (ready) metric_.on("change.horizon-" + id, cubism_identity); 143 | }); 144 | }); 145 | } 146 | 147 | horizon.remove = function(selection) { 148 | 149 | selection 150 | .on("mousemove.horizon", null) 151 | .on("mouseout.horizon", null); 152 | 153 | selection.selectAll("canvas") 154 | .each(remove) 155 | .remove(); 156 | 157 | selection.selectAll(".title,.value") 158 | .remove(); 159 | 160 | function remove(d) { 161 | d.metric.on("change.horizon-" + d.id, null); 162 | context.on("change.horizon-" + d.id, null); 163 | context.on("focus.horizon-" + d.id, null); 164 | } 165 | }; 166 | 167 | horizon.mode = function(_) { 168 | if (!arguments.length) return mode; 169 | mode = _ + ""; 170 | return horizon; 171 | }; 172 | 173 | horizon.height = function(_) { 174 | if (!arguments.length) return height; 175 | buffer.height = height = +_; 176 | return horizon; 177 | }; 178 | 179 | horizon.metric = function(_) { 180 | if (!arguments.length) return metric; 181 | metric = _; 182 | return horizon; 183 | }; 184 | 185 | horizon.scale = function(_) { 186 | if (!arguments.length) return scale; 187 | scale = _; 188 | return horizon; 189 | }; 190 | 191 | horizon.extent = function(_) { 192 | if (!arguments.length) return extent; 193 | extent = _; 194 | return horizon; 195 | }; 196 | 197 | horizon.title = function(_) { 198 | if (!arguments.length) return title; 199 | title = _; 200 | return horizon; 201 | }; 202 | 203 | horizon.format = function(_) { 204 | if (!arguments.length) return format; 205 | format = _; 206 | return horizon; 207 | }; 208 | 209 | horizon.colors = function(_) { 210 | if (!arguments.length) return colors; 211 | colors = _; 212 | return horizon; 213 | }; 214 | 215 | return horizon; 216 | }; 217 | -------------------------------------------------------------------------------- /src/id.js: -------------------------------------------------------------------------------- 1 | var cubism_id = 0; 2 | -------------------------------------------------------------------------------- /src/identity.js: -------------------------------------------------------------------------------- 1 | function cubism_identity(d) { return d; } 2 | -------------------------------------------------------------------------------- /src/librato.js: -------------------------------------------------------------------------------- 1 | /* librato (http://dev.librato.com/v1/post/metrics) source 2 | * If you want to see an example of how to use this source, check: https://gist.github.com/drio/5792680 3 | */ 4 | cubism_contextPrototype.librato = function(user, token) { 5 | var source = {}, 6 | context = this; 7 | auth_string = "Basic " + btoa(user + ":" + token); 8 | avail_rsts = [ 1, 60, 900, 3600 ]; 9 | 10 | /* Given a step, find the best librato resolution to use. 11 | * 12 | * Example: 13 | * 14 | * (s) : cubism step 15 | * 16 | * avail_rsts 1 --------------- 60 --------------- 900 ---------------- 3600 17 | * | (s) | 18 | * | | 19 | * [low_res top_res] 20 | * 21 | * return: low_res (60) 22 | */ 23 | function find_ideal_librato_resolution(step) { 24 | var highest_res = avail_rsts[0], 25 | lowest_res = avail_rsts[avail_rsts.length]; // high and lowest available resolution from librato 26 | 27 | /* If step is outside the highest or lowest librato resolution, pick them and we are done */ 28 | if (step >= lowest_res) 29 | return lowest_res; 30 | 31 | if (step <= highest_res) 32 | return highest_res; 33 | 34 | /* If not, find in what resolution interval the step lands. */ 35 | var iof, top_res, i; 36 | for (i=step; i<=lowest_res; i++) { 37 | iof = avail_rsts.indexOf(i); 38 | if (iof > -1) { 39 | top_res = avail_rsts[iof]; 40 | break; 41 | } 42 | } 43 | 44 | var low_res; 45 | for (i=step; i>=highest_res; i--) { 46 | iof = avail_rsts.indexOf(i); 47 | if (iof > -1) { 48 | low_res = avail_rsts[iof]; 49 | break; 50 | } 51 | } 52 | 53 | /* What's the closest librato resolution given the step ? */ 54 | return ((top_res-step) < (step-low_res)) ? top_res : low_res; 55 | } 56 | 57 | function find_librato_resolution(sdate, edate, step) { 58 | var i_size = edate - sdate, // interval size 59 | month = 2419200, 60 | week = 604800, 61 | two_days = 172800, 62 | ideal_res; 63 | 64 | if (i_size > month) 65 | return 3600; 66 | 67 | ideal_res = find_ideal_librato_resolution(step); 68 | 69 | /* 70 | * Now we have the ideal resolution, but due to the retention policies at librato, maybe we have 71 | * to use a higher resolution. 72 | * http://support.metrics.librato.com/knowledgebase/articles/66838-understanding-metrics-roll-ups-retention-and-grap 73 | */ 74 | if (i_size > week && ideal_res < 900) 75 | return 900; 76 | else if (i_size > two_days && ideal_res < 60) 77 | return 60; 78 | else 79 | return ideal_res; 80 | } 81 | 82 | /* All the logic to query the librato API is here */ 83 | var librato_request = function(composite) { 84 | var url_prefix = "https://metrics-api.librato.com/v1/metrics"; 85 | 86 | function make_url(sdate, edate, step) { 87 | var params = "compose=" + composite + 88 | "&start_time=" + sdate + 89 | "&end_time=" + edate + 90 | "&resolution=" + find_librato_resolution(sdate, edate, step); 91 | return url_prefix + "?" + params; 92 | } 93 | 94 | /* 95 | * We are most likely not going to get the same number of measurements 96 | * cubism expects for a particular context: We have to perform down/up 97 | * sampling 98 | */ 99 | function down_up_sampling(isdate, iedate, step, librato_mm) { 100 | var av = []; 101 | 102 | for (i=isdate; i<=iedate; i+=step) { 103 | var int_mes = []; 104 | while (librato_mm.length && librato_mm[0].measure_time <= i) { 105 | int_mes.push(librato_mm.shift().value); 106 | } 107 | 108 | var v; 109 | if (int_mes.length) { /* Compute the average */ 110 | v = int_mes.reduce(function(a, b) { return a + b }) / int_mes.length; 111 | } else { /* No librato values on interval */ 112 | v = (av.length) ? av[av.length-1] : 0; 113 | } 114 | av.push(v); 115 | } 116 | 117 | return av; 118 | } 119 | 120 | request = {}; 121 | 122 | request.fire = function(isdate, iedate, step, callback_done) { 123 | var a_values = []; /* Store partial values from librato */ 124 | 125 | /* 126 | * Librato has a limit in the number of measurements we get back in a request (100). 127 | * We recursively perform requests to the API to ensure we have all the data points 128 | * for the interval we are working on. 129 | */ 130 | function actual_request(full_url) { 131 | d3.json(full_url) 132 | .header("X-Requested-With", "XMLHttpRequest") 133 | .header("Authorization", auth_string) 134 | .header("Librato-User-Agent", 'cubism/' + cubism.version) 135 | .get(function (error, data) { /* Callback; data available */ 136 | if (!error) { 137 | if (data.measurements.length === 0) { 138 | return 139 | } 140 | data.measurements[0].series.forEach(function(o) { a_values.push(o); }); 141 | 142 | var still_more_values = 'query' in data && 'next_time' in data.query; 143 | if (still_more_values) { 144 | actual_request(make_url(data.query.next_time, iedate, step)); 145 | } else { 146 | var a_adjusted = down_up_sampling(isdate, iedate, step, a_values); 147 | callback_done(a_adjusted); 148 | } 149 | } 150 | }); 151 | } 152 | 153 | actual_request(make_url(isdate, iedate, step)); 154 | }; 155 | 156 | return request; 157 | }; 158 | 159 | /* 160 | * The user will use this method to create a cubism source (librato in this case) 161 | * and call .metric() as necessary to create metrics. 162 | */ 163 | source.metric = function(m_composite) { 164 | return context.metric(function(start, stop, step, callback) { 165 | /* All the librato logic is here; .fire() retrieves the metrics' data */ 166 | librato_request(m_composite) 167 | .fire(cubism_libratoFormatDate(start), 168 | cubism_libratoFormatDate(stop), 169 | cubism_libratoFormatDate(step), 170 | function(a_values) { callback(null, a_values); }); 171 | 172 | }, m_composite += ""); 173 | }; 174 | 175 | /* This is not used when the source is librato */ 176 | source.toString = function() { 177 | return "librato"; 178 | }; 179 | 180 | return source; 181 | }; 182 | 183 | var cubism_libratoFormatDate = function(time) { 184 | return Math.floor(time / 1000); 185 | }; 186 | -------------------------------------------------------------------------------- /src/metric-constant.js: -------------------------------------------------------------------------------- 1 | function cubism_metricConstant(context, value) { 2 | cubism_metric.call(this, context); 3 | value = +value; 4 | var name = value + ""; 5 | this.valueOf = function() { return value; }; 6 | this.toString = function() { return name; }; 7 | } 8 | 9 | var cubism_metricConstantPrototype = cubism_metricConstant.prototype = Object.create(cubism_metric.prototype); 10 | 11 | cubism_metricConstantPrototype.valueAt = function() { 12 | return +this; 13 | }; 14 | 15 | cubism_metricConstantPrototype.extent = function() { 16 | return [+this, +this]; 17 | }; 18 | -------------------------------------------------------------------------------- /src/metric-operator.js: -------------------------------------------------------------------------------- 1 | function cubism_metricOperator(name, operate) { 2 | 3 | function cubism_metricOperator(left, right) { 4 | if (!(right instanceof cubism_metric)) right = new cubism_metricConstant(left.context, right); 5 | else if (left.context !== right.context) throw new Error("mismatch context"); 6 | cubism_metric.call(this, left.context); 7 | this.left = left; 8 | this.right = right; 9 | this.toString = function() { return left + " " + name + " " + right; }; 10 | } 11 | 12 | var cubism_metricOperatorPrototype = cubism_metricOperator.prototype = Object.create(cubism_metric.prototype); 13 | 14 | cubism_metricOperatorPrototype.valueAt = function(i) { 15 | return operate(this.left.valueAt(i), this.right.valueAt(i)); 16 | }; 17 | 18 | cubism_metricOperatorPrototype.shift = function(offset) { 19 | return new cubism_metricOperator(this.left.shift(offset), this.right.shift(offset)); 20 | }; 21 | 22 | cubism_metricOperatorPrototype.on = function(type, listener) { 23 | if (arguments.length < 2) return this.left.on(type); 24 | this.left.on(type, listener); 25 | this.right.on(type, listener); 26 | return this; 27 | }; 28 | 29 | return function(right) { 30 | return new cubism_metricOperator(this, right); 31 | }; 32 | } 33 | 34 | cubism_metricPrototype.add = cubism_metricOperator("+", function(left, right) { 35 | return left + right; 36 | }); 37 | 38 | cubism_metricPrototype.subtract = cubism_metricOperator("-", function(left, right) { 39 | return left - right; 40 | }); 41 | 42 | cubism_metricPrototype.multiply = cubism_metricOperator("*", function(left, right) { 43 | return left * right; 44 | }); 45 | 46 | cubism_metricPrototype.divide = cubism_metricOperator("/", function(left, right) { 47 | return left / right; 48 | }); 49 | -------------------------------------------------------------------------------- /src/metric.js: -------------------------------------------------------------------------------- 1 | function cubism_metric(context) { 2 | if (!(context instanceof cubism_context)) throw new Error("invalid context"); 3 | this.context = context; 4 | } 5 | 6 | var cubism_metricPrototype = cubism_metric.prototype; 7 | 8 | cubism.metric = cubism_metric; 9 | 10 | cubism_metricPrototype.valueAt = function() { 11 | return NaN; 12 | }; 13 | 14 | cubism_metricPrototype.alias = function(name) { 15 | this.toString = function() { return name; }; 16 | return this; 17 | }; 18 | 19 | cubism_metricPrototype.extent = function() { 20 | var i = 0, 21 | n = this.context.size(), 22 | value, 23 | min = Infinity, 24 | max = -Infinity; 25 | while (++i < n) { 26 | value = this.valueAt(i); 27 | if (value < min) min = value; 28 | if (value > max) max = value; 29 | } 30 | return [min, max]; 31 | }; 32 | 33 | cubism_metricPrototype.on = function(type, listener) { 34 | return arguments.length < 2 ? null : this; 35 | }; 36 | 37 | cubism_metricPrototype.shift = function() { 38 | return this; 39 | }; 40 | 41 | cubism_metricPrototype.on = function() { 42 | return arguments.length < 2 ? null : this; 43 | }; 44 | 45 | cubism_contextPrototype.metric = function(request, name) { 46 | var context = this, 47 | metric = new cubism_metric(context), 48 | id = ".metric-" + ++cubism_id, 49 | start = -Infinity, 50 | stop, 51 | step = context.step(), 52 | size = context.size(), 53 | values = [], 54 | event = d3.dispatch("change"), 55 | listening = 0, 56 | fetching; 57 | 58 | // Prefetch new data into a temporary array. 59 | function prepare(start1, stop) { 60 | var steps = Math.min(size, Math.round((start1 - start) / step)); 61 | if (!steps || fetching) return; // already fetched, or fetching! 62 | fetching = true; 63 | steps = Math.min(size, steps + cubism_metricOverlap); 64 | var start0 = new Date(stop - steps * step); 65 | request(start0, stop, step, function(error, data) { 66 | fetching = false; 67 | if (error) return console.warn(error); 68 | var i = isFinite(start) ? Math.round((start0 - start) / step) : 0; 69 | for (var j = 0, m = data.length; j < m; ++j) values[j + i] = data[j]; 70 | event.change.call(metric, start, stop); 71 | }); 72 | } 73 | 74 | // When the context changes, switch to the new data, ready-or-not! 75 | function beforechange(start1, stop1) { 76 | if (!isFinite(start)) start = start1; 77 | values.splice(0, Math.max(0, Math.min(size, Math.round((start1 - start) / step)))); 78 | start = start1; 79 | stop = stop1; 80 | } 81 | 82 | // 83 | metric.valueAt = function(i) { 84 | return values[i]; 85 | }; 86 | 87 | // 88 | metric.shift = function(offset) { 89 | return context.metric(cubism_metricShift(request, +offset)); 90 | }; 91 | 92 | // 93 | metric.on = function(type, listener) { 94 | if (!arguments.length) return event.on(type); 95 | 96 | // If there are no listeners, then stop listening to the context, 97 | // and avoid unnecessary fetches. 98 | if (listener == null) { 99 | if (event.on(type) != null && --listening == 0) { 100 | context.on("prepare" + id, null).on("beforechange" + id, null); 101 | } 102 | } else { 103 | if (event.on(type) == null && ++listening == 1) { 104 | context.on("prepare" + id, prepare).on("beforechange" + id, beforechange); 105 | } 106 | } 107 | 108 | event.on(type, listener); 109 | 110 | // Notify the listener of the current start and stop time, as appropriate. 111 | // This way, charts can display synchronous metrics immediately. 112 | if (listener != null) { 113 | if (/^change(\.|$)/.test(type)) listener.call(context, start, stop); 114 | } 115 | 116 | return metric; 117 | }; 118 | 119 | // 120 | if (arguments.length > 1) metric.toString = function() { 121 | return name; 122 | }; 123 | 124 | return metric; 125 | }; 126 | 127 | // Number of metric to refetch each period, in case of lag. 128 | var cubism_metricOverlap = 6; 129 | 130 | // Wraps the specified request implementation, and shifts time by the given offset. 131 | function cubism_metricShift(request, offset) { 132 | return function(start, stop, step, callback) { 133 | request(new Date(+start + offset), new Date(+stop + offset), step, callback); 134 | }; 135 | } 136 | -------------------------------------------------------------------------------- /src/option.js: -------------------------------------------------------------------------------- 1 | cubism.option = function(name, defaultValue) { 2 | var values = cubism.options(name); 3 | return values.length ? values[0] : defaultValue; 4 | }; 5 | 6 | cubism.options = function(name, defaultValues) { 7 | var options = location.search.substring(1).split("&"), 8 | values = [], 9 | i = -1, 10 | n = options.length, 11 | o; 12 | while (++i < n) { 13 | if ((o = options[i].split("="))[0] == name) { 14 | values.push(decodeURIComponent(o[1])); 15 | } 16 | } 17 | return values.length || arguments.length < 2 ? values : defaultValues; 18 | }; 19 | -------------------------------------------------------------------------------- /src/package.js: -------------------------------------------------------------------------------- 1 | var util = require("util"); 2 | 3 | navigator = {} 4 | 5 | var d3 = require("d3"), 6 | cubism = require("../cubism.v1").cubism; 7 | 8 | util.puts(JSON.stringify({ 9 | "name": "cubism", 10 | "version": cubism.version, 11 | "description": "A JavaScript library for time series visualization.", 12 | "keywords": ["time series", "visualization", "d3"], 13 | "homepage": "http://square.github.com/cubism/", 14 | "author": {"name": "Mike Bostock", "url": "http://bost.ocks.org/mike"}, 15 | "repository": {"type": "git", "url": "http://github.com/square/cubism.git"}, 16 | "main": "./index.js", 17 | "dependencies": { 18 | "d3": "3.x" 19 | }, 20 | "devDependencies": { 21 | "vows": "0.6.1", 22 | "uglify-js": "1.2.5" 23 | } 24 | } 25 | , null, 2)); 26 | -------------------------------------------------------------------------------- /src/rule.js: -------------------------------------------------------------------------------- 1 | cubism_contextPrototype.rule = function() { 2 | var context = this, 3 | metric = cubism_identity; 4 | 5 | function rule(selection) { 6 | var id = ++cubism_id; 7 | 8 | var line = selection.append("div") 9 | .datum({id: id}) 10 | .attr("class", "line") 11 | .call(cubism_ruleStyle); 12 | 13 | selection.each(function(d, i) { 14 | var that = this, 15 | id = ++cubism_id, 16 | metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric; 17 | 18 | if (!metric_) return; 19 | 20 | function change(start, stop) { 21 | var values = []; 22 | 23 | for (var i = 0, n = context.size(); i < n; ++i) { 24 | if (metric_.valueAt(i)) { 25 | values.push(i); 26 | } 27 | } 28 | 29 | var lines = selection.selectAll(".metric").data(values); 30 | lines.exit().remove(); 31 | lines.enter().append("div").attr("class", "metric line").call(cubism_ruleStyle); 32 | lines.style("left", cubism_ruleLeft); 33 | } 34 | 35 | context.on("change.rule-" + id, change); 36 | metric_.on("change.rule-" + id, change); 37 | }); 38 | 39 | context.on("focus.rule-" + id, function(i) { 40 | line.datum(i) 41 | .style("display", i == null ? "none" : null) 42 | .style("left", i == null ? null : cubism_ruleLeft); 43 | }); 44 | } 45 | 46 | rule.remove = function(selection) { 47 | 48 | selection.selectAll(".line") 49 | .each(remove) 50 | .remove(); 51 | 52 | function remove(d) { 53 | context.on("focus.rule-" + d.id, null); 54 | } 55 | }; 56 | 57 | rule.metric = function(_) { 58 | if (!arguments.length) return metric; 59 | metric = _; 60 | return rule; 61 | }; 62 | 63 | return rule; 64 | }; 65 | 66 | function cubism_ruleStyle(line) { 67 | line 68 | .style("position", "absolute") 69 | .style("top", 0) 70 | .style("bottom", 0) 71 | .style("width", "1px") 72 | .style("pointer-events", "none"); 73 | } 74 | 75 | function cubism_ruleLeft(i) { 76 | return i + "px"; 77 | } 78 | --------------------------------------------------------------------------------