123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- import {Fragment} from 'react';
- import {useTheme} from '@emotion/react';
- import styled from '@emotion/styled';
- import {Location} from 'history';
- import {BarChart, BarChartProps} from 'sentry/components/charts/barChart';
- import BarChartZoom from 'sentry/components/charts/barChartZoom';
- import ErrorPanel from 'sentry/components/charts/errorPanel';
- import {HeaderTitleLegend} from 'sentry/components/charts/styles';
- import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
- import Placeholder from 'sentry/components/placeholder';
- import QuestionTooltip from 'sentry/components/questionTooltip';
- import {IconWarning} from 'sentry/icons/iconWarning';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import {Organization} from 'sentry/types';
- import {Series} from 'sentry/types/echarts';
- import EventView from 'sentry/utils/discover/eventView';
- import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
- import getDynamicText from 'sentry/utils/getDynamicText';
- import HistogramQuery from 'sentry/utils/performance/histogram/histogramQuery';
- import {HistogramData} from 'sentry/utils/performance/histogram/types';
- import {
- computeBuckets,
- formatHistogramData,
- } from 'sentry/utils/performance/histogram/utils';
- import {DoubleHeaderContainer} from '../../styles';
- import {getFieldOrBackup} from '../display/utils';
- const NUM_BUCKETS = 50;
- const PRECISION = 0;
- type Props = {
- eventView: EventView;
- field: string;
- location: Location;
- onFilterChange: (minValue: number, maxValue: number) => void;
- organization: Organization;
- title: string;
- titleTooltip: string;
- usingBackupAxis: boolean;
- backupField?: string;
- didReceiveMultiAxis?: (axisCounts: Record<string, number>) => void;
- };
- export function HistogramChart(props: Props) {
- const {
- location,
- onFilterChange,
- organization,
- eventView,
- field,
- title,
- titleTooltip,
- didReceiveMultiAxis,
- backupField,
- usingBackupAxis,
- } = props;
- const _backupField = backupField ? [backupField] : [];
- return (
- <div>
- <DoubleHeaderContainer>
- <HeaderTitleLegend>
- {title}
- <QuestionTooltip position="top" size="sm" title={titleTooltip} />
- </HeaderTitleLegend>
- </DoubleHeaderContainer>
- <HistogramQuery
- location={location}
- orgSlug={organization.slug}
- eventView={eventView}
- numBuckets={NUM_BUCKETS}
- precision={PRECISION}
- fields={[field, ..._backupField]}
- dataFilter="exclude_outliers"
- didReceiveMultiAxis={didReceiveMultiAxis}
- >
- {results => {
- const _field = usingBackupAxis ? getFieldOrBackup(field, backupField) : field;
- const isLoading = results.isLoading;
- const isErrored = results.error !== null;
- const chartData = results.histograms?.[_field];
- if (isErrored) {
- return (
- <ErrorPanel height="250px">
- <IconWarning color="gray300" size="lg" />
- </ErrorPanel>
- );
- }
- if (!chartData) {
- return null;
- }
- return (
- <Chart
- isLoading={isLoading}
- isErrored={isErrored}
- chartData={chartData}
- location={location}
- onFilterChange={onFilterChange}
- field={_field}
- />
- );
- }}
- </HistogramQuery>
- </div>
- );
- }
- type ChartProps = {
- field: string;
- isErrored: boolean;
- isLoading: boolean;
- location: Location;
- onFilterChange: Props['onFilterChange'];
- chartData?: HistogramData;
- colors?: string[];
- disableChartPadding?: boolean;
- disableXAxis?: boolean;
- disableZoom?: boolean;
- grid?: BarChartProps['grid'];
- height?: number;
- };
- export function Chart(props: ChartProps) {
- const {
- isLoading,
- isErrored,
- chartData,
- location,
- field,
- onFilterChange,
- height,
- grid,
- disableXAxis,
- disableZoom,
- disableChartPadding,
- colors,
- } = props;
- const theme = useTheme();
- if (!chartData) {
- return null;
- }
- const series = {
- seriesName: t('Count'),
- data: formatHistogramData(chartData, {type: 'duration'}),
- };
- const xAxis = {
- type: 'category' as const,
- truncate: true,
- axisTick: {
- alignWithLabel: true,
- },
- };
- const allSeries: Series[] = [];
- if (!isLoading && !isErrored) {
- allSeries.push(series);
- }
- const yAxis = {
- type: 'value' as const,
- axisLabel: {
- color: theme.chartLabel,
- formatter: (value: number | string) => formatAbbreviatedNumber(value),
- },
- };
- return (
- <Fragment>
- <BarChartZoom
- minZoomWidth={10 ** -PRECISION * NUM_BUCKETS}
- location={location}
- paramStart={`${field}:>=`}
- paramEnd={`${field}:<=`}
- xAxisIndex={[0]}
- buckets={computeBuckets(chartData)}
- onHistoryPush={onFilterChange}
- >
- {zoomRenderProps => {
- return (
- <BarChartContainer hasPadding={!disableChartPadding}>
- <MaskContainer>
- <TransparentLoadingMask visible={isLoading} />
- {getDynamicText({
- value: (
- <BarChart
- height={height ?? 250}
- series={allSeries}
- xAxis={disableXAxis ? {show: false} : xAxis}
- yAxis={yAxis}
- colors={colors}
- grid={
- grid ?? {
- left: space(3),
- right: space(3),
- top: space(3),
- bottom: isLoading ? space(4) : space(1.5),
- }
- }
- stacked
- {...(disableZoom ? {} : zoomRenderProps)}
- />
- ),
- fixed: <Placeholder height="250px" testId="skeleton-ui" />,
- })}
- </MaskContainer>
- </BarChartContainer>
- );
- }}
- </BarChartZoom>
- </Fragment>
- );
- }
- const BarChartContainer = styled('div')<{hasPadding?: boolean}>`
- padding-top: ${p => (p.hasPadding ? space(1) : 0)};
- position: relative;
- `;
- const MaskContainer = styled('div')`
- position: relative;
- `;
- export default HistogramChart;
|