import { ResponsiveContainer } from 'recharts'; import styled, { useTheme } from 'styled-components'; import { spacing, Stack, Wrap } from '../../../spacing'; import { Box } from '../../box/Box'; import { IconHelp } from '../../iconhelper/IconHelper'; import { Loader } from '../../loader/Loader.component'; import { Text } from '../../text/Text.component'; import { ConstrainedText } from '../../constrainedtext/Constrainedtext.component'; import { FormattedDateTime } from '../../date/FormattedDateTime'; import { formatXAxisDate, maxWidthTooltip, splitTickLines } from './chartUtils'; import { TimeType, CategoryType } from '../types'; /** * Styled ResponsiveContainer for charts * Shared by Barchart and LineTimeSerieChart * Ensures tooltip overflow is visible and removes outline */ export const StyledResponsiveContainer = styled(ResponsiveContainer)` // Avoid tooltip over constrained text to be cut off & .recharts-surface { outline: none; overflow: visible; } `; // Tick labels are top-anchored (not vertically centered) at the tick, then grow // downward. The first line therefore sits at the same place for every tick — // single- or multi-line — instead of its position depending on the box height. // All lines share one line-height so the first line of a multi-line label is // identical to a single-line label. const TickContainer = styled.div` width: 100%; height: 100%; display: flex; align-items: flex-start; justify-content: center; & span { line-height: 0.9; } `; // Stacks the lines of a multi-line tick label (e.g. time + date on a midnight // crossover); each line stays its own ConstrainedText so it truncates // independently. Line spacing and first-line alignment come from TickContainer. const MultilineTickContainer = styled.div` width: 100%; display: flex; flex-direction: column; align-items: center; `; // Extra height granted per wrapped line so a second line is not clipped. export const TICK_LINE_HEIGHT = 12; // Height of a single-line x-axis tick band (matches the foreignObject base height). export const TICK_BASE_HEIGHT = 30; interface ChartLoadingOrErrorProps { height: number; } /** * Error state component for charts */ export const ChartError = ({ height }: ChartLoadingOrErrorProps) => { return ( Chart data is not available ); }; /** * Loading state component for charts */ export const ChartLoading = ({ height }: ChartLoadingOrErrorProps) => { return ( Loading Chart Data...} /> ); }; interface ChartHeaderProps { title?: string; secondaryTitle?: string; helpTooltip?: React.ReactNode; rightTitle?: React.ReactNode; isLoading?: boolean; } /** * Shared chart header component * Used by Barchart and LineTimeSerieChart */ export const ChartHeader = ({ title, secondaryTitle, helpTooltip, rightTitle, isLoading, }: ChartHeaderProps) => { return ( {title && {title}} {helpTooltip && ( )} {secondaryTitle && ( {secondaryTitle} )} {isLoading && } {rightTitle} ); }; interface CustomTickProps { x: number | string; y: number | string; payload: { // A timestamp for time ticks, or the category label (possibly "\n"-split // into multiple lines) for category ticks. value: number | string; }; visibleTicksCount?: number; width?: number | string; type: TimeType | CategoryType; tickWidthOffset?: number; } /** * Custom tick component for charts * Used by Barchart for time-based x-axis ticks */ export const CustomTick = ({ x, y, payload, visibleTicksCount, width, type, tickWidthOffset = 4, }: CustomTickProps) => { const theme = useTheme(); const numX = typeof x === 'string' ? parseFloat(x) : x; const numY = typeof y === 'string' ? parseFloat(y) : y; const numWidth = typeof width === 'string' ? parseFloat(width) : (width ?? 0); const tickCount = visibleTicksCount ?? 1; const tickWidth = numWidth / tickCount - tickWidthOffset; const centerX = numX - tickWidth / 2; const duration = type.type === 'time' ? (type.timeRange.endDate.getTime() - type.timeRange.startDate.getTime()) / 1000 : 0; const tooltipStyle = { backgroundColor: theme.backgroundLevel1, padding: spacing.r10, borderRadius: spacing.r8, border: `1px solid ${theme.border}`, position: 'absolute' as const, }; const labelLine = (content: React.ReactNode, key?: React.Key) => ( {content}} centered tooltipStyle={tooltipStyle} /> ); // A category label may carry a second line (e.g. a date on a midnight // crossover) separated by "\n"; those lines are stacked tightly. const lines = type.type === 'time' ? [] : splitTickLines(payload.value); const isMultiline = lines.length > 1; const content = type.type === 'time' ? ( labelLine( , ) ) : isMultiline ? ( {lines.map((line, index) => labelLine(line, index))} ) : ( labelLine(String(payload.value)) ); return ( {content} ); };