utils.tsx 4.8 KB

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