Skip to content

Commit 7b30f5a

Browse files
committed
Merge remote-tracking branch 'origin/better-static-tables' into better-static-tables
# Conflicts: # examples/notebooks/github/github.ipynb
2 parents 81ca529 + 6a50814 commit 7b30f5a

File tree

4 files changed

+555
-474
lines changed

4 files changed

+555
-474
lines changed

core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt

Lines changed: 80 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ internal fun tableJs(columns: List<ColumnDataForJs>, id: Int, rootId: Int, nrow:
108108
val colName = col.renderHeader().escapeForHtmlInJs()
109109
append("{ name: \"$colName\", children: $children, rightAlign: ${col.rightAlign}, values: $values }, \n")
110110

111-
return colIndex
111+
return@appendColWithChildren colIndex
112112
}
113113
columns.forEach { appendColWithChildren(it) }
114114
append("]")
@@ -193,109 +193,137 @@ public fun AnyFrame.toStaticHtml(
193193
): DataFrameHtmlData {
194194
val df = this
195195
val id = "static_df_${nextTableId()}"
196+
197+
// Retrieve all columns, including nested ones
196198
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
197201
val colGrid = getColumnsHeaderGrid()
198202
val borders = colGrid.last().map { it.borders - Border.BOTTOM }
203+
204+
// Limit for number of rows in dataframes inside frame columns
199205
val nestedRowsLimit = configuration.nestedRowsLimit
200206

207+
// Adds the given tag to the html, with the given attributes and contents
201208
fun StringBuilder.emitTag(tag: String, attributes: String = "", tagContents: StringBuilder.() -> Unit) {
202-
append("<")
203-
append(tag)
209+
append("<$tag")
204210
if (attributes.isNotEmpty()) {
205211
append(" ")
206212
append(attributes)
207213
}
208214
append(">")
209-
210215
tagContents()
211-
212-
append("</")
213-
append(tag)
214-
append(">")
216+
append("</$tag>")
215217
}
216218

219+
// Adds a header to the html. This header contains all the column names including nested ones
220+
// properly laid out with borders.
217221
fun StringBuilder.emitHeader() = emitTag("thead") {
218222
for (row in colGrid) {
219223
emitTag("tr") {
220224
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
222227
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
225229
}
226-
emitTag("th", borders.toClass()) {
230+
emitTag("th", "${colBorders.toClass()} style=\"text-align:left\"") {
227231
append(col.columnWithPath?.name ?: "")
228232
}
229233
}
230234
}
231235
}
232236
}
233237

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+
}
251260
}
252261
}
253-
}
254262

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+
}
257267
}
258-
}
259268

269+
// Adds a single row to the html. This row uses the flattened columns to get them displayed non-collapsed.
260270
fun StringBuilder.emitRow(row: AnyRow) = emitTag("tr") {
261271
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
263274
if (borders.getOrNull(i + 1)?.contains(Border.LEFT) == true) {
264-
// because left does not work unless at first column
265275
border += Border.RIGHT
266276
}
267277
val cell = row[col.path()]
268278
emitCell(cell, border)
269279
}
270280
}
271281

282+
// Adds the body of the html. This body contains all the cols and rows of the dataframe.
272283
fun StringBuilder.emitBody() = emitTag("tbody") {
273284
val rowsCountToRender = minOf(rowsCount(), configuration.rowsLimit ?: Int.MAX_VALUE)
274285
for (rowIndex in 0..<rowsCountToRender) {
275286
emitRow(df[rowIndex])
276287
}
277288
}
278289

290+
// Base function which adds the table to the html.
279291
fun StringBuilder.emitTable() = emitTag("table", """class="dataframe" id="$id"""") {
280292
emitHeader()
281293
emitBody()
282294
}
283295

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)
286303
}
287304

305+
/**
306+
* Border enum used for static rendering of tables in html/css.
307+
* @see toClass
308+
*/
288309
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.
290311
LEFT("leftBorder"),
291312
RIGHT("rightBorder"),
292313
BOTTOM("bottomBorder");
293314
}
294315

316+
/**
317+
* Converts a set of borders to a class string for html/css rendering.
318+
*/
295319
private fun Set<Border>.toClass(): String =
296320
if (isEmpty()) ""
297321
else "class=\"${joinToString(" ") { it.className }}\""
298322

323+
/**
324+
* Wrapper class which contains a nullable [ColumnWithPath] and a set of [Border]s.
325+
* (Empty cells can have borders too)
326+
*/
299327
private data class ColumnWithPathWithBorder<T>(
300328
val columnWithPath: ColumnWithPath<T>? = null,
301329
val borders: Set<Border> = emptySet(),
@@ -327,36 +355,41 @@ private fun AnyFrame.getColumnsHeaderGrid(): List<List<ColumnWithPathWithBorder<
327355
val colGroup = asColumnGroup()
328356
val maxDepth = maxDepth()
329357
val maxWidth = colGroup.maxWidth()
330-
val map =
358+
val matrix =
331359
MutableList(maxDepth + 1) { MutableList(maxWidth) { ColumnWithPathWithBorder<Any?>() } }
332360

333361
fun ColumnWithPath<*>.addChildren(depth: Int = 0, breadth: Int = 0) {
334362
var breadth = breadth
335363
val children = children()
336364
val lastIndex = children.lastIndex
337365
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)
339367

340-
// draw colGroup borders unless at start/end of table
368+
// draw colGroup side borders unless at start/end of table
341369
val borders = mutableSetOf<Border>()
342370
if (i == 0 && breadth != 0) borders += Border.LEFT
343371
if (i == lastIndex && breadth != maxWidth - 1) borders += Border.RIGHT
372+
373+
// draw bottom border if at max depth
344374
if (depth == maxDepth) borders += Border.BOTTOM
345375

346376
if (borders.isNotEmpty()) {
377+
// draw borders in other cells
378+
// from depth - 1 (to include not just the children but the current cell too)
347379
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) }
349381
}
350382
}
351383

384+
// recurse if needed to render children of the current column group
352385
if (child is ColumnGroup<*>) {
353386
child.addChildren(depth + 1, breadth)
354387
}
355388
breadth += child.maxWidth()
356389
}
357390
}
358391
colGroup.addPath().addChildren()
359-
return map
392+
return matrix
360393
}
361394

362395
internal fun DataFrameHtmlData.print() = println(this)
@@ -421,7 +454,7 @@ public fun <T> DataFrame<T>.toHTML(
421454
public data class DataFrameHtmlData(
422455
@Language("css") val style: String = "",
423456
@Language("html", prefix = "<body>", suffix = "</body>") val body: String = "",
424-
@Language("js") val script: String = ""
457+
@Language("js") val script: String = "",
425458
) {
426459
override fun toString(): String = buildString {
427460
appendLine("<html>")
@@ -589,7 +622,7 @@ internal class DataFrameFormatter(
589622

590623
private fun RenderedContent.addCss(css: String? = null): RenderedContent {
591624
return if (css != null) {
592-
copy(truncatedContent = "<span class=\"$css\">" + truncatedContent + "</span>", isFormatted = true)
625+
copy(truncatedContent = "<span class=\"$css\">$truncatedContent</span>", isFormatted = true)
593626
} else this
594627
}
595628

@@ -598,7 +631,7 @@ internal class DataFrameFormatter(
598631
val ellipsis = "...".ellipsis(str)
599632
if (limit < 4) ellipsis
600633
else {
601-
val len = Math.max(limit - 3, 1)
634+
val len = (limit - 3).coerceAtLeast(1)
602635
RenderedContent.textWithLength(str.substring(0, len).escapeHTML(), len) + ellipsis
603636
}
604637
} else {
@@ -733,7 +766,7 @@ internal class DataFrameFormatter(
733766
val keyLimit = limit - sizeOfValue
734767
if (key.length > keyLimit) {
735768
if (limit > 3) {
736-
(key + "...").truncate(limit).addCss(structuralClass)
769+
("$key...").truncate(limit).addCss(structuralClass)
737770
} else null
738771
} else {
739772
val renderedValue =

0 commit comments

Comments
 (0)