GPQAPP

Форк
0
/
uPlot.esm.js 
5210 строк · 118.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
const FEAT_TIME          = true;
11

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

19
	while (hi - lo > 1) {
20
		mid = bitwise ? (lo + hi) >> 1 : floor((lo + hi) / 2);
21

22
		if (arr[mid] < num)
23
			lo = mid;
24
		else
25
			hi = mid;
26
	}
27

28
	if (num - arr[lo] <= arr[hi] - num)
29
		return lo;
30

31
	return hi;
32
}
33

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

40
	return -1;
41
}
42

43
function getMinMax(data, _i0, _i1, sorted) {
44
//	console.log("getMinMax()");
45

46
	let _min = inf;
47
	let _max = -inf;
48

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

66
	return [_min, _max];
67
}
68

69
function getMinMaxLog(data, _i0, _i1) {
70
//	console.log("getMinMax()");
71

72
	let _min = inf;
73
	let _max = -inf;
74

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

82
	return [
83
		_min ==  inf ?  1 : _min,
84
		_max == -inf ? 10 : _max,
85
	];
86
}
87

88
const _fixedTuple = [0, 0];
89

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

96
function rangeLog(min, max, base, fullMags) {
97
	let minSign = sign(min);
98

99
	let logFn = base == 10 ? log10 : log2;
100

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

112
	let minExp, maxExp, minMaxIncrs;
113

114
	if (fullMags) {
115
		minExp = floor(logFn(min));
116
		maxExp =  ceil(logFn(max));
117

118
		minMaxIncrs = fixIncr(pow(base, minExp), pow(base, maxExp), minExp, maxExp);
119

120
		min = minMaxIncrs[0];
121
		max = minMaxIncrs[1];
122
	}
123
	else {
124
		minExp = floor(logFn(abs(min)));
125
		maxExp = floor(logFn(abs(max)));
126

127
		minMaxIncrs = fixIncr(pow(base, minExp), pow(base, maxExp), minExp, maxExp);
128

129
		min = incrRoundDn(min, minMaxIncrs[0]);
130
		max = incrRoundUp(max, minMaxIncrs[1]);
131
	}
132

133
	return [min, max];
134
}
135

136
function rangeAsinh(min, max, base, fullMags) {
137
	let minMax = rangeLog(min, max, base, fullMags);
138

139
	if (min == 0)
140
		minMax[0] = 0;
141

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

145
	return minMax;
146
}
147

148
const rangePad = 0.1;
149

150
const autoRangePart = {
151
	mode: 3,
152
	pad: rangePad,
153
};
154

155
const _eqRangePart = {
156
	pad:  0,
157
	soft: null,
158
	mode: 0,
159
};
160

161
const _eqRange = {
162
	min: _eqRangePart,
163
	max: _eqRangePart,
164
};
165

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

172
	_eqRangePart.pad  = mult;
173
	_eqRangePart.soft = extra ? 0 : null;
174
	_eqRangePart.mode = extra ? 3 : 0;
175

176
	return _rangeNum(_min, _max, _eqRange);
177
}
178

179
// nullish coalesce
180
function ifNull(lh, rh) {
181
	return lh == null ? rh : lh;
182
}
183

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

190
	while (idx0 <= idx1) {
191
		if (data[idx0] != null)
192
			return true;
193
		idx0++;
194
	}
195

196
	return false;
197
}
198

199
function _rangeNum(_min, _max, cfg) {
200
	let cmin = cfg.min;
201
	let cmax = cfg.max;
202

203
	let padMin = ifNull(cmin.pad, 0);
204
	let padMax = ifNull(cmax.pad, 0);
205

206
	let hardMin = ifNull(cmin.hard, -inf);
207
	let hardMax = ifNull(cmax.hard,  inf);
208

209
	let softMin = ifNull(cmin.soft,  inf);
210
	let softMax = ifNull(cmax.soft, -inf);
211

212
	let softMinMode = ifNull(cmin.mode, 0);
213
	let softMaxMode = ifNull(cmax.mode, 0);
214

215
	let delta        = _max - _min;
216

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

222
	// treat data as flat if delta is less than 1 billionth
223
	if (delta < 1e-9) {
224
		delta = 0;
225

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

231
			if (softMinMode == 2 && softMin != inf)
232
				padMin = 0;
233

234
			if (softMaxMode == 2 && softMax != -inf)
235
				padMax = 0;
236
		}
237
	}
238

239
	let nonZeroDelta = delta || abs(_max) || 1e3;
240
	let mag          = log10(nonZeroDelta);
241
	let base         = pow(10, floor(mag));
242

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

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

253
	if (minLim == maxLim && minLim == 0)
254
		maxLim = 100;
255

256
	return [minLim, maxLim];
257
}
258

259
// alternative: https://stackoverflow.com/a/2254896
260
const fmtNum = new Intl.NumberFormat(navigator.language).format;
261

262
const M = Math;
263

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

279
const inf = Infinity;
280

281
function numIntDigits(x) {
282
	return (log10((x ^ (x >> 31)) - (x >> 31)) | 0) + 1;
283
}
284

285
function incrRound(num, incr) {
286
	return round(num/incr)*incr;
287
}
288

289
function clamp(num, _min, _max) {
290
	return min(max(num, _min), _max);
291
}
292

293
function fnOrSelf(v) {
294
	return typeof v == "function" ? v : () => v;
295
}
296

297
const retArg0 = _0 => _0;
298

299
const retArg1 = (_0, _1) => _1;
300

301
const retNull = _ => null;
302

303
const retTrue = _ => true;
304

305
const retEq = (a, b) => a == b;
306

307
function incrRoundUp(num, incr) {
308
	return ceil(num/incr)*incr;
309
}
310

311
function incrRoundDn(num, incr) {
312
	return floor(num/incr)*incr;
313
}
314

315
function roundDec(val, dec) {
316
	return round(val * (dec = 10**dec)) / dec;
317
}
318

319
const fixedDec = new Map();
320

321
function guessDec(num) {
322
	return ((""+num).split(".")[1] || "").length;
323
}
324

325
function genIncrs(base, minExp, maxExp, mults) {
326
	let incrs = [];
327

328
	let multDec = mults.map(guessDec);
329

330
	for (let exp = minExp; exp < maxExp; exp++) {
331
		let expa = abs(exp);
332
		let mag = roundDec(pow(base, exp), expa);
333

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

343
	return incrs;
344
}
345

346
//export const assign = Object.assign;
347

348
const EMPTY_OBJ = {};
349
const EMPTY_ARR = [];
350

351
const nullNullTuple = [null, null];
352

353
const isArr = Array.isArray;
354

355
function isStr(v) {
356
	return typeof v == 'string';
357
}
358

359
function isObj(v) {
360
	let is = false;
361

362
	if (v != null) {
363
		let c = v.constructor;
364
		is = c == null || c == Object;
365
	}
366

367
	return is;
368
}
369

370
function fastIsObj(v) {
371
	return v != null && typeof v == 'object';
372
}
373

374
function copy(o, _isObj = isObj) {
375
	let out;
376

377
	if (isArr(o)) {
378
		let val = o.find(v => v != null);
379

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

396
	return out;
397
}
398

399
function assign(targ) {
400
	let args = arguments;
401

402
	for (let i = 1; i < args.length; i++) {
403
		let src = args[i];
404

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

413
	return targ;
414
}
415

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

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

426
		if (nullIdx > lastNullIdx) {
427
			xi = nullIdx - 1;
428
			while (xi >= 0 && yVals[xi] == null)
429
				yVals[xi--] = null;
430

431
			xi = nullIdx + 1;
432
			while (xi < alignedLen && yVals[xi] == null)
433
				yVals[lastNullIdx = xi++] = null;
434
		}
435
	}
436
}
437

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

443
	for (let ti = 0; ti < tables.length; ti++) {
444
		let t = tables[ti];
445
		let xs = t[0];
446
		let len = xs.length;
447

448
		for (let i = 0; i < len; i++)
449
			xVals.add(xs[i]);
450
	}
451

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

454
	let alignedLen = data[0].length;
455

456
	let xIdxs = new Map();
457

458
	for (let i = 0; i < alignedLen; i++)
459
		xIdxs.set(data[0][i], i);
460

461
	for (let ti = 0; ti < tables.length; ti++) {
462
		let t = tables[ti];
463
		let xs = t[0];
464

465
		for (let si = 1; si < t.length; si++) {
466
			let ys = t[si];
467

468
			let yVals = Array(alignedLen).fill(undefined);
469

470
			let nullMode = nullModes ? nullModes[ti][si] : NULL_RETAIN;
471

472
			let nullIdxs = [];
473

474
			for (let i = 0; i < ys.length; i++) {
475
				let yVal = ys[i];
476
				let alignedIdx = xIdxs.get(xs[i]);
477

478
				if (yVal === null) {
479
					if (nullMode != NULL_REMOVE) {
480
						yVals[alignedIdx] = yVal;
481

482
						if (nullMode == NULL_EXPAND)
483
							nullIdxs.push(alignedIdx);
484
					}
485
				}
486
				else
487
					yVals[alignedIdx] = yVal;
488
			}
489

490
			nullExpand(yVals, nullIdxs, alignedLen);
491

492
			data.push(yVals);
493
		}
494
	}
495

496
	return data;
497
}
498

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

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

510
const mousemove   = "mousemove";
511
const mousedown   = "mousedown";
512
const mouseup     = "mouseup";
513
const mouseenter  = "mouseenter";
514
const mouseleave  = "mouseleave";
515
const dblclick    = "dblclick";
516
const resize      = "resize";
517
const scroll      = "scroll";
518

519
const change      = "change";
520
const dppxchange  = "dppxchange";
521

522
const pre = "u-";
523

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

546
const doc = document;
547
const win = window;
548
let pxRatio;
549

550
let query;
551

552
function setPxRatio() {
553
	let _pxRatio = devicePixelRatio;
554

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

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

563
		win.dispatchEvent(new CustomEvent(dppxchange));
564
	}
565
}
566

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

574
function remClass(el, c) {
575
	let cl = el.classList;
576
	cl.contains(c) && cl.remove(c);
577
}
578

579
function setStylePx(el, name, value) {
580
	el.style[name] = value + "px";
581
}
582

583
function placeTag(tag, cls, targ, refEl) {
584
	let el = doc.createElement(tag);
585

586
	if (cls != null)
587
		addClass(el, cls);
588

589
	if (targ != null)
590
		targ.insertBefore(el, refEl);
591

592
	return el;
593
}
594

595
function placeDiv(cls, targ) {
596
	return placeTag("div", cls, targ);
597
}
598

599
const xformCache = new WeakMap();
600

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

605
	if (xform != xformOld) {
606
		el.style.transform = xform;
607
		xformCache.set(el, xform);
608

609
		if (xPos < 0 || yPos < 0 || xPos > xMax || yPos > yMax)
610
			addClass(el, OFF);
611
		else
612
			remClass(el, OFF);
613
	}
614
}
615

616
const colorCache = new WeakMap();
617

618
function elColor(el, background, borderColor) {
619
	let newColor = background + borderColor;
620
	let oldColor = colorCache.get(el);
621

622
	if (newColor != oldColor) {
623
		colorCache.set(el, newColor);
624
		el.style.background = background;
625
		el.style.borderColor = borderColor;
626
	}
627
}
628

629
const sizeCache = new WeakMap();
630

631
function elSize(el, newWid, newHgt, centered) {
632
	let newSize = newWid + "" + newHgt;
633
	let oldSize = sizeCache.get(el);
634

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

644
const evOpts = {passive: true};
645
const evOpts2 = assign({capture: true}, evOpts);
646

647
function on(ev, el, cb, capt) {
648
	el.addEventListener(ev, cb, capt ? evOpts2 : evOpts);
649
}
650

651
function off(ev, el, cb, capt) {
652
	el.removeEventListener(ev, cb, capt ? evOpts2 : evOpts);
653
}
654

655
setPxRatio();
656

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

672
const days = [
673
	"Sunday",
674
	"Monday",
675
	"Tuesday",
676
	"Wednesday",
677
	"Thursday",
678
	"Friday",
679
	"Saturday",
680
];
681

682
function slice3(str) {
683
	return str.slice(0, 3);
684
}
685

686
const days3 = days.map(slice3);
687

688
const months3 = months.map(slice3);
689

690
const engNames = {
691
	MMMM: months,
692
	MMM:  months3,
693
	WWWW: days,
694
	WWW:  days3,
695
};
696

697
function zeroPad2(int) {
698
	return (int < 10 ? '0' : '') + int;
699
}
700

701
function zeroPad3(int) {
702
	return (int < 10 ? '00' : int < 100 ? '0' : '') + int;
703
}
704

705
/*
706
function suffix(int) {
707
	let mod10 = int % 10;
708

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

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

762
function fmtDate(tpl, names) {
763
	names = names || engNames;
764
	let parts = [];
765

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

768
	while (m = R.exec(tpl))
769
		parts.push(m[0][0] == '{' ? subs[m[1]] : m[0]);
770

771
	return d => {
772
		let out = '';
773

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

777
		return out;
778
	}
779
}
780

781
const localTz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
782

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

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

797
	return date2;
798
}
799

800
//export const series = [];
801

802
// default formatters:
803

804
const onlyWhole = v => v % 1 == 0;
805

806
const allMults = [1,2,2.5,5];
807

808
// ...0.01, 0.02, 0.025, 0.05, 0.1, 0.2, 0.25, 0.5
809
const decIncrs = genIncrs(10, -16, 0, allMults);
810

811
// 1, 2, 2.5, 5, 10, 20, 25, 50...
812
const oneIncrs = genIncrs(10, 0, 16, allMults);
813

814
// 1, 2,      5, 10, 20, 25, 50...
815
const wholeIncrs = oneIncrs.filter(onlyWhole);
816

817
const numIncrs = decIncrs.concat(oneIncrs);
818

819
const NL = "\n";
820

821
const yyyy    = "{YYYY}";
822
const NLyyyy  = NL + yyyy;
823
const md      = "{M}/{D}";
824
const NLmd    = NL + md;
825
const NLmdyy  = NLmd + "/{YY}";
826

827
const aa      = "{aa}";
828
const hmm     = "{h}:{mm}";
829
const hmmaa   = hmm + aa;
830
const NLhmmaa = NL + hmmaa;
831
const ss      = ":{ss}";
832

833
const _ = null;
834

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

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

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

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

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

920
			// get the timezone-adjusted date
921
			let minDate = tzDate(scaleMin);
922
			let minDateTs = roundDec(minDate * ms, 3);
923

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

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

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

941
					split = roundDec((+next + offs) * ms, 3);
942

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

953
				let date0 = tzDate(split);
954

955
				let prevHour = date0.getHours() + (date0.getMinutes() / m) + (date0.getSeconds() / h);
956
				let incrHours = foundIncr / h;
957

958
				let minSpace = self.axes[axisIdx]._space;
959
				let pctSpace = foundSpace / minSpace;
960

961
				while (1) {
962
					split = roundDec(split + foundIncr, ms == 1 ? 0 : 3);
963

964
					if (split > scaleMax)
965
						break;
966

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

972
						let dstShift = actualHour - expectedHour;
973

974
						if (dstShift > 1)
975
							dstShift = -1;
976

977
						split -= dstShift * h;
978

979
						prevHour = (prevHour + incrHours) % 24;
980

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

985
						if (pctIncr * pctSpace >= .7)
986
							splits.push(split);
987
					}
988
					else
989
						splits.push(split);
990
				}
991
			}
992

993
			return splits;
994
		}
995
	}
996

997
	return [
998
		timeIncrs,
999
		_timeAxisStamps,
1000
		timeAxisSplits,
1001
	];
1002
}
1003

1004
const [ timeIncrsMs, _timeAxisStampsMs, timeAxisSplitsMs ] = genTimeStuffs(1);
1005
const [ timeIncrsS,  _timeAxisStampsS,  timeAxisSplitsS  ] = genTimeStuffs(1e-3);
1006

1007
// base 2
1008
genIncrs(2, -53, 53, [1]);
1009

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

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

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

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

1041
		return splits.map(split => {
1042
			let date = tzDate(split);
1043

1044
			let newYear = date.getFullYear();
1045
			let newMnth = date.getMonth();
1046
			let newDate = date.getDate();
1047
			let newHour = date.getHours();
1048
			let newMins = date.getMinutes();
1049
			let newSecs = date.getSeconds();
1050

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

1061
			prevYear = newYear;
1062
			prevMnth = newMnth;
1063
			prevDate = newDate;
1064
			prevHour = newHour;
1065
			prevMins = newMins;
1066
			prevSecs = newSecs;
1067

1068
			return stamp(date);
1069
		});
1070
	}
1071
}
1072

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

1079
function mkDate(y, m, d) {
1080
	return new Date(y, m, d);
1081
}
1082

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

1088
function timeSeriesVal(tzDate, stamp) {
1089
	return (self, val) => stamp(tzDate(val));
1090
}
1091

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

1097
function legendFill(self, seriesIdx) {
1098
	return self.series[seriesIdx].fill(self, seriesIdx);
1099
}
1100

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

1117
function cursorPointShow(self, si) {
1118
	let o = self.cursor.points;
1119

1120
	let pt = placeDiv();
1121

1122
	let size = o.size(self, si);
1123
	setStylePx(pt, WIDTH, size);
1124
	setStylePx(pt, HEIGHT, size);
1125

1126
	let mar = size / -2;
1127
	setStylePx(pt, "marginLeft", mar);
1128
	setStylePx(pt, "marginTop", mar);
1129

1130
	let width = o.width(self, si, size);
1131
	width && setStylePx(pt, "borderWidth", width);
1132

1133
	return pt;
1134
}
1135

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

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

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

1151
function dataIdx(self, seriesIdx, cursorIdx) {
1152
	return cursorIdx;
1153
}
1154

1155
const moveTuple = [0,0];
1156

1157
function cursorMove(self, mouseLeft1, mouseTop1) {
1158
	moveTuple[0] = mouseLeft1;
1159
	moveTuple[1] = mouseTop1;
1160
	return moveTuple;
1161
}
1162

1163
function filtBtn0(self, targ, handle) {
1164
	return e => {
1165
		e.button == 0 && handle(e);
1166
	};
1167
}
1168

1169
function passThru(self, targ, handle) {
1170
	return handle;
1171
}
1172

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

1187
	bind: {
1188
		mousedown:   filtBtn0,
1189
		mouseup:     filtBtn0,
1190
		click:       filtBtn0,
1191
		dblclick:    filtBtn0,
1192

1193
		mousemove:   passThru,
1194
		mouseleave:  passThru,
1195
		mouseenter:  passThru,
1196
	},
1197

1198
	drag: {
1199
		setScale: true,
1200
		x: true,
1201
		y: false,
1202
		dist: 0,
1203
		uni: null,
1204
		_x: false,
1205
		_y: false,
1206
	},
1207

1208
	focus: {
1209
		prox: -1,
1210
	},
1211

1212
	left: -10,
1213
	top: -10,
1214
	idx: null,
1215
	dataIdx,
1216
	idxs: null,
1217
};
1218

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

1227
const ticks = assign({}, grid, {size: 10});
1228

1229
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"';
1230
const labelFont = "bold " + font;
1231
const lineMult = 1.5;		// font-size multiplier
1232

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

1254
const numSeriesLabel = "Value";
1255
const timeSeriesLabel = "Time";
1256

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

1265
	// internal caches
1266
	min: inf,
1267
	max: -inf,
1268
	idxs: [],
1269
};
1270

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

1275
function numAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {
1276
	let splits = [];
1277

1278
	let numDec = fixedDec.get(foundIncr) || 0;
1279

1280
	scaleMin = forceMin ? scaleMin : roundDec(incrRoundUp(scaleMin, foundIncr), numDec);
1281

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

1285
	return splits;
1286
}
1287

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

1292
	const logBase = self.scales[self.axes[axisIdx].scale].log;
1293

1294
	const logFn = logBase == 10 ? log10 : log2;
1295

1296
	const exp = floor(logFn(scaleMin));
1297

1298
	foundIncr = pow(logBase, exp);
1299

1300
	if (exp < 0)
1301
		foundIncr = roundDec(foundIncr, -exp);
1302

1303
	let split = scaleMin;
1304

1305
	do {
1306
		splits.push(split);
1307
		split = roundDec(split + foundIncr, fixedDec.get(foundIncr));
1308

1309
		if (split >= foundIncr * logBase)
1310
			foundIncr = split;
1311

1312
	} while (split <= scaleMax);
1313

1314
	return splits;
1315
}
1316

1317
function asinhAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {
1318
	let sc = self.scales[self.axes[axisIdx].scale];
1319

1320
	let linthresh = sc.asinh;
1321

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

1326
	return negSplits.reverse().map(v => -v).concat(zero, posSplits);
1327
}
1328

1329
const RE_ALL   = /./;
1330
const RE_12357 = /[12357]/;
1331
const RE_125   = /[125]/;
1332
const RE_1     = /1/;
1333

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

1339
	if (sc.distr == 3 && sc.log == 2)
1340
		return splits;
1341

1342
	let valToPos = self.valToPos;
1343

1344
	let minSpace = axis._space;
1345

1346
	let _10 = valToPos(10, scaleKey);
1347

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

1355
	return splits.map(v => ((sc.distr == 4 && v == 0) || re.test(v)) ? v : null);
1356
}
1357

1358
function numSeriesVal(self, val) {
1359
	return val == null ? "" : fmtNum(val);
1360
}
1361

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

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

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

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

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

1408
const facet = {
1409
	scale: null,
1410
	auto: true,
1411

1412
	// internal caches
1413
	min: inf,
1414
	max: -inf,
1415
};
1416

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

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

1449
	// internal caches
1450
	min: inf,
1451
	max: -inf,
1452
	idxs: [],
1453

1454
	path: null,
1455
	clip: null,
1456
};
1457

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

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

1482
const yScaleOpts = assign({}, xScaleOpts, {
1483
	time: false,
1484
	ori: 1,
1485
});
1486

1487
const syncs = {};
1488

1489
function _sync(key, opts) {
1490
	let s = syncs[key];
1491

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

1508
		if (key != null)
1509
			syncs[key] = s;
1510
	}
1511

1512
	return s;
1513
}
1514

1515
const BAND_CLIP_FILL   = 1 << 0;
1516
const BAND_CLIP_STROKE = 1 << 1;
1517

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

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

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

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

1580
		const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
1581
		const lineTo = scaleX.ori == 0 ? lineToH : lineToV;
1582

1583
		let frIdx, toIdx;
1584

1585
		if (dir == 1) {
1586
			frIdx = idx0;
1587
			toIdx = idx1;
1588
		}
1589
		else {
1590
			frIdx = idx1;
1591
			toIdx = idx0;
1592
		}
1593

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

1602
		let clip = new Path2D(strokePath);
1603

1604
		lineTo(clip, x1, yLimit);
1605
		lineTo(clip, x0, yLimit);
1606
		lineTo(clip, x0, y0);
1607

1608
		return clip;
1609
	});
1610
}
1611

1612
function clipGaps(gaps, ori, plotLft, plotTop, plotWid, plotHgt) {
1613
	let clip = null;
1614

1615
	// create clip path (invert gaps and non-gaps)
1616
	if (gaps.length > 0) {
1617
		clip = new Path2D();
1618

1619
		const rect = ori == 0 ? rectH : rectV;
1620

1621
		let prevGapEnd = plotLft;
1622

1623
		for (let i = 0; i < gaps.length; i++) {
1624
			let g = gaps[i];
1625

1626
			if (g[1] > g[0]) {
1627
				let w = g[0] - prevGapEnd;
1628

1629
				w > 0 && rect(clip, prevGapEnd, plotTop, w, plotTop + plotHgt);
1630

1631
				prevGapEnd = g[1];
1632
			}
1633
		}
1634

1635
		let w = plotLft + plotWid - prevGapEnd;
1636

1637
		w > 0 && rect(clip, prevGapEnd, plotTop, w, plotTop + plotHgt);
1638
	}
1639

1640
	return clip;
1641
}
1642

1643
function addGap(gaps, fromX, toX) {
1644
	let prevGap = gaps[gaps.length - 1];
1645

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

1652
function pxRoundGen(pxAlign) {
1653
	return pxAlign == 0 ? retArg0 : pxAlign == 1 ? round : v => incrRound(v, pxAlign);
1654
}
1655

1656
function rect(ori) {
1657
	let moveTo = ori == 0 ?
1658
		moveToH :
1659
		moveToV;
1660

1661
	let arcTo = ori == 0 ?
1662
		(p, x1, y1, x2, y2, r) => { p.arcTo(x1, y1, x2, y2, r); } :
1663
		(p, y1, x1, y2, x2, r) => { p.arcTo(x1, y1, x2, y2, r); };
1664

1665
	let rect = ori == 0 ?
1666
		(p, x, y, w, h) => { p.rect(x, y, w, h); } :
1667
		(p, y, x, h, w) => { p.rect(x, y, w, h); };
1668

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

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

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

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

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

1706
			let moveTo, arc;
1707

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

1717
			const width = roundDec(points.width * pxRatio, 3);
1718

1719
			let rad = (points.size - points.width) / 2 * pxRatio;
1720
			let dia = roundDec(rad * 2, 3);
1721

1722
			let fill = new Path2D();
1723
			let clip = new Path2D();
1724

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

1727
			rectH(clip,
1728
				lft - dia,
1729
				top - dia,
1730
				wid + dia * 2,
1731
				hgt + dia * 2,
1732
			);
1733

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

1739
					moveTo(fill, x + rad, y);
1740
					arc(fill, x, y, rad, 0, PI * 2);
1741
				}
1742
			};
1743

1744
			if (filtIdxs)
1745
				filtIdxs.forEach(drawPoint);
1746
			else {
1747
				for (let pi = idx0; pi <= idx1; pi++)
1748
					drawPoint(pi);
1749
			}
1750

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

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

1769
			lineTo(stroke, accX, outY);
1770
		}
1771
	};
1772
}
1773

1774
const drawAccH = _drawAcc(lineToH);
1775
const drawAccV = _drawAcc(lineToV);
1776

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

1782
			let lineTo, drawAcc;
1783

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

1793
			const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
1794

1795
			const _paths = {stroke: new Path2D(), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};
1796
			const stroke = _paths.stroke;
1797

1798
			let minY = inf,
1799
				maxY = -inf,
1800
				inY, outY, outX, drawnAtX;
1801

1802
			let gaps = [];
1803

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

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

1814
			if (lftX > xOff)
1815
				addGap(gaps, xOff, lftX);
1816

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

1820
				if (x == accX) {
1821
					if (dataY[i] != null) {
1822
						outY = pxRound(valToPosY(dataY[i], scaleY, yDim, yOff));
1823

1824
						if (minY == inf) {
1825
							lineTo(stroke, x, outY);
1826
							inY = outY;
1827
						}
1828

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

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

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

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

1856
						prevYNull = false;
1857
					}
1858
					else {
1859
						minY = inf;
1860
						maxY = -inf;
1861

1862
						if (dataY[i] === null) {
1863
							accGaps = true;
1864

1865
							if (x - accX > 1)
1866
								_addGap = true;
1867
						}
1868
					}
1869

1870
					_addGap && addGap(gaps, outX, x);
1871

1872
					accX = x;
1873
				}
1874
			}
1875

1876
			if (minY != inf && minY != maxY && drawnAtX != accX)
1877
				drawAcc(stroke, accX, minY, maxY, inY, outY);
1878

1879
			if (rgtX < xOff + xDim)
1880
				addGap(gaps, rgtX, xOff + xDim);
1881

1882
			if (series.fill != null) {
1883
				let fill = _paths.fill = new Path2D(stroke);
1884

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

1887
				lineTo(fill, rgtX, fillTo);
1888
				lineTo(fill, lftX, fillTo);
1889
			}
1890

1891
			_paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
1892

1893
			if (!series.spanGaps)
1894
				_paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
1895

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

1902
			return _paths;
1903
		});
1904
	};
1905
}
1906

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

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

1916
			let lineTo = scaleX.ori == 0 ? lineToH : lineToV;
1917

1918
			const _paths = {stroke: new Path2D(), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};
1919
			const stroke = _paths.stroke;
1920

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

1923
			idx0 = nonNullIdx(dataY, idx0, idx1,  1);
1924
			idx1 = nonNullIdx(dataY, idx0, idx1, -1);
1925

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

1932
			lineTo(stroke, firstXPos, prevYPos);
1933

1934
			for (let i = _dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dir) {
1935
				let yVal1 = dataY[i];
1936

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

1939
				if (yVal1 == null) {
1940
					if (yVal1 === null) {
1941
						addGap(gaps, prevXPos, x1);
1942
						inGap = true;
1943
					}
1944
					continue;
1945
				}
1946

1947
				let y1 = pxRound(valToPosY(yVal1, scaleY, yDim, yOff));
1948

1949
				if (inGap) {
1950
					addGap(gaps, prevXPos, x1);
1951
					inGap = false;
1952
				}
1953

1954
				if (align == 1)
1955
					lineTo(stroke, x1, prevYPos);
1956
				else
1957
					lineTo(stroke, prevXPos, y1);
1958

1959
				lineTo(stroke, x1, y1);
1960

1961
				prevYPos = y1;
1962
				prevXPos = x1;
1963
			}
1964

1965
			if (series.fill != null) {
1966
				let fill = _paths.fill = new Path2D(stroke);
1967

1968
				let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
1969
				let minY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff));
1970

1971
				lineTo(fill, prevXPos, minY);
1972
				lineTo(fill, firstXPos, minY);
1973
			}
1974

1975
			_paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
1976

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

1982
			gaps.forEach(g => {
1983
				g[0] += startsOffset;
1984
				g[1] += endsOffset;
1985
			});
1986

1987
			if (!series.spanGaps)
1988
				_paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
1989

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

1996
			return _paths;
1997
		});
1998
	};
1999
}
2000

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

2007
	const radius = ifNull(opts.radius, 0);
2008

2009
	const gapFactor = 1 - size[0];
2010
	const maxWidth  = ifNull(size[1], inf) * pxRatio;
2011
	const minWidth  = ifNull(size[2], 1) * pxRatio;
2012

2013
	const disp = ifNull(opts.disp, EMPTY_OBJ);
2014
	const _each = ifNull(opts.each, _ => {});
2015

2016
	const { fill: dispFills, stroke: dispStrokes } = disp;
2017

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

2022
			const _dirX = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
2023
			const _dirY = scaleY.dir * (scaleY.ori == 1 ? 1 : -1);
2024

2025
			let rect = scaleX.ori == 0 ? rectH : rectV;
2026

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

2031
			let fillToY = series.fillTo(u, seriesIdx, series.min, series.max);
2032

2033
			let y0Pos = valToPosY(fillToY, scaleY, yDim, yOff);
2034

2035
			// barWid is to center of stroke
2036
			let xShift, barWid;
2037

2038
			let strokeWidth = pxRound(series.width * pxRatio);
2039

2040
			let multiPath = false;
2041

2042
			let fillColors = null;
2043
			let fillPaths = null;
2044
			let strokeColors = null;
2045
			let strokePaths = null;
2046

2047
			if (dispFills != null && dispStrokes != null) {
2048
				multiPath = true;
2049

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

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

2065
			let { x0, size } = disp;
2066

2067
			if (x0 != null && size != null) {
2068
				dataX = x0.values(u, seriesIdx, idx0, idx1);
2069

2070
				if (x0.unit == 2)
2071
					dataX = dataX.map(pct => u.posToVal(xOff + pct * xDim, scaleX.key, true));
2072

2073
				// assumes uniform sizes, for now
2074
				let sizes = size.values(u, seriesIdx, idx0, idx1);
2075

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

2081
				barWid = pxRound(barWid - strokeWidth);
2082

2083
				xShift = (_dirX == 1 ? -strokeWidth / 2 : barWid + strokeWidth / 2);
2084
			}
2085
			else {
2086
				let colWid = xDim;
2087

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

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

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

2105
							prevIdx = i;
2106
						}
2107
					}
2108
				}
2109

2110
				let gapWid = colWid * gapFactor;
2111

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

2114
				xShift = (align == 0 ? barWid / 2 : align == _dirX ? 0 : barWid) - align * _dirX * extraGap / 2;
2115
			}
2116

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

2119
			const hasBands = u.bands.length > 0;
2120
			let yLimit;
2121

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

2129
			const stroke = multiPath ? null : new Path2D();
2130
			const band = _paths.band;
2131

2132
			for (let i = _dirX == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dirX) {
2133
				let yVal = dataY[i];
2134

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

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

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

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

2157
				let r = radius * barWid;
2158

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

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

2170
					each(u, seriesIdx, i,
2171
						lft    - strokeWidth / 2,
2172
						top,
2173
						barWid + strokeWidth,
2174
						barHgt,
2175
					);
2176
				}
2177

2178
				if (hasBands) {
2179
					if (_dirY == 1) {
2180
						btm = top;
2181
						top = yLimit;
2182
					}
2183
					else {
2184
						top = btm;
2185
						btm = yLimit;
2186
					}
2187

2188
					barHgt = btm - top;
2189

2190
					rect(band, lft - strokeWidth / 2, top, barWid + strokeWidth, max(0, barHgt), 0);
2191
				}
2192
			}
2193

2194
			if (strokeWidth > 0)
2195
				_paths.stroke = multiPath ? strokePaths : stroke;
2196

2197
			_paths.fill = multiPath ? fillPaths : stroke;
2198

2199
			return _paths;
2200
		});
2201
	};
2202
}
2203

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

2209
			let moveTo, bezierCurveTo, lineTo;
2210

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

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

2224
			idx0 = nonNullIdx(dataY, idx0, idx1,  1);
2225
			idx1 = nonNullIdx(dataY, idx0, idx1, -1);
2226

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

2232
			let xCoords = [];
2233
			let yCoords = [];
2234

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

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

2253
					xCoords.push((prevXPos = xPos));
2254
					yCoords.push(valToPosY(dataY[i], scaleY, yDim, yOff));
2255
				}
2256
			}
2257

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

2261
			if (series.fill != null && stroke != null) {
2262
				let fill = _paths.fill = new Path2D(stroke);
2263

2264
				let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
2265
				let minY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff));
2266

2267
				lineTo(fill, prevXPos, minY);
2268
				lineTo(fill, firstXPos, minY);
2269
			}
2270

2271
			_paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
2272

2273
			if (!series.spanGaps)
2274
				_paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
2275

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

2282
			return _paths;
2283

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

2299
function monotoneCubic(opts) {
2300
	return splineInterp(_monotoneCubic);
2301
}
2302

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

2308
	if (n < 2)
2309
		return null;
2310

2311
	const path = new Path2D();
2312

2313
	moveTo(path, xs[0], ys[0]);
2314

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

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

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

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

2343
				if (!isFinite(ms[i]))
2344
					ms[i] = 0;
2345
			}
2346
		}
2347

2348
		ms[n - 1] = ds[n - 2];
2349

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

2363
	return path;
2364
}
2365

2366
const cursorPlots = new Set();
2367

2368
function invalidateRects() {
2369
	cursorPlots.forEach(u => {
2370
		u.syncRect(true);
2371
	});
2372
}
2373

2374
on(resize, win, invalidateRects);
2375
on(scroll, win, invalidateRects, true);
2376

2377
const linearPath = linear() ;
2378
const pointsPath = points() ;
2379

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

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

2389
function setDefault(o, i, xo, yo) {
2390
	return assign({}, (i == 0 ? xo : yo), o);
2391
}
2392

2393
function snapNumX(self, dataMin, dataMax) {
2394
	return dataMin == null ? nullNullTuple : [dataMin, dataMax];
2395
}
2396

2397
const snapTimeX = snapNumX;
2398

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

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

2409
const snapLogX = snapLogY;
2410

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

2415
const snapAsinhX = snapAsinhY;
2416

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

2421
	let delta = maxVal - minVal;
2422

2423
	let incrIdx = closestIdx((minSpace / dim) * delta, incrs);
2424

2425
	do {
2426
		let foundIncr = incrs[incrIdx];
2427
		let foundSpace = dim * foundIncr / delta;
2428

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

2433
	return [0, 0];
2434
}
2435

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

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

2452
function uPlot(opts, data, then) {
2453
	const self = {
2454
		mode: ifNull(opts.mode, 1),
2455
	};
2456

2457
	const mode = self.mode;
2458

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

2467
		return (_val - scale._min) / (scale._max - scale._min);
2468
	}
2469

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

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

2480
	function getPos(val, scale, dim, off) {
2481
		return scale.ori == 0 ? getHPos(val, scale, dim, off) : getVPos(val, scale, dim, off);
2482
	}
2483

2484
	self.valToPosH = getHPos;
2485
	self.valToPosV = getVPos;
2486

2487
	let ready = false;
2488
	self.status = 0;
2489

2490
	const root = self.root = placeDiv(UPLOT);
2491

2492
	if (opts.id != null)
2493
		root.id = opts.id;
2494

2495
	addClass(root, opts.class);
2496

2497
	if (opts.title) {
2498
		let title = placeDiv(TITLE, root);
2499
		title.textContent = opts.title;
2500
	}
2501

2502
	const can = placeTag("canvas");
2503
	const ctx = self.ctx = can.getContext("2d");
2504

2505
	const wrap = placeDiv(WRAP, root);
2506
	const under = self.under = placeDiv(UNDER, wrap);
2507
	wrap.appendChild(can);
2508
	const over = self.over = placeDiv(OVER, wrap);
2509

2510
	opts = copy(opts);
2511

2512
	const pxAlign = +ifNull(opts.pxAlign, 1);
2513

2514
	const pxRound = pxRoundGen(pxAlign);
2515

2516
	(opts.plugins || []).forEach(p => {
2517
		if (p.opts)
2518
			opts = p.opts(self, opts) || opts;
2519
	});
2520

2521
	const ms = opts.ms || 1e-3;
2522

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

2530
	bands.forEach(b => {
2531
		b.fill = fnOrSelf(b.fill || null);
2532
	});
2533

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

2536
	const drawOrderMap = {
2537
		axes: drawAxesGrid,
2538
		series: drawSeries,
2539
	};
2540

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

2543
	function initScale(scaleKey) {
2544
		let sc = scales[scaleKey];
2545

2546
		if (sc == null) {
2547
			let scaleOpts = (opts.scales || EMPTY_OBJ)[scaleKey] || EMPTY_OBJ;
2548

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

2558
				if (mode == 2)
2559
					sc.time = false;
2560

2561
				sc.key = scaleKey;
2562

2563
				let isTime = sc.time;
2564

2565
				let rn = sc.range;
2566

2567
				let rangeIsArr = isArr(rn);
2568

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

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

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

2599
				sc.auto = fnOrSelf(rangeIsArr ? false : sc.auto);
2600

2601
				sc.clamp = fnOrSelf(sc.clamp || clampScale);
2602

2603
				// caches for expensive ops like asinh() & log()
2604
				sc._min = sc._max = null;
2605
			}
2606
		}
2607
	}
2608

2609
	initScale("x");
2610
	initScale("y");
2611

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

2619
	axes.forEach(a => {
2620
		initScale(a.scale);
2621
	});
2622

2623
	for (let k in opts.scales)
2624
		initScale(k);
2625

2626
	const scaleX = scales[xScaleKey];
2627

2628
	const xScaleDistr = scaleX.distr;
2629

2630
	let valToPosX, valToPosY;
2631

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

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

2661
			xDimCss = plotHgtCss;
2662
			xOffCss = plotTopCss;
2663
			yDimCss = plotWidCss;
2664
			yOffCss = plotLftCss;
2665
		};
2666
		*/
2667
	}
2668

2669
	const pendScales = {};
2670

2671
	// explicitly-set initial scales
2672
	for (let k in scales) {
2673
		let sc = scales[k];
2674

2675
		if (sc.min != null || sc.max != null) {
2676
			pendScales[k] = {min: sc.min, max: sc.max};
2677
			sc.min = sc.max = null;
2678
		}
2679
	}
2680

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

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

2689
	const activeIdxs = [];
2690

2691
	const legend     = (self.legend = assign({}, legendOpts, opts.legend));
2692
	const showLegend = legend.show;
2693
	const markers    = legend.markers;
2694

2695
	{
2696
		legend.idxs = activeIdxs;
2697

2698
		markers.width  = fnOrSelf(markers.width);
2699
		markers.dash   = fnOrSelf(markers.dash);
2700
		markers.stroke = fnOrSelf(markers.stroke);
2701
		markers.fill   = fnOrSelf(markers.fill);
2702
	}
2703

2704
	let legendEl;
2705
	let legendRows = [];
2706
	let legendCells = [];
2707
	let legendCols;
2708
	let multiValLegend = false;
2709
	let NULL_LEGEND_VALUES = {};
2710

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

2716
		for (let k in legendCols)
2717
			NULL_LEGEND_VALUES[k] = "--";
2718
	}
2719

2720
	if (showLegend) {
2721
		legendEl = placeTag("table", LEGEND, root);
2722

2723
		if (multiValLegend) {
2724
			let head = placeTag("tr", LEGEND_THEAD, legendEl);
2725
			placeTag("th", null, head);
2726

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

2736
	const son  = {show: true};
2737
	const soff = {show: false};
2738

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

2743
		let cells = [];
2744

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

2747
		addClass(row, s.class);
2748

2749
		if (!s.show)
2750
			addClass(row, OFF);
2751

2752
		let label = placeTag("th", null, row);
2753

2754
		if (markers.show) {
2755
			let indic = placeDiv(LEGEND_MARKER, label);
2756

2757
			if (i > 0) {
2758
				let width  = markers.width(self, i);
2759

2760
				if (width)
2761
					indic.style.border = width + "px " + markers.dash(self, i) + " " + markers.stroke(self, i);
2762

2763
				indic.style.background = markers.fill(self, i);
2764
			}
2765
		}
2766

2767
		let text = placeDiv(LEGEND_LABEL, label);
2768
		text.textContent = s.label;
2769

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

2774
			onMouse("click", label, e => {
2775
				if (cursor._lock)
2776
					return;
2777

2778
				let seriesIdx = series.indexOf(s);
2779

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

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

2792
			if (cursorFocus) {
2793
				onMouse(mouseenter, label, e => {
2794
					if (cursor._lock)
2795
						return;
2796

2797
					setSeries(series.indexOf(s), FOCUS_TRUE, true, syncOpts.setSeries);
2798
				});
2799
			}
2800
		}
2801

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

2808
		return [row, cells];
2809
	}
2810

2811
	const mouseListeners = new Map();
2812

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

2817
		if (listener) {
2818
			on(ev, targ, targListeners[ev] = listener);
2819
			mouseListeners.set(targ, targListeners);
2820
		}
2821
	}
2822

2823
	function offMouse(ev, targ, fn) {
2824
		const targListeners = mouseListeners.get(targ) || {};
2825

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

2833
		if (ev == null)
2834
			mouseListeners.delete(targ);
2835
	}
2836

2837
	let fullWidCss = 0;
2838
	let fullHgtCss = 0;
2839

2840
	let plotWidCss = 0;
2841
	let plotHgtCss = 0;
2842

2843
	// plot margins to account for axes
2844
	let plotLftCss = 0;
2845
	let plotTopCss = 0;
2846

2847
	let plotLft = 0;
2848
	let plotTop = 0;
2849
	let plotWid = 0;
2850
	let plotHgt = 0;
2851

2852
	self.bbox = {};
2853

2854
	let shouldSetScales = false;
2855
	let shouldSetSize = false;
2856
	let shouldConvergeSize = false;
2857
	let shouldSetCursor = false;
2858
	let shouldSetLegend = false;
2859

2860
	function _setSize(width, height, force) {
2861
		if (force || (width != self.width || height != self.height))
2862
			calcSize(width, height);
2863

2864
		resetYSeries(false);
2865

2866
		shouldConvergeSize = true;
2867
		shouldSetSize = true;
2868
		shouldSetCursor = shouldSetLegend = cursor.left >= 0;
2869
		commit();
2870
	}
2871

2872
	function calcSize(width, height) {
2873
	//	log("calcSize()", arguments);
2874

2875
		self.width  = fullWidCss = plotWidCss = width;
2876
		self.height = fullHgtCss = plotHgtCss = height;
2877
		plotLftCss  = plotTopCss = 0;
2878

2879
		calcPlotRect();
2880
		calcAxesRects();
2881

2882
		let bb = self.bbox;
2883

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

2889
	//	updOriDims();
2890
	}
2891

2892
	// ensures size calc convergence
2893
	const CYCLE_LIMIT = 3;
2894

2895
	function convergeSize() {
2896
		let converged = false;
2897

2898
		let cycleNum = 0;
2899

2900
		while (!converged) {
2901
			cycleNum++;
2902

2903
			let axesConverged = axesCalc(cycleNum);
2904
			let paddingConverged = paddingCalc(cycleNum);
2905

2906
			converged = cycleNum == CYCLE_LIMIT || (axesConverged && paddingConverged);
2907

2908
			if (!converged) {
2909
				calcSize(self.width, self.height);
2910
				shouldSetSize = true;
2911
			}
2912
		}
2913
	}
2914

2915
	function setSize({width, height}) {
2916
		_setSize(width, height);
2917
	}
2918

2919
	self.setSize = setSize;
2920

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

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

2935
				let fullSize = _size + labelSize;
2936

2937
				if (fullSize > 0) {
2938
					if (isVt) {
2939
						plotWidCss -= fullSize;
2940

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

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

2962
		sidesWithAxes[0] = hasTopAxis;
2963
		sidesWithAxes[1] = hasRgtAxis;
2964
		sidesWithAxes[2] = hasBtmAxis;
2965
		sidesWithAxes[3] = hasLftAxis;
2966

2967
		// hz padding
2968
		plotWidCss -= _padding[1] + _padding[3];
2969
		plotLftCss += _padding[3];
2970

2971
		// vt padding
2972
		plotHgtCss -= _padding[2] + _padding[0];
2973
		plotTopCss += _padding[0];
2974
	}
2975

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

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

2993
		axes.forEach((axis, i) => {
2994
			if (axis.show && axis._show) {
2995
				let side = axis.side;
2996

2997
				axis._pos = incrOffset(side, axis._size);
2998

2999
				if (axis.label != null)
3000
					axis._lpos = incrOffset(side, axis.labelSize);
3001
			}
3002
		});
3003
	}
3004

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

3007
	{
3008
		cursor.idxs = activeIdxs;
3009

3010
		cursor._lock = false;
3011

3012
		let points = cursor.points;
3013

3014
		points.show   = fnOrSelf(points.show);
3015
		points.size   = fnOrSelf(points.size);
3016
		points.stroke = fnOrSelf(points.stroke);
3017
		points.width  = fnOrSelf(points.width);
3018
		points.fill   = fnOrSelf(points.fill);
3019
	}
3020

3021
	const focus = self.focus = assign({}, opts.focus || {alpha: 0.3}, cursor.focus);
3022
	const cursorFocus = focus.prox >= 0;
3023

3024
	// series-intersection markers
3025
	let cursorPts = [null];
3026

3027
	function initCursorPt(s, si) {
3028
		if (si > 0) {
3029
			let pt = cursor.points.show(self, si);
3030

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

3037
				return pt;
3038
			}
3039
		}
3040
	}
3041

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

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

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

3058
			s.stroke = fnOrSelf(s.stroke || null);
3059
			s.fill   = fnOrSelf(s.fill || null);
3060
			s._stroke = s._fill = s._paths = s._focus = null;
3061

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

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

3087
		if (cursor.show) {
3088
			activeIdxs.splice(i, 0, null);
3089

3090
			let pt = initCursorPt(s, i);
3091
			pt && cursorPts.splice(i, 0, pt);
3092
		}
3093
	}
3094

3095
	function addSeries(opts, si) {
3096
		si = si == null ? series.length : si;
3097

3098
		opts = setDefault(opts, si, xSeriesOpts, ySeriesOpts);
3099
		series.splice(si, 0, opts);
3100
		initSeries(series[si], si);
3101
	}
3102

3103
	self.addSeries = addSeries;
3104

3105
	function delSeries(i) {
3106
		series.splice(i, 1);
3107

3108
		if (showLegend) {
3109
			legend.values.splice(i, 1);
3110

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

3117
		if (cursor.show) {
3118
			activeIdxs.splice(i, 1);
3119

3120
			cursorPts.length > 1 && cursorPts.splice(i, 1)[0].remove();
3121
		}
3122

3123
		// TODO: de-init no-longer-needed scales?
3124
	}
3125

3126
	self.delSeries = delSeries;
3127

3128
	const sidesWithAxes = [false, false, false, false];
3129

3130
	function initAxis(axis, i) {
3131
		axis._show = axis.show;
3132

3133
		if (axis.show) {
3134
			let isVt = axis.side % 2;
3135

3136
			let sc = scales[axis.scale];
3137

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

3144
			// also set defaults for incrs & values based on axis distr
3145
			let isTime = sc.time;
3146

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

3153
			axis.stroke       = fnOrSelf(axis.stroke);
3154
			axis.grid.stroke  = fnOrSelf(axis.grid.stroke);
3155
			axis.ticks.stroke = fnOrSelf(axis.ticks.stroke);
3156

3157
			let av = axis.values;
3158

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

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

3176
			axis.font      = pxRatioFont(axis.font);
3177
			axis.labelFont = pxRatioFont(axis.labelFont);
3178

3179
			axis._size   = axis.size(self, null, i, 0);
3180

3181
			axis._space  =
3182
			axis._rotate =
3183
			axis._incrs  =
3184
			axis._found  =	// foundIncrSpace
3185
			axis._splits =
3186
			axis._values = null;
3187

3188
			if (axis._size > 0)
3189
				sidesWithAxes[i] = true;
3190

3191
			axis._el = placeDiv(AXIS, wrap);
3192

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

3198
	function autoPadSide(self, side, sidesWithAxes, cycleNum) {
3199
		let [hasTopAxis, hasRgtAxis, hasBtmAxis, hasLftAxis] = sidesWithAxes;
3200

3201
		let ori = side % 2;
3202
		let size = 0;
3203

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

3209
		return size;
3210
	}
3211

3212
	const padding = self.padding = (opts.padding || [autoPadSide,autoPadSide,autoPadSide,autoPadSide]).map(p => fnOrSelf(ifNull(p, autoPadSide)));
3213
	const _padding = self._padding = padding.map((p, i) => p(self, i, sidesWithAxes, 0));
3214

3215
	let dataLen;
3216

3217
	// rendered data window
3218
	let i0 = null;
3219
	let i1 = null;
3220
	const idxs = mode == 1 ? series[0].idxs : null;
3221

3222
	let data0 = null;
3223

3224
	let viaAutoScaleX = false;
3225

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

3237
			self.data = data.slice();
3238
			data0 = data[0];
3239
			dataLen = data0.length;
3240

3241
			if (xScaleDistr == 2)
3242
				data[0] = data0.map((v, i) => i);
3243
		}
3244

3245
		self._data = data;
3246

3247
		resetYSeries(true);
3248

3249
		fire("setData");
3250

3251
		if (_resetScales !== false) {
3252
			let xsc = scaleX;
3253

3254
			if (xsc.auto(self, viaAutoScaleX))
3255
				autoScaleX();
3256
			else
3257
				_setScale(xScaleKey, xsc.min, xsc.max);
3258

3259
			shouldSetCursor = cursor.left >= 0;
3260
			shouldSetLegend = true;
3261
			commit();
3262
		}
3263
	}
3264

3265
	self.setData = setData;
3266

3267
	function autoScaleX() {
3268
		viaAutoScaleX = true;
3269

3270
		let _min, _max;
3271

3272
		if (mode == 1) {
3273
			if (dataLen > 0) {
3274
				i0 = idxs[0] = 0;
3275
				i1 = idxs[1] = dataLen - 1;
3276

3277
				_min = data[0][i0];
3278
				_max = data[0][i1];
3279

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

3301
		_setScale(xScaleKey, _min, _max);
3302
	}
3303

3304
	let ctxStroke, ctxFill, ctxWidth, ctxDash, ctxJoin, ctxCap, ctxFont, ctxAlign, ctxBaseline;
3305
	let ctxAlpha;
3306

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

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

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

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

3341
			// initial min/max
3342
			wsc.min = min(wsc.min, facet.min = minMax[0]);
3343
			wsc.max = max(wsc.max, facet.max = minMax[1]);
3344
		}
3345
	}
3346

3347
	function setScales() {
3348
	//	log("setScales()", arguments);
3349

3350
		// wip scales
3351
		let wipScales = copy(scales, fastIsObj);
3352

3353
		for (let k in wipScales) {
3354
			let wsc = wipScales[k];
3355
			let psc = pendScales[k];
3356

3357
			if (psc != null && psc.min != null) {
3358
				assign(wsc, psc);
3359

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

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

3385
					if (i == 0) {
3386
						let minMax = wsc.range(self, wsc.min, wsc.max, k);
3387

3388
						wsc.min = minMax[0];
3389
						wsc.max = minMax[1];
3390

3391
						i0 = closestIdx(wsc.min, data[0]);
3392
						i1 = closestIdx(wsc.max, data[0]);
3393

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

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

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

3418
							accScale(wipScales[xScaleKey], pendScales[xScaleKey], xFacet, xData);
3419
							accScale(wipScales[yScaleKey], pendScales[yScaleKey], yFacet, yData);
3420

3421
							// temp
3422
							s.min = yFacet.min;
3423
							s.max = yFacet.max;
3424
						}
3425
					}
3426
				}
3427
			});
3428

3429
			// range independent scales
3430
			for (let k in wipScales) {
3431
				let wsc = wipScales[k];
3432
				let psc = pendScales[k];
3433

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

3447
		// range dependent scales
3448
		for (let k in wipScales) {
3449
			let wsc = wipScales[k];
3450

3451
			if (wsc.from != null) {
3452
				let base = wipScales[wsc.from];
3453

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

3464
		let changed = {};
3465
		let anyChanged = false;
3466

3467
		for (let k in wipScales) {
3468
			let wsc = wipScales[k];
3469
			let sc = scales[k];
3470

3471
			if (sc.min != wsc.min || sc.max != wsc.max) {
3472
				sc.min = wsc.min;
3473
				sc.max = wsc.max;
3474

3475
				let distr = sc.distr;
3476

3477
				sc._min = distr == 3 ? log10(sc.min) : distr == 4 ? asinh(sc.min, sc.asinh) : sc.min;
3478
				sc._max = distr == 3 ? log10(sc.max) : distr == 4 ? asinh(sc.max, sc.asinh) : sc.max;
3479

3480
				changed[k] = anyChanged = true;
3481
			}
3482
		}
3483

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

3497
			for (let k in changed) {
3498
				shouldConvergeSize = true;
3499
				fire("setScale", k);
3500
			}
3501

3502
			if (cursor.show)
3503
				shouldSetCursor = shouldSetLegend = cursor.left >= 0;
3504
		}
3505

3506
		for (let k in pendScales)
3507
			pendScales[k] = null;
3508
	}
3509

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

3515
		while (ydata[_i0] == null && _i0 > 0)
3516
			_i0--;
3517

3518
		while (ydata[_i1] == null && _i1 < dataLen - 1)
3519
			_i1++;
3520

3521
		return [_i0, _i1];
3522
	}
3523

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

3533
			series.forEach((s, i) => {
3534
				if (i > 0 && s.show) {
3535
					if (ctxAlpha != s.alpha)
3536
						ctx.globalAlpha = ctxAlpha = s.alpha;
3537

3538
					{
3539
						cacheStrokeFill(i, false);
3540
						s._paths && drawPath(i, false);
3541
					}
3542

3543
					{
3544
						cacheStrokeFill(i, true);
3545

3546
						let show = s.points.show(self, i, i0, i1);
3547
						let idxs = s.points.filter(self, i, show, s._paths ? s._paths.gaps : null);
3548

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

3555
					if (ctxAlpha != 1)
3556
						ctx.globalAlpha = ctxAlpha = 1;
3557

3558
					fire("drawSeries", i);
3559
				}
3560
			});
3561
		}
3562
	}
3563

3564
	function cacheStrokeFill(si, _points) {
3565
		let s = _points ? series[si].points : series[si];
3566

3567
		s._stroke = s.stroke(self, si);
3568
		s._fill   = s.fill(self, si);
3569
	}
3570

3571
	function drawPath(si, _points) {
3572
		let s = _points ? series[si].points : series[si];
3573

3574
		let strokeStyle = s._stroke;
3575
		let fillStyle   = s._fill;
3576

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

3582
		if (_points && fillStyle == null)
3583
			fillStyle = width > 0 ? "#fff" : strokeStyle;
3584

3585
		let _pxAlign = s.pxAlign == 1;
3586

3587
		_pxAlign && ctx.translate(offset, offset);
3588

3589
		if (!_points) {
3590
			let lft = plotLft,
3591
				top = plotTop,
3592
				wid = plotWid,
3593
				hgt = plotHgt;
3594

3595
			let halfWid = width * pxRatio / 2;
3596

3597
			if (s.min == 0)
3598
				hgt += halfWid;
3599

3600
			if (s.max == 0) {
3601
				top -= halfWid;
3602
				hgt += halfWid;
3603
			}
3604

3605
			boundsClip = new Path2D();
3606
			boundsClip.rect(lft, top, wid, hgt);
3607
		}
3608

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

3615
		_pxAlign && ctx.translate(-offset, -offset);
3616
	}
3617

3618
	function fillStroke(si, strokeStyle, lineWidth, lineDash, lineCap, fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip) {
3619
		let didStrokeFill = false;
3620

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

3629
				let bandClip = (lowerEdge._paths || EMPTY_OBJ).band;
3630
				let gapsClip2;
3631

3632
				let _fillStyle = null;
3633

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

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

3644
				didStrokeFill = true;
3645
			}
3646
		});
3647

3648
		if (!didStrokeFill)
3649
			strokeFill(strokeStyle, lineWidth, lineDash, lineCap, fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip);
3650
	}
3651

3652
	const CLIP_FILL_STROKE = BAND_CLIP_FILL | BAND_CLIP_STROKE;
3653

3654
	function strokeFill(strokeStyle, lineWidth, lineDash, lineCap, fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip, gapsClip2, bandClip) {
3655
		setCtxStyle(strokeStyle, lineWidth, lineDash, lineCap, fillStyle);
3656

3657
		if (boundsClip || gapsClip || bandClip) {
3658
			ctx.save();
3659
			boundsClip && ctx.clip(boundsClip);
3660
			gapsClip && ctx.clip(gapsClip);
3661
		}
3662

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

3689
		if (boundsClip || gapsClip || bandClip)
3690
			ctx.restore();
3691
	}
3692

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

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

3717
	function getIncrSpace(axisIdx, min, max, fullDim) {
3718
		let axis = axes[axisIdx];
3719

3720
		let incrSpace;
3721

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

3730
		return (axis._found = incrSpace);
3731
	}
3732

3733
	function drawOrthoLines(offs, filts, ori, side, pos0, len, width, stroke, dash, cap) {
3734
		let offset = (width % 2) / 2;
3735

3736
		pxAlign == 1 && ctx.translate(offset, offset);
3737

3738
		setCtxStyle(stroke, width, dash, cap, stroke);
3739

3740
		ctx.beginPath();
3741

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

3744
		if (ori == 0) {
3745
			y0 = pos0;
3746
			y1 = pos1;
3747
		}
3748
		else {
3749
			x0 = pos0;
3750
			x1 = pos1;
3751
		}
3752

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

3760
				ctx.moveTo(x0, y0);
3761
				ctx.lineTo(x1, y1);
3762
			}
3763
		}
3764

3765
		ctx.stroke();
3766

3767
		pxAlign == 1 && ctx.translate(-offset, -offset);
3768
	}
3769

3770
	function axesCalc(cycleNum) {
3771
	//	log("axesCalc()", arguments);
3772

3773
		let converged = true;
3774

3775
		axes.forEach((axis, i) => {
3776
			if (!axis.show)
3777
				return;
3778

3779
			let scale = scales[axis.scale];
3780

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

3797
			let side = axis.side;
3798
			let ori = side % 2;
3799

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

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

3804
			if (_space == 0)
3805
				return;
3806

3807
			// if we're using index positions, force first tick to match passed index
3808
			let forceMin = scale.distr == 2;
3809

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

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

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

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

3822
			let oldSize = axis._size;
3823

3824
			axis._size = ceil(axis.size(self, values, i, cycleNum));
3825

3826
			if (oldSize != null && axis._size != oldSize)			// ready && ?
3827
				converged = false;
3828
		});
3829

3830
		return converged;
3831
	}
3832

3833
	function paddingCalc(cycleNum) {
3834
		let converged = true;
3835

3836
		padding.forEach((p, i) => {
3837
			let _p = p(self, i, sidesWithAxes, cycleNum);
3838

3839
			if (_p != _padding[i])
3840
				converged = false;
3841

3842
			_padding[i] = _p;
3843
		});
3844

3845
		return converged;
3846
	}
3847

3848
	function drawAxesGrid() {
3849
		for (let i = 0; i < axes.length; i++) {
3850
			let axis = axes[i];
3851

3852
			if (!axis.show || !axis._show)
3853
				continue;
3854

3855
			let side = axis.side;
3856
			let ori = side % 2;
3857

3858
			let x, y;
3859

3860
			let fillStyle = axis.stroke(self, i);
3861

3862
			let shiftDir = side == 0 || side == 3 ? -1 : 1;
3863

3864
			// axis label
3865
			if (axis.label) {
3866
				let shiftAmt = axis.labelGap * shiftDir;
3867
				let baseLpos = round((axis._lpos + shiftAmt) * pxRatio);
3868

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

3871
				ctx.save();
3872

3873
				if (ori == 1) {
3874
					x = y = 0;
3875

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

3882
				}
3883
				else {
3884
					x = round(plotLft + plotWid / 2);
3885
					y = baseLpos;
3886
				}
3887

3888
				ctx.fillText(axis.label, x, y);
3889

3890
				ctx.restore();
3891
			}
3892

3893
			let [_incr, _space] = axis._found;
3894

3895
			if (_space == 0)
3896
				continue;
3897

3898
			let scale = scales[axis.scale];
3899

3900
			let plotDim = ori == 0 ? plotWid : plotHgt;
3901
			let plotOff = ori == 0 ? plotLft : plotTop;
3902

3903
			let axisGap = round(axis.gap * pxRatio);
3904

3905
			let _splits = axis._splits;
3906

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

3912
			let ticks = axis.ticks;
3913
			let tickSize = ticks.show ? round(ticks.size * pxRatio) : 0;
3914

3915
			// rotating of labels only supported on bottom x axis
3916
			let angle = axis._rotate * -PI/180;
3917

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

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

3933
			setFontStyle(font, fillStyle, textAlign, textBaseline);
3934

3935
			let lineHeight = axis.font[1] * lineMult;
3936

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

3939
			let _values = axis._values;
3940

3941
			for (let i = 0; i < _values.length; i++) {
3942
				let val = _values[i];
3943

3944
				if (val != null) {
3945
					if (ori == 0)
3946
						x = canOffs[i];
3947
					else
3948
						y = canOffs[i];
3949

3950
					val = "" + val;
3951

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

3954
					for (let j = 0; j < _parts.length; j++) {
3955
						let text = _parts[j];
3956

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

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

3986
			// grid
3987
			let grid = axis.grid;
3988

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

4005
		fire("drawAxes");
4006
	}
4007

4008
	function resetYSeries(minMax) {
4009
	//	log("resetYSeries()", arguments);
4010

4011
		series.forEach((s, i) => {
4012
			if (i > 0) {
4013
				s._paths = null;
4014

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

4031
	let queuedCommit = false;
4032

4033
	function commit() {
4034
		if (!queuedCommit) {
4035
			microTask(_commit);
4036
			queuedCommit = true;
4037
		}
4038
	}
4039

4040
	function _commit() {
4041
	//	log("_commit()", arguments);
4042

4043
		if (shouldSetScales) {
4044
			setScales();
4045
			shouldSetScales = false;
4046
		}
4047

4048
		if (shouldConvergeSize) {
4049
			convergeSize();
4050
			shouldConvergeSize = false;
4051
		}
4052

4053
		if (shouldSetSize) {
4054
			setStylePx(under, LEFT,   plotLftCss);
4055
			setStylePx(under, TOP,    plotTopCss);
4056
			setStylePx(under, WIDTH,  plotWidCss);
4057
			setStylePx(under, HEIGHT, plotHgtCss);
4058

4059
			setStylePx(over, LEFT,    plotLftCss);
4060
			setStylePx(over, TOP,     plotTopCss);
4061
			setStylePx(over, WIDTH,   plotWidCss);
4062
			setStylePx(over, HEIGHT,  plotHgtCss);
4063

4064
			setStylePx(wrap, WIDTH,   fullWidCss);
4065
			setStylePx(wrap, HEIGHT,  fullHgtCss);
4066

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

4072

4073
			axes.forEach(a => {
4074
				let { _show, _el, _size, _pos, side } = a;
4075

4076
				if (_show) {
4077
					let posOffset = (side === 3 || side === 0 ? _size : 0);
4078
					let isVt = side % 2 == 1;
4079

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

4085
					_el && remClass(_el, OFF);
4086
				}
4087
				else
4088
					_el && addClass(_el, OFF);
4089
			});
4090

4091
			// invalidate ctx style cache
4092
			ctxStroke = ctxFill = ctxWidth = ctxJoin = ctxCap = ctxFont = ctxAlign = ctxBaseline = ctxDash = null;
4093
			ctxAlpha = 1;
4094

4095
			syncRect(false);
4096

4097
			fire("setSize");
4098

4099
			shouldSetSize = false;
4100
		}
4101

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

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

4118
		if (cursor.show && shouldSetCursor) {
4119
			updateCursor(null, true, false);
4120
			shouldSetCursor = false;
4121
		}
4122

4123
	//	if (FEAT_LEGEND && legend.show && legend.live && shouldSetLegend) {}
4124

4125
		if (!ready) {
4126
			ready = true;
4127
			self.status = 1;
4128

4129
			fire("ready");
4130
		}
4131

4132
		viaAutoScaleX = false;
4133

4134
		queuedCommit = false;
4135
	}
4136

4137
	self.redraw = (rebuildPaths, recalcAxes) => {
4138
		shouldConvergeSize = recalcAxes || false;
4139

4140
		if (rebuildPaths !== false)
4141
			_setScale(xScaleKey, scaleX.min, scaleX.max);
4142
		else
4143
			commit();
4144
	};
4145

4146
	// redraw() => setScale('x', scales.x.min, scales.x.max);
4147

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

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

4159
			if (opts.min > opts.max) {
4160
				let _min = opts.min;
4161
				opts.min = opts.max;
4162
				opts.max = _min;
4163
			}
4164

4165
			if (dataLen > 1 && opts.min != null && opts.max != null && opts.max - opts.min < 1e-16)
4166
				return;
4167

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

4173
					if (opts.min == opts.max)
4174
						opts.max++;
4175
				}
4176
			}
4177

4178
		//	log("setScale()", arguments);
4179

4180
			pendScales[key] = opts;
4181

4182
			shouldSetScales = true;
4183
			commit();
4184
		}
4185
	}
4186

4187
	self.setScale = setScale;
4188

4189
//	INTERACTION
4190

4191
	let xCursor;
4192
	let yCursor;
4193
	let vCursor;
4194
	let hCursor;
4195

4196
	// starting position before cursor.move
4197
	let rawMouseLeft0;
4198
	let rawMouseTop0;
4199

4200
	// starting position
4201
	let mouseLeft0;
4202
	let mouseTop0;
4203

4204
	// current position before cursor.move
4205
	let rawMouseLeft1;
4206
	let rawMouseTop1;
4207

4208
	// current position
4209
	let mouseLeft1;
4210
	let mouseTop1;
4211

4212
	let dragging = false;
4213

4214
	const drag = cursor.drag;
4215

4216
	let dragX = drag.x;
4217
	let dragY = drag.y;
4218

4219
	if (cursor.show) {
4220
		if (cursor.x)
4221
			xCursor = placeDiv(CURSOR_X, over);
4222
		if (cursor.y)
4223
			yCursor = placeDiv(CURSOR_Y, over);
4224

4225
		if (scaleX.ori == 0) {
4226
			vCursor = xCursor;
4227
			hCursor = yCursor;
4228
		}
4229
		else {
4230
			vCursor = yCursor;
4231
			hCursor = xCursor;
4232
		}
4233

4234
		mouseLeft1 = cursor.left;
4235
		mouseTop1 = cursor.top;
4236
	}
4237

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

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

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

4254
			_fire !== false && fire("setSelect");
4255
		}
4256
	}
4257

4258
	self.setSelect = setSelect;
4259

4260
	function toggleDOM(i, onOff) {
4261
		let s = series[i];
4262
		let label = showLegend ? legendRows[i] : null;
4263

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

4272
	function _setScale(key, min, max) {
4273
		setScale(key, {min, max});
4274
	}
4275

4276
	function setSeries(i, opts, _fire, _pub) {
4277
	//	log("setSeries()", arguments);
4278

4279
		let s = series[i];
4280

4281
		if (opts.focus != null)
4282
			setFocus(i);
4283

4284
		if (opts.show != null) {
4285
			s.show = opts.show;
4286
			toggleDOM(i, opts.show);
4287

4288
			_setScale(mode == 2 ? s.facets[1].scale : s.scale, null, null);
4289
			commit();
4290
		}
4291

4292
		_fire !== false && fire("setSeries", i, opts);
4293

4294
		_pub && pubSync("setSeries", self, i, opts);
4295
	}
4296

4297
	self.setSeries = setSeries;
4298

4299
	function setBand(bi, opts) {
4300
		assign(bands[bi], opts);
4301
	}
4302

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

4309
	function delBand(bi) {
4310
		if (bi == null)
4311
			bands.length = 0;
4312
		else
4313
			bands.splice(bi, 1);
4314
	}
4315

4316
	self.addBand = addBand;
4317
	self.setBand = setBand;
4318
	self.delBand = delBand;
4319

4320
	function setAlpha(i, value) {
4321
		series[i].alpha = value;
4322

4323
		if (cursor.show && cursorPts[i])
4324
			cursorPts[i].style.opacity = value;
4325

4326
		if (showLegend && legendRows[i])
4327
			legendRows[i].style.opacity = value;
4328
	}
4329

4330
	// y-distance
4331
	let closestDist;
4332
	let closestSeries;
4333
	let focusedSeries;
4334
	const FOCUS_TRUE  = {focus: true};
4335
	const FOCUS_FALSE = {focus: false};
4336

4337
	function setFocus(i) {
4338
		if (i != focusedSeries) {
4339
		//	log("setFocus()", arguments);
4340

4341
			let allFocused = i == null;
4342

4343
			let _setAlpha = focus.alpha != 1;
4344

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

4351
			focusedSeries = i;
4352
			_setAlpha && commit();
4353
		}
4354
	}
4355

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

4365
	function posToVal(pos, scale, can) {
4366
		let sc = scales[scale];
4367

4368
		if (can)
4369
			pos = pos / pxRatio - (sc.ori == 1 ? plotTopCss : plotLftCss);
4370

4371
		let dim = plotWidCss;
4372

4373
		if (sc.ori == 1) {
4374
			dim = plotHgtCss;
4375
			pos = dim - pos;
4376
		}
4377

4378
		if (sc.dir == -1)
4379
			pos = dim - pos;
4380

4381
		let _min = sc._min,
4382
			_max = sc._max,
4383
			pct = pos / dim;
4384

4385
		let sv = _min + (_max - _min) * pct;
4386

4387
		let distr = sc.distr;
4388

4389
		return (
4390
			distr == 3 ? pow(10, sv) :
4391
			distr == 4 ? sinh(sv, sc.asinh) :
4392
			sv
4393
		);
4394
	}
4395

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

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

4416
	// defers calling expensive functions
4417
	function batch(fn) {
4418
		fn(self);
4419
		commit();
4420
	}
4421

4422
	self.batch = batch;
4423

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

4431
	function setSelH(off, dim) {
4432
		setStylePx(selectDiv, LEFT,  select.left = off);
4433
		setStylePx(selectDiv, WIDTH, select.width = dim);
4434
	}
4435

4436
	function setSelV(off, dim) {
4437
		setStylePx(selectDiv, TOP,    select.top = off);
4438
		setStylePx(selectDiv, HEIGHT, select.height = dim);
4439
	}
4440

4441
	let setSelX = scaleX.ori == 0 ? setSelH : setSelV;
4442
	let setSelY = scaleX.ori == 1 ? setSelH : setSelV;
4443

4444
	function syncLegend() {
4445
		if (showLegend && legend.live) {
4446
			for (let i = mode == 2 ? 1 : 0; i < series.length; i++) {
4447
				if (i == 0 && multiValLegend)
4448
					continue;
4449

4450
				let vals = legend.values[i];
4451

4452
				let j = 0;
4453

4454
				for (let k in vals)
4455
					legendCells[i][j++].firstChild.nodeValue = vals[k];
4456
			}
4457
		}
4458
	}
4459

4460
	function setLegend(opts, _fire) {
4461
		if (opts != null) {
4462
			let idx = opts.idx;
4463

4464
			legend.idx = idx;
4465
			series.forEach((s, sidx) => {
4466
				(sidx > 0 || !multiValLegend) && setLegendValues(sidx, idx);
4467
			});
4468
		}
4469

4470
		if (showLegend && legend.live)
4471
			syncLegend();
4472

4473
		shouldSetLegend = false;
4474

4475
		_fire !== false && fire("setLegend");
4476
	}
4477

4478
	self.setLegend = setLegend;
4479

4480
	function setLegendValues(sidx, idx) {
4481
		let val;
4482

4483
		if (idx == null)
4484
			val = NULL_LEGEND_VALUES;
4485
		else {
4486
			let s = series[sidx];
4487
			let src = sidx == 0 && xScaleDistr == 2 ? data0 : data[sidx];
4488
			val = multiValLegend ? s.values(self, sidx, idx) : {_: s.value(self, src[idx], sidx, idx)};
4489
		}
4490

4491
		legend.values[sidx] = val;
4492
	}
4493

4494
	function updateCursor(src, _fire, _pub) {
4495
	//	ts == null && log("updateCursor()", arguments);
4496

4497
		rawMouseLeft1 = mouseLeft1;
4498
		rawMouseTop1 = mouseTop1;
4499

4500
		[mouseLeft1, mouseTop1] = cursor.move(self, mouseLeft1, mouseTop1);
4501

4502
		if (cursor.show) {
4503
			vCursor && elTrans(vCursor, round(mouseLeft1), 0, plotWidCss, plotHgtCss);
4504
			hCursor && elTrans(hCursor, 0, round(mouseTop1), plotWidCss, plotHgtCss);
4505
		}
4506

4507
		let idx;
4508

4509
		// when zooming to an x scale range between datapoints the binary search
4510
		// for nearest min/max indices results in this condition. cheap hack :D
4511
		let noDataInRange = i0 > i1; // works for mode 1 only
4512

4513
		closestDist = inf;
4514

4515
		// TODO: extract
4516
		let xDim = scaleX.ori == 0 ? plotWidCss : plotHgtCss;
4517
		let yDim = scaleX.ori == 1 ? plotWidCss : plotHgtCss;
4518

4519
		// if cursor hidden, hide points & clear legend vals
4520
		if (mouseLeft1 < 0 || dataLen == 0 || noDataInRange) {
4521
			idx = null;
4522

4523
			for (let i = 0; i < series.length; i++) {
4524
				if (i > 0) {
4525
					cursorPts.length > 1 && elTrans(cursorPts[i], -10, -10, plotWidCss, plotHgtCss);
4526
				}
4527
			}
4528

4529
			if (cursorFocus)
4530
				setSeries(null, FOCUS_TRUE, true, src == null && syncOpts.setSeries);
4531

4532
			if (legend.live) {
4533
				activeIdxs.fill(null);
4534
				shouldSetLegend = true;
4535

4536
				for (let i = 0; i < series.length; i++)
4537
					legend.values[i] = NULL_LEGEND_VALUES;
4538
			}
4539
		}
4540
		else {
4541
		//	let pctY = 1 - (y / rect.height);
4542

4543
			let mouseXPos, valAtPosX, xPos;
4544

4545
			if (mode == 1) {
4546
				mouseXPos = scaleX.ori == 0 ? mouseLeft1 : mouseTop1;
4547
				valAtPosX = posToVal(mouseXPos, xScaleKey);
4548
				idx = closestIdx(valAtPosX, data[0], i0, i1);
4549
				xPos = incrRoundUp(valToPosX(data[0][idx], scaleX, xDim, 0), 0.5);
4550
			}
4551

4552
			for (let i = mode == 2 ? 1 : 0; i < series.length; i++) {
4553
				let s = series[i];
4554

4555
				let idx1  = activeIdxs[i];
4556
				let yVal1 = mode == 1 ? data[i][idx1] : data[i][1][idx1];
4557

4558
				let idx2  = cursor.dataIdx(self, i, idx, valAtPosX);
4559
				let yVal2 = mode == 1 ? data[i][idx2] : data[i][1][idx2];
4560

4561
				shouldSetLegend = shouldSetLegend || yVal2 != yVal1 || idx2 != idx1;
4562

4563
				activeIdxs[i] = idx2;
4564

4565
				let xPos2 = idx2 == idx ? xPos : incrRoundUp(valToPosX(mode == 1 ? data[0][idx2] : data[i][0][idx2], scaleX, xDim, 0), 0.5);
4566

4567
				if (i > 0 && s.show) {
4568
					let yPos = yVal2 == null ? -10 : incrRoundUp(valToPosY(yVal2, mode == 1 ? scales[s.scale] : scales[s.facets[1].scale], yDim, 0), 0.5);
4569

4570
					if (yPos > 0 && mode == 1) {
4571
						let dist = abs(yPos - mouseTop1);
4572

4573
						if (dist <= closestDist) {
4574
							closestDist = dist;
4575
							closestSeries = i;
4576
						}
4577
					}
4578

4579
					let hPos, vPos;
4580

4581
					if (scaleX.ori == 0) {
4582
						hPos = xPos2;
4583
						vPos = yPos;
4584
					}
4585
					else {
4586
						hPos = yPos;
4587
						vPos = xPos2;
4588
					}
4589

4590
					if (shouldSetLegend && cursorPts.length > 1) {
4591
						elColor(cursorPts[i], cursor.points.fill(self, i), cursor.points.stroke(self, i));
4592

4593
						let ptWid, ptHgt, ptLft, ptTop,
4594
							centered = true,
4595
							getBBox = cursor.points.bbox;
4596

4597
						if (getBBox != null) {
4598
							centered = false;
4599

4600
							let bbox = getBBox(self, i);
4601

4602
							ptLft = bbox.left;
4603
							ptTop = bbox.top;
4604
							ptWid = bbox.width;
4605
							ptHgt = bbox.height;
4606
						}
4607
						else {
4608
							ptLft = hPos;
4609
							ptTop = vPos;
4610
							ptWid = ptHgt = cursor.points.size(self, i);
4611
						}
4612

4613
						elSize(cursorPts[i], ptWid, ptHgt, centered);
4614
						elTrans(cursorPts[i], ptLft, ptTop, plotWidCss, plotHgtCss);
4615
					}
4616
				}
4617

4618
				if (legend.live) {
4619
					if (!shouldSetLegend || i == 0 && multiValLegend)
4620
						continue;
4621

4622
					setLegendValues(i, idx2);
4623
				}
4624
			}
4625
		}
4626

4627
		cursor.idx = idx;
4628
		cursor.left = mouseLeft1;
4629
		cursor.top = mouseTop1;
4630

4631
		if (shouldSetLegend) {
4632
			legend.idx = idx;
4633
			setLegend();
4634
		}
4635

4636
		// nit: cursor.drag.setSelect is assumed always true
4637
		if (select.show && dragging) {
4638
			if (src != null) {
4639
				let [xKey, yKey] = syncOpts.scales;
4640
				let [matchXKeys, matchYKeys] = syncOpts.match;
4641
				let [xKeySrc, yKeySrc] = src.cursor.sync.scales;
4642

4643
				// match the dragX/dragY implicitness/explicitness of src
4644
				let sdrag = src.cursor.drag;
4645
				dragX = sdrag._x;
4646
				dragY = sdrag._y;
4647

4648
				let { left, top, width, height } = src.select;
4649

4650
				let sori = src.scales[xKey].ori;
4651
				let sPosToVal = src.posToVal;
4652

4653
				let sOff, sDim, sc, a, b;
4654

4655
				let matchingX = xKey != null && matchXKeys(xKey, xKeySrc);
4656
				let matchingY = yKey != null && matchYKeys(yKey, yKeySrc);
4657

4658
				if (matchingX) {
4659
					if (sori == 0) {
4660
						sOff = left;
4661
						sDim = width;
4662
					}
4663
					else {
4664
						sOff = top;
4665
						sDim = height;
4666
					}
4667

4668
					if (dragX) {
4669
						sc = scales[xKey];
4670

4671
						a = valToPosX(sPosToVal(sOff, xKeySrc),        sc, xDim, 0);
4672
						b = valToPosX(sPosToVal(sOff + sDim, xKeySrc), sc, xDim, 0);
4673

4674
						setSelX(min(a,b), abs(b-a));
4675
					}
4676
					else
4677
						setSelX(0, xDim);
4678

4679
					if (!matchingY)
4680
						setSelY(0, yDim);
4681
				}
4682

4683
				if (matchingY) {
4684
					if (sori == 1) {
4685
						sOff = left;
4686
						sDim = width;
4687
					}
4688
					else {
4689
						sOff = top;
4690
						sDim = height;
4691
					}
4692

4693
					if (dragY) {
4694
						sc = scales[yKey];
4695

4696
						a = valToPosY(sPosToVal(sOff, yKeySrc),        sc, yDim, 0);
4697
						b = valToPosY(sPosToVal(sOff + sDim, yKeySrc), sc, yDim, 0);
4698

4699
						setSelY(min(a,b), abs(b-a));
4700
					}
4701
					else
4702
						setSelY(0, yDim);
4703

4704
					if (!matchingX)
4705
						setSelX(0, xDim);
4706
				}
4707
			}
4708
			else {
4709
				let rawDX = abs(rawMouseLeft1 - rawMouseLeft0);
4710
				let rawDY = abs(rawMouseTop1 - rawMouseTop0);
4711

4712
				if (scaleX.ori == 1) {
4713
					let _rawDX = rawDX;
4714
					rawDX = rawDY;
4715
					rawDY = _rawDX;
4716
				}
4717

4718
				dragX = drag.x && rawDX >= drag.dist;
4719
				dragY = drag.y && rawDY >= drag.dist;
4720

4721
				let uni = drag.uni;
4722

4723
				if (uni != null) {
4724
					// only calc drag status if they pass the dist thresh
4725
					if (dragX && dragY) {
4726
						dragX = rawDX >= uni;
4727
						dragY = rawDY >= uni;
4728

4729
						// force unidirectionality when both are under uni limit
4730
						if (!dragX && !dragY) {
4731
							if (rawDY > rawDX)
4732
								dragY = true;
4733
							else
4734
								dragX = true;
4735
						}
4736
					}
4737
				}
4738
				else if (drag.x && drag.y && (dragX || dragY))
4739
					// if omni with no uni then both dragX / dragY should be true if either is true
4740
					dragX = dragY = true;
4741

4742
				let p0, p1;
4743

4744
				if (dragX) {
4745
					if (scaleX.ori == 0) {
4746
						p0 = mouseLeft0;
4747
						p1 = mouseLeft1;
4748
					}
4749
					else {
4750
						p0 = mouseTop0;
4751
						p1 = mouseTop1;
4752
					}
4753

4754
					setSelX(min(p0, p1), abs(p1 - p0));
4755

4756
					if (!dragY)
4757
						setSelY(0, yDim);
4758
				}
4759

4760
				if (dragY) {
4761
					if (scaleX.ori == 1) {
4762
						p0 = mouseLeft0;
4763
						p1 = mouseLeft1;
4764
					}
4765
					else {
4766
						p0 = mouseTop0;
4767
						p1 = mouseTop1;
4768
					}
4769

4770
					setSelY(min(p0, p1), abs(p1 - p0));
4771

4772
					if (!dragX)
4773
						setSelX(0, xDim);
4774
				}
4775

4776
				// the drag didn't pass the dist requirement
4777
				if (!dragX && !dragY) {
4778
					setSelX(0, 0);
4779
					setSelY(0, 0);
4780
				}
4781
			}
4782
		}
4783

4784
		drag._x = dragX;
4785
		drag._y = dragY;
4786

4787
		if (src == null) {
4788
			if (_pub) {
4789
				if (syncKey != null) {
4790
					let [xSyncKey, ySyncKey] = syncOpts.scales;
4791

4792
					syncOpts.values[0] = xSyncKey != null ? posToVal(scaleX.ori == 0 ? mouseLeft1 : mouseTop1, xSyncKey) : null;
4793
					syncOpts.values[1] = ySyncKey != null ? posToVal(scaleX.ori == 1 ? mouseLeft1 : mouseTop1, ySyncKey) : null;
4794
				}
4795

4796
				pubSync(mousemove, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, idx);
4797
			}
4798

4799
			if (cursorFocus) {
4800
				let shouldPub = _pub && syncOpts.setSeries;
4801
				let p = focus.prox;
4802

4803
				if (focusedSeries == null) {
4804
					if (closestDist <= p)
4805
						setSeries(closestSeries, FOCUS_TRUE, true, shouldPub);
4806
				}
4807
				else {
4808
					if (closestDist > p)
4809
						setSeries(null, FOCUS_TRUE, true, shouldPub);
4810
					else if (closestSeries != focusedSeries)
4811
						setSeries(closestSeries, FOCUS_TRUE, true, shouldPub);
4812
				}
4813
			}
4814
		}
4815

4816
		ready && _fire !== false && fire("setCursor");
4817
	}
4818

4819
	let rect = null;
4820

4821
	function syncRect(defer) {
4822
		if (defer === true)
4823
			rect = null;
4824
		else {
4825
			rect = over.getBoundingClientRect();
4826
			fire("syncRect", rect);
4827
		}
4828
	}
4829

4830
	function mouseMove(e, src, _l, _t, _w, _h, _i) {
4831
		if (cursor._lock)
4832
			return;
4833

4834
		cacheMouse(e, src, _l, _t, _w, _h, _i, false, e != null);
4835

4836
		if (e != null)
4837
			updateCursor(null, true, true);
4838
		else
4839
			updateCursor(src, true, false);
4840
	}
4841

4842
	function cacheMouse(e, src, _l, _t, _w, _h, _i, initial, snap) {
4843
		if (rect == null)
4844
			syncRect(false);
4845

4846
		if (e != null) {
4847
			_l = e.clientX - rect.left;
4848
			_t = e.clientY - rect.top;
4849
		}
4850
		else {
4851
			if (_l < 0 || _t < 0) {
4852
				mouseLeft1 = -10;
4853
				mouseTop1 = -10;
4854
				return;
4855
			}
4856

4857
			let [xKey, yKey] = syncOpts.scales;
4858

4859
			let syncOptsSrc = src.cursor.sync;
4860
			let [xValSrc, yValSrc] = syncOptsSrc.values;
4861
			let [xKeySrc, yKeySrc] = syncOptsSrc.scales;
4862
			let [matchXKeys, matchYKeys] = syncOpts.match;
4863

4864
			let rotSrc = src.scales[xKeySrc].ori == 1;
4865

4866
			let xDim = scaleX.ori == 0 ? plotWidCss : plotHgtCss,
4867
				yDim = scaleX.ori == 1 ? plotWidCss : plotHgtCss,
4868
				_xDim = rotSrc ? _h : _w,
4869
				_yDim = rotSrc ? _w : _h,
4870
				_xPos = rotSrc ? _t : _l,
4871
				_yPos = rotSrc ? _l : _t;
4872

4873
			if (xKeySrc != null)
4874
				_l = matchXKeys(xKey, xKeySrc) ? getPos(xValSrc, scales[xKey], xDim, 0) : -10;
4875
			else
4876
				_l = xDim * (_xPos/_xDim);
4877

4878
			if (yKeySrc != null)
4879
				_t = matchYKeys(yKey, yKeySrc) ? getPos(yValSrc, scales[yKey], yDim, 0) : -10;
4880
			else
4881
				_t = yDim * (_yPos/_yDim);
4882

4883
			if (scaleX.ori == 1) {
4884
				let __l = _l;
4885
				_l = _t;
4886
				_t = __l;
4887
			}
4888
		}
4889

4890
		if (snap) {
4891
			if (_l <= 1 || _l >= plotWidCss - 1)
4892
				_l = incrRound(_l, plotWidCss);
4893

4894
			if (_t <= 1 || _t >= plotHgtCss - 1)
4895
				_t = incrRound(_t, plotHgtCss);
4896
		}
4897

4898
		if (initial) {
4899
			rawMouseLeft0 = _l;
4900
			rawMouseTop0 = _t;
4901

4902
			[mouseLeft0, mouseTop0] = cursor.move(self, _l, _t);
4903
		}
4904
		else {
4905
			mouseLeft1 = _l;
4906
			mouseTop1 = _t;
4907
		}
4908
	}
4909

4910
	function hideSelect() {
4911
		setSelect({
4912
			width: 0,
4913
			height: 0,
4914
		}, false);
4915
	}
4916

4917
	function mouseDown(e, src, _l, _t, _w, _h, _i) {
4918
		dragging = true;
4919
		dragX = dragY = drag._x = drag._y = false;
4920

4921
		cacheMouse(e, src, _l, _t, _w, _h, _i, true, false);
4922

4923
		if (e != null) {
4924
			onMouse(mouseup, doc, mouseUp);
4925
			pubSync(mousedown, self, mouseLeft0, mouseTop0, plotWidCss, plotHgtCss, null);
4926
		}
4927
	}
4928

4929
	function mouseUp(e, src, _l, _t, _w, _h, _i) {
4930
		dragging = drag._x = drag._y = false;
4931

4932
		cacheMouse(e, src, _l, _t, _w, _h, _i, false, true);
4933

4934
		let { left, top, width, height } = select;
4935

4936
		let hasSelect = width > 0 || height > 0;
4937

4938
		hasSelect && setSelect(select);
4939

4940
		if (drag.setScale && hasSelect) {
4941
		//	if (syncKey != null) {
4942
		//		dragX = drag.x;
4943
		//		dragY = drag.y;
4944
		//	}
4945

4946
			let xOff = left,
4947
				xDim = width,
4948
				yOff = top,
4949
				yDim = height;
4950

4951
			if (scaleX.ori == 1) {
4952
				xOff = top,
4953
				xDim = height,
4954
				yOff = left,
4955
				yDim = width;
4956
			}
4957

4958
			if (dragX) {
4959
				_setScale(xScaleKey,
4960
					posToVal(xOff, xScaleKey),
4961
					posToVal(xOff + xDim, xScaleKey)
4962
				);
4963
			}
4964

4965
			if (dragY) {
4966
				for (let k in scales) {
4967
					let sc = scales[k];
4968

4969
					if (k != xScaleKey && sc.from == null && sc.min != inf) {
4970
						_setScale(k,
4971
							posToVal(yOff + yDim, k),
4972
							posToVal(yOff, k)
4973
						);
4974
					}
4975
				}
4976
			}
4977

4978
			hideSelect();
4979
		}
4980
		else if (cursor.lock) {
4981
			cursor._lock = !cursor._lock;
4982

4983
			if (!cursor._lock)
4984
				updateCursor(null, true, false);
4985
		}
4986

4987
		if (e != null) {
4988
			offMouse(mouseup, doc);
4989
			pubSync(mouseup, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null);
4990
		}
4991
	}
4992

4993
	function mouseLeave(e, src, _l, _t, _w, _h, _i) {
4994
		if (!cursor._lock) {
4995
			let _dragging = dragging;
4996

4997
			if (dragging) {
4998
				// handle case when mousemove aren't fired all the way to edges by browser
4999
				let snapH = true;
5000
				let snapV = true;
5001
				let snapProx = 10;
5002

5003
				let dragH, dragV;
5004

5005
				if (scaleX.ori == 0) {
5006
					dragH = dragX;
5007
					dragV = dragY;
5008
				}
5009
				else {
5010
					dragH = dragY;
5011
					dragV = dragX;
5012
				}
5013

5014
				if (dragH && dragV) {
5015
					// maybe omni corner snap
5016
					snapH = mouseLeft1 <= snapProx || mouseLeft1 >= plotWidCss - snapProx;
5017
					snapV = mouseTop1  <= snapProx || mouseTop1  >= plotHgtCss - snapProx;
5018
				}
5019

5020
				if (dragH && snapH)
5021
					mouseLeft1 = mouseLeft1 < mouseLeft0 ? 0 : plotWidCss;
5022

5023
				if (dragV && snapV)
5024
					mouseTop1 = mouseTop1 < mouseTop0 ? 0 : plotHgtCss;
5025

5026
				updateCursor(null, true, true);
5027

5028
				dragging = false;
5029
			}
5030

5031
			mouseLeft1 = -10;
5032
			mouseTop1 = -10;
5033

5034
			// passing a non-null timestamp to force sync/mousemove event
5035
			updateCursor(null, true, true);
5036

5037
			if (_dragging)
5038
				dragging = _dragging;
5039
		}
5040
	}
5041

5042
	function dblClick(e, src, _l, _t, _w, _h, _i) {
5043
		autoScaleX();
5044

5045
		hideSelect();
5046

5047
		if (e != null)
5048
			pubSync(dblclick, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null);
5049
	}
5050

5051
	function syncPxRatio() {
5052
		axes.forEach(syncFontSize);
5053
		_setSize(self.width, self.height, true);
5054
	}
5055

5056
	on(dppxchange, win, syncPxRatio);
5057

5058
	// internal pub/sub
5059
	const events = {};
5060

5061
	events.mousedown = mouseDown;
5062
	events.mousemove = mouseMove;
5063
	events.mouseup = mouseUp;
5064
	events.dblclick = dblClick;
5065
	events["setSeries"] = (e, src, idx, opts) => {
5066
		setSeries(idx, opts, true, false);
5067
	};
5068

5069
	if (cursor.show) {
5070
		onMouse(mousedown,  over, mouseDown);
5071
		onMouse(mousemove,  over, mouseMove);
5072
		onMouse(mouseenter, over, syncRect);
5073
		onMouse(mouseleave, over, mouseLeave);
5074

5075
		onMouse(dblclick, over, dblClick);
5076

5077
		cursorPlots.add(self);
5078

5079
		self.syncRect = syncRect;
5080
	}
5081

5082
	// external on/off
5083
	const hooks = self.hooks = opts.hooks || {};
5084

5085
	function fire(evName, a1, a2) {
5086
		if (evName in hooks) {
5087
			hooks[evName].forEach(fn => {
5088
				fn.call(null, self, a1, a2);
5089
			});
5090
		}
5091
	}
5092

5093
	(opts.plugins || []).forEach(p => {
5094
		for (let evName in p.hooks)
5095
			hooks[evName] = (hooks[evName] || []).concat(p.hooks[evName]);
5096
	});
5097

5098
	const syncOpts = assign({
5099
		key: null,
5100
		setSeries: false,
5101
		filters: {
5102
			pub: retTrue,
5103
			sub: retTrue,
5104
		},
5105
		scales: [xScaleKey, series[1] ? series[1].scale : null],
5106
		match: [retEq, retEq],
5107
		values: [null, null],
5108
	}, cursor.sync);
5109

5110
	(cursor.sync = syncOpts);
5111

5112
	const syncKey = syncOpts.key;
5113

5114
	const sync = _sync(syncKey);
5115

5116
	function pubSync(type, src, x, y, w, h, i) {
5117
		if (syncOpts.filters.pub(type, src, x, y, w, h, i))
5118
			sync.pub(type, src, x, y, w, h, i);
5119
	}
5120

5121
	sync.sub(self);
5122

5123
	function pub(type, src, x, y, w, h, i) {
5124
		if (syncOpts.filters.sub(type, src, x, y, w, h, i))
5125
			events[type](null, src, x, y, w, h, i);
5126
	}
5127

5128
	(self.pub = pub);
5129

5130
	function destroy() {
5131
		sync.unsub(self);
5132
		cursorPlots.delete(self);
5133
		mouseListeners.clear();
5134
		off(dppxchange, win, syncPxRatio);
5135
		root.remove();
5136
		fire("destroy");
5137
	}
5138

5139
	self.destroy = destroy;
5140

5141
	function _init() {
5142
		fire("init", opts, data);
5143

5144
		setData(data || opts.data, false);
5145

5146
		if (pendScales[xScaleKey])
5147
			setScale(xScaleKey, pendScales[xScaleKey]);
5148
		else
5149
			autoScaleX();
5150

5151
		_setSize(opts.width, opts.height);
5152

5153
		updateCursor(null, true, false);
5154

5155
		setSelect(select, false);
5156
	}
5157

5158
	series.forEach(initSeries);
5159

5160
	axes.forEach(initAxis);
5161

5162
	if (then) {
5163
		if (then instanceof HTMLElement) {
5164
			then.appendChild(root);
5165
			_init();
5166
		}
5167
		else
5168
			then(self, _init);
5169
	}
5170
	else
5171
		_init();
5172

5173
	return self;
5174
}
5175

5176
uPlot.assign = assign;
5177
uPlot.fmtNum = fmtNum;
5178
uPlot.rangeNum = rangeNum;
5179
uPlot.rangeLog = rangeLog;
5180
uPlot.rangeAsinh = rangeAsinh;
5181
uPlot.orient   = orient;
5182

5183
{
5184
	uPlot.join = join;
5185
}
5186

5187
{
5188
	uPlot.fmtDate = fmtDate;
5189
	uPlot.tzDate  = tzDate;
5190
}
5191

5192
{
5193
	uPlot.sync = _sync;
5194
}
5195

5196
{
5197
	uPlot.addGap = addGap;
5198
	uPlot.clipGaps = clipGaps;
5199

5200
	let paths = uPlot.paths = {
5201
		points,
5202
	};
5203

5204
	(paths.linear  = linear);
5205
	(paths.stepped = stepped);
5206
	(paths.bars    = bars);
5207
	(paths.spline  = monotoneCubic);
5208
}
5209

5210
export { uPlot as default };
5211

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

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

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

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