UNPKG

105 kBJavaScriptView Raw
1/* Javascript plotting library for jQuery, version 3.0.0.
2
3Copyright (c) 2007-2014 IOLA and Ole Laursen.
4Licensed 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);