charts.tsx 6.1 KB

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