Last active
December 3, 2021 17:33
-
-
Save nielsuit227/c6b1678a79c8501b13c60a2c5ecdda4e to your computer and use it in GitHub Desktop.
Time Series Plot (leeoniya/uPlot) with brush and selection option
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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