charts.spec.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import {LegendComponentOption} from 'echarts';
  2. import {Series} from 'sentry/types/echarts';
  3. import {
  4. axisLabelFormatter,
  5. axisLabelFormatterUsingAggregateOutputType,
  6. categorizeDuration,
  7. findRangeOfMultiSeries,
  8. getDurationUnit,
  9. tooltipFormatter,
  10. tooltipFormatterUsingAggregateOutputType,
  11. } from 'sentry/utils/discover/charts';
  12. import {aggregateOutputType} from 'sentry/utils/discover/fields';
  13. import {HOUR, MINUTE, SECOND} from 'sentry/utils/formatters';
  14. describe('tooltipFormatter()', () => {
  15. it('formats values', () => {
  16. const cases: [string, number, string][] = [
  17. // function, input, expected
  18. ['count()', 0.1, '0.1'],
  19. ['avg(thing)', 0.125126, '0.125'],
  20. ['failure_rate()', 0.66123, '66.12%'],
  21. ['p50()', 100, '100.00ms'],
  22. ['p50()', 100.23, '100.23ms'],
  23. ['p50()', 1200, '1.20s'],
  24. ['p50()', 86400000, '1.00d'],
  25. ];
  26. for (const scenario of cases) {
  27. expect(tooltipFormatter(scenario[1], aggregateOutputType(scenario[0]))).toEqual(
  28. scenario[2]
  29. );
  30. }
  31. });
  32. });
  33. describe('tooltipFormatterUsingAggregateOutputType()', () => {
  34. it('formats values', () => {
  35. const cases: [string, number, string][] = [
  36. // function, input, expected
  37. ['number', 0.1, '0.1'],
  38. ['integer', 0.125, '0.125'],
  39. ['percentage', 0.6612, '66.12%'],
  40. ['duration', 321, '321.00ms'],
  41. ['size', 416 * 1024, '416.0 KiB'],
  42. ['', 444, '444'],
  43. ];
  44. for (const scenario of cases) {
  45. expect(tooltipFormatterUsingAggregateOutputType(scenario[1], scenario[0])).toEqual(
  46. scenario[2]
  47. );
  48. }
  49. });
  50. });
  51. describe('axisLabelFormatter()', () => {
  52. it('formats values', () => {
  53. const cases: [string, number, string][] = [
  54. // type, input, expected
  55. ['count()', 0.1, '0.1'],
  56. ['avg(thing)', 0.125126, '0.125'],
  57. ['failure_rate()', 0.66123, '66%'],
  58. ['p50()', 100, '100ms'],
  59. ['p50()', 541, '541ms'],
  60. ['p50()', 1200, '1s'],
  61. ['p50()', 60000, '1min'],
  62. ['p50()', 120000, '2min'],
  63. ['p50()', 3600000, '1hr'],
  64. ['p50()', 86400000, '1d'],
  65. ];
  66. for (const scenario of cases) {
  67. expect(axisLabelFormatter(scenario[1], aggregateOutputType(scenario[0]))).toEqual(
  68. scenario[2]
  69. );
  70. }
  71. });
  72. describe('When a duration unit is passed', () => {
  73. const getAxisLabels = (axisValues: number[], durationUnit: number) => {
  74. return axisValues.map(value =>
  75. axisLabelFormatter(value, 'duration', undefined, durationUnit)
  76. );
  77. };
  78. const generateDurationUnit = (axisValues: number[]) => {
  79. const max = Math.max(...axisValues);
  80. const min = Math.min(...axisValues);
  81. return categorizeDuration((max + min) * 0.5);
  82. };
  83. it('should not contain duplicate axis labels', () => {
  84. const axisValues = [40 * SECOND, 50 * SECOND, 60 * SECOND, 70 * SECOND];
  85. const durationUnit = generateDurationUnit(axisValues);
  86. const labels = getAxisLabels(axisValues, durationUnit);
  87. expect(labels.length).toBe(new Set(labels).size);
  88. });
  89. it('should use the same duration unit', () => {
  90. const axisValues = [50 * MINUTE, 150 * MINUTE, 250 * MINUTE, 350 * MINUTE];
  91. const durationUnit = generateDurationUnit(axisValues);
  92. const labels = getAxisLabels(axisValues, durationUnit);
  93. expect(labels.length).toBe(labels.filter(label => label.endsWith('hr')).length);
  94. });
  95. });
  96. });
  97. describe('axisLabelFormatterUsingAggregateOutputType()', () => {
  98. it('formats values', () => {
  99. const cases: [string, number, string][] = [
  100. // type, input, expected
  101. ['number', 0.1, '0.1'],
  102. ['integer', 0.125, '0.125'],
  103. ['percentage', 0.6612, '66%'],
  104. ['duration', 321, '321ms'],
  105. ['size', 416 * 1024, '416 KiB'],
  106. ['', 444, '444'],
  107. ];
  108. for (const scenario of cases) {
  109. expect(
  110. axisLabelFormatterUsingAggregateOutputType(scenario[1], scenario[0])
  111. ).toEqual(scenario[2]);
  112. }
  113. });
  114. });
  115. describe('findRangeOfMultiSeries()', () => {
  116. const series: Series[] = [
  117. {
  118. seriesName: 'p100()',
  119. data: [
  120. {name: 1, value: 2300},
  121. {name: 2, value: 1900},
  122. {name: 3, value: 1950},
  123. ],
  124. },
  125. {
  126. seriesName: 'p95()',
  127. data: [
  128. {name: 1, value: 300},
  129. {name: 2, value: 280},
  130. {name: 3, value: 290},
  131. ],
  132. },
  133. {
  134. seriesName: 'p50()',
  135. data: [
  136. {name: 1, value: 100},
  137. {name: 2, value: 50},
  138. {name: 3, value: 80},
  139. ],
  140. },
  141. ];
  142. it('should find min and max when no items selected in legend', () => {
  143. expect(findRangeOfMultiSeries(series)).toStrictEqual({max: 2300, min: 50});
  144. });
  145. it('should find min and max when series has no data', () => {
  146. const noDataSeries: Series[] = [
  147. {
  148. seriesName: 'p100()',
  149. data: [
  150. {name: 1, value: 2300},
  151. {name: 2, value: 1900},
  152. {name: 3, value: 1950},
  153. ],
  154. },
  155. {
  156. seriesName: 'p95()',
  157. data: [],
  158. },
  159. {
  160. seriesName: 'p50()',
  161. data: [],
  162. },
  163. ];
  164. expect(findRangeOfMultiSeries(noDataSeries)).toStrictEqual({max: 2300, min: 1900});
  165. });
  166. it('should not find range if no items selected', () => {
  167. const legend: LegendComponentOption = {
  168. selected: {'p100()': false, 'p95()': false, 'p50()': false},
  169. };
  170. expect(findRangeOfMultiSeries(series, legend)).toStrictEqual(undefined);
  171. });
  172. it('should ignore p100 series if not selected', () => {
  173. const legend: LegendComponentOption = {
  174. selected: {'p100()': false},
  175. };
  176. expect(findRangeOfMultiSeries(series, legend)).toStrictEqual({max: 300, min: 50});
  177. });
  178. it('should ignore p50 series if not selected', () => {
  179. const legend: LegendComponentOption = {
  180. selected: {'p50()': false},
  181. };
  182. expect(findRangeOfMultiSeries(series, legend)).toStrictEqual({max: 2300, min: 280});
  183. });
  184. it('should display p100 value if selected and in legend object', () => {
  185. const legend: LegendComponentOption = {
  186. selected: {'p100()': true},
  187. };
  188. expect(findRangeOfMultiSeries(series, legend)).toStrictEqual({max: 2300, min: 50});
  189. });
  190. });
  191. describe('getDurationUnit()', () => {
  192. const MILLISECOND = 1;
  193. const generateSeries = (axisValues: number[]): Series[] => {
  194. return [
  195. {
  196. seriesName: 'p100()',
  197. data: axisValues.map((val, idx) => ({name: idx, value: val})),
  198. },
  199. ];
  200. };
  201. it('should return ms during transtion between ms to s', () => {
  202. const series = generateSeries([700, 800, 900, SECOND, 1.1 * SECOND]);
  203. expect(getDurationUnit(series)).toBe(MILLISECOND);
  204. });
  205. it('should return s during transtion between s to min', () => {
  206. const series = generateSeries([40 * SECOND, 50 * SECOND, MINUTE, 1.3 * MINUTE]);
  207. expect(getDurationUnit(series)).toBe(SECOND);
  208. });
  209. it('should return ms if y range is small', () => {
  210. const series = generateSeries([1000, 1050, 1100, 1150, 1200]);
  211. expect(getDurationUnit(series)).toBe(MILLISECOND);
  212. });
  213. it('should return min if yAxis range >= 5 min', () => {
  214. const series = generateSeries([1 * MINUTE, 2 * MINUTE, 4 * MINUTE, 6 * MINUTE]);
  215. expect(getDurationUnit(series)).toBe(MINUTE);
  216. });
  217. it('should return sec if yAxis range < 5 min', () => {
  218. const series = generateSeries([1 * MINUTE, 2 * MINUTE, 4 * MINUTE, 5 * MINUTE]);
  219. expect(getDurationUnit(series)).toBe(SECOND);
  220. });
  221. it('should use second with ms yAxis range if label length is long', () => {
  222. const series = generateSeries([4 * HOUR, 4.0001 * HOUR, 4.0002 * HOUR]);
  223. const durationUnit = getDurationUnit(series);
  224. const numOfDigits = ((4.0001 * HOUR) / durationUnit).toFixed(0).length;
  225. expect(numOfDigits).toBeLessThan(6);
  226. expect(durationUnit).not.toBe(MILLISECOND);
  227. });
  228. it('Should return ms if all values are 0', () => {
  229. const series = generateSeries([0, 0, 0]);
  230. const durationUnit = getDurationUnit(series);
  231. expect(durationUnit).toBe(MILLISECOND);
  232. });
  233. });