content.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import {useState} from 'react';
  2. import {Location} from 'history';
  3. import {BarChart} from 'sentry/components/charts/barChart';
  4. import BarChartZoom from 'sentry/components/charts/barChartZoom';
  5. import ErrorPanel from 'sentry/components/charts/errorPanel';
  6. import LoadingPanel from 'sentry/components/charts/loadingPanel';
  7. import {IconWarning} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import {OrganizationSummary} from 'sentry/types';
  10. import EventView from 'sentry/utils/discover/eventView';
  11. import {formatPercentage} from 'sentry/utils/formatters';
  12. import Histogram from 'sentry/utils/performance/histogram';
  13. import HistogramQuery from 'sentry/utils/performance/histogram/histogramQuery';
  14. import {HistogramData} from 'sentry/utils/performance/histogram/types';
  15. import {
  16. computeBuckets,
  17. formatHistogramData,
  18. } from 'sentry/utils/performance/histogram/utils';
  19. import theme from 'sentry/utils/theme';
  20. import toArray from 'sentry/utils/toArray';
  21. import {ViewProps} from '../../../types';
  22. import {filterToColor, filterToField, SpanOperationBreakdownFilter} from '../../filter';
  23. import {decodeHistogramZoom, ZOOM_END, ZOOM_START} from './utils';
  24. const NUM_BUCKETS = 50;
  25. type Props = ViewProps & {
  26. currentFilter: SpanOperationBreakdownFilter;
  27. location: Location;
  28. organization: OrganizationSummary;
  29. queryExtras?: Record<string, string>;
  30. totalCount?: number | null;
  31. };
  32. /**
  33. * Fetch and render a bar chart that shows event volume
  34. * for each duration bucket. We always render 50 buckets of
  35. * equal widths based on the endpoints min + max durations.
  36. *
  37. * This graph visualizes how many transactions were recorded
  38. * at each duration bucket, showing the modality of the transaction.
  39. */
  40. function Content({
  41. organization,
  42. query,
  43. start,
  44. end,
  45. statsPeriod,
  46. environment,
  47. project,
  48. location,
  49. currentFilter,
  50. queryExtras,
  51. totalCount,
  52. }: Props) {
  53. const [zoomError, setZoomError] = useState(false);
  54. const displayCountAsPercentage = !!totalCount;
  55. function handleMouseOver() {
  56. // Hide the zoom error tooltip on the next hover.
  57. if (zoomError) {
  58. setZoomError(false);
  59. }
  60. }
  61. function parseHistogramData(data: HistogramData): HistogramData {
  62. // display each bin's count as a % of total count
  63. if (totalCount) {
  64. return data.map(({bin, count}) => ({bin, count: count / totalCount}));
  65. }
  66. return data;
  67. }
  68. function renderChart(data: HistogramData) {
  69. const xAxis = {
  70. type: 'category' as const,
  71. truncate: true,
  72. axisTick: {
  73. interval: 0,
  74. alignWithLabel: true,
  75. },
  76. };
  77. const colors =
  78. currentFilter === SpanOperationBreakdownFilter.None
  79. ? [...theme.charts.getColorPalette(1)]
  80. : [filterToColor(currentFilter)];
  81. // Use a custom tooltip formatter as we need to replace
  82. // the tooltip content entirely when zooming is no longer available.
  83. const tooltip = {
  84. formatter(series) {
  85. const seriesData = toArray(series);
  86. let contents: string[] = [];
  87. if (!zoomError) {
  88. // Replicate the necessary logic from sentry/components/charts/components/tooltip.jsx
  89. contents = seriesData.map(item => {
  90. const label = displayCountAsPercentage ? t('Transactions') : item.seriesName;
  91. const value = displayCountAsPercentage
  92. ? formatPercentage(item.value[1])
  93. : item.value[1].toLocaleString();
  94. return [
  95. '<div class="tooltip-series">',
  96. `<div><span class="tooltip-label">${item.marker} <strong>${label}</strong></span> ${value}</div>`,
  97. '</div>',
  98. ].join('');
  99. });
  100. const seriesLabel = seriesData[0].value[0];
  101. contents.push(`<div class="tooltip-footer">${seriesLabel}</div>`);
  102. } else {
  103. contents = [
  104. '<div class="tooltip-series tooltip-series-solo">',
  105. t('Target zoom region too small'),
  106. '</div>',
  107. ];
  108. }
  109. contents.push('<div class="tooltip-arrow"></div>');
  110. return contents.join('');
  111. },
  112. };
  113. const parsedData = parseHistogramData(data);
  114. const series = {
  115. seriesName: t('Count'),
  116. data: formatHistogramData(parsedData, {type: 'duration'}),
  117. };
  118. return (
  119. <BarChartZoom
  120. minZoomWidth={NUM_BUCKETS}
  121. location={location}
  122. paramStart={ZOOM_START}
  123. paramEnd={ZOOM_END}
  124. xAxisIndex={[0]}
  125. buckets={computeBuckets(data)}
  126. onDataZoomCancelled={() => setZoomError(true)}
  127. >
  128. {zoomRenderProps => (
  129. <BarChart
  130. grid={{left: '10px', right: '10px', top: '40px', bottom: '0px'}}
  131. xAxis={xAxis}
  132. yAxis={{type: 'value'}}
  133. series={[series]}
  134. tooltip={tooltip}
  135. colors={colors}
  136. onMouseOver={handleMouseOver}
  137. {...zoomRenderProps}
  138. />
  139. )}
  140. </BarChartZoom>
  141. );
  142. }
  143. const eventView = EventView.fromNewQueryWithLocation(
  144. {
  145. id: undefined,
  146. version: 2,
  147. name: '',
  148. fields: ['transaction.duration'],
  149. projects: project,
  150. range: statsPeriod,
  151. query,
  152. environment,
  153. start,
  154. end,
  155. },
  156. location
  157. );
  158. const {min, max} = decodeHistogramZoom(location);
  159. const field = filterToField(currentFilter) ?? 'transaction.duration';
  160. return (
  161. <Histogram location={location} zoomKeys={[ZOOM_START, ZOOM_END]}>
  162. {({activeFilter}) => (
  163. <HistogramQuery
  164. location={location}
  165. orgSlug={organization.slug}
  166. eventView={eventView}
  167. numBuckets={NUM_BUCKETS}
  168. fields={[field]}
  169. min={min}
  170. max={max}
  171. dataFilter={activeFilter.value}
  172. queryExtras={queryExtras}
  173. >
  174. {({histograms, isLoading, error}) => {
  175. if (isLoading) {
  176. return <LoadingPanel data-test-id="histogram-loading" />;
  177. }
  178. if (error) {
  179. return (
  180. <ErrorPanel>
  181. <IconWarning color="gray300" size="lg" />
  182. </ErrorPanel>
  183. );
  184. }
  185. return renderChart(histograms?.[field] ?? []);
  186. }}
  187. </HistogramQuery>
  188. )}
  189. </Histogram>
  190. );
  191. }
  192. export default Content;