utils.tsx 5.0 KB

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