content.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import {useState} from 'react';
  2. import type {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 type {OrganizationSummary} from 'sentry/types/organization';
  10. import toArray from 'sentry/utils/array/toArray';
  11. import EventView from 'sentry/utils/discover/eventView';
  12. import {formatPercentage} from 'sentry/utils/number/formatPercentage';
  13. import Histogram from 'sentry/utils/performance/histogram';
  14. import HistogramQuery from 'sentry/utils/performance/histogram/histogramQuery';
  15. import type {HistogramData} from 'sentry/utils/performance/histogram/types';
  16. import {
  17. computeBuckets,
  18. formatHistogramData,
  19. } from 'sentry/utils/performance/histogram/utils';
  20. import theme from 'sentry/utils/theme';
  21. import type {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. function handleMouseOver() {
  55. // Hide the zoom error tooltip on the next hover.
  56. if (zoomError) {
  57. setZoomError(false);
  58. }
  59. }
  60. function parseHistogramData(data: HistogramData): HistogramData {
  61. // display each bin's count as a % of total count
  62. if (totalCount) {
  63. return data.map(({bin, count}) => ({bin, count: count / totalCount}));
  64. }
  65. return data;
  66. }
  67. function renderChart(data: HistogramData) {
  68. const xAxis = {
  69. type: 'category' as const,
  70. truncate: true,
  71. axisTick: {
  72. interval: 0,
  73. alignWithLabel: true,
  74. },
  75. };
  76. const colors =
  77. currentFilter === SpanOperationBreakdownFilter.NONE
  78. ? [...theme.charts.getColorPalette(1)]
  79. : [filterToColor(currentFilter)];
  80. // Use a custom tooltip formatter as we need to replace
  81. // the tooltip content entirely when zooming is no longer available.
  82. const tooltip = {
  83. formatter(series) {
  84. const seriesData = toArray(series);
  85. let contents: string[] = [];
  86. if (!zoomError) {
  87. // Replicate the necessary logic from sentry/components/charts/components/tooltip.jsx
  88. contents = seriesData.map(item => {
  89. const label = t('Transactions');
  90. const value = formatPercentage(item.value[1]);
  91. return [
  92. '<div class="tooltip-series">',
  93. `<div><span class="tooltip-label">${item.marker} <strong>${label}</strong></span> ${value}</div>`,
  94. '</div>',
  95. ].join('');
  96. });
  97. const seriesLabel = seriesData[0].value[0];
  98. contents.push(`<div class="tooltip-footer">${seriesLabel}</div>`);
  99. } else {
  100. contents = [
  101. '<div class="tooltip-series tooltip-series-solo">',
  102. t('Target zoom region too small'),
  103. '</div>',
  104. ];
  105. }
  106. contents.push('<div class="tooltip-arrow"></div>');
  107. return contents.join('');
  108. },
  109. };
  110. const parsedData = parseHistogramData(data);
  111. const series = {
  112. seriesName: t('Count'),
  113. data: formatHistogramData(parsedData, {type: 'duration'}),
  114. };
  115. return (
  116. <BarChartZoom
  117. minZoomWidth={NUM_BUCKETS}
  118. location={location}
  119. paramStart={ZOOM_START}
  120. paramEnd={ZOOM_END}
  121. xAxisIndex={[0]}
  122. buckets={computeBuckets(data)}
  123. onDataZoomCancelled={() => setZoomError(true)}
  124. >
  125. {zoomRenderProps => (
  126. <BarChart
  127. grid={{left: '10px', right: '10px', top: '40px', bottom: '0px'}}
  128. xAxis={xAxis}
  129. yAxis={{
  130. type: 'value',
  131. axisLabel: {
  132. formatter: value => formatPercentage(value, 0),
  133. },
  134. }}
  135. series={[series]}
  136. tooltip={tooltip}
  137. colors={colors}
  138. onMouseOver={handleMouseOver}
  139. {...zoomRenderProps}
  140. />
  141. )}
  142. </BarChartZoom>
  143. );
  144. }
  145. const eventView = EventView.fromNewQueryWithLocation(
  146. {
  147. id: undefined,
  148. version: 2,
  149. name: '',
  150. fields: ['transaction.duration'],
  151. projects: project,
  152. range: statsPeriod,
  153. query,
  154. environment,
  155. start,
  156. end,
  157. },
  158. location
  159. );
  160. const {min, max} = decodeHistogramZoom(location);
  161. const field = filterToField(currentFilter) ?? 'transaction.duration';
  162. return (
  163. <Histogram location={location} zoomKeys={[ZOOM_START, ZOOM_END]}>
  164. {({activeFilter}) => (
  165. <HistogramQuery
  166. location={location}
  167. orgSlug={organization.slug}
  168. eventView={eventView}
  169. numBuckets={NUM_BUCKETS}
  170. fields={[field]}
  171. min={min}
  172. max={max}
  173. dataFilter={activeFilter.value}
  174. queryExtras={queryExtras}
  175. >
  176. {({histograms, isLoading, error}) => {
  177. if (isLoading) {
  178. return <LoadingPanel data-test-id="histogram-loading" />;
  179. }
  180. if (error) {
  181. return (
  182. <ErrorPanel>
  183. <IconWarning color="gray300" size="lg" />
  184. </ErrorPanel>
  185. );
  186. }
  187. return renderChart(histograms?.[field] ?? []);
  188. }}
  189. </HistogramQuery>
  190. )}
  191. </Histogram>
  192. );
  193. }
  194. export default Content;