fix: sort tooltip value based on value and highlight on hover (#4059)

* fix: sort tooltip value based on value and highlight on hover

* fix: tsc issues
This commit is contained in:
Yunus M 2023-11-27 18:07:15 +05:30 committed by GitHub
parent e18bb7d5bc
commit 97ed163002
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 230 additions and 161 deletions

View File

@ -44,136 +44,142 @@ export const getUPlotChartOptions = ({
setGraphsVisibilityStates, setGraphsVisibilityStates,
thresholds, thresholds,
fillSpans, fillSpans,
}: GetUPlotChartOptions): uPlot.Options => ({ }: GetUPlotChartOptions): uPlot.Options => {
id, // eslint-disable-next-line sonarjs/prefer-immediate-return
width: dimensions.width, const chartOptions = {
height: dimensions.height - 45, id,
// tzDate: (ts) => uPlot.tzDate(new Date(ts * 1e3), ''), // Pass timezone for 2nd param width: dimensions.width,
legend: { height: dimensions.height - 45,
show: true, // tzDate: (ts) => uPlot.tzDate(new Date(ts * 1e3), ''), // Pass timezone for 2nd param
live: false, legend: {
}, show: true,
focus: { live: false,
alpha: 0.3, },
},
cursor: {
focus: { focus: {
prox: 1e6, alpha: 0.3,
bias: 1,
}, },
points: { cursor: {
size: (u, seriesIdx): number => u.series[seriesIdx].points.size * 2.5, lock: false,
width: (u, seriesIdx, size): number => size / 4, focus: {
stroke: (u, seriesIdx): string => prox: 1e6,
`${u.series[seriesIdx].points.stroke(u, seriesIdx)}90`, bias: 1,
fill: (): string => '#fff',
},
},
padding: [16, 16, 16, 16],
scales: {
x: {
time: true,
auto: true, // Automatically adjust scale range
},
y: {
auto: true,
},
},
plugins: [
tooltipPlugin(apiResponse, yAxisUnit, fillSpans),
onClickPlugin({
onClick: onClickHandler,
}),
],
hooks: {
draw: [
(u): void => {
thresholds?.forEach((threshold) => {
if (threshold.thresholdValue !== undefined) {
const { ctx } = u;
ctx.save();
const yPos = u.valToPos(
convertValue(
threshold.thresholdValue,
threshold.thresholdUnit,
yAxisUnit,
),
'y',
true,
);
ctx.strokeStyle = threshold.thresholdColor || 'red';
ctx.lineWidth = 2;
ctx.setLineDash([10, 5]);
ctx.beginPath();
const plotLeft = u.bbox.left; // left edge of the plot area
const plotRight = plotLeft + u.bbox.width; // right edge of the plot area
ctx.moveTo(plotLeft, yPos);
ctx.lineTo(plotRight, yPos);
ctx.stroke();
// Text configuration
if (threshold.thresholdLabel) {
const text = threshold.thresholdLabel;
const textX = plotRight - ctx.measureText(text).width - 20;
const textY = yPos - 15;
ctx.fillStyle = threshold.thresholdColor || 'red';
ctx.fillText(text, textX, textY);
}
ctx.restore();
}
});
}, },
], points: {
setSelect: [ size: (u, seriesIdx): number => u.series[seriesIdx].points.size * 2.5,
(self): void => { width: (u, seriesIdx, size): number => size / 4,
const selection = self.select; stroke: (u, seriesIdx): string =>
if (selection) { `${u.series[seriesIdx].points.stroke(u, seriesIdx)}90`,
const startTime = self.posToVal(selection.left, 'x'); fill: (): string => '#fff',
const endTime = self.posToVal(selection.left + selection.width, 'x');
const diff = endTime - startTime;
if (typeof onDragSelect === 'function' && diff > 0) {
onDragSelect(startTime * 1000, endTime * 1000);
}
}
}, },
},
padding: [16, 16, 16, 16],
scales: {
x: {
time: true,
auto: true, // Automatically adjust scale range
},
y: {
auto: true,
},
},
plugins: [
tooltipPlugin(apiResponse, yAxisUnit, fillSpans),
onClickPlugin({
onClick: onClickHandler,
}),
], ],
ready: [ hooks: {
(self): void => { draw: [
const legend = self.root.querySelector('.u-legend'); (u): void => {
if (legend) { thresholds?.forEach((threshold) => {
const seriesEls = legend.querySelectorAll('.u-label'); if (threshold.thresholdValue !== undefined) {
const seriesArray = Array.from(seriesEls); const { ctx } = u;
seriesArray.forEach((seriesEl, index) => { ctx.save();
seriesEl.addEventListener('click', () => {
if (graphsVisibilityStates) { const yPos = u.valToPos(
setGraphsVisibilityStates?.((prev) => { convertValue(
const newGraphVisibilityStates = [...prev]; threshold.thresholdValue,
newGraphVisibilityStates[index + 1] = !newGraphVisibilityStates[ threshold.thresholdUnit,
index + 1 yAxisUnit,
]; ),
return newGraphVisibilityStates; 'y',
}); true,
);
ctx.strokeStyle = threshold.thresholdColor || 'red';
ctx.lineWidth = 2;
ctx.setLineDash([10, 5]);
ctx.beginPath();
const plotLeft = u.bbox.left; // left edge of the plot area
const plotRight = plotLeft + u.bbox.width; // right edge of the plot area
ctx.moveTo(plotLeft, yPos);
ctx.lineTo(plotRight, yPos);
ctx.stroke();
// Text configuration
if (threshold.thresholdLabel) {
const text = threshold.thresholdLabel;
const textX = plotRight - ctx.measureText(text).width - 20;
const textY = yPos - 15;
ctx.fillStyle = threshold.thresholdColor || 'red';
ctx.fillText(text, textX, textY);
} }
});
ctx.restore();
}
}); });
} },
}, ],
], setSelect: [
}, (self): void => {
series: getSeries( const selection = self.select;
apiResponse, if (selection) {
apiResponse?.data.result, const startTime = self.posToVal(selection.left, 'x');
graphsVisibilityStates, const endTime = self.posToVal(selection.left + selection.width, 'x');
fillSpans,
), const diff = endTime - startTime;
axes: getAxes(isDarkMode, yAxisUnit),
}); if (typeof onDragSelect === 'function' && diff > 0) {
onDragSelect(startTime * 1000, endTime * 1000);
}
}
},
],
ready: [
(self): void => {
const legend = self.root.querySelector('.u-legend');
if (legend) {
const seriesEls = legend.querySelectorAll('.u-label');
const seriesArray = Array.from(seriesEls);
seriesArray.forEach((seriesEl, index) => {
seriesEl.addEventListener('click', () => {
if (graphsVisibilityStates) {
setGraphsVisibilityStates?.((prev) => {
const newGraphVisibilityStates = [...prev];
newGraphVisibilityStates[index + 1] = !newGraphVisibilityStates[
index + 1
];
return newGraphVisibilityStates;
});
}
});
});
}
},
],
},
series: getSeries(
apiResponse,
apiResponse?.data.result,
graphsVisibilityStates,
fillSpans,
),
axes: getAxes(isDarkMode, yAxisUnit),
};
return chartOptions;
};

View File

@ -9,7 +9,17 @@ import { placement } from '../placement';
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat);
const createDivsFromArray = ( interface UplotTooltipDataProps {
show: boolean;
color: string;
label: string;
focus: boolean;
value: string | number;
tooltipValue: string;
textContent: string;
}
const generateTooltipContent = (
seriesList: any[], seriesList: any[],
data: any[], data: any[],
idx: number, idx: number,
@ -21,30 +31,14 @@ const createDivsFromArray = (
const container = document.createElement('div'); const container = document.createElement('div');
container.classList.add('tooltip-container'); container.classList.add('tooltip-container');
let tooltipTitle = '';
const formattedData: Record<string, UplotTooltipDataProps> = {};
if (Array.isArray(series) && series.length > 0) { if (Array.isArray(series) && series.length > 0) {
series.forEach((item, index) => { series.forEach((item, index) => {
const div = document.createElement('div');
div.classList.add('tooltip-content-row');
if (index === 0) { if (index === 0) {
const formattedDate = dayjs(data[0][idx] * 1000).format( tooltipTitle = dayjs(data[0][idx] * 1000).format('MMM DD YYYY HH:mm:ss');
'MMM DD YYYY HH:mm:ss',
);
div.textContent = formattedDate;
div.classList.add('tooltip-content-header');
} else if (fillSpans ? item.show : item.show && data[index][idx]) { } else if (fillSpans ? item.show : item.show && data[index][idx]) {
div.classList.add('tooltip-content');
const color = colors[(index - 1) % colors.length];
const squareBox = document.createElement('div');
squareBox.classList.add('pointSquare');
squareBox.style.borderColor = color;
const text = document.createElement('div');
text.classList.add('tooltip-data-point');
const { metric = {}, queryName = '', legend = '' } = const { metric = {}, queryName = '', legend = '' } =
seriesList[index - 1] || {}; seriesList[index - 1] || {};
@ -55,20 +49,80 @@ const createDivsFromArray = (
); );
const value = data[index][idx] || 0; const value = data[index][idx] || 0;
const tooltipValue = getToolTipValue(value, yAxisUnit); const tooltipValue = getToolTipValue(value, yAxisUnit);
text.textContent = `${label} : ${tooltipValue || 0}`; const dataObj = {
show: item.show || false,
color: colors[(index - 1) % colors.length],
label,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
focus: item?._focus || false,
value,
tooltipValue,
textContent: `${label} : ${tooltipValue || 0}`,
};
formattedData[value] = dataObj;
}
});
}
// Get the keys and sort them
const sortedKeys = Object.keys(formattedData).sort(
(a, b) => parseInt(b, 10) - parseInt(a, 10),
);
// Create a new object with sorted keys
const sortedData: Record<string, UplotTooltipDataProps> = {};
sortedKeys.forEach((key) => {
sortedData[key] = formattedData[key];
});
const div = document.createElement('div');
div.classList.add('tooltip-content-row');
div.textContent = tooltipTitle;
div.classList.add('tooltip-content-header');
container.appendChild(div);
if (Array.isArray(sortedKeys) && sortedKeys.length > 0) {
sortedKeys.forEach((key) => {
if (sortedData[key]) {
const { textContent, color, focus } = sortedData[key];
const div = document.createElement('div');
div.classList.add('tooltip-content-row');
div.classList.add('tooltip-content');
const squareBox = document.createElement('div');
squareBox.classList.add('pointSquare');
squareBox.style.borderColor = color;
const text = document.createElement('div');
text.classList.add('tooltip-data-point');
text.textContent = textContent;
text.style.color = color; text.style.color = color;
if (focus) {
text.classList.add('focus');
} else {
text.classList.remove('focus');
}
div.appendChild(squareBox); div.appendChild(squareBox);
div.appendChild(text); div.appendChild(text);
}
container.appendChild(div); container.appendChild(div);
}
}); });
} }
const overlay = document.getElementById('overlay');
if (overlay && overlay.style.display === 'none') {
overlay.style.display = 'block';
}
return container; return container;
}; };
@ -127,10 +181,9 @@ const tooltipPlugin = (
if (overlay) { if (overlay) {
overlay.textContent = ''; overlay.textContent = '';
const { left, top, idx } = u.cursor; const { left, top, idx } = u.cursor;
if (idx) { if (idx) {
const anchor = { left: left + bLeft, top: top + bTop }; const anchor = { left: left + bLeft, top: top + bTop };
const content = createDivsFromArray( const content = generateTooltipContent(
apiResult, apiResult,
u.data, u.data,
idx, idx,

View File

@ -14,6 +14,10 @@
.tooltip-data-point { .tooltip-data-point {
font-size: 11px; font-size: 11px;
flex: 1;
overflow: hidden;
word-wrap: break-word;
} }
.tooltip-content { .tooltip-content {
@ -24,6 +28,12 @@
.pointSquare, .pointSquare,
.tooltip-data-point { .tooltip-data-point {
font-size: 13px !important; font-size: 12px !important;
opacity: 0.9;
&.focus {
opacity: 1;
font-weight: 700;
}
} }
} }

View File

@ -51,23 +51,23 @@ body {
} }
#overlay { #overlay {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, font-family: 'Inter';
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
font-size: 12px; font-size: 12px;
position: absolute; position: absolute;
margin: 0.5rem; margin: 0.5rem;
background: rgba(0, 0, 0, 0.9); background: rgba(0, 0, 0);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
color: #fff; color: #fff;
z-index: 10000; z-index: 10000;
pointer-events: none; // pointer-events: none;
overflow: auto; overflow: auto;
max-height: 600px !important; max-height: 480px !important;
max-width: 240px !important;
border-radius: 5px; border-radius: 5px;
border: 1px solid rgba(255, 255, 255, 0.1);
.tooltip-container { .tooltip-container {
padding: 0.5rem; padding: 1rem;
} }
&::-webkit-scrollbar { &::-webkit-scrollbar {