diff --git a/assets/css/main.css b/assets/css/main.css
index 4fbf6a7..8b3ca8b 100644
--- a/assets/css/main.css
+++ b/assets/css/main.css
@@ -1,4 +1,7 @@
@import url(https://fonts.googleapis.com/css?family=Open+Sans:700,300);
+
+@import url(uplot/dist/uPlot.min.css);
+
@import url(../css/icons.css);
* {
diff --git a/assets/html/index.html b/assets/html/index.html
index f2fa20b..ae2f675 100644
--- a/assets/html/index.html
+++ b/assets/html/index.html
@@ -6,8 +6,6 @@
-
-
diff --git a/assets/js/graph.js b/assets/js/graph.js
index 70227a8..df1971d 100644
--- a/assets/js/graph.js
+++ b/assets/js/graph.js
@@ -1,4 +1,4 @@
-import uPlot from '../lib/uPlot.esm'
+import uPlot from 'uplot'
import { RelativeScale } from './scale'
diff --git a/assets/js/servers.js b/assets/js/servers.js
index e5387e6..4e3d056 100644
--- a/assets/js/servers.js
+++ b/assets/js/servers.js
@@ -1,4 +1,4 @@
-import uPlot from '../lib/uPlot.esm'
+import uPlot from 'uplot'
import { RelativeScale } from './scale'
diff --git a/assets/lib/uPlot.esm.js b/assets/lib/uPlot.esm.js
deleted file mode 100644
index 0c32cb9..0000000
--- a/assets/lib/uPlot.esm.js
+++ /dev/null
@@ -1,2640 +0,0 @@
-/**
-* Copyright (c) 2020, Leon Sorokin
-* All rights reserved. (MIT Licensed)
-*
-* uPlot.js (μPlot)
-* A small, fast chart for time series, lines, areas, ohlc & bars
-* https://github.com/leeoniya/uPlot (v1.0.8)
-*/
-
-function debounce(fn, time) {
- let pending = null;
-
- function run() {
- pending = null;
- fn();
- }
-
- return function() {
- clearTimeout(pending);
- pending = setTimeout(run, time);
- }
-}
-
-// binary search for index of closest value
-function closestIdx(num, arr, lo, hi) {
- let mid;
- lo = lo || 0;
- hi = hi || arr.length - 1;
- let bitwise = hi <= 2147483647;
-
- while (hi - lo > 1) {
- mid = bitwise ? (lo + hi) >> 1 : floor((lo + hi) / 2);
-
- if (arr[mid] < num)
- lo = mid;
- else
- hi = mid;
- }
-
- if (num - arr[lo] <= arr[hi] - num)
- return lo;
-
- return hi;
-}
-
-function getMinMax(data, _i0, _i1) {
-// console.log("getMinMax()");
-
- let _min = inf;
- let _max = -inf;
-
- for (let i = _i0; i <= _i1; i++) {
- if (data[i] != null) {
- _min = min(_min, data[i]);
- _max = max(_max, data[i]);
- }
- }
-
- return [_min, _max];
-}
-
-// this ensures that non-temporal/numeric y-axes get multiple-snapped padding added above/below
-// TODO: also account for incrs when snapping to ensure top of axis gets a tick & value
-function rangeNum(min, max, mult, extra) {
- // auto-scale Y
- const delta = max - min;
- const mag = log10(delta || abs(max) || 1);
- const exp = floor(mag);
- const incr = pow(10, exp) * mult;
- const buf = delta == 0 ? incr : 0;
-
- let snappedMin = round6(incrRoundDn(min - buf, incr));
- let snappedMax = round6(incrRoundUp(max + buf, incr));
-
- if (extra) {
- // for flat data, always use 0 as one chart extreme & place data in center
- if (delta == 0) {
- if (max > 0) {
- snappedMin = 0;
- snappedMax = max * 2;
- }
- else if (max < 0) {
- snappedMax = 0;
- snappedMin = min * 2;
- }
- }
- else {
- // if buffer is too small, increase it
- if (snappedMax - max < incr)
- snappedMax += incr;
-
- if (min - snappedMin < incr)
- snappedMin -= incr;
-
- // if original data never crosses 0, use 0 as one chart extreme
- if (min >= 0 && snappedMin < 0)
- snappedMin = 0;
-
- if (max <= 0 && snappedMax > 0)
- snappedMax = 0;
- }
- }
-
- return [snappedMin, snappedMax];
-}
-
-const M = Math;
-
-const abs = M.abs;
-const floor = M.floor;
-const round = M.round;
-const ceil = M.ceil;
-const min = M.min;
-const max = M.max;
-const pow = M.pow;
-const log10 = M.log10;
-const PI = M.PI;
-
-const inf = Infinity;
-
-function incrRound(num, incr) {
- return round(num/incr)*incr;
-}
-
-function clamp(num, _min, _max) {
- return min(max(num, _min), _max);
-}
-
-function fnOrSelf(v) {
- return typeof v == "function" ? v : () => v;
-}
-
-function retArg2(a, b) {
- return b;
-}
-
-function incrRoundUp(num, incr) {
- return ceil(num/incr)*incr;
-}
-
-function incrRoundDn(num, incr) {
- return floor(num/incr)*incr;
-}
-
-function round3(val) {
- return round(val * 1e3) / 1e3;
-}
-
-function round6(val) {
- return round(val * 1e6) / 1e6;
-}
-
-//export const assign = Object.assign;
-
-const isArr = Array.isArray;
-
-function isStr(v) {
- return typeof v === 'string';
-}
-
-function isObj(v) {
- return typeof v === 'object' && v !== null;
-}
-
-function copy(o) {
- let out;
-
- if (isArr(o))
- out = o.map(copy);
- else if (isObj(o)) {
- out = {};
- for (var k in o)
- out[k] = copy(o[k]);
- }
- else
- out = o;
-
- return out;
-}
-
-function assign(targ) {
- let args = arguments;
-
- for (let i = 1; i < args.length; i++) {
- let src = args[i];
-
- for (let key in src) {
- if (isObj(targ[key]))
- assign(targ[key], copy(src[key]));
- else
- targ[key] = copy(src[key]);
- }
- }
-
- return targ;
-}
-
-const WIDTH = "width";
-const HEIGHT = "height";
-const TOP = "top";
-const BOTTOM = "bottom";
-const LEFT = "left";
-const RIGHT = "right";
-const firstChild = "firstChild";
-const createElement = "createElement";
-const hexBlack = "#000";
-const classList = "classList";
-
-const mousemove = "mousemove";
-const mousedown = "mousedown";
-const mouseup = "mouseup";
-const mouseenter = "mouseenter";
-const mouseleave = "mouseleave";
-const dblclick = "dblclick";
-const resize = "resize";
-const scroll = "scroll";
-
-const rAF = requestAnimationFrame;
-const doc = document;
-const win = window;
-const pxRatio = devicePixelRatio;
-
-function addClass(el, c) {
- c != null && el[classList].add(c);
-}
-
-function remClass(el, c) {
- el[classList].remove(c);
-}
-
-function setStylePx(el, name, value) {
- el.style[name] = value + "px";
-}
-
-function placeTag(tag, cls, targ, refEl) {
- let el = doc[createElement](tag);
-
- if (cls != null)
- addClass(el, cls);
-
- if (targ != null)
- targ.insertBefore(el, refEl);
-
- return el;
-}
-
-function placeDiv(cls, targ) {
- return placeTag("div", cls, targ);
-}
-
-function trans(el, xPos, yPos) {
- el.style.transform = "translate(" + xPos + "px," + yPos + "px)";
-}
-
-const evOpts = {passive: true};
-
-function on(ev, el, cb) {
- el.addEventListener(ev, cb, evOpts);
-}
-
-function off(ev, el, cb) {
- el.removeEventListener(ev, cb, evOpts);
-}
-
-const months = [
- "January",
- "February",
- "March",
- "April",
- "May",
- "June",
- "July",
- "August",
- "September",
- "October",
- "November",
- "December",
-];
-
-const days = [
- "Sunday",
- "Monday",
- "Tuesday",
- "Wednesday",
- "Thursday",
- "Friday",
- "Saturday",
-];
-
-function slice3(str) {
- return str.slice(0, 3);
-}
-
-const days3 = days.map(slice3);
-
-const months3 = months.map(slice3);
-
-const engNames = {
- MMMM: months,
- MMM: months3,
- WWWW: days,
- WWW: days3,
-};
-
-function zeroPad2(int) {
- return (int < 10 ? '0' : '') + int;
-}
-
-function zeroPad3(int) {
- return (int < 10 ? '00' : int < 100 ? '0' : '') + int;
-}
-
-/*
-function suffix(int) {
- let mod10 = int % 10;
-
- return int + (
- mod10 == 1 && int != 11 ? "st" :
- mod10 == 2 && int != 12 ? "nd" :
- mod10 == 3 && int != 13 ? "rd" : "th"
- );
-}
-*/
-
-const getFullYear = 'getFullYear';
-const getMonth = 'getMonth';
-const getDate = 'getDate';
-const getDay = 'getDay';
-const getHours = 'getHours';
-const getMinutes = 'getMinutes';
-const getSeconds = 'getSeconds';
-const getMilliseconds = 'getMilliseconds';
-
-const subs = {
- // 2019
- YYYY: d => d[getFullYear](),
- // 19
- YY: d => (d[getFullYear]()+'').slice(2),
- // July
- MMMM: (d, names) => names.MMMM[d[getMonth]()],
- // Jul
- MMM: (d, names) => names.MMM[d[getMonth]()],
- // 07
- MM: d => zeroPad2(d[getMonth]()+1),
- // 7
- M: d => d[getMonth]()+1,
- // 09
- DD: d => zeroPad2(d[getDate]()),
- // 9
- D: d => d[getDate](),
- // Monday
- WWWW: (d, names) => names.WWWW[d[getDay]()],
- // Mon
- WWW: (d, names) => names.WWW[d[getDay]()],
- // 03
- HH: d => zeroPad2(d[getHours]()),
- // 3
- H: d => d[getHours](),
- // 9 (12hr, unpadded)
- h: d => {let h = d[getHours](); return h == 0 ? 12 : h > 12 ? h - 12 : h;},
- // AM
- AA: d => d[getHours]() >= 12 ? 'PM' : 'AM',
- // am
- aa: d => d[getHours]() >= 12 ? 'pm' : 'am',
- // a
- a: d => d[getHours]() >= 12 ? 'p' : 'a',
- // 09
- mm: d => zeroPad2(d[getMinutes]()),
- // 9
- m: d => d[getMinutes](),
- // 09
- ss: d => zeroPad2(d[getSeconds]()),
- // 9
- s: d => d[getSeconds](),
- // 374
- fff: d => zeroPad3(d[getMilliseconds]()),
-};
-
-function fmtDate(tpl, names) {
- names = names || engNames;
- let parts = [];
-
- let R = /\{([a-z]+)\}|[^{]+/gi, m;
-
- while (m = R.exec(tpl))
- parts.push(m[0][0] == '{' ? subs[m[1]] : m[0]);
-
- return d => {
- let out = '';
-
- for (let i = 0; i < parts.length; i++)
- out += typeof parts[i] == "string" ? parts[i] : parts[i](d, names);
-
- return out;
- }
-}
-
-// https://stackoverflow.com/questions/15141762/how-to-initialize-a-javascript-date-to-a-particular-time-zone/53652131#53652131
-function tzDate(date, tz) {
- let date2 = new Date(date.toLocaleString('en-US', {timeZone: tz}));
- date2.setMilliseconds(date[getMilliseconds]());
- return date2;
-}
-
-//export const series = [];
-
-// default formatters:
-
-function genIncrs(minExp, maxExp, mults) {
- let incrs = [];
-
- for (let exp = minExp; exp < maxExp; exp++) {
- for (let i = 0; i < mults.length; i++) {
- let incr = mults[i] * pow(10, exp);
- incrs.push(+incr.toFixed(abs(exp)));
- }
- }
-
- return incrs;
-}
-
-const incrMults = [1,2,5];
-
-const decIncrs = genIncrs(-12, 0, incrMults);
-
-const intIncrs = genIncrs(0, 12, incrMults);
-
-const numIncrs = decIncrs.concat(intIncrs);
-
-let s = 1,
- m = 60,
- h = m * m,
- d = h * 24,
- mo = d * 30,
- y = d * 365;
-
-// starting below 1e-3 is a hack to allow the incr finder to choose & bail out at incr < 1ms
-const timeIncrs = [5e-4].concat(genIncrs(-3, 0, incrMults), [
- // minute divisors (# of secs)
- 1,
- 5,
- 10,
- 15,
- 30,
- // hour divisors (# of mins)
- m,
- m * 5,
- m * 10,
- m * 15,
- m * 30,
- // day divisors (# of hrs)
- h,
- h * 2,
- h * 3,
- h * 4,
- h * 6,
- h * 8,
- h * 12,
- // month divisors TODO: need more?
- d,
- d * 2,
- d * 3,
- d * 4,
- d * 5,
- d * 6,
- d * 7,
- d * 8,
- d * 9,
- d * 10,
- d * 15,
- // year divisors (# months, approx)
- mo,
- mo * 2,
- mo * 3,
- mo * 4,
- mo * 6,
- // century divisors
- y,
- y * 2,
- y * 5,
- y * 10,
- y * 25,
- y * 50,
- y * 100,
-]);
-
-function timeAxisStamps(stampCfg, fmtDate) {
- return stampCfg.map(s => [
- s[0],
- fmtDate(s[1]),
- s[2],
- fmtDate(s[4] ? s[1] + s[3] : s[3]),
- ]);
-}
-
-const yyyy = "{YYYY}";
-const NLyyyy = "\n" + yyyy;
-const md = "{M}/{D}";
-const NLmd = "\n" + md;
-
-const aa = "{aa}";
-const hmm = "{h}:{mm}";
-const hmmaa = hmm + aa;
-const ss = ":{ss}";
-
-// [0]: minimum num secs in the tick incr
-// [1]: normal tick format
-// [2]: when a differing is encountered - 1: sec, 2: min, 3: hour, 4: day, 5: week, 6: month, 7: year
-// [3]: use a longer more contextual format
-// [4]: modes: 0: replace [1] -> [3], 1: concat [1] + [3]
-const _timeAxisStamps = [
- [y, yyyy, 7, "", 1],
- [d * 28, "{MMM}", 7, NLyyyy, 1],
- [d, md, 7, NLyyyy, 1],
- [h, "{h}" + aa, 4, NLmd, 1],
- [m, hmmaa, 4, NLmd, 1],
- [s, ss, 2, NLmd + " " + hmmaa, 1],
- [1e-3, ss + ".{fff}", 2, NLmd + " " + hmmaa, 1],
-];
-
-// TODO: will need to accept spaces[] and pull incr into the loop when grid will be non-uniform, eg for log scales.
-// currently we ignore this for months since they're *nearly* uniform and the added complexity is not worth it
-function timeAxisVals(tzDate, stamps) {
- return (self, splits, space, incr) => {
- let s = stamps.find(e => incr >= e[0]);
-
- // these track boundaries when a full label is needed again
- let prevYear = null;
- let prevDate = null;
- let prevMinu = null;
-
- return splits.map((split, i) => {
- let date = tzDate(split);
-
- let newYear = date[getFullYear]();
- let newDate = date[getDate]();
- let newMinu = date[getMinutes]();
-
- let diffYear = newYear != prevYear;
- let diffDate = newDate != prevDate;
- let diffMinu = newMinu != prevMinu;
-
- let stamp = s[2] == 7 && diffYear || s[2] == 4 && diffDate || s[2] == 2 && diffMinu ? s[3] : s[1];
-
- prevYear = newYear;
- prevDate = newDate;
- prevMinu = newMinu;
-
- return stamp(date);
- });
- }
-}
-
-function mkDate(y, m, d) {
- return new Date(y, m, d);
-}
-
-// the ensures that axis ticks, values & grid are aligned to logical temporal breakpoints and not an arbitrary timestamp
-// https://www.timeanddate.com/time/dst/
-// https://www.timeanddate.com/time/dst/2019.html
-// https://www.epochconverter.com/timezones
-function timeAxisSplits(tzDate) {
- return (self, scaleMin, scaleMax, incr, pctSpace) => {
- let splits = [];
- let isMo = incr >= mo && incr < y;
-
- // get the timezone-adjusted date
- let minDate = tzDate(scaleMin);
- let minDateTs = minDate / 1e3;
-
- // get ts of 12am (this lands us at or before the original scaleMin)
- let minMin = mkDate(minDate[getFullYear](), minDate[getMonth](), isMo ? 1 : minDate[getDate]());
- let minMinTs = minMin / 1e3;
-
- if (isMo) {
- let moIncr = incr / mo;
- // let tzOffset = scaleMin - minDateTs; // needed?
- let split = minDateTs == minMinTs ? minDateTs : mkDate(minMin[getFullYear](), minMin[getMonth]() + moIncr, 1) / 1e3;
- let splitDate = new Date(split * 1e3);
- let baseYear = splitDate[getFullYear]();
- let baseMonth = splitDate[getMonth]();
-
- for (let i = 0; split <= scaleMax; i++) {
- let next = mkDate(baseYear, baseMonth + moIncr * i, 1);
- let offs = next - tzDate(next / 1e3);
-
- split = (+next + offs) / 1e3;
-
- if (split <= scaleMax)
- splits.push(split);
- }
- }
- else {
- let incr0 = incr >= d ? d : incr;
- let tzOffset = floor(scaleMin) - floor(minDateTs);
- let split = minMinTs + tzOffset + incrRoundUp(minDateTs - minMinTs, incr0);
- splits.push(split);
-
- let date0 = tzDate(split);
-
- let prevHour = date0[getHours]() + (date0[getMinutes]() / m) + (date0[getSeconds]() / h);
- let incrHours = incr / h;
-
- while (1) {
- split = round3(split + incr);
-
- let expectedHour = floor(round6(prevHour + incrHours)) % 24;
- let splitDate = tzDate(split);
- let actualHour = splitDate.getHours();
-
- let dstShift = actualHour - expectedHour;
-
- if (dstShift > 1)
- dstShift = -1;
-
- split -= dstShift * h;
-
- if (split > scaleMax)
- break;
-
- prevHour = (prevHour + incrHours) % 24;
-
- // add a tick only if it's further than 70% of the min allowed label spacing
- let prevSplit = splits[splits.length - 1];
- let pctIncr = round3((split - prevSplit) / incr);
-
- if (pctIncr * pctSpace >= .7)
- splits.push(split);
- }
- }
-
- return splits;
- }
-}
-
-function timeSeriesStamp(stampCfg, fmtDate) {
- return fmtDate(stampCfg);
-}
-const _timeSeriesStamp = '{YYYY}-{MM}-{DD} {h}:{mm}{aa}';
-
-function timeSeriesVal(tzDate, stamp) {
- return (self, val) => stamp(tzDate(val));
-}
-
-function cursorPoint(self, si) {
- let s = self.series[si];
-
- let pt = placeDiv();
-
- pt.style.background = s.stroke || hexBlack;
-
- let dia = ptDia(s.width, 1);
- let mar = (dia - 1) / -2;
-
- setStylePx(pt, WIDTH, dia);
- setStylePx(pt, HEIGHT, dia);
- setStylePx(pt, "marginLeft", mar);
- setStylePx(pt, "marginTop", mar);
-
- return pt;
-}
-
-const cursorOpts = {
- show: true,
- x: true,
- y: true,
- lock: false,
- points: {
- show: cursorPoint,
- },
-
- drag: {
- setScale: true,
- x: true,
- y: false,
- },
-
- focus: {
- prox: -1,
- },
-
- locked: false,
- left: -10,
- top: -10,
- idx: null,
-};
-
-const grid = {
- show: true,
- stroke: "rgba(0,0,0,0.07)",
- width: 2,
-// dash: [],
-};
-
-const ticks = assign({}, grid, {size: 10});
-
-const font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
-const labelFont = "bold " + font;
-const lineMult = 1.5; // font-size multiplier
-
-const xAxisOpts = {
- type: "x",
- show: true,
- scale: "x",
- space: 50,
- gap: 5,
- size: 50,
- labelSize: 30,
- labelFont,
- side: 2,
-// class: "x-vals",
-// incrs: timeIncrs,
-// values: timeVals,
- grid,
- ticks,
- font,
- rotate: 0,
-};
-
-const numSeriesLabel = "Value";
-const timeSeriesLabel = "Time";
-
-const xSeriesOpts = {
- show: true,
- scale: "x",
-// label: "Time",
-// value: v => stamp(new Date(v * 1e3)),
-
- // internal caches
- min: inf,
- max: -inf,
- idxs: [],
-};
-
-// alternative: https://stackoverflow.com/a/2254896
-let fmtNum = new Intl.NumberFormat(navigator.language);
-
-function numAxisVals(self, splits, space, incr) {
- return splits.map(fmtNum.format);
-}
-
-function numAxisSplits(self, scaleMin, scaleMax, incr, pctSpace, forceMin) {
- scaleMin = forceMin ? scaleMin : +incrRoundUp(scaleMin, incr).toFixed(12);
-
- let splits = [];
-
- for (let val = scaleMin; val <= scaleMax; val = +(val + incr).toFixed(12))
- splits.push(val);
-
- return splits;
-}
-
-function numSeriesVal(self, val) {
- return val;
-}
-
-const yAxisOpts = {
- type: "y",
- show: true,
- scale: "y",
- space: 40,
- gap: 5,
- size: 50,
- labelSize: 30,
- labelFont,
- side: 3,
-// class: "y-vals",
-// incrs: numIncrs,
-// values: (vals, space) => vals,
- grid,
- ticks,
- font,
- rotate: 0,
-};
-
-// takes stroke width
-function ptDia(width, mult) {
- return max(round3(5 * mult), round3(width * mult) * 2 - 1);
-}
-
-function seriesPoints(self, si) {
- const dia = ptDia(self.series[si].width, pxRatio);
- let maxPts = self.bbox.width / dia / 2;
- let idxs = self.series[0].idxs;
- return idxs[1] - idxs[0] <= maxPts;
-}
-
-const ySeriesOpts = {
-// type: "n",
- scale: "y",
- show: true,
- band: false,
- alpha: 1,
- points: {
- show: seriesPoints,
- // stroke: "#000",
- // fill: "#fff",
- // width: 1,
- // size: 10,
- },
-// label: "Value",
-// value: v => v,
- values: null,
-
- // internal caches
- min: inf,
- max: -inf,
- idxs: [],
-
- path: null,
- clip: null,
-};
-
-const xScaleOpts = {
- time: true,
- auto: false,
- distr: 1,
- min: inf,
- max: -inf,
-};
-
-const yScaleOpts = assign({}, xScaleOpts, {
- time: false,
- auto: true,
-});
-
-const syncs = {};
-
-function _sync(opts) {
- let clients = [];
-
- return {
- sub(client) {
- clients.push(client);
- },
- unsub(client) {
- clients = clients.filter(c => c != client);
- },
- pub(type, self, x, y, w, h, i) {
- if (clients.length > 1) {
- clients.forEach(client => {
- client != self && client.pub(type, self, x, y, w, h, i);
- });
- }
- }
- };
-}
-
-function setDefaults(d, xo, yo) {
- return [d[0], d[1]].concat(d.slice(2)).map((o, i) => setDefault(o, i, xo, yo));
-}
-
-function setDefault(o, i, xo, yo) {
- return assign({}, (i == 0 || o && o.side % 2 == 0 ? xo : yo), o);
-}
-
-function getYPos(val, scale, hgt, top) {
- let pctY = (val - scale.min) / (scale.max - scale.min);
- return top + (1 - pctY) * hgt;
-}
-
-function getXPos(val, scale, wid, lft) {
- let pctX = (val - scale.min) / (scale.max - scale.min);
- return lft + pctX * wid;
-}
-
-function snapTimeX(self, dataMin, dataMax) {
- return [dataMin, dataMax > dataMin ? dataMax : dataMax + 86400];
-}
-
-function snapNumX(self, dataMin, dataMax) {
- const delta = dataMax - dataMin;
-
- if (delta == 0) {
- const mag = log10(delta || abs(dataMax) || 1);
- const exp = floor(mag) + 1;
- return [dataMin, incrRoundUp(dataMax, pow(10, exp))];
- }
- else
- return [dataMin, dataMax];
-}
-
-// this ensures that non-temporal/numeric y-axes get multiple-snapped padding added above/below
-// TODO: also account for incrs when snapping to ensure top of axis gets a tick & value
-function snapNumY(self, dataMin, dataMax) {
- return rangeNum(dataMin, dataMax, 0.2, true);
-}
-
-// dim is logical (getClientBoundingRect) pixels, not canvas pixels
-function findIncr(valDelta, incrs, dim, minSpace) {
- let pxPerUnit = dim / valDelta;
-
- for (var i = 0; i < incrs.length; i++) {
- let space = incrs[i] * pxPerUnit;
-
- if (space >= minSpace)
- return [incrs[i], space];
- }
-}
-
-function filtMouse(e) {
- return e.button == 0;
-}
-
-function pxRatioFont(font) {
- let fontSize;
- font = font.replace(/\d+/, m => (fontSize = round(m * pxRatio)));
- return [font, fontSize];
-}
-
-function uPlot(opts, data, then) {
- const self = {};
-
- const root = self.root = placeDiv("uplot");
-
- if (opts.id != null)
- root.id = opts.id;
-
- addClass(root, opts.class);
-
- if (opts.title) {
- let title = placeDiv("title", root);
- title.textContent = opts.title;
- }
-
- const can = placeTag("canvas");
- const ctx = self.ctx = can.getContext("2d");
-
- const wrap = placeDiv("wrap", root);
- const under = placeDiv("under", wrap);
- wrap.appendChild(can);
- const over = placeDiv("over", wrap);
-
- opts = copy(opts);
-
- (opts.plugins || []).forEach(p => {
- if (p.opts)
- opts = p.opts(self, opts) || opts;
- });
-
- let ready = false;
-
- const series = setDefaults(opts.series, xSeriesOpts, ySeriesOpts);
- const axes = setDefaults(opts.axes || [], xAxisOpts, yAxisOpts);
- const scales = (opts.scales = opts.scales || {});
-
- const gutters = assign({
- x: round(yAxisOpts.size / 2),
- y: round(xAxisOpts.size / 3),
- }, opts.gutters);
-
-// self.tz = opts.tz || Intl.DateTimeFormat().resolvedOptions().timeZone;
- const _tzDate = (opts.tzDate || (ts => new Date(ts * 1e3)));
- const _fmtDate = (opts.fmtDate || fmtDate);
-
- const _timeAxisSplits = timeAxisSplits(_tzDate);
- const _timeAxisVals = timeAxisVals(_tzDate, timeAxisStamps(_timeAxisStamps, _fmtDate));
- const _timeSeriesVal = timeSeriesVal(_tzDate, timeSeriesStamp(_timeSeriesStamp, _fmtDate));
-
- self.series = series;
- self.axes = axes;
- self.scales = scales;
-
- const pendScales = {};
-
- // explicitly-set initial scales
- for (let k in scales) {
- let sc = scales[k];
-
- if (sc.min != null || sc.max != null)
- pendScales[k] = {min: sc.min, max: sc.max};
- }
-
- const legend = assign({show: true}, opts.legend);
- const showLegend = legend.show;
-
- let legendEl;
- let legendRows = [];
- let legendCols;
- let multiValLegend = false;
-
- if (showLegend) {
- legendEl = placeTag("table", "legend", root);
-
- const getMultiVals = series[1].values;
- multiValLegend = getMultiVals != null;
-
- if (multiValLegend) {
- let head = placeTag("tr", "labels", legendEl);
- placeTag("th", null, head);
- legendCols = getMultiVals(self, 1, 0);
-
- for (var key in legendCols)
- placeTag("th", null, head).textContent = key;
- }
- else {
- legendCols = {_: 0};
- addClass(legendEl, "inline");
- }
- }
-
- function initLegendRow(s, i) {
- if (i == 0 && multiValLegend)
- return null;
-
- let _row = [];
-
- let row = placeTag("tr", "series", legendEl, legendEl.childNodes[i]);
-
- addClass(row, s.class);
-
- if (!s.show)
- addClass(row, "off");
-
- let label = placeTag("th", null, row);
-
- let indic = placeDiv("ident", label);
- s.width && (indic.style.borderColor = s.stroke);
- indic.style.backgroundColor = s.fill;
-
- let text = placeDiv("text", label);
- text.textContent = s.label;
-
- if (i > 0) {
- on("click", label, e => {
- if ( cursor.locked)
- return;
-
- filtMouse(e) && setSeries(series.indexOf(s), {show: !s.show}, syncOpts.setSeries);
- });
-
- if (cursorFocus) {
- on(mouseenter, label, e => {
- if (cursor.locked)
- return;
-
- setSeries(series.indexOf(s), {focus: true}, syncOpts.setSeries);
- });
- }
- }
-
- for (var key in legendCols) {
- let v = placeTag("td", null, row);
- v.textContent = "--";
- _row.push(v);
- }
-
- return _row;
- }
-
- const cursor = (self.cursor = assign({}, cursorOpts, opts.cursor));
-
- (cursor.points.show = fnOrSelf(cursor.points.show));
-
- const focus = self.focus = assign({}, opts.focus || {alpha: 0.3}, cursor.focus);
- const cursorFocus = focus.prox >= 0;
-
- // series-intersection markers
- let cursorPts = [null];
-
- function initCursorPt(s, si) {
- if (si > 0) {
- let pt = cursor.points.show(self, si);
-
- if (pt) {
- addClass(pt, "cursor-pt");
- addClass(pt, s.class);
- trans(pt, -10, -10);
- over.insertBefore(pt, cursorPts[si]);
-
- return pt;
- }
- }
- }
-
- function initSeries(s, i) {
- // init scales & defaults
- const scKey = s.scale;
-
- const sc = scales[scKey] = assign({}, (i == 0 ? xScaleOpts : yScaleOpts), scales[scKey]);
-
- let isTime = sc.time;
-
- sc.range = fnOrSelf(sc.range || (isTime ? snapTimeX : i == 0 ? snapNumX : snapNumY));
-
- s.spanGaps = s.spanGaps === true ? retArg2 : fnOrSelf(s.spanGaps || []);
-
- let sv = s.value;
- s.value = isTime ? (isStr(sv) ? timeSeriesVal(_tzDate, timeSeriesStamp(sv, _fmtDate)) : sv || _timeSeriesVal) : sv || numSeriesVal;
- s.label = s.label || (isTime ? timeSeriesLabel : numSeriesLabel);
-
- if (i > 0) {
- s.width = s.width == null ? 1 : s.width;
- s.paths = s.paths || ( buildPaths);
- let _ptDia = ptDia(s.width, 1);
- s.points = assign({}, {
- size: _ptDia,
- width: max(1, _ptDia * .2),
- }, s.points);
- s.points.show = fnOrSelf(s.points.show);
- s._paths = null;
- }
-
- if (showLegend)
- legendRows.splice(i, 0, initLegendRow(s, i));
-
- if ( cursor.show) {
- let pt = initCursorPt(s, i);
- pt && cursorPts.splice(i, 0, pt);
- }
- }
-
- function addSeries(opts, si) {
- si = si == null ? series.length : si;
-
- opts = setDefault(opts, si, xSeriesOpts, ySeriesOpts);
- series.splice(si, 0, opts);
- initSeries(series[si], si);
- }
-
- self.addSeries = addSeries;
-
- function delSeries(i) {
- series.splice(i, 1);
- legendRows.splice(i, 1)[0][0].parentNode.remove();
- cursorPts.splice(i, 1)[0].remove();
-
- // TODO: de-init no-longer-needed scales?
- }
-
- self.delSeries = delSeries;
-
- series.forEach(initSeries);
-
- // dependent scales inherit
- for (let k in scales) {
- let sc = scales[k];
-
- if (sc.from != null)
- scales[k] = assign({}, scales[sc.from], sc);
- }
-
- const xScaleKey = series[0].scale;
- const xScaleDistr = scales[xScaleKey].distr;
-
- function initAxis(axis, i) {
- if (axis.show) {
- let isVt = axis.side % 2;
-
- let sc = scales[axis.scale];
-
- // this can occur if all series specify non-default scales
- if (sc == null) {
- axis.scale = isVt ? series[1].scale : xScaleKey;
- sc = scales[axis.scale];
- }
-
- // also set defaults for incrs & values based on axis distr
- let isTime = sc.time;
-
- axis.space = fnOrSelf(axis.space);
- axis.rotate = fnOrSelf(axis.rotate);
- axis.incrs = fnOrSelf(axis.incrs || ( sc.distr == 2 ? intIncrs : (isTime ? timeIncrs : numIncrs)));
- axis.split = fnOrSelf(axis.split || (isTime && sc.distr == 1 ? _timeAxisSplits : numAxisSplits));
- let av = axis.values;
- axis.values = isTime ? (isArr(av) ? timeAxisVals(_tzDate, timeAxisStamps(av, _fmtDate)) : av || _timeAxisVals) : av || numAxisVals;
-
- axis.font = pxRatioFont(axis.font);
- axis.labelFont = pxRatioFont(axis.labelFont);
- }
- }
-
- // set axis defaults
- axes.forEach(initAxis);
-
- let dataLen;
-
- // rendered data window
- let i0 = null;
- let i1 = null;
- const idxs = series[0].idxs;
-
- let data0 = null;
-
- function setData(_data, _resetScales) {
- self.data = _data;
- data = _data.slice();
- data0 = data[0];
- dataLen = data0.length;
-
- if (xScaleDistr == 2)
- data[0] = data0.map((v, i) => i);
-
- resetYSeries();
-
- fire("setData");
-
- _resetScales !== false && autoScaleX();
- }
-
- self.setData = setData;
-
- function autoScaleX() {
- i0 = idxs[0] = 0;
- i1 = idxs[1] = dataLen - 1;
-
- let _min = xScaleDistr == 2 ? i0 : data[0][i0],
- _max = xScaleDistr == 2 ? i1 : data[0][i1];
-
- _setScale(xScaleKey, _min, _max);
- }
-
- function setCtxStyle(stroke, width, dash, fill) {
- ctx.strokeStyle = stroke || hexBlack;
- ctx.lineWidth = width;
- ctx.lineJoin = "round";
- ctx.setLineDash(dash || []);
- ctx.fillStyle = fill || hexBlack;
- }
-
- let fullWidCss;
- let fullHgtCss;
-
- let plotWidCss;
- let plotHgtCss;
-
- // plot margins to account for axes
- let plotLftCss;
- let plotTopCss;
-
- let plotLft;
- let plotTop;
- let plotWid;
- let plotHgt;
-
- self.bbox = {};
-
- function _setSize(width, height) {
- self.width = fullWidCss = plotWidCss = width;
- self.height = fullHgtCss = plotHgtCss = height;
- plotLftCss = plotTopCss = 0;
-
- calcPlotRect();
- calcAxesRects();
-
- let bb = self.bbox;
-
- plotLft = bb[LEFT] = incrRound(plotLftCss * pxRatio, 0.5);
- plotTop = bb[TOP] = incrRound(plotTopCss * pxRatio, 0.5);
- plotWid = bb[WIDTH] = incrRound(plotWidCss * pxRatio, 0.5);
- plotHgt = bb[HEIGHT] = incrRound(plotHgtCss * pxRatio, 0.5);
-
- setStylePx(under, LEFT, plotLftCss);
- setStylePx(under, TOP, plotTopCss);
- setStylePx(under, WIDTH, plotWidCss);
- setStylePx(under, HEIGHT, plotHgtCss);
-
- setStylePx(over, LEFT, plotLftCss);
- setStylePx(over, TOP, plotTopCss);
- setStylePx(over, WIDTH, plotWidCss);
- setStylePx(over, HEIGHT, plotHgtCss);
-
- setStylePx(wrap, WIDTH, fullWidCss);
- setStylePx(wrap, HEIGHT, fullHgtCss);
-
- can[WIDTH] = round(fullWidCss * pxRatio);
- can[HEIGHT] = round(fullHgtCss * pxRatio);
-
- syncRect();
-
- ready && _setScale(xScaleKey, scales[xScaleKey].min, scales[xScaleKey].max);
-
- ready && fire("setSize");
- }
-
- function setSize({width, height}) {
- _setSize(width, height);
- }
-
- self.setSize = setSize;
-
- // accumulate axis offsets, reduce canvas width
- function calcPlotRect() {
- // easements for edge labels
- let hasTopAxis = false;
- let hasBtmAxis = false;
- let hasRgtAxis = false;
- let hasLftAxis = false;
-
- axes.forEach((axis, i) => {
- if (axis.show) {
- let {side, size} = axis;
- let isVt = side % 2;
- let labelSize = axis.labelSize = (axis.label != null ? (axis.labelSize || 30) : 0);
-
- let fullSize = size + labelSize;
-
- if (fullSize > 0) {
- if (isVt) {
- plotWidCss -= fullSize;
-
- if (side == 3) {
- plotLftCss += fullSize;
- hasLftAxis = true;
- }
- else
- hasRgtAxis = true;
- }
- else {
- plotHgtCss -= fullSize;
-
- if (side == 0) {
- plotTopCss += fullSize;
- hasTopAxis = true;
- }
- else
- hasBtmAxis = true;
- }
- }
- }
- });
-
- // hz gutters
- if (hasTopAxis || hasBtmAxis) {
- if (!hasRgtAxis)
- plotWidCss -= gutters.x;
- if (!hasLftAxis) {
- plotWidCss -= gutters.x;
- plotLftCss += gutters.x;
- }
- }
-
- // vt gutters
- if (hasLftAxis || hasRgtAxis) {
- if (!hasBtmAxis)
- plotHgtCss -= gutters.y;
- if (!hasTopAxis) {
- plotHgtCss -= gutters.y;
- plotTopCss += gutters.y;
- }
- }
- }
-
- function calcAxesRects() {
- // will accum +
- let off1 = plotLftCss + plotWidCss;
- let off2 = plotTopCss + plotHgtCss;
- // will accum -
- let off3 = plotLftCss;
- let off0 = plotTopCss;
-
- function incrOffset(side, size) {
-
- switch (side) {
- case 1: off1 += size; return off1 - size;
- case 2: off2 += size; return off2 - size;
- case 3: off3 -= size; return off3 + size;
- case 0: off0 -= size; return off0 + size;
- }
- }
-
- axes.forEach((axis, i) => {
- let side = axis.side;
-
- axis._pos = incrOffset(side, axis.size);
-
- if (axis.label != null)
- axis._lpos = incrOffset(side, axis.labelSize);
- });
- }
-
- function setScales() {
- if (inBatch) {
- shouldSetScales = true;
- return;
- }
-
- // log("setScales()", arguments);
-
- if (dataLen > 0) {
- // wip scales
- let wipScales = copy(scales);
-
- for (let k in wipScales) {
- let wsc = wipScales[k];
- let psc = pendScales[k];
-
- if (psc != null) {
- assign(wsc, psc);
-
- // explicitly setting the x-scale invalidates everything (acts as redraw)
- if (k == xScaleKey)
- resetYSeries();
- }
- else if (k != xScaleKey) {
- wsc.min = inf;
- wsc.max = -inf;
- }
- }
-
- // pre-range y-scales from y series' data values
- series.forEach((s, i) => {
- let k = s.scale;
- let wsc = wipScales[k];
-
- // setting the x scale invalidates everything
- if (i == 0) {
- let minMax = wsc.range(self, wsc.min, wsc.max);
-
- wsc.min = minMax[0];
- wsc.max = minMax[1];
-
- i0 = closestIdx(wsc.min, data[0]);
- i1 = closestIdx(wsc.max, data[0]);
-
- // closest indices can be outside of view
- if (data[0][i0] < wsc.min)
- i0++;
- if (data[0][i1] > wsc.max)
- i1--;
-
- s.min = data0[i0];
- s.max = data0[i1];
- }
- else if (s.show && pendScales[k] == null) {
- // only run getMinMax() for invalidated series data, else reuse
- let minMax = s.min == inf ? (wsc.auto ? getMinMax(data[i], i0, i1) : [0,100]) : [s.min, s.max];
-
- // initial min/max
- wsc.min = min(wsc.min, s.min = minMax[0]);
- wsc.max = max(wsc.max, s.max = minMax[1]);
- }
-
- s.idxs[0] = i0;
- s.idxs[1] = i1;
- });
-
- // range independent scales
- for (let k in wipScales) {
- let wsc = wipScales[k];
-
- if (wsc.from == null && wsc.min != inf && pendScales[k] == null) {
- let minMax = wsc.range(self, wsc.min, wsc.max);
- wsc.min = minMax[0];
- wsc.max = minMax[1];
- }
- }
-
- // range dependent scales
- for (let k in wipScales) {
- let wsc = wipScales[k];
-
- if (wsc.from != null) {
- let base = wipScales[wsc.from];
-
- if (base.min != inf) {
- let minMax = wsc.range(self, base.min, base.max);
- wsc.min = minMax[0];
- wsc.max = minMax[1];
- }
- }
- }
-
- let changed = {};
-
- for (let k in wipScales) {
- let wsc = wipScales[k];
- let sc = scales[k];
-
- if (sc.min != wsc.min || sc.max != wsc.max) {
- sc.min = wsc.min;
- sc.max = wsc.max;
- changed[k] = true;
- }
-
- pendScales[k] = null;
- }
-
- // invalidate paths of all series on changed scales
- series.forEach(s => {
- if (changed[s.scale])
- s._paths = null;
- });
-
- for (let k in changed)
- fire("setScale", k);
- }
-
- cursor.show && updateCursor();
- }
-
- // TODO: drawWrap(si, drawPoints) (save, restore, translate, clip)
-
- function drawPoints(si) {
- // log("drawPoints()", arguments);
-
- let s = series[si];
- let p = s.points;
-
- const width = round3(s[WIDTH] * pxRatio);
- const offset = (width % 2) / 2;
- const isStroked = p.width > 0;
-
- let rad = (p.size - p.width) / 2 * pxRatio;
- let dia = round3(rad * 2);
-
- ctx.translate(offset, offset);
-
- ctx.save();
-
- ctx.beginPath();
- ctx.rect(
- plotLft - dia,
- plotTop - dia,
- plotWid + dia * 2,
- plotHgt + dia * 2,
- );
- ctx.clip();
-
- ctx.globalAlpha = s.alpha;
-
- const path = new Path2D();
-
- for (let pi = i0; pi <= i1; pi++) {
- if (data[si][pi] != null) {
- let x = round(getXPos(data[0][pi], scales[xScaleKey], plotWid, plotLft));
- let y = round(getYPos(data[si][pi], scales[s.scale], plotHgt, plotTop));
-
- path.moveTo(x + rad, y);
- path.arc(x, y, rad, 0, PI * 2);
- }
- }
-
- setCtxStyle(
- p.stroke || s.stroke || hexBlack,
- width,
- null,
- p.fill || (isStroked ? "#fff" : s.stroke || hexBlack),
- );
-
- ctx.fill(path);
- isStroked && ctx.stroke(path);
-
- ctx.globalAlpha = 1;
-
- ctx.restore();
-
- ctx.translate(-offset, -offset);
- }
-
- // grabs the nearest indices with y data outside of x-scale limits
- function getOuterIdxs(ydata) {
- let _i0 = clamp(i0 - 1, 0, dataLen - 1);
- let _i1 = clamp(i1 + 1, 0, dataLen - 1);
-
- while (ydata[_i0] == null && _i0 > 0)
- _i0--;
-
- while (ydata[_i1] == null && _i1 < dataLen - 1)
- _i1++;
-
- return [_i0, _i1];
- }
-
- let dir = 1;
-
- function drawSeries() {
- // path building loop must be before draw loop to ensure that all bands are fully constructed
- series.forEach((s, i) => {
- if (i > 0 && s.show && s._paths == null) {
- let _idxs = getOuterIdxs(data[i]);
- s._paths = s.paths(self, i, _idxs[0], _idxs[1]);
- }
- });
-
- series.forEach((s, i) => {
- if (i > 0 && s.show) {
- if (s._paths)
- drawPath(i);
-
- if (s.points.show(self, i, i0, i1))
- drawPoints(i);
-
- fire("drawSeries", i);
- }
- });
- }
-
- function drawPath(si) {
- const s = series[si];
-
- if (dir == 1) {
- const { stroke, fill, clip } = s._paths;
- const width = round3(s[WIDTH] * pxRatio);
- const offset = (width % 2) / 2;
-
- setCtxStyle(s.stroke, width, s.dash, s.fill);
-
- ctx.globalAlpha = s.alpha;
-
- ctx.translate(offset, offset);
-
- ctx.save();
-
- let lft = plotLft,
- top = plotTop,
- wid = plotWid,
- hgt = plotHgt;
-
- let halfWid = width * pxRatio / 2;
-
- if (s.min == 0)
- hgt += halfWid;
-
- if (s.max == 0) {
- top -= halfWid;
- hgt += halfWid;
- }
-
- ctx.beginPath();
- ctx.rect(lft, top, wid, hgt);
- ctx.clip();
-
- if (clip != null)
- ctx.clip(clip);
-
- if (s.band) {
- ctx.fill(stroke);
- width && ctx.stroke(stroke);
- }
- else {
- width && ctx.stroke(stroke);
-
- if (s.fill != null)
- ctx.fill(fill);
- }
-
- ctx.restore();
-
- ctx.translate(-offset, -offset);
-
- ctx.globalAlpha = 1;
- }
-
- if (s.band)
- dir *= -1;
- }
-
- function buildClip(is, gaps) {
- let s = series[is];
- let toSpan = new Set(s.spanGaps(self, gaps, is));
- gaps = gaps.filter(g => !toSpan.has(g));
-
- let clip = null;
-
- // create clip path (invert gaps and non-gaps)
- if (gaps.length > 0) {
- clip = new Path2D();
-
- let prevGapEnd = plotLft;
-
- for (let i = 0; i < gaps.length; i++) {
- let g = gaps[i];
-
- clip.rect(prevGapEnd, plotTop, g[0] - prevGapEnd, plotTop + plotHgt);
-
- prevGapEnd = g[1];
- }
-
- clip.rect(prevGapEnd, plotTop, plotLft + plotWid - prevGapEnd, plotTop + plotHgt);
- }
-
- return clip;
- }
-
- function buildPaths(self, is, _i0, _i1) {
- const s = series[is];
-
- const xdata = data[0];
- const ydata = data[is];
- const scaleX = scales[xScaleKey];
- const scaleY = scales[s.scale];
-
- const _paths = dir == 1 ? {stroke: new Path2D(), fill: null, clip: null} : series[is-1]._paths;
- const stroke = _paths.stroke;
- const width = round3(s[WIDTH] * pxRatio);
-
- let minY = inf,
- maxY = -inf,
- outY, outX;
-
- // todo: don't build gaps on dir = -1 pass
- let gaps = [];
-
- let accX = round(getXPos(xdata[dir == 1 ? _i0 : _i1], scaleX, plotWid, plotLft));
-
- // the moves the shape edge outside the canvas so stroke doesnt bleed in
- if (s.band && dir == 1 && _i0 == i0) {
- if (width)
- stroke.lineTo(-width, round(getYPos(ydata[_i0], scaleY, plotHgt, plotTop)));
-
- if (scaleX.min < xdata[0])
- gaps.push([plotLft, accX - 1]);
- }
-
- for (let i = dir == 1 ? _i0 : _i1; i >= _i0 && i <= _i1; i += dir) {
- let x = round(getXPos(xdata[i], scaleX, plotWid, plotLft));
-
- if (x == accX) {
- if (ydata[i] != null) {
- outY = round(getYPos(ydata[i], scaleY, plotHgt, plotTop));
- minY = min(outY, minY);
- maxY = max(outY, maxY);
- }
- }
- else {
- let addGap = false;
-
- if (minY != inf) {
- stroke.lineTo(accX, minY);
- stroke.lineTo(accX, maxY);
- stroke.lineTo(accX, outY);
- outX = accX;
- }
- else
- addGap = true;
-
- if (ydata[i] != null) {
- outY = round(getYPos(ydata[i], scaleY, plotHgt, plotTop));
- stroke.lineTo(x, outY);
- minY = maxY = outY;
-
- // prior pixel can have data but still start a gap if ends with null
- if (x - accX > 1 && ydata[i-1] == null)
- addGap = true;
- }
- else {
- minY = inf;
- maxY = -inf;
- }
-
- if (addGap) {
- let prevGap = gaps[gaps.length - 1];
-
- if (prevGap && prevGap[0] == outX) // TODO: gaps must be encoded at stroke widths?
- prevGap[1] = x;
- else
- gaps.push([outX, x]);
- }
-
- accX = x;
- }
- }
-
- if (s.band) {
- let overShoot = width * 100, _iy, _x;
-
- // the moves the shape edge outside the canvas so stroke doesnt bleed in
- if (dir == -1 && _i0 == i0) {
- _x = plotLft - overShoot;
- _iy = _i0;
- }
-
- if (dir == 1 && _i1 == i1) {
- _x = plotLft + plotWid + overShoot;
- _iy = _i1;
-
- if (scaleX.max > xdata[dataLen - 1])
- gaps.push([accX, plotLft + plotWid]);
- }
-
- stroke.lineTo(_x, round(getYPos(ydata[_iy], scaleY, plotHgt, plotTop)));
- }
-
- if (dir == 1) {
- _paths.clip = buildClip(is, gaps);
-
- if (s.fill != null) {
- let fill = _paths.fill = new Path2D(stroke);
-
- let zeroY = round(getYPos(0, scaleY, plotHgt, plotTop));
- fill.lineTo(plotLft + plotWid, zeroY);
- fill.lineTo(plotLft, zeroY);
- }
- }
-
- if (s.band)
- dir *= -1;
-
- return _paths;
- }
-
- function getIncrSpace(axis, min, max, fullDim) {
- let incrSpace;
-
- if (fullDim <= 0)
- incrSpace = [0, 0];
- else {
- let minSpace = axis.space(self, min, max, fullDim);
- let incrs = axis.incrs(self, min, max, fullDim, minSpace);
- incrSpace = findIncr(max - min, incrs, fullDim, minSpace);
- incrSpace.push(incrSpace[1]/minSpace);
- }
-
- return incrSpace;
- }
-
- function drawOrthoLines(offs, ori, side, pos0, len, width, stroke, dash) {
- let offset = (width % 2) / 2;
-
- ctx.translate(offset, offset);
-
- setCtxStyle(stroke, width, dash);
-
- ctx.beginPath();
-
- let x0, y0, x1, y1, pos1 = pos0 + (side == 0 || side == 3 ? -len : len);
-
- if (ori == 0) {
- y0 = pos0;
- y1 = pos1;
- }
- else {
- x0 = pos0;
- x1 = pos1;
- }
-
- offs.forEach((off, i) => {
- if (ori == 0)
- x0 = x1 = off;
- else
- y0 = y1 = off;
-
- ctx.moveTo(x0, y0);
- ctx.lineTo(x1, y1);
- });
-
- ctx.stroke();
-
- ctx.translate(-offset, -offset);
- }
-
- function drawAxesGrid() {
- axes.forEach((axis, i) => {
- if (!axis.show)
- return;
-
- let scale = scales[axis.scale];
-
- // this will happen if all series using a specific scale are toggled off
- if (scale.min == inf)
- return;
-
- let side = axis.side;
- let ori = side % 2;
-
- let {min, max} = scale;
-
- let [incr, space, pctSpace] = getIncrSpace(axis, min, max, ori == 0 ? plotWidCss : plotHgtCss);
-
- // if we're using index positions, force first tick to match passed index
- let forceMin = scale.distr == 2;
-
- let splits = axis.split(self, min, max, incr, pctSpace, forceMin);
-
- let getPos = ori == 0 ? getXPos : getYPos;
- let plotDim = ori == 0 ? plotWid : plotHgt;
- let plotOff = ori == 0 ? plotLft : plotTop;
-
- let canOffs = splits.map(val => round(getPos(val, scale, plotDim, plotOff)));
-
- let axisGap = round(axis.gap * pxRatio);
-
- let ticks = axis.ticks;
- let tickSize = ticks.show ? round(ticks.size * pxRatio) : 0;
-
- // tick labels
- // BOO this assumes a specific data/series
- let values = axis.values(
- self,
- scale.distr == 2 ? splits.map(i => data0[i]) : splits,
- space,
- scale.distr == 2 ? data0[splits[1]] - data0[splits[0]] : incr,
- );
-
- // rotating of labels only supported on bottom x axis
- let angle = side == 2 ? axis.rotate(self, values, space) * -PI/180 : 0;
-
- let basePos = round(axis._pos * pxRatio);
- let shiftAmt = tickSize + axisGap;
- let shiftDir = ori == 0 && side == 0 || ori == 1 && side == 3 ? -1 : 1;
- let finalPos = basePos + shiftAmt * shiftDir;
- let y = ori == 0 ? finalPos : 0;
- let x = ori == 1 ? finalPos : 0;
-
- ctx.font = axis.font[0];
- ctx.fillStyle = axis.stroke || hexBlack; // rgba?
- ctx.textAlign = angle > 0 ? LEFT :
- angle < 0 ? RIGHT :
- ori == 0 ? "center" : side == 3 ? RIGHT : LEFT;
- ctx.textBaseline = angle ||
- ori == 1 ? "middle" : side == 2 ? TOP : BOTTOM;
-
- let lineHeight = axis.font[1] * lineMult;
-
- values.forEach((val, i) => {
- if (ori == 0)
- x = canOffs[i];
- else
- y = canOffs[i];
-
- (""+val).split(/\n/gm).forEach((text, j) => {
- if (angle) {
- ctx.save();
- ctx.translate(x, y + j * lineHeight);
- ctx.rotate(angle);
- ctx.fillText(text, 0, 0);
- ctx.restore();
- }
- else
- ctx.fillText(text, x, y + j * lineHeight);
- });
- });
-
- // axis label
- if (axis.label) {
- ctx.save();
-
- let baseLpos = round(axis._lpos * pxRatio);
-
- if (ori == 1) {
- x = y = 0;
-
- ctx.translate(
- baseLpos,
- round(plotTop + plotHgt / 2),
- );
- ctx.rotate((side == 3 ? -PI : PI) / 2);
-
- }
- else {
- x = round(plotLft + plotWid / 2);
- y = baseLpos;
- }
-
- ctx.font = axis.labelFont[0];
- // ctx.fillStyle = axis.labelStroke || hexBlack; // rgba?
- ctx.textAlign = "center";
- ctx.textBaseline = side == 2 ? TOP : BOTTOM;
-
- ctx.fillText(axis.label, x, y);
-
- ctx.restore();
- }
-
- // ticks
- if (ticks.show) {
- drawOrthoLines(
- canOffs,
- ori,
- side,
- basePos,
- tickSize,
- round3(ticks[WIDTH] * pxRatio),
- ticks.stroke,
- );
- }
-
- // grid
- let grid = axis.grid;
-
- if (grid.show) {
- drawOrthoLines(
- canOffs,
- ori,
- ori == 0 ? 2 : 1,
- ori == 0 ? plotTop : plotLft,
- ori == 0 ? plotHgt : plotWid,
- round3(grid[WIDTH] * pxRatio),
- grid.stroke,
- grid.dash,
- );
- }
- });
-
- fire("drawAxes");
- }
-
- function resetYSeries() {
- // log("resetYSeries()", arguments);
-
- series.forEach((s, i) => {
- if (i > 0) {
- s.min = inf;
- s.max = -inf;
- s._paths = null;
- }
- });
- }
-
- let didPaint;
-
- function paint() {
- if (inBatch) {
- shouldPaint = true;
- return;
- }
-
- // log("paint()", arguments);
-
- ctx.clearRect(0, 0, can[WIDTH], can[HEIGHT]);
- fire("drawClear");
- drawAxesGrid();
- drawSeries();
- didPaint = true;
- fire("draw");
- }
-
- self.redraw = rebuildPaths => {
- if (rebuildPaths !== false)
- _setScale(xScaleKey, scales[xScaleKey].min, scales[xScaleKey].max);
- else
- paint();
- };
-
- // redraw() => setScale('x', scales.x.min, scales.x.max);
-
- // explicit, never re-ranged (is this actually true? for x and y)
- function setScale(key, opts) {
- let sc = scales[key];
-
- if (sc.from == null) {
- if (key == xScaleKey) {
- if (sc.distr == 2) {
- opts.min = closestIdx(opts.min, data[0]);
- opts.max = closestIdx(opts.max, data[0]);
- }
-
- // prevent setting a temporal x scale too small since Date objects cannot advance ticks smaller than 1ms
- if ( sc.time && axes[0].show && opts.max > opts.min) {
- // since scales and axes are loosly coupled, we have to make some assumptions here :(
- let incr = getIncrSpace(axes[0], opts.min, opts.max, plotWidCss)[0];
-
- if (incr < 1e-3)
- return;
- }
- }
-
- // log("setScale()", arguments);
-
- pendScales[key] = opts;
-
- didPaint = false;
- setScales();
- !didPaint && paint();
- didPaint = false;
- }
- }
-
- self.setScale = setScale;
-
-// INTERACTION
-
- let vt;
- let hz;
-
- // starting position
- let mouseLeft0;
- let mouseTop0;
-
- // current position
- let mouseLeft1;
- let mouseTop1;
-
- let dragging = false;
-
- const drag = cursor.drag;
-
- if ( cursor.show) {
- let c = "cursor-";
-
- if (cursor.x) {
- mouseLeft1 = cursor.left;
- vt = placeDiv(c + "x", over);
- }
-
- if (cursor.y) {
- mouseTop1 = cursor.top;
- hz = placeDiv(c + "y", over);
- }
- }
-
- const select = self.select = assign({
- show: true,
- left: 0,
- width: 0,
- top: 0,
- height: 0,
- }, opts.select);
-
- const selectDiv = select.show ? placeDiv("select", over) : null;
-
- function setSelect(opts, _fire) {
- if (select.show) {
- for (let prop in opts)
- setStylePx(selectDiv, prop, select[prop] = opts[prop]);
-
- _fire !== false && fire("setSelect");
- }
- }
-
- self.setSelect = setSelect;
-
- function toggleDOM(i, onOff) {
- let s = series[i];
- let label = showLegend ? legendRows[i][0].parentNode : null;
-
- if (s.show)
- label && remClass(label, "off");
- else {
- label && addClass(label, "off");
- cursorPts.length > 1 && trans(cursorPts[i], 0, -10);
- }
- }
-
- function _setScale(key, min, max) {
- setScale(key, {min, max});
- }
-
- function setSeries(i, opts, pub) {
- // log("setSeries()", arguments);
-
- let s = series[i];
-
- // batch(() => {
- // will this cause redundant paint() if both show and focus are set?
- if (opts.focus != null)
- setFocus(i);
-
- if (opts.show != null) {
- s.show = opts.show;
- toggleDOM(i, opts.show);
-
- if (s.band) {
- // not super robust, will break if two bands are adjacent
- let ip = series[i+1] && series[i+1].band ? i+1 : i-1;
- series[ip].show = s.show;
- toggleDOM(ip, opts.show);
- }
-
- _setScale(xScaleKey, scales[xScaleKey].min, scales[xScaleKey].max); // redraw
- }
- // });
-
- // firing setSeries after setScale seems out of order, but provides access to the updated props
- // could improve by predefining firing order and building a queue
- fire("setSeries", i, opts);
-
- pub && sync.pub("setSeries", self, i, opts);
- }
-
- self.setSeries = setSeries;
-
- function _alpha(i, value) {
- series[i].alpha = value;
-
- if ( legendRows)
- legendRows[i][0].parentNode.style.opacity = value;
- }
-
- function _setAlpha(i, value) {
- let s = series[i];
-
- _alpha(i, value);
-
- if (s.band) {
- // not super robust, will break if two bands are adjacent
- let ip = series[i+1].band ? i+1 : i-1;
- _alpha(ip, value);
- }
- }
-
- // y-distance
- const distsToCursor = Array(series.length);
-
- let focused = null;
-
- function setFocus(i) {
- if (i != focused) {
- // log("setFocus()", arguments);
-
- series.forEach((s, i2) => {
- _setAlpha(i2, i == null || i2 == 0 || i2 == i ? 1 : focus.alpha);
- });
-
- focused = i;
- paint();
- }
- }
-
- if (showLegend && cursorFocus) {
- on(mouseleave, legendEl, e => {
- if (cursor.locked)
- return;
- setSeries(null, {focus: false}, syncOpts.setSeries);
- updateCursor();
- });
- }
-
- function scaleValueAtPos(pos, scale) {
- let dim = scale == xScaleKey ? plotWidCss : plotHgtCss;
- let pct = clamp(pos / dim, 0, 1);
-
- let sc = scales[scale];
- let d = sc.max - sc.min;
- return sc.min + pct * d;
- }
-
- function closestIdxFromXpos(pos) {
- let v = scaleValueAtPos(pos, xScaleKey);
- return closestIdx(v, data[0], i0, i1);
- }
-
- self.valToIdx = val => closestIdx(val, data[0]);
- self.posToIdx = closestIdxFromXpos;
- self.posToVal = (pos, scale) => scaleValueAtPos(scale == xScaleKey ? pos : plotHgtCss - pos, scale);
- self.valToPos = (val, scale, can) => (
- scale == xScaleKey ?
- getXPos(val, scales[scale],
- can ? plotWid : plotWidCss,
- can ? plotLft : 0,
- ) :
- getYPos(val, scales[scale],
- can ? plotHgt : plotHgtCss,
- can ? plotTop : 0,
- )
- );
-
- let inBatch = false;
- let shouldPaint = false;
- let shouldSetScales = false;
- let shouldUpdateCursor = false;
-
- // defers calling expensive functions
- function batch(fn) {
- inBatch = true;
- fn(self);
- inBatch = false;
- shouldSetScales && setScales();
- shouldUpdateCursor && updateCursor();
- shouldPaint && !didPaint && paint();
- shouldSetScales = shouldUpdateCursor = shouldPaint = didPaint = inBatch;
- }
-
- self.batch = batch;
-
- (self.setCursor = opts => {
- mouseLeft1 = opts.left;
- mouseTop1 = opts.top;
- // assign(cursor, opts);
- updateCursor();
- });
-
- let cursorRaf = 0;
-
- function updateCursor(ts) {
- if (inBatch) {
- shouldUpdateCursor = true;
- return;
- }
-
- // ts == null && log("updateCursor()", arguments);
-
- cursorRaf = 0;
-
- if (cursor.show) {
- cursor.x && trans(vt,round(mouseLeft1),0);
- cursor.y && trans(hz,0,round(mouseTop1));
- }
-
- let idx;
-
- // if cursor hidden, hide points & clear legend vals
- if (mouseLeft1 < 0 || dataLen == 0) {
- idx = null;
-
- for (let i = 0; i < series.length; i++) {
- if (i > 0) {
- distsToCursor[i] = inf;
- cursorPts.length > 1 && trans(cursorPts[i], -10, -10);
- }
-
- if (showLegend) {
- if (i == 0 && multiValLegend)
- continue;
-
- for (let j = 0; j < legendRows[i].length; j++)
- legendRows[i][j][firstChild].nodeValue = '--';
- }
- }
-
- if (cursorFocus)
- setSeries(null, {focus: true}, syncOpts.setSeries);
- }
- else {
- // let pctY = 1 - (y / rect[HEIGHT]);
-
- idx = closestIdxFromXpos(mouseLeft1);
-
- let scX = scales[xScaleKey];
-
- let xPos = round3(getXPos(data[0][idx], scX, plotWidCss, 0));
-
- for (let i = 0; i < series.length; i++) {
- let s = series[i];
-
- if (i > 0 && s.show) {
- let valAtIdx = data[i][idx];
-
- let yPos = valAtIdx == null ? -10 : round3(getYPos(valAtIdx, scales[s.scale], plotHgtCss, 0));
-
- distsToCursor[i] = yPos > 0 ? abs(yPos - mouseTop1) : inf;
-
- cursorPts.length > 1 && trans(cursorPts[i], xPos, yPos);
- }
- else
- distsToCursor[i] = inf;
-
- if (showLegend) {
- if (i == 0 && multiValLegend)
- continue;
-
- let src = i == 0 && xScaleDistr == 2 ? data0 : data[i];
-
- let vals = multiValLegend ? s.values(self, i, idx) : {_: s.value(self, src[idx], i, idx)};
-
- let j = 0;
-
- for (let k in vals)
- legendRows[i][j++][firstChild].nodeValue = vals[k];
- }
- }
- }
-
- // nit: cursor.drag.setSelect is assumed always true
- if (mouseLeft1 >= 0 && select.show && dragging) {
- // setSelect should not be triggered on move events
- if (drag.x) {
- let minX = min(mouseLeft0, mouseLeft1);
- let maxX = max(mouseLeft0, mouseLeft1);
- setStylePx(selectDiv, LEFT, select[LEFT] = minX);
- setStylePx(selectDiv, WIDTH, select[WIDTH] = maxX - minX);
- }
-
- if (drag.y) {
- let minY = min(mouseTop0, mouseTop1);
- let maxY = max(mouseTop0, mouseTop1);
- setStylePx(selectDiv, TOP, select[TOP] = minY);
- setStylePx(selectDiv, HEIGHT, select[HEIGHT] = maxY - minY);
- }
- }
-
- // if ts is present, means we're implicitly syncing own cursor as a result of debounced rAF
- if (ts != null) {
- // this is not technically a "mousemove" event, since it's debounced, rename to setCursor?
- // since this is internal, we can tweak it later
- sync.pub(mousemove, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, idx);
-
- if (cursorFocus) {
- let minDist = min.apply(null, distsToCursor);
-
- let fi = null;
-
- if (minDist <= focus.prox) {
- distsToCursor.some((dist, i) => {
- if (dist == minDist)
- return fi = i;
- });
- }
-
- setSeries(fi, {focus: true}, syncOpts.setSeries);
- }
- }
-
- cursor.idx = idx;
- cursor.left = mouseLeft1;
- cursor.top = mouseTop1;
-
- ready && fire("setCursor");
- }
-
- let rect = null;
-
- function syncRect() {
- rect = over.getBoundingClientRect();
- }
-
- function mouseMove(e, src, _x, _y, _w, _h, _i) {
- if (cursor.locked)
- return;
-
- cacheMouse(e, src, _x, _y, _w, _h, _i, false, e != null);
-
- if (e != null) {
- if (cursorRaf == 0)
- cursorRaf = rAF(updateCursor);
- }
- else
- updateCursor();
- }
-
- function cacheMouse(e, src, _x, _y, _w, _h, _i, initial, snap) {
- if (e != null) {
- _x = e.clientX - rect.left;
- _y = e.clientY - rect.top;
- }
- else {
- _x = plotWidCss * (_x/_w);
- _y = plotHgtCss * (_y/_h);
- }
-
- if (snap) {
- if (_x <= 1 || _x >= plotWidCss - 1)
- _x = incrRound(_x, plotWidCss);
-
- if (_y <= 1 || _y >= plotHgtCss - 1)
- _y = incrRound(_y, plotHgtCss);
- }
-
- if (initial) {
- mouseLeft0 = _x;
- mouseTop0 = _y;
- }
- else {
- mouseLeft1 = _x;
- mouseTop1 = _y;
- }
- }
-
- function hideSelect() {
- setSelect({
- width: !drag.x ? plotWidCss : 0,
- height: !drag.y ? plotHgtCss : 0,
- }, false);
- }
-
- function mouseDown(e, src, _x, _y, _w, _h, _i) {
- if (e == null || filtMouse(e)) {
- dragging = true;
-
- cacheMouse(e, src, _x, _y, _w, _h, _i, true, true);
-
- if (select.show && (drag.x || drag.y))
- hideSelect();
-
- if (e != null) {
- on(mouseup, doc, mouseUp);
- sync.pub(mousedown, self, mouseLeft0, mouseTop0, plotWidCss, plotHgtCss, null);
- }
- }
- }
-
- function mouseUp(e, src, _x, _y, _w, _h, _i) {
- if ((e == null || filtMouse(e))) {
- dragging = false;
-
- cacheMouse(e, src, _x, _y, _w, _h, _i, false, true);
-
- if (mouseLeft1 != mouseLeft0 || mouseTop1 != mouseTop0) {
- setSelect(select);
-
- if (drag.setScale) {
- batch(() => {
- if (drag.x) {
- _setScale(xScaleKey,
- scaleValueAtPos(select[LEFT], xScaleKey),
- scaleValueAtPos(select[LEFT] + select[WIDTH], xScaleKey),
- );
- }
-
- if (drag.y) {
- for (let k in scales) {
- let sc = scales[k];
-
- if (k != xScaleKey && sc.from == null) {
- _setScale(k,
- scaleValueAtPos(plotHgtCss - select[TOP] - select[HEIGHT], k),
- scaleValueAtPos(plotHgtCss - select[TOP], k),
- );
- }
- }
- }
- });
-
- hideSelect();
- }
- }
- else if (cursor.lock) {
- cursor.locked = !cursor.locked;
-
- if (!cursor.locked)
- updateCursor();
- }
-
- if (e != null) {
- off(mouseup, doc, mouseUp);
- sync.pub(mouseup, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null);
- }
- }
- }
-
- function mouseLeave(e, src, _x, _y, _w, _h, _i) {
- if (!cursor.locked && !dragging) {
- mouseLeft1 = -10;
- mouseTop1 = -10;
- // passing a non-null timestamp to force sync/mousemove event
- updateCursor(1);
- }
- }
-
- function dblClick(e, src, _x, _y, _w, _h, _i) {
- autoScaleX();
-
- if (e != null)
- sync.pub(dblclick, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null);
- }
-
- // internal pub/sub
- const events = {};
-
- events[mousedown] = mouseDown;
- events[mousemove] = mouseMove;
- events[mouseup] = mouseUp;
- events[dblclick] = dblClick;
- events["setSeries"] = (e, src, idx, opts) => {
- setSeries(idx, opts);
- };
-
- let deb;
-
- if ( cursor.show) {
- on(mousedown, over, mouseDown);
- on(mousemove, over, mouseMove);
- on(mouseenter, over, syncRect);
- on(mouseleave, over, mouseLeave);
- drag.setScale && on(dblclick, over, dblClick);
-
- deb = debounce(syncRect, 100);
-
- on(resize, win, deb);
- on(scroll, win, deb);
-
- self.syncRect = syncRect;
- }
-
- // external on/off
- const hooks = self.hooks = opts.hooks || {};
-
- function fire(evName, a1, a2) {
- if (evName in hooks) {
- hooks[evName].forEach(fn => {
- fn.call(null, self, a1, a2);
- });
- }
- }
-
- (opts.plugins || []).forEach(p => {
- for (let evName in p.hooks)
- hooks[evName] = (hooks[evName] || []).concat(p.hooks[evName]);
- });
-
- const syncOpts = assign({
- key: null,
- setSeries: false,
- }, cursor.sync);
-
- const syncKey = syncOpts.key;
-
- const sync = (syncKey != null ? (syncs[syncKey] = syncs[syncKey] || _sync()) : _sync());
-
- sync.sub(self);
-
- function pub(type, src, x, y, w, h, i) {
- events[type](null, src, x, y, w, h, i);
- }
-
- (self.pub = pub);
-
- function destroy() {
- sync.unsub(self);
- off(resize, win, deb);
- off(scroll, win, deb);
- root.remove();
- fire("destroy");
- }
-
- self.destroy = destroy;
-
- function _init() {
- _setSize(opts[WIDTH], opts[HEIGHT]);
-
- fire("init", opts, data);
-
- setData(data || opts.data, false);
-
- if (pendScales[xScaleKey])
- setScale(xScaleKey, pendScales[xScaleKey]);
- else
- autoScaleX();
-
- setSelect(select, false);
-
- ready = true;
-
- fire("ready");
- }
-
- if (then) {
- if (then instanceof HTMLElement) {
- then.appendChild(root);
- _init();
- }
- else
- then(self, _init);
- }
- else
- _init();
-
- return self;
-}
-
-uPlot.assign = assign;
-uPlot.rangeNum = rangeNum;
-
-{
- uPlot.fmtDate = fmtDate;
- uPlot.tzDate = tzDate;
-}
-
-export default uPlot;
diff --git a/assets/lib/uPlot.min.css b/assets/lib/uPlot.min.css
deleted file mode 100644
index 82b70b5..0000000
--- a/assets/lib/uPlot.min.css
+++ /dev/null
@@ -1 +0,0 @@
-.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: max-content;}.uplot .title {text-align: center;font-size: 18px;font-weight: bold;}.uplot .wrap {position: relative;user-select: none;}.uplot .over, .uplot .under {position: absolute;overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.uplot .legend {font-size: 14px;margin: auto;text-align: center;}.uplot .legend.inline {display: block;}.uplot .legend.inline * {display: inline-block;}.uplot .legend.inline tr {margin-right: 16px;}.uplot .legend th {font-weight: 600;}.uplot .legend th > * {vertical-align: middle;display: inline-block;}.uplot .legend .ident {width: 1em;height: 1em;margin-right: 4px;border: 2px solid transparent;}.uplot .legend.inline th::after {content: ":";vertical-align: middle;}.uplot .legend .series > * {padding: 4px;}.uplot .legend .series th {cursor: pointer;}.uplot .legend .off > * {opacity: 0.3;}.uplot .select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.uplot .select.off {display: none;}.uplot .cursor-x, .uplot .cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;z-index: 100;}.uplot .cursor-x {height: 100%;border-right: 1px dashed #607D8B;}.uplot .cursor-y {width: 100%;border-bottom: 1px dashed #607D8B;}.uplot .cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;filter: brightness(85%);pointer-events: none;will-change: transform;z-index: 100;}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index b5c4568..93a463a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8519,6 +8519,11 @@
"integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==",
"dev": true
},
+ "uplot": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.0.8.tgz",
+ "integrity": "sha512-oS0YVdq6iEU4B+BXSX1Ln3Dd8iVHk9vKL9elWlIEa7cYzlhqDmnnJQsXSaLjYWTQbnDLRJuuaO3oyGF2q7loiw=="
+ },
"uri-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
diff --git a/package.json b/package.json
index 69a1bb4..6a0dabb 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"request": "2.88.2",
"serve-static": "^1.14.1",
"sqlite3": "4.1.1",
+ "uplot": "^1.0.8",
"winston": "^2.0.0",
"ws": "^7.2.5"
},