|
1 /* Javascript plotting library for jQuery, version 3.0.0. |
|
2 |
|
3 Copyright (c) 2007-2014 IOLA and Ole Laursen. |
|
4 Licensed under the MIT license. |
|
5 |
|
6 */ |
|
7 |
|
8 // the actual Flot code |
|
9 (function($) { |
|
10 "use strict"; |
|
11 |
|
12 var Canvas = window.Flot.Canvas; |
|
13 |
|
14 function defaultTickGenerator(axis) { |
|
15 var ticks = [], |
|
16 start = $.plot.saturated.saturate($.plot.saturated.floorInBase(axis.min, axis.tickSize)), |
|
17 i = 0, |
|
18 v = Number.NaN, |
|
19 prev; |
|
20 |
|
21 if (start === -Number.MAX_VALUE) { |
|
22 ticks.push(start); |
|
23 start = $.plot.saturated.floorInBase(axis.min + axis.tickSize, axis.tickSize); |
|
24 } |
|
25 |
|
26 do { |
|
27 prev = v; |
|
28 //v = start + i * axis.tickSize; |
|
29 v = $.plot.saturated.multiplyAdd(axis.tickSize, i, start); |
|
30 ticks.push(v); |
|
31 ++i; |
|
32 } while (v < axis.max && v !== prev); |
|
33 |
|
34 return ticks; |
|
35 } |
|
36 |
|
37 function defaultTickFormatter(value, axis, precision) { |
|
38 var oldTickDecimals = axis.tickDecimals, |
|
39 expPosition = ("" + value).indexOf("e"); |
|
40 |
|
41 if (expPosition !== -1) { |
|
42 return expRepTickFormatter(value, axis, precision); |
|
43 } |
|
44 |
|
45 if (precision > 0) { |
|
46 axis.tickDecimals = precision; |
|
47 } |
|
48 |
|
49 var factor = axis.tickDecimals ? parseFloat('1e' + axis.tickDecimals) : 1, |
|
50 formatted = "" + Math.round(value * factor) / factor; |
|
51 |
|
52 // If tickDecimals was specified, ensure that we have exactly that |
|
53 // much precision; otherwise default to the value's own precision. |
|
54 if (axis.tickDecimals != null) { |
|
55 var decimal = formatted.indexOf("."), |
|
56 decimalPrecision = decimal === -1 ? 0 : formatted.length - decimal - 1; |
|
57 if (decimalPrecision < axis.tickDecimals) { |
|
58 var decimals = ("" + factor).substr(1, axis.tickDecimals - decimalPrecision); |
|
59 formatted = (decimalPrecision ? formatted : formatted + ".") + decimals; |
|
60 } |
|
61 } |
|
62 |
|
63 axis.tickDecimals = oldTickDecimals; |
|
64 return formatted; |
|
65 }; |
|
66 |
|
67 function expRepTickFormatter(value, axis, precision) { |
|
68 var expPosition = ("" + value).indexOf("e"), |
|
69 exponentValue = parseInt(("" + value).substr(expPosition + 1)), |
|
70 tenExponent = expPosition !== -1 ? exponentValue : (value > 0 ? Math.floor(Math.log(value) / Math.LN10) : 0), |
|
71 roundWith = parseFloat('1e' + tenExponent), |
|
72 x = value / roundWith; |
|
73 |
|
74 if (precision) { |
|
75 var updatedPrecision = recomputePrecision(value, precision); |
|
76 return (value / roundWith).toFixed(updatedPrecision) + 'e' + tenExponent; |
|
77 } |
|
78 |
|
79 if (axis.tickDecimals > 0) { |
|
80 return x.toFixed(recomputePrecision(value, axis.tickDecimals)) + 'e' + tenExponent; |
|
81 } |
|
82 return x.toFixed() + 'e' + tenExponent; |
|
83 } |
|
84 |
|
85 function recomputePrecision(num, precision) { |
|
86 //for numbers close to zero, the precision from flot will be a big number |
|
87 //while for big numbers, the precision will be negative |
|
88 var log10Value = Math.log(Math.abs(num)) * Math.LOG10E, |
|
89 newPrecision = Math.abs(log10Value + precision); |
|
90 |
|
91 return newPrecision <= 20 ? Math.floor(newPrecision) : 20; |
|
92 } |
|
93 |
|
94 /////////////////////////////////////////////////////////////////////////// |
|
95 // The top-level container for the entire plot. |
|
96 function Plot(placeholder, data_, options_, plugins) { |
|
97 // data is on the form: |
|
98 // [ series1, series2 ... ] |
|
99 // where series is either just the data as [ [x1, y1], [x2, y2], ... ] |
|
100 // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } |
|
101 |
|
102 var series = [], |
|
103 options = { |
|
104 // the color theme used for graphs |
|
105 colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], |
|
106 xaxis: { |
|
107 show: null, // null = auto-detect, true = always, false = never |
|
108 position: "bottom", // or "top" |
|
109 mode: null, // null or "time" |
|
110 font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } |
|
111 color: null, // base color, labels, ticks |
|
112 tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" |
|
113 transform: null, // null or f: number -> number to transform axis |
|
114 inverseTransform: null, // if transform is set, this should be the inverse function |
|
115 min: null, // min. value to show, null means set automatically |
|
116 max: null, // max. value to show, null means set automatically |
|
117 autoScaleMargin: null, // margin in % to add if autoScale option is on "loose" mode, |
|
118 autoScale: "exact", // Available modes: "none", "loose", "exact", "sliding-window" |
|
119 windowSize: null, // null or number. This is the size of sliding-window. |
|
120 growOnly: null, // grow only, useful for smoother auto-scale, the scales will grow to accomodate data but won't shrink back. |
|
121 ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks |
|
122 tickFormatter: null, // fn: number -> string |
|
123 showTickLabels: "major", // "none", "endpoints", "major", "all" |
|
124 labelWidth: null, // size of tick labels in pixels |
|
125 labelHeight: null, |
|
126 reserveSpace: null, // whether to reserve space even if axis isn't shown |
|
127 tickLength: null, // size in pixels of major tick marks |
|
128 showMinorTicks: null, // true = show minor tick marks, false = hide minor tick marks |
|
129 showTicks: null, // true = show tick marks, false = hide all tick marks |
|
130 gridLines: null, // true = show grid lines, false = hide grid lines |
|
131 alignTicksWithAxis: null, // axis number or null for no sync |
|
132 tickDecimals: null, // no. of decimals, null means auto |
|
133 tickSize: null, // number or [number, "unit"] |
|
134 minTickSize: null, // number or [number, "unit"] |
|
135 offset: { below: 0, above: 0 }, // the plot drawing offset. this is calculated by the flot.navigate for each axis |
|
136 boxPosition: { centerX: 0, centerY: 0 } //position of the axis on the corresponding axis box |
|
137 }, |
|
138 yaxis: { |
|
139 autoScaleMargin: 0.02, // margin in % to add if autoScale option is on "loose" mode |
|
140 autoScale: "loose", // Available modes: "none", "loose", "exact" |
|
141 growOnly: null, // grow only, useful for smoother auto-scale, the scales will grow to accomodate data but won't shrink back. |
|
142 position: "left", // or "right" |
|
143 showTickLabels: "major", // "none", "endpoints", "major", "all" |
|
144 offset: { below: 0, above: 0 }, // the plot drawing offset. this is calculated by the flot.navigate for each axis |
|
145 boxPosition: { centerX: 0, centerY: 0 } //position of the axis on the corresponding axis box |
|
146 }, |
|
147 xaxes: [], |
|
148 yaxes: [], |
|
149 series: { |
|
150 points: { |
|
151 show: false, |
|
152 radius: 3, |
|
153 lineWidth: 2, // in pixels |
|
154 fill: true, |
|
155 fillColor: "#ffffff", |
|
156 symbol: 'circle' // or callback |
|
157 }, |
|
158 lines: { |
|
159 // we don't put in show: false so we can see |
|
160 // whether lines were actively disabled |
|
161 lineWidth: 1, // in pixels |
|
162 fill: false, |
|
163 fillColor: null, |
|
164 steps: false |
|
165 // Omit 'zero', so we can later default its value to |
|
166 // match that of the 'fill' option. |
|
167 }, |
|
168 bars: { |
|
169 show: false, |
|
170 lineWidth: 2, // in pixels |
|
171 // barWidth: number or [number, absolute] |
|
172 // when 'absolute' is false, 'number' is relative to the minimum distance between points for the series |
|
173 // when 'absolute' is true, 'number' is considered to be in units of the x-axis |
|
174 horizontal: false, |
|
175 barWidth: 0.8, |
|
176 fill: true, |
|
177 fillColor: null, |
|
178 align: "left", // "left", "right", or "center" |
|
179 zero: true |
|
180 }, |
|
181 shadowSize: 3, |
|
182 highlightColor: null |
|
183 }, |
|
184 grid: { |
|
185 show: true, |
|
186 aboveData: false, |
|
187 color: "#545454", // primary color used for outline and labels |
|
188 backgroundColor: null, // null for transparent, else color |
|
189 borderColor: null, // set if different from the grid color |
|
190 tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" |
|
191 margin: 0, // distance from the canvas edge to the grid |
|
192 labelMargin: 5, // in pixels |
|
193 axisMargin: 8, // in pixels |
|
194 borderWidth: 1, // in pixels |
|
195 minBorderMargin: null, // in pixels, null means taken from points radius |
|
196 markings: null, // array of ranges or fn: axes -> array of ranges |
|
197 markingsColor: "#f4f4f4", |
|
198 markingsLineWidth: 2, |
|
199 // interactive stuff |
|
200 clickable: false, |
|
201 hoverable: false, |
|
202 autoHighlight: true, // highlight in case mouse is near |
|
203 mouseActiveRadius: 15 // how far the mouse can be away to activate an item |
|
204 }, |
|
205 interaction: { |
|
206 redrawOverlayInterval: 1000 / 60 // time between updates, -1 means in same flow |
|
207 }, |
|
208 hooks: {} |
|
209 }, |
|
210 surface = null, // the canvas for the plot itself |
|
211 overlay = null, // canvas for interactive stuff on top of plot |
|
212 eventHolder = null, // jQuery object that events should be bound to |
|
213 ctx = null, |
|
214 octx = null, |
|
215 xaxes = [], |
|
216 yaxes = [], |
|
217 plotOffset = { |
|
218 left: 0, |
|
219 right: 0, |
|
220 top: 0, |
|
221 bottom: 0 |
|
222 }, |
|
223 plotWidth = 0, |
|
224 plotHeight = 0, |
|
225 hooks = { |
|
226 processOptions: [], |
|
227 processRawData: [], |
|
228 processDatapoints: [], |
|
229 processOffset: [], |
|
230 setupGrid: [], |
|
231 adjustSeriesDataRange: [], |
|
232 setRange: [], |
|
233 drawBackground: [], |
|
234 drawSeries: [], |
|
235 drawAxis: [], |
|
236 draw: [], |
|
237 axisReserveSpace: [], |
|
238 bindEvents: [], |
|
239 drawOverlay: [], |
|
240 resize: [], |
|
241 shutdown: [] |
|
242 }, |
|
243 plot = this; |
|
244 |
|
245 var eventManager = {}; |
|
246 |
|
247 // interactive features |
|
248 |
|
249 var redrawTimeout = null; |
|
250 |
|
251 // public functions |
|
252 plot.setData = setData; |
|
253 plot.setupGrid = setupGrid; |
|
254 plot.draw = draw; |
|
255 plot.getPlaceholder = function() { |
|
256 return placeholder; |
|
257 }; |
|
258 plot.getCanvas = function() { |
|
259 return surface.element; |
|
260 }; |
|
261 plot.getSurface = function() { |
|
262 return surface; |
|
263 }; |
|
264 plot.getEventHolder = function() { |
|
265 return eventHolder[0]; |
|
266 }; |
|
267 plot.getPlotOffset = function() { |
|
268 return plotOffset; |
|
269 }; |
|
270 plot.width = function() { |
|
271 return plotWidth; |
|
272 }; |
|
273 plot.height = function() { |
|
274 return plotHeight; |
|
275 }; |
|
276 plot.offset = function() { |
|
277 var o = eventHolder.offset(); |
|
278 o.left += plotOffset.left; |
|
279 o.top += plotOffset.top; |
|
280 return o; |
|
281 }; |
|
282 plot.getData = function() { |
|
283 return series; |
|
284 }; |
|
285 plot.getAxes = function() { |
|
286 var res = {}; |
|
287 $.each(xaxes.concat(yaxes), function(_, axis) { |
|
288 if (axis) { |
|
289 res[axis.direction + (axis.n !== 1 ? axis.n : "") + "axis"] = axis; |
|
290 } |
|
291 }); |
|
292 return res; |
|
293 }; |
|
294 plot.getXAxes = function() { |
|
295 return xaxes; |
|
296 }; |
|
297 plot.getYAxes = function() { |
|
298 return yaxes; |
|
299 }; |
|
300 plot.c2p = canvasToCartesianAxisCoords; |
|
301 plot.p2c = cartesianAxisToCanvasCoords; |
|
302 plot.getOptions = function() { |
|
303 return options; |
|
304 }; |
|
305 plot.triggerRedrawOverlay = triggerRedrawOverlay; |
|
306 plot.pointOffset = function(point) { |
|
307 return { |
|
308 left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), |
|
309 top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) |
|
310 }; |
|
311 }; |
|
312 plot.shutdown = shutdown; |
|
313 plot.destroy = function() { |
|
314 shutdown(); |
|
315 placeholder.removeData("plot").empty(); |
|
316 |
|
317 series = []; |
|
318 options = null; |
|
319 surface = null; |
|
320 overlay = null; |
|
321 eventHolder = null; |
|
322 ctx = null; |
|
323 octx = null; |
|
324 xaxes = []; |
|
325 yaxes = []; |
|
326 hooks = null; |
|
327 plot = null; |
|
328 }; |
|
329 |
|
330 plot.resize = function() { |
|
331 var width = placeholder.width(), |
|
332 height = placeholder.height(); |
|
333 surface.resize(width, height); |
|
334 overlay.resize(width, height); |
|
335 |
|
336 executeHooks(hooks.resize, [width, height]); |
|
337 }; |
|
338 |
|
339 plot.clearTextCache = function () { |
|
340 surface.clearCache(); |
|
341 overlay.clearCache(); |
|
342 }; |
|
343 |
|
344 plot.autoScaleAxis = autoScaleAxis; |
|
345 plot.computeRangeForDataSeries = computeRangeForDataSeries; |
|
346 plot.adjustSeriesDataRange = adjustSeriesDataRange; |
|
347 plot.findNearbyItem = findNearbyItem; |
|
348 plot.findNearbyInterpolationPoint = findNearbyInterpolationPoint; |
|
349 plot.computeValuePrecision = computeValuePrecision; |
|
350 plot.computeTickSize = computeTickSize; |
|
351 plot.addEventHandler = addEventHandler; |
|
352 |
|
353 // public attributes |
|
354 plot.hooks = hooks; |
|
355 |
|
356 // initialize |
|
357 var MINOR_TICKS_COUNT_CONSTANT = $.plot.uiConstants.MINOR_TICKS_COUNT_CONSTANT; |
|
358 var TICK_LENGTH_CONSTANT = $.plot.uiConstants.TICK_LENGTH_CONSTANT; |
|
359 initPlugins(plot); |
|
360 setupCanvases(); |
|
361 parseOptions(options_); |
|
362 setData(data_); |
|
363 setupGrid(true); |
|
364 draw(); |
|
365 bindEvents(); |
|
366 |
|
367 function executeHooks(hook, args) { |
|
368 args = [plot].concat(args); |
|
369 for (var i = 0; i < hook.length; ++i) { |
|
370 hook[i].apply(this, args); |
|
371 } |
|
372 } |
|
373 |
|
374 function initPlugins() { |
|
375 // References to key classes, allowing plugins to modify them |
|
376 |
|
377 var classes = { |
|
378 Canvas: Canvas |
|
379 }; |
|
380 |
|
381 for (var i = 0; i < plugins.length; ++i) { |
|
382 var p = plugins[i]; |
|
383 p.init(plot, classes); |
|
384 if (p.options) { |
|
385 $.extend(true, options, p.options); |
|
386 } |
|
387 } |
|
388 } |
|
389 |
|
390 function parseOptions(opts) { |
|
391 $.extend(true, options, opts); |
|
392 |
|
393 // $.extend merges arrays, rather than replacing them. When less |
|
394 // colors are provided than the size of the default palette, we |
|
395 // end up with those colors plus the remaining defaults, which is |
|
396 // not expected behavior; avoid it by replacing them here. |
|
397 |
|
398 if (opts && opts.colors) { |
|
399 options.colors = opts.colors; |
|
400 } |
|
401 |
|
402 if (options.xaxis.color == null) { |
|
403 options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); |
|
404 } |
|
405 |
|
406 if (options.yaxis.color == null) { |
|
407 options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); |
|
408 } |
|
409 |
|
410 if (options.xaxis.tickColor == null) { |
|
411 // grid.tickColor for back-compatibility |
|
412 options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; |
|
413 } |
|
414 |
|
415 if (options.yaxis.tickColor == null) { |
|
416 // grid.tickColor for back-compatibility |
|
417 options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; |
|
418 } |
|
419 |
|
420 if (options.grid.borderColor == null) { |
|
421 options.grid.borderColor = options.grid.color; |
|
422 } |
|
423 |
|
424 if (options.grid.tickColor == null) { |
|
425 options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); |
|
426 } |
|
427 |
|
428 // Fill in defaults for axis options, including any unspecified |
|
429 // font-spec fields, if a font-spec was provided. |
|
430 |
|
431 // If no x/y axis options were provided, create one of each anyway, |
|
432 // since the rest of the code assumes that they exist. |
|
433 |
|
434 var i, axisOptions, axisCount, |
|
435 fontSize = placeholder.css("font-size"), |
|
436 fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, |
|
437 fontDefaults = { |
|
438 style: placeholder.css("font-style"), |
|
439 size: Math.round(0.8 * fontSizeDefault), |
|
440 variant: placeholder.css("font-variant"), |
|
441 weight: placeholder.css("font-weight"), |
|
442 family: placeholder.css("font-family") |
|
443 }; |
|
444 |
|
445 axisCount = options.xaxes.length || 1; |
|
446 for (i = 0; i < axisCount; ++i) { |
|
447 axisOptions = options.xaxes[i]; |
|
448 if (axisOptions && !axisOptions.tickColor) { |
|
449 axisOptions.tickColor = axisOptions.color; |
|
450 } |
|
451 |
|
452 axisOptions = $.extend(true, {}, options.xaxis, axisOptions); |
|
453 options.xaxes[i] = axisOptions; |
|
454 |
|
455 if (axisOptions.font) { |
|
456 axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); |
|
457 if (!axisOptions.font.color) { |
|
458 axisOptions.font.color = axisOptions.color; |
|
459 } |
|
460 if (!axisOptions.font.lineHeight) { |
|
461 axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); |
|
462 } |
|
463 } |
|
464 } |
|
465 |
|
466 axisCount = options.yaxes.length || 1; |
|
467 for (i = 0; i < axisCount; ++i) { |
|
468 axisOptions = options.yaxes[i]; |
|
469 if (axisOptions && !axisOptions.tickColor) { |
|
470 axisOptions.tickColor = axisOptions.color; |
|
471 } |
|
472 |
|
473 axisOptions = $.extend(true, {}, options.yaxis, axisOptions); |
|
474 options.yaxes[i] = axisOptions; |
|
475 |
|
476 if (axisOptions.font) { |
|
477 axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); |
|
478 if (!axisOptions.font.color) { |
|
479 axisOptions.font.color = axisOptions.color; |
|
480 } |
|
481 if (!axisOptions.font.lineHeight) { |
|
482 axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); |
|
483 } |
|
484 } |
|
485 } |
|
486 |
|
487 // save options on axes for future reference |
|
488 for (i = 0; i < options.xaxes.length; ++i) { |
|
489 getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; |
|
490 } |
|
491 |
|
492 for (i = 0; i < options.yaxes.length; ++i) { |
|
493 getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; |
|
494 } |
|
495 |
|
496 //process boxPosition options used for axis.box size |
|
497 $.each(allAxes(), function(_, axis) { |
|
498 axis.boxPosition = axis.options.boxPosition || {centerX: 0, centerY: 0}; |
|
499 }); |
|
500 |
|
501 // add hooks from options |
|
502 for (var n in hooks) { |
|
503 if (options.hooks[n] && options.hooks[n].length) { |
|
504 hooks[n] = hooks[n].concat(options.hooks[n]); |
|
505 } |
|
506 } |
|
507 |
|
508 executeHooks(hooks.processOptions, [options]); |
|
509 } |
|
510 |
|
511 function setData(d) { |
|
512 var oldseries = series; |
|
513 series = parseData(d); |
|
514 fillInSeriesOptions(); |
|
515 processData(oldseries); |
|
516 } |
|
517 |
|
518 function parseData(d) { |
|
519 var res = []; |
|
520 for (var i = 0; i < d.length; ++i) { |
|
521 var s = $.extend(true, {}, options.series); |
|
522 |
|
523 if (d[i].data != null) { |
|
524 s.data = d[i].data; // move the data instead of deep-copy |
|
525 delete d[i].data; |
|
526 |
|
527 $.extend(true, s, d[i]); |
|
528 |
|
529 d[i].data = s.data; |
|
530 } else { |
|
531 s.data = d[i]; |
|
532 } |
|
533 |
|
534 res.push(s); |
|
535 } |
|
536 |
|
537 return res; |
|
538 } |
|
539 |
|
540 function axisNumber(obj, coord) { |
|
541 var a = obj[coord + "axis"]; |
|
542 if (typeof a === "object") { |
|
543 // if we got a real axis, extract number |
|
544 a = a.n; |
|
545 } |
|
546 |
|
547 if (typeof a !== "number") { |
|
548 a = 1; // default to first axis |
|
549 } |
|
550 |
|
551 return a; |
|
552 } |
|
553 |
|
554 function allAxes() { |
|
555 // return flat array without annoying null entries |
|
556 return xaxes.concat(yaxes).filter(function(a) { |
|
557 return a; |
|
558 }); |
|
559 } |
|
560 |
|
561 // canvas to axis for cartesian axes |
|
562 function canvasToCartesianAxisCoords(pos) { |
|
563 // return an object with x/y corresponding to all used axes |
|
564 var res = {}, |
|
565 i, axis; |
|
566 for (i = 0; i < xaxes.length; ++i) { |
|
567 axis = xaxes[i]; |
|
568 if (axis && axis.used) { |
|
569 res["x" + axis.n] = axis.c2p(pos.left); |
|
570 } |
|
571 } |
|
572 |
|
573 for (i = 0; i < yaxes.length; ++i) { |
|
574 axis = yaxes[i]; |
|
575 if (axis && axis.used) { |
|
576 res["y" + axis.n] = axis.c2p(pos.top); |
|
577 } |
|
578 } |
|
579 |
|
580 if (res.x1 !== undefined) { |
|
581 res.x = res.x1; |
|
582 } |
|
583 |
|
584 if (res.y1 !== undefined) { |
|
585 res.y = res.y1; |
|
586 } |
|
587 |
|
588 return res; |
|
589 } |
|
590 |
|
591 // axis to canvas for cartesian axes |
|
592 function cartesianAxisToCanvasCoords(pos) { |
|
593 // get canvas coords from the first pair of x/y found in pos |
|
594 var res = {}, |
|
595 i, axis, key; |
|
596 |
|
597 for (i = 0; i < xaxes.length; ++i) { |
|
598 axis = xaxes[i]; |
|
599 if (axis && axis.used) { |
|
600 key = "x" + axis.n; |
|
601 if (pos[key] == null && axis.n === 1) { |
|
602 key = "x"; |
|
603 } |
|
604 |
|
605 if (pos[key] != null) { |
|
606 res.left = axis.p2c(pos[key]); |
|
607 break; |
|
608 } |
|
609 } |
|
610 } |
|
611 |
|
612 for (i = 0; i < yaxes.length; ++i) { |
|
613 axis = yaxes[i]; |
|
614 if (axis && axis.used) { |
|
615 key = "y" + axis.n; |
|
616 if (pos[key] == null && axis.n === 1) { |
|
617 key = "y"; |
|
618 } |
|
619 |
|
620 if (pos[key] != null) { |
|
621 res.top = axis.p2c(pos[key]); |
|
622 break; |
|
623 } |
|
624 } |
|
625 } |
|
626 |
|
627 return res; |
|
628 } |
|
629 |
|
630 function getOrCreateAxis(axes, number) { |
|
631 if (!axes[number - 1]) { |
|
632 axes[number - 1] = { |
|
633 n: number, // save the number for future reference |
|
634 direction: axes === xaxes ? "x" : "y", |
|
635 options: $.extend(true, {}, axes === xaxes ? options.xaxis : options.yaxis) |
|
636 }; |
|
637 } |
|
638 |
|
639 return axes[number - 1]; |
|
640 } |
|
641 |
|
642 function fillInSeriesOptions() { |
|
643 var neededColors = series.length, |
|
644 maxIndex = -1, |
|
645 i; |
|
646 |
|
647 // Subtract the number of series that already have fixed colors or |
|
648 // color indexes from the number that we still need to generate. |
|
649 |
|
650 for (i = 0; i < series.length; ++i) { |
|
651 var sc = series[i].color; |
|
652 if (sc != null) { |
|
653 neededColors--; |
|
654 if (typeof sc === "number" && sc > maxIndex) { |
|
655 maxIndex = sc; |
|
656 } |
|
657 } |
|
658 } |
|
659 |
|
660 // If any of the series have fixed color indexes, then we need to |
|
661 // generate at least as many colors as the highest index. |
|
662 |
|
663 if (neededColors <= maxIndex) { |
|
664 neededColors = maxIndex + 1; |
|
665 } |
|
666 |
|
667 // Generate all the colors, using first the option colors and then |
|
668 // variations on those colors once they're exhausted. |
|
669 |
|
670 var c, colors = [], |
|
671 colorPool = options.colors, |
|
672 colorPoolSize = colorPool.length, |
|
673 variation = 0, |
|
674 definedColors = Math.max(0, series.length - neededColors); |
|
675 |
|
676 for (i = 0; i < neededColors; i++) { |
|
677 c = $.color.parse(colorPool[(definedColors + i) % colorPoolSize] || "#666"); |
|
678 |
|
679 // Each time we exhaust the colors in the pool we adjust |
|
680 // a scaling factor used to produce more variations on |
|
681 // those colors. The factor alternates negative/positive |
|
682 // to produce lighter/darker colors. |
|
683 |
|
684 // Reset the variation after every few cycles, or else |
|
685 // it will end up producing only white or black colors. |
|
686 |
|
687 if (i % colorPoolSize === 0 && i) { |
|
688 if (variation >= 0) { |
|
689 if (variation < 0.5) { |
|
690 variation = -variation - 0.2; |
|
691 } else variation = 0; |
|
692 } else variation = -variation; |
|
693 } |
|
694 |
|
695 colors[i] = c.scale('rgb', 1 + variation); |
|
696 } |
|
697 |
|
698 // Finalize the series options, filling in their colors |
|
699 |
|
700 var colori = 0, |
|
701 s; |
|
702 for (i = 0; i < series.length; ++i) { |
|
703 s = series[i]; |
|
704 |
|
705 // assign colors |
|
706 if (s.color == null) { |
|
707 s.color = colors[colori].toString(); |
|
708 ++colori; |
|
709 } else if (typeof s.color === "number") { |
|
710 s.color = colors[s.color].toString(); |
|
711 } |
|
712 |
|
713 // turn on lines automatically in case nothing is set |
|
714 if (s.lines.show == null) { |
|
715 var v, show = true; |
|
716 for (v in s) { |
|
717 if (s[v] && s[v].show) { |
|
718 show = false; |
|
719 break; |
|
720 } |
|
721 } |
|
722 |
|
723 if (show) { |
|
724 s.lines.show = true; |
|
725 } |
|
726 } |
|
727 |
|
728 // If nothing was provided for lines.zero, default it to match |
|
729 // lines.fill, since areas by default should extend to zero. |
|
730 |
|
731 if (s.lines.zero == null) { |
|
732 s.lines.zero = !!s.lines.fill; |
|
733 } |
|
734 |
|
735 // setup axes |
|
736 s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); |
|
737 s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); |
|
738 } |
|
739 } |
|
740 |
|
741 function processData(prevSeries) { |
|
742 var topSentry = Number.POSITIVE_INFINITY, |
|
743 bottomSentry = Number.NEGATIVE_INFINITY, |
|
744 i, j, k, m, |
|
745 s, points, ps, val, f, p, |
|
746 data, format; |
|
747 |
|
748 function updateAxis(axis, min, max) { |
|
749 if (min < axis.datamin && min !== -Infinity) { |
|
750 axis.datamin = min; |
|
751 } |
|
752 |
|
753 if (max > axis.datamax && max !== Infinity) { |
|
754 axis.datamax = max; |
|
755 } |
|
756 } |
|
757 |
|
758 function reusePoints(prevSeries, i) { |
|
759 if (prevSeries && prevSeries[i] && prevSeries[i].datapoints && prevSeries[i].datapoints.points) { |
|
760 return prevSeries[i].datapoints.points; |
|
761 } |
|
762 |
|
763 return []; |
|
764 } |
|
765 |
|
766 $.each(allAxes(), function(_, axis) { |
|
767 // init axis |
|
768 if (axis.options.growOnly !== true) { |
|
769 axis.datamin = topSentry; |
|
770 axis.datamax = bottomSentry; |
|
771 } else { |
|
772 if (axis.datamin === undefined) { |
|
773 axis.datamin = topSentry; |
|
774 } |
|
775 if (axis.datamax === undefined) { |
|
776 axis.datamax = bottomSentry; |
|
777 } |
|
778 } |
|
779 axis.used = false; |
|
780 }); |
|
781 |
|
782 for (i = 0; i < series.length; ++i) { |
|
783 s = series[i]; |
|
784 s.datapoints = { |
|
785 points: [] |
|
786 }; |
|
787 |
|
788 if (s.datapoints.points.length === 0) { |
|
789 s.datapoints.points = reusePoints(prevSeries, i); |
|
790 } |
|
791 |
|
792 executeHooks(hooks.processRawData, [s, s.data, s.datapoints]); |
|
793 } |
|
794 |
|
795 // first pass: clean and copy data |
|
796 for (i = 0; i < series.length; ++i) { |
|
797 s = series[i]; |
|
798 |
|
799 data = s.data; |
|
800 format = s.datapoints.format; |
|
801 |
|
802 if (!format) { |
|
803 format = []; |
|
804 // find out how to copy |
|
805 format.push({ |
|
806 x: true, |
|
807 y: false, |
|
808 number: true, |
|
809 required: true, |
|
810 computeRange: s.xaxis.options.autoScale !== 'none', |
|
811 defaultValue: null |
|
812 }); |
|
813 |
|
814 format.push({ |
|
815 x: false, |
|
816 y: true, |
|
817 number: true, |
|
818 required: true, |
|
819 computeRange: s.yaxis.options.autoScale !== 'none', |
|
820 defaultValue: null |
|
821 }); |
|
822 |
|
823 if (s.stack || s.bars.show || (s.lines.show && s.lines.fill)) { |
|
824 var expectedPs = s.datapoints.pointsize != null ? s.datapoints.pointsize : (s.data && s.data[0] && s.data[0].length ? s.data[0].length : 3); |
|
825 if (expectedPs > 2) { |
|
826 format.push({ |
|
827 x: false, |
|
828 y: true, |
|
829 number: true, |
|
830 required: false, |
|
831 computeRange: s.yaxis.options.autoScale !== 'none', |
|
832 defaultValue: 0 |
|
833 }); |
|
834 } |
|
835 } |
|
836 |
|
837 s.datapoints.format = format; |
|
838 } |
|
839 |
|
840 s.xaxis.used = s.yaxis.used = true; |
|
841 |
|
842 if (s.datapoints.pointsize != null) continue; // already filled in |
|
843 |
|
844 s.datapoints.pointsize = format.length; |
|
845 ps = s.datapoints.pointsize; |
|
846 points = s.datapoints.points; |
|
847 |
|
848 var insertSteps = s.lines.show && s.lines.steps; |
|
849 |
|
850 for (j = k = 0; j < data.length; ++j, k += ps) { |
|
851 p = data[j]; |
|
852 |
|
853 var nullify = p == null; |
|
854 if (!nullify) { |
|
855 for (m = 0; m < ps; ++m) { |
|
856 val = p[m]; |
|
857 f = format[m]; |
|
858 |
|
859 if (f) { |
|
860 if (f.number && val != null) { |
|
861 val = +val; // convert to number |
|
862 if (isNaN(val)) { |
|
863 val = null; |
|
864 } |
|
865 } |
|
866 |
|
867 if (val == null) { |
|
868 if (f.required) nullify = true; |
|
869 |
|
870 if (f.defaultValue != null) val = f.defaultValue; |
|
871 } |
|
872 } |
|
873 |
|
874 points[k + m] = val; |
|
875 } |
|
876 } |
|
877 |
|
878 if (nullify) { |
|
879 for (m = 0; m < ps; ++m) { |
|
880 val = points[k + m]; |
|
881 if (val != null) { |
|
882 f = format[m]; |
|
883 // extract min/max info |
|
884 if (f.computeRange) { |
|
885 if (f.x) { |
|
886 updateAxis(s.xaxis, val, val); |
|
887 } |
|
888 if (f.y) { |
|
889 updateAxis(s.yaxis, val, val); |
|
890 } |
|
891 } |
|
892 } |
|
893 points[k + m] = null; |
|
894 } |
|
895 } |
|
896 } |
|
897 |
|
898 points.length = k; //trims the internal buffer to the correct length |
|
899 } |
|
900 |
|
901 // give the hooks a chance to run |
|
902 for (i = 0; i < series.length; ++i) { |
|
903 s = series[i]; |
|
904 |
|
905 executeHooks(hooks.processDatapoints, [s, s.datapoints]); |
|
906 } |
|
907 |
|
908 // second pass: find datamax/datamin for auto-scaling |
|
909 for (i = 0; i < series.length; ++i) { |
|
910 s = series[i]; |
|
911 format = s.datapoints.format; |
|
912 |
|
913 if (format.every(function (f) { return !f.computeRange; })) { |
|
914 continue; |
|
915 } |
|
916 |
|
917 var range = plot.adjustSeriesDataRange(s, |
|
918 plot.computeRangeForDataSeries(s)); |
|
919 |
|
920 executeHooks(hooks.adjustSeriesDataRange, [s, range]); |
|
921 |
|
922 updateAxis(s.xaxis, range.xmin, range.xmax); |
|
923 updateAxis(s.yaxis, range.ymin, range.ymax); |
|
924 } |
|
925 |
|
926 $.each(allAxes(), function(_, axis) { |
|
927 if (axis.datamin === topSentry) { |
|
928 axis.datamin = null; |
|
929 } |
|
930 |
|
931 if (axis.datamax === bottomSentry) { |
|
932 axis.datamax = null; |
|
933 } |
|
934 }); |
|
935 } |
|
936 |
|
937 function setupCanvases() { |
|
938 // Make sure the placeholder is clear of everything except canvases |
|
939 // from a previous plot in this container that we'll try to re-use. |
|
940 |
|
941 placeholder.css("padding", 0) // padding messes up the positioning |
|
942 .children().filter(function() { |
|
943 return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); |
|
944 }).remove(); |
|
945 |
|
946 if (placeholder.css("position") === 'static') { |
|
947 placeholder.css("position", "relative"); // for positioning labels and overlay |
|
948 } |
|
949 |
|
950 surface = new Canvas("flot-base", placeholder[0]); |
|
951 overlay = new Canvas("flot-overlay", placeholder[0]); // overlay canvas for interactive features |
|
952 |
|
953 ctx = surface.context; |
|
954 octx = overlay.context; |
|
955 |
|
956 // define which element we're listening for events on |
|
957 eventHolder = $(overlay.element).unbind(); |
|
958 |
|
959 // If we're re-using a plot object, shut down the old one |
|
960 |
|
961 var existing = placeholder.data("plot"); |
|
962 |
|
963 if (existing) { |
|
964 existing.shutdown(); |
|
965 overlay.clear(); |
|
966 } |
|
967 |
|
968 // save in case we get replotted |
|
969 placeholder.data("plot", plot); |
|
970 } |
|
971 |
|
972 function bindEvents() { |
|
973 executeHooks(hooks.bindEvents, [eventHolder]); |
|
974 } |
|
975 |
|
976 function addEventHandler(event, handler, eventHolder, priority) { |
|
977 var key = eventHolder + event; |
|
978 var eventList = eventManager[key] || []; |
|
979 |
|
980 eventList.push({"event": event, "handler": handler, "eventHolder": eventHolder, "priority": priority}); |
|
981 eventList.sort((a, b) => b.priority - a.priority ); |
|
982 eventList.forEach( eventData => { |
|
983 eventData.eventHolder.unbind(eventData.event, eventData.handler); |
|
984 eventData.eventHolder.bind(eventData.event, eventData.handler); |
|
985 }); |
|
986 |
|
987 eventManager[key] = eventList; |
|
988 } |
|
989 |
|
990 function shutdown() { |
|
991 if (redrawTimeout) { |
|
992 clearTimeout(redrawTimeout); |
|
993 } |
|
994 |
|
995 executeHooks(hooks.shutdown, [eventHolder]); |
|
996 } |
|
997 |
|
998 function setTransformationHelpers(axis) { |
|
999 // set helper functions on the axis, assumes plot area |
|
1000 // has been computed already |
|
1001 |
|
1002 function identity(x) { |
|
1003 return x; |
|
1004 } |
|
1005 |
|
1006 var s, m, t = axis.options.transform || identity, |
|
1007 it = axis.options.inverseTransform; |
|
1008 |
|
1009 // precompute how much the axis is scaling a point |
|
1010 // in canvas space |
|
1011 if (axis.direction === "x") { |
|
1012 if (isFinite(t(axis.max) - t(axis.min))) { |
|
1013 s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); |
|
1014 } else { |
|
1015 s = axis.scale = 1 / Math.abs($.plot.saturated.delta(t(axis.min), t(axis.max), plotWidth)); |
|
1016 } |
|
1017 m = Math.min(t(axis.max), t(axis.min)); |
|
1018 } else { |
|
1019 if (isFinite(t(axis.max) - t(axis.min))) { |
|
1020 s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); |
|
1021 } else { |
|
1022 s = axis.scale = 1 / Math.abs($.plot.saturated.delta(t(axis.min), t(axis.max), plotHeight)); |
|
1023 } |
|
1024 s = -s; |
|
1025 m = Math.max(t(axis.max), t(axis.min)); |
|
1026 } |
|
1027 |
|
1028 // data point to canvas coordinate |
|
1029 if (t === identity) { |
|
1030 // slight optimization |
|
1031 axis.p2c = function(p) { |
|
1032 if (isFinite(p - m)) { |
|
1033 return (p - m) * s; |
|
1034 } else { |
|
1035 return (p / 4 - m / 4) * s * 4; |
|
1036 } |
|
1037 }; |
|
1038 } else { |
|
1039 axis.p2c = function(p) { |
|
1040 var tp = t(p); |
|
1041 |
|
1042 if (isFinite(tp - m)) { |
|
1043 return (tp - m) * s; |
|
1044 } else { |
|
1045 return (tp / 4 - m / 4) * s * 4; |
|
1046 } |
|
1047 }; |
|
1048 } |
|
1049 |
|
1050 // canvas coordinate to data point |
|
1051 if (!it) { |
|
1052 axis.c2p = function(c) { |
|
1053 return m + c / s; |
|
1054 }; |
|
1055 } else { |
|
1056 axis.c2p = function(c) { |
|
1057 return it(m + c / s); |
|
1058 }; |
|
1059 } |
|
1060 } |
|
1061 |
|
1062 function measureTickLabels(axis) { |
|
1063 var opts = axis.options, |
|
1064 ticks = opts.showTickLabels !== 'none' && axis.ticks ? axis.ticks : [], |
|
1065 showMajorTickLabels = opts.showTickLabels === 'major' || opts.showTickLabels === 'all', |
|
1066 showEndpointsTickLabels = opts.showTickLabels === 'endpoints' || opts.showTickLabels === 'all', |
|
1067 labelWidth = opts.labelWidth || 0, |
|
1068 labelHeight = opts.labelHeight || 0, |
|
1069 legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", |
|
1070 layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, |
|
1071 font = opts.font || "flot-tick-label tickLabel"; |
|
1072 |
|
1073 for (var i = 0; i < ticks.length; ++i) { |
|
1074 var t = ticks[i]; |
|
1075 var label = t.label; |
|
1076 |
|
1077 if (!t.label || |
|
1078 (showMajorTickLabels === false && i > 0 && i < ticks.length - 1) || |
|
1079 (showEndpointsTickLabels === false && (i === 0 || i === ticks.length - 1))) { |
|
1080 continue; |
|
1081 } |
|
1082 |
|
1083 if (typeof t.label === 'object') { |
|
1084 label = t.label.name; |
|
1085 } |
|
1086 |
|
1087 var info = surface.getTextInfo(layer, label, font); |
|
1088 |
|
1089 labelWidth = Math.max(labelWidth, info.width); |
|
1090 labelHeight = Math.max(labelHeight, info.height); |
|
1091 } |
|
1092 |
|
1093 axis.labelWidth = opts.labelWidth || labelWidth; |
|
1094 axis.labelHeight = opts.labelHeight || labelHeight; |
|
1095 } |
|
1096 |
|
1097 function allocateAxisBoxFirstPhase(axis) { |
|
1098 // find the bounding box of the axis by looking at label |
|
1099 // widths/heights and ticks, make room by diminishing the |
|
1100 // plotOffset; this first phase only looks at one |
|
1101 // dimension per axis, the other dimension depends on the |
|
1102 // other axes so will have to wait |
|
1103 |
|
1104 // here reserve additional space |
|
1105 executeHooks(hooks.axisReserveSpace, [axis]); |
|
1106 |
|
1107 var lw = axis.labelWidth, |
|
1108 lh = axis.labelHeight, |
|
1109 pos = axis.options.position, |
|
1110 isXAxis = axis.direction === "x", |
|
1111 tickLength = axis.options.tickLength, |
|
1112 showTicks = axis.options.showTicks, |
|
1113 showMinorTicks = axis.options.showMinorTicks, |
|
1114 gridLines = axis.options.gridLines, |
|
1115 axisMargin = options.grid.axisMargin, |
|
1116 padding = options.grid.labelMargin, |
|
1117 innermost = true, |
|
1118 outermost = true, |
|
1119 found = false; |
|
1120 |
|
1121 // Determine the axis's position in its direction and on its side |
|
1122 |
|
1123 $.each(isXAxis ? xaxes : yaxes, function(i, a) { |
|
1124 if (a && (a.show || a.reserveSpace)) { |
|
1125 if (a === axis) { |
|
1126 found = true; |
|
1127 } else if (a.options.position === pos) { |
|
1128 if (found) { |
|
1129 outermost = false; |
|
1130 } else { |
|
1131 innermost = false; |
|
1132 } |
|
1133 } |
|
1134 } |
|
1135 }); |
|
1136 |
|
1137 // The outermost axis on each side has no margin |
|
1138 if (outermost) { |
|
1139 axisMargin = 0; |
|
1140 } |
|
1141 |
|
1142 // Set the default tickLength if necessary |
|
1143 if (tickLength == null) { |
|
1144 tickLength = TICK_LENGTH_CONSTANT; |
|
1145 } |
|
1146 |
|
1147 // By default, major tick marks are visible |
|
1148 if (showTicks == null) { |
|
1149 showTicks = true; |
|
1150 } |
|
1151 |
|
1152 // By default, minor tick marks are visible |
|
1153 if (showMinorTicks == null) { |
|
1154 showMinorTicks = true; |
|
1155 } |
|
1156 |
|
1157 // By default, grid lines are visible |
|
1158 if (gridLines == null) { |
|
1159 if (innermost) { |
|
1160 gridLines = true; |
|
1161 } else { |
|
1162 gridLines = false; |
|
1163 } |
|
1164 } |
|
1165 |
|
1166 if (!isNaN(+tickLength)) { |
|
1167 padding += showTicks ? +tickLength : 0; |
|
1168 } |
|
1169 |
|
1170 if (isXAxis) { |
|
1171 lh += padding; |
|
1172 |
|
1173 if (pos === "bottom") { |
|
1174 plotOffset.bottom += lh + axisMargin; |
|
1175 axis.box = { |
|
1176 top: surface.height - plotOffset.bottom, |
|
1177 height: lh |
|
1178 }; |
|
1179 } else { |
|
1180 axis.box = { |
|
1181 top: plotOffset.top + axisMargin, |
|
1182 height: lh |
|
1183 }; |
|
1184 plotOffset.top += lh + axisMargin; |
|
1185 } |
|
1186 } else { |
|
1187 lw += padding; |
|
1188 |
|
1189 if (pos === "left") { |
|
1190 axis.box = { |
|
1191 left: plotOffset.left + axisMargin, |
|
1192 width: lw |
|
1193 }; |
|
1194 plotOffset.left += lw + axisMargin; |
|
1195 } else { |
|
1196 plotOffset.right += lw + axisMargin; |
|
1197 axis.box = { |
|
1198 left: surface.width - plotOffset.right, |
|
1199 width: lw |
|
1200 }; |
|
1201 } |
|
1202 } |
|
1203 |
|
1204 // save for future reference |
|
1205 axis.position = pos; |
|
1206 axis.tickLength = tickLength; |
|
1207 axis.showMinorTicks = showMinorTicks; |
|
1208 axis.showTicks = showTicks; |
|
1209 axis.gridLines = gridLines; |
|
1210 axis.box.padding = padding; |
|
1211 axis.innermost = innermost; |
|
1212 } |
|
1213 |
|
1214 function allocateAxisBoxSecondPhase(axis) { |
|
1215 // now that all axis boxes have been placed in one |
|
1216 // dimension, we can set the remaining dimension coordinates |
|
1217 if (axis.direction === "x") { |
|
1218 axis.box.left = plotOffset.left - axis.labelWidth / 2; |
|
1219 axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; |
|
1220 } else { |
|
1221 axis.box.top = plotOffset.top - axis.labelHeight / 2; |
|
1222 axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; |
|
1223 } |
|
1224 } |
|
1225 |
|
1226 function adjustLayoutForThingsStickingOut() { |
|
1227 // possibly adjust plot offset to ensure everything stays |
|
1228 // inside the canvas and isn't clipped off |
|
1229 |
|
1230 var minMargin = options.grid.minBorderMargin, |
|
1231 i; |
|
1232 |
|
1233 // check stuff from the plot (FIXME: this should just read |
|
1234 // a value from the series, otherwise it's impossible to |
|
1235 // customize) |
|
1236 if (minMargin == null) { |
|
1237 minMargin = 0; |
|
1238 for (i = 0; i < series.length; ++i) { |
|
1239 minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth / 2)); |
|
1240 } |
|
1241 } |
|
1242 |
|
1243 var a, offset = {}, |
|
1244 margins = { |
|
1245 left: minMargin, |
|
1246 right: minMargin, |
|
1247 top: minMargin, |
|
1248 bottom: minMargin |
|
1249 }; |
|
1250 |
|
1251 // check axis labels, note we don't check the actual |
|
1252 // labels but instead use the overall width/height to not |
|
1253 // jump as much around with replots |
|
1254 $.each(allAxes(), function(_, axis) { |
|
1255 if (axis.reserveSpace && axis.ticks && axis.ticks.length) { |
|
1256 if (axis.direction === "x") { |
|
1257 margins.left = Math.max(margins.left, axis.labelWidth / 2); |
|
1258 margins.right = Math.max(margins.right, axis.labelWidth / 2); |
|
1259 } else { |
|
1260 margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); |
|
1261 margins.top = Math.max(margins.top, axis.labelHeight / 2); |
|
1262 } |
|
1263 } |
|
1264 }); |
|
1265 |
|
1266 for (a in margins) { |
|
1267 offset[a] = margins[a] - plotOffset[a]; |
|
1268 } |
|
1269 $.each(xaxes.concat(yaxes), function(_, axis) { |
|
1270 alignAxisWithGrid(axis, offset, function (offset) { |
|
1271 return offset > 0; |
|
1272 }); |
|
1273 }); |
|
1274 |
|
1275 plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); |
|
1276 plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); |
|
1277 plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); |
|
1278 plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); |
|
1279 } |
|
1280 |
|
1281 function alignAxisWithGrid(axis, offset, isValid) { |
|
1282 if (axis.direction === "x") { |
|
1283 if (axis.position === "bottom" && isValid(offset.bottom)) { |
|
1284 axis.box.top -= Math.ceil(offset.bottom); |
|
1285 } |
|
1286 if (axis.position === "top" && isValid(offset.top)) { |
|
1287 axis.box.top += Math.ceil(offset.top); |
|
1288 } |
|
1289 } else { |
|
1290 if (axis.position === "left" && isValid(offset.left)) { |
|
1291 axis.box.left += Math.ceil(offset.left); |
|
1292 } |
|
1293 if (axis.position === "right" && isValid(offset.right)) { |
|
1294 axis.box.left -= Math.ceil(offset.right); |
|
1295 } |
|
1296 } |
|
1297 } |
|
1298 |
|
1299 function setupGrid(autoScale) { |
|
1300 var i, a, axes = allAxes(), |
|
1301 showGrid = options.grid.show; |
|
1302 |
|
1303 // Initialize the plot's offset from the edge of the canvas |
|
1304 |
|
1305 for (a in plotOffset) { |
|
1306 plotOffset[a] = 0; |
|
1307 } |
|
1308 |
|
1309 executeHooks(hooks.processOffset, [plotOffset]); |
|
1310 |
|
1311 // If the grid is visible, add its border width to the offset |
|
1312 for (a in plotOffset) { |
|
1313 if (typeof (options.grid.borderWidth) === "object") { |
|
1314 plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; |
|
1315 } else { |
|
1316 plotOffset[a] += showGrid ? options.grid.borderWidth : 0; |
|
1317 } |
|
1318 } |
|
1319 |
|
1320 $.each(axes, function(_, axis) { |
|
1321 var axisOpts = axis.options; |
|
1322 axis.show = axisOpts.show == null ? axis.used : axisOpts.show; |
|
1323 axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; |
|
1324 setupTickFormatter(axis); |
|
1325 executeHooks(hooks.setRange, [axis, autoScale]); |
|
1326 setRange(axis, autoScale); |
|
1327 }); |
|
1328 |
|
1329 if (showGrid) { |
|
1330 plotWidth = surface.width - plotOffset.left - plotOffset.right; |
|
1331 plotHeight = surface.height - plotOffset.bottom - plotOffset.top; |
|
1332 |
|
1333 var allocatedAxes = $.grep(axes, function(axis) { |
|
1334 return axis.show || axis.reserveSpace; |
|
1335 }); |
|
1336 |
|
1337 $.each(allocatedAxes, function(_, axis) { |
|
1338 // make the ticks |
|
1339 setupTickGeneration(axis); |
|
1340 setMajorTicks(axis); |
|
1341 snapRangeToTicks(axis, axis.ticks, series); |
|
1342 |
|
1343 //for computing the endpoints precision, transformationHelpers are needed |
|
1344 setTransformationHelpers(axis); |
|
1345 setEndpointTicks(axis, series); |
|
1346 |
|
1347 // find labelWidth/Height for axis |
|
1348 measureTickLabels(axis); |
|
1349 }); |
|
1350 |
|
1351 // with all dimensions calculated, we can compute the |
|
1352 // axis bounding boxes, start from the outside |
|
1353 // (reverse order) |
|
1354 for (i = allocatedAxes.length - 1; i >= 0; --i) { |
|
1355 allocateAxisBoxFirstPhase(allocatedAxes[i]); |
|
1356 } |
|
1357 |
|
1358 // make sure we've got enough space for things that |
|
1359 // might stick out |
|
1360 adjustLayoutForThingsStickingOut(); |
|
1361 |
|
1362 $.each(allocatedAxes, function(_, axis) { |
|
1363 allocateAxisBoxSecondPhase(axis); |
|
1364 }); |
|
1365 } |
|
1366 |
|
1367 //adjust axis and plotOffset according to grid.margins |
|
1368 if (options.grid.margin) { |
|
1369 for (a in plotOffset) { |
|
1370 var margin = options.grid.margin || 0; |
|
1371 plotOffset[a] += typeof margin === "number" ? margin : (margin[a] || 0); |
|
1372 } |
|
1373 $.each(xaxes.concat(yaxes), function(_, axis) { |
|
1374 alignAxisWithGrid(axis, options.grid.margin, function(offset) { |
|
1375 return offset !== undefined && offset !== null; |
|
1376 }); |
|
1377 }); |
|
1378 } |
|
1379 |
|
1380 //after adjusting the axis, plot width and height will be modified |
|
1381 plotWidth = surface.width - plotOffset.left - plotOffset.right; |
|
1382 plotHeight = surface.height - plotOffset.bottom - plotOffset.top; |
|
1383 |
|
1384 // now we got the proper plot dimensions, we can compute the scaling |
|
1385 $.each(axes, function(_, axis) { |
|
1386 setTransformationHelpers(axis); |
|
1387 }); |
|
1388 |
|
1389 if (showGrid) { |
|
1390 drawAxisLabels(); |
|
1391 } |
|
1392 |
|
1393 executeHooks(hooks.setupGrid, []); |
|
1394 } |
|
1395 |
|
1396 function widenMinMax(minimum, maximum) { |
|
1397 var min = (minimum === undefined ? null : minimum); |
|
1398 var max = (maximum === undefined ? null : maximum); |
|
1399 var delta = max - min; |
|
1400 if (delta === 0.0) { |
|
1401 // degenerate case |
|
1402 var widen = max === 0 ? 1 : 0.01; |
|
1403 var wmin = null; |
|
1404 if (min == null) { |
|
1405 wmin -= widen; |
|
1406 } |
|
1407 |
|
1408 // always widen max if we couldn't widen min to ensure we |
|
1409 // don't fall into min == max which doesn't work |
|
1410 if (max == null || min != null) { |
|
1411 max += widen; |
|
1412 } |
|
1413 |
|
1414 if (wmin != null) { |
|
1415 min = wmin; |
|
1416 } |
|
1417 } |
|
1418 |
|
1419 return { |
|
1420 min: min, |
|
1421 max: max |
|
1422 }; |
|
1423 } |
|
1424 |
|
1425 function autoScaleAxis(axis) { |
|
1426 var opts = axis.options, |
|
1427 min = opts.min, |
|
1428 max = opts.max, |
|
1429 datamin = axis.datamin, |
|
1430 datamax = axis.datamax, |
|
1431 delta; |
|
1432 |
|
1433 switch (opts.autoScale) { |
|
1434 case "none": |
|
1435 min = +(opts.min != null ? opts.min : datamin); |
|
1436 max = +(opts.max != null ? opts.max : datamax); |
|
1437 break; |
|
1438 case "loose": |
|
1439 if (datamin != null && datamax != null) { |
|
1440 min = datamin; |
|
1441 max = datamax; |
|
1442 delta = $.plot.saturated.saturate(max - min); |
|
1443 var margin = ((typeof opts.autoScaleMargin === 'number') ? opts.autoScaleMargin : 0.02); |
|
1444 min = $.plot.saturated.saturate(min - delta * margin); |
|
1445 max = $.plot.saturated.saturate(max + delta * margin); |
|
1446 |
|
1447 // make sure we don't go below zero if all values are positive |
|
1448 if (min < 0 && datamin >= 0) { |
|
1449 min = 0; |
|
1450 } |
|
1451 } else { |
|
1452 min = opts.min; |
|
1453 max = opts.max; |
|
1454 } |
|
1455 break; |
|
1456 case "exact": |
|
1457 min = (datamin != null ? datamin : opts.min); |
|
1458 max = (datamax != null ? datamax : opts.max); |
|
1459 break; |
|
1460 case "sliding-window": |
|
1461 if (datamax > max) { |
|
1462 // move the window to fit the new data, |
|
1463 // keeping the axis range constant |
|
1464 max = datamax; |
|
1465 min = Math.max(datamax - (opts.windowSize || 100), min); |
|
1466 } |
|
1467 break; |
|
1468 } |
|
1469 |
|
1470 var widenedMinMax = widenMinMax(min, max); |
|
1471 min = widenedMinMax.min; |
|
1472 max = widenedMinMax.max; |
|
1473 |
|
1474 // grow loose or grow exact supported |
|
1475 if (opts.growOnly === true && opts.autoScale !== "none" && opts.autoScale !== "sliding-window") { |
|
1476 min = (min < datamin) ? min : (datamin !== null ? datamin : min); |
|
1477 max = (max > datamax) ? max : (datamax !== null ? datamax : max); |
|
1478 } |
|
1479 |
|
1480 axis.autoScaledMin = min; |
|
1481 axis.autoScaledMax = max; |
|
1482 } |
|
1483 |
|
1484 function setRange(axis, autoScale) { |
|
1485 var min = typeof axis.options.min === 'number' ? axis.options.min : axis.min, |
|
1486 max = typeof axis.options.max === 'number' ? axis.options.max : axis.max, |
|
1487 plotOffset = axis.options.offset; |
|
1488 |
|
1489 if (autoScale) { |
|
1490 autoScaleAxis(axis); |
|
1491 min = axis.autoScaledMin; |
|
1492 max = axis.autoScaledMax; |
|
1493 } |
|
1494 |
|
1495 min = (min != null ? min : -1) + (plotOffset.below || 0); |
|
1496 max = (max != null ? max : 1) + (plotOffset.above || 0); |
|
1497 |
|
1498 if (min > max) { |
|
1499 var tmp = min; |
|
1500 min = max; |
|
1501 max = tmp; |
|
1502 axis.options.offset = { above: 0, below: 0 }; |
|
1503 } |
|
1504 |
|
1505 axis.min = $.plot.saturated.saturate(min); |
|
1506 axis.max = $.plot.saturated.saturate(max); |
|
1507 } |
|
1508 |
|
1509 function computeValuePrecision (min, max, direction, ticks, tickDecimals) { |
|
1510 var noTicks = fixupNumberOfTicks(direction, surface, ticks); |
|
1511 |
|
1512 var delta = $.plot.saturated.delta(min, max, noTicks), |
|
1513 dec = -Math.floor(Math.log(delta) / Math.LN10); |
|
1514 |
|
1515 //if it is called with tickDecimals, then the precision should not be greather then that |
|
1516 if (tickDecimals && dec > tickDecimals) { |
|
1517 dec = tickDecimals; |
|
1518 } |
|
1519 |
|
1520 var magn = parseFloat('1e' + (-dec)), |
|
1521 norm = delta / magn; |
|
1522 |
|
1523 if (norm > 2.25 && norm < 3 && (dec + 1) <= tickDecimals) { |
|
1524 //we need an extra decimals when tickSize is 2.5 |
|
1525 ++dec; |
|
1526 } |
|
1527 |
|
1528 return isFinite(dec) ? dec : 0; |
|
1529 }; |
|
1530 |
|
1531 function computeTickSize (min, max, noTicks, tickDecimals) { |
|
1532 var delta = $.plot.saturated.delta(min, max, noTicks), |
|
1533 dec = -Math.floor(Math.log(delta) / Math.LN10); |
|
1534 |
|
1535 //if it is called with tickDecimals, then the precision should not be greather then that |
|
1536 if (tickDecimals && dec > tickDecimals) { |
|
1537 dec = tickDecimals; |
|
1538 } |
|
1539 |
|
1540 var magn = parseFloat('1e' + (-dec)), |
|
1541 norm = delta / magn, // norm is between 1.0 and 10.0 |
|
1542 size; |
|
1543 |
|
1544 if (norm < 1.5) { |
|
1545 size = 1; |
|
1546 } else if (norm < 3) { |
|
1547 size = 2; |
|
1548 if (norm > 2.25 && (tickDecimals == null || (dec + 1) <= tickDecimals)) { |
|
1549 size = 2.5; |
|
1550 } |
|
1551 } else if (norm < 7.5) { |
|
1552 size = 5; |
|
1553 } else { |
|
1554 size = 10; |
|
1555 } |
|
1556 |
|
1557 size *= magn; |
|
1558 return size; |
|
1559 } |
|
1560 |
|
1561 function getAxisTickSize(min, max, direction, options, tickDecimals) { |
|
1562 var noTicks; |
|
1563 |
|
1564 if (typeof options.ticks === "number" && options.ticks > 0) { |
|
1565 noTicks = options.ticks; |
|
1566 } else { |
|
1567 // heuristic based on the model a*sqrt(x) fitted to |
|
1568 // some data points that seemed reasonable |
|
1569 noTicks = 0.3 * Math.sqrt(direction === "x" ? surface.width : surface.height); |
|
1570 } |
|
1571 |
|
1572 var size = computeTickSize(min, max, noTicks, tickDecimals); |
|
1573 |
|
1574 if (options.minTickSize != null && size < options.minTickSize) { |
|
1575 size = options.minTickSize; |
|
1576 } |
|
1577 |
|
1578 return options.tickSize || size; |
|
1579 }; |
|
1580 |
|
1581 function fixupNumberOfTicks(direction, surface, ticksOption) { |
|
1582 var noTicks; |
|
1583 |
|
1584 if (typeof ticksOption === "number" && ticksOption > 0) { |
|
1585 noTicks = ticksOption; |
|
1586 } else { |
|
1587 noTicks = 0.3 * Math.sqrt(direction === "x" ? surface.width : surface.height); |
|
1588 } |
|
1589 |
|
1590 return noTicks; |
|
1591 } |
|
1592 |
|
1593 function setupTickFormatter(axis) { |
|
1594 var opts = axis.options; |
|
1595 if (!axis.tickFormatter) { |
|
1596 if (typeof opts.tickFormatter === 'function') { |
|
1597 axis.tickFormatter = function() { |
|
1598 var args = Array.prototype.slice.call(arguments); |
|
1599 return "" + opts.tickFormatter.apply(null, args); |
|
1600 }; |
|
1601 } else { |
|
1602 axis.tickFormatter = defaultTickFormatter; |
|
1603 } |
|
1604 } |
|
1605 } |
|
1606 |
|
1607 function setupTickGeneration(axis) { |
|
1608 var opts = axis.options; |
|
1609 var noTicks; |
|
1610 |
|
1611 noTicks = fixupNumberOfTicks(axis.direction, surface, opts.ticks); |
|
1612 |
|
1613 axis.delta = $.plot.saturated.delta(axis.min, axis.max, noTicks); |
|
1614 var precision = plot.computeValuePrecision(axis.min, axis.max, axis.direction, noTicks, opts.tickDecimals); |
|
1615 |
|
1616 axis.tickDecimals = Math.max(0, opts.tickDecimals != null ? opts.tickDecimals : precision); |
|
1617 axis.tickSize = getAxisTickSize(axis.min, axis.max, axis.direction, opts, opts.tickDecimals); |
|
1618 |
|
1619 // Flot supports base-10 axes; any other mode else is handled by a plug-in, |
|
1620 // like flot.time.js. |
|
1621 |
|
1622 if (!axis.tickGenerator) { |
|
1623 if (typeof opts.tickGenerator === 'function') { |
|
1624 axis.tickGenerator = opts.tickGenerator; |
|
1625 } else { |
|
1626 axis.tickGenerator = defaultTickGenerator; |
|
1627 } |
|
1628 } |
|
1629 |
|
1630 if (opts.alignTicksWithAxis != null) { |
|
1631 var otherAxis = (axis.direction === "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; |
|
1632 if (otherAxis && otherAxis.used && otherAxis !== axis) { |
|
1633 // consider snapping min/max to outermost nice ticks |
|
1634 var niceTicks = axis.tickGenerator(axis, plot); |
|
1635 if (niceTicks.length > 0) { |
|
1636 if (opts.min == null) { |
|
1637 axis.min = Math.min(axis.min, niceTicks[0]); |
|
1638 } |
|
1639 |
|
1640 if (opts.max == null && niceTicks.length > 1) { |
|
1641 axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); |
|
1642 } |
|
1643 } |
|
1644 |
|
1645 axis.tickGenerator = function(axis) { |
|
1646 // copy ticks, scaled to this axis |
|
1647 var ticks = [], |
|
1648 v, i; |
|
1649 for (i = 0; i < otherAxis.ticks.length; ++i) { |
|
1650 v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); |
|
1651 v = axis.min + v * (axis.max - axis.min); |
|
1652 ticks.push(v); |
|
1653 } |
|
1654 return ticks; |
|
1655 }; |
|
1656 |
|
1657 // we might need an extra decimal since forced |
|
1658 // ticks don't necessarily fit naturally |
|
1659 if (!axis.mode && opts.tickDecimals == null) { |
|
1660 var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), |
|
1661 ts = axis.tickGenerator(axis, plot); |
|
1662 |
|
1663 // only proceed if the tick interval rounded |
|
1664 // with an extra decimal doesn't give us a |
|
1665 // zero at end |
|
1666 if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) { |
|
1667 axis.tickDecimals = extraDec; |
|
1668 } |
|
1669 } |
|
1670 } |
|
1671 } |
|
1672 } |
|
1673 |
|
1674 function setMajorTicks(axis) { |
|
1675 var oticks = axis.options.ticks, |
|
1676 ticks = []; |
|
1677 if (oticks == null || (typeof oticks === "number" && oticks > 0)) { |
|
1678 ticks = axis.tickGenerator(axis, plot); |
|
1679 } else if (oticks) { |
|
1680 if ($.isFunction(oticks)) { |
|
1681 // generate the ticks |
|
1682 ticks = oticks(axis); |
|
1683 } else { |
|
1684 ticks = oticks; |
|
1685 } |
|
1686 } |
|
1687 |
|
1688 // clean up/labelify the supplied ticks, copy them over |
|
1689 var i, v; |
|
1690 axis.ticks = []; |
|
1691 for (i = 0; i < ticks.length; ++i) { |
|
1692 var label = null; |
|
1693 var t = ticks[i]; |
|
1694 if (typeof t === "object") { |
|
1695 v = +t[0]; |
|
1696 if (t.length > 1) { |
|
1697 label = t[1]; |
|
1698 } |
|
1699 } else { |
|
1700 v = +t; |
|
1701 } |
|
1702 |
|
1703 if (!isNaN(v)) { |
|
1704 axis.ticks.push( |
|
1705 newTick(v, label, axis, 'major')); |
|
1706 } |
|
1707 } |
|
1708 } |
|
1709 |
|
1710 function newTick(v, label, axis, type) { |
|
1711 if (label === null) { |
|
1712 switch (type) { |
|
1713 case 'min': |
|
1714 case 'max': |
|
1715 //improving the precision of endpoints |
|
1716 var precision = getEndpointPrecision(v, axis); |
|
1717 label = isFinite(precision) ? axis.tickFormatter(v, axis, precision, plot) : axis.tickFormatter(v, axis, precision, plot); |
|
1718 break; |
|
1719 case 'major': |
|
1720 label = axis.tickFormatter(v, axis, undefined, plot); |
|
1721 } |
|
1722 } |
|
1723 return { |
|
1724 v: v, |
|
1725 label: label |
|
1726 }; |
|
1727 } |
|
1728 |
|
1729 function snapRangeToTicks(axis, ticks, series) { |
|
1730 var anyDataInSeries = function(series) { |
|
1731 return series.some(e => e.datapoints.points.length > 0); |
|
1732 } |
|
1733 |
|
1734 if (axis.options.autoScale === "loose" && ticks.length > 0 && anyDataInSeries(series)) { |
|
1735 // snap to ticks |
|
1736 axis.min = Math.min(axis.min, ticks[0].v); |
|
1737 axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); |
|
1738 } |
|
1739 } |
|
1740 |
|
1741 function getEndpointPrecision(value, axis) { |
|
1742 var canvas1 = Math.floor(axis.p2c(value)), |
|
1743 canvas2 = axis.direction === "x" ? canvas1 + 1 : canvas1 - 1, |
|
1744 point1 = axis.c2p(canvas1), |
|
1745 point2 = axis.c2p(canvas2), |
|
1746 precision = computeValuePrecision(point1, point2, axis.direction, 1); |
|
1747 |
|
1748 return precision; |
|
1749 } |
|
1750 |
|
1751 function setEndpointTicks(axis, series) { |
|
1752 if (isValidEndpointTick(axis, series)) { |
|
1753 axis.ticks.unshift(newTick(axis.min, null, axis, 'min')); |
|
1754 axis.ticks.push(newTick(axis.max, null, axis, 'max')); |
|
1755 } |
|
1756 } |
|
1757 |
|
1758 function isValidEndpointTick(axis, series) { |
|
1759 if (axis.options.showTickLabels === 'endpoints') { |
|
1760 return true; |
|
1761 } |
|
1762 if (axis.options.showTickLabels === 'all') { |
|
1763 var associatedSeries = series.filter(function(s) { |
|
1764 return s.xaxis === axis; |
|
1765 }), |
|
1766 notAllBarSeries = associatedSeries.some(function(s) { |
|
1767 return !s.bars.show; |
|
1768 }); |
|
1769 return associatedSeries.length === 0 || notAllBarSeries; |
|
1770 } |
|
1771 if (axis.options.showTickLabels === 'major' || axis.options.showTickLabels === 'none') { |
|
1772 return false; |
|
1773 } |
|
1774 } |
|
1775 |
|
1776 function draw() { |
|
1777 surface.clear(); |
|
1778 executeHooks(hooks.drawBackground, [ctx]); |
|
1779 |
|
1780 var grid = options.grid; |
|
1781 |
|
1782 // draw background, if any |
|
1783 if (grid.show && grid.backgroundColor) { |
|
1784 drawBackground(); |
|
1785 } |
|
1786 |
|
1787 if (grid.show && !grid.aboveData) { |
|
1788 drawGrid(); |
|
1789 } |
|
1790 |
|
1791 for (var i = 0; i < series.length; ++i) { |
|
1792 executeHooks(hooks.drawSeries, [ctx, series[i], i, getColorOrGradient]); |
|
1793 drawSeries(series[i]); |
|
1794 } |
|
1795 |
|
1796 executeHooks(hooks.draw, [ctx]); |
|
1797 |
|
1798 if (grid.show && grid.aboveData) { |
|
1799 drawGrid(); |
|
1800 } |
|
1801 |
|
1802 surface.render(); |
|
1803 |
|
1804 // A draw implies that either the axes or data have changed, so we |
|
1805 // should probably update the overlay highlights as well. |
|
1806 triggerRedrawOverlay(); |
|
1807 } |
|
1808 |
|
1809 function extractRange(ranges, coord) { |
|
1810 var axis, from, to, key, axes = allAxes(); |
|
1811 |
|
1812 for (var i = 0; i < axes.length; ++i) { |
|
1813 axis = axes[i]; |
|
1814 if (axis.direction === coord) { |
|
1815 key = coord + axis.n + "axis"; |
|
1816 if (!ranges[key] && axis.n === 1) { |
|
1817 // support x1axis as xaxis |
|
1818 key = coord + "axis"; |
|
1819 } |
|
1820 |
|
1821 if (ranges[key]) { |
|
1822 from = ranges[key].from; |
|
1823 to = ranges[key].to; |
|
1824 break; |
|
1825 } |
|
1826 } |
|
1827 } |
|
1828 |
|
1829 // backwards-compat stuff - to be removed in future |
|
1830 if (!ranges[key]) { |
|
1831 axis = coord === "x" ? xaxes[0] : yaxes[0]; |
|
1832 from = ranges[coord + "1"]; |
|
1833 to = ranges[coord + "2"]; |
|
1834 } |
|
1835 |
|
1836 // auto-reverse as an added bonus |
|
1837 if (from != null && to != null && from > to) { |
|
1838 var tmp = from; |
|
1839 from = to; |
|
1840 to = tmp; |
|
1841 } |
|
1842 |
|
1843 return { |
|
1844 from: from, |
|
1845 to: to, |
|
1846 axis: axis |
|
1847 }; |
|
1848 } |
|
1849 |
|
1850 function drawBackground() { |
|
1851 ctx.save(); |
|
1852 ctx.translate(plotOffset.left, plotOffset.top); |
|
1853 |
|
1854 ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); |
|
1855 ctx.fillRect(0, 0, plotWidth, plotHeight); |
|
1856 ctx.restore(); |
|
1857 } |
|
1858 |
|
1859 function drawMarkings() { |
|
1860 // draw markings |
|
1861 var markings = options.grid.markings, |
|
1862 axes; |
|
1863 |
|
1864 if (markings) { |
|
1865 if ($.isFunction(markings)) { |
|
1866 axes = plot.getAxes(); |
|
1867 // xmin etc. is backwards compatibility, to be |
|
1868 // removed in the future |
|
1869 axes.xmin = axes.xaxis.min; |
|
1870 axes.xmax = axes.xaxis.max; |
|
1871 axes.ymin = axes.yaxis.min; |
|
1872 axes.ymax = axes.yaxis.max; |
|
1873 |
|
1874 markings = markings(axes); |
|
1875 } |
|
1876 |
|
1877 var i; |
|
1878 for (i = 0; i < markings.length; ++i) { |
|
1879 var m = markings[i], |
|
1880 xrange = extractRange(m, "x"), |
|
1881 yrange = extractRange(m, "y"); |
|
1882 |
|
1883 // fill in missing |
|
1884 if (xrange.from == null) { |
|
1885 xrange.from = xrange.axis.min; |
|
1886 } |
|
1887 |
|
1888 if (xrange.to == null) { |
|
1889 xrange.to = xrange.axis.max; |
|
1890 } |
|
1891 |
|
1892 if (yrange.from == null) { |
|
1893 yrange.from = yrange.axis.min; |
|
1894 } |
|
1895 |
|
1896 if (yrange.to == null) { |
|
1897 yrange.to = yrange.axis.max; |
|
1898 } |
|
1899 |
|
1900 // clip |
|
1901 if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || |
|
1902 yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) { |
|
1903 continue; |
|
1904 } |
|
1905 |
|
1906 xrange.from = Math.max(xrange.from, xrange.axis.min); |
|
1907 xrange.to = Math.min(xrange.to, xrange.axis.max); |
|
1908 yrange.from = Math.max(yrange.from, yrange.axis.min); |
|
1909 yrange.to = Math.min(yrange.to, yrange.axis.max); |
|
1910 |
|
1911 var xequal = xrange.from === xrange.to, |
|
1912 yequal = yrange.from === yrange.to; |
|
1913 |
|
1914 if (xequal && yequal) { |
|
1915 continue; |
|
1916 } |
|
1917 |
|
1918 // then draw |
|
1919 xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); |
|
1920 xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); |
|
1921 yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); |
|
1922 yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); |
|
1923 |
|
1924 if (xequal || yequal) { |
|
1925 var lineWidth = m.lineWidth || options.grid.markingsLineWidth, |
|
1926 subPixel = lineWidth % 2 ? 0.5 : 0; |
|
1927 ctx.beginPath(); |
|
1928 ctx.strokeStyle = m.color || options.grid.markingsColor; |
|
1929 ctx.lineWidth = lineWidth; |
|
1930 if (xequal) { |
|
1931 ctx.moveTo(xrange.to + subPixel, yrange.from); |
|
1932 ctx.lineTo(xrange.to + subPixel, yrange.to); |
|
1933 } else { |
|
1934 ctx.moveTo(xrange.from, yrange.to + subPixel); |
|
1935 ctx.lineTo(xrange.to, yrange.to + subPixel); |
|
1936 } |
|
1937 ctx.stroke(); |
|
1938 } else { |
|
1939 ctx.fillStyle = m.color || options.grid.markingsColor; |
|
1940 ctx.fillRect(xrange.from, yrange.to, |
|
1941 xrange.to - xrange.from, |
|
1942 yrange.from - yrange.to); |
|
1943 } |
|
1944 } |
|
1945 } |
|
1946 } |
|
1947 |
|
1948 function findEdges(axis) { |
|
1949 var box = axis.box, |
|
1950 x = 0, |
|
1951 y = 0; |
|
1952 |
|
1953 // find the edges |
|
1954 if (axis.direction === "x") { |
|
1955 x = 0; |
|
1956 y = box.top - plotOffset.top + (axis.position === "top" ? box.height : 0); |
|
1957 } else { |
|
1958 y = 0; |
|
1959 x = box.left - plotOffset.left + (axis.position === "left" ? box.width : 0) + axis.boxPosition.centerX; |
|
1960 } |
|
1961 |
|
1962 return { |
|
1963 x: x, |
|
1964 y: y |
|
1965 }; |
|
1966 }; |
|
1967 |
|
1968 function alignPosition(lineWidth, pos) { |
|
1969 return ((lineWidth % 2) !== 0) ? Math.floor(pos) + 0.5 : pos; |
|
1970 }; |
|
1971 |
|
1972 function drawTickBar(axis) { |
|
1973 ctx.lineWidth = 1; |
|
1974 var edges = findEdges(axis), |
|
1975 x = edges.x, |
|
1976 y = edges.y; |
|
1977 |
|
1978 // draw tick bar |
|
1979 if (axis.show) { |
|
1980 var xoff = 0, |
|
1981 yoff = 0; |
|
1982 |
|
1983 ctx.strokeStyle = axis.options.color; |
|
1984 ctx.beginPath(); |
|
1985 if (axis.direction === "x") { |
|
1986 xoff = plotWidth + 1; |
|
1987 } else { |
|
1988 yoff = plotHeight + 1; |
|
1989 } |
|
1990 |
|
1991 if (axis.direction === "x") { |
|
1992 y = alignPosition(ctx.lineWidth, y); |
|
1993 } else { |
|
1994 x = alignPosition(ctx.lineWidth, x); |
|
1995 } |
|
1996 |
|
1997 ctx.moveTo(x, y); |
|
1998 ctx.lineTo(x + xoff, y + yoff); |
|
1999 ctx.stroke(); |
|
2000 } |
|
2001 }; |
|
2002 |
|
2003 function drawTickMarks(axis) { |
|
2004 var t = axis.tickLength, |
|
2005 minorTicks = axis.showMinorTicks, |
|
2006 minorTicksNr = MINOR_TICKS_COUNT_CONSTANT, |
|
2007 edges = findEdges(axis), |
|
2008 x = edges.x, |
|
2009 y = edges.y, |
|
2010 i = 0; |
|
2011 |
|
2012 // draw major tick marks |
|
2013 ctx.strokeStyle = axis.options.color; |
|
2014 ctx.beginPath(); |
|
2015 |
|
2016 for (i = 0; i < axis.ticks.length; ++i) { |
|
2017 var v = axis.ticks[i].v, |
|
2018 xoff = 0, |
|
2019 yoff = 0, |
|
2020 xminor = 0, |
|
2021 yminor = 0, |
|
2022 j; |
|
2023 |
|
2024 if (!isNaN(v) && v >= axis.min && v <= axis.max) { |
|
2025 if (axis.direction === "x") { |
|
2026 x = axis.p2c(v); |
|
2027 yoff = t; |
|
2028 |
|
2029 if (axis.position === "top") { |
|
2030 yoff = -yoff; |
|
2031 } |
|
2032 } else { |
|
2033 y = axis.p2c(v); |
|
2034 xoff = t; |
|
2035 |
|
2036 if (axis.position === "left") { |
|
2037 xoff = -xoff; |
|
2038 } |
|
2039 } |
|
2040 |
|
2041 if (axis.direction === "x") { |
|
2042 x = alignPosition(ctx.lineWidth, x); |
|
2043 } else { |
|
2044 y = alignPosition(ctx.lineWidth, y); |
|
2045 } |
|
2046 |
|
2047 ctx.moveTo(x, y); |
|
2048 ctx.lineTo(x + xoff, y + yoff); |
|
2049 } |
|
2050 |
|
2051 //draw minor tick marks |
|
2052 if (minorTicks === true && i < axis.ticks.length - 1) { |
|
2053 var v1 = axis.ticks[i].v, |
|
2054 v2 = axis.ticks[i + 1].v, |
|
2055 step = (v2 - v1) / (minorTicksNr + 1); |
|
2056 |
|
2057 for (j = 1; j <= minorTicksNr; j++) { |
|
2058 // compute minor tick position |
|
2059 if (axis.direction === "x") { |
|
2060 yminor = t / 2; // minor ticks are half length |
|
2061 x = alignPosition(ctx.lineWidth, axis.p2c(v1 + j * step)) |
|
2062 |
|
2063 if (axis.position === "top") { |
|
2064 yminor = -yminor; |
|
2065 } |
|
2066 |
|
2067 // don't go over the plot borders |
|
2068 if ((x < 0) || (x > plotWidth)) { |
|
2069 continue; |
|
2070 } |
|
2071 } else { |
|
2072 xminor = t / 2; // minor ticks are half length |
|
2073 y = alignPosition(ctx.lineWidth, axis.p2c(v1 + j * step)); |
|
2074 |
|
2075 if (axis.position === "left") { |
|
2076 xminor = -xminor; |
|
2077 } |
|
2078 |
|
2079 // don't go over the plot borders |
|
2080 if ((y < 0) || (y > plotHeight)) { |
|
2081 continue; |
|
2082 } |
|
2083 } |
|
2084 |
|
2085 ctx.moveTo(x, y); |
|
2086 ctx.lineTo(x + xminor, y + yminor); |
|
2087 } |
|
2088 } |
|
2089 } |
|
2090 |
|
2091 ctx.stroke(); |
|
2092 }; |
|
2093 |
|
2094 function drawGridLines(axis) { |
|
2095 // check if the line will be overlapped with a border |
|
2096 var overlappedWithBorder = function (value) { |
|
2097 var bw = options.grid.borderWidth; |
|
2098 return (((typeof bw === "object" && bw[axis.position] > 0) || bw > 0) && (value === axis.min || value === axis.max)); |
|
2099 }; |
|
2100 |
|
2101 ctx.strokeStyle = options.grid.tickColor; |
|
2102 ctx.beginPath(); |
|
2103 var i; |
|
2104 for (i = 0; i < axis.ticks.length; ++i) { |
|
2105 var v = axis.ticks[i].v, |
|
2106 xoff = 0, |
|
2107 yoff = 0, |
|
2108 x = 0, |
|
2109 y = 0; |
|
2110 |
|
2111 if (isNaN(v) || v < axis.min || v > axis.max) continue; |
|
2112 |
|
2113 // skip those lying on the axes if we got a border |
|
2114 if (overlappedWithBorder(v)) continue; |
|
2115 |
|
2116 if (axis.direction === "x") { |
|
2117 x = axis.p2c(v); |
|
2118 y = plotHeight; |
|
2119 yoff = -plotHeight; |
|
2120 } else { |
|
2121 x = 0; |
|
2122 y = axis.p2c(v); |
|
2123 xoff = plotWidth; |
|
2124 } |
|
2125 |
|
2126 if (axis.direction === "x") { |
|
2127 x = alignPosition(ctx.lineWidth, x); |
|
2128 } else { |
|
2129 y = alignPosition(ctx.lineWidth, y); |
|
2130 } |
|
2131 |
|
2132 ctx.moveTo(x, y); |
|
2133 ctx.lineTo(x + xoff, y + yoff); |
|
2134 } |
|
2135 |
|
2136 ctx.stroke(); |
|
2137 }; |
|
2138 |
|
2139 function drawBorder() { |
|
2140 // If either borderWidth or borderColor is an object, then draw the border |
|
2141 // line by line instead of as one rectangle |
|
2142 var bw = options.grid.borderWidth, |
|
2143 bc = options.grid.borderColor; |
|
2144 |
|
2145 if (typeof bw === "object" || typeof bc === "object") { |
|
2146 if (typeof bw !== "object") { |
|
2147 bw = { |
|
2148 top: bw, |
|
2149 right: bw, |
|
2150 bottom: bw, |
|
2151 left: bw |
|
2152 }; |
|
2153 } |
|
2154 if (typeof bc !== "object") { |
|
2155 bc = { |
|
2156 top: bc, |
|
2157 right: bc, |
|
2158 bottom: bc, |
|
2159 left: bc |
|
2160 }; |
|
2161 } |
|
2162 |
|
2163 if (bw.top > 0) { |
|
2164 ctx.strokeStyle = bc.top; |
|
2165 ctx.lineWidth = bw.top; |
|
2166 ctx.beginPath(); |
|
2167 ctx.moveTo(0 - bw.left, 0 - bw.top / 2); |
|
2168 ctx.lineTo(plotWidth, 0 - bw.top / 2); |
|
2169 ctx.stroke(); |
|
2170 } |
|
2171 |
|
2172 if (bw.right > 0) { |
|
2173 ctx.strokeStyle = bc.right; |
|
2174 ctx.lineWidth = bw.right; |
|
2175 ctx.beginPath(); |
|
2176 ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); |
|
2177 ctx.lineTo(plotWidth + bw.right / 2, plotHeight); |
|
2178 ctx.stroke(); |
|
2179 } |
|
2180 |
|
2181 if (bw.bottom > 0) { |
|
2182 ctx.strokeStyle = bc.bottom; |
|
2183 ctx.lineWidth = bw.bottom; |
|
2184 ctx.beginPath(); |
|
2185 ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); |
|
2186 ctx.lineTo(0, plotHeight + bw.bottom / 2); |
|
2187 ctx.stroke(); |
|
2188 } |
|
2189 |
|
2190 if (bw.left > 0) { |
|
2191 ctx.strokeStyle = bc.left; |
|
2192 ctx.lineWidth = bw.left; |
|
2193 ctx.beginPath(); |
|
2194 ctx.moveTo(0 - bw.left / 2, plotHeight + bw.bottom); |
|
2195 ctx.lineTo(0 - bw.left / 2, 0); |
|
2196 ctx.stroke(); |
|
2197 } |
|
2198 } else { |
|
2199 ctx.lineWidth = bw; |
|
2200 ctx.strokeStyle = options.grid.borderColor; |
|
2201 ctx.strokeRect(-bw / 2, -bw / 2, plotWidth + bw, plotHeight + bw); |
|
2202 } |
|
2203 }; |
|
2204 |
|
2205 function drawGrid() { |
|
2206 var axes, bw; |
|
2207 |
|
2208 ctx.save(); |
|
2209 ctx.translate(plotOffset.left, plotOffset.top); |
|
2210 |
|
2211 drawMarkings(); |
|
2212 |
|
2213 axes = allAxes(); |
|
2214 bw = options.grid.borderWidth; |
|
2215 |
|
2216 for (var j = 0; j < axes.length; ++j) { |
|
2217 var axis = axes[j]; |
|
2218 |
|
2219 if (!axis.show) { |
|
2220 continue; |
|
2221 } |
|
2222 |
|
2223 drawTickBar(axis); |
|
2224 if (axis.showTicks === true) { |
|
2225 drawTickMarks(axis); |
|
2226 } |
|
2227 |
|
2228 if (axis.gridLines === true) { |
|
2229 drawGridLines(axis, bw); |
|
2230 } |
|
2231 } |
|
2232 |
|
2233 // draw border |
|
2234 if (bw) { |
|
2235 drawBorder(); |
|
2236 } |
|
2237 |
|
2238 ctx.restore(); |
|
2239 } |
|
2240 |
|
2241 function drawAxisLabels() { |
|
2242 $.each(allAxes(), function(_, axis) { |
|
2243 var box = axis.box, |
|
2244 legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", |
|
2245 layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, |
|
2246 font = axis.options.font || "flot-tick-label tickLabel", |
|
2247 i, x, y, halign, valign, info, |
|
2248 margin = 3, |
|
2249 nullBox = {x: NaN, y: NaN, width: NaN, height: NaN}, newLabelBox, labelBoxes = [], |
|
2250 overlapping = function(x11, y11, x12, y12, x21, y21, x22, y22) { |
|
2251 return ((x11 <= x21 && x21 <= x12) || (x21 <= x11 && x11 <= x22)) && |
|
2252 ((y11 <= y21 && y21 <= y12) || (y21 <= y11 && y11 <= y22)); |
|
2253 }, |
|
2254 overlapsOtherLabels = function(newLabelBox, previousLabelBoxes) { |
|
2255 return previousLabelBoxes.some(function(labelBox) { |
|
2256 return overlapping( |
|
2257 newLabelBox.x, newLabelBox.y, newLabelBox.x + newLabelBox.width, newLabelBox.y + newLabelBox.height, |
|
2258 labelBox.x, labelBox.y, labelBox.x + labelBox.width, labelBox.y + labelBox.height); |
|
2259 }); |
|
2260 }, |
|
2261 drawAxisLabel = function (tick, labelBoxes) { |
|
2262 if (!tick || !tick.label || tick.v < axis.min || tick.v > axis.max) { |
|
2263 return nullBox; |
|
2264 } |
|
2265 |
|
2266 info = surface.getTextInfo(layer, tick.label, font); |
|
2267 |
|
2268 if (axis.direction === "x") { |
|
2269 halign = "center"; |
|
2270 x = plotOffset.left + axis.p2c(tick.v); |
|
2271 if (axis.position === "bottom") { |
|
2272 y = box.top + box.padding - axis.boxPosition.centerY; |
|
2273 } else { |
|
2274 y = box.top + box.height - box.padding + axis.boxPosition.centerY; |
|
2275 valign = "bottom"; |
|
2276 } |
|
2277 newLabelBox = {x: x - info.width / 2 - margin, y: y - margin, width: info.width + 2 * margin, height: info.height + 2 * margin}; |
|
2278 } else { |
|
2279 valign = "middle"; |
|
2280 y = plotOffset.top + axis.p2c(tick.v); |
|
2281 if (axis.position === "left") { |
|
2282 x = box.left + box.width - box.padding - axis.boxPosition.centerX; |
|
2283 halign = "right"; |
|
2284 } else { |
|
2285 x = box.left + box.padding + axis.boxPosition.centerX; |
|
2286 } |
|
2287 newLabelBox = {x: x - info.width / 2 - margin, y: y - margin, width: info.width + 2 * margin, height: info.height + 2 * margin}; |
|
2288 } |
|
2289 |
|
2290 if (overlapsOtherLabels(newLabelBox, labelBoxes)) { |
|
2291 return nullBox; |
|
2292 } |
|
2293 |
|
2294 surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); |
|
2295 |
|
2296 return newLabelBox; |
|
2297 }; |
|
2298 |
|
2299 // Remove text before checking for axis.show and ticks.length; |
|
2300 // otherwise plugins, like flot-tickrotor, that draw their own |
|
2301 // tick labels will end up with both theirs and the defaults. |
|
2302 |
|
2303 surface.removeText(layer); |
|
2304 |
|
2305 executeHooks(hooks.drawAxis, [axis, surface]); |
|
2306 |
|
2307 if (!axis.show) { |
|
2308 return; |
|
2309 } |
|
2310 |
|
2311 switch (axis.options.showTickLabels) { |
|
2312 case 'none': |
|
2313 break; |
|
2314 case 'endpoints': |
|
2315 labelBoxes.push(drawAxisLabel(axis.ticks[0], labelBoxes)); |
|
2316 labelBoxes.push(drawAxisLabel(axis.ticks[axis.ticks.length - 1], labelBoxes)); |
|
2317 break; |
|
2318 case 'major': |
|
2319 labelBoxes.push(drawAxisLabel(axis.ticks[0], labelBoxes)); |
|
2320 labelBoxes.push(drawAxisLabel(axis.ticks[axis.ticks.length - 1], labelBoxes)); |
|
2321 for (i = 1; i < axis.ticks.length - 1; ++i) { |
|
2322 labelBoxes.push(drawAxisLabel(axis.ticks[i], labelBoxes)); |
|
2323 } |
|
2324 break; |
|
2325 case 'all': |
|
2326 labelBoxes.push(drawAxisLabel(axis.ticks[0], [])); |
|
2327 labelBoxes.push(drawAxisLabel(axis.ticks[axis.ticks.length - 1], labelBoxes)); |
|
2328 for (i = 1; i < axis.ticks.length - 1; ++i) { |
|
2329 labelBoxes.push(drawAxisLabel(axis.ticks[i], labelBoxes)); |
|
2330 } |
|
2331 break; |
|
2332 } |
|
2333 }); |
|
2334 } |
|
2335 |
|
2336 function drawSeries(series) { |
|
2337 if (series.lines.show) { |
|
2338 $.plot.drawSeries.drawSeriesLines(series, ctx, plotOffset, plotWidth, plotHeight, plot.drawSymbol, getColorOrGradient); |
|
2339 } |
|
2340 |
|
2341 if (series.bars.show) { |
|
2342 $.plot.drawSeries.drawSeriesBars(series, ctx, plotOffset, plotWidth, plotHeight, plot.drawSymbol, getColorOrGradient); |
|
2343 } |
|
2344 |
|
2345 if (series.points.show) { |
|
2346 $.plot.drawSeries.drawSeriesPoints(series, ctx, plotOffset, plotWidth, plotHeight, plot.drawSymbol, getColorOrGradient); |
|
2347 } |
|
2348 } |
|
2349 |
|
2350 function computeRangeForDataSeries(series, force, isValid) { |
|
2351 var points = series.datapoints.points, |
|
2352 ps = series.datapoints.pointsize, |
|
2353 format = series.datapoints.format, |
|
2354 topSentry = Number.POSITIVE_INFINITY, |
|
2355 bottomSentry = Number.NEGATIVE_INFINITY, |
|
2356 range = { |
|
2357 xmin: topSentry, |
|
2358 ymin: topSentry, |
|
2359 xmax: bottomSentry, |
|
2360 ymax: bottomSentry |
|
2361 }; |
|
2362 |
|
2363 for (var j = 0; j < points.length; j += ps) { |
|
2364 if (points[j] === null) { |
|
2365 continue; |
|
2366 } |
|
2367 |
|
2368 if (typeof (isValid) === 'function' && !isValid(points[j])) { |
|
2369 continue; |
|
2370 } |
|
2371 |
|
2372 for (var m = 0; m < ps; ++m) { |
|
2373 var val = points[j + m], |
|
2374 f = format[m]; |
|
2375 if (f === null || f === undefined) { |
|
2376 continue; |
|
2377 } |
|
2378 |
|
2379 if (typeof (isValid) === 'function' && !isValid(val)) { |
|
2380 continue; |
|
2381 } |
|
2382 |
|
2383 if ((!force && !f.computeRange) || val === Infinity || val === -Infinity) { |
|
2384 continue; |
|
2385 } |
|
2386 |
|
2387 if (f.x === true) { |
|
2388 if (val < range.xmin) { |
|
2389 range.xmin = val; |
|
2390 } |
|
2391 |
|
2392 if (val > range.xmax) { |
|
2393 range.xmax = val; |
|
2394 } |
|
2395 } |
|
2396 |
|
2397 if (f.y === true) { |
|
2398 if (val < range.ymin) { |
|
2399 range.ymin = val; |
|
2400 } |
|
2401 |
|
2402 if (val > range.ymax) { |
|
2403 range.ymax = val; |
|
2404 } |
|
2405 } |
|
2406 } |
|
2407 } |
|
2408 |
|
2409 return range; |
|
2410 }; |
|
2411 |
|
2412 function adjustSeriesDataRange(series, range) { |
|
2413 if (series.bars.show) { |
|
2414 // make sure we got room for the bar on the dancing floor |
|
2415 var delta; |
|
2416 |
|
2417 // update bar width if needed |
|
2418 var useAbsoluteBarWidth = series.bars.barWidth[1]; |
|
2419 if (series.datapoints && series.datapoints.points && !useAbsoluteBarWidth) { |
|
2420 computeBarWidth(series); |
|
2421 } |
|
2422 |
|
2423 var barWidth = series.bars.barWidth[0] || series.bars.barWidth; |
|
2424 switch (series.bars.align) { |
|
2425 case "left": |
|
2426 delta = 0; |
|
2427 break; |
|
2428 case "right": |
|
2429 delta = -barWidth; |
|
2430 break; |
|
2431 default: |
|
2432 delta = -barWidth / 2; |
|
2433 } |
|
2434 |
|
2435 if (series.bars.horizontal) { |
|
2436 range.ymin += delta; |
|
2437 range.ymax += delta + barWidth; |
|
2438 } |
|
2439 else { |
|
2440 range.xmin += delta; |
|
2441 range.xmax += delta + barWidth; |
|
2442 } |
|
2443 } |
|
2444 |
|
2445 if ((series.bars.show && series.bars.zero) || (series.lines.show && series.lines.zero)) { |
|
2446 var ps = series.datapoints.pointsize; |
|
2447 |
|
2448 // make sure the 0 point is included in the computed y range when requested |
|
2449 if (ps <= 2) { |
|
2450 /*if ps > 0 the points were already taken into account for autoScale */ |
|
2451 range.ymin = Math.min(0, range.ymin); |
|
2452 range.ymax = Math.max(0, range.ymax); |
|
2453 } |
|
2454 } |
|
2455 |
|
2456 return range; |
|
2457 }; |
|
2458 |
|
2459 function computeBarWidth(series) { |
|
2460 var xValues = []; |
|
2461 var pointsize = series.datapoints.pointsize, minDistance = Number.MAX_VALUE; |
|
2462 |
|
2463 if (series.datapoints.points.length <= pointsize) { |
|
2464 minDistance = 1; |
|
2465 } |
|
2466 |
|
2467 var start = series.bars.horizontal ? 1 : 0; |
|
2468 for (var j = start; j < series.datapoints.points.length; j += pointsize) { |
|
2469 if (isFinite(series.datapoints.points[j]) && series.datapoints.points[j] !== null) { |
|
2470 xValues.push(series.datapoints.points[j]); |
|
2471 } |
|
2472 } |
|
2473 |
|
2474 function onlyUnique(value, index, self) { |
|
2475 return self.indexOf(value) === index; |
|
2476 } |
|
2477 |
|
2478 xValues = xValues.filter( onlyUnique ); |
|
2479 xValues.sort(function(a, b){return a - b}); |
|
2480 |
|
2481 for (var j = 1; j < xValues.length; j++) { |
|
2482 var distance = Math.abs(xValues[j] - xValues[j - 1]); |
|
2483 if (distance < minDistance && isFinite(distance)) { |
|
2484 minDistance = distance; |
|
2485 } |
|
2486 } |
|
2487 |
|
2488 if (typeof series.bars.barWidth === "number") { |
|
2489 series.bars.barWidth = series.bars.barWidth * minDistance; |
|
2490 } else { |
|
2491 series.bars.barWidth[0] = series.bars.barWidth[0] * minDistance; |
|
2492 } |
|
2493 } |
|
2494 |
|
2495 // returns the data item the mouse is over/ the cursor is closest to, or null if none is found |
|
2496 function findNearbyItem(mouseX, mouseY, seriesFilter, radius, computeDistance) { |
|
2497 var i, j, |
|
2498 item = null, |
|
2499 smallestDistance = radius * radius + 1; |
|
2500 |
|
2501 for (var i = series.length - 1; i >= 0; --i) { |
|
2502 if (!seriesFilter(i)) continue; |
|
2503 |
|
2504 var s = series[i]; |
|
2505 if (!s.datapoints) return; |
|
2506 |
|
2507 if (s.lines.show || s.points.show) { |
|
2508 var found = findNearbyPoint(s, mouseX, mouseY, radius, smallestDistance, computeDistance); |
|
2509 if (found) { |
|
2510 smallestDistance = found.distance; |
|
2511 item = [i, found.dataIndex]; |
|
2512 } |
|
2513 } |
|
2514 |
|
2515 if (s.bars.show && !item) { // no other point can be nearby |
|
2516 var foundIndex = findNearbyBar(s, mouseX, mouseY); |
|
2517 if (foundIndex >= 0) item = [i, foundIndex]; |
|
2518 } |
|
2519 } |
|
2520 |
|
2521 if (item) { |
|
2522 i = item[0]; |
|
2523 j = item[1]; |
|
2524 var ps = series[i].datapoints.pointsize; |
|
2525 |
|
2526 return { |
|
2527 datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), |
|
2528 dataIndex: j, |
|
2529 series: series[i], |
|
2530 seriesIndex: i |
|
2531 }; |
|
2532 } |
|
2533 |
|
2534 return null; |
|
2535 } |
|
2536 |
|
2537 function findNearbyPoint (series, mouseX, mouseY, maxDistance, smallestDistance, computeDistance) { |
|
2538 var mx = series.xaxis.c2p(mouseX), |
|
2539 my = series.yaxis.c2p(mouseY), |
|
2540 maxx = maxDistance / series.xaxis.scale, |
|
2541 maxy = maxDistance / series.yaxis.scale, |
|
2542 points = series.datapoints.points, |
|
2543 ps = series.datapoints.pointsize; |
|
2544 |
|
2545 // with inverse transforms, we can't use the maxx/maxy |
|
2546 // optimization, sadly |
|
2547 if (series.xaxis.options.inverseTransform) { |
|
2548 maxx = Number.MAX_VALUE; |
|
2549 } |
|
2550 |
|
2551 if (series.yaxis.options.inverseTransform) { |
|
2552 maxy = Number.MAX_VALUE; |
|
2553 } |
|
2554 |
|
2555 var found = null; |
|
2556 for (var j = 0; j < points.length; j += ps) { |
|
2557 var x = points[j]; |
|
2558 var y = points[j + 1]; |
|
2559 if (x == null) { |
|
2560 continue; |
|
2561 } |
|
2562 |
|
2563 if (x - mx > maxx || x - mx < -maxx || |
|
2564 y - my > maxy || y - my < -maxy) { |
|
2565 continue; |
|
2566 } |
|
2567 |
|
2568 // We have to calculate distances in pixels, not in |
|
2569 // data units, because the scales of the axes may be different |
|
2570 var dx = Math.abs(series.xaxis.p2c(x) - mouseX); |
|
2571 var dy = Math.abs(series.yaxis.p2c(y) - mouseY); |
|
2572 var dist = computeDistance ? computeDistance(dx, dy) : dx * dx + dy * dy; |
|
2573 |
|
2574 // use <= to ensure last point takes precedence |
|
2575 // (last generally means on top of) |
|
2576 if (dist < smallestDistance) { |
|
2577 smallestDistance = dist; |
|
2578 found = { dataIndex: j / ps, distance: dist }; |
|
2579 } |
|
2580 } |
|
2581 |
|
2582 return found; |
|
2583 } |
|
2584 |
|
2585 function findNearbyBar (series, mouseX, mouseY) { |
|
2586 var barLeft, barRight, |
|
2587 barWidth = series.bars.barWidth[0] || series.bars.barWidth, |
|
2588 mx = series.xaxis.c2p(mouseX), |
|
2589 my = series.yaxis.c2p(mouseY), |
|
2590 points = series.datapoints.points, |
|
2591 ps = series.datapoints.pointsize; |
|
2592 |
|
2593 switch (series.bars.align) { |
|
2594 case "left": |
|
2595 barLeft = 0; |
|
2596 break; |
|
2597 case "right": |
|
2598 barLeft = -barWidth; |
|
2599 break; |
|
2600 default: |
|
2601 barLeft = -barWidth / 2; |
|
2602 } |
|
2603 |
|
2604 barRight = barLeft + barWidth; |
|
2605 |
|
2606 var fillTowards = series.bars.fillTowards || 0; |
|
2607 var bottom = fillTowards > series.yaxis.min ? Math.min(series.yaxis.max, fillTowards) : series.yaxis.min; |
|
2608 |
|
2609 var foundIndex = -1; |
|
2610 for (var j = 0; j < points.length; j += ps) { |
|
2611 var x = points[j], y = points[j + 1]; |
|
2612 if (x == null) |
|
2613 continue; |
|
2614 |
|
2615 // for a bar graph, the cursor must be inside the bar |
|
2616 if (series.bars.horizontal ? |
|
2617 (mx <= Math.max(bottom, x) && mx >= Math.min(bottom, x) && |
|
2618 my >= y + barLeft && my <= y + barRight) : |
|
2619 (mx >= x + barLeft && mx <= x + barRight && |
|
2620 my >= Math.min(bottom, y) && my <= Math.max(bottom, y))) |
|
2621 foundIndex = j / ps; |
|
2622 } |
|
2623 |
|
2624 return foundIndex; |
|
2625 } |
|
2626 |
|
2627 function findNearbyInterpolationPoint(posX, posY, seriesFilter) { |
|
2628 var i, j, dist, dx, dy, ps, |
|
2629 item, |
|
2630 smallestDistance = Number.MAX_VALUE; |
|
2631 |
|
2632 for (i = 0; i < series.length; ++i) { |
|
2633 if (!seriesFilter(i)) { |
|
2634 continue; |
|
2635 } |
|
2636 var points = series[i].datapoints.points; |
|
2637 ps = series[i].datapoints.pointsize; |
|
2638 |
|
2639 // if the data is coming from positive -> negative, reverse the comparison |
|
2640 const comparer = points[points.length - ps] < points[0] |
|
2641 ? function (x1, x2) { return x1 > x2 } |
|
2642 : function (x1, x2) { return x2 > x1 }; |
|
2643 |
|
2644 // do not interpolate outside the bounds of the data. |
|
2645 if (comparer(posX, points[0])) { |
|
2646 continue; |
|
2647 } |
|
2648 |
|
2649 // Find the nearest points, x-wise |
|
2650 for (j = ps; j < points.length; j += ps) { |
|
2651 if (comparer(posX, points[j])) { |
|
2652 break; |
|
2653 } |
|
2654 } |
|
2655 |
|
2656 // Now Interpolate |
|
2657 var y, |
|
2658 p1x = points[j - ps], |
|
2659 p1y = points[j - ps + 1], |
|
2660 p2x = points[j], |
|
2661 p2y = points[j + 1]; |
|
2662 |
|
2663 if ((p1x === undefined) || (p2x === undefined) || |
|
2664 (p1y === undefined) || (p2y === undefined)) { |
|
2665 continue; |
|
2666 } |
|
2667 |
|
2668 if (p1x === p2x) { |
|
2669 y = p2y |
|
2670 } else { |
|
2671 y = p1y + (p2y - p1y) * (posX - p1x) / (p2x - p1x); |
|
2672 } |
|
2673 |
|
2674 posY = y; |
|
2675 |
|
2676 dx = Math.abs(series[i].xaxis.p2c(p2x) - posX); |
|
2677 dy = Math.abs(series[i].yaxis.p2c(p2y) - posY); |
|
2678 dist = dx * dx + dy * dy; |
|
2679 |
|
2680 if (dist < smallestDistance) { |
|
2681 smallestDistance = dist; |
|
2682 item = [posX, posY, i, j]; |
|
2683 } |
|
2684 } |
|
2685 |
|
2686 if (item) { |
|
2687 i = item[2]; |
|
2688 j = item[3]; |
|
2689 ps = series[i].datapoints.pointsize; |
|
2690 points = series[i].datapoints.points; |
|
2691 p1x = points[j - ps]; |
|
2692 p1y = points[j - ps + 1]; |
|
2693 p2x = points[j]; |
|
2694 p2y = points[j + 1]; |
|
2695 |
|
2696 return { |
|
2697 datapoint: [item[0], item[1]], |
|
2698 leftPoint: [p1x, p1y], |
|
2699 rightPoint: [p2x, p2y], |
|
2700 seriesIndex: i |
|
2701 }; |
|
2702 } |
|
2703 |
|
2704 return null; |
|
2705 } |
|
2706 |
|
2707 function triggerRedrawOverlay() { |
|
2708 var t = options.interaction.redrawOverlayInterval; |
|
2709 if (t === -1) { // skip event queue |
|
2710 drawOverlay(); |
|
2711 return; |
|
2712 } |
|
2713 |
|
2714 if (!redrawTimeout) { |
|
2715 redrawTimeout = setTimeout(function() { |
|
2716 drawOverlay(plot); |
|
2717 }, t); |
|
2718 } |
|
2719 } |
|
2720 |
|
2721 function drawOverlay(plot) { |
|
2722 redrawTimeout = null; |
|
2723 |
|
2724 if (!octx) { |
|
2725 return; |
|
2726 } |
|
2727 overlay.clear(); |
|
2728 executeHooks(hooks.drawOverlay, [octx, overlay]); |
|
2729 var event = new CustomEvent('onDrawingDone'); |
|
2730 plot.getEventHolder().dispatchEvent(event); |
|
2731 plot.getPlaceholder().trigger('drawingdone'); |
|
2732 } |
|
2733 |
|
2734 function getColorOrGradient(spec, bottom, top, defaultColor) { |
|
2735 if (typeof spec === "string") { |
|
2736 return spec; |
|
2737 } else { |
|
2738 // assume this is a gradient spec; IE currently only |
|
2739 // supports a simple vertical gradient properly, so that's |
|
2740 // what we support too |
|
2741 var gradient = ctx.createLinearGradient(0, top, 0, bottom); |
|
2742 |
|
2743 for (var i = 0, l = spec.colors.length; i < l; ++i) { |
|
2744 var c = spec.colors[i]; |
|
2745 if (typeof c !== "string") { |
|
2746 var co = $.color.parse(defaultColor); |
|
2747 if (c.brightness != null) { |
|
2748 co = co.scale('rgb', c.brightness); |
|
2749 } |
|
2750 |
|
2751 if (c.opacity != null) { |
|
2752 co.a *= c.opacity; |
|
2753 } |
|
2754 |
|
2755 c = co.toString(); |
|
2756 } |
|
2757 gradient.addColorStop(i / (l - 1), c); |
|
2758 } |
|
2759 |
|
2760 return gradient; |
|
2761 } |
|
2762 } |
|
2763 } |
|
2764 |
|
2765 // Add the plot function to the top level of the jQuery object |
|
2766 |
|
2767 $.plot = function(placeholder, data, options) { |
|
2768 var plot = new Plot($(placeholder), data, options, $.plot.plugins); |
|
2769 return plot; |
|
2770 }; |
|
2771 |
|
2772 $.plot.version = "3.0.0"; |
|
2773 |
|
2774 $.plot.plugins = []; |
|
2775 |
|
2776 // Also add the plot function as a chainable property |
|
2777 $.fn.plot = function(data, options) { |
|
2778 return this.each(function() { |
|
2779 $.plot(this, data, options); |
|
2780 }); |
|
2781 }; |
|
2782 |
|
2783 $.plot.linearTickGenerator = defaultTickGenerator; |
|
2784 $.plot.defaultTickFormatter = defaultTickFormatter; |
|
2785 $.plot.expRepTickFormatter = expRepTickFormatter; |
|
2786 })(jQuery); |