flot/flot — pull request #935 by dnschnur (2013-01-19 04:41:56+00:00)
Title: Canvas text

This is my project to revert Flot to HTML text, while enabling canvas text via a plugin. It still requires cleanup, optimization, and testing in several areas, but I'd appreciate any feedback on the design, tradeoffs, whether the change is even worth it, etc. My goals were:

1. Revert to HTML/CSS text
2. Add a plugin to draw text (and eventually other things, like the legend) to the canvas
3. Generalize text drawing so it can be used for axis labels, titles, etc.

##### Design #####

I started by factoring some of Flot's canvas management code out into a new class called Canvas. While I don't want to go overboard with objects, I would like to see a little more encapsulation in the codebase. This allowed me to remove some duplicate code.

I then gave the Canvas class two methods: drawText and getTextInfo:

- getTextInfo(text, font, angle)
- drawText(x, y, text, font, angle, halign, valign)

The base implementation of getTextInfo creates a new div, assigns it font/style via either class or inline style, measures its dimensions, then saves the dimensions and div HTML in a cache keyed on the text, font and angle.

The base implementation of drawText uses getTextInfo to load an info object, calculates horizontal/vertical alignment based on the text's dimensions, then appends the div HTML to a string buffer.

The Canvas class has a method render() that is called on redraw. It creates, if it doesn't already exist, a new div in front of the canvas to act as a text layer. It then dumps the buffer full of text divs into this layer.

Plugins now receive an object holding Flot's classes - currently just Canvas - as part of their init method. This gives them deeper access to the library's internals than the existing hooks system.

The canvas text plugin uses this to override drawText and getTextInfo with its own implementations. Its getTextInfo splits the text into lines, uses canvas measureText to find the dimensions of each line, and saves the results as an info object in the same kind of cache as the base implementation. Its drawText method uses getTextInfo to load the info, then draws each line separately using canvas fillText.

This is mostly the same code as already exists in 0.8.0-alpha, with one key difference: if the font is provided as CSS styles, and a font-spec object is not provided, getTextInfo will create a dummy element and derive the font definition from its styles. This is quite nice, since it means that a regular HTML-text plot can be redrawn using canvas text without any other changes in options.

##### Advantages #####

The abstraction allows the same code to be re-used for all text drawing, not just axis labels. This should make it trivial to implement axis labels, plot titles, and data labels in the future.

The way in which the canvas plugin overrides these methods lets the change transparently affect all text, and can be toggled via an option. The canvas plugin's ability to use CSS makes this transition even cleaner.

HTML text makes it possible to embed mark-up in labels (#772), though it's not clear how often this is useful. It allows text to be styled via CSS, which is desirable for some users, though probably not critical. It enables the use of em units in font sizes (#898), plus various other features like letter-spacing and text-shadow.

HTML text is reliably good-looking; some browsers render canvas text with inferior quality (#789), and others (#71, #815) have bugs that cause measureText to return inaccurate values.

The fact that HTML text is vector-based makes it look nicer when exporting the page to PDF (#913), although there may be work-arounds to that particular problem.

##### Disadvantages #####

The abstraction greatly reduces performance in the base case, since it prevents us from using the 0.7 optimization where all labels from each axis are added to a single div and measured together. It is equally slow with canvas drawing enabled if CSS classes are used, since it must still add every element to the DOM individually to query styles.

On the other hand there are normally not many axis labels, and the use of a cache mostly eliminates the performance hit for plots that are frequently redrawn.

It is also slower than 0.7 in that labels are cleared and re-created on every redraw, instead of only when setupGrid is called. This also prevents external interaction with the axes, since the divs are destroyed on redraw. The move to canvas text in 0.8.0-alpha already had both of these effects, though, and over many months nobody has complained, so it's probably not a big deal.

A canvas-only implementation is naturally cleaner in code than one that needs to switch.