useChartSamples.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import type {RefObject} from 'react';
  2. import {useCallback, useEffect, useMemo, useState} from 'react';
  3. import {useTheme} from '@emotion/react';
  4. import type {XAXisOption} from 'echarts/types/dist/shared';
  5. import moment from 'moment';
  6. import {t} from 'sentry/locale';
  7. import type {ReactEchartsRef, Series} from 'sentry/types/echarts';
  8. import {isCumulativeOp} from 'sentry/utils/metrics';
  9. import type {MetricCorrelation, MetricSummary} from 'sentry/utils/metrics/types';
  10. import {fitToValueRect, getValueRect} from 'sentry/views/ddm/chartUtils';
  11. import type {Sample} from 'sentry/views/ddm/widget';
  12. type UseChartSamplesProps = {
  13. timeseries: Series[];
  14. chartRef?: RefObject<ReactEchartsRef>;
  15. correlations?: MetricCorrelation[];
  16. highlightedSampleId?: string;
  17. onClick?: (sample: Sample) => void;
  18. onMouseOut?: (sample: Sample) => void;
  19. onMouseOver?: (sample: Sample) => void;
  20. operation?: string;
  21. valueFormatter?: (value: number) => string;
  22. };
  23. // TODO: remove this once we have a stabilized type for this
  24. type ChartSample = MetricCorrelation & MetricSummary;
  25. function getDateRange(timeseries: Series[]) {
  26. if (!timeseries?.length) {
  27. return {min: -Infinity, max: Infinity};
  28. }
  29. const min = timeseries[0].data[0].name as number;
  30. const max = timeseries[0].data[timeseries[0].data.length - 1].name as number;
  31. return {min, max};
  32. }
  33. export function useChartSamples({
  34. correlations,
  35. onClick,
  36. highlightedSampleId,
  37. chartRef,
  38. operation,
  39. timeseries,
  40. valueFormatter,
  41. }: UseChartSamplesProps) {
  42. const theme = useTheme();
  43. const [valueRect, setValueRect] = useState(getValueRect(chartRef));
  44. const samples: Record<string, ChartSample> = useMemo(() => {
  45. return (correlations ?? [])
  46. ?.flatMap(correlation => [
  47. ...correlation.metricSummaries.map(summaries => ({...summaries, ...correlation})),
  48. ])
  49. .reduce((acc, sample) => {
  50. acc[sample.transactionId] = sample;
  51. return acc;
  52. }, {});
  53. }, [correlations]);
  54. useEffect(() => {
  55. // Changes in timeseries change the valueRect since the timeseries yAxis auto scales
  56. // and scatter yAxis needs to match the scale
  57. setValueRect(getValueRect(chartRef));
  58. }, [chartRef, timeseries]);
  59. const xAxis: XAXisOption = useMemo(() => {
  60. const {min, max} = getDateRange(timeseries);
  61. return {
  62. id: 'xAxisScatter',
  63. scale: false,
  64. show: false,
  65. axisLabel: {
  66. formatter: () => {
  67. return '';
  68. },
  69. },
  70. axisPointer: {
  71. type: 'none',
  72. },
  73. min: Math.max(valueRect.xMin, min),
  74. max: Math.min(valueRect.xMax, max),
  75. };
  76. }, [valueRect.xMin, valueRect.xMax, timeseries]);
  77. const yAxis = useMemo(() => {
  78. return {
  79. id: 'yAxisScatter',
  80. scale: false,
  81. show: false,
  82. axisLabel: {
  83. formatter: () => {
  84. return '';
  85. },
  86. },
  87. min: valueRect.yMin,
  88. max: valueRect.yMax,
  89. };
  90. }, [valueRect.yMin, valueRect.yMax]);
  91. const getSample = useCallback(
  92. event => {
  93. return samples?.[event.seriesName] as Sample;
  94. },
  95. [samples]
  96. );
  97. const handleClick = useCallback(
  98. event => {
  99. if (!onClick) {
  100. return;
  101. }
  102. const sample = getSample(event);
  103. if (!sample) {
  104. return;
  105. }
  106. onClick(sample);
  107. },
  108. [getSample, onClick]
  109. );
  110. const series = useMemo(() => {
  111. if (isCumulativeOp(operation)) {
  112. // TODO: for now we do not show samples for cumulative operations,
  113. // we will implement them as marklines
  114. return [];
  115. }
  116. return Object.values(samples).map(sample => {
  117. const isHighlighted = highlightedSampleId === sample.transactionId;
  118. const xValue = moment(sample.timestamp).valueOf();
  119. const yValue = ((sample.min ?? 0) + (sample.max ?? 0)) / 2;
  120. const [xPosition, yPosition] = fitToValueRect(xValue, yValue, valueRect);
  121. const symbol = yPosition === yValue ? 'circle' : 'arrow';
  122. const symbolRotate = yPosition > yValue ? 180 : 0;
  123. return {
  124. seriesName: sample.transactionId,
  125. // TODO: we should not use the same Series type for samples and metrics
  126. operation: '',
  127. unit: '',
  128. symbolSize: isHighlighted ? 20 : 10,
  129. animation: false,
  130. symbol,
  131. symbolRotate,
  132. color: theme.purple400,
  133. // TODO: for now we just pass these ids through, but we should probably index
  134. // samples by an id and then just pass that reference
  135. transactionId: sample.transactionId,
  136. transactionSpanId: sample.transactionSpanId,
  137. spanId: sample.spanId,
  138. projectId: sample.projectId,
  139. itemStyle: {
  140. color: theme.purple400,
  141. opacity: 1,
  142. },
  143. yAxisIndex: 1,
  144. xAxisIndex: 1,
  145. xValue,
  146. yValue,
  147. tooltip: {
  148. axisPointer: {
  149. type: 'none',
  150. },
  151. },
  152. data: [
  153. {
  154. name: xPosition,
  155. value: yPosition,
  156. },
  157. ],
  158. z: 10,
  159. };
  160. });
  161. }, [samples, highlightedSampleId, theme.purple400, valueRect, operation]);
  162. const formatters = useMemo(() => {
  163. return {
  164. isGroupedByDate: true,
  165. limit: 1,
  166. showTimeInTooltip: true,
  167. addSecondsToTimeFormat: true,
  168. nameFormatter: (name: string) => {
  169. return t('Event %s', name.substring(0, 8));
  170. },
  171. valueFormatter: (_, label?: string) => {
  172. const sample = samples[label ?? ''];
  173. const yValue = ((sample.min ?? 0) + (sample.max ?? 0)) / 2;
  174. if (!valueFormatter) {
  175. return yValue.toPrecision(5);
  176. }
  177. return valueFormatter(yValue);
  178. },
  179. };
  180. }, [samples, valueFormatter]);
  181. return {
  182. handleClick,
  183. series,
  184. xAxis,
  185. yAxis,
  186. formatters,
  187. };
  188. }