@thoughtspot/ts-chart-sdk 0.0.2-alpha.24 → 0.0.2-alpha.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/ts-chart-sdk.d.ts +63 -3
  2. package/lib/index.d.ts +2 -0
  3. package/lib/index.d.ts.map +1 -1
  4. package/lib/index.js +2 -0
  5. package/lib/index.js.map +1 -1
  6. package/lib/main/custom-chart-context.d.ts.map +1 -1
  7. package/lib/main/custom-chart-context.js +3 -1
  8. package/lib/main/custom-chart-context.js.map +1 -1
  9. package/lib/types/answer-column.types.d.ts +3 -3
  10. package/lib/types/answer-column.types.d.ts.map +1 -1
  11. package/lib/types/answer-column.types.js +3 -3
  12. package/lib/types/answer-column.types.js.map +1 -1
  13. package/lib/types/visual-prop.types.d.ts +40 -0
  14. package/lib/types/visual-prop.types.d.ts.map +1 -1
  15. package/lib/types/visual-prop.types.js +32 -1
  16. package/lib/types/visual-prop.types.js.map +1 -1
  17. package/lib/utils/globalize-Initializer/globalize-utils.d.ts +16 -0
  18. package/lib/utils/globalize-Initializer/globalize-utils.d.ts.map +1 -0
  19. package/lib/utils/globalize-Initializer/globalize-utils.js +101 -0
  20. package/lib/utils/globalize-Initializer/globalize-utils.js.map +1 -0
  21. package/lib/utils/globalize-Initializer/globalize-utils.spec.d.ts +2 -0
  22. package/lib/utils/globalize-Initializer/globalize-utils.spec.d.ts.map +1 -0
  23. package/lib/utils/globalize-Initializer/globalize-utils.spec.js +149 -0
  24. package/lib/utils/globalize-Initializer/globalize-utils.spec.js.map +1 -0
  25. package/lib/utils/number-formatting/number-formatting-utils.d.ts +20 -0
  26. package/lib/utils/number-formatting/number-formatting-utils.d.ts.map +1 -0
  27. package/lib/utils/number-formatting/number-formatting-utils.js +159 -0
  28. package/lib/utils/number-formatting/number-formatting-utils.js.map +1 -0
  29. package/lib/utils/number-formatting/number-formatting-utils.spec.d.ts +2 -0
  30. package/lib/utils/number-formatting/number-formatting-utils.spec.d.ts.map +1 -0
  31. package/lib/utils/number-formatting/number-formatting-utils.spec.js +221 -0
  32. package/lib/utils/number-formatting/number-formatting-utils.spec.js.map +1 -0
  33. package/lib/utils/number-formatting/number-formatting.d.ts +5 -0
  34. package/lib/utils/number-formatting/number-formatting.d.ts.map +1 -0
  35. package/lib/utils/number-formatting/number-formatting.js +128 -0
  36. package/lib/utils/number-formatting/number-formatting.js.map +1 -0
  37. package/lib/utils/number-formatting/number-formatting.spec.d.ts +2 -0
  38. package/lib/utils/number-formatting/number-formatting.spec.d.ts.map +1 -0
  39. package/lib/utils/number-formatting/number-formatting.spec.js +159 -0
  40. package/lib/utils/number-formatting/number-formatting.spec.js.map +1 -0
  41. package/package.json +4 -1
  42. package/src/index.ts +2 -0
  43. package/src/main/custom-chart-context.ts +4 -1
  44. package/src/types/answer-column.types.ts +3 -3
  45. package/src/types/visual-prop.types.ts +86 -0
  46. package/src/utils/globalize-Initializer/globalize-utils.spec.ts +192 -0
  47. package/src/utils/globalize-Initializer/globalize-utils.ts +216 -0
  48. package/src/utils/number-formatting/number-formatting-utils.spec.ts +296 -0
  49. package/src/utils/number-formatting/number-formatting-utils.ts +260 -0
  50. package/src/utils/number-formatting/number-formatting.spec.ts +243 -0
  51. package/src/utils/number-formatting/number-formatting.ts +264 -0
@@ -0,0 +1,260 @@
1
+ /**
2
+ * @file: Formatting Utils
3
+ *
4
+ * @author Yashvardhan Nehra <yashvardhan.nehra@thoughtspot.com>
5
+ *
6
+ * Copyright: ThoughtSpot Inc. 2024
7
+ */
8
+
9
+ import _ from 'lodash';
10
+ import {
11
+ ColumnFormat,
12
+ CurrencyFormat,
13
+ CurrencyFormatType,
14
+ } from '../../types/answer-column.types';
15
+ import { Maybe } from '../../types/common.types';
16
+ import {
17
+ CategoryType,
18
+ CurrencyFormatConfig,
19
+ FormatConfig,
20
+ NegativeValueFormat,
21
+ NumberFormatConfig,
22
+ Unit,
23
+ } from '../../types/number-formatting.types';
24
+ import { getDefaultCurrencyCode } from '../globalize-Initializer/globalize-utils';
25
+
26
+ interface FormatterConfig {
27
+ unitDetails: Unit;
28
+ decimalDetails: number;
29
+ shouldRemoveTrailingZeros: boolean;
30
+ }
31
+ /**
32
+ * Constants for unit conversions and suffixes.
33
+ */
34
+ export const UNITS_TO_DIVIDING_FACTOR: Record<Unit, number> = {
35
+ [Unit.None]: 1,
36
+ [Unit.Thousands]: 1000,
37
+ [Unit.Million]: 1000 * 1000,
38
+ [Unit.Billion]: 1000 * 1000 * 1000,
39
+ [Unit.Trillion]: 1000 * 1000 * 1000 * 1000,
40
+ [Unit.Auto]: 1,
41
+ };
42
+
43
+ /**
44
+ * Default strings for placeholders.
45
+ */
46
+ let strings: Record<string, string> = {
47
+ NULL_VALUE_PLACEHOLDER_LABEL: '{Null}',
48
+ EMPTY_VALUE_PLACEHOLDER_LABEL: '{Empty}',
49
+ };
50
+
51
+ export const setLocaleBasedStringFormats = (
52
+ tsLocaleBasedStringsFormats?: Record<string, string>,
53
+ ) => {
54
+ if (tsLocaleBasedStringsFormats) {
55
+ strings = tsLocaleBasedStringsFormats;
56
+ }
57
+ };
58
+
59
+ export const getLocaleBasedStringFormats = () => strings;
60
+
61
+ export const UNITS_TO_SUFFIX: Record<Unit, string> = {
62
+ [Unit.None]: '',
63
+ [Unit.Thousands]: 'K',
64
+ [Unit.Million]: 'M',
65
+ [Unit.Billion]: 'B',
66
+ [Unit.Trillion]: 'T',
67
+ [Unit.Auto]: '',
68
+ };
69
+
70
+ const DEFAULT_DECIMAL_PRECISION = 2;
71
+
72
+ /**
73
+ * Formats negative values according to the specified format.
74
+ *
75
+ * @param formattedValue - The formatted value to modify.
76
+ * @param negativeFormat - The desired negative value format.
77
+ * @returns The formatted negative value.
78
+ */
79
+ export const formatNegativeValue = (
80
+ formattedValue: string,
81
+ negativeFormat?: Maybe<NegativeValueFormat> | number,
82
+ ): string => {
83
+ switch (negativeFormat) {
84
+ case NegativeValueFormat.PrefixDash:
85
+ return `-${formattedValue}`;
86
+ case NegativeValueFormat.SuffixDash:
87
+ return `${formattedValue}-`;
88
+ case NegativeValueFormat.BracesNodash:
89
+ return `(${formattedValue})`;
90
+ default:
91
+ return `-${formattedValue}`;
92
+ }
93
+ };
94
+
95
+ /**
96
+ * Determines the appropriate unit for auto-scaling values.
97
+ *
98
+ * @param value - The numeric value to evaluate.
99
+ * @returns The appropriate unit (e.g., Thousand, Million, etc.).
100
+ */
101
+ export const getAutoUnit = (value: number): Unit => {
102
+ if (value >= UNITS_TO_DIVIDING_FACTOR[Unit.Trillion]) {
103
+ return Unit.Trillion;
104
+ }
105
+ if (value >= UNITS_TO_DIVIDING_FACTOR[Unit.Billion]) {
106
+ return Unit.Billion;
107
+ }
108
+ if (value >= UNITS_TO_DIVIDING_FACTOR[Unit.Million]) {
109
+ return Unit.Million;
110
+ }
111
+ if (value >= UNITS_TO_DIVIDING_FACTOR[Unit.Thousands]) {
112
+ return Unit.Thousands;
113
+ }
114
+ return Unit.None;
115
+ };
116
+
117
+ /**
118
+ * Retrieves the locale name based on the provided currency format.
119
+ *
120
+ * @param currencyFormat - The currency format configuration.
121
+ * @returns The locale name as a string.
122
+ */
123
+ export const getLocaleName = (currencyFormat: CurrencyFormat): string => {
124
+ if (currencyFormat.type === CurrencyFormatType.ISO_CODE) {
125
+ return currencyFormat.isoCode;
126
+ }
127
+
128
+ const locale = getDefaultCurrencyCode();
129
+ return locale;
130
+ };
131
+
132
+ /**
133
+ * Generates a default format configuration based on the column settings.
134
+ *
135
+ * @param columnFormatConfig - The column-specific formatting settings.
136
+ * @returns The default format configuration.
137
+ */
138
+ export const defaultFormatConfig = (
139
+ columnFormatConfig?: ColumnFormat,
140
+ ): FormatConfig => {
141
+ if (columnFormatConfig?.pattern) {
142
+ return {
143
+ __typename: 'FormatConfig',
144
+ category: CategoryType.Custom,
145
+ isCategoryEditable: true,
146
+ customFormatConfig: {
147
+ __typename: 'CustomFormatConfig',
148
+ format: columnFormatConfig?.pattern,
149
+ },
150
+ };
151
+ }
152
+ if (columnFormatConfig?.currencyFormat) {
153
+ const locale = getLocaleName(columnFormatConfig?.currencyFormat);
154
+ const currencyFormatConfig: CurrencyFormatConfig = {
155
+ __typename: 'CurrencyFormatConfig',
156
+ decimals: 0,
157
+ locale,
158
+ removeTrailingZeroes: false,
159
+ toSeparateThousands: true,
160
+ unit: Unit.Auto,
161
+ };
162
+ const formatConfig: FormatConfig = {
163
+ __typename: 'FormatConfig',
164
+ category: CategoryType.Currency,
165
+ isCategoryEditable: true,
166
+ currencyFormatConfig,
167
+ };
168
+ return formatConfig;
169
+ }
170
+ const numberFormatConfig: NumberFormatConfig = {
171
+ __typename: 'NumberFormatConfig',
172
+ decimals: 0,
173
+ negativeValueFormat: NegativeValueFormat.PrefixDash,
174
+ removeTrailingZeroes: false,
175
+ toSeparateThousands: true,
176
+ unit: Unit.Auto,
177
+ };
178
+ const formatConfig: FormatConfig = {
179
+ __typename: 'FormatConfig',
180
+ category: CategoryType.Number,
181
+ isCategoryEditable: true,
182
+ numberFormatConfig,
183
+ percentageFormatConfig: null,
184
+ };
185
+ return formatConfig;
186
+ };
187
+
188
+ /**
189
+ * Maps configuration details to a formatter configuration object.
190
+ *
191
+ * @param absFloatValue - The absolute value of the number being formatted.
192
+ * @param configDetails - The number or currency format configuration.
193
+ * @returns A mapped formatter configuration.
194
+ */
195
+ export const mapFormatterConfig = (
196
+ absFloatValue: number,
197
+ configDetails: NumberFormatConfig | CurrencyFormatConfig,
198
+ ): FormatterConfig => {
199
+ const isAutoFormatted = configDetails.unit === Unit.Auto;
200
+ let unitDetails = configDetails.unit || Unit.Auto;
201
+ let decimalDetails = configDetails.decimals || 0;
202
+
203
+ if (isAutoFormatted) {
204
+ unitDetails = getAutoUnit(absFloatValue);
205
+ if (unitDetails !== Unit.None) {
206
+ decimalDetails = DEFAULT_DECIMAL_PRECISION;
207
+ }
208
+ }
209
+ const shouldRemoveTrailingZeros =
210
+ configDetails.removeTrailingZeroes || isAutoFormatted;
211
+ return {
212
+ unitDetails,
213
+ decimalDetails,
214
+ shouldRemoveTrailingZeros,
215
+ };
216
+ };
217
+
218
+ /**
219
+ * Handles formatting of special data values (e.g., null, empty, NaN, Infinity).
220
+ *
221
+ * @param value - The value to check and format.
222
+ * @returns A formatted special value or null if not applicable.
223
+ */
224
+ export function formatSpecialDataValue(value: any) {
225
+ if (
226
+ value === strings.NULL_VALUE_PLACEHOLDER_LABEL ||
227
+ value === strings.EMPTY_VALUE_PLACEHOLDER_LABEL
228
+ ) {
229
+ return value;
230
+ }
231
+
232
+ if (value === null || value === undefined) {
233
+ return strings.NULL_VALUE_PLACEHOLDER_LABEL;
234
+ }
235
+
236
+ if (value === Infinity || value === -Infinity || _.isNaN(value)) {
237
+ return value.toString();
238
+ }
239
+ // {Empty} placeholder is set for empty string or no characters
240
+ // other than spaces.
241
+ if (value === '') {
242
+ return strings.EMPTY_VALUE_PLACEHOLDER_LABEL;
243
+ }
244
+ if (value instanceof Array) {
245
+ if (_.isEmpty(value) || _.isNil(value[0])) {
246
+ return strings.NULL_VALUE_PLACEHOLDER_LABEL;
247
+ }
248
+
249
+ switch (value[0]) {
250
+ case 'NaN':
251
+ return 'NaN';
252
+ case 'Infinity':
253
+ return 'Infinity';
254
+ default:
255
+ return null;
256
+ }
257
+ }
258
+
259
+ return null;
260
+ }
@@ -0,0 +1,243 @@
1
+ import { create, LogLevel, logMethods } from '../../main/logger';
2
+ import {
3
+ ColumnFormat,
4
+ CurrencyFormatType,
5
+ FormatType,
6
+ } from '../../types/answer-column.types';
7
+ import {
8
+ CategoryType,
9
+ FormatConfig,
10
+ NegativeValueFormat,
11
+ Unit,
12
+ } from '../../types/number-formatting.types';
13
+ import { initGlobalize } from '../globalize-Initializer/globalize-utils';
14
+ import * as globalizeUtils from '../globalize-Initializer/globalize-utils';
15
+ import {
16
+ formatCurrencyWithCustomPattern,
17
+ getFormattedValue,
18
+ } from './number-formatting';
19
+
20
+ jest.mock('../../main/util', () => ({
21
+ getQueryParam: jest.fn().mockReturnValue('true'),
22
+ }));
23
+
24
+ describe('formatCurrencyWithCustomPattern', () => {
25
+ beforeAll(() => {
26
+ initGlobalize('en-US');
27
+ });
28
+ test('formats value with a custom pattern and currency symbol', () => {
29
+ const value = 12345.678;
30
+ const currencyCode = 'USD';
31
+ const formatPattern = '#,##0.00';
32
+
33
+ const result = formatCurrencyWithCustomPattern(
34
+ value,
35
+ currencyCode,
36
+ formatPattern,
37
+ );
38
+ expect(result).toBe('$12,345.68');
39
+ });
40
+ });
41
+
42
+ describe('getFormattedValue', () => {
43
+ beforeAll(() => {
44
+ initGlobalize('en-US');
45
+ });
46
+ const columnFormatConfig: ColumnFormat = {
47
+ type: FormatType.CURRENCY,
48
+ currencyFormat: {
49
+ type: CurrencyFormatType.ISO_CODE,
50
+ column: '',
51
+ isoCode: 'USD',
52
+ },
53
+ };
54
+
55
+ beforeEach(() => {
56
+ jest.clearAllMocks();
57
+ });
58
+
59
+ test('uses Number Fromatting as default configuration when formatConfigProp & columnFormatConfig are null', () => {
60
+ const result = getFormattedValue(12345, {}, {} as ColumnFormat);
61
+ expect(result).toBe('12.35K');
62
+ });
63
+
64
+ test('use Custom Formatting as default configuration when only formatConfigProp is null', () => {
65
+ const result = getFormattedValue(12345, {}, columnFormatConfig);
66
+ expect(result).toBe('$12.35K');
67
+ });
68
+
69
+ test('formats special values (NaN, Infinity)', () => {
70
+ expect(getFormattedValue(NaN, {}, {} as ColumnFormat)).toBe('NaN');
71
+ expect(getFormattedValue(Infinity, {}, {} as ColumnFormat)).toBe(
72
+ 'Infinity',
73
+ );
74
+ expect(getFormattedValue(-Infinity, {}, {} as ColumnFormat)).toBe(
75
+ '-Infinity',
76
+ );
77
+ });
78
+
79
+ test('formats number category values with units and trailing zeroes', () => {
80
+ const formatConfig: FormatConfig = {
81
+ category: CategoryType.Number,
82
+ numberFormatConfig: {
83
+ unit: Unit.Thousands,
84
+ decimals: 2,
85
+ toSeparateThousands: true,
86
+ negativeValueFormat: NegativeValueFormat.PrefixDash,
87
+ },
88
+ };
89
+ const result = getFormattedValue(
90
+ -12345,
91
+ formatConfig,
92
+ {} as ColumnFormat,
93
+ );
94
+ expect(result).toBe('-12.35K'); // Update based on actual expected output
95
+ });
96
+
97
+ test('formats percentage values correctly', () => {
98
+ const formatConfig: FormatConfig = {
99
+ category: CategoryType.Percentage,
100
+ percentageFormatConfig: { decimals: 2, removeTrailingZeroes: true },
101
+ };
102
+ const result = getFormattedValue(
103
+ 0.1234,
104
+ formatConfig,
105
+ {} as ColumnFormat,
106
+ );
107
+ expect(result).toBe('12.34%');
108
+ });
109
+
110
+ test('formats currency values with compact units', () => {
111
+ const formatConfig: FormatConfig = {
112
+ category: CategoryType.Currency,
113
+ currencyFormatConfig: {
114
+ locale: 'USD',
115
+ toSeparateThousands: true,
116
+ },
117
+ };
118
+ const result = getFormattedValue(
119
+ 12345.678,
120
+ formatConfig,
121
+ {} as ColumnFormat,
122
+ );
123
+ // eslint-disable-next-line security/detect-unsafe-regex
124
+ expect(result).toMatch(/^\$\d+,\d+(\.\d+)?$/); // Update expected value
125
+ });
126
+
127
+ test('formats currency values with user Locale', () => {
128
+ jest.spyOn(
129
+ globalizeUtils,
130
+ 'getDefaultCurrencyCode',
131
+ ).mockImplementationOnce(() => 'INR');
132
+ const formatConfig: FormatConfig = {
133
+ category: CategoryType.Currency,
134
+ currencyFormatConfig: {
135
+ unit: Unit.Thousands,
136
+ decimals: 2,
137
+ locale: CurrencyFormatType.USER_LOCALE,
138
+ toSeparateThousands: true,
139
+ },
140
+ };
141
+ const result = getFormattedValue(
142
+ 12345.678,
143
+ formatConfig,
144
+ {} as ColumnFormat,
145
+ );
146
+ expect(result).toMatch('₹12.35K'); // Update expected value
147
+ });
148
+
149
+ test('should normalize category to CategoryType.Number when it is a number', () => {
150
+ // Mock formatConfig with category as a number
151
+ const formatConfig: FormatConfig = {
152
+ category: CategoryType.Number,
153
+ numberFormatConfig: {
154
+ unit: Unit.Thousands,
155
+ decimals: 2,
156
+ toSeparateThousands: true,
157
+ negativeValueFormat: NegativeValueFormat.PrefixDash,
158
+ },
159
+ };
160
+
161
+ const result = getFormattedValue(
162
+ 1000,
163
+ formatConfig,
164
+ {} as ColumnFormat,
165
+ );
166
+
167
+ expect(result).toBe('1.00K'); // Update based on actual expected output
168
+ });
169
+
170
+ test('falls back to default formatting for unsupported categories', () => {
171
+ const formatConfig: FormatConfig = { category: 'Unknown' as any };
172
+ const result = getFormattedValue(
173
+ 12345.678,
174
+ formatConfig,
175
+ {} as ColumnFormat,
176
+ );
177
+ expect(result).toBe('12,345.678'); // Update based on fallback behavior
178
+ });
179
+
180
+ test('formats custom pattern correctly', () => {
181
+ const formatConfig: FormatConfig = {
182
+ category: CategoryType.Custom,
183
+ customFormatConfig: { format: '#,##0.00' },
184
+ };
185
+ const result = getFormattedValue(
186
+ 12345.678,
187
+ formatConfig,
188
+ {} as ColumnFormat,
189
+ );
190
+ expect(result).toBe('12,345.68'); // Update based on actual output
191
+ });
192
+
193
+ test('formats custom pattern with valid customColumn format', () => {
194
+ const formatConfig: FormatConfig = {
195
+ category: CategoryType.Custom,
196
+ customFormatConfig: { format: '#,##0.00' },
197
+ };
198
+ const result = getFormattedValue(
199
+ 12345.678,
200
+ formatConfig,
201
+ columnFormatConfig,
202
+ );
203
+ expect(result).toBe('$12,345.68'); // Update based on actual output
204
+ });
205
+
206
+ test('logs error for invalid custom format patterns', () => {
207
+ const formatConfig: FormatConfig = {
208
+ category: CategoryType.Custom,
209
+ customFormatConfig: { format: 'invalid-format' },
210
+ };
211
+ const error = jest.fn();
212
+ logMethods[LogLevel.ERROR] = error;
213
+ const logger = create('TestLogger');
214
+ logger.error = error;
215
+
216
+ getFormattedValue(12345.678, formatConfig, {} as ColumnFormat);
217
+ expect(logger.error).toHaveBeenCalled();
218
+
219
+ jest.restoreAllMocks();
220
+ });
221
+
222
+ test('handles currency formatting failure', () => {
223
+ const formatConfig: FormatConfig = {
224
+ category: CategoryType.Currency,
225
+ };
226
+ jest.spyOn(
227
+ globalizeUtils,
228
+ 'globalizeCurrencyFormatter',
229
+ ).mockImplementationOnce(() => {
230
+ throw new Error('Currency format error');
231
+ });
232
+
233
+ const error = jest.fn();
234
+ logMethods[LogLevel.ERROR] = error;
235
+ const logger = create('TestLogger');
236
+ logger.error = error;
237
+
238
+ getFormattedValue(12345.678, formatConfig, {} as ColumnFormat);
239
+ expect(logger.error).toHaveBeenCalled();
240
+
241
+ jest.restoreAllMocks();
242
+ });
243
+ });