PageRenderTime 35ms CodeModel.GetById 5ms RepoModel.GetById 1ms app.codeStats 0ms

/slippy-chart.js

https://bitbucket.org/kelvin/slippy-chart
JavaScript | 3341 lines | 2458 code | 532 blank | 351 comment | 309 complexity | 15df4ed905aea099527e72a5ccd3b5b0 MD5 | raw file
  1. /*!
  2. * Slippy Chart Library v0.1
  3. * http://www.scaledlabs.com
  4. *
  5. * Copyright (c) 2009 Kelvin Kakugawa
  6. *
  7. */
  8. /*jslint browser: true, debug: true, rhino: true*/
  9. /*extern jQuery*/
  10. (function($) {
  11. var
  12. window = this,
  13. document = window.document,
  14. undefined,
  15. _SlippyChart = window.SlippyChart,
  16. _$c = window.$c,
  17. $c = window.$c = SlippyChart = window.SlippyChart = {};
  18. // ========== Compatibility ==========
  19. // ---------- Event Handling ----------
  20. var addEvent = function(el, type, fn) {
  21. if (window.attachEvent) {
  22. addEvent = function(el, type, fn) {
  23. el.attachEvent("on"+type, fn);
  24. };
  25. } else {
  26. addEvent = function(el, type, fn) {
  27. if (type =="mousewheel") {
  28. type = "DOMMouseScroll";
  29. }
  30. el.addEventListener(type, fn, false);
  31. };
  32. }
  33. addEvent(el, type, fn);
  34. };
  35. ;;; $c.addEvent = addEvent;
  36. var removeEvent = function(el, type, fn) {
  37. if (window.detachEvent) {
  38. removeEvent = function(el, type, fn) {
  39. el.detachEvent("on"+type, fn);
  40. };
  41. } else {
  42. removeEvent = function(el, type, fn) {
  43. el.removeEventListener(type, fn, false);
  44. };
  45. }
  46. removeEvent(el, type, fn);
  47. };
  48. ;;; $c.removeEvent = removeEvent;
  49. var cancelEvent = function(e) {
  50. e = e ? e : window.event;
  51. if (e.stopPropagation) {
  52. e.stopPropagation();
  53. }
  54. if (e.preventDefault) {
  55. e.preventDefault();
  56. }
  57. e["cancelBubble"] = true;
  58. e["cancel"] = true;
  59. e["returnValue"] = false;
  60. return false; // note: exception for window.status
  61. };
  62. ;;; $c.cancelEvent = cancelEvent;
  63. // ========== Utilities ==========
  64. // ---------- Functional ----------
  65. var _map = function(func_, list_) {
  66. var len = list_.length || 0;
  67. var res = [];
  68. res.length = len;
  69. for (var i=len-1; i>=0; i--) {
  70. res[i] = func_(list_[i]);
  71. }
  72. return res;
  73. };
  74. ;;; $c._map = _map;
  75. var _reduce = function(func_, list_, initial_) {
  76. var len = list_.length || 0;
  77. if (len === 0) {
  78. return initial_;
  79. }
  80. var i = 0;
  81. if (typeof initial_ === "undefined") {
  82. initial_ = list_[i++];
  83. }
  84. for (; i<len; i++) {
  85. initial_ = func_(initial_, list_[i]);
  86. }
  87. return initial_;
  88. };
  89. ;;; $c._reduce = _reduce;
  90. var _filter = function(func_, list_) {
  91. var len = list_.length || 0;
  92. var res = [];
  93. for (var i=len-1; i>=0; i--) {
  94. if (func_(list_[i])) {
  95. res.push(list_[i]);
  96. }
  97. }
  98. return res.reverse();
  99. };
  100. ;;; $c._filter = _filter;
  101. var _zip = function() {
  102. var num_lists = arguments.length || 0;
  103. if (num_lists === 0) {
  104. return [];
  105. }
  106. var len = arguments[0].length || 0;
  107. var res = [];
  108. res.length = len;
  109. for (var i=len-1; i>=0; i--) {
  110. res[i] = _map(function (x) { return x[i];}, arguments);
  111. }
  112. return res;
  113. };
  114. ;;; $c._zip = _zip;
  115. // ---------- Binary Search ----------
  116. var _search = function(bucket_, key_) {
  117. var low = 0;
  118. var high = bucket_.length-1;
  119. while (low <= high) {
  120. var mid = Math.floor((low + high) / 2);
  121. var midVal = bucket_[mid];
  122. if (midVal < key_) {
  123. low = mid + 1;
  124. } else if (midVal > key_) {
  125. high = mid - 1;
  126. } else { // midVal == key_
  127. return mid;
  128. }
  129. }
  130. return -(low+1); // flag: key not found
  131. };
  132. ;;; $c._search = _search;
  133. var _range_r = function(bucket_, key_) {
  134. var low = 0;
  135. var high = bucket_.length-1;
  136. while (low <= high) {
  137. var mid = Math.floor((low + high) / 2);
  138. var midVal = bucket_[mid];
  139. if (midVal < key_) {
  140. low = mid + 1;
  141. } else if (midVal > key_) {
  142. high = mid - 1;
  143. } else { // midVal == key_
  144. return bucket_.slice(mid);
  145. }
  146. }
  147. return bucket_.slice(low);
  148. };
  149. ;;; $c._range_r = _range_r;
  150. var _range_l = function(bucket_, key_) {
  151. var low = 0;
  152. var high = bucket_.length-1;
  153. while (low <= high) {
  154. var mid = Math.floor((low + high) / 2);
  155. var midVal = bucket_[mid];
  156. if (midVal < key_) {
  157. low = mid + 1;
  158. } else if (midVal > key_) {
  159. high = mid - 1;
  160. } else { // midVal == key_
  161. return bucket_.slice(0, mid+1);
  162. }
  163. }
  164. return bucket_.slice(0, high+1);
  165. };
  166. ;;; $c._range_l = _range_l;
  167. var _range = function(bucket_, lkey_, rkey_) {
  168. var low, high, mid, midVal, lindex, rindex;
  169. low = 0;
  170. high = bucket_.length-1;
  171. while (low <= high) {
  172. mid = Math.floor((low + high) / 2);
  173. midVal = bucket_[mid];
  174. if (midVal < lkey_) {
  175. low = mid + 1;
  176. } else if (midVal > lkey_) {
  177. high = mid - 1;
  178. } else { // midval == lkey_
  179. low = mid;
  180. break;
  181. }
  182. }
  183. lindex = low;
  184. high = bucket_.length-1;
  185. while (low <= high) {
  186. mid = Math.floor((low + high) / 2);
  187. midVal = bucket_[mid];
  188. if (midVal < rkey_) {
  189. low = mid + 1;
  190. } else if (midVal > rkey_) {
  191. high = mid - 1;
  192. } else { // midval == rkey_
  193. high = mid;
  194. break;
  195. }
  196. }
  197. rindex = high + 1;
  198. return bucket_.slice(lindex, rindex);
  199. };
  200. ;;; $c._range = _range;
  201. // ---------- Linked List ----------
  202. var LinkedList = function() {
  203. this.first = null;
  204. this.last = null;
  205. this.length = 0;
  206. };
  207. ;;; $c.LinkedList = LinkedList;
  208. var LinkedListNode = function(data) {
  209. this.prev = null;
  210. this.next = null;
  211. this.data = data;
  212. };
  213. ;;; $c.LinkedListNode = LinkedListNode;
  214. LinkedList.prototype.prepend = function(node) {
  215. if (this.first === null) {
  216. this.first = node;
  217. this.last = node;
  218. } else {
  219. node.prev = null;
  220. node.next = this.first;
  221. this.first.prev = node;
  222. this.first = node;
  223. }
  224. this.length++;
  225. };
  226. LinkedList.prototype.append = function(node) {
  227. if (this.first === null) {
  228. this.first = node;
  229. this.last = node;
  230. } else {
  231. node.prev = this.last;
  232. node.next = null;
  233. this.last.next = node;
  234. this.last = node;
  235. }
  236. this.length++;
  237. };
  238. LinkedList.prototype.remove = function(node) {
  239. if (this.first == this.last) {
  240. this.first = null;
  241. this.last = null;
  242. } else {
  243. if (node.prev) {
  244. node.prev.next = node.next;
  245. }
  246. if (node.next) {
  247. node.next.prev = node.prev;
  248. }
  249. if (node == this.first) {
  250. this.first = node.next;
  251. }
  252. if (node == this.last) {
  253. this.last = node.prev;
  254. }
  255. }
  256. this.length--;
  257. node.prev = null;
  258. node.next = null;
  259. };
  260. // ---------- Mapped Linked List ----------
  261. var MappedLinkedList = function() {
  262. this.map = {};
  263. this.list = new LinkedList();
  264. };
  265. ;;; $c.MappedLinkedList = MappedLinkedList;
  266. MappedLinkedList.prototype.add = function(key, val) {
  267. if (typeof this.map[key] !== "undefined") {
  268. return; // don't re-add
  269. }
  270. var node = new LinkedListNode(val);
  271. this.map[key] = node;
  272. this.list.append(node);
  273. };
  274. MappedLinkedList.prototype.remove = function(key) {
  275. if (typeof this.map[key] === "undefined") {
  276. return;
  277. }
  278. this.list.remove(this.map[key]);
  279. delete(this.map[key]);
  280. };
  281. // ---------- Pub/Sub Broker ----------
  282. var PubSubBroker = function() {
  283. this.channels = {};
  284. };
  285. PubSubBroker.prototype = {
  286. uid_counter: 0
  287. };
  288. ;;; $c.PubSubBroker = PubSubBroker;
  289. PubSubBroker.prototype.publish = function(signal) {
  290. setTimeout((function(signal) { // make it asynch
  291. var passed = arguments;
  292. var channels = this.channels;
  293. return function() {
  294. var args = Array.prototype.slice.call(passed, 1);
  295. var node = channels[signal] && channels[signal].list.first;
  296. while (node) {
  297. var handler = node.data;
  298. setTimeout((function(handler, args) { // make it asynch
  299. return function() {
  300. handler.apply(this, args);
  301. };
  302. }).call(this, handler, args), 0);
  303. node = node.next;
  304. }
  305. };
  306. }).apply(this, arguments), 0);
  307. };
  308. PubSubBroker.prototype.subscribe = function(signal, scope, handlerName){
  309. if (typeof this.channels[signal] === "undefined") {
  310. this.channels[signal] = new MappedLinkedList();
  311. }
  312. var curryArray = Array.prototype.slice.call(arguments, 3);
  313. if (typeof scope._psb_uid == "undefined") {
  314. scope._psb_uid = ++this.uid_counter;
  315. }
  316. var key = scope._psb_uid + ":" + handlerName + ":" + curryArray;
  317. this.channels[signal].add(key, function(){
  318. var normalizedArgs = Array.prototype.slice.call(arguments, 0);
  319. scope[handlerName].apply((scope || window), curryArray.concat(normalizedArgs));
  320. });
  321. };
  322. PubSubBroker.prototype.unsubscribe = function(signal, scope, handlerName) {
  323. if (typeof this.channels[signal] === "undefined") {
  324. return; // throw warn?
  325. }
  326. var curryArray = Array.prototype.slice.call(arguments, 3);
  327. var key = scope._psb_uid + ":" + handlerName + ":" + curryArray;
  328. this.channels[signal].remove(key);
  329. };
  330. // ========== Canvas Utilities ==========
  331. // ---------- Context Helpers ----------
  332. function decorate_context(context_) {
  333. context_._calc = function(pos_) {
  334. return context_.lineWidth % 2 ? Math.floor(pos_) + 0.5 : Math.floor(pos_);
  335. };
  336. context_._moveTo = function(x_, y_) {
  337. context_.moveTo(context_._calc(x_), context_._calc(y_));
  338. };
  339. context_._lineTo = function(x_, y_) {
  340. context_.lineTo(context_._calc(x_), context_._calc(y_));
  341. };
  342. }
  343. // ========== Chart ==========
  344. // ---------- LineDataCache ----------
  345. //TODO: consider: auto-generating the zoom schema
  346. var _ZOOM_SCHEMA_HALF = [
  347. // [delta, low period, high period]
  348. [60, 1, 15],
  349. [60*5, 3, 12],
  350. [60*30, 2, 12],
  351. [60*60*3, 2, 16],
  352. [60*60*12, 4, 16]
  353. ];
  354. var _ZOOM_SCHEMA_RELAXED = [
  355. // [delta, low period, high period]
  356. [60, 1, 20],
  357. [60*5, 4, 18],
  358. [60*30, 3, 18],
  359. [60*60*3, 3, 20],
  360. [60*60*12, 5, 31]
  361. ];
  362. var _ZOOM_SCHEMA_STANDARD = [
  363. // [delta, low period, high period]
  364. [60, 1, 30],
  365. [60*5, 6, 30],
  366. [60*30, 5, 30],
  367. [60*60*3, 5, 32],
  368. [60*60*12, 8, 31]
  369. ];
  370. var _ZOOM_SCHEMA_AGGRESSIVE = [
  371. // [delta, low period, high period]
  372. [60, 1, 40],
  373. [60*5, 8, 42],
  374. [60*30, 7, 42],
  375. [60*60*3, 7, 40],
  376. [60*60*12, 10, 31]
  377. ];
  378. var _ZOOM_SCHEMA_INSANE = [
  379. // [delta, low period, high period]
  380. [60, 1, 60],
  381. [60*5, 12, 60],
  382. [60*30, 10, 60],
  383. [60*60*3, 10, 60],
  384. [60*60*12, 15, 31]
  385. ];
  386. var _ZOOM_SCHEMA_MAX = [
  387. // [delta, low period, high period]
  388. [60, 1, 120],
  389. [60*5, 24, 120],
  390. [60*30, 20, 120],
  391. [60*60*3, 20, 120],
  392. [60*60*12, 30, 31]
  393. ];
  394. /**
  395. * Data Cache for a Line
  396. * a cache for each zoom scale [different granularity of time data]
  397. * w/in each zoom scale cache, the point interval is the time delta at that zoom scale
  398. * @class
  399. */
  400. var LineDataCache = function(model_) {
  401. // Initialize
  402. var
  403. model = model_,
  404. min_time = 0,
  405. max_time = 0,
  406. bucket_size = 0,
  407. min_buckets = 0,
  408. max_buckets = 0,
  409. bucket_ttl = 0,
  410. cache = [], // at zoom scale
  411. proxy_bucket = [];
  412. this.model = function() { return model; };
  413. this.min_time = function() { return min_time; };
  414. this.max_time = function() { return max_time; };
  415. this.cache = function() { return cache; };
  416. /* Broker Channels:
  417. /cache/flush flush expired buckets
  418. */
  419. // Setup
  420. function init(min_time_, max_time_) {
  421. min_time = min_time_;
  422. max_time = max_time_;
  423. bucket_size = LineDataCache.prototype.bucket_size;
  424. min_buckets = LineDataCache.prototype.min_buckets;
  425. max_buckets = LineDataCache.prototype.max_buckets;
  426. bucket_ttl = LineDataCache.prototype.bucket_ttl;
  427. proxy_bucket.length = bucket_size;
  428. for (var i=0; i<model.zoom_schema().length; i++) {
  429. cache[i] = {};
  430. }
  431. //FIXME: pull these for real
  432. //XXX: bucket: 0; increment: 60
  433. for (var i=0; i<4; i++) {
  434. var bucket = [];
  435. bucket.length = bucket_size; //XXX: set length to get undefined from slice
  436. var range = 70-i*10;
  437. for (var j=0; j<bucket_size; j++) {
  438. if (i==2) {
  439. bucket[j] = 50;
  440. } else {
  441. bucket[j] = range+Math.random()*30;
  442. }
  443. }
  444. cache[0][i] = bucket;
  445. }
  446. var zoom_schema = model.zoom_schema();
  447. if (zoom_schema.length >= 2) {
  448. //XXX: bucket: 1; increment: 60*5
  449. __sum = function(x,y) { return (x || 0) + (y || 0); };
  450. _sum = function(list_) { return _reduce(__sum, list_, 0); };
  451. cache[1][0] = [];
  452. cache[1][0].length = bucket_size;
  453. var k=0;
  454. for (i=0; i<4; i++) {
  455. for (j=0; j<bucket_size; j+=5) {
  456. cache[1][0][k++] = _sum(cache[0][i].slice(j,j+5)) / 5;
  457. }
  458. }
  459. var len = k;
  460. }
  461. if (zoom_schema.length >= 3) {
  462. //XXX: bucket: 2; increment: 60*30
  463. cache[2][0] = [];
  464. cache[2][0].length = bucket_size;
  465. k=0;
  466. for (i=0; i<len; i+=6) {
  467. cache[2][0][k++] = _sum(cache[1][0].slice(i,i+6)) / 6;
  468. }
  469. len = k;
  470. }
  471. if (zoom_schema.length >= 4) {
  472. //XXX: bucket: 3; increment: 60*60*3
  473. cache[3][0] = [];
  474. cache[3][0].length = bucket_size;
  475. k=0;
  476. for (i=0; i<len; i+=6) {
  477. cache[3][0][k++] = _sum(cache[2][0].slice(i,i+6)) / 6;
  478. }
  479. len = k;
  480. }
  481. if (zoom_schema.length >= 5) {
  482. //XXX: bucket: 4; increment: 60*60*12
  483. cache[4][0] = [];
  484. cache[4][0].length = bucket_size;
  485. k=0;
  486. for (i=0; i<len; i+=4) {
  487. cache[4][0][k++] = _sum(cache[3][0].slice(i,i+4)) / 4;
  488. }
  489. }
  490. // Register for Upkeep
  491. var broker = model.broker();
  492. broker.subscribe("/cache/flush", this, "_flush");
  493. }
  494. this.init = init;
  495. // Data
  496. /**
  497. * @function
  498. * @param {int} zoom_scale_ cache data level to use
  499. * @param {int} start_offset_ first point in range
  500. * @param {int} end_offset_ last point in range
  501. * @returns cache[zoom_scale_][start_offset_:end_offset_+1]
  502. */
  503. function fetch(zoom_scale_, start_offset_, end_offset_) {
  504. var _data = cache[zoom_scale_];
  505. var start_idx = Math.floor(start_offset_/bucket_size);
  506. var end_idx = Math.floor(end_offset_/bucket_size);
  507. //XXX: avoid % (IE6 is slow)
  508. start_offset_ -= start_idx * bucket_size;
  509. end_offset_ -= end_idx * bucket_size;
  510. end_offset_ += 1; //XXX: compensate for exclusive
  511. if (start_idx === end_idx) {
  512. var _bucket = _data[start_idx];
  513. if (!_bucket) {
  514. _bucket = proxy_bucket;
  515. //TODO: fetch bucket from remote data source
  516. }
  517. return _bucket.slice(start_offset_, end_offset_);
  518. }
  519. var _start_bucket = _data[start_idx];
  520. if (!_start_bucket) {
  521. _start_bucket = proxy_bucket;
  522. //TODO: fetch bucket from remote data source
  523. }
  524. var _end_bucket = _data[end_idx];
  525. if (!_end_bucket) {
  526. _end_bucket = proxy_bucket;
  527. //TODO: fetch bucket from remote data source
  528. }
  529. var start_range = _start_bucket.slice(start_offset_, bucket_size);
  530. var end_range = _end_bucket.slice(0, end_offset_);
  531. return start_range.concat(end_range);
  532. }
  533. this.fetch = fetch;
  534. // Upkeep
  535. function _flush() {
  536. //TODO: flush expired buckets
  537. }
  538. this._flush = _flush;
  539. };
  540. //TODO: implement bucket caching
  541. //TODO: implement cache using a MappedLinkedList
  542. LineDataCache.prototype = {
  543. bucket_size: 60*24, // # of time points (depending on zoom scale)
  544. min_buckets: 10, // # of buckets to keep, regardless of TTL
  545. max_buckets: 100, // # of buckets to keep, at any time
  546. bucket_ttl: 60*1000 // 1m
  547. };
  548. // ---------- Model ----------
  549. var Model = function(parent_, zoom_schema_) {
  550. // Initialize
  551. var
  552. parent = parent_,
  553. zoom_schema = zoom_schema_,
  554. broker = parent.broker(),
  555. min_time = 0,
  556. max_time = 0,
  557. _time_range = 0,
  558. data = [],
  559. upkeep_interval = Model.prototype.upkeep_interval;
  560. this.parent = function() { return parent; };
  561. this.zoom_schema = function() { return zoom_schema; };
  562. this.zoom_level = function() { return zoom_schema[parent.zoom_scale()]; };
  563. this.zoom_delta = function() { return zoom_schema[parent.zoom_scale()][0]; };
  564. this.broker = function() { return broker; };
  565. this.min_time = function() { return min_time; };
  566. this.max_time = function() { return max_time; };
  567. this._time_range = function() { return _time_range; };
  568. this.data = function() { return data; };
  569. // Setup
  570. function init(min_time_, max_time_, chart_width_, pane_width_) {
  571. min_time = min_time_;
  572. max_time = max_time_;
  573. _time_range = max_time - min_time;
  574. // calculate: max zoom level and period
  575. var _num_panes = chart_width_ / pane_width_;
  576. for (var i=0; i<zoom_schema.length; i++) {
  577. var _level = zoom_schema[i];
  578. if (_time_range < (_level[0]*chart_width_)) {
  579. zoom_schema.length = i+1;
  580. _max_points = Math.floor(_time_range / _level[0]);
  581. zoom_schema[i][2] = Math.ceil(_max_points / _num_panes);
  582. break;
  583. }
  584. }
  585. //FIXME: create this for real
  586. var line_cache = new LineDataCache(this);
  587. line_cache.init(min_time, max_time);
  588. data[0] = line_cache;
  589. // Upkeep Thread
  590. setTimeout(_upkeep, upkeep_interval);
  591. }
  592. this.init = init;
  593. // Data
  594. function fetch(zoom_scale_, start_offset_, period_) {
  595. var _end_offset = start_offset_ + period_;
  596. var section = [];
  597. for (var i=data.length-1; i>=0; i--) {
  598. section.push(data[i].fetch(zoom_scale_, start_offset_, _end_offset));
  599. }
  600. section.reverse(); //XXX: think: is this necessary? consider how to identify lines
  601. return section;
  602. }
  603. this.fetch = fetch;
  604. // Upkeep
  605. function _upkeep() {
  606. broker.publish("/cache/flush");
  607. setTimeout(_upkeep, upkeep_interval);
  608. }
  609. };
  610. Model.prototype = {
  611. //TODO: make zoom_schema private (potentially, multiple charts)
  612. upkeep_interval: 60*1000 // background upkeep thread interval
  613. };
  614. // ---------- Pane ----------
  615. var PLOT_LINE_WIDTH = 1.25;
  616. if ($.browser.msie) { //XXX: hack: IE6
  617. PLOT_LINE_WIDTH = 1.6;
  618. }
  619. var BOTTOM_TAPE_HEIGHT = 16;
  620. var BOTTOM_TAPE_TOP_ADJUST = 0;
  621. //var BOTTOM_TAPE_COLOR = "rgb(147,190,255)";
  622. //var BOTTOM_TAPE_COLOR = "rgb(198,228,255)";
  623. var BOTTOM_TAPE_COLOR = "rgb(194,218,241)";
  624. if ($.browser.msie) { //XXX: hack: IE6
  625. BOTTOM_TAPE_TOP_ADJUST = -1;
  626. }
  627. var TAPE_MARKER_HEIGHT = 5;
  628. var Pane = function(parent_, width_) {
  629. // Initialize
  630. var
  631. parent = parent_,
  632. model = parent.model(),
  633. time_offset = 0,
  634. _data_offset = 0,
  635. width = width_,
  636. _half_width = Math.floor(width/2),
  637. draw_height = 0,
  638. chart_height = 0,
  639. container = null,
  640. _style = null,
  641. canvas = null,
  642. c = null,
  643. tape = null,
  644. _data = [],
  645. _min_y = 0,
  646. _max_y = 0;
  647. this.parent = function() { return parent; };
  648. this.time_offset = function() { return time_offset; };
  649. this._data_offset = function() { return _data_offset; };
  650. this.container = function() { return container; };
  651. this.canvas = function() { return canvas; };
  652. this.offset = function() { return parseInt(_style.left, 10); };
  653. this.c = function() { return c; };
  654. this.tape = function() { return tape; };
  655. this._data = function() { return _data; };
  656. this._min_y = function() { return _min_y; };
  657. this._max_y = function() { return _max_y; };
  658. /* Broker Channels:
  659. /pane/translate translate pane by delta
  660. /pane/redraw redraw pane
  661. */
  662. // Setup
  663. function init() {
  664. // Setup Variables
  665. draw_height = parent.draw_height();
  666. chart_height = parent.chart_height();
  667. // Container
  668. container = $([
  669. '<div',
  670. 'style="',
  671. 'position: absolute;',
  672. 'left: 0px;',
  673. 'top: 0px;',
  674. 'z-index: 0;',
  675. 'width: '+width+'px;',
  676. 'height: '+draw_height+'px;',
  677. '"></div>'].join(' ')).appendTo(parent.container());
  678. _style = container[0].style;
  679. // Canvas
  680. //XXX: build manually (excanvas r3 workaround)
  681. canvas = document.createElement("canvas");
  682. canvas.width = width;
  683. canvas.height = chart_height;
  684. var _canvas = $(canvas);
  685. _canvas.css("position", "absolute");
  686. _canvas.css("left", "0px");
  687. _canvas.css("top", "0px");
  688. _canvas.css("z-index", "0");
  689. _canvas.css("width", width+"px");
  690. _canvas.css("height", chart_height+"px");
  691. _canvas.appendTo(container);
  692. if ($.browser.msie) { //XXX: hack: excanvas
  693. canvas = window.G_vmlCanvasManager.initElement(canvas);
  694. }
  695. c = canvas.getContext("2d");
  696. decorate_context(c);
  697. if ($.browser.msie && c.setTransform) {
  698. //XXX: exception: IE6 renders more smoothly w/ setTransform
  699. c.setTransform(1, 0, 0, -1, 0, chart_height);
  700. }
  701. // Tape
  702. tape = $([
  703. '<div',
  704. 'style="',
  705. 'position: absolute;',
  706. 'left: 0px;',
  707. 'top: '+chart_height+'px;',
  708. 'z-index: 0;',
  709. 'width: '+width+'px;',
  710. 'height: '+(draw_height-chart_height)+'px;',
  711. 'text-align: left;',
  712. '"></div>'].join(' ')).appendTo(container);
  713. // Broker Subscriptions
  714. var broker = parent.broker();
  715. broker.subscribe("/pane/translate", this, "translate");
  716. broker.subscribe("/pane/redraw", this, "_draw");
  717. //XXX: needs reset()
  718. }
  719. this.init = init;
  720. // Translate Methods
  721. function translate(delta_) {
  722. _style.left = (parseInt(_style.left, 10)+delta_)+"px";
  723. }
  724. this.translate = translate;
  725. // Draw Methods
  726. function __grid_x(x_tick_) {
  727. if ($.browser.msie && c.setTransform) {
  728. //XXX: exception: IE6 renders more smoothly w/ setTransform
  729. __grid_x = function(x_tick_) {
  730. c.lineWidth = 1;
  731. // Vertical Line at 0
  732. c.beginPath();
  733. c.strokeStyle = "rgb(200,200,200)";
  734. c._moveTo(x_tick_, 0);
  735. c._lineTo(x_tick_, chart_height);
  736. c.stroke();
  737. c.beginPath();
  738. c.strokeStyle = "rgb(100,100,100)";
  739. c._moveTo(x_tick_, 0);
  740. c._lineTo(x_tick_, TAPE_MARKER_HEIGHT);
  741. c.stroke();
  742. };
  743. } else { //XXX: Safari,FF2 don't support setTransform; FF3 renders faster
  744. __grid_x = function(x_tick_) {
  745. c.lineWidth = 1;
  746. // Vertical Line at 0
  747. c.beginPath();
  748. c.strokeStyle = "rgb(200,200,200)";
  749. c._moveTo(x_tick_, 0);
  750. c._lineTo(x_tick_, chart_height);
  751. c.stroke();
  752. c.beginPath();
  753. c.strokeStyle = "rgb(100,100,100)";
  754. c._moveTo(x_tick_, chart_height);
  755. var _tape_y = chart_height-TAPE_MARKER_HEIGHT;
  756. c._lineTo(x_tick_, _tape_y);
  757. c.stroke();
  758. };
  759. }
  760. return __grid_x(x_tick_);
  761. }
  762. this.__grid_x = __grid_x;
  763. function __grid_y(y_intervals_) {
  764. if ($.browser.msie && c.setTransform) {
  765. //XXX: exception: IE6 renders more smoothly w/ setTransform
  766. __grid_y = function(y_intervals_) {
  767. c.lineWidth = 1;
  768. // Horizontal Lines
  769. c.strokeStyle = "rgb(200,200,200)";
  770. //XXX: consider Duff's device
  771. for (var i=y_intervals_.length-1; i>=0; i--) {
  772. c.beginPath();
  773. var y = y_intervals_[i];
  774. c._moveTo(0, y);
  775. c._lineTo(width, y);
  776. c.stroke();
  777. }
  778. };
  779. } else { //XXX: Safari,FF2 don't support setTransform; FF3 renders faster
  780. __grid_y = function(y_intervals_) {
  781. c.lineWidth = 1;
  782. // Horizontal Lines
  783. c.strokeStyle = "rgb(200,200,200)";
  784. //XXX: consider Duff's device
  785. for (var i=y_intervals_.length-1; i>=0; i--) {
  786. c.beginPath();
  787. var _y = chart_height - y_intervals_[i];
  788. c._moveTo(0, _y);
  789. c._lineTo(width, _y);
  790. c.stroke();
  791. }
  792. };
  793. }
  794. return __grid_y(y_intervals_);
  795. }
  796. this.__grid_y = __grid_y;
  797. function __plot(x_scale_, y_scale_, points_) {
  798. if ($.browser.msie && c.setTransform) {
  799. //XXX: exception: IE6 renders more smoothly w/ setTransform
  800. __plot = function(x_scale_, y_scale_, points_) {
  801. c.lineWidth = PLOT_LINE_WIDTH;
  802. c.strokeStyle = "rgb(25,106,227)";
  803. c.lineJoin = "miter"; //XXX: don't use round (prob w/ seams)
  804. c.beginPath();
  805. // first point @ 0
  806. //XXX: fix seams: account for idx: -1
  807. c._moveTo(-1*x_scale_, (points_[-1] || 0)*y_scale_);
  808. c._lineTo(0, (points_[0] || 0)*y_scale_);
  809. //XXX: faster cleaner modified duff's device
  810. //XXX: ref: http://home.earthlink.net/~kendrasg/info/js_opt/jsOptMain.html
  811. var i = 1;
  812. var iter = points_.length-2;
  813. var n = iter % 8;
  814. while (n--) {
  815. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  816. }
  817. n = parseInt(iter/8, 10);
  818. while (n--) {
  819. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  820. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  821. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  822. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  823. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  824. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  825. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  826. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  827. }
  828. // last point @ width
  829. c._lineTo(width, (points_[points_.length-1] || 0)*y_scale_);
  830. c._lineTo(width+1, (points_[points_.length-1] || 0)*y_scale_);
  831. c.stroke();
  832. c._lineTo(width+1, 0);
  833. //XXX: fix seams: account for idx: -1
  834. c._lineTo(-1*x_scale_, 0);
  835. c.fillStyle = "rgba(183,220,255, 0.4)";
  836. c.fill();
  837. };
  838. } else { //XXX: Safari,FF2 don't support setTransform; FF3 renders faster
  839. __plot = function(x_scale_, y_scale_, points_) {
  840. c.lineWidth = PLOT_LINE_WIDTH;
  841. c.strokeStyle = "rgb(25,106,227)";
  842. c.lineJoin = "miter"; //XXX: don't use round (prob w/ seams)
  843. c.beginPath();
  844. // first point @ 0
  845. //XXX: fix seams: account for idx: -1
  846. c._moveTo(-1*x_scale_, chart_height - (points_[-1] || 0)*y_scale_);
  847. c._lineTo(0, chart_height - (points_[0] || 0)*y_scale_);
  848. //XXX: faster cleaner modified duff's device
  849. //XXX: ref: http://home.earthlink.net/~kendrasg/info/js_opt/jsOptMain.html
  850. var i = 1;
  851. var iter = points_.length-2;
  852. var n = iter % 8;
  853. while (n--) {
  854. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  855. }
  856. n = parseInt(iter/8, 10);
  857. while (n--) {
  858. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  859. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  860. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  861. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  862. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  863. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  864. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  865. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  866. }
  867. // last point @ width
  868. c._lineTo(width, chart_height - (points_[points_.length-1] || 0)*y_scale_);
  869. c._lineTo(width+1, chart_height - (points_[points_.length-1] || 0)*y_scale_);
  870. c.stroke();
  871. c.fillStyle = "rgba(183,220,255, 0.4)";
  872. c._lineTo(width+1, chart_height);
  873. //XXX: fix seams: account for idx: -1
  874. c._lineTo(-1*x_scale_, chart_height);
  875. c.fill();
  876. };
  877. }
  878. return __plot(x_scale_, y_scale_, points_);
  879. }
  880. this.__plot = __plot;
  881. function __tape_marker(x_tick_, x_text_) {
  882. //TODO: consider: color code tape?
  883. // Set Text
  884. tape.append([
  885. '<div',
  886. 'class="tape-tick"',
  887. 'style="',
  888. 'position: absolute;',
  889. 'left: '+x_tick_+'px;',
  890. 'top: 0px;',
  891. 'z-index: 0;',
  892. 'width: '+width+'px;',
  893. '">'+x_text_+'</div>'].join(' '));
  894. }
  895. this.__tape_marker = __tape_marker;
  896. function _fetch_data() {
  897. //XXX: fix seams: grab idx: -1
  898. _data = model.fetch(
  899. parent.zoom_scale(),
  900. _data_offset-1,
  901. parent._pane_period()+1);
  902. // save min/max-y
  903. if (_data.length === 0) {
  904. _min_y = 0;
  905. _max_y = 0;
  906. return;
  907. }
  908. var i,j, _source;
  909. //XXX: fix seams: put idx: -1 at [-1]
  910. for (i=_data.length-1; i>=0; i--) {
  911. _source = _data[i];
  912. _source[-1] = _source.shift();
  913. }
  914. _min_y = _max_y = _data[0][0] || 0;
  915. for (i=_data.length-1; i>=0; i--) {
  916. _source = _data[i];
  917. for (j=_source.length-1; j>=0; j--) {
  918. var _y = _source[j] || 0;
  919. _min_y = Math.min(_min_y, _y);
  920. _max_y = Math.max(_max_y, _y);
  921. }
  922. }
  923. }
  924. function _draw() {
  925. // reset canvas
  926. c.clearRect(0, 0, width, draw_height);
  927. // reset tape
  928. tape.empty();
  929. // determine: scale
  930. var x_scale = parent._x_scale();
  931. var y_scale = parent._y_scale();
  932. // draw grid
  933. var _x_data = parent._x_tick()(time_offset);
  934. var _x_tick = (_x_data[0] / model.zoom_delta()) * x_scale;
  935. if (_x_tick < width) {
  936. __grid_x(_x_tick);
  937. // draw tape
  938. __tape_marker(_x_tick, _x_data[1]);
  939. }
  940. __grid_y(parent._y_intervals());
  941. // draw chart
  942. for (var i=_data.length-1;i>=0;i--) {
  943. __plot(x_scale, y_scale, _data[i]);
  944. }
  945. }
  946. this._draw = _draw;
  947. function _reset(data_offset_, offset_) {
  948. _data_offset = data_offset_;
  949. _style.left = offset_ + "px";
  950. time_offset = model.min_time() + _data_offset * model.zoom_delta();
  951. _fetch_data();
  952. }
  953. this._reset = _reset;
  954. function reset(data_offset_, offset_) {
  955. _reset(data_offset_, offset_);
  956. _draw();
  957. }
  958. this.reset = reset;
  959. function range_min_max_y(start_, end_) {
  960. if (_data.length === 0) {
  961. return [0, 0];
  962. }
  963. var min, max;
  964. min = max = _data[0][start_] || 0;
  965. for (var i=_data.length-1; i>=0; i--) {
  966. var _source = _data[i].slice(start_, end_);
  967. for (var j=_source.length-1; j>=0; j--) {
  968. var _y = _source[j] || 0;
  969. min = Math.min(min, _y);
  970. max = Math.max(max, _y);
  971. }
  972. }
  973. return [min, max];
  974. }
  975. this.range_min_max_y = range_min_max_y;
  976. };
  977. // ---------- Navigation Pane ----------
  978. NAV_HEIGHT = 75;
  979. NAV_BUTTON_HEIGHT = 13;
  980. NAV_BUTTON_WIDTH = 16;
  981. NAV_SCROLL_RATE = 1000 * 0.1;
  982. NAV_SCROLL_SPEED = 50;
  983. NAV_SCROLL_MIN_WIDTH = 20;
  984. NAV_SCROLL_MID_THRESHOLD = 60;
  985. NAV_OVERVIEW_TOP_OFFSET = 5;
  986. NAV_OVERVIEW_SCALE_FACTOR = 1.1;
  987. NAV_RANGE_OFFSET = -9;
  988. NAV_RANGE_WIDTH = 13;
  989. NAV_RANGE_CENTER_THRESHOLD = 13;
  990. NAV_RANGE_CENTER_OFFSET = -8;
  991. var NavigationPane = function(parent_) {
  992. // Initialize
  993. var
  994. parent = parent_,
  995. model = parent.model(),
  996. broker = parent.broker(),
  997. width = 0,
  998. height = 0,
  999. container = null,
  1000. nav_left = null,
  1001. nav_right = null,
  1002. nav_bar = null,
  1003. zoom_level = null,
  1004. overview_left = 0,
  1005. overview = null,
  1006. oc = null,
  1007. preview = null,
  1008. _preview = null,
  1009. pc = null,
  1010. _data = [],
  1011. _x_scale = 0,
  1012. _y_scale = 0,
  1013. viewer = null,
  1014. _viewer = null,
  1015. viewer_width = 0,
  1016. slider = null,
  1017. _slider = null,
  1018. slider_middle = null,
  1019. drag_modifier = 1,
  1020. range_min = null,
  1021. _range_min = null,
  1022. range_max = null,
  1023. _range_max = null,
  1024. __is_hover__ = false,
  1025. __is_drag_min__ = false;
  1026. this.parent = function() { return parent; };
  1027. this.model = function() { return model; };
  1028. this.width = function() { return width; };
  1029. this.height = function() { return height; };
  1030. this.container = function() { return container; };
  1031. this.zoom_level = function() { return zoom_level; };
  1032. this.overview_left = function() { return overview_left; };
  1033. this.overview = function() { return overview; };
  1034. this.oc = function() { return oc; };
  1035. this.preview = function() { return preview; };
  1036. this._preview = function() { return _preview; };
  1037. this.pc = function() { return pc; };
  1038. this._data = function() { return _data; };
  1039. this._x_scale = function() { return _x_scale; };
  1040. this._y_scale = function() { return _y_scale; };
  1041. this.viewer = function() { return viewer; };
  1042. this._viewer = function() { return _viewer; };
  1043. this.viewer_width = function() { return viewer_width; };
  1044. this.slider = function() { return slider; };
  1045. this._slider = function() { return _slider; };
  1046. this.slider_middle = function() { return slider_middle; };
  1047. this.drag_modifier = function() { return drag_modifier; };
  1048. this.range_min = function() { return range_min; };
  1049. this._range_min = function() { return _range_min; };
  1050. this.range_max = function() { return range_max; };
  1051. this._range_max = function() { return _range_max; };
  1052. /* Broker Channels:
  1053. /nav/center (re-)center nav
  1054. /nav/translate translate nav
  1055. */
  1056. // Helper Methods
  1057. function _render_overview(context_, strokeStyle_, fillStyle_, boxStrokeStyle_) {
  1058. // reset canvas
  1059. context_.clearRect(0, 0, overview.width, overview.height);
  1060. context_.lineWidth = 1;
  1061. context_.strokeStyle = strokeStyle_;
  1062. context_.lineJoin = "round";
  1063. context_.beginPath();
  1064. // last point @ width
  1065. context_._moveTo(overview.width, overview.height - (_data[_data.length-2] || 0) * _y_scale);
  1066. // draw overview
  1067. for (var i=_data.length-3; i>0; i--) {
  1068. context_._lineTo(i*_x_scale, overview.height - (_data[i] || 0) * _y_scale);
  1069. }
  1070. // first point @ 0
  1071. context_._lineTo(
  1072. 0,
  1073. overview.height - (_data[0] || 0) * _y_scale);
  1074. context_.stroke();
  1075. // fill overview
  1076. context_.fillStyle = fillStyle_;
  1077. context_._lineTo(0, overview.height);
  1078. context_._lineTo(overview.width, overview.height);
  1079. context_.fill();
  1080. // draw outline
  1081. context_.lineWidth = 1;
  1082. context_.strokeStyle = boxStrokeStyle_;
  1083. context_.lineJoin = "round";
  1084. context_.beginPath();
  1085. context_.moveTo(0, overview.height);
  1086. context_.lineTo(0, 0);
  1087. context_.lineTo(overview.width, 0);
  1088. context_.lineTo(overview.width, overview.height);
  1089. context_.stroke();
  1090. }
  1091. // Setup
  1092. function init(target_, width_, height_, top_, left_) {
  1093. // Setup Variables
  1094. width = width_;
  1095. height = height_;
  1096. // Container
  1097. container = $([
  1098. '<div',
  1099. 'style="',
  1100. 'position: absolute;',
  1101. 'left: '+left_+'px;',
  1102. 'top: '+top_+'px;',
  1103. 'width: '+width+'px;',
  1104. 'height: '+height+'px;',
  1105. '"></div>'].join(' ')).appendTo(target_);
  1106. // Navigation Scroll Bar
  1107. nav_left = $([
  1108. '<div',
  1109. 'class="nav-left-unsel"',
  1110. 'style="',
  1111. 'position: absolute;',
  1112. 'overflow: hidden;',
  1113. 'left: '+(-BORDER_OFFSET_X)+'px;',
  1114. 'top: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
  1115. 'width: '+NAV_BUTTON_WIDTH+'px;',
  1116. 'height: '+NAV_BUTTON_HEIGHT+'px;',
  1117. '"></div>'].join(' ')).appendTo(container);
  1118. nav_right = $([
  1119. '<div',
  1120. 'class="nav-unsel nav-right-unsel"',
  1121. 'style="',
  1122. 'position: absolute;',
  1123. 'overflow: hidden;',
  1124. 'left: '+(-BORDER_OFFSET_X+NAV_BUTTON_WIDTH)+'px;',
  1125. 'top: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
  1126. 'width: '+NAV_BUTTON_WIDTH+'px;',
  1127. 'height: '+NAV_BUTTON_HEIGHT+'px;',
  1128. '"></div>'].join(' ')).appendTo(container);
  1129. nav_bar = $([
  1130. '<div',
  1131. 'style="',
  1132. 'position: absolute;',
  1133. 'left: '+(BORDER_OFFSET_X+2*NAV_BUTTON_WIDTH)+'px;',
  1134. 'top: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
  1135. 'width: '+(width-BORDER_OFFSET_X-2*NAV_BUTTON_WIDTH)+'px;',
  1136. 'height: '+(NAV_BUTTON_HEIGHT)+'px;',
  1137. 'font-size: 0px;', // hack: IE6 fix
  1138. 'border-top: 1px solid #aaa;',
  1139. 'border-right: 1px solid #aaa;',
  1140. 'border-bottom: 1px solid #aaa;',
  1141. '"></div>'].join(' ')).appendTo(container);
  1142. // Overview
  1143. //XXX: build manually (excanvas r3 workaround)
  1144. overview = document.createElement("canvas");
  1145. overview.width = width-2*NAV_BUTTON_WIDTH;
  1146. overview.height = NAV_HEIGHT-NAV_BUTTON_HEIGHT-NAV_OVERVIEW_TOP_OFFSET;
  1147. overview_left = BORDER_OFFSET_X+2*NAV_BUTTON_WIDTH;
  1148. var _overview = $(overview);
  1149. _overview.css("position", "absolute");
  1150. _overview.css("left", overview_left+"px");
  1151. _overview.css("top", NAV_OVERVIEW_TOP_OFFSET+"px");
  1152. _overview.css("z-index", "0");
  1153. _overview.css("width", overview.width+"px");
  1154. _overview.css("height", overview.height+"px");
  1155. _overview.appendTo(container);
  1156. if ($.browser.msie) { //XXX: hack: excanvas
  1157. overview = window.G_vmlCanvasManager.initElement(overview);
  1158. }
  1159. oc = overview.getContext("2d");
  1160. decorate_context(oc);
  1161. // Draw Overview
  1162. var _zoom_idx = model.zoom_schema().length - 1;
  1163. zoom_level = model.zoom_schema()[_zoom_idx];
  1164. _data = model.fetch(
  1165. _zoom_idx,
  1166. 0,
  1167. Math.ceil(model._time_range() / zoom_level[0]));
  1168. _data = _data[0] || []; //XXX: use first data source for overview
  1169. // Calculate X-coord Scale
  1170. _x_scale = overview.width / (_data.length - 1);
  1171. // Calculate Y-coord Scale
  1172. //XXX: optional: make it logarithmic?
  1173. //_data = _map(function (x) { return Math.log(x || 1); }, _data);
  1174. var _min_y, _max_y, _point;
  1175. _min_y = _max_y = _data[_data.length-1] || 0;
  1176. for (var i=_data.length-2; i>=0; i--) {
  1177. _point = _data[i] || 0;
  1178. _min_y = _point < _min_y ? _point : _min_y;
  1179. _max_y = _point > _max_y ? _point : _max_y;
  1180. }
  1181. _max_y = (_max_y - _min_y) * NAV_OVERVIEW_SCALE_FACTOR + _min_y;
  1182. _y_scale = (overview.height - 1) / (_max_y - _min_y);
  1183. _render_overview(oc, "rgb(225,225,225)", "rgba(200,200,200,0.2)", "rgb(200,200,200)");
  1184. // Slider
  1185. slider = $([
  1186. '<div',
  1187. 'class="nav-slider"',
  1188. 'style="',
  1189. 'display: none;',
  1190. 'position: absolute;',
  1191. 'overflow: hidden;',
  1192. 'left: '+overview_left+'px;',
  1193. 'top: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
  1194. 'width: 10px;',
  1195. 'height: '+NAV_BUTTON_HEIGHT+'px;',
  1196. 'text-align: center;',
  1197. '"></div>'].join(' ')).appendTo(container);
  1198. _slider = slider[0];
  1199. slider.append([
  1200. '<img',
  1201. 'src="/assets/slider.left.gif"',
  1202. 'style="',
  1203. 'position: absolute;',
  1204. 'top: 0px;',
  1205. 'left: 0px;',
  1206. '"/>'].join(' '));
  1207. slider.append([
  1208. '<img',
  1209. 'src="/assets/slider.right.gif"',
  1210. 'style="',
  1211. 'position: absolute;',
  1212. 'top: 0px;',
  1213. 'right: 0px;',
  1214. '"/>'].join(' '));
  1215. slider_middle = $([
  1216. '<img',
  1217. 'src="/assets/slider.middle.gif"',
  1218. 'style="',
  1219. 'display: none;',
  1220. '"/>'].join(' ')).appendTo(slider);
  1221. // Viewer
  1222. viewer = $([
  1223. '<div',
  1224. 'class="nav-viewer"',
  1225. 'style="',
  1226. 'position: absolute;',
  1227. 'left: '+overview_left+'px;',
  1228. 'top: 0px;',
  1229. 'width: 1px;',
  1230. 'height: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
  1231. '"></div>'].join(' ')).appendTo(container);
  1232. _viewer = viewer[0];
  1233. //XXX: hide preview, but allow range controls to overflow
  1234. var internal = $([
  1235. '<div',
  1236. 'style="',
  1237. 'position: absolute;',
  1238. 'overflow: hidden;',
  1239. 'left: 0px;',
  1240. 'top: 0px;',
  1241. 'width: 100%;',
  1242. 'height: 100%;',
  1243. '"><div>'].join(' ')).appendTo(viewer);
  1244. // Preview
  1245. //XXX: build manually (excanvas r3 workaround)
  1246. preview = document.createElement("canvas");
  1247. preview.width = overview.width;
  1248. preview.height = overview.height;
  1249. _preview = $(preview);
  1250. _preview.css("position", "absolute");
  1251. _preview.css("left", (-BORDER_OFFSET_X)+"px");
  1252. _preview.css("top", NAV_OVERVIEW_TOP_OFFSET+"px");
  1253. _preview.css("width", preview.width+"px");
  1254. _preview.css("height", preview.height+"px");
  1255. _preview.css("z-index", "0");
  1256. _preview.css("border-bottom", "1px solid #aaa");
  1257. _preview.appendTo(internal);
  1258. if ($.browser.msie) { //XXX: hack: excanvas
  1259. preview = window.G_vmlCanvasManager.initElement(preview);
  1260. }
  1261. pc = preview.getContext("2d");
  1262. decorate_context(pc);
  1263. _render_overview(pc, "rgb(25,106,227)", "rgba(25,106,227,0.1)", "rgb(200,200,200)");
  1264. //XXX: ie6 doesn't render in a hidden DOM element
  1265. viewer.css("display", "none");
  1266. // View Range Controls
  1267. range_min = $([
  1268. '<img',
  1269. 'src="/assets/range.gif"',
  1270. 'style="',
  1271. 'position: absolute;',
  1272. 'top: '+Math.floor(preview.height/2)+'px;',
  1273. 'left: '+NAV_RANGE_OFFSET+'px;',
  1274. 'cursor: col-resize;',
  1275. '"/>'].join(' ')).appendTo(viewer);
  1276. _range_min = range_min[0];
  1277. range_max = $([
  1278. '<img',
  1279. 'src="/assets/range.gif"',
  1280. 'style="',
  1281. 'position: absolute;',
  1282. 'top: '+Math.floor(preview.height/2)+'px;',
  1283. 'right: '+NAV_RANGE_OFFSET+'px;',
  1284. 'cursor: col-resize;',
  1285. '"/>'].join(' ')).appendTo(viewer);
  1286. _range_max = range_max[0];
  1287. __is_hover__ = true; //XXX: initially display range controls
  1288. // --- DOM Events
  1289. // ----- Left/Right Navigation Buttons
  1290. addEvent(nav_left[0], "mousedown", nav_left_mousedown);
  1291. addEvent(nav_right[0], "mousedown", nav_right_mousedown);
  1292. // ----- Preview Slider
  1293. addEvent(_slider, "mousedown", drag_init);
  1294. _slider.onmousedown = function(e) { return cancelEvent(e); };
  1295. // ----- Preview Pane
  1296. addEvent(_viewer, "mousedown", drag_init);
  1297. _viewer.onmousedown = function(e) { return cancelEvent(e); };
  1298. // ----- Preview Range Controls
  1299. container.hover(container_mouseover, container_mouseout);
  1300. addEvent(_range_min, "mousedown", range_min_drag_init);
  1301. _range_min.onmousedown = function(e) { return cancelEvent(e); };
  1302. addEvent(_range_max, "mousedown", range_max_drag_init);
  1303. _range_max.onmousedown = function(e) { return cancelEvent(e); };
  1304. // Events Setup
  1305. broker.subscribe("/nav/center", this, "center");
  1306. broker.subscribe("/nav/translate", this, "translate");
  1307. broker.subscribe("/nav/range/update", this, "update_range");
  1308. //XXX: post init
  1309. broker.subscribe("/nav/center", this, "post_init");
  1310. }
  1311. this.init = init;
  1312. function post_init() {
  1313. broker.unsubscribe("/nav/center", this, "post_init");
  1314. viewer.css("display", "inline");
  1315. slider.css("display", "inline");
  1316. }
  1317. this.post_init = post_init;
  1318. // View Translation Methods
  1319. function center() {
  1320. // static variables
  1321. var _overview_width = overview.width - 2; //XXX: account for border-right/left
  1322. center = function() {
  1323. // calculate: width
  1324. var _num_points = parent._chart_period() * model.zoom_delta() / zoom_level[0];
  1325. viewer_width =
  1326. Math.max(1,
  1327. Math.min(_overview_width,
  1328. Math.floor(_num_points * _x_scale)));
  1329. _viewer.style.width = viewer_width+"px";
  1330. _slider.style.width = Math.max(NAV_SCROLL_MIN_WIDTH, viewer_width)+"px";
  1331. if (viewer_width > NAV_SCROLL_MID_THRESHOLD) {
  1332. slider_middle.css("display", "inline");
  1333. } else {
  1334. slider_middle.css("display", "none");
  1335. }
  1336. // translate
  1337. translate(); //XXX: don't publish, has to be in-sync (otherwise, flickers)
  1338. // update: drag modifier
  1339. drag_modifier = (zoom_level[0] / model.zoom_delta()) * parent._x_scale() / _x_scale;
  1340. // range controls: re-position, if necessary
  1341. var _viewer_width = viewer.width();
  1342. if (_viewer_width > NAV_RANGE_CENTER_THRESHOLD) {
  1343. _range_min.style.left = NAV_RANGE_OFFSET+"px";
  1344. if (__is_hover__) {
  1345. range_max.css("display", "inline");
  1346. }
  1347. } else { // viewer.width <= range control width
  1348. _range_min.style.left =
  1349. Math.floor(_viewer_width/2+NAV_RANGE_CENTER_OFFSET)+"px";
  1350. range_max.css("display", "none");
  1351. }
  1352. };
  1353. return center();
  1354. }
  1355. this.center = center;
  1356. function translate() {
  1357. // static variables
  1358. var _half_width = parent.width() / 2;
  1359. var _overview_width = overview.width - 2;
  1360. var _overview_max = _overview_width + overview_left;
  1361. translate = function() {
  1362. // shared references
  1363. var _min_pixel = parent.min_pixel();
  1364. var _max_pixel = parent.max_pixel();
  1365. var _center_pixel = parent.center_pixel();
  1366. var _real_min_pixel = _min_pixel - _half_width;
  1367. var _real_max_pixel = _max_pixel + _half_width;
  1368. // calculate: offset
  1369. var _offset;
  1370. if (_center_pixel < _max_pixel) {
  1371. _offset = overview_left;
  1372. if (_center_pixel > _min_pixel) {
  1373. var _pixel_range = _real_max_pixel - _real_min_pixel;
  1374. _offset +=
  1375. (_center_pixel - _real_min_pixel) / _pixel_range * _overview_width;
  1376. _offset -= (viewer_width / 2);
  1377. _offset = Math.round(_offset);
  1378. } // else: center_pixel == min_pixel
  1379. } else { // center_pixel == max_pixel
  1380. _offset = _overview_max - viewer_width;
  1381. }
  1382. //XXX: correct out-of-bounds
  1383. _offset = Math.min((_overview_max - viewer_width), _offset);
  1384. _offset = Math.max(overview_left, _offset);
  1385. // position view and set width
  1386. preview.style.left = (overview_left-_offset)+"px";
  1387. _viewer.style.left = _offset+"px";
  1388. if (viewer_width < NAV_SCROLL_MIN_WIDTH) {
  1389. _offset -= NAV_SCROLL_MIN_WIDTH - viewer_width;
  1390. }
  1391. _slider.style.left =
  1392. Math.max(overview_left, _offset)+"px";
  1393. };
  1394. return translate();
  1395. }
  1396. this.translate = translate;
  1397. // DOM Event Methods
  1398. // --- Left/Right Navigation
  1399. function nav_left_mousedown(e) {
  1400. nav_right[0].className = "nav-right-unsel nav-sel";
  1401. nav_left[0].className = "nav-left-sel";
  1402. addEvent(document, "mouseup", nav_left_mouseup);
  1403. broker.publish("/chart/draw/start"); // start draw loop
  1404. broker.publish("/chart/scroll/start", 1); // start scroll loop
  1405. }
  1406. function nav_left_mouseup(e) {
  1407. nav_right[0].className = "nav-right-unsel nav-unsel";
  1408. nav_left[0].className = "nav-left-unsel";
  1409. removeEvent(document, "mouseup", nav_left_mouseup);
  1410. broker.publish("/chart/draw/stop"); // stop draw loop
  1411. broker.publish("/chart/scroll/stop"); // stop scroll loop
  1412. }
  1413. function nav_right_mousedown(e) {
  1414. nav_right[0].className = "nav-right-sel nav-sel";
  1415. addEvent(document, "mouseup", nav_right_mouseup);
  1416. broker.publish("/chart/draw/start"); // start draw loop
  1417. broker.publish("/chart/scroll/start", -1); // start scroll loop
  1418. }
  1419. function nav_right_mouseup(e) {
  1420. nav_right[0].className = "nav-right-unsel nav-unsel";
  1421. removeEvent(document, "mouseup", nav_right_mouseup);
  1422. broker.publish("/chart/draw/stop"); // stop draw loop
  1423. broker.publish("/chart/scroll/stop"); // stop scroll loop
  1424. }
  1425. // --- Preview Pane
  1426. function drag_init(e) {
  1427. e = e ? e : window.event;
  1428. addEvent(document, "mousemove", drag_move);
  1429. viewer._drag_x = e.clientX;
  1430. addEvent(document, "mouseup", drag_end);
  1431. broker.publish("/chart/draw/start"); // start draw loop
  1432. }
  1433. function drag_move(e) {
  1434. e = e ? e : window.event;
  1435. var delta = e.clientX - viewer._drag_x;
  1436. if (Math.abs(delta) <= 1) {
  1437. return cancelEvent(e);
  1438. }
  1439. viewer._drag_x = e.clientX;
  1440. delta = -1 * Math.floor(drag_modifier * delta);
  1441. broker.publish("/chart/translate", delta);
  1442. return cancelEvent(e);
  1443. }
  1444. function drag_end(e) {
  1445. removeEvent(document, "mousemove", drag_move);
  1446. removeEvent(document, "mouseup", drag_end);
  1447. broker.publish("/chart/draw/stop"); // stop draw loop
  1448. }
  1449. // Preview Range Control
  1450. function container_mouseover(e) {
  1451. __is_hover__ = true;
  1452. range_min.css("display", "inline");
  1453. if (viewer.width() > NAV_RANGE_CENTER_THRESHOLD) {
  1454. range_max.css("display", "inline");
  1455. }
  1456. }
  1457. function container_mouseout(e) {
  1458. __is_hover__ = false;
  1459. range_min.css("display", "none");
  1460. range_max.css("display", "none");
  1461. }
  1462. function range_min_drag_init(e) {
  1463. __is_drag_min__ = true;
  1464. _range_drag_init(e);
  1465. }
  1466. function range_max_drag_init(e) {
  1467. __is_drag_min__ = false;
  1468. _range_drag_init(e);
  1469. }
  1470. function _range_drag_init(e) {
  1471. e = e ? e : window.event;
  1472. addEvent(document, "mousemove", range_drag_move);
  1473. viewer._range_x = e.clientX;
  1474. addEvent(document, "mouseup", range_drag_end);
  1475. broker.publish("/chart/draw/start"); // start draw loop
  1476. }
  1477. function update_range(modified, fixed) {
  1478. // static variables
  1479. var _overview_width = overview.width-2;
  1480. var _zoom_schema = model.zoom_schema();
  1481. var _visible_panes = parent.width() / parent._pane_width();
  1482. var _half_width = Math.floor(parent.width() / 2);
  1483. update_range = function(modified, fixed) {
  1484. // sanitize parameters
  1485. var _min = Math.min(modified, fixed);
  1486. var _max = Math.max(modified, fixed);
  1487. var _min_fixed = _min === fixed ? true : false;
  1488. var _rel_min = Math.max(0, _min - overview_left);
  1489. var _rel_max = Math.min(_overview_width, _max - overview_left);
  1490. // calculate new time
  1491. var _rel_range = (_rel_max - _rel_min) / _overview_width;
  1492. var _time_range = Math.floor(model._time_range() * _rel_range);
  1493. // determine new zoom/period
  1494. var _zoom_level, _zoom_scale;
  1495. for (var i=0; i<_zoom_schema.length; i++) {
  1496. _zoom_scale = i;
  1497. _zoom_level = _zoom_schema[_zoom_scale];
  1498. if ((_zoom_level[0] * _zoom_level[2] * _visible_panes) >= _time_range) {
  1499. break;
  1500. }
  1501. }
  1502. var _pane_period, _zoom_time_range;
  1503. for (i=_zoom_level[1]; i<=_zoom_level[2]; i++) {
  1504. _pane_period = i;
  1505. _zoom_time_range = _zoom_level[0] * i * _visible_panes;
  1506. if (_zoom_time_range >= _time_range) {
  1507. break;
  1508. }
  1509. }
  1510. var _half_time = _zoom_time_range / 2;
  1511. var _time_offset;
  1512. if (_min_fixed) {
  1513. _time_offset = model.min_time();
  1514. _time_offset += model._time_range() * (_rel_min / _overview_width);
  1515. _time_offset += _half_time;
  1516. } else {
  1517. _time_offset = model.min_time();
  1518. _time_offset += model._time_range() * (_rel_max / _overview_width);
  1519. _time_offset -= _half_time;
  1520. }
  1521. _time_offset = Math.floor(_time_offset);
  1522. parent._update_zoom(_zoom_scale, _pane_period);
  1523. parent.center(_time_offset, _half_width);
  1524. };
  1525. return update_range(modified, fixed);
  1526. }
  1527. this.update_range = update_range;
  1528. function range_drag_move(e) {
  1529. e = e ? e : window.event;
  1530. if (Math.abs(e.clientX - viewer._range_x) <= 1) {
  1531. return cancelEvent(e);
  1532. }
  1533. viewer._range_x = e.clientX;
  1534. var _viewer_left = parseInt(_viewer.style.left, 10);
  1535. var delta;
  1536. //XXX: use a global to switch, because events can be queued up
  1537. if (__is_drag_min__) { // dragging range min
  1538. delta = viewer.offset().left - e.clientX;
  1539. var _new_min = _viewer_left-delta;
  1540. var _max = _viewer_left+viewer.width();
  1541. if (_new_min > _max) {
  1542. __is_drag_min__ = false;
  1543. return cancelEvent(e);
  1544. }
  1545. broker.publish("/nav/range/update", _new_min, _max);
  1546. } else { // draggging range max
  1547. var _viewer_width = viewer.width();
  1548. delta = (viewer.offset().left+_viewer_width) - e.clientX;
  1549. var _new_max = _viewer_left+_viewer_width-delta;
  1550. if (_new_max < _viewer_left) {
  1551. __is_drag_min__ = true;
  1552. return cancelEvent(e);
  1553. }
  1554. broker.publish("/nav/range/update", _new_max, _viewer_left);
  1555. }
  1556. return cancelEvent(e);
  1557. }
  1558. function range_drag_end(e) {
  1559. removeEvent(document, "mousemove", range_drag_move);
  1560. removeEvent(document, "mouseup", range_drag_end);
  1561. broker.publish("/chart/draw/stop"); // stop draw loop
  1562. }
  1563. };
  1564. // ---------- Chart ----------
  1565. var LARGE_CHART_THRESHOLD_WIDTH = 720; // target: 720px x 6 panes @ 120px
  1566. var LARGE_CHART_NUM_PANES = 6;
  1567. var SMALL_CHART_NUM_PANES = 3; // target: 180px x 3 panes @ 60px
  1568. var BORDER_OFFSET_X = 1;
  1569. var BORDER_OFFSET_Y = 1;
  1570. var TOP_PANE_HEIGHT = 25;
  1571. var TOP_PANE_BORDER_COLOR = "rgb(175,175,175)"; //XXX: consider: 150
  1572. var UPDATE_DELTA_FACTOR = 0.5; //XXX: speed at which updates will improve
  1573. var UPDATE_BOUND = 1000 / 25; //XXX: lower bound on re-draw interval
  1574. var UPDATE_AFTER_STOP = 1000 * 0.5; //XXX: allow a second between stop/start drag
  1575. if ($.browser.msie) { // IE6 needs help
  1576. UPDATE_BOUND = 4 * 1000;
  1577. UPDATE_AFTER_STOP = 1000;
  1578. }
  1579. var Y_SCALE_FACTOR = 1.10;
  1580. var Y_LABELS_WIDTH = 100;
  1581. var Y_LABELS_RIGHT_OFFSET = 4;
  1582. var Y_LABEL_OFFSET = -12;
  1583. var Y_LABEL = "W"; //XXX: make this a parameter
  1584. var MOUSE_SCROLL_ZOOM_FACTOR = 0.5;
  1585. var MOUSE_SCROLL_ZOOM_DELTA_MAX = 2;
  1586. if ($.browser.msie) { // IE6 needs help
  1587. MOUSE_SCROLL_ZOOM_FACTOR = 0.75;
  1588. }
  1589. var TICK_MARK_INCREMENTS = [
  1590. 60,
  1591. 60*5,
  1592. 60*10,
  1593. 60*15,
  1594. 60*30,
  1595. 60*60,
  1596. 60*60*3,
  1597. 60*60*6,
  1598. 60*60*12,
  1599. 60*60*24,
  1600. 60*60*24*7,
  1601. 60*60*24*14,
  1602. 60*60*24*31
  1603. ];
  1604. //FIXME: take into account: Date.getTimezoneOffset() [returns offset in minutes]
  1605. var TICK_DATE_PROXY = new Date();
  1606. var _format_date = function(date_) {
  1607. if (date_.getSeconds() !== 0) {
  1608. return formatDate(date_, "h:mm:ss a");
  1609. }
  1610. if (date_.getMinutes() !== 0) {
  1611. return formatDate(date_, "h:mm a");
  1612. }
  1613. if (date_.getHours() !== 0) {
  1614. return formatDate(date_, "h a");
  1615. }
  1616. if (date_.getDay() !== 1) {
  1617. return formatDate(date_, "MMM d");
  1618. }
  1619. if (date_.getMonth() !== 0) {
  1620. return formatDate(date_, "MMM");
  1621. }
  1622. return formatDate(date_, "yyyy");
  1623. };
  1624. ;;; $c._format_date = _format_date;
  1625. var TICK_1_MINUTE = function(timestamp_) {
  1626. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1627. if (TICK_DATE_PROXY.getSeconds() !== 0) {
  1628. TICK_DATE_PROXY.setSeconds(60);
  1629. }
  1630. return [
  1631. (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
  1632. _format_date(TICK_DATE_PROXY)
  1633. ];
  1634. };
  1635. ;;; $c.TICK_1_MINUTE = TICK_1_MINUTE;
  1636. var TICK_10_MINUTES = function(timestamp_) {
  1637. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1638. var _min = TICK_DATE_PROXY.getMinutes();
  1639. var _min_mod = _min % 10;
  1640. if (TICK_DATE_PROXY.getSeconds() !== 0 || _min_mod !== 0) {
  1641. TICK_DATE_PROXY.setSeconds(0);
  1642. TICK_DATE_PROXY.setMinutes(_min + (10 - _min_mod));
  1643. }
  1644. return [
  1645. (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
  1646. _format_date(TICK_DATE_PROXY)
  1647. ];
  1648. };
  1649. ;;; $c.TICK_10_MINUTES = TICK_10_MINUTES;
  1650. var TICK_5_MINUTES = function(timestamp_) {
  1651. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1652. var _min = TICK_DATE_PROXY.getMinutes();
  1653. var _min_mod = _min % 5;
  1654. if (TICK_DATE_PROXY.getSeconds() !== 0 || _min_mod !== 0) {
  1655. TICK_DATE_PROXY.setSeconds(0);
  1656. TICK_DATE_PROXY.setMinutes(_min + (5 - _min_mod));
  1657. }
  1658. return [
  1659. (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
  1660. _format_date(TICK_DATE_PROXY)
  1661. ];
  1662. };
  1663. ;;; $c.TICK_5_MINUTES = TICK_5_MINUTES;
  1664. var TICK_15_MINUTES = function(timestamp_) {
  1665. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1666. var _min = TICK_DATE_PROXY.getMinutes();
  1667. var _min_mod = _min % 15;
  1668. if (TICK_DATE_PROXY.getSeconds() !== 0 || _min_mod !== 0) {
  1669. TICK_DATE_PROXY.setSeconds(0);
  1670. TICK_DATE_PROXY.setMinutes(_min + (15 - _min_mod));
  1671. }
  1672. return [
  1673. (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
  1674. _format_date(TICK_DATE_PROXY)
  1675. ];
  1676. };
  1677. ;;; $c.TICK_15_MINUTES = TICK_15_MINUTES;
  1678. var TICK_30_MINUTES = function(timestamp_) {
  1679. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1680. var _min = TICK_DATE_PROXY.getMinutes();
  1681. var _min_mod = _min % 30;
  1682. if (TICK_DATE_PROXY.getSeconds() !== 0 || _min_mod !== 0) {
  1683. TICK_DATE_PROXY.setSeconds(0);
  1684. TICK_DATE_PROXY.setMinutes(_min + (30 - _min_mod));
  1685. }
  1686. return [
  1687. (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
  1688. _format_date(TICK_DATE_PROXY)
  1689. ];
  1690. };
  1691. ;;; $c.TICK_30_MINUTES = TICK_30_MINUTES;
  1692. var TICK_1_HOUR = function(timestamp_) {
  1693. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1694. if (TICK_DATE_PROXY.getSeconds() !== 0 || TICK_DATE_PROXY.getMinutes() !== 0) {
  1695. TICK_DATE_PROXY.setSeconds(0);
  1696. TICK_DATE_PROXY.setMinutes(0);
  1697. TICK_DATE_PROXY.setHours(TICK_DATE_PROXY.getHours() + 1);
  1698. }
  1699. return [
  1700. (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
  1701. _format_date(TICK_DATE_PROXY)
  1702. ];
  1703. };
  1704. ;;; $c.TICK_1_HOUR = TICK_1_HOUR;
  1705. var TICK_3_HOURS = function(timestamp_) {
  1706. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1707. var _hour = TICK_DATE_PROXY.getHours();
  1708. var _hour_mod = _hour % 3;
  1709. if (TICK_DATE_PROXY.getSeconds() !== 0 || TICK_DATE_PROXY.getMinutes() !== 0 || _hour_mod !== 0) {
  1710. TICK_DATE_PROXY.setSeconds(0);
  1711. TICK_DATE_PROXY.setMinutes(0);
  1712. TICK_DATE_PROXY.setHours(_hour + (3 - _hour_mod));
  1713. }
  1714. return [
  1715. (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
  1716. _format_date(TICK_DATE_PROXY)
  1717. ];
  1718. };
  1719. ;;; $c.TICK_3_HOURS = TICK_3_HOURS;
  1720. var TICK_6_HOURS = function(timestamp_) {
  1721. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1722. var _hour = TICK_DATE_PROXY.getHours();
  1723. var _hour_mod = _hour % 6;
  1724. if (TICK_DATE_PROXY.getSeconds() !== 0 || TICK_DATE_PROXY.getMinutes() !== 0 || _hour_mod !== 0) {
  1725. TICK_DATE_PROXY.setSeconds(0);
  1726. TICK_DATE_PROXY.setMinutes(0);
  1727. TICK_DATE_PROXY.setHours(_hour + (6 - _hour_mod));
  1728. }
  1729. return [
  1730. (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
  1731. _format_date(TICK_DATE_PROXY)
  1732. ];
  1733. };
  1734. ;;; $c.TICK_6_HOURS = TICK_6_HOURS;
  1735. var TICK_12_HOURS = function(timestamp_) {
  1736. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1737. var _hour = TICK_DATE_PROXY.getHours();
  1738. var _hour_mod = _hour % 12;
  1739. if (TICK_DATE_PROXY.getSeconds() !== 0 || TICK_DATE_PROXY.getMinutes() !== 0 || _hour_mod !== 0) {
  1740. TICK_DATE_PROXY.setSeconds(0);
  1741. TICK_DATE_PROXY.setMinutes(0);
  1742. TICK_DATE_PROXY.setHours(_hour + (12 - _hour_mod));
  1743. }
  1744. return [
  1745. (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
  1746. _format_date(TICK_DATE_PROXY)
  1747. ];
  1748. };
  1749. ;;; $c.TICK_12_HOURS = TICK_12_HOURS;
  1750. var TICK_1_DAY = function(timestamp_) {
  1751. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1752. if (TICK_DATE_PROXY.getSeconds() !== 0 || TICK_DATE_PROXY.getMinutes() !== 0 || TICK_DATE_PROXY.getHours() !== 0) {
  1753. TICK_DATE_PROXY.setSeconds(0);
  1754. TICK_DATE_PROXY.setMinutes(0);
  1755. TICK_DATE_PROXY.setHours(24);
  1756. }
  1757. return [
  1758. (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
  1759. _format_date(TICK_DATE_PROXY)
  1760. ];
  1761. };
  1762. ;;; $c.TICK_1_DAY = TICK_1_DAY;
  1763. var WEEK_IN_SECS = 60*60*24*7;
  1764. var TICK_1_WEEK = function(timestamp_) {
  1765. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1766. //XXX: calc from start of year
  1767. TICK_DATE_PROXY.setSeconds(0);
  1768. TICK_DATE_PROXY.setMinutes(0);
  1769. TICK_DATE_PROXY.setHours(0);
  1770. TICK_DATE_PROXY.setMonth(0, 1);
  1771. var _year_start = TICK_DATE_PROXY.getTime() / 1000;
  1772. var _delta = timestamp_ - _year_start;
  1773. return [
  1774. WEEK_IN_SECS - (_delta % WEEK_IN_SECS),
  1775. _format_date(TICK_DATE_PROXY)
  1776. ];
  1777. };
  1778. ;;; $c.TICK_1_WEEK = TICK_1_WEEK;
  1779. var TWO_WEEKS_IN_SECS = 2*WEEK_IN_SECS;
  1780. var TICK_2_WEEKS = function(timestamp_) {
  1781. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1782. //XXX: calc from start of year
  1783. TICK_DATE_PROXY.setSeconds(0);
  1784. TICK_DATE_PROXY.setMinutes(0);
  1785. TICK_DATE_PROXY.setHours(0);
  1786. TICK_DATE_PROXY.setMonth(0, 1);
  1787. var _year_start = TICK_DATE_PROXY.getTime() / 1000;
  1788. var _delta = timestamp_ - _year_start;
  1789. return [
  1790. TWO_WEEKS_IN_SECS - (_delta % TWO_WEEKS_IN_SECS),
  1791. _format_date(TICK_DATE_PROXY)
  1792. ];
  1793. };
  1794. ;;; $c.TICK_2_WEEKS = TICK_2_WEEKS;
  1795. var TICK_1_MONTH = function(timestamp_) {
  1796. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1797. if (TICK_DATE_PROXY.getSeconds() !== 0 || TICK_DATE_PROXY.getMinutes() !== 0 || TICK_DATE_PROXY.getHours() !== 0 || TICK_DATE_PROXY.getDay() !== 1 || TICK_DATE_PROXY.getMonth() !== 0) {
  1798. TICK_DATE_PROXY.setSeconds(0);
  1799. TICK_DATE_PROXY.setMinutes(0);
  1800. TICK_DATE_PROXY.setHours(0);
  1801. TICK_DATE_PROXY.setMonth(TICK_DATE_PROXY.getMonth()+1, 1);
  1802. }
  1803. return [
  1804. (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
  1805. _format_date(TICK_DATE_PROXY)
  1806. ];
  1807. };
  1808. ;;; $c.TICK_1_MONTH = TICK_1_MONTH;
  1809. var TICK_OVERFLOW = function(timestamp_) {
  1810. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1811. return [
  1812. 0,
  1813. _format_date(TICK_DATE_PROXY)
  1814. ];
  1815. };
  1816. ;;; $c.TICK_OVERFLOW = TICK_OVERFLOW;
  1817. var TICK_MARK_SELECTORS = {};
  1818. TICK_MARK_SELECTORS[60] = TICK_1_MINUTE;
  1819. TICK_MARK_SELECTORS[60*5] = TICK_5_MINUTES;
  1820. TICK_MARK_SELECTORS[60*10] = TICK_10_MINUTES;
  1821. TICK_MARK_SELECTORS[60*15] = TICK_15_MINUTES;
  1822. TICK_MARK_SELECTORS[60*30] = TICK_30_MINUTES;
  1823. TICK_MARK_SELECTORS[60*60] = TICK_1_HOUR;
  1824. TICK_MARK_SELECTORS[60*60*3] = TICK_3_HOURS;
  1825. TICK_MARK_SELECTORS[60*60*6] = TICK_6_HOURS;
  1826. TICK_MARK_SELECTORS[60*60*12] = TICK_12_HOURS;
  1827. TICK_MARK_SELECTORS[60*60*24] = TICK_1_DAY;
  1828. TICK_MARK_SELECTORS[60*60*24*7] = TICK_1_WEEK;
  1829. TICK_MARK_SELECTORS[60*60*24*14] = TICK_2_WEEKS;
  1830. TICK_MARK_SELECTORS[60*60*24*31] = TICK_1_MONTH;
  1831. ;;; $c.TICK_MARK_SELECTORS = TICK_MARK_SELECTORS;
  1832. var _bisect = function(list_, target_) {
  1833. //XXX: warning: returns list_.length, if target_ higher than last element
  1834. var lo = 0;
  1835. var hi = list_.length;
  1836. while (lo < hi) {
  1837. var mid = Math.floor((lo + hi) / 2);
  1838. if (list_[mid] < target_) {
  1839. lo = mid+1;
  1840. } else {
  1841. hi = mid;
  1842. }
  1843. }
  1844. return lo;
  1845. };
  1846. ;;; $c._bisect = _bisect;
  1847. var ZOOM_POINTS = [
  1848. [60*5, '5m'],
  1849. [60*30, '30m'],
  1850. [60*60, '1h'],
  1851. [60*60*6, '6h'],
  1852. [60*60*12, '12h'],
  1853. [60*60*24, '1d'],
  1854. [60*60*24*7, '1w'],
  1855. [60*60*24*7*2, '2w'],
  1856. [60*60*24*31, '1M'],
  1857. [60*60*24*31*3, '3M']
  1858. // max
  1859. ];
  1860. var Chart = function(target_) {
  1861. // Initialize
  1862. var
  1863. target = $(target_),
  1864. model = null,
  1865. container = null,
  1866. y_labels = null,
  1867. width = 0,
  1868. max_left = 0,
  1869. max_right = 0,
  1870. height = 0,
  1871. top_pane = null,
  1872. _top_range = null,
  1873. _top_range_span = null,
  1874. _top_point = null,
  1875. _top_point_time = null,
  1876. _top_point_val = null,
  1877. _overlay_line = null,
  1878. _is_mouseover = false,
  1879. zoom_scale = 0, // zoom schema index [1m, 5m, 30m, 3h, 12h]
  1880. _pane_period = 1,
  1881. _pane_width = 0,
  1882. max_y = 0,
  1883. min_y = 0,
  1884. _x_scale = 0,
  1885. _x_tick = TICK_OVERFLOW,
  1886. _y_scale = 0,
  1887. _y_intervals = [],
  1888. panes = [],
  1889. bg = null,
  1890. draw_width = 0,
  1891. draw_height = 0,
  1892. chart_height = 0,
  1893. broker = new PubSubBroker(),
  1894. updating = false,
  1895. update_delta = 0,
  1896. update_id = undefined,
  1897. update_time = 0,
  1898. center_pixel = 0,
  1899. min_pixel = 0,
  1900. max_pixel = 0,
  1901. nav = null,
  1902. scroll_rate = 0,
  1903. scrolling = false,
  1904. scroll_id = 0;
  1905. this.target = function() { return target; };
  1906. this.model = function() { return model; };
  1907. this.container = function() { return container; };
  1908. this.y_labels = function() { return y_labels; };
  1909. this.width = function() { return width; };
  1910. this.max_left = function() { return max_left; };
  1911. this.max_right = function() { return max_right; };
  1912. this.height = function() { return height; };
  1913. this.top_pane = function() { return top_pane; };
  1914. this.zoom_scale = function() { return zoom_scale; };
  1915. this._pane_period = function() { return _pane_period; };
  1916. this._pane_width = function() { return _pane_width; };
  1917. this.max_y = function() { return max_y; };
  1918. this.min_y = function() { return min_y; };
  1919. this._x_scale = function() { return _x_scale; };
  1920. this._x_tick = function() { return _x_tick; };
  1921. this._y_scale = function() { return _y_scale; };
  1922. this._y_intervals = function() { return _y_intervals; };
  1923. this.panes = function() { return panes; };
  1924. this.bg = function() { return bg; };
  1925. this.draw_width = function() { return draw_width; };
  1926. this.draw_height = function() { return draw_height; };
  1927. this.chart_height = function() { return chart_height; };
  1928. this.broker = function() { return broker; };
  1929. this.updating = function() { return updating; };
  1930. this.update_delta = function() { return update_delta; };
  1931. this.update_id = function() { return update_id; };
  1932. this.update_time = function() { return update_time; };
  1933. this.center_pixel = function() { return center_pixel; };
  1934. this.min_pixel = function() { return min_pixel; };
  1935. this.max_pixel = function() { return max_pixel; };
  1936. this.nav = function() { return nav; };
  1937. this.scroll_id = function() { return scroll_id; };
  1938. /* Broker Channels:
  1939. /chart/center (re-)center chart
  1940. /chart/translate translate chart by delta
  1941. /chart/recycle recycle panes
  1942. /chart/replace replace panes
  1943. /chart/draw/start start draw loop
  1944. /chart/draw/stop stop draw loop
  1945. /chart/draw/rescale rescale y-axis
  1946. /chart/scroll/start start scroll loop
  1947. /chart/scroll/stop stop scroll loop
  1948. /info/range/update update range (in top pane)
  1949. /info/point/update update point (in top pane)
  1950. */
  1951. // Helper
  1952. function _chart_period() {
  1953. //XXX: consider: better memoization
  1954. var _visible_panes = width / _pane_width;
  1955. var map = {};
  1956. for (var i=_pane_width; i>0; i--) {
  1957. map[i] = _visible_panes * i;
  1958. }
  1959. _chart_period = function() {
  1960. return map[_pane_period];
  1961. };
  1962. return _chart_period();
  1963. }
  1964. this._chart_period = _chart_period;
  1965. function _calc_x_scale() {
  1966. // x-coord scale
  1967. _x_scale = _pane_width / _pane_period;
  1968. var _idx = _bisect(TICK_MARK_INCREMENTS, model.zoom_delta() * _pane_period);
  1969. var _tick_func = TICK_MARK_SELECTORS[TICK_MARK_INCREMENTS[_idx]];
  1970. if (_tick_func) {
  1971. _x_tick = _tick_func;
  1972. } else {
  1973. _x_tick = TICK_OVERFLOW;
  1974. }
  1975. }
  1976. this._calc_x_scale = _calc_x_scale;
  1977. function _min_max_y() {
  1978. var min, max, _min_max, _pane, _pane_offset;
  1979. //XXX: can be memoized
  1980. var _pane_idx = panes.length-1;
  1981. while (_pane_idx >= 0) {
  1982. _pane = panes[_pane_idx--];
  1983. _pane_offset = _pane.offset();
  1984. if (_pane_offset <= width) {
  1985. // last visible pane (partial)
  1986. _min_max = _pane.range_min_max_y(0, Math.ceil(((width - _pane_offset) / _pane_width) * _pane_period));
  1987. min = _min_max[0];
  1988. max = _min_max[1];
  1989. break;
  1990. }
  1991. }
  1992. for (var i=_pane_idx; i>=0; i--) {
  1993. _pane = panes[i];
  1994. _pane_offset = _pane.offset();
  1995. if (_pane_offset < width) {
  1996. if (_pane_offset > 0) {
  1997. min = Math.min(min, _pane._min_y());
  1998. max = Math.max(max, _pane._max_y());
  1999. } else if (_pane_offset > -_pane_width) { // first pane (partial)
  2000. _min_max = _pane.range_min_max_y(Math.floor((-_pane_offset / _pane_width) * _pane_period), _pane_period);
  2001. min = Math.min(min, _min_max[0]);
  2002. max = Math.max(max, _min_max[1]);
  2003. }
  2004. }
  2005. }
  2006. return [min, max];
  2007. }
  2008. this._min_max_y = _min_max_y;
  2009. var MATH_LOG_10 = Math.log(10);
  2010. function _calc_y_scale() {
  2011. //XXX: assumption: min-y == 0
  2012. // Reset Y Labels
  2013. y_labels.empty();
  2014. // Calculate y-coord
  2015. var _power = Math.pow(10, Math.floor(Math.log(max_y) / MATH_LOG_10));
  2016. var _scale, _num_lines;
  2017. if (max_y < (2 * _power)) {
  2018. _scale = 0.5 * _power;
  2019. _num_lines = 4;
  2020. } else if (max_y < (3 * _power)) {
  2021. _scale = _power;
  2022. _num_lines = 3;
  2023. } else if (max_y < (4 * _power)) {
  2024. _scale = _power;
  2025. _num_lines = 4;
  2026. } else if (max_y < (5 * _power)) {
  2027. _scale = _power;
  2028. _num_lines = 5;
  2029. } else if (max_y < (6 * _power)) {
  2030. _scale = 2 * _power;
  2031. _num_lines = 3;
  2032. } else if (max_y < (8 * _power)) {
  2033. _scale = 2 * _power;
  2034. _num_lines = 4;
  2035. } else { // < 10 * _power
  2036. _scale = 2 * _power;
  2037. _num_lines = 5;
  2038. }
  2039. var _scaled_range = _num_lines * _scale * Y_SCALE_FACTOR;
  2040. // y-coord scale
  2041. _y_scale = chart_height / _scaled_range;
  2042. // y-coord grid line intervals @ y-translation
  2043. _y_intervals = [];
  2044. for (var i=_num_lines;i>0;i--) {
  2045. var _y = i*_scale;
  2046. var _y_scaled = _y * _y_scale;
  2047. _y_intervals.push(_y_scaled);
  2048. y_labels.append([
  2049. '<div',
  2050. 'class="y-tick"',
  2051. 'style="',
  2052. 'position: absolute;',
  2053. 'left: 0px;',
  2054. 'top: '+(chart_height-_y_scaled+Y_LABEL_OFFSET)+'px;',
  2055. 'z-index: 0;',
  2056. 'width: '+Y_LABELS_WIDTH+'px;',
  2057. '">'+_y+' '+Y_LABEL+'</div>'].join(' '));
  2058. }
  2059. }
  2060. this._calc_y_scale = _calc_y_scale;
  2061. function _rescale_y_axis() {
  2062. _calc_y_scale();
  2063. broker.publish("/pane/redraw");
  2064. }
  2065. this._rescale_y_axis = _rescale_y_axis;
  2066. // Setup
  2067. function init() {
  2068. // Preprocess: calculate pane width and zoom schema
  2069. width = target.width();
  2070. height = target.height() - NAV_HEIGHT;
  2071. var _zoom_schema = _ZOOM_SCHEMA_STANDARD;
  2072. if (width > LARGE_CHART_THRESHOLD_WIDTH) {
  2073. _pane_width = Math.floor(width/LARGE_CHART_NUM_PANES);
  2074. if ($.browser.msie) { // reduce granularity
  2075. _zoom_schema = _ZOOM_SCHEMA_RELAXED;
  2076. }
  2077. } else { // small chart
  2078. _pane_width = Math.floor(width/SMALL_CHART_NUM_PANES);
  2079. _zoom_schema = _ZOOM_SCHEMA_HALF;
  2080. }
  2081. // Model
  2082. model = new Model(this, _zoom_schema);
  2083. //FIXME: pass these in
  2084. model.init(1243411200, 1243411200+4*LineDataCache.prototype.bucket_size*60-60, width, _pane_width);
  2085. //FIXME: pass these in; maybe, via: $.extend(true, options, o)
  2086. var init_time_ = 1243411200+3*LineDataCache.prototype.bucket_size*60;
  2087. zoom_scale = 0;
  2088. _pane_period = 20;
  2089. // Chart
  2090. panes = [];
  2091. target.html("");
  2092. target.css("position", "relative");
  2093. // Background
  2094. bg = $([
  2095. '<div',
  2096. 'style="',
  2097. 'overflow: hidden;',
  2098. 'position: absolute;',
  2099. 'left: 0px;',
  2100. 'top: 0px;',
  2101. 'z-index: -1;',
  2102. 'width: '+width+'px;',
  2103. 'height: '+height+'px;',
  2104. 'border: 1px solid rgb(0,0,0);',
  2105. '"></div>'].join(' ')).appendTo(target);
  2106. // Top Pane
  2107. top_pane = $([
  2108. '<div',
  2109. 'class="top-pane"',
  2110. 'style="',
  2111. 'position: absolute;',
  2112. 'left: '+BORDER_OFFSET_X+'px;',
  2113. 'top: 0px;',
  2114. 'width: '+width+'px;',
  2115. 'height: '+TOP_PANE_HEIGHT+'px;',
  2116. 'border-bottom: 1px solid '+TOP_PANE_BORDER_COLOR+';',
  2117. '"></div>'].join(' ')).appendTo(target);
  2118. // Top Pane: Zoom
  2119. var _top_pane_zoom = $([
  2120. '<div',
  2121. 'class="top-zoom"',
  2122. '></div>'].join(' ')).appendTo(top_pane);
  2123. _top_pane_zoom.append('<strong>Zoom:</strong>');
  2124. var _max_idx = model.zoom_schema().length-1;
  2125. var _max_zoom = model.zoom_schema()[_max_idx];
  2126. var _max_delta = _max_zoom[0] * _max_zoom[2];
  2127. var _max_pt = ZOOM_POINTS.length-1;
  2128. for (; _max_pt>=0; _max_pt--) {
  2129. if (ZOOM_POINTS[_max_pt][0] < _max_delta) {
  2130. break;
  2131. }
  2132. }
  2133. _max_pt += 1;
  2134. var _visible_panes = width / _pane_width;
  2135. var _link_correlations = [];
  2136. var _link_idx = 0;
  2137. for (i=0; i<=_max_idx; i++) {
  2138. var _zoom_level = model.zoom_schema()[i];
  2139. for (var j=_zoom_level[1]; j<=_zoom_level[2]; j++) {
  2140. if ((_zoom_level[0] * j * _visible_panes) >= ZOOM_POINTS[_link_idx][0]) {
  2141. _link_correlations[_link_idx] = [i,j];
  2142. _link_idx += 1;
  2143. if (_link_idx > _max_pt) {
  2144. break;
  2145. }
  2146. }
  2147. }
  2148. }
  2149. for (i=0; i<=_max_pt; i++) {
  2150. var _zoom_point = ZOOM_POINTS[i];
  2151. var _zoom_link = $([
  2152. '<a',
  2153. 'class="zoom-link"',
  2154. 'href="#'+_zoom_point[1]+'"',
  2155. '>'+_zoom_point[1]+'</a>'].join(' ')).appendTo(_top_pane_zoom)[0];
  2156. (function(obj, zoom_idx, pane_period) {
  2157. addEvent(obj, "click", function() {
  2158. set_zoom(zoom_idx, pane_period);
  2159. });
  2160. })(_zoom_link, _link_correlations[i][0], _link_correlations[i][1]);
  2161. }
  2162. var _max_zoom_link = $([
  2163. '<a',
  2164. 'class="zoom-link"',
  2165. 'href="#max"',
  2166. '>max</a>'].join(' ')).appendTo(_top_pane_zoom)[0];
  2167. addEvent(_max_zoom_link, "click", function() {
  2168. set_zoom(_max_idx, _max_zoom[2]);
  2169. });
  2170. // Top Pane: Range
  2171. _top_range = $([
  2172. '<div',
  2173. 'class="top-right"',
  2174. '></div>'].join(' ')).appendTo(top_pane);
  2175. _top_range_span = $('<span></span>').appendTo(_top_range);
  2176. // Top Pane: Point
  2177. _top_point = $([
  2178. '<div',
  2179. 'class="top-right"',
  2180. 'style="',
  2181. 'display: none;',
  2182. '"></div>'].join(' ')).appendTo(top_pane);
  2183. _top_point_time = $('<span class="top-point-time"></span>').appendTo(_top_point);
  2184. _top_point_val = $('<span class="top-point-value"></span>').appendTo(_top_point);
  2185. // Bottom Tape
  2186. bg.append([
  2187. '<div',
  2188. 'style="',
  2189. 'position: absolute;',
  2190. 'left: 0px;',
  2191. 'top: '+(height-BOTTOM_TAPE_HEIGHT+BOTTOM_TAPE_TOP_ADJUST)+'px;',
  2192. 'width: '+width+'px;',
  2193. 'height: '+BOTTOM_TAPE_HEIGHT+'px;',
  2194. 'background-color: '+BOTTOM_TAPE_COLOR+';',
  2195. 'border: 0px;',
  2196. '"></div>'].join(' '));
  2197. // Container
  2198. draw_width = width;
  2199. draw_height = height - TOP_PANE_HEIGHT - BORDER_OFFSET_Y;
  2200. chart_height = draw_height - BOTTOM_TAPE_HEIGHT;
  2201. container = $([
  2202. '<div',
  2203. 'style="',
  2204. 'position: absolute;',
  2205. 'overflow: hidden;',
  2206. 'left: '+BORDER_OFFSET_X+'px;',
  2207. 'top: '+(TOP_PANE_HEIGHT+2*BORDER_OFFSET_Y)+'px;',
  2208. 'width: '+draw_width+'px;',
  2209. 'height: '+draw_height+'px;',
  2210. 'cursor: move;',
  2211. '"></div>'].join(' ')).appendTo(target);
  2212. // Panes
  2213. var num_panes = Math.ceil(width/_pane_width)+2;
  2214. var centered_offset = (-num_panes*_pane_width/2)-(-width/2);
  2215. max_left = centered_offset - (_pane_width/2);
  2216. max_right = centered_offset + ((num_panes - 0.5) * _pane_width);
  2217. for (i=0;i<num_panes;i++) {
  2218. var pane = new Pane(this, _pane_width);
  2219. pane.init();
  2220. panes.push(pane);
  2221. }
  2222. // Y-Labels
  2223. y_labels = $([
  2224. '<div',
  2225. 'style="',
  2226. 'overflow: hidden;',
  2227. 'position: absolute;',
  2228. 'right: '+Y_LABELS_RIGHT_OFFSET+'px;',
  2229. 'top: 0px;',
  2230. 'width: '+Y_LABELS_WIDTH+'px;',
  2231. 'height: '+chart_height+'px;',
  2232. '"></div>'].join(' ')).appendTo(container);
  2233. // Navigation Pane
  2234. nav = new NavigationPane(this);
  2235. nav.init(target, width, NAV_HEIGHT, (height+BORDER_OFFSET_Y), BORDER_OFFSET_X);
  2236. // Overlay: Line
  2237. _overlay_line = $([
  2238. '<div',
  2239. 'style="',
  2240. 'display: none;',
  2241. 'position: absolute;',
  2242. 'top: '+(TOP_PANE_HEIGHT+BORDER_OFFSET_Y)+'px;',
  2243. 'width: 1px;',
  2244. 'height: '+(chart_height+BORDER_OFFSET_Y)+'px;',
  2245. 'background-color: #666;',
  2246. '"></div>'].join(' ')).appendTo(target);
  2247. // Events Setup
  2248. // --- Pane Translation Events
  2249. broker.subscribe("/chart/center", this, "center");
  2250. broker.subscribe("/chart/translate", this, "translate");
  2251. broker.subscribe("/chart/recycle", this, "recycle");
  2252. broker.subscribe("/chart/replace", this, "replace");
  2253. // --- Draw Loop Events
  2254. broker.subscribe("/chart/draw/start", this, "draw_start");
  2255. broker.subscribe("/chart/draw/stop", this, "draw_stop");
  2256. broker.subscribe("/chart/draw/rescale", this, "_rescale_y_axis");
  2257. // --- Navigation Translate Events
  2258. broker.subscribe("/chart/scroll/start", this, "scroll_start");
  2259. broker.subscribe("/chart/scroll/stop", this, "scroll_stop");
  2260. // --- Info Display Events
  2261. broker.subscribe("/info/range/update", this, "_update_top_range");
  2262. broker.subscribe("/info/point/update", this, "_update_top_point");
  2263. // --- DOM Events
  2264. // ----- Translation
  2265. addEvent(container[0], "mousedown", drag_init);
  2266. container[0].onmousedown = function(e) { return cancelEvent(e); };
  2267. // ----- Zooming
  2268. addEvent(container[0], "dblclick", double_click);
  2269. addEvent(container[0], "mousewheel", mouse_wheel);
  2270. // ----- Cursor Tracking
  2271. container.hover(chart_mouseover, chart_mouseout);
  2272. // Draw Panes
  2273. broker.publish("/chart/center", init_time_, width/2);
  2274. }
  2275. this.init = init;
  2276. // DOM Event Methods
  2277. // ----- Translation
  2278. function drag_init(e) {
  2279. e = e ? e : window.event;
  2280. addEvent(document, "mousemove", drag_move);
  2281. container._drag_x = e.clientX;
  2282. addEvent(document, "mouseup", drag_end);
  2283. broker.publish("/chart/draw/start"); // start draw loop
  2284. removeEvent(container[0], "mousemove", cursor_move);
  2285. }
  2286. function drag_move(e) {
  2287. e = e ? e : window.event;
  2288. var delta = e.clientX - container._drag_x;
  2289. if (Math.abs(delta) <= 1) {
  2290. return cancelEvent(e);
  2291. }
  2292. container._drag_x = e.clientX;
  2293. broker.publish("/chart/translate", delta);
  2294. return cancelEvent(e);
  2295. }
  2296. function drag_end(e) {
  2297. removeEvent(document, "mousemove", drag_move);
  2298. removeEvent(document, "mouseup", drag_end);
  2299. broker.publish("/chart/draw/stop"); // stop draw loop
  2300. if (_is_mouseover) {
  2301. addEvent(container[0], "mousemove", cursor_move);
  2302. container._cursor_x = e.clientX;
  2303. }
  2304. }
  2305. // ----- Zooming
  2306. function _zoom_chart(e, delta) {
  2307. var _offset = container.offset();
  2308. var _x = e.clientX - _offset.left;
  2309. var _y = e.clientY - _offset.top;
  2310. var _pane = panes[0];
  2311. var _time_offset = _pane.time_offset() + (_x - _pane.offset()) / _x_scale * model.zoom_delta();
  2312. if (delta > 0) { // zoom in
  2313. //XXX: on zoom in, maybe make delta larger at very low zoom levels
  2314. _pane_period = Math.max(model.zoom_level()[1], _pane_period-delta);
  2315. _pane_period -= delta;
  2316. if (_pane_period < model.zoom_level()[1]) {
  2317. if (zoom_scale > 0) {
  2318. zoom_scale -= 1;
  2319. _pane_period = model.zoom_level()[2];
  2320. } else { // zoom_scale === 0
  2321. _pane_period = model.zoom_level()[1];
  2322. }
  2323. }
  2324. } else { // zoom out
  2325. _pane_period -= delta;
  2326. if (_pane_period > model.zoom_level()[2]) {
  2327. if (zoom_scale < (model.zoom_schema().length-1)) {
  2328. zoom_scale += 1;
  2329. _pane_period = model.zoom_level()[1];
  2330. } else { // _zoom_scale === (_zoom_schema.length-1)
  2331. _pane_period = model.zoom_level()[2];
  2332. }
  2333. }
  2334. }
  2335. broker.publish("/chart/center", _time_offset, _x);
  2336. }
  2337. function double_click(e) {
  2338. //FIXME: may not be cross-browser compatible; doesn't handle triple-clicks
  2339. e = e ? e : window.event;
  2340. _zoom_chart(e, Math.max(1, Math.floor(_pane_period * 0.25)));
  2341. }
  2342. function mouse_wheel(e) {
  2343. //XXX: while zooming, maintain time position at mouse pointer
  2344. e = e ? e : window.event;
  2345. //FIXME? Opera uses -wheelDelta; what about Chrome?
  2346. var delta = e.detail ? -e.detail : e.wheelDelta / 40;
  2347. if (Math.abs(delta) > 0) {
  2348. delta = Math.min(Math.round(delta * MOUSE_SCROLL_ZOOM_FACTOR), MOUSE_SCROLL_ZOOM_DELTA_MAX);
  2349. _zoom_chart(e, delta);
  2350. }
  2351. return cancelEvent(e);
  2352. }
  2353. // ----- Cursor Tracking
  2354. function chart_mouseover(e) {
  2355. _is_mouseover = true;
  2356. e = e ? e : window.event;
  2357. addEvent(container[0], "mousemove", cursor_move);
  2358. container._cursor_x = e.clientX;
  2359. }
  2360. function chart_mouseout(e) {
  2361. _is_mouseover = false;
  2362. removeEvent(container[0], "mousemove", cursor_move);
  2363. }
  2364. function cursor_move(e) {
  2365. e = e ? e : window.event;
  2366. if (Math.abs(e.clientX - container._cursor_x) <= 1) {
  2367. return e;
  2368. }
  2369. container._cursor_x = e.clientX;
  2370. _update_top_point(e.clientX - (container.offset().left + width/2));
  2371. return e;
  2372. }
  2373. // Chart Translation Methods
  2374. function center(time_offset_, pixel_offset_) {
  2375. // determine: x-coord scale
  2376. _calc_x_scale();
  2377. // shared references
  2378. var _zoom_delta = model.zoom_delta();
  2379. var _half_width = width / 2;
  2380. // calculate: plot offsets
  2381. pixel_offset_ = Math.floor(pixel_offset_);
  2382. var _point_offset =
  2383. Math.floor((time_offset_ - model.min_time()) / _zoom_delta);
  2384. //XXX: special case: plot span w/in chart width
  2385. var _plot_points = Math.floor(model._time_range() / _zoom_delta);
  2386. if (_plot_points <= _chart_period()) {
  2387. _point_offset =
  2388. Math.floor(_plot_points / 2); //XXX: last pixel is always length+1
  2389. pixel_offset_ = Math.floor(_half_width);
  2390. // chart center pixel tracking
  2391. min_pixel = max_pixel = center_pixel =
  2392. Math.round((_point_offset * _x_scale) + _half_width - pixel_offset_);
  2393. } else {
  2394. var _width_left =
  2395. (((model.max_time() - time_offset_) / _zoom_delta) - 1) * _x_scale;
  2396. if ((width - pixel_offset_) > _width_left) {
  2397. //XXX: special case: plot end w/in chart width
  2398. pixel_offset_ = Math.floor(width - _width_left);
  2399. } else if (pixel_offset_ > (_point_offset * _x_scale)) {
  2400. //XXX: special case: plot start w/in chart start
  2401. pixel_offset_ = Math.floor(_point_offset * _x_scale);
  2402. }
  2403. // chart center pixel tracking
  2404. min_pixel = Math.ceil(_half_width);
  2405. var _num_points =
  2406. (model._time_range() / _zoom_delta) - 1; //XXX: last pixel is always length+1
  2407. max_pixel = Math.floor((_num_points * _x_scale) - _half_width);
  2408. center_pixel = Math.round((_point_offset * _x_scale) + _half_width - pixel_offset_);
  2409. }
  2410. // calculate: pane reset
  2411. var len = panes.length;
  2412. var pane_pixel = width + (pixel_offset_ % _pane_width);
  2413. var pane_point =
  2414. _point_offset + (len - Math.floor(pixel_offset_ / _pane_width) - 2) * _pane_period;
  2415. // reset panes
  2416. var i = len;
  2417. while (i--) {
  2418. var pane = panes[i];
  2419. pane._reset(pane_point, pane_pixel);
  2420. pane_point -= _pane_period;
  2421. pane_pixel -= _pane_width;
  2422. }
  2423. // determine: y-coord scale
  2424. var min_max_y = _min_max_y();
  2425. min_y = min_max_y[0];
  2426. max_y = min_max_y[1];
  2427. broker.publish("/chart/draw/rescale");
  2428. // center nav
  2429. broker.publish("/nav/center");
  2430. // update range info
  2431. broker.publish("/info/range/update");
  2432. }
  2433. this.center = center;
  2434. function recycle(delta_) {
  2435. // recycle available panes
  2436. var first_pane, last_pane;
  2437. if (delta_ > 0) {
  2438. while (panes[panes.length-1].offset() > max_right) {
  2439. last_pane = panes.pop();
  2440. first_pane = panes[0];
  2441. last_pane.reset(
  2442. first_pane._data_offset()-_pane_period,
  2443. first_pane.offset()-_pane_width);
  2444. panes.unshift(last_pane);
  2445. }
  2446. } else if (delta_ < 0) {
  2447. while (panes[0].offset() < max_left) {
  2448. first_pane = panes.shift();
  2449. last_pane = panes[panes.length-1];
  2450. first_pane.reset(
  2451. last_pane._data_offset()+_pane_period,
  2452. last_pane.offset()+_pane_width);
  2453. panes.push(first_pane);
  2454. }
  2455. }
  2456. }
  2457. this.recycle = recycle;
  2458. function translate(delta_) {
  2459. //XXX: hot code path: omg, make everything in the translate code tighter...
  2460. // check for end of model range
  2461. center_pixel -= delta_;
  2462. if (center_pixel < max_pixel) { //XXX: take most common branches, first
  2463. if (center_pixel < min_pixel) {
  2464. delta_ -= min_pixel - center_pixel;
  2465. center_pixel = min_pixel;
  2466. if (!delta_) { //XXX: optimizing: delta_ === 0
  2467. return;
  2468. }
  2469. }
  2470. } else { // center_pixel >= max_pixel
  2471. delta_ += center_pixel - max_pixel;
  2472. center_pixel = max_pixel;
  2473. if (!delta_) { //XXX: optimizing: delta_ === 0
  2474. return;
  2475. }
  2476. }
  2477. if (Math.abs(delta_) < width) {
  2478. // translate panes
  2479. broker.publish("/pane/translate", delta_);
  2480. // recycle panes
  2481. broker.publish("/chart/recycle", delta_);
  2482. } else { // replace all panes, instead
  2483. // replace panes
  2484. broker.publish("/chart/replace", center_pixel);
  2485. }
  2486. // translate nav
  2487. broker.publish("/nav/translate", delta_);
  2488. // update range info
  2489. broker.publish("/info/range/update");
  2490. }
  2491. this.translate = translate;
  2492. function replace(center_pixel_) {
  2493. //XXX: calculate from center_pixel
  2494. var _num_panes = Math.floor(center_pixel_ / _pane_width);
  2495. var _pixel_rem = center_pixel_ % _pane_width;
  2496. var _idx_offset = Math.floor(panes.length/2);
  2497. var pane_point = (_num_panes + _idx_offset) * _pane_period;
  2498. var pane_pixel = Math.floor(width / 2) + _idx_offset * _pane_width - _pixel_rem;
  2499. var i = panes.length;
  2500. while (i--) {
  2501. var pane = panes[i];
  2502. pane.reset(pane_point, pane_pixel);
  2503. pane_point -= _pane_period;
  2504. pane_pixel -= _pane_width;
  2505. }
  2506. // determine: y-coord scale
  2507. var min_max_y = _min_max_y();
  2508. min_y = min_max_y[0];
  2509. max_y = min_max_y[1];
  2510. broker.publish("/chart/draw/rescale");
  2511. // center nav
  2512. broker.publish("/nav/center");
  2513. }
  2514. this.replace = replace;
  2515. // Draw Loop Methods
  2516. function _draw_loop() {
  2517. var min_max_y = _min_max_y();
  2518. if (min_max_y[0] != min_y || min_max_y[1] != max_y) {
  2519. min_y = min_max_y[0];
  2520. max_y = min_max_y[1];
  2521. broker.publish("/chart/draw/rescale");
  2522. }
  2523. if (!updating) {
  2524. return;
  2525. }
  2526. var now = (new Date()).getTime();
  2527. //XXX: dynamically adjust re-draw rate
  2528. update_delta = Math.max(UPDATE_DELTA_FACTOR * (now - update_time), UPDATE_BOUND);
  2529. update_time = now;
  2530. update_id = setTimeout(_draw_loop, update_delta);
  2531. }
  2532. function draw_start() {
  2533. clearTimeout(update_id);
  2534. updating = true;
  2535. update_time = (new Date()).getTime();
  2536. update_id = setTimeout(_draw_loop, update_delta);
  2537. }
  2538. this.draw_start = draw_start;
  2539. function draw_stop() {
  2540. //XXX: Make final draw update more deterministic, after stop
  2541. clearTimeout(update_id);
  2542. updating = false;
  2543. update_id = setTimeout(_draw_loop, UPDATE_AFTER_STOP);
  2544. }
  2545. this.draw_stop = draw_stop;
  2546. // Scroll Loop Methods
  2547. function _scroll_loop() {
  2548. if (!scrolling) {
  2549. return;
  2550. }
  2551. broker.publish("/chart/translate", scroll_rate);
  2552. scroll_id = setTimeout(_scroll_loop, NAV_SCROLL_RATE);
  2553. }
  2554. function scroll_start(sign) {
  2555. clearTimeout(scroll_id);
  2556. scroll_rate = sign * NAV_SCROLL_SPEED;
  2557. scrolling = true;
  2558. _scroll_loop();
  2559. }
  2560. this.scroll_start = scroll_start;
  2561. function scroll_stop() {
  2562. scrolling = false;
  2563. }
  2564. this.scroll_stop = scroll_stop;
  2565. // Modify Zoom
  2566. function _update_zoom(zoom_scale_, pane_period_) {
  2567. zoom_scale = zoom_scale_;
  2568. _pane_period = pane_period_;
  2569. }
  2570. this._update_zoom = _update_zoom;
  2571. function set_zoom(zoom_scale_, pane_period_) {
  2572. var _num_panes = center_pixel / _pane_width;
  2573. var _time_offset = model.min_time();
  2574. _time_offset += _num_panes * _pane_period * model.zoom_delta();
  2575. _time_offset = Math.floor(_time_offset);
  2576. _update_zoom(zoom_scale_, pane_period_);
  2577. center(_time_offset, Math.floor(width/2));
  2578. }
  2579. this.set_zoom = set_zoom;
  2580. // Update Top Range
  2581. function _update_top_range() {
  2582. // static variables
  2583. var _half_width = width/2;
  2584. var _day_in_secs = 60*60*24;
  2585. _update_top_range = function() {
  2586. // hide cursor info
  2587. _overlay_line.css("display", "none");
  2588. _top_point.css("display", "none");
  2589. // calculate range
  2590. var _min_pixel = center_pixel - _half_width;
  2591. var _max_pixel = center_pixel + _half_width;
  2592. var _min_time = model.min_time() + (_min_pixel / _x_scale * model.zoom_delta());
  2593. var _max_time = model.min_time() + (_max_pixel / _x_scale * model.zoom_delta());
  2594. var _min_date = new Date(_min_time * 1000);
  2595. var _max_date = new Date(_max_time * 1000);
  2596. if ((_max_time - _min_time) < _day_in_secs && _min_date.getDay() === _max_date.getDay()) {
  2597. _top_range_span.text(formatDate(_min_date, "MMM d, yyyy"));
  2598. } else {
  2599. _top_range_span.text(
  2600. formatDate(_min_date, "NNN d, yyyy") + " - " + formatDate(_max_date, "NNN d, yyyy"));
  2601. }
  2602. // display viewable range
  2603. _top_range.css("display", "inline");
  2604. };
  2605. return _update_top_range();
  2606. }
  2607. this._update_top_range = _update_top_range;
  2608. // Update Top Point
  2609. function _update_top_point(pixel_offset_) {
  2610. // static variables
  2611. var _half_width = width/2;
  2612. _update_top_point = function(pixel_offset_) {
  2613. // hide range tracking
  2614. _top_range.css("display", "none");
  2615. // calculate time and value
  2616. var _point = Math.round((center_pixel + pixel_offset_) / _pane_width * _pane_period);
  2617. var _pixel = _point * _x_scale;
  2618. var _date = new Date((model.min_time() + (_point * model.zoom_delta())) * 1000);
  2619. var _val = model.fetch(zoom_scale, _point, 0)[0][0] || 0;
  2620. _top_point_time.text(formatDate(_date, "MMM d, yyyy"));
  2621. _top_point_val.text(Math.round(_val * 1000) / 1000);
  2622. var _min = center_pixel - _half_width;
  2623. _overlay_line.css("left", Math.floor(_pixel - _min)+1);
  2624. // display cursor info
  2625. _top_point.css("display", "inline");
  2626. _overlay_line.css("display", "inline");
  2627. };
  2628. return _update_top_point(pixel_offset_);
  2629. }
  2630. this._update_top_point = _update_top_point;
  2631. };
  2632. Chart.prototype = {
  2633. version: "0.1"
  2634. };
  2635. $c.chart = function(target) {
  2636. var chart = new Chart(target);
  2637. chart.init();
  2638. return chart;
  2639. };
  2640. // ========== External ==========
  2641. // reference: http://www.mattkruse.com/javascript/date/source.html
  2642. // ===================================================================
  2643. // Author: Matt Kruse <matt@mattkruse.com>
  2644. // WWW: http://www.mattkruse.com/
  2645. //
  2646. // NOTICE: You may use this code for any purpose, commercial or
  2647. // private, without any further permission from the author. You may
  2648. // remove this notice from your final code if you wish, however it is
  2649. // appreciated by the author if at least my web site address is kept.
  2650. //
  2651. // You may *NOT* re-distribute this code in any way except through its
  2652. // use. That means, you can include it in your product, or your web
  2653. // site, or any other form where the code is actually being used. You
  2654. // may not put the plain javascript up on your site for download or
  2655. // include it in your javascript libraries for download.
  2656. // If you wish to share this code with others, please just point them
  2657. // to the URL instead.
  2658. // Please DO NOT link directly to my .js files from your site. Copy
  2659. // the files to your server and use them there. Thank you.
  2660. // ===================================================================
  2661. // HISTORY
  2662. // ------------------------------------------------------------------
  2663. // May 17, 2003: Fixed bug in parseDate() for dates <1970
  2664. // March 11, 2003: Added parseDate() function
  2665. // March 11, 2003: Added "NNN" formatting option. Doesn't match up
  2666. // perfectly with SimpleDateFormat formats, but
  2667. // backwards-compatability was required.
  2668. // ------------------------------------------------------------------
  2669. // These functions use the same 'format' strings as the
  2670. // java.text.SimpleDateFormat class, with minor exceptions.
  2671. // The format string consists of the following abbreviations:
  2672. //
  2673. // Field | Full Form | Short Form
  2674. // -------------+--------------------+-----------------------
  2675. // Year | yyyy (4 digits) | yy (2 digits), y (2 or 4 digits)
  2676. // Month | MMM (name or abbr.)| MM (2 digits), M (1 or 2 digits)
  2677. // | NNN (abbr.) |
  2678. // Day of Month | dd (2 digits) | d (1 or 2 digits)
  2679. // Day of Week | EE (name) | E (abbr)
  2680. // Hour (1-12) | hh (2 digits) | h (1 or 2 digits)
  2681. // Hour (0-23) | HH (2 digits) | H (1 or 2 digits)
  2682. // Hour (0-11) | KK (2 digits) | K (1 or 2 digits)
  2683. // Hour (1-24) | kk (2 digits) | k (1 or 2 digits)
  2684. // Minute | mm (2 digits) | m (1 or 2 digits)
  2685. // Second | ss (2 digits) | s (1 or 2 digits)
  2686. // AM/PM | a |
  2687. //
  2688. // NOTE THE DIFFERENCE BETWEEN MM and mm! Month=MM, not mm!
  2689. // Examples:
  2690. // "MMM d, y" matches: January 01, 2000
  2691. // Dec 1, 1900
  2692. // Nov 20, 00
  2693. // "M/d/yy" matches: 01/20/00
  2694. // 9/2/00
  2695. // "MMM dd, yyyy hh:mm:ssa" matches: "January 01, 2000 12:30:45AM"
  2696. // ------------------------------------------------------------------
  2697. var MONTH_NAMES = [
  2698. 'January',
  2699. 'February',
  2700. 'March',
  2701. 'April',
  2702. 'May',
  2703. 'June',
  2704. 'July',
  2705. 'August',
  2706. 'September',
  2707. 'October',
  2708. 'November',
  2709. 'December',
  2710. 'Jan',
  2711. 'Feb',
  2712. 'Mar',
  2713. 'Apr',
  2714. 'May',
  2715. 'Jun',
  2716. 'Jul',
  2717. 'Aug',
  2718. 'Sep',
  2719. 'Oct',
  2720. 'Nov',
  2721. 'Dec'
  2722. ];
  2723. var DAY_NAMES = [
  2724. 'Sunday',
  2725. 'Monday',
  2726. 'Tuesday',
  2727. 'Wednesday',
  2728. 'Thursday',
  2729. 'Friday',
  2730. 'Saturday',
  2731. 'Sun',
  2732. 'Mon',
  2733. 'Tue',
  2734. 'Wed',
  2735. 'Thu',
  2736. 'Fri',
  2737. 'Sat'
  2738. ];
  2739. function LZ(x) {
  2740. return (x < 0 || x > 9 ? "" : "0" ) + x;
  2741. }
  2742. // ------------------------------------------------------------------
  2743. // formatDate (date_object, format)
  2744. // Returns a date in the output format specified.
  2745. // The format string uses the same abbreviations as in getDateFromFormat()
  2746. // ------------------------------------------------------------------
  2747. function formatDate(date,format) {
  2748. format = format + "";
  2749. var result="";
  2750. var i_format=0;
  2751. var c="";
  2752. var token="";
  2753. var y=date.getYear()+"";
  2754. var M=date.getMonth()+1;
  2755. var d=date.getDate();
  2756. var E=date.getDay();
  2757. var H=date.getHours();
  2758. var m=date.getMinutes();
  2759. var s=date.getSeconds();
  2760. var yyyy,yy,MMM,MM,dd,hh,h,mm,ss,ampm,HH,KK,K,kk,k;
  2761. // Convert real date parts into formatted versions
  2762. var value = {};
  2763. if (y.length < 4) {y=""+(y-0+1900);}
  2764. value.y=""+y;
  2765. value.yyyy=y;
  2766. value.yy=y.substring(2,4);
  2767. value.M=M;
  2768. value.MM=LZ(M);
  2769. value.MMM=MONTH_NAMES[M-1];
  2770. value.NNN=MONTH_NAMES[M+11];
  2771. value.d=d;
  2772. value.dd=LZ(d);
  2773. value.E=DAY_NAMES[E+7];
  2774. value.EE=DAY_NAMES[E];
  2775. value.H=H;
  2776. value.HH=LZ(H);
  2777. if (H==0){value.h=12;}
  2778. else if (H>12){value.h=H-12;}
  2779. else {value.h=H;}
  2780. value.hh=LZ(value.h);
  2781. if (H>11){value.K=H-12;} else {value.K=H;}
  2782. value.k=H+1;
  2783. value.KK=LZ(value.K);
  2784. value.kk=LZ(value.k);
  2785. if (H > 11) { value.a="PM"; }
  2786. else { value.a="AM"; }
  2787. value.m=m;
  2788. value.mm=LZ(m);
  2789. value.s=s;
  2790. value.ss=LZ(s);
  2791. while (i_format < format.length) {
  2792. c = format.charAt(i_format);
  2793. token="";
  2794. while ((format.charAt(i_format)==c) && (i_format < format.length)) {
  2795. token += format.charAt(i_format++);
  2796. }
  2797. if (value[token]) {
  2798. result=result + value[token];
  2799. } else {
  2800. result=result + token;
  2801. }
  2802. }
  2803. return result;
  2804. }
  2805. ;;; $c.formatDate = formatDate;
  2806. })(jQuery);