svg-table 0.0.1 → 0.1.0

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 (43) hide show
  1. package/CHANGES.md +5 -0
  2. package/README.md +447 -1
  3. package/{tableData.ts → TableData.ts} +2 -3
  4. package/TableFormatter.ts +587 -0
  5. package/{tableStyler.ts → TableStyler.ts} +75 -6
  6. package/dist/{tableData.d.ts → TableData.d.ts} +2 -2
  7. package/dist/{tableData.d.ts.map → TableData.d.ts.map} +1 -1
  8. package/dist/{tableData.js → TableData.js} +10 -10
  9. package/dist/{tableData.js.map → TableData.js.map} +1 -1
  10. package/dist/TableFormatter.d.ts +459 -0
  11. package/dist/TableFormatter.d.ts.map +1 -0
  12. package/dist/TableFormatter.js +579 -0
  13. package/dist/TableFormatter.js.map +1 -0
  14. package/dist/{tableStyler.d.ts → TableStyler.d.ts} +58 -3
  15. package/dist/TableStyler.d.ts.map +1 -0
  16. package/dist/{tableStyler.js → TableStyler.js} +90 -31
  17. package/dist/TableStyler.js.map +1 -0
  18. package/dist/index.d.ts +5 -5
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +12 -12
  21. package/dist/index.js.map +1 -1
  22. package/dist/tableData.test.js +15 -15
  23. package/dist/tableFormatter.test.js +54 -15
  24. package/dist/tableFormatter.test.js.map +1 -1
  25. package/dist/tableStyler.test.js +236 -20
  26. package/dist/tableStyler.test.js.map +1 -1
  27. package/dist/tableSvg.d.ts +2 -2
  28. package/dist/tableSvg.js +7 -7
  29. package/index.ts +13 -12
  30. package/package.json +2 -2
  31. package/svg-table-0.0.1.tgz +0 -0
  32. package/tableData.test.ts +1 -1
  33. package/tableFormatter.test.ts +48 -4
  34. package/tableStyler.test.ts +133 -10
  35. package/tableSvg.ts +3 -3
  36. package/dist/tableFormatter.d.ts +0 -179
  37. package/dist/tableFormatter.d.ts.map +0 -1
  38. package/dist/tableFormatter.js +0 -298
  39. package/dist/tableFormatter.js.map +0 -1
  40. package/dist/tableStyler.d.ts.map +0 -1
  41. package/dist/tableStyler.js.map +0 -1
  42. package/svg-table-0.0.1-snapshot.tgz +0 -0
  43. package/tableFormatter.ts +0 -306
@@ -0,0 +1,587 @@
1
+ import {DataFrame, type Tag, type TagCoordinate, type TagValue} from "data-frame-ts";
2
+ import {failureResult, type Result, successResult} from "result-fn";
3
+ import {TableData} from "./TableData";
4
+
5
+ /**
6
+ * Type representing a formatter function
7
+ */
8
+ export type Formatter<D> = (value: D) => string
9
+
10
+ /**
11
+ * Default formatter that converts a {@link value} of type `V` to a string
12
+ * @param value The value to convert
13
+ * @return a string representation of the {@link value}
14
+ */
15
+ export function defaultFormatter<D>(value: D): string {
16
+ return value === undefined || value === null ? '' : `${value}`
17
+ }
18
+
19
+ export type Formatting<V> = {
20
+ formatter: Formatter<V>
21
+ priority: number
22
+ }
23
+
24
+ export function defaultFormatting<D>(): Formatting<D> {
25
+ return {
26
+ formatter: defaultFormatter<D>,
27
+ priority: 0
28
+ }
29
+ }
30
+
31
+ export enum TableFormatterType {
32
+ CELL = "cell-formatter",
33
+ COLUMN = "column-formatter",
34
+ ROW = "row-formatter"
35
+ }
36
+
37
+ export function isFormattingTag(tag: Tag<TagValue, TagCoordinate>): boolean {
38
+ return (tag.name === TableFormatterType.COLUMN ||
39
+ tag.name === TableFormatterType.ROW ||
40
+ tag.name === TableFormatterType.CELL) &&
41
+ tag.value.hasOwnProperty("formatter") &&
42
+ tag.value.hasOwnProperty("priority")
43
+ }
44
+
45
+ /**
46
+ * Represents a formatter used to apply row, column, and cell formatting to a data table.
47
+ * This class is used to define custom formatters for specific rows, columns, or cells,
48
+ * with support for priority-based selection of formatters.
49
+ */
50
+ export class TableFormatter<V> {
51
+
52
+ /**
53
+ * @param dataFrame A data-frame that will be tagged with the row, column, and
54
+ * cell formatters.
55
+ * @private
56
+ */
57
+ private constructor(private readonly dataFrame: DataFrame<V>) {
58
+ }
59
+
60
+ /**
61
+ * Constructs a new {@link TableFormatter} from a {@link TableData} object. Underneath,
62
+ * this factory method merely unwraps the {@link DataFrame} from the {@link TableData}
63
+ * and hands it to the constructor.
64
+ * @param tableData The table data object from which to construct the table formatter
65
+ * @return A new {@link TableFormatter} based on the {@link TableData}
66
+ * @see TableFormatter.fromDataFrame
67
+ */
68
+ static fromTableData<V>(tableData: TableData<V>): TableFormatter<V> {
69
+ return new TableFormatter<V>(tableData.unwrapDataFrame())
70
+ }
71
+
72
+ /**
73
+ * Constructs a new {@link TableFormatter} from a {@link DataFrame} object
74
+ * @param dataFrame The data-frame object from which to construct the table formatter
75
+ * @return A new {@link TableFormatter} based on the {@link DataFrame}
76
+ @see TableFormatter.fromTableData
77
+ */
78
+ static fromDataFrame<V>(dataFrame: DataFrame<V>): TableFormatter<V> {
79
+ return new TableFormatter<V>(dataFrame.copy())
80
+ }
81
+
82
+ /**
83
+ * Formatters convert the column value types to formatted strings. The formatter used to format a given
84
+ * cell depends on the priority of each formatter associated with that cell. The formatter with the
85
+ * highest priority is used. If two or more formatters for a given cell have the same priority, the selected
86
+ * formatter is indeterminant.
87
+ * @param columnIndex The index of the column to which to add the formatter
88
+ * @param formatter The formatter
89
+ * @param [priority = 0] The priority of this formatter. If cells have more than one associated formatter,
90
+ * the one with the highest priority number is used.
91
+ * @see addColumnFormatters
92
+ * @see addRowFormatter
93
+ * @see addRowFormatters
94
+ * @example
95
+ * ```typescript
96
+ * // create the data
97
+ * const data = DataFrame.from<string | number | Date>([
98
+ * [dateTimeFor(1, 1), 12345, 'gnm-f234', 123.45, 4],
99
+ * [dateTimeFor(2, 2), 23456, 'gnm-g234', 23.45, 5],
100
+ * [dateTimeFor(3, 3), 34567, 'gnm-h234', 3.65, 40],
101
+ * [dateTimeFor(4, 4), 45678, 'gnm-i234', 314.15, 9],
102
+ * ]).getOrThrow()
103
+ *
104
+ * // create the table-data object from the data, and then hand the table-data
105
+ * // to the table formatter, add column formats, and format the table, getting
106
+ * // back a new TableData<string>
107
+ * const tableData: TableData<string> = TableData.fromDataFrame<string | number | Date>(data)
108
+ * // from the table-data, create a table-formatter
109
+ * .flatMap(tableData => createTableFormatterFrom(tableData)
110
+ * // add a column formatter for the first column of dates
111
+ * .addColumnFormatter(0, value => (value as Date).toLocaleDateString())
112
+ * // add a column formatter to the second column of number
113
+ * .flatMap(tf => tf.addColumnFormatter(1, value => defaultFormatter(value)))
114
+ * // add a column formatter to the fourth column of currencies
115
+ * .flatMap(tf => tf.addColumnFormatter(3, value => `$ ${(value as number).toFixed(2)}`))
116
+ * .flatMap(tf => tf.addColumnFormatter(4, value => `${(value as number).toFixed(0)}`))
117
+ * // format the table into a new TableData<string> object
118
+ * .flatMap(tf => tf.formatTable())
119
+ * )
120
+ * .getOrThrow()
121
+ *
122
+ * // we expect the data-frame in the table data to be the following
123
+ * const expectedData = DataFrame.from<string>([
124
+ * ['2/1/2021', '12345', 'gnm-f234', '$ 123.45', '4'],
125
+ * ['2/2/2021', '23456', 'gnm-g234', '$ 23.45', '5'],
126
+ * ['2/3/2021', '34567', 'GNM-H234', '$ 3.65', '40'],
127
+ * ['2/4/2021', '45678', 'gnm-i234', '$ 314.15', '9'],
128
+ * ]).getOrThrow()
129
+ * ```
130
+ */
131
+ addColumnFormatter(columnIndex: number, formatter: Formatter<V>, priority: number = 0): Result<TableFormatter<V>, string> {
132
+ return this.dataFrame
133
+ .tagColumn<Formatting<V>>(columnIndex, TableFormatterType.COLUMN, {formatter, priority})
134
+ .map(data => new TableFormatter<V>(data))
135
+ }
136
+
137
+ /**
138
+ * Adds formatters to specified column indexes in a table formatter.
139
+ *
140
+ * @param columnIndexes An array of column indexes to which the formatter will be applied.
141
+ * @param formatter The formatting function to apply to the specified columns.
142
+ * @param [priority = 0] The priority level for the formatter. Higher priority formatters are applied first.
143
+ * @return A result indicating the success or failure of adding the column formatters.
144
+ * @see addColumnFormatter
145
+ * @see addRowFormatter
146
+ * @see addRowFormatters
147
+ * @see addCellFormatter
148
+ * @see addCellFormatters
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * const data = DataFrame.from<string | number | Date>([
153
+ * [dateTimeFor(1, 1), 12345, 'gnm-f234', 123.45, 4],
154
+ * [dateTimeFor(2, 2), 23456, 'gnm-g234', 23.45, 5],
155
+ * [dateTimeFor(3, 3), 34567, 'gnm-h234', 3.65, 40],
156
+ * [dateTimeFor(4, 4), 45678, 'gnm-i234', 314.15, 9],
157
+ * ]).getOrThrow()
158
+ * const columnHeader = ['Date-Time', 'Customer ID', 'Product ID', 'Purchase Price', 'Amount']
159
+ * const rowHeader = [1, 2, 3, 4]
160
+ *
161
+ * const tableData = TableData.fromDataFrame<string | number | Date>(data)
162
+ * .withColumnHeader(columnHeader)
163
+ * .flatMap(td => td.withRowHeader(rowHeader))
164
+ * .flatMap(tableData => TableFormatter.fromTableData(tableData)
165
+ * // add the default formatter for the column header, at the highest priority so that
166
+ * // it is the one that applies to the row representing the column header
167
+ * .addRowFormatters([0], defaultFormatter, Infinity)
168
+ * // add the default formatter for the row header, at the highest priority so that
169
+ * // it is the one that applies to the column representing the row header
170
+ * .flatMap(tf => tf.addColumnFormatters([0], defaultFormatter, Infinity))
171
+ * // add the column formatters for each column at the default (lowest) priority
172
+ * // (notice that the columns are shifted by one for the columns because the row-header
173
+ * // occupies the first column (index=0))
174
+ * .flatMap(tf => tf.addColumnFormatter(1, value => (value as Date).toLocaleDateString()))
175
+ * .flatMap(tf => tf.addColumnFormatter(2, value => defaultFormatter(value)))
176
+ * .flatMap(tf => tf.addColumnFormatter(4, value => `$ ${(value as number).toFixed(2)}`))
177
+ * .flatMap(tf => tf.addColumnFormatter(5, value => `${(value as number).toFixed(0)}`))
178
+ * // format the table data and get back a TableData<string>
179
+ * .flatMap(tf => tf.formatTable())
180
+ * )
181
+ * .getOrThrow()
182
+ *
183
+ * const expectedData = DataFrame.from<string>([
184
+ * ['2/1/2021', '12345', 'gnm-f234', '$ 123.45', '4'],
185
+ * ['2/2/2021', '23456', 'gnm-g234', '$ 23.45', '5'],
186
+ * ['2/3/2021', '34567', 'gnm-h234', '$ 3.65', '40'],
187
+ * ['2/4/2021', '45678', 'gnm-i234', '$ 314.15', '9'],
188
+ * ]).getOrThrow()
189
+ * ```
190
+ */
191
+ addColumnFormatters(columnIndexes: Array<number>, formatter: Formatter<V>, priority: number = 0): Result<TableFormatter<V>, string> {
192
+ return TableFormatter.addColumnFormatters(this, columnIndexes.slice(), formatter, priority)
193
+ }
194
+
195
+ private static addColumnFormatters<V>(
196
+ tableFormatter: TableFormatter<V>,
197
+ columnIndexes: Array<number>,
198
+ formatter: Formatter<V>,
199
+ priority: number = 0
200
+ ): Result<TableFormatter<V>, string> {
201
+ if (columnIndexes.length > 0) {
202
+ const columnIndex = columnIndexes.shift()
203
+ if (columnIndex != null) {
204
+ return tableFormatter
205
+ .addColumnFormatter(columnIndex, formatter, priority)
206
+ .flatMap(tf => TableFormatter.addColumnFormatters(tf, columnIndexes, formatter, priority))
207
+ }
208
+ }
209
+ return successResult(tableFormatter)
210
+ }
211
+
212
+ /**
213
+ * Formatters convert the row value types to formatted strings. The formatter used to format each cell in
214
+ * a given row depends on the priority of each formatter associated with that cell. The formatter with the
215
+ * highest priority is used. If two or more formatters for a given cell have the same priority, the selected
216
+ * formatter is indeterminant.
217
+ * @param rowIndex The index of the row to which to add the formatter
218
+ * @param formatter The formatter
219
+ * @param [priority = 0] The priority of this formatter. If cells have more than one associated formatter,
220
+ * the one with the highest priority number is used.
221
+ * @see addColumnFormatter
222
+ * @see addColumnFormatters
223
+ * @see addRowFormatters
224
+ * @example
225
+ * ```typescript
226
+ * // create the data
227
+ * const data = DataFrame.from<string | number | Date>([
228
+ * [dateTimeFor(1, 1), 12345, 'gnm-f234', 123.45, 4],
229
+ * [dateTimeFor(2, 2), 23456, 'gnm-g234', 23.45, 5],
230
+ * [dateTimeFor(3, 3), 34567, 'gnm-h234', 3.65, 40],
231
+ * [dateTimeFor(4, 4), 45678, 'gnm-i234', 314.15, 9],
232
+ * ]).getOrThrow()
233
+ *
234
+ * // create the table-data object from the data, and then hand the table-data
235
+ * // to the table formatter, add column formats, and format the table, getting
236
+ * // back a new TableData<string>
237
+ * const tableData: TableData<string> = TableData.fromDataFrame<string | number | Date>(data)
238
+ * // from the table-data, create a table-formatter
239
+ * .flatMap(tableData => createTableFormatterFrom(tableData)
240
+ * // add a column formatter for the first column of dates
241
+ * .addColumnFormatter(0, value => (value as Date).toLocaleDateString())
242
+ * // add a column formatter to the second column of number
243
+ * .flatMap(tf => tf.addColumnFormatter(1, value => defaultFormatter(value)))
244
+ * // add a column formatter to the fourth column of currencies
245
+ * .flatMap(tf => tf.addColumnFormatter(3, value => `$ ${(value as number).toFixed(2)}`))
246
+ * .flatMap(tf => tf.addColumnFormatter(4, value => `${(value as number).toFixed(0)}`))
247
+ * // format the table into a new TableData<string> object
248
+ * .flatMap(tf => tf.formatTable())
249
+ * )
250
+ * .getOrThrow()
251
+ *
252
+ * // we expect the data-frame in the table data to be the following
253
+ * const expectedData = DataFrame.from<string>([
254
+ * ['2/1/2021', '12345', 'gnm-f234', '$ 123.45', '4'],
255
+ * ['2/2/2021', '23456', 'gnm-g234', '$ 23.45', '5'],
256
+ * ['2/3/2021', '34567', 'GNM-H234', '$ 3.65', '40'],
257
+ * ['2/4/2021', '45678', 'gnm-i234', '$ 314.15', '9'],
258
+ * ]).getOrThrow()
259
+ * ```
260
+ */
261
+ addRowFormatter(rowIndex: number, formatter: Formatter<V>, priority: number = 0): Result<TableFormatter<V>, string> {
262
+ return this.dataFrame
263
+ .tagRow<Formatting<V>>(rowIndex, TableFormatterType.ROW, {formatter, priority})
264
+ .map(data => new TableFormatter<V>(data))
265
+ }
266
+
267
+ /**
268
+ * Adds formatters to specified row indexes in a table formatter.
269
+ *
270
+ * @param rowIndexes An array of row indexes to which the formatter will be applied.
271
+ * @param formatter The formatting function to apply to the specified columns.
272
+ * @param [priority = 0] The priority level for the formatter. Higher priority formatters are applied first.
273
+ * @return A result indicating the success or failure of adding the row formatters.
274
+ * @see addColumnFormatter
275
+ * @see addRowFormatter
276
+ * @see addRowFormatters
277
+ * @see addCellFormatter
278
+ * @see addCellFormatters
279
+ * @example
280
+ * ```typescript
281
+ * const data = DataFrame.from<string | number | Date>([
282
+ * [dateTimeFor(1, 1), 12345, 'gnm-f234', 123.45, 4],
283
+ * [dateTimeFor(2, 2), 23456, 'gnm-g234', 23.45, 5],
284
+ * [dateTimeFor(3, 3), 34567, 'gnm-h234', 3.65, 40],
285
+ * [dateTimeFor(4, 4), 45678, 'gnm-i234', 314.15, 9],
286
+ * ]).getOrThrow()
287
+ * const columnHeader = ['Date-Time', 'Customer ID', 'Product ID', 'Purchase Price', 'Amount']
288
+ * const rowHeader = [1, 2, 3, 4]
289
+ *
290
+ * const tableData = TableData.fromDataFrame<string | number | Date>(data)
291
+ * .withColumnHeader(columnHeader)
292
+ * .flatMap(td => td.withRowHeader(rowHeader))
293
+ * .flatMap(tableData => TableFormatter.fromTableData(tableData)
294
+ * // add the default formatter for the column header, at the highest priority so that
295
+ * // it is the one that applies to the row representing the column header
296
+ * .addRowFormatters([0], defaultFormatter, Infinity)
297
+ * // add the default formatter for the row header, at the highest priority so that
298
+ * // it is the one that applies to the column representing the row header
299
+ * .flatMap(tf => tf.addColumnFormatters([0], defaultFormatter, Infinity))
300
+ * // add the column formatters for each column at the default (lowest) priority
301
+ * // (notice that the columns are shifted by one for the columns because the row-header
302
+ * // occupies the first column (index=0))
303
+ * .flatMap(tf => tf.addColumnFormatter(1, value => (value as Date).toLocaleDateString()))
304
+ * .flatMap(tf => tf.addColumnFormatter(2, value => defaultFormatter(value)))
305
+ * .flatMap(tf => tf.addColumnFormatter(4, value => `$ ${(value as number).toFixed(2)}`))
306
+ * .flatMap(tf => tf.addColumnFormatter(5, value => `${(value as number).toFixed(0)}`))
307
+ * // format the table data and get back a TableData<string>
308
+ * .flatMap(tf => tf.formatTable())
309
+ * )
310
+ * .getOrThrow()
311
+ *
312
+ * const expectedData = DataFrame.from<string>([
313
+ * ['2/1/2021', '12345', 'gnm-f234', '$ 123.45', '4'],
314
+ * ['2/2/2021', '23456', 'gnm-g234', '$ 23.45', '5'],
315
+ * ['2/3/2021', '34567', 'gnm-h234', '$ 3.65', '40'],
316
+ * ['2/4/2021', '45678', 'gnm-i234', '$ 314.15', '9'],
317
+ * ]).getOrThrow()
318
+ * ```
319
+ */
320
+ addRowFormatters(rowIndexes: Array<number>, formatter: Formatter<V>, priority: number = 0): Result<TableFormatter<V>, string> {
321
+ return TableFormatter.addRowFormatters(this, rowIndexes.slice(), formatter, priority)
322
+ }
323
+
324
+ private static addRowFormatters<V>(
325
+ tableFormatter: TableFormatter<V>,
326
+ rowIndexes: Array<number>,
327
+ formatter: Formatter<V>,
328
+ priority: number = 0
329
+ ): Result<TableFormatter<V>, string> {
330
+ if (rowIndexes.length > 0) {
331
+ const rowIndex = rowIndexes.shift()
332
+ if (rowIndex != null) {
333
+ return tableFormatter
334
+ .addRowFormatter(rowIndex, formatter, priority)
335
+ .flatMap(tf => TableFormatter.addRowFormatters(tf, rowIndexes, formatter, priority))
336
+ }
337
+ }
338
+ return successResult(tableFormatter)
339
+ }
340
+
341
+ /**
342
+ * Adds a formatter to a specified cell based on the row and column indexes.
343
+ * @param rowIndex The row index of the cell.
344
+ * @param columnIndex The column index of the cell.
345
+ * @param formatter The formatter to apply to the cell.
346
+ * @param [priority = 0] The priority level for the formatter. Higher priority formatters are applied first.
347
+ * @return A result indicating the success or failure of adding the cell formatter.
348
+ * @see addColumnFormatter
349
+ * @see addRowFormatter
350
+ * @see addRowFormatters
351
+ * @see addCellFormatters
352
+ * @see addCellFormatters
353
+ * @example
354
+ * ```typescript
355
+ * const data = DataFrame.from<string | number | Date>([
356
+ * [dateTimeFor(1, 1), 12345, 'gnm-f234', 123.45, 4],
357
+ * [dateTimeFor(2, 2), 23456, 'gnm-g234', 23.45, 5],
358
+ * [dateTimeFor(3, 3), 34567, 'gnm-h234', 3.65, 40],
359
+ * [dateTimeFor(4, 4), 45678, 'gnm-i234', 314.15, 9],
360
+ * ]).getOrThrow()
361
+ * const columnHeader = ['Date-Time', 'Customer ID', 'Product ID', 'Purchase Price', 'Amount']
362
+ * const rowHeader = [1, 2, 3, 4]
363
+ *
364
+ * const tableData = TableData.fromDataFrame<string | number | Date>(data)
365
+ * .withColumnHeader(columnHeader)
366
+ * .flatMap(td => td.withRowHeader(rowHeader))
367
+ * .flatMap(tableData => TableFormatter.fromTableData(tableData)
368
+ * // add the default formatter for the column header, at the highest priority so that
369
+ * // it is the one that applies to the row representing the column header
370
+ * .addRowFormatters([0], defaultFormatter, Infinity)
371
+ * // add the default formatter for the row header, at the highest priority so that
372
+ * // it is the one that applies to the column representing the row header
373
+ * .flatMap(tf => tf.addColumnFormatters([0], defaultFormatter, Infinity))
374
+ * // add the column formatters for each column at the default (lowest) priority
375
+ * // (notice that the columns are shifted by one for the columns because the row-header
376
+ * // occupies the first column (index=0))
377
+ * .flatMap(tf => tf.addColumnFormatter(1, value => (value as Date).toLocaleDateString()))
378
+ * .flatMap(tf => tf.addColumnFormatter(2, value => defaultFormatter(value)))
379
+ * .flatMap(tf => tf.addColumnFormatter(4, value => `$ ${(value as number).toFixed(2)}`))
380
+ * .flatMap(tf => tf.addColumnFormatter(5, value => `${(value as number).toFixed(0)}`))
381
+ * // override the cell formatter for a select set of cells
382
+ * .flatMap(tf => tf.addCellFormatter(1, 4, value => `${(value as number).toFixed(2)}`, 1000))
383
+ * .flatMap(tf => tf.addCellFormatters([[3, 5], [4, 5]], value => `${((value as number) * 10).toFixed(0)}`, 1000))
384
+ * // format the table data and get back a TableData<string>
385
+ * .flatMap(tf => tf.formatTable())
386
+ * )
387
+ * .getOrThrow()
388
+ *
389
+ * const expectedData = DataFrame.from<string>([
390
+ * ['2/1/2021', '12345', 'gnm-f234', '123.45', '4'], // overwrite (1, 4) to remove "$"
391
+ * ['2/2/2021', '23456', 'gnm-g234', '$ 23.45', '5'],
392
+ * ['2/3/2021', '34567', 'gnm-h234', '$ 3.65', '400'], // overwrite (3, 5) to multiply by 10
393
+ * ['2/4/2021', '45678', 'gnm-i234', '$ 314.15', '90'], // overwrite (4, 5) to multiply by 10
394
+ * ]).getOrThrow()
395
+ * ```
396
+ */
397
+ addCellFormatter(rowIndex: number, columnIndex: number, formatter: Formatter<V>, priority: number = 0): Result<TableFormatter<V>, string> {
398
+ return this.dataFrame
399
+ .tagCell(rowIndex, columnIndex, TableFormatterType.CELL, {formatter, priority})
400
+ .map(data => new TableFormatter<V>(data))
401
+ }
402
+
403
+ /**
404
+ * Adds formatters to specified cell indexes in a table formatter.
405
+ * @param cellIndexes An array of cell indexes to which the formatter will be applied.
406
+ * @param formatter The formatting function to apply to the specified cells.
407
+ * @param [priority = 0] The priority level for the formatter. Higher priority formatters are applied first.
408
+ * @return A result indicating the success or failure of adding the cell formatters.
409
+ * @see addColumnFormatter
410
+ * @see addRowFormatter
411
+ * @see addRowFormatters
412
+ * @see addCellFormatters
413
+ * @see addCellFormatters
414
+ * @example
415
+ * ```typescript
416
+ * const data = DataFrame.from<string | number | Date>([
417
+ * [dateTimeFor(1, 1), 12345, 'gnm-f234', 123.45, 4],
418
+ * [dateTimeFor(2, 2), 23456, 'gnm-g234', 23.45, 5],
419
+ * [dateTimeFor(3, 3), 34567, 'gnm-h234', 3.65, 40],
420
+ * [dateTimeFor(4, 4), 45678, 'gnm-i234', 314.15, 9],
421
+ * ]).getOrThrow()
422
+ * const columnHeader = ['Date-Time', 'Customer ID', 'Product ID', 'Purchase Price', 'Amount']
423
+ * const rowHeader = [1, 2, 3, 4]
424
+ *
425
+ * const tableData = TableData.fromDataFrame<string | number | Date>(data)
426
+ * .withColumnHeader(columnHeader)
427
+ * .flatMap(td => td.withRowHeader(rowHeader))
428
+ * .flatMap(tableData => TableFormatter.fromTableData(tableData)
429
+ * // add the default formatter for the column header, at the highest priority so that
430
+ * // it is the one that applies to the row representing the column header
431
+ * .addRowFormatters([0], defaultFormatter, Infinity)
432
+ * // add the default formatter for the row header, at the highest priority so that
433
+ * // it is the one that applies to the column representing the row header
434
+ * .flatMap(tf => tf.addColumnFormatters([0], defaultFormatter, Infinity))
435
+ * // add the column formatters for each column at the default (lowest) priority
436
+ * // (notice that the columns are shifted by one for the columns because the row-header
437
+ * // occupies the first column (index=0))
438
+ * .flatMap(tf => tf.addColumnFormatter(1, value => (value as Date).toLocaleDateString()))
439
+ * .flatMap(tf => tf.addColumnFormatter(2, value => defaultFormatter(value)))
440
+ * .flatMap(tf => tf.addColumnFormatter(4, value => `$ ${(value as number).toFixed(2)}`))
441
+ * .flatMap(tf => tf.addColumnFormatter(5, value => `${(value as number).toFixed(0)}`))
442
+ * // override the cell formatter for a select set of cells
443
+ * .flatMap(tf => tf.addCellFormatter(1, 4, value => `${(value as number).toFixed(2)}`, 1000))
444
+ * .flatMap(tf => tf.addCellFormatters([[3, 5], [4, 5]], value => `${((value as number) * 10).toFixed(0)}`, 1000))
445
+ * // format the table data and get back a TableData<string>
446
+ * .flatMap(tf => tf.formatTable())
447
+ * )
448
+ * .getOrThrow()
449
+ *
450
+ * const expectedData = DataFrame.from<string>([
451
+ * ['2/1/2021', '12345', 'gnm-f234', '123.45', '4'], // overwrite (1, 4) to remove "$"
452
+ * ['2/2/2021', '23456', 'gnm-g234', '$ 23.45', '5'],
453
+ * ['2/3/2021', '34567', 'gnm-h234', '$ 3.65', '400'], // overwrite (3, 5) to multiply by 10
454
+ * ['2/4/2021', '45678', 'gnm-i234', '$ 314.15', '90'], // overwrite (4, 5) to multiply by 10
455
+ * ]).getOrThrow()
456
+ * ```
457
+ */
458
+ addCellFormatters(cellIndexes: Array<[x: number, y: number]>, formatter: Formatter<V>, priority: number = 0): Result<TableFormatter<V>, string> {
459
+ return TableFormatter.addCellFormatters(this, cellIndexes.slice(), formatter, priority)
460
+ }
461
+
462
+ private static addCellFormatters<V>(
463
+ tableFormatter: TableFormatter<V>,
464
+ cellIndexes: Array<[x: number, y: number]>,
465
+ formatter: Formatter<V>,
466
+ priority: number = 0
467
+ ): Result<TableFormatter<V>, string> {
468
+ if (cellIndexes.length > 0) {
469
+ const [rowIndex, columnIndex] = cellIndexes.shift() ?? [undefined, undefined]
470
+ if (rowIndex != null && columnIndex != null) {
471
+ return tableFormatter
472
+ .addCellFormatter(rowIndex, columnIndex, formatter, priority)
473
+ .flatMap(tf => TableFormatter.addCellFormatters(tf, cellIndexes, formatter, priority))
474
+ }
475
+ }
476
+ return successResult(tableFormatter)
477
+ }
478
+
479
+ /**
480
+ * Formats the table headers, footer, and values using the formatters that have been added
481
+ * to this `TableData<V>` object and returns a new `TableData<string>` object where all the
482
+ * elements have been converted to a formatted string.
483
+ * @return a new `TableData<string>` object where all the elements have been converted to a
484
+ * formatted string.
485
+ * @example
486
+ * ```typescript
487
+ * function dateTimeFor(day: number, hour: number): Date {
488
+ * return new Date(2021, 1, day, hour, 0, 0, 0);
489
+ * }
490
+ *
491
+ * // the headers for the table
492
+ * const columnHeader = ['Date-Time', 'Customer ID', 'Product ID', 'Purchase Price', 'Amount']
493
+ * const rowHeader = [1, 2, 3, 4]
494
+ *
495
+ * // this is the actual data used to creat the data table (in row-form)
496
+ * const data = DataFrame.from<string | number | Date>([
497
+ * [dateTimeFor(1, 1), 12345, 'gnm-f234', 123.45, 4],
498
+ * [dateTimeFor(2, 2), 23456, 'gnm-g234', 23.45, 5],
499
+ * [dateTimeFor(3, 3), 34567, 'gnm-h234', 3.65, 40],
500
+ * [dateTimeFor(4, 4), 45678, 'gnm-i234', 314.15, 9],
501
+ * ]).getOrThrow()
502
+ *
503
+ * // this is what we expect that formatted data to look like
504
+ * const expectedData = DataFrame.from<string>([
505
+ * ['2/1/2021', '12345', 'gnm-f234', '$ 123.45', '4'],
506
+ * ['2/2/2021', '23456', 'gnm-g234', '$ 23.45', '5'],
507
+ * ['2/3/2021', '34567', 'gnm-h234', '$ 3.65', '40'],
508
+ * ['2/4/2021', '45678', 'gnm-i234', '$ 314.15', '9'],
509
+ * ]).getOrThrow()
510
+ *
511
+ * // 1. create a data-table that has a column-header and a row-header and mixed-type
512
+ * // data (number, string, Date)
513
+ * // 2. add formatters for the row and column headers (at highest priority)
514
+ * // 3. add formatters for some of the other data columns
515
+ * // 4. format the table
516
+ * const tableData = TableData.fromDataFrame<string | number | Date>(data)
517
+ * .withColumnHeader(columnHeader)
518
+ * .flatMap(table => table.withRowHeader(rowHeader))
519
+ * // add the default formatter for the column header, at the highest priority so that
520
+ * // it is the one that applies to the row representing the column header
521
+ * .flatMap(td => td.addRowFormatter(0, defaultFormatter, Infinity))
522
+ * // add the default formatter for the row header, at the highest priority so that
523
+ * // it is the one that applies to the column representing the row header
524
+ * .flatMap(td => td.addColumnFormatter(0, defaultFormatter, Infinity))
525
+ * // add the column formatters for each column at the default (lowest) priority
526
+ * // (notice that the columns are shifted by one for the columns because the row-header
527
+ * // occupies the first column (index=0))
528
+ * .flatMap(td => td.addColumnFormatter(1, value => (value as Date).toLocaleDateString()))
529
+ * .flatMap(td => td.addColumnFormatter(2, value => defaultFormatter(value)))
530
+ * .flatMap(td => td.addColumnFormatter(4, value => `$ ${(value as number).toFixed(2)}`))
531
+ * .flatMap(td => td.addColumnFormatter(5, value => `${(value as number).toFixed(0)}`))
532
+ * // format the table data and get back a TableData<string>
533
+ * .map(td => td.formatTable())
534
+ * .getOrThrow()
535
+ *
536
+ * // the column header of the formatted table should be the same as the one specified
537
+ * expect(tableData.columnHeader().getOrThrow()).toEqual(columnHeader)
538
+ *
539
+ * // the row header of the formatted table should be the same as the one specified
540
+ * expect(tableData.rowHeader().getOrThrow()).toEqual(rowHeader.map(hdr => defaultFormatter(hdr)))
541
+ *
542
+ * // the data should be equal to the expected data
543
+ * expect(tableData.data().map(df => df.equals(expectedData)).getOrThrow()).toBeTruthy()
544
+ * ```
545
+ */
546
+ formatTable<C extends TagCoordinate>(): Result<TableData<string>, string> {
547
+ return this.formatTableInto<C, TableData<string>>(dataFrame => TableData.fromDataFrame<string>(dataFrame))
548
+ }
549
+
550
+ /**
551
+ * Generally, the {@link TableFormatter} formats the {@link DataFrame} into a `TableData<string>`
552
+ * where all the elements of the {@link TableData} are a string. Because a formatted table does
553
+ * not necessarily have to be a `TableData<string>` (which would contain a `DataFrame<string>`,
554
+ * this method allows mapping a {@link DataFrame} into any desired type.
555
+ * @param mapper A function that takes a `DataFrame<string>` and returns a desired type (`D`).
556
+ * @return A `Result<D, string>` where the `Result<D, string>` is either success, containing the
557
+ * desired type (`D`), or failure, containing an error message.
558
+ * @see formatTable
559
+ */
560
+ formatTableInto<C extends TagCoordinate, D = TableData<string>>(mapper: (dataFrame: DataFrame<string>) => D): Result<D, string> {
561
+ const formattingFailures: Array<string> = []
562
+ const formattedDataFrame = this
563
+ .dataFrame
564
+ .mapElements<string>((elem, row, col) => {
565
+ const tags = this.dataFrame
566
+ .tagsFor(row, col)
567
+ .filter(tag => isFormattingTag(tag)) as Array<Tag<Formatting<V>, C>>
568
+ const sorted = tags
569
+ .sort((t1: Tag<Formatting<V>, C>, t2: Tag<Formatting<V>, C>) => t2.value.priority - t1.value.priority)
570
+ if (sorted.length > 0) {
571
+ const formatter = sorted[0].value.formatter
572
+ try {
573
+ return formatter(elem)
574
+ } catch (e) {
575
+ formattingFailures.push(
576
+ `(TableData::formatTable) Failed to format cell (${row}, ${col}); value: ${elem}; error: ${e}`
577
+ )
578
+ }
579
+ }
580
+ return defaultFormatter<V>(elem)
581
+ })
582
+ if (formattingFailures.length === 0) {
583
+ return successResult(mapper(formattedDataFrame))
584
+ }
585
+ return failureResult(formattingFailures.concat('').join('\n'))
586
+ }
587
+ }