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,290 @@
1
+ import {DataFrame} from "data-frame-ts";
2
+ import {TableData} from "./tableData";
3
+
4
+
5
+ describe('creating and manipulating table data', () => {
6
+
7
+ test('should be able to create a simple table from row data without a footer', () => {
8
+ const columnHeader = ['A', 'B', 'C', 'D', 'E']
9
+ const rowHeader = ['one', 'two', 'three', 'four']
10
+ const data = DataFrame.from([
11
+ ['a1', 'b1', 'c1', 'd1', 'e1'],
12
+ ['a2', 'b2', 'c2', 'd2', 'e2'],
13
+ ['a3', 'b3', 'c3', 'd3', 'e3'],
14
+ ['a4', 'b4', 'c4', 'd4', 'e4'],
15
+ ]).getOrThrow()
16
+ const tableData = TableData.fromDataFrame<string>(data)
17
+ .withColumnHeader(columnHeader)
18
+ .flatMap(table => table.withRowHeader(rowHeader))
19
+ .getOrThrow()
20
+ expect(tableData.hasColumnHeader()).toBeTruthy()
21
+ expect(tableData.tableRowCount()).toBe(4 + 1)
22
+ expect(tableData.dataRowCount()).toBe(4)
23
+ expect(tableData.hasFooter()).toBeFalsy()
24
+ expect(tableData.tableColumnCount()).toEqual(5 + 1)
25
+ expect(tableData.dataColumnCount()).toEqual(5)
26
+
27
+ expect(tableData.columnHeader().getOrThrow()).toEqual(['A', 'B', 'C', 'D', 'E'])
28
+ expect(tableData.rowHeader().getOrThrow()).toEqual(['one', 'two', 'three', 'four'])
29
+ expect(tableData.footer().getOrElse([])).toEqual([])
30
+ expect(tableData.data().getOrThrow().rowSlices()).toEqual(data.rowSlices())
31
+ });
32
+
33
+ test('should be able to create a simple table from row data with a footer', () => {
34
+ const columnHeader = ['A', 'B', 'C', 'D', 'E']
35
+ const rowHeader = ['one', 'two', 'three', 'four']
36
+ const footer = ['a10', 'b10', 'c10', 'd10', 'e10']
37
+ const data = DataFrame.from([
38
+ ['a1', 'b1', 'c1', 'd1', 'e1'],
39
+ ['a2', 'b2', 'c2', 'd2', 'e2'],
40
+ ['a3', 'b3', 'c3', 'd3', 'e3'],
41
+ ['a4', 'b4', 'c4', 'd4', 'e4'],
42
+ ]).getOrThrow()
43
+ const tableData = TableData.fromDataFrame<string>(data)
44
+ .withColumnHeader(columnHeader)
45
+ .flatMap(table => table.withFooter(footer))
46
+ .flatMap(table => table.withRowHeader(rowHeader))
47
+ .getOrThrow()
48
+ expect(tableData.hasColumnHeader()).toBeTruthy()
49
+ expect(tableData.tableRowCount()).toBe(/*data*/ 4 + /*header*/ 1 + /*footer*/ 1)
50
+ expect(tableData.hasFooter()).toBeTruthy()
51
+ expect(tableData.tableColumnCount()).toEqual(/*data*/ 5 + /*header*/ 1)
52
+
53
+ expect(tableData.columnHeader().getOrThrow()).toEqual(columnHeader)
54
+ expect(tableData.rowHeader().getOrThrow()).toEqual(rowHeader)
55
+ expect(tableData.footer().getOrThrow()).toEqual(footer)
56
+ expect(tableData.data().getOrThrow().rowSlices()).toEqual(data.rowSlices())
57
+ });
58
+
59
+ test('should be able to create a simple table from row data with a footer 2', () => {
60
+ const columnHeader = ['A', 'B', 'C', 'D', 'E']
61
+ const rowHeader = ['one', 'two', 'three', 'four']
62
+ const footer = ['a10', 'b10', 'c10', 'd10', 'e10']
63
+ const data = DataFrame.from([
64
+ ['a1', 'b1', 'c1', 'd1', 'e1'],
65
+ ['a2', 'b2', 'c2', 'd2', 'e2'],
66
+ ['a3', 'b3', 'c3', 'd3', 'e3'],
67
+ ['a4', 'b4', 'c4', 'd4', 'e4'],
68
+ ]).getOrThrow()
69
+ const tableData = TableData.fromDataFrame<string>(data)
70
+ .withColumnHeader(columnHeader)
71
+ .flatMap(table => table.withRowHeader(rowHeader))
72
+ .flatMap(table => table.withFooter(footer))
73
+ .getOrThrow()
74
+ expect(tableData.hasColumnHeader()).toBeTruthy()
75
+ expect(tableData.tableRowCount()).toBe(/*data*/ 4 + /*header*/ 1 + /*footer*/ 1)
76
+ expect(tableData.hasFooter()).toBeTruthy()
77
+ expect(tableData.tableColumnCount()).toEqual(/*data*/ 5 + /*header*/ 1)
78
+
79
+ expect(tableData.columnHeader().getOrThrow()).toEqual(columnHeader)
80
+ expect(tableData.rowHeader().getOrThrow()).toEqual(rowHeader)
81
+ expect(tableData.footer().getOrThrow()).toEqual(footer)
82
+ expect(tableData.data().getOrThrow().rowSlices()).toEqual(data.rowSlices())
83
+ });
84
+
85
+ test('should be able to create a simple table from row data with a footer 3', () => {
86
+ const columnHeader = ['A', 'B', 'C', 'D', 'E']
87
+ const rowHeader = ['one', 'two', 'three', 'four']
88
+ const footer = ['a10', 'b10', 'c10', 'd10', 'e10']
89
+ const data = DataFrame.from([
90
+ ['a1', 'b1', 'c1', 'd1', 'e1'],
91
+ ['a2', 'b2', 'c2', 'd2', 'e2'],
92
+ ['a3', 'b3', 'c3', 'd3', 'e3'],
93
+ ['a4', 'b4', 'c4', 'd4', 'e4'],
94
+ ]).getOrThrow()
95
+ const tableData = TableData.fromDataFrame<string>(data)
96
+ .withFooter(footer)
97
+ .flatMap(table => table.withColumnHeader(columnHeader))
98
+ .flatMap(table => table.withRowHeader(rowHeader))
99
+ .getOrThrow()
100
+ expect(tableData.hasColumnHeader()).toBeTruthy()
101
+ expect(tableData.tableRowCount()).toBe(/*data*/ 4 + /*header*/ 1 + /*footer*/ 1)
102
+ expect(tableData.hasFooter()).toBeTruthy()
103
+ expect(tableData.tableColumnCount()).toEqual(/*data*/ 5 + /*header*/ 1)
104
+
105
+ expect(tableData.columnHeader().getOrThrow()).toEqual(columnHeader)
106
+ expect(tableData.rowHeader().getOrThrow()).toEqual(rowHeader)
107
+ expect(tableData.footer().getOrThrow()).toEqual(footer)
108
+ expect(tableData.data().getOrThrow().rowSlices()).toEqual(data.rowSlices())
109
+ });
110
+
111
+ test('should be able to create a simple table from row data with a footer with no header', () => {
112
+ const columnHeader = ['A', 'B', 'C', 'D', 'E']
113
+ const rowHeader = ['one', 'two', 'three', 'four']
114
+ const footer = ['a10', 'b10', 'c10', 'd10', 'e10']
115
+ const data = DataFrame.from([
116
+ ['a1', 'b1', 'c1', 'd1', 'e1'],
117
+ ['a2', 'b2', 'c2', 'd2', 'e2'],
118
+ ['a3', 'b3', 'c3', 'd3', 'e3'],
119
+ ['a4', 'b4', 'c4', 'd4', 'e4'],
120
+ ]).getOrThrow()
121
+ const tableData = TableData.fromDataFrame<string>(data)
122
+ .withColumnHeader(columnHeader)
123
+ .flatMap(table => table.withFooter(footer))
124
+ .flatMap(table => table.withRowHeader(rowHeader))
125
+ .getOrThrow()
126
+ expect(tableData.hasColumnHeader()).toBeTruthy()
127
+ expect(tableData.tableRowCount()).toBe(/*data*/ 4 + /*header*/ 1 + /*footer*/ 1)
128
+ expect(tableData.hasFooter()).toBeTruthy()
129
+ expect(tableData.tableColumnCount()).toEqual(/*data*/ 5 + /*header*/ 1)
130
+
131
+ expect(tableData.columnHeader().getOrThrow()).toEqual(columnHeader)
132
+ expect(tableData.rowHeader().getOrThrow()).toEqual(rowHeader)
133
+ expect(tableData.footer().getOrThrow()).toEqual(footer)
134
+ expect(tableData.data().getOrThrow().equals(data)).toBeTruthy()
135
+ });
136
+
137
+ test('should be able to create a simple table from column data without a footer', () => {
138
+ const columnHeader = ['A', 'B', 'C', 'D'];
139
+ const rowHeader = ['one', 'two', 'three', 'four', 'five']
140
+ const data = DataFrame.fromColumnData([
141
+ ['a1', 'b1', 'c1', 'd1', 'e1'],
142
+ ['a2', 'b2', 'c2', 'd2', 'e2'],
143
+ ['a3', 'b3', 'c3', 'd3', 'e3'],
144
+ ['a4', 'b4', 'c4', 'd4', 'e4'],
145
+ ]).getOrThrow()
146
+ const tableData = TableData.fromDataFrame<string>(data)
147
+ .withColumnHeader(columnHeader)
148
+ .flatMap(table => table.withRowHeader(rowHeader))
149
+ .getOrThrow()
150
+ expect(tableData.hasRowHeader()).toBeTruthy()
151
+ expect(tableData.tableRowCount()).toEqual(5 + 1) // the thing is transposed
152
+ expect(tableData.hasFooter()).toBeFalsy()
153
+ expect(tableData.tableColumnCount()).toEqual(4 + 1)
154
+
155
+ expect(tableData.columnHeader().getOrThrow()).toEqual(columnHeader)
156
+ expect(tableData.rowHeader().getOrThrow()).toEqual(rowHeader)
157
+ expect(tableData.data().getOrThrow().equals(data)).toBeTruthy()
158
+ });
159
+
160
+ test('should be able to create a simple table from column data without a footer 2', () => {
161
+ const columnHeader = ['A', 'B', 'C', 'D'];
162
+ const rowHeader = ['one', 'two', 'three', 'four', 'five']
163
+ const data = DataFrame.fromColumnData([
164
+ ['a1', 'b1', 'c1', 'd1', 'e1'],
165
+ ['a2', 'b2', 'c2', 'd2', 'e2'],
166
+ ['a3', 'b3', 'c3', 'd3', 'e3'],
167
+ ['a4', 'b4', 'c4', 'd4', 'e4'],
168
+ ]).getOrThrow()
169
+ const tableData = TableData.fromDataFrame<string>(data)
170
+ .withRowHeader(rowHeader)
171
+ .flatMap(table => table.withColumnHeader(columnHeader))
172
+ .getOrThrow()
173
+ expect(tableData.hasRowHeader()).toBeTruthy()
174
+ expect(tableData.tableRowCount()).toEqual(5 + 1) // the thing is transposed
175
+ expect(tableData.hasFooter()).toBeFalsy()
176
+ expect(tableData.tableColumnCount()).toEqual(4 + 1)
177
+
178
+ expect(tableData.columnHeader().getOrThrow()).toEqual(columnHeader)
179
+ expect(tableData.rowHeader().getOrThrow()).toEqual(rowHeader)
180
+ expect(tableData.data().getOrThrow().equals(data)).toBeTruthy()
181
+ });
182
+
183
+ test('should be able to create a table from column data without a footer', () => {
184
+ const header = ['A', 'B', 'C', 'D'];
185
+ const data = DataFrame.fromColumnData<string | number>([
186
+ ['a1', 'b1', 'c1', 'd1', 'e1'],
187
+ [10, 20, 30, 40, 50],
188
+ ['a3', 'b3', 'c3', 'd3', 'e3'],
189
+ ['a4', 'b4', 'c4', 'd4', 'e4'],
190
+ ]).getOrThrow()
191
+ const tableData = TableData.fromDataFrame<string | number>(data)
192
+ .withColumnHeader(header)
193
+ .getOrThrow()
194
+ expect(tableData.tableRowCount()).toBe(6)
195
+ expect(tableData.hasFooter()).toBeFalsy()
196
+ expect(tableData.tableColumnCount()).toEqual(4)
197
+
198
+ expect(tableData.columnHeader().getOrThrow()).toEqual(header)
199
+ expect(tableData.data().getOrThrow().equals(data)).toBeTruthy()
200
+ });
201
+
202
+ test('should throw error when the data dimensions are inconsistent with the header dimensions', () => {
203
+ let result = DataFrame.from([['a1'], ['a2']])
204
+ .map(df => TableData.fromDataFrame<string>(df))
205
+ .flatMap(table => table.withColumnHeader(['a', 'b']))
206
+ expect(result.failed).toBeTruthy()
207
+ expect(result.error).toEqual("(DataFrame::insertRowBefore) The row must have the same number of elements as the data has columns. num_rows: 2; num_columns: 2")
208
+
209
+ result = DataFrame.fromColumnData([['a1'], ['a2'], ['a3']])
210
+ .map(df => TableData.fromDataFrame<string>(df))
211
+ .flatMap(table => table.withColumnHeader(['a', 'b']))
212
+ expect(result.failed).toBeTruthy()
213
+ expect(result.error).toEqual("(DataFrame::insertRowBefore) The row must have the same number of elements as the data has columns. num_rows: 1; num_columns: 2")
214
+ })
215
+
216
+ test('should throw error when the rows do not all have the same number of columns', () => {
217
+ let result = DataFrame.from([['a1', 'a2'], ['b2']])
218
+ .map(df => TableData.fromDataFrame<string>(df))
219
+ .flatMap(table => table.withColumnHeader(['a', 'b']))
220
+ expect(result.failed).toBeTruthy()
221
+ expect(result.error).toEqual("(DataFrame.validateDimensions) All rows must have the same number of columns; min_num_columns: 1, maximum_columns: 2")
222
+
223
+ result = DataFrame.fromColumnData([['a1'], ['a2', 'b2']])
224
+ .map(df => TableData.fromDataFrame<string>(df))
225
+ .flatMap(table => table.withColumnHeader(['a', 'b']))
226
+ expect(result.failed).toBeTruthy()
227
+ expect(result.error).toEqual("(DataFrame.validateDimensions) All columns must have the same number of rows; min_num_rows: 1, maximum_rows: 2")
228
+ })
229
+
230
+ test('should be able to create a table of numbers when no formatter is specified', () => {
231
+ const tableData = TableData.fromDataFrame<number>(DataFrame.from([[11, 12, 13], [21, 22, 23]]).getOrThrow())
232
+ expect(tableData.tableRowCount()).toEqual(2)
233
+ expect(tableData.data().flatMap(df => df.rowSlice(0).map(row => row.length)).getOrThrow()).toEqual(3)
234
+ expect(tableData.data().flatMap(df => df.rowSlice(1).map(row => row.length)).getOrThrow()).toEqual(3)
235
+ })
236
+
237
+ describe('retrieving information about the table', () => {
238
+ const columnHeader = ['A', 'B', 'C', 'D', 'E']
239
+ const rowHeader = ['one', 'two', 'three', 'four']
240
+ const footer = ['a10', 'b10', 'c10', 'd10', 'e10']
241
+ const data = DataFrame.from([
242
+ ['a1', 'b1', 'c1', 'd1', 'e1'],
243
+ ['a2', 'b2', 'c2', 'd2', 'e2'],
244
+ ['a3', 'b3', 'c3', 'd3', 'e3'],
245
+ ['a4', 'b4', 'c4', 'd4', 'e4'],
246
+ ]).getOrThrow()
247
+
248
+ const tableData = TableData.fromDataFrame<string>(data)
249
+ .withColumnHeader(columnHeader)
250
+ .flatMap(table => table.withRowHeader(rowHeader))
251
+ .flatMap(table => table.withFooter(footer))
252
+ .getOrThrow()
253
+
254
+ test("should be able to get the column header", () => {
255
+ expect(tableData.columnHeader().getOrThrow()).toEqual(columnHeader)
256
+ })
257
+
258
+ test("should be able to get the row header", () => {
259
+ expect(tableData.rowHeader().getOrThrow()).toEqual(rowHeader)
260
+ })
261
+
262
+ test("should be able to get the footer", () => {
263
+ expect(tableData.footer().getOrThrow()).toEqual(footer)
264
+ })
265
+
266
+ test("should be able to get the data", () => {
267
+ expect(tableData.data().getOrThrow().equals(data)).toBeTruthy()
268
+ })
269
+
270
+ test("should be able to get the table row count", () => {
271
+ expect(tableData.tableRowCount()).toEqual(4 + 1 + 1) // data + column_header + footer
272
+ })
273
+
274
+ test("should be able to get the table column count", () => {
275
+ expect(tableData.tableColumnCount()).toEqual(5 + 1) // data + row_header
276
+ })
277
+
278
+ test("should be able to determine whether the table has a column header", () => {
279
+ expect(tableData.hasColumnHeader()).toBeTruthy()
280
+ })
281
+
282
+ test("should be able to determine whether the table has a row header", () => {
283
+ expect(tableData.hasRowHeader()).toBeTruthy()
284
+ })
285
+
286
+ test("should be able to determine whether the table has a footer", () => {
287
+ expect(tableData.hasFooter()).toBeTruthy()
288
+ })
289
+ })
290
+ })
package/tableData.ts ADDED
@@ -0,0 +1,359 @@
1
+ import {DataFrame} from "data-frame-ts";
2
+ import {failureResult, type Result, successResult} from "result-fn";
3
+ import {indexFrom} from "data-frame-ts/dist/DataFrame";
4
+ import {defaultFormatting, type Formatting, TableFormatterType} from "./tableFormatter";
5
+
6
+ /**
7
+ * The types of tags the {@link TableData} supports
8
+ */
9
+ export enum TableTagType {
10
+ COLUMN_HEADER = "column-header",
11
+ ROW_HEADER = "row-header",
12
+ FOOTER = "footer"
13
+ }
14
+
15
+ /**
16
+ * Represents the table data, row and column headers, and footers
17
+ */
18
+ export class TableData<V> {
19
+ /**
20
+ * The matrix of data, including row, column headers, and footers, if they exist
21
+ */
22
+ private constructor(private readonly dataFrame: DataFrame<V>) {
23
+ }
24
+
25
+ static fromDataFrame<V>(dataFrame: DataFrame<V>): TableData<V> {
26
+ return new TableData<V>(dataFrame.copy())
27
+ }
28
+
29
+ /**
30
+ * Adds a column-header where each element in the column-header is the name of the column.
31
+ * Note that the specified column {@link header} determines the number of data columns in the table. This
32
+ * means that you must specify a header for each data column. No need to account for a possible column
33
+ * containing row-headers. The builder will take care of any adjustments needed for that.
34
+ * @param header An array describing the columns in the table.
35
+ * @param formatting The formatter, and its priority, for the row that represents the column-header. Note that the column
36
+ * header should not account for a possible column containing row-headers. The builder will take care
37
+ * of any adjustments needed for that.
38
+ * @param rowHeaderProvider
39
+ * @return A {@link TableData} which represents the next step in the guided builder
40
+ */
41
+ public withColumnHeader(
42
+ header: Array<V>,
43
+ formatting: Formatting<V> = defaultFormatting<V>(),
44
+ rowHeaderProvider: () => V | undefined = () => undefined
45
+ ): Result<TableData<V>, string> {
46
+ // just return a copy of the data table if the header is empty
47
+ if (header.length === 0) {
48
+ return successResult(new TableData<V>(this.dataFrame.copy()))
49
+ }
50
+
51
+ // when a row header has already been applied, then the table has grown by one column,
52
+ // and so we need to insert an empty cell at the beginning of the column header
53
+ const updatedHeader = header.slice()
54
+ if (this.dataFrame.columnTagsFor(0).some(tag => tag.value === TableTagType.ROW_HEADER)) {
55
+ updatedHeader.unshift(rowHeaderProvider() as V)
56
+ }
57
+
58
+ // when there is already a column-header, then replace it with the new one
59
+ if (this.dataFrame.rowTagsFor(0).some(tag => tag.value === TableTagType.COLUMN_HEADER)) {
60
+ return this.dataFrame
61
+ .mapRow(0, (_, rowIndex) => header[rowIndex])
62
+ .map(df => new TableData<V>(df))
63
+ }
64
+
65
+ return this.dataFrame
66
+ .insertRowBefore(0, updatedHeader)
67
+ .flatMap(df => df.tagRow(0, "column-header", TableTagType.COLUMN_HEADER))
68
+ .flatMap(df => df.tagRow<Formatting<V>>(0, TableFormatterType.COLUMN, formatting))
69
+ .map(df => new TableData<V>(df))
70
+ }
71
+
72
+ public withRowHeader(
73
+ header: Array<V>,
74
+ formatting: Formatting<V> = defaultFormatting<V>(),
75
+ columnHeaderProvider: () => V | undefined = () => undefined,
76
+ footerProvider: () => V | undefined = () => undefined,
77
+ ): Result<TableData<V>, string> {
78
+ // just return a copy of the data table if the header is empty
79
+ if (header.length === 0) {
80
+ return successResult(TableData.fromDataFrame<V>(this.dataFrame.copy()))
81
+ }
82
+
83
+ // when there is a column-header and/or footer, we adjust the (row) header by adding
84
+ // empty elements so that the length of the (row) header matches the number of rows,
85
+ // including the column header and the footer
86
+ const updatedHeader = header.slice()
87
+ // recall that a column-header is a row, specifically the first row
88
+ if (this.dataFrame.rowTagsFor(0).some(tag => tag.value === TableTagType.COLUMN_HEADER)) {
89
+ updatedHeader.unshift(columnHeaderProvider() as V)
90
+ }
91
+ if (this.dataFrame.rowTagsFor(this.dataFrame.rowCount() - 1).some(tag => tag.value === TableTagType.FOOTER)) {
92
+ updatedHeader.push(footerProvider() as V)
93
+ }
94
+
95
+
96
+ // when there is already a row-header, then replace it with the new one
97
+ if (this.dataFrame.columnTagsFor(0).some(tag => tag.value === TableTagType.ROW_HEADER)) {
98
+ return this.dataFrame
99
+ .mapColumn(0, (_, rowIndex) => header[rowIndex])
100
+ .map(df => TableData.fromDataFrame<V>(df))
101
+ }
102
+
103
+ return this.dataFrame
104
+ .insertColumnBefore(0, updatedHeader)
105
+ .flatMap(df => df.tagColumn(0, "row-header", TableTagType.ROW_HEADER))
106
+ .flatMap(df => df.tagColumn<Formatting<V>>(0, TableFormatterType.COLUMN, formatting))
107
+ .map(df => TableData.fromDataFrame<V>(df))
108
+ }
109
+
110
+ public withFooter(
111
+ footer: Array<V>,
112
+ formatting: Formatting<V> = defaultFormatting<V>(),
113
+ rowHeaderProvider: () => V | undefined = () => undefined
114
+ ): Result<TableData<V>, string> {
115
+ // just return a copy of the data table if the footer is empty
116
+ if (footer.length === 0) {
117
+ return successResult(TableData.fromDataFrame<V>(this.dataFrame.copy()))
118
+ }
119
+
120
+ // when a row header has already been applied, then the table has grown by one column,
121
+ // and so we need to insert an empty cell at the beginning of the footer
122
+ const updatedFooter = footer.slice()
123
+ if (this.dataFrame.columnTagsFor(0).some(tag => tag.value === TableTagType.ROW_HEADER)) {
124
+ updatedFooter.unshift(rowHeaderProvider() as V)
125
+ }
126
+
127
+ // when there is already a footer, then replace it with the new one
128
+ if (this.dataFrame.rowTagsFor(0).some(tag => tag.value === TableTagType.FOOTER)) {
129
+ return this.dataFrame
130
+ .mapRow(this.dataFrame.rowCount() -1 , (_, rowIndex) => footer[rowIndex])
131
+ .map(df => TableData.fromDataFrame<V>(df))
132
+ }
133
+
134
+ return this.dataFrame
135
+ .pushRow(updatedFooter)
136
+ .flatMap(df => df.tagRow(df.rowCount()-1, "footer", TableTagType.FOOTER))
137
+ .flatMap(df => df.tagRow<Formatting<V>>(df.rowCount()-1, TableFormatterType.ROW, formatting))
138
+ .map(df => TableData.fromDataFrame<V>(df))
139
+ }
140
+
141
+ static hasColumnHeader<V>(dataFrame: DataFrame<V>): boolean {
142
+ return dataFrame.rowTagsFor(0).some(tag => tag.value === TableTagType.COLUMN_HEADER)
143
+ }
144
+
145
+ static hasRowHeader<V>(dataFrame: DataFrame<V>): boolean {
146
+ return dataFrame.columnTagsFor(0).some(tag => tag.value === TableTagType.ROW_HEADER)
147
+ }
148
+
149
+ static hasFooter<V>(dataFrame: DataFrame<V>): boolean {
150
+ return dataFrame.rowTagsFor(dataFrame.rowCount() - 1).some(tag => tag.value === TableTagType.FOOTER)
151
+ }
152
+
153
+ static dataRowCount<V>(dataFrame: DataFrame<V>): number {
154
+ return dataFrame.rowCount() - (TableData.hasColumnHeader(dataFrame) ? 1 : 0) - (TableData.hasFooter(dataFrame) ? 1 : 0)
155
+ }
156
+
157
+ static dataColumnCount<V>(dataFrame: DataFrame<V>): number {
158
+ return dataFrame.columnCount() - (TableData.hasRowHeader(dataFrame) ? 1 : 0)
159
+ }
160
+
161
+ static tableRowCount<V>(dataFrame: DataFrame<V>): number {
162
+ return dataFrame.rowCount()
163
+ }
164
+
165
+ static tableColumnCount<V>(dataFrame: DataFrame<V>): number {
166
+ return dataFrame.columnCount()
167
+ }
168
+
169
+ hasColumnHeader(): boolean {
170
+ // return this.dataFrame.rowTagsFor(0).some(tag => tag.value === TableTagType.COLUMN_HEADER)
171
+ return TableData.hasColumnHeader(this.dataFrame)
172
+ }
173
+
174
+ hasRowHeader(): boolean {
175
+ // return this.dataFrame.columnTagsFor(0).some(tag => tag.value === TableTagType.ROW_HEADER)
176
+ return TableData.hasRowHeader(this.dataFrame)
177
+ }
178
+
179
+ hasFooter(): boolean {
180
+ // return this.dataFrame.rowTagsFor(this.dataFrame.rowCount() - 1).some(tag => tag.value === TableTagType.FOOTER)
181
+ return TableData.hasFooter(this.dataFrame)
182
+ }
183
+
184
+ dataRowCount(): number {
185
+ // return this.dataFrame.rowCount() - (this.hasColumnHeader() ? 1 : 0) - (this.hasFooter() ? 1 : 0)
186
+ return TableData.dataRowCount(this.dataFrame)
187
+ }
188
+
189
+ dataColumnCount(): number {
190
+ // return this.dataFrame.columnCount() - (this.hasRowHeader() ? 1 : 0)
191
+ return TableData.dataColumnCount(this.dataFrame)
192
+ }
193
+
194
+ tableRowCount(): number {
195
+ return this.dataFrame.rowCount()
196
+ }
197
+
198
+ tableColumnCount(): number {
199
+ return this.dataFrame.columnCount()
200
+ }
201
+
202
+ /**
203
+ * @param [includeRowHeader=false] When `true` includes an extra empty column element when the table
204
+ * has a row-header. When `false` returns only the column-header elements
205
+ * @return A {@link Result} holding the column header if it exists; or a failure if no column-header exists
206
+ * @example
207
+ * ```typescript
208
+ * const columnHeader = ['A', 'B', 'C', 'D', 'E']
209
+ * const rowHeader = ['one', 'two', 'three', 'four']
210
+ * const footer = ['a10', 'b10', 'c10', 'd10', 'e10']
211
+ * const data = DataFrame.from([
212
+ * ['a1', 'b1', 'c1', 'd1', 'e1'],
213
+ * ['a2', 'b2', 'c2', 'd2', 'e2'],
214
+ * ['a3', 'b3', 'c3', 'd3', 'e3'],
215
+ * ['a4', 'b4', 'c4', 'd4', 'e4'],
216
+ * ]).getOrThrow()
217
+ *
218
+ * // create a data-table with a column-header, row-header, footer, and data
219
+ * const tableData = createTableData<string>(data)
220
+ * .withColumnHeader(columnHeader)
221
+ * .flatMap(table => table.withRowHeader(rowHeader))
222
+ * .flatMap(table => table.withFooter(footer))
223
+ * .getOrThrow()
224
+ *
225
+ * // the column-header retrieved from the table-data should equal the column-header originally set
226
+ * expect(tableData.columnHeader().getOrThrow().equals(columnHeader)).toBeTruthy()
227
+ * ``` */
228
+ columnHeader(includeRowHeader: boolean = false): Result<Array<V>, string> {
229
+ if (this.hasColumnHeader()) {
230
+ const startColumn = this.hasRowHeader() && !includeRowHeader ? 1 : 0
231
+ return this.dataFrame
232
+ .rowSlice(0)
233
+ .map(row => row.slice(startColumn))
234
+ .mapFailure(err => "(TableData::columnHeader) Failed to retrieve column-header.\n" + err)
235
+ }
236
+ return failureResult("(TableData::columnHeader) Failed to retrieve the column-header because no column header exists.")
237
+ }
238
+
239
+ /**
240
+ * @param [includeColumnHeader=false] When `true` returns an extra empty row element if the table data
241
+ * has a column header. When `false` does not include the empty extra row element.
242
+ * @param [includeFooter=false] When `true` returns an extra empty row element if the table data
243
+ * has a footer. When `false` does not include the empty extra row element.
244
+ * @return A {@link Result} holding the row-header if it exists; or a failure if no row-header exists
245
+ * @example
246
+ * ```typescript
247
+ * const columnHeader = ['A', 'B', 'C', 'D', 'E']
248
+ * const rowHeader = ['one', 'two', 'three', 'four']
249
+ * const footer = ['a10', 'b10', 'c10', 'd10', 'e10']
250
+ * const data = DataFrame.from([
251
+ * ['a1', 'b1', 'c1', 'd1', 'e1'],
252
+ * ['a2', 'b2', 'c2', 'd2', 'e2'],
253
+ * ['a3', 'b3', 'c3', 'd3', 'e3'],
254
+ * ['a4', 'b4', 'c4', 'd4', 'e4'],
255
+ * ]).getOrThrow()
256
+ *
257
+ * // create a data-table with a column-header, row-header, footer, and data
258
+ * const tableData = createTableData<string>(data)
259
+ * .withColumnHeader(columnHeader)
260
+ * .flatMap(table => table.withRowHeader(rowHeader))
261
+ * .flatMap(table => table.withFooter(footer))
262
+ * .getOrThrow()
263
+ *
264
+ * // the row-header retrieved from the table-data should equal the row-header originally set
265
+ * expect(tableData.rowHeader().getOrThrow().equals(rowHeader)).toBeTruthy()
266
+ * ``` */
267
+ rowHeader(includeColumnHeader: boolean = false, includeFooter: boolean = false): Result<Array<V>, string> {
268
+ if (this.hasRowHeader()) {
269
+ const startRow = this.hasColumnHeader() && !includeColumnHeader? 1 : 0
270
+ const endRow = this.hasFooter() && !includeFooter ? this.dataFrame.rowCount() - 1 : this.dataFrame.rowCount()
271
+ return this.dataFrame
272
+ .columnSlice(0)
273
+ .map(row => row.slice(startRow, endRow))
274
+ .mapFailure(err => "(TableData::rowHeader) Failed to retrieve row-header.\n" + err)
275
+ }
276
+ return failureResult("(TableData::rowHeader) Failed to retrieve the row-header because no row header exists.")
277
+ }
278
+
279
+ /**
280
+ * @param [includeRowHeader=false] When `true` includes an extra empty column element when the table
281
+ * has a row-header. When `false` returns only the column-header elements
282
+ * @return A {@link Result} holding the footer, if exists; or a failure if no footer exists
283
+ * @example
284
+ * ```typescript
285
+ * const columnHeader = ['A', 'B', 'C', 'D', 'E']
286
+ * const rowHeader = ['one', 'two', 'three', 'four']
287
+ * const footer = ['a10', 'b10', 'c10', 'd10', 'e10']
288
+ * const data = DataFrame.from([
289
+ * ['a1', 'b1', 'c1', 'd1', 'e1'],
290
+ * ['a2', 'b2', 'c2', 'd2', 'e2'],
291
+ * ['a3', 'b3', 'c3', 'd3', 'e3'],
292
+ * ['a4', 'b4', 'c4', 'd4', 'e4'],
293
+ * ]).getOrThrow()
294
+ *
295
+ * // create a data-table with a column-header, row-header, footer, and data
296
+ * const tableData = createTableData<string>(data)
297
+ * .withColumnHeader(columnHeader)
298
+ * .flatMap(table => table.withRowHeader(rowHeader))
299
+ * .flatMap(table => table.withFooter(footer))
300
+ * .getOrThrow()
301
+ *
302
+ * // the footer retrieved from the table-data should equal the footer originally set
303
+ * expect(tableData.footer().getOrThrow().equals(footer)).toBeTruthy()
304
+ * ```
305
+ */
306
+ footer(includeRowHeader: boolean = false): Result<Array<V>, string> {
307
+ if (this.hasFooter()) {
308
+ const startColumn = this.hasRowHeader() && !includeRowHeader ? 1 : 0
309
+ return this.dataFrame
310
+ .rowSlice(this.dataFrame.rowCount() - 1)
311
+ .map(row => row.slice(startColumn))
312
+ .mapFailure(err => "(TableData::footer) Failed to retrieve footer.\n" + err)
313
+ }
314
+ return failureResult("(TableData::footer) Failed to retrieve the footer because no footer exists.")
315
+ }
316
+
317
+ /**
318
+ * Retrieves the "data" from the data-table. This excludes any column headers, row-headers, and footers.
319
+ * @return A {@link Result} holding a {@link DataFrame} with the "data" from the data-table. This excludes any
320
+ * column headers, row-headers, and footers.
321
+ * @example
322
+ * ```typescript
323
+ * const columnHeader = ['A', 'B', 'C', 'D', 'E']
324
+ * const rowHeader = ['one', 'two', 'three', 'four']
325
+ * const footer = ['a10', 'b10', 'c10', 'd10', 'e10']
326
+ * const data = DataFrame.from([
327
+ * ['a1', 'b1', 'c1', 'd1', 'e1'],
328
+ * ['a2', 'b2', 'c2', 'd2', 'e2'],
329
+ * ['a3', 'b3', 'c3', 'd3', 'e3'],
330
+ * ['a4', 'b4', 'c4', 'd4', 'e4'],
331
+ * ]).getOrThrow()
332
+ *
333
+ * // create a data-table with a column-header, row-header, footer, and data
334
+ * const tableData = createTableData<string>(data)
335
+ * .withColumnHeader(columnHeader)
336
+ * .flatMap(table => table.withRowHeader(rowHeader))
337
+ * .flatMap(table => table.withFooter(footer))
338
+ * .getOrThrow()
339
+ *
340
+ * // the data retrieved from the table-data should equal the data originally set
341
+ * expect(tableData.data().getOrThrow().equals(data)).toBeTruthy()
342
+ * ```
343
+ */
344
+ data(): Result<DataFrame<V>, string> {
345
+ const startRow = this.hasColumnHeader() ? 1 : 0
346
+ const endRow = this.hasFooter() ? this.dataFrame.rowCount() - 2 : this.dataFrame.rowCount() - 1
347
+ const startColumn = this.hasRowHeader() ? 1 : 0
348
+ return this.dataFrame
349
+ .subFrame(indexFrom(startRow, startColumn), indexFrom(endRow, this.dataFrame.columnCount() - 1))
350
+ .mapFailure(err => "(TableData::data) Failed to retrieve data.\n" + err)
351
+ }
352
+
353
+ /**
354
+ * @return a copy of the {@link DataFrame} that has been prepared by this class.
355
+ */
356
+ unwrapDataFrame(): DataFrame<V> {
357
+ return this.dataFrame.copy()
358
+ }
359
+ }