Files
2024-02-15 17:01:50 +00:00

216 lines
6.2 KiB
TypeScript

import {useState} from 'react';
import type {Location} from 'history';
import {BarChart} from 'sentry/components/charts/barChart';
import BarChartZoom from 'sentry/components/charts/barChartZoom';
import ErrorPanel from 'sentry/components/charts/errorPanel';
import LoadingPanel from 'sentry/components/charts/loadingPanel';
import {IconWarning} from 'sentry/icons';
import {t} from 'sentry/locale';
import type {OrganizationSummary} from 'sentry/types';
import EventView from 'sentry/utils/discover/eventView';
import {formatPercentage} from 'sentry/utils/formatters';
import Histogram from 'sentry/utils/performance/histogram';
import HistogramQuery from 'sentry/utils/performance/histogram/histogramQuery';
import type {HistogramData} from 'sentry/utils/performance/histogram/types';
import {
computeBuckets,
formatHistogramData,
} from 'sentry/utils/performance/histogram/utils';
import theme from 'sentry/utils/theme';
import toArray from 'sentry/utils/toArray';
import type {ViewProps} from '../../../types';
import {filterToColor, filterToField, SpanOperationBreakdownFilter} from '../../filter';
import {decodeHistogramZoom, ZOOM_END, ZOOM_START} from './utils';
const NUM_BUCKETS = 50;
type Props = ViewProps & {
currentFilter: SpanOperationBreakdownFilter;
location: Location;
organization: OrganizationSummary;
queryExtras?: Record<string, string>;
totalCount?: number | null;
};
/**
* Fetch and render a bar chart that shows event volume
* for each duration bucket. We always render 50 buckets of
* equal widths based on the endpoints min + max durations.
*
* This graph visualizes how many transactions were recorded
* at each duration bucket, showing the modality of the transaction.
*/
function Content({
organization,
query,
start,
end,
statsPeriod,
environment,
project,
location,
currentFilter,
queryExtras,
totalCount,
}: Props) {
const [zoomError, setZoomError] = useState(false);
function handleMouseOver() {
// Hide the zoom error tooltip on the next hover.
if (zoomError) {
setZoomError(false);
}
}
function parseHistogramData(data: HistogramData): HistogramData {
// display each bin's count as a % of total count
if (totalCount) {
return data.map(({bin, count}) => ({bin, count: count / totalCount}));
}
return data;
}
function renderChart(data: HistogramData) {
const xAxis = {
type: 'category' as const,
truncate: true,
axisTick: {
interval: 0,
alignWithLabel: true,
},
};
const colors =
currentFilter === SpanOperationBreakdownFilter.NONE
? [...theme.charts.getColorPalette(1)]
: [filterToColor(currentFilter)];
// Use a custom tooltip formatter as we need to replace
// the tooltip content entirely when zooming is no longer available.
const tooltip = {
formatter(series) {
const seriesData = toArray(series);
let contents: string[] = [];
if (!zoomError) {
// Replicate the necessary logic from sentry/components/charts/components/tooltip.jsx
contents = seriesData.map(item => {
const label = t('Transactions');
const value = formatPercentage(item.value[1]);
return [
'<div class="tooltip-series">',
`<div><span class="tooltip-label">${item.marker} <strong>${label}</strong></span> ${value}</div>`,
'</div>',
].join('');
});
const seriesLabel = seriesData[0].value[0];
contents.push(`<div class="tooltip-footer">${seriesLabel}</div>`);
} else {
contents = [
'<div class="tooltip-series tooltip-series-solo">',
t('Target zoom region too small'),
'</div>',
];
}
contents.push('<div class="tooltip-arrow"></div>');
return contents.join('');
},
};
const parsedData = parseHistogramData(data);
const series = {
seriesName: t('Count'),
data: formatHistogramData(parsedData, {type: 'duration'}),
};
return (
<BarChartZoom
minZoomWidth={NUM_BUCKETS}
location={location}
paramStart={ZOOM_START}
paramEnd={ZOOM_END}
xAxisIndex={[0]}
buckets={computeBuckets(data)}
onDataZoomCancelled={() => setZoomError(true)}
>
{zoomRenderProps => (
<BarChart
grid={{left: '10px', right: '10px', top: '40px', bottom: '0px'}}
xAxis={xAxis}
yAxis={{
type: 'value',
axisLabel: {
formatter: value => formatPercentage(value, 0),
},
}}
series={[series]}
tooltip={tooltip}
colors={colors}
onMouseOver={handleMouseOver}
{...zoomRenderProps}
/>
)}
</BarChartZoom>
);
}
const eventView = EventView.fromNewQueryWithLocation(
{
id: undefined,
version: 2,
name: '',
fields: ['transaction.duration'],
projects: project,
range: statsPeriod,
query,
environment,
start,
end,
},
location
);
const {min, max} = decodeHistogramZoom(location);
const field = filterToField(currentFilter) ?? 'transaction.duration';
return (
<Histogram location={location} zoomKeys={[ZOOM_START, ZOOM_END]}>
{({activeFilter}) => (
<HistogramQuery
location={location}
orgSlug={organization.slug}
eventView={eventView}
numBuckets={NUM_BUCKETS}
fields={[field]}
min={min}
max={max}
dataFilter={activeFilter.value}
queryExtras={queryExtras}
>
{({histograms, isLoading, error}) => {
if (isLoading) {
return <LoadingPanel data-test-id="histogram-loading" />;
}
if (error) {
return (
<ErrorPanel>
<IconWarning color="gray300" size="lg" />
</ErrorPanel>
);
}
return renderChart(histograms?.[field] ?? []);
}}
</HistogramQuery>
)}
</Histogram>
);
}
export default Content;