flot-graph-rails 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/flot_graph-rails/version.rb +1 -1
- data/vendor/assets/javascripts/excanvas.js +1428 -0
- data/vendor/assets/javascripts/excanvas.min.js +1 -0
- data/vendor/assets/javascripts/jquery.colorhelpers.js +179 -0
- data/vendor/assets/javascripts/jquery.flot.categories.js +190 -0
- data/vendor/assets/javascripts/jquery.flot.crosshair.js +176 -0
- data/vendor/assets/javascripts/jquery.flot.errorbars.js +353 -0
- data/vendor/assets/javascripts/jquery.flot.fillbetween.js +226 -0
- data/vendor/assets/javascripts/jquery.flot.image.js +241 -0
- data/vendor/assets/javascripts/jquery.flot.js +2691 -0
- data/vendor/assets/javascripts/jquery.flot.navigate.js +345 -0
- data/vendor/assets/javascripts/jquery.flot.pie.js +829 -0
- data/vendor/assets/javascripts/jquery.flot.resize.js +60 -0
- data/vendor/assets/javascripts/jquery.flot.selection.js +344 -0
- data/vendor/assets/javascripts/jquery.flot.stack.js +188 -0
- data/vendor/assets/javascripts/jquery.flot.symbol.js +71 -0
- data/vendor/assets/javascripts/jquery.flot.threshold.js +142 -0
- data/vendor/assets/javascripts/jquery.flot.time.js +417 -0
- metadata +20 -7
- data/.gitignore +0 -17
- data/Gemfile +0 -4
- data/Rakefile +0 -1
- data/flot_graph-rails.gemspec +0 -24
@@ -0,0 +1,2691 @@
|
|
1
|
+
/* Javascript plotting library for jQuery, version 0.8 alpha.
|
2
|
+
|
3
|
+
Copyright (c) 2007-2012 IOLA and Ole Laursen.
|
4
|
+
Licensed under the MIT license.
|
5
|
+
|
6
|
+
*/
|
7
|
+
|
8
|
+
// first an inline dependency, jquery.colorhelpers.js, we inline it here
|
9
|
+
// for convenience
|
10
|
+
|
11
|
+
/* Plugin for jQuery for working with colors.
|
12
|
+
*
|
13
|
+
* Version 1.1.
|
14
|
+
*
|
15
|
+
* Inspiration from jQuery color animation plugin by John Resig.
|
16
|
+
*
|
17
|
+
* Released under the MIT license by Ole Laursen, October 2009.
|
18
|
+
*
|
19
|
+
* Examples:
|
20
|
+
*
|
21
|
+
* $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
|
22
|
+
* var c = $.color.extract($("#mydiv"), 'background-color');
|
23
|
+
* console.log(c.r, c.g, c.b, c.a);
|
24
|
+
* $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
|
25
|
+
*
|
26
|
+
* Note that .scale() and .add() return the same modified object
|
27
|
+
* instead of making a new one.
|
28
|
+
*
|
29
|
+
* V. 1.1: Fix error handling so e.g. parsing an empty string does
|
30
|
+
* produce a color rather than just crashing.
|
31
|
+
*/
|
32
|
+
(function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]+=I}return G.normalize()};G.scale=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]*=I}return G.normalize()};G.toString=function(){if(G.a>=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return K<J?J:(K>I?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);
|
33
|
+
|
34
|
+
// the actual Flot code
|
35
|
+
(function($) {
|
36
|
+
function Plot(placeholder, data_, options_, plugins) {
|
37
|
+
// data is on the form:
|
38
|
+
// [ series1, series2 ... ]
|
39
|
+
// where series is either just the data as [ [x1, y1], [x2, y2], ... ]
|
40
|
+
// or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
|
41
|
+
|
42
|
+
var series = [],
|
43
|
+
options = {
|
44
|
+
// the color theme used for graphs
|
45
|
+
colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
|
46
|
+
legend: {
|
47
|
+
show: true,
|
48
|
+
noColumns: 1, // number of colums in legend table
|
49
|
+
labelFormatter: null, // fn: string -> string
|
50
|
+
labelBoxBorderColor: "#ccc", // border color for the little label boxes
|
51
|
+
container: null, // container (as jQuery object) to put legend in, null means default on top of graph
|
52
|
+
position: "ne", // position of default legend container within plot
|
53
|
+
margin: 5, // distance from grid edge to default legend container within plot
|
54
|
+
backgroundColor: null, // null means auto-detect
|
55
|
+
backgroundOpacity: 0.85, // set to 0 to avoid background
|
56
|
+
sorted: null // default to no legend sorting
|
57
|
+
},
|
58
|
+
xaxis: {
|
59
|
+
show: null, // null = auto-detect, true = always, false = never
|
60
|
+
position: "bottom", // or "top"
|
61
|
+
mode: null, // null or "time"
|
62
|
+
timezone: null, // "browser" for local to the client or timezone for timezone-js
|
63
|
+
font: null, // null (derived from CSS in placeholder) or object like { size: 11, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" }
|
64
|
+
color: null, // base color, labels, ticks
|
65
|
+
tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"
|
66
|
+
transform: null, // null or f: number -> number to transform axis
|
67
|
+
inverseTransform: null, // if transform is set, this should be the inverse function
|
68
|
+
min: null, // min. value to show, null means set automatically
|
69
|
+
max: null, // max. value to show, null means set automatically
|
70
|
+
autoscaleMargin: null, // margin in % to add if auto-setting min/max
|
71
|
+
ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
|
72
|
+
tickFormatter: null, // fn: number -> string
|
73
|
+
labelWidth: null, // size of tick labels in pixels
|
74
|
+
labelHeight: null,
|
75
|
+
reserveSpace: null, // whether to reserve space even if axis isn't shown
|
76
|
+
tickLength: null, // size in pixels of ticks, or "full" for whole line
|
77
|
+
alignTicksWithAxis: null, // axis number or null for no sync
|
78
|
+
|
79
|
+
// mode specific options
|
80
|
+
tickDecimals: null, // no. of decimals, null means auto
|
81
|
+
tickSize: null, // number or [number, "unit"]
|
82
|
+
minTickSize: null, // number or [number, "unit"]
|
83
|
+
monthNames: null, // list of names of months
|
84
|
+
timeformat: null, // format string to use
|
85
|
+
twelveHourClock: false // 12 or 24 time in time mode
|
86
|
+
},
|
87
|
+
yaxis: {
|
88
|
+
autoscaleMargin: 0.02,
|
89
|
+
position: "left" // or "right"
|
90
|
+
},
|
91
|
+
xaxes: [],
|
92
|
+
yaxes: [],
|
93
|
+
series: {
|
94
|
+
points: {
|
95
|
+
show: false,
|
96
|
+
radius: 3,
|
97
|
+
lineWidth: 2, // in pixels
|
98
|
+
fill: true,
|
99
|
+
fillColor: "#ffffff",
|
100
|
+
symbol: "circle" // or callback
|
101
|
+
},
|
102
|
+
lines: {
|
103
|
+
// we don't put in show: false so we can see
|
104
|
+
// whether lines were actively disabled
|
105
|
+
lineWidth: 2, // in pixels
|
106
|
+
fill: false,
|
107
|
+
fillColor: null,
|
108
|
+
steps: false
|
109
|
+
// Omit 'zero', so we can later default its value to
|
110
|
+
// match that of the 'fill' option.
|
111
|
+
},
|
112
|
+
bars: {
|
113
|
+
show: false,
|
114
|
+
lineWidth: 2, // in pixels
|
115
|
+
barWidth: 1, // in units of the x axis
|
116
|
+
fill: true,
|
117
|
+
fillColor: null,
|
118
|
+
align: "left", // "left", "right", or "center"
|
119
|
+
horizontal: false,
|
120
|
+
zero: true
|
121
|
+
},
|
122
|
+
shadowSize: 3,
|
123
|
+
highlightColor: null
|
124
|
+
},
|
125
|
+
grid: {
|
126
|
+
show: true,
|
127
|
+
aboveData: false,
|
128
|
+
color: "#545454", // primary color used for outline and labels
|
129
|
+
backgroundColor: null, // null for transparent, else color
|
130
|
+
borderColor: null, // set if different from the grid color
|
131
|
+
tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
|
132
|
+
margin: 0, // distance from the canvas edge to the grid
|
133
|
+
labelMargin: 5, // in pixels
|
134
|
+
axisMargin: 8, // in pixels
|
135
|
+
borderWidth: 2, // in pixels
|
136
|
+
minBorderMargin: null, // in pixels, null means taken from points radius
|
137
|
+
markings: null, // array of ranges or fn: axes -> array of ranges
|
138
|
+
markingsColor: "#f4f4f4",
|
139
|
+
markingsLineWidth: 2,
|
140
|
+
// interactive stuff
|
141
|
+
clickable: false,
|
142
|
+
hoverable: false,
|
143
|
+
autoHighlight: true, // highlight in case mouse is near
|
144
|
+
mouseActiveRadius: 10 // how far the mouse can be away to activate an item
|
145
|
+
},
|
146
|
+
interaction: {
|
147
|
+
redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow
|
148
|
+
},
|
149
|
+
hooks: {}
|
150
|
+
},
|
151
|
+
canvas = null, // the canvas for the plot itself
|
152
|
+
overlay = null, // canvas for interactive stuff on top of plot
|
153
|
+
eventHolder = null, // jQuery object that events should be bound to
|
154
|
+
ctx = null, octx = null,
|
155
|
+
xaxes = [], yaxes = [],
|
156
|
+
plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
|
157
|
+
canvasWidth = 0, canvasHeight = 0,
|
158
|
+
plotWidth = 0, plotHeight = 0,
|
159
|
+
hooks = {
|
160
|
+
processOptions: [],
|
161
|
+
processRawData: [],
|
162
|
+
processDatapoints: [],
|
163
|
+
processOffset: [],
|
164
|
+
drawBackground: [],
|
165
|
+
drawSeries: [],
|
166
|
+
draw: [],
|
167
|
+
bindEvents: [],
|
168
|
+
drawOverlay: [],
|
169
|
+
shutdown: []
|
170
|
+
},
|
171
|
+
plot = this;
|
172
|
+
|
173
|
+
// public functions
|
174
|
+
plot.setData = setData;
|
175
|
+
plot.setupGrid = setupGrid;
|
176
|
+
plot.draw = draw;
|
177
|
+
plot.getPlaceholder = function() { return placeholder; };
|
178
|
+
plot.getCanvas = function() { return canvas; };
|
179
|
+
plot.getPlotOffset = function() { return plotOffset; };
|
180
|
+
plot.width = function () { return plotWidth; };
|
181
|
+
plot.height = function () { return plotHeight; };
|
182
|
+
plot.offset = function () {
|
183
|
+
var o = eventHolder.offset();
|
184
|
+
o.left += plotOffset.left;
|
185
|
+
o.top += plotOffset.top;
|
186
|
+
return o;
|
187
|
+
};
|
188
|
+
plot.getData = function () { return series; };
|
189
|
+
plot.getAxes = function () {
|
190
|
+
var res = {}, i;
|
191
|
+
$.each(xaxes.concat(yaxes), function (_, axis) {
|
192
|
+
if (axis)
|
193
|
+
res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;
|
194
|
+
});
|
195
|
+
return res;
|
196
|
+
};
|
197
|
+
plot.getXAxes = function () { return xaxes; };
|
198
|
+
plot.getYAxes = function () { return yaxes; };
|
199
|
+
plot.c2p = canvasToAxisCoords;
|
200
|
+
plot.p2c = axisToCanvasCoords;
|
201
|
+
plot.getOptions = function () { return options; };
|
202
|
+
plot.highlight = highlight;
|
203
|
+
plot.unhighlight = unhighlight;
|
204
|
+
plot.triggerRedrawOverlay = triggerRedrawOverlay;
|
205
|
+
plot.pointOffset = function(point) {
|
206
|
+
return {
|
207
|
+
left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10),
|
208
|
+
top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10)
|
209
|
+
};
|
210
|
+
};
|
211
|
+
plot.shutdown = shutdown;
|
212
|
+
plot.resize = function () {
|
213
|
+
getCanvasDimensions();
|
214
|
+
resizeCanvas(canvas);
|
215
|
+
resizeCanvas(overlay);
|
216
|
+
};
|
217
|
+
|
218
|
+
// public attributes
|
219
|
+
plot.hooks = hooks;
|
220
|
+
|
221
|
+
// initialize
|
222
|
+
initPlugins(plot);
|
223
|
+
parseOptions(options_);
|
224
|
+
setupCanvases();
|
225
|
+
setData(data_);
|
226
|
+
setupGrid();
|
227
|
+
draw();
|
228
|
+
bindEvents();
|
229
|
+
|
230
|
+
|
231
|
+
function executeHooks(hook, args) {
|
232
|
+
args = [plot].concat(args);
|
233
|
+
for (var i = 0; i < hook.length; ++i)
|
234
|
+
hook[i].apply(this, args);
|
235
|
+
}
|
236
|
+
|
237
|
+
function initPlugins() {
|
238
|
+
for (var i = 0; i < plugins.length; ++i) {
|
239
|
+
var p = plugins[i];
|
240
|
+
p.init(plot);
|
241
|
+
if (p.options)
|
242
|
+
$.extend(true, options, p.options);
|
243
|
+
}
|
244
|
+
}
|
245
|
+
|
246
|
+
function parseOptions(opts) {
|
247
|
+
var i;
|
248
|
+
|
249
|
+
$.extend(true, options, opts);
|
250
|
+
|
251
|
+
if (options.xaxis.color == null)
|
252
|
+
options.xaxis.color = options.grid.color;
|
253
|
+
if (options.yaxis.color == null)
|
254
|
+
options.yaxis.color = options.grid.color;
|
255
|
+
|
256
|
+
if (options.xaxis.tickColor == null) // backwards-compatibility
|
257
|
+
options.xaxis.tickColor = options.grid.tickColor;
|
258
|
+
if (options.yaxis.tickColor == null) // backwards-compatibility
|
259
|
+
options.yaxis.tickColor = options.grid.tickColor;
|
260
|
+
|
261
|
+
if (options.grid.borderColor == null)
|
262
|
+
options.grid.borderColor = options.grid.color;
|
263
|
+
if (options.grid.tickColor == null)
|
264
|
+
options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();
|
265
|
+
|
266
|
+
// fill in defaults in axes, copy at least always the
|
267
|
+
// first as the rest of the code assumes it'll be there
|
268
|
+
for (i = 0; i < Math.max(1, options.xaxes.length); ++i)
|
269
|
+
options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]);
|
270
|
+
for (i = 0; i < Math.max(1, options.yaxes.length); ++i)
|
271
|
+
options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]);
|
272
|
+
|
273
|
+
// backwards compatibility, to be removed in future
|
274
|
+
if (options.xaxis.noTicks && options.xaxis.ticks == null)
|
275
|
+
options.xaxis.ticks = options.xaxis.noTicks;
|
276
|
+
if (options.yaxis.noTicks && options.yaxis.ticks == null)
|
277
|
+
options.yaxis.ticks = options.yaxis.noTicks;
|
278
|
+
if (options.x2axis) {
|
279
|
+
options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
|
280
|
+
options.xaxes[1].position = "top";
|
281
|
+
}
|
282
|
+
if (options.y2axis) {
|
283
|
+
options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);
|
284
|
+
options.yaxes[1].position = "right";
|
285
|
+
}
|
286
|
+
if (options.grid.coloredAreas)
|
287
|
+
options.grid.markings = options.grid.coloredAreas;
|
288
|
+
if (options.grid.coloredAreasColor)
|
289
|
+
options.grid.markingsColor = options.grid.coloredAreasColor;
|
290
|
+
if (options.lines)
|
291
|
+
$.extend(true, options.series.lines, options.lines);
|
292
|
+
if (options.points)
|
293
|
+
$.extend(true, options.series.points, options.points);
|
294
|
+
if (options.bars)
|
295
|
+
$.extend(true, options.series.bars, options.bars);
|
296
|
+
if (options.shadowSize != null)
|
297
|
+
options.series.shadowSize = options.shadowSize;
|
298
|
+
if (options.highlightColor != null)
|
299
|
+
options.series.highlightColor = options.highlightColor;
|
300
|
+
|
301
|
+
// save options on axes for future reference
|
302
|
+
for (i = 0; i < options.xaxes.length; ++i)
|
303
|
+
getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
|
304
|
+
for (i = 0; i < options.yaxes.length; ++i)
|
305
|
+
getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];
|
306
|
+
|
307
|
+
// add hooks from options
|
308
|
+
for (var n in hooks)
|
309
|
+
if (options.hooks[n] && options.hooks[n].length)
|
310
|
+
hooks[n] = hooks[n].concat(options.hooks[n]);
|
311
|
+
|
312
|
+
executeHooks(hooks.processOptions, [options]);
|
313
|
+
}
|
314
|
+
|
315
|
+
function setData(d) {
|
316
|
+
series = parseData(d);
|
317
|
+
fillInSeriesOptions();
|
318
|
+
processData();
|
319
|
+
}
|
320
|
+
|
321
|
+
function parseData(d) {
|
322
|
+
var res = [];
|
323
|
+
for (var i = 0; i < d.length; ++i) {
|
324
|
+
var s = $.extend(true, {}, options.series);
|
325
|
+
|
326
|
+
if (d[i].data != null) {
|
327
|
+
s.data = d[i].data; // move the data instead of deep-copy
|
328
|
+
delete d[i].data;
|
329
|
+
|
330
|
+
$.extend(true, s, d[i]);
|
331
|
+
|
332
|
+
d[i].data = s.data;
|
333
|
+
}
|
334
|
+
else
|
335
|
+
s.data = d[i];
|
336
|
+
res.push(s);
|
337
|
+
}
|
338
|
+
|
339
|
+
return res;
|
340
|
+
}
|
341
|
+
|
342
|
+
function axisNumber(obj, coord) {
|
343
|
+
var a = obj[coord + "axis"];
|
344
|
+
if (typeof a == "object") // if we got a real axis, extract number
|
345
|
+
a = a.n;
|
346
|
+
if (typeof a != "number")
|
347
|
+
a = 1; // default to first axis
|
348
|
+
return a;
|
349
|
+
}
|
350
|
+
|
351
|
+
function allAxes() {
|
352
|
+
// return flat array without annoying null entries
|
353
|
+
return $.grep(xaxes.concat(yaxes), function (a) { return a; });
|
354
|
+
}
|
355
|
+
|
356
|
+
function canvasToAxisCoords(pos) {
|
357
|
+
// return an object with x/y corresponding to all used axes
|
358
|
+
var res = {}, i, axis;
|
359
|
+
for (i = 0; i < xaxes.length; ++i) {
|
360
|
+
axis = xaxes[i];
|
361
|
+
if (axis && axis.used)
|
362
|
+
res["x" + axis.n] = axis.c2p(pos.left);
|
363
|
+
}
|
364
|
+
|
365
|
+
for (i = 0; i < yaxes.length; ++i) {
|
366
|
+
axis = yaxes[i];
|
367
|
+
if (axis && axis.used)
|
368
|
+
res["y" + axis.n] = axis.c2p(pos.top);
|
369
|
+
}
|
370
|
+
|
371
|
+
if (res.x1 !== undefined)
|
372
|
+
res.x = res.x1;
|
373
|
+
if (res.y1 !== undefined)
|
374
|
+
res.y = res.y1;
|
375
|
+
|
376
|
+
return res;
|
377
|
+
}
|
378
|
+
|
379
|
+
function axisToCanvasCoords(pos) {
|
380
|
+
// get canvas coords from the first pair of x/y found in pos
|
381
|
+
var res = {}, i, axis, key;
|
382
|
+
|
383
|
+
for (i = 0; i < xaxes.length; ++i) {
|
384
|
+
axis = xaxes[i];
|
385
|
+
if (axis && axis.used) {
|
386
|
+
key = "x" + axis.n;
|
387
|
+
if (pos[key] == null && axis.n == 1)
|
388
|
+
key = "x";
|
389
|
+
|
390
|
+
if (pos[key] != null) {
|
391
|
+
res.left = axis.p2c(pos[key]);
|
392
|
+
break;
|
393
|
+
}
|
394
|
+
}
|
395
|
+
}
|
396
|
+
|
397
|
+
for (i = 0; i < yaxes.length; ++i) {
|
398
|
+
axis = yaxes[i];
|
399
|
+
if (axis && axis.used) {
|
400
|
+
key = "y" + axis.n;
|
401
|
+
if (pos[key] == null && axis.n == 1)
|
402
|
+
key = "y";
|
403
|
+
|
404
|
+
if (pos[key] != null) {
|
405
|
+
res.top = axis.p2c(pos[key]);
|
406
|
+
break;
|
407
|
+
}
|
408
|
+
}
|
409
|
+
}
|
410
|
+
|
411
|
+
return res;
|
412
|
+
}
|
413
|
+
|
414
|
+
function getOrCreateAxis(axes, number) {
|
415
|
+
if (!axes[number - 1])
|
416
|
+
axes[number - 1] = {
|
417
|
+
n: number, // save the number for future reference
|
418
|
+
direction: axes == xaxes ? "x" : "y",
|
419
|
+
options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)
|
420
|
+
};
|
421
|
+
|
422
|
+
return axes[number - 1];
|
423
|
+
}
|
424
|
+
|
425
|
+
function fillInSeriesOptions() {
|
426
|
+
|
427
|
+
var neededColors = series.length, maxIndex = -1, i;
|
428
|
+
|
429
|
+
// Subtract the number of series that already have fixed colors or
|
430
|
+
// color indexes from the number that we still need to generate.
|
431
|
+
|
432
|
+
for (i = 0; i < series.length; ++i) {
|
433
|
+
var sc = series[i].color;
|
434
|
+
if (sc != null) {
|
435
|
+
neededColors--;
|
436
|
+
if (typeof sc == "number" && sc > maxIndex) {
|
437
|
+
maxIndex = sc;
|
438
|
+
}
|
439
|
+
}
|
440
|
+
}
|
441
|
+
|
442
|
+
// If any of the series have fixed color indexes, then we need to
|
443
|
+
// generate at least as many colors as the highest index.
|
444
|
+
|
445
|
+
if (neededColors <= maxIndex) {
|
446
|
+
neededColors = maxIndex + 1;
|
447
|
+
}
|
448
|
+
|
449
|
+
// Generate all the colors, using first the option colors and then
|
450
|
+
// variations on those colors once they're exhausted.
|
451
|
+
|
452
|
+
var c, colors = [], colorPool = options.colors,
|
453
|
+
colorPoolSize = colorPool.length, variation = 0;
|
454
|
+
|
455
|
+
for (i = 0; i < neededColors; i++) {
|
456
|
+
|
457
|
+
c = $.color.parse(colorPool[i % colorPoolSize] || "#666");
|
458
|
+
|
459
|
+
// Each time we exhaust the colors in the pool we adjust
|
460
|
+
// a scaling factor used to produce more variations on
|
461
|
+
// those colors. The factor alternates negative/positive
|
462
|
+
// to produce lighter/darker colors.
|
463
|
+
|
464
|
+
// Reset the variation after every few cycles, or else
|
465
|
+
// it will end up producing only white or black colors.
|
466
|
+
|
467
|
+
if (i % colorPoolSize == 0 && i) {
|
468
|
+
if (variation >= 0) {
|
469
|
+
if (variation < 0.5) {
|
470
|
+
variation = -variation - 0.2;
|
471
|
+
} else variation = 0;
|
472
|
+
} else variation = -variation;
|
473
|
+
}
|
474
|
+
|
475
|
+
colors[i] = c.scale('rgb', 1 + variation);
|
476
|
+
}
|
477
|
+
|
478
|
+
// Finalize the series options, filling in their colors
|
479
|
+
|
480
|
+
var colori = 0, s;
|
481
|
+
for (i = 0; i < series.length; ++i) {
|
482
|
+
s = series[i];
|
483
|
+
|
484
|
+
// assign colors
|
485
|
+
if (s.color == null) {
|
486
|
+
s.color = colors[colori].toString();
|
487
|
+
++colori;
|
488
|
+
}
|
489
|
+
else if (typeof s.color == "number")
|
490
|
+
s.color = colors[s.color].toString();
|
491
|
+
|
492
|
+
// turn on lines automatically in case nothing is set
|
493
|
+
if (s.lines.show == null) {
|
494
|
+
var v, show = true;
|
495
|
+
for (v in s)
|
496
|
+
if (s[v] && s[v].show) {
|
497
|
+
show = false;
|
498
|
+
break;
|
499
|
+
}
|
500
|
+
if (show)
|
501
|
+
s.lines.show = true;
|
502
|
+
}
|
503
|
+
|
504
|
+
// If nothing was provided for lines.zero, default it to match
|
505
|
+
// lines.fill, since areas by default should extend to zero.
|
506
|
+
|
507
|
+
if (s.lines.zero == null) {
|
508
|
+
s.lines.zero = !!s.lines.fill;
|
509
|
+
}
|
510
|
+
|
511
|
+
// setup axes
|
512
|
+
s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x"));
|
513
|
+
s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y"));
|
514
|
+
}
|
515
|
+
}
|
516
|
+
|
517
|
+
function processData() {
|
518
|
+
var topSentry = Number.POSITIVE_INFINITY,
|
519
|
+
bottomSentry = Number.NEGATIVE_INFINITY,
|
520
|
+
fakeInfinity = Number.MAX_VALUE,
|
521
|
+
i, j, k, m, length,
|
522
|
+
s, points, ps, x, y, axis, val, f, p,
|
523
|
+
data, format;
|
524
|
+
|
525
|
+
function updateAxis(axis, min, max) {
|
526
|
+
if (min < axis.datamin && min != -fakeInfinity)
|
527
|
+
axis.datamin = min;
|
528
|
+
if (max > axis.datamax && max != fakeInfinity)
|
529
|
+
axis.datamax = max;
|
530
|
+
}
|
531
|
+
|
532
|
+
$.each(allAxes(), function (_, axis) {
|
533
|
+
// init axis
|
534
|
+
axis.datamin = topSentry;
|
535
|
+
axis.datamax = bottomSentry;
|
536
|
+
axis.used = false;
|
537
|
+
});
|
538
|
+
|
539
|
+
for (i = 0; i < series.length; ++i) {
|
540
|
+
s = series[i];
|
541
|
+
s.datapoints = { points: [] };
|
542
|
+
|
543
|
+
executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
|
544
|
+
}
|
545
|
+
|
546
|
+
// first pass: clean and copy data
|
547
|
+
for (i = 0; i < series.length; ++i) {
|
548
|
+
s = series[i];
|
549
|
+
|
550
|
+
data = s.data;
|
551
|
+
format = s.datapoints.format;
|
552
|
+
|
553
|
+
if (!format) {
|
554
|
+
format = [];
|
555
|
+
// find out how to copy
|
556
|
+
format.push({ x: true, number: true, required: true });
|
557
|
+
format.push({ y: true, number: true, required: true });
|
558
|
+
|
559
|
+
if (s.bars.show || (s.lines.show && s.lines.fill)) {
|
560
|
+
var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero));
|
561
|
+
format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale });
|
562
|
+
if (s.bars.horizontal) {
|
563
|
+
delete format[format.length - 1].y;
|
564
|
+
format[format.length - 1].x = true;
|
565
|
+
}
|
566
|
+
}
|
567
|
+
|
568
|
+
s.datapoints.format = format;
|
569
|
+
}
|
570
|
+
|
571
|
+
if (s.datapoints.pointsize != null)
|
572
|
+
continue; // already filled in
|
573
|
+
|
574
|
+
s.datapoints.pointsize = format.length;
|
575
|
+
|
576
|
+
ps = s.datapoints.pointsize;
|
577
|
+
points = s.datapoints.points;
|
578
|
+
|
579
|
+
var insertSteps = s.lines.show && s.lines.steps;
|
580
|
+
s.xaxis.used = s.yaxis.used = true;
|
581
|
+
|
582
|
+
for (j = k = 0; j < data.length; ++j, k += ps) {
|
583
|
+
p = data[j];
|
584
|
+
|
585
|
+
var nullify = p == null;
|
586
|
+
if (!nullify) {
|
587
|
+
for (m = 0; m < ps; ++m) {
|
588
|
+
val = p[m];
|
589
|
+
f = format[m];
|
590
|
+
|
591
|
+
if (f) {
|
592
|
+
if (f.number && val != null) {
|
593
|
+
val = +val; // convert to number
|
594
|
+
if (isNaN(val))
|
595
|
+
val = null;
|
596
|
+
else if (val == Infinity)
|
597
|
+
val = fakeInfinity;
|
598
|
+
else if (val == -Infinity)
|
599
|
+
val = -fakeInfinity;
|
600
|
+
}
|
601
|
+
|
602
|
+
if (val == null) {
|
603
|
+
if (f.required)
|
604
|
+
nullify = true;
|
605
|
+
|
606
|
+
if (f.defaultValue != null)
|
607
|
+
val = f.defaultValue;
|
608
|
+
}
|
609
|
+
}
|
610
|
+
|
611
|
+
points[k + m] = val;
|
612
|
+
}
|
613
|
+
}
|
614
|
+
|
615
|
+
if (nullify) {
|
616
|
+
for (m = 0; m < ps; ++m) {
|
617
|
+
val = points[k + m];
|
618
|
+
if (val != null) {
|
619
|
+
f = format[m];
|
620
|
+
// extract min/max info
|
621
|
+
if (f.x)
|
622
|
+
updateAxis(s.xaxis, val, val);
|
623
|
+
if (f.y)
|
624
|
+
updateAxis(s.yaxis, val, val);
|
625
|
+
}
|
626
|
+
points[k + m] = null;
|
627
|
+
}
|
628
|
+
}
|
629
|
+
else {
|
630
|
+
// a little bit of line specific stuff that
|
631
|
+
// perhaps shouldn't be here, but lacking
|
632
|
+
// better means...
|
633
|
+
if (insertSteps && k > 0
|
634
|
+
&& points[k - ps] != null
|
635
|
+
&& points[k - ps] != points[k]
|
636
|
+
&& points[k - ps + 1] != points[k + 1]) {
|
637
|
+
// copy the point to make room for a middle point
|
638
|
+
for (m = 0; m < ps; ++m)
|
639
|
+
points[k + ps + m] = points[k + m];
|
640
|
+
|
641
|
+
// middle point has same y
|
642
|
+
points[k + 1] = points[k - ps + 1];
|
643
|
+
|
644
|
+
// we've added a point, better reflect that
|
645
|
+
k += ps;
|
646
|
+
}
|
647
|
+
}
|
648
|
+
}
|
649
|
+
}
|
650
|
+
|
651
|
+
// give the hooks a chance to run
|
652
|
+
for (i = 0; i < series.length; ++i) {
|
653
|
+
s = series[i];
|
654
|
+
|
655
|
+
executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
|
656
|
+
}
|
657
|
+
|
658
|
+
// second pass: find datamax/datamin for auto-scaling
|
659
|
+
for (i = 0; i < series.length; ++i) {
|
660
|
+
s = series[i];
|
661
|
+
points = s.datapoints.points,
|
662
|
+
ps = s.datapoints.pointsize;
|
663
|
+
format = s.datapoints.format;
|
664
|
+
|
665
|
+
var xmin = topSentry, ymin = topSentry,
|
666
|
+
xmax = bottomSentry, ymax = bottomSentry;
|
667
|
+
|
668
|
+
for (j = 0; j < points.length; j += ps) {
|
669
|
+
if (points[j] == null)
|
670
|
+
continue;
|
671
|
+
|
672
|
+
for (m = 0; m < ps; ++m) {
|
673
|
+
val = points[j + m];
|
674
|
+
f = format[m];
|
675
|
+
if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity)
|
676
|
+
continue;
|
677
|
+
|
678
|
+
if (f.x) {
|
679
|
+
if (val < xmin)
|
680
|
+
xmin = val;
|
681
|
+
if (val > xmax)
|
682
|
+
xmax = val;
|
683
|
+
}
|
684
|
+
if (f.y) {
|
685
|
+
if (val < ymin)
|
686
|
+
ymin = val;
|
687
|
+
if (val > ymax)
|
688
|
+
ymax = val;
|
689
|
+
}
|
690
|
+
}
|
691
|
+
}
|
692
|
+
|
693
|
+
if (s.bars.show) {
|
694
|
+
// make sure we got room for the bar on the dancing floor
|
695
|
+
var delta;
|
696
|
+
|
697
|
+
switch (s.bars.align) {
|
698
|
+
case "left":
|
699
|
+
delta = 0;
|
700
|
+
break;
|
701
|
+
case "right":
|
702
|
+
delta = -s.bars.barWidth;
|
703
|
+
break;
|
704
|
+
case "center":
|
705
|
+
delta = -s.bars.barWidth / 2;
|
706
|
+
break;
|
707
|
+
default:
|
708
|
+
throw new Error("Invalid bar alignment: " + s.bars.align);
|
709
|
+
}
|
710
|
+
|
711
|
+
if (s.bars.horizontal) {
|
712
|
+
ymin += delta;
|
713
|
+
ymax += delta + s.bars.barWidth;
|
714
|
+
}
|
715
|
+
else {
|
716
|
+
xmin += delta;
|
717
|
+
xmax += delta + s.bars.barWidth;
|
718
|
+
}
|
719
|
+
}
|
720
|
+
|
721
|
+
updateAxis(s.xaxis, xmin, xmax);
|
722
|
+
updateAxis(s.yaxis, ymin, ymax);
|
723
|
+
}
|
724
|
+
|
725
|
+
$.each(allAxes(), function (_, axis) {
|
726
|
+
if (axis.datamin == topSentry)
|
727
|
+
axis.datamin = null;
|
728
|
+
if (axis.datamax == bottomSentry)
|
729
|
+
axis.datamax = null;
|
730
|
+
});
|
731
|
+
}
|
732
|
+
|
733
|
+
//////////////////////////////////////////////////////////////////////////////////
|
734
|
+
// Returns the display's ratio between physical and device-independent pixels.
|
735
|
+
//
|
736
|
+
// This is the ratio between the width that the browser advertises and the number
|
737
|
+
// of pixels actually available in that space. The iPhone 4, for example, has a
|
738
|
+
// device-independent width of 320px, but its screen is actually 640px wide. It
|
739
|
+
// therefore has a pixel ratio of 2, while most normal devices have a ratio of 1.
|
740
|
+
|
741
|
+
function getPixelRatio(cctx) {
|
742
|
+
var devicePixelRatio = window.devicePixelRatio || 1;
|
743
|
+
var backingStoreRatio =
|
744
|
+
cctx.webkitBackingStorePixelRatio ||
|
745
|
+
cctx.mozBackingStorePixelRatio ||
|
746
|
+
cctx.msBackingStorePixelRatio ||
|
747
|
+
cctx.oBackingStorePixelRatio ||
|
748
|
+
cctx.backingStorePixelRatio || 1;
|
749
|
+
|
750
|
+
return devicePixelRatio / backingStoreRatio;
|
751
|
+
}
|
752
|
+
|
753
|
+
function makeCanvas(cls) {
|
754
|
+
|
755
|
+
var c = document.createElement('canvas');
|
756
|
+
c.className = cls;
|
757
|
+
|
758
|
+
$(c).css({ direction: "ltr", position: "absolute", left: 0, top: 0 })
|
759
|
+
.appendTo(placeholder);
|
760
|
+
|
761
|
+
// If HTML5 Canvas isn't available, fall back to Excanvas
|
762
|
+
|
763
|
+
if (!c.getContext) {
|
764
|
+
if (window.G_vmlCanvasManager) {
|
765
|
+
c = window.G_vmlCanvasManager.initElement(c);
|
766
|
+
} else {
|
767
|
+
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.");
|
768
|
+
}
|
769
|
+
}
|
770
|
+
|
771
|
+
var cctx = c.getContext("2d");
|
772
|
+
|
773
|
+
// Increase the canvas density based on the display's pixel ratio; basically
|
774
|
+
// giving the canvas more pixels without increasing the size of its element,
|
775
|
+
// to take advantage of the fact that retina displays have that many more
|
776
|
+
// pixels than they actually use for page & element widths.
|
777
|
+
|
778
|
+
var pixelRatio = getPixelRatio(cctx);
|
779
|
+
|
780
|
+
c.width = canvasWidth * pixelRatio;
|
781
|
+
c.height = canvasHeight * pixelRatio;
|
782
|
+
c.style.width = canvasWidth + "px";
|
783
|
+
c.style.height = canvasHeight + "px";
|
784
|
+
|
785
|
+
// Save the context so we can reset in case we get replotted
|
786
|
+
|
787
|
+
cctx.save();
|
788
|
+
|
789
|
+
// Scale the coordinate space to match the display density; so even though we
|
790
|
+
// may have twice as many pixels, we still want lines and other drawing to
|
791
|
+
// appear at the same size; the extra pixels will just make them crisper.
|
792
|
+
|
793
|
+
cctx.scale(pixelRatio, pixelRatio);
|
794
|
+
|
795
|
+
return c;
|
796
|
+
}
|
797
|
+
|
798
|
+
function getCanvasDimensions() {
|
799
|
+
canvasWidth = placeholder.width();
|
800
|
+
canvasHeight = placeholder.height();
|
801
|
+
|
802
|
+
if (canvasWidth <= 0 || canvasHeight <= 0)
|
803
|
+
throw new Error("Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight);
|
804
|
+
}
|
805
|
+
|
806
|
+
function resizeCanvas(c) {
|
807
|
+
|
808
|
+
var cctx = c.getContext("2d");
|
809
|
+
|
810
|
+
// Handle pixel ratios > 1 for retina displays, as explained in makeCanvas
|
811
|
+
|
812
|
+
var pixelRatio = getPixelRatio(cctx);
|
813
|
+
|
814
|
+
// Resizing should reset the state (excanvas seems to be buggy though)
|
815
|
+
|
816
|
+
if (c.style.width != canvasWidth) {
|
817
|
+
c.width = canvasWidth * pixelRatio;
|
818
|
+
c.style.width = canvasWidth + "px";
|
819
|
+
}
|
820
|
+
|
821
|
+
if (c.style.height != canvasHeight) {
|
822
|
+
c.height = canvasHeight * pixelRatio;
|
823
|
+
c.style.height = canvasHeight + "px";
|
824
|
+
}
|
825
|
+
|
826
|
+
// so try to get back to the initial state (even if it's
|
827
|
+
// gone now, this should be safe according to the spec)
|
828
|
+
cctx.restore();
|
829
|
+
|
830
|
+
// and save again
|
831
|
+
cctx.save();
|
832
|
+
|
833
|
+
// Apply scaling for retina displays, as explained in makeCanvas
|
834
|
+
|
835
|
+
cctx.scale(pixelRatio, pixelRatio);
|
836
|
+
}
|
837
|
+
|
838
|
+
function setupCanvases() {
|
839
|
+
var reused,
|
840
|
+
existingCanvas = placeholder.children("canvas.flot-base"),
|
841
|
+
existingOverlay = placeholder.children("canvas.flot-overlay");
|
842
|
+
|
843
|
+
if (existingCanvas.length == 0 || existingOverlay == 0) {
|
844
|
+
// init everything
|
845
|
+
|
846
|
+
placeholder.html(""); // make sure placeholder is clear
|
847
|
+
|
848
|
+
placeholder.css({ padding: 0 }); // padding messes up the positioning
|
849
|
+
|
850
|
+
if (placeholder.css("position") == 'static')
|
851
|
+
placeholder.css("position", "relative"); // for positioning labels and overlay
|
852
|
+
|
853
|
+
getCanvasDimensions();
|
854
|
+
|
855
|
+
canvas = makeCanvas("flot-base");
|
856
|
+
overlay = makeCanvas("flot-overlay"); // overlay canvas for interactive features
|
857
|
+
|
858
|
+
reused = false;
|
859
|
+
}
|
860
|
+
else {
|
861
|
+
// reuse existing elements
|
862
|
+
|
863
|
+
canvas = existingCanvas.get(0);
|
864
|
+
overlay = existingOverlay.get(0);
|
865
|
+
|
866
|
+
reused = true;
|
867
|
+
}
|
868
|
+
|
869
|
+
ctx = canvas.getContext("2d");
|
870
|
+
octx = overlay.getContext("2d");
|
871
|
+
|
872
|
+
// define which element we're listening for events on
|
873
|
+
eventHolder = $(overlay);
|
874
|
+
|
875
|
+
if (reused) {
|
876
|
+
// run shutdown in the old plot object
|
877
|
+
placeholder.data("plot").shutdown();
|
878
|
+
|
879
|
+
// reset reused canvases
|
880
|
+
plot.resize();
|
881
|
+
|
882
|
+
// make sure overlay pixels are cleared (canvas is cleared when we redraw)
|
883
|
+
octx.clearRect(0, 0, canvasWidth, canvasHeight);
|
884
|
+
|
885
|
+
// then whack any remaining obvious garbage left
|
886
|
+
eventHolder.unbind();
|
887
|
+
placeholder.children().not([canvas, overlay]).remove();
|
888
|
+
}
|
889
|
+
|
890
|
+
// save in case we get replotted
|
891
|
+
placeholder.data("plot", plot);
|
892
|
+
}
|
893
|
+
|
894
|
+
function bindEvents() {
|
895
|
+
// bind events
|
896
|
+
if (options.grid.hoverable) {
|
897
|
+
eventHolder.mousemove(onMouseMove);
|
898
|
+
|
899
|
+
// Use bind, rather than .mouseleave, because we officially
|
900
|
+
// still support jQuery 1.2.6, which doesn't define a shortcut
|
901
|
+
// for mouseenter or mouseleave. This was a bug/oversight that
|
902
|
+
// was fixed somewhere around 1.3.x. We can return to using
|
903
|
+
// .mouseleave when we drop support for 1.2.6.
|
904
|
+
|
905
|
+
eventHolder.bind("mouseleave", onMouseLeave);
|
906
|
+
}
|
907
|
+
|
908
|
+
if (options.grid.clickable)
|
909
|
+
eventHolder.click(onClick);
|
910
|
+
|
911
|
+
executeHooks(hooks.bindEvents, [eventHolder]);
|
912
|
+
}
|
913
|
+
|
914
|
+
function shutdown() {
|
915
|
+
if (redrawTimeout)
|
916
|
+
clearTimeout(redrawTimeout);
|
917
|
+
|
918
|
+
eventHolder.unbind("mousemove", onMouseMove);
|
919
|
+
eventHolder.unbind("mouseleave", onMouseLeave);
|
920
|
+
eventHolder.unbind("click", onClick);
|
921
|
+
|
922
|
+
executeHooks(hooks.shutdown, [eventHolder]);
|
923
|
+
}
|
924
|
+
|
925
|
+
function setTransformationHelpers(axis) {
|
926
|
+
// set helper functions on the axis, assumes plot area
|
927
|
+
// has been computed already
|
928
|
+
|
929
|
+
function identity(x) { return x; }
|
930
|
+
|
931
|
+
var s, m, t = axis.options.transform || identity,
|
932
|
+
it = axis.options.inverseTransform;
|
933
|
+
|
934
|
+
// precompute how much the axis is scaling a point
|
935
|
+
// in canvas space
|
936
|
+
if (axis.direction == "x") {
|
937
|
+
s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
|
938
|
+
m = Math.min(t(axis.max), t(axis.min));
|
939
|
+
}
|
940
|
+
else {
|
941
|
+
s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
|
942
|
+
s = -s;
|
943
|
+
m = Math.max(t(axis.max), t(axis.min));
|
944
|
+
}
|
945
|
+
|
946
|
+
// data point to canvas coordinate
|
947
|
+
if (t == identity) // slight optimization
|
948
|
+
axis.p2c = function (p) { return (p - m) * s; };
|
949
|
+
else
|
950
|
+
axis.p2c = function (p) { return (t(p) - m) * s; };
|
951
|
+
// canvas coordinate to data point
|
952
|
+
if (!it)
|
953
|
+
axis.c2p = function (c) { return m + c / s; };
|
954
|
+
else
|
955
|
+
axis.c2p = function (c) { return it(m + c / s); };
|
956
|
+
}
|
957
|
+
|
958
|
+
function measureTickLabels(axis) {
|
959
|
+
var opts = axis.options, ticks = axis.ticks || [],
|
960
|
+
axisw = opts.labelWidth || 0, axish = opts.labelHeight || 0,
|
961
|
+
f = axis.font;
|
962
|
+
|
963
|
+
ctx.save();
|
964
|
+
ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px '" + f.family + "'";
|
965
|
+
|
966
|
+
for (var i = 0; i < ticks.length; ++i) {
|
967
|
+
var t = ticks[i];
|
968
|
+
|
969
|
+
t.lines = [];
|
970
|
+
t.width = t.height = 0;
|
971
|
+
|
972
|
+
if (!t.label)
|
973
|
+
continue;
|
974
|
+
|
975
|
+
// accept various kinds of newlines, including HTML ones
|
976
|
+
// (you can actually split directly on regexps in Javascript,
|
977
|
+
// but IE < 9 is unfortunately broken)
|
978
|
+
var lines = (t.label + "").replace(/<br ?\/?>|\r\n|\r/g, "\n").split("\n");
|
979
|
+
for (var j = 0; j < lines.length; ++j) {
|
980
|
+
var line = { text: lines[j] },
|
981
|
+
m = ctx.measureText(line.text);
|
982
|
+
|
983
|
+
line.width = m.width;
|
984
|
+
// m.height might not be defined, not in the
|
985
|
+
// standard yet
|
986
|
+
line.height = m.height != null ? m.height : f.size;
|
987
|
+
|
988
|
+
// add a bit of margin since font rendering is
|
989
|
+
// not pixel perfect and cut off letters look
|
990
|
+
// bad, this also doubles as spacing between
|
991
|
+
// lines
|
992
|
+
line.height += Math.round(f.size * 0.15);
|
993
|
+
|
994
|
+
t.width = Math.max(line.width, t.width);
|
995
|
+
t.height += line.height;
|
996
|
+
|
997
|
+
t.lines.push(line);
|
998
|
+
}
|
999
|
+
|
1000
|
+
if (opts.labelWidth == null)
|
1001
|
+
axisw = Math.max(axisw, t.width);
|
1002
|
+
if (opts.labelHeight == null)
|
1003
|
+
axish = Math.max(axish, t.height);
|
1004
|
+
}
|
1005
|
+
ctx.restore();
|
1006
|
+
|
1007
|
+
axis.labelWidth = Math.ceil(axisw);
|
1008
|
+
axis.labelHeight = Math.ceil(axish);
|
1009
|
+
}
|
1010
|
+
|
1011
|
+
function allocateAxisBoxFirstPhase(axis) {
|
1012
|
+
// find the bounding box of the axis by looking at label
|
1013
|
+
// widths/heights and ticks, make room by diminishing the
|
1014
|
+
// plotOffset; this first phase only looks at one
|
1015
|
+
// dimension per axis, the other dimension depends on the
|
1016
|
+
// other axes so will have to wait
|
1017
|
+
|
1018
|
+
var lw = axis.labelWidth,
|
1019
|
+
lh = axis.labelHeight,
|
1020
|
+
pos = axis.options.position,
|
1021
|
+
tickLength = axis.options.tickLength,
|
1022
|
+
axisMargin = options.grid.axisMargin,
|
1023
|
+
padding = options.grid.labelMargin,
|
1024
|
+
all = axis.direction == "x" ? xaxes : yaxes,
|
1025
|
+
index, innermost;
|
1026
|
+
|
1027
|
+
// determine axis margin
|
1028
|
+
var samePosition = $.grep(all, function (a) {
|
1029
|
+
return a && a.options.position == pos && a.reserveSpace;
|
1030
|
+
});
|
1031
|
+
if ($.inArray(axis, samePosition) == samePosition.length - 1)
|
1032
|
+
axisMargin = 0; // outermost
|
1033
|
+
|
1034
|
+
// determine tick length - if we're innermost, we can use "full"
|
1035
|
+
if (tickLength == null) {
|
1036
|
+
var sameDirection = $.grep(all, function (a) {
|
1037
|
+
return a && a.reserveSpace;
|
1038
|
+
});
|
1039
|
+
|
1040
|
+
innermost = $.inArray(axis, sameDirection) == 0;
|
1041
|
+
if (innermost)
|
1042
|
+
tickLength = "full";
|
1043
|
+
else
|
1044
|
+
tickLength = 5;
|
1045
|
+
}
|
1046
|
+
|
1047
|
+
if (!isNaN(+tickLength))
|
1048
|
+
padding += +tickLength;
|
1049
|
+
|
1050
|
+
// compute box
|
1051
|
+
if (axis.direction == "x") {
|
1052
|
+
lh += padding;
|
1053
|
+
|
1054
|
+
if (pos == "bottom") {
|
1055
|
+
plotOffset.bottom += lh + axisMargin;
|
1056
|
+
axis.box = { top: canvasHeight - plotOffset.bottom, height: lh };
|
1057
|
+
}
|
1058
|
+
else {
|
1059
|
+
axis.box = { top: plotOffset.top + axisMargin, height: lh };
|
1060
|
+
plotOffset.top += lh + axisMargin;
|
1061
|
+
}
|
1062
|
+
}
|
1063
|
+
else {
|
1064
|
+
lw += padding;
|
1065
|
+
|
1066
|
+
if (pos == "left") {
|
1067
|
+
axis.box = { left: plotOffset.left + axisMargin, width: lw };
|
1068
|
+
plotOffset.left += lw + axisMargin;
|
1069
|
+
}
|
1070
|
+
else {
|
1071
|
+
plotOffset.right += lw + axisMargin;
|
1072
|
+
axis.box = { left: canvasWidth - plotOffset.right, width: lw };
|
1073
|
+
}
|
1074
|
+
}
|
1075
|
+
|
1076
|
+
// save for future reference
|
1077
|
+
axis.position = pos;
|
1078
|
+
axis.tickLength = tickLength;
|
1079
|
+
axis.box.padding = padding;
|
1080
|
+
axis.innermost = innermost;
|
1081
|
+
}
|
1082
|
+
|
1083
|
+
function allocateAxisBoxSecondPhase(axis) {
|
1084
|
+
// now that all axis boxes have been placed in one
|
1085
|
+
// dimension, we can set the remaining dimension coordinates
|
1086
|
+
if (axis.direction == "x") {
|
1087
|
+
axis.box.left = plotOffset.left - axis.labelWidth / 2;
|
1088
|
+
axis.box.width = canvasWidth - plotOffset.left - plotOffset.right + axis.labelWidth;
|
1089
|
+
}
|
1090
|
+
else {
|
1091
|
+
axis.box.top = plotOffset.top - axis.labelHeight / 2;
|
1092
|
+
axis.box.height = canvasHeight - plotOffset.bottom - plotOffset.top + axis.labelHeight;
|
1093
|
+
}
|
1094
|
+
}
|
1095
|
+
|
1096
|
+
function adjustLayoutForThingsStickingOut() {
|
1097
|
+
// possibly adjust plot offset to ensure everything stays
|
1098
|
+
// inside the canvas and isn't clipped off
|
1099
|
+
|
1100
|
+
var minMargin = options.grid.minBorderMargin,
|
1101
|
+
margins = { x: 0, y: 0 }, i, axis;
|
1102
|
+
|
1103
|
+
// check stuff from the plot (FIXME: this should just read
|
1104
|
+
// a value from the series, otherwise it's impossible to
|
1105
|
+
// customize)
|
1106
|
+
if (minMargin == null) {
|
1107
|
+
minMargin = 0;
|
1108
|
+
for (i = 0; i < series.length; ++i)
|
1109
|
+
minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2));
|
1110
|
+
}
|
1111
|
+
|
1112
|
+
margins.x = margins.y = Math.ceil(minMargin);
|
1113
|
+
|
1114
|
+
// check axis labels, note we don't check the actual
|
1115
|
+
// labels but instead use the overall width/height to not
|
1116
|
+
// jump as much around with replots
|
1117
|
+
$.each(allAxes(), function (_, axis) {
|
1118
|
+
var dir = axis.direction;
|
1119
|
+
if (axis.reserveSpace)
|
1120
|
+
margins[dir] = Math.ceil(Math.max(margins[dir], (dir == "x" ? axis.labelWidth : axis.labelHeight) / 2));
|
1121
|
+
});
|
1122
|
+
|
1123
|
+
plotOffset.left = Math.max(margins.x, plotOffset.left);
|
1124
|
+
plotOffset.right = Math.max(margins.x, plotOffset.right);
|
1125
|
+
plotOffset.top = Math.max(margins.y, plotOffset.top);
|
1126
|
+
plotOffset.bottom = Math.max(margins.y, plotOffset.bottom);
|
1127
|
+
}
|
1128
|
+
|
1129
|
+
function setupGrid() {
|
1130
|
+
var i, axes = allAxes(), showGrid = options.grid.show;
|
1131
|
+
|
1132
|
+
// Initialize the plot's offset from the edge of the canvas
|
1133
|
+
|
1134
|
+
for (var a in plotOffset) {
|
1135
|
+
var margin = options.grid.margin || 0;
|
1136
|
+
plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0;
|
1137
|
+
}
|
1138
|
+
|
1139
|
+
executeHooks(hooks.processOffset, [plotOffset]);
|
1140
|
+
|
1141
|
+
// If the grid is visible, add its border width to the offset
|
1142
|
+
|
1143
|
+
for (var a in plotOffset) {
|
1144
|
+
if(typeof(options.grid.borderWidth) == "object") {
|
1145
|
+
plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0;
|
1146
|
+
}
|
1147
|
+
else {
|
1148
|
+
plotOffset[a] += showGrid ? options.grid.borderWidth : 0;
|
1149
|
+
}
|
1150
|
+
}
|
1151
|
+
|
1152
|
+
// init axes
|
1153
|
+
$.each(axes, function (_, axis) {
|
1154
|
+
axis.show = axis.options.show;
|
1155
|
+
if (axis.show == null)
|
1156
|
+
axis.show = axis.used; // by default an axis is visible if it's got data
|
1157
|
+
|
1158
|
+
axis.reserveSpace = axis.show || axis.options.reserveSpace;
|
1159
|
+
|
1160
|
+
setRange(axis);
|
1161
|
+
});
|
1162
|
+
|
1163
|
+
if (showGrid) {
|
1164
|
+
// determine from the placeholder the font size ~ height of font ~ 1 em
|
1165
|
+
var fontDefaults = {
|
1166
|
+
style: placeholder.css("font-style"),
|
1167
|
+
size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)),
|
1168
|
+
variant: placeholder.css("font-variant"),
|
1169
|
+
weight: placeholder.css("font-weight"),
|
1170
|
+
family: placeholder.css("font-family")
|
1171
|
+
};
|
1172
|
+
|
1173
|
+
var allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; });
|
1174
|
+
|
1175
|
+
$.each(allocatedAxes, function (_, axis) {
|
1176
|
+
// make the ticks
|
1177
|
+
setupTickGeneration(axis);
|
1178
|
+
setTicks(axis);
|
1179
|
+
snapRangeToTicks(axis, axis.ticks);
|
1180
|
+
|
1181
|
+
// find labelWidth/Height for axis
|
1182
|
+
axis.font = $.extend({}, fontDefaults, axis.options.font);
|
1183
|
+
measureTickLabels(axis);
|
1184
|
+
});
|
1185
|
+
|
1186
|
+
// with all dimensions calculated, we can compute the
|
1187
|
+
// axis bounding boxes, start from the outside
|
1188
|
+
// (reverse order)
|
1189
|
+
for (i = allocatedAxes.length - 1; i >= 0; --i)
|
1190
|
+
allocateAxisBoxFirstPhase(allocatedAxes[i]);
|
1191
|
+
|
1192
|
+
// make sure we've got enough space for things that
|
1193
|
+
// might stick out
|
1194
|
+
adjustLayoutForThingsStickingOut();
|
1195
|
+
|
1196
|
+
$.each(allocatedAxes, function (_, axis) {
|
1197
|
+
allocateAxisBoxSecondPhase(axis);
|
1198
|
+
});
|
1199
|
+
}
|
1200
|
+
|
1201
|
+
plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
|
1202
|
+
plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
|
1203
|
+
|
1204
|
+
// now we got the proper plot dimensions, we can compute the scaling
|
1205
|
+
$.each(axes, function (_, axis) {
|
1206
|
+
setTransformationHelpers(axis);
|
1207
|
+
});
|
1208
|
+
|
1209
|
+
insertLegend();
|
1210
|
+
}
|
1211
|
+
|
1212
|
+
function setRange(axis) {
|
1213
|
+
var opts = axis.options,
|
1214
|
+
min = +(opts.min != null ? opts.min : axis.datamin),
|
1215
|
+
max = +(opts.max != null ? opts.max : axis.datamax),
|
1216
|
+
delta = max - min;
|
1217
|
+
|
1218
|
+
if (delta == 0.0) {
|
1219
|
+
// degenerate case
|
1220
|
+
var widen = max == 0 ? 1 : 0.01;
|
1221
|
+
|
1222
|
+
if (opts.min == null)
|
1223
|
+
min -= widen;
|
1224
|
+
// always widen max if we couldn't widen min to ensure we
|
1225
|
+
// don't fall into min == max which doesn't work
|
1226
|
+
if (opts.max == null || opts.min != null)
|
1227
|
+
max += widen;
|
1228
|
+
}
|
1229
|
+
else {
|
1230
|
+
// consider autoscaling
|
1231
|
+
var margin = opts.autoscaleMargin;
|
1232
|
+
if (margin != null) {
|
1233
|
+
if (opts.min == null) {
|
1234
|
+
min -= delta * margin;
|
1235
|
+
// make sure we don't go below zero if all values
|
1236
|
+
// are positive
|
1237
|
+
if (min < 0 && axis.datamin != null && axis.datamin >= 0)
|
1238
|
+
min = 0;
|
1239
|
+
}
|
1240
|
+
if (opts.max == null) {
|
1241
|
+
max += delta * margin;
|
1242
|
+
if (max > 0 && axis.datamax != null && axis.datamax <= 0)
|
1243
|
+
max = 0;
|
1244
|
+
}
|
1245
|
+
}
|
1246
|
+
}
|
1247
|
+
axis.min = min;
|
1248
|
+
axis.max = max;
|
1249
|
+
}
|
1250
|
+
|
1251
|
+
function setupTickGeneration(axis) {
|
1252
|
+
var opts = axis.options;
|
1253
|
+
|
1254
|
+
// estimate number of ticks
|
1255
|
+
var noTicks;
|
1256
|
+
if (typeof opts.ticks == "number" && opts.ticks > 0)
|
1257
|
+
noTicks = opts.ticks;
|
1258
|
+
else
|
1259
|
+
// heuristic based on the model a*sqrt(x) fitted to
|
1260
|
+
// some data points that seemed reasonable
|
1261
|
+
noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight);
|
1262
|
+
|
1263
|
+
axis.delta = (axis.max - axis.min) / noTicks;
|
1264
|
+
|
1265
|
+
// Time mode was moved to a plug-in in 0.8, but since so many people use this
|
1266
|
+
// we'll add an especially friendly make sure they remembered to include it.
|
1267
|
+
|
1268
|
+
if (opts.mode == "time" && !axis.tickGenerator) {
|
1269
|
+
throw new Error("Time mode requires the flot.time plugin.");
|
1270
|
+
}
|
1271
|
+
|
1272
|
+
// Flot supports base-10 axes; any other mode else is handled by a plug-in,
|
1273
|
+
// like flot.time.js.
|
1274
|
+
|
1275
|
+
if (!axis.tickGenerator) {
|
1276
|
+
|
1277
|
+
axis.tickGenerator = function (axis) {
|
1278
|
+
var maxDec = opts.tickDecimals,
|
1279
|
+
dec = -Math.floor(Math.log(axis.delta) / Math.LN10);
|
1280
|
+
|
1281
|
+
if (maxDec != null && dec > maxDec)
|
1282
|
+
dec = maxDec;
|
1283
|
+
|
1284
|
+
var magn = Math.pow(10, -dec),
|
1285
|
+
norm = axis.delta / magn, // norm is between 1.0 and 10.0
|
1286
|
+
size,
|
1287
|
+
|
1288
|
+
ticks = [],
|
1289
|
+
start,
|
1290
|
+
i = 0,
|
1291
|
+
v = Number.NaN,
|
1292
|
+
prev;
|
1293
|
+
|
1294
|
+
if (norm < 1.5)
|
1295
|
+
size = 1;
|
1296
|
+
else if (norm < 3) {
|
1297
|
+
size = 2;
|
1298
|
+
// special case for 2.5, requires an extra decimal
|
1299
|
+
if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
|
1300
|
+
size = 2.5;
|
1301
|
+
++dec;
|
1302
|
+
}
|
1303
|
+
}
|
1304
|
+
else if (norm < 7.5)
|
1305
|
+
size = 5;
|
1306
|
+
else size = 10;
|
1307
|
+
|
1308
|
+
size *= magn;
|
1309
|
+
|
1310
|
+
if (opts.minTickSize != null && size < opts.minTickSize)
|
1311
|
+
size = opts.minTickSize;
|
1312
|
+
|
1313
|
+
axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
|
1314
|
+
axis.tickSize = opts.tickSize || size;
|
1315
|
+
|
1316
|
+
start = floorInBase(axis.min, axis.tickSize)
|
1317
|
+
|
1318
|
+
do {
|
1319
|
+
prev = v;
|
1320
|
+
v = start + i * axis.tickSize;
|
1321
|
+
ticks.push(v);
|
1322
|
+
++i;
|
1323
|
+
} while (v < axis.max && v != prev);
|
1324
|
+
return ticks;
|
1325
|
+
};
|
1326
|
+
|
1327
|
+
axis.tickFormatter = function (value, axis) {
|
1328
|
+
|
1329
|
+
var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1;
|
1330
|
+
var formatted = "" + Math.round(value * factor) / factor;
|
1331
|
+
|
1332
|
+
// If tickDecimals was specified, ensure that we have exactly that
|
1333
|
+
// much precision; otherwise default to the value's own precision.
|
1334
|
+
|
1335
|
+
if (axis.tickDecimals != null) {
|
1336
|
+
var decimal = formatted.indexOf(".");
|
1337
|
+
var precision = decimal == -1 ? 0 : formatted.length - decimal - 1;
|
1338
|
+
if (precision < axis.tickDecimals) {
|
1339
|
+
return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision);
|
1340
|
+
}
|
1341
|
+
}
|
1342
|
+
|
1343
|
+
return formatted;
|
1344
|
+
};
|
1345
|
+
}
|
1346
|
+
|
1347
|
+
if ($.isFunction(opts.tickFormatter))
|
1348
|
+
axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); };
|
1349
|
+
|
1350
|
+
if (opts.alignTicksWithAxis != null) {
|
1351
|
+
var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];
|
1352
|
+
if (otherAxis && otherAxis.used && otherAxis != axis) {
|
1353
|
+
// consider snapping min/max to outermost nice ticks
|
1354
|
+
var niceTicks = axis.tickGenerator(axis);
|
1355
|
+
if (niceTicks.length > 0) {
|
1356
|
+
if (opts.min == null)
|
1357
|
+
axis.min = Math.min(axis.min, niceTicks[0]);
|
1358
|
+
if (opts.max == null && niceTicks.length > 1)
|
1359
|
+
axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);
|
1360
|
+
}
|
1361
|
+
|
1362
|
+
axis.tickGenerator = function (axis) {
|
1363
|
+
// copy ticks, scaled to this axis
|
1364
|
+
var ticks = [], v, i;
|
1365
|
+
for (i = 0; i < otherAxis.ticks.length; ++i) {
|
1366
|
+
v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);
|
1367
|
+
v = axis.min + v * (axis.max - axis.min);
|
1368
|
+
ticks.push(v);
|
1369
|
+
}
|
1370
|
+
return ticks;
|
1371
|
+
};
|
1372
|
+
|
1373
|
+
// we might need an extra decimal since forced
|
1374
|
+
// ticks don't necessarily fit naturally
|
1375
|
+
if (!axis.mode && opts.tickDecimals == null) {
|
1376
|
+
var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1),
|
1377
|
+
ts = axis.tickGenerator(axis);
|
1378
|
+
|
1379
|
+
// only proceed if the tick interval rounded
|
1380
|
+
// with an extra decimal doesn't give us a
|
1381
|
+
// zero at end
|
1382
|
+
if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))
|
1383
|
+
axis.tickDecimals = extraDec;
|
1384
|
+
}
|
1385
|
+
}
|
1386
|
+
}
|
1387
|
+
}
|
1388
|
+
|
1389
|
+
function setTicks(axis) {
|
1390
|
+
var oticks = axis.options.ticks, ticks = [];
|
1391
|
+
if (oticks == null || (typeof oticks == "number" && oticks > 0))
|
1392
|
+
ticks = axis.tickGenerator(axis);
|
1393
|
+
else if (oticks) {
|
1394
|
+
if ($.isFunction(oticks))
|
1395
|
+
// generate the ticks
|
1396
|
+
ticks = oticks(axis);
|
1397
|
+
else
|
1398
|
+
ticks = oticks;
|
1399
|
+
}
|
1400
|
+
|
1401
|
+
// clean up/labelify the supplied ticks, copy them over
|
1402
|
+
var i, v;
|
1403
|
+
axis.ticks = [];
|
1404
|
+
for (i = 0; i < ticks.length; ++i) {
|
1405
|
+
var label = null;
|
1406
|
+
var t = ticks[i];
|
1407
|
+
if (typeof t == "object") {
|
1408
|
+
v = +t[0];
|
1409
|
+
if (t.length > 1)
|
1410
|
+
label = t[1];
|
1411
|
+
}
|
1412
|
+
else
|
1413
|
+
v = +t;
|
1414
|
+
if (label == null)
|
1415
|
+
label = axis.tickFormatter(v, axis);
|
1416
|
+
if (!isNaN(v))
|
1417
|
+
axis.ticks.push({ v: v, label: label });
|
1418
|
+
}
|
1419
|
+
}
|
1420
|
+
|
1421
|
+
function snapRangeToTicks(axis, ticks) {
|
1422
|
+
if (axis.options.autoscaleMargin && ticks.length > 0) {
|
1423
|
+
// snap to ticks
|
1424
|
+
if (axis.options.min == null)
|
1425
|
+
axis.min = Math.min(axis.min, ticks[0].v);
|
1426
|
+
if (axis.options.max == null && ticks.length > 1)
|
1427
|
+
axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
|
1428
|
+
}
|
1429
|
+
}
|
1430
|
+
|
1431
|
+
function draw() {
|
1432
|
+
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
1433
|
+
|
1434
|
+
executeHooks(hooks.drawBackground, [ctx]);
|
1435
|
+
|
1436
|
+
var grid = options.grid;
|
1437
|
+
|
1438
|
+
// draw background, if any
|
1439
|
+
if (grid.show && grid.backgroundColor)
|
1440
|
+
drawBackground();
|
1441
|
+
|
1442
|
+
if (grid.show && !grid.aboveData) {
|
1443
|
+
drawGrid();
|
1444
|
+
drawAxisLabels();
|
1445
|
+
}
|
1446
|
+
|
1447
|
+
for (var i = 0; i < series.length; ++i) {
|
1448
|
+
executeHooks(hooks.drawSeries, [ctx, series[i]]);
|
1449
|
+
drawSeries(series[i]);
|
1450
|
+
}
|
1451
|
+
|
1452
|
+
executeHooks(hooks.draw, [ctx]);
|
1453
|
+
|
1454
|
+
if (grid.show && grid.aboveData) {
|
1455
|
+
drawGrid();
|
1456
|
+
drawAxisLabels();
|
1457
|
+
}
|
1458
|
+
}
|
1459
|
+
|
1460
|
+
function extractRange(ranges, coord) {
|
1461
|
+
var axis, from, to, key, axes = allAxes();
|
1462
|
+
|
1463
|
+
for (var i = 0; i < axes.length; ++i) {
|
1464
|
+
axis = axes[i];
|
1465
|
+
if (axis.direction == coord) {
|
1466
|
+
key = coord + axis.n + "axis";
|
1467
|
+
if (!ranges[key] && axis.n == 1)
|
1468
|
+
key = coord + "axis"; // support x1axis as xaxis
|
1469
|
+
if (ranges[key]) {
|
1470
|
+
from = ranges[key].from;
|
1471
|
+
to = ranges[key].to;
|
1472
|
+
break;
|
1473
|
+
}
|
1474
|
+
}
|
1475
|
+
}
|
1476
|
+
|
1477
|
+
// backwards-compat stuff - to be removed in future
|
1478
|
+
if (!ranges[key]) {
|
1479
|
+
axis = coord == "x" ? xaxes[0] : yaxes[0];
|
1480
|
+
from = ranges[coord + "1"];
|
1481
|
+
to = ranges[coord + "2"];
|
1482
|
+
}
|
1483
|
+
|
1484
|
+
// auto-reverse as an added bonus
|
1485
|
+
if (from != null && to != null && from > to) {
|
1486
|
+
var tmp = from;
|
1487
|
+
from = to;
|
1488
|
+
to = tmp;
|
1489
|
+
}
|
1490
|
+
|
1491
|
+
return { from: from, to: to, axis: axis };
|
1492
|
+
}
|
1493
|
+
|
1494
|
+
function drawBackground() {
|
1495
|
+
ctx.save();
|
1496
|
+
ctx.translate(plotOffset.left, plotOffset.top);
|
1497
|
+
|
1498
|
+
ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
|
1499
|
+
ctx.fillRect(0, 0, plotWidth, plotHeight);
|
1500
|
+
ctx.restore();
|
1501
|
+
}
|
1502
|
+
|
1503
|
+
function drawGrid() {
|
1504
|
+
var i, axes, bw, bc;
|
1505
|
+
|
1506
|
+
ctx.save();
|
1507
|
+
ctx.translate(plotOffset.left, plotOffset.top);
|
1508
|
+
|
1509
|
+
// draw markings
|
1510
|
+
var markings = options.grid.markings;
|
1511
|
+
if (markings) {
|
1512
|
+
if ($.isFunction(markings)) {
|
1513
|
+
axes = plot.getAxes();
|
1514
|
+
// xmin etc. is backwards compatibility, to be
|
1515
|
+
// removed in the future
|
1516
|
+
axes.xmin = axes.xaxis.min;
|
1517
|
+
axes.xmax = axes.xaxis.max;
|
1518
|
+
axes.ymin = axes.yaxis.min;
|
1519
|
+
axes.ymax = axes.yaxis.max;
|
1520
|
+
|
1521
|
+
markings = markings(axes);
|
1522
|
+
}
|
1523
|
+
|
1524
|
+
for (i = 0; i < markings.length; ++i) {
|
1525
|
+
var m = markings[i],
|
1526
|
+
xrange = extractRange(m, "x"),
|
1527
|
+
yrange = extractRange(m, "y");
|
1528
|
+
|
1529
|
+
// fill in missing
|
1530
|
+
if (xrange.from == null)
|
1531
|
+
xrange.from = xrange.axis.min;
|
1532
|
+
if (xrange.to == null)
|
1533
|
+
xrange.to = xrange.axis.max;
|
1534
|
+
if (yrange.from == null)
|
1535
|
+
yrange.from = yrange.axis.min;
|
1536
|
+
if (yrange.to == null)
|
1537
|
+
yrange.to = yrange.axis.max;
|
1538
|
+
|
1539
|
+
// clip
|
1540
|
+
if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
|
1541
|
+
yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
|
1542
|
+
continue;
|
1543
|
+
|
1544
|
+
xrange.from = Math.max(xrange.from, xrange.axis.min);
|
1545
|
+
xrange.to = Math.min(xrange.to, xrange.axis.max);
|
1546
|
+
yrange.from = Math.max(yrange.from, yrange.axis.min);
|
1547
|
+
yrange.to = Math.min(yrange.to, yrange.axis.max);
|
1548
|
+
|
1549
|
+
if (xrange.from == xrange.to && yrange.from == yrange.to)
|
1550
|
+
continue;
|
1551
|
+
|
1552
|
+
// then draw
|
1553
|
+
xrange.from = xrange.axis.p2c(xrange.from);
|
1554
|
+
xrange.to = xrange.axis.p2c(xrange.to);
|
1555
|
+
yrange.from = yrange.axis.p2c(yrange.from);
|
1556
|
+
yrange.to = yrange.axis.p2c(yrange.to);
|
1557
|
+
|
1558
|
+
if (xrange.from == xrange.to || yrange.from == yrange.to) {
|
1559
|
+
// draw line
|
1560
|
+
ctx.beginPath();
|
1561
|
+
ctx.strokeStyle = m.color || options.grid.markingsColor;
|
1562
|
+
ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
|
1563
|
+
ctx.moveTo(xrange.from, yrange.from);
|
1564
|
+
ctx.lineTo(xrange.to, yrange.to);
|
1565
|
+
ctx.stroke();
|
1566
|
+
}
|
1567
|
+
else {
|
1568
|
+
// fill area
|
1569
|
+
ctx.fillStyle = m.color || options.grid.markingsColor;
|
1570
|
+
ctx.fillRect(xrange.from, yrange.to,
|
1571
|
+
xrange.to - xrange.from,
|
1572
|
+
yrange.from - yrange.to);
|
1573
|
+
}
|
1574
|
+
}
|
1575
|
+
}
|
1576
|
+
|
1577
|
+
// draw the ticks
|
1578
|
+
axes = allAxes();
|
1579
|
+
bw = options.grid.borderWidth;
|
1580
|
+
|
1581
|
+
for (var j = 0; j < axes.length; ++j) {
|
1582
|
+
var axis = axes[j], box = axis.box,
|
1583
|
+
t = axis.tickLength, x, y, xoff, yoff;
|
1584
|
+
if (!axis.show || axis.ticks.length == 0)
|
1585
|
+
continue;
|
1586
|
+
|
1587
|
+
ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString();
|
1588
|
+
ctx.lineWidth = 1;
|
1589
|
+
|
1590
|
+
// find the edges
|
1591
|
+
if (axis.direction == "x") {
|
1592
|
+
x = 0;
|
1593
|
+
if (t == "full")
|
1594
|
+
y = (axis.position == "top" ? 0 : plotHeight);
|
1595
|
+
else
|
1596
|
+
y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0);
|
1597
|
+
}
|
1598
|
+
else {
|
1599
|
+
y = 0;
|
1600
|
+
if (t == "full")
|
1601
|
+
x = (axis.position == "left" ? 0 : plotWidth);
|
1602
|
+
else
|
1603
|
+
x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0);
|
1604
|
+
}
|
1605
|
+
|
1606
|
+
// draw tick bar
|
1607
|
+
if (!axis.innermost) {
|
1608
|
+
ctx.beginPath();
|
1609
|
+
xoff = yoff = 0;
|
1610
|
+
if (axis.direction == "x")
|
1611
|
+
xoff = plotWidth;
|
1612
|
+
else
|
1613
|
+
yoff = plotHeight;
|
1614
|
+
|
1615
|
+
if (ctx.lineWidth == 1) {
|
1616
|
+
x = Math.floor(x) + 0.5;
|
1617
|
+
y = Math.floor(y) + 0.5;
|
1618
|
+
}
|
1619
|
+
|
1620
|
+
ctx.moveTo(x, y);
|
1621
|
+
ctx.lineTo(x + xoff, y + yoff);
|
1622
|
+
ctx.stroke();
|
1623
|
+
}
|
1624
|
+
|
1625
|
+
// draw ticks
|
1626
|
+
ctx.beginPath();
|
1627
|
+
for (i = 0; i < axis.ticks.length; ++i) {
|
1628
|
+
var v = axis.ticks[i].v;
|
1629
|
+
|
1630
|
+
xoff = yoff = 0;
|
1631
|
+
|
1632
|
+
if (v < axis.min || v > axis.max
|
1633
|
+
// skip those lying on the axes if we got a border
|
1634
|
+
|| (t == "full"
|
1635
|
+
&& ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0)
|
1636
|
+
&& (v == axis.min || v == axis.max)))
|
1637
|
+
continue;
|
1638
|
+
|
1639
|
+
if (axis.direction == "x") {
|
1640
|
+
x = axis.p2c(v);
|
1641
|
+
yoff = t == "full" ? -plotHeight : t;
|
1642
|
+
|
1643
|
+
if (axis.position == "top")
|
1644
|
+
yoff = -yoff;
|
1645
|
+
}
|
1646
|
+
else {
|
1647
|
+
y = axis.p2c(v);
|
1648
|
+
xoff = t == "full" ? -plotWidth : t;
|
1649
|
+
|
1650
|
+
if (axis.position == "left")
|
1651
|
+
xoff = -xoff;
|
1652
|
+
}
|
1653
|
+
|
1654
|
+
if (ctx.lineWidth == 1) {
|
1655
|
+
if (axis.direction == "x")
|
1656
|
+
x = Math.floor(x) + 0.5;
|
1657
|
+
else
|
1658
|
+
y = Math.floor(y) + 0.5;
|
1659
|
+
}
|
1660
|
+
|
1661
|
+
ctx.moveTo(x, y);
|
1662
|
+
ctx.lineTo(x + xoff, y + yoff);
|
1663
|
+
}
|
1664
|
+
|
1665
|
+
ctx.stroke();
|
1666
|
+
}
|
1667
|
+
|
1668
|
+
|
1669
|
+
// draw border
|
1670
|
+
if (bw) {
|
1671
|
+
// If either borderWidth or borderColor is an object, then draw the border
|
1672
|
+
// line by line instead of as one rectangle
|
1673
|
+
bc = options.grid.borderColor;
|
1674
|
+
if(typeof bw == "object" || typeof bc == "object") {
|
1675
|
+
if (typeof bw !== "object") {
|
1676
|
+
bw = {top: bw, right: bw, bottom: bw, left: bw};
|
1677
|
+
}
|
1678
|
+
if (typeof bc !== "object") {
|
1679
|
+
bc = {top: bc, right: bc, bottom: bc, left: bc};
|
1680
|
+
}
|
1681
|
+
|
1682
|
+
if (bw.top > 0) {
|
1683
|
+
ctx.strokeStyle = bc.top;
|
1684
|
+
ctx.lineWidth = bw.top;
|
1685
|
+
ctx.beginPath();
|
1686
|
+
ctx.moveTo(0 - bw.left, 0 - bw.top/2);
|
1687
|
+
ctx.lineTo(plotWidth, 0 - bw.top/2);
|
1688
|
+
ctx.stroke();
|
1689
|
+
}
|
1690
|
+
|
1691
|
+
if (bw.right > 0) {
|
1692
|
+
ctx.strokeStyle = bc.right;
|
1693
|
+
ctx.lineWidth = bw.right;
|
1694
|
+
ctx.beginPath();
|
1695
|
+
ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top);
|
1696
|
+
ctx.lineTo(plotWidth + bw.right / 2, plotHeight);
|
1697
|
+
ctx.stroke();
|
1698
|
+
}
|
1699
|
+
|
1700
|
+
if (bw.bottom > 0) {
|
1701
|
+
ctx.strokeStyle = bc.bottom;
|
1702
|
+
ctx.lineWidth = bw.bottom;
|
1703
|
+
ctx.beginPath();
|
1704
|
+
ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2);
|
1705
|
+
ctx.lineTo(0, plotHeight + bw.bottom / 2);
|
1706
|
+
ctx.stroke();
|
1707
|
+
}
|
1708
|
+
|
1709
|
+
if (bw.left > 0) {
|
1710
|
+
ctx.strokeStyle = bc.left;
|
1711
|
+
ctx.lineWidth = bw.left;
|
1712
|
+
ctx.beginPath();
|
1713
|
+
ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom);
|
1714
|
+
ctx.lineTo(0- bw.left/2, 0);
|
1715
|
+
ctx.stroke();
|
1716
|
+
}
|
1717
|
+
}
|
1718
|
+
else {
|
1719
|
+
ctx.lineWidth = bw;
|
1720
|
+
ctx.strokeStyle = options.grid.borderColor;
|
1721
|
+
ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
|
1722
|
+
}
|
1723
|
+
}
|
1724
|
+
|
1725
|
+
ctx.restore();
|
1726
|
+
}
|
1727
|
+
|
1728
|
+
function drawAxisLabels() {
|
1729
|
+
ctx.save();
|
1730
|
+
|
1731
|
+
$.each(allAxes(), function (_, axis) {
|
1732
|
+
if (!axis.show || axis.ticks.length == 0)
|
1733
|
+
return;
|
1734
|
+
|
1735
|
+
var box = axis.box, f = axis.font;
|
1736
|
+
// 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
|
1737
|
+
|
1738
|
+
ctx.fillStyle = axis.options.color;
|
1739
|
+
// Important: Don't use quotes around axis.font.family! Just around single
|
1740
|
+
// font names like 'Times New Roman' that have a space or special character in it.
|
1741
|
+
ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px " + f.family;
|
1742
|
+
ctx.textAlign = "start";
|
1743
|
+
// middle align the labels - top would be more
|
1744
|
+
// natural, but browsers can differ a pixel or two in
|
1745
|
+
// where they consider the top to be, so instead we
|
1746
|
+
// middle align to minimize variation between browsers
|
1747
|
+
// and compensate when calculating the coordinates
|
1748
|
+
ctx.textBaseline = "middle";
|
1749
|
+
|
1750
|
+
for (var i = 0; i < axis.ticks.length; ++i) {
|
1751
|
+
var tick = axis.ticks[i];
|
1752
|
+
if (!tick.label || tick.v < axis.min || tick.v > axis.max)
|
1753
|
+
continue;
|
1754
|
+
|
1755
|
+
var x, y, offset = 0, line;
|
1756
|
+
for (var k = 0; k < tick.lines.length; ++k) {
|
1757
|
+
line = tick.lines[k];
|
1758
|
+
|
1759
|
+
if (axis.direction == "x") {
|
1760
|
+
x = plotOffset.left + axis.p2c(tick.v) - line.width/2;
|
1761
|
+
if (axis.position == "bottom")
|
1762
|
+
y = box.top + box.padding;
|
1763
|
+
else
|
1764
|
+
y = box.top + box.height - box.padding - tick.height;
|
1765
|
+
}
|
1766
|
+
else {
|
1767
|
+
y = plotOffset.top + axis.p2c(tick.v) - tick.height/2;
|
1768
|
+
if (axis.position == "left")
|
1769
|
+
x = box.left + box.width - box.padding - line.width;
|
1770
|
+
else
|
1771
|
+
x = box.left + box.padding;
|
1772
|
+
}
|
1773
|
+
|
1774
|
+
// account for middle aligning and line number
|
1775
|
+
y += line.height/2 + offset;
|
1776
|
+
offset += line.height;
|
1777
|
+
|
1778
|
+
if (!!(window.opera && window.opera.version().split('.')[0] < 12)) {
|
1779
|
+
// FIXME: LEGACY BROWSER FIX
|
1780
|
+
// AFFECTS: Opera < 12.00
|
1781
|
+
|
1782
|
+
// round the coordinates since Opera
|
1783
|
+
// otherwise switches to more ugly
|
1784
|
+
// rendering (probably non-hinted) and
|
1785
|
+
// offset the y coordinates since it seems
|
1786
|
+
// to be off pretty consistently compared
|
1787
|
+
// to the other browsers
|
1788
|
+
x = Math.floor(x);
|
1789
|
+
y = Math.ceil(y - 2);
|
1790
|
+
}
|
1791
|
+
ctx.fillText(line.text, x, y);
|
1792
|
+
}
|
1793
|
+
}
|
1794
|
+
});
|
1795
|
+
|
1796
|
+
ctx.restore();
|
1797
|
+
}
|
1798
|
+
|
1799
|
+
function drawSeries(series) {
|
1800
|
+
if (series.lines.show)
|
1801
|
+
drawSeriesLines(series);
|
1802
|
+
if (series.bars.show)
|
1803
|
+
drawSeriesBars(series);
|
1804
|
+
if (series.points.show)
|
1805
|
+
drawSeriesPoints(series);
|
1806
|
+
}
|
1807
|
+
|
1808
|
+
function drawSeriesLines(series) {
|
1809
|
+
function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
|
1810
|
+
var points = datapoints.points,
|
1811
|
+
ps = datapoints.pointsize,
|
1812
|
+
prevx = null, prevy = null;
|
1813
|
+
|
1814
|
+
ctx.beginPath();
|
1815
|
+
for (var i = ps; i < points.length; i += ps) {
|
1816
|
+
var x1 = points[i - ps], y1 = points[i - ps + 1],
|
1817
|
+
x2 = points[i], y2 = points[i + 1];
|
1818
|
+
|
1819
|
+
if (x1 == null || x2 == null)
|
1820
|
+
continue;
|
1821
|
+
|
1822
|
+
// clip with ymin
|
1823
|
+
if (y1 <= y2 && y1 < axisy.min) {
|
1824
|
+
if (y2 < axisy.min)
|
1825
|
+
continue; // line segment is outside
|
1826
|
+
// compute new intersection point
|
1827
|
+
x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
|
1828
|
+
y1 = axisy.min;
|
1829
|
+
}
|
1830
|
+
else if (y2 <= y1 && y2 < axisy.min) {
|
1831
|
+
if (y1 < axisy.min)
|
1832
|
+
continue;
|
1833
|
+
x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
|
1834
|
+
y2 = axisy.min;
|
1835
|
+
}
|
1836
|
+
|
1837
|
+
// clip with ymax
|
1838
|
+
if (y1 >= y2 && y1 > axisy.max) {
|
1839
|
+
if (y2 > axisy.max)
|
1840
|
+
continue;
|
1841
|
+
x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
|
1842
|
+
y1 = axisy.max;
|
1843
|
+
}
|
1844
|
+
else if (y2 >= y1 && y2 > axisy.max) {
|
1845
|
+
if (y1 > axisy.max)
|
1846
|
+
continue;
|
1847
|
+
x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
|
1848
|
+
y2 = axisy.max;
|
1849
|
+
}
|
1850
|
+
|
1851
|
+
// clip with xmin
|
1852
|
+
if (x1 <= x2 && x1 < axisx.min) {
|
1853
|
+
if (x2 < axisx.min)
|
1854
|
+
continue;
|
1855
|
+
y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1856
|
+
x1 = axisx.min;
|
1857
|
+
}
|
1858
|
+
else if (x2 <= x1 && x2 < axisx.min) {
|
1859
|
+
if (x1 < axisx.min)
|
1860
|
+
continue;
|
1861
|
+
y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1862
|
+
x2 = axisx.min;
|
1863
|
+
}
|
1864
|
+
|
1865
|
+
// clip with xmax
|
1866
|
+
if (x1 >= x2 && x1 > axisx.max) {
|
1867
|
+
if (x2 > axisx.max)
|
1868
|
+
continue;
|
1869
|
+
y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1870
|
+
x1 = axisx.max;
|
1871
|
+
}
|
1872
|
+
else if (x2 >= x1 && x2 > axisx.max) {
|
1873
|
+
if (x1 > axisx.max)
|
1874
|
+
continue;
|
1875
|
+
y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1876
|
+
x2 = axisx.max;
|
1877
|
+
}
|
1878
|
+
|
1879
|
+
if (x1 != prevx || y1 != prevy)
|
1880
|
+
ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
|
1881
|
+
|
1882
|
+
prevx = x2;
|
1883
|
+
prevy = y2;
|
1884
|
+
ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
|
1885
|
+
}
|
1886
|
+
ctx.stroke();
|
1887
|
+
}
|
1888
|
+
|
1889
|
+
function plotLineArea(datapoints, axisx, axisy) {
|
1890
|
+
var points = datapoints.points,
|
1891
|
+
ps = datapoints.pointsize,
|
1892
|
+
bottom = Math.min(Math.max(0, axisy.min), axisy.max),
|
1893
|
+
i = 0, top, areaOpen = false,
|
1894
|
+
ypos = 1, segmentStart = 0, segmentEnd = 0;
|
1895
|
+
|
1896
|
+
// we process each segment in two turns, first forward
|
1897
|
+
// direction to sketch out top, then once we hit the
|
1898
|
+
// end we go backwards to sketch the bottom
|
1899
|
+
while (true) {
|
1900
|
+
if (ps > 0 && i > points.length + ps)
|
1901
|
+
break;
|
1902
|
+
|
1903
|
+
i += ps; // ps is negative if going backwards
|
1904
|
+
|
1905
|
+
var x1 = points[i - ps],
|
1906
|
+
y1 = points[i - ps + ypos],
|
1907
|
+
x2 = points[i], y2 = points[i + ypos];
|
1908
|
+
|
1909
|
+
if (areaOpen) {
|
1910
|
+
if (ps > 0 && x1 != null && x2 == null) {
|
1911
|
+
// at turning point
|
1912
|
+
segmentEnd = i;
|
1913
|
+
ps = -ps;
|
1914
|
+
ypos = 2;
|
1915
|
+
continue;
|
1916
|
+
}
|
1917
|
+
|
1918
|
+
if (ps < 0 && i == segmentStart + ps) {
|
1919
|
+
// done with the reverse sweep
|
1920
|
+
ctx.fill();
|
1921
|
+
areaOpen = false;
|
1922
|
+
ps = -ps;
|
1923
|
+
ypos = 1;
|
1924
|
+
i = segmentStart = segmentEnd + ps;
|
1925
|
+
continue;
|
1926
|
+
}
|
1927
|
+
}
|
1928
|
+
|
1929
|
+
if (x1 == null || x2 == null)
|
1930
|
+
continue;
|
1931
|
+
|
1932
|
+
// clip x values
|
1933
|
+
|
1934
|
+
// clip with xmin
|
1935
|
+
if (x1 <= x2 && x1 < axisx.min) {
|
1936
|
+
if (x2 < axisx.min)
|
1937
|
+
continue;
|
1938
|
+
y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1939
|
+
x1 = axisx.min;
|
1940
|
+
}
|
1941
|
+
else if (x2 <= x1 && x2 < axisx.min) {
|
1942
|
+
if (x1 < axisx.min)
|
1943
|
+
continue;
|
1944
|
+
y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1945
|
+
x2 = axisx.min;
|
1946
|
+
}
|
1947
|
+
|
1948
|
+
// clip with xmax
|
1949
|
+
if (x1 >= x2 && x1 > axisx.max) {
|
1950
|
+
if (x2 > axisx.max)
|
1951
|
+
continue;
|
1952
|
+
y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1953
|
+
x1 = axisx.max;
|
1954
|
+
}
|
1955
|
+
else if (x2 >= x1 && x2 > axisx.max) {
|
1956
|
+
if (x1 > axisx.max)
|
1957
|
+
continue;
|
1958
|
+
y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1959
|
+
x2 = axisx.max;
|
1960
|
+
}
|
1961
|
+
|
1962
|
+
if (!areaOpen) {
|
1963
|
+
// open area
|
1964
|
+
ctx.beginPath();
|
1965
|
+
ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
|
1966
|
+
areaOpen = true;
|
1967
|
+
}
|
1968
|
+
|
1969
|
+
// now first check the case where both is outside
|
1970
|
+
if (y1 >= axisy.max && y2 >= axisy.max) {
|
1971
|
+
ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
|
1972
|
+
ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
|
1973
|
+
continue;
|
1974
|
+
}
|
1975
|
+
else if (y1 <= axisy.min && y2 <= axisy.min) {
|
1976
|
+
ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
|
1977
|
+
ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
|
1978
|
+
continue;
|
1979
|
+
}
|
1980
|
+
|
1981
|
+
// else it's a bit more complicated, there might
|
1982
|
+
// be a flat maxed out rectangle first, then a
|
1983
|
+
// triangular cutout or reverse; to find these
|
1984
|
+
// keep track of the current x values
|
1985
|
+
var x1old = x1, x2old = x2;
|
1986
|
+
|
1987
|
+
// clip the y values, without shortcutting, we
|
1988
|
+
// go through all cases in turn
|
1989
|
+
|
1990
|
+
// clip with ymin
|
1991
|
+
if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
|
1992
|
+
x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
|
1993
|
+
y1 = axisy.min;
|
1994
|
+
}
|
1995
|
+
else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
|
1996
|
+
x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
|
1997
|
+
y2 = axisy.min;
|
1998
|
+
}
|
1999
|
+
|
2000
|
+
// clip with ymax
|
2001
|
+
if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
|
2002
|
+
x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
|
2003
|
+
y1 = axisy.max;
|
2004
|
+
}
|
2005
|
+
else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
|
2006
|
+
x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
|
2007
|
+
y2 = axisy.max;
|
2008
|
+
}
|
2009
|
+
|
2010
|
+
// if the x value was changed we got a rectangle
|
2011
|
+
// to fill
|
2012
|
+
if (x1 != x1old) {
|
2013
|
+
ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
|
2014
|
+
// it goes to (x1, y1), but we fill that below
|
2015
|
+
}
|
2016
|
+
|
2017
|
+
// fill triangular section, this sometimes result
|
2018
|
+
// in redundant points if (x1, y1) hasn't changed
|
2019
|
+
// from previous line to, but we just ignore that
|
2020
|
+
ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
|
2021
|
+
ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
|
2022
|
+
|
2023
|
+
// fill the other rectangle if it's there
|
2024
|
+
if (x2 != x2old) {
|
2025
|
+
ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
|
2026
|
+
ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
|
2027
|
+
}
|
2028
|
+
}
|
2029
|
+
}
|
2030
|
+
|
2031
|
+
ctx.save();
|
2032
|
+
ctx.translate(plotOffset.left, plotOffset.top);
|
2033
|
+
ctx.lineJoin = "round";
|
2034
|
+
|
2035
|
+
var lw = series.lines.lineWidth,
|
2036
|
+
sw = series.shadowSize;
|
2037
|
+
// FIXME: consider another form of shadow when filling is turned on
|
2038
|
+
if (lw > 0 && sw > 0) {
|
2039
|
+
// draw shadow as a thick and thin line with transparency
|
2040
|
+
ctx.lineWidth = sw;
|
2041
|
+
ctx.strokeStyle = "rgba(0,0,0,0.1)";
|
2042
|
+
// position shadow at angle from the mid of line
|
2043
|
+
var angle = Math.PI/18;
|
2044
|
+
plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
|
2045
|
+
ctx.lineWidth = sw/2;
|
2046
|
+
plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
|
2047
|
+
}
|
2048
|
+
|
2049
|
+
ctx.lineWidth = lw;
|
2050
|
+
ctx.strokeStyle = series.color;
|
2051
|
+
var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
|
2052
|
+
if (fillStyle) {
|
2053
|
+
ctx.fillStyle = fillStyle;
|
2054
|
+
plotLineArea(series.datapoints, series.xaxis, series.yaxis);
|
2055
|
+
}
|
2056
|
+
|
2057
|
+
if (lw > 0)
|
2058
|
+
plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
|
2059
|
+
ctx.restore();
|
2060
|
+
}
|
2061
|
+
|
2062
|
+
function drawSeriesPoints(series) {
|
2063
|
+
function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {
|
2064
|
+
var points = datapoints.points, ps = datapoints.pointsize;
|
2065
|
+
|
2066
|
+
for (var i = 0; i < points.length; i += ps) {
|
2067
|
+
var x = points[i], y = points[i + 1];
|
2068
|
+
if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
|
2069
|
+
continue;
|
2070
|
+
|
2071
|
+
ctx.beginPath();
|
2072
|
+
x = axisx.p2c(x);
|
2073
|
+
y = axisy.p2c(y) + offset;
|
2074
|
+
if (symbol == "circle")
|
2075
|
+
ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
|
2076
|
+
else
|
2077
|
+
symbol(ctx, x, y, radius, shadow);
|
2078
|
+
ctx.closePath();
|
2079
|
+
|
2080
|
+
if (fillStyle) {
|
2081
|
+
ctx.fillStyle = fillStyle;
|
2082
|
+
ctx.fill();
|
2083
|
+
}
|
2084
|
+
ctx.stroke();
|
2085
|
+
}
|
2086
|
+
}
|
2087
|
+
|
2088
|
+
ctx.save();
|
2089
|
+
ctx.translate(plotOffset.left, plotOffset.top);
|
2090
|
+
|
2091
|
+
var lw = series.points.lineWidth,
|
2092
|
+
sw = series.shadowSize,
|
2093
|
+
radius = series.points.radius,
|
2094
|
+
symbol = series.points.symbol;
|
2095
|
+
if (lw > 0 && sw > 0) {
|
2096
|
+
// draw shadow in two steps
|
2097
|
+
var w = sw / 2;
|
2098
|
+
ctx.lineWidth = w;
|
2099
|
+
ctx.strokeStyle = "rgba(0,0,0,0.1)";
|
2100
|
+
plotPoints(series.datapoints, radius, null, w + w/2, true,
|
2101
|
+
series.xaxis, series.yaxis, symbol);
|
2102
|
+
|
2103
|
+
ctx.strokeStyle = "rgba(0,0,0,0.2)";
|
2104
|
+
plotPoints(series.datapoints, radius, null, w/2, true,
|
2105
|
+
series.xaxis, series.yaxis, symbol);
|
2106
|
+
}
|
2107
|
+
|
2108
|
+
ctx.lineWidth = lw;
|
2109
|
+
ctx.strokeStyle = series.color;
|
2110
|
+
plotPoints(series.datapoints, radius,
|
2111
|
+
getFillStyle(series.points, series.color), 0, false,
|
2112
|
+
series.xaxis, series.yaxis, symbol);
|
2113
|
+
ctx.restore();
|
2114
|
+
}
|
2115
|
+
|
2116
|
+
function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
|
2117
|
+
var left, right, bottom, top,
|
2118
|
+
drawLeft, drawRight, drawTop, drawBottom,
|
2119
|
+
tmp;
|
2120
|
+
|
2121
|
+
// in horizontal mode, we start the bar from the left
|
2122
|
+
// instead of from the bottom so it appears to be
|
2123
|
+
// horizontal rather than vertical
|
2124
|
+
if (horizontal) {
|
2125
|
+
drawBottom = drawRight = drawTop = true;
|
2126
|
+
drawLeft = false;
|
2127
|
+
left = b;
|
2128
|
+
right = x;
|
2129
|
+
top = y + barLeft;
|
2130
|
+
bottom = y + barRight;
|
2131
|
+
|
2132
|
+
// account for negative bars
|
2133
|
+
if (right < left) {
|
2134
|
+
tmp = right;
|
2135
|
+
right = left;
|
2136
|
+
left = tmp;
|
2137
|
+
drawLeft = true;
|
2138
|
+
drawRight = false;
|
2139
|
+
}
|
2140
|
+
}
|
2141
|
+
else {
|
2142
|
+
drawLeft = drawRight = drawTop = true;
|
2143
|
+
drawBottom = false;
|
2144
|
+
left = x + barLeft;
|
2145
|
+
right = x + barRight;
|
2146
|
+
bottom = b;
|
2147
|
+
top = y;
|
2148
|
+
|
2149
|
+
// account for negative bars
|
2150
|
+
if (top < bottom) {
|
2151
|
+
tmp = top;
|
2152
|
+
top = bottom;
|
2153
|
+
bottom = tmp;
|
2154
|
+
drawBottom = true;
|
2155
|
+
drawTop = false;
|
2156
|
+
}
|
2157
|
+
}
|
2158
|
+
|
2159
|
+
// clip
|
2160
|
+
if (right < axisx.min || left > axisx.max ||
|
2161
|
+
top < axisy.min || bottom > axisy.max)
|
2162
|
+
return;
|
2163
|
+
|
2164
|
+
if (left < axisx.min) {
|
2165
|
+
left = axisx.min;
|
2166
|
+
drawLeft = false;
|
2167
|
+
}
|
2168
|
+
|
2169
|
+
if (right > axisx.max) {
|
2170
|
+
right = axisx.max;
|
2171
|
+
drawRight = false;
|
2172
|
+
}
|
2173
|
+
|
2174
|
+
if (bottom < axisy.min) {
|
2175
|
+
bottom = axisy.min;
|
2176
|
+
drawBottom = false;
|
2177
|
+
}
|
2178
|
+
|
2179
|
+
if (top > axisy.max) {
|
2180
|
+
top = axisy.max;
|
2181
|
+
drawTop = false;
|
2182
|
+
}
|
2183
|
+
|
2184
|
+
left = axisx.p2c(left);
|
2185
|
+
bottom = axisy.p2c(bottom);
|
2186
|
+
right = axisx.p2c(right);
|
2187
|
+
top = axisy.p2c(top);
|
2188
|
+
|
2189
|
+
// fill the bar
|
2190
|
+
if (fillStyleCallback) {
|
2191
|
+
c.beginPath();
|
2192
|
+
c.moveTo(left, bottom);
|
2193
|
+
c.lineTo(left, top);
|
2194
|
+
c.lineTo(right, top);
|
2195
|
+
c.lineTo(right, bottom);
|
2196
|
+
c.fillStyle = fillStyleCallback(bottom, top);
|
2197
|
+
c.fill();
|
2198
|
+
}
|
2199
|
+
|
2200
|
+
// draw outline
|
2201
|
+
if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
|
2202
|
+
c.beginPath();
|
2203
|
+
|
2204
|
+
// FIXME: inline moveTo is buggy with excanvas
|
2205
|
+
c.moveTo(left, bottom + offset);
|
2206
|
+
if (drawLeft)
|
2207
|
+
c.lineTo(left, top + offset);
|
2208
|
+
else
|
2209
|
+
c.moveTo(left, top + offset);
|
2210
|
+
if (drawTop)
|
2211
|
+
c.lineTo(right, top + offset);
|
2212
|
+
else
|
2213
|
+
c.moveTo(right, top + offset);
|
2214
|
+
if (drawRight)
|
2215
|
+
c.lineTo(right, bottom + offset);
|
2216
|
+
else
|
2217
|
+
c.moveTo(right, bottom + offset);
|
2218
|
+
if (drawBottom)
|
2219
|
+
c.lineTo(left, bottom + offset);
|
2220
|
+
else
|
2221
|
+
c.moveTo(left, bottom + offset);
|
2222
|
+
c.stroke();
|
2223
|
+
}
|
2224
|
+
}
|
2225
|
+
|
2226
|
+
function drawSeriesBars(series) {
|
2227
|
+
function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
|
2228
|
+
var points = datapoints.points, ps = datapoints.pointsize;
|
2229
|
+
|
2230
|
+
for (var i = 0; i < points.length; i += ps) {
|
2231
|
+
if (points[i] == null)
|
2232
|
+
continue;
|
2233
|
+
drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
|
2234
|
+
}
|
2235
|
+
}
|
2236
|
+
|
2237
|
+
ctx.save();
|
2238
|
+
ctx.translate(plotOffset.left, plotOffset.top);
|
2239
|
+
|
2240
|
+
// FIXME: figure out a way to add shadows (for instance along the right edge)
|
2241
|
+
ctx.lineWidth = series.bars.lineWidth;
|
2242
|
+
ctx.strokeStyle = series.color;
|
2243
|
+
|
2244
|
+
var barLeft;
|
2245
|
+
|
2246
|
+
switch (series.bars.align) {
|
2247
|
+
case "left":
|
2248
|
+
barLeft = 0;
|
2249
|
+
break;
|
2250
|
+
case "right":
|
2251
|
+
barLeft = -series.bars.barWidth;
|
2252
|
+
break;
|
2253
|
+
case "center":
|
2254
|
+
barLeft = -series.bars.barWidth / 2;
|
2255
|
+
break;
|
2256
|
+
default:
|
2257
|
+
throw new Error("Invalid bar alignment: " + series.bars.align);
|
2258
|
+
}
|
2259
|
+
|
2260
|
+
var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
|
2261
|
+
plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
|
2262
|
+
ctx.restore();
|
2263
|
+
}
|
2264
|
+
|
2265
|
+
function getFillStyle(filloptions, seriesColor, bottom, top) {
|
2266
|
+
var fill = filloptions.fill;
|
2267
|
+
if (!fill)
|
2268
|
+
return null;
|
2269
|
+
|
2270
|
+
if (filloptions.fillColor)
|
2271
|
+
return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
|
2272
|
+
|
2273
|
+
var c = $.color.parse(seriesColor);
|
2274
|
+
c.a = typeof fill == "number" ? fill : 0.4;
|
2275
|
+
c.normalize();
|
2276
|
+
return c.toString();
|
2277
|
+
}
|
2278
|
+
|
2279
|
+
function insertLegend() {
|
2280
|
+
|
2281
|
+
placeholder.find(".legend").remove();
|
2282
|
+
|
2283
|
+
if (!options.legend.show)
|
2284
|
+
return;
|
2285
|
+
|
2286
|
+
var fragments = [], entries = [], rowStarted = false,
|
2287
|
+
lf = options.legend.labelFormatter, s, label;
|
2288
|
+
|
2289
|
+
// Build a list of legend entries, with each having a label and a color
|
2290
|
+
|
2291
|
+
for (var i = 0; i < series.length; ++i) {
|
2292
|
+
s = series[i];
|
2293
|
+
if (s.label) {
|
2294
|
+
label = lf ? lf(s.label, s) : s.label;
|
2295
|
+
if (label) {
|
2296
|
+
entries.push({
|
2297
|
+
label: label,
|
2298
|
+
color: s.color
|
2299
|
+
});
|
2300
|
+
}
|
2301
|
+
}
|
2302
|
+
}
|
2303
|
+
|
2304
|
+
// Sort the legend using either the default or a custom comparator
|
2305
|
+
|
2306
|
+
if (options.legend.sorted) {
|
2307
|
+
if ($.isFunction(options.legend.sorted)) {
|
2308
|
+
entries.sort(options.legend.sorted);
|
2309
|
+
} else {
|
2310
|
+
var ascending = options.legend.sorted != "descending";
|
2311
|
+
entries.sort(function(a, b) {
|
2312
|
+
return a.label == b.label ? 0 : (
|
2313
|
+
(a.label < b.label) != ascending ? 1 : -1 // Logical XOR
|
2314
|
+
);
|
2315
|
+
});
|
2316
|
+
}
|
2317
|
+
}
|
2318
|
+
|
2319
|
+
// Generate markup for the list of entries, in their final order
|
2320
|
+
|
2321
|
+
for (var i = 0; i < entries.length; ++i) {
|
2322
|
+
|
2323
|
+
var entry = entries[i];
|
2324
|
+
|
2325
|
+
if (i % options.legend.noColumns == 0) {
|
2326
|
+
if (rowStarted)
|
2327
|
+
fragments.push('</tr>');
|
2328
|
+
fragments.push('<tr>');
|
2329
|
+
rowStarted = true;
|
2330
|
+
}
|
2331
|
+
|
2332
|
+
fragments.push(
|
2333
|
+
'<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + entry.color + ';overflow:hidden"></div></div></td>' +
|
2334
|
+
'<td class="legendLabel">' + entry.label + '</td>'
|
2335
|
+
);
|
2336
|
+
}
|
2337
|
+
|
2338
|
+
if (rowStarted)
|
2339
|
+
fragments.push('</tr>');
|
2340
|
+
|
2341
|
+
if (fragments.length == 0)
|
2342
|
+
return;
|
2343
|
+
|
2344
|
+
var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
|
2345
|
+
if (options.legend.container != null)
|
2346
|
+
$(options.legend.container).html(table);
|
2347
|
+
else {
|
2348
|
+
var pos = "",
|
2349
|
+
p = options.legend.position,
|
2350
|
+
m = options.legend.margin;
|
2351
|
+
if (m[0] == null)
|
2352
|
+
m = [m, m];
|
2353
|
+
if (p.charAt(0) == "n")
|
2354
|
+
pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
|
2355
|
+
else if (p.charAt(0) == "s")
|
2356
|
+
pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
|
2357
|
+
if (p.charAt(1) == "e")
|
2358
|
+
pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
|
2359
|
+
else if (p.charAt(1) == "w")
|
2360
|
+
pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
|
2361
|
+
var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
|
2362
|
+
if (options.legend.backgroundOpacity != 0.0) {
|
2363
|
+
// put in the transparent background
|
2364
|
+
// separately to avoid blended labels and
|
2365
|
+
// label boxes
|
2366
|
+
var c = options.legend.backgroundColor;
|
2367
|
+
if (c == null) {
|
2368
|
+
c = options.grid.backgroundColor;
|
2369
|
+
if (c && typeof c == "string")
|
2370
|
+
c = $.color.parse(c);
|
2371
|
+
else
|
2372
|
+
c = $.color.extract(legend, 'background-color');
|
2373
|
+
c.a = 1;
|
2374
|
+
c = c.toString();
|
2375
|
+
}
|
2376
|
+
var div = legend.children();
|
2377
|
+
$('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
|
2378
|
+
}
|
2379
|
+
}
|
2380
|
+
}
|
2381
|
+
|
2382
|
+
|
2383
|
+
// interactive features
|
2384
|
+
|
2385
|
+
var highlights = [],
|
2386
|
+
redrawTimeout = null;
|
2387
|
+
|
2388
|
+
// returns the data item the mouse is over, or null if none is found
|
2389
|
+
function findNearbyItem(mouseX, mouseY, seriesFilter) {
|
2390
|
+
var maxDistance = options.grid.mouseActiveRadius,
|
2391
|
+
smallestDistance = maxDistance * maxDistance + 1,
|
2392
|
+
item = null, foundPoint = false, i, j, ps;
|
2393
|
+
|
2394
|
+
for (i = series.length - 1; i >= 0; --i) {
|
2395
|
+
if (!seriesFilter(series[i]))
|
2396
|
+
continue;
|
2397
|
+
|
2398
|
+
var s = series[i],
|
2399
|
+
axisx = s.xaxis,
|
2400
|
+
axisy = s.yaxis,
|
2401
|
+
points = s.datapoints.points,
|
2402
|
+
mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
|
2403
|
+
my = axisy.c2p(mouseY),
|
2404
|
+
maxx = maxDistance / axisx.scale,
|
2405
|
+
maxy = maxDistance / axisy.scale;
|
2406
|
+
|
2407
|
+
ps = s.datapoints.pointsize;
|
2408
|
+
// with inverse transforms, we can't use the maxx/maxy
|
2409
|
+
// optimization, sadly
|
2410
|
+
if (axisx.options.inverseTransform)
|
2411
|
+
maxx = Number.MAX_VALUE;
|
2412
|
+
if (axisy.options.inverseTransform)
|
2413
|
+
maxy = Number.MAX_VALUE;
|
2414
|
+
|
2415
|
+
if (s.lines.show || s.points.show) {
|
2416
|
+
for (j = 0; j < points.length; j += ps) {
|
2417
|
+
var x = points[j], y = points[j + 1];
|
2418
|
+
if (x == null)
|
2419
|
+
continue;
|
2420
|
+
|
2421
|
+
// For points and lines, the cursor must be within a
|
2422
|
+
// certain distance to the data point
|
2423
|
+
if (x - mx > maxx || x - mx < -maxx ||
|
2424
|
+
y - my > maxy || y - my < -maxy)
|
2425
|
+
continue;
|
2426
|
+
|
2427
|
+
// We have to calculate distances in pixels, not in
|
2428
|
+
// data units, because the scales of the axes may be different
|
2429
|
+
var dx = Math.abs(axisx.p2c(x) - mouseX),
|
2430
|
+
dy = Math.abs(axisy.p2c(y) - mouseY),
|
2431
|
+
dist = dx * dx + dy * dy; // we save the sqrt
|
2432
|
+
|
2433
|
+
// use <= to ensure last point takes precedence
|
2434
|
+
// (last generally means on top of)
|
2435
|
+
if (dist < smallestDistance) {
|
2436
|
+
smallestDistance = dist;
|
2437
|
+
item = [i, j / ps];
|
2438
|
+
}
|
2439
|
+
}
|
2440
|
+
}
|
2441
|
+
|
2442
|
+
if (s.bars.show && !item) { // no other point can be nearby
|
2443
|
+
var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
|
2444
|
+
barRight = barLeft + s.bars.barWidth;
|
2445
|
+
|
2446
|
+
for (j = 0; j < points.length; j += ps) {
|
2447
|
+
var x = points[j], y = points[j + 1], b = points[j + 2];
|
2448
|
+
if (x == null)
|
2449
|
+
continue;
|
2450
|
+
|
2451
|
+
// for a bar graph, the cursor must be inside the bar
|
2452
|
+
if (series[i].bars.horizontal ?
|
2453
|
+
(mx <= Math.max(b, x) && mx >= Math.min(b, x) &&
|
2454
|
+
my >= y + barLeft && my <= y + barRight) :
|
2455
|
+
(mx >= x + barLeft && mx <= x + barRight &&
|
2456
|
+
my >= Math.min(b, y) && my <= Math.max(b, y)))
|
2457
|
+
item = [i, j / ps];
|
2458
|
+
}
|
2459
|
+
}
|
2460
|
+
}
|
2461
|
+
|
2462
|
+
if (item) {
|
2463
|
+
i = item[0];
|
2464
|
+
j = item[1];
|
2465
|
+
ps = series[i].datapoints.pointsize;
|
2466
|
+
|
2467
|
+
return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
|
2468
|
+
dataIndex: j,
|
2469
|
+
series: series[i],
|
2470
|
+
seriesIndex: i };
|
2471
|
+
}
|
2472
|
+
|
2473
|
+
return null;
|
2474
|
+
}
|
2475
|
+
|
2476
|
+
function onMouseMove(e) {
|
2477
|
+
if (options.grid.hoverable)
|
2478
|
+
triggerClickHoverEvent("plothover", e,
|
2479
|
+
function (s) { return s["hoverable"] != false; });
|
2480
|
+
}
|
2481
|
+
|
2482
|
+
function onMouseLeave(e) {
|
2483
|
+
if (options.grid.hoverable)
|
2484
|
+
triggerClickHoverEvent("plothover", e,
|
2485
|
+
function (s) { return false; });
|
2486
|
+
}
|
2487
|
+
|
2488
|
+
function onClick(e) {
|
2489
|
+
triggerClickHoverEvent("plotclick", e,
|
2490
|
+
function (s) { return s["clickable"] != false; });
|
2491
|
+
}
|
2492
|
+
|
2493
|
+
// trigger click or hover event (they send the same parameters
|
2494
|
+
// so we share their code)
|
2495
|
+
function triggerClickHoverEvent(eventname, event, seriesFilter) {
|
2496
|
+
var offset = eventHolder.offset(),
|
2497
|
+
canvasX = event.pageX - offset.left - plotOffset.left,
|
2498
|
+
canvasY = event.pageY - offset.top - plotOffset.top,
|
2499
|
+
pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
|
2500
|
+
|
2501
|
+
pos.pageX = event.pageX;
|
2502
|
+
pos.pageY = event.pageY;
|
2503
|
+
|
2504
|
+
var item = findNearbyItem(canvasX, canvasY, seriesFilter);
|
2505
|
+
|
2506
|
+
if (item) {
|
2507
|
+
// fill in mouse pos for any listeners out there
|
2508
|
+
item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10);
|
2509
|
+
item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10);
|
2510
|
+
}
|
2511
|
+
|
2512
|
+
if (options.grid.autoHighlight) {
|
2513
|
+
// clear auto-highlights
|
2514
|
+
for (var i = 0; i < highlights.length; ++i) {
|
2515
|
+
var h = highlights[i];
|
2516
|
+
if (h.auto == eventname &&
|
2517
|
+
!(item && h.series == item.series &&
|
2518
|
+
h.point[0] == item.datapoint[0] &&
|
2519
|
+
h.point[1] == item.datapoint[1]))
|
2520
|
+
unhighlight(h.series, h.point);
|
2521
|
+
}
|
2522
|
+
|
2523
|
+
if (item)
|
2524
|
+
highlight(item.series, item.datapoint, eventname);
|
2525
|
+
}
|
2526
|
+
|
2527
|
+
placeholder.trigger(eventname, [ pos, item ]);
|
2528
|
+
}
|
2529
|
+
|
2530
|
+
function triggerRedrawOverlay() {
|
2531
|
+
var t = options.interaction.redrawOverlayInterval;
|
2532
|
+
if (t == -1) { // skip event queue
|
2533
|
+
drawOverlay();
|
2534
|
+
return;
|
2535
|
+
}
|
2536
|
+
|
2537
|
+
if (!redrawTimeout)
|
2538
|
+
redrawTimeout = setTimeout(drawOverlay, t);
|
2539
|
+
}
|
2540
|
+
|
2541
|
+
function drawOverlay() {
|
2542
|
+
redrawTimeout = null;
|
2543
|
+
|
2544
|
+
// draw highlights
|
2545
|
+
octx.save();
|
2546
|
+
octx.clearRect(0, 0, canvasWidth, canvasHeight);
|
2547
|
+
octx.translate(plotOffset.left, plotOffset.top);
|
2548
|
+
|
2549
|
+
var i, hi;
|
2550
|
+
for (i = 0; i < highlights.length; ++i) {
|
2551
|
+
hi = highlights[i];
|
2552
|
+
|
2553
|
+
if (hi.series.bars.show)
|
2554
|
+
drawBarHighlight(hi.series, hi.point);
|
2555
|
+
else
|
2556
|
+
drawPointHighlight(hi.series, hi.point);
|
2557
|
+
}
|
2558
|
+
octx.restore();
|
2559
|
+
|
2560
|
+
executeHooks(hooks.drawOverlay, [octx]);
|
2561
|
+
}
|
2562
|
+
|
2563
|
+
function highlight(s, point, auto) {
|
2564
|
+
if (typeof s == "number")
|
2565
|
+
s = series[s];
|
2566
|
+
|
2567
|
+
if (typeof point == "number") {
|
2568
|
+
var ps = s.datapoints.pointsize;
|
2569
|
+
point = s.datapoints.points.slice(ps * point, ps * (point + 1));
|
2570
|
+
}
|
2571
|
+
|
2572
|
+
var i = indexOfHighlight(s, point);
|
2573
|
+
if (i == -1) {
|
2574
|
+
highlights.push({ series: s, point: point, auto: auto });
|
2575
|
+
|
2576
|
+
triggerRedrawOverlay();
|
2577
|
+
}
|
2578
|
+
else if (!auto)
|
2579
|
+
highlights[i].auto = false;
|
2580
|
+
}
|
2581
|
+
|
2582
|
+
function unhighlight(s, point) {
|
2583
|
+
if (s == null && point == null) {
|
2584
|
+
highlights = [];
|
2585
|
+
triggerRedrawOverlay();
|
2586
|
+
}
|
2587
|
+
|
2588
|
+
if (typeof s == "number")
|
2589
|
+
s = series[s];
|
2590
|
+
|
2591
|
+
if (typeof point == "number")
|
2592
|
+
point = s.data[point];
|
2593
|
+
|
2594
|
+
var i = indexOfHighlight(s, point);
|
2595
|
+
if (i != -1) {
|
2596
|
+
highlights.splice(i, 1);
|
2597
|
+
|
2598
|
+
triggerRedrawOverlay();
|
2599
|
+
}
|
2600
|
+
}
|
2601
|
+
|
2602
|
+
function indexOfHighlight(s, p) {
|
2603
|
+
for (var i = 0; i < highlights.length; ++i) {
|
2604
|
+
var h = highlights[i];
|
2605
|
+
if (h.series == s && h.point[0] == p[0]
|
2606
|
+
&& h.point[1] == p[1])
|
2607
|
+
return i;
|
2608
|
+
}
|
2609
|
+
return -1;
|
2610
|
+
}
|
2611
|
+
|
2612
|
+
function drawPointHighlight(series, point) {
|
2613
|
+
var x = point[0], y = point[1],
|
2614
|
+
axisx = series.xaxis, axisy = series.yaxis,
|
2615
|
+
highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString();
|
2616
|
+
|
2617
|
+
if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
|
2618
|
+
return;
|
2619
|
+
|
2620
|
+
var pointRadius = series.points.radius + series.points.lineWidth / 2;
|
2621
|
+
octx.lineWidth = pointRadius;
|
2622
|
+
octx.strokeStyle = highlightColor;
|
2623
|
+
var radius = 1.5 * pointRadius;
|
2624
|
+
x = axisx.p2c(x);
|
2625
|
+
y = axisy.p2c(y);
|
2626
|
+
|
2627
|
+
octx.beginPath();
|
2628
|
+
if (series.points.symbol == "circle")
|
2629
|
+
octx.arc(x, y, radius, 0, 2 * Math.PI, false);
|
2630
|
+
else
|
2631
|
+
series.points.symbol(octx, x, y, radius, false);
|
2632
|
+
octx.closePath();
|
2633
|
+
octx.stroke();
|
2634
|
+
}
|
2635
|
+
|
2636
|
+
function drawBarHighlight(series, point) {
|
2637
|
+
var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(),
|
2638
|
+
fillStyle = highlightColor,
|
2639
|
+
barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
|
2640
|
+
|
2641
|
+
octx.lineWidth = series.bars.lineWidth;
|
2642
|
+
octx.strokeStyle = highlightColor;
|
2643
|
+
|
2644
|
+
drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
|
2645
|
+
0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
|
2646
|
+
}
|
2647
|
+
|
2648
|
+
function getColorOrGradient(spec, bottom, top, defaultColor) {
|
2649
|
+
if (typeof spec == "string")
|
2650
|
+
return spec;
|
2651
|
+
else {
|
2652
|
+
// assume this is a gradient spec; IE currently only
|
2653
|
+
// supports a simple vertical gradient properly, so that's
|
2654
|
+
// what we support too
|
2655
|
+
var gradient = ctx.createLinearGradient(0, top, 0, bottom);
|
2656
|
+
|
2657
|
+
for (var i = 0, l = spec.colors.length; i < l; ++i) {
|
2658
|
+
var c = spec.colors[i];
|
2659
|
+
if (typeof c != "string") {
|
2660
|
+
var co = $.color.parse(defaultColor);
|
2661
|
+
if (c.brightness != null)
|
2662
|
+
co = co.scale('rgb', c.brightness);
|
2663
|
+
if (c.opacity != null)
|
2664
|
+
co.a *= c.opacity;
|
2665
|
+
c = co.toString();
|
2666
|
+
}
|
2667
|
+
gradient.addColorStop(i / (l - 1), c);
|
2668
|
+
}
|
2669
|
+
|
2670
|
+
return gradient;
|
2671
|
+
}
|
2672
|
+
}
|
2673
|
+
}
|
2674
|
+
|
2675
|
+
$.plot = function(placeholder, data, options) {
|
2676
|
+
//var t0 = new Date();
|
2677
|
+
var plot = new Plot($(placeholder), data, options, $.plot.plugins);
|
2678
|
+
//(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
|
2679
|
+
return plot;
|
2680
|
+
};
|
2681
|
+
|
2682
|
+
$.plot.version = "0.8-alpha";
|
2683
|
+
|
2684
|
+
$.plot.plugins = [];
|
2685
|
+
|
2686
|
+
// round to nearby lower multiple of base
|
2687
|
+
function floorInBase(n, base) {
|
2688
|
+
return base * Math.floor(n / base);
|
2689
|
+
}
|
2690
|
+
|
2691
|
+
})(jQuery);
|