PageRenderTime 58ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/ajax/libs/cubism/1.1.0/cubism.v1.js

https://gitlab.com/Mirros/cdnjs
JavaScript | 995 lines | 774 code | 163 blank | 58 comment | 113 complexity | 3ac3c920d84136318f151cb94502095e MD5 | raw file
  1. (function(exports){
  2. var cubism = exports.cubism = {version: "1.1.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;
  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. cubism_contextPrototype.graphite = function(host) {
  162. if (!arguments.length) host = "";
  163. var source = {},
  164. context = this;
  165. source.metric = function(expression) {
  166. var metric = context.metric(function(start, stop, step, callback) {
  167. d3.text(host + "/render?format=raw"
  168. + "&target=" + encodeURIComponent("alias(" + expression + ",'')")
  169. + "&from=" + cubism_graphiteFormatDate(start - 2 * step) // off-by-two?
  170. + "&until=" + cubism_graphiteFormatDate(stop - 1000), function(text) {
  171. if (!text) return callback(new Error("unable to load data"));
  172. callback(null, cubism_graphiteParse(text));
  173. });
  174. }, expression += "");
  175. metric.summarize = summarize;
  176. return metric;
  177. };
  178. source.find = function(pattern, callback) {
  179. d3.json(host + "/metrics/find?format=completer"
  180. + "&query=" + encodeURIComponent(pattern), function(result) {
  181. if (!result) return callback(new Error("unable to find metrics"));
  182. callback(null, result.metrics.map(function(d) { return d.path; }));
  183. });
  184. };
  185. // Returns the graphite host.
  186. source.toString = function() {
  187. return host;
  188. };
  189. function summarize(method) {
  190. var step = Math.round(context.step() / 1e3);
  191. if (step === 10) return this;
  192. step = !(step % 3600) ? step / 3600 + "hour" : !(step % 60) ? step / 60 + "min" : step + "sec";
  193. return source.metric("summarize(" + this + ",'" + step + "','" + method + "')");
  194. }
  195. return source;
  196. };
  197. // Graphite understands seconds since UNIX epoch.
  198. function cubism_graphiteFormatDate(time) {
  199. return Math.floor(time / 1000);
  200. }
  201. // Helper method for parsing graphite's raw format.
  202. function cubism_graphiteParse(text) {
  203. var i = text.indexOf("|"),
  204. meta = text.substring(0, i),
  205. c = meta.lastIndexOf(","),
  206. b = meta.lastIndexOf(",", c - 1),
  207. a = meta.lastIndexOf(",", b - 1),
  208. start = meta.substring(a + 1, b) * 1000,
  209. step = meta.substring(c + 1) * 1000;
  210. return text
  211. .substring(i + 1)
  212. .split(",")
  213. .slice(1) // the first value is always None?
  214. .map(function(d) { return +d; });
  215. }
  216. function cubism_metric(context) {
  217. if (!(context instanceof cubism_context)) throw new Error("invalid context");
  218. this.context = context;
  219. }
  220. var cubism_metricPrototype = cubism_metric.prototype;
  221. cubism_metricPrototype.valueAt = function() {
  222. return NaN;
  223. };
  224. cubism_metricPrototype.alias = function(name) {
  225. this.toString = function() { return name; };
  226. return this;
  227. };
  228. cubism_metricPrototype.extent = function() {
  229. var i = 0,
  230. n = this.context.size(),
  231. value,
  232. min = Infinity,
  233. max = -Infinity;
  234. while (++i < n) {
  235. value = this.valueAt(i);
  236. if (value < min) min = value;
  237. if (value > max) max = value;
  238. }
  239. return [min, max];
  240. };
  241. cubism_metricPrototype.on = function(type, listener) {
  242. return arguments.length < 2 ? null : this;
  243. };
  244. cubism_metricPrototype.shift = function() {
  245. return this;
  246. };
  247. cubism_metricPrototype.on = function() {
  248. return arguments.length < 2 ? null : this;
  249. };
  250. cubism_contextPrototype.metric = function(request, name) {
  251. var context = this,
  252. metric = new cubism_metric(context),
  253. id = ".metric-" + ++cubism_id,
  254. start = -Infinity,
  255. stop,
  256. step = context.step(),
  257. size = context.size(),
  258. values = [],
  259. event = d3.dispatch("change"),
  260. listening = 0,
  261. fetching;
  262. // Prefetch new data into a temporary array.
  263. function prepare(start1, stop) {
  264. var steps = Math.min(size, Math.round((start1 - start) / step));
  265. if (!steps || fetching) return; // already fetched, or fetching!
  266. fetching = true;
  267. steps = Math.min(size, steps + cubism_metricOverlap);
  268. var start0 = new Date(stop - steps * step);
  269. request(start0, stop, step, function(error, data) {
  270. fetching = false;
  271. if (error) return console.warn(error);
  272. var i = isFinite(start) ? Math.round((start0 - start) / step) : 0;
  273. for (var j = 0, m = data.length; j < m; ++j) values[j + i] = data[j];
  274. event.change.call(metric, start, stop);
  275. });
  276. }
  277. // When the context changes, switch to the new data, ready-or-not!
  278. function beforechange(start1, stop1) {
  279. if (!isFinite(start)) start = start1;
  280. values.splice(0, Math.max(0, Math.min(size, Math.round((start1 - start) / step))));
  281. start = start1;
  282. stop = stop1;
  283. }
  284. //
  285. metric.valueAt = function(i) {
  286. return values[i];
  287. };
  288. //
  289. metric.shift = function(offset) {
  290. return context.metric(cubism_metricShift(request, +offset));
  291. };
  292. //
  293. metric.on = function(type, listener) {
  294. if (!arguments.length) return event.on(type);
  295. // If there are no listeners, then stop listening to the context,
  296. // and avoid unnecessary fetches.
  297. if (listener == null) {
  298. if (event.on(type) != null && --listening == 0) {
  299. context.on("prepare" + id, null).on("beforechange" + id, null);
  300. }
  301. } else {
  302. if (event.on(type) == null && ++listening == 1) {
  303. context.on("prepare" + id, prepare).on("beforechange" + id, beforechange);
  304. }
  305. }
  306. event.on(type, listener);
  307. // Notify the listener of the current start and stop time, as appropriate.
  308. // This way, charts can display synchronous metrics immediately.
  309. if (listener != null) {
  310. if (/^change(\.|$)/.test(type)) listener.call(context, start, stop);
  311. }
  312. return metric;
  313. };
  314. //
  315. if (arguments.length > 1) metric.toString = function() {
  316. return name;
  317. };
  318. return metric;
  319. };
  320. // Number of metric to refetch each period, in case of lag.
  321. var cubism_metricOverlap = 6;
  322. // Wraps the specified request implementation, and shifts time by the given offset.
  323. function cubism_metricShift(request, offset) {
  324. return function(start, stop, step, callback) {
  325. request(new Date(+start + offset), new Date(+stop + offset), step, callback);
  326. };
  327. }
  328. function cubism_metricConstant(context, value) {
  329. cubism_metric.call(this, context);
  330. value = +value;
  331. var name = value + "";
  332. this.valueOf = function() { return value; };
  333. this.toString = function() { return name; };
  334. }
  335. var cubism_metricConstantPrototype = cubism_metricConstant.prototype = Object.create(cubism_metric.prototype);
  336. cubism_metricConstantPrototype.valueAt = function() {
  337. return +this;
  338. };
  339. cubism_metricConstantPrototype.extent = function() {
  340. return [+this, +this];
  341. };
  342. function cubism_metricOperator(name, operate) {
  343. function cubism_metricOperator(left, right) {
  344. if (!(right instanceof cubism_metric)) right = new cubism_metricConstant(left.context, right);
  345. else if (left.context !== right.context) throw new Error("mismatch context");
  346. cubism_metric.call(this, left.context);
  347. this.left = left;
  348. this.right = right;
  349. this.toString = function() { return left + " " + name + " " + right; };
  350. }
  351. var cubism_metricOperatorPrototype = cubism_metricOperator.prototype = Object.create(cubism_metric.prototype);
  352. cubism_metricOperatorPrototype.valueAt = function(i) {
  353. return operate(this.left.valueAt(i), this.right.valueAt(i));
  354. };
  355. cubism_metricOperatorPrototype.shift = function(offset) {
  356. return new cubism_metricOperator(this.left.shift(offset), this.right.shift(offset));
  357. };
  358. cubism_metricOperatorPrototype.on = function(type, listener) {
  359. if (arguments.length < 2) return this.left.on(type);
  360. this.left.on(type, listener);
  361. this.right.on(type, listener);
  362. return this;
  363. };
  364. return function(right) {
  365. return new cubism_metricOperator(this, right);
  366. };
  367. }
  368. cubism_metricPrototype.add = cubism_metricOperator("+", function(left, right) {
  369. return left + right;
  370. });
  371. cubism_metricPrototype.subtract = cubism_metricOperator("-", function(left, right) {
  372. return left - right;
  373. });
  374. cubism_metricPrototype.multiply = cubism_metricOperator("*", function(left, right) {
  375. return left * right;
  376. });
  377. cubism_metricPrototype.divide = cubism_metricOperator("/", function(left, right) {
  378. return left / right;
  379. });
  380. cubism_contextPrototype.horizon = function() {
  381. var context = this,
  382. mode = "offset",
  383. buffer = document.createElement("canvas"),
  384. width = buffer.width = context.size(),
  385. height = buffer.height = 30,
  386. scale = d3.scale.linear().interpolate(d3.interpolateRound),
  387. metric = cubism_identity,
  388. extent = null,
  389. title = cubism_identity,
  390. format = d3.format(".2s"),
  391. colors = ["#08519c","#3182bd","#6baed6","#bdd7e7","#bae4b3","#74c476","#31a354","#006d2c"];
  392. function horizon(selection) {
  393. selection
  394. .on("mousemove.horizon", function() { context.focus(d3.mouse(this)[0]); })
  395. .on("mouseout.horizon", function() { context.focus(null); });
  396. selection.append("canvas")
  397. .attr("width", width)
  398. .attr("height", height);
  399. selection.append("span")
  400. .attr("class", "title")
  401. .text(title);
  402. selection.append("span")
  403. .attr("class", "value");
  404. selection.each(function(d, i) {
  405. var that = this,
  406. id = ++cubism_id,
  407. metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric,
  408. colors_ = typeof colors === "function" ? colors.call(that, d, i) : colors,
  409. extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent,
  410. start = -Infinity,
  411. step = context.step(),
  412. canvas = d3.select(that).select("canvas"),
  413. span = d3.select(that).select(".value"),
  414. max_,
  415. m = colors_.length >> 1,
  416. ready;
  417. canvas.datum({id: id, metric: metric_});
  418. canvas = canvas.node().getContext("2d");
  419. function change(start1, stop) {
  420. canvas.save();
  421. // compute the new extent and ready flag
  422. var extent = metric_.extent();
  423. ready = extent.every(isFinite);
  424. if (extent_ != null) extent = extent_;
  425. // if this is an update (with no extent change), copy old values!
  426. var i0 = 0, max = Math.max(-extent[0], extent[1]);
  427. if (this === context) {
  428. if (max == max_) {
  429. i0 = width - cubism_metricOverlap;
  430. var dx = (start1 - start) / step;
  431. if (dx < width) {
  432. var canvas0 = buffer.getContext("2d");
  433. canvas0.clearRect(0, 0, width, height);
  434. canvas0.drawImage(canvas.canvas, dx, 0, width - dx, height, 0, 0, width - dx, height);
  435. canvas.clearRect(0, 0, width, height);
  436. canvas.drawImage(canvas0.canvas, 0, 0);
  437. }
  438. }
  439. start = start1;
  440. }
  441. // update the domain
  442. scale.domain([0, max_ = max]);
  443. // clear for the new data
  444. canvas.clearRect(i0, 0, width - i0, height);
  445. // record whether there are negative values to display
  446. var negative;
  447. // positive bands
  448. for (var j = 0; j < m; ++j) {
  449. canvas.fillStyle = colors_[m + j];
  450. // Adjust the range based on the current band index.
  451. var y0 = (j - m + 1) * height;
  452. scale.range([m * height + y0, y0]);
  453. y0 = scale(0);
  454. for (var i = i0, n = width, y1; i < n; ++i) {
  455. y1 = metric_.valueAt(i);
  456. if (y1 <= 0) { negative = true; continue; }
  457. canvas.fillRect(i, y1 = scale(y1), 1, y0 - y1);
  458. }
  459. }
  460. if (negative) {
  461. // enable offset mode
  462. if (mode === "offset") {
  463. canvas.translate(0, height);
  464. canvas.scale(1, -1);
  465. }
  466. // negative bands
  467. for (var j = 0; j < m; ++j) {
  468. canvas.fillStyle = colors_[m - 1 - j];
  469. // Adjust the range based on the current band index.
  470. var y0 = (j - m + 1) * height;
  471. scale.range([m * height + y0, y0]);
  472. y0 = scale(0);
  473. for (var i = i0, n = width, y1; i < n; ++i) {
  474. y1 = metric_.valueAt(i);
  475. if (y1 >= 0) continue;
  476. canvas.fillRect(i, scale(-y1), 1, y0 - scale(-y1));
  477. }
  478. }
  479. }
  480. canvas.restore();
  481. }
  482. function focus(i) {
  483. if (i == null) i = width - 1;
  484. var value = metric_.valueAt(i);
  485. span.datum(value).text(isNaN(value) ? null : format);
  486. }
  487. // Update the chart when the context changes.
  488. context.on("change.horizon-" + id, change);
  489. context.on("focus.horizon-" + id, focus);
  490. // Display the first metric change immediately,
  491. // but defer subsequent updates to the canvas change.
  492. // Note that someone still needs to listen to the metric,
  493. // so that it continues to update automatically.
  494. metric_.on("change.horizon-" + id, function(start, stop) {
  495. change(start, stop), focus();
  496. if (ready) metric_.on("change.horizon-" + id, cubism_identity);
  497. });
  498. });
  499. }
  500. horizon.remove = function(selection) {
  501. selection
  502. .on("mousemove.horizon", null)
  503. .on("mouseout.horizon", null);
  504. selection.selectAll("canvas")
  505. .each(remove)
  506. .remove();
  507. selection.selectAll(".title,.value")
  508. .remove();
  509. function remove(d) {
  510. d.metric.on("change.horizon-" + d.id, null);
  511. context.on("change.horizon-" + d.id, null);
  512. context.on("focus.horizon-" + d.id, null);
  513. }
  514. };
  515. horizon.mode = function(_) {
  516. if (!arguments.length) return mode;
  517. mode = _ + "";
  518. return horizon;
  519. };
  520. horizon.height = function(_) {
  521. if (!arguments.length) return height;
  522. buffer.height = height = +_;
  523. return horizon;
  524. };
  525. horizon.metric = function(_) {
  526. if (!arguments.length) return metric;
  527. metric = _;
  528. return horizon;
  529. };
  530. horizon.scale = function(_) {
  531. if (!arguments.length) return scale;
  532. scale = _;
  533. return horizon;
  534. };
  535. horizon.extent = function(_) {
  536. if (!arguments.length) return extent;
  537. extent = _;
  538. return horizon;
  539. };
  540. horizon.title = function(_) {
  541. if (!arguments.length) return title;
  542. title = _;
  543. return horizon;
  544. };
  545. horizon.format = function(_) {
  546. if (!arguments.length) return format;
  547. format = _;
  548. return horizon;
  549. };
  550. horizon.colors = function(_) {
  551. if (!arguments.length) return colors;
  552. colors = _;
  553. return horizon;
  554. };
  555. return horizon;
  556. };
  557. cubism_contextPrototype.comparison = function() {
  558. var context = this,
  559. width = context.size(),
  560. height = 120,
  561. scale = d3.scale.linear().interpolate(d3.interpolateRound),
  562. primary = function(d) { return d[0]; },
  563. secondary = function(d) { return d[1]; },
  564. extent = null,
  565. title = cubism_identity,
  566. formatPrimary = cubism_comparisonPrimaryFormat,
  567. formatChange = cubism_comparisonChangeFormat,
  568. colors = ["#9ecae1", "#225b84", "#a1d99b", "#22723a"],
  569. strokeWidth = 1.5;
  570. function comparison(selection) {
  571. selection
  572. .on("mousemove.comparison", function() { context.focus(d3.mouse(this)[0]); })
  573. .on("mouseout.comparison", function() { context.focus(null); });
  574. selection.append("canvas")
  575. .attr("width", width)
  576. .attr("height", height);
  577. selection.append("span")
  578. .attr("class", "title")
  579. .text(title);
  580. selection.append("span")
  581. .attr("class", "value primary");
  582. selection.append("span")
  583. .attr("class", "value change");
  584. selection.each(function(d, i) {
  585. var that = this,
  586. id = ++cubism_id,
  587. primary_ = typeof primary === "function" ? primary.call(that, d, i) : primary,
  588. secondary_ = typeof secondary === "function" ? secondary.call(that, d, i) : secondary,
  589. extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent,
  590. div = d3.select(that),
  591. canvas = div.select("canvas"),
  592. spanPrimary = div.select(".value.primary"),
  593. spanChange = div.select(".value.change"),
  594. ready;
  595. canvas.datum({id: id, primary: primary_, secondary: secondary_});
  596. canvas = canvas.node().getContext("2d");
  597. function change(start, stop) {
  598. canvas.save();
  599. canvas.clearRect(0, 0, width, height);
  600. // update the scale
  601. var primaryExtent = primary_.extent(),
  602. secondaryExtent = secondary_.extent(),
  603. extent = extent_ == null ? primaryExtent : extent_;
  604. scale.domain(extent).range([height, 0]);
  605. ready = primaryExtent.concat(secondaryExtent).every(isFinite);
  606. // consistent overplotting
  607. var round = start / context.step() & 1
  608. ? cubism_comparisonRoundOdd
  609. : cubism_comparisonRoundEven;
  610. // positive changes
  611. canvas.fillStyle = colors[2];
  612. for (var i = 0, n = width; i < n; ++i) {
  613. var y0 = scale(primary_.valueAt(i)),
  614. y1 = scale(secondary_.valueAt(i));
  615. if (y0 < y1) canvas.fillRect(round(i), y0, 1, y1 - y0);
  616. }
  617. // negative changes
  618. canvas.fillStyle = colors[0];
  619. for (i = 0; i < n; ++i) {
  620. var y0 = scale(primary_.valueAt(i)),
  621. y1 = scale(secondary_.valueAt(i));
  622. if (y0 > y1) canvas.fillRect(round(i), y1, 1, y0 - y1);
  623. }
  624. // positive values
  625. canvas.fillStyle = colors[3];
  626. for (i = 0; i < n; ++i) {
  627. var y0 = scale(primary_.valueAt(i)),
  628. y1 = scale(secondary_.valueAt(i));
  629. if (y0 <= y1) canvas.fillRect(round(i), y0, 1, strokeWidth);
  630. }
  631. // negative values
  632. canvas.fillStyle = colors[1];
  633. for (i = 0; i < n; ++i) {
  634. var y0 = scale(primary_.valueAt(i)),
  635. y1 = scale(secondary_.valueAt(i));
  636. if (y0 > y1) canvas.fillRect(round(i), y0 - strokeWidth, 1, strokeWidth);
  637. }
  638. canvas.restore();
  639. }
  640. function focus(i) {
  641. if (i == null) i = width - 1;
  642. var valuePrimary = primary_.valueAt(i),
  643. valueSecondary = secondary_.valueAt(i),
  644. valueChange = (valuePrimary - valueSecondary) / valueSecondary;
  645. spanPrimary
  646. .datum(valuePrimary)
  647. .text(isNaN(valuePrimary) ? null : formatPrimary);
  648. spanChange
  649. .datum(valueChange)
  650. .text(isNaN(valueChange) ? null : formatChange)
  651. .attr("class", "value change " + (valueChange > 0 ? "positive" : valueChange < 0 ? "negative" : ""));
  652. }
  653. // Display the first primary change immediately,
  654. // but defer subsequent updates to the context change.
  655. // Note that someone still needs to listen to the metric,
  656. // so that it continues to update automatically.
  657. primary_.on("change.comparison-" + id, firstChange);
  658. secondary_.on("change.comparison-" + id, firstChange);
  659. function firstChange(start, stop) {
  660. change(start, stop), focus();
  661. if (ready) {
  662. primary_.on("change.comparison-" + id, cubism_identity);
  663. secondary_.on("change.comparison-" + id, cubism_identity);
  664. }
  665. }
  666. // Update the chart when the context changes.
  667. context.on("change.comparison-" + id, change);
  668. context.on("focus.comparison-" + id, focus);
  669. });
  670. }
  671. comparison.remove = function(selection) {
  672. selection
  673. .on("mousemove.comparison", null)
  674. .on("mouseout.comparison", null);
  675. selection.selectAll("canvas")
  676. .each(remove)
  677. .remove();
  678. selection.selectAll(".title,.value")
  679. .remove();
  680. function remove(d) {
  681. d.primary.on("change.comparison-" + d.id, null);
  682. d.secondary.on("change.comparison-" + d.id, null);
  683. context.on("change.comparison-" + d.id, null);
  684. context.on("focus.comparison-" + d.id, null);
  685. }
  686. };
  687. comparison.height = function(_) {
  688. if (!arguments.length) return height;
  689. height = +_;
  690. return comparison;
  691. };
  692. comparison.primary = function(_) {
  693. if (!arguments.length) return primary;
  694. primary = _;
  695. return comparison;
  696. };
  697. comparison.secondary = function(_) {
  698. if (!arguments.length) return secondary;
  699. secondary = _;
  700. return comparison;
  701. };
  702. comparison.scale = function(_) {
  703. if (!arguments.length) return scale;
  704. scale = _;
  705. return comparison;
  706. };
  707. comparison.extent = function(_) {
  708. if (!arguments.length) return extent;
  709. extent = _;
  710. return comparison;
  711. };
  712. comparison.title = function(_) {
  713. if (!arguments.length) return title;
  714. title = _;
  715. return comparison;
  716. };
  717. comparison.formatPrimary = function(_) {
  718. if (!arguments.length) return formatPrimary;
  719. formatPrimary = _;
  720. return comparison;
  721. };
  722. comparison.formatChange = function(_) {
  723. if (!arguments.length) return formatChange;
  724. formatChange = _;
  725. return comparison;
  726. };
  727. comparison.colors = function(_) {
  728. if (!arguments.length) return colors;
  729. colors = _;
  730. return comparison;
  731. };
  732. comparison.strokeWidth = function(_) {
  733. if (!arguments.length) return strokeWidth;
  734. strokeWidth = _;
  735. return comparison;
  736. };
  737. return comparison;
  738. };
  739. var cubism_comparisonPrimaryFormat = d3.format(".2s"),
  740. cubism_comparisonChangeFormat = d3.format("+.0%");
  741. function cubism_comparisonRoundEven(i) {
  742. return i & 0xfffffe;
  743. }
  744. function cubism_comparisonRoundOdd(i) {
  745. return ((i + 1) & 0xfffffe) - 1;
  746. }
  747. cubism_contextPrototype.axis = function() {
  748. var context = this,
  749. scale = context.scale,
  750. axis_ = d3.svg.axis().scale(scale);
  751. var format = context.step() < 6e4 ? cubism_axisFormatSeconds
  752. : context.step() < 864e5 ? cubism_axisFormatMinutes
  753. : cubism_axisFormatDays;
  754. function axis(selection) {
  755. var id = ++cubism_id,
  756. tick;
  757. var g = selection.append("svg")
  758. .datum({id: id})
  759. .attr("width", context.size())
  760. .attr("height", Math.max(28, -axis.tickSize()))
  761. .append("g")
  762. .attr("transform", "translate(0," + (axis_.orient() === "top" ? 27 : 4) + ")")
  763. .call(axis_);
  764. context.on("change.axis-" + id, function() {
  765. g.call(axis_);
  766. if (!tick) tick = d3.select(g.node().appendChild(g.selectAll("text").node().cloneNode(true)))
  767. .style("display", "none")
  768. .text(null);
  769. });
  770. context.on("focus.axis-" + id, function(i) {
  771. if (tick) {
  772. if (i == null) {
  773. tick.style("display", "none");
  774. g.selectAll("text").style("fill-opacity", null);
  775. } else {
  776. tick.style("display", null).attr("x", i).text(format(scale.invert(i)));
  777. var dx = tick.node().getComputedTextLength() + 6;
  778. g.selectAll("text").style("fill-opacity", function(d) { return Math.abs(scale(d) - i) < dx ? 0 : 1; });
  779. }
  780. }
  781. });
  782. }
  783. axis.remove = function(selection) {
  784. selection.selectAll("svg")
  785. .each(remove)
  786. .remove();
  787. function remove(d) {
  788. context.on("change.axis-" + d.id, null);
  789. context.on("focus.axis-" + d.id, null);
  790. }
  791. };
  792. return d3.rebind(axis, axis_,
  793. "orient",
  794. "ticks",
  795. "tickSubdivide",
  796. "tickSize",
  797. "tickPadding",
  798. "tickFormat");
  799. };
  800. var cubism_axisFormatSeconds = d3.time.format("%I:%M:%S %p"),
  801. cubism_axisFormatMinutes = d3.time.format("%I:%M %p"),
  802. cubism_axisFormatDays = d3.time.format("%B %d");
  803. cubism_contextPrototype.rule = function() {
  804. var context = this;
  805. function rule(selection) {
  806. var id = ++cubism_id;
  807. var line = selection.append("div")
  808. .datum({id: id})
  809. .attr("class", "line")
  810. .style("position", "fixed")
  811. .style("top", 0)
  812. .style("right", 0)
  813. .style("bottom", 0)
  814. .style("width", "1px")
  815. .style("pointer-events", "none");
  816. context.on("focus.rule-" + id, function(i) {
  817. line
  818. .style("display", i == null ? "none" : null)
  819. .style("left", function() { return this.parentNode.getBoundingClientRect().left + i + "px"; });
  820. });
  821. }
  822. rule.remove = function(selection) {
  823. selection.selectAll(".line")
  824. .each(remove)
  825. .remove();
  826. function remove(d) {
  827. context.on("focus.rule-" + d.id, null);
  828. }
  829. };
  830. return rule;
  831. };
  832. })(this);