transformProps.test.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  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 {
  20. AnnotationSourceType,
  21. AnnotationStyle,
  22. AnnotationType,
  23. ChartProps,
  24. EventAnnotationLayer,
  25. FormulaAnnotationLayer,
  26. IntervalAnnotationLayer,
  27. SqlaFormData,
  28. supersetTheme,
  29. TimeseriesAnnotationLayer,
  30. } from '@superset-ui/core';
  31. import { EchartsTimeseriesChartProps } from '../../src/types';
  32. import transformProps from '../../src/Timeseries/transformProps';
  33. const formData: SqlaFormData = {
  34. colorScheme: 'bnbColors',
  35. datasource: '3__table',
  36. granularity_sqla: 'ds',
  37. metric: 'sum__num',
  38. groupby: ['foo', 'bar'],
  39. viz_type: 'my_viz',
  40. };
  41. const queriesData = [
  42. {
  43. data: [
  44. { 'San Francisco': 1, 'New York': 2, __timestamp: 599616000000 },
  45. { 'San Francisco': 3, 'New York': 4, __timestamp: 599916000000 },
  46. ],
  47. },
  48. ];
  49. const chartPropsConfig = {
  50. formData,
  51. width: 800,
  52. height: 600,
  53. queriesData,
  54. theme: supersetTheme,
  55. };
  56. describe('EchartsTimeseries transformProps', () => {
  57. it('should transform chart props for viz', () => {
  58. const chartProps = new ChartProps(chartPropsConfig);
  59. expect(transformProps(chartProps as EchartsTimeseriesChartProps)).toEqual(
  60. expect.objectContaining({
  61. width: 800,
  62. height: 600,
  63. echartOptions: expect.objectContaining({
  64. legend: expect.objectContaining({
  65. data: ['San Francisco', 'New York'],
  66. }),
  67. series: expect.arrayContaining([
  68. expect.objectContaining({
  69. data: [
  70. [599616000000, 1],
  71. [599916000000, 3],
  72. ],
  73. name: 'San Francisco',
  74. }),
  75. expect.objectContaining({
  76. data: [
  77. [599616000000, 2],
  78. [599916000000, 4],
  79. ],
  80. name: 'New York',
  81. }),
  82. ]),
  83. }),
  84. }),
  85. );
  86. });
  87. it('should transform chart props for horizontal viz', () => {
  88. const chartProps = new ChartProps({
  89. ...chartPropsConfig,
  90. formData: {
  91. ...formData,
  92. orientation: 'horizontal',
  93. },
  94. });
  95. expect(transformProps(chartProps as EchartsTimeseriesChartProps)).toEqual(
  96. expect.objectContaining({
  97. width: 800,
  98. height: 600,
  99. echartOptions: expect.objectContaining({
  100. legend: expect.objectContaining({
  101. data: ['San Francisco', 'New York'],
  102. }),
  103. series: expect.arrayContaining([
  104. expect.objectContaining({
  105. data: [
  106. [1, 599616000000],
  107. [3, 599916000000],
  108. ],
  109. name: 'San Francisco',
  110. }),
  111. expect.objectContaining({
  112. data: [
  113. [2, 599616000000],
  114. [4, 599916000000],
  115. ],
  116. name: 'New York',
  117. }),
  118. ]),
  119. }),
  120. }),
  121. );
  122. });
  123. it('should add a formula annotation to viz', () => {
  124. const formula: FormulaAnnotationLayer = {
  125. name: 'My Formula',
  126. annotationType: AnnotationType.Formula,
  127. value: 'x+1',
  128. style: AnnotationStyle.Solid,
  129. show: true,
  130. showLabel: true,
  131. };
  132. const chartProps = new ChartProps({
  133. ...chartPropsConfig,
  134. formData: {
  135. ...formData,
  136. annotationLayers: [formula],
  137. },
  138. });
  139. expect(transformProps(chartProps as EchartsTimeseriesChartProps)).toEqual(
  140. expect.objectContaining({
  141. width: 800,
  142. height: 600,
  143. echartOptions: expect.objectContaining({
  144. legend: expect.objectContaining({
  145. data: ['San Francisco', 'New York', 'My Formula'],
  146. }),
  147. series: expect.arrayContaining([
  148. expect.objectContaining({
  149. data: [
  150. [599616000000, 1],
  151. [599916000000, 3],
  152. ],
  153. name: 'San Francisco',
  154. }),
  155. expect.objectContaining({
  156. data: [
  157. [599616000000, 2],
  158. [599916000000, 4],
  159. ],
  160. name: 'New York',
  161. }),
  162. expect.objectContaining({
  163. data: [
  164. [599616000000, 599616000001],
  165. [599916000000, 599916000001],
  166. ],
  167. name: 'My Formula',
  168. }),
  169. ]),
  170. }),
  171. }),
  172. );
  173. });
  174. it('should add an interval, event and timeseries annotation to viz', () => {
  175. const event: EventAnnotationLayer = {
  176. annotationType: AnnotationType.Event,
  177. name: 'My Event',
  178. show: true,
  179. showLabel: true,
  180. sourceType: AnnotationSourceType.Native,
  181. style: AnnotationStyle.Solid,
  182. value: 1,
  183. };
  184. const interval: IntervalAnnotationLayer = {
  185. annotationType: AnnotationType.Interval,
  186. name: 'My Interval',
  187. show: true,
  188. showLabel: true,
  189. sourceType: AnnotationSourceType.Table,
  190. titleColumn: '',
  191. timeColumn: 'start',
  192. intervalEndColumn: '',
  193. descriptionColumns: [],
  194. style: AnnotationStyle.Dashed,
  195. value: 2,
  196. };
  197. const timeseries: TimeseriesAnnotationLayer = {
  198. annotationType: AnnotationType.Timeseries,
  199. name: 'My Timeseries',
  200. show: true,
  201. showLabel: true,
  202. sourceType: AnnotationSourceType.Line,
  203. style: AnnotationStyle.Solid,
  204. titleColumn: '',
  205. value: 3,
  206. };
  207. const annotationData = {
  208. 'My Event': {
  209. columns: [
  210. 'start_dttm',
  211. 'end_dttm',
  212. 'short_descr',
  213. 'long_descr',
  214. 'json_metadata',
  215. ],
  216. records: [
  217. {
  218. start_dttm: 0,
  219. end_dttm: 1000,
  220. short_descr: '',
  221. long_descr: '',
  222. json_metadata: null,
  223. },
  224. ],
  225. },
  226. 'My Interval': {
  227. columns: ['start', 'end', 'title'],
  228. records: [
  229. {
  230. start: 2000,
  231. end: 3000,
  232. title: 'My Title',
  233. },
  234. ],
  235. },
  236. 'My Timeseries': {
  237. records: [
  238. { x: 10000, y: 11000 },
  239. { x: 20000, y: 21000 },
  240. ],
  241. },
  242. };
  243. const chartProps = new ChartProps({
  244. ...chartPropsConfig,
  245. formData: {
  246. ...formData,
  247. annotationLayers: [event, interval, timeseries],
  248. },
  249. annotationData,
  250. queriesData: [
  251. {
  252. ...queriesData[0],
  253. annotation_data: annotationData,
  254. },
  255. ],
  256. });
  257. expect(transformProps(chartProps as EchartsTimeseriesChartProps)).toEqual(
  258. expect.objectContaining({
  259. echartOptions: expect.objectContaining({
  260. legend: expect.objectContaining({
  261. data: ['San Francisco', 'New York', 'My Timeseries'],
  262. }),
  263. series: expect.arrayContaining([
  264. expect.objectContaining({
  265. type: 'line',
  266. id: 'My Timeseries',
  267. }),
  268. expect.objectContaining({
  269. type: 'line',
  270. id: 'Event - My Event',
  271. }),
  272. expect.objectContaining({
  273. type: 'line',
  274. id: 'Interval - My Interval',
  275. }),
  276. ]),
  277. }),
  278. }),
  279. );
  280. });
  281. it('Should add a baseline series for stream graph', () => {
  282. const streamQueriesData = [
  283. {
  284. data: [
  285. {
  286. 'San Francisco': 120,
  287. 'New York': 220,
  288. Boston: 150,
  289. Miami: 270,
  290. Denver: 800,
  291. __timestamp: 599616000000,
  292. },
  293. {
  294. 'San Francisco': 150,
  295. 'New York': 190,
  296. Boston: 240,
  297. Miami: 350,
  298. Denver: 700,
  299. __timestamp: 599616000001,
  300. },
  301. {
  302. 'San Francisco': 130,
  303. 'New York': 300,
  304. Boston: 250,
  305. Miami: 410,
  306. Denver: 650,
  307. __timestamp: 599616000002,
  308. },
  309. {
  310. 'San Francisco': 90,
  311. 'New York': 340,
  312. Boston: 300,
  313. Miami: 480,
  314. Denver: 590,
  315. __timestamp: 599616000003,
  316. },
  317. {
  318. 'San Francisco': 260,
  319. 'New York': 200,
  320. Boston: 420,
  321. Miami: 490,
  322. Denver: 760,
  323. __timestamp: 599616000004,
  324. },
  325. {
  326. 'San Francisco': 250,
  327. 'New York': 250,
  328. Boston: 380,
  329. Miami: 360,
  330. Denver: 400,
  331. __timestamp: 599616000005,
  332. },
  333. {
  334. 'San Francisco': 160,
  335. 'New York': 210,
  336. Boston: 330,
  337. Miami: 440,
  338. Denver: 580,
  339. __timestamp: 599616000006,
  340. },
  341. ],
  342. },
  343. ];
  344. const streamFormData = { ...formData, stack: 'Stream' };
  345. const props = {
  346. ...chartPropsConfig,
  347. formData: streamFormData,
  348. queriesData: streamQueriesData,
  349. };
  350. const chartProps = new ChartProps(props);
  351. expect(
  352. (
  353. transformProps(chartProps as EchartsTimeseriesChartProps).echartOptions
  354. .series as any[]
  355. )[0],
  356. ).toEqual({
  357. areaStyle: {
  358. opacity: 0,
  359. },
  360. lineStyle: {
  361. opacity: 0,
  362. },
  363. name: 'baseline',
  364. showSymbol: false,
  365. silent: true,
  366. smooth: false,
  367. stack: 'obs',
  368. stackStrategy: 'all',
  369. step: undefined,
  370. tooltip: {
  371. show: false,
  372. },
  373. type: 'line',
  374. data: [
  375. [599616000000, -415.7692307692308],
  376. [599616000001, -403.6219915054271],
  377. [599616000002, -476.32314093071443],
  378. [599616000003, -514.2120298196033],
  379. [599616000004, -485.7378514158475],
  380. [599616000005, -419.6402904402378],
  381. [599616000006, -442.9833136960517],
  382. ],
  383. });
  384. });
  385. });
  386. describe('Does transformProps transform series correctly', () => {
  387. type seriesDataType = [Date, number];
  388. type labelFormatterType = (params: {
  389. value: seriesDataType;
  390. dataIndex: number;
  391. seriesIndex: number;
  392. }) => string;
  393. type seriesType = {
  394. label: { show: boolean; formatter: labelFormatterType };
  395. data: seriesDataType[];
  396. name: string;
  397. };
  398. const formData: SqlaFormData = {
  399. viz_type: 'my_viz',
  400. colorScheme: 'bnbColors',
  401. datasource: '3__table',
  402. granularity_sqla: 'ds',
  403. metric: 'sum__num',
  404. groupby: ['foo', 'bar'],
  405. showValue: true,
  406. stack: true,
  407. onlyTotal: false,
  408. percentageThreshold: 50,
  409. };
  410. const queriesData = [
  411. {
  412. data: [
  413. {
  414. 'San Francisco': 1,
  415. 'New York': 2,
  416. Boston: 1,
  417. __timestamp: 599616000000,
  418. },
  419. {
  420. 'San Francisco': 3,
  421. 'New York': 4,
  422. Boston: 1,
  423. __timestamp: 599916000000,
  424. },
  425. {
  426. 'San Francisco': 5,
  427. 'New York': 8,
  428. Boston: 6,
  429. __timestamp: 600216000000,
  430. },
  431. {
  432. 'San Francisco': 2,
  433. 'New York': 7,
  434. Boston: 2,
  435. __timestamp: 600516000000,
  436. },
  437. ],
  438. },
  439. ];
  440. const chartPropsConfig = {
  441. formData,
  442. width: 800,
  443. height: 600,
  444. queriesData,
  445. theme: supersetTheme,
  446. };
  447. const totalStackedValues = queriesData[0].data.reduce(
  448. (totals, currentStack) => {
  449. const total = Object.keys(currentStack).reduce((stackSum, key) => {
  450. if (key === '__timestamp') return stackSum;
  451. return stackSum + currentStack[key as keyof typeof currentStack];
  452. }, 0);
  453. totals.push(total);
  454. return totals;
  455. },
  456. [] as number[],
  457. );
  458. it('should show labels when showValue is true', () => {
  459. const chartProps = new ChartProps(chartPropsConfig);
  460. const transformedSeries = transformProps(
  461. chartProps as EchartsTimeseriesChartProps,
  462. ).echartOptions.series as seriesType[];
  463. transformedSeries.forEach(series => {
  464. expect(series.label.show).toBe(true);
  465. });
  466. });
  467. it('should not show labels when showValue is false', () => {
  468. const updatedChartPropsConfig = {
  469. ...chartPropsConfig,
  470. formData: { ...formData, showValue: false },
  471. };
  472. const chartProps = new ChartProps(updatedChartPropsConfig);
  473. const transformedSeries = transformProps(
  474. chartProps as EchartsTimeseriesChartProps,
  475. ).echartOptions.series as seriesType[];
  476. transformedSeries.forEach(series => {
  477. expect(series.label.show).toBe(false);
  478. });
  479. });
  480. it('should show only totals when onlyTotal is true', () => {
  481. const updatedChartPropsConfig = {
  482. ...chartPropsConfig,
  483. formData: { ...formData, onlyTotal: true },
  484. };
  485. const chartProps = new ChartProps(updatedChartPropsConfig);
  486. const transformedSeries = transformProps(
  487. chartProps as EchartsTimeseriesChartProps,
  488. ).echartOptions.series as seriesType[];
  489. const showValueIndexes: number[] = [];
  490. transformedSeries.forEach((entry, seriesIndex) => {
  491. const { data = [] } = entry;
  492. (data as [Date, number][]).forEach((datum, dataIndex) => {
  493. if (datum[1] !== null) {
  494. showValueIndexes[dataIndex] = seriesIndex;
  495. }
  496. });
  497. });
  498. transformedSeries.forEach((series, seriesIndex) => {
  499. expect(series.label.show).toBe(true);
  500. series.data.forEach((value, dataIndex) => {
  501. const params = {
  502. value,
  503. dataIndex,
  504. seriesIndex,
  505. };
  506. let expectedLabel: string;
  507. if (seriesIndex === showValueIndexes[dataIndex]) {
  508. expectedLabel = String(totalStackedValues[dataIndex]);
  509. } else {
  510. expectedLabel = '';
  511. }
  512. expect(series.label.formatter(params)).toBe(expectedLabel);
  513. });
  514. });
  515. });
  516. it('should show labels on values >= percentageThreshold if onlyTotal is false', () => {
  517. const chartProps = new ChartProps(chartPropsConfig);
  518. const transformedSeries = transformProps(
  519. chartProps as EchartsTimeseriesChartProps,
  520. ).echartOptions.series as seriesType[];
  521. const expectedThresholds = totalStackedValues.map(
  522. total => ((formData.percentageThreshold || 0) / 100) * total,
  523. );
  524. transformedSeries.forEach((series, seriesIndex) => {
  525. expect(series.label.show).toBe(true);
  526. series.data.forEach((value, dataIndex) => {
  527. const params = {
  528. value,
  529. dataIndex,
  530. seriesIndex,
  531. };
  532. const expectedLabel =
  533. value[1] >= expectedThresholds[dataIndex] ? String(value[1]) : '';
  534. expect(series.label.formatter(params)).toBe(expectedLabel);
  535. });
  536. });
  537. });
  538. it('should not apply percentage threshold when showValue is true and stack is false', () => {
  539. const updatedChartPropsConfig = {
  540. ...chartPropsConfig,
  541. formData: { ...formData, stack: false },
  542. };
  543. const chartProps = new ChartProps(updatedChartPropsConfig);
  544. const transformedSeries = transformProps(
  545. chartProps as EchartsTimeseriesChartProps,
  546. ).echartOptions.series as seriesType[];
  547. transformedSeries.forEach((series, seriesIndex) => {
  548. expect(series.label.show).toBe(true);
  549. series.data.forEach((value, dataIndex) => {
  550. const params = {
  551. value,
  552. dataIndex,
  553. seriesIndex,
  554. };
  555. const expectedLabel = String(value[1]);
  556. expect(series.label.formatter(params)).toBe(expectedLabel);
  557. });
  558. });
  559. });
  560. it('should remove time shift labels from label_map', () => {
  561. const updatedChartPropsConfig = {
  562. ...chartPropsConfig,
  563. formData: {
  564. ...formData,
  565. timeCompare: ['1 year ago'],
  566. },
  567. queriesData: [
  568. {
  569. ...queriesData[0],
  570. label_map: {
  571. '1 year ago, foo1, bar1': ['1 year ago', 'foo1', 'bar1'],
  572. '1 year ago, foo2, bar2': ['1 year ago', 'foo2', 'bar2'],
  573. 'foo1, bar1': ['foo1', 'bar1'],
  574. 'foo2, bar2': ['foo2', 'bar2'],
  575. },
  576. },
  577. ],
  578. };
  579. const chartProps = new ChartProps(updatedChartPropsConfig);
  580. const transformedProps = transformProps(
  581. chartProps as EchartsTimeseriesChartProps,
  582. );
  583. expect(transformedProps.labelMap).toEqual({
  584. '1 year ago, foo1, bar1': ['foo1', 'bar1'],
  585. '1 year ago, foo2, bar2': ['foo2', 'bar2'],
  586. 'foo1, bar1': ['foo1', 'bar1'],
  587. 'foo2, bar2': ['foo2', 'bar2'],
  588. });
  589. });
  590. });
  591. describe('legend sorting', () => {
  592. const legendSortData = [
  593. {
  594. data: [
  595. {
  596. Milton: 40,
  597. 'San Francisco': 1,
  598. 'New York': 2,
  599. Boston: 1,
  600. __timestamp: 599616000000,
  601. },
  602. {
  603. Milton: 20,
  604. 'San Francisco': 3,
  605. 'New York': 4,
  606. Boston: 1,
  607. __timestamp: 599916000000,
  608. },
  609. {
  610. Milton: 60,
  611. 'San Francisco': 5,
  612. 'New York': 8,
  613. Boston: 6,
  614. __timestamp: 600216000000,
  615. },
  616. {
  617. Milton: 10,
  618. 'San Francisco': 2,
  619. 'New York': 7,
  620. Boston: 2,
  621. __timestamp: 600516000000,
  622. },
  623. ],
  624. },
  625. ];
  626. const getChartProps = (formData: Partial<SqlaFormData>) =>
  627. new ChartProps({
  628. ...chartPropsConfig,
  629. formData: { ...formData },
  630. queriesData: legendSortData,
  631. });
  632. it('sort legend by data', () => {
  633. const chartProps = getChartProps({
  634. legendSort: null,
  635. sortSeriesType: 'min',
  636. sortSeriesAscending: true,
  637. });
  638. const transformed = transformProps(
  639. chartProps as EchartsTimeseriesChartProps,
  640. );
  641. expect((transformed.echartOptions.legend as any).data).toEqual([
  642. 'San Francisco',
  643. 'Boston',
  644. 'New York',
  645. 'Milton',
  646. ]);
  647. });
  648. it('sort legend by label ascending', () => {
  649. const chartProps = getChartProps({
  650. legendSort: 'asc',
  651. sortSeriesType: 'min',
  652. sortSeriesAscending: true,
  653. });
  654. const transformed = transformProps(
  655. chartProps as EchartsTimeseriesChartProps,
  656. );
  657. expect((transformed.echartOptions.legend as any).data).toEqual([
  658. 'Boston',
  659. 'Milton',
  660. 'New York',
  661. 'San Francisco',
  662. ]);
  663. });
  664. it('sort legend by label descending', () => {
  665. const chartProps = getChartProps({
  666. legendSort: 'desc',
  667. sortSeriesType: 'min',
  668. sortSeriesAscending: true,
  669. });
  670. const transformed = transformProps(
  671. chartProps as EchartsTimeseriesChartProps,
  672. );
  673. expect((transformed.echartOptions.legend as any).data).toEqual([
  674. 'San Francisco',
  675. 'New York',
  676. 'Milton',
  677. 'Boston',
  678. ]);
  679. });
  680. });