prometheus

Форк
0
3406 строк · 107.2 Кб
1
/*
2
SPDX-License-Identifier: MIT
3
Source: https://github.com/grafana/grafana/blob/main/public/vendor/flot/jquery.flot.js
4
*/
5

6
/* eslint-disable prefer-spread */
7
/* eslint-disable no-loop-func */
8
/* eslint-disable @typescript-eslint/no-this-alias */
9
/* eslint-disable no-redeclare */
10
/* eslint-disable no-useless-escape */
11
/* eslint-disable prefer-const */
12
/* eslint-disable @typescript-eslint/explicit-function-return-type */
13
/* eslint-disable @typescript-eslint/no-use-before-define */
14
/* eslint-disable eqeqeq */
15
/* eslint-disable no-var */
16
/* Javascript plotting library for jQuery, version 0.8.3.
17

18
Copyright (c) 2007-2014 IOLA and Ole Laursen.
19
Licensed under the MIT license.
20

21
*/
22

23
// first an inline dependency, jquery.colorhelpers.js, we inline it here
24
// for convenience
25

26
/* Plugin for jQuery for working with colors.
27
 *
28
 * Version 1.1.
29
 *
30
 * Inspiration from jQuery color animation plugin by John Resig.
31
 *
32
 * Released under the MIT license by Ole Laursen, October 2009.
33
 *
34
 * Examples:
35
 *
36
 *   $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
37
 *   var c = $.color.extract($("#mydiv"), 'background-color');
38
 *   console.log(c.r, c.g, c.b, c.a);
39
 *   $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
40
 *
41
 * Note that .scale() and .add() return the same modified object
42
 * instead of making a new one.
43
 *
44
 * V. 1.1: Fix error handling so e.g. parsing an empty string does
45
 * produce a color rather than just crashing.
46
 */
47

48
(function($) {
49
  $.color = {};
50
  $.color.make = function(r, g, b, a) {
51
    var o = {};
52
    o.r = r || 0;
53
    o.g = g || 0;
54
    o.b = b || 0;
55
    o.a = a != null ? a : 1;
56
    o.add = function(c, d) {
57
      for (var i = 0; i < c.length; ++i) o[c.charAt(i)] += d;
58
      return o.normalize();
59
    };
60
    o.scale = function(c, f) {
61
      for (var i = 0; i < c.length; ++i) o[c.charAt(i)] *= f;
62
      return o.normalize();
63
    };
64
    o.toString = function() {
65
      if (o.a >= 1) {
66
        return 'rgb(' + [o.r, o.g, o.b].join(',') + ')';
67
      } else {
68
        return 'rgba(' + [o.r, o.g, o.b, o.a].join(',') + ')';
69
      }
70
    };
71
    o.normalize = function() {
72
      function clamp(min, value, max) {
73
        return value < min ? min : value > max ? max : value;
74
      }
75
      o.r = clamp(0, parseInt(o.r), 255);
76
      o.g = clamp(0, parseInt(o.g), 255);
77
      o.b = clamp(0, parseInt(o.b), 255);
78
      o.a = clamp(0, o.a, 1);
79
      return o;
80
    };
81
    o.clone = function() {
82
      return $.color.make(o.r, o.b, o.g, o.a);
83
    };
84
    return o.normalize();
85
  };
86
  $.color.extract = function(elem, css) {
87
    var c;
88
    do {
89
      c = elem.css(css).toLowerCase();
90
      if (c != '' && c != 'transparent') break;
91
      elem = elem.parent();
92
    } while (elem.length && !$.nodeName(elem.get(0), 'body'));
93
    if (c == 'rgba(0, 0, 0, 0)') c = 'transparent';
94
    return $.color.parse(c);
95
  };
96
  $.color.parse = function(str) {
97
    var res,
98
      m = $.color.make;
99
    if ((res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)))
100
      return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10));
101
    if ((res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)))
102
      return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4]));
103
    if ((res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)))
104
      return m(parseFloat(res[1]) * 2.55, parseFloat(res[2]) * 2.55, parseFloat(res[3]) * 2.55);
105
    if (
106
      (res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(
107
        str
108
      ))
109
    )
110
      return m(parseFloat(res[1]) * 2.55, parseFloat(res[2]) * 2.55, parseFloat(res[3]) * 2.55, parseFloat(res[4]));
111
    if ((res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)))
112
      return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16));
113
    if ((res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)))
114
      return m(parseInt(res[1] + res[1], 16), parseInt(res[2] + res[2], 16), parseInt(res[3] + res[3], 16));
115
    var name = $.trim(str).toLowerCase();
116
    if (name == 'transparent') return m(255, 255, 255, 0);
117
    else {
118
      res = lookupColors[name] || [0, 0, 0];
119
      return m(res[0], res[1], res[2]);
120
    }
121
  };
122
  var lookupColors = {
123
    aqua: [0, 255, 255],
124
    azure: [240, 255, 255],
125
    beige: [245, 245, 220],
126
    black: [0, 0, 0],
127
    blue: [0, 0, 255],
128
    brown: [165, 42, 42],
129
    cyan: [0, 255, 255],
130
    darkblue: [0, 0, 139],
131
    darkcyan: [0, 139, 139],
132
    darkgrey: [169, 169, 169],
133
    darkgreen: [0, 100, 0],
134
    darkkhaki: [189, 183, 107],
135
    darkmagenta: [139, 0, 139],
136
    darkolivegreen: [85, 107, 47],
137
    darkorange: [255, 140, 0],
138
    darkorchid: [153, 50, 204],
139
    darkred: [139, 0, 0],
140
    darksalmon: [233, 150, 122],
141
    darkviolet: [148, 0, 211],
142
    fuchsia: [255, 0, 255],
143
    gold: [255, 215, 0],
144
    green: [0, 128, 0],
145
    indigo: [75, 0, 130],
146
    khaki: [240, 230, 140],
147
    lightblue: [173, 216, 230],
148
    lightcyan: [224, 255, 255],
149
    lightgreen: [144, 238, 144],
150
    lightgrey: [211, 211, 211],
151
    lightpink: [255, 182, 193],
152
    lightyellow: [255, 255, 224],
153
    lime: [0, 255, 0],
154
    magenta: [255, 0, 255],
155
    maroon: [128, 0, 0],
156
    navy: [0, 0, 128],
157
    olive: [128, 128, 0],
158
    orange: [255, 165, 0],
159
    pink: [255, 192, 203],
160
    purple: [128, 0, 128],
161
    violet: [128, 0, 128],
162
    red: [255, 0, 0],
163
    silver: [192, 192, 192],
164
    white: [255, 255, 255],
165
    yellow: [255, 255, 0],
166
  };
167
})(window.jQuery);
168

169
// the actual Flot code
170
(function($) {
171
  // Cache the prototype hasOwnProperty for faster access
172

173
  let hasOwnProperty = Object.prototype.hasOwnProperty;
174

175
  // A shim to provide 'detach' to jQuery versions prior to 1.4.  Using a DOM
176
  // operation produces the same effect as detach, i.e. removing the element
177
  // without touching its jQuery data.
178

179
  // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+.
180

181
  if (!$.fn.detach) {
182
    $.fn.detach = function() {
183
      return this.each(function() {
184
        if (this.parentNode) {
185
          this.parentNode.removeChild(this);
186
        }
187
      });
188
    };
189
  }
190

191
  ///////////////////////////////////////////////////////////////////////////
192
  // The Canvas object is a wrapper around an HTML5 <canvas> tag.
193
  //
194
  // @constructor
195
  // @param {string} cls List of classes to apply to the canvas.
196
  // @param {element} container Element onto which to append the canvas.
197
  //
198
  // Requiring a container is a little iffy, but unfortunately canvas
199
  // operations don't work unless the canvas is attached to the DOM.
200

201
  function Canvas(cls, container) {
202
    var element = container.children('.' + cls)[0];
203

204
    if (element == null) {
205
      element = document.createElement('canvas');
206
      element.className = cls;
207

208
      $(element)
209
        .css({ direction: 'ltr', position: 'absolute', left: 0, top: 0 })
210
        .appendTo(container);
211

212
      // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas
213

214
      if (!element.getContext) {
215
        if (window.G_vmlCanvasManager) {
216
          element = window.G_vmlCanvasManager.initElement(element);
217
        } else {
218
          throw new Error(
219
            "Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."
220
          );
221
        }
222
      }
223
    }
224

225
    this.element = element;
226

227
    var context = (this.context = element.getContext('2d'));
228

229
    // Determine the screen's ratio of physical to device-independent
230
    // pixels.  This is the ratio between the canvas width that the browser
231
    // advertises and the number of pixels actually present in that space.
232

233
    // The iPhone 4, for example, has a device-independent width of 320px,
234
    // but its screen is actually 640px wide.  It therefore has a pixel
235
    // ratio of 2, while most normal devices have a ratio of 1.
236

237
    let devicePixelRatio = window.devicePixelRatio || 1,
238
      backingStoreRatio =
239
        context.webkitBackingStorePixelRatio ||
240
        context.mozBackingStorePixelRatio ||
241
        context.msBackingStorePixelRatio ||
242
        context.oBackingStorePixelRatio ||
243
        context.backingStorePixelRatio ||
244
        1;
245

246
    this.pixelRatio = devicePixelRatio / backingStoreRatio;
247

248
    // Size the canvas to match the internal dimensions of its container
249

250
    this.resize(container.width(), container.height());
251

252
    // Collection of HTML div layers for text overlaid onto the canvas
253

254
    this.textContainer = null;
255
    this.text = {};
256

257
    // Cache of text fragments and metrics, so we can avoid expensively
258
    // re-calculating them when the plot is re-rendered in a loop.
259

260
    this._textCache = {};
261
    this._textSizeCache = window.flotTextSizeCache = window.flotTextSizeCache || {};
262
  }
263

264
  // Resizes the canvas to the given dimensions.
265
  //
266
  // @param {number} width New width of the canvas, in pixels.
267
  // @param {number} width New height of the canvas, in pixels.
268

269
  Canvas.prototype.resize = function(width, height) {
270
    if (width <= 0 || height <= 0) {
271
      throw new Error('Invalid dimensions for plot, width = ' + width + ', height = ' + height);
272
    }
273

274
    let element = this.element,
275
      context = this.context,
276
      pixelRatio = this.pixelRatio;
277

278
    // Resize the canvas, increasing its density based on the display's
279
    // pixel ratio; basically giving it more pixels without increasing the
280
    // size of its element, to take advantage of the fact that retina
281
    // displays have that many more pixels in the same advertised space.
282

283
    // Resizing should reset the state (excanvas seems to be buggy though)
284

285
    if (this.width != width) {
286
      element.width = width * pixelRatio;
287
      element.style.width = width + 'px';
288
      this.width = width;
289
    }
290

291
    if (this.height != height) {
292
      element.height = height * pixelRatio;
293
      element.style.height = height + 'px';
294
      this.height = height;
295
    }
296

297
    // Save the context, so we can reset in case we get replotted.  The
298
    // restore ensure that we're really back at the initial state, and
299
    // should be safe even if we haven't saved the initial state yet.
300

301
    context.restore();
302
    context.save();
303

304
    // Scale the coordinate space to match the display density; so even though we
305
    // may have twice as many pixels, we still want lines and other drawing to
306
    // appear at the same size; the extra pixels will just make them crisper.
307

308
    context.scale(pixelRatio, pixelRatio);
309
  };
310

311
  // Clears the entire canvas area, not including any overlaid HTML text
312

313
  Canvas.prototype.clear = function() {
314
    this.context.clearRect(0, 0, this.width, this.height);
315
  };
316

317
  // Finishes rendering the canvas, including managing the text overlay.
318

319
  Canvas.prototype.render = function() {
320
    let cache = this._textCache;
321

322
    // For each text layer, add elements marked as active that haven't
323
    // already been rendered, and remove those that are no longer active.
324

325
    for (let layerKey in cache) {
326
      if (hasOwnProperty.call(cache, layerKey)) {
327
        let layer = this.getTextLayer(layerKey),
328
          layerCache = cache[layerKey];
329

330
        layer.hide();
331

332
        for (let styleKey in layerCache) {
333
          if (hasOwnProperty.call(layerCache, styleKey)) {
334
            let styleCache = layerCache[styleKey];
335
            for (let key in styleCache) {
336
              if (hasOwnProperty.call(styleCache, key)) {
337
                let positions = styleCache[key].positions;
338

339
                for (var i = 0, position; (position = positions[i]); i++) {
340
                  if (position.active) {
341
                    if (!position.rendered) {
342
                      layer.append(position.element);
343
                      position.rendered = true;
344
                    }
345
                  } else {
346
                    positions.splice(i--, 1);
347
                    if (position.rendered) {
348
                      position.element.detach();
349
                    }
350
                  }
351
                }
352

353
                if (positions.length == 0) {
354
                  delete styleCache[key];
355
                }
356
              }
357
            }
358
          }
359
        }
360

361
        layer.show();
362
      }
363
    }
364
  };
365

366
  // Creates (if necessary) and returns the text overlay container.
367
  //
368
  // @param {string} classes String of space-separated CSS classes used to
369
  //     uniquely identify the text layer.
370
  // @return {object} The jQuery-wrapped text-layer div.
371

372
  Canvas.prototype.getTextLayer = function(classes) {
373
    let layer = this.text[classes];
374

375
    // Create the text layer if it doesn't exist
376

377
    if (layer == null) {
378
      // Create the text layer container, if it doesn't exist
379

380
      if (this.textContainer == null) {
381
        this.textContainer = $("<div class='flot-text flot-temp-elem'></div>")
382
          .css({
383
            position: 'absolute',
384
            top: 0,
385
            left: 0,
386
            bottom: 0,
387
            right: 0,
388
            'font-size': 'smaller',
389
            color: '#545454',
390
          })
391
          .insertAfter(this.element);
392
      }
393

394
      layer = this.text[classes] = $('<div></div>')
395
        .addClass(classes)
396
        .css({
397
          position: 'absolute',
398
          top: 0,
399
          left: 0,
400
          bottom: 0,
401
          right: 0,
402
        })
403
        .appendTo(this.textContainer);
404
    }
405

406
    return layer;
407
  };
408

409
  // Creates (if necessary) and returns a text info object.
410
  //
411
  // The object looks like this:
412
  //
413
  // {
414
  //     width: Width of the text's wrapper div.
415
  //     height: Height of the text's wrapper div.
416
  //     element: The jQuery-wrapped HTML div containing the text.
417
  //     positions: Array of positions at which this text is drawn.
418
  // }
419
  //
420
  // The positions array contains objects that look like this:
421
  //
422
  // {
423
  //     active: Flag indicating whether the text should be visible.
424
  //     rendered: Flag indicating whether the text is currently visible.
425
  //     element: The jQuery-wrapped HTML div containing the text.
426
  //     x: X coordinate at which to draw the text.
427
  //     y: Y coordinate at which to draw the text.
428
  // }
429
  //
430
  // Each position after the first receives a clone of the original element.
431
  //
432
  // The idea is that that the width, height, and general 'identity' of the
433
  // text is constant no matter where it is placed; the placements are a
434
  // secondary property.
435
  //
436
  // Canvas maintains a cache of recently-used text info objects; getTextInfo
437
  // either returns the cached element or creates a new entry.
438
  //
439
  // @param {string} layer A string of space-separated CSS classes uniquely
440
  //     identifying the layer containing this text.
441
  // @param {string} text Text string to retrieve info for.
442
  // @param {(string|object)=} font Either a string of space-separated CSS
443
  //     classes or a font-spec object, defining the text's font and style.
444
  // @param {number=} angle Angle at which to rotate the text, in degrees.
445
  //     Angle is currently unused, it will be implemented in the future.
446
  // @param {number=} width Maximum width of the text before it wraps.
447
  // @return {object} a text info object.
448

449
  Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) {
450
    let textStyle, layerCache, styleCache, info;
451

452
    // Cast the value to a string, in case we were given a number or such
453

454
    text = '' + text;
455

456
    // If the font is a font-spec object, generate a CSS font definition
457

458
    if (typeof font === 'object') {
459
      textStyle =
460
        font.style +
461
        ' ' +
462
        font.variant +
463
        ' ' +
464
        font.weight +
465
        ' ' +
466
        font.size +
467
        'px/' +
468
        font.lineHeight +
469
        'px ' +
470
        font.family;
471
    } else {
472
      textStyle = font;
473
    }
474

475
    // Retrieve (or create) the cache for the text's layer and styles
476

477
    layerCache = this._textCache[layer];
478

479
    if (layerCache == null) {
480
      layerCache = this._textCache[layer] = {};
481
    }
482

483
    styleCache = layerCache[textStyle];
484

485
    if (styleCache == null) {
486
      styleCache = layerCache[textStyle] = {};
487
    }
488

489
    info = styleCache[text];
490

491
    // If we can't find a matching element in our cache, create a new one
492

493
    if (info == null) {
494
      var element = $('<div></div>')
495
        .html(text)
496
        .css({
497
          position: 'absolute',
498
          'max-width': width,
499
          top: -9999,
500
        })
501
        .appendTo(this.getTextLayer(layer));
502

503
      if (typeof font === 'object') {
504
        element.css({
505
          font: textStyle,
506
          color: font.color,
507
        });
508
      } else if (typeof font === 'string') {
509
        element.addClass(font);
510
      }
511

512
      info = styleCache[text] = { element: element, positions: [] };
513

514
      let size = this._textSizeCache[text];
515
      if (size) {
516
        info.width = size.width;
517
        info.height = size.height;
518
      } else {
519
        info.width = element.outerWidth(true);
520
        info.height = element.outerHeight(true);
521
        this._textSizeCache[text] = { width: info.width, height: info.height };
522
      }
523
      element.detach();
524
    }
525

526
    return info;
527
  };
528

529
  // Adds a text string to the canvas text overlay.
530
  //
531
  // The text isn't drawn immediately; it is marked as rendering, which will
532
  // result in its addition to the canvas on the next render pass.
533
  //
534
  // @param {string} layer A string of space-separated CSS classes uniquely
535
  //     identifying the layer containing this text.
536
  // @param {number} x X coordinate at which to draw the text.
537
  // @param {number} y Y coordinate at which to draw the text.
538
  // @param {string} text Text string to draw.
539
  // @param {(string|object)=} font Either a string of space-separated CSS
540
  //     classes or a font-spec object, defining the text's font and style.
541
  // @param {number=} angle Angle at which to rotate the text, in degrees.
542
  //     Angle is currently unused, it will be implemented in the future.
543
  // @param {number=} width Maximum width of the text before it wraps.
544
  // @param {string=} halign Horizontal alignment of the text; either "left",
545
  //     "center" or "right".
546
  // @param {string=} valign Vertical alignment of the text; either "top",
547
  //     "middle" or "bottom".
548

549
  Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) {
550
    let info = this.getTextInfo(layer, text, font, angle, width),
551
      positions = info.positions;
552

553
    // Tweak the div's position to match the text's alignment
554

555
    if (halign == 'center') {
556
      x -= info.width / 2;
557
    } else if (halign == 'right') {
558
      x -= info.width;
559
    }
560

561
    if (valign == 'middle') {
562
      y -= info.height / 2;
563
    } else if (valign == 'bottom') {
564
      y -= info.height;
565
    }
566

567
    // Determine whether this text already exists at this position.
568
    // If so, mark it for inclusion in the next render pass.
569

570
    for (var i = 0, position; (position = positions[i]); i++) {
571
      if (position.x == x && position.y == y) {
572
        position.active = true;
573
        return;
574
      }
575
    }
576

577
    // If the text doesn't exist at this position, create a new entry
578

579
    // For the very first position we'll re-use the original element,
580
    // while for subsequent ones we'll clone it.
581

582
    position = {
583
      active: true,
584
      rendered: false,
585
      element: positions.length ? info.element.clone() : info.element,
586
      x: x,
587
      y: y,
588
    };
589

590
    positions.push(position);
591

592
    // Move the element to its final position within the container
593

594
    position.element.css({
595
      top: Math.round(y),
596
      left: Math.round(x),
597
      'text-align': halign, // In case the text wraps
598
    });
599
  };
600

601
  // Removes one or more text strings from the canvas text overlay.
602
  //
603
  // If no parameters are given, all text within the layer is removed.
604
  //
605
  // Note that the text is not immediately removed; it is simply marked as
606
  // inactive, which will result in its removal on the next render pass.
607
  // This avoids the performance penalty for 'clear and redraw' behavior,
608
  // where we potentially get rid of all text on a layer, but will likely
609
  // add back most or all of it later, as when redrawing axes, for example.
610
  //
611
  // @param {string} layer A string of space-separated CSS classes uniquely
612
  //     identifying the layer containing this text.
613
  // @param {number=} x X coordinate of the text.
614
  // @param {number=} y Y coordinate of the text.
615
  // @param {string=} text Text string to remove.
616
  // @param {(string|object)=} font Either a string of space-separated CSS
617
  //     classes or a font-spec object, defining the text's font and style.
618
  // @param {number=} angle Angle at which the text is rotated, in degrees.
619
  //     Angle is currently unused, it will be implemented in the future.
620

621
  Canvas.prototype.removeText = function(layer, x, y, text, font, angle) {
622
    if (text == null) {
623
      let layerCache = this._textCache[layer];
624
      if (layerCache != null) {
625
        for (let styleKey in layerCache) {
626
          if (hasOwnProperty.call(layerCache, styleKey)) {
627
            let styleCache = layerCache[styleKey];
628
            for (let key in styleCache) {
629
              if (hasOwnProperty.call(styleCache, key)) {
630
                var positions = styleCache[key].positions;
631
                for (var i = 0, position; (position = positions[i]); i++) {
632
                  position.active = false;
633
                }
634
              }
635
            }
636
          }
637
        }
638
      }
639
    } else {
640
      var positions = this.getTextInfo(layer, text, font, angle).positions;
641
      for (var i = 0, position; (position = positions[i]); i++) {
642
        if (position.x == x && position.y == y) {
643
          position.active = false;
644
        }
645
      }
646
    }
647
  };
648

649
  ///////////////////////////////////////////////////////////////////////////
650
  // The top-level container for the entire plot.
651

652
  function Plot(placeholder, data_, options_, plugins) {
653
    // data is on the form:
654
    //   [ series1, series2 ... ]
655
    // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
656
    // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
657

658
    let series = [],
659
      options = {
660
        // the color theme used for graphs
661
        colors: ['#edc240', '#afd8f8', '#cb4b4b', '#4da74d', '#9440ed'],
662
        legend: {
663
          show: true,
664
          noColumns: 1, // number of colums in legend table
665
          labelFormatter: null, // fn: string -> string
666
          labelBoxBorderColor: '#ccc', // border color for the little label boxes
667
          container: null, // container (as jQuery object) to put legend in, null means default on top of graph
668
          position: 'ne', // position of default legend container within plot
669
          margin: 5, // distance from grid edge to default legend container within plot
670
          backgroundColor: null, // null means auto-detect
671
          backgroundOpacity: 0.85, // set to 0 to avoid background
672
          sorted: null, // default to no legend sorting
673
        },
674
        xaxis: {
675
          show: null, // null = auto-detect, true = always, false = never
676
          position: 'bottom', // or "top"
677
          mode: null, // null or "time"
678
          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" }
679
          color: null, // base color, labels, ticks
680
          tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"
681
          transform: null, // null or f: number -> number to transform axis
682
          inverseTransform: null, // if transform is set, this should be the inverse function
683
          min: null, // min. value to show, null means set automatically
684
          max: null, // max. value to show, null means set automatically
685
          autoscaleMargin: null, // margin in % to add if auto-setting min/max
686
          ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
687
          tickFormatter: null, // fn: number -> string
688
          labelWidth: null, // size of tick labels in pixels
689
          labelHeight: null,
690
          reserveSpace: null, // whether to reserve space even if axis isn't shown
691
          tickLength: null, // size in pixels of ticks, or "full" for whole line
692
          alignTicksWithAxis: null, // axis number or null for no sync
693
          tickDecimals: null, // no. of decimals, null means auto
694
          tickSize: null, // number or [number, "unit"]
695
          minTickSize: null, // number or [number, "unit"]
696
        },
697
        yaxis: {
698
          autoscaleMargin: 0.02,
699
          position: 'left', // or "right"
700
        },
701
        xaxes: [],
702
        yaxes: [],
703
        series: {
704
          points: {
705
            show: false,
706
            radius: 3,
707
            lineWidth: 2, // in pixels
708
            fill: true,
709
            fillColor: '#ffffff',
710
            symbol: 'circle', // or callback
711
          },
712
          lines: {
713
            // we don't put in show: false so we can see
714
            // whether lines were actively disabled
715
            lineWidth: 2, // in pixels
716
            fill: false,
717
            fillColor: null,
718
            steps: false,
719
            // Omit 'zero', so we can later default its value to
720
            // match that of the 'fill' option.
721
          },
722
          bars: {
723
            show: false,
724
            lineWidth: 2, // in pixels
725
            barWidth: 1, // in units of the x axis
726
            fill: true,
727
            fillColor: null,
728
            align: 'left', // "left", "right", or "center"
729
            horizontal: false,
730
            zero: true,
731
          },
732
          shadowSize: 3,
733
          highlightColor: null,
734
        },
735
        grid: {
736
          show: true,
737
          aboveData: false,
738
          color: '#545454', // primary color used for outline and labels
739
          backgroundColor: null, // null for transparent, else color
740
          borderColor: null, // set if different from the grid color
741
          tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
742
          margin: 0, // distance from the canvas edge to the grid
743
          labelMargin: 5, // in pixels
744
          eventSectionHeight: 0, // space for event section
745
          axisMargin: 8, // in pixels
746
          borderWidth: 2, // in pixels
747
          minBorderMargin: null, // in pixels, null means taken from points radius
748
          markings: null, // array of ranges or fn: axes -> array of ranges
749
          markingsColor: '#f4f4f4',
750
          markingsLineWidth: 2,
751
          // interactive stuff
752
          clickable: false,
753
          hoverable: false,
754
          autoHighlight: true, // highlight in case mouse is near
755
          mouseActiveRadius: 10, // how far the mouse can be away to activate an item
756
        },
757
        interaction: {
758
          redrawOverlayInterval: 1000 / 60, // time between updates, -1 means in same flow
759
        },
760
        hooks: {},
761
      },
762
      surface = null, // the canvas for the plot itself
763
      overlay = null, // canvas for interactive stuff on top of plot
764
      eventHolder = null, // jQuery object that events should be bound to
765
      ctx = null,
766
      octx = null,
767
      xaxes = [],
768
      yaxes = [],
769
      plotOffset = { left: 0, right: 0, top: 0, bottom: 0 },
770
      plotWidth = 0,
771
      plotHeight = 0,
772
      hooks = {
773
        processOptions: [],
774
        processRawData: [],
775
        processDatapoints: [],
776
        processOffset: [],
777
        processRange: [],
778
        drawBackground: [],
779
        drawSeries: [],
780
        draw: [],
781
        bindEvents: [],
782
        drawOverlay: [],
783
        shutdown: [],
784
      },
785
      plot = this;
786

787
    // public functions
788
    plot.setData = setData;
789
    plot.setupGrid = setupGrid;
790
    plot.draw = draw;
791
    plot.getPlaceholder = function() {
792
      return placeholder;
793
    };
794
    plot.getCanvas = function() {
795
      return surface.element;
796
    };
797
    plot.getPlotOffset = function() {
798
      return plotOffset;
799
    };
800
    plot.width = function() {
801
      return plotWidth;
802
    };
803
    plot.height = function() {
804
      return plotHeight;
805
    };
806
    plot.offset = function() {
807
      let o = eventHolder.offset();
808
      o.left += plotOffset.left;
809
      o.top += plotOffset.top;
810
      return o;
811
    };
812
    plot.getData = function() {
813
      return series;
814
    };
815
    plot.getAxes = function() {
816
      var res = {};
817
      $.each(xaxes.concat(yaxes), function(_, axis) {
818
        if (axis) res[axis.direction + (axis.n != 1 ? axis.n : '') + 'axis'] = axis;
819
      });
820
      return res;
821
    };
822
    plot.getXAxes = function() {
823
      return xaxes;
824
    };
825
    plot.getYAxes = function() {
826
      return yaxes;
827
    };
828
    plot.c2p = canvasToAxisCoords;
829
    plot.p2c = axisToCanvasCoords;
830
    plot.getOptions = function() {
831
      return options;
832
    };
833
    plot.highlight = highlight;
834
    plot.unhighlight = unhighlight;
835
    plot.triggerRedrawOverlay = triggerRedrawOverlay;
836
    plot.pointOffset = function(point) {
837
      return {
838
        left: parseInt(xaxes[axisNumber(point, 'x') - 1].p2c(+point.x) + plotOffset.left, 10),
839
        top: parseInt(yaxes[axisNumber(point, 'y') - 1].p2c(+point.y) + plotOffset.top, 10),
840
      };
841
    };
842
    plot.shutdown = shutdown;
843
    plot.destroy = function() {
844
      shutdown();
845
      placeholder.removeData('plot').empty();
846

847
      series = [];
848
      options = null;
849
      surface = null;
850
      overlay = null;
851
      eventHolder = null;
852
      ctx = null;
853
      octx = null;
854
      xaxes = [];
855
      yaxes = [];
856
      hooks = null;
857
      highlights = [];
858
      plot = null;
859
    };
860
    plot.resize = function() {
861
      let width = placeholder.width(),
862
        height = placeholder.height();
863
      surface.resize(width, height);
864
      overlay.resize(width, height);
865
    };
866

867
    // public attributes
868
    plot.hooks = hooks;
869

870
    // initialize
871
    initPlugins(plot);
872
    parseOptions(options_);
873
    setupCanvases();
874
    setData(data_);
875
    setupGrid();
876
    draw();
877
    bindEvents();
878

879
    function executeHooks(hook, args) {
880
      args = [plot].concat(args);
881
      for (var i = 0; i < hook.length; ++i) hook[i].apply(this, args);
882
    }
883

884
    function initPlugins() {
885
      // References to key classes, allowing plugins to modify them
886

887
      let classes = {
888
        Canvas: Canvas,
889
      };
890

891
      for (let i = 0; i < plugins.length; ++i) {
892
        let p = plugins[i];
893
        p.init(plot, classes);
894
        if (p.options) $.extend(true, options, p.options);
895
      }
896
    }
897

898
    function parseOptions(opts) {
899
      $.extend(true, options, opts);
900

901
      // $.extend merges arrays, rather than replacing them.  When less
902
      // colors are provided than the size of the default palette, we
903
      // end up with those colors plus the remaining defaults, which is
904
      // not expected behavior; avoid it by replacing them here.
905

906
      if (opts && opts.colors) {
907
        options.colors = opts.colors;
908
      }
909

910
      if (options.xaxis.color == null)
911
        options.xaxis.color = $.color
912
          .parse(options.grid.color)
913
          .scale('a', 0.22)
914
          .toString();
915
      if (options.yaxis.color == null)
916
        options.yaxis.color = $.color
917
          .parse(options.grid.color)
918
          .scale('a', 0.22)
919
          .toString();
920

921
      if (options.xaxis.tickColor == null)
922
        // grid.tickColor for back-compatibility
923
        options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color;
924
      if (options.yaxis.tickColor == null)
925
        // grid.tickColor for back-compatibility
926
        options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color;
927

928
      if (options.grid.borderColor == null) options.grid.borderColor = options.grid.color;
929
      if (options.grid.tickColor == null)
930
        options.grid.tickColor = $.color
931
          .parse(options.grid.color)
932
          .scale('a', 0.22)
933
          .toString();
934

935
      // Fill in defaults for axis options, including any unspecified
936
      // font-spec fields, if a font-spec was provided.
937

938
      // If no x/y axis options were provided, create one of each anyway,
939
      // since the rest of the code assumes that they exist.
940

941
      var i,
942
        axisOptions,
943
        axisCount,
944
        fontSize = placeholder.css('font-size'),
945
        fontSizeDefault = fontSize ? +fontSize.replace('px', '') : 13,
946
        fontDefaults = {
947
          style: placeholder.css('font-style'),
948
          size: Math.round(0.8 * fontSizeDefault),
949
          variant: placeholder.css('font-variant'),
950
          weight: placeholder.css('font-weight'),
951
          family: placeholder.css('font-family'),
952
        };
953

954
      axisCount = options.xaxes.length || 1;
955
      for (i = 0; i < axisCount; ++i) {
956
        axisOptions = options.xaxes[i];
957
        if (axisOptions && !axisOptions.tickColor) {
958
          axisOptions.tickColor = axisOptions.color;
959
        }
960

961
        axisOptions = $.extend(true, {}, options.xaxis, axisOptions);
962
        options.xaxes[i] = axisOptions;
963

964
        if (axisOptions.font) {
965
          axisOptions.font = $.extend({}, fontDefaults, axisOptions.font);
966
          if (!axisOptions.font.color) {
967
            axisOptions.font.color = axisOptions.color;
968
          }
969
          if (!axisOptions.font.lineHeight) {
970
            axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15);
971
          }
972
        }
973
      }
974

975
      axisCount = options.yaxes.length || 1;
976
      for (i = 0; i < axisCount; ++i) {
977
        axisOptions = options.yaxes[i];
978
        if (axisOptions && !axisOptions.tickColor) {
979
          axisOptions.tickColor = axisOptions.color;
980
        }
981

982
        axisOptions = $.extend(true, {}, options.yaxis, axisOptions);
983
        options.yaxes[i] = axisOptions;
984

985
        if (axisOptions.font) {
986
          axisOptions.font = $.extend({}, fontDefaults, axisOptions.font);
987
          if (!axisOptions.font.color) {
988
            axisOptions.font.color = axisOptions.color;
989
          }
990
          if (!axisOptions.font.lineHeight) {
991
            axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15);
992
          }
993
        }
994
      }
995

996
      // backwards compatibility, to be removed in future
997
      if (options.xaxis.noTicks && options.xaxis.ticks == null) options.xaxis.ticks = options.xaxis.noTicks;
998
      if (options.yaxis.noTicks && options.yaxis.ticks == null) options.yaxis.ticks = options.yaxis.noTicks;
999
      if (options.x2axis) {
1000
        options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
1001
        options.xaxes[1].position = 'top';
1002
        // Override the inherit to allow the axis to auto-scale
1003
        if (options.x2axis.min == null) {
1004
          options.xaxes[1].min = null;
1005
        }
1006
        if (options.x2axis.max == null) {
1007
          options.xaxes[1].max = null;
1008
        }
1009
      }
1010
      if (options.y2axis) {
1011
        options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);
1012
        options.yaxes[1].position = 'right';
1013
        // Override the inherit to allow the axis to auto-scale
1014
        if (options.y2axis.min == null) {
1015
          options.yaxes[1].min = null;
1016
        }
1017
        if (options.y2axis.max == null) {
1018
          options.yaxes[1].max = null;
1019
        }
1020
      }
1021
      if (options.grid.coloredAreas) options.grid.markings = options.grid.coloredAreas;
1022
      if (options.grid.coloredAreasColor) options.grid.markingsColor = options.grid.coloredAreasColor;
1023
      if (options.lines) $.extend(true, options.series.lines, options.lines);
1024
      if (options.points) $.extend(true, options.series.points, options.points);
1025
      if (options.bars) $.extend(true, options.series.bars, options.bars);
1026
      if (options.shadowSize != null) options.series.shadowSize = options.shadowSize;
1027
      if (options.highlightColor != null) options.series.highlightColor = options.highlightColor;
1028

1029
      // save options on axes for future reference
1030
      for (i = 0; i < options.xaxes.length; ++i) getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
1031
      for (i = 0; i < options.yaxes.length; ++i) getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];
1032

1033
      // add hooks from options
1034
      for (var n in hooks) if (options.hooks[n] && options.hooks[n].length) hooks[n] = hooks[n].concat(options.hooks[n]);
1035

1036
      executeHooks(hooks.processOptions, [options]);
1037
    }
1038

1039
    function setData(d) {
1040
      series = parseData(d);
1041
      fillInSeriesOptions();
1042
      processData();
1043
    }
1044

1045
    function parseData(d) {
1046
      let res = [];
1047
      for (let i = 0; i < d.length; ++i) {
1048
        let s = $.extend(true, {}, options.series);
1049

1050
        if (d[i].data != null) {
1051
          s.data = d[i].data; // move the data instead of deep-copy
1052
          delete d[i].data;
1053

1054
          $.extend(true, s, d[i]);
1055

1056
          d[i].data = s.data;
1057
        } else s.data = d[i];
1058
        res.push(s);
1059
      }
1060

1061
      return res;
1062
    }
1063

1064
    function axisNumber(obj, coord) {
1065
      var a = obj[coord + 'axis'];
1066
      if (typeof a == 'object')
1067
        // if we got a real axis, extract number
1068
        a = a.n;
1069
      if (typeof a != 'number') a = 1; // default to first axis
1070
      return a;
1071
    }
1072

1073
    function allAxes() {
1074
      // return flat array without annoying null entries
1075
      return $.grep(xaxes.concat(yaxes), function(a) {
1076
        return a;
1077
      });
1078
    }
1079

1080
    function canvasToAxisCoords(pos) {
1081
      // return an object with x/y corresponding to all used axes
1082
      var res = {},
1083
        i,
1084
        axis;
1085
      for (i = 0; i < xaxes.length; ++i) {
1086
        axis = xaxes[i];
1087
        if (axis) res['x' + axis.n] = axis.c2p(pos.left);
1088
      }
1089

1090
      for (i = 0; i < yaxes.length; ++i) {
1091
        axis = yaxes[i];
1092
        if (axis) res['y' + axis.n] = axis.c2p(pos.top);
1093
      }
1094

1095
      if (res.x1 !== undefined) res.x = res.x1;
1096
      if (res.y1 !== undefined) res.y = res.y1;
1097

1098
      return res;
1099
    }
1100

1101
    function axisToCanvasCoords(pos) {
1102
      // get canvas coords from the first pair of x/y found in pos
1103
      var res = {},
1104
        i,
1105
        axis,
1106
        key;
1107

1108
      for (i = 0; i < xaxes.length; ++i) {
1109
        axis = xaxes[i];
1110
        if (axis && axis.used) {
1111
          key = 'x' + axis.n;
1112
          if (pos[key] == null && axis.n == 1) key = 'x';
1113

1114
          if (pos[key] != null) {
1115
            res.left = axis.p2c(pos[key]);
1116
            break;
1117
          }
1118
        }
1119
      }
1120

1121
      for (i = 0; i < yaxes.length; ++i) {
1122
        axis = yaxes[i];
1123
        if (axis && axis.used) {
1124
          key = 'y' + axis.n;
1125
          if (pos[key] == null && axis.n == 1) key = 'y';
1126

1127
          if (pos[key] != null) {
1128
            res.top = axis.p2c(pos[key]);
1129
            break;
1130
          }
1131
        }
1132
      }
1133

1134
      return res;
1135
    }
1136

1137
    function getOrCreateAxis(axes, number) {
1138
      if (!axes[number - 1])
1139
        axes[number - 1] = {
1140
          n: number, // save the number for future reference
1141
          direction: axes == xaxes ? 'x' : 'y',
1142
          options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis),
1143
        };
1144

1145
      return axes[number - 1];
1146
    }
1147

1148
    function fillInSeriesOptions() {
1149
      var neededColors = series.length,
1150
        maxIndex = -1,
1151
        i;
1152

1153
      // Subtract the number of series that already have fixed colors or
1154
      // color indexes from the number that we still need to generate.
1155

1156
      for (i = 0; i < series.length; ++i) {
1157
        let sc = series[i].color;
1158
        if (sc != null) {
1159
          neededColors--;
1160
          if (typeof sc == 'number' && sc > maxIndex) {
1161
            maxIndex = sc;
1162
          }
1163
        }
1164
      }
1165

1166
      // If any of the series have fixed color indexes, then we need to
1167
      // generate at least as many colors as the highest index.
1168

1169
      if (neededColors <= maxIndex) {
1170
        neededColors = maxIndex + 1;
1171
      }
1172

1173
      // Generate all the colors, using first the option colors and then
1174
      // variations on those colors once they're exhausted.
1175

1176
      var c,
1177
        colors = [],
1178
        colorPool = options.colors,
1179
        colorPoolSize = colorPool.length,
1180
        variation = 0;
1181

1182
      for (i = 0; i < neededColors; i++) {
1183
        c = $.color.parse(colorPool[i % colorPoolSize] || '#666');
1184

1185
        // Each time we exhaust the colors in the pool we adjust
1186
        // a scaling factor used to produce more variations on
1187
        // those colors. The factor alternates negative/positive
1188
        // to produce lighter/darker colors.
1189

1190
        // Reset the variation after every few cycles, or else
1191
        // it will end up producing only white or black colors.
1192

1193
        if (i % colorPoolSize == 0 && i) {
1194
          if (variation >= 0) {
1195
            if (variation < 0.5) {
1196
              variation = -variation - 0.2;
1197
            } else variation = 0;
1198
          } else variation = -variation;
1199
        }
1200

1201
        colors[i] = c.scale('rgb', 1 + variation);
1202
      }
1203

1204
      // Finalize the series options, filling in their colors
1205

1206
      var colori = 0,
1207
        s;
1208
      for (i = 0; i < series.length; ++i) {
1209
        s = series[i];
1210

1211
        // assign colors
1212
        if (s.color == null) {
1213
          s.color = colors[colori].toString();
1214
          ++colori;
1215
        } else if (typeof s.color == 'number') s.color = colors[s.color].toString();
1216

1217
        // turn on lines automatically in case nothing is set
1218
        if (s.lines.show == null) {
1219
          var v,
1220
            show = true;
1221
          for (v in s)
1222
            if (s[v] && s[v].show) {
1223
              show = false;
1224
              break;
1225
            }
1226
          if (show) s.lines.show = true;
1227
        }
1228

1229
        // If nothing was provided for lines.zero, default it to match
1230
        // lines.fill, since areas by default should extend to zero.
1231

1232
        if (s.lines.zero == null) {
1233
          s.lines.zero = !!s.lines.fill;
1234
        }
1235

1236
        // setup axes
1237
        s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, 'x'));
1238
        s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, 'y'));
1239
      }
1240
    }
1241

1242
    function processData() {
1243
      let topSentry = Number.POSITIVE_INFINITY,
1244
        bottomSentry = Number.NEGATIVE_INFINITY,
1245
        fakeInfinity = Number.MAX_VALUE,
1246
        i,
1247
        j,
1248
        k,
1249
        m,
1250
        s,
1251
        points,
1252
        ps,
1253
        val,
1254
        f,
1255
        p,
1256
        data,
1257
        format;
1258

1259
      function updateAxis(axis, min, max) {
1260
        if (min < axis.datamin && min != -fakeInfinity) axis.datamin = min;
1261
        if (max > axis.datamax && max != fakeInfinity) axis.datamax = max;
1262
      }
1263

1264
      $.each(allAxes(), function(_, axis) {
1265
        // init axis
1266
        axis.datamin = topSentry;
1267
        axis.datamax = bottomSentry;
1268
        axis.used = false;
1269
      });
1270

1271
      for (i = 0; i < series.length; ++i) {
1272
        s = series[i];
1273
        s.datapoints = { points: [] };
1274

1275
        executeHooks(hooks.processRawData, [s, s.data, s.datapoints]);
1276
      }
1277

1278
      // first pass: clean and copy data
1279
      for (i = 0; i < series.length; ++i) {
1280
        s = series[i];
1281

1282
        data = s.data;
1283
        format = s.datapoints.format;
1284

1285
        if (!format) {
1286
          format = [];
1287
          // find out how to copy
1288
          format.push({ x: true, number: true, required: true });
1289
          format.push({ y: true, number: true, required: true });
1290

1291
          if (s.stack || s.bars.show || (s.lines.show && s.lines.fill)) {
1292
            let autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero));
1293
            format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale });
1294
            if (s.bars.horizontal) {
1295
              delete format[format.length - 1].y;
1296
              format[format.length - 1].x = true;
1297
            }
1298
          }
1299

1300
          s.datapoints.format = format;
1301
        }
1302

1303
        if (s.datapoints.pointsize != null) continue; // already filled in
1304

1305
        s.datapoints.pointsize = format.length;
1306

1307
        ps = s.datapoints.pointsize;
1308
        points = s.datapoints.points;
1309

1310
        let insertSteps = s.lines.show && s.lines.steps;
1311
        s.xaxis.used = s.yaxis.used = true;
1312

1313
        for (j = k = 0; j < data.length; ++j, k += ps) {
1314
          p = data[j];
1315

1316
          let nullify = p == null;
1317
          if (!nullify) {
1318
            for (m = 0; m < ps; ++m) {
1319
              val = p[m];
1320
              f = format[m];
1321

1322
              if (f) {
1323
                if (f.number && val != null) {
1324
                  val = +val; // convert to number
1325
                  if (isNaN(val)) val = null;
1326
                  else if (val == Infinity) val = fakeInfinity;
1327
                  else if (val == -Infinity) val = -fakeInfinity;
1328
                }
1329

1330
                if (val == null) {
1331
                  if (f.required) nullify = true;
1332

1333
                  if (f.defaultValue != null) val = f.defaultValue;
1334
                }
1335
              }
1336

1337
              points[k + m] = val;
1338
            }
1339
          }
1340

1341
          if (nullify) {
1342
            for (m = 0; m < ps; ++m) {
1343
              val = points[k + m];
1344
              if (val != null) {
1345
                f = format[m];
1346
                // extract min/max info
1347
                if (f.autoscale !== false) {
1348
                  if (f.x) {
1349
                    updateAxis(s.xaxis, val, val);
1350
                  }
1351
                  if (f.y) {
1352
                    updateAxis(s.yaxis, val, val);
1353
                  }
1354
                }
1355
              }
1356
              points[k + m] = null;
1357
            }
1358
          }
1359

1360
          if (insertSteps && k > 0 && (!nullify || points[k - ps] != null)) {
1361
            // copy the point to make room for a middle point
1362
            for (m = 0; m < ps; ++m) points[k + ps + m] = points[k + m];
1363

1364
            // middle point has same y
1365
            points[k + 1] = points[k - ps + 1] || 0;
1366

1367
            // if series has null values, let's give the last !null value a nice step
1368
            if (nullify) points[k] = p[0];
1369

1370
            // we've added a point, better reflect that
1371
            k += ps;
1372
          }
1373
        }
1374
      }
1375

1376
      // give the hooks a chance to run
1377
      for (i = 0; i < series.length; ++i) {
1378
        s = series[i];
1379
        points = s.datapoints.points;
1380
        ps = s.datapoints.pointsize;
1381

1382
        // grafana
1383
        if (s.transform === 'negative-Y') {
1384
          for (j = 0; j < points.length; j += ps) {
1385
            if (points[j] == null) continue;
1386

1387
            val = points[j + 1];
1388
            points[j + 1] = -val;
1389
          }
1390
        }
1391

1392
        executeHooks(hooks.processDatapoints, [s, s.datapoints]);
1393
      }
1394

1395
      // second pass: find datamax/datamin for auto-scaling
1396
      for (i = 0; i < series.length; ++i) {
1397
        s = series[i];
1398
        points = s.datapoints.points;
1399
        ps = s.datapoints.pointsize;
1400
        format = s.datapoints.format;
1401

1402
        var xmin = topSentry,
1403
          ymin = topSentry,
1404
          xmax = bottomSentry,
1405
          ymax = bottomSentry;
1406

1407
        for (j = 0; j < points.length; j += ps) {
1408
          if (points[j] == null) continue;
1409

1410
          for (m = 0; m < ps; ++m) {
1411
            val = points[j + m];
1412
            f = format[m];
1413
            if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) continue;
1414

1415
            if (f.x) {
1416
              if (val < xmin) xmin = val;
1417
              if (val > xmax) xmax = val;
1418
            }
1419
            if (f.y) {
1420
              if (val < ymin) ymin = val;
1421
              if (val > ymax) ymax = val;
1422
            }
1423
          }
1424
        }
1425

1426
        if (s.bars.show) {
1427
          // make sure we got room for the bar on the dancing floor
1428
          var delta;
1429

1430
          switch (s.bars.align) {
1431
            case 'left':
1432
              delta = 0;
1433
              break;
1434
            case 'right':
1435
              delta = -s.bars.barWidth;
1436
              break;
1437
            default:
1438
              delta = -s.bars.barWidth / 2;
1439
          }
1440

1441
          if (s.bars.horizontal) {
1442
            ymin += delta;
1443
            ymax += delta + s.bars.barWidth;
1444
          } else {
1445
            xmin += delta;
1446
            xmax += delta + s.bars.barWidth;
1447
          }
1448
        }
1449

1450
        updateAxis(s.xaxis, xmin, xmax);
1451
        updateAxis(s.yaxis, ymin, ymax);
1452
      }
1453

1454
      $.each(allAxes(), function(_, axis) {
1455
        if (axis.datamin == topSentry) axis.datamin = null;
1456
        if (axis.datamax == bottomSentry) axis.datamax = null;
1457
      });
1458
    }
1459

1460
    function setupCanvases() {
1461
      // Make sure the placeholder is clear of everything except canvases
1462
      // from a previous plot in this container that we'll try to re-use.
1463

1464
      placeholder.find('.flot-temp-elem').remove();
1465

1466
      if (placeholder.css('position') == 'static') placeholder.css('position', 'relative'); // for positioning labels and overlay
1467

1468
      surface = new Canvas('flot-base', placeholder);
1469
      overlay = new Canvas('flot-overlay', placeholder); // overlay canvas for interactive features
1470

1471
      ctx = surface.context;
1472
      octx = overlay.context;
1473

1474
      // define which element we're listening for events on
1475
      eventHolder = $(overlay.element).unbind();
1476

1477
      // If we're re-using a plot object, shut down the old one
1478

1479
      var existing = placeholder.data('plot');
1480

1481
      if (existing) {
1482
        existing.shutdown();
1483
        overlay.clear();
1484
      }
1485

1486
      // save in case we get replotted
1487
      placeholder.data('plot', plot);
1488
    }
1489

1490
    function bindEvents() {
1491
      // bind events
1492
      if (options.grid.hoverable) {
1493
        eventHolder.mousemove(onMouseMove);
1494

1495
        // Use bind, rather than .mouseleave, because we officially
1496
        // still support jQuery 1.2.6, which doesn't define a shortcut
1497
        // for mouseenter or mouseleave.  This was a bug/oversight that
1498
        // was fixed somewhere around 1.3.x.  We can return to using
1499
        // .mouseleave when we drop support for 1.2.6.
1500

1501
        eventHolder.bind('mouseleave', onMouseLeave);
1502
      }
1503

1504
      if (options.grid.clickable) eventHolder.click(onClick);
1505

1506
      executeHooks(hooks.bindEvents, [eventHolder]);
1507
    }
1508

1509
    function shutdown() {
1510
      if (redrawTimeout) clearTimeout(redrawTimeout);
1511

1512
      eventHolder.unbind('mousemove', onMouseMove);
1513
      eventHolder.unbind('mouseleave', onMouseLeave);
1514
      eventHolder.unbind('click', onClick);
1515

1516
      executeHooks(hooks.shutdown, [eventHolder]);
1517
    }
1518

1519
    function setTransformationHelpers(axis) {
1520
      // set helper functions on the axis, assumes plot area
1521
      // has been computed already
1522

1523
      function identity(x) {
1524
        return x;
1525
      }
1526

1527
      var s,
1528
        m,
1529
        t = axis.options.transform || identity,
1530
        it = axis.options.inverseTransform;
1531

1532
      // precompute how much the axis is scaling a point
1533
      // in canvas space
1534
      if (axis.direction == 'x') {
1535
        s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
1536
        m = Math.min(t(axis.max), t(axis.min));
1537
      } else {
1538
        s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
1539
        s = -s;
1540
        m = Math.max(t(axis.max), t(axis.min));
1541
      }
1542

1543
      // data point to canvas coordinate
1544
      if (t == identity)
1545
        // slight optimization
1546
        axis.p2c = function(p) {
1547
          return (p - m) * s;
1548
        };
1549
      else
1550
        axis.p2c = function(p) {
1551
          return (t(p) - m) * s;
1552
        };
1553
      // canvas coordinate to data point
1554
      if (!it)
1555
        axis.c2p = function(c) {
1556
          return m + c / s;
1557
        };
1558
      else
1559
        axis.c2p = function(c) {
1560
          return it(m + c / s);
1561
        };
1562
    }
1563

1564
    function measureTickLabels(axis) {
1565
      let opts = axis.options,
1566
        ticks = axis.ticks || [],
1567
        labelWidth = opts.labelWidth || 0,
1568
        labelHeight = opts.labelHeight || 0,
1569
        maxWidth = labelWidth || (axis.direction == 'x' ? Math.floor(surface.width / (ticks.length || 1)) : null),
1570
        legacyStyles = axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis',
1571
        layer = 'flot-' + axis.direction + '-axis flot-' + axis.direction + axis.n + '-axis ' + legacyStyles,
1572
        font = opts.font || 'flot-tick-label tickLabel';
1573

1574
      for (let i = 0; i < ticks.length; ++i) {
1575
        let t = ticks[i];
1576

1577
        if (!t.label) continue;
1578

1579
        let info = surface.getTextInfo(layer, t.label, font, null, maxWidth);
1580

1581
        /// Grafana fix, add +1 to label width
1582
        labelWidth = Math.max(labelWidth, info.width + 1);
1583
        labelHeight = Math.max(labelHeight, info.height);
1584
      }
1585

1586
      axis.labelWidth = opts.labelWidth || labelWidth;
1587
      axis.labelHeight = opts.labelHeight || labelHeight;
1588
    }
1589

1590
    function allocateAxisBoxFirstPhase(axis) {
1591
      // find the bounding box of the axis by looking at label
1592
      // widths/heights and ticks, make room by diminishing the
1593
      // plotOffset; this first phase only looks at one
1594
      // dimension per axis, the other dimension depends on the
1595
      // other axes so will have to wait
1596

1597
      let lw = axis.labelWidth,
1598
        lh = axis.labelHeight,
1599
        pos = axis.options.position,
1600
        isXAxis = axis.direction === 'x',
1601
        tickLength = axis.options.tickLength,
1602
        axisMargin = options.grid.axisMargin,
1603
        padding = options.grid.labelMargin,
1604
        eventSectionPadding = options.grid.eventSectionHeight,
1605
        innermost = true,
1606
        outermost = true,
1607
        first = true,
1608
        found = false;
1609

1610
      // Determine the axis's position in its direction and on its side
1611

1612
      $.each(isXAxis ? xaxes : yaxes, function(i, a) {
1613
        if (a && (a.show || a.reserveSpace)) {
1614
          if (a === axis) {
1615
            found = true;
1616
          } else if (a.options.position === pos) {
1617
            if (found) {
1618
              outermost = false;
1619
            } else {
1620
              innermost = false;
1621
            }
1622
          }
1623
          if (!found) {
1624
            first = false;
1625
          }
1626
        }
1627
      });
1628

1629
      // The outermost axis on each side has no margin
1630

1631
      if (outermost) {
1632
        axisMargin = 0;
1633
      }
1634

1635
      // The ticks for the first axis in each direction stretch across
1636

1637
      if (tickLength == null) {
1638
        tickLength = first ? 'full' : 5;
1639
      }
1640

1641
      if (!isNaN(+tickLength)) padding += +tickLength;
1642

1643
      if (isXAxis) {
1644
        // Add space for event section
1645
        lh += padding;
1646
        lh += eventSectionPadding;
1647

1648
        if (pos == 'bottom') {
1649
          plotOffset.bottom += lh + axisMargin;
1650
          axis.box = { top: surface.height - plotOffset.bottom, height: lh };
1651
        } else {
1652
          axis.box = { top: plotOffset.top + axisMargin, height: lh };
1653
          plotOffset.top += lh + axisMargin;
1654
        }
1655
      } else {
1656
        lw += padding;
1657

1658
        if (pos == 'left') {
1659
          axis.box = { left: plotOffset.left + axisMargin, width: lw };
1660
          plotOffset.left += lw + axisMargin;
1661
        } else {
1662
          plotOffset.right += lw + axisMargin;
1663
          axis.box = { left: surface.width - plotOffset.right, width: lw };
1664
        }
1665
      }
1666

1667
      // save for future reference
1668
      axis.position = pos;
1669
      axis.tickLength = tickLength;
1670
      axis.box.padding = padding;
1671
      axis.box.eventSectionPadding = eventSectionPadding;
1672
      axis.innermost = innermost;
1673
    }
1674

1675
    function allocateAxisBoxSecondPhase(axis) {
1676
      // now that all axis boxes have been placed in one
1677
      // dimension, we can set the remaining dimension coordinates
1678
      if (axis.direction == 'x') {
1679
        axis.box.left = plotOffset.left - axis.labelWidth / 2;
1680
        axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth;
1681
      } else {
1682
        axis.box.top = plotOffset.top - axis.labelHeight / 2;
1683
        axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight;
1684
      }
1685
    }
1686

1687
    function adjustLayoutForThingsStickingOut() {
1688
      // possibly adjust plot offset to ensure everything stays
1689
      // inside the canvas and isn't clipped off
1690

1691
      let minMargin = options.grid.minBorderMargin,
1692
        i;
1693

1694
      // check stuff from the plot (FIXME: this should just read
1695
      // a value from the series, otherwise it's impossible to
1696
      // customize)
1697
      if (minMargin == null) {
1698
        minMargin = 0;
1699
        for (i = 0; i < series.length; ++i)
1700
          minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth / 2));
1701
      }
1702

1703
      let margins = {
1704
        left: minMargin,
1705
        right: minMargin,
1706
        top: minMargin,
1707
        bottom: minMargin,
1708
      };
1709

1710
      // check axis labels, note we don't check the actual
1711
      // labels but instead use the overall width/height to not
1712
      // jump as much around with replots
1713
      $.each(allAxes(), function(_, axis) {
1714
        if (axis.reserveSpace && axis.ticks && axis.ticks.length) {
1715
          if (axis.direction === 'x') {
1716
            margins.left = Math.max(margins.left, axis.labelWidth / 2);
1717
            margins.right = Math.max(margins.right, axis.labelWidth / 2);
1718
          } else {
1719
            margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2);
1720
            margins.top = Math.max(margins.top, axis.labelHeight / 2);
1721
          }
1722
        }
1723
      });
1724

1725
      plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left));
1726
      plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right));
1727
      plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top));
1728
      plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom));
1729
    }
1730

1731
    function setupGrid() {
1732
      var i,
1733
        axes = allAxes(),
1734
        showGrid = options.grid.show;
1735

1736
      // Initialize the plot's offset from the edge of the canvas
1737

1738
      for (var a in plotOffset) {
1739
        let margin = options.grid.margin || 0;
1740
        plotOffset[a] = typeof margin == 'number' ? margin : margin[a] || 0;
1741
      }
1742

1743
      executeHooks(hooks.processOffset, [plotOffset]);
1744

1745
      // If the grid is visible, add its border width to the offset
1746

1747
      for (var a in plotOffset) {
1748
        if (typeof options.grid.borderWidth == 'object') {
1749
          plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0;
1750
        } else {
1751
          plotOffset[a] += showGrid ? options.grid.borderWidth : 0;
1752
        }
1753
      }
1754

1755
      $.each(axes, function(_, axis) {
1756
        let axisOpts = axis.options;
1757
        axis.show = axisOpts.show == null ? axis.used : axisOpts.show;
1758
        axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace;
1759
        setRange(axis);
1760
      });
1761

1762
      executeHooks(hooks.processRange, []);
1763

1764
      if (showGrid) {
1765
        var allocatedAxes = $.grep(axes, function(axis) {
1766
          return axis.show || axis.reserveSpace;
1767
        });
1768

1769
        let snaped = false;
1770
        for (var i = 0; i < 2; i++) {
1771
          $.each(allocatedAxes, function(_, axis) {
1772
            // make the ticks
1773
            setupTickGeneration(axis);
1774
            setTicks(axis);
1775
            snaped = snapRangeToTicks(axis, axis.ticks) || snaped;
1776
            // find labelWidth/Height for axis
1777
            measureTickLabels(axis);
1778
          });
1779

1780
          if (snaped && hooks.processRange.length > 0) {
1781
            executeHooks(hooks.processRange, []);
1782
            snaped = false;
1783
          } else {
1784
            break;
1785
          }
1786
        }
1787

1788
        // with all dimensions calculated, we can compute the
1789
        // axis bounding boxes, start from the outside
1790
        // (reverse order)
1791
        for (i = allocatedAxes.length - 1; i >= 0; --i) allocateAxisBoxFirstPhase(allocatedAxes[i]);
1792

1793
        // make sure we've got enough space for things that
1794
        // might stick out
1795
        adjustLayoutForThingsStickingOut();
1796

1797
        $.each(allocatedAxes, function(_, axis) {
1798
          allocateAxisBoxSecondPhase(axis);
1799
        });
1800
      }
1801

1802
      plotWidth = surface.width - plotOffset.left - plotOffset.right;
1803
      plotHeight = surface.height - plotOffset.bottom - plotOffset.top;
1804

1805
      // now we got the proper plot dimensions, we can compute the scaling
1806
      $.each(axes, function(_, axis) {
1807
        setTransformationHelpers(axis);
1808
      });
1809

1810
      if (showGrid) {
1811
        drawAxisLabels();
1812
      }
1813

1814
      insertLegend();
1815
    }
1816

1817
    function setRange(axis) {
1818
      let opts = axis.options,
1819
        min = +(opts.min != null ? opts.min : axis.datamin),
1820
        max = +(opts.max != null ? opts.max : axis.datamax),
1821
        delta = max - min;
1822

1823
      if (delta == 0.0) {
1824
        // Grafana fix: wide Y min and max using increased wideFactor
1825
        // when all series values are the same
1826
        let wideFactor = 0.25;
1827
        let widen = Math.abs(max == 0 ? 1 : max * wideFactor);
1828

1829
        if (opts.min == null) {
1830
          min -= widen;
1831
        }
1832
        // always widen max if we couldn't widen min to ensure we
1833
        // don't fall into min == max which doesn't work
1834
        if (opts.max == null || opts.min != null) {
1835
          max += widen;
1836
        }
1837
      } else {
1838
        // consider autoscaling
1839
        let margin = opts.autoscaleMargin;
1840
        if (margin != null) {
1841
          if (opts.min == null) {
1842
            min -= delta * margin;
1843
            // make sure we don't go below zero if all values
1844
            // are positive
1845
            if (min < 0 && axis.datamin != null && axis.datamin >= 0) min = 0;
1846
          }
1847
          if (opts.max == null) {
1848
            max += delta * margin;
1849
            if (max > 0 && axis.datamax != null && axis.datamax <= 0) max = 0;
1850
          }
1851
        }
1852
      }
1853
      axis.min = min;
1854
      axis.max = max;
1855
    }
1856

1857
    function setupTickGeneration(axis) {
1858
      let opts = axis.options;
1859

1860
      // estimate number of ticks
1861
      let noTicks;
1862
      if (typeof opts.ticks == 'number' && opts.ticks > 0) noTicks = opts.ticks;
1863
      // heuristic based on the model a*sqrt(x) fitted to
1864
      // some data points that seemed reasonable
1865
      else noTicks = 0.3 * Math.sqrt(axis.direction == 'x' ? surface.width : surface.height);
1866

1867
      let delta = (axis.max - axis.min) / noTicks,
1868
        dec = -Math.floor(Math.log(delta) / Math.LN10),
1869
        maxDec = opts.tickDecimals;
1870

1871
      if (maxDec != null && dec > maxDec) {
1872
        dec = maxDec;
1873
      }
1874

1875
      let magn = Math.pow(10, -dec),
1876
        norm = delta / magn, // norm is between 1.0 and 10.0
1877
        size;
1878

1879
      if (norm < 1.5) {
1880
        size = 1;
1881
      } else if (norm < 3) {
1882
        size = 2;
1883
        // special case for 2.5, requires an extra decimal
1884
        if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
1885
          size = 2.5;
1886
          ++dec;
1887
        }
1888
      } else if (norm < 7.5) {
1889
        size = 5;
1890
      } else {
1891
        size = 10;
1892
      }
1893

1894
      size *= magn;
1895

1896
      if (opts.minTickSize != null && size < opts.minTickSize) {
1897
        size = opts.minTickSize;
1898
      }
1899

1900
      axis.delta = delta;
1901
      axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
1902
      axis.tickSize = opts.tickSize || size;
1903

1904
      // grafana addition
1905
      if (opts.tickDecimals === null || opts.tickDecimals === undefined) {
1906
        axis.scaledDecimals = axis.tickDecimals + dec;
1907
      }
1908

1909
      // Time mode was moved to a plug-in in 0.8, and since so many people use it
1910
      // we'll add an especially friendly reminder to make sure they included it.
1911

1912
      if (opts.mode == 'time' && !axis.tickGenerator) {
1913
        throw new Error('Time mode requires the flot.time plugin.');
1914
      }
1915

1916
      // Flot supports base-10 axes; any other mode else is handled by a plug-in,
1917
      // like flot.time.js.
1918

1919
      if (!axis.tickGenerator) {
1920
        axis.tickGenerator = function(axis) {
1921
          let ticks = [],
1922
            start = floorInBase(axis.min, axis.tickSize),
1923
            i = 0,
1924
            v = Number.NaN,
1925
            prev;
1926

1927
          do {
1928
            prev = v;
1929
            v = start + i * axis.tickSize;
1930
            ticks.push(v);
1931
            ++i;
1932
          } while (v < axis.max && v != prev);
1933
          return ticks;
1934
        };
1935

1936
        axis.tickFormatter = function(value, axis) {
1937
          let factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1;
1938
          var formatted = '' + Math.round(value * factor) / factor;
1939

1940
          // If tickDecimals was specified, ensure that we have exactly that
1941
          // much precision; otherwise default to the value's own precision.
1942

1943
          if (axis.tickDecimals != null) {
1944
            var decimal = formatted.indexOf('.');
1945
            let precision = decimal == -1 ? 0 : formatted.length - decimal - 1;
1946
            if (precision < axis.tickDecimals) {
1947
              return (precision ? formatted : formatted + '.') + ('' + factor).substr(1, axis.tickDecimals - precision);
1948
            }
1949
          }
1950

1951
          return formatted;
1952
        };
1953
      }
1954

1955
      if ($.isFunction(opts.tickFormatter))
1956
        axis.tickFormatter = function(v, axis) {
1957
          return '' + opts.tickFormatter(v, axis);
1958
        };
1959

1960
      if (opts.alignTicksWithAxis != null) {
1961
        var otherAxis = (axis.direction == 'x' ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];
1962
        if (otherAxis && otherAxis.used && otherAxis != axis) {
1963
          // consider snapping min/max to outermost nice ticks
1964
          let niceTicks = axis.tickGenerator(axis);
1965
          if (niceTicks.length > 0) {
1966
            if (opts.min == null) axis.min = Math.min(axis.min, niceTicks[0]);
1967
            if (opts.max == null && niceTicks.length > 1) axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);
1968
          }
1969

1970
          axis.tickGenerator = function(axis) {
1971
            // copy ticks, scaled to this axis
1972
            var ticks = [],
1973
              v,
1974
              i;
1975
            for (i = 0; i < otherAxis.ticks.length; ++i) {
1976
              v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);
1977
              v = axis.min + v * (axis.max - axis.min);
1978
              ticks.push(v);
1979
            }
1980
            return ticks;
1981
          };
1982

1983
          // we might need an extra decimal since forced
1984
          // ticks don't necessarily fit naturally
1985
          if (!axis.mode && opts.tickDecimals == null) {
1986
            let extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1),
1987
              ts = axis.tickGenerator(axis);
1988

1989
            // only proceed if the tick interval rounded
1990
            // with an extra decimal doesn't give us a
1991
            // zero at end
1992
            if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) axis.tickDecimals = extraDec;
1993
          }
1994
        }
1995
      }
1996
    }
1997

1998
    function setTicks(axis) {
1999
      var oticks = axis.options.ticks,
2000
        ticks = [];
2001
      if (oticks == null || (typeof oticks == 'number' && oticks > 0)) ticks = axis.tickGenerator(axis);
2002
      else if (oticks) {
2003
        if ($.isFunction(oticks))
2004
          // generate the ticks
2005
          ticks = oticks(axis);
2006
        else ticks = oticks;
2007
      }
2008

2009
      // clean up/labelify the supplied ticks, copy them over
2010
      let i, v;
2011
      axis.ticks = [];
2012
      for (i = 0; i < ticks.length; ++i) {
2013
        let label = null;
2014
        let t = ticks[i];
2015
        if (typeof t == 'object') {
2016
          v = +t[0];
2017
          if (t.length > 1) label = t[1];
2018
        } else v = +t;
2019
        if (label == null) label = axis.tickFormatter(v, axis);
2020
        if (!isNaN(v)) axis.ticks.push({ v: v, label: label });
2021
      }
2022
    }
2023

2024
    function snapRangeToTicks(axis, ticks) {
2025
      let changed = false;
2026
      if (axis.options.autoscaleMargin && ticks.length > 0) {
2027
        // snap to ticks
2028
        if (axis.options.min == null) {
2029
          axis.min = Math.min(axis.min, ticks[0].v);
2030
          changed = true;
2031
        }
2032
        if (axis.options.max == null && ticks.length > 1) {
2033
          axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
2034
          changed = true;
2035
        }
2036
      }
2037
      return changed;
2038
    }
2039

2040
    function draw() {
2041
      surface.clear();
2042

2043
      executeHooks(hooks.drawBackground, [ctx]);
2044

2045
      let grid = options.grid;
2046

2047
      // draw background, if any
2048
      if (grid.show && grid.backgroundColor) drawBackground();
2049

2050
      if (grid.show && !grid.aboveData) {
2051
        drawGrid();
2052
      }
2053

2054
      for (let i = 0; i < series.length; ++i) {
2055
        executeHooks(hooks.drawSeries, [ctx, series[i]]);
2056
        drawSeries(series[i]);
2057
      }
2058

2059
      executeHooks(hooks.draw, [ctx]);
2060

2061
      if (grid.show && grid.aboveData) {
2062
        drawGrid();
2063
      }
2064

2065
      surface.render();
2066

2067
      // A draw implies that either the axes or data have changed, so we
2068
      // should probably update the overlay highlights as well.
2069

2070
      triggerRedrawOverlay();
2071
    }
2072

2073
    function extractRange(ranges, coord) {
2074
      var axis,
2075
        from,
2076
        to,
2077
        key,
2078
        axes = allAxes();
2079

2080
      for (let i = 0; i < axes.length; ++i) {
2081
        axis = axes[i];
2082
        if (axis.direction == coord) {
2083
          key = coord + axis.n + 'axis';
2084
          if (!ranges[key] && axis.n == 1) key = coord + 'axis'; // support x1axis as xaxis
2085
          if (ranges[key]) {
2086
            from = ranges[key].from;
2087
            to = ranges[key].to;
2088
            break;
2089
          }
2090
        }
2091
      }
2092

2093
      // backwards-compat stuff - to be removed in future
2094
      if (!ranges[key]) {
2095
        axis = coord == 'x' ? xaxes[0] : yaxes[0];
2096
        from = ranges[coord + '1'];
2097
        to = ranges[coord + '2'];
2098
      }
2099

2100
      // auto-reverse as an added bonus
2101
      if (from != null && to != null && from > to) {
2102
        let tmp = from;
2103
        from = to;
2104
        to = tmp;
2105
      }
2106

2107
      return { from: from, to: to, axis: axis };
2108
    }
2109

2110
    function drawBackground() {
2111
      ctx.save();
2112
      ctx.translate(plotOffset.left, plotOffset.top);
2113

2114
      ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, 'rgba(255, 255, 255, 0)');
2115
      ctx.fillRect(0, 0, plotWidth, plotHeight);
2116
      ctx.restore();
2117
    }
2118

2119
    function drawGrid() {
2120
      let i, axes, bw, bc;
2121

2122
      ctx.save();
2123
      ctx.translate(plotOffset.left, plotOffset.top);
2124

2125
      // draw markings
2126
      let markings = options.grid.markings;
2127
      if (markings) {
2128
        if ($.isFunction(markings)) {
2129
          axes = plot.getAxes();
2130
          // xmin etc. is backwards compatibility, to be
2131
          // removed in the future
2132
          axes.xmin = axes.xaxis.min;
2133
          axes.xmax = axes.xaxis.max;
2134
          axes.ymin = axes.yaxis.min;
2135
          axes.ymax = axes.yaxis.max;
2136

2137
          markings = markings(axes);
2138
        }
2139

2140
        for (i = 0; i < markings.length; ++i) {
2141
          let m = markings[i],
2142
            xrange = extractRange(m, 'x'),
2143
            yrange = extractRange(m, 'y');
2144

2145
          // fill in missing
2146
          if (xrange.from == null) xrange.from = xrange.axis.min;
2147
          if (xrange.to == null) xrange.to = xrange.axis.max;
2148
          if (yrange.from == null) yrange.from = yrange.axis.min;
2149
          if (yrange.to == null) yrange.to = yrange.axis.max;
2150

2151
          // clip
2152
          if (
2153
            xrange.to < xrange.axis.min ||
2154
            xrange.from > xrange.axis.max ||
2155
            yrange.to < yrange.axis.min ||
2156
            yrange.from > yrange.axis.max
2157
          )
2158
            continue;
2159

2160
          xrange.from = Math.max(xrange.from, xrange.axis.min);
2161
          xrange.to = Math.min(xrange.to, xrange.axis.max);
2162
          yrange.from = Math.max(yrange.from, yrange.axis.min);
2163
          yrange.to = Math.min(yrange.to, yrange.axis.max);
2164

2165
          let xequal = xrange.from === xrange.to,
2166
            yequal = yrange.from === yrange.to;
2167

2168
          if (xequal && yequal) {
2169
            continue;
2170
          }
2171

2172
          // then draw
2173
          xrange.from = Math.floor(xrange.axis.p2c(xrange.from));
2174
          xrange.to = Math.floor(xrange.axis.p2c(xrange.to));
2175
          yrange.from = Math.floor(yrange.axis.p2c(yrange.from));
2176
          yrange.to = Math.floor(yrange.axis.p2c(yrange.to));
2177

2178
          if (xequal || yequal) {
2179
            let lineWidth = m.lineWidth || options.grid.markingsLineWidth,
2180
              subPixel = lineWidth % 2 ? 0.5 : 0;
2181
            ctx.beginPath();
2182
            ctx.strokeStyle = m.color || options.grid.markingsColor;
2183
            ctx.lineWidth = lineWidth;
2184
            if (xequal) {
2185
              ctx.moveTo(xrange.to + subPixel, yrange.from);
2186
              ctx.lineTo(xrange.to + subPixel, yrange.to);
2187
            } else {
2188
              ctx.moveTo(xrange.from, yrange.to + subPixel);
2189
              ctx.lineTo(xrange.to, yrange.to + subPixel);
2190
            }
2191
            ctx.stroke();
2192
          } else {
2193
            ctx.fillStyle = m.color || options.grid.markingsColor;
2194
            ctx.fillRect(xrange.from, yrange.to, xrange.to - xrange.from, yrange.from - yrange.to);
2195
          }
2196
        }
2197
      }
2198

2199
      // draw the ticks
2200
      axes = allAxes();
2201
      bw = options.grid.borderWidth;
2202

2203
      for (let j = 0; j < axes.length; ++j) {
2204
        var axis = axes[j],
2205
          box = axis.box,
2206
          t = axis.tickLength,
2207
          x,
2208
          y,
2209
          xoff,
2210
          yoff;
2211
        if (!axis.show || axis.ticks.length == 0) continue;
2212

2213
        ctx.lineWidth = 1;
2214

2215
        // find the edges
2216
        if (axis.direction == 'x') {
2217
          x = 0;
2218
          if (t == 'full') y = axis.position == 'top' ? 0 : plotHeight;
2219
          else y = box.top - plotOffset.top + (axis.position == 'top' ? box.height : 0);
2220
        } else {
2221
          y = 0;
2222
          if (t == 'full') x = axis.position == 'left' ? 0 : plotWidth;
2223
          else x = box.left - plotOffset.left + (axis.position == 'left' ? box.width : 0);
2224
        }
2225

2226
        // draw tick bar
2227
        if (!axis.innermost) {
2228
          ctx.strokeStyle = axis.options.color;
2229
          ctx.beginPath();
2230
          xoff = yoff = 0;
2231
          if (axis.direction == 'x') xoff = plotWidth + 1;
2232
          else yoff = plotHeight + 1;
2233

2234
          if (ctx.lineWidth == 1) {
2235
            if (axis.direction == 'x') {
2236
              y = Math.floor(y) + 0.5;
2237
            } else {
2238
              x = Math.floor(x) + 0.5;
2239
            }
2240
          }
2241

2242
          ctx.moveTo(x, y);
2243
          ctx.lineTo(x + xoff, y + yoff);
2244
          ctx.stroke();
2245
        }
2246

2247
        // draw ticks
2248

2249
        ctx.strokeStyle = axis.options.tickColor;
2250

2251
        ctx.beginPath();
2252
        for (i = 0; i < axis.ticks.length; ++i) {
2253
          let v = axis.ticks[i].v;
2254

2255
          xoff = yoff = 0;
2256

2257
          if (
2258
            isNaN(v) ||
2259
            v < axis.min ||
2260
            v > axis.max ||
2261
            // skip those lying on the axes if we got a border
2262
            (t == 'full' && ((typeof bw == 'object' && bw[axis.position] > 0) || bw > 0) && (v == axis.min || v == axis.max))
2263
          )
2264
            continue;
2265

2266
          if (axis.direction == 'x') {
2267
            x = axis.p2c(v);
2268
            yoff = t == 'full' ? -plotHeight : t;
2269

2270
            if (axis.position == 'top') yoff = -yoff;
2271
          } else {
2272
            y = axis.p2c(v);
2273
            xoff = t == 'full' ? -plotWidth : t;
2274

2275
            if (axis.position == 'left') xoff = -xoff;
2276
          }
2277

2278
          if (ctx.lineWidth == 1) {
2279
            if (axis.direction == 'x') x = Math.floor(x) + 0.5;
2280
            else y = Math.floor(y) + 0.5;
2281
          }
2282

2283
          ctx.moveTo(x, y);
2284
          ctx.lineTo(x + xoff, y + yoff);
2285
        }
2286

2287
        ctx.stroke();
2288
      }
2289

2290
      // draw border
2291
      if (bw) {
2292
        // If either borderWidth or borderColor is an object, then draw the border
2293
        // line by line instead of as one rectangle
2294
        bc = options.grid.borderColor;
2295
        if (typeof bw == 'object' || typeof bc == 'object') {
2296
          if (typeof bw !== 'object') {
2297
            bw = { top: bw, right: bw, bottom: bw, left: bw };
2298
          }
2299
          if (typeof bc !== 'object') {
2300
            bc = { top: bc, right: bc, bottom: bc, left: bc };
2301
          }
2302

2303
          if (bw.top > 0) {
2304
            ctx.strokeStyle = bc.top;
2305
            ctx.lineWidth = bw.top;
2306
            ctx.beginPath();
2307
            ctx.moveTo(0 - bw.left, 0 - bw.top / 2);
2308
            ctx.lineTo(plotWidth, 0 - bw.top / 2);
2309
            ctx.stroke();
2310
          }
2311

2312
          if (bw.right > 0) {
2313
            ctx.strokeStyle = bc.right;
2314
            ctx.lineWidth = bw.right;
2315
            ctx.beginPath();
2316
            ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top);
2317
            ctx.lineTo(plotWidth + bw.right / 2, plotHeight);
2318
            ctx.stroke();
2319
          }
2320

2321
          if (bw.bottom > 0) {
2322
            ctx.strokeStyle = bc.bottom;
2323
            ctx.lineWidth = bw.bottom;
2324
            ctx.beginPath();
2325
            ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2);
2326
            ctx.lineTo(0, plotHeight + bw.bottom / 2);
2327
            ctx.stroke();
2328
          }
2329

2330
          if (bw.left > 0) {
2331
            ctx.strokeStyle = bc.left;
2332
            ctx.lineWidth = bw.left;
2333
            ctx.beginPath();
2334
            ctx.moveTo(0 - bw.left / 2, plotHeight + bw.bottom);
2335
            ctx.lineTo(0 - bw.left / 2, 0);
2336
            ctx.stroke();
2337
          }
2338
        } else {
2339
          ctx.lineWidth = bw;
2340
          ctx.strokeStyle = options.grid.borderColor;
2341
          ctx.strokeRect(-bw / 2, -bw / 2, plotWidth + bw, plotHeight + bw);
2342
        }
2343
      }
2344

2345
      ctx.restore();
2346
    }
2347

2348
    function drawAxisLabels() {
2349
      $.each(allAxes(), function(_, axis) {
2350
        let box = axis.box,
2351
          legacyStyles = axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis',
2352
          layer = 'flot-' + axis.direction + '-axis flot-' + axis.direction + axis.n + '-axis ' + legacyStyles,
2353
          font = axis.options.font || 'flot-tick-label tickLabel',
2354
          tick,
2355
          x,
2356
          y,
2357
          halign,
2358
          valign;
2359

2360
        // Remove text before checking for axis.show and ticks.length;
2361
        // otherwise plugins, like flot-tickrotor, that draw their own
2362
        // tick labels will end up with both theirs and the defaults.
2363

2364
        surface.removeText(layer);
2365

2366
        if (!axis.show || axis.ticks.length == 0) return;
2367

2368
        for (let i = 0; i < axis.ticks.length; ++i) {
2369
          tick = axis.ticks[i];
2370
          if (!tick.label || tick.v < axis.min || tick.v > axis.max) continue;
2371

2372
          if (axis.direction == 'x') {
2373
            halign = 'center';
2374
            x = plotOffset.left + axis.p2c(tick.v);
2375
            if (axis.position == 'bottom') {
2376
              y = box.top + box.padding + box.eventSectionPadding;
2377
            } else {
2378
              y = box.top + box.height - box.padding;
2379
              valign = 'bottom';
2380
            }
2381
          } else {
2382
            valign = 'middle';
2383
            y = plotOffset.top + axis.p2c(tick.v);
2384
            if (axis.position == 'left') {
2385
              x = box.left + box.width - box.padding;
2386
              halign = 'right';
2387
            } else {
2388
              x = box.left + box.padding;
2389
            }
2390
          }
2391

2392
          surface.addText(layer, x, y, tick.label, font, null, null, halign, valign);
2393
        }
2394
      });
2395
    }
2396

2397
    function drawOrphanedPoints(series) {
2398
      /* Filters series data for points with no neighbors before or after
2399
       * and plots single 0.5 radius points for them so that they are displayed.
2400
       */
2401
      let abandonedPoints = [];
2402
      let beforeX = null;
2403
      let afterX = null;
2404
      let datapoints = series.datapoints;
2405
      // find any points with no neighbors before or after
2406
      let emptyPoints = [];
2407
      for (let j = 0; j < datapoints.pointsize - 2; j++) {
2408
        emptyPoints.push(0);
2409
      }
2410
      for (let i = 0; i < datapoints.points.length; i += datapoints.pointsize) {
2411
        var x = datapoints.points[i],
2412
          y = datapoints.points[i + 1];
2413
        if (i === datapoints.points.length - datapoints.pointsize) {
2414
          afterX = null;
2415
        } else {
2416
          afterX = datapoints.points[i + datapoints.pointsize];
2417
        }
2418
        if (x !== null && y !== null && beforeX === null && afterX === null) {
2419
          abandonedPoints.push(x);
2420
          abandonedPoints.push(y);
2421
          abandonedPoints.push.apply(abandonedPoints, emptyPoints);
2422
        }
2423
        beforeX = x;
2424
      }
2425
      var olddatapoints = datapoints.points;
2426
      datapoints.points = abandonedPoints;
2427

2428
      series.points.radius = series.lines.lineWidth / 2;
2429
      // plot the orphan points with a radius of lineWidth/2
2430
      drawSeriesPoints(series);
2431
      // reset old info
2432
      datapoints.points = olddatapoints;
2433
    }
2434

2435
    function drawSeries(series) {
2436
      if (series.lines.show) {
2437
        drawSeriesLines(series);
2438
        if (!series.points.show && !series.bars.show) {
2439
          // not necessary if user wants points displayed for everything
2440
          drawOrphanedPoints(series);
2441
        }
2442
      }
2443
      if (series.bars.show) drawSeriesBars(series);
2444
      if (series.points.show) drawSeriesPoints(series);
2445
    }
2446

2447
    function drawSeriesLines(series) {
2448
      function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
2449
        let points = datapoints.points,
2450
          ps = datapoints.pointsize,
2451
          prevx = null,
2452
          prevy = null;
2453

2454
        ctx.beginPath();
2455
        for (let i = ps; i < points.length; i += ps) {
2456
          var x1 = points[i - ps],
2457
            y1 = points[i - ps + 1],
2458
            x2 = points[i],
2459
            y2 = points[i + 1];
2460

2461
          if (x1 == null || x2 == null) continue;
2462

2463
          // clip with ymin
2464
          if (y1 <= y2 && y1 < axisy.min) {
2465
            if (y2 < axisy.min) continue; // line segment is outside
2466
            // compute new intersection point
2467
            x1 = ((axisy.min - y1) / (y2 - y1)) * (x2 - x1) + x1;
2468
            y1 = axisy.min;
2469
          } else if (y2 <= y1 && y2 < axisy.min) {
2470
            if (y1 < axisy.min) continue;
2471
            x2 = ((axisy.min - y1) / (y2 - y1)) * (x2 - x1) + x1;
2472
            y2 = axisy.min;
2473
          }
2474

2475
          // clip with ymax
2476
          if (y1 >= y2 && y1 > axisy.max) {
2477
            if (y2 > axisy.max) continue;
2478
            x1 = ((axisy.max - y1) / (y2 - y1)) * (x2 - x1) + x1;
2479
            y1 = axisy.max;
2480
          } else if (y2 >= y1 && y2 > axisy.max) {
2481
            if (y1 > axisy.max) continue;
2482
            x2 = ((axisy.max - y1) / (y2 - y1)) * (x2 - x1) + x1;
2483
            y2 = axisy.max;
2484
          }
2485

2486
          // clip with xmin
2487
          if (x1 <= x2 && x1 < axisx.min) {
2488
            if (x2 < axisx.min) continue;
2489
            y1 = ((axisx.min - x1) / (x2 - x1)) * (y2 - y1) + y1;
2490
            x1 = axisx.min;
2491
          } else if (x2 <= x1 && x2 < axisx.min) {
2492
            if (x1 < axisx.min) continue;
2493
            y2 = ((axisx.min - x1) / (x2 - x1)) * (y2 - y1) + y1;
2494
            x2 = axisx.min;
2495
          }
2496

2497
          // clip with xmax
2498
          if (x1 >= x2 && x1 > axisx.max) {
2499
            if (x2 > axisx.max) continue;
2500
            y1 = ((axisx.max - x1) / (x2 - x1)) * (y2 - y1) + y1;
2501
            x1 = axisx.max;
2502
          } else if (x2 >= x1 && x2 > axisx.max) {
2503
            if (x1 > axisx.max) continue;
2504
            y2 = ((axisx.max - x1) / (x2 - x1)) * (y2 - y1) + y1;
2505
            x2 = axisx.max;
2506
          }
2507

2508
          if (x1 != prevx || y1 != prevy) ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
2509

2510
          prevx = x2;
2511
          prevy = y2;
2512
          ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
2513
        }
2514
        ctx.stroke();
2515
      }
2516

2517
      function plotLineArea(datapoints, axisx, axisy) {
2518
        let points = datapoints.points,
2519
          ps = datapoints.pointsize,
2520
          bottom = Math.min(Math.max(0, axisy.min), axisy.max),
2521
          i = 0,
2522
          areaOpen = false,
2523
          ypos = 1,
2524
          segmentStart = 0,
2525
          segmentEnd = 0;
2526

2527
        // we process each segment in two turns, first forward
2528
        // direction to sketch out top, then once we hit the
2529
        // end we go backwards to sketch the bottom
2530
        while (true) {
2531
          if (ps > 0 && i > points.length + ps) break;
2532

2533
          i += ps; // ps is negative if going backwards
2534

2535
          let x1 = points[i - ps],
2536
            y1 = points[i - ps + ypos],
2537
            x2 = points[i],
2538
            y2 = points[i + ypos];
2539

2540
          if (areaOpen) {
2541
            if (ps > 0 && x1 != null && x2 == null) {
2542
              // at turning point
2543
              segmentEnd = i;
2544
              ps = -ps;
2545
              ypos = 2;
2546
              continue;
2547
            }
2548

2549
            if (ps < 0 && i == segmentStart + ps) {
2550
              // done with the reverse sweep
2551
              ctx.fill();
2552
              areaOpen = false;
2553
              ps = -ps;
2554
              ypos = 1;
2555
              i = segmentStart = segmentEnd + ps;
2556
              continue;
2557
            }
2558
          }
2559

2560
          if (x1 == null || x2 == null) continue;
2561

2562
          // clip x values
2563

2564
          // clip with xmin
2565
          if (x1 <= x2 && x1 < axisx.min) {
2566
            if (x2 < axisx.min) continue;
2567
            y1 = ((axisx.min - x1) / (x2 - x1)) * (y2 - y1) + y1;
2568
            x1 = axisx.min;
2569
          } else if (x2 <= x1 && x2 < axisx.min) {
2570
            if (x1 < axisx.min) continue;
2571
            y2 = ((axisx.min - x1) / (x2 - x1)) * (y2 - y1) + y1;
2572
            x2 = axisx.min;
2573
          }
2574

2575
          // clip with xmax
2576
          if (x1 >= x2 && x1 > axisx.max) {
2577
            if (x2 > axisx.max) continue;
2578
            y1 = ((axisx.max - x1) / (x2 - x1)) * (y2 - y1) + y1;
2579
            x1 = axisx.max;
2580
          } else if (x2 >= x1 && x2 > axisx.max) {
2581
            if (x1 > axisx.max) continue;
2582
            y2 = ((axisx.max - x1) / (x2 - x1)) * (y2 - y1) + y1;
2583
            x2 = axisx.max;
2584
          }
2585

2586
          if (!areaOpen) {
2587
            // open area
2588
            ctx.beginPath();
2589
            ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
2590
            areaOpen = true;
2591
          }
2592

2593
          // now first check the case where both is outside
2594
          if (y1 >= axisy.max && y2 >= axisy.max) {
2595
            ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
2596
            ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
2597
            continue;
2598
          } else if (y1 <= axisy.min && y2 <= axisy.min) {
2599
            ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
2600
            ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
2601
            continue;
2602
          }
2603

2604
          // else it's a bit more complicated, there might
2605
          // be a flat maxed out rectangle first, then a
2606
          // triangular cutout or reverse; to find these
2607
          // keep track of the current x values
2608
          var x1old = x1,
2609
            x2old = x2;
2610

2611
          // clip the y values, without shortcutting, we
2612
          // go through all cases in turn
2613

2614
          // clip with ymin
2615
          if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
2616
            x1 = ((axisy.min - y1) / (y2 - y1)) * (x2 - x1) + x1;
2617
            y1 = axisy.min;
2618
          } else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
2619
            x2 = ((axisy.min - y1) / (y2 - y1)) * (x2 - x1) + x1;
2620
            y2 = axisy.min;
2621
          }
2622

2623
          // clip with ymax
2624
          if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
2625
            x1 = ((axisy.max - y1) / (y2 - y1)) * (x2 - x1) + x1;
2626
            y1 = axisy.max;
2627
          } else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
2628
            x2 = ((axisy.max - y1) / (y2 - y1)) * (x2 - x1) + x1;
2629
            y2 = axisy.max;
2630
          }
2631

2632
          // if the x value was changed we got a rectangle
2633
          // to fill
2634
          if (x1 != x1old) {
2635
            ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
2636
            // it goes to (x1, y1), but we fill that below
2637
          }
2638

2639
          // fill triangular section, this sometimes result
2640
          // in redundant points if (x1, y1) hasn't changed
2641
          // from previous line to, but we just ignore that
2642
          ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
2643
          ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
2644

2645
          // fill the other rectangle if it's there
2646
          if (x2 != x2old) {
2647
            ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
2648
            ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
2649
          }
2650
        }
2651
      }
2652

2653
      ctx.save();
2654
      ctx.translate(plotOffset.left, plotOffset.top);
2655
      ctx.lineJoin = 'round';
2656

2657
      let lw = series.lines.lineWidth,
2658
        sw = series.shadowSize;
2659
      // FIXME: consider another form of shadow when filling is turned on
2660
      if (lw > 0 && sw > 0) {
2661
        // draw shadow as a thick and thin line with transparency
2662
        ctx.lineWidth = sw;
2663
        ctx.strokeStyle = 'rgba(0,0,0,0.1)';
2664
        // position shadow at angle from the mid of line
2665
        var angle = Math.PI / 18;
2666
        plotLine(
2667
          series.datapoints,
2668
          Math.sin(angle) * (lw / 2 + sw / 2),
2669
          Math.cos(angle) * (lw / 2 + sw / 2),
2670
          series.xaxis,
2671
          series.yaxis
2672
        );
2673
        ctx.lineWidth = sw / 2;
2674
        plotLine(
2675
          series.datapoints,
2676
          Math.sin(angle) * (lw / 2 + sw / 4),
2677
          Math.cos(angle) * (lw / 2 + sw / 4),
2678
          series.xaxis,
2679
          series.yaxis
2680
        );
2681
      }
2682

2683
      ctx.lineWidth = lw;
2684
      ctx.strokeStyle = series.color;
2685
      let fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
2686
      if (fillStyle) {
2687
        ctx.fillStyle = fillStyle;
2688
        plotLineArea(series.datapoints, series.xaxis, series.yaxis);
2689
      }
2690

2691
      if (lw > 0) plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
2692
      ctx.restore();
2693
    }
2694

2695
    function drawSeriesPoints(series) {
2696
      function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {
2697
        var points = datapoints.points,
2698
          ps = datapoints.pointsize;
2699

2700
        for (let i = 0; i < points.length; i += ps) {
2701
          var x = points[i],
2702
            y = points[i + 1];
2703
          if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) continue;
2704

2705
          ctx.beginPath();
2706
          x = axisx.p2c(x);
2707
          y = axisy.p2c(y) + offset;
2708
          if (symbol == 'circle') ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
2709
          else symbol(ctx, x, y, radius, shadow);
2710
          ctx.closePath();
2711

2712
          if (fillStyle) {
2713
            ctx.fillStyle = fillStyle;
2714
            ctx.fill();
2715
          }
2716
          ctx.stroke();
2717
        }
2718
      }
2719

2720
      ctx.save();
2721
      ctx.translate(plotOffset.left, plotOffset.top);
2722

2723
      let lw = series.points.lineWidth,
2724
        sw = series.shadowSize,
2725
        radius = series.points.radius,
2726
        symbol = series.points.symbol;
2727

2728
      // If the user sets the line width to 0, we change it to a very
2729
      // small value. A line width of 0 seems to force the default of 1.
2730
      // Doing the conditional here allows the shadow setting to still be
2731
      // optional even with a lineWidth of 0.
2732

2733
      if (lw == 0) lw = 0.0001;
2734

2735
      if (lw > 0 && sw > 0) {
2736
        // draw shadow in two steps
2737
        let w = sw / 2;
2738
        ctx.lineWidth = w;
2739
        ctx.strokeStyle = 'rgba(0,0,0,0.1)';
2740
        plotPoints(series.datapoints, radius, null, w + w / 2, true, series.xaxis, series.yaxis, symbol);
2741

2742
        ctx.strokeStyle = 'rgba(0,0,0,0.2)';
2743
        plotPoints(series.datapoints, radius, null, w / 2, true, series.xaxis, series.yaxis, symbol);
2744
      }
2745

2746
      ctx.lineWidth = lw;
2747
      ctx.strokeStyle = series.color;
2748
      plotPoints(
2749
        series.datapoints,
2750
        radius,
2751
        getFillStyle(series.points, series.color),
2752
        0,
2753
        false,
2754
        series.xaxis,
2755
        series.yaxis,
2756
        symbol
2757
      );
2758
      ctx.restore();
2759
    }
2760

2761
    function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
2762
      var left, right, bottom, top, drawLeft, drawRight, drawTop, drawBottom, tmp;
2763

2764
      // in horizontal mode, we start the bar from the left
2765
      // instead of from the bottom so it appears to be
2766
      // horizontal rather than vertical
2767
      if (horizontal) {
2768
        drawBottom = drawRight = drawTop = true;
2769
        drawLeft = false;
2770
        left = b;
2771
        right = x;
2772
        top = y + barLeft;
2773
        bottom = y + barRight;
2774

2775
        // account for negative bars
2776
        if (right < left) {
2777
          tmp = right;
2778
          right = left;
2779
          left = tmp;
2780
          drawLeft = true;
2781
          drawRight = false;
2782
        }
2783
      } else {
2784
        drawLeft = drawRight = drawTop = true;
2785
        drawBottom = false;
2786
        left = x + barLeft;
2787
        right = x + barRight;
2788
        bottom = b;
2789
        top = y;
2790

2791
        // account for negative bars
2792
        if (top < bottom) {
2793
          tmp = top;
2794
          top = bottom;
2795
          bottom = tmp;
2796
          drawBottom = true;
2797
          drawTop = false;
2798
        }
2799
      }
2800

2801
      // clip
2802
      if (right < axisx.min || left > axisx.max || top < axisy.min || bottom > axisy.max) return;
2803

2804
      if (left < axisx.min) {
2805
        left = axisx.min;
2806
        drawLeft = false;
2807
      }
2808

2809
      if (right > axisx.max) {
2810
        right = axisx.max;
2811
        drawRight = false;
2812
      }
2813

2814
      if (bottom < axisy.min) {
2815
        bottom = axisy.min;
2816
        drawBottom = false;
2817
      }
2818

2819
      if (top > axisy.max) {
2820
        top = axisy.max;
2821
        drawTop = false;
2822
      }
2823

2824
      left = axisx.p2c(left);
2825
      bottom = axisy.p2c(bottom);
2826
      right = axisx.p2c(right);
2827
      top = axisy.p2c(top);
2828

2829
      // fill the bar
2830
      if (fillStyleCallback) {
2831
        c.fillStyle = fillStyleCallback(bottom, top);
2832
        c.fillRect(left, top, right - left, bottom - top);
2833
      }
2834

2835
      // draw outline
2836
      if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
2837
        c.beginPath();
2838

2839
        // FIXME: inline moveTo is buggy with excanvas
2840
        c.moveTo(left, bottom);
2841
        if (drawLeft) c.lineTo(left, top);
2842
        else c.moveTo(left, top);
2843
        if (drawTop) c.lineTo(right, top);
2844
        else c.moveTo(right, top);
2845
        if (drawRight) c.lineTo(right, bottom);
2846
        else c.moveTo(right, bottom);
2847
        if (drawBottom) c.lineTo(left, bottom);
2848
        else c.moveTo(left, bottom);
2849
        c.stroke();
2850
      }
2851
    }
2852

2853
    function drawSeriesBars(series) {
2854
      function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) {
2855
        var points = datapoints.points,
2856
          ps = datapoints.pointsize;
2857

2858
        for (let i = 0; i < points.length; i += ps) {
2859
          if (points[i] == null) continue;
2860
          drawBar(
2861
            points[i],
2862
            points[i + 1],
2863
            points[i + 2],
2864
            barLeft,
2865
            barRight,
2866
            fillStyleCallback,
2867
            axisx,
2868
            axisy,
2869
            ctx,
2870
            series.bars.horizontal,
2871
            series.bars.lineWidth
2872
          );
2873
        }
2874
      }
2875

2876
      ctx.save();
2877
      ctx.translate(plotOffset.left, plotOffset.top);
2878

2879
      // FIXME: figure out a way to add shadows (for instance along the right edge)
2880
      ctx.lineWidth = series.bars.lineWidth;
2881
      ctx.strokeStyle = series.color;
2882

2883
      let barLeft;
2884

2885
      switch (series.bars.align) {
2886
        case 'left':
2887
          barLeft = 0;
2888
          break;
2889
        case 'right':
2890
          barLeft = -series.bars.barWidth;
2891
          break;
2892
        default:
2893
          barLeft = -series.bars.barWidth / 2;
2894
      }
2895

2896
      var fillStyleCallback = series.bars.fill
2897
        ? function(bottom, top) {
2898
            return getFillStyle(series.bars, series.color, bottom, top);
2899
          }
2900
        : null;
2901
      plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis);
2902
      ctx.restore();
2903
    }
2904

2905
    function getFillStyle(filloptions, seriesColor, bottom, top) {
2906
      let fill = filloptions.fill;
2907
      if (!fill) return null;
2908

2909
      if (filloptions.fillColor) return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
2910

2911
      let c = $.color.parse(seriesColor);
2912
      c.a = typeof fill == 'number' ? fill : 0.4;
2913
      c.normalize();
2914
      return c.toString();
2915
    }
2916

2917
    function insertLegend() {
2918
      if (options.legend.container != null) {
2919
        $(options.legend.container).html('');
2920
      } else {
2921
        placeholder.find('.legend').remove();
2922
      }
2923

2924
      if (!options.legend.show) {
2925
        return;
2926
      }
2927

2928
      var fragments = [],
2929
        entries = [],
2930
        rowStarted = false,
2931
        lf = options.legend.labelFormatter,
2932
        s,
2933
        label;
2934

2935
      // Build a list of legend entries, with each having a label and a color
2936

2937
      for (var i = 0; i < series.length; ++i) {
2938
        s = series[i];
2939
        if (s.label) {
2940
          label = lf ? lf(s.label, s) : s.label;
2941
          if (label) {
2942
            entries.push({
2943
              label: label,
2944
              color: s.color,
2945
            });
2946
          }
2947
        }
2948
      }
2949

2950
      // Sort the legend using either the default or a custom comparator
2951

2952
      if (options.legend.sorted) {
2953
        if ($.isFunction(options.legend.sorted)) {
2954
          entries.sort(options.legend.sorted);
2955
        } else if (options.legend.sorted == 'reverse') {
2956
          entries.reverse();
2957
        } else {
2958
          var ascending = options.legend.sorted != 'descending';
2959
          entries.sort(function(a, b) {
2960
            // eslint-disable-next-line
2961
            return a.label == b.label ? 0 : a.label < b.label != ascending ? 1 : -1; // Logical XOR
2962
          });
2963
        }
2964
      }
2965

2966
      // Generate markup for the list of entries, in their final order
2967

2968
      for (var i = 0; i < entries.length; ++i) {
2969
        let entry = entries[i];
2970

2971
        if (i % options.legend.noColumns == 0) {
2972
          if (rowStarted) fragments.push('</tr>');
2973
          fragments.push('<tr>');
2974
          rowStarted = true;
2975
        }
2976

2977
        fragments.push(
2978
          '<td class="legendColorBox"><div style="border:1px solid ' +
2979
            options.legend.labelBoxBorderColor +
2980
            ';padding:1px"><div style="width:4px;height:0;border:5px solid ' +
2981
            entry.color +
2982
            ';overflow:hidden"></div></div></td>' +
2983
            '<td class="legendLabel">' +
2984
            entry.label +
2985
            '</td>'
2986
        );
2987
      }
2988

2989
      if (rowStarted) fragments.push('</tr>');
2990

2991
      if (fragments.length == 0) return;
2992

2993
      var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join('') + '</table>';
2994
      if (options.legend.container != null) $(options.legend.container).html(table);
2995
      else {
2996
        var pos = '',
2997
          p = options.legend.position,
2998
          m = options.legend.margin;
2999
        if (m[0] == null) m = [m, m];
3000
        if (p.charAt(0) == 'n') pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
3001
        else if (p.charAt(0) == 's') pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
3002
        if (p.charAt(1) == 'e') pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
3003
        else if (p.charAt(1) == 'w') pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
3004
        var legend = $(
3005
          '<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos + ';') + '</div>'
3006
        ).appendTo(placeholder);
3007
        if (options.legend.backgroundOpacity != 0.0) {
3008
          // put in the transparent background
3009
          // separately to avoid blended labels and
3010
          // label boxes
3011
          let c = options.legend.backgroundColor;
3012
          if (c == null) {
3013
            c = options.grid.backgroundColor;
3014
            if (c && typeof c == 'string') c = $.color.parse(c);
3015
            else c = $.color.extract(legend, 'background-color');
3016
            c.a = 1;
3017
            c = c.toString();
3018
          }
3019
          let div = legend.children();
3020
          $(
3021
            '<div style="position:absolute;width:' +
3022
              div.width() +
3023
              'px;height:' +
3024
              div.height() +
3025
              'px;' +
3026
              pos +
3027
              'background-color:' +
3028
              c +
3029
              ';"> </div>'
3030
          )
3031
            .prependTo(legend)
3032
            .css('opacity', options.legend.backgroundOpacity);
3033
        }
3034
      }
3035
    }
3036

3037
    // interactive features
3038

3039
    var highlights = [],
3040
      redrawTimeout = null;
3041

3042
    // returns the data item the mouse is over, or null if none is found
3043
    function findNearbyItem(mouseX, mouseY, seriesFilter) {
3044
      let maxDistance = options.grid.mouseActiveRadius,
3045
        smallestDistance = maxDistance * maxDistance + 1,
3046
        item = null,
3047
        i,
3048
        j,
3049
        ps;
3050

3051
      for (i = series.length - 1; i >= 0; --i) {
3052
        if (!seriesFilter(series[i])) continue;
3053

3054
        let s = series[i],
3055
          axisx = s.xaxis,
3056
          axisy = s.yaxis,
3057
          points = s.datapoints.points,
3058
          mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
3059
          my = axisy.c2p(mouseY),
3060
          maxx = maxDistance / axisx.scale,
3061
          maxy = maxDistance / axisy.scale;
3062

3063
        ps = s.datapoints.pointsize;
3064
        // with inverse transforms, we can't use the maxx/maxy
3065
        // optimization, sadly
3066
        if (axisx.options.inverseTransform) maxx = Number.MAX_VALUE;
3067
        if (axisy.options.inverseTransform) maxy = Number.MAX_VALUE;
3068

3069
        if (s.lines.show || s.points.show) {
3070
          for (j = 0; j < points.length; j += ps) {
3071
            var x = points[j],
3072
              y = points[j + 1];
3073
            if (x == null) continue;
3074

3075
            // For points and lines, the cursor must be within a
3076
            // certain distance to the data point
3077
            if (x - mx > maxx || x - mx < -maxx || y - my > maxy || y - my < -maxy) continue;
3078

3079
            // We have to calculate distances in pixels, not in
3080
            // data units, because the scales of the axes may be different
3081
            let dx = Math.abs(axisx.p2c(x) - mouseX),
3082
              dy = Math.abs(axisy.p2c(y) - mouseY),
3083
              dist = dx * dx + dy * dy; // we save the sqrt
3084

3085
            // use <= to ensure last point takes precedence
3086
            // (last generally means on top of)
3087
            if (dist < smallestDistance) {
3088
              smallestDistance = dist;
3089
              item = [i, j / ps];
3090
            }
3091
          }
3092
        }
3093

3094
        if (s.bars.show && !item) {
3095
          // no other point can be nearby
3096

3097
          var barLeft, barRight;
3098

3099
          switch (s.bars.align) {
3100
            case 'left':
3101
              barLeft = 0;
3102
              break;
3103
            case 'right':
3104
              barLeft = -s.bars.barWidth;
3105
              break;
3106
            default:
3107
              barLeft = -s.bars.barWidth / 2;
3108
          }
3109

3110
          barRight = barLeft + s.bars.barWidth;
3111

3112
          for (j = 0; j < points.length; j += ps) {
3113
            var x = points[j],
3114
              y = points[j + 1],
3115
              b = points[j + 2];
3116
            if (x == null) continue;
3117

3118
            // for a bar graph, the cursor must be inside the bar
3119
            if (
3120
              series[i].bars.horizontal
3121
                ? mx <= Math.max(b, x) && mx >= Math.min(b, x) && my >= y + barLeft && my <= y + barRight
3122
                : mx >= x + barLeft && mx <= x + barRight && my >= Math.min(b, y) && my <= Math.max(b, y)
3123
            )
3124
              item = [i, j / ps];
3125
          }
3126
        }
3127
      }
3128

3129
      if (item) {
3130
        i = item[0];
3131
        j = item[1];
3132
        ps = series[i].datapoints.pointsize;
3133

3134
        return {
3135
          datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
3136
          dataIndex: j,
3137
          series: series[i],
3138
          seriesIndex: i,
3139
        };
3140
      }
3141

3142
      return null;
3143
    }
3144

3145
    function onMouseMove(e) {
3146
      if (options.grid.hoverable)
3147
        triggerClickHoverEvent('plothover', e, function(s) {
3148
          return s['hoverable'] != false;
3149
        });
3150
    }
3151

3152
    function onMouseLeave(e) {
3153
      if (options.grid.hoverable)
3154
        triggerClickHoverEvent('plothover', e, function() {
3155
          return false;
3156
        });
3157
    }
3158

3159
    function onClick(e) {
3160
      if (plot.isSelecting) {
3161
        return;
3162
      }
3163

3164
      triggerClickHoverEvent('plotclick', e, function(s) {
3165
        return s['clickable'] != false;
3166
      });
3167
    }
3168

3169
    // trigger click or hover event (they send the same parameters
3170
    // so we share their code)
3171
    function triggerClickHoverEvent(eventname, event, seriesFilter) {
3172
      let offset = eventHolder.offset(),
3173
        canvasX = event.pageX - offset.left - plotOffset.left,
3174
        canvasY = event.pageY - offset.top - plotOffset.top,
3175
        pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
3176

3177
      pos.pageX = event.pageX;
3178
      pos.pageY = event.pageY;
3179

3180
      // Add ctrlKey and metaKey to event
3181
      pos.ctrlKey = event.ctrlKey;
3182
      pos.metaKey = event.metaKey;
3183

3184
      let item = findNearbyItem(canvasX, canvasY, seriesFilter);
3185

3186
      if (item) {
3187
        // fill in mouse pos for any listeners out there
3188
        item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10);
3189
        item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10);
3190
      }
3191

3192
      if (options.grid.autoHighlight) {
3193
        // clear auto-highlights
3194
        for (let i = 0; i < highlights.length; ++i) {
3195
          let h = highlights[i];
3196
          if (
3197
            h.auto == eventname &&
3198
            !(item && h.series == item.series && h.point[0] == item.datapoint[0] && h.point[1] == item.datapoint[1])
3199
          )
3200
            unhighlight(h.series, h.point);
3201
        }
3202

3203
        if (item) highlight(item.series, item.datapoint, eventname);
3204
      }
3205

3206
      placeholder.trigger(eventname, [pos, item]);
3207
    }
3208

3209
    function triggerRedrawOverlay() {
3210
      let t = options.interaction.redrawOverlayInterval;
3211
      if (t == -1) {
3212
        // skip event queue
3213
        drawOverlay();
3214
        return;
3215
      }
3216

3217
      if (!redrawTimeout) redrawTimeout = setTimeout(drawOverlay, t);
3218
    }
3219

3220
    function drawOverlay() {
3221
      redrawTimeout = null;
3222

3223
      // draw highlights
3224
      octx.save();
3225
      overlay.clear();
3226
      octx.translate(plotOffset.left, plotOffset.top);
3227

3228
      let i, hi;
3229
      for (i = 0; i < highlights.length; ++i) {
3230
        hi = highlights[i];
3231

3232
        if (hi.series.bars.show) drawBarHighlight(hi.series, hi.point);
3233
        else drawPointHighlight(hi.series, hi.point);
3234
      }
3235
      octx.restore();
3236

3237
      executeHooks(hooks.drawOverlay, [octx]);
3238
    }
3239

3240
    function highlight(s, point, auto) {
3241
      if (typeof s == 'number') s = series[s];
3242

3243
      if (typeof point == 'number') {
3244
        let ps = s.datapoints.pointsize;
3245
        point = s.datapoints.points.slice(ps * point, ps * (point + 1));
3246
      }
3247

3248
      let i = indexOfHighlight(s, point);
3249
      if (i == -1) {
3250
        highlights.push({ series: s, point: point, auto: auto });
3251

3252
        triggerRedrawOverlay();
3253
      } else if (!auto) highlights[i].auto = false;
3254
    }
3255

3256
    function unhighlight(s, point) {
3257
      if (s == null && point == null) {
3258
        highlights = [];
3259
        triggerRedrawOverlay();
3260
        return;
3261
      }
3262

3263
      if (typeof s == 'number') s = series[s];
3264

3265
      if (typeof point == 'number') {
3266
        let ps = s.datapoints.pointsize;
3267
        point = s.datapoints.points.slice(ps * point, ps * (point + 1));
3268
      }
3269

3270
      let i = indexOfHighlight(s, point);
3271
      if (i != -1) {
3272
        highlights.splice(i, 1);
3273

3274
        triggerRedrawOverlay();
3275
      }
3276
    }
3277

3278
    function indexOfHighlight(s, p) {
3279
      for (let i = 0; i < highlights.length; ++i) {
3280
        let h = highlights[i];
3281
        if (h.series == s && h.point[0] == p[0] && h.point[1] == p[1]) return i;
3282
      }
3283
      return -1;
3284
    }
3285

3286
    function drawPointHighlight(series, point) {
3287
      var x = point[0],
3288
        y = point[1],
3289
        axisx = series.xaxis,
3290
        axisy = series.yaxis,
3291
        highlightColor =
3292
          typeof series.highlightColor === 'string'
3293
            ? series.highlightColor
3294
            : $.color
3295
                .parse(series.color)
3296
                .scale('a', 0.5)
3297
                .toString();
3298

3299
      if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) return;
3300

3301
      let pointRadius = series.points.radius + series.points.lineWidth / 2;
3302
      octx.lineWidth = pointRadius;
3303
      octx.strokeStyle = highlightColor;
3304
      let radius = 1.5 * pointRadius;
3305
      x = axisx.p2c(x);
3306
      y = axisy.p2c(y);
3307

3308
      octx.beginPath();
3309
      if (series.points.symbol == 'circle') octx.arc(x, y, radius, 0, 2 * Math.PI, false);
3310
      else series.points.symbol(octx, x, y, radius, false);
3311
      octx.closePath();
3312
      octx.stroke();
3313
    }
3314

3315
    function drawBarHighlight(series, point) {
3316
      var highlightColor =
3317
          typeof series.highlightColor === 'string'
3318
            ? series.highlightColor
3319
            : $.color
3320
                .parse(series.color)
3321
                .scale('a', 0.5)
3322
                .toString(),
3323
        fillStyle = highlightColor,
3324
        barLeft;
3325

3326
      switch (series.bars.align) {
3327
        case 'left':
3328
          barLeft = 0;
3329
          break;
3330
        case 'right':
3331
          barLeft = -series.bars.barWidth;
3332
          break;
3333
        default:
3334
          barLeft = -series.bars.barWidth / 2;
3335
      }
3336

3337
      octx.lineWidth = series.bars.lineWidth;
3338
      octx.strokeStyle = highlightColor;
3339

3340
      drawBar(
3341
        point[0],
3342
        point[1],
3343
        point[2] || 0,
3344
        barLeft,
3345
        barLeft + series.bars.barWidth,
3346
        function() {
3347
          return fillStyle;
3348
        },
3349
        series.xaxis,
3350
        series.yaxis,
3351
        octx,
3352
        series.bars.horizontal,
3353
        series.bars.lineWidth
3354
      );
3355
    }
3356

3357
    function getColorOrGradient(spec, bottom, top, defaultColor) {
3358
      if (typeof spec == 'string') return spec;
3359
      else {
3360
        // assume this is a gradient spec; IE currently only
3361
        // supports a simple vertical gradient properly, so that's
3362
        // what we support too
3363
        let gradient = ctx.createLinearGradient(0, top, 0, bottom);
3364

3365
        for (let i = 0, l = spec.colors.length; i < l; ++i) {
3366
          let c = spec.colors[i];
3367
          if (typeof c != 'string') {
3368
            let co = $.color.parse(defaultColor);
3369
            if (c.brightness != null) co = co.scale('rgb', c.brightness);
3370
            if (c.opacity != null) co.a *= c.opacity;
3371
            c = co.toString();
3372
          }
3373
          gradient.addColorStop(i / (l - 1), c);
3374
        }
3375

3376
        return gradient;
3377
      }
3378
    }
3379
  }
3380

3381
  // Add the plot function to the top level of the jQuery object
3382

3383
  $.plot = function(placeholder, data, options) {
3384
    //var t0 = new Date();
3385
    let plot = new Plot($(placeholder), data, options, $.plot.plugins);
3386
    //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
3387
    return plot;
3388
  };
3389

3390
  $.plot.version = '0.8.3';
3391

3392
  $.plot.plugins = [];
3393

3394
  // Also add the plot function as a chainable property
3395

3396
  $.fn.plot = function(data, options) {
3397
    return this.each(function() {
3398
      $.plot(this, data, options);
3399
    });
3400
  };
3401

3402
  // round to nearby lower multiple of base
3403
  function floorInBase(n, base) {
3404
    return base * Math.floor(n / base);
3405
  }
3406
})(window.jQuery);
3407

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.