mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-23 02:17:11 +00:00
247 lines
7.5 KiB
TypeScript
247 lines
7.5 KiB
TypeScript
|
|
import { Dimensions } from 'hooks/useDimensions';
|
||
|
|
import { LegendPosition } from 'types/api/dashboard/getAll';
|
||
|
|
|
||
|
|
export interface EnhancedLegendConfig {
|
||
|
|
minHeight: number;
|
||
|
|
maxHeight: number;
|
||
|
|
calculatedHeight: number;
|
||
|
|
showScrollbar: boolean;
|
||
|
|
requiredRows: number;
|
||
|
|
// For right-side legend
|
||
|
|
minWidth?: number;
|
||
|
|
maxWidth?: number;
|
||
|
|
calculatedWidth?: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Calculate legend configuration based on panel dimensions and series count
|
||
|
|
* Prioritizes chart space while ensuring legend usability
|
||
|
|
*/
|
||
|
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||
|
|
export function calculateEnhancedLegendConfig(
|
||
|
|
dimensions: Dimensions,
|
||
|
|
seriesCount: number,
|
||
|
|
seriesLabels?: string[],
|
||
|
|
legendPosition: LegendPosition = LegendPosition.BOTTOM,
|
||
|
|
): EnhancedLegendConfig {
|
||
|
|
const lineHeight = 34;
|
||
|
|
const padding = 12;
|
||
|
|
const maxRowsToShow = 2; // Reduced from 3 to 2 for better chart/legend ratio
|
||
|
|
|
||
|
|
// Different configurations for bottom vs right positioning
|
||
|
|
if (legendPosition === LegendPosition.RIGHT) {
|
||
|
|
// Right-side legend configuration
|
||
|
|
const maxLegendWidthRatio = 0.3; // Legend should not take more than 30% of panel width
|
||
|
|
const absoluteMaxWidth = Math.min(
|
||
|
|
400,
|
||
|
|
dimensions.width * maxLegendWidthRatio,
|
||
|
|
);
|
||
|
|
const minWidth = 150;
|
||
|
|
|
||
|
|
// For right-side legend, calculate based on text length
|
||
|
|
const avgCharWidth = 8;
|
||
|
|
let avgTextLength = 15;
|
||
|
|
if (seriesLabels && seriesLabels.length > 0) {
|
||
|
|
const totalLength = seriesLabels.reduce(
|
||
|
|
(sum, label) => sum + Math.min(label.length, 40),
|
||
|
|
0,
|
||
|
|
);
|
||
|
|
avgTextLength = Math.max(
|
||
|
|
10,
|
||
|
|
Math.min(35, totalLength / seriesLabels.length),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fix: Ensure width respects the ratio constraint even if it's less than minWidth
|
||
|
|
const estimatedWidth = 80 + avgCharWidth * avgTextLength;
|
||
|
|
const calculatedWidth = Math.min(
|
||
|
|
Math.max(minWidth, estimatedWidth),
|
||
|
|
absoluteMaxWidth,
|
||
|
|
);
|
||
|
|
|
||
|
|
// For right-side legend, height can be more flexible
|
||
|
|
const maxHeight = dimensions.height - 40; // Leave some padding
|
||
|
|
const idealHeight = seriesCount * lineHeight + padding;
|
||
|
|
const calculatedHeight = Math.min(idealHeight, maxHeight);
|
||
|
|
const showScrollbar = idealHeight > calculatedHeight;
|
||
|
|
|
||
|
|
return {
|
||
|
|
minHeight: lineHeight + padding,
|
||
|
|
maxHeight,
|
||
|
|
calculatedHeight,
|
||
|
|
showScrollbar,
|
||
|
|
requiredRows: seriesCount, // Each series on its own row for right-side
|
||
|
|
minWidth,
|
||
|
|
maxWidth: absoluteMaxWidth,
|
||
|
|
calculatedWidth,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Bottom legend configuration (existing logic)
|
||
|
|
const maxLegendRatio = 0.15;
|
||
|
|
// Fix: For very small dimensions, respect the ratio instead of using fixed 80px minimum
|
||
|
|
const ratioBasedMaxHeight = dimensions.height * maxLegendRatio;
|
||
|
|
|
||
|
|
// Handle edge cases and calculate absolute max height
|
||
|
|
let absoluteMaxHeight;
|
||
|
|
if (dimensions.height <= 0) {
|
||
|
|
absoluteMaxHeight = 46; // Fallback for invalid dimensions
|
||
|
|
} else if (dimensions.height <= 400) {
|
||
|
|
// For small to medium panels, prioritize ratio constraint
|
||
|
|
absoluteMaxHeight = Math.min(80, Math.max(15, ratioBasedMaxHeight));
|
||
|
|
} else {
|
||
|
|
// For larger panels, maintain a reasonable minimum
|
||
|
|
absoluteMaxHeight = Math.min(80, Math.max(20, ratioBasedMaxHeight));
|
||
|
|
}
|
||
|
|
|
||
|
|
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),
|
||
|
|
);
|
||
|
|
let requiredRows = Math.ceil(seriesCount / itemsPerRow);
|
||
|
|
|
||
|
|
if (requiredRows === 1 && seriesCount > 3) {
|
||
|
|
requiredRows = 2;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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);
|
||
|
|
}
|
||
|
|
|
||
|
|
// For very small dimensions, allow the minHeight to be smaller to respect ratio constraints
|
||
|
|
if (dimensions.height < 200) {
|
||
|
|
minHeight = Math.min(minHeight, absoluteMaxHeight);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Maximum height constraint - prioritize chart space
|
||
|
|
// Fix: Ensure we respect the ratio-based constraint for small dimensions
|
||
|
|
const rowBasedMaxHeight = maxRowsToShow * lineHeight + padding;
|
||
|
|
const maxHeight = Math.min(rowBasedMaxHeight, absoluteMaxHeight);
|
||
|
|
|
||
|
|
const calculatedHeight = Math.max(minHeight, Math.min(idealHeight, maxHeight));
|
||
|
|
const showScrollbar = idealHeight > calculatedHeight;
|
||
|
|
|
||
|
|
return {
|
||
|
|
minHeight,
|
||
|
|
maxHeight,
|
||
|
|
calculatedHeight,
|
||
|
|
showScrollbar,
|
||
|
|
requiredRows,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// CSS class constants
|
||
|
|
const LEGEND_SINGLE_LINE_CLASS = 'u-legend-single-line';
|
||
|
|
const LEGEND_MULTI_LINE_CLASS = 'u-legend-multi-line';
|
||
|
|
const LEGEND_RIGHT_ALIGNED_CLASS = 'u-legend-right-aligned';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Apply enhanced legend styling to a legend element
|
||
|
|
*/
|
||
|
|
export function applyEnhancedLegendStyling(
|
||
|
|
legend: HTMLElement,
|
||
|
|
config: EnhancedLegendConfig,
|
||
|
|
requiredRows: number,
|
||
|
|
legendPosition: LegendPosition = LegendPosition.BOTTOM,
|
||
|
|
): void {
|
||
|
|
const legendElement = legend;
|
||
|
|
legendElement.classList.add('u-legend-enhanced');
|
||
|
|
|
||
|
|
// Apply position-specific styling
|
||
|
|
if (legendPosition === LegendPosition.RIGHT) {
|
||
|
|
legendElement.classList.add('u-legend-right');
|
||
|
|
legendElement.classList.remove('u-legend-bottom');
|
||
|
|
|
||
|
|
// Set width for right-side legend
|
||
|
|
if (config.calculatedWidth) {
|
||
|
|
legendElement.style.width = `${config.calculatedWidth}px`;
|
||
|
|
legendElement.style.minWidth = `${config.minWidth}px`;
|
||
|
|
legendElement.style.maxWidth = `${config.maxWidth}px`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Height for right-side legend
|
||
|
|
legendElement.style.height = `${config.calculatedHeight}px`;
|
||
|
|
legendElement.style.minHeight = `${config.minHeight}px`;
|
||
|
|
legendElement.style.maxHeight = `${config.maxHeight}px`;
|
||
|
|
} else {
|
||
|
|
legendElement.classList.add('u-legend-bottom');
|
||
|
|
legendElement.classList.remove('u-legend-right');
|
||
|
|
|
||
|
|
// Height for bottom legend
|
||
|
|
legendElement.style.height = `${config.calculatedHeight}px`;
|
||
|
|
legendElement.style.minHeight = `${config.minHeight}px`;
|
||
|
|
legendElement.style.maxHeight = `${config.maxHeight}px`;
|
||
|
|
|
||
|
|
// Reset width for bottom legend
|
||
|
|
legendElement.style.width = '';
|
||
|
|
legendElement.style.minWidth = '';
|
||
|
|
legendElement.style.maxWidth = '';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Apply alignment based on position and number of rows
|
||
|
|
if (legendPosition === LegendPosition.RIGHT) {
|
||
|
|
legendElement.classList.add(LEGEND_RIGHT_ALIGNED_CLASS);
|
||
|
|
legendElement.classList.remove(
|
||
|
|
LEGEND_SINGLE_LINE_CLASS,
|
||
|
|
LEGEND_MULTI_LINE_CLASS,
|
||
|
|
);
|
||
|
|
} else if (requiredRows === 1) {
|
||
|
|
legendElement.classList.add(LEGEND_SINGLE_LINE_CLASS);
|
||
|
|
legendElement.classList.remove(
|
||
|
|
LEGEND_MULTI_LINE_CLASS,
|
||
|
|
LEGEND_RIGHT_ALIGNED_CLASS,
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
legendElement.classList.add(LEGEND_MULTI_LINE_CLASS);
|
||
|
|
legendElement.classList.remove(
|
||
|
|
LEGEND_SINGLE_LINE_CLASS,
|
||
|
|
LEGEND_RIGHT_ALIGNED_CLASS,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add scrollbar indicator if needed
|
||
|
|
if (config.showScrollbar) {
|
||
|
|
legendElement.classList.add('u-legend-scrollable');
|
||
|
|
} else {
|
||
|
|
legendElement.classList.remove('u-legend-scrollable');
|
||
|
|
}
|
||
|
|
}
|