series.test.ts 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250
  1. /**
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. import { SortSeriesType } from '@superset-ui/chart-controls';
  20. import {
  21. AxisType,
  22. DataRecord,
  23. getNumberFormatter,
  24. getTimeFormatter,
  25. supersetTheme as theme,
  26. } from '@superset-ui/core';
  27. import { GenericDataType } from '@apache-superset/core/api/core';
  28. import {
  29. calculateLowerLogTick,
  30. dedupSeries,
  31. extractGroupbyLabel,
  32. extractSeries,
  33. extractShowValueIndexes,
  34. extractTooltipKeys,
  35. formatSeriesName,
  36. getAxisType,
  37. getChartPadding,
  38. getLegendProps,
  39. getOverMaxHiddenFormatter,
  40. getMinAndMaxFromBounds,
  41. sanitizeHtml,
  42. sortAndFilterSeries,
  43. sortRows,
  44. getTimeCompareStackId,
  45. } from '../../src/utils/series';
  46. import {
  47. EchartsTimeseriesSeriesType,
  48. LegendOrientation,
  49. LegendType,
  50. } from '../../src/types';
  51. import { defaultLegendPadding } from '../../src/defaults';
  52. import { NULL_STRING } from '../../src/constants';
  53. const expectedThemeProps = {
  54. selector: ['all', 'inverse'],
  55. selected: undefined,
  56. selectorLabel: {
  57. fontFamily: theme.fontFamily,
  58. fontSize: theme.fontSizeSM,
  59. color: theme.colorText,
  60. borderColor: theme.colorBorder,
  61. },
  62. };
  63. const sortData: DataRecord[] = [
  64. { my_x_axis: 'abc', x: 1, y: 0, z: 2 },
  65. { my_x_axis: 'foo', x: null, y: 10, z: 5 },
  66. { my_x_axis: null, x: 4, y: 3, z: 7 },
  67. ];
  68. const sortDataWithNumbers: DataRecord[] = [
  69. {
  70. my_x_axis: 'my_axis',
  71. '9. September': 6,
  72. 6: 1,
  73. '11. November': 8,
  74. 8: 2,
  75. '10. October': 1,
  76. 10: 4,
  77. '3. March': 2,
  78. '8. August': 6,
  79. 2: 1,
  80. 12: 3,
  81. 9: 1,
  82. '1. January': 1,
  83. '4. April': 12,
  84. '2. February': 9,
  85. 5: 4,
  86. 3: 1,
  87. 11: 2,
  88. '12. December': 4,
  89. 1: 7,
  90. '6. June': 1,
  91. 4: 5,
  92. 7: 2,
  93. c: 0,
  94. '7. July': 2,
  95. d: 0,
  96. '5. May': 4,
  97. a: 1,
  98. },
  99. ];
  100. const totalStackedValues = [3, 15, 14];
  101. test('sortRows by name ascending', () => {
  102. expect(
  103. sortRows(
  104. sortData,
  105. totalStackedValues,
  106. 'my_x_axis',
  107. SortSeriesType.Name,
  108. true,
  109. ),
  110. ).toEqual([
  111. { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
  112. { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
  113. { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
  114. ]);
  115. });
  116. test('sortRows by name descending', () => {
  117. expect(
  118. sortRows(
  119. sortData,
  120. totalStackedValues,
  121. 'my_x_axis',
  122. SortSeriesType.Name,
  123. false,
  124. ),
  125. ).toEqual([
  126. { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
  127. { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
  128. { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
  129. ]);
  130. });
  131. test('sortRows by sum ascending', () => {
  132. expect(
  133. sortRows(
  134. sortData,
  135. totalStackedValues,
  136. 'my_x_axis',
  137. SortSeriesType.Sum,
  138. true,
  139. ),
  140. ).toEqual([
  141. { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
  142. { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
  143. { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
  144. ]);
  145. });
  146. test('sortRows by sum descending', () => {
  147. expect(
  148. sortRows(
  149. sortData,
  150. totalStackedValues,
  151. 'my_x_axis',
  152. SortSeriesType.Sum,
  153. false,
  154. ),
  155. ).toEqual([
  156. { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
  157. { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
  158. { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
  159. ]);
  160. });
  161. test('sortRows by avg ascending', () => {
  162. expect(
  163. sortRows(
  164. sortData,
  165. totalStackedValues,
  166. 'my_x_axis',
  167. SortSeriesType.Avg,
  168. true,
  169. ),
  170. ).toEqual([
  171. { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
  172. { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
  173. { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
  174. ]);
  175. });
  176. test('sortRows by avg descending', () => {
  177. expect(
  178. sortRows(
  179. sortData,
  180. totalStackedValues,
  181. 'my_x_axis',
  182. SortSeriesType.Avg,
  183. false,
  184. ),
  185. ).toEqual([
  186. { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
  187. { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
  188. { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
  189. ]);
  190. });
  191. test('sortRows by min ascending', () => {
  192. expect(
  193. sortRows(
  194. sortData,
  195. totalStackedValues,
  196. 'my_x_axis',
  197. SortSeriesType.Min,
  198. true,
  199. ),
  200. ).toEqual([
  201. { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
  202. { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
  203. { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
  204. ]);
  205. });
  206. test('sortRows by min descending', () => {
  207. expect(
  208. sortRows(
  209. sortData,
  210. totalStackedValues,
  211. 'my_x_axis',
  212. SortSeriesType.Min,
  213. false,
  214. ),
  215. ).toEqual([
  216. { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
  217. { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
  218. { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
  219. ]);
  220. });
  221. test('sortRows by max ascending', () => {
  222. expect(
  223. sortRows(
  224. sortData,
  225. totalStackedValues,
  226. 'my_x_axis',
  227. SortSeriesType.Min,
  228. true,
  229. ),
  230. ).toEqual([
  231. { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
  232. { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
  233. { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
  234. ]);
  235. });
  236. test('sortRows by max descending', () => {
  237. expect(
  238. sortRows(
  239. sortData,
  240. totalStackedValues,
  241. 'my_x_axis',
  242. SortSeriesType.Min,
  243. false,
  244. ),
  245. ).toEqual([
  246. { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
  247. { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
  248. { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
  249. ]);
  250. });
  251. test('sortAndFilterSeries by min ascending', () => {
  252. expect(
  253. sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Min, true),
  254. ).toEqual(['y', 'x', 'z']);
  255. });
  256. test('sortAndFilterSeries by min descending', () => {
  257. expect(
  258. sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Min, false),
  259. ).toEqual(['z', 'x', 'y']);
  260. });
  261. test('sortAndFilterSeries by max ascending', () => {
  262. expect(
  263. sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Max, true),
  264. ).toEqual(['x', 'z', 'y']);
  265. });
  266. test('sortAndFilterSeries by max descending', () => {
  267. expect(
  268. sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Max, false),
  269. ).toEqual(['y', 'z', 'x']);
  270. });
  271. test('sortAndFilterSeries by avg ascending', () => {
  272. expect(
  273. sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Avg, true),
  274. ).toEqual(['x', 'y', 'z']);
  275. });
  276. test('sortAndFilterSeries by avg descending', () => {
  277. expect(
  278. sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Avg, false),
  279. ).toEqual(['z', 'y', 'x']);
  280. });
  281. test('sortAndFilterSeries by sum ascending', () => {
  282. expect(
  283. sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Sum, true),
  284. ).toEqual(['x', 'y', 'z']);
  285. });
  286. test('sortAndFilterSeries by sum descending', () => {
  287. expect(
  288. sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Sum, false),
  289. ).toEqual(['z', 'y', 'x']);
  290. });
  291. test('sortAndFilterSeries by name ascending', () => {
  292. expect(
  293. sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Name, true),
  294. ).toEqual(['x', 'y', 'z']);
  295. });
  296. test('sortAndFilterSeries by name descending', () => {
  297. expect(
  298. sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Name, false),
  299. ).toEqual(['z', 'y', 'x']);
  300. });
  301. test('sortAndFilterSeries by name with numbers asc', () => {
  302. expect(
  303. sortAndFilterSeries(
  304. sortDataWithNumbers,
  305. 'my_x_axis',
  306. [],
  307. SortSeriesType.Name,
  308. true,
  309. ),
  310. ).toEqual([
  311. '1',
  312. '1. January',
  313. '2',
  314. '2. February',
  315. '3',
  316. '3. March',
  317. '4',
  318. '4. April',
  319. '5',
  320. '5. May',
  321. '6',
  322. '6. June',
  323. '7',
  324. '7. July',
  325. '8',
  326. '8. August',
  327. '9',
  328. '9. September',
  329. '10',
  330. '10. October',
  331. '11',
  332. '11. November',
  333. '12',
  334. '12. December',
  335. 'a',
  336. 'c',
  337. 'd',
  338. ]);
  339. });
  340. test('sortAndFilterSeries by name with numbers desc', () => {
  341. expect(
  342. sortAndFilterSeries(
  343. sortDataWithNumbers,
  344. 'my_x_axis',
  345. [],
  346. SortSeriesType.Name,
  347. false,
  348. ),
  349. ).toEqual([
  350. 'd',
  351. 'c',
  352. 'a',
  353. '12. December',
  354. '12',
  355. '11. November',
  356. '11',
  357. '10. October',
  358. '10',
  359. '9. September',
  360. '9',
  361. '8. August',
  362. '8',
  363. '7. July',
  364. '7',
  365. '6. June',
  366. '6',
  367. '5. May',
  368. '5',
  369. '4. April',
  370. '4',
  371. '3. March',
  372. '3',
  373. '2. February',
  374. '2',
  375. '1. January',
  376. '1',
  377. ]);
  378. });
  379. describe('extractSeries', () => {
  380. it('should generate a valid ECharts timeseries series object', () => {
  381. const data = [
  382. {
  383. __timestamp: '2000-01-01',
  384. Hulk: null,
  385. abc: 2,
  386. },
  387. {
  388. __timestamp: '2000-02-01',
  389. Hulk: 2,
  390. abc: 10,
  391. },
  392. {
  393. __timestamp: '2000-03-01',
  394. Hulk: 1,
  395. abc: 5,
  396. },
  397. ];
  398. const totalStackedValues = [2, 12, 6];
  399. expect(extractSeries(data, { totalStackedValues })).toEqual([
  400. [
  401. {
  402. id: 'Hulk',
  403. name: 'Hulk',
  404. data: [
  405. ['2000-01-01', null],
  406. ['2000-02-01', 2],
  407. ['2000-03-01', 1],
  408. ],
  409. },
  410. {
  411. id: 'abc',
  412. name: 'abc',
  413. data: [
  414. ['2000-01-01', 2],
  415. ['2000-02-01', 10],
  416. ['2000-03-01', 5],
  417. ],
  418. },
  419. ],
  420. totalStackedValues,
  421. 1,
  422. ]);
  423. });
  424. it('should remove rows that have a null x-value', () => {
  425. const data = [
  426. {
  427. x: 1,
  428. Hulk: null,
  429. abc: 2,
  430. },
  431. {
  432. x: null,
  433. Hulk: 2,
  434. abc: 10,
  435. },
  436. {
  437. x: 2,
  438. Hulk: 1,
  439. abc: 5,
  440. },
  441. ];
  442. const totalStackedValues = [3, 12, 8];
  443. expect(
  444. extractSeries(data, {
  445. totalStackedValues,
  446. xAxis: 'x',
  447. removeNulls: true,
  448. }),
  449. ).toEqual([
  450. [
  451. {
  452. id: 'Hulk',
  453. name: 'Hulk',
  454. data: [[2, 1]],
  455. },
  456. {
  457. id: 'abc',
  458. name: 'abc',
  459. data: [
  460. [1, 2],
  461. [2, 5],
  462. ],
  463. },
  464. ],
  465. totalStackedValues,
  466. 1,
  467. ]);
  468. });
  469. it('should convert NULL x-values to NULL_STRING for categorical axis', () => {
  470. const data = [
  471. {
  472. browser: 'Firefox',
  473. count: 5,
  474. },
  475. {
  476. browser: null,
  477. count: 10,
  478. },
  479. {
  480. browser: 'Chrome',
  481. count: 8,
  482. },
  483. ];
  484. expect(
  485. extractSeries(data, {
  486. xAxis: 'browser',
  487. xAxisType: AxisType.Category,
  488. }),
  489. ).toEqual([
  490. [
  491. {
  492. id: 'count',
  493. name: 'count',
  494. data: [
  495. ['Firefox', 5],
  496. [NULL_STRING, 10],
  497. ['Chrome', 8],
  498. ],
  499. },
  500. ],
  501. [],
  502. 5,
  503. ]);
  504. });
  505. it('should do missing value imputation', () => {
  506. const data = [
  507. {
  508. __timestamp: '2000-01-01',
  509. abc: null,
  510. },
  511. {
  512. __timestamp: '2000-02-01',
  513. abc: null,
  514. },
  515. {
  516. __timestamp: '2000-03-01',
  517. abc: 1,
  518. },
  519. {
  520. __timestamp: '2000-04-01',
  521. abc: null,
  522. },
  523. {
  524. __timestamp: '2000-05-01',
  525. abc: null,
  526. },
  527. {
  528. __timestamp: '2000-06-01',
  529. abc: null,
  530. },
  531. {
  532. __timestamp: '2000-07-01',
  533. abc: 2,
  534. },
  535. {
  536. __timestamp: '2000-08-01',
  537. abc: 3,
  538. },
  539. {
  540. __timestamp: '2000-09-01',
  541. abc: null,
  542. },
  543. {
  544. __timestamp: '2000-10-01',
  545. abc: null,
  546. },
  547. ];
  548. const totalStackedValues = [0, 0, 1, 0, 0, 0, 2, 3, 0, 0];
  549. expect(
  550. extractSeries(data, { totalStackedValues, fillNeighborValue: 0 }),
  551. ).toEqual([
  552. [
  553. {
  554. id: 'abc',
  555. name: 'abc',
  556. data: [
  557. ['2000-01-01', null],
  558. ['2000-02-01', 0],
  559. ['2000-03-01', 1],
  560. ['2000-04-01', 0],
  561. ['2000-05-01', null],
  562. ['2000-06-01', 0],
  563. ['2000-07-01', 2],
  564. ['2000-08-01', 3],
  565. ['2000-09-01', 0],
  566. ['2000-10-01', null],
  567. ],
  568. },
  569. ],
  570. totalStackedValues,
  571. 1,
  572. ]);
  573. });
  574. });
  575. describe('extractGroupbyLabel', () => {
  576. it('should join together multiple groupby labels', () => {
  577. expect(
  578. extractGroupbyLabel({
  579. datum: { a: 'abc', b: 'qwerty' },
  580. groupby: ['a', 'b'],
  581. }),
  582. ).toEqual('abc, qwerty');
  583. });
  584. it('should handle a single groupby', () => {
  585. expect(
  586. extractGroupbyLabel({ datum: { xyz: 'qqq' }, groupby: ['xyz'] }),
  587. ).toEqual('qqq');
  588. });
  589. it('should handle mixed types', () => {
  590. expect(
  591. extractGroupbyLabel({
  592. datum: { strcol: 'abc', intcol: 123, floatcol: 0.123, boolcol: true },
  593. groupby: ['strcol', 'intcol', 'floatcol', 'boolcol'],
  594. }),
  595. ).toEqual('abc, 123, 0.123, true');
  596. });
  597. it('should handle null and undefined groupby', () => {
  598. expect(
  599. extractGroupbyLabel({
  600. datum: { strcol: 'abc', intcol: 123, floatcol: 0.123, boolcol: true },
  601. groupby: null,
  602. }),
  603. ).toEqual('');
  604. expect(extractGroupbyLabel({})).toEqual('');
  605. });
  606. });
  607. describe('extractShowValueIndexes', () => {
  608. it('should return the latest index for stack', () => {
  609. expect(
  610. extractShowValueIndexes(
  611. [
  612. {
  613. id: 'abc',
  614. name: 'abc',
  615. data: [
  616. ['2000-01-01', null],
  617. ['2000-02-01', 0],
  618. ['2000-03-01', 1],
  619. ['2000-04-01', 0],
  620. ['2000-05-01', null],
  621. ['2000-06-01', 0],
  622. ['2000-07-01', 2],
  623. ['2000-08-01', 3],
  624. ['2000-09-01', null],
  625. ['2000-10-01', null],
  626. ],
  627. },
  628. {
  629. id: 'def',
  630. name: 'def',
  631. data: [
  632. ['2000-01-01', null],
  633. ['2000-02-01', 0],
  634. ['2000-03-01', null],
  635. ['2000-04-01', 0],
  636. ['2000-05-01', null],
  637. ['2000-06-01', 0],
  638. ['2000-07-01', 2],
  639. ['2000-08-01', 3],
  640. ['2000-09-01', null],
  641. ['2000-10-01', 0],
  642. ],
  643. },
  644. {
  645. id: 'def',
  646. name: 'def',
  647. data: [
  648. ['2000-01-01', null],
  649. ['2000-02-01', null],
  650. ['2000-03-01', null],
  651. ['2000-04-01', null],
  652. ['2000-05-01', null],
  653. ['2000-06-01', 3],
  654. ['2000-07-01', null],
  655. ['2000-08-01', null],
  656. ['2000-09-01', null],
  657. ['2000-10-01', null],
  658. ],
  659. },
  660. ],
  661. { stack: true, onlyTotal: false, isHorizontal: false },
  662. ),
  663. ).toEqual([undefined, 1, 0, 1, undefined, 2, 1, 1, undefined, 1]);
  664. });
  665. it('should handle the negative numbers for total only', () => {
  666. expect(
  667. extractShowValueIndexes(
  668. [
  669. {
  670. id: 'abc',
  671. name: 'abc',
  672. data: [
  673. ['2000-01-01', null],
  674. ['2000-02-01', 0],
  675. ['2000-03-01', -1],
  676. ['2000-04-01', 0],
  677. ['2000-05-01', null],
  678. ['2000-06-01', 0],
  679. ['2000-07-01', -2],
  680. ['2000-08-01', -3],
  681. ['2000-09-01', null],
  682. ['2000-10-01', null],
  683. ],
  684. },
  685. {
  686. id: 'def',
  687. name: 'def',
  688. data: [
  689. ['2000-01-01', null],
  690. ['2000-02-01', 0],
  691. ['2000-03-01', null],
  692. ['2000-04-01', 0],
  693. ['2000-05-01', null],
  694. ['2000-06-01', 0],
  695. ['2000-07-01', 2],
  696. ['2000-08-01', -3],
  697. ['2000-09-01', null],
  698. ['2000-10-01', 0],
  699. ],
  700. },
  701. {
  702. id: 'def',
  703. name: 'def',
  704. data: [
  705. ['2000-01-01', null],
  706. ['2000-02-01', 0],
  707. ['2000-03-01', null],
  708. ['2000-04-01', 1],
  709. ['2000-05-01', null],
  710. ['2000-06-01', 0],
  711. ['2000-07-01', -2],
  712. ['2000-08-01', 3],
  713. ['2000-09-01', null],
  714. ['2000-10-01', 0],
  715. ],
  716. },
  717. ],
  718. { stack: true, onlyTotal: true, isHorizontal: false },
  719. ),
  720. ).toEqual([undefined, 1, 0, 2, undefined, 1, 1, 2, undefined, 1]);
  721. });
  722. });
  723. describe('formatSeriesName', () => {
  724. const numberFormatter = getNumberFormatter();
  725. const timeFormatter = getTimeFormatter();
  726. it('should handle missing values properly', () => {
  727. expect(formatSeriesName(undefined)).toEqual('<NULL>');
  728. expect(formatSeriesName(null)).toEqual('<NULL>');
  729. });
  730. it('should handle string values properly', () => {
  731. expect(formatSeriesName('abc XYZ!')).toEqual('abc XYZ!');
  732. });
  733. it('should handle boolean values properly', () => {
  734. expect(formatSeriesName(true)).toEqual('true');
  735. });
  736. it('should use default formatting for numeric values without formatter', () => {
  737. expect(formatSeriesName(12345678.9)).toEqual('12345678.9');
  738. });
  739. it('should use numberFormatter for numeric values when formatter is provided', () => {
  740. expect(formatSeriesName(12345678.9, { numberFormatter })).toEqual('12.3M');
  741. });
  742. it('should use default formatting for date values without formatter', () => {
  743. expect(formatSeriesName(new Date('2020-09-11'))).toEqual(
  744. '2020-09-11T00:00:00.000Z',
  745. );
  746. });
  747. it('should use timeFormatter for date values when formatter is provided', () => {
  748. expect(formatSeriesName(new Date('2020-09-11'), { timeFormatter })).toEqual(
  749. '2020-09-11 00:00:00',
  750. );
  751. });
  752. it('should normalize non-UTC string based timestamp', () => {
  753. const annualTimeFormatter = getTimeFormatter('%Y');
  754. expect(
  755. formatSeriesName('1995-01-01 00:00:00.000000', {
  756. timeFormatter: annualTimeFormatter,
  757. coltype: GenericDataType.Temporal,
  758. }),
  759. ).toEqual('1995');
  760. });
  761. });
  762. describe('getLegendProps', () => {
  763. it('should return the correct props for scroll type with top orientation without zoom', () => {
  764. expect(
  765. getLegendProps(
  766. LegendType.Scroll,
  767. LegendOrientation.Top,
  768. true,
  769. theme,
  770. false,
  771. ),
  772. ).toEqual({
  773. show: true,
  774. top: 0,
  775. right: 0,
  776. orient: 'horizontal',
  777. type: 'scroll',
  778. ...expectedThemeProps,
  779. });
  780. });
  781. it('should return the correct props for scroll type with top orientation with zoom', () => {
  782. expect(
  783. getLegendProps(
  784. LegendType.Scroll,
  785. LegendOrientation.Top,
  786. true,
  787. theme,
  788. true,
  789. ),
  790. ).toEqual({
  791. show: true,
  792. top: 0,
  793. right: 55,
  794. orient: 'horizontal',
  795. type: 'scroll',
  796. ...expectedThemeProps,
  797. });
  798. });
  799. it('should return the correct props for plain type with left orientation', () => {
  800. expect(
  801. getLegendProps(LegendType.Plain, LegendOrientation.Left, true, theme),
  802. ).toEqual({
  803. show: true,
  804. left: 0,
  805. orient: 'vertical',
  806. type: 'plain',
  807. ...expectedThemeProps,
  808. });
  809. });
  810. it('should return the correct props for plain type with right orientation without zoom', () => {
  811. expect(
  812. getLegendProps(
  813. LegendType.Plain,
  814. LegendOrientation.Right,
  815. false,
  816. theme,
  817. false,
  818. ),
  819. ).toEqual({
  820. show: false,
  821. right: 0,
  822. top: 0,
  823. orient: 'vertical',
  824. type: 'plain',
  825. ...expectedThemeProps,
  826. });
  827. });
  828. it('should return the correct props for plain type with right orientation with zoom', () => {
  829. expect(
  830. getLegendProps(
  831. LegendType.Plain,
  832. LegendOrientation.Right,
  833. false,
  834. theme,
  835. true,
  836. ),
  837. ).toEqual({
  838. show: false,
  839. right: 0,
  840. top: 30,
  841. orient: 'vertical',
  842. type: 'plain',
  843. ...expectedThemeProps,
  844. });
  845. });
  846. it('should return the correct props for plain type with bottom orientation', () => {
  847. expect(
  848. getLegendProps(LegendType.Plain, LegendOrientation.Bottom, false, theme),
  849. ).toEqual({
  850. show: false,
  851. bottom: 0,
  852. orient: 'horizontal',
  853. type: 'plain',
  854. ...expectedThemeProps,
  855. });
  856. });
  857. });
  858. describe('getChartPadding', () => {
  859. it('should handle top default', () => {
  860. expect(getChartPadding(true, LegendOrientation.Top)).toEqual({
  861. bottom: 0,
  862. left: 0,
  863. right: 0,
  864. top: defaultLegendPadding[LegendOrientation.Top],
  865. });
  866. });
  867. it('should handle left default', () => {
  868. expect(getChartPadding(true, LegendOrientation.Left)).toEqual({
  869. bottom: 0,
  870. left: defaultLegendPadding[LegendOrientation.Left],
  871. right: 0,
  872. top: 0,
  873. });
  874. });
  875. it('should return the default padding when show is false', () => {
  876. expect(
  877. getChartPadding(false, LegendOrientation.Left, 100, {
  878. top: 10,
  879. bottom: 20,
  880. left: 30,
  881. right: 40,
  882. }),
  883. ).toEqual({
  884. bottom: 20,
  885. left: 30,
  886. right: 40,
  887. top: 10,
  888. });
  889. });
  890. it('should return the correct padding for left orientation', () => {
  891. expect(getChartPadding(true, LegendOrientation.Left, 100)).toEqual({
  892. bottom: 0,
  893. left: 100,
  894. right: 0,
  895. top: 0,
  896. });
  897. expect(
  898. getChartPadding(true, LegendOrientation.Left, 100, undefined, true),
  899. ).toEqual({
  900. bottom: 100,
  901. left: 0,
  902. right: 0,
  903. top: 0,
  904. });
  905. });
  906. it('should return the correct padding for right orientation', () => {
  907. expect(getChartPadding(true, LegendOrientation.Right, 50)).toEqual({
  908. bottom: 0,
  909. left: 0,
  910. right: 50,
  911. top: 0,
  912. });
  913. expect(
  914. getChartPadding(true, LegendOrientation.Right, 50, undefined, true),
  915. ).toEqual({
  916. bottom: 0,
  917. left: 0,
  918. right: 50,
  919. top: 0,
  920. });
  921. });
  922. it('should return the correct padding for top orientation', () => {
  923. expect(getChartPadding(true, LegendOrientation.Top, 20)).toEqual({
  924. bottom: 0,
  925. left: 0,
  926. right: 0,
  927. top: 20,
  928. });
  929. expect(
  930. getChartPadding(true, LegendOrientation.Top, 20, undefined, true),
  931. ).toEqual({
  932. bottom: 0,
  933. left: 0,
  934. right: 0,
  935. top: 20,
  936. });
  937. });
  938. it('should return the correct padding for bottom orientation', () => {
  939. expect(getChartPadding(true, LegendOrientation.Bottom, 10)).toEqual({
  940. bottom: 10,
  941. left: 0,
  942. right: 0,
  943. top: 0,
  944. });
  945. expect(
  946. getChartPadding(true, LegendOrientation.Bottom, 10, undefined, true),
  947. ).toEqual({
  948. bottom: 0,
  949. left: 10,
  950. right: 0,
  951. top: 0,
  952. });
  953. });
  954. });
  955. describe('dedupSeries', () => {
  956. it('should deduplicate ids in series', () => {
  957. expect(
  958. dedupSeries([
  959. {
  960. id: 'foo',
  961. },
  962. {
  963. id: 'bar',
  964. },
  965. {
  966. id: 'foo',
  967. },
  968. {
  969. id: 'foo',
  970. },
  971. ]),
  972. ).toEqual([
  973. { id: 'foo' },
  974. { id: 'bar' },
  975. { id: 'foo (1)' },
  976. { id: 'foo (2)' },
  977. ]);
  978. });
  979. });
  980. describe('sanitizeHtml', () => {
  981. it('should remove html tags from series name', () => {
  982. expect(sanitizeHtml(NULL_STRING)).toEqual('&lt;NULL&gt;');
  983. });
  984. });
  985. describe('getOverMaxHiddenFormatter', () => {
  986. it('should hide value if greater than max', () => {
  987. const formatter = getOverMaxHiddenFormatter({ max: 81000 });
  988. expect(formatter.format(84500)).toEqual('');
  989. });
  990. it('should show value if less or equal than max', () => {
  991. const formatter = getOverMaxHiddenFormatter({ max: 81000 });
  992. expect(formatter.format(81000)).toEqual('81000');
  993. expect(formatter.format(50000)).toEqual('50000');
  994. });
  995. });
  996. test('calculateLowerLogTick', () => {
  997. expect(calculateLowerLogTick(1000000)).toEqual(1000000);
  998. expect(calculateLowerLogTick(456)).toEqual(100);
  999. expect(calculateLowerLogTick(100)).toEqual(100);
  1000. expect(calculateLowerLogTick(99)).toEqual(10);
  1001. expect(calculateLowerLogTick(2)).toEqual(1);
  1002. expect(calculateLowerLogTick(0.005)).toEqual(0.001);
  1003. });
  1004. test('getAxisType without forced categorical', () => {
  1005. expect(getAxisType(false, false, GenericDataType.Temporal)).toEqual(
  1006. AxisType.Time,
  1007. );
  1008. expect(getAxisType(false, false, GenericDataType.Numeric)).toEqual(
  1009. AxisType.Value,
  1010. );
  1011. expect(getAxisType(true, false, GenericDataType.Numeric)).toEqual(
  1012. AxisType.Category,
  1013. );
  1014. expect(getAxisType(false, false, GenericDataType.Boolean)).toEqual(
  1015. AxisType.Category,
  1016. );
  1017. expect(getAxisType(false, false, GenericDataType.String)).toEqual(
  1018. AxisType.Category,
  1019. );
  1020. });
  1021. test('getAxisType with forced categorical', () => {
  1022. expect(getAxisType(false, true, GenericDataType.Numeric)).toEqual(
  1023. AxisType.Category,
  1024. );
  1025. });
  1026. test('getMinAndMaxFromBounds returns empty object when not truncating', () => {
  1027. expect(
  1028. getMinAndMaxFromBounds(
  1029. AxisType.Value,
  1030. false,
  1031. 10,
  1032. 100,
  1033. EchartsTimeseriesSeriesType.Bar,
  1034. ),
  1035. ).toEqual({});
  1036. });
  1037. test('getMinAndMaxFromBounds returns empty object for categorical axis', () => {
  1038. expect(
  1039. getMinAndMaxFromBounds(
  1040. AxisType.Category,
  1041. false,
  1042. 10,
  1043. 100,
  1044. EchartsTimeseriesSeriesType.Bar,
  1045. ),
  1046. ).toEqual({});
  1047. });
  1048. test('getMinAndMaxFromBounds returns empty object for time axis', () => {
  1049. expect(
  1050. getMinAndMaxFromBounds(
  1051. AxisType.Time,
  1052. false,
  1053. 10,
  1054. 100,
  1055. EchartsTimeseriesSeriesType.Bar,
  1056. ),
  1057. ).toEqual({});
  1058. });
  1059. test('getMinAndMaxFromBounds returns dataMin/dataMax for non-bar charts', () => {
  1060. expect(
  1061. getMinAndMaxFromBounds(
  1062. AxisType.Value,
  1063. true,
  1064. undefined,
  1065. undefined,
  1066. EchartsTimeseriesSeriesType.Line,
  1067. ),
  1068. ).toEqual({
  1069. min: 'dataMin',
  1070. max: 'dataMax',
  1071. });
  1072. });
  1073. test('getMinAndMaxFromBounds returns bound without scale for non-bar charts', () => {
  1074. expect(
  1075. getMinAndMaxFromBounds(
  1076. AxisType.Value,
  1077. true,
  1078. 10,
  1079. undefined,
  1080. EchartsTimeseriesSeriesType.Line,
  1081. ),
  1082. ).toEqual({
  1083. min: 10,
  1084. max: 'dataMax',
  1085. });
  1086. });
  1087. test('getMinAndMaxFromBounds returns scale when truncating without bounds', () => {
  1088. expect(
  1089. getMinAndMaxFromBounds(
  1090. AxisType.Value,
  1091. true,
  1092. undefined,
  1093. undefined,
  1094. EchartsTimeseriesSeriesType.Bar,
  1095. ),
  1096. ).toEqual({ scale: true });
  1097. });
  1098. test('getMinAndMaxFromBounds returns automatic upper bound when truncating', () => {
  1099. expect(
  1100. getMinAndMaxFromBounds(
  1101. AxisType.Value,
  1102. true,
  1103. 10,
  1104. undefined,
  1105. EchartsTimeseriesSeriesType.Bar,
  1106. ),
  1107. ).toEqual({
  1108. min: 10,
  1109. scale: true,
  1110. });
  1111. });
  1112. test('getMinAndMaxFromBounds returns automatic lower bound when truncating', () => {
  1113. expect(
  1114. getMinAndMaxFromBounds(
  1115. AxisType.Value,
  1116. true,
  1117. undefined,
  1118. 100,
  1119. EchartsTimeseriesSeriesType.Bar,
  1120. ),
  1121. ).toEqual({
  1122. max: 100,
  1123. scale: true,
  1124. });
  1125. });
  1126. describe('getTimeCompareStackId', () => {
  1127. it('returns the defaultId when timeCompare is empty', () => {
  1128. const result = getTimeCompareStackId('default', []);
  1129. expect(result).toEqual('default');
  1130. });
  1131. it('returns the defaultId when no value in timeCompare is included in name', () => {
  1132. const result = getTimeCompareStackId(
  1133. 'default',
  1134. ['compare1', 'compare2'],
  1135. 'test__name',
  1136. );
  1137. expect(result).toEqual('default');
  1138. });
  1139. it('returns the first value in timeCompare that is included in name', () => {
  1140. const result = getTimeCompareStackId(
  1141. 'default',
  1142. ['compare1', 'compare2'],
  1143. 'test__compare1',
  1144. );
  1145. expect(result).toEqual('compare1');
  1146. });
  1147. it('handles name being a number', () => {
  1148. const result = getTimeCompareStackId('default', ['123', '456'], 123);
  1149. expect(result).toEqual('123');
  1150. });
  1151. });
  1152. const forecastValue = [
  1153. {
  1154. data: [0, 1],
  1155. seriesId: 'foo',
  1156. },
  1157. {
  1158. data: [0, 2],
  1159. seriesId: 'bar',
  1160. },
  1161. ];
  1162. test('extractTooltipKeys with rich tooltip', () => {
  1163. const result = extractTooltipKeys(forecastValue, 1, true, false);
  1164. expect(result).toEqual(['foo', 'bar']);
  1165. });
  1166. test('extractTooltipKeys with rich tooltip and sorting by metrics', () => {
  1167. const result = extractTooltipKeys(forecastValue, 1, true, true);
  1168. expect(result).toEqual(['bar', 'foo']);
  1169. });
  1170. test('extractTooltipKeys with non-rich tooltip', () => {
  1171. const result = extractTooltipKeys(forecastValue, 1, false, false);
  1172. expect(result).toEqual(['foo']);
  1173. });