Commit 4e027816 authored by olau@iola.dk's avatar olau@iola.dk

Refactor replot behaviour so Flot tries to reuse the existing canvas,

adding shutdown() methods to the plot (based on patch by Ryley
Breiddal, issue 269). This prevents a memory leak in Chrome and
hopefully makes replotting faster for those who are using $.plot
instead of .setData()/.draw(). Also update jQuery to 1.5.1 to prevent
IE leaks fixed some time ago in jQuery.


git-svn-id: https://flot.googlecode.com/svn/trunk@317 1e0a6537-2640-0410-bfb7-f154510ff394
parent e47b4315
...@@ -901,7 +901,19 @@ can call: ...@@ -901,7 +901,19 @@ can call:
o = pointOffset({ x: xpos, y: ypos, xaxis: 2, yaxis: 3 }) o = pointOffset({ x: xpos, y: ypos, xaxis: 2, yaxis: 3 })
// o.left and o.top now contains the offset within the div // o.left and o.top now contains the offset within the div
- resize()
Tells Flot to resize the drawing canvas to the size of the
placeholder. You need to run setupGrid() and draw() afterwards as
canvas resizing is a destructive operation. This is used
internally by the resize plugin.
- shutdown()
Cleans up any event handlers Flot has currently registered. This
is used internally.
There are also some members that let you peek inside the internal There are also some members that let you peek inside the internal
workings of Flot which is useful in some cases. Note that if you change workings of Flot which is useful in some cases. Note that if you change
...@@ -998,6 +1010,8 @@ Here's an overview of the phases Flot goes through: ...@@ -998,6 +1010,8 @@ Here's an overview of the phases Flot goes through:
7. Responding to events, if any 7. Responding to events, if any
8. Shutdown: this mostly happens in case a plot is overwritten
Each hook is simply a function which is put in the appropriate array. Each hook is simply a function which is put in the appropriate array.
You can add them through the "hooks" option, and they are also available You can add them through the "hooks" option, and they are also available
after the plot is constructed as the "hooks" attribute on the returned after the plot is constructed as the "hooks" attribute on the returned
...@@ -1146,6 +1160,16 @@ hooks in the plugins bundled with Flot. ...@@ -1146,6 +1160,16 @@ hooks in the plugins bundled with Flot.
crosshair plugin for an example. crosshair plugin for an example.
- shutdown [phase 8]
function (plot, eventHolder)
Run when plot.shutdown() is called, which usually only happens in
case a plot is overwritten by a new plot. If you're writing a
plugin that adds extra DOM elements or event handlers, you should
add a callback to clean up after you. Take a look at the section in
PLUGINS.txt for more info.
Plugins Plugins
------- -------
...@@ -1163,7 +1187,7 @@ Here's a brief explanation of how the plugin plumbings work: ...@@ -1163,7 +1187,7 @@ Here's a brief explanation of how the plugin plumbings work:
Each plugin registers itself in the global array $.plot.plugins. When Each plugin registers itself in the global array $.plot.plugins. When
you make a new plot object with $.plot, Flot goes through this array you make a new plot object with $.plot, Flot goes through this array
calling the "init" function of each plugin and merging default options calling the "init" function of each plugin and merging default options
from the plugin's "option" attribute. The init function gets a from the "option" attribute of the plugin. The init function gets a
reference to the plot object created and uses this to register hooks reference to the plot object created and uses this to register hooks
and add new public methods if needed. and add new public methods if needed.
......
...@@ -94,8 +94,14 @@ Changes: ...@@ -94,8 +94,14 @@ Changes:
- The version comment is now included in the minified jquery.flot.min.js. - The version comment is now included in the minified jquery.flot.min.js.
- New options.grid.minBorderMargin for adjusting the minimum margin - New options.grid.minBorderMargin for adjusting the minimum margin
provided around the border (based on patch by corani, issue 188). provided around the border (based on patch by corani, issue 188).
- Refactor replot behaviour so Flot tries to reuse the existing
- New hooks: drawSeries canvas, adding shutdown() methods to the plot (based on patch by
Ryley Breiddal, issue 269). This prevents a memory leak in Chrome
and hopefully makes replotting faster for those who are using $.plot
instead of .setData()/.draw(). Also update jQuery to 1.5.1 to
prevent IE leaks fixed in jQuery.
- New hooks: drawSeries, shutdown
Bug fixes: Bug fixes:
......
Writing plugins Writing plugins
--------------- ---------------
To make a new plugin, create an init function and a set of options (if All you need to do to make a new plugin is creating an init function
needed), stuff it into an object and put it in the $.plot.plugins and a set of options (if needed), stuffing it into an object and
array. For example: putting it in the $.plot.plugins array. For example:
function myCoolPluginInit(plot) { plot.coolstring = "Hello!" }; function myCoolPluginInit(plot) {
var myCoolOptions = { coolstuff: { show: true } } plot.coolstring = "Hello!";
$.plot.plugins.push({ init: myCoolPluginInit, options: myCoolOptions }); };
// now when $.plot is called, the returned object will have the $.plot.plugins.push({ init: myCoolPluginInit, options: { ... } });
// if $.plot is called, it will return a plot object with the
// attribute "coolstring" // attribute "coolstring"
Now, given that the plugin might run in many different places, it's Now, given that the plugin might run in many different places, it's
a good idea to avoid leaking names. We can avoid this by wrapping the a good idea to avoid leaking names. The usual trick here is wrap the
above lines in an anonymous function which we call immediately, like above lines in an anonymous function which is called immediately, like
this: (function () { inner code ... })(). To make it even more robust this: (function () { inner code ... })(). To make it even more robust
in case $ is not bound to jQuery but some other Javascript library, we in case $ is not bound to jQuery but some other Javascript library, we
can write it as can write it as
...@@ -24,6 +26,13 @@ can write it as ...@@ -24,6 +26,13 @@ can write it as
// ... // ...
})(jQuery); })(jQuery);
There's a complete example below, but you should also check out the
plugins bundled with Flot.
Complete example
----------------
Here is a simple debug plugin which alerts each of the series in the Here is a simple debug plugin which alerts each of the series in the
plot. It has a single option that control whether it is enabled and plot. It has a single option that control whether it is enabled and
how much info to output: how much info to output:
...@@ -75,16 +84,41 @@ This simple plugin illustrates a couple of points: ...@@ -75,16 +84,41 @@ This simple plugin illustrates a couple of points:
- Variables in the init function can be used to store plot-specific - Variables in the init function can be used to store plot-specific
state between the hooks. state between the hooks.
The two last points are important because there may be multiple plots
on the same page, and you'd want to make sure they are not mixed up.
Shutting down a plugin
----------------------
Each plot object has a shutdown hook which is run when plot.shutdown()
is called. This usually mostly happens in case another plot is made on
top of an existing one.
The purpose of the hook is to give you a chance to unbind any event
handlers you've registered and remove any extra DOM things you've
inserted.
The problem with event handlers is that you can have registered a
handler which is run in some point in the future, e.g. with
setTimeout(). Meanwhile, the plot may have been shutdown and removed,
but because your event handler is still referencing it, it can't be
garbage collected yet, and worse, if your handler eventually runs, it
may overwrite stuff on a completely different plot.
Options guidelines Some hints on the options
================== -------------------------
Plugins should always support appropriate options to enable/disable Plugins should always support appropriate options to enable/disable
them because the plugin user may have several plots on the same page them because the plugin user may have several plots on the same page
where only one should use the plugin. where only one should use the plugin. In most cases it's probably a
good idea if the plugin is turned off rather than on per default, just
like most of the powerful features in Flot.
If the plugin needs series-specific options, you can put them in If the plugin needs options that are specific to each series, like the
"series" in the options object, e.g. points or lines options in core Flot, you can put them in "series" in
the options object, e.g.
var options = { var options = {
series: { series: {
...@@ -95,10 +129,8 @@ If the plugin needs series-specific options, you can put them in ...@@ -95,10 +129,8 @@ If the plugin needs series-specific options, you can put them in
} }
} }
Then they will be copied by Flot into each series, providing the Then they will be copied by Flot into each series, providing default
defaults in case the plugin user doesn't specify any. Again, in most values in case none are specified.
cases it's probably a good idea if the plugin is turned off rather
than on per default, just like most of the powerful features in Flot.
Think hard and long about naming the options. These names are going to Think hard and long about naming the options. These names are going to
be public API, and code is going to depend on them if the plugin is be public API, and code is going to depend on them if the plugin is
......
...@@ -90,34 +90,37 @@ The plugin also adds four public methods: ...@@ -90,34 +90,37 @@ The plugin also adds four public methods:
crosshair.locked = false; crosshair.locked = false;
} }
plot.hooks.bindEvents.push(function (plot, eventHolder) { function onMouseOut(e) {
if (!plot.getOptions().crosshair.mode) if (crosshair.locked)
return; return;
eventHolder.mouseout(function () { if (crosshair.x != -1) {
if (crosshair.locked) crosshair.x = -1;
return; plot.triggerRedrawOverlay();
}
}
if (crosshair.x != -1) { function onMouseMove(e) {
crosshair.x = -1; if (crosshair.locked)
plot.triggerRedrawOverlay(); return;
}
});
eventHolder.mousemove(function (e) {
if (crosshair.locked)
return;
if (plot.getSelection && plot.getSelection()) { if (plot.getSelection && plot.getSelection()) {
crosshair.x = -1; // hide the crosshair while selecting crosshair.x = -1; // hide the crosshair while selecting
return; return;
} }
var offset = plot.offset(); var offset = plot.offset();
crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
plot.triggerRedrawOverlay(); plot.triggerRedrawOverlay();
}); }
plot.hooks.bindEvents.push(function (plot, eventHolder) {
if (!plot.getOptions().crosshair.mode)
return;
eventHolder.mouseout(onMouseOut);
eventHolder.mousemove(onMouseMove);
}); });
plot.hooks.drawOverlay.push(function (plot, ctx) { plot.hooks.drawOverlay.push(function (plot, ctx) {
...@@ -148,6 +151,11 @@ The plugin also adds four public methods: ...@@ -148,6 +151,11 @@ The plugin also adds four public methods:
} }
ctx.restore(); ctx.restore();
}); });
plot.hooks.shutdown.push(function (plot, eventHolder) {
eventHolder.unbind("mouseout", onMouseOut);
eventHolder.unbind("mousemove", onMouseMove);
});
} }
$.plot.plugins.push({ $.plot.plugins.push({
......
...@@ -148,7 +148,8 @@ ...@@ -148,7 +148,8 @@
drawSeries: [], drawSeries: [],
draw: [], draw: [],
bindEvents: [], bindEvents: [],
drawOverlay: [] drawOverlay: [],
shutdown: []
}, },
plot = this; plot = this;
...@@ -190,6 +191,12 @@ ...@@ -190,6 +191,12 @@
top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top) top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top)
}; };
}; };
plot.shutdown = shutdown;
plot.resize = function () {
getCanvasDimensions();
resizeCanvas(canvas);
resizeCanvas(overlay);
};
// public attributes // public attributes
plot.hooks = hooks; plot.hooks = hooks;
...@@ -197,7 +204,7 @@ ...@@ -197,7 +204,7 @@
// initialize // initialize
initPlugins(plot); initPlugins(plot);
parseOptions(options_); parseOptions(options_);
constructCanvas(); setupCanvases();
setData(data_); setData(data_);
setupGrid(); setupGrid();
draw(); draw();
...@@ -673,56 +680,110 @@ ...@@ -673,56 +680,110 @@
}); });
} }
function constructCanvas() { function makeCanvas(skipPositioning, cls) {
canvasWidth = placeholder.width(); var c = document.createElement('canvas');
canvasHeight = placeholder.height(); c.className = cls;
c.width = canvasWidth;
c.height = canvasHeight;
if (!skipPositioning)
$(c).css({ position: 'absolute', left: 0, top: 0 });
$(c).appendTo(placeholder);
if (!c.getContext) // excanvas hack
c = window.G_vmlCanvasManager.initElement(c);
// excanvas hack, if there are any canvases here, whack // used for resetting in case we get replotted
// the state on them manually c.getContext("2d").save();
if (window.G_vmlCanvasManager)
placeholder.find("canvas").each(function () {
this.context_ = null;
});
placeholder.html(""); // clear placeholder
if (placeholder.css("position") == 'static') return c;
placeholder.css("position", "relative"); // for positioning labels and overlay }
placeholder.css({ padding: 0 });
function getCanvasDimensions() {
canvasWidth = placeholder.width();
canvasHeight = placeholder.height();
if (canvasWidth <= 0 || canvasHeight <= 0) if (canvasWidth <= 0 || canvasHeight <= 0)
throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight; throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
}
function makeCanvas(skipPositioning) { function resizeCanvas(c) {
var c = document.createElement('canvas'); // resizing should reset the state (excanvas seems to be
// buggy though)
if (c.width != canvasWidth)
c.width = canvasWidth; c.width = canvasWidth;
if (c.height != canvasHeight)
c.height = canvasHeight; c.height = canvasHeight;
// so try to get back to the initial state (even if it's
// gone now, this should be safe according to the spec)
var cctx = c.getContext("2d");
cctx.restore();
// and save again
cctx.save();
}
function setupCanvases() {
var reused,
existingCanvas = placeholder.children("canvas.base"),
existingOverlay = placeholder.children("canvas.overlay");
if (existingCanvas.length == 0 || existingOverlay == 0) {
// init everything
if (!skipPositioning) placeholder.html(""); // make sure placeholder is clear
$(c).css({ position: 'absolute', left: 0, top: 0 });
placeholder.css({ padding: 0 }); // padding messes up the positioning
$(c).appendTo(placeholder); if (placeholder.css("position") == 'static')
placeholder.css("position", "relative"); // for positioning labels and overlay
getCanvasDimensions();
if (!c.getContext) // excanvas hack canvas = makeCanvas(true, "base");
c = window.G_vmlCanvasManager.initElement(c); overlay = makeCanvas(false, "overlay"); // overlay canvas for interactive features
return c; reused = false;
}
else {
// reuse existing elements
canvas = existingCanvas.get(0);
overlay = existingOverlay.get(0);
reused = true;
} }
// the canvas
canvas = makeCanvas(true);
ctx = canvas.getContext("2d");
// overlay canvas for interactive features ctx = canvas.getContext("2d");
overlay = makeCanvas();
octx = overlay.getContext("2d"); octx = overlay.getContext("2d");
}
function bindEvents() {
// we include the canvas in the event holder too, because IE 7 // we include the canvas in the event holder too, because IE 7
// sometimes has trouble with the stacking order // sometimes has trouble with the stacking order
eventHolder = $([overlay, canvas]); eventHolder = $([overlay, canvas]);
if (reused) {
// run shutdown in the old plot object
placeholder.data("plot").shutdown();
// reset reused canvases
plot.resize();
// make sure overlay pixels are cleared (canvas is cleared when we redraw)
octx.clearRect(0, 0, canvasWidth, canvasHeight);
// then whack any remaining obvious garbage left
eventHolder.unbind();
placeholder.children().not([canvas, overlay]).remove();
}
// save in case we get replotted
placeholder.data("plot", plot);
}
function bindEvents() {
// bind events // bind events
if (options.grid.hoverable) { if (options.grid.hoverable) {
eventHolder.mousemove(onMouseMove); eventHolder.mousemove(onMouseMove);
...@@ -735,6 +796,17 @@ ...@@ -735,6 +796,17 @@
executeHooks(hooks.bindEvents, [eventHolder]); executeHooks(hooks.bindEvents, [eventHolder]);
} }
function shutdown() {
if (redrawTimeout)
clearTimeout(redrawTimeout);
eventHolder.unbind("mousemove", onMouseMove);
eventHolder.unbind("mouseleave", onMouseLeave);
eventHolder.unbind("click", onClick);
executeHooks(hooks.shutdown, [eventHolder]);
}
function setTransformationHelpers(axis) { function setTransformationHelpers(axis) {
// set helper functions on the axis, assumes plot area // set helper functions on the axis, assumes plot area
// has been computed already // has been computed already
......
...@@ -126,63 +126,72 @@ Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-L ...@@ -126,63 +126,72 @@ Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-L
}; };
function init(plot) { function init(plot) {
function onZoomClick(e, zoomOut) {
var c = plot.offset();
c.left = e.pageX - c.left;
c.top = e.pageY - c.top;
if (zoomOut)
plot.zoomOut({ center: c });
else
plot.zoom({ center: c });
}
function onMouseWheel(e, delta) {
onZoomClick(e, delta < 0);
return false;
}
var prevCursor = 'default', prevPageX = 0, prevPageY = 0,
panTimeout = null;
function onDragStart(e) {
if (e.which != 1) // only accept left-click
return false;
var c = plot.getPlaceholder().css('cursor');
if (c)
prevCursor = c;
plot.getPlaceholder().css('cursor', plot.getOptions().pan.cursor);
prevPageX = e.pageX;
prevPageY = e.pageY;
}
function onDrag(e) {
var frameRate = plot.getOptions().pan.frameRate;
if (panTimeout || !frameRate)
return;
panTimeout = setTimeout(function () {
plot.pan({ left: prevPageX - e.pageX,
top: prevPageY - e.pageY });
prevPageX = e.pageX;
prevPageY = e.pageY;
panTimeout = null;
}, 1 / frameRate * 1000);
}
function onDragEnd(e) {
if (panTimeout) {
clearTimeout(panTimeout);
panTimeout = null;
}
plot.getPlaceholder().css('cursor', prevCursor);
plot.pan({ left: prevPageX - e.pageX,
top: prevPageY - e.pageY });
}
function bindEvents(plot, eventHolder) { function bindEvents(plot, eventHolder) {
var o = plot.getOptions(); var o = plot.getOptions();
if (o.zoom.interactive) { if (o.zoom.interactive) {
function clickHandler(e, zoomOut) { eventHolder[o.zoom.trigger](onZoomClick);
var c = plot.offset(); eventHolder.mousewheel(onMouseWheel);
c.left = e.pageX - c.left;
c.top = e.pageY - c.top;
if (zoomOut)
plot.zoomOut({ center: c });
else
plot.zoom({ center: c });
}
eventHolder[o.zoom.trigger](clickHandler);
eventHolder.mousewheel(function (e, delta) {
clickHandler(e, delta < 0);
return false;
});
} }
if (o.pan.interactive) { if (o.pan.interactive) {
var prevCursor = 'default', pageX = 0, pageY = 0, eventHolder.bind("dragstart", { distance: 10 }, onDragStart);
panTimeout = null; eventHolder.bind("drag", onDrag);
eventHolder.bind("dragend", onDragEnd);
eventHolder.bind("dragstart", { distance: 10 }, function (e) {
if (e.which != 1) // only accept left-click
return false;
var c = eventHolder.css('cursor');
if (c)
prevCursor = c;
eventHolder.css('cursor', o.pan.cursor);
pageX = e.pageX;
pageY = e.pageY;
});
eventHolder.bind("drag", function (e) {
if (panTimeout || !o.pan.frameRate)
return;
panTimeout = setTimeout(function () {
plot.pan({ left: pageX - e.pageX,
top: pageY - e.pageY });
pageX = e.pageX;
pageY = e.pageY;
panTimeout = null;
}, 1/o.pan.frameRate * 1000);
});
eventHolder.bind("dragend", function (e) {
if (panTimeout) {
clearTimeout(panTimeout);
panTimeout = null;
}
eventHolder.css('cursor', prevCursor);
plot.pan({ left: pageX - e.pageX,
top: pageY - e.pageY });
});
} }
} }
...@@ -303,8 +312,19 @@ Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-L ...@@ -303,8 +312,19 @@ Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-L
if (!args.preventEvent) if (!args.preventEvent)
plot.getPlaceholder().trigger("plotpan", [ plot ]); plot.getPlaceholder().trigger("plotpan", [ plot ]);
} }
function shutdown(plot, eventHolder) {
eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick);
eventHolder.unbind("mousewheel", onMouseWheel);
eventHolder.unbind("dragstart", onDragStart);
eventHolder.unbind("drag", onDrag);
eventHolder.unbind("dragend", onDragEnd);
if (panTimeout)
clearTimeout(panTimeout);
}
plot.hooks.bindEvents.push(bindEvents); plot.hooks.bindEvents.push(bindEvents);
plot.hooks.shutdown.push(shutdown);
} }
$.plot.plugins.push({ $.plot.plugins.push({
......
...@@ -81,11 +81,13 @@ The plugin allso adds the following methods to the plot object: ...@@ -81,11 +81,13 @@ The plugin allso adds the following methods to the plot object:
// make this plugin much slimmer. // make this plugin much slimmer.
var savedhandlers = {}; var savedhandlers = {};
var mouseUpHandler = null;
function onMouseMove(e) { function onMouseMove(e) {
if (selection.active) { if (selection.active) {
plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]);
updateSelection(e); updateSelection(e);
plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]);
} }
} }
...@@ -109,18 +111,24 @@ The plugin allso adds the following methods to the plot object: ...@@ -109,18 +111,24 @@ The plugin allso adds the following methods to the plot object:
setSelectionPos(selection.first, e); setSelectionPos(selection.first, e);
selection.active = true; selection.active = true;
// this is a bit silly, but we have to use a closure to be
// able to whack the same handler again
mouseUpHandler = function (e) { onMouseUp(e); };
$(document).one("mouseup", onMouseUp); $(document).one("mouseup", mouseUpHandler);
} }
function onMouseUp(e) { function onMouseUp(e) {
mouseUpHandler = null;
// revert drag stuff for old-school browsers // revert drag stuff for old-school browsers
if (document.onselectstart !== undefined) if (document.onselectstart !== undefined)
document.onselectstart = savedhandlers.onselectstart; document.onselectstart = savedhandlers.onselectstart;
if (document.ondrag !== undefined) if (document.ondrag !== undefined)
document.ondrag = savedhandlers.ondrag; document.ondrag = savedhandlers.ondrag;
// no more draggy-dee-drag // no more dragging
selection.active = false; selection.active = false;
updateSelection(e); updateSelection(e);
...@@ -277,11 +285,10 @@ The plugin allso adds the following methods to the plot object: ...@@ -277,11 +285,10 @@ The plugin allso adds the following methods to the plot object:
plot.hooks.bindEvents.push(function(plot, eventHolder) { plot.hooks.bindEvents.push(function(plot, eventHolder) {
var o = plot.getOptions(); var o = plot.getOptions();
if (o.selection.mode != null) if (o.selection.mode != null) {
eventHolder.mousemove(onMouseMove); eventHolder.mousemove(onMouseMove);
if (o.selection.mode != null)
eventHolder.mousedown(onMouseDown); eventHolder.mousedown(onMouseDown);
}
}); });
...@@ -312,6 +319,15 @@ The plugin allso adds the following methods to the plot object: ...@@ -312,6 +319,15 @@ The plugin allso adds the following methods to the plot object:
ctx.restore(); ctx.restore();
} }
}); });
plot.hooks.shutdown.push(function (plot, eventHolder) {
eventHolder.unbind("mousemove", onMouseMove);
eventHolder.unbind("mousedown", onMouseDown);
if (mouseUpHandler)
$(document).unbind("mouseup", mouseUpHandler);
});
} }
$.plot.plugins.push({ $.plot.plugins.push({
...@@ -323,6 +339,6 @@ The plugin allso adds the following methods to the plot object: ...@@ -323,6 +339,6 @@ The plugin allso adds the following methods to the plot object:
} }
}, },
name: 'selection', name: 'selection',
version: '1.0' version: '1.1'
}); });
})(jQuery); })(jQuery);
This diff is collapsed.
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