forked from flot/flot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
jquery.flot.canvas.js
342 lines (250 loc) · 9.2 KB
/
jquery.flot.canvas.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
/* 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();
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);