utils.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import type {ECharts} from 'echarts';
  2. import type {Query} from 'history';
  3. import type {WebVital} from 'sentry/utils/fields';
  4. import type {HistogramData} from 'sentry/utils/performance/histogram/types';
  5. import {getBucketWidth} from 'sentry/utils/performance/histogram/utils';
  6. import type {VitalsData} from 'sentry/utils/performance/vitals/vitalsCardsDiscoverQuery';
  7. import {getTransactionSummaryBaseUrl} from 'sentry/views/performance/transactionSummary/utils';
  8. import type {Point, Rectangle} from './types';
  9. export function generateVitalsRoute({orgSlug}: {orgSlug: string}): string {
  10. return `${getTransactionSummaryBaseUrl(orgSlug)}/vitals/`;
  11. }
  12. export function vitalsRouteWithQuery({
  13. orgSlug,
  14. transaction,
  15. projectID,
  16. query,
  17. }: {
  18. orgSlug: string;
  19. query: Query;
  20. transaction: string;
  21. projectID?: string | string[];
  22. }) {
  23. const pathname = generateVitalsRoute({
  24. orgSlug,
  25. });
  26. return {
  27. pathname,
  28. query: {
  29. transaction,
  30. project: projectID,
  31. environment: query.environment,
  32. statsPeriod: query.statsPeriod,
  33. start: query.start,
  34. end: query.end,
  35. query: query.query,
  36. },
  37. };
  38. }
  39. /**
  40. * Given a value on the x-axis, return the index of the nearest bucket or null
  41. * if it cannot be found.
  42. *
  43. * A bucket contains a range of values, and nearest is defined as the bucket the
  44. * value will fall in.
  45. */
  46. export function findNearestBucketIndex(
  47. chartData: HistogramData,
  48. xAxis: number
  49. ): number | null {
  50. const width = getBucketWidth(chartData);
  51. // it's possible that the data is not available yet or the x axis is out of range
  52. if (!chartData.length || xAxis >= chartData[chartData.length - 1].bin + width) {
  53. return null;
  54. }
  55. if (xAxis < chartData[0].bin) {
  56. return -1;
  57. }
  58. return Math.floor((xAxis - chartData[0].bin) / width);
  59. }
  60. /**
  61. * To compute pixel coordinates, we need at least 2 unique points on the chart.
  62. * The two points must have different x axis and y axis values for it to work.
  63. *
  64. * If all bars have the same y value, pick the most naive reference rect. This
  65. * may result in floating point errors, but should be okay for our purposes.
  66. */
  67. export function getRefRect(chartData: HistogramData): Rectangle | null {
  68. // not enough points to construct 2 reference points
  69. if (chartData.length < 2) {
  70. return null;
  71. }
  72. for (let i = 0; i < chartData.length; i++) {
  73. const data1 = chartData[i];
  74. for (let j = i + 1; j < chartData.length; j++) {
  75. const data2 = chartData[j];
  76. if (data1.bin !== data2.bin && data1.count !== data2.count) {
  77. return {
  78. point1: {x: i, y: Math.min(data1.count, data2.count)},
  79. point2: {x: j, y: Math.max(data1.count, data2.count)},
  80. };
  81. }
  82. }
  83. }
  84. // all data points have the same count, just pick any 2 histogram bins
  85. // and use 0 and 1 as the count as we can rely on them being on the graph
  86. return {
  87. point1: {x: 0, y: 0},
  88. point2: {x: 1, y: 1},
  89. };
  90. }
  91. /**
  92. * Given an ECharts instance and a rectangle defined in terms of the x and y axis,
  93. * compute the corresponding pixel coordinates. If it cannot be done, return null.
  94. */
  95. export function asPixelRect(chartRef: ECharts, dataRect: Rectangle): Rectangle | null {
  96. const point1 = chartRef.convertToPixel({xAxisIndex: 0, yAxisIndex: 0}, [
  97. dataRect.point1.x,
  98. dataRect.point1.y,
  99. ]);
  100. if (isNaN(point1?.[0]) || isNaN(point1?.[1])) {
  101. return null;
  102. }
  103. const point2 = chartRef.convertToPixel({xAxisIndex: 0, yAxisIndex: 0}, [
  104. dataRect.point2.x,
  105. dataRect.point2.y,
  106. ]);
  107. if (isNaN(point2?.[0]) || isNaN(point2?.[1])) {
  108. return null;
  109. }
  110. return {
  111. point1: {x: point1[0], y: point1[1]},
  112. point2: {x: point2[0], y: point2[1]},
  113. };
  114. }
  115. /**
  116. * Given a point on a source rectangle, map it to the corresponding point on the
  117. * destination rectangle. Assumes that the two rectangles are related by a simple
  118. * transformation containing only translations and scaling.
  119. */
  120. export function mapPoint(
  121. point: Point,
  122. srcRect: Rectangle,
  123. destRect: Rectangle
  124. ): Point | null {
  125. if (
  126. srcRect.point1.x === srcRect.point2.x ||
  127. srcRect.point1.y === srcRect.point2.y ||
  128. destRect.point1.x === destRect.point2.x ||
  129. destRect.point1.y === destRect.point2.y
  130. ) {
  131. return null;
  132. }
  133. const xPercentage =
  134. (point.x - srcRect.point1.x) / (srcRect.point2.x - srcRect.point1.x);
  135. const yPercentage =
  136. (point.y - srcRect.point1.y) / (srcRect.point2.y - srcRect.point1.y);
  137. return {
  138. x: destRect.point1.x + (destRect.point2.x - destRect.point1.x) * xPercentage,
  139. y: destRect.point1.y + (destRect.point2.y - destRect.point1.y) * yPercentage,
  140. };
  141. }
  142. export function isMissingVitalsData(
  143. vitalsData: VitalsData | null,
  144. allVitals: WebVital[]
  145. ): boolean {
  146. if (!vitalsData || allVitals.some(vital => !vitalsData[vital])) {
  147. return true;
  148. }
  149. const measurementsWithoutCounts = Object.values(vitalsData).filter(
  150. vitalObj => vitalObj.total === 0
  151. );
  152. return measurementsWithoutCounts.length > 0;
  153. }