cubism.v1.js 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085
  1. (function (exports) {
  2. var cubism = exports.cubism = {version:"1.2.0"};
  3. var cubism_id = 0;
  4. function cubism_identity(d) {
  5. return d;
  6. }
  7. cubism.option = function (name, defaultValue) {
  8. var values = cubism.options(name);
  9. return values.length ? values[0] : defaultValue;
  10. };
  11. cubism.options = function (name, defaultValues) {
  12. var options = location.search.substring(1).split("&"),
  13. values = [],
  14. i = -1,
  15. n = options.length,
  16. o;
  17. while (++i < n) {
  18. if ((o = options[i].split("="))[0] == name) {
  19. values.push(decodeURIComponent(o[1]));
  20. }
  21. }
  22. return values.length || arguments.length < 2 ? values : defaultValues;
  23. };
  24. cubism.context = function () {
  25. var context = new cubism_context,
  26. step = 1e4, // ten seconds, in milliseconds
  27. size = 1440, // four hours at ten seconds, in pixels
  28. start0, stop0, // the start and stop for the previous change event
  29. start1, stop1, // the start and stop for the next prepare event
  30. serverDelay = 5e3,
  31. clientDelay = 5e3,
  32. event = d3.dispatch("prepare", "beforechange", "change", "focus"),
  33. scale = context.scale = d3.time.scale().range([0, size]),
  34. timeout,
  35. focus;
  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. context.start = function () {
  46. if (timeout) clearTimeout(timeout);
  47. var delay = +stop1 + serverDelay - Date.now();
  48. // If we're too late for the first prepare event, skip it.
  49. if (delay < clientDelay) delay += step;
  50. timeout = setTimeout(function prepare() {
  51. stop1 = new Date(Math.floor((Date.now() - serverDelay) / step) * step);
  52. start1 = new Date(stop1 - size * step);
  53. event.prepare.call(context, start1, stop1);
  54. setTimeout(function () {
  55. scale.domain([start0 = start1, stop0 = stop1]);
  56. event.beforechange.call(context, start1, stop1);
  57. event.change.call(context, start1, stop1);
  58. event.focus.call(context, focus);
  59. }, clientDelay);
  60. timeout = setTimeout(prepare, step);
  61. }, delay);
  62. return context;
  63. };
  64. context.stop = function () {
  65. timeout = clearTimeout(timeout);
  66. return context;
  67. };
  68. timeout = setTimeout(context.start, 10);
  69. // Set or get the step interval in milliseconds.
  70. // Defaults to ten seconds.
  71. context.step = function (_) {
  72. if (!arguments.length) return step;
  73. step = +_;
  74. return update();
  75. };
  76. // Set or get the context size (the count of metric values).
  77. // Defaults to 1440 (four hours at ten seconds).
  78. context.size = function (_) {
  79. if (!arguments.length) return size;
  80. scale.range([0, size = +_]);
  81. return update();
  82. };
  83. // The server delay is the amount of time we wait for the server to compute a
  84. // metric. This delay may result from clock skew or from delays collecting
  85. // metrics from various hosts. Defaults to 4 seconds.
  86. context.serverDelay = function (_) {
  87. if (!arguments.length) return serverDelay;
  88. serverDelay = +_;
  89. return update();
  90. };
  91. // The client delay is the amount of additional time we wait to fetch those
  92. // metrics from the server. The client and server delay combined represent the
  93. // age of the most recent displayed metric. Defaults to 1 second.
  94. context.clientDelay = function (_) {
  95. if (!arguments.length) return clientDelay;
  96. clientDelay = +_;
  97. return update();
  98. };
  99. // Sets the focus to the specified index, and dispatches a "focus" event.
  100. context.focus = function (i) {
  101. event.focus.call(context, focus = i);
  102. return context;
  103. };
  104. // Add, remove or get listeners for events.
  105. context.on = function (type, listener) {
  106. if (arguments.length < 2) return event.on(type);
  107. event.on(type, listener);
  108. // Notify the listener of the current start and stop time, as appropriate.
  109. // This way, metrics can make requests for data immediately,
  110. // and likewise the axis can display itself synchronously.
  111. if (listener != null) {
  112. if (/^prepare(\.|$)/.test(type)) listener.call(context, start1, stop1);
  113. if (/^beforechange(\.|$)/.test(type)) listener.call(context, start0, stop0);
  114. if (/^change(\.|$)/.test(type)) listener.call(context, start0, stop0);
  115. if (/^focus(\.|$)/.test(type)) listener.call(context, focus);
  116. }
  117. return context;
  118. };
  119. d3.select(window).on("keydown.context-" + ++cubism_id, function () {
  120. switch (!d3.event.metaKey && d3.event.keyCode) {
  121. case 37: // left
  122. if (focus == null) focus = size - 1;
  123. if (focus > 0) context.focus(--focus);
  124. break;
  125. case 39: // right
  126. if (focus == null) focus = size - 2;
  127. if (focus < size - 1) context.focus(++focus);
  128. break;
  129. default:
  130. return;
  131. }
  132. d3.event.preventDefault();
  133. });
  134. return update();
  135. };
  136. function cubism_context() {
  137. }
  138. var cubism_contextPrototype = cubism.context.prototype = cubism_context.prototype;
  139. cubism_contextPrototype.constant = function (value) {
  140. return new cubism_metricConstant(this, +value);
  141. };
  142. cubism_contextPrototype.cube = function (host) {
  143. if (!arguments.length) host = "";
  144. var source = {},
  145. context = this;
  146. source.metric = function (expression) {
  147. return context.metric(function (start, stop, step, callback) {
  148. d3.json(host + "/1.0/metric"
  149. + "?expression=" + encodeURIComponent(expression)
  150. + "&start=" + cubism_cubeFormatDate(start)
  151. + "&stop=" + cubism_cubeFormatDate(stop)
  152. + "&step=" + step, function (data) {
  153. if (!data) return callback(new Error("unable to load data"));
  154. callback(null, data.map(function (d) {
  155. return d.value;
  156. }));
  157. });
  158. }, expression += "");
  159. };
  160. // Returns the Cube host.
  161. source.toString = function () {
  162. return host;
  163. };
  164. return source;
  165. };
  166. var cubism_cubeFormatDate = d3.time.format.iso;
  167. cubism_contextPrototype.graphite = function (host) {
  168. if (!arguments.length) host = "";
  169. var source = {},
  170. context = this;
  171. source.metric = function (expression) {
  172. var sum = "sum";
  173. var metric = context.metric(function (start, stop, step, callback) {
  174. var target = expression;
  175. // Apply the summarize, if necessary.
  176. if (step !== 1e4) target = "summarize(" + target + ",'"
  177. + (!(step % 36e5) ? step / 36e5 + "hour" : !(step % 6e4) ? step / 6e4 + "min" : step + "sec")
  178. + "','" + sum + "')";
  179. d3.text(host + "/render?format=raw"
  180. + "&target=" + encodeURIComponent("alias(" + target + ",'')")
  181. + "&from=" + cubism_graphiteFormatDate(start - 2 * step) // off-by-two?
  182. + "&until=" + cubism_graphiteFormatDate(stop - 1000), function (text) {
  183. if (!text) return callback(new Error("unable to load data"));
  184. callback(null, cubism_graphiteParse(text));
  185. });
  186. }, expression += "");
  187. metric.summarize = function (_) {
  188. sum = _;
  189. return metric;
  190. };
  191. return metric;
  192. };
  193. source.find = function (pattern, callback) {
  194. d3.json(host + "/metrics/find?format=completer"
  195. + "&query=" + encodeURIComponent(pattern), function (result) {
  196. if (!result) return callback(new Error("unable to find metrics"));
  197. callback(null, result.metrics.map(function (d) {
  198. return d.path;
  199. }));
  200. });
  201. };
  202. // Returns the graphite host.
  203. source.toString = function () {
  204. return host;
  205. };
  206. return source;
  207. };
  208. // Graphite understands seconds since UNIX epoch.
  209. function cubism_graphiteFormatDate(time) {
  210. return Math.floor(time / 1000);
  211. }
  212. // Helper method for parsing graphite's raw format.
  213. function cubism_graphiteParse(text) {
  214. var i = text.indexOf("|"),
  215. meta = text.substring(0, i),
  216. c = meta.lastIndexOf(","),
  217. b = meta.lastIndexOf(",", c - 1),
  218. a = meta.lastIndexOf(",", b - 1),
  219. start = meta.substring(a + 1, b) * 1000,
  220. step = meta.substring(c + 1) * 1000;
  221. return text
  222. .substring(i + 1)
  223. .split(",")
  224. .slice(1)// the first value is always None?
  225. .map(function (d) {
  226. return +d;
  227. });
  228. }
  229. function cubism_metric(context) {
  230. if (!(context instanceof cubism_context)) throw new Error("invalid context");
  231. this.context = context;
  232. }
  233. var cubism_metricPrototype = cubism_metric.prototype;
  234. cubism.metric = cubism_metric;
  235. cubism_metricPrototype.valueAt = function () {
  236. return NaN;
  237. };
  238. cubism_metricPrototype.alias = function (name) {
  239. this.toString = function () {
  240. return name;
  241. };
  242. return this;
  243. };
  244. cubism_metricPrototype.extent = function () {
  245. var i = 0,
  246. n = this.context.size(),
  247. value,
  248. min = Infinity,
  249. max = -Infinity;
  250. while (++i < n) {
  251. value = this.valueAt(i);
  252. if (value < min) min = value;
  253. if (value > max) max = value;
  254. }
  255. return [min, max];
  256. };
  257. cubism_metricPrototype.on = function (type, listener) {
  258. return arguments.length < 2 ? null : this;
  259. };
  260. cubism_metricPrototype.shift = function () {
  261. return this;
  262. };
  263. cubism_metricPrototype.on = function () {
  264. return arguments.length < 2 ? null : this;
  265. };
  266. cubism_contextPrototype.metric = function (request, name) {
  267. var context = this,
  268. metric = new cubism_metric(context),
  269. id = ".metric-" + ++cubism_id,
  270. start = -Infinity,
  271. stop,
  272. step = context.step(),
  273. size = context.size(),
  274. values = [],
  275. event = d3.dispatch("change"),
  276. listening = 0,
  277. fetching;
  278. // Prefetch new data into a temporary array.
  279. function prepare(start1, stop) {
  280. var steps = Math.min(size, Math.round((start1 - start) / step));
  281. if (!steps || fetching) return; // already fetched, or fetching!
  282. fetching = true;
  283. steps = Math.min(size, steps + cubism_metricOverlap);
  284. var start0 = new Date(stop - steps * step);
  285. request(start0, stop, step, function (error, data) {
  286. fetching = false;
  287. if (error) return console.warn(error);
  288. var i = isFinite(start) ? Math.round((start0 - start) / step) : 0;
  289. for (var j = 0, m = data.length; j < m; ++j) values[j + i] = data[j];
  290. event.change.call(metric, start, stop);
  291. });
  292. }
  293. // When the context changes, switch to the new data, ready-or-not!
  294. function beforechange(start1, stop1) {
  295. if (!isFinite(start)) start = start1;
  296. values.splice(0, Math.max(0, Math.min(size, Math.round((start1 - start) / step))));
  297. start = start1;
  298. stop = stop1;
  299. }
  300. //
  301. metric.valueAt = function (i) {
  302. return values[i];
  303. };
  304. //
  305. metric.shift = function (offset) {
  306. return context.metric(cubism_metricShift(request, +offset));
  307. };
  308. //
  309. metric.on = function (type, listener) {
  310. if (!arguments.length) return event.on(type);
  311. // If there are no listeners, then stop listening to the context,
  312. // and avoid unnecessary fetches.
  313. if (listener == null) {
  314. if (event.on(type) != null && --listening == 0) {
  315. context.on("prepare" + id, null).on("beforechange" + id, null);
  316. }
  317. } else {
  318. if (event.on(type) == null && ++listening == 1) {
  319. context.on("prepare" + id, prepare).on("beforechange" + id, beforechange);
  320. }
  321. }
  322. event.on(type, listener);
  323. // Notify the listener of the current start and stop time, as appropriate.
  324. // This way, charts can display synchronous metrics immediately.
  325. if (listener != null) {
  326. if (/^change(\.|$)/.test(type)) listener.call(context, start, stop);
  327. }
  328. return metric;
  329. };
  330. //
  331. if (arguments.length > 1) metric.toString = function () {
  332. return name;
  333. };
  334. return metric;
  335. };
  336. // Number of metric to refetch each period, in case of lag.
  337. var cubism_metricOverlap = 6;
  338. // Wraps the specified request implementation, and shifts time by the given offset.
  339. function cubism_metricShift(request, offset) {
  340. return function (start, stop, step, callback) {
  341. request(new Date(+start + offset), new Date(+stop + offset), step, callback);
  342. };
  343. }
  344. function cubism_metricConstant(context, value) {
  345. cubism_metric.call(this, context);
  346. value = +value;
  347. var name = value + "";
  348. this.valueOf = function () {
  349. return value;
  350. };
  351. this.toString = function () {
  352. return name;
  353. };
  354. }
  355. var cubism_metricConstantPrototype = cubism_metricConstant.prototype = Object.create(cubism_metric.prototype);
  356. cubism_metricConstantPrototype.valueAt = function () {
  357. return +this;
  358. };
  359. cubism_metricConstantPrototype.extent = function () {
  360. return [+this, +this];
  361. };
  362. function cubism_metricOperator(name, operate) {
  363. function cubism_metricOperator(left, right) {
  364. if (!(right instanceof cubism_metric)) right = new cubism_metricConstant(left.context, right);
  365. else if (left.context !== right.context) throw new Error("mismatch context");
  366. cubism_metric.call(this, left.context);
  367. this.left = left;
  368. this.right = right;
  369. this.toString = function () {
  370. return left + " " + name + " " + right;
  371. };
  372. }
  373. var cubism_metricOperatorPrototype = cubism_metricOperator.prototype = Object.create(cubism_metric.prototype);
  374. cubism_metricOperatorPrototype.valueAt = function (i) {
  375. return operate(this.left.valueAt(i), this.right.valueAt(i));
  376. };
  377. cubism_metricOperatorPrototype.shift = function (offset) {
  378. return new cubism_metricOperator(this.left.shift(offset), this.right.shift(offset));
  379. };
  380. cubism_metricOperatorPrototype.on = function (type, listener) {
  381. if (arguments.length < 2) return this.left.on(type);
  382. this.left.on(type, listener);
  383. this.right.on(type, listener);
  384. return this;
  385. };
  386. return function (right) {
  387. return new cubism_metricOperator(this, right);
  388. };
  389. }
  390. cubism_metricPrototype.add = cubism_metricOperator("+", function (left, right) {
  391. return left + right;
  392. });
  393. cubism_metricPrototype.subtract = cubism_metricOperator("-", function (left, right) {
  394. return left - right;
  395. });
  396. cubism_metricPrototype.multiply = cubism_metricOperator("*", function (left, right) {
  397. return left * right;
  398. });
  399. cubism_metricPrototype.divide = cubism_metricOperator("/", function (left, right) {
  400. return left / right;
  401. });
  402. cubism_contextPrototype.horizon = function () {
  403. var context = this,
  404. mode = "offset",
  405. buffer = document.createElement("canvas"),
  406. width = buffer.width = context.size(),
  407. height = buffer.height = 30,
  408. scale = d3.scale.linear().interpolate(d3.interpolateRound),
  409. metric = cubism_identity,
  410. extent = null,
  411. title = cubism_identity,
  412. format = d3.format(".2s"),
  413. colors = ["#08519c", "#3182bd", "#6baed6", "#bdd7e7", "#bae4b3", "#74c476", "#31a354", "#006d2c"];
  414. function horizon(selection) {
  415. selection
  416. .on("mousemove.horizon", function () {
  417. context.focus(d3.mouse(this)[0]);
  418. })
  419. .on("mouseout.horizon", function () {
  420. context.focus(null);
  421. });
  422. selection.append("canvas")
  423. .attr("width", width)
  424. .attr("height", height);
  425. selection.append("span")
  426. .attr("class", "title")
  427. .text(title);
  428. selection.append("span")
  429. .attr("class", "value");
  430. selection.each(function (d, i) {
  431. var that = this,
  432. id = ++cubism_id,
  433. metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric,
  434. colors_ = typeof colors === "function" ? colors.call(that, d, i) : colors,
  435. extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent,
  436. start = -Infinity,
  437. step = context.step(),
  438. canvas = d3.select(that).select("canvas"),
  439. span = d3.select(that).select(".value"),
  440. max_,
  441. m = colors_.length >> 1,
  442. ready;
  443. canvas.datum({id:id, metric:metric_});
  444. canvas = canvas.node().getContext("2d");
  445. function change(start1, stop) {
  446. canvas.save();
  447. // compute the new extent and ready flag
  448. var extent = metric_.extent();
  449. ready = extent.every(isFinite);
  450. if (extent_ != null) extent = extent_;
  451. // if this is an update (with no extent change), copy old values!
  452. var i0 = 0, max = Math.max(-extent[0], extent[1]);
  453. if (this === context) {
  454. if (max == max_) {
  455. i0 = width - cubism_metricOverlap;
  456. var dx = (start1 - start) / step;
  457. if (dx < width) {
  458. var canvas0 = buffer.getContext("2d");
  459. canvas0.clearRect(0, 0, width, height);
  460. canvas0.drawImage(canvas.canvas, dx, 0, width - dx, height, 0, 0, width - dx, height);
  461. canvas.clearRect(0, 0, width, height);
  462. canvas.drawImage(canvas0.canvas, 0, 0);
  463. }
  464. }
  465. start = start1;
  466. }
  467. // update the domain
  468. scale.domain([0, max_ = max]);
  469. // clear for the new data
  470. canvas.clearRect(i0, 0, width - i0, height);
  471. // record whether there are negative values to display
  472. var negative;
  473. // positive bands
  474. for (var j = 0; j < m; ++j) {
  475. canvas.fillStyle = colors_[m + j];
  476. // Adjust the range based on the current band index.
  477. var y0 = (j - m + 1) * height;
  478. scale.range([m * height + y0, y0]);
  479. y0 = scale(0);
  480. for (var i = i0, n = width, y1; i < n; ++i) {
  481. y1 = metric_.valueAt(i);
  482. if (y1 <= 0) {
  483. negative = true;
  484. continue;
  485. }
  486. canvas.fillRect(i, y1 = scale(y1), 1, y0 - y1);
  487. }
  488. }
  489. if (negative) {
  490. // enable offset mode
  491. if (mode === "offset") {
  492. canvas.translate(0, height);
  493. canvas.scale(1, -1);
  494. }
  495. // negative bands
  496. for (var j = 0; j < m; ++j) {
  497. canvas.fillStyle = colors_[m - 1 - j];
  498. // Adjust the range based on the current band index.
  499. var y0 = (j - m + 1) * height;
  500. scale.range([m * height + y0, y0]);
  501. y0 = scale(0);
  502. for (var i = i0, n = width, y1; i < n; ++i) {
  503. y1 = metric_.valueAt(i);
  504. if (y1 >= 0) continue;
  505. canvas.fillRect(i, scale(-y1), 1, y0 - scale(-y1));
  506. }
  507. }
  508. }
  509. canvas.restore();
  510. }
  511. function focus(i) {
  512. if (i == null) i = width - 1;
  513. var value = metric_.valueAt(i);
  514. span.datum(value).text(isNaN(value) ? null : format);
  515. }
  516. // Update the chart when the context changes.
  517. context.on("change.horizon-" + id, change);
  518. context.on("focus.horizon-" + id, focus);
  519. // Display the first metric change immediately,
  520. // but defer subsequent updates to the canvas change.
  521. // Note that someone still needs to listen to the metric,
  522. // so that it continues to update automatically.
  523. metric_.on("change.horizon-" + id, function (start, stop) {
  524. change(start, stop), focus();
  525. if (ready) metric_.on("change.horizon-" + id, cubism_identity);
  526. });
  527. });
  528. }
  529. horizon.remove = function (selection) {
  530. selection
  531. .on("mousemove.horizon", null)
  532. .on("mouseout.horizon", null);
  533. selection.selectAll("canvas")
  534. .each(remove)
  535. .remove();
  536. selection.selectAll(".title,.value")
  537. .remove();
  538. function remove(d) {
  539. d.metric.on("change.horizon-" + d.id, null);
  540. context.on("change.horizon-" + d.id, null);
  541. context.on("focus.horizon-" + d.id, null);
  542. }
  543. };
  544. horizon.mode = function (_) {
  545. if (!arguments.length) return mode;
  546. mode = _ + "";
  547. return horizon;
  548. };
  549. horizon.height = function (_) {
  550. if (!arguments.length) return height;
  551. buffer.height = height = +_;
  552. return horizon;
  553. };
  554. horizon.metric = function (_) {
  555. if (!arguments.length) return metric;
  556. metric = _;
  557. return horizon;
  558. };
  559. horizon.scale = function (_) {
  560. if (!arguments.length) return scale;
  561. scale = _;
  562. return horizon;
  563. };
  564. horizon.extent = function (_) {
  565. if (!arguments.length) return extent;
  566. extent = _;
  567. return horizon;
  568. };
  569. horizon.title = function (_) {
  570. if (!arguments.length) return title;
  571. title = _;
  572. return horizon;
  573. };
  574. horizon.format = function (_) {
  575. if (!arguments.length) return format;
  576. format = _;
  577. return horizon;
  578. };
  579. horizon.colors = function (_) {
  580. if (!arguments.length) return colors;
  581. colors = _;
  582. return horizon;
  583. };
  584. return horizon;
  585. };
  586. cubism_contextPrototype.comparison = function () {
  587. var context = this,
  588. width = context.size(),
  589. height = 120,
  590. scale = d3.scale.linear().interpolate(d3.interpolateRound),
  591. primary = function (d) {
  592. return d[0];
  593. },
  594. secondary = function (d) {
  595. return d[1];
  596. },
  597. extent = null,
  598. title = cubism_identity,
  599. formatPrimary = cubism_comparisonPrimaryFormat,
  600. formatChange = cubism_comparisonChangeFormat,
  601. colors = ["#9ecae1", "#225b84", "#a1d99b", "#22723a"],
  602. strokeWidth = 1.5;
  603. function comparison(selection) {
  604. selection
  605. .on("mousemove.comparison", function () {
  606. context.focus(d3.mouse(this)[0]);
  607. })
  608. .on("mouseout.comparison", function () {
  609. context.focus(null);
  610. });
  611. selection.append("canvas")
  612. .attr("width", width)
  613. .attr("height", height);
  614. selection.append("span")
  615. .attr("class", "title")
  616. .text(title);
  617. selection.append("span")
  618. .attr("class", "value primary");
  619. selection.append("span")
  620. .attr("class", "value change");
  621. selection.each(function (d, i) {
  622. var that = this,
  623. id = ++cubism_id,
  624. primary_ = typeof primary === "function" ? primary.call(that, d, i) : primary,
  625. secondary_ = typeof secondary === "function" ? secondary.call(that, d, i) : secondary,
  626. extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent,
  627. div = d3.select(that),
  628. canvas = div.select("canvas"),
  629. spanPrimary = div.select(".value.primary"),
  630. spanChange = div.select(".value.change"),
  631. ready;
  632. canvas.datum({id:id, primary:primary_, secondary:secondary_});
  633. canvas = canvas.node().getContext("2d");
  634. function change(start, stop) {
  635. canvas.save();
  636. canvas.clearRect(0, 0, width, height);
  637. // update the scale
  638. var primaryExtent = primary_.extent(),
  639. secondaryExtent = secondary_.extent(),
  640. extent = extent_ == null ? primaryExtent : extent_;
  641. scale.domain(extent).range([height, 0]);
  642. ready = primaryExtent.concat(secondaryExtent).every(isFinite);
  643. // consistent overplotting
  644. var round = start / context.step() & 1
  645. ? cubism_comparisonRoundOdd
  646. : cubism_comparisonRoundEven;
  647. // positive changes
  648. canvas.fillStyle = colors[2];
  649. for (var i = 0, n = width; i < n; ++i) {
  650. var y0 = scale(primary_.valueAt(i)),
  651. y1 = scale(secondary_.valueAt(i));
  652. if (y0 < y1) canvas.fillRect(round(i), y0, 1, y1 - y0);
  653. }
  654. // negative changes
  655. canvas.fillStyle = colors[0];
  656. for (i = 0; i < n; ++i) {
  657. var y0 = scale(primary_.valueAt(i)),
  658. y1 = scale(secondary_.valueAt(i));
  659. if (y0 > y1) canvas.fillRect(round(i), y1, 1, y0 - y1);
  660. }
  661. // positive values
  662. canvas.fillStyle = colors[3];
  663. for (i = 0; i < n; ++i) {
  664. var y0 = scale(primary_.valueAt(i)),
  665. y1 = scale(secondary_.valueAt(i));
  666. if (y0 <= y1) canvas.fillRect(round(i), y0, 1, strokeWidth);
  667. }
  668. // negative values
  669. canvas.fillStyle = colors[1];
  670. for (i = 0; i < n; ++i) {
  671. var y0 = scale(primary_.valueAt(i)),
  672. y1 = scale(secondary_.valueAt(i));
  673. if (y0 > y1) canvas.fillRect(round(i), y0 - strokeWidth, 1, strokeWidth);
  674. }
  675. canvas.restore();
  676. }
  677. function focus(i) {
  678. if (i == null) i = width - 1;
  679. var valuePrimary = primary_.valueAt(i),
  680. valueSecondary = secondary_.valueAt(i),
  681. valueChange = (valuePrimary - valueSecondary) / valueSecondary;
  682. spanPrimary
  683. .datum(valuePrimary)
  684. .text(isNaN(valuePrimary) ? null : formatPrimary);
  685. spanChange
  686. .datum(valueChange)
  687. .text(isNaN(valueChange) ? null : formatChange)
  688. .attr("class", "value change " + (valueChange > 0 ? "positive" : valueChange < 0 ? "negative" : ""));
  689. }
  690. // Display the first primary change immediately,
  691. // but defer subsequent updates to the context change.
  692. // Note that someone still needs to listen to the metric,
  693. // so that it continues to update automatically.
  694. primary_.on("change.comparison-" + id, firstChange);
  695. secondary_.on("change.comparison-" + id, firstChange);
  696. function firstChange(start, stop) {
  697. change(start, stop), focus();
  698. if (ready) {
  699. primary_.on("change.comparison-" + id, cubism_identity);
  700. secondary_.on("change.comparison-" + id, cubism_identity);
  701. }
  702. }
  703. // Update the chart when the context changes.
  704. context.on("change.comparison-" + id, change);
  705. context.on("focus.comparison-" + id, focus);
  706. });
  707. }
  708. comparison.remove = function (selection) {
  709. selection
  710. .on("mousemove.comparison", null)
  711. .on("mouseout.comparison", null);
  712. selection.selectAll("canvas")
  713. .each(remove)
  714. .remove();
  715. selection.selectAll(".title,.value")
  716. .remove();
  717. function remove(d) {
  718. d.primary.on("change.comparison-" + d.id, null);
  719. d.secondary.on("change.comparison-" + d.id, null);
  720. context.on("change.comparison-" + d.id, null);
  721. context.on("focus.comparison-" + d.id, null);
  722. }
  723. };
  724. comparison.height = function (_) {
  725. if (!arguments.length) return height;
  726. height = +_;
  727. return comparison;
  728. };
  729. comparison.primary = function (_) {
  730. if (!arguments.length) return primary;
  731. primary = _;
  732. return comparison;
  733. };
  734. comparison.secondary = function (_) {
  735. if (!arguments.length) return secondary;
  736. secondary = _;
  737. return comparison;
  738. };
  739. comparison.scale = function (_) {
  740. if (!arguments.length) return scale;
  741. scale = _;
  742. return comparison;
  743. };
  744. comparison.extent = function (_) {
  745. if (!arguments.length) return extent;
  746. extent = _;
  747. return comparison;
  748. };
  749. comparison.title = function (_) {
  750. if (!arguments.length) return title;
  751. title = _;
  752. return comparison;
  753. };
  754. comparison.formatPrimary = function (_) {
  755. if (!arguments.length) return formatPrimary;
  756. formatPrimary = _;
  757. return comparison;
  758. };
  759. comparison.formatChange = function (_) {
  760. if (!arguments.length) return formatChange;
  761. formatChange = _;
  762. return comparison;
  763. };
  764. comparison.colors = function (_) {
  765. if (!arguments.length) return colors;
  766. colors = _;
  767. return comparison;
  768. };
  769. comparison.strokeWidth = function (_) {
  770. if (!arguments.length) return strokeWidth;
  771. strokeWidth = _;
  772. return comparison;
  773. };
  774. return comparison;
  775. };
  776. var cubism_comparisonPrimaryFormat = d3.format(".2s"),
  777. cubism_comparisonChangeFormat = d3.format("+.0%");
  778. function cubism_comparisonRoundEven(i) {
  779. return i & 0xfffffe;
  780. }
  781. function cubism_comparisonRoundOdd(i) {
  782. return ((i + 1) & 0xfffffe) - 1;
  783. }
  784. cubism_contextPrototype.axis = function () {
  785. var context = this,
  786. scale = context.scale,
  787. axis_ = d3.svg.axis().scale(scale);
  788. var format = context.step() < 6e4 ? cubism_axisFormatSeconds
  789. : context.step() < 864e5 ? cubism_axisFormatMinutes
  790. : cubism_axisFormatDays;
  791. function axis(selection) {
  792. var id = ++cubism_id,
  793. tick;
  794. var g = selection.append("svg")
  795. .datum({id:id})
  796. .attr("width", context.size())
  797. .attr("height", Math.max(28, -axis.tickSize()))
  798. .append("g")
  799. .attr("transform", "translate(0," + (axis_.orient() === "top" ? 27 : 4) + ")")
  800. .call(axis_);
  801. context.on("change.axis-" + id, function () {
  802. g.call(axis_);
  803. if (!tick) tick = d3.select(g.node().appendChild(g.selectAll("text").node().cloneNode(true)))
  804. .style("display", "none")
  805. .text(null);
  806. });
  807. context.on("focus.axis-" + id, function (i) {
  808. if (tick) {
  809. if (i == null) {
  810. tick.style("display", "none");
  811. g.selectAll("text").style("fill-opacity", null);
  812. } else {
  813. tick.style("display", null).attr("x", i).text(format(scale.invert(i)));
  814. var dx = tick.node().getComputedTextLength() + 6;
  815. g.selectAll("text").style("fill-opacity", function (d) {
  816. return Math.abs(scale(d) - i) < dx ? 0 : 1;
  817. });
  818. }
  819. }
  820. });
  821. }
  822. axis.remove = function (selection) {
  823. selection.selectAll("svg")
  824. .each(remove)
  825. .remove();
  826. function remove(d) {
  827. context.on("change.axis-" + d.id, null);
  828. context.on("focus.axis-" + d.id, null);
  829. }
  830. };
  831. return d3.rebind(axis, axis_,
  832. "orient",
  833. "ticks",
  834. "tickSubdivide",
  835. "tickSize",
  836. "tickPadding",
  837. "tickFormat");
  838. };
  839. var cubism_axisFormatSeconds = d3.time.format("%I:%M:%S %p"),
  840. cubism_axisFormatMinutes = d3.time.format("%I:%M %p"),
  841. cubism_axisFormatDays = d3.time.format("%B %d");
  842. cubism_contextPrototype.rule = function () {
  843. var context = this,
  844. metric = cubism_identity;
  845. function rule(selection) {
  846. var id = ++cubism_id;
  847. var line = selection.append("div")
  848. .datum({id:id})
  849. .attr("class", "line")
  850. .call(cubism_ruleStyle);
  851. selection.each(function (d, i) {
  852. var that = this,
  853. id = ++cubism_id,
  854. metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric;
  855. if (!metric_) return;
  856. function change(start, stop) {
  857. var values = [];
  858. for (var i = 0, n = context.size(); i < n; ++i) {
  859. if (metric_.valueAt(i)) {
  860. values.push(i);
  861. }
  862. }
  863. var lines = selection.selectAll(".metric").data(values);
  864. lines.exit().remove();
  865. lines.enter().append("div").attr("class", "metric line").call(cubism_ruleStyle);
  866. lines.style("left", cubism_ruleLeft);
  867. }
  868. context.on("change.rule-" + id, change);
  869. metric_.on("change.rule-" + id, change);
  870. });
  871. context.on("focus.rule-" + id, function (i) {
  872. line.datum(i)
  873. .style("display", i == null ? "none" : null)
  874. .style("left", cubism_ruleLeft);
  875. });
  876. }
  877. rule.remove = function (selection) {
  878. selection.selectAll(".line")
  879. .each(remove)
  880. .remove();
  881. function remove(d) {
  882. context.on("focus.rule-" + d.id, null);
  883. }
  884. };
  885. rule.metric = function (_) {
  886. if (!arguments.length) return metric;
  887. metric = _;
  888. return rule;
  889. };
  890. return rule;
  891. };
  892. function cubism_ruleStyle(line) {
  893. line
  894. .style("position", "absolute")
  895. .style("top", 0)
  896. .style("bottom", 0)
  897. .style("width", "1px")
  898. .style("pointer-events", "none");
  899. }
  900. function cubism_ruleLeft(i) {
  901. return i + "px";
  902. }
  903. })(this);