histogramChart.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import {Fragment} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import type {Location} from 'history';
  5. import type {BarChartProps} from 'sentry/components/charts/barChart';
  6. import {BarChart} from 'sentry/components/charts/barChart';
  7. import BarChartZoom from 'sentry/components/charts/barChartZoom';
  8. import ErrorPanel from 'sentry/components/charts/errorPanel';
  9. import {HeaderTitleLegend} from 'sentry/components/charts/styles';
  10. import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
  11. import Placeholder from 'sentry/components/placeholder';
  12. import QuestionTooltip from 'sentry/components/questionTooltip';
  13. import {IconWarning} from 'sentry/icons/iconWarning';
  14. import {t} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import type {Organization} from 'sentry/types';
  17. import type {Series} from 'sentry/types/echarts';
  18. import type EventView from 'sentry/utils/discover/eventView';
  19. import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
  20. import getDynamicText from 'sentry/utils/getDynamicText';
  21. import HistogramQuery from 'sentry/utils/performance/histogram/histogramQuery';
  22. import type {HistogramData} from 'sentry/utils/performance/histogram/types';
  23. import {
  24. computeBuckets,
  25. formatHistogramData,
  26. } from 'sentry/utils/performance/histogram/utils';
  27. import {DoubleHeaderContainer} from '../../styles';
  28. import {getFieldOrBackup} from '../display/utils';
  29. const NUM_BUCKETS = 50;
  30. const PRECISION = 0;
  31. type Props = {
  32. eventView: EventView;
  33. field: string;
  34. location: Location;
  35. onFilterChange: (minValue: number, maxValue: number) => void;
  36. organization: Organization;
  37. title: string;
  38. titleTooltip: string;
  39. usingBackupAxis: boolean;
  40. backupField?: string;
  41. didReceiveMultiAxis?: (axisCounts: Record<string, number>) => void;
  42. };
  43. export function HistogramChart(props: Props) {
  44. const {
  45. location,
  46. onFilterChange,
  47. organization,
  48. eventView,
  49. field,
  50. title,
  51. titleTooltip,
  52. didReceiveMultiAxis,
  53. backupField,
  54. usingBackupAxis,
  55. } = props;
  56. const _backupField = backupField ? [backupField] : [];
  57. return (
  58. <div>
  59. <DoubleHeaderContainer>
  60. <HeaderTitleLegend>
  61. {title}
  62. <QuestionTooltip position="top" size="sm" title={titleTooltip} />
  63. </HeaderTitleLegend>
  64. </DoubleHeaderContainer>
  65. <HistogramQuery
  66. location={location}
  67. orgSlug={organization.slug}
  68. eventView={eventView}
  69. numBuckets={NUM_BUCKETS}
  70. precision={PRECISION}
  71. fields={[field, ..._backupField]}
  72. dataFilter="exclude_outliers"
  73. didReceiveMultiAxis={didReceiveMultiAxis}
  74. >
  75. {results => {
  76. const _field = usingBackupAxis ? getFieldOrBackup(field, backupField) : field;
  77. const isLoading = results.isLoading;
  78. const isErrored = results.error !== null;
  79. const chartData = results.histograms?.[_field];
  80. if (isErrored) {
  81. return (
  82. <ErrorPanel height="250px">
  83. <IconWarning color="gray300" size="lg" />
  84. </ErrorPanel>
  85. );
  86. }
  87. if (!chartData) {
  88. return null;
  89. }
  90. return (
  91. <Chart
  92. isLoading={isLoading}
  93. isErrored={isErrored}
  94. chartData={chartData}
  95. location={location}
  96. onFilterChange={onFilterChange}
  97. field={_field}
  98. />
  99. );
  100. }}
  101. </HistogramQuery>
  102. </div>
  103. );
  104. }
  105. type ChartProps = {
  106. field: string;
  107. isErrored: boolean;
  108. isLoading: boolean;
  109. location: Location;
  110. onFilterChange: Props['onFilterChange'];
  111. chartData?: HistogramData;
  112. colors?: string[];
  113. disableChartPadding?: boolean;
  114. disableXAxis?: boolean;
  115. disableZoom?: boolean;
  116. grid?: BarChartProps['grid'];
  117. height?: number;
  118. };
  119. export function Chart(props: ChartProps) {
  120. const {
  121. isLoading,
  122. isErrored,
  123. chartData,
  124. location,
  125. field,
  126. onFilterChange,
  127. height,
  128. grid,
  129. disableXAxis,
  130. disableZoom,
  131. disableChartPadding,
  132. colors,
  133. } = props;
  134. const theme = useTheme();
  135. if (!chartData) {
  136. return null;
  137. }
  138. const series = {
  139. seriesName: t('Count'),
  140. data: formatHistogramData(chartData, {type: 'duration'}),
  141. };
  142. const xAxis = {
  143. type: 'category' as const,
  144. truncate: true,
  145. axisTick: {
  146. alignWithLabel: true,
  147. },
  148. };
  149. const allSeries: Series[] = [];
  150. if (!isLoading && !isErrored) {
  151. allSeries.push(series);
  152. }
  153. const yAxis = {
  154. type: 'value' as const,
  155. axisLabel: {
  156. color: theme.chartLabel,
  157. formatter: (value: number | string) => formatAbbreviatedNumber(value),
  158. },
  159. };
  160. return (
  161. <Fragment>
  162. <BarChartZoom
  163. minZoomWidth={10 ** -PRECISION * NUM_BUCKETS}
  164. location={location}
  165. paramStart={`${field}:>=`}
  166. paramEnd={`${field}:<=`}
  167. xAxisIndex={[0]}
  168. buckets={computeBuckets(chartData)}
  169. onHistoryPush={onFilterChange}
  170. >
  171. {zoomRenderProps => {
  172. return (
  173. <BarChartContainer hasPadding={!disableChartPadding}>
  174. <MaskContainer>
  175. <TransparentLoadingMask visible={isLoading} />
  176. {getDynamicText({
  177. value: (
  178. <BarChart
  179. height={height ?? 250}
  180. series={allSeries}
  181. xAxis={disableXAxis ? {show: false} : xAxis}
  182. yAxis={yAxis}
  183. colors={colors}
  184. grid={
  185. grid ?? {
  186. left: space(3),
  187. right: space(3),
  188. top: space(3),
  189. bottom: isLoading ? space(4) : space(1.5),
  190. }
  191. }
  192. stacked
  193. {...(disableZoom ? {} : zoomRenderProps)}
  194. />
  195. ),
  196. fixed: <Placeholder height="250px" testId="skeleton-ui" />,
  197. })}
  198. </MaskContainer>
  199. </BarChartContainer>
  200. );
  201. }}
  202. </BarChartZoom>
  203. </Fragment>
  204. );
  205. }
  206. const BarChartContainer = styled('div')<{hasPadding?: boolean}>`
  207. padding-top: ${p => (p.hasPadding ? space(1) : 0)};
  208. position: relative;
  209. `;
  210. const MaskContainer = styled('div')`
  211. position: relative;
  212. `;
  213. export default HistogramChart;