Commit 60ed6b29 authored by David Schnur's avatar David Schnur

Merge pull request #935 from dnschnur/canvas-text

Moved canvas text support into a plugin.
parents 82ab0c46 39698d38
......@@ -275,11 +275,27 @@ you can also set the color of the ticks separately with "tickColor"
(otherwise it's autogenerated as the base color with some
transparency).
You can customize the font used to draw the labels with CSS or
directly with "font". The default value of null means that the font is
read from the font style on the placeholder element (80% the size of
that to be precise). If you set it directly with "font: { ... }", the
format is like this:
You can customize the font used to draw the labels with CSS or directly via the
"font" option. When "font" is null - the default - each tick label is given the
'flot-tick-label' class. For compatibility with Flot 0.7 and earlier the labels
are also given the 'tickLabel' class, but this is deprecated and scheduled to
be removed with the release of version 1.0.0.
To enable more granular control over styles, labels are divided between a set
of text containers, with each holding the labels for one axis. These containers
are given the classes 'flot-text', 'flot-[x|y]-axis', and 'flot-[x|y]#-axis',
where '#' is the number of the axis when there are multiple axes. For example,
the x-axis labels for a simple plot with only one x-axis might look like this:
```html
<div class='flot-text flot-x-axis flot-x1-axis'>
<div class='flot-tick-label'>January 2013</div>
...
</div>
```
For direct control over label styles you can also provide "font" as an object
with this format:
```js
{
......@@ -287,7 +303,8 @@ format is like this:
style: "italic",
weight: "bold",
family: "sans-serif",
variant: "small-caps"
variant: "small-caps",
color: "#545454"
}
```
......
......@@ -17,12 +17,26 @@ standard strftime specifiers, plus one nonstandard specifier for quarters.
Additionally, if a strftime function is found in the Date object's prototype,
it will be used instead of the built-in formatter.
Axis labels are now drawn with canvas text with some parsing to support
newlines. This solves various issues but also means that they no longer
support HTML markup, can be accessed as DOM elements or styled directly with
CSS. Some older browsers lack this function of the canvas API (this doesn't
affect IE); if this is a problem, either continue using an older version of
Flot or try an emulation helper such as canvas-text or Flashcanvas.
Axis tick labels now use the class 'flot-tick-label' instead of 'tickLabel'.
The text containers for each axis now use the classes 'flot-[x|y]-axis' and
'flot-[x|y]#-axis' instead of '[x|y]Axis' and '[x|y]#Axis'. For compatibility
with Flot 0.7 and earlier text will continue to use the old classes as well,
but they are considered deprecated and will be removed in a future version.
A new plugin, jquery.flot.canvas.js, allows axis tick labels to be rendered
directly to the canvas, rather than using HTML elements. This feature can be
toggled with a simple option, making it easy to create interactive plots in the
browser using HTML, then re-render them to canvas for export as an image.
The plugin tries to remain as faithful as possible to the original HTML render,
and goes so far as to automatically extract styles from CSS, to avoid having to
provide a separate set of styles when rendering to canvas. Due to limitations
of the canvas text API, the plugin cannot reproduce certain features, including
HTML markup embedded in labels, and advanced text styles such as 'em' units.
The plugin requires support for canvas text, which may not be present in some
older browsers, even if they support the canvas tag itself. To use the plugin
with these browsers try using a shim such as canvas-text or FlashCanvas.
The base and overlay canvas are now using the CSS classes "flot-base" and
"flot-overlay" to prevent accidental clashes (issue 540).
......@@ -43,7 +57,8 @@ The base and overlay canvas are now using the CSS classes "flot-base" and
- Display time series in different time zones. (patch by Knut Forkalsrud,
issue 141)
- Canvas text support for labels. (sponsored by YCharts.com)
- Added a canvas plugin to enable rendering axis tick labels to the canvas.
(sponsored by YCharts.com, implementation by Ole Laursen and David Schnur)
- Support for setting the interval between redraws of the overlay canvas with
redrawOverlayInterval. (suggested in issue 185)
......
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Flot Examples</title>
<link href="layout.css" rel="stylesheet" type="text/css">
<!--[if lte IE 8]><script language="javascript" type="text/javascript" src="../excanvas.min.js"></script><![endif]-->
<script language="javascript" type="text/javascript" src="../jquery.js"></script>
<script language="javascript" type="text/javascript" src="../jquery.flot.js"></script>
<script language="javascript" type="text/javascript" src="../jquery.flot.canvas.js"></script>
</head>
<body>
<h1>Flot Examples</h1>
<div id="placeholder" style="width:600px;height:300px;"></div>
<p>Simple example. You don't need to specify much to get an
attractive look. Put in a placeholder, make sure you set its
dimensions (otherwise the plot library will barf) and call the
plot function with the data. The axes are automatically
scaled.</p>
<script type="text/javascript">
$(function () {
var d1 = [];
for (var i = 0; i < 14; i += 0.5)
d1.push([i, Math.sin(i)]);
var d2 = [[0, 3], [4, 8], [8, 5], [9, 13]];
// a null signifies separate line segments
var d3 = [[0, 12], [7, 12], null, [7, 2.5], [12, 2.5]];
$.plot($("#placeholder"), [ d1, d2, d3 ], {canvas: true});
});
</script>
</body>
</html>
/* Flot plugin for drawing all elements of a plot on the canvas.
Copyright (c) 2007-2012 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
};
// Cache the prototype hasOwnProperty for faster access
var hasOwnProperty = Object.prototype.hasOwnProperty;
function init(plot, classes) {
var Canvas = classes.Canvas,
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();
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];
if (!info.active) {
delete styleCache[key];
continue;
}
var x = info.x,
y = info.y,
lines = info.lines,
halign = info.halign;
// 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;
}
// TODO: Comments in Ole's implementation indicate that
// some browsers differ in their interpretation of 'top';
// so far I don't see this, but it requires more testing.
// We'll stick with top until this can be verified.
// Original comment was:
// Top alignment would be more natural, but browsers can
// differ a pixel or two in where they consider the top to
// be, so instead we middle align to minimize variation
// between browsers and compensate when calculating the
// coordinates.
context.textBaseline = "top";
for (var i = 0; i < lines.length; ++i) {
var line = lines[i],
linex = x;
// Apply horizontal alignment per-line
if (halign == "center") {
linex -= line.width / 2;
} else if (halign == "right") {
linex -= line.width;
}
// FIXME: LEGACY BROWSER FIX
// AFFECTS: Opera < 12.00
// Round the coordinates, since Opera otherwise
// switches to uglier (probably non-hinted) rendering.
// Also offset the y coordinate, since Opera is off
// pretty consistently compared to the other browsers.
if (!!(window.opera && window.opera.version().split(".")[0] < 12)) {
linex = Math.floor(linex);
y = Math.ceil(y - 2);
}
context.fillText(line.text, linex, y);
y += line.height;
}
}
}
}
}
}
}
context.restore();
};
// Creates (if necessary) and returns a text info object.
//
// When the canvas option is set, the object looks like this:
//
// {
// x: X coordinate at which the text is located.
// x: Y coordinate at which the text is located.
// width: Width of the text's bounding box.
// height: Height of the text's bounding box.
// active: Flag indicating whether the text should be visible.
// 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.
// },
// }
Canvas.prototype.getTextInfo = function(layer, text, font, angle) {
if (!plot.getOptions().canvas) {
return getTextInfo.call(this, layer, text, font, angle);
}
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>").html(text)
.addClass(typeof font === "string" ? font : null)
.css({
position: "absolute",
top: -9999
})
.appendTo(this.getTextLayer(layer));
font = {
style: element.css("font-style"),
variant: element.css("font-variant"),
weight: element.css("font-weight"),
size: parseInt(element.css("font-size"), 10),
family: element.css("font-family"),
color: element.css("color")
};
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] = {
x: null,
y: null,
width: 0,
height: 0,
active: false,
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),
lineWidth, lineHeight;
lineWidth = measured.width;
// Height might not be defined; not in the standard yet
lineHeight = measured.height || font.size;
// Add a bit of margin since font rendering is not pixel
// perfect and cut off letters look bad. This also doubles
// as spacing between lines.
lineHeight += Math.round(font.size * 0.15);
info.width = Math.max(lineWidth, info.width);
info.height += lineHeight;
info.lines.push({
text: lineText,
width: lineWidth,
height: lineHeight
});
}
context.restore();
}
return info;
};
// Adds a text string to the canvas text overlay.
Canvas.prototype.addText = function(layer, x, y, text, font, angle, halign, valign) {
if (!plot.getOptions().canvas) {
return addText.call(this, layer, x, y, text, font, angle, halign, valign);
}
var info = this.getTextInfo(layer, text, font, angle);
info.x = x;
info.y = y;
// Mark the text for inclusion in the next render pass
info.active = true;
// Save horizontal alignment for later; we'll apply it per-line
info.halign = halign;
// Tweak the initial y-position to match vertical alignment
if (valign == "middle") {
info.y = y - info.height / 2;
} else if (valign == "bottom") {
info.y = y - info.height;
}
};
}
$.plot.plugins.push({
init: init,
options: options,
name: "canvas",
version: "1.0"
});
})(jQuery);
......@@ -33,6 +33,388 @@ Licensed under the MIT license.
// the actual Flot code
(function($) {
// Cache the prototype hasOwnProperty for faster access
var hasOwnProperty = Object.prototype.hasOwnProperty;
// Add default styles for tick labels and other text
var STYLES = [
".flot-tick-label {font-size:smaller;color:#545454;}"
];
$(function() {
$("head").prepend("<style id='flot-default-styles'>" + STYLES.join("") + "</style>");
});
///////////////////////////////////////////////////////////////////////////
// The Canvas object is a wrapper around an HTML5 <canvas> tag.
//
// @constructor
// @param {string} cls List of classes to apply to the canvas.
// @param {element} container Element onto which to append the canvas.
//
// Requiring a container is a little iffy, but unfortunately canvas
// operations don't work unless the canvas is attached to the DOM.
function Canvas(cls, container) {
var element = document.createElement("canvas");
element.className = cls;
this.element = element;
$(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 })
.data("canvas", this)
.appendTo(container);
// If HTML5 Canvas isn't available, fall back to Excanvas
if (!element.getContext) {
if (window.G_vmlCanvasManager) {
element = window.G_vmlCanvasManager.initElement(element);
} else {
throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode.");
}
}
var context = element.getContext("2d");
this.context = context;
// Determine the screen's ratio of physical to device-independent
// pixels. This is the ratio between the canvas width that the browser
// advertises and the number of pixels actually present in that space.
// The iPhone 4, for example, has a device-independent width of 320px,
// but its screen is actually 640px wide. It therefore has a pixel
// ratio of 2, while most normal devices have a ratio of 1.
var devicePixelRatio = window.devicePixelRatio || 1,
backingStoreRatio =
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
this.pixelRatio = devicePixelRatio / backingStoreRatio;
// Size the canvas to match the internal dimensions of its container
this.resize(container.width(), container.height());
// Collection of HTML div layers for text overlaid onto the canvas
this.text = {};
// Cache of text fragments and metrics, so we can avoid expensively
// re-calculating them when the plot is re-rendered in a loop.
this._textCache = {};
}
// Resizes the canvas to the given dimensions.
//
// @param {number} width New width of the canvas, in pixels.
// @param {number} width New height of the canvas, in pixels.
Canvas.prototype.resize = function(width, height) {
if (width <= 0 || height <= 0) {
throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height);
}
var element = this.element,
context = this.context,
pixelRatio = this.pixelRatio;
// Resize the canvas, increasing its density based on the display's
// pixel ratio; basically giving it more pixels without increasing the
// size of its element, to take advantage of the fact that retina
// displays have that many more pixels in the same advertised space.
// Resizing should reset the state (excanvas seems to be buggy though)
if (this.width != width) {
element.width = width * pixelRatio;
element.style.width = width + "px";
this.width = width;
}
if (this.height != height) {
element.height = height * pixelRatio;
element.style.height = height + "px";
this.height = height;
}
// Save the context, so we can reset in case we get replotted. The
// restore ensure that we're really back at the initial state, and
// should be safe even if we haven't saved the initial state yet.
context.restore();
context.save();
// Scale the coordinate space to match the display density; so even though we
// may have twice as many pixels, we still want lines and other drawing to
// appear at the same size; the extra pixels will just make them crisper.
context.scale(pixelRatio, pixelRatio);
};
// Clears the entire canvas area, not including any overlaid HTML text
Canvas.prototype.clear = function() {
this.context.clearRect(0, 0, this.width, this.height);
};
// Finishes rendering the canvas, including managing the text overlay.
Canvas.prototype.render = function() {
var cache = this._textCache;
// For each text layer, add elements marked as active that haven't
// already been rendered, and remove those that are no longer active.
for (var layerKey in cache) {
if (hasOwnProperty.call(cache, layerKey)) {
var layer = this.getTextLayer(layerKey),
layerCache = cache[layerKey];
layer.hide();
for (var styleKey in layerCache) {
if (hasOwnProperty.call(layerCache, styleKey)) {
var styleCache = layerCache[styleKey];
for (var key in styleCache) {
if (hasOwnProperty.call(styleCache, key)) {
var info = styleCache[key];
if (info.active) {
if (!info.rendered) {
layer.append(info.element);
info.rendered = true;
}
} else {
delete styleCache[key];
if (info.rendered) {
info.element.detach();
}
}
}
}
}
}
layer.show();
}
}
};
// Creates (if necessary) and returns the text overlay container.
//
// @param {string} classes String of space-separated CSS classes used to
// uniquely identify the text layer.
// @return {object} The jQuery-wrapped text-layer div.
Canvas.prototype.getTextLayer = function(classes) {
var layer = this.text[classes];
// Create the text layer if it doesn't exist
if (layer == null) {
layer = this.text[classes] = $("<div></div>")
.addClass("flot-text " + classes)
.css({
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0
})
.insertAfter(this.element);
}
return layer;
};
// Creates (if necessary) and returns a text info object.
//
// The object looks like this:
//
// {
// width: Width of the text's wrapper div.
// height: Height of the text's wrapper div.
// active: Flag indicating whether the text should be visible.
// rendered: Flag indicating whether the text is currently visible.
// element: The jQuery-wrapped HTML div containing the text.
// }
//
// Canvas maintains a cache of recently-used text info objects; getTextInfo
// either returns the cached element or creates a new entry.
//
// @param {string} layer A string of space-separated CSS classes uniquely
// identifying the layer containing this text.
// @param {string} text Text string to retrieve info for.
// @param {(string|object)=} font Either a string of space-separated CSS
// classes or a font-spec object, defining the text's font and style.
// @param {number=} angle Angle at which to rotate the text, in degrees.
// Angle is currently unused, it will be implemented in the future.
// @return {object} a text info object.
Canvas.prototype.getTextInfo = function(layer, text, font, angle) {
var textStyle, layerCache, styleCache, info;
// Cast the value to a string, in case we were given a number or such
text = "" + text;
// If the font is a font-spec object, generate a CSS font 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 we can't find a matching element in our cache, create a new one
if (info == null) {
var element = $("<div></div>").html(text)
.css({
position: "absolute",
top: -9999
})
.appendTo(this.getTextLayer(layer));
if (typeof font === "object") {
element.css({
font: textStyle,
color: font.color
});
} else if (typeof font === "string") {
element.addClass(font);
}
info = styleCache[text] = {
active: false,
rendered: false,
element: element,
width: element.outerWidth(true),
height: element.outerHeight(true)
};
element.detach();
}
return info;
};
// Adds a text string to the canvas text overlay.
//
// The text isn't drawn immediately; it is marked as rendering, which will
// result in its addition to the canvas on the next render pass.
//
// @param {string} layer A string of space-separated CSS classes uniquely
// identifying the layer containing this text.
// @param {number} x X coordinate at which to draw the text.
// @param {number} y Y coordinate at which to draw the text.
// @param {string} text Text string to draw.
// @param {(string|object)=} font Either a string of space-separated CSS
// classes or a font-spec object, defining the text's font and style.
// @param {number=} angle Angle at which to rotate the text, in degrees.
// Angle is currently unused, it will be implemented in the future.
// @param {string=} halign Horizontal alignment of the text; either "left",
// "center" or "right".
// @param {string=} valign Vertical alignment of the text; either "top",
// "middle" or "bottom".
Canvas.prototype.addText = function(layer, x, y, text, font, angle, halign, valign) {
var info = this.getTextInfo(layer, text, font, angle);
// Mark the div for inclusion in the next render pass
info.active = true;
// Tweak the div's position to match the text's alignment
if (halign == "center") {
x -= info.width / 2;
} else if (halign == "right") {
x -= info.width;
}
if (valign == "middle") {
y -= info.height / 2;
} else if (valign == "bottom") {
y -= info.height;
}
// Move the element to its final position within the container
info.element.css({
top: parseInt(y, 10),
left: parseInt(x, 10)
});
};
// Removes one or more text strings from the canvas text overlay.
//
// If no parameters are given, all text within the layer is removed.
// The text is not actually removed; it is simply marked as inactive, which
// will result in its removal on the next render pass.
//
// @param {string} layer A string of space-separated CSS classes uniquely
// identifying the layer containing this text.
// @param {string} text Text string to remove.
// @param {(string|object)=} font Either a string of space-separated CSS
// classes or a font-spec object, defining the text's font and style.
// @param {number=} angle Angle at which the text is rotated, in degrees.
// Angle is currently unused, it will be implemented in the future.
Canvas.prototype.removeText = function(layer, text, font, angle) {
if (text == null) {
var layerCache = this._textCache[layer];
if (layerCache != null) {
for (var styleKey in layerCache) {
if (hasOwnProperty.call(layerCache, styleKey)) {
var styleCache = layerCache[styleKey]
for (var key in styleCache) {
if (hasOwnProperty.call(styleCache, key)) {
styleCache[key].active = false;
}
}
}
}
}
} else {
this.getTextInfo(layer, text, font, angle).active = false;
}
};
///////////////////////////////////////////////////////////////////////////
// The top-level container for the entire plot.
function Plot(placeholder, data_, options_, plugins) {
// data is on the form:
// [ series1, series2 ... ]
......@@ -148,13 +530,12 @@ Licensed under the MIT license.
},
hooks: {}
},
canvas = null, // the canvas for the plot itself
surface = null, // the canvas for the plot itself
overlay = null, // canvas for interactive stuff on top of plot
eventHolder = null, // jQuery object that events should be bound to
ctx = null, octx = null,
xaxes = [], yaxes = [],
plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
canvasWidth = 0, canvasHeight = 0,
plotWidth = 0, plotHeight = 0,
hooks = {
processOptions: [],
......@@ -175,7 +556,7 @@ Licensed under the MIT license.
plot.setupGrid = setupGrid;
plot.draw = draw;
plot.getPlaceholder = function() { return placeholder; };
plot.getCanvas = function() { return canvas; };
plot.getCanvas = function() { return surface.element; };
plot.getPlotOffset = function() { return plotOffset; };
plot.width = function () { return plotWidth; };
plot.height = function () { return plotHeight; };
......@@ -210,9 +591,10 @@ Licensed under the MIT license.
};
plot.shutdown = shutdown;
plot.resize = function () {
getCanvasDimensions();
resizeCanvas(canvas);
resizeCanvas(overlay);
var width = placeholder.width(),
height = placeholder.height();
surface.resize(width, height);
overlay.resize(width, height);
};
// public attributes
......@@ -235,16 +617,22 @@ Licensed under the MIT license.
}
function initPlugins() {
// References to key classes, allowing plugins to modify them
var classes = {
Canvas: Canvas
};
for (var i = 0; i < plugins.length; ++i) {
var p = plugins[i];
p.init(plot);
p.init(plot, classes);
if (p.options)
$.extend(true, options, p.options);
}
}
function parseOptions(opts) {
var i;
$.extend(true, options, opts);
......@@ -263,12 +651,44 @@ Licensed under the MIT license.
if (options.grid.tickColor == null)
options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();
// fill in defaults in axes, copy at least always the
// first as the rest of the code assumes it'll be there
for (i = 0; i < Math.max(1, options.xaxes.length); ++i)
options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]);
for (i = 0; i < Math.max(1, options.yaxes.length); ++i)
options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]);
// Fill in defaults for axis options, including any unspecified
// font-spec fields, if a font-spec was provided.
// If no x/y axis options were provided, create one of each anyway,
// since the rest of the code assumes that they exist.
var i, axisOptions, axisCount,
fontDefaults = {
style: placeholder.css("font-style"),
size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)),
variant: placeholder.css("font-variant"),
weight: placeholder.css("font-weight"),
family: placeholder.css("font-family")
};
axisCount = options.xaxes.length || 1;
for (i = 0; i < axisCount; ++i) {
axisOptions = $.extend(true, {}, options.xaxis, options.xaxes[i]);
options.xaxes[i] = axisOptions;
if (axisOptions.font) {
axisOptions.font = $.extend({}, fontDefaults, axisOptions.font);
if (!axisOptions.font.color) {
axisOptions.font.color = axisOptions.color;
}
}
}
axisCount = options.yaxes.length || 1;
for (i = 0; i < axisCount; ++i) {
axisOptions = $.extend(true, {}, options.yaxis, options.yaxes[i]);
options.yaxes[i] = axisOptions;
if (axisOptions.font) {
axisOptions.font = $.extend({}, fontDefaults, axisOptions.font);
if (!axisOptions.font.color) {
axisOptions.font.color = axisOptions.color;
}
}
}
// backwards compatibility, to be removed in future
if (options.xaxis.noTicks && options.xaxis.ticks == null)
......@@ -730,117 +1150,12 @@ Licensed under the MIT license.
});
}
//////////////////////////////////////////////////////////////////////////////////
// Returns the display's ratio between physical and device-independent pixels.
//
// This is the ratio between the width that the browser advertises and the number
// of pixels actually available in that space. The iPhone 4, for example, has a
// device-independent width of 320px, but its screen is actually 640px wide. It
// therefore has a pixel ratio of 2, while most normal devices have a ratio of 1.
function getPixelRatio(cctx) {
var devicePixelRatio = window.devicePixelRatio || 1;
var backingStoreRatio =
cctx.webkitBackingStorePixelRatio ||
cctx.mozBackingStorePixelRatio ||
cctx.msBackingStorePixelRatio ||
cctx.oBackingStorePixelRatio ||
cctx.backingStorePixelRatio || 1;
return devicePixelRatio / backingStoreRatio;
}
function makeCanvas(cls) {
var c = document.createElement('canvas');
c.className = cls;
$(c).css({ direction: "ltr", position: "absolute", left: 0, top: 0 })
.appendTo(placeholder);
// If HTML5 Canvas isn't available, fall back to Excanvas
if (!c.getContext) {
if (window.G_vmlCanvasManager) {
c = window.G_vmlCanvasManager.initElement(c);
} else {
throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode.");
}
}
var cctx = c.getContext("2d");
// Increase the canvas density based on the display's pixel ratio; basically
// giving the canvas more pixels without increasing the size of its element,
// to take advantage of the fact that retina displays have that many more
// pixels than they actually use for page & element widths.
var pixelRatio = getPixelRatio(cctx);
c.width = canvasWidth * pixelRatio;
c.height = canvasHeight * pixelRatio;
c.style.width = canvasWidth + "px";
c.style.height = canvasHeight + "px";
// Save the context so we can reset in case we get replotted
cctx.save();
// Scale the coordinate space to match the display density; so even though we
// may have twice as many pixels, we still want lines and other drawing to
// appear at the same size; the extra pixels will just make them crisper.
cctx.scale(pixelRatio, pixelRatio);
return c;
}
function getCanvasDimensions() {
canvasWidth = placeholder.width();
canvasHeight = placeholder.height();
if (canvasWidth <= 0 || canvasHeight <= 0)
throw new Error("Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight);
}
function resizeCanvas(c) {
var cctx = c.getContext("2d");
// Handle pixel ratios > 1 for retina displays, as explained in makeCanvas
var pixelRatio = getPixelRatio(cctx);
// Resizing should reset the state (excanvas seems to be buggy though)
if (c.style.width != canvasWidth) {
c.width = canvasWidth * pixelRatio;
c.style.width = canvasWidth + "px";
}
if (c.style.height != canvasHeight) {
c.height = canvasHeight * pixelRatio;
c.style.height = canvasHeight + "px";
}
// so try to get back to the initial state (even if it's
// gone now, this should be safe according to the spec)
cctx.restore();
// and save again
cctx.save();
// Apply scaling for retina displays, as explained in makeCanvas
cctx.scale(pixelRatio, pixelRatio);
}
function setupCanvases() {
var reused,
existingCanvas = placeholder.children("canvas.flot-base"),
existingSurface = placeholder.children("canvas.flot-base"),
existingOverlay = placeholder.children("canvas.flot-overlay");
if (existingCanvas.length == 0 || existingOverlay == 0) {
if (existingSurface.length == 0 || existingOverlay == 0) {
// init everything
placeholder.html(""); // make sure placeholder is clear
......@@ -850,27 +1165,25 @@ Licensed under the MIT license.
if (placeholder.css("position") == 'static')
placeholder.css("position", "relative"); // for positioning labels and overlay
getCanvasDimensions();
canvas = makeCanvas("flot-base");
overlay = makeCanvas("flot-overlay"); // overlay canvas for interactive features
surface = new Canvas("flot-base", placeholder);
overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features
reused = false;
}
else {
// reuse existing elements
canvas = existingCanvas.get(0);
overlay = existingOverlay.get(0);
surface = existingSurface.data("canvas");
overlay = existingOverlay.data("canvas");
reused = true;
}
ctx = canvas.getContext("2d");
octx = overlay.getContext("2d");
ctx = surface.context;
octx = overlay.context;
// define which element we're listening for events on
eventHolder = $(overlay);
eventHolder = $(overlay.element);
if (reused) {
// run shutdown in the old plot object
......@@ -880,11 +1193,12 @@ Licensed under the MIT license.
plot.resize();
// make sure overlay pixels are cleared (canvas is cleared when we redraw)
octx.clearRect(0, 0, canvasWidth, canvasHeight);
overlay.clear();
// then whack any remaining obvious garbage left
eventHolder.unbind();
placeholder.children().not([canvas, overlay]).remove();
placeholder.children(":not(.flot-base,.flot-overlay,.flot-text)").remove();
}
// save in case we get replotted
......@@ -956,53 +1270,27 @@ Licensed under the MIT license.
}
function measureTickLabels(axis) {
var opts = axis.options, ticks = axis.ticks || [],
axisw = opts.labelWidth || 0, axish = opts.labelHeight || 0,
f = axis.font;
ctx.save();
ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px '" + f.family + "'";
legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis",
layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles,
font = opts.font || "flot-tick-label tickLabel";
for (var i = 0; i < ticks.length; ++i) {
var t = ticks[i];
t.lines = [];
t.width = t.height = 0;
var t = ticks[i];
if (!t.label)
continue;
// accept various kinds of newlines, including HTML ones
// (you can actually split directly on regexps in Javascript,
// but IE < 9 is unfortunately broken)
var lines = (t.label + "").replace(/<br ?\/?>|\r\n|\r/g, "\n").split("\n");
for (var j = 0; j < lines.length; ++j) {
var line = { text: lines[j] },
m = ctx.measureText(line.text);
line.width = m.width;
// m.height might not be defined, not in the
// standard yet
line.height = m.height != null ? m.height : f.size;
// add a bit of margin since font rendering is
// not pixel perfect and cut off letters look
// bad, this also doubles as spacing between
// lines
line.height += Math.round(f.size * 0.15);
t.width = Math.max(line.width, t.width);
t.height += line.height;
t.lines.push(line);
}
var info = surface.getTextInfo(layer, t.label, font);
if (opts.labelWidth == null)
axisw = Math.max(axisw, t.width);
axisw = Math.max(axisw, info.width);
if (opts.labelHeight == null)
axish = Math.max(axish, t.height);
axish = Math.max(axish, info.height);
}
ctx.restore();
axis.labelWidth = Math.ceil(axisw);
axis.labelHeight = Math.ceil(axish);
......@@ -1053,7 +1341,7 @@ Licensed under the MIT license.
if (pos == "bottom") {
plotOffset.bottom += lh + axisMargin;
axis.box = { top: canvasHeight - plotOffset.bottom, height: lh };
axis.box = { top: surface.height - plotOffset.bottom, height: lh };
}
else {
axis.box = { top: plotOffset.top + axisMargin, height: lh };
......@@ -1069,7 +1357,7 @@ Licensed under the MIT license.
}
else {
plotOffset.right += lw + axisMargin;
axis.box = { left: canvasWidth - plotOffset.right, width: lw };
axis.box = { left: surface.width - plotOffset.right, width: lw };
}
}
......@@ -1085,11 +1373,11 @@ Licensed under the MIT license.
// dimension, we can set the remaining dimension coordinates
if (axis.direction == "x") {
axis.box.left = plotOffset.left - axis.labelWidth / 2;
axis.box.width = canvasWidth - plotOffset.left - plotOffset.right + axis.labelWidth;
axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth;
}
else {
axis.box.top = plotOffset.top - axis.labelHeight / 2;
axis.box.height = canvasHeight - plotOffset.bottom - plotOffset.top + axis.labelHeight;
axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight;
}
}
......@@ -1161,14 +1449,6 @@ Licensed under the MIT license.
});
if (showGrid) {
// determine from the placeholder the font size ~ height of font ~ 1 em
var fontDefaults = {
style: placeholder.css("font-style"),
size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)),
variant: placeholder.css("font-variant"),
weight: placeholder.css("font-weight"),
family: placeholder.css("font-family")
};
var allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; });
......@@ -1177,9 +1457,7 @@ Licensed under the MIT license.
setupTickGeneration(axis);
setTicks(axis);
snapRangeToTicks(axis, axis.ticks);
// find labelWidth/Height for axis
axis.font = $.extend({}, fontDefaults, axis.options.font);
measureTickLabels(axis);
});
......@@ -1198,14 +1476,18 @@ Licensed under the MIT license.
});
}
plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
plotWidth = surface.width - plotOffset.left - plotOffset.right;
plotHeight = surface.height - plotOffset.bottom - plotOffset.top;
// now we got the proper plot dimensions, we can compute the scaling
$.each(axes, function (_, axis) {
setTransformationHelpers(axis);
});
if (showGrid) {
drawAxisLabels();
}
insertLegend();
}
......@@ -1258,7 +1540,7 @@ Licensed under the MIT license.
else
// heuristic based on the model a*sqrt(x) fitted to
// some data points that seemed reasonable
noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight);
noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height);
axis.delta = (axis.max - axis.min) / noTicks;
......@@ -1313,7 +1595,7 @@ Licensed under the MIT license.
axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
axis.tickSize = opts.tickSize || size;
start = floorInBase(axis.min, axis.tickSize)
start = floorInBase(axis.min, axis.tickSize);
do {
prev = v;
......@@ -1429,7 +1711,8 @@ Licensed under the MIT license.
}
function draw() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
surface.clear();
executeHooks(hooks.drawBackground, [ctx]);
......@@ -1441,7 +1724,6 @@ Licensed under the MIT license.
if (grid.show && !grid.aboveData) {
drawGrid();
drawAxisLabels();
}
for (var i = 0; i < series.length; ++i) {
......@@ -1453,8 +1735,9 @@ Licensed under the MIT license.
if (grid.show && grid.aboveData) {
drawGrid();
drawAxisLabels();
}
surface.render();
}
function extractRange(ranges, coord) {
......@@ -1726,74 +2009,48 @@ Licensed under the MIT license.
}
function drawAxisLabels() {
ctx.save();
$.each(allAxes(), function (_, axis) {
if (!axis.show || axis.ticks.length == 0)
return;
var box = axis.box, f = axis.font;
// placeholder.append('<div style="position:absolute;opacity:0.10;background-color:red;left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.height + 'px"></div>') // debug
ctx.fillStyle = axis.options.color;
// Important: Don't use quotes around axis.font.family! Just around single
// font names like 'Times New Roman' that have a space or special character in it.
ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px " + f.family;
ctx.textAlign = "start";
// middle align the labels - top would be more
// natural, but browsers can differ a pixel or two in
// where they consider the top to be, so instead we
// middle align to minimize variation between browsers
// and compensate when calculating the coordinates
ctx.textBaseline = "middle";
var box = axis.box,
legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis",
layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles,
font = axis.options.font || "flot-tick-label tickLabel",
tick, x, y, halign, valign;
surface.removeText(layer);
for (var i = 0; i < axis.ticks.length; ++i) {
var tick = axis.ticks[i];
tick = axis.ticks[i];
if (!tick.label || tick.v < axis.min || tick.v > axis.max)
continue;
var x, y, offset = 0, line;
for (var k = 0; k < tick.lines.length; ++k) {
line = tick.lines[k];
if (axis.direction == "x") {
x = plotOffset.left + axis.p2c(tick.v) - line.width/2;
if (axis.position == "bottom")
y = box.top + box.padding;
else
y = box.top + box.height - box.padding - tick.height;
if (axis.direction == "x") {
halign = "center";
x = plotOffset.left + axis.p2c(tick.v);
if (axis.position == "bottom") {
y = box.top + box.padding;
} else {
y = box.top + box.height - box.padding;
valign = "bottom";
}
else {
y = plotOffset.top + axis.p2c(tick.v) - tick.height/2;
if (axis.position == "left")
x = box.left + box.width - box.padding - line.width;
else
x = box.left + box.padding;
} else {
valign = "middle";
y = plotOffset.top + axis.p2c(tick.v);
if (axis.position == "left") {
x = box.left + box.width - box.padding;
halign = "right";
} else {
x = box.left + box.padding;
}
// account for middle aligning and line number
y += line.height/2 + offset;
offset += line.height;
if (!!(window.opera && window.opera.version().split('.')[0] < 12)) {
// FIXME: LEGACY BROWSER FIX
// AFFECTS: Opera < 12.00
// round the coordinates since Opera
// otherwise switches to more ugly
// rendering (probably non-hinted) and
// offset the y coordinates since it seems
// to be off pretty consistently compared
// to the other browsers
x = Math.floor(x);
y = Math.ceil(y - 2);
}
ctx.fillText(line.text, x, y);
}
surface.addText(layer, x, y, tick.label, font, null, halign, valign);
}
});
ctx.restore();
}
function drawSeries(series) {
......@@ -2554,7 +2811,7 @@ Licensed under the MIT license.
// draw highlights
octx.save();
octx.clearRect(0, 0, canvasWidth, canvasHeight);
overlay.clear();
octx.translate(plotOffset.left, plotOffset.top);
var i, hi;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment