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
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
|
+
|