summaryrefslogtreecommitdiff
path: root/jquery.flot.canvas.js
diff options
context:
space:
mode:
Diffstat (limited to 'jquery.flot.canvas.js')
-rw-r--r--jquery.flot.canvas.js345
1 files changed, 345 insertions, 0 deletions
diff --git a/jquery.flot.canvas.js b/jquery.flot.canvas.js
new file mode 100644
index 0000000..d94b961
--- /dev/null
+++ b/jquery.flot.canvas.js
@@ -0,0 +1,345 @@
+/* Flot plugin for drawing all elements of a plot on the canvas.
+
+Copyright (c) 2007-2013 IOLA and Ole Laursen.
+Licensed under the MIT license.
+
+Flot normally produces certain elements, like axis labels and the legend, using
+HTML elements. This permits greater interactivity and customization, and often
+looks better, due to cross-browser canvas text inconsistencies and limitations.
+
+It can also be desirable to render the plot entirely in canvas, particularly
+if the goal is to save it as an image, or if Flot is being used in a context
+where the HTML DOM does not exist, as is the case within Node.js. This plugin
+switches out Flot's standard drawing operations for canvas-only replacements.
+
+Currently the plugin supports only axis labels, but it will eventually allow
+every element of the plot to be rendered directly to canvas.
+
+The plugin supports these options:
+
+{
+ canvas: boolean
+}
+
+The "canvas" option controls whether full canvas drawing is enabled, making it
+possible to toggle on and off. This is useful when a plot uses HTML text in the
+browser, but needs to redraw with canvas text when exporting as an image.
+
+*/
+
+(function($) {
+
+ var options = {
+ canvas: true
+ };
+
+ var render, getTextInfo, addText;
+
+ // Cache the prototype hasOwnProperty for faster access
+
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+ function init(plot, classes) {
+
+ var Canvas = classes.Canvas;
+
+ // We only want to replace the functions once; the second time around
+ // we would just get our new function back. This whole replacing of
+ // prototype functions is a disaster, and needs to be changed ASAP.
+
+ if (render == null) {
+ getTextInfo = Canvas.prototype.getTextInfo,
+ addText = Canvas.prototype.addText,
+ render = Canvas.prototype.render;
+ }
+
+ // Finishes rendering the canvas, including overlaid text
+
+ Canvas.prototype.render = function() {
+
+ if (!plot.getOptions().canvas) {
+ return render.call(this);
+ }
+
+ var context = this.context,
+ cache = this._textCache;
+
+ // For each text layer, render elements marked as active
+
+ context.save();
+ context.textBaseline = "middle";
+
+ for (var layerKey in cache) {
+ if (hasOwnProperty.call(cache, layerKey)) {
+ var layerCache = cache[layerKey];
+ for (var styleKey in layerCache) {
+ if (hasOwnProperty.call(layerCache, styleKey)) {
+ var styleCache = layerCache[styleKey],
+ updateStyles = true;
+ for (var key in styleCache) {
+ if (hasOwnProperty.call(styleCache, key)) {
+
+ var info = styleCache[key],
+ positions = info.positions,
+ lines = info.lines;
+
+ // Since every element at this level of the cache have the
+ // same font and fill styles, we can just change them once
+ // using the values from the first element.
+
+ if (updateStyles) {
+ context.fillStyle = info.font.color;
+ context.font = info.font.definition;
+ updateStyles = false;
+ }
+
+ for (var i = 0, position; position = positions[i]; i++) {
+ if (position.active) {
+ for (var j = 0, line; line = position.lines[j]; j++) {
+ context.fillText(lines[j].text, line[0], line[1]);
+ }
+ } else {
+ positions.splice(i--, 1);
+ }
+ }
+
+ if (positions.length == 0) {
+ delete styleCache[key];
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ context.restore();
+ };
+
+ // Creates (if necessary) and returns a text info object.
+ //
+ // When the canvas option is set, the object looks like this:
+ //
+ // {
+ // width: Width of the text's bounding box.
+ // height: Height of the text's bounding box.
+ // positions: Array of positions at which this text is drawn.
+ // lines: [{
+ // height: Height of this line.
+ // widths: Width of this line.
+ // text: Text on this line.
+ // }],
+ // font: {
+ // definition: Canvas font property string.
+ // color: Color of the text.
+ // },
+ // }
+ //
+ // The positions array contains objects that look like this:
+ //
+ // {
+ // active: Flag indicating whether the text should be visible.
+ // lines: Array of [x, y] coordinates at which to draw the line.
+ // x: X coordinate at which to draw the text.
+ // y: Y coordinate at which to draw the text.
+ // }
+
+ Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) {
+
+ if (!plot.getOptions().canvas) {
+ return getTextInfo.call(this, layer, text, font, angle, width);
+ }
+
+ var textStyle, layerCache, styleCache, info;
+
+ // Cast the value to a string, in case we were given a number
+
+ text = "" + text;
+
+ // If the font is a font-spec object, generate a CSS definition
+
+ if (typeof font === "object") {
+ textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family;
+ } else {
+ textStyle = font;
+ }
+
+ // Retrieve (or create) the cache for the text's layer and styles
+
+ layerCache = this._textCache[layer];
+
+ if (layerCache == null) {
+ layerCache = this._textCache[layer] = {};
+ }
+
+ styleCache = layerCache[textStyle];
+
+ if (styleCache == null) {
+ styleCache = layerCache[textStyle] = {};
+ }
+
+ info = styleCache[text];
+
+ if (info == null) {
+
+ var context = this.context;
+
+ // If the font was provided as CSS, create a div with those
+ // classes and examine it to generate a canvas font spec.
+
+ if (typeof font !== "object") {
+
+ var element = $("<div>&nbsp;</div>")
+ .css("position", "absolute")
+ .addClass(typeof font === "string" ? font : null)
+ .appendTo(this.getTextLayer(layer));
+
+ font = {
+ lineHeight: element.height(),
+ style: element.css("font-style"),
+ variant: element.css("font-variant"),
+ weight: element.css("font-weight"),
+ family: element.css("font-family"),
+ color: element.css("color")
+ };
+
+ // Setting line-height to 1, without units, sets it equal
+ // to the font-size, even if the font-size is abstract,
+ // like 'smaller'. This enables us to read the real size
+ // via the element's height, working around browsers that
+ // return the literal 'smaller' value.
+
+ font.size = element.css("line-height", 1).height();
+
+ element.remove();
+ }
+
+ textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family;
+
+ // Create a new info object, initializing the dimensions to
+ // zero so we can count them up line-by-line.
+
+ info = styleCache[text] = {
+ width: 0,
+ height: 0,
+ positions: [],
+ lines: [],
+ font: {
+ definition: textStyle,
+ color: font.color
+ }
+ };
+
+ context.save();
+ context.font = textStyle;
+
+ // Canvas can't handle multi-line strings; break on various
+ // newlines, including HTML brs, to build a list of lines.
+ // Note that we could split directly on regexps, but IE < 9 is
+ // broken; revisit when we drop IE 7/8 support.
+
+ var lines = (text + "").replace(/<br ?\/?>|\r\n|\r/g, "\n").split("\n");
+
+ for (var i = 0; i < lines.length; ++i) {
+
+ var lineText = lines[i],
+ measured = context.measureText(lineText);
+
+ info.width = Math.max(measured.width, info.width);
+ info.height += font.lineHeight;
+
+ info.lines.push({
+ text: lineText,
+ width: measured.width,
+ height: font.lineHeight
+ });
+ }
+
+ context.restore();
+ }
+
+ return info;
+ };
+
+ // Adds a text string to the canvas text overlay.
+
+ Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) {
+
+ if (!plot.getOptions().canvas) {
+ return addText.call(this, layer, x, y, text, font, angle, width, halign, valign);
+ }
+
+ var info = this.getTextInfo(layer, text, font, angle, width),
+ positions = info.positions,
+ lines = info.lines;
+
+ // Text is drawn with baseline 'middle', which we need to account
+ // for by adding half a line's height to the y position.
+
+ y += info.height / lines.length / 2;
+
+ // Tweak the initial y-position to match vertical alignment
+
+ if (valign == "middle") {
+ y = Math.round(y - info.height / 2);
+ } else if (valign == "bottom") {
+ y = Math.round(y - info.height);
+ } else {
+ y = Math.round(y);
+ }
+
+ // FIXME: LEGACY BROWSER FIX
+ // AFFECTS: Opera < 12.00
+
+ // Offset the y coordinate, since Opera is off pretty
+ // consistently compared to the other browsers.
+
+ if (!!(window.opera && window.opera.version().split(".")[0] < 12)) {
+ y -= 2;
+ }
+
+ // Determine whether this text already exists at this position.
+ // If so, mark it for inclusion in the next render pass.
+
+ for (var i = 0, position; position = positions[i]; i++) {
+ if (position.x == x && position.y == y) {
+ position.active = true;
+ return;
+ }
+ }
+
+ // If the text doesn't exist at this position, create a new entry
+
+ position = {
+ active: true,
+ lines: [],
+ x: x,
+ y: y
+ };
+
+ positions.push(position);
+
+ // Fill in the x & y positions of each line, adjusting them
+ // individually for horizontal alignment.
+
+ for (var i = 0, line; line = lines[i]; i++) {
+ if (halign == "center") {
+ position.lines.push([Math.round(x - line.width / 2), y]);
+ } else if (halign == "right") {
+ position.lines.push([Math.round(x - line.width), y]);
+ } else {
+ position.lines.push([Math.round(x), y]);
+ }
+ y += line.height;
+ }
+ };
+ }
+
+ $.plot.plugins.push({
+ init: init,
+ options: options,
+ name: "canvas",
+ version: "1.0"
+ });
+
+})(jQuery);