From 97ed163002048a043f5788192b0b8ee13db0e79d Mon Sep 17 00:00:00 2001 From: Yunus M Date: Mon, 27 Nov 2023 18:07:15 +0530 Subject: [PATCH] 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 --- .../src/lib/uPlotLib/getUplotChartOptions.ts | 258 +++++++++--------- .../src/lib/uPlotLib/plugins/tooltipPlugin.ts | 107 ++++++-- .../src/lib/uPlotLib/uPlotLib.styles.scss | 12 +- frontend/src/styles.scss | 14 +- 4 files changed, 230 insertions(+), 161 deletions(-) diff --git a/frontend/src/lib/uPlotLib/getUplotChartOptions.ts b/frontend/src/lib/uPlotLib/getUplotChartOptions.ts index 98b0f14f89..6cb07299bb 100644 --- a/frontend/src/lib/uPlotLib/getUplotChartOptions.ts +++ b/frontend/src/lib/uPlotLib/getUplotChartOptions.ts @@ -44,136 +44,142 @@ export const getUPlotChartOptions = ({ setGraphsVisibilityStates, thresholds, fillSpans, -}: GetUPlotChartOptions): uPlot.Options => ({ - id, - width: dimensions.width, - height: dimensions.height - 45, - // tzDate: (ts) => uPlot.tzDate(new Date(ts * 1e3), ''), // Pass timezone for 2nd param - legend: { - show: true, - live: false, - }, - focus: { - alpha: 0.3, - }, - cursor: { +}: GetUPlotChartOptions): uPlot.Options => { + // eslint-disable-next-line sonarjs/prefer-immediate-return + const chartOptions = { + id, + width: dimensions.width, + height: dimensions.height - 45, + // tzDate: (ts) => uPlot.tzDate(new Date(ts * 1e3), ''), // Pass timezone for 2nd param + legend: { + show: true, + live: false, + }, focus: { - prox: 1e6, - bias: 1, + alpha: 0.3, }, - points: { - size: (u, seriesIdx): number => u.series[seriesIdx].points.size * 2.5, - width: (u, seriesIdx, size): number => size / 4, - stroke: (u, seriesIdx): string => - `${u.series[seriesIdx].points.stroke(u, seriesIdx)}90`, - 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(); - } - }); + cursor: { + lock: false, + focus: { + prox: 1e6, + bias: 1, }, - ], - setSelect: [ - (self): void => { - const selection = self.select; - if (selection) { - const startTime = self.posToVal(selection.left, 'x'); - const endTime = self.posToVal(selection.left + selection.width, 'x'); - - const diff = endTime - startTime; - - if (typeof onDragSelect === 'function' && diff > 0) { - onDragSelect(startTime * 1000, endTime * 1000); - } - } + points: { + size: (u, seriesIdx): number => u.series[seriesIdx].points.size * 2.5, + width: (u, seriesIdx, size): number => size / 4, + stroke: (u, seriesIdx): string => + `${u.series[seriesIdx].points.stroke(u, seriesIdx)}90`, + 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, + }), ], - 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; - }); + 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(); + } }); - } - }, - ], - }, - series: getSeries( - apiResponse, - apiResponse?.data.result, - graphsVisibilityStates, - fillSpans, - ), - axes: getAxes(isDarkMode, yAxisUnit), -}); + }, + ], + setSelect: [ + (self): void => { + const selection = self.select; + if (selection) { + const startTime = self.posToVal(selection.left, 'x'); + const endTime = self.posToVal(selection.left + selection.width, 'x'); + + const diff = endTime - startTime; + + 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; +}; diff --git a/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts b/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts index 2840b0ffc2..f9d5fe9d6a 100644 --- a/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts +++ b/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts @@ -9,7 +9,17 @@ import { placement } from '../placement'; 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[], data: any[], idx: number, @@ -21,30 +31,14 @@ const createDivsFromArray = ( const container = document.createElement('div'); container.classList.add('tooltip-container'); + let tooltipTitle = ''; + const formattedData: Record = {}; + if (Array.isArray(series) && series.length > 0) { series.forEach((item, index) => { - const div = document.createElement('div'); - div.classList.add('tooltip-content-row'); - if (index === 0) { - const formattedDate = dayjs(data[0][idx] * 1000).format( - 'MMM DD YYYY HH:mm:ss', - ); - - div.textContent = formattedDate; - div.classList.add('tooltip-content-header'); + tooltipTitle = dayjs(data[0][idx] * 1000).format('MMM DD YYYY HH:mm:ss'); } 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 = '' } = seriesList[index - 1] || {}; @@ -55,20 +49,80 @@ const createDivsFromArray = ( ); const value = data[index][idx] || 0; - 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 = {}; + 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; + if (focus) { + text.classList.add('focus'); + } else { + text.classList.remove('focus'); + } + div.appendChild(squareBox); 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; }; @@ -127,10 +181,9 @@ const tooltipPlugin = ( if (overlay) { overlay.textContent = ''; const { left, top, idx } = u.cursor; - if (idx) { const anchor = { left: left + bLeft, top: top + bTop }; - const content = createDivsFromArray( + const content = generateTooltipContent( apiResult, u.data, idx, diff --git a/frontend/src/lib/uPlotLib/uPlotLib.styles.scss b/frontend/src/lib/uPlotLib/uPlotLib.styles.scss index 42bb247772..538754f5d8 100644 --- a/frontend/src/lib/uPlotLib/uPlotLib.styles.scss +++ b/frontend/src/lib/uPlotLib/uPlotLib.styles.scss @@ -14,6 +14,10 @@ .tooltip-data-point { font-size: 11px; + + flex: 1; + overflow: hidden; + word-wrap: break-word; } .tooltip-content { @@ -24,6 +28,12 @@ .pointSquare, .tooltip-data-point { - font-size: 13px !important; + font-size: 12px !important; + opacity: 0.9; + + &.focus { + opacity: 1; + font-weight: 700; + } } } diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 10525a1f02..8efda895d4 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -51,23 +51,23 @@ body { } #overlay { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, - 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', - 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + font-family: 'Inter'; font-size: 12px; position: absolute; margin: 0.5rem; - background: rgba(0, 0, 0, 0.9); + background: rgba(0, 0, 0); -webkit-font-smoothing: antialiased; color: #fff; z-index: 10000; - pointer-events: none; + // pointer-events: none; overflow: auto; - max-height: 600px !important; + max-height: 480px !important; + max-width: 240px !important; border-radius: 5px; + border: 1px solid rgba(255, 255, 255, 0.1); .tooltip-container { - padding: 0.5rem; + padding: 1rem; } &::-webkit-scrollbar {