LaravelTest

Форк
0
5215 строк · 122.2 Кб
1
/**
2
* Copyright (c) 2021, Leon Sorokin
3
* All rights reserved. (MIT Licensed)
4
*
5
* uPlot.js (μPlot)
6
* A small, fast chart for time series, lines, areas, ohlc & bars
7
* https://github.com/leeoniya/uPlot (v1.6.18)
8
*/
9

10
var uPlot = (function () {
11
	'use strict';
12

13
	const FEAT_TIME          = true;
14

15
	// binary search for index of closest value
16
	function closestIdx(num, arr, lo, hi) {
17
		let mid;
18
		lo = lo || 0;
19
		hi = hi || arr.length - 1;
20
		let bitwise = hi <= 2147483647;
21

22
		while (hi - lo > 1) {
23
			mid = bitwise ? (lo + hi) >> 1 : floor((lo + hi) / 2);
24

25
			if (arr[mid] < num)
26
				lo = mid;
27
			else
28
				hi = mid;
29
		}
30

31
		if (num - arr[lo] <= arr[hi] - num)
32
			return lo;
33

34
		return hi;
35
	}
36

37
	function nonNullIdx(data, _i0, _i1, dir) {
38
		for (let i = dir == 1 ? _i0 : _i1; i >= _i0 && i <= _i1; i += dir) {
39
			if (data[i] != null)
40
				return i;
41
		}
42

43
		return -1;
44
	}
45

46
	function getMinMax(data, _i0, _i1, sorted) {
47
	//	console.log("getMinMax()");
48

49
		let _min = inf;
50
		let _max = -inf;
51

52
		if (sorted == 1) {
53
			_min = data[_i0];
54
			_max = data[_i1];
55
		}
56
		else if (sorted == -1) {
57
			_min = data[_i1];
58
			_max = data[_i0];
59
		}
60
		else {
61
			for (let i = _i0; i <= _i1; i++) {
62
				if (data[i] != null) {
63
					_min = min(_min, data[i]);
64
					_max = max(_max, data[i]);
65
				}
66
			}
67
		}
68

69
		return [_min, _max];
70
	}
71

72
	function getMinMaxLog(data, _i0, _i1) {
73
	//	console.log("getMinMax()");
74

75
		let _min = inf;
76
		let _max = -inf;
77

78
		for (let i = _i0; i <= _i1; i++) {
79
			if (data[i] > 0) {
80
				_min = min(_min, data[i]);
81
				_max = max(_max, data[i]);
82
			}
83
		}
84

85
		return [
86
			_min ==  inf ?  1 : _min,
87
			_max == -inf ? 10 : _max,
88
		];
89
	}
90

91
	const _fixedTuple = [0, 0];
92

93
	function fixIncr(minIncr, maxIncr, minExp, maxExp) {
94
		_fixedTuple[0] = minExp < 0 ? roundDec(minIncr, -minExp) : minIncr;
95
		_fixedTuple[1] = maxExp < 0 ? roundDec(maxIncr, -maxExp) : maxIncr;
96
		return _fixedTuple;
97
	}
98

99
	function rangeLog(min, max, base, fullMags) {
100
		let minSign = sign(min);
101

102
		let logFn = base == 10 ? log10 : log2;
103

104
		if (min == max) {
105
			if (minSign == -1) {
106
				min *= base;
107
				max /= base;
108
			}
109
			else {
110
				min /= base;
111
				max *= base;
112
			}
113
		}
114

115
		let minExp, maxExp, minMaxIncrs;
116

117
		if (fullMags) {
118
			minExp = floor(logFn(min));
119
			maxExp =  ceil(logFn(max));
120

121
			minMaxIncrs = fixIncr(pow(base, minExp), pow(base, maxExp), minExp, maxExp);
122

123
			min = minMaxIncrs[0];
124
			max = minMaxIncrs[1];
125
		}
126
		else {
127
			minExp = floor(logFn(abs(min)));
128
			maxExp = floor(logFn(abs(max)));
129

130
			minMaxIncrs = fixIncr(pow(base, minExp), pow(base, maxExp), minExp, maxExp);
131

132
			min = incrRoundDn(min, minMaxIncrs[0]);
133
			max = incrRoundUp(max, minMaxIncrs[1]);
134
		}
135

136
		return [min, max];
137
	}
138

139
	function rangeAsinh(min, max, base, fullMags) {
140
		let minMax = rangeLog(min, max, base, fullMags);
141

142
		if (min == 0)
143
			minMax[0] = 0;
144

145
		if (max == 0)
146
			minMax[1] = 0;
147

148
		return minMax;
149
	}
150

151
	const rangePad = 0.1;
152

153
	const autoRangePart = {
154
		mode: 3,
155
		pad: rangePad,
156
	};
157

158
	const _eqRangePart = {
159
		pad:  0,
160
		soft: null,
161
		mode: 0,
162
	};
163

164
	const _eqRange = {
165
		min: _eqRangePart,
166
		max: _eqRangePart,
167
	};
168

169
	// this ensures that non-temporal/numeric y-axes get multiple-snapped padding added above/below
170
	// TODO: also account for incrs when snapping to ensure top of axis gets a tick & value
171
	function rangeNum(_min, _max, mult, extra) {
172
		if (isObj(mult))
173
			return _rangeNum(_min, _max, mult);
174

175
		_eqRangePart.pad  = mult;
176
		_eqRangePart.soft = extra ? 0 : null;
177
		_eqRangePart.mode = extra ? 3 : 0;
178

179
		return _rangeNum(_min, _max, _eqRange);
180
	}
181

182
	// nullish coalesce
183
	function ifNull(lh, rh) {
184
		return lh == null ? rh : lh;
185
	}
186

187
	// checks if given index range in an array contains a non-null value
188
	// aka a range-bounded Array.some()
189
	function hasData(data, idx0, idx1) {
190
		idx0 = ifNull(idx0, 0);
191
		idx1 = ifNull(idx1, data.length - 1);
192

193
		while (idx0 <= idx1) {
194
			if (data[idx0] != null)
195
				return true;
196
			idx0++;
197
		}
198

199
		return false;
200
	}
201

202
	function _rangeNum(_min, _max, cfg) {
203
		let cmin = cfg.min;
204
		let cmax = cfg.max;
205

206
		let padMin = ifNull(cmin.pad, 0);
207
		let padMax = ifNull(cmax.pad, 0);
208

209
		let hardMin = ifNull(cmin.hard, -inf);
210
		let hardMax = ifNull(cmax.hard,  inf);
211

212
		let softMin = ifNull(cmin.soft,  inf);
213
		let softMax = ifNull(cmax.soft, -inf);
214

215
		let softMinMode = ifNull(cmin.mode, 0);
216
		let softMaxMode = ifNull(cmax.mode, 0);
217

218
		let delta        = _max - _min;
219

220
		// this handles situations like 89.7, 89.69999999999999
221
		// by assuming 0.001x deltas are precision errors
222
	//	if (delta > 0 && delta < abs(_max) / 1e3)
223
	//		delta = 0;
224

225
		// treat data as flat if delta is less than 1 billionth
226
		if (delta < 1e-9) {
227
			delta = 0;
228

229
			// if soft mode is 2 and all vals are flat at 0, avoid the 0.1 * 1e3 fallback
230
			// this prevents 0,0,0 from ranging to -100,100 when softMin/softMax are -1,1
231
			if (_min == 0 || _max == 0) {
232
				delta = 1e-9;
233

234
				if (softMinMode == 2 && softMin != inf)
235
					padMin = 0;
236

237
				if (softMaxMode == 2 && softMax != -inf)
238
					padMax = 0;
239
			}
240
		}
241

242
		let nonZeroDelta = delta || abs(_max) || 1e3;
243
		let mag          = log10(nonZeroDelta);
244
		let base         = pow(10, floor(mag));
245

246
		let _padMin  = nonZeroDelta * (delta == 0 ? (_min == 0 ? .1 : 1) : padMin);
247
		let _newMin  = roundDec(incrRoundDn(_min - _padMin, base/10), 9);
248
		let _softMin = _min >= softMin && (softMinMode == 1 || softMinMode == 3 && _newMin <= softMin || softMinMode == 2 && _newMin >= softMin) ? softMin : inf;
249
		let minLim   = max(hardMin, _newMin < _softMin && _min >= _softMin ? _softMin : min(_softMin, _newMin));
250

251
		let _padMax  = nonZeroDelta * (delta == 0 ? (_max == 0 ? .1 : 1) : padMax);
252
		let _newMax  = roundDec(incrRoundUp(_max + _padMax, base/10), 9);
253
		let _softMax = _max <= softMax && (softMaxMode == 1 || softMaxMode == 3 && _newMax >= softMax || softMaxMode == 2 && _newMax <= softMax) ? softMax : -inf;
254
		let maxLim   = min(hardMax, _newMax > _softMax && _max <= _softMax ? _softMax : max(_softMax, _newMax));
255

256
		if (minLim == maxLim && minLim == 0)
257
			maxLim = 100;
258

259
		return [minLim, maxLim];
260
	}
261

262
	// alternative: https://stackoverflow.com/a/2254896
263
	const fmtNum = new Intl.NumberFormat(navigator.language).format;
264

265
	const M = Math;
266

267
	const PI = M.PI;
268
	const abs = M.abs;
269
	const floor = M.floor;
270
	const round = M.round;
271
	const ceil = M.ceil;
272
	const min = M.min;
273
	const max = M.max;
274
	const pow = M.pow;
275
	const sign = M.sign;
276
	const log10 = M.log10;
277
	const log2 = M.log2;
278
	// TODO: seems like this needs to match asinh impl if the passed v is tweaked?
279
	const sinh =  (v, linthresh = 1) => M.sinh(v) * linthresh;
280
	const asinh = (v, linthresh = 1) => M.asinh(v / linthresh);
281

282
	const inf = Infinity;
283

284
	function numIntDigits(x) {
285
		return (log10((x ^ (x >> 31)) - (x >> 31)) | 0) + 1;
286
	}
287

288
	function incrRound(num, incr) {
289
		return round(num/incr)*incr;
290
	}
291

292
	function clamp(num, _min, _max) {
293
		return min(max(num, _min), _max);
294
	}
295

296
	function fnOrSelf(v) {
297
		return typeof v == "function" ? v : () => v;
298
	}
299

300
	const retArg0 = _0 => _0;
301

302
	const retArg1 = (_0, _1) => _1;
303

304
	const retNull = _ => null;
305

306
	const retTrue = _ => true;
307

308
	const retEq = (a, b) => a == b;
309

310
	function incrRoundUp(num, incr) {
311
		return ceil(num/incr)*incr;
312
	}
313

314
	function incrRoundDn(num, incr) {
315
		return floor(num/incr)*incr;
316
	}
317

318
	function roundDec(val, dec) {
319
		return round(val * (dec = 10**dec)) / dec;
320
	}
321

322
	const fixedDec = new Map();
323

324
	function guessDec(num) {
325
		return ((""+num).split(".")[1] || "").length;
326
	}
327

328
	function genIncrs(base, minExp, maxExp, mults) {
329
		let incrs = [];
330

331
		let multDec = mults.map(guessDec);
332

333
		for (let exp = minExp; exp < maxExp; exp++) {
334
			let expa = abs(exp);
335
			let mag = roundDec(pow(base, exp), expa);
336

337
			for (let i = 0; i < mults.length; i++) {
338
				let _incr = mults[i] * mag;
339
				let dec = (_incr >= 0 && exp >= 0 ? 0 : expa) + (exp >= multDec[i] ? 0 : multDec[i]);
340
				let incr = roundDec(_incr, dec);
341
				incrs.push(incr);
342
				fixedDec.set(incr, dec);
343
			}
344
		}
345

346
		return incrs;
347
	}
348

349
	//export const assign = Object.assign;
350

351
	const EMPTY_OBJ = {};
352
	const EMPTY_ARR = [];
353

354
	const nullNullTuple = [null, null];
355

356
	const isArr = Array.isArray;
357

358
	function isStr(v) {
359
		return typeof v == 'string';
360
	}
361

362
	function isObj(v) {
363
		let is = false;
364

365
		if (v != null) {
366
			let c = v.constructor;
367
			is = c == null || c == Object;
368
		}
369

370
		return is;
371
	}
372

373
	function fastIsObj(v) {
374
		return v != null && typeof v == 'object';
375
	}
376

377
	function copy(o, _isObj = isObj) {
378
		let out;
379

380
		if (isArr(o)) {
381
			let val = o.find(v => v != null);
382

383
			if (isArr(val) || _isObj(val)) {
384
				out = Array(o.length);
385
				for (let i = 0; i < o.length; i++)
386
				  out[i] = copy(o[i], _isObj);
387
			}
388
			else
389
				out = o.slice();
390
		}
391
		else if (_isObj(o)) {
392
			out = {};
393
			for (let k in o)
394
				out[k] = copy(o[k], _isObj);
395
		}
396
		else
397
			out = o;
398

399
		return out;
400
	}
401

402
	function assign(targ) {
403
		let args = arguments;
404

405
		for (let i = 1; i < args.length; i++) {
406
			let src = args[i];
407

408
			for (let key in src) {
409
				if (isObj(targ[key]))
410
					assign(targ[key], copy(src[key]));
411
				else
412
					targ[key] = copy(src[key]);
413
			}
414
		}
415

416
		return targ;
417
	}
418

419
	// nullModes
420
	const NULL_REMOVE = 0;  // nulls are converted to undefined (e.g. for spanGaps: true)
421
	const NULL_RETAIN = 1;  // nulls are retained, with alignment artifacts set to undefined (default)
422
	const NULL_EXPAND = 2;  // nulls are expanded to include any adjacent alignment artifacts
423

424
	// sets undefined values to nulls when adjacent to existing nulls (minesweeper)
425
	function nullExpand(yVals, nullIdxs, alignedLen) {
426
		for (let i = 0, xi, lastNullIdx = -1; i < nullIdxs.length; i++) {
427
			let nullIdx = nullIdxs[i];
428

429
			if (nullIdx > lastNullIdx) {
430
				xi = nullIdx - 1;
431
				while (xi >= 0 && yVals[xi] == null)
432
					yVals[xi--] = null;
433

434
				xi = nullIdx + 1;
435
				while (xi < alignedLen && yVals[xi] == null)
436
					yVals[lastNullIdx = xi++] = null;
437
			}
438
		}
439
	}
440

441
	// nullModes is a tables-matched array indicating how to treat nulls in each series
442
	// output is sorted ASC on the joined field (table[0]) and duplicate join values are collapsed
443
	function join(tables, nullModes) {
444
		let xVals = new Set();
445

446
		for (let ti = 0; ti < tables.length; ti++) {
447
			let t = tables[ti];
448
			let xs = t[0];
449
			let len = xs.length;
450

451
			for (let i = 0; i < len; i++)
452
				xVals.add(xs[i]);
453
		}
454

455
		let data = [Array.from(xVals).sort((a, b) => a - b)];
456

457
		let alignedLen = data[0].length;
458

459
		let xIdxs = new Map();
460

461
		for (let i = 0; i < alignedLen; i++)
462
			xIdxs.set(data[0][i], i);
463

464
		for (let ti = 0; ti < tables.length; ti++) {
465
			let t = tables[ti];
466
			let xs = t[0];
467

468
			for (let si = 1; si < t.length; si++) {
469
				let ys = t[si];
470

471
				let yVals = Array(alignedLen).fill(undefined);
472

473
				let nullMode = nullModes ? nullModes[ti][si] : NULL_RETAIN;
474

475
				let nullIdxs = [];
476

477
				for (let i = 0; i < ys.length; i++) {
478
					let yVal = ys[i];
479
					let alignedIdx = xIdxs.get(xs[i]);
480

481
					if (yVal === null) {
482
						if (nullMode != NULL_REMOVE) {
483
							yVals[alignedIdx] = yVal;
484

485
							if (nullMode == NULL_EXPAND)
486
								nullIdxs.push(alignedIdx);
487
						}
488
					}
489
					else
490
						yVals[alignedIdx] = yVal;
491
				}
492

493
				nullExpand(yVals, nullIdxs, alignedLen);
494

495
				data.push(yVals);
496
			}
497
		}
498

499
		return data;
500
	}
501

502
	const microTask = typeof queueMicrotask == "undefined" ? fn => Promise.resolve().then(fn) : queueMicrotask;
503

504
	const WIDTH       = "width";
505
	const HEIGHT      = "height";
506
	const TOP         = "top";
507
	const BOTTOM      = "bottom";
508
	const LEFT        = "left";
509
	const RIGHT       = "right";
510
	const hexBlack    = "#000";
511
	const transparent = hexBlack + "0";
512

513
	const mousemove   = "mousemove";
514
	const mousedown   = "mousedown";
515
	const mouseup     = "mouseup";
516
	const mouseenter  = "mouseenter";
517
	const mouseleave  = "mouseleave";
518
	const dblclick    = "dblclick";
519
	const resize      = "resize";
520
	const scroll      = "scroll";
521

522
	const change      = "change";
523
	const dppxchange  = "dppxchange";
524

525
	const pre = "u-";
526

527
	const UPLOT          =       "uplot";
528
	const ORI_HZ         = pre + "hz";
529
	const ORI_VT         = pre + "vt";
530
	const TITLE          = pre + "title";
531
	const WRAP           = pre + "wrap";
532
	const UNDER          = pre + "under";
533
	const OVER           = pre + "over";
534
	const AXIS           = pre + "axis";
535
	const OFF            = pre + "off";
536
	const SELECT         = pre + "select";
537
	const CURSOR_X       = pre + "cursor-x";
538
	const CURSOR_Y       = pre + "cursor-y";
539
	const CURSOR_PT      = pre + "cursor-pt";
540
	const LEGEND         = pre + "legend";
541
	const LEGEND_LIVE    = pre + "live";
542
	const LEGEND_INLINE  = pre + "inline";
543
	const LEGEND_THEAD   = pre + "thead";
544
	const LEGEND_SERIES  = pre + "series";
545
	const LEGEND_MARKER  = pre + "marker";
546
	const LEGEND_LABEL   = pre + "label";
547
	const LEGEND_VALUE   = pre + "value";
548

549
	const doc = document;
550
	const win = window;
551
	let pxRatio;
552

553
	let query;
554

555
	function setPxRatio() {
556
		let _pxRatio = devicePixelRatio;
557

558
		// during print preview, Chrome fires off these dppx queries even without changes
559
		if (pxRatio != _pxRatio) {
560
			pxRatio = _pxRatio;
561

562
			query && off(change, query, setPxRatio);
563
			query = matchMedia(`(min-resolution: ${pxRatio - 0.001}dppx) and (max-resolution: ${pxRatio + 0.001}dppx)`);
564
			on(change, query, setPxRatio);
565

566
			win.dispatchEvent(new CustomEvent(dppxchange));
567
		}
568
	}
569

570
	function addClass(el, c) {
571
		if (c != null) {
572
			let cl = el.classList;
573
			!cl.contains(c) && cl.add(c);
574
		}
575
	}
576

577
	function remClass(el, c) {
578
		let cl = el.classList;
579
		cl.contains(c) && cl.remove(c);
580
	}
581

582
	function setStylePx(el, name, value) {
583
		el.style[name] = value + "px";
584
	}
585

586
	function placeTag(tag, cls, targ, refEl) {
587
		let el = doc.createElement(tag);
588

589
		if (cls != null)
590
			addClass(el, cls);
591

592
		if (targ != null)
593
			targ.insertBefore(el, refEl);
594

595
		return el;
596
	}
597

598
	function placeDiv(cls, targ) {
599
		return placeTag("div", cls, targ);
600
	}
601

602
	const xformCache = new WeakMap();
603

604
	function elTrans(el, xPos, yPos, xMax, yMax) {
605
		let xform = "translate(" + xPos + "px," + yPos + "px)";
606
		let xformOld = xformCache.get(el);
607

608
		if (xform != xformOld) {
609
			el.style.transform = xform;
610
			xformCache.set(el, xform);
611

612
			if (xPos < 0 || yPos < 0 || xPos > xMax || yPos > yMax)
613
				addClass(el, OFF);
614
			else
615
				remClass(el, OFF);
616
		}
617
	}
618

619
	const colorCache = new WeakMap();
620

621
	function elColor(el, background, borderColor) {
622
		let newColor = background + borderColor;
623
		let oldColor = colorCache.get(el);
624

625
		if (newColor != oldColor) {
626
			colorCache.set(el, newColor);
627
			el.style.background = background;
628
			el.style.borderColor = borderColor;
629
		}
630
	}
631

632
	const sizeCache = new WeakMap();
633

634
	function elSize(el, newWid, newHgt, centered) {
635
		let newSize = newWid + "" + newHgt;
636
		let oldSize = sizeCache.get(el);
637

638
		if (newSize != oldSize) {
639
			sizeCache.set(el, newSize);
640
			el.style.height = newHgt + "px";
641
			el.style.width = newWid + "px";
642
			el.style.marginLeft = centered ? -newWid/2 + "px" : 0;
643
			el.style.marginTop = centered ? -newHgt/2 + "px" : 0;
644
		}
645
	}
646

647
	const evOpts = {passive: true};
648
	const evOpts2 = assign({capture: true}, evOpts);
649

650
	function on(ev, el, cb, capt) {
651
		el.addEventListener(ev, cb, capt ? evOpts2 : evOpts);
652
	}
653

654
	function off(ev, el, cb, capt) {
655
		el.removeEventListener(ev, cb, capt ? evOpts2 : evOpts);
656
	}
657

658
	setPxRatio();
659

660
	const months = [
661
		"January",
662
		"February",
663
		"March",
664
		"April",
665
		"May",
666
		"June",
667
		"July",
668
		"August",
669
		"September",
670
		"October",
671
		"November",
672
		"December",
673
	];
674

675
	const days = [
676
		"Sunday",
677
		"Monday",
678
		"Tuesday",
679
		"Wednesday",
680
		"Thursday",
681
		"Friday",
682
		"Saturday",
683
	];
684

685
	function slice3(str) {
686
		return str.slice(0, 3);
687
	}
688

689
	const days3 = days.map(slice3);
690

691
	const months3 = months.map(slice3);
692

693
	const engNames = {
694
		MMMM: months,
695
		MMM:  months3,
696
		WWWW: days,
697
		WWW:  days3,
698
	};
699

700
	function zeroPad2(int) {
701
		return (int < 10 ? '0' : '') + int;
702
	}
703

704
	function zeroPad3(int) {
705
		return (int < 10 ? '00' : int < 100 ? '0' : '') + int;
706
	}
707

708
	/*
709
	function suffix(int) {
710
		let mod10 = int % 10;
711

712
		return int + (
713
			mod10 == 1 && int != 11 ? "st" :
714
			mod10 == 2 && int != 12 ? "nd" :
715
			mod10 == 3 && int != 13 ? "rd" : "th"
716
		);
717
	}
718
	*/
719

720
	const subs = {
721
		// 2019
722
		YYYY:	d => d.getFullYear(),
723
		// 19
724
		YY:		d => (d.getFullYear()+'').slice(2),
725
		// July
726
		MMMM:	(d, names) => names.MMMM[d.getMonth()],
727
		// Jul
728
		MMM:	(d, names) => names.MMM[d.getMonth()],
729
		// 07
730
		MM:		d => zeroPad2(d.getMonth()+1),
731
		// 7
732
		M:		d => d.getMonth()+1,
733
		// 09
734
		DD:		d => zeroPad2(d.getDate()),
735
		// 9
736
		D:		d => d.getDate(),
737
		// Monday
738
		WWWW:	(d, names) => names.WWWW[d.getDay()],
739
		// Mon
740
		WWW:	(d, names) => names.WWW[d.getDay()],
741
		// 03
742
		HH:		d => zeroPad2(d.getHours()),
743
		// 3
744
		H:		d => d.getHours(),
745
		// 9 (12hr, unpadded)
746
		h:		d => {let h = d.getHours(); return h == 0 ? 12 : h > 12 ? h - 12 : h;},
747
		// AM
748
		AA:		d => d.getHours() >= 12 ? 'PM' : 'AM',
749
		// am
750
		aa:		d => d.getHours() >= 12 ? 'pm' : 'am',
751
		// a
752
		a:		d => d.getHours() >= 12 ? 'p' : 'a',
753
		// 09
754
		mm:		d => zeroPad2(d.getMinutes()),
755
		// 9
756
		m:		d => d.getMinutes(),
757
		// 09
758
		ss:		d => zeroPad2(d.getSeconds()),
759
		// 9
760
		s:		d => d.getSeconds(),
761
		// 374
762
		fff:	d => zeroPad3(d.getMilliseconds()),
763
	};
764

765
	function fmtDate(tpl, names) {
766
		names = names || engNames;
767
		let parts = [];
768

769
		let R = /\{([a-z]+)\}|[^{]+/gi, m;
770

771
		while (m = R.exec(tpl))
772
			parts.push(m[0][0] == '{' ? subs[m[1]] : m[0]);
773

774
		return d => {
775
			let out = '';
776

777
			for (let i = 0; i < parts.length; i++)
778
				out += typeof parts[i] == "string" ? parts[i] : parts[i](d, names);
779

780
			return out;
781
		}
782
	}
783

784
	const localTz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
785

786
	// https://stackoverflow.com/questions/15141762/how-to-initialize-a-javascript-date-to-a-particular-time-zone/53652131#53652131
787
	function tzDate(date, tz) {
788
		let date2;
789

790
		// perf optimization
791
		if (tz == 'UTC' || tz == 'Etc/UTC')
792
			date2 = new Date(+date + date.getTimezoneOffset() * 6e4);
793
		else if (tz == localTz)
794
			date2 = date;
795
		else {
796
			date2 = new Date(date.toLocaleString('en-US', {timeZone: tz}));
797
			date2.setMilliseconds(date.getMilliseconds());
798
		}
799

800
		return date2;
801
	}
802

803
	//export const series = [];
804

805
	// default formatters:
806

807
	const onlyWhole = v => v % 1 == 0;
808

809
	const allMults = [1,2,2.5,5];
810

811
	// ...0.01, 0.02, 0.025, 0.05, 0.1, 0.2, 0.25, 0.5
812
	const decIncrs = genIncrs(10, -16, 0, allMults);
813

814
	// 1, 2, 2.5, 5, 10, 20, 25, 50...
815
	const oneIncrs = genIncrs(10, 0, 16, allMults);
816

817
	// 1, 2,      5, 10, 20, 25, 50...
818
	const wholeIncrs = oneIncrs.filter(onlyWhole);
819

820
	const numIncrs = decIncrs.concat(oneIncrs);
821

822
	const NL = "\n";
823

824
	const yyyy    = "{YYYY}";
825
	const NLyyyy  = NL + yyyy;
826
	const md      = "{M}/{D}";
827
	const NLmd    = NL + md;
828
	const NLmdyy  = NLmd + "/{YY}";
829

830
	const aa      = "{aa}";
831
	const hmm     = "{h}:{mm}";
832
	const hmmaa   = hmm + aa;
833
	const NLhmmaa = NL + hmmaa;
834
	const ss      = ":{ss}";
835

836
	const _ = null;
837

838
	function genTimeStuffs(ms) {
839
		let	s  = ms * 1e3,
840
			m  = s  * 60,
841
			h  = m  * 60,
842
			d  = h  * 24,
843
			mo = d  * 30,
844
			y  = d  * 365;
845

846
		// min of 1e-3 prevents setting a temporal x ticks too small since Date objects cannot advance ticks smaller than 1ms
847
		let subSecIncrs = ms == 1 ? genIncrs(10, 0, 3, allMults).filter(onlyWhole) : genIncrs(10, -3, 0, allMults);
848

849
		let timeIncrs = subSecIncrs.concat([
850
			// minute divisors (# of secs)
851
			s,
852
			s * 5,
853
			s * 10,
854
			s * 15,
855
			s * 30,
856
			// hour divisors (# of mins)
857
			m,
858
			m * 5,
859
			m * 10,
860
			m * 15,
861
			m * 30,
862
			// day divisors (# of hrs)
863
			h,
864
			h * 2,
865
			h * 3,
866
			h * 4,
867
			h * 6,
868
			h * 8,
869
			h * 12,
870
			// month divisors TODO: need more?
871
			d,
872
			d * 2,
873
			d * 3,
874
			d * 4,
875
			d * 5,
876
			d * 6,
877
			d * 7,
878
			d * 8,
879
			d * 9,
880
			d * 10,
881
			d * 15,
882
			// year divisors (# months, approx)
883
			mo,
884
			mo * 2,
885
			mo * 3,
886
			mo * 4,
887
			mo * 6,
888
			// century divisors
889
			y,
890
			y * 2,
891
			y * 5,
892
			y * 10,
893
			y * 25,
894
			y * 50,
895
			y * 100,
896
		]);
897

898
		// [0]:   minimum num secs in the tick incr
899
		// [1]:   default tick format
900
		// [2-7]: rollover tick formats
901
		// [8]:   mode: 0: replace [1] -> [2-7], 1: concat [1] + [2-7]
902
		const _timeAxisStamps = [
903
		//   tick incr    default          year                    month   day                   hour    min       sec   mode
904
			[y,           yyyy,            _,                      _,      _,                    _,      _,        _,       1],
905
			[d * 28,      "{MMM}",         NLyyyy,                 _,      _,                    _,      _,        _,       1],
906
			[d,           md,              NLyyyy,                 _,      _,                    _,      _,        _,       1],
907
			[h,           "{h}" + aa,      NLmdyy,                 _,      NLmd,                 _,      _,        _,       1],
908
			[m,           hmmaa,           NLmdyy,                 _,      NLmd,                 _,      _,        _,       1],
909
			[s,           ss,              NLmdyy + " " + hmmaa,   _,      NLmd + " " + hmmaa,   _,      NLhmmaa,  _,       1],
910
			[ms,          ss + ".{fff}",   NLmdyy + " " + hmmaa,   _,      NLmd + " " + hmmaa,   _,      NLhmmaa,  _,       1],
911
		];
912

913
		// the ensures that axis ticks, values & grid are aligned to logical temporal breakpoints and not an arbitrary timestamp
914
		// https://www.timeanddate.com/time/dst/
915
		// https://www.timeanddate.com/time/dst/2019.html
916
		// https://www.epochconverter.com/timezones
917
		function timeAxisSplits(tzDate) {
918
			return (self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace) => {
919
				let splits = [];
920
				let isYr = foundIncr >= y;
921
				let isMo = foundIncr >= mo && foundIncr < y;
922

923
				// get the timezone-adjusted date
924
				let minDate = tzDate(scaleMin);
925
				let minDateTs = roundDec(minDate * ms, 3);
926

927
				// get ts of 12am (this lands us at or before the original scaleMin)
928
				let minMin = mkDate(minDate.getFullYear(), isYr ? 0 : minDate.getMonth(), isMo || isYr ? 1 : minDate.getDate());
929
				let minMinTs = roundDec(minMin * ms, 3);
930

931
				if (isMo || isYr) {
932
					let moIncr = isMo ? foundIncr / mo : 0;
933
					let yrIncr = isYr ? foundIncr / y  : 0;
934
				//	let tzOffset = scaleMin - minDateTs;		// needed?
935
					let split = minDateTs == minMinTs ? minDateTs : roundDec(mkDate(minMin.getFullYear() + yrIncr, minMin.getMonth() + moIncr, 1) * ms, 3);
936
					let splitDate = new Date(round(split / ms));
937
					let baseYear = splitDate.getFullYear();
938
					let baseMonth = splitDate.getMonth();
939

940
					for (let i = 0; split <= scaleMax; i++) {
941
						let next = mkDate(baseYear + yrIncr * i, baseMonth + moIncr * i, 1);
942
						let offs = next - tzDate(roundDec(next * ms, 3));
943

944
						split = roundDec((+next + offs) * ms, 3);
945

946
						if (split <= scaleMax)
947
							splits.push(split);
948
					}
949
				}
950
				else {
951
					let incr0 = foundIncr >= d ? d : foundIncr;
952
					let tzOffset = floor(scaleMin) - floor(minDateTs);
953
					let split = minMinTs + tzOffset + incrRoundUp(minDateTs - minMinTs, incr0);
954
					splits.push(split);
955

956
					let date0 = tzDate(split);
957

958
					let prevHour = date0.getHours() + (date0.getMinutes() / m) + (date0.getSeconds() / h);
959
					let incrHours = foundIncr / h;
960

961
					let minSpace = self.axes[axisIdx]._space;
962
					let pctSpace = foundSpace / minSpace;
963

964
					while (1) {
965
						split = roundDec(split + foundIncr, ms == 1 ? 0 : 3);
966

967
						if (split > scaleMax)
968
							break;
969

970
						if (incrHours > 1) {
971
							let expectedHour = floor(roundDec(prevHour + incrHours, 6)) % 24;
972
							let splitDate = tzDate(split);
973
							let actualHour = splitDate.getHours();
974

975
							let dstShift = actualHour - expectedHour;
976

977
							if (dstShift > 1)
978
								dstShift = -1;
979

980
							split -= dstShift * h;
981

982
							prevHour = (prevHour + incrHours) % 24;
983

984
							// add a tick only if it's further than 70% of the min allowed label spacing
985
							let prevSplit = splits[splits.length - 1];
986
							let pctIncr = roundDec((split - prevSplit) / foundIncr, 3);
987

988
							if (pctIncr * pctSpace >= .7)
989
								splits.push(split);
990
						}
991
						else
992
							splits.push(split);
993
					}
994
				}
995

996
				return splits;
997
			}
998
		}
999

1000
		return [
1001
			timeIncrs,
1002
			_timeAxisStamps,
1003
			timeAxisSplits,
1004
		];
1005
	}
1006

1007
	const [ timeIncrsMs, _timeAxisStampsMs, timeAxisSplitsMs ] = genTimeStuffs(1);
1008
	const [ timeIncrsS,  _timeAxisStampsS,  timeAxisSplitsS  ] = genTimeStuffs(1e-3);
1009

1010
	// base 2
1011
	genIncrs(2, -53, 53, [1]);
1012

1013
	/*
1014
	console.log({
1015
		decIncrs,
1016
		oneIncrs,
1017
		wholeIncrs,
1018
		numIncrs,
1019
		timeIncrs,
1020
		fixedDec,
1021
	});
1022
	*/
1023

1024
	function timeAxisStamps(stampCfg, fmtDate) {
1025
		return stampCfg.map(s => s.map((v, i) =>
1026
			i == 0 || i == 8 || v == null ? v : fmtDate(i == 1 || s[8] == 0 ? v : s[1] + v)
1027
		));
1028
	}
1029

1030
	// TODO: will need to accept spaces[] and pull incr into the loop when grid will be non-uniform, eg for log scales.
1031
	// currently we ignore this for months since they're *nearly* uniform and the added complexity is not worth it
1032
	function timeAxisVals(tzDate, stamps) {
1033
		return (self, splits, axisIdx, foundSpace, foundIncr) => {
1034
			let s = stamps.find(s => foundIncr >= s[0]) || stamps[stamps.length - 1];
1035

1036
			// these track boundaries when a full label is needed again
1037
			let prevYear;
1038
			let prevMnth;
1039
			let prevDate;
1040
			let prevHour;
1041
			let prevMins;
1042
			let prevSecs;
1043

1044
			return splits.map(split => {
1045
				let date = tzDate(split);
1046

1047
				let newYear = date.getFullYear();
1048
				let newMnth = date.getMonth();
1049
				let newDate = date.getDate();
1050
				let newHour = date.getHours();
1051
				let newMins = date.getMinutes();
1052
				let newSecs = date.getSeconds();
1053

1054
				let stamp = (
1055
					newYear != prevYear && s[2] ||
1056
					newMnth != prevMnth && s[3] ||
1057
					newDate != prevDate && s[4] ||
1058
					newHour != prevHour && s[5] ||
1059
					newMins != prevMins && s[6] ||
1060
					newSecs != prevSecs && s[7] ||
1061
					                       s[1]
1062
				);
1063

1064
				prevYear = newYear;
1065
				prevMnth = newMnth;
1066
				prevDate = newDate;
1067
				prevHour = newHour;
1068
				prevMins = newMins;
1069
				prevSecs = newSecs;
1070

1071
				return stamp(date);
1072
			});
1073
		}
1074
	}
1075

1076
	// for when axis.values is defined as a static fmtDate template string
1077
	function timeAxisVal(tzDate, dateTpl) {
1078
		let stamp = fmtDate(dateTpl);
1079
		return (self, splits, axisIdx, foundSpace, foundIncr) => splits.map(split => stamp(tzDate(split)));
1080
	}
1081

1082
	function mkDate(y, m, d) {
1083
		return new Date(y, m, d);
1084
	}
1085

1086
	function timeSeriesStamp(stampCfg, fmtDate) {
1087
		return fmtDate(stampCfg);
1088
	}
1089
	const _timeSeriesStamp = '{YYYY}-{MM}-{DD} {h}:{mm}{aa}';
1090

1091
	function timeSeriesVal(tzDate, stamp) {
1092
		return (self, val) => stamp(tzDate(val));
1093
	}
1094

1095
	function legendStroke(self, seriesIdx) {
1096
		let s = self.series[seriesIdx];
1097
		return s.width ? s.stroke(self, seriesIdx) : s.points.width ? s.points.stroke(self, seriesIdx) : null;
1098
	}
1099

1100
	function legendFill(self, seriesIdx) {
1101
		return self.series[seriesIdx].fill(self, seriesIdx);
1102
	}
1103

1104
	const legendOpts = {
1105
		show: true,
1106
		live: true,
1107
		isolate: false,
1108
		markers: {
1109
			show: true,
1110
			width: 2,
1111
			stroke: legendStroke,
1112
			fill: legendFill,
1113
			dash: "solid",
1114
		},
1115
		idx: null,
1116
		idxs: null,
1117
		values: [],
1118
	};
1119

1120
	function cursorPointShow(self, si) {
1121
		let o = self.cursor.points;
1122

1123
		let pt = placeDiv();
1124

1125
		let size = o.size(self, si);
1126
		setStylePx(pt, WIDTH, size);
1127
		setStylePx(pt, HEIGHT, size);
1128

1129
		let mar = size / -2;
1130
		setStylePx(pt, "marginLeft", mar);
1131
		setStylePx(pt, "marginTop", mar);
1132

1133
		let width = o.width(self, si, size);
1134
		width && setStylePx(pt, "borderWidth", width);
1135

1136
		return pt;
1137
	}
1138

1139
	function cursorPointFill(self, si) {
1140
		let sp = self.series[si].points;
1141
		return sp._fill || sp._stroke;
1142
	}
1143

1144
	function cursorPointStroke(self, si) {
1145
		let sp = self.series[si].points;
1146
		return sp._stroke || sp._fill;
1147
	}
1148

1149
	function cursorPointSize(self, si) {
1150
		let sp = self.series[si].points;
1151
		return ptDia(sp.width, 1);
1152
	}
1153

1154
	function dataIdx(self, seriesIdx, cursorIdx) {
1155
		return cursorIdx;
1156
	}
1157

1158
	const moveTuple = [0,0];
1159

1160
	function cursorMove(self, mouseLeft1, mouseTop1) {
1161
		moveTuple[0] = mouseLeft1;
1162
		moveTuple[1] = mouseTop1;
1163
		return moveTuple;
1164
	}
1165

1166
	function filtBtn0(self, targ, handle) {
1167
		return e => {
1168
			e.button == 0 && handle(e);
1169
		};
1170
	}
1171

1172
	function passThru(self, targ, handle) {
1173
		return handle;
1174
	}
1175

1176
	const cursorOpts = {
1177
		show: true,
1178
		x: true,
1179
		y: true,
1180
		lock: false,
1181
		move: cursorMove,
1182
		points: {
1183
			show:   cursorPointShow,
1184
			size:   cursorPointSize,
1185
			width:  0,
1186
			stroke: cursorPointStroke,
1187
			fill:   cursorPointFill,
1188
		},
1189

1190
		bind: {
1191
			mousedown:   filtBtn0,
1192
			mouseup:     filtBtn0,
1193
			click:       filtBtn0,
1194
			dblclick:    filtBtn0,
1195

1196
			mousemove:   passThru,
1197
			mouseleave:  passThru,
1198
			mouseenter:  passThru,
1199
		},
1200

1201
		drag: {
1202
			setScale: true,
1203
			x: true,
1204
			y: false,
1205
			dist: 0,
1206
			uni: null,
1207
			_x: false,
1208
			_y: false,
1209
		},
1210

1211
		focus: {
1212
			prox: -1,
1213
		},
1214

1215
		left: -10,
1216
		top: -10,
1217
		idx: null,
1218
		dataIdx,
1219
		idxs: null,
1220
	};
1221

1222
	const grid = {
1223
		show: true,
1224
		stroke: "rgba(0,0,0,0.07)",
1225
		width: 2,
1226
	//	dash: [],
1227
		filter: retArg1,
1228
	};
1229

1230
	const ticks = assign({}, grid, {size: 10});
1231

1232
	const font      = '12px system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
1233
	const labelFont = "bold " + font;
1234
	const lineMult = 1.5;		// font-size multiplier
1235

1236
	const xAxisOpts = {
1237
		show: true,
1238
		scale: "x",
1239
		stroke: hexBlack,
1240
		space: 50,
1241
		gap: 5,
1242
		size: 50,
1243
		labelGap: 0,
1244
		labelSize: 30,
1245
		labelFont,
1246
		side: 2,
1247
	//	class: "x-vals",
1248
	//	incrs: timeIncrs,
1249
	//	values: timeVals,
1250
	//	filter: retArg1,
1251
		grid,
1252
		ticks,
1253
		font,
1254
		rotate: 0,
1255
	};
1256

1257
	const numSeriesLabel = "Value";
1258
	const timeSeriesLabel = "Time";
1259

1260
	const xSeriesOpts = {
1261
		show: true,
1262
		scale: "x",
1263
		auto: false,
1264
		sorted: 1,
1265
	//	label: "Time",
1266
	//	value: v => stamp(new Date(v * 1e3)),
1267

1268
		// internal caches
1269
		min: inf,
1270
		max: -inf,
1271
		idxs: [],
1272
	};
1273

1274
	function numAxisVals(self, splits, axisIdx, foundSpace, foundIncr) {
1275
		return splits.map(v => v == null ? "" : fmtNum(v));
1276
	}
1277

1278
	function numAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {
1279
		let splits = [];
1280

1281
		let numDec = fixedDec.get(foundIncr) || 0;
1282

1283
		scaleMin = forceMin ? scaleMin : roundDec(incrRoundUp(scaleMin, foundIncr), numDec);
1284

1285
		for (let val = scaleMin; val <= scaleMax; val = roundDec(val + foundIncr, numDec))
1286
			splits.push(Object.is(val, -0) ? 0 : val);		// coalesces -0
1287

1288
		return splits;
1289
	}
1290

1291
	// this doesnt work for sin, which needs to come off from 0 independently in pos and neg dirs
1292
	function logAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {
1293
		const splits = [];
1294

1295
		const logBase = self.scales[self.axes[axisIdx].scale].log;
1296

1297
		const logFn = logBase == 10 ? log10 : log2;
1298

1299
		const exp = floor(logFn(scaleMin));
1300

1301
		foundIncr = pow(logBase, exp);
1302

1303
		if (exp < 0)
1304
			foundIncr = roundDec(foundIncr, -exp);
1305

1306
		let split = scaleMin;
1307

1308
		do {
1309
			splits.push(split);
1310
			split = roundDec(split + foundIncr, fixedDec.get(foundIncr));
1311

1312
			if (split >= foundIncr * logBase)
1313
				foundIncr = split;
1314

1315
		} while (split <= scaleMax);
1316

1317
		return splits;
1318
	}
1319

1320
	function asinhAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {
1321
		let sc = self.scales[self.axes[axisIdx].scale];
1322

1323
		let linthresh = sc.asinh;
1324

1325
		let posSplits = scaleMax > linthresh ? logAxisSplits(self, axisIdx, max(linthresh, scaleMin), scaleMax, foundIncr) : [linthresh];
1326
		let zero = scaleMax >= 0 && scaleMin <= 0 ? [0] : [];
1327
		let negSplits = scaleMin < -linthresh ? logAxisSplits(self, axisIdx, max(linthresh, -scaleMax), -scaleMin, foundIncr): [linthresh];
1328

1329
		return negSplits.reverse().map(v => -v).concat(zero, posSplits);
1330
	}
1331

1332
	const RE_ALL   = /./;
1333
	const RE_12357 = /[12357]/;
1334
	const RE_125   = /[125]/;
1335
	const RE_1     = /1/;
1336

1337
	function logAxisValsFilt(self, splits, axisIdx, foundSpace, foundIncr) {
1338
		let axis = self.axes[axisIdx];
1339
		let scaleKey = axis.scale;
1340
		let sc = self.scales[scaleKey];
1341

1342
		if (sc.distr == 3 && sc.log == 2)
1343
			return splits;
1344

1345
		let valToPos = self.valToPos;
1346

1347
		let minSpace = axis._space;
1348

1349
		let _10 = valToPos(10, scaleKey);
1350

1351
		let re = (
1352
			valToPos(9, scaleKey) - _10 >= minSpace ? RE_ALL :
1353
			valToPos(7, scaleKey) - _10 >= minSpace ? RE_12357 :
1354
			valToPos(5, scaleKey) - _10 >= minSpace ? RE_125 :
1355
			RE_1
1356
		);
1357

1358
		return splits.map(v => ((sc.distr == 4 && v == 0) || re.test(v)) ? v : null);
1359
	}
1360

1361
	function numSeriesVal(self, val) {
1362
		return val == null ? "" : fmtNum(val);
1363
	}
1364

1365
	const yAxisOpts = {
1366
		show: true,
1367
		scale: "y",
1368
		stroke: hexBlack,
1369
		space: 30,
1370
		gap: 5,
1371
		size: 50,
1372
		labelGap: 0,
1373
		labelSize: 30,
1374
		labelFont,
1375
		side: 3,
1376
	//	class: "y-vals",
1377
	//	incrs: numIncrs,
1378
	//	values: (vals, space) => vals,
1379
	//	filter: retArg1,
1380
		grid,
1381
		ticks,
1382
		font,
1383
		rotate: 0,
1384
	};
1385

1386
	// takes stroke width
1387
	function ptDia(width, mult) {
1388
		let dia = 3 + (width || 1) * 2;
1389
		return roundDec(dia * mult, 3);
1390
	}
1391

1392
	function seriesPointsShow(self, si) {
1393
		let { scale, idxs } = self.series[0];
1394
		let xData = self._data[0];
1395
		let p0 = self.valToPos(xData[idxs[0]], scale, true);
1396
		let p1 = self.valToPos(xData[idxs[1]], scale, true);
1397
		let dim = abs(p1 - p0);
1398

1399
		let s = self.series[si];
1400
	//	const dia = ptDia(s.width, pxRatio);
1401
		let maxPts = dim / (s.points.space * pxRatio);
1402
		return idxs[1] - idxs[0] <= maxPts;
1403
	}
1404

1405
	function seriesFillTo(self, seriesIdx, dataMin, dataMax) {
1406
		let scale = self.scales[self.series[seriesIdx].scale];
1407
		let isUpperBandEdge = self.bands && self.bands.some(b => b.series[0] == seriesIdx);
1408
		return scale.distr == 3 || isUpperBandEdge ? scale.min : 0;
1409
	}
1410

1411
	const facet = {
1412
		scale: null,
1413
		auto: true,
1414

1415
		// internal caches
1416
		min: inf,
1417
		max: -inf,
1418
	};
1419

1420
	const xySeriesOpts = {
1421
		show: true,
1422
		auto: true,
1423
		sorted: 0,
1424
		alpha: 1,
1425
		facets: [
1426
			assign({}, facet, {scale: 'x'}),
1427
			assign({}, facet, {scale: 'y'}),
1428
		],
1429
	};
1430

1431
	const ySeriesOpts = {
1432
		scale: "y",
1433
		auto: true,
1434
		sorted: 0,
1435
		show: true,
1436
		spanGaps: false,
1437
		gaps: (self, seriesIdx, idx0, idx1, nullGaps) => nullGaps,
1438
		alpha: 1,
1439
		points: {
1440
			show: seriesPointsShow,
1441
			filter: null,
1442
		//  paths:
1443
		//	stroke: "#000",
1444
		//	fill: "#fff",
1445
		//	width: 1,
1446
		//	size: 10,
1447
		},
1448
	//	label: "Value",
1449
	//	value: v => v,
1450
		values: null,
1451

1452
		// internal caches
1453
		min: inf,
1454
		max: -inf,
1455
		idxs: [],
1456

1457
		path: null,
1458
		clip: null,
1459
	};
1460

1461
	function clampScale(self, val, scaleMin, scaleMax, scaleKey) {
1462
	/*
1463
		if (val < 0) {
1464
			let cssHgt = self.bbox.height / pxRatio;
1465
			let absPos = self.valToPos(abs(val), scaleKey);
1466
			let fromBtm = cssHgt - absPos;
1467
			return self.posToVal(cssHgt + fromBtm, scaleKey);
1468
		}
1469
	*/
1470
		return scaleMin / 10;
1471
	}
1472

1473
	const xScaleOpts = {
1474
		time: FEAT_TIME,
1475
		auto: true,
1476
		distr: 1,
1477
		log: 10,
1478
		asinh: 1,
1479
		min: null,
1480
		max: null,
1481
		dir: 1,
1482
		ori: 0,
1483
	};
1484

1485
	const yScaleOpts = assign({}, xScaleOpts, {
1486
		time: false,
1487
		ori: 1,
1488
	});
1489

1490
	const syncs = {};
1491

1492
	function _sync(key, opts) {
1493
		let s = syncs[key];
1494

1495
		if (!s) {
1496
			s = {
1497
				key,
1498
				plots: [],
1499
				sub(plot) {
1500
					s.plots.push(plot);
1501
				},
1502
				unsub(plot) {
1503
					s.plots = s.plots.filter(c => c != plot);
1504
				},
1505
				pub(type, self, x, y, w, h, i) {
1506
					for (let j = 0; j < s.plots.length; j++)
1507
						s.plots[j] != self && s.plots[j].pub(type, self, x, y, w, h, i);
1508
				},
1509
			};
1510

1511
			if (key != null)
1512
				syncs[key] = s;
1513
		}
1514

1515
		return s;
1516
	}
1517

1518
	const BAND_CLIP_FILL   = 1 << 0;
1519
	const BAND_CLIP_STROKE = 1 << 1;
1520

1521
	function orient(u, seriesIdx, cb) {
1522
		const series = u.series[seriesIdx];
1523
		const scales = u.scales;
1524
		const bbox   = u.bbox;
1525
		const scaleX = u.mode == 2 ? scales[series.facets[0].scale] : scales[u.series[0].scale];
1526

1527
		let dx = u._data[0],
1528
			dy = u._data[seriesIdx],
1529
			sx = scaleX,
1530
			sy = u.mode == 2 ? scales[series.facets[1].scale] : scales[series.scale],
1531
			l = bbox.left,
1532
			t = bbox.top,
1533
			w = bbox.width,
1534
			h = bbox.height,
1535
			H = u.valToPosH,
1536
			V = u.valToPosV;
1537

1538
		return (sx.ori == 0
1539
			? cb(
1540
				series,
1541
				dx,
1542
				dy,
1543
				sx,
1544
				sy,
1545
				H,
1546
				V,
1547
				l,
1548
				t,
1549
				w,
1550
				h,
1551
				moveToH,
1552
				lineToH,
1553
				rectH,
1554
				arcH,
1555
				bezierCurveToH,
1556
			)
1557
			: cb(
1558
				series,
1559
				dx,
1560
				dy,
1561
				sx,
1562
				sy,
1563
				V,
1564
				H,
1565
				t,
1566
				l,
1567
				h,
1568
				w,
1569
				moveToV,
1570
				lineToV,
1571
				rectV,
1572
				arcV,
1573
				bezierCurveToV,
1574
			)
1575
		);
1576
	}
1577

1578
	// creates inverted band clip path (towards from stroke path -> yMax)
1579
	function clipBandLine(self, seriesIdx, idx0, idx1, strokePath) {
1580
		return orient(self, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
1581
			let pxRound = series.pxRound;
1582

1583
			const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
1584
			const lineTo = scaleX.ori == 0 ? lineToH : lineToV;
1585

1586
			let frIdx, toIdx;
1587

1588
			if (dir == 1) {
1589
				frIdx = idx0;
1590
				toIdx = idx1;
1591
			}
1592
			else {
1593
				frIdx = idx1;
1594
				toIdx = idx0;
1595
			}
1596

1597
			// path start
1598
			let x0 = pxRound(valToPosX(dataX[frIdx], scaleX, xDim, xOff));
1599
			let y0 = pxRound(valToPosY(dataY[frIdx], scaleY, yDim, yOff));
1600
			// path end x
1601
			let x1 = pxRound(valToPosX(dataX[toIdx], scaleX, xDim, xOff));
1602
			// upper y limit
1603
			let yLimit = pxRound(valToPosY(scaleY.max, scaleY, yDim, yOff));
1604

1605
			let clip = new Path2D(strokePath);
1606

1607
			lineTo(clip, x1, yLimit);
1608
			lineTo(clip, x0, yLimit);
1609
			lineTo(clip, x0, y0);
1610

1611
			return clip;
1612
		});
1613
	}
1614

1615
	function clipGaps(gaps, ori, plotLft, plotTop, plotWid, plotHgt) {
1616
		let clip = null;
1617

1618
		// create clip path (invert gaps and non-gaps)
1619
		if (gaps.length > 0) {
1620
			clip = new Path2D();
1621

1622
			const rect = ori == 0 ? rectH : rectV;
1623

1624
			let prevGapEnd = plotLft;
1625

1626
			for (let i = 0; i < gaps.length; i++) {
1627
				let g = gaps[i];
1628

1629
				if (g[1] > g[0]) {
1630
					let w = g[0] - prevGapEnd;
1631

1632
					w > 0 && rect(clip, prevGapEnd, plotTop, w, plotTop + plotHgt);
1633

1634
					prevGapEnd = g[1];
1635
				}
1636
			}
1637

1638
			let w = plotLft + plotWid - prevGapEnd;
1639

1640
			w > 0 && rect(clip, prevGapEnd, plotTop, w, plotTop + plotHgt);
1641
		}
1642

1643
		return clip;
1644
	}
1645

1646
	function addGap(gaps, fromX, toX) {
1647
		let prevGap = gaps[gaps.length - 1];
1648

1649
		if (prevGap && prevGap[0] == fromX)			// TODO: gaps must be encoded at stroke widths?
1650
			prevGap[1] = toX;
1651
		else
1652
			gaps.push([fromX, toX]);
1653
	}
1654

1655
	function pxRoundGen(pxAlign) {
1656
		return pxAlign == 0 ? retArg0 : pxAlign == 1 ? round : v => incrRound(v, pxAlign);
1657
	}
1658

1659
	function rect(ori) {
1660
		let moveTo = ori == 0 ?
1661
			moveToH :
1662
			moveToV;
1663

1664
		let arcTo = ori == 0 ?
1665
			(p, x1, y1, x2, y2, r) => { p.arcTo(x1, y1, x2, y2, r); } :
1666
			(p, y1, x1, y2, x2, r) => { p.arcTo(x1, y1, x2, y2, r); };
1667

1668
		let rect = ori == 0 ?
1669
			(p, x, y, w, h) => { p.rect(x, y, w, h); } :
1670
			(p, y, x, h, w) => { p.rect(x, y, w, h); };
1671

1672
		return (p, x, y, w, h, r = 0) => {
1673
			if (r == 0)
1674
				rect(p, x, y, w, h);
1675
			else {
1676
				r = min(r, w / 2, h / 2);
1677

1678
				// adapted from https://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-using-html-canvas/7838871#7838871
1679
				moveTo(p, x + r, y);
1680
				arcTo(p, x + w, y, x + w, y + h, r);
1681
				arcTo(p, x + w, y + h, x, y + h, r);
1682
				arcTo(p, x, y + h, x, y, r);
1683
				arcTo(p, x, y, x + w, y, r);
1684
				p.closePath();
1685
			}
1686
		};
1687
	}
1688

1689
	// orientation-inverting canvas functions
1690
	const moveToH = (p, x, y) => { p.moveTo(x, y); };
1691
	const moveToV = (p, y, x) => { p.moveTo(x, y); };
1692
	const lineToH = (p, x, y) => { p.lineTo(x, y); };
1693
	const lineToV = (p, y, x) => { p.lineTo(x, y); };
1694
	const rectH = rect(0);
1695
	const rectV = rect(1);
1696
	const arcH = (p, x, y, r, startAngle, endAngle) => { p.arc(x, y, r, startAngle, endAngle); };
1697
	const arcV = (p, y, x, r, startAngle, endAngle) => { p.arc(x, y, r, startAngle, endAngle); };
1698
	const bezierCurveToH = (p, bp1x, bp1y, bp2x, bp2y, p2x, p2y) => { p.bezierCurveTo(bp1x, bp1y, bp2x, bp2y, p2x, p2y); };
1699
	const bezierCurveToV = (p, bp1y, bp1x, bp2y, bp2x, p2y, p2x) => { p.bezierCurveTo(bp1x, bp1y, bp2x, bp2y, p2x, p2y); };
1700

1701
	// TODO: drawWrap(seriesIdx, drawPoints) (save, restore, translate, clip)
1702
	function points(opts) {
1703
		return (u, seriesIdx, idx0, idx1, filtIdxs) => {
1704
		//	log("drawPoints()", arguments);
1705

1706
			return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
1707
				let { pxRound, points } = series;
1708

1709
				let moveTo, arc;
1710

1711
				if (scaleX.ori == 0) {
1712
					moveTo = moveToH;
1713
					arc = arcH;
1714
				}
1715
				else {
1716
					moveTo = moveToV;
1717
					arc = arcV;
1718
				}
1719

1720
				const width = roundDec(points.width * pxRatio, 3);
1721

1722
				let rad = (points.size - points.width) / 2 * pxRatio;
1723
				let dia = roundDec(rad * 2, 3);
1724

1725
				let fill = new Path2D();
1726
				let clip = new Path2D();
1727

1728
				let { left: lft, top: top, width: wid, height: hgt } = u.bbox;
1729

1730
				rectH(clip,
1731
					lft - dia,
1732
					top - dia,
1733
					wid + dia * 2,
1734
					hgt + dia * 2,
1735
				);
1736

1737
				const drawPoint = pi => {
1738
					if (dataY[pi] != null) {
1739
						let x = pxRound(valToPosX(dataX[pi], scaleX, xDim, xOff));
1740
						let y = pxRound(valToPosY(dataY[pi], scaleY, yDim, yOff));
1741

1742
						moveTo(fill, x + rad, y);
1743
						arc(fill, x, y, rad, 0, PI * 2);
1744
					}
1745
				};
1746

1747
				if (filtIdxs)
1748
					filtIdxs.forEach(drawPoint);
1749
				else {
1750
					for (let pi = idx0; pi <= idx1; pi++)
1751
						drawPoint(pi);
1752
				}
1753

1754
				return {
1755
					stroke: width > 0 ? fill : null,
1756
					fill,
1757
					clip,
1758
					flags: BAND_CLIP_FILL | BAND_CLIP_STROKE,
1759
				};
1760
			});
1761
		};
1762
	}
1763

1764
	function _drawAcc(lineTo) {
1765
		return (stroke, accX, minY, maxY, inY, outY) => {
1766
			if (minY != maxY) {
1767
				if (inY != minY && outY != minY)
1768
					lineTo(stroke, accX, minY);
1769
				if (inY != maxY && outY != maxY)
1770
					lineTo(stroke, accX, maxY);
1771

1772
				lineTo(stroke, accX, outY);
1773
			}
1774
		};
1775
	}
1776

1777
	const drawAccH = _drawAcc(lineToH);
1778
	const drawAccV = _drawAcc(lineToV);
1779

1780
	function linear() {
1781
		return (u, seriesIdx, idx0, idx1) => {
1782
			return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
1783
				let pxRound = series.pxRound;
1784

1785
				let lineTo, drawAcc;
1786

1787
				if (scaleX.ori == 0) {
1788
					lineTo = lineToH;
1789
					drawAcc = drawAccH;
1790
				}
1791
				else {
1792
					lineTo = lineToV;
1793
					drawAcc = drawAccV;
1794
				}
1795

1796
				const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
1797

1798
				const _paths = {stroke: new Path2D(), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};
1799
				const stroke = _paths.stroke;
1800

1801
				let minY = inf,
1802
					maxY = -inf,
1803
					inY, outY, outX, drawnAtX;
1804

1805
				let gaps = [];
1806

1807
				let accX = pxRound(valToPosX(dataX[dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));
1808
				let accGaps = false;
1809
				let prevYNull = false;
1810

1811
				// data edges
1812
				let lftIdx = nonNullIdx(dataY, idx0, idx1,  1 * dir);
1813
				let rgtIdx = nonNullIdx(dataY, idx0, idx1, -1 * dir);
1814
				let lftX =  pxRound(valToPosX(dataX[lftIdx], scaleX, xDim, xOff));
1815
				let rgtX =  pxRound(valToPosX(dataX[rgtIdx], scaleX, xDim, xOff));
1816

1817
				if (lftX > xOff)
1818
					addGap(gaps, xOff, lftX);
1819

1820
				for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) {
1821
					let x = pxRound(valToPosX(dataX[i], scaleX, xDim, xOff));
1822

1823
					if (x == accX) {
1824
						if (dataY[i] != null) {
1825
							outY = pxRound(valToPosY(dataY[i], scaleY, yDim, yOff));
1826

1827
							if (minY == inf) {
1828
								lineTo(stroke, x, outY);
1829
								inY = outY;
1830
							}
1831

1832
							minY = min(outY, minY);
1833
							maxY = max(outY, maxY);
1834
						}
1835
						else if (dataY[i] === null)
1836
							accGaps = prevYNull = true;
1837
					}
1838
					else {
1839
						let _addGap = false;
1840

1841
						if (minY != inf) {
1842
							drawAcc(stroke, accX, minY, maxY, inY, outY);
1843
							outX = drawnAtX = accX;
1844
						}
1845
						else if (accGaps) {
1846
							_addGap = true;
1847
							accGaps = false;
1848
						}
1849

1850
						if (dataY[i] != null) {
1851
							outY = pxRound(valToPosY(dataY[i], scaleY, yDim, yOff));
1852
							lineTo(stroke, x, outY);
1853
							minY = maxY = inY = outY;
1854

1855
							// prior pixel can have data but still start a gap if ends with null
1856
							if (prevYNull && x - accX > 1)
1857
								_addGap = true;
1858

1859
							prevYNull = false;
1860
						}
1861
						else {
1862
							minY = inf;
1863
							maxY = -inf;
1864

1865
							if (dataY[i] === null) {
1866
								accGaps = true;
1867

1868
								if (x - accX > 1)
1869
									_addGap = true;
1870
							}
1871
						}
1872

1873
						_addGap && addGap(gaps, outX, x);
1874

1875
						accX = x;
1876
					}
1877
				}
1878

1879
				if (minY != inf && minY != maxY && drawnAtX != accX)
1880
					drawAcc(stroke, accX, minY, maxY, inY, outY);
1881

1882
				if (rgtX < xOff + xDim)
1883
					addGap(gaps, rgtX, xOff + xDim);
1884

1885
				if (series.fill != null) {
1886
					let fill = _paths.fill = new Path2D(stroke);
1887

1888
					let fillTo = pxRound(valToPosY(series.fillTo(u, seriesIdx, series.min, series.max), scaleY, yDim, yOff));
1889

1890
					lineTo(fill, rgtX, fillTo);
1891
					lineTo(fill, lftX, fillTo);
1892
				}
1893

1894
				_paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
1895

1896
				if (!series.spanGaps)
1897
					_paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
1898

1899
				if (u.bands.length > 0) {
1900
					// ADDL OPT: only create band clips for series that are band lower edges
1901
					// if (b.series[1] == i && _paths.band == null)
1902
					_paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);
1903
				}
1904

1905
				return _paths;
1906
			});
1907
		};
1908
	}
1909

1910
	function stepped(opts) {
1911
		const align = ifNull(opts.align, 1);
1912
		// whether to draw ascenders/descenders at null/gap bondaries
1913
		const ascDesc = ifNull(opts.ascDesc, false);
1914

1915
		return (u, seriesIdx, idx0, idx1) => {
1916
			return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
1917
				let pxRound = series.pxRound;
1918

1919
				let lineTo = scaleX.ori == 0 ? lineToH : lineToV;
1920

1921
				const _paths = {stroke: new Path2D(), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};
1922
				const stroke = _paths.stroke;
1923

1924
				const _dir = 1 * scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
1925

1926
				idx0 = nonNullIdx(dataY, idx0, idx1,  1);
1927
				idx1 = nonNullIdx(dataY, idx0, idx1, -1);
1928

1929
				let gaps = [];
1930
				let inGap = false;
1931
				let prevYPos  = pxRound(valToPosY(dataY[_dir == 1 ? idx0 : idx1], scaleY, yDim, yOff));
1932
				let firstXPos = pxRound(valToPosX(dataX[_dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));
1933
				let prevXPos = firstXPos;
1934

1935
				lineTo(stroke, firstXPos, prevYPos);
1936

1937
				for (let i = _dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dir) {
1938
					let yVal1 = dataY[i];
1939

1940
					let x1 = pxRound(valToPosX(dataX[i], scaleX, xDim, xOff));
1941

1942
					if (yVal1 == null) {
1943
						if (yVal1 === null) {
1944
							addGap(gaps, prevXPos, x1);
1945
							inGap = true;
1946
						}
1947
						continue;
1948
					}
1949

1950
					let y1 = pxRound(valToPosY(yVal1, scaleY, yDim, yOff));
1951

1952
					if (inGap) {
1953
						addGap(gaps, prevXPos, x1);
1954
						inGap = false;
1955
					}
1956

1957
					if (align == 1)
1958
						lineTo(stroke, x1, prevYPos);
1959
					else
1960
						lineTo(stroke, prevXPos, y1);
1961

1962
					lineTo(stroke, x1, y1);
1963

1964
					prevYPos = y1;
1965
					prevXPos = x1;
1966
				}
1967

1968
				if (series.fill != null) {
1969
					let fill = _paths.fill = new Path2D(stroke);
1970

1971
					let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
1972
					let minY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff));
1973

1974
					lineTo(fill, prevXPos, minY);
1975
					lineTo(fill, firstXPos, minY);
1976
				}
1977

1978
				_paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
1979

1980
				// expand/contract clips for ascenders/descenders
1981
				let halfStroke = (series.width * pxRatio) / 2;
1982
				let startsOffset = (ascDesc || align ==  1) ?  halfStroke : -halfStroke;
1983
				let endsOffset   = (ascDesc || align == -1) ? -halfStroke :  halfStroke;
1984

1985
				gaps.forEach(g => {
1986
					g[0] += startsOffset;
1987
					g[1] += endsOffset;
1988
				});
1989

1990
				if (!series.spanGaps)
1991
					_paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
1992

1993
				if (u.bands.length > 0) {
1994
					// ADDL OPT: only create band clips for series that are band lower edges
1995
					// if (b.series[1] == i && _paths.band == null)
1996
					_paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);
1997
				}
1998

1999
				return _paths;
2000
			});
2001
		};
2002
	}
2003

2004
	function bars(opts) {
2005
		opts = opts || EMPTY_OBJ;
2006
		const size = ifNull(opts.size, [0.6, inf, 1]);
2007
		const align = opts.align || 0;
2008
		const extraGap = (opts.gap || 0) * pxRatio;
2009

2010
		const radius = ifNull(opts.radius, 0);
2011

2012
		const gapFactor = 1 - size[0];
2013
		const maxWidth  = ifNull(size[1], inf) * pxRatio;
2014
		const minWidth  = ifNull(size[2], 1) * pxRatio;
2015

2016
		const disp = ifNull(opts.disp, EMPTY_OBJ);
2017
		const _each = ifNull(opts.each, _ => {});
2018

2019
		const { fill: dispFills, stroke: dispStrokes } = disp;
2020

2021
		return (u, seriesIdx, idx0, idx1) => {
2022
			return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
2023
				let pxRound = series.pxRound;
2024

2025
				const _dirX = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
2026
				const _dirY = scaleY.dir * (scaleY.ori == 1 ? 1 : -1);
2027

2028
				let rect = scaleX.ori == 0 ? rectH : rectV;
2029

2030
				let each = scaleX.ori == 0 ? _each : (u, seriesIdx, i, top, lft, hgt, wid) => {
2031
					_each(u, seriesIdx, i, lft, top, wid, hgt);
2032
				};
2033

2034
				let fillToY = series.fillTo(u, seriesIdx, series.min, series.max);
2035

2036
				let y0Pos = valToPosY(fillToY, scaleY, yDim, yOff);
2037

2038
				// barWid is to center of stroke
2039
				let xShift, barWid;
2040

2041
				let strokeWidth = pxRound(series.width * pxRatio);
2042

2043
				let multiPath = false;
2044

2045
				let fillColors = null;
2046
				let fillPaths = null;
2047
				let strokeColors = null;
2048
				let strokePaths = null;
2049

2050
				if (dispFills != null && dispStrokes != null) {
2051
					multiPath = true;
2052

2053
					fillColors = dispFills.values(u, seriesIdx, idx0, idx1);
2054
					fillPaths = new Map();
2055
					(new Set(fillColors)).forEach(color => {
2056
						if (color != null)
2057
							fillPaths.set(color, new Path2D());
2058
					});
2059

2060
					strokeColors = dispStrokes.values(u, seriesIdx, idx0, idx1);
2061
					strokePaths = new Map();
2062
					(new Set(strokeColors)).forEach(color => {
2063
						if (color != null)
2064
							strokePaths.set(color, new Path2D());
2065
					});
2066
				}
2067

2068
				let { x0, size } = disp;
2069

2070
				if (x0 != null && size != null) {
2071
					dataX = x0.values(u, seriesIdx, idx0, idx1);
2072

2073
					if (x0.unit == 2)
2074
						dataX = dataX.map(pct => u.posToVal(xOff + pct * xDim, scaleX.key, true));
2075

2076
					// assumes uniform sizes, for now
2077
					let sizes = size.values(u, seriesIdx, idx0, idx1);
2078

2079
					if (size.unit == 2)
2080
						barWid = sizes[0] * xDim;
2081
					else
2082
						barWid = valToPosX(sizes[0], scaleX, xDim, xOff) - valToPosX(0, scaleX, xDim, xOff); // assumes linear scale (delta from 0)
2083

2084
					barWid = pxRound(barWid - strokeWidth);
2085

2086
					xShift = (_dirX == 1 ? -strokeWidth / 2 : barWid + strokeWidth / 2);
2087
				}
2088
				else {
2089
					let colWid = xDim;
2090

2091
					if (dataX.length > 1) {
2092
						// prior index with non-undefined y data
2093
						let prevIdx = null;
2094

2095
						// scan full dataset for smallest adjacent delta
2096
						// will not work properly for non-linear x scales, since does not do expensive valToPosX calcs till end
2097
						for (let i = 0, minDelta = Infinity; i < dataX.length; i++) {
2098
							if (dataY[i] !== undefined) {
2099
								if (prevIdx != null) {
2100
									let delta = abs(dataX[i] - dataX[prevIdx]);
2101

2102
									if (delta < minDelta) {
2103
										minDelta = delta;
2104
										colWid = abs(valToPosX(dataX[i], scaleX, xDim, xOff) - valToPosX(dataX[prevIdx], scaleX, xDim, xOff));
2105
									}
2106
								}
2107

2108
								prevIdx = i;
2109
							}
2110
						}
2111
					}
2112

2113
					let gapWid = colWid * gapFactor;
2114

2115
					barWid = pxRound(min(maxWidth, max(minWidth, colWid - gapWid)) - strokeWidth - extraGap);
2116

2117
					xShift = (align == 0 ? barWid / 2 : align == _dirX ? 0 : barWid) - align * _dirX * extraGap / 2;
2118
				}
2119

2120
				const _paths = {stroke: null, fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL | BAND_CLIP_STROKE};  // disp, geom
2121

2122
				const hasBands = u.bands.length > 0;
2123
				let yLimit;
2124

2125
				if (hasBands) {
2126
					// ADDL OPT: only create band clips for series that are band lower edges
2127
					// if (b.series[1] == i && _paths.band == null)
2128
					_paths.band = new Path2D();
2129
					yLimit = pxRound(valToPosY(scaleY.max, scaleY, yDim, yOff));
2130
				}
2131

2132
				const stroke = multiPath ? null : new Path2D();
2133
				const band = _paths.band;
2134

2135
				for (let i = _dirX == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dirX) {
2136
					let yVal = dataY[i];
2137

2138
				/*
2139
					// interpolate upwards band clips
2140
					if (yVal == null) {
2141
					//	if (hasBands)
2142
					//		yVal = costlyLerp(i, idx0, idx1, _dirX, dataY);
2143
					//	else
2144
							continue;
2145
					}
2146
				*/
2147

2148
					let xVal = scaleX.distr != 2 || disp != null ? dataX[i] : i;
2149

2150
					// TODO: all xPos can be pre-computed once for all series in aligned set
2151
					let xPos = valToPosX(xVal, scaleX, xDim, xOff);
2152
					let yPos = valToPosY(ifNull(yVal, fillToY) , scaleY, yDim, yOff);
2153

2154
					let lft = pxRound(xPos - xShift);
2155
					let btm = pxRound(max(yPos, y0Pos));
2156
					let top = pxRound(min(yPos, y0Pos));
2157
					// this includes the stroke
2158
					let barHgt = btm - top;
2159

2160
					let r = radius * barWid;
2161

2162
					if (yVal != null) {  // && yVal != fillToY (0 height bar)
2163
						if (multiPath) {
2164
							if (strokeWidth > 0 && strokeColors[i] != null)
2165
								rect(strokePaths.get(strokeColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);
2166

2167
							if (fillColors[i] != null)
2168
								rect(fillPaths.get(fillColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);
2169
						}
2170
						else
2171
							rect(stroke, lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);
2172

2173
						each(u, seriesIdx, i,
2174
							lft    - strokeWidth / 2,
2175
							top,
2176
							barWid + strokeWidth,
2177
							barHgt,
2178
						);
2179
					}
2180

2181
					if (hasBands) {
2182
						if (_dirY == 1) {
2183
							btm = top;
2184
							top = yLimit;
2185
						}
2186
						else {
2187
							top = btm;
2188
							btm = yLimit;
2189
						}
2190

2191
						barHgt = btm - top;
2192

2193
						rect(band, lft - strokeWidth / 2, top, barWid + strokeWidth, max(0, barHgt), 0);
2194
					}
2195
				}
2196

2197
				if (strokeWidth > 0)
2198
					_paths.stroke = multiPath ? strokePaths : stroke;
2199

2200
				_paths.fill = multiPath ? fillPaths : stroke;
2201

2202
				return _paths;
2203
			});
2204
		};
2205
	}
2206

2207
	function splineInterp(interp, opts) {
2208
		return (u, seriesIdx, idx0, idx1) => {
2209
			return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
2210
				let pxRound = series.pxRound;
2211

2212
				let moveTo, bezierCurveTo, lineTo;
2213

2214
				if (scaleX.ori == 0) {
2215
					moveTo = moveToH;
2216
					lineTo = lineToH;
2217
					bezierCurveTo = bezierCurveToH;
2218
				}
2219
				else {
2220
					moveTo = moveToV;
2221
					lineTo = lineToV;
2222
					bezierCurveTo = bezierCurveToV;
2223
				}
2224

2225
				const _dir = 1 * scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
2226

2227
				idx0 = nonNullIdx(dataY, idx0, idx1,  1);
2228
				idx1 = nonNullIdx(dataY, idx0, idx1, -1);
2229

2230
				let gaps = [];
2231
				let inGap = false;
2232
				let firstXPos = pxRound(valToPosX(dataX[_dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));
2233
				let prevXPos = firstXPos;
2234

2235
				let xCoords = [];
2236
				let yCoords = [];
2237

2238
				for (let i = _dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dir) {
2239
					let yVal = dataY[i];
2240
					let xVal = dataX[i];
2241
					let xPos = valToPosX(xVal, scaleX, xDim, xOff);
2242

2243
					if (yVal == null) {
2244
						if (yVal === null) {
2245
							addGap(gaps, prevXPos, xPos);
2246
							inGap = true;
2247
						}
2248
						continue;
2249
					}
2250
					else {
2251
						if (inGap) {
2252
							addGap(gaps, prevXPos, xPos);
2253
							inGap = false;
2254
						}
2255

2256
						xCoords.push((prevXPos = xPos));
2257
						yCoords.push(valToPosY(dataY[i], scaleY, yDim, yOff));
2258
					}
2259
				}
2260

2261
				const _paths = {stroke: interp(xCoords, yCoords, moveTo, lineTo, bezierCurveTo, pxRound), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};
2262
				const stroke = _paths.stroke;
2263

2264
				if (series.fill != null && stroke != null) {
2265
					let fill = _paths.fill = new Path2D(stroke);
2266

2267
					let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
2268
					let minY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff));
2269

2270
					lineTo(fill, prevXPos, minY);
2271
					lineTo(fill, firstXPos, minY);
2272
				}
2273

2274
				_paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
2275

2276
				if (!series.spanGaps)
2277
					_paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
2278

2279
				if (u.bands.length > 0) {
2280
					// ADDL OPT: only create band clips for series that are band lower edges
2281
					// if (b.series[1] == i && _paths.band == null)
2282
					_paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);
2283
				}
2284

2285
				return _paths;
2286

2287
				//  if FEAT_PATHS: false in rollup.config.js
2288
				//	u.ctx.save();
2289
				//	u.ctx.beginPath();
2290
				//	u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
2291
				//	u.ctx.clip();
2292
				//	u.ctx.strokeStyle = u.series[sidx].stroke;
2293
				//	u.ctx.stroke(stroke);
2294
				//	u.ctx.fillStyle = u.series[sidx].fill;
2295
				//	u.ctx.fill(fill);
2296
				//	u.ctx.restore();
2297
				//	return null;
2298
			});
2299
		};
2300
	}
2301

2302
	function monotoneCubic(opts) {
2303
		return splineInterp(_monotoneCubic);
2304
	}
2305

2306
	// Monotone Cubic Spline interpolation, adapted from the Chartist.js implementation:
2307
	// https://github.com/gionkunz/chartist-js/blob/e7e78201bffe9609915e5e53cfafa29a5d6c49f9/src/scripts/interpolation.js#L240-L369
2308
	function _monotoneCubic(xs, ys, moveTo, lineTo, bezierCurveTo, pxRound) {
2309
		const n = xs.length;
2310

2311
		if (n < 2)
2312
			return null;
2313

2314
		const path = new Path2D();
2315

2316
		moveTo(path, xs[0], ys[0]);
2317

2318
		if (n == 2)
2319
			lineTo(path, xs[1], ys[1]);
2320
		else {
2321
			let ms  = Array(n),
2322
				ds  = Array(n - 1),
2323
				dys = Array(n - 1),
2324
				dxs = Array(n - 1);
2325

2326
			// calc deltas and derivative
2327
			for (let i = 0; i < n - 1; i++) {
2328
				dys[i] = ys[i + 1] - ys[i];
2329
				dxs[i] = xs[i + 1] - xs[i];
2330
				ds[i]  = dys[i] / dxs[i];
2331
			}
2332

2333
			// determine desired slope (m) at each point using Fritsch-Carlson method
2334
			// http://math.stackexchange.com/questions/45218/implementation-of-monotone-cubic-interpolation
2335
			ms[0] = ds[0];
2336

2337
			for (let i = 1; i < n - 1; i++) {
2338
				if (ds[i] === 0 || ds[i - 1] === 0 || (ds[i - 1] > 0) !== (ds[i] > 0))
2339
					ms[i] = 0;
2340
				else {
2341
					ms[i] = 3 * (dxs[i - 1] + dxs[i]) / (
2342
						(2 * dxs[i] + dxs[i - 1]) / ds[i - 1] +
2343
						(dxs[i] + 2 * dxs[i - 1]) / ds[i]
2344
					);
2345

2346
					if (!isFinite(ms[i]))
2347
						ms[i] = 0;
2348
				}
2349
			}
2350

2351
			ms[n - 1] = ds[n - 2];
2352

2353
			for (let i = 0; i < n - 1; i++) {
2354
				bezierCurveTo(
2355
					path,
2356
					xs[i] + dxs[i] / 3,
2357
					ys[i] + ms[i] * dxs[i] / 3,
2358
					xs[i + 1] - dxs[i] / 3,
2359
					ys[i + 1] - ms[i + 1] * dxs[i] / 3,
2360
					xs[i + 1],
2361
					ys[i + 1],
2362
				);
2363
			}
2364
		}
2365

2366
		return path;
2367
	}
2368

2369
	const cursorPlots = new Set();
2370

2371
	function invalidateRects() {
2372
		cursorPlots.forEach(u => {
2373
			u.syncRect(true);
2374
		});
2375
	}
2376

2377
	on(resize, win, invalidateRects);
2378
	on(scroll, win, invalidateRects, true);
2379

2380
	const linearPath = linear() ;
2381
	const pointsPath = points() ;
2382

2383
	function setDefaults(d, xo, yo, initY) {
2384
		let d2 = initY ? [d[0], d[1]].concat(d.slice(2)) : [d[0]].concat(d.slice(1));
2385
		return d2.map((o, i) => setDefault(o, i, xo, yo));
2386
	}
2387

2388
	function setDefaults2(d, xyo) {
2389
		return d.map((o, i) => i == 0 ? null : assign({}, xyo, o));  // todo: assign() will not merge facet arrays
2390
	}
2391

2392
	function setDefault(o, i, xo, yo) {
2393
		return assign({}, (i == 0 ? xo : yo), o);
2394
	}
2395

2396
	function snapNumX(self, dataMin, dataMax) {
2397
		return dataMin == null ? nullNullTuple : [dataMin, dataMax];
2398
	}
2399

2400
	const snapTimeX = snapNumX;
2401

2402
	// this ensures that non-temporal/numeric y-axes get multiple-snapped padding added above/below
2403
	// TODO: also account for incrs when snapping to ensure top of axis gets a tick & value
2404
	function snapNumY(self, dataMin, dataMax) {
2405
		return dataMin == null ? nullNullTuple : rangeNum(dataMin, dataMax, rangePad, true);
2406
	}
2407

2408
	function snapLogY(self, dataMin, dataMax, scale) {
2409
		return dataMin == null ? nullNullTuple : rangeLog(dataMin, dataMax, self.scales[scale].log, false);
2410
	}
2411

2412
	const snapLogX = snapLogY;
2413

2414
	function snapAsinhY(self, dataMin, dataMax, scale) {
2415
		return dataMin == null ? nullNullTuple : rangeAsinh(dataMin, dataMax, self.scales[scale].log, false);
2416
	}
2417

2418
	const snapAsinhX = snapAsinhY;
2419

2420
	// dim is logical (getClientBoundingRect) pixels, not canvas pixels
2421
	function findIncr(minVal, maxVal, incrs, dim, minSpace) {
2422
		let intDigits = max(numIntDigits(minVal), numIntDigits(maxVal));
2423

2424
		let delta = maxVal - minVal;
2425

2426
		let incrIdx = closestIdx((minSpace / dim) * delta, incrs);
2427

2428
		do {
2429
			let foundIncr = incrs[incrIdx];
2430
			let foundSpace = dim * foundIncr / delta;
2431

2432
			if (foundSpace >= minSpace && intDigits + (foundIncr < 5 ? fixedDec.get(foundIncr) : 0) <= 17)
2433
				return [foundIncr, foundSpace];
2434
		} while (++incrIdx < incrs.length);
2435

2436
		return [0, 0];
2437
	}
2438

2439
	function pxRatioFont(font) {
2440
		let fontSize, fontSizeCss;
2441
		font = font.replace(/(\d+)px/, (m, p1) => (fontSize = round((fontSizeCss = +p1) * pxRatio)) + 'px');
2442
		return [font, fontSize, fontSizeCss];
2443
	}
2444

2445
	function syncFontSize(axis) {
2446
		if (axis.show) {
2447
			[axis.font, axis.labelFont].forEach(f => {
2448
				let size = roundDec(f[2] * pxRatio, 1);
2449
				f[0] = f[0].replace(/[0-9.]+px/, size + 'px');
2450
				f[1] = size;
2451
			});
2452
		}
2453
	}
2454

2455
	function uPlot(opts, data, then) {
2456
		const self = {
2457
			mode: ifNull(opts.mode, 1),
2458
		};
2459

2460
		const mode = self.mode;
2461

2462
		// TODO: cache denoms & mins scale.cache = {r, min, }
2463
		function getValPct(val, scale) {
2464
			let _val = (
2465
				scale.distr == 3 ? log10(val > 0 ? val : scale.clamp(self, val, scale.min, scale.max, scale.key)) :
2466
				scale.distr == 4 ? asinh(val, scale.asinh) :
2467
				val
2468
			);
2469

2470
			return (_val - scale._min) / (scale._max - scale._min);
2471
		}
2472

2473
		function getHPos(val, scale, dim, off) {
2474
			let pct = getValPct(val, scale);
2475
			return off + dim * (scale.dir == -1 ? (1 - pct) : pct);
2476
		}
2477

2478
		function getVPos(val, scale, dim, off) {
2479
			let pct = getValPct(val, scale);
2480
			return off + dim * (scale.dir == -1 ? pct : (1 - pct));
2481
		}
2482

2483
		function getPos(val, scale, dim, off) {
2484
			return scale.ori == 0 ? getHPos(val, scale, dim, off) : getVPos(val, scale, dim, off);
2485
		}
2486

2487
		self.valToPosH = getHPos;
2488
		self.valToPosV = getVPos;
2489

2490
		let ready = false;
2491
		self.status = 0;
2492

2493
		const root = self.root = placeDiv(UPLOT);
2494

2495
		if (opts.id != null)
2496
			root.id = opts.id;
2497

2498
		addClass(root, opts.class);
2499

2500
		if (opts.title) {
2501
			let title = placeDiv(TITLE, root);
2502
			title.textContent = opts.title;
2503
		}
2504

2505
		const can = placeTag("canvas");
2506
		const ctx = self.ctx = can.getContext("2d");
2507

2508
		const wrap = placeDiv(WRAP, root);
2509
		const under = self.under = placeDiv(UNDER, wrap);
2510
		wrap.appendChild(can);
2511
		const over = self.over = placeDiv(OVER, wrap);
2512

2513
		opts = copy(opts);
2514

2515
		const pxAlign = +ifNull(opts.pxAlign, 1);
2516

2517
		const pxRound = pxRoundGen(pxAlign);
2518

2519
		(opts.plugins || []).forEach(p => {
2520
			if (p.opts)
2521
				opts = p.opts(self, opts) || opts;
2522
		});
2523

2524
		const ms = opts.ms || 1e-3;
2525

2526
		const series  = self.series = mode == 1 ?
2527
			setDefaults(opts.series || [], xSeriesOpts, ySeriesOpts, false) :
2528
			setDefaults2(opts.series || [null], xySeriesOpts);
2529
		const axes    = self.axes   = setDefaults(opts.axes   || [], xAxisOpts,   yAxisOpts,    true);
2530
		const scales  = self.scales = {};
2531
		const bands   = self.bands  = opts.bands || [];
2532

2533
		bands.forEach(b => {
2534
			b.fill = fnOrSelf(b.fill || null);
2535
		});
2536

2537
		const xScaleKey = mode == 2 ? series[1].facets[0].scale : series[0].scale;
2538

2539
		const drawOrderMap = {
2540
			axes: drawAxesGrid,
2541
			series: drawSeries,
2542
		};
2543

2544
		const drawOrder = (opts.drawOrder || ["axes", "series"]).map(key => drawOrderMap[key]);
2545

2546
		function initScale(scaleKey) {
2547
			let sc = scales[scaleKey];
2548

2549
			if (sc == null) {
2550
				let scaleOpts = (opts.scales || EMPTY_OBJ)[scaleKey] || EMPTY_OBJ;
2551

2552
				if (scaleOpts.from != null) {
2553
					// ensure parent is initialized
2554
					initScale(scaleOpts.from);
2555
					// dependent scales inherit
2556
					scales[scaleKey] = assign({}, scales[scaleOpts.from], scaleOpts, {key: scaleKey});
2557
				}
2558
				else {
2559
					sc = scales[scaleKey] = assign({}, (scaleKey == xScaleKey ? xScaleOpts : yScaleOpts), scaleOpts);
2560

2561
					if (mode == 2)
2562
						sc.time = false;
2563

2564
					sc.key = scaleKey;
2565

2566
					let isTime = sc.time;
2567

2568
					let rn = sc.range;
2569

2570
					let rangeIsArr = isArr(rn);
2571

2572
					if (scaleKey != xScaleKey || mode == 2) {
2573
						// if range array has null limits, it should be auto
2574
						if (rangeIsArr && (rn[0] == null || rn[1] == null)) {
2575
							rn = {
2576
								min: rn[0] == null ? autoRangePart : {
2577
									mode: 1,
2578
									hard: rn[0],
2579
									soft: rn[0],
2580
								},
2581
								max: rn[1] == null ? autoRangePart : {
2582
									mode: 1,
2583
									hard: rn[1],
2584
									soft: rn[1],
2585
								},
2586
							};
2587
							rangeIsArr = false;
2588
						}
2589

2590
						if (!rangeIsArr && isObj(rn)) {
2591
							let cfg = rn;
2592
							// this is similar to snapNumY
2593
							rn = (self, dataMin, dataMax) => dataMin == null ? nullNullTuple : rangeNum(dataMin, dataMax, cfg);
2594
						}
2595
					}
2596

2597
					sc.range = fnOrSelf(rn || (isTime ? snapTimeX : scaleKey == xScaleKey ?
2598
						(sc.distr == 3 ? snapLogX : sc.distr == 4 ? snapAsinhX : snapNumX) :
2599
						(sc.distr == 3 ? snapLogY : sc.distr == 4 ? snapAsinhY : snapNumY)
2600
					));
2601

2602
					sc.auto = fnOrSelf(rangeIsArr ? false : sc.auto);
2603

2604
					sc.clamp = fnOrSelf(sc.clamp || clampScale);
2605

2606
					// caches for expensive ops like asinh() & log()
2607
					sc._min = sc._max = null;
2608
				}
2609
			}
2610
		}
2611

2612
		initScale("x");
2613
		initScale("y");
2614

2615
		// TODO: init scales from facets in mode: 2
2616
		if (mode == 1) {
2617
			series.forEach(s => {
2618
				initScale(s.scale);
2619
			});
2620
		}
2621

2622
		axes.forEach(a => {
2623
			initScale(a.scale);
2624
		});
2625

2626
		for (let k in opts.scales)
2627
			initScale(k);
2628

2629
		const scaleX = scales[xScaleKey];
2630

2631
		const xScaleDistr = scaleX.distr;
2632

2633
		let valToPosX, valToPosY;
2634

2635
		if (scaleX.ori == 0) {
2636
			addClass(root, ORI_HZ);
2637
			valToPosX = getHPos;
2638
			valToPosY = getVPos;
2639
			/*
2640
			updOriDims = () => {
2641
				xDimCan = plotWid;
2642
				xOffCan = plotLft;
2643
				yDimCan = plotHgt;
2644
				yOffCan = plotTop;
2645

2646
				xDimCss = plotWidCss;
2647
				xOffCss = plotLftCss;
2648
				yDimCss = plotHgtCss;
2649
				yOffCss = plotTopCss;
2650
			};
2651
			*/
2652
		}
2653
		else {
2654
			addClass(root, ORI_VT);
2655
			valToPosX = getVPos;
2656
			valToPosY = getHPos;
2657
			/*
2658
			updOriDims = () => {
2659
				xDimCan = plotHgt;
2660
				xOffCan = plotTop;
2661
				yDimCan = plotWid;
2662
				yOffCan = plotLft;
2663

2664
				xDimCss = plotHgtCss;
2665
				xOffCss = plotTopCss;
2666
				yDimCss = plotWidCss;
2667
				yOffCss = plotLftCss;
2668
			};
2669
			*/
2670
		}
2671

2672
		const pendScales = {};
2673

2674
		// explicitly-set initial scales
2675
		for (let k in scales) {
2676
			let sc = scales[k];
2677

2678
			if (sc.min != null || sc.max != null) {
2679
				pendScales[k] = {min: sc.min, max: sc.max};
2680
				sc.min = sc.max = null;
2681
			}
2682
		}
2683

2684
	//	self.tz = opts.tz || Intl.DateTimeFormat().resolvedOptions().timeZone;
2685
		const _tzDate  = (opts.tzDate || (ts => new Date(round(ts / ms))));
2686
		const _fmtDate = (opts.fmtDate || fmtDate);
2687

2688
		const _timeAxisSplits = (ms == 1 ? timeAxisSplitsMs(_tzDate) : timeAxisSplitsS(_tzDate));
2689
		const _timeAxisVals   = timeAxisVals(_tzDate, timeAxisStamps((ms == 1 ? _timeAxisStampsMs : _timeAxisStampsS), _fmtDate));
2690
		const _timeSeriesVal  = timeSeriesVal(_tzDate, timeSeriesStamp(_timeSeriesStamp, _fmtDate));
2691

2692
		const activeIdxs = [];
2693

2694
		const legend     = (self.legend = assign({}, legendOpts, opts.legend));
2695
		const showLegend = legend.show;
2696
		const markers    = legend.markers;
2697

2698
		{
2699
			legend.idxs = activeIdxs;
2700

2701
			markers.width  = fnOrSelf(markers.width);
2702
			markers.dash   = fnOrSelf(markers.dash);
2703
			markers.stroke = fnOrSelf(markers.stroke);
2704
			markers.fill   = fnOrSelf(markers.fill);
2705
		}
2706

2707
		let legendEl;
2708
		let legendRows = [];
2709
		let legendCells = [];
2710
		let legendCols;
2711
		let multiValLegend = false;
2712
		let NULL_LEGEND_VALUES = {};
2713

2714
		if (legend.live) {
2715
			const getMultiVals = series[1] ? series[1].values : null;
2716
			multiValLegend = getMultiVals != null;
2717
			legendCols = multiValLegend ? getMultiVals(self, 1, 0) : {_: 0};
2718

2719
			for (let k in legendCols)
2720
				NULL_LEGEND_VALUES[k] = "--";
2721
		}
2722

2723
		if (showLegend) {
2724
			legendEl = placeTag("table", LEGEND, root);
2725

2726
			if (multiValLegend) {
2727
				let head = placeTag("tr", LEGEND_THEAD, legendEl);
2728
				placeTag("th", null, head);
2729

2730
				for (var key in legendCols)
2731
					placeTag("th", LEGEND_LABEL, head).textContent = key;
2732
			}
2733
			else {
2734
				addClass(legendEl, LEGEND_INLINE);
2735
				legend.live && addClass(legendEl, LEGEND_LIVE);
2736
			}
2737
		}
2738

2739
		const son  = {show: true};
2740
		const soff = {show: false};
2741

2742
		function initLegendRow(s, i) {
2743
			if (i == 0 && (multiValLegend || !legend.live || mode == 2))
2744
				return nullNullTuple;
2745

2746
			let cells = [];
2747

2748
			let row = placeTag("tr", LEGEND_SERIES, legendEl, legendEl.childNodes[i]);
2749

2750
			addClass(row, s.class);
2751

2752
			if (!s.show)
2753
				addClass(row, OFF);
2754

2755
			let label = placeTag("th", null, row);
2756

2757
			if (markers.show) {
2758
				let indic = placeDiv(LEGEND_MARKER, label);
2759

2760
				if (i > 0) {
2761
					let width  = markers.width(self, i);
2762

2763
					if (width)
2764
						indic.style.border = width + "px " + markers.dash(self, i) + " " + markers.stroke(self, i);
2765

2766
					indic.style.background = markers.fill(self, i);
2767
				}
2768
			}
2769

2770
			let text = placeDiv(LEGEND_LABEL, label);
2771
			text.textContent = s.label;
2772

2773
			if (i > 0) {
2774
				if (!markers.show)
2775
					text.style.color = s.width > 0 ? markers.stroke(self, i) : markers.fill(self, i);
2776

2777
				onMouse("click", label, e => {
2778
					if (cursor._lock)
2779
						return;
2780

2781
					let seriesIdx = series.indexOf(s);
2782

2783
					if ((e.ctrlKey || e.metaKey) != legend.isolate) {
2784
						// if any other series is shown, isolate this one. else show all
2785
						let isolate = series.some((s, i) => i > 0 && i != seriesIdx && s.show);
2786

2787
						series.forEach((s, i) => {
2788
							i > 0 && setSeries(i, isolate ? (i == seriesIdx ? son : soff) : son, true, syncOpts.setSeries);
2789
						});
2790
					}
2791
					else
2792
						setSeries(seriesIdx, {show: !s.show}, true, syncOpts.setSeries);
2793
				});
2794

2795
				if (cursorFocus) {
2796
					onMouse(mouseenter, label, e => {
2797
						if (cursor._lock)
2798
							return;
2799

2800
						setSeries(series.indexOf(s), FOCUS_TRUE, true, syncOpts.setSeries);
2801
					});
2802
				}
2803
			}
2804

2805
			for (var key in legendCols) {
2806
				let v = placeTag("td", LEGEND_VALUE, row);
2807
				v.textContent = "--";
2808
				cells.push(v);
2809
			}
2810

2811
			return [row, cells];
2812
		}
2813

2814
		const mouseListeners = new Map();
2815

2816
		function onMouse(ev, targ, fn) {
2817
			const targListeners = mouseListeners.get(targ) || {};
2818
			const listener = cursor.bind[ev](self, targ, fn);
2819

2820
			if (listener) {
2821
				on(ev, targ, targListeners[ev] = listener);
2822
				mouseListeners.set(targ, targListeners);
2823
			}
2824
		}
2825

2826
		function offMouse(ev, targ, fn) {
2827
			const targListeners = mouseListeners.get(targ) || {};
2828

2829
			for (let k in targListeners) {
2830
				if (ev == null || k == ev) {
2831
					off(k, targ, targListeners[k]);
2832
					delete targListeners[k];
2833
				}
2834
			}
2835

2836
			if (ev == null)
2837
				mouseListeners.delete(targ);
2838
		}
2839

2840
		let fullWidCss = 0;
2841
		let fullHgtCss = 0;
2842

2843
		let plotWidCss = 0;
2844
		let plotHgtCss = 0;
2845

2846
		// plot margins to account for axes
2847
		let plotLftCss = 0;
2848
		let plotTopCss = 0;
2849

2850
		let plotLft = 0;
2851
		let plotTop = 0;
2852
		let plotWid = 0;
2853
		let plotHgt = 0;
2854

2855
		self.bbox = {};
2856

2857
		let shouldSetScales = false;
2858
		let shouldSetSize = false;
2859
		let shouldConvergeSize = false;
2860
		let shouldSetCursor = false;
2861
		let shouldSetLegend = false;
2862

2863
		function _setSize(width, height, force) {
2864
			if (force || (width != self.width || height != self.height))
2865
				calcSize(width, height);
2866

2867
			resetYSeries(false);
2868

2869
			shouldConvergeSize = true;
2870
			shouldSetSize = true;
2871
			shouldSetCursor = shouldSetLegend = cursor.left >= 0;
2872
			commit();
2873
		}
2874

2875
		function calcSize(width, height) {
2876
		//	log("calcSize()", arguments);
2877

2878
			self.width  = fullWidCss = plotWidCss = width;
2879
			self.height = fullHgtCss = plotHgtCss = height;
2880
			plotLftCss  = plotTopCss = 0;
2881

2882
			calcPlotRect();
2883
			calcAxesRects();
2884

2885
			let bb = self.bbox;
2886

2887
			plotLft = bb.left   = incrRound(plotLftCss * pxRatio, 0.5);
2888
			plotTop = bb.top    = incrRound(plotTopCss * pxRatio, 0.5);
2889
			plotWid = bb.width  = incrRound(plotWidCss * pxRatio, 0.5);
2890
			plotHgt = bb.height = incrRound(plotHgtCss * pxRatio, 0.5);
2891

2892
		//	updOriDims();
2893
		}
2894

2895
		// ensures size calc convergence
2896
		const CYCLE_LIMIT = 3;
2897

2898
		function convergeSize() {
2899
			let converged = false;
2900

2901
			let cycleNum = 0;
2902

2903
			while (!converged) {
2904
				cycleNum++;
2905

2906
				let axesConverged = axesCalc(cycleNum);
2907
				let paddingConverged = paddingCalc(cycleNum);
2908

2909
				converged = cycleNum == CYCLE_LIMIT || (axesConverged && paddingConverged);
2910

2911
				if (!converged) {
2912
					calcSize(self.width, self.height);
2913
					shouldSetSize = true;
2914
				}
2915
			}
2916
		}
2917

2918
		function setSize({width, height}) {
2919
			_setSize(width, height);
2920
		}
2921

2922
		self.setSize = setSize;
2923

2924
		// accumulate axis offsets, reduce canvas width
2925
		function calcPlotRect() {
2926
			// easements for edge labels
2927
			let hasTopAxis = false;
2928
			let hasBtmAxis = false;
2929
			let hasRgtAxis = false;
2930
			let hasLftAxis = false;
2931

2932
			axes.forEach((axis, i) => {
2933
				if (axis.show && axis._show) {
2934
					let {side, _size} = axis;
2935
					let isVt = side % 2;
2936
					let labelSize = axis.label != null ? axis.labelSize : 0;
2937

2938
					let fullSize = _size + labelSize;
2939

2940
					if (fullSize > 0) {
2941
						if (isVt) {
2942
							plotWidCss -= fullSize;
2943

2944
							if (side == 3) {
2945
								plotLftCss += fullSize;
2946
								hasLftAxis = true;
2947
							}
2948
							else
2949
								hasRgtAxis = true;
2950
						}
2951
						else {
2952
							plotHgtCss -= fullSize;
2953

2954
							if (side == 0) {
2955
								plotTopCss += fullSize;
2956
								hasTopAxis = true;
2957
							}
2958
							else
2959
								hasBtmAxis = true;
2960
						}
2961
					}
2962
				}
2963
			});
2964

2965
			sidesWithAxes[0] = hasTopAxis;
2966
			sidesWithAxes[1] = hasRgtAxis;
2967
			sidesWithAxes[2] = hasBtmAxis;
2968
			sidesWithAxes[3] = hasLftAxis;
2969

2970
			// hz padding
2971
			plotWidCss -= _padding[1] + _padding[3];
2972
			plotLftCss += _padding[3];
2973

2974
			// vt padding
2975
			plotHgtCss -= _padding[2] + _padding[0];
2976
			plotTopCss += _padding[0];
2977
		}
2978

2979
		function calcAxesRects() {
2980
			// will accum +
2981
			let off1 = plotLftCss + plotWidCss;
2982
			let off2 = plotTopCss + plotHgtCss;
2983
			// will accum -
2984
			let off3 = plotLftCss;
2985
			let off0 = plotTopCss;
2986

2987
			function incrOffset(side, size) {
2988
				switch (side) {
2989
					case 1: off1 += size; return off1 - size;
2990
					case 2: off2 += size; return off2 - size;
2991
					case 3: off3 -= size; return off3 + size;
2992
					case 0: off0 -= size; return off0 + size;
2993
				}
2994
			}
2995

2996
			axes.forEach((axis, i) => {
2997
				if (axis.show && axis._show) {
2998
					let side = axis.side;
2999

3000
					axis._pos = incrOffset(side, axis._size);
3001

3002
					if (axis.label != null)
3003
						axis._lpos = incrOffset(side, axis.labelSize);
3004
				}
3005
			});
3006
		}
3007

3008
		const cursor = (self.cursor = assign({}, cursorOpts, {drag: {y: mode == 2}}, opts.cursor));
3009

3010
		{
3011
			cursor.idxs = activeIdxs;
3012

3013
			cursor._lock = false;
3014

3015
			let points = cursor.points;
3016

3017
			points.show   = fnOrSelf(points.show);
3018
			points.size   = fnOrSelf(points.size);
3019
			points.stroke = fnOrSelf(points.stroke);
3020
			points.width  = fnOrSelf(points.width);
3021
			points.fill   = fnOrSelf(points.fill);
3022
		}
3023

3024
		const focus = self.focus = assign({}, opts.focus || {alpha: 0.3}, cursor.focus);
3025
		const cursorFocus = focus.prox >= 0;
3026

3027
		// series-intersection markers
3028
		let cursorPts = [null];
3029

3030
		function initCursorPt(s, si) {
3031
			if (si > 0) {
3032
				let pt = cursor.points.show(self, si);
3033

3034
				if (pt) {
3035
					addClass(pt, CURSOR_PT);
3036
					addClass(pt, s.class);
3037
					elTrans(pt, -10, -10, plotWidCss, plotHgtCss);
3038
					over.insertBefore(pt, cursorPts[si]);
3039

3040
					return pt;
3041
				}
3042
			}
3043
		}
3044

3045
		function initSeries(s, i) {
3046
			if (mode == 1 || i > 0) {
3047
				let isTime = mode == 1 && scales[s.scale].time;
3048

3049
				let sv = s.value;
3050
				s.value = isTime ? (isStr(sv) ? timeSeriesVal(_tzDate, timeSeriesStamp(sv, _fmtDate)) : sv || _timeSeriesVal) : sv || numSeriesVal;
3051
				s.label = s.label || (isTime ? timeSeriesLabel : numSeriesLabel);
3052
			}
3053

3054
			if (i > 0) {
3055
				s.width  = s.width == null ? 1 : s.width;
3056
				s.paths  = s.paths || linearPath || retNull;
3057
				s.fillTo = fnOrSelf(s.fillTo || seriesFillTo);
3058
				s.pxAlign = +ifNull(s.pxAlign, pxAlign);
3059
				s.pxRound = pxRoundGen(s.pxAlign);
3060

3061
				s.stroke = fnOrSelf(s.stroke || null);
3062
				s.fill   = fnOrSelf(s.fill || null);
3063
				s._stroke = s._fill = s._paths = s._focus = null;
3064

3065
				let _ptDia = ptDia(s.width, 1);
3066
				let points = s.points = assign({}, {
3067
					size: _ptDia,
3068
					width: max(1, _ptDia * .2),
3069
					stroke: s.stroke,
3070
					space: _ptDia * 2,
3071
					paths: pointsPath,
3072
					_stroke: null,
3073
					_fill: null,
3074
				}, s.points);
3075
				points.show   = fnOrSelf(points.show);
3076
				points.filter = fnOrSelf(points.filter);
3077
				points.fill   = fnOrSelf(points.fill);
3078
				points.stroke = fnOrSelf(points.stroke);
3079
				points.paths  = fnOrSelf(points.paths);
3080
				points.pxAlign = s.pxAlign;
3081
			}
3082

3083
			if (showLegend) {
3084
				let rowCells = initLegendRow(s, i);
3085
				legendRows.splice(i, 0, rowCells[0]);
3086
				legendCells.splice(i, 0, rowCells[1]);
3087
				legend.values.push(null);	// NULL_LEGEND_VALS not yet avil here :(
3088
			}
3089

3090
			if (cursor.show) {
3091
				activeIdxs.splice(i, 0, null);
3092

3093
				let pt = initCursorPt(s, i);
3094
				pt && cursorPts.splice(i, 0, pt);
3095
			}
3096
		}
3097

3098
		function addSeries(opts, si) {
3099
			si = si == null ? series.length : si;
3100

3101
			opts = setDefault(opts, si, xSeriesOpts, ySeriesOpts);
3102
			series.splice(si, 0, opts);
3103
			initSeries(series[si], si);
3104
		}
3105

3106
		self.addSeries = addSeries;
3107

3108
		function delSeries(i) {
3109
			series.splice(i, 1);
3110

3111
			if (showLegend) {
3112
				legend.values.splice(i, 1);
3113

3114
				legendCells.splice(i, 1);
3115
				let tr = legendRows.splice(i, 1)[0];
3116
				offMouse(null, tr.firstChild);
3117
				tr.remove();
3118
			}
3119

3120
			if (cursor.show) {
3121
				activeIdxs.splice(i, 1);
3122

3123
				cursorPts.length > 1 && cursorPts.splice(i, 1)[0].remove();
3124
			}
3125

3126
			// TODO: de-init no-longer-needed scales?
3127
		}
3128

3129
		self.delSeries = delSeries;
3130

3131
		const sidesWithAxes = [false, false, false, false];
3132

3133
		function initAxis(axis, i) {
3134
			axis._show = axis.show;
3135

3136
			if (axis.show) {
3137
				let isVt = axis.side % 2;
3138

3139
				let sc = scales[axis.scale];
3140

3141
				// this can occur if all series specify non-default scales
3142
				if (sc == null) {
3143
					axis.scale = isVt ? series[1].scale : xScaleKey;
3144
					sc = scales[axis.scale];
3145
				}
3146

3147
				// also set defaults for incrs & values based on axis distr
3148
				let isTime = sc.time;
3149

3150
				axis.size   = fnOrSelf(axis.size);
3151
				axis.space  = fnOrSelf(axis.space);
3152
				axis.rotate = fnOrSelf(axis.rotate);
3153
				axis.incrs  = fnOrSelf(axis.incrs  || (          sc.distr == 2 ? wholeIncrs : (isTime ? (ms == 1 ? timeIncrsMs : timeIncrsS) : numIncrs)));
3154
				axis.splits = fnOrSelf(axis.splits || (isTime && sc.distr == 1 ? _timeAxisSplits : sc.distr == 3 ? logAxisSplits : sc.distr == 4 ? asinhAxisSplits : numAxisSplits));
3155

3156
				axis.stroke       = fnOrSelf(axis.stroke);
3157
				axis.grid.stroke  = fnOrSelf(axis.grid.stroke);
3158
				axis.ticks.stroke = fnOrSelf(axis.ticks.stroke);
3159

3160
				let av = axis.values;
3161

3162
				axis.values = (
3163
					// static array of tick values
3164
					isArr(av) && !isArr(av[0]) ? fnOrSelf(av) :
3165
					// temporal
3166
					isTime ? (
3167
						// config array of fmtDate string tpls
3168
						isArr(av) ?
3169
							timeAxisVals(_tzDate, timeAxisStamps(av, _fmtDate)) :
3170
						// fmtDate string tpl
3171
						isStr(av) ?
3172
							timeAxisVal(_tzDate, av) :
3173
						av || _timeAxisVals
3174
					) : av || numAxisVals
3175
				);
3176

3177
				axis.filter = fnOrSelf(axis.filter || (          sc.distr >= 3 ? logAxisValsFilt : retArg1));
3178

3179
				axis.font      = pxRatioFont(axis.font);
3180
				axis.labelFont = pxRatioFont(axis.labelFont);
3181

3182
				axis._size   = axis.size(self, null, i, 0);
3183

3184
				axis._space  =
3185
				axis._rotate =
3186
				axis._incrs  =
3187
				axis._found  =	// foundIncrSpace
3188
				axis._splits =
3189
				axis._values = null;
3190

3191
				if (axis._size > 0)
3192
					sidesWithAxes[i] = true;
3193

3194
				axis._el = placeDiv(AXIS, wrap);
3195

3196
				// debug
3197
			//	axis._el.style.background = "#"  + Math.floor(Math.random()*16777215).toString(16) + '80';
3198
			}
3199
		}
3200

3201
		function autoPadSide(self, side, sidesWithAxes, cycleNum) {
3202
			let [hasTopAxis, hasRgtAxis, hasBtmAxis, hasLftAxis] = sidesWithAxes;
3203

3204
			let ori = side % 2;
3205
			let size = 0;
3206

3207
			if (ori == 0 && (hasLftAxis || hasRgtAxis))
3208
				size = (side == 0 && !hasTopAxis || side == 2 && !hasBtmAxis ? round(xAxisOpts.size / 3) : 0);
3209
			if (ori == 1 && (hasTopAxis || hasBtmAxis))
3210
				size = (side == 1 && !hasRgtAxis || side == 3 && !hasLftAxis ? round(yAxisOpts.size / 2) : 0);
3211

3212
			return size;
3213
		}
3214

3215
		const padding = self.padding = (opts.padding || [autoPadSide,autoPadSide,autoPadSide,autoPadSide]).map(p => fnOrSelf(ifNull(p, autoPadSide)));
3216
		const _padding = self._padding = padding.map((p, i) => p(self, i, sidesWithAxes, 0));
3217

3218
		let dataLen;
3219

3220
		// rendered data window
3221
		let i0 = null;
3222
		let i1 = null;
3223
		const idxs = mode == 1 ? series[0].idxs : null;
3224

3225
		let data0 = null;
3226

3227
		let viaAutoScaleX = false;
3228

3229
		function setData(_data, _resetScales) {
3230
			if (mode == 2) {
3231
				dataLen = 0;
3232
				for (let i = 1; i < series.length; i++)
3233
					dataLen += data[i][0].length;
3234
				self.data = data = _data;
3235
			}
3236
			else {
3237
				data = (_data || []).slice();
3238
				data[0] = data[0] || [];
3239

3240
				self.data = data.slice();
3241
				data0 = data[0];
3242
				dataLen = data0.length;
3243

3244
				if (xScaleDistr == 2)
3245
					data[0] = data0.map((v, i) => i);
3246
			}
3247

3248
			self._data = data;
3249

3250
			resetYSeries(true);
3251

3252
			fire("setData");
3253

3254
			if (_resetScales !== false) {
3255
				let xsc = scaleX;
3256

3257
				if (xsc.auto(self, viaAutoScaleX))
3258
					autoScaleX();
3259
				else
3260
					_setScale(xScaleKey, xsc.min, xsc.max);
3261

3262
				shouldSetCursor = cursor.left >= 0;
3263
				shouldSetLegend = true;
3264
				commit();
3265
			}
3266
		}
3267

3268
		self.setData = setData;
3269

3270
		function autoScaleX() {
3271
			viaAutoScaleX = true;
3272

3273
			let _min, _max;
3274

3275
			if (mode == 1) {
3276
				if (dataLen > 0) {
3277
					i0 = idxs[0] = 0;
3278
					i1 = idxs[1] = dataLen - 1;
3279

3280
					_min = data[0][i0];
3281
					_max = data[0][i1];
3282

3283
					if (xScaleDistr == 2) {
3284
						_min = i0;
3285
						_max = i1;
3286
					}
3287
					else if (dataLen == 1) {
3288
						if (xScaleDistr == 3)
3289
							[_min, _max] = rangeLog(_min, _min, scaleX.log, false);
3290
						else if (xScaleDistr == 4)
3291
							[_min, _max] = rangeAsinh(_min, _min, scaleX.log, false);
3292
						else if (scaleX.time)
3293
							_max = _min + round(86400 / ms);
3294
						else
3295
							[_min, _max] = rangeNum(_min, _max, rangePad, true);
3296
					}
3297
				}
3298
				else {
3299
					i0 = idxs[0] = _min = null;
3300
					i1 = idxs[1] = _max = null;
3301
				}
3302
			}
3303

3304
			_setScale(xScaleKey, _min, _max);
3305
		}
3306

3307
		let ctxStroke, ctxFill, ctxWidth, ctxDash, ctxJoin, ctxCap, ctxFont, ctxAlign, ctxBaseline;
3308
		let ctxAlpha;
3309

3310
		function setCtxStyle(stroke = transparent, width, dash = EMPTY_ARR, cap = "butt", fill = transparent, join = "round") {
3311
			if (stroke != ctxStroke)
3312
				ctx.strokeStyle = ctxStroke = stroke;
3313
			if (fill != ctxFill)
3314
				ctx.fillStyle = ctxFill = fill;
3315
			if (width != ctxWidth)
3316
				ctx.lineWidth = ctxWidth = width;
3317
			if (join != ctxJoin)
3318
				ctx.lineJoin = ctxJoin = join;
3319
			if (cap != ctxCap)
3320
				ctx.lineCap = ctxCap = cap; // (‿|‿)
3321
			if (dash != ctxDash)
3322
				ctx.setLineDash(ctxDash = dash);
3323
		}
3324

3325
		function setFontStyle(font, fill, align, baseline) {
3326
			if (fill != ctxFill)
3327
				ctx.fillStyle = ctxFill = fill;
3328
			if (font != ctxFont)
3329
				ctx.font = ctxFont = font;
3330
			if (align != ctxAlign)
3331
				ctx.textAlign = ctxAlign = align;
3332
			if (baseline != ctxBaseline)
3333
				ctx.textBaseline = ctxBaseline = baseline;
3334
		}
3335

3336
		function accScale(wsc, psc, facet, data) {
3337
			if (wsc.auto(self, viaAutoScaleX) && (psc == null || psc.min == null)) {
3338
				let _i0 = ifNull(i0, 0);
3339
				let _i1 = ifNull(i1, data.length - 1);
3340

3341
				// only run getMinMax() for invalidated series data, else reuse
3342
				let minMax = facet.min == null ? (wsc.distr == 3 ? getMinMaxLog(data, _i0, _i1) : getMinMax(data, _i0, _i1)) : [facet.min, facet.max];
3343

3344
				// initial min/max
3345
				wsc.min = min(wsc.min, facet.min = minMax[0]);
3346
				wsc.max = max(wsc.max, facet.max = minMax[1]);
3347
			}
3348
		}
3349

3350
		function setScales() {
3351
		//	log("setScales()", arguments);
3352

3353
			// wip scales
3354
			let wipScales = copy(scales, fastIsObj);
3355

3356
			for (let k in wipScales) {
3357
				let wsc = wipScales[k];
3358
				let psc = pendScales[k];
3359

3360
				if (psc != null && psc.min != null) {
3361
					assign(wsc, psc);
3362

3363
					// explicitly setting the x-scale invalidates everything (acts as redraw)
3364
					if (k == xScaleKey)
3365
						resetYSeries(true);
3366
				}
3367
				else if (k != xScaleKey || mode == 2) {
3368
					if (dataLen == 0 && wsc.from == null) {
3369
						let minMax = wsc.range(self, null, null, k);
3370
						wsc.min = minMax[0];
3371
						wsc.max = minMax[1];
3372
					}
3373
					else {
3374
						wsc.min = inf;
3375
						wsc.max = -inf;
3376
					}
3377
				}
3378
			}
3379

3380
			if (dataLen > 0) {
3381
				// pre-range y-scales from y series' data values
3382
				series.forEach((s, i) => {
3383
					if (mode == 1) {
3384
						let k = s.scale;
3385
						let wsc = wipScales[k];
3386
						let psc = pendScales[k];
3387

3388
						if (i == 0) {
3389
							let minMax = wsc.range(self, wsc.min, wsc.max, k);
3390

3391
							wsc.min = minMax[0];
3392
							wsc.max = minMax[1];
3393

3394
							i0 = closestIdx(wsc.min, data[0]);
3395
							i1 = closestIdx(wsc.max, data[0]);
3396

3397
							// closest indices can be outside of view
3398
							if (data[0][i0] < wsc.min)
3399
								i0++;
3400
							if (data[0][i1] > wsc.max)
3401
								i1--;
3402

3403
							s.min = data0[i0];
3404
							s.max = data0[i1];
3405
						}
3406
						else if (s.show && s.auto)
3407
							accScale(wsc, psc, s, data[i]);
3408

3409
						s.idxs[0] = i0;
3410
						s.idxs[1] = i1;
3411
					}
3412
					else {
3413
						if (i > 0) {
3414
							if (s.show && s.auto) {
3415
								// TODO: only handles, assumes and requires facets[0] / 'x' scale, and facets[1] / 'y' scale
3416
								let [ xFacet, yFacet ] = s.facets;
3417
								let xScaleKey = xFacet.scale;
3418
								let yScaleKey = yFacet.scale;
3419
								let [ xData, yData ] = data[i];
3420

3421
								accScale(wipScales[xScaleKey], pendScales[xScaleKey], xFacet, xData);
3422
								accScale(wipScales[yScaleKey], pendScales[yScaleKey], yFacet, yData);
3423

3424
								// temp
3425
								s.min = yFacet.min;
3426
								s.max = yFacet.max;
3427
							}
3428
						}
3429
					}
3430
				});
3431

3432
				// range independent scales
3433
				for (let k in wipScales) {
3434
					let wsc = wipScales[k];
3435
					let psc = pendScales[k];
3436

3437
					if (wsc.from == null && (psc == null || psc.min == null)) {
3438
						let minMax = wsc.range(
3439
							self,
3440
							wsc.min ==  inf ? null : wsc.min,
3441
							wsc.max == -inf ? null : wsc.max,
3442
							k
3443
						);
3444
						wsc.min = minMax[0];
3445
						wsc.max = minMax[1];
3446
					}
3447
				}
3448
			}
3449

3450
			// range dependent scales
3451
			for (let k in wipScales) {
3452
				let wsc = wipScales[k];
3453

3454
				if (wsc.from != null) {
3455
					let base = wipScales[wsc.from];
3456

3457
					if (base.min == null)
3458
						wsc.min = wsc.max = null;
3459
					else {
3460
						let minMax = wsc.range(self, base.min, base.max, k);
3461
						wsc.min = minMax[0];
3462
						wsc.max = minMax[1];
3463
					}
3464
				}
3465
			}
3466

3467
			let changed = {};
3468
			let anyChanged = false;
3469

3470
			for (let k in wipScales) {
3471
				let wsc = wipScales[k];
3472
				let sc = scales[k];
3473

3474
				if (sc.min != wsc.min || sc.max != wsc.max) {
3475
					sc.min = wsc.min;
3476
					sc.max = wsc.max;
3477

3478
					let distr = sc.distr;
3479

3480
					sc._min = distr == 3 ? log10(sc.min) : distr == 4 ? asinh(sc.min, sc.asinh) : sc.min;
3481
					sc._max = distr == 3 ? log10(sc.max) : distr == 4 ? asinh(sc.max, sc.asinh) : sc.max;
3482

3483
					changed[k] = anyChanged = true;
3484
				}
3485
			}
3486

3487
			if (anyChanged) {
3488
				// invalidate paths of all series on changed scales
3489
				series.forEach((s, i) => {
3490
					if (mode == 2) {
3491
						if (i > 0 && changed.y)
3492
							s._paths = null;
3493
					}
3494
					else {
3495
						if (changed[s.scale])
3496
							s._paths = null;
3497
					}
3498
				});
3499

3500
				for (let k in changed) {
3501
					shouldConvergeSize = true;
3502
					fire("setScale", k);
3503
				}
3504

3505
				if (cursor.show)
3506
					shouldSetCursor = shouldSetLegend = cursor.left >= 0;
3507
			}
3508

3509
			for (let k in pendScales)
3510
				pendScales[k] = null;
3511
		}
3512

3513
		// grabs the nearest indices with y data outside of x-scale limits
3514
		function getOuterIdxs(ydata) {
3515
			let _i0 = clamp(i0 - 1, 0, dataLen - 1);
3516
			let _i1 = clamp(i1 + 1, 0, dataLen - 1);
3517

3518
			while (ydata[_i0] == null && _i0 > 0)
3519
				_i0--;
3520

3521
			while (ydata[_i1] == null && _i1 < dataLen - 1)
3522
				_i1++;
3523

3524
			return [_i0, _i1];
3525
		}
3526

3527
		function drawSeries() {
3528
			if (dataLen > 0) {
3529
				series.forEach((s, i) => {
3530
					if (i > 0 && s.show && s._paths == null) {
3531
						let _idxs = getOuterIdxs(data[i]);
3532
						s._paths = s.paths(self, i, _idxs[0], _idxs[1]);
3533
					}
3534
				});
3535

3536
				series.forEach((s, i) => {
3537
					if (i > 0 && s.show) {
3538
						if (ctxAlpha != s.alpha)
3539
							ctx.globalAlpha = ctxAlpha = s.alpha;
3540

3541
						{
3542
							cacheStrokeFill(i, false);
3543
							s._paths && drawPath(i, false);
3544
						}
3545

3546
						{
3547
							cacheStrokeFill(i, true);
3548

3549
							let show = s.points.show(self, i, i0, i1);
3550
							let idxs = s.points.filter(self, i, show, s._paths ? s._paths.gaps : null);
3551

3552
							if (show || idxs) {
3553
								s.points._paths = s.points.paths(self, i, i0, i1, idxs);
3554
								drawPath(i, true);
3555
							}
3556
						}
3557

3558
						if (ctxAlpha != 1)
3559
							ctx.globalAlpha = ctxAlpha = 1;
3560

3561
						fire("drawSeries", i);
3562
					}
3563
				});
3564
			}
3565
		}
3566

3567
		function cacheStrokeFill(si, _points) {
3568
			let s = _points ? series[si].points : series[si];
3569

3570
			s._stroke = s.stroke(self, si);
3571
			s._fill   = s.fill(self, si);
3572
		}
3573

3574
		function drawPath(si, _points) {
3575
			let s = _points ? series[si].points : series[si];
3576

3577
			let strokeStyle = s._stroke;
3578
			let fillStyle   = s._fill;
3579

3580
			let { stroke, fill, clip: gapsClip, flags } = s._paths;
3581
			let boundsClip = null;
3582
			let width = roundDec(s.width * pxRatio, 3);
3583
			let offset = (width % 2) / 2;
3584

3585
			if (_points && fillStyle == null)
3586
				fillStyle = width > 0 ? "#fff" : strokeStyle;
3587

3588
			let _pxAlign = s.pxAlign == 1;
3589

3590
			_pxAlign && ctx.translate(offset, offset);
3591

3592
			if (!_points) {
3593
				let lft = plotLft,
3594
					top = plotTop,
3595
					wid = plotWid,
3596
					hgt = plotHgt;
3597

3598
				let halfWid = width * pxRatio / 2;
3599

3600
				if (s.min == 0)
3601
					hgt += halfWid;
3602

3603
				if (s.max == 0) {
3604
					top -= halfWid;
3605
					hgt += halfWid;
3606
				}
3607

3608
				boundsClip = new Path2D();
3609
				boundsClip.rect(lft, top, wid, hgt);
3610
			}
3611

3612
			// the points pathbuilder's gapsClip is its boundsClip, since points dont need gaps clipping, and bounds depend on point size
3613
			if (_points)
3614
				strokeFill(strokeStyle, width, s.dash, s.cap, fillStyle, stroke, fill, flags, gapsClip);
3615
			else
3616
				fillStroke(si, strokeStyle, width, s.dash, s.cap, fillStyle, stroke, fill, flags, boundsClip, gapsClip);
3617

3618
			_pxAlign && ctx.translate(-offset, -offset);
3619
		}
3620

3621
		function fillStroke(si, strokeStyle, lineWidth, lineDash, lineCap, fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip) {
3622
			let didStrokeFill = false;
3623

3624
			// for all bands where this series is the top edge, create upwards clips using the bottom edges
3625
			// and apply clips + fill with band fill or dfltFill
3626
			bands.forEach((b, bi) => {
3627
				// isUpperEdge?
3628
				if (b.series[0] == si) {
3629
					let lowerEdge = series[b.series[1]];
3630
					let lowerData = data[b.series[1]];
3631

3632
					let bandClip = (lowerEdge._paths || EMPTY_OBJ).band;
3633
					let gapsClip2;
3634

3635
					let _fillStyle = null;
3636

3637
					// hasLowerEdge?
3638
					if (lowerEdge.show && bandClip && hasData(lowerData, i0, i1)) {
3639
						_fillStyle = b.fill(self, bi) || fillStyle;
3640
						gapsClip2 = lowerEdge._paths.clip;
3641
					}
3642
					else
3643
						bandClip = null;
3644

3645
					strokeFill(strokeStyle, lineWidth, lineDash, lineCap, _fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip, gapsClip2, bandClip);
3646

3647
					didStrokeFill = true;
3648
				}
3649
			});
3650

3651
			if (!didStrokeFill)
3652
				strokeFill(strokeStyle, lineWidth, lineDash, lineCap, fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip);
3653
		}
3654

3655
		const CLIP_FILL_STROKE = BAND_CLIP_FILL | BAND_CLIP_STROKE;
3656

3657
		function strokeFill(strokeStyle, lineWidth, lineDash, lineCap, fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip, gapsClip2, bandClip) {
3658
			setCtxStyle(strokeStyle, lineWidth, lineDash, lineCap, fillStyle);
3659

3660
			if (boundsClip || gapsClip || bandClip) {
3661
				ctx.save();
3662
				boundsClip && ctx.clip(boundsClip);
3663
				gapsClip && ctx.clip(gapsClip);
3664
			}
3665

3666
			if (bandClip) {
3667
				if ((flags & CLIP_FILL_STROKE) == CLIP_FILL_STROKE) {
3668
					ctx.clip(bandClip);
3669
					gapsClip2 && ctx.clip(gapsClip2);
3670
					doFill(fillStyle, fillPath);
3671
					doStroke(strokeStyle, strokePath, lineWidth);
3672
				}
3673
				else if (flags & BAND_CLIP_STROKE) {
3674
					doFill(fillStyle, fillPath);
3675
					ctx.clip(bandClip);
3676
					doStroke(strokeStyle, strokePath, lineWidth);
3677
				}
3678
				else if (flags & BAND_CLIP_FILL) {
3679
					ctx.save();
3680
					ctx.clip(bandClip);
3681
					gapsClip2 && ctx.clip(gapsClip2);
3682
					doFill(fillStyle, fillPath);
3683
					ctx.restore();
3684
					doStroke(strokeStyle, strokePath, lineWidth);
3685
				}
3686
			}
3687
			else {
3688
				doFill(fillStyle, fillPath);
3689
				doStroke(strokeStyle, strokePath, lineWidth);
3690
			}
3691

3692
			if (boundsClip || gapsClip || bandClip)
3693
				ctx.restore();
3694
		}
3695

3696
		function doStroke(strokeStyle, strokePath, lineWidth) {
3697
			if (lineWidth > 0) {
3698
				if (strokePath instanceof Map) {
3699
					strokePath.forEach((strokePath, strokeStyle) => {
3700
						ctx.strokeStyle = ctxStroke = strokeStyle;
3701
						ctx.stroke(strokePath);
3702
					});
3703
				}
3704
				else
3705
					strokePath != null && strokeStyle && ctx.stroke(strokePath);
3706
			}
3707
		}
3708

3709
		function doFill(fillStyle, fillPath) {
3710
			if (fillPath instanceof Map) {
3711
				fillPath.forEach((fillPath, fillStyle) => {
3712
					ctx.fillStyle = ctxFill = fillStyle;
3713
					ctx.fill(fillPath);
3714
				});
3715
			}
3716
			else
3717
				fillPath != null && fillStyle && ctx.fill(fillPath);
3718
		}
3719

3720
		function getIncrSpace(axisIdx, min, max, fullDim) {
3721
			let axis = axes[axisIdx];
3722

3723
			let incrSpace;
3724

3725
			if (fullDim <= 0)
3726
				incrSpace = [0, 0];
3727
			else {
3728
				let minSpace = axis._space = axis.space(self, axisIdx, min, max, fullDim);
3729
				let incrs    = axis._incrs = axis.incrs(self, axisIdx, min, max, fullDim, minSpace);
3730
				incrSpace    = findIncr(min, max, incrs, fullDim, minSpace);
3731
			}
3732

3733
			return (axis._found = incrSpace);
3734
		}
3735

3736
		function drawOrthoLines(offs, filts, ori, side, pos0, len, width, stroke, dash, cap) {
3737
			let offset = (width % 2) / 2;
3738

3739
			pxAlign == 1 && ctx.translate(offset, offset);
3740

3741
			setCtxStyle(stroke, width, dash, cap, stroke);
3742

3743
			ctx.beginPath();
3744

3745
			let x0, y0, x1, y1, pos1 = pos0 + (side == 0 || side == 3 ? -len : len);
3746

3747
			if (ori == 0) {
3748
				y0 = pos0;
3749
				y1 = pos1;
3750
			}
3751
			else {
3752
				x0 = pos0;
3753
				x1 = pos1;
3754
			}
3755

3756
			for (let i = 0; i < offs.length; i++) {
3757
				if (filts[i] != null) {
3758
					if (ori == 0)
3759
						x0 = x1 = offs[i];
3760
					else
3761
						y0 = y1 = offs[i];
3762

3763
					ctx.moveTo(x0, y0);
3764
					ctx.lineTo(x1, y1);
3765
				}
3766
			}
3767

3768
			ctx.stroke();
3769

3770
			pxAlign == 1 && ctx.translate(-offset, -offset);
3771
		}
3772

3773
		function axesCalc(cycleNum) {
3774
		//	log("axesCalc()", arguments);
3775

3776
			let converged = true;
3777

3778
			axes.forEach((axis, i) => {
3779
				if (!axis.show)
3780
					return;
3781

3782
				let scale = scales[axis.scale];
3783

3784
				if (scale.min == null) {
3785
					if (axis._show) {
3786
						converged = false;
3787
						axis._show = false;
3788
						resetYSeries(false);
3789
					}
3790
					return;
3791
				}
3792
				else {
3793
					if (!axis._show) {
3794
						converged = false;
3795
						axis._show = true;
3796
						resetYSeries(false);
3797
					}
3798
				}
3799

3800
				let side = axis.side;
3801
				let ori = side % 2;
3802

3803
				let {min, max} = scale;		// 		// should this toggle them ._show = false
3804

3805
				let [_incr, _space] = getIncrSpace(i, min, max, ori == 0 ? plotWidCss : plotHgtCss);
3806

3807
				if (_space == 0)
3808
					return;
3809

3810
				// if we're using index positions, force first tick to match passed index
3811
				let forceMin = scale.distr == 2;
3812

3813
				let _splits = axis._splits = axis.splits(self, i, min, max, _incr, _space, forceMin);
3814

3815
				// tick labels
3816
				// BOO this assumes a specific data/series
3817
				let splits = scale.distr == 2 ? _splits.map(i => data0[i]) : _splits;
3818
				let incr   = scale.distr == 2 ? data0[_splits[1]] - data0[_splits[0]] : _incr;
3819

3820
				let values = axis._values = axis.values(self, axis.filter(self, splits, i, _space, incr), i, _space, incr);
3821

3822
				// rotating of labels only supported on bottom x axis
3823
				axis._rotate = side == 2 ? axis.rotate(self, values, i, _space) : 0;
3824

3825
				let oldSize = axis._size;
3826

3827
				axis._size = ceil(axis.size(self, values, i, cycleNum));
3828

3829
				if (oldSize != null && axis._size != oldSize)			// ready && ?
3830
					converged = false;
3831
			});
3832

3833
			return converged;
3834
		}
3835

3836
		function paddingCalc(cycleNum) {
3837
			let converged = true;
3838

3839
			padding.forEach((p, i) => {
3840
				let _p = p(self, i, sidesWithAxes, cycleNum);
3841

3842
				if (_p != _padding[i])
3843
					converged = false;
3844

3845
				_padding[i] = _p;
3846
			});
3847

3848
			return converged;
3849
		}
3850

3851
		function drawAxesGrid() {
3852
			for (let i = 0; i < axes.length; i++) {
3853
				let axis = axes[i];
3854

3855
				if (!axis.show || !axis._show)
3856
					continue;
3857

3858
				let side = axis.side;
3859
				let ori = side % 2;
3860

3861
				let x, y;
3862

3863
				let fillStyle = axis.stroke(self, i);
3864

3865
				let shiftDir = side == 0 || side == 3 ? -1 : 1;
3866

3867
				// axis label
3868
				if (axis.label) {
3869
					let shiftAmt = axis.labelGap * shiftDir;
3870
					let baseLpos = round((axis._lpos + shiftAmt) * pxRatio);
3871

3872
					setFontStyle(axis.labelFont[0], fillStyle, "center", side == 2 ? TOP : BOTTOM);
3873

3874
					ctx.save();
3875

3876
					if (ori == 1) {
3877
						x = y = 0;
3878

3879
						ctx.translate(
3880
							baseLpos,
3881
							round(plotTop + plotHgt / 2),
3882
						);
3883
						ctx.rotate((side == 3 ? -PI : PI) / 2);
3884

3885
					}
3886
					else {
3887
						x = round(plotLft + plotWid / 2);
3888
						y = baseLpos;
3889
					}
3890

3891
					ctx.fillText(axis.label, x, y);
3892

3893
					ctx.restore();
3894
				}
3895

3896
				let [_incr, _space] = axis._found;
3897

3898
				if (_space == 0)
3899
					continue;
3900

3901
				let scale = scales[axis.scale];
3902

3903
				let plotDim = ori == 0 ? plotWid : plotHgt;
3904
				let plotOff = ori == 0 ? plotLft : plotTop;
3905

3906
				let axisGap = round(axis.gap * pxRatio);
3907

3908
				let _splits = axis._splits;
3909

3910
				// tick labels
3911
				// BOO this assumes a specific data/series
3912
				let splits = scale.distr == 2 ? _splits.map(i => data0[i]) : _splits;
3913
				let incr   = scale.distr == 2 ? data0[_splits[1]] - data0[_splits[0]] : _incr;
3914

3915
				let ticks = axis.ticks;
3916
				let tickSize = ticks.show ? round(ticks.size * pxRatio) : 0;
3917

3918
				// rotating of labels only supported on bottom x axis
3919
				let angle = axis._rotate * -PI/180;
3920

3921
				let basePos  = pxRound(axis._pos * pxRatio);
3922
				let shiftAmt = (tickSize + axisGap) * shiftDir;
3923
				let finalPos = basePos + shiftAmt;
3924
				    y        = ori == 0 ? finalPos : 0;
3925
				    x        = ori == 1 ? finalPos : 0;
3926

3927
				let font         = axis.font[0];
3928
				let textAlign    = axis.align == 1 ? LEFT :
3929
				                   axis.align == 2 ? RIGHT :
3930
				                   angle > 0 ? LEFT :
3931
				                   angle < 0 ? RIGHT :
3932
				                   ori == 0 ? "center" : side == 3 ? RIGHT : LEFT;
3933
				let textBaseline = angle ||
3934
				                   ori == 1 ? "middle" : side == 2 ? TOP   : BOTTOM;
3935

3936
				setFontStyle(font, fillStyle, textAlign, textBaseline);
3937

3938
				let lineHeight = axis.font[1] * lineMult;
3939

3940
				let canOffs = _splits.map(val => pxRound(getPos(val, scale, plotDim, plotOff)));
3941

3942
				let _values = axis._values;
3943

3944
				for (let i = 0; i < _values.length; i++) {
3945
					let val = _values[i];
3946

3947
					if (val != null) {
3948
						if (ori == 0)
3949
							x = canOffs[i];
3950
						else
3951
							y = canOffs[i];
3952

3953
						val = "" + val;
3954

3955
						let _parts = val.indexOf("\n") == -1 ? [val] : val.split(/\n/gm);
3956

3957
						for (let j = 0; j < _parts.length; j++) {
3958
							let text = _parts[j];
3959

3960
							if (angle) {
3961
								ctx.save();
3962
								ctx.translate(x, y + j * lineHeight); // can this be replaced with position math?
3963
								ctx.rotate(angle); // can this be done once?
3964
								ctx.fillText(text, 0, 0);
3965
								ctx.restore();
3966
							}
3967
							else
3968
								ctx.fillText(text, x, y + j * lineHeight);
3969
						}
3970
					}
3971
				}
3972

3973
				// ticks
3974
				if (ticks.show) {
3975
					drawOrthoLines(
3976
						canOffs,
3977
						ticks.filter(self, splits, i, _space, incr),
3978
						ori,
3979
						side,
3980
						basePos,
3981
						tickSize,
3982
						roundDec(ticks.width * pxRatio, 3),
3983
						ticks.stroke(self, i),
3984
						ticks.dash,
3985
						ticks.cap,
3986
					);
3987
				}
3988

3989
				// grid
3990
				let grid = axis.grid;
3991

3992
				if (grid.show) {
3993
					drawOrthoLines(
3994
						canOffs,
3995
						grid.filter(self, splits, i, _space, incr),
3996
						ori,
3997
						ori == 0 ? 2 : 1,
3998
						ori == 0 ? plotTop : plotLft,
3999
						ori == 0 ? plotHgt : plotWid,
4000
						roundDec(grid.width * pxRatio, 3),
4001
						grid.stroke(self, i),
4002
						grid.dash,
4003
						grid.cap,
4004
					);
4005
				}
4006
			}
4007

4008
			fire("drawAxes");
4009
		}
4010

4011
		function resetYSeries(minMax) {
4012
		//	log("resetYSeries()", arguments);
4013

4014
			series.forEach((s, i) => {
4015
				if (i > 0) {
4016
					s._paths = null;
4017

4018
					if (minMax) {
4019
						if (mode == 1) {
4020
							s.min = null;
4021
							s.max = null;
4022
						}
4023
						else {
4024
							s.facets.forEach(f => {
4025
								f.min = null;
4026
								f.max = null;
4027
							});
4028
						}
4029
					}
4030
				}
4031
			});
4032
		}
4033

4034
		let queuedCommit = false;
4035

4036
		function commit() {
4037
			if (!queuedCommit) {
4038
				microTask(_commit);
4039
				queuedCommit = true;
4040
			}
4041
		}
4042

4043
		function _commit() {
4044
		//	log("_commit()", arguments);
4045

4046
			if (shouldSetScales) {
4047
				setScales();
4048
				shouldSetScales = false;
4049
			}
4050

4051
			if (shouldConvergeSize) {
4052
				convergeSize();
4053
				shouldConvergeSize = false;
4054
			}
4055

4056
			if (shouldSetSize) {
4057
				setStylePx(under, LEFT,   plotLftCss);
4058
				setStylePx(under, TOP,    plotTopCss);
4059
				setStylePx(under, WIDTH,  plotWidCss);
4060
				setStylePx(under, HEIGHT, plotHgtCss);
4061

4062
				setStylePx(over, LEFT,    plotLftCss);
4063
				setStylePx(over, TOP,     plotTopCss);
4064
				setStylePx(over, WIDTH,   plotWidCss);
4065
				setStylePx(over, HEIGHT,  plotHgtCss);
4066

4067
				setStylePx(wrap, WIDTH,   fullWidCss);
4068
				setStylePx(wrap, HEIGHT,  fullHgtCss);
4069

4070
				// NOTE: mutating this during print preview in Chrome forces transparent
4071
				// canvas pixels to white, even when followed up with clearRect() below
4072
				can.width  = round(fullWidCss * pxRatio);
4073
				can.height = round(fullHgtCss * pxRatio);
4074

4075

4076
				axes.forEach(a => {
4077
					let { _show, _el, _size, _pos, side } = a;
4078

4079
					if (_show) {
4080
						let posOffset = (side === 3 || side === 0 ? _size : 0);
4081
						let isVt = side % 2 == 1;
4082

4083
						setStylePx(_el, isVt ? "left"   : "top",    _pos - posOffset);
4084
						setStylePx(_el, isVt ? "width"  : "height", _size);
4085
						setStylePx(_el, isVt ? "top"    : "left",   isVt ? plotTopCss : plotLftCss);
4086
						setStylePx(_el, isVt ? "height" : "width",  isVt ? plotHgtCss : plotWidCss);
4087

4088
						_el && remClass(_el, OFF);
4089
					}
4090
					else
4091
						_el && addClass(_el, OFF);
4092
				});
4093

4094
				// invalidate ctx style cache
4095
				ctxStroke = ctxFill = ctxWidth = ctxJoin = ctxCap = ctxFont = ctxAlign = ctxBaseline = ctxDash = null;
4096
				ctxAlpha = 1;
4097

4098
				syncRect(false);
4099

4100
				fire("setSize");
4101

4102
				shouldSetSize = false;
4103
			}
4104

4105
			if (fullWidCss > 0 && fullHgtCss > 0) {
4106
				ctx.clearRect(0, 0, can.width, can.height);
4107
				fire("drawClear");
4108
				drawOrder.forEach(fn => fn());
4109
				fire("draw");
4110
			}
4111

4112
		//	if (shouldSetSelect) {
4113
			// TODO: update .u-select metrics (if visible)
4114
			//	setStylePx(selectDiv, TOP, select.top = 0);
4115
			//	setStylePx(selectDiv, LEFT, select.left = 0);
4116
			//	setStylePx(selectDiv, WIDTH, select.width = 0);
4117
			//	setStylePx(selectDiv, HEIGHT, select.height = 0);
4118
			//	shouldSetSelect = false;
4119
		//	}
4120

4121
			if (cursor.show && shouldSetCursor) {
4122
				updateCursor(null, true, false);
4123
				shouldSetCursor = false;
4124
			}
4125

4126
		//	if (FEAT_LEGEND && legend.show && legend.live && shouldSetLegend) {}
4127

4128
			if (!ready) {
4129
				ready = true;
4130
				self.status = 1;
4131

4132
				fire("ready");
4133
			}
4134

4135
			viaAutoScaleX = false;
4136

4137
			queuedCommit = false;
4138
		}
4139

4140
		self.redraw = (rebuildPaths, recalcAxes) => {
4141
			shouldConvergeSize = recalcAxes || false;
4142

4143
			if (rebuildPaths !== false)
4144
				_setScale(xScaleKey, scaleX.min, scaleX.max);
4145
			else
4146
				commit();
4147
		};
4148

4149
		// redraw() => setScale('x', scales.x.min, scales.x.max);
4150

4151
		// explicit, never re-ranged (is this actually true? for x and y)
4152
		function setScale(key, opts) {
4153
			let sc = scales[key];
4154

4155
			if (sc.from == null) {
4156
				if (dataLen == 0) {
4157
					let minMax = sc.range(self, opts.min, opts.max, key);
4158
					opts.min = minMax[0];
4159
					opts.max = minMax[1];
4160
				}
4161

4162
				if (opts.min > opts.max) {
4163
					let _min = opts.min;
4164
					opts.min = opts.max;
4165
					opts.max = _min;
4166
				}
4167

4168
				if (dataLen > 1 && opts.min != null && opts.max != null && opts.max - opts.min < 1e-16)
4169
					return;
4170

4171
				if (key == xScaleKey) {
4172
					if (sc.distr == 2 && dataLen > 0) {
4173
						opts.min = closestIdx(opts.min, data[0]);
4174
						opts.max = closestIdx(opts.max, data[0]);
4175

4176
						if (opts.min == opts.max)
4177
							opts.max++;
4178
					}
4179
				}
4180

4181
			//	log("setScale()", arguments);
4182

4183
				pendScales[key] = opts;
4184

4185
				shouldSetScales = true;
4186
				commit();
4187
			}
4188
		}
4189

4190
		self.setScale = setScale;
4191

4192
	//	INTERACTION
4193

4194
		let xCursor;
4195
		let yCursor;
4196
		let vCursor;
4197
		let hCursor;
4198

4199
		// starting position before cursor.move
4200
		let rawMouseLeft0;
4201
		let rawMouseTop0;
4202

4203
		// starting position
4204
		let mouseLeft0;
4205
		let mouseTop0;
4206

4207
		// current position before cursor.move
4208
		let rawMouseLeft1;
4209
		let rawMouseTop1;
4210

4211
		// current position
4212
		let mouseLeft1;
4213
		let mouseTop1;
4214

4215
		let dragging = false;
4216

4217
		const drag = cursor.drag;
4218

4219
		let dragX = drag.x;
4220
		let dragY = drag.y;
4221

4222
		if (cursor.show) {
4223
			if (cursor.x)
4224
				xCursor = placeDiv(CURSOR_X, over);
4225
			if (cursor.y)
4226
				yCursor = placeDiv(CURSOR_Y, over);
4227

4228
			if (scaleX.ori == 0) {
4229
				vCursor = xCursor;
4230
				hCursor = yCursor;
4231
			}
4232
			else {
4233
				vCursor = yCursor;
4234
				hCursor = xCursor;
4235
			}
4236

4237
			mouseLeft1 = cursor.left;
4238
			mouseTop1 = cursor.top;
4239
		}
4240

4241
		const select = self.select = assign({
4242
			show:   true,
4243
			over:   true,
4244
			left:   0,
4245
			width:  0,
4246
			top:    0,
4247
			height: 0,
4248
		}, opts.select);
4249

4250
		const selectDiv = select.show ? placeDiv(SELECT, select.over ? over : under) : null;
4251

4252
		function setSelect(opts, _fire) {
4253
			if (select.show) {
4254
				for (let prop in opts)
4255
					setStylePx(selectDiv, prop, select[prop] = opts[prop]);
4256

4257
				_fire !== false && fire("setSelect");
4258
			}
4259
		}
4260

4261
		self.setSelect = setSelect;
4262

4263
		function toggleDOM(i, onOff) {
4264
			let s = series[i];
4265
			let label = showLegend ? legendRows[i] : null;
4266

4267
			if (s.show)
4268
				label && remClass(label, OFF);
4269
			else {
4270
				label && addClass(label, OFF);
4271
				cursorPts.length > 1 && elTrans(cursorPts[i], -10, -10, plotWidCss, plotHgtCss);
4272
			}
4273
		}
4274

4275
		function _setScale(key, min, max) {
4276
			setScale(key, {min, max});
4277
		}
4278

4279
		function setSeries(i, opts, _fire, _pub) {
4280
		//	log("setSeries()", arguments);
4281

4282
			let s = series[i];
4283

4284
			if (opts.focus != null)
4285
				setFocus(i);
4286

4287
			if (opts.show != null) {
4288
				s.show = opts.show;
4289
				toggleDOM(i, opts.show);
4290

4291
				_setScale(mode == 2 ? s.facets[1].scale : s.scale, null, null);
4292
				commit();
4293
			}
4294

4295
			_fire !== false && fire("setSeries", i, opts);
4296

4297
			_pub && pubSync("setSeries", self, i, opts);
4298
		}
4299

4300
		self.setSeries = setSeries;
4301

4302
		function setBand(bi, opts) {
4303
			assign(bands[bi], opts);
4304
		}
4305

4306
		function addBand(opts, bi) {
4307
			opts.fill = fnOrSelf(opts.fill || null);
4308
			bi = bi == null ? bands.length : bi;
4309
			bands.splice(bi, 0, opts);
4310
		}
4311

4312
		function delBand(bi) {
4313
			if (bi == null)
4314
				bands.length = 0;
4315
			else
4316
				bands.splice(bi, 1);
4317
		}
4318

4319
		self.addBand = addBand;
4320
		self.setBand = setBand;
4321
		self.delBand = delBand;
4322

4323
		function setAlpha(i, value) {
4324
			series[i].alpha = value;
4325

4326
			if (cursor.show && cursorPts[i])
4327
				cursorPts[i].style.opacity = value;
4328

4329
			if (showLegend && legendRows[i])
4330
				legendRows[i].style.opacity = value;
4331
		}
4332

4333
		// y-distance
4334
		let closestDist;
4335
		let closestSeries;
4336
		let focusedSeries;
4337
		const FOCUS_TRUE  = {focus: true};
4338
		const FOCUS_FALSE = {focus: false};
4339

4340
		function setFocus(i) {
4341
			if (i != focusedSeries) {
4342
			//	log("setFocus()", arguments);
4343

4344
				let allFocused = i == null;
4345

4346
				let _setAlpha = focus.alpha != 1;
4347

4348
				series.forEach((s, i2) => {
4349
					let isFocused = allFocused || i2 == 0 || i2 == i;
4350
					s._focus = allFocused ? null : isFocused;
4351
					_setAlpha && setAlpha(i2, isFocused ? 1 : focus.alpha);
4352
				});
4353

4354
				focusedSeries = i;
4355
				_setAlpha && commit();
4356
			}
4357
		}
4358

4359
		if (showLegend && cursorFocus) {
4360
			on(mouseleave, legendEl, e => {
4361
				if (cursor._lock)
4362
					return;
4363
				setSeries(null, FOCUS_FALSE, true, syncOpts.setSeries);
4364
				updateCursor(null, true, false);
4365
			});
4366
		}
4367

4368
		function posToVal(pos, scale, can) {
4369
			let sc = scales[scale];
4370

4371
			if (can)
4372
				pos = pos / pxRatio - (sc.ori == 1 ? plotTopCss : plotLftCss);
4373

4374
			let dim = plotWidCss;
4375

4376
			if (sc.ori == 1) {
4377
				dim = plotHgtCss;
4378
				pos = dim - pos;
4379
			}
4380

4381
			if (sc.dir == -1)
4382
				pos = dim - pos;
4383

4384
			let _min = sc._min,
4385
				_max = sc._max,
4386
				pct = pos / dim;
4387

4388
			let sv = _min + (_max - _min) * pct;
4389

4390
			let distr = sc.distr;
4391

4392
			return (
4393
				distr == 3 ? pow(10, sv) :
4394
				distr == 4 ? sinh(sv, sc.asinh) :
4395
				sv
4396
			);
4397
		}
4398

4399
		function closestIdxFromXpos(pos, can) {
4400
			let v = posToVal(pos, xScaleKey, can);
4401
			return closestIdx(v, data[0], i0, i1);
4402
		}
4403

4404
		self.valToIdx = val => closestIdx(val, data[0]);
4405
		self.posToIdx = closestIdxFromXpos;
4406
		self.posToVal = posToVal;
4407
		self.valToPos = (val, scale, can) => (
4408
			scales[scale].ori == 0 ?
4409
			getHPos(val, scales[scale],
4410
				can ? plotWid : plotWidCss,
4411
				can ? plotLft : 0,
4412
			) :
4413
			getVPos(val, scales[scale],
4414
				can ? plotHgt : plotHgtCss,
4415
				can ? plotTop : 0,
4416
			)
4417
		);
4418

4419
		// defers calling expensive functions
4420
		function batch(fn) {
4421
			fn(self);
4422
			commit();
4423
		}
4424

4425
		self.batch = batch;
4426

4427
		(self.setCursor = (opts, _fire, _pub) => {
4428
			mouseLeft1 = opts.left;
4429
			mouseTop1 = opts.top;
4430
		//	assign(cursor, opts);
4431
			updateCursor(null, _fire, _pub);
4432
		});
4433

4434
		function setSelH(off, dim) {
4435
			setStylePx(selectDiv, LEFT,  select.left = off);
4436
			setStylePx(selectDiv, WIDTH, select.width = dim);
4437
		}
4438

4439
		function setSelV(off, dim) {
4440
			setStylePx(selectDiv, TOP,    select.top = off);
4441
			setStylePx(selectDiv, HEIGHT, select.height = dim);
4442
		}
4443

4444
		let setSelX = scaleX.ori == 0 ? setSelH : setSelV;
4445
		let setSelY = scaleX.ori == 1 ? setSelH : setSelV;
4446

4447
		function syncLegend() {
4448
			if (showLegend && legend.live) {
4449
				for (let i = mode == 2 ? 1 : 0; i < series.length; i++) {
4450
					if (i == 0 && multiValLegend)
4451
						continue;
4452

4453
					let vals = legend.values[i];
4454

4455
					let j = 0;
4456

4457
					for (let k in vals)
4458
						legendCells[i][j++].firstChild.nodeValue = vals[k];
4459
				}
4460
			}
4461
		}
4462

4463
		function setLegend(opts, _fire) {
4464
			if (opts != null) {
4465
				let idx = opts.idx;
4466

4467
				legend.idx = idx;
4468
				series.forEach((s, sidx) => {
4469
					(sidx > 0 || !multiValLegend) && setLegendValues(sidx, idx);
4470
				});
4471
			}
4472

4473
			if (showLegend && legend.live)
4474
				syncLegend();
4475

4476
			shouldSetLegend = false;
4477

4478
			_fire !== false && fire("setLegend");
4479
		}
4480

4481
		self.setLegend = setLegend;
4482

4483
		function setLegendValues(sidx, idx) {
4484
			let val;
4485

4486
			if (idx == null)
4487
				val = NULL_LEGEND_VALUES;
4488
			else {
4489
				let s = series[sidx];
4490
				let src = sidx == 0 && xScaleDistr == 2 ? data0 : data[sidx];
4491
				val = multiValLegend ? s.values(self, sidx, idx) : {_: s.value(self, src[idx], sidx, idx)};
4492
			}
4493

4494
			legend.values[sidx] = val;
4495
		}
4496

4497
		function updateCursor(src, _fire, _pub) {
4498
		//	ts == null && log("updateCursor()", arguments);
4499

4500
			rawMouseLeft1 = mouseLeft1;
4501
			rawMouseTop1 = mouseTop1;
4502

4503
			[mouseLeft1, mouseTop1] = cursor.move(self, mouseLeft1, mouseTop1);
4504

4505
			if (cursor.show) {
4506
				vCursor && elTrans(vCursor, round(mouseLeft1), 0, plotWidCss, plotHgtCss);
4507
				hCursor && elTrans(hCursor, 0, round(mouseTop1), plotWidCss, plotHgtCss);
4508
			}
4509

4510
			let idx;
4511

4512
			// when zooming to an x scale range between datapoints the binary search
4513
			// for nearest min/max indices results in this condition. cheap hack :D
4514
			let noDataInRange = i0 > i1; // works for mode 1 only
4515

4516
			closestDist = inf;
4517

4518
			// TODO: extract
4519
			let xDim = scaleX.ori == 0 ? plotWidCss : plotHgtCss;
4520
			let yDim = scaleX.ori == 1 ? plotWidCss : plotHgtCss;
4521

4522
			// if cursor hidden, hide points & clear legend vals
4523
			if (mouseLeft1 < 0 || dataLen == 0 || noDataInRange) {
4524
				idx = null;
4525

4526
				for (let i = 0; i < series.length; i++) {
4527
					if (i > 0) {
4528
						cursorPts.length > 1 && elTrans(cursorPts[i], -10, -10, plotWidCss, plotHgtCss);
4529
					}
4530
				}
4531

4532
				if (cursorFocus)
4533
					setSeries(null, FOCUS_TRUE, true, src == null && syncOpts.setSeries);
4534

4535
				if (legend.live) {
4536
					activeIdxs.fill(null);
4537
					shouldSetLegend = true;
4538

4539
					for (let i = 0; i < series.length; i++)
4540
						legend.values[i] = NULL_LEGEND_VALUES;
4541
				}
4542
			}
4543
			else {
4544
			//	let pctY = 1 - (y / rect.height);
4545

4546
				let mouseXPos, valAtPosX, xPos;
4547

4548
				if (mode == 1) {
4549
					mouseXPos = scaleX.ori == 0 ? mouseLeft1 : mouseTop1;
4550
					valAtPosX = posToVal(mouseXPos, xScaleKey);
4551
					idx = closestIdx(valAtPosX, data[0], i0, i1);
4552
					xPos = incrRoundUp(valToPosX(data[0][idx], scaleX, xDim, 0), 0.5);
4553
				}
4554

4555
				for (let i = mode == 2 ? 1 : 0; i < series.length; i++) {
4556
					let s = series[i];
4557

4558
					let idx1  = activeIdxs[i];
4559
					let yVal1 = mode == 1 ? data[i][idx1] : data[i][1][idx1];
4560

4561
					let idx2  = cursor.dataIdx(self, i, idx, valAtPosX);
4562
					let yVal2 = mode == 1 ? data[i][idx2] : data[i][1][idx2];
4563

4564
					shouldSetLegend = shouldSetLegend || yVal2 != yVal1 || idx2 != idx1;
4565

4566
					activeIdxs[i] = idx2;
4567

4568
					let xPos2 = idx2 == idx ? xPos : incrRoundUp(valToPosX(mode == 1 ? data[0][idx2] : data[i][0][idx2], scaleX, xDim, 0), 0.5);
4569

4570
					if (i > 0 && s.show) {
4571
						let yPos = yVal2 == null ? -10 : incrRoundUp(valToPosY(yVal2, mode == 1 ? scales[s.scale] : scales[s.facets[1].scale], yDim, 0), 0.5);
4572

4573
						if (yPos > 0 && mode == 1) {
4574
							let dist = abs(yPos - mouseTop1);
4575

4576
							if (dist <= closestDist) {
4577
								closestDist = dist;
4578
								closestSeries = i;
4579
							}
4580
						}
4581

4582
						let hPos, vPos;
4583

4584
						if (scaleX.ori == 0) {
4585
							hPos = xPos2;
4586
							vPos = yPos;
4587
						}
4588
						else {
4589
							hPos = yPos;
4590
							vPos = xPos2;
4591
						}
4592

4593
						if (shouldSetLegend && cursorPts.length > 1) {
4594
							elColor(cursorPts[i], cursor.points.fill(self, i), cursor.points.stroke(self, i));
4595

4596
							let ptWid, ptHgt, ptLft, ptTop,
4597
								centered = true,
4598
								getBBox = cursor.points.bbox;
4599

4600
							if (getBBox != null) {
4601
								centered = false;
4602

4603
								let bbox = getBBox(self, i);
4604

4605
								ptLft = bbox.left;
4606
								ptTop = bbox.top;
4607
								ptWid = bbox.width;
4608
								ptHgt = bbox.height;
4609
							}
4610
							else {
4611
								ptLft = hPos;
4612
								ptTop = vPos;
4613
								ptWid = ptHgt = cursor.points.size(self, i);
4614
							}
4615

4616
							elSize(cursorPts[i], ptWid, ptHgt, centered);
4617
							elTrans(cursorPts[i], ptLft, ptTop, plotWidCss, plotHgtCss);
4618
						}
4619
					}
4620

4621
					if (legend.live) {
4622
						if (!shouldSetLegend || i == 0 && multiValLegend)
4623
							continue;
4624

4625
						setLegendValues(i, idx2);
4626
					}
4627
				}
4628
			}
4629

4630
			cursor.idx = idx;
4631
			cursor.left = mouseLeft1;
4632
			cursor.top = mouseTop1;
4633

4634
			if (shouldSetLegend) {
4635
				legend.idx = idx;
4636
				setLegend();
4637
			}
4638

4639
			// nit: cursor.drag.setSelect is assumed always true
4640
			if (select.show && dragging) {
4641
				if (src != null) {
4642
					let [xKey, yKey] = syncOpts.scales;
4643
					let [matchXKeys, matchYKeys] = syncOpts.match;
4644
					let [xKeySrc, yKeySrc] = src.cursor.sync.scales;
4645

4646
					// match the dragX/dragY implicitness/explicitness of src
4647
					let sdrag = src.cursor.drag;
4648
					dragX = sdrag._x;
4649
					dragY = sdrag._y;
4650

4651
					let { left, top, width, height } = src.select;
4652

4653
					let sori = src.scales[xKey].ori;
4654
					let sPosToVal = src.posToVal;
4655

4656
					let sOff, sDim, sc, a, b;
4657

4658
					let matchingX = xKey != null && matchXKeys(xKey, xKeySrc);
4659
					let matchingY = yKey != null && matchYKeys(yKey, yKeySrc);
4660

4661
					if (matchingX) {
4662
						if (sori == 0) {
4663
							sOff = left;
4664
							sDim = width;
4665
						}
4666
						else {
4667
							sOff = top;
4668
							sDim = height;
4669
						}
4670

4671
						if (dragX) {
4672
							sc = scales[xKey];
4673

4674
							a = valToPosX(sPosToVal(sOff, xKeySrc),        sc, xDim, 0);
4675
							b = valToPosX(sPosToVal(sOff + sDim, xKeySrc), sc, xDim, 0);
4676

4677
							setSelX(min(a,b), abs(b-a));
4678
						}
4679
						else
4680
							setSelX(0, xDim);
4681

4682
						if (!matchingY)
4683
							setSelY(0, yDim);
4684
					}
4685

4686
					if (matchingY) {
4687
						if (sori == 1) {
4688
							sOff = left;
4689
							sDim = width;
4690
						}
4691
						else {
4692
							sOff = top;
4693
							sDim = height;
4694
						}
4695

4696
						if (dragY) {
4697
							sc = scales[yKey];
4698

4699
							a = valToPosY(sPosToVal(sOff, yKeySrc),        sc, yDim, 0);
4700
							b = valToPosY(sPosToVal(sOff + sDim, yKeySrc), sc, yDim, 0);
4701

4702
							setSelY(min(a,b), abs(b-a));
4703
						}
4704
						else
4705
							setSelY(0, yDim);
4706

4707
						if (!matchingX)
4708
							setSelX(0, xDim);
4709
					}
4710
				}
4711
				else {
4712
					let rawDX = abs(rawMouseLeft1 - rawMouseLeft0);
4713
					let rawDY = abs(rawMouseTop1 - rawMouseTop0);
4714

4715
					if (scaleX.ori == 1) {
4716
						let _rawDX = rawDX;
4717
						rawDX = rawDY;
4718
						rawDY = _rawDX;
4719
					}
4720

4721
					dragX = drag.x && rawDX >= drag.dist;
4722
					dragY = drag.y && rawDY >= drag.dist;
4723

4724
					let uni = drag.uni;
4725

4726
					if (uni != null) {
4727
						// only calc drag status if they pass the dist thresh
4728
						if (dragX && dragY) {
4729
							dragX = rawDX >= uni;
4730
							dragY = rawDY >= uni;
4731

4732
							// force unidirectionality when both are under uni limit
4733
							if (!dragX && !dragY) {
4734
								if (rawDY > rawDX)
4735
									dragY = true;
4736
								else
4737
									dragX = true;
4738
							}
4739
						}
4740
					}
4741
					else if (drag.x && drag.y && (dragX || dragY))
4742
						// if omni with no uni then both dragX / dragY should be true if either is true
4743
						dragX = dragY = true;
4744

4745
					let p0, p1;
4746

4747
					if (dragX) {
4748
						if (scaleX.ori == 0) {
4749
							p0 = mouseLeft0;
4750
							p1 = mouseLeft1;
4751
						}
4752
						else {
4753
							p0 = mouseTop0;
4754
							p1 = mouseTop1;
4755
						}
4756

4757
						setSelX(min(p0, p1), abs(p1 - p0));
4758

4759
						if (!dragY)
4760
							setSelY(0, yDim);
4761
					}
4762

4763
					if (dragY) {
4764
						if (scaleX.ori == 1) {
4765
							p0 = mouseLeft0;
4766
							p1 = mouseLeft1;
4767
						}
4768
						else {
4769
							p0 = mouseTop0;
4770
							p1 = mouseTop1;
4771
						}
4772

4773
						setSelY(min(p0, p1), abs(p1 - p0));
4774

4775
						if (!dragX)
4776
							setSelX(0, xDim);
4777
					}
4778

4779
					// the drag didn't pass the dist requirement
4780
					if (!dragX && !dragY) {
4781
						setSelX(0, 0);
4782
						setSelY(0, 0);
4783
					}
4784
				}
4785
			}
4786

4787
			drag._x = dragX;
4788
			drag._y = dragY;
4789

4790
			if (src == null) {
4791
				if (_pub) {
4792
					if (syncKey != null) {
4793
						let [xSyncKey, ySyncKey] = syncOpts.scales;
4794

4795
						syncOpts.values[0] = xSyncKey != null ? posToVal(scaleX.ori == 0 ? mouseLeft1 : mouseTop1, xSyncKey) : null;
4796
						syncOpts.values[1] = ySyncKey != null ? posToVal(scaleX.ori == 1 ? mouseLeft1 : mouseTop1, ySyncKey) : null;
4797
					}
4798

4799
					pubSync(mousemove, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, idx);
4800
				}
4801

4802
				if (cursorFocus) {
4803
					let shouldPub = _pub && syncOpts.setSeries;
4804
					let p = focus.prox;
4805

4806
					if (focusedSeries == null) {
4807
						if (closestDist <= p)
4808
							setSeries(closestSeries, FOCUS_TRUE, true, shouldPub);
4809
					}
4810
					else {
4811
						if (closestDist > p)
4812
							setSeries(null, FOCUS_TRUE, true, shouldPub);
4813
						else if (closestSeries != focusedSeries)
4814
							setSeries(closestSeries, FOCUS_TRUE, true, shouldPub);
4815
					}
4816
				}
4817
			}
4818

4819
			ready && _fire !== false && fire("setCursor");
4820
		}
4821

4822
		let rect = null;
4823

4824
		function syncRect(defer) {
4825
			if (defer === true)
4826
				rect = null;
4827
			else {
4828
				rect = over.getBoundingClientRect();
4829
				fire("syncRect", rect);
4830
			}
4831
		}
4832

4833
		function mouseMove(e, src, _l, _t, _w, _h, _i) {
4834
			if (cursor._lock)
4835
				return;
4836

4837
			cacheMouse(e, src, _l, _t, _w, _h, _i, false, e != null);
4838

4839
			if (e != null)
4840
				updateCursor(null, true, true);
4841
			else
4842
				updateCursor(src, true, false);
4843
		}
4844

4845
		function cacheMouse(e, src, _l, _t, _w, _h, _i, initial, snap) {
4846
			if (rect == null)
4847
				syncRect(false);
4848

4849
			if (e != null) {
4850
				_l = e.clientX - rect.left;
4851
				_t = e.clientY - rect.top;
4852
			}
4853
			else {
4854
				if (_l < 0 || _t < 0) {
4855
					mouseLeft1 = -10;
4856
					mouseTop1 = -10;
4857
					return;
4858
				}
4859

4860
				let [xKey, yKey] = syncOpts.scales;
4861

4862
				let syncOptsSrc = src.cursor.sync;
4863
				let [xValSrc, yValSrc] = syncOptsSrc.values;
4864
				let [xKeySrc, yKeySrc] = syncOptsSrc.scales;
4865
				let [matchXKeys, matchYKeys] = syncOpts.match;
4866

4867
				let rotSrc = src.scales[xKeySrc].ori == 1;
4868

4869
				let xDim = scaleX.ori == 0 ? plotWidCss : plotHgtCss,
4870
					yDim = scaleX.ori == 1 ? plotWidCss : plotHgtCss,
4871
					_xDim = rotSrc ? _h : _w,
4872
					_yDim = rotSrc ? _w : _h,
4873
					_xPos = rotSrc ? _t : _l,
4874
					_yPos = rotSrc ? _l : _t;
4875

4876
				if (xKeySrc != null)
4877
					_l = matchXKeys(xKey, xKeySrc) ? getPos(xValSrc, scales[xKey], xDim, 0) : -10;
4878
				else
4879
					_l = xDim * (_xPos/_xDim);
4880

4881
				if (yKeySrc != null)
4882
					_t = matchYKeys(yKey, yKeySrc) ? getPos(yValSrc, scales[yKey], yDim, 0) : -10;
4883
				else
4884
					_t = yDim * (_yPos/_yDim);
4885

4886
				if (scaleX.ori == 1) {
4887
					let __l = _l;
4888
					_l = _t;
4889
					_t = __l;
4890
				}
4891
			}
4892

4893
			if (snap) {
4894
				if (_l <= 1 || _l >= plotWidCss - 1)
4895
					_l = incrRound(_l, plotWidCss);
4896

4897
				if (_t <= 1 || _t >= plotHgtCss - 1)
4898
					_t = incrRound(_t, plotHgtCss);
4899
			}
4900

4901
			if (initial) {
4902
				rawMouseLeft0 = _l;
4903
				rawMouseTop0 = _t;
4904

4905
				[mouseLeft0, mouseTop0] = cursor.move(self, _l, _t);
4906
			}
4907
			else {
4908
				mouseLeft1 = _l;
4909
				mouseTop1 = _t;
4910
			}
4911
		}
4912

4913
		function hideSelect() {
4914
			setSelect({
4915
				width: 0,
4916
				height: 0,
4917
			}, false);
4918
		}
4919

4920
		function mouseDown(e, src, _l, _t, _w, _h, _i) {
4921
			dragging = true;
4922
			dragX = dragY = drag._x = drag._y = false;
4923

4924
			cacheMouse(e, src, _l, _t, _w, _h, _i, true, false);
4925

4926
			if (e != null) {
4927
				onMouse(mouseup, doc, mouseUp);
4928
				pubSync(mousedown, self, mouseLeft0, mouseTop0, plotWidCss, plotHgtCss, null);
4929
			}
4930
		}
4931

4932
		function mouseUp(e, src, _l, _t, _w, _h, _i) {
4933
			dragging = drag._x = drag._y = false;
4934

4935
			cacheMouse(e, src, _l, _t, _w, _h, _i, false, true);
4936

4937
			let { left, top, width, height } = select;
4938

4939
			let hasSelect = width > 0 || height > 0;
4940

4941
			hasSelect && setSelect(select);
4942

4943
			if (drag.setScale && hasSelect) {
4944
			//	if (syncKey != null) {
4945
			//		dragX = drag.x;
4946
			//		dragY = drag.y;
4947
			//	}
4948

4949
				let xOff = left,
4950
					xDim = width,
4951
					yOff = top,
4952
					yDim = height;
4953

4954
				if (scaleX.ori == 1) {
4955
					xOff = top,
4956
					xDim = height,
4957
					yOff = left,
4958
					yDim = width;
4959
				}
4960

4961
				if (dragX) {
4962
					_setScale(xScaleKey,
4963
						posToVal(xOff, xScaleKey),
4964
						posToVal(xOff + xDim, xScaleKey)
4965
					);
4966
				}
4967

4968
				if (dragY) {
4969
					for (let k in scales) {
4970
						let sc = scales[k];
4971

4972
						if (k != xScaleKey && sc.from == null && sc.min != inf) {
4973
							_setScale(k,
4974
								posToVal(yOff + yDim, k),
4975
								posToVal(yOff, k)
4976
							);
4977
						}
4978
					}
4979
				}
4980

4981
				hideSelect();
4982
			}
4983
			else if (cursor.lock) {
4984
				cursor._lock = !cursor._lock;
4985

4986
				if (!cursor._lock)
4987
					updateCursor(null, true, false);
4988
			}
4989

4990
			if (e != null) {
4991
				offMouse(mouseup, doc);
4992
				pubSync(mouseup, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null);
4993
			}
4994
		}
4995

4996
		function mouseLeave(e, src, _l, _t, _w, _h, _i) {
4997
			if (!cursor._lock) {
4998
				let _dragging = dragging;
4999

5000
				if (dragging) {
5001
					// handle case when mousemove aren't fired all the way to edges by browser
5002
					let snapH = true;
5003
					let snapV = true;
5004
					let snapProx = 10;
5005

5006
					let dragH, dragV;
5007

5008
					if (scaleX.ori == 0) {
5009
						dragH = dragX;
5010
						dragV = dragY;
5011
					}
5012
					else {
5013
						dragH = dragY;
5014
						dragV = dragX;
5015
					}
5016

5017
					if (dragH && dragV) {
5018
						// maybe omni corner snap
5019
						snapH = mouseLeft1 <= snapProx || mouseLeft1 >= plotWidCss - snapProx;
5020
						snapV = mouseTop1  <= snapProx || mouseTop1  >= plotHgtCss - snapProx;
5021
					}
5022

5023
					if (dragH && snapH)
5024
						mouseLeft1 = mouseLeft1 < mouseLeft0 ? 0 : plotWidCss;
5025

5026
					if (dragV && snapV)
5027
						mouseTop1 = mouseTop1 < mouseTop0 ? 0 : plotHgtCss;
5028

5029
					updateCursor(null, true, true);
5030

5031
					dragging = false;
5032
				}
5033

5034
				mouseLeft1 = -10;
5035
				mouseTop1 = -10;
5036

5037
				// passing a non-null timestamp to force sync/mousemove event
5038
				updateCursor(null, true, true);
5039

5040
				if (_dragging)
5041
					dragging = _dragging;
5042
			}
5043
		}
5044

5045
		function dblClick(e, src, _l, _t, _w, _h, _i) {
5046
			autoScaleX();
5047

5048
			hideSelect();
5049

5050
			if (e != null)
5051
				pubSync(dblclick, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null);
5052
		}
5053

5054
		function syncPxRatio() {
5055
			axes.forEach(syncFontSize);
5056
			_setSize(self.width, self.height, true);
5057
		}
5058

5059
		on(dppxchange, win, syncPxRatio);
5060

5061
		// internal pub/sub
5062
		const events = {};
5063

5064
		events.mousedown = mouseDown;
5065
		events.mousemove = mouseMove;
5066
		events.mouseup = mouseUp;
5067
		events.dblclick = dblClick;
5068
		events["setSeries"] = (e, src, idx, opts) => {
5069
			setSeries(idx, opts, true, false);
5070
		};
5071

5072
		if (cursor.show) {
5073
			onMouse(mousedown,  over, mouseDown);
5074
			onMouse(mousemove,  over, mouseMove);
5075
			onMouse(mouseenter, over, syncRect);
5076
			onMouse(mouseleave, over, mouseLeave);
5077

5078
			onMouse(dblclick, over, dblClick);
5079

5080
			cursorPlots.add(self);
5081

5082
			self.syncRect = syncRect;
5083
		}
5084

5085
		// external on/off
5086
		const hooks = self.hooks = opts.hooks || {};
5087

5088
		function fire(evName, a1, a2) {
5089
			if (evName in hooks) {
5090
				hooks[evName].forEach(fn => {
5091
					fn.call(null, self, a1, a2);
5092
				});
5093
			}
5094
		}
5095

5096
		(opts.plugins || []).forEach(p => {
5097
			for (let evName in p.hooks)
5098
				hooks[evName] = (hooks[evName] || []).concat(p.hooks[evName]);
5099
		});
5100

5101
		const syncOpts = assign({
5102
			key: null,
5103
			setSeries: false,
5104
			filters: {
5105
				pub: retTrue,
5106
				sub: retTrue,
5107
			},
5108
			scales: [xScaleKey, series[1] ? series[1].scale : null],
5109
			match: [retEq, retEq],
5110
			values: [null, null],
5111
		}, cursor.sync);
5112

5113
		(cursor.sync = syncOpts);
5114

5115
		const syncKey = syncOpts.key;
5116

5117
		const sync = _sync(syncKey);
5118

5119
		function pubSync(type, src, x, y, w, h, i) {
5120
			if (syncOpts.filters.pub(type, src, x, y, w, h, i))
5121
				sync.pub(type, src, x, y, w, h, i);
5122
		}
5123

5124
		sync.sub(self);
5125

5126
		function pub(type, src, x, y, w, h, i) {
5127
			if (syncOpts.filters.sub(type, src, x, y, w, h, i))
5128
				events[type](null, src, x, y, w, h, i);
5129
		}
5130

5131
		(self.pub = pub);
5132

5133
		function destroy() {
5134
			sync.unsub(self);
5135
			cursorPlots.delete(self);
5136
			mouseListeners.clear();
5137
			off(dppxchange, win, syncPxRatio);
5138
			root.remove();
5139
			fire("destroy");
5140
		}
5141

5142
		self.destroy = destroy;
5143

5144
		function _init() {
5145
			fire("init", opts, data);
5146

5147
			setData(data || opts.data, false);
5148

5149
			if (pendScales[xScaleKey])
5150
				setScale(xScaleKey, pendScales[xScaleKey]);
5151
			else
5152
				autoScaleX();
5153

5154
			_setSize(opts.width, opts.height);
5155

5156
			updateCursor(null, true, false);
5157

5158
			setSelect(select, false);
5159
		}
5160

5161
		series.forEach(initSeries);
5162

5163
		axes.forEach(initAxis);
5164

5165
		if (then) {
5166
			if (then instanceof HTMLElement) {
5167
				then.appendChild(root);
5168
				_init();
5169
			}
5170
			else
5171
				then(self, _init);
5172
		}
5173
		else
5174
			_init();
5175

5176
		return self;
5177
	}
5178

5179
	uPlot.assign = assign;
5180
	uPlot.fmtNum = fmtNum;
5181
	uPlot.rangeNum = rangeNum;
5182
	uPlot.rangeLog = rangeLog;
5183
	uPlot.rangeAsinh = rangeAsinh;
5184
	uPlot.orient   = orient;
5185

5186
	{
5187
		uPlot.join = join;
5188
	}
5189

5190
	{
5191
		uPlot.fmtDate = fmtDate;
5192
		uPlot.tzDate  = tzDate;
5193
	}
5194

5195
	{
5196
		uPlot.sync = _sync;
5197
	}
5198

5199
	{
5200
		uPlot.addGap = addGap;
5201
		uPlot.clipGaps = clipGaps;
5202

5203
		let paths = uPlot.paths = {
5204
			points,
5205
		};
5206

5207
		(paths.linear  = linear);
5208
		(paths.stepped = stepped);
5209
		(paths.bars    = bars);
5210
		(paths.spline  = monotoneCubic);
5211
	}
5212

5213
	return uPlot;
5214

5215
})();
5216

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

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

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

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