diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt index d30de389a4..5e87764e05 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt @@ -12,9 +12,10 @@ import org.jetbrains.kotlinx.dataframe.impl.api.SingleAttribute import org.jetbrains.kotlinx.dataframe.impl.api.encode import org.jetbrains.kotlinx.dataframe.impl.api.formatImpl import org.jetbrains.kotlinx.dataframe.impl.api.linearGradient +import org.jetbrains.kotlinx.dataframe.io.DataFrameHtmlData import org.jetbrains.kotlinx.dataframe.io.DisplayConfiguration import org.jetbrains.kotlinx.dataframe.io.toHTML -import org.jetbrains.kotlinx.jupyter.api.HtmlData +import org.jetbrains.kotlinx.dataframe.io.toStandaloneHTML import kotlin.reflect.KProperty // region DataFrame @@ -98,7 +99,19 @@ public class FormattedFrame( internal val df: DataFrame, internal val formatter: RowColFormatter? = null, ) { - public fun toHTML(configuration: DisplayConfiguration): HtmlData = df.toHTML(getDisplayConfiguration(configuration)) + /** + * @return DataFrameHtmlData without additional definitions. Can be rendered in Jupyter kernel environments + */ + public fun toHTML(configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT): DataFrameHtmlData { + return df.toHTML(getDisplayConfiguration(configuration)) + } + + /** + * @return DataFrameHtmlData with table script and css definitions. Can be saved as an *.html file and displayed in the browser + */ + public fun toStandaloneHTML(configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT): DataFrameHtmlData { + return df.toStandaloneHTML(getDisplayConfiguration(configuration)) + } public fun getDisplayConfiguration(configuration: DisplayConfiguration): DisplayConfiguration { return configuration.copy(cellFormatter = formatter as RowColFormatter<*, *>?) 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 05f0e5d415..f543e76026 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 @@ -1,5 +1,6 @@ package org.jetbrains.kotlinx.dataframe.io +import org.intellij.lang.annotations.Language import org.jetbrains.kotlinx.dataframe.AnyCol import org.jetbrains.kotlinx.dataframe.AnyFrame import org.jetbrains.kotlinx.dataframe.AnyRow @@ -21,11 +22,14 @@ import org.jetbrains.kotlinx.dataframe.jupyter.RenderedContent import org.jetbrains.kotlinx.dataframe.name import org.jetbrains.kotlinx.dataframe.nrow import org.jetbrains.kotlinx.dataframe.size -import org.jetbrains.kotlinx.jupyter.api.HtmlData +import java.awt.Desktop +import java.io.File import java.io.InputStreamReader import java.net.URL +import java.nio.file.Path import java.util.LinkedList import java.util.Random +import kotlin.io.path.writeText internal val tooltipLimit = 1000 @@ -116,7 +120,7 @@ internal fun nextTableId() = sessionId + (tableInSessionId++) internal fun AnyFrame.toHtmlData( configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT, cellRenderer: CellRenderer, -): HtmlData { +): DataFrameHtmlData { val scripts = mutableListOf() val queue = LinkedList>() @@ -165,30 +169,31 @@ internal fun AnyFrame.toHtmlData( } val body = getResourceText("/table.html", "ID" to rootId) val script = scripts.joinToString("\n") + "\n" + getResourceText("/renderTable.js", "___ID___" to rootId) - return HtmlData("", body, script) + return DataFrameHtmlData("", body, script) } -internal fun HtmlData.print() = println(this) - -internal fun initHtml( - includeJs: Boolean = true, - includeCss: Boolean = true, - useDarkColorScheme: Boolean = false, -): HtmlData = - HtmlData( - style = if (includeCss) getResources("/table.css") else "", - script = if (includeJs) getResourceText("/init.js") else "", - body = "", - ) +internal fun DataFrameHtmlData.print() = println(this) + +@Deprecated("Clarify difference with .toHTML()", ReplaceWith("this.toStandaloneHTML().toString()", "org.jetbrains.kotlinx.dataframe.io.toStandaloneHTML")) +public fun DataFrame.html(): String = toStandaloneHTML().toString() -public fun DataFrame.html(): String = toHTML(extraHtml = initHtml()).toString() +/** + * @return DataFrameHtmlData with table script and css definitions. Can be saved as an *.html file and displayed in the browser + */ +public fun DataFrame.toStandaloneHTML( + configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT, + cellRenderer: CellRenderer = org.jetbrains.kotlinx.dataframe.jupyter.DefaultCellRenderer, + getFooter: (DataFrame) -> String = { "DataFrame [${it.size}]" }, +): DataFrameHtmlData = toHTML(configuration, cellRenderer, getFooter).withTableDefinitions() +/** + * @return DataFrameHtmlData without additional definitions. Can be rendered in Jupyter kernel environments + */ public fun DataFrame.toHTML( configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT, - extraHtml: HtmlData? = null, cellRenderer: CellRenderer = org.jetbrains.kotlinx.dataframe.jupyter.DefaultCellRenderer, getFooter: (DataFrame) -> String = { "DataFrame [${it.size}]" }, -): HtmlData { +): DataFrameHtmlData { val limit = configuration.rowsLimit ?: Int.MAX_VALUE val footer = getFooter(this) @@ -204,11 +209,79 @@ public fun DataFrame.toHTML( } val tableHtml = toHtmlData(configuration, cellRenderer) - val html = tableHtml + HtmlData("", bodyFooter, "") - return if (extraHtml != null) extraHtml + html else html + return tableHtml + DataFrameHtmlData("", bodyFooter, "") +} + +/** + * Container for HTML page data in form of String + * Can be used to compose rendered dataframe tables with additional HTML elements + */ +public data class DataFrameHtmlData(val style: String = "", val body: String = "", val script: String = "") { + @Language("html") + override fun toString(): String = """ + + + + + + $body + + + + """.trimIndent() + + public operator fun plus(other: DataFrameHtmlData): DataFrameHtmlData = + DataFrameHtmlData( + style + "\n" + other.style, + body + "\n" + other.body, + script + "\n" + other.script, + ) + + public fun writeHTML(destination: File) { + destination.writeText(toString()) + } + + public fun writeHTML(destination: Path) { + destination.writeText(toString()) + } + + public fun openInBrowser() { + val file = File.createTempFile("df_rendering", ".html") + writeHTML(file) + val uri = file.toURI() + val desktop = Desktop.getDesktop() + desktop.browse(uri) + } + + public fun withTableDefinitions(): DataFrameHtmlData = tableDefinitions() + this + + public companion object { + /** + * @return CSS and JS required to render DataFrame tables + * Can be used as a starting point to create page with multiple tables + * @see DataFrame.toHTML + * @see DataFrameHtmlData.plus + */ + public fun tableDefinitions( + includeJs: Boolean = true, + includeCss: Boolean = true, + ): DataFrameHtmlData = DataFrameHtmlData( + style = if (includeCss) getResources("/table.css") else "", + script = if (includeJs) getResourceText("/init.js") else "", + body = "", + ) + } } +/** + * @param rowsLimit null to disable rows limit + * @param cellContentLimit -1 to disable content trimming + */ public data class DisplayConfiguration( var rowsLimit: Int? = 20, var nestedRowsLimit: Int? = 5, @@ -452,7 +525,7 @@ internal class DataFrameFormatter( "" ) - is HtmlData -> RenderedContent.text(value.body) + is DataFrameHtmlData -> RenderedContent.text(value.body) else -> renderer.content(value, configuration) } if (result != null && result.textLength > configuration.cellContentLimit) return null diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/Integration.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/Integration.kt index 93e7b7902d..0203d5d32b 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/Integration.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/Integration.kt @@ -17,10 +17,10 @@ import org.jetbrains.kotlinx.dataframe.impl.codeGen.CodeGenerationReadResult import org.jetbrains.kotlinx.dataframe.impl.codeGen.urlCodeGenReader import org.jetbrains.kotlinx.dataframe.impl.createStarProjectedType import org.jetbrains.kotlinx.dataframe.impl.renderType +import org.jetbrains.kotlinx.dataframe.io.DataFrameHtmlData import org.jetbrains.kotlinx.dataframe.io.SupportedCodeGenerationFormat import org.jetbrains.kotlinx.dataframe.io.supportedFormats import org.jetbrains.kotlinx.jupyter.api.HTML -import org.jetbrains.kotlinx.jupyter.api.HtmlData import org.jetbrains.kotlinx.jupyter.api.JupyterClientType import org.jetbrains.kotlinx.jupyter.api.KotlinKernelHost import org.jetbrains.kotlinx.jupyter.api.Notebook @@ -29,7 +29,6 @@ import org.jetbrains.kotlinx.jupyter.api.declare import org.jetbrains.kotlinx.jupyter.api.libraries.ColorScheme import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration import org.jetbrains.kotlinx.jupyter.api.libraries.resources -import org.jetbrains.kotlinx.jupyter.api.renderHtmlAsIFrameIfNeeded import kotlin.reflect.KClass import kotlin.reflect.KProperty import kotlin.reflect.full.isSubtypeOf @@ -95,7 +94,16 @@ internal class Integration( applyRowsLimit = false ) - render { notebook.renderHtmlAsIFrameIfNeeded(it) } + render { + // Our integration declares script and css definition. But in Kotlin Notebook outputs are isolated in IFrames + // That's why we include them directly in the output + if (notebook.jupyterClientType == JupyterClientType.KOTLIN_NOTEBOOK) { + it.withTableDefinitions().toJupyterHtmlData().toIFrame(notebook.currentColorScheme) + } else { + it.toJupyterHtmlData().toSimpleHtml(notebook.currentColorScheme) + } + } + render( { "DataRow: index = ${it.index()}, columnsCount = ${it.columnsCount()}" }, ) diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/JupyterHtmlRenderer.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/JupyterHtmlRenderer.kt index 15cdec63c8..7add25c68e 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/JupyterHtmlRenderer.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/JupyterHtmlRenderer.kt @@ -4,10 +4,10 @@ import com.beust.klaxon.json import org.jetbrains.kotlinx.dataframe.api.rows import org.jetbrains.kotlinx.dataframe.api.toDataFrame import org.jetbrains.kotlinx.dataframe.io.* -import org.jetbrains.kotlinx.dataframe.io.initHtml import org.jetbrains.kotlinx.dataframe.nrow import org.jetbrains.kotlinx.dataframe.size import org.jetbrains.kotlinx.jupyter.api.* +import org.jetbrains.kotlinx.jupyter.api.HtmlData import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration /** Starting from this version, dataframe integration will respond with additional data for rendering in Kotlin Notebooks plugin. */ @@ -35,15 +35,15 @@ internal inline fun JupyterHtmlRenderer.render( df.nrow } - val html = df.toHTML( - reifiedDisplayConfiguration, - extraHtml = initHtml( + val html = ( + DataFrameHtmlData.tableDefinitions( includeJs = reifiedDisplayConfiguration.isolatedOutputs, - includeCss = true, - useDarkColorScheme = reifiedDisplayConfiguration.useDarkColorScheme - ), - contextRenderer - ) { footer } + includeCss = true + ) + df.toHTML( + reifiedDisplayConfiguration, + contextRenderer + ) { footer } + ).toJupyterHtmlData() if (notebook.kernelVersion >= KotlinKernelVersion.from(MIN_KERNEL_VERSION_FOR_NEW_TABLES_UI)!!) { val jsonEncodedDf = json { @@ -72,3 +72,5 @@ internal fun Notebook.renderAsIFrameAsNeeded(data: HtmlData, jsonEncodedDf: Stri "application/kotlindataframe+json" to jsonEncodedDf ).also { it.isolatedHtml = false } } + +internal fun DataFrameHtmlData.toJupyterHtmlData() = HtmlData(style, body, script) diff --git a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/html/Utils.kt b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/html/Utils.kt index 7497d29e09..27a07d068b 100644 --- a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/html/Utils.kt +++ b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/html/Utils.kt @@ -1,15 +1,8 @@ package org.jetbrains.kotlinx.dataframe.rendering.html import org.jetbrains.kotlinx.dataframe.AnyFrame -import org.jetbrains.kotlinx.dataframe.io.initHtml -import org.jetbrains.kotlinx.dataframe.io.toHTML -import java.awt.Desktop -import java.io.File +import org.jetbrains.kotlinx.dataframe.io.toStandaloneHTML fun AnyFrame.browse() { - val file = File("temp.html") // File.createTempFile("df_rendering", ".html") - file.writeText(toHTML(extraHtml = initHtml()).toString()) - val uri = file.toURI() - val desktop = Desktop.getDesktop() - desktop.browse(uri) + toStandaloneHTML().openInBrowser() } diff --git a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Render.kt b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Render.kt new file mode 100644 index 0000000000..3b9eb3d0a7 --- /dev/null +++ b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Render.kt @@ -0,0 +1,43 @@ +package org.jetbrains.kotlinx.dataframe.samples.api + +import org.jetbrains.kotlinx.dataframe.api.reorderColumnsByName +import org.jetbrains.kotlinx.dataframe.api.sortBy +import org.jetbrains.kotlinx.dataframe.api.sortByDesc +import org.jetbrains.kotlinx.dataframe.io.DataFrameHtmlData +import org.jetbrains.kotlinx.dataframe.io.DisplayConfiguration +import org.jetbrains.kotlinx.dataframe.io.toHTML +import org.jetbrains.kotlinx.dataframe.io.toStandaloneHTML +import org.junit.Ignore +import org.junit.Test +import java.io.File +import kotlin.io.path.Path + +class Render : TestBase() { + @Test + @Ignore + fun useRenderingResult() { + // SampleStart + df.toStandaloneHTML(DisplayConfiguration(rowsLimit = null)).openInBrowser() + df.toStandaloneHTML(DisplayConfiguration(rowsLimit = null)).writeHTML(File("/path/to/file")) + df.toStandaloneHTML(DisplayConfiguration(rowsLimit = null)).writeHTML(Path("/path/to/file")) + // SampleEnd + } + + @Test + fun composeTables() { + // SampleStart + val df1 = df.reorderColumnsByName() + val df2 = df.sortBy { age } + val df3 = df.sortByDesc { age } + + listOf(df1, df2, df3).fold(DataFrameHtmlData.tableDefinitions()) { acc, df -> acc + df.toHTML() } + // SampleEnd + } + + @Test + fun configureCellOutput() { + // SampleStart + df.toHTML(DisplayConfiguration(cellContentLimit = -1)) + // SampleEnd + } +} diff --git a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/HtmlRenderingTests.kt b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/HtmlRenderingTests.kt index ff55301011..90afe78d75 100644 --- a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/HtmlRenderingTests.kt +++ b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/HtmlRenderingTests.kt @@ -7,9 +7,7 @@ import org.jetbrains.kotlinx.dataframe.api.dataFrameOf import org.jetbrains.kotlinx.dataframe.api.group import org.jetbrains.kotlinx.dataframe.api.into import org.jetbrains.kotlinx.dataframe.api.parse -import org.jetbrains.kotlinx.dataframe.io.html -import org.jetbrains.kotlinx.dataframe.io.initHtml -import org.jetbrains.kotlinx.dataframe.io.toHTML +import org.jetbrains.kotlinx.dataframe.io.toStandaloneHTML import org.jetbrains.kotlinx.jupyter.findNthSubstring import org.junit.Ignore import org.junit.Test @@ -20,7 +18,7 @@ class HtmlRenderingTests : BaseTest() { fun AnyFrame.browse() { val file = File("temp.html") // File.createTempFile("df_rendering", ".html") - file.writeText(toHTML(extraHtml = initHtml()).toString()) + file.writeText(toStandaloneHTML().toString()) val uri = file.toURI() val desktop = Desktop.getDesktop() desktop.browse(uri) @@ -36,7 +34,7 @@ class HtmlRenderingTests : BaseTest() { fun `render url`() { val address = "http://www.google.com" val df = dataFrameOf("url")(address).parse() - val html = df.html() + val html = df.toStandaloneHTML().toString() html shouldContain "href" html.findNthSubstring(address, 2) shouldNotBe -1 } diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt index d30de389a4..5e87764e05 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/format.kt @@ -12,9 +12,10 @@ import org.jetbrains.kotlinx.dataframe.impl.api.SingleAttribute import org.jetbrains.kotlinx.dataframe.impl.api.encode import org.jetbrains.kotlinx.dataframe.impl.api.formatImpl import org.jetbrains.kotlinx.dataframe.impl.api.linearGradient +import org.jetbrains.kotlinx.dataframe.io.DataFrameHtmlData import org.jetbrains.kotlinx.dataframe.io.DisplayConfiguration import org.jetbrains.kotlinx.dataframe.io.toHTML -import org.jetbrains.kotlinx.jupyter.api.HtmlData +import org.jetbrains.kotlinx.dataframe.io.toStandaloneHTML import kotlin.reflect.KProperty // region DataFrame @@ -98,7 +99,19 @@ public class FormattedFrame( internal val df: DataFrame, internal val formatter: RowColFormatter? = null, ) { - public fun toHTML(configuration: DisplayConfiguration): HtmlData = df.toHTML(getDisplayConfiguration(configuration)) + /** + * @return DataFrameHtmlData without additional definitions. Can be rendered in Jupyter kernel environments + */ + public fun toHTML(configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT): DataFrameHtmlData { + return df.toHTML(getDisplayConfiguration(configuration)) + } + + /** + * @return DataFrameHtmlData with table script and css definitions. Can be saved as an *.html file and displayed in the browser + */ + public fun toStandaloneHTML(configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT): DataFrameHtmlData { + return df.toStandaloneHTML(getDisplayConfiguration(configuration)) + } public fun getDisplayConfiguration(configuration: DisplayConfiguration): DisplayConfiguration { return configuration.copy(cellFormatter = formatter as RowColFormatter<*, *>?) 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 05f0e5d415..f543e76026 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 @@ -1,5 +1,6 @@ package org.jetbrains.kotlinx.dataframe.io +import org.intellij.lang.annotations.Language import org.jetbrains.kotlinx.dataframe.AnyCol import org.jetbrains.kotlinx.dataframe.AnyFrame import org.jetbrains.kotlinx.dataframe.AnyRow @@ -21,11 +22,14 @@ import org.jetbrains.kotlinx.dataframe.jupyter.RenderedContent import org.jetbrains.kotlinx.dataframe.name import org.jetbrains.kotlinx.dataframe.nrow import org.jetbrains.kotlinx.dataframe.size -import org.jetbrains.kotlinx.jupyter.api.HtmlData +import java.awt.Desktop +import java.io.File import java.io.InputStreamReader import java.net.URL +import java.nio.file.Path import java.util.LinkedList import java.util.Random +import kotlin.io.path.writeText internal val tooltipLimit = 1000 @@ -116,7 +120,7 @@ internal fun nextTableId() = sessionId + (tableInSessionId++) internal fun AnyFrame.toHtmlData( configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT, cellRenderer: CellRenderer, -): HtmlData { +): DataFrameHtmlData { val scripts = mutableListOf() val queue = LinkedList>() @@ -165,30 +169,31 @@ internal fun AnyFrame.toHtmlData( } val body = getResourceText("/table.html", "ID" to rootId) val script = scripts.joinToString("\n") + "\n" + getResourceText("/renderTable.js", "___ID___" to rootId) - return HtmlData("", body, script) + return DataFrameHtmlData("", body, script) } -internal fun HtmlData.print() = println(this) - -internal fun initHtml( - includeJs: Boolean = true, - includeCss: Boolean = true, - useDarkColorScheme: Boolean = false, -): HtmlData = - HtmlData( - style = if (includeCss) getResources("/table.css") else "", - script = if (includeJs) getResourceText("/init.js") else "", - body = "", - ) +internal fun DataFrameHtmlData.print() = println(this) + +@Deprecated("Clarify difference with .toHTML()", ReplaceWith("this.toStandaloneHTML().toString()", "org.jetbrains.kotlinx.dataframe.io.toStandaloneHTML")) +public fun DataFrame.html(): String = toStandaloneHTML().toString() -public fun DataFrame.html(): String = toHTML(extraHtml = initHtml()).toString() +/** + * @return DataFrameHtmlData with table script and css definitions. Can be saved as an *.html file and displayed in the browser + */ +public fun DataFrame.toStandaloneHTML( + configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT, + cellRenderer: CellRenderer = org.jetbrains.kotlinx.dataframe.jupyter.DefaultCellRenderer, + getFooter: (DataFrame) -> String = { "DataFrame [${it.size}]" }, +): DataFrameHtmlData = toHTML(configuration, cellRenderer, getFooter).withTableDefinitions() +/** + * @return DataFrameHtmlData without additional definitions. Can be rendered in Jupyter kernel environments + */ public fun DataFrame.toHTML( configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT, - extraHtml: HtmlData? = null, cellRenderer: CellRenderer = org.jetbrains.kotlinx.dataframe.jupyter.DefaultCellRenderer, getFooter: (DataFrame) -> String = { "DataFrame [${it.size}]" }, -): HtmlData { +): DataFrameHtmlData { val limit = configuration.rowsLimit ?: Int.MAX_VALUE val footer = getFooter(this) @@ -204,11 +209,79 @@ public fun DataFrame.toHTML( } val tableHtml = toHtmlData(configuration, cellRenderer) - val html = tableHtml + HtmlData("", bodyFooter, "") - return if (extraHtml != null) extraHtml + html else html + return tableHtml + DataFrameHtmlData("", bodyFooter, "") +} + +/** + * Container for HTML page data in form of String + * Can be used to compose rendered dataframe tables with additional HTML elements + */ +public data class DataFrameHtmlData(val style: String = "", val body: String = "", val script: String = "") { + @Language("html") + override fun toString(): String = """ + + + + + + $body + + + + """.trimIndent() + + public operator fun plus(other: DataFrameHtmlData): DataFrameHtmlData = + DataFrameHtmlData( + style + "\n" + other.style, + body + "\n" + other.body, + script + "\n" + other.script, + ) + + public fun writeHTML(destination: File) { + destination.writeText(toString()) + } + + public fun writeHTML(destination: Path) { + destination.writeText(toString()) + } + + public fun openInBrowser() { + val file = File.createTempFile("df_rendering", ".html") + writeHTML(file) + val uri = file.toURI() + val desktop = Desktop.getDesktop() + desktop.browse(uri) + } + + public fun withTableDefinitions(): DataFrameHtmlData = tableDefinitions() + this + + public companion object { + /** + * @return CSS and JS required to render DataFrame tables + * Can be used as a starting point to create page with multiple tables + * @see DataFrame.toHTML + * @see DataFrameHtmlData.plus + */ + public fun tableDefinitions( + includeJs: Boolean = true, + includeCss: Boolean = true, + ): DataFrameHtmlData = DataFrameHtmlData( + style = if (includeCss) getResources("/table.css") else "", + script = if (includeJs) getResourceText("/init.js") else "", + body = "", + ) + } } +/** + * @param rowsLimit null to disable rows limit + * @param cellContentLimit -1 to disable content trimming + */ public data class DisplayConfiguration( var rowsLimit: Int? = 20, var nestedRowsLimit: Int? = 5, @@ -452,7 +525,7 @@ internal class DataFrameFormatter( "" ) - is HtmlData -> RenderedContent.text(value.body) + is DataFrameHtmlData -> RenderedContent.text(value.body) else -> renderer.content(value, configuration) } if (result != null && result.textLength > configuration.cellContentLimit) return null diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/Integration.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/Integration.kt index 93e7b7902d..0203d5d32b 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/Integration.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/Integration.kt @@ -17,10 +17,10 @@ import org.jetbrains.kotlinx.dataframe.impl.codeGen.CodeGenerationReadResult import org.jetbrains.kotlinx.dataframe.impl.codeGen.urlCodeGenReader import org.jetbrains.kotlinx.dataframe.impl.createStarProjectedType import org.jetbrains.kotlinx.dataframe.impl.renderType +import org.jetbrains.kotlinx.dataframe.io.DataFrameHtmlData import org.jetbrains.kotlinx.dataframe.io.SupportedCodeGenerationFormat import org.jetbrains.kotlinx.dataframe.io.supportedFormats import org.jetbrains.kotlinx.jupyter.api.HTML -import org.jetbrains.kotlinx.jupyter.api.HtmlData import org.jetbrains.kotlinx.jupyter.api.JupyterClientType import org.jetbrains.kotlinx.jupyter.api.KotlinKernelHost import org.jetbrains.kotlinx.jupyter.api.Notebook @@ -29,7 +29,6 @@ import org.jetbrains.kotlinx.jupyter.api.declare import org.jetbrains.kotlinx.jupyter.api.libraries.ColorScheme import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration import org.jetbrains.kotlinx.jupyter.api.libraries.resources -import org.jetbrains.kotlinx.jupyter.api.renderHtmlAsIFrameIfNeeded import kotlin.reflect.KClass import kotlin.reflect.KProperty import kotlin.reflect.full.isSubtypeOf @@ -95,7 +94,16 @@ internal class Integration( applyRowsLimit = false ) - render { notebook.renderHtmlAsIFrameIfNeeded(it) } + render { + // Our integration declares script and css definition. But in Kotlin Notebook outputs are isolated in IFrames + // That's why we include them directly in the output + if (notebook.jupyterClientType == JupyterClientType.KOTLIN_NOTEBOOK) { + it.withTableDefinitions().toJupyterHtmlData().toIFrame(notebook.currentColorScheme) + } else { + it.toJupyterHtmlData().toSimpleHtml(notebook.currentColorScheme) + } + } + render( { "DataRow: index = ${it.index()}, columnsCount = ${it.columnsCount()}" }, ) diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/JupyterHtmlRenderer.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/JupyterHtmlRenderer.kt index 15cdec63c8..7add25c68e 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/JupyterHtmlRenderer.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/JupyterHtmlRenderer.kt @@ -4,10 +4,10 @@ import com.beust.klaxon.json import org.jetbrains.kotlinx.dataframe.api.rows import org.jetbrains.kotlinx.dataframe.api.toDataFrame import org.jetbrains.kotlinx.dataframe.io.* -import org.jetbrains.kotlinx.dataframe.io.initHtml import org.jetbrains.kotlinx.dataframe.nrow import org.jetbrains.kotlinx.dataframe.size import org.jetbrains.kotlinx.jupyter.api.* +import org.jetbrains.kotlinx.jupyter.api.HtmlData import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration /** Starting from this version, dataframe integration will respond with additional data for rendering in Kotlin Notebooks plugin. */ @@ -35,15 +35,15 @@ internal inline fun JupyterHtmlRenderer.render( df.nrow } - val html = df.toHTML( - reifiedDisplayConfiguration, - extraHtml = initHtml( + val html = ( + DataFrameHtmlData.tableDefinitions( includeJs = reifiedDisplayConfiguration.isolatedOutputs, - includeCss = true, - useDarkColorScheme = reifiedDisplayConfiguration.useDarkColorScheme - ), - contextRenderer - ) { footer } + includeCss = true + ) + df.toHTML( + reifiedDisplayConfiguration, + contextRenderer + ) { footer } + ).toJupyterHtmlData() if (notebook.kernelVersion >= KotlinKernelVersion.from(MIN_KERNEL_VERSION_FOR_NEW_TABLES_UI)!!) { val jsonEncodedDf = json { @@ -72,3 +72,5 @@ internal fun Notebook.renderAsIFrameAsNeeded(data: HtmlData, jsonEncodedDf: Stri "application/kotlindataframe+json" to jsonEncodedDf ).also { it.isolatedHtml = false } } + +internal fun DataFrameHtmlData.toJupyterHtmlData() = HtmlData(style, body, script) diff --git a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/html/Utils.kt b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/html/Utils.kt index 7497d29e09..27a07d068b 100644 --- a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/html/Utils.kt +++ b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/html/Utils.kt @@ -1,15 +1,8 @@ package org.jetbrains.kotlinx.dataframe.rendering.html import org.jetbrains.kotlinx.dataframe.AnyFrame -import org.jetbrains.kotlinx.dataframe.io.initHtml -import org.jetbrains.kotlinx.dataframe.io.toHTML -import java.awt.Desktop -import java.io.File +import org.jetbrains.kotlinx.dataframe.io.toStandaloneHTML fun AnyFrame.browse() { - val file = File("temp.html") // File.createTempFile("df_rendering", ".html") - file.writeText(toHTML(extraHtml = initHtml()).toString()) - val uri = file.toURI() - val desktop = Desktop.getDesktop() - desktop.browse(uri) + toStandaloneHTML().openInBrowser() } diff --git a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Render.kt b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Render.kt new file mode 100644 index 0000000000..3b9eb3d0a7 --- /dev/null +++ b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Render.kt @@ -0,0 +1,43 @@ +package org.jetbrains.kotlinx.dataframe.samples.api + +import org.jetbrains.kotlinx.dataframe.api.reorderColumnsByName +import org.jetbrains.kotlinx.dataframe.api.sortBy +import org.jetbrains.kotlinx.dataframe.api.sortByDesc +import org.jetbrains.kotlinx.dataframe.io.DataFrameHtmlData +import org.jetbrains.kotlinx.dataframe.io.DisplayConfiguration +import org.jetbrains.kotlinx.dataframe.io.toHTML +import org.jetbrains.kotlinx.dataframe.io.toStandaloneHTML +import org.junit.Ignore +import org.junit.Test +import java.io.File +import kotlin.io.path.Path + +class Render : TestBase() { + @Test + @Ignore + fun useRenderingResult() { + // SampleStart + df.toStandaloneHTML(DisplayConfiguration(rowsLimit = null)).openInBrowser() + df.toStandaloneHTML(DisplayConfiguration(rowsLimit = null)).writeHTML(File("/path/to/file")) + df.toStandaloneHTML(DisplayConfiguration(rowsLimit = null)).writeHTML(Path("/path/to/file")) + // SampleEnd + } + + @Test + fun composeTables() { + // SampleStart + val df1 = df.reorderColumnsByName() + val df2 = df.sortBy { age } + val df3 = df.sortByDesc { age } + + listOf(df1, df2, df3).fold(DataFrameHtmlData.tableDefinitions()) { acc, df -> acc + df.toHTML() } + // SampleEnd + } + + @Test + fun configureCellOutput() { + // SampleStart + df.toHTML(DisplayConfiguration(cellContentLimit = -1)) + // SampleEnd + } +} diff --git a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/HtmlRenderingTests.kt b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/HtmlRenderingTests.kt index ff55301011..90afe78d75 100644 --- a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/HtmlRenderingTests.kt +++ b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/HtmlRenderingTests.kt @@ -7,9 +7,7 @@ import org.jetbrains.kotlinx.dataframe.api.dataFrameOf import org.jetbrains.kotlinx.dataframe.api.group import org.jetbrains.kotlinx.dataframe.api.into import org.jetbrains.kotlinx.dataframe.api.parse -import org.jetbrains.kotlinx.dataframe.io.html -import org.jetbrains.kotlinx.dataframe.io.initHtml -import org.jetbrains.kotlinx.dataframe.io.toHTML +import org.jetbrains.kotlinx.dataframe.io.toStandaloneHTML import org.jetbrains.kotlinx.jupyter.findNthSubstring import org.junit.Ignore import org.junit.Test @@ -20,7 +18,7 @@ class HtmlRenderingTests : BaseTest() { fun AnyFrame.browse() { val file = File("temp.html") // File.createTempFile("df_rendering", ".html") - file.writeText(toHTML(extraHtml = initHtml()).toString()) + file.writeText(toStandaloneHTML().toString()) val uri = file.toURI() val desktop = Desktop.getDesktop() desktop.browse(uri) @@ -36,7 +34,7 @@ class HtmlRenderingTests : BaseTest() { fun `render url`() { val address = "http://www.google.com" val df = dataFrameOf("url")(address).parse() - val html = df.html() + val html = df.toStandaloneHTML().toString() html shouldContain "href" html.findNthSubstring(address, 2) shouldNotBe -1 } diff --git a/docs/StardustDocs/d.tree b/docs/StardustDocs/d.tree index 0b78581596..fd14d48e04 100644 --- a/docs/StardustDocs/d.tree +++ b/docs/StardustDocs/d.tree @@ -169,7 +169,9 @@ + + diff --git a/docs/StardustDocs/topics/jupyterRendering.md b/docs/StardustDocs/topics/jupyterRendering.md new file mode 100644 index 0000000000..9d128b3a96 --- /dev/null +++ b/docs/StardustDocs/topics/jupyterRendering.md @@ -0,0 +1,13 @@ +[//]: # (title: Jupyter Notebooks) + +Rendering in Jupyter Notebooks can be configured using `dataFrameConfig.display` value. +Have a look at [toHTML](toHTML.md#configuring-display-for-individual-output) function to configure output for single cell + +### Content limit length + +Content in each cell gets truncated to 40 characters by default. +This can be changed by setting `cellContentLimit` to a different value on the display configuration. + +```kotlin +dataFrameConfig.display.cellContentLimit = 100 +``` diff --git a/docs/StardustDocs/topics/rendering.md b/docs/StardustDocs/topics/rendering.md index ffad0bdb64..4c536081b3 100644 --- a/docs/StardustDocs/topics/rendering.md +++ b/docs/StardustDocs/topics/rendering.md @@ -1,16 +1,6 @@ [//]: # (title: Rendering) -// TODO +This section describes APIs that you can use to render DataFrame types and configure display. -## Jupyter Notebooks - -Rendering in Jupyter Notebooks can be configured using `dataFrameConfig.display` value. - -### Content limit length - -Content in each cell gets truncated to 40 characters by default. -This can be changed by setting `cellContentLimit` to a different value on the display configuration. - -```kotlin -dataFrameConfig.display.cellContentLimit = 100 -``` +* [`toHTML`](toHTML.md) — operation for rendering DataFrame object to an HTML table +* [`Jupyter Notebooks`](jupyterRendering.md) — configuration specific to notebook environments diff --git a/docs/StardustDocs/topics/toHTML.md b/docs/StardustDocs/topics/toHTML.md new file mode 100644 index 0000000000..78abf8d1ff --- /dev/null +++ b/docs/StardustDocs/topics/toHTML.md @@ -0,0 +1,57 @@ +[//]: # (title: toHTML) + + + +DataFrame can be rendered to HTML. +Rendering of hierarchical tables in HTML is supported by JS and CSS definitions +that can be found in project resources. + +Depending on your environment there can be different ways to use result of `toHTML` functions + +## IntelliJ IDEA + +### Working with result + +The following function produces HTML that includes JS and CSS definitions. It can be displayed in the browser and has parameters for customization. + + + +```kotlin +df.toStandaloneHTML(DisplayConfiguration(rowsLimit = null)).openInBrowser() +df.toStandaloneHTML(DisplayConfiguration(rowsLimit = null)).writeHTML(File("/path/to/file")) +df.toStandaloneHTML(DisplayConfiguration(rowsLimit = null)).writeHTML(Path("/path/to/file")) +``` + + + +### Composing multiple tables + +`toHTML` and `toStandaloneHTML` return composable `DataFrameHtmlData`. You can use it to include additional scripts, elements, styles on final page or just merge together multiple tables. + + + +```kotlin +val df1 = df.reorderColumnsByName() +val df2 = df.sortBy { age } +val df3 = df.sortByDesc { age } + +listOf(df1, df2, df3).fold(DataFrameHtmlData.tableDefinitions()) { acc, df -> acc + df.toHTML() } +``` + + + +## Jupyter Notebooks + +### Configuring display for individual output + +`toHTML` is useful if you want to configure how a single cell is displayed. To configure the display for the entire notebook, please refer to [Jupyter Notebooks](jupyterRendering.md) section. + + + +```kotlin +df.toHTML(DisplayConfiguration(cellContentLimit = -1)) +``` + + + +