@@ -108,7 +108,7 @@ internal fun tableJs(columns: List<ColumnDataForJs>, id: Int, rootId: Int, nrow:
108
108
val colName = col.renderHeader().escapeForHtmlInJs()
109
109
append(" { name: \" $colName \" , children: $children , rightAlign: ${col.rightAlign} , values: $values }, \n " )
110
110
111
- return colIndex
111
+ return @appendColWithChildren colIndex
112
112
}
113
113
columns.forEach { appendColWithChildren(it) }
114
114
append(" ]" )
@@ -193,109 +193,137 @@ public fun AnyFrame.toStaticHtml(
193
193
): DataFrameHtmlData {
194
194
val df = this
195
195
val id = " static_df_${nextTableId()} "
196
+
197
+ // Retrieve all columns, including nested ones
196
198
val flattenedCols = getColumnsWithPaths { cols { ! it.isColumnGroup() }.recursively() }
199
+
200
+ // Get a grid of columns for the header, as well as the side borders for each cell
197
201
val colGrid = getColumnsHeaderGrid()
198
202
val borders = colGrid.last().map { it.borders - Border .BOTTOM }
203
+
204
+ // Limit for number of rows in dataframes inside frame columns
199
205
val nestedRowsLimit = configuration.nestedRowsLimit
200
206
207
+ // Adds the given tag to the html, with the given attributes and contents
201
208
fun StringBuilder.emitTag (tag : String , attributes : String = "", tagContents : StringBuilder .() -> Unit ) {
202
- append(" <" )
203
- append(tag)
209
+ append(" <$tag " )
204
210
if (attributes.isNotEmpty()) {
205
211
append(" " )
206
212
append(attributes)
207
213
}
208
214
append(" >" )
209
-
210
215
tagContents()
211
-
212
- append(" </" )
213
- append(tag)
214
- append(" >" )
216
+ append(" </$tag >" )
215
217
}
216
218
219
+ // Adds a header to the html. This header contains all the column names including nested ones
220
+ // properly laid out with borders.
217
221
fun StringBuilder.emitHeader () = emitTag(" thead" ) {
218
222
for (row in colGrid) {
219
223
emitTag(" tr" ) {
220
224
for ((j, col) in row.withIndex()) {
221
- var borders = col.borders
225
+ val colBorders = col.borders.toMutableSet()
226
+ // check if the next cell has a left border, and if so, add a right border to this cell
222
227
if (row.getOrNull(j + 1 )?.borders?.contains(Border .LEFT ) == true ) {
223
- // because left does not work unless at first column
224
- borders + = Border .RIGHT
228
+ colBorders + = Border .RIGHT
225
229
}
226
- emitTag(" th" , borders .toClass()) {
230
+ emitTag(" th" , " ${colBorders .toClass()} style= \" text-align:left \" " ) {
227
231
append(col.columnWithPath?.name ? : " " )
228
232
}
229
233
}
230
234
}
231
235
}
232
236
}
233
237
234
- fun StringBuilder.emitCell (cellValue : Any? , borders : Set <Border >): Unit = emitTag(" td" , borders.toClass()) {
235
- when (cellValue) {
236
- is AnyFrame ->
237
- emitTag(" details" ) {
238
- emitTag(" summary" ) {
239
- append(" DataFrame " )
240
- append(cellRenderer.content(cellValue, configuration).truncatedContent)
241
- }
242
- append(
243
- cellValue.take(nestedRowsLimit ? : Int .MAX_VALUE )
244
- .toStaticHtml(configuration, cellRenderer, includeCss = false )
245
- .toString()
246
- )
247
- val size = cellValue.rowsCount()
248
- if (size > nestedRowsLimit ? : Int .MAX_VALUE ) {
249
- emitTag(" p" ) {
250
- append(" ... showing only top $nestedRowsLimit of $size rows" )
238
+ // Adds a single cell to the html. DataRows from column groups already need to be split up into separate cells.
239
+ fun StringBuilder.emitCell (cellValue : Any? , borders : Set <Border >): Unit =
240
+ emitTag(" td" , borders.toClass()) {
241
+ when (cellValue) {
242
+ // uses the <details> and <summary> to create a collapsible cell for dataframes
243
+ is AnyFrame ->
244
+ emitTag(" details" ) {
245
+ emitTag(" summary" ) {
246
+ append(" DataFrame [${cellValue.size} ]" )
247
+ }
248
+ // add the dataframe as a nested table limiting the number of rows if needed
249
+ // CSS will not be included here, as it is already included in the main table
250
+ append(
251
+ cellValue.take(nestedRowsLimit ? : Int .MAX_VALUE )
252
+ .toStaticHtml(configuration, cellRenderer, includeCss = false )
253
+ .body
254
+ )
255
+ val size = cellValue.rowsCount()
256
+ if (size > (nestedRowsLimit ? : Int .MAX_VALUE )) {
257
+ emitTag(" p" ) {
258
+ append(" ... showing only top $nestedRowsLimit of $size rows" )
259
+ }
251
260
}
252
261
}
253
- }
254
262
255
- else ->
256
- append(cellRenderer.content(cellValue, configuration).truncatedContent)
263
+ // Else use the default cell renderer
264
+ else ->
265
+ append(cellRenderer.content(cellValue, configuration).truncatedContent)
266
+ }
257
267
}
258
- }
259
268
269
+ // Adds a single row to the html. This row uses the flattened columns to get them displayed non-collapsed.
260
270
fun StringBuilder.emitRow (row : AnyRow ) = emitTag(" tr" ) {
261
271
for ((i, col) in flattenedCols.withIndex()) {
262
- var border = borders[i]
272
+ val border = borders[i].toMutableSet()
273
+ // check if the next cell has a left border, and if so, add a right border to this cell
263
274
if (borders.getOrNull(i + 1 )?.contains(Border .LEFT ) == true ) {
264
- // because left does not work unless at first column
265
275
border + = Border .RIGHT
266
276
}
267
277
val cell = row[col.path()]
268
278
emitCell(cell, border)
269
279
}
270
280
}
271
281
282
+ // Adds the body of the html. This body contains all the cols and rows of the dataframe.
272
283
fun StringBuilder.emitBody () = emitTag(" tbody" ) {
273
284
val rowsCountToRender = minOf(rowsCount(), configuration.rowsLimit ? : Int .MAX_VALUE )
274
285
for (rowIndex in 0 .. < rowsCountToRender) {
275
286
emitRow(df[rowIndex])
276
287
}
277
288
}
278
289
290
+ // Base function which adds the table to the html.
279
291
fun StringBuilder.emitTable () = emitTag(" table" , """ class="dataframe" id="$id """" ) {
280
292
emitHeader()
281
293
emitBody()
282
294
}
283
295
284
- return DataFrameHtmlData (body = buildString { emitTable() }) +
285
- DataFrameHtmlData .tableDefinitions(includeJs = false , includeCss = includeCss)
296
+ return DataFrameHtmlData (
297
+ body = buildString { emitTable() },
298
+ // will hide the table if JS is enabled
299
+ script = """
300
+ document.getElementById("$id ").style.display = "none";
301
+ """ .trimIndent(),
302
+ ) + DataFrameHtmlData .tableDefinitions(includeJs = false , includeCss = includeCss)
286
303
}
287
304
305
+ /* *
306
+ * Border enum used for static rendering of tables in html/css.
307
+ * @see toClass
308
+ */
288
309
private enum class Border (val className : String ) {
289
- // NOTE: these don't render unless at leftmost; add rightborder to the previous cell.
310
+ // NOTE: these don't render unless at leftmost; add rightBorder to the previous cell.
290
311
LEFT (" leftBorder" ),
291
312
RIGHT (" rightBorder" ),
292
313
BOTTOM (" bottomBorder" );
293
314
}
294
315
316
+ /* *
317
+ * Converts a set of borders to a class string for html/css rendering.
318
+ */
295
319
private fun Set<Border>.toClass (): String =
296
320
if (isEmpty()) " "
297
321
else " class=\" ${joinToString(" " ) { it.className }} \" "
298
322
323
+ /* *
324
+ * Wrapper class which contains a nullable [ColumnWithPath] and a set of [Border]s.
325
+ * (Empty cells can have borders too)
326
+ */
299
327
private data class ColumnWithPathWithBorder <T >(
300
328
val columnWithPath : ColumnWithPath <T >? = null ,
301
329
val borders : Set <Border > = emptySet(),
@@ -327,36 +355,41 @@ private fun AnyFrame.getColumnsHeaderGrid(): List<List<ColumnWithPathWithBorder<
327
355
val colGroup = asColumnGroup()
328
356
val maxDepth = maxDepth()
329
357
val maxWidth = colGroup.maxWidth()
330
- val map =
358
+ val matrix =
331
359
MutableList (maxDepth + 1 ) { MutableList (maxWidth) { ColumnWithPathWithBorder <Any ?>() } }
332
360
333
361
fun ColumnWithPath <* >.addChildren (depth : Int = 0, breadth : Int = 0) {
334
362
var breadth = breadth
335
363
val children = children()
336
364
val lastIndex = children.lastIndex
337
365
for ((i, child) in children().withIndex()) {
338
- map [depth][breadth] = map [depth][breadth].copy(columnWithPath = child)
366
+ matrix [depth][breadth] = matrix [depth][breadth].copy(columnWithPath = child)
339
367
340
- // draw colGroup borders unless at start/end of table
368
+ // draw colGroup side borders unless at start/end of table
341
369
val borders = mutableSetOf<Border >()
342
370
if (i == 0 && breadth != 0 ) borders + = Border .LEFT
343
371
if (i == lastIndex && breadth != maxWidth - 1 ) borders + = Border .RIGHT
372
+
373
+ // draw bottom border if at max depth
344
374
if (depth == maxDepth) borders + = Border .BOTTOM
345
375
346
376
if (borders.isNotEmpty()) {
377
+ // draw borders in other cells
378
+ // from depth - 1 (to include not just the children but the current cell too)
347
379
for (j in (depth - 1 ).coerceAtLeast(0 ).. maxDepth) {
348
- map [j][breadth] = map [j][breadth].let { it.copy(borders = it.borders + borders) }
380
+ matrix [j][breadth] = matrix [j][breadth].let { it.copy(borders = it.borders + borders) }
349
381
}
350
382
}
351
383
384
+ // recurse if needed to render children of the current column group
352
385
if (child is ColumnGroup <* >) {
353
386
child.addChildren(depth + 1 , breadth)
354
387
}
355
388
breadth + = child.maxWidth()
356
389
}
357
390
}
358
391
colGroup.addPath().addChildren()
359
- return map
392
+ return matrix
360
393
}
361
394
362
395
internal fun DataFrameHtmlData.print () = println (this )
@@ -421,7 +454,7 @@ public fun <T> DataFrame<T>.toHTML(
421
454
public data class DataFrameHtmlData (
422
455
@Language(" css" ) val style : String = " " ,
423
456
@Language(" html" , prefix = " <body>" , suffix = " </body>" ) val body : String = " " ,
424
- @Language(" js" ) val script : String = " "
457
+ @Language(" js" ) val script : String = " " ,
425
458
) {
426
459
override fun toString (): String = buildString {
427
460
appendLine(" <html>" )
@@ -589,7 +622,7 @@ internal class DataFrameFormatter(
589
622
590
623
private fun RenderedContent.addCss (css : String? = null): RenderedContent {
591
624
return if (css != null ) {
592
- copy(truncatedContent = " <span class=\" $css \" >" + truncatedContent + " </span>" , isFormatted = true )
625
+ copy(truncatedContent = " <span class=\" $css \" >$ truncatedContent </span>" , isFormatted = true )
593
626
} else this
594
627
}
595
628
@@ -598,7 +631,7 @@ internal class DataFrameFormatter(
598
631
val ellipsis = " ..." .ellipsis(str)
599
632
if (limit < 4 ) ellipsis
600
633
else {
601
- val len = Math .max (limit - 3 , 1 )
634
+ val len = (limit - 3 ).coerceAtLeast( 1 )
602
635
RenderedContent .textWithLength(str.substring(0 , len).escapeHTML(), len) + ellipsis
603
636
}
604
637
} else {
@@ -733,7 +766,7 @@ internal class DataFrameFormatter(
733
766
val keyLimit = limit - sizeOfValue
734
767
if (key.length > keyLimit) {
735
768
if (limit > 3 ) {
736
- (key + " ..." ).truncate(limit).addCss(structuralClass)
769
+ (" $key ..." ).truncate(limit).addCss(structuralClass)
737
770
} else null
738
771
} else {
739
772
val renderedValue =
0 commit comments