diff --git a/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx b/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx index 77b29c779854..4e48296ed514 100644 --- a/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx +++ b/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx @@ -138,6 +138,7 @@ function UplotPanelWrapper({ timezone: timezone.value, customSeries, isLogScale: widget?.isLogScale, + enhancedLegend: true, // Enable enhanced legend }), [ widget?.id, diff --git a/frontend/src/container/PanelWrapper/enhancedLegend.ts b/frontend/src/container/PanelWrapper/enhancedLegend.ts new file mode 100644 index 000000000000..c33a376719c4 --- /dev/null +++ b/frontend/src/container/PanelWrapper/enhancedLegend.ts @@ -0,0 +1,123 @@ +import { Dimensions } from 'hooks/useDimensions'; + +export interface EnhancedLegendConfig { + minHeight: number; + maxHeight: number; + calculatedHeight: number; + showScrollbar: boolean; + requiredRows: number; +} + +/** + * Calculate legend configuration based on panel dimensions and series count + * Prioritizes chart space while ensuring legend usability + */ +export function calculateEnhancedLegendConfig( + dimensions: Dimensions, + seriesCount: number, + seriesLabels?: string[], +): EnhancedLegendConfig { + const lineHeight = 34; + const padding = 12; + const maxRowsToShow = 2; // Reduced from 3 to 2 for better chart/legend ratio + + // Legend should not take more than 15% of panel height, with absolute max of 80px + const maxLegendRatio = 0.15; + const absoluteMaxHeight = Math.min(80, dimensions.height * maxLegendRatio); + + const baseItemWidth = 44; + const avgCharWidth = 8; + + let avgTextLength = 15; + if (seriesLabels && seriesLabels.length > 0) { + const totalLength = seriesLabels.reduce( + (sum, label) => sum + Math.min(label.length, 30), + 0, + ); + avgTextLength = Math.max(8, Math.min(25, totalLength / seriesLabels.length)); + } + + // Estimate item width based on actual or estimated text length + let estimatedItemWidth = baseItemWidth + avgCharWidth * avgTextLength; + + // For very wide panels, allow longer text + if (dimensions.width > 800) { + estimatedItemWidth = Math.max( + estimatedItemWidth, + baseItemWidth + avgCharWidth * 22, + ); + } else if (dimensions.width < 400) { + estimatedItemWidth = Math.min( + estimatedItemWidth, + baseItemWidth + avgCharWidth * 14, + ); + } + + // Calculate items per row based on available width + const availableWidth = dimensions.width - padding * 2; + const itemsPerRow = Math.max( + 1, + Math.floor(availableWidth / estimatedItemWidth), + ); + const requiredRows = Math.ceil(seriesCount / itemsPerRow); + + // Calculate heights + const idealHeight = requiredRows * lineHeight + padding; + + // For single row, use minimal height + let minHeight; + if (requiredRows <= 1) { + minHeight = lineHeight + padding; // Single row + } else { + // Multiple rows: show 2 rows max, then scroll + minHeight = Math.min(2 * lineHeight + padding, idealHeight); + } + + // Maximum height constraint - prioritize chart space + const maxHeight = Math.min( + maxRowsToShow * lineHeight + padding, + absoluteMaxHeight, + ); + + const calculatedHeight = Math.max(minHeight, Math.min(idealHeight, maxHeight)); + const showScrollbar = idealHeight > calculatedHeight; + + return { + minHeight, + maxHeight, + calculatedHeight, + showScrollbar, + requiredRows, + }; +} + +/** + * Apply enhanced legend styling to a legend element + */ +export function applyEnhancedLegendStyling( + legend: HTMLElement, + config: EnhancedLegendConfig, + requiredRows: number, +): void { + const legendElement = legend; + legendElement.classList.add('u-legend-enhanced'); + legendElement.style.height = `${config.calculatedHeight}px`; + legendElement.style.minHeight = `${config.minHeight}px`; + legendElement.style.maxHeight = `${config.maxHeight}px`; + + // Apply alignment based on number of rows + if (requiredRows === 1) { + legendElement.classList.add('u-legend-single-line'); + legendElement.classList.remove('u-legend-multi-line'); + } else { + legendElement.classList.add('u-legend-multi-line'); + legendElement.classList.remove('u-legend-single-line'); + } + + // Add scrollbar indicator if needed + if (config.showScrollbar) { + legendElement.classList.add('u-legend-scrollable'); + } else { + legendElement.classList.remove('u-legend-scrollable'); + } +} diff --git a/frontend/src/lib/uPlotLib/getUplotChartOptions.ts b/frontend/src/lib/uPlotLib/getUplotChartOptions.ts index 9c29e29d5bca..9c4ed06d932b 100644 --- a/frontend/src/lib/uPlotLib/getUplotChartOptions.ts +++ b/frontend/src/lib/uPlotLib/getUplotChartOptions.ts @@ -8,8 +8,13 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; import { FullViewProps } from 'container/GridCardLayout/GridCard/FullView/types'; import { saveLegendEntriesToLocalStorage } from 'container/GridCardLayout/GridCard/FullView/utils'; import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types'; +import { + applyEnhancedLegendStyling, + calculateEnhancedLegendConfig, +} from 'container/PanelWrapper/enhancedLegend'; import { Dimensions } from 'hooks/useDimensions'; import { convertValue } from 'lib/getConvertedValue'; +import getLabelName from 'lib/getLabelName'; import { cloneDeep, isUndefined } from 'lodash-es'; import _noop from 'lodash-es/noop'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; @@ -60,6 +65,7 @@ export interface GetUPlotChartOptions { customSeries?: (data: QueryData[]) => uPlot.Series[]; isLogScale?: boolean; colorMapping?: Record; + enhancedLegend?: boolean; } /** the function converts series A , series B , series C to @@ -168,6 +174,7 @@ export const getUPlotChartOptions = ({ customSeries, isLogScale, colorMapping, + enhancedLegend = true, }: GetUPlotChartOptions): uPlot.Options => { const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale); @@ -180,10 +187,27 @@ export const getUPlotChartOptions = ({ const bands = stackBarChart ? getBands(series) : null; + // Calculate dynamic legend height based on panel dimensions and series count + const seriesCount = (apiResponse?.data?.result || []).length; + const seriesLabels = enhancedLegend + ? (apiResponse?.data?.result || []).map((item) => + getLabelName(item.metric || {}, item.queryName || '', item.legend || ''), + ) + : []; + const legendConfig = enhancedLegend + ? calculateEnhancedLegendConfig(dimensions, seriesCount, seriesLabels) + : { + calculatedHeight: 30, + minHeight: 30, + maxHeight: 30, + itemsPerRow: 3, + showScrollbar: false, + }; + return { id, width: dimensions.width, - height: dimensions.height - 30, + height: dimensions.height - legendConfig.calculatedHeight - 10, // Adjust chart height for enhanced legend legend: { show: true, live: false, @@ -335,6 +359,15 @@ export const getUPlotChartOptions = ({ (self): void => { const legend = self.root.querySelector('.u-legend'); if (legend) { + // Apply enhanced legend styling + if (enhancedLegend) { + applyEnhancedLegendStyling( + legend as HTMLElement, + legendConfig, + legendConfig.requiredRows, + ); + } + const seriesEls = legend.querySelectorAll('.u-series'); const seriesArray = Array.from(seriesEls); seriesArray.forEach((seriesEl, index) => { diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 0f574942cfaf..43db326af29b 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -17,12 +17,12 @@ body { } .u-legend { - max-height: 30px; // slicing the height of the widget Header height ; + max-height: 30px; // Default height for backward compatibility overflow-y: auto; overflow-x: hidden; &::-webkit-scrollbar { - width: 0.3rem; + width: 0.5rem; } &::-webkit-scrollbar-corner { background: transparent; @@ -53,6 +53,99 @@ body { text-decoration-thickness: 3px; } } + + // Enhanced legend styles + &.u-legend-enhanced { + max-height: none; // Remove default max-height restriction + padding: 6px 4px; // Back to original padding + + // Thin and neat scrollbar for enhanced legend + &::-webkit-scrollbar { + width: 0.25rem; + height: 0.25rem; + } + &::-webkit-scrollbar-thumb { + background: rgba(136, 136, 136, 0.4); + border-radius: 0.125rem; + + &:hover { + background: rgba(136, 136, 136, 0.7); + } + } + &::-webkit-scrollbar-track { + background: transparent; + } + + // Enhanced table layout for better responsiveness + table { + width: 100%; + table-layout: fixed; + } + + tbody { + display: flex; + flex-wrap: wrap; + gap: 2px 2px; + align-items: center; + justify-content: flex-start; + width: 100%; + } + + // Center alignment for single-line legends + &.u-legend-single-line tbody { + justify-content: center; + } + + tr.u-series { + display: flex; + flex: 0 0 auto; + min-width: fit-content; + + th { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 400; + padding: 6px 10px; + cursor: pointer; + white-space: nowrap; + -webkit-font-smoothing: antialiased; + border-radius: 2px; + + &:hover { + background: rgba(255, 255, 255, 0.05); + } + + .u-marker { + border-radius: 50%; + min-width: 10px; + min-height: 10px; + width: 10px; + height: 10px; + flex-shrink: 0; + } + } + + &.u-off { + opacity: 0.5; + text-decoration: line-through; + text-decoration-thickness: 1px; + + th { + &:hover { + opacity: 0.7; + } + } + } + + // Focus styles for keyboard navigation + &:focus { + outline: 1px solid rgba(66, 165, 245, 0.8); + outline-offset: 1px; + } + } + } } /* Style the selected background */ @@ -250,6 +343,39 @@ body { } } } + + // Enhanced legend light mode styles + .u-legend-enhanced { + // Light mode scrollbar styling + &::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); + + &:hover { + background: rgba(0, 0, 0, 0.4); + } + } + + tr.u-series { + th { + &:hover { + background: rgba(0, 0, 0, 0.05); + } + } + + &.u-off { + th { + &:hover { + background: rgba(0, 0, 0, 0.08); + } + } + } + + // Light mode focus styles + &:focus { + outline: 1px solid rgba(25, 118, 210, 0.8); + } + } + } } .ant-notification-notice-message {