content.tsx 5.5 KB

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