charts.tsx 5.5 KB

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