GPQAPP

Форк
0
/
uPlot.cjs.js 
5212 строк · 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
'use strict';
11

12
const FEAT_TIME          = true;
13

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

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

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

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

33
	return hi;
34
}
35

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

42
	return -1;
43
}
44

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

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

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

68
	return [_min, _max];
69
}
70

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

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

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

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

90
const _fixedTuple = [0, 0];
91

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

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

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

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

114
	let minExp, maxExp, minMaxIncrs;
115

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

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

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

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

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

135
	return [min, max];
136
}
137

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

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

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

147
	return minMax;
148
}
149

150
const rangePad = 0.1;
151

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

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

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

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

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

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

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

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

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

198
	return false;
199
}
200

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

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

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

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

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

217
	let delta        = _max - _min;
218

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

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

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

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

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

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

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

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

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

258
	return [minLim, maxLim];
259
}
260

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

264
const M = Math;
265

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

281
const inf = Infinity;
282

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

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

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

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

299
const retArg0 = _0 => _0;
300

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

303
const retNull = _ => null;
304

305
const retTrue = _ => true;
306

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

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

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

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

321
const fixedDec = new Map();
322

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

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

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

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

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

345
	return incrs;
346
}
347

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

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

353
const nullNullTuple = [null, null];
354

355
const isArr = Array.isArray;
356

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

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

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

369
	return is;
370
}
371

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

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

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

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

398
	return out;
399
}
400

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

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

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

415
	return targ;
416
}
417

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

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

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

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

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

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

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

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

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

458
	let xIdxs = new Map();
459

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

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

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

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

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

474
			let nullIdxs = [];
475

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

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

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

492
			nullExpand(yVals, nullIdxs, alignedLen);
493

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

498
	return data;
499
}
500

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

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

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

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

524
const pre = "u-";
525

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

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

552
let query;
553

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

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

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

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

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

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

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

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

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

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

594
	return el;
595
}
596

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

601
const xformCache = new WeakMap();
602

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

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

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

618
const colorCache = new WeakMap();
619

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

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

631
const sizeCache = new WeakMap();
632

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

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

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

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

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

657
setPxRatio();
658

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

779
		return out;
780
	}
781
}
782

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

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

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

799
	return date2;
800
}
801

802
//export const series = [];
803

804
// default formatters:
805

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

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

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

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

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

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

821
const NL = "\n";
822

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

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

835
const _ = null;
836

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

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

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

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

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

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

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

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

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

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

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

955
				let date0 = tzDate(split);
956

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

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

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

966
					if (split > scaleMax)
967
						break;
968

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

974
						let dstShift = actualHour - expectedHour;
975

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

979
						split -= dstShift * h;
980

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

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

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

995
			return splits;
996
		}
997
	}
998

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1122
	let pt = placeDiv();
1123

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

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

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

1135
	return pt;
1136
}
1137

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

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

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

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

1157
const moveTuple = [0,0];
1158

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1287
	return splits;
1288
}
1289

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

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

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

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

1300
	foundIncr = pow(logBase, exp);
1301

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

1305
	let split = scaleMin;
1306

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

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

1314
	} while (split <= scaleMax);
1315

1316
	return splits;
1317
}
1318

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

1322
	let linthresh = sc.asinh;
1323

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

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

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

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

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

1344
	let valToPos = self.valToPos;
1345

1346
	let minSpace = axis._space;
1347

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1489
const syncs = {};
1490

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

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

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

1514
	return s;
1515
}
1516

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

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

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

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

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

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

1585
		let frIdx, toIdx;
1586

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

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

1604
		let clip = new Path2D(strokePath);
1605

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

1610
		return clip;
1611
	});
1612
}
1613

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

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

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

1623
		let prevGapEnd = plotLft;
1624

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

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

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

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

1637
		let w = plotLft + plotWid - prevGapEnd;
1638

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

1642
	return clip;
1643
}
1644

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

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

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

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

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

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

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

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

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

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

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

1708
			let moveTo, arc;
1709

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1784
			let lineTo, drawAcc;
1785

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

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

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

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

1804
			let gaps = [];
1805

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1874
					accX = x;
1875
				}
1876
			}
1877

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1934
			lineTo(stroke, firstXPos, prevYPos);
1935

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

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

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

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

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

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

1961
				lineTo(stroke, x1, y1);
1962

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2042
			let multiPath = false;
2043

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

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

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

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

2067
			let { x0, size } = disp;
2068

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

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

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

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

2083
				barWid = pxRound(barWid - strokeWidth);
2084

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

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

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

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

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

2112
				let gapWid = colWid * gapFactor;
2113

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

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

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

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

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

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

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

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

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

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

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

2159
				let r = radius * barWid;
2160

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

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

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

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

2190
					barHgt = btm - top;
2191

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

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

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

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

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

2211
			let moveTo, bezierCurveTo, lineTo;
2212

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2284
			return _paths;
2285

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

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

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

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

2313
	const path = new Path2D();
2314

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

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

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

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

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

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

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

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

2365
	return path;
2366
}
2367

2368
const cursorPlots = new Set();
2369

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

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

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

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

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

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

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

2399
const snapTimeX = snapNumX;
2400

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

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

2411
const snapLogX = snapLogY;
2412

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

2417
const snapAsinhX = snapAsinhY;
2418

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

2423
	let delta = maxVal - minVal;
2424

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

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

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

2435
	return [0, 0];
2436
}
2437

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

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

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

2459
	const mode = self.mode;
2460

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

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

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

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

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

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

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

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

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

2497
	addClass(root, opts.class);
2498

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

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

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

2512
	opts = copy(opts);
2513

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

2516
	const pxRound = pxRoundGen(pxAlign);
2517

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

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

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

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

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

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

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

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

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

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

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

2563
				sc.key = scaleKey;
2564

2565
				let isTime = sc.time;
2566

2567
				let rn = sc.range;
2568

2569
				let rangeIsArr = isArr(rn);
2570

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

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

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

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

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

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

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

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

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

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

2628
	const scaleX = scales[xScaleKey];
2629

2630
	const xScaleDistr = scaleX.distr;
2631

2632
	let valToPosX, valToPosY;
2633

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

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

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

2671
	const pendScales = {};
2672

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

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

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

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

2691
	const activeIdxs = [];
2692

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

2697
	{
2698
		legend.idxs = activeIdxs;
2699

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

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

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

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

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

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

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

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

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

2745
		let cells = [];
2746

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

2749
		addClass(row, s.class);
2750

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2810
		return [row, cells];
2811
	}
2812

2813
	const mouseListeners = new Map();
2814

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

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

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

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

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

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

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

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

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

2854
	self.bbox = {};
2855

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

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

2866
		resetYSeries(false);
2867

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

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

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

2881
		calcPlotRect();
2882
		calcAxesRects();
2883

2884
		let bb = self.bbox;
2885

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

2891
	//	updOriDims();
2892
	}
2893

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

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

2900
		let cycleNum = 0;
2901

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

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

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

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

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

2921
	self.setSize = setSize;
2922

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

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

2937
				let fullSize = _size + labelSize;
2938

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

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

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

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

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

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

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

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

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

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

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

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

3009
	{
3010
		cursor.idxs = activeIdxs;
3011

3012
		cursor._lock = false;
3013

3014
		let points = cursor.points;
3015

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

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

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

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

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

3039
				return pt;
3040
			}
3041
		}
3042
	}
3043

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

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

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

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

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

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

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

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

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

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

3105
	self.addSeries = addSeries;
3106

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

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

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

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

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

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

3128
	self.delSeries = delSeries;
3129

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

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

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

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

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

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

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

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

3159
			let av = axis.values;
3160

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

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

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

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

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

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

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

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

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

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

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

3211
		return size;
3212
	}
3213

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

3217
	let dataLen;
3218

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

3224
	let data0 = null;
3225

3226
	let viaAutoScaleX = false;
3227

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

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

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

3247
		self._data = data;
3248

3249
		resetYSeries(true);
3250

3251
		fire("setData");
3252

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

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

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

3267
	self.setData = setData;
3268

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

3272
		let _min, _max;
3273

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3477
				let distr = sc.distr;
3478

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

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

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

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

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

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

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

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

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

3523
		return [_i0, _i1];
3524
	}
3525

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

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

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

3545
					{
3546
						cacheStrokeFill(i, true);
3547

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

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

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

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

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

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

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

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

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

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

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

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

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

3597
			let halfWid = width * pxRatio / 2;
3598

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

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

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

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

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

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

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

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

3634
				let _fillStyle = null;
3635

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

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

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

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

3654
	const CLIP_FILL_STROKE = BAND_CLIP_FILL | BAND_CLIP_STROKE;
3655

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

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

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

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

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

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

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

3722
		let incrSpace;
3723

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

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

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

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

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

3742
		ctx.beginPath();
3743

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

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

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

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

3767
		ctx.stroke();
3768

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

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

3775
		let converged = true;
3776

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

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

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

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

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

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

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

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

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

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

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

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

3824
			let oldSize = axis._size;
3825

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

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

3832
		return converged;
3833
	}
3834

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

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

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

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

3847
		return converged;
3848
	}
3849

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

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

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

3860
			let x, y;
3861

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

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

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

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

3873
				ctx.save();
3874

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

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

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

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

3892
				ctx.restore();
3893
			}
3894

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

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

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

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

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

3907
			let _splits = axis._splits;
3908

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

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

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

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

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

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

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

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

3941
			let _values = axis._values;
3942

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

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

3952
					val = "" + val;
3953

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

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

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

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

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

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

4007
		fire("drawAxes");
4008
	}
4009

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

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

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

4033
	let queuedCommit = false;
4034

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

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

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

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

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

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

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

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

4074

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

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

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

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

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

4097
			syncRect(false);
4098

4099
			fire("setSize");
4100

4101
			shouldSetSize = false;
4102
		}
4103

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

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

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

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

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

4131
			fire("ready");
4132
		}
4133

4134
		viaAutoScaleX = false;
4135

4136
		queuedCommit = false;
4137
	}
4138

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

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

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

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

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

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

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

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

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

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

4182
			pendScales[key] = opts;
4183

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

4189
	self.setScale = setScale;
4190

4191
//	INTERACTION
4192

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

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

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

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

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

4214
	let dragging = false;
4215

4216
	const drag = cursor.drag;
4217

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

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

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

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

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

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

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

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

4260
	self.setSelect = setSelect;
4261

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

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

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

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

4281
		let s = series[i];
4282

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

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

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

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

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

4299
	self.setSeries = setSeries;
4300

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

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

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

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

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

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

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

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

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

4343
			let allFocused = i == null;
4344

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

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

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

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

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

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

4373
		let dim = plotWidCss;
4374

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

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

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

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

4389
		let distr = sc.distr;
4390

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

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

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

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

4424
	self.batch = batch;
4425

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

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

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

4443
	let setSelX = scaleX.ori == 0 ? setSelH : setSelV;
4444
	let setSelY = scaleX.ori == 1 ? setSelH : setSelV;
4445

4446
	function syncLegend() {
4447
		if (showLegend && legend.live) {
4448
			for (let i = mode == 2 ? 1 : 0; i < series.length; i++) {
4449
				if (i == 0 && multiValLegend)
4450
					continue;
4451

4452
				let vals = legend.values[i];
4453

4454
				let j = 0;
4455

4456
				for (let k in vals)
4457
					legendCells[i][j++].firstChild.nodeValue = vals[k];
4458
			}
4459
		}
4460
	}
4461

4462
	function setLegend(opts, _fire) {
4463
		if (opts != null) {
4464
			let idx = opts.idx;
4465

4466
			legend.idx = idx;
4467
			series.forEach((s, sidx) => {
4468
				(sidx > 0 || !multiValLegend) && setLegendValues(sidx, idx);
4469
			});
4470
		}
4471

4472
		if (showLegend && legend.live)
4473
			syncLegend();
4474

4475
		shouldSetLegend = false;
4476

4477
		_fire !== false && fire("setLegend");
4478
	}
4479

4480
	self.setLegend = setLegend;
4481

4482
	function setLegendValues(sidx, idx) {
4483
		let val;
4484

4485
		if (idx == null)
4486
			val = NULL_LEGEND_VALUES;
4487
		else {
4488
			let s = series[sidx];
4489
			let src = sidx == 0 && xScaleDistr == 2 ? data0 : data[sidx];
4490
			val = multiValLegend ? s.values(self, sidx, idx) : {_: s.value(self, src[idx], sidx, idx)};
4491
		}
4492

4493
		legend.values[sidx] = val;
4494
	}
4495

4496
	function updateCursor(src, _fire, _pub) {
4497
	//	ts == null && log("updateCursor()", arguments);
4498

4499
		rawMouseLeft1 = mouseLeft1;
4500
		rawMouseTop1 = mouseTop1;
4501

4502
		[mouseLeft1, mouseTop1] = cursor.move(self, mouseLeft1, mouseTop1);
4503

4504
		if (cursor.show) {
4505
			vCursor && elTrans(vCursor, round(mouseLeft1), 0, plotWidCss, plotHgtCss);
4506
			hCursor && elTrans(hCursor, 0, round(mouseTop1), plotWidCss, plotHgtCss);
4507
		}
4508

4509
		let idx;
4510

4511
		// when zooming to an x scale range between datapoints the binary search
4512
		// for nearest min/max indices results in this condition. cheap hack :D
4513
		let noDataInRange = i0 > i1; // works for mode 1 only
4514

4515
		closestDist = inf;
4516

4517
		// TODO: extract
4518
		let xDim = scaleX.ori == 0 ? plotWidCss : plotHgtCss;
4519
		let yDim = scaleX.ori == 1 ? plotWidCss : plotHgtCss;
4520

4521
		// if cursor hidden, hide points & clear legend vals
4522
		if (mouseLeft1 < 0 || dataLen == 0 || noDataInRange) {
4523
			idx = null;
4524

4525
			for (let i = 0; i < series.length; i++) {
4526
				if (i > 0) {
4527
					cursorPts.length > 1 && elTrans(cursorPts[i], -10, -10, plotWidCss, plotHgtCss);
4528
				}
4529
			}
4530

4531
			if (cursorFocus)
4532
				setSeries(null, FOCUS_TRUE, true, src == null && syncOpts.setSeries);
4533

4534
			if (legend.live) {
4535
				activeIdxs.fill(null);
4536
				shouldSetLegend = true;
4537

4538
				for (let i = 0; i < series.length; i++)
4539
					legend.values[i] = NULL_LEGEND_VALUES;
4540
			}
4541
		}
4542
		else {
4543
		//	let pctY = 1 - (y / rect.height);
4544

4545
			let mouseXPos, valAtPosX, xPos;
4546

4547
			if (mode == 1) {
4548
				mouseXPos = scaleX.ori == 0 ? mouseLeft1 : mouseTop1;
4549
				valAtPosX = posToVal(mouseXPos, xScaleKey);
4550
				idx = closestIdx(valAtPosX, data[0], i0, i1);
4551
				xPos = incrRoundUp(valToPosX(data[0][idx], scaleX, xDim, 0), 0.5);
4552
			}
4553

4554
			for (let i = mode == 2 ? 1 : 0; i < series.length; i++) {
4555
				let s = series[i];
4556

4557
				let idx1  = activeIdxs[i];
4558
				let yVal1 = mode == 1 ? data[i][idx1] : data[i][1][idx1];
4559

4560
				let idx2  = cursor.dataIdx(self, i, idx, valAtPosX);
4561
				let yVal2 = mode == 1 ? data[i][idx2] : data[i][1][idx2];
4562

4563
				shouldSetLegend = shouldSetLegend || yVal2 != yVal1 || idx2 != idx1;
4564

4565
				activeIdxs[i] = idx2;
4566

4567
				let xPos2 = idx2 == idx ? xPos : incrRoundUp(valToPosX(mode == 1 ? data[0][idx2] : data[i][0][idx2], scaleX, xDim, 0), 0.5);
4568

4569
				if (i > 0 && s.show) {
4570
					let yPos = yVal2 == null ? -10 : incrRoundUp(valToPosY(yVal2, mode == 1 ? scales[s.scale] : scales[s.facets[1].scale], yDim, 0), 0.5);
4571

4572
					if (yPos > 0 && mode == 1) {
4573
						let dist = abs(yPos - mouseTop1);
4574

4575
						if (dist <= closestDist) {
4576
							closestDist = dist;
4577
							closestSeries = i;
4578
						}
4579
					}
4580

4581
					let hPos, vPos;
4582

4583
					if (scaleX.ori == 0) {
4584
						hPos = xPos2;
4585
						vPos = yPos;
4586
					}
4587
					else {
4588
						hPos = yPos;
4589
						vPos = xPos2;
4590
					}
4591

4592
					if (shouldSetLegend && cursorPts.length > 1) {
4593
						elColor(cursorPts[i], cursor.points.fill(self, i), cursor.points.stroke(self, i));
4594

4595
						let ptWid, ptHgt, ptLft, ptTop,
4596
							centered = true,
4597
							getBBox = cursor.points.bbox;
4598

4599
						if (getBBox != null) {
4600
							centered = false;
4601

4602
							let bbox = getBBox(self, i);
4603

4604
							ptLft = bbox.left;
4605
							ptTop = bbox.top;
4606
							ptWid = bbox.width;
4607
							ptHgt = bbox.height;
4608
						}
4609
						else {
4610
							ptLft = hPos;
4611
							ptTop = vPos;
4612
							ptWid = ptHgt = cursor.points.size(self, i);
4613
						}
4614

4615
						elSize(cursorPts[i], ptWid, ptHgt, centered);
4616
						elTrans(cursorPts[i], ptLft, ptTop, plotWidCss, plotHgtCss);
4617
					}
4618
				}
4619

4620
				if (legend.live) {
4621
					if (!shouldSetLegend || i == 0 && multiValLegend)
4622
						continue;
4623

4624
					setLegendValues(i, idx2);
4625
				}
4626
			}
4627
		}
4628

4629
		cursor.idx = idx;
4630
		cursor.left = mouseLeft1;
4631
		cursor.top = mouseTop1;
4632

4633
		if (shouldSetLegend) {
4634
			legend.idx = idx;
4635
			setLegend();
4636
		}
4637

4638
		// nit: cursor.drag.setSelect is assumed always true
4639
		if (select.show && dragging) {
4640
			if (src != null) {
4641
				let [xKey, yKey] = syncOpts.scales;
4642
				let [matchXKeys, matchYKeys] = syncOpts.match;
4643
				let [xKeySrc, yKeySrc] = src.cursor.sync.scales;
4644

4645
				// match the dragX/dragY implicitness/explicitness of src
4646
				let sdrag = src.cursor.drag;
4647
				dragX = sdrag._x;
4648
				dragY = sdrag._y;
4649

4650
				let { left, top, width, height } = src.select;
4651

4652
				let sori = src.scales[xKey].ori;
4653
				let sPosToVal = src.posToVal;
4654

4655
				let sOff, sDim, sc, a, b;
4656

4657
				let matchingX = xKey != null && matchXKeys(xKey, xKeySrc);
4658
				let matchingY = yKey != null && matchYKeys(yKey, yKeySrc);
4659

4660
				if (matchingX) {
4661
					if (sori == 0) {
4662
						sOff = left;
4663
						sDim = width;
4664
					}
4665
					else {
4666
						sOff = top;
4667
						sDim = height;
4668
					}
4669

4670
					if (dragX) {
4671
						sc = scales[xKey];
4672

4673
						a = valToPosX(sPosToVal(sOff, xKeySrc),        sc, xDim, 0);
4674
						b = valToPosX(sPosToVal(sOff + sDim, xKeySrc), sc, xDim, 0);
4675

4676
						setSelX(min(a,b), abs(b-a));
4677
					}
4678
					else
4679
						setSelX(0, xDim);
4680

4681
					if (!matchingY)
4682
						setSelY(0, yDim);
4683
				}
4684

4685
				if (matchingY) {
4686
					if (sori == 1) {
4687
						sOff = left;
4688
						sDim = width;
4689
					}
4690
					else {
4691
						sOff = top;
4692
						sDim = height;
4693
					}
4694

4695
					if (dragY) {
4696
						sc = scales[yKey];
4697

4698
						a = valToPosY(sPosToVal(sOff, yKeySrc),        sc, yDim, 0);
4699
						b = valToPosY(sPosToVal(sOff + sDim, yKeySrc), sc, yDim, 0);
4700

4701
						setSelY(min(a,b), abs(b-a));
4702
					}
4703
					else
4704
						setSelY(0, yDim);
4705

4706
					if (!matchingX)
4707
						setSelX(0, xDim);
4708
				}
4709
			}
4710
			else {
4711
				let rawDX = abs(rawMouseLeft1 - rawMouseLeft0);
4712
				let rawDY = abs(rawMouseTop1 - rawMouseTop0);
4713

4714
				if (scaleX.ori == 1) {
4715
					let _rawDX = rawDX;
4716
					rawDX = rawDY;
4717
					rawDY = _rawDX;
4718
				}
4719

4720
				dragX = drag.x && rawDX >= drag.dist;
4721
				dragY = drag.y && rawDY >= drag.dist;
4722

4723
				let uni = drag.uni;
4724

4725
				if (uni != null) {
4726
					// only calc drag status if they pass the dist thresh
4727
					if (dragX && dragY) {
4728
						dragX = rawDX >= uni;
4729
						dragY = rawDY >= uni;
4730

4731
						// force unidirectionality when both are under uni limit
4732
						if (!dragX && !dragY) {
4733
							if (rawDY > rawDX)
4734
								dragY = true;
4735
							else
4736
								dragX = true;
4737
						}
4738
					}
4739
				}
4740
				else if (drag.x && drag.y && (dragX || dragY))
4741
					// if omni with no uni then both dragX / dragY should be true if either is true
4742
					dragX = dragY = true;
4743

4744
				let p0, p1;
4745

4746
				if (dragX) {
4747
					if (scaleX.ori == 0) {
4748
						p0 = mouseLeft0;
4749
						p1 = mouseLeft1;
4750
					}
4751
					else {
4752
						p0 = mouseTop0;
4753
						p1 = mouseTop1;
4754
					}
4755

4756
					setSelX(min(p0, p1), abs(p1 - p0));
4757

4758
					if (!dragY)
4759
						setSelY(0, yDim);
4760
				}
4761

4762
				if (dragY) {
4763
					if (scaleX.ori == 1) {
4764
						p0 = mouseLeft0;
4765
						p1 = mouseLeft1;
4766
					}
4767
					else {
4768
						p0 = mouseTop0;
4769
						p1 = mouseTop1;
4770
					}
4771

4772
					setSelY(min(p0, p1), abs(p1 - p0));
4773

4774
					if (!dragX)
4775
						setSelX(0, xDim);
4776
				}
4777

4778
				// the drag didn't pass the dist requirement
4779
				if (!dragX && !dragY) {
4780
					setSelX(0, 0);
4781
					setSelY(0, 0);
4782
				}
4783
			}
4784
		}
4785

4786
		drag._x = dragX;
4787
		drag._y = dragY;
4788

4789
		if (src == null) {
4790
			if (_pub) {
4791
				if (syncKey != null) {
4792
					let [xSyncKey, ySyncKey] = syncOpts.scales;
4793

4794
					syncOpts.values[0] = xSyncKey != null ? posToVal(scaleX.ori == 0 ? mouseLeft1 : mouseTop1, xSyncKey) : null;
4795
					syncOpts.values[1] = ySyncKey != null ? posToVal(scaleX.ori == 1 ? mouseLeft1 : mouseTop1, ySyncKey) : null;
4796
				}
4797

4798
				pubSync(mousemove, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, idx);
4799
			}
4800

4801
			if (cursorFocus) {
4802
				let shouldPub = _pub && syncOpts.setSeries;
4803
				let p = focus.prox;
4804

4805
				if (focusedSeries == null) {
4806
					if (closestDist <= p)
4807
						setSeries(closestSeries, FOCUS_TRUE, true, shouldPub);
4808
				}
4809
				else {
4810
					if (closestDist > p)
4811
						setSeries(null, FOCUS_TRUE, true, shouldPub);
4812
					else if (closestSeries != focusedSeries)
4813
						setSeries(closestSeries, FOCUS_TRUE, true, shouldPub);
4814
				}
4815
			}
4816
		}
4817

4818
		ready && _fire !== false && fire("setCursor");
4819
	}
4820

4821
	let rect = null;
4822

4823
	function syncRect(defer) {
4824
		if (defer === true)
4825
			rect = null;
4826
		else {
4827
			rect = over.getBoundingClientRect();
4828
			fire("syncRect", rect);
4829
		}
4830
	}
4831

4832
	function mouseMove(e, src, _l, _t, _w, _h, _i) {
4833
		if (cursor._lock)
4834
			return;
4835

4836
		cacheMouse(e, src, _l, _t, _w, _h, _i, false, e != null);
4837

4838
		if (e != null)
4839
			updateCursor(null, true, true);
4840
		else
4841
			updateCursor(src, true, false);
4842
	}
4843

4844
	function cacheMouse(e, src, _l, _t, _w, _h, _i, initial, snap) {
4845
		if (rect == null)
4846
			syncRect(false);
4847

4848
		if (e != null) {
4849
			_l = e.clientX - rect.left;
4850
			_t = e.clientY - rect.top;
4851
		}
4852
		else {
4853
			if (_l < 0 || _t < 0) {
4854
				mouseLeft1 = -10;
4855
				mouseTop1 = -10;
4856
				return;
4857
			}
4858

4859
			let [xKey, yKey] = syncOpts.scales;
4860

4861
			let syncOptsSrc = src.cursor.sync;
4862
			let [xValSrc, yValSrc] = syncOptsSrc.values;
4863
			let [xKeySrc, yKeySrc] = syncOptsSrc.scales;
4864
			let [matchXKeys, matchYKeys] = syncOpts.match;
4865

4866
			let rotSrc = src.scales[xKeySrc].ori == 1;
4867

4868
			let xDim = scaleX.ori == 0 ? plotWidCss : plotHgtCss,
4869
				yDim = scaleX.ori == 1 ? plotWidCss : plotHgtCss,
4870
				_xDim = rotSrc ? _h : _w,
4871
				_yDim = rotSrc ? _w : _h,
4872
				_xPos = rotSrc ? _t : _l,
4873
				_yPos = rotSrc ? _l : _t;
4874

4875
			if (xKeySrc != null)
4876
				_l = matchXKeys(xKey, xKeySrc) ? getPos(xValSrc, scales[xKey], xDim, 0) : -10;
4877
			else
4878
				_l = xDim * (_xPos/_xDim);
4879

4880
			if (yKeySrc != null)
4881
				_t = matchYKeys(yKey, yKeySrc) ? getPos(yValSrc, scales[yKey], yDim, 0) : -10;
4882
			else
4883
				_t = yDim * (_yPos/_yDim);
4884

4885
			if (scaleX.ori == 1) {
4886
				let __l = _l;
4887
				_l = _t;
4888
				_t = __l;
4889
			}
4890
		}
4891

4892
		if (snap) {
4893
			if (_l <= 1 || _l >= plotWidCss - 1)
4894
				_l = incrRound(_l, plotWidCss);
4895

4896
			if (_t <= 1 || _t >= plotHgtCss - 1)
4897
				_t = incrRound(_t, plotHgtCss);
4898
		}
4899

4900
		if (initial) {
4901
			rawMouseLeft0 = _l;
4902
			rawMouseTop0 = _t;
4903

4904
			[mouseLeft0, mouseTop0] = cursor.move(self, _l, _t);
4905
		}
4906
		else {
4907
			mouseLeft1 = _l;
4908
			mouseTop1 = _t;
4909
		}
4910
	}
4911

4912
	function hideSelect() {
4913
		setSelect({
4914
			width: 0,
4915
			height: 0,
4916
		}, false);
4917
	}
4918

4919
	function mouseDown(e, src, _l, _t, _w, _h, _i) {
4920
		dragging = true;
4921
		dragX = dragY = drag._x = drag._y = false;
4922

4923
		cacheMouse(e, src, _l, _t, _w, _h, _i, true, false);
4924

4925
		if (e != null) {
4926
			onMouse(mouseup, doc, mouseUp);
4927
			pubSync(mousedown, self, mouseLeft0, mouseTop0, plotWidCss, plotHgtCss, null);
4928
		}
4929
	}
4930

4931
	function mouseUp(e, src, _l, _t, _w, _h, _i) {
4932
		dragging = drag._x = drag._y = false;
4933

4934
		cacheMouse(e, src, _l, _t, _w, _h, _i, false, true);
4935

4936
		let { left, top, width, height } = select;
4937

4938
		let hasSelect = width > 0 || height > 0;
4939

4940
		hasSelect && setSelect(select);
4941

4942
		if (drag.setScale && hasSelect) {
4943
		//	if (syncKey != null) {
4944
		//		dragX = drag.x;
4945
		//		dragY = drag.y;
4946
		//	}
4947

4948
			let xOff = left,
4949
				xDim = width,
4950
				yOff = top,
4951
				yDim = height;
4952

4953
			if (scaleX.ori == 1) {
4954
				xOff = top,
4955
				xDim = height,
4956
				yOff = left,
4957
				yDim = width;
4958
			}
4959

4960
			if (dragX) {
4961
				_setScale(xScaleKey,
4962
					posToVal(xOff, xScaleKey),
4963
					posToVal(xOff + xDim, xScaleKey)
4964
				);
4965
			}
4966

4967
			if (dragY) {
4968
				for (let k in scales) {
4969
					let sc = scales[k];
4970

4971
					if (k != xScaleKey && sc.from == null && sc.min != inf) {
4972
						_setScale(k,
4973
							posToVal(yOff + yDim, k),
4974
							posToVal(yOff, k)
4975
						);
4976
					}
4977
				}
4978
			}
4979

4980
			hideSelect();
4981
		}
4982
		else if (cursor.lock) {
4983
			cursor._lock = !cursor._lock;
4984

4985
			if (!cursor._lock)
4986
				updateCursor(null, true, false);
4987
		}
4988

4989
		if (e != null) {
4990
			offMouse(mouseup, doc);
4991
			pubSync(mouseup, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null);
4992
		}
4993
	}
4994

4995
	function mouseLeave(e, src, _l, _t, _w, _h, _i) {
4996
		if (!cursor._lock) {
4997
			let _dragging = dragging;
4998

4999
			if (dragging) {
5000
				// handle case when mousemove aren't fired all the way to edges by browser
5001
				let snapH = true;
5002
				let snapV = true;
5003
				let snapProx = 10;
5004

5005
				let dragH, dragV;
5006

5007
				if (scaleX.ori == 0) {
5008
					dragH = dragX;
5009
					dragV = dragY;
5010
				}
5011
				else {
5012
					dragH = dragY;
5013
					dragV = dragX;
5014
				}
5015

5016
				if (dragH && dragV) {
5017
					// maybe omni corner snap
5018
					snapH = mouseLeft1 <= snapProx || mouseLeft1 >= plotWidCss - snapProx;
5019
					snapV = mouseTop1  <= snapProx || mouseTop1  >= plotHgtCss - snapProx;
5020
				}
5021

5022
				if (dragH && snapH)
5023
					mouseLeft1 = mouseLeft1 < mouseLeft0 ? 0 : plotWidCss;
5024

5025
				if (dragV && snapV)
5026
					mouseTop1 = mouseTop1 < mouseTop0 ? 0 : plotHgtCss;
5027

5028
				updateCursor(null, true, true);
5029

5030
				dragging = false;
5031
			}
5032

5033
			mouseLeft1 = -10;
5034
			mouseTop1 = -10;
5035

5036
			// passing a non-null timestamp to force sync/mousemove event
5037
			updateCursor(null, true, true);
5038

5039
			if (_dragging)
5040
				dragging = _dragging;
5041
		}
5042
	}
5043

5044
	function dblClick(e, src, _l, _t, _w, _h, _i) {
5045
		autoScaleX();
5046

5047
		hideSelect();
5048

5049
		if (e != null)
5050
			pubSync(dblclick, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null);
5051
	}
5052

5053
	function syncPxRatio() {
5054
		axes.forEach(syncFontSize);
5055
		_setSize(self.width, self.height, true);
5056
	}
5057

5058
	on(dppxchange, win, syncPxRatio);
5059

5060
	// internal pub/sub
5061
	const events = {};
5062

5063
	events.mousedown = mouseDown;
5064
	events.mousemove = mouseMove;
5065
	events.mouseup = mouseUp;
5066
	events.dblclick = dblClick;
5067
	events["setSeries"] = (e, src, idx, opts) => {
5068
		setSeries(idx, opts, true, false);
5069
	};
5070

5071
	if (cursor.show) {
5072
		onMouse(mousedown,  over, mouseDown);
5073
		onMouse(mousemove,  over, mouseMove);
5074
		onMouse(mouseenter, over, syncRect);
5075
		onMouse(mouseleave, over, mouseLeave);
5076

5077
		onMouse(dblclick, over, dblClick);
5078

5079
		cursorPlots.add(self);
5080

5081
		self.syncRect = syncRect;
5082
	}
5083

5084
	// external on/off
5085
	const hooks = self.hooks = opts.hooks || {};
5086

5087
	function fire(evName, a1, a2) {
5088
		if (evName in hooks) {
5089
			hooks[evName].forEach(fn => {
5090
				fn.call(null, self, a1, a2);
5091
			});
5092
		}
5093
	}
5094

5095
	(opts.plugins || []).forEach(p => {
5096
		for (let evName in p.hooks)
5097
			hooks[evName] = (hooks[evName] || []).concat(p.hooks[evName]);
5098
	});
5099

5100
	const syncOpts = assign({
5101
		key: null,
5102
		setSeries: false,
5103
		filters: {
5104
			pub: retTrue,
5105
			sub: retTrue,
5106
		},
5107
		scales: [xScaleKey, series[1] ? series[1].scale : null],
5108
		match: [retEq, retEq],
5109
		values: [null, null],
5110
	}, cursor.sync);
5111

5112
	(cursor.sync = syncOpts);
5113

5114
	const syncKey = syncOpts.key;
5115

5116
	const sync = _sync(syncKey);
5117

5118
	function pubSync(type, src, x, y, w, h, i) {
5119
		if (syncOpts.filters.pub(type, src, x, y, w, h, i))
5120
			sync.pub(type, src, x, y, w, h, i);
5121
	}
5122

5123
	sync.sub(self);
5124

5125
	function pub(type, src, x, y, w, h, i) {
5126
		if (syncOpts.filters.sub(type, src, x, y, w, h, i))
5127
			events[type](null, src, x, y, w, h, i);
5128
	}
5129

5130
	(self.pub = pub);
5131

5132
	function destroy() {
5133
		sync.unsub(self);
5134
		cursorPlots.delete(self);
5135
		mouseListeners.clear();
5136
		off(dppxchange, win, syncPxRatio);
5137
		root.remove();
5138
		fire("destroy");
5139
	}
5140

5141
	self.destroy = destroy;
5142

5143
	function _init() {
5144
		fire("init", opts, data);
5145

5146
		setData(data || opts.data, false);
5147

5148
		if (pendScales[xScaleKey])
5149
			setScale(xScaleKey, pendScales[xScaleKey]);
5150
		else
5151
			autoScaleX();
5152

5153
		_setSize(opts.width, opts.height);
5154

5155
		updateCursor(null, true, false);
5156

5157
		setSelect(select, false);
5158
	}
5159

5160
	series.forEach(initSeries);
5161

5162
	axes.forEach(initAxis);
5163

5164
	if (then) {
5165
		if (then instanceof HTMLElement) {
5166
			then.appendChild(root);
5167
			_init();
5168
		}
5169
		else
5170
			then(self, _init);
5171
	}
5172
	else
5173
		_init();
5174

5175
	return self;
5176
}
5177

5178
uPlot.assign = assign;
5179
uPlot.fmtNum = fmtNum;
5180
uPlot.rangeNum = rangeNum;
5181
uPlot.rangeLog = rangeLog;
5182
uPlot.rangeAsinh = rangeAsinh;
5183
uPlot.orient   = orient;
5184

5185
{
5186
	uPlot.join = join;
5187
}
5188

5189
{
5190
	uPlot.fmtDate = fmtDate;
5191
	uPlot.tzDate  = tzDate;
5192
}
5193

5194
{
5195
	uPlot.sync = _sync;
5196
}
5197

5198
{
5199
	uPlot.addGap = addGap;
5200
	uPlot.clipGaps = clipGaps;
5201

5202
	let paths = uPlot.paths = {
5203
		points,
5204
	};
5205

5206
	(paths.linear  = linear);
5207
	(paths.stepped = stepped);
5208
	(paths.bars    = bars);
5209
	(paths.spline  = monotoneCubic);
5210
}
5211

5212
module.exports = uPlot;
5213

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

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.