@@ -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()) {
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 " )
247
+ append(cellRenderer.content(cellValue, configuration).truncatedContent)
248
+ }
249
+ // add the dataframe as a nested table limiting the number of rows if needed
250
+ // CSS will not be included here, as it is already included in the main table
251
+ append(
252
+ cellValue.take(nestedRowsLimit ? : Int .MAX_VALUE )
253
+ .toStaticHtml(configuration, cellRenderer, includeCss = false )
254
+ .toString()
255
+ )
256
+ val size = cellValue.rowsCount()
257
+ if (size > (nestedRowsLimit ? : Int .MAX_VALUE )) {
258
+ emitTag(" p" ) {
259
+ append(" ... showing only top $nestedRowsLimit of $size rows" )
260
+ }
251
261
}
252
262
}
253
- }
254
263
255
- else ->
256
- append(cellRenderer.content(cellValue, configuration).truncatedContent)
264
+ // Else use the default cell renderer
265
+ else ->
266
+ append(cellRenderer.content(cellValue, configuration).truncatedContent)
267
+ }
257
268
}
258
- }
259
269
270
+ // Adds a single row to the html. This row uses the flattened columns to get them displayed non-collapsed.
260
271
fun StringBuilder.emitRow (row : AnyRow ) = emitTag(" tr" ) {
261
272
for ((i, col) in flattenedCols.withIndex()) {
262
- var border = borders[i]
273
+ val border = borders[i].toMutableSet()
274
+ // check if the next cell has a left border, and if so, add a right border to this cell
263
275
if (borders.getOrNull(i + 1 )?.contains(Border .LEFT ) == true ) {
264
- // because left does not work unless at first column
265
276
border + = Border .RIGHT
266
277
}
267
278
val cell = row[col.path()]
268
279
emitCell(cell, border)
269
280
}
270
281
}
271
282
283
+ // Adds the body of the html. This body contains all the cols and rows of the dataframe.
272
284
fun StringBuilder.emitBody () = emitTag(" tbody" ) {
273
285
val rowsCountToRender = minOf(rowsCount(), configuration.rowsLimit ? : Int .MAX_VALUE )
274
286
for (rowIndex in 0 .. < rowsCountToRender) {
275
287
emitRow(df[rowIndex])
276
288
}
277
289
}
278
290
291
+ // Base function which adds the table to the html.
279
292
fun StringBuilder.emitTable () = emitTag(" table" , """ class="dataframe" id="$id """" ) {
280
293
emitHeader()
281
294
emitBody()
282
295
}
283
296
284
- return DataFrameHtmlData (body = buildString { emitTable() }) +
285
- DataFrameHtmlData .tableDefinitions(includeJs = false , includeCss = includeCss)
297
+ return DataFrameHtmlData (
298
+ body = buildString { emitTable() },
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>" )
0 commit comments