transformProps.test.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  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 { ChartProps, SqlaFormData, supersetTheme } from '@superset-ui/core';
  20. import { EchartsTimeseriesChartProps } from '../../../src/types';
  21. import transformProps from '../../../src/Timeseries/transformProps';
  22. import { DEFAULT_FORM_DATA } from '../../../src/Timeseries/constants';
  23. import { EchartsTimeseriesSeriesType } from '../../../src/Timeseries/types';
  24. describe('Bar Chart X-axis Time Formatting', () => {
  25. const baseFormData: SqlaFormData = {
  26. ...DEFAULT_FORM_DATA,
  27. colorScheme: 'bnbColors',
  28. datasource: '3__table',
  29. granularity_sqla: '__timestamp',
  30. metric: ['Sales', 'Marketing', 'Operations'],
  31. groupby: [],
  32. viz_type: 'echarts_timeseries_bar',
  33. seriesType: EchartsTimeseriesSeriesType.Bar,
  34. orientation: 'vertical',
  35. };
  36. const timeseriesData = [
  37. {
  38. data: [
  39. { Sales: 100, __timestamp: 1609459200000 }, // 2021-01-01
  40. { Marketing: 150, __timestamp: 1612137600000 }, // 2021-02-01
  41. { Operations: 200, __timestamp: 1614556800000 }, // 2021-03-01
  42. ],
  43. colnames: ['Sales', 'Marketing', 'Operations', '__timestamp'],
  44. coltypes: ['BIGINT', 'BIGINT', 'BIGINT', 'TIMESTAMP'],
  45. },
  46. ];
  47. const baseChartPropsConfig = {
  48. width: 800,
  49. height: 600,
  50. queriesData: timeseriesData,
  51. theme: supersetTheme,
  52. };
  53. describe('Default xAxisTimeFormat', () => {
  54. it('should use smart_date as default xAxisTimeFormat', () => {
  55. const chartProps = new ChartProps({
  56. ...baseChartPropsConfig,
  57. formData: baseFormData,
  58. });
  59. const transformedProps = transformProps(
  60. chartProps as EchartsTimeseriesChartProps,
  61. );
  62. // Check that the x-axis has a formatter applied
  63. expect(transformedProps.echartOptions.xAxis).toHaveProperty('axisLabel');
  64. const xAxis = transformedProps.echartOptions.xAxis as any;
  65. expect(xAxis.axisLabel).toHaveProperty('formatter');
  66. expect(typeof xAxis.axisLabel.formatter).toBe('function');
  67. });
  68. it('should apply xAxisTimeFormat from DEFAULT_FORM_DATA when not explicitly set', () => {
  69. const formDataWithoutTimeFormat = {
  70. ...baseFormData,
  71. };
  72. delete formDataWithoutTimeFormat.xAxisTimeFormat;
  73. const chartProps = new ChartProps({
  74. ...baseChartPropsConfig,
  75. formData: formDataWithoutTimeFormat,
  76. });
  77. const transformedProps = transformProps(
  78. chartProps as EchartsTimeseriesChartProps,
  79. );
  80. // Should still have a formatter since DEFAULT_FORM_DATA includes xAxisTimeFormat
  81. expect(transformedProps.echartOptions.xAxis).toHaveProperty('axisLabel');
  82. const xAxis = transformedProps.echartOptions.xAxis as any;
  83. expect(xAxis.axisLabel).toHaveProperty('formatter');
  84. });
  85. });
  86. describe('Custom xAxisTimeFormat', () => {
  87. it('should respect custom xAxisTimeFormat when explicitly set', () => {
  88. const customFormData = {
  89. ...baseFormData,
  90. xAxisTimeFormat: '%Y-%m-%d',
  91. };
  92. const chartProps = new ChartProps({
  93. ...baseChartPropsConfig,
  94. formData: customFormData,
  95. });
  96. const transformedProps = transformProps(
  97. chartProps as EchartsTimeseriesChartProps,
  98. );
  99. // Verify the formatter function exists and is applied
  100. expect(transformedProps.echartOptions.xAxis).toHaveProperty('axisLabel');
  101. const xAxis = transformedProps.echartOptions.xAxis as any;
  102. expect(xAxis.axisLabel).toHaveProperty('formatter');
  103. expect(typeof xAxis.axisLabel.formatter).toBe('function');
  104. // The key test is that a formatter exists - the actual formatting is handled by d3-time-format
  105. const { formatter } = xAxis.axisLabel;
  106. expect(formatter).toBeDefined();
  107. expect(typeof formatter).toBe('function');
  108. });
  109. it('should handle different time format options', () => {
  110. const timeFormats = [
  111. '%Y-%m-%d',
  112. '%Y/%m/%d',
  113. '%m/%d/%Y',
  114. '%b %d, %Y',
  115. 'smart_date',
  116. ];
  117. timeFormats.forEach(timeFormat => {
  118. const customFormData = {
  119. ...baseFormData,
  120. xAxisTimeFormat: timeFormat,
  121. };
  122. const chartProps = new ChartProps({
  123. ...baseChartPropsConfig,
  124. formData: customFormData,
  125. });
  126. const transformedProps = transformProps(
  127. chartProps as EchartsTimeseriesChartProps,
  128. );
  129. const xAxis = transformedProps.echartOptions.xAxis as any;
  130. expect(xAxis.axisLabel).toHaveProperty('formatter');
  131. expect(typeof xAxis.axisLabel.formatter).toBe('function');
  132. });
  133. });
  134. });
  135. describe('Orientation-specific behavior', () => {
  136. it('should apply time formatting to x-axis in vertical bar charts', () => {
  137. const verticalFormData = {
  138. ...baseFormData,
  139. orientation: 'vertical',
  140. xAxisTimeFormat: '%Y-%m',
  141. };
  142. const chartProps = new ChartProps({
  143. ...baseChartPropsConfig,
  144. formData: verticalFormData,
  145. });
  146. const transformedProps = transformProps(
  147. chartProps as EchartsTimeseriesChartProps,
  148. );
  149. // In vertical orientation, time should be on x-axis
  150. const xAxis = transformedProps.echartOptions.xAxis as any;
  151. expect(xAxis.axisLabel).toHaveProperty('formatter');
  152. expect(typeof xAxis.axisLabel.formatter).toBe('function');
  153. });
  154. it('should apply time formatting to y-axis in horizontal bar charts', () => {
  155. const horizontalFormData = {
  156. ...baseFormData,
  157. orientation: 'horizontal',
  158. xAxisTimeFormat: '%Y-%m',
  159. };
  160. const chartProps = new ChartProps({
  161. ...baseChartPropsConfig,
  162. formData: horizontalFormData,
  163. });
  164. const transformedProps = transformProps(
  165. chartProps as EchartsTimeseriesChartProps,
  166. );
  167. // In horizontal orientation, axes are swapped, so time should be on y-axis
  168. const yAxis = transformedProps.echartOptions.yAxis as any;
  169. expect(yAxis.axisLabel).toHaveProperty('formatter');
  170. expect(typeof yAxis.axisLabel.formatter).toBe('function');
  171. });
  172. });
  173. describe('Integration with existing features', () => {
  174. it('should work with axis bounds', () => {
  175. const formDataWithBounds = {
  176. ...baseFormData,
  177. xAxisTimeFormat: '%Y-%m-%d',
  178. truncateXAxis: true,
  179. xAxisBounds: [null, null] as [number | null, number | null],
  180. };
  181. const chartProps = new ChartProps({
  182. ...baseChartPropsConfig,
  183. formData: formDataWithBounds,
  184. });
  185. const transformedProps = transformProps(
  186. chartProps as EchartsTimeseriesChartProps,
  187. );
  188. const xAxis = transformedProps.echartOptions.xAxis as any;
  189. expect(xAxis.axisLabel).toHaveProperty('formatter');
  190. // The xAxis should be configured with the time formatting
  191. expect(transformedProps.echartOptions.xAxis).toBeDefined();
  192. });
  193. it('should work with label rotation', () => {
  194. const formDataWithRotation = {
  195. ...baseFormData,
  196. xAxisTimeFormat: '%Y-%m-%d',
  197. xAxisLabelRotation: 45,
  198. };
  199. const chartProps = new ChartProps({
  200. ...baseChartPropsConfig,
  201. formData: formDataWithRotation,
  202. });
  203. const transformedProps = transformProps(
  204. chartProps as EchartsTimeseriesChartProps,
  205. );
  206. const xAxis = transformedProps.echartOptions.xAxis as any;
  207. expect(xAxis.axisLabel).toHaveProperty('formatter');
  208. expect(xAxis.axisLabel).toHaveProperty('rotate', 45);
  209. });
  210. it('should maintain time formatting consistency with tooltip', () => {
  211. const formDataWithTooltip = {
  212. ...baseFormData,
  213. xAxisTimeFormat: '%Y-%m-%d',
  214. tooltipTimeFormat: '%Y-%m-%d',
  215. };
  216. const chartProps = new ChartProps({
  217. ...baseChartPropsConfig,
  218. formData: formDataWithTooltip,
  219. });
  220. const transformedProps = transformProps(
  221. chartProps as EchartsTimeseriesChartProps,
  222. );
  223. // Both axis and tooltip should have formatters
  224. const xAxis = transformedProps.echartOptions.xAxis as any;
  225. expect(xAxis.axisLabel).toHaveProperty('formatter');
  226. expect(transformedProps.xValueFormatter).toBeDefined();
  227. expect(typeof transformedProps.xValueFormatter).toBe('function');
  228. });
  229. });
  230. describe('Regression test for Issue #30373', () => {
  231. it('should not be stuck on adaptive formatting', () => {
  232. // Test the exact scenario described in the issue
  233. const issueFormData = {
  234. ...baseFormData,
  235. xAxisTimeFormat: '%Y-%m-%d %H:%M:%S', // Non-adaptive format
  236. };
  237. const chartProps = new ChartProps({
  238. ...baseChartPropsConfig,
  239. formData: issueFormData,
  240. });
  241. const transformedProps = transformProps(
  242. chartProps as EchartsTimeseriesChartProps,
  243. );
  244. // Verify formatter exists - this is the key fix, ensuring xAxisTimeFormat is used
  245. const xAxis = transformedProps.echartOptions.xAxis as any;
  246. const { formatter } = xAxis.axisLabel;
  247. expect(formatter).toBeDefined();
  248. expect(typeof formatter).toBe('function');
  249. // The important part is that the xAxisTimeFormat is being used from formData
  250. // The actual formatting is handled by the underlying time formatter
  251. });
  252. it('should allow changing from smart_date to other formats', () => {
  253. // First create with smart_date (default)
  254. const smartDateFormData = {
  255. ...baseFormData,
  256. xAxisTimeFormat: 'smart_date',
  257. };
  258. const smartDateChartProps = new ChartProps({
  259. ...baseChartPropsConfig,
  260. formData: smartDateFormData,
  261. });
  262. const smartDateProps = transformProps(
  263. smartDateChartProps as EchartsTimeseriesChartProps,
  264. );
  265. // Then change to a different format
  266. const customFormatFormData = {
  267. ...baseFormData,
  268. xAxisTimeFormat: '%b %Y',
  269. };
  270. const customFormatChartProps = new ChartProps({
  271. ...baseChartPropsConfig,
  272. formData: customFormatFormData,
  273. });
  274. const customFormatProps = transformProps(
  275. customFormatChartProps as EchartsTimeseriesChartProps,
  276. );
  277. // Both should have formatters - the key is that they're not undefined
  278. const smartDateXAxis = smartDateProps.echartOptions.xAxis as any;
  279. const customFormatXAxis = customFormatProps.echartOptions.xAxis as any;
  280. expect(smartDateXAxis.axisLabel.formatter).toBeDefined();
  281. expect(customFormatXAxis.axisLabel.formatter).toBeDefined();
  282. // Both should be functions that can format time
  283. expect(typeof smartDateXAxis.axisLabel.formatter).toBe('function');
  284. expect(typeof customFormatXAxis.axisLabel.formatter).toBe('function');
  285. });
  286. it('should have xAxisTimeFormat in formData by default', () => {
  287. // This test specifically verifies our fix - that DEFAULT_FORM_DATA includes xAxisTimeFormat
  288. const chartProps = new ChartProps({
  289. ...baseChartPropsConfig,
  290. formData: baseFormData,
  291. });
  292. expect(chartProps.formData.xAxisTimeFormat).toBeDefined();
  293. expect(chartProps.formData.xAxisTimeFormat).toBe('smart_date');
  294. });
  295. });
  296. });