Skip to content

Instantly share code, notes, and snippets.

@nielsuit227
Last active December 3, 2021 17:33
Show Gist options
  • Save nielsuit227/c6b1678a79c8501b13c60a2c5ecdda4e to your computer and use it in GitHub Desktop.
Save nielsuit227/c6b1678a79c8501b13c60a2c5ecdda4e to your computer and use it in GitHub Desktop.
Time Series Plot (leeoniya/uPlot) with brush and selection option
import React, {useEffect, useRef, useState} from 'react';
import { Switch, Flex, Text } from '@chakra-ui/react';
import UplotReact from 'uplot-react';
import { toast } from 'react-toastify';
import 'uplot/dist/uPlot.min.css';
const colors = [
'#007BFF',
'#FFA62B',
'#236868',
'#64606C',
'#729CE9',
'#ABA9B2',
'#007BFF',
'#FFA62B',
'#236868',
'#64606C',
'#729CE9',
'#ABA9B2',
'#007BFF',
'#FFA62B',
'#236868',
'#64606C',
'#729CE9',
'#ABA9B2',
];
const root = document.querySelector("#root");
export default function SelectBrushTimeSeries(props) {
let uBrush = null;
let uGraph = null;
const doc = document;
function debounce(fn) {
let raf;
return (...args) => {
if (raf)
return;
raf = requestAnimationFrame(() => {
fn(...args);
raf = null;
});
};
}
function placeDiv(par, cls) {
let el = doc.createElement("div");
el.classList.add(cls);
par.appendChild(el);
return el;
}
function on(ev, el, fn) {
el.addEventListener(ev, fn);
}
function off(ev, el, fn) {
el.removeEventListener(ev, fn);
}
//----------------
let x0;
let lft0;
let wid0;
const lftWid = {left: null, width: null};
const minMax = {min: null, max: null};
function update(newLft, newWid) {
let newRgt = newLft + newWid;
let maxRgt = uBrush.bbox.width / devicePixelRatio;
if (newLft >= 0 && newRgt <= maxRgt) {
select(newLft, newWid);
zoom(newLft, newWid);
}
}
function select(newLft, newWid) {
lftWid.left = newLft;
lftWid.width = newWid;
// initXmin = minMax.min;
setXmin(minMax.min);
setXmax(minMax.max);
uBrush.setSelect(lftWid, false);
}
function zoom(newLft, newWid) {
minMax.min = uBrush.posToVal(newLft, 'x');
minMax.max = uBrush.posToVal(newLft + newWid, 'x');
setXmin(minMax.min);
setXmax(minMax.max);
uGraph.setScale('x', minMax);
}
function bindMove(e, onMove) {
x0 = e.clientX;
lft0 = uBrush.select.left;
wid0 = uBrush.select.width;
const _onMove = debounce(onMove);
on("mousemove", doc, _onMove);
const _onUp = e => {
off("mouseup", doc, _onUp);
off("mousemove", doc, _onMove);
// viaGrip = false;
};
on("mouseup", doc, _onUp);
e.stopPropagation();
}
///////////////////////////////////////////////////
// 'state'
const [addMode, setAddMode] = useState(true);
const signals = 10;
// Select X & Y axis
let data = props.data.slice(0, signals + 1);
// Data values
const cols = data.length;
const len = data[0].length;
let yMin = null;
let yMax = null;
let initXmin = useRef(props.data[0][0]);
let initXmax = useRef(props.data[0][Math.floor(len / 10)]);
const setXmin = (v) => initXmin.current = v;
const setXmax = (v) => initXmax.current = v;
// Add sequence
data.push(props.selected);
// Annotation values
let annotation = false;
let annotStartIdx = null;
let annotEndIdx = null;
// Create Series
let series = [
{},
...Array(signals).fill(1).map((_, i) => ({
stroke: colors[i],
label: props.cols[i]
})),
{
fill: '#e53e3e',
spanGaps: false,
alpha: 0.2,
fillTo: yMin,
}];
// Chart Options
const opts = {
width: 1800,
height: 600,
cursor: {
bind: {
mousedown: (u, target, handler) => {
return e => {
if (e.button === 0) {
handler(e);
if (e.shiftKey) {
annotStartIdx = u.valToIdx(u.cursor.sync.values[0]);
annotation = true;
} else {
setXmin(u.cursor.sync.values[0])
}
}
}
},
mouseup: (self, target, handler) => {
return e => {
if (e.button == 0) {
if (annotation) {
annotEndIdx = uGraph.valToIdx(uGraph.cursor.sync.values[0]);
const _setScale = uGraph.cursor.drag.setScale;
const _setScaleR = uBrush.cursor.drag.setScale;
uGraph.cursor.drag.setScale = false;
uBrush.cursor.drag.setScale = false;
handler(e);
uGraph.cursor.drag.setScale = _setScale;
uBrush.cursor.drag.setScale = _setScaleR;
} else {
handler(e);
setXmax(uGraph.cursor.sync.values[0]);
}
}
}
},
dblclick: (self, target, handler) => {
return e => {
if (e.button == 0) {
handler(e);
setXmin(data[0][0]);
setXmax(data[0][0]);
let left = Math.round(uBrush.valToPos(initXmin.current, 'x'));
let width = Math.round(uBrush.valToPos(initXmax.current, 'x')) - left;
let height = uBrush.bbox.height / devicePixelRatio;
uBrush.setSelect({left, width, height}, false);
}
}
}
},
sync: {
key: 'moo',
},
},
hooks: {
ready: [
u => {
uGraph = u;
yMax = Math.max(...data[cols]) === 0 ? u.scales.y.max : Math.max(...data[cols]);
yMin = Math.min(...data[cols]) === 0 ? u.scales.y.min : Math.min(...data[cols]);
},
],
setSelect: [
u => {
if (annotation) {
annotation = false;
if (annotStartIdx === null) toast.error('Annotation start lost.')
else if (annotEndIdx === null) toast.error('Annotation End lost')
else {
// If props.annot = 'add'
let s = Math.min(annotStartIdx, annotEndIdx);
let e = Math.max(annotStartIdx, annotEndIdx) + 1;
if (addMode) {
data[cols] = [...data[cols].slice(0, s), ...Array(e - s).fill(yMax), ...data[cols].slice(e)];
} else {
data[cols] = [...data[cols].slice(0, s), ...Array(e - s).fill(null), ...data[cols].slice(e)];
}
annotStartIdx = null;
annotEndIdx = null;
uGraph.setData(data, false);
uBrush.setData(data, false);
uGraph.redraw(true, false);
uBrush.redraw(true, false);
props.setSelected(data[cols]);
}
} else {
setTimeout(()=> {
let left = Math.round(uBrush.valToPos(initXmin.current, 'x'));
let width = Math.round(uBrush.valToPos(initXmax.current, 'x')) - left;
let height = uBrush.bbox.height / devicePixelRatio;
uBrush.setSelect({left, width, height}, false);
}, 100);
}
}
],
},
scales: {
x: {
time: false,
min: initXmin.current,
max: initXmax.current,
},
},
series: series,
};
const brushOpts = {
width: 1800,
height: 150,
legend: {
show: false
},
cursor: {
y: false,
points: {
show: false,
},
drag: {
setScale: false,
x: true,
y: false,
},
sync: {
key: 'noo',
},
bind: {
mousedown: (u, target, handler) => {
return e => {
if (e.button === 0) {
handler(e);
// initXmin = u.cursor.sync.values[0];
setXmin(u.cursor.sync.values[0]);
}
}
},
mouseup: (u, target, handler) => {
return e => {
if (e.button == 0) {
handler(e);
setXmax(u.cursor.sync.values[0]);
}
}
},
dblclick: (self, target, handler) => {
return e => {
if (e.button == 0) {
handler(e);
setXmin(data[0][0]);
setXmax(data[0][data[0].length - 1]);
uGraph.setScale('x', {min: initXmin.current, max: initXmax.current});
}
}
}
},
},
scales: {
x: {
time: false,
min: data[0][0],
max: data[0][data[0].length-1]
},
},
hooks: {
ready: [
u => {
uBrush = u;
let left = Math.round(uBrush.valToPos(initXmin.current, 'x'));
let width = Math.round(uBrush.valToPos(initXmax.current, 'x')) - left;
let height = uBrush.bbox.height / devicePixelRatio;
uBrush.setSelect({left, width, height}, false);
const sel = uBrush.root.querySelector(".u-select");
on("mousedown", sel, e => {
bindMove(e, e => update(lft0 + (e.clientX - x0), wid0));
});
on("mousedown", placeDiv(sel, "u-grip-l"), e => {
bindMove(e, e => update(lft0 + (e.clientX - x0), wid0 - (e.clientX - x0)));
});
on("mousedown", placeDiv(sel, "u-grip-r"), e => {
bindMove(e, e => update(lft0, wid0 + (e.clientX - x0)));
});
}
],
// setScale: [
// u => uGraph.setScale('y', {min: yMin, max: yMax}),
// ],
setSelect: [
u => {
setTimeout(()=>{
const left = Math.round(uBrush.valToPos(initXmin.current, 'x'));
const width = Math.round(uBrush.valToPos(initXmax.current, 'x')) - left;
zoom(left, width);
}, 100);
}
]
},
series: series,
}
return(
<div>
<Flex flexDir='row' alignContent='center'>
<Text fontSize={18} mx={4}>Delete Sequences</Text>
<Switch size='lg' isChecked={addMode} onChange={()=>setAddMode(!addMode)}/>
</Flex>
<UplotReact options={opts} data={data}/>
<UplotReact options={brushOpts} data={data}/>
</div>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment