Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve experience with HTML rendering of dataframes #300

Merged
merged 8 commits into from
Mar 31, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -98,7 +99,19 @@ public class FormattedFrame<T>(
internal val df: DataFrame<T>,
internal val formatter: RowColFormatter<T, *>? = 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<*, *>?)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down Expand Up @@ -116,7 +120,7 @@ internal fun nextTableId() = sessionId + (tableInSessionId++)
internal fun AnyFrame.toHtmlData(
configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT,
cellRenderer: CellRenderer,
): HtmlData {
): DataFrameHtmlData {
val scripts = mutableListOf<String>()
val queue = LinkedList<Pair<AnyFrame, Int>>()

Expand Down Expand Up @@ -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 <T> DataFrame<T>.html(): String = toStandaloneHTML().toString()

public fun <T> DataFrame<T>.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 <T> DataFrame<T>.toStandaloneHTML(
configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT,
cellRenderer: CellRenderer = org.jetbrains.kotlinx.dataframe.jupyter.DefaultCellRenderer,
getFooter: (DataFrame<T>) -> String = { "DataFrame [${it.size}]" },
): DataFrameHtmlData = toHTML(configuration, cellRenderer, getFooter).withTableDefinitions()

/**
* @return DataFrameHtmlData without additional definitions. Can be rendered in Jupyter kernel environments
*/
public fun <T> DataFrame<T>.toHTML(
configuration: DisplayConfiguration = DisplayConfiguration.DEFAULT,
extraHtml: HtmlData? = null,
cellRenderer: CellRenderer = org.jetbrains.kotlinx.dataframe.jupyter.DefaultCellRenderer,
getFooter: (DataFrame<T>) -> String = { "DataFrame [${it.size}]" },
): HtmlData {
): DataFrameHtmlData {
val limit = configuration.rowsLimit ?: Int.MAX_VALUE

val footer = getFooter(this)
Expand All @@ -204,11 +209,79 @@ public fun <T> DataFrame<T>.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 = """
<html>
<head>
<style type="text/css">
$style
</style>
</head>
<body>
$body
</body>
<script>
$script
</script>
</html>
""".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,
Expand Down Expand Up @@ -452,7 +525,7 @@ internal class DataFrameFormatter(
"</a>"
)

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -95,7 +94,14 @@ internal class Integration(
applyRowsLimit = false
)

render<HtmlData> { notebook.renderHtmlAsIFrameIfNeeded(it) }
render<DataFrameHtmlData> {
if (notebook.jupyterClientType == JupyterClientType.KOTLIN_NOTEBOOK) {
(it.withTableDefinitions().toJupyterHtmlData()).toIFrame(notebook.currentColorScheme)
} else {
it.toJupyterHtmlData().toSimpleHtml(notebook.currentColorScheme)
}
}

render<AnyRow>(
{ "DataRow: index = ${it.index()}, columnsCount = ${it.columnsCount()}" },
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -35,15 +35,15 @@ internal inline fun <reified T : Any> 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 {
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
}
Expand Down
Loading