svg-table 0.0.1

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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/d3types.ts +17 -0
  4. package/dist/d3types.d.ts +12 -0
  5. package/dist/d3types.d.ts.map +1 -0
  6. package/dist/d3types.js +3 -0
  7. package/dist/d3types.js.map +1 -0
  8. package/dist/index.d.ts +10 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +36 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/stylings.d.ts +206 -0
  13. package/dist/stylings.d.ts.map +1 -0
  14. package/dist/stylings.js +123 -0
  15. package/dist/stylings.js.map +1 -0
  16. package/dist/tableData.d.ts +168 -0
  17. package/dist/tableData.d.ts.map +1 -0
  18. package/dist/tableData.js +329 -0
  19. package/dist/tableData.js.map +1 -0
  20. package/dist/tableData.test.d.ts +2 -0
  21. package/dist/tableData.test.d.ts.map +1 -0
  22. package/dist/tableData.test.js +259 -0
  23. package/dist/tableData.test.js.map +1 -0
  24. package/dist/tableFormatter.d.ts +179 -0
  25. package/dist/tableFormatter.d.ts.map +1 -0
  26. package/dist/tableFormatter.js +298 -0
  27. package/dist/tableFormatter.js.map +1 -0
  28. package/dist/tableFormatter.test.d.ts +2 -0
  29. package/dist/tableFormatter.test.d.ts.map +1 -0
  30. package/dist/tableFormatter.test.js +101 -0
  31. package/dist/tableFormatter.test.js.map +1 -0
  32. package/dist/tableStyler.d.ts +310 -0
  33. package/dist/tableStyler.d.ts.map +1 -0
  34. package/dist/tableStyler.js +665 -0
  35. package/dist/tableStyler.js.map +1 -0
  36. package/dist/tableStyler.test.d.ts +2 -0
  37. package/dist/tableStyler.test.d.ts.map +1 -0
  38. package/dist/tableStyler.test.js +225 -0
  39. package/dist/tableStyler.test.js.map +1 -0
  40. package/dist/tableSvg.d.ts +41 -0
  41. package/dist/tableSvg.d.ts.map +1 -0
  42. package/dist/tableSvg.js +634 -0
  43. package/dist/tableSvg.js.map +1 -0
  44. package/dist/tableUtils.d.ts +14 -0
  45. package/dist/tableUtils.d.ts.map +1 -0
  46. package/dist/tableUtils.js +18 -0
  47. package/dist/tableUtils.js.map +1 -0
  48. package/eslint.config.js +23 -0
  49. package/index.ts +82 -0
  50. package/jest.config.js +5 -0
  51. package/package.json +44 -0
  52. package/stylings.ts +311 -0
  53. package/svg-table-0.0.1-snapshot.tgz +0 -0
  54. package/tableData.test.ts +290 -0
  55. package/tableData.ts +359 -0
  56. package/tableFormatter.test.ts +122 -0
  57. package/tableFormatter.ts +306 -0
  58. package/tableStyler.test.ts +268 -0
  59. package/tableStyler.ts +798 -0
  60. package/tableSvg.ts +820 -0
  61. package/tableUtils.ts +20 -0
  62. package/tsconfig.json +102 -0
@@ -0,0 +1,122 @@
1
+ import {TableData} from "./tableData";
2
+ import {DataFrame} from "data-frame-ts"
3
+ import {TableFormatter, defaultFormatter} from "./tableFormatter";
4
+
5
+
6
+ describe('creating tables with mixed data types', () => {
7
+ function dateTimeFor(day: number, hour: number): Date {
8
+ return new Date(2021, 1, day, hour, 0, 0, 0);
9
+ }
10
+
11
+ const data = DataFrame.from<string | number | Date>([
12
+ [dateTimeFor(1, 1), 12345, 'gnm-f234', 123.45, 4],
13
+ [dateTimeFor(2, 2), 23456, 'gnm-g234', 23.45, 5],
14
+ [dateTimeFor(3, 3), 34567, 'gnm-h234', 3.65, 40],
15
+ [dateTimeFor(4, 4), 45678, 'gnm-i234', 314.15, 9],
16
+ ]).getOrThrow()
17
+ const columnHeader = ['Date-Time', 'Customer ID', 'Product ID', 'Purchase Price', 'Amount']
18
+ const rowHeader = [1, 2, 3, 4]
19
+
20
+ test('should be able to create a table with string headers and numeric values', () => {
21
+
22
+ const expectedData = DataFrame.from<string>([
23
+ ['2/1/2021', '12345', 'gnm-f234', '$ 123.45', '4'],
24
+ ['2/2/2021', '23456', 'gnm-g234', '$ 23.45', '5'],
25
+ ['2/3/2021', '34567', 'GNM-H234', '$ 3.65', '40'],
26
+ ['2/4/2021', '45678', 'gnm-i234', '$ 314.15', '9'],
27
+ ]).getOrThrow()
28
+
29
+ const tableData = TableData.fromDataFrame<string | number | Date>(data)
30
+ .withColumnHeader(columnHeader)
31
+ .flatMap(tableData => TableFormatter.fromTableData(tableData)
32
+ // add the default formatter for the column header, at the highest priority so that
33
+ // it is the one that applies to the row representing the column header
34
+ .addRowFormatter(0, defaultFormatter, 100)
35
+ // add the column formatters for each column at the default (lowest) priority
36
+ .flatMap(tf => tf.addColumnFormatter(0, value => (value as Date).toLocaleDateString()))
37
+ .flatMap(tf => tf.addColumnFormatter(1, value => defaultFormatter(value)))
38
+ .flatMap(tf => tf.addColumnFormatter(3, value => `$ ${(value as number).toFixed(2)}`))
39
+ .flatMap(tf => tf.addColumnFormatter(4, value => `${(value as number).toFixed(0)}`))
40
+ .flatMap(tf => tf.addCellFormatter(3, 2, value => (value as string).toUpperCase(), 1))
41
+ // format the table into a new TableData object
42
+ .flatMap(tf => tf.formatTable())
43
+ )
44
+ .getOrThrow()
45
+
46
+ expect(tableData.columnHeader().getOrThrow()).toEqual(columnHeader)
47
+ expect(tableData.data().map(df => df.equals(expectedData)).getOrThrow()).toBeTruthy()
48
+ expect(tableData.tableColumnCount()).toEqual(5)
49
+ expect(tableData.tableRowCount()).toEqual(/*data*/4 + /*header*/ 1)
50
+ expect(tableData.hasColumnHeader()).toBeTruthy()
51
+ expect(tableData.hasRowHeader()).toBeFalsy()
52
+ expect(tableData.hasFooter()).toBeFalsy()
53
+ })
54
+
55
+ test('should be able to create a table with string column and row headers and numeric values', () => {
56
+
57
+ const expectedData = DataFrame.from<string>([
58
+ ['2/1/2021', '12345', 'gnm-f234', '$ 123.45', '4'],
59
+ ['2/2/2021', '23456', 'gnm-g234', '$ 23.45', '5'],
60
+ ['2/3/2021', '34567', 'gnm-h234', '$ 3.65', '40'],
61
+ ['2/4/2021', '45678', 'gnm-i234', '$ 314.15', '9'],
62
+ ]).getOrThrow()
63
+
64
+ const tableData = TableData.fromDataFrame<string | number | Date>(data)
65
+ .withColumnHeader(columnHeader)
66
+ .flatMap(td => td.withRowHeader(rowHeader))
67
+ .flatMap(tableData => TableFormatter.fromTableData(tableData)
68
+ // add the default formatter for the column header, at the highest priority so that
69
+ // it is the one that applies to the row representing the column header
70
+ .addRowFormatter(0, defaultFormatter, Infinity)
71
+ // add the default formatter for the row header, at the highest priority so that
72
+ // it is the one that applies to the column representing the row header
73
+ .flatMap(tf => tf.addColumnFormatter(0, defaultFormatter, Infinity))
74
+ // add the column formatters for each column at the default (lowest) priority
75
+ // (notice that the columns are shifted by one for the columns because the row-header
76
+ // occupies the first column (index=0))
77
+ .flatMap(tf => tf.addColumnFormatter(1, value => (value as Date).toLocaleDateString()))
78
+ .flatMap(tf => tf.addColumnFormatter(2, value => defaultFormatter(value)))
79
+ .flatMap(tf => tf.addColumnFormatter(4, value => `$ ${(value as number).toFixed(2)}`))
80
+ .flatMap(tf => tf.addColumnFormatter(5, value => `${(value as number).toFixed(0)}`))
81
+ // format the table data and get back a TableData<string>
82
+ .flatMap(tf => tf.formatTable())
83
+ )
84
+ .getOrThrow()
85
+
86
+ expect(tableData.columnHeader().getOrThrow()).toEqual(columnHeader)
87
+ expect(tableData.rowHeader().getOrThrow()).toEqual(rowHeader.map(hdr => defaultFormatter(hdr)))
88
+ expect(tableData.data().map(df => df.equals(expectedData)).getOrThrow()).toBeTruthy()
89
+ expect(tableData.tableColumnCount()).toEqual(5 + 1) // data + row-header
90
+ expect(tableData.tableRowCount()).toEqual(4 + 1) // data + column-header
91
+ expect(tableData.hasColumnHeader()).toBeTruthy()
92
+ expect(tableData.hasRowHeader()).toBeTruthy()
93
+ expect(tableData.hasFooter()).toBeFalsy()
94
+ })
95
+
96
+ test('should be report failures when formatting function fails', () => {
97
+ const columnHeader = ['Date-Time', 'Customer ID', 'Product ID', 'Purchase Price', 'Amount']
98
+
99
+ const result = TableData.fromDataFrame<string | number | Date>(data)
100
+ .withColumnHeader(columnHeader)
101
+ .flatMap(tableData => TableFormatter.fromDataFrame(tableData.unwrapDataFrame())
102
+ // add the default formatter for the column header, at the highest priority so that
103
+ // it is the one that applies to the row representing the column header
104
+ .addRowFormatter(0, defaultFormatter, 100)
105
+ // add a column formatter to the incorrect column (data-type is a number) and attempt to
106
+ // format it as if it where a string. errors will be collected for each format error
107
+ .flatMap(tf => tf.addColumnFormatter(3, value => (value as string).toUpperCase()))
108
+ // format the table data and get back a TableData<string>
109
+ .flatMap(tf => tf.formatTable())
110
+ )
111
+
112
+ expect(result.failed).toBeTruthy()
113
+ expect(result.error).toEqual(
114
+ `(TableData::formatTable) Failed to format cell (1, 3); value: 123.45; error: TypeError: value.toUpperCase is not a function
115
+ (TableData::formatTable) Failed to format cell (2, 3); value: 23.45; error: TypeError: value.toUpperCase is not a function
116
+ (TableData::formatTable) Failed to format cell (3, 3); value: 3.65; error: TypeError: value.toUpperCase is not a function
117
+ (TableData::formatTable) Failed to format cell (4, 3); value: 314.15; error: TypeError: value.toUpperCase is not a function
118
+ `
119
+ )
120
+ })
121
+
122
+ })
@@ -0,0 +1,306 @@
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
+ * @example
92
+ * ```typescript
93
+ * // create the data
94
+ * const data = DataFrame.from<string | number | Date>([
95
+ * [dateTimeFor(1, 1), 12345, 'gnm-f234', 123.45, 4],
96
+ * [dateTimeFor(2, 2), 23456, 'gnm-g234', 23.45, 5],
97
+ * [dateTimeFor(3, 3), 34567, 'gnm-h234', 3.65, 40],
98
+ * [dateTimeFor(4, 4), 45678, 'gnm-i234', 314.15, 9],
99
+ * ]).getOrThrow()
100
+ *
101
+ * // create the table-data object from the data, and then hand the table-data
102
+ * // to the table formatter, add column formats, and format the table, getting
103
+ * // back a new TableData<string>
104
+ * const tableData: TableData<string> = TableData.fromDataFrame<string | number | Date>(data)
105
+ * // from the table-data, create a table-formatter
106
+ * .flatMap(tableData => createTableFormatterFrom(tableData)
107
+ * // add a column formatter for the first column of dates
108
+ * .addColumnFormatter(0, value => (value as Date).toLocaleDateString())
109
+ * // add a column formatter to the second column of number
110
+ * .flatMap(tf => tf.addColumnFormatter(1, value => defaultFormatter(value)))
111
+ * // add a column formatter to the fourth column of currencies
112
+ * .flatMap(tf => tf.addColumnFormatter(3, value => `$ ${(value as number).toFixed(2)}`))
113
+ * .flatMap(tf => tf.addColumnFormatter(4, value => `${(value as number).toFixed(0)}`))
114
+ * // format the table into a new TableData<string> object
115
+ * .flatMap(tf => tf.formatTable())
116
+ * )
117
+ * .getOrThrow()
118
+ *
119
+ * // we expect the data-frame in the table data to be the following
120
+ * const expectedData = DataFrame.from<string>([
121
+ * ['2/1/2021', '12345', 'gnm-f234', '$ 123.45', '4'],
122
+ * ['2/2/2021', '23456', 'gnm-g234', '$ 23.45', '5'],
123
+ * ['2/3/2021', '34567', 'GNM-H234', '$ 3.65', '40'],
124
+ * ['2/4/2021', '45678', 'gnm-i234', '$ 314.15', '9'],
125
+ * ]).getOrThrow()
126
+ * ```
127
+ */
128
+ addColumnFormatter(columnIndex: number, formatter: Formatter<V>, priority: number = 0): Result<TableFormatter<V>, string> {
129
+ return this.dataFrame
130
+ .tagColumn<Formatting<V>>(columnIndex, TableFormatterType.COLUMN, {formatter, priority})
131
+ .map(data => new TableFormatter<V>(data))
132
+ }
133
+
134
+ addColumnFormatters(columnIndexes: Array<number>, formatter: Formatter<V>, priority: number = 0): Result<TableFormatter<V>, string> {
135
+ return TableFormatter.addColumnFormatters(this, columnIndexes, formatter, priority)
136
+ }
137
+
138
+ private static addColumnFormatters<V>(
139
+ tableFormatter: TableFormatter<V>,
140
+ columnIndexes: Array<number>,
141
+ formatter: Formatter<V>,
142
+ priority: number = 0
143
+ ): Result<TableFormatter<V>, string> {
144
+ if (columnIndexes.length > 0) {
145
+ const columnIndex = columnIndexes.slice().shift()
146
+ if (columnIndex != null) {
147
+ return tableFormatter
148
+ .addColumnFormatter(columnIndex, formatter, priority)
149
+ .flatMap(tf => TableFormatter.addColumnFormatters(tf, columnIndexes, formatter, priority))
150
+ }
151
+ }
152
+ return successResult(tableFormatter)
153
+ }
154
+
155
+ addRowFormatter(rowIndex: number, formatter: Formatter<V>, priority: number = 0): Result<TableFormatter<V>, string> {
156
+ return this.dataFrame
157
+ .tagRow<Formatting<V>>(rowIndex, TableFormatterType.ROW, {formatter, priority})
158
+ .map(data => new TableFormatter<V>(data))
159
+ }
160
+
161
+ addRowFormatters(rowIndexes: Array<number>, formatter: Formatter<V>, priority: number = 0): Result<TableFormatter<V>, string> {
162
+ return TableFormatter.addRowFormatters(this, rowIndexes.slice(), formatter, priority)
163
+ }
164
+
165
+ private static addRowFormatters<V>(
166
+ tableFormatter: TableFormatter<V>,
167
+ rowIndexes: Array<number>,
168
+ formatter: Formatter<V>,
169
+ priority: number = 0
170
+ ): Result<TableFormatter<V>, string> {
171
+ if (rowIndexes.length > 0) {
172
+ const rowIndex = rowIndexes.shift()
173
+ if (rowIndex != null) {
174
+ return tableFormatter
175
+ .addRowFormatter(rowIndex, formatter, priority)
176
+ .flatMap(tf => TableFormatter.addRowFormatters(tf, rowIndexes, formatter, priority))
177
+ }
178
+ }
179
+ return successResult(tableFormatter)
180
+ }
181
+
182
+ addCellFormatter(rowIndex: number, columnIndex: number, formatter: Formatter<V>, priority: number = 0): Result<TableFormatter<V>, string> {
183
+ return this.dataFrame
184
+ .tagCell(rowIndex, columnIndex, TableFormatterType.CELL, {formatter, priority})
185
+ .map(data => new TableFormatter<V>(data))
186
+ }
187
+
188
+ addCellFormatters(cellIndexes: Array<[x: number, y: number]>, formatter: Formatter<V>, priority: number = 0): Result<TableFormatter<V>, string> {
189
+ return TableFormatter.addCellFormatters(this, cellIndexes, formatter, priority)
190
+ }
191
+
192
+ private static addCellFormatters<V>(
193
+ tableFormatter: TableFormatter<V>,
194
+ cellIndexes: Array<[x: number, y: number]>,
195
+ formatter: Formatter<V>,
196
+ priority: number = 0
197
+ ): Result<TableFormatter<V>, string> {
198
+ if (cellIndexes.length > 0) {
199
+ const [columnIndex, rowIndex] = cellIndexes.slice().shift() ?? [undefined, undefined]
200
+ if (rowIndex != null && columnIndex != null) {
201
+ return tableFormatter
202
+ .addCellFormatter(rowIndex, columnIndex, formatter, priority)
203
+ .flatMap(tf => TableFormatter.addCellFormatters(tf, cellIndexes, formatter, priority))
204
+ }
205
+ }
206
+ return successResult(tableFormatter)
207
+ }
208
+
209
+ /**
210
+ * Formats the table headers, footer, and values using the formatters that have been added
211
+ * to this `TableData<V>` object and returns a new `TableData<string>` object where all the
212
+ * elements have been converted to a formatted string.
213
+ * @return a new `TableData<string>` object where all the elements have been converted to a
214
+ * formatted string.
215
+ * @example
216
+ * ```typescript
217
+ * function dateTimeFor(day: number, hour: number): Date {
218
+ * return new Date(2021, 1, day, hour, 0, 0, 0);
219
+ * }
220
+ *
221
+ * // the headers for the table
222
+ * const columnHeader = ['Date-Time', 'Customer ID', 'Product ID', 'Purchase Price', 'Amount']
223
+ * const rowHeader = [1, 2, 3, 4]
224
+ *
225
+ * // this is the actual data used to creat the data table (in row-form)
226
+ * const data = DataFrame.from<string | number | Date>([
227
+ * [dateTimeFor(1, 1), 12345, 'gnm-f234', 123.45, 4],
228
+ * [dateTimeFor(2, 2), 23456, 'gnm-g234', 23.45, 5],
229
+ * [dateTimeFor(3, 3), 34567, 'gnm-h234', 3.65, 40],
230
+ * [dateTimeFor(4, 4), 45678, 'gnm-i234', 314.15, 9],
231
+ * ]).getOrThrow()
232
+ *
233
+ * // this is what we expect that formatted data to look like
234
+ * const expectedData = DataFrame.from<string>([
235
+ * ['2/1/2021', '12345', 'gnm-f234', '$ 123.45', '4'],
236
+ * ['2/2/2021', '23456', 'gnm-g234', '$ 23.45', '5'],
237
+ * ['2/3/2021', '34567', 'gnm-h234', '$ 3.65', '40'],
238
+ * ['2/4/2021', '45678', 'gnm-i234', '$ 314.15', '9'],
239
+ * ]).getOrThrow()
240
+ *
241
+ * // 1. create a data-table that has a column-header and a row-header and mixed-type
242
+ * // data (number, string, Date)
243
+ * // 2. add formatters for the row and column headers (at highest priority)
244
+ * // 3. add formatters for some of the other data columns
245
+ * // 4. format the table
246
+ * const tableData = TableData.fromDataFrame<string | number | Date>(data)
247
+ * .withColumnHeader(columnHeader)
248
+ * .flatMap(table => table.withRowHeader(rowHeader))
249
+ * // add the default formatter for the column header, at the highest priority so that
250
+ * // it is the one that applies to the row representing the column header
251
+ * .flatMap(td => td.addRowFormatter(0, defaultFormatter, Infinity))
252
+ * // add the default formatter for the row header, at the highest priority so that
253
+ * // it is the one that applies to the column representing the row header
254
+ * .flatMap(td => td.addColumnFormatter(0, defaultFormatter, Infinity))
255
+ * // add the column formatters for each column at the default (lowest) priority
256
+ * // (notice that the columns are shifted by one for the columns because the row-header
257
+ * // occupies the first column (index=0))
258
+ * .flatMap(td => td.addColumnFormatter(1, value => (value as Date).toLocaleDateString()))
259
+ * .flatMap(td => td.addColumnFormatter(2, value => defaultFormatter(value)))
260
+ * .flatMap(td => td.addColumnFormatter(4, value => `$ ${(value as number).toFixed(2)}`))
261
+ * .flatMap(td => td.addColumnFormatter(5, value => `${(value as number).toFixed(0)}`))
262
+ * // format the table data and get back a TableData<string>
263
+ * .map(td => td.formatTable())
264
+ * .getOrThrow()
265
+ *
266
+ * // the column header of the formatted table should be the same as the one specified
267
+ * expect(tableData.columnHeader().getOrThrow()).toEqual(columnHeader)
268
+ *
269
+ * // the row header of the formatted table should be the same as the one specified
270
+ * expect(tableData.rowHeader().getOrThrow()).toEqual(rowHeader.map(hdr => defaultFormatter(hdr)))
271
+ *
272
+ * // the data should be equal to the expected data
273
+ * expect(tableData.data().map(df => df.equals(expectedData)).getOrThrow()).toBeTruthy()
274
+ * ```
275
+ */
276
+ formatTable<C extends TagCoordinate>(): Result<TableData<string>, string> {
277
+ return this.formatTableInto<C, TableData<string>>(dataFrame => TableData.fromDataFrame<string>(dataFrame))
278
+ }
279
+
280
+ formatTableInto<C extends TagCoordinate, D = TableData<string>>(mapper: (dataFrame: DataFrame<string>) => D): Result<D, string> {
281
+ const formattingFailures: Array<string> = []
282
+ const formattedDataFrame = this.dataFrame
283
+ .mapElements<string>((elem, row, col) => {
284
+ const tags = this.dataFrame
285
+ .tagsFor(row, col)
286
+ .filter(tag => isFormattingTag(tag)) as Array<Tag<Formatting<V>, C>>
287
+ const sorted = tags
288
+ .sort((t1: Tag<Formatting<V>, C>, t2: Tag<Formatting<V>, C>) => t2.value.priority - t1.value.priority)
289
+ if (sorted.length > 0) {
290
+ const formatter = sorted[0].value.formatter
291
+ try {
292
+ return formatter(elem)
293
+ } catch (e) {
294
+ formattingFailures.push(
295
+ `(TableData::formatTable) Failed to format cell (${row}, ${col}); value: ${elem}; error: ${e}`
296
+ )
297
+ }
298
+ }
299
+ return defaultFormatter<V>(elem)
300
+ })
301
+ if (formattingFailures.length === 0) {
302
+ return successResult(mapper(formattedDataFrame))
303
+ }
304
+ return failureResult(formattingFailures.concat('').join('\n'))
305
+ }
306
+ }