PageRenderTime 75ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/timeplot/scripts/timeplot.js

http://showslow.googlecode.com/
JavaScript | 543 lines | 361 code | 63 blank | 119 comment | 67 complexity | 1761e4c0191684592112cdf07ea37bca MD5 | raw file
  1. /**
  2. * Timeplot
  3. *
  4. * @fileOverview Timeplot
  5. * @name Timeplot
  6. */
  7. Timeline.Debug = SimileAjax.Debug; // timeline uses it's own debug system which is not as advanced
  8. var log = SimileAjax.Debug.log; // shorter name is easier to use
  9. /*
  10. * This function is used to implement a raw but effective OOP-like inheritance
  11. * in various Timeplot classes.
  12. */
  13. Object.extend = function(destination, source) {
  14. for (var property in source) {
  15. destination[property] = source[property];
  16. }
  17. return destination;
  18. }
  19. // ---------------------------------------------
  20. /**
  21. * Create a timeplot attached to the given element and using the configuration from the given array of PlotInfos
  22. */
  23. Timeplot.create = function(elmt, plotInfos) {
  24. return new Timeplot._Impl(elmt, plotInfos);
  25. };
  26. /**
  27. * Create a PlotInfo configuration from the given map of params
  28. */
  29. Timeplot.createPlotInfo = function(params) {
  30. return {
  31. id: ("id" in params) ? params.id : "p" + Math.round(Math.random() * 1000000),
  32. dataSource: ("dataSource" in params) ? params.dataSource : null,
  33. eventSource: ("eventSource" in params) ? params.eventSource : null,
  34. timeGeometry: ("timeGeometry" in params) ? params.timeGeometry : new Timeplot.DefaultTimeGeometry(),
  35. valueGeometry: ("valueGeometry" in params) ? params.valueGeometry : new Timeplot.DefaultValueGeometry(),
  36. timeZone: ("timeZone" in params) ? params.timeZone : 0,
  37. fillColor: ("fillColor" in params) ? ((params.fillColor == "string") ? new Timeplot.Color(params.fillColor) : params.fillColor) : null,
  38. fillGradient: ("fillGradient" in params) ? params.fillGradient : true,
  39. fillFrom: ("fillFrom" in params) ? params.fillFrom : Number.NEGATIVE_INFINITY,
  40. lineColor: ("lineColor" in params) ? ((params.lineColor == "string") ? new Timeplot.Color(params.lineColor) : params.lineColor) : new Timeplot.Color("#606060"),
  41. lineWidth: ("lineWidth" in params) ? params.lineWidth : 1.0,
  42. dotRadius: ("dotRadius" in params) ? params.dotRadius : 2.0,
  43. dotColor: ("dotColor" in params) ? params.dotColor : null,
  44. eventLineWidth: ("eventLineWidth" in params) ? params.eventLineWidth : 1.0,
  45. showValues: ("showValues" in params) ? params.showValues : false,
  46. roundValues: ("roundValues" in params) ? params.roundValues : true,
  47. valuesOpacity: ("valuesOpacity" in params) ? params.valuesOpacity : 75,
  48. bubbleWidth: ("bubbleWidth" in params) ? params.bubbleWidth : 300,
  49. bubbleHeight: ("bubbleHeight" in params) ? params.bubbleHeight : 200
  50. };
  51. };
  52. // -------------------------------------------------------
  53. /**
  54. * This is the implementation of the Timeplot object.
  55. *
  56. * @constructor
  57. */
  58. Timeplot._Impl = function(elmt, plotInfos) {
  59. this._id = "t" + Math.round(Math.random() * 1000000);
  60. this._containerDiv = elmt;
  61. this._plotInfos = plotInfos;
  62. this._painters = {
  63. background: [],
  64. foreground: []
  65. };
  66. this._painter = null;
  67. this._active = false;
  68. this._upright = false;
  69. this._initialize();
  70. };
  71. Timeplot._Impl.prototype = {
  72. dispose: function() {
  73. for (var i = 0; i < this._plots.length; i++) {
  74. this._plots[i].dispose();
  75. }
  76. this._plots = null;
  77. this._plotsInfos = null;
  78. this._containerDiv.innerHTML = "";
  79. },
  80. /**
  81. * Returns the main container div this timeplot is operating on.
  82. */
  83. getElement: function() {
  84. return this._containerDiv;
  85. },
  86. /**
  87. * Returns document this timeplot belongs to.
  88. */
  89. getDocument: function() {
  90. return this._containerDiv.ownerDocument;
  91. },
  92. /**
  93. * Append the given element to the timeplot DOM
  94. */
  95. add: function(div) {
  96. this._containerDiv.appendChild(div);
  97. },
  98. /**
  99. * Remove the given element to the timeplot DOM
  100. */
  101. remove: function(div) {
  102. this._containerDiv.removeChild(div);
  103. },
  104. /**
  105. * Add a painter to the timeplot
  106. */
  107. addPainter: function(layerName, painter) {
  108. var layer = this._painters[layerName];
  109. if (layer) {
  110. for (var i = 0; i < layer.length; i++) {
  111. if (layer[i].context._id == painter.context._id) {
  112. return;
  113. }
  114. }
  115. layer.push(painter);
  116. }
  117. },
  118. /**
  119. * Remove a painter from the timeplot
  120. */
  121. removePainter: function(layerName, painter) {
  122. var layer = this._painters[layerName];
  123. if (layer) {
  124. for (var i = 0; i < layer.length; i++) {
  125. if (layer[i].context._id == painter.context._id) {
  126. layer.splice(i, 1);
  127. break;
  128. }
  129. }
  130. }
  131. },
  132. /**
  133. * Get the width in pixels of the area occupied by the entire timeplot in the page
  134. */
  135. getWidth: function() {
  136. return this._containerDiv.clientWidth;
  137. },
  138. /**
  139. * Get the height in pixels of the area occupied by the entire timeplot in the page
  140. */
  141. getHeight: function() {
  142. return this._containerDiv.clientHeight;
  143. },
  144. /**
  145. * Get the drawing canvas associated with this timeplot
  146. */
  147. getCanvas: function() {
  148. return this._canvas;
  149. },
  150. /**
  151. * <p>Load the data from the given url into the given eventSource, using
  152. * the given separator to parse the columns and preprocess it before parsing
  153. * thru the optional filter function. The filter is useful for when
  154. * the data is row-oriented but the format is not compatible with the
  155. * one that Timeplot expects.</p>
  156. *
  157. * <p>Here is an example of a filter that changes dates in the form 'yyyy/mm/dd'
  158. * in the required 'yyyy-mm-dd' format:
  159. * <pre>var dataFilter = function(data) {
  160. * for (var i = 0; i < data.length; i++) {
  161. * var row = data[i];
  162. * row[0] = row[0].replace(/\//g,"-");
  163. * }
  164. * return data;
  165. * };</pre></p>
  166. */
  167. loadText: function(url, separator, eventSource, filter, format) {
  168. if (this._active) {
  169. var tp = this;
  170. var fError = function(statusText, status, xmlhttp) {
  171. alert("Failed to load data xml from " + url + "\n" + statusText);
  172. tp.hideLoadingMessage();
  173. };
  174. var fDone = function(xmlhttp) {
  175. try {
  176. eventSource.loadText(xmlhttp.responseText, separator, url, filter, format);
  177. } catch (e) {
  178. SimileAjax.Debug.exception(e);
  179. } finally {
  180. tp.hideLoadingMessage();
  181. }
  182. };
  183. this.showLoadingMessage();
  184. window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0);
  185. }
  186. },
  187. /**
  188. * Load event data from the given url into the given eventSource, using
  189. * the Timeline XML event format.
  190. */
  191. loadXML: function(url, eventSource) {
  192. if (this._active) {
  193. var tl = this;
  194. var fError = function(statusText, status, xmlhttp) {
  195. alert("Failed to load data xml from " + url + "\n" + statusText);
  196. tl.hideLoadingMessage();
  197. };
  198. var fDone = function(xmlhttp) {
  199. try {
  200. var xml = xmlhttp.responseXML;
  201. if (!xml.documentElement && xmlhttp.responseStream) {
  202. xml.load(xmlhttp.responseStream);
  203. }
  204. eventSource.loadXML(xml, url);
  205. } finally {
  206. tl.hideLoadingMessage();
  207. }
  208. };
  209. this.showLoadingMessage();
  210. window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0);
  211. }
  212. },
  213. /**
  214. * Overlay a 'div' element filled with the given text and styles to this timeplot
  215. * This is used to implement labels since canvas does not support drawing text.
  216. */
  217. putText: function(id, text, clazz, styles) {
  218. var div = this.putDiv(id, "timeplot-div " + clazz, styles);
  219. div.innerHTML = text;
  220. return div;
  221. },
  222. /**
  223. * Overlay a 'div' element, with the given class and the given styles to this timeplot.
  224. * This is used for labels and horizontal and vertical grids.
  225. */
  226. putDiv: function(id, clazz, styles) {
  227. var tid = this._id + "-" + id;
  228. var div = document.getElementById(tid);
  229. if (!div) {
  230. var container = this._containerDiv.firstChild; // get the divs container
  231. div = document.createElement("div");
  232. div.setAttribute("id",tid);
  233. container.appendChild(div);
  234. }
  235. div.setAttribute("class","timeplot-div " + clazz);
  236. div.setAttribute("className","timeplot-div " + clazz);
  237. this.placeDiv(div,styles);
  238. return div;
  239. },
  240. /**
  241. * Associate the given map of styles to the given element.
  242. * In case such styles indicate position (left,right,top,bottom) correct them
  243. * with the padding information so that they align to the 'internal' area
  244. * of the timeplot.
  245. */
  246. placeDiv: function(div, styles) {
  247. if (styles) {
  248. for (style in styles) {
  249. if (style == "left") {
  250. styles[style] += this._paddingX;
  251. styles[style] += "px";
  252. } else if (style == "right") {
  253. styles[style] += this._paddingX;
  254. styles[style] += "px";
  255. } else if (style == "top") {
  256. styles[style] += this._paddingY;
  257. styles[style] += "px";
  258. } else if (style == "bottom") {
  259. styles[style] += this._paddingY;
  260. styles[style] += "px";
  261. } else if (style == "width") {
  262. if (styles[style] < 0) styles[style] = 0;
  263. styles[style] += "px";
  264. } else if (style == "height") {
  265. if (styles[style] < 0) styles[style] = 0;
  266. styles[style] += "px";
  267. }
  268. div.style[style] = styles[style];
  269. }
  270. }
  271. },
  272. /**
  273. * return a {x,y} map with the location of the given element relative to the 'internal' area of the timeplot
  274. * (that is, without the container padding)
  275. */
  276. locate: function(div) {
  277. return {
  278. x: div.offsetLeft - this._paddingX,
  279. y: div.offsetTop - this._paddingY
  280. }
  281. },
  282. /**
  283. * Forces timeplot to re-evaluate the various value and time geometries
  284. * associated with its plot layers and repaint accordingly. This should
  285. * be invoked after the data in any of the data sources has been
  286. * modified.
  287. */
  288. update: function() {
  289. if (this._active) {
  290. for (var i = 0; i < this._plots.length; i++) {
  291. var plot = this._plots[i];
  292. var dataSource = plot.getDataSource();
  293. if (dataSource) {
  294. var range = dataSource.getRange();
  295. if (range) {
  296. plot._valueGeometry.setRange(range);
  297. plot._timeGeometry.setRange(range);
  298. }
  299. }
  300. plot.hideValues();
  301. }
  302. this.paint();
  303. }
  304. },
  305. /**
  306. * Forces timeplot to re-evaluate its own geometry, clear itself and paint.
  307. * This should be used instead of paint() when you're not sure if the
  308. * geometry of the page has changed or not.
  309. */
  310. repaint: function() {
  311. if (this._active) {
  312. this._prepareCanvas();
  313. for (var i = 0; i < this._plots.length; i++) {
  314. var plot = this._plots[i];
  315. if (plot._timeGeometry) plot._timeGeometry.reset();
  316. if (plot._valueGeometry) plot._valueGeometry.reset();
  317. }
  318. this.paint();
  319. }
  320. },
  321. /**
  322. * Calls all the painters that were registered to this timeplot and makes them
  323. * paint the timeplot. This should be used only when you're sure that the geometry
  324. * of the page hasn't changed.
  325. * NOTE: painting is performed by a different thread and it's safe to call this
  326. * function in bursts (as in mousemove or during window resizing
  327. */
  328. paint: function() {
  329. if (this._active && this._painter == null) {
  330. var timeplot = this;
  331. this._painter = window.setTimeout(function() {
  332. timeplot._clearCanvas();
  333. var run = function(action,context) {
  334. try {
  335. if (context.setTimeplot) context.setTimeplot(timeplot);
  336. action.apply(context,[]);
  337. } catch (e) {
  338. SimileAjax.Debug.exception(e);
  339. }
  340. }
  341. var background = timeplot._painters.background;
  342. for (var i = 0; i < background.length; i++) {
  343. run(background[i].action, background[i].context);
  344. }
  345. var foreground = timeplot._painters.foreground;
  346. for (var i = 0; i < foreground.length; i++) {
  347. run(foreground[i].action, foreground[i].context);
  348. }
  349. timeplot._painter = null;
  350. }, 20);
  351. }
  352. },
  353. _clearCanvas: function() {
  354. var canvas = this.getCanvas();
  355. var ctx = canvas.getContext('2d');
  356. ctx.clearRect(0,0,canvas.width,canvas.height);
  357. },
  358. _clearLabels: function() {
  359. var labels = this._containerDiv.firstChild;
  360. if (labels) this._containerDiv.removeChild(labels);
  361. labels = document.createElement("div");
  362. this._containerDiv.appendChild(labels);
  363. },
  364. _prepareCanvas: function() {
  365. var canvas = this.getCanvas();
  366. // using jQuery. note we calculate the average padding; if your
  367. // padding settings are not symmetrical, the labels will be off
  368. // since they expect to be centered on the canvas.
  369. var con = SimileAjax.jQuery(this._containerDiv);
  370. this._paddingX = (parseInt(con.css('paddingLeft')) +
  371. parseInt(con.css('paddingRight'))) / 2;
  372. this._paddingY = (parseInt(con.css('paddingTop')) +
  373. parseInt(con.css('paddingBottom'))) / 2;
  374. canvas.width = this.getWidth() - (this._paddingX * 2);
  375. canvas.height = this.getHeight() - (this._paddingY * 2);
  376. var ctx = canvas.getContext('2d');
  377. this._setUpright(ctx, canvas);
  378. ctx.globalCompositeOperation = 'source-over';
  379. },
  380. _setUpright: function(ctx, canvas) {
  381. // excanvas+IE requires this to be done only once, ever; actual canvas
  382. // implementations reset and require this for each call to re-layout
  383. if (!SimileAjax.Platform.browser.isIE) this._upright = false;
  384. if (!this._upright) {
  385. this._upright = true;
  386. ctx.translate(0, canvas.height);
  387. ctx.scale(1,-1);
  388. }
  389. },
  390. _isBrowserSupported: function(canvas) {
  391. var browser = SimileAjax.Platform.browser;
  392. if ((canvas.getContext && window.getComputedStyle) ||
  393. (browser.isIE && browser.majorVersion >= 6)) {
  394. return true;
  395. } else {
  396. return false;
  397. }
  398. },
  399. _initialize: function() {
  400. // initialize the window manager (used to handle the popups)
  401. // NOTE: this is a singleton and it's safe to call multiple times
  402. SimileAjax.WindowManager.initialize();
  403. var containerDiv = this._containerDiv;
  404. var doc = containerDiv.ownerDocument;
  405. // make sure the timeplot div has the right class
  406. containerDiv.className = "timeplot-container " + containerDiv.className;
  407. // clean it up if it contains some content
  408. while (containerDiv.firstChild) {
  409. containerDiv.removeChild(containerDiv.firstChild);
  410. }
  411. var canvas = doc.createElement("canvas");
  412. if (this._isBrowserSupported(canvas)) {
  413. this._clearLabels();
  414. this._canvas = canvas;
  415. canvas.className = "timeplot-canvas";
  416. containerDiv.appendChild(canvas);
  417. if(!canvas.getContext && G_vmlCanvasManager) {
  418. canvas = G_vmlCanvasManager.initElement(this._canvas);
  419. this._canvas = canvas;
  420. }
  421. this._prepareCanvas();
  422. // inserting copyright and link to simile
  423. var elmtCopyright = SimileAjax.Graphics.createTranslucentImage(Timeplot.urlPrefix + "images/copyright.png");
  424. elmtCopyright.className = "timeplot-copyright";
  425. elmtCopyright.title = "SIMILE Timeplot - http://www.simile-widgets.organ/timeplot/";
  426. SimileAjax.DOM.registerEvent(elmtCopyright, "click", function() { window.location = "http://www.simile-widgets.organ/timeplot/"; });
  427. containerDiv.appendChild(elmtCopyright);
  428. var timeplot = this;
  429. var painter = {
  430. onAddMany: function() { timeplot.update(); },
  431. onClear: function() { timeplot.update(); }
  432. }
  433. // creating painters
  434. this._plots = [];
  435. if (this._plotInfos) {
  436. for (var i = 0; i < this._plotInfos.length; i++) {
  437. var plot = new Timeplot.Plot(this, this._plotInfos[i]);
  438. var dataSource = plot.getDataSource();
  439. if (dataSource) {
  440. dataSource.addListener(painter);
  441. }
  442. this.addPainter("background", {
  443. context: plot.getTimeGeometry(),
  444. action: plot.getTimeGeometry().paint
  445. });
  446. this.addPainter("background", {
  447. context: plot.getValueGeometry(),
  448. action: plot.getValueGeometry().paint
  449. });
  450. this.addPainter("foreground", {
  451. context: plot,
  452. action: plot.paint
  453. });
  454. this._plots.push(plot);
  455. plot.initialize();
  456. }
  457. }
  458. // creating loading UI
  459. var message = SimileAjax.Graphics.createMessageBubble(doc);
  460. message.containerDiv.className = "timeplot-message-container";
  461. containerDiv.appendChild(message.containerDiv);
  462. message.contentDiv.className = "timeplot-message";
  463. message.contentDiv.innerHTML = "<img src='" + Timeplot.urlPrefix + "images/progress-running.gif' /> Loading...";
  464. this.showLoadingMessage = function() { message.containerDiv.style.display = "block"; };
  465. this.hideLoadingMessage = function() { message.containerDiv.style.display = "none"; };
  466. this._active = true;
  467. } else {
  468. this._message = SimileAjax.Graphics.createMessageBubble(doc);
  469. this._message.containerDiv.className = "timeplot-message-container";
  470. this._message.containerDiv.style.top = "15%";
  471. this._message.containerDiv.style.left = "20%";
  472. this._message.containerDiv.style.right = "20%";
  473. this._message.containerDiv.style.minWidth = "20em";
  474. this._message.contentDiv.className = "timeplot-message";
  475. this._message.contentDiv.innerHTML = "We're terribly sorry, but your browser is not currently supported by <a href='http://www.simile-widgets.org/timeplot/'>Timeplot</a>.";
  476. this._message.containerDiv.style.display = "block";
  477. containerDiv.appendChild(this._message.containerDiv);
  478. }
  479. }
  480. };