Skip to content

Commit 361effd

Browse files
committed
Replace 4 output options with one. Usage: '%use lets-plot(output="js,ktnb,svg,png")'
Fix size of PNG in output Upgrade LP -> 4.8.1-rc1
1 parent a18031f commit 361effd

File tree

7 files changed

+119
-23
lines changed

7 files changed

+119
-23
lines changed

demo/js-frontend-app/src/jsMain/resources/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</head>
1111
<body>
1212
<script type="text/javascript"
13-
src="https://cdn.jsdelivr.net/gh/JetBrains/lets-plot@v4.8.0/js-package/distr/lets-plot.min.js"></script>
13+
src="https://cdn.jsdelivr.net/gh/JetBrains/lets-plot@v4.8.1rc1/js-package/distr/lets-plot.min.js"></script>
1414
<script src="js-frontend-app.js"></script>
1515
<div>
1616
<h2>Lets-Plot Kotlin/JS Demo.</h2>

devdocs/JUPYTER_KOTLIN_KERNEL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Lets-Plot library descriptors are files:
88
- lets-plot.json
99
- lets-plot-gt.json
1010

11-
After installing kotlin kernel, the "bundled" library descriptors are located in
11+
After installing the kotlin kernel, the "bundled" library descriptors are located in
1212

1313
> /opt/anaconda3/envs/<env name>/lib/python3.7/site-packages/run_kotlin_kernel/libraries/
1414

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ ksp.version=1.9.25-1.0.20
3636
jupyterApi.version=0.12.0-313
3737

3838
# Also update the JS version in <home>/demo/js-frontend-app/src/jsMain/resources/index.html
39-
letsPlot.version=4.8.0
39+
letsPlot.version=4.8.1-rc1
4040

4141
# https://geotoolsnews.blogspot.com/
4242
geotools.version=33.2

plot-api/src/commonMain/kotlin/org/jetbrains/letsPlot/LetsPlot.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,18 @@ object LetsPlot {
4242
@Suppress("MemberVisibilityCanBePrivate")
4343
var apiVersion: String = "Unknown"
4444

45-
@Suppress("unused")
46-
fun getInfo() = "Lets-Plot Kotlin API v.$apiVersion. Frontend: ${frontendContext.getInfo()}"
45+
var outputsDescription: String? = null
4746

4847
@Suppress("unused")
48+
fun getInfo(): String {
49+
val info = "Lets-Plot Kotlin API v.$apiVersion. Frontend: ${frontendContext.getInfo()}"
50+
return if (outputsDescription != null) {
51+
"$info\nOutputs: $outputsDescription"
52+
} else {
53+
info
54+
}
55+
}
56+
4957
fun setupNotebook(
5058
jsVersion: String,
5159
isolatedFrame: Boolean?,
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
lets_plot.version=4.8.0
2-
lets_plot_kotlin_api.version=4.11.2
1+
lets_plot.version=4.8.1-rc1
2+
lets_plot_kotlin_api.version=4.11.3-SNAPSHOT

toolkit/jupyter/src/main/kotlin/org/jetbrains/letsPlot/toolkit/jupyter/Integration.kt

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ internal class Integration(private val notebook: Notebook, options: MutableMap<S
1919
internal val config = JupyterConfig()
2020
private lateinit var frontendContext: NotebookFrontendContext
2121

22+
companion object {
23+
private const val JS = "js" // Classic Web output: HTML+JS
24+
private const val KTNB = "ktnb" // Kotlin Notebook Swing-based rendering
25+
private const val SVG = "svg" // Static SVG output
26+
private const val PNG = "png" // Static PNG output
27+
28+
private const val DEFAULT_OUTPUT = "$JS, $KTNB, $SVG"
29+
}
30+
2231
// Take integration options from descriptor by default;
2332
// If used via Kotlin Notebook plugin as a dependency,
2433
// provide defaults (versions from `VersionChecker`
@@ -28,10 +37,40 @@ internal class Integration(private val notebook: Notebook, options: MutableMap<S
2837
private val isolatedFrame = options["isolatedFrame"] ?: ""
2938

3039
// Output options
31-
private val addWebOutput = (options["addWebOutput"] ?: "true").toBoolean()
32-
private val addKTNBOutput = (options["addKTNBOutput"] ?: "true").toBoolean()
33-
private val addStaticSvg = (options["addStaticSvg"] ?: "true").toBoolean()
34-
private val addStaticPng = (options["addStaticPng"] ?: "false").toBoolean()
40+
private val addWebOutput: Boolean
41+
private val addKTNBOutput: Boolean
42+
private val addStaticSvg: Boolean
43+
private val addStaticPng: Boolean
44+
45+
init {
46+
val outputOption = options["output"] ?: DEFAULT_OUTPUT
47+
if (outputOption.isEmpty()) {
48+
throw IllegalArgumentException(
49+
"Output option cannot be an empty string. " +
50+
"Valid types are: $JS, $KTNB, $SVG, $PNG"
51+
)
52+
}
53+
54+
val outputTypes = outputOption
55+
.split(",")
56+
.map { it.trim().lowercase() }
57+
.toSet()
58+
.also { types ->
59+
val validTypes = setOf(JS, KTNB, SVG, PNG)
60+
val invalidTypes = types - validTypes
61+
if (invalidTypes.isNotEmpty()) {
62+
throw IllegalArgumentException(
63+
"Invalid output type(s): ${invalidTypes.joinToString(", ")}. " +
64+
"Valid types are: ${validTypes.joinToString(", ")}"
65+
)
66+
}
67+
}
68+
69+
addWebOutput = JS in outputTypes
70+
addKTNBOutput = KTNB in outputTypes
71+
addStaticSvg = SVG in outputTypes
72+
addStaticPng = PNG in outputTypes
73+
}
3574

3675
override fun Builder.onLoaded() {
3776
import("org.jetbrains.letsPlot.*")
@@ -69,6 +108,13 @@ internal class Integration(private val notebook: Notebook, options: MutableMap<S
69108
// add figure renders AFTER frontendContext initialization
70109
addRenders()
71110
declare("letsPlotNotebookConfig" to config)
111+
112+
LetsPlot.outputsDescription = NotebookRenderingContext.OutputOptions(
113+
addWebOutput = addWebOutput,
114+
addKTNBOutput = addKTNBOutput,
115+
addStaticSvg = addStaticSvg,
116+
addStaticPng = addStaticPng
117+
).describe()
72118
}
73119
}
74120

@@ -86,7 +132,7 @@ internal class Integration(private val notebook: Notebook, options: MutableMap<S
86132

87133
renderWithHost<Figure> { host, value ->
88134
// For cases when Integration is added via Kotlin Notebook project dependency;
89-
// display configure HTML with the first `Figure` rendering
135+
// display the "configure HTML" with the first `Figure` rendering
90136
if (addWebOutput && !firstFigureRendered) {
91137
firstFigureRendered = true
92138
host.execute { display(HTML(frontendContext.getConfigureHtml()), null) }

toolkit/jupyter/src/main/kotlin/org/jetbrains/letsPlot/toolkit/jupyter/NotebookRenderingContext.kt

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,25 @@ internal class NotebookRenderingContext(
2525
val addKTNBOutput: Boolean,
2626
val addStaticSvg: Boolean,
2727
val addStaticPng: Boolean,
28-
)
28+
) {
29+
fun hasInteractiveOutput(): Boolean {
30+
return addWebOutput || addKTNBOutput
31+
}
32+
33+
fun describe() : String {
34+
val outputs = mutableListOf<String>()
35+
val hasInteractive = hasInteractiveOutput()
36+
if (addWebOutput) outputs.add("Web (HTML+JS)")
37+
if (addKTNBOutput) outputs.add("Kotlin Notebook (Swing)")
38+
if (addStaticSvg) outputs.add("Static SVG${if (hasInteractive) " (hidden)" else ""}")
39+
if (addStaticPng) outputs.add("Static PNG${if (hasInteractive) " (hidden)" else ""}")
40+
return if (outputs.isEmpty()) {
41+
"No outputs"
42+
} else {
43+
outputs.joinToString(", ")
44+
}
45+
}
46+
}
2947

3048
/**
3149
* Creates Mime JSON with two output options - HTML and application/plot.
@@ -70,42 +88,66 @@ internal class NotebookRenderingContext(
7088
* The SVG is made hidden using an inline <script> tag. This script does not execute on platforms
7189
* like GitHub or Gist, allowing the output to be displayed statically on those platforms.
7290
*/
73-
private fun figureToHiddenSvg(figure: Figure): Map<String, JsonPrimitive> {
91+
private fun figureToSvgOutput(figure: Figure, hidden: Boolean): Map<String, JsonPrimitive> {
7492
val plotSVG = PlotSvgExport.buildSvgImageFromRawSpecs(figure.toSpec())
7593
val id = UUID.randomUUID().toString()
7694
val svgWithID = with(plotSVG) {
7795
val svgSplit = split('\n')
7896
(listOf(updateSvg(svgSplit.first(), id)) + svgSplit.drop(1)).joinToString("\n")
7997
}
98+
99+
val styleDisplayNone = if (hidden) {
100+
"""<script>document.getElementById("$id").style.display = "none";</script>"""
101+
} else {
102+
""
103+
}
80104
val htmlWithSvg = """
81105
$svgWithID
82-
<script>document.getElementById("$id").style.display = "none";</script>
106+
$styleDisplayNone
83107
""".trimIndent()
84108

85109
return mapOf(MimeTypes.HTML to JsonPrimitive(htmlWithSvg))
86110
}
87111

88-
private fun figureToHiddenPng(figure: Figure): Map<String, JsonPrimitive> {
112+
/**
113+
* Converts a `Figure` object into a hidden PNG embedded in an HTML <img> element.
114+
* The PNG is made hidden using an inline <script> tag. This allows the output to be displayed
115+
* statically on platforms like GitHub or Gist.
116+
*/
117+
private fun figureToPngOutput(figure: Figure, hidden: Boolean): Map<String, JsonPrimitive> {
118+
val imageData = PlotImageExport.buildImageFromRawSpecs(
119+
plotSpec = figure.toSpec(),
120+
format = PlotImageExport.Format.PNG,
121+
scalingFactor = 2.0
122+
)
89123
val base64 = Base64.getEncoder().encodeToString(
90-
PlotImageExport.buildImageFromRawSpecs(
91-
figure.toSpec(), PlotImageExport.Format.PNG
92-
).bytes
124+
imageData.bytes
93125
)
126+
val width = imageData.plotSize.x.toInt()
127+
val height = imageData.plotSize.y.toInt()
94128
val id = UUID.randomUUID().toString()
129+
130+
val styleDisplayNone = if (hidden) {
131+
"""<script>document.getElementById("$id").style.display = "none";</script>"""
132+
} else {
133+
""
134+
}
95135
val htmlWithPng = """
96-
<img id="$id" src="data:image/png;base64,$base64" alt="image">
97-
<script>document.getElementById("$id").style.display = "none";</script>
136+
<img id="$id" src="data:image/png;base64,$base64" alt="image" width="$width" height="$height">
137+
$styleDisplayNone
98138
""".trimIndent()
99139

100140
return mapOf(MimeTypes.HTML to JsonPrimitive(htmlWithPng))
101141
}
102142

103143
fun figureToMimeResult(figure: Figure): MimeTypedResultEx {
144+
// Hide static images if interactive outputs are present
145+
val hidden = outputOptions.hasInteractiveOutput()
104146
val mimeJson = figureToMimeJson(figure)
105147
.let {
106-
if (outputOptions.addStaticSvg) it.extendedByJson(figureToHiddenSvg(figure)) else it
148+
if (outputOptions.addStaticSvg) it.extendedByJson(figureToSvgOutput(figure, hidden)) else it
107149
}.let {
108-
if (outputOptions.addStaticPng) it.extendedByJson(figureToHiddenPng(figure)) else it
150+
if (outputOptions.addStaticPng) it.extendedByJson(figureToPngOutput(figure, hidden)) else it
109151
}
110152
return MimeTypedResultEx(
111153
mimeJson,

0 commit comments

Comments
 (0)