Fix Plotly issues after refactoring

This commit is contained in:
2025-02-13 11:51:15 +03:00
parent 97b5973894
commit 3e4bedde48
18 changed files with 154 additions and 161 deletions

View File

@@ -3,6 +3,7 @@ kotlin.mpp.stability.nowarn=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx4G
org.gradle.workers.max=4
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
kotlin.native.enableKlibsCrossCompilation=true

View File

@@ -15,20 +15,24 @@ import dev.datlag.kcef.KCEF
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
import java.io.File
private val allowedPages = listOf(
"Static",
"Dynamic"
)
val port = 7778
@Composable
fun App() {
var downloadProgress by remember { mutableStateOf(-1F) }
var initialized by remember { mutableStateOf(false) } // if true, KCEF can be used to create clients, browsers etc
val scaleFlow = remember { MutableStateFlow(1f) }
val scale by scaleFlow.collectAsState()
val scope = rememberCoroutineScope()
val server = remember {
scope.servePlots(scaleFlow)
scope.servePlots(scaleFlow, port)
}
val state = rememberWebViewStateWithHTMLData(staticPlot())
@@ -43,12 +47,50 @@ fun App() {
)
}
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) { // IO scope recommended but not required
KCEF.init(
builder = {
progress {
onDownloading {
downloadProgress = it
println("Downloading $it")
// use this if you want to display a download progress for example
}
onInitialized {
initialized = true
}
}
},
onError = {
// error during initialization
it?.printStackTrace()
},
onRestartRequired = {
// all required CEF packages downloaded but the application needs a restart to load them (unlikely to happen)
println("Restart required")
}
)
}
}
DisposableEffect(Unit) {
onDispose {
KCEF.disposeBlocking()
server.stop()
}
}
Row(Modifier.fillMaxSize()) {
Column(Modifier.width(300.dp)) {
Button({ navigator.loadHtml(staticPlot()) }, modifier = Modifier.fillMaxWidth()) {
Button({
val html = staticPlot()
println(html)
navigator.loadHtml(html)
}, modifier = Modifier.fillMaxWidth()) {
Text("Static")
}
Button({ navigator.loadUrl("http://localhost:7778/Dynamic") }, modifier = Modifier.fillMaxWidth()) {
Button({ navigator.loadUrl("http://localhost:$port/Dynamic") }, modifier = Modifier.fillMaxWidth()) {
Text("Dynamic")
}
@@ -61,60 +103,25 @@ fun App() {
}
Column(Modifier.fillMaxSize()) {
WebView(
state = state,
navigator = navigator,
modifier = Modifier.fillMaxSize()
)
if (initialized) {
WebView(
state = state,
navigator = navigator,
modifier = Modifier.fillMaxSize()
)
} else {
Text("Downloading CEF: ${downloadProgress}%")
}
}
}
}
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
var downloadProgress by remember { mutableStateOf(-1F) }
var initialized by remember { mutableStateOf(false) } // if true, KCEF can be used to create clients, browsers etc
val bundleLocation = System.getProperty("compose.application.resources.dir")?.let { File(it) } ?: File(".")
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) { // IO scope recommended but not required
KCEF.init(
builder = {
installDir(File(bundleLocation, "kcef-bundle")) // recommended, but not necessary
progress {
onDownloading {
downloadProgress = it
println("Downloading $it")
// use this if you want to display a download progress for example
}
onInitialized {
initialized = true
}
}
},
onError = {
// error during initialization
it?.printStackTrace()
},
onRestartRequired = {
// all required CEF packages downloaded but the application needs a restart to load them (unlikely to happen)
println("Restart required")
}
)
}
}
if (initialized) {
MaterialTheme {
App()
}
}
DisposableEffect(Unit) {
onDispose {
KCEF.disposeBlocking()
}
MaterialTheme {
App()
}
}
}

View File

@@ -11,6 +11,7 @@ import kotlinx.coroutines.launch
import space.kscience.plotly.*
import space.kscience.plotly.models.Scatter
import space.kscience.plotly.models.Trace
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.makeString
import space.kscience.visionforge.plotly.plotlyPage
import kotlin.math.PI
@@ -31,7 +32,7 @@ public fun Scatter(
}
fun staticPlot(): String = Plotly.page {
internal fun staticPlot(): String = Plotly.plugin.visionManager.VisionPage(cdnPlotlyHeader) {
val x = (0..100).map { it.toDouble() / 100.0 }.toDoubleArray()
val y1 = x.map { sin(2.0 * PI * it) }.toDoubleArray()
val y2 = x.map { cos(2.0 * PI * it) }.toDoubleArray()
@@ -41,19 +42,17 @@ fun staticPlot(): String = Plotly.page {
val trace2 = Scatter(x, y2) {
name = "cos"
}
vision {
plotly(config = PlotlyConfig { responsive = true }) {//static plot
traces(trace1, trace2)
layout {
title = "First graph, row: 1, size: 8/12"
xaxis.title = "x axis name"
yaxis { title = "y axis name" }
}
staticPlot {
traces(trace1, trace2)
layout {
title = "First graph, row: 1, size: 8/12"
xaxis.title = "x axis name"
yaxis { title = "y axis name" }
}
}
}.makeString()
fun CoroutineScope.servePlots(scale: StateFlow<Number>): EmbeddedServer<*, *> = embeddedServer(CIO, port = 7777) {
fun CoroutineScope.servePlots(scale: StateFlow<Number>, port: Int = 7778): EmbeddedServer<*, *> = embeddedServer(CIO, port = port) {
val x = (0..100).map { it.toDouble() / 100.0 }.toDoubleArray()
@@ -107,5 +106,5 @@ fun CoroutineScope.servePlots(scale: StateFlow<Number>): EmbeddedServer<*, *> =
}
}
}
}
}.start()

View File

@@ -2,11 +2,10 @@ package contour
import space.kscience.plotly.Plotly
import space.kscience.plotly.layout
import space.kscience.plotly.makePageFile
import space.kscience.plotly.models.Contour
import space.kscience.plotly.models.invoke
import space.kscience.plotly.page
import space.kscience.plotly.plot
import space.kscience.visionforge.html.openInBrowser
/**
@@ -43,7 +42,7 @@ fun main() {
connectgaps = true
}
Plotly.page {
Plotly.makePageFile {
plot {
traces(contour1)
layout {
@@ -61,5 +60,5 @@ fun main() {
title = "Connected Gaps"
}
}
}.openInBrowser()
}
}

View File

@@ -1,13 +1,9 @@
import kotlinx.html.div
import kotlinx.html.h1
import kotlinx.html.hr
import space.kscience.plotly.Plotly
import space.kscience.plotly.layout
import space.kscience.plotly.*
import space.kscience.plotly.models.Trace
import space.kscience.plotly.models.invoke
import space.kscience.plotly.page
import space.kscience.plotly.staticPlot
import space.kscience.visionforge.html.openInBrowser
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
@@ -21,8 +17,8 @@ fun main() {
val trace1 = Trace(x1, y1) { name = "sin" }
val trace2 = Trace(x1, y2) { name = "cos" }
Plotly.page {
staticPlot{
Plotly.makePageFile {
plot {
traces(trace1, trace2)
layout {
title = "The plot above"
@@ -43,5 +39,5 @@ fun main() {
}
}
}
}.openInBrowser()
}
}

View File

@@ -1,7 +1,6 @@
import space.kscience.plotly.*
import space.kscience.plotly.models.ScatterMode
import space.kscience.plotly.models.scatter
import space.kscience.visionforge.html.openInBrowser
/**
@@ -9,7 +8,7 @@ import space.kscience.visionforge.html.openInBrowser
* - Download plot as SVG using configuration button
*/
fun main() {
val fragment = Plotly.page {
Plotly.makePageFile {
val plotConfig = PlotlyConfig{
saveAsSvg()
}
@@ -34,5 +33,4 @@ fun main() {
}
}
}
fragment.openInBrowser()
}

View File

@@ -1,8 +1,8 @@
import space.kscience.dataforge.meta.invoke
import space.kscience.plotly.Plotly
import space.kscience.plotly.ResourceLocation
import space.kscience.plotly.openInBrowser
import space.kscience.plotly.trace
import space.kscience.visionforge.html.ResourceLocation
import kotlin.math.PI
import kotlin.math.sin

View File

@@ -64,7 +64,7 @@ private class PlotGrid {
private fun Plotly.grid(block: PlotGrid.() -> Unit): VisionPage {
val grid = PlotGrid().apply(block)
return page(cdnBootstrap, cdnPlotlyHeader) {
return plugin.visionManager.VisionPage(cdnBootstrap, cdnPlotlyHeader) {
div("col") {
grid.grid.forEach { row ->
div("row") {

View File

@@ -2,11 +2,10 @@ package heatmap
import space.kscience.plotly.Plotly
import space.kscience.plotly.layout
import space.kscience.plotly.makePageFile
import space.kscience.plotly.models.Heatmap
import space.kscience.plotly.models.invoke
import space.kscience.plotly.page
import space.kscience.plotly.plot
import space.kscience.visionforge.html.openInBrowser
/**
@@ -43,7 +42,7 @@ fun main() {
connectgaps = true
}
Plotly.page {
Plotly.makePageFile {
plot {
traces(heatmap1)
layout {
@@ -61,5 +60,5 @@ fun main() {
title = "Connected Gaps"
}
}
}.openInBrowser()
}
}

View File

@@ -2,11 +2,10 @@ package pie
import space.kscience.plotly.Plotly
import space.kscience.plotly.layout
import space.kscience.plotly.makePageFile
import space.kscience.plotly.models.Pie
import space.kscience.plotly.models.invoke
import space.kscience.plotly.page
import space.kscience.plotly.plot
import space.kscience.visionforge.html.openInBrowser
fun main() {
@@ -22,7 +21,7 @@ fun main() {
hole = 0.4
}
Plotly.page {
Plotly.makePageFile {
plot {
traces(donut1)
@@ -42,5 +41,5 @@ fun main() {
title = "CO2"
}
}
}.openInBrowser()
}
}

View File

@@ -3,7 +3,10 @@ import space.kscience.plotly.*
import space.kscience.plotly.models.Trace
import space.kscience.plotly.models.invoke
import space.kscience.plotly.palettes.T10
import space.kscience.visionforge.html.*
import space.kscience.visionforge.html.HtmlFragment
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.appendTo
import space.kscience.visionforge.html.openInBrowser
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
@@ -29,12 +32,12 @@ public val cdnBootstrap: HtmlFragment = HtmlFragment {
public class PlotTabs {
public data class Tab(val title: String, val id: String, val content: HtmlVisionFragment)
public data class Tab(val title: String, val id: String, val content: HtmlFragment)
private val _tabs = ArrayList<Tab>()
public val tabs: List<Tab> get() = _tabs
public fun tab(title: String, id: String = title, fragment: HtmlVisionFragment) {
public fun tab(title: String, id: String = title, fragment: HtmlFragment) {
_tabs.add(Tab(title, id, fragment))
}
}
@@ -42,7 +45,7 @@ public class PlotTabs {
public fun Plotly.tabs(tabsID: String = "tabs", block: PlotTabs.() -> Unit): VisionPage {
val grid = PlotTabs().apply(block)
return page(cdnBootstrap, cdnPlotlyHeader) {
return plugin.visionManager.VisionPage(cdnBootstrap, cdnPlotlyHeader) {
ul("nav nav-tabs") {
role = "tablist"
id = tabsID
@@ -73,7 +76,7 @@ public fun Plotly.tabs(tabsID: String = "tabs", block: PlotTabs.() -> Unit): Vis
id = tab.id
role = "tabpanel"
attributes["aria-labelledby"] = "${tab.id}-tab"
visionFragment(visionManager, fragment = tab.content)
tab.content.appendTo(consumer)
}
}
}
@@ -105,10 +108,10 @@ fun main() {
responsive = true
}
val plot = Plotly.tabs {
val page = Plotly.tabs {
tab("First") {
plot(config = responsive) {
staticPlot(config = responsive) {
traces(trace1)
layout {
title = "First graph"
@@ -118,7 +121,7 @@ fun main() {
}
}
tab("Second") {
plot(config = responsive) {
staticPlot(config = responsive) {
traces(trace2)
layout {
title = "Second graph"
@@ -129,5 +132,5 @@ fun main() {
}
}
plot.openInBrowser()
page.openInBrowser()
}

View File

@@ -4,7 +4,6 @@ import space.kscience.dataforge.meta.Value
import space.kscience.plotly.*
import space.kscience.plotly.models.*
import space.kscience.plotly.palettes.Xkcd
import space.kscience.visionforge.html.openInBrowser
import kotlin.math.PI
import kotlin.math.sin
@@ -42,7 +41,7 @@ fun main() {
}
}
Plotly.page(mathJaxHeader, cdnPlotlyHeader) {
Plotly.makePageFile(additionalHeaders = mapOf("mathJax" to mathJaxHeader)) {
plot {
scatter { // sinus
x.set(xValues)
@@ -152,5 +151,5 @@ fun main() {
}
}
}
}.openInBrowser()
}
}

View File

@@ -1,5 +1,6 @@
package space.kscience.plotly
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonArray
@@ -21,6 +22,7 @@ import space.kscience.visionforge.*
@Serializable
public class Plot : AbstractVision(), MutableVisionGroup<Trace> {
@SerialName("data")
private val _data = mutableListOf<Trace>()
public val data: List<Trace> get() = _data

View File

@@ -10,7 +10,6 @@ import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.plotly.models.Trace
import space.kscience.visionforge.VisionBuilder
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.*
import kotlin.js.JsName
@@ -93,24 +92,6 @@ public inline fun VisionOutput.plotly(
return Plotly.plot(block)
}
public fun Plotly.page(
pageHeaders: Map<String, HtmlFragment> = emptyMap(),
content: HtmlVisionFragment,
): VisionPage = VisionPage(
visionManager = context.request(VisionManager),
pageHeaders = mapOf("plotly" to cdnPlotlyHeader) + pageHeaders,
content = content
)
public fun Plotly.page(
vararg pageHeaders: HtmlFragment,
content: HtmlVisionFragment,
): VisionPage = VisionPage(
visionManager = context.request(VisionManager),
pageHeaders = mapOf("plotly" to cdnPlotlyHeader) + pageHeaders.associateBy { it.toString() },
content = content
)
context(rootConsumer: VisionTagConsumer<*>)
public fun TagConsumer<*>.plot(
config: PlotlyConfig = PlotlyConfig(),

View File

@@ -1,6 +1,7 @@
package space.kscience.plotly
import space.kscience.visionforge.html.HtmlFragment
import space.kscience.visionforge.html.*
import space.kscience.visionforge.visionManager
import java.awt.Desktop
import java.nio.file.Files
import java.nio.file.Path
@@ -10,31 +11,6 @@ import javax.swing.filechooser.FileNameExtensionFilter
internal const val assetsDirectory = "assets"
/**
* The location of resources for plot.
*/
public enum class ResourceLocation {
/**
* Use cdn or other remote source for assets
*/
REMOTE,
/**
* Store assets in a sibling folder `plotly-assets` or in a system-wide folder if this is a default temporary file
*/
LOCAL,
/**
* Store assets in a system-window `~/.plotly/plotly-assets` folder
*/
SYSTEM,
/**
* Embed the asset into the html. Could produce very large files.
*/
EMBED
}
/**
* Create a standalone html with the plot
* @param path the reference to html file. If null, create a temporary file
@@ -61,6 +37,28 @@ public fun Plot.openInBrowser(
Desktop.getDesktop().browse(path.toFile().toURI())
}
public fun Plotly.makePageFile(
path: Path? = null,
title: String = "VisionForge Plotly page",
additionalHeaders: Map<String, HtmlFragment> = emptyMap(),
resourceLocation: ResourceLocation = ResourceLocation.SYSTEM,
show: Boolean = true,
content: HtmlVisionFragment,
): Path {
val actualPath = VisionPage(context.visionManager, content = content).makeFile(path) { actualPath ->
mapOf(
"title" to VisionPage.title(title),
"plotly" to VisionPage.importScriptHeader(
"js/plotly-kt.js",
resourceLocation,
actualPath
),
) + additionalHeaders
}
if (show) Desktop.getDesktop().browse(actualPath.toFile().toURI())
return actualPath
}
///**
// * The same as [Plot.makeFile].
// */

View File

@@ -4,6 +4,7 @@ import kotlinx.html.link
import kotlinx.html.script
import kotlinx.html.unsafe
import space.kscience.visionforge.html.HtmlFragment
import space.kscience.visionforge.html.ResourceLocation
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
@@ -107,14 +108,15 @@ internal val embededPlotlyHeader = HtmlFragment {
internal fun inferPlotlyHeader(
target: Path?,
resourceLocation: ResourceLocation
resourceLocation: ResourceLocation?
): HtmlFragment = when (resourceLocation) {
ResourceLocation.REMOTE -> cdnPlotlyHeader
null -> cdnPlotlyHeader
ResourceLocation.LOCAL -> if (target != null) {
localPlotlyHeader(target)
} else {
systemPlotlyHeader
}
ResourceLocation.SYSTEM -> systemPlotlyHeader
ResourceLocation.EMBED -> embededPlotlyHeader
}

View File

@@ -13,11 +13,11 @@ public data class VisionPage(
public val pageHeaders: Map<String, HtmlFragment> = emptyMap(),
public val content: HtmlVisionFragment,
) {
public companion object{
public companion object {
/**
* Use a script with given [src] as a global header for all pages.
*/
public fun scriptHeader(src: String, block: SCRIPT.() -> Unit = {}): HtmlFragment = HtmlFragment{
public fun scriptHeader(src: String, block: SCRIPT.() -> Unit = {}): HtmlFragment = HtmlFragment {
script {
type = "text/javascript"
this.src = src
@@ -28,7 +28,7 @@ public data class VisionPage(
/**
* Use css with the given stylesheet link as a global header for all pages.
*/
public fun styleSheetHeader(href: String, block: LINK.() -> Unit = {}): HtmlFragment = HtmlFragment{
public fun styleSheetHeader(href: String, block: LINK.() -> Unit = {}): HtmlFragment = HtmlFragment {
link {
rel = "stylesheet"
this.href = href
@@ -36,8 +36,13 @@ public data class VisionPage(
}
}
public fun title(title:String): HtmlFragment = HtmlFragment{
public fun title(title: String): HtmlFragment = HtmlFragment {
title(title)
}
}
}
}
public fun VisionManager.VisionPage(
vararg headers: HtmlFragment,
content: HtmlVisionFragment
): VisionPage = VisionPage(this, headers.associateBy { it.toString() }, content)

View File

@@ -2,6 +2,7 @@ package space.kscience.visionforge.html
import kotlinx.html.body
import kotlinx.html.head
import kotlinx.html.html
import kotlinx.html.meta
import kotlinx.html.stream.createHTML
import java.awt.Desktop
@@ -11,18 +12,22 @@ import java.nio.file.Path
/**
* Render given [VisionPage] to a string using a set of [additionalHeaders] that override current page headers.
*/
public fun VisionPage.makeString(additionalHeaders: Map<String, HtmlFragment> = emptyMap()): String = createHTML().apply {
head {
meta {
charset = "utf-8"
public fun VisionPage.makeString(
additionalHeaders: Map<String, HtmlFragment> = emptyMap()
): String = "<!DOCTYPE html>\n" + createHTML().apply {
html {
head {
meta {
charset = "utf-8"
}
(pageHeaders + additionalHeaders).values.forEach {
appendFragment(it)
}
}
(pageHeaders + additionalHeaders).values.forEach {
appendFragment(it)
body {
visionFragment(visionManager, fragment = content)
}
}
body {
visionFragment(visionManager, fragment = content)
}
}.finalize()
/**