text: import {
  	FEAT_TIME,
  	FEAT_CURSOR,
  	FEAT_LEGEND,
  
  	FEAT_POINTS,
  
  	FEAT_PATHS,
  	FEAT_PATHS_LINEAR,
  	FEAT_PATHS_SPLINE,
  	FEAT_PATHS_SPLINE2,
  	FEAT_PATHS_STEPPED,
  	FEAT_PATHS_BARS,
  
  	FEAT_JOIN,
  } from './feats';
  
  import {
  	copy,
  	assign,
  	PI,
  	inf,
  	abs,
  	floor,
  	round,
  	roundDec,
  	ceil,
  	min,
  	max,
  	clamp,
  	pow,
  	asinh,
  	sinh,
  	log10,
  	closestIdx,
  	getMinMax,
  	getMinMaxLog,
  	rangeNum,
  	rangeLog,
  	rangeAsinh,
  	incrRound,
  	incrRoundUp,
  	isArr,
  	isObj,
  	fastIsObj,
  	isStr,
  	fnOrSelf,
  	fmtNum,
  	fixedDec,
  	ifNull,
  	join,
  	microTask,
  	retArg0,
  	retArg1,
  	retNull,
  	retTrue,
  	EMPTY_OBJ,
  	EMPTY_ARR,
  	nullNullTuple,
  	retEq,
  	autoRangePart,
  	rangePad,
  	hasData,
  	numIntDigits,
  } from './utils';
  
  import {
  	WIDTH,
  	HEIGHT,
  	TOP,
  	BOTTOM,
  	LEFT,
  	RIGHT,
  	transparent,
  
  	mousemove,
  	mousedown,
  	mouseup,
  	mouseleave,
  	mouseenter,
  	dblclick,
  	resize,
  	scroll,
  
  	dppxchange
  } from './strings';
  
  import {
  	UPLOT,
  	ORI_HZ,
  	ORI_VT,
  	TITLE,
  	WRAP,
  	UNDER,
  	OVER,
  	AXIS,
  	OFF,
  	SELECT,
  	CURSOR_X,
  	CURSOR_Y,
  	CURSOR_PT,
  	LEGEND,
  	LEGEND_LIVE,
  	LEGEND_INLINE,
  	LEGEND_THEAD,
  	LEGEND_SERIES,
  	LEGEND_MARKER,
  	LEGEND_LABEL,
  	LEGEND_VALUE,
  } from './domClasses';
  
  import {
  	doc,
  	win,
  	pxRatio,
  
  	addClass,
  	remClass,
  	setStylePx,
  	placeTag,
  	placeDiv,
  	elTrans,
  	elColor,
  	elSize,
  	on,
  	off,
  } from './dom';
  
  import {
  	fmtDate,
  	tzDate,
  } from './fmtDate';
  
  import {
  	lineMult,
  	ptDia,
  	cursorOpts,
  	seriesFillTo,
  
  	xAxisOpts,
  	yAxisOpts,
  	xSeriesOpts,
  	ySeriesOpts,
  	xScaleOpts,
  	yScaleOpts,
  
  	xySeriesOpts,
  
  	clampScale,
  
  	timeIncrsMs,
  	timeIncrsS,
  
  	wholeIncrs,
  	numIncrs,
  	timeAxisVal,
  	timeAxisVals,
  	numAxisVals,
  
  	logAxisValsFilt,
  
  	timeSeriesVal,
  	numSeriesVal,
  
  	timeSeriesLabel,
  	numSeriesLabel,
  
  	timeAxisSplitsMs,
  	timeAxisSplitsS,
  
  	numAxisSplits,
  	logAxisSplits,
  	asinhAxisSplits,
  
  	timeAxisStamps,
  
  	_timeAxisStampsMs,
  	_timeAxisStampsS,
  
  	timeSeriesStamp,
  	_timeSeriesStamp,
  
  	legendOpts,
  } from './opts';
  
  import { _sync } from './sync';
  
  import { points   } from './paths/points';
  import { linear   } from './paths/linear';
  import { stepped  } from './paths/stepped';
  import { bars     } from './paths/bars';
  import { monotoneCubic     as spline  } from './paths/monotoneCubic';
  import { catmullRomCentrip as spline2 } from './paths/catmullRomCentrip';
  
  import { addGap, clipGaps, moveToH, moveToV, arcH, arcV, orient, pxRoundGen, BAND_CLIP_FILL, BAND_CLIP_STROKE } from './paths/utils';
  
  function log(name, args) {
  	console.log.apply(console, [name].concat(Array.prototype.slice.call(args)));
  }
  
  const cursorPlots = new Set();
  
  function invalidateRects() {
  	cursorPlots.forEach(u => {
  		u.syncRect(true);
  	});
  }
  
  on(resize, win, invalidateRects);
  on(scroll, win, invalidateRects, true);
  
  const linearPath = FEAT_PATHS && FEAT_PATHS_LINEAR ? linear() : null;
  const pointsPath = FEAT_POINTS ? points() : null;
  
  function setDefaults(d, xo, yo, initY) {
  	let d2 = initY ? [d[0], d[1]].concat(d.slice(2)) : [d[0]].concat(d.slice(1));
  	return d2.map((o, i) => setDefault(o, i, xo, yo));
  }
  
  function setDefaults2(d, xyo) {
  	return d.map((o, i) => i == 0 ? null : assign({}, xyo, o));  // todo: assign() will not merge facet arrays
  }
  
  function setDefault(o, i, xo, yo) {
  	return assign({}, (i == 0 ? xo : yo), o);
  }
  
  function snapNumX(self, dataMin, dataMax) {
  	return dataMin == null ? nullNullTuple : [dataMin, dataMax];
  }
  
  const snapTimeX = snapNumX;
  
  // this ensures that non-temporal/numeric y-axes get multiple-snapped padding added above/below
  // TODO: also account for incrs when snapping to ensure top of axis gets a tick & value
  function snapNumY(self, dataMin, dataMax) {
  	return dataMin == null ? nullNullTuple : rangeNum(dataMin, dataMax, rangePad, true);
  }
  
  function snapLogY(self, dataMin, dataMax, scale) {
  	return dataMin == null ? nullNullTuple : rangeLog(dataMin, dataMax, self.scales[scale].log, false);
  }
  
  const snapLogX = snapLogY;
  
  function snapAsinhY(self, dataMin, dataMax, scale) {
  	return dataMin == null ? nullNullTuple : rangeAsinh(dataMin, dataMax, self.scales[scale].log, false);
  }
  
  const snapAsinhX = snapAsinhY;
  
  // dim is logical (getClientBoundingRect) pixels, not canvas pixels
  function findIncr(minVal, maxVal, incrs, dim, minSpace) {
  	let intDigits = max(numIntDigits(minVal), numIntDigits(maxVal));
  
  	let delta = maxVal - minVal;
  
  	let incrIdx = closestIdx((minSpace / dim) * delta, incrs);
  
  	do {
  		let foundIncr = incrs[incrIdx];
  		let foundSpace = dim * foundIncr / delta;
  
  		if (foundSpace >= minSpace && intDigits + (foundIncr < 5 ? fixedDec.get(foundIncr) : 0) <= 17)
  			return [foundIncr, foundSpace];
  	} while (++incrIdx < incrs.length);
  
  	return [0, 0];
  }
  
  function pxRatioFont(font) {
  	let fontSize, fontSizeCss;
  	font = font.replace(/(\d+)px/, (m, p1) => (fontSize = round((fontSizeCss = +p1) * pxRatio)) + 'px');
  	return [font, fontSize, fontSizeCss];
  }
  
  function syncFontSize(axis) {
  	if (axis.show) {
  		[axis.font, axis.labelFont].forEach(f => {
  			let size = roundDec(f[2] * pxRatio, 1);
  			f[0] = f[0].replace(/[0-9.]+px/, size + 'px');
  			f[1] = size;
  		});
  	}
  }
  
  export default function uPlot(opts, data, then) {
  	const self = {
  		mode: ifNull(opts.mode, 1),
  	};
  
  	const mode = self.mode;
  
  	// TODO: cache denoms & mins scale.cache = {r, min, }
  	function getValPct(val, scale) {
  		let _val = (
  			scale.distr == 3 ? log10(val > 0 ? val : scale.clamp(self, val, scale.min, scale.max, scale.key)) :
  			scale.distr == 4 ? asinh(val, scale.asinh) :
  			val
  		);
  
  		return (_val - scale._min) / (scale._max - scale._min);
  	}
  
  	function getHPos(val, scale, dim, off) {
  		let pct = getValPct(val, scale);
  		return off + dim * (scale.dir == -1 ? (1 - pct) : pct);
  	}
  
  	function getVPos(val, scale, dim, off) {
  		let pct = getValPct(val, scale);
  		return off + dim * (scale.dir == -1 ? pct : (1 - pct));
  	}
  
  	function getPos(val, scale, dim, off) {
  		return scale.ori == 0 ? getHPos(val, scale, dim, off) : getVPos(val, scale, dim, off);
  	}
  
  	self.valToPosH = getHPos;
  	self.valToPosV = getVPos
  
  	let ready = false;
  	self.status = 0;
  
  	const root = self.root = placeDiv(UPLOT);
  
  	if (opts.id != null)
  		root.id = opts.id;
  
  	addClass(root, opts.class);
  
  	if (opts.title) {
  		let title = placeDiv(TITLE, root);
  		title.textContent = opts.title;
  	}
  
  	const can = placeTag("canvas");
  	const ctx = self.ctx = can.getContext("2d");
  
  	const wrap = placeDiv(WRAP, root);
  	const under = self.under = placeDiv(UNDER, wrap);
  	wrap.appendChild(can);
  	const over = self.over = placeDiv(OVER, wrap);
  
  	opts = copy(opts);
  
  	const pxAlign = +ifNull(opts.pxAlign, 1);
  
  	const pxRound = pxRoundGen(pxAlign);
  
  	(opts.plugins || []).forEach(p => {
  		if (p.opts)
  			opts = p.opts(self, opts) || opts;
  	});
  
  	const ms = opts.ms || 1e-3;
  
  	const series  = self.series = mode == 1 ?
  		setDefaults(opts.series || [], xSeriesOpts, ySeriesOpts, false) :
  		setDefaults2(opts.series || [null], xySeriesOpts);
  	const axes    = self.axes   = setDefaults(opts.axes   || [], xAxisOpts,   yAxisOpts,    true);
  	const scales  = self.scales = {};
  	const bands   = self.bands  = opts.bands || [];
  
  	bands.forEach(b => {
  		b.fill = fnOrSelf(b.fill || null);
  	});
  
  	const xScaleKey = mode == 2 ? series[1].facets[0].scale : series[0].scale;
  
  	const drawOrderMap = {
  		axes: drawAxesGrid,
  		series: drawSeries,
  	};
  
  	const drawOrder = (opts.drawOrder || ["axes", "series"]).map(key => drawOrderMap[key]);
  
  	function initScale(scaleKey) {
  		let sc = scales[scaleKey];
  
  		if (sc == null) {
  			let scaleOpts = (opts.scales || EMPTY_OBJ)[scaleKey] || EMPTY_OBJ;
  
  			if (scaleOpts.from != null) {
  				// ensure parent is initialized
  				initScale(scaleOpts.from);
  				// dependent scales inherit
  				scales[scaleKey] = assign({}, scales[scaleOpts.from], scaleOpts, {key: scaleKey});
  			}
  			else {
  				sc = scales[scaleKey] = assign({}, (scaleKey == xScaleKey ? xScaleOpts : yScaleOpts), scaleOpts);
  
  				if (mode == 2)
  					sc.time = false;
  
  				sc.key = scaleKey;
  
  				let isTime = FEAT_TIME && sc.time;
  
  				let rn = sc.range;
  
  				let rangeIsArr = isArr(rn);
  
  				if (scaleKey != xScaleKey || mode == 2) {
  					// if range array has null limits, it should be auto
  					if (rangeIsArr && (rn[0] == null || rn[1] == null)) {
  						rn = {
  							min: rn[0] == null ? autoRangePart : {
  								mode: 1,
  								hard: rn[0],
  								soft: rn[0],
  							},
  							max: rn[1] == null ? autoRangePart : {
  								mode: 1,
  								hard: rn[1],
  								soft: rn[1],
  							},
  						};
  						rangeIsArr = false;
  					}
  
  					if (!rangeIsArr && isObj(rn)) {
  						let cfg = rn;
  						// this is similar to snapNumY
  						rn = (self, dataMin, dataMax) => dataMin == null ? nullNullTuple : rangeNum(dataMin, dataMax, cfg);
  					}
  				}
  
  				sc.range = fnOrSelf(rn || (isTime ? snapTimeX : scaleKey == xScaleKey ?
  					(sc.distr == 3 ? snapLogX : sc.distr == 4 ? snapAsinhX : snapNumX) :
  					(sc.distr == 3 ? snapLogY : sc.distr == 4 ? snapAsinhY : snapNumY)
  				));
  
  				sc.auto = fnOrSelf(rangeIsArr ? false : sc.auto);
  
  				sc.clamp = fnOrSelf(sc.clamp || clampScale);
  
  				// caches for expensive ops like asinh() & log()
  				sc._min = sc._max = null;
  			}
  		}
  	}
  
  	initScale("x");
  	initScale("y");
  
  	// TODO: init scales from facets in mode: 2
  	if (mode == 1) {
  		series.forEach(s => {
  			initScale(s.scale);
  		});
  	}
  
  	axes.forEach(a => {
  		initScale(a.scale);
  	});
  
  	for (let k in opts.scales)
  		initScale(k);
  
  	const scaleX = scales[xScaleKey];
  
  	const xScaleDistr = scaleX.distr;
  
  	let valToPosX, valToPosY, moveTo, arc, xDimCan, xOffCan, yDimCan, yOffCan, xDimCss, xOffCss, yDimCss, yOffCss, updOriDims;
  
  	if (scaleX.ori == 0) {
  		addClass(root, ORI_HZ);
  		valToPosX = getHPos;
  		valToPosY = getVPos;
  		moveTo    = moveToH;
  		arc       = arcH;
  		/*
  		updOriDims = () => {
  			xDimCan = plotWid;
  			xOffCan = plotLft;
  			yDimCan = plotHgt;
  			yOffCan = plotTop;
  
  			xDimCss = plotWidCss;
  			xOffCss = plotLftCss;
  			yDimCss = plotHgtCss;
  			yOffCss = plotTopCss;
  		};
  		*/
  	}
  	else {
  		addClass(root, ORI_VT);
  		valToPosX = getVPos;
  		valToPosY = getHPos;
  		moveTo    = moveToV;
  		arc       = arcV;
  		/*
  		updOriDims = () => {
  			xDimCan = plotHgt;
  			xOffCan = plotTop;
  			yDimCan = plotWid;
  			yOffCan = plotLft;
  
  			xDimCss = plotHgtCss;
  			xOffCss = plotTopCss;
  			yDimCss = plotWidCss;
  			yOffCss = plotLftCss;
  		};
  		*/
  	}
  
  	const pendScales = {};
  
  	// explicitly-set initial scales
  	for (let k in scales) {
  		let sc = scales[k];
  
  		if (sc.min != null || sc.max != null) {
  			pendScales[k] = {min: sc.min, max: sc.max};
  			sc.min = sc.max = null;
  		}
  	}
  
  //	self.tz = opts.tz || Intl.DateTimeFormat().resolvedOptions().timeZone;
  	const _tzDate  = FEAT_TIME && (opts.tzDate || (ts => new Date(round(ts / ms))));
  	const _fmtDate = FEAT_TIME && (opts.fmtDate || fmtDate);
  
  	const _timeAxisSplits = FEAT_TIME && (ms == 1 ? timeAxisSplitsMs(_tzDate) : timeAxisSplitsS(_tzDate));
  	const _timeAxisVals   = FEAT_TIME && timeAxisVals(_tzDate, timeAxisStamps((ms == 1 ? _timeAxisStampsMs : _timeAxisStampsS), _fmtDate));
  	const _timeSeriesVal  = FEAT_TIME && timeSeriesVal(_tzDate, timeSeriesStamp(_timeSeriesStamp, _fmtDate));
  
  	const activeIdxs = [];
  
  	const legend     = FEAT_LEGEND && (self.legend = assign({}, legendOpts, opts.legend));
  	const showLegend = FEAT_LEGEND && legend.show;
  	const markers    = FEAT_LEGEND && legend.markers;
  
  	if (FEAT_LEGEND) {
  		legend.idxs = activeIdxs;
  
  		markers.width  = fnOrSelf(markers.width);
  		markers.dash   = fnOrSelf(markers.dash);
  		markers.stroke = fnOrSelf(markers.stroke);
  		markers.fill   = fnOrSelf(markers.fill);
  	}
  
  	let legendEl;
  	let legendRows = [];
  	let legendCells = [];
  	let legendCols;
  	let multiValLegend = false;
  	let NULL_LEGEND_VALUES = {};
  
  	if (FEAT_LEGEND && legend.live) {
  		const getMultiVals = series[1] ? series[1].values : null;
  		multiValLegend = getMultiVals != null;
  		legendCols = multiValLegend ? getMultiVals(self, 1, 0) : {_: 0};
  
  		for (let k in legendCols)
  			NULL_LEGEND_VALUES[k] = "--";
  	}
  
  	if (showLegend) {
  		legendEl = placeTag("table", LEGEND, root);
  
  		if (multiValLegend) {
  			let head = placeTag("tr", LEGEND_THEAD, legendEl);
  			placeTag("th", null, head);
  
  			for (var key in legendCols)
  				placeTag("th", LEGEND_LABEL, head).textContent = key;
  		}
  		else {
  			addClass(legendEl, LEGEND_INLINE);
  			legend.live && addClass(legendEl, LEGEND_LIVE);
  		}
  	}
  
  	const son  = {show: true};
  	const soff = {show: false};
  
  	function initLegendRow(s, i) {
  		if (i == 0 && (multiValLegend || !legend.live || mode == 2))
  			return nullNullTuple;
  
  		let cells = [];
  
  		let row = placeTag("tr", LEGEND_SERIES, legendEl, legendEl.childNodes[i]);
  
  		addClass(row, s.class);
  
  		if (!s.show)
  			addClass(row, OFF);
  
  		let label = placeTag("th", null, row);
  
  		if (markers.show) {
  			let indic = placeDiv(LEGEND_MARKER, label);
  
  			if (i > 0) {
  				let width  = markers.width(self, i);
  
  				if (width)
  					indic.style.border = width + "px " + markers.dash(self, i) + " " + markers.stroke(self, i);
  
  				indic.style.background = markers.fill(self, i);
  			}
  		}
  
  		let text = placeDiv(LEGEND_LABEL, label);
  		text.textContent = s.label;
  
  		if (i > 0) {
  			if (!markers.show)
  				text.style.color = s.width > 0 ? markers.stroke(self, i) : markers.fill(self, i);
  
  			onMouse("click", label, e => {
  				if (FEAT_CURSOR && cursor._lock)
  					return;
  
  				let seriesIdx = series.indexOf(s);
  
  				if ((e.ctrlKey || e.metaKey) != legend.isolate) {
  					// if any other series is shown, isolate this one. else show all
  					let isolate = series.some((s, i) => i > 0 && i != seriesIdx && s.show);
  
  					series.forEach((s, i) => {
  						i > 0 && setSeries(i, isolate ? (i == seriesIdx ? son : soff) : son, true, FEAT_CURSOR && syncOpts.setSeries);
  					});
  				}
  				else
  					setSeries(seriesIdx, {show: !s.show}, true, FEAT_CURSOR && syncOpts.setSeries);
  			});
  
  			if (cursorFocus) {
  				onMouse(mouseenter, label, e => {
  					if (cursor._lock)
  						return;
  
  					setSeries(series.indexOf(s), FOCUS_TRUE, true, syncOpts.setSeries);
  				});
  			}
  		}
  
  		for (var key in legendCols) {
  			let v = placeTag("td", LEGEND_VALUE, row);
  			v.textContent = "--";
  			cells.push(v);
  		}
  
  		return [row, cells];
  	}
  
  	const mouseListeners = new Map();
  
  	function onMouse(ev, targ, fn) {
  		const targListeners = mouseListeners.get(targ) || {};
  		const listener = cursor.bind[ev](self, targ, fn);
  
  		if (listener) {
  			on(ev, targ, targListeners[ev] = listener);
  			mouseListeners.set(targ, targListeners);
  		}
  	}
  
  	function offMouse(ev, targ, fn) {
  		const targListeners = mouseListeners.get(targ) || {};
  
  		for (let k in targListeners) {
  			if (ev == null || k == ev) {
  				off(k, targ, targListeners[k]);
  				delete targListeners[k];
  			}
  		}
  
  		if (ev == null)
  			mouseListeners.delete(targ);
  	}
  
  	let fullWidCss = 0;
  	let fullHgtCss = 0;
  
  	let plotWidCss = 0;
  	let plotHgtCss = 0;
  
  	// plot margins to account for axes
  	let plotLftCss = 0;
  	let plotTopCss = 0;
  
  	let plotLft = 0;
  	let plotTop = 0;
  	let plotWid = 0;
  	let plotHgt = 0;
  
  	self.bbox = {};
  
  	let shouldSetScales = false;
  	let shouldSetSize = false;
  	let shouldConvergeSize = false;
  	let shouldSetCursor = false;
  	let shouldSetLegend = false;
  
  	function _setSize(width, height, force) {
  		if (force || (width != self.width || height != self.height))
  			calcSize(width, height);
  
  		resetYSeries(false);
  
  		shouldConvergeSize = true;
  		shouldSetSize = true;
  		shouldSetCursor = shouldSetLegend = cursor.left >= 0;
  		commit();
  	}
  
  	function calcSize(width, height) {
  	//	log("calcSize()", arguments);
  
  		self.width  = fullWidCss = plotWidCss = width;
  		self.height = fullHgtCss = plotHgtCss = height;
  		plotLftCss  = plotTopCss = 0;
  
  		calcPlotRect();
  		calcAxesRects();
  
  		let bb = self.bbox;
  
  		plotLft = bb.left   = incrRound(plotLftCss * pxRatio, 0.5);
  		plotTop = bb.top    = incrRound(plotTopCss * pxRatio, 0.5);
  		plotWid = bb.width  = incrRound(plotWidCss * pxRatio, 0.5);
  		plotHgt = bb.height = incrRound(plotHgtCss * pxRatio, 0.5);
  
  	//	updOriDims();
  	}
  
  	// ensures size calc convergence
  	const CYCLE_LIMIT = 3;
  
  	function convergeSize() {
  		let converged = false;
  
  		let cycleNum = 0;
  
  		while (!converged) {
  			cycleNum++;
  
  			let axesConverged = axesCalc(cycleNum);
  			let paddingConverged = paddingCalc(cycleNum);
  
  			converged = cycleNum == CYCLE_LIMIT || (axesConverged && paddingConverged);
  
  			if (!converged) {
  				calcSize(self.width, self.height);
  				shouldSetSize = true;
  			}
  		}
  	}
  
  	function setSize({width, height}) {
  		_setSize(width, height);
  	}
  
  	self.setSize = setSize;
  
  	// accumulate axis offsets, reduce canvas width
  	function calcPlotRect() {
  		// easements for edge labels
  		let hasTopAxis = false;
  		let hasBtmAxis = false;
  		let hasRgtAxis = false;
  		let hasLftAxis = false;
  
  		axes.forEach((axis, i) => {
  			if (axis.show && axis._show) {
  				let {side, _size} = axis;
  				let isVt = side % 2;
  				let labelSize = axis.label != null ? axis.labelSize : 0;
  
  				let fullSize = _size + labelSize;
  
  				if (fullSize > 0) {
  					if (isVt) {
  						plotWidCss -= fullSize;
  
  						if (side == 3) {
  							plotLftCss += fullSize;
  							hasLftAxis = true;
  						}
  						else
  							hasRgtAxis = true;
  					}
  					else {
  						plotHgtCss -= fullSize;
  
  						if (side == 0) {
  							plotTopCss += fullSize;
  							hasTopAxis = true;
  						}
  						else
  							hasBtmAxis = true;
  					}
  				}
  			}
  		});
  
  		sidesWithAxes[0] = hasTopAxis;
  		sidesWithAxes[1] = hasRgtAxis;
  		sidesWithAxes[2] = hasBtmAxis;
  		sidesWithAxes[3] = hasLftAxis;
  
  		// hz padding
  		plotWidCss -= _padding[1] + _padding[3];
  		plotLftCss += _padding[3];
  
  		// vt padding
  		plotHgtCss -= _padding[2] + _padding[0];
  		plotTopCss += _padding[0];
  	}
  
  	function calcAxesRects() {
  		// will accum +
  		let off1 = plotLftCss + plotWidCss;
  		let off2 = plotTopCss + plotHgtCss;
  		// will accum -
  		let off3 = plotLftCss;
  		let off0 = plotTopCss;
  
  		function incrOffset(side, size) {
  			switch (side) {
  				case 1: off1 += size; return off1 - size;
  				case 2: off2 += size; return off2 - size;
  				case 3: off3 -= size; return off3 + size;
  				case 0: off0 -= size; return off0 + size;
  			}
  		}
  
  		axes.forEach((axis, i) => {
  			if (axis.show && axis._show) {
  				let side = axis.side;
  
  				axis._pos = incrOffset(side, axis._size);
  
  				if (axis.label != null)
  					axis._lpos = incrOffset(side, axis.labelSize);
  			}
  		});
  	}
  
  	const cursor = FEAT_CURSOR && (self.cursor = assign({}, cursorOpts, {drag: {y: mode == 2}}, opts.cursor));
  
  	if (FEAT_CURSOR) {
  		cursor.idxs = activeIdxs;
  
  		cursor._lock = false;
  
  		let points = cursor.points;
  
  		points.show   = fnOrSelf(points.show);
  		points.size   = fnOrSelf(points.size);
  		points.stroke = fnOrSelf(points.stroke);
  		points.width  = fnOrSelf(points.width);
  		points.fill   = fnOrSelf(points.fill);
  	}
  
  	const focus = self.focus = assign({}, opts.focus || {alpha: 0.3}, FEAT_CURSOR && cursor.focus);
  	const cursorFocus = FEAT_CURSOR && focus.prox >= 0;
  
  	// series-intersection markers
  	let cursorPts = [null];
  
  	function initCursorPt(s, si) {
  		if (si > 0) {
  			let pt = cursor.points.show(self, si);
  
  			if (pt) {
  				addClass(pt, CURSOR_PT);
  				addClass(pt, s.class);
  				elTrans(pt, -10, -10, plotWidCss, plotHgtCss);
  				over.insertBefore(pt, cursorPts[si]);
  
  				return pt;
  			}
  		}
  	}
  
  	function initSeries(s, i) {
  		if (mode == 1 || i > 0) {
  			let isTime = FEAT_TIME && mode == 1 && scales[s.scale].time;
  
  			let sv = s.value;
  			s.value = isTime ? (isStr(sv) ? timeSeriesVal(_tzDate, timeSeriesStamp(sv, _fmtDate)) : sv || _timeSeriesVal) : sv || numSeriesVal;
  			s.label = s.label || (isTime ? timeSeriesLabel : numSeriesLabel);
  		}
  
  		if (i > 0) {
  			s.width  = s.width == null ? 1 : s.width;
  			s.paths  = s.paths || linearPath || retNull;
  			s.fillTo = fnOrSelf(s.fillTo || seriesFillTo);
  			s.pxAlign = +ifNull(s.pxAlign, pxAlign);
  			s.pxRound = pxRoundGen(s.pxAlign);
  
  			s.stroke = fnOrSelf(s.stroke || null);
  			s.fill   = fnOrSelf(s.fill || null);
  			s._stroke = s._fill = s._paths = s._focus = null;
  
  			let _ptDia = ptDia(s.width, 1);
  			let points = s.points = assign({}, {
  				size: _ptDia,
  				width: max(1, _ptDia * .2),
  				stroke: s.stroke,
  				space: _ptDia * 2,
  				paths: pointsPath,
  				_stroke: null,
  				_fill: null,
  			}, s.points);
  			points.show   = fnOrSelf(points.show);
  			points.filter = fnOrSelf(points.filter);
  			points.fill   = fnOrSelf(points.fill);
  			points.stroke = fnOrSelf(points.stroke);
  			points.paths  = fnOrSelf(points.paths);
  			points.pxAlign = s.pxAlign;
  		}
  
  		if (showLegend) {
  			let rowCells = initLegendRow(s, i);
  			legendRows.splice(i, 0, rowCells[0]);
  			legendCells.splice(i, 0, rowCells[1]);
  			legend.values.push(null);	// NULL_LEGEND_VALS not yet avil here :(
  		}
  
  		if (FEAT_CURSOR && cursor.show) {
  			activeIdxs.splice(i, 0, null);
  
  			let pt = initCursorPt(s, i);
  			pt && cursorPts.splice(i, 0, pt);
  		}
  
  		fire("addSeries", i);
  	}
  
  	function addSeries(opts, si) {
  		si = si == null ? series.length : si;
  
  		opts = setDefault(opts, si, xSeriesOpts, ySeriesOpts);
  		series.splice(si, 0, opts);
  		initSeries(series[si], si);
  	}
  
  	self.addSeries = addSeries;
  
  	function delSeries(i) {
  		series.splice(i, 1);
  
  		if (showLegend) {
  			legend.values.splice(i, 1);
  
  			legendCells.splice(i, 1);
  			let tr = legendRows.splice(i, 1)[0];
  			offMouse(null, tr.firstChild);
  			tr.remove();
  		}
  
  		if (FEAT_CURSOR && cursor.show) {
  			activeIdxs.splice(i, 1);
  
  			cursorPts.length > 1 && cursorPts.splice(i, 1)[0].remove();
  		}
  
  		// TODO: de-init no-longer-needed scales?
  
  		fire("delSeries", i);
  	}
  
  	self.delSeries = delSeries;
  
  	const sidesWithAxes = [false, false, false, false];
  
  	function initAxis(axis, i) {
  		axis._show = axis.show;
  
  		if (axis.show) {
  			let isVt = axis.side % 2;
  
  			let sc = scales[axis.scale];
  
  			// this can occur if all series specify non-default scales
  			if (sc == null) {
  				axis.scale = isVt ? series[1].scale : xScaleKey;
  				sc = scales[axis.scale];
  			}
  
  			// also set defaults for incrs & values based on axis distr
  			let isTime = FEAT_TIME && sc.time;
  
  			axis.size   = fnOrSelf(axis.size);
  			axis.space  = fnOrSelf(axis.space);
  			axis.rotate = fnOrSelf(axis.rotate);
  			axis.incrs  = fnOrSelf(axis.incrs  || (          sc.distr == 2 ? wholeIncrs : (isTime ? (ms == 1 ? timeIncrsMs : timeIncrsS) : numIncrs)));
  			axis.splits = fnOrSelf(axis.splits || (isTime && sc.distr == 1 ? _timeAxisSplits : sc.distr == 3 ? logAxisSplits : sc.distr == 4 ? asinhAxisSplits : numAxisSplits));
  
  			axis.stroke       = fnOrSelf(axis.stroke);
  			axis.grid.stroke  = fnOrSelf(axis.grid.stroke);
  			axis.ticks.stroke = fnOrSelf(axis.ticks.stroke);
  
  			let av = axis.values;
  
  			axis.values = (
  				// static array of tick values
  				isArr(av) && !isArr(av[0]) ? fnOrSelf(av) :
  				// temporal
  				isTime ? (
  					// config array of fmtDate string tpls
  					isArr(av) ?
  						timeAxisVals(_tzDate, timeAxisStamps(av, _fmtDate)) :
  					// fmtDate string tpl
  					isStr(av) ?
  						timeAxisVal(_tzDate, av) :
  					av || _timeAxisVals
  				) : av || numAxisVals
  			);
  
  			axis.filter = fnOrSelf(axis.filter || (          sc.distr >= 3 ? logAxisValsFilt : retArg1));
  
  			axis.font      = pxRatioFont(axis.font);
  			axis.labelFont = pxRatioFont(axis.labelFont);
  
  			axis._size   = axis.size(self, null, i, 0);
  
  			axis._space  =
  			axis._rotate =
  			axis._incrs  =
  			axis._found  =	// foundIncrSpace
  			axis._splits =
  			axis._values = null;
  
  			if (axis._size > 0) {
  				sidesWithAxes[i] = true;
  				axis._el = placeDiv(AXIS, wrap);
  			}
  
  			// debug
  		//	axis._el.style.background = "#"  + Math.floor(Math.random()*16777215).toString(16) + '80';
  		}
  	}
  
  	function autoPadSide(self, side, sidesWithAxes, cycleNum) {
  		let [hasTopAxis, hasRgtAxis, hasBtmAxis, hasLftAxis] = sidesWithAxes;
  
  		let ori = side % 2;
  		let size = 0;
  
  		if (ori == 0 && (hasLftAxis || hasRgtAxis))
  			size = (side == 0 && !hasTopAxis || side == 2 && !hasBtmAxis ? round(xAxisOpts.size / 3) : 0);
  		if (ori == 1 && (hasTopAxis || hasBtmAxis))
  			size = (side == 1 && !hasRgtAxis || side == 3 && !hasLftAxis ? round(yAxisOpts.size / 2) : 0);
  
  		return size;
  	}
  
  	const padding = self.padding = (opts.padding || [autoPadSide,autoPadSide,autoPadSide,autoPadSide]).map(p => fnOrSelf(ifNull(p, autoPadSide)));
  	const _padding = self._padding = padding.map((p, i) => p(self, i, sidesWithAxes, 0));
  
  	let dataLen;
  
  	// rendered data window
  	let i0 = null;
  	let i1 = null;
  	const idxs = mode == 1 ? series[0].idxs : null;
  
  	let data0 = null;
  
  	let viaAutoScaleX = false;
  
  	function setData(_data, _resetScales) {
  		if (mode == 2) {
  			dataLen = 0;
  			for (let i = 1; i < series.length; i++)
  				dataLen += data[i][0].length;
  			self.data = data = _data;
  		}
  		else {
  			data = (_data || []).slice();
  			data[0] = data[0] || [];
  
  			self.data = data.slice();
  			data0 = data[0];
  			dataLen = data0.length;
  
  			if (xScaleDistr == 2)
  				data[0] = data0.map((v, i) => i);
  		}
  
  		self._data = data;
  
  		resetYSeries(true);
  
  		fire("setData");
  
  		if (_resetScales !== false) {
  			let xsc = scaleX;
  
  			if (xsc.auto(self, viaAutoScaleX))
  				autoScaleX();
  			else
  				_setScale(xScaleKey, xsc.min, xsc.max);
  
  			shouldSetCursor = cursor.left >= 0;
  			shouldSetLegend = true;
  			commit();
  		}
  	}
  
  	self.setData = setData;
  
  	function autoScaleX() {
  		viaAutoScaleX = true;
  
  		let _min, _max;
  
  		if (mode == 1) {
  			if (dataLen > 0) {
  				i0 = idxs[0] = 0;
  				i1 = idxs[1] = dataLen - 1;
  
  				_min = data[0][i0];
  				_max = data[0][i1];
  
  				if (xScaleDistr == 2) {
  					_min = i0;
  					_max = i1;
  				}
  				else if (dataLen == 1) {
  					if (xScaleDistr == 3)
  						[_min, _max] = rangeLog(_min, _min, scaleX.log, false);
  					else if (xScaleDistr == 4)
  						[_min, _max] = rangeAsinh(_min, _min, scaleX.log, false);
  					else if (scaleX.time)
  						_max = _min + round(86400 / ms);
  					else
  						[_min, _max] = rangeNum(_min, _max, rangePad, true);
  				}
  			}
  			else {
  				i0 = idxs[0] = _min = null;
  				i1 = idxs[1] = _max = null;
  			}
  		}
  
  		_setScale(xScaleKey, _min, _max);
  	}
  
  	let ctxStroke, ctxFill, ctxWidth, ctxDash, ctxJoin, ctxCap, ctxFont, ctxAlign, ctxBaseline;
  	let ctxAlpha;
  
  	function setCtxStyle(stroke = transparent, width, dash = EMPTY_ARR, cap = "butt", fill = transparent, join = "round") {
  		if (stroke != ctxStroke)
  			ctx.strokeStyle = ctxStroke = stroke;
  		if (fill != ctxFill)
  			ctx.fillStyle = ctxFill = fill;
  		if (width != ctxWidth)
  			ctx.lineWidth = ctxWidth = width;
  		if (join != ctxJoin)
  			ctx.lineJoin = ctxJoin = join;
  		if (cap != ctxCap)
  			ctx.lineCap = ctxCap = cap; // (‿|‿)
  		if (dash != ctxDash)
  			ctx.setLineDash(ctxDash = dash);
  	}
  
  	function setFontStyle(font, fill, align, baseline) {
  		if (fill != ctxFill)
  			ctx.fillStyle = ctxFill = fill;
  		if (font != ctxFont)
  			ctx.font = ctxFont = font;
  		if (align != ctxAlign)
  			ctx.textAlign = ctxAlign = align;
  		if (baseline != ctxBaseline)
  			ctx.textBaseline = ctxBaseline = baseline;
  	}
  
  	function accScale(wsc, psc, facet, data) {
  		if (wsc.auto(self, viaAutoScaleX) && (psc == null || psc.min == null)) {
  			let _i0 = ifNull(i0, 0);
  			let _i1 = ifNull(i1, data.length - 1);
  
  			// only run getMinMax() for invalidated series data, else reuse
  			let minMax = facet.min == null ? (wsc.distr == 3 ? getMinMaxLog(data, _i0, _i1) : getMinMax(data, _i0, _i1)) : [facet.min, facet.max];
  
  			// initial min/max
  			wsc.min = min(wsc.min, facet.min = minMax[0]);
  			wsc.max = max(wsc.max, facet.max = minMax[1]);
  		}
  	}
  
  	function setScales() {
  	//	log("setScales()", arguments);
  
  		// wip scales
  		let wipScales = copy(scales, fastIsObj);
  
  		for (let k in wipScales) {
  			let wsc = wipScales[k];
  			let psc = pendScales[k];
  
  			if (psc != null && psc.min != null) {
  				assign(wsc, psc);
  
  				// explicitly setting the x-scale invalidates everything (acts as redraw)
  				if (k == xScaleKey)
  					resetYSeries(true);
  			}
  			else if (k != xScaleKey || mode == 2) {
  				if (dataLen == 0 && wsc.from == null) {
  					let minMax = wsc.range(self, null, null, k);
  					wsc.min = minMax[0];
  					wsc.max = minMax[1];
  				}
  				else {
  					wsc.min = inf;
  					wsc.max = -inf;
  				}
  			}
  		}
  
  		if (dataLen > 0) {
  			// pre-range y-scales from y series' data values
  			series.forEach((s, i) => {
  				if (mode == 1) {
  					let k = s.scale;
  					let wsc = wipScales[k];
  					let psc = pendScales[k];
  
  					if (i == 0) {
  						let minMax = wsc.range(self, wsc.min, wsc.max, k);
  
  						wsc.min = minMax[0];
  						wsc.max = minMax[1];
  
  						i0 = closestIdx(wsc.min, data[0]);
  						i1 = closestIdx(wsc.max, data[0]);
  
  						// closest indices can be outside of view
  						if (data[0][i0] < wsc.min)
  							i0++;
  						if (data[0][i1] > wsc.max)
  							i1--;
  
  						s.min = data0[i0];
  						s.max = data0[i1];
  					}
  					else if (s.show && s.auto)
  						accScale(wsc, psc, s, data[i]);
  
  					s.idxs[0] = i0;
  					s.idxs[1] = i1;
  				}
  				else {
  					if (i > 0) {
  						if (s.show && s.auto) {
  							// TODO: only handles, assumes and requires facets[0] / 'x' scale, and facets[1] / 'y' scale
  							let [ xFacet, yFacet ] = s.facets;
  							let xScaleKey = xFacet.scale;
  							let yScaleKey = yFacet.scale;
  							let [ xData, yData ] = data[i];
  
  							accScale(wipScales[xScaleKey], pendScales[xScaleKey], xFacet, xData);
  							accScale(wipScales[yScaleKey], pendScales[yScaleKey], yFacet, yData);
  
  							// temp
  							s.min = yFacet.min;
  							s.max = yFacet.max;
  						}
  					}
  				}
  			});
  
  			// range independent scales
  			for (let k in wipScales) {
  				let wsc = wipScales[k];
  				let psc = pendScales[k];
  
  				if (wsc.from == null && (psc == null || psc.min == null)) {
  					let minMax = wsc.range(
  						self,
  						wsc.min ==  inf ? null : wsc.min,
  						wsc.max == -inf ? null : wsc.max,
  						k
  					);
  					wsc.min = minMax[0];
  					wsc.max = minMax[1];
  				}
  			}
  		}
  
  		// range dependent scales
  		for (let k in wipScales) {
  			let wsc = wipScales[k];
  
  			if (wsc.from != null) {
  				let base = wipScales[wsc.from];
  
  				if (base.min == null)
  					wsc.min = wsc.max = null;
  				else {
  					let minMax = wsc.range(self, base.min, base.max, k);
  					wsc.min = minMax[0];
  					wsc.max = minMax[1];
  				}
  			}
  		}
  
  		let changed = {};
  		let anyChanged = false;
  
  		for (let k in wipScales) {
  			let wsc = wipScales[k];
  			let sc = scales[k];
  
  			if (sc.min != wsc.min || sc.max != wsc.max) {
  				sc.min = wsc.min;
  				sc.max = wsc.max;
  
  				let distr = sc.distr;
  
  				sc._min = distr == 3 ? log10(sc.min) : distr == 4 ? asinh(sc.min, sc.asinh) : sc.min;
  				sc._max = distr == 3 ? log10(sc.max) : distr == 4 ? asinh(sc.max, sc.asinh) : sc.max;
  
  				changed[k] = anyChanged = true;
  			}
  		}
  
  		if (anyChanged) {
  			// invalidate paths of all series on changed scales
  			series.forEach((s, i) => {
  				if (mode == 2) {
  					if (i > 0 && changed.y)
  						s._paths = null;
  				}
  				else {
  					if (changed[s.scale])
  						s._paths = null;
  				}
  			});
  
  			for (let k in changed) {
  				shouldConvergeSize = true;
  				fire("setScale", k);
  			}
  
  			if (FEAT_CURSOR && cursor.show)
  				shouldSetCursor = shouldSetLegend = cursor.left >= 0;
  		}
  
  		for (let k in pendScales)
  			pendScales[k] = null;
  	}
  
  	// grabs the nearest indices with y data outside of x-scale limits
  	function getOuterIdxs(ydata) {
  		let _i0 = clamp(i0 - 1, 0, dataLen - 1);
  		let _i1 = clamp(i1 + 1, 0, dataLen - 1);
  
  		while (ydata[_i0] == null && _i0 > 0)
  			_i0--;
  
  		while (ydata[_i1] == null && _i1 < dataLen - 1)
  			_i1++;
  
  		return [_i0, _i1];
  	}
  
  	function drawSeries() {
  		if (dataLen > 0) {
  			series.forEach((s, i) => {
  				if (i > 0 && s.show && s._paths == null) {
  					let _idxs = getOuterIdxs(data[i]);
  					s._paths = s.paths(self, i, _idxs[0], _idxs[1]);
  				}
  			});
  
  			series.forEach((s, i) => {
  				if (i > 0 && s.show) {
  					if (ctxAlpha != s.alpha)
  						ctx.globalAlpha = ctxAlpha = s.alpha;
  
  					if (FEAT_PATHS) {
  						cacheStrokeFill(i, false);
  						s._paths && drawPath(i, false);
  					}
  
  					if (FEAT_POINTS) {
  						cacheStrokeFill(i, true);
  
  						let show = s.points.show(self, i, i0, i1);
  						let idxs = s.points.filter(self, i, show, s._paths ? s._paths.gaps : null);
  
  						if (show || idxs) {
  							s.points._paths = s.points.paths(self, i, i0, i1, idxs);
  							drawPath(i, true);
  						}
  					}
  
  					if (ctxAlpha != 1)
  						ctx.globalAlpha = ctxAlpha = 1;
  
  					fire("drawSeries", i);
  				}
  			});
  		}
  	}
  
  	function cacheStrokeFill(si, _points) {
  		let s = _points ? series[si].points : series[si];
  
  		s._stroke = s.stroke(self, si);
  		s._fill   = s.fill(self, si);
  	}
  
  	function drawPath(si, _points) {
  		let s = _points ? series[si].points : series[si];
  
  		let strokeStyle = s._stroke;
  		let fillStyle   = s._fill;
  
  		let { stroke, fill, clip: gapsClip, flags } = s._paths;
  		let boundsClip = null;
  		let width = roundDec(s.width * pxRatio, 3);
  		let offset = (width % 2) / 2;
  
  		if (_points && fillStyle == null)
  			fillStyle = width > 0 ? "#fff" : strokeStyle;
  
  		let _pxAlign = s.pxAlign == 1;
  
  		_pxAlign && ctx.translate(offset, offset);
  
  		if (!_points) {
  			let lft = plotLft,
  				top = plotTop,
  				wid = plotWid,
  				hgt = plotHgt;
  
  			let halfWid = width * pxRatio / 2;
  
  			if (s.min == 0)
  				hgt += halfWid;
  
  			if (s.max == 0) {
  				top -= halfWid;
  				hgt += halfWid;
  			}
  
  			boundsClip = new Path2D();
  			boundsClip.rect(lft, top, wid, hgt);
  		}
  
  		// the points pathbuilder's gapsClip is its boundsClip, since points dont need gaps clipping, and bounds depend on point size
  		if (_points)
  			strokeFill(strokeStyle, width, s.dash, s.cap, fillStyle, stroke, fill, flags, gapsClip);
  		else
  			fillStroke(si, strokeStyle, width, s.dash, s.cap, fillStyle, stroke, fill, flags, boundsClip, gapsClip);
  
  		_pxAlign && ctx.translate(-offset, -offset);
  	}
  
  	function fillStroke(si, strokeStyle, lineWidth, lineDash, lineCap, fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip) {
  		let didStrokeFill = false;
  
  		// for all bands where this series is the top edge, create upwards clips using the bottom edges
  		// and apply clips + fill with band fill or dfltFill
  		bands.forEach((b, bi) => {
  			// isUpperEdge?
  			if (b.series[0] == si) {
  				let lowerEdge = series[b.series[1]];
  				let lowerData = data[b.series[1]];
  
  				let bandClip = (lowerEdge._paths || EMPTY_OBJ).band;
  				let gapsClip2;
  
  				let _fillStyle = null;
  
  				// hasLowerEdge?
  				if (lowerEdge.show && bandClip && hasData(lowerData, i0, i1)) {
  					_fillStyle = b.fill(self, bi) || fillStyle;
  					gapsClip2 = lowerEdge._paths.clip;
  				}
  				else
  					bandClip = null;
  
  				strokeFill(strokeStyle, lineWidth, lineDash, lineCap, _fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip, gapsClip2, bandClip);
  
  				didStrokeFill = true;
  			}
  		});
  
  		if (!didStrokeFill)
  			strokeFill(strokeStyle, lineWidth, lineDash, lineCap, fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip);
  	}
  
  	const CLIP_FILL_STROKE = BAND_CLIP_FILL | BAND_CLIP_STROKE;
  
  	function strokeFill(strokeStyle, lineWidth, lineDash, lineCap, fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip, gapsClip2, bandClip) {
  		setCtxStyle(strokeStyle, lineWidth, lineDash, lineCap, fillStyle);
  
  		if (boundsClip || gapsClip || bandClip) {
  			ctx.save();
  			boundsClip && ctx.clip(boundsClip);
  			gapsClip && ctx.clip(gapsClip);
  		}
  
  		if (bandClip) {
  			if ((flags & CLIP_FILL_STROKE) == CLIP_FILL_STROKE) {
  				ctx.clip(bandClip);
  				gapsClip2 && ctx.clip(gapsClip2);
  				doFill(fillStyle, fillPath);
  				doStroke(strokeStyle, strokePath, lineWidth);
  			}
  			else if (flags & BAND_CLIP_STROKE) {
  				doFill(fillStyle, fillPath);
  				ctx.clip(bandClip);
  				doStroke(strokeStyle, strokePath, lineWidth);
  			}
  			else if (flags & BAND_CLIP_FILL) {
  				ctx.save();
  				ctx.clip(bandClip);
  				gapsClip2 && ctx.clip(gapsClip2);
  				doFill(fillStyle, fillPath);
  				ctx.restore();
  				doStroke(strokeStyle, strokePath, lineWidth);
  			}
  		}
  		else {
  			doFill(fillStyle, fillPath);
  			doStroke(strokeStyle, strokePath, lineWidth);
  		}
  
  		if (boundsClip || gapsClip || bandClip)
  			ctx.restore();
  	}
  
  	function doStroke(strokeStyle, strokePath, lineWidth) {
  		if (lineWidth > 0) {
  			if (strokePath instanceof Map) {
  				strokePath.forEach((strokePath, strokeStyle) => {
  					ctx.strokeStyle = ctxStroke = strokeStyle;
  					ctx.stroke(strokePath);
  				});
  			}
  			else
  				strokePath != null && strokeStyle && ctx.stroke(strokePath);
  		}
  	}
  
  	function doFill(fillStyle, fillPath) {
  		if (fillPath instanceof Map) {
  			fillPath.forEach((fillPath, fillStyle) => {
  				ctx.fillStyle = ctxFill = fillStyle;
  				ctx.fill(fillPath);
  			});
  		}
  		else
  			fillPath != null && fillStyle && ctx.fill(fillPath);
  	}
  
  	function getIncrSpace(axisIdx, min, max, fullDim) {
  		let axis = axes[axisIdx];
  
  		let incrSpace;
  
  		if (fullDim <= 0)
  			incrSpace = [0, 0];
  		else {
  			let minSpace = axis._space = axis.space(self, axisIdx, min, max, fullDim);
  			let incrs    = axis._incrs = axis.incrs(self, axisIdx, min, max, fullDim, minSpace);
  			incrSpace    = findIncr(min, max, incrs, fullDim, minSpace);
  		}
  
  		return (axis._found = incrSpace);
  	}
  
  	function drawOrthoLines(offs, filts, ori, side, pos0, len, width, stroke, dash, cap) {
  		let offset = (width % 2) / 2;
  
  		pxAlign == 1 && ctx.translate(offset, offset);
  
  		setCtxStyle(stroke, width, dash, cap, stroke);
  
  		ctx.beginPath();
  
  		let x0, y0, x1, y1, pos1 = pos0 + (side == 0 || side == 3 ? -len : len);
  
  		if (ori == 0) {
  			y0 = pos0;
  			y1 = pos1;
  		}
  		else {
  			x0 = pos0;
  			x1 = pos1;
  		}
  
  		for (let i = 0; i < offs.length; i++) {
  			if (filts[i] != null) {
  				if (ori == 0)
  					x0 = x1 = offs[i];
  				else
  					y0 = y1 = offs[i];
  
  				ctx.moveTo(x0, y0);
  				ctx.lineTo(x1, y1);
  			}
  		}
  
  		ctx.stroke();
  
  		pxAlign == 1 && ctx.translate(-offset, -offset);
  	}
  
  	function axesCalc(cycleNum) {
  	//	log("axesCalc()", arguments);
  
  		let converged = true;
  
  		axes.forEach((axis, i) => {
  			if (!axis.show)
  				return;
  
  			let scale = scales[axis.scale];
  
  			if (scale.min == null) {
  				if (axis._show) {
  					converged = false;
  					axis._show = false;
  					resetYSeries(false);
  				}
  				return;
  			}
  			else {
  				if (!axis._show) {
  					converged = false;
  					axis._show = true;
  					resetYSeries(false);
  				}
  			}
  
  			let side = axis.side;
  			let ori = side % 2;
  
  			let {min, max} = scale;		// 		// should this toggle them ._show = false
  
  			let [_incr, _space] = getIncrSpace(i, min, max, ori == 0 ? plotWidCss : plotHgtCss);
  
  			if (_space == 0)
  				return;
  
  			// if we're using index positions, force first tick to match passed index
  			let forceMin = scale.distr == 2;
  
  			let _splits = axis._splits = axis.splits(self, i, min, max, _incr, _space, forceMin);
  
  			// tick labels
  			// BOO this assumes a specific data/series
  			let splits = scale.distr == 2 ? _splits.map(i => data0[i]) : _splits;
  			let incr   = scale.distr == 2 ? data0[_splits[1]] - data0[_splits[0]] : _incr;
  
  			let values = axis._values = axis.values(self, axis.filter(self, splits, i, _space, incr), i, _space, incr);
  
  			// rotating of labels only supported on bottom x axis
  			axis._rotate = side == 2 ? axis.rotate(self, values, i, _space) : 0;
  
  			let oldSize = axis._size;
  
  			axis._size = ceil(axis.size(self, values, i, cycleNum));
  
  			if (oldSize != null && axis._size != oldSize)			// ready && ?
  				converged = false;
  		});
  
  		return converged;
  	}
  
  	function paddingCalc(cycleNum) {
  		let converged = true;
  
  		padding.forEach((p, i) => {
  			let _p = p(self, i, sidesWithAxes, cycleNum);
  
  			if (_p != _padding[i])
  				converged = false;
  
  			_padding[i] = _p;
  		});
  
  		return converged;
  	}
  
  	function drawAxesGrid() {
  		for (let i = 0; i < axes.length; i++) {
  			let axis = axes[i];
  
  			if (!axis.show || !axis._show)
  				continue;
  
  			let side = axis.side;
  			let ori = side % 2;
  
  			let x, y;
  
  			let fillStyle = axis.stroke(self, i);
  
  			let shiftDir = side == 0 || side == 3 ? -1 : 1;
  
  			// axis label
  			if (axis.label) {
  				let shiftAmt = axis.labelGap * shiftDir;
  				let baseLpos = round((axis._lpos + shiftAmt) * pxRatio);
  
  				setFontStyle(axis.labelFont[0], fillStyle, "center", side == 2 ? TOP : BOTTOM);
  
  				ctx.save();
  
  				if (ori == 1) {
  					x = y = 0;
  
  					ctx.translate(
  						baseLpos,
  						round(plotTop + plotHgt / 2),
  					);
  					ctx.rotate((side == 3 ? -PI : PI) / 2);
  
  				}
  				else {
  					x = round(plotLft + plotWid / 2);
  					y = baseLpos;
  				}
  
  				ctx.fillText(axis.label, x, y);
  
  				ctx.restore();
  			}
  
  			let [_incr, _space] = axis._found;
  
  			if (_space == 0)
  				continue;
  
  			let scale = scales[axis.scale];
  
  			let plotDim = ori == 0 ? plotWid : plotHgt;
  			let plotOff = ori == 0 ? plotLft : plotTop;
  
  			let axisGap = round(axis.gap * pxRatio);
  
  			let _splits = axis._splits;
  
  			// tick labels
  			// BOO this assumes a specific data/series
  			let splits = scale.distr == 2 ? _splits.map(i => data0[i]) : _splits;
  			let incr   = scale.distr == 2 ? data0[_splits[1]] - data0[_splits[0]] : _incr;
  
  			let ticks = axis.ticks;
  			let tickSize = ticks.show ? round(ticks.size * pxRatio) : 0;
  
  			// rotating of labels only supported on bottom x axis
  			let angle = axis._rotate * -PI/180;
  
  			let basePos  = pxRound(axis._pos * pxRatio);
  			let shiftAmt = (tickSize + axisGap) * shiftDir;
  			let finalPos = basePos + shiftAmt;
  			    y        = ori == 0 ? finalPos : 0;
  			    x        = ori == 1 ? finalPos : 0;
  
  			let font         = axis.font[0];
  			let textAlign    = axis.align == 1 ? LEFT :
  			                   axis.align == 2 ? RIGHT :
  			                   angle > 0 ? LEFT :
  			                   angle < 0 ? RIGHT :
  			                   ori == 0 ? "center" : side == 3 ? RIGHT : LEFT;
  			let textBaseline = angle ||
  			                   ori == 1 ? "middle" : side == 2 ? TOP   : BOTTOM;
  
  			setFontStyle(font, fillStyle, textAlign, textBaseline);
  
  			let lineHeight = axis.font[1] * lineMult;
  
  			let canOffs = _splits.map(val => pxRound(getPos(val, scale, plotDim, plotOff)));
  
  			let _values = axis._values;
  
  			for (let i = 0; i < _values.length; i++) {
  				let val = _values[i];
  
  				if (val != null) {
  					if (ori == 0)
  						x = canOffs[i];
  					else
  						y = canOffs[i];
  
  					val = "" + val;
  
  					let _parts = val.indexOf("\n") == -1 ? [val] : val.split(/\n/gm);
  
  					for (let j = 0; j < _parts.length; j++) {
  						let text = _parts[j];
  
  						if (angle) {
  							ctx.save();
  							ctx.translate(x, y + j * lineHeight); // can this be replaced with position math?
  							ctx.rotate(angle); // can this be done once?
  							ctx.fillText(text, 0, 0);
  							ctx.restore();
  						}
  						else
  							ctx.fillText(text, x, y + j * lineHeight);
  					}
  				}
  			}
  
  			// ticks
  			if (ticks.show) {
  				drawOrthoLines(
  					canOffs,
  					ticks.filter(self, splits, i, _space, incr),
  					ori,
  					side,
  					basePos,
  					tickSize,
  					roundDec(ticks.width * pxRatio, 3),
  					ticks.stroke(self, i),
  					ticks.dash,
  					ticks.cap,
  				);
  			}
  
  			// grid
  			let grid = axis.grid;
  
  			if (grid.show) {
  				drawOrthoLines(
  					canOffs,
  					grid.filter(self, splits, i, _space, incr),
  					ori,
  					ori == 0 ? 2 : 1,
  					ori == 0 ? plotTop : plotLft,
  					ori == 0 ? plotHgt : plotWid,
  					roundDec(grid.width * pxRatio, 3),
  					grid.stroke(self, i),
  					grid.dash,
  					grid.cap,
  				);
  			}
  		}
  
  		fire("drawAxes");
  	}
  
  	function resetYSeries(minMax) {
  	//	log("resetYSeries()", arguments);
  
  		series.forEach((s, i) => {
  			if (i > 0) {
  				s._paths = null;
  
  				if (minMax) {
  					if (mode == 1) {
  						s.min = null;
  						s.max = null;
  					}
  					else {
  						s.facets.forEach(f => {
  							f.min = null;
  							f.max = null;
  						});
  					}
  				}
  			}
  		});
  	}
  
  	let queuedCommit = false;
  
  	function commit() {
  		if (!queuedCommit) {
  			microTask(_commit);
  			queuedCommit = true;
  		}
  	}
  
  	function _commit() {
  	//	log("_commit()", arguments);
  
  		if (shouldSetScales) {
  			setScales();
  			shouldSetScales = false;
  		}
  
  		if (shouldConvergeSize) {
  			convergeSize();
  			shouldConvergeSize = false;
  		}
  
  		if (shouldSetSize) {
  			setStylePx(under, LEFT,   plotLftCss);
  			setStylePx(under, TOP,    plotTopCss);
  			setStylePx(under, WIDTH,  plotWidCss);
  			setStylePx(under, HEIGHT, plotHgtCss);
  
  			setStylePx(over, LEFT,    plotLftCss);
  			setStylePx(over, TOP,     plotTopCss);
  			setStylePx(over, WIDTH,   plotWidCss);
  			setStylePx(over, HEIGHT,  plotHgtCss);
  
  			setStylePx(wrap, WIDTH,   fullWidCss);
  			setStylePx(wrap, HEIGHT,  fullHgtCss);
  
  			// NOTE: mutating this during print preview in Chrome forces transparent
  			// canvas pixels to white, even when followed up with clearRect() below
  			can.width  = round(fullWidCss * pxRatio);
  			can.height = round(fullHgtCss * pxRatio);
  
  			axes.forEach(({ _el, _show, _size, _pos, side }) => {
  				if (_el != null) {
  					if (_show) {
  						let posOffset = (side === 3 || side === 0 ? _size : 0);
  						let isVt = side % 2 == 1;
  	
  						setStylePx(_el, isVt ? "left"   : "top",    _pos - posOffset);
  						setStylePx(_el, isVt ? "width"  : "height", _size);
  						setStylePx(_el, isVt ? "top"    : "left",   isVt ? plotTopCss : plotLftCss);
  						setStylePx(_el, isVt ? "height" : "width",  isVt ? plotHgtCss : plotWidCss);
  	
  						remClass(_el, OFF);
  					}
  					else
  						addClass(_el, OFF);
  				}
  			});
  
  			// invalidate ctx style cache
  			ctxStroke = ctxFill = ctxWidth = ctxJoin = ctxCap = ctxFont = ctxAlign = ctxBaseline = ctxDash = null;
  			ctxAlpha = 1;
  
  			syncRect(false);
  
  			fire("setSize");
  
  			shouldSetSize = false;
  		}
  
  		if (fullWidCss > 0 && fullHgtCss > 0) {
  			ctx.clearRect(0, 0, can.width, can.height);
  			fire("drawClear");
  			drawOrder.forEach(fn => fn());
  			fire("draw");
  		}
  
  	//	if (shouldSetSelect) {
  		// TODO: update .u-select metrics (if visible)
  		//	setStylePx(selectDiv, TOP, select.top = 0);
  		//	setStylePx(selectDiv, LEFT, select.left = 0);
  		//	setStylePx(selectDiv, WIDTH, select.width = 0);
  		//	setStylePx(selectDiv, HEIGHT, select.height = 0);
  		//	shouldSetSelect = false;
  	//	}
  
  		if (FEAT_CURSOR && cursor.show && shouldSetCursor) {
  			updateCursor(null, true, false);
  			shouldSetCursor = false;
  		}
  
  	//	if (FEAT_LEGEND && legend.show && legend.live && shouldSetLegend) {}
  
  		if (!ready) {
  			ready = true;
  			self.status = 1;
  
  			fire("ready");
  		}
  
  		viaAutoScaleX = false;
  
  		queuedCommit = false;
  	}
  
  	self.redraw = (rebuildPaths, recalcAxes) => {
  		shouldConvergeSize = recalcAxes || false;
  
  		if (rebuildPaths !== false)
  			_setScale(xScaleKey, scaleX.min, scaleX.max);
  		else
  			commit();
  	};
  
  	// redraw() => setScale('x', scales.x.min, scales.x.max);
  
  	// explicit, never re-ranged (is this actually true? for x and y)
  	function setScale(key, opts) {
  		let sc = scales[key];
  
  		if (sc.from == null) {
  			if (dataLen == 0) {
  				let minMax = sc.range(self, opts.min, opts.max, key);
  				opts.min = minMax[0];
  				opts.max = minMax[1];
  			}
  
  			if (opts.min > opts.max) {
  				let _min = opts.min;
  				opts.min = opts.max;
  				opts.max = _min;
  			}
  
  			if (dataLen > 1 && opts.min != null && opts.max != null && opts.max - opts.min < 1e-16)
  				return;
  
  			if (key == xScaleKey) {
  				if (sc.distr == 2 && dataLen > 0) {
  					opts.min = closestIdx(opts.min, data[0]);
  					opts.max = closestIdx(opts.max, data[0]);
  
  					if (opts.min == opts.max)
  						opts.max++;
  				}
  			}
  
  		//	log("setScale()", arguments);
  
  			pendScales[key] = opts;
  
  			shouldSetScales = true;
  			commit();
  		}
  	}
  
  	self.setScale = setScale;
  
  //	INTERACTION
  
  	let xCursor;
  	let yCursor;
  	let vCursor;
  	let hCursor;
  
  	// starting position before cursor.move
  	let rawMouseLeft0;
  	let rawMouseTop0;
  
  	// starting position
  	let mouseLeft0;
  	let mouseTop0;
  
  	// current position before cursor.move
  	let rawMouseLeft1;
  	let rawMouseTop1;
  
  	// current position
  	let mouseLeft1;
  	let mouseTop1;
  
  	let dragging = false;
  
  	const drag = FEAT_CURSOR && cursor.drag;
  
  	let dragX = FEAT_CURSOR && drag.x;
  	let dragY = FEAT_CURSOR && drag.y;
  
  	if (FEAT_CURSOR && cursor.show) {
  		if (cursor.x)
  			xCursor = placeDiv(CURSOR_X, over);
  		if (cursor.y)
  			yCursor = placeDiv(CURSOR_Y, over);
  
  		if (scaleX.ori == 0) {
  			vCursor = xCursor;
  			hCursor = yCursor;
  		}
  		else {
  			vCursor = yCursor;
  			hCursor = xCursor;
  		}
  
  		mouseLeft1 = cursor.left;
  		mouseTop1 = cursor.top;
  	}
  
  	const select = self.select = assign({
  		show:   true,
  		over:   true,
  		left:   0,
  		width:  0,
  		top:    0,
  		height: 0,
  	}, opts.select);
  
  	const selectDiv = select.show ? placeDiv(SELECT, select.over ? over : under) : null;
  
  	function setSelect(opts, _fire) {
  		if (select.show) {
  			for (let prop in opts)
  				setStylePx(selectDiv, prop, select[prop] = opts[prop]);
  
  			_fire !== false && fire("setSelect");
  		}
  	}
  
  	self.setSelect = setSelect;
  
  	function toggleDOM(i, onOff) {
  		let s = series[i];
  		let label = showLegend ? legendRows[i] : null;
  
  		if (s.show)
  			label && remClass(label, OFF);
  		else {
  			label && addClass(label, OFF);
  			FEAT_CURSOR && cursorPts.length > 1 && elTrans(cursorPts[i], -10, -10, plotWidCss, plotHgtCss);
  		}
  	}
  
  	function _setScale(key, min, max) {
  		setScale(key, {min, max});
  	}
  
  	function setSeries(i, opts, _fire, _pub) {
  	//	log("setSeries()", arguments);
  
  		let s = series[i];
  
  		if (opts.focus != null)
  			setFocus(i);
  
  		if (opts.show != null) {
  			s.show = opts.show;
  			FEAT_LEGEND && toggleDOM(i, opts.show);
  
  			_setScale(mode == 2 ? s.facets[1].scale : s.scale, null, null);
  			commit();
  		}
  
  		_fire !== false && fire("setSeries", i, opts);
  
  		FEAT_CURSOR && _pub && pubSync("setSeries", self, i, opts);
  	}
  
  	self.setSeries = setSeries;
  
  	function setBand(bi, opts) {
  		assign(bands[bi], opts);
  	}
  
  	function addBand(opts, bi) {
  		opts.fill = fnOrSelf(opts.fill || null);
  		bi = bi == null ? bands.length : bi;
  		bands.splice(bi, 0, opts);
  	}
  
  	function delBand(bi) {
  		if (bi == null)
  			bands.length = 0;
  		else
  			bands.splice(bi, 1);
  	}
  
  	self.addBand = addBand;
  	self.setBand = setBand;
  	self.delBand = delBand;
  
  	function setAlpha(i, value) {
  		series[i].alpha = value;
  
  		if (FEAT_CURSOR && cursor.show && cursorPts[i])
  			cursorPts[i].style.opacity = value;
  
  		if (FEAT_LEGEND && showLegend && legendRows[i])
  			legendRows[i].style.opacity = value;
  	}
  
  	// y-distance
  	let closestDist;
  	let closestSeries;
  	let focusedSeries;
  	const FOCUS_TRUE  = {focus: true};
  	const FOCUS_FALSE = {focus: false};
  
  	function setFocus(i) {
  		if (i != focusedSeries) {
  		//	log("setFocus()", arguments);
  
  			let allFocused = i == null;
  
  			let _setAlpha = focus.alpha != 1;
  
  			series.forEach((s, i2) => {
  				let isFocused = allFocused || i2 == 0 || i2 == i;
  				s._focus = allFocused ? null : isFocused;
  				_setAlpha && setAlpha(i2, isFocused ? 1 : focus.alpha);
  			});
  
  			focusedSeries = i;
  			_setAlpha && commit();
  		}
  	}
  
  	if (showLegend && cursorFocus) {
  		on(mouseleave, legendEl, e => {
  			if (cursor._lock)
  				return;
  			setSeries(null, FOCUS_FALSE, true, syncOpts.setSeries);
  			updateCursor(null, true, false);
  		});
  	}
  
  	function posToVal(pos, scale, can) {
  		let sc = scales[scale];
  
  		if (can)
  			pos = pos / pxRatio - (sc.ori == 1 ? plotTopCss : plotLftCss);
  
  		let dim = plotWidCss;
  
  		if (sc.ori == 1) {
  			dim = plotHgtCss;
  			pos = dim - pos;
  		}
  
  		if (sc.dir == -1)
  			pos = dim - pos;
  
  		let _min = sc._min,
  			_max = sc._max,
  			pct = pos / dim;
  
  		let sv = _min + (_max - _min) * pct;
  
  		let distr = sc.distr;
  
  		return (
  			distr == 3 ? pow(10, sv) :
  			distr == 4 ? sinh(sv, sc.asinh) :
  			sv
  		);
  	}
  
  	function closestIdxFromXpos(pos, can) {
  		let v = posToVal(pos, xScaleKey, can);
  		return closestIdx(v, data[0], i0, i1);
  	}
  
  	self.valToIdx = val => closestIdx(val, data[0]);
  	self.posToIdx = closestIdxFromXpos;
  	self.posToVal = posToVal;
  	self.valToPos = (val, scale, can) => (
  		scales[scale].ori == 0 ?
  		getHPos(val, scales[scale],
  			can ? plotWid : plotWidCss,
  			can ? plotLft : 0,
  		) :
  		getVPos(val, scales[scale],
  			can ? plotHgt : plotHgtCss,
  			can ? plotTop : 0,
  		)
  	);
  
  	// defers calling expensive functions
  	function batch(fn) {
  		fn(self);
  		commit();
  	}
  
  	self.batch = batch;
  
  	FEAT_CURSOR && (self.setCursor = (opts, _fire, _pub) => {
  		mouseLeft1 = opts.left;
  		mouseTop1 = opts.top;
  	//	assign(cursor, opts);
  		updateCursor(null, _fire, _pub);
  	});
  
  	function setSelH(off, dim) {
  		setStylePx(selectDiv, LEFT,  select.left = off);
  		setStylePx(selectDiv, WIDTH, select.width = dim);
  	}
  
  	function setSelV(off, dim) {
  		setStylePx(selectDiv, TOP,    select.top = off);
  		setStylePx(selectDiv, HEIGHT, select.height = dim);
  	}
  
  	let setSelX = scaleX.ori == 0 ? setSelH : setSelV;
  	let setSelY = scaleX.ori == 1 ? setSelH : setSelV;
  
  	function syncLegend() {
  		if (showLegend && legend.live) {
  			for (let i = mode == 2 ? 1 : 0; i < series.length; i++) {
  				if (i == 0 && multiValLegend)
  					continue;
  
  				let vals = legend.values[i];
  
  				let j = 0;
  
  				for (let k in vals)
  					legendCells[i][j++].firstChild.nodeValue = vals[k];
  			}
  		}
  	}
  
  	function setLegend(opts, _fire) {
  		if (opts != null) {
  			let idx = opts.idx;
  
  			legend.idx = idx;
  			series.forEach((s, sidx) => {
  				(sidx > 0 || !multiValLegend) && setLegendValues(sidx, idx);
  			});
  		}
  
  		if (showLegend && legend.live)
  			syncLegend();
  
  		shouldSetLegend = false;
  
  		_fire !== false && fire("setLegend");
  	}
  
  	self.setLegend = setLegend;
  
  	function setLegendValues(sidx, idx) {
  		let val;
  
  		if (idx == null)
  			val = NULL_LEGEND_VALUES;
  		else {
  			let s = series[sidx];
  			let src = sidx == 0 && xScaleDistr == 2 ? data0 : data[sidx];
  			val = multiValLegend ? s.values(self, sidx, idx) : {_: s.value(self, src[idx], sidx, idx)};
  		}
  
  		legend.values[sidx] = val;
  	}
  
  	function updateCursor(src, _fire, _pub) {
  	//	ts == null && log("updateCursor()", arguments);
  
  		rawMouseLeft1 = mouseLeft1;
  		rawMouseTop1 = mouseTop1;
  
  		[mouseLeft1, mouseTop1] = cursor.move(self, mouseLeft1, mouseTop1);
  
  		if (cursor.show) {
  			vCursor && elTrans(vCursor, round(mouseLeft1), 0, plotWidCss, plotHgtCss);
  			hCursor && elTrans(hCursor, 0, round(mouseTop1), plotWidCss, plotHgtCss);
  		}
  
  		let idx;
  
  		// when zooming to an x scale range between datapoints the binary search
  		// for nearest min/max indices results in this condition. cheap hack :D
  		let noDataInRange = i0 > i1; // works for mode 1 only
  
  		closestDist = inf;
  
  		// TODO: extract
  		let xDim = scaleX.ori == 0 ? plotWidCss : plotHgtCss;
  		let yDim = scaleX.ori == 1 ? plotWidCss : plotHgtCss;
  
  		// if cursor hidden, hide points & clear legend vals
  		if (mouseLeft1 < 0 || dataLen == 0 || noDataInRange) {
  			idx = null;
  
  			for (let i = 0; i < series.length; i++) {
  				if (i > 0) {
  					FEAT_CURSOR && cursorPts.length > 1 && elTrans(cursorPts[i], -10, -10, plotWidCss, plotHgtCss);
  				}
  			}
  
  			if (cursorFocus)
  				setSeries(null, FOCUS_TRUE, true, src == null && syncOpts.setSeries);
  
  			if (FEAT_LEGEND && legend.live) {
  				activeIdxs.fill(null);
  				shouldSetLegend = true;
  
  				for (let i = 0; i < series.length; i++)
  					legend.values[i] = NULL_LEGEND_VALUES;
  			}
  		}
  		else {
  		//	let pctY = 1 - (y / rect.height);
  
  			let mouseXPos, valAtPosX, xPos;
  
  			if (mode == 1) {
  				mouseXPos = scaleX.ori == 0 ? mouseLeft1 : mouseTop1;
  				valAtPosX = posToVal(mouseXPos, xScaleKey);
  				idx = closestIdx(valAtPosX, data[0], i0, i1);
  				xPos = incrRoundUp(valToPosX(data[0][idx], scaleX, xDim, 0), 0.5);
  			}
  
  			for (let i = mode == 2 ? 1 : 0; i < series.length; i++) {
  				let s = series[i];
  
  				let idx1  = activeIdxs[i];
  				let yVal1 = mode == 1 ? data[i][idx1] : data[i][1][idx1];
  
  				let idx2  = cursor.dataIdx(self, i, idx, valAtPosX);
  				let yVal2 = mode == 1 ? data[i][idx2] : data[i][1][idx2];
  
  				shouldSetLegend = shouldSetLegend || yVal2 != yVal1 || idx2 != idx1;
  
  				activeIdxs[i] = idx2;
  
  				let xPos2 = idx2 == idx ? xPos : incrRoundUp(valToPosX(mode == 1 ? data[0][idx2] : data[i][0][idx2], scaleX, xDim, 0), 0.5);
  
  				if (i > 0 && s.show) {
  					let yPos = yVal2 == null ? -10 : incrRoundUp(valToPosY(yVal2, mode == 1 ? scales[s.scale] : scales[s.facets[1].scale], yDim, 0), 0.5);
  
  					if (yPos > 0 && mode == 1) {
  						let dist = abs(yPos - mouseTop1);
  
  						if (dist <= closestDist) {
  							closestDist = dist;
  							closestSeries = i;
  						}
  					}
  
  					let hPos, vPos;
  
  					if (scaleX.ori == 0) {
  						hPos = xPos2;
  						vPos = yPos;
  					}
  					else {
  						hPos = yPos;
  						vPos = xPos2;
  					}
  
  					if (FEAT_CURSOR && shouldSetLegend && cursorPts.length > 1) {
  						elColor(cursorPts[i], cursor.points.fill(self, i), cursor.points.stroke(self, i));
  
  						let ptWid, ptHgt, ptLft, ptTop,
  							centered = true,
  							getBBox = cursor.points.bbox;
  
  						if (getBBox != null) {
  							centered = false;
  
  							let bbox = getBBox(self, i);
  
  							ptLft = bbox.left;
  							ptTop = bbox.top;
  							ptWid = bbox.width;
  							ptHgt = bbox.height;
  						}
  						else {
  							ptLft = hPos;
  							ptTop = vPos;
  							ptWid = ptHgt = cursor.points.size(self, i);
  						}
  
  						elSize(cursorPts[i], ptWid, ptHgt, centered);
  						elTrans(cursorPts[i], ptLft, ptTop, plotWidCss, plotHgtCss);
  					}
  				}
  
  				if (FEAT_LEGEND && legend.live) {
  					if (!shouldSetLegend || i == 0 && multiValLegend)
  						continue;
  
  					setLegendValues(i, idx2);
  				}
  			}
  		}
  
  		cursor.idx = idx;
  		cursor.left = mouseLeft1;
  		cursor.top = mouseTop1;
  
  		if (shouldSetLegend) {
  			legend.idx = idx;
  			setLegend();
  		}
  
  		// nit: cursor.drag.setSelect is assumed always true
  		if (select.show && dragging) {
  			if (src != null) {
  				let [xKey, yKey] = syncOpts.scales;
  				let [matchXKeys, matchYKeys] = syncOpts.match;
  				let [xKeySrc, yKeySrc] = src.cursor.sync.scales;
  
  				// match the dragX/dragY implicitness/explicitness of src
  				let sdrag = src.cursor.drag;
  				dragX = sdrag._x;
  				dragY = sdrag._y;
  
  				let { left, top, width, height } = src.select;
  
  				let sori = src.scales[xKey].ori;
  				let sPosToVal = src.posToVal;
  
  				let sOff, sDim, sc, a, b;
  
  				let matchingX = xKey != null && matchXKeys(xKey, xKeySrc);
  				let matchingY = yKey != null && matchYKeys(yKey, yKeySrc);
  
  				if (matchingX) {
  					if (sori == 0) {
  						sOff = left;
  						sDim = width;
  					}
  					else {
  						sOff = top;
  						sDim = height;
  					}
  
  					if (dragX) {
  						sc = scales[xKey];
  
  						a = valToPosX(sPosToVal(sOff, xKeySrc),        sc, xDim, 0);
  						b = valToPosX(sPosToVal(sOff + sDim, xKeySrc), sc, xDim, 0);
  
  						setSelX(min(a,b), abs(b-a));
  					}
  					else
  						setSelX(0, xDim);
  
  					if (!matchingY)
  						setSelY(0, yDim);
  				}
  
  				if (matchingY) {
  					if (sori == 1) {
  						sOff = left;
  						sDim = width;
  					}
  					else {
  						sOff = top;
  						sDim = height;
  					}
  
  					if (dragY) {
  						sc = scales[yKey];
  
  						a = valToPosY(sPosToVal(sOff, yKeySrc),        sc, yDim, 0);
  						b = valToPosY(sPosToVal(sOff + sDim, yKeySrc), sc, yDim, 0);
  
  						setSelY(min(a,b), abs(b-a));
  					}
  					else
  						setSelY(0, yDim);
  
  					if (!matchingX)
  						setSelX(0, xDim);
  				}
  			}
  			else {
  				let rawDX = abs(rawMouseLeft1 - rawMouseLeft0);
  				let rawDY = abs(rawMouseTop1 - rawMouseTop0);
  
  				if (scaleX.ori == 1) {
  					let _rawDX = rawDX;
  					rawDX = rawDY;
  					rawDY = _rawDX;
  				}
  
  				dragX = drag.x && rawDX >= drag.dist;
  				dragY = drag.y && rawDY >= drag.dist;
  
  				let uni = drag.uni;
  
  				if (uni != null) {
  					// only calc drag status if they pass the dist thresh
  					if (dragX && dragY) {
  						dragX = rawDX >= uni;
  						dragY = rawDY >= uni;
  
  						// force unidirectionality when both are under uni limit
  						if (!dragX && !dragY) {
  							if (rawDY > rawDX)
  								dragY = true;
  							else
  								dragX = true;
  						}
  					}
  				}
  				else if (drag.x && drag.y && (dragX || dragY))
  					// if omni with no uni then both dragX / dragY should be true if either is true
  					dragX = dragY = true;
  
  				let p0, p1;
  
  				if (dragX) {
  					if (scaleX.ori == 0) {
  						p0 = mouseLeft0;
  						p1 = mouseLeft1;
  					}
  					else {
  						p0 = mouseTop0;
  						p1 = mouseTop1;
  					}
  
  					setSelX(min(p0, p1), abs(p1 - p0));
  
  					if (!dragY)
  						setSelY(0, yDim);
  				}
  
  				if (dragY) {
  					if (scaleX.ori == 1) {
  						p0 = mouseLeft0;
  						p1 = mouseLeft1;
  					}
  					else {
  						p0 = mouseTop0;
  						p1 = mouseTop1;
  					}
  
  					setSelY(min(p0, p1), abs(p1 - p0));
  
  					if (!dragX)
  						setSelX(0, xDim);
  				}
  
  				// the drag didn't pass the dist requirement
  				if (!dragX && !dragY) {
  					setSelX(0, 0);
  					setSelY(0, 0);
  				}
  			}
  		}
  
  		drag._x = dragX;
  		drag._y = dragY;
  
  		if (src == null) {
  			if (_pub) {
  				if (syncKey != null) {
  					let [xSyncKey, ySyncKey] = syncOpts.scales;
  
  					syncOpts.values[0] = xSyncKey != null ? posToVal(scaleX.ori == 0 ? mouseLeft1 : mouseTop1, xSyncKey) : null;
  					syncOpts.values[1] = ySyncKey != null ? posToVal(scaleX.ori == 1 ? mouseLeft1 : mouseTop1, ySyncKey) : null;
  				}
  
  				pubSync(mousemove, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, idx);
  			}
  
  			if (cursorFocus) {
  				let shouldPub = _pub && syncOpts.setSeries;
  				let p = focus.prox;
  
  				if (focusedSeries == null) {
  					if (closestDist <= p)
  						setSeries(closestSeries, FOCUS_TRUE, true, shouldPub);
  				}
  				else {
  					if (closestDist > p)
  						setSeries(null, FOCUS_TRUE, true, shouldPub);
  					else if (closestSeries != focusedSeries)
  						setSeries(closestSeries, FOCUS_TRUE, true, shouldPub);
  				}
  			}
  		}
  
  		ready && _fire !== false && fire("setCursor");
  	}
  
  	let rect = null;
  
  	function syncRect(defer) {
  		if (defer === true)
  			rect = null;
  		else {
  			rect = over.getBoundingClientRect();
  			fire("syncRect", rect);
  		}
  	}
  
  	function mouseMove(e, src, _l, _t, _w, _h, _i) {
  		if (cursor._lock)
  			return;
  
  		cacheMouse(e, src, _l, _t, _w, _h, _i, false, e != null);
  
  		if (e != null)
  			updateCursor(null, true, true);
  		else
  			updateCursor(src, true, false);
  	}
  
  	function cacheMouse(e, src, _l, _t, _w, _h, _i, initial, snap) {
  		if (rect == null)
  			syncRect(false);
  
  		if (e != null) {
  			_l = e.clientX - rect.left;
  			_t = e.clientY - rect.top;
  		}
  		else {
  			if (_l < 0 || _t < 0) {
  				mouseLeft1 = -10;
  				mouseTop1 = -10;
  				return;
  			}
  
  			let [xKey, yKey] = syncOpts.scales;
  
  			let syncOptsSrc = src.cursor.sync;
  			let [xValSrc, yValSrc] = syncOptsSrc.values;
  			let [xKeySrc, yKeySrc] = syncOptsSrc.scales;
  			let [matchXKeys, matchYKeys] = syncOpts.match;
  
  			let rotSrc = src.scales[xKeySrc].ori == 1;
  
  			let xDim = scaleX.ori == 0 ? plotWidCss : plotHgtCss,
  				yDim = scaleX.ori == 1 ? plotWidCss : plotHgtCss,
  				_xDim = rotSrc ? _h : _w,
  				_yDim = rotSrc ? _w : _h,
  				_xPos = rotSrc ? _t : _l,
  				_yPos = rotSrc ? _l : _t;
  
  			if (xKeySrc != null)
  				_l = matchXKeys(xKey, xKeySrc) ? getPos(xValSrc, scales[xKey], xDim, 0) : -10;
  			else
  				_l = xDim * (_xPos/_xDim);
  
  			if (yKeySrc != null)
  				_t = matchYKeys(yKey, yKeySrc) ? getPos(yValSrc, scales[yKey], yDim, 0) : -10;
  			else
  				_t = yDim * (_yPos/_yDim);
  
  			if (scaleX.ori == 1) {
  				let __l = _l;
  				_l = _t;
  				_t = __l;
  			}
  		}
  
  		if (snap) {
  			if (_l <= 1 || _l >= plotWidCss - 1)
  				_l = incrRound(_l, plotWidCss);
  
  			if (_t <= 1 || _t >= plotHgtCss - 1)
  				_t = incrRound(_t, plotHgtCss);
  		}
  
  		if (initial) {
  			rawMouseLeft0 = _l;
  			rawMouseTop0 = _t;
  
  			[mouseLeft0, mouseTop0] = cursor.move(self, _l, _t);
  		}
  		else {
  			mouseLeft1 = _l;
  			mouseTop1 = _t;
  		}
  	}
  
  	function hideSelect() {
  		setSelect({
  			width: 0,
  			height: 0,
  		}, false);
  	}
  
  	function mouseDown(e, src, _l, _t, _w, _h, _i) {
  		dragging = true;
  		dragX = dragY = drag._x = drag._y = false;
  
  		cacheMouse(e, src, _l, _t, _w, _h, _i, true, false);
  
  		if (e != null) {
  			onMouse(mouseup, doc, mouseUp);
  			pubSync(mousedown, self, mouseLeft0, mouseTop0, plotWidCss, plotHgtCss, null);
  		}
  	}
  
  	function mouseUp(e, src, _l, _t, _w, _h, _i) {
  		dragging = drag._x = drag._y = false;
  
  		cacheMouse(e, src, _l, _t, _w, _h, _i, false, true);
  
  		let { left, top, width, height } = select;
  
  		let hasSelect = width > 0 || height > 0;
  
  		hasSelect && setSelect(select);
  
  		if (drag.setScale && hasSelect) {
  		//	if (syncKey != null) {
  		//		dragX = drag.x;
  		//		dragY = drag.y;
  		//	}
  
  			let xOff = left,
  				xDim = width,
  				yOff = top,
  				yDim = height;
  
  			if (scaleX.ori == 1) {
  				xOff = top,
  				xDim = height,
  				yOff = left,
  				yDim = width;
  			}
  
  			if (dragX) {
  				_setScale(xScaleKey,
  					posToVal(xOff, xScaleKey),
  					posToVal(xOff + xDim, xScaleKey)
  				);
  			}
  
  			if (dragY) {
  				for (let k in scales) {
  					let sc = scales[k];
  
  					if (k != xScaleKey && sc.from == null && sc.min != inf) {
  						_setScale(k,
  							posToVal(yOff + yDim, k),
  							posToVal(yOff, k)
  						);
  					}
  				}
  			}
  
  			hideSelect();
  		}
  		else if (cursor.lock) {
  			cursor._lock = !cursor._lock;
  
  			if (!cursor._lock)
  				updateCursor(null, true, false);
  		}
  
  		if (e != null) {
  			offMouse(mouseup, doc, mouseUp);
  			pubSync(mouseup, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null);
  		}
  	}
  
  	function mouseLeave(e, src, _l, _t, _w, _h, _i) {
  		if (!cursor._lock) {
  			let _dragging = dragging;
  
  			if (dragging) {
  				// handle case when mousemove aren't fired all the way to edges by browser
  				let snapH = true;
  				let snapV = true;
  				let snapProx = 10;
  
  				let dragH, dragV;
  
  				if (scaleX.ori == 0) {
  					dragH = dragX;
  					dragV = dragY;
  				}
  				else {
  					dragH = dragY;
  					dragV = dragX;
  				}
  
  				if (dragH && dragV) {
  					// maybe omni corner snap
  					snapH = mouseLeft1 <= snapProx || mouseLeft1 >= plotWidCss - snapProx;
  					snapV = mouseTop1  <= snapProx || mouseTop1  >= plotHgtCss - snapProx;
  				}
  
  				if (dragH && snapH)
  					mouseLeft1 = mouseLeft1 < mouseLeft0 ? 0 : plotWidCss;
  
  				if (dragV && snapV)
  					mouseTop1 = mouseTop1 < mouseTop0 ? 0 : plotHgtCss;
  
  				updateCursor(null, true, true);
  
  				dragging = false;
  			}
  
  			mouseLeft1 = -10;
  			mouseTop1 = -10;
  
  			// passing a non-null timestamp to force sync/mousemove event
  			updateCursor(null, true, true);
  
  			if (_dragging)
  				dragging = _dragging;
  		}
  	}
  
  	function dblClick(e, src, _l, _t, _w, _h, _i) {
  		autoScaleX();
  
  		hideSelect();
  
  		if (e != null)
  			pubSync(dblclick, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null);
  	}
  
  	function syncPxRatio() {
  		axes.forEach(syncFontSize);
  		_setSize(self.width, self.height, true);
  	}
  
  	on(dppxchange, win, syncPxRatio);
  
  	// internal pub/sub
  	const events = {};
  
  	events.mousedown = mouseDown;
  	events.mousemove = mouseMove;
  	events.mouseup = mouseUp;
  	events.dblclick = dblClick;
  	events["setSeries"] = (e, src, idx, opts) => {
  		setSeries(idx, opts, true, false);
  	};
  
  	if (FEAT_CURSOR && cursor.show) {
  		onMouse(mousedown,  over, mouseDown);
  		onMouse(mousemove,  over, mouseMove);
  		onMouse(mouseenter, over, syncRect);
  		onMouse(mouseleave, over, mouseLeave);
  
  		onMouse(dblclick, over, dblClick);
  
  		cursorPlots.add(self);
  
  		self.syncRect = syncRect;
  	}
  
  	// external on/off
  	const hooks = self.hooks = opts.hooks || {};
  
  	function fire(evName, a1, a2) {
  		if (evName in hooks) {
  			hooks[evName].forEach(fn => {
  				fn.call(null, self, a1, a2);
  			});
  		}
  	}
  
  	(opts.plugins || []).forEach(p => {
  		for (let evName in p.hooks)
  			hooks[evName] = (hooks[evName] || []).concat(p.hooks[evName]);
  	});
  
  	const syncOpts = FEAT_CURSOR && assign({
  		key: null,
  		setSeries: false,
  		filters: {
  			pub: retTrue,
  			sub: retTrue,
  		},
  		scales: [xScaleKey, series[1] ? series[1].scale : null],
  		match: [retEq, retEq],
  		values: [null, null],
  	}, cursor.sync);
  
  	FEAT_CURSOR && (cursor.sync = syncOpts);
  
  	const syncKey = FEAT_CURSOR && syncOpts.key;
  
  	const sync = FEAT_CURSOR && _sync(syncKey);
  
  	function pubSync(type, src, x, y, w, h, i) {
  		if (syncOpts.filters.pub(type, src, x, y, w, h, i))
  			sync.pub(type, src, x, y, w, h, i);
  	}
  
  	FEAT_CURSOR && sync.sub(self);
  
  	function pub(type, src, x, y, w, h, i) {
  		if (syncOpts.filters.sub(type, src, x, y, w, h, i))
  			events[type](null, src, x, y, w, h, i);
  	}
  
  	FEAT_CURSOR && (self.pub = pub);
  
  	function destroy() {
  		FEAT_CURSOR && sync.unsub(self);
  		FEAT_CURSOR && cursorPlots.delete(self);
  		mouseListeners.clear();
  		off(dppxchange, win, syncPxRatio);
  		root.remove();
  		fire("destroy");
  	}
  
  	self.destroy = destroy;
  
  	function _init() {
  		fire("init", opts, data);
  
  		setData(data || opts.data, false);
  
  		if (pendScales[xScaleKey])
  			setScale(xScaleKey, pendScales[xScaleKey]);
  		else
  			autoScaleX();
  
  		_setSize(opts.width, opts.height);
  
  		updateCursor(null, true, false);
  
  		setSelect(select, false);
  	}
  
  	series.forEach(initSeries);
  
  	axes.forEach(initAxis);
  
  	if (then) {
  		if (then instanceof HTMLElement) {
  			then.appendChild(root);
  			_init();
  		}
  		else
  			then(self, _init);
  	}
  	else
  		_init();
  
  	return self;
  }
  
  uPlot.assign = assign;
  uPlot.fmtNum = fmtNum;
  uPlot.rangeNum = rangeNum;
  uPlot.rangeLog = rangeLog;
  uPlot.rangeAsinh = rangeAsinh;
  uPlot.orient   = orient;
  
  if (FEAT_JOIN) {
  	uPlot.join = join;
  }
  
  if (FEAT_TIME) {
  	uPlot.fmtDate = fmtDate;
  	uPlot.tzDate  = tzDate;
  }
  
  if (FEAT_CURSOR) {
  	uPlot.sync = _sync;
  }
  
  if (FEAT_PATHS) {
  	uPlot.addGap = addGap;
  	uPlot.clipGaps = clipGaps;
  
  	let paths = uPlot.paths = {
  		points,
  	};
  
  	FEAT_PATHS_LINEAR  && (paths.linear  = linear);
  	FEAT_PATHS_STEPPED && (paths.stepped = stepped);
  	FEAT_PATHS_BARS    && (paths.bars    = bars);
  	FEAT_PATHS_SPLINE  && (paths.spline  = spline);
  	FEAT_PATHS_SPLINE2 && (paths.spline2 = spline2);
  }
  