mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-16 04:25:54 +08:00
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:
parent
e18bb7d5bc
commit
97ed163002
@ -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;
|
||||||
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user