summaryrefslogtreecommitdiff
path: root/build/resources/main/static/plugins/uplot
diff options
context:
space:
mode:
Diffstat (limited to 'build/resources/main/static/plugins/uplot')
-rw-r--r--build/resources/main/static/plugins/uplot/uPlot.cjs.js5212
-rw-r--r--build/resources/main/static/plugins/uplot/uPlot.esm.js5210
-rw-r--r--build/resources/main/static/plugins/uplot/uPlot.iife.js5215
-rw-r--r--build/resources/main/static/plugins/uplot/uPlot.iife.min.js2
-rw-r--r--build/resources/main/static/plugins/uplot/uPlot.min.css1
5 files changed, 15640 insertions, 0 deletions
diff --git a/build/resources/main/static/plugins/uplot/uPlot.cjs.js b/build/resources/main/static/plugins/uplot/uPlot.cjs.js
new file mode 100644
index 0000000..84a4894
--- /dev/null
+++ b/build/resources/main/static/plugins/uplot/uPlot.cjs.js
@@ -0,0 +1,5212 @@
+/**
+* Copyright (c) 2021, Leon Sorokin
+* All rights reserved. (MIT Licensed)
+*
+* uPlot.js (μPlot)
+* A small, fast chart for time series, lines, areas, ohlc & bars
+* https://github.com/leeoniya/uPlot (v1.6.18)
+*/
+
+'use strict';
+
+const FEAT_TIME = true;
+
+// binary search for index of closest value
+function closestIdx(num, arr, lo, hi) {
+ let mid;
+ lo = lo || 0;
+ hi = hi || arr.length - 1;
+ let bitwise = hi <= 2147483647;
+
+ while (hi - lo > 1) {
+ mid = bitwise ? (lo + hi) >> 1 : floor((lo + hi) / 2);
+
+ if (arr[mid] < num)
+ lo = mid;
+ else
+ hi = mid;
+ }
+
+ if (num - arr[lo] <= arr[hi] - num)
+ return lo;
+
+ return hi;
+}
+
+function nonNullIdx(data, _i0, _i1, dir) {
+ for (let i = dir == 1 ? _i0 : _i1; i >= _i0 && i <= _i1; i += dir) {
+ if (data[i] != null)
+ return i;
+ }
+
+ return -1;
+}
+
+function getMinMax(data, _i0, _i1, sorted) {
+// console.log("getMinMax()");
+
+ let _min = inf;
+ let _max = -inf;
+
+ if (sorted == 1) {
+ _min = data[_i0];
+ _max = data[_i1];
+ }
+ else if (sorted == -1) {
+ _min = data[_i1];
+ _max = data[_i0];
+ }
+ else {
+ for (let i = _i0; i <= _i1; i++) {
+ if (data[i] != null) {
+ _min = min(_min, data[i]);
+ _max = max(_max, data[i]);
+ }
+ }
+ }
+
+ return [_min, _max];
+}
+
+function getMinMaxLog(data, _i0, _i1) {
+// console.log("getMinMax()");
+
+ let _min = inf;
+ let _max = -inf;
+
+ for (let i = _i0; i <= _i1; i++) {
+ if (data[i] > 0) {
+ _min = min(_min, data[i]);
+ _max = max(_max, data[i]);
+ }
+ }
+
+ return [
+ _min == inf ? 1 : _min,
+ _max == -inf ? 10 : _max,
+ ];
+}
+
+const _fixedTuple = [0, 0];
+
+function fixIncr(minIncr, maxIncr, minExp, maxExp) {
+ _fixedTuple[0] = minExp < 0 ? roundDec(minIncr, -minExp) : minIncr;
+ _fixedTuple[1] = maxExp < 0 ? roundDec(maxIncr, -maxExp) : maxIncr;
+ return _fixedTuple;
+}
+
+function rangeLog(min, max, base, fullMags) {
+ let minSign = sign(min);
+
+ let logFn = base == 10 ? log10 : log2;
+
+ if (min == max) {
+ if (minSign == -1) {
+ min *= base;
+ max /= base;
+ }
+ else {
+ min /= base;
+ max *= base;
+ }
+ }
+
+ let minExp, maxExp, minMaxIncrs;
+
+ if (fullMags) {
+ minExp = floor(logFn(min));
+ maxExp = ceil(logFn(max));
+
+ minMaxIncrs = fixIncr(pow(base, minExp), pow(base, maxExp), minExp, maxExp);
+
+ min = minMaxIncrs[0];
+ max = minMaxIncrs[1];
+ }
+ else {
+ minExp = floor(logFn(abs(min)));
+ maxExp = floor(logFn(abs(max)));
+
+ minMaxIncrs = fixIncr(pow(base, minExp), pow(base, maxExp), minExp, maxExp);
+
+ min = incrRoundDn(min, minMaxIncrs[0]);
+ max = incrRoundUp(max, minMaxIncrs[1]);
+ }
+
+ return [min, max];
+}
+
+function rangeAsinh(min, max, base, fullMags) {
+ let minMax = rangeLog(min, max, base, fullMags);
+
+ if (min == 0)
+ minMax[0] = 0;
+
+ if (max == 0)
+ minMax[1] = 0;
+
+ return minMax;
+}
+
+const rangePad = 0.1;
+
+const autoRangePart = {
+ mode: 3,
+ pad: rangePad,
+};
+
+const _eqRangePart = {
+ pad: 0,
+ soft: null,
+ mode: 0,
+};
+
+const _eqRange = {
+ min: _eqRangePart,
+ max: _eqRangePart,
+};
+
+// 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 rangeNum(_min, _max, mult, extra) {
+ if (isObj(mult))
+ return _rangeNum(_min, _max, mult);
+
+ _eqRangePart.pad = mult;
+ _eqRangePart.soft = extra ? 0 : null;
+ _eqRangePart.mode = extra ? 3 : 0;
+
+ return _rangeNum(_min, _max, _eqRange);
+}
+
+// nullish coalesce
+function ifNull(lh, rh) {
+ return lh == null ? rh : lh;
+}
+
+// checks if given index range in an array contains a non-null value
+// aka a range-bounded Array.some()
+function hasData(data, idx0, idx1) {
+ idx0 = ifNull(idx0, 0);
+ idx1 = ifNull(idx1, data.length - 1);
+
+ while (idx0 <= idx1) {
+ if (data[idx0] != null)
+ return true;
+ idx0++;
+ }
+
+ return false;
+}
+
+function _rangeNum(_min, _max, cfg) {
+ let cmin = cfg.min;
+ let cmax = cfg.max;
+
+ let padMin = ifNull(cmin.pad, 0);
+ let padMax = ifNull(cmax.pad, 0);
+
+ let hardMin = ifNull(cmin.hard, -inf);
+ let hardMax = ifNull(cmax.hard, inf);
+
+ let softMin = ifNull(cmin.soft, inf);
+ let softMax = ifNull(cmax.soft, -inf);
+
+ let softMinMode = ifNull(cmin.mode, 0);
+ let softMaxMode = ifNull(cmax.mode, 0);
+
+ let delta = _max - _min;
+
+ // this handles situations like 89.7, 89.69999999999999
+ // by assuming 0.001x deltas are precision errors
+// if (delta > 0 && delta < abs(_max) / 1e3)
+// delta = 0;
+
+ // treat data as flat if delta is less than 1 billionth
+ if (delta < 1e-9) {
+ delta = 0;
+
+ // if soft mode is 2 and all vals are flat at 0, avoid the 0.1 * 1e3 fallback
+ // this prevents 0,0,0 from ranging to -100,100 when softMin/softMax are -1,1
+ if (_min == 0 || _max == 0) {
+ delta = 1e-9;
+
+ if (softMinMode == 2 && softMin != inf)
+ padMin = 0;
+
+ if (softMaxMode == 2 && softMax != -inf)
+ padMax = 0;
+ }
+ }
+
+ let nonZeroDelta = delta || abs(_max) || 1e3;
+ let mag = log10(nonZeroDelta);
+ let base = pow(10, floor(mag));
+
+ let _padMin = nonZeroDelta * (delta == 0 ? (_min == 0 ? .1 : 1) : padMin);
+ let _newMin = roundDec(incrRoundDn(_min - _padMin, base/10), 9);
+ let _softMin = _min >= softMin && (softMinMode == 1 || softMinMode == 3 && _newMin <= softMin || softMinMode == 2 && _newMin >= softMin) ? softMin : inf;
+ let minLim = max(hardMin, _newMin < _softMin && _min >= _softMin ? _softMin : min(_softMin, _newMin));
+
+ let _padMax = nonZeroDelta * (delta == 0 ? (_max == 0 ? .1 : 1) : padMax);
+ let _newMax = roundDec(incrRoundUp(_max + _padMax, base/10), 9);
+ let _softMax = _max <= softMax && (softMaxMode == 1 || softMaxMode == 3 && _newMax >= softMax || softMaxMode == 2 && _newMax <= softMax) ? softMax : -inf;
+ let maxLim = min(hardMax, _newMax > _softMax && _max <= _softMax ? _softMax : max(_softMax, _newMax));
+
+ if (minLim == maxLim && minLim == 0)
+ maxLim = 100;
+
+ return [minLim, maxLim];
+}
+
+// alternative: https://stackoverflow.com/a/2254896
+const fmtNum = new Intl.NumberFormat(navigator.language).format;
+
+const M = Math;
+
+const PI = M.PI;
+const abs = M.abs;
+const floor = M.floor;
+const round = M.round;
+const ceil = M.ceil;
+const min = M.min;
+const max = M.max;
+const pow = M.pow;
+const sign = M.sign;
+const log10 = M.log10;
+const log2 = M.log2;
+// TODO: seems like this needs to match asinh impl if the passed v is tweaked?
+const sinh = (v, linthresh = 1) => M.sinh(v) * linthresh;
+const asinh = (v, linthresh = 1) => M.asinh(v / linthresh);
+
+const inf = Infinity;
+
+function numIntDigits(x) {
+ return (log10((x ^ (x >> 31)) - (x >> 31)) | 0) + 1;
+}
+
+function incrRound(num, incr) {
+ return round(num/incr)*incr;
+}
+
+function clamp(num, _min, _max) {
+ return min(max(num, _min), _max);
+}
+
+function fnOrSelf(v) {
+ return typeof v == "function" ? v : () => v;
+}
+
+const retArg0 = _0 => _0;
+
+const retArg1 = (_0, _1) => _1;
+
+const retNull = _ => null;
+
+const retTrue = _ => true;
+
+const retEq = (a, b) => a == b;
+
+function incrRoundUp(num, incr) {
+ return ceil(num/incr)*incr;
+}
+
+function incrRoundDn(num, incr) {
+ return floor(num/incr)*incr;
+}
+
+function roundDec(val, dec) {
+ return round(val * (dec = 10**dec)) / dec;
+}
+
+const fixedDec = new Map();
+
+function guessDec(num) {
+ return ((""+num).split(".")[1] || "").length;
+}
+
+function genIncrs(base, minExp, maxExp, mults) {
+ let incrs = [];
+
+ let multDec = mults.map(guessDec);
+
+ for (let exp = minExp; exp < maxExp; exp++) {
+ let expa = abs(exp);
+ let mag = roundDec(pow(base, exp), expa);
+
+ for (let i = 0; i < mults.length; i++) {
+ let _incr = mults[i] * mag;
+ let dec = (_incr >= 0 && exp >= 0 ? 0 : expa) + (exp >= multDec[i] ? 0 : multDec[i]);
+ let incr = roundDec(_incr, dec);
+ incrs.push(incr);
+ fixedDec.set(incr, dec);
+ }
+ }
+
+ return incrs;
+}
+
+//export const assign = Object.assign;
+
+const EMPTY_OBJ = {};
+const EMPTY_ARR = [];
+
+const nullNullTuple = [null, null];
+
+const isArr = Array.isArray;
+
+function isStr(v) {
+ return typeof v == 'string';
+}
+
+function isObj(v) {
+ let is = false;
+
+ if (v != null) {
+ let c = v.constructor;
+ is = c == null || c == Object;
+ }
+
+ return is;
+}
+
+function fastIsObj(v) {
+ return v != null && typeof v == 'object';
+}
+
+function copy(o, _isObj = isObj) {
+ let out;
+
+ if (isArr(o)) {
+ let val = o.find(v => v != null);
+
+ if (isArr(val) || _isObj(val)) {
+ out = Array(o.length);
+ for (let i = 0; i < o.length; i++)
+ out[i] = copy(o[i], _isObj);
+ }
+ else
+ out = o.slice();
+ }
+ else if (_isObj(o)) {
+ out = {};
+ for (let k in o)
+ out[k] = copy(o[k], _isObj);
+ }
+ else
+ out = o;
+
+ return out;
+}
+
+function assign(targ) {
+ let args = arguments;
+
+ for (let i = 1; i < args.length; i++) {
+ let src = args[i];
+
+ for (let key in src) {
+ if (isObj(targ[key]))
+ assign(targ[key], copy(src[key]));
+ else
+ targ[key] = copy(src[key]);
+ }
+ }
+
+ return targ;
+}
+
+// nullModes
+const NULL_REMOVE = 0; // nulls are converted to undefined (e.g. for spanGaps: true)
+const NULL_RETAIN = 1; // nulls are retained, with alignment artifacts set to undefined (default)
+const NULL_EXPAND = 2; // nulls are expanded to include any adjacent alignment artifacts
+
+// sets undefined values to nulls when adjacent to existing nulls (minesweeper)
+function nullExpand(yVals, nullIdxs, alignedLen) {
+ for (let i = 0, xi, lastNullIdx = -1; i < nullIdxs.length; i++) {
+ let nullIdx = nullIdxs[i];
+
+ if (nullIdx > lastNullIdx) {
+ xi = nullIdx - 1;
+ while (xi >= 0 && yVals[xi] == null)
+ yVals[xi--] = null;
+
+ xi = nullIdx + 1;
+ while (xi < alignedLen && yVals[xi] == null)
+ yVals[lastNullIdx = xi++] = null;
+ }
+ }
+}
+
+// nullModes is a tables-matched array indicating how to treat nulls in each series
+// output is sorted ASC on the joined field (table[0]) and duplicate join values are collapsed
+function join(tables, nullModes) {
+ let xVals = new Set();
+
+ for (let ti = 0; ti < tables.length; ti++) {
+ let t = tables[ti];
+ let xs = t[0];
+ let len = xs.length;
+
+ for (let i = 0; i < len; i++)
+ xVals.add(xs[i]);
+ }
+
+ let data = [Array.from(xVals).sort((a, b) => a - b)];
+
+ let alignedLen = data[0].length;
+
+ let xIdxs = new Map();
+
+ for (let i = 0; i < alignedLen; i++)
+ xIdxs.set(data[0][i], i);
+
+ for (let ti = 0; ti < tables.length; ti++) {
+ let t = tables[ti];
+ let xs = t[0];
+
+ for (let si = 1; si < t.length; si++) {
+ let ys = t[si];
+
+ let yVals = Array(alignedLen).fill(undefined);
+
+ let nullMode = nullModes ? nullModes[ti][si] : NULL_RETAIN;
+
+ let nullIdxs = [];
+
+ for (let i = 0; i < ys.length; i++) {
+ let yVal = ys[i];
+ let alignedIdx = xIdxs.get(xs[i]);
+
+ if (yVal === null) {
+ if (nullMode != NULL_REMOVE) {
+ yVals[alignedIdx] = yVal;
+
+ if (nullMode == NULL_EXPAND)
+ nullIdxs.push(alignedIdx);
+ }
+ }
+ else
+ yVals[alignedIdx] = yVal;
+ }
+
+ nullExpand(yVals, nullIdxs, alignedLen);
+
+ data.push(yVals);
+ }
+ }
+
+ return data;
+}
+
+const microTask = typeof queueMicrotask == "undefined" ? fn => Promise.resolve().then(fn) : queueMicrotask;
+
+const WIDTH = "width";
+const HEIGHT = "height";
+const TOP = "top";
+const BOTTOM = "bottom";
+const LEFT = "left";
+const RIGHT = "right";
+const hexBlack = "#000";
+const transparent = hexBlack + "0";
+
+const mousemove = "mousemove";
+const mousedown = "mousedown";
+const mouseup = "mouseup";
+const mouseenter = "mouseenter";
+const mouseleave = "mouseleave";
+const dblclick = "dblclick";
+const resize = "resize";
+const scroll = "scroll";
+
+const change = "change";
+const dppxchange = "dppxchange";
+
+const pre = "u-";
+
+const UPLOT = "uplot";
+const ORI_HZ = pre + "hz";
+const ORI_VT = pre + "vt";
+const TITLE = pre + "title";
+const WRAP = pre + "wrap";
+const UNDER = pre + "under";
+const OVER = pre + "over";
+const AXIS = pre + "axis";
+const OFF = pre + "off";
+const SELECT = pre + "select";
+const CURSOR_X = pre + "cursor-x";
+const CURSOR_Y = pre + "cursor-y";
+const CURSOR_PT = pre + "cursor-pt";
+const LEGEND = pre + "legend";
+const LEGEND_LIVE = pre + "live";
+const LEGEND_INLINE = pre + "inline";
+const LEGEND_THEAD = pre + "thead";
+const LEGEND_SERIES = pre + "series";
+const LEGEND_MARKER = pre + "marker";
+const LEGEND_LABEL = pre + "label";
+const LEGEND_VALUE = pre + "value";
+
+const doc = document;
+const win = window;
+let pxRatio;
+
+let query;
+
+function setPxRatio() {
+ let _pxRatio = devicePixelRatio;
+
+ // during print preview, Chrome fires off these dppx queries even without changes
+ if (pxRatio != _pxRatio) {
+ pxRatio = _pxRatio;
+
+ query && off(change, query, setPxRatio);
+ query = matchMedia(`(min-resolution: ${pxRatio - 0.001}dppx) and (max-resolution: ${pxRatio + 0.001}dppx)`);
+ on(change, query, setPxRatio);
+
+ win.dispatchEvent(new CustomEvent(dppxchange));
+ }
+}
+
+function addClass(el, c) {
+ if (c != null) {
+ let cl = el.classList;
+ !cl.contains(c) && cl.add(c);
+ }
+}
+
+function remClass(el, c) {
+ let cl = el.classList;
+ cl.contains(c) && cl.remove(c);
+}
+
+function setStylePx(el, name, value) {
+ el.style[name] = value + "px";
+}
+
+function placeTag(tag, cls, targ, refEl) {
+ let el = doc.createElement(tag);
+
+ if (cls != null)
+ addClass(el, cls);
+
+ if (targ != null)
+ targ.insertBefore(el, refEl);
+
+ return el;
+}
+
+function placeDiv(cls, targ) {
+ return placeTag("div", cls, targ);
+}
+
+const xformCache = new WeakMap();
+
+function elTrans(el, xPos, yPos, xMax, yMax) {
+ let xform = "translate(" + xPos + "px," + yPos + "px)";
+ let xformOld = xformCache.get(el);
+
+ if (xform != xformOld) {
+ el.style.transform = xform;
+ xformCache.set(el, xform);
+
+ if (xPos < 0 || yPos < 0 || xPos > xMax || yPos > yMax)
+ addClass(el, OFF);
+ else
+ remClass(el, OFF);
+ }
+}
+
+const colorCache = new WeakMap();
+
+function elColor(el, background, borderColor) {
+ let newColor = background + borderColor;
+ let oldColor = colorCache.get(el);
+
+ if (newColor != oldColor) {
+ colorCache.set(el, newColor);
+ el.style.background = background;
+ el.style.borderColor = borderColor;
+ }
+}
+
+const sizeCache = new WeakMap();
+
+function elSize(el, newWid, newHgt, centered) {
+ let newSize = newWid + "" + newHgt;
+ let oldSize = sizeCache.get(el);
+
+ if (newSize != oldSize) {
+ sizeCache.set(el, newSize);
+ el.style.height = newHgt + "px";
+ el.style.width = newWid + "px";
+ el.style.marginLeft = centered ? -newWid/2 + "px" : 0;
+ el.style.marginTop = centered ? -newHgt/2 + "px" : 0;
+ }
+}
+
+const evOpts = {passive: true};
+const evOpts2 = assign({capture: true}, evOpts);
+
+function on(ev, el, cb, capt) {
+ el.addEventListener(ev, cb, capt ? evOpts2 : evOpts);
+}
+
+function off(ev, el, cb, capt) {
+ el.removeEventListener(ev, cb, capt ? evOpts2 : evOpts);
+}
+
+setPxRatio();
+
+const months = [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+];
+
+const days = [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+];
+
+function slice3(str) {
+ return str.slice(0, 3);
+}
+
+const days3 = days.map(slice3);
+
+const months3 = months.map(slice3);
+
+const engNames = {
+ MMMM: months,
+ MMM: months3,
+ WWWW: days,
+ WWW: days3,
+};
+
+function zeroPad2(int) {
+ return (int < 10 ? '0' : '') + int;
+}
+
+function zeroPad3(int) {
+ return (int < 10 ? '00' : int < 100 ? '0' : '') + int;
+}
+
+/*
+function suffix(int) {
+ let mod10 = int % 10;
+
+ return int + (
+ mod10 == 1 && int != 11 ? "st" :
+ mod10 == 2 && int != 12 ? "nd" :
+ mod10 == 3 && int != 13 ? "rd" : "th"
+ );
+}
+*/
+
+const subs = {
+ // 2019
+ YYYY: d => d.getFullYear(),
+ // 19
+ YY: d => (d.getFullYear()+'').slice(2),
+ // July
+ MMMM: (d, names) => names.MMMM[d.getMonth()],
+ // Jul
+ MMM: (d, names) => names.MMM[d.getMonth()],
+ // 07
+ MM: d => zeroPad2(d.getMonth()+1),
+ // 7
+ M: d => d.getMonth()+1,
+ // 09
+ DD: d => zeroPad2(d.getDate()),
+ // 9
+ D: d => d.getDate(),
+ // Monday
+ WWWW: (d, names) => names.WWWW[d.getDay()],
+ // Mon
+ WWW: (d, names) => names.WWW[d.getDay()],
+ // 03
+ HH: d => zeroPad2(d.getHours()),
+ // 3
+ H: d => d.getHours(),
+ // 9 (12hr, unpadded)
+ h: d => {let h = d.getHours(); return h == 0 ? 12 : h > 12 ? h - 12 : h;},
+ // AM
+ AA: d => d.getHours() >= 12 ? 'PM' : 'AM',
+ // am
+ aa: d => d.getHours() >= 12 ? 'pm' : 'am',
+ // a
+ a: d => d.getHours() >= 12 ? 'p' : 'a',
+ // 09
+ mm: d => zeroPad2(d.getMinutes()),
+ // 9
+ m: d => d.getMinutes(),
+ // 09
+ ss: d => zeroPad2(d.getSeconds()),
+ // 9
+ s: d => d.getSeconds(),
+ // 374
+ fff: d => zeroPad3(d.getMilliseconds()),
+};
+
+function fmtDate(tpl, names) {
+ names = names || engNames;
+ let parts = [];
+
+ let R = /\{([a-z]+)\}|[^{]+/gi, m;
+
+ while (m = R.exec(tpl))
+ parts.push(m[0][0] == '{' ? subs[m[1]] : m[0]);
+
+ return d => {
+ let out = '';
+
+ for (let i = 0; i < parts.length; i++)
+ out += typeof parts[i] == "string" ? parts[i] : parts[i](d, names);
+
+ return out;
+ }
+}
+
+const localTz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+// https://stackoverflow.com/questions/15141762/how-to-initialize-a-javascript-date-to-a-particular-time-zone/53652131#53652131
+function tzDate(date, tz) {
+ let date2;
+
+ // perf optimization
+ if (tz == 'UTC' || tz == 'Etc/UTC')
+ date2 = new Date(+date + date.getTimezoneOffset() * 6e4);
+ else if (tz == localTz)
+ date2 = date;
+ else {
+ date2 = new Date(date.toLocaleString('en-US', {timeZone: tz}));
+ date2.setMilliseconds(date.getMilliseconds());
+ }
+
+ return date2;
+}
+
+//export const series = [];
+
+// default formatters:
+
+const onlyWhole = v => v % 1 == 0;
+
+const allMults = [1,2,2.5,5];
+
+// ...0.01, 0.02, 0.025, 0.05, 0.1, 0.2, 0.25, 0.5
+const decIncrs = genIncrs(10, -16, 0, allMults);
+
+// 1, 2, 2.5, 5, 10, 20, 25, 50...
+const oneIncrs = genIncrs(10, 0, 16, allMults);
+
+// 1, 2, 5, 10, 20, 25, 50...
+const wholeIncrs = oneIncrs.filter(onlyWhole);
+
+const numIncrs = decIncrs.concat(oneIncrs);
+
+const NL = "\n";
+
+const yyyy = "{YYYY}";
+const NLyyyy = NL + yyyy;
+const md = "{M}/{D}";
+const NLmd = NL + md;
+const NLmdyy = NLmd + "/{YY}";
+
+const aa = "{aa}";
+const hmm = "{h}:{mm}";
+const hmmaa = hmm + aa;
+const NLhmmaa = NL + hmmaa;
+const ss = ":{ss}";
+
+const _ = null;
+
+function genTimeStuffs(ms) {
+ let s = ms * 1e3,
+ m = s * 60,
+ h = m * 60,
+ d = h * 24,
+ mo = d * 30,
+ y = d * 365;
+
+ // min of 1e-3 prevents setting a temporal x ticks too small since Date objects cannot advance ticks smaller than 1ms
+ let subSecIncrs = ms == 1 ? genIncrs(10, 0, 3, allMults).filter(onlyWhole) : genIncrs(10, -3, 0, allMults);
+
+ let timeIncrs = subSecIncrs.concat([
+ // minute divisors (# of secs)
+ s,
+ s * 5,
+ s * 10,
+ s * 15,
+ s * 30,
+ // hour divisors (# of mins)
+ m,
+ m * 5,
+ m * 10,
+ m * 15,
+ m * 30,
+ // day divisors (# of hrs)
+ h,
+ h * 2,
+ h * 3,
+ h * 4,
+ h * 6,
+ h * 8,
+ h * 12,
+ // month divisors TODO: need more?
+ d,
+ d * 2,
+ d * 3,
+ d * 4,
+ d * 5,
+ d * 6,
+ d * 7,
+ d * 8,
+ d * 9,
+ d * 10,
+ d * 15,
+ // year divisors (# months, approx)
+ mo,
+ mo * 2,
+ mo * 3,
+ mo * 4,
+ mo * 6,
+ // century divisors
+ y,
+ y * 2,
+ y * 5,
+ y * 10,
+ y * 25,
+ y * 50,
+ y * 100,
+ ]);
+
+ // [0]: minimum num secs in the tick incr
+ // [1]: default tick format
+ // [2-7]: rollover tick formats
+ // [8]: mode: 0: replace [1] -> [2-7], 1: concat [1] + [2-7]
+ const _timeAxisStamps = [
+ // tick incr default year month day hour min sec mode
+ [y, yyyy, _, _, _, _, _, _, 1],
+ [d * 28, "{MMM}", NLyyyy, _, _, _, _, _, 1],
+ [d, md, NLyyyy, _, _, _, _, _, 1],
+ [h, "{h}" + aa, NLmdyy, _, NLmd, _, _, _, 1],
+ [m, hmmaa, NLmdyy, _, NLmd, _, _, _, 1],
+ [s, ss, NLmdyy + " " + hmmaa, _, NLmd + " " + hmmaa, _, NLhmmaa, _, 1],
+ [ms, ss + ".{fff}", NLmdyy + " " + hmmaa, _, NLmd + " " + hmmaa, _, NLhmmaa, _, 1],
+ ];
+
+ // the ensures that axis ticks, values & grid are aligned to logical temporal breakpoints and not an arbitrary timestamp
+ // https://www.timeanddate.com/time/dst/
+ // https://www.timeanddate.com/time/dst/2019.html
+ // https://www.epochconverter.com/timezones
+ function timeAxisSplits(tzDate) {
+ return (self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace) => {
+ let splits = [];
+ let isYr = foundIncr >= y;
+ let isMo = foundIncr >= mo && foundIncr < y;
+
+ // get the timezone-adjusted date
+ let minDate = tzDate(scaleMin);
+ let minDateTs = roundDec(minDate * ms, 3);
+
+ // get ts of 12am (this lands us at or before the original scaleMin)
+ let minMin = mkDate(minDate.getFullYear(), isYr ? 0 : minDate.getMonth(), isMo || isYr ? 1 : minDate.getDate());
+ let minMinTs = roundDec(minMin * ms, 3);
+
+ if (isMo || isYr) {
+ let moIncr = isMo ? foundIncr / mo : 0;
+ let yrIncr = isYr ? foundIncr / y : 0;
+ // let tzOffset = scaleMin - minDateTs; // needed?
+ let split = minDateTs == minMinTs ? minDateTs : roundDec(mkDate(minMin.getFullYear() + yrIncr, minMin.getMonth() + moIncr, 1) * ms, 3);
+ let splitDate = new Date(round(split / ms));
+ let baseYear = splitDate.getFullYear();
+ let baseMonth = splitDate.getMonth();
+
+ for (let i = 0; split <= scaleMax; i++) {
+ let next = mkDate(baseYear + yrIncr * i, baseMonth + moIncr * i, 1);
+ let offs = next - tzDate(roundDec(next * ms, 3));
+
+ split = roundDec((+next + offs) * ms, 3);
+
+ if (split <= scaleMax)
+ splits.push(split);
+ }
+ }
+ else {
+ let incr0 = foundIncr >= d ? d : foundIncr;
+ let tzOffset = floor(scaleMin) - floor(minDateTs);
+ let split = minMinTs + tzOffset + incrRoundUp(minDateTs - minMinTs, incr0);
+ splits.push(split);
+
+ let date0 = tzDate(split);
+
+ let prevHour = date0.getHours() + (date0.getMinutes() / m) + (date0.getSeconds() / h);
+ let incrHours = foundIncr / h;
+
+ let minSpace = self.axes[axisIdx]._space;
+ let pctSpace = foundSpace / minSpace;
+
+ while (1) {
+ split = roundDec(split + foundIncr, ms == 1 ? 0 : 3);
+
+ if (split > scaleMax)
+ break;
+
+ if (incrHours > 1) {
+ let expectedHour = floor(roundDec(prevHour + incrHours, 6)) % 24;
+ let splitDate = tzDate(split);
+ let actualHour = splitDate.getHours();
+
+ let dstShift = actualHour - expectedHour;
+
+ if (dstShift > 1)
+ dstShift = -1;
+
+ split -= dstShift * h;
+
+ prevHour = (prevHour + incrHours) % 24;
+
+ // add a tick only if it's further than 70% of the min allowed label spacing
+ let prevSplit = splits[splits.length - 1];
+ let pctIncr = roundDec((split - prevSplit) / foundIncr, 3);
+
+ if (pctIncr * pctSpace >= .7)
+ splits.push(split);
+ }
+ else
+ splits.push(split);
+ }
+ }
+
+ return splits;
+ }
+ }
+
+ return [
+ timeIncrs,
+ _timeAxisStamps,
+ timeAxisSplits,
+ ];
+}
+
+const [ timeIncrsMs, _timeAxisStampsMs, timeAxisSplitsMs ] = genTimeStuffs(1);
+const [ timeIncrsS, _timeAxisStampsS, timeAxisSplitsS ] = genTimeStuffs(1e-3);
+
+// base 2
+genIncrs(2, -53, 53, [1]);
+
+/*
+console.log({
+ decIncrs,
+ oneIncrs,
+ wholeIncrs,
+ numIncrs,
+ timeIncrs,
+ fixedDec,
+});
+*/
+
+function timeAxisStamps(stampCfg, fmtDate) {
+ return stampCfg.map(s => s.map((v, i) =>
+ i == 0 || i == 8 || v == null ? v : fmtDate(i == 1 || s[8] == 0 ? v : s[1] + v)
+ ));
+}
+
+// TODO: will need to accept spaces[] and pull incr into the loop when grid will be non-uniform, eg for log scales.
+// currently we ignore this for months since they're *nearly* uniform and the added complexity is not worth it
+function timeAxisVals(tzDate, stamps) {
+ return (self, splits, axisIdx, foundSpace, foundIncr) => {
+ let s = stamps.find(s => foundIncr >= s[0]) || stamps[stamps.length - 1];
+
+ // these track boundaries when a full label is needed again
+ let prevYear;
+ let prevMnth;
+ let prevDate;
+ let prevHour;
+ let prevMins;
+ let prevSecs;
+
+ return splits.map(split => {
+ let date = tzDate(split);
+
+ let newYear = date.getFullYear();
+ let newMnth = date.getMonth();
+ let newDate = date.getDate();
+ let newHour = date.getHours();
+ let newMins = date.getMinutes();
+ let newSecs = date.getSeconds();
+
+ let stamp = (
+ newYear != prevYear && s[2] ||
+ newMnth != prevMnth && s[3] ||
+ newDate != prevDate && s[4] ||
+ newHour != prevHour && s[5] ||
+ newMins != prevMins && s[6] ||
+ newSecs != prevSecs && s[7] ||
+ s[1]
+ );
+
+ prevYear = newYear;
+ prevMnth = newMnth;
+ prevDate = newDate;
+ prevHour = newHour;
+ prevMins = newMins;
+ prevSecs = newSecs;
+
+ return stamp(date);
+ });
+ }
+}
+
+// for when axis.values is defined as a static fmtDate template string
+function timeAxisVal(tzDate, dateTpl) {
+ let stamp = fmtDate(dateTpl);
+ return (self, splits, axisIdx, foundSpace, foundIncr) => splits.map(split => stamp(tzDate(split)));
+}
+
+function mkDate(y, m, d) {
+ return new Date(y, m, d);
+}
+
+function timeSeriesStamp(stampCfg, fmtDate) {
+ return fmtDate(stampCfg);
+}
+const _timeSeriesStamp = '{YYYY}-{MM}-{DD} {h}:{mm}{aa}';
+
+function timeSeriesVal(tzDate, stamp) {
+ return (self, val) => stamp(tzDate(val));
+}
+
+function legendStroke(self, seriesIdx) {
+ let s = self.series[seriesIdx];
+ return s.width ? s.stroke(self, seriesIdx) : s.points.width ? s.points.stroke(self, seriesIdx) : null;
+}
+
+function legendFill(self, seriesIdx) {
+ return self.series[seriesIdx].fill(self, seriesIdx);
+}
+
+const legendOpts = {
+ show: true,
+ live: true,
+ isolate: false,
+ markers: {
+ show: true,
+ width: 2,
+ stroke: legendStroke,
+ fill: legendFill,
+ dash: "solid",
+ },
+ idx: null,
+ idxs: null,
+ values: [],
+};
+
+function cursorPointShow(self, si) {
+ let o = self.cursor.points;
+
+ let pt = placeDiv();
+
+ let size = o.size(self, si);
+ setStylePx(pt, WIDTH, size);
+ setStylePx(pt, HEIGHT, size);
+
+ let mar = size / -2;
+ setStylePx(pt, "marginLeft", mar);
+ setStylePx(pt, "marginTop", mar);
+
+ let width = o.width(self, si, size);
+ width && setStylePx(pt, "borderWidth", width);
+
+ return pt;
+}
+
+function cursorPointFill(self, si) {
+ let sp = self.series[si].points;
+ return sp._fill || sp._stroke;
+}
+
+function cursorPointStroke(self, si) {
+ let sp = self.series[si].points;
+ return sp._stroke || sp._fill;
+}
+
+function cursorPointSize(self, si) {
+ let sp = self.series[si].points;
+ return ptDia(sp.width, 1);
+}
+
+function dataIdx(self, seriesIdx, cursorIdx) {
+ return cursorIdx;
+}
+
+const moveTuple = [0,0];
+
+function cursorMove(self, mouseLeft1, mouseTop1) {
+ moveTuple[0] = mouseLeft1;
+ moveTuple[1] = mouseTop1;
+ return moveTuple;
+}
+
+function filtBtn0(self, targ, handle) {
+ return e => {
+ e.button == 0 && handle(e);
+ };
+}
+
+function passThru(self, targ, handle) {
+ return handle;
+}
+
+const cursorOpts = {
+ show: true,
+ x: true,
+ y: true,
+ lock: false,
+ move: cursorMove,
+ points: {
+ show: cursorPointShow,
+ size: cursorPointSize,
+ width: 0,
+ stroke: cursorPointStroke,
+ fill: cursorPointFill,
+ },
+
+ bind: {
+ mousedown: filtBtn0,
+ mouseup: filtBtn0,
+ click: filtBtn0,
+ dblclick: filtBtn0,
+
+ mousemove: passThru,
+ mouseleave: passThru,
+ mouseenter: passThru,
+ },
+
+ drag: {
+ setScale: true,
+ x: true,
+ y: false,
+ dist: 0,
+ uni: null,
+ _x: false,
+ _y: false,
+ },
+
+ focus: {
+ prox: -1,
+ },
+
+ left: -10,
+ top: -10,
+ idx: null,
+ dataIdx,
+ idxs: null,
+};
+
+const grid = {
+ show: true,
+ stroke: "rgba(0,0,0,0.07)",
+ width: 2,
+// dash: [],
+ filter: retArg1,
+};
+
+const ticks = assign({}, grid, {size: 10});
+
+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"';
+const labelFont = "bold " + font;
+const lineMult = 1.5; // font-size multiplier
+
+const xAxisOpts = {
+ show: true,
+ scale: "x",
+ stroke: hexBlack,
+ space: 50,
+ gap: 5,
+ size: 50,
+ labelGap: 0,
+ labelSize: 30,
+ labelFont,
+ side: 2,
+// class: "x-vals",
+// incrs: timeIncrs,
+// values: timeVals,
+// filter: retArg1,
+ grid,
+ ticks,
+ font,
+ rotate: 0,
+};
+
+const numSeriesLabel = "Value";
+const timeSeriesLabel = "Time";
+
+const xSeriesOpts = {
+ show: true,
+ scale: "x",
+ auto: false,
+ sorted: 1,
+// label: "Time",
+// value: v => stamp(new Date(v * 1e3)),
+
+ // internal caches
+ min: inf,
+ max: -inf,
+ idxs: [],
+};
+
+function numAxisVals(self, splits, axisIdx, foundSpace, foundIncr) {
+ return splits.map(v => v == null ? "" : fmtNum(v));
+}
+
+function numAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {
+ let splits = [];
+
+ let numDec = fixedDec.get(foundIncr) || 0;
+
+ scaleMin = forceMin ? scaleMin : roundDec(incrRoundUp(scaleMin, foundIncr), numDec);
+
+ for (let val = scaleMin; val <= scaleMax; val = roundDec(val + foundIncr, numDec))
+ splits.push(Object.is(val, -0) ? 0 : val); // coalesces -0
+
+ return splits;
+}
+
+// this doesnt work for sin, which needs to come off from 0 independently in pos and neg dirs
+function logAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {
+ const splits = [];
+
+ const logBase = self.scales[self.axes[axisIdx].scale].log;
+
+ const logFn = logBase == 10 ? log10 : log2;
+
+ const exp = floor(logFn(scaleMin));
+
+ foundIncr = pow(logBase, exp);
+
+ if (exp < 0)
+ foundIncr = roundDec(foundIncr, -exp);
+
+ let split = scaleMin;
+
+ do {
+ splits.push(split);
+ split = roundDec(split + foundIncr, fixedDec.get(foundIncr));
+
+ if (split >= foundIncr * logBase)
+ foundIncr = split;
+
+ } while (split <= scaleMax);
+
+ return splits;
+}
+
+function asinhAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {
+ let sc = self.scales[self.axes[axisIdx].scale];
+
+ let linthresh = sc.asinh;
+
+ let posSplits = scaleMax > linthresh ? logAxisSplits(self, axisIdx, max(linthresh, scaleMin), scaleMax, foundIncr) : [linthresh];
+ let zero = scaleMax >= 0 && scaleMin <= 0 ? [0] : [];
+ let negSplits = scaleMin < -linthresh ? logAxisSplits(self, axisIdx, max(linthresh, -scaleMax), -scaleMin, foundIncr): [linthresh];
+
+ return negSplits.reverse().map(v => -v).concat(zero, posSplits);
+}
+
+const RE_ALL = /./;
+const RE_12357 = /[12357]/;
+const RE_125 = /[125]/;
+const RE_1 = /1/;
+
+function logAxisValsFilt(self, splits, axisIdx, foundSpace, foundIncr) {
+ let axis = self.axes[axisIdx];
+ let scaleKey = axis.scale;
+ let sc = self.scales[scaleKey];
+
+ if (sc.distr == 3 && sc.log == 2)
+ return splits;
+
+ let valToPos = self.valToPos;
+
+ let minSpace = axis._space;
+
+ let _10 = valToPos(10, scaleKey);
+
+ let re = (
+ valToPos(9, scaleKey) - _10 >= minSpace ? RE_ALL :
+ valToPos(7, scaleKey) - _10 >= minSpace ? RE_12357 :
+ valToPos(5, scaleKey) - _10 >= minSpace ? RE_125 :
+ RE_1
+ );
+
+ return splits.map(v => ((sc.distr == 4 && v == 0) || re.test(v)) ? v : null);
+}
+
+function numSeriesVal(self, val) {
+ return val == null ? "" : fmtNum(val);
+}
+
+const yAxisOpts = {
+ show: true,
+ scale: "y",
+ stroke: hexBlack,
+ space: 30,
+ gap: 5,
+ size: 50,
+ labelGap: 0,
+ labelSize: 30,
+ labelFont,
+ side: 3,
+// class: "y-vals",
+// incrs: numIncrs,
+// values: (vals, space) => vals,
+// filter: retArg1,
+ grid,
+ ticks,
+ font,
+ rotate: 0,
+};
+
+// takes stroke width
+function ptDia(width, mult) {
+ let dia = 3 + (width || 1) * 2;
+ return roundDec(dia * mult, 3);
+}
+
+function seriesPointsShow(self, si) {
+ let { scale, idxs } = self.series[0];
+ let xData = self._data[0];
+ let p0 = self.valToPos(xData[idxs[0]], scale, true);
+ let p1 = self.valToPos(xData[idxs[1]], scale, true);
+ let dim = abs(p1 - p0);
+
+ let s = self.series[si];
+// const dia = ptDia(s.width, pxRatio);
+ let maxPts = dim / (s.points.space * pxRatio);
+ return idxs[1] - idxs[0] <= maxPts;
+}
+
+function seriesFillTo(self, seriesIdx, dataMin, dataMax) {
+ let scale = self.scales[self.series[seriesIdx].scale];
+ let isUpperBandEdge = self.bands && self.bands.some(b => b.series[0] == seriesIdx);
+ return scale.distr == 3 || isUpperBandEdge ? scale.min : 0;
+}
+
+const facet = {
+ scale: null,
+ auto: true,
+
+ // internal caches
+ min: inf,
+ max: -inf,
+};
+
+const xySeriesOpts = {
+ show: true,
+ auto: true,
+ sorted: 0,
+ alpha: 1,
+ facets: [
+ assign({}, facet, {scale: 'x'}),
+ assign({}, facet, {scale: 'y'}),
+ ],
+};
+
+const ySeriesOpts = {
+ scale: "y",
+ auto: true,
+ sorted: 0,
+ show: true,
+ spanGaps: false,
+ gaps: (self, seriesIdx, idx0, idx1, nullGaps) => nullGaps,
+ alpha: 1,
+ points: {
+ show: seriesPointsShow,
+ filter: null,
+ // paths:
+ // stroke: "#000",
+ // fill: "#fff",
+ // width: 1,
+ // size: 10,
+ },
+// label: "Value",
+// value: v => v,
+ values: null,
+
+ // internal caches
+ min: inf,
+ max: -inf,
+ idxs: [],
+
+ path: null,
+ clip: null,
+};
+
+function clampScale(self, val, scaleMin, scaleMax, scaleKey) {
+/*
+ if (val < 0) {
+ let cssHgt = self.bbox.height / pxRatio;
+ let absPos = self.valToPos(abs(val), scaleKey);
+ let fromBtm = cssHgt - absPos;
+ return self.posToVal(cssHgt + fromBtm, scaleKey);
+ }
+*/
+ return scaleMin / 10;
+}
+
+const xScaleOpts = {
+ time: FEAT_TIME,
+ auto: true,
+ distr: 1,
+ log: 10,
+ asinh: 1,
+ min: null,
+ max: null,
+ dir: 1,
+ ori: 0,
+};
+
+const yScaleOpts = assign({}, xScaleOpts, {
+ time: false,
+ ori: 1,
+});
+
+const syncs = {};
+
+function _sync(key, opts) {
+ let s = syncs[key];
+
+ if (!s) {
+ s = {
+ key,
+ plots: [],
+ sub(plot) {
+ s.plots.push(plot);
+ },
+ unsub(plot) {
+ s.plots = s.plots.filter(c => c != plot);
+ },
+ pub(type, self, x, y, w, h, i) {
+ for (let j = 0; j < s.plots.length; j++)
+ s.plots[j] != self && s.plots[j].pub(type, self, x, y, w, h, i);
+ },
+ };
+
+ if (key != null)
+ syncs[key] = s;
+ }
+
+ return s;
+}
+
+const BAND_CLIP_FILL = 1 << 0;
+const BAND_CLIP_STROKE = 1 << 1;
+
+function orient(u, seriesIdx, cb) {
+ const series = u.series[seriesIdx];
+ const scales = u.scales;
+ const bbox = u.bbox;
+ const scaleX = u.mode == 2 ? scales[series.facets[0].scale] : scales[u.series[0].scale];
+
+ let dx = u._data[0],
+ dy = u._data[seriesIdx],
+ sx = scaleX,
+ sy = u.mode == 2 ? scales[series.facets[1].scale] : scales[series.scale],
+ l = bbox.left,
+ t = bbox.top,
+ w = bbox.width,
+ h = bbox.height,
+ H = u.valToPosH,
+ V = u.valToPosV;
+
+ return (sx.ori == 0
+ ? cb(
+ series,
+ dx,
+ dy,
+ sx,
+ sy,
+ H,
+ V,
+ l,
+ t,
+ w,
+ h,
+ moveToH,
+ lineToH,
+ rectH,
+ arcH,
+ bezierCurveToH,
+ )
+ : cb(
+ series,
+ dx,
+ dy,
+ sx,
+ sy,
+ V,
+ H,
+ t,
+ l,
+ h,
+ w,
+ moveToV,
+ lineToV,
+ rectV,
+ arcV,
+ bezierCurveToV,
+ )
+ );
+}
+
+// creates inverted band clip path (towards from stroke path -> yMax)
+function clipBandLine(self, seriesIdx, idx0, idx1, strokePath) {
+ return orient(self, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+ const lineTo = scaleX.ori == 0 ? lineToH : lineToV;
+
+ let frIdx, toIdx;
+
+ if (dir == 1) {
+ frIdx = idx0;
+ toIdx = idx1;
+ }
+ else {
+ frIdx = idx1;
+ toIdx = idx0;
+ }
+
+ // path start
+ let x0 = pxRound(valToPosX(dataX[frIdx], scaleX, xDim, xOff));
+ let y0 = pxRound(valToPosY(dataY[frIdx], scaleY, yDim, yOff));
+ // path end x
+ let x1 = pxRound(valToPosX(dataX[toIdx], scaleX, xDim, xOff));
+ // upper y limit
+ let yLimit = pxRound(valToPosY(scaleY.max, scaleY, yDim, yOff));
+
+ let clip = new Path2D(strokePath);
+
+ lineTo(clip, x1, yLimit);
+ lineTo(clip, x0, yLimit);
+ lineTo(clip, x0, y0);
+
+ return clip;
+ });
+}
+
+function clipGaps(gaps, ori, plotLft, plotTop, plotWid, plotHgt) {
+ let clip = null;
+
+ // create clip path (invert gaps and non-gaps)
+ if (gaps.length > 0) {
+ clip = new Path2D();
+
+ const rect = ori == 0 ? rectH : rectV;
+
+ let prevGapEnd = plotLft;
+
+ for (let i = 0; i < gaps.length; i++) {
+ let g = gaps[i];
+
+ if (g[1] > g[0]) {
+ let w = g[0] - prevGapEnd;
+
+ w > 0 && rect(clip, prevGapEnd, plotTop, w, plotTop + plotHgt);
+
+ prevGapEnd = g[1];
+ }
+ }
+
+ let w = plotLft + plotWid - prevGapEnd;
+
+ w > 0 && rect(clip, prevGapEnd, plotTop, w, plotTop + plotHgt);
+ }
+
+ return clip;
+}
+
+function addGap(gaps, fromX, toX) {
+ let prevGap = gaps[gaps.length - 1];
+
+ if (prevGap && prevGap[0] == fromX) // TODO: gaps must be encoded at stroke widths?
+ prevGap[1] = toX;
+ else
+ gaps.push([fromX, toX]);
+}
+
+function pxRoundGen(pxAlign) {
+ return pxAlign == 0 ? retArg0 : pxAlign == 1 ? round : v => incrRound(v, pxAlign);
+}
+
+function rect(ori) {
+ let moveTo = ori == 0 ?
+ moveToH :
+ moveToV;
+
+ let arcTo = ori == 0 ?
+ (p, x1, y1, x2, y2, r) => { p.arcTo(x1, y1, x2, y2, r); } :
+ (p, y1, x1, y2, x2, r) => { p.arcTo(x1, y1, x2, y2, r); };
+
+ let rect = ori == 0 ?
+ (p, x, y, w, h) => { p.rect(x, y, w, h); } :
+ (p, y, x, h, w) => { p.rect(x, y, w, h); };
+
+ return (p, x, y, w, h, r = 0) => {
+ if (r == 0)
+ rect(p, x, y, w, h);
+ else {
+ r = min(r, w / 2, h / 2);
+
+ // adapted from https://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-using-html-canvas/7838871#7838871
+ moveTo(p, x + r, y);
+ arcTo(p, x + w, y, x + w, y + h, r);
+ arcTo(p, x + w, y + h, x, y + h, r);
+ arcTo(p, x, y + h, x, y, r);
+ arcTo(p, x, y, x + w, y, r);
+ p.closePath();
+ }
+ };
+}
+
+// orientation-inverting canvas functions
+const moveToH = (p, x, y) => { p.moveTo(x, y); };
+const moveToV = (p, y, x) => { p.moveTo(x, y); };
+const lineToH = (p, x, y) => { p.lineTo(x, y); };
+const lineToV = (p, y, x) => { p.lineTo(x, y); };
+const rectH = rect(0);
+const rectV = rect(1);
+const arcH = (p, x, y, r, startAngle, endAngle) => { p.arc(x, y, r, startAngle, endAngle); };
+const arcV = (p, y, x, r, startAngle, endAngle) => { p.arc(x, y, r, startAngle, endAngle); };
+const bezierCurveToH = (p, bp1x, bp1y, bp2x, bp2y, p2x, p2y) => { p.bezierCurveTo(bp1x, bp1y, bp2x, bp2y, p2x, p2y); };
+const bezierCurveToV = (p, bp1y, bp1x, bp2y, bp2x, p2y, p2x) => { p.bezierCurveTo(bp1x, bp1y, bp2x, bp2y, p2x, p2y); };
+
+// TODO: drawWrap(seriesIdx, drawPoints) (save, restore, translate, clip)
+function points(opts) {
+ return (u, seriesIdx, idx0, idx1, filtIdxs) => {
+ // log("drawPoints()", arguments);
+
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let { pxRound, points } = series;
+
+ let moveTo, arc;
+
+ if (scaleX.ori == 0) {
+ moveTo = moveToH;
+ arc = arcH;
+ }
+ else {
+ moveTo = moveToV;
+ arc = arcV;
+ }
+
+ const width = roundDec(points.width * pxRatio, 3);
+
+ let rad = (points.size - points.width) / 2 * pxRatio;
+ let dia = roundDec(rad * 2, 3);
+
+ let fill = new Path2D();
+ let clip = new Path2D();
+
+ let { left: lft, top: top, width: wid, height: hgt } = u.bbox;
+
+ rectH(clip,
+ lft - dia,
+ top - dia,
+ wid + dia * 2,
+ hgt + dia * 2,
+ );
+
+ const drawPoint = pi => {
+ if (dataY[pi] != null) {
+ let x = pxRound(valToPosX(dataX[pi], scaleX, xDim, xOff));
+ let y = pxRound(valToPosY(dataY[pi], scaleY, yDim, yOff));
+
+ moveTo(fill, x + rad, y);
+ arc(fill, x, y, rad, 0, PI * 2);
+ }
+ };
+
+ if (filtIdxs)
+ filtIdxs.forEach(drawPoint);
+ else {
+ for (let pi = idx0; pi <= idx1; pi++)
+ drawPoint(pi);
+ }
+
+ return {
+ stroke: width > 0 ? fill : null,
+ fill,
+ clip,
+ flags: BAND_CLIP_FILL | BAND_CLIP_STROKE,
+ };
+ });
+ };
+}
+
+function _drawAcc(lineTo) {
+ return (stroke, accX, minY, maxY, inY, outY) => {
+ if (minY != maxY) {
+ if (inY != minY && outY != minY)
+ lineTo(stroke, accX, minY);
+ if (inY != maxY && outY != maxY)
+ lineTo(stroke, accX, maxY);
+
+ lineTo(stroke, accX, outY);
+ }
+ };
+}
+
+const drawAccH = _drawAcc(lineToH);
+const drawAccV = _drawAcc(lineToV);
+
+function linear() {
+ return (u, seriesIdx, idx0, idx1) => {
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ let lineTo, drawAcc;
+
+ if (scaleX.ori == 0) {
+ lineTo = lineToH;
+ drawAcc = drawAccH;
+ }
+ else {
+ lineTo = lineToV;
+ drawAcc = drawAccV;
+ }
+
+ const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+
+ const _paths = {stroke: new Path2D(), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};
+ const stroke = _paths.stroke;
+
+ let minY = inf,
+ maxY = -inf,
+ inY, outY, outX, drawnAtX;
+
+ let gaps = [];
+
+ let accX = pxRound(valToPosX(dataX[dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));
+ let accGaps = false;
+ let prevYNull = false;
+
+ // data edges
+ let lftIdx = nonNullIdx(dataY, idx0, idx1, 1 * dir);
+ let rgtIdx = nonNullIdx(dataY, idx0, idx1, -1 * dir);
+ let lftX = pxRound(valToPosX(dataX[lftIdx], scaleX, xDim, xOff));
+ let rgtX = pxRound(valToPosX(dataX[rgtIdx], scaleX, xDim, xOff));
+
+ if (lftX > xOff)
+ addGap(gaps, xOff, lftX);
+
+ for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) {
+ let x = pxRound(valToPosX(dataX[i], scaleX, xDim, xOff));
+
+ if (x == accX) {
+ if (dataY[i] != null) {
+ outY = pxRound(valToPosY(dataY[i], scaleY, yDim, yOff));
+
+ if (minY == inf) {
+ lineTo(stroke, x, outY);
+ inY = outY;
+ }
+
+ minY = min(outY, minY);
+ maxY = max(outY, maxY);
+ }
+ else if (dataY[i] === null)
+ accGaps = prevYNull = true;
+ }
+ else {
+ let _addGap = false;
+
+ if (minY != inf) {
+ drawAcc(stroke, accX, minY, maxY, inY, outY);
+ outX = drawnAtX = accX;
+ }
+ else if (accGaps) {
+ _addGap = true;
+ accGaps = false;
+ }
+
+ if (dataY[i] != null) {
+ outY = pxRound(valToPosY(dataY[i], scaleY, yDim, yOff));
+ lineTo(stroke, x, outY);
+ minY = maxY = inY = outY;
+
+ // prior pixel can have data but still start a gap if ends with null
+ if (prevYNull && x - accX > 1)
+ _addGap = true;
+
+ prevYNull = false;
+ }
+ else {
+ minY = inf;
+ maxY = -inf;
+
+ if (dataY[i] === null) {
+ accGaps = true;
+
+ if (x - accX > 1)
+ _addGap = true;
+ }
+ }
+
+ _addGap && addGap(gaps, outX, x);
+
+ accX = x;
+ }
+ }
+
+ if (minY != inf && minY != maxY && drawnAtX != accX)
+ drawAcc(stroke, accX, minY, maxY, inY, outY);
+
+ if (rgtX < xOff + xDim)
+ addGap(gaps, rgtX, xOff + xDim);
+
+ if (series.fill != null) {
+ let fill = _paths.fill = new Path2D(stroke);
+
+ let fillTo = pxRound(valToPosY(series.fillTo(u, seriesIdx, series.min, series.max), scaleY, yDim, yOff));
+
+ lineTo(fill, rgtX, fillTo);
+ lineTo(fill, lftX, fillTo);
+ }
+
+ _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
+
+ if (!series.spanGaps)
+ _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
+
+ if (u.bands.length > 0) {
+ // ADDL OPT: only create band clips for series that are band lower edges
+ // if (b.series[1] == i && _paths.band == null)
+ _paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);
+ }
+
+ return _paths;
+ });
+ };
+}
+
+function stepped(opts) {
+ const align = ifNull(opts.align, 1);
+ // whether to draw ascenders/descenders at null/gap bondaries
+ const ascDesc = ifNull(opts.ascDesc, false);
+
+ return (u, seriesIdx, idx0, idx1) => {
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ let lineTo = scaleX.ori == 0 ? lineToH : lineToV;
+
+ const _paths = {stroke: new Path2D(), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};
+ const stroke = _paths.stroke;
+
+ const _dir = 1 * scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+
+ idx0 = nonNullIdx(dataY, idx0, idx1, 1);
+ idx1 = nonNullIdx(dataY, idx0, idx1, -1);
+
+ let gaps = [];
+ let inGap = false;
+ let prevYPos = pxRound(valToPosY(dataY[_dir == 1 ? idx0 : idx1], scaleY, yDim, yOff));
+ let firstXPos = pxRound(valToPosX(dataX[_dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));
+ let prevXPos = firstXPos;
+
+ lineTo(stroke, firstXPos, prevYPos);
+
+ for (let i = _dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dir) {
+ let yVal1 = dataY[i];
+
+ let x1 = pxRound(valToPosX(dataX[i], scaleX, xDim, xOff));
+
+ if (yVal1 == null) {
+ if (yVal1 === null) {
+ addGap(gaps, prevXPos, x1);
+ inGap = true;
+ }
+ continue;
+ }
+
+ let y1 = pxRound(valToPosY(yVal1, scaleY, yDim, yOff));
+
+ if (inGap) {
+ addGap(gaps, prevXPos, x1);
+ inGap = false;
+ }
+
+ if (align == 1)
+ lineTo(stroke, x1, prevYPos);
+ else
+ lineTo(stroke, prevXPos, y1);
+
+ lineTo(stroke, x1, y1);
+
+ prevYPos = y1;
+ prevXPos = x1;
+ }
+
+ if (series.fill != null) {
+ let fill = _paths.fill = new Path2D(stroke);
+
+ let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
+ let minY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff));
+
+ lineTo(fill, prevXPos, minY);
+ lineTo(fill, firstXPos, minY);
+ }
+
+ _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
+
+ // expand/contract clips for ascenders/descenders
+ let halfStroke = (series.width * pxRatio) / 2;
+ let startsOffset = (ascDesc || align == 1) ? halfStroke : -halfStroke;
+ let endsOffset = (ascDesc || align == -1) ? -halfStroke : halfStroke;
+
+ gaps.forEach(g => {
+ g[0] += startsOffset;
+ g[1] += endsOffset;
+ });
+
+ if (!series.spanGaps)
+ _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
+
+ if (u.bands.length > 0) {
+ // ADDL OPT: only create band clips for series that are band lower edges
+ // if (b.series[1] == i && _paths.band == null)
+ _paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);
+ }
+
+ return _paths;
+ });
+ };
+}
+
+function bars(opts) {
+ opts = opts || EMPTY_OBJ;
+ const size = ifNull(opts.size, [0.6, inf, 1]);
+ const align = opts.align || 0;
+ const extraGap = (opts.gap || 0) * pxRatio;
+
+ const radius = ifNull(opts.radius, 0);
+
+ const gapFactor = 1 - size[0];
+ const maxWidth = ifNull(size[1], inf) * pxRatio;
+ const minWidth = ifNull(size[2], 1) * pxRatio;
+
+ const disp = ifNull(opts.disp, EMPTY_OBJ);
+ const _each = ifNull(opts.each, _ => {});
+
+ const { fill: dispFills, stroke: dispStrokes } = disp;
+
+ return (u, seriesIdx, idx0, idx1) => {
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ const _dirX = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+ const _dirY = scaleY.dir * (scaleY.ori == 1 ? 1 : -1);
+
+ let rect = scaleX.ori == 0 ? rectH : rectV;
+
+ let each = scaleX.ori == 0 ? _each : (u, seriesIdx, i, top, lft, hgt, wid) => {
+ _each(u, seriesIdx, i, lft, top, wid, hgt);
+ };
+
+ let fillToY = series.fillTo(u, seriesIdx, series.min, series.max);
+
+ let y0Pos = valToPosY(fillToY, scaleY, yDim, yOff);
+
+ // barWid is to center of stroke
+ let xShift, barWid;
+
+ let strokeWidth = pxRound(series.width * pxRatio);
+
+ let multiPath = false;
+
+ let fillColors = null;
+ let fillPaths = null;
+ let strokeColors = null;
+ let strokePaths = null;
+
+ if (dispFills != null && dispStrokes != null) {
+ multiPath = true;
+
+ fillColors = dispFills.values(u, seriesIdx, idx0, idx1);
+ fillPaths = new Map();
+ (new Set(fillColors)).forEach(color => {
+ if (color != null)
+ fillPaths.set(color, new Path2D());
+ });
+
+ strokeColors = dispStrokes.values(u, seriesIdx, idx0, idx1);
+ strokePaths = new Map();
+ (new Set(strokeColors)).forEach(color => {
+ if (color != null)
+ strokePaths.set(color, new Path2D());
+ });
+ }
+
+ let { x0, size } = disp;
+
+ if (x0 != null && size != null) {
+ dataX = x0.values(u, seriesIdx, idx0, idx1);
+
+ if (x0.unit == 2)
+ dataX = dataX.map(pct => u.posToVal(xOff + pct * xDim, scaleX.key, true));
+
+ // assumes uniform sizes, for now
+ let sizes = size.values(u, seriesIdx, idx0, idx1);
+
+ if (size.unit == 2)
+ barWid = sizes[0] * xDim;
+ else
+ barWid = valToPosX(sizes[0], scaleX, xDim, xOff) - valToPosX(0, scaleX, xDim, xOff); // assumes linear scale (delta from 0)
+
+ barWid = pxRound(barWid - strokeWidth);
+
+ xShift = (_dirX == 1 ? -strokeWidth / 2 : barWid + strokeWidth / 2);
+ }
+ else {
+ let colWid = xDim;
+
+ if (dataX.length > 1) {
+ // prior index with non-undefined y data
+ let prevIdx = null;
+
+ // scan full dataset for smallest adjacent delta
+ // will not work properly for non-linear x scales, since does not do expensive valToPosX calcs till end
+ for (let i = 0, minDelta = Infinity; i < dataX.length; i++) {
+ if (dataY[i] !== undefined) {
+ if (prevIdx != null) {
+ let delta = abs(dataX[i] - dataX[prevIdx]);
+
+ if (delta < minDelta) {
+ minDelta = delta;
+ colWid = abs(valToPosX(dataX[i], scaleX, xDim, xOff) - valToPosX(dataX[prevIdx], scaleX, xDim, xOff));
+ }
+ }
+
+ prevIdx = i;
+ }
+ }
+ }
+
+ let gapWid = colWid * gapFactor;
+
+ barWid = pxRound(min(maxWidth, max(minWidth, colWid - gapWid)) - strokeWidth - extraGap);
+
+ xShift = (align == 0 ? barWid / 2 : align == _dirX ? 0 : barWid) - align * _dirX * extraGap / 2;
+ }
+
+ const _paths = {stroke: null, fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL | BAND_CLIP_STROKE}; // disp, geom
+
+ const hasBands = u.bands.length > 0;
+ let yLimit;
+
+ if (hasBands) {
+ // ADDL OPT: only create band clips for series that are band lower edges
+ // if (b.series[1] == i && _paths.band == null)
+ _paths.band = new Path2D();
+ yLimit = pxRound(valToPosY(scaleY.max, scaleY, yDim, yOff));
+ }
+
+ const stroke = multiPath ? null : new Path2D();
+ const band = _paths.band;
+
+ for (let i = _dirX == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dirX) {
+ let yVal = dataY[i];
+
+ /*
+ // interpolate upwards band clips
+ if (yVal == null) {
+ // if (hasBands)
+ // yVal = costlyLerp(i, idx0, idx1, _dirX, dataY);
+ // else
+ continue;
+ }
+ */
+
+ let xVal = scaleX.distr != 2 || disp != null ? dataX[i] : i;
+
+ // TODO: all xPos can be pre-computed once for all series in aligned set
+ let xPos = valToPosX(xVal, scaleX, xDim, xOff);
+ let yPos = valToPosY(ifNull(yVal, fillToY) , scaleY, yDim, yOff);
+
+ let lft = pxRound(xPos - xShift);
+ let btm = pxRound(max(yPos, y0Pos));
+ let top = pxRound(min(yPos, y0Pos));
+ // this includes the stroke
+ let barHgt = btm - top;
+
+ let r = radius * barWid;
+
+ if (yVal != null) { // && yVal != fillToY (0 height bar)
+ if (multiPath) {
+ if (strokeWidth > 0 && strokeColors[i] != null)
+ rect(strokePaths.get(strokeColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);
+
+ if (fillColors[i] != null)
+ rect(fillPaths.get(fillColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);
+ }
+ else
+ rect(stroke, lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);
+
+ each(u, seriesIdx, i,
+ lft - strokeWidth / 2,
+ top,
+ barWid + strokeWidth,
+ barHgt,
+ );
+ }
+
+ if (hasBands) {
+ if (_dirY == 1) {
+ btm = top;
+ top = yLimit;
+ }
+ else {
+ top = btm;
+ btm = yLimit;
+ }
+
+ barHgt = btm - top;
+
+ rect(band, lft - strokeWidth / 2, top, barWid + strokeWidth, max(0, barHgt), 0);
+ }
+ }
+
+ if (strokeWidth > 0)
+ _paths.stroke = multiPath ? strokePaths : stroke;
+
+ _paths.fill = multiPath ? fillPaths : stroke;
+
+ return _paths;
+ });
+ };
+}
+
+function splineInterp(interp, opts) {
+ return (u, seriesIdx, idx0, idx1) => {
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ let moveTo, bezierCurveTo, lineTo;
+
+ if (scaleX.ori == 0) {
+ moveTo = moveToH;
+ lineTo = lineToH;
+ bezierCurveTo = bezierCurveToH;
+ }
+ else {
+ moveTo = moveToV;
+ lineTo = lineToV;
+ bezierCurveTo = bezierCurveToV;
+ }
+
+ const _dir = 1 * scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+
+ idx0 = nonNullIdx(dataY, idx0, idx1, 1);
+ idx1 = nonNullIdx(dataY, idx0, idx1, -1);
+
+ let gaps = [];
+ let inGap = false;
+ let firstXPos = pxRound(valToPosX(dataX[_dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));
+ let prevXPos = firstXPos;
+
+ let xCoords = [];
+ let yCoords = [];
+
+ for (let i = _dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dir) {
+ let yVal = dataY[i];
+ let xVal = dataX[i];
+ let xPos = valToPosX(xVal, scaleX, xDim, xOff);
+
+ if (yVal == null) {
+ if (yVal === null) {
+ addGap(gaps, prevXPos, xPos);
+ inGap = true;
+ }
+ continue;
+ }
+ else {
+ if (inGap) {
+ addGap(gaps, prevXPos, xPos);
+ inGap = false;
+ }
+
+ xCoords.push((prevXPos = xPos));
+ yCoords.push(valToPosY(dataY[i], scaleY, yDim, yOff));
+ }
+ }
+
+ const _paths = {stroke: interp(xCoords, yCoords, moveTo, lineTo, bezierCurveTo, pxRound), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};
+ const stroke = _paths.stroke;
+
+ if (series.fill != null && stroke != null) {
+ let fill = _paths.fill = new Path2D(stroke);
+
+ let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
+ let minY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff));
+
+ lineTo(fill, prevXPos, minY);
+ lineTo(fill, firstXPos, minY);
+ }
+
+ _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
+
+ if (!series.spanGaps)
+ _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
+
+ if (u.bands.length > 0) {
+ // ADDL OPT: only create band clips for series that are band lower edges
+ // if (b.series[1] == i && _paths.band == null)
+ _paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);
+ }
+
+ return _paths;
+
+ // if FEAT_PATHS: false in rollup.config.js
+ // u.ctx.save();
+ // u.ctx.beginPath();
+ // u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
+ // u.ctx.clip();
+ // u.ctx.strokeStyle = u.series[sidx].stroke;
+ // u.ctx.stroke(stroke);
+ // u.ctx.fillStyle = u.series[sidx].fill;
+ // u.ctx.fill(fill);
+ // u.ctx.restore();
+ // return null;
+ });
+ };
+}
+
+function monotoneCubic(opts) {
+ return splineInterp(_monotoneCubic);
+}
+
+// Monotone Cubic Spline interpolation, adapted from the Chartist.js implementation:
+// https://github.com/gionkunz/chartist-js/blob/e7e78201bffe9609915e5e53cfafa29a5d6c49f9/src/scripts/interpolation.js#L240-L369
+function _monotoneCubic(xs, ys, moveTo, lineTo, bezierCurveTo, pxRound) {
+ const n = xs.length;
+
+ if (n < 2)
+ return null;
+
+ const path = new Path2D();
+
+ moveTo(path, xs[0], ys[0]);
+
+ if (n == 2)
+ lineTo(path, xs[1], ys[1]);
+ else {
+ let ms = Array(n),
+ ds = Array(n - 1),
+ dys = Array(n - 1),
+ dxs = Array(n - 1);
+
+ // calc deltas and derivative
+ for (let i = 0; i < n - 1; i++) {
+ dys[i] = ys[i + 1] - ys[i];
+ dxs[i] = xs[i + 1] - xs[i];
+ ds[i] = dys[i] / dxs[i];
+ }
+
+ // determine desired slope (m) at each point using Fritsch-Carlson method
+ // http://math.stackexchange.com/questions/45218/implementation-of-monotone-cubic-interpolation
+ ms[0] = ds[0];
+
+ for (let i = 1; i < n - 1; i++) {
+ if (ds[i] === 0 || ds[i - 1] === 0 || (ds[i - 1] > 0) !== (ds[i] > 0))
+ ms[i] = 0;
+ else {
+ ms[i] = 3 * (dxs[i - 1] + dxs[i]) / (
+ (2 * dxs[i] + dxs[i - 1]) / ds[i - 1] +
+ (dxs[i] + 2 * dxs[i - 1]) / ds[i]
+ );
+
+ if (!isFinite(ms[i]))
+ ms[i] = 0;
+ }
+ }
+
+ ms[n - 1] = ds[n - 2];
+
+ for (let i = 0; i < n - 1; i++) {
+ bezierCurveTo(
+ path,
+ xs[i] + dxs[i] / 3,
+ ys[i] + ms[i] * dxs[i] / 3,
+ xs[i + 1] - dxs[i] / 3,
+ ys[i + 1] - ms[i + 1] * dxs[i] / 3,
+ xs[i + 1],
+ ys[i + 1],
+ );
+ }
+ }
+
+ return path;
+}
+
+const cursorPlots = new Set();
+
+function invalidateRects() {
+ cursorPlots.forEach(u => {
+ u.syncRect(true);
+ });
+}
+
+on(resize, win, invalidateRects);
+on(scroll, win, invalidateRects, true);
+
+const linearPath = linear() ;
+const pointsPath = points() ;
+
+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;
+ });
+ }
+}
+
+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 = 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;
+
+ if (scaleX.ori == 0) {
+ addClass(root, ORI_HZ);
+ valToPosX = getHPos;
+ valToPosY = getVPos;
+ /*
+ 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;
+ /*
+ 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 = (opts.tzDate || (ts => new Date(round(ts / ms))));
+ const _fmtDate = (opts.fmtDate || fmtDate);
+
+ const _timeAxisSplits = (ms == 1 ? timeAxisSplitsMs(_tzDate) : timeAxisSplitsS(_tzDate));
+ const _timeAxisVals = timeAxisVals(_tzDate, timeAxisStamps((ms == 1 ? _timeAxisStampsMs : _timeAxisStampsS), _fmtDate));
+ const _timeSeriesVal = timeSeriesVal(_tzDate, timeSeriesStamp(_timeSeriesStamp, _fmtDate));
+
+ const activeIdxs = [];
+
+ const legend = (self.legend = assign({}, legendOpts, opts.legend));
+ const showLegend = legend.show;
+ const markers = legend.markers;
+
+ {
+ 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 (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 (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, syncOpts.setSeries);
+ });
+ }
+ else
+ setSeries(seriesIdx, {show: !s.show}, true, 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 = (self.cursor = assign({}, cursorOpts, {drag: {y: mode == 2}}, opts.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}, cursor.focus);
+ const cursorFocus = 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 = 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 (cursor.show) {
+ activeIdxs.splice(i, 0, null);
+
+ let pt = initCursorPt(s, i);
+ pt && cursorPts.splice(i, 0, pt);
+ }
+ }
+
+ 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 (cursor.show) {
+ activeIdxs.splice(i, 1);
+
+ cursorPts.length > 1 && cursorPts.splice(i, 1)[0].remove();
+ }
+
+ // TODO: de-init no-longer-needed scales?
+ }
+
+ 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 = 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 (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;
+
+ {
+ cacheStrokeFill(i, false);
+ s._paths && drawPath(i, false);
+ }
+
+ {
+ 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(a => {
+ let { _show, _el, _size, _pos, side } = a;
+
+ 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);
+
+ _el && remClass(_el, OFF);
+ }
+ else
+ _el && 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 (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 = cursor.drag;
+
+ let dragX = drag.x;
+ let dragY = drag.y;
+
+ if (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);
+ 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;
+ toggleDOM(i, opts.show);
+
+ _setScale(mode == 2 ? s.facets[1].scale : s.scale, null, null);
+ commit();
+ }
+
+ _fire !== false && fire("setSeries", i, opts);
+
+ _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 (cursor.show && cursorPts[i])
+ cursorPts[i].style.opacity = value;
+
+ if (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;
+
+ (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) {
+ cursorPts.length > 1 && elTrans(cursorPts[i], -10, -10, plotWidCss, plotHgtCss);
+ }
+ }
+
+ if (cursorFocus)
+ setSeries(null, FOCUS_TRUE, true, src == null && syncOpts.setSeries);
+
+ if (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 (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 (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);
+ 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 (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 = 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);
+
+ (cursor.sync = syncOpts);
+
+ const syncKey = syncOpts.key;
+
+ const sync = _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);
+ }
+
+ 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);
+ }
+
+ (self.pub = pub);
+
+ function destroy() {
+ sync.unsub(self);
+ 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;
+
+{
+ uPlot.join = join;
+}
+
+{
+ uPlot.fmtDate = fmtDate;
+ uPlot.tzDate = tzDate;
+}
+
+{
+ uPlot.sync = _sync;
+}
+
+{
+ uPlot.addGap = addGap;
+ uPlot.clipGaps = clipGaps;
+
+ let paths = uPlot.paths = {
+ points,
+ };
+
+ (paths.linear = linear);
+ (paths.stepped = stepped);
+ (paths.bars = bars);
+ (paths.spline = monotoneCubic);
+}
+
+module.exports = uPlot;
diff --git a/build/resources/main/static/plugins/uplot/uPlot.esm.js b/build/resources/main/static/plugins/uplot/uPlot.esm.js
new file mode 100644
index 0000000..f7c486d
--- /dev/null
+++ b/build/resources/main/static/plugins/uplot/uPlot.esm.js
@@ -0,0 +1,5210 @@
+/**
+* Copyright (c) 2021, Leon Sorokin
+* All rights reserved. (MIT Licensed)
+*
+* uPlot.js (μPlot)
+* A small, fast chart for time series, lines, areas, ohlc & bars
+* https://github.com/leeoniya/uPlot (v1.6.18)
+*/
+
+const FEAT_TIME = true;
+
+// binary search for index of closest value
+function closestIdx(num, arr, lo, hi) {
+ let mid;
+ lo = lo || 0;
+ hi = hi || arr.length - 1;
+ let bitwise = hi <= 2147483647;
+
+ while (hi - lo > 1) {
+ mid = bitwise ? (lo + hi) >> 1 : floor((lo + hi) / 2);
+
+ if (arr[mid] < num)
+ lo = mid;
+ else
+ hi = mid;
+ }
+
+ if (num - arr[lo] <= arr[hi] - num)
+ return lo;
+
+ return hi;
+}
+
+function nonNullIdx(data, _i0, _i1, dir) {
+ for (let i = dir == 1 ? _i0 : _i1; i >= _i0 && i <= _i1; i += dir) {
+ if (data[i] != null)
+ return i;
+ }
+
+ return -1;
+}
+
+function getMinMax(data, _i0, _i1, sorted) {
+// console.log("getMinMax()");
+
+ let _min = inf;
+ let _max = -inf;
+
+ if (sorted == 1) {
+ _min = data[_i0];
+ _max = data[_i1];
+ }
+ else if (sorted == -1) {
+ _min = data[_i1];
+ _max = data[_i0];
+ }
+ else {
+ for (let i = _i0; i <= _i1; i++) {
+ if (data[i] != null) {
+ _min = min(_min, data[i]);
+ _max = max(_max, data[i]);
+ }
+ }
+ }
+
+ return [_min, _max];
+}
+
+function getMinMaxLog(data, _i0, _i1) {
+// console.log("getMinMax()");
+
+ let _min = inf;
+ let _max = -inf;
+
+ for (let i = _i0; i <= _i1; i++) {
+ if (data[i] > 0) {
+ _min = min(_min, data[i]);
+ _max = max(_max, data[i]);
+ }
+ }
+
+ return [
+ _min == inf ? 1 : _min,
+ _max == -inf ? 10 : _max,
+ ];
+}
+
+const _fixedTuple = [0, 0];
+
+function fixIncr(minIncr, maxIncr, minExp, maxExp) {
+ _fixedTuple[0] = minExp < 0 ? roundDec(minIncr, -minExp) : minIncr;
+ _fixedTuple[1] = maxExp < 0 ? roundDec(maxIncr, -maxExp) : maxIncr;
+ return _fixedTuple;
+}
+
+function rangeLog(min, max, base, fullMags) {
+ let minSign = sign(min);
+
+ let logFn = base == 10 ? log10 : log2;
+
+ if (min == max) {
+ if (minSign == -1) {
+ min *= base;
+ max /= base;
+ }
+ else {
+ min /= base;
+ max *= base;
+ }
+ }
+
+ let minExp, maxExp, minMaxIncrs;
+
+ if (fullMags) {
+ minExp = floor(logFn(min));
+ maxExp = ceil(logFn(max));
+
+ minMaxIncrs = fixIncr(pow(base, minExp), pow(base, maxExp), minExp, maxExp);
+
+ min = minMaxIncrs[0];
+ max = minMaxIncrs[1];
+ }
+ else {
+ minExp = floor(logFn(abs(min)));
+ maxExp = floor(logFn(abs(max)));
+
+ minMaxIncrs = fixIncr(pow(base, minExp), pow(base, maxExp), minExp, maxExp);
+
+ min = incrRoundDn(min, minMaxIncrs[0]);
+ max = incrRoundUp(max, minMaxIncrs[1]);
+ }
+
+ return [min, max];
+}
+
+function rangeAsinh(min, max, base, fullMags) {
+ let minMax = rangeLog(min, max, base, fullMags);
+
+ if (min == 0)
+ minMax[0] = 0;
+
+ if (max == 0)
+ minMax[1] = 0;
+
+ return minMax;
+}
+
+const rangePad = 0.1;
+
+const autoRangePart = {
+ mode: 3,
+ pad: rangePad,
+};
+
+const _eqRangePart = {
+ pad: 0,
+ soft: null,
+ mode: 0,
+};
+
+const _eqRange = {
+ min: _eqRangePart,
+ max: _eqRangePart,
+};
+
+// 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 rangeNum(_min, _max, mult, extra) {
+ if (isObj(mult))
+ return _rangeNum(_min, _max, mult);
+
+ _eqRangePart.pad = mult;
+ _eqRangePart.soft = extra ? 0 : null;
+ _eqRangePart.mode = extra ? 3 : 0;
+
+ return _rangeNum(_min, _max, _eqRange);
+}
+
+// nullish coalesce
+function ifNull(lh, rh) {
+ return lh == null ? rh : lh;
+}
+
+// checks if given index range in an array contains a non-null value
+// aka a range-bounded Array.some()
+function hasData(data, idx0, idx1) {
+ idx0 = ifNull(idx0, 0);
+ idx1 = ifNull(idx1, data.length - 1);
+
+ while (idx0 <= idx1) {
+ if (data[idx0] != null)
+ return true;
+ idx0++;
+ }
+
+ return false;
+}
+
+function _rangeNum(_min, _max, cfg) {
+ let cmin = cfg.min;
+ let cmax = cfg.max;
+
+ let padMin = ifNull(cmin.pad, 0);
+ let padMax = ifNull(cmax.pad, 0);
+
+ let hardMin = ifNull(cmin.hard, -inf);
+ let hardMax = ifNull(cmax.hard, inf);
+
+ let softMin = ifNull(cmin.soft, inf);
+ let softMax = ifNull(cmax.soft, -inf);
+
+ let softMinMode = ifNull(cmin.mode, 0);
+ let softMaxMode = ifNull(cmax.mode, 0);
+
+ let delta = _max - _min;
+
+ // this handles situations like 89.7, 89.69999999999999
+ // by assuming 0.001x deltas are precision errors
+// if (delta > 0 && delta < abs(_max) / 1e3)
+// delta = 0;
+
+ // treat data as flat if delta is less than 1 billionth
+ if (delta < 1e-9) {
+ delta = 0;
+
+ // if soft mode is 2 and all vals are flat at 0, avoid the 0.1 * 1e3 fallback
+ // this prevents 0,0,0 from ranging to -100,100 when softMin/softMax are -1,1
+ if (_min == 0 || _max == 0) {
+ delta = 1e-9;
+
+ if (softMinMode == 2 && softMin != inf)
+ padMin = 0;
+
+ if (softMaxMode == 2 && softMax != -inf)
+ padMax = 0;
+ }
+ }
+
+ let nonZeroDelta = delta || abs(_max) || 1e3;
+ let mag = log10(nonZeroDelta);
+ let base = pow(10, floor(mag));
+
+ let _padMin = nonZeroDelta * (delta == 0 ? (_min == 0 ? .1 : 1) : padMin);
+ let _newMin = roundDec(incrRoundDn(_min - _padMin, base/10), 9);
+ let _softMin = _min >= softMin && (softMinMode == 1 || softMinMode == 3 && _newMin <= softMin || softMinMode == 2 && _newMin >= softMin) ? softMin : inf;
+ let minLim = max(hardMin, _newMin < _softMin && _min >= _softMin ? _softMin : min(_softMin, _newMin));
+
+ let _padMax = nonZeroDelta * (delta == 0 ? (_max == 0 ? .1 : 1) : padMax);
+ let _newMax = roundDec(incrRoundUp(_max + _padMax, base/10), 9);
+ let _softMax = _max <= softMax && (softMaxMode == 1 || softMaxMode == 3 && _newMax >= softMax || softMaxMode == 2 && _newMax <= softMax) ? softMax : -inf;
+ let maxLim = min(hardMax, _newMax > _softMax && _max <= _softMax ? _softMax : max(_softMax, _newMax));
+
+ if (minLim == maxLim && minLim == 0)
+ maxLim = 100;
+
+ return [minLim, maxLim];
+}
+
+// alternative: https://stackoverflow.com/a/2254896
+const fmtNum = new Intl.NumberFormat(navigator.language).format;
+
+const M = Math;
+
+const PI = M.PI;
+const abs = M.abs;
+const floor = M.floor;
+const round = M.round;
+const ceil = M.ceil;
+const min = M.min;
+const max = M.max;
+const pow = M.pow;
+const sign = M.sign;
+const log10 = M.log10;
+const log2 = M.log2;
+// TODO: seems like this needs to match asinh impl if the passed v is tweaked?
+const sinh = (v, linthresh = 1) => M.sinh(v) * linthresh;
+const asinh = (v, linthresh = 1) => M.asinh(v / linthresh);
+
+const inf = Infinity;
+
+function numIntDigits(x) {
+ return (log10((x ^ (x >> 31)) - (x >> 31)) | 0) + 1;
+}
+
+function incrRound(num, incr) {
+ return round(num/incr)*incr;
+}
+
+function clamp(num, _min, _max) {
+ return min(max(num, _min), _max);
+}
+
+function fnOrSelf(v) {
+ return typeof v == "function" ? v : () => v;
+}
+
+const retArg0 = _0 => _0;
+
+const retArg1 = (_0, _1) => _1;
+
+const retNull = _ => null;
+
+const retTrue = _ => true;
+
+const retEq = (a, b) => a == b;
+
+function incrRoundUp(num, incr) {
+ return ceil(num/incr)*incr;
+}
+
+function incrRoundDn(num, incr) {
+ return floor(num/incr)*incr;
+}
+
+function roundDec(val, dec) {
+ return round(val * (dec = 10**dec)) / dec;
+}
+
+const fixedDec = new Map();
+
+function guessDec(num) {
+ return ((""+num).split(".")[1] || "").length;
+}
+
+function genIncrs(base, minExp, maxExp, mults) {
+ let incrs = [];
+
+ let multDec = mults.map(guessDec);
+
+ for (let exp = minExp; exp < maxExp; exp++) {
+ let expa = abs(exp);
+ let mag = roundDec(pow(base, exp), expa);
+
+ for (let i = 0; i < mults.length; i++) {
+ let _incr = mults[i] * mag;
+ let dec = (_incr >= 0 && exp >= 0 ? 0 : expa) + (exp >= multDec[i] ? 0 : multDec[i]);
+ let incr = roundDec(_incr, dec);
+ incrs.push(incr);
+ fixedDec.set(incr, dec);
+ }
+ }
+
+ return incrs;
+}
+
+//export const assign = Object.assign;
+
+const EMPTY_OBJ = {};
+const EMPTY_ARR = [];
+
+const nullNullTuple = [null, null];
+
+const isArr = Array.isArray;
+
+function isStr(v) {
+ return typeof v == 'string';
+}
+
+function isObj(v) {
+ let is = false;
+
+ if (v != null) {
+ let c = v.constructor;
+ is = c == null || c == Object;
+ }
+
+ return is;
+}
+
+function fastIsObj(v) {
+ return v != null && typeof v == 'object';
+}
+
+function copy(o, _isObj = isObj) {
+ let out;
+
+ if (isArr(o)) {
+ let val = o.find(v => v != null);
+
+ if (isArr(val) || _isObj(val)) {
+ out = Array(o.length);
+ for (let i = 0; i < o.length; i++)
+ out[i] = copy(o[i], _isObj);
+ }
+ else
+ out = o.slice();
+ }
+ else if (_isObj(o)) {
+ out = {};
+ for (let k in o)
+ out[k] = copy(o[k], _isObj);
+ }
+ else
+ out = o;
+
+ return out;
+}
+
+function assign(targ) {
+ let args = arguments;
+
+ for (let i = 1; i < args.length; i++) {
+ let src = args[i];
+
+ for (let key in src) {
+ if (isObj(targ[key]))
+ assign(targ[key], copy(src[key]));
+ else
+ targ[key] = copy(src[key]);
+ }
+ }
+
+ return targ;
+}
+
+// nullModes
+const NULL_REMOVE = 0; // nulls are converted to undefined (e.g. for spanGaps: true)
+const NULL_RETAIN = 1; // nulls are retained, with alignment artifacts set to undefined (default)
+const NULL_EXPAND = 2; // nulls are expanded to include any adjacent alignment artifacts
+
+// sets undefined values to nulls when adjacent to existing nulls (minesweeper)
+function nullExpand(yVals, nullIdxs, alignedLen) {
+ for (let i = 0, xi, lastNullIdx = -1; i < nullIdxs.length; i++) {
+ let nullIdx = nullIdxs[i];
+
+ if (nullIdx > lastNullIdx) {
+ xi = nullIdx - 1;
+ while (xi >= 0 && yVals[xi] == null)
+ yVals[xi--] = null;
+
+ xi = nullIdx + 1;
+ while (xi < alignedLen && yVals[xi] == null)
+ yVals[lastNullIdx = xi++] = null;
+ }
+ }
+}
+
+// nullModes is a tables-matched array indicating how to treat nulls in each series
+// output is sorted ASC on the joined field (table[0]) and duplicate join values are collapsed
+function join(tables, nullModes) {
+ let xVals = new Set();
+
+ for (let ti = 0; ti < tables.length; ti++) {
+ let t = tables[ti];
+ let xs = t[0];
+ let len = xs.length;
+
+ for (let i = 0; i < len; i++)
+ xVals.add(xs[i]);
+ }
+
+ let data = [Array.from(xVals).sort((a, b) => a - b)];
+
+ let alignedLen = data[0].length;
+
+ let xIdxs = new Map();
+
+ for (let i = 0; i < alignedLen; i++)
+ xIdxs.set(data[0][i], i);
+
+ for (let ti = 0; ti < tables.length; ti++) {
+ let t = tables[ti];
+ let xs = t[0];
+
+ for (let si = 1; si < t.length; si++) {
+ let ys = t[si];
+
+ let yVals = Array(alignedLen).fill(undefined);
+
+ let nullMode = nullModes ? nullModes[ti][si] : NULL_RETAIN;
+
+ let nullIdxs = [];
+
+ for (let i = 0; i < ys.length; i++) {
+ let yVal = ys[i];
+ let alignedIdx = xIdxs.get(xs[i]);
+
+ if (yVal === null) {
+ if (nullMode != NULL_REMOVE) {
+ yVals[alignedIdx] = yVal;
+
+ if (nullMode == NULL_EXPAND)
+ nullIdxs.push(alignedIdx);
+ }
+ }
+ else
+ yVals[alignedIdx] = yVal;
+ }
+
+ nullExpand(yVals, nullIdxs, alignedLen);
+
+ data.push(yVals);
+ }
+ }
+
+ return data;
+}
+
+const microTask = typeof queueMicrotask == "undefined" ? fn => Promise.resolve().then(fn) : queueMicrotask;
+
+const WIDTH = "width";
+const HEIGHT = "height";
+const TOP = "top";
+const BOTTOM = "bottom";
+const LEFT = "left";
+const RIGHT = "right";
+const hexBlack = "#000";
+const transparent = hexBlack + "0";
+
+const mousemove = "mousemove";
+const mousedown = "mousedown";
+const mouseup = "mouseup";
+const mouseenter = "mouseenter";
+const mouseleave = "mouseleave";
+const dblclick = "dblclick";
+const resize = "resize";
+const scroll = "scroll";
+
+const change = "change";
+const dppxchange = "dppxchange";
+
+const pre = "u-";
+
+const UPLOT = "uplot";
+const ORI_HZ = pre + "hz";
+const ORI_VT = pre + "vt";
+const TITLE = pre + "title";
+const WRAP = pre + "wrap";
+const UNDER = pre + "under";
+const OVER = pre + "over";
+const AXIS = pre + "axis";
+const OFF = pre + "off";
+const SELECT = pre + "select";
+const CURSOR_X = pre + "cursor-x";
+const CURSOR_Y = pre + "cursor-y";
+const CURSOR_PT = pre + "cursor-pt";
+const LEGEND = pre + "legend";
+const LEGEND_LIVE = pre + "live";
+const LEGEND_INLINE = pre + "inline";
+const LEGEND_THEAD = pre + "thead";
+const LEGEND_SERIES = pre + "series";
+const LEGEND_MARKER = pre + "marker";
+const LEGEND_LABEL = pre + "label";
+const LEGEND_VALUE = pre + "value";
+
+const doc = document;
+const win = window;
+let pxRatio;
+
+let query;
+
+function setPxRatio() {
+ let _pxRatio = devicePixelRatio;
+
+ // during print preview, Chrome fires off these dppx queries even without changes
+ if (pxRatio != _pxRatio) {
+ pxRatio = _pxRatio;
+
+ query && off(change, query, setPxRatio);
+ query = matchMedia(`(min-resolution: ${pxRatio - 0.001}dppx) and (max-resolution: ${pxRatio + 0.001}dppx)`);
+ on(change, query, setPxRatio);
+
+ win.dispatchEvent(new CustomEvent(dppxchange));
+ }
+}
+
+function addClass(el, c) {
+ if (c != null) {
+ let cl = el.classList;
+ !cl.contains(c) && cl.add(c);
+ }
+}
+
+function remClass(el, c) {
+ let cl = el.classList;
+ cl.contains(c) && cl.remove(c);
+}
+
+function setStylePx(el, name, value) {
+ el.style[name] = value + "px";
+}
+
+function placeTag(tag, cls, targ, refEl) {
+ let el = doc.createElement(tag);
+
+ if (cls != null)
+ addClass(el, cls);
+
+ if (targ != null)
+ targ.insertBefore(el, refEl);
+
+ return el;
+}
+
+function placeDiv(cls, targ) {
+ return placeTag("div", cls, targ);
+}
+
+const xformCache = new WeakMap();
+
+function elTrans(el, xPos, yPos, xMax, yMax) {
+ let xform = "translate(" + xPos + "px," + yPos + "px)";
+ let xformOld = xformCache.get(el);
+
+ if (xform != xformOld) {
+ el.style.transform = xform;
+ xformCache.set(el, xform);
+
+ if (xPos < 0 || yPos < 0 || xPos > xMax || yPos > yMax)
+ addClass(el, OFF);
+ else
+ remClass(el, OFF);
+ }
+}
+
+const colorCache = new WeakMap();
+
+function elColor(el, background, borderColor) {
+ let newColor = background + borderColor;
+ let oldColor = colorCache.get(el);
+
+ if (newColor != oldColor) {
+ colorCache.set(el, newColor);
+ el.style.background = background;
+ el.style.borderColor = borderColor;
+ }
+}
+
+const sizeCache = new WeakMap();
+
+function elSize(el, newWid, newHgt, centered) {
+ let newSize = newWid + "" + newHgt;
+ let oldSize = sizeCache.get(el);
+
+ if (newSize != oldSize) {
+ sizeCache.set(el, newSize);
+ el.style.height = newHgt + "px";
+ el.style.width = newWid + "px";
+ el.style.marginLeft = centered ? -newWid/2 + "px" : 0;
+ el.style.marginTop = centered ? -newHgt/2 + "px" : 0;
+ }
+}
+
+const evOpts = {passive: true};
+const evOpts2 = assign({capture: true}, evOpts);
+
+function on(ev, el, cb, capt) {
+ el.addEventListener(ev, cb, capt ? evOpts2 : evOpts);
+}
+
+function off(ev, el, cb, capt) {
+ el.removeEventListener(ev, cb, capt ? evOpts2 : evOpts);
+}
+
+setPxRatio();
+
+const months = [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+];
+
+const days = [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+];
+
+function slice3(str) {
+ return str.slice(0, 3);
+}
+
+const days3 = days.map(slice3);
+
+const months3 = months.map(slice3);
+
+const engNames = {
+ MMMM: months,
+ MMM: months3,
+ WWWW: days,
+ WWW: days3,
+};
+
+function zeroPad2(int) {
+ return (int < 10 ? '0' : '') + int;
+}
+
+function zeroPad3(int) {
+ return (int < 10 ? '00' : int < 100 ? '0' : '') + int;
+}
+
+/*
+function suffix(int) {
+ let mod10 = int % 10;
+
+ return int + (
+ mod10 == 1 && int != 11 ? "st" :
+ mod10 == 2 && int != 12 ? "nd" :
+ mod10 == 3 && int != 13 ? "rd" : "th"
+ );
+}
+*/
+
+const subs = {
+ // 2019
+ YYYY: d => d.getFullYear(),
+ // 19
+ YY: d => (d.getFullYear()+'').slice(2),
+ // July
+ MMMM: (d, names) => names.MMMM[d.getMonth()],
+ // Jul
+ MMM: (d, names) => names.MMM[d.getMonth()],
+ // 07
+ MM: d => zeroPad2(d.getMonth()+1),
+ // 7
+ M: d => d.getMonth()+1,
+ // 09
+ DD: d => zeroPad2(d.getDate()),
+ // 9
+ D: d => d.getDate(),
+ // Monday
+ WWWW: (d, names) => names.WWWW[d.getDay()],
+ // Mon
+ WWW: (d, names) => names.WWW[d.getDay()],
+ // 03
+ HH: d => zeroPad2(d.getHours()),
+ // 3
+ H: d => d.getHours(),
+ // 9 (12hr, unpadded)
+ h: d => {let h = d.getHours(); return h == 0 ? 12 : h > 12 ? h - 12 : h;},
+ // AM
+ AA: d => d.getHours() >= 12 ? 'PM' : 'AM',
+ // am
+ aa: d => d.getHours() >= 12 ? 'pm' : 'am',
+ // a
+ a: d => d.getHours() >= 12 ? 'p' : 'a',
+ // 09
+ mm: d => zeroPad2(d.getMinutes()),
+ // 9
+ m: d => d.getMinutes(),
+ // 09
+ ss: d => zeroPad2(d.getSeconds()),
+ // 9
+ s: d => d.getSeconds(),
+ // 374
+ fff: d => zeroPad3(d.getMilliseconds()),
+};
+
+function fmtDate(tpl, names) {
+ names = names || engNames;
+ let parts = [];
+
+ let R = /\{([a-z]+)\}|[^{]+/gi, m;
+
+ while (m = R.exec(tpl))
+ parts.push(m[0][0] == '{' ? subs[m[1]] : m[0]);
+
+ return d => {
+ let out = '';
+
+ for (let i = 0; i < parts.length; i++)
+ out += typeof parts[i] == "string" ? parts[i] : parts[i](d, names);
+
+ return out;
+ }
+}
+
+const localTz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+// https://stackoverflow.com/questions/15141762/how-to-initialize-a-javascript-date-to-a-particular-time-zone/53652131#53652131
+function tzDate(date, tz) {
+ let date2;
+
+ // perf optimization
+ if (tz == 'UTC' || tz == 'Etc/UTC')
+ date2 = new Date(+date + date.getTimezoneOffset() * 6e4);
+ else if (tz == localTz)
+ date2 = date;
+ else {
+ date2 = new Date(date.toLocaleString('en-US', {timeZone: tz}));
+ date2.setMilliseconds(date.getMilliseconds());
+ }
+
+ return date2;
+}
+
+//export const series = [];
+
+// default formatters:
+
+const onlyWhole = v => v % 1 == 0;
+
+const allMults = [1,2,2.5,5];
+
+// ...0.01, 0.02, 0.025, 0.05, 0.1, 0.2, 0.25, 0.5
+const decIncrs = genIncrs(10, -16, 0, allMults);
+
+// 1, 2, 2.5, 5, 10, 20, 25, 50...
+const oneIncrs = genIncrs(10, 0, 16, allMults);
+
+// 1, 2, 5, 10, 20, 25, 50...
+const wholeIncrs = oneIncrs.filter(onlyWhole);
+
+const numIncrs = decIncrs.concat(oneIncrs);
+
+const NL = "\n";
+
+const yyyy = "{YYYY}";
+const NLyyyy = NL + yyyy;
+const md = "{M}/{D}";
+const NLmd = NL + md;
+const NLmdyy = NLmd + "/{YY}";
+
+const aa = "{aa}";
+const hmm = "{h}:{mm}";
+const hmmaa = hmm + aa;
+const NLhmmaa = NL + hmmaa;
+const ss = ":{ss}";
+
+const _ = null;
+
+function genTimeStuffs(ms) {
+ let s = ms * 1e3,
+ m = s * 60,
+ h = m * 60,
+ d = h * 24,
+ mo = d * 30,
+ y = d * 365;
+
+ // min of 1e-3 prevents setting a temporal x ticks too small since Date objects cannot advance ticks smaller than 1ms
+ let subSecIncrs = ms == 1 ? genIncrs(10, 0, 3, allMults).filter(onlyWhole) : genIncrs(10, -3, 0, allMults);
+
+ let timeIncrs = subSecIncrs.concat([
+ // minute divisors (# of secs)
+ s,
+ s * 5,
+ s * 10,
+ s * 15,
+ s * 30,
+ // hour divisors (# of mins)
+ m,
+ m * 5,
+ m * 10,
+ m * 15,
+ m * 30,
+ // day divisors (# of hrs)
+ h,
+ h * 2,
+ h * 3,
+ h * 4,
+ h * 6,
+ h * 8,
+ h * 12,
+ // month divisors TODO: need more?
+ d,
+ d * 2,
+ d * 3,
+ d * 4,
+ d * 5,
+ d * 6,
+ d * 7,
+ d * 8,
+ d * 9,
+ d * 10,
+ d * 15,
+ // year divisors (# months, approx)
+ mo,
+ mo * 2,
+ mo * 3,
+ mo * 4,
+ mo * 6,
+ // century divisors
+ y,
+ y * 2,
+ y * 5,
+ y * 10,
+ y * 25,
+ y * 50,
+ y * 100,
+ ]);
+
+ // [0]: minimum num secs in the tick incr
+ // [1]: default tick format
+ // [2-7]: rollover tick formats
+ // [8]: mode: 0: replace [1] -> [2-7], 1: concat [1] + [2-7]
+ const _timeAxisStamps = [
+ // tick incr default year month day hour min sec mode
+ [y, yyyy, _, _, _, _, _, _, 1],
+ [d * 28, "{MMM}", NLyyyy, _, _, _, _, _, 1],
+ [d, md, NLyyyy, _, _, _, _, _, 1],
+ [h, "{h}" + aa, NLmdyy, _, NLmd, _, _, _, 1],
+ [m, hmmaa, NLmdyy, _, NLmd, _, _, _, 1],
+ [s, ss, NLmdyy + " " + hmmaa, _, NLmd + " " + hmmaa, _, NLhmmaa, _, 1],
+ [ms, ss + ".{fff}", NLmdyy + " " + hmmaa, _, NLmd + " " + hmmaa, _, NLhmmaa, _, 1],
+ ];
+
+ // the ensures that axis ticks, values & grid are aligned to logical temporal breakpoints and not an arbitrary timestamp
+ // https://www.timeanddate.com/time/dst/
+ // https://www.timeanddate.com/time/dst/2019.html
+ // https://www.epochconverter.com/timezones
+ function timeAxisSplits(tzDate) {
+ return (self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace) => {
+ let splits = [];
+ let isYr = foundIncr >= y;
+ let isMo = foundIncr >= mo && foundIncr < y;
+
+ // get the timezone-adjusted date
+ let minDate = tzDate(scaleMin);
+ let minDateTs = roundDec(minDate * ms, 3);
+
+ // get ts of 12am (this lands us at or before the original scaleMin)
+ let minMin = mkDate(minDate.getFullYear(), isYr ? 0 : minDate.getMonth(), isMo || isYr ? 1 : minDate.getDate());
+ let minMinTs = roundDec(minMin * ms, 3);
+
+ if (isMo || isYr) {
+ let moIncr = isMo ? foundIncr / mo : 0;
+ let yrIncr = isYr ? foundIncr / y : 0;
+ // let tzOffset = scaleMin - minDateTs; // needed?
+ let split = minDateTs == minMinTs ? minDateTs : roundDec(mkDate(minMin.getFullYear() + yrIncr, minMin.getMonth() + moIncr, 1) * ms, 3);
+ let splitDate = new Date(round(split / ms));
+ let baseYear = splitDate.getFullYear();
+ let baseMonth = splitDate.getMonth();
+
+ for (let i = 0; split <= scaleMax; i++) {
+ let next = mkDate(baseYear + yrIncr * i, baseMonth + moIncr * i, 1);
+ let offs = next - tzDate(roundDec(next * ms, 3));
+
+ split = roundDec((+next + offs) * ms, 3);
+
+ if (split <= scaleMax)
+ splits.push(split);
+ }
+ }
+ else {
+ let incr0 = foundIncr >= d ? d : foundIncr;
+ let tzOffset = floor(scaleMin) - floor(minDateTs);
+ let split = minMinTs + tzOffset + incrRoundUp(minDateTs - minMinTs, incr0);
+ splits.push(split);
+
+ let date0 = tzDate(split);
+
+ let prevHour = date0.getHours() + (date0.getMinutes() / m) + (date0.getSeconds() / h);
+ let incrHours = foundIncr / h;
+
+ let minSpace = self.axes[axisIdx]._space;
+ let pctSpace = foundSpace / minSpace;
+
+ while (1) {
+ split = roundDec(split + foundIncr, ms == 1 ? 0 : 3);
+
+ if (split > scaleMax)
+ break;
+
+ if (incrHours > 1) {
+ let expectedHour = floor(roundDec(prevHour + incrHours, 6)) % 24;
+ let splitDate = tzDate(split);
+ let actualHour = splitDate.getHours();
+
+ let dstShift = actualHour - expectedHour;
+
+ if (dstShift > 1)
+ dstShift = -1;
+
+ split -= dstShift * h;
+
+ prevHour = (prevHour + incrHours) % 24;
+
+ // add a tick only if it's further than 70% of the min allowed label spacing
+ let prevSplit = splits[splits.length - 1];
+ let pctIncr = roundDec((split - prevSplit) / foundIncr, 3);
+
+ if (pctIncr * pctSpace >= .7)
+ splits.push(split);
+ }
+ else
+ splits.push(split);
+ }
+ }
+
+ return splits;
+ }
+ }
+
+ return [
+ timeIncrs,
+ _timeAxisStamps,
+ timeAxisSplits,
+ ];
+}
+
+const [ timeIncrsMs, _timeAxisStampsMs, timeAxisSplitsMs ] = genTimeStuffs(1);
+const [ timeIncrsS, _timeAxisStampsS, timeAxisSplitsS ] = genTimeStuffs(1e-3);
+
+// base 2
+genIncrs(2, -53, 53, [1]);
+
+/*
+console.log({
+ decIncrs,
+ oneIncrs,
+ wholeIncrs,
+ numIncrs,
+ timeIncrs,
+ fixedDec,
+});
+*/
+
+function timeAxisStamps(stampCfg, fmtDate) {
+ return stampCfg.map(s => s.map((v, i) =>
+ i == 0 || i == 8 || v == null ? v : fmtDate(i == 1 || s[8] == 0 ? v : s[1] + v)
+ ));
+}
+
+// TODO: will need to accept spaces[] and pull incr into the loop when grid will be non-uniform, eg for log scales.
+// currently we ignore this for months since they're *nearly* uniform and the added complexity is not worth it
+function timeAxisVals(tzDate, stamps) {
+ return (self, splits, axisIdx, foundSpace, foundIncr) => {
+ let s = stamps.find(s => foundIncr >= s[0]) || stamps[stamps.length - 1];
+
+ // these track boundaries when a full label is needed again
+ let prevYear;
+ let prevMnth;
+ let prevDate;
+ let prevHour;
+ let prevMins;
+ let prevSecs;
+
+ return splits.map(split => {
+ let date = tzDate(split);
+
+ let newYear = date.getFullYear();
+ let newMnth = date.getMonth();
+ let newDate = date.getDate();
+ let newHour = date.getHours();
+ let newMins = date.getMinutes();
+ let newSecs = date.getSeconds();
+
+ let stamp = (
+ newYear != prevYear && s[2] ||
+ newMnth != prevMnth && s[3] ||
+ newDate != prevDate && s[4] ||
+ newHour != prevHour && s[5] ||
+ newMins != prevMins && s[6] ||
+ newSecs != prevSecs && s[7] ||
+ s[1]
+ );
+
+ prevYear = newYear;
+ prevMnth = newMnth;
+ prevDate = newDate;
+ prevHour = newHour;
+ prevMins = newMins;
+ prevSecs = newSecs;
+
+ return stamp(date);
+ });
+ }
+}
+
+// for when axis.values is defined as a static fmtDate template string
+function timeAxisVal(tzDate, dateTpl) {
+ let stamp = fmtDate(dateTpl);
+ return (self, splits, axisIdx, foundSpace, foundIncr) => splits.map(split => stamp(tzDate(split)));
+}
+
+function mkDate(y, m, d) {
+ return new Date(y, m, d);
+}
+
+function timeSeriesStamp(stampCfg, fmtDate) {
+ return fmtDate(stampCfg);
+}
+const _timeSeriesStamp = '{YYYY}-{MM}-{DD} {h}:{mm}{aa}';
+
+function timeSeriesVal(tzDate, stamp) {
+ return (self, val) => stamp(tzDate(val));
+}
+
+function legendStroke(self, seriesIdx) {
+ let s = self.series[seriesIdx];
+ return s.width ? s.stroke(self, seriesIdx) : s.points.width ? s.points.stroke(self, seriesIdx) : null;
+}
+
+function legendFill(self, seriesIdx) {
+ return self.series[seriesIdx].fill(self, seriesIdx);
+}
+
+const legendOpts = {
+ show: true,
+ live: true,
+ isolate: false,
+ markers: {
+ show: true,
+ width: 2,
+ stroke: legendStroke,
+ fill: legendFill,
+ dash: "solid",
+ },
+ idx: null,
+ idxs: null,
+ values: [],
+};
+
+function cursorPointShow(self, si) {
+ let o = self.cursor.points;
+
+ let pt = placeDiv();
+
+ let size = o.size(self, si);
+ setStylePx(pt, WIDTH, size);
+ setStylePx(pt, HEIGHT, size);
+
+ let mar = size / -2;
+ setStylePx(pt, "marginLeft", mar);
+ setStylePx(pt, "marginTop", mar);
+
+ let width = o.width(self, si, size);
+ width && setStylePx(pt, "borderWidth", width);
+
+ return pt;
+}
+
+function cursorPointFill(self, si) {
+ let sp = self.series[si].points;
+ return sp._fill || sp._stroke;
+}
+
+function cursorPointStroke(self, si) {
+ let sp = self.series[si].points;
+ return sp._stroke || sp._fill;
+}
+
+function cursorPointSize(self, si) {
+ let sp = self.series[si].points;
+ return ptDia(sp.width, 1);
+}
+
+function dataIdx(self, seriesIdx, cursorIdx) {
+ return cursorIdx;
+}
+
+const moveTuple = [0,0];
+
+function cursorMove(self, mouseLeft1, mouseTop1) {
+ moveTuple[0] = mouseLeft1;
+ moveTuple[1] = mouseTop1;
+ return moveTuple;
+}
+
+function filtBtn0(self, targ, handle) {
+ return e => {
+ e.button == 0 && handle(e);
+ };
+}
+
+function passThru(self, targ, handle) {
+ return handle;
+}
+
+const cursorOpts = {
+ show: true,
+ x: true,
+ y: true,
+ lock: false,
+ move: cursorMove,
+ points: {
+ show: cursorPointShow,
+ size: cursorPointSize,
+ width: 0,
+ stroke: cursorPointStroke,
+ fill: cursorPointFill,
+ },
+
+ bind: {
+ mousedown: filtBtn0,
+ mouseup: filtBtn0,
+ click: filtBtn0,
+ dblclick: filtBtn0,
+
+ mousemove: passThru,
+ mouseleave: passThru,
+ mouseenter: passThru,
+ },
+
+ drag: {
+ setScale: true,
+ x: true,
+ y: false,
+ dist: 0,
+ uni: null,
+ _x: false,
+ _y: false,
+ },
+
+ focus: {
+ prox: -1,
+ },
+
+ left: -10,
+ top: -10,
+ idx: null,
+ dataIdx,
+ idxs: null,
+};
+
+const grid = {
+ show: true,
+ stroke: "rgba(0,0,0,0.07)",
+ width: 2,
+// dash: [],
+ filter: retArg1,
+};
+
+const ticks = assign({}, grid, {size: 10});
+
+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"';
+const labelFont = "bold " + font;
+const lineMult = 1.5; // font-size multiplier
+
+const xAxisOpts = {
+ show: true,
+ scale: "x",
+ stroke: hexBlack,
+ space: 50,
+ gap: 5,
+ size: 50,
+ labelGap: 0,
+ labelSize: 30,
+ labelFont,
+ side: 2,
+// class: "x-vals",
+// incrs: timeIncrs,
+// values: timeVals,
+// filter: retArg1,
+ grid,
+ ticks,
+ font,
+ rotate: 0,
+};
+
+const numSeriesLabel = "Value";
+const timeSeriesLabel = "Time";
+
+const xSeriesOpts = {
+ show: true,
+ scale: "x",
+ auto: false,
+ sorted: 1,
+// label: "Time",
+// value: v => stamp(new Date(v * 1e3)),
+
+ // internal caches
+ min: inf,
+ max: -inf,
+ idxs: [],
+};
+
+function numAxisVals(self, splits, axisIdx, foundSpace, foundIncr) {
+ return splits.map(v => v == null ? "" : fmtNum(v));
+}
+
+function numAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {
+ let splits = [];
+
+ let numDec = fixedDec.get(foundIncr) || 0;
+
+ scaleMin = forceMin ? scaleMin : roundDec(incrRoundUp(scaleMin, foundIncr), numDec);
+
+ for (let val = scaleMin; val <= scaleMax; val = roundDec(val + foundIncr, numDec))
+ splits.push(Object.is(val, -0) ? 0 : val); // coalesces -0
+
+ return splits;
+}
+
+// this doesnt work for sin, which needs to come off from 0 independently in pos and neg dirs
+function logAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {
+ const splits = [];
+
+ const logBase = self.scales[self.axes[axisIdx].scale].log;
+
+ const logFn = logBase == 10 ? log10 : log2;
+
+ const exp = floor(logFn(scaleMin));
+
+ foundIncr = pow(logBase, exp);
+
+ if (exp < 0)
+ foundIncr = roundDec(foundIncr, -exp);
+
+ let split = scaleMin;
+
+ do {
+ splits.push(split);
+ split = roundDec(split + foundIncr, fixedDec.get(foundIncr));
+
+ if (split >= foundIncr * logBase)
+ foundIncr = split;
+
+ } while (split <= scaleMax);
+
+ return splits;
+}
+
+function asinhAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {
+ let sc = self.scales[self.axes[axisIdx].scale];
+
+ let linthresh = sc.asinh;
+
+ let posSplits = scaleMax > linthresh ? logAxisSplits(self, axisIdx, max(linthresh, scaleMin), scaleMax, foundIncr) : [linthresh];
+ let zero = scaleMax >= 0 && scaleMin <= 0 ? [0] : [];
+ let negSplits = scaleMin < -linthresh ? logAxisSplits(self, axisIdx, max(linthresh, -scaleMax), -scaleMin, foundIncr): [linthresh];
+
+ return negSplits.reverse().map(v => -v).concat(zero, posSplits);
+}
+
+const RE_ALL = /./;
+const RE_12357 = /[12357]/;
+const RE_125 = /[125]/;
+const RE_1 = /1/;
+
+function logAxisValsFilt(self, splits, axisIdx, foundSpace, foundIncr) {
+ let axis = self.axes[axisIdx];
+ let scaleKey = axis.scale;
+ let sc = self.scales[scaleKey];
+
+ if (sc.distr == 3 && sc.log == 2)
+ return splits;
+
+ let valToPos = self.valToPos;
+
+ let minSpace = axis._space;
+
+ let _10 = valToPos(10, scaleKey);
+
+ let re = (
+ valToPos(9, scaleKey) - _10 >= minSpace ? RE_ALL :
+ valToPos(7, scaleKey) - _10 >= minSpace ? RE_12357 :
+ valToPos(5, scaleKey) - _10 >= minSpace ? RE_125 :
+ RE_1
+ );
+
+ return splits.map(v => ((sc.distr == 4 && v == 0) || re.test(v)) ? v : null);
+}
+
+function numSeriesVal(self, val) {
+ return val == null ? "" : fmtNum(val);
+}
+
+const yAxisOpts = {
+ show: true,
+ scale: "y",
+ stroke: hexBlack,
+ space: 30,
+ gap: 5,
+ size: 50,
+ labelGap: 0,
+ labelSize: 30,
+ labelFont,
+ side: 3,
+// class: "y-vals",
+// incrs: numIncrs,
+// values: (vals, space) => vals,
+// filter: retArg1,
+ grid,
+ ticks,
+ font,
+ rotate: 0,
+};
+
+// takes stroke width
+function ptDia(width, mult) {
+ let dia = 3 + (width || 1) * 2;
+ return roundDec(dia * mult, 3);
+}
+
+function seriesPointsShow(self, si) {
+ let { scale, idxs } = self.series[0];
+ let xData = self._data[0];
+ let p0 = self.valToPos(xData[idxs[0]], scale, true);
+ let p1 = self.valToPos(xData[idxs[1]], scale, true);
+ let dim = abs(p1 - p0);
+
+ let s = self.series[si];
+// const dia = ptDia(s.width, pxRatio);
+ let maxPts = dim / (s.points.space * pxRatio);
+ return idxs[1] - idxs[0] <= maxPts;
+}
+
+function seriesFillTo(self, seriesIdx, dataMin, dataMax) {
+ let scale = self.scales[self.series[seriesIdx].scale];
+ let isUpperBandEdge = self.bands && self.bands.some(b => b.series[0] == seriesIdx);
+ return scale.distr == 3 || isUpperBandEdge ? scale.min : 0;
+}
+
+const facet = {
+ scale: null,
+ auto: true,
+
+ // internal caches
+ min: inf,
+ max: -inf,
+};
+
+const xySeriesOpts = {
+ show: true,
+ auto: true,
+ sorted: 0,
+ alpha: 1,
+ facets: [
+ assign({}, facet, {scale: 'x'}),
+ assign({}, facet, {scale: 'y'}),
+ ],
+};
+
+const ySeriesOpts = {
+ scale: "y",
+ auto: true,
+ sorted: 0,
+ show: true,
+ spanGaps: false,
+ gaps: (self, seriesIdx, idx0, idx1, nullGaps) => nullGaps,
+ alpha: 1,
+ points: {
+ show: seriesPointsShow,
+ filter: null,
+ // paths:
+ // stroke: "#000",
+ // fill: "#fff",
+ // width: 1,
+ // size: 10,
+ },
+// label: "Value",
+// value: v => v,
+ values: null,
+
+ // internal caches
+ min: inf,
+ max: -inf,
+ idxs: [],
+
+ path: null,
+ clip: null,
+};
+
+function clampScale(self, val, scaleMin, scaleMax, scaleKey) {
+/*
+ if (val < 0) {
+ let cssHgt = self.bbox.height / pxRatio;
+ let absPos = self.valToPos(abs(val), scaleKey);
+ let fromBtm = cssHgt - absPos;
+ return self.posToVal(cssHgt + fromBtm, scaleKey);
+ }
+*/
+ return scaleMin / 10;
+}
+
+const xScaleOpts = {
+ time: FEAT_TIME,
+ auto: true,
+ distr: 1,
+ log: 10,
+ asinh: 1,
+ min: null,
+ max: null,
+ dir: 1,
+ ori: 0,
+};
+
+const yScaleOpts = assign({}, xScaleOpts, {
+ time: false,
+ ori: 1,
+});
+
+const syncs = {};
+
+function _sync(key, opts) {
+ let s = syncs[key];
+
+ if (!s) {
+ s = {
+ key,
+ plots: [],
+ sub(plot) {
+ s.plots.push(plot);
+ },
+ unsub(plot) {
+ s.plots = s.plots.filter(c => c != plot);
+ },
+ pub(type, self, x, y, w, h, i) {
+ for (let j = 0; j < s.plots.length; j++)
+ s.plots[j] != self && s.plots[j].pub(type, self, x, y, w, h, i);
+ },
+ };
+
+ if (key != null)
+ syncs[key] = s;
+ }
+
+ return s;
+}
+
+const BAND_CLIP_FILL = 1 << 0;
+const BAND_CLIP_STROKE = 1 << 1;
+
+function orient(u, seriesIdx, cb) {
+ const series = u.series[seriesIdx];
+ const scales = u.scales;
+ const bbox = u.bbox;
+ const scaleX = u.mode == 2 ? scales[series.facets[0].scale] : scales[u.series[0].scale];
+
+ let dx = u._data[0],
+ dy = u._data[seriesIdx],
+ sx = scaleX,
+ sy = u.mode == 2 ? scales[series.facets[1].scale] : scales[series.scale],
+ l = bbox.left,
+ t = bbox.top,
+ w = bbox.width,
+ h = bbox.height,
+ H = u.valToPosH,
+ V = u.valToPosV;
+
+ return (sx.ori == 0
+ ? cb(
+ series,
+ dx,
+ dy,
+ sx,
+ sy,
+ H,
+ V,
+ l,
+ t,
+ w,
+ h,
+ moveToH,
+ lineToH,
+ rectH,
+ arcH,
+ bezierCurveToH,
+ )
+ : cb(
+ series,
+ dx,
+ dy,
+ sx,
+ sy,
+ V,
+ H,
+ t,
+ l,
+ h,
+ w,
+ moveToV,
+ lineToV,
+ rectV,
+ arcV,
+ bezierCurveToV,
+ )
+ );
+}
+
+// creates inverted band clip path (towards from stroke path -> yMax)
+function clipBandLine(self, seriesIdx, idx0, idx1, strokePath) {
+ return orient(self, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+ const lineTo = scaleX.ori == 0 ? lineToH : lineToV;
+
+ let frIdx, toIdx;
+
+ if (dir == 1) {
+ frIdx = idx0;
+ toIdx = idx1;
+ }
+ else {
+ frIdx = idx1;
+ toIdx = idx0;
+ }
+
+ // path start
+ let x0 = pxRound(valToPosX(dataX[frIdx], scaleX, xDim, xOff));
+ let y0 = pxRound(valToPosY(dataY[frIdx], scaleY, yDim, yOff));
+ // path end x
+ let x1 = pxRound(valToPosX(dataX[toIdx], scaleX, xDim, xOff));
+ // upper y limit
+ let yLimit = pxRound(valToPosY(scaleY.max, scaleY, yDim, yOff));
+
+ let clip = new Path2D(strokePath);
+
+ lineTo(clip, x1, yLimit);
+ lineTo(clip, x0, yLimit);
+ lineTo(clip, x0, y0);
+
+ return clip;
+ });
+}
+
+function clipGaps(gaps, ori, plotLft, plotTop, plotWid, plotHgt) {
+ let clip = null;
+
+ // create clip path (invert gaps and non-gaps)
+ if (gaps.length > 0) {
+ clip = new Path2D();
+
+ const rect = ori == 0 ? rectH : rectV;
+
+ let prevGapEnd = plotLft;
+
+ for (let i = 0; i < gaps.length; i++) {
+ let g = gaps[i];
+
+ if (g[1] > g[0]) {
+ let w = g[0] - prevGapEnd;
+
+ w > 0 && rect(clip, prevGapEnd, plotTop, w, plotTop + plotHgt);
+
+ prevGapEnd = g[1];
+ }
+ }
+
+ let w = plotLft + plotWid - prevGapEnd;
+
+ w > 0 && rect(clip, prevGapEnd, plotTop, w, plotTop + plotHgt);
+ }
+
+ return clip;
+}
+
+function addGap(gaps, fromX, toX) {
+ let prevGap = gaps[gaps.length - 1];
+
+ if (prevGap && prevGap[0] == fromX) // TODO: gaps must be encoded at stroke widths?
+ prevGap[1] = toX;
+ else
+ gaps.push([fromX, toX]);
+}
+
+function pxRoundGen(pxAlign) {
+ return pxAlign == 0 ? retArg0 : pxAlign == 1 ? round : v => incrRound(v, pxAlign);
+}
+
+function rect(ori) {
+ let moveTo = ori == 0 ?
+ moveToH :
+ moveToV;
+
+ let arcTo = ori == 0 ?
+ (p, x1, y1, x2, y2, r) => { p.arcTo(x1, y1, x2, y2, r); } :
+ (p, y1, x1, y2, x2, r) => { p.arcTo(x1, y1, x2, y2, r); };
+
+ let rect = ori == 0 ?
+ (p, x, y, w, h) => { p.rect(x, y, w, h); } :
+ (p, y, x, h, w) => { p.rect(x, y, w, h); };
+
+ return (p, x, y, w, h, r = 0) => {
+ if (r == 0)
+ rect(p, x, y, w, h);
+ else {
+ r = min(r, w / 2, h / 2);
+
+ // adapted from https://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-using-html-canvas/7838871#7838871
+ moveTo(p, x + r, y);
+ arcTo(p, x + w, y, x + w, y + h, r);
+ arcTo(p, x + w, y + h, x, y + h, r);
+ arcTo(p, x, y + h, x, y, r);
+ arcTo(p, x, y, x + w, y, r);
+ p.closePath();
+ }
+ };
+}
+
+// orientation-inverting canvas functions
+const moveToH = (p, x, y) => { p.moveTo(x, y); };
+const moveToV = (p, y, x) => { p.moveTo(x, y); };
+const lineToH = (p, x, y) => { p.lineTo(x, y); };
+const lineToV = (p, y, x) => { p.lineTo(x, y); };
+const rectH = rect(0);
+const rectV = rect(1);
+const arcH = (p, x, y, r, startAngle, endAngle) => { p.arc(x, y, r, startAngle, endAngle); };
+const arcV = (p, y, x, r, startAngle, endAngle) => { p.arc(x, y, r, startAngle, endAngle); };
+const bezierCurveToH = (p, bp1x, bp1y, bp2x, bp2y, p2x, p2y) => { p.bezierCurveTo(bp1x, bp1y, bp2x, bp2y, p2x, p2y); };
+const bezierCurveToV = (p, bp1y, bp1x, bp2y, bp2x, p2y, p2x) => { p.bezierCurveTo(bp1x, bp1y, bp2x, bp2y, p2x, p2y); };
+
+// TODO: drawWrap(seriesIdx, drawPoints) (save, restore, translate, clip)
+function points(opts) {
+ return (u, seriesIdx, idx0, idx1, filtIdxs) => {
+ // log("drawPoints()", arguments);
+
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let { pxRound, points } = series;
+
+ let moveTo, arc;
+
+ if (scaleX.ori == 0) {
+ moveTo = moveToH;
+ arc = arcH;
+ }
+ else {
+ moveTo = moveToV;
+ arc = arcV;
+ }
+
+ const width = roundDec(points.width * pxRatio, 3);
+
+ let rad = (points.size - points.width) / 2 * pxRatio;
+ let dia = roundDec(rad * 2, 3);
+
+ let fill = new Path2D();
+ let clip = new Path2D();
+
+ let { left: lft, top: top, width: wid, height: hgt } = u.bbox;
+
+ rectH(clip,
+ lft - dia,
+ top - dia,
+ wid + dia * 2,
+ hgt + dia * 2,
+ );
+
+ const drawPoint = pi => {
+ if (dataY[pi] != null) {
+ let x = pxRound(valToPosX(dataX[pi], scaleX, xDim, xOff));
+ let y = pxRound(valToPosY(dataY[pi], scaleY, yDim, yOff));
+
+ moveTo(fill, x + rad, y);
+ arc(fill, x, y, rad, 0, PI * 2);
+ }
+ };
+
+ if (filtIdxs)
+ filtIdxs.forEach(drawPoint);
+ else {
+ for (let pi = idx0; pi <= idx1; pi++)
+ drawPoint(pi);
+ }
+
+ return {
+ stroke: width > 0 ? fill : null,
+ fill,
+ clip,
+ flags: BAND_CLIP_FILL | BAND_CLIP_STROKE,
+ };
+ });
+ };
+}
+
+function _drawAcc(lineTo) {
+ return (stroke, accX, minY, maxY, inY, outY) => {
+ if (minY != maxY) {
+ if (inY != minY && outY != minY)
+ lineTo(stroke, accX, minY);
+ if (inY != maxY && outY != maxY)
+ lineTo(stroke, accX, maxY);
+
+ lineTo(stroke, accX, outY);
+ }
+ };
+}
+
+const drawAccH = _drawAcc(lineToH);
+const drawAccV = _drawAcc(lineToV);
+
+function linear() {
+ return (u, seriesIdx, idx0, idx1) => {
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ let lineTo, drawAcc;
+
+ if (scaleX.ori == 0) {
+ lineTo = lineToH;
+ drawAcc = drawAccH;
+ }
+ else {
+ lineTo = lineToV;
+ drawAcc = drawAccV;
+ }
+
+ const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+
+ const _paths = {stroke: new Path2D(), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};
+ const stroke = _paths.stroke;
+
+ let minY = inf,
+ maxY = -inf,
+ inY, outY, outX, drawnAtX;
+
+ let gaps = [];
+
+ let accX = pxRound(valToPosX(dataX[dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));
+ let accGaps = false;
+ let prevYNull = false;
+
+ // data edges
+ let lftIdx = nonNullIdx(dataY, idx0, idx1, 1 * dir);
+ let rgtIdx = nonNullIdx(dataY, idx0, idx1, -1 * dir);
+ let lftX = pxRound(valToPosX(dataX[lftIdx], scaleX, xDim, xOff));
+ let rgtX = pxRound(valToPosX(dataX[rgtIdx], scaleX, xDim, xOff));
+
+ if (lftX > xOff)
+ addGap(gaps, xOff, lftX);
+
+ for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) {
+ let x = pxRound(valToPosX(dataX[i], scaleX, xDim, xOff));
+
+ if (x == accX) {
+ if (dataY[i] != null) {
+ outY = pxRound(valToPosY(dataY[i], scaleY, yDim, yOff));
+
+ if (minY == inf) {
+ lineTo(stroke, x, outY);
+ inY = outY;
+ }
+
+ minY = min(outY, minY);
+ maxY = max(outY, maxY);
+ }
+ else if (dataY[i] === null)
+ accGaps = prevYNull = true;
+ }
+ else {
+ let _addGap = false;
+
+ if (minY != inf) {
+ drawAcc(stroke, accX, minY, maxY, inY, outY);
+ outX = drawnAtX = accX;
+ }
+ else if (accGaps) {
+ _addGap = true;
+ accGaps = false;
+ }
+
+ if (dataY[i] != null) {
+ outY = pxRound(valToPosY(dataY[i], scaleY, yDim, yOff));
+ lineTo(stroke, x, outY);
+ minY = maxY = inY = outY;
+
+ // prior pixel can have data but still start a gap if ends with null
+ if (prevYNull && x - accX > 1)
+ _addGap = true;
+
+ prevYNull = false;
+ }
+ else {
+ minY = inf;
+ maxY = -inf;
+
+ if (dataY[i] === null) {
+ accGaps = true;
+
+ if (x - accX > 1)
+ _addGap = true;
+ }
+ }
+
+ _addGap && addGap(gaps, outX, x);
+
+ accX = x;
+ }
+ }
+
+ if (minY != inf && minY != maxY && drawnAtX != accX)
+ drawAcc(stroke, accX, minY, maxY, inY, outY);
+
+ if (rgtX < xOff + xDim)
+ addGap(gaps, rgtX, xOff + xDim);
+
+ if (series.fill != null) {
+ let fill = _paths.fill = new Path2D(stroke);
+
+ let fillTo = pxRound(valToPosY(series.fillTo(u, seriesIdx, series.min, series.max), scaleY, yDim, yOff));
+
+ lineTo(fill, rgtX, fillTo);
+ lineTo(fill, lftX, fillTo);
+ }
+
+ _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
+
+ if (!series.spanGaps)
+ _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
+
+ if (u.bands.length > 0) {
+ // ADDL OPT: only create band clips for series that are band lower edges
+ // if (b.series[1] == i && _paths.band == null)
+ _paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);
+ }
+
+ return _paths;
+ });
+ };
+}
+
+function stepped(opts) {
+ const align = ifNull(opts.align, 1);
+ // whether to draw ascenders/descenders at null/gap bondaries
+ const ascDesc = ifNull(opts.ascDesc, false);
+
+ return (u, seriesIdx, idx0, idx1) => {
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ let lineTo = scaleX.ori == 0 ? lineToH : lineToV;
+
+ const _paths = {stroke: new Path2D(), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};
+ const stroke = _paths.stroke;
+
+ const _dir = 1 * scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+
+ idx0 = nonNullIdx(dataY, idx0, idx1, 1);
+ idx1 = nonNullIdx(dataY, idx0, idx1, -1);
+
+ let gaps = [];
+ let inGap = false;
+ let prevYPos = pxRound(valToPosY(dataY[_dir == 1 ? idx0 : idx1], scaleY, yDim, yOff));
+ let firstXPos = pxRound(valToPosX(dataX[_dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));
+ let prevXPos = firstXPos;
+
+ lineTo(stroke, firstXPos, prevYPos);
+
+ for (let i = _dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dir) {
+ let yVal1 = dataY[i];
+
+ let x1 = pxRound(valToPosX(dataX[i], scaleX, xDim, xOff));
+
+ if (yVal1 == null) {
+ if (yVal1 === null) {
+ addGap(gaps, prevXPos, x1);
+ inGap = true;
+ }
+ continue;
+ }
+
+ let y1 = pxRound(valToPosY(yVal1, scaleY, yDim, yOff));
+
+ if (inGap) {
+ addGap(gaps, prevXPos, x1);
+ inGap = false;
+ }
+
+ if (align == 1)
+ lineTo(stroke, x1, prevYPos);
+ else
+ lineTo(stroke, prevXPos, y1);
+
+ lineTo(stroke, x1, y1);
+
+ prevYPos = y1;
+ prevXPos = x1;
+ }
+
+ if (series.fill != null) {
+ let fill = _paths.fill = new Path2D(stroke);
+
+ let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
+ let minY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff));
+
+ lineTo(fill, prevXPos, minY);
+ lineTo(fill, firstXPos, minY);
+ }
+
+ _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
+
+ // expand/contract clips for ascenders/descenders
+ let halfStroke = (series.width * pxRatio) / 2;
+ let startsOffset = (ascDesc || align == 1) ? halfStroke : -halfStroke;
+ let endsOffset = (ascDesc || align == -1) ? -halfStroke : halfStroke;
+
+ gaps.forEach(g => {
+ g[0] += startsOffset;
+ g[1] += endsOffset;
+ });
+
+ if (!series.spanGaps)
+ _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
+
+ if (u.bands.length > 0) {
+ // ADDL OPT: only create band clips for series that are band lower edges
+ // if (b.series[1] == i && _paths.band == null)
+ _paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);
+ }
+
+ return _paths;
+ });
+ };
+}
+
+function bars(opts) {
+ opts = opts || EMPTY_OBJ;
+ const size = ifNull(opts.size, [0.6, inf, 1]);
+ const align = opts.align || 0;
+ const extraGap = (opts.gap || 0) * pxRatio;
+
+ const radius = ifNull(opts.radius, 0);
+
+ const gapFactor = 1 - size[0];
+ const maxWidth = ifNull(size[1], inf) * pxRatio;
+ const minWidth = ifNull(size[2], 1) * pxRatio;
+
+ const disp = ifNull(opts.disp, EMPTY_OBJ);
+ const _each = ifNull(opts.each, _ => {});
+
+ const { fill: dispFills, stroke: dispStrokes } = disp;
+
+ return (u, seriesIdx, idx0, idx1) => {
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ const _dirX = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+ const _dirY = scaleY.dir * (scaleY.ori == 1 ? 1 : -1);
+
+ let rect = scaleX.ori == 0 ? rectH : rectV;
+
+ let each = scaleX.ori == 0 ? _each : (u, seriesIdx, i, top, lft, hgt, wid) => {
+ _each(u, seriesIdx, i, lft, top, wid, hgt);
+ };
+
+ let fillToY = series.fillTo(u, seriesIdx, series.min, series.max);
+
+ let y0Pos = valToPosY(fillToY, scaleY, yDim, yOff);
+
+ // barWid is to center of stroke
+ let xShift, barWid;
+
+ let strokeWidth = pxRound(series.width * pxRatio);
+
+ let multiPath = false;
+
+ let fillColors = null;
+ let fillPaths = null;
+ let strokeColors = null;
+ let strokePaths = null;
+
+ if (dispFills != null && dispStrokes != null) {
+ multiPath = true;
+
+ fillColors = dispFills.values(u, seriesIdx, idx0, idx1);
+ fillPaths = new Map();
+ (new Set(fillColors)).forEach(color => {
+ if (color != null)
+ fillPaths.set(color, new Path2D());
+ });
+
+ strokeColors = dispStrokes.values(u, seriesIdx, idx0, idx1);
+ strokePaths = new Map();
+ (new Set(strokeColors)).forEach(color => {
+ if (color != null)
+ strokePaths.set(color, new Path2D());
+ });
+ }
+
+ let { x0, size } = disp;
+
+ if (x0 != null && size != null) {
+ dataX = x0.values(u, seriesIdx, idx0, idx1);
+
+ if (x0.unit == 2)
+ dataX = dataX.map(pct => u.posToVal(xOff + pct * xDim, scaleX.key, true));
+
+ // assumes uniform sizes, for now
+ let sizes = size.values(u, seriesIdx, idx0, idx1);
+
+ if (size.unit == 2)
+ barWid = sizes[0] * xDim;
+ else
+ barWid = valToPosX(sizes[0], scaleX, xDim, xOff) - valToPosX(0, scaleX, xDim, xOff); // assumes linear scale (delta from 0)
+
+ barWid = pxRound(barWid - strokeWidth);
+
+ xShift = (_dirX == 1 ? -strokeWidth / 2 : barWid + strokeWidth / 2);
+ }
+ else {
+ let colWid = xDim;
+
+ if (dataX.length > 1) {
+ // prior index with non-undefined y data
+ let prevIdx = null;
+
+ // scan full dataset for smallest adjacent delta
+ // will not work properly for non-linear x scales, since does not do expensive valToPosX calcs till end
+ for (let i = 0, minDelta = Infinity; i < dataX.length; i++) {
+ if (dataY[i] !== undefined) {
+ if (prevIdx != null) {
+ let delta = abs(dataX[i] - dataX[prevIdx]);
+
+ if (delta < minDelta) {
+ minDelta = delta;
+ colWid = abs(valToPosX(dataX[i], scaleX, xDim, xOff) - valToPosX(dataX[prevIdx], scaleX, xDim, xOff));
+ }
+ }
+
+ prevIdx = i;
+ }
+ }
+ }
+
+ let gapWid = colWid * gapFactor;
+
+ barWid = pxRound(min(maxWidth, max(minWidth, colWid - gapWid)) - strokeWidth - extraGap);
+
+ xShift = (align == 0 ? barWid / 2 : align == _dirX ? 0 : barWid) - align * _dirX * extraGap / 2;
+ }
+
+ const _paths = {stroke: null, fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL | BAND_CLIP_STROKE}; // disp, geom
+
+ const hasBands = u.bands.length > 0;
+ let yLimit;
+
+ if (hasBands) {
+ // ADDL OPT: only create band clips for series that are band lower edges
+ // if (b.series[1] == i && _paths.band == null)
+ _paths.band = new Path2D();
+ yLimit = pxRound(valToPosY(scaleY.max, scaleY, yDim, yOff));
+ }
+
+ const stroke = multiPath ? null : new Path2D();
+ const band = _paths.band;
+
+ for (let i = _dirX == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dirX) {
+ let yVal = dataY[i];
+
+ /*
+ // interpolate upwards band clips
+ if (yVal == null) {
+ // if (hasBands)
+ // yVal = costlyLerp(i, idx0, idx1, _dirX, dataY);
+ // else
+ continue;
+ }
+ */
+
+ let xVal = scaleX.distr != 2 || disp != null ? dataX[i] : i;
+
+ // TODO: all xPos can be pre-computed once for all series in aligned set
+ let xPos = valToPosX(xVal, scaleX, xDim, xOff);
+ let yPos = valToPosY(ifNull(yVal, fillToY) , scaleY, yDim, yOff);
+
+ let lft = pxRound(xPos - xShift);
+ let btm = pxRound(max(yPos, y0Pos));
+ let top = pxRound(min(yPos, y0Pos));
+ // this includes the stroke
+ let barHgt = btm - top;
+
+ let r = radius * barWid;
+
+ if (yVal != null) { // && yVal != fillToY (0 height bar)
+ if (multiPath) {
+ if (strokeWidth > 0 && strokeColors[i] != null)
+ rect(strokePaths.get(strokeColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);
+
+ if (fillColors[i] != null)
+ rect(fillPaths.get(fillColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);
+ }
+ else
+ rect(stroke, lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);
+
+ each(u, seriesIdx, i,
+ lft - strokeWidth / 2,
+ top,
+ barWid + strokeWidth,
+ barHgt,
+ );
+ }
+
+ if (hasBands) {
+ if (_dirY == 1) {
+ btm = top;
+ top = yLimit;
+ }
+ else {
+ top = btm;
+ btm = yLimit;
+ }
+
+ barHgt = btm - top;
+
+ rect(band, lft - strokeWidth / 2, top, barWid + strokeWidth, max(0, barHgt), 0);
+ }
+ }
+
+ if (strokeWidth > 0)
+ _paths.stroke = multiPath ? strokePaths : stroke;
+
+ _paths.fill = multiPath ? fillPaths : stroke;
+
+ return _paths;
+ });
+ };
+}
+
+function splineInterp(interp, opts) {
+ return (u, seriesIdx, idx0, idx1) => {
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ let moveTo, bezierCurveTo, lineTo;
+
+ if (scaleX.ori == 0) {
+ moveTo = moveToH;
+ lineTo = lineToH;
+ bezierCurveTo = bezierCurveToH;
+ }
+ else {
+ moveTo = moveToV;
+ lineTo = lineToV;
+ bezierCurveTo = bezierCurveToV;
+ }
+
+ const _dir = 1 * scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+
+ idx0 = nonNullIdx(dataY, idx0, idx1, 1);
+ idx1 = nonNullIdx(dataY, idx0, idx1, -1);
+
+ let gaps = [];
+ let inGap = false;
+ let firstXPos = pxRound(valToPosX(dataX[_dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));
+ let prevXPos = firstXPos;
+
+ let xCoords = [];
+ let yCoords = [];
+
+ for (let i = _dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dir) {
+ let yVal = dataY[i];
+ let xVal = dataX[i];
+ let xPos = valToPosX(xVal, scaleX, xDim, xOff);
+
+ if (yVal == null) {
+ if (yVal === null) {
+ addGap(gaps, prevXPos, xPos);
+ inGap = true;
+ }
+ continue;
+ }
+ else {
+ if (inGap) {
+ addGap(gaps, prevXPos, xPos);
+ inGap = false;
+ }
+
+ xCoords.push((prevXPos = xPos));
+ yCoords.push(valToPosY(dataY[i], scaleY, yDim, yOff));
+ }
+ }
+
+ const _paths = {stroke: interp(xCoords, yCoords, moveTo, lineTo, bezierCurveTo, pxRound), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};
+ const stroke = _paths.stroke;
+
+ if (series.fill != null && stroke != null) {
+ let fill = _paths.fill = new Path2D(stroke);
+
+ let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
+ let minY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff));
+
+ lineTo(fill, prevXPos, minY);
+ lineTo(fill, firstXPos, minY);
+ }
+
+ _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
+
+ if (!series.spanGaps)
+ _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
+
+ if (u.bands.length > 0) {
+ // ADDL OPT: only create band clips for series that are band lower edges
+ // if (b.series[1] == i && _paths.band == null)
+ _paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);
+ }
+
+ return _paths;
+
+ // if FEAT_PATHS: false in rollup.config.js
+ // u.ctx.save();
+ // u.ctx.beginPath();
+ // u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
+ // u.ctx.clip();
+ // u.ctx.strokeStyle = u.series[sidx].stroke;
+ // u.ctx.stroke(stroke);
+ // u.ctx.fillStyle = u.series[sidx].fill;
+ // u.ctx.fill(fill);
+ // u.ctx.restore();
+ // return null;
+ });
+ };
+}
+
+function monotoneCubic(opts) {
+ return splineInterp(_monotoneCubic);
+}
+
+// Monotone Cubic Spline interpolation, adapted from the Chartist.js implementation:
+// https://github.com/gionkunz/chartist-js/blob/e7e78201bffe9609915e5e53cfafa29a5d6c49f9/src/scripts/interpolation.js#L240-L369
+function _monotoneCubic(xs, ys, moveTo, lineTo, bezierCurveTo, pxRound) {
+ const n = xs.length;
+
+ if (n < 2)
+ return null;
+
+ const path = new Path2D();
+
+ moveTo(path, xs[0], ys[0]);
+
+ if (n == 2)
+ lineTo(path, xs[1], ys[1]);
+ else {
+ let ms = Array(n),
+ ds = Array(n - 1),
+ dys = Array(n - 1),
+ dxs = Array(n - 1);
+
+ // calc deltas and derivative
+ for (let i = 0; i < n - 1; i++) {
+ dys[i] = ys[i + 1] - ys[i];
+ dxs[i] = xs[i + 1] - xs[i];
+ ds[i] = dys[i] / dxs[i];
+ }
+
+ // determine desired slope (m) at each point using Fritsch-Carlson method
+ // http://math.stackexchange.com/questions/45218/implementation-of-monotone-cubic-interpolation
+ ms[0] = ds[0];
+
+ for (let i = 1; i < n - 1; i++) {
+ if (ds[i] === 0 || ds[i - 1] === 0 || (ds[i - 1] > 0) !== (ds[i] > 0))
+ ms[i] = 0;
+ else {
+ ms[i] = 3 * (dxs[i - 1] + dxs[i]) / (
+ (2 * dxs[i] + dxs[i - 1]) / ds[i - 1] +
+ (dxs[i] + 2 * dxs[i - 1]) / ds[i]
+ );
+
+ if (!isFinite(ms[i]))
+ ms[i] = 0;
+ }
+ }
+
+ ms[n - 1] = ds[n - 2];
+
+ for (let i = 0; i < n - 1; i++) {
+ bezierCurveTo(
+ path,
+ xs[i] + dxs[i] / 3,
+ ys[i] + ms[i] * dxs[i] / 3,
+ xs[i + 1] - dxs[i] / 3,
+ ys[i + 1] - ms[i + 1] * dxs[i] / 3,
+ xs[i + 1],
+ ys[i + 1],
+ );
+ }
+ }
+
+ return path;
+}
+
+const cursorPlots = new Set();
+
+function invalidateRects() {
+ cursorPlots.forEach(u => {
+ u.syncRect(true);
+ });
+}
+
+on(resize, win, invalidateRects);
+on(scroll, win, invalidateRects, true);
+
+const linearPath = linear() ;
+const pointsPath = points() ;
+
+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;
+ });
+ }
+}
+
+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 = 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;
+
+ if (scaleX.ori == 0) {
+ addClass(root, ORI_HZ);
+ valToPosX = getHPos;
+ valToPosY = getVPos;
+ /*
+ 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;
+ /*
+ 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 = (opts.tzDate || (ts => new Date(round(ts / ms))));
+ const _fmtDate = (opts.fmtDate || fmtDate);
+
+ const _timeAxisSplits = (ms == 1 ? timeAxisSplitsMs(_tzDate) : timeAxisSplitsS(_tzDate));
+ const _timeAxisVals = timeAxisVals(_tzDate, timeAxisStamps((ms == 1 ? _timeAxisStampsMs : _timeAxisStampsS), _fmtDate));
+ const _timeSeriesVal = timeSeriesVal(_tzDate, timeSeriesStamp(_timeSeriesStamp, _fmtDate));
+
+ const activeIdxs = [];
+
+ const legend = (self.legend = assign({}, legendOpts, opts.legend));
+ const showLegend = legend.show;
+ const markers = legend.markers;
+
+ {
+ 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 (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 (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, syncOpts.setSeries);
+ });
+ }
+ else
+ setSeries(seriesIdx, {show: !s.show}, true, 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 = (self.cursor = assign({}, cursorOpts, {drag: {y: mode == 2}}, opts.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}, cursor.focus);
+ const cursorFocus = 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 = 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 (cursor.show) {
+ activeIdxs.splice(i, 0, null);
+
+ let pt = initCursorPt(s, i);
+ pt && cursorPts.splice(i, 0, pt);
+ }
+ }
+
+ 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 (cursor.show) {
+ activeIdxs.splice(i, 1);
+
+ cursorPts.length > 1 && cursorPts.splice(i, 1)[0].remove();
+ }
+
+ // TODO: de-init no-longer-needed scales?
+ }
+
+ 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 = 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 (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;
+
+ {
+ cacheStrokeFill(i, false);
+ s._paths && drawPath(i, false);
+ }
+
+ {
+ 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(a => {
+ let { _show, _el, _size, _pos, side } = a;
+
+ 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);
+
+ _el && remClass(_el, OFF);
+ }
+ else
+ _el && 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 (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 = cursor.drag;
+
+ let dragX = drag.x;
+ let dragY = drag.y;
+
+ if (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);
+ 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;
+ toggleDOM(i, opts.show);
+
+ _setScale(mode == 2 ? s.facets[1].scale : s.scale, null, null);
+ commit();
+ }
+
+ _fire !== false && fire("setSeries", i, opts);
+
+ _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 (cursor.show && cursorPts[i])
+ cursorPts[i].style.opacity = value;
+
+ if (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;
+
+ (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) {
+ cursorPts.length > 1 && elTrans(cursorPts[i], -10, -10, plotWidCss, plotHgtCss);
+ }
+ }
+
+ if (cursorFocus)
+ setSeries(null, FOCUS_TRUE, true, src == null && syncOpts.setSeries);
+
+ if (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 (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 (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);
+ 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 (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 = 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);
+
+ (cursor.sync = syncOpts);
+
+ const syncKey = syncOpts.key;
+
+ const sync = _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);
+ }
+
+ 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);
+ }
+
+ (self.pub = pub);
+
+ function destroy() {
+ sync.unsub(self);
+ 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;
+
+{
+ uPlot.join = join;
+}
+
+{
+ uPlot.fmtDate = fmtDate;
+ uPlot.tzDate = tzDate;
+}
+
+{
+ uPlot.sync = _sync;
+}
+
+{
+ uPlot.addGap = addGap;
+ uPlot.clipGaps = clipGaps;
+
+ let paths = uPlot.paths = {
+ points,
+ };
+
+ (paths.linear = linear);
+ (paths.stepped = stepped);
+ (paths.bars = bars);
+ (paths.spline = monotoneCubic);
+}
+
+export { uPlot as default };
diff --git a/build/resources/main/static/plugins/uplot/uPlot.iife.js b/build/resources/main/static/plugins/uplot/uPlot.iife.js
new file mode 100644
index 0000000..e0e776c
--- /dev/null
+++ b/build/resources/main/static/plugins/uplot/uPlot.iife.js
@@ -0,0 +1,5215 @@
+/**
+* Copyright (c) 2021, Leon Sorokin
+* All rights reserved. (MIT Licensed)
+*
+* uPlot.js (μPlot)
+* A small, fast chart for time series, lines, areas, ohlc & bars
+* https://github.com/leeoniya/uPlot (v1.6.18)
+*/
+
+var uPlot = (function () {
+ 'use strict';
+
+ const FEAT_TIME = true;
+
+ // binary search for index of closest value
+ function closestIdx(num, arr, lo, hi) {
+ let mid;
+ lo = lo || 0;
+ hi = hi || arr.length - 1;
+ let bitwise = hi <= 2147483647;
+
+ while (hi - lo > 1) {
+ mid = bitwise ? (lo + hi) >> 1 : floor((lo + hi) / 2);
+
+ if (arr[mid] < num)
+ lo = mid;
+ else
+ hi = mid;
+ }
+
+ if (num - arr[lo] <= arr[hi] - num)
+ return lo;
+
+ return hi;
+ }
+
+ function nonNullIdx(data, _i0, _i1, dir) {
+ for (let i = dir == 1 ? _i0 : _i1; i >= _i0 && i <= _i1; i += dir) {
+ if (data[i] != null)
+ return i;
+ }
+
+ return -1;
+ }
+
+ function getMinMax(data, _i0, _i1, sorted) {
+ // console.log("getMinMax()");
+
+ let _min = inf;
+ let _max = -inf;
+
+ if (sorted == 1) {
+ _min = data[_i0];
+ _max = data[_i1];
+ }
+ else if (sorted == -1) {
+ _min = data[_i1];
+ _max = data[_i0];
+ }
+ else {
+ for (let i = _i0; i <= _i1; i++) {
+ if (data[i] != null) {
+ _min = min(_min, data[i]);
+ _max = max(_max, data[i]);
+ }
+ }
+ }
+
+ return [_min, _max];
+ }
+
+ function getMinMaxLog(data, _i0, _i1) {
+ // console.log("getMinMax()");
+
+ let _min = inf;
+ let _max = -inf;
+
+ for (let i = _i0; i <= _i1; i++) {
+ if (data[i] > 0) {
+ _min = min(_min, data[i]);
+ _max = max(_max, data[i]);
+ }
+ }
+
+ return [
+ _min == inf ? 1 : _min,
+ _max == -inf ? 10 : _max,
+ ];
+ }
+
+ const _fixedTuple = [0, 0];
+
+ function fixIncr(minIncr, maxIncr, minExp, maxExp) {
+ _fixedTuple[0] = minExp < 0 ? roundDec(minIncr, -minExp) : minIncr;
+ _fixedTuple[1] = maxExp < 0 ? roundDec(maxIncr, -maxExp) : maxIncr;
+ return _fixedTuple;
+ }
+
+ function rangeLog(min, max, base, fullMags) {
+ let minSign = sign(min);
+
+ let logFn = base == 10 ? log10 : log2;
+
+ if (min == max) {
+ if (minSign == -1) {
+ min *= base;
+ max /= base;
+ }
+ else {
+ min /= base;
+ max *= base;
+ }
+ }
+
+ let minExp, maxExp, minMaxIncrs;
+
+ if (fullMags) {
+ minExp = floor(logFn(min));
+ maxExp = ceil(logFn(max));
+
+ minMaxIncrs = fixIncr(pow(base, minExp), pow(base, maxExp), minExp, maxExp);
+
+ min = minMaxIncrs[0];
+ max = minMaxIncrs[1];
+ }
+ else {
+ minExp = floor(logFn(abs(min)));
+ maxExp = floor(logFn(abs(max)));
+
+ minMaxIncrs = fixIncr(pow(base, minExp), pow(base, maxExp), minExp, maxExp);
+
+ min = incrRoundDn(min, minMaxIncrs[0]);
+ max = incrRoundUp(max, minMaxIncrs[1]);
+ }
+
+ return [min, max];
+ }
+
+ function rangeAsinh(min, max, base, fullMags) {
+ let minMax = rangeLog(min, max, base, fullMags);
+
+ if (min == 0)
+ minMax[0] = 0;
+
+ if (max == 0)
+ minMax[1] = 0;
+
+ return minMax;
+ }
+
+ const rangePad = 0.1;
+
+ const autoRangePart = {
+ mode: 3,
+ pad: rangePad,
+ };
+
+ const _eqRangePart = {
+ pad: 0,
+ soft: null,
+ mode: 0,
+ };
+
+ const _eqRange = {
+ min: _eqRangePart,
+ max: _eqRangePart,
+ };
+
+ // 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 rangeNum(_min, _max, mult, extra) {
+ if (isObj(mult))
+ return _rangeNum(_min, _max, mult);
+
+ _eqRangePart.pad = mult;
+ _eqRangePart.soft = extra ? 0 : null;
+ _eqRangePart.mode = extra ? 3 : 0;
+
+ return _rangeNum(_min, _max, _eqRange);
+ }
+
+ // nullish coalesce
+ function ifNull(lh, rh) {
+ return lh == null ? rh : lh;
+ }
+
+ // checks if given index range in an array contains a non-null value
+ // aka a range-bounded Array.some()
+ function hasData(data, idx0, idx1) {
+ idx0 = ifNull(idx0, 0);
+ idx1 = ifNull(idx1, data.length - 1);
+
+ while (idx0 <= idx1) {
+ if (data[idx0] != null)
+ return true;
+ idx0++;
+ }
+
+ return false;
+ }
+
+ function _rangeNum(_min, _max, cfg) {
+ let cmin = cfg.min;
+ let cmax = cfg.max;
+
+ let padMin = ifNull(cmin.pad, 0);
+ let padMax = ifNull(cmax.pad, 0);
+
+ let hardMin = ifNull(cmin.hard, -inf);
+ let hardMax = ifNull(cmax.hard, inf);
+
+ let softMin = ifNull(cmin.soft, inf);
+ let softMax = ifNull(cmax.soft, -inf);
+
+ let softMinMode = ifNull(cmin.mode, 0);
+ let softMaxMode = ifNull(cmax.mode, 0);
+
+ let delta = _max - _min;
+
+ // this handles situations like 89.7, 89.69999999999999
+ // by assuming 0.001x deltas are precision errors
+ // if (delta > 0 && delta < abs(_max) / 1e3)
+ // delta = 0;
+
+ // treat data as flat if delta is less than 1 billionth
+ if (delta < 1e-9) {
+ delta = 0;
+
+ // if soft mode is 2 and all vals are flat at 0, avoid the 0.1 * 1e3 fallback
+ // this prevents 0,0,0 from ranging to -100,100 when softMin/softMax are -1,1
+ if (_min == 0 || _max == 0) {
+ delta = 1e-9;
+
+ if (softMinMode == 2 && softMin != inf)
+ padMin = 0;
+
+ if (softMaxMode == 2 && softMax != -inf)
+ padMax = 0;
+ }
+ }
+
+ let nonZeroDelta = delta || abs(_max) || 1e3;
+ let mag = log10(nonZeroDelta);
+ let base = pow(10, floor(mag));
+
+ let _padMin = nonZeroDelta * (delta == 0 ? (_min == 0 ? .1 : 1) : padMin);
+ let _newMin = roundDec(incrRoundDn(_min - _padMin, base/10), 9);
+ let _softMin = _min >= softMin && (softMinMode == 1 || softMinMode == 3 && _newMin <= softMin || softMinMode == 2 && _newMin >= softMin) ? softMin : inf;
+ let minLim = max(hardMin, _newMin < _softMin && _min >= _softMin ? _softMin : min(_softMin, _newMin));
+
+ let _padMax = nonZeroDelta * (delta == 0 ? (_max == 0 ? .1 : 1) : padMax);
+ let _newMax = roundDec(incrRoundUp(_max + _padMax, base/10), 9);
+ let _softMax = _max <= softMax && (softMaxMode == 1 || softMaxMode == 3 && _newMax >= softMax || softMaxMode == 2 && _newMax <= softMax) ? softMax : -inf;
+ let maxLim = min(hardMax, _newMax > _softMax && _max <= _softMax ? _softMax : max(_softMax, _newMax));
+
+ if (minLim == maxLim && minLim == 0)
+ maxLim = 100;
+
+ return [minLim, maxLim];
+ }
+
+ // alternative: https://stackoverflow.com/a/2254896
+ const fmtNum = new Intl.NumberFormat(navigator.language).format;
+
+ const M = Math;
+
+ const PI = M.PI;
+ const abs = M.abs;
+ const floor = M.floor;
+ const round = M.round;
+ const ceil = M.ceil;
+ const min = M.min;
+ const max = M.max;
+ const pow = M.pow;
+ const sign = M.sign;
+ const log10 = M.log10;
+ const log2 = M.log2;
+ // TODO: seems like this needs to match asinh impl if the passed v is tweaked?
+ const sinh = (v, linthresh = 1) => M.sinh(v) * linthresh;
+ const asinh = (v, linthresh = 1) => M.asinh(v / linthresh);
+
+ const inf = Infinity;
+
+ function numIntDigits(x) {
+ return (log10((x ^ (x >> 31)) - (x >> 31)) | 0) + 1;
+ }
+
+ function incrRound(num, incr) {
+ return round(num/incr)*incr;
+ }
+
+ function clamp(num, _min, _max) {
+ return min(max(num, _min), _max);
+ }
+
+ function fnOrSelf(v) {
+ return typeof v == "function" ? v : () => v;
+ }
+
+ const retArg0 = _0 => _0;
+
+ const retArg1 = (_0, _1) => _1;
+
+ const retNull = _ => null;
+
+ const retTrue = _ => true;
+
+ const retEq = (a, b) => a == b;
+
+ function incrRoundUp(num, incr) {
+ return ceil(num/incr)*incr;
+ }
+
+ function incrRoundDn(num, incr) {
+ return floor(num/incr)*incr;
+ }
+
+ function roundDec(val, dec) {
+ return round(val * (dec = 10**dec)) / dec;
+ }
+
+ const fixedDec = new Map();
+
+ function guessDec(num) {
+ return ((""+num).split(".")[1] || "").length;
+ }
+
+ function genIncrs(base, minExp, maxExp, mults) {
+ let incrs = [];
+
+ let multDec = mults.map(guessDec);
+
+ for (let exp = minExp; exp < maxExp; exp++) {
+ let expa = abs(exp);
+ let mag = roundDec(pow(base, exp), expa);
+
+ for (let i = 0; i < mults.length; i++) {
+ let _incr = mults[i] * mag;
+ let dec = (_incr >= 0 && exp >= 0 ? 0 : expa) + (exp >= multDec[i] ? 0 : multDec[i]);
+ let incr = roundDec(_incr, dec);
+ incrs.push(incr);
+ fixedDec.set(incr, dec);
+ }
+ }
+
+ return incrs;
+ }
+
+ //export const assign = Object.assign;
+
+ const EMPTY_OBJ = {};
+ const EMPTY_ARR = [];
+
+ const nullNullTuple = [null, null];
+
+ const isArr = Array.isArray;
+
+ function isStr(v) {
+ return typeof v == 'string';
+ }
+
+ function isObj(v) {
+ let is = false;
+
+ if (v != null) {
+ let c = v.constructor;
+ is = c == null || c == Object;
+ }
+
+ return is;
+ }
+
+ function fastIsObj(v) {
+ return v != null && typeof v == 'object';
+ }
+
+ function copy(o, _isObj = isObj) {
+ let out;
+
+ if (isArr(o)) {
+ let val = o.find(v => v != null);
+
+ if (isArr(val) || _isObj(val)) {
+ out = Array(o.length);
+ for (let i = 0; i < o.length; i++)
+ out[i] = copy(o[i], _isObj);
+ }
+ else
+ out = o.slice();
+ }
+ else if (_isObj(o)) {
+ out = {};
+ for (let k in o)
+ out[k] = copy(o[k], _isObj);
+ }
+ else
+ out = o;
+
+ return out;
+ }
+
+ function assign(targ) {
+ let args = arguments;
+
+ for (let i = 1; i < args.length; i++) {
+ let src = args[i];
+
+ for (let key in src) {
+ if (isObj(targ[key]))
+ assign(targ[key], copy(src[key]));
+ else
+ targ[key] = copy(src[key]);
+ }
+ }
+
+ return targ;
+ }
+
+ // nullModes
+ const NULL_REMOVE = 0; // nulls are converted to undefined (e.g. for spanGaps: true)
+ const NULL_RETAIN = 1; // nulls are retained, with alignment artifacts set to undefined (default)
+ const NULL_EXPAND = 2; // nulls are expanded to include any adjacent alignment artifacts
+
+ // sets undefined values to nulls when adjacent to existing nulls (minesweeper)
+ function nullExpand(yVals, nullIdxs, alignedLen) {
+ for (let i = 0, xi, lastNullIdx = -1; i < nullIdxs.length; i++) {
+ let nullIdx = nullIdxs[i];
+
+ if (nullIdx > lastNullIdx) {
+ xi = nullIdx - 1;
+ while (xi >= 0 && yVals[xi] == null)
+ yVals[xi--] = null;
+
+ xi = nullIdx + 1;
+ while (xi < alignedLen && yVals[xi] == null)
+ yVals[lastNullIdx = xi++] = null;
+ }
+ }
+ }
+
+ // nullModes is a tables-matched array indicating how to treat nulls in each series
+ // output is sorted ASC on the joined field (table[0]) and duplicate join values are collapsed
+ function join(tables, nullModes) {
+ let xVals = new Set();
+
+ for (let ti = 0; ti < tables.length; ti++) {
+ let t = tables[ti];
+ let xs = t[0];
+ let len = xs.length;
+
+ for (let i = 0; i < len; i++)
+ xVals.add(xs[i]);
+ }
+
+ let data = [Array.from(xVals).sort((a, b) => a - b)];
+
+ let alignedLen = data[0].length;
+
+ let xIdxs = new Map();
+
+ for (let i = 0; i < alignedLen; i++)
+ xIdxs.set(data[0][i], i);
+
+ for (let ti = 0; ti < tables.length; ti++) {
+ let t = tables[ti];
+ let xs = t[0];
+
+ for (let si = 1; si < t.length; si++) {
+ let ys = t[si];
+
+ let yVals = Array(alignedLen).fill(undefined);
+
+ let nullMode = nullModes ? nullModes[ti][si] : NULL_RETAIN;
+
+ let nullIdxs = [];
+
+ for (let i = 0; i < ys.length; i++) {
+ let yVal = ys[i];
+ let alignedIdx = xIdxs.get(xs[i]);
+
+ if (yVal === null) {
+ if (nullMode != NULL_REMOVE) {
+ yVals[alignedIdx] = yVal;
+
+ if (nullMode == NULL_EXPAND)
+ nullIdxs.push(alignedIdx);
+ }
+ }
+ else
+ yVals[alignedIdx] = yVal;
+ }
+
+ nullExpand(yVals, nullIdxs, alignedLen);
+
+ data.push(yVals);
+ }
+ }
+
+ return data;
+ }
+
+ const microTask = typeof queueMicrotask == "undefined" ? fn => Promise.resolve().then(fn) : queueMicrotask;
+
+ const WIDTH = "width";
+ const HEIGHT = "height";
+ const TOP = "top";
+ const BOTTOM = "bottom";
+ const LEFT = "left";
+ const RIGHT = "right";
+ const hexBlack = "#000";
+ const transparent = hexBlack + "0";
+
+ const mousemove = "mousemove";
+ const mousedown = "mousedown";
+ const mouseup = "mouseup";
+ const mouseenter = "mouseenter";
+ const mouseleave = "mouseleave";
+ const dblclick = "dblclick";
+ const resize = "resize";
+ const scroll = "scroll";
+
+ const change = "change";
+ const dppxchange = "dppxchange";
+
+ const pre = "u-";
+
+ const UPLOT = "uplot";
+ const ORI_HZ = pre + "hz";
+ const ORI_VT = pre + "vt";
+ const TITLE = pre + "title";
+ const WRAP = pre + "wrap";
+ const UNDER = pre + "under";
+ const OVER = pre + "over";
+ const AXIS = pre + "axis";
+ const OFF = pre + "off";
+ const SELECT = pre + "select";
+ const CURSOR_X = pre + "cursor-x";
+ const CURSOR_Y = pre + "cursor-y";
+ const CURSOR_PT = pre + "cursor-pt";
+ const LEGEND = pre + "legend";
+ const LEGEND_LIVE = pre + "live";
+ const LEGEND_INLINE = pre + "inline";
+ const LEGEND_THEAD = pre + "thead";
+ const LEGEND_SERIES = pre + "series";
+ const LEGEND_MARKER = pre + "marker";
+ const LEGEND_LABEL = pre + "label";
+ const LEGEND_VALUE = pre + "value";
+
+ const doc = document;
+ const win = window;
+ let pxRatio;
+
+ let query;
+
+ function setPxRatio() {
+ let _pxRatio = devicePixelRatio;
+
+ // during print preview, Chrome fires off these dppx queries even without changes
+ if (pxRatio != _pxRatio) {
+ pxRatio = _pxRatio;
+
+ query && off(change, query, setPxRatio);
+ query = matchMedia(`(min-resolution: ${pxRatio - 0.001}dppx) and (max-resolution: ${pxRatio + 0.001}dppx)`);
+ on(change, query, setPxRatio);
+
+ win.dispatchEvent(new CustomEvent(dppxchange));
+ }
+ }
+
+ function addClass(el, c) {
+ if (c != null) {
+ let cl = el.classList;
+ !cl.contains(c) && cl.add(c);
+ }
+ }
+
+ function remClass(el, c) {
+ let cl = el.classList;
+ cl.contains(c) && cl.remove(c);
+ }
+
+ function setStylePx(el, name, value) {
+ el.style[name] = value + "px";
+ }
+
+ function placeTag(tag, cls, targ, refEl) {
+ let el = doc.createElement(tag);
+
+ if (cls != null)
+ addClass(el, cls);
+
+ if (targ != null)
+ targ.insertBefore(el, refEl);
+
+ return el;
+ }
+
+ function placeDiv(cls, targ) {
+ return placeTag("div", cls, targ);
+ }
+
+ const xformCache = new WeakMap();
+
+ function elTrans(el, xPos, yPos, xMax, yMax) {
+ let xform = "translate(" + xPos + "px," + yPos + "px)";
+ let xformOld = xformCache.get(el);
+
+ if (xform != xformOld) {
+ el.style.transform = xform;
+ xformCache.set(el, xform);
+
+ if (xPos < 0 || yPos < 0 || xPos > xMax || yPos > yMax)
+ addClass(el, OFF);
+ else
+ remClass(el, OFF);
+ }
+ }
+
+ const colorCache = new WeakMap();
+
+ function elColor(el, background, borderColor) {
+ let newColor = background + borderColor;
+ let oldColor = colorCache.get(el);
+
+ if (newColor != oldColor) {
+ colorCache.set(el, newColor);
+ el.style.background = background;
+ el.style.borderColor = borderColor;
+ }
+ }
+
+ const sizeCache = new WeakMap();
+
+ function elSize(el, newWid, newHgt, centered) {
+ let newSize = newWid + "" + newHgt;
+ let oldSize = sizeCache.get(el);
+
+ if (newSize != oldSize) {
+ sizeCache.set(el, newSize);
+ el.style.height = newHgt + "px";
+ el.style.width = newWid + "px";
+ el.style.marginLeft = centered ? -newWid/2 + "px" : 0;
+ el.style.marginTop = centered ? -newHgt/2 + "px" : 0;
+ }
+ }
+
+ const evOpts = {passive: true};
+ const evOpts2 = assign({capture: true}, evOpts);
+
+ function on(ev, el, cb, capt) {
+ el.addEventListener(ev, cb, capt ? evOpts2 : evOpts);
+ }
+
+ function off(ev, el, cb, capt) {
+ el.removeEventListener(ev, cb, capt ? evOpts2 : evOpts);
+ }
+
+ setPxRatio();
+
+ const months = [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+ ];
+
+ const days = [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ ];
+
+ function slice3(str) {
+ return str.slice(0, 3);
+ }
+
+ const days3 = days.map(slice3);
+
+ const months3 = months.map(slice3);
+
+ const engNames = {
+ MMMM: months,
+ MMM: months3,
+ WWWW: days,
+ WWW: days3,
+ };
+
+ function zeroPad2(int) {
+ return (int < 10 ? '0' : '') + int;
+ }
+
+ function zeroPad3(int) {
+ return (int < 10 ? '00' : int < 100 ? '0' : '') + int;
+ }
+
+ /*
+ function suffix(int) {
+ let mod10 = int % 10;
+
+ return int + (
+ mod10 == 1 && int != 11 ? "st" :
+ mod10 == 2 && int != 12 ? "nd" :
+ mod10 == 3 && int != 13 ? "rd" : "th"
+ );
+ }
+ */
+
+ const subs = {
+ // 2019
+ YYYY: d => d.getFullYear(),
+ // 19
+ YY: d => (d.getFullYear()+'').slice(2),
+ // July
+ MMMM: (d, names) => names.MMMM[d.getMonth()],
+ // Jul
+ MMM: (d, names) => names.MMM[d.getMonth()],
+ // 07
+ MM: d => zeroPad2(d.getMonth()+1),
+ // 7
+ M: d => d.getMonth()+1,
+ // 09
+ DD: d => zeroPad2(d.getDate()),
+ // 9
+ D: d => d.getDate(),
+ // Monday
+ WWWW: (d, names) => names.WWWW[d.getDay()],
+ // Mon
+ WWW: (d, names) => names.WWW[d.getDay()],
+ // 03
+ HH: d => zeroPad2(d.getHours()),
+ // 3
+ H: d => d.getHours(),
+ // 9 (12hr, unpadded)
+ h: d => {let h = d.getHours(); return h == 0 ? 12 : h > 12 ? h - 12 : h;},
+ // AM
+ AA: d => d.getHours() >= 12 ? 'PM' : 'AM',
+ // am
+ aa: d => d.getHours() >= 12 ? 'pm' : 'am',
+ // a
+ a: d => d.getHours() >= 12 ? 'p' : 'a',
+ // 09
+ mm: d => zeroPad2(d.getMinutes()),
+ // 9
+ m: d => d.getMinutes(),
+ // 09
+ ss: d => zeroPad2(d.getSeconds()),
+ // 9
+ s: d => d.getSeconds(),
+ // 374
+ fff: d => zeroPad3(d.getMilliseconds()),
+ };
+
+ function fmtDate(tpl, names) {
+ names = names || engNames;
+ let parts = [];
+
+ let R = /\{([a-z]+)\}|[^{]+/gi, m;
+
+ while (m = R.exec(tpl))
+ parts.push(m[0][0] == '{' ? subs[m[1]] : m[0]);
+
+ return d => {
+ let out = '';
+
+ for (let i = 0; i < parts.length; i++)
+ out += typeof parts[i] == "string" ? parts[i] : parts[i](d, names);
+
+ return out;
+ }
+ }
+
+ const localTz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+ // https://stackoverflow.com/questions/15141762/how-to-initialize-a-javascript-date-to-a-particular-time-zone/53652131#53652131
+ function tzDate(date, tz) {
+ let date2;
+
+ // perf optimization
+ if (tz == 'UTC' || tz == 'Etc/UTC')
+ date2 = new Date(+date + date.getTimezoneOffset() * 6e4);
+ else if (tz == localTz)
+ date2 = date;
+ else {
+ date2 = new Date(date.toLocaleString('en-US', {timeZone: tz}));
+ date2.setMilliseconds(date.getMilliseconds());
+ }
+
+ return date2;
+ }
+
+ //export const series = [];
+
+ // default formatters:
+
+ const onlyWhole = v => v % 1 == 0;
+
+ const allMults = [1,2,2.5,5];
+
+ // ...0.01, 0.02, 0.025, 0.05, 0.1, 0.2, 0.25, 0.5
+ const decIncrs = genIncrs(10, -16, 0, allMults);
+
+ // 1, 2, 2.5, 5, 10, 20, 25, 50...
+ const oneIncrs = genIncrs(10, 0, 16, allMults);
+
+ // 1, 2, 5, 10, 20, 25, 50...
+ const wholeIncrs = oneIncrs.filter(onlyWhole);
+
+ const numIncrs = decIncrs.concat(oneIncrs);
+
+ const NL = "\n";
+
+ const yyyy = "{YYYY}";
+ const NLyyyy = NL + yyyy;
+ const md = "{M}/{D}";
+ const NLmd = NL + md;
+ const NLmdyy = NLmd + "/{YY}";
+
+ const aa = "{aa}";
+ const hmm = "{h}:{mm}";
+ const hmmaa = hmm + aa;
+ const NLhmmaa = NL + hmmaa;
+ const ss = ":{ss}";
+
+ const _ = null;
+
+ function genTimeStuffs(ms) {
+ let s = ms * 1e3,
+ m = s * 60,
+ h = m * 60,
+ d = h * 24,
+ mo = d * 30,
+ y = d * 365;
+
+ // min of 1e-3 prevents setting a temporal x ticks too small since Date objects cannot advance ticks smaller than 1ms
+ let subSecIncrs = ms == 1 ? genIncrs(10, 0, 3, allMults).filter(onlyWhole) : genIncrs(10, -3, 0, allMults);
+
+ let timeIncrs = subSecIncrs.concat([
+ // minute divisors (# of secs)
+ s,
+ s * 5,
+ s * 10,
+ s * 15,
+ s * 30,
+ // hour divisors (# of mins)
+ m,
+ m * 5,
+ m * 10,
+ m * 15,
+ m * 30,
+ // day divisors (# of hrs)
+ h,
+ h * 2,
+ h * 3,
+ h * 4,
+ h * 6,
+ h * 8,
+ h * 12,
+ // month divisors TODO: need more?
+ d,
+ d * 2,
+ d * 3,
+ d * 4,
+ d * 5,
+ d * 6,
+ d * 7,
+ d * 8,
+ d * 9,
+ d * 10,
+ d * 15,
+ // year divisors (# months, approx)
+ mo,
+ mo * 2,
+ mo * 3,
+ mo * 4,
+ mo * 6,
+ // century divisors
+ y,
+ y * 2,
+ y * 5,
+ y * 10,
+ y * 25,
+ y * 50,
+ y * 100,
+ ]);
+
+ // [0]: minimum num secs in the tick incr
+ // [1]: default tick format
+ // [2-7]: rollover tick formats
+ // [8]: mode: 0: replace [1] -> [2-7], 1: concat [1] + [2-7]
+ const _timeAxisStamps = [
+ // tick incr default year month day hour min sec mode
+ [y, yyyy, _, _, _, _, _, _, 1],
+ [d * 28, "{MMM}", NLyyyy, _, _, _, _, _, 1],
+ [d, md, NLyyyy, _, _, _, _, _, 1],
+ [h, "{h}" + aa, NLmdyy, _, NLmd, _, _, _, 1],
+ [m, hmmaa, NLmdyy, _, NLmd, _, _, _, 1],
+ [s, ss, NLmdyy + " " + hmmaa, _, NLmd + " " + hmmaa, _, NLhmmaa, _, 1],
+ [ms, ss + ".{fff}", NLmdyy + " " + hmmaa, _, NLmd + " " + hmmaa, _, NLhmmaa, _, 1],
+ ];
+
+ // the ensures that axis ticks, values & grid are aligned to logical temporal breakpoints and not an arbitrary timestamp
+ // https://www.timeanddate.com/time/dst/
+ // https://www.timeanddate.com/time/dst/2019.html
+ // https://www.epochconverter.com/timezones
+ function timeAxisSplits(tzDate) {
+ return (self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace) => {
+ let splits = [];
+ let isYr = foundIncr >= y;
+ let isMo = foundIncr >= mo && foundIncr < y;
+
+ // get the timezone-adjusted date
+ let minDate = tzDate(scaleMin);
+ let minDateTs = roundDec(minDate * ms, 3);
+
+ // get ts of 12am (this lands us at or before the original scaleMin)
+ let minMin = mkDate(minDate.getFullYear(), isYr ? 0 : minDate.getMonth(), isMo || isYr ? 1 : minDate.getDate());
+ let minMinTs = roundDec(minMin * ms, 3);
+
+ if (isMo || isYr) {
+ let moIncr = isMo ? foundIncr / mo : 0;
+ let yrIncr = isYr ? foundIncr / y : 0;
+ // let tzOffset = scaleMin - minDateTs; // needed?
+ let split = minDateTs == minMinTs ? minDateTs : roundDec(mkDate(minMin.getFullYear() + yrIncr, minMin.getMonth() + moIncr, 1) * ms, 3);
+ let splitDate = new Date(round(split / ms));
+ let baseYear = splitDate.getFullYear();
+ let baseMonth = splitDate.getMonth();
+
+ for (let i = 0; split <= scaleMax; i++) {
+ let next = mkDate(baseYear + yrIncr * i, baseMonth + moIncr * i, 1);
+ let offs = next - tzDate(roundDec(next * ms, 3));
+
+ split = roundDec((+next + offs) * ms, 3);
+
+ if (split <= scaleMax)
+ splits.push(split);
+ }
+ }
+ else {
+ let incr0 = foundIncr >= d ? d : foundIncr;
+ let tzOffset = floor(scaleMin) - floor(minDateTs);
+ let split = minMinTs + tzOffset + incrRoundUp(minDateTs - minMinTs, incr0);
+ splits.push(split);
+
+ let date0 = tzDate(split);
+
+ let prevHour = date0.getHours() + (date0.getMinutes() / m) + (date0.getSeconds() / h);
+ let incrHours = foundIncr / h;
+
+ let minSpace = self.axes[axisIdx]._space;
+ let pctSpace = foundSpace / minSpace;
+
+ while (1) {
+ split = roundDec(split + foundIncr, ms == 1 ? 0 : 3);
+
+ if (split > scaleMax)
+ break;
+
+ if (incrHours > 1) {
+ let expectedHour = floor(roundDec(prevHour + incrHours, 6)) % 24;
+ let splitDate = tzDate(split);
+ let actualHour = splitDate.getHours();
+
+ let dstShift = actualHour - expectedHour;
+
+ if (dstShift > 1)
+ dstShift = -1;
+
+ split -= dstShift * h;
+
+ prevHour = (prevHour + incrHours) % 24;
+
+ // add a tick only if it's further than 70% of the min allowed label spacing
+ let prevSplit = splits[splits.length - 1];
+ let pctIncr = roundDec((split - prevSplit) / foundIncr, 3);
+
+ if (pctIncr * pctSpace >= .7)
+ splits.push(split);
+ }
+ else
+ splits.push(split);
+ }
+ }
+
+ return splits;
+ }
+ }
+
+ return [
+ timeIncrs,
+ _timeAxisStamps,
+ timeAxisSplits,
+ ];
+ }
+
+ const [ timeIncrsMs, _timeAxisStampsMs, timeAxisSplitsMs ] = genTimeStuffs(1);
+ const [ timeIncrsS, _timeAxisStampsS, timeAxisSplitsS ] = genTimeStuffs(1e-3);
+
+ // base 2
+ genIncrs(2, -53, 53, [1]);
+
+ /*
+ console.log({
+ decIncrs,
+ oneIncrs,
+ wholeIncrs,
+ numIncrs,
+ timeIncrs,
+ fixedDec,
+ });
+ */
+
+ function timeAxisStamps(stampCfg, fmtDate) {
+ return stampCfg.map(s => s.map((v, i) =>
+ i == 0 || i == 8 || v == null ? v : fmtDate(i == 1 || s[8] == 0 ? v : s[1] + v)
+ ));
+ }
+
+ // TODO: will need to accept spaces[] and pull incr into the loop when grid will be non-uniform, eg for log scales.
+ // currently we ignore this for months since they're *nearly* uniform and the added complexity is not worth it
+ function timeAxisVals(tzDate, stamps) {
+ return (self, splits, axisIdx, foundSpace, foundIncr) => {
+ let s = stamps.find(s => foundIncr >= s[0]) || stamps[stamps.length - 1];
+
+ // these track boundaries when a full label is needed again
+ let prevYear;
+ let prevMnth;
+ let prevDate;
+ let prevHour;
+ let prevMins;
+ let prevSecs;
+
+ return splits.map(split => {
+ let date = tzDate(split);
+
+ let newYear = date.getFullYear();
+ let newMnth = date.getMonth();
+ let newDate = date.getDate();
+ let newHour = date.getHours();
+ let newMins = date.getMinutes();
+ let newSecs = date.getSeconds();
+
+ let stamp = (
+ newYear != prevYear && s[2] ||
+ newMnth != prevMnth && s[3] ||
+ newDate != prevDate && s[4] ||
+ newHour != prevHour && s[5] ||
+ newMins != prevMins && s[6] ||
+ newSecs != prevSecs && s[7] ||
+ s[1]
+ );
+
+ prevYear = newYear;
+ prevMnth = newMnth;
+ prevDate = newDate;
+ prevHour = newHour;
+ prevMins = newMins;
+ prevSecs = newSecs;
+
+ return stamp(date);
+ });
+ }
+ }
+
+ // for when axis.values is defined as a static fmtDate template string
+ function timeAxisVal(tzDate, dateTpl) {
+ let stamp = fmtDate(dateTpl);
+ return (self, splits, axisIdx, foundSpace, foundIncr) => splits.map(split => stamp(tzDate(split)));
+ }
+
+ function mkDate(y, m, d) {
+ return new Date(y, m, d);
+ }
+
+ function timeSeriesStamp(stampCfg, fmtDate) {
+ return fmtDate(stampCfg);
+ }
+ const _timeSeriesStamp = '{YYYY}-{MM}-{DD} {h}:{mm}{aa}';
+
+ function timeSeriesVal(tzDate, stamp) {
+ return (self, val) => stamp(tzDate(val));
+ }
+
+ function legendStroke(self, seriesIdx) {
+ let s = self.series[seriesIdx];
+ return s.width ? s.stroke(self, seriesIdx) : s.points.width ? s.points.stroke(self, seriesIdx) : null;
+ }
+
+ function legendFill(self, seriesIdx) {
+ return self.series[seriesIdx].fill(self, seriesIdx);
+ }
+
+ const legendOpts = {
+ show: true,
+ live: true,
+ isolate: false,
+ markers: {
+ show: true,
+ width: 2,
+ stroke: legendStroke,
+ fill: legendFill,
+ dash: "solid",
+ },
+ idx: null,
+ idxs: null,
+ values: [],
+ };
+
+ function cursorPointShow(self, si) {
+ let o = self.cursor.points;
+
+ let pt = placeDiv();
+
+ let size = o.size(self, si);
+ setStylePx(pt, WIDTH, size);
+ setStylePx(pt, HEIGHT, size);
+
+ let mar = size / -2;
+ setStylePx(pt, "marginLeft", mar);
+ setStylePx(pt, "marginTop", mar);
+
+ let width = o.width(self, si, size);
+ width && setStylePx(pt, "borderWidth", width);
+
+ return pt;
+ }
+
+ function cursorPointFill(self, si) {
+ let sp = self.series[si].points;
+ return sp._fill || sp._stroke;
+ }
+
+ function cursorPointStroke(self, si) {
+ let sp = self.series[si].points;
+ return sp._stroke || sp._fill;
+ }
+
+ function cursorPointSize(self, si) {
+ let sp = self.series[si].points;
+ return ptDia(sp.width, 1);
+ }
+
+ function dataIdx(self, seriesIdx, cursorIdx) {
+ return cursorIdx;
+ }
+
+ const moveTuple = [0,0];
+
+ function cursorMove(self, mouseLeft1, mouseTop1) {
+ moveTuple[0] = mouseLeft1;
+ moveTuple[1] = mouseTop1;
+ return moveTuple;
+ }
+
+ function filtBtn0(self, targ, handle) {
+ return e => {
+ e.button == 0 && handle(e);
+ };
+ }
+
+ function passThru(self, targ, handle) {
+ return handle;
+ }
+
+ const cursorOpts = {
+ show: true,
+ x: true,
+ y: true,
+ lock: false,
+ move: cursorMove,
+ points: {
+ show: cursorPointShow,
+ size: cursorPointSize,
+ width: 0,
+ stroke: cursorPointStroke,
+ fill: cursorPointFill,
+ },
+
+ bind: {
+ mousedown: filtBtn0,
+ mouseup: filtBtn0,
+ click: filtBtn0,
+ dblclick: filtBtn0,
+
+ mousemove: passThru,
+ mouseleave: passThru,
+ mouseenter: passThru,
+ },
+
+ drag: {
+ setScale: true,
+ x: true,
+ y: false,
+ dist: 0,
+ uni: null,
+ _x: false,
+ _y: false,
+ },
+
+ focus: {
+ prox: -1,
+ },
+
+ left: -10,
+ top: -10,
+ idx: null,
+ dataIdx,
+ idxs: null,
+ };
+
+ const grid = {
+ show: true,
+ stroke: "rgba(0,0,0,0.07)",
+ width: 2,
+ // dash: [],
+ filter: retArg1,
+ };
+
+ const ticks = assign({}, grid, {size: 10});
+
+ 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"';
+ const labelFont = "bold " + font;
+ const lineMult = 1.5; // font-size multiplier
+
+ const xAxisOpts = {
+ show: true,
+ scale: "x",
+ stroke: hexBlack,
+ space: 50,
+ gap: 5,
+ size: 50,
+ labelGap: 0,
+ labelSize: 30,
+ labelFont,
+ side: 2,
+ // class: "x-vals",
+ // incrs: timeIncrs,
+ // values: timeVals,
+ // filter: retArg1,
+ grid,
+ ticks,
+ font,
+ rotate: 0,
+ };
+
+ const numSeriesLabel = "Value";
+ const timeSeriesLabel = "Time";
+
+ const xSeriesOpts = {
+ show: true,
+ scale: "x",
+ auto: false,
+ sorted: 1,
+ // label: "Time",
+ // value: v => stamp(new Date(v * 1e3)),
+
+ // internal caches
+ min: inf,
+ max: -inf,
+ idxs: [],
+ };
+
+ function numAxisVals(self, splits, axisIdx, foundSpace, foundIncr) {
+ return splits.map(v => v == null ? "" : fmtNum(v));
+ }
+
+ function numAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {
+ let splits = [];
+
+ let numDec = fixedDec.get(foundIncr) || 0;
+
+ scaleMin = forceMin ? scaleMin : roundDec(incrRoundUp(scaleMin, foundIncr), numDec);
+
+ for (let val = scaleMin; val <= scaleMax; val = roundDec(val + foundIncr, numDec))
+ splits.push(Object.is(val, -0) ? 0 : val); // coalesces -0
+
+ return splits;
+ }
+
+ // this doesnt work for sin, which needs to come off from 0 independently in pos and neg dirs
+ function logAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {
+ const splits = [];
+
+ const logBase = self.scales[self.axes[axisIdx].scale].log;
+
+ const logFn = logBase == 10 ? log10 : log2;
+
+ const exp = floor(logFn(scaleMin));
+
+ foundIncr = pow(logBase, exp);
+
+ if (exp < 0)
+ foundIncr = roundDec(foundIncr, -exp);
+
+ let split = scaleMin;
+
+ do {
+ splits.push(split);
+ split = roundDec(split + foundIncr, fixedDec.get(foundIncr));
+
+ if (split >= foundIncr * logBase)
+ foundIncr = split;
+
+ } while (split <= scaleMax);
+
+ return splits;
+ }
+
+ function asinhAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {
+ let sc = self.scales[self.axes[axisIdx].scale];
+
+ let linthresh = sc.asinh;
+
+ let posSplits = scaleMax > linthresh ? logAxisSplits(self, axisIdx, max(linthresh, scaleMin), scaleMax, foundIncr) : [linthresh];
+ let zero = scaleMax >= 0 && scaleMin <= 0 ? [0] : [];
+ let negSplits = scaleMin < -linthresh ? logAxisSplits(self, axisIdx, max(linthresh, -scaleMax), -scaleMin, foundIncr): [linthresh];
+
+ return negSplits.reverse().map(v => -v).concat(zero, posSplits);
+ }
+
+ const RE_ALL = /./;
+ const RE_12357 = /[12357]/;
+ const RE_125 = /[125]/;
+ const RE_1 = /1/;
+
+ function logAxisValsFilt(self, splits, axisIdx, foundSpace, foundIncr) {
+ let axis = self.axes[axisIdx];
+ let scaleKey = axis.scale;
+ let sc = self.scales[scaleKey];
+
+ if (sc.distr == 3 && sc.log == 2)
+ return splits;
+
+ let valToPos = self.valToPos;
+
+ let minSpace = axis._space;
+
+ let _10 = valToPos(10, scaleKey);
+
+ let re = (
+ valToPos(9, scaleKey) - _10 >= minSpace ? RE_ALL :
+ valToPos(7, scaleKey) - _10 >= minSpace ? RE_12357 :
+ valToPos(5, scaleKey) - _10 >= minSpace ? RE_125 :
+ RE_1
+ );
+
+ return splits.map(v => ((sc.distr == 4 && v == 0) || re.test(v)) ? v : null);
+ }
+
+ function numSeriesVal(self, val) {
+ return val == null ? "" : fmtNum(val);
+ }
+
+ const yAxisOpts = {
+ show: true,
+ scale: "y",
+ stroke: hexBlack,
+ space: 30,
+ gap: 5,
+ size: 50,
+ labelGap: 0,
+ labelSize: 30,
+ labelFont,
+ side: 3,
+ // class: "y-vals",
+ // incrs: numIncrs,
+ // values: (vals, space) => vals,
+ // filter: retArg1,
+ grid,
+ ticks,
+ font,
+ rotate: 0,
+ };
+
+ // takes stroke width
+ function ptDia(width, mult) {
+ let dia = 3 + (width || 1) * 2;
+ return roundDec(dia * mult, 3);
+ }
+
+ function seriesPointsShow(self, si) {
+ let { scale, idxs } = self.series[0];
+ let xData = self._data[0];
+ let p0 = self.valToPos(xData[idxs[0]], scale, true);
+ let p1 = self.valToPos(xData[idxs[1]], scale, true);
+ let dim = abs(p1 - p0);
+
+ let s = self.series[si];
+ // const dia = ptDia(s.width, pxRatio);
+ let maxPts = dim / (s.points.space * pxRatio);
+ return idxs[1] - idxs[0] <= maxPts;
+ }
+
+ function seriesFillTo(self, seriesIdx, dataMin, dataMax) {
+ let scale = self.scales[self.series[seriesIdx].scale];
+ let isUpperBandEdge = self.bands && self.bands.some(b => b.series[0] == seriesIdx);
+ return scale.distr == 3 || isUpperBandEdge ? scale.min : 0;
+ }
+
+ const facet = {
+ scale: null,
+ auto: true,
+
+ // internal caches
+ min: inf,
+ max: -inf,
+ };
+
+ const xySeriesOpts = {
+ show: true,
+ auto: true,
+ sorted: 0,
+ alpha: 1,
+ facets: [
+ assign({}, facet, {scale: 'x'}),
+ assign({}, facet, {scale: 'y'}),
+ ],
+ };
+
+ const ySeriesOpts = {
+ scale: "y",
+ auto: true,
+ sorted: 0,
+ show: true,
+ spanGaps: false,
+ gaps: (self, seriesIdx, idx0, idx1, nullGaps) => nullGaps,
+ alpha: 1,
+ points: {
+ show: seriesPointsShow,
+ filter: null,
+ // paths:
+ // stroke: "#000",
+ // fill: "#fff",
+ // width: 1,
+ // size: 10,
+ },
+ // label: "Value",
+ // value: v => v,
+ values: null,
+
+ // internal caches
+ min: inf,
+ max: -inf,
+ idxs: [],
+
+ path: null,
+ clip: null,
+ };
+
+ function clampScale(self, val, scaleMin, scaleMax, scaleKey) {
+ /*
+ if (val < 0) {
+ let cssHgt = self.bbox.height / pxRatio;
+ let absPos = self.valToPos(abs(val), scaleKey);
+ let fromBtm = cssHgt - absPos;
+ return self.posToVal(cssHgt + fromBtm, scaleKey);
+ }
+ */
+ return scaleMin / 10;
+ }
+
+ const xScaleOpts = {
+ time: FEAT_TIME,
+ auto: true,
+ distr: 1,
+ log: 10,
+ asinh: 1,
+ min: null,
+ max: null,
+ dir: 1,
+ ori: 0,
+ };
+
+ const yScaleOpts = assign({}, xScaleOpts, {
+ time: false,
+ ori: 1,
+ });
+
+ const syncs = {};
+
+ function _sync(key, opts) {
+ let s = syncs[key];
+
+ if (!s) {
+ s = {
+ key,
+ plots: [],
+ sub(plot) {
+ s.plots.push(plot);
+ },
+ unsub(plot) {
+ s.plots = s.plots.filter(c => c != plot);
+ },
+ pub(type, self, x, y, w, h, i) {
+ for (let j = 0; j < s.plots.length; j++)
+ s.plots[j] != self && s.plots[j].pub(type, self, x, y, w, h, i);
+ },
+ };
+
+ if (key != null)
+ syncs[key] = s;
+ }
+
+ return s;
+ }
+
+ const BAND_CLIP_FILL = 1 << 0;
+ const BAND_CLIP_STROKE = 1 << 1;
+
+ function orient(u, seriesIdx, cb) {
+ const series = u.series[seriesIdx];
+ const scales = u.scales;
+ const bbox = u.bbox;
+ const scaleX = u.mode == 2 ? scales[series.facets[0].scale] : scales[u.series[0].scale];
+
+ let dx = u._data[0],
+ dy = u._data[seriesIdx],
+ sx = scaleX,
+ sy = u.mode == 2 ? scales[series.facets[1].scale] : scales[series.scale],
+ l = bbox.left,
+ t = bbox.top,
+ w = bbox.width,
+ h = bbox.height,
+ H = u.valToPosH,
+ V = u.valToPosV;
+
+ return (sx.ori == 0
+ ? cb(
+ series,
+ dx,
+ dy,
+ sx,
+ sy,
+ H,
+ V,
+ l,
+ t,
+ w,
+ h,
+ moveToH,
+ lineToH,
+ rectH,
+ arcH,
+ bezierCurveToH,
+ )
+ : cb(
+ series,
+ dx,
+ dy,
+ sx,
+ sy,
+ V,
+ H,
+ t,
+ l,
+ h,
+ w,
+ moveToV,
+ lineToV,
+ rectV,
+ arcV,
+ bezierCurveToV,
+ )
+ );
+ }
+
+ // creates inverted band clip path (towards from stroke path -> yMax)
+ function clipBandLine(self, seriesIdx, idx0, idx1, strokePath) {
+ return orient(self, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+ const lineTo = scaleX.ori == 0 ? lineToH : lineToV;
+
+ let frIdx, toIdx;
+
+ if (dir == 1) {
+ frIdx = idx0;
+ toIdx = idx1;
+ }
+ else {
+ frIdx = idx1;
+ toIdx = idx0;
+ }
+
+ // path start
+ let x0 = pxRound(valToPosX(dataX[frIdx], scaleX, xDim, xOff));
+ let y0 = pxRound(valToPosY(dataY[frIdx], scaleY, yDim, yOff));
+ // path end x
+ let x1 = pxRound(valToPosX(dataX[toIdx], scaleX, xDim, xOff));
+ // upper y limit
+ let yLimit = pxRound(valToPosY(scaleY.max, scaleY, yDim, yOff));
+
+ let clip = new Path2D(strokePath);
+
+ lineTo(clip, x1, yLimit);
+ lineTo(clip, x0, yLimit);
+ lineTo(clip, x0, y0);
+
+ return clip;
+ });
+ }
+
+ function clipGaps(gaps, ori, plotLft, plotTop, plotWid, plotHgt) {
+ let clip = null;
+
+ // create clip path (invert gaps and non-gaps)
+ if (gaps.length > 0) {
+ clip = new Path2D();
+
+ const rect = ori == 0 ? rectH : rectV;
+
+ let prevGapEnd = plotLft;
+
+ for (let i = 0; i < gaps.length; i++) {
+ let g = gaps[i];
+
+ if (g[1] > g[0]) {
+ let w = g[0] - prevGapEnd;
+
+ w > 0 && rect(clip, prevGapEnd, plotTop, w, plotTop + plotHgt);
+
+ prevGapEnd = g[1];
+ }
+ }
+
+ let w = plotLft + plotWid - prevGapEnd;
+
+ w > 0 && rect(clip, prevGapEnd, plotTop, w, plotTop + plotHgt);
+ }
+
+ return clip;
+ }
+
+ function addGap(gaps, fromX, toX) {
+ let prevGap = gaps[gaps.length - 1];
+
+ if (prevGap && prevGap[0] == fromX) // TODO: gaps must be encoded at stroke widths?
+ prevGap[1] = toX;
+ else
+ gaps.push([fromX, toX]);
+ }
+
+ function pxRoundGen(pxAlign) {
+ return pxAlign == 0 ? retArg0 : pxAlign == 1 ? round : v => incrRound(v, pxAlign);
+ }
+
+ function rect(ori) {
+ let moveTo = ori == 0 ?
+ moveToH :
+ moveToV;
+
+ let arcTo = ori == 0 ?
+ (p, x1, y1, x2, y2, r) => { p.arcTo(x1, y1, x2, y2, r); } :
+ (p, y1, x1, y2, x2, r) => { p.arcTo(x1, y1, x2, y2, r); };
+
+ let rect = ori == 0 ?
+ (p, x, y, w, h) => { p.rect(x, y, w, h); } :
+ (p, y, x, h, w) => { p.rect(x, y, w, h); };
+
+ return (p, x, y, w, h, r = 0) => {
+ if (r == 0)
+ rect(p, x, y, w, h);
+ else {
+ r = min(r, w / 2, h / 2);
+
+ // adapted from https://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-using-html-canvas/7838871#7838871
+ moveTo(p, x + r, y);
+ arcTo(p, x + w, y, x + w, y + h, r);
+ arcTo(p, x + w, y + h, x, y + h, r);
+ arcTo(p, x, y + h, x, y, r);
+ arcTo(p, x, y, x + w, y, r);
+ p.closePath();
+ }
+ };
+ }
+
+ // orientation-inverting canvas functions
+ const moveToH = (p, x, y) => { p.moveTo(x, y); };
+ const moveToV = (p, y, x) => { p.moveTo(x, y); };
+ const lineToH = (p, x, y) => { p.lineTo(x, y); };
+ const lineToV = (p, y, x) => { p.lineTo(x, y); };
+ const rectH = rect(0);
+ const rectV = rect(1);
+ const arcH = (p, x, y, r, startAngle, endAngle) => { p.arc(x, y, r, startAngle, endAngle); };
+ const arcV = (p, y, x, r, startAngle, endAngle) => { p.arc(x, y, r, startAngle, endAngle); };
+ const bezierCurveToH = (p, bp1x, bp1y, bp2x, bp2y, p2x, p2y) => { p.bezierCurveTo(bp1x, bp1y, bp2x, bp2y, p2x, p2y); };
+ const bezierCurveToV = (p, bp1y, bp1x, bp2y, bp2x, p2y, p2x) => { p.bezierCurveTo(bp1x, bp1y, bp2x, bp2y, p2x, p2y); };
+
+ // TODO: drawWrap(seriesIdx, drawPoints) (save, restore, translate, clip)
+ function points(opts) {
+ return (u, seriesIdx, idx0, idx1, filtIdxs) => {
+ // log("drawPoints()", arguments);
+
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let { pxRound, points } = series;
+
+ let moveTo, arc;
+
+ if (scaleX.ori == 0) {
+ moveTo = moveToH;
+ arc = arcH;
+ }
+ else {
+ moveTo = moveToV;
+ arc = arcV;
+ }
+
+ const width = roundDec(points.width * pxRatio, 3);
+
+ let rad = (points.size - points.width) / 2 * pxRatio;
+ let dia = roundDec(rad * 2, 3);
+
+ let fill = new Path2D();
+ let clip = new Path2D();
+
+ let { left: lft, top: top, width: wid, height: hgt } = u.bbox;
+
+ rectH(clip,
+ lft - dia,
+ top - dia,
+ wid + dia * 2,
+ hgt + dia * 2,
+ );
+
+ const drawPoint = pi => {
+ if (dataY[pi] != null) {
+ let x = pxRound(valToPosX(dataX[pi], scaleX, xDim, xOff));
+ let y = pxRound(valToPosY(dataY[pi], scaleY, yDim, yOff));
+
+ moveTo(fill, x + rad, y);
+ arc(fill, x, y, rad, 0, PI * 2);
+ }
+ };
+
+ if (filtIdxs)
+ filtIdxs.forEach(drawPoint);
+ else {
+ for (let pi = idx0; pi <= idx1; pi++)
+ drawPoint(pi);
+ }
+
+ return {
+ stroke: width > 0 ? fill : null,
+ fill,
+ clip,
+ flags: BAND_CLIP_FILL | BAND_CLIP_STROKE,
+ };
+ });
+ };
+ }
+
+ function _drawAcc(lineTo) {
+ return (stroke, accX, minY, maxY, inY, outY) => {
+ if (minY != maxY) {
+ if (inY != minY && outY != minY)
+ lineTo(stroke, accX, minY);
+ if (inY != maxY && outY != maxY)
+ lineTo(stroke, accX, maxY);
+
+ lineTo(stroke, accX, outY);
+ }
+ };
+ }
+
+ const drawAccH = _drawAcc(lineToH);
+ const drawAccV = _drawAcc(lineToV);
+
+ function linear() {
+ return (u, seriesIdx, idx0, idx1) => {
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ let lineTo, drawAcc;
+
+ if (scaleX.ori == 0) {
+ lineTo = lineToH;
+ drawAcc = drawAccH;
+ }
+ else {
+ lineTo = lineToV;
+ drawAcc = drawAccV;
+ }
+
+ const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+
+ const _paths = {stroke: new Path2D(), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};
+ const stroke = _paths.stroke;
+
+ let minY = inf,
+ maxY = -inf,
+ inY, outY, outX, drawnAtX;
+
+ let gaps = [];
+
+ let accX = pxRound(valToPosX(dataX[dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));
+ let accGaps = false;
+ let prevYNull = false;
+
+ // data edges
+ let lftIdx = nonNullIdx(dataY, idx0, idx1, 1 * dir);
+ let rgtIdx = nonNullIdx(dataY, idx0, idx1, -1 * dir);
+ let lftX = pxRound(valToPosX(dataX[lftIdx], scaleX, xDim, xOff));
+ let rgtX = pxRound(valToPosX(dataX[rgtIdx], scaleX, xDim, xOff));
+
+ if (lftX > xOff)
+ addGap(gaps, xOff, lftX);
+
+ for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) {
+ let x = pxRound(valToPosX(dataX[i], scaleX, xDim, xOff));
+
+ if (x == accX) {
+ if (dataY[i] != null) {
+ outY = pxRound(valToPosY(dataY[i], scaleY, yDim, yOff));
+
+ if (minY == inf) {
+ lineTo(stroke, x, outY);
+ inY = outY;
+ }
+
+ minY = min(outY, minY);
+ maxY = max(outY, maxY);
+ }
+ else if (dataY[i] === null)
+ accGaps = prevYNull = true;
+ }
+ else {
+ let _addGap = false;
+
+ if (minY != inf) {
+ drawAcc(stroke, accX, minY, maxY, inY, outY);
+ outX = drawnAtX = accX;
+ }
+ else if (accGaps) {
+ _addGap = true;
+ accGaps = false;
+ }
+
+ if (dataY[i] != null) {
+ outY = pxRound(valToPosY(dataY[i], scaleY, yDim, yOff));
+ lineTo(stroke, x, outY);
+ minY = maxY = inY = outY;
+
+ // prior pixel can have data but still start a gap if ends with null
+ if (prevYNull && x - accX > 1)
+ _addGap = true;
+
+ prevYNull = false;
+ }
+ else {
+ minY = inf;
+ maxY = -inf;
+
+ if (dataY[i] === null) {
+ accGaps = true;
+
+ if (x - accX > 1)
+ _addGap = true;
+ }
+ }
+
+ _addGap && addGap(gaps, outX, x);
+
+ accX = x;
+ }
+ }
+
+ if (minY != inf && minY != maxY && drawnAtX != accX)
+ drawAcc(stroke, accX, minY, maxY, inY, outY);
+
+ if (rgtX < xOff + xDim)
+ addGap(gaps, rgtX, xOff + xDim);
+
+ if (series.fill != null) {
+ let fill = _paths.fill = new Path2D(stroke);
+
+ let fillTo = pxRound(valToPosY(series.fillTo(u, seriesIdx, series.min, series.max), scaleY, yDim, yOff));
+
+ lineTo(fill, rgtX, fillTo);
+ lineTo(fill, lftX, fillTo);
+ }
+
+ _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
+
+ if (!series.spanGaps)
+ _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
+
+ if (u.bands.length > 0) {
+ // ADDL OPT: only create band clips for series that are band lower edges
+ // if (b.series[1] == i && _paths.band == null)
+ _paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);
+ }
+
+ return _paths;
+ });
+ };
+ }
+
+ function stepped(opts) {
+ const align = ifNull(opts.align, 1);
+ // whether to draw ascenders/descenders at null/gap bondaries
+ const ascDesc = ifNull(opts.ascDesc, false);
+
+ return (u, seriesIdx, idx0, idx1) => {
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ let lineTo = scaleX.ori == 0 ? lineToH : lineToV;
+
+ const _paths = {stroke: new Path2D(), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};
+ const stroke = _paths.stroke;
+
+ const _dir = 1 * scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+
+ idx0 = nonNullIdx(dataY, idx0, idx1, 1);
+ idx1 = nonNullIdx(dataY, idx0, idx1, -1);
+
+ let gaps = [];
+ let inGap = false;
+ let prevYPos = pxRound(valToPosY(dataY[_dir == 1 ? idx0 : idx1], scaleY, yDim, yOff));
+ let firstXPos = pxRound(valToPosX(dataX[_dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));
+ let prevXPos = firstXPos;
+
+ lineTo(stroke, firstXPos, prevYPos);
+
+ for (let i = _dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dir) {
+ let yVal1 = dataY[i];
+
+ let x1 = pxRound(valToPosX(dataX[i], scaleX, xDim, xOff));
+
+ if (yVal1 == null) {
+ if (yVal1 === null) {
+ addGap(gaps, prevXPos, x1);
+ inGap = true;
+ }
+ continue;
+ }
+
+ let y1 = pxRound(valToPosY(yVal1, scaleY, yDim, yOff));
+
+ if (inGap) {
+ addGap(gaps, prevXPos, x1);
+ inGap = false;
+ }
+
+ if (align == 1)
+ lineTo(stroke, x1, prevYPos);
+ else
+ lineTo(stroke, prevXPos, y1);
+
+ lineTo(stroke, x1, y1);
+
+ prevYPos = y1;
+ prevXPos = x1;
+ }
+
+ if (series.fill != null) {
+ let fill = _paths.fill = new Path2D(stroke);
+
+ let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
+ let minY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff));
+
+ lineTo(fill, prevXPos, minY);
+ lineTo(fill, firstXPos, minY);
+ }
+
+ _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
+
+ // expand/contract clips for ascenders/descenders
+ let halfStroke = (series.width * pxRatio) / 2;
+ let startsOffset = (ascDesc || align == 1) ? halfStroke : -halfStroke;
+ let endsOffset = (ascDesc || align == -1) ? -halfStroke : halfStroke;
+
+ gaps.forEach(g => {
+ g[0] += startsOffset;
+ g[1] += endsOffset;
+ });
+
+ if (!series.spanGaps)
+ _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
+
+ if (u.bands.length > 0) {
+ // ADDL OPT: only create band clips for series that are band lower edges
+ // if (b.series[1] == i && _paths.band == null)
+ _paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);
+ }
+
+ return _paths;
+ });
+ };
+ }
+
+ function bars(opts) {
+ opts = opts || EMPTY_OBJ;
+ const size = ifNull(opts.size, [0.6, inf, 1]);
+ const align = opts.align || 0;
+ const extraGap = (opts.gap || 0) * pxRatio;
+
+ const radius = ifNull(opts.radius, 0);
+
+ const gapFactor = 1 - size[0];
+ const maxWidth = ifNull(size[1], inf) * pxRatio;
+ const minWidth = ifNull(size[2], 1) * pxRatio;
+
+ const disp = ifNull(opts.disp, EMPTY_OBJ);
+ const _each = ifNull(opts.each, _ => {});
+
+ const { fill: dispFills, stroke: dispStrokes } = disp;
+
+ return (u, seriesIdx, idx0, idx1) => {
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ const _dirX = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+ const _dirY = scaleY.dir * (scaleY.ori == 1 ? 1 : -1);
+
+ let rect = scaleX.ori == 0 ? rectH : rectV;
+
+ let each = scaleX.ori == 0 ? _each : (u, seriesIdx, i, top, lft, hgt, wid) => {
+ _each(u, seriesIdx, i, lft, top, wid, hgt);
+ };
+
+ let fillToY = series.fillTo(u, seriesIdx, series.min, series.max);
+
+ let y0Pos = valToPosY(fillToY, scaleY, yDim, yOff);
+
+ // barWid is to center of stroke
+ let xShift, barWid;
+
+ let strokeWidth = pxRound(series.width * pxRatio);
+
+ let multiPath = false;
+
+ let fillColors = null;
+ let fillPaths = null;
+ let strokeColors = null;
+ let strokePaths = null;
+
+ if (dispFills != null && dispStrokes != null) {
+ multiPath = true;
+
+ fillColors = dispFills.values(u, seriesIdx, idx0, idx1);
+ fillPaths = new Map();
+ (new Set(fillColors)).forEach(color => {
+ if (color != null)
+ fillPaths.set(color, new Path2D());
+ });
+
+ strokeColors = dispStrokes.values(u, seriesIdx, idx0, idx1);
+ strokePaths = new Map();
+ (new Set(strokeColors)).forEach(color => {
+ if (color != null)
+ strokePaths.set(color, new Path2D());
+ });
+ }
+
+ let { x0, size } = disp;
+
+ if (x0 != null && size != null) {
+ dataX = x0.values(u, seriesIdx, idx0, idx1);
+
+ if (x0.unit == 2)
+ dataX = dataX.map(pct => u.posToVal(xOff + pct * xDim, scaleX.key, true));
+
+ // assumes uniform sizes, for now
+ let sizes = size.values(u, seriesIdx, idx0, idx1);
+
+ if (size.unit == 2)
+ barWid = sizes[0] * xDim;
+ else
+ barWid = valToPosX(sizes[0], scaleX, xDim, xOff) - valToPosX(0, scaleX, xDim, xOff); // assumes linear scale (delta from 0)
+
+ barWid = pxRound(barWid - strokeWidth);
+
+ xShift = (_dirX == 1 ? -strokeWidth / 2 : barWid + strokeWidth / 2);
+ }
+ else {
+ let colWid = xDim;
+
+ if (dataX.length > 1) {
+ // prior index with non-undefined y data
+ let prevIdx = null;
+
+ // scan full dataset for smallest adjacent delta
+ // will not work properly for non-linear x scales, since does not do expensive valToPosX calcs till end
+ for (let i = 0, minDelta = Infinity; i < dataX.length; i++) {
+ if (dataY[i] !== undefined) {
+ if (prevIdx != null) {
+ let delta = abs(dataX[i] - dataX[prevIdx]);
+
+ if (delta < minDelta) {
+ minDelta = delta;
+ colWid = abs(valToPosX(dataX[i], scaleX, xDim, xOff) - valToPosX(dataX[prevIdx], scaleX, xDim, xOff));
+ }
+ }
+
+ prevIdx = i;
+ }
+ }
+ }
+
+ let gapWid = colWid * gapFactor;
+
+ barWid = pxRound(min(maxWidth, max(minWidth, colWid - gapWid)) - strokeWidth - extraGap);
+
+ xShift = (align == 0 ? barWid / 2 : align == _dirX ? 0 : barWid) - align * _dirX * extraGap / 2;
+ }
+
+ const _paths = {stroke: null, fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL | BAND_CLIP_STROKE}; // disp, geom
+
+ const hasBands = u.bands.length > 0;
+ let yLimit;
+
+ if (hasBands) {
+ // ADDL OPT: only create band clips for series that are band lower edges
+ // if (b.series[1] == i && _paths.band == null)
+ _paths.band = new Path2D();
+ yLimit = pxRound(valToPosY(scaleY.max, scaleY, yDim, yOff));
+ }
+
+ const stroke = multiPath ? null : new Path2D();
+ const band = _paths.band;
+
+ for (let i = _dirX == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dirX) {
+ let yVal = dataY[i];
+
+ /*
+ // interpolate upwards band clips
+ if (yVal == null) {
+ // if (hasBands)
+ // yVal = costlyLerp(i, idx0, idx1, _dirX, dataY);
+ // else
+ continue;
+ }
+ */
+
+ let xVal = scaleX.distr != 2 || disp != null ? dataX[i] : i;
+
+ // TODO: all xPos can be pre-computed once for all series in aligned set
+ let xPos = valToPosX(xVal, scaleX, xDim, xOff);
+ let yPos = valToPosY(ifNull(yVal, fillToY) , scaleY, yDim, yOff);
+
+ let lft = pxRound(xPos - xShift);
+ let btm = pxRound(max(yPos, y0Pos));
+ let top = pxRound(min(yPos, y0Pos));
+ // this includes the stroke
+ let barHgt = btm - top;
+
+ let r = radius * barWid;
+
+ if (yVal != null) { // && yVal != fillToY (0 height bar)
+ if (multiPath) {
+ if (strokeWidth > 0 && strokeColors[i] != null)
+ rect(strokePaths.get(strokeColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);
+
+ if (fillColors[i] != null)
+ rect(fillPaths.get(fillColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);
+ }
+ else
+ rect(stroke, lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);
+
+ each(u, seriesIdx, i,
+ lft - strokeWidth / 2,
+ top,
+ barWid + strokeWidth,
+ barHgt,
+ );
+ }
+
+ if (hasBands) {
+ if (_dirY == 1) {
+ btm = top;
+ top = yLimit;
+ }
+ else {
+ top = btm;
+ btm = yLimit;
+ }
+
+ barHgt = btm - top;
+
+ rect(band, lft - strokeWidth / 2, top, barWid + strokeWidth, max(0, barHgt), 0);
+ }
+ }
+
+ if (strokeWidth > 0)
+ _paths.stroke = multiPath ? strokePaths : stroke;
+
+ _paths.fill = multiPath ? fillPaths : stroke;
+
+ return _paths;
+ });
+ };
+ }
+
+ function splineInterp(interp, opts) {
+ return (u, seriesIdx, idx0, idx1) => {
+ return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
+ let pxRound = series.pxRound;
+
+ let moveTo, bezierCurveTo, lineTo;
+
+ if (scaleX.ori == 0) {
+ moveTo = moveToH;
+ lineTo = lineToH;
+ bezierCurveTo = bezierCurveToH;
+ }
+ else {
+ moveTo = moveToV;
+ lineTo = lineToV;
+ bezierCurveTo = bezierCurveToV;
+ }
+
+ const _dir = 1 * scaleX.dir * (scaleX.ori == 0 ? 1 : -1);
+
+ idx0 = nonNullIdx(dataY, idx0, idx1, 1);
+ idx1 = nonNullIdx(dataY, idx0, idx1, -1);
+
+ let gaps = [];
+ let inGap = false;
+ let firstXPos = pxRound(valToPosX(dataX[_dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));
+ let prevXPos = firstXPos;
+
+ let xCoords = [];
+ let yCoords = [];
+
+ for (let i = _dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dir) {
+ let yVal = dataY[i];
+ let xVal = dataX[i];
+ let xPos = valToPosX(xVal, scaleX, xDim, xOff);
+
+ if (yVal == null) {
+ if (yVal === null) {
+ addGap(gaps, prevXPos, xPos);
+ inGap = true;
+ }
+ continue;
+ }
+ else {
+ if (inGap) {
+ addGap(gaps, prevXPos, xPos);
+ inGap = false;
+ }
+
+ xCoords.push((prevXPos = xPos));
+ yCoords.push(valToPosY(dataY[i], scaleY, yDim, yOff));
+ }
+ }
+
+ const _paths = {stroke: interp(xCoords, yCoords, moveTo, lineTo, bezierCurveTo, pxRound), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};
+ const stroke = _paths.stroke;
+
+ if (series.fill != null && stroke != null) {
+ let fill = _paths.fill = new Path2D(stroke);
+
+ let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
+ let minY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff));
+
+ lineTo(fill, prevXPos, minY);
+ lineTo(fill, firstXPos, minY);
+ }
+
+ _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);
+
+ if (!series.spanGaps)
+ _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);
+
+ if (u.bands.length > 0) {
+ // ADDL OPT: only create band clips for series that are band lower edges
+ // if (b.series[1] == i && _paths.band == null)
+ _paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);
+ }
+
+ return _paths;
+
+ // if FEAT_PATHS: false in rollup.config.js
+ // u.ctx.save();
+ // u.ctx.beginPath();
+ // u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
+ // u.ctx.clip();
+ // u.ctx.strokeStyle = u.series[sidx].stroke;
+ // u.ctx.stroke(stroke);
+ // u.ctx.fillStyle = u.series[sidx].fill;
+ // u.ctx.fill(fill);
+ // u.ctx.restore();
+ // return null;
+ });
+ };
+ }
+
+ function monotoneCubic(opts) {
+ return splineInterp(_monotoneCubic);
+ }
+
+ // Monotone Cubic Spline interpolation, adapted from the Chartist.js implementation:
+ // https://github.com/gionkunz/chartist-js/blob/e7e78201bffe9609915e5e53cfafa29a5d6c49f9/src/scripts/interpolation.js#L240-L369
+ function _monotoneCubic(xs, ys, moveTo, lineTo, bezierCurveTo, pxRound) {
+ const n = xs.length;
+
+ if (n < 2)
+ return null;
+
+ const path = new Path2D();
+
+ moveTo(path, xs[0], ys[0]);
+
+ if (n == 2)
+ lineTo(path, xs[1], ys[1]);
+ else {
+ let ms = Array(n),
+ ds = Array(n - 1),
+ dys = Array(n - 1),
+ dxs = Array(n - 1);
+
+ // calc deltas and derivative
+ for (let i = 0; i < n - 1; i++) {
+ dys[i] = ys[i + 1] - ys[i];
+ dxs[i] = xs[i + 1] - xs[i];
+ ds[i] = dys[i] / dxs[i];
+ }
+
+ // determine desired slope (m) at each point using Fritsch-Carlson method
+ // http://math.stackexchange.com/questions/45218/implementation-of-monotone-cubic-interpolation
+ ms[0] = ds[0];
+
+ for (let i = 1; i < n - 1; i++) {
+ if (ds[i] === 0 || ds[i - 1] === 0 || (ds[i - 1] > 0) !== (ds[i] > 0))
+ ms[i] = 0;
+ else {
+ ms[i] = 3 * (dxs[i - 1] + dxs[i]) / (
+ (2 * dxs[i] + dxs[i - 1]) / ds[i - 1] +
+ (dxs[i] + 2 * dxs[i - 1]) / ds[i]
+ );
+
+ if (!isFinite(ms[i]))
+ ms[i] = 0;
+ }
+ }
+
+ ms[n - 1] = ds[n - 2];
+
+ for (let i = 0; i < n - 1; i++) {
+ bezierCurveTo(
+ path,
+ xs[i] + dxs[i] / 3,
+ ys[i] + ms[i] * dxs[i] / 3,
+ xs[i + 1] - dxs[i] / 3,
+ ys[i + 1] - ms[i + 1] * dxs[i] / 3,
+ xs[i + 1],
+ ys[i + 1],
+ );
+ }
+ }
+
+ return path;
+ }
+
+ const cursorPlots = new Set();
+
+ function invalidateRects() {
+ cursorPlots.forEach(u => {
+ u.syncRect(true);
+ });
+ }
+
+ on(resize, win, invalidateRects);
+ on(scroll, win, invalidateRects, true);
+
+ const linearPath = linear() ;
+ const pointsPath = points() ;
+
+ 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;
+ });
+ }
+ }
+
+ 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 = 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;
+
+ if (scaleX.ori == 0) {
+ addClass(root, ORI_HZ);
+ valToPosX = getHPos;
+ valToPosY = getVPos;
+ /*
+ 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;
+ /*
+ 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 = (opts.tzDate || (ts => new Date(round(ts / ms))));
+ const _fmtDate = (opts.fmtDate || fmtDate);
+
+ const _timeAxisSplits = (ms == 1 ? timeAxisSplitsMs(_tzDate) : timeAxisSplitsS(_tzDate));
+ const _timeAxisVals = timeAxisVals(_tzDate, timeAxisStamps((ms == 1 ? _timeAxisStampsMs : _timeAxisStampsS), _fmtDate));
+ const _timeSeriesVal = timeSeriesVal(_tzDate, timeSeriesStamp(_timeSeriesStamp, _fmtDate));
+
+ const activeIdxs = [];
+
+ const legend = (self.legend = assign({}, legendOpts, opts.legend));
+ const showLegend = legend.show;
+ const markers = legend.markers;
+
+ {
+ 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 (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 (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, syncOpts.setSeries);
+ });
+ }
+ else
+ setSeries(seriesIdx, {show: !s.show}, true, 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 = (self.cursor = assign({}, cursorOpts, {drag: {y: mode == 2}}, opts.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}, cursor.focus);
+ const cursorFocus = 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 = 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 (cursor.show) {
+ activeIdxs.splice(i, 0, null);
+
+ let pt = initCursorPt(s, i);
+ pt && cursorPts.splice(i, 0, pt);
+ }
+ }
+
+ 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 (cursor.show) {
+ activeIdxs.splice(i, 1);
+
+ cursorPts.length > 1 && cursorPts.splice(i, 1)[0].remove();
+ }
+
+ // TODO: de-init no-longer-needed scales?
+ }
+
+ 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 = 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 (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;
+
+ {
+ cacheStrokeFill(i, false);
+ s._paths && drawPath(i, false);
+ }
+
+ {
+ 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(a => {
+ let { _show, _el, _size, _pos, side } = a;
+
+ 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);
+
+ _el && remClass(_el, OFF);
+ }
+ else
+ _el && 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 (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 = cursor.drag;
+
+ let dragX = drag.x;
+ let dragY = drag.y;
+
+ if (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);
+ 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;
+ toggleDOM(i, opts.show);
+
+ _setScale(mode == 2 ? s.facets[1].scale : s.scale, null, null);
+ commit();
+ }
+
+ _fire !== false && fire("setSeries", i, opts);
+
+ _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 (cursor.show && cursorPts[i])
+ cursorPts[i].style.opacity = value;
+
+ if (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;
+
+ (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) {
+ cursorPts.length > 1 && elTrans(cursorPts[i], -10, -10, plotWidCss, plotHgtCss);
+ }
+ }
+
+ if (cursorFocus)
+ setSeries(null, FOCUS_TRUE, true, src == null && syncOpts.setSeries);
+
+ if (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 (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 (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);
+ 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 (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 = 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);
+
+ (cursor.sync = syncOpts);
+
+ const syncKey = syncOpts.key;
+
+ const sync = _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);
+ }
+
+ 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);
+ }
+
+ (self.pub = pub);
+
+ function destroy() {
+ sync.unsub(self);
+ 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;
+
+ {
+ uPlot.join = join;
+ }
+
+ {
+ uPlot.fmtDate = fmtDate;
+ uPlot.tzDate = tzDate;
+ }
+
+ {
+ uPlot.sync = _sync;
+ }
+
+ {
+ uPlot.addGap = addGap;
+ uPlot.clipGaps = clipGaps;
+
+ let paths = uPlot.paths = {
+ points,
+ };
+
+ (paths.linear = linear);
+ (paths.stepped = stepped);
+ (paths.bars = bars);
+ (paths.spline = monotoneCubic);
+ }
+
+ return uPlot;
+
+})();
diff --git a/build/resources/main/static/plugins/uplot/uPlot.iife.min.js b/build/resources/main/static/plugins/uplot/uPlot.iife.min.js
new file mode 100644
index 0000000..0403c32
--- /dev/null
+++ b/build/resources/main/static/plugins/uplot/uPlot.iife.min.js
@@ -0,0 +1,2 @@
+/*! https://github.com/leeoniya/uPlot (v1.6.18) */
+var uPlot=function(){"use strict";function e(e,t,l,n){let i;l=l||0;let o=2147483647>=(n=n||t.length-1);for(;n-l>1;)i=o?l+n>>1:g((l+n)/2),e>t[i]?l=i:n=i;return e-t[l]>t[n]-e?n:l}function t(e,t,l,n){for(let i=1==n?t:l;i>=t&&l>=i;i+=n)if(null!=e[i])return i;return-1}const l=[0,0];function n(e,t,n,i){return l[0]=0>n?L(e,-n):e,l[1]=0>i?L(t,-i):t,l}function i(e,t,l,i){let o,s,r,u=v(e),a=10==l?y:M;return e==t&&(-1==u?(e*=l,t/=l):(e/=l,t*=l)),i?(o=g(a(e)),s=w(a(t)),r=n(k(l,o),k(l,s),o,s),e=r[0],t=r[1]):(o=g(a(m(e))),s=g(a(m(t))),r=n(k(l,o),k(l,s),o,s),e=R(e,r[0]),t=H(t,r[1])),[e,t]}function o(e,t,l,n){let o=i(e,t,l,n);return 0==e&&(o[0]=0),0==t&&(o[1]=0),o}const s={mode:3,pad:.1},r={pad:0,soft:null,mode:0},u={min:r,max:r};function a(e,t,l,n){return J(l)?f(e,t,l):(r.pad=l,r.soft=n?0:null,r.mode=n?3:0,f(e,t,u))}function c(e,t){return null==e?t:e}function f(e,t,l){let n=l.min,i=l.max,o=c(n.pad,0),s=c(i.pad,0),r=c(n.hard,-E),u=c(i.hard,E),a=c(n.soft,E),f=c(i.soft,-E),h=c(n.mode,0),d=c(i.mode,0),p=t-e;1e-9>p&&(p=0,0!=e&&0!=t||(p=1e-9,2==h&&a!=E&&(o=0),2==d&&f!=-E&&(s=0)));let x=p||m(t)||1e3,w=y(x),v=k(10,g(w)),M=L(R(e-x*(0==p?0==e?.1:1:o),v/10),9),S=a>e||1!=h&&(3!=h||M>a)&&(2!=h||a>M)?E:a,D=b(r,S>M&&e>=S?S:_(S,M)),T=L(H(t+x*(0==p?0==t?.1:1:s),v/10),9),z=t>f||1!=d&&(3!=d||f>T)&&(2!=d||T>f)?-E:f,P=_(u,T>z&&z>=t?z:b(z,T));return D==P&&0==D&&(P=100),[D,P]}const h=new Intl.NumberFormat(navigator.language).format,d=Math,p=d.PI,m=d.abs,g=d.floor,x=d.round,w=d.ceil,_=d.min,b=d.max,k=d.pow,v=d.sign,y=d.log10,M=d.log2,S=(e,t=1)=>d.asinh(e/t),E=1/0;function D(e){return 1+(0|y((e^e>>31)-(e>>31)))}function T(e,t){return x(e/t)*t}function z(e,t,l){return _(b(e,t),l)}function P(e){return"function"==typeof e?e:()=>e}const A=e=>e,W=(e,t)=>t,Y=()=>null,C=()=>!0,F=(e,t)=>e==t;function H(e,t){return w(e/t)*t}function R(e,t){return g(e/t)*t}function L(e,t){return x(e*(t=10**t))/t}const I=new Map;function G(e){return((""+e).split(".")[1]||"").length}function O(e,t,l,n){let i=[],o=n.map(G);for(let s=t;l>s;s++){let t=m(s),l=L(k(e,s),t);for(let e=0;n.length>e;e++){let r=n[e]*l,u=(0>r||0>s?t:0)+(o[e]>s?o[e]:0),a=L(r,u);i.push(a),I.set(a,u)}}return i}const N={},j=[],B=[null,null],V=Array.isArray;function U(e){return"string"==typeof e}function J(e){let t=!1;if(null!=e){let l=e.constructor;t=null==l||l==Object}return t}function q(e){return null!=e&&"object"==typeof e}function K(e,t=J){let l;if(V(e)){let n=e.find((e=>null!=e));if(V(n)||t(n)){l=Array(e.length);for(let n=0;e.length>n;n++)l[n]=K(e[n],t)}else l=e.slice()}else if(t(e)){l={};for(let n in e)l[n]=K(e[n],t)}else l=e;return l}function Z(e){let t=arguments;for(let l=1;t.length>l;l++){let n=t[l];for(let t in n)J(e[t])?Z(e[t],K(n[t])):e[t]=K(n[t])}return e}function $(e,t,l){for(let n,i=0,o=-1;t.length>i;i++){let s=t[i];if(s>o){for(n=s-1;n>=0&&null==e[n];)e[n--]=null;for(n=s+1;l>n&&null==e[n];)e[o=n++]=null}}}const X="undefined"==typeof queueMicrotask?e=>Promise.resolve().then(e):queueMicrotask,Q="width",ee="height",te="top",le="bottom",ne="left",ie="right",oe="#000",se="mousemove",re="mousedown",ue="mouseup",ae="mouseenter",ce="mouseleave",fe="dblclick",he="change",de="dppxchange",pe="u-off",me="u-label",ge=document,xe=window;let we,_e;function be(e,t){if(null!=t){let l=e.classList;!l.contains(t)&&l.add(t)}}function ke(e,t){let l=e.classList;l.contains(t)&&l.remove(t)}function ve(e,t,l){e.style[t]=l+"px"}function ye(e,t,l,n){let i=ge.createElement(e);return null!=t&&be(i,t),null!=l&&l.insertBefore(i,n),i}function Me(e,t){return ye("div",e,t)}const Se=new WeakMap;function Ee(e,t,l,n,i){let o="translate("+t+"px,"+l+"px)";o!=Se.get(e)&&(e.style.transform=o,Se.set(e,o),0>t||0>l||t>n||l>i?be(e,pe):ke(e,pe))}const De=new WeakMap;function Te(e,t,l){let n=t+l;n!=De.get(e)&&(De.set(e,n),e.style.background=t,e.style.borderColor=l)}const ze=new WeakMap;function Pe(e,t,l,n){let i=t+""+l;i!=ze.get(e)&&(ze.set(e,i),e.style.height=l+"px",e.style.width=t+"px",e.style.marginLeft=n?-t/2+"px":0,e.style.marginTop=n?-l/2+"px":0)}const Ae={passive:!0},We=Z({capture:!0},Ae);function Ye(e,t,l,n){t.addEventListener(e,l,n?We:Ae)}function Ce(e,t,l,n){t.removeEventListener(e,l,n?We:Ae)}!function e(){let t=devicePixelRatio;we!=t&&(we=t,_e&&Ce(he,_e,e),_e=matchMedia(`(min-resolution: ${we-.001}dppx) and (max-resolution: ${we+.001}dppx)`),Ye(he,_e,e),xe.dispatchEvent(new CustomEvent(de)))}();const Fe=["January","February","March","April","May","June","July","August","September","October","November","December"],He=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];function Re(e){return e.slice(0,3)}const Le=He.map(Re),Ie=Fe.map(Re),Ge={MMMM:Fe,MMM:Ie,WWWW:He,WWW:Le};function Oe(e){return(10>e?"0":"")+e}const Ne={YYYY:e=>e.getFullYear(),YY:e=>(e.getFullYear()+"").slice(2),MMMM:(e,t)=>t.MMMM[e.getMonth()],MMM:(e,t)=>t.MMM[e.getMonth()],MM:e=>Oe(e.getMonth()+1),M:e=>e.getMonth()+1,DD:e=>Oe(e.getDate()),D:e=>e.getDate(),WWWW:(e,t)=>t.WWWW[e.getDay()],WWW:(e,t)=>t.WWW[e.getDay()],HH:e=>Oe(e.getHours()),H:e=>e.getHours(),h:e=>{let t=e.getHours();return 0==t?12:t>12?t-12:t},AA:e=>12>e.getHours()?"AM":"PM",aa:e=>12>e.getHours()?"am":"pm",a:e=>12>e.getHours()?"a":"p",mm:e=>Oe(e.getMinutes()),m:e=>e.getMinutes(),ss:e=>Oe(e.getSeconds()),s:e=>e.getSeconds(),fff:e=>function(e){return(10>e?"00":100>e?"0":"")+e}(e.getMilliseconds())};function je(e,t){t=t||Ge;let l,n=[],i=/\{([a-z]+)\}|[^{]+/gi;for(;l=i.exec(e);)n.push("{"==l[0][0]?Ne[l[1]]:l[0]);return e=>{let l="";for(let i=0;n.length>i;i++)l+="string"==typeof n[i]?n[i]:n[i](e,t);return l}}const Be=(new Intl.DateTimeFormat).resolvedOptions().timeZone,Ve=e=>e%1==0,Ue=[1,2,2.5,5],Je=O(10,-16,0,Ue),qe=O(10,0,16,Ue),Ke=qe.filter(Ve),Ze=Je.concat(qe),$e="{YYYY}",Xe="\n"+$e,Qe="{M}/{D}",et="\n"+Qe,tt=et+"/{YY}",lt="{aa}",nt="{h}:{mm}"+lt,it="\n"+nt,ot=":{ss}",st=null;function rt(e){let t=1e3*e,l=60*t,n=60*l,i=24*n,o=30*i,s=365*i;return[(1==e?O(10,0,3,Ue).filter(Ve):O(10,-3,0,Ue)).concat([t,5*t,10*t,15*t,30*t,l,5*l,10*l,15*l,30*l,n,2*n,3*n,4*n,6*n,8*n,12*n,i,2*i,3*i,4*i,5*i,6*i,7*i,8*i,9*i,10*i,15*i,o,2*o,3*o,4*o,6*o,s,2*s,5*s,10*s,25*s,50*s,100*s]),[[s,$e,st,st,st,st,st,st,1],[28*i,"{MMM}",Xe,st,st,st,st,st,1],[i,Qe,Xe,st,st,st,st,st,1],[n,"{h}"+lt,tt,st,et,st,st,st,1],[l,nt,tt,st,et,st,st,st,1],[t,ot,tt+" "+nt,st,et+" "+nt,st,it,st,1],[e,ot+".{fff}",tt+" "+nt,st,et+" "+nt,st,it,st,1]],function(t){return(r,u,a,c,f,h)=>{let d=[],p=f>=s,m=f>=o&&s>f,w=t(a),_=L(w*e,3),b=gt(w.getFullYear(),p?0:w.getMonth(),m||p?1:w.getDate()),k=L(b*e,3);if(m||p){let l=m?f/o:0,n=p?f/s:0,i=_==k?_:L(gt(b.getFullYear()+n,b.getMonth()+l,1)*e,3),r=new Date(x(i/e)),u=r.getFullYear(),a=r.getMonth();for(let o=0;c>=i;o++){let s=gt(u+n*o,a+l*o,1),r=s-t(L(s*e,3));i=L((+s+r)*e,3),i>c||d.push(i)}}else{let o=i>f?f:i,s=k+(g(a)-g(_))+H(_-k,o);d.push(s);let p=t(s),m=p.getHours()+p.getMinutes()/l+p.getSeconds()/n,x=f/n,w=h/r.axes[u]._space;for(;s=L(s+f,1==e?0:3),c>=s;)if(x>1){let e=g(L(m+x,6))%24,l=t(s).getHours()-e;l>1&&(l=-1),s-=l*n,m=(m+x)%24,.7>L((s-d[d.length-1])/f,3)*w||d.push(s)}else d.push(s)}return d}}]}const[ut,at,ct]=rt(1),[ft,ht,dt]=rt(.001);function pt(e,t){return e.map((e=>e.map(((l,n)=>0==n||8==n||null==l?l:t(1==n||0==e[8]?l:e[1]+l)))))}function mt(e,t){return(l,n,i,o,s)=>{let r,u,a,c,f,h,d=t.find((e=>s>=e[0]))||t[t.length-1];return n.map((t=>{let l=e(t),n=l.getFullYear(),i=l.getMonth(),o=l.getDate(),s=l.getHours(),p=l.getMinutes(),m=l.getSeconds(),g=n!=r&&d[2]||i!=u&&d[3]||o!=a&&d[4]||s!=c&&d[5]||p!=f&&d[6]||m!=h&&d[7]||d[1];return r=n,u=i,a=o,c=s,f=p,h=m,g(l)}))}}function gt(e,t,l){return new Date(e,t,l)}function xt(e,t){return t(e)}function wt(e,t){return(l,n)=>t(e(n))}O(2,-53,53,[1]);const _t={show:!0,live:!0,isolate:!1,markers:{show:!0,width:2,stroke:function(e,t){let l=e.series[t];return l.width?l.stroke(e,t):l.points.width?l.points.stroke(e,t):null},fill:function(e,t){return e.series[t].fill(e,t)},dash:"solid"},idx:null,idxs:null,values:[]},bt=[0,0];function kt(e,t,l){return e=>{0==e.button&&l(e)}}function vt(e,t,l){return l}const yt={show:!0,x:!0,y:!0,lock:!1,move:function(e,t,l){return bt[0]=t,bt[1]=l,bt},points:{show:function(e,t){let l=e.cursor.points,n=Me(),i=l.size(e,t);ve(n,Q,i),ve(n,ee,i);let o=i/-2;ve(n,"marginLeft",o),ve(n,"marginTop",o);let s=l.width(e,t,i);return s&&ve(n,"borderWidth",s),n},size:function(e,t){return Ot(e.series[t].points.width,1)},width:0,stroke:function(e,t){let l=e.series[t].points;return l._stroke||l._fill},fill:function(e,t){let l=e.series[t].points;return l._fill||l._stroke}},bind:{mousedown:kt,mouseup:kt,click:kt,dblclick:kt,mousemove:vt,mouseleave:vt,mouseenter:vt},drag:{setScale:!0,x:!0,y:!1,dist:0,uni:null,_x:!1,_y:!1},focus:{prox:-1},left:-10,top:-10,idx:null,dataIdx:function(e,t,l){return l},idxs:null},Mt={show:!0,stroke:"rgba(0,0,0,0.07)",width:2,filter:W},St=Z({},Mt,{size:10}),Et='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"',Dt="bold "+Et,Tt={show:!0,scale:"x",stroke:oe,space:50,gap:5,size:50,labelGap:0,labelSize:30,labelFont:Dt,side:2,grid:Mt,ticks:St,font:Et,rotate:0},zt={show:!0,scale:"x",auto:!1,sorted:1,min:E,max:-E,idxs:[]};function Pt(e,t){return t.map((e=>null==e?"":h(e)))}function At(e,t,l,n,i,o,s){let r=[],u=I.get(i)||0;for(let e=l=s?l:L(H(l,i),u);n>=e;e=L(e+i,u))r.push(Object.is(e,-0)?0:e);return r}function Wt(e,t,l,n,i){const o=[],s=e.scales[e.axes[t].scale].log,r=g((10==s?y:M)(l));i=k(s,r),0>r&&(i=L(i,-r));let u=l;do{o.push(u),u=L(u+i,I.get(i)),i*s>u||(i=u)}while(n>=u);return o}function Yt(e,t,l,n,i){let o=e.scales[e.axes[t].scale].asinh,s=n>o?Wt(e,t,b(o,l),n,i):[o],r=0>n||l>0?[]:[0];return(-o>l?Wt(e,t,b(o,-n),-l,i):[o]).reverse().map((e=>-e)).concat(r,s)}const Ct=/./,Ft=/[12357]/,Ht=/[125]/,Rt=/1/;function Lt(e,t,l){let n=e.axes[l],i=n.scale,o=e.scales[i];if(3==o.distr&&2==o.log)return t;let s=e.valToPos,r=n._space,u=s(10,i),a=s(9,i)-u<r?s(7,i)-u<r?s(5,i)-u<r?Rt:Ht:Ft:Ct;return t.map((e=>4==o.distr&&0==e||a.test(e)?e:null))}function It(e,t){return null==t?"":h(t)}const Gt={show:!0,scale:"y",stroke:oe,space:30,gap:5,size:50,labelGap:0,labelSize:30,labelFont:Dt,side:3,grid:Mt,ticks:St,font:Et,rotate:0};function Ot(e,t){return L((3+2*(e||1))*t,3)}function Nt(e,t){let l=e.scales[e.series[t].scale],n=e.bands&&e.bands.some((e=>e.series[0]==t));return 3==l.distr||n?l.min:0}const jt={scale:null,auto:!0,min:E,max:-E},Bt={show:!0,auto:!0,sorted:0,alpha:1,facets:[Z({},jt,{scale:"x"}),Z({},jt,{scale:"y"})]},Vt={scale:"y",auto:!0,sorted:0,show:!0,spanGaps:!1,gaps:(e,t,l,n,i)=>i,alpha:1,points:{show:function(e,t){let{scale:l,idxs:n}=e.series[0],i=e._data[0],o=e.valToPos(i[n[0]],l,!0),s=e.valToPos(i[n[1]],l,!0);return m(s-o)/(e.series[t].points.space*we)>=n[1]-n[0]},filter:null},values:null,min:E,max:-E,idxs:[],path:null,clip:null};function Ut(e,t,l){return l/10}const Jt={time:!0,auto:!0,distr:1,log:10,asinh:1,min:null,max:null,dir:1,ori:0},qt=Z({},Jt,{time:!1,ori:1}),Kt={};function Zt(e){let t=Kt[e];return t||(t={key:e,plots:[],sub(e){t.plots.push(e)},unsub(e){t.plots=t.plots.filter((t=>t!=e))},pub(e,l,n,i,o,s,r){for(let u=0;t.plots.length>u;u++)t.plots[u]!=l&&t.plots[u].pub(e,l,n,i,o,s,r)}},null!=e&&(Kt[e]=t)),t}function $t(e,t,l){const n=e.series[t],i=e.scales,o=e.bbox;let s=e._data[0],r=e._data[t],u=2==e.mode?i[n.facets[0].scale]:i[e.series[0].scale],a=2==e.mode?i[n.facets[1].scale]:i[n.scale],c=o.left,f=o.top,h=o.width,d=o.height,p=e.valToPosH,m=e.valToPosV;return 0==u.ori?l(n,s,r,u,a,p,m,c,f,h,d,nl,ol,rl,al,fl):l(n,s,r,u,a,m,p,f,c,d,h,il,sl,ul,cl,hl)}function Xt(e,t,l,n,i){return $t(e,t,((e,t,o,s,r,u,a,c,f,h,d)=>{let p=e.pxRound;const m=0==s.ori?ol:sl;let g,x;1==s.dir*(0==s.ori?1:-1)?(g=l,x=n):(g=n,x=l);let w=p(u(t[g],s,h,c)),_=p(a(o[g],r,d,f)),b=p(u(t[x],s,h,c)),k=p(a(r.max,r,d,f)),v=new Path2D(i);return m(v,b,k),m(v,w,k),m(v,w,_),v}))}function Qt(e,t,l,n,i,o){let s=null;if(e.length>0){s=new Path2D;const r=0==t?rl:ul;let u=l;for(let t=0;e.length>t;t++){let l=e[t];if(l[1]>l[0]){let e=l[0]-u;e>0&&r(s,u,n,e,n+o),u=l[1]}}let a=l+i-u;a>0&&r(s,u,n,a,n+o)}return s}function el(e,t,l){let n=e[e.length-1];n&&n[0]==t?n[1]=l:e.push([t,l])}function tl(e){return 0==e?A:1==e?x:t=>T(t,e)}function ll(e){let t=0==e?nl:il,l=0==e?(e,t,l,n,i,o)=>{e.arcTo(t,l,n,i,o)}:(e,t,l,n,i,o)=>{e.arcTo(l,t,i,n,o)},n=0==e?(e,t,l,n,i)=>{e.rect(t,l,n,i)}:(e,t,l,n,i)=>{e.rect(l,t,i,n)};return(e,i,o,s,r,u=0)=>{0==u?n(e,i,o,s,r):(u=_(u,s/2,r/2),t(e,i+u,o),l(e,i+s,o,i+s,o+r,u),l(e,i+s,o+r,i,o+r,u),l(e,i,o+r,i,o,u),l(e,i,o,i+s,o,u),e.closePath())}}const nl=(e,t,l)=>{e.moveTo(t,l)},il=(e,t,l)=>{e.moveTo(l,t)},ol=(e,t,l)=>{e.lineTo(t,l)},sl=(e,t,l)=>{e.lineTo(l,t)},rl=ll(0),ul=ll(1),al=(e,t,l,n,i,o)=>{e.arc(t,l,n,i,o)},cl=(e,t,l,n,i,o)=>{e.arc(l,t,n,i,o)},fl=(e,t,l,n,i,o,s)=>{e.bezierCurveTo(t,l,n,i,o,s)},hl=(e,t,l,n,i,o,s)=>{e.bezierCurveTo(l,t,i,n,s,o)};function dl(){return(e,t,l,n,i)=>$t(e,t,((t,o,s,r,u,a,c,f,h,d,m)=>{let g,x,{pxRound:w,points:_}=t;0==r.ori?(g=nl,x=al):(g=il,x=cl);const b=L(_.width*we,3);let k=(_.size-_.width)/2*we,v=L(2*k,3),y=new Path2D,M=new Path2D,{left:S,top:E,width:D,height:T}=e.bbox;rl(M,S-v,E-v,D+2*v,T+2*v);const z=e=>{if(null!=s[e]){let t=w(a(o[e],r,d,f)),l=w(c(s[e],u,m,h));g(y,t+k,l),x(y,t,l,k,0,2*p)}};if(i)i.forEach(z);else for(let e=l;n>=e;e++)z(e);return{stroke:b>0?y:null,fill:y,clip:M,flags:3}}))}function pl(e){return(t,l,n,i,o,s)=>{n!=i&&(o!=n&&s!=n&&e(t,l,n),o!=i&&s!=i&&e(t,l,i),e(t,l,s))}}const ml=pl(ol),gl=pl(sl);function xl(){return(e,l,n,i)=>$t(e,l,((o,s,r,u,a,c,f,h,d,p,m)=>{let g,x,w=o.pxRound;0==u.ori?(g=ol,x=ml):(g=sl,x=gl);const k=u.dir*(0==u.ori?1:-1),v={stroke:new Path2D,fill:null,clip:null,band:null,gaps:null,flags:1},y=v.stroke;let M,S,D,T,z=E,P=-E,A=[],W=w(c(s[1==k?n:i],u,p,h)),Y=!1,C=!1,F=t(r,n,i,1*k),H=t(r,n,i,-1*k),R=w(c(s[F],u,p,h)),L=w(c(s[H],u,p,h));R>h&&el(A,h,R);for(let e=1==k?n:i;e>=n&&i>=e;e+=k){let t=w(c(s[e],u,p,h));if(t==W)null!=r[e]?(S=w(f(r[e],a,m,d)),z==E&&(g(y,t,S),M=S),z=_(S,z),P=b(S,P)):null===r[e]&&(Y=C=!0);else{let l=!1;z!=E?(x(y,W,z,P,M,S),D=T=W):Y&&(l=!0,Y=!1),null!=r[e]?(S=w(f(r[e],a,m,d)),g(y,t,S),z=P=M=S,C&&t-W>1&&(l=!0),C=!1):(z=E,P=-E,null===r[e]&&(Y=!0,t-W>1&&(l=!0))),l&&el(A,D,t),W=t}}if(z!=E&&z!=P&&T!=W&&x(y,W,z,P,M,S),h+p>L&&el(A,L,h+p),null!=o.fill){let t=v.fill=new Path2D(y),n=w(f(o.fillTo(e,l,o.min,o.max),a,m,d));g(t,L,n),g(t,R,n)}return v.gaps=A=o.gaps(e,l,n,i,A),o.spanGaps||(v.clip=Qt(A,u.ori,h,d,p,m)),e.bands.length>0&&(v.band=Xt(e,l,n,i,y)),v}))}function wl(e,t,l,n,i){const o=e.length;if(2>o)return null;const s=new Path2D;if(l(s,e[0],t[0]),2==o)n(s,e[1],t[1]);else{let l=Array(o),n=Array(o-1),r=Array(o-1),u=Array(o-1);for(let l=0;o-1>l;l++)r[l]=t[l+1]-t[l],u[l]=e[l+1]-e[l],n[l]=r[l]/u[l];l[0]=n[0];for(let e=1;o-1>e;e++)0===n[e]||0===n[e-1]||n[e-1]>0!=n[e]>0?l[e]=0:(l[e]=3*(u[e-1]+u[e])/((2*u[e]+u[e-1])/n[e-1]+(u[e]+2*u[e-1])/n[e]),isFinite(l[e])||(l[e]=0));l[o-1]=n[o-2];for(let n=0;o-1>n;n++)i(s,e[n]+u[n]/3,t[n]+l[n]*u[n]/3,e[n+1]-u[n]/3,t[n+1]-l[n+1]*u[n]/3,e[n+1],t[n+1])}return s}const _l=new Set;function bl(){_l.forEach((e=>{e.syncRect(!0)}))}Ye("resize",xe,bl),Ye("scroll",xe,bl,!0);const kl=xl(),vl=dl();function yl(e,t,l,n){return(n?[e[0],e[1]].concat(e.slice(2)):[e[0]].concat(e.slice(1))).map(((e,n)=>Ml(e,n,t,l)))}function Ml(e,t,l,n){return Z({},0==t?l:n,e)}function Sl(e,t,l){return null==t?B:[t,l]}const El=Sl;function Dl(e,t,l){return null==t?B:a(t,l,.1,!0)}function Tl(e,t,l,n){return null==t?B:i(t,l,e.scales[n].log,!1)}const zl=Tl;function Pl(e,t,l,n){return null==t?B:o(t,l,e.scales[n].log,!1)}const Al=Pl;function Wl(t,l,n,i,o){let s=b(D(t),D(l)),r=l-t,u=e(o/i*r,n);do{let e=n[u],t=i*e/r;if(t>=o&&17>=s+(5>e?I.get(e):0))return[e,t]}while(++u<n.length);return[0,0]}function Yl(e){let t,l;return[e=e.replace(/(\d+)px/,((e,n)=>(t=x((l=+n)*we))+"px")),t,l]}function Cl(e){e.show&&[e.font,e.labelFont].forEach((e=>{let t=L(e[2]*we,1);e[0]=e[0].replace(/[0-9.]+px/,t+"px"),e[1]=t}))}function Fl(t,l,n){const r={mode:c(t.mode,1)},u=r.mode;function f(e,t){return((3==t.distr?y(e>0?e:t.clamp(r,e,t.min,t.max,t.key)):4==t.distr?S(e,t.asinh):e)-t._min)/(t._max-t._min)}function h(e,t,l,n){let i=f(e,t);return n+l*(-1==t.dir?1-i:i)}function g(e,t,l,n){let i=f(e,t);return n+l*(-1==t.dir?i:1-i)}function v(e,t,l,n){return 0==t.ori?h(e,t,l,n):g(e,t,l,n)}r.valToPosH=h,r.valToPosV=g;let M=!1;r.status=0;const D=r.root=Me("uplot");null!=t.id&&(D.id=t.id),be(D,t.class),t.title&&(Me("u-title",D).textContent=t.title);const A=ye("canvas"),R=r.ctx=A.getContext("2d"),I=Me("u-wrap",D),G=r.under=Me("u-under",I);I.appendChild(A);const O=r.over=Me("u-over",I),$=+c((t=K(t)).pxAlign,1),oe=tl($);(t.plugins||[]).forEach((e=>{e.opts&&(t=e.opts(r,t)||t)}));const he=t.ms||.001,_e=r.series=1==u?yl(t.series||[],zt,Vt,!1):function(e,t){return e.map(((e,l)=>0==l?null:Z({},t,e)))}(t.series||[null],Bt),Se=r.axes=yl(t.axes||[],Tt,Gt,!0),De=r.scales={},ze=r.bands=t.bands||[];ze.forEach((e=>{e.fill=P(e.fill||null)}));const Ae=2==u?_e[1].facets[0].scale:_e[0].scale,We={axes:function(){for(let e=0;Se.length>e;e++){let t=Se[e];if(!t.show||!t._show)continue;let l,n,i=t.side,o=i%2,s=t.stroke(r,e),u=0==i||3==i?-1:1;if(t.label){let e=x((t._lpos+t.labelGap*u)*we);ql(t.labelFont[0],s,"center",2==i?te:le),R.save(),1==o?(l=n=0,R.translate(e,x(Rt+Kt/2)),R.rotate((3==i?-p:p)/2)):(l=x(Ht+jt/2),n=e),R.fillText(t.label,l,n),R.restore()}let[a,c]=t._found;if(0==c)continue;let f=De[t.scale],h=0==o?jt:Kt,d=0==o?Ht:Rt,m=x(t.gap*we),g=t._splits,w=2==f.distr?g.map((e=>jl[e])):g,_=2==f.distr?jl[g[1]]-jl[g[0]]:a,b=t.ticks,k=b.show?x(b.size*we):0,y=t._rotate*-p/180,M=oe(t._pos*we),S=M+(k+m)*u;n=0==o?S:0,l=1==o?S:0,ql(t.font[0],s,1==t.align?ne:2==t.align?ie:y>0?ne:0>y?ie:0==o?"center":3==i?ie:ne,y||1==o?"middle":2==i?te:le);let E=1.5*t.font[1],D=g.map((e=>oe(v(e,f,h,d)))),T=t._values;for(let e=0;T.length>e;e++){let t=T[e];if(null!=t){0==o?l=D[e]:n=D[e],t=""+t;let i=-1==t.indexOf("\n")?[t]:t.split(/\n/gm);for(let e=0;i.length>e;e++){let t=i[e];y?(R.save(),R.translate(l,n+e*E),R.rotate(y),R.fillText(t,0,0),R.restore()):R.fillText(t,l,n+e*E)}}}b.show&&tn(D,b.filter(r,w,e,c,_),o,i,M,k,L(b.width*we,3),b.stroke(r,e),b.dash,b.cap);let z=t.grid;z.show&&tn(D,z.filter(r,w,e,c,_),o,0==o?2:1,0==o?Rt:Ht,0==o?Kt:jt,L(z.width*we,3),z.stroke(r,e),z.dash,z.cap)}ti("drawAxes")},series:function(){pl>0&&(_e.forEach(((e,t)=>{if(t>0&&e.show&&null==e._paths){let n=function(e){let t=z(ml-1,0,pl-1),l=z(gl+1,0,pl-1);for(;null==e[t]&&t>0;)t--;for(;null==e[l]&&pl-1>l;)l++;return[t,l]}(l[t]);e._paths=e.paths(r,t,n[0],n[1])}})),_e.forEach(((e,t)=>{if(t>0&&e.show){Nl!=e.alpha&&(R.globalAlpha=Nl=e.alpha),Zl(t,!1),e._paths&&$l(t,!1);{Zl(t,!0);let l=e.points.show(r,t,ml,gl),n=e.points.filter(r,t,l,e._paths?e._paths.gaps:null);(l||n)&&(e.points._paths=e.points.paths(r,t,ml,gl,n),$l(t,!0))}1!=Nl&&(R.globalAlpha=Nl=1),ti("drawSeries",t)}})))}},Fe=(t.drawOrder||["axes","series"]).map((e=>We[e]));function He(e){let l=De[e];if(null==l){let n=(t.scales||N)[e]||N;if(null!=n.from)He(n.from),De[e]=Z({},De[n.from],n,{key:e});else{l=De[e]=Z({},e==Ae?Jt:qt,n),2==u&&(l.time=!1),l.key=e;let t=l.time,i=l.range,o=V(i);if((e!=Ae||2==u)&&(!o||null!=i[0]&&null!=i[1]||(i={min:null==i[0]?s:{mode:1,hard:i[0],soft:i[0]},max:null==i[1]?s:{mode:1,hard:i[1],soft:i[1]}},o=!1),!o&&J(i))){let e=i;i=(t,l,n)=>null==l?B:a(l,n,e)}l.range=P(i||(t?El:e==Ae?3==l.distr?zl:4==l.distr?Al:Sl:3==l.distr?Tl:4==l.distr?Pl:Dl)),l.auto=P(!o&&l.auto),l.clamp=P(l.clamp||Ut),l._min=l._max=null}}}He("x"),He("y"),1==u&&_e.forEach((e=>{He(e.scale)})),Se.forEach((e=>{He(e.scale)}));for(let e in t.scales)He(e);const Re=De[Ae],Le=Re.distr;let Ie,Ge;0==Re.ori?(be(D,"u-hz"),Ie=h,Ge=g):(be(D,"u-vt"),Ie=g,Ge=h);const Oe={};for(let e in De){let t=De[e];null==t.min&&null==t.max||(Oe[e]={min:t.min,max:t.max},t.min=t.max=null)}const Ne=t.tzDate||(e=>new Date(x(e/he))),Be=t.fmtDate||je,Ve=1==he?ct(Ne):dt(Ne),Ue=mt(Ne,pt(1==he?at:ht,Be)),Je=wt(Ne,xt("{YYYY}-{MM}-{DD} {h}:{mm}{aa}",Be)),qe=[],$e=r.legend=Z({},_t,t.legend),Xe=$e.show,Qe=$e.markers;let et;$e.idxs=qe,Qe.width=P(Qe.width),Qe.dash=P(Qe.dash),Qe.stroke=P(Qe.stroke),Qe.fill=P(Qe.fill);let tt,lt=[],nt=[],it=!1,ot={};if($e.live){const e=_e[1]?_e[1].values:null;it=null!=e,tt=it?e(r,1,0):{_:0};for(let e in tt)ot[e]="--"}if(Xe)if(et=ye("table","u-legend",D),it){let e=ye("tr","u-thead",et);for(var st in ye("th",null,e),tt)ye("th",me,e).textContent=st}else be(et,"u-inline"),$e.live&&be(et,"u-live");const rt={show:!0},gt={show:!1},bt=new Map;function kt(e,t,l){const n=bt.get(t)||{},i=ol.bind[e](r,t,l);i&&(Ye(e,t,n[e]=i),bt.set(t,n))}function vt(e,t){const l=bt.get(t)||{};for(let n in l)null!=e&&n!=e||(Ce(n,t,l[n]),delete l[n]);null==e&&bt.delete(t)}let Mt=0,St=0,Et=0,Dt=0,Ct=0,Ft=0,Ht=0,Rt=0,jt=0,Kt=0;r.bbox={};let $t=!1,Xt=!1,Qt=!1,el=!1,ll=!1;function nl(e,t,l){(l||e!=r.width||t!=r.height)&&il(e,t),on(!1),Qt=!0,Xt=!0,el=ll=ol.left>=0,_n()}function il(e,t){r.width=Mt=Et=e,r.height=St=Dt=t,Ct=Ft=0,function(){let e=!1,t=!1,l=!1,n=!1;Se.forEach((i=>{if(i.show&&i._show){let{side:o,_size:s}=i,r=o%2,u=s+(null!=i.label?i.labelSize:0);u>0&&(r?(Et-=u,3==o?(Ct+=u,n=!0):l=!0):(Dt-=u,0==o?(Ft+=u,e=!0):t=!0))}})),cl[0]=e,cl[1]=l,cl[2]=t,cl[3]=n,Et-=dl[1]+dl[3],Ct+=dl[3],Dt-=dl[2]+dl[0],Ft+=dl[0]}(),function(){let e=Ct+Et,t=Ft+Dt,l=Ct,n=Ft;function i(i,o){switch(i){case 1:return e+=o,e-o;case 2:return t+=o,t-o;case 3:return l-=o,l+o;case 0:return n-=o,n+o}}Se.forEach((e=>{if(e.show&&e._show){let t=e.side;e._pos=i(t,e._size),null!=e.label&&(e._lpos=i(t,e.labelSize))}}))}();let l=r.bbox;Ht=l.left=T(Ct*we,.5),Rt=l.top=T(Ft*we,.5),jt=l.width=T(Et*we,.5),Kt=l.height=T(Dt*we,.5)}r.setSize=function({width:e,height:t}){nl(e,t)};const ol=r.cursor=Z({},yt,{drag:{y:2==u}},t.cursor);{ol.idxs=qe,ol._lock=!1;let e=ol.points;e.show=P(e.show),e.size=P(e.size),e.stroke=P(e.stroke),e.width=P(e.width),e.fill=P(e.fill)}const sl=r.focus=Z({},t.focus||{alpha:.3},ol.focus),rl=sl.prox>=0;let ul=[null];function al(e,t){if(1==u||t>0){let t=1==u&&De[e.scale].time,l=e.value;e.value=t?U(l)?wt(Ne,xt(l,Be)):l||Je:l||It,e.label=e.label||(t?"Time":"Value")}if(t>0){e.width=null==e.width?1:e.width,e.paths=e.paths||kl||Y,e.fillTo=P(e.fillTo||Nt),e.pxAlign=+c(e.pxAlign,$),e.pxRound=tl(e.pxAlign),e.stroke=P(e.stroke||null),e.fill=P(e.fill||null),e._stroke=e._fill=e._paths=e._focus=null;let t=Ot(e.width,1),l=e.points=Z({},{size:t,width:b(1,.2*t),stroke:e.stroke,space:2*t,paths:vl,_stroke:null,_fill:null},e.points);l.show=P(l.show),l.filter=P(l.filter),l.fill=P(l.fill),l.stroke=P(l.stroke),l.paths=P(l.paths),l.pxAlign=e.pxAlign}if(Xe){let l=function(e,t){if(0==t&&(it||!$e.live||2==u))return B;let l=[],n=ye("tr","u-series",et,et.childNodes[t]);be(n,e.class),e.show||be(n,pe);let i=ye("th",null,n);if(Qe.show){let e=Me("u-marker",i);if(t>0){let l=Qe.width(r,t);l&&(e.style.border=l+"px "+Qe.dash(r,t)+" "+Qe.stroke(r,t)),e.style.background=Qe.fill(r,t)}}let o=Me(me,i);for(var s in o.textContent=e.label,t>0&&(Qe.show||(o.style.color=e.width>0?Qe.stroke(r,t):Qe.fill(r,t)),kt("click",i,(t=>{if(ol._lock)return;let l=_e.indexOf(e);if((t.ctrlKey||t.metaKey)!=$e.isolate){let e=_e.some(((e,t)=>t>0&&t!=l&&e.show));_e.forEach(((t,n)=>{n>0&&Pn(n,e?n==l?rt:gt:rt,!0,li.setSeries)}))}else Pn(l,{show:!e.show},!0,li.setSeries)})),rl&&kt(ae,i,(()=>{ol._lock||Pn(_e.indexOf(e),Cn,!0,li.setSeries)}))),tt){let e=ye("td","u-value",n);e.textContent="--",l.push(e)}return[n,l]}(e,t);lt.splice(t,0,l[0]),nt.splice(t,0,l[1]),$e.values.push(null)}if(ol.show){qe.splice(t,0,null);let l=function(e,t){if(t>0){let l=ol.points.show(r,t);if(l)return be(l,"u-cursor-pt"),be(l,e.class),Ee(l,-10,-10,Et,Dt),O.insertBefore(l,ul[t]),l}}(e,t);l&&ul.splice(t,0,l)}}r.addSeries=function(e,t){e=Ml(e,t=null==t?_e.length:t,zt,Vt),_e.splice(t,0,e),al(_e[t],t)},r.delSeries=function(e){if(_e.splice(e,1),Xe){$e.values.splice(e,1),nt.splice(e,1);let t=lt.splice(e,1)[0];vt(null,t.firstChild),t.remove()}ol.show&&(qe.splice(e,1),ul.length>1&&ul.splice(e,1)[0].remove())};const cl=[!1,!1,!1,!1];function fl(e,t,l){let[n,i,o,s]=l,r=t%2,u=0;return 0==r&&(s||i)&&(u=0==t&&!n||2==t&&!o?x(Tt.size/3):0),1==r&&(n||o)&&(u=1==t&&!i||3==t&&!s?x(Gt.size/2):0),u}const hl=r.padding=(t.padding||[fl,fl,fl,fl]).map((e=>P(c(e,fl)))),dl=r._padding=hl.map(((e,t)=>e(r,t,cl,0)));let pl,ml=null,gl=null;const xl=1==u?_e[0].idxs:null;let wl,bl,Fl,Hl,Rl,Ll,Il,Gl,Ol,Nl,jl=null,Bl=!1;function Vl(e,t){if(2==u){pl=0;for(let e=1;_e.length>e;e++)pl+=l[e][0].length;r.data=l=e}else(l=(e||[]).slice())[0]=l[0]||[],r.data=l.slice(),jl=l[0],pl=jl.length,2==Le&&(l[0]=jl.map(((e,t)=>t)));if(r._data=l,on(!0),ti("setData"),!1!==t){let e=Re;e.auto(r,Bl)?Ul():zn(Ae,e.min,e.max),el=ol.left>=0,ll=!0,_n()}}function Ul(){let e,t;Bl=!0,1==u&&(pl>0?(ml=xl[0]=0,gl=xl[1]=pl-1,e=l[0][ml],t=l[0][gl],2==Le?(e=ml,t=gl):1==pl&&(3==Le?[e,t]=i(e,e,Re.log,!1):4==Le?[e,t]=o(e,e,Re.log,!1):Re.time?t=e+x(86400/he):[e,t]=a(e,t,.1,!0))):(ml=xl[0]=e=null,gl=xl[1]=t=null)),zn(Ae,e,t)}function Jl(e="#0000",t,l=j,n="butt",i="#0000",o="round"){e!=wl&&(R.strokeStyle=wl=e),i!=bl&&(R.fillStyle=bl=i),t!=Fl&&(R.lineWidth=Fl=t),o!=Rl&&(R.lineJoin=Rl=o),n!=Ll&&(R.lineCap=Ll=n),l!=Hl&&R.setLineDash(Hl=l)}function ql(e,t,l,n){t!=bl&&(R.fillStyle=bl=t),e!=Il&&(R.font=Il=e),l!=Gl&&(R.textAlign=Gl=l),n!=Ol&&(R.textBaseline=Ol=n)}function Kl(e,t,l,n){if(e.auto(r,Bl)&&(null==t||null==t.min)){let t=c(ml,0),i=c(gl,n.length-1),o=null==l.min?3==e.distr?function(e,t,l){let n=E,i=-E;for(let o=t;l>=o;o++)e[o]>0&&(n=_(n,e[o]),i=b(i,e[o]));return[n==E?1:n,i==-E?10:i]}(n,t,i):function(e,t,l){let n=E,i=-E;for(let o=t;l>=o;o++)null!=e[o]&&(n=_(n,e[o]),i=b(i,e[o]));return[n,i]}(n,t,i):[l.min,l.max];e.min=_(e.min,l.min=o[0]),e.max=b(e.max,l.max=o[1])}}function Zl(e,t){let l=t?_e[e].points:_e[e];l._stroke=l.stroke(r,e),l._fill=l.fill(r,e)}function $l(e,t){let n=t?_e[e].points:_e[e],i=n._stroke,o=n._fill,{stroke:s,fill:u,clip:a,flags:f}=n._paths,h=null,d=L(n.width*we,3),p=d%2/2;t&&null==o&&(o=d>0?"#fff":i);let m=1==n.pxAlign;if(m&&R.translate(p,p),!t){let e=Ht,t=Rt,l=jt,i=Kt,o=d*we/2;0==n.min&&(i+=o),0==n.max&&(t-=o,i+=o),h=new Path2D,h.rect(e,t,l,i)}t?Xl(i,d,n.dash,n.cap,o,s,u,f,a):function(e,t,n,i,o,s,u,a,f,h,d){let p=!1;ze.forEach(((m,g)=>{if(m.series[0]==e){let e,x=_e[m.series[1]],w=l[m.series[1]],_=(x._paths||N).band,b=null;x.show&&_&&function(e,t,l){for(t=c(t,0),l=c(l,e.length-1);l>=t;){if(null!=e[t])return!0;t++}return!1}(w,ml,gl)?(b=m.fill(r,g)||s,e=x._paths.clip):_=null,Xl(t,n,i,o,b,u,a,f,h,d,e,_),p=!0}})),p||Xl(t,n,i,o,s,u,a,f,h,d)}(e,i,d,n.dash,n.cap,o,s,u,f,h,a),m&&R.translate(-p,-p)}function Xl(e,t,l,n,i,o,s,r,u,a,c,f){Jl(e,t,l,n,i),(u||a||f)&&(R.save(),u&&R.clip(u),a&&R.clip(a)),f?3==(3&r)?(R.clip(f),c&&R.clip(c),en(i,s),Ql(e,o,t)):2&r?(en(i,s),R.clip(f),Ql(e,o,t)):1&r&&(R.save(),R.clip(f),c&&R.clip(c),en(i,s),R.restore(),Ql(e,o,t)):(en(i,s),Ql(e,o,t)),(u||a||f)&&R.restore()}function Ql(e,t,l){l>0&&(t instanceof Map?t.forEach(((e,t)=>{R.strokeStyle=wl=t,R.stroke(e)})):null!=t&&e&&R.stroke(t))}function en(e,t){t instanceof Map?t.forEach(((e,t)=>{R.fillStyle=bl=t,R.fill(e)})):null!=t&&e&&R.fill(t)}function tn(e,t,l,n,i,o,s,r,u,a){let c=s%2/2;1==$&&R.translate(c,c),Jl(r,s,u,a,r),R.beginPath();let f,h,d,p,m=i+(0==n||3==n?-o:o);0==l?(h=i,p=m):(f=i,d=m);for(let n=0;e.length>n;n++)null!=t[n]&&(0==l?f=d=e[n]:h=p=e[n],R.moveTo(f,h),R.lineTo(d,p));R.stroke(),1==$&&R.translate(-c,-c)}function ln(e){let t=!0;return Se.forEach(((l,n)=>{if(!l.show)return;let i=De[l.scale];if(null==i.min)return void(l._show&&(t=!1,l._show=!1,on(!1)));l._show||(t=!1,l._show=!0,on(!1));let o=l.side,s=o%2,{min:u,max:a}=i,[c,f]=function(e,t,l,n){let i,o=Se[e];if(n>0){let s=o._space=o.space(r,e,t,l,n);i=Wl(t,l,o._incrs=o.incrs(r,e,t,l,n,s),n,s)}else i=[0,0];return o._found=i}(n,u,a,0==s?Et:Dt);if(0==f)return;let h=l._splits=l.splits(r,n,u,a,c,f,2==i.distr),d=2==i.distr?h.map((e=>jl[e])):h,p=2==i.distr?jl[h[1]]-jl[h[0]]:c,m=l._values=l.values(r,l.filter(r,d,n,f,p),n,f,p);l._rotate=2==o?l.rotate(r,m,n,f):0;let g=l._size;l._size=w(l.size(r,m,n,e)),null!=g&&l._size!=g&&(t=!1)})),t}function nn(e){let t=!0;return hl.forEach(((l,n)=>{let i=l(r,n,cl,e);i!=dl[n]&&(t=!1),dl[n]=i})),t}function on(e){_e.forEach(((t,l)=>{l>0&&(t._paths=null,e&&(1==u?(t.min=null,t.max=null):t.facets.forEach((e=>{e.min=null,e.max=null}))))}))}r.setData=Vl;let sn,rn,un,an,cn,fn,hn,dn,pn,mn,gn,xn,wn=!1;function _n(){wn||(X(bn),wn=!0)}function bn(){$t&&(function(){let t=K(De,q);for(let e in t){let l=t[e],n=Oe[e];if(null!=n&&null!=n.min)Z(l,n),e==Ae&&on(!0);else if(e!=Ae||2==u)if(0==pl&&null==l.from){let t=l.range(r,null,null,e);l.min=t[0],l.max=t[1]}else l.min=E,l.max=-E}if(pl>0){_e.forEach(((n,i)=>{if(1==u){let o=n.scale,s=t[o],u=Oe[o];if(0==i){let t=s.range(r,s.min,s.max,o);s.min=t[0],s.max=t[1],ml=e(s.min,l[0]),gl=e(s.max,l[0]),s.min>l[0][ml]&&ml++,l[0][gl]>s.max&&gl--,n.min=jl[ml],n.max=jl[gl]}else n.show&&n.auto&&Kl(s,u,n,l[i]);n.idxs[0]=ml,n.idxs[1]=gl}else if(i>0&&n.show&&n.auto){let[e,o]=n.facets,s=e.scale,r=o.scale,[u,a]=l[i];Kl(t[s],Oe[s],e,u),Kl(t[r],Oe[r],o,a),n.min=o.min,n.max=o.max}}));for(let e in t){let l=t[e],n=Oe[e];if(null==l.from&&(null==n||null==n.min)){let t=l.range(r,l.min==E?null:l.min,l.max==-E?null:l.max,e);l.min=t[0],l.max=t[1]}}}for(let e in t){let l=t[e];if(null!=l.from){let n=t[l.from];if(null==n.min)l.min=l.max=null;else{let t=l.range(r,n.min,n.max,e);l.min=t[0],l.max=t[1]}}}let n={},i=!1;for(let e in t){let l=t[e],o=De[e];if(o.min!=l.min||o.max!=l.max){o.min=l.min,o.max=l.max;let t=o.distr;o._min=3==t?y(o.min):4==t?S(o.min,o.asinh):o.min,o._max=3==t?y(o.max):4==t?S(o.max,o.asinh):o.max,n[e]=i=!0}}if(i){_e.forEach(((e,t)=>{2==u?t>0&&n.y&&(e._paths=null):n[e.scale]&&(e._paths=null)}));for(let e in n)Qt=!0,ti("setScale",e);ol.show&&(el=ll=ol.left>=0)}for(let e in Oe)Oe[e]=null}(),$t=!1),Qt&&(function(){let e=!1,t=0;for(;!e;){t++;let l=ln(t),n=nn(t);e=3==t||l&&n,e||(il(r.width,r.height),Xt=!0)}}(),Qt=!1),Xt&&(ve(G,ne,Ct),ve(G,te,Ft),ve(G,Q,Et),ve(G,ee,Dt),ve(O,ne,Ct),ve(O,te,Ft),ve(O,Q,Et),ve(O,ee,Dt),ve(I,Q,Mt),ve(I,ee,St),A.width=x(Mt*we),A.height=x(St*we),Se.forEach((e=>{let{_show:t,_el:l,_size:n,_pos:i,side:o}=e;if(t){let e=o%2==1;ve(l,e?"left":"top",i-(3===o||0===o?n:0)),ve(l,e?"width":"height",n),ve(l,e?"top":"left",e?Ft:Ct),ve(l,e?"height":"width",e?Dt:Et),l&&ke(l,pe)}else l&&be(l,pe)})),wl=bl=Fl=Rl=Ll=Il=Gl=Ol=Hl=null,Nl=1,Vn(!1),ti("setSize"),Xt=!1),Mt>0&&St>0&&(R.clearRect(0,0,A.width,A.height),ti("drawClear"),Fe.forEach((e=>e())),ti("draw")),ol.show&&el&&(jn(null,!0,!1),el=!1),M||(M=!0,r.status=1,ti("ready")),Bl=!1,wn=!1}function kn(t,n){let i=De[t];if(null==i.from){if(0==pl){let e=i.range(r,n.min,n.max,t);n.min=e[0],n.max=e[1]}if(n.min>n.max){let e=n.min;n.min=n.max,n.max=e}if(pl>1&&null!=n.min&&null!=n.max&&1e-16>n.max-n.min)return;t==Ae&&2==i.distr&&pl>0&&(n.min=e(n.min,l[0]),n.max=e(n.max,l[0]),n.min==n.max&&n.max++),Oe[t]=n,$t=!0,_n()}}r.redraw=(e,t)=>{Qt=t||!1,!1!==e?zn(Ae,Re.min,Re.max):_n()},r.setScale=kn;let vn=!1;const yn=ol.drag;let Mn=yn.x,Sn=yn.y;ol.show&&(ol.x&&(sn=Me("u-cursor-x",O)),ol.y&&(rn=Me("u-cursor-y",O)),0==Re.ori?(un=sn,an=rn):(un=rn,an=sn),gn=ol.left,xn=ol.top);const En=r.select=Z({show:!0,over:!0,left:0,width:0,top:0,height:0},t.select),Dn=En.show?Me("u-select",En.over?O:G):null;function Tn(e,t){if(En.show){for(let t in e)ve(Dn,t,En[t]=e[t]);!1!==t&&ti("setSelect")}}function zn(e,t,l){kn(e,{min:t,max:l})}function Pn(e,t,l,n){let i=_e[e];null!=t.focus&&function(e){if(e!=Yn){let t=null==e,l=1!=sl.alpha;_e.forEach(((n,i)=>{let o=t||0==i||i==e;n._focus=t?null:o,l&&function(e,t){_e[e].alpha=t,ol.show&&ul[e]&&(ul[e].style.opacity=t),Xe&&lt[e]&&(lt[e].style.opacity=t)}(i,o?1:sl.alpha)})),Yn=e,l&&_n()}}(e),null!=t.show&&(i.show=t.show,function(e){let t=Xe?lt[e]:null;_e[e].show?t&&ke(t,pe):(t&&be(t,pe),ul.length>1&&Ee(ul[e],-10,-10,Et,Dt))}(e),zn(2==u?i.facets[1].scale:i.scale,null,null),_n()),!1!==l&&ti("setSeries",e,t),n&&oi("setSeries",r,e,t)}let An,Wn,Yn;r.setSelect=Tn,r.setSeries=Pn,r.addBand=function(e,t){e.fill=P(e.fill||null),ze.splice(t=null==t?ze.length:t,0,e)},r.setBand=function(e,t){Z(ze[e],t)},r.delBand=function(e){null==e?ze.length=0:ze.splice(e,1)};const Cn={focus:!0},Fn={focus:!1};function Hn(e,t,l){let n=De[t];l&&(e=e/we-(1==n.ori?Ft:Ct));let i=Et;1==n.ori&&(i=Dt,e=i-e),-1==n.dir&&(e=i-e);let o=n._min,s=o+e/i*(n._max-o),r=n.distr;return 3==r?k(10,s):4==r?((e,t=1)=>d.sinh(e)*t)(s,n.asinh):s}function Rn(e,t){ve(Dn,ne,En.left=e),ve(Dn,Q,En.width=t)}function Ln(e,t){ve(Dn,te,En.top=e),ve(Dn,ee,En.height=t)}Xe&&rl&&Ye(ce,et,(()=>{ol._lock||(Pn(null,Fn,!0,li.setSeries),jn(null,!0,!1))})),r.valToIdx=t=>e(t,l[0]),r.posToIdx=function(t,n){return e(Hn(t,Ae,n),l[0],ml,gl)},r.posToVal=Hn,r.valToPos=(e,t,l)=>0==De[t].ori?h(e,De[t],l?jt:Et,l?Ht:0):g(e,De[t],l?Kt:Dt,l?Rt:0),r.batch=function(e){e(r),_n()},r.setCursor=(e,t,l)=>{gn=e.left,xn=e.top,jn(null,t,l)};let In=0==Re.ori?Rn:Ln,Gn=1==Re.ori?Rn:Ln;function On(e,t){if(null!=e){let t=e.idx;$e.idx=t,_e.forEach(((e,l)=>{(l>0||!it)&&Nn(l,t)}))}Xe&&$e.live&&function(){if(Xe&&$e.live)for(let e=2==u?1:0;_e.length>e;e++){if(0==e&&it)continue;let t=$e.values[e],l=0;for(let n in t)nt[e][l++].firstChild.nodeValue=t[n]}}(),ll=!1,!1!==t&&ti("setLegend")}function Nn(e,t){let n;if(null==t)n=ot;else{let i=_e[e],o=0==e&&2==Le?jl:l[e];n=it?i.values(r,e,t):{_:i.value(r,o[t],e,t)}}$e.values[e]=n}function jn(t,n,i){let o;pn=gn,mn=xn,[gn,xn]=ol.move(r,gn,xn),ol.show&&(un&&Ee(un,x(gn),0,Et,Dt),an&&Ee(an,0,x(xn),Et,Dt)),An=E;let s=0==Re.ori?Et:Dt,a=1==Re.ori?Et:Dt;if(0>gn||0==pl||ml>gl){o=null;for(let e=0;_e.length>e;e++)e>0&&ul.length>1&&Ee(ul[e],-10,-10,Et,Dt);if(rl&&Pn(null,Cn,!0,null==t&&li.setSeries),$e.live){qe.fill(null),ll=!0;for(let e=0;_e.length>e;e++)$e.values[e]=ot}}else{let t,n,i;1==u&&(t=0==Re.ori?gn:xn,n=Hn(t,Ae),o=e(n,l[0],ml,gl),i=H(Ie(l[0][o],Re,s,0),.5));for(let e=2==u?1:0;_e.length>e;e++){let t=_e[e],c=qe[e],f=1==u?l[e][c]:l[e][1][c],h=ol.dataIdx(r,e,o,n),d=1==u?l[e][h]:l[e][1][h];ll=ll||d!=f||h!=c,qe[e]=h;let p=h==o?i:H(Ie(1==u?l[0][h]:l[e][0][h],Re,s,0),.5);if(e>0&&t.show){let l,n,i=null==d?-10:H(Ge(d,1==u?De[t.scale]:De[t.facets[1].scale],a,0),.5);if(i>0&&1==u){let t=m(i-xn);t>An||(An=t,Wn=e)}if(0==Re.ori?(l=p,n=i):(l=i,n=p),ll&&ul.length>1){Te(ul[e],ol.points.fill(r,e),ol.points.stroke(r,e));let t,i,o,s,u=!0,a=ol.points.bbox;if(null!=a){u=!1;let l=a(r,e);o=l.left,s=l.top,t=l.width,i=l.height}else o=l,s=n,t=i=ol.points.size(r,e);Pe(ul[e],t,i,u),Ee(ul[e],o,s,Et,Dt)}}if($e.live){if(!ll||0==e&&it)continue;Nn(e,h)}}}if(ol.idx=o,ol.left=gn,ol.top=xn,ll&&($e.idx=o,On()),En.show&&vn)if(null!=t){let[e,l]=li.scales,[n,i]=li.match,[o,r]=t.cursor.sync.scales,u=t.cursor.drag;Mn=u._x,Sn=u._y;let c,f,h,d,p,{left:g,top:x,width:w,height:b}=t.select,k=t.scales[e].ori,v=t.posToVal,y=null!=e&&n(e,o),M=null!=l&&i(l,r);y&&(0==k?(c=g,f=w):(c=x,f=b),Mn?(h=De[e],d=Ie(v(c,o),h,s,0),p=Ie(v(c+f,o),h,s,0),In(_(d,p),m(p-d))):In(0,s),M||Gn(0,a)),M&&(1==k?(c=g,f=w):(c=x,f=b),Sn?(h=De[l],d=Ge(v(c,r),h,a,0),p=Ge(v(c+f,r),h,a,0),Gn(_(d,p),m(p-d))):Gn(0,a),y||In(0,s))}else{let e=m(pn-cn),t=m(mn-fn);if(1==Re.ori){let l=e;e=t,t=l}Mn=yn.x&&e>=yn.dist,Sn=yn.y&&t>=yn.dist;let l,n,i=yn.uni;null!=i?Mn&&Sn&&(Mn=e>=i,Sn=t>=i,Mn||Sn||(t>e?Sn=!0:Mn=!0)):yn.x&&yn.y&&(Mn||Sn)&&(Mn=Sn=!0),Mn&&(0==Re.ori?(l=hn,n=gn):(l=dn,n=xn),In(_(l,n),m(n-l)),Sn||Gn(0,a)),Sn&&(1==Re.ori?(l=hn,n=gn):(l=dn,n=xn),Gn(_(l,n),m(n-l)),Mn||In(0,s)),Mn||Sn||(In(0,0),Gn(0,0))}if(yn._x=Mn,yn._y=Sn,null==t){if(i){if(null!=ni){let[e,t]=li.scales;li.values[0]=null!=e?Hn(0==Re.ori?gn:xn,e):null,li.values[1]=null!=t?Hn(1==Re.ori?gn:xn,t):null}oi(se,r,gn,xn,Et,Dt,o)}if(rl){let e=i&&li.setSeries,t=sl.prox;null==Yn?An>t||Pn(Wn,Cn,!0,e):An>t?Pn(null,Cn,!0,e):Wn!=Yn&&Pn(Wn,Cn,!0,e)}}M&&!1!==n&&ti("setCursor")}r.setLegend=On;let Bn=null;function Vn(e){!0===e?Bn=null:(Bn=O.getBoundingClientRect(),ti("syncRect",Bn))}function Un(e,t,l,n,i,o){ol._lock||(Jn(e,t,l,n,i,o,0,!1,null!=e),null!=e?jn(null,!0,!0):jn(t,!0,!1))}function Jn(e,t,l,n,i,o,s,u,a){if(null==Bn&&Vn(!1),null!=e)l=e.clientX-Bn.left,n=e.clientY-Bn.top;else{if(0>l||0>n)return gn=-10,void(xn=-10);let[e,s]=li.scales,r=t.cursor.sync,[u,a]=r.values,[c,f]=r.scales,[h,d]=li.match,p=1==t.scales[c].ori,m=0==Re.ori?Et:Dt,g=1==Re.ori?Et:Dt,x=p?o:i,w=p?i:o,_=p?n:l,b=p?l:n;if(l=null!=c?h(e,c)?v(u,De[e],m,0):-10:m*(_/x),n=null!=f?d(s,f)?v(a,De[s],g,0):-10:g*(b/w),1==Re.ori){let e=l;l=n,n=e}}a&&(l>1&&Et-1>l||(l=T(l,Et)),n>1&&Dt-1>n||(n=T(n,Dt))),u?(cn=l,fn=n,[hn,dn]=ol.move(r,l,n)):(gn=l,xn=n)}function qn(){Tn({width:0,height:0},!1)}function Kn(e,t,l,n,i,o){vn=!0,Mn=Sn=yn._x=yn._y=!1,Jn(e,t,l,n,i,o,0,!0,!1),null!=e&&(kt(ue,ge,Zn),oi(re,r,hn,dn,Et,Dt,null))}function Zn(e,t,l,n,i,o){vn=yn._x=yn._y=!1,Jn(e,t,l,n,i,o,0,!1,!0);let{left:s,top:u,width:a,height:c}=En,f=a>0||c>0;if(f&&Tn(En),yn.setScale&&f){let e=s,t=a,l=u,n=c;if(1==Re.ori&&(e=u,t=c,l=s,n=a),Mn&&zn(Ae,Hn(e,Ae),Hn(e+t,Ae)),Sn)for(let e in De){let t=De[e];e!=Ae&&null==t.from&&t.min!=E&&zn(e,Hn(l+n,e),Hn(l,e))}qn()}else ol.lock&&(ol._lock=!ol._lock,ol._lock||jn(null,!0,!1));null!=e&&(vt(ue,ge),oi(ue,r,gn,xn,Et,Dt,null))}function $n(e){Ul(),qn(),null!=e&&oi(fe,r,gn,xn,Et,Dt,null)}function Xn(){Se.forEach(Cl),nl(r.width,r.height,!0)}Ye(de,xe,Xn);const Qn={};Qn.mousedown=Kn,Qn.mousemove=Un,Qn.mouseup=Zn,Qn.dblclick=$n,Qn.setSeries=(e,t,l,n)=>{Pn(l,n,!0,!1)},ol.show&&(kt(re,O,Kn),kt(se,O,Un),kt(ae,O,Vn),kt(ce,O,(function(){if(!ol._lock){let e=vn;if(vn){let e,t,l=!0,n=!0,i=10;0==Re.ori?(e=Mn,t=Sn):(e=Sn,t=Mn),e&&t&&(l=i>=gn||gn>=Et-i,n=i>=xn||xn>=Dt-i),e&&l&&(gn=hn>gn?0:Et),t&&n&&(xn=dn>xn?0:Dt),jn(null,!0,!0),vn=!1}gn=-10,xn=-10,jn(null,!0,!0),e&&(vn=e)}})),kt(fe,O,$n),_l.add(r),r.syncRect=Vn);const ei=r.hooks=t.hooks||{};function ti(e,t,l){e in ei&&ei[e].forEach((e=>{e.call(null,r,t,l)}))}(t.plugins||[]).forEach((e=>{for(let t in e.hooks)ei[t]=(ei[t]||[]).concat(e.hooks[t])}));const li=Z({key:null,setSeries:!1,filters:{pub:C,sub:C},scales:[Ae,_e[1]?_e[1].scale:null],match:[F,F],values:[null,null]},ol.sync);ol.sync=li;const ni=li.key,ii=Zt(ni);function oi(e,t,l,n,i,o,s){li.filters.pub(e,t,l,n,i,o,s)&&ii.pub(e,t,l,n,i,o,s)}function si(){ti("init",t,l),Vl(l||t.data,!1),Oe[Ae]?kn(Ae,Oe[Ae]):Ul(),nl(t.width,t.height),jn(null,!0,!1),Tn(En,!1)}return ii.sub(r),r.pub=function(e,t,l,n,i,o,s){li.filters.sub(e,t,l,n,i,o,s)&&Qn[e](null,t,l,n,i,o,s)},r.destroy=function(){ii.unsub(r),_l.delete(r),bt.clear(),Ce(de,xe,Xn),D.remove(),ti("destroy")},_e.forEach(al),Se.forEach((function(e,t){if(e._show=e.show,e.show){let l=e.side%2,n=De[e.scale];null==n&&(e.scale=l?_e[1].scale:Ae,n=De[e.scale]);let i=n.time;e.size=P(e.size),e.space=P(e.space),e.rotate=P(e.rotate),e.incrs=P(e.incrs||(2==n.distr?Ke:i?1==he?ut:ft:Ze)),e.splits=P(e.splits||(i&&1==n.distr?Ve:3==n.distr?Wt:4==n.distr?Yt:At)),e.stroke=P(e.stroke),e.grid.stroke=P(e.grid.stroke),e.ticks.stroke=P(e.ticks.stroke);let o=e.values;e.values=V(o)&&!V(o[0])?P(o):i?V(o)?mt(Ne,pt(o,Be)):U(o)?function(e,t){let l=je(t);return(t,n)=>n.map((t=>l(e(t))))}(Ne,o):o||Ue:o||Pt,e.filter=P(e.filter||(3>n.distr?W:Lt)),e.font=Yl(e.font),e.labelFont=Yl(e.labelFont),e._size=e.size(r,null,t,0),e._space=e._rotate=e._incrs=e._found=e._splits=e._values=null,e._size>0&&(cl[t]=!0),e._el=Me("u-axis",I)}})),n?n instanceof HTMLElement?(n.appendChild(D),si()):n(r,si):si(),r}Fl.assign=Z,Fl.fmtNum=h,Fl.rangeNum=a,Fl.rangeLog=i,Fl.rangeAsinh=o,Fl.orient=$t,Fl.join=function(e,t){let l=new Set;for(let t=0;e.length>t;t++){let n=e[t][0],i=n.length;for(let e=0;i>e;e++)l.add(n[e])}let n=[Array.from(l).sort(((e,t)=>e-t))],i=n[0].length,o=new Map;for(let e=0;i>e;e++)o.set(n[0][e],e);for(let l=0;e.length>l;l++){let s=e[l],r=s[0];for(let e=1;s.length>e;e++){let u=s[e],a=Array(i).fill(void 0),c=t?t[l][e]:1,f=[];for(let e=0;u.length>e;e++){let t=u[e],l=o.get(r[e]);null===t?0!=c&&(a[l]=t,2==c&&f.push(l)):a[l]=t}$(a,f,i),n.push(a)}}return n},Fl.fmtDate=je,Fl.tzDate=function(e,t){let l;return"UTC"==t||"Etc/UTC"==t?l=new Date(+e+6e4*e.getTimezoneOffset()):t==Be?l=e:(l=new Date(e.toLocaleString("en-US",{timeZone:t})),l.setMilliseconds(e.getMilliseconds())),l},Fl.sync=Zt;{Fl.addGap=el,Fl.clipGaps=Qt;let e=Fl.paths={points:dl};e.linear=xl,e.stepped=function(e){const l=c(e.align,1),n=c(e.ascDesc,!1);return(e,i,o,s)=>$t(e,i,((r,u,a,c,f,h,d,p,m,g,x)=>{let w=r.pxRound,_=0==c.ori?ol:sl;const b={stroke:new Path2D,fill:null,clip:null,band:null,gaps:null,flags:1},k=b.stroke,v=1*c.dir*(0==c.ori?1:-1);o=t(a,o,s,1),s=t(a,o,s,-1);let y=[],M=!1,S=w(d(a[1==v?o:s],f,x,m)),E=w(h(u[1==v?o:s],c,g,p)),D=E;_(k,E,S);for(let e=1==v?o:s;e>=o&&s>=e;e+=v){let t=a[e],n=w(h(u[e],c,g,p));if(null==t){null===t&&(el(y,D,n),M=!0);continue}let i=w(d(t,f,x,m));M&&(el(y,D,n),M=!1),1==l?_(k,n,S):_(k,D,i),_(k,n,i),S=i,D=n}if(null!=r.fill){let t=b.fill=new Path2D(k),l=w(d(r.fillTo(e,i,r.min,r.max),f,x,m));_(t,D,l),_(t,E,l)}b.gaps=y=r.gaps(e,i,o,s,y);let T=r.width*we/2,z=n||1==l?T:-T,P=n||-1==l?-T:T;return y.forEach((e=>{e[0]+=z,e[1]+=P})),r.spanGaps||(b.clip=Qt(y,c.ori,p,m,g,x)),e.bands.length>0&&(b.band=Xt(e,i,o,s,k)),b}))},e.bars=function(e){const t=c((e=e||N).size,[.6,E,1]),l=e.align||0,n=(e.gap||0)*we,i=c(e.radius,0),o=1-t[0],s=c(t[1],E)*we,r=c(t[2],1)*we,u=c(e.disp,N),a=c(e.each,(()=>{})),{fill:f,stroke:h}=u;return(e,t,d,p)=>$t(e,t,((x,w,k,v,y,M,S,E,D,T,z)=>{let P=x.pxRound;const A=v.dir*(0==v.ori?1:-1),W=y.dir*(1==y.ori?1:-1);let Y,C,F=0==v.ori?rl:ul,H=0==v.ori?a:(e,t,l,n,i,o,s)=>{a(e,t,l,i,n,s,o)},R=x.fillTo(e,t,x.min,x.max),L=S(R,y,z,D),I=P(x.width*we),G=!1,O=null,N=null,j=null,B=null;null!=f&&null!=h&&(G=!0,O=f.values(e,t,d,p),N=new Map,new Set(O).forEach((e=>{null!=e&&N.set(e,new Path2D)})),j=h.values(e,t,d,p),B=new Map,new Set(j).forEach((e=>{null!=e&&B.set(e,new Path2D)})));let{x0:V,size:U}=u;if(null!=V&&null!=U){w=V.values(e,t,d,p),2==V.unit&&(w=w.map((t=>e.posToVal(E+t*T,v.key,!0))));let l=U.values(e,t,d,p);C=2==U.unit?l[0]*T:M(l[0],v,T,E)-M(0,v,T,E),C=P(C-I),Y=1==A?-I/2:C+I/2}else{let e=T;if(w.length>1){let t=null;for(let l=0,n=1/0;w.length>l;l++)if(void 0!==k[l]){if(null!=t){let i=m(w[l]-w[t]);n>i&&(n=i,e=m(M(w[l],v,T,E)-M(w[t],v,T,E)))}t=l}}C=P(_(s,b(r,e-e*o))-I-n),Y=(0==l?C/2:l==A?0:C)-l*A*n/2}const J={stroke:null,fill:null,clip:null,band:null,gaps:null,flags:3},q=e.bands.length>0;let K;q&&(J.band=new Path2D,K=P(S(y.max,y,z,D)));const Z=G?null:new Path2D,$=J.band;for(let l=1==A?d:p;l>=d&&p>=l;l+=A){let n=k[l],o=M(2!=v.distr||null!=u?w[l]:l,v,T,E),s=S(c(n,R),y,z,D),r=P(o-Y),a=P(b(s,L)),f=P(_(s,L)),h=a-f,d=i*C;null!=n&&(G?(I>0&&null!=j[l]&&F(B.get(j[l]),r,f+g(I/2),C,b(0,h-I),d),null!=O[l]&&F(N.get(O[l]),r,f+g(I/2),C,b(0,h-I),d)):F(Z,r,f+g(I/2),C,b(0,h-I),d),H(e,t,l,r-I/2,f,C+I,h)),q&&(1==W?(a=f,f=K):(f=a,a=K),h=a-f,F($,r-I/2,f,C+I,b(0,h),0))}return I>0&&(J.stroke=G?B:Z),J.fill=G?N:Z,J}))},e.spline=function(){return function(e){return(l,n,i,o)=>$t(l,n,((s,r,u,a,c,f,h,d,p,m,g)=>{let x,w,_,b=s.pxRound;0==a.ori?(x=nl,_=ol,w=fl):(x=il,_=sl,w=hl);const k=1*a.dir*(0==a.ori?1:-1);i=t(u,i,o,1),o=t(u,i,o,-1);let v=[],y=!1,M=b(f(r[1==k?i:o],a,m,d)),S=M,E=[],D=[];for(let e=1==k?i:o;e>=i&&o>=e;e+=k){let t=u[e],l=f(r[e],a,m,d);null!=t?(y&&(el(v,S,l),y=!1),E.push(S=l),D.push(h(u[e],c,g,p))):null===t&&(el(v,S,l),y=!0)}const T={stroke:e(E,D,x,_,w,b),fill:null,clip:null,band:null,gaps:null,flags:1},z=T.stroke;if(null!=s.fill&&null!=z){let e=T.fill=new Path2D(z),t=b(h(s.fillTo(l,n,s.min,s.max),c,g,p));_(e,S,t),_(e,M,t)}return T.gaps=v=s.gaps(l,n,i,o,v),s.spanGaps||(T.clip=Qt(v,a.ori,d,p,m,g)),l.bands.length>0&&(T.band=Xt(l,n,i,o,z)),T}))}(wl)}}return Fl}();
diff --git a/build/resources/main/static/plugins/uplot/uPlot.min.css b/build/resources/main/static/plugins/uplot/uPlot.min.css
new file mode 100644
index 0000000..c54627d
--- /dev/null
+++ b/build/resources/main/static/plugins/uplot/uPlot.min.css
@@ -0,0 +1 @@
+.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: 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";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;z-index: 100;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;z-index: 100;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;} \ No newline at end of file