diff options
| author | emilybrooks <emilybrooksemilybrooks@gmail.com> | 2024-06-30 09:42:49 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-06-30 09:42:49 -0700 |
| commit | b11600b26cf594f8e4b54bba3f99a39eaa554942 (patch) | |
| tree | 0e77e7bf6609741c61296ce3399fdc1a1ce8b632 | |
| parent | 235b6575c208667fa39592bfe0c885c3ca006595 (diff) | |
Add website (#197)
* add website
* newline
28 files changed, 20387 insertions, 0 deletions
diff --git a/docs/autumn.css b/docs/autumn.css new file mode 100644 index 0000000..fdf93e8 --- /dev/null +++ b/docs/autumn.css @@ -0,0 +1,9 @@ +body
+{
+ background: #14504a url(website_assets/bg_autumn.webp);
+ background-attachment: fixed;
+ background-position-x: 50%;
+ background-position-y: 100%;
+ background-repeat: no-repeat;
+ background-size: cover;
+}
diff --git a/docs/dist/uPlot.cjs.js b/docs/dist/uPlot.cjs.js new file mode 100644 index 0000000..07e5e24 --- /dev/null +++ b/docs/dist/uPlot.cjs.js @@ -0,0 +1,5961 @@ +/** +* Copyright (c) 2024, 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.30) +*/ + +'use strict'; + +const FEAT_TIME = true; + +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_SERIES = pre + "series"; +const LEGEND_MARKER = pre + "marker"; +const LEGEND_LABEL = pre + "label"; +const LEGEND_VALUE = pre + "value"; + +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 LEGEND_DISP = "--"; + +const domEnv = typeof window != 'undefined'; + +const doc = domEnv ? document : null; +const win = domEnv ? window : null; +const nav = domEnv ? navigator : null; + +let pxRatio; + +//export const canHover = domEnv && !win.matchMedia('(hover: none)').matches; + +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 = {...evOpts, capture: true}; + +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); +} + +domEnv && setPxRatio(); + +// 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++) { + let v = data[i]; + + if (v != null) { + if (v < _min) + _min = v; + if (v > _max) + _max = v; + } + } + } + + return [_min, _max]; +} + +function getMinMaxLog(data, _i0, _i1) { +// console.log("getMinMax()"); + + let _min = inf; + let _max = -inf; + + for (let i = _i0; i <= _i1; i++) { + let v = data[i]; + + if (v != null && v > 0) { + if (v < _min) + _min = v; + if (v > _max) + _max = v; + } + } + + return [_min, _max]; +} + +function rangeLog(min, max, base, fullMags) { + let minSign = sign(min); + let maxSign = sign(max); + + if (min == max) { + if (minSign == -1) { + min *= base; + max /= base; + } + else { + min /= base; + max *= base; + } + } + + let logFn = base == 10 ? log10 : log2; + + let growMinAbs = minSign == 1 ? floor : ceil; + let growMaxAbs = maxSign == 1 ? ceil : floor; + + let minExp = growMinAbs(logFn(abs(min))); + let maxExp = growMaxAbs(logFn(abs(max))); + + let minIncr = pow(base, minExp); + let maxIncr = pow(base, maxExp); + + // fix values like Math.pow(10, -5) === 0.000009999999999999999 + if (base == 10) { + if (minExp < 0) + minIncr = roundDec(minIncr, -minExp); + if (maxExp < 0) + maxIncr = roundDec(maxIncr, -maxExp); + } + + if (fullMags || base == 2) { + min = minIncr * minSign; + max = maxIncr * maxSign; + } + else { + min = incrRoundDn(min, minIncr); + max = incrRoundUp(max, maxIncr); + } + + 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; + let deltaMag = log10(delta); + + let scalarMax = max(abs(_min), abs(_max)); + let scalarMag = log10(scalarMax); + + let scalarMagDelta = abs(scalarMag - deltaMag); + + // 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 + // or range is 11+ orders of magnitude below raw values, e.g. 99999999.99999996 - 100000000.00000004 + if (delta < 1e-9 || scalarMagDelta > 10) { + 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 || scalarMax || 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 numFormatter = new Intl.NumberFormat(domEnv ? nav.language : 'en-US'); +const fmtNum = val => numFormatter.format(val); + +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 clamp(num, _min, _max) { + return min(max(num, _min), _max); +} + +function fnOrSelf(v) { + return typeof v == "function" ? v : () => v; +} + +const noop = () => {}; + +const retArg0 = _0 => _0; + +const retArg1 = (_0, _1) => _1; + +const retNull = _ => null; + +const retTrue = _ => true; + +const retEq = (a, b) => a == b; + +// this will probably prevent tick incrs > 14 decimal places +// (we generate up to 17 dec, see fixedDec const) +const fixFloat = v => roundDec(v, 14); + +function incrRound(num, incr) { + return fixFloat(roundDec(fixFloat(num/incr))*incr); +} + +function incrRoundUp(num, incr) { + return fixFloat(ceil(fixFloat(num/incr))*incr); +} + +function incrRoundDn(num, incr) { + return fixFloat(floor(fixFloat(num/incr))*incr); +} + +// https://stackoverflow.com/a/48764436 +// rounds half away from zero +function roundDec(val, dec = 0) { + if (isInt(val)) + return val; +// else if (dec == 0) +// return round(val); + + let p = 10 ** dec; + let n = (val * p) * (1 + Number.EPSILON); + return round(n) / p; +} + +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; +const isInt = Number.isInteger; +const isUndef = v => v === void 0; + +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'; +} + +const TypedArray = Object.getPrototypeOf(Uint8Array); + +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 (o instanceof TypedArray) // also (ArrayBuffer.isView(o) && !(o instanceof DataView)) + 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) { + if (allHeadersSame(tables)) { + // console.log('cheap join!'); + + let table = tables[0].slice(); + + for (let i = 1; i < tables.length; i++) + table.push(...tables[i].slice(1)); + + if (!isAsc(table[0])) + table = sortCols(table); + + return table; + } + + 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; + +// TODO: https://github.com/dy/sort-ids (~2x faster for 1e5+ arrays) +function sortCols(table) { + let head = table[0]; + let rlen = head.length; + + let idxs = Array(rlen); + for (let i = 0; i < idxs.length; i++) + idxs[i] = i; + + idxs.sort((i0, i1) => head[i0] - head[i1]); + + let table2 = []; + for (let i = 0; i < table.length; i++) { + let row = table[i]; + let row2 = Array(rlen); + + for (let j = 0; j < rlen; j++) + row2[j] = row[idxs[j]]; + + table2.push(row2); + } + + return table2; +} + +// test if we can do cheap join (all join fields same) +function allHeadersSame(tables) { + let vals0 = tables[0][0]; + let len0 = vals0.length; + + for (let i = 1; i < tables.length; i++) { + let vals1 = tables[i][0]; + + if (vals1.length != len0) + return false; + + if (vals1 != vals0) { + for (let j = 0; j < len0; j++) { + if (vals1[j] != vals0[j]) + return false; + } + } + } + + return true; +} + +function isAsc(vals, samples = 100) { + const len = vals.length; + + // empty or single value + if (len <= 1) + return true; + + // skip leading & trailing nullish + let firstIdx = 0; + let lastIdx = len - 1; + + while (firstIdx <= lastIdx && vals[firstIdx] == null) + firstIdx++; + + while (lastIdx >= firstIdx && vals[lastIdx] == null) + lastIdx--; + + // all nullish or one value surrounded by nullish + if (lastIdx <= firstIdx) + return true; + + const stride = max(1, floor((lastIdx - firstIdx + 1) / samples)); + + for (let prevVal = vals[firstIdx], i = firstIdx + stride; i <= lastIdx; i += stride) { + const v = vals[i]; + + if (v != null) { + if (v <= prevVal) + return false; + + prevVal = v; + } + } + + return true; +} + +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, seriesIdx, dataIdx) => dataIdx == null ? LEGEND_DISP : 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, + mount: noop, + 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 sp.size; +} + +const moveTuple = [0,0]; + +function cursorMove(self, mouseLeft1, mouseTop1) { + moveTuple[0] = mouseLeft1; + moveTuple[1] = mouseTop1; + return moveTuple; +} + +function filtBtn0(self, targ, handle, onlyTarg = true) { + return e => { + e.button == 0 && (!onlyTarg || e.target == targ) && handle(e); + }; +} + +function filtTarg(self, targ, handle, onlyTarg = true) { + return e => { + (!onlyTarg || e.target == targ) && handle(e); + }; +} + +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, // legend clicks, not .u-over clicks + dblclick: filtBtn0, + + mousemove: filtTarg, + mouseleave: filtTarg, + mouseenter: filtTarg, + }, + + drag: { + setScale: true, + x: true, + y: false, + dist: 0, + uni: null, + click: (self, e) => { + // e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + }, + _x: false, + _y: false, + }, + + focus: { + dist: (self, seriesIdx, dataIdx, valPos, curPos) => valPos - curPos, + prox: -1, + bias: 0, + }, + + hover: { + skip: [void 0], + prox: null, + bias: 0, + }, + + left: -10, + top: -10, + idx: null, + dataIdx: null, + idxs: null, + + event: null, +}; + +const axisLines = { + show: true, + stroke: "rgba(0,0,0,0.07)", + width: 2, +// dash: [], +}; + +const grid = assign({}, axisLines, { + filter: retArg1, +}); + +const ticks = assign({}, grid, { + size: 10, +}); + +const border = assign({}, axisLines, { + show: false, +}); + +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 lineGap = 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, + border, + font, + lineGap, + 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 (logBase == 10 && exp < 0) + foundIncr = roundDec(foundIncr, -exp); + + let split = scaleMin; + + do { + splits.push(split); + split = split + foundIncr; + + if (logBase == 10) + split = roundDec(split, 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/; + +const _filt = (splits, distr, re, keepMod) => splits.map((v, i) => ((distr == 4 && v == 0) || i % keepMod == 0 && re.test(v.toExponential()[v < 0 ? 1 : 0])) ? v : null); + +function log10AxisValsFilt(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 + ); + + if (re == RE_1) { + let magSpace = abs(valToPos(1, scaleKey) - _10); + + if (magSpace < minSpace) + return _filt(splits.slice().reverse(), sc.distr, re, ceil(minSpace / magSpace)).reverse(); // max->min skip + } + + return _filt(splits, sc.distr, re, 1); +} + +function log2AxisValsFilt(self, splits, axisIdx, foundSpace, foundIncr) { + let axis = self.axes[axisIdx]; + let scaleKey = axis.scale; + let minSpace = axis._space; + let valToPos = self.valToPos; + + let magSpace = abs(valToPos(1, scaleKey) - valToPos(2, scaleKey)); + + if (magSpace < minSpace) + return _filt(splits.slice().reverse(), 3, RE_ALL, ceil(minSpace / magSpace)).reverse(); // max->min skip + + return splits; +} + +function numSeriesVal(self, val, seriesIdx, dataIdx) { + return dataIdx == null ? LEGEND_DISP : 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, + border, + font, + lineGap, + 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; +} + +const facet = { + scale: null, + auto: true, + sorted: 0, + + // internal caches + min: inf, + max: -inf, +}; + +const gaps = (self, seriesIdx, idx0, idx1, nullGaps) => nullGaps; + +const xySeriesOpts = { + show: true, + auto: true, + sorted: 0, + gaps, + alpha: 1, + facets: [ + assign({}, facet, {scale: 'x'}), + assign({}, facet, {scale: 'y'}), + ], +}; + +const ySeriesOpts = { + scale: "y", + auto: true, + sorted: 0, + show: true, + spanGaps: false, + gaps, + 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 mode = u.mode; + const series = u.series[seriesIdx]; + const data = mode == 2 ? u._data[seriesIdx] : u._data; + const scales = u.scales; + const bbox = u.bbox; + + let dx = data[0], + dy = mode == 2 ? data[1] : data[seriesIdx], + sx = mode == 2 ? scales[series.facets[0].scale] : scales[u.series[0].scale], + sy = 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, + ) + ); +} + +function bandFillClipDirs(self, seriesIdx) { + let fillDir = 0; + + // 2 bits, -1 | 1 + let clipDirs = 0; + + let bands = ifNull(self.bands, EMPTY_ARR); + + for (let i = 0; i < bands.length; i++) { + let b = bands[i]; + + // is a "from" band edge + if (b.series[0] == seriesIdx) + fillDir = b.dir; + // is a "to" band edge + else if (b.series[1] == seriesIdx) { + if (b.dir == 1) + clipDirs |= 1; + else + clipDirs |= 2; + } + } + + return [ + fillDir, + ( + clipDirs == 1 ? -1 : // neg only + clipDirs == 2 ? 1 : // pos only + clipDirs == 3 ? 2 : // both + 0 // neither + ) + ]; +} + +function seriesFillTo(self, seriesIdx, dataMin, dataMax, bandFillDir) { + let mode = self.mode; + let series = self.series[seriesIdx]; + let scaleKey = mode == 2 ? series.facets[1].scale : series.scale; + let scale = self.scales[scaleKey]; + + return ( + bandFillDir == -1 ? scale.min : + bandFillDir == 1 ? scale.max : + scale.distr == 3 ? ( + scale.dir == 1 ? scale.min : + scale.max + ) : 0 + ); +} + +// creates inverted band clip path (from stroke path -> yMax || yMin) +// clipDir is always inverse of fillDir +// default clip dir is upwards (1), since default band fill is downwards/fillBelowTo (-1) (highIdx -> lowIdx) +function clipBandLine(self, seriesIdx, idx0, idx1, strokePath, clipDir) { + 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 or lower y limit + let yLimit = pxRound(valToPosY(clipDir == 1 ? scaleY.max : scaleY.min, 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; + + // hack to ensure we expand the clip enough to avoid cutting off strokes at edges + let maxStrokeWidth = 10; + + w > 0 && rect(clip, prevGapEnd, plotTop - maxStrokeWidth / 2, w, plotTop + plotHgt + maxStrokeWidth); + } + + 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 findGaps(xs, ys, idx0, idx1, dir, pixelForX, align) { + let gaps = []; + let len = xs.length; + + for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) { + let yVal = ys[i]; + + if (yVal === null) { + let fr = i, to = i; + + if (dir == 1) { + while (++i <= idx1 && ys[i] === null) + to = i; + } + else { + while (--i >= idx0 && ys[i] === null) + to = i; + } + + let frPx = pixelForX(xs[fr]); + let toPx = to == fr ? frPx : pixelForX(xs[to]); + + // if value adjacent to edge null is same pixel, then it's partially + // filled and gap should start at next pixel + let fri2 = fr - dir; + let frPx2 = align <= 0 && fri2 >= 0 && fri2 < len ? pixelForX(xs[fri2]) : frPx; + // if (frPx2 == frPx) + // frPx++; + // else + frPx = frPx2; + + let toi2 = to + dir; + let toPx2 = align >= 0 && toi2 >= 0 && toi2 < len ? pixelForX(xs[toi2]) : toPx; + // if (toPx2 == toPx) + // toPx--; + // else + toPx = toPx2; + + if (toPx >= frPx) + gaps.push([frPx, toPx]); // addGap + } + } + + return gaps; +} + +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); }; + + // TODO (pending better browser support): https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/roundRect + return (p, x, y, w, h, endRad = 0, baseRad = 0) => { + if (endRad == 0 && baseRad == 0) + rect(p, x, y, w, h); + else { + endRad = min(endRad, w / 2, h / 2); + baseRad = min(baseRad, 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 + endRad, y); + arcTo(p, x + w, y, x + w, y + h, endRad); + arcTo(p, x + w, y + h, x, y + h, baseRad); + arcTo(p, x, y + h, x, y, baseRad); + arcTo(p, x, y, x + w, y, endRad); + 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(opts) { + const alignGaps = ifNull(opts?.alignGaps, 0); + + 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 pixelForX = val => pxRound(valToPosX(val, scaleX, xDim, xOff)); + let pixelForY = val => pxRound(valToPosY(val, scaleY, yDim, yOff)); + + 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, drawnAtX; + + let accX = pixelForX(dataX[dir == 1 ? idx0 : idx1]); + + // data edges + let lftIdx = nonNullIdx(dataY, idx0, idx1, 1 * dir); + let rgtIdx = nonNullIdx(dataY, idx0, idx1, -1 * dir); + let lftX = pixelForX(dataX[lftIdx]); + let rgtX = pixelForX(dataX[rgtIdx]); + + let hasGap = false; + + for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) { + let x = pixelForX(dataX[i]); + let yVal = dataY[i]; + + if (x == accX) { + if (yVal != null) { + outY = pixelForY(yVal); + + if (minY == inf) { + lineTo(stroke, x, outY); + inY = outY; + } + + minY = min(outY, minY); + maxY = max(outY, maxY); + } + else { + if (yVal === null) + hasGap = true; + } + } + else { + if (minY != inf) { + drawAcc(stroke, accX, minY, maxY, inY, outY); + drawnAtX = accX; + } + + if (yVal != null) { + outY = pixelForY(yVal); + lineTo(stroke, x, outY); + minY = maxY = inY = outY; + } + else { + minY = inf; + maxY = -inf; + + if (yVal === null) + hasGap = true; + } + + accX = x; + } + } + + if (minY != inf && minY != maxY && drawnAtX != accX) + drawAcc(stroke, accX, minY, maxY, inY, outY); + + let [ bandFillDir, bandClipDir ] = bandFillClipDirs(u, seriesIdx); + + if (series.fill != null || bandFillDir != 0) { + let fill = _paths.fill = new Path2D(stroke); + + let fillToVal = series.fillTo(u, seriesIdx, series.min, series.max, bandFillDir); + let fillToY = pixelForY(fillToVal); + + lineTo(fill, rgtX, fillToY); + lineTo(fill, lftX, fillToY); + } + + if (!series.spanGaps) { + // console.time('gaps'); + let gaps = []; + + hasGap && gaps.push(...findGaps(dataX, dataY, idx0, idx1, dir, pixelForX, alignGaps)); + + // console.timeEnd('gaps'); + + // console.log('gaps', JSON.stringify(gaps)); + + _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps); + + _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim); + } + + if (bandClipDir != 0) { + _paths.band = bandClipDir == 2 ? [ + clipBandLine(u, seriesIdx, idx0, idx1, stroke, -1), + clipBandLine(u, seriesIdx, idx0, idx1, stroke, 1), + ] : clipBandLine(u, seriesIdx, idx0, idx1, stroke, bandClipDir); + } + + return _paths; + }); + }; +} + +// BUG: align: -1 behaves like align: 1 when scale.dir: -1 +function stepped(opts) { + const align = ifNull(opts.align, 1); + // whether to draw ascenders/descenders at null/gap bondaries + const ascDesc = ifNull(opts.ascDesc, false); + const alignGaps = ifNull(opts.alignGaps, 0); + const extend = ifNull(opts.extend, 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 { left, width } = u.bbox; + + let pixelForX = val => pxRound(valToPosX(val, scaleX, xDim, xOff)); + let pixelForY = val => pxRound(valToPosY(val, scaleY, yDim, yOff)); + + 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 = scaleX.dir * (scaleX.ori == 0 ? 1 : -1); + + idx0 = nonNullIdx(dataY, idx0, idx1, 1); + idx1 = nonNullIdx(dataY, idx0, idx1, -1); + + let prevYPos = pixelForY(dataY[dir == 1 ? idx0 : idx1]); + let firstXPos = pixelForX(dataX[dir == 1 ? idx0 : idx1]); + let prevXPos = firstXPos; + + let firstXPosExt = firstXPos; + + if (extend && align == -1) { + firstXPosExt = left; + lineTo(stroke, firstXPosExt, prevYPos); + } + + lineTo(stroke, firstXPos, prevYPos); + + for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) { + let yVal1 = dataY[i]; + + if (yVal1 == null) + continue; + + let x1 = pixelForX(dataX[i]); + let y1 = pixelForY(yVal1); + + if (align == 1) + lineTo(stroke, x1, prevYPos); + else + lineTo(stroke, prevXPos, y1); + + lineTo(stroke, x1, y1); + + prevYPos = y1; + prevXPos = x1; + } + + let prevXPosExt = prevXPos; + + if (extend && align == 1) { + prevXPosExt = left + width; + lineTo(stroke, prevXPosExt, prevYPos); + } + + let [ bandFillDir, bandClipDir ] = bandFillClipDirs(u, seriesIdx); + + if (series.fill != null || bandFillDir != 0) { + let fill = _paths.fill = new Path2D(stroke); + + let fillTo = series.fillTo(u, seriesIdx, series.min, series.max, bandFillDir); + let fillToY = pixelForY(fillTo); + + lineTo(fill, prevXPosExt, fillToY); + lineTo(fill, firstXPosExt, fillToY); + } + + if (!series.spanGaps) { + // console.time('gaps'); + let gaps = []; + + gaps.push(...findGaps(dataX, dataY, idx0, idx1, dir, pixelForX, alignGaps)); + + // console.timeEnd('gaps'); + + // console.log('gaps', JSON.stringify(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; + }); + + _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps); + + _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim); + } + + if (bandClipDir != 0) { + _paths.band = bandClipDir == 2 ? [ + clipBandLine(u, seriesIdx, idx0, idx1, stroke, -1), + clipBandLine(u, seriesIdx, idx0, idx1, stroke, 1), + ] : clipBandLine(u, seriesIdx, idx0, idx1, stroke, bandClipDir); + } + + return _paths; + }); + }; +} + +function findColWidth(dataX, dataY, valToPosX, scaleX, xDim, xOff, colWid = inf) { + 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; + } + } + } + + return colWid; +} + +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); + + let ro = opts.radius; + + ro = + // [valueRadius, baselineRadius] + ro == null ? [0, 0] : + typeof ro == 'number' ? [ro, 0] : ro; + + const radiusFn = fnOrSelf(ro); + + const gapFactor = 1 - size[0]; + const _maxWidth = ifNull(size[1], inf); + const _minWidth = ifNull(size[2], 1); + + 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; + let _align = align; + + let extraGap = _extraGap * pxRatio; + let maxWidth = _maxWidth * pxRatio; + let minWidth = _minWidth * pxRatio; + + let valRadius, baseRadius; + + if (scaleX.ori == 0) + [valRadius, baseRadius] = radiusFn(u, seriesIdx); + else + [baseRadius, valRadius] = radiusFn(u, seriesIdx); + + 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); + }; + + // band where this series is the "from" edge + let band = ifNull(u.bands, EMPTY_ARR).find(b => b.series[0] == seriesIdx); + + let fillDir = band != null ? band.dir : 0; + let fillTo = series.fillTo(u, seriesIdx, series.min, series.max, fillDir); + let fillToY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff)); + + // barWid is to center of stroke + let xShift, barWid, fullGap, colWid = xDim; + + let strokeWidth = pxRound(series.width * pxRatio); + + let multiPath = false; + + let fillColors = null; + let fillPaths = null; + let strokeColors = null; + let strokePaths = null; + + if (dispFills != null && (strokeWidth == 0 || 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()); + }); + + if (strokeWidth > 0) { + 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) { + _align = 1; + 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) + + colWid = findColWidth(dataX, dataY, valToPosX, scaleX, xDim, xOff, colWid); + + let gapWid = colWid - barWid; + fullGap = gapWid + extraGap; + } + else { + colWid = findColWidth(dataX, dataY, valToPosX, scaleX, xDim, xOff, colWid); + + let gapWid = colWid * gapFactor; + + fullGap = gapWid + extraGap; + barWid = colWid - fullGap; + } + + if (fullGap < 1) + fullGap = 0; + + if (strokeWidth >= barWid / 2) + strokeWidth = 0; + + // for small gaps, disable pixel snapping since gap inconsistencies become noticible and annoying + if (fullGap < 5) + pxRound = retArg0; + + let insetStroke = fullGap > 0; + + let rawBarWid = colWid - fullGap - (insetStroke ? strokeWidth : 0); + + barWid = pxRound(clamp(rawBarWid, minWidth, maxWidth)); + + xShift = (_align == 0 ? barWid / 2 : _align == _dirX ? 0 : barWid) - _align * _dirX * ((_align == 0 ? extraGap / 2 : 0) + (insetStroke ? strokeWidth / 2 : 0)); + + + const _paths = {stroke: null, fill: null, clip: null, band: null, gaps: null, flags: 0}; // disp, geom + + const stroke = multiPath ? null : new Path2D(); + + let dataY0 = null; + + if (band != null) + dataY0 = u.data[band.series[1]]; + else { + let { y0, y1 } = disp; + + if (y0 != null && y1 != null) { + dataY = y1.values(u, seriesIdx, idx0, idx1); + dataY0 = y0.values(u, seriesIdx, idx0, idx1); + } + } + + let radVal = valRadius * barWid; + let radBase = baseRadius * barWid; + + for (let i = _dirX == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dirX) { + let yVal = dataY[i]; + + if (yVal == null) + continue; + + if (dataY0 != null) { + let yVal0 = dataY0[i] ?? 0; + + if (yVal - yVal0 == 0) + continue; + + fillToY = valToPosY(yVal0, scaleY, yDim, yOff); + } + + 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, fillTo), scaleY, yDim, yOff); + + let lft = pxRound(xPos - xShift); + let btm = pxRound(max(yPos, fillToY)); + let top = pxRound(min(yPos, fillToY)); + // this includes the stroke + let barHgt = btm - top; + + if (yVal != null) { // && yVal != fillTo (0 height bar) + let rv = yVal < 0 ? radBase : radVal; + let rb = yVal < 0 ? radVal : radBase; + + if (multiPath) { + if (strokeWidth > 0 && strokeColors[i] != null) + rect(strokePaths.get(strokeColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), rv, rb); + + if (fillColors[i] != null) + rect(fillPaths.get(fillColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), rv, rb); + } + else + rect(stroke, lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), rv, rb); + + each(u, seriesIdx, i, + lft - strokeWidth / 2, + top, + barWid + strokeWidth, + barHgt, + ); + } + } + + if (strokeWidth > 0) + _paths.stroke = multiPath ? strokePaths : stroke; + else if (!multiPath) { + _paths._fill = series.width == 0 ? series._fill : series._stroke ?? series._fill; + _paths.width = 0; + } + + _paths.fill = multiPath ? fillPaths : stroke; + + return _paths; + }); + }; +} + +function splineInterp(interp, opts) { + const alignGaps = ifNull(opts?.alignGaps, 0); + + 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 pixelForX = val => pxRound(valToPosX(val, scaleX, xDim, xOff)); + let pixelForY = val => pxRound(valToPosY(val, scaleY, yDim, yOff)); + + let moveTo, bezierCurveTo, lineTo; + + if (scaleX.ori == 0) { + moveTo = moveToH; + lineTo = lineToH; + bezierCurveTo = bezierCurveToH; + } + else { + moveTo = moveToV; + lineTo = lineToV; + bezierCurveTo = bezierCurveToV; + } + + const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1); + + idx0 = nonNullIdx(dataY, idx0, idx1, 1); + idx1 = nonNullIdx(dataY, idx0, idx1, -1); + + let firstXPos = pixelForX(dataX[dir == 1 ? idx0 : idx1]); + let prevXPos = firstXPos; + + let xCoords = []; + let yCoords = []; + + for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) { + let yVal = dataY[i]; + + if (yVal != null) { + let xVal = dataX[i]; + let xPos = pixelForX(xVal); + + xCoords.push(prevXPos = xPos); + yCoords.push(pixelForY(dataY[i])); + } + } + + 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; + + let [ bandFillDir, bandClipDir ] = bandFillClipDirs(u, seriesIdx); + + if (series.fill != null || bandFillDir != 0) { + let fill = _paths.fill = new Path2D(stroke); + + let fillTo = series.fillTo(u, seriesIdx, series.min, series.max, bandFillDir); + let fillToY = pixelForY(fillTo); + + lineTo(fill, prevXPos, fillToY); + lineTo(fill, firstXPos, fillToY); + } + + if (!series.spanGaps) { + // console.time('gaps'); + let gaps = []; + + gaps.push(...findGaps(dataX, dataY, idx0, idx1, dir, pixelForX, alignGaps)); + + // console.timeEnd('gaps'); + + // console.log('gaps', JSON.stringify(gaps)); + + _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps); + + _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim); + } + + if (bandClipDir != 0) { + _paths.band = bandClipDir == 2 ? [ + clipBandLine(u, seriesIdx, idx0, idx1, stroke, -1), + clipBandLine(u, seriesIdx, idx0, idx1, stroke, 1), + ] : clipBandLine(u, seriesIdx, idx0, idx1, stroke, bandClipDir); + } + + 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, opts); +} + +// 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() { + for (let u of cursorPlots) + u.syncRect(true); +} + +if (domEnv) { + on(resize, win, invalidateRects); + on(scroll, win, invalidateRects, true); + on(dppxchange, win, () => { uPlot.pxRatio = pxRatio; }); +} + +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); + + on("click", wrap, e => { + if (e.target === over) { + let didDrag = mouseLeft1 != mouseLeft0 || mouseTop1 != mouseTop0; + didDrag && drag.click(self, e); + } + }, true); + + 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); + b.dir = ifNull(b.dir, -1); + }); + + 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); + + sc.key = scaleKey; + + let isTime = sc.time; + + let rn = sc.range; + + let rangeIsArr = isArr(rn); + + if (scaleKey != xScaleKey || (mode == 2 && !isTime)) { + // 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 legendTable; + let legendHead; + let legendBody; + 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] = LEGEND_DISP; + } + + if (showLegend) { + legendTable = placeTag("table", LEGEND, root); + legendBody = placeTag("tbody", null, legendTable); + + // allows legend to be moved out of root + legend.mount(self, legendTable); + + if (multiValLegend) { + legendHead = placeTag("thead", null, legendTable, legendBody); + + let head = placeTag("tr", null, legendHead); + placeTag("th", null, head); + + for (var key in legendCols) + placeTag("th", LEGEND_LABEL, head).textContent = key; + } + else { + addClass(legendTable, LEGEND_INLINE); + legend.live && addClass(legendTable, 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, legendBody, legendBody.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; + + setCursorEvent(e); + + 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); + }, false); + + if (cursorFocus) { + onMouse(mouseenter, label, e => { + if (cursor._lock) + return; + + setCursorEvent(e); + + setSeries(series.indexOf(s), FOCUS_TRUE, true, syncOpts.setSeries); + }, false); + } + } + + 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, onlyTarg = true) { + const targListeners = mouseListeners.get(targ) || {}; + const listener = cursor.bind[ev](self, targ, fn, onlyTarg); + + 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; + + // previous values for diffing + let _plotLftCss = plotLftCss; + let _plotTopCss = plotTopCss; + let _plotWidCss = plotWidCss; + let _plotHgtCss = plotHgtCss; + + + 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 shouldSetSelect = 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; + + 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); + + if (cursor.dataIdx == null) { + let hov = cursor.hover; + + let skip = hov.skip = new Set(hov.skip ?? []); + skip.add(void 0); // alignment artifacts + let prox = hov.prox = fnOrSelf(hov.prox); + let bias = hov.bias ??= 0; + + // TODO: only scan between in-view idxs (i0, i1) + cursor.dataIdx = (self, seriesIdx, cursorIdx, valAtPosX) => { + if (seriesIdx == 0) + return cursorIdx; + + let idx2 = cursorIdx; + + let _prox = prox(self, seriesIdx, cursorIdx, valAtPosX) ?? inf; + let withProx = _prox >= 0 && _prox < inf; + let xDim = scaleX.ori == 0 ? plotWidCss : plotHgtCss; + let cursorLft = cursor.left; + + let xValues = data[0]; + let yValues = data[seriesIdx]; + + if (skip.has(yValues[cursorIdx])) { + idx2 = null; + + let nonNullLft = null, + nonNullRgt = null, + j; + + if (bias == 0 || bias == -1) { + j = cursorIdx; + while (nonNullLft == null && j-- > 0) { + if (!skip.has(yValues[j])) + nonNullLft = j; + } + } + + if (bias == 0 || bias == 1) { + j = cursorIdx; + while (nonNullRgt == null && j++ < yValues.length) { + if (!skip.has(yValues[j])) + nonNullRgt = j; + } + } + + if (nonNullLft != null || nonNullRgt != null) { + if (withProx) { + let lftPos = nonNullLft == null ? -Infinity : valToPosX(xValues[nonNullLft], scaleX, xDim, 0); + let rgtPos = nonNullRgt == null ? Infinity : valToPosX(xValues[nonNullRgt], scaleX, xDim, 0); + + let lftDelta = cursorLft - lftPos; + let rgtDelta = rgtPos - cursorLft; + + if (lftDelta <= rgtDelta) { + if (lftDelta <= _prox) + idx2 = nonNullLft; + } else { + if (rgtDelta <= _prox) + idx2 = nonNullRgt; + } + } + else { + idx2 = + nonNullRgt == null ? nonNullLft : + nonNullLft == null ? nonNullRgt : + cursorIdx - nonNullLft <= nonNullRgt - cursorIdx ? nonNullLft : nonNullRgt; + } + } + } + else if (withProx) { + let dist = abs(cursorLft - valToPosX(xValues[cursorIdx], scaleX, xDim, 0)); + + if (dist > _prox) + idx2 = null; + } + + return idx2; + }; + } + + const setCursorEvent = e => { cursor.event = e; }; + + 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]; + // position caches in CSS pixels + let cursorPtsLft = [null]; + let cursorPtsTop = [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(max(1, 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); + + if (pt != null) { + cursorPts.splice(i, 0, pt); + cursorPtsLft.splice(i, 0, 0); + cursorPtsTop.splice(i, 0, 0); + } + } + + fire("addSeries", i); + } + + function addSeries(opts, si) { + si = si == null ? series.length : si; + + opts = mode == 1 ? setDefault(opts, si, xSeriesOpts, ySeriesOpts) : setDefault(opts, si, null, xySeriesOpts); + + 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); + + if (cursorPts.length > 1) { + cursorPts.splice(i, 1)[0].remove(); + cursorPtsLft.splice(i, 1); + cursorPtsTop.splice(i, 1); + } + } + + // TODO: de-init no-longer-needed scales? + + fire("delSeries", i); + } + + self.delSeries = delSeries; + + const sidesWithAxes = [false, false, false, false]; + + function initAxis(axis, i) { + axis._show = axis.show; + + if (axis.show) { + let isVt = axis.side % 2; + + let sc = scales[axis.scale]; + + // this can occur if all series specify non-default scales + if (sc == null) { + axis.scale = isVt ? series[1].scale : xScaleKey; + sc = scales[axis.scale]; + } + + // also set defaults for incrs & values based on axis distr + let isTime = sc.time; + + axis.size = fnOrSelf(axis.size); + axis.space = fnOrSelf(axis.space); + axis.rotate = fnOrSelf(axis.rotate); + + if (isArr(axis.incrs)) { + axis.incrs.forEach(incr => { + !fixedDec.has(incr) && fixedDec.set(incr, guessDec(incr)); + }); + } + + 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); + axis.border.stroke = fnOrSelf(axis.border.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 && sc.log == 10 ? log10AxisValsFilt : sc.distr == 3 && sc.log == 2 ? log2AxisValsFilt : 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) { + data = _data == null ? [] : _data; + + self.data = self._data = data; + + if (mode == 2) { + dataLen = 0; + for (let i = 1; i < series.length; i++) + dataLen += data[i][0].length; + } + else { + if (data.length == 0) + self.data = self._data = data = [[]]; + + data0 = data[0]; + dataLen = data0.length; + + let scaleData = data; + + if (xScaleDistr == 2) { + scaleData = data.slice(); + + let _data0 = scaleData[0] = Array(dataLen); + for (let i = 0; i < dataLen; i++) + _data0[i] = i; + } + + self._data = data = scaleData; + } + + resetYSeries(true); + + fire("setData"); + + // forces x axis tick values to re-generate when neither x scale nor y scale changes + // in ordinal mode, scale range is by index, so will not change if new data has same length, but tick values are from data + if (xScaleDistr == 2) { + shouldConvergeSize = true; + + /* or somewhat cheaper, and uglier: + if (ready) { + // logic extracted from axesCalc() + let i = 0; + let axis = axes[i]; + let _splits = axis._splits.map(i => data0[i]); + let [_incr, _space] = axis._found; + let incr = data0[_splits[1]] - data0[_splits[0]]; + axis._values = axis.values(self, axis.filter(self, _splits, i, _space, incr), i, _space, incr); + } + */ + } + + if (_resetScales !== false) { + let xsc = scaleX; + + if (xsc.auto(self, viaAutoScaleX)) + autoScaleX(); + else + _setScale(xScaleKey, xsc.min, xsc.max); + + shouldSetCursor = 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 (_min == _max) { + 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, width, dash, cap, fill, join) { + stroke ??= transparent; + 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, sorted = 0) { + if (data.length > 0 && 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, sorted)) : [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]); + } + } + + const AUTOSCALE = {min: null, max: null}; + + function setScales() { + // log("setScales()", arguments); + + // implicitly add auto scales, and unranged scales + for (let k in scales) { + let sc = scales[k]; + + if (pendScales[k] == null && + ( + // scales that have never been set (on init) + sc.min == null || + // or auto scales when the x scale was explicitly set + pendScales[xScaleKey] != null && sc.auto(self, viaAutoScaleX) + ) + ) { + pendScales[k] = AUTOSCALE; + } + } + + // implicitly add dependent scales + for (let k in scales) { + let sc = scales[k]; + + if (pendScales[k] == null && sc.from != null && pendScales[sc.from] != null) + pendScales[k] = AUTOSCALE; + } + + // explicitly setting the x-scale invalidates everything (acts as redraw) + if (pendScales[xScaleKey] != null) + resetYSeries(true); // TODO: only reset series on auto scales? + + let wipScales = {}; + + for (let k in pendScales) { + let psc = pendScales[k]; + + if (psc != null) { + let wsc = wipScales[k] = copy(scales[k], fastIsObj); + + if (psc.min != null) + assign(wsc, psc); + 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 psc = pendScales[k]; + + if (psc == null) + return; + + let wsc = wipScales[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]); + + // don't try to contract same or adjacent idxs + if (i1 - i0 > 1) { + // 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.sorted); + + 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]; + + let wscx = wipScales[xScaleKey]; + let wscy = wipScales[yScaleKey]; + + // null can happen when only x is zoomed, but y has static range and doesnt get auto-added to pending + wscx != null && accScale(wscx, pendScales[xScaleKey], xFacet, xData, xFacet.sorted); + wscy != null && accScale(wscy, pendScales[yScaleKey], yFacet, yData, yFacet.sorted); + + // 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 && cursor.left >= 0) + shouldSetCursor = shouldSetLegend = true; + } + + 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) { + cacheStrokeFill(i, false); + cacheStrokeFill(i, true); + + if (s._paths == null) { + if (ctxAlpha != s.alpha) + ctx.globalAlpha = ctxAlpha = s.alpha; + + let _idxs = mode == 2 ? [0, data[i][0].length - 1] : getOuterIdxs(data[i]); + s._paths = s.paths(self, i, _idxs[0], _idxs[1]); + + if (ctxAlpha != 1) + ctx.globalAlpha = ctxAlpha = 1; + } + } + }); + + series.forEach((s, i) => { + if (i > 0 && s.show) { + if (ctxAlpha != s.alpha) + ctx.globalAlpha = ctxAlpha = s.alpha; + + s._paths != null && drawPath(i, false); + + { + let _gaps = s._paths != null ? s._paths.gaps : null; + + let show = s.points.show(self, i, i0, i1, _gaps); + let idxs = s.points.filter(self, i, show, _gaps); + + 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 { + stroke, + fill, + clip: gapsClip, + flags, + + _stroke: strokeStyle = s._stroke, + _fill: fillStyle = s._fill, + _width: width = s.width, + } = s._paths; + + width = roundDec(width * pxRatio, 3); + + let boundsClip = null; + let offset = (width % 2) / 2; + + if (_points && fillStyle == null) + fillStyle = width > 0 ? "#fff" : strokeStyle; + + let _pxAlign = s.pxAlign == 1 && offset > 0; + + _pxAlign && ctx.translate(offset, offset); + + if (!_points) { + let lft = plotLft - width / 2, + top = plotTop - width / 2, + wid = plotWid + width, + hgt = plotHgt + width; + + 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 + flags != 0 && 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; + + if (isArr(bandClip)) + bandClip = b.dir == 1 ? bandClip[0] : bandClip[1]; + + 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 border = axis.border; + 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] * axis.lineGap; + + 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, + ); + } + + if (border.show) { + drawOrthoLines( + [basePos], + [1], + ori == 0 ? 1 : 0, + ori == 0 ? 1 : 2, + ori == 1 ? plotTop : plotLft, + ori == 1 ? plotHgt : plotWid, + roundDec(border.width * pxRatio, 3), + border.stroke(self, i), + border.dash, + border.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; + let deferHooks = false; + let hooksQueue = []; + + function flushHooks() { + deferHooks = false; + + for (let i = 0; i < hooksQueue.length; i++) + fire(...hooksQueue[i]); + + hooksQueue.length = 0; + } + + function commit() { + if (!queuedCommit) { + microTask(_commit); + queuedCommit = true; + } + } + + // manual batching (aka immediate mode), skips microtask queue + function batch(fn, _deferHooks = false) { + queuedCommit = true; + deferHooks = _deferHooks; + + fn(self); + _commit(); + + if (_deferHooks && hooksQueue.length > 0) + queueMicrotask(flushHooks); + } + + self.batch = batch; + + function _commit() { + // log("_commit()", arguments); + + if (shouldSetScales) { + setScales(); + shouldSetScales = false; + } + + if (shouldConvergeSize) { + convergeSize(); + shouldConvergeSize = false; + } + + if (shouldSetSize) { + setStylePx(under, LEFT, plotLftCss); + setStylePx(under, TOP, plotTopCss); + setStylePx(under, WIDTH, plotWidCss); + setStylePx(under, HEIGHT, plotHgtCss); + + setStylePx(over, LEFT, plotLftCss); + setStylePx(over, TOP, plotTopCss); + setStylePx(over, WIDTH, plotWidCss); + setStylePx(over, HEIGHT, plotHgtCss); + + setStylePx(wrap, WIDTH, fullWidCss); + setStylePx(wrap, HEIGHT, fullHgtCss); + + // NOTE: mutating this during print preview in Chrome forces transparent + // canvas pixels to white, even when followed up with clearRect() below + can.width = round(fullWidCss * pxRatio); + can.height = round(fullHgtCss * pxRatio); + + axes.forEach(({ _el, _show, _size, _pos, side }) => { + if (_el != null) { + if (_show) { + let posOffset = (side === 3 || side === 0 ? _size : 0); + let isVt = side % 2 == 1; + + setStylePx(_el, isVt ? "left" : "top", _pos - posOffset); + setStylePx(_el, isVt ? "width" : "height", _size); + setStylePx(_el, isVt ? "top" : "left", isVt ? plotTopCss : plotLftCss); + setStylePx(_el, isVt ? "height" : "width", isVt ? plotHgtCss : plotWidCss); + + remClass(_el, OFF); + } + else + addClass(_el, OFF); + } + }); + + // invalidate ctx style cache + ctxStroke = ctxFill = ctxWidth = ctxJoin = ctxCap = ctxFont = ctxAlign = ctxBaseline = ctxDash = null; + ctxAlpha = 1; + + syncRect(true); + + if ( + plotLftCss != _plotLftCss || + plotTopCss != _plotTopCss || + plotWidCss != _plotWidCss || + plotHgtCss != _plotHgtCss + ) { + resetYSeries(false); + + let pctWid = plotWidCss / _plotWidCss; + let pctHgt = plotHgtCss / _plotHgtCss; + + if (cursor.show && !shouldSetCursor && cursor.left >= 0) { + cursor.left *= pctWid; + cursor.top *= pctHgt; + + vCursor && elTrans(vCursor, round(cursor.left), 0, plotWidCss, plotHgtCss); + hCursor && elTrans(hCursor, 0, round(cursor.top), plotWidCss, plotHgtCss); + + for (let i = 1; i < cursorPts.length; i++) { + cursorPtsLft[i] *= pctWid; + cursorPtsTop[i] *= pctHgt; + elTrans(cursorPts[i], incrRoundUp(cursorPtsLft[i], 1), incrRoundUp(cursorPtsTop[i], 1), plotWidCss, plotHgtCss); + } + } + + if (select.show && !shouldSetSelect && select.left >= 0 && select.width > 0) { + select.left *= pctWid; + select.width *= pctWid; + select.top *= pctHgt; + select.height *= pctHgt; + + for (let prop in _hideProps) + setStylePx(selectDiv, prop, select[prop]); + } + + _plotLftCss = plotLftCss; + _plotTopCss = plotTopCss; + _plotWidCss = plotWidCss; + _plotHgtCss = plotHgtCss; + } + + 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 (select.show && shouldSetSelect) { + setSelect(select); + shouldSetSelect = false; + } + + if (cursor.show && shouldSetCursor) { + updateCursor(null, true, false); + shouldSetCursor = false; + } + + if (legend.show && legend.live && shouldSetLegend) { + setLegend(); + shouldSetLegend = false; // redundant currently + } + + 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) { + select[prop] = opts[prop]; + + if (prop in _hideProps) + setStylePx(selectDiv, 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); + + if (opts.focus != null) + setFocus(i); + + if (opts.show != null) { + series.forEach((s, si) => { + if (si > 0 && (i == si || i == null)) { + s.show = opts.show; + toggleDOM(si, opts.show); + + if (mode == 2) { + _setScale(s.facets[0].scale, null, null); + _setScale(s.facets[1].scale, null, null); + } + else + _setScale(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); + opts.dir = ifNull(opts.dir, -1); + 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}; + + function setFocus(i) { + if (i != focusedSeries) { + // log("setFocus()", arguments); + + let allFocused = i == null; + + let _setAlpha = focus.alpha != 1; + + series.forEach((s, i2) => { + if (mode == 1 || i2 > 0) { + 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) { + onMouse(mouseleave, legendTable, e => { + if (cursor._lock) + return; + + setCursorEvent(e); + + if (focusedSeries != null) + setSeries(null, FOCUS_TRUE, true, syncOpts.setSeries); + }); + } + + 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, + ) + ); + + 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) { + if (opts.idxs) { + opts.idxs.forEach((didx, sidx) => { + activeIdxs[sidx] = didx; + }); + } + else if (!isUndef(opts.idx)) + activeIdxs.fill(opts.idx); + + legend.idx = activeIdxs[0]; + } + + for (let sidx = 0; sidx < series.length; sidx++) { + if (sidx > 0 || mode == 1 && !multiValLegend) + setLegendValues(sidx, activeIdxs[sidx]); + } + + if (showLegend && legend.live) + syncLegend(); + + shouldSetLegend = false; + + _fire !== false && fire("setLegend"); + } + + self.setLegend = setLegend; + + function setLegendValues(sidx, idx) { + let s = series[sidx]; + let src = sidx == 0 && xScaleDistr == 2 ? data0 : data[sidx]; + let val; + + if (multiValLegend) + val = s.values(self, sidx, idx) ?? NULL_LEGEND_VALUES; + else { + val = s.value(self, idx == null ? null : src[idx], sidx, idx); + val = val == null ? NULL_LEGEND_VALUES : {_: val}; + } + + 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); + + cursor.left = mouseLeft1; + cursor.top = 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 = cursor.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(idx); + shouldSetLegend = true; + } + } + 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 = cursor.idx = closestIdx(valAtPosX, data[0], i0, i1); + xPos = valToPosX(data[0][idx], scaleX, xDim, 0); + } + + for (let i = mode == 2 ? 1 : 0; i < series.length; i++) { + let s = series[i]; + + let idx1 = activeIdxs[i]; + let yVal1 = idx1 == null ? null : (mode == 1 ? data[i][idx1] : data[i][1][idx1]); + + let idx2 = cursor.dataIdx(self, i, idx, valAtPosX); + let yVal2 = idx2 == null ? null : (mode == 1 ? data[i][idx2] : data[i][1][idx2]); + + shouldSetLegend = shouldSetLegend || yVal2 != yVal1 || idx2 != idx1; + + activeIdxs[i] = idx2; + + let xPos2 = idx2 == idx ? xPos : valToPosX(mode == 1 ? data[0][idx2] : data[i][0][idx2], scaleX, xDim, 0); + + if (i > 0 && s.show) { + // this doesnt really work for state timeline, heatmap, status history (where the value maps to color, not y coords) + let yPos = yVal2 == null ? -10 : valToPosY(yVal2, mode == 1 ? scales[s.scale] : scales[s.facets[1].scale], yDim, 0); + + if (cursorFocus && yVal2 != null) { + let mouseYPos = scaleX.ori == 1 ? mouseLeft1 : mouseTop1; + let dist = abs(focus.dist(self, i, idx2, yPos, mouseYPos)); + + if (dist < closestDist) { + let bias = focus.bias; + + if (bias != 0) { + let mouseYVal = posToVal(mouseYPos, s.scale); + + let seriesYValSign = yVal2 >= 0 ? 1 : -1; + let mouseYValSign = mouseYVal >= 0 ? 1 : -1; + + // with a focus bias, we will never cross zero when prox testing + // it's either closest towards zero, or closest away from zero + if (mouseYValSign == seriesYValSign && ( + mouseYValSign == 1 ? + (bias == 1 ? yVal2 >= mouseYVal : yVal2 <= mouseYVal) : // >= 0 + (bias == 1 ? yVal2 <= mouseYVal : yVal2 >= mouseYVal) // < 0 + )) { + closestDist = dist; + closestSeries = i; + } + } + else { + 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); + + cursorPtsLft[i] = ptLft; + cursorPtsTop[i] = ptTop; + + elTrans(cursorPts[i], incrRoundUp(ptLft, 1), incrRoundUp(ptTop, 1), plotWidCss, plotHgtCss); + } + } + } + } + + // 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; + + if (dragX || dragY) { + 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 && dragX) { + if (sori == 0) { + sOff = left; + sDim = width; + } + else { + sOff = top; + sDim = height; + } + + 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 && dragY) { + if (sori == 1) { + sOff = left; + sDim = width; + } + else { + sOff = top; + sDim = height; + } + + 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); + } + else + hideSelect(); + } + 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); + } + } + } + + if (shouldSetLegend) { + legend.idx = idx; + setLegend(); + } + + _fire !== false && fire("setCursor"); + } + + let rect = null; + + Object.defineProperty(self, 'rect', { + get() { + if (rect == null) + syncRect(false); + + return rect; + }, + }); + + function syncRect(defer = false) { + if (defer) + rect = null; + else { + rect = over.getBoundingClientRect(); + fire("syncRect", rect); + } + } + + function mouseMove(e, src, _l, _t, _w, _h, _i) { + if (cursor._lock) + return; + + // Chrome on Windows has a bug which triggers a stray mousemove event after an initial mousedown event + // when clicking into a plot as part of re-focusing the browser window. + // we gotta ignore it to avoid triggering a phantom drag / setSelect + // However, on touch-only devices Chrome-based browsers trigger a 0-distance mousemove before mousedown + // so we don't ignore it when mousedown has set the dragging flag + if (dragging && e != null && e.movementX == 0 && e.movementY == 0) + 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); + + setCursorEvent(e); + + 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.axes[0].side % 2 == 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; + } + } + + const _hideProps = { + width: 0, + height: 0, + left: 0, + top: 0, + }; + + function hideSelect() { + setSelect(_hideProps, false); + } + + let downSelectLeft; + let downSelectTop; + let downSelectWidth; + let downSelectHeight; + + 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, false); + pubSync(mousedown, self, mouseLeft0, mouseTop0, plotWidCss, plotHgtCss, null); + } + + let { left, top, width, height } = select; + + downSelectLeft = left; + downSelectTop = top; + downSelectWidth = width; + downSelectHeight = height; + + hideSelect(); + } + + 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; + let chgSelect = ( + downSelectLeft != left || + downSelectTop != top || + downSelectWidth != width || + downSelectHeight != height + ); + + hasSelect && chgSelect && setSelect(select); + + if (drag.setScale && hasSelect && chgSelect) { + // 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) + return; + + setCursorEvent(e); + + 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) { + if (cursor._lock) + return; + + setCursorEvent(e); + + 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) => { + let seriesIdxMatcher = syncOpts.match[2]; + idx = seriesIdxMatcher(self, src, idx); + idx != -1 && setSeries(idx, opts, true, false); + }; + + if (cursor.show) { + onMouse(mousedown, over, mouseDown); + onMouse(mousemove, over, mouseMove); + onMouse(mouseenter, over, e => { + setCursorEvent(e); + syncRect(false); + }); + 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 (deferHooks) + hooksQueue.push([evName, a1, a2]); + else { + 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 seriesIdxMatcher = (self, src, srcSeriesIdx) => srcSeriesIdx; + + const syncOpts = assign({ + key: null, + setSeries: false, + filters: { + pub: retTrue, + sub: retTrue, + }, + scales: [xScaleKey, series[1] ? series[1].scale : null], + match: [retEq, retEq, seriesIdxMatcher], + values: [null, null], + }, cursor.sync); + + if (syncOpts.match.length == 2) + syncOpts.match.push(seriesIdxMatcher); + + 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(); + legendTable?.remove(); // in case mounted outside of root + 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(); + + shouldSetSelect = select.show && (select.width > 0 || select.height > 0); + shouldSetCursor = shouldSetLegend = true; + + _setSize(opts.width, opts.height); + } + + 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.pxRatio = pxRatio; + +{ + 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/docs/dist/uPlot.d.ts b/docs/dist/uPlot.d.ts new file mode 100644 index 0000000..9f79579 --- /dev/null +++ b/docs/dist/uPlot.d.ts @@ -0,0 +1,1179 @@ +declare class uPlot { + /** when passing a function for @targ, call init() after attaching self.root to the DOM */ + constructor( + opts: uPlot.Options, + data?: uPlot.AlignedData, + targ?: HTMLElement | ((self: uPlot, init: Function) => void) + ); + + /** chart container */ + readonly root: HTMLElement; + + /** status */ + readonly status: 0 | 1; + + /** width of the plotting area + axes in CSS pixels */ + readonly width: number; + + /** height of the plotting area + axes in CSS pixels (excludes title & legend height) */ + readonly height: number; + + /** context of canvas used for plotting area + axes */ + readonly ctx: CanvasRenderingContext2D; + + /** coords of plotting area in canvas pixels (relative to full canvas w/axes) */ + readonly bbox: uPlot.BBox; + + /** cached global DOMRect of plotting area in CSS pixels */ + get rect(): DOMRect; + + /** coords of selected region in CSS pixels (relative to plotting area) */ + readonly select: uPlot.BBox; + + /** cursor state & opts*/ + readonly cursor: uPlot.Cursor; + + readonly legend: uPlot.Legend; + +// /** focus opts */ +// readonly focus: uPlot.Focus; + + /** series state & opts */ + readonly series: uPlot.Series[]; + + /** scales state & opts */ + readonly scales: { + [key: string]: uPlot.Scale; + }; + + /** axes state & opts */ + readonly axes: uPlot.Axis[]; + + /** hooks, including any added by plugins */ + readonly hooks: uPlot.Hooks.Arrays; + + /** current data */ + readonly data: uPlot.AlignedData; + + /** .u-over dom element */ + readonly over: HTMLDivElement; + + /** .u-under dom element */ + readonly under: HTMLDivElement; + + /** clears and redraws the canvas. if rebuildPaths = false, uses cached series' Path2D objects */ + redraw(rebuildPaths?: boolean, recalcAxes?: boolean): void; + + /** manual batching of multiple ops (aka immediate mode that skips implicit microtask queue), ops, e.g. setScale('x', ...) && setScale('y', ...) */ + batch(txn: Function, deferHooks?: boolean): void; + + /** destroys DOM, removes resize & scroll listeners, etc. */ + destroy(): void; + + /** sets the chart data & redraws. (default resetScales = true) */ + setData(data: uPlot.AlignedData, resetScales?: boolean): void; + + /** sets the limits of a scale & redraws (used for zooming) */ + setScale(scaleKey: string, limits: { min: number; max: number }): void; + + /** sets the cursor position (relative to plotting area) */ + setCursor(opts: {left: number, top: number}, fireHook?: boolean): void; + + /** sets the legend to the values of the specified idx */ + setLegend(opts: {idx?: number, idxs?: (number | null)[]}, fireHook?: boolean): void; + + // TODO: include other series style opts which are dynamically pulled? + /** toggles series visibility or focus */ + setSeries(seriesIdx: number | null, opts: {show?: boolean, focus?: boolean}, fireHook?: boolean): void; + + /** adds a series */ + addSeries(opts: uPlot.Series, seriesIdx?: number): void; + + /** deletes a series */ + delSeries(seriesIdx: number): void; + + /** adds a band */ + addBand(opts: uPlot.Band, bandIdx?: number): void; + + /** modifies an existing band */ + setBand(bandIdx: number, opts: uPlot.Band): void; + + /** deletes a band. if null bandIdx, clears all bands */ + delBand(bandIdx?: number | null): void; + + /** sets visually selected region without triggering setScale (zoom). (default fireHook = true) */ + setSelect(opts: {left: number, top: number, width: number, height: number}, fireHook?: boolean): void; + + /** sets the width & height of the plotting area + axes (excludes title & legend height) */ + setSize(opts: { width: number; height: number }): void; + + /** converts a CSS pixel position (relative to plotting area) to the closest data index */ + posToIdx(left: number, canvasPixels?: boolean): number; + + /** converts a CSS pixel position (relative to plotting area) to a value along the given scale */ + posToVal(leftTop: number, scaleKey: string, canvasPixels?: boolean): number; + + /** converts a value along the given scale to a CSS (default) or canvas pixel position. (default canvasPixels = false) */ + valToPos(val: number, scaleKey: string, canvasPixels?: boolean): number; + + /** converts a value along x to the closest data index */ + valToIdx(val: number): number; + + /** updates getBoundingClientRect() cache for cursor positioning. use when plot's position changes (excluding window scroll & resize) */ + syncRect(defer?: boolean): void; + + /** uPlot's path-builder factories */ + static paths: uPlot.Series.PathBuilderFactories; + + /** a deep merge util fn */ + static assign(targ: object, ...srcs: object[]): object; + + /** re-ranges a given min/max by a multiple of the range's magnitude (used internally to expand/snap/pad numeric y scales) */ + static rangeNum(min: number, max: number, mult: number, extra: boolean): uPlot.Range.MinMax; + static rangeNum(min: number, max: number, cfg: uPlot.Range.Config): uPlot.Range.MinMax; + + /** re-ranges a given min/max outwards to nearest 10% of given min/max's magnitudes, unless fullMags = true */ + static rangeLog(min: number, max: number, base: uPlot.Scale.LogBase, fullMags: boolean): uPlot.Range.MinMax; + + /** re-ranges a given min/max outwards to nearest 10% of given min/max's magnitudes, unless fullMags = true */ + static rangeAsinh(min: number, max: number, base: uPlot.Scale.LogBase, fullMags: boolean): uPlot.Range.MinMax; + + /** default numeric formatter using browser's locale: new Intl.NumberFormat(navigator.language).format */ + static fmtNum(val: number): string; + + /** creates an efficient formatter for Date objects from a template string, e.g. {YYYY}-{MM}-{DD} */ + static fmtDate(tpl: string, names?: uPlot.DateNames): (date: Date) => string; + + /** converts a Date into new Date that's time-adjusted for the given IANA Time Zone Name */ + static tzDate(date: Date, tzName: string): Date; + + /** outerJoins multiple data tables on table[0] values */ + static join(tables: uPlot.AlignedData[], nullModes?: uPlot.JoinNullMode[][]): uPlot.AlignedData; + + static addGap: uPlot.Series.AddGap; + + static clipGaps: uPlot.Series.ClipPathBuilder; + + /** helper function for grabbing proper drawing orientation vars and fns for a plot instance (all dims in canvas pixels) */ + static orient(u: uPlot, seriesIdx: number, callback: uPlot.OrientCallback): any; + + /** returns a pub/sub instance shared by all plots using the provided key */ + static sync(key: string): uPlot.SyncPubSub; + + /** cached devicePixelRatio (faster than reading it from window.devicePixelRatio) */ + static pxRatio: number; +} + +export = uPlot; + +declare namespace uPlot { + type OrientCallback = ( + series: Series, + dataX: number[], + dataY: (number | null)[], + scaleX: Scale, + scaleY: Scale, + valToPosX: ValToPos, + valToPosY: ValToPos, + xOff: number, + yOff: number, + xDim: number, + yDim: number, + moveTo: MoveToH | MoveToV, + lineTo: LineToH | LineToV, + rect: RectH | RectV, + arc: ArcH | ArcV, + bezierCurveTo: BezierCurveToH | BezierCurveToV, + ) => any; + + type ValToPos = (val: number, scale: Scale, fullDim: number, offset: number) => number; + + type Drawable = Path2D | CanvasRenderingContext2D; + + type MoveToH = (p: Drawable, x: number, y: number) => void; + type MoveToV = (p: Drawable, y: number, x: number) => void; + type LineToH = (p: Drawable, x: number, y: number) => void; + type LineToV = (p: Drawable, y: number, x: number) => void; + type RectH = (p: Drawable, x: number, y: number, w: number, h: number) => void; + type RectV = (p: Drawable, y: number, x: number, h: number, w: number) => void; + type ArcH = (p: Drawable, x: number, y: number, r: number, startAngle: number, endAngle: number) => void; + type ArcV = (p: Drawable, y: number, x: number, r: number, startAngle: number, endAngle: number) => void; + type BezierCurveToH = (p: Drawable, bp1x: number, bp1y: number, bp2x: number, bp2y: number, p2x: number, p2y: number) => void; + type BezierCurveToV = (p: Drawable, bp1y: number, bp1x: number, bp2y: number, bp2x: number, p2y: number, p2x: number) => void; + + export const enum JoinNullMode { + /** use for series with spanGaps: true */ + Remove = 0, + /** retain explicit nulls gaps (default) */ + Retain = 1, + /** expand explicit null gaps to include adjacent alignment artifacts (undefined values) */ + Expand = 2, + } + + export const enum Orientation { + Horizontal = 0, + Vertical = 1, + } + + export type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array; + + export type AlignedData = TypedArray[] | [ + xValues: number[] | TypedArray, + ...yValues: ((number | null | undefined)[] | TypedArray)[], + ] + + export interface DateNames { + /** long month names */ + MMMM: string[]; + + /** short month names */ + MMM: string[]; + + /** long weekday names (0: Sunday) */ + WWWW: string[]; + + /** short weekday names (0: Sun) */ + WWW: string[]; + } + + export namespace Range { + export type MinMax = [min: number | null, max: number | null]; + + export type Function = (self: uPlot, initMin: number, initMax: number, scaleKey: string) => MinMax; + + export type SoftMode = 0 | 1 | 2 | 3; + + export interface Limit { + /** initial multiplier for dataMax-dataMin delta */ + pad?: number; // 0.1 + + /** soft limit */ + soft?: number; // 0 + + /** soft limit active if... 0: never, 1: data <= limit, 2: data + padding <= limit, 3: data <= limit <= data + padding */ + mode?: SoftMode; // 3 + + /** hard limit */ + hard?: number; + } + + export interface Config { + min: Range.Limit; + max: Range.Limit; + } + } + + export interface Scales { + [key: string]: Scale; + } + + type SidesWithAxes = [top: boolean, right: boolean, bottom: boolean, left: boolean]; + + export type PaddingSide = number | null | ((self: uPlot, side: Axis.Side, sidesWithAxes: SidesWithAxes, cycleNum: number) => number); + + export type Padding = [top: PaddingSide, right: PaddingSide, bottom: PaddingSide, left: PaddingSide]; + + export interface Legend { + show?: boolean; // true + /** show series values at current cursor.idx */ + live?: boolean; // true + /** switches primary interaction mode to toggle-one/toggle-all */ + isolate?: boolean; // false + /** series indicators */ + markers?: Legend.Markers; + + /** callback for moving the legend elsewhere. e.g. external DOM container */ + mount?: (self: uPlot, el: HTMLElement) => void; + + /** current index (readback-only, not for init) */ + idx?: number | null; + /** current indices (readback-only, not for init) */ + idxs?: (number | null)[]; + /** current values (readback-only, not for init) */ + values?: Legend.Values; + } + + export namespace Legend { + export type Width = number | ((self: uPlot, seriesIdx: number) => number); + + export type Stroke = CSSStyleDeclaration['borderColor'] | ((self: uPlot, seriesIdx: number) => CSSStyleDeclaration['borderColor']); + + export type Dash = CSSStyleDeclaration['borderStyle'] | ((self: uPlot, seriesIdx: number) => CSSStyleDeclaration['borderStyle']); + + export type Fill = CSSStyleDeclaration['background'] | ((self: uPlot, seriesIdx: number) => CSSStyleDeclaration['background']); + + export type Value = { + [key: string]: string | number; + }; + + export type Values = Value[]; + + export interface Markers { + show?: boolean; // true + /** series indicator line width */ + width?: Legend.Width; + /** series indicator stroke (CSS borderColor) */ + stroke?: Legend.Stroke; + /** series indicator fill */ + fill?: Legend.Fill; + /** series indicator stroke style (CSS borderStyle) */ + dash?: Legend.Dash; + } + } + + export type DateFormatterFactory = (tpl: string) => (date: Date) => string; + + export type LocalDateFromUnix = (ts: number) => Date; + + export const enum DrawOrderKey { + Axes = 'axes', + Series = 'series', + } + + export const enum Mode { + Aligned = 1, + Faceted = 2, + } + + export interface Options { + /** 1: aligned & ordered, single-x / y-per-series, 2: unordered & faceted, per-series/per-point x,y,size,label,color,shape,etc. */ + mode?: Mode, + + /** chart title */ + title?: string; + + /** id to set on chart div */ + id?: string; + + /** className to add to chart div */ + class?: string; + + /** width of plotting area + axes in CSS pixels */ + width: number; + + /** height of plotting area + axes in CSS pixels (excludes title & legend height) */ + height: number; + + /** data for chart, if none is provided as argument to constructor */ + data?: AlignedData; + + /** converts a unix timestamp to Date that's time-adjusted for the desired timezone */ + tzDate?: LocalDateFromUnix; + + /** creates an efficient formatter for Date objects from a template string, e.g. {YYYY}-{MM}-{DD} */ + fmtDate?: DateFormatterFactory; + + /** timestamp multiplier that yields 1 millisecond */ + ms?: 1e-3 | 1; // 1e-3 + + /** drawing order for axes/grid & series (default: ["axes", "series"]) */ + drawOrder?: DrawOrderKey[]; + + /** whether vt & hz lines of series/grid/ticks should be crisp/sharp or sub-px antialiased */ + pxAlign?: boolean | number; // true + + series: Series[]; + + bands?: Band[]; + + scales?: Scales; + + axes?: Axis[]; + + /** padding per side, in CSS pixels (can prevent cross-axis labels at the plotting area limits from being chopped off) */ + padding?: Padding; + + select?: Select; + + legend?: Legend; + + cursor?: Cursor; + + focus?: Focus; + + hooks?: Hooks.Arrays; + + plugins?: Plugin[]; + } + + export interface Focus { + /** alpha-transparancy of de-focused series */ + alpha: number; + } + + export interface BBox { + show?: boolean; + left: number; + top: number; + width: number; + height: number; + } + + export interface Select extends BBox { + /** div into which .u-select will be placed: .u-over or .u-under */ + over?: boolean; // true + } + + export interface SyncPubSub { + key: string; + sub: (client: uPlot) => void; + unsub: (client: uPlot) => void; + pub: (type: string, client: uPlot, x: number, y: number, w: number, h: number, i: number) => void; + plots: uPlot[]; + } + + export namespace Cursor { + export type LeftTop = [left: number, top: number]; + + export type MouseListener = (e: MouseEvent) => null; + + export type MouseListenerFactory = (self: uPlot, targ: HTMLElement, handler: MouseListener) => MouseListener | null; + + export type DataIdxRefiner = (self: uPlot, seriesIdx: number, closestIdx: number, xValue: number) => number | null; + + export type MousePosRefiner = (self: uPlot, mouseLeft: number, mouseTop: number) => LeftTop; + + export interface Bind { + mousedown?: MouseListenerFactory; + mouseup?: MouseListenerFactory; + click?: MouseListenerFactory; + dblclick?: MouseListenerFactory; + + mousemove?: MouseListenerFactory; + mouseleave?: MouseListenerFactory; + mouseenter?: MouseListenerFactory; + } + + export namespace Points { + export type Show = boolean | ((self: uPlot, seriesIdx: number) => HTMLElement); + export type Size = number | ((self: uPlot, seriesIdx: number) => number); + export type BBox = (self: uPlot, seriesIdx: number) => uPlot.BBox; + export type Width = number | ((self: uPlot, seriesIdx: number, size: number) => number); + export type Stroke = CanvasRenderingContext2D['strokeStyle'] | ((self: uPlot, seriesIdx: number) => CanvasRenderingContext2D['strokeStyle']); + export type Fill = CanvasRenderingContext2D['fillStyle'] | ((self: uPlot, seriesIdx: number) => CanvasRenderingContext2D['fillStyle']); + } + + export interface Points { + show?: Points.Show; + /** hover point diameter in CSS pixels */ + size?: Points.Size; + /** hover point bbox in CSS pixels (will be used instead of size) */ + bbox?: Points.BBox; + /** hover point outline width in CSS pixels */ + width?: Points.Width; + /** hover point outline color, pattern or gradient */ + stroke?: Points.Stroke; + /** hover point fill color, pattern or gradient */ + fill?: Points.Fill; + } + + export interface Drag { + setScale?: boolean; // true + /** toggles dragging along x */ + x?: boolean; // true + /** toggles dragging along y */ + y?: boolean; // false + /** min drag distance threshold */ + dist?: number; // 0 + /** when x & y are true, sets an upper drag limit in CSS px for adaptive/unidirectional behavior */ + uni?: number; // null + /** post-drag "click" event proxy, default is to prevent these click events */ + click?: (self: uPlot, e: MouseEvent) => void; + } + + export namespace Sync { + export type Scales = [xScaleKey: string | null, yScaleKey: string | null]; + + export type Filter = (type: string, client: uPlot, x: number, y: number, w: number, h: number, i: number) => boolean; + + export interface Filters { + /** filters emitted events */ + pub?: Filter; + /** filters received events */ + sub?: Filter; + } + + export type ScaleKeyMatcher = (subScaleKey: string | null, pubScaleKey: string | null) => boolean; + + export type SeriesIdxMatcher = (sub: uPlot, pub: uPlot, pubSeriesIdx: number) => number | null; + + export type Match = [matchX: ScaleKeyMatcher, matchY: ScaleKeyMatcher, matchSeriesIdx?: SeriesIdxMatcher]; + + export type Values = [xScaleValue: number, yScaleValue: number]; + } + + export interface Sync { + /** sync key must match between all charts in a synced group */ + key: string; + /** determines if series toggling and focus via cursor is synced across charts */ + setSeries?: boolean; // true + /** sets the x and y scales to sync by values. null will sync by relative (%) position */ + scales?: Sync.Scales; // [xScaleKey, null] + /** fns that match x and y scale keys and seriesIdxs between publisher and subscriber */ + match?: Sync.Match; + /** event filters */ + filters?: Sync.Filters; + /** sync scales' values at the cursor position (exposed for read-back by subscribers) */ + values?: Sync.Values, + } + + // options that compile the cursor.dataIdx callback (the index scanner) + export interface Hover { + /** minimum cursor proximity to datapoint in CSS pixels for point hover */ + prox?: number | null | ((self: uPlot, seriesIdx: number, closestIdx: number, xValue: number) => number | null); // null/Infinity + /** when non-zero, will only proximity-test indices forward or backward */ + bias?: HoverBias; // 0 + /** what values to treat as non-hoverable and trigger scanning to another index */ + skip?: any[]; // [undefined] + } + + export interface Focus { + /** minimum cursor proximity to datapoint in CSS pixels for focus activation, disabled: < 0, enabled: <= 1e6 */ + prox: number; + /** when non-zero, will only focus next series towards or away from zero */ + bias?: FocusBias; // 0 + /** measures cursor y distance to a series in CSS pixels (for triggering setSeries hook with closest) */ + dist?: (self: uPlot, seriesIdx: number, dataIdx: number, valPos: number, curPos: number) => number; + } + + export const enum FocusBias { + None = 0, + AwayFromZero = 1, + TowardsZero = -1, + } + + export const enum HoverBias { + None = 0, + Forward = 1, + Backward = -1, + } + } + + export interface Cursor { + /** cursor on/off */ + show?: boolean; + + /** vertical crosshair on/off */ + x?: boolean; + + /** horizontal crosshair on/off */ + y?: boolean; + + /** cursor position left offset in CSS pixels (relative to plotting area) */ + left?: number; + + /** cursor position top offset in CSS pixels (relative to plotting area) */ + top?: number; + + /** closest data index to cursor (closestIdx) */ + idx?: number | null; + + /** returns data idx used for hover points & legend display (defaults to closestIdx) */ + dataIdx?: Cursor.DataIdxRefiner; + + /** a series-matched array of indices returned by dataIdx() */ + idxs?: (number | null)[]; + + /** fires on debounced mousemove events; returns refined [left, top] tuple to snap cursor position */ + move?: Cursor.MousePosRefiner; + + /** series hover points */ + points?: Cursor.Points; + + /** event listener proxies (can be overridden to tweak interaction behavior) */ + bind?: Cursor.Bind; + + /** determines vt/hz cursor dragging to set selection & setScale (zoom) */ + drag?: Cursor.Drag; + + /** sync cursor between multiple charts */ + sync?: Cursor.Sync; + + /** focus series closest to cursor (y) */ + focus?: Cursor.Focus; + + /** hover data points closest to cursor (x) */ + hover?: Cursor.Hover; + + /** lock cursor on mouse click in plotting area */ + lock?: boolean; // false + + /** the most recent mouse event */ + event?: MouseEvent; + } + + export namespace Scale { + export type Auto = boolean | ((self: uPlot, resetScales: boolean) => boolean); + + export type Range = Range.MinMax | Range.Function | Range.Config; + + export const enum Distr { + Linear = 1, + Ordinal = 2, + Logarithmic = 3, + ArcSinh = 4, + } + + export type LogBase = 10 | 2; + + export type Clamp = number | ((self: uPlot, val: number, scaleMin: number, scaleMax: number, scaleKey: string) => number); + } + + export interface Scale { + /** is this scale temporal, with series' data in UNIX timestamps? */ + time?: boolean; + + /** determines whether all series' data on this scale will be scanned to find the full min/max range */ + auto?: Scale.Auto; + + /** can define a static scale range or re-range an initially-determined range from series data */ + range?: Scale.Range; + + /** scale key from which this scale is derived */ + from?: string; + + /** scale distribution. 1: linear, 2: ordinal, 3: logarithmic, 4: arcsinh */ + distr?: Scale.Distr; // 1 + + /** logarithmic base */ + log?: Scale.LogBase; // 10; + + /** clamps log scale values <= 0 (default = scaleMin / 10) */ + clamp?: Scale.Clamp; + + /** arcsinh linear threshold */ + asinh?: number; // 1 + + /** current min scale value */ + min?: number; + + /** current max scale value */ + max?: number; + + /** scale direction */ + dir?: 1 | -1; + + /** scale orientation - 0: hz, 1: vt */ + ori?: 0 | 1; + + /** own key (for read-back) */ + key?: string; + } + + export namespace Series { + export interface Paths { + /** path to stroke */ + stroke?: Path2D | Map<CanvasRenderingContext2D['strokeStyle'], Path2D> | null; + + /** path to fill */ + fill?: Path2D | Map<CanvasRenderingContext2D['fillStyle'], Path2D> | null; + + /** path for clipping fill & stroke (used for gaps) */ + clip?: Path2D | null; + + /** yMin-ward (dir: -1) and/or yMax-ward (dir: 1) clips built using the stroke path (inverted dirs from band.dir fills) */ + band? : Path2D | null | [yMinClip: Path2D, yMaxClip: Path2D]; + + /** tuples of canvas pixel coordinates that were used to construct the gaps clip */ + gaps?: [from: number, to: number][]; + + /** line width in CSS pixels, if differs from series.width (for dynamic rendering optimization) */ + width?: number; + + /** fill style, if differs from series.fill (for dynamic rendering optimization) */ + _fill?: CanvasRenderingContext2D['fillStyle']; + + /** stroke style, if differs from series.stroke (for dynamic rendering optimization) */ + _stroke?: CanvasRenderingContext2D['strokeStyle']; + + /** bitmap of whether the band clip should be applied to stroke, fill, or both */ + flags?: number; + } + + export interface SteppedPathBuilderOpts { + align?: -1 | 1; // 1 + + alignGaps?: -1 | 0 | 1; // 0 + + // whether to draw ascenders/descenders at null/gap boundaries + ascDesc?: boolean; // false + + // extend hz lines to plot edges when x scale is beyond x data limits? + extend?: boolean; // false + } + + export const enum BarsPathBuilderFacetUnit { + ScaleValue = 1, + PixelPercent = 2, + Color = 3, + } + + export const enum BarsPathBuilderFacetKind { + Unary = 1, + Discrete = 2, + Continuous = 3, + } + + export type BarsPathBuilderFacetValue = string | number | boolean | null | undefined; + + export interface BarsPathBuilderFacet { + /** unit of measure for output of values() */ + unit: BarsPathBuilderFacetUnit; + /** are the values unary, discrete, or continuous */ + kind?: BarsPathBuilderFacetKind; + /** values to use for this facet */ + values: (self: uPlot, seriesIdx: number, idx0: number, idx1: number) => BarsPathBuilderFacetValue[]; + } + + /** custom per-datapoint styling and positioning */ + export interface BarsPathBuilderDisplay { + x0?: BarsPathBuilderFacet; + // x1?: BarsPathBuilderFacet; + y0?: BarsPathBuilderFacet; + y1?: BarsPathBuilderFacet; + size?: BarsPathBuilderFacet; + fill?: BarsPathBuilderFacet; + stroke?: BarsPathBuilderFacet; + } + + /** radii for bar end (at bar's value) and bar start (baseline, zero) */ + export type BarsPathBuilderRadii = [endRadius: number, baseRadius: number]; + + export type BarsPathBuilderRadius = number | BarsPathBuilderRadii | ((self: uPlot, seriesIdx: number) => BarsPathBuilderRadii); + + export interface BarsPathBuilderOpts { + align?: -1 | 0 | 1; // 0 + + size?: [factor?: number, max?: number, min?: number]; + + // corner radius factor of bar size (0 - 0.5) + radius?: BarsPathBuilderRadius; // 0 + + /** fixed-size gap between bars in CSS pixels (reduces bar width) */ + gap?: number; + + /** should return a custom [cached] layout for bars in % of plotting area (0..1) */ + disp?: BarsPathBuilderDisplay; + + /** called with bbox geometry of each drawn bar in canvas pixels. useful for spatial index, etc. */ + each?: (self: uPlot, seriesIdx: number, idx: number, left: number, top: number, width: number, height: number) => void; + } + + export interface LinearPathBuilderOpts { + alignGaps?: -1 | 0 | 1; // 0 + } + + export interface SplinePathBuilderOpts { + alignGaps?: -1 | 0 | 1; // 0 + } + + export type PointsPathBuilderFactory = () => Points.PathBuilder; + export type LinearPathBuilderFactory = (opts?: LinearPathBuilderOpts) => Series.PathBuilder; + export type SplinePathBuilderFactory = (opts?: SplinePathBuilderOpts) => Series.PathBuilder; + export type SteppedPathBuilderFactory = (opts?: SteppedPathBuilderOpts) => Series.PathBuilder; + export type BarsPathBuilderFactory = (opts?: BarsPathBuilderOpts) => Series.PathBuilder; + + export interface PathBuilderFactories { + linear?: LinearPathBuilderFactory; + spline?: SplinePathBuilderFactory; + stepped?: SteppedPathBuilderFactory; + bars?: BarsPathBuilderFactory; + points?: PointsPathBuilderFactory; + } + + export type Stroke = CanvasRenderingContext2D['strokeStyle'] | ((self: uPlot, seriesIdx: number) => CanvasRenderingContext2D['strokeStyle']); + + export type Fill = CanvasRenderingContext2D['fillStyle'] | ((self: uPlot, seriesIdx: number) => CanvasRenderingContext2D['fillStyle']); + + export type Cap = CanvasRenderingContext2D['lineCap']; + + export namespace Points { + export interface Paths { + /** path to stroke */ + stroke?: Path2D | null; + + /** path to fill */ + fill?: Path2D | null; + + /** path for clipping fill & stroke */ + clip?: Path2D | null; + + /** bitmap of whether the clip should be applied to stroke, fill, or both */ + flags?: number; + } + + export type Show = boolean | ((self: uPlot, seriesIdx: number, idx0: number, idx1: number, gaps?: null | number[][]) => boolean | undefined); + + export type Filter = number[] | null | ((self: uPlot, seriesIdx: number, show: boolean, gaps?: null | number[][]) => number[] | null); + + export type PathBuilder = (self: uPlot, seriesIdx: number, idx0: number, idx1: number, filtIdxs?: number[] | null) => Paths | null; + } + + export interface Points { + /** if boolean or returns boolean, round points are drawn with defined options, else fn should draw own custom points via self.ctx */ + show?: Points.Show; + + paths?: Points.PathBuilder; + + /** may return an array of points indices to draw */ + filter?: Points.Filter; + + /** diameter of point in CSS pixels */ + size?: number; + + /** minimum avg space between point centers before they're shown (default: size * 2) */ + space?: number; + + /** line width of circle outline in CSS pixels */ + width?: number; + + /** line color of circle outline (defaults to series.stroke) */ + stroke?: Stroke; + + /** line dash segment array */ + dash?: number[]; + + /** line cap */ + cap?: Series.Cap; + + /** fill color of circle (defaults to #fff) */ + fill?: Fill; + } + + export interface Facet { + scale: string; + + auto?: boolean; + + sorted?: Sorted; + } + + export type Gap = [from: number, to: number]; + + export type Gaps = Gap[]; + + export type GapsRefiner = Gaps | ((self: uPlot, seriesIdx: number, idx0: number, idx1: number, nullGaps: Gaps) => Gaps); + + export type AddGap = (gaps: Gaps, from: number, to: number) => void; + + export type ClipPathBuilder = (gaps: Gaps, ori: Orientation, left: number, top: number, width: number, height: number) => Path2D | null; + + export type PathBuilder = (self: uPlot, seriesIdx: number, idx0: number, idx1: number) => Paths | null; + + export type MinMaxIdxs = [minIdx: number, maxIdx: number]; + + export type Value = string | ((self: uPlot, rawValue: number, seriesIdx: number, idx: number | null) => string | number); + + export type Values = (self: uPlot, seriesIdx: number, idx: number | null) => object; + + export type FillTo = number | ((self: uPlot, seriesIdx: number, dataMin: number, dataMax: number) => number); + + export const enum Sorted { + Unsorted = 0, + Ascending = 1, + Descending = -1, + } + } + + export interface Series { + /** series on/off. when off, it will not affect its scale */ + show?: boolean; + + /** className to add to legend parts and cursor hover points */ + class?: string; + + /** scale key */ + scale?: string; + + /** whether this series' data is scanned during auto-ranging of its scale */ + auto?: boolean; // true + + /** if & how the data is pre-sorted (scale.auto optimization) */ + sorted?: Series.Sorted; + + /** when true, null data values will not cause line breaks */ + spanGaps?: boolean; + + /** may mutate and/or augment gaps array found from null values */ + gaps?: Series.GapsRefiner; + + /** whether path and point drawing should offset canvas to try drawing crisp lines */ + pxAlign?: number | boolean; // 1 + + /** legend label */ + label?: string; + + /** inline-legend value formatter. can be an fmtDate formatting string when scale.time: true */ + value?: Series.Value; + + /** table-legend multi-values formatter */ + values?: Series.Values; + + paths?: Series.PathBuilder; + + /** rendered datapoints */ + points?: Series.Points; + + /** facets */ + facets?: Series.Facet[]; + + /** line width in CSS pixels */ + width?: number; + + /** line & legend color */ + stroke?: Series.Stroke; + + /** area fill & legend color */ + fill?: Series.Fill; + + /** area fill baseline (default: 0) */ + fillTo?: Series.FillTo; + + /** line dash segment array */ + dash?: number[]; + + /** line cap */ + cap?: Series.Cap; + + /** alpha-transparancy */ + alpha?: number; + + /** current min and max data indices rendered */ + idxs?: Series.MinMaxIdxs; + + /** current min rendered value */ + min?: number; + + /** current max rendered value */ + max?: number; + } + + export namespace Band { + export type Fill = CanvasRenderingContext2D['fillStyle'] | ((self: uPlot, bandIdx: number, highSeriesFill: CanvasRenderingContext2D['fillStyle']) => CanvasRenderingContext2D['fillStyle']); + + export type Bounds = [fromSeriesIdx: number, toSeriesIdx: number]; + } + + export interface Band { + /** band on/off */ + // show?: boolean; + + /** series indices of upper and lower band edges */ + series: Band.Bounds; + + /** area fill style */ + fill?: Band.Fill; + + /** whether to fill towards yMin (-1) or yMax (+1) between "from" & "to" series */ + dir?: 1 | -1; // -1 + } + + export namespace Axis { + /** must return an array of same length as splits, e.g. via splits.map() */ + export type Filter = (self: uPlot, splits: number[], axisIdx: number, foundSpace: number, foundIncr: number) => (number | null)[]; + + export type Size = number | ((self: uPlot, values: string[], axisIdx: number, cycleNum: number) => number); + + export type Space = number | ((self: uPlot, axisIdx: number, scaleMin: number, scaleMax: number, plotDim: number) => number); + + export type Incrs = number[] | ((self: uPlot, axisIdx: number, scaleMin: number, scaleMax: number, fullDim: number, minSpace: number) => number[]); + + export type Splits = number[] | ((self: uPlot, axisIdx: number, scaleMin: number, scaleMax: number, foundIncr: number, foundSpace: number) => number[]); + + export type StaticValues = (string | number | null)[]; + + export type DynamicValues = (self: uPlot, splits: number[], axisIdx: number, foundSpace: number, foundIncr: number) => StaticValues; + + export type TimeValuesConfig = (string | number | null)[][]; + + export type TimeValuesTpl = string; + + export type Values = StaticValues | DynamicValues | TimeValuesTpl | TimeValuesConfig; + + export type Stroke = CanvasRenderingContext2D['strokeStyle'] | ((self: uPlot, axisIdx: number) => CanvasRenderingContext2D['strokeStyle']); + + export const enum Side { + Top = 0, + Right = 1, + Bottom = 2, + Left = 3, + } + + export const enum Align { + Left = 1, + Right = 2, + } + + export type Rotate = number | ((self: uPlot, values: (string | number)[], axisIdx: number, foundSpace: number) => number); + + interface OrthoLines { + /** on/off */ + show?: boolean; // true + + /** line color */ + stroke?: Stroke; + + /** line width in CSS pixels */ + width?: number; + + /** line dash segment array */ + dash?: number[]; + + /** line cap */ + cap?: Series.Cap; + } + + export interface Border extends OrthoLines {} + + interface FilterableOrthoLines extends OrthoLines { + /** can filter which splits render lines. e.g splits.map(v => v % 2 == 0 ? v : null) */ + filter?: Filter; + } + + export interface Grid extends FilterableOrthoLines {} + + export interface Ticks extends FilterableOrthoLines { + /** length of tick in CSS pixels */ + size?: number; + } + } + + export interface Axis { + /** axis on/off */ + show?: boolean; + + /** scale key */ + scale?: string; + + /** side of chart - 0: top, 1: rgt, 2: btm, 3: lft */ + side?: Axis.Side; + + /** height of x axis or width of y axis in CSS pixels alloted for values, gap & ticks, but excluding axis label */ + size?: Axis.Size; + + /** gap between axis values and axis baseline (or ticks, if enabled) in CSS pixels */ + gap?: number; + + /** font used for axis values */ + font?: CanvasRenderingContext2D['font']; + + /** font-size multiplier for multi-line axis values (similar to CSS line-height: 1.5em) */ + lineGap?: number; // 1.5 + + /** color of axis label & values */ + stroke?: Axis.Stroke; + + /** axis label text */ + label?: string; + + /** height of x axis label or width of y axis label in CSS pixels alloted for label text + labelGap */ + labelSize?: number; + + /** gap between label baseline and tick values in CSS pixels */ + labelGap?: number; + + /** font used for axis label */ + labelFont?: CanvasRenderingContext2D['font']; + + /** minimum grid & tick spacing in CSS pixels */ + space?: Axis.Space; + + /** available divisors for axis ticks, values, grid */ + incrs?: Axis.Incrs; + + /** determines how and where the axis must be split for placing ticks, values, grid */ + splits?: Axis.Splits; + + /** can filter which splits are passed to axis.values() for rendering. e.g splits.map(v => v % 2 == 0 ? v : null) */ + filter?: Axis.Filter; + + /** formats values for rendering */ + values?: Axis.Values; + + /** values rotation in degrees off horizontal (only bottom axes w/ side: 2) */ + rotate?: Axis.Rotate; + + /** text alignment of axis values - 1: left, 2: right */ + align?: Axis.Align; + + /** gridlines to draw from this axis' splits */ + grid?: Axis.Grid; + + /** ticks to draw from this axis' splits */ + ticks?: Axis.Ticks; + + /** axis border/edge rendering */ + border?: Axis.Border; + } + + export namespace Hooks { + export interface Defs { + /** fires after opts are defaulted & merged but data has not been set and scales have not been ranged */ + init?: (self: uPlot, opts: Options, data: AlignedData) => void; + + /** fires after each initial and subsequent series addition (discern via self.status == 0 or 1) */ + addSeries?: (self: uPlot, seriesIdx: number) => void; + + /** fires after each series deletion */ + delSeries?: (self: uPlot, seriesIdx: number) => void; + + /** fires after any scale has changed */ + setScale?: (self: uPlot, scaleKey: string) => void; + + /** fires after the cursor is moved */ + setCursor?: (self: uPlot) => void; + + /** fires when cursor changes idx and legend updates (or should update) */ + setLegend?: (self: uPlot) => void; + + /** fires after a selection is completed */ + setSelect?: (self: uPlot) => void; + + /** fires after a series is toggled or focused */ + setSeries?: (self: uPlot, seriesIdx: number | null, opts: Series) => void; + + /** fires after data is updated updated */ + setData?: (self: uPlot) => void; + + /** fires after the chart is resized */ + setSize?: (self: uPlot) => void; + + /** fires at start of every redraw */ + drawClear?: (self: uPlot) => void; + + /** fires after all axes are drawn */ + drawAxes?: (self: uPlot) => void; + + /** fires after each series is drawn */ + drawSeries?: (self: uPlot, seriesIdx: number) => void; + + /** fires after everything is drawn */ + draw?: (self: uPlot) => void; + + /** fires after the chart is fully initialized and in the DOM */ + ready?: (self: uPlot) => void; + + /** fires after the chart is destroyed */ + destroy?: (self: uPlot) => void; + + /** fires after .u-over's getBoundingClientRect() is called (due to scroll or resize events) */ + syncRect?: (self: uPlot, rect: DOMRect) => void; + } + + export type Arrays = { + [P in keyof Defs]: Defs[P][] + } + + export type ArraysOrFuncs = { + [P in keyof Defs]: Defs[P][] | Defs[P] + } + } + + export interface Plugin { + /** can mutate provided opts as necessary */ + opts?: (self: uPlot, opts: Options) => void | Options; + hooks: Hooks.ArraysOrFuncs; + } +} + +export as namespace uPlot; diff --git a/docs/dist/uPlot.esm.js b/docs/dist/uPlot.esm.js new file mode 100644 index 0000000..712eee9 --- /dev/null +++ b/docs/dist/uPlot.esm.js @@ -0,0 +1,5959 @@ +/** +* Copyright (c) 2024, 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.30) +*/ + +const FEAT_TIME = true; + +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_SERIES = pre + "series"; +const LEGEND_MARKER = pre + "marker"; +const LEGEND_LABEL = pre + "label"; +const LEGEND_VALUE = pre + "value"; + +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 LEGEND_DISP = "--"; + +const domEnv = typeof window != 'undefined'; + +const doc = domEnv ? document : null; +const win = domEnv ? window : null; +const nav = domEnv ? navigator : null; + +let pxRatio; + +//export const canHover = domEnv && !win.matchMedia('(hover: none)').matches; + +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 = {...evOpts, capture: true}; + +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); +} + +domEnv && setPxRatio(); + +// 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++) { + let v = data[i]; + + if (v != null) { + if (v < _min) + _min = v; + if (v > _max) + _max = v; + } + } + } + + return [_min, _max]; +} + +function getMinMaxLog(data, _i0, _i1) { +// console.log("getMinMax()"); + + let _min = inf; + let _max = -inf; + + for (let i = _i0; i <= _i1; i++) { + let v = data[i]; + + if (v != null && v > 0) { + if (v < _min) + _min = v; + if (v > _max) + _max = v; + } + } + + return [_min, _max]; +} + +function rangeLog(min, max, base, fullMags) { + let minSign = sign(min); + let maxSign = sign(max); + + if (min == max) { + if (minSign == -1) { + min *= base; + max /= base; + } + else { + min /= base; + max *= base; + } + } + + let logFn = base == 10 ? log10 : log2; + + let growMinAbs = minSign == 1 ? floor : ceil; + let growMaxAbs = maxSign == 1 ? ceil : floor; + + let minExp = growMinAbs(logFn(abs(min))); + let maxExp = growMaxAbs(logFn(abs(max))); + + let minIncr = pow(base, minExp); + let maxIncr = pow(base, maxExp); + + // fix values like Math.pow(10, -5) === 0.000009999999999999999 + if (base == 10) { + if (minExp < 0) + minIncr = roundDec(minIncr, -minExp); + if (maxExp < 0) + maxIncr = roundDec(maxIncr, -maxExp); + } + + if (fullMags || base == 2) { + min = minIncr * minSign; + max = maxIncr * maxSign; + } + else { + min = incrRoundDn(min, minIncr); + max = incrRoundUp(max, maxIncr); + } + + 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; + let deltaMag = log10(delta); + + let scalarMax = max(abs(_min), abs(_max)); + let scalarMag = log10(scalarMax); + + let scalarMagDelta = abs(scalarMag - deltaMag); + + // 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 + // or range is 11+ orders of magnitude below raw values, e.g. 99999999.99999996 - 100000000.00000004 + if (delta < 1e-9 || scalarMagDelta > 10) { + 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 || scalarMax || 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 numFormatter = new Intl.NumberFormat(domEnv ? nav.language : 'en-US'); +const fmtNum = val => numFormatter.format(val); + +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 clamp(num, _min, _max) { + return min(max(num, _min), _max); +} + +function fnOrSelf(v) { + return typeof v == "function" ? v : () => v; +} + +const noop = () => {}; + +const retArg0 = _0 => _0; + +const retArg1 = (_0, _1) => _1; + +const retNull = _ => null; + +const retTrue = _ => true; + +const retEq = (a, b) => a == b; + +// this will probably prevent tick incrs > 14 decimal places +// (we generate up to 17 dec, see fixedDec const) +const fixFloat = v => roundDec(v, 14); + +function incrRound(num, incr) { + return fixFloat(roundDec(fixFloat(num/incr))*incr); +} + +function incrRoundUp(num, incr) { + return fixFloat(ceil(fixFloat(num/incr))*incr); +} + +function incrRoundDn(num, incr) { + return fixFloat(floor(fixFloat(num/incr))*incr); +} + +// https://stackoverflow.com/a/48764436 +// rounds half away from zero +function roundDec(val, dec = 0) { + if (isInt(val)) + return val; +// else if (dec == 0) +// return round(val); + + let p = 10 ** dec; + let n = (val * p) * (1 + Number.EPSILON); + return round(n) / p; +} + +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; +const isInt = Number.isInteger; +const isUndef = v => v === void 0; + +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'; +} + +const TypedArray = Object.getPrototypeOf(Uint8Array); + +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 (o instanceof TypedArray) // also (ArrayBuffer.isView(o) && !(o instanceof DataView)) + 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) { + if (allHeadersSame(tables)) { + // console.log('cheap join!'); + + let table = tables[0].slice(); + + for (let i = 1; i < tables.length; i++) + table.push(...tables[i].slice(1)); + + if (!isAsc(table[0])) + table = sortCols(table); + + return table; + } + + 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; + +// TODO: https://github.com/dy/sort-ids (~2x faster for 1e5+ arrays) +function sortCols(table) { + let head = table[0]; + let rlen = head.length; + + let idxs = Array(rlen); + for (let i = 0; i < idxs.length; i++) + idxs[i] = i; + + idxs.sort((i0, i1) => head[i0] - head[i1]); + + let table2 = []; + for (let i = 0; i < table.length; i++) { + let row = table[i]; + let row2 = Array(rlen); + + for (let j = 0; j < rlen; j++) + row2[j] = row[idxs[j]]; + + table2.push(row2); + } + + return table2; +} + +// test if we can do cheap join (all join fields same) +function allHeadersSame(tables) { + let vals0 = tables[0][0]; + let len0 = vals0.length; + + for (let i = 1; i < tables.length; i++) { + let vals1 = tables[i][0]; + + if (vals1.length != len0) + return false; + + if (vals1 != vals0) { + for (let j = 0; j < len0; j++) { + if (vals1[j] != vals0[j]) + return false; + } + } + } + + return true; +} + +function isAsc(vals, samples = 100) { + const len = vals.length; + + // empty or single value + if (len <= 1) + return true; + + // skip leading & trailing nullish + let firstIdx = 0; + let lastIdx = len - 1; + + while (firstIdx <= lastIdx && vals[firstIdx] == null) + firstIdx++; + + while (lastIdx >= firstIdx && vals[lastIdx] == null) + lastIdx--; + + // all nullish or one value surrounded by nullish + if (lastIdx <= firstIdx) + return true; + + const stride = max(1, floor((lastIdx - firstIdx + 1) / samples)); + + for (let prevVal = vals[firstIdx], i = firstIdx + stride; i <= lastIdx; i += stride) { + const v = vals[i]; + + if (v != null) { + if (v <= prevVal) + return false; + + prevVal = v; + } + } + + return true; +} + +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, seriesIdx, dataIdx) => dataIdx == null ? LEGEND_DISP : 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, + mount: noop, + 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 sp.size; +} + +const moveTuple = [0,0]; + +function cursorMove(self, mouseLeft1, mouseTop1) { + moveTuple[0] = mouseLeft1; + moveTuple[1] = mouseTop1; + return moveTuple; +} + +function filtBtn0(self, targ, handle, onlyTarg = true) { + return e => { + e.button == 0 && (!onlyTarg || e.target == targ) && handle(e); + }; +} + +function filtTarg(self, targ, handle, onlyTarg = true) { + return e => { + (!onlyTarg || e.target == targ) && handle(e); + }; +} + +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, // legend clicks, not .u-over clicks + dblclick: filtBtn0, + + mousemove: filtTarg, + mouseleave: filtTarg, + mouseenter: filtTarg, + }, + + drag: { + setScale: true, + x: true, + y: false, + dist: 0, + uni: null, + click: (self, e) => { + // e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + }, + _x: false, + _y: false, + }, + + focus: { + dist: (self, seriesIdx, dataIdx, valPos, curPos) => valPos - curPos, + prox: -1, + bias: 0, + }, + + hover: { + skip: [void 0], + prox: null, + bias: 0, + }, + + left: -10, + top: -10, + idx: null, + dataIdx: null, + idxs: null, + + event: null, +}; + +const axisLines = { + show: true, + stroke: "rgba(0,0,0,0.07)", + width: 2, +// dash: [], +}; + +const grid = assign({}, axisLines, { + filter: retArg1, +}); + +const ticks = assign({}, grid, { + size: 10, +}); + +const border = assign({}, axisLines, { + show: false, +}); + +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 lineGap = 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, + border, + font, + lineGap, + 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 (logBase == 10 && exp < 0) + foundIncr = roundDec(foundIncr, -exp); + + let split = scaleMin; + + do { + splits.push(split); + split = split + foundIncr; + + if (logBase == 10) + split = roundDec(split, 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/; + +const _filt = (splits, distr, re, keepMod) => splits.map((v, i) => ((distr == 4 && v == 0) || i % keepMod == 0 && re.test(v.toExponential()[v < 0 ? 1 : 0])) ? v : null); + +function log10AxisValsFilt(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 + ); + + if (re == RE_1) { + let magSpace = abs(valToPos(1, scaleKey) - _10); + + if (magSpace < minSpace) + return _filt(splits.slice().reverse(), sc.distr, re, ceil(minSpace / magSpace)).reverse(); // max->min skip + } + + return _filt(splits, sc.distr, re, 1); +} + +function log2AxisValsFilt(self, splits, axisIdx, foundSpace, foundIncr) { + let axis = self.axes[axisIdx]; + let scaleKey = axis.scale; + let minSpace = axis._space; + let valToPos = self.valToPos; + + let magSpace = abs(valToPos(1, scaleKey) - valToPos(2, scaleKey)); + + if (magSpace < minSpace) + return _filt(splits.slice().reverse(), 3, RE_ALL, ceil(minSpace / magSpace)).reverse(); // max->min skip + + return splits; +} + +function numSeriesVal(self, val, seriesIdx, dataIdx) { + return dataIdx == null ? LEGEND_DISP : 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, + border, + font, + lineGap, + 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; +} + +const facet = { + scale: null, + auto: true, + sorted: 0, + + // internal caches + min: inf, + max: -inf, +}; + +const gaps = (self, seriesIdx, idx0, idx1, nullGaps) => nullGaps; + +const xySeriesOpts = { + show: true, + auto: true, + sorted: 0, + gaps, + alpha: 1, + facets: [ + assign({}, facet, {scale: 'x'}), + assign({}, facet, {scale: 'y'}), + ], +}; + +const ySeriesOpts = { + scale: "y", + auto: true, + sorted: 0, + show: true, + spanGaps: false, + gaps, + 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 mode = u.mode; + const series = u.series[seriesIdx]; + const data = mode == 2 ? u._data[seriesIdx] : u._data; + const scales = u.scales; + const bbox = u.bbox; + + let dx = data[0], + dy = mode == 2 ? data[1] : data[seriesIdx], + sx = mode == 2 ? scales[series.facets[0].scale] : scales[u.series[0].scale], + sy = 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, + ) + ); +} + +function bandFillClipDirs(self, seriesIdx) { + let fillDir = 0; + + // 2 bits, -1 | 1 + let clipDirs = 0; + + let bands = ifNull(self.bands, EMPTY_ARR); + + for (let i = 0; i < bands.length; i++) { + let b = bands[i]; + + // is a "from" band edge + if (b.series[0] == seriesIdx) + fillDir = b.dir; + // is a "to" band edge + else if (b.series[1] == seriesIdx) { + if (b.dir == 1) + clipDirs |= 1; + else + clipDirs |= 2; + } + } + + return [ + fillDir, + ( + clipDirs == 1 ? -1 : // neg only + clipDirs == 2 ? 1 : // pos only + clipDirs == 3 ? 2 : // both + 0 // neither + ) + ]; +} + +function seriesFillTo(self, seriesIdx, dataMin, dataMax, bandFillDir) { + let mode = self.mode; + let series = self.series[seriesIdx]; + let scaleKey = mode == 2 ? series.facets[1].scale : series.scale; + let scale = self.scales[scaleKey]; + + return ( + bandFillDir == -1 ? scale.min : + bandFillDir == 1 ? scale.max : + scale.distr == 3 ? ( + scale.dir == 1 ? scale.min : + scale.max + ) : 0 + ); +} + +// creates inverted band clip path (from stroke path -> yMax || yMin) +// clipDir is always inverse of fillDir +// default clip dir is upwards (1), since default band fill is downwards/fillBelowTo (-1) (highIdx -> lowIdx) +function clipBandLine(self, seriesIdx, idx0, idx1, strokePath, clipDir) { + 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 or lower y limit + let yLimit = pxRound(valToPosY(clipDir == 1 ? scaleY.max : scaleY.min, 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; + + // hack to ensure we expand the clip enough to avoid cutting off strokes at edges + let maxStrokeWidth = 10; + + w > 0 && rect(clip, prevGapEnd, plotTop - maxStrokeWidth / 2, w, plotTop + plotHgt + maxStrokeWidth); + } + + 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 findGaps(xs, ys, idx0, idx1, dir, pixelForX, align) { + let gaps = []; + let len = xs.length; + + for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) { + let yVal = ys[i]; + + if (yVal === null) { + let fr = i, to = i; + + if (dir == 1) { + while (++i <= idx1 && ys[i] === null) + to = i; + } + else { + while (--i >= idx0 && ys[i] === null) + to = i; + } + + let frPx = pixelForX(xs[fr]); + let toPx = to == fr ? frPx : pixelForX(xs[to]); + + // if value adjacent to edge null is same pixel, then it's partially + // filled and gap should start at next pixel + let fri2 = fr - dir; + let frPx2 = align <= 0 && fri2 >= 0 && fri2 < len ? pixelForX(xs[fri2]) : frPx; + // if (frPx2 == frPx) + // frPx++; + // else + frPx = frPx2; + + let toi2 = to + dir; + let toPx2 = align >= 0 && toi2 >= 0 && toi2 < len ? pixelForX(xs[toi2]) : toPx; + // if (toPx2 == toPx) + // toPx--; + // else + toPx = toPx2; + + if (toPx >= frPx) + gaps.push([frPx, toPx]); // addGap + } + } + + return gaps; +} + +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); }; + + // TODO (pending better browser support): https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/roundRect + return (p, x, y, w, h, endRad = 0, baseRad = 0) => { + if (endRad == 0 && baseRad == 0) + rect(p, x, y, w, h); + else { + endRad = min(endRad, w / 2, h / 2); + baseRad = min(baseRad, 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 + endRad, y); + arcTo(p, x + w, y, x + w, y + h, endRad); + arcTo(p, x + w, y + h, x, y + h, baseRad); + arcTo(p, x, y + h, x, y, baseRad); + arcTo(p, x, y, x + w, y, endRad); + 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(opts) { + const alignGaps = ifNull(opts?.alignGaps, 0); + + 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 pixelForX = val => pxRound(valToPosX(val, scaleX, xDim, xOff)); + let pixelForY = val => pxRound(valToPosY(val, scaleY, yDim, yOff)); + + 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, drawnAtX; + + let accX = pixelForX(dataX[dir == 1 ? idx0 : idx1]); + + // data edges + let lftIdx = nonNullIdx(dataY, idx0, idx1, 1 * dir); + let rgtIdx = nonNullIdx(dataY, idx0, idx1, -1 * dir); + let lftX = pixelForX(dataX[lftIdx]); + let rgtX = pixelForX(dataX[rgtIdx]); + + let hasGap = false; + + for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) { + let x = pixelForX(dataX[i]); + let yVal = dataY[i]; + + if (x == accX) { + if (yVal != null) { + outY = pixelForY(yVal); + + if (minY == inf) { + lineTo(stroke, x, outY); + inY = outY; + } + + minY = min(outY, minY); + maxY = max(outY, maxY); + } + else { + if (yVal === null) + hasGap = true; + } + } + else { + if (minY != inf) { + drawAcc(stroke, accX, minY, maxY, inY, outY); + drawnAtX = accX; + } + + if (yVal != null) { + outY = pixelForY(yVal); + lineTo(stroke, x, outY); + minY = maxY = inY = outY; + } + else { + minY = inf; + maxY = -inf; + + if (yVal === null) + hasGap = true; + } + + accX = x; + } + } + + if (minY != inf && minY != maxY && drawnAtX != accX) + drawAcc(stroke, accX, minY, maxY, inY, outY); + + let [ bandFillDir, bandClipDir ] = bandFillClipDirs(u, seriesIdx); + + if (series.fill != null || bandFillDir != 0) { + let fill = _paths.fill = new Path2D(stroke); + + let fillToVal = series.fillTo(u, seriesIdx, series.min, series.max, bandFillDir); + let fillToY = pixelForY(fillToVal); + + lineTo(fill, rgtX, fillToY); + lineTo(fill, lftX, fillToY); + } + + if (!series.spanGaps) { + // console.time('gaps'); + let gaps = []; + + hasGap && gaps.push(...findGaps(dataX, dataY, idx0, idx1, dir, pixelForX, alignGaps)); + + // console.timeEnd('gaps'); + + // console.log('gaps', JSON.stringify(gaps)); + + _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps); + + _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim); + } + + if (bandClipDir != 0) { + _paths.band = bandClipDir == 2 ? [ + clipBandLine(u, seriesIdx, idx0, idx1, stroke, -1), + clipBandLine(u, seriesIdx, idx0, idx1, stroke, 1), + ] : clipBandLine(u, seriesIdx, idx0, idx1, stroke, bandClipDir); + } + + return _paths; + }); + }; +} + +// BUG: align: -1 behaves like align: 1 when scale.dir: -1 +function stepped(opts) { + const align = ifNull(opts.align, 1); + // whether to draw ascenders/descenders at null/gap bondaries + const ascDesc = ifNull(opts.ascDesc, false); + const alignGaps = ifNull(opts.alignGaps, 0); + const extend = ifNull(opts.extend, 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 { left, width } = u.bbox; + + let pixelForX = val => pxRound(valToPosX(val, scaleX, xDim, xOff)); + let pixelForY = val => pxRound(valToPosY(val, scaleY, yDim, yOff)); + + 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 = scaleX.dir * (scaleX.ori == 0 ? 1 : -1); + + idx0 = nonNullIdx(dataY, idx0, idx1, 1); + idx1 = nonNullIdx(dataY, idx0, idx1, -1); + + let prevYPos = pixelForY(dataY[dir == 1 ? idx0 : idx1]); + let firstXPos = pixelForX(dataX[dir == 1 ? idx0 : idx1]); + let prevXPos = firstXPos; + + let firstXPosExt = firstXPos; + + if (extend && align == -1) { + firstXPosExt = left; + lineTo(stroke, firstXPosExt, prevYPos); + } + + lineTo(stroke, firstXPos, prevYPos); + + for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) { + let yVal1 = dataY[i]; + + if (yVal1 == null) + continue; + + let x1 = pixelForX(dataX[i]); + let y1 = pixelForY(yVal1); + + if (align == 1) + lineTo(stroke, x1, prevYPos); + else + lineTo(stroke, prevXPos, y1); + + lineTo(stroke, x1, y1); + + prevYPos = y1; + prevXPos = x1; + } + + let prevXPosExt = prevXPos; + + if (extend && align == 1) { + prevXPosExt = left + width; + lineTo(stroke, prevXPosExt, prevYPos); + } + + let [ bandFillDir, bandClipDir ] = bandFillClipDirs(u, seriesIdx); + + if (series.fill != null || bandFillDir != 0) { + let fill = _paths.fill = new Path2D(stroke); + + let fillTo = series.fillTo(u, seriesIdx, series.min, series.max, bandFillDir); + let fillToY = pixelForY(fillTo); + + lineTo(fill, prevXPosExt, fillToY); + lineTo(fill, firstXPosExt, fillToY); + } + + if (!series.spanGaps) { + // console.time('gaps'); + let gaps = []; + + gaps.push(...findGaps(dataX, dataY, idx0, idx1, dir, pixelForX, alignGaps)); + + // console.timeEnd('gaps'); + + // console.log('gaps', JSON.stringify(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; + }); + + _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps); + + _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim); + } + + if (bandClipDir != 0) { + _paths.band = bandClipDir == 2 ? [ + clipBandLine(u, seriesIdx, idx0, idx1, stroke, -1), + clipBandLine(u, seriesIdx, idx0, idx1, stroke, 1), + ] : clipBandLine(u, seriesIdx, idx0, idx1, stroke, bandClipDir); + } + + return _paths; + }); + }; +} + +function findColWidth(dataX, dataY, valToPosX, scaleX, xDim, xOff, colWid = inf) { + 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; + } + } + } + + return colWid; +} + +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); + + let ro = opts.radius; + + ro = + // [valueRadius, baselineRadius] + ro == null ? [0, 0] : + typeof ro == 'number' ? [ro, 0] : ro; + + const radiusFn = fnOrSelf(ro); + + const gapFactor = 1 - size[0]; + const _maxWidth = ifNull(size[1], inf); + const _minWidth = ifNull(size[2], 1); + + 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; + let _align = align; + + let extraGap = _extraGap * pxRatio; + let maxWidth = _maxWidth * pxRatio; + let minWidth = _minWidth * pxRatio; + + let valRadius, baseRadius; + + if (scaleX.ori == 0) + [valRadius, baseRadius] = radiusFn(u, seriesIdx); + else + [baseRadius, valRadius] = radiusFn(u, seriesIdx); + + 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); + }; + + // band where this series is the "from" edge + let band = ifNull(u.bands, EMPTY_ARR).find(b => b.series[0] == seriesIdx); + + let fillDir = band != null ? band.dir : 0; + let fillTo = series.fillTo(u, seriesIdx, series.min, series.max, fillDir); + let fillToY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff)); + + // barWid is to center of stroke + let xShift, barWid, fullGap, colWid = xDim; + + let strokeWidth = pxRound(series.width * pxRatio); + + let multiPath = false; + + let fillColors = null; + let fillPaths = null; + let strokeColors = null; + let strokePaths = null; + + if (dispFills != null && (strokeWidth == 0 || 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()); + }); + + if (strokeWidth > 0) { + 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) { + _align = 1; + 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) + + colWid = findColWidth(dataX, dataY, valToPosX, scaleX, xDim, xOff, colWid); + + let gapWid = colWid - barWid; + fullGap = gapWid + extraGap; + } + else { + colWid = findColWidth(dataX, dataY, valToPosX, scaleX, xDim, xOff, colWid); + + let gapWid = colWid * gapFactor; + + fullGap = gapWid + extraGap; + barWid = colWid - fullGap; + } + + if (fullGap < 1) + fullGap = 0; + + if (strokeWidth >= barWid / 2) + strokeWidth = 0; + + // for small gaps, disable pixel snapping since gap inconsistencies become noticible and annoying + if (fullGap < 5) + pxRound = retArg0; + + let insetStroke = fullGap > 0; + + let rawBarWid = colWid - fullGap - (insetStroke ? strokeWidth : 0); + + barWid = pxRound(clamp(rawBarWid, minWidth, maxWidth)); + + xShift = (_align == 0 ? barWid / 2 : _align == _dirX ? 0 : barWid) - _align * _dirX * ((_align == 0 ? extraGap / 2 : 0) + (insetStroke ? strokeWidth / 2 : 0)); + + + const _paths = {stroke: null, fill: null, clip: null, band: null, gaps: null, flags: 0}; // disp, geom + + const stroke = multiPath ? null : new Path2D(); + + let dataY0 = null; + + if (band != null) + dataY0 = u.data[band.series[1]]; + else { + let { y0, y1 } = disp; + + if (y0 != null && y1 != null) { + dataY = y1.values(u, seriesIdx, idx0, idx1); + dataY0 = y0.values(u, seriesIdx, idx0, idx1); + } + } + + let radVal = valRadius * barWid; + let radBase = baseRadius * barWid; + + for (let i = _dirX == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dirX) { + let yVal = dataY[i]; + + if (yVal == null) + continue; + + if (dataY0 != null) { + let yVal0 = dataY0[i] ?? 0; + + if (yVal - yVal0 == 0) + continue; + + fillToY = valToPosY(yVal0, scaleY, yDim, yOff); + } + + 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, fillTo), scaleY, yDim, yOff); + + let lft = pxRound(xPos - xShift); + let btm = pxRound(max(yPos, fillToY)); + let top = pxRound(min(yPos, fillToY)); + // this includes the stroke + let barHgt = btm - top; + + if (yVal != null) { // && yVal != fillTo (0 height bar) + let rv = yVal < 0 ? radBase : radVal; + let rb = yVal < 0 ? radVal : radBase; + + if (multiPath) { + if (strokeWidth > 0 && strokeColors[i] != null) + rect(strokePaths.get(strokeColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), rv, rb); + + if (fillColors[i] != null) + rect(fillPaths.get(fillColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), rv, rb); + } + else + rect(stroke, lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), rv, rb); + + each(u, seriesIdx, i, + lft - strokeWidth / 2, + top, + barWid + strokeWidth, + barHgt, + ); + } + } + + if (strokeWidth > 0) + _paths.stroke = multiPath ? strokePaths : stroke; + else if (!multiPath) { + _paths._fill = series.width == 0 ? series._fill : series._stroke ?? series._fill; + _paths.width = 0; + } + + _paths.fill = multiPath ? fillPaths : stroke; + + return _paths; + }); + }; +} + +function splineInterp(interp, opts) { + const alignGaps = ifNull(opts?.alignGaps, 0); + + 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 pixelForX = val => pxRound(valToPosX(val, scaleX, xDim, xOff)); + let pixelForY = val => pxRound(valToPosY(val, scaleY, yDim, yOff)); + + let moveTo, bezierCurveTo, lineTo; + + if (scaleX.ori == 0) { + moveTo = moveToH; + lineTo = lineToH; + bezierCurveTo = bezierCurveToH; + } + else { + moveTo = moveToV; + lineTo = lineToV; + bezierCurveTo = bezierCurveToV; + } + + const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1); + + idx0 = nonNullIdx(dataY, idx0, idx1, 1); + idx1 = nonNullIdx(dataY, idx0, idx1, -1); + + let firstXPos = pixelForX(dataX[dir == 1 ? idx0 : idx1]); + let prevXPos = firstXPos; + + let xCoords = []; + let yCoords = []; + + for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) { + let yVal = dataY[i]; + + if (yVal != null) { + let xVal = dataX[i]; + let xPos = pixelForX(xVal); + + xCoords.push(prevXPos = xPos); + yCoords.push(pixelForY(dataY[i])); + } + } + + 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; + + let [ bandFillDir, bandClipDir ] = bandFillClipDirs(u, seriesIdx); + + if (series.fill != null || bandFillDir != 0) { + let fill = _paths.fill = new Path2D(stroke); + + let fillTo = series.fillTo(u, seriesIdx, series.min, series.max, bandFillDir); + let fillToY = pixelForY(fillTo); + + lineTo(fill, prevXPos, fillToY); + lineTo(fill, firstXPos, fillToY); + } + + if (!series.spanGaps) { + // console.time('gaps'); + let gaps = []; + + gaps.push(...findGaps(dataX, dataY, idx0, idx1, dir, pixelForX, alignGaps)); + + // console.timeEnd('gaps'); + + // console.log('gaps', JSON.stringify(gaps)); + + _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps); + + _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim); + } + + if (bandClipDir != 0) { + _paths.band = bandClipDir == 2 ? [ + clipBandLine(u, seriesIdx, idx0, idx1, stroke, -1), + clipBandLine(u, seriesIdx, idx0, idx1, stroke, 1), + ] : clipBandLine(u, seriesIdx, idx0, idx1, stroke, bandClipDir); + } + + 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, opts); +} + +// 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() { + for (let u of cursorPlots) + u.syncRect(true); +} + +if (domEnv) { + on(resize, win, invalidateRects); + on(scroll, win, invalidateRects, true); + on(dppxchange, win, () => { uPlot.pxRatio = pxRatio; }); +} + +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); + + on("click", wrap, e => { + if (e.target === over) { + let didDrag = mouseLeft1 != mouseLeft0 || mouseTop1 != mouseTop0; + didDrag && drag.click(self, e); + } + }, true); + + 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); + b.dir = ifNull(b.dir, -1); + }); + + 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); + + sc.key = scaleKey; + + let isTime = sc.time; + + let rn = sc.range; + + let rangeIsArr = isArr(rn); + + if (scaleKey != xScaleKey || (mode == 2 && !isTime)) { + // 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 legendTable; + let legendHead; + let legendBody; + 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] = LEGEND_DISP; + } + + if (showLegend) { + legendTable = placeTag("table", LEGEND, root); + legendBody = placeTag("tbody", null, legendTable); + + // allows legend to be moved out of root + legend.mount(self, legendTable); + + if (multiValLegend) { + legendHead = placeTag("thead", null, legendTable, legendBody); + + let head = placeTag("tr", null, legendHead); + placeTag("th", null, head); + + for (var key in legendCols) + placeTag("th", LEGEND_LABEL, head).textContent = key; + } + else { + addClass(legendTable, LEGEND_INLINE); + legend.live && addClass(legendTable, 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, legendBody, legendBody.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; + + setCursorEvent(e); + + 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); + }, false); + + if (cursorFocus) { + onMouse(mouseenter, label, e => { + if (cursor._lock) + return; + + setCursorEvent(e); + + setSeries(series.indexOf(s), FOCUS_TRUE, true, syncOpts.setSeries); + }, false); + } + } + + 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, onlyTarg = true) { + const targListeners = mouseListeners.get(targ) || {}; + const listener = cursor.bind[ev](self, targ, fn, onlyTarg); + + 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; + + // previous values for diffing + let _plotLftCss = plotLftCss; + let _plotTopCss = plotTopCss; + let _plotWidCss = plotWidCss; + let _plotHgtCss = plotHgtCss; + + + 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 shouldSetSelect = 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; + + 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); + + if (cursor.dataIdx == null) { + let hov = cursor.hover; + + let skip = hov.skip = new Set(hov.skip ?? []); + skip.add(void 0); // alignment artifacts + let prox = hov.prox = fnOrSelf(hov.prox); + let bias = hov.bias ??= 0; + + // TODO: only scan between in-view idxs (i0, i1) + cursor.dataIdx = (self, seriesIdx, cursorIdx, valAtPosX) => { + if (seriesIdx == 0) + return cursorIdx; + + let idx2 = cursorIdx; + + let _prox = prox(self, seriesIdx, cursorIdx, valAtPosX) ?? inf; + let withProx = _prox >= 0 && _prox < inf; + let xDim = scaleX.ori == 0 ? plotWidCss : plotHgtCss; + let cursorLft = cursor.left; + + let xValues = data[0]; + let yValues = data[seriesIdx]; + + if (skip.has(yValues[cursorIdx])) { + idx2 = null; + + let nonNullLft = null, + nonNullRgt = null, + j; + + if (bias == 0 || bias == -1) { + j = cursorIdx; + while (nonNullLft == null && j-- > 0) { + if (!skip.has(yValues[j])) + nonNullLft = j; + } + } + + if (bias == 0 || bias == 1) { + j = cursorIdx; + while (nonNullRgt == null && j++ < yValues.length) { + if (!skip.has(yValues[j])) + nonNullRgt = j; + } + } + + if (nonNullLft != null || nonNullRgt != null) { + if (withProx) { + let lftPos = nonNullLft == null ? -Infinity : valToPosX(xValues[nonNullLft], scaleX, xDim, 0); + let rgtPos = nonNullRgt == null ? Infinity : valToPosX(xValues[nonNullRgt], scaleX, xDim, 0); + + let lftDelta = cursorLft - lftPos; + let rgtDelta = rgtPos - cursorLft; + + if (lftDelta <= rgtDelta) { + if (lftDelta <= _prox) + idx2 = nonNullLft; + } else { + if (rgtDelta <= _prox) + idx2 = nonNullRgt; + } + } + else { + idx2 = + nonNullRgt == null ? nonNullLft : + nonNullLft == null ? nonNullRgt : + cursorIdx - nonNullLft <= nonNullRgt - cursorIdx ? nonNullLft : nonNullRgt; + } + } + } + else if (withProx) { + let dist = abs(cursorLft - valToPosX(xValues[cursorIdx], scaleX, xDim, 0)); + + if (dist > _prox) + idx2 = null; + } + + return idx2; + }; + } + + const setCursorEvent = e => { cursor.event = e; }; + + 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]; + // position caches in CSS pixels + let cursorPtsLft = [null]; + let cursorPtsTop = [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(max(1, 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); + + if (pt != null) { + cursorPts.splice(i, 0, pt); + cursorPtsLft.splice(i, 0, 0); + cursorPtsTop.splice(i, 0, 0); + } + } + + fire("addSeries", i); + } + + function addSeries(opts, si) { + si = si == null ? series.length : si; + + opts = mode == 1 ? setDefault(opts, si, xSeriesOpts, ySeriesOpts) : setDefault(opts, si, null, xySeriesOpts); + + 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); + + if (cursorPts.length > 1) { + cursorPts.splice(i, 1)[0].remove(); + cursorPtsLft.splice(i, 1); + cursorPtsTop.splice(i, 1); + } + } + + // TODO: de-init no-longer-needed scales? + + fire("delSeries", i); + } + + self.delSeries = delSeries; + + const sidesWithAxes = [false, false, false, false]; + + function initAxis(axis, i) { + axis._show = axis.show; + + if (axis.show) { + let isVt = axis.side % 2; + + let sc = scales[axis.scale]; + + // this can occur if all series specify non-default scales + if (sc == null) { + axis.scale = isVt ? series[1].scale : xScaleKey; + sc = scales[axis.scale]; + } + + // also set defaults for incrs & values based on axis distr + let isTime = sc.time; + + axis.size = fnOrSelf(axis.size); + axis.space = fnOrSelf(axis.space); + axis.rotate = fnOrSelf(axis.rotate); + + if (isArr(axis.incrs)) { + axis.incrs.forEach(incr => { + !fixedDec.has(incr) && fixedDec.set(incr, guessDec(incr)); + }); + } + + 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); + axis.border.stroke = fnOrSelf(axis.border.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 && sc.log == 10 ? log10AxisValsFilt : sc.distr == 3 && sc.log == 2 ? log2AxisValsFilt : 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) { + data = _data == null ? [] : _data; + + self.data = self._data = data; + + if (mode == 2) { + dataLen = 0; + for (let i = 1; i < series.length; i++) + dataLen += data[i][0].length; + } + else { + if (data.length == 0) + self.data = self._data = data = [[]]; + + data0 = data[0]; + dataLen = data0.length; + + let scaleData = data; + + if (xScaleDistr == 2) { + scaleData = data.slice(); + + let _data0 = scaleData[0] = Array(dataLen); + for (let i = 0; i < dataLen; i++) + _data0[i] = i; + } + + self._data = data = scaleData; + } + + resetYSeries(true); + + fire("setData"); + + // forces x axis tick values to re-generate when neither x scale nor y scale changes + // in ordinal mode, scale range is by index, so will not change if new data has same length, but tick values are from data + if (xScaleDistr == 2) { + shouldConvergeSize = true; + + /* or somewhat cheaper, and uglier: + if (ready) { + // logic extracted from axesCalc() + let i = 0; + let axis = axes[i]; + let _splits = axis._splits.map(i => data0[i]); + let [_incr, _space] = axis._found; + let incr = data0[_splits[1]] - data0[_splits[0]]; + axis._values = axis.values(self, axis.filter(self, _splits, i, _space, incr), i, _space, incr); + } + */ + } + + if (_resetScales !== false) { + let xsc = scaleX; + + if (xsc.auto(self, viaAutoScaleX)) + autoScaleX(); + else + _setScale(xScaleKey, xsc.min, xsc.max); + + shouldSetCursor = 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 (_min == _max) { + 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, width, dash, cap, fill, join) { + stroke ??= transparent; + 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, sorted = 0) { + if (data.length > 0 && 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, sorted)) : [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]); + } + } + + const AUTOSCALE = {min: null, max: null}; + + function setScales() { + // log("setScales()", arguments); + + // implicitly add auto scales, and unranged scales + for (let k in scales) { + let sc = scales[k]; + + if (pendScales[k] == null && + ( + // scales that have never been set (on init) + sc.min == null || + // or auto scales when the x scale was explicitly set + pendScales[xScaleKey] != null && sc.auto(self, viaAutoScaleX) + ) + ) { + pendScales[k] = AUTOSCALE; + } + } + + // implicitly add dependent scales + for (let k in scales) { + let sc = scales[k]; + + if (pendScales[k] == null && sc.from != null && pendScales[sc.from] != null) + pendScales[k] = AUTOSCALE; + } + + // explicitly setting the x-scale invalidates everything (acts as redraw) + if (pendScales[xScaleKey] != null) + resetYSeries(true); // TODO: only reset series on auto scales? + + let wipScales = {}; + + for (let k in pendScales) { + let psc = pendScales[k]; + + if (psc != null) { + let wsc = wipScales[k] = copy(scales[k], fastIsObj); + + if (psc.min != null) + assign(wsc, psc); + 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 psc = pendScales[k]; + + if (psc == null) + return; + + let wsc = wipScales[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]); + + // don't try to contract same or adjacent idxs + if (i1 - i0 > 1) { + // 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.sorted); + + 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]; + + let wscx = wipScales[xScaleKey]; + let wscy = wipScales[yScaleKey]; + + // null can happen when only x is zoomed, but y has static range and doesnt get auto-added to pending + wscx != null && accScale(wscx, pendScales[xScaleKey], xFacet, xData, xFacet.sorted); + wscy != null && accScale(wscy, pendScales[yScaleKey], yFacet, yData, yFacet.sorted); + + // 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 && cursor.left >= 0) + shouldSetCursor = shouldSetLegend = true; + } + + 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) { + cacheStrokeFill(i, false); + cacheStrokeFill(i, true); + + if (s._paths == null) { + if (ctxAlpha != s.alpha) + ctx.globalAlpha = ctxAlpha = s.alpha; + + let _idxs = mode == 2 ? [0, data[i][0].length - 1] : getOuterIdxs(data[i]); + s._paths = s.paths(self, i, _idxs[0], _idxs[1]); + + if (ctxAlpha != 1) + ctx.globalAlpha = ctxAlpha = 1; + } + } + }); + + series.forEach((s, i) => { + if (i > 0 && s.show) { + if (ctxAlpha != s.alpha) + ctx.globalAlpha = ctxAlpha = s.alpha; + + s._paths != null && drawPath(i, false); + + { + let _gaps = s._paths != null ? s._paths.gaps : null; + + let show = s.points.show(self, i, i0, i1, _gaps); + let idxs = s.points.filter(self, i, show, _gaps); + + 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 { + stroke, + fill, + clip: gapsClip, + flags, + + _stroke: strokeStyle = s._stroke, + _fill: fillStyle = s._fill, + _width: width = s.width, + } = s._paths; + + width = roundDec(width * pxRatio, 3); + + let boundsClip = null; + let offset = (width % 2) / 2; + + if (_points && fillStyle == null) + fillStyle = width > 0 ? "#fff" : strokeStyle; + + let _pxAlign = s.pxAlign == 1 && offset > 0; + + _pxAlign && ctx.translate(offset, offset); + + if (!_points) { + let lft = plotLft - width / 2, + top = plotTop - width / 2, + wid = plotWid + width, + hgt = plotHgt + width; + + 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 + flags != 0 && 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; + + if (isArr(bandClip)) + bandClip = b.dir == 1 ? bandClip[0] : bandClip[1]; + + 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 border = axis.border; + 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] * axis.lineGap; + + 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, + ); + } + + if (border.show) { + drawOrthoLines( + [basePos], + [1], + ori == 0 ? 1 : 0, + ori == 0 ? 1 : 2, + ori == 1 ? plotTop : plotLft, + ori == 1 ? plotHgt : plotWid, + roundDec(border.width * pxRatio, 3), + border.stroke(self, i), + border.dash, + border.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; + let deferHooks = false; + let hooksQueue = []; + + function flushHooks() { + deferHooks = false; + + for (let i = 0; i < hooksQueue.length; i++) + fire(...hooksQueue[i]); + + hooksQueue.length = 0; + } + + function commit() { + if (!queuedCommit) { + microTask(_commit); + queuedCommit = true; + } + } + + // manual batching (aka immediate mode), skips microtask queue + function batch(fn, _deferHooks = false) { + queuedCommit = true; + deferHooks = _deferHooks; + + fn(self); + _commit(); + + if (_deferHooks && hooksQueue.length > 0) + queueMicrotask(flushHooks); + } + + self.batch = batch; + + function _commit() { + // log("_commit()", arguments); + + if (shouldSetScales) { + setScales(); + shouldSetScales = false; + } + + if (shouldConvergeSize) { + convergeSize(); + shouldConvergeSize = false; + } + + if (shouldSetSize) { + setStylePx(under, LEFT, plotLftCss); + setStylePx(under, TOP, plotTopCss); + setStylePx(under, WIDTH, plotWidCss); + setStylePx(under, HEIGHT, plotHgtCss); + + setStylePx(over, LEFT, plotLftCss); + setStylePx(over, TOP, plotTopCss); + setStylePx(over, WIDTH, plotWidCss); + setStylePx(over, HEIGHT, plotHgtCss); + + setStylePx(wrap, WIDTH, fullWidCss); + setStylePx(wrap, HEIGHT, fullHgtCss); + + // NOTE: mutating this during print preview in Chrome forces transparent + // canvas pixels to white, even when followed up with clearRect() below + can.width = round(fullWidCss * pxRatio); + can.height = round(fullHgtCss * pxRatio); + + axes.forEach(({ _el, _show, _size, _pos, side }) => { + if (_el != null) { + if (_show) { + let posOffset = (side === 3 || side === 0 ? _size : 0); + let isVt = side % 2 == 1; + + setStylePx(_el, isVt ? "left" : "top", _pos - posOffset); + setStylePx(_el, isVt ? "width" : "height", _size); + setStylePx(_el, isVt ? "top" : "left", isVt ? plotTopCss : plotLftCss); + setStylePx(_el, isVt ? "height" : "width", isVt ? plotHgtCss : plotWidCss); + + remClass(_el, OFF); + } + else + addClass(_el, OFF); + } + }); + + // invalidate ctx style cache + ctxStroke = ctxFill = ctxWidth = ctxJoin = ctxCap = ctxFont = ctxAlign = ctxBaseline = ctxDash = null; + ctxAlpha = 1; + + syncRect(true); + + if ( + plotLftCss != _plotLftCss || + plotTopCss != _plotTopCss || + plotWidCss != _plotWidCss || + plotHgtCss != _plotHgtCss + ) { + resetYSeries(false); + + let pctWid = plotWidCss / _plotWidCss; + let pctHgt = plotHgtCss / _plotHgtCss; + + if (cursor.show && !shouldSetCursor && cursor.left >= 0) { + cursor.left *= pctWid; + cursor.top *= pctHgt; + + vCursor && elTrans(vCursor, round(cursor.left), 0, plotWidCss, plotHgtCss); + hCursor && elTrans(hCursor, 0, round(cursor.top), plotWidCss, plotHgtCss); + + for (let i = 1; i < cursorPts.length; i++) { + cursorPtsLft[i] *= pctWid; + cursorPtsTop[i] *= pctHgt; + elTrans(cursorPts[i], incrRoundUp(cursorPtsLft[i], 1), incrRoundUp(cursorPtsTop[i], 1), plotWidCss, plotHgtCss); + } + } + + if (select.show && !shouldSetSelect && select.left >= 0 && select.width > 0) { + select.left *= pctWid; + select.width *= pctWid; + select.top *= pctHgt; + select.height *= pctHgt; + + for (let prop in _hideProps) + setStylePx(selectDiv, prop, select[prop]); + } + + _plotLftCss = plotLftCss; + _plotTopCss = plotTopCss; + _plotWidCss = plotWidCss; + _plotHgtCss = plotHgtCss; + } + + 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 (select.show && shouldSetSelect) { + setSelect(select); + shouldSetSelect = false; + } + + if (cursor.show && shouldSetCursor) { + updateCursor(null, true, false); + shouldSetCursor = false; + } + + if (legend.show && legend.live && shouldSetLegend) { + setLegend(); + shouldSetLegend = false; // redundant currently + } + + 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) { + select[prop] = opts[prop]; + + if (prop in _hideProps) + setStylePx(selectDiv, 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); + + if (opts.focus != null) + setFocus(i); + + if (opts.show != null) { + series.forEach((s, si) => { + if (si > 0 && (i == si || i == null)) { + s.show = opts.show; + toggleDOM(si, opts.show); + + if (mode == 2) { + _setScale(s.facets[0].scale, null, null); + _setScale(s.facets[1].scale, null, null); + } + else + _setScale(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); + opts.dir = ifNull(opts.dir, -1); + 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}; + + function setFocus(i) { + if (i != focusedSeries) { + // log("setFocus()", arguments); + + let allFocused = i == null; + + let _setAlpha = focus.alpha != 1; + + series.forEach((s, i2) => { + if (mode == 1 || i2 > 0) { + 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) { + onMouse(mouseleave, legendTable, e => { + if (cursor._lock) + return; + + setCursorEvent(e); + + if (focusedSeries != null) + setSeries(null, FOCUS_TRUE, true, syncOpts.setSeries); + }); + } + + 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, + ) + ); + + 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) { + if (opts.idxs) { + opts.idxs.forEach((didx, sidx) => { + activeIdxs[sidx] = didx; + }); + } + else if (!isUndef(opts.idx)) + activeIdxs.fill(opts.idx); + + legend.idx = activeIdxs[0]; + } + + for (let sidx = 0; sidx < series.length; sidx++) { + if (sidx > 0 || mode == 1 && !multiValLegend) + setLegendValues(sidx, activeIdxs[sidx]); + } + + if (showLegend && legend.live) + syncLegend(); + + shouldSetLegend = false; + + _fire !== false && fire("setLegend"); + } + + self.setLegend = setLegend; + + function setLegendValues(sidx, idx) { + let s = series[sidx]; + let src = sidx == 0 && xScaleDistr == 2 ? data0 : data[sidx]; + let val; + + if (multiValLegend) + val = s.values(self, sidx, idx) ?? NULL_LEGEND_VALUES; + else { + val = s.value(self, idx == null ? null : src[idx], sidx, idx); + val = val == null ? NULL_LEGEND_VALUES : {_: val}; + } + + 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); + + cursor.left = mouseLeft1; + cursor.top = 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 = cursor.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(idx); + shouldSetLegend = true; + } + } + 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 = cursor.idx = closestIdx(valAtPosX, data[0], i0, i1); + xPos = valToPosX(data[0][idx], scaleX, xDim, 0); + } + + for (let i = mode == 2 ? 1 : 0; i < series.length; i++) { + let s = series[i]; + + let idx1 = activeIdxs[i]; + let yVal1 = idx1 == null ? null : (mode == 1 ? data[i][idx1] : data[i][1][idx1]); + + let idx2 = cursor.dataIdx(self, i, idx, valAtPosX); + let yVal2 = idx2 == null ? null : (mode == 1 ? data[i][idx2] : data[i][1][idx2]); + + shouldSetLegend = shouldSetLegend || yVal2 != yVal1 || idx2 != idx1; + + activeIdxs[i] = idx2; + + let xPos2 = idx2 == idx ? xPos : valToPosX(mode == 1 ? data[0][idx2] : data[i][0][idx2], scaleX, xDim, 0); + + if (i > 0 && s.show) { + // this doesnt really work for state timeline, heatmap, status history (where the value maps to color, not y coords) + let yPos = yVal2 == null ? -10 : valToPosY(yVal2, mode == 1 ? scales[s.scale] : scales[s.facets[1].scale], yDim, 0); + + if (cursorFocus && yVal2 != null) { + let mouseYPos = scaleX.ori == 1 ? mouseLeft1 : mouseTop1; + let dist = abs(focus.dist(self, i, idx2, yPos, mouseYPos)); + + if (dist < closestDist) { + let bias = focus.bias; + + if (bias != 0) { + let mouseYVal = posToVal(mouseYPos, s.scale); + + let seriesYValSign = yVal2 >= 0 ? 1 : -1; + let mouseYValSign = mouseYVal >= 0 ? 1 : -1; + + // with a focus bias, we will never cross zero when prox testing + // it's either closest towards zero, or closest away from zero + if (mouseYValSign == seriesYValSign && ( + mouseYValSign == 1 ? + (bias == 1 ? yVal2 >= mouseYVal : yVal2 <= mouseYVal) : // >= 0 + (bias == 1 ? yVal2 <= mouseYVal : yVal2 >= mouseYVal) // < 0 + )) { + closestDist = dist; + closestSeries = i; + } + } + else { + 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); + + cursorPtsLft[i] = ptLft; + cursorPtsTop[i] = ptTop; + + elTrans(cursorPts[i], incrRoundUp(ptLft, 1), incrRoundUp(ptTop, 1), plotWidCss, plotHgtCss); + } + } + } + } + + // 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; + + if (dragX || dragY) { + 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 && dragX) { + if (sori == 0) { + sOff = left; + sDim = width; + } + else { + sOff = top; + sDim = height; + } + + 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 && dragY) { + if (sori == 1) { + sOff = left; + sDim = width; + } + else { + sOff = top; + sDim = height; + } + + 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); + } + else + hideSelect(); + } + 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); + } + } + } + + if (shouldSetLegend) { + legend.idx = idx; + setLegend(); + } + + _fire !== false && fire("setCursor"); + } + + let rect = null; + + Object.defineProperty(self, 'rect', { + get() { + if (rect == null) + syncRect(false); + + return rect; + }, + }); + + function syncRect(defer = false) { + if (defer) + rect = null; + else { + rect = over.getBoundingClientRect(); + fire("syncRect", rect); + } + } + + function mouseMove(e, src, _l, _t, _w, _h, _i) { + if (cursor._lock) + return; + + // Chrome on Windows has a bug which triggers a stray mousemove event after an initial mousedown event + // when clicking into a plot as part of re-focusing the browser window. + // we gotta ignore it to avoid triggering a phantom drag / setSelect + // However, on touch-only devices Chrome-based browsers trigger a 0-distance mousemove before mousedown + // so we don't ignore it when mousedown has set the dragging flag + if (dragging && e != null && e.movementX == 0 && e.movementY == 0) + 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); + + setCursorEvent(e); + + 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.axes[0].side % 2 == 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; + } + } + + const _hideProps = { + width: 0, + height: 0, + left: 0, + top: 0, + }; + + function hideSelect() { + setSelect(_hideProps, false); + } + + let downSelectLeft; + let downSelectTop; + let downSelectWidth; + let downSelectHeight; + + 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, false); + pubSync(mousedown, self, mouseLeft0, mouseTop0, plotWidCss, plotHgtCss, null); + } + + let { left, top, width, height } = select; + + downSelectLeft = left; + downSelectTop = top; + downSelectWidth = width; + downSelectHeight = height; + + hideSelect(); + } + + 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; + let chgSelect = ( + downSelectLeft != left || + downSelectTop != top || + downSelectWidth != width || + downSelectHeight != height + ); + + hasSelect && chgSelect && setSelect(select); + + if (drag.setScale && hasSelect && chgSelect) { + // 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) + return; + + setCursorEvent(e); + + 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) { + if (cursor._lock) + return; + + setCursorEvent(e); + + 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) => { + let seriesIdxMatcher = syncOpts.match[2]; + idx = seriesIdxMatcher(self, src, idx); + idx != -1 && setSeries(idx, opts, true, false); + }; + + if (cursor.show) { + onMouse(mousedown, over, mouseDown); + onMouse(mousemove, over, mouseMove); + onMouse(mouseenter, over, e => { + setCursorEvent(e); + syncRect(false); + }); + 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 (deferHooks) + hooksQueue.push([evName, a1, a2]); + else { + 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 seriesIdxMatcher = (self, src, srcSeriesIdx) => srcSeriesIdx; + + const syncOpts = assign({ + key: null, + setSeries: false, + filters: { + pub: retTrue, + sub: retTrue, + }, + scales: [xScaleKey, series[1] ? series[1].scale : null], + match: [retEq, retEq, seriesIdxMatcher], + values: [null, null], + }, cursor.sync); + + if (syncOpts.match.length == 2) + syncOpts.match.push(seriesIdxMatcher); + + 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(); + legendTable?.remove(); // in case mounted outside of root + 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(); + + shouldSetSelect = select.show && (select.width > 0 || select.height > 0); + shouldSetCursor = shouldSetLegend = true; + + _setSize(opts.width, opts.height); + } + + 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.pxRatio = pxRatio; + +{ + 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/docs/dist/uPlot.iife.js b/docs/dist/uPlot.iife.js new file mode 100644 index 0000000..ac7c350 --- /dev/null +++ b/docs/dist/uPlot.iife.js @@ -0,0 +1,5964 @@ +/** +* Copyright (c) 2024, 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.30) +*/ + +var uPlot = (function () { + 'use strict'; + + const FEAT_TIME = true; + + 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_SERIES = pre + "series"; + const LEGEND_MARKER = pre + "marker"; + const LEGEND_LABEL = pre + "label"; + const LEGEND_VALUE = pre + "value"; + + 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 LEGEND_DISP = "--"; + + const domEnv = typeof window != 'undefined'; + + const doc = domEnv ? document : null; + const win = domEnv ? window : null; + const nav = domEnv ? navigator : null; + + let pxRatio; + + //export const canHover = domEnv && !win.matchMedia('(hover: none)').matches; + + 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 = {...evOpts, capture: true}; + + 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); + } + + domEnv && setPxRatio(); + + // 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++) { + let v = data[i]; + + if (v != null) { + if (v < _min) + _min = v; + if (v > _max) + _max = v; + } + } + } + + return [_min, _max]; + } + + function getMinMaxLog(data, _i0, _i1) { + // console.log("getMinMax()"); + + let _min = inf; + let _max = -inf; + + for (let i = _i0; i <= _i1; i++) { + let v = data[i]; + + if (v != null && v > 0) { + if (v < _min) + _min = v; + if (v > _max) + _max = v; + } + } + + return [_min, _max]; + } + + function rangeLog(min, max, base, fullMags) { + let minSign = sign(min); + let maxSign = sign(max); + + if (min == max) { + if (minSign == -1) { + min *= base; + max /= base; + } + else { + min /= base; + max *= base; + } + } + + let logFn = base == 10 ? log10 : log2; + + let growMinAbs = minSign == 1 ? floor : ceil; + let growMaxAbs = maxSign == 1 ? ceil : floor; + + let minExp = growMinAbs(logFn(abs(min))); + let maxExp = growMaxAbs(logFn(abs(max))); + + let minIncr = pow(base, minExp); + let maxIncr = pow(base, maxExp); + + // fix values like Math.pow(10, -5) === 0.000009999999999999999 + if (base == 10) { + if (minExp < 0) + minIncr = roundDec(minIncr, -minExp); + if (maxExp < 0) + maxIncr = roundDec(maxIncr, -maxExp); + } + + if (fullMags || base == 2) { + min = minIncr * minSign; + max = maxIncr * maxSign; + } + else { + min = incrRoundDn(min, minIncr); + max = incrRoundUp(max, maxIncr); + } + + 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; + let deltaMag = log10(delta); + + let scalarMax = max(abs(_min), abs(_max)); + let scalarMag = log10(scalarMax); + + let scalarMagDelta = abs(scalarMag - deltaMag); + + // 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 + // or range is 11+ orders of magnitude below raw values, e.g. 99999999.99999996 - 100000000.00000004 + if (delta < 1e-9 || scalarMagDelta > 10) { + 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 || scalarMax || 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 numFormatter = new Intl.NumberFormat(domEnv ? nav.language : 'en-US'); + const fmtNum = val => numFormatter.format(val); + + 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 clamp(num, _min, _max) { + return min(max(num, _min), _max); + } + + function fnOrSelf(v) { + return typeof v == "function" ? v : () => v; + } + + const noop = () => {}; + + const retArg0 = _0 => _0; + + const retArg1 = (_0, _1) => _1; + + const retNull = _ => null; + + const retTrue = _ => true; + + const retEq = (a, b) => a == b; + + // this will probably prevent tick incrs > 14 decimal places + // (we generate up to 17 dec, see fixedDec const) + const fixFloat = v => roundDec(v, 14); + + function incrRound(num, incr) { + return fixFloat(roundDec(fixFloat(num/incr))*incr); + } + + function incrRoundUp(num, incr) { + return fixFloat(ceil(fixFloat(num/incr))*incr); + } + + function incrRoundDn(num, incr) { + return fixFloat(floor(fixFloat(num/incr))*incr); + } + + // https://stackoverflow.com/a/48764436 + // rounds half away from zero + function roundDec(val, dec = 0) { + if (isInt(val)) + return val; + // else if (dec == 0) + // return round(val); + + let p = 10 ** dec; + let n = (val * p) * (1 + Number.EPSILON); + return round(n) / p; + } + + 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; + const isInt = Number.isInteger; + const isUndef = v => v === void 0; + + 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'; + } + + const TypedArray = Object.getPrototypeOf(Uint8Array); + + 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 (o instanceof TypedArray) // also (ArrayBuffer.isView(o) && !(o instanceof DataView)) + 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) { + if (allHeadersSame(tables)) { + // console.log('cheap join!'); + + let table = tables[0].slice(); + + for (let i = 1; i < tables.length; i++) + table.push(...tables[i].slice(1)); + + if (!isAsc(table[0])) + table = sortCols(table); + + return table; + } + + 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; + + // TODO: https://github.com/dy/sort-ids (~2x faster for 1e5+ arrays) + function sortCols(table) { + let head = table[0]; + let rlen = head.length; + + let idxs = Array(rlen); + for (let i = 0; i < idxs.length; i++) + idxs[i] = i; + + idxs.sort((i0, i1) => head[i0] - head[i1]); + + let table2 = []; + for (let i = 0; i < table.length; i++) { + let row = table[i]; + let row2 = Array(rlen); + + for (let j = 0; j < rlen; j++) + row2[j] = row[idxs[j]]; + + table2.push(row2); + } + + return table2; + } + + // test if we can do cheap join (all join fields same) + function allHeadersSame(tables) { + let vals0 = tables[0][0]; + let len0 = vals0.length; + + for (let i = 1; i < tables.length; i++) { + let vals1 = tables[i][0]; + + if (vals1.length != len0) + return false; + + if (vals1 != vals0) { + for (let j = 0; j < len0; j++) { + if (vals1[j] != vals0[j]) + return false; + } + } + } + + return true; + } + + function isAsc(vals, samples = 100) { + const len = vals.length; + + // empty or single value + if (len <= 1) + return true; + + // skip leading & trailing nullish + let firstIdx = 0; + let lastIdx = len - 1; + + while (firstIdx <= lastIdx && vals[firstIdx] == null) + firstIdx++; + + while (lastIdx >= firstIdx && vals[lastIdx] == null) + lastIdx--; + + // all nullish or one value surrounded by nullish + if (lastIdx <= firstIdx) + return true; + + const stride = max(1, floor((lastIdx - firstIdx + 1) / samples)); + + for (let prevVal = vals[firstIdx], i = firstIdx + stride; i <= lastIdx; i += stride) { + const v = vals[i]; + + if (v != null) { + if (v <= prevVal) + return false; + + prevVal = v; + } + } + + return true; + } + + 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, seriesIdx, dataIdx) => dataIdx == null ? LEGEND_DISP : 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, + mount: noop, + 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 sp.size; + } + + const moveTuple = [0,0]; + + function cursorMove(self, mouseLeft1, mouseTop1) { + moveTuple[0] = mouseLeft1; + moveTuple[1] = mouseTop1; + return moveTuple; + } + + function filtBtn0(self, targ, handle, onlyTarg = true) { + return e => { + e.button == 0 && (!onlyTarg || e.target == targ) && handle(e); + }; + } + + function filtTarg(self, targ, handle, onlyTarg = true) { + return e => { + (!onlyTarg || e.target == targ) && handle(e); + }; + } + + 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, // legend clicks, not .u-over clicks + dblclick: filtBtn0, + + mousemove: filtTarg, + mouseleave: filtTarg, + mouseenter: filtTarg, + }, + + drag: { + setScale: true, + x: true, + y: false, + dist: 0, + uni: null, + click: (self, e) => { + // e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + }, + _x: false, + _y: false, + }, + + focus: { + dist: (self, seriesIdx, dataIdx, valPos, curPos) => valPos - curPos, + prox: -1, + bias: 0, + }, + + hover: { + skip: [void 0], + prox: null, + bias: 0, + }, + + left: -10, + top: -10, + idx: null, + dataIdx: null, + idxs: null, + + event: null, + }; + + const axisLines = { + show: true, + stroke: "rgba(0,0,0,0.07)", + width: 2, + // dash: [], + }; + + const grid = assign({}, axisLines, { + filter: retArg1, + }); + + const ticks = assign({}, grid, { + size: 10, + }); + + const border = assign({}, axisLines, { + show: false, + }); + + 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 lineGap = 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, + border, + font, + lineGap, + 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 (logBase == 10 && exp < 0) + foundIncr = roundDec(foundIncr, -exp); + + let split = scaleMin; + + do { + splits.push(split); + split = split + foundIncr; + + if (logBase == 10) + split = roundDec(split, 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/; + + const _filt = (splits, distr, re, keepMod) => splits.map((v, i) => ((distr == 4 && v == 0) || i % keepMod == 0 && re.test(v.toExponential()[v < 0 ? 1 : 0])) ? v : null); + + function log10AxisValsFilt(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 + ); + + if (re == RE_1) { + let magSpace = abs(valToPos(1, scaleKey) - _10); + + if (magSpace < minSpace) + return _filt(splits.slice().reverse(), sc.distr, re, ceil(minSpace / magSpace)).reverse(); // max->min skip + } + + return _filt(splits, sc.distr, re, 1); + } + + function log2AxisValsFilt(self, splits, axisIdx, foundSpace, foundIncr) { + let axis = self.axes[axisIdx]; + let scaleKey = axis.scale; + let minSpace = axis._space; + let valToPos = self.valToPos; + + let magSpace = abs(valToPos(1, scaleKey) - valToPos(2, scaleKey)); + + if (magSpace < minSpace) + return _filt(splits.slice().reverse(), 3, RE_ALL, ceil(minSpace / magSpace)).reverse(); // max->min skip + + return splits; + } + + function numSeriesVal(self, val, seriesIdx, dataIdx) { + return dataIdx == null ? LEGEND_DISP : 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, + border, + font, + lineGap, + 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; + } + + const facet = { + scale: null, + auto: true, + sorted: 0, + + // internal caches + min: inf, + max: -inf, + }; + + const gaps = (self, seriesIdx, idx0, idx1, nullGaps) => nullGaps; + + const xySeriesOpts = { + show: true, + auto: true, + sorted: 0, + gaps, + alpha: 1, + facets: [ + assign({}, facet, {scale: 'x'}), + assign({}, facet, {scale: 'y'}), + ], + }; + + const ySeriesOpts = { + scale: "y", + auto: true, + sorted: 0, + show: true, + spanGaps: false, + gaps, + 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 mode = u.mode; + const series = u.series[seriesIdx]; + const data = mode == 2 ? u._data[seriesIdx] : u._data; + const scales = u.scales; + const bbox = u.bbox; + + let dx = data[0], + dy = mode == 2 ? data[1] : data[seriesIdx], + sx = mode == 2 ? scales[series.facets[0].scale] : scales[u.series[0].scale], + sy = 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, + ) + ); + } + + function bandFillClipDirs(self, seriesIdx) { + let fillDir = 0; + + // 2 bits, -1 | 1 + let clipDirs = 0; + + let bands = ifNull(self.bands, EMPTY_ARR); + + for (let i = 0; i < bands.length; i++) { + let b = bands[i]; + + // is a "from" band edge + if (b.series[0] == seriesIdx) + fillDir = b.dir; + // is a "to" band edge + else if (b.series[1] == seriesIdx) { + if (b.dir == 1) + clipDirs |= 1; + else + clipDirs |= 2; + } + } + + return [ + fillDir, + ( + clipDirs == 1 ? -1 : // neg only + clipDirs == 2 ? 1 : // pos only + clipDirs == 3 ? 2 : // both + 0 // neither + ) + ]; + } + + function seriesFillTo(self, seriesIdx, dataMin, dataMax, bandFillDir) { + let mode = self.mode; + let series = self.series[seriesIdx]; + let scaleKey = mode == 2 ? series.facets[1].scale : series.scale; + let scale = self.scales[scaleKey]; + + return ( + bandFillDir == -1 ? scale.min : + bandFillDir == 1 ? scale.max : + scale.distr == 3 ? ( + scale.dir == 1 ? scale.min : + scale.max + ) : 0 + ); + } + + // creates inverted band clip path (from stroke path -> yMax || yMin) + // clipDir is always inverse of fillDir + // default clip dir is upwards (1), since default band fill is downwards/fillBelowTo (-1) (highIdx -> lowIdx) + function clipBandLine(self, seriesIdx, idx0, idx1, strokePath, clipDir) { + 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 or lower y limit + let yLimit = pxRound(valToPosY(clipDir == 1 ? scaleY.max : scaleY.min, 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; + + // hack to ensure we expand the clip enough to avoid cutting off strokes at edges + let maxStrokeWidth = 10; + + w > 0 && rect(clip, prevGapEnd, plotTop - maxStrokeWidth / 2, w, plotTop + plotHgt + maxStrokeWidth); + } + + 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 findGaps(xs, ys, idx0, idx1, dir, pixelForX, align) { + let gaps = []; + let len = xs.length; + + for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) { + let yVal = ys[i]; + + if (yVal === null) { + let fr = i, to = i; + + if (dir == 1) { + while (++i <= idx1 && ys[i] === null) + to = i; + } + else { + while (--i >= idx0 && ys[i] === null) + to = i; + } + + let frPx = pixelForX(xs[fr]); + let toPx = to == fr ? frPx : pixelForX(xs[to]); + + // if value adjacent to edge null is same pixel, then it's partially + // filled and gap should start at next pixel + let fri2 = fr - dir; + let frPx2 = align <= 0 && fri2 >= 0 && fri2 < len ? pixelForX(xs[fri2]) : frPx; + // if (frPx2 == frPx) + // frPx++; + // else + frPx = frPx2; + + let toi2 = to + dir; + let toPx2 = align >= 0 && toi2 >= 0 && toi2 < len ? pixelForX(xs[toi2]) : toPx; + // if (toPx2 == toPx) + // toPx--; + // else + toPx = toPx2; + + if (toPx >= frPx) + gaps.push([frPx, toPx]); // addGap + } + } + + return gaps; + } + + 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); }; + + // TODO (pending better browser support): https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/roundRect + return (p, x, y, w, h, endRad = 0, baseRad = 0) => { + if (endRad == 0 && baseRad == 0) + rect(p, x, y, w, h); + else { + endRad = min(endRad, w / 2, h / 2); + baseRad = min(baseRad, 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 + endRad, y); + arcTo(p, x + w, y, x + w, y + h, endRad); + arcTo(p, x + w, y + h, x, y + h, baseRad); + arcTo(p, x, y + h, x, y, baseRad); + arcTo(p, x, y, x + w, y, endRad); + 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(opts) { + const alignGaps = ifNull(opts?.alignGaps, 0); + + 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 pixelForX = val => pxRound(valToPosX(val, scaleX, xDim, xOff)); + let pixelForY = val => pxRound(valToPosY(val, scaleY, yDim, yOff)); + + 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, drawnAtX; + + let accX = pixelForX(dataX[dir == 1 ? idx0 : idx1]); + + // data edges + let lftIdx = nonNullIdx(dataY, idx0, idx1, 1 * dir); + let rgtIdx = nonNullIdx(dataY, idx0, idx1, -1 * dir); + let lftX = pixelForX(dataX[lftIdx]); + let rgtX = pixelForX(dataX[rgtIdx]); + + let hasGap = false; + + for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) { + let x = pixelForX(dataX[i]); + let yVal = dataY[i]; + + if (x == accX) { + if (yVal != null) { + outY = pixelForY(yVal); + + if (minY == inf) { + lineTo(stroke, x, outY); + inY = outY; + } + + minY = min(outY, minY); + maxY = max(outY, maxY); + } + else { + if (yVal === null) + hasGap = true; + } + } + else { + if (minY != inf) { + drawAcc(stroke, accX, minY, maxY, inY, outY); + drawnAtX = accX; + } + + if (yVal != null) { + outY = pixelForY(yVal); + lineTo(stroke, x, outY); + minY = maxY = inY = outY; + } + else { + minY = inf; + maxY = -inf; + + if (yVal === null) + hasGap = true; + } + + accX = x; + } + } + + if (minY != inf && minY != maxY && drawnAtX != accX) + drawAcc(stroke, accX, minY, maxY, inY, outY); + + let [ bandFillDir, bandClipDir ] = bandFillClipDirs(u, seriesIdx); + + if (series.fill != null || bandFillDir != 0) { + let fill = _paths.fill = new Path2D(stroke); + + let fillToVal = series.fillTo(u, seriesIdx, series.min, series.max, bandFillDir); + let fillToY = pixelForY(fillToVal); + + lineTo(fill, rgtX, fillToY); + lineTo(fill, lftX, fillToY); + } + + if (!series.spanGaps) { + // console.time('gaps'); + let gaps = []; + + hasGap && gaps.push(...findGaps(dataX, dataY, idx0, idx1, dir, pixelForX, alignGaps)); + + // console.timeEnd('gaps'); + + // console.log('gaps', JSON.stringify(gaps)); + + _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps); + + _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim); + } + + if (bandClipDir != 0) { + _paths.band = bandClipDir == 2 ? [ + clipBandLine(u, seriesIdx, idx0, idx1, stroke, -1), + clipBandLine(u, seriesIdx, idx0, idx1, stroke, 1), + ] : clipBandLine(u, seriesIdx, idx0, idx1, stroke, bandClipDir); + } + + return _paths; + }); + }; + } + + // BUG: align: -1 behaves like align: 1 when scale.dir: -1 + function stepped(opts) { + const align = ifNull(opts.align, 1); + // whether to draw ascenders/descenders at null/gap bondaries + const ascDesc = ifNull(opts.ascDesc, false); + const alignGaps = ifNull(opts.alignGaps, 0); + const extend = ifNull(opts.extend, 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 { left, width } = u.bbox; + + let pixelForX = val => pxRound(valToPosX(val, scaleX, xDim, xOff)); + let pixelForY = val => pxRound(valToPosY(val, scaleY, yDim, yOff)); + + 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 = scaleX.dir * (scaleX.ori == 0 ? 1 : -1); + + idx0 = nonNullIdx(dataY, idx0, idx1, 1); + idx1 = nonNullIdx(dataY, idx0, idx1, -1); + + let prevYPos = pixelForY(dataY[dir == 1 ? idx0 : idx1]); + let firstXPos = pixelForX(dataX[dir == 1 ? idx0 : idx1]); + let prevXPos = firstXPos; + + let firstXPosExt = firstXPos; + + if (extend && align == -1) { + firstXPosExt = left; + lineTo(stroke, firstXPosExt, prevYPos); + } + + lineTo(stroke, firstXPos, prevYPos); + + for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) { + let yVal1 = dataY[i]; + + if (yVal1 == null) + continue; + + let x1 = pixelForX(dataX[i]); + let y1 = pixelForY(yVal1); + + if (align == 1) + lineTo(stroke, x1, prevYPos); + else + lineTo(stroke, prevXPos, y1); + + lineTo(stroke, x1, y1); + + prevYPos = y1; + prevXPos = x1; + } + + let prevXPosExt = prevXPos; + + if (extend && align == 1) { + prevXPosExt = left + width; + lineTo(stroke, prevXPosExt, prevYPos); + } + + let [ bandFillDir, bandClipDir ] = bandFillClipDirs(u, seriesIdx); + + if (series.fill != null || bandFillDir != 0) { + let fill = _paths.fill = new Path2D(stroke); + + let fillTo = series.fillTo(u, seriesIdx, series.min, series.max, bandFillDir); + let fillToY = pixelForY(fillTo); + + lineTo(fill, prevXPosExt, fillToY); + lineTo(fill, firstXPosExt, fillToY); + } + + if (!series.spanGaps) { + // console.time('gaps'); + let gaps = []; + + gaps.push(...findGaps(dataX, dataY, idx0, idx1, dir, pixelForX, alignGaps)); + + // console.timeEnd('gaps'); + + // console.log('gaps', JSON.stringify(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; + }); + + _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps); + + _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim); + } + + if (bandClipDir != 0) { + _paths.band = bandClipDir == 2 ? [ + clipBandLine(u, seriesIdx, idx0, idx1, stroke, -1), + clipBandLine(u, seriesIdx, idx0, idx1, stroke, 1), + ] : clipBandLine(u, seriesIdx, idx0, idx1, stroke, bandClipDir); + } + + return _paths; + }); + }; + } + + function findColWidth(dataX, dataY, valToPosX, scaleX, xDim, xOff, colWid = inf) { + 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; + } + } + } + + return colWid; + } + + 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); + + let ro = opts.radius; + + ro = + // [valueRadius, baselineRadius] + ro == null ? [0, 0] : + typeof ro == 'number' ? [ro, 0] : ro; + + const radiusFn = fnOrSelf(ro); + + const gapFactor = 1 - size[0]; + const _maxWidth = ifNull(size[1], inf); + const _minWidth = ifNull(size[2], 1); + + 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; + let _align = align; + + let extraGap = _extraGap * pxRatio; + let maxWidth = _maxWidth * pxRatio; + let minWidth = _minWidth * pxRatio; + + let valRadius, baseRadius; + + if (scaleX.ori == 0) + [valRadius, baseRadius] = radiusFn(u, seriesIdx); + else + [baseRadius, valRadius] = radiusFn(u, seriesIdx); + + 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); + }; + + // band where this series is the "from" edge + let band = ifNull(u.bands, EMPTY_ARR).find(b => b.series[0] == seriesIdx); + + let fillDir = band != null ? band.dir : 0; + let fillTo = series.fillTo(u, seriesIdx, series.min, series.max, fillDir); + let fillToY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff)); + + // barWid is to center of stroke + let xShift, barWid, fullGap, colWid = xDim; + + let strokeWidth = pxRound(series.width * pxRatio); + + let multiPath = false; + + let fillColors = null; + let fillPaths = null; + let strokeColors = null; + let strokePaths = null; + + if (dispFills != null && (strokeWidth == 0 || 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()); + }); + + if (strokeWidth > 0) { + 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) { + _align = 1; + 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) + + colWid = findColWidth(dataX, dataY, valToPosX, scaleX, xDim, xOff, colWid); + + let gapWid = colWid - barWid; + fullGap = gapWid + extraGap; + } + else { + colWid = findColWidth(dataX, dataY, valToPosX, scaleX, xDim, xOff, colWid); + + let gapWid = colWid * gapFactor; + + fullGap = gapWid + extraGap; + barWid = colWid - fullGap; + } + + if (fullGap < 1) + fullGap = 0; + + if (strokeWidth >= barWid / 2) + strokeWidth = 0; + + // for small gaps, disable pixel snapping since gap inconsistencies become noticible and annoying + if (fullGap < 5) + pxRound = retArg0; + + let insetStroke = fullGap > 0; + + let rawBarWid = colWid - fullGap - (insetStroke ? strokeWidth : 0); + + barWid = pxRound(clamp(rawBarWid, minWidth, maxWidth)); + + xShift = (_align == 0 ? barWid / 2 : _align == _dirX ? 0 : barWid) - _align * _dirX * ((_align == 0 ? extraGap / 2 : 0) + (insetStroke ? strokeWidth / 2 : 0)); + + + const _paths = {stroke: null, fill: null, clip: null, band: null, gaps: null, flags: 0}; // disp, geom + + const stroke = multiPath ? null : new Path2D(); + + let dataY0 = null; + + if (band != null) + dataY0 = u.data[band.series[1]]; + else { + let { y0, y1 } = disp; + + if (y0 != null && y1 != null) { + dataY = y1.values(u, seriesIdx, idx0, idx1); + dataY0 = y0.values(u, seriesIdx, idx0, idx1); + } + } + + let radVal = valRadius * barWid; + let radBase = baseRadius * barWid; + + for (let i = _dirX == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dirX) { + let yVal = dataY[i]; + + if (yVal == null) + continue; + + if (dataY0 != null) { + let yVal0 = dataY0[i] ?? 0; + + if (yVal - yVal0 == 0) + continue; + + fillToY = valToPosY(yVal0, scaleY, yDim, yOff); + } + + 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, fillTo), scaleY, yDim, yOff); + + let lft = pxRound(xPos - xShift); + let btm = pxRound(max(yPos, fillToY)); + let top = pxRound(min(yPos, fillToY)); + // this includes the stroke + let barHgt = btm - top; + + if (yVal != null) { // && yVal != fillTo (0 height bar) + let rv = yVal < 0 ? radBase : radVal; + let rb = yVal < 0 ? radVal : radBase; + + if (multiPath) { + if (strokeWidth > 0 && strokeColors[i] != null) + rect(strokePaths.get(strokeColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), rv, rb); + + if (fillColors[i] != null) + rect(fillPaths.get(fillColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), rv, rb); + } + else + rect(stroke, lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), rv, rb); + + each(u, seriesIdx, i, + lft - strokeWidth / 2, + top, + barWid + strokeWidth, + barHgt, + ); + } + } + + if (strokeWidth > 0) + _paths.stroke = multiPath ? strokePaths : stroke; + else if (!multiPath) { + _paths._fill = series.width == 0 ? series._fill : series._stroke ?? series._fill; + _paths.width = 0; + } + + _paths.fill = multiPath ? fillPaths : stroke; + + return _paths; + }); + }; + } + + function splineInterp(interp, opts) { + const alignGaps = ifNull(opts?.alignGaps, 0); + + 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 pixelForX = val => pxRound(valToPosX(val, scaleX, xDim, xOff)); + let pixelForY = val => pxRound(valToPosY(val, scaleY, yDim, yOff)); + + let moveTo, bezierCurveTo, lineTo; + + if (scaleX.ori == 0) { + moveTo = moveToH; + lineTo = lineToH; + bezierCurveTo = bezierCurveToH; + } + else { + moveTo = moveToV; + lineTo = lineToV; + bezierCurveTo = bezierCurveToV; + } + + const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1); + + idx0 = nonNullIdx(dataY, idx0, idx1, 1); + idx1 = nonNullIdx(dataY, idx0, idx1, -1); + + let firstXPos = pixelForX(dataX[dir == 1 ? idx0 : idx1]); + let prevXPos = firstXPos; + + let xCoords = []; + let yCoords = []; + + for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) { + let yVal = dataY[i]; + + if (yVal != null) { + let xVal = dataX[i]; + let xPos = pixelForX(xVal); + + xCoords.push(prevXPos = xPos); + yCoords.push(pixelForY(dataY[i])); + } + } + + 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; + + let [ bandFillDir, bandClipDir ] = bandFillClipDirs(u, seriesIdx); + + if (series.fill != null || bandFillDir != 0) { + let fill = _paths.fill = new Path2D(stroke); + + let fillTo = series.fillTo(u, seriesIdx, series.min, series.max, bandFillDir); + let fillToY = pixelForY(fillTo); + + lineTo(fill, prevXPos, fillToY); + lineTo(fill, firstXPos, fillToY); + } + + if (!series.spanGaps) { + // console.time('gaps'); + let gaps = []; + + gaps.push(...findGaps(dataX, dataY, idx0, idx1, dir, pixelForX, alignGaps)); + + // console.timeEnd('gaps'); + + // console.log('gaps', JSON.stringify(gaps)); + + _paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps); + + _paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim); + } + + if (bandClipDir != 0) { + _paths.band = bandClipDir == 2 ? [ + clipBandLine(u, seriesIdx, idx0, idx1, stroke, -1), + clipBandLine(u, seriesIdx, idx0, idx1, stroke, 1), + ] : clipBandLine(u, seriesIdx, idx0, idx1, stroke, bandClipDir); + } + + 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, opts); + } + + // 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() { + for (let u of cursorPlots) + u.syncRect(true); + } + + if (domEnv) { + on(resize, win, invalidateRects); + on(scroll, win, invalidateRects, true); + on(dppxchange, win, () => { uPlot.pxRatio = pxRatio; }); + } + + 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); + + on("click", wrap, e => { + if (e.target === over) { + let didDrag = mouseLeft1 != mouseLeft0 || mouseTop1 != mouseTop0; + didDrag && drag.click(self, e); + } + }, true); + + 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); + b.dir = ifNull(b.dir, -1); + }); + + 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); + + sc.key = scaleKey; + + let isTime = sc.time; + + let rn = sc.range; + + let rangeIsArr = isArr(rn); + + if (scaleKey != xScaleKey || (mode == 2 && !isTime)) { + // 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 legendTable; + let legendHead; + let legendBody; + 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] = LEGEND_DISP; + } + + if (showLegend) { + legendTable = placeTag("table", LEGEND, root); + legendBody = placeTag("tbody", null, legendTable); + + // allows legend to be moved out of root + legend.mount(self, legendTable); + + if (multiValLegend) { + legendHead = placeTag("thead", null, legendTable, legendBody); + + let head = placeTag("tr", null, legendHead); + placeTag("th", null, head); + + for (var key in legendCols) + placeTag("th", LEGEND_LABEL, head).textContent = key; + } + else { + addClass(legendTable, LEGEND_INLINE); + legend.live && addClass(legendTable, 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, legendBody, legendBody.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; + + setCursorEvent(e); + + 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); + }, false); + + if (cursorFocus) { + onMouse(mouseenter, label, e => { + if (cursor._lock) + return; + + setCursorEvent(e); + + setSeries(series.indexOf(s), FOCUS_TRUE, true, syncOpts.setSeries); + }, false); + } + } + + 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, onlyTarg = true) { + const targListeners = mouseListeners.get(targ) || {}; + const listener = cursor.bind[ev](self, targ, fn, onlyTarg); + + 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; + + // previous values for diffing + let _plotLftCss = plotLftCss; + let _plotTopCss = plotTopCss; + let _plotWidCss = plotWidCss; + let _plotHgtCss = plotHgtCss; + + + 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 shouldSetSelect = 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; + + 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); + + if (cursor.dataIdx == null) { + let hov = cursor.hover; + + let skip = hov.skip = new Set(hov.skip ?? []); + skip.add(void 0); // alignment artifacts + let prox = hov.prox = fnOrSelf(hov.prox); + let bias = hov.bias ??= 0; + + // TODO: only scan between in-view idxs (i0, i1) + cursor.dataIdx = (self, seriesIdx, cursorIdx, valAtPosX) => { + if (seriesIdx == 0) + return cursorIdx; + + let idx2 = cursorIdx; + + let _prox = prox(self, seriesIdx, cursorIdx, valAtPosX) ?? inf; + let withProx = _prox >= 0 && _prox < inf; + let xDim = scaleX.ori == 0 ? plotWidCss : plotHgtCss; + let cursorLft = cursor.left; + + let xValues = data[0]; + let yValues = data[seriesIdx]; + + if (skip.has(yValues[cursorIdx])) { + idx2 = null; + + let nonNullLft = null, + nonNullRgt = null, + j; + + if (bias == 0 || bias == -1) { + j = cursorIdx; + while (nonNullLft == null && j-- > 0) { + if (!skip.has(yValues[j])) + nonNullLft = j; + } + } + + if (bias == 0 || bias == 1) { + j = cursorIdx; + while (nonNullRgt == null && j++ < yValues.length) { + if (!skip.has(yValues[j])) + nonNullRgt = j; + } + } + + if (nonNullLft != null || nonNullRgt != null) { + if (withProx) { + let lftPos = nonNullLft == null ? -Infinity : valToPosX(xValues[nonNullLft], scaleX, xDim, 0); + let rgtPos = nonNullRgt == null ? Infinity : valToPosX(xValues[nonNullRgt], scaleX, xDim, 0); + + let lftDelta = cursorLft - lftPos; + let rgtDelta = rgtPos - cursorLft; + + if (lftDelta <= rgtDelta) { + if (lftDelta <= _prox) + idx2 = nonNullLft; + } else { + if (rgtDelta <= _prox) + idx2 = nonNullRgt; + } + } + else { + idx2 = + nonNullRgt == null ? nonNullLft : + nonNullLft == null ? nonNullRgt : + cursorIdx - nonNullLft <= nonNullRgt - cursorIdx ? nonNullLft : nonNullRgt; + } + } + } + else if (withProx) { + let dist = abs(cursorLft - valToPosX(xValues[cursorIdx], scaleX, xDim, 0)); + + if (dist > _prox) + idx2 = null; + } + + return idx2; + }; + } + + const setCursorEvent = e => { cursor.event = e; }; + + 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]; + // position caches in CSS pixels + let cursorPtsLft = [null]; + let cursorPtsTop = [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(max(1, 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); + + if (pt != null) { + cursorPts.splice(i, 0, pt); + cursorPtsLft.splice(i, 0, 0); + cursorPtsTop.splice(i, 0, 0); + } + } + + fire("addSeries", i); + } + + function addSeries(opts, si) { + si = si == null ? series.length : si; + + opts = mode == 1 ? setDefault(opts, si, xSeriesOpts, ySeriesOpts) : setDefault(opts, si, null, xySeriesOpts); + + 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); + + if (cursorPts.length > 1) { + cursorPts.splice(i, 1)[0].remove(); + cursorPtsLft.splice(i, 1); + cursorPtsTop.splice(i, 1); + } + } + + // TODO: de-init no-longer-needed scales? + + fire("delSeries", i); + } + + self.delSeries = delSeries; + + const sidesWithAxes = [false, false, false, false]; + + function initAxis(axis, i) { + axis._show = axis.show; + + if (axis.show) { + let isVt = axis.side % 2; + + let sc = scales[axis.scale]; + + // this can occur if all series specify non-default scales + if (sc == null) { + axis.scale = isVt ? series[1].scale : xScaleKey; + sc = scales[axis.scale]; + } + + // also set defaults for incrs & values based on axis distr + let isTime = sc.time; + + axis.size = fnOrSelf(axis.size); + axis.space = fnOrSelf(axis.space); + axis.rotate = fnOrSelf(axis.rotate); + + if (isArr(axis.incrs)) { + axis.incrs.forEach(incr => { + !fixedDec.has(incr) && fixedDec.set(incr, guessDec(incr)); + }); + } + + 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); + axis.border.stroke = fnOrSelf(axis.border.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 && sc.log == 10 ? log10AxisValsFilt : sc.distr == 3 && sc.log == 2 ? log2AxisValsFilt : 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) { + data = _data == null ? [] : _data; + + self.data = self._data = data; + + if (mode == 2) { + dataLen = 0; + for (let i = 1; i < series.length; i++) + dataLen += data[i][0].length; + } + else { + if (data.length == 0) + self.data = self._data = data = [[]]; + + data0 = data[0]; + dataLen = data0.length; + + let scaleData = data; + + if (xScaleDistr == 2) { + scaleData = data.slice(); + + let _data0 = scaleData[0] = Array(dataLen); + for (let i = 0; i < dataLen; i++) + _data0[i] = i; + } + + self._data = data = scaleData; + } + + resetYSeries(true); + + fire("setData"); + + // forces x axis tick values to re-generate when neither x scale nor y scale changes + // in ordinal mode, scale range is by index, so will not change if new data has same length, but tick values are from data + if (xScaleDistr == 2) { + shouldConvergeSize = true; + + /* or somewhat cheaper, and uglier: + if (ready) { + // logic extracted from axesCalc() + let i = 0; + let axis = axes[i]; + let _splits = axis._splits.map(i => data0[i]); + let [_incr, _space] = axis._found; + let incr = data0[_splits[1]] - data0[_splits[0]]; + axis._values = axis.values(self, axis.filter(self, _splits, i, _space, incr), i, _space, incr); + } + */ + } + + if (_resetScales !== false) { + let xsc = scaleX; + + if (xsc.auto(self, viaAutoScaleX)) + autoScaleX(); + else + _setScale(xScaleKey, xsc.min, xsc.max); + + shouldSetCursor = 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 (_min == _max) { + 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, width, dash, cap, fill, join) { + stroke ??= transparent; + 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, sorted = 0) { + if (data.length > 0 && 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, sorted)) : [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]); + } + } + + const AUTOSCALE = {min: null, max: null}; + + function setScales() { + // log("setScales()", arguments); + + // implicitly add auto scales, and unranged scales + for (let k in scales) { + let sc = scales[k]; + + if (pendScales[k] == null && + ( + // scales that have never been set (on init) + sc.min == null || + // or auto scales when the x scale was explicitly set + pendScales[xScaleKey] != null && sc.auto(self, viaAutoScaleX) + ) + ) { + pendScales[k] = AUTOSCALE; + } + } + + // implicitly add dependent scales + for (let k in scales) { + let sc = scales[k]; + + if (pendScales[k] == null && sc.from != null && pendScales[sc.from] != null) + pendScales[k] = AUTOSCALE; + } + + // explicitly setting the x-scale invalidates everything (acts as redraw) + if (pendScales[xScaleKey] != null) + resetYSeries(true); // TODO: only reset series on auto scales? + + let wipScales = {}; + + for (let k in pendScales) { + let psc = pendScales[k]; + + if (psc != null) { + let wsc = wipScales[k] = copy(scales[k], fastIsObj); + + if (psc.min != null) + assign(wsc, psc); + 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 psc = pendScales[k]; + + if (psc == null) + return; + + let wsc = wipScales[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]); + + // don't try to contract same or adjacent idxs + if (i1 - i0 > 1) { + // 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.sorted); + + 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]; + + let wscx = wipScales[xScaleKey]; + let wscy = wipScales[yScaleKey]; + + // null can happen when only x is zoomed, but y has static range and doesnt get auto-added to pending + wscx != null && accScale(wscx, pendScales[xScaleKey], xFacet, xData, xFacet.sorted); + wscy != null && accScale(wscy, pendScales[yScaleKey], yFacet, yData, yFacet.sorted); + + // 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 && cursor.left >= 0) + shouldSetCursor = shouldSetLegend = true; + } + + 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) { + cacheStrokeFill(i, false); + cacheStrokeFill(i, true); + + if (s._paths == null) { + if (ctxAlpha != s.alpha) + ctx.globalAlpha = ctxAlpha = s.alpha; + + let _idxs = mode == 2 ? [0, data[i][0].length - 1] : getOuterIdxs(data[i]); + s._paths = s.paths(self, i, _idxs[0], _idxs[1]); + + if (ctxAlpha != 1) + ctx.globalAlpha = ctxAlpha = 1; + } + } + }); + + series.forEach((s, i) => { + if (i > 0 && s.show) { + if (ctxAlpha != s.alpha) + ctx.globalAlpha = ctxAlpha = s.alpha; + + s._paths != null && drawPath(i, false); + + { + let _gaps = s._paths != null ? s._paths.gaps : null; + + let show = s.points.show(self, i, i0, i1, _gaps); + let idxs = s.points.filter(self, i, show, _gaps); + + 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 { + stroke, + fill, + clip: gapsClip, + flags, + + _stroke: strokeStyle = s._stroke, + _fill: fillStyle = s._fill, + _width: width = s.width, + } = s._paths; + + width = roundDec(width * pxRatio, 3); + + let boundsClip = null; + let offset = (width % 2) / 2; + + if (_points && fillStyle == null) + fillStyle = width > 0 ? "#fff" : strokeStyle; + + let _pxAlign = s.pxAlign == 1 && offset > 0; + + _pxAlign && ctx.translate(offset, offset); + + if (!_points) { + let lft = plotLft - width / 2, + top = plotTop - width / 2, + wid = plotWid + width, + hgt = plotHgt + width; + + 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 + flags != 0 && 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; + + if (isArr(bandClip)) + bandClip = b.dir == 1 ? bandClip[0] : bandClip[1]; + + 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 border = axis.border; + 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] * axis.lineGap; + + 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, + ); + } + + if (border.show) { + drawOrthoLines( + [basePos], + [1], + ori == 0 ? 1 : 0, + ori == 0 ? 1 : 2, + ori == 1 ? plotTop : plotLft, + ori == 1 ? plotHgt : plotWid, + roundDec(border.width * pxRatio, 3), + border.stroke(self, i), + border.dash, + border.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; + let deferHooks = false; + let hooksQueue = []; + + function flushHooks() { + deferHooks = false; + + for (let i = 0; i < hooksQueue.length; i++) + fire(...hooksQueue[i]); + + hooksQueue.length = 0; + } + + function commit() { + if (!queuedCommit) { + microTask(_commit); + queuedCommit = true; + } + } + + // manual batching (aka immediate mode), skips microtask queue + function batch(fn, _deferHooks = false) { + queuedCommit = true; + deferHooks = _deferHooks; + + fn(self); + _commit(); + + if (_deferHooks && hooksQueue.length > 0) + queueMicrotask(flushHooks); + } + + self.batch = batch; + + function _commit() { + // log("_commit()", arguments); + + if (shouldSetScales) { + setScales(); + shouldSetScales = false; + } + + if (shouldConvergeSize) { + convergeSize(); + shouldConvergeSize = false; + } + + if (shouldSetSize) { + setStylePx(under, LEFT, plotLftCss); + setStylePx(under, TOP, plotTopCss); + setStylePx(under, WIDTH, plotWidCss); + setStylePx(under, HEIGHT, plotHgtCss); + + setStylePx(over, LEFT, plotLftCss); + setStylePx(over, TOP, plotTopCss); + setStylePx(over, WIDTH, plotWidCss); + setStylePx(over, HEIGHT, plotHgtCss); + + setStylePx(wrap, WIDTH, fullWidCss); + setStylePx(wrap, HEIGHT, fullHgtCss); + + // NOTE: mutating this during print preview in Chrome forces transparent + // canvas pixels to white, even when followed up with clearRect() below + can.width = round(fullWidCss * pxRatio); + can.height = round(fullHgtCss * pxRatio); + + axes.forEach(({ _el, _show, _size, _pos, side }) => { + if (_el != null) { + if (_show) { + let posOffset = (side === 3 || side === 0 ? _size : 0); + let isVt = side % 2 == 1; + + setStylePx(_el, isVt ? "left" : "top", _pos - posOffset); + setStylePx(_el, isVt ? "width" : "height", _size); + setStylePx(_el, isVt ? "top" : "left", isVt ? plotTopCss : plotLftCss); + setStylePx(_el, isVt ? "height" : "width", isVt ? plotHgtCss : plotWidCss); + + remClass(_el, OFF); + } + else + addClass(_el, OFF); + } + }); + + // invalidate ctx style cache + ctxStroke = ctxFill = ctxWidth = ctxJoin = ctxCap = ctxFont = ctxAlign = ctxBaseline = ctxDash = null; + ctxAlpha = 1; + + syncRect(true); + + if ( + plotLftCss != _plotLftCss || + plotTopCss != _plotTopCss || + plotWidCss != _plotWidCss || + plotHgtCss != _plotHgtCss + ) { + resetYSeries(false); + + let pctWid = plotWidCss / _plotWidCss; + let pctHgt = plotHgtCss / _plotHgtCss; + + if (cursor.show && !shouldSetCursor && cursor.left >= 0) { + cursor.left *= pctWid; + cursor.top *= pctHgt; + + vCursor && elTrans(vCursor, round(cursor.left), 0, plotWidCss, plotHgtCss); + hCursor && elTrans(hCursor, 0, round(cursor.top), plotWidCss, plotHgtCss); + + for (let i = 1; i < cursorPts.length; i++) { + cursorPtsLft[i] *= pctWid; + cursorPtsTop[i] *= pctHgt; + elTrans(cursorPts[i], incrRoundUp(cursorPtsLft[i], 1), incrRoundUp(cursorPtsTop[i], 1), plotWidCss, plotHgtCss); + } + } + + if (select.show && !shouldSetSelect && select.left >= 0 && select.width > 0) { + select.left *= pctWid; + select.width *= pctWid; + select.top *= pctHgt; + select.height *= pctHgt; + + for (let prop in _hideProps) + setStylePx(selectDiv, prop, select[prop]); + } + + _plotLftCss = plotLftCss; + _plotTopCss = plotTopCss; + _plotWidCss = plotWidCss; + _plotHgtCss = plotHgtCss; + } + + 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 (select.show && shouldSetSelect) { + setSelect(select); + shouldSetSelect = false; + } + + if (cursor.show && shouldSetCursor) { + updateCursor(null, true, false); + shouldSetCursor = false; + } + + if (legend.show && legend.live && shouldSetLegend) { + setLegend(); + shouldSetLegend = false; // redundant currently + } + + 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) { + select[prop] = opts[prop]; + + if (prop in _hideProps) + setStylePx(selectDiv, 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); + + if (opts.focus != null) + setFocus(i); + + if (opts.show != null) { + series.forEach((s, si) => { + if (si > 0 && (i == si || i == null)) { + s.show = opts.show; + toggleDOM(si, opts.show); + + if (mode == 2) { + _setScale(s.facets[0].scale, null, null); + _setScale(s.facets[1].scale, null, null); + } + else + _setScale(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); + opts.dir = ifNull(opts.dir, -1); + 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}; + + function setFocus(i) { + if (i != focusedSeries) { + // log("setFocus()", arguments); + + let allFocused = i == null; + + let _setAlpha = focus.alpha != 1; + + series.forEach((s, i2) => { + if (mode == 1 || i2 > 0) { + 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) { + onMouse(mouseleave, legendTable, e => { + if (cursor._lock) + return; + + setCursorEvent(e); + + if (focusedSeries != null) + setSeries(null, FOCUS_TRUE, true, syncOpts.setSeries); + }); + } + + 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, + ) + ); + + 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) { + if (opts.idxs) { + opts.idxs.forEach((didx, sidx) => { + activeIdxs[sidx] = didx; + }); + } + else if (!isUndef(opts.idx)) + activeIdxs.fill(opts.idx); + + legend.idx = activeIdxs[0]; + } + + for (let sidx = 0; sidx < series.length; sidx++) { + if (sidx > 0 || mode == 1 && !multiValLegend) + setLegendValues(sidx, activeIdxs[sidx]); + } + + if (showLegend && legend.live) + syncLegend(); + + shouldSetLegend = false; + + _fire !== false && fire("setLegend"); + } + + self.setLegend = setLegend; + + function setLegendValues(sidx, idx) { + let s = series[sidx]; + let src = sidx == 0 && xScaleDistr == 2 ? data0 : data[sidx]; + let val; + + if (multiValLegend) + val = s.values(self, sidx, idx) ?? NULL_LEGEND_VALUES; + else { + val = s.value(self, idx == null ? null : src[idx], sidx, idx); + val = val == null ? NULL_LEGEND_VALUES : {_: val}; + } + + 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); + + cursor.left = mouseLeft1; + cursor.top = 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 = cursor.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(idx); + shouldSetLegend = true; + } + } + 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 = cursor.idx = closestIdx(valAtPosX, data[0], i0, i1); + xPos = valToPosX(data[0][idx], scaleX, xDim, 0); + } + + for (let i = mode == 2 ? 1 : 0; i < series.length; i++) { + let s = series[i]; + + let idx1 = activeIdxs[i]; + let yVal1 = idx1 == null ? null : (mode == 1 ? data[i][idx1] : data[i][1][idx1]); + + let idx2 = cursor.dataIdx(self, i, idx, valAtPosX); + let yVal2 = idx2 == null ? null : (mode == 1 ? data[i][idx2] : data[i][1][idx2]); + + shouldSetLegend = shouldSetLegend || yVal2 != yVal1 || idx2 != idx1; + + activeIdxs[i] = idx2; + + let xPos2 = idx2 == idx ? xPos : valToPosX(mode == 1 ? data[0][idx2] : data[i][0][idx2], scaleX, xDim, 0); + + if (i > 0 && s.show) { + // this doesnt really work for state timeline, heatmap, status history (where the value maps to color, not y coords) + let yPos = yVal2 == null ? -10 : valToPosY(yVal2, mode == 1 ? scales[s.scale] : scales[s.facets[1].scale], yDim, 0); + + if (cursorFocus && yVal2 != null) { + let mouseYPos = scaleX.ori == 1 ? mouseLeft1 : mouseTop1; + let dist = abs(focus.dist(self, i, idx2, yPos, mouseYPos)); + + if (dist < closestDist) { + let bias = focus.bias; + + if (bias != 0) { + let mouseYVal = posToVal(mouseYPos, s.scale); + + let seriesYValSign = yVal2 >= 0 ? 1 : -1; + let mouseYValSign = mouseYVal >= 0 ? 1 : -1; + + // with a focus bias, we will never cross zero when prox testing + // it's either closest towards zero, or closest away from zero + if (mouseYValSign == seriesYValSign && ( + mouseYValSign == 1 ? + (bias == 1 ? yVal2 >= mouseYVal : yVal2 <= mouseYVal) : // >= 0 + (bias == 1 ? yVal2 <= mouseYVal : yVal2 >= mouseYVal) // < 0 + )) { + closestDist = dist; + closestSeries = i; + } + } + else { + 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); + + cursorPtsLft[i] = ptLft; + cursorPtsTop[i] = ptTop; + + elTrans(cursorPts[i], incrRoundUp(ptLft, 1), incrRoundUp(ptTop, 1), plotWidCss, plotHgtCss); + } + } + } + } + + // 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; + + if (dragX || dragY) { + 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 && dragX) { + if (sori == 0) { + sOff = left; + sDim = width; + } + else { + sOff = top; + sDim = height; + } + + 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 && dragY) { + if (sori == 1) { + sOff = left; + sDim = width; + } + else { + sOff = top; + sDim = height; + } + + 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); + } + else + hideSelect(); + } + 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); + } + } + } + + if (shouldSetLegend) { + legend.idx = idx; + setLegend(); + } + + _fire !== false && fire("setCursor"); + } + + let rect = null; + + Object.defineProperty(self, 'rect', { + get() { + if (rect == null) + syncRect(false); + + return rect; + }, + }); + + function syncRect(defer = false) { + if (defer) + rect = null; + else { + rect = over.getBoundingClientRect(); + fire("syncRect", rect); + } + } + + function mouseMove(e, src, _l, _t, _w, _h, _i) { + if (cursor._lock) + return; + + // Chrome on Windows has a bug which triggers a stray mousemove event after an initial mousedown event + // when clicking into a plot as part of re-focusing the browser window. + // we gotta ignore it to avoid triggering a phantom drag / setSelect + // However, on touch-only devices Chrome-based browsers trigger a 0-distance mousemove before mousedown + // so we don't ignore it when mousedown has set the dragging flag + if (dragging && e != null && e.movementX == 0 && e.movementY == 0) + 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); + + setCursorEvent(e); + + 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.axes[0].side % 2 == 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; + } + } + + const _hideProps = { + width: 0, + height: 0, + left: 0, + top: 0, + }; + + function hideSelect() { + setSelect(_hideProps, false); + } + + let downSelectLeft; + let downSelectTop; + let downSelectWidth; + let downSelectHeight; + + 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, false); + pubSync(mousedown, self, mouseLeft0, mouseTop0, plotWidCss, plotHgtCss, null); + } + + let { left, top, width, height } = select; + + downSelectLeft = left; + downSelectTop = top; + downSelectWidth = width; + downSelectHeight = height; + + hideSelect(); + } + + 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; + let chgSelect = ( + downSelectLeft != left || + downSelectTop != top || + downSelectWidth != width || + downSelectHeight != height + ); + + hasSelect && chgSelect && setSelect(select); + + if (drag.setScale && hasSelect && chgSelect) { + // 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) + return; + + setCursorEvent(e); + + 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) { + if (cursor._lock) + return; + + setCursorEvent(e); + + 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) => { + let seriesIdxMatcher = syncOpts.match[2]; + idx = seriesIdxMatcher(self, src, idx); + idx != -1 && setSeries(idx, opts, true, false); + }; + + if (cursor.show) { + onMouse(mousedown, over, mouseDown); + onMouse(mousemove, over, mouseMove); + onMouse(mouseenter, over, e => { + setCursorEvent(e); + syncRect(false); + }); + 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 (deferHooks) + hooksQueue.push([evName, a1, a2]); + else { + 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 seriesIdxMatcher = (self, src, srcSeriesIdx) => srcSeriesIdx; + + const syncOpts = assign({ + key: null, + setSeries: false, + filters: { + pub: retTrue, + sub: retTrue, + }, + scales: [xScaleKey, series[1] ? series[1].scale : null], + match: [retEq, retEq, seriesIdxMatcher], + values: [null, null], + }, cursor.sync); + + if (syncOpts.match.length == 2) + syncOpts.match.push(seriesIdxMatcher); + + 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(); + legendTable?.remove(); // in case mounted outside of root + 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(); + + shouldSetSelect = select.show && (select.width > 0 || select.height > 0); + shouldSetCursor = shouldSetLegend = true; + + _setSize(opts.width, opts.height); + } + + 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.pxRatio = pxRatio; + + { + 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/docs/dist/uPlot.iife.min.js b/docs/dist/uPlot.iife.min.js new file mode 100644 index 0000000..172c74e --- /dev/null +++ b/docs/dist/uPlot.iife.min.js @@ -0,0 +1,2 @@ +/*! https://github.com/leeoniya/uPlot (v1.6.30) */ +var uPlot=function(){"use strict";const l="u-off",e="u-label",t="width",n="height",i="top",o="bottom",s="left",r="right",u="#000",a=u+"0",f="mousemove",c="mousedown",h="mouseup",d="mouseenter",p="mouseleave",m="dblclick",g="change",x="dppxchange",w="--",_="undefined"!=typeof window,b=_?document:null,v=_?window:null,k=_?navigator:null;let y,M;function S(l,e){if(null!=e){let t=l.classList;!t.contains(e)&&t.add(e)}}function E(l,e){let t=l.classList;t.contains(e)&&t.remove(e)}function T(l,e,t){l.style[e]=t+"px"}function z(l,e,t,n){let i=b.createElement(l);return null!=e&&S(i,e),null!=t&&t.insertBefore(i,n),i}function D(l,e){return z("div",l,e)}const P=new WeakMap;function A(e,t,n,i,o){let s="translate("+t+"px,"+n+"px)";s!=P.get(e)&&(e.style.transform=s,P.set(e,s),0>t||0>n||t>i||n>o?S(e,l):E(e,l))}const W=new WeakMap;function Y(l,e,t){let n=e+t;n!=W.get(l)&&(W.set(l,n),l.style.background=e,l.style.borderColor=t)}const C=new WeakMap;function F(l,e,t,n){let i=e+""+t;i!=C.get(l)&&(C.set(l,i),l.style.height=t+"px",l.style.width=e+"px",l.style.marginLeft=n?-e/2+"px":0,l.style.marginTop=n?-t/2+"px":0)}const H={passive:!0},R={...H,capture:!0};function G(l,e,t,n){e.addEventListener(l,t,n?R:H)}function I(l,e,t,n){e.removeEventListener(l,t,n?R:H)}function L(l,e,t,n){let i;t=t||0;let o=2147483647>=(n=n||e.length-1);for(;n-t>1;)i=o?t+n>>1:tl((t+n)/2),l>e[i]?t=i:n=i;return l-e[t]>e[n]-l?n:t}function O(l,e,t,n){for(let i=1==n?e:t;i>=e&&t>=i;i+=n)if(null!=l[i])return i;return-1}function N(l,e,t,n){let i=ul(l),o=ul(e);l==e&&(-1==i?(l*=t,e/=t):(l/=t,e*=t));let s=10==t?al:fl,r=1==o?il:tl,u=(1==i?tl:il)(s(el(l))),a=r(s(el(e))),f=rl(t,u),c=rl(t,a);return 10==t&&(0>u&&(f=Sl(f,-u)),0>a&&(c=Sl(c,-a))),n||2==t?(l=f*i,e=c*o):(l=Ml(l,f),e=yl(e,c)),[l,e]}function j(l,e,t,n){let i=N(l,e,t,n);return 0==l&&(i[0]=0),0==e&&(i[1]=0),i}_&&function l(){let e=devicePixelRatio;y!=e&&(y=e,M&&I(g,M,l),M=matchMedia(`(min-resolution: ${y-.001}dppx) and (max-resolution: ${y+.001}dppx)`),G(g,M,l),v.dispatchEvent(new CustomEvent(x)))}();const U=.1,B={mode:3,pad:U},V={pad:0,soft:null,mode:0},J={min:V,max:V};function q(l,e,t,n){return Fl(t)?X(l,e,t):(V.pad=t,V.soft=n?0:null,V.mode=n?3:0,X(l,e,J))}function K(l,e){return null==l?e:l}function X(l,e,t){let n=t.min,i=t.max,o=K(n.pad,0),s=K(i.pad,0),r=K(n.hard,-hl),u=K(i.hard,hl),a=K(n.soft,hl),f=K(i.soft,-hl),c=K(n.mode,0),h=K(i.mode,0),d=e-l,p=al(d),m=sl(el(l),el(e)),g=al(m),x=el(g-p);(1e-9>d||x>10)&&(d=0,0!=l&&0!=e||(d=1e-9,2==c&&a!=hl&&(o=0),2==h&&f!=-hl&&(s=0)));let w=d||m||1e3,_=al(w),b=rl(10,tl(_)),v=Sl(Ml(l-w*(0==d?0==l?.1:1:o),b/10),9),k=a>l||1!=c&&(3!=c||v>a)&&(2!=c||a>v)?hl:a,y=sl(r,k>v&&l>=k?k:ol(k,v)),M=Sl(yl(e+w*(0==d?0==e?.1:1:s),b/10),9),S=e>f||1!=h&&(3!=h||f>M)&&(2!=h||M>f)?-hl:f,E=ol(u,M>S&&S>=e?S:sl(S,M));return y==E&&0==y&&(E=100),[y,E]}const Z=new Intl.NumberFormat(_?k.language:"en-US"),$=l=>Z.format(l),Q=Math,ll=Q.PI,el=Q.abs,tl=Q.floor,nl=Q.round,il=Q.ceil,ol=Q.min,sl=Q.max,rl=Q.pow,ul=Q.sign,al=Q.log10,fl=Q.log2,cl=(l,e=1)=>Q.asinh(l/e),hl=1/0;function dl(l){return 1+(0|al((l^l>>31)-(l>>31)))}function pl(l,e,t){return ol(sl(l,e),t)}function ml(l){return"function"==typeof l?l:()=>l}const gl=l=>l,xl=(l,e)=>e,wl=()=>null,_l=()=>!0,bl=(l,e)=>l==e,vl=l=>Sl(l,14);function kl(l,e){return vl(Sl(vl(l/e))*e)}function yl(l,e){return vl(il(vl(l/e))*e)}function Ml(l,e){return vl(tl(vl(l/e))*e)}function Sl(l,e=0){if(Yl(l))return l;let t=10**e;return nl(l*t*(1+Number.EPSILON))/t}const El=new Map;function Tl(l){return((""+l).split(".")[1]||"").length}function zl(l,e,t,n){let i=[],o=n.map(Tl);for(let s=e;t>s;s++){let e=el(s),t=Sl(rl(l,s),e);for(let l=0;n.length>l;l++){let r=n[l]*t,u=(0>r||0>s?e:0)+(o[l]>s?o[l]:0),a=Sl(r,u);i.push(a),El.set(a,u)}}return i}const Dl={},Pl=[],Al=[null,null],Wl=Array.isArray,Yl=Number.isInteger;function Cl(l){return"string"==typeof l}function Fl(l){let e=!1;if(null!=l){let t=l.constructor;e=null==t||t==Object}return e}function Hl(l){return null!=l&&"object"==typeof l}const Rl=Object.getPrototypeOf(Uint8Array);function Gl(l,e=Fl){let t;if(Wl(l)){let n=l.find((l=>null!=l));if(Wl(n)||e(n)){t=Array(l.length);for(let n=0;l.length>n;n++)t[n]=Gl(l[n],e)}else t=l.slice()}else if(l instanceof Rl)t=l.slice();else if(e(l)){t={};for(let n in l)t[n]=Gl(l[n],e)}else t=l;return t}function Il(l){let e=arguments;for(let t=1;e.length>t;t++){let n=e[t];for(let e in n)Fl(l[e])?Il(l[e],Gl(n[e])):l[e]=Gl(n[e])}return l}function Ll(l,e,t){for(let n,i=0,o=-1;e.length>i;i++){let s=e[i];if(s>o){for(n=s-1;n>=0&&null==l[n];)l[n--]=null;for(n=s+1;t>n&&null==l[n];)l[o=n++]=null}}}const Ol="undefined"==typeof queueMicrotask?l=>Promise.resolve().then(l):queueMicrotask,Nl=["January","February","March","April","May","June","July","August","September","October","November","December"],jl=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];function Ul(l){return l.slice(0,3)}const Bl=jl.map(Ul),Vl=Nl.map(Ul),Jl={MMMM:Nl,MMM:Vl,WWWW:jl,WWW:Bl};function ql(l){return(10>l?"0":"")+l}const Kl={YYYY:l=>l.getFullYear(),YY:l=>(l.getFullYear()+"").slice(2),MMMM:(l,e)=>e.MMMM[l.getMonth()],MMM:(l,e)=>e.MMM[l.getMonth()],MM:l=>ql(l.getMonth()+1),M:l=>l.getMonth()+1,DD:l=>ql(l.getDate()),D:l=>l.getDate(),WWWW:(l,e)=>e.WWWW[l.getDay()],WWW:(l,e)=>e.WWW[l.getDay()],HH:l=>ql(l.getHours()),H:l=>l.getHours(),h:l=>{let e=l.getHours();return 0==e?12:e>12?e-12:e},AA:l=>12>l.getHours()?"AM":"PM",aa:l=>12>l.getHours()?"am":"pm",a:l=>12>l.getHours()?"a":"p",mm:l=>ql(l.getMinutes()),m:l=>l.getMinutes(),ss:l=>ql(l.getSeconds()),s:l=>l.getSeconds(),fff:l=>function(l){return(10>l?"00":100>l?"0":"")+l}(l.getMilliseconds())};function Xl(l,e){e=e||Jl;let t,n=[],i=/\{([a-z]+)\}|[^{]+/gi;for(;t=i.exec(l);)n.push("{"==t[0][0]?Kl[t[1]]:t[0]);return l=>{let t="";for(let i=0;n.length>i;i++)t+="string"==typeof n[i]?n[i]:n[i](l,e);return t}}const Zl=(new Intl.DateTimeFormat).resolvedOptions().timeZone,$l=l=>l%1==0,Ql=[1,2,2.5,5],le=zl(10,-16,0,Ql),ee=zl(10,0,16,Ql),te=ee.filter($l),ne=le.concat(ee),ie="{YYYY}",oe="\n"+ie,se="{M}/{D}",re="\n"+se,ue=re+"/{YY}",ae="{aa}",fe="{h}:{mm}"+ae,ce="\n"+fe,he=":{ss}",de=null;function pe(l){let e=1e3*l,t=60*e,n=60*t,i=24*n,o=30*i,s=365*i;return[(1==l?zl(10,0,3,Ql).filter($l):zl(10,-3,0,Ql)).concat([e,5*e,10*e,15*e,30*e,t,5*t,10*t,15*t,30*t,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,ie,de,de,de,de,de,de,1],[28*i,"{MMM}",oe,de,de,de,de,de,1],[i,se,oe,de,de,de,de,de,1],[n,"{h}"+ae,ue,de,re,de,de,de,1],[t,fe,ue,de,re,de,de,de,1],[e,he,ue+" "+fe,de,re+" "+fe,de,ce,de,1],[l,he+".{fff}",ue+" "+fe,de,re+" "+fe,de,ce,de,1]],function(e){return(r,u,a,f,c,h)=>{let d=[],p=c>=s,m=c>=o&&s>c,g=e(a),x=Sl(g*l,3),w=ye(g.getFullYear(),p?0:g.getMonth(),m||p?1:g.getDate()),_=Sl(w*l,3);if(m||p){let t=m?c/o:0,n=p?c/s:0,i=x==_?x:Sl(ye(w.getFullYear()+n,w.getMonth()+t,1)*l,3),r=new Date(nl(i/l)),u=r.getFullYear(),a=r.getMonth();for(let o=0;f>=i;o++){let s=ye(u+n*o,a+t*o,1),r=s-e(Sl(s*l,3));i=Sl((+s+r)*l,3),i>f||d.push(i)}}else{let o=i>c?c:i,s=_+(tl(a)-tl(x))+yl(x-_,o);d.push(s);let p=e(s),m=p.getHours()+p.getMinutes()/t+p.getSeconds()/n,g=c/n,w=h/r.axes[u]._space;for(;s=Sl(s+c,1==l?0:3),f>=s;)if(g>1){let l=tl(Sl(m+g,6))%24,t=e(s).getHours()-l;t>1&&(t=-1),s-=t*n,m=(m+g)%24,.7>Sl((s-d[d.length-1])/c,3)*w||d.push(s)}else d.push(s)}return d}}]}const[me,ge,xe]=pe(1),[we,_e,be]=pe(.001);function ve(l,e){return l.map((l=>l.map(((t,n)=>0==n||8==n||null==t?t:e(1==n||0==l[8]?t:l[1]+t)))))}function ke(l,e){return(t,n,i,o,s)=>{let r,u,a,f,c,h,d=e.find((l=>s>=l[0]))||e[e.length-1];return n.map((e=>{let t=l(e),n=t.getFullYear(),i=t.getMonth(),o=t.getDate(),s=t.getHours(),p=t.getMinutes(),m=t.getSeconds(),g=n!=r&&d[2]||i!=u&&d[3]||o!=a&&d[4]||s!=f&&d[5]||p!=c&&d[6]||m!=h&&d[7]||d[1];return r=n,u=i,a=o,f=s,c=p,h=m,g(t)}))}}function ye(l,e,t){return new Date(l,e,t)}function Me(l,e){return e(l)}function Se(l,e){return(t,n,i,o)=>null==o?w:e(l(n))}zl(2,-53,53,[1]);const Ee={show:!0,live:!0,isolate:!1,mount:()=>{},markers:{show:!0,width:2,stroke:function(l,e){let t=l.series[e];return t.width?t.stroke(l,e):t.points.width?t.points.stroke(l,e):null},fill:function(l,e){return l.series[e].fill(l,e)},dash:"solid"},idx:null,idxs:null,values:[]},Te=[0,0];function ze(l,e,t,n=!0){return l=>{0==l.button&&(!n||l.target==e)&&t(l)}}function De(l,e,t,n=!0){return l=>{(!n||l.target==e)&&t(l)}}const Pe={show:!0,x:!0,y:!0,lock:!1,move:function(l,e,t){return Te[0]=e,Te[1]=t,Te},points:{show:function(l,e){let i=l.cursor.points,o=D(),s=i.size(l,e);T(o,t,s),T(o,n,s);let r=s/-2;T(o,"marginLeft",r),T(o,"marginTop",r);let u=i.width(l,e,s);return u&&T(o,"borderWidth",u),o},size:function(l,e){return l.series[e].points.size},width:0,stroke:function(l,e){let t=l.series[e].points;return t._stroke||t._fill},fill:function(l,e){let t=l.series[e].points;return t._fill||t._stroke}},bind:{mousedown:ze,mouseup:ze,click:ze,dblclick:ze,mousemove:De,mouseleave:De,mouseenter:De},drag:{setScale:!0,x:!0,y:!1,dist:0,uni:null,click:(l,e)=>{e.stopPropagation(),e.stopImmediatePropagation()},_x:!1,_y:!1},focus:{dist:(l,e,t,n,i)=>n-i,prox:-1,bias:0},hover:{skip:[void 0],prox:null,bias:0},left:-10,top:-10,idx:null,dataIdx:null,idxs:null,event:null},Ae={show:!0,stroke:"rgba(0,0,0,0.07)",width:2},We=Il({},Ae,{filter:xl}),Ye=Il({},We,{size:10}),Ce=Il({},Ae,{show:!1}),Fe='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"',He="bold "+Fe,Re={show:!0,scale:"x",stroke:u,space:50,gap:5,size:50,labelGap:0,labelSize:30,labelFont:He,side:2,grid:We,ticks:Ye,border:Ce,font:Fe,lineGap:1.5,rotate:0},Ge={show:!0,scale:"x",auto:!1,sorted:1,min:hl,max:-hl,idxs:[]};function Ie(l,e){return e.map((l=>null==l?"":$(l)))}function Le(l,e,t,n,i,o,s){let r=[],u=El.get(i)||0;for(let l=t=s?t:Sl(yl(t,i),u);n>=l;l=Sl(l+i,u))r.push(Object.is(l,-0)?0:l);return r}function Oe(l,e,t,n,i){const o=[],s=l.scales[l.axes[e].scale].log,r=tl((10==s?al:fl)(t));i=rl(s,r),10==s&&0>r&&(i=Sl(i,-r));let u=t;do{o.push(u),u+=i,10==s&&(u=Sl(u,El.get(i))),i*s>u||(i=u)}while(n>=u);return o}function Ne(l,e,t,n,i){let o=l.scales[l.axes[e].scale].asinh,s=n>o?Oe(l,e,sl(o,t),n,i):[o],r=0>n||t>0?[]:[0];return(-o>t?Oe(l,e,sl(o,-n),-t,i):[o]).reverse().map((l=>-l)).concat(r,s)}const je=/./,Ue=/[12357]/,Be=/[125]/,Ve=/1/,Je=(l,e,t,n)=>l.map(((l,i)=>4==e&&0==l||i%n==0&&t.test(l.toExponential()[0>l?1:0])?l:null));function qe(l,e,t){let n=l.axes[t],i=n.scale,o=l.scales[i],s=l.valToPos,r=n._space,u=s(10,i),a=s(9,i)-u<r?s(7,i)-u<r?s(5,i)-u<r?Ve:Be:Ue:je;if(a==Ve){let l=el(s(1,i)-u);if(r>l)return Je(e.slice().reverse(),o.distr,a,il(r/l)).reverse()}return Je(e,o.distr,a,1)}function Ke(l,e,t){let n=l.axes[t],i=n.scale,o=n._space,s=l.valToPos,r=el(s(1,i)-s(2,i));return o>r?Je(e.slice().reverse(),3,je,il(o/r)).reverse():e}function Xe(l,e,t,n){return null==n?w:null==e?"":$(e)}const Ze={show:!0,scale:"y",stroke:u,space:30,gap:5,size:50,labelGap:0,labelSize:30,labelFont:He,side:3,grid:We,ticks:Ye,border:Ce,font:Fe,lineGap:1.5,rotate:0},$e={scale:null,auto:!0,sorted:0,min:hl,max:-hl},Qe=(l,e,t,n,i)=>i,lt={show:!0,auto:!0,sorted:0,gaps:Qe,alpha:1,facets:[Il({},$e,{scale:"x"}),Il({},$e,{scale:"y"})]},et={scale:"y",auto:!0,sorted:0,show:!0,spanGaps:!1,gaps:Qe,alpha:1,points:{show:function(l,e){let{scale:t,idxs:n}=l.series[0],i=l._data[0],o=l.valToPos(i[n[0]],t,!0),s=l.valToPos(i[n[1]],t,!0);return el(s-o)/(l.series[e].points.space*y)>=n[1]-n[0]},filter:null},values:null,min:hl,max:-hl,idxs:[],path:null,clip:null};function tt(l,e,t){return t/10}const nt={time:!0,auto:!0,distr:1,log:10,asinh:1,min:null,max:null,dir:1,ori:0},it=Il({},nt,{time:!1,ori:1}),ot={};function st(l){let e=ot[l];return e||(e={key:l,plots:[],sub(l){e.plots.push(l)},unsub(l){e.plots=e.plots.filter((e=>e!=l))},pub(l,t,n,i,o,s,r){for(let u=0;e.plots.length>u;u++)e.plots[u]!=t&&e.plots[u].pub(l,t,n,i,o,s,r)}},null!=l&&(ot[l]=e)),e}function rt(l,e,t){const n=l.mode,i=l.series[e],o=2==n?l._data[e]:l._data,s=l.scales,r=l.bbox;let u=o[0],a=2==n?o[1]:o[e],f=2==n?s[i.facets[0].scale]:s[l.series[0].scale],c=2==n?s[i.facets[1].scale]:s[i.scale],h=r.left,d=r.top,p=r.width,m=r.height,g=l.valToPosH,x=l.valToPosV;return 0==f.ori?t(i,u,a,f,c,g,x,h,d,p,m,mt,xt,_t,vt,yt):t(i,u,a,f,c,x,g,d,h,m,p,gt,wt,bt,kt,Mt)}function ut(l,e){let t=0,n=0,i=K(l.bands,Pl);for(let l=0;i.length>l;l++){let o=i[l];o.series[0]==e?t=o.dir:o.series[1]==e&&(n|=1==o.dir?1:2)}return[t,1==n?-1:2==n?1:3==n?2:0]}function at(l,e,t,n,i){let o=l.series[e],s=l.scales[2==l.mode?o.facets[1].scale:o.scale];return-1==i?s.min:1==i?s.max:3==s.distr?1==s.dir?s.min:s.max:0}function ft(l,e,t,n,i,o){return rt(l,e,((l,e,s,r,u,a,f,c,h,d,p)=>{let m=l.pxRound;const g=0==r.ori?xt:wt;let x,w;1==r.dir*(0==r.ori?1:-1)?(x=t,w=n):(x=n,w=t);let _=m(a(e[x],r,d,c)),b=m(f(s[x],u,p,h)),v=m(a(e[w],r,d,c)),k=m(f(1==o?u.max:u.min,u,p,h)),y=new Path2D(i);return g(y,v,k),g(y,_,k),g(y,_,b),y}))}function ct(l,e,t,n,i,o){let s=null;if(l.length>0){s=new Path2D;const r=0==e?_t:bt;let u=t;for(let e=0;l.length>e;e++){let t=l[e];if(t[1]>t[0]){let l=t[0]-u;l>0&&r(s,u,n,l,n+o),u=t[1]}}let a=t+i-u,f=10;a>0&&r(s,u,n-f/2,a,n+o+f)}return s}function ht(l,e,t,n,i,o,s){let r=[],u=l.length;for(let a=1==i?t:n;a>=t&&n>=a;a+=i)if(null===e[a]){let f=a,c=a;if(1==i)for(;++a<=n&&null===e[a];)c=a;else for(;--a>=t&&null===e[a];)c=a;let h=o(l[f]),d=c==f?h:o(l[c]),p=f-i;h=s>0||0>p||p>=u?h:o(l[p]);let m=c+i;d=0>s||0>m||m>=u?d:o(l[m]),h>d||r.push([h,d])}return r}function dt(l){return 0==l?gl:1==l?nl:e=>kl(e,l)}function pt(l){let e=0==l?mt:gt,t=0==l?(l,e,t,n,i,o)=>{l.arcTo(e,t,n,i,o)}:(l,e,t,n,i,o)=>{l.arcTo(t,e,i,n,o)},n=0==l?(l,e,t,n,i)=>{l.rect(e,t,n,i)}:(l,e,t,n,i)=>{l.rect(t,e,i,n)};return(l,i,o,s,r,u=0,a=0)=>{0==u&&0==a?n(l,i,o,s,r):(u=ol(u,s/2,r/2),a=ol(a,s/2,r/2),e(l,i+u,o),t(l,i+s,o,i+s,o+r,u),t(l,i+s,o+r,i,o+r,a),t(l,i,o+r,i,o,a),t(l,i,o,i+s,o,u),l.closePath())}}const mt=(l,e,t)=>{l.moveTo(e,t)},gt=(l,e,t)=>{l.moveTo(t,e)},xt=(l,e,t)=>{l.lineTo(e,t)},wt=(l,e,t)=>{l.lineTo(t,e)},_t=pt(0),bt=pt(1),vt=(l,e,t,n,i,o)=>{l.arc(e,t,n,i,o)},kt=(l,e,t,n,i,o)=>{l.arc(t,e,n,i,o)},yt=(l,e,t,n,i,o,s)=>{l.bezierCurveTo(e,t,n,i,o,s)},Mt=(l,e,t,n,i,o,s)=>{l.bezierCurveTo(t,e,i,n,s,o)};function St(){return(l,e,t,n,i)=>rt(l,e,((e,o,s,r,u,a,f,c,h,d,p)=>{let m,g,{pxRound:x,points:w}=e;0==r.ori?(m=mt,g=vt):(m=gt,g=kt);const _=Sl(w.width*y,3);let b=(w.size-w.width)/2*y,v=Sl(2*b,3),k=new Path2D,M=new Path2D,{left:S,top:E,width:T,height:z}=l.bbox;_t(M,S-v,E-v,T+2*v,z+2*v);const D=l=>{if(null!=s[l]){let e=x(a(o[l],r,d,c)),t=x(f(s[l],u,p,h));m(k,e+b,t),g(k,e,t,b,0,2*ll)}};if(i)i.forEach(D);else for(let l=t;n>=l;l++)D(l);return{stroke:_>0?k:null,fill:k,clip:M,flags:3}}))}function Et(l){return(e,t,n,i,o,s)=>{n!=i&&(o!=n&&s!=n&&l(e,t,n),o!=i&&s!=i&&l(e,t,i),l(e,t,s))}}const Tt=Et(xt),zt=Et(wt);function Dt(l){const e=K(l?.alignGaps,0);return(l,t,n,i)=>rt(l,t,((o,s,r,u,a,f,c,h,d,p,m)=>{let g,x,w=o.pxRound,_=l=>w(f(l,u,p,h)),b=l=>w(c(l,a,m,d));0==u.ori?(g=xt,x=Tt):(g=wt,x=zt);const v=u.dir*(0==u.ori?1:-1),k={stroke:new Path2D,fill:null,clip:null,band:null,gaps:null,flags:1},y=k.stroke;let M,S,E,T=hl,z=-hl,D=_(s[1==v?n:i]),P=O(r,n,i,1*v),A=O(r,n,i,-1*v),W=_(s[P]),Y=_(s[A]),C=!1;for(let l=1==v?n:i;l>=n&&i>=l;l+=v){let e=_(s[l]),t=r[l];e==D?null!=t?(S=b(t),T==hl&&(g(y,e,S),M=S),T=ol(S,T),z=sl(S,z)):null===t&&(C=!0):(T!=hl&&(x(y,D,T,z,M,S),E=D),null!=t?(S=b(t),g(y,e,S),T=z=M=S):(T=hl,z=-hl,null===t&&(C=!0)),D=e)}T!=hl&&T!=z&&E!=D&&x(y,D,T,z,M,S);let[F,H]=ut(l,t);if(null!=o.fill||0!=F){let e=k.fill=new Path2D(y),n=b(o.fillTo(l,t,o.min,o.max,F));g(e,Y,n),g(e,W,n)}if(!o.spanGaps){let a=[];C&&a.push(...ht(s,r,n,i,v,_,e)),k.gaps=a=o.gaps(l,t,n,i,a),k.clip=ct(a,u.ori,h,d,p,m)}return 0!=H&&(k.band=2==H?[ft(l,t,n,i,y,-1),ft(l,t,n,i,y,1)]:ft(l,t,n,i,y,H)),k}))}function Pt(l,e,t,n,i,o,s=hl){if(l.length>1){let r=null;for(let u=0,a=1/0;l.length>u;u++)if(void 0!==e[u]){if(null!=r){let e=el(l[u]-l[r]);a>e&&(a=e,s=el(t(l[u],n,i,o)-t(l[r],n,i,o)))}r=u}}return s}function At(l,e,t,n,i){const o=l.length;if(2>o)return null;const s=new Path2D;if(t(s,l[0],e[0]),2==o)n(s,l[1],e[1]);else{let t=Array(o),n=Array(o-1),r=Array(o-1),u=Array(o-1);for(let t=0;o-1>t;t++)r[t]=e[t+1]-e[t],u[t]=l[t+1]-l[t],n[t]=r[t]/u[t];t[0]=n[0];for(let l=1;o-1>l;l++)0===n[l]||0===n[l-1]||n[l-1]>0!=n[l]>0?t[l]=0:(t[l]=3*(u[l-1]+u[l])/((2*u[l]+u[l-1])/n[l-1]+(u[l]+2*u[l-1])/n[l]),isFinite(t[l])||(t[l]=0));t[o-1]=n[o-2];for(let n=0;o-1>n;n++)i(s,l[n]+u[n]/3,e[n]+t[n]*u[n]/3,l[n+1]-u[n]/3,e[n+1]-t[n+1]*u[n]/3,l[n+1],e[n+1])}return s}const Wt=new Set;function Yt(){for(let l of Wt)l.syncRect(!0)}_&&(G("resize",v,Yt),G("scroll",v,Yt,!0),G(x,v,(()=>{qt.pxRatio=y})));const Ct=Dt(),Ft=St();function Ht(l,e,t,n){return(n?[l[0],l[1]].concat(l.slice(2)):[l[0]].concat(l.slice(1))).map(((l,n)=>Rt(l,n,e,t)))}function Rt(l,e,t,n){return Il({},0==e?t:n,l)}function Gt(l,e,t){return null==e?Al:[e,t]}const It=Gt;function Lt(l,e,t){return null==e?Al:q(e,t,U,!0)}function Ot(l,e,t,n){return null==e?Al:N(e,t,l.scales[n].log,!1)}const Nt=Ot;function jt(l,e,t,n){return null==e?Al:j(e,t,l.scales[n].log,!1)}const Ut=jt;function Bt(l,e,t,n,i){let o=sl(dl(l),dl(e)),s=e-l,r=L(i/n*s,t);do{let l=t[r],e=n*l/s;if(e>=i&&17>=o+(5>l?El.get(l):0))return[l,e]}while(++r<t.length);return[0,0]}function Vt(l){let e,t;return[l=l.replace(/(\d+)px/,((l,n)=>(e=nl((t=+n)*y))+"px")),e,t]}function Jt(l){l.show&&[l.font,l.labelFont].forEach((l=>{let e=Sl(l[2]*y,1);l[0]=l[0].replace(/[0-9.]+px/,e+"px"),l[1]=e}))}function qt(u,g,_){const k={mode:K(u.mode,1)},M=k.mode;function P(l,e){return((3==e.distr?al(l>0?l:e.clamp(k,l,e.min,e.max,e.key)):4==e.distr?cl(l,e.asinh):l)-e._min)/(e._max-e._min)}function W(l,e,t,n){let i=P(l,e);return n+t*(-1==e.dir?1-i:i)}function C(l,e,t,n){let i=P(l,e);return n+t*(-1==e.dir?i:1-i)}function H(l,e,t,n){return 0==e.ori?W(l,e,t,n):C(l,e,t,n)}k.valToPosH=W,k.valToPosV=C;let R=!1;k.status=0;const O=k.root=D("uplot");null!=u.id&&(O.id=u.id),S(O,u.class),u.title&&(D("u-title",O).textContent=u.title);const V=z("canvas"),J=k.ctx=V.getContext("2d"),X=D("u-wrap",O);G("click",X,(l=>{l.target===$&&(Fn!=An||Hn!=Wn)&&Bn.click(k,l)}),!0);const Z=k.under=D("u-under",X);X.appendChild(V);const $=k.over=D("u-over",X),tl=+K((u=Gl(u)).pxAlign,1),ul=dt(tl);(u.plugins||[]).forEach((l=>{l.opts&&(u=l.opts(k,u)||u)}));const fl=u.ms||.001,dl=k.series=1==M?Ht(u.series||[],Ge,et,!1):function(l,e){return l.map(((l,t)=>0==t?null:Il({},e,l)))}(u.series||[null],lt),gl=k.axes=Ht(u.axes||[],Re,Ze,!0),vl=k.scales={},Ml=k.bands=u.bands||[];Ml.forEach((l=>{l.fill=ml(l.fill||null),l.dir=K(l.dir,-1)}));const zl=2==M?dl[1].facets[0].scale:dl[0].scale,Yl={axes:function(){for(let l=0;gl.length>l;l++){let e=gl[l];if(!e.show||!e._show)continue;let t,n,u=e.side,a=u%2,f=e.stroke(k,l),c=0==u||3==u?-1:1;if(e.label){let l=nl((e._lpos+e.labelGap*c)*y);hn(e.labelFont[0],f,"center",2==u?i:o),J.save(),1==a?(t=n=0,J.translate(l,nl($e+ot/2)),J.rotate((3==u?-ll:ll)/2)):(t=nl(Je+Qe/2),n=l),J.fillText(e.label,t,n),J.restore()}let[h,d]=e._found;if(0==d)continue;let p=vl[e.scale],m=0==a?Qe:ot,g=0==a?Je:$e,x=nl(e.gap*y),w=e._splits,_=2==p.distr?w.map((l=>rn[l])):w,b=2==p.distr?rn[w[1]]-rn[w[0]]:h,v=e.ticks,M=e.border,S=v.show?nl(v.size*y):0,E=e._rotate*-ll/180,T=ul(e._pos*y),z=T+(S+x)*c;n=0==a?z:0,t=1==a?z:0,hn(e.font[0],f,1==e.align?s:2==e.align?r:E>0?s:0>E?r:0==a?"center":3==u?r:s,E||1==a?"middle":2==u?i:o);let D=e.font[1]*e.lineGap,P=w.map((l=>ul(H(l,p,m,g)))),A=e._values;for(let l=0;A.length>l;l++){let e=A[l];if(null!=e){0==a?t=P[l]:n=P[l],e=""+e;let i=-1==e.indexOf("\n")?[e]:e.split(/\n/gm);for(let l=0;i.length>l;l++){let e=i[l];E?(J.save(),J.translate(t,n+l*D),J.rotate(E),J.fillText(e,0,0),J.restore()):J.fillText(e,t,n+l*D)}}}v.show&&vn(P,v.filter(k,_,l,d,b),a,u,T,S,Sl(v.width*y,3),v.stroke(k,l),v.dash,v.cap);let W=e.grid;W.show&&vn(P,W.filter(k,_,l,d,b),a,0==a?2:1,0==a?$e:Je,0==a?ot:Qe,Sl(W.width*y,3),W.stroke(k,l),W.dash,W.cap),M.show&&vn([T],[1],0==a?1:0,0==a?1:2,1==a?$e:Je,1==a?ot:Qe,Sl(M.width*y,3),M.stroke(k,l),M.dash,M.cap)}Ti("drawAxes")},series:function(){At>0&&(dl.forEach(((l,e)=>{if(e>0&&l.show&&(mn(e,!1),mn(e,!0),null==l._paths)){sn!=l.alpha&&(J.globalAlpha=sn=l.alpha);let t=2==M?[0,g[e][0].length-1]:function(l){let e=pl(Yt-1,0,At-1),t=pl(qt+1,0,At-1);for(;null==l[e]&&e>0;)e--;for(;null==l[t]&&At-1>t;)t++;return[e,t]}(g[e]);l._paths=l.paths(k,e,t[0],t[1]),1!=sn&&(J.globalAlpha=sn=1)}})),dl.forEach(((l,e)=>{if(e>0&&l.show){sn!=l.alpha&&(J.globalAlpha=sn=l.alpha),null!=l._paths&&gn(e,!1);{let t=null!=l._paths?l._paths.gaps:null,n=l.points.show(k,e,Yt,qt,t),i=l.points.filter(k,e,n,t);(n||i)&&(l.points._paths=l.points.paths(k,e,Yt,qt,i),gn(e,!0))}1!=sn&&(J.globalAlpha=sn=1),Ti("drawSeries",e)}})))}},Rl=(u.drawOrder||["axes","series"]).map((l=>Yl[l]));function Ll(l){let e=vl[l];if(null==e){let t=(u.scales||Dl)[l]||Dl;if(null!=t.from)Ll(t.from),vl[l]=Il({},vl[t.from],t,{key:l});else{e=vl[l]=Il({},l==zl?nt:it,t),e.key=l;let n=e.time,i=e.range,o=Wl(i);if((l!=zl||2==M&&!n)&&(!o||null!=i[0]&&null!=i[1]||(i={min:null==i[0]?B:{mode:1,hard:i[0],soft:i[0]},max:null==i[1]?B:{mode:1,hard:i[1],soft:i[1]}},o=!1),!o&&Fl(i))){let l=i;i=(e,t,n)=>null==t?Al:q(t,n,l)}e.range=ml(i||(n?It:l==zl?3==e.distr?Nt:4==e.distr?Ut:Gt:3==e.distr?Ot:4==e.distr?jt:Lt)),e.auto=ml(!o&&e.auto),e.clamp=ml(e.clamp||tt),e._min=e._max=null}}}Ll("x"),Ll("y"),1==M&&dl.forEach((l=>{Ll(l.scale)})),gl.forEach((l=>{Ll(l.scale)}));for(let l in u.scales)Ll(l);const Nl=vl[zl],jl=Nl.distr;let Ul,Bl;0==Nl.ori?(S(O,"u-hz"),Ul=W,Bl=C):(S(O,"u-vt"),Ul=C,Bl=W);const Vl={};for(let l in vl){let e=vl[l];null==e.min&&null==e.max||(Vl[l]={min:e.min,max:e.max},e.min=e.max=null)}const Jl=u.tzDate||(l=>new Date(nl(l/fl))),ql=u.fmtDate||Xl,Kl=1==fl?xe(Jl):be(Jl),Zl=ke(Jl,ve(1==fl?ge:_e,ql)),$l=Se(Jl,Me("{YYYY}-{MM}-{DD} {h}:{mm}{aa}",ql)),Ql=[],le=k.legend=Il({},Ee,u.legend),ee=le.show,ie=le.markers;let oe,se,re;le.idxs=Ql,ie.width=ml(ie.width),ie.dash=ml(ie.dash),ie.stroke=ml(ie.stroke),ie.fill=ml(ie.fill);let ue,ae=[],fe=[],ce=!1,he={};if(le.live){const l=dl[1]?dl[1].values:null;ce=null!=l,ue=ce?l(k,1,0):{_:0};for(let l in ue)he[l]=w}if(ee)if(oe=z("table","u-legend",O),re=z("tbody",null,oe),le.mount(k,oe),ce){se=z("thead",null,oe,re);let l=z("tr",null,se);for(var de in z("th",null,l),ue)z("th",e,l).textContent=de}else S(oe,"u-inline"),le.live&&S(oe,"u-live");const pe={show:!0},ye={show:!1},Te=new Map;function ze(l,e,t,n=!0){const i=Te.get(e)||{},o=wt.bind[l](k,e,t,n);o&&(G(l,e,i[l]=o),Te.set(e,i))}function De(l,e){const t=Te.get(e)||{};for(let n in t)null!=l&&n!=l||(I(n,e,t[n]),delete t[n]);null==l&&Te.delete(e)}let Ae=0,We=0,Ye=0,Ce=0,Fe=0,He=0,je=Fe,Ue=He,Be=Ye,Ve=Ce,Je=0,$e=0,Qe=0,ot=0;k.bbox={};let rt=!1,ut=!1,ft=!1,ct=!1,ht=!1,pt=!1;function mt(l,e,t){(t||l!=k.width||e!=k.height)&>(l,e),Mn(!1),ft=!0,ut=!0,On()}function gt(l,e){k.width=Ae=Ye=l,k.height=We=Ce=e,Fe=He=0,function(){let l=!1,e=!1,t=!1,n=!1;gl.forEach((i=>{if(i.show&&i._show){let{side:o,_size:s}=i,r=s+(null!=i.label?i.labelSize:0);r>0&&(o%2?(Ye-=r,3==o?(Fe+=r,n=!0):t=!0):(Ce-=r,0==o?(He+=r,l=!0):e=!0))}})),Tt[0]=l,Tt[1]=t,Tt[2]=e,Tt[3]=n,Ye-=Pt[1]+Pt[3],Fe+=Pt[3],Ce-=Pt[2]+Pt[0],He+=Pt[0]}(),function(){let l=Fe+Ye,e=He+Ce,t=Fe,n=He;function i(i,o){switch(i){case 1:return l+=o,l-o;case 2:return e+=o,e-o;case 3:return t-=o,t+o;case 0:return n-=o,n+o}}gl.forEach((l=>{if(l.show&&l._show){let e=l.side;l._pos=i(e,l._size),null!=l.label&&(l._lpos=i(e,l.labelSize))}}))}();let t=k.bbox;Je=t.left=kl(Fe*y,.5),$e=t.top=kl(He*y,.5),Qe=t.width=kl(Ye*y,.5),ot=t.height=kl(Ce*y,.5)}const xt=3;k.setSize=function({width:l,height:e}){mt(l,e)};const wt=k.cursor=Il({},Pe,{drag:{y:2==M}},u.cursor);if(null==wt.dataIdx){let l=wt.hover,e=l.skip=new Set(l.skip??[]);e.add(void 0);let t=l.prox=ml(l.prox),n=l.bias??=0;wt.dataIdx=(l,i,o,s)=>{if(0==i)return o;let r=o,u=t(l,i,o,s)??hl,a=u>=0&&hl>u,f=0==Nl.ori?Ye:Ce,c=wt.left,h=g[0],d=g[i];if(e.has(d[o])){r=null;let l,t=null,i=null;if(0==n||-1==n)for(l=o;null==t&&l-- >0;)e.has(d[l])||(t=l);if(0==n||1==n)for(l=o;null==i&&l++<d.length;)e.has(d[l])||(i=l);if(null!=t||null!=i)if(a){let l=c-(null==t?-1/0:Ul(h[t],Nl,f,0)),e=(null==i?1/0:Ul(h[i],Nl,f,0))-c;l>e?e>u||(r=i):l>u||(r=t)}else r=null==i?t:null==t||o-t>i-o?i:t}else a&&el(c-Ul(h[o],Nl,f,0))>u&&(r=null);return r}}const _t=l=>{wt.event=l};wt.idxs=Ql,wt._lock=!1;let bt=wt.points;bt.show=ml(bt.show),bt.size=ml(bt.size),bt.stroke=ml(bt.stroke),bt.width=ml(bt.width),bt.fill=ml(bt.fill);const vt=k.focus=Il({},u.focus||{alpha:.3},wt.focus),kt=vt.prox>=0;let yt=[null],Mt=[null],St=[null];function Et(t,n){if(1==M||n>0){let l=1==M&&vl[t.scale].time,e=t.value;t.value=l?Cl(e)?Se(Jl,Me(e,ql)):e||$l:e||Xe,t.label=t.label||(l?"Time":"Value")}if(n>0){t.width=null==t.width?1:t.width,t.paths=t.paths||Ct||wl,t.fillTo=ml(t.fillTo||at),t.pxAlign=+K(t.pxAlign,tl),t.pxRound=dt(t.pxAlign),t.stroke=ml(t.stroke||null),t.fill=ml(t.fill||null),t._stroke=t._fill=t._paths=t._focus=null;let l=function(l){return Sl(1*(3+2*(l||1)),3)}(sl(1,t.width)),e=t.points=Il({},{size:l,width:sl(1,.2*l),stroke:t.stroke,space:2*l,paths:Ft,_stroke:null,_fill:null},t.points);e.show=ml(e.show),e.filter=ml(e.filter),e.fill=ml(e.fill),e.stroke=ml(e.stroke),e.paths=ml(e.paths),e.pxAlign=t.pxAlign}if(ee){let i=function(t,n){if(0==n&&(ce||!le.live||2==M))return Al;let i=[],o=z("tr","u-series",re,re.childNodes[n]);S(o,t.class),t.show||S(o,l);let s=z("th",null,o);if(ie.show){let l=D("u-marker",s);if(n>0){let e=ie.width(k,n);e&&(l.style.border=e+"px "+ie.dash(k,n)+" "+ie.stroke(k,n)),l.style.background=ie.fill(k,n)}}let r=D(e,s);for(var u in r.textContent=t.label,n>0&&(ie.show||(r.style.color=t.width>0?ie.stroke(k,n):ie.fill(k,n)),ze("click",s,(l=>{if(wt._lock)return;_t(l);let e=dl.indexOf(t);if((l.ctrlKey||l.metaKey)!=le.isolate){let l=dl.some(((l,t)=>t>0&&t!=e&&l.show));dl.forEach(((t,n)=>{n>0&&$n(n,l?n==e?pe:ye:pe,!0,Di.setSeries)}))}else $n(e,{show:!t.show},!0,Di.setSeries)}),!1),kt&&ze(d,s,(l=>{wt._lock||(_t(l),$n(dl.indexOf(t),ti,!0,Di.setSeries))}),!1)),ue){let l=z("td","u-value",o);l.textContent="--",i.push(l)}return[o,i]}(t,n);ae.splice(n,0,i[0]),fe.splice(n,0,i[1]),le.values.push(null)}if(wt.show){Ql.splice(n,0,null);let l=function(l,e){if(e>0){let t=wt.points.show(k,e);if(t)return S(t,"u-cursor-pt"),S(t,l.class),A(t,-10,-10,Ye,Ce),$.insertBefore(t,yt[e]),t}}(t,n);null!=l&&(yt.splice(n,0,l),Mt.splice(n,0,0),St.splice(n,0,0))}Ti("addSeries",n)}k.addSeries=function(l,e){e=null==e?dl.length:e,l=1==M?Rt(l,e,Ge,et):Rt(l,e,null,lt),dl.splice(e,0,l),Et(dl[e],e)},k.delSeries=function(l){if(dl.splice(l,1),ee){le.values.splice(l,1),fe.splice(l,1);let e=ae.splice(l,1)[0];De(null,e.firstChild),e.remove()}wt.show&&(Ql.splice(l,1),yt.length>1&&(yt.splice(l,1)[0].remove(),Mt.splice(l,1),St.splice(l,1))),Ti("delSeries",l)};const Tt=[!1,!1,!1,!1];function zt(l,e,t){let[n,i,o,s]=t,r=e%2,u=0;return 0==r&&(s||i)&&(u=0==e&&!n||2==e&&!o?nl(Re.size/3):0),1==r&&(n||o)&&(u=1==e&&!i||3==e&&!s?nl(Ze.size/2):0),u}const Dt=k.padding=(u.padding||[zt,zt,zt,zt]).map((l=>ml(K(l,zt)))),Pt=k._padding=Dt.map(((l,e)=>l(k,e,Tt,0)));let At,Yt=null,qt=null;const Kt=1==M?dl[0].idxs:null;let Xt,Zt,$t,Qt,ln,en,tn,nn,on,sn,rn=null,un=!1;function an(l,e){if(k.data=k._data=g=null==l?[]:l,2==M){At=0;for(let l=1;dl.length>l;l++)At+=g[l][0].length}else{0==g.length&&(k.data=k._data=g=[[]]),rn=g[0],At=rn.length;let l=g;if(2==jl){l=g.slice();let e=l[0]=Array(At);for(let l=0;At>l;l++)e[l]=l}k._data=g=l}if(Mn(!0),Ti("setData"),2==jl&&(ft=!0),!1!==e){let l=Nl;l.auto(k,un)?fn():Zn(zl,l.min,l.max),ct=ct||wt.left>=0,pt=!0,On()}}function fn(){let l,e;un=!0,1==M&&(At>0?(Yt=Kt[0]=0,qt=Kt[1]=At-1,l=g[0][Yt],e=g[0][qt],2==jl?(l=Yt,e=qt):l==e&&(3==jl?[l,e]=N(l,l,Nl.log,!1):4==jl?[l,e]=j(l,l,Nl.log,!1):Nl.time?e=l+nl(86400/fl):[l,e]=q(l,e,U,!0))):(Yt=Kt[0]=l=null,qt=Kt[1]=e=null)),Zn(zl,l,e)}function cn(l,e,t,n,i,o){l??=a,t??=Pl,n??="butt",i??=a,o??="round",l!=Xt&&(J.strokeStyle=Xt=l),i!=Zt&&(J.fillStyle=Zt=i),e!=$t&&(J.lineWidth=$t=e),o!=ln&&(J.lineJoin=ln=o),n!=en&&(J.lineCap=en=n),t!=Qt&&J.setLineDash(Qt=t)}function hn(l,e,t,n){e!=Zt&&(J.fillStyle=Zt=e),l!=tn&&(J.font=tn=l),t!=nn&&(J.textAlign=nn=t),n!=on&&(J.textBaseline=on=n)}function dn(l,e,t,n,i=0){if(n.length>0&&l.auto(k,un)&&(null==e||null==e.min)){let e=K(Yt,0),o=K(qt,n.length-1),s=null==t.min?3==l.distr?function(l,e,t){let n=hl,i=-hl;for(let o=e;t>=o;o++){let e=l[o];null!=e&&e>0&&(n>e&&(n=e),e>i&&(i=e))}return[n,i]}(n,e,o):function(l,e,t,n){let i=hl,o=-hl;if(1==n)i=l[e],o=l[t];else if(-1==n)i=l[t],o=l[e];else for(let n=e;t>=n;n++){let e=l[n];null!=e&&(i>e&&(i=e),e>o&&(o=e))}return[i,o]}(n,e,o,i):[t.min,t.max];l.min=ol(l.min,t.min=s[0]),l.max=sl(l.max,t.max=s[1])}}k.setData=an;const pn={min:null,max:null};function mn(l,e){let t=e?dl[l].points:dl[l];t._stroke=t.stroke(k,l),t._fill=t.fill(k,l)}function gn(l,e){let t=e?dl[l].points:dl[l],{stroke:n,fill:i,clip:o,flags:s,_stroke:r=t._stroke,_fill:u=t._fill,_width:a=t.width}=t._paths;a=Sl(a*y,3);let f=null,c=a%2/2;e&&null==u&&(u=a>0?"#fff":r);let h=1==t.pxAlign&&c>0;if(h&&J.translate(c,c),!e){let l=Je-a/2,e=$e-a/2,t=Qe+a,n=ot+a;f=new Path2D,f.rect(l,e,t,n)}e?wn(r,a,t.dash,t.cap,u,n,i,s,o):function(l,e,t,n,i,o,s,r,u,a,f){let c=!1;0!=u&&Ml.forEach(((h,d)=>{if(h.series[0]==l){let l,p=dl[h.series[1]],m=g[h.series[1]],x=(p._paths||Dl).band;Wl(x)&&(x=1==h.dir?x[0]:x[1]);let w=null;p.show&&x&&function(l,e,t){for(e=K(e,0),t=K(t,l.length-1);t>=e;){if(null!=l[e])return!0;e++}return!1}(m,Yt,qt)?(w=h.fill(k,d)||o,l=p._paths.clip):x=null,wn(e,t,n,i,w,s,r,u,a,f,l,x),c=!0}})),c||wn(e,t,n,i,o,s,r,u,a,f)}(l,r,a,t.dash,t.cap,u,n,i,s,f,o),h&&J.translate(-c,-c)}const xn=3;function wn(l,e,t,n,i,o,s,r,u,a,f,c){cn(l,e,t,n,i),(u||a||c)&&(J.save(),u&&J.clip(u),a&&J.clip(a)),c?(r&xn)==xn?(J.clip(c),f&&J.clip(f),bn(i,s),_n(l,o,e)):2&r?(bn(i,s),J.clip(c),_n(l,o,e)):1&r&&(J.save(),J.clip(c),f&&J.clip(f),bn(i,s),J.restore(),_n(l,o,e)):(bn(i,s),_n(l,o,e)),(u||a||c)&&J.restore()}function _n(l,e,t){t>0&&(e instanceof Map?e.forEach(((l,e)=>{J.strokeStyle=Xt=e,J.stroke(l)})):null!=e&&l&&J.stroke(e))}function bn(l,e){e instanceof Map?e.forEach(((l,e)=>{J.fillStyle=Zt=e,J.fill(l)})):null!=e&&l&&J.fill(e)}function vn(l,e,t,n,i,o,s,r,u,a){let f=s%2/2;1==tl&&J.translate(f,f),cn(r,s,u,a,r),J.beginPath();let c,h,d,p,m=i+(0==n||3==n?-o:o);0==t?(h=i,p=m):(c=i,d=m);for(let n=0;l.length>n;n++)null!=e[n]&&(0==t?c=d=l[n]:h=p=l[n],J.moveTo(c,h),J.lineTo(d,p));J.stroke(),1==tl&&J.translate(-f,-f)}function kn(l){let e=!0;return gl.forEach(((t,n)=>{if(!t.show)return;let i=vl[t.scale];if(null==i.min)return void(t._show&&(e=!1,t._show=!1,Mn(!1)));t._show||(e=!1,t._show=!0,Mn(!1));let o=t.side,s=o%2,{min:r,max:u}=i,[a,f]=function(l,e,t,n){let i,o=gl[l];if(n>0){let s=o._space=o.space(k,l,e,t,n);i=Bt(e,t,o._incrs=o.incrs(k,l,e,t,n,s),n,s)}else i=[0,0];return o._found=i}(n,r,u,0==s?Ye:Ce);if(0==f)return;let c=t._splits=t.splits(k,n,r,u,a,f,2==i.distr),h=2==i.distr?c.map((l=>rn[l])):c,d=2==i.distr?rn[c[1]]-rn[c[0]]:a,p=t._values=t.values(k,t.filter(k,h,n,f,d),n,f,d);t._rotate=2==o?t.rotate(k,p,n,f):0;let m=t._size;t._size=il(t.size(k,p,n,l)),null!=m&&t._size!=m&&(e=!1)})),e}function yn(l){let e=!0;return Dt.forEach(((t,n)=>{let i=t(k,n,Tt,l);i!=Pt[n]&&(e=!1),Pt[n]=i})),e}function Mn(l){dl.forEach(((e,t)=>{t>0&&(e._paths=null,l&&(1==M?(e.min=null,e.max=null):e.facets.forEach((l=>{l.min=null,l.max=null}))))}))}let Sn,En,Tn,zn,Dn,Pn,An,Wn,Yn,Cn,Fn,Hn,Rn=!1,Gn=!1,In=[];function Ln(){Gn=!1;for(let l=0;In.length>l;l++)Ti(...In[l]);In.length=0}function On(){Rn||(Ol(Nn),Rn=!0)}function Nn(){if(rt&&(function(){for(let l in vl){let e=vl[l];null==Vl[l]&&(null==e.min||null!=Vl[zl]&&e.auto(k,un))&&(Vl[l]=pn)}for(let l in vl){let e=vl[l];null==Vl[l]&&null!=e.from&&null!=Vl[e.from]&&(Vl[l]=pn)}null!=Vl[zl]&&Mn(!0);let l={};for(let e in Vl){let t=Vl[e];if(null!=t){let n=l[e]=Gl(vl[e],Hl);if(null!=t.min)Il(n,t);else if(e!=zl||2==M)if(0==At&&null==n.from){let l=n.range(k,null,null,e);n.min=l[0],n.max=l[1]}else n.min=hl,n.max=-hl}}if(At>0){dl.forEach(((e,t)=>{if(1==M){let n=e.scale,i=Vl[n];if(null==i)return;let o=l[n];if(0==t){let l=o.range(k,o.min,o.max,n);o.min=l[0],o.max=l[1],Yt=L(o.min,g[0]),qt=L(o.max,g[0]),qt-Yt>1&&(o.min>g[0][Yt]&&Yt++,g[0][qt]>o.max&&qt--),e.min=rn[Yt],e.max=rn[qt]}else e.show&&e.auto&&dn(o,i,e,g[t],e.sorted);e.idxs[0]=Yt,e.idxs[1]=qt}else if(t>0&&e.show&&e.auto){let[n,i]=e.facets,o=n.scale,s=i.scale,[r,u]=g[t],a=l[o],f=l[s];null!=a&&dn(a,Vl[o],n,r,n.sorted),null!=f&&dn(f,Vl[s],i,u,i.sorted),e.min=i.min,e.max=i.max}}));for(let e in l){let t=l[e],n=Vl[e];if(null==t.from&&(null==n||null==n.min)){let l=t.range(k,t.min==hl?null:t.min,t.max==-hl?null:t.max,e);t.min=l[0],t.max=l[1]}}}for(let e in l){let t=l[e];if(null!=t.from){let n=l[t.from];if(null==n.min)t.min=t.max=null;else{let l=t.range(k,n.min,n.max,e);t.min=l[0],t.max=l[1]}}}let e={},t=!1;for(let n in l){let i=l[n],o=vl[n];if(o.min!=i.min||o.max!=i.max){o.min=i.min,o.max=i.max;let l=o.distr;o._min=3==l?al(o.min):4==l?cl(o.min,o.asinh):o.min,o._max=3==l?al(o.max):4==l?cl(o.max,o.asinh):o.max,e[n]=t=!0}}if(t){dl.forEach(((l,t)=>{2==M?t>0&&e.y&&(l._paths=null):e[l.scale]&&(l._paths=null)}));for(let l in e)ft=!0,Ti("setScale",l);wt.show&&wt.left>=0&&(ct=pt=!0)}for(let l in Vl)Vl[l]=null}(),rt=!1),ft&&(function(){let l=!1,e=0;for(;!l;){e++;let t=kn(e),n=yn(e);l=e==xt||t&&n,l||(gt(k.width,k.height),ut=!0)}}(),ft=!1),ut){if(T(Z,s,Fe),T(Z,i,He),T(Z,t,Ye),T(Z,n,Ce),T($,s,Fe),T($,i,He),T($,t,Ye),T($,n,Ce),T(X,t,Ae),T(X,n,We),V.width=nl(Ae*y),V.height=nl(We*y),gl.forEach((({_el:e,_show:t,_size:n,_pos:i,side:o})=>{if(null!=e)if(t){let t=o%2==1;T(e,t?"left":"top",i-(3===o||0===o?n:0)),T(e,t?"width":"height",n),T(e,t?"top":"left",t?He:Fe),T(e,t?"height":"width",t?Ce:Ye),E(e,l)}else S(e,l)})),Xt=Zt=$t=ln=en=tn=nn=on=Qt=null,sn=1,hi(!0),Fe!=je||He!=Ue||Ye!=Be||Ce!=Ve){Mn(!1);let l=Ye/Be,e=Ce/Ve;if(wt.show&&!ct&&wt.left>=0){wt.left*=l,wt.top*=e,Tn&&A(Tn,nl(wt.left),0,Ye,Ce),zn&&A(zn,0,nl(wt.top),Ye,Ce);for(let t=1;yt.length>t;t++)Mt[t]*=l,St[t]*=e,A(yt[t],yl(Mt[t],1),yl(St[t],1),Ye,Ce)}if(qn.show&&!ht&&qn.left>=0&&qn.width>0){qn.left*=l,qn.width*=l,qn.top*=e,qn.height*=e;for(let l in mi)T(Kn,l,qn[l])}je=Fe,Ue=He,Be=Ye,Ve=Ce}Ti("setSize"),ut=!1}Ae>0&&We>0&&(J.clearRect(0,0,V.width,V.height),Ti("drawClear"),Rl.forEach((l=>l())),Ti("draw")),qn.show&&ht&&(Xn(qn),ht=!1),wt.show&&ct&&(fi(null,!0,!1),ct=!1),le.show&&le.live&&pt&&(ui(),pt=!1),R||(R=!0,k.status=1,Ti("ready")),un=!1,Rn=!1}function jn(l,e){let t=vl[l];if(null==t.from){if(0==At){let n=t.range(k,e.min,e.max,l);e.min=n[0],e.max=n[1]}if(e.min>e.max){let l=e.min;e.min=e.max,e.max=l}if(At>1&&null!=e.min&&null!=e.max&&1e-16>e.max-e.min)return;l==zl&&2==t.distr&&At>0&&(e.min=L(e.min,g[0]),e.max=L(e.max,g[0]),e.min==e.max&&e.max++),Vl[l]=e,rt=!0,On()}}k.batch=function(l,e=!1){Rn=!0,Gn=e,l(k),Nn(),e&&In.length>0&&queueMicrotask(Ln)},k.redraw=(l,e)=>{ft=e||!1,!1!==l?Zn(zl,Nl.min,Nl.max):On()},k.setScale=jn;let Un=!1;const Bn=wt.drag;let Vn=Bn.x,Jn=Bn.y;wt.show&&(wt.x&&(Sn=D("u-cursor-x",$)),wt.y&&(En=D("u-cursor-y",$)),0==Nl.ori?(Tn=Sn,zn=En):(Tn=En,zn=Sn),Fn=wt.left,Hn=wt.top);const qn=k.select=Il({show:!0,over:!0,left:0,width:0,top:0,height:0},u.select),Kn=qn.show?D("u-select",qn.over?$:Z):null;function Xn(l,e){if(qn.show){for(let e in l)qn[e]=l[e],e in mi&&T(Kn,e,l[e]);!1!==e&&Ti("setSelect")}}function Zn(l,e,t){jn(l,{min:e,max:t})}function $n(e,t,n,i){null!=t.focus&&function(l){if(l!=ei){let e=null==l,t=1!=vt.alpha;dl.forEach(((n,i)=>{if(1==M||i>0){let o=e||0==i||i==l;n._focus=e?null:o,t&&function(l,e){dl[l].alpha=e,wt.show&&yt[l]&&(yt[l].style.opacity=e),ee&&ae[l]&&(ae[l].style.opacity=e)}(i,o?1:vt.alpha)}})),ei=l,t&&On()}}(e),null!=t.show&&dl.forEach(((n,i)=>{0>=i||e!=i&&null!=e||(n.show=t.show,function(e){let t=ee?ae[e]:null;dl[e].show?t&&E(t,l):(t&&S(t,l),yt.length>1&&A(yt[e],-10,-10,Ye,Ce))}(i),2==M?(Zn(n.facets[0].scale,null,null),Zn(n.facets[1].scale,null,null)):Zn(n.scale,null,null),On())})),!1!==n&&Ti("setSeries",e,t),i&&Wi("setSeries",k,e,t)}let Qn,li,ei;k.setSelect=Xn,k.setSeries=$n,k.addBand=function(l,e){l.fill=ml(l.fill||null),l.dir=K(l.dir,-1),Ml.splice(e=null==e?Ml.length:e,0,l)},k.setBand=function(l,e){Il(Ml[l],e)},k.delBand=function(l){null==l?Ml.length=0:Ml.splice(l,1)};const ti={focus:!0};function ni(l,e,t){let n=vl[e];t&&(l=l/y-(1==n.ori?He:Fe));let i=Ye;1==n.ori&&(i=Ce,l=i-l),-1==n.dir&&(l=i-l);let o=n._min,s=o+l/i*(n._max-o),r=n.distr;return 3==r?rl(10,s):4==r?((l,e=1)=>Q.sinh(l)*e)(s,n.asinh):s}function ii(l,e){T(Kn,s,qn.left=l),T(Kn,t,qn.width=e)}function oi(l,e){T(Kn,i,qn.top=l),T(Kn,n,qn.height=e)}ee&&kt&&ze(p,oe,(l=>{wt._lock||(_t(l),null!=ei&&$n(null,ti,!0,Di.setSeries))})),k.valToIdx=l=>L(l,g[0]),k.posToIdx=function(l,e){return L(ni(l,zl,e),g[0],Yt,qt)},k.posToVal=ni,k.valToPos=(l,e,t)=>0==vl[e].ori?W(l,vl[e],t?Qe:Ye,t?Je:0):C(l,vl[e],t?ot:Ce,t?$e:0),k.setCursor=(l,e,t)=>{Fn=l.left,Hn=l.top,fi(null,e,t)};let si=0==Nl.ori?ii:oi,ri=1==Nl.ori?ii:oi;function ui(l,e){null!=l&&(l.idxs?l.idxs.forEach(((l,e)=>{Ql[e]=l})):(l=>void 0===l)(l.idx)||Ql.fill(l.idx),le.idx=Ql[0]);for(let l=0;dl.length>l;l++)(l>0||1==M&&!ce)&&ai(l,Ql[l]);ee&&le.live&&function(){if(ee&&le.live)for(let l=2==M?1:0;dl.length>l;l++){if(0==l&&ce)continue;let e=le.values[l],t=0;for(let n in e)fe[l][t++].firstChild.nodeValue=e[n]}}(),pt=!1,!1!==e&&Ti("setLegend")}function ai(l,e){let t,n=dl[l],i=0==l&&2==jl?rn:g[l];ce?t=n.values(k,l,e)??he:(t=n.value(k,null==e?null:i[e],l,e),t=null==t?he:{_:t}),le.values[l]=t}function fi(l,e,t){let n;Yn=Fn,Cn=Hn,[Fn,Hn]=wt.move(k,Fn,Hn),wt.left=Fn,wt.top=Hn,wt.show&&(Tn&&A(Tn,nl(Fn),0,Ye,Ce),zn&&A(zn,0,nl(Hn),Ye,Ce)),Qn=hl;let i=0==Nl.ori?Ye:Ce,o=1==Nl.ori?Ye:Ce;if(0>Fn||0==At||Yt>qt){n=wt.idx=null;for(let l=0;dl.length>l;l++)l>0&&yt.length>1&&A(yt[l],-10,-10,Ye,Ce);kt&&$n(null,ti,!0,null==l&&Di.setSeries),le.live&&(Ql.fill(n),pt=!0)}else{let l,e,t;1==M&&(l=0==Nl.ori?Fn:Hn,e=ni(l,zl),n=wt.idx=L(e,g[0],Yt,qt),t=Ul(g[0][n],Nl,i,0));for(let l=2==M?1:0;dl.length>l;l++){let s=dl[l],r=Ql[l],u=null==r?null:1==M?g[l][r]:g[l][1][r],a=wt.dataIdx(k,l,n,e),f=null==a?null:1==M?g[l][a]:g[l][1][a];pt=pt||f!=u||a!=r,Ql[l]=a;let c=a==n?t:Ul(1==M?g[0][a]:g[l][0][a],Nl,i,0);if(l>0&&s.show){let e,t,n=null==f?-10:Bl(f,1==M?vl[s.scale]:vl[s.facets[1].scale],o,0);if(kt&&null!=f){let e=1==Nl.ori?Fn:Hn,t=el(vt.dist(k,l,a,n,e));if(Qn>t){let n=vt.bias;if(0!=n){let i=ni(e,s.scale),o=0>i?-1:1;o!=(0>f?-1:1)||(1==o?1==n?i>f:f>i:1==n?f>i:i>f)||(Qn=t,li=l)}else Qn=t,li=l}}if(0==Nl.ori?(e=c,t=n):(e=n,t=c),pt&&yt.length>1){Y(yt[l],wt.points.fill(k,l),wt.points.stroke(k,l));let n,i,o,s,r=!0,u=wt.points.bbox;if(null!=u){r=!1;let e=u(k,l);o=e.left,s=e.top,n=e.width,i=e.height}else o=e,s=t,n=i=wt.points.size(k,l);F(yt[l],n,i,r),Mt[l]=o,St[l]=s,A(yt[l],yl(o,1),yl(s,1),Ye,Ce)}}}}if(qn.show&&Un)if(null!=l){let[e,t]=Di.scales,[n,s]=Di.match,[r,u]=l.cursor.sync.scales,a=l.cursor.drag;if(Vn=a._x,Jn=a._y,Vn||Jn){let a,f,c,h,d,{left:p,top:m,width:g,height:x}=l.select,w=l.scales[e].ori,_=l.posToVal,b=null!=e&&n(e,r),v=null!=t&&s(t,u);b&&Vn?(0==w?(a=p,f=g):(a=m,f=x),c=vl[e],h=Ul(_(a,r),c,i,0),d=Ul(_(a+f,r),c,i,0),si(ol(h,d),el(d-h))):si(0,i),v&&Jn?(1==w?(a=p,f=g):(a=m,f=x),c=vl[t],h=Bl(_(a,u),c,o,0),d=Bl(_(a+f,u),c,o,0),ri(ol(h,d),el(d-h))):ri(0,o)}else gi()}else{let l=el(Yn-Dn),e=el(Cn-Pn);if(1==Nl.ori){let t=l;l=e,e=t}Vn=Bn.x&&l>=Bn.dist,Jn=Bn.y&&e>=Bn.dist;let t,n,s=Bn.uni;null!=s?Vn&&Jn&&(Vn=l>=s,Jn=e>=s,Vn||Jn||(e>l?Jn=!0:Vn=!0)):Bn.x&&Bn.y&&(Vn||Jn)&&(Vn=Jn=!0),Vn&&(0==Nl.ori?(t=An,n=Fn):(t=Wn,n=Hn),si(ol(t,n),el(n-t)),Jn||ri(0,o)),Jn&&(1==Nl.ori?(t=An,n=Fn):(t=Wn,n=Hn),ri(ol(t,n),el(n-t)),Vn||si(0,i)),Vn||Jn||(si(0,0),ri(0,0))}if(Bn._x=Vn,Bn._y=Jn,null==l){if(t){if(null!=Pi){let[l,e]=Di.scales;Di.values[0]=null!=l?ni(0==Nl.ori?Fn:Hn,l):null,Di.values[1]=null!=e?ni(1==Nl.ori?Fn:Hn,e):null}Wi(f,k,Fn,Hn,Ye,Ce,n)}if(kt){let l=t&&Di.setSeries,e=vt.prox;null==ei?Qn>e||$n(li,ti,!0,l):Qn>e?$n(null,ti,!0,l):li!=ei&&$n(li,ti,!0,l)}}pt&&(le.idx=n,ui()),!1!==e&&Ti("setCursor")}k.setLegend=ui;let ci=null;function hi(l=!1){l?ci=null:(ci=$.getBoundingClientRect(),Ti("syncRect",ci))}function di(l,e,t,n,i,o){wt._lock||Un&&null!=l&&0==l.movementX&&0==l.movementY||(pi(l,e,t,n,i,o,0,!1,null!=l),null!=l?fi(null,!0,!0):fi(e,!0,!1))}function pi(l,e,t,n,i,o,s,r,u){if(null==ci&&hi(!1),_t(l),null!=l)t=l.clientX-ci.left,n=l.clientY-ci.top;else{if(0>t||0>n)return Fn=-10,void(Hn=-10);let[l,s]=Di.scales,r=e.cursor.sync,[u,a]=r.values,[f,c]=r.scales,[h,d]=Di.match,p=e.axes[0].side%2==1,m=0==Nl.ori?Ye:Ce,g=1==Nl.ori?Ye:Ce,x=p?o:i,w=p?i:o,_=p?n:t,b=p?t:n;if(t=null!=f?h(l,f)?H(u,vl[l],m,0):-10:m*(_/x),n=null!=c?d(s,c)?H(a,vl[s],g,0):-10:g*(b/w),1==Nl.ori){let l=t;t=n,n=l}}u&&(t>1&&Ye-1>t||(t=kl(t,Ye)),n>1&&Ce-1>n||(n=kl(n,Ce))),r?(Dn=t,Pn=n,[An,Wn]=wt.move(k,t,n)):(Fn=t,Hn=n)}Object.defineProperty(k,"rect",{get:()=>(null==ci&&hi(!1),ci)});const mi={width:0,height:0,left:0,top:0};function gi(){Xn(mi,!1)}let xi,wi,_i,bi;function vi(l,e,t,n,i,o){Un=!0,Vn=Jn=Bn._x=Bn._y=!1,pi(l,e,t,n,i,o,0,!0,!1),null!=l&&(ze(h,b,ki,!1),Wi(c,k,An,Wn,Ye,Ce,null));let{left:s,top:r,width:u,height:a}=qn;xi=s,wi=r,_i=u,bi=a,gi()}function ki(l,e,t,n,i,o){Un=Bn._x=Bn._y=!1,pi(l,e,t,n,i,o,0,!1,!0);let{left:s,top:r,width:u,height:a}=qn,f=u>0||a>0,c=xi!=s||wi!=r||_i!=u||bi!=a;if(f&&c&&Xn(qn),Bn.setScale&&f&&c){let l=s,e=u,t=r,n=a;if(1==Nl.ori&&(l=r,e=a,t=s,n=u),Vn&&Zn(zl,ni(l,zl),ni(l+e,zl)),Jn)for(let l in vl){let e=vl[l];l!=zl&&null==e.from&&e.min!=hl&&Zn(l,ni(t+n,l),ni(t,l))}gi()}else wt.lock&&(wt._lock=!wt._lock,wt._lock||fi(null,!0,!1));null!=l&&(De(h,b),Wi(h,k,Fn,Hn,Ye,Ce,null))}function yi(l){wt._lock||(_t(l),fn(),gi(),null!=l&&Wi(m,k,Fn,Hn,Ye,Ce,null))}function Mi(){gl.forEach(Jt),mt(k.width,k.height,!0)}G(x,v,Mi);const Si={};Si.mousedown=vi,Si.mousemove=di,Si.mouseup=ki,Si.dblclick=yi,Si.setSeries=(l,e,t,n)=>{-1!=(t=(0,Di.match[2])(k,e,t))&&$n(t,n,!0,!1)},wt.show&&(ze(c,$,vi),ze(f,$,di),ze(d,$,(l=>{_t(l),hi(!1)})),ze(p,$,(function(l){if(wt._lock)return;_t(l);let e=Un;if(Un){let l,e,t=!0,n=!0,i=10;0==Nl.ori?(l=Vn,e=Jn):(l=Jn,e=Vn),l&&e&&(t=i>=Fn||Fn>=Ye-i,n=i>=Hn||Hn>=Ce-i),l&&t&&(Fn=An>Fn?0:Ye),e&&n&&(Hn=Wn>Hn?0:Ce),fi(null,!0,!0),Un=!1}Fn=-10,Hn=-10,fi(null,!0,!0),e&&(Un=e)})),ze(m,$,yi),Wt.add(k),k.syncRect=hi);const Ei=k.hooks=u.hooks||{};function Ti(l,e,t){Gn?In.push([l,e,t]):l in Ei&&Ei[l].forEach((l=>{l.call(null,k,e,t)}))}(u.plugins||[]).forEach((l=>{for(let e in l.hooks)Ei[e]=(Ei[e]||[]).concat(l.hooks[e])}));const zi=(l,e,t)=>t,Di=Il({key:null,setSeries:!1,filters:{pub:_l,sub:_l},scales:[zl,dl[1]?dl[1].scale:null],match:[bl,bl,zi],values:[null,null]},wt.sync);2==Di.match.length&&Di.match.push(zi),wt.sync=Di;const Pi=Di.key,Ai=st(Pi);function Wi(l,e,t,n,i,o,s){Di.filters.pub(l,e,t,n,i,o,s)&&Ai.pub(l,e,t,n,i,o,s)}function Yi(){Ti("init",u,g),an(g||u.data,!1),Vl[zl]?jn(zl,Vl[zl]):fn(),ht=qn.show&&(qn.width>0||qn.height>0),ct=pt=!0,mt(u.width,u.height)}return Ai.sub(k),k.pub=function(l,e,t,n,i,o,s){Di.filters.sub(l,e,t,n,i,o,s)&&Si[l](null,e,t,n,i,o,s)},k.destroy=function(){Ai.unsub(k),Wt.delete(k),Te.clear(),I(x,v,Mi),O.remove(),oe?.remove(),Ti("destroy")},dl.forEach(Et),gl.forEach((function(l,e){if(l._show=l.show,l.show){let t=vl[l.scale];null==t&&(l.scale=l.side%2?dl[1].scale:zl,t=vl[l.scale]);let n=t.time;l.size=ml(l.size),l.space=ml(l.space),l.rotate=ml(l.rotate),Wl(l.incrs)&&l.incrs.forEach((l=>{!El.has(l)&&El.set(l,Tl(l))})),l.incrs=ml(l.incrs||(2==t.distr?te:n?1==fl?me:we:ne)),l.splits=ml(l.splits||(n&&1==t.distr?Kl:3==t.distr?Oe:4==t.distr?Ne:Le)),l.stroke=ml(l.stroke),l.grid.stroke=ml(l.grid.stroke),l.ticks.stroke=ml(l.ticks.stroke),l.border.stroke=ml(l.border.stroke);let i=l.values;l.values=Wl(i)&&!Wl(i[0])?ml(i):n?Wl(i)?ke(Jl,ve(i,ql)):Cl(i)?function(l,e){let t=Xl(e);return(e,n)=>n.map((e=>t(l(e))))}(Jl,i):i||Zl:i||Ie,l.filter=ml(l.filter||(3>t.distr||10!=t.log?3==t.distr&&2==t.log?Ke:xl:qe)),l.font=Vt(l.font),l.labelFont=Vt(l.labelFont),l._size=l.size(k,null,e,0),l._space=l._rotate=l._incrs=l._found=l._splits=l._values=null,l._size>0&&(Tt[e]=!0,l._el=D("u-axis",X))}})),_?_ instanceof HTMLElement?(_.appendChild(O),Yi()):_(k,Yi):Yi(),k}qt.assign=Il,qt.fmtNum=$,qt.rangeNum=q,qt.rangeLog=N,qt.rangeAsinh=j,qt.orient=rt,qt.pxRatio=y,qt.join=function(l,e){if(function(l){let e=l[0][0],t=e.length;for(let n=1;l.length>n;n++){let i=l[n][0];if(i.length!=t)return!1;if(i!=e)for(let l=0;t>l;l++)if(i[l]!=e[l])return!1}return!0}(l)){let e=l[0].slice();for(let t=1;l.length>t;t++)e.push(...l[t].slice(1));return function(l,e=100){const t=l.length;if(1>=t)return!0;let n=0,i=t-1;for(;i>=n&&null==l[n];)n++;for(;i>=n&&null==l[i];)i--;if(n>=i)return!0;const o=sl(1,tl((i-n+1)/e));for(let e=l[n],t=n+o;i>=t;t+=o){const n=l[t];if(null!=n){if(e>=n)return!1;e=n}}return!0}(e[0])||(e=function(l){let e=l[0],t=e.length,n=Array(t);for(let l=0;n.length>l;l++)n[l]=l;n.sort(((l,t)=>e[l]-e[t]));let i=[];for(let e=0;l.length>e;e++){let o=l[e],s=Array(t);for(let l=0;t>l;l++)s[l]=o[n[l]];i.push(s)}return i}(e)),e}let t=new Set;for(let e=0;l.length>e;e++){let n=l[e][0],i=n.length;for(let l=0;i>l;l++)t.add(n[l])}let n=[Array.from(t).sort(((l,e)=>l-e))],i=n[0].length,o=new Map;for(let l=0;i>l;l++)o.set(n[0][l],l);for(let t=0;l.length>t;t++){let s=l[t],r=s[0];for(let l=1;s.length>l;l++){let u=s[l],a=Array(i).fill(void 0),f=e?e[t][l]:1,c=[];for(let l=0;u.length>l;l++){let e=u[l],t=o.get(r[l]);null===e?0!=f&&(a[t]=e,2==f&&c.push(t)):a[t]=e}Ll(a,c,i),n.push(a)}}return n},qt.fmtDate=Xl,qt.tzDate=function(l,e){let t;return"UTC"==e||"Etc/UTC"==e?t=new Date(+l+6e4*l.getTimezoneOffset()):e==Zl?t=l:(t=new Date(l.toLocaleString("en-US",{timeZone:e})),t.setMilliseconds(l.getMilliseconds())),t},qt.sync=st;{qt.addGap=function(l,e,t){let n=l[l.length-1];n&&n[0]==e?n[1]=t:l.push([e,t])},qt.clipGaps=ct;let l=qt.paths={points:St};l.linear=Dt,l.stepped=function(l){const e=K(l.align,1),t=K(l.ascDesc,!1),n=K(l.alignGaps,0),i=K(l.extend,!1);return(l,o,s,r)=>rt(l,o,((u,a,f,c,h,d,p,m,g,x,w)=>{let _=u.pxRound,{left:b,width:v}=l.bbox,k=l=>_(d(l,c,x,m)),M=l=>_(p(l,h,w,g)),S=0==c.ori?xt:wt;const E={stroke:new Path2D,fill:null,clip:null,band:null,gaps:null,flags:1},T=E.stroke,z=c.dir*(0==c.ori?1:-1);s=O(f,s,r,1),r=O(f,s,r,-1);let D=M(f[1==z?s:r]),P=k(a[1==z?s:r]),A=P,W=P;i&&-1==e&&(W=b,S(T,W,D)),S(T,P,D);for(let l=1==z?s:r;l>=s&&r>=l;l+=z){let t=f[l];if(null==t)continue;let n=k(a[l]),i=M(t);1==e?S(T,n,D):S(T,A,i),S(T,n,i),D=i,A=n}let Y=A;i&&1==e&&(Y=b+v,S(T,Y,D));let[C,F]=ut(l,o);if(null!=u.fill||0!=C){let e=E.fill=new Path2D(T),t=M(u.fillTo(l,o,u.min,u.max,C));S(e,Y,t),S(e,W,t)}if(!u.spanGaps){let i=[];i.push(...ht(a,f,s,r,z,k,n));let h=u.width*y/2,d=t||1==e?h:-h,p=t||-1==e?-h:h;i.forEach((l=>{l[0]+=d,l[1]+=p})),E.gaps=i=u.gaps(l,o,s,r,i),E.clip=ct(i,c.ori,m,g,x,w)}return 0!=F&&(E.band=2==F?[ft(l,o,s,r,T,-1),ft(l,o,s,r,T,1)]:ft(l,o,s,r,T,F)),E}))},l.bars=function(l){const e=K((l=l||Dl).size,[.6,hl,1]),t=l.align||0,n=l.gap||0;let i=l.radius;i=null==i?[0,0]:"number"==typeof i?[i,0]:i;const o=ml(i),s=1-e[0],r=K(e[1],hl),u=K(e[2],1),a=K(l.disp,Dl),f=K(l.each,(()=>{})),{fill:c,stroke:h}=a;return(l,e,i,d)=>rt(l,e,((p,m,g,x,w,_,b,v,k,M,S)=>{let E,T,z=p.pxRound,D=t,P=n*y,A=r*y,W=u*y;0==x.ori?[E,T]=o(l,e):[T,E]=o(l,e);const Y=x.dir*(0==x.ori?1:-1);let C,F,H,R=0==x.ori?_t:bt,G=0==x.ori?f:(l,e,t,n,i,o,s)=>{f(l,e,t,i,n,s,o)},I=K(l.bands,Pl).find((l=>l.series[0]==e)),L=p.fillTo(l,e,p.min,p.max,null!=I?I.dir:0),O=z(b(L,w,S,k)),N=M,j=z(p.width*y),U=!1,B=null,V=null,J=null,q=null;null==c||0!=j&&null==h||(U=!0,B=c.values(l,e,i,d),V=new Map,new Set(B).forEach((l=>{null!=l&&V.set(l,new Path2D)})),j>0&&(J=h.values(l,e,i,d),q=new Map,new Set(J).forEach((l=>{null!=l&&q.set(l,new Path2D)}))));let{x0:X,size:Z}=a;if(null!=X&&null!=Z){D=1,m=X.values(l,e,i,d),2==X.unit&&(m=m.map((e=>l.posToVal(v+e*M,x.key,!0))));let t=Z.values(l,e,i,d);F=2==Z.unit?t[0]*M:_(t[0],x,M,v)-_(0,x,M,v),N=Pt(m,g,_,x,M,v,N),H=N-F+P}else N=Pt(m,g,_,x,M,v,N),H=N*s+P,F=N-H;1>H&&(H=0),F/2>j||(j=0),5>H&&(z=gl);let $=H>0;F=z(pl(N-H-($?j:0),W,A)),C=(0==D?F/2:D==Y?0:F)-D*Y*((0==D?P/2:0)+($?j/2:0));const Q={stroke:null,fill:null,clip:null,band:null,gaps:null,flags:0},ll=U?null:new Path2D;let el=null;if(null!=I)el=l.data[I.series[1]];else{let{y0:t,y1:n}=a;null!=t&&null!=n&&(g=n.values(l,e,i,d),el=t.values(l,e,i,d))}let nl=E*F,il=T*F;for(let t=1==Y?i:d;t>=i&&d>=t;t+=Y){let n=g[t];if(null==n)continue;if(null!=el){let l=el[t]??0;if(n-l==0)continue;O=b(l,w,S,k)}let i=_(2!=x.distr||null!=a?m[t]:t,x,M,v),o=b(K(n,L),w,S,k),s=z(i-C),r=z(sl(o,O)),u=z(ol(o,O)),f=r-u;if(null!=n){let i=0>n?il:nl,o=0>n?nl:il;U?(j>0&&null!=J[t]&&R(q.get(J[t]),s,u+tl(j/2),F,sl(0,f-j),i,o),null!=B[t]&&R(V.get(B[t]),s,u+tl(j/2),F,sl(0,f-j),i,o)):R(ll,s,u+tl(j/2),F,sl(0,f-j),i,o),G(l,e,t,s-j/2,u,F+j,f)}}return j>0?Q.stroke=U?q:ll:U||(Q._fill=0==p.width?p._fill:p._stroke??p._fill,Q.width=0),Q.fill=U?V:ll,Q}))},l.spline=function(l){return function(l,e){const t=K(e?.alignGaps,0);return(e,n,i,o)=>rt(e,n,((s,r,u,a,f,c,h,d,p,m,g)=>{let x,w,_,b=s.pxRound,v=l=>b(c(l,a,m,d)),k=l=>b(h(l,f,g,p));0==a.ori?(x=mt,_=xt,w=yt):(x=gt,_=wt,w=Mt);const y=a.dir*(0==a.ori?1:-1);i=O(u,i,o,1),o=O(u,i,o,-1);let M=v(r[1==y?i:o]),S=M,E=[],T=[];for(let l=1==y?i:o;l>=i&&o>=l;l+=y)if(null!=u[l]){let e=v(r[l]);E.push(S=e),T.push(k(u[l]))}const z={stroke:l(E,T,x,_,w,b),fill:null,clip:null,band:null,gaps:null,flags:1},D=z.stroke;let[P,A]=ut(e,n);if(null!=s.fill||0!=P){let l=z.fill=new Path2D(D),t=k(s.fillTo(e,n,s.min,s.max,P));_(l,S,t),_(l,M,t)}if(!s.spanGaps){let l=[];l.push(...ht(r,u,i,o,y,v,t)),z.gaps=l=s.gaps(e,n,i,o,l),z.clip=ct(l,a.ori,d,p,m,g)}return 0!=A&&(z.band=2==A?[ft(e,n,i,o,D,-1),ft(e,n,i,o,D,1)]:ft(e,n,i,o,D,A)),z}))}(At,l)}}return qt}(); diff --git a/docs/dist/uPlot.min.css b/docs/dist/uPlot.min.css new file mode 100644 index 0000000..a030d63 --- /dev/null +++ b/docs/dist/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;}.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;/*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 diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..edd2c35 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,139 @@ +<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width"/>
+ <title>Animal Forest Decompilation</title>
+ <link rel="stylesheet" href="dist/uPlot.min.css">
+ <link rel="stylesheet" href="style.css">
+ <link id="season" rel="stylesheet" href="">
+ <link rel="icon" type="image/webp" href="website_assets/logo.webp"/>
+</head>
+<body>
+<script src="dist/uPlot.iife.js"></script>
+
+<ul class="navBar">
+ <li><a id="titleLink" href="index.html"><img id="logo" src="website_assets/logo.webp">Animal Forest Decompilation</a></li>
+ <li><a class="extraLink" href="https://github.com/zeldaret/af/">GitHub</a></li>
+ <li><a class="extraLink" href="https://zelda64.dev/">ZeldaRET</a></li>
+ <li><a class="extraLink" href="https://discord.com/invite/DqwyCBYKqf/">Discord</a></li>
+
+ <li><img src="website_assets/grass_winter.webp" class="seasonLink" onclick="changeSeason('winter.css')"></li>
+ <li><img src="website_assets/grass_autumn.webp" class="seasonLink" onclick="changeSeason('autumn.css')"></li>
+ <li><img src="website_assets/grass_summer.webp" class="seasonLink" onclick="changeSeason('summer.css')"></li>
+ <li><img src="website_assets/grass_spring.webp" class="seasonLink" onclick="changeSeason('spring.css')"></li>
+
+ <li><button id="hamburger" onclick="toggleHamburger()">Menu</button></li>
+</ul>
+
+<ul id="hamburgerContents" class="hamburgerClosed">
+ <li><a href="https://github.com/zeldaret/af/">GitHub</a></li>
+ <li><a href="https://zelda64.dev/">ZeldaRET</a></li>
+ <li><a href="https://discord.com/invite/DqwyCBYKqf/">Discord</a></li>
+ <li id="seasonsHamburger">
+ <img src="website_assets/grass_winter.webp" onclick="changeSeason('winter.css')">
+ <img src="website_assets/grass_autumn.webp" onclick="changeSeason('autumn.css')">
+ <img src="website_assets/grass_summer.webp" onclick="changeSeason('summer.css')">
+ <img src="website_assets/grass_spring.webp" onclick="changeSeason('spring.css')">
+ </li>
+</ul>
+
+<div id="wrapper">
+
+ <div id="codeGraph" class="progressGraph">
+ <h2>Decompilation Progress</h2>
+ <p class="progressTotalPercent" id="codeProgressTotal"></p>
+ <table class="progressTable">
+ <tr>
+ <td>Boot</td>
+ <td class="percent" id="codeProgressBoot"></td>
+ </tr>
+ <tr>
+ <td>Code</td>
+ <td class="percent" id="codeProgressCode"></td>
+ </tr>
+ <tr>
+ <td>Overlays</td>
+ <td class="percent" id="codeProgressOverlays"></td>
+ </tr>
+ </table>
+ </div>
+
+ <div id="assetGraph" class="progressGraph">
+ <h2>Asset Analysis Progress</h2>
+ <div class="progressStats">
+ <p class="progressTotalPercent" id="assetProgressTotal"></p>
+ <table class="progressTable">
+ <tr>
+ <td>Objects</td>
+ <td class="percent" id="assetProgressObjects"></td>
+ </tr>
+ <tr><td class="percentPlaceholder"> </td></td>
+ <tr><td class="percentPlaceholder"> </td></td>
+ </table>
+ </div>
+ </div>
+
+ <div id="letterTop"></div>
+ <div id="ribbon"></div>
+ <div class="description">
+ <p>Animal Forest is a reverse engineering project to decompile the Nintendo 64 game Doubutsu no Mori into C code. The project also extracts assets such as models, textures, and animations from the ROM. The C code and assets can then be recompiled to create a 1-to-1 ("matching") copy of the game.</p>
+
+ <h2>Why?</h2>
+ <p>Doubutsu no Mori is the starting point of the incredibly successful Animal Crossing series and a historically significant game. By decompiling it, we can learn more about how it was engineered, and what limitations the original developers had to work with. Looking at the source code can help us understand exactly how a game mechanic works, or what causes a glitch to happen.</p>
+
+ <h2>Contribute</h2>
+ <p>Contributions are always welcome! See our <a href="https://github.com/zeldaret/af/blob/main/CONTRIBUTING.md">Contributing Guide</a> for how to get started.</p>
+ </div>
+ <div id="letterBottom"></div>
+
+<script src="progress.js"></script>
+<script>
+function toggleHamburger()
+{
+ var menu = document.getElementById("hamburgerContents");
+ if (menu.className === "hamburgerClosed")
+ {
+ menu.className = "hamburgerOpen";
+ }
+ else
+ {
+ menu.className = "hamburgerClosed";
+ }
+}
+
+function changeSeason(stylesheet)
+{
+ var season = document.getElementById('season');
+ season.href = stylesheet;
+}
+
+let month = new Date().getMonth();
+switch(month)
+{
+ case 11:
+ case 0:
+ case 1:
+ changeSeason('winter.css');
+ break;
+ case 2:
+ case 3:
+ case 4:
+ changeSeason('spring.css');
+ break;
+ case 5:
+ case 6:
+ case 7:
+ changeSeason('summer.css');
+ break;
+ case 8:
+ case 9:
+ case 10:
+ changeSeason('autumn.css');
+ break;
+ break;
+}
+</script>
+
+</body>
+</html>
diff --git a/docs/progress.js b/docs/progress.js new file mode 100644 index 0000000..6c9127f --- /dev/null +++ b/docs/progress.js @@ -0,0 +1,217 @@ +function formatDate(self, rawValue)
+{
+ if (rawValue == null)
+ {
+ return null;
+ }
+
+ return new Date(rawValue * 1000).toLocaleDateString();
+}
+
+function percentValue(self, rawValue)
+{
+ if (rawValue == null)
+ {
+ return null;
+ }
+
+ return rawValue.toFixed(2) + "%";
+}
+
+function getSize()
+{
+ // mobile layout
+ let newWidth = window.innerWidth - 0.06 * window.innerWidth;
+ let newHeight = newWidth / 2;
+
+ // min height
+ if (newHeight < 300)
+ {
+ newHeight = 300;
+ }
+
+ // full layout
+ if (window.innerWidth >= 900)
+ {
+ newWidth = 730;
+ newHeight = 370;
+ }
+
+ return {
+ width: newWidth,
+ height: newHeight,
+ }
+}
+
+let codeOpts =
+{
+ ...getSize(),
+ series:
+ [
+ {
+ label: "Date",
+ value: formatDate,
+ },
+ {
+ label: "Total",
+ scale: "%",
+ width: 3,
+ stroke: "rgb(85, 191, 59)",
+ value: percentValue,
+ },
+ {
+ show: false,
+ label: "Boot",
+ scale: "%",
+ width: 3,
+ stroke: "rgb(255, 99, 132)",
+ value: percentValue,
+ },
+ {
+ show: false,
+ label: "Code",
+ scale: "%",
+ width: 3,
+ stroke: "rgb(82, 146, 252)",
+ value: percentValue,
+ },
+ {
+ show: false,
+ label: "Overlays",
+ scale: "%",
+ width: 3,
+ stroke: "rgb(227, 172, 52)",
+ value: percentValue,
+ },
+ ],
+ axes:
+ [
+ {
+ grid: {show: false},
+ },
+ {
+ scale: "%",
+ incrs: [25,],
+ values: (self, ticks) => ticks.map(rawValue => rawValue.toFixed(0) + "%"),
+ grid:
+ {
+ stroke: "rgb(182, 235, 253)",
+ width: 3,
+ },
+ },
+ ],
+ scales:
+ {
+ "%":
+ {
+ auto: true,
+ range: [0, 100],
+ }
+ },
+};
+
+let assetOpts =
+{
+ ...getSize(),
+ series:
+ [
+ {
+ label: "Date",
+ value: formatDate,
+ },
+ {
+ label: "Total",
+ scale: "%",
+ width: 3,
+ stroke: "rgb(85, 191, 59)",
+ value: percentValue,
+ },
+ {
+ show: false,
+ label: "Objects",
+ scale: "%",
+ width: 3,
+ stroke: "rgb(255, 99, 132)",
+ value: percentValue,
+ },
+ ],
+ axes:
+ [
+ {
+ grid: {show: false},
+ },
+ {
+ scale: "%",
+ incrs: [25,],
+ values: (self, ticks) => ticks.map(rawValue => rawValue.toFixed(0) + "%"),
+ grid:
+ {
+ stroke: "rgb(182, 235, 253)",
+ width: 3,
+ },
+ },
+ ],
+ scales:
+ {
+ "%":
+ {
+ auto: true,
+ range: [0, 100],
+ }
+ },
+};
+
+let codeGraph = new uPlot(codeOpts, null, document.getElementById("codeGraph"));
+let assetGraph = new uPlot(assetOpts, null, document.getElementById("assetGraph"));
+
+fetch('https://progress.decomp.club/data/animalforest/jp/?mode=all')
+.then(v => v.json())
+.then(result =>
+{
+ const codeProgress = result['animalforest']['jp']['code'][0]['measures'];
+ document.getElementById("codeProgressTotal").innerHTML = (100 * codeProgress['all'] / codeProgress['all/total']).toFixed(2) + "%";
+ document.getElementById("codeProgressBoot").innerHTML = (100 * codeProgress['boot'] / codeProgress['boot/total']).toFixed(2) + "%";
+ document.getElementById("codeProgressCode").innerHTML = (100 * codeProgress['code'] / codeProgress['code/total']).toFixed(2) + "%";
+ document.getElementById("codeProgressOverlays").innerHTML = (100 * codeProgress['overlays'] / codeProgress['overlays/total']).toFixed(2) + "%";
+
+ const codeEntries = result['animalforest']['jp']['code'].reverse();
+ codeGraph.setData([
+ codeEntries.map(a => a.timestamp),
+ codeEntries.map(a => 100 * (a.measures['all'] / a.measures['all/total'])),
+ codeEntries.map(a => 100 * (a.measures['boot'] / a.measures['boot/total'])),
+ codeEntries.map(a => 100 * (a.measures['code'] / a.measures['code/total'])),
+ codeEntries.map(a => 100 * (a.measures['overlays'] / a.measures['overlays/total'])),
+ ]);
+
+ const assetProgress = result['animalforest']['jp']['assets'][0]['measures'];
+ document.getElementById("assetProgressTotal").innerHTML = (100 * assetProgress['all'] / assetProgress['all/total']).toFixed(2) + "%";
+ document.getElementById("assetProgressObjects").innerHTML = (100 * assetProgress['objects'] / assetProgress['objects/total']).toFixed(2) + "%";
+
+ const assetEntries = result['animalforest']['jp']['assets'].reverse();
+ assetGraph.setData([
+ assetEntries.map(a => a.timestamp),
+ assetEntries.map(a => 100 * (a.measures['all'] / a.measures['all/total'])),
+ assetEntries.map(a => 100 * (a.measures['objects'] / a.measures['objects/total'])),
+ ]);
+});
+
+function throttle(cb, limit)
+{
+ var wait = false;
+
+ return () =>
+ {
+ if (!wait)
+ {
+ requestAnimationFrame(cb);
+ wait = true;
+ setTimeout(() =>
+ {
+ wait = false;
+ }, limit);
+ }
+ }
+}
+
+window.addEventListener("resize", throttle(() => codeGraph.setSize(getSize()), 100));
+window.addEventListener("resize", throttle(() => assetGraph.setSize(getSize()), 100));
diff --git a/docs/spring.css b/docs/spring.css new file mode 100644 index 0000000..0f60eb2 --- /dev/null +++ b/docs/spring.css @@ -0,0 +1,9 @@ +body
+{
+ background: #14504a url(website_assets/bg_spring.webp);
+ background-attachment: fixed;
+ background-position-x: 50%;
+ background-position-y: 100%;
+ background-repeat: no-repeat;
+ background-size: cover;
+}
diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 0000000..25bd87e --- /dev/null +++ b/docs/style.css @@ -0,0 +1,354 @@ +@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wdth,wght@87.5,600&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
+
+*
+{
+ margin: 0;
+ padding: 0;
+}
+
+body
+{
+ background: #14504a url(website_assets/bg_autumn.webp);
+ background-attachment: fixed;
+ background-position-x: 50%;
+ background-position-y: 100%;
+ background-repeat: no-repeat;
+ background-size: cover;
+}
+
+.navBar
+{
+ width: auto;
+ height: 57px;
+ list-style-type: none;
+ background-color: #14504a;
+ box-shadow: 0 1px 10px #12214c;
+}
+
+.navBar a
+{
+ font-family: 'Arial';
+ font-size: 16px;
+ text-decoration: none;
+ color: white;
+ height:36px;
+ padding-top: 21px;
+ padding-left: 10px;
+ padding-right: 10px;
+ user-select: none;
+}
+
+#logo
+{
+ height: 48px;
+ float: left;
+ padding: 5px 9px 4px 5px;
+ margin-top: -15px;
+}
+
+#titleLink
+{
+ font-family: "Open Sans", sans-serif;
+ font-size: 20px;
+ height: 42px;
+ padding-top: 15px;
+ padding-left: 0px;
+ float: left;
+}
+
+/* hidden in mobile version */
+.extraLink
+{
+ display: none;
+ float: left;
+}
+
+/* hidden in mobile version */
+.seasonLink
+{
+ display: none;
+ float: right;
+ margin-top: 13px;
+ margin-right: 5px;
+}
+
+#hamburger
+{
+ color: transparent;
+ border: 0px;
+ background-color: transparent;
+ background-image: url(website_assets/hamburger.png);
+ background-repeat: no-repeat;
+ background-position: 50%;
+ width: 57px;
+ height: 57px;
+ float: right;
+ cursor: pointer;
+}
+
+#hamburgerContents
+{
+ background-color: #14504a;
+ margin-left: auto;
+ width: 100%;
+ list-style: none;
+}
+
+.hamburgerOpen
+{
+ display: block;
+}
+
+.hamburgerClosed
+{
+ display: none;
+}
+
+#hamburgerContents a
+{
+ display: block;
+ font-family: 'arial';
+ font-size: 20px;
+ color: white;
+ text-decoration: none;
+ text-align: right;
+ width: auto;
+ padding: 17px;
+}
+
+#hamburgerContents img
+{
+ margin-right: 5px;
+ float: right;
+}
+
+#seasonsHamburger
+{
+ margin-top: 13px;
+ height: 42px;
+}
+
+
+#hamburger:hover, .navBar a:hover, .themeList button:hover, #hamburgerContents a:hover
+{
+ background-color: #2c625c;
+}
+
+#wrapper
+{
+ width: auto;
+ padding: 1vw;
+}
+
+#letterTop
+{
+ width: auto;
+ height: 34px;
+ background-image: url(website_assets/letter_top.svg);
+}
+
+#letterBottom
+{
+ width: auto;
+ height: 34px;
+ background-image: url(website_assets/letter_bottom.svg);
+}
+
+#ribbon
+{
+ display: block;
+ background-image: url(website_assets/ribbon.webp);
+ width :69px;
+ height: 29px;
+ position: absolute;
+ left: 50%;
+ margin-left: -34px;
+ margin-top: -20px;
+}
+
+.progressGraph
+{
+ background-color: #e0fcff;
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 1vw;
+ border-style: solid;
+ border-color: #5292fc;
+ border-width: 1vw;
+ border-radius: 4vw;
+}
+
+.progressGraph h2
+{
+ font-family: "Open Sans", sans-serif;
+ font-size: 35px;
+ color: #335075;
+ text-align: center;
+}
+
+.progressTotalPercent
+{
+ font-family: "Open Sans", sans-serif;
+ font-size: 40px;
+ color: #335075;
+ text-align: center;
+ height: 50px;
+ margin-top: -10px;
+ margin-bottom: -5px;
+}
+
+.progressTable
+{
+ font-family: "Open Sans", sans-serif;
+ font-size: 17px;
+ color: #335075;
+ width: 130px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.percent
+{
+ text-align: right;
+}
+
+.percentPlaceholder
+{
+ visibility: hidden;
+}
+
+.uplot
+{
+ background-color: #e0fcff;
+ color: black;
+ margin-left: auto;
+ margin-right: auto;
+ border-radius: 4vw;
+}
+
+.u-legend
+{
+ min-height: 95px;
+}
+
+.description
+{
+ font-family: "Roboto", sans-serif;
+ font-size: 17px;
+ color: #323c32;
+ background-color: white;
+ padding: 20px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.description h2
+{
+ font-size: 20px;
+}
+
+.description p
+{
+ margin-bottom: 30px;
+}
+
+.description a
+{
+ text-decoration: underline dotted;
+ color: #323c32;
+}
+
+.description a:hover
+{
+ color: #e70029;
+}
+
+@media only screen and (min-width: 900px)
+{
+
+.navBar
+{
+ z-index: 10;
+ position: sticky;
+ top: 0px;
+ padding-right: 7px;
+}
+
+.extraLink, .seasonLink
+{
+ display: inline;
+}
+
+#hamburger
+{
+ display: none;
+}
+
+.hamburgerOpen
+{
+ display: none;
+}
+
+#wrapper
+{
+ padding-bottom: 24vh;
+}
+
+#letterTop, #letterBottom
+{
+ display: none;
+}
+
+#ribbon
+{
+ margin-top: 15px;
+}
+
+.progressGraph
+{
+ width: 860px;
+ height:678px;
+ border-style: none;
+ background-color: transparent;
+ background-image: url(website_assets/graph_window.svg);
+ padding-top: 25px;
+ margin-bottom: 20px;
+}
+
+.progressGraph h2
+{
+ font-size: 40px;
+}
+
+.uplot
+{
+ background-color: white;
+ border-style: solid;
+ border-color: #2c74fd;
+ border-width: 10px;
+ border-radius: 40px;
+ color: #335075;
+}
+
+.u-legend
+{
+ min-height: 0px;
+}
+
+.description
+{
+ background-color: transparent;
+ background-image: url(website_assets/letter.svg);
+ width: 560px;
+ height: 407px;
+ padding: 70px;
+ margin-bottom: 20px;
+}
+
+.description p:first-child
+{
+ margin-top: 20px;
+}
+
+}
diff --git a/docs/summer.css b/docs/summer.css new file mode 100644 index 0000000..5a2572d --- /dev/null +++ b/docs/summer.css @@ -0,0 +1,9 @@ +body
+{
+ background: #14504a url(website_assets/bg_summer.webp);
+ background-attachment: fixed;
+ background-position-x: 50%;
+ background-position-y: 100%;
+ background-repeat: no-repeat;
+ background-size: cover;
+}
diff --git a/docs/website_assets/bg_autumn.webp b/docs/website_assets/bg_autumn.webp Binary files differnew file mode 100644 index 0000000..a3a1c0d --- /dev/null +++ b/docs/website_assets/bg_autumn.webp diff --git a/docs/website_assets/bg_spring.webp b/docs/website_assets/bg_spring.webp Binary files differnew file mode 100644 index 0000000..8bc58f9 --- /dev/null +++ b/docs/website_assets/bg_spring.webp diff --git a/docs/website_assets/bg_summer.webp b/docs/website_assets/bg_summer.webp Binary files differnew file mode 100644 index 0000000..1259012 --- /dev/null +++ b/docs/website_assets/bg_summer.webp diff --git a/docs/website_assets/bg_winter.webp b/docs/website_assets/bg_winter.webp Binary files differnew file mode 100644 index 0000000..b0e6f1d --- /dev/null +++ b/docs/website_assets/bg_winter.webp diff --git a/docs/website_assets/graph_window.svg b/docs/website_assets/graph_window.svg new file mode 100644 index 0000000..345c8ef --- /dev/null +++ b/docs/website_assets/graph_window.svg @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="860.188px" height="702.475px" viewBox="0 0 860.188 702.475" enable-background="new 0 0 860.188 702.475"
+ xml:space="preserve">
+<path fill="#E0FCFF" stroke="#5292FC" stroke-width="18" stroke-miterlimit="10" d="M851.188,192.701h-0.005
+ C850.818,91.32,773.497,9.231,430.094,9l0,0c0,0,0,0,0,0l0,0l0,0C86.69,9.231,9.368,91.32,9.005,192.701H9v0.001
+ c0,0.13,0.003,0.26,0.003,0.39c0,0.11-0.002,0.22-0.002,0.33l0,0h0.004c0.108,30.252,7.071,58.783,25.909,83.94
+ c-16.191,23.494-25.023,48.842-25.023,75.276c0,27.16,9.324,53.173,26.368,77.207c-19.179,24.801-26.255,52.976-26.364,82.865H9.891
+ c0,0.106,0.002,0.212,0.002,0.318c0,0.129-0.002,0.258-0.002,0.385v0.001h0.005c0.361,99.284,77.553,179.696,420.199,180.062l0,0
+ l0,0c0,0,0,0,0,0l0,0c342.647-0.365,419.834-80.777,420.202-180.062h0.004v-0.001c0-0.128-0.002-0.256-0.002-0.385
+ c0-0.106,0.002-0.213,0.002-0.32h-0.004c-0.111-29.888-7.189-58.063-26.366-82.863c17.043-24.033,26.37-50.047,26.37-77.207
+ c0-26.434-8.834-51.782-25.025-75.276c18.838-25.157,25.801-53.688,25.909-83.94h0.005l0,0c0-0.109-0.002-0.22-0.002-0.33
+ C851.187,192.962,851.188,192.832,851.188,192.701L851.188,192.701z"/>
+</svg>
diff --git a/docs/website_assets/grass_autumn.webp b/docs/website_assets/grass_autumn.webp Binary files differnew file mode 100644 index 0000000..8ad84a9 --- /dev/null +++ b/docs/website_assets/grass_autumn.webp diff --git a/docs/website_assets/grass_spring.webp b/docs/website_assets/grass_spring.webp Binary files differnew file mode 100644 index 0000000..6e033e4 --- /dev/null +++ b/docs/website_assets/grass_spring.webp diff --git a/docs/website_assets/grass_summer.webp b/docs/website_assets/grass_summer.webp Binary files differnew file mode 100644 index 0000000..90cf291 --- /dev/null +++ b/docs/website_assets/grass_summer.webp diff --git a/docs/website_assets/grass_winter.webp b/docs/website_assets/grass_winter.webp Binary files differnew file mode 100644 index 0000000..f0c19c2 --- /dev/null +++ b/docs/website_assets/grass_winter.webp diff --git a/docs/website_assets/hamburger.png b/docs/website_assets/hamburger.png Binary files differnew file mode 100644 index 0000000..89c35b8 --- /dev/null +++ b/docs/website_assets/hamburger.png diff --git a/docs/website_assets/letter.svg b/docs/website_assets/letter.svg new file mode 100644 index 0000000..93d37f8 --- /dev/null +++ b/docs/website_assets/letter.svg @@ -0,0 +1,537 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="699.935px" height="546.935px" viewBox="0 0 699.935 546.935" enable-background="new 0 0 699.935 546.935"
+ xml:space="preserve">
+<image display="none" overflow="visible" width="482" height="374" xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAgEAMgAyAAD/7AARRHVja3kAAQAEAAAAHgAA/+4AIUFkb2JlAGTAAAAAAQMA +EAMCAwYAAA8RAAAeGAAAQdD/2wCEABALCwsMCxAMDBAXDw0PFxsUEBAUGx8XFxcXFx8eFxoaGhoX +Hh4jJSclIx4vLzMzLy9AQEBAQEBAQEBAQEBAQEABEQ8PERMRFRISFRQRFBEUGhQWFhQaJhoaHBoa +JjAjHh4eHiMwKy4nJycuKzU1MDA1NUBAP0BAQEBAQEBAQEBAQP/CABEIAXgB5AMBIgACEQEDEQH/ +xADSAAEAAgMBAQAAAAAAAAAAAAAABAUBAwYCBwEBAQEBAQEAAAAAAAAAAAAAAAECBAMFEAABBAIC +AAMIAwADAQAAAAADAQIEBQAREgYQExUgQFAhMTIUFjQ1NjBBIiURAAIBAgQDBgQDBgQGAwEAAAEC +EQADITESE0FRBBBhcZEiMlCBkhQgUjOxQmJyI4OhgqIVQNFDJEQFssJjcxIAAQMBBgMGBQQBBAMA +AAAAAQARAjEhkaESIgMQQSNAUWHhMlJxQhMzBCCBwWLwMHJzFFDRU//aAAwDAQACEQMRAAAAlX3m +6XnbGxJzF3MLy99KHNXcoc3cTBz9hYIpdlsqsiXyIEC+ESq6ARqPpRGqOgEWqv6ZudWwNufe6g1c +iriJX9U863XbL40siyFHYTBz9vJHNXkkc9byhz1nOVRSbRFPrvFVtf0QhUnUCHRdSSJznXeF4dIT +Vnf1Vtc8j1uMz2oeb+g4nRy/V+fV5+SqfoOM9VPcs64uD1d+z3x8S2uD59u7lnv9Ul3jXB8+tOsz +nt9cr1XnXJ8+6S8zOn1yXWw74fLM/SpOPp/LPP1PynAfTqu01x6OF+geWOC7zPpKvkfoXlvjuz8+ +75c1Q/QcTo57pPPq8nFQfoDPbWWXtrh4DHfM/Q1xLFr5/wA+mdmz3eud6LGuKtn7MXPEJiJnQVtl +c1Vr8++g57MjXEAAAAAAAAAAAAAAAAAAAAAAAAAhTeXnR0HupumOGWKSbdwp1zw3cMvYLzgAAAAA +AAAAAAAAAAAAAAAAAAAOc6PE9aiy2k4Vao89PAn686W6+ffQc92RrhAAAAAAAAAAAAAAAAAAAAAA +AAAQJ/LzovtlVdMcKuUmrpoU7WON7FmewXwAAAAAAAAAAAAAAAAAAAAAAAAAc90OJ61VhtM8GvWd +a+jiS9Y5/oPn30GduRrgAAAAAAAAAAAAAAAAAAAAAAAAAVtly86LzbV3TPAr9lq6SHN158p1WT0C ++OMYiTctE8zc3MLKTMRtZNQsrNxG8pLQ/Kz0XyktEwsxDykxDySkIs3MLYkhDws3MHYkrETBMQvS +zEMkzETys1DyTMR9STUIs5F8pLzC2koa8lFep6Vc7dhPnLqmdw+ohTdefMdP8++gzuyNfPAAAAAA +AAAAAAAAAAAAAAAAAAAVNty86bqRVXTHz907NruqiTNeXNdJk2F8sYxEm5aJ5m5uYWUmYjayahZW +biN5SWh+VnovlJmImFmIeUmIeSUhFm5hbEkIeFm5g7ElYiYJmYPpZiGSZiJ5Wah5JmI+tJiEWci+ +Ul5hbSUNeSluk9K2ZuWfOnUs+sTpoku+PKdX8++g57sjfzwAAAAAAAAAAAAAAAAAAAAAAAAFPccv +Om5k1V2x84dUah9REmPHnugybC+WHmFNz0DE3YK/KT0PUWKuytgh+Enq/wArZIXlJ6BhbBX5J6B6 +SarsrYIHtJiB5WxV21JiAJ6u9LPQCT0Hwtir8k9D1pYK7K2CH4Ser9xLGvJT3Cbrpe5Xzd1iekLq +IM6+XHdj8++g478jfzgAAAAAAAAAAAAAAAAAAAAAAAAFJd8vnqtptTdXHz50CI3VQZ+vGju2ZsL5 +4eYU3PQPM3Yq/KT0PUWKuytgh+Enq/ytkheUnoGFsFfknoHpJquLYq/YkxA8rYq7akxAE9XelnoB +J6D4WxV+Seh60sFdlbBD8JPV+4ljXkqrU3B378J8ydib93MSXfLie2+ffQfP6WRv5oAAAAAAAAAA +AAAAAAAAAAAAAAChvuXz1Wk+purjjVw14a76vsZai2ZmguMPMObnIGJuwQMpOQ9ZYK8tgh+Enq/C +2KH5ScgYWwQBPQcpNV2VsEDYktA8rYq7akxBwT1d7WcgknIPlbBX5J6JqSwV2VsEPwk9A3Eoa8ld +Y4bi7thOOWq5zd1tknCd38++g+f1MjfzAAAAAAAAAAAAAAAAAAAAAAAAAHPdDy+euxsqm6vnyCc1 +4bruuskrLLz6npkMYeYU3PQMTdggEnoessFdlbBD8JPV/lbJCwk5Awtgr8k9Byk1XZWwQPaTEDyt +irtqTEAT1d6WegEnoPhbFX5J6JqSwV2VsEPwk9X7iWNeSBPw1G27Cca2pi3tq6x1eC735/8AQPP6 +mRv5gAAAAAAAAAAAAAAAAAAAAAAAADm+k5fPXPs6q5vnxCSzy31jXWPpuFN17JvIYAAAAAAAAAAA +AAAAAAAAAAAAAARJfhrX694TkHpnzvbKmud74Dv/AJ/9A8/p5G/mAAAAAAAAAAAAAAAAAAAAAAAA +AOZ6bl89c63p7e45RCOXqLOhvtajyI0mby8eGdzXsDX5Nzx6TLVhrcwZy0mtzGGfTTlraeGfbT7X +2aja07D0xqNzTtMsajc1bDLx4NzXsDX5Nzx6TLVhrc85Zy0mtzGGfTTlrbo3aj1nHlOURWddLa8/ +0O8cj1uWfei5z6AnvynVl5+VqfoCdNFeZXk4bR9AZ7oXuU3wfP8AZ3jHf5qbhrh+fz+xZ7HNdK1y +fP77ok6XF9ovj8/6y0T018J35ngO43iBx30AvD9vlcUXOfQE9uU6svPylV9ATqo7vK8fDaPoDPdX +yd7fBwGe+Y79dfaNcPz+b2bPbjneja46uy9aGeVVaS+sT0zomkV9oFVbBU24VdoFXYhCkhrwG+IR +NrSratIsqwLOtCxgBLjBtyGqQJFmi1toWVtkRVWhbW2BECYVH9B6jBOrwsaIlvucI6Dng1iX/9oA +CAECAAEFAPZ1ms1mkzSZpMVEzSYus23NtxNLmk3pN6TNZpM1ms1/wImsVqqqJpFau0TScFz/AK4L +iptEau1TaI1UVybTgucFxjVTHIq41FTFaqqiaxWqqomkVi4n04Ln/XBcVNon0xMaqr725dIi7T54 +3ekTXvaptETSfPwaqr725dInzT54mImve1TaImk0uJ9Gqq+9uXSJ800uJ9ERE8F3m3a27Nuzbs27 +Nuzbs27Nuzbs27Nuzbs27Nuzbs27Nuzbs27NuzbsRXeCptET5axMaqr725dInzTjiYiIngu827W3 +Zt2bdm3Zt2bdm3Zt2bdm3Zt2bdm3Zt2bdm3Zt2bdm3Zt2bdiK7wVNoiaTWJ9Gqq+9uXSJ9NYn0RE +TwXebdrbs27Nuzbs27Nuzbs27Nuzbs27Nuzbs27Nuzbs27Nuzbs27NuxFd4Km0RPFi+9u+ifTing +iIngu827W3Zt2bdm3Zt2bdm3Zt2bdm3Zt2bdm3Zt2bdrbs27Nuzbs27NuxFd4Km/Ffqz3t32t+m8 +3vNIngu827W3Zt2bdm3Zt2bdm3Zt2bdm3Zt2bdm3Zt2bdrbs27Nuzbs27NuxFd4Km/Ffqz3t32t+ +nzxfrpPBVXNu1t2bdm3Zt2bdm3Zt2bdm3Zt2bdm3Zt2bdm3a27Nuzbs27NuzbsRV9ja4u9s97f8A +a36bTF3v33k3Hb2z3t/2t+mO3v33/wA4v1RNYrEVUTSKxFVE0nlpmvl5aYqbRGImKm0RiIqoi4jU +TFai4jUTFai4jUTFYiqiaRWIqomk8tM18vLTFT5cExU2ifJNpi6z5Yus+WLrPlny9hP+H5ex8vD5 +b+WLi7xd58s//9oACAEDAAEFAFXN5tc2ubXNrm13tdo5UXku0cqYjlTNrgRvK/8ACkoqwpKKcBQZ +tdcl1tdK5VxXKuKqriqu9rva72ubXN4i58sX6nM0iR5ohDkmaUgp4mMMRCESxCic08xbEKoEiDIW +eJ7I5miJImiKOGZoZDrYXL1UXKwlMkPiymBSVJYbI0wYmSjtM8E4QxyCtKRlgJrSPR5PUQ6Y9Gkf +YBc0BWiIR6Pf8sX6zY7At96iCaU8hiDN8sdrciS4ye9RzKEpSeYTbcRFTJscYm+9QxNKeQxozbbj +vqeS8ye9AMoSFIpCbTHLtZwBib71DG0kiQxrD8kx67caQ8yY1BqnEPLiHOIc4g3xFriDaNDriHEa +HFaHXEG+ItcQZxDnEOcQ74h1xDviHOIc4g3xFpzRImBK4RCEUhOaY5drPCMbfeoQ2kkSWNYfkmOX +alOQqY1GKnEO+Ic4hziDfEWuINo0WuIcRocVodcQb4i1xDnEOcQ5xDviHXEO+Ic4hziDfEWnNEiY +ErhEIRXv549dusBMY33qExr5MlrWH545dqU5CpjUYqcA74hziHOAd8Ra4A2jRa4BxGBxWi1wDviL +XAOcQ5xDnAO+IdcA74BziLOAd8Rac0SJgiuE973PfzXHKqrYjY1nvUFjXyZTUZI8x2Ku1IYhExqM +VOAd8A5xDnAO+ItcA7RotcA4jA4rRa4B3xFrgHOIc4BzgHfEOuAd8A5xFnAO+ItOaJEwZHDe97nv +8zEyxa1rPeoDWulSmo2RippXlI9MajFTgHfAOcRZwDviLXAO0aLXAOIwOK0WuAd8Ra4BziLOAc4B +3xDrgHfAOcRZwDviLTmiRMGRw3Pc57sTLJERnvUBEWVLREkYmPIR6Y1rFTgLlwFnAWcA74C1wDtG +C1wFiMFisFrgHfAWuAc4CzgLOAt8Ba4C3wFnAWcA74C05o0TGPcxznOc7liZZIiM96r0RZUxE/I/ +84mPc9fe2Oc1zlVV/wDOJln9nvVf/Lmfyfl4P54jXLioqYjXLmtZwd4cHeHF2IiritcmIiritVMR +FXFRUxGquKipiNcuKipiNcuKms4O8ODvDi7GcuTuW/8A14HkvMgLEwRyJDzvFZmGwxXFI22OiKRy +kW2OqCK4ZH2h3sAdwSGsjFHGkvjukTynZGmEj5JmkkJGnFjskyiSFBYmCORIed4rMw2GK4pG2x0R +xHOJ6ufTCOYR9qdzQGcEhHqR+l9lPaX2/wDvPnie2ms+WJrPnn//2gAIAQEAAQUATyLuPTUI4Ma/ +64+W2BTxoYLjqxJEuPVxY4ZnViktGV0VgW9XIls+vjPFX9aIKfKrY0kNL14kZ9jUhlx6SkWKC4pk +lx6unHEj3lA+U2BUx4YLSgIc0eujAFK6699ikCMgmdXe21fAjPFB688U09fHOKqoCANKrY8kVPRE +jrNqwSw0tI+MOwqgywUtK+MKzqWS49TTtigu6NZYaynDCj29E+RkatjxwzevPJObXxmiXq70tHQI +zhROuPHYSK6McVV10gDzaqPLDSUL4rbWmHNj0tIkSNeUSywVtOGHGvOuEkuiVUaKGz6qU1gOriDC +7qb1tn1kR4Q9aSFO/YY/qXX6iPFH712G7JUsF3gXGX3SKkeD3ZSHN3eE18Ht5ZVh70UQzM/W43rf +XrsMhckWFyO8TeuzzLGKP1nsOdcsLiRMzsVlcAn+s9hzrEqfJjOVUbLuOwNkpc9h3VlOaBbFkBge +s9hyHb9gdJbvXZ5dhGj+s9hzr9lcnsM7LYW8aZ6z2HOrzLGUzO9/a7W2sejRtcrdayj/ALbJjyMi +vuew8hXHYVIBz3B7DJmR4PrPYcpbS7NPztE60jG9Z7DnV51pJJlxa3grD1nsOdckzZMA6vaEtx2F +CMuew8obyEi3x5YK/wBZ7DlTa3hJ2dom2kZ3XzSj10iQKMH9lZ6tRVoIoM8tnLHNa7PJFjWMb4OG +xy+SLGta1MUQlXyRYiIiKiKnkixBDRcc1rk8kWNGxq45jHZ5Isa1rfCwq4li0fXKgbX1kAgw09aF +5qSrM8HXayPJ8PJFnlCTwVqOTyRYgxtXHMY7PJFjWMb4KMbl8kWI1GpnlCXPJF4KiKnkixBDauOY +1yI1Go9jCN/X4vrnXrth34XsUIU5Pn8Ws7SPWigzQzo8mSKKD9k/+pS14YsfJv8ApG/b8V7v/F6l +/UOa1yehxPXaO280mS6+W7sSfT4r3GOY0Tq4SBqpUgcUH7ET1KpjCDHzSfF1RFzSJiojk9Gieu9Y +vSHNhuzRRWKLtPitrbAqxV84U+NKksig/YTeo00YQImTf9I37fivd/4vUv6jPSovrvV7eQY+SqSw +JfJ8k+K9uhSZcXrUY0arlH/HB67J9QqYwo8X436fG9f69YzXycP2hRWqLtPit1cNqg1dgyxiSzvj +x/V5nqdWIY4mTf8ASN+34r3f+L1L+oz8OP8AsfWpk55MkdcmlukTSeCqiJ5g1RCDXFINFUg0xSMR +EexcQg1XzB7Ug0zmzSEGuIQa55o9q9iZ5jNIQa4hBqqkGiqQaZ5jNI9i55o9qQaKpBpnNmkINc80 +aqpRoqvYieYzSEGuIUaqpBpikGieYzSEYueYPakGmKQaYj2KiEGueaPakGmc2IiEGuIQa4j2u9jt +NbKnxuvQzwq2WUwY/q1l+fUjEOH7JRtKNlTGYKPURo7yU0UhTU8UykqYxBhrI4GDpogiOpojimp4 +pnOrY7ghqIwWip4oXrSRFJIrI8hPSY3kiqIwmBp4oSGp4piGqIpmuqYzgx6uPHRtLEaUtNFKU9RF +PiVoEACoigVlLEYQlLEIQ1ZHMNlTGYINRFDg6WIMkipjSHEqYxBpUxkFHqo0dVpoilNTxTPLURSt +FWgEIVPFE/0WJ5pqeKZ762O8IqiMJgaeKF0WAGK/2vxYvr/VT2TjZI7LKHcou0+K31u6qBUWHqMK +U84wflWnqFQ0TYWTf9I37fivd/4vUv6jOMf17qjrXz8kdYMW2RNJ4KqInmMVEINcUg0VSDTFIxER +7FxCDVfMHtSDTObNIQa4hBrnmj2r2JnmM0hBriEGqqQaKpBpnmM0j2Lnmj2pBoqkGmc2aQg1zzRq +qlGiq9iJ5jNIQa4hRqqkGmKRiJ5g9IRi55g9qQaYpBpiPYqIQa55g9qQaZzYiIQa4hBriPa72Oy1 +Uiyj0MAsCvlrJbH822/OqkGkP2SjaUbKmMwUepjR3kpopCmqIp1JUxiDDWRwsHTRRkdTRXFNTxTO +dWgcENTGC0VPFC9aWIpJFZHkJ6TG8kVTGEwNPFCQ1PFMQ1RGM11TGcGPVx46NporSlp4pSnqIx8S +tAgAVEYCspYrCEpopCGrI5hsqYzBBqIwcHTRRkkVMaQ4lTGINKmMgo9VGjqtNFUpqeKZ5aiMVoq0 +AhCp4on+jRfNNTxTPfWgeEVTGEwNPFC6LADFf7Wo37D1lLXz8kdiniu0XafFew25asFLYOsYMtZK +R+Vv+fVKJYeTf9I37fivd/4vUv6jOUX9i6uC1Q+H6v5tqiaTx2mbTNpm0zaZtM2mbTNpm0zaZtM2 +mbTNpm0zaZtM2mbTNpm0zaZtM2mbTNpm0zaZtM2mbTNpm0zaZtM2mbTNpm0zaZtM2mbTNpm0zaex +2KoNaR6SvJXQJaSlj+Tceo1bhuieyUbSiZTxmCjU0aM8lJFIU9LGOpKeMQQKoABipIoiOo4rinpI +p3uqwOAGmjBaGkihetFEUkmqjyW+jxvJDTRgsBSRQEPSxTkNTRjNdTxnBjVMeMjaOK0paSKUp6WM +fG1YGxwU0YCjo4oyEo4pCHqwHGynjMECmjAwdHFGSRTxpLyU8Yg0p4yBjVEaMq0cVSmpYp3mpoxm +irACEGkihJ6HF809JFOQlWAgRU0YLAUsYDoleGI/2vMjfsHVY9o42SbyyHfJ80+K9ltZNbHop5bC +vksO8GrD1KmOI8HJv+kb9vxXu/8AF6l/UZ+ZG/Zup1k0Z8N1iOWyRNJ47TNpm0zaZtM2mbTNpm08 +Npm0zaZtM2mbTNpm0zaZtM2mbTNpm0zaZtM2mbTN5tM2mbTNpm0zaZtM2mbTNpm82mbTNpm82mbT +2b2nW1BUV3psKYI5o3o1n6lTmGWF7JRtKJlPHYKNTR4zyUkYhT0sY6kp45BAqgAGKkjCI6jiuKek +jHe6rA4AaaOFoaSMF60UVSSaoElvo8fyA00cLAUkYBD0sY5DU0YzXU8dwI1SCMjaOK0paSMUp6aM +fG1YGxwU0YCjo4oyFo4pSHqwHGynjsECmjAwdHFGSTTx5Ly08co0p46BjVEeMq0cVSmpYxnmpoxm +irACEGkjBJ6HF809JGOQlWAgRU0cTAUsYDoleGI/2vOD+wU9a4Rsl2k9nYU+nxXtVjKgxuvTDza2 +QJxg+lS/y6yQM8fJv+kb9vxXu/8AF6l/UZ+fH9Z69WvYuF65CLPT5J48m5yaucm5ybnJM2mcm5yb +nJubTOTc5Nzk3Nomcm5ybnJucm5ybnJubRc5Nzk3OTUzaZyaucm5ybm0zkmcm5ybnJqZyTOSZyRc +5Nzk3OTc2mcm5ybnJubTOTc5NxFRfYuKcdqGsrx10WQHzw+jn/OpZYpEXOSb8SjQomVAWCj1AY7y +UwCFPTgOpKgJBArBAGKlAIjqUDinpgGc6tE4AagIWhpgBetJHUkmrDIb6QHyQ1ARMBTAAQ9OA5DV +ATNdUBcGNVhjo2lA0paYBSnpwHxtaJoAU4AKOlAwhKUBCGrBGGyoCwQKgAcHSgGSRUBkPJUBINKg +KBjVIYyrSgUpqYBnmqAmaKtEIQaYAieig809MAxCVgnhFUBEwFOALokAcV/tepR/WaSpaEmTJclv +Y2/T4r2+ZIjResyDSKuQBsgPohPz6WwDLBk3/SN+34r3f+L1L+oVURPWYnrVLSCjyMLQQCzfY2mb +TNpm0zaZtM2mbTNpm0zaZtM2mbTNpm0zaZtM2mbTNpm0zaZtM2mbTNpm0zaZtM2mbTNpm0zaZtM2 +mbTNpm0zaZtM2mbTNpm0zaL7FpUgtBQIIYEaQBsgPojvUqaeKVHzzB8vEo/NEyqawUaqbHeSnaQp +6lplJVNIIFa0IxU7RkdTtcU9Q0z3VzXADVNE0NQ0T1pmKSTWtkN9Kb5IapomAqGhIeoaYhqlpmuq +muDGrGx0bTtaUtQ0pT1LTY2uakcFS0Kjp2sISnaQh61phsqmsECpaHB07RkkVTTuJVNINKpqBjVb +Y6rTtUpqhpnmqWlaKuaIQahoiejt809Q0xCVzXhFVNEwFS0LokFIr/FVRMRUVHOa1P2SH6/TVQo6 +ZMkGTsjft+K9yOYMXqxSFqpcUMyP+sJ+fU2ATsyb/pG/b8V7v/F6l/UPewbf2ON69WVgArhKevJK ++LT62LYDiRAQwyI4pIf1aP8Am1ViGQ3FlR0J8WIUYmsex7SEGJn7TD9Yp68QB5Me9OyN+34r3V7m +xepuc6oKIZh/rUH1OkuAHXJqL+yN+34r3f8Ai9S/qDnFHF+0i9apK8McOOrobz/FpMSPLYEAgDIN +hWfq0b1vr92KQ7FsYTZGPKMafmRcYcJFx5wjX8yLjCDImOlRmr+XFxHI5FVET8uLiSozlx5GDT8y +LjJACLhDCFn5kXBlETwfIAxfzIuMex7cWXGRfy4uIu8c5rU/Mi42VHcuPIMafmRcYcJFx5wjX8yL +jCDImOkx2L+ZFxrmvT6Z+XFTElxVXHPaxPzIuMkx3rkmXHisAcUgcmSGID9wZ6lQ1seLHw9DZlu2 +oqJ2asmzxfrF7nXqS1hzc7HSWcyb+sXudYrZ0AS/Sd1u6JL/AFi9ynjHi19iAkiEvWL3cfrV4w40 +VrOx18udE/WL3KSht41hnZqiynSP1i9zq9TYQH5bdfuZE/8AWL3OvwpMGvM1zwv6zeq5vWL3cMTw +xbqNIl1/6xe5A65dDlomk7NWzp4v1i9zrtJaQ5udipLSZO/WL3Os1k2AFfpO65dllfrF7lJEPDr5 +gyFiu6xe8h9ZvEeBjhh7BClTYP6xe5UdetwTs7PU2Fg7r0ORCr3sYRv67D/Yeu3LTP8AjUuUKHH/ +AGt3qv5AquDV3LJEe5u3hHCtgSA2d6VhwWUcw5l6ds5ljGeN9+ZLNbCMg414VZxbCOMddeFceVZB +AKpuHlbYWjAAq7bzgWlt5Q4VmM4ba3KNItmA4rS7MOSKeAgz3RGzknR1G27J+es6Og4dyV0s9gAI +622I90u0CAVTauIGxs0CCvs0MGztVGyJZCMKxtnscCxAYc66MyYycB433RUnLPjoMN4T80lhHGOu +uivPJsgAHV3BHJNtBBDVW6lFa2vkx662ZIBc3RAsiWgTitLsozBtYpRTO0mS0bZxXBXtcn1t9zCY +GF2o5LP0yu9b/9oACAECAgY/AP8AVc8k6dOE6f8A1jaqsquqnh6jw9R4VKqyq6I4VRdVZVdVZVdV +XevUeHqPD1Fd36D4drJQPC1Hx7WyZfvwPge1kocT49rZMv34F+/tZKC/fh8eFgVBeqC9UF6oL1QX +qgvVBeqC9UF6oL1QXqgvVBeqC9UF6oL1QXqgvVBeqC9UF6oL1aBfwZMq8+B+Paygq8+FnCwKgvVB +eqC9UF6oL1QXqgvVBeqC9UF6oL1QXqgvVBeqC9UF6oL1QXqgvVBeqC9UF6tGPBky/fgfj2socbOF +gVBeqC9UxVBeqYqgvVMVQXqgvVMVQXqmKoL1TFUxVBeqYqgvVBeqYqgvVMVaMeDJuJ+Payh+mwKm +KpiqYqgvVMV6ReqYqmKpiqYr0i9UxVBeqYqmKpiqYqmKpiqYr0i9UxVox4N+g/HtZQ4W8bAqYqmK +piqYqmKpiqYqmKpiqYqmKpiqYqmKpiqYqmKpiqYqmKpiqYq0Y/qPx7WUP02B1TFUxVMV6cVTFenF +UxVMVTFUxXpxVMV6cVTFUxXpxVMVTFUxVMV6cVTFWjH9FeatR+Payh8Fgre3U8Faj8e1lDhb/wCA +PinTJ0y5plzTLmmTq1OrVZwsTpk6Zc0y5plzTdusVnH/2gAIAQMCBj8Ab9Tp06fgW58BtwtlOn7W +rJkNpbwWXI7h7KIR3A2YZgmTJuFvLg6dOn/R+ysUBHbjt5I5S3zeKEJbENwj5jVZ4wjtBmyxUYH8 +fbkYhsx5qUxEQEj6RQID/q7dg/zks+UNnzZeXwRH/V27R/nJRnKImIn0mhUoj8bbiZBn7sFnlCO6 +GbLJGEdiG2T8wqFDck5jF3bxDIEQkQ9qGiWVv3UJQBGWDF1IS2o7ubnLko5NqO1l9vNZJbEN0u+a +VUJR247TBmiowl+PCZj8x5ozjAbYPyxooxP422coAf8AwKUxERBk+UU+Cb/q7eH/AKQmYggSzZeX +wRiPxtsOGf8AwITlAbgHyyopTAyiRfKOS/bhsmD9TbzSfv7XDbl6ZG1lOApCZAX7IstsGIj9KGWz +n2uO4BmyGiluENnkZMqckxLrYMAR9TbzS+Pa4bc/TI2rchH0xmQFTlw2xIAfSjlDdrjuRAJgaFS3 +CGM5ZmVPlRWwYDLn23l4nte3CYeMjatyERpjMgKnyolmUBNunHKG4apGJfkHsXrmzVy870NcvHT5 +o65f101xQ1zpbo80dcnezTyvR6k2/wBnmg85O9unzR1z8NNcUHnLx00xRacnewZfNeubN7Od6GuT +vbp80dc/DR5oa5f200xR1y8NPmvXNu/L5r1yr7eV6OuTcjl80NcvHTTFHXL+umuKGubN7Od69cnf +28r0cs5HueLfzwjuRYmJe1S3JVnLMV6fl4bBhERz7bybme17cJh4yNoW5GIaMZkAKnyoqAmX+nHL +H4cNUjEvyD2L1yZq5fNDXLx0+aOuX9dNcUNc6W6PNHXJ3s08r0epNm9nmg85O9unzR1y8NNcUHnL +x00xRacnewZfNfcmzeznehrk726fNHXPw0+aGuX9tNMUdcvDT5r1ybvy+a9cq+3lejrk3I5fNDXL +x00xR1y/rprih1Js3s53r1yd/byvRyzke54t/PCO5H1Ro6lOVZHMV6flR5L8cwiI5tpy3Pte3CYz +RJtC3IxDRjMgBen5XRURMv8ATjlj8OGqRiXoIvYm+pJmrk/h0OpLx0UxR6krPTorjYh1JUt0eaPU +k72aOV6PUk3+zzQfck72jJ5o9SVlNFcUOpK2uimKLbknewZPNfckzezneh1JO9oyUxR6kvDRXFDq +Stropij1JeGiuKA+pJu/J/Dr7kq+zlej1JNyOTzQ6kvHRTFHqSs9OiuNiHUkzeznevuSd6ZOV6OW +cpd2hnx4R3IeqNEZy9UjmKp8rq1fjmMRHNtWsGfte3GQEok2grcjEMIzLBUHp4RE5Zsgyx8Bw1SM +S9BF7E31JM1cn8Oh1JeOimKPUlZ6dFcbEOpKlujzR6knewZOV6PVk3+zzQfck72jJ5o9SVlNFcUO +pK2uimKJG5J3sGTzX3JM3s53odSTvaMlMUepLw0VxQ6kra6KYo9SXhorigPqSbvyfw6+5Kvs5Xo9 +STcjk80OpLx0UxR6krPTorjYh1JM3s53r7knemTlejlnKXdoZ8eAnAtKNEZyLykXK5Ux4fj5QA+1 +a3a9uMgJAmhW6IhgJlgOBURORlkDRfkOGqRiXplexN9STNXJ5odSXjopij1DZTRXFDqSpbo80eoX +ezTyR6kmb2eaD7hd7Rl80epKymiuKHUlbXRTFEjcLvYMvmvuSZvZzvQ6hd7Rloj1JeGiuKHUNtdF +MUeofDRXFN9STd+TzX3DX2cr0epJuRyeaHUl46KYo9Q2U01xQ6kmb2c719wu9MvJHLMy7tLPwE4H +LKNCjKReUi5PH8ZgB0u17YIcPzW6AGGc8YicjLKGi/IcNUspejPYm+pY1cqHUrXTRHqUppqh1eVu +hHqWvTKj1SzexB9y17RlR6lKaaodStdNET9S16ZV90s3s5odS17Rloj1fhoqh1K100R6nw01TfUs +78q+5z9vJEfUs5HKh1K100R6lKaaodU09i+5a9MqOXczNTSz8BKByyFCEZSLklySq8+H4zf/AC7X +tA22rdHLPJYcI5zIsNObu8O1gwJEhQiqJk5Jq6w4fjf8Xa9r4rd/5CqcI581NObu8FYCVbYrAbla +vSbuHpN3ChuVlqtBH7KwOrQQrASrQR8VYCVaGVgJVoZWA3K1ek3cPSbuFDchkfNyy1RzPme16uq8 +uEBIRH045QyG3GMCB3i1Z5iILNpsUYCO2REMHCluSABl3UQGXbsDUX1LM2bP4PVNl27fBDcizxL2 +0UoGO20g1O9DcgASLNVLVLblGAEqsLUZQESZBtQdZJiADvpClkETnZ8w7lETERlt0hGEBAgl9QUT +MRGUMMoQ24xgQO8WrPMRBZtKjAR2yIBg4UtyTPLuogMu3YGojuFsxlm8HTZdu5DcDPGWa2iMTHb1 +BqIbkQCR3qUzWZct2239H//aAAgBAQEGPwC90tu89n/2ll3VNLsNYDGONKL7vd6hh/UZ3Y48hjS3 +ehuXLV8MNQV2AK8eNC3L3Hga3dmJJ86s3eivXbSM39dRcaI5jGhZUMwAglmYk/41aexfup0TY3VF +xsCOAx41shTpiMWM+c0XN+79hGoJrb3flzyrZIOmIwYz5zV+51N67c6ZCPt1LtjPPHhRtNqWcmVi +CD51cu9bduXXDkWlLmNIybPjTIrNbuAHQ6sRB86FzrHe51JmdTkhRlAxpj07Pb6hR6CrEA9xxpd1 +nu32H9RmYnHkMaFzorj2r0jUFcgFfOhbBa40DU7sSSfOrbdJduW1Zv6oDnLmMaFpQzAZlmJJ/wAa +tXLF64nSnG6oc4EcseNbWk6SI9xnzmmc37p6CNSprb3flzyo2iDpIjBjPnNXXv3rj9Op/oqXOPjj +wo22DAHirEEf41dudZduXArxYBcwVGIY40bbak5MrEEHzp7vW3HuOHItguY0jI58aNuWttB0urEE +Hzrd613uXyTALkhRiBx40UVmtXADodWIg+dC51jvc6g8C5IX/GmFpmt3gPQysRj50rdQz3OoYess +xIHcMaL9I72uoGWlyAR50qMz3LpA3HdiZPnSN0dx7bFv6gDmI4nOhaGpozZmJJPnVq50924lgn+q +oc4eGNbUErESWM/toOt+79iRqKa2935c62iDpiPcZ85q4969cfpVxtKXOPjjwo2mDKDkVYgj/Grl +zrL1y4qsRZUuYK8CcaNslrbQdLqxBB86a51tx7t7UQoLkgLw40wtM9q+AdtlYjHvxpW6pnudSw9e +pyQO4Y0bnRu9rqVjTpcgN3HGlR2e5dIG4zMTj51bu9DduWnLAXVDsBpOZzoWhqeBizMSSfOrNzpb +9230zH/uFFxsB3Y0LKqxWIksxPnNK637o6CNTJuN7vy55VslW0RGDNPnNX+q67qrv+32vVbBuNjP +PGt31fYbm1EnLT7s6fqSoa/euOxfiBqOA/4uwbaB2vFs+AWP+dDdsnXxjKg/SLquT6lbgKA6q2Es +/vMMxUWrbMvOrXTbQFq84QHj6jA/4s27ih0bAqcQa+2gfba97R3RlVzoLmF+1ccLhgRqPZsIWNgu +IWMNNCc6t/ZSNXuYCa99z6aKdUWa1GJYRHYbfSlltACComa99z6auN1skhvSSIJFEjOMKugM6gMQ +AFwia97/AE1ZudQIusvqq9c6YTeVfSO+vfc+mrasXZSwBBGEUJzirZ6KQSfUQJIr33PppbfUlmsk +HVqER2KnSFltaZlRMmvfc+mrv3skKRoZhHZ0Xjc/+nYxKnScjGFMFBJOAAqDgeRrov8A+yf/ACHZ +de0JuKpKjvo+txjlppRqc4jArSM+DlQW8aL9HO5MEgSQK99z6at27xZrRPqkcOy2vR6hbIklRONe ++59NXV63UUABVmEY9l5LJZbatCADCK99z6a19ZO5qIBIgkU7J7wpK+NMNTjE4BaHqc45aatPdEXG +UFh309zpJ3Ryxwr33Ppq0l0s1tjDAjCOy39lqCEeplE0tzqyTdJOJ5U1+8dNtBLGvvo/7fc2+/TG +dPeCg3rzuzPGOLHDs1aRq5xj2eoA+NexfIV6VA8B2SygnvFexfIVCgAd3ZJQE+FexfIVAyqDlXsX +yFSEAPh2QwBHfXsXyFSqgHuHZ6lB8RXsXyFQoA8OxF6ldW2SVI4TnQGwDHE0LTWE0DIRWu3YUNzi +tb9OpbuFL1Nq3DodS8ge32L5V7B5dkESO+vYvkKlVAPcOz1AHxr2L5CvSAPDskqCeZFexfIVCiBy +HZ7B5V7F8uyCJFexfIVIUA9w7IYAjvqFEDkKKOAynMHKtmBsa97Rwyyq50FwHdt3HCmMCNR7B0LT +uE6Z4TU/FhcvzDGABS9RZMo1NfumLaCSRjX38HY3NvTx0RTOqjcuu7M0Y4sez+6KHh8Ws/zUniaK +sAQcwa29I2tW7t8JirnRXAdaOwVowiT2axaYobgbVGEUPi1s2kL6WxgTSLcUq0kwae/d9iCTGNff +wdrd0af4IougGq4zMzRicT2THxfHGsKhhIOYNaNI2tW7txhMVe/9detuWt3XCXQJXTqOB7B0BRiZ +0l+Emp+LLcvAtqMACl6i1OluBp79wEqgkhRJ8hX3+k6N3Rt8dEVqtgBrjuzGMSdR7P7ooeHxaz/N +SeJ7PaNE7uiMNUV1H/r7ll3S1dfTeGKqCxwbs31tk2S4bXwigPi1v7dDcKNJC4mkt31KPJOk5097 +Q1zQJ0IJY19/pM7unax9sRFf0xGt2ZjhJJY/HJ0//ppwjVFXujayXsJcaL3BcTgewdCLMpqCl5xk +1PxZbhTWWMAUvUoNIbMU95LbXmUSLa5mvvNJ3N2NrH26fbQ2xGpmY+JY9n90UPD4tZ/mpPE9mrSJ +jX/miKvdObWvpluvF3LT6jh39n3YjZ1hp8KA7ZOAqQwjxrBgfnUFhPjWLAfOpLCPGpDA/OoDCfGo +1CfGsWA+dTqEc5rBgfnUBgfnUahPjWLAfOp1COc1gwPzqAwJ8agsAfGsWA+dTqEc5rBgfnUahPjU +FhPjWLAfOp1COdYMD86gMJ8agsJ8aksAPGp1CPGsGB+dQGE+NYsB86ksI8anUI5zWDA/Oo1CfGsW +A+dYsB86kMCKgMD86jUJ8agsB86ksI5zUhgfnWDA/OoUgxnH4EHTLrZDJFJZvjS8kkU9yzb3rij0 +2wYmvutB+6+407cd0aaAtgAFnJjmWP4mtt7XBBjvo2gW0tniZrWhae8k1usX1TODGgXLYYCGIpbb +FtK5QTRRC0NnJrcUvq72NbpL6pn3GKDOXkcmNbJLaR340yqWhsDJNa1LyebGtyX1Z+40A5aFygxW +zLafEzTIpaGwMk1uIXnvY1uOW1dzGlVi0LgIJoWSW0jvM0Qhb1ZyZrdBfV/Ma3WL6pnBjFDWW9Ig +QSK2AW0HvxolC2OckmtwF9X8xrcYvq7mNC25bSuUGjaBbSc8TNHQW9WBkmtxS+rvY1qctI5Eilts +WhcoJrZltPiZolC2PMk1uy+rP3GtbFpHJjSqxaFygmjaUtpPM41rUvPexrdl9WfuMVrcvPcxoWSW +0jvxpkUtDZyTRZC0nmxp3tlpfOTP49Wkavf/AJtMV1FopPRLdeHOYOo5dg6NVG0HCHnjQPxZLiJr +ZzABpepK6ScCKd+nTcugelcpre0n7vfjTHCMqXaiCzkxz1Gez+6KHh8Ws/zUnieyPTqif80V1KkA +9ALzwWznUfb2fei4NvUGI44UB2ycBU6hHjWDA/OoLCfGsWA+dSWEeNSGBHjUBhPjUahPjWLAfOp1 +COdYMD86gMD86jUJ8axYD51OoRzmpDA/OoDAnxqCwB8axYD51OoRzmsGB+dRqE+NQWE+NYsB86nU +I51gwPzqAwnxqCwnxqSwA8anUI8awYH51AYT41iwHzqSwjxqdQjnNYMD86jUJ8agsB86xYD51IYE +VAYH51GoT41BYD51JYRzqQwPzrBgfnUKQYzj8CL08F0MwcKTp70awSTFOelVWvgehWMCa1wfv9/2 +/LLwpRbiNTTHPUZ/E1tva4IMd9G0C2ls8TNa0LT3ma3WL6pnBjFAuWwwEE0tti2lcoNFELQ2cmtx +S+rvY1ukvqmfcYoM5eRyY1sktpHfjTKpaGwMmtal5PNjW5Lzn7jQVy0LlBitmW0+ONMilobOSa3E +Lz3sa3HL6u4mlVi0LgIJoWSW0jvxohC3qzkzW6C+r+Y1usX1TODGKGst6RAgmjYBbQe/GiULY5yT +W4C+r+Y1uMX1dzGhbYtC5QaNoFtJzxxo6C3qwMk1uKX1Z4sa1OWkcjFLbYtC5Qa2ZbT440ShbHmZ +rdl9WfuMVrYvI5E0qsWhcoJo2lLaTzONa1Lz3sa3ZfVn7jWti89zGhZJbSO/GmRS0NnJoshaTzJp +3tliXzkz+Phrj/VFdQZA6DdeA2c6j7Oz7RdOxrCaY599A/Fke0oZnMY0nUuuljgQKc9IFN+PQHym +pg/7h9xl8v2UNqI1NOnnqM9n90UPD4tZ/mpPE9kSmuO6dWn9tdRdNwL0BuvCNiSdR9vLsHXC9C6g +xSMZFR+DOs6zrOs+zOs6z7M6zrPszrOs6zrOs+zOs6z7M6zrPszrOs6zrOs6zrOs6z7M6zrPszrP +8KJYZVdDPqypOnukM4xMZU46Qot+PQbklZ74rRLf7j9xOqe7PwobcQGYGOeoz+JrTEhXBUkGDjRt +B7pVsyXM+da0e6T/ABOSK3We6GmYDkDyoF3uiMBpciltM90KuRDkGmto9whsyzEmtxbl0nkXJFbp +uXdUzGsx5UHd7oI/K5FCwXuaRx1GfOmVXukNgZcmtavdJPNyRW5uXZzjcMUqu9wBctLkVsa7unnr +M+dMivdIbAy5JrcR7pPJnJFbjvdDclcgUqs90BRA0uRQsF7ukYzrM+dEI9w6s9Tk1ui5d1ZwXMeV +brXLoaZgOQPKl1vdGkQNLkUenD3NB46jq86JR7p1YHU5NbouXS3IuSK3GuXQ3IOQKW2z3AFyKsQa +NoPd0niXM+dNoe6dWB1OTW4ty6WzguSK1u90Eflcilts90BciHINbOu7p56zPnRKPcM/mcmt3cu6 +pmNZjyoOz3QR+VyBSqz3QFwEORRtK1wqeJYk+dbivdJ5FyRW7uXdUzGsx5VuM90H+FyBQsl7gUcQ +xB86ZFe6Q2cuSaLK90k/mcmne2zsbmetiw+U/j4aojL96K6jqnvbfRG84Wzg2uGOP8PZ9ut2LIcL +twIg0D8WR+mgO5iSJpOovABzgY7qdencW7pHpciQPlX2eP32/wC+cMp1TQNttQVnVu4hj2f3RQ8P +i1n+ak8T2aNfqjRkfdpmK6jrXvsnT3Lr6bAyb1HEz2Dr9xgdWopwkVHxZba3NtkMgkSKXpi2sjEt +T2unvfb3WELdADafka+y1n7v7jVvyeU65oaD7GdWHIhj+J7TEgOCpIMHHlRtC5dKtmS5nzrWly6x +/ickVum5dDTMByB5UC1y6NIgaXIpbTXLoVciHINNbV7hDZlmJNbi3LpPIuSK3Tcu6pmNZjyoO1y6 +CPyuRQsF7mkcdRnzplW5dIbAy5NF1uXSTzckVubl6c41mKVXe4AuWlyK2Ny7p56zPnTIty6Q2BJc +k1uLcuk8mckVuNcug8lcgUqtcugKIGlyKFg3LukYzrM+dMEe42rPU5NbouXdWcFzHlW61y6GmYDk +Dypddy6NIgaXIo9OHuaTx1HV50Sly6dWB1OTW4Ll0nkXJFbjXLoOcByBS22e4AuRViDRtC5dKniX +M+dNpuXTqwOpya3Bcuk5wXJFa3uXQR+VyKW21y6AuRDkGtncu6Tx1mfOiUe40/mcmt3cu6pmNZjy +oO1y6COCuQKVWuXQFwEORRtK9wqeJYk+dbi3LpPIuSK3dy7qmY1mPKtxrl0HkrkChZL3Ao4hiD50 +yLcukNmS5Josty6SfzOTTvbZ2NzMOxYfKfx6pEeyf4oq51YuMiO7RaGCnHM9m2t5hbFwLon0x4UP +i1s9M+hnaCaS9fOq5JBNNbRzaZhAdcxX2mo7m7q3cconVXoMlGZWHEEE9n90UPD4tZ/mpPE9kasJ +254aoq51t1mUvcfSk4RqOJ7B1xkXAdUcJqPwZisCKzFZisxWdZisxWYrOsxWYrMViazFZisxWYrM +VmKwNZisxWYqZrMVmKzFZ1mKzFZisSKzFZisCKzFZisxWdZisxWYrOsxWYrA/gW07lCpkMKXprZL +BeJ401rUU1CNS4EV9nqP6mrc/hjOiqGHtO6us4iGPZEieX4GtkkBwQSM8aNoXLhDcS2Na1uXGPJm +mt03LgMzAYxQLXLgjDBiKW0blwBciGxprau5DcSZNbguXCeRYxW7uXJmY1GKDNcuAjkxFCyXfSOM +40yrcuHVgZaa1rcuEngWJrc3bk5xqMUFZ3XT+VorZ3LmnnqxpkW5cIbAktW4ty4TyLEitxrlwHkG +IFKrXLg0iBDRQsm5c0jjqxohbjtq/M01ui7cnlqMVum5cBmYDGKGq5cGkRg0UbGt9J4zjRK3Lh1c +2mtwXbhPIsYrcN24DyDGKW2zuAvEGDRtC5cIPEtjTablw6sMWmtwXbhOcFjFamuXFI/K0Uts3LgC +5ENjWzuXI56saJW5cafzNNbu5cmZjUYoO1y4COAYilVrlwBcBDUbQdyDxJxrWLlwnkWMVu7tyZmN +RitbXLgPIMQKFku4UcQcaZBcuENmS2NFluXCTzYmndHZjczDGR8vx5jTO3qnDVFXOtuE7juxVQYE +Sc+yBcaBcCgSYih4fFrew5tlmgkZ0ly8xd5Ik501liQrCCVMHzr7PUdG5q18dMUyKwF2y7I6ccGP +Z/dFDw+LWf5qTxNScBWfo1bWvhMVe69id267FRwAnsHWsp3QZ7p/DnWdZ1nWdZ1nWdZ1nWdZ1nWd +Z1nWdZ1nWdZ1nWdZ1nWdZ1nWdZ1nWdZ1nWdZ1nWdZ1nWdZ1nWdZ1n+EWrxKhTIIzpenszpXiaay5 +IVhBIwNfY/8AT3NevjpiioIFy27Ky8cGPZp1DVynH8DW5K6wRIzE0bW9cM8Sca1i9cbuYzW7vXBj +MA4UCb1xYEYGlt71waeIONMguu2riTjW5v3D3E4Vu79wYzE4UGN64scAaFndcAfvTjTKL1xtWEk1 +rF643cTW5v3OcThQBuuun8pitneuR+acaZN642riTW4L1xu4nCtw3ri9wOFKpvXF0iMDQs71wAfv +TjRAuu8/mM1ub9w904Vu71wYzAOFLN64ukRgaNjdeD+9ONEi9caeZrc37h7icK3N+4O4HCltm666 +eIONG1vXDPEnGmi9cbVzNbm/cPGCcKDG9cWOCmKW3vXBp4g41tb1yPzTjRIvO8/mM1u79zOYnCg5 +vXFjgDSqb1xdPEGja3XaeJONa9643cThW7v3M5icK1m9cXuBwoWd1wB+8DjTJvXG1cSaLC9caeBN +O4uM+vgxkDw/BJMCpGIosxgDMmtUjY17G7OExTdQwm47sw7gSezBzhcAz4UPD4taFtyupsYMUjXG +LNJEmn6e+NVu4IYAkfsr/bY/7bd3Nf72mKawWAvW3ZSnE4nLs/uih4fFrP8ANSeJou5CqMycBW7q +H22vZ18JjOm6kqDedmIbkJ4dg6trQN4GdXf8XFvqV1KDIjA0tiwum2uQprF5dVtxDLX2P/j7m7/F +EZU1gkC6jMNPMTw7No3FFz8kifL4vquMEXmTFBkIZTkRlRuXGCIuJYmAK3dQ+317W58s6a+QGu3H +ZtXISezAn9UCh4fFrQBIBbGkLGcTnTWrqh0bAqcjX2WkbGve0zjlT9G7aeotu4VfzCTl2Zf9UUPD +4tZ/mpPE0168wS2glmNfff8Ai7uzljp05016A1667MXjGNRgdg6hrSm6MnjH4vo6hBcXkaFq0oRF +yAopcUMrZg5Vo/8AG3N7b4e3Lzq50N0/17dxwvIrqPZ9sbqi8f3OPZNxgo5kxX6yfUKi26se4z2R +cdVPImK/WT6hU22DDmDPYVa6oIzBIr9ZPqFAqZByIqSYA41+sn1CoW6hJyAI7NTsFHM4V+sn1CtK +XFY8gQewbjhJykxX6yfUK/puHjODPZpe4qnkSBX6yfUK1IQyniMeyDdQEZjUK/WT6hUiiWMAZk1+ +sn1CtK3VJOQBHZLsFHMmK/WT6hUW3VjyBnsi46qeRMV+sn1CptsGHMGezS11QRmCRX6yfUK1KQQc +iKk1Bup9QqBdQn+YdmpyFA4mv1k+oVpS4rHkCD2a77hF5mhdssHQ5EU/UX2020Esa+80n7bd2446 +YzpryqDdvO7M8Y4sew9WoC2dYYNPAUAc6t/aHFTisxNe0fVQu9SdNsDETM9gvdL6rZUCJiCK9o+q +ro6w4uRpWZiKNXXT1ozEq2qMCa9v+qrVjqDquqPUau2bR03HUhT317Qf81IxGkAgk6qVTiQADSp0 +hh1MkTE17R9VW71/02lxb1TPZbudIZQLpKzGPOvaPqq8/WGA4AVZnLj2Xr1r123aVOqMOVe0fVS2 +eqM3JJ5xPCnVDDMpAPeRRw1Y56s6Hpjv1VatXDqdFCseZAq7Z6cxdbKvb/qq09z0IrAsdU4CgKtj +pD7T6lmJr2j6q3up9NsCImZ7De6b1WyAAJiIr2j6quDqz6nMqszFGrrp61ZiQdUYV7R9VW7PUHVc +GdXbdsw7KQp7zR9M9+qlkaRIk6sqtoxllUAnvFG10pi5IMTEivaPqq3dvei2hlvVM9ls9LDKuakx +S2OoEXATImaKOAynMHEVtaRsa97bjCdOVXegug7tu44RokEaj8bfqLs6LYk6RJ8q/wBx0nZ3dvRx +0aauL0tp26y87MWVDIljxilHUK9u8o9WpTj35ULfRJce7IJYKYA8qVnD23j1KykY+VW7fR27jKjT +dYIYI5ULnqXmrKQf2Va2LVxumTC56T6poXAWAzgqZ/ZShbVw9GRpPoOf5q3JaImNJn9lXN21cHTN +hb9J9Mf86L+po4BSTVxOrt3FR2m0SpgDlRdQzt+6qqTjRt9YjrcklWKmIPCm2Va5dI9AVTnSp1Ku +l5fcWU499G10qu15smCmBSm4rJcA9QZTnS2+jR2cEFmCmI5UHIa237yspEGrKdJbuMitN0hTDDlQ +f1LPAqQaTat3D0y4P6TjPGtyTETGkzTFrdz7WIHpOY/erckkRMBTNXRftutlz/TOk+mi/qbkqqSS +ae31aOpZibbFTAB/d+VFkDXHg6VCnOha6tXW8s+oqYYZ0y2FZ7zCFKqcJ40q31ZLqiGlTjHGtrpV +drpj1BTAFAuGRwPUCpzpU6VHYq0uQpiOVBzqU8QVIM1ZXp7dxrKn+qQpxoPiMJgqZoabdw9MBB9J +z51uS0RMaTNPuWrg6Y4J6Th30bnqYcgpmrq9TbdUZptkqcByouAzt+6qqTJo2usRw8kq5UxB4Uxt +hnuEHSFU50LfVK63lzYqYamXpld7zD0lVOHfSi+r27yj1hlOJpbfRo73Cw1MqmAKDMGtvHqVlIxq +2nSW7jKrTcIUwRyoXPUnNWVgf2Vat9PYvHpEwuNob1f4VvSwWJjSZ8or9C9/t0aPY2f5sqN6XIAn +SEafKKvL1fT3h0NzC3Ntjpjnhxrf0H7TVvadB06tOURX/9k=" transform="matrix(1.4434 0 0 1.4434 3.1011 0)">
+</image>
+<g id="circles_4">
+ <ellipse fill="#FFFFFF" cx="11.244" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="33.089" cy="10.968" rx="10.923" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="54.934" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="76.779" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="98.624" cy="10.968" rx="10.923" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="120.469" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="142.313" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="164.158" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="186.002" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="207.847" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="229.692" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="251.537" cy="10.968" rx="10.923" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="273.382" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="295.227" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="317.072" cy="10.968" rx="10.923" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="338.917" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="360.761" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="382.605" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="404.45" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="426.295" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="448.14" cy="10.968" rx="10.923" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="469.985" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="491.83" cy="10.968" rx="10.923" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="513.675" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="535.52" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="557.364" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="579.209" cy="10.968" rx="10.923" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="601.054" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="622.899" cy="10.968" rx="10.923" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="644.744" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="666.589" cy="10.968" rx="10.922" ry="10.968"/>
+ <ellipse fill="#FFFFFF" cx="688.691" cy="10.968" rx="10.922" ry="10.968"/>
+</g>
+<g id="circles_3">
+ <ellipse fill="#FFFFFF" cx="10.968" cy="33.244" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="55.089" rx="10.967" ry="10.923"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="76.934" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="98.779" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="120.624" rx="10.967" ry="10.923"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="142.469" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="164.313" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="186.158" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="208.002" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="229.847" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="251.692" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="273.537" rx="10.967" ry="10.923"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="295.382" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="317.227" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="339.072" rx="10.967" ry="10.923"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="360.917" rx="10.967" ry="10.923"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="382.761" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="404.605" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="426.45" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="448.295" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="470.14" rx="10.967" ry="10.923"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="491.985" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="10.968" cy="513.83" rx="10.967" ry="10.923"/>
+</g>
+<g id="circles_2">
+ <ellipse fill="#FFFFFF" cx="688.968" cy="33.244" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="55.089" rx="10.967" ry="10.923"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="76.934" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="98.779" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="120.624" rx="10.967" ry="10.923"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="142.469" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="164.313" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="186.158" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="208.002" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="229.847" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="251.692" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="273.537" rx="10.967" ry="10.923"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="295.382" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="317.227" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="339.072" rx="10.967" ry="10.923"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="360.917" rx="10.967" ry="10.923"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="382.761" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="404.605" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="426.45" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="448.295" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="470.14" rx="10.967" ry="10.923"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="491.985" rx="10.967" ry="10.922"/>
+ <ellipse fill="#FFFFFF" cx="688.968" cy="513.83" rx="10.967" ry="10.923"/>
+</g>
+<g id="circles_1">
+ <ellipse fill="#FFFFFF" cx="33.089" cy="535.968" rx="10.923" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="11.089" cy="535.968" rx="10.923" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="54.934" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="76.779" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="98.624" cy="535.968" rx="10.923" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="120.469" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="142.313" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="164.158" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="186.002" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="207.847" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="229.692" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="251.537" cy="535.968" rx="10.923" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="273.382" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="295.227" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="317.072" cy="535.968" rx="10.923" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="338.917" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="360.761" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="382.605" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="404.45" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="426.295" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="448.14" cy="535.968" rx="10.923" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="469.985" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="491.83" cy="535.968" rx="10.923" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="513.675" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="535.52" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="557.364" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="579.209" cy="535.968" rx="10.923" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="601.054" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="622.899" cy="535.968" rx="10.923" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="644.744" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="666.589" cy="535.968" rx="10.922" ry="10.969"/>
+ <ellipse fill="#FFFFFF" cx="688.589" cy="535.968" rx="10.922" ry="10.969"/>
+</g>
+<rect id="body" x="6.322" y="5.333" fill="#FFFFFF" width="687" height="536"/>
+<g id="thread_top">
+ <path fill="#E70029" d="M166.322,29.833c0,2.22-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.28-4.5-4.5l0,0c0-2.22,2.015-4.5,4.5-4.5h17
+ C164.307,25.333,166.322,27.613,166.322,29.833L166.322,29.833z"/>
+ <path fill="#E70029" d="M210.322,29.833c0,2.22-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.28-4.5-4.5l0,0c0-2.22,2.015-4.5,4.5-4.5h17
+ C208.307,25.333,210.322,27.613,210.322,29.833L210.322,29.833z"/>
+ <path fill="#E70029" d="M254.322,29.833c0,2.22-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.28-4.5-4.5l0,0c0-2.22,2.015-4.5,4.5-4.5h17
+ C252.307,25.333,254.322,27.613,254.322,29.833L254.322,29.833z"/>
+ <path fill="#E70029" d="M296.322,29.833c0,2.22-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.28-4.5-4.5l0,0c0-2.22,2.015-4.5,4.5-4.5h17
+ C294.307,25.333,296.322,27.613,296.322,29.833L296.322,29.833z"/>
+ <path fill="#E70029" d="M429.322,29.833c0,2.22-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.28-4.5-4.5l0,0c0-2.22,2.015-4.5,4.5-4.5h17
+ C427.307,25.333,429.322,27.613,429.322,29.833L429.322,29.833z"/>
+ <path fill="#E70029" d="M473.322,29.833c0,2.22-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.28-4.5-4.5l0,0c0-2.22,2.015-4.5,4.5-4.5h17
+ C471.307,25.333,473.322,27.613,473.322,29.833L473.322,29.833z"/>
+ <path fill="#E70029" d="M516.322,29.833c0,2.22-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.28-4.5-4.5l0,0c0-2.22,2.015-4.5,4.5-4.5h17
+ C514.307,25.333,516.322,27.613,516.322,29.833L516.322,29.833z"/>
+ <path fill="#E70029" d="M559.322,29.833c0,2.22-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.28-4.5-4.5l0,0c0-2.22,2.015-4.5,4.5-4.5h17
+ C557.307,25.333,559.322,27.613,559.322,29.833L559.322,29.833z"/>
+ <path fill="#E70029" d="M603.322,29.833c0,2.22-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.28-4.5-4.5l0,0c0-2.22,2.015-4.5,4.5-4.5h17
+ C601.307,25.333,603.322,27.613,603.322,29.833L603.322,29.833z"/>
+ <path fill="#E70029" d="M646.322,29.833c0,2.22-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.28-4.5-4.5l0,0c0-2.22,2.015-4.5,4.5-4.5h17
+ C644.307,25.333,646.322,27.613,646.322,29.833L646.322,29.833z"/>
+ <path fill="#E70029" d="M123.322,29.833c0,2.22-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.28-4.5-4.5l0,0c0-2.22,2.015-4.5,4.5-4.5h17
+ C121.307,25.333,123.322,27.613,123.322,29.833L123.322,29.833z"/>
+ <path fill="#E70029" d="M79.322,29.833c0,2.22-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.28-4.5-4.5l0,0c0-2.22,2.015-4.5,4.5-4.5h17
+ C77.307,25.333,79.322,27.613,79.322,29.833L79.322,29.833z"/>
+ <circle fill="#E70029" cx="668.656" cy="30.333" r="5"/>
+ <circle fill="#E70029" cx="32.655" cy="30.333" r="5"/>
+</g>
+<g id="thread_bottom">
+ <path fill="#E70029" d="M166.322,516.833c0,2.219-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.281-4.5-4.5l0,0c0-2.219,2.015-4.5,4.5-4.5
+ h17C164.307,512.333,166.322,514.615,166.322,516.833L166.322,516.833z"/>
+ <path fill="#E70029" d="M210.322,516.833c0,2.219-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.281-4.5-4.5l0,0c0-2.219,2.015-4.5,4.5-4.5
+ h17C208.307,512.333,210.322,514.615,210.322,516.833L210.322,516.833z"/>
+ <path fill="#E70029" d="M254.322,516.833c0,2.219-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.281-4.5-4.5l0,0c0-2.219,2.015-4.5,4.5-4.5
+ h17C252.307,512.333,254.322,514.615,254.322,516.833L254.322,516.833z"/>
+ <path fill="#E70029" d="M296.322,516.833c0,2.219-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.281-4.5-4.5l0,0c0-2.219,2.015-4.5,4.5-4.5
+ h17C294.307,512.333,296.322,514.615,296.322,516.833L296.322,516.833z"/>
+ <path fill="#E70029" d="M341.322,516.833c0,2.219-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.281-4.5-4.5l0,0c0-2.219,2.015-4.5,4.5-4.5
+ h17C339.307,512.333,341.322,514.615,341.322,516.833L341.322,516.833z"/>
+ <path fill="#E70029" d="M384.322,516.833c0,2.219-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.281-4.5-4.5l0,0c0-2.219,2.015-4.5,4.5-4.5
+ h17C382.307,512.333,384.322,514.615,384.322,516.833L384.322,516.833z"/>
+ <path fill="#E70029" d="M429.322,516.833c0,2.219-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.281-4.5-4.5l0,0c0-2.219,2.015-4.5,4.5-4.5
+ h17C427.307,512.333,429.322,514.615,429.322,516.833L429.322,516.833z"/>
+ <path fill="#E70029" d="M473.322,516.833c0,2.219-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.281-4.5-4.5l0,0c0-2.219,2.015-4.5,4.5-4.5
+ h17C471.307,512.333,473.322,514.615,473.322,516.833L473.322,516.833z"/>
+ <path fill="#E70029" d="M516.322,516.833c0,2.219-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.281-4.5-4.5l0,0c0-2.219,2.015-4.5,4.5-4.5
+ h17C514.307,512.333,516.322,514.615,516.322,516.833L516.322,516.833z"/>
+ <path fill="#E70029" d="M559.322,516.833c0,2.219-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.281-4.5-4.5l0,0c0-2.219,2.015-4.5,4.5-4.5
+ h17C557.307,512.333,559.322,514.615,559.322,516.833L559.322,516.833z"/>
+ <path fill="#E70029" d="M603.322,516.833c0,2.219-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.281-4.5-4.5l0,0c0-2.219,2.015-4.5,4.5-4.5
+ h17C601.307,512.333,603.322,514.615,603.322,516.833L603.322,516.833z"/>
+ <path fill="#E70029" d="M646.322,516.833c0,2.219-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.281-4.5-4.5l0,0c0-2.219,2.015-4.5,4.5-4.5
+ h17C644.307,512.333,646.322,514.615,646.322,516.833L646.322,516.833z"/>
+ <path fill="#E70029" d="M123.322,516.833c0,2.219-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.281-4.5-4.5l0,0c0-2.219,2.015-4.5,4.5-4.5
+ h17C121.307,512.333,123.322,514.615,123.322,516.833L123.322,516.833z"/>
+ <path fill="#E70029" d="M79.322,516.833c0,2.219-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.281-4.5-4.5l0,0c0-2.219,2.015-4.5,4.5-4.5
+ h17C77.307,512.333,79.322,514.615,79.322,516.833L79.322,516.833z"/>
+ <circle fill="#E70029" cx="668.656" cy="516.333" r="5"/>
+ <circle fill="#E70029" cx="31.655" cy="516.333" r="5"/>
+</g>
+<g id="thread_left">
+ <path fill="#E70029" d="M31.822,159.807c-2.219,0-4.5-1.916-4.5-4.28v-16.168c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.917,4.5,4.28v16.168C36.322,157.891,34.041,159.807,31.822,159.807L31.822,159.807z"/>
+ <path fill="#E70029" d="M31.822,201.656c-2.219,0-4.5-1.917-4.5-4.28v-16.168c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.916,4.5,4.28v16.168C36.322,199.739,34.041,201.656,31.822,201.656L31.822,201.656z"/>
+ <path fill="#E70029" d="M31.822,243.504c-2.219,0-4.5-1.916-4.5-4.28v-16.169c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.916,4.5,4.28v16.169C36.322,241.588,34.041,243.504,31.822,243.504L31.822,243.504z"/>
+ <path fill="#E70029" d="M31.822,283.45c-2.219,0-4.5-1.916-4.5-4.28v-16.168c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.917,4.5,4.28v16.168C36.322,281.534,34.041,283.45,31.822,283.45L31.822,283.45z"/>
+ <path fill="#E70029" d="M31.822,326.249c-2.219,0-4.5-1.916-4.5-4.28v-16.168c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.917,4.5,4.28v16.168C36.322,324.333,34.041,326.249,31.822,326.249L31.822,326.249z"/>
+ <path fill="#E70029" d="M31.822,367.146c-2.219,0-4.5-1.916-4.5-4.279v-16.169c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.916,4.5,4.28v16.169C36.322,365.23,34.041,367.146,31.822,367.146L31.822,367.146z"/>
+ <path fill="#E70029" d="M31.822,409.946c-2.219,0-4.5-1.916-4.5-4.28v-16.168c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.916,4.5,4.28v16.168C36.322,408.03,34.041,409.946,31.822,409.946L31.822,409.946z"/>
+ <path fill="#E70029" d="M31.822,451.793c-2.219,0-4.5-1.916-4.5-4.279v-16.169c0-2.363,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.917,4.5,4.28v16.169C36.322,449.877,34.041,451.793,31.822,451.793L31.822,451.793z"/>
+ <path fill="#E70029" d="M31.822,492.691c-2.219,0-4.5-1.916-4.5-4.28v-16.168c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.916,4.5,4.28v16.168C36.322,490.775,34.041,492.691,31.822,492.691L31.822,492.691z"/>
+ <path fill="#E70029" d="M31.822,118.91c-2.219,0-4.5-1.916-4.5-4.28V98.461c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.916,4.5,4.28v16.169C36.322,116.994,34.041,118.91,31.822,118.91L31.822,118.91z"/>
+ <path fill="#E70029" d="M31.822,77.062c-2.219,0-4.5-1.916-4.5-4.28V56.613c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.916,4.5,4.28v16.169C36.322,75.146,34.041,77.062,31.822,77.062L31.822,77.062z"/>
+</g>
+<g id="thread_right">
+ <path fill="#E70029" d="M668.822,159.807c-2.219,0-4.5-1.916-4.5-4.28v-16.168c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.917,4.5,4.28v16.168C673.322,157.891,671.041,159.807,668.822,159.807L668.822,159.807z"/>
+ <path fill="#E70029" d="M668.822,201.656c-2.219,0-4.5-1.917-4.5-4.28v-16.168c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.916,4.5,4.28v16.168C673.322,199.739,671.041,201.656,668.822,201.656L668.822,201.656z"/>
+ <path fill="#E70029" d="M668.822,243.504c-2.219,0-4.5-1.916-4.5-4.28v-16.169c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.916,4.5,4.28v16.169C673.322,241.588,671.041,243.504,668.822,243.504L668.822,243.504z"/>
+ <path fill="#E70029" d="M668.822,283.45c-2.219,0-4.5-1.916-4.5-4.28v-16.168c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.917,4.5,4.28v16.168C673.322,281.534,671.041,283.45,668.822,283.45L668.822,283.45z"/>
+ <path fill="#E70029" d="M668.822,326.249c-2.219,0-4.5-1.916-4.5-4.28v-16.168c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.917,4.5,4.28v16.168C673.322,324.333,671.041,326.249,668.822,326.249L668.822,326.249z"/>
+ <path fill="#E70029" d="M668.822,367.146c-2.219,0-4.5-1.916-4.5-4.279v-16.169c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.916,4.5,4.28v16.169C673.322,365.23,671.041,367.146,668.822,367.146L668.822,367.146z"/>
+ <path fill="#E70029" d="M668.822,409.946c-2.219,0-4.5-1.916-4.5-4.28v-16.168c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.916,4.5,4.28v16.168C673.322,408.03,671.041,409.946,668.822,409.946L668.822,409.946z"/>
+ <path fill="#E70029" d="M668.822,451.793c-2.219,0-4.5-1.916-4.5-4.279v-16.169c0-2.363,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.917,4.5,4.28v16.169C673.322,449.877,671.041,451.793,668.822,451.793L668.822,451.793z"/>
+ <path fill="#E70029" d="M668.822,492.691c-2.219,0-4.5-1.916-4.5-4.28v-16.168c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.916,4.5,4.28v16.168C673.322,490.775,671.041,492.691,668.822,492.691L668.822,492.691z"/>
+ <path fill="#E70029" d="M668.822,118.91c-2.219,0-4.5-1.916-4.5-4.28V98.461c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.916,4.5,4.28v16.169C673.322,116.994,671.041,118.91,668.822,118.91L668.822,118.91z"/>
+ <path fill="#E70029" d="M668.822,77.062c-2.219,0-4.5-1.916-4.5-4.28V56.613c0-2.364,2.281-4.28,4.5-4.28l0,0
+ c2.219,0,4.5,1.916,4.5,4.28v16.169C673.322,75.146,671.041,77.062,668.822,77.062L668.822,77.062z"/>
+</g>
+<path display="none" fill="#E7022B" d="M349.759,36.083"/>
+<radialGradient id="SVGID_1_" cx="350.3218" cy="26.3335" r="21.9659" gradientUnits="userSpaceOnUse">
+ <stop offset="0.3817" style="stop-color:#AD001A"/>
+ <stop offset="1" style="stop-color:#DE0022"/>
+</radialGradient>
+<rect x="319.322" y="24.333" fill="url(#SVGID_1_)" width="62" height="4"/>
+</svg>
diff --git a/docs/website_assets/letter_bottom.svg b/docs/website_assets/letter_bottom.svg new file mode 100644 index 0000000..25cfdfd --- /dev/null +++ b/docs/website_assets/letter_bottom.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="44px" height="34px" viewBox="0 0 44 34" enable-background="new 0 0 44 34" xml:space="preserve">
+<ellipse fill="#FFFFFF" cx="33.066" cy="23.032" rx="10.922" ry="10.968"/>
+<ellipse fill="#FFFFFF" cx="11.221" cy="23.032" rx="10.922" ry="10.968"/>
+<rect id="body" y="0" fill="#FFFFFF" width="44" height="29"/>
+<path fill="#E70029" d="M8.678,4.5c0-2.22,2.015-4.5,4.5-4.5h17c2.485,0,4.5,2.28,4.5,4.5l0,0c0,2.22-2.015,4.5-4.5,4.5h-17
+ C10.693,9,8.678,6.72,8.678,4.5L8.678,4.5z"/>
+</svg>
diff --git a/docs/website_assets/letter_top.svg b/docs/website_assets/letter_top.svg new file mode 100644 index 0000000..c9d7ad1 --- /dev/null +++ b/docs/website_assets/letter_top.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="44px" height="34px" viewBox="0 0 44 34" enable-background="new 0 0 44 34" xml:space="preserve">
+<ellipse fill="#FFFFFF" cx="10.934" cy="10.968" rx="10.922" ry="10.968"/>
+<ellipse fill="#FFFFFF" cx="32.779" cy="10.968" rx="10.922" ry="10.968"/>
+<rect id="body" y="5" fill="#FFFFFF" width="44" height="29"/>
+<path fill="#E70029" d="M35.322,29.5c0,2.22-2.015,4.5-4.5,4.5h-17c-2.485,0-4.5-2.28-4.5-4.5l0,0c0-2.22,2.015-4.5,4.5-4.5h17
+ C33.307,25,35.322,27.279,35.322,29.5L35.322,29.5z"/>
+</svg>
diff --git a/docs/website_assets/logo.webp b/docs/website_assets/logo.webp Binary files differnew file mode 100644 index 0000000..4f655e4 --- /dev/null +++ b/docs/website_assets/logo.webp diff --git a/docs/website_assets/ribbon.webp b/docs/website_assets/ribbon.webp Binary files differnew file mode 100644 index 0000000..d80ec8e --- /dev/null +++ b/docs/website_assets/ribbon.webp diff --git a/docs/winter.css b/docs/winter.css new file mode 100644 index 0000000..bf80325 --- /dev/null +++ b/docs/winter.css @@ -0,0 +1,9 @@ +body
+{
+ background: #14504a url(website_assets/bg_winter.webp);
+ background-attachment: fixed;
+ background-position-x: 50%;
+ background-position-y: 100%;
+ background-repeat: no-repeat;
+ background-size: cover;
+}
|
