-
Notifications
You must be signed in to change notification settings - Fork 41
Add jupyter integration for lets-plot-kotlin and lets-plot-kotlin-geotools #258
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
Merged
Merged
Changes from 1 commit
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
9fadc51
Agg jupyter integration for lets-plot-kotlin and lets-plot-kotlin-geo…
AndreiKingsley 831de94
Extract integrations to separate modules and artifacts with all LP/LP…
AndreiKingsley 7599b93
little code clean up
AndreiKingsley bda81dd
rename util to json & remove lets-plot mention
AndreiKingsley 5da00aa
Merge branch 'master' into jupyter_integration
AndreiKingsley 90ea70f
move serialization version
AndreiKingsley File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
98 changes: 98 additions & 0 deletions
98
plot-api/src/jvmMain/kotlin/org/jetbrains/letsPlot/jupyter/Integration.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| package org.jetbrains.letsPlot.jupyter | ||
|
|
||
| import org.jetbrains.kotlinx.jupyter.api.* | ||
| import org.jetbrains.kotlinx.jupyter.api.libraries.ExecutionHost | ||
| import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration | ||
| import org.jetbrains.kotlinx.jupyter.api.libraries.resources | ||
| import org.jetbrains.letsPlot.Figure | ||
| import org.jetbrains.letsPlot.LetsPlot | ||
| import org.jetbrains.letsPlot.core.util.PlotHtmlHelper.scriptUrl | ||
| import org.jetbrains.letsPlot.export.VersionChecker | ||
| import org.jetbrains.letsPlot.frontend.NotebookFrontendContext | ||
|
|
||
| @Suppress("unused") | ||
| internal class Integration(private val notebook: Notebook, private val options: MutableMap<String, String?>) : | ||
| JupyterIntegration() { | ||
|
|
||
| private val config = JupyterConfig() | ||
| private lateinit var frontendContext: NotebookFrontendContext | ||
|
|
||
|
|
||
| override fun Builder.onLoaded() { | ||
|
|
||
| import("org.jetbrains.letsPlot.*") | ||
| import("org.jetbrains.letsPlot.geom.*") | ||
| import("org.jetbrains.letsPlot.geom.extras.*") | ||
| import("org.jetbrains.letsPlot.stat.*") | ||
| import("org.jetbrains.letsPlot.label.*") | ||
| import("org.jetbrains.letsPlot.scale.*") | ||
| import("org.jetbrains.letsPlot.facet.*") | ||
| import("org.jetbrains.letsPlot.sampling.*") | ||
| import("org.jetbrains.letsPlot.export.*") | ||
| import("org.jetbrains.letsPlot.tooltips.*") | ||
| import("org.jetbrains.letsPlot.annotations.*") | ||
| import("org.jetbrains.letsPlot.themes.*") | ||
| import("org.jetbrains.letsPlot.font.*") | ||
| import("org.jetbrains.letsPlot.coord.*") | ||
| import("org.jetbrains.letsPlot.pos.*") | ||
| import("org.jetbrains.letsPlot.bistro.corr.*") | ||
| import("org.jetbrains.letsPlot.bistro.qq.*") | ||
| import("org.jetbrains.letsPlot.bistro.joint.*") | ||
| import("org.jetbrains.letsPlot.bistro.residual.*") | ||
| import("org.jetbrains.letsPlot.bistro.waterfall.*") | ||
| import("org.jetbrains.letsPlot.intern.toSpec") | ||
| import("org.jetbrains.letsPlot.spatial.SpatialDataset") | ||
|
|
||
|
|
||
| onLoaded { | ||
| // Take integration options from descriptor by default; | ||
| // If used via Kotlin Notebook plugin as a dependency, | ||
| // provide defaults (versions from `VersionChecker` | ||
| // and empty `isolatedFrame`) | ||
| val api = options["api"] ?: VersionChecker.letsPlotKotlinAPIVersion | ||
| val js = options["js"] ?: VersionChecker.letsPlotJsVersion | ||
| val isolatedFrame = options["isolatedFrame"] ?: "" | ||
| val isolatedFrameParam = if (isolatedFrame.isNotEmpty()) isolatedFrame.toBoolean() else null | ||
| frontendContext = LetsPlot.setupNotebook(js, isolatedFrameParam) { display(HTML(it), null) } | ||
| LetsPlot.apiVersion = api | ||
| // Load library JS | ||
| display(HTML(frontendContext.getConfigureHtml()), null) | ||
| // add figure renders AFTER frontendContext initialization | ||
| addRenders(js) | ||
| declare("letsPlotNotebookConfig" to config) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| private fun Builder.addRenders(jsVersion: String) { | ||
| var firstFigureRendered = false | ||
| resources { | ||
| js("letsPlotJs") { | ||
| url(scriptUrl(jsVersion)) | ||
| } | ||
| } | ||
| renderWithHostTemp<Figure> { host, value -> | ||
| // For cases when Integration is added via Kotlin Notebook project dependency; | ||
| // display configure HTML with the first `Figure rendering | ||
| if (!firstFigureRendered) { | ||
| firstFigureRendered = true | ||
| host.execute { display(HTML(frontendContext.getConfigureHtml()), null) } | ||
| } | ||
| NotebookRenderingContext(config, frontendContext).figureToMimeResult(value) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| // copy-pasted from jupyter api; | ||
| // it is not possible to use `renderWithHost` directly because it is inline and built with JVM 11 | ||
| private inline fun <reified T : Any> Builder.renderWithHostTemp(noinline renderer: CodeCell.(ExecutionHost, T) -> Any) { | ||
| val execution = | ||
| ResultHandlerExecution { host, property -> | ||
| val currentCell = notebook.currentCell | ||
| ?: throw IllegalStateException("Current cell should not be null on renderer invocation") | ||
| FieldValue(renderer(currentCell, host, property.value as T), null) | ||
| } | ||
| addRenderer(SubtypeRendererTypeHandler(T::class, execution)) | ||
| } | ||
|
|
||
| } |
21 changes: 21 additions & 0 deletions
21
plot-api/src/jvmMain/kotlin/org/jetbrains/letsPlot/jupyter/JupyterConfig.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package org.jetbrains.letsPlot.jupyter | ||
|
|
||
| /** | ||
| * Lets-Plot config for Jupyter. | ||
| */ | ||
| class JupyterConfig { | ||
| /** | ||
| * Is IDEA theme applied to plots in the notebook outputs rendered with Swing. | ||
| * | ||
| * Enabled by default. | ||
| */ | ||
| var themeApplied: Boolean = true | ||
|
|
||
| /** | ||
| * Is Java Swing applied to plots in the notebook outputs. | ||
| * Otherwise, web (HTML + JS) rendering is used. | ||
| * | ||
| * Enabled by default. | ||
| */ | ||
| var swingEnabled: Boolean = true | ||
| } |
81 changes: 81 additions & 0 deletions
81
plot-api/src/jvmMain/kotlin/org/jetbrains/letsPlot/jupyter/NotebookRenderingContext.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| package org.jetbrains.letsPlot.jupyter | ||
|
|
||
| import kotlinx.serialization.json.JsonObject | ||
| import kotlinx.serialization.json.JsonPrimitive | ||
| import kotlinx.serialization.json.buildJsonObject | ||
| 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.frontend.NotebookFrontendContext | ||
| import org.jetbrains.letsPlot.intern.toSpec | ||
| import java.util.* | ||
|
|
||
| internal class NotebookRenderingContext( | ||
| private val config: JupyterConfig, | ||
| private val frontendContext: NotebookFrontendContext | ||
| ) { | ||
| /** | ||
| * 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.getHtml(figure) | ||
| return buildJsonObject { | ||
| put(MimeTypes.HTML, JsonPrimitive(html)) | ||
| put("application/plot+json", buildJsonObject { | ||
| put("output_type", JsonPrimitive("lets_plot_spec")) | ||
| put("output", serializeSpec(spec)) | ||
| put("apply_color_scheme", JsonPrimitive(config.themeApplied)) | ||
| put("swing_enabled", JsonPrimitive(config.swingEnabled)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Modifies an SVG by adding a new identifier and setting the width and height of the SVG to 100%, | ||
| * while retaining the original dimensions in the style for responsiveness. | ||
| * It also adds viewBox and preserveAspectRatio attributes to ensure the SVG scales properly without distortion. | ||
| * Updates SVG id with provided one. | ||
| */ | ||
| private fun updateSvg(svgString: String, id: String): String { | ||
| val regex = Regex("""width=["']([^"']*)["']\s*height=["']([^"']*)["']""") | ||
| return regex.replace(svgString) { | ||
| val currentWidth = it.groupValues[1] | ||
| val currentHeight = it.groupValues[2] | ||
| """id=$id width="100%" height="100%" style="max-width: ${currentWidth}px; max-height: ${currentHeight}px;" viewBox="0 0 $currentWidth $currentHeight" preserveAspectRatio="xMinYMin meet"""" | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Converts a `Figure` object into a hidden SVG embedded in an HTML <figure> element. | ||
| * The SVG is made hidden using an inline <script> tag. This script does not execute on platforms | ||
| * like GitHub or Gist, allowing the output to be displayed statically on those platforms. | ||
| */ | ||
| private fun figureToHiddenSvg(figure: Figure): Map<String, JsonPrimitive> { | ||
| val plotSVG = PlotSvgExport.buildSvgImageFromRawSpecs(figure.toSpec()) | ||
| val id = UUID.randomUUID().toString() | ||
| val svgWithID = with(plotSVG) { | ||
| val svgSplit = split('\n') | ||
| (listOf(updateSvg(svgSplit.first(), id)) + svgSplit.drop(1)).joinToString("\n") | ||
| } | ||
| val extraHTML = """ | ||
| $svgWithID | ||
| <script>document.getElementById("$id").style.display = "none";</script> | ||
| """.trimIndent() | ||
|
|
||
| return mapOf(MimeTypes.HTML to JsonPrimitive(extraHTML)) | ||
| } | ||
|
|
||
| fun figureToMimeResult(figure: Figure): MimeTypedResultEx { | ||
| val basicResult = figureToMimeJson(figure) | ||
| val extraSvg = figureToHiddenSvg(figure) | ||
| return MimeTypedResultEx( | ||
| basicResult extendedByJson extraSvg, | ||
| id = null, | ||
| metadataModifiers = emptyList() | ||
| ) | ||
| } | ||
| } |
42 changes: 42 additions & 0 deletions
42
plot-api/src/jvmMain/kotlin/org/jetbrains/letsPlot/jupyter/containerUtil.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package org.jetbrains.letsPlot.jupyter | ||
|
|
||
| import kotlinx.serialization.json.JsonArray | ||
| import kotlinx.serialization.json.JsonElement | ||
| import kotlinx.serialization.json.JsonObject | ||
| import kotlinx.serialization.json.JsonPrimitive | ||
| import kotlin.contracts.ExperimentalContracts | ||
| import kotlin.contracts.contract | ||
|
|
||
| internal fun <K, V : Any> Map<K, V>.extendedBy(other: Map<K, V>, join: (V, V) -> V): Map<K, V> { | ||
| return (this + other).mapValues { (k, v) -> | ||
| if (k in this && k in other) | ||
| join(this[k]!!, other[k]!!) | ||
| else | ||
| v | ||
| } | ||
| } | ||
|
|
||
| @OptIn(ExperimentalContracts::class) | ||
| private inline fun <reified T> bothOfType(a: Any, b: Any): Boolean { | ||
| contract { returns(true) implies (a is T && b is T) } | ||
| return a is T && b is T | ||
| } | ||
|
|
||
| @OptIn(ExperimentalContracts::class) | ||
| private inline fun <reified T> bothOfTypeAnd(a: Any, b: Any, condition: (T) -> Boolean): Boolean { | ||
| contract { returns(true) implies (a is T && b is T) } | ||
| return a is T && b is T && condition(a) && condition(b) | ||
| } | ||
|
|
||
| internal infix fun Map<String, JsonElement>.extendedByJson(other: Map<String, JsonElement>): JsonObject { | ||
| val map = this.extendedBy(other) { a, b -> | ||
| // This logic might be enhanced | ||
| when { | ||
| bothOfTypeAnd<JsonPrimitive>(a, b) { it.isString } -> JsonPrimitive(a.content + b.content) | ||
| bothOfType<JsonArray>(a, b) -> JsonArray(a + b) | ||
| bothOfType<JsonObject>(a, b) -> JsonObject(a + b) | ||
| else -> a | ||
| } | ||
| } | ||
| return JsonObject(map) | ||
| } |
39 changes: 39 additions & 0 deletions
39
plot-api/src/jvmMain/kotlin/org/jetbrains/letsPlot/jupyter/serializeSpec.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package org.jetbrains.letsPlot.jupyter | ||
|
|
||
| import kotlinx.serialization.json.* | ||
|
|
||
| /** | ||
| * Serialize Lets-Plot spec into `JsonElement` | ||
| */ | ||
| internal fun serializeSpec(spec: Map<*, *>): JsonElement { | ||
| return serialize(spec) | ||
| } | ||
|
|
||
| private fun serializeAny(obj: Any?): JsonElement { | ||
| return when (obj) { | ||
| null -> JsonNull | ||
| is Map<*, *> -> serialize(obj) | ||
| is List<*> -> serialize(obj) | ||
| is String -> JsonPrimitive(obj) | ||
| is Boolean -> JsonPrimitive(obj) | ||
| is Number -> JsonPrimitive(obj) | ||
| else -> error("Don't know how to parse object [$obj] of class ${obj::class}") | ||
| } | ||
| } | ||
|
|
||
| private fun serialize(map: Map<*, *>): JsonObject { | ||
| return buildJsonObject { | ||
| for ((key, value) in map) { | ||
| if (key !is String) error("Map key [$key] is of type ${key?.let { it::class }}. Don't know how to serialize it.") | ||
| put(key, serializeAny(value)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun serialize(list: List<*>): JsonArray { | ||
| return buildJsonArray { | ||
| for (value in list) { | ||
| add(serializeAny(value)) | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.