diff options
author | AlisaLinUwU <alisalinuwu@gmail.com> | 2025-01-26 10:42:28 +0500 |
---|---|---|
committer | AlisaLinUwU <alisalinuwu@gmail.com> | 2025-01-26 10:42:28 +0500 |
commit | 0225bdb772d1334cc1aa7ab0fc3678df0864df6b (patch) | |
tree | 85a8c8e4fcf1d935fcbad54886b73410c8cb2e26 /src/main/resources/static/plugins/uplot |
Initializemain
Diffstat (limited to 'src/main/resources/static/plugins/uplot')
-rw-r--r-- | src/main/resources/static/plugins/uplot/uPlot.cjs.js | 5212 | ||||
-rw-r--r-- | src/main/resources/static/plugins/uplot/uPlot.esm.js | 5210 | ||||
-rw-r--r-- | src/main/resources/static/plugins/uplot/uPlot.iife.js | 5215 | ||||
-rw-r--r-- | src/main/resources/static/plugins/uplot/uPlot.iife.min.js | 2 | ||||
-rw-r--r-- | src/main/resources/static/plugins/uplot/uPlot.min.css | 1 |
5 files changed, 15640 insertions, 0 deletions
diff --git a/src/main/resources/static/plugins/uplot/uPlot.cjs.js b/src/main/resources/static/plugins/uplot/uPlot.cjs.js new file mode 100644 index 0000000..84a4894 --- /dev/null +++ b/src/main/resources/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/src/main/resources/static/plugins/uplot/uPlot.esm.js b/src/main/resources/static/plugins/uplot/uPlot.esm.js new file mode 100644 index 0000000..f7c486d --- /dev/null +++ b/src/main/resources/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/src/main/resources/static/plugins/uplot/uPlot.iife.js b/src/main/resources/static/plugins/uplot/uPlot.iife.js new file mode 100644 index 0000000..e0e776c --- /dev/null +++ b/src/main/resources/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/src/main/resources/static/plugins/uplot/uPlot.iife.min.js b/src/main/resources/static/plugins/uplot/uPlot.iife.min.js new file mode 100644 index 0000000..0403c32 --- /dev/null +++ b/src/main/resources/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&<[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/src/main/resources/static/plugins/uplot/uPlot.min.css b/src/main/resources/static/plugins/uplot/uPlot.min.css new file mode 100644 index 0000000..c54627d --- /dev/null +++ b/src/main/resources/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 |