| 1 |
/* |
| 2 |
Flot plugin for plotting images, e.g. useful for putting ticks on a |
| 3 |
prerendered complex visualization. |
| 4 |
|
| 5 |
The data syntax is [[image, x1, y1, x2, y2], ...] where (x1, y1) and |
| 6 |
(x2, y2) are where you intend the two opposite corners of the image to |
| 7 |
end up in the plot. Image must be a fully loaded Javascript image (you |
| 8 |
can make one with new Image()). If the image is not complete, it's |
| 9 |
skipped when plotting. |
| 10 |
|
| 11 |
There are two helpers included for retrieving images. The easiest work |
| 12 |
the way that you put in URLs instead of images in the data (like |
| 13 |
["myimage.png", 0, 0, 10, 10]), then call $.plot.image.loadData(data, |
| 14 |
options, callback) where data and options are the same as you pass in |
| 15 |
to $.plot. This loads the images, replaces the URLs in the data with |
| 16 |
the corresponding images and calls "callback" when all images are |
| 17 |
loaded (or failed loading). In the callback, you can then call $.plot |
| 18 |
with the data set. See the included example. |
| 19 |
|
| 20 |
A more low-level helper, $.plot.image.load(urls, callback) is also |
| 21 |
included. Given a list of URLs, it calls callback with an object |
| 22 |
mapping from URL to Image object when all images are loaded or have |
| 23 |
failed loading. |
| 24 |
|
| 25 |
Options for the plugin are |
| 26 |
|
| 27 |
series: { |
| 28 |
images: { |
| 29 |
show: boolean |
| 30 |
anchor: "corner" or "center" |
| 31 |
alpha: [0,1] |
| 32 |
} |
| 33 |
} |
| 34 |
|
| 35 |
which can be specified for a specific series |
| 36 |
|
| 37 |
$.plot($("#placeholder"), [{ data: [ ... ], images: { ... } ]) |
| 38 |
|
| 39 |
Note that because the data format is different from usual data points, |
| 40 |
you can't use images with anything else in a specific data series. |
| 41 |
|
| 42 |
Setting "anchor" to "center" causes the pixels in the image to be |
| 43 |
anchored at the corner pixel centers inside of at the pixel corners, |
| 44 |
effectively letting half a pixel stick out to each side in the plot. |
| 45 |
|
| 46 |
|
| 47 |
A possible future direction could be support for tiling for large |
| 48 |
images (like Google Maps). |
| 49 |
|
| 50 |
*/ |
| 51 |
|
| 52 |
(function ($) { |
| 53 |
var options = { |
| 54 |
series: { |
| 55 |
images: { |
| 56 |
show: false, |
| 57 |
alpha: 1, |
| 58 |
anchor: "corner" // or "center" |
| 59 |
} |
| 60 |
} |
| 61 |
}; |
| 62 |
|
| 63 |
$.plot.image = {}; |
| 64 |
|
| 65 |
$.plot.image.loadDataImages = function (series, options, callback) { |
| 66 |
var urls = [], points = []; |
| 67 |
|
| 68 |
var defaultShow = options.series.images.show; |
| 69 |
|
| 70 |
$.each(series, function (i, s) { |
| 71 |
if (!(defaultShow || s.images.show)) |
| 72 |
return; |
| 73 |
|
| 74 |
if (s.data) |
| 75 |
s = s.data; |
| 76 |
|
| 77 |
$.each(s, function (i, p) { |
| 78 |
if (typeof p[0] == "string") { |
| 79 |
urls.push(p[0]); |
| 80 |
points.push(p); |
| 81 |
} |
| 82 |
}); |
| 83 |
}); |
| 84 |
|
| 85 |
$.plot.image.load(urls, function (loadedImages) { |
| 86 |
$.each(points, function (i, p) { |
| 87 |
var url = p[0]; |
| 88 |
if (loadedImages[url]) |
| 89 |
p[0] = loadedImages[url]; |
| 90 |
}); |
| 91 |
|
| 92 |
callback(); |
| 93 |
}); |
| 94 |
} |
| 95 |
|
| 96 |
$.plot.image.load = function (urls, callback) { |
| 97 |
var missing = urls.length, loaded = {}; |
| 98 |
if (missing == 0) |
| 99 |
callback({}); |
| 100 |
|
| 101 |
$.each(urls, function (i, url) { |
| 102 |
var handler = function () { |
| 103 |
--missing; |
| 104 |
|
| 105 |
loaded[url] = this; |
| 106 |
|
| 107 |
if (missing == 0) |
| 108 |
callback(loaded); |
| 109 |
}; |
| 110 |
|
| 111 |
$('<img />').load(handler).error(handler).attr('src', url); |
| 112 |
}); |
| 113 |
} |
| 114 |
|
| 115 |
function drawSeries(plot, ctx, series) { |
| 116 |
var plotOffset = plot.getPlotOffset(); |
| 117 |
|
| 118 |
if (!series.images || !series.images.show) |
| 119 |
return; |
| 120 |
|
| 121 |
var points = series.datapoints.points, |
| 122 |
ps = series.datapoints.pointsize; |
| 123 |
|
| 124 |
for (var i = 0; i < points.length; i += ps) { |
| 125 |
var img = points[i], |
| 126 |
x1 = points[i + 1], y1 = points[i + 2], |
| 127 |
x2 = points[i + 3], y2 = points[i + 4], |
| 128 |
xaxis = series.xaxis, yaxis = series.yaxis, |
| 129 |
tmp; |
| 130 |
|
| 131 |
// actually we should check img.complete, but it |
| 132 |
// appears to be a somewhat unreliable indicator in |
| 133 |
// IE6 (false even after load event) |
| 134 |
if (!img || img.width <= 0 || img.height <= 0) |
| 135 |
continue; |
| 136 |
|
| 137 |
if (x1 > x2) { |
| 138 |
tmp = x2; |
| 139 |
x2 = x1; |
| 140 |
x1 = tmp; |
| 141 |
} |
| 142 |
if (y1 > y2) { |
| 143 |
tmp = y2; |
| 144 |
y2 = y1; |
| 145 |
y1 = tmp; |
| 146 |
} |
| 147 |
|
| 148 |
// if the anchor is at the center of the pixel, expand the |
| 149 |
// image by 1/2 pixel in each direction |
| 150 |
if (series.images.anchor == "center") { |
| 151 |
tmp = 0.5 * (x2-x1) / (img.width - 1); |
| 152 |
x1 -= tmp; |
| 153 |
x2 += tmp; |
| 154 |
tmp = 0.5 * (y2-y1) / (img.height - 1); |
| 155 |
y1 -= tmp; |
| 156 |
y2 += tmp; |
| 157 |
} |
| 158 |
|
| 159 |
// clip |
| 160 |
if (x1 == x2 || y1 == y2 || |
| 161 |
x1 >= xaxis.max || x2 <= xaxis.min || |
| 162 |
y1 >= yaxis.max || y2 <= yaxis.min) |
| 163 |
continue; |
| 164 |
|
| 165 |
var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height; |
| 166 |
if (x1 < xaxis.min) { |
| 167 |
sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1); |
| 168 |
x1 = xaxis.min; |
| 169 |
} |
| 170 |
|
| 171 |
if (x2 > xaxis.max) { |
| 172 |
sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1); |
| 173 |
x2 = xaxis.max; |
| 174 |
} |
| 175 |
|
| 176 |
if (y1 < yaxis.min) { |
| 177 |
sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1); |
| 178 |
y1 = yaxis.min; |
| 179 |
} |
| 180 |
|
| 181 |
if (y2 > yaxis.max) { |
| 182 |
sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1); |
| 183 |
y2 = yaxis.max; |
| 184 |
} |
| 185 |
|
| 186 |
x1 = xaxis.p2c(x1); |
| 187 |
x2 = xaxis.p2c(x2); |
| 188 |
y1 = yaxis.p2c(y1); |
| 189 |
y2 = yaxis.p2c(y2); |
| 190 |
|
| 191 |
// the transformation may have swapped us |
| 192 |
if (x1 > x2) { |
| 193 |
tmp = x2; |
| 194 |
x2 = x1; |
| 195 |
x1 = tmp; |
| 196 |
} |
| 197 |
if (y1 > y2) { |
| 198 |
tmp = y2; |
| 199 |
y2 = y1; |
| 200 |
y1 = tmp; |
| 201 |
} |
| 202 |
|
| 203 |
tmp = ctx.globalAlpha; |
| 204 |
ctx.globalAlpha *= series.images.alpha; |
| 205 |
ctx.drawImage(img, |
| 206 |
sx1, sy1, sx2 - sx1, sy2 - sy1, |
| 207 |
x1 + plotOffset.left, y1 + plotOffset.top, |
| 208 |
x2 - x1, y2 - y1); |
| 209 |
ctx.globalAlpha = tmp; |
| 210 |
} |
| 211 |
} |
| 212 |
|
| 213 |
function processRawData(plot, series, data, datapoints) { |
| 214 |
if (!series.images.show) |
| 215 |
return; |
| 216 |
|
| 217 |
// format is Image, x1, y1, x2, y2 (opposite corners) |
| 218 |
datapoints.format = [ |
| 219 |
{ required: true }, |
| 220 |
{ x: true, number: true, required: true }, |
| 221 |
{ y: true, number: true, required: true }, |
| 222 |
{ x: true, number: true, required: true }, |
| 223 |
{ y: true, number: true, required: true } |
| 224 |
]; |
| 225 |
} |
| 226 |
|
| 227 |
function init(plot) { |
| 228 |
plot.hooks.processRawData.push(processRawData); |
| 229 |
plot.hooks.drawSeries.push(drawSeries); |
| 230 |
} |
| 231 |
|
| 232 |
$.plot.plugins.push({ |
| 233 |
init: init, |
| 234 |
options: options, |
| 235 |
name: 'image', |
| 236 |
version: '1.1' |
| 237 |
}); |
| 238 |
})(jQuery); |
| 239 |
|