charts.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import {LegendComponentOption} from 'echarts';
  2. import {t} from 'sentry/locale';
  3. import {Series} from 'sentry/types/echarts';
  4. import {defined} from 'sentry/utils';
  5. import {aggregateOutputType} from 'sentry/utils/discover/fields';
  6. import {
  7. DAY,
  8. formatAbbreviatedNumber,
  9. formatPercentage,
  10. getDuration,
  11. HOUR,
  12. MINUTE,
  13. SECOND,
  14. WEEK,
  15. } from 'sentry/utils/formatters';
  16. /**
  17. * Formatter for chart tooltips that handle a variety of discover and metrics result values.
  18. * If the result is metric values, the value can be of type number or null
  19. */
  20. export function tooltipFormatter(value: number | null, seriesName: string = ''): string {
  21. if (!defined(value)) {
  22. return '\u2014';
  23. }
  24. switch (aggregateOutputType(seriesName)) {
  25. case 'integer':
  26. case 'number':
  27. return value.toLocaleString();
  28. case 'percentage':
  29. return formatPercentage(value, 2);
  30. case 'duration':
  31. return getDuration(value / 1000, 2, true);
  32. default:
  33. return value.toString();
  34. }
  35. }
  36. /**
  37. * Formatter for chart axis labels that handle a variety of discover result values
  38. * This function is *very similar* to tooltipFormatter but outputs data with less precision.
  39. */
  40. export function axisLabelFormatter(
  41. value: number,
  42. seriesName: string,
  43. abbreviation: boolean = false,
  44. durationUnit?: number
  45. ): string {
  46. switch (aggregateOutputType(seriesName)) {
  47. case 'integer':
  48. case 'number':
  49. return abbreviation ? formatAbbreviatedNumber(value) : value.toLocaleString();
  50. case 'percentage':
  51. return formatPercentage(value, 0);
  52. case 'duration':
  53. return axisDuration(value, durationUnit);
  54. default:
  55. return value.toString();
  56. }
  57. }
  58. /**
  59. * Specialized duration formatting for axis labels.
  60. * In that context we are ok sacrificing accuracy for more
  61. * consistent sizing.
  62. *
  63. * @param value Number of milliseconds to format.
  64. */
  65. export function axisDuration(value: number, durationUnit?: number): string {
  66. durationUnit ??= categorizeDuration(value);
  67. if (value === 0) {
  68. return '0';
  69. }
  70. switch (durationUnit) {
  71. case WEEK: {
  72. const label = (value / WEEK).toFixed(0);
  73. return t('%swk', label);
  74. }
  75. case DAY: {
  76. const label = (value / DAY).toFixed(0);
  77. return t('%sd', label);
  78. }
  79. case HOUR: {
  80. const label = (value / HOUR).toFixed(0);
  81. return t('%shr', label);
  82. }
  83. case MINUTE: {
  84. const label = (value / MINUTE).toFixed(0);
  85. return t('%smin', label);
  86. }
  87. case SECOND: {
  88. const label = (value / SECOND).toFixed(0);
  89. return t('%ss', label);
  90. }
  91. default:
  92. const label = value.toFixed(0);
  93. return t('%sms', label);
  94. }
  95. }
  96. /**
  97. * Given an array of series and an eCharts legend object,
  98. * finds the range of y values (min and max) based on which series is selected in the legend
  99. * Assumes series[0] > series[1] > ...
  100. * @param series Array of eCharts series
  101. * @param legend eCharts legend object
  102. * @returns
  103. */
  104. export function findRangeOfMultiSeries(series: Series[], legend?: LegendComponentOption) {
  105. let range: {max: number; min: number} | undefined;
  106. if (series[0]?.data) {
  107. let minSeries = series[0];
  108. let maxSeries;
  109. series.forEach(({seriesName}, idx) => {
  110. if (legend?.selected?.[seriesName] !== false) {
  111. minSeries = series[idx];
  112. maxSeries ??= series[idx];
  113. }
  114. });
  115. if (maxSeries?.data) {
  116. const max = Math.max(...maxSeries.data.map(({value}) => value));
  117. const min = Math.min(
  118. ...minSeries.data.map(({value}) => value).filter(value => !!value)
  119. );
  120. range = {max, min};
  121. }
  122. }
  123. return range;
  124. }
  125. /**
  126. * Given a eCharts series and legend, returns the unit to be used on the yAxis for a duration chart
  127. * @param series eCharts series array
  128. * @param legend eCharts legend object
  129. * @returns
  130. */
  131. export function getDurationUnit(
  132. series: Series[],
  133. legend?: LegendComponentOption
  134. ): number {
  135. let durationUnit = 0;
  136. const range = findRangeOfMultiSeries(series, legend);
  137. if (range) {
  138. const avg = (range.max + range.min) / 2;
  139. durationUnit = categorizeDuration((range.max - range.min) / 5); // avg of 5 yAxis ticks per chart
  140. const numOfDigits = (avg / durationUnit).toFixed(0).length;
  141. if (numOfDigits > 6) {
  142. durationUnit = categorizeDuration(avg);
  143. }
  144. }
  145. return durationUnit;
  146. }
  147. /**
  148. * Categorizes the duration by Second, Minute, Hour, etc
  149. * Ex) categorizeDuration(1200) = MINUTE
  150. * @param value Duration in ms
  151. */
  152. export function categorizeDuration(value): number {
  153. if (value >= WEEK) {
  154. return WEEK;
  155. }
  156. if (value >= DAY) {
  157. return DAY;
  158. }
  159. if (value >= HOUR) {
  160. return HOUR;
  161. }
  162. if (value >= MINUTE) {
  163. return MINUTE;
  164. }
  165. if (value >= SECOND) {
  166. return SECOND;
  167. }
  168. return 1;
  169. }