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
package/tableSvg.ts ADDED
@@ -0,0 +1,820 @@
1
+ import {select, type Selection} from 'd3';
2
+ import {type BorderSelection, type GroupSelection, type RectSelection, type TextSelection} from "./d3types";
3
+ import {textHeightOf, textWidthOf} from "./tableUtils";
4
+ import {TableData} from "./tableData";
5
+ import {type Result} from "result-fn";
6
+ import {StyledTable} from "./tableStyler";
7
+ import {
8
+ BorderLocation,
9
+ type CellStyle,
10
+ defaultCellStyle,
11
+ defaultColumnStyle,
12
+ defaultFooterStyle,
13
+ defaultRowHeaderStyle,
14
+ defaultRowStyle,
15
+ type TextAlignment,
16
+ type VerticalTextAlignment
17
+ } from './stylings';
18
+ import {DataFrame} from "data-frame-ts";
19
+ import {defaultFormatting} from "./tableFormatter";
20
+
21
+ export type ElementPlacementInfo = {
22
+ cellSelection: RectSelection
23
+ textSelection: TextSelection
24
+ borderSelection: BorderSelection
25
+ textWidth: number
26
+ textHeight: number
27
+ cellStyle: CellStyle
28
+ }
29
+
30
+ export type TextAnchor = "start" | "middle" | "end"
31
+
32
+ function textAnchorFrom(align: TextAlignment): TextAnchor {
33
+ switch (align) {
34
+ case "left":
35
+ return "start"
36
+ case "center":
37
+ return "middle"
38
+ case "right":
39
+ return "end"
40
+ }
41
+ }
42
+
43
+ export type DominantBaseline =
44
+ "auto"
45
+ | "text-top"
46
+ | "central"
47
+ | "middle"
48
+ | "alphabetic"
49
+ | "ideographic"
50
+ | "hanging"
51
+ | "mathematical"
52
+
53
+ function dominantBaselineFrom(align: VerticalTextAlignment): DominantBaseline {
54
+ switch (align) {
55
+ case "top":
56
+ return "hanging"
57
+ case "middle":
58
+ return "central"
59
+ case "bottom":
60
+ return "auto"
61
+ }
62
+ }
63
+
64
+ export type CellRenderingDimensions = {
65
+ // the dimensions of the cell to be used when rendering
66
+ width: number
67
+ height: number
68
+ // the coordinates relative to the table group of the text
69
+ x: number
70
+ y: number
71
+ cellX: number
72
+ cellY: number
73
+ }
74
+
75
+ export type TableRenderingInfo = {
76
+ tableWidth: number
77
+ tableHeight: number
78
+ tableX: number
79
+ tableY: number
80
+ renderingInfo: TableData<ElementPlacementInfo & CellRenderingDimensions>
81
+ }
82
+
83
+ export function elementInfoFrom(
84
+ textSelection: TextSelection,
85
+ cellSelection: RectSelection,
86
+ borderSelection: BorderSelection,
87
+ cellStyle: CellStyle
88
+ ): ElementPlacementInfo {
89
+ return {
90
+ cellSelection,
91
+ textSelection,
92
+ borderSelection,
93
+ textWidth: textWidthOf(textSelection),
94
+ textHeight: textHeightOf(textSelection),
95
+ cellStyle
96
+ }
97
+ }
98
+
99
+ // export class SvgTable<V> {
100
+ // private constructor(
101
+ // private readonly uniqueTableId: string,
102
+ // private readonly tableSelection: GSelection,
103
+ // private readonly renderingInfo: TableRenderingInfo,
104
+ // private readonly styledTable: StyledTable<V>,
105
+ // private readonly container: SVGSVGElement,
106
+ // private readonly coordinates: [x: number, y: number] | ((width: number, height: number) => [x: number, y: number])
107
+ // ) {
108
+ // }
109
+ //
110
+ // static createTable<V>(
111
+ // styledTable: StyledTable<V>,
112
+ // container: SVGSVGElement,
113
+ // uniqueTableId: string,
114
+ // coordinates: [x: number, y: number] | ((width: number, height: number) => [x: number, y: number])
115
+ // ): Result<SvgTable<V>, string> {
116
+ // // return new SvgTable(styledTable, container, uniqueTableId, coordinates)
117
+ // // grab a copy of the data as a data-frame
118
+ // const tableData = styledTable.tableData()
119
+ //
120
+ // // add the group <g> representing the table, to which all the elements will be added, and
121
+ // // the group will be translated to the mouse (x, y) coordinates as appropriate
122
+ // const tableSelection = select<SVGSVGElement | null, any>(container)
123
+ // .append('g')
124
+ // .attr('id', tableId(uniqueTableId))
125
+ // .attr('class', 'tooltip')
126
+ // .style('fill', styledTable.tableBackground().color)
127
+ // .style('font-family', styledTable.tableFont().family)
128
+ // .style('font-size', styledTable.tableFont().size)
129
+ // .style('font-weight', styledTable.tableFont().weight)
130
+ //
131
+ // // creates an SVG group to hold the column header (if there is one), and
132
+ // // then add a cell for each column header element
133
+ // const {data: columnHeaders} = createColumnHeaderPlacementInfo(tableData, tableSelection, uniqueTableId, styledTable)
134
+ // const {columnHeader, data: rowHeaders, footer} = createRowHeaderPlacementInfo(tableData, tableSelection, uniqueTableId, styledTable)
135
+ // const {rowHeader, data: footers} = createFooterPlacementInfo(tableData, tableSelection, uniqueTableId, styledTable)
136
+ // const data = createDataPlacementInfo(tableData, tableSelection, uniqueTableId, styledTable)
137
+ //
138
+ // const {top, left} = styledTable.tablePadding()
139
+ //
140
+ // return TableData
141
+ // .fromDataFrame<ElementPlacementInfo>(data)
142
+ // // the column header doesn't need to deal with the value providers, because it is
143
+ // // the first one added and so there are no row-headers or footers yet.
144
+ // .withColumnHeader(columnHeaders)
145
+ // .flatMap(td => td.withRowHeader(
146
+ // rowHeaders,
147
+ // defaultFormatting<ElementPlacementInfo>(),
148
+ // () => columnHeader,
149
+ // () => footer
150
+ // ))
151
+ // .flatMap(td => td.withFooter(
152
+ // footers,
153
+ // defaultFormatting<ElementPlacementInfo>(),
154
+ // () => rowHeader
155
+ // ))
156
+ // .map(td => {
157
+ // const info = calculateRenderingInfo(td, styledTable, coordinates)
158
+ // tableSelection.attr('transform', `translate(${info.tableX + left}, ${info.tableY + top})`)
159
+ // return info
160
+ // })
161
+ // .map(renderingInfo => placeTextInTable(renderingInfo))
162
+ // .map(renderingInfo => new SvgTable(uniqueTableId, tableSelection, renderingInfo, styledTable, container, coordinates))
163
+ // }
164
+ //
165
+ // private static updateTable<V>(
166
+ // styledTable: StyledTable<V>,
167
+ // container: SVGSVGElement,
168
+ // uniqueTableId: string,
169
+ // coordinates: [x: number, y: number] | ((width: number, height: number) => [x: number, y: number])
170
+ // ): Result<SvgTable<V>, string> {
171
+ // const {top, left} = styledTable.tablePadding()
172
+ //
173
+ // return TableData
174
+ // .fromDataFrame<ElementPlacementInfo>(data)
175
+ // // the column header doesn't need to deal with the value providers, because it is
176
+ // // the first one added and so there are no row-headers or footers yet.
177
+ // .withColumnHeader(columnHeaders)
178
+ // .flatMap(td => td.withRowHeader(
179
+ // rowHeaders,
180
+ // defaultFormatting<ElementPlacementInfo>(),
181
+ // () => columnHeader,
182
+ // () => footer
183
+ // ))
184
+ // .flatMap(td => td.withFooter(
185
+ // footers,
186
+ // defaultFormatting<ElementPlacementInfo>(),
187
+ // () => rowHeader
188
+ // ))
189
+ // .map(td => {
190
+ // const info = calculateRenderingInfo(td, styledTable, coordinates)
191
+ // tableSelection.attr('transform', `translate(${info.tableX + left}, ${info.tableY + top})`)
192
+ // return info
193
+ // })
194
+ // .map(renderingInfo => placeTextInTable(renderingInfo))
195
+ // .map(renderingInfo => new SvgTable(uniqueTableId, tableSelection, renderingInfo, styledTable, container, coordinates))
196
+ // }
197
+ // }
198
+
199
+ /**
200
+ * Creates the table
201
+ * @param styledTable
202
+ * @param container
203
+ * @param uniqueTableId
204
+ * @param coordinates
205
+ */
206
+ export function createTable<V>(
207
+ styledTable: StyledTable<V>,
208
+ container: SVGSVGElement,
209
+ uniqueTableId: string,
210
+ coordinates: [x: number, y: number] | ((width: number, height: number) => [x: number, y: number])
211
+ ): Result<TableRenderingInfo, string> {
212
+
213
+ // grab a copy of the data as a data-frame
214
+ const tableData = styledTable.tableData()
215
+
216
+ // add the group <g> representing the table, to which all the elements will be added, and
217
+ // the group will be translated to the mouse (x, y) coordinates as appropriate
218
+ const tableSelection = select<SVGSVGElement | null, any>(container)
219
+ .append('g')
220
+ .attr('id', tableId(uniqueTableId))
221
+ .attr('class', 'tooltip')
222
+ .style('fill', styledTable.tableBackground().color)
223
+ .style('font-family', styledTable.tableFont().family)
224
+ .style('font-size', styledTable.tableFont().size)
225
+ .style('font-weight', styledTable.tableFont().weight)
226
+
227
+ // creates an SVG group to hold the column header (if there is one), and
228
+ // then add a cell for each column header element
229
+ const {data: columnHeaders} = createColumnHeaderPlacementInfo(tableData, tableSelection, uniqueTableId, styledTable)
230
+ const {
231
+ columnHeader,
232
+ data: rowHeaders,
233
+ footer
234
+ } = createRowHeaderPlacementInfo(tableData, tableSelection, uniqueTableId, styledTable)
235
+ const {rowHeader, data: footers} = createFooterPlacementInfo(tableData, tableSelection, uniqueTableId, styledTable)
236
+ const data = createDataPlacementInfo(tableData, tableSelection, uniqueTableId, styledTable)
237
+
238
+ const {top, left} = styledTable.tablePadding()
239
+
240
+ return TableData
241
+ .fromDataFrame<ElementPlacementInfo>(data)
242
+ // the column header doesn't need to deal with the value providers, because it is
243
+ // the first one added and so there are no row-headers or footers yet.
244
+ .withColumnHeader(columnHeaders)
245
+ .flatMap(td => td.withRowHeader(
246
+ rowHeaders,
247
+ defaultFormatting<ElementPlacementInfo>(),
248
+ () => columnHeader,
249
+ () => footer
250
+ ))
251
+ .flatMap(td => td.withFooter(
252
+ footers,
253
+ defaultFormatting<ElementPlacementInfo>(),
254
+ () => rowHeader
255
+ ))
256
+ .map(td => {
257
+ const info = calculateRenderingInfo(td, styledTable, coordinates)
258
+ tableSelection.attr('transform', `translate(${info.tableX + left}, ${info.tableY + top})`)
259
+ return info
260
+ })
261
+ .map(renderingInfo => placeTextInTable(renderingInfo))
262
+ }
263
+
264
+ /*
265
+ HELPER STUFF
266
+ */
267
+
268
+ /*
269
+ Add all the SVG elements representing the table and then update the
270
+ coordinates for each of those elements to make a table
271
+ */
272
+ enum ELEMENT_TYPE_ID {
273
+ ROW_HEADER = "row-header",
274
+ COLUMN_HEADER = "column-header",
275
+ DATA = "data",
276
+ FOOTER = "footer",
277
+ CELL_TEXT = "cell-data",
278
+ CELL = "cell"
279
+ }
280
+
281
+ export function tableId(uniqueTableId: string): string {
282
+ return `svg-table-group-${uniqueTableId}`
283
+ }
284
+
285
+ function columnHeaderGroupId(uniqueTableId: string): string {
286
+ return `svg-table-${ELEMENT_TYPE_ID.COLUMN_HEADER}-group-${uniqueTableId}`
287
+ }
288
+
289
+ function rowHeaderGroupId(uniqueTableId: string): string {
290
+ return `svg-table-${ELEMENT_TYPE_ID.ROW_HEADER}-group-${uniqueTableId}`
291
+ }
292
+
293
+ function footerGroupId(uniqueTableId: string): string {
294
+ return `svg-table-${ELEMENT_TYPE_ID.FOOTER}-group-${uniqueTableId}`
295
+ }
296
+
297
+ function dataGroupId(uniqueTableId: string): string {
298
+ return `svg-table-${ELEMENT_TYPE_ID.DATA}-group-${uniqueTableId}`
299
+ }
300
+
301
+ function cellTextId(uniqueTableId: string, rowIndex: number, columnIndex: number): string {
302
+ return `svg-table-${ELEMENT_TYPE_ID.CELL_TEXT}-${rowIndex}-${columnIndex}-${uniqueTableId}`
303
+ }
304
+
305
+ function cellId(uniqueTableId: string, rowIndex: number, columnIndex: number): string {
306
+ return `svg-table-${ELEMENT_TYPE_ID.CELL}-${rowIndex}-${columnIndex}-${uniqueTableId}`
307
+ }
308
+
309
+ function borderId(uniqueTableId: string, location: BorderLocation, rowIndex: number, columnIndex: number): string {
310
+ return `svg-table-${ELEMENT_TYPE_ID.CELL}-${rowIndex}-${columnIndex}-border-${location}-${uniqueTableId}`
311
+ }
312
+
313
+ function createCellSelection(
314
+ uniqueTableId: string,
315
+ rowIndex: number,
316
+ columnIndex: number,
317
+ groupSelection: GroupSelection,
318
+ style: CellStyle
319
+ ): RectSelection {
320
+ return groupSelection
321
+ .append<SVGRectElement>("rect")
322
+ .attr('id', cellId(uniqueTableId, rowIndex, columnIndex))
323
+ .style('fill', style.background.color)
324
+ .style('fill-opacity', style.background.opacity)
325
+ }
326
+
327
+ function createBorderSelection(
328
+ uniqueTableId: string,
329
+ rowIndex: number,
330
+ columnIndex: number,
331
+ groupSelection: GroupSelection,
332
+ style: CellStyle
333
+ ): BorderSelection {
334
+ let borderSelection: BorderSelection = {}
335
+ if (style.border.top) {
336
+ borderSelection.top = groupSelection
337
+ .append<SVGLineElement>("line")
338
+ .attr('id', borderId(uniqueTableId, BorderLocation.TOP, rowIndex, columnIndex))
339
+ .style('stroke', style.border.top.color)
340
+ .style('stroke-width', style.border.top.width)
341
+ }
342
+ if (style.border.bottom) {
343
+ borderSelection.bottom = groupSelection
344
+ .append<SVGLineElement>("line")
345
+ .attr('id', borderId(uniqueTableId, BorderLocation.BOTTOM, rowIndex, columnIndex))
346
+ .style('stroke', style.border.bottom.color)
347
+ .style('stroke-width', style.border.bottom.width)
348
+ }
349
+ if (style.border.left) {
350
+ borderSelection.left = groupSelection
351
+ .append<SVGLineElement>("line")
352
+ .attr('id', borderId(uniqueTableId, BorderLocation.LEFT, rowIndex, columnIndex))
353
+ .style('stroke', style.border.left.color)
354
+ .style('stroke-width', style.border.left.width)
355
+ }
356
+ if (style.border.right) {
357
+ borderSelection.right = groupSelection
358
+ .append<SVGLineElement>("line")
359
+ .attr('id', borderId(uniqueTableId, BorderLocation.RIGHT, rowIndex, columnIndex))
360
+ .style('stroke', style.border.right.color)
361
+ .style('stroke-width', style.border.right.width)
362
+ }
363
+ return borderSelection
364
+ }
365
+
366
+ function createTextSelection<V>(
367
+ uniqueTableId: string,
368
+ rowIndex: number,
369
+ columnIndex: number,
370
+ groupSelection: GroupSelection,
371
+ style: CellStyle,
372
+ element: V
373
+ ): TextSelection {
374
+ return groupSelection
375
+ .append<SVGTextElement>("text")
376
+ .attr('id', cellTextId(uniqueTableId, rowIndex, columnIndex))
377
+ .style('font-family', style.font.family)
378
+ .style('font-size', style.font.size)
379
+ .style('font-weight', style.font.weight)
380
+ .style('fill', style.font.color)
381
+ .text(() => `${element}`)
382
+ }
383
+
384
+ function createColumnHeaderPlacementInfo<V>(
385
+ tableData: TableData<V>,
386
+ tableSelection: Selection<SVGGElement, any, null, undefined>,
387
+ uniqueTableId: string,
388
+ styledTable: StyledTable<V>
389
+ ): { rowHeader?: ElementPlacementInfo, data: Array<ElementPlacementInfo> } {
390
+ return tableData.columnHeader(true)
391
+ .map(columnHeader => {
392
+ const groupSelection = tableSelection
393
+ .append('g')
394
+ .attr('id', columnHeaderGroupId(uniqueTableId))
395
+ .attr('class', 'tooltip-table-header')
396
+
397
+ return columnHeader.map((header, columnIndex) => {
398
+ // the style with the highest priority for the cell
399
+ const style = styledTable
400
+ .stylesForTableCoordinates(0, columnIndex)
401
+ .getOrElse({...defaultCellStyle})
402
+
403
+ const cellSelection = createCellSelection(uniqueTableId, 0, columnIndex, groupSelection, style)
404
+ const borderSelection = createBorderSelection(uniqueTableId, 0, columnIndex, groupSelection, style)
405
+ const textSelection = createTextSelection(uniqueTableId, 0, columnIndex, groupSelection, style, header)
406
+ return elementInfoFrom(textSelection, cellSelection, borderSelection, {...style})
407
+ })
408
+ })
409
+ .map(columnHeader => {
410
+ const rowHeaderElem = tableData.hasRowHeader() ? columnHeader.shift() : undefined
411
+ return {rowHeader: rowHeaderElem, data: columnHeader}
412
+ })
413
+ .getOrElse({rowHeader: undefined, data: [] as ElementPlacementInfo[]})
414
+ }
415
+
416
+ /**
417
+ *
418
+ * @param tableData
419
+ * @param tableSelection
420
+ * @param uniqueTableId
421
+ * @param styledTable
422
+ */
423
+ function createRowHeaderPlacementInfo<V>(
424
+ tableData: TableData<V>,
425
+ tableSelection: Selection<SVGGElement, any, null, undefined>,
426
+ uniqueTableId: string,
427
+ styledTable: StyledTable<V>
428
+ ): { columnHeader?: ElementPlacementInfo, data: Array<ElementPlacementInfo>, footer?: ElementPlacementInfo } {
429
+ return tableData
430
+ .rowHeader(true, true)
431
+ .map(rowHeader => {
432
+ const groupSelection = tableSelection
433
+ .append('g')
434
+ .attr('id', rowHeaderGroupId(uniqueTableId))
435
+ .attr('class', 'tooltip-table-row-header')
436
+ .style('fill', styledTable.rowHeaderStyle()
437
+ .map(styling => styling.style.background.color)
438
+ .getOrElse(defaultRowHeaderStyle.background.color)
439
+ )
440
+ return rowHeader.map((header, rowIndex) => {
441
+ // the style with the highest priority for the cell
442
+ const style = styledTable
443
+ .stylesForTableCoordinates(rowIndex, 0)
444
+ .getOrElse({...defaultCellStyle})
445
+
446
+ const cellSelection = createCellSelection(uniqueTableId, rowIndex, 0, groupSelection, style)
447
+ const borderSelection = createBorderSelection(uniqueTableId, rowIndex, 0, groupSelection, style)
448
+ const textSelection = createTextSelection(uniqueTableId, rowIndex, 0, groupSelection, style, header)
449
+ return elementInfoFrom(textSelection, cellSelection, borderSelection, {...style})
450
+ })
451
+ })
452
+ .map(rowHeader => {
453
+ const columnHeaderElem = tableData.hasRowHeader() ? rowHeader.shift() : undefined
454
+ const footerElem = tableData.hasFooter() ? rowHeader.pop() : undefined
455
+ return {columnHeader: columnHeaderElem, data: rowHeader, footer: footerElem}
456
+ })
457
+ .getOrElse({columnHeader: undefined, data: [] as ElementPlacementInfo[], footer: undefined})
458
+ }
459
+
460
+ /**
461
+ *
462
+ * @param tableData
463
+ * @param tableSelection
464
+ * @param uniqueTableId
465
+ * @param styledTable
466
+ */
467
+ function createFooterPlacementInfo<V>(
468
+ tableData: TableData<V>,
469
+ tableSelection: Selection<SVGGElement, any, null, undefined>,
470
+ uniqueTableId: string,
471
+ styledTable: StyledTable<V>
472
+ ): { rowHeader?: ElementPlacementInfo, data: Array<ElementPlacementInfo> } {
473
+ return tableData
474
+ .footer(true)
475
+ .map(footer => {
476
+ const groupSelection = tableSelection
477
+ .append('g')
478
+ .attr('id', footerGroupId(uniqueTableId))
479
+ .attr('class', 'tooltip-table-footer')
480
+ .style('fill', styledTable.columnHeaderStyle()
481
+ .map(styling => styling.style.background.color)
482
+ .getOrElse(defaultFooterStyle.background.color)
483
+ )
484
+ return footer.map((ftr, columnIndex) => {
485
+ // the style with the highest priority for the cell
486
+ const style = styledTable
487
+ .stylesForTableCoordinates(footer.length - 1, columnIndex)
488
+ .getOrElse({...defaultCellStyle})
489
+
490
+ const cellSelection = createCellSelection(uniqueTableId, footer.length - 1, columnIndex, groupSelection, style)
491
+ const borderSelection = createBorderSelection(uniqueTableId, footer.length - 1, columnIndex, groupSelection, style)
492
+ const textSelection = createTextSelection(uniqueTableId, footer.length - 1, columnIndex, groupSelection, style, ftr)
493
+ return elementInfoFrom(textSelection, cellSelection, borderSelection, {...style})
494
+ })
495
+ })
496
+ .map(footer => {
497
+ const rowHeaderElem = tableData.hasRowHeader() ? footer.shift() : undefined
498
+ return {rowHeader: rowHeaderElem, data: footer}
499
+ })
500
+ .getOrElse({rowHeader: undefined, data: [] as ElementPlacementInfo[]})
501
+ }
502
+
503
+ /**
504
+ *
505
+ * @param tableData
506
+ * @param tableSelection
507
+ * @param uniqueTableId
508
+ * @param styledTable
509
+ */
510
+ function createDataPlacementInfo<V>(
511
+ tableData: TableData<V>,
512
+ tableSelection: Selection<SVGGElement, any, null, undefined>,
513
+ uniqueTableId: string,
514
+ styledTable: StyledTable<V>
515
+ ): DataFrame<ElementPlacementInfo> {
516
+ // when the table has a column header, then the data starts at row 1
517
+ // otherwise, the data starts at row 0
518
+ const rowOffset = tableData.hasRowHeader() ? 1 : 0
519
+ const columnOffset = tableData.hasColumnHeader() ? 1 : 0
520
+ return tableData
521
+ .data()
522
+ .map(df => {
523
+ const groupSelection = tableSelection
524
+ .append('g')
525
+ .attr('class', 'tooltip-table-data')
526
+ .attr('id', dataGroupId(uniqueTableId))
527
+ //
528
+ return df.mapElements((element, rowIndex, columnIndex) => {
529
+ // the style with the highest priority for the cell
530
+ const style = styledTable
531
+ .stylesForTableCoordinates(rowIndex + rowOffset, columnIndex + columnOffset)
532
+ .getOrElse({...defaultCellStyle})
533
+
534
+ const cellSelection = createCellSelection(uniqueTableId, rowIndex + rowOffset, columnIndex + columnOffset, groupSelection, style)
535
+ const borderSelection = createBorderSelection(uniqueTableId, rowIndex + rowOffset, columnIndex + columnOffset, groupSelection, style)
536
+ const textSelection = createTextSelection(uniqueTableId, rowIndex + rowOffset, columnIndex + columnOffset, groupSelection, style, element)
537
+ return elementInfoFrom(textSelection, cellSelection, borderSelection, {...style})
538
+ })
539
+ })
540
+ .getOrElse(DataFrame.empty<ElementPlacementInfo>())
541
+ }
542
+
543
+ /**
544
+ * Calculates the
545
+ * 1. table width and height,
546
+ * 2. width of each column in the table,
547
+ * 3. height of each row in the table,
548
+ * 4.
549
+ *
550
+ * @param tableData
551
+ * @param styledTable
552
+ * @param coordinates
553
+ */
554
+ function calculateRenderingInfo<V>(
555
+ tableData: TableData<ElementPlacementInfo>,
556
+ styledTable: StyledTable<V>,
557
+ coordinates: [x: number, y: number] | ((width: number, height: number) => [x: number, y: number])
558
+ ): TableRenderingInfo {
559
+
560
+ type WithWidthHeight = ElementPlacementInfo & { cellWidth: number, cellHeight: number }
561
+
562
+ type MinMax = { min: number, max: number, minValues: Array<number>, maxValues: Array<number> }
563
+
564
+ /**
565
+ * Calculates the min and max values for the extracted value from the cell
566
+ * @param elements An array of rows (data frame row slices) or columns (data
567
+ * frame column slices)
568
+ * @param extractor
569
+ */
570
+ function minMaxFor(
571
+ elements: Array<Array<WithWidthHeight>>,
572
+ extractor: (cell: WithWidthHeight) => number
573
+ ): MinMax {
574
+ return elements.reduce(
575
+ (minMax: MinMax, row: Array<WithWidthHeight>): MinMax => {
576
+ const cellValue = row.map(cell => Math.ceil(extractor(cell)))
577
+ const {min, max, minValues, maxValues} = minMax
578
+ const mins = Math.min(...cellValue)
579
+ minValues.push(mins)
580
+ const maxes = Math.max(...cellValue)
581
+ maxValues.push(maxes)
582
+ return {
583
+ minValues,
584
+ min: Math.min(min, mins),
585
+ maxValues,
586
+ max: Math.max(max, maxes)
587
+ }
588
+ },
589
+ {min: Infinity, max: -Infinity, minValues: [], maxValues: []})
590
+ }
591
+
592
+ const df = tableData.unwrapDataFrame()
593
+
594
+ // 1. calculate the bounding box for the element
595
+ // 2. calculate the cell width/height using the bounding width/height and the
596
+ // maximum and minimum cell width/height from the styling
597
+ const whdf: DataFrame<WithWidthHeight> = df.mapElements((element, rowIndex, columnIndex) => {
598
+
599
+ // grab the style for the cell
600
+ const style = styledTable
601
+ .stylesForTableCoordinates(rowIndex, columnIndex)
602
+ .getOrElse({...defaultCellStyle})
603
+
604
+ // calculate the actual width and height of the cell
605
+ const {width, height} = calculateCellDimensions(style, element.textWidth, element.textHeight)
606
+
607
+ return {...element, cellWidth: width, cellHeight: height}
608
+ })
609
+
610
+ // 3. calculate the cell width for each column based on the max and min width of
611
+ // all the cells in the column
612
+ // 4. calculate the cell height for each row based on the max and min height of
613
+ // all the cells in the row
614
+ // 5. calculate the table width/height by adding up all the column-widths/row-heights
615
+ // 6. calculate the (x, y)-coordinates of the text for each cell
616
+ // 7. update each cell's coordinates (here we need to update the header groups, footer
617
+ // groups, and then the cell's coordinates relative to their group.. :()
618
+ const minMaxColumnWidths = minMaxFor(whdf.columnSlices(), cell => cell.cellWidth)
619
+ const minMaxRowHeights = minMaxFor(whdf.rowSlices(), cell => cell.cellHeight)
620
+
621
+ const columnWidths = df.columnSlices().map((_, index) => {
622
+ if (index === 0 && tableData.hasRowHeader()) {
623
+ return styledTable.rowHeaderStyle()
624
+ // todo update the styling for the row-headers to include the width and the min and max width
625
+ .map(_ => minMaxColumnWidths.maxValues[index])
626
+ .getOrElse(Math.max(defaultColumnStyle.dimension.minWidth, Math.min(defaultColumnStyle.dimension.maxWidth, minMaxColumnWidths.maxValues[index])))
627
+ }
628
+ return styledTable.columnStyleFor(index)
629
+ .map(styling => Math.max(styling.style.dimension.minWidth, Math.min(styling.style.dimension.maxWidth, minMaxColumnWidths.maxValues[index])))
630
+ .getOrElse(Math.max(defaultColumnStyle.dimension.minWidth, Math.min(defaultColumnStyle.dimension.maxWidth, minMaxColumnWidths.maxValues[index])))
631
+ })
632
+ const rowHeights = df.rowSlices().map((_, index) => {
633
+ if (index === 0 && tableData.hasColumnHeader()) {
634
+ return styledTable.columnHeaderStyle()
635
+ .map(styling => Math.max(styling.style.dimension.minHeight, Math.min(styling.style.dimension.maxHeight, minMaxRowHeights.maxValues[index])))
636
+ .getOrElse(Math.max(defaultRowStyle.dimension.minHeight, Math.min(defaultRowStyle.dimension.maxHeight, minMaxRowHeights.maxValues[index])))
637
+ }
638
+ return styledTable.rowStyleFor(index)
639
+ .map(styling => Math.max(
640
+ styling.style.dimension.minHeight,
641
+ Math.min(
642
+ styling.style.dimension.maxHeight,
643
+ minMaxRowHeights.maxValues[index]
644
+ )
645
+ ))
646
+ .getOrElse(Math.max(defaultRowStyle.dimension.minHeight, Math.min(defaultRowStyle.dimension.maxHeight, minMaxRowHeights.maxValues[index])))
647
+ })
648
+
649
+ const dimAdjustedDf = df
650
+ .mapElements((element, rowIndex, columnIndex): ElementPlacementInfo =>
651
+ ({
652
+ ...element,
653
+ cellStyle: {
654
+ ...element.cellStyle,
655
+ dimension: {
656
+ ...element.cellStyle.dimension,
657
+ width: columnWidths[columnIndex],
658
+ height: rowHeights[rowIndex]
659
+ }
660
+ }
661
+ })
662
+ )
663
+
664
+ // todo gross as shit, is there a better way
665
+ const cumColumnWidths = columnWidths.reduce((sum: Array<number>, curr: number, index) => {
666
+ sum.push(Math.ceil(curr) + sum[index])
667
+ return sum
668
+ }, [0])
669
+ const cumRowHeights = rowHeights.reduce((sum: Array<number>, curr: number, index) => {
670
+ sum.push(Math.ceil(curr) + sum[index])
671
+ return sum
672
+ }, [0])
673
+ const positionAdjustedDf = dimAdjustedDf
674
+ .mapElements((element, rowIndex, columnIndex): ElementPlacementInfo & CellRenderingDimensions => {
675
+ const cellWidth = element.cellStyle.dimension.width
676
+ const cellHeight = element.cellStyle.dimension.height
677
+ return {
678
+ ...element,
679
+ width: cellWidth,
680
+ height: cellHeight,
681
+ x: cumColumnWidths[columnIndex] + textXOffset(element, cellWidth),
682
+ y: cumRowHeights[rowIndex] + textYOffset(element, cellHeight),
683
+ cellX: cumColumnWidths[columnIndex],
684
+ cellY: cumRowHeights[rowIndex]
685
+ }
686
+ })
687
+
688
+ const tableWidth = columnWidths.reduce((sum, width) => sum + width, 0)
689
+ const tableHeight = rowHeights.reduce((sum, height) => sum + height, 0)
690
+ const [x, y] = (Array.isArray(coordinates)) ? coordinates : coordinates(tableWidth, tableWidth)
691
+
692
+ return {
693
+ tableWidth,
694
+ tableHeight,
695
+ tableX: x,
696
+ tableY: y,
697
+ renderingInfo: TableData.fromDataFrame(positionAdjustedDf)
698
+ }
699
+ }
700
+
701
+
702
+ /**
703
+ * Calculates the row and column dimensions based on the min and max width and hieght of
704
+ * the cells.
705
+ * @param style
706
+ * @param width
707
+ * @param height
708
+ */
709
+ function calculateCellDimensions(style: CellStyle, width: number, height: number): {
710
+ width: number,
711
+ height: number
712
+ } {
713
+ return {
714
+ width: Math.max(
715
+ Math.min(
716
+ width + style.padding.left + style.padding.right,
717
+ style.dimension.maxWidth
718
+ ),
719
+ style.dimension.minWidth
720
+ ),
721
+ height: Math.max(
722
+ Math.min(
723
+ height + style.padding.top + style.padding.bottom,
724
+ style.dimension.maxHeight
725
+ ),
726
+ style.dimension.minHeight
727
+ )
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Calculates the x-offset of the text with the cell
733
+ * @param element The element to render
734
+ * @param cellWidth The width of the cell into which the text is rendered
735
+ */
736
+ function textXOffset(element: ElementPlacementInfo, cellWidth: number): number {
737
+ switch (element.cellStyle.alignText) {
738
+ case "left":
739
+ return element.cellStyle.padding.left
740
+
741
+ case "center":
742
+ return cellWidth / 2
743
+
744
+ case "right":
745
+ return cellWidth - element.cellStyle.padding.right
746
+
747
+ default:
748
+ return cellWidth / 2
749
+ }
750
+ }
751
+
752
+ function textYOffset(element: ElementPlacementInfo, cellHeight: number): number {
753
+ switch (element.cellStyle.verticalAlignText) {
754
+ case "top":
755
+ return element.cellStyle.padding.top
756
+
757
+ case "middle":
758
+ return cellHeight / 2
759
+
760
+ case "bottom":
761
+ return cellHeight - element.cellStyle.padding.bottom
762
+
763
+ default:
764
+ return cellHeight / 2
765
+ }
766
+ }
767
+
768
+ /**
769
+ * Places the text into the table at its (x, y) coordinates, and returns the {@link TableData} holding
770
+ * the updated values.
771
+ * @param tableRenderingInfo The information for rendering the text as a table
772
+ * @return A {@link TableData} holding the updated values.
773
+ */
774
+ function placeTextInTable(tableRenderingInfo: TableRenderingInfo): TableRenderingInfo {
775
+ const updatedDf = tableRenderingInfo.renderingInfo.unwrapDataFrame()
776
+ .mapElements(info => {
777
+ info.cellSelection
778
+ .attr('width', info.width)
779
+ .attr('height', info.height)
780
+ .attr('transform', `translate(${info.cellX}, ${info.cellY})`)
781
+ info.textSelection
782
+ .attr('text-anchor', textAnchorFrom(info.cellStyle.alignText))
783
+ .attr('dominant-baseline', dominantBaselineFrom(info.cellStyle.verticalAlignText))
784
+ .attr('transform', `translate(${info.x}, ${info.y})`)
785
+ if (info.borderSelection.top) {
786
+ info.borderSelection.top
787
+ .attr('x1', info.cellX)
788
+ .attr('y1', info.cellY)
789
+ .attr('x2', info.cellX + info.width)
790
+ .attr('y2', info.cellY)
791
+ }
792
+ if (info.borderSelection.bottom) {
793
+ info.borderSelection.bottom
794
+ .attr('x1', info.cellX)
795
+ .attr('y1', info.cellY + info.height)
796
+ .attr('x2', info.cellX + info.width)
797
+ .attr('y2', info.cellY + info.height)
798
+ }
799
+ if (info.borderSelection.right) {
800
+ info.borderSelection.right
801
+ .attr('x1', info.cellX + info.width)
802
+ .attr('y1', info.cellY)
803
+ .attr('x2', info.cellX + info.width)
804
+ .attr('y2', info.cellY + info.height)
805
+ }
806
+ if (info.borderSelection.left) {
807
+ info.borderSelection.left
808
+ .attr('x1', info.cellX)
809
+ .attr('y1', info.cellY)
810
+ .attr('x2', info.cellX)
811
+ .attr('y2', info.cellY + info.height)
812
+ }
813
+ return info
814
+ })
815
+ return {
816
+ ...tableRenderingInfo,
817
+ renderingInfo: TableData.fromDataFrame(updatedDf)
818
+ }
819
+ }
820
+