jquery.flot.canvas.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. /* Flot plugin for drawing all elements of a plot on the canvas.
  2. Copyright (c) 2007-2013 IOLA and Ole Laursen.
  3. Licensed under the MIT license.
  4. Flot normally produces certain elements, like axis labels and the legend, using
  5. HTML elements. This permits greater interactivity and customization, and often
  6. looks better, due to cross-browser canvas text inconsistencies and limitations.
  7. It can also be desirable to render the plot entirely in canvas, particularly
  8. if the goal is to save it as an image, or if Flot is being used in a context
  9. where the HTML DOM does not exist, as is the case within Node.js. This plugin
  10. switches out Flot's standard drawing operations for canvas-only replacements.
  11. Currently the plugin supports only axis labels, but it will eventually allow
  12. every element of the plot to be rendered directly to canvas.
  13. The plugin supports these options:
  14. {
  15. canvas: boolean
  16. }
  17. The "canvas" option controls whether full canvas drawing is enabled, making it
  18. possible to toggle on and off. This is useful when a plot uses HTML text in the
  19. browser, but needs to redraw with canvas text when exporting as an image.
  20. */
  21. (function($) {
  22. var options = {
  23. canvas: true
  24. };
  25. var render, getTextInfo, addText;
  26. // Cache the prototype hasOwnProperty for faster access
  27. var hasOwnProperty = Object.prototype.hasOwnProperty;
  28. function init(plot, classes) {
  29. var Canvas = classes.Canvas;
  30. // We only want to replace the functions once; the second time around
  31. // we would just get our new function back. This whole replacing of
  32. // prototype functions is a disaster, and needs to be changed ASAP.
  33. if (render == null) {
  34. getTextInfo = Canvas.prototype.getTextInfo,
  35. addText = Canvas.prototype.addText,
  36. render = Canvas.prototype.render;
  37. }
  38. // Finishes rendering the canvas, including overlaid text
  39. Canvas.prototype.render = function() {
  40. if (!plot.getOptions().canvas) {
  41. return render.call(this);
  42. }
  43. var context = this.context,
  44. cache = this._textCache;
  45. // For each text layer, render elements marked as active
  46. context.save();
  47. context.textBaseline = "middle";
  48. for (var layerKey in cache) {
  49. if (hasOwnProperty.call(cache, layerKey)) {
  50. var layerCache = cache[layerKey];
  51. for (var styleKey in layerCache) {
  52. if (hasOwnProperty.call(layerCache, styleKey)) {
  53. var styleCache = layerCache[styleKey],
  54. updateStyles = true;
  55. for (var key in styleCache) {
  56. if (hasOwnProperty.call(styleCache, key)) {
  57. var info = styleCache[key],
  58. positions = info.positions,
  59. lines = info.lines;
  60. // Since every element at this level of the cache have the
  61. // same font and fill styles, we can just change them once
  62. // using the values from the first element.
  63. if (updateStyles) {
  64. context.fillStyle = info.font.color;
  65. context.font = info.font.definition;
  66. updateStyles = false;
  67. }
  68. for (var i = 0, position; position = positions[i]; i++) {
  69. if (position.active) {
  70. for (var j = 0, line; line = position.lines[j]; j++) {
  71. context.fillText(lines[j].text, line[0], line[1]);
  72. }
  73. } else {
  74. positions.splice(i--, 1);
  75. }
  76. }
  77. if (positions.length == 0) {
  78. delete styleCache[key];
  79. }
  80. }
  81. }
  82. }
  83. }
  84. }
  85. }
  86. context.restore();
  87. };
  88. // Creates (if necessary) and returns a text info object.
  89. //
  90. // When the canvas option is set, the object looks like this:
  91. //
  92. // {
  93. // width: Width of the text's bounding box.
  94. // height: Height of the text's bounding box.
  95. // positions: Array of positions at which this text is drawn.
  96. // lines: [{
  97. // height: Height of this line.
  98. // widths: Width of this line.
  99. // text: Text on this line.
  100. // }],
  101. // font: {
  102. // definition: Canvas font property string.
  103. // color: Color of the text.
  104. // },
  105. // }
  106. //
  107. // The positions array contains objects that look like this:
  108. //
  109. // {
  110. // active: Flag indicating whether the text should be visible.
  111. // lines: Array of [x, y] coordinates at which to draw the line.
  112. // x: X coordinate at which to draw the text.
  113. // y: Y coordinate at which to draw the text.
  114. // }
  115. Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) {
  116. if (!plot.getOptions().canvas) {
  117. return getTextInfo.call(this, layer, text, font, angle, width);
  118. }
  119. var textStyle, layerCache, styleCache, info;
  120. // Cast the value to a string, in case we were given a number
  121. text = "" + text;
  122. // If the font is a font-spec object, generate a CSS definition
  123. if (typeof font === "object") {
  124. textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family;
  125. } else {
  126. textStyle = font;
  127. }
  128. // Retrieve (or create) the cache for the text's layer and styles
  129. layerCache = this._textCache[layer];
  130. if (layerCache == null) {
  131. layerCache = this._textCache[layer] = {};
  132. }
  133. styleCache = layerCache[textStyle];
  134. if (styleCache == null) {
  135. styleCache = layerCache[textStyle] = {};
  136. }
  137. info = styleCache[text];
  138. if (info == null) {
  139. var context = this.context;
  140. // If the font was provided as CSS, create a div with those
  141. // classes and examine it to generate a canvas font spec.
  142. if (typeof font !== "object") {
  143. var element = $("<div>&nbsp;</div>")
  144. .css("position", "absolute")
  145. .addClass(typeof font === "string" ? font : null)
  146. .appendTo(this.getTextLayer(layer));
  147. font = {
  148. lineHeight: element.height(),
  149. style: element.css("font-style"),
  150. variant: element.css("font-variant"),
  151. weight: element.css("font-weight"),
  152. family: element.css("font-family"),
  153. color: element.css("color")
  154. };
  155. // Setting line-height to 1, without units, sets it equal
  156. // to the font-size, even if the font-size is abstract,
  157. // like 'smaller'. This enables us to read the real size
  158. // via the element's height, working around browsers that
  159. // return the literal 'smaller' value.
  160. font.size = element.css("line-height", 1).height();
  161. element.remove();
  162. }
  163. textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family;
  164. // Create a new info object, initializing the dimensions to
  165. // zero so we can count them up line-by-line.
  166. info = styleCache[text] = {
  167. width: 0,
  168. height: 0,
  169. positions: [],
  170. lines: [],
  171. font: {
  172. definition: textStyle,
  173. color: font.color
  174. }
  175. };
  176. context.save();
  177. context.font = textStyle;
  178. // Canvas can't handle multi-line strings; break on various
  179. // newlines, including HTML brs, to build a list of lines.
  180. // Note that we could split directly on regexps, but IE < 9 is
  181. // broken; revisit when we drop IE 7/8 support.
  182. var lines = (text + "").replace(/<br ?\/?>|\r\n|\r/g, "\n").split("\n");
  183. for (var i = 0; i < lines.length; ++i) {
  184. var lineText = lines[i],
  185. measured = context.measureText(lineText);
  186. info.width = Math.max(measured.width, info.width);
  187. info.height += font.lineHeight;
  188. info.lines.push({
  189. text: lineText,
  190. width: measured.width,
  191. height: font.lineHeight
  192. });
  193. }
  194. context.restore();
  195. }
  196. return info;
  197. };
  198. // Adds a text string to the canvas text overlay.
  199. Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) {
  200. if (!plot.getOptions().canvas) {
  201. return addText.call(this, layer, x, y, text, font, angle, width, halign, valign);
  202. }
  203. var info = this.getTextInfo(layer, text, font, angle, width),
  204. positions = info.positions,
  205. lines = info.lines;
  206. // Text is drawn with baseline 'middle', which we need to account
  207. // for by adding half a line's height to the y position.
  208. y += info.height / lines.length / 2;
  209. // Tweak the initial y-position to match vertical alignment
  210. if (valign == "middle") {
  211. y = Math.round(y - info.height / 2);
  212. } else if (valign == "bottom") {
  213. y = Math.round(y - info.height);
  214. } else {
  215. y = Math.round(y);
  216. }
  217. // FIXME: LEGACY BROWSER FIX
  218. // AFFECTS: Opera < 12.00
  219. // Offset the y coordinate, since Opera is off pretty
  220. // consistently compared to the other browsers.
  221. if (!!(window.opera && window.opera.version().split(".")[0] < 12)) {
  222. y -= 2;
  223. }
  224. // Determine whether this text already exists at this position.
  225. // If so, mark it for inclusion in the next render pass.
  226. for (var i = 0, position; position = positions[i]; i++) {
  227. if (position.x == x && position.y == y) {
  228. position.active = true;
  229. return;
  230. }
  231. }
  232. // If the text doesn't exist at this position, create a new entry
  233. position = {
  234. active: true,
  235. lines: [],
  236. x: x,
  237. y: y
  238. };
  239. positions.push(position);
  240. // Fill in the x & y positions of each line, adjusting them
  241. // individually for horizontal alignment.
  242. for (var i = 0, line; line = lines[i]; i++) {
  243. if (halign == "center") {
  244. position.lines.push([Math.round(x - line.width / 2), y]);
  245. } else if (halign == "right") {
  246. position.lines.push([Math.round(x - line.width), y]);
  247. } else {
  248. position.lines.push([Math.round(x), y]);
  249. }
  250. y += line.height;
  251. }
  252. };
  253. }
  254. $.plot.plugins.push({
  255. init: init,
  256. options: options,
  257. name: "canvas",
  258. version: "1.0"
  259. });
  260. })(jQuery);