import React, { useRef, useMemo } from 'react';
import UplotReact from 'uplot-react';
import { useResizeDetector } from 'react-resize-detector';
import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
import { useTheme } from '@material-ui/styles';
function tooltipPlugin(refreshRate) {
let tooltipTopOffset = -20;
let tooltipLeftOffset = 10;
function showTooltip() {
if(!window.uplotTooltip) {
window.uplotTooltip = document.createElement('div');
window.uplotTooltip.className = 'uplot-tooltip';
document.body.appendChild(window.uplotTooltip);
}
}
function hideTooltip() {
window.uplotTooltip?.remove();
window.uplotTooltip = null;
}
function setTooltip(u) {
if(u.cursor.top <= 0) {
hideTooltip();
return;
}
if(u.legend?.values?.slice(1).every((v)=>v['_']=='')) {
return;
}
showTooltip();
let tooltipHtml=`
${(u.data[1].length-1-parseInt(u.legend.values[0]['_'])) * refreshRate + gettext(' seconds ago')}
`;
for(let i=1; i ${u.series[i].label}: ${u.legend.values[i]['_']}`;
}
window.uplotTooltip.innerHTML = tooltipHtml;
let overBBox = u.over.getBoundingClientRect();
let tooltipBBox = window.uplotTooltip.getBoundingClientRect();
let left = (tooltipLeftOffset + u.cursor.left + overBBox.left);
/* Should not outside the graph right */
if((left+tooltipBBox.width) > overBBox.right) {
left = left - tooltipBBox.width - tooltipLeftOffset*2;
}
window.uplotTooltip.style.left = left + 'px';
window.uplotTooltip.style.top = (tooltipTopOffset + u.cursor.top + overBBox.top) + 'px';
}
return {
hooks: {
setCursor: [
u => {
setTooltip(u);
}
],
}
};
}
export default function StreamingChart({xRange=75, data, options, valueFormatter, showSecondAxis=false}) {
const chartRef = useRef();
const theme = useTheme();
const { width, height, ref:containerRef } = useResizeDetector();
const defaultOptions = useMemo(()=> {
const series = [
{},
...(data.datasets?.map((datum, index) => ({
label: datum.label,
stroke: datum.borderColor,
value: valueFormatter ? (_u, t)=>valueFormatter(t) : undefined,
width: options.lineBorderWidth ?? 1,
scale: showSecondAxis && (index === 1) ? 'y1' : 'y',
points: { show: options.showDataPoints ?? false, size: datum.pointHitRadius * 2 },
})) ?? []),
];
const axes = [
{
show: false,
stroke: theme.palette.text.primary,
},
];
const yAxesValues = (self, values) => {
if(valueFormatter && values) {
return values.map((value) => {
return valueFormatter(value);
});
}
return values ?? [];
};
// ref: https://raw.githubusercontent.com/leeoniya/uPlot/master/demos/axis-autosize.html
const yAxesSize = (self, values, axisIdx, cycleNum) => {
let axis = self.axes[axisIdx];
// bail out, force convergence
if (cycleNum > 1)
return axis._size;
let axisSize = axis.ticks.size + axis.gap + 8;
// find longest value
let longestVal = (values ?? []).reduce((acc, val) => (
val.length > acc.length ? val : acc
), '');
if (longestVal != '') {
self.ctx.font = axis.font[0];
axisSize += self.ctx.measureText(longestVal).width / devicePixelRatio;
}
return Math.ceil(axisSize);
};
axes.push({
scale: 'y',
grid: {
stroke: theme.otherVars.borderColor,
width: 0.5,
},
stroke: theme.palette.text.primary,
size: yAxesSize,
values: valueFormatter ? yAxesValues : undefined,
});
if(showSecondAxis){
axes.push({
scale: 'y1',
side: 1,
stroke: theme.palette.text.primary,
grid: {show: false},
size: yAxesSize,
values: valueFormatter ? yAxesValues : undefined,
});
}
return {
title: '',
width: width,
height: height,
padding: [10, 0, 10, 0],
focus: {
alpha: 0.3,
},
cursor: {
y: false,
drag: {
setScale: false,
}
},
series: series,
scales: {
x: {
time: false,
auto: false,
range: [0, xRange-1],
}
},
axes: axes,
plugins: options.showTooltip ? [tooltipPlugin(data.refreshRate)] : [],
};
}, [data.refreshRate, data?.datasets?.length, width, height, options]);
const initialState = [
Array.from(new Array(xRange).keys()),
...(data.datasets?.map((d)=>{
let ret = new Array(xRange).fill(null);
ret.splice(0, d.data.length, ...d.data);
ret.reverse();
return ret;
})??{}),
];
return (
{
chartRef.current=obj;
}} resetScales={false} />
);
}
const propTypeData = PropTypes.shape({
datasets: PropTypes.array,
refreshRate: PropTypes.number.isRequired,
});
StreamingChart.propTypes = {
xRange: PropTypes.number.isRequired,
data: propTypeData.isRequired,
options: PropTypes.object,
showSecondAxis: PropTypes.bool,
valueFormatter: PropTypes.func,
};