PageRenderTime 36ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/ajax/libs/cubism/1.6.0/cubism.v1.js

https://gitlab.com/Mirros/cdnjs
JavaScript | 1331 lines | 993 code | 224 blank | 114 comment | 154 complexity | 58f165a3470748c92243f81809517904 MD5 | raw file
  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. cubism.options = function(name, defaultValues) {
  10. var options = location.search.substring(1).split("&"),
  11. values = [],
  12. i = -1,
  13. n = options.length,
  14. o;
  15. while (++i < n) {
  16. if ((o = options[i].split("="))[0] == name) {
  17. values.push(decodeURIComponent(o[1]));
  18. }
  19. }
  20. return values.length || arguments.length < 2 ? values : defaultValues;
  21. };
  22. cubism.context = function() {
  23. var context = new cubism_context,
  24. step = 1e4, // ten seconds, in milliseconds
  25. size = 1440, // four hours at ten seconds, in pixels
  26. start0, stop0, // the start and stop for the previous change event
  27. start1, stop1, // the start and stop for the next prepare event
  28. serverDelay = 5e3,
  29. clientDelay = 5e3,
  30. event = d3.dispatch("prepare", "beforechange", "change", "focus"),
  31. scale = context.scale = d3.time.scale().range([0, size]),
  32. timeout,
  33. focus;
  34. function update() {
  35. var now = Date.now();
  36. stop0 = new Date(Math.floor((now - serverDelay - clientDelay) / step) * step);
  37. start0 = new Date(stop0 - size * step);
  38. stop1 = new Date(Math.floor((now - serverDelay) / step) * step);
  39. start1 = new Date(stop1 - size * step);
  40. scale.domain([start0, stop0]);
  41. return context;
  42. }
  43. context.start = function() {
  44. if (timeout) clearTimeout(timeout);
  45. var delay = +stop1 + serverDelay - Date.now();
  46. // If we're too late for the first prepare event, skip it.
  47. if (delay < clientDelay) delay += step;
  48. timeout = setTimeout(function prepare() {
  49. stop1 = new Date(Math.floor((Date.now() - serverDelay) / step) * step);
  50. start1 = new Date(stop1 - size * step);
  51. event.prepare.call(context, start1, stop1);
  52. setTimeout(function() {
  53. scale.domain([start0 = start1, stop0 = stop1]);
  54. event.beforechange.call(context, start1, stop1);
  55. event.change.call(context, start1, stop1);
  56. event.focus.call(context, focus);
  57. }, clientDelay);
  58. timeout = setTimeout(prepare, step);
  59. }, delay);
  60. return context;
  61. };
  62. context.stop = function() {
  63. timeout = clearTimeout(timeout);
  64. return context;
  65. };
  66. timeout = setTimeout(context.start, 10);
  67. // Set or get the step interval in milliseconds.
  68. // Defaults to ten seconds.
  69. context.step = function(_) {
  70. if (!arguments.length) return step;
  71. step = +_;
  72. return update();
  73. };
  74. // Set or get the context size (the count of metric values).
  75. // Defaults to 1440 (four hours at ten seconds).
  76. context.size = function(_) {
  77. if (!arguments.length) return size;
  78. scale.range([0, size = +_]);
  79. return update();
  80. };
  81. // The server delay is the amount of time we wait for the server to compute a
  82. // metric. This delay may result from clock skew or from delays collecting
  83. // metrics from various hosts. Defaults to 4 seconds.
  84. context.serverDelay = function(_) {
  85. if (!arguments.length) return serverDelay;
  86. serverDelay = +_;
  87. return update();
  88. };
  89. // The client delay is the amount of additional time we wait to fetch those
  90. // metrics from the server. The client and server delay combined represent the
  91. // age of the most recent displayed metric. Defaults to 1 second.
  92. context.clientDelay = function(_) {
  93. if (!arguments.length) return clientDelay;
  94. clientDelay = +_;
  95. return update();
  96. };
  97. // Sets the focus to the specified index, and dispatches a "focus" event.
  98. context.focus = function(i) {
  99. event.focus.call(context, focus = i);
  100. return context;
  101. };
  102. // Add, remove or get listeners for events.
  103. context.on = function(type, listener) {
  104. if (arguments.length < 2) return event.on(type);
  105. event.on(type, listener);
  106. // Notify the listener of the current start and stop time, as appropriate.
  107. // This way, metrics can make requests for data immediately,
  108. // and likewise the axis can display itself synchronously.
  109. if (listener != null) {
  110. if (/^prepare(\.|$)/.test(type)) listener.call(context, start1, stop1);
  111. if (/^beforechange(\.|$)/.test(type)) listener.call(context, start0, stop0);
  112. if (/^change(\.|$)/.test(type)) listener.call(context, start0, stop0);
  113. if (/^focus(\.|$)/.test(type)) listener.call(context, focus);
  114. }
  115. return context;
  116. };
  117. d3.select(window).on("keydown.context-" + ++cubism_id, function() {
  118. switch (!d3.event.metaKey && d3.event.keyCode) {
  119. case 37: // left
  120. if (focus == null) focus = size - 1;
  121. if (focus > 0) context.focus(--focus);
  122. break;
  123. case 39: // right
  124. if (focus == null) focus = size - 2;
  125. if (focus < size - 1) context.focus(++focus);
  126. break;
  127. default: return;
  128. }
  129. d3.event.preventDefault();
  130. });
  131. return update();
  132. };
  133. function cubism_context() {}
  134. var cubism_contextPrototype = cubism.context.prototype = cubism_context.prototype;
  135. cubism_contextPrototype.constant = function(value) {
  136. return new cubism_metricConstant(this, +value);
  137. };
  138. cubism_contextPrototype.cube = function(host) {
  139. if (!arguments.length) host = "";
  140. var source = {},
  141. context = this;
  142. source.metric = function(expression) {
  143. return context.metric(function(start, stop, step, callback) {
  144. d3.json(host + "/1.0/metric"
  145. + "?expression=" + encodeURIComponent(expression)
  146. + "&start=" + cubism_cubeFormatDate(start)
  147. + "&stop=" + cubism_cubeFormatDate(stop)
  148. + "&step=" + step, function(data) {
  149. if (!data) return callback(new Error("unable to load data"));
  150. callback(null, data.map(function(d) { return d.value; }));
  151. });
  152. }, expression += "");
  153. };
  154. // Returns the Cube host.
  155. source.toString = function() {
  156. return host;
  157. };
  158. return source;
  159. };
  160. var cubism_cubeFormatDate = d3.time.format.iso;
  161. /* librato (http://dev.librato.com/v1/post/metrics) source
  162. * If you want to see an example of how to use this source, check: https://gist.github.com/drio/5792680
  163. */
  164. cubism_contextPrototype.librato = function(user, token) {
  165. var source = {},
  166. context = this;
  167. auth_string = "Basic " + btoa(user + ":" + token);
  168. avail_rsts = [ 1, 60, 900, 3600 ];
  169. /* Given a step, find the best librato resolution to use.
  170. *
  171. * Example:
  172. *
  173. * (s) : cubism step
  174. *
  175. * avail_rsts 1 --------------- 60 --------------- 900 ---------------- 3600
  176. * | (s) |
  177. * | |
  178. * [low_res top_res]
  179. *
  180. * return: low_res (60)
  181. */
  182. function find_ideal_librato_resolution(step) {
  183. var highest_res = avail_rsts[0],
  184. lowest_res = avail_rsts[avail_rsts.length]; // high and lowest available resolution from librato
  185. /* If step is outside the highest or lowest librato resolution, pick them and we are done */
  186. if (step >= lowest_res)
  187. return lowest_res;
  188. if (step <= highest_res)
  189. return highest_res;
  190. /* If not, find in what resolution interval the step lands. */
  191. var iof, top_res, i;
  192. for (i=step; i<=lowest_res; i++) {
  193. iof = avail_rsts.indexOf(i);
  194. if (iof > -1) {
  195. top_res = avail_rsts[iof];
  196. break;
  197. }
  198. }
  199. var low_res;
  200. for (i=step; i>=highest_res; i--) {
  201. iof = avail_rsts.indexOf(i);
  202. if (iof > -1) {
  203. low_res = avail_rsts[iof];
  204. break;
  205. }
  206. }
  207. /* What's the closest librato resolution given the step ? */
  208. return ((top_res-step) < (step-low_res)) ? top_res : low_res;
  209. }
  210. function find_librato_resolution(sdate, edate, step) {
  211. var i_size = edate - sdate, // interval size
  212. month = 2419200,
  213. week = 604800,
  214. two_days = 172800,
  215. ideal_res;
  216. if (i_size > month)
  217. return 3600;
  218. ideal_res = find_ideal_librato_resolution(step);
  219. /*
  220. * Now we have the ideal resolution, but due to the retention policies at librato, maybe we have
  221. * to use a higher resolution.
  222. * http://support.metrics.librato.com/knowledgebase/articles/66838-understanding-metrics-roll-ups-retention-and-grap
  223. */
  224. if (i_size > week && ideal_res < 900)
  225. return 900;
  226. else if (i_size > two_days && ideal_res < 60)
  227. return 60;
  228. else
  229. return ideal_res;
  230. }
  231. /* All the logic to query the librato API is here */
  232. var librato_request = function(composite) {
  233. var url_prefix = "https://metrics-api.librato.com/v1/metrics";
  234. function make_url(sdate, edate, step) {
  235. var params = "compose=" + composite +
  236. "&start_time=" + sdate +
  237. "&end_time=" + edate +
  238. "&resolution=" + find_librato_resolution(sdate, edate, step);
  239. return url_prefix + "?" + params;
  240. }
  241. /*
  242. * We are most likely not going to get the same number of measurements
  243. * cubism expects for a particular context: We have to perform down/up
  244. * sampling
  245. */
  246. function down_up_sampling(isdate, iedate, step, librato_mm) {
  247. var av = [];
  248. for (i=isdate; i<=iedate; i+=step) {
  249. var int_mes = [];
  250. while (librato_mm.length && librato_mm[0].measure_time <= i) {
  251. int_mes.push(librato_mm.shift().value);
  252. }
  253. var v;
  254. if (int_mes.length) { /* Compute the average */
  255. v = int_mes.reduce(function(a, b) { return a + b }) / int_mes.length;
  256. } else { /* No librato values on interval */
  257. v = (av.length) ? av[av.length-1] : 0;
  258. }
  259. av.push(v);
  260. }
  261. return av;
  262. }
  263. request = {};
  264. request.fire = function(isdate, iedate, step, callback_done) {
  265. var a_values = []; /* Store partial values from librato */
  266. /*
  267. * Librato has a limit in the number of measurements we get back in a request (100).
  268. * We recursively perform requests to the API to ensure we have all the data points
  269. * for the interval we are working on.
  270. */
  271. function actual_request(full_url) {
  272. d3.json(full_url)
  273. .header("X-Requested-With", "XMLHttpRequest")
  274. .header("Authorization", auth_string)
  275. .header("Librato-User-Agent", 'cubism/' + cubism.version)
  276. .get(function (error, data) { /* Callback; data available */
  277. if (!error) {
  278. if (data.measurements.length === 0) {
  279. return
  280. }
  281. data.measurements[0].series.forEach(function(o) { a_values.push(o); });
  282. var still_more_values = 'query' in data && 'next_time' in data.query;
  283. if (still_more_values) {
  284. actual_request(make_url(data.query.next_time, iedate, step));
  285. } else {
  286. var a_adjusted = down_up_sampling(isdate, iedate, step, a_values);
  287. callback_done(a_adjusted);
  288. }
  289. }
  290. });
  291. }
  292. actual_request(make_url(isdate, iedate, step));
  293. };
  294. return request;
  295. };
  296. /*
  297. * The user will use this method to create a cubism source (librato in this case)
  298. * and call .metric() as necessary to create metrics.
  299. */
  300. source.metric = function(m_composite) {
  301. return context.metric(function(start, stop, step, callback) {
  302. /* All the librato logic is here; .fire() retrieves the metrics' data */
  303. librato_request(m_composite)
  304. .fire(cubism_libratoFormatDate(start),
  305. cubism_libratoFormatDate(stop),
  306. cubism_libratoFormatDate(step),
  307. function(a_values) { callback(null, a_values); });
  308. }, m_composite += "");
  309. };
  310. /* This is not used when the source is librato */
  311. source.toString = function() {
  312. return "librato";
  313. };
  314. return source;
  315. };
  316. var cubism_libratoFormatDate = function(time) {
  317. return Math.floor(time / 1000);
  318. };
  319. cubism_contextPrototype.graphite = function(host) {
  320. if (!arguments.length) host = "";
  321. var source = {},
  322. context = this;
  323. source.metric = function(expression) {
  324. var sum = "sum";
  325. var metric = context.metric(function(start, stop, step, callback) {
  326. var target = expression;
  327. // Apply the summarize, if necessary.
  328. if (step !== 1e4) target = "summarize(" + target + ",'"
  329. + (!(step % 36e5) ? step / 36e5 + "hour" : !(step % 6e4) ? step / 6e4 + "min" : step / 1e3 + "sec")
  330. + "','" + sum + "')";
  331. d3.text(host + "/render?format=raw"
  332. + "&target=" + encodeURIComponent("alias(" + target + ",'')")
  333. + "&from=" + cubism_graphiteFormatDate(start - 2 * step) // off-by-two?
  334. + "&until=" + cubism_graphiteFormatDate(stop - 1000), function(text) {
  335. if (!text) return callback(new Error("unable to load data"));
  336. callback(null, cubism_graphiteParse(text));
  337. });
  338. }, expression += "");
  339. metric.summarize = function(_) {
  340. sum = _;
  341. return metric;
  342. };
  343. return metric;
  344. };
  345. source.find = function(pattern, callback) {
  346. d3.json(host + "/metrics/find?format=completer"
  347. + "&query=" + encodeURIComponent(pattern), function(result) {
  348. if (!result) return callback(new Error("unable to find metrics"));
  349. callback(null, result.metrics.map(function(d) { return d.path; }));
  350. });
  351. };
  352. // Returns the graphite host.
  353. source.toString = function() {
  354. return host;
  355. };
  356. return source;
  357. };
  358. // Graphite understands seconds since UNIX epoch.
  359. function cubism_graphiteFormatDate(time) {
  360. return Math.floor(time / 1000);
  361. }
  362. // Helper method for parsing graphite's raw format.
  363. function cubism_graphiteParse(text) {
  364. var i = text.indexOf("|"),
  365. meta = text.substring(0, i),
  366. c = meta.lastIndexOf(","),
  367. b = meta.lastIndexOf(",", c - 1),
  368. a = meta.lastIndexOf(",", b - 1),
  369. start = meta.substring(a + 1, b) * 1000,
  370. step = meta.substring(c + 1) * 1000;
  371. return text
  372. .substring(i + 1)
  373. .split(",")
  374. .slice(1) // the first value is always None?
  375. .map(function(d) { return +d; });
  376. }
  377. cubism_contextPrototype.gangliaWeb = function(config) {
  378. var host = '',
  379. uriPathPrefix = '/ganglia2/';
  380. if (arguments.length) {
  381. if (config.host) {
  382. host = config.host;
  383. }
  384. if (config.uriPathPrefix) {
  385. uriPathPrefix = config.uriPathPrefix;
  386. /* Add leading and trailing slashes, as appropriate. */
  387. if( uriPathPrefix[0] != '/' ) {
  388. uriPathPrefix = '/' + uriPathPrefix;
  389. }
  390. if( uriPathPrefix[uriPathPrefix.length - 1] != '/' ) {
  391. uriPathPrefix += '/';
  392. }
  393. }
  394. }
  395. var source = {},
  396. context = this;
  397. source.metric = function(metricInfo) {
  398. /* Store the members from metricInfo into local variables. */
  399. var clusterName = metricInfo.clusterName,
  400. metricName = metricInfo.metricName,
  401. hostName = metricInfo.hostName,
  402. isReport = metricInfo.isReport || false,
  403. titleGenerator = metricInfo.titleGenerator ||
  404. /* Reasonable (not necessarily pretty) default for titleGenerator. */
  405. function(unusedMetricInfo) {
  406. /* unusedMetricInfo is, well, unused in this default case. */
  407. return ('clusterName:' + clusterName +
  408. ' metricName:' + metricName +
  409. (hostName ? ' hostName:' + hostName : ''));
  410. },
  411. onChangeCallback = metricInfo.onChangeCallback;
  412. /* Default to plain, simple metrics. */
  413. var metricKeyName = isReport ? 'g' : 'm';
  414. var gangliaWebMetric = context.metric(function(start, stop, step, callback) {
  415. function constructGangliaWebRequestQueryParams() {
  416. return ('c=' + clusterName +
  417. '&' + metricKeyName + '=' + metricName +
  418. (hostName ? '&h=' + hostName : '') +
  419. '&cs=' + start/1000 + '&ce=' + stop/1000 + '&step=' + step/1000 + '&graphlot=1');
  420. }
  421. d3.json(host + uriPathPrefix + 'graph.php?' + constructGangliaWebRequestQueryParams(),
  422. function(result) {
  423. if( !result ) {
  424. return callback(new Error("Unable to fetch GangliaWeb data"));
  425. }
  426. callback(null, result[0].data);
  427. });
  428. }, titleGenerator(metricInfo));
  429. gangliaWebMetric.toString = function() {
  430. return titleGenerator(metricInfo);
  431. };
  432. /* Allow users to run their custom code each time a gangliaWebMetric changes.
  433. *
  434. * TODO Consider abstracting away the naked Cubism call, and instead exposing
  435. * a callback that takes in the values array (maybe alongwith the original
  436. * start and stop 'naked' parameters), since it's handy to have the entire
  437. * dataset at your disposal (and users will likely implement onChangeCallback
  438. * primarily to get at this dataset).
  439. */
  440. if (onChangeCallback) {
  441. gangliaWebMetric.on('change', onChangeCallback);
  442. }
  443. return gangliaWebMetric;
  444. };
  445. // Returns the gangliaWeb host + uriPathPrefix.
  446. source.toString = function() {
  447. return host + uriPathPrefix;
  448. };
  449. return source;
  450. };
  451. function cubism_metric(context) {
  452. if (!(context instanceof cubism_context)) throw new Error("invalid context");
  453. this.context = context;
  454. }
  455. var cubism_metricPrototype = cubism_metric.prototype;
  456. cubism.metric = cubism_metric;
  457. cubism_metricPrototype.valueAt = function() {
  458. return NaN;
  459. };
  460. cubism_metricPrototype.alias = function(name) {
  461. this.toString = function() { return name; };
  462. return this;
  463. };
  464. cubism_metricPrototype.extent = function() {
  465. var i = 0,
  466. n = this.context.size(),
  467. value,
  468. min = Infinity,
  469. max = -Infinity;
  470. while (++i < n) {
  471. value = this.valueAt(i);
  472. if (value < min) min = value;
  473. if (value > max) max = value;
  474. }
  475. return [min, max];
  476. };
  477. cubism_metricPrototype.on = function(type, listener) {
  478. return arguments.length < 2 ? null : this;
  479. };
  480. cubism_metricPrototype.shift = function() {
  481. return this;
  482. };
  483. cubism_metricPrototype.on = function() {
  484. return arguments.length < 2 ? null : this;
  485. };
  486. cubism_contextPrototype.metric = function(request, name) {
  487. var context = this,
  488. metric = new cubism_metric(context),
  489. id = ".metric-" + ++cubism_id,
  490. start = -Infinity,
  491. stop,
  492. step = context.step(),
  493. size = context.size(),
  494. values = [],
  495. event = d3.dispatch("change"),
  496. listening = 0,
  497. fetching;
  498. // Prefetch new data into a temporary array.
  499. function prepare(start1, stop) {
  500. var steps = Math.min(size, Math.round((start1 - start) / step));
  501. if (!steps || fetching) return; // already fetched, or fetching!
  502. fetching = true;
  503. steps = Math.min(size, steps + cubism_metricOverlap);
  504. var start0 = new Date(stop - steps * step);
  505. request(start0, stop, step, function(error, data) {
  506. fetching = false;
  507. if (error) return console.warn(error);
  508. var i = isFinite(start) ? Math.round((start0 - start) / step) : 0;
  509. for (var j = 0, m = data.length; j < m; ++j) values[j + i] = data[j];
  510. event.change.call(metric, start, stop);
  511. });
  512. }
  513. // When the context changes, switch to the new data, ready-or-not!
  514. function beforechange(start1, stop1) {
  515. if (!isFinite(start)) start = start1;
  516. values.splice(0, Math.max(0, Math.min(size, Math.round((start1 - start) / step))));
  517. start = start1;
  518. stop = stop1;
  519. }
  520. //
  521. metric.valueAt = function(i) {
  522. return values[i];
  523. };
  524. //
  525. metric.shift = function(offset) {
  526. return context.metric(cubism_metricShift(request, +offset));
  527. };
  528. //
  529. metric.on = function(type, listener) {
  530. if (!arguments.length) return event.on(type);
  531. // If there are no listeners, then stop listening to the context,
  532. // and avoid unnecessary fetches.
  533. if (listener == null) {
  534. if (event.on(type) != null && --listening == 0) {
  535. context.on("prepare" + id, null).on("beforechange" + id, null);
  536. }
  537. } else {
  538. if (event.on(type) == null && ++listening == 1) {
  539. context.on("prepare" + id, prepare).on("beforechange" + id, beforechange);
  540. }
  541. }
  542. event.on(type, listener);
  543. // Notify the listener of the current start and stop time, as appropriate.
  544. // This way, charts can display synchronous metrics immediately.
  545. if (listener != null) {
  546. if (/^change(\.|$)/.test(type)) listener.call(context, start, stop);
  547. }
  548. return metric;
  549. };
  550. //
  551. if (arguments.length > 1) metric.toString = function() {
  552. return name;
  553. };
  554. return metric;
  555. };
  556. // Number of metric to refetch each period, in case of lag.
  557. var cubism_metricOverlap = 6;
  558. // Wraps the specified request implementation, and shifts time by the given offset.
  559. function cubism_metricShift(request, offset) {
  560. return function(start, stop, step, callback) {
  561. request(new Date(+start + offset), new Date(+stop + offset), step, callback);
  562. };
  563. }
  564. function cubism_metricConstant(context, value) {
  565. cubism_metric.call(this, context);
  566. value = +value;
  567. var name = value + "";
  568. this.valueOf = function() { return value; };
  569. this.toString = function() { return name; };
  570. }
  571. var cubism_metricConstantPrototype = cubism_metricConstant.prototype = Object.create(cubism_metric.prototype);
  572. cubism_metricConstantPrototype.valueAt = function() {
  573. return +this;
  574. };
  575. cubism_metricConstantPrototype.extent = function() {
  576. return [+this, +this];
  577. };
  578. function cubism_metricOperator(name, operate) {
  579. function cubism_metricOperator(left, right) {
  580. if (!(right instanceof cubism_metric)) right = new cubism_metricConstant(left.context, right);
  581. else if (left.context !== right.context) throw new Error("mismatch context");
  582. cubism_metric.call(this, left.context);
  583. this.left = left;
  584. this.right = right;
  585. this.toString = function() { return left + " " + name + " " + right; };
  586. }
  587. var cubism_metricOperatorPrototype = cubism_metricOperator.prototype = Object.create(cubism_metric.prototype);
  588. cubism_metricOperatorPrototype.valueAt = function(i) {
  589. return operate(this.left.valueAt(i), this.right.valueAt(i));
  590. };
  591. cubism_metricOperatorPrototype.shift = function(offset) {
  592. return new cubism_metricOperator(this.left.shift(offset), this.right.shift(offset));
  593. };
  594. cubism_metricOperatorPrototype.on = function(type, listener) {
  595. if (arguments.length < 2) return this.left.on(type);
  596. this.left.on(type, listener);
  597. this.right.on(type, listener);
  598. return this;
  599. };
  600. return function(right) {
  601. return new cubism_metricOperator(this, right);
  602. };
  603. }
  604. cubism_metricPrototype.add = cubism_metricOperator("+", function(left, right) {
  605. return left + right;
  606. });
  607. cubism_metricPrototype.subtract = cubism_metricOperator("-", function(left, right) {
  608. return left - right;
  609. });
  610. cubism_metricPrototype.multiply = cubism_metricOperator("*", function(left, right) {
  611. return left * right;
  612. });
  613. cubism_metricPrototype.divide = cubism_metricOperator("/", function(left, right) {
  614. return left / right;
  615. });
  616. cubism_contextPrototype.horizon = function() {
  617. var context = this,
  618. mode = "offset",
  619. buffer = document.createElement("canvas"),
  620. width = buffer.width = context.size(),
  621. height = buffer.height = 30,
  622. scale = d3.scale.linear().interpolate(d3.interpolateRound),
  623. metric = cubism_identity,
  624. extent = null,
  625. title = cubism_identity,
  626. format = d3.format(".2s"),
  627. colors = ["#08519c","#3182bd","#6baed6","#bdd7e7","#bae4b3","#74c476","#31a354","#006d2c"];
  628. function horizon(selection) {
  629. selection
  630. .on("mousemove.horizon", function() { context.focus(Math.round(d3.mouse(this)[0])); })
  631. .on("mouseout.horizon", function() { context.focus(null); });
  632. selection.append("canvas")
  633. .attr("width", width)
  634. .attr("height", height);
  635. selection.append("span")
  636. .attr("class", "title")
  637. .text(title);
  638. selection.append("span")
  639. .attr("class", "value");
  640. selection.each(function(d, i) {
  641. var that = this,
  642. id = ++cubism_id,
  643. metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric,
  644. colors_ = typeof colors === "function" ? colors.call(that, d, i) : colors,
  645. extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent,
  646. start = -Infinity,
  647. step = context.step(),
  648. canvas = d3.select(that).select("canvas"),
  649. span = d3.select(that).select(".value"),
  650. max_,
  651. m = colors_.length >> 1,
  652. ready;
  653. canvas.datum({id: id, metric: metric_});
  654. canvas = canvas.node().getContext("2d");
  655. function change(start1, stop) {
  656. canvas.save();
  657. // compute the new extent and ready flag
  658. var extent = metric_.extent();
  659. ready = extent.every(isFinite);
  660. if (extent_ != null) extent = extent_;
  661. // if this is an update (with no extent change), copy old values!
  662. var i0 = 0, max = Math.max(-extent[0], extent[1]);
  663. if (this === context) {
  664. if (max == max_) {
  665. i0 = width - cubism_metricOverlap;
  666. var dx = (start1 - start) / step;
  667. if (dx < width) {
  668. var canvas0 = buffer.getContext("2d");
  669. canvas0.clearRect(0, 0, width, height);
  670. canvas0.drawImage(canvas.canvas, dx, 0, width - dx, height, 0, 0, width - dx, height);
  671. canvas.clearRect(0, 0, width, height);
  672. canvas.drawImage(canvas0.canvas, 0, 0);
  673. }
  674. }
  675. start = start1;
  676. }
  677. // update the domain
  678. scale.domain([0, max_ = max]);
  679. // clear for the new data
  680. canvas.clearRect(i0, 0, width - i0, height);
  681. // record whether there are negative values to display
  682. var negative;
  683. // positive bands
  684. for (var j = 0; j < m; ++j) {
  685. canvas.fillStyle = colors_[m + j];
  686. // Adjust the range based on the current band index.
  687. var y0 = (j - m + 1) * height;
  688. scale.range([m * height + y0, y0]);
  689. y0 = scale(0);
  690. for (var i = i0, n = width, y1; i < n; ++i) {
  691. y1 = metric_.valueAt(i);
  692. if (y1 <= 0) { negative = true; continue; }
  693. if (y1 === undefined) continue;
  694. canvas.fillRect(i, y1 = scale(y1), 1, y0 - y1);
  695. }
  696. }
  697. if (negative) {
  698. // enable offset mode
  699. if (mode === "offset") {
  700. canvas.translate(0, height);
  701. canvas.scale(1, -1);
  702. }
  703. // negative bands
  704. for (var j = 0; j < m; ++j) {
  705. canvas.fillStyle = colors_[m - 1 - j];
  706. // Adjust the range based on the current band index.
  707. var y0 = (j - m + 1) * height;
  708. scale.range([m * height + y0, y0]);
  709. y0 = scale(0);
  710. for (var i = i0, n = width, y1; i < n; ++i) {
  711. y1 = metric_.valueAt(i);
  712. if (y1 >= 0) continue;
  713. canvas.fillRect(i, scale(-y1), 1, y0 - scale(-y1));
  714. }
  715. }
  716. }
  717. canvas.restore();
  718. }
  719. function focus(i) {
  720. if (i == null) i = width - 1;
  721. var value = metric_.valueAt(i);
  722. span.datum(value).text(isNaN(value) ? null : format);
  723. }
  724. // Update the chart when the context changes.
  725. context.on("change.horizon-" + id, change);
  726. context.on("focus.horizon-" + id, focus);
  727. // Display the first metric change immediately,
  728. // but defer subsequent updates to the canvas change.
  729. // Note that someone still needs to listen to the metric,
  730. // so that it continues to update automatically.
  731. metric_.on("change.horizon-" + id, function(start, stop) {
  732. change(start, stop), focus();
  733. if (ready) metric_.on("change.horizon-" + id, cubism_identity);
  734. });
  735. });
  736. }
  737. horizon.remove = function(selection) {
  738. selection
  739. .on("mousemove.horizon", null)
  740. .on("mouseout.horizon", null);
  741. selection.selectAll("canvas")
  742. .each(remove)
  743. .remove();
  744. selection.selectAll(".title,.value")
  745. .remove();
  746. function remove(d) {
  747. d.metric.on("change.horizon-" + d.id, null);
  748. context.on("change.horizon-" + d.id, null);
  749. context.on("focus.horizon-" + d.id, null);
  750. }
  751. };
  752. horizon.mode = function(_) {
  753. if (!arguments.length) return mode;
  754. mode = _ + "";
  755. return horizon;
  756. };
  757. horizon.height = function(_) {
  758. if (!arguments.length) return height;
  759. buffer.height = height = +_;
  760. return horizon;
  761. };
  762. horizon.metric = function(_) {
  763. if (!arguments.length) return metric;
  764. metric = _;
  765. return horizon;
  766. };
  767. horizon.scale = function(_) {
  768. if (!arguments.length) return scale;
  769. scale = _;
  770. return horizon;
  771. };
  772. horizon.extent = function(_) {
  773. if (!arguments.length) return extent;
  774. extent = _;
  775. return horizon;
  776. };
  777. horizon.title = function(_) {
  778. if (!arguments.length) return title;
  779. title = _;
  780. return horizon;
  781. };
  782. horizon.format = function(_) {
  783. if (!arguments.length) return format;
  784. format = _;
  785. return horizon;
  786. };
  787. horizon.colors = function(_) {
  788. if (!arguments.length) return colors;
  789. colors = _;
  790. return horizon;
  791. };
  792. return horizon;
  793. };
  794. cubism_contextPrototype.comparison = function() {
  795. var context = this,
  796. width = context.size(),
  797. height = 120,
  798. scale = d3.scale.linear().interpolate(d3.interpolateRound),
  799. primary = function(d) { return d[0]; },
  800. secondary = function(d) { return d[1]; },
  801. extent = null,
  802. title = cubism_identity,
  803. formatPrimary = cubism_comparisonPrimaryFormat,
  804. formatChange = cubism_comparisonChangeFormat,
  805. colors = ["#9ecae1", "#225b84", "#a1d99b", "#22723a"],
  806. strokeWidth = 1.5;
  807. function comparison(selection) {
  808. selection
  809. .on("mousemove.comparison", function() { context.focus(Math.round(d3.mouse(this)[0])); })
  810. .on("mouseout.comparison", function() { context.focus(null); });
  811. selection.append("canvas")
  812. .attr("width", width)
  813. .attr("height", height);
  814. selection.append("span")
  815. .attr("class", "title")
  816. .text(title);
  817. selection.append("span")
  818. .attr("class", "value primary");
  819. selection.append("span")
  820. .attr("class", "value change");
  821. selection.each(function(d, i) {
  822. var that = this,
  823. id = ++cubism_id,
  824. primary_ = typeof primary === "function" ? primary.call(that, d, i) : primary,
  825. secondary_ = typeof secondary === "function" ? secondary.call(that, d, i) : secondary,
  826. extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent,
  827. div = d3.select(that),
  828. canvas = div.select("canvas"),
  829. spanPrimary = div.select(".value.primary"),
  830. spanChange = div.select(".value.change"),
  831. ready;
  832. canvas.datum({id: id, primary: primary_, secondary: secondary_});
  833. canvas = canvas.node().getContext("2d");
  834. function change(start, stop) {
  835. canvas.save();
  836. canvas.clearRect(0, 0, width, height);
  837. // update the scale
  838. var primaryExtent = primary_.extent(),
  839. secondaryExtent = secondary_.extent(),
  840. extent = extent_ == null ? primaryExtent : extent_;
  841. scale.domain(extent).range([height, 0]);
  842. ready = primaryExtent.concat(secondaryExtent).every(isFinite);
  843. // consistent overplotting
  844. var round = start / context.step() & 1
  845. ? cubism_comparisonRoundOdd
  846. : cubism_comparisonRoundEven;
  847. // positive changes
  848. canvas.fillStyle = colors[2];
  849. for (var i = 0, n = width; i < n; ++i) {
  850. var y0 = scale(primary_.valueAt(i)),
  851. y1 = scale(secondary_.valueAt(i));
  852. if (y0 < y1) canvas.fillRect(round(i), y0, 1, y1 - y0);
  853. }
  854. // negative changes
  855. canvas.fillStyle = colors[0];
  856. for (i = 0; i < n; ++i) {
  857. var y0 = scale(primary_.valueAt(i)),
  858. y1 = scale(secondary_.valueAt(i));
  859. if (y0 > y1) canvas.fillRect(round(i), y1, 1, y0 - y1);
  860. }
  861. // positive values
  862. canvas.fillStyle = colors[3];
  863. for (i = 0; i < n; ++i) {
  864. var y0 = scale(primary_.valueAt(i)),
  865. y1 = scale(secondary_.valueAt(i));
  866. if (y0 <= y1) canvas.fillRect(round(i), y0, 1, strokeWidth);
  867. }
  868. // negative values
  869. canvas.fillStyle = colors[1];
  870. for (i = 0; i < n; ++i) {
  871. var y0 = scale(primary_.valueAt(i)),
  872. y1 = scale(secondary_.valueAt(i));
  873. if (y0 > y1) canvas.fillRect(round(i), y0 - strokeWidth, 1, strokeWidth);
  874. }
  875. canvas.restore();
  876. }
  877. function focus(i) {
  878. if (i == null) i = width - 1;
  879. var valuePrimary = primary_.valueAt(i),
  880. valueSecondary = secondary_.valueAt(i),
  881. valueChange = (valuePrimary - valueSecondary) / valueSecondary;
  882. spanPrimary
  883. .datum(valuePrimary)
  884. .text(isNaN(valuePrimary) ? null : formatPrimary);
  885. spanChange
  886. .datum(valueChange)
  887. .text(isNaN(valueChange) ? null : formatChange)
  888. .attr("class", "value change " + (valueChange > 0 ? "positive" : valueChange < 0 ? "negative" : ""));
  889. }
  890. // Display the first primary change immediately,
  891. // but defer subsequent updates to the context change.
  892. // Note that someone still needs to listen to the metric,
  893. // so that it continues to update automatically.
  894. primary_.on("change.comparison-" + id, firstChange);
  895. secondary_.on("change.comparison-" + id, firstChange);
  896. function firstChange(start, stop) {
  897. change(start, stop), focus();
  898. if (ready) {
  899. primary_.on("change.comparison-" + id, cubism_identity);
  900. secondary_.on("change.comparison-" + id, cubism_identity);
  901. }
  902. }
  903. // Update the chart when the context changes.
  904. context.on("change.comparison-" + id, change);
  905. context.on("focus.comparison-" + id, focus);
  906. });
  907. }
  908. comparison.remove = function(selection) {
  909. selection
  910. .on("mousemove.comparison", null)
  911. .on("mouseout.comparison", null);
  912. selection.selectAll("canvas")
  913. .each(remove)
  914. .remove();
  915. selection.selectAll(".title,.value")
  916. .remove();
  917. function remove(d) {
  918. d.primary.on("change.comparison-" + d.id, null);
  919. d.secondary.on("change.comparison-" + d.id, null);
  920. context.on("change.comparison-" + d.id, null);
  921. context.on("focus.comparison-" + d.id, null);
  922. }
  923. };
  924. comparison.height = function(_) {
  925. if (!arguments.length) return height;
  926. height = +_;
  927. return comparison;
  928. };
  929. comparison.primary = function(_) {
  930. if (!arguments.length) return primary;
  931. primary = _;
  932. return comparison;
  933. };
  934. comparison.secondary = function(_) {
  935. if (!arguments.length) return secondary;
  936. secondary = _;
  937. return comparison;
  938. };
  939. comparison.scale = function(_) {
  940. if (!arguments.length) return scale;
  941. scale = _;
  942. return comparison;
  943. };
  944. comparison.extent = function(_) {
  945. if (!arguments.length) return extent;
  946. extent = _;
  947. return comparison;
  948. };
  949. comparison.title = function(_) {
  950. if (!arguments.length) return title;
  951. title = _;
  952. return comparison;
  953. };
  954. comparison.formatPrimary = function(_) {
  955. if (!arguments.length) return formatPrimary;
  956. formatPrimary = _;
  957. return comparison;
  958. };
  959. comparison.formatChange = function(_) {
  960. if (!arguments.length) return formatChange;
  961. formatChange = _;
  962. return comparison;
  963. };
  964. comparison.colors = function(_) {
  965. if (!arguments.length) return colors;
  966. colors = _;
  967. return comparison;
  968. };
  969. comparison.strokeWidth = function(_) {
  970. if (!arguments.length) return strokeWidth;
  971. strokeWidth = _;
  972. return comparison;
  973. };
  974. return comparison;
  975. };
  976. var cubism_comparisonPrimaryFormat = d3.format(".2s"),
  977. cubism_comparisonChangeFormat = d3.format("+.0%");
  978. function cubism_comparisonRoundEven(i) {
  979. return i & 0xfffffe;
  980. }
  981. function cubism_comparisonRoundOdd(i) {
  982. return ((i + 1) & 0xfffffe) - 1;
  983. }
  984. cubism_contextPrototype.axis = function() {
  985. var context = this,
  986. scale = context.scale,
  987. axis_ = d3.svg.axis().scale(scale);
  988. var formatDefault = context.step() < 6e4 ? cubism_axisFormatSeconds
  989. : context.step() < 864e5 ? cubism_axisFormatMinutes
  990. : cubism_axisFormatDays;
  991. var format = formatDefault;
  992. function axis(selection) {
  993. var id = ++cubism_id,
  994. tick;
  995. var g = selection.append("svg")
  996. .datum({id: id})
  997. .attr("width", context.size())
  998. .attr("height", Math.max(28, -axis.tickSize()))
  999. .append("g")
  1000. .attr("transform", "translate(0," + (axis_.orient() === "top" ? 27 : 4) + ")")
  1001. .call(axis_);
  1002. context.on("change.axis-" + id, function() {
  1003. g.call(axis_);
  1004. if (!tick) tick = d3.select(g.node().appendChild(g.selectAll("text").node().cloneNode(true)))
  1005. .style("display", "none")
  1006. .text(null);
  1007. });
  1008. context.on("focus.axis-" + id, function(i) {
  1009. if (tick) {
  1010. if (i == null) {
  1011. tick.style("display", "none");
  1012. g.selectAll("text").style("fill-opacity", null);
  1013. } else {
  1014. tick.style("display", null).attr("x", i).text(format(scale.invert(i)));
  1015. var dx = tick.node().getComputedTextLength() + 6;
  1016. g.selectAll("text").style("fill-opacity", function(d) { return Math.abs(scale(d) - i) < dx ? 0 : 1; });
  1017. }
  1018. }
  1019. });
  1020. }
  1021. axis.remove = function(selection) {
  1022. selection.selectAll("svg")
  1023. .each(remove)
  1024. .remove();
  1025. function remove(d) {
  1026. context.on("change.axis-" + d.id, null);
  1027. context.on("focus.axis-" + d.id, null);
  1028. }
  1029. };
  1030. axis.focusFormat = function(_) {
  1031. if (!arguments.length) return format == formatDefault ? null : _;
  1032. format = _ == null ? formatDefault : _;
  1033. return axis;
  1034. };
  1035. return d3.rebind(axis, axis_,
  1036. "orient",
  1037. "ticks",
  1038. "tickSubdivide",
  1039. "tickSize",
  1040. "tickPadding",
  1041. "tickFormat");
  1042. };
  1043. var cubism_axisFormatSeconds = d3.time.format("%I:%M:%S %p"),
  1044. cubism_axisFormatMinutes = d3.time.format("%I:%M %p"),
  1045. cubism_axisFormatDays = d3.time.format("%B %d");
  1046. cubism_contextPrototype.rule = function() {
  1047. var context = this,
  1048. metric = cubism_identity;
  1049. function rule(selection) {
  1050. var id = ++cubism_id;
  1051. var line = selection.append("div")
  1052. .datum({id: id})
  1053. .attr("class", "line")
  1054. .call(cubism_ruleStyle);
  1055. selection.each(function(d, i) {
  1056. var that = this,
  1057. id = ++cubism_id,
  1058. metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric;
  1059. if (!metric_) return;
  1060. function change(start, stop) {
  1061. var values = [];
  1062. for (var i = 0, n = context.size(); i < n; ++i) {
  1063. if (metric_.valueAt(i)) {
  1064. values.push(i);
  1065. }
  1066. }
  1067. var lines = selection.selectAll(".metric").data(values);
  1068. lines.exit().remove();
  1069. lines.enter().append("div").attr("class", "metric line").call(cubism_ruleStyle);
  1070. lines.style("left", cubism_ruleLeft);
  1071. }
  1072. context.on("change.rule-" + id, change);
  1073. metric_.on("change.rule-" + id, change);
  1074. });
  1075. context.on("focus.rule-" + id, function(i) {
  1076. line.datum(i)
  1077. .style("display", i == null ? "none" : null)
  1078. .style("left", i == null ? null : cubism_ruleLeft);
  1079. });
  1080. }
  1081. rule.remove = function(selection) {
  1082. selection.selectAll(".line")
  1083. .each(remove)
  1084. .remove();
  1085. function remove(d) {
  1086. context.on("focus.rule-" + d.id, null);
  1087. }
  1088. };
  1089. rule.metric = function(_) {
  1090. if (!arguments.length) return metric;
  1091. metric = _;
  1092. return rule;
  1093. };
  1094. return rule;
  1095. };
  1096. function cubism_ruleStyle(line) {
  1097. line
  1098. .style("position", "absolute")
  1099. .style("top", 0)
  1100. .style("bottom", 0)
  1101. .style("width", "1px")
  1102. .style("pointer-events", "none");
  1103. }
  1104. function cubism_ruleLeft(i) {
  1105. return i + "px";
  1106. }
  1107. })(this);