charts.spec.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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 is unordered', () => {
  146. const mixedSeries = [series[1], series[0], series[2]];
  147. expect(findRangeOfMultiSeries(mixedSeries)).toStrictEqual({max: 2300, min: 50});
  148. });
  149. it('should find min and max when one of the series has all 0 values', () => {
  150. const mixedSeries = [
  151. {
  152. seriesName: 'p75(spans.db)',
  153. data: [
  154. {name: 1, value: 0},
  155. {name: 2, value: 0},
  156. {name: 3, value: 0},
  157. ],
  158. },
  159. series[1],
  160. series[0],
  161. series[2],
  162. ];
  163. expect(findRangeOfMultiSeries(mixedSeries)).toStrictEqual({max: 2300, min: 0});
  164. });
  165. it('should find min and max when one of the series has negative values', () => {
  166. const mixedSeries = [
  167. {
  168. seriesName: 'p75(custom.measurement)',
  169. data: [
  170. {name: 1, value: 10},
  171. {name: 2, value: -10},
  172. {name: 3, value: 10},
  173. ],
  174. },
  175. series[1],
  176. series[0],
  177. series[2],
  178. ];
  179. expect(findRangeOfMultiSeries(mixedSeries)).toStrictEqual({max: 2300, min: -10});
  180. });
  181. it('should find min and max when series has no data', () => {
  182. const noDataSeries: Series[] = [
  183. {
  184. seriesName: 'p100()',
  185. data: [
  186. {name: 1, value: 2300},
  187. {name: 2, value: 1900},
  188. {name: 3, value: 1950},
  189. ],
  190. },
  191. {
  192. seriesName: 'p95()',
  193. data: [],
  194. },
  195. {
  196. seriesName: 'p50()',
  197. data: [],
  198. },
  199. ];
  200. expect(findRangeOfMultiSeries(noDataSeries)).toStrictEqual({max: 2300, min: 1900});
  201. });
  202. it('should not find range if no items selected', () => {
  203. const legend: LegendComponentOption = {
  204. selected: {'p100()': false, 'p95()': false, 'p50()': false},
  205. };
  206. expect(findRangeOfMultiSeries(series, legend)).toStrictEqual(undefined);
  207. });
  208. it('should ignore p100 series if not selected', () => {
  209. const legend: LegendComponentOption = {
  210. selected: {'p100()': false},
  211. };
  212. expect(findRangeOfMultiSeries(series, legend)).toStrictEqual({max: 300, min: 50});
  213. });
  214. it('should ignore p50 series if not selected', () => {
  215. const legend: LegendComponentOption = {
  216. selected: {'p50()': false},
  217. };
  218. expect(findRangeOfMultiSeries(series, legend)).toStrictEqual({max: 2300, min: 280});
  219. });
  220. it('should display p100 value if selected and in legend object', () => {
  221. const legend: LegendComponentOption = {
  222. selected: {'p100()': true},
  223. };
  224. expect(findRangeOfMultiSeries(series, legend)).toStrictEqual({max: 2300, min: 50});
  225. });
  226. });
  227. describe('getDurationUnit()', () => {
  228. const MILLISECOND = 1;
  229. const generateSeries = (axisValues: number[]): Series[] => {
  230. return [
  231. {
  232. seriesName: 'p100()',
  233. data: axisValues.map((val, idx) => ({name: idx, value: val})),
  234. },
  235. ];
  236. };
  237. it('should return ms during transtion between ms to s', () => {
  238. const series = generateSeries([700, 800, 900, SECOND, 1.1 * SECOND]);
  239. expect(getDurationUnit(series)).toBe(MILLISECOND);
  240. });
  241. it('should return s during transtion between s to min', () => {
  242. const series = generateSeries([40 * SECOND, 50 * SECOND, MINUTE, 1.3 * MINUTE]);
  243. expect(getDurationUnit(series)).toBe(SECOND);
  244. });
  245. it('should return ms if y range is small', () => {
  246. const series = generateSeries([1000, 1050, 1100, 1150, 1200]);
  247. expect(getDurationUnit(series)).toBe(MILLISECOND);
  248. });
  249. it('should return min if yAxis range >= 5 min', () => {
  250. const series = generateSeries([1 * MINUTE, 2 * MINUTE, 4 * MINUTE, 6 * MINUTE]);
  251. expect(getDurationUnit(series)).toBe(MINUTE);
  252. });
  253. it('should return sec if yAxis range < 5 min', () => {
  254. const series = generateSeries([1 * MINUTE, 2 * MINUTE, 4 * MINUTE, 5 * MINUTE]);
  255. expect(getDurationUnit(series)).toBe(SECOND);
  256. });
  257. it('should use second with ms yAxis range if label length is long', () => {
  258. const series = generateSeries([4 * HOUR, 4.0001 * HOUR, 4.0002 * HOUR]);
  259. const durationUnit = getDurationUnit(series);
  260. const numOfDigits = ((4.0001 * HOUR) / durationUnit).toFixed(0).length;
  261. expect(numOfDigits).toBeLessThan(6);
  262. expect(durationUnit).not.toBe(MILLISECOND);
  263. });
  264. it('Should return ms if all values are 0', () => {
  265. const series = generateSeries([0, 0, 0]);
  266. const durationUnit = getDurationUnit(series);
  267. expect(durationUnit).toBe(MILLISECOND);
  268. });
  269. });