123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- /* Flot plugin for drawing all elements of a plot on the canvas.
- Copyright (c) 2007-2014 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> </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);
|