From db50eea97aba0c5f0285b87400719509f03cb05c Mon Sep 17 00:00:00 2001 From: Ilya Muradyan Date: Fri, 20 Oct 2023 21:56:12 +0200 Subject: [PATCH 1/2] Add static rendering for GitHub Fixes #476 Signed-off-by: Ilya Muradyan --- .../jetbrains/kotlinx/dataframe/io/html.kt | 64 +++++++++++++++++++ .../dataframe/rendering/RenderingTests.kt | 20 ++++++ .../jetbrains/kotlinx/dataframe/io/html.kt | 64 +++++++++++++++++++ .../dataframe/rendering/RenderingTests.kt | 20 ++++++ 4 files changed, 168 insertions(+) diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt index bedaee0da8..92d8fc5ad6 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt @@ -174,6 +174,68 @@ internal fun AnyFrame.toHtmlData( return DataFrameHtmlData("", body, script) } +internal fun AnyFrame.toStaticHtml( + configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT, + cellRenderer: CellRenderer, +): DataFrameHtmlData { + val df = this + val id = "static_df_${nextTableId()}" + val columnsToRender = columns() + + fun StringBuilder.emitTag(tag: String, attributes: String = "", tagContents: StringBuilder.() -> Unit) { + append("<") + append(tag) + if (attributes.isNotEmpty()) { + append(" ") + append(attributes) + } + append(">") + + tagContents() + + append("") + } + + fun StringBuilder.emitHeader() = emitTag("thead") { + emitTag("tr") { + columnsToRender.forEach { col -> + emitTag("th") { + append(col.name()) + } + } + } + } + + fun StringBuilder.emitRow(row: AnyRow) = emitTag("tr") { + columnsToRender.forEach { col -> + emitTag("td") { + append(cellRenderer.content(row[col.path()], configuration).truncatedContent) + } + } + } + + fun StringBuilder.emitBody() = emitTag("tbody") { + val rowsCountToRender = minOf(rowsCount(), configuration.rowsLimit ?: Int.MAX_VALUE) + for (rowIndex in 0.. DataFrame.toHTML( var tableHtml = toHtmlData(configuration, cellRenderer) + tableHtml += toStaticHtml(configuration, cellRenderer) + if (bodyFooter != null) { tableHtml += DataFrameHtmlData("", bodyFooter, "") } diff --git a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/RenderingTests.kt b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/RenderingTests.kt index ed6f5c9f1d..17fb3759f0 100644 --- a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/RenderingTests.kt +++ b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/RenderingTests.kt @@ -125,4 +125,24 @@ class RenderingTests { df.toHTML().script shouldContain rendered } } + + @Test + fun `static rendering should be present`() { + val df = dataFrameOf("a", "b")(listOf(1, 1), listOf(2, 4)) + val actualHtml = df.toHTML() + + actualHtml.body shouldContain """ + + + ab + + + + + [1, 1][2, 4] + + + + """.trimIndent().replace("\n", "") + } } diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt index bedaee0da8..92d8fc5ad6 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt @@ -174,6 +174,68 @@ internal fun AnyFrame.toHtmlData( return DataFrameHtmlData("", body, script) } +internal fun AnyFrame.toStaticHtml( + configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT, + cellRenderer: CellRenderer, +): DataFrameHtmlData { + val df = this + val id = "static_df_${nextTableId()}" + val columnsToRender = columns() + + fun StringBuilder.emitTag(tag: String, attributes: String = "", tagContents: StringBuilder.() -> Unit) { + append("<") + append(tag) + if (attributes.isNotEmpty()) { + append(" ") + append(attributes) + } + append(">") + + tagContents() + + append("") + } + + fun StringBuilder.emitHeader() = emitTag("thead") { + emitTag("tr") { + columnsToRender.forEach { col -> + emitTag("th") { + append(col.name()) + } + } + } + } + + fun StringBuilder.emitRow(row: AnyRow) = emitTag("tr") { + columnsToRender.forEach { col -> + emitTag("td") { + append(cellRenderer.content(row[col.path()], configuration).truncatedContent) + } + } + } + + fun StringBuilder.emitBody() = emitTag("tbody") { + val rowsCountToRender = minOf(rowsCount(), configuration.rowsLimit ?: Int.MAX_VALUE) + for (rowIndex in 0.. DataFrame.toHTML( var tableHtml = toHtmlData(configuration, cellRenderer) + tableHtml += toStaticHtml(configuration, cellRenderer) + if (bodyFooter != null) { tableHtml += DataFrameHtmlData("", bodyFooter, "") } diff --git a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/RenderingTests.kt b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/RenderingTests.kt index ed6f5c9f1d..17fb3759f0 100644 --- a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/RenderingTests.kt +++ b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/RenderingTests.kt @@ -125,4 +125,24 @@ class RenderingTests { df.toHTML().script shouldContain rendered } } + + @Test + fun `static rendering should be present`() { + val df = dataFrameOf("a", "b")(listOf(1, 1), listOf(2, 4)) + val actualHtml = df.toHTML() + + actualHtml.body shouldContain """ + + + ab + + + + + [1, 1][2, 4] + + + + """.trimIndent().replace("\n", "") + } } From 1451fce1db96b95f0385b64058cda2cae1254693 Mon Sep 17 00:00:00 2001 From: Ilya Muradyan Date: Wed, 15 Nov 2023 17:32:55 +0100 Subject: [PATCH 2/2] Fix performance regression by switching to default renderer Signed-off-by: Ilya Muradyan --- .../org/jetbrains/kotlinx/dataframe/io/html.kt | 15 +++++++++------ .../org/jetbrains/kotlinx/dataframe/io/html.kt | 15 +++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt index 92d8fc5ad6..260eae32d3 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt @@ -18,6 +18,7 @@ import org.jetbrains.kotlinx.dataframe.impl.renderType import org.jetbrains.kotlinx.dataframe.impl.scale import org.jetbrains.kotlinx.dataframe.impl.truncate import org.jetbrains.kotlinx.dataframe.jupyter.CellRenderer +import org.jetbrains.kotlinx.dataframe.jupyter.DefaultCellRenderer import org.jetbrains.kotlinx.dataframe.jupyter.RenderedContent import org.jetbrains.kotlinx.dataframe.name import org.jetbrains.kotlinx.dataframe.nrow @@ -208,11 +209,13 @@ internal fun AnyFrame.toStaticHtml( } } + fun StringBuilder.emitCell(cellValue: Any?) = emitTag("td") { + append(cellRenderer.content(cellValue, configuration).truncatedContent) + } + fun StringBuilder.emitRow(row: AnyRow) = emitTag("tr") { columnsToRender.forEach { col -> - emitTag("td") { - append(cellRenderer.content(row[col.path()], configuration).truncatedContent) - } + emitCell(row[col.path()]) } } @@ -250,7 +253,7 @@ public fun DataFrame.html(): String = toStandaloneHTML().toString() */ public fun DataFrame.toStandaloneHTML( configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT, - cellRenderer: CellRenderer = org.jetbrains.kotlinx.dataframe.jupyter.DefaultCellRenderer, + cellRenderer: CellRenderer = DefaultCellRenderer, getFooter: (DataFrame) -> String? = { "DataFrame [${it.size}]" }, ): DataFrameHtmlData = toHTML(configuration, cellRenderer, getFooter).withTableDefinitions() @@ -259,7 +262,7 @@ public fun DataFrame.toStandaloneHTML( */ public fun DataFrame.toHTML( configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT, - cellRenderer: CellRenderer = org.jetbrains.kotlinx.dataframe.jupyter.DefaultCellRenderer, + cellRenderer: CellRenderer = DefaultCellRenderer, getFooter: (DataFrame) -> String? = { "DataFrame [${it.size}]" }, ): DataFrameHtmlData { val limit = configuration.rowsLimit ?: Int.MAX_VALUE @@ -280,7 +283,7 @@ public fun DataFrame.toHTML( var tableHtml = toHtmlData(configuration, cellRenderer) - tableHtml += toStaticHtml(configuration, cellRenderer) + tableHtml += toStaticHtml(configuration, DefaultCellRenderer) if (bodyFooter != null) { tableHtml += DataFrameHtmlData("", bodyFooter, "") diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt index 92d8fc5ad6..260eae32d3 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt @@ -18,6 +18,7 @@ import org.jetbrains.kotlinx.dataframe.impl.renderType import org.jetbrains.kotlinx.dataframe.impl.scale import org.jetbrains.kotlinx.dataframe.impl.truncate import org.jetbrains.kotlinx.dataframe.jupyter.CellRenderer +import org.jetbrains.kotlinx.dataframe.jupyter.DefaultCellRenderer import org.jetbrains.kotlinx.dataframe.jupyter.RenderedContent import org.jetbrains.kotlinx.dataframe.name import org.jetbrains.kotlinx.dataframe.nrow @@ -208,11 +209,13 @@ internal fun AnyFrame.toStaticHtml( } } + fun StringBuilder.emitCell(cellValue: Any?) = emitTag("td") { + append(cellRenderer.content(cellValue, configuration).truncatedContent) + } + fun StringBuilder.emitRow(row: AnyRow) = emitTag("tr") { columnsToRender.forEach { col -> - emitTag("td") { - append(cellRenderer.content(row[col.path()], configuration).truncatedContent) - } + emitCell(row[col.path()]) } } @@ -250,7 +253,7 @@ public fun DataFrame.html(): String = toStandaloneHTML().toString() */ public fun DataFrame.toStandaloneHTML( configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT, - cellRenderer: CellRenderer = org.jetbrains.kotlinx.dataframe.jupyter.DefaultCellRenderer, + cellRenderer: CellRenderer = DefaultCellRenderer, getFooter: (DataFrame) -> String? = { "DataFrame [${it.size}]" }, ): DataFrameHtmlData = toHTML(configuration, cellRenderer, getFooter).withTableDefinitions() @@ -259,7 +262,7 @@ public fun DataFrame.toStandaloneHTML( */ public fun DataFrame.toHTML( configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT, - cellRenderer: CellRenderer = org.jetbrains.kotlinx.dataframe.jupyter.DefaultCellRenderer, + cellRenderer: CellRenderer = DefaultCellRenderer, getFooter: (DataFrame) -> String? = { "DataFrame [${it.size}]" }, ): DataFrameHtmlData { val limit = configuration.rowsLimit ?: Int.MAX_VALUE @@ -280,7 +283,7 @@ public fun DataFrame.toHTML( var tableHtml = toHtmlData(configuration, cellRenderer) - tableHtml += toStaticHtml(configuration, cellRenderer) + tableHtml += toStaticHtml(configuration, DefaultCellRenderer) if (bodyFooter != null) { tableHtml += DataFrameHtmlData("", bodyFooter, "")