LaravelTest
5210 строк · 118.2 Кб
1/**
2* Copyright (c) 2021, Leon Sorokin
3* All rights reserved. (MIT Licensed)
4*
5* uPlot.js (μPlot)
6* A small, fast chart for time series, lines, areas, ohlc & bars
7* https://github.com/leeoniya/uPlot (v1.6.18)
8*/
9
10const FEAT_TIME = true;11
12// binary search for index of closest value
13function closestIdx(num, arr, lo, hi) {14let mid;15lo = lo || 0;16hi = hi || arr.length - 1;17let bitwise = hi <= 2147483647;18
19while (hi - lo > 1) {20mid = bitwise ? (lo + hi) >> 1 : floor((lo + hi) / 2);21
22if (arr[mid] < num)23lo = mid;24else25hi = mid;26}27
28if (num - arr[lo] <= arr[hi] - num)29return lo;30
31return hi;32}
33
34function nonNullIdx(data, _i0, _i1, dir) {35for (let i = dir == 1 ? _i0 : _i1; i >= _i0 && i <= _i1; i += dir) {36if (data[i] != null)37return i;38}39
40return -1;41}
42
43function getMinMax(data, _i0, _i1, sorted) {44// console.log("getMinMax()");
45
46let _min = inf;47let _max = -inf;48
49if (sorted == 1) {50_min = data[_i0];51_max = data[_i1];52}53else if (sorted == -1) {54_min = data[_i1];55_max = data[_i0];56}57else {58for (let i = _i0; i <= _i1; i++) {59if (data[i] != null) {60_min = min(_min, data[i]);61_max = max(_max, data[i]);62}63}64}65
66return [_min, _max];67}
68
69function getMinMaxLog(data, _i0, _i1) {70// console.log("getMinMax()");
71
72let _min = inf;73let _max = -inf;74
75for (let i = _i0; i <= _i1; i++) {76if (data[i] > 0) {77_min = min(_min, data[i]);78_max = max(_max, data[i]);79}80}81
82return [83_min == inf ? 1 : _min,84_max == -inf ? 10 : _max,85];86}
87
88const _fixedTuple = [0, 0];89
90function fixIncr(minIncr, maxIncr, minExp, maxExp) {91_fixedTuple[0] = minExp < 0 ? roundDec(minIncr, -minExp) : minIncr;92_fixedTuple[1] = maxExp < 0 ? roundDec(maxIncr, -maxExp) : maxIncr;93return _fixedTuple;94}
95
96function rangeLog(min, max, base, fullMags) {97let minSign = sign(min);98
99let logFn = base == 10 ? log10 : log2;100
101if (min == max) {102if (minSign == -1) {103min *= base;104max /= base;105}106else {107min /= base;108max *= base;109}110}111
112let minExp, maxExp, minMaxIncrs;113
114if (fullMags) {115minExp = floor(logFn(min));116maxExp = ceil(logFn(max));117
118minMaxIncrs = fixIncr(pow(base, minExp), pow(base, maxExp), minExp, maxExp);119
120min = minMaxIncrs[0];121max = minMaxIncrs[1];122}123else {124minExp = floor(logFn(abs(min)));125maxExp = floor(logFn(abs(max)));126
127minMaxIncrs = fixIncr(pow(base, minExp), pow(base, maxExp), minExp, maxExp);128
129min = incrRoundDn(min, minMaxIncrs[0]);130max = incrRoundUp(max, minMaxIncrs[1]);131}132
133return [min, max];134}
135
136function rangeAsinh(min, max, base, fullMags) {137let minMax = rangeLog(min, max, base, fullMags);138
139if (min == 0)140minMax[0] = 0;141
142if (max == 0)143minMax[1] = 0;144
145return minMax;146}
147
148const rangePad = 0.1;149
150const autoRangePart = {151mode: 3,152pad: rangePad,153};154
155const _eqRangePart = {156pad: 0,157soft: null,158mode: 0,159};160
161const _eqRange = {162min: _eqRangePart,163max: _eqRangePart,164};165
166// this ensures that non-temporal/numeric y-axes get multiple-snapped padding added above/below
167// TODO: also account for incrs when snapping to ensure top of axis gets a tick & value
168function rangeNum(_min, _max, mult, extra) {169if (isObj(mult))170return _rangeNum(_min, _max, mult);171
172_eqRangePart.pad = mult;173_eqRangePart.soft = extra ? 0 : null;174_eqRangePart.mode = extra ? 3 : 0;175
176return _rangeNum(_min, _max, _eqRange);177}
178
179// nullish coalesce
180function ifNull(lh, rh) {181return lh == null ? rh : lh;182}
183
184// checks if given index range in an array contains a non-null value
185// aka a range-bounded Array.some()
186function hasData(data, idx0, idx1) {187idx0 = ifNull(idx0, 0);188idx1 = ifNull(idx1, data.length - 1);189
190while (idx0 <= idx1) {191if (data[idx0] != null)192return true;193idx0++;194}195
196return false;197}
198
199function _rangeNum(_min, _max, cfg) {200let cmin = cfg.min;201let cmax = cfg.max;202
203let padMin = ifNull(cmin.pad, 0);204let padMax = ifNull(cmax.pad, 0);205
206let hardMin = ifNull(cmin.hard, -inf);207let hardMax = ifNull(cmax.hard, inf);208
209let softMin = ifNull(cmin.soft, inf);210let softMax = ifNull(cmax.soft, -inf);211
212let softMinMode = ifNull(cmin.mode, 0);213let softMaxMode = ifNull(cmax.mode, 0);214
215let delta = _max - _min;216
217// this handles situations like 89.7, 89.69999999999999218// by assuming 0.001x deltas are precision errors219// if (delta > 0 && delta < abs(_max) / 1e3)
220// delta = 0;
221
222// treat data as flat if delta is less than 1 billionth223if (delta < 1e-9) {224delta = 0;225
226// if soft mode is 2 and all vals are flat at 0, avoid the 0.1 * 1e3 fallback227// this prevents 0,0,0 from ranging to -100,100 when softMin/softMax are -1,1228if (_min == 0 || _max == 0) {229delta = 1e-9;230
231if (softMinMode == 2 && softMin != inf)232padMin = 0;233
234if (softMaxMode == 2 && softMax != -inf)235padMax = 0;236}237}238
239let nonZeroDelta = delta || abs(_max) || 1e3;240let mag = log10(nonZeroDelta);241let base = pow(10, floor(mag));242
243let _padMin = nonZeroDelta * (delta == 0 ? (_min == 0 ? .1 : 1) : padMin);244let _newMin = roundDec(incrRoundDn(_min - _padMin, base/10), 9);245let _softMin = _min >= softMin && (softMinMode == 1 || softMinMode == 3 && _newMin <= softMin || softMinMode == 2 && _newMin >= softMin) ? softMin : inf;246let minLim = max(hardMin, _newMin < _softMin && _min >= _softMin ? _softMin : min(_softMin, _newMin));247
248let _padMax = nonZeroDelta * (delta == 0 ? (_max == 0 ? .1 : 1) : padMax);249let _newMax = roundDec(incrRoundUp(_max + _padMax, base/10), 9);250let _softMax = _max <= softMax && (softMaxMode == 1 || softMaxMode == 3 && _newMax >= softMax || softMaxMode == 2 && _newMax <= softMax) ? softMax : -inf;251let maxLim = min(hardMax, _newMax > _softMax && _max <= _softMax ? _softMax : max(_softMax, _newMax));252
253if (minLim == maxLim && minLim == 0)254maxLim = 100;255
256return [minLim, maxLim];257}
258
259// alternative: https://stackoverflow.com/a/2254896
260const fmtNum = new Intl.NumberFormat(navigator.language).format;261
262const M = Math;263
264const PI = M.PI;265const abs = M.abs;266const floor = M.floor;267const round = M.round;268const ceil = M.ceil;269const min = M.min;270const max = M.max;271const pow = M.pow;272const sign = M.sign;273const log10 = M.log10;274const log2 = M.log2;275// TODO: seems like this needs to match asinh impl if the passed v is tweaked?
276const sinh = (v, linthresh = 1) => M.sinh(v) * linthresh;277const asinh = (v, linthresh = 1) => M.asinh(v / linthresh);278
279const inf = Infinity;280
281function numIntDigits(x) {282return (log10((x ^ (x >> 31)) - (x >> 31)) | 0) + 1;283}
284
285function incrRound(num, incr) {286return round(num/incr)*incr;287}
288
289function clamp(num, _min, _max) {290return min(max(num, _min), _max);291}
292
293function fnOrSelf(v) {294return typeof v == "function" ? v : () => v;295}
296
297const retArg0 = _0 => _0;298
299const retArg1 = (_0, _1) => _1;300
301const retNull = _ => null;302
303const retTrue = _ => true;304
305const retEq = (a, b) => a == b;306
307function incrRoundUp(num, incr) {308return ceil(num/incr)*incr;309}
310
311function incrRoundDn(num, incr) {312return floor(num/incr)*incr;313}
314
315function roundDec(val, dec) {316return round(val * (dec = 10**dec)) / dec;317}
318
319const fixedDec = new Map();320
321function guessDec(num) {322return ((""+num).split(".")[1] || "").length;323}
324
325function genIncrs(base, minExp, maxExp, mults) {326let incrs = [];327
328let multDec = mults.map(guessDec);329
330for (let exp = minExp; exp < maxExp; exp++) {331let expa = abs(exp);332let mag = roundDec(pow(base, exp), expa);333
334for (let i = 0; i < mults.length; i++) {335let _incr = mults[i] * mag;336let dec = (_incr >= 0 && exp >= 0 ? 0 : expa) + (exp >= multDec[i] ? 0 : multDec[i]);337let incr = roundDec(_incr, dec);338incrs.push(incr);339fixedDec.set(incr, dec);340}341}342
343return incrs;344}
345
346//export const assign = Object.assign;
347
348const EMPTY_OBJ = {};349const EMPTY_ARR = [];350
351const nullNullTuple = [null, null];352
353const isArr = Array.isArray;354
355function isStr(v) {356return typeof v == 'string';357}
358
359function isObj(v) {360let is = false;361
362if (v != null) {363let c = v.constructor;364is = c == null || c == Object;365}366
367return is;368}
369
370function fastIsObj(v) {371return v != null && typeof v == 'object';372}
373
374function copy(o, _isObj = isObj) {375let out;376
377if (isArr(o)) {378let val = o.find(v => v != null);379
380if (isArr(val) || _isObj(val)) {381out = Array(o.length);382for (let i = 0; i < o.length; i++)383out[i] = copy(o[i], _isObj);384}385else386out = o.slice();387}388else if (_isObj(o)) {389out = {};390for (let k in o)391out[k] = copy(o[k], _isObj);392}393else394out = o;395
396return out;397}
398
399function assign(targ) {400let args = arguments;401
402for (let i = 1; i < args.length; i++) {403let src = args[i];404
405for (let key in src) {406if (isObj(targ[key]))407assign(targ[key], copy(src[key]));408else409targ[key] = copy(src[key]);410}411}412
413return targ;414}
415
416// nullModes
417const NULL_REMOVE = 0; // nulls are converted to undefined (e.g. for spanGaps: true)418const NULL_RETAIN = 1; // nulls are retained, with alignment artifacts set to undefined (default)419const NULL_EXPAND = 2; // nulls are expanded to include any adjacent alignment artifacts420
421// sets undefined values to nulls when adjacent to existing nulls (minesweeper)
422function nullExpand(yVals, nullIdxs, alignedLen) {423for (let i = 0, xi, lastNullIdx = -1; i < nullIdxs.length; i++) {424let nullIdx = nullIdxs[i];425
426if (nullIdx > lastNullIdx) {427xi = nullIdx - 1;428while (xi >= 0 && yVals[xi] == null)429yVals[xi--] = null;430
431xi = nullIdx + 1;432while (xi < alignedLen && yVals[xi] == null)433yVals[lastNullIdx = xi++] = null;434}435}436}
437
438// nullModes is a tables-matched array indicating how to treat nulls in each series
439// output is sorted ASC on the joined field (table[0]) and duplicate join values are collapsed
440function join(tables, nullModes) {441let xVals = new Set();442
443for (let ti = 0; ti < tables.length; ti++) {444let t = tables[ti];445let xs = t[0];446let len = xs.length;447
448for (let i = 0; i < len; i++)449xVals.add(xs[i]);450}451
452let data = [Array.from(xVals).sort((a, b) => a - b)];453
454let alignedLen = data[0].length;455
456let xIdxs = new Map();457
458for (let i = 0; i < alignedLen; i++)459xIdxs.set(data[0][i], i);460
461for (let ti = 0; ti < tables.length; ti++) {462let t = tables[ti];463let xs = t[0];464
465for (let si = 1; si < t.length; si++) {466let ys = t[si];467
468let yVals = Array(alignedLen).fill(undefined);469
470let nullMode = nullModes ? nullModes[ti][si] : NULL_RETAIN;471
472let nullIdxs = [];473
474for (let i = 0; i < ys.length; i++) {475let yVal = ys[i];476let alignedIdx = xIdxs.get(xs[i]);477
478if (yVal === null) {479if (nullMode != NULL_REMOVE) {480yVals[alignedIdx] = yVal;481
482if (nullMode == NULL_EXPAND)483nullIdxs.push(alignedIdx);484}485}486else487yVals[alignedIdx] = yVal;488}489
490nullExpand(yVals, nullIdxs, alignedLen);491
492data.push(yVals);493}494}495
496return data;497}
498
499const microTask = typeof queueMicrotask == "undefined" ? fn => Promise.resolve().then(fn) : queueMicrotask;500
501const WIDTH = "width";502const HEIGHT = "height";503const TOP = "top";504const BOTTOM = "bottom";505const LEFT = "left";506const RIGHT = "right";507const hexBlack = "#000";508const transparent = hexBlack + "0";509
510const mousemove = "mousemove";511const mousedown = "mousedown";512const mouseup = "mouseup";513const mouseenter = "mouseenter";514const mouseleave = "mouseleave";515const dblclick = "dblclick";516const resize = "resize";517const scroll = "scroll";518
519const change = "change";520const dppxchange = "dppxchange";521
522const pre = "u-";523
524const UPLOT = "uplot";525const ORI_HZ = pre + "hz";526const ORI_VT = pre + "vt";527const TITLE = pre + "title";528const WRAP = pre + "wrap";529const UNDER = pre + "under";530const OVER = pre + "over";531const AXIS = pre + "axis";532const OFF = pre + "off";533const SELECT = pre + "select";534const CURSOR_X = pre + "cursor-x";535const CURSOR_Y = pre + "cursor-y";536const CURSOR_PT = pre + "cursor-pt";537const LEGEND = pre + "legend";538const LEGEND_LIVE = pre + "live";539const LEGEND_INLINE = pre + "inline";540const LEGEND_THEAD = pre + "thead";541const LEGEND_SERIES = pre + "series";542const LEGEND_MARKER = pre + "marker";543const LEGEND_LABEL = pre + "label";544const LEGEND_VALUE = pre + "value";545
546const doc = document;547const win = window;548let pxRatio;549
550let query;551
552function setPxRatio() {553let _pxRatio = devicePixelRatio;554
555// during print preview, Chrome fires off these dppx queries even without changes556if (pxRatio != _pxRatio) {557pxRatio = _pxRatio;558
559query && off(change, query, setPxRatio);560query = matchMedia(`(min-resolution: ${pxRatio - 0.001}dppx) and (max-resolution: ${pxRatio + 0.001}dppx)`);561on(change, query, setPxRatio);562
563win.dispatchEvent(new CustomEvent(dppxchange));564}565}
566
567function addClass(el, c) {568if (c != null) {569let cl = el.classList;570!cl.contains(c) && cl.add(c);571}572}
573
574function remClass(el, c) {575let cl = el.classList;576cl.contains(c) && cl.remove(c);577}
578
579function setStylePx(el, name, value) {580el.style[name] = value + "px";581}
582
583function placeTag(tag, cls, targ, refEl) {584let el = doc.createElement(tag);585
586if (cls != null)587addClass(el, cls);588
589if (targ != null)590targ.insertBefore(el, refEl);591
592return el;593}
594
595function placeDiv(cls, targ) {596return placeTag("div", cls, targ);597}
598
599const xformCache = new WeakMap();600
601function elTrans(el, xPos, yPos, xMax, yMax) {602let xform = "translate(" + xPos + "px," + yPos + "px)";603let xformOld = xformCache.get(el);604
605if (xform != xformOld) {606el.style.transform = xform;607xformCache.set(el, xform);608
609if (xPos < 0 || yPos < 0 || xPos > xMax || yPos > yMax)610addClass(el, OFF);611else612remClass(el, OFF);613}614}
615
616const colorCache = new WeakMap();617
618function elColor(el, background, borderColor) {619let newColor = background + borderColor;620let oldColor = colorCache.get(el);621
622if (newColor != oldColor) {623colorCache.set(el, newColor);624el.style.background = background;625el.style.borderColor = borderColor;626}627}
628
629const sizeCache = new WeakMap();630
631function elSize(el, newWid, newHgt, centered) {632let newSize = newWid + "" + newHgt;633let oldSize = sizeCache.get(el);634
635if (newSize != oldSize) {636sizeCache.set(el, newSize);637el.style.height = newHgt + "px";638el.style.width = newWid + "px";639el.style.marginLeft = centered ? -newWid/2 + "px" : 0;640el.style.marginTop = centered ? -newHgt/2 + "px" : 0;641}642}
643
644const evOpts = {passive: true};645const evOpts2 = assign({capture: true}, evOpts);646
647function on(ev, el, cb, capt) {648el.addEventListener(ev, cb, capt ? evOpts2 : evOpts);649}
650
651function off(ev, el, cb, capt) {652el.removeEventListener(ev, cb, capt ? evOpts2 : evOpts);653}
654
655setPxRatio();656
657const months = [658"January",659"February",660"March",661"April",662"May",663"June",664"July",665"August",666"September",667"October",668"November",669"December",670];671
672const days = [673"Sunday",674"Monday",675"Tuesday",676"Wednesday",677"Thursday",678"Friday",679"Saturday",680];681
682function slice3(str) {683return str.slice(0, 3);684}
685
686const days3 = days.map(slice3);687
688const months3 = months.map(slice3);689
690const engNames = {691MMMM: months,692MMM: months3,693WWWW: days,694WWW: days3,695};696
697function zeroPad2(int) {698return (int < 10 ? '0' : '') + int;699}
700
701function zeroPad3(int) {702return (int < 10 ? '00' : int < 100 ? '0' : '') + int;703}
704
705/*
706function suffix(int) {
707let mod10 = int % 10;
708
709return int + (
710mod10 == 1 && int != 11 ? "st" :
711mod10 == 2 && int != 12 ? "nd" :
712mod10 == 3 && int != 13 ? "rd" : "th"
713);
714}
715*/
716
717const subs = {718// 2019719YYYY: d => d.getFullYear(),720// 19721YY: d => (d.getFullYear()+'').slice(2),722// July723MMMM: (d, names) => names.MMMM[d.getMonth()],724// Jul725MMM: (d, names) => names.MMM[d.getMonth()],726// 07727MM: d => zeroPad2(d.getMonth()+1),728// 7729M: d => d.getMonth()+1,730// 09731DD: d => zeroPad2(d.getDate()),732// 9733D: d => d.getDate(),734// Monday735WWWW: (d, names) => names.WWWW[d.getDay()],736// Mon737WWW: (d, names) => names.WWW[d.getDay()],738// 03739HH: d => zeroPad2(d.getHours()),740// 3741H: d => d.getHours(),742// 9 (12hr, unpadded)743h: d => {let h = d.getHours(); return h == 0 ? 12 : h > 12 ? h - 12 : h;},744// AM745AA: d => d.getHours() >= 12 ? 'PM' : 'AM',746// am747aa: d => d.getHours() >= 12 ? 'pm' : 'am',748// a749a: d => d.getHours() >= 12 ? 'p' : 'a',750// 09751mm: d => zeroPad2(d.getMinutes()),752// 9753m: d => d.getMinutes(),754// 09755ss: d => zeroPad2(d.getSeconds()),756// 9757s: d => d.getSeconds(),758// 374759fff: d => zeroPad3(d.getMilliseconds()),760};761
762function fmtDate(tpl, names) {763names = names || engNames;764let parts = [];765
766let R = /\{([a-z]+)\}|[^{]+/gi, m;767
768while (m = R.exec(tpl))769parts.push(m[0][0] == '{' ? subs[m[1]] : m[0]);770
771return d => {772let out = '';773
774for (let i = 0; i < parts.length; i++)775out += typeof parts[i] == "string" ? parts[i] : parts[i](d, names);776
777return out;778}779}
780
781const localTz = new Intl.DateTimeFormat().resolvedOptions().timeZone;782
783// https://stackoverflow.com/questions/15141762/how-to-initialize-a-javascript-date-to-a-particular-time-zone/53652131#53652131
784function tzDate(date, tz) {785let date2;786
787// perf optimization788if (tz == 'UTC' || tz == 'Etc/UTC')789date2 = new Date(+date + date.getTimezoneOffset() * 6e4);790else if (tz == localTz)791date2 = date;792else {793date2 = new Date(date.toLocaleString('en-US', {timeZone: tz}));794date2.setMilliseconds(date.getMilliseconds());795}796
797return date2;798}
799
800//export const series = [];
801
802// default formatters:
803
804const onlyWhole = v => v % 1 == 0;805
806const allMults = [1,2,2.5,5];807
808// ...0.01, 0.02, 0.025, 0.05, 0.1, 0.2, 0.25, 0.5
809const decIncrs = genIncrs(10, -16, 0, allMults);810
811// 1, 2, 2.5, 5, 10, 20, 25, 50...
812const oneIncrs = genIncrs(10, 0, 16, allMults);813
814// 1, 2, 5, 10, 20, 25, 50...
815const wholeIncrs = oneIncrs.filter(onlyWhole);816
817const numIncrs = decIncrs.concat(oneIncrs);818
819const NL = "\n";820
821const yyyy = "{YYYY}";822const NLyyyy = NL + yyyy;823const md = "{M}/{D}";824const NLmd = NL + md;825const NLmdyy = NLmd + "/{YY}";826
827const aa = "{aa}";828const hmm = "{h}:{mm}";829const hmmaa = hmm + aa;830const NLhmmaa = NL + hmmaa;831const ss = ":{ss}";832
833const _ = null;834
835function genTimeStuffs(ms) {836let s = ms * 1e3,837m = s * 60,838h = m * 60,839d = h * 24,840mo = d * 30,841y = d * 365;842
843// min of 1e-3 prevents setting a temporal x ticks too small since Date objects cannot advance ticks smaller than 1ms844let subSecIncrs = ms == 1 ? genIncrs(10, 0, 3, allMults).filter(onlyWhole) : genIncrs(10, -3, 0, allMults);845
846let timeIncrs = subSecIncrs.concat([847// minute divisors (# of secs)848s,849s * 5,850s * 10,851s * 15,852s * 30,853// hour divisors (# of mins)854m,855m * 5,856m * 10,857m * 15,858m * 30,859// day divisors (# of hrs)860h,861h * 2,862h * 3,863h * 4,864h * 6,865h * 8,866h * 12,867// month divisors TODO: need more?868d,869d * 2,870d * 3,871d * 4,872d * 5,873d * 6,874d * 7,875d * 8,876d * 9,877d * 10,878d * 15,879// year divisors (# months, approx)880mo,881mo * 2,882mo * 3,883mo * 4,884mo * 6,885// century divisors886y,887y * 2,888y * 5,889y * 10,890y * 25,891y * 50,892y * 100,893]);894
895// [0]: minimum num secs in the tick incr896// [1]: default tick format897// [2-7]: rollover tick formats898// [8]: mode: 0: replace [1] -> [2-7], 1: concat [1] + [2-7]899const _timeAxisStamps = [900// tick incr default year month day hour min sec mode901[y, yyyy, _, _, _, _, _, _, 1],902[d * 28, "{MMM}", NLyyyy, _, _, _, _, _, 1],903[d, md, NLyyyy, _, _, _, _, _, 1],904[h, "{h}" + aa, NLmdyy, _, NLmd, _, _, _, 1],905[m, hmmaa, NLmdyy, _, NLmd, _, _, _, 1],906[s, ss, NLmdyy + " " + hmmaa, _, NLmd + " " + hmmaa, _, NLhmmaa, _, 1],907[ms, ss + ".{fff}", NLmdyy + " " + hmmaa, _, NLmd + " " + hmmaa, _, NLhmmaa, _, 1],908];909
910// the ensures that axis ticks, values & grid are aligned to logical temporal breakpoints and not an arbitrary timestamp911// https://www.timeanddate.com/time/dst/912// https://www.timeanddate.com/time/dst/2019.html913// https://www.epochconverter.com/timezones914function timeAxisSplits(tzDate) {915return (self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace) => {916let splits = [];917let isYr = foundIncr >= y;918let isMo = foundIncr >= mo && foundIncr < y;919
920// get the timezone-adjusted date921let minDate = tzDate(scaleMin);922let minDateTs = roundDec(minDate * ms, 3);923
924// get ts of 12am (this lands us at or before the original scaleMin)925let minMin = mkDate(minDate.getFullYear(), isYr ? 0 : minDate.getMonth(), isMo || isYr ? 1 : minDate.getDate());926let minMinTs = roundDec(minMin * ms, 3);927
928if (isMo || isYr) {929let moIncr = isMo ? foundIncr / mo : 0;930let yrIncr = isYr ? foundIncr / y : 0;931// let tzOffset = scaleMin - minDateTs; // needed?932let split = minDateTs == minMinTs ? minDateTs : roundDec(mkDate(minMin.getFullYear() + yrIncr, minMin.getMonth() + moIncr, 1) * ms, 3);933let splitDate = new Date(round(split / ms));934let baseYear = splitDate.getFullYear();935let baseMonth = splitDate.getMonth();936
937for (let i = 0; split <= scaleMax; i++) {938let next = mkDate(baseYear + yrIncr * i, baseMonth + moIncr * i, 1);939let offs = next - tzDate(roundDec(next * ms, 3));940
941split = roundDec((+next + offs) * ms, 3);942
943if (split <= scaleMax)944splits.push(split);945}946}947else {948let incr0 = foundIncr >= d ? d : foundIncr;949let tzOffset = floor(scaleMin) - floor(minDateTs);950let split = minMinTs + tzOffset + incrRoundUp(minDateTs - minMinTs, incr0);951splits.push(split);952
953let date0 = tzDate(split);954
955let prevHour = date0.getHours() + (date0.getMinutes() / m) + (date0.getSeconds() / h);956let incrHours = foundIncr / h;957
958let minSpace = self.axes[axisIdx]._space;959let pctSpace = foundSpace / minSpace;960
961while (1) {962split = roundDec(split + foundIncr, ms == 1 ? 0 : 3);963
964if (split > scaleMax)965break;966
967if (incrHours > 1) {968let expectedHour = floor(roundDec(prevHour + incrHours, 6)) % 24;969let splitDate = tzDate(split);970let actualHour = splitDate.getHours();971
972let dstShift = actualHour - expectedHour;973
974if (dstShift > 1)975dstShift = -1;976
977split -= dstShift * h;978
979prevHour = (prevHour + incrHours) % 24;980
981// add a tick only if it's further than 70% of the min allowed label spacing982let prevSplit = splits[splits.length - 1];983let pctIncr = roundDec((split - prevSplit) / foundIncr, 3);984
985if (pctIncr * pctSpace >= .7)986splits.push(split);987}988else989splits.push(split);990}991}992
993return splits;994}995}996
997return [998timeIncrs,999_timeAxisStamps,1000timeAxisSplits,1001];1002}
1003
1004const [ timeIncrsMs, _timeAxisStampsMs, timeAxisSplitsMs ] = genTimeStuffs(1);1005const [ timeIncrsS, _timeAxisStampsS, timeAxisSplitsS ] = genTimeStuffs(1e-3);1006
1007// base 2
1008genIncrs(2, -53, 53, [1]);1009
1010/*
1011console.log({
1012decIncrs,
1013oneIncrs,
1014wholeIncrs,
1015numIncrs,
1016timeIncrs,
1017fixedDec,
1018});
1019*/
1020
1021function timeAxisStamps(stampCfg, fmtDate) {1022return stampCfg.map(s => s.map((v, i) =>1023i == 0 || i == 8 || v == null ? v : fmtDate(i == 1 || s[8] == 0 ? v : s[1] + v)1024));1025}
1026
1027// TODO: will need to accept spaces[] and pull incr into the loop when grid will be non-uniform, eg for log scales.
1028// currently we ignore this for months since they're *nearly* uniform and the added complexity is not worth it
1029function timeAxisVals(tzDate, stamps) {1030return (self, splits, axisIdx, foundSpace, foundIncr) => {1031let s = stamps.find(s => foundIncr >= s[0]) || stamps[stamps.length - 1];1032
1033// these track boundaries when a full label is needed again1034let prevYear;1035let prevMnth;1036let prevDate;1037let prevHour;1038let prevMins;1039let prevSecs;1040
1041return splits.map(split => {1042let date = tzDate(split);1043
1044let newYear = date.getFullYear();1045let newMnth = date.getMonth();1046let newDate = date.getDate();1047let newHour = date.getHours();1048let newMins = date.getMinutes();1049let newSecs = date.getSeconds();1050
1051let stamp = (1052newYear != prevYear && s[2] ||1053newMnth != prevMnth && s[3] ||1054newDate != prevDate && s[4] ||1055newHour != prevHour && s[5] ||1056newMins != prevMins && s[6] ||1057newSecs != prevSecs && s[7] ||1058s[1]1059);1060
1061prevYear = newYear;1062prevMnth = newMnth;1063prevDate = newDate;1064prevHour = newHour;1065prevMins = newMins;1066prevSecs = newSecs;1067
1068return stamp(date);1069});1070}1071}
1072
1073// for when axis.values is defined as a static fmtDate template string
1074function timeAxisVal(tzDate, dateTpl) {1075let stamp = fmtDate(dateTpl);1076return (self, splits, axisIdx, foundSpace, foundIncr) => splits.map(split => stamp(tzDate(split)));1077}
1078
1079function mkDate(y, m, d) {1080return new Date(y, m, d);1081}
1082
1083function timeSeriesStamp(stampCfg, fmtDate) {1084return fmtDate(stampCfg);1085}
1086const _timeSeriesStamp = '{YYYY}-{MM}-{DD} {h}:{mm}{aa}';1087
1088function timeSeriesVal(tzDate, stamp) {1089return (self, val) => stamp(tzDate(val));1090}
1091
1092function legendStroke(self, seriesIdx) {1093let s = self.series[seriesIdx];1094return s.width ? s.stroke(self, seriesIdx) : s.points.width ? s.points.stroke(self, seriesIdx) : null;1095}
1096
1097function legendFill(self, seriesIdx) {1098return self.series[seriesIdx].fill(self, seriesIdx);1099}
1100
1101const legendOpts = {1102show: true,1103live: true,1104isolate: false,1105markers: {1106show: true,1107width: 2,1108stroke: legendStroke,1109fill: legendFill,1110dash: "solid",1111},1112idx: null,1113idxs: null,1114values: [],1115};1116
1117function cursorPointShow(self, si) {1118let o = self.cursor.points;1119
1120let pt = placeDiv();1121
1122let size = o.size(self, si);1123setStylePx(pt, WIDTH, size);1124setStylePx(pt, HEIGHT, size);1125
1126let mar = size / -2;1127setStylePx(pt, "marginLeft", mar);1128setStylePx(pt, "marginTop", mar);1129
1130let width = o.width(self, si, size);1131width && setStylePx(pt, "borderWidth", width);1132
1133return pt;1134}
1135
1136function cursorPointFill(self, si) {1137let sp = self.series[si].points;1138return sp._fill || sp._stroke;1139}
1140
1141function cursorPointStroke(self, si) {1142let sp = self.series[si].points;1143return sp._stroke || sp._fill;1144}
1145
1146function cursorPointSize(self, si) {1147let sp = self.series[si].points;1148return ptDia(sp.width, 1);1149}
1150
1151function dataIdx(self, seriesIdx, cursorIdx) {1152return cursorIdx;1153}
1154
1155const moveTuple = [0,0];1156
1157function cursorMove(self, mouseLeft1, mouseTop1) {1158moveTuple[0] = mouseLeft1;1159moveTuple[1] = mouseTop1;1160return moveTuple;1161}
1162
1163function filtBtn0(self, targ, handle) {1164return e => {1165e.button == 0 && handle(e);1166};1167}
1168
1169function passThru(self, targ, handle) {1170return handle;1171}
1172
1173const cursorOpts = {1174show: true,1175x: true,1176y: true,1177lock: false,1178move: cursorMove,1179points: {1180show: cursorPointShow,1181size: cursorPointSize,1182width: 0,1183stroke: cursorPointStroke,1184fill: cursorPointFill,1185},1186
1187bind: {1188mousedown: filtBtn0,1189mouseup: filtBtn0,1190click: filtBtn0,1191dblclick: filtBtn0,1192
1193mousemove: passThru,1194mouseleave: passThru,1195mouseenter: passThru,1196},1197
1198drag: {1199setScale: true,1200x: true,1201y: false,1202dist: 0,1203uni: null,1204_x: false,1205_y: false,1206},1207
1208focus: {1209prox: -1,1210},1211
1212left: -10,1213top: -10,1214idx: null,1215dataIdx,1216idxs: null,1217};1218
1219const grid = {1220show: true,1221stroke: "rgba(0,0,0,0.07)",1222width: 2,1223// dash: [],
1224filter: retArg1,1225};1226
1227const ticks = assign({}, grid, {size: 10});1228
1229const 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"';1230const labelFont = "bold " + font;1231const lineMult = 1.5; // font-size multiplier1232
1233const xAxisOpts = {1234show: true,1235scale: "x",1236stroke: hexBlack,1237space: 50,1238gap: 5,1239size: 50,1240labelGap: 0,1241labelSize: 30,1242labelFont,1243side: 2,1244// class: "x-vals",
1245// incrs: timeIncrs,
1246// values: timeVals,
1247// filter: retArg1,
1248grid,1249ticks,1250font,1251rotate: 0,1252};1253
1254const numSeriesLabel = "Value";1255const timeSeriesLabel = "Time";1256
1257const xSeriesOpts = {1258show: true,1259scale: "x",1260auto: false,1261sorted: 1,1262// label: "Time",
1263// value: v => stamp(new Date(v * 1e3)),
1264
1265// internal caches1266min: inf,1267max: -inf,1268idxs: [],1269};1270
1271function numAxisVals(self, splits, axisIdx, foundSpace, foundIncr) {1272return splits.map(v => v == null ? "" : fmtNum(v));1273}
1274
1275function numAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {1276let splits = [];1277
1278let numDec = fixedDec.get(foundIncr) || 0;1279
1280scaleMin = forceMin ? scaleMin : roundDec(incrRoundUp(scaleMin, foundIncr), numDec);1281
1282for (let val = scaleMin; val <= scaleMax; val = roundDec(val + foundIncr, numDec))1283splits.push(Object.is(val, -0) ? 0 : val); // coalesces -01284
1285return splits;1286}
1287
1288// this doesnt work for sin, which needs to come off from 0 independently in pos and neg dirs
1289function logAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {1290const splits = [];1291
1292const logBase = self.scales[self.axes[axisIdx].scale].log;1293
1294const logFn = logBase == 10 ? log10 : log2;1295
1296const exp = floor(logFn(scaleMin));1297
1298foundIncr = pow(logBase, exp);1299
1300if (exp < 0)1301foundIncr = roundDec(foundIncr, -exp);1302
1303let split = scaleMin;1304
1305do {1306splits.push(split);1307split = roundDec(split + foundIncr, fixedDec.get(foundIncr));1308
1309if (split >= foundIncr * logBase)1310foundIncr = split;1311
1312} while (split <= scaleMax);1313
1314return splits;1315}
1316
1317function asinhAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace, forceMin) {1318let sc = self.scales[self.axes[axisIdx].scale];1319
1320let linthresh = sc.asinh;1321
1322let posSplits = scaleMax > linthresh ? logAxisSplits(self, axisIdx, max(linthresh, scaleMin), scaleMax, foundIncr) : [linthresh];1323let zero = scaleMax >= 0 && scaleMin <= 0 ? [0] : [];1324let negSplits = scaleMin < -linthresh ? logAxisSplits(self, axisIdx, max(linthresh, -scaleMax), -scaleMin, foundIncr): [linthresh];1325
1326return negSplits.reverse().map(v => -v).concat(zero, posSplits);1327}
1328
1329const RE_ALL = /./;1330const RE_12357 = /[12357]/;1331const RE_125 = /[125]/;1332const RE_1 = /1/;1333
1334function logAxisValsFilt(self, splits, axisIdx, foundSpace, foundIncr) {1335let axis = self.axes[axisIdx];1336let scaleKey = axis.scale;1337let sc = self.scales[scaleKey];1338
1339if (sc.distr == 3 && sc.log == 2)1340return splits;1341
1342let valToPos = self.valToPos;1343
1344let minSpace = axis._space;1345
1346let _10 = valToPos(10, scaleKey);1347
1348let re = (1349valToPos(9, scaleKey) - _10 >= minSpace ? RE_ALL :1350valToPos(7, scaleKey) - _10 >= minSpace ? RE_12357 :1351valToPos(5, scaleKey) - _10 >= minSpace ? RE_125 :1352RE_11353);1354
1355return splits.map(v => ((sc.distr == 4 && v == 0) || re.test(v)) ? v : null);1356}
1357
1358function numSeriesVal(self, val) {1359return val == null ? "" : fmtNum(val);1360}
1361
1362const yAxisOpts = {1363show: true,1364scale: "y",1365stroke: hexBlack,1366space: 30,1367gap: 5,1368size: 50,1369labelGap: 0,1370labelSize: 30,1371labelFont,1372side: 3,1373// class: "y-vals",
1374// incrs: numIncrs,
1375// values: (vals, space) => vals,
1376// filter: retArg1,
1377grid,1378ticks,1379font,1380rotate: 0,1381};1382
1383// takes stroke width
1384function ptDia(width, mult) {1385let dia = 3 + (width || 1) * 2;1386return roundDec(dia * mult, 3);1387}
1388
1389function seriesPointsShow(self, si) {1390let { scale, idxs } = self.series[0];1391let xData = self._data[0];1392let p0 = self.valToPos(xData[idxs[0]], scale, true);1393let p1 = self.valToPos(xData[idxs[1]], scale, true);1394let dim = abs(p1 - p0);1395
1396let s = self.series[si];1397// const dia = ptDia(s.width, pxRatio);
1398let maxPts = dim / (s.points.space * pxRatio);1399return idxs[1] - idxs[0] <= maxPts;1400}
1401
1402function seriesFillTo(self, seriesIdx, dataMin, dataMax) {1403let scale = self.scales[self.series[seriesIdx].scale];1404let isUpperBandEdge = self.bands && self.bands.some(b => b.series[0] == seriesIdx);1405return scale.distr == 3 || isUpperBandEdge ? scale.min : 0;1406}
1407
1408const facet = {1409scale: null,1410auto: true,1411
1412// internal caches1413min: inf,1414max: -inf,1415};1416
1417const xySeriesOpts = {1418show: true,1419auto: true,1420sorted: 0,1421alpha: 1,1422facets: [1423assign({}, facet, {scale: 'x'}),1424assign({}, facet, {scale: 'y'}),1425],1426};1427
1428const ySeriesOpts = {1429scale: "y",1430auto: true,1431sorted: 0,1432show: true,1433spanGaps: false,1434gaps: (self, seriesIdx, idx0, idx1, nullGaps) => nullGaps,1435alpha: 1,1436points: {1437show: seriesPointsShow,1438filter: null,1439// paths:1440// stroke: "#000",1441// fill: "#fff",1442// width: 1,1443// size: 10,1444},1445// label: "Value",
1446// value: v => v,
1447values: null,1448
1449// internal caches1450min: inf,1451max: -inf,1452idxs: [],1453
1454path: null,1455clip: null,1456};1457
1458function clampScale(self, val, scaleMin, scaleMax, scaleKey) {1459/*
1460if (val < 0) {
1461let cssHgt = self.bbox.height / pxRatio;
1462let absPos = self.valToPos(abs(val), scaleKey);
1463let fromBtm = cssHgt - absPos;
1464return self.posToVal(cssHgt + fromBtm, scaleKey);
1465}
1466*/
1467return scaleMin / 10;1468}
1469
1470const xScaleOpts = {1471time: FEAT_TIME,1472auto: true,1473distr: 1,1474log: 10,1475asinh: 1,1476min: null,1477max: null,1478dir: 1,1479ori: 0,1480};1481
1482const yScaleOpts = assign({}, xScaleOpts, {1483time: false,1484ori: 1,1485});1486
1487const syncs = {};1488
1489function _sync(key, opts) {1490let s = syncs[key];1491
1492if (!s) {1493s = {1494key,1495plots: [],1496sub(plot) {1497s.plots.push(plot);1498},1499unsub(plot) {1500s.plots = s.plots.filter(c => c != plot);1501},1502pub(type, self, x, y, w, h, i) {1503for (let j = 0; j < s.plots.length; j++)1504s.plots[j] != self && s.plots[j].pub(type, self, x, y, w, h, i);1505},1506};1507
1508if (key != null)1509syncs[key] = s;1510}1511
1512return s;1513}
1514
1515const BAND_CLIP_FILL = 1 << 0;1516const BAND_CLIP_STROKE = 1 << 1;1517
1518function orient(u, seriesIdx, cb) {1519const series = u.series[seriesIdx];1520const scales = u.scales;1521const bbox = u.bbox;1522const scaleX = u.mode == 2 ? scales[series.facets[0].scale] : scales[u.series[0].scale];1523
1524let dx = u._data[0],1525dy = u._data[seriesIdx],1526sx = scaleX,1527sy = u.mode == 2 ? scales[series.facets[1].scale] : scales[series.scale],1528l = bbox.left,1529t = bbox.top,1530w = bbox.width,1531h = bbox.height,1532H = u.valToPosH,1533V = u.valToPosV;1534
1535return (sx.ori == 01536? cb(1537series,1538dx,1539dy,1540sx,1541sy,1542H,1543V,1544l,1545t,1546w,1547h,1548moveToH,1549lineToH,1550rectH,1551arcH,1552bezierCurveToH,1553)1554: cb(1555series,1556dx,1557dy,1558sx,1559sy,1560V,1561H,1562t,1563l,1564h,1565w,1566moveToV,1567lineToV,1568rectV,1569arcV,1570bezierCurveToV,1571)1572);1573}
1574
1575// creates inverted band clip path (towards from stroke path -> yMax)
1576function clipBandLine(self, seriesIdx, idx0, idx1, strokePath) {1577return orient(self, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {1578let pxRound = series.pxRound;1579
1580const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);1581const lineTo = scaleX.ori == 0 ? lineToH : lineToV;1582
1583let frIdx, toIdx;1584
1585if (dir == 1) {1586frIdx = idx0;1587toIdx = idx1;1588}1589else {1590frIdx = idx1;1591toIdx = idx0;1592}1593
1594// path start1595let x0 = pxRound(valToPosX(dataX[frIdx], scaleX, xDim, xOff));1596let y0 = pxRound(valToPosY(dataY[frIdx], scaleY, yDim, yOff));1597// path end x1598let x1 = pxRound(valToPosX(dataX[toIdx], scaleX, xDim, xOff));1599// upper y limit1600let yLimit = pxRound(valToPosY(scaleY.max, scaleY, yDim, yOff));1601
1602let clip = new Path2D(strokePath);1603
1604lineTo(clip, x1, yLimit);1605lineTo(clip, x0, yLimit);1606lineTo(clip, x0, y0);1607
1608return clip;1609});1610}
1611
1612function clipGaps(gaps, ori, plotLft, plotTop, plotWid, plotHgt) {1613let clip = null;1614
1615// create clip path (invert gaps and non-gaps)1616if (gaps.length > 0) {1617clip = new Path2D();1618
1619const rect = ori == 0 ? rectH : rectV;1620
1621let prevGapEnd = plotLft;1622
1623for (let i = 0; i < gaps.length; i++) {1624let g = gaps[i];1625
1626if (g[1] > g[0]) {1627let w = g[0] - prevGapEnd;1628
1629w > 0 && rect(clip, prevGapEnd, plotTop, w, plotTop + plotHgt);1630
1631prevGapEnd = g[1];1632}1633}1634
1635let w = plotLft + plotWid - prevGapEnd;1636
1637w > 0 && rect(clip, prevGapEnd, plotTop, w, plotTop + plotHgt);1638}1639
1640return clip;1641}
1642
1643function addGap(gaps, fromX, toX) {1644let prevGap = gaps[gaps.length - 1];1645
1646if (prevGap && prevGap[0] == fromX) // TODO: gaps must be encoded at stroke widths?1647prevGap[1] = toX;1648else1649gaps.push([fromX, toX]);1650}
1651
1652function pxRoundGen(pxAlign) {1653return pxAlign == 0 ? retArg0 : pxAlign == 1 ? round : v => incrRound(v, pxAlign);1654}
1655
1656function rect(ori) {1657let moveTo = ori == 0 ?1658moveToH :1659moveToV;1660
1661let arcTo = ori == 0 ?1662(p, x1, y1, x2, y2, r) => { p.arcTo(x1, y1, x2, y2, r); } :1663(p, y1, x1, y2, x2, r) => { p.arcTo(x1, y1, x2, y2, r); };1664
1665let rect = ori == 0 ?1666(p, x, y, w, h) => { p.rect(x, y, w, h); } :1667(p, y, x, h, w) => { p.rect(x, y, w, h); };1668
1669return (p, x, y, w, h, r = 0) => {1670if (r == 0)1671rect(p, x, y, w, h);1672else {1673r = min(r, w / 2, h / 2);1674
1675// adapted from https://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-using-html-canvas/7838871#78388711676moveTo(p, x + r, y);1677arcTo(p, x + w, y, x + w, y + h, r);1678arcTo(p, x + w, y + h, x, y + h, r);1679arcTo(p, x, y + h, x, y, r);1680arcTo(p, x, y, x + w, y, r);1681p.closePath();1682}1683};1684}
1685
1686// orientation-inverting canvas functions
1687const moveToH = (p, x, y) => { p.moveTo(x, y); };1688const moveToV = (p, y, x) => { p.moveTo(x, y); };1689const lineToH = (p, x, y) => { p.lineTo(x, y); };1690const lineToV = (p, y, x) => { p.lineTo(x, y); };1691const rectH = rect(0);1692const rectV = rect(1);1693const arcH = (p, x, y, r, startAngle, endAngle) => { p.arc(x, y, r, startAngle, endAngle); };1694const arcV = (p, y, x, r, startAngle, endAngle) => { p.arc(x, y, r, startAngle, endAngle); };1695const bezierCurveToH = (p, bp1x, bp1y, bp2x, bp2y, p2x, p2y) => { p.bezierCurveTo(bp1x, bp1y, bp2x, bp2y, p2x, p2y); };1696const bezierCurveToV = (p, bp1y, bp1x, bp2y, bp2x, p2y, p2x) => { p.bezierCurveTo(bp1x, bp1y, bp2x, bp2y, p2x, p2y); };1697
1698// TODO: drawWrap(seriesIdx, drawPoints) (save, restore, translate, clip)
1699function points(opts) {1700return (u, seriesIdx, idx0, idx1, filtIdxs) => {1701// log("drawPoints()", arguments);1702
1703return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {1704let { pxRound, points } = series;1705
1706let moveTo, arc;1707
1708if (scaleX.ori == 0) {1709moveTo = moveToH;1710arc = arcH;1711}1712else {1713moveTo = moveToV;1714arc = arcV;1715}1716
1717const width = roundDec(points.width * pxRatio, 3);1718
1719let rad = (points.size - points.width) / 2 * pxRatio;1720let dia = roundDec(rad * 2, 3);1721
1722let fill = new Path2D();1723let clip = new Path2D();1724
1725let { left: lft, top: top, width: wid, height: hgt } = u.bbox;1726
1727rectH(clip,1728lft - dia,1729top - dia,1730wid + dia * 2,1731hgt + dia * 2,1732);1733
1734const drawPoint = pi => {1735if (dataY[pi] != null) {1736let x = pxRound(valToPosX(dataX[pi], scaleX, xDim, xOff));1737let y = pxRound(valToPosY(dataY[pi], scaleY, yDim, yOff));1738
1739moveTo(fill, x + rad, y);1740arc(fill, x, y, rad, 0, PI * 2);1741}1742};1743
1744if (filtIdxs)1745filtIdxs.forEach(drawPoint);1746else {1747for (let pi = idx0; pi <= idx1; pi++)1748drawPoint(pi);1749}1750
1751return {1752stroke: width > 0 ? fill : null,1753fill,1754clip,1755flags: BAND_CLIP_FILL | BAND_CLIP_STROKE,1756};1757});1758};1759}
1760
1761function _drawAcc(lineTo) {1762return (stroke, accX, minY, maxY, inY, outY) => {1763if (minY != maxY) {1764if (inY != minY && outY != minY)1765lineTo(stroke, accX, minY);1766if (inY != maxY && outY != maxY)1767lineTo(stroke, accX, maxY);1768
1769lineTo(stroke, accX, outY);1770}1771};1772}
1773
1774const drawAccH = _drawAcc(lineToH);1775const drawAccV = _drawAcc(lineToV);1776
1777function linear() {1778return (u, seriesIdx, idx0, idx1) => {1779return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {1780let pxRound = series.pxRound;1781
1782let lineTo, drawAcc;1783
1784if (scaleX.ori == 0) {1785lineTo = lineToH;1786drawAcc = drawAccH;1787}1788else {1789lineTo = lineToV;1790drawAcc = drawAccV;1791}1792
1793const dir = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);1794
1795const _paths = {stroke: new Path2D(), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};1796const stroke = _paths.stroke;1797
1798let minY = inf,1799maxY = -inf,1800inY, outY, outX, drawnAtX;1801
1802let gaps = [];1803
1804let accX = pxRound(valToPosX(dataX[dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));1805let accGaps = false;1806let prevYNull = false;1807
1808// data edges1809let lftIdx = nonNullIdx(dataY, idx0, idx1, 1 * dir);1810let rgtIdx = nonNullIdx(dataY, idx0, idx1, -1 * dir);1811let lftX = pxRound(valToPosX(dataX[lftIdx], scaleX, xDim, xOff));1812let rgtX = pxRound(valToPosX(dataX[rgtIdx], scaleX, xDim, xOff));1813
1814if (lftX > xOff)1815addGap(gaps, xOff, lftX);1816
1817for (let i = dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += dir) {1818let x = pxRound(valToPosX(dataX[i], scaleX, xDim, xOff));1819
1820if (x == accX) {1821if (dataY[i] != null) {1822outY = pxRound(valToPosY(dataY[i], scaleY, yDim, yOff));1823
1824if (minY == inf) {1825lineTo(stroke, x, outY);1826inY = outY;1827}1828
1829minY = min(outY, minY);1830maxY = max(outY, maxY);1831}1832else if (dataY[i] === null)1833accGaps = prevYNull = true;1834}1835else {1836let _addGap = false;1837
1838if (minY != inf) {1839drawAcc(stroke, accX, minY, maxY, inY, outY);1840outX = drawnAtX = accX;1841}1842else if (accGaps) {1843_addGap = true;1844accGaps = false;1845}1846
1847if (dataY[i] != null) {1848outY = pxRound(valToPosY(dataY[i], scaleY, yDim, yOff));1849lineTo(stroke, x, outY);1850minY = maxY = inY = outY;1851
1852// prior pixel can have data but still start a gap if ends with null1853if (prevYNull && x - accX > 1)1854_addGap = true;1855
1856prevYNull = false;1857}1858else {1859minY = inf;1860maxY = -inf;1861
1862if (dataY[i] === null) {1863accGaps = true;1864
1865if (x - accX > 1)1866_addGap = true;1867}1868}1869
1870_addGap && addGap(gaps, outX, x);1871
1872accX = x;1873}1874}1875
1876if (minY != inf && minY != maxY && drawnAtX != accX)1877drawAcc(stroke, accX, minY, maxY, inY, outY);1878
1879if (rgtX < xOff + xDim)1880addGap(gaps, rgtX, xOff + xDim);1881
1882if (series.fill != null) {1883let fill = _paths.fill = new Path2D(stroke);1884
1885let fillTo = pxRound(valToPosY(series.fillTo(u, seriesIdx, series.min, series.max), scaleY, yDim, yOff));1886
1887lineTo(fill, rgtX, fillTo);1888lineTo(fill, lftX, fillTo);1889}1890
1891_paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);1892
1893if (!series.spanGaps)1894_paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);1895
1896if (u.bands.length > 0) {1897// ADDL OPT: only create band clips for series that are band lower edges1898// if (b.series[1] == i && _paths.band == null)1899_paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);1900}1901
1902return _paths;1903});1904};1905}
1906
1907function stepped(opts) {1908const align = ifNull(opts.align, 1);1909// whether to draw ascenders/descenders at null/gap bondaries1910const ascDesc = ifNull(opts.ascDesc, false);1911
1912return (u, seriesIdx, idx0, idx1) => {1913return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {1914let pxRound = series.pxRound;1915
1916let lineTo = scaleX.ori == 0 ? lineToH : lineToV;1917
1918const _paths = {stroke: new Path2D(), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};1919const stroke = _paths.stroke;1920
1921const _dir = 1 * scaleX.dir * (scaleX.ori == 0 ? 1 : -1);1922
1923idx0 = nonNullIdx(dataY, idx0, idx1, 1);1924idx1 = nonNullIdx(dataY, idx0, idx1, -1);1925
1926let gaps = [];1927let inGap = false;1928let prevYPos = pxRound(valToPosY(dataY[_dir == 1 ? idx0 : idx1], scaleY, yDim, yOff));1929let firstXPos = pxRound(valToPosX(dataX[_dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));1930let prevXPos = firstXPos;1931
1932lineTo(stroke, firstXPos, prevYPos);1933
1934for (let i = _dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dir) {1935let yVal1 = dataY[i];1936
1937let x1 = pxRound(valToPosX(dataX[i], scaleX, xDim, xOff));1938
1939if (yVal1 == null) {1940if (yVal1 === null) {1941addGap(gaps, prevXPos, x1);1942inGap = true;1943}1944continue;1945}1946
1947let y1 = pxRound(valToPosY(yVal1, scaleY, yDim, yOff));1948
1949if (inGap) {1950addGap(gaps, prevXPos, x1);1951inGap = false;1952}1953
1954if (align == 1)1955lineTo(stroke, x1, prevYPos);1956else1957lineTo(stroke, prevXPos, y1);1958
1959lineTo(stroke, x1, y1);1960
1961prevYPos = y1;1962prevXPos = x1;1963}1964
1965if (series.fill != null) {1966let fill = _paths.fill = new Path2D(stroke);1967
1968let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);1969let minY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff));1970
1971lineTo(fill, prevXPos, minY);1972lineTo(fill, firstXPos, minY);1973}1974
1975_paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);1976
1977// expand/contract clips for ascenders/descenders1978let halfStroke = (series.width * pxRatio) / 2;1979let startsOffset = (ascDesc || align == 1) ? halfStroke : -halfStroke;1980let endsOffset = (ascDesc || align == -1) ? -halfStroke : halfStroke;1981
1982gaps.forEach(g => {1983g[0] += startsOffset;1984g[1] += endsOffset;1985});1986
1987if (!series.spanGaps)1988_paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);1989
1990if (u.bands.length > 0) {1991// ADDL OPT: only create band clips for series that are band lower edges1992// if (b.series[1] == i && _paths.band == null)1993_paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);1994}1995
1996return _paths;1997});1998};1999}
2000
2001function bars(opts) {2002opts = opts || EMPTY_OBJ;2003const size = ifNull(opts.size, [0.6, inf, 1]);2004const align = opts.align || 0;2005const extraGap = (opts.gap || 0) * pxRatio;2006
2007const radius = ifNull(opts.radius, 0);2008
2009const gapFactor = 1 - size[0];2010const maxWidth = ifNull(size[1], inf) * pxRatio;2011const minWidth = ifNull(size[2], 1) * pxRatio;2012
2013const disp = ifNull(opts.disp, EMPTY_OBJ);2014const _each = ifNull(opts.each, _ => {});2015
2016const { fill: dispFills, stroke: dispStrokes } = disp;2017
2018return (u, seriesIdx, idx0, idx1) => {2019return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {2020let pxRound = series.pxRound;2021
2022const _dirX = scaleX.dir * (scaleX.ori == 0 ? 1 : -1);2023const _dirY = scaleY.dir * (scaleY.ori == 1 ? 1 : -1);2024
2025let rect = scaleX.ori == 0 ? rectH : rectV;2026
2027let each = scaleX.ori == 0 ? _each : (u, seriesIdx, i, top, lft, hgt, wid) => {2028_each(u, seriesIdx, i, lft, top, wid, hgt);2029};2030
2031let fillToY = series.fillTo(u, seriesIdx, series.min, series.max);2032
2033let y0Pos = valToPosY(fillToY, scaleY, yDim, yOff);2034
2035// barWid is to center of stroke2036let xShift, barWid;2037
2038let strokeWidth = pxRound(series.width * pxRatio);2039
2040let multiPath = false;2041
2042let fillColors = null;2043let fillPaths = null;2044let strokeColors = null;2045let strokePaths = null;2046
2047if (dispFills != null && dispStrokes != null) {2048multiPath = true;2049
2050fillColors = dispFills.values(u, seriesIdx, idx0, idx1);2051fillPaths = new Map();2052(new Set(fillColors)).forEach(color => {2053if (color != null)2054fillPaths.set(color, new Path2D());2055});2056
2057strokeColors = dispStrokes.values(u, seriesIdx, idx0, idx1);2058strokePaths = new Map();2059(new Set(strokeColors)).forEach(color => {2060if (color != null)2061strokePaths.set(color, new Path2D());2062});2063}2064
2065let { x0, size } = disp;2066
2067if (x0 != null && size != null) {2068dataX = x0.values(u, seriesIdx, idx0, idx1);2069
2070if (x0.unit == 2)2071dataX = dataX.map(pct => u.posToVal(xOff + pct * xDim, scaleX.key, true));2072
2073// assumes uniform sizes, for now2074let sizes = size.values(u, seriesIdx, idx0, idx1);2075
2076if (size.unit == 2)2077barWid = sizes[0] * xDim;2078else2079barWid = valToPosX(sizes[0], scaleX, xDim, xOff) - valToPosX(0, scaleX, xDim, xOff); // assumes linear scale (delta from 0)2080
2081barWid = pxRound(barWid - strokeWidth);2082
2083xShift = (_dirX == 1 ? -strokeWidth / 2 : barWid + strokeWidth / 2);2084}2085else {2086let colWid = xDim;2087
2088if (dataX.length > 1) {2089// prior index with non-undefined y data2090let prevIdx = null;2091
2092// scan full dataset for smallest adjacent delta2093// will not work properly for non-linear x scales, since does not do expensive valToPosX calcs till end2094for (let i = 0, minDelta = Infinity; i < dataX.length; i++) {2095if (dataY[i] !== undefined) {2096if (prevIdx != null) {2097let delta = abs(dataX[i] - dataX[prevIdx]);2098
2099if (delta < minDelta) {2100minDelta = delta;2101colWid = abs(valToPosX(dataX[i], scaleX, xDim, xOff) - valToPosX(dataX[prevIdx], scaleX, xDim, xOff));2102}2103}2104
2105prevIdx = i;2106}2107}2108}2109
2110let gapWid = colWid * gapFactor;2111
2112barWid = pxRound(min(maxWidth, max(minWidth, colWid - gapWid)) - strokeWidth - extraGap);2113
2114xShift = (align == 0 ? barWid / 2 : align == _dirX ? 0 : barWid) - align * _dirX * extraGap / 2;2115}2116
2117const _paths = {stroke: null, fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL | BAND_CLIP_STROKE}; // disp, geom2118
2119const hasBands = u.bands.length > 0;2120let yLimit;2121
2122if (hasBands) {2123// ADDL OPT: only create band clips for series that are band lower edges2124// if (b.series[1] == i && _paths.band == null)2125_paths.band = new Path2D();2126yLimit = pxRound(valToPosY(scaleY.max, scaleY, yDim, yOff));2127}2128
2129const stroke = multiPath ? null : new Path2D();2130const band = _paths.band;2131
2132for (let i = _dirX == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dirX) {2133let yVal = dataY[i];2134
2135/*2136// interpolate upwards band clips
2137if (yVal == null) {
2138// if (hasBands)
2139// yVal = costlyLerp(i, idx0, idx1, _dirX, dataY);
2140// else
2141continue;
2142}
2143*/
2144
2145let xVal = scaleX.distr != 2 || disp != null ? dataX[i] : i;2146
2147// TODO: all xPos can be pre-computed once for all series in aligned set2148let xPos = valToPosX(xVal, scaleX, xDim, xOff);2149let yPos = valToPosY(ifNull(yVal, fillToY) , scaleY, yDim, yOff);2150
2151let lft = pxRound(xPos - xShift);2152let btm = pxRound(max(yPos, y0Pos));2153let top = pxRound(min(yPos, y0Pos));2154// this includes the stroke2155let barHgt = btm - top;2156
2157let r = radius * barWid;2158
2159if (yVal != null) { // && yVal != fillToY (0 height bar)2160if (multiPath) {2161if (strokeWidth > 0 && strokeColors[i] != null)2162rect(strokePaths.get(strokeColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);2163
2164if (fillColors[i] != null)2165rect(fillPaths.get(fillColors[i]), lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);2166}2167else2168rect(stroke, lft, top + floor(strokeWidth / 2), barWid, max(0, barHgt - strokeWidth), r);2169
2170each(u, seriesIdx, i,2171lft - strokeWidth / 2,2172top,2173barWid + strokeWidth,2174barHgt,2175);2176}2177
2178if (hasBands) {2179if (_dirY == 1) {2180btm = top;2181top = yLimit;2182}2183else {2184top = btm;2185btm = yLimit;2186}2187
2188barHgt = btm - top;2189
2190rect(band, lft - strokeWidth / 2, top, barWid + strokeWidth, max(0, barHgt), 0);2191}2192}2193
2194if (strokeWidth > 0)2195_paths.stroke = multiPath ? strokePaths : stroke;2196
2197_paths.fill = multiPath ? fillPaths : stroke;2198
2199return _paths;2200});2201};2202}
2203
2204function splineInterp(interp, opts) {2205return (u, seriesIdx, idx0, idx1) => {2206return orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {2207let pxRound = series.pxRound;2208
2209let moveTo, bezierCurveTo, lineTo;2210
2211if (scaleX.ori == 0) {2212moveTo = moveToH;2213lineTo = lineToH;2214bezierCurveTo = bezierCurveToH;2215}2216else {2217moveTo = moveToV;2218lineTo = lineToV;2219bezierCurveTo = bezierCurveToV;2220}2221
2222const _dir = 1 * scaleX.dir * (scaleX.ori == 0 ? 1 : -1);2223
2224idx0 = nonNullIdx(dataY, idx0, idx1, 1);2225idx1 = nonNullIdx(dataY, idx0, idx1, -1);2226
2227let gaps = [];2228let inGap = false;2229let firstXPos = pxRound(valToPosX(dataX[_dir == 1 ? idx0 : idx1], scaleX, xDim, xOff));2230let prevXPos = firstXPos;2231
2232let xCoords = [];2233let yCoords = [];2234
2235for (let i = _dir == 1 ? idx0 : idx1; i >= idx0 && i <= idx1; i += _dir) {2236let yVal = dataY[i];2237let xVal = dataX[i];2238let xPos = valToPosX(xVal, scaleX, xDim, xOff);2239
2240if (yVal == null) {2241if (yVal === null) {2242addGap(gaps, prevXPos, xPos);2243inGap = true;2244}2245continue;2246}2247else {2248if (inGap) {2249addGap(gaps, prevXPos, xPos);2250inGap = false;2251}2252
2253xCoords.push((prevXPos = xPos));2254yCoords.push(valToPosY(dataY[i], scaleY, yDim, yOff));2255}2256}2257
2258const _paths = {stroke: interp(xCoords, yCoords, moveTo, lineTo, bezierCurveTo, pxRound), fill: null, clip: null, band: null, gaps: null, flags: BAND_CLIP_FILL};2259const stroke = _paths.stroke;2260
2261if (series.fill != null && stroke != null) {2262let fill = _paths.fill = new Path2D(stroke);2263
2264let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);2265let minY = pxRound(valToPosY(fillTo, scaleY, yDim, yOff));2266
2267lineTo(fill, prevXPos, minY);2268lineTo(fill, firstXPos, minY);2269}2270
2271_paths.gaps = gaps = series.gaps(u, seriesIdx, idx0, idx1, gaps);2272
2273if (!series.spanGaps)2274_paths.clip = clipGaps(gaps, scaleX.ori, xOff, yOff, xDim, yDim);2275
2276if (u.bands.length > 0) {2277// ADDL OPT: only create band clips for series that are band lower edges2278// if (b.series[1] == i && _paths.band == null)2279_paths.band = clipBandLine(u, seriesIdx, idx0, idx1, stroke);2280}2281
2282return _paths;2283
2284// if FEAT_PATHS: false in rollup.config.js2285// u.ctx.save();2286// u.ctx.beginPath();2287// u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);2288// u.ctx.clip();2289// u.ctx.strokeStyle = u.series[sidx].stroke;2290// u.ctx.stroke(stroke);2291// u.ctx.fillStyle = u.series[sidx].fill;2292// u.ctx.fill(fill);2293// u.ctx.restore();2294// return null;2295});2296};2297}
2298
2299function monotoneCubic(opts) {2300return splineInterp(_monotoneCubic);2301}
2302
2303// Monotone Cubic Spline interpolation, adapted from the Chartist.js implementation:
2304// https://github.com/gionkunz/chartist-js/blob/e7e78201bffe9609915e5e53cfafa29a5d6c49f9/src/scripts/interpolation.js#L240-L369
2305function _monotoneCubic(xs, ys, moveTo, lineTo, bezierCurveTo, pxRound) {2306const n = xs.length;2307
2308if (n < 2)2309return null;2310
2311const path = new Path2D();2312
2313moveTo(path, xs[0], ys[0]);2314
2315if (n == 2)2316lineTo(path, xs[1], ys[1]);2317else {2318let ms = Array(n),2319ds = Array(n - 1),2320dys = Array(n - 1),2321dxs = Array(n - 1);2322
2323// calc deltas and derivative2324for (let i = 0; i < n - 1; i++) {2325dys[i] = ys[i + 1] - ys[i];2326dxs[i] = xs[i + 1] - xs[i];2327ds[i] = dys[i] / dxs[i];2328}2329
2330// determine desired slope (m) at each point using Fritsch-Carlson method2331// http://math.stackexchange.com/questions/45218/implementation-of-monotone-cubic-interpolation2332ms[0] = ds[0];2333
2334for (let i = 1; i < n - 1; i++) {2335if (ds[i] === 0 || ds[i - 1] === 0 || (ds[i - 1] > 0) !== (ds[i] > 0))2336ms[i] = 0;2337else {2338ms[i] = 3 * (dxs[i - 1] + dxs[i]) / (2339(2 * dxs[i] + dxs[i - 1]) / ds[i - 1] +2340(dxs[i] + 2 * dxs[i - 1]) / ds[i]2341);2342
2343if (!isFinite(ms[i]))2344ms[i] = 0;2345}2346}2347
2348ms[n - 1] = ds[n - 2];2349
2350for (let i = 0; i < n - 1; i++) {2351bezierCurveTo(2352path,2353xs[i] + dxs[i] / 3,2354ys[i] + ms[i] * dxs[i] / 3,2355xs[i + 1] - dxs[i] / 3,2356ys[i + 1] - ms[i + 1] * dxs[i] / 3,2357xs[i + 1],2358ys[i + 1],2359);2360}2361}2362
2363return path;2364}
2365
2366const cursorPlots = new Set();2367
2368function invalidateRects() {2369cursorPlots.forEach(u => {2370u.syncRect(true);2371});2372}
2373
2374on(resize, win, invalidateRects);2375on(scroll, win, invalidateRects, true);2376
2377const linearPath = linear() ;2378const pointsPath = points() ;2379
2380function setDefaults(d, xo, yo, initY) {2381let d2 = initY ? [d[0], d[1]].concat(d.slice(2)) : [d[0]].concat(d.slice(1));2382return d2.map((o, i) => setDefault(o, i, xo, yo));2383}
2384
2385function setDefaults2(d, xyo) {2386return d.map((o, i) => i == 0 ? null : assign({}, xyo, o)); // todo: assign() will not merge facet arrays2387}
2388
2389function setDefault(o, i, xo, yo) {2390return assign({}, (i == 0 ? xo : yo), o);2391}
2392
2393function snapNumX(self, dataMin, dataMax) {2394return dataMin == null ? nullNullTuple : [dataMin, dataMax];2395}
2396
2397const snapTimeX = snapNumX;2398
2399// this ensures that non-temporal/numeric y-axes get multiple-snapped padding added above/below
2400// TODO: also account for incrs when snapping to ensure top of axis gets a tick & value
2401function snapNumY(self, dataMin, dataMax) {2402return dataMin == null ? nullNullTuple : rangeNum(dataMin, dataMax, rangePad, true);2403}
2404
2405function snapLogY(self, dataMin, dataMax, scale) {2406return dataMin == null ? nullNullTuple : rangeLog(dataMin, dataMax, self.scales[scale].log, false);2407}
2408
2409const snapLogX = snapLogY;2410
2411function snapAsinhY(self, dataMin, dataMax, scale) {2412return dataMin == null ? nullNullTuple : rangeAsinh(dataMin, dataMax, self.scales[scale].log, false);2413}
2414
2415const snapAsinhX = snapAsinhY;2416
2417// dim is logical (getClientBoundingRect) pixels, not canvas pixels
2418function findIncr(minVal, maxVal, incrs, dim, minSpace) {2419let intDigits = max(numIntDigits(minVal), numIntDigits(maxVal));2420
2421let delta = maxVal - minVal;2422
2423let incrIdx = closestIdx((minSpace / dim) * delta, incrs);2424
2425do {2426let foundIncr = incrs[incrIdx];2427let foundSpace = dim * foundIncr / delta;2428
2429if (foundSpace >= minSpace && intDigits + (foundIncr < 5 ? fixedDec.get(foundIncr) : 0) <= 17)2430return [foundIncr, foundSpace];2431} while (++incrIdx < incrs.length);2432
2433return [0, 0];2434}
2435
2436function pxRatioFont(font) {2437let fontSize, fontSizeCss;2438font = font.replace(/(\d+)px/, (m, p1) => (fontSize = round((fontSizeCss = +p1) * pxRatio)) + 'px');2439return [font, fontSize, fontSizeCss];2440}
2441
2442function syncFontSize(axis) {2443if (axis.show) {2444[axis.font, axis.labelFont].forEach(f => {2445let size = roundDec(f[2] * pxRatio, 1);2446f[0] = f[0].replace(/[0-9.]+px/, size + 'px');2447f[1] = size;2448});2449}2450}
2451
2452function uPlot(opts, data, then) {2453const self = {2454mode: ifNull(opts.mode, 1),2455};2456
2457const mode = self.mode;2458
2459// TODO: cache denoms & mins scale.cache = {r, min, }2460function getValPct(val, scale) {2461let _val = (2462scale.distr == 3 ? log10(val > 0 ? val : scale.clamp(self, val, scale.min, scale.max, scale.key)) :2463scale.distr == 4 ? asinh(val, scale.asinh) :2464val
2465);2466
2467return (_val - scale._min) / (scale._max - scale._min);2468}2469
2470function getHPos(val, scale, dim, off) {2471let pct = getValPct(val, scale);2472return off + dim * (scale.dir == -1 ? (1 - pct) : pct);2473}2474
2475function getVPos(val, scale, dim, off) {2476let pct = getValPct(val, scale);2477return off + dim * (scale.dir == -1 ? pct : (1 - pct));2478}2479
2480function getPos(val, scale, dim, off) {2481return scale.ori == 0 ? getHPos(val, scale, dim, off) : getVPos(val, scale, dim, off);2482}2483
2484self.valToPosH = getHPos;2485self.valToPosV = getVPos;2486
2487let ready = false;2488self.status = 0;2489
2490const root = self.root = placeDiv(UPLOT);2491
2492if (opts.id != null)2493root.id = opts.id;2494
2495addClass(root, opts.class);2496
2497if (opts.title) {2498let title = placeDiv(TITLE, root);2499title.textContent = opts.title;2500}2501
2502const can = placeTag("canvas");2503const ctx = self.ctx = can.getContext("2d");2504
2505const wrap = placeDiv(WRAP, root);2506const under = self.under = placeDiv(UNDER, wrap);2507wrap.appendChild(can);2508const over = self.over = placeDiv(OVER, wrap);2509
2510opts = copy(opts);2511
2512const pxAlign = +ifNull(opts.pxAlign, 1);2513
2514const pxRound = pxRoundGen(pxAlign);2515
2516(opts.plugins || []).forEach(p => {2517if (p.opts)2518opts = p.opts(self, opts) || opts;2519});2520
2521const ms = opts.ms || 1e-3;2522
2523const series = self.series = mode == 1 ?2524setDefaults(opts.series || [], xSeriesOpts, ySeriesOpts, false) :2525setDefaults2(opts.series || [null], xySeriesOpts);2526const axes = self.axes = setDefaults(opts.axes || [], xAxisOpts, yAxisOpts, true);2527const scales = self.scales = {};2528const bands = self.bands = opts.bands || [];2529
2530bands.forEach(b => {2531b.fill = fnOrSelf(b.fill || null);2532});2533
2534const xScaleKey = mode == 2 ? series[1].facets[0].scale : series[0].scale;2535
2536const drawOrderMap = {2537axes: drawAxesGrid,2538series: drawSeries,2539};2540
2541const drawOrder = (opts.drawOrder || ["axes", "series"]).map(key => drawOrderMap[key]);2542
2543function initScale(scaleKey) {2544let sc = scales[scaleKey];2545
2546if (sc == null) {2547let scaleOpts = (opts.scales || EMPTY_OBJ)[scaleKey] || EMPTY_OBJ;2548
2549if (scaleOpts.from != null) {2550// ensure parent is initialized2551initScale(scaleOpts.from);2552// dependent scales inherit2553scales[scaleKey] = assign({}, scales[scaleOpts.from], scaleOpts, {key: scaleKey});2554}2555else {2556sc = scales[scaleKey] = assign({}, (scaleKey == xScaleKey ? xScaleOpts : yScaleOpts), scaleOpts);2557
2558if (mode == 2)2559sc.time = false;2560
2561sc.key = scaleKey;2562
2563let isTime = sc.time;2564
2565let rn = sc.range;2566
2567let rangeIsArr = isArr(rn);2568
2569if (scaleKey != xScaleKey || mode == 2) {2570// if range array has null limits, it should be auto2571if (rangeIsArr && (rn[0] == null || rn[1] == null)) {2572rn = {2573min: rn[0] == null ? autoRangePart : {2574mode: 1,2575hard: rn[0],2576soft: rn[0],2577},2578max: rn[1] == null ? autoRangePart : {2579mode: 1,2580hard: rn[1],2581soft: rn[1],2582},2583};2584rangeIsArr = false;2585}2586
2587if (!rangeIsArr && isObj(rn)) {2588let cfg = rn;2589// this is similar to snapNumY2590rn = (self, dataMin, dataMax) => dataMin == null ? nullNullTuple : rangeNum(dataMin, dataMax, cfg);2591}2592}2593
2594sc.range = fnOrSelf(rn || (isTime ? snapTimeX : scaleKey == xScaleKey ?2595(sc.distr == 3 ? snapLogX : sc.distr == 4 ? snapAsinhX : snapNumX) :2596(sc.distr == 3 ? snapLogY : sc.distr == 4 ? snapAsinhY : snapNumY)2597));2598
2599sc.auto = fnOrSelf(rangeIsArr ? false : sc.auto);2600
2601sc.clamp = fnOrSelf(sc.clamp || clampScale);2602
2603// caches for expensive ops like asinh() & log()2604sc._min = sc._max = null;2605}2606}2607}2608
2609initScale("x");2610initScale("y");2611
2612// TODO: init scales from facets in mode: 22613if (mode == 1) {2614series.forEach(s => {2615initScale(s.scale);2616});2617}2618
2619axes.forEach(a => {2620initScale(a.scale);2621});2622
2623for (let k in opts.scales)2624initScale(k);2625
2626const scaleX = scales[xScaleKey];2627
2628const xScaleDistr = scaleX.distr;2629
2630let valToPosX, valToPosY;2631
2632if (scaleX.ori == 0) {2633addClass(root, ORI_HZ);2634valToPosX = getHPos;2635valToPosY = getVPos;2636/*2637updOriDims = () => {
2638xDimCan = plotWid;
2639xOffCan = plotLft;
2640yDimCan = plotHgt;
2641yOffCan = plotTop;
2642
2643xDimCss = plotWidCss;
2644xOffCss = plotLftCss;
2645yDimCss = plotHgtCss;
2646yOffCss = plotTopCss;
2647};
2648*/
2649}2650else {2651addClass(root, ORI_VT);2652valToPosX = getVPos;2653valToPosY = getHPos;2654/*2655updOriDims = () => {
2656xDimCan = plotHgt;
2657xOffCan = plotTop;
2658yDimCan = plotWid;
2659yOffCan = plotLft;
2660
2661xDimCss = plotHgtCss;
2662xOffCss = plotTopCss;
2663yDimCss = plotWidCss;
2664yOffCss = plotLftCss;
2665};
2666*/
2667}2668
2669const pendScales = {};2670
2671// explicitly-set initial scales2672for (let k in scales) {2673let sc = scales[k];2674
2675if (sc.min != null || sc.max != null) {2676pendScales[k] = {min: sc.min, max: sc.max};2677sc.min = sc.max = null;2678}2679}2680
2681// self.tz = opts.tz || Intl.DateTimeFormat().resolvedOptions().timeZone;
2682const _tzDate = (opts.tzDate || (ts => new Date(round(ts / ms))));2683const _fmtDate = (opts.fmtDate || fmtDate);2684
2685const _timeAxisSplits = (ms == 1 ? timeAxisSplitsMs(_tzDate) : timeAxisSplitsS(_tzDate));2686const _timeAxisVals = timeAxisVals(_tzDate, timeAxisStamps((ms == 1 ? _timeAxisStampsMs : _timeAxisStampsS), _fmtDate));2687const _timeSeriesVal = timeSeriesVal(_tzDate, timeSeriesStamp(_timeSeriesStamp, _fmtDate));2688
2689const activeIdxs = [];2690
2691const legend = (self.legend = assign({}, legendOpts, opts.legend));2692const showLegend = legend.show;2693const markers = legend.markers;2694
2695{2696legend.idxs = activeIdxs;2697
2698markers.width = fnOrSelf(markers.width);2699markers.dash = fnOrSelf(markers.dash);2700markers.stroke = fnOrSelf(markers.stroke);2701markers.fill = fnOrSelf(markers.fill);2702}2703
2704let legendEl;2705let legendRows = [];2706let legendCells = [];2707let legendCols;2708let multiValLegend = false;2709let NULL_LEGEND_VALUES = {};2710
2711if (legend.live) {2712const getMultiVals = series[1] ? series[1].values : null;2713multiValLegend = getMultiVals != null;2714legendCols = multiValLegend ? getMultiVals(self, 1, 0) : {_: 0};2715
2716for (let k in legendCols)2717NULL_LEGEND_VALUES[k] = "--";2718}2719
2720if (showLegend) {2721legendEl = placeTag("table", LEGEND, root);2722
2723if (multiValLegend) {2724let head = placeTag("tr", LEGEND_THEAD, legendEl);2725placeTag("th", null, head);2726
2727for (var key in legendCols)2728placeTag("th", LEGEND_LABEL, head).textContent = key;2729}2730else {2731addClass(legendEl, LEGEND_INLINE);2732legend.live && addClass(legendEl, LEGEND_LIVE);2733}2734}2735
2736const son = {show: true};2737const soff = {show: false};2738
2739function initLegendRow(s, i) {2740if (i == 0 && (multiValLegend || !legend.live || mode == 2))2741return nullNullTuple;2742
2743let cells = [];2744
2745let row = placeTag("tr", LEGEND_SERIES, legendEl, legendEl.childNodes[i]);2746
2747addClass(row, s.class);2748
2749if (!s.show)2750addClass(row, OFF);2751
2752let label = placeTag("th", null, row);2753
2754if (markers.show) {2755let indic = placeDiv(LEGEND_MARKER, label);2756
2757if (i > 0) {2758let width = markers.width(self, i);2759
2760if (width)2761indic.style.border = width + "px " + markers.dash(self, i) + " " + markers.stroke(self, i);2762
2763indic.style.background = markers.fill(self, i);2764}2765}2766
2767let text = placeDiv(LEGEND_LABEL, label);2768text.textContent = s.label;2769
2770if (i > 0) {2771if (!markers.show)2772text.style.color = s.width > 0 ? markers.stroke(self, i) : markers.fill(self, i);2773
2774onMouse("click", label, e => {2775if (cursor._lock)2776return;2777
2778let seriesIdx = series.indexOf(s);2779
2780if ((e.ctrlKey || e.metaKey) != legend.isolate) {2781// if any other series is shown, isolate this one. else show all2782let isolate = series.some((s, i) => i > 0 && i != seriesIdx && s.show);2783
2784series.forEach((s, i) => {2785i > 0 && setSeries(i, isolate ? (i == seriesIdx ? son : soff) : son, true, syncOpts.setSeries);2786});2787}2788else2789setSeries(seriesIdx, {show: !s.show}, true, syncOpts.setSeries);2790});2791
2792if (cursorFocus) {2793onMouse(mouseenter, label, e => {2794if (cursor._lock)2795return;2796
2797setSeries(series.indexOf(s), FOCUS_TRUE, true, syncOpts.setSeries);2798});2799}2800}2801
2802for (var key in legendCols) {2803let v = placeTag("td", LEGEND_VALUE, row);2804v.textContent = "--";2805cells.push(v);2806}2807
2808return [row, cells];2809}2810
2811const mouseListeners = new Map();2812
2813function onMouse(ev, targ, fn) {2814const targListeners = mouseListeners.get(targ) || {};2815const listener = cursor.bind[ev](self, targ, fn);2816
2817if (listener) {2818on(ev, targ, targListeners[ev] = listener);2819mouseListeners.set(targ, targListeners);2820}2821}2822
2823function offMouse(ev, targ, fn) {2824const targListeners = mouseListeners.get(targ) || {};2825
2826for (let k in targListeners) {2827if (ev == null || k == ev) {2828off(k, targ, targListeners[k]);2829delete targListeners[k];2830}2831}2832
2833if (ev == null)2834mouseListeners.delete(targ);2835}2836
2837let fullWidCss = 0;2838let fullHgtCss = 0;2839
2840let plotWidCss = 0;2841let plotHgtCss = 0;2842
2843// plot margins to account for axes2844let plotLftCss = 0;2845let plotTopCss = 0;2846
2847let plotLft = 0;2848let plotTop = 0;2849let plotWid = 0;2850let plotHgt = 0;2851
2852self.bbox = {};2853
2854let shouldSetScales = false;2855let shouldSetSize = false;2856let shouldConvergeSize = false;2857let shouldSetCursor = false;2858let shouldSetLegend = false;2859
2860function _setSize(width, height, force) {2861if (force || (width != self.width || height != self.height))2862calcSize(width, height);2863
2864resetYSeries(false);2865
2866shouldConvergeSize = true;2867shouldSetSize = true;2868shouldSetCursor = shouldSetLegend = cursor.left >= 0;2869commit();2870}2871
2872function calcSize(width, height) {2873// log("calcSize()", arguments);2874
2875self.width = fullWidCss = plotWidCss = width;2876self.height = fullHgtCss = plotHgtCss = height;2877plotLftCss = plotTopCss = 0;2878
2879calcPlotRect();2880calcAxesRects();2881
2882let bb = self.bbox;2883
2884plotLft = bb.left = incrRound(plotLftCss * pxRatio, 0.5);2885plotTop = bb.top = incrRound(plotTopCss * pxRatio, 0.5);2886plotWid = bb.width = incrRound(plotWidCss * pxRatio, 0.5);2887plotHgt = bb.height = incrRound(plotHgtCss * pxRatio, 0.5);2888
2889// updOriDims();2890}2891
2892// ensures size calc convergence2893const CYCLE_LIMIT = 3;2894
2895function convergeSize() {2896let converged = false;2897
2898let cycleNum = 0;2899
2900while (!converged) {2901cycleNum++;2902
2903let axesConverged = axesCalc(cycleNum);2904let paddingConverged = paddingCalc(cycleNum);2905
2906converged = cycleNum == CYCLE_LIMIT || (axesConverged && paddingConverged);2907
2908if (!converged) {2909calcSize(self.width, self.height);2910shouldSetSize = true;2911}2912}2913}2914
2915function setSize({width, height}) {2916_setSize(width, height);2917}2918
2919self.setSize = setSize;2920
2921// accumulate axis offsets, reduce canvas width2922function calcPlotRect() {2923// easements for edge labels2924let hasTopAxis = false;2925let hasBtmAxis = false;2926let hasRgtAxis = false;2927let hasLftAxis = false;2928
2929axes.forEach((axis, i) => {2930if (axis.show && axis._show) {2931let {side, _size} = axis;2932let isVt = side % 2;2933let labelSize = axis.label != null ? axis.labelSize : 0;2934
2935let fullSize = _size + labelSize;2936
2937if (fullSize > 0) {2938if (isVt) {2939plotWidCss -= fullSize;2940
2941if (side == 3) {2942plotLftCss += fullSize;2943hasLftAxis = true;2944}2945else2946hasRgtAxis = true;2947}2948else {2949plotHgtCss -= fullSize;2950
2951if (side == 0) {2952plotTopCss += fullSize;2953hasTopAxis = true;2954}2955else2956hasBtmAxis = true;2957}2958}2959}2960});2961
2962sidesWithAxes[0] = hasTopAxis;2963sidesWithAxes[1] = hasRgtAxis;2964sidesWithAxes[2] = hasBtmAxis;2965sidesWithAxes[3] = hasLftAxis;2966
2967// hz padding2968plotWidCss -= _padding[1] + _padding[3];2969plotLftCss += _padding[3];2970
2971// vt padding2972plotHgtCss -= _padding[2] + _padding[0];2973plotTopCss += _padding[0];2974}2975
2976function calcAxesRects() {2977// will accum +2978let off1 = plotLftCss + plotWidCss;2979let off2 = plotTopCss + plotHgtCss;2980// will accum -2981let off3 = plotLftCss;2982let off0 = plotTopCss;2983
2984function incrOffset(side, size) {2985switch (side) {2986case 1: off1 += size; return off1 - size;2987case 2: off2 += size; return off2 - size;2988case 3: off3 -= size; return off3 + size;2989case 0: off0 -= size; return off0 + size;2990}2991}2992
2993axes.forEach((axis, i) => {2994if (axis.show && axis._show) {2995let side = axis.side;2996
2997axis._pos = incrOffset(side, axis._size);2998
2999if (axis.label != null)3000axis._lpos = incrOffset(side, axis.labelSize);3001}3002});3003}3004
3005const cursor = (self.cursor = assign({}, cursorOpts, {drag: {y: mode == 2}}, opts.cursor));3006
3007{3008cursor.idxs = activeIdxs;3009
3010cursor._lock = false;3011
3012let points = cursor.points;3013
3014points.show = fnOrSelf(points.show);3015points.size = fnOrSelf(points.size);3016points.stroke = fnOrSelf(points.stroke);3017points.width = fnOrSelf(points.width);3018points.fill = fnOrSelf(points.fill);3019}3020
3021const focus = self.focus = assign({}, opts.focus || {alpha: 0.3}, cursor.focus);3022const cursorFocus = focus.prox >= 0;3023
3024// series-intersection markers3025let cursorPts = [null];3026
3027function initCursorPt(s, si) {3028if (si > 0) {3029let pt = cursor.points.show(self, si);3030
3031if (pt) {3032addClass(pt, CURSOR_PT);3033addClass(pt, s.class);3034elTrans(pt, -10, -10, plotWidCss, plotHgtCss);3035over.insertBefore(pt, cursorPts[si]);3036
3037return pt;3038}3039}3040}3041
3042function initSeries(s, i) {3043if (mode == 1 || i > 0) {3044let isTime = mode == 1 && scales[s.scale].time;3045
3046let sv = s.value;3047s.value = isTime ? (isStr(sv) ? timeSeriesVal(_tzDate, timeSeriesStamp(sv, _fmtDate)) : sv || _timeSeriesVal) : sv || numSeriesVal;3048s.label = s.label || (isTime ? timeSeriesLabel : numSeriesLabel);3049}3050
3051if (i > 0) {3052s.width = s.width == null ? 1 : s.width;3053s.paths = s.paths || linearPath || retNull;3054s.fillTo = fnOrSelf(s.fillTo || seriesFillTo);3055s.pxAlign = +ifNull(s.pxAlign, pxAlign);3056s.pxRound = pxRoundGen(s.pxAlign);3057
3058s.stroke = fnOrSelf(s.stroke || null);3059s.fill = fnOrSelf(s.fill || null);3060s._stroke = s._fill = s._paths = s._focus = null;3061
3062let _ptDia = ptDia(s.width, 1);3063let points = s.points = assign({}, {3064size: _ptDia,3065width: max(1, _ptDia * .2),3066stroke: s.stroke,3067space: _ptDia * 2,3068paths: pointsPath,3069_stroke: null,3070_fill: null,3071}, s.points);3072points.show = fnOrSelf(points.show);3073points.filter = fnOrSelf(points.filter);3074points.fill = fnOrSelf(points.fill);3075points.stroke = fnOrSelf(points.stroke);3076points.paths = fnOrSelf(points.paths);3077points.pxAlign = s.pxAlign;3078}3079
3080if (showLegend) {3081let rowCells = initLegendRow(s, i);3082legendRows.splice(i, 0, rowCells[0]);3083legendCells.splice(i, 0, rowCells[1]);3084legend.values.push(null); // NULL_LEGEND_VALS not yet avil here :(3085}3086
3087if (cursor.show) {3088activeIdxs.splice(i, 0, null);3089
3090let pt = initCursorPt(s, i);3091pt && cursorPts.splice(i, 0, pt);3092}3093}3094
3095function addSeries(opts, si) {3096si = si == null ? series.length : si;3097
3098opts = setDefault(opts, si, xSeriesOpts, ySeriesOpts);3099series.splice(si, 0, opts);3100initSeries(series[si], si);3101}3102
3103self.addSeries = addSeries;3104
3105function delSeries(i) {3106series.splice(i, 1);3107
3108if (showLegend) {3109legend.values.splice(i, 1);3110
3111legendCells.splice(i, 1);3112let tr = legendRows.splice(i, 1)[0];3113offMouse(null, tr.firstChild);3114tr.remove();3115}3116
3117if (cursor.show) {3118activeIdxs.splice(i, 1);3119
3120cursorPts.length > 1 && cursorPts.splice(i, 1)[0].remove();3121}3122
3123// TODO: de-init no-longer-needed scales?3124}3125
3126self.delSeries = delSeries;3127
3128const sidesWithAxes = [false, false, false, false];3129
3130function initAxis(axis, i) {3131axis._show = axis.show;3132
3133if (axis.show) {3134let isVt = axis.side % 2;3135
3136let sc = scales[axis.scale];3137
3138// this can occur if all series specify non-default scales3139if (sc == null) {3140axis.scale = isVt ? series[1].scale : xScaleKey;3141sc = scales[axis.scale];3142}3143
3144// also set defaults for incrs & values based on axis distr3145let isTime = sc.time;3146
3147axis.size = fnOrSelf(axis.size);3148axis.space = fnOrSelf(axis.space);3149axis.rotate = fnOrSelf(axis.rotate);3150axis.incrs = fnOrSelf(axis.incrs || ( sc.distr == 2 ? wholeIncrs : (isTime ? (ms == 1 ? timeIncrsMs : timeIncrsS) : numIncrs)));3151axis.splits = fnOrSelf(axis.splits || (isTime && sc.distr == 1 ? _timeAxisSplits : sc.distr == 3 ? logAxisSplits : sc.distr == 4 ? asinhAxisSplits : numAxisSplits));3152
3153axis.stroke = fnOrSelf(axis.stroke);3154axis.grid.stroke = fnOrSelf(axis.grid.stroke);3155axis.ticks.stroke = fnOrSelf(axis.ticks.stroke);3156
3157let av = axis.values;3158
3159axis.values = (3160// static array of tick values3161isArr(av) && !isArr(av[0]) ? fnOrSelf(av) :3162// temporal3163isTime ? (3164// config array of fmtDate string tpls3165isArr(av) ?3166timeAxisVals(_tzDate, timeAxisStamps(av, _fmtDate)) :3167// fmtDate string tpl3168isStr(av) ?3169timeAxisVal(_tzDate, av) :3170av || _timeAxisVals3171) : av || numAxisVals3172);3173
3174axis.filter = fnOrSelf(axis.filter || ( sc.distr >= 3 ? logAxisValsFilt : retArg1));3175
3176axis.font = pxRatioFont(axis.font);3177axis.labelFont = pxRatioFont(axis.labelFont);3178
3179axis._size = axis.size(self, null, i, 0);3180
3181axis._space =3182axis._rotate =3183axis._incrs =3184axis._found = // foundIncrSpace3185axis._splits =3186axis._values = null;3187
3188if (axis._size > 0)3189sidesWithAxes[i] = true;3190
3191axis._el = placeDiv(AXIS, wrap);3192
3193// debug3194// axis._el.style.background = "#" + Math.floor(Math.random()*16777215).toString(16) + '80';3195}3196}3197
3198function autoPadSide(self, side, sidesWithAxes, cycleNum) {3199let [hasTopAxis, hasRgtAxis, hasBtmAxis, hasLftAxis] = sidesWithAxes;3200
3201let ori = side % 2;3202let size = 0;3203
3204if (ori == 0 && (hasLftAxis || hasRgtAxis))3205size = (side == 0 && !hasTopAxis || side == 2 && !hasBtmAxis ? round(xAxisOpts.size / 3) : 0);3206if (ori == 1 && (hasTopAxis || hasBtmAxis))3207size = (side == 1 && !hasRgtAxis || side == 3 && !hasLftAxis ? round(yAxisOpts.size / 2) : 0);3208
3209return size;3210}3211
3212const padding = self.padding = (opts.padding || [autoPadSide,autoPadSide,autoPadSide,autoPadSide]).map(p => fnOrSelf(ifNull(p, autoPadSide)));3213const _padding = self._padding = padding.map((p, i) => p(self, i, sidesWithAxes, 0));3214
3215let dataLen;3216
3217// rendered data window3218let i0 = null;3219let i1 = null;3220const idxs = mode == 1 ? series[0].idxs : null;3221
3222let data0 = null;3223
3224let viaAutoScaleX = false;3225
3226function setData(_data, _resetScales) {3227if (mode == 2) {3228dataLen = 0;3229for (let i = 1; i < series.length; i++)3230dataLen += data[i][0].length;3231self.data = data = _data;3232}3233else {3234data = (_data || []).slice();3235data[0] = data[0] || [];3236
3237self.data = data.slice();3238data0 = data[0];3239dataLen = data0.length;3240
3241if (xScaleDistr == 2)3242data[0] = data0.map((v, i) => i);3243}3244
3245self._data = data;3246
3247resetYSeries(true);3248
3249fire("setData");3250
3251if (_resetScales !== false) {3252let xsc = scaleX;3253
3254if (xsc.auto(self, viaAutoScaleX))3255autoScaleX();3256else3257_setScale(xScaleKey, xsc.min, xsc.max);3258
3259shouldSetCursor = cursor.left >= 0;3260shouldSetLegend = true;3261commit();3262}3263}3264
3265self.setData = setData;3266
3267function autoScaleX() {3268viaAutoScaleX = true;3269
3270let _min, _max;3271
3272if (mode == 1) {3273if (dataLen > 0) {3274i0 = idxs[0] = 0;3275i1 = idxs[1] = dataLen - 1;3276
3277_min = data[0][i0];3278_max = data[0][i1];3279
3280if (xScaleDistr == 2) {3281_min = i0;3282_max = i1;3283}3284else if (dataLen == 1) {3285if (xScaleDistr == 3)3286[_min, _max] = rangeLog(_min, _min, scaleX.log, false);3287else if (xScaleDistr == 4)3288[_min, _max] = rangeAsinh(_min, _min, scaleX.log, false);3289else if (scaleX.time)3290_max = _min + round(86400 / ms);3291else3292[_min, _max] = rangeNum(_min, _max, rangePad, true);3293}3294}3295else {3296i0 = idxs[0] = _min = null;3297i1 = idxs[1] = _max = null;3298}3299}3300
3301_setScale(xScaleKey, _min, _max);3302}3303
3304let ctxStroke, ctxFill, ctxWidth, ctxDash, ctxJoin, ctxCap, ctxFont, ctxAlign, ctxBaseline;3305let ctxAlpha;3306
3307function setCtxStyle(stroke = transparent, width, dash = EMPTY_ARR, cap = "butt", fill = transparent, join = "round") {3308if (stroke != ctxStroke)3309ctx.strokeStyle = ctxStroke = stroke;3310if (fill != ctxFill)3311ctx.fillStyle = ctxFill = fill;3312if (width != ctxWidth)3313ctx.lineWidth = ctxWidth = width;3314if (join != ctxJoin)3315ctx.lineJoin = ctxJoin = join;3316if (cap != ctxCap)3317ctx.lineCap = ctxCap = cap; // (‿|‿)3318if (dash != ctxDash)3319ctx.setLineDash(ctxDash = dash);3320}3321
3322function setFontStyle(font, fill, align, baseline) {3323if (fill != ctxFill)3324ctx.fillStyle = ctxFill = fill;3325if (font != ctxFont)3326ctx.font = ctxFont = font;3327if (align != ctxAlign)3328ctx.textAlign = ctxAlign = align;3329if (baseline != ctxBaseline)3330ctx.textBaseline = ctxBaseline = baseline;3331}3332
3333function accScale(wsc, psc, facet, data) {3334if (wsc.auto(self, viaAutoScaleX) && (psc == null || psc.min == null)) {3335let _i0 = ifNull(i0, 0);3336let _i1 = ifNull(i1, data.length - 1);3337
3338// only run getMinMax() for invalidated series data, else reuse3339let minMax = facet.min == null ? (wsc.distr == 3 ? getMinMaxLog(data, _i0, _i1) : getMinMax(data, _i0, _i1)) : [facet.min, facet.max];3340
3341// initial min/max3342wsc.min = min(wsc.min, facet.min = minMax[0]);3343wsc.max = max(wsc.max, facet.max = minMax[1]);3344}3345}3346
3347function setScales() {3348// log("setScales()", arguments);3349
3350// wip scales3351let wipScales = copy(scales, fastIsObj);3352
3353for (let k in wipScales) {3354let wsc = wipScales[k];3355let psc = pendScales[k];3356
3357if (psc != null && psc.min != null) {3358assign(wsc, psc);3359
3360// explicitly setting the x-scale invalidates everything (acts as redraw)3361if (k == xScaleKey)3362resetYSeries(true);3363}3364else if (k != xScaleKey || mode == 2) {3365if (dataLen == 0 && wsc.from == null) {3366let minMax = wsc.range(self, null, null, k);3367wsc.min = minMax[0];3368wsc.max = minMax[1];3369}3370else {3371wsc.min = inf;3372wsc.max = -inf;3373}3374}3375}3376
3377if (dataLen > 0) {3378// pre-range y-scales from y series' data values3379series.forEach((s, i) => {3380if (mode == 1) {3381let k = s.scale;3382let wsc = wipScales[k];3383let psc = pendScales[k];3384
3385if (i == 0) {3386let minMax = wsc.range(self, wsc.min, wsc.max, k);3387
3388wsc.min = minMax[0];3389wsc.max = minMax[1];3390
3391i0 = closestIdx(wsc.min, data[0]);3392i1 = closestIdx(wsc.max, data[0]);3393
3394// closest indices can be outside of view3395if (data[0][i0] < wsc.min)3396i0++;3397if (data[0][i1] > wsc.max)3398i1--;3399
3400s.min = data0[i0];3401s.max = data0[i1];3402}3403else if (s.show && s.auto)3404accScale(wsc, psc, s, data[i]);3405
3406s.idxs[0] = i0;3407s.idxs[1] = i1;3408}3409else {3410if (i > 0) {3411if (s.show && s.auto) {3412// TODO: only handles, assumes and requires facets[0] / 'x' scale, and facets[1] / 'y' scale3413let [ xFacet, yFacet ] = s.facets;3414let xScaleKey = xFacet.scale;3415let yScaleKey = yFacet.scale;3416let [ xData, yData ] = data[i];3417
3418accScale(wipScales[xScaleKey], pendScales[xScaleKey], xFacet, xData);3419accScale(wipScales[yScaleKey], pendScales[yScaleKey], yFacet, yData);3420
3421// temp3422s.min = yFacet.min;3423s.max = yFacet.max;3424}3425}3426}3427});3428
3429// range independent scales3430for (let k in wipScales) {3431let wsc = wipScales[k];3432let psc = pendScales[k];3433
3434if (wsc.from == null && (psc == null || psc.min == null)) {3435let minMax = wsc.range(3436self,3437wsc.min == inf ? null : wsc.min,3438wsc.max == -inf ? null : wsc.max,3439k
3440);3441wsc.min = minMax[0];3442wsc.max = minMax[1];3443}3444}3445}3446
3447// range dependent scales3448for (let k in wipScales) {3449let wsc = wipScales[k];3450
3451if (wsc.from != null) {3452let base = wipScales[wsc.from];3453
3454if (base.min == null)3455wsc.min = wsc.max = null;3456else {3457let minMax = wsc.range(self, base.min, base.max, k);3458wsc.min = minMax[0];3459wsc.max = minMax[1];3460}3461}3462}3463
3464let changed = {};3465let anyChanged = false;3466
3467for (let k in wipScales) {3468let wsc = wipScales[k];3469let sc = scales[k];3470
3471if (sc.min != wsc.min || sc.max != wsc.max) {3472sc.min = wsc.min;3473sc.max = wsc.max;3474
3475let distr = sc.distr;3476
3477sc._min = distr == 3 ? log10(sc.min) : distr == 4 ? asinh(sc.min, sc.asinh) : sc.min;3478sc._max = distr == 3 ? log10(sc.max) : distr == 4 ? asinh(sc.max, sc.asinh) : sc.max;3479
3480changed[k] = anyChanged = true;3481}3482}3483
3484if (anyChanged) {3485// invalidate paths of all series on changed scales3486series.forEach((s, i) => {3487if (mode == 2) {3488if (i > 0 && changed.y)3489s._paths = null;3490}3491else {3492if (changed[s.scale])3493s._paths = null;3494}3495});3496
3497for (let k in changed) {3498shouldConvergeSize = true;3499fire("setScale", k);3500}3501
3502if (cursor.show)3503shouldSetCursor = shouldSetLegend = cursor.left >= 0;3504}3505
3506for (let k in pendScales)3507pendScales[k] = null;3508}3509
3510// grabs the nearest indices with y data outside of x-scale limits3511function getOuterIdxs(ydata) {3512let _i0 = clamp(i0 - 1, 0, dataLen - 1);3513let _i1 = clamp(i1 + 1, 0, dataLen - 1);3514
3515while (ydata[_i0] == null && _i0 > 0)3516_i0--;3517
3518while (ydata[_i1] == null && _i1 < dataLen - 1)3519_i1++;3520
3521return [_i0, _i1];3522}3523
3524function drawSeries() {3525if (dataLen > 0) {3526series.forEach((s, i) => {3527if (i > 0 && s.show && s._paths == null) {3528let _idxs = getOuterIdxs(data[i]);3529s._paths = s.paths(self, i, _idxs[0], _idxs[1]);3530}3531});3532
3533series.forEach((s, i) => {3534if (i > 0 && s.show) {3535if (ctxAlpha != s.alpha)3536ctx.globalAlpha = ctxAlpha = s.alpha;3537
3538{3539cacheStrokeFill(i, false);3540s._paths && drawPath(i, false);3541}3542
3543{3544cacheStrokeFill(i, true);3545
3546let show = s.points.show(self, i, i0, i1);3547let idxs = s.points.filter(self, i, show, s._paths ? s._paths.gaps : null);3548
3549if (show || idxs) {3550s.points._paths = s.points.paths(self, i, i0, i1, idxs);3551drawPath(i, true);3552}3553}3554
3555if (ctxAlpha != 1)3556ctx.globalAlpha = ctxAlpha = 1;3557
3558fire("drawSeries", i);3559}3560});3561}3562}3563
3564function cacheStrokeFill(si, _points) {3565let s = _points ? series[si].points : series[si];3566
3567s._stroke = s.stroke(self, si);3568s._fill = s.fill(self, si);3569}3570
3571function drawPath(si, _points) {3572let s = _points ? series[si].points : series[si];3573
3574let strokeStyle = s._stroke;3575let fillStyle = s._fill;3576
3577let { stroke, fill, clip: gapsClip, flags } = s._paths;3578let boundsClip = null;3579let width = roundDec(s.width * pxRatio, 3);3580let offset = (width % 2) / 2;3581
3582if (_points && fillStyle == null)3583fillStyle = width > 0 ? "#fff" : strokeStyle;3584
3585let _pxAlign = s.pxAlign == 1;3586
3587_pxAlign && ctx.translate(offset, offset);3588
3589if (!_points) {3590let lft = plotLft,3591top = plotTop,3592wid = plotWid,3593hgt = plotHgt;3594
3595let halfWid = width * pxRatio / 2;3596
3597if (s.min == 0)3598hgt += halfWid;3599
3600if (s.max == 0) {3601top -= halfWid;3602hgt += halfWid;3603}3604
3605boundsClip = new Path2D();3606boundsClip.rect(lft, top, wid, hgt);3607}3608
3609// the points pathbuilder's gapsClip is its boundsClip, since points dont need gaps clipping, and bounds depend on point size3610if (_points)3611strokeFill(strokeStyle, width, s.dash, s.cap, fillStyle, stroke, fill, flags, gapsClip);3612else3613fillStroke(si, strokeStyle, width, s.dash, s.cap, fillStyle, stroke, fill, flags, boundsClip, gapsClip);3614
3615_pxAlign && ctx.translate(-offset, -offset);3616}3617
3618function fillStroke(si, strokeStyle, lineWidth, lineDash, lineCap, fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip) {3619let didStrokeFill = false;3620
3621// for all bands where this series is the top edge, create upwards clips using the bottom edges3622// and apply clips + fill with band fill or dfltFill3623bands.forEach((b, bi) => {3624// isUpperEdge?3625if (b.series[0] == si) {3626let lowerEdge = series[b.series[1]];3627let lowerData = data[b.series[1]];3628
3629let bandClip = (lowerEdge._paths || EMPTY_OBJ).band;3630let gapsClip2;3631
3632let _fillStyle = null;3633
3634// hasLowerEdge?3635if (lowerEdge.show && bandClip && hasData(lowerData, i0, i1)) {3636_fillStyle = b.fill(self, bi) || fillStyle;3637gapsClip2 = lowerEdge._paths.clip;3638}3639else3640bandClip = null;3641
3642strokeFill(strokeStyle, lineWidth, lineDash, lineCap, _fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip, gapsClip2, bandClip);3643
3644didStrokeFill = true;3645}3646});3647
3648if (!didStrokeFill)3649strokeFill(strokeStyle, lineWidth, lineDash, lineCap, fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip);3650}3651
3652const CLIP_FILL_STROKE = BAND_CLIP_FILL | BAND_CLIP_STROKE;3653
3654function strokeFill(strokeStyle, lineWidth, lineDash, lineCap, fillStyle, strokePath, fillPath, flags, boundsClip, gapsClip, gapsClip2, bandClip) {3655setCtxStyle(strokeStyle, lineWidth, lineDash, lineCap, fillStyle);3656
3657if (boundsClip || gapsClip || bandClip) {3658ctx.save();3659boundsClip && ctx.clip(boundsClip);3660gapsClip && ctx.clip(gapsClip);3661}3662
3663if (bandClip) {3664if ((flags & CLIP_FILL_STROKE) == CLIP_FILL_STROKE) {3665ctx.clip(bandClip);3666gapsClip2 && ctx.clip(gapsClip2);3667doFill(fillStyle, fillPath);3668doStroke(strokeStyle, strokePath, lineWidth);3669}3670else if (flags & BAND_CLIP_STROKE) {3671doFill(fillStyle, fillPath);3672ctx.clip(bandClip);3673doStroke(strokeStyle, strokePath, lineWidth);3674}3675else if (flags & BAND_CLIP_FILL) {3676ctx.save();3677ctx.clip(bandClip);3678gapsClip2 && ctx.clip(gapsClip2);3679doFill(fillStyle, fillPath);3680ctx.restore();3681doStroke(strokeStyle, strokePath, lineWidth);3682}3683}3684else {3685doFill(fillStyle, fillPath);3686doStroke(strokeStyle, strokePath, lineWidth);3687}3688
3689if (boundsClip || gapsClip || bandClip)3690ctx.restore();3691}3692
3693function doStroke(strokeStyle, strokePath, lineWidth) {3694if (lineWidth > 0) {3695if (strokePath instanceof Map) {3696strokePath.forEach((strokePath, strokeStyle) => {3697ctx.strokeStyle = ctxStroke = strokeStyle;3698ctx.stroke(strokePath);3699});3700}3701else3702strokePath != null && strokeStyle && ctx.stroke(strokePath);3703}3704}3705
3706function doFill(fillStyle, fillPath) {3707if (fillPath instanceof Map) {3708fillPath.forEach((fillPath, fillStyle) => {3709ctx.fillStyle = ctxFill = fillStyle;3710ctx.fill(fillPath);3711});3712}3713else3714fillPath != null && fillStyle && ctx.fill(fillPath);3715}3716
3717function getIncrSpace(axisIdx, min, max, fullDim) {3718let axis = axes[axisIdx];3719
3720let incrSpace;3721
3722if (fullDim <= 0)3723incrSpace = [0, 0];3724else {3725let minSpace = axis._space = axis.space(self, axisIdx, min, max, fullDim);3726let incrs = axis._incrs = axis.incrs(self, axisIdx, min, max, fullDim, minSpace);3727incrSpace = findIncr(min, max, incrs, fullDim, minSpace);3728}3729
3730return (axis._found = incrSpace);3731}3732
3733function drawOrthoLines(offs, filts, ori, side, pos0, len, width, stroke, dash, cap) {3734let offset = (width % 2) / 2;3735
3736pxAlign == 1 && ctx.translate(offset, offset);3737
3738setCtxStyle(stroke, width, dash, cap, stroke);3739
3740ctx.beginPath();3741
3742let x0, y0, x1, y1, pos1 = pos0 + (side == 0 || side == 3 ? -len : len);3743
3744if (ori == 0) {3745y0 = pos0;3746y1 = pos1;3747}3748else {3749x0 = pos0;3750x1 = pos1;3751}3752
3753for (let i = 0; i < offs.length; i++) {3754if (filts[i] != null) {3755if (ori == 0)3756x0 = x1 = offs[i];3757else3758y0 = y1 = offs[i];3759
3760ctx.moveTo(x0, y0);3761ctx.lineTo(x1, y1);3762}3763}3764
3765ctx.stroke();3766
3767pxAlign == 1 && ctx.translate(-offset, -offset);3768}3769
3770function axesCalc(cycleNum) {3771// log("axesCalc()", arguments);3772
3773let converged = true;3774
3775axes.forEach((axis, i) => {3776if (!axis.show)3777return;3778
3779let scale = scales[axis.scale];3780
3781if (scale.min == null) {3782if (axis._show) {3783converged = false;3784axis._show = false;3785resetYSeries(false);3786}3787return;3788}3789else {3790if (!axis._show) {3791converged = false;3792axis._show = true;3793resetYSeries(false);3794}3795}3796
3797let side = axis.side;3798let ori = side % 2;3799
3800let {min, max} = scale; // // should this toggle them ._show = false3801
3802let [_incr, _space] = getIncrSpace(i, min, max, ori == 0 ? plotWidCss : plotHgtCss);3803
3804if (_space == 0)3805return;3806
3807// if we're using index positions, force first tick to match passed index3808let forceMin = scale.distr == 2;3809
3810let _splits = axis._splits = axis.splits(self, i, min, max, _incr, _space, forceMin);3811
3812// tick labels3813// BOO this assumes a specific data/series3814let splits = scale.distr == 2 ? _splits.map(i => data0[i]) : _splits;3815let incr = scale.distr == 2 ? data0[_splits[1]] - data0[_splits[0]] : _incr;3816
3817let values = axis._values = axis.values(self, axis.filter(self, splits, i, _space, incr), i, _space, incr);3818
3819// rotating of labels only supported on bottom x axis3820axis._rotate = side == 2 ? axis.rotate(self, values, i, _space) : 0;3821
3822let oldSize = axis._size;3823
3824axis._size = ceil(axis.size(self, values, i, cycleNum));3825
3826if (oldSize != null && axis._size != oldSize) // ready && ?3827converged = false;3828});3829
3830return converged;3831}3832
3833function paddingCalc(cycleNum) {3834let converged = true;3835
3836padding.forEach((p, i) => {3837let _p = p(self, i, sidesWithAxes, cycleNum);3838
3839if (_p != _padding[i])3840converged = false;3841
3842_padding[i] = _p;3843});3844
3845return converged;3846}3847
3848function drawAxesGrid() {3849for (let i = 0; i < axes.length; i++) {3850let axis = axes[i];3851
3852if (!axis.show || !axis._show)3853continue;3854
3855let side = axis.side;3856let ori = side % 2;3857
3858let x, y;3859
3860let fillStyle = axis.stroke(self, i);3861
3862let shiftDir = side == 0 || side == 3 ? -1 : 1;3863
3864// axis label3865if (axis.label) {3866let shiftAmt = axis.labelGap * shiftDir;3867let baseLpos = round((axis._lpos + shiftAmt) * pxRatio);3868
3869setFontStyle(axis.labelFont[0], fillStyle, "center", side == 2 ? TOP : BOTTOM);3870
3871ctx.save();3872
3873if (ori == 1) {3874x = y = 0;3875
3876ctx.translate(3877baseLpos,3878round(plotTop + plotHgt / 2),3879);3880ctx.rotate((side == 3 ? -PI : PI) / 2);3881
3882}3883else {3884x = round(plotLft + plotWid / 2);3885y = baseLpos;3886}3887
3888ctx.fillText(axis.label, x, y);3889
3890ctx.restore();3891}3892
3893let [_incr, _space] = axis._found;3894
3895if (_space == 0)3896continue;3897
3898let scale = scales[axis.scale];3899
3900let plotDim = ori == 0 ? plotWid : plotHgt;3901let plotOff = ori == 0 ? plotLft : plotTop;3902
3903let axisGap = round(axis.gap * pxRatio);3904
3905let _splits = axis._splits;3906
3907// tick labels3908// BOO this assumes a specific data/series3909let splits = scale.distr == 2 ? _splits.map(i => data0[i]) : _splits;3910let incr = scale.distr == 2 ? data0[_splits[1]] - data0[_splits[0]] : _incr;3911
3912let ticks = axis.ticks;3913let tickSize = ticks.show ? round(ticks.size * pxRatio) : 0;3914
3915// rotating of labels only supported on bottom x axis3916let angle = axis._rotate * -PI/180;3917
3918let basePos = pxRound(axis._pos * pxRatio);3919let shiftAmt = (tickSize + axisGap) * shiftDir;3920let finalPos = basePos + shiftAmt;3921y = ori == 0 ? finalPos : 0;3922x = ori == 1 ? finalPos : 0;3923
3924let font = axis.font[0];3925let textAlign = axis.align == 1 ? LEFT :3926axis.align == 2 ? RIGHT :3927angle > 0 ? LEFT :3928angle < 0 ? RIGHT :3929ori == 0 ? "center" : side == 3 ? RIGHT : LEFT;3930let textBaseline = angle ||3931ori == 1 ? "middle" : side == 2 ? TOP : BOTTOM;3932
3933setFontStyle(font, fillStyle, textAlign, textBaseline);3934
3935let lineHeight = axis.font[1] * lineMult;3936
3937let canOffs = _splits.map(val => pxRound(getPos(val, scale, plotDim, plotOff)));3938
3939let _values = axis._values;3940
3941for (let i = 0; i < _values.length; i++) {3942let val = _values[i];3943
3944if (val != null) {3945if (ori == 0)3946x = canOffs[i];3947else3948y = canOffs[i];3949
3950val = "" + val;3951
3952let _parts = val.indexOf("\n") == -1 ? [val] : val.split(/\n/gm);3953
3954for (let j = 0; j < _parts.length; j++) {3955let text = _parts[j];3956
3957if (angle) {3958ctx.save();3959ctx.translate(x, y + j * lineHeight); // can this be replaced with position math?3960ctx.rotate(angle); // can this be done once?3961ctx.fillText(text, 0, 0);3962ctx.restore();3963}3964else3965ctx.fillText(text, x, y + j * lineHeight);3966}3967}3968}3969
3970// ticks3971if (ticks.show) {3972drawOrthoLines(3973canOffs,3974ticks.filter(self, splits, i, _space, incr),3975ori,3976side,3977basePos,3978tickSize,3979roundDec(ticks.width * pxRatio, 3),3980ticks.stroke(self, i),3981ticks.dash,3982ticks.cap,3983);3984}3985
3986// grid3987let grid = axis.grid;3988
3989if (grid.show) {3990drawOrthoLines(3991canOffs,3992grid.filter(self, splits, i, _space, incr),3993ori,3994ori == 0 ? 2 : 1,3995ori == 0 ? plotTop : plotLft,3996ori == 0 ? plotHgt : plotWid,3997roundDec(grid.width * pxRatio, 3),3998grid.stroke(self, i),3999grid.dash,4000grid.cap,4001);4002}4003}4004
4005fire("drawAxes");4006}4007
4008function resetYSeries(minMax) {4009// log("resetYSeries()", arguments);4010
4011series.forEach((s, i) => {4012if (i > 0) {4013s._paths = null;4014
4015if (minMax) {4016if (mode == 1) {4017s.min = null;4018s.max = null;4019}4020else {4021s.facets.forEach(f => {4022f.min = null;4023f.max = null;4024});4025}4026}4027}4028});4029}4030
4031let queuedCommit = false;4032
4033function commit() {4034if (!queuedCommit) {4035microTask(_commit);4036queuedCommit = true;4037}4038}4039
4040function _commit() {4041// log("_commit()", arguments);4042
4043if (shouldSetScales) {4044setScales();4045shouldSetScales = false;4046}4047
4048if (shouldConvergeSize) {4049convergeSize();4050shouldConvergeSize = false;4051}4052
4053if (shouldSetSize) {4054setStylePx(under, LEFT, plotLftCss);4055setStylePx(under, TOP, plotTopCss);4056setStylePx(under, WIDTH, plotWidCss);4057setStylePx(under, HEIGHT, plotHgtCss);4058
4059setStylePx(over, LEFT, plotLftCss);4060setStylePx(over, TOP, plotTopCss);4061setStylePx(over, WIDTH, plotWidCss);4062setStylePx(over, HEIGHT, plotHgtCss);4063
4064setStylePx(wrap, WIDTH, fullWidCss);4065setStylePx(wrap, HEIGHT, fullHgtCss);4066
4067// NOTE: mutating this during print preview in Chrome forces transparent4068// canvas pixels to white, even when followed up with clearRect() below4069can.width = round(fullWidCss * pxRatio);4070can.height = round(fullHgtCss * pxRatio);4071
4072
4073axes.forEach(a => {4074let { _show, _el, _size, _pos, side } = a;4075
4076if (_show) {4077let posOffset = (side === 3 || side === 0 ? _size : 0);4078let isVt = side % 2 == 1;4079
4080setStylePx(_el, isVt ? "left" : "top", _pos - posOffset);4081setStylePx(_el, isVt ? "width" : "height", _size);4082setStylePx(_el, isVt ? "top" : "left", isVt ? plotTopCss : plotLftCss);4083setStylePx(_el, isVt ? "height" : "width", isVt ? plotHgtCss : plotWidCss);4084
4085_el && remClass(_el, OFF);4086}4087else4088_el && addClass(_el, OFF);4089});4090
4091// invalidate ctx style cache4092ctxStroke = ctxFill = ctxWidth = ctxJoin = ctxCap = ctxFont = ctxAlign = ctxBaseline = ctxDash = null;4093ctxAlpha = 1;4094
4095syncRect(false);4096
4097fire("setSize");4098
4099shouldSetSize = false;4100}4101
4102if (fullWidCss > 0 && fullHgtCss > 0) {4103ctx.clearRect(0, 0, can.width, can.height);4104fire("drawClear");4105drawOrder.forEach(fn => fn());4106fire("draw");4107}4108
4109// if (shouldSetSelect) {4110// TODO: update .u-select metrics (if visible)4111// setStylePx(selectDiv, TOP, select.top = 0);4112// setStylePx(selectDiv, LEFT, select.left = 0);4113// setStylePx(selectDiv, WIDTH, select.width = 0);4114// setStylePx(selectDiv, HEIGHT, select.height = 0);4115// shouldSetSelect = false;4116// }4117
4118if (cursor.show && shouldSetCursor) {4119updateCursor(null, true, false);4120shouldSetCursor = false;4121}4122
4123// if (FEAT_LEGEND && legend.show && legend.live && shouldSetLegend) {}4124
4125if (!ready) {4126ready = true;4127self.status = 1;4128
4129fire("ready");4130}4131
4132viaAutoScaleX = false;4133
4134queuedCommit = false;4135}4136
4137self.redraw = (rebuildPaths, recalcAxes) => {4138shouldConvergeSize = recalcAxes || false;4139
4140if (rebuildPaths !== false)4141_setScale(xScaleKey, scaleX.min, scaleX.max);4142else4143commit();4144};4145
4146// redraw() => setScale('x', scales.x.min, scales.x.max);4147
4148// explicit, never re-ranged (is this actually true? for x and y)4149function setScale(key, opts) {4150let sc = scales[key];4151
4152if (sc.from == null) {4153if (dataLen == 0) {4154let minMax = sc.range(self, opts.min, opts.max, key);4155opts.min = minMax[0];4156opts.max = minMax[1];4157}4158
4159if (opts.min > opts.max) {4160let _min = opts.min;4161opts.min = opts.max;4162opts.max = _min;4163}4164
4165if (dataLen > 1 && opts.min != null && opts.max != null && opts.max - opts.min < 1e-16)4166return;4167
4168if (key == xScaleKey) {4169if (sc.distr == 2 && dataLen > 0) {4170opts.min = closestIdx(opts.min, data[0]);4171opts.max = closestIdx(opts.max, data[0]);4172
4173if (opts.min == opts.max)4174opts.max++;4175}4176}4177
4178// log("setScale()", arguments);4179
4180pendScales[key] = opts;4181
4182shouldSetScales = true;4183commit();4184}4185}4186
4187self.setScale = setScale;4188
4189// INTERACTION
4190
4191let xCursor;4192let yCursor;4193let vCursor;4194let hCursor;4195
4196// starting position before cursor.move4197let rawMouseLeft0;4198let rawMouseTop0;4199
4200// starting position4201let mouseLeft0;4202let mouseTop0;4203
4204// current position before cursor.move4205let rawMouseLeft1;4206let rawMouseTop1;4207
4208// current position4209let mouseLeft1;4210let mouseTop1;4211
4212let dragging = false;4213
4214const drag = cursor.drag;4215
4216let dragX = drag.x;4217let dragY = drag.y;4218
4219if (cursor.show) {4220if (cursor.x)4221xCursor = placeDiv(CURSOR_X, over);4222if (cursor.y)4223yCursor = placeDiv(CURSOR_Y, over);4224
4225if (scaleX.ori == 0) {4226vCursor = xCursor;4227hCursor = yCursor;4228}4229else {4230vCursor = yCursor;4231hCursor = xCursor;4232}4233
4234mouseLeft1 = cursor.left;4235mouseTop1 = cursor.top;4236}4237
4238const select = self.select = assign({4239show: true,4240over: true,4241left: 0,4242width: 0,4243top: 0,4244height: 0,4245}, opts.select);4246
4247const selectDiv = select.show ? placeDiv(SELECT, select.over ? over : under) : null;4248
4249function setSelect(opts, _fire) {4250if (select.show) {4251for (let prop in opts)4252setStylePx(selectDiv, prop, select[prop] = opts[prop]);4253
4254_fire !== false && fire("setSelect");4255}4256}4257
4258self.setSelect = setSelect;4259
4260function toggleDOM(i, onOff) {4261let s = series[i];4262let label = showLegend ? legendRows[i] : null;4263
4264if (s.show)4265label && remClass(label, OFF);4266else {4267label && addClass(label, OFF);4268cursorPts.length > 1 && elTrans(cursorPts[i], -10, -10, plotWidCss, plotHgtCss);4269}4270}4271
4272function _setScale(key, min, max) {4273setScale(key, {min, max});4274}4275
4276function setSeries(i, opts, _fire, _pub) {4277// log("setSeries()", arguments);4278
4279let s = series[i];4280
4281if (opts.focus != null)4282setFocus(i);4283
4284if (opts.show != null) {4285s.show = opts.show;4286toggleDOM(i, opts.show);4287
4288_setScale(mode == 2 ? s.facets[1].scale : s.scale, null, null);4289commit();4290}4291
4292_fire !== false && fire("setSeries", i, opts);4293
4294_pub && pubSync("setSeries", self, i, opts);4295}4296
4297self.setSeries = setSeries;4298
4299function setBand(bi, opts) {4300assign(bands[bi], opts);4301}4302
4303function addBand(opts, bi) {4304opts.fill = fnOrSelf(opts.fill || null);4305bi = bi == null ? bands.length : bi;4306bands.splice(bi, 0, opts);4307}4308
4309function delBand(bi) {4310if (bi == null)4311bands.length = 0;4312else4313bands.splice(bi, 1);4314}4315
4316self.addBand = addBand;4317self.setBand = setBand;4318self.delBand = delBand;4319
4320function setAlpha(i, value) {4321series[i].alpha = value;4322
4323if (cursor.show && cursorPts[i])4324cursorPts[i].style.opacity = value;4325
4326if (showLegend && legendRows[i])4327legendRows[i].style.opacity = value;4328}4329
4330// y-distance4331let closestDist;4332let closestSeries;4333let focusedSeries;4334const FOCUS_TRUE = {focus: true};4335const FOCUS_FALSE = {focus: false};4336
4337function setFocus(i) {4338if (i != focusedSeries) {4339// log("setFocus()", arguments);4340
4341let allFocused = i == null;4342
4343let _setAlpha = focus.alpha != 1;4344
4345series.forEach((s, i2) => {4346let isFocused = allFocused || i2 == 0 || i2 == i;4347s._focus = allFocused ? null : isFocused;4348_setAlpha && setAlpha(i2, isFocused ? 1 : focus.alpha);4349});4350
4351focusedSeries = i;4352_setAlpha && commit();4353}4354}4355
4356if (showLegend && cursorFocus) {4357on(mouseleave, legendEl, e => {4358if (cursor._lock)4359return;4360setSeries(null, FOCUS_FALSE, true, syncOpts.setSeries);4361updateCursor(null, true, false);4362});4363}4364
4365function posToVal(pos, scale, can) {4366let sc = scales[scale];4367
4368if (can)4369pos = pos / pxRatio - (sc.ori == 1 ? plotTopCss : plotLftCss);4370
4371let dim = plotWidCss;4372
4373if (sc.ori == 1) {4374dim = plotHgtCss;4375pos = dim - pos;4376}4377
4378if (sc.dir == -1)4379pos = dim - pos;4380
4381let _min = sc._min,4382_max = sc._max,4383pct = pos / dim;4384
4385let sv = _min + (_max - _min) * pct;4386
4387let distr = sc.distr;4388
4389return (4390distr == 3 ? pow(10, sv) :4391distr == 4 ? sinh(sv, sc.asinh) :4392sv
4393);4394}4395
4396function closestIdxFromXpos(pos, can) {4397let v = posToVal(pos, xScaleKey, can);4398return closestIdx(v, data[0], i0, i1);4399}4400
4401self.valToIdx = val => closestIdx(val, data[0]);4402self.posToIdx = closestIdxFromXpos;4403self.posToVal = posToVal;4404self.valToPos = (val, scale, can) => (4405scales[scale].ori == 0 ?4406getHPos(val, scales[scale],4407can ? plotWid : plotWidCss,4408can ? plotLft : 0,4409) :4410getVPos(val, scales[scale],4411can ? plotHgt : plotHgtCss,4412can ? plotTop : 0,4413)4414);4415
4416// defers calling expensive functions4417function batch(fn) {4418fn(self);4419commit();4420}4421
4422self.batch = batch;4423
4424(self.setCursor = (opts, _fire, _pub) => {4425mouseLeft1 = opts.left;4426mouseTop1 = opts.top;4427// assign(cursor, opts);4428updateCursor(null, _fire, _pub);4429});4430
4431function setSelH(off, dim) {4432setStylePx(selectDiv, LEFT, select.left = off);4433setStylePx(selectDiv, WIDTH, select.width = dim);4434}4435
4436function setSelV(off, dim) {4437setStylePx(selectDiv, TOP, select.top = off);4438setStylePx(selectDiv, HEIGHT, select.height = dim);4439}4440
4441let setSelX = scaleX.ori == 0 ? setSelH : setSelV;4442let setSelY = scaleX.ori == 1 ? setSelH : setSelV;4443
4444function syncLegend() {4445if (showLegend && legend.live) {4446for (let i = mode == 2 ? 1 : 0; i < series.length; i++) {4447if (i == 0 && multiValLegend)4448continue;4449
4450let vals = legend.values[i];4451
4452let j = 0;4453
4454for (let k in vals)4455legendCells[i][j++].firstChild.nodeValue = vals[k];4456}4457}4458}4459
4460function setLegend(opts, _fire) {4461if (opts != null) {4462let idx = opts.idx;4463
4464legend.idx = idx;4465series.forEach((s, sidx) => {4466(sidx > 0 || !multiValLegend) && setLegendValues(sidx, idx);4467});4468}4469
4470if (showLegend && legend.live)4471syncLegend();4472
4473shouldSetLegend = false;4474
4475_fire !== false && fire("setLegend");4476}4477
4478self.setLegend = setLegend;4479
4480function setLegendValues(sidx, idx) {4481let val;4482
4483if (idx == null)4484val = NULL_LEGEND_VALUES;4485else {4486let s = series[sidx];4487let src = sidx == 0 && xScaleDistr == 2 ? data0 : data[sidx];4488val = multiValLegend ? s.values(self, sidx, idx) : {_: s.value(self, src[idx], sidx, idx)};4489}4490
4491legend.values[sidx] = val;4492}4493
4494function updateCursor(src, _fire, _pub) {4495// ts == null && log("updateCursor()", arguments);4496
4497rawMouseLeft1 = mouseLeft1;4498rawMouseTop1 = mouseTop1;4499
4500[mouseLeft1, mouseTop1] = cursor.move(self, mouseLeft1, mouseTop1);4501
4502if (cursor.show) {4503vCursor && elTrans(vCursor, round(mouseLeft1), 0, plotWidCss, plotHgtCss);4504hCursor && elTrans(hCursor, 0, round(mouseTop1), plotWidCss, plotHgtCss);4505}4506
4507let idx;4508
4509// when zooming to an x scale range between datapoints the binary search4510// for nearest min/max indices results in this condition. cheap hack :D4511let noDataInRange = i0 > i1; // works for mode 1 only4512
4513closestDist = inf;4514
4515// TODO: extract4516let xDim = scaleX.ori == 0 ? plotWidCss : plotHgtCss;4517let yDim = scaleX.ori == 1 ? plotWidCss : plotHgtCss;4518
4519// if cursor hidden, hide points & clear legend vals4520if (mouseLeft1 < 0 || dataLen == 0 || noDataInRange) {4521idx = null;4522
4523for (let i = 0; i < series.length; i++) {4524if (i > 0) {4525cursorPts.length > 1 && elTrans(cursorPts[i], -10, -10, plotWidCss, plotHgtCss);4526}4527}4528
4529if (cursorFocus)4530setSeries(null, FOCUS_TRUE, true, src == null && syncOpts.setSeries);4531
4532if (legend.live) {4533activeIdxs.fill(null);4534shouldSetLegend = true;4535
4536for (let i = 0; i < series.length; i++)4537legend.values[i] = NULL_LEGEND_VALUES;4538}4539}4540else {4541// let pctY = 1 - (y / rect.height);4542
4543let mouseXPos, valAtPosX, xPos;4544
4545if (mode == 1) {4546mouseXPos = scaleX.ori == 0 ? mouseLeft1 : mouseTop1;4547valAtPosX = posToVal(mouseXPos, xScaleKey);4548idx = closestIdx(valAtPosX, data[0], i0, i1);4549xPos = incrRoundUp(valToPosX(data[0][idx], scaleX, xDim, 0), 0.5);4550}4551
4552for (let i = mode == 2 ? 1 : 0; i < series.length; i++) {4553let s = series[i];4554
4555let idx1 = activeIdxs[i];4556let yVal1 = mode == 1 ? data[i][idx1] : data[i][1][idx1];4557
4558let idx2 = cursor.dataIdx(self, i, idx, valAtPosX);4559let yVal2 = mode == 1 ? data[i][idx2] : data[i][1][idx2];4560
4561shouldSetLegend = shouldSetLegend || yVal2 != yVal1 || idx2 != idx1;4562
4563activeIdxs[i] = idx2;4564
4565let xPos2 = idx2 == idx ? xPos : incrRoundUp(valToPosX(mode == 1 ? data[0][idx2] : data[i][0][idx2], scaleX, xDim, 0), 0.5);4566
4567if (i > 0 && s.show) {4568let yPos = yVal2 == null ? -10 : incrRoundUp(valToPosY(yVal2, mode == 1 ? scales[s.scale] : scales[s.facets[1].scale], yDim, 0), 0.5);4569
4570if (yPos > 0 && mode == 1) {4571let dist = abs(yPos - mouseTop1);4572
4573if (dist <= closestDist) {4574closestDist = dist;4575closestSeries = i;4576}4577}4578
4579let hPos, vPos;4580
4581if (scaleX.ori == 0) {4582hPos = xPos2;4583vPos = yPos;4584}4585else {4586hPos = yPos;4587vPos = xPos2;4588}4589
4590if (shouldSetLegend && cursorPts.length > 1) {4591elColor(cursorPts[i], cursor.points.fill(self, i), cursor.points.stroke(self, i));4592
4593let ptWid, ptHgt, ptLft, ptTop,4594centered = true,4595getBBox = cursor.points.bbox;4596
4597if (getBBox != null) {4598centered = false;4599
4600let bbox = getBBox(self, i);4601
4602ptLft = bbox.left;4603ptTop = bbox.top;4604ptWid = bbox.width;4605ptHgt = bbox.height;4606}4607else {4608ptLft = hPos;4609ptTop = vPos;4610ptWid = ptHgt = cursor.points.size(self, i);4611}4612
4613elSize(cursorPts[i], ptWid, ptHgt, centered);4614elTrans(cursorPts[i], ptLft, ptTop, plotWidCss, plotHgtCss);4615}4616}4617
4618if (legend.live) {4619if (!shouldSetLegend || i == 0 && multiValLegend)4620continue;4621
4622setLegendValues(i, idx2);4623}4624}4625}4626
4627cursor.idx = idx;4628cursor.left = mouseLeft1;4629cursor.top = mouseTop1;4630
4631if (shouldSetLegend) {4632legend.idx = idx;4633setLegend();4634}4635
4636// nit: cursor.drag.setSelect is assumed always true4637if (select.show && dragging) {4638if (src != null) {4639let [xKey, yKey] = syncOpts.scales;4640let [matchXKeys, matchYKeys] = syncOpts.match;4641let [xKeySrc, yKeySrc] = src.cursor.sync.scales;4642
4643// match the dragX/dragY implicitness/explicitness of src4644let sdrag = src.cursor.drag;4645dragX = sdrag._x;4646dragY = sdrag._y;4647
4648let { left, top, width, height } = src.select;4649
4650let sori = src.scales[xKey].ori;4651let sPosToVal = src.posToVal;4652
4653let sOff, sDim, sc, a, b;4654
4655let matchingX = xKey != null && matchXKeys(xKey, xKeySrc);4656let matchingY = yKey != null && matchYKeys(yKey, yKeySrc);4657
4658if (matchingX) {4659if (sori == 0) {4660sOff = left;4661sDim = width;4662}4663else {4664sOff = top;4665sDim = height;4666}4667
4668if (dragX) {4669sc = scales[xKey];4670
4671a = valToPosX(sPosToVal(sOff, xKeySrc), sc, xDim, 0);4672b = valToPosX(sPosToVal(sOff + sDim, xKeySrc), sc, xDim, 0);4673
4674setSelX(min(a,b), abs(b-a));4675}4676else4677setSelX(0, xDim);4678
4679if (!matchingY)4680setSelY(0, yDim);4681}4682
4683if (matchingY) {4684if (sori == 1) {4685sOff = left;4686sDim = width;4687}4688else {4689sOff = top;4690sDim = height;4691}4692
4693if (dragY) {4694sc = scales[yKey];4695
4696a = valToPosY(sPosToVal(sOff, yKeySrc), sc, yDim, 0);4697b = valToPosY(sPosToVal(sOff + sDim, yKeySrc), sc, yDim, 0);4698
4699setSelY(min(a,b), abs(b-a));4700}4701else4702setSelY(0, yDim);4703
4704if (!matchingX)4705setSelX(0, xDim);4706}4707}4708else {4709let rawDX = abs(rawMouseLeft1 - rawMouseLeft0);4710let rawDY = abs(rawMouseTop1 - rawMouseTop0);4711
4712if (scaleX.ori == 1) {4713let _rawDX = rawDX;4714rawDX = rawDY;4715rawDY = _rawDX;4716}4717
4718dragX = drag.x && rawDX >= drag.dist;4719dragY = drag.y && rawDY >= drag.dist;4720
4721let uni = drag.uni;4722
4723if (uni != null) {4724// only calc drag status if they pass the dist thresh4725if (dragX && dragY) {4726dragX = rawDX >= uni;4727dragY = rawDY >= uni;4728
4729// force unidirectionality when both are under uni limit4730if (!dragX && !dragY) {4731if (rawDY > rawDX)4732dragY = true;4733else4734dragX = true;4735}4736}4737}4738else if (drag.x && drag.y && (dragX || dragY))4739// if omni with no uni then both dragX / dragY should be true if either is true4740dragX = dragY = true;4741
4742let p0, p1;4743
4744if (dragX) {4745if (scaleX.ori == 0) {4746p0 = mouseLeft0;4747p1 = mouseLeft1;4748}4749else {4750p0 = mouseTop0;4751p1 = mouseTop1;4752}4753
4754setSelX(min(p0, p1), abs(p1 - p0));4755
4756if (!dragY)4757setSelY(0, yDim);4758}4759
4760if (dragY) {4761if (scaleX.ori == 1) {4762p0 = mouseLeft0;4763p1 = mouseLeft1;4764}4765else {4766p0 = mouseTop0;4767p1 = mouseTop1;4768}4769
4770setSelY(min(p0, p1), abs(p1 - p0));4771
4772if (!dragX)4773setSelX(0, xDim);4774}4775
4776// the drag didn't pass the dist requirement4777if (!dragX && !dragY) {4778setSelX(0, 0);4779setSelY(0, 0);4780}4781}4782}4783
4784drag._x = dragX;4785drag._y = dragY;4786
4787if (src == null) {4788if (_pub) {4789if (syncKey != null) {4790let [xSyncKey, ySyncKey] = syncOpts.scales;4791
4792syncOpts.values[0] = xSyncKey != null ? posToVal(scaleX.ori == 0 ? mouseLeft1 : mouseTop1, xSyncKey) : null;4793syncOpts.values[1] = ySyncKey != null ? posToVal(scaleX.ori == 1 ? mouseLeft1 : mouseTop1, ySyncKey) : null;4794}4795
4796pubSync(mousemove, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, idx);4797}4798
4799if (cursorFocus) {4800let shouldPub = _pub && syncOpts.setSeries;4801let p = focus.prox;4802
4803if (focusedSeries == null) {4804if (closestDist <= p)4805setSeries(closestSeries, FOCUS_TRUE, true, shouldPub);4806}4807else {4808if (closestDist > p)4809setSeries(null, FOCUS_TRUE, true, shouldPub);4810else if (closestSeries != focusedSeries)4811setSeries(closestSeries, FOCUS_TRUE, true, shouldPub);4812}4813}4814}4815
4816ready && _fire !== false && fire("setCursor");4817}4818
4819let rect = null;4820
4821function syncRect(defer) {4822if (defer === true)4823rect = null;4824else {4825rect = over.getBoundingClientRect();4826fire("syncRect", rect);4827}4828}4829
4830function mouseMove(e, src, _l, _t, _w, _h, _i) {4831if (cursor._lock)4832return;4833
4834cacheMouse(e, src, _l, _t, _w, _h, _i, false, e != null);4835
4836if (e != null)4837updateCursor(null, true, true);4838else4839updateCursor(src, true, false);4840}4841
4842function cacheMouse(e, src, _l, _t, _w, _h, _i, initial, snap) {4843if (rect == null)4844syncRect(false);4845
4846if (e != null) {4847_l = e.clientX - rect.left;4848_t = e.clientY - rect.top;4849}4850else {4851if (_l < 0 || _t < 0) {4852mouseLeft1 = -10;4853mouseTop1 = -10;4854return;4855}4856
4857let [xKey, yKey] = syncOpts.scales;4858
4859let syncOptsSrc = src.cursor.sync;4860let [xValSrc, yValSrc] = syncOptsSrc.values;4861let [xKeySrc, yKeySrc] = syncOptsSrc.scales;4862let [matchXKeys, matchYKeys] = syncOpts.match;4863
4864let rotSrc = src.scales[xKeySrc].ori == 1;4865
4866let xDim = scaleX.ori == 0 ? plotWidCss : plotHgtCss,4867yDim = scaleX.ori == 1 ? plotWidCss : plotHgtCss,4868_xDim = rotSrc ? _h : _w,4869_yDim = rotSrc ? _w : _h,4870_xPos = rotSrc ? _t : _l,4871_yPos = rotSrc ? _l : _t;4872
4873if (xKeySrc != null)4874_l = matchXKeys(xKey, xKeySrc) ? getPos(xValSrc, scales[xKey], xDim, 0) : -10;4875else4876_l = xDim * (_xPos/_xDim);4877
4878if (yKeySrc != null)4879_t = matchYKeys(yKey, yKeySrc) ? getPos(yValSrc, scales[yKey], yDim, 0) : -10;4880else4881_t = yDim * (_yPos/_yDim);4882
4883if (scaleX.ori == 1) {4884let __l = _l;4885_l = _t;4886_t = __l;4887}4888}4889
4890if (snap) {4891if (_l <= 1 || _l >= plotWidCss - 1)4892_l = incrRound(_l, plotWidCss);4893
4894if (_t <= 1 || _t >= plotHgtCss - 1)4895_t = incrRound(_t, plotHgtCss);4896}4897
4898if (initial) {4899rawMouseLeft0 = _l;4900rawMouseTop0 = _t;4901
4902[mouseLeft0, mouseTop0] = cursor.move(self, _l, _t);4903}4904else {4905mouseLeft1 = _l;4906mouseTop1 = _t;4907}4908}4909
4910function hideSelect() {4911setSelect({4912width: 0,4913height: 0,4914}, false);4915}4916
4917function mouseDown(e, src, _l, _t, _w, _h, _i) {4918dragging = true;4919dragX = dragY = drag._x = drag._y = false;4920
4921cacheMouse(e, src, _l, _t, _w, _h, _i, true, false);4922
4923if (e != null) {4924onMouse(mouseup, doc, mouseUp);4925pubSync(mousedown, self, mouseLeft0, mouseTop0, plotWidCss, plotHgtCss, null);4926}4927}4928
4929function mouseUp(e, src, _l, _t, _w, _h, _i) {4930dragging = drag._x = drag._y = false;4931
4932cacheMouse(e, src, _l, _t, _w, _h, _i, false, true);4933
4934let { left, top, width, height } = select;4935
4936let hasSelect = width > 0 || height > 0;4937
4938hasSelect && setSelect(select);4939
4940if (drag.setScale && hasSelect) {4941// if (syncKey != null) {4942// dragX = drag.x;4943// dragY = drag.y;4944// }4945
4946let xOff = left,4947xDim = width,4948yOff = top,4949yDim = height;4950
4951if (scaleX.ori == 1) {4952xOff = top,4953xDim = height,4954yOff = left,4955yDim = width;4956}4957
4958if (dragX) {4959_setScale(xScaleKey,4960posToVal(xOff, xScaleKey),4961posToVal(xOff + xDim, xScaleKey)4962);4963}4964
4965if (dragY) {4966for (let k in scales) {4967let sc = scales[k];4968
4969if (k != xScaleKey && sc.from == null && sc.min != inf) {4970_setScale(k,4971posToVal(yOff + yDim, k),4972posToVal(yOff, k)4973);4974}4975}4976}4977
4978hideSelect();4979}4980else if (cursor.lock) {4981cursor._lock = !cursor._lock;4982
4983if (!cursor._lock)4984updateCursor(null, true, false);4985}4986
4987if (e != null) {4988offMouse(mouseup, doc);4989pubSync(mouseup, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null);4990}4991}4992
4993function mouseLeave(e, src, _l, _t, _w, _h, _i) {4994if (!cursor._lock) {4995let _dragging = dragging;4996
4997if (dragging) {4998// handle case when mousemove aren't fired all the way to edges by browser4999let snapH = true;5000let snapV = true;5001let snapProx = 10;5002
5003let dragH, dragV;5004
5005if (scaleX.ori == 0) {5006dragH = dragX;5007dragV = dragY;5008}5009else {5010dragH = dragY;5011dragV = dragX;5012}5013
5014if (dragH && dragV) {5015// maybe omni corner snap5016snapH = mouseLeft1 <= snapProx || mouseLeft1 >= plotWidCss - snapProx;5017snapV = mouseTop1 <= snapProx || mouseTop1 >= plotHgtCss - snapProx;5018}5019
5020if (dragH && snapH)5021mouseLeft1 = mouseLeft1 < mouseLeft0 ? 0 : plotWidCss;5022
5023if (dragV && snapV)5024mouseTop1 = mouseTop1 < mouseTop0 ? 0 : plotHgtCss;5025
5026updateCursor(null, true, true);5027
5028dragging = false;5029}5030
5031mouseLeft1 = -10;5032mouseTop1 = -10;5033
5034// passing a non-null timestamp to force sync/mousemove event5035updateCursor(null, true, true);5036
5037if (_dragging)5038dragging = _dragging;5039}5040}5041
5042function dblClick(e, src, _l, _t, _w, _h, _i) {5043autoScaleX();5044
5045hideSelect();5046
5047if (e != null)5048pubSync(dblclick, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null);5049}5050
5051function syncPxRatio() {5052axes.forEach(syncFontSize);5053_setSize(self.width, self.height, true);5054}5055
5056on(dppxchange, win, syncPxRatio);5057
5058// internal pub/sub5059const events = {};5060
5061events.mousedown = mouseDown;5062events.mousemove = mouseMove;5063events.mouseup = mouseUp;5064events.dblclick = dblClick;5065events["setSeries"] = (e, src, idx, opts) => {5066setSeries(idx, opts, true, false);5067};5068
5069if (cursor.show) {5070onMouse(mousedown, over, mouseDown);5071onMouse(mousemove, over, mouseMove);5072onMouse(mouseenter, over, syncRect);5073onMouse(mouseleave, over, mouseLeave);5074
5075onMouse(dblclick, over, dblClick);5076
5077cursorPlots.add(self);5078
5079self.syncRect = syncRect;5080}5081
5082// external on/off5083const hooks = self.hooks = opts.hooks || {};5084
5085function fire(evName, a1, a2) {5086if (evName in hooks) {5087hooks[evName].forEach(fn => {5088fn.call(null, self, a1, a2);5089});5090}5091}5092
5093(opts.plugins || []).forEach(p => {5094for (let evName in p.hooks)5095hooks[evName] = (hooks[evName] || []).concat(p.hooks[evName]);5096});5097
5098const syncOpts = assign({5099key: null,5100setSeries: false,5101filters: {5102pub: retTrue,5103sub: retTrue,5104},5105scales: [xScaleKey, series[1] ? series[1].scale : null],5106match: [retEq, retEq],5107values: [null, null],5108}, cursor.sync);5109
5110(cursor.sync = syncOpts);5111
5112const syncKey = syncOpts.key;5113
5114const sync = _sync(syncKey);5115
5116function pubSync(type, src, x, y, w, h, i) {5117if (syncOpts.filters.pub(type, src, x, y, w, h, i))5118sync.pub(type, src, x, y, w, h, i);5119}5120
5121sync.sub(self);5122
5123function pub(type, src, x, y, w, h, i) {5124if (syncOpts.filters.sub(type, src, x, y, w, h, i))5125events[type](null, src, x, y, w, h, i);5126}5127
5128(self.pub = pub);5129
5130function destroy() {5131sync.unsub(self);5132cursorPlots.delete(self);5133mouseListeners.clear();5134off(dppxchange, win, syncPxRatio);5135root.remove();5136fire("destroy");5137}5138
5139self.destroy = destroy;5140
5141function _init() {5142fire("init", opts, data);5143
5144setData(data || opts.data, false);5145
5146if (pendScales[xScaleKey])5147setScale(xScaleKey, pendScales[xScaleKey]);5148else5149autoScaleX();5150
5151_setSize(opts.width, opts.height);5152
5153updateCursor(null, true, false);5154
5155setSelect(select, false);5156}5157
5158series.forEach(initSeries);5159
5160axes.forEach(initAxis);5161
5162if (then) {5163if (then instanceof HTMLElement) {5164then.appendChild(root);5165_init();5166}5167else5168then(self, _init);5169}5170else5171_init();5172
5173return self;5174}
5175
5176uPlot.assign = assign;5177uPlot.fmtNum = fmtNum;5178uPlot.rangeNum = rangeNum;5179uPlot.rangeLog = rangeLog;5180uPlot.rangeAsinh = rangeAsinh;5181uPlot.orient = orient;5182
5183{
5184uPlot.join = join;5185}
5186
5187{
5188uPlot.fmtDate = fmtDate;5189uPlot.tzDate = tzDate;5190}
5191
5192{
5193uPlot.sync = _sync;5194}
5195
5196{
5197uPlot.addGap = addGap;5198uPlot.clipGaps = clipGaps;5199
5200let paths = uPlot.paths = {5201points,5202};5203
5204(paths.linear = linear);5205(paths.stepped = stepped);5206(paths.bars = bars);5207(paths.spline = monotoneCubic);5208}
5209
5210export { uPlot as default };5211