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