Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -27,7 +27,11 @@ internal class Integration(private val notebook: Notebook, options: MutableMap<S
private val lpJsVersion = VersionChecker.letsPlotJsVersion
private val isolatedFrame = options["isolatedFrame"] ?: ""

private val webOnly: Boolean = options["webOnly"].toBoolean()
// Output options
private val addWebOutput = (options["addWebOutput"] ?: "true").toBoolean()
private val addKTNBOutput = (options["addKTNBOutput"] ?: "true").toBoolean()
private val addStaticSvg = (options["addStaticSvg"] ?: "true").toBoolean()
private val addStaticPng = (options["addStaticPng"] ?: "false").toBoolean()

override fun Builder.onLoaded() {
import("org.jetbrains.letsPlot.*")
Expand Down Expand Up @@ -59,29 +63,43 @@ internal class Integration(private val notebook: Notebook, options: MutableMap<S
frontendContext = LetsPlot.setupNotebook(lpJsVersion, isolatedFrameParam) { display(HTML(it), null) }
LetsPlot.apiVersion = lpkVersion
// Load library JS
display(HTML(frontendContext.getConfigureHtml()), null)
if (addWebOutput) {
display(HTML(frontendContext.getConfigureHtml()), null)
}
// add figure renders AFTER frontendContext initialization
addRenders(lpJsVersion)
addRenders()
declare("letsPlotNotebookConfig" to config)
}
}


private fun Builder.addRenders(jsVersion: String) {
private fun Builder.addRenders() {
var firstFigureRendered = false
resources {
js("letsPlotJs") {
url(scriptUrl(jsVersion))

if (addWebOutput) {
resources {
js("letsPlotJs") {
url(scriptUrl(lpJsVersion))
}
}
}

renderWithHost<Figure> { host, value ->
// For cases when Integration is added via Kotlin Notebook project dependency;
// display configure HTML with the first `Figure` rendering
if (!firstFigureRendered) {
if (addWebOutput && !firstFigureRendered) {
firstFigureRendered = true
host.execute { display(HTML(frontendContext.getConfigureHtml()), null) }
}
NotebookRenderingContext(config, frontendContext, webOnly).figureToMimeResult(value)
NotebookRenderingContext(
config, frontendContext,
NotebookRenderingContext.OutputOptions(
addWebOutput = addWebOutput,
addKTNBOutput = addKTNBOutput,
addStaticSvg = addStaticSvg,
addStaticPng = addStaticPng
)
).figureToMimeResult(value)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,39 @@ import org.jetbrains.kotlinx.jupyter.api.MimeTypedResultEx
import org.jetbrains.kotlinx.jupyter.api.MimeTypes
import org.jetbrains.letsPlot.Figure
import org.jetbrains.letsPlot.awt.plot.PlotSvgExport
import org.jetbrains.letsPlot.core.plot.export.PlotImageExport
import org.jetbrains.letsPlot.frontend.NotebookFrontendContext
import org.jetbrains.letsPlot.intern.toSpec
import org.jetbrains.letsPlot.toolkit.jupyter.json.extendedByJson
import org.jetbrains.letsPlot.toolkit.json.serializeJsonMap
import org.jetbrains.letsPlot.toolkit.jupyter.json.extendedByJson
import java.util.*

internal class NotebookRenderingContext(
private val config: JupyterConfig,
private val frontendContext: NotebookFrontendContext,
private val webOnly: Boolean
private val outputOptions: OutputOptions
) {

data class OutputOptions(
val addWebOutput: Boolean,
val addKTNBOutput: Boolean,
val addStaticSvg: Boolean,
val addStaticPng: Boolean,
)

/**
* Creates Mime JSON with two output options - HTML and application/plot.
* The HTML output is used in Jupyter Notebooks and Datalore (the other one is ignored).
* The application/plot is used in Kotlin Notebook when native rendering via Swing is enabled.
*/
private fun figureToMimeJson(figure: Figure): JsonObject {
val spec = figure.toSpec()
val html = frontendContext.getDisplayHtml(figure.toSpec())
return buildJsonObject {
put(MimeTypes.HTML, JsonPrimitive(html))
if (!webOnly) {
if (outputOptions.addWebOutput) {
val plotHtml = frontendContext.getDisplayHtml(spec)
put(MimeTypes.HTML, JsonPrimitive(plotHtml))
}
if (outputOptions.addKTNBOutput) {
put("application/plot+json", buildJsonObject {
put("output_type", JsonPrimitive("lets_plot_spec"))
put("output", serializeJsonMap(spec))
Expand Down Expand Up @@ -66,19 +77,38 @@ internal class NotebookRenderingContext(
val svgSplit = split('\n')
(listOf(updateSvg(svgSplit.first(), id)) + svgSplit.drop(1)).joinToString("\n")
}
val extraHTML = """
val htmlWithSvg = """
$svgWithID
<script>document.getElementById("$id").style.display = "none";</script>
""".trimIndent()

return mapOf(MimeTypes.HTML to JsonPrimitive(extraHTML))
return mapOf(MimeTypes.HTML to JsonPrimitive(htmlWithSvg))
}

private fun figureToHiddenPng(figure: Figure): Map<String, JsonPrimitive> {
val base64 = Base64.getEncoder().encodeToString(
PlotImageExport.buildImageFromRawSpecs(
figure.toSpec(), PlotImageExport.Format.PNG
).bytes
)
val id = UUID.randomUUID().toString()
val htmlWithPng = """
<img id="$id" src="data:image/png;base64,$base64" alt="image">
<script>document.getElementById("$id").style.display = "none";</script>
""".trimIndent()

return mapOf(MimeTypes.HTML to JsonPrimitive(htmlWithPng))
}

fun figureToMimeResult(figure: Figure): MimeTypedResultEx {
val basicResult = figureToMimeJson(figure)
val extraSvg = figureToHiddenSvg(figure)
val mimeJson = figureToMimeJson(figure)
.let {
if (outputOptions.addStaticSvg) it.extendedByJson(figureToHiddenSvg(figure)) else it
}.let {
if (outputOptions.addStaticPng) it.extendedByJson(figureToHiddenPng(figure)) else it
}
return MimeTypedResultEx(
basicResult extendedByJson extraSvg,
mimeJson,
id = null,
metadataModifiers = emptyList()
)
Expand Down