Compare commits

..

1 Commits

Author SHA1 Message Date
Abhi Kumar
7974f4fb08 feat: replaced external apis barchart with the new bar chart 2026-03-01 19:16:45 +05:30
13 changed files with 163 additions and 254 deletions

View File

@@ -3,16 +3,14 @@ import { UseQueryResult } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Button, Card, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import { useGetGraphCustomSeries } from 'components/CeleryTask/useGetGraphCustomSeries';
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
import Uplot from 'components/Uplot';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
getCustomFiltersForBarChart,
getFormattedEndPointStatusCodeChartData,
getStatusCodeBarChartWidgetData,
statusCodeWidgetInfo,
} from 'container/ApiMonitoring/utils';
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
@@ -20,15 +18,16 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { useNotifications } from 'hooks/useNotifications';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useTimezone } from 'providers/Timezone';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { Options } from 'uplot';
import ErrorState from './ErrorState';
import { prepareStatusCodeBarChartsConfig } from './utils';
function StatusCodeBarCharts({
endPointStatusCodeBarChartsDataQuery,
@@ -67,13 +66,6 @@ function StatusCodeBarCharts({
} = endPointStatusCodeLatencyBarChartsDataQuery;
const { startTime: minTime, endTime: maxTime } = timeRange;
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
@@ -119,6 +111,7 @@ function StatusCodeBarCharts({
const navigateToExplorer = useNavigateToExplorer();
const { currentQuery } = useQueryBuilder();
const { timezone } = useTimezone();
const navigateToExplorerPages = useNavigateToExplorerPages();
const { notifications } = useNotifications();
@@ -134,12 +127,6 @@ function StatusCodeBarCharts({
[],
);
const { getCustomSeries } = useGetGraphCustomSeries({
isDarkMode,
drawStyle: 'bars',
colorMapping,
});
const widget = useMemo<Widgets>(
() =>
getStatusCodeBarChartWidgetData(domainName, {
@@ -193,49 +180,36 @@ function StatusCodeBarCharts({
],
);
const options = useMemo(
() =>
getUPlotChartOptions({
apiResponse:
currentWidgetInfoIndex === 0
? formattedEndPointStatusCodeBarChartsDataPayload
: formattedEndPointStatusCodeLatencyBarChartsDataPayload,
isDarkMode,
dimensions,
yAxisUnit: statusCodeWidgetInfo[currentWidgetInfoIndex].yAxisUnit,
softMax: null,
softMin: null,
minTimeScale: minTime,
maxTimeScale: maxTime,
panelType: PANEL_TYPES.BAR,
onClickHandler: graphClickHandler,
customSeries: getCustomSeries,
onDragSelect,
colorMapping,
query: currentQuery,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
}),
[
minTime,
maxTime,
currentWidgetInfoIndex,
dimensions,
formattedEndPointStatusCodeBarChartsDataPayload,
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
const config = useMemo(() => {
const apiResponse =
currentWidgetInfoIndex === 0
? formattedEndPointStatusCodeBarChartsDataPayload
: formattedEndPointStatusCodeLatencyBarChartsDataPayload;
return prepareStatusCodeBarChartsConfig({
timezone,
isDarkMode,
graphClickHandler,
getCustomSeries,
query: currentQuery,
onDragSelect,
onClick: graphClickHandler,
apiResponse,
minTimeScale: minTime,
maxTimeScale: maxTime,
yAxisUnit: statusCodeWidgetInfo[currentWidgetInfoIndex].yAxisUnit,
colorMapping,
currentQuery,
],
);
});
}, [
currentQuery,
isDarkMode,
minTime,
maxTime,
graphClickHandler,
onDragSelect,
formattedEndPointStatusCodeBarChartsDataPayload,
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
timezone,
currentWidgetInfoIndex,
colorMapping,
]);
const renderCardContent = useCallback(
(query: UseQueryResult<SuccessResponse<any>, unknown>): JSX.Element => {
@@ -253,11 +227,20 @@ function StatusCodeBarCharts({
!query.isLoading && !query?.data?.payload?.data?.result?.length,
})}
>
<Uplot options={options as Options} data={chartData} />
<BarChart
config={config}
data={chartData}
width={dimensions.width}
height={dimensions.height}
timezone={timezone}
legendConfig={{
position: LegendPosition.BOTTOM,
}}
/>
</div>
);
},
[options, chartData],
[config, chartData, dimensions, timezone],
);
return (

View File

@@ -0,0 +1,81 @@
import { ExecStats } from 'api/v5/v5';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
import { getLegend } from 'lib/dashboard/getQueryResults';
import getLabelName from 'lib/getLabelName';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { DrawStyle } from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { get } from 'lodash-es';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryData } from 'types/api/widgets/getQuery';
export const prepareStatusCodeBarChartsConfig = ({
timezone,
isDarkMode,
query,
onDragSelect,
onClick,
apiResponse,
minTimeScale,
maxTimeScale,
yAxisUnit,
colorMapping,
}: {
timezone: Timezone;
isDarkMode: boolean;
query: Query;
onDragSelect: (startTime: number, endTime: number) => void;
onClick?: OnClickPluginOpts['onClick'];
minTimeScale?: number;
maxTimeScale?: number;
apiResponse: MetricRangePayloadProps;
yAxisUnit?: string;
colorMapping?: Record<string, string>;
}): UPlotConfigBuilder => {
const stepIntervals: ExecStats['stepIntervals'] = get(
apiResponse,
'data.newResult.meta.stepIntervals',
{},
);
const minStepInterval = Math.min(...Object.values(stepIntervals));
const config = buildBaseConfig({
yAxisUnit: yAxisUnit,
apiResponse,
isDarkMode,
onDragSelect,
timezone,
onClick,
minTimeScale,
maxTimeScale,
stepInterval: minStepInterval,
panelType: PANEL_TYPES.BAR,
});
const seriesList: QueryData[] = apiResponse?.data?.result || [];
seriesList.forEach((series) => {
const baseLabelName = getLabelName(
series.metric,
series.queryName || '', // query
series.legend || '',
);
const label = query ? getLegend(series, query, baseLabelName) : baseLabelName;
const currentStepInterval = get(stepIntervals, series.queryName, undefined);
config.addSeries({
scaleKey: 'y',
drawStyle: DrawStyle.Bar,
label: label,
colorMapping: colorMapping ?? {},
isDarkMode,
stepInterval: currentStepInterval,
});
});
return config;
};

View File

@@ -10,15 +10,7 @@ import { useBarChartStacking } from '../../hooks/useBarChartStacking';
import { BarChartProps } from '../types';
export default function BarChart(props: BarChartProps): JSX.Element {
const {
children,
isStackedBarChart,
renderTooltip: customRenderTooltip,
config,
data,
pinnedTooltipElement,
...rest
} = props;
const { children, isStackedBarChart, config, data, ...rest } = props;
const chartData = useBarChartStacking({
data,
@@ -28,27 +20,16 @@ export default function BarChart(props: BarChartProps): JSX.Element {
const renderTooltip = useCallback(
(props: TooltipRenderArgs): React.ReactNode => {
if (customRenderTooltip) {
return customRenderTooltip(props);
}
const tooltipProps: BarTooltipProps = {
...props,
timezone: rest.timezone,
yAxisUnit: rest.yAxisUnit,
decimalPrecision: rest.decimalPrecision,
isStackedBarChart: isStackedBarChart,
pinnedTooltipElement,
};
return <BarChartTooltip {...tooltipProps} />;
},
[
customRenderTooltip,
rest.timezone,
rest.yAxisUnit,
rest.decimalPrecision,
isStackedBarChart,
pinnedTooltipElement,
],
[rest.timezone, rest.yAxisUnit, rest.decimalPrecision, isStackedBarChart],
);
return (

View File

@@ -13,7 +13,6 @@ export default function Histogram(props: HistogramChartProps): JSX.Element {
children,
renderTooltip: customRenderTooltip,
isQueriesMerged,
pinnedTooltipElement,
...rest
} = props;
@@ -27,17 +26,10 @@ export default function Histogram(props: HistogramChartProps): JSX.Element {
timezone: rest.timezone,
yAxisUnit: rest.yAxisUnit,
decimalPrecision: rest.decimalPrecision,
pinnedTooltipElement,
};
return <HistogramTooltip {...tooltipProps} />;
},
[
customRenderTooltip,
pinnedTooltipElement,
rest.timezone,
rest.yAxisUnit,
rest.decimalPrecision,
],
[customRenderTooltip, rest.timezone, rest.yAxisUnit, rest.decimalPrecision],
);
return (

View File

@@ -9,12 +9,7 @@ import {
import { TimeSeriesChartProps } from '../types';
export default function TimeSeries(props: TimeSeriesChartProps): JSX.Element {
const {
children,
renderTooltip: customRenderTooltip,
pinnedTooltipElement,
...rest
} = props;
const { children, renderTooltip: customRenderTooltip, ...rest } = props;
const renderTooltip = useCallback(
(props: TooltipRenderArgs): React.ReactNode => {
@@ -26,17 +21,10 @@ export default function TimeSeries(props: TimeSeriesChartProps): JSX.Element {
timezone: rest.timezone,
yAxisUnit: rest.yAxisUnit,
decimalPrecision: rest.decimalPrecision,
pinnedTooltipElement,
};
return <TimeSeriesTooltip {...tooltipProps} />;
},
[
customRenderTooltip,
pinnedTooltipElement,
rest.timezone,
rest.yAxisUnit,
rest.decimalPrecision,
],
[customRenderTooltip, rest.timezone, rest.yAxisUnit, rest.decimalPrecision],
);
return (

View File

@@ -2,10 +2,7 @@ import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PrecisionOption } from 'components/Graph/types';
import { LegendConfig, TooltipRenderArgs } from 'lib/uPlotV2/components/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import {
DashboardCursorSync,
TooltipClickData,
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
interface BaseChartProps {
width: number;
@@ -16,7 +13,6 @@ interface BaseChartProps {
canPinTooltip?: boolean;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
pinnedTooltipElement?: (clickData: TooltipClickData) => React.ReactNode;
renderTooltip?: (props: TooltipRenderArgs) => React.ReactNode;
'data-testid'?: string;
}

View File

@@ -18,10 +18,7 @@ export default function Tooltip({
uPlotInstance,
timezone,
content,
isPinned,
showTooltipHeader = true,
pinnedTooltipElement,
clickData,
}: TooltipProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [listHeight, setListHeight] = useState(0);
@@ -75,45 +72,39 @@ export default function Tooltip({
)}
data-testid="uplot-tooltip-container"
>
{isPinned && clickData ? (
pinnedTooltipElement?.(clickData)
) : (
<>
{showTooltipHeader && (
<div className="uplot-tooltip-header" data-testid="uplot-tooltip-header">
<span>{headerTitle}</span>
</div>
)}
<div className="uplot-tooltip-list-container">
{tooltipContent.length > 0 ? (
<Virtuoso
className="uplot-tooltip-list"
data-testid="uplot-tooltip-list"
data={tooltipContent}
style={virtuosoStyle}
totalListHeightChanged={setListHeight}
itemContent={(_, item): JSX.Element => (
<div className="uplot-tooltip-item" data-testid="uplot-tooltip-item">
<div
className="uplot-tooltip-item-marker"
style={{ borderColor: item.color }}
data-is-legend-marker={true}
data-testid="uplot-tooltip-item-marker"
/>
<div
className="uplot-tooltip-item-content"
style={{ color: item.color, fontWeight: item.isActive ? 700 : 400 }}
data-testid="uplot-tooltip-item-content"
>
{item.label}: {item.tooltipValue}
</div>
</div>
)}
/>
) : null}
</div>
</>
{showTooltipHeader && (
<div className="uplot-tooltip-header" data-testid="uplot-tooltip-header">
<span>{headerTitle}</span>
</div>
)}
<div className="uplot-tooltip-list-container">
{tooltipContent.length > 0 ? (
<Virtuoso
className="uplot-tooltip-list"
data-testid="uplot-tooltip-list"
data={tooltipContent}
style={virtuosoStyle}
totalListHeightChanged={setListHeight}
itemContent={(_, item): JSX.Element => (
<div className="uplot-tooltip-item" data-testid="uplot-tooltip-item">
<div
className="uplot-tooltip-item-marker"
style={{ borderColor: item.color }}
data-is-legend-marker={true}
data-testid="uplot-tooltip-item-marker"
/>
<div
className="uplot-tooltip-item-content"
style={{ color: item.color, fontWeight: item.isActive ? 700 : 400 }}
data-testid="uplot-tooltip-item-content"
>
{item.label}: {item.tooltipValue}
</div>
</div>
)}
/>
) : null}
</div>
</div>
);
}

View File

@@ -6,7 +6,6 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { render, RenderResult, screen } from 'tests/test-utils';
import uPlot from 'uplot';
import type { TooltipClickData } from '../../../plugins/TooltipPlugin/types';
import { TooltipContentItem } from '../../types';
import Tooltip from '../Tooltip';
@@ -93,7 +92,6 @@ function renderTooltip(props: Partial<TooltipTestProps> = {}): RenderResult {
isPinned: false,
dismiss: jest.fn(),
viaSync: false,
clickData: null,
} as TooltipTestProps;
return render(
@@ -207,43 +205,4 @@ describe('Tooltip', () => {
// Falls back to content length: 2 items * 38px = 76px
expect(list).toHaveStyle({ height: '76px' });
});
it('renders pinned tooltip content when clickData is provided', () => {
const uPlotInstance = createUPlotInstance(0);
const clickData: TooltipClickData = {
xValue: 100,
yValue: 200,
focusedSeries: {
seriesIndex: 1,
seriesName: 'Series A',
value: 10,
color: '#ff0000',
},
clickedDataTimestamp: 100,
mouseX: 50,
mouseY: 60,
absoluteMouseX: 150,
absoluteMouseY: 160,
};
const pinnedTooltipElement = jest.fn(
(data: TooltipClickData): JSX.Element => (
<div data-testid="pinned-tooltip">{`Pinned at ${data.xValue}`}</div>
),
);
renderTooltip({
uPlotInstance,
isPinned: true,
clickData,
pinnedTooltipElement,
content: [createTooltipContent()],
});
expect(pinnedTooltipElement).toHaveBeenCalledWith(clickData);
expect(screen.getByTestId('pinned-tooltip')).toBeInTheDocument();
expect(screen.queryByTestId('uplot-tooltip-header')).not.toBeInTheDocument();
expect(screen.queryByTestId('uplot-tooltip-list')).not.toBeInTheDocument();
});
});

View File

@@ -4,7 +4,6 @@ import { PrecisionOption } from 'components/Graph/types';
import uPlot from 'uplot';
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
import { TooltipClickData } from '../plugins/TooltipPlugin/types';
/**
* Props for the Plot component
@@ -59,7 +58,6 @@ export interface TooltipRenderArgs {
isPinned: boolean;
dismiss: () => void;
viaSync: boolean;
clickData: TooltipClickData | null;
}
export interface BaseTooltipProps {
@@ -68,7 +66,6 @@ export interface BaseTooltipProps {
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
content?: TooltipContentItem[];
pinnedTooltipElement?: (clickData: TooltipClickData) => ReactNode;
}
export interface TimeSeriesTooltipProps

View File

@@ -1,7 +1,6 @@
import { useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import cx from 'classnames';
import { getFocusedSeriesAtPosition } from 'lib/uPlotLib/plugins/onClickPlugin';
import uPlot from 'uplot';
import {
@@ -143,7 +142,6 @@ export default function TooltipPlugin({
const isPinnedBeforeDismiss = controller.pinned;
controller.pinned = false;
controller.hoverActive = false;
controller.clickData = null;
if (controller.plot) {
controller.plot.setCursor({ left: -10, top: -10 });
}
@@ -163,7 +161,6 @@ export default function TooltipPlugin({
isPinned: controller.pinned,
dismiss: dismissTooltip,
viaSync: controller.cursorDrivenBySync,
clickData: controller.clickData,
});
}
@@ -250,26 +247,6 @@ export default function TooltipPlugin({
!controller.pinned &&
controller.focusedSeriesIndex != null
) {
const xValue = u.posToVal(event.offsetX, 'x');
const yValue = u.posToVal(event.offsetY, 'y');
const focusedSeries = getFocusedSeriesAtPosition(event, u);
let clickedDataTimestamp = xValue;
if (focusedSeries) {
clickedDataTimestamp = u.data[0][u.posToIdx(event.offsetX)];
}
controller.clickData = {
xValue,
yValue,
focusedSeries,
clickedDataTimestamp,
mouseX: event.offsetX,
mouseY: event.offsetY,
absoluteMouseX: event.clientX,
absoluteMouseY: event.clientY,
};
setTimeout(() => {
controller.pinned = true;
scheduleRender(true);

View File

@@ -26,7 +26,6 @@ export function createInitialControllerState(): TooltipControllerState {
windowWidth: window.innerWidth - WINDOW_OFFSET,
windowHeight: window.innerHeight - WINDOW_OFFSET,
pendingPinnedUpdate: false,
clickData: null,
};
}

View File

@@ -42,22 +42,6 @@ export interface TooltipPluginProps {
maxHeight?: number;
}
export interface TooltipClickData {
xValue: number;
yValue: number;
focusedSeries: {
seriesIndex: number;
seriesName: string;
value: number;
color: string;
} | null;
clickedDataTimestamp: number;
mouseX: number;
mouseY: number;
absoluteMouseX: number;
absoluteMouseY: number;
}
/**
* Mutable, non-React state that drives tooltip behaviour:
* - whether the tooltip is active / pinned
@@ -84,9 +68,6 @@ export interface TooltipControllerState {
windowWidth: number;
windowHeight: number;
pendingPinnedUpdate: boolean;
/** Data about the click that triggered the tooltip */
clickData: TooltipClickData | null;
}
/**

View File

@@ -9,12 +9,6 @@ import { UPlotConfigBuilder } from '../../config/UPlotConfigBuilder';
import TooltipPlugin from '../TooltipPlugin/TooltipPlugin';
import { DashboardCursorSync } from '../TooltipPlugin/types';
// Avoid depending on the full uPlot + onClickPlugin behaviour in these tests.
// We only care that pinning logic runs without throwing, not which series is focused.
jest.mock('lib/uPlotLib/plugins/onClickPlugin', () => ({
getFocusedSeriesAtPosition: jest.fn(() => null),
}));
// ---------------------------------------------------------------------------
// Mock helpers
// ---------------------------------------------------------------------------
@@ -61,21 +55,11 @@ function createFakePlot(): {
over: HTMLDivElement;
setCursor: jest.Mock<void, [uPlot.Cursor]>;
cursor: { event: Record<string, unknown> };
posToVal: jest.Mock<number, [value: number]>;
posToIdx: jest.Mock<number, []>;
data: [number[], number[]];
} {
const over = document.createElement('div');
// Provide the minimal uPlot surface used by TooltipPlugin's pin logic.
return {
over,
over: document.createElement('div'),
setCursor: jest.fn(),
cursor: { event: {} },
// In real uPlot these map overlay coordinates to data-space values.
posToVal: jest.fn((value: number) => value),
posToIdx: jest.fn(() => 0),
data: [[0], [0]],
};
}