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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/d3types.ts +17 -0
- package/dist/d3types.d.ts +12 -0
- package/dist/d3types.d.ts.map +1 -0
- package/dist/d3types.js +3 -0
- package/dist/d3types.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/stylings.d.ts +206 -0
- package/dist/stylings.d.ts.map +1 -0
- package/dist/stylings.js +123 -0
- package/dist/stylings.js.map +1 -0
- package/dist/tableData.d.ts +168 -0
- package/dist/tableData.d.ts.map +1 -0
- package/dist/tableData.js +329 -0
- package/dist/tableData.js.map +1 -0
- package/dist/tableData.test.d.ts +2 -0
- package/dist/tableData.test.d.ts.map +1 -0
- package/dist/tableData.test.js +259 -0
- package/dist/tableData.test.js.map +1 -0
- package/dist/tableFormatter.d.ts +179 -0
- package/dist/tableFormatter.d.ts.map +1 -0
- package/dist/tableFormatter.js +298 -0
- package/dist/tableFormatter.js.map +1 -0
- package/dist/tableFormatter.test.d.ts +2 -0
- package/dist/tableFormatter.test.d.ts.map +1 -0
- package/dist/tableFormatter.test.js +101 -0
- package/dist/tableFormatter.test.js.map +1 -0
- package/dist/tableStyler.d.ts +310 -0
- package/dist/tableStyler.d.ts.map +1 -0
- package/dist/tableStyler.js +665 -0
- package/dist/tableStyler.js.map +1 -0
- package/dist/tableStyler.test.d.ts +2 -0
- package/dist/tableStyler.test.d.ts.map +1 -0
- package/dist/tableStyler.test.js +225 -0
- package/dist/tableStyler.test.js.map +1 -0
- package/dist/tableSvg.d.ts +41 -0
- package/dist/tableSvg.d.ts.map +1 -0
- package/dist/tableSvg.js +634 -0
- package/dist/tableSvg.js.map +1 -0
- package/dist/tableUtils.d.ts +14 -0
- package/dist/tableUtils.d.ts.map +1 -0
- package/dist/tableUtils.js +18 -0
- package/dist/tableUtils.js.map +1 -0
- package/eslint.config.js +23 -0
- package/index.ts +82 -0
- package/jest.config.js +5 -0
- package/package.json +44 -0
- package/stylings.ts +311 -0
- package/svg-table-0.0.1-snapshot.tgz +0 -0
- package/tableData.test.ts +290 -0
- package/tableData.ts +359 -0
- package/tableFormatter.test.ts +122 -0
- package/tableFormatter.ts +306 -0
- package/tableStyler.test.ts +268 -0
- package/tableStyler.ts +798 -0
- package/tableSvg.ts +820 -0
- package/tableUtils.ts +20 -0
- 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
|
+
}
|