useChartSamples.tsx 5.5 KB

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