[WIP] gradual merge of Plotly
This commit is contained in:
@@ -28,8 +28,8 @@ kotlin {
|
||||
|
||||
kscience {
|
||||
dependencies {
|
||||
implementation(projects.plotly.plotlyktCore)
|
||||
implementation(projects.visionforge.visionforgeGdml)
|
||||
implementation(projects.visionforge.visionforgePlotly)
|
||||
implementation(projects.visionforge.visionforgeMarkdown)
|
||||
implementation(projects.visionforge.visionforgeThreejs)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.plotly.plotlyktServer)
|
||||
implementation(projects.plotly.plotlyktJupyter)
|
||||
implementation(projects.plotly.plotlyktCore)
|
||||
implementation(projects.plotly.plotlyktScript)
|
||||
implementation(kotlin("script-runtime"))
|
||||
implementation("org.jetbrains.kotlinx:dataframe:0.13.1")
|
||||
|
||||
@@ -21,7 +21,6 @@ kotlin {
|
||||
implementation(compose.material)
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation("io.github.kevinnzou:compose-webview-multiplatform:1.9.8")
|
||||
implementation(projects.plotly.plotlyktServer)
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ fun CoroutineScope.servePlots(scale: StateFlow<Number>): ApplicationEngine = Plo
|
||||
val trace2 = Scatter(x, y2) {
|
||||
name = "cos"
|
||||
}
|
||||
plot(renderer = container) {//static plot
|
||||
plotly(renderer = container) {//static plot
|
||||
traces(trace1, trace2)
|
||||
layout {
|
||||
title = "First graph, row: 1, size: 8/12"
|
||||
@@ -62,7 +62,7 @@ fun CoroutineScope.servePlots(scale: StateFlow<Number>): ApplicationEngine = Plo
|
||||
|
||||
val trace = Scatter(x, y) { name = "sin" }
|
||||
|
||||
val plot = plot("dynamic", config = PlotlyConfig { responsive = true }, renderer = container) {
|
||||
val plot = plotly("dynamic", config = PlotlyConfig { responsive = true }, renderer = container) {
|
||||
traces(trace)
|
||||
layout {
|
||||
title = "Dynamic plot"
|
||||
|
||||
@@ -10,7 +10,6 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":plotly:plotlykt-server"))
|
||||
implementation("no.tornado:tornadofx:1.7.20")
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import space.kscience.plotly.Plotly
|
||||
import space.kscience.plotly.layout
|
||||
import space.kscience.plotly.models.Trace
|
||||
import space.kscience.plotly.models.invoke
|
||||
import space.kscience.plotly.plot
|
||||
import space.kscience.plotly.plotly
|
||||
import space.kscience.plotly.server.pushUpdates
|
||||
import space.kscience.plotly.server.serve
|
||||
import kotlin.math.PI
|
||||
@@ -27,7 +27,7 @@ fun serve(scale: ObservableIntegerValue) = Plotly.serve(port = 7778) {
|
||||
val trace2 = Trace(x, y2) {
|
||||
name = "cos"
|
||||
}
|
||||
plot {//static plot
|
||||
plotly {//static plot
|
||||
traces(trace1, trace2)
|
||||
layout {
|
||||
title = "First graph, row: 1, size: 8/12"
|
||||
@@ -43,7 +43,7 @@ fun serve(scale: ObservableIntegerValue) = Plotly.serve(port = 7778) {
|
||||
|
||||
val trace = Trace(x, y) { name = "sin" }
|
||||
|
||||
val plot = plot("dynamic", renderer = container) {
|
||||
val plot = plotly("dynamic", renderer = container) {
|
||||
traces(trace)
|
||||
layout {
|
||||
title = "Dynamic plot"
|
||||
|
||||
@@ -7,7 +7,7 @@ import space.kscience.plotly.Plotly
|
||||
import space.kscience.plotly.layout
|
||||
import space.kscience.plotly.models.AxisType
|
||||
import space.kscience.plotly.models.DragMode
|
||||
import space.kscience.plotly.plot
|
||||
import space.kscience.plotly.plotly
|
||||
import space.kscience.plotly.server.close
|
||||
import space.kscience.plotly.server.pushUpdates
|
||||
import space.kscience.plotly.server.serve
|
||||
@@ -18,7 +18,7 @@ fun main() {
|
||||
val server = Plotly.serve {
|
||||
pushUpdates(50)
|
||||
page { plotly ->
|
||||
plot(renderer = plotly) {
|
||||
plotly(renderer = plotly) {
|
||||
traces(candleStickTrace)
|
||||
layout {
|
||||
dragmode = DragMode.zoom
|
||||
|
||||
@@ -8,7 +8,7 @@ import kotlinx.html.link
|
||||
import space.kscience.plotly.Plotly
|
||||
import space.kscience.plotly.layout
|
||||
import space.kscience.plotly.models.Trace
|
||||
import space.kscience.plotly.plot
|
||||
import space.kscience.plotly.plotly
|
||||
import space.kscience.plotly.server.pushUpdates
|
||||
import space.kscience.plotly.server.serve
|
||||
import space.kscience.plotly.server.show
|
||||
@@ -76,7 +76,7 @@ fun main() {
|
||||
}
|
||||
div("row") {
|
||||
div("col-6") {
|
||||
plot(renderer = renderer) {
|
||||
plotly(renderer = renderer) {
|
||||
layout {
|
||||
title = "sin property"
|
||||
xaxis.title = "point index"
|
||||
@@ -91,7 +91,7 @@ fun main() {
|
||||
}
|
||||
}
|
||||
div("col-6") {
|
||||
plot(renderer = renderer) {
|
||||
plotly(renderer = renderer) {
|
||||
layout {
|
||||
title = "cos property"
|
||||
xaxis.title = "point index"
|
||||
@@ -108,7 +108,7 @@ fun main() {
|
||||
}
|
||||
div("row") {
|
||||
div("col-12") {
|
||||
plot(renderer = renderer) {
|
||||
plotly(renderer = renderer) {
|
||||
layout {
|
||||
title = "cos vs sin"
|
||||
xaxis.title = "sin"
|
||||
|
||||
@@ -5,7 +5,7 @@ import space.kscience.dataforge.meta.invoke
|
||||
import space.kscience.plotly.Plotly
|
||||
import space.kscience.plotly.models.Trace
|
||||
import space.kscience.plotly.models.invoke
|
||||
import space.kscience.plotly.plot
|
||||
import space.kscience.plotly.plotly
|
||||
import space.kscience.plotly.server.close
|
||||
import space.kscience.plotly.server.pushUpdates
|
||||
import space.kscience.plotly.server.serve
|
||||
@@ -35,7 +35,7 @@ fun main() {
|
||||
page { plotly ->
|
||||
h1 { +"This is the plot page" }
|
||||
a("/other") { +"The other page" }
|
||||
plot(renderer = plotly) {
|
||||
plotly(renderer = plotly) {
|
||||
traces(sinTrace, cosTrace)
|
||||
layout {
|
||||
title = "Other dynamic plot"
|
||||
@@ -48,7 +48,7 @@ fun main() {
|
||||
page("other") { plotly ->
|
||||
h1 { +"This is the other plot page" }
|
||||
a("/") { +"Back to the main page" }
|
||||
plot(renderer = plotly) {
|
||||
plotly(renderer = plotly) {
|
||||
traces(sinTrace)
|
||||
layout {
|
||||
title = "Dynamic plot"
|
||||
|
||||
@@ -68,7 +68,7 @@ private fun Plotly.grid(block: PlotGrid.() -> Unit): PlotlyPage {
|
||||
div("row") {
|
||||
row.forEach { cell ->
|
||||
div("col-${cell.width}") {
|
||||
plot(cell.plot, cell.id, renderer = container)
|
||||
plotly(cell.plot, cell.id, renderer = container)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import space.kscience.plotly.Plotly
|
||||
import space.kscience.plotly.fragment
|
||||
import space.kscience.plotly.makeFile
|
||||
import space.kscience.plotly.models.Pie
|
||||
import space.kscience.plotly.plot
|
||||
|
||||
|
||||
fun main() {
|
||||
|
||||
@@ -6,7 +6,7 @@ val trace1 = Trace(x1, y1) { name = "sin" }
|
||||
val trace2 = Trace(x1, y2) { name = "cos" }
|
||||
|
||||
|
||||
plot {
|
||||
plotly {
|
||||
traces(trace1, trace2)
|
||||
layout {
|
||||
title = "The plot above"
|
||||
@@ -18,7 +18,7 @@ hr()
|
||||
h1 { +"A custom separator" }
|
||||
hr()
|
||||
div {
|
||||
plot {
|
||||
plotly {
|
||||
traces(trace1, trace2)
|
||||
layout {
|
||||
title = "The plot below"
|
||||
|
||||
@@ -8,7 +8,7 @@ import space.kscience.plotly.Plot
|
||||
import space.kscience.plotly.Plotly
|
||||
import space.kscience.plotly.models.Trace
|
||||
import space.kscience.plotly.models.invoke
|
||||
import space.kscience.plotly.plot
|
||||
import space.kscience.plotly.plotly
|
||||
import space.kscience.plotly.server.close
|
||||
import space.kscience.plotly.server.serve
|
||||
import space.kscience.plotly.server.show
|
||||
@@ -43,11 +43,11 @@ fun main() {
|
||||
style = "display: flex; align-items: stretch; "
|
||||
div {
|
||||
style = "width: 64%;"
|
||||
plot(plot1)
|
||||
plotly(plot1)
|
||||
}
|
||||
div {
|
||||
style = "width: 32%;"
|
||||
plot {
|
||||
plotly {
|
||||
traces(trace1, trace2)
|
||||
layout {
|
||||
title = "Second graph, row: 1, size: 4/12"
|
||||
@@ -61,7 +61,7 @@ fun main() {
|
||||
|
||||
|
||||
div {
|
||||
plot {
|
||||
plotly {
|
||||
traces(trace1, trace2)
|
||||
layout {
|
||||
title = "Third graph, row: 2, size: 12/12"
|
||||
@@ -75,7 +75,7 @@ fun main() {
|
||||
page("other") {
|
||||
h1 { +"This is the other plot page" }
|
||||
a("/") { +"Back to the main page" }
|
||||
plot(plot1)
|
||||
plotly(plot1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -84,5 +84,5 @@ fun main() {
|
||||
}
|
||||
}
|
||||
|
||||
plot.export(selectFile(FileNameExtensionFilter("SVG","svg")) ?: error("File not selected"))
|
||||
plot.export(Plotly.selectFile(FileNameExtensionFilter("SVG","svg")) ?: error("File not selected"))
|
||||
}
|
||||
@@ -3,10 +3,31 @@ 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.HtmlFragment
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
public val cdnBootstrap: HtmlFragment = HtmlFragment {
|
||||
script {
|
||||
src = "https://code.jquery.com/jquery-3.5.1.slim.min.js"
|
||||
integrity = "sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
|
||||
attributes["crossorigin"] = "anonymous"
|
||||
}
|
||||
script {
|
||||
src = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.bundle.min.js"
|
||||
integrity = "sha384-1CmrxMRARb6aLqgBO7yyAxTOQE2AKb9GfXnEo760AUcUmFx3ibVJJAzGytlQcNXd"
|
||||
attributes["crossorigin"] = "anonymous"
|
||||
}
|
||||
link {
|
||||
rel = "stylesheet"
|
||||
href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
|
||||
attributes["integrity"] = "sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
|
||||
attributes["crossorigin"] = "anonymous"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class PlotTabs {
|
||||
public data class Tab(val title: String, val id: String, val content: PlotlyFragment)
|
||||
@@ -88,7 +109,7 @@ fun main() {
|
||||
val plot = Plotly.tabs {
|
||||
|
||||
tab("First") {
|
||||
plot(config = responsive) {
|
||||
plotly(config = responsive) {
|
||||
traces(trace1)
|
||||
layout {
|
||||
title = "First graph"
|
||||
@@ -98,7 +119,7 @@ fun main() {
|
||||
}
|
||||
}
|
||||
tab("Second") {
|
||||
plot(config = responsive) {
|
||||
plotly(config = responsive) {
|
||||
traces(trace2)
|
||||
layout {
|
||||
title = "Second graph"
|
||||
|
||||
@@ -2,7 +2,7 @@ import space.kscience.dataforge.meta.invoke
|
||||
import space.kscience.plotly.Plotly
|
||||
import space.kscience.plotly.models.ScatterMode
|
||||
import space.kscience.plotly.scatter
|
||||
import space.kscience.plotly.toHTML
|
||||
import space.kscience.plotly.toHTMLPage
|
||||
|
||||
Plotly.plot {
|
||||
scatter {
|
||||
@@ -26,4 +26,4 @@ Plotly.plot {
|
||||
layout {
|
||||
title = "Line and Scatter Plot"
|
||||
}
|
||||
}.toHTML()
|
||||
}.toHTMLPage()
|
||||
@@ -1,5 +1,6 @@
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
kotlin("jupyter.api")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
@@ -15,6 +16,7 @@ kscience {
|
||||
fullStack(bundleName = "js/plotly-kt.js")
|
||||
native()
|
||||
wasm()
|
||||
useSerialization()
|
||||
|
||||
commonMain {
|
||||
api(projects.visionforgeCore)
|
||||
@@ -32,6 +34,9 @@ kscience {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.processJupyterApiResources{
|
||||
libraryProducers = listOf("space.kscience.plotly.PlotlyIntegration")
|
||||
}
|
||||
|
||||
readme {
|
||||
maturity = space.kscience.gradle.Maturity.DEVELOPMENT
|
||||
|
||||
@@ -2,38 +2,69 @@
|
||||
|
||||
package space.kscience.plotly
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import space.kscience.dataforge.meta.*
|
||||
import space.kscience.dataforge.meta.descriptors.Described
|
||||
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||
import space.kscience.dataforge.meta.descriptors.node
|
||||
import space.kscience.dataforge.misc.DFBuilder
|
||||
import space.kscience.dataforge.misc.DFExperimental
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.NameToken
|
||||
import space.kscience.plotly.models.Layout
|
||||
import space.kscience.plotly.models.Trace
|
||||
import space.kscience.visionforge.*
|
||||
|
||||
@Serializable
|
||||
public class VisionOfTrace(
|
||||
traceMeta: MutableMeta,
|
||||
) : AbstractVision(traceMeta)
|
||||
|
||||
|
||||
/**
|
||||
* The main plot class. The changes to plot could be observed by attaching listener to root [meta] property.
|
||||
* The main plot class.
|
||||
*/
|
||||
@DFBuilder
|
||||
public class Plot(
|
||||
override val meta: ObservableMutableMeta = ObservableMutableMeta(),
|
||||
) : Configurable, MetaRepr, Described{
|
||||
@Serializable
|
||||
public class Plot : AbstractVision(), VisionGroup {
|
||||
|
||||
private val traces = mutableListOf<VisionOfTrace>()
|
||||
|
||||
@Transient
|
||||
private val traceFlow = MutableSharedFlow<Name>()
|
||||
|
||||
override val children: VisionChildren = object : VisionChildren {
|
||||
override val parent: Vision get() = this@Plot
|
||||
|
||||
override val keys: Collection<NameToken> get() = traces.indices.map { NameToken("trace", it.toString()) }
|
||||
|
||||
override val changes: Flow<Name> get() = traceFlow
|
||||
|
||||
override fun get(token: NameToken): VisionOfTrace? {
|
||||
return if (token.body == "trace") {
|
||||
val index = token.index?.toIntOrNull() ?: return null
|
||||
traces.getOrNull(index)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordered list ot traces in the plot
|
||||
*/
|
||||
public val data: List<Trace> by meta.listOfScheme(Trace)
|
||||
public val data: List<Trace> get() = traces.map { Trace.write(properties.root()) }
|
||||
|
||||
/**
|
||||
* Layout specification for th plot
|
||||
*/
|
||||
public val layout: Layout by meta.scheme(Layout)
|
||||
public val layout: Layout by properties.root().scheme(Layout)
|
||||
|
||||
public fun addTrace(trace: Trace) {
|
||||
meta.appendAndAttach("data", trace.meta)
|
||||
traces.add(VisionOfTrace((trace)))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,11 +86,9 @@ public class Plot(
|
||||
*/
|
||||
@UnstablePlotlyAPI
|
||||
internal fun removeTrace(index: Int) {
|
||||
meta.remove("data[$index]")
|
||||
traces.removeAt(index)
|
||||
}
|
||||
|
||||
override fun toMeta(): Meta = meta
|
||||
|
||||
override val descriptor: MetaDescriptor get() = Companion.descriptor
|
||||
|
||||
public companion object {
|
||||
@@ -73,7 +102,33 @@ public class Plot(
|
||||
}
|
||||
}
|
||||
|
||||
public fun Plot(meta: Meta): Plot = Plot(ObservableMutableMeta { update(meta) })
|
||||
public fun Plot(meta: Meta): Plot = Plot().apply {
|
||||
meta["layout"]?.let { layoutMeta -> layout { update(layoutMeta) } }
|
||||
meta.getIndexed("data").forEach { (_, data) ->
|
||||
addTrace(Trace.read(data))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add plot data change listener to each trace
|
||||
*/
|
||||
public fun Plot.onDataChange(owner: Any?, callback: (index: Int, trace: Trace, propertyName: Name) -> Unit) {
|
||||
data.forEachIndexed { index, trace ->
|
||||
trace.meta.onChange(owner) { name ->
|
||||
callback(index, trace, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove change listeners with given [owner] from all traces
|
||||
*/
|
||||
public fun Plot.removeChangeListener(owner: Any?) {
|
||||
data.forEach { trace ->
|
||||
trace.meta.removeListener(owner)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Plot.toJson(): JsonObject = buildJsonObject {
|
||||
put("layout", layout.meta.toJson())
|
||||
|
||||
@@ -4,6 +4,8 @@ import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import space.kscience.dataforge.meta.*
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.visionforge.VisionBuilder
|
||||
import space.kscience.visionforge.html.VisionOutput
|
||||
import kotlin.js.JsName
|
||||
|
||||
/**
|
||||
@@ -64,3 +66,15 @@ public class PlotlyConfig : Scheme() {
|
||||
public companion object : SchemeSpec<PlotlyConfig>(::PlotlyConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed a dynamic plotly plot in a vision
|
||||
*/
|
||||
@VisionBuilder
|
||||
public inline fun VisionOutput.plotly(
|
||||
config: PlotlyConfig = PlotlyConfig(),
|
||||
block: Plot.() -> Unit,
|
||||
): Plot {
|
||||
requirePlugin(PlotlyPlugin)
|
||||
meta = config.meta
|
||||
return Plotly.plot(block)
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package space.kscience.plotly
|
||||
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.stream.createHTML
|
||||
import space.kscience.visionforge.html.HtmlFragment
|
||||
import space.kscience.visionforge.html.appendTo
|
||||
|
||||
/**
|
||||
* A custom HTML fragment including plotly container reference
|
||||
*/
|
||||
public class PlotlyFragment(public val render: FlowContent.(renderer: PlotlyRenderer) -> Unit)
|
||||
|
||||
/**
|
||||
* A complete page including headers and title
|
||||
*/
|
||||
public data class PlotlyPage(
|
||||
val headers: Collection<HtmlFragment>,
|
||||
val fragment: PlotlyFragment,
|
||||
val title: String = "Plotly.kt",
|
||||
val renderer: PlotlyRenderer = StaticPlotlyRenderer
|
||||
) {
|
||||
public fun render(): String = createHTML().html {
|
||||
head {
|
||||
meta {
|
||||
charset = "utf-8"
|
||||
}
|
||||
title(this@PlotlyPage.title)
|
||||
headers.distinct().forEach { it.appendTo(consumer) }
|
||||
}
|
||||
body {
|
||||
fragment.render(this, renderer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun Plotly.fragment(content: FlowContent.(renderer: PlotlyRenderer) -> Unit): PlotlyFragment = PlotlyFragment(content)
|
||||
|
||||
/**
|
||||
* Create a complete page including plots
|
||||
*/
|
||||
public fun Plotly.page(
|
||||
vararg headers: HtmlFragment = arrayOf(cdnPlotlyHeader),
|
||||
title: String = "Plotly.kt",
|
||||
renderer: PlotlyRenderer = StaticPlotlyRenderer,
|
||||
content: FlowContent.(renderer: PlotlyRenderer) -> Unit
|
||||
): PlotlyPage = PlotlyPage(headers.toList(), fragment(content), title, renderer)
|
||||
|
||||
/**
|
||||
* Convert an html plot fragment to page
|
||||
*/
|
||||
public fun PlotlyFragment.toPage(
|
||||
vararg headers: HtmlFragment = arrayOf(cdnPlotlyHeader),
|
||||
title: String = "Plotly.kt",
|
||||
renderer: PlotlyRenderer = StaticPlotlyRenderer
|
||||
): PlotlyPage = PlotlyPage(headers.toList(), this, title, renderer)
|
||||
|
||||
/**
|
||||
* Convert a plot to the sigle-plot page
|
||||
*/
|
||||
public fun Plot.toPage(
|
||||
vararg headers: HtmlFragment = arrayOf(cdnPlotlyHeader),
|
||||
config: PlotlyConfig = PlotlyConfig.empty(),
|
||||
title: String = "Plotly.kt",
|
||||
renderer: PlotlyRenderer = StaticPlotlyRenderer
|
||||
): PlotlyPage = PlotlyFragment {
|
||||
plot(this@toPage, config = config, renderer = renderer)
|
||||
}.toPage(*headers, title = title)
|
||||
@@ -0,0 +1,31 @@
|
||||
package space.kscience.plotly
|
||||
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import kotlinx.serialization.modules.subclass
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.PluginFactory
|
||||
import space.kscience.dataforge.context.PluginTag
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.visionforge.Vision
|
||||
import space.kscience.visionforge.VisionPlugin
|
||||
|
||||
public class PlotlyPlugin : VisionPlugin() {
|
||||
|
||||
override val tag: PluginTag get() = Companion.tag
|
||||
|
||||
override val visionSerializersModule: SerializersModule get() = plotlySerializersModule
|
||||
|
||||
public companion object : PluginFactory<PlotlyPlugin> {
|
||||
override val tag: PluginTag = PluginTag("vision.plotly", PluginTag.DATAFORGE_GROUP)
|
||||
|
||||
override fun build(context: Context, meta: Meta): PlotlyPlugin = PlotlyPlugin()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
internal val plotlySerializersModule = SerializersModule {
|
||||
polymorphic(Vision::class) {
|
||||
subclass(Plot.serializer())
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package space.kscience.plotly
|
||||
|
||||
import kotlinx.html.*
|
||||
|
||||
public interface PlotlyRenderer {
|
||||
public fun FlowContent.renderPlot(
|
||||
plot: Plot,
|
||||
plotId: String = plot.toString(),
|
||||
config: PlotlyConfig = PlotlyConfig()
|
||||
): Plot
|
||||
}
|
||||
|
||||
public object StaticPlotlyRenderer : PlotlyRenderer {
|
||||
override fun FlowContent.renderPlot(plot: Plot, plotId: String, config: PlotlyConfig): Plot {
|
||||
div {
|
||||
id = plotId
|
||||
script {
|
||||
val tracesString = plot.data.toJsonString()
|
||||
val layoutString = plot.layout.toJsonString()
|
||||
unsafe {
|
||||
//language=JavaScript
|
||||
+"""
|
||||
Plotly.react(
|
||||
'$plotId',
|
||||
$tracesString,
|
||||
$layoutString,
|
||||
$config
|
||||
);
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
}
|
||||
return plot
|
||||
}
|
||||
}
|
||||
|
||||
public fun FlowContent.plot(
|
||||
plot: Plot,
|
||||
plotId: String = plot.toString(),
|
||||
config: PlotlyConfig = PlotlyConfig(),
|
||||
renderer: PlotlyRenderer = StaticPlotlyRenderer
|
||||
): Plot = with(renderer) {
|
||||
renderPlot(plot, plotId, config)
|
||||
}
|
||||
|
||||
public fun FlowContent.plot(
|
||||
plotId: String? = null,
|
||||
config: PlotlyConfig = PlotlyConfig(),
|
||||
renderer: PlotlyRenderer = StaticPlotlyRenderer,
|
||||
builder: Plot.() -> Unit
|
||||
): Plot {
|
||||
val plot = Plot().apply(builder)
|
||||
return plot(plot, plotId ?: plot.toString(), config, renderer)
|
||||
}
|
||||
@@ -6,17 +6,42 @@ import space.kscience.visionforge.html.HtmlFragment
|
||||
import space.kscience.visionforge.html.appendTo
|
||||
|
||||
|
||||
public val cdnPlotlyHeader: HtmlFragment = HtmlFragment{
|
||||
public val cdnPlotlyHeader: HtmlFragment = HtmlFragment {
|
||||
script {
|
||||
type = "text/javascript"
|
||||
src = Plotly.PLOTLY_CDN
|
||||
}
|
||||
}
|
||||
|
||||
public fun FlowContent.plotly(
|
||||
plot: Plot,
|
||||
config: PlotlyConfig = PlotlyConfig(),
|
||||
plotId: String = "plotly[${plot.hashCode().toUInt().toString(16)}]",
|
||||
) {
|
||||
div {
|
||||
id = plotId
|
||||
script {
|
||||
val tracesString = plot.data.toJsonString()
|
||||
val layoutString = plot.layout.toJsonString()
|
||||
unsafe {
|
||||
//language=JavaScript
|
||||
+"""
|
||||
Plotly.react(
|
||||
$plotId,
|
||||
$tracesString,
|
||||
$layoutString,
|
||||
$config
|
||||
);
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a html (including headers) string from plot
|
||||
* Create an html (including headers) string from plot
|
||||
*/
|
||||
public fun Plot.toHTML(
|
||||
public fun Plot.toHTMLPage(
|
||||
vararg headers: HtmlFragment = arrayOf(cdnPlotlyHeader),
|
||||
config: PlotlyConfig = PlotlyConfig(),
|
||||
): String = createHTML().html {
|
||||
@@ -30,9 +55,7 @@ public fun Plot.toHTML(
|
||||
}
|
||||
}
|
||||
body {
|
||||
StaticPlotlyRenderer.run {
|
||||
renderPlot(this@toHTML, this@toHTML.toString(), config)
|
||||
}
|
||||
plotly(this@toHTMLPage, config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ package space.kscience.plotly
|
||||
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import org.junit.jupiter.api.Test
|
||||
import space.kscience.dataforge.meta.ListValue
|
||||
import space.kscience.dataforge.meta.ObservableMutableMeta
|
||||
import space.kscience.plotly.models.ShapeType
|
||||
import space.kscience.plotly.models.TraceType
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@@ -29,7 +29,7 @@ class PlotSerializationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shapeSerialization(){
|
||||
fun shapeSerialization() {
|
||||
val plot = Plotly.plot {
|
||||
shape {
|
||||
type = ShapeType.rect
|
||||
@@ -12,12 +12,11 @@ import org.w3c.dom.events.MouseEvent
|
||||
import space.kscience.dataforge.meta.MetaSerializer
|
||||
import space.kscience.dataforge.meta.Scheme
|
||||
import space.kscience.dataforge.meta.Value
|
||||
import space.kscience.dataforge.names.asName
|
||||
import space.kscience.dataforge.names.firstOrNull
|
||||
import space.kscience.dataforge.names.startsWith
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.plotly.events.PlotlyEvent
|
||||
import space.kscience.plotly.events.PlotlyEventListenerType
|
||||
import space.kscience.plotly.events.PlotlyEventPoint
|
||||
import space.kscience.plotly.models.Trace
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private fun Scheme.toDynamic(): dynamic = Json.encodeToDynamic(MetaSerializer, meta)
|
||||
@@ -41,24 +40,23 @@ public fun Element.plot(plotlyConfig: PlotlyConfig = PlotlyConfig(), plot: Plot)
|
||||
|
||||
PlotlyJs.react(this, tracesData, layout, plotlyConfig.toDynamic())
|
||||
|
||||
plot.meta.onChange(this) { name ->
|
||||
if (name.startsWith(plot::layout.name.asName())) {
|
||||
PlotlyJs.relayout(this@plot, plot.layout.toDynamic())
|
||||
} else if (name.firstOrNull()?.body == "data") {
|
||||
val traceName = name.firstOrNull()!!
|
||||
val traceIndex = traceName.index?.toInt() ?: 0
|
||||
val traceData = plot.data[traceIndex].toDynamic()
|
||||
|
||||
Plotly.coordinateNames.forEach { coordinate ->
|
||||
val data = traceData[coordinate]
|
||||
if (traceData[coordinate] != null) {
|
||||
traceData[coordinate] = arrayOf(data)
|
||||
}
|
||||
}
|
||||
|
||||
PlotlyJs.restyle(this@plot, traceData, arrayOf(traceIndex))
|
||||
}
|
||||
plot.layout.meta.onChange(this){
|
||||
PlotlyJs.relayout(this@plot, plot.layout.toDynamic())
|
||||
}
|
||||
|
||||
plot.onDataChange(this){ index: Int, trace: Trace, _: Name ->
|
||||
val traceData = trace.toDynamic()
|
||||
|
||||
Plotly.coordinateNames.forEach { coordinate ->
|
||||
val data = traceData[coordinate]
|
||||
if (traceData[coordinate] != null) {
|
||||
traceData[coordinate] = arrayOf(data)
|
||||
}
|
||||
}
|
||||
|
||||
PlotlyJs.restyle(this@plot, traceData, arrayOf(index))
|
||||
}
|
||||
//TODO remove listeners on element removal
|
||||
}
|
||||
|
||||
@Deprecated("Change arguments positions", ReplaceWith("plot(plotlyConfig, plot)"))
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package space.kscience.plotly
|
||||
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import org.w3c.dom.Element
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.PluginFactory
|
||||
import space.kscience.dataforge.context.PluginTag
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.asName
|
||||
import space.kscience.plotly.plot
|
||||
import space.kscience.visionforge.Vision
|
||||
import space.kscience.visionforge.VisionPlugin
|
||||
import space.kscience.visionforge.html.ElementVisionRenderer
|
||||
import space.kscience.visionforge.html.JsVisionClient
|
||||
|
||||
public class PlotlyJSPlugin : VisionPlugin(), ElementVisionRenderer {
|
||||
public val plotly: PlotlyPlugin by require(PlotlyPlugin)
|
||||
public val visionClient: JsVisionClient by require(JsVisionClient)
|
||||
|
||||
override val tag: PluginTag get() = Companion.tag
|
||||
|
||||
override val visionSerializersModule: SerializersModule get() = plotlySerializersModule
|
||||
|
||||
override fun rateVision(vision: Vision): Int = when (vision) {
|
||||
is VisionOfPlotly -> ElementVisionRenderer.DEFAULT_RATING
|
||||
else -> ElementVisionRenderer.ZERO_RATING
|
||||
}
|
||||
|
||||
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) {
|
||||
val plot = (vision as? VisionOfPlotly)?.plot ?: error("VisionOfPlotly expected but ${vision::class} found")
|
||||
val config = PlotlyConfig.read(meta)
|
||||
element.plot(config, plot)
|
||||
}
|
||||
|
||||
override fun toString(): String = "Plotly"
|
||||
|
||||
override fun content(target: String): Map<Name, Any> = when (target) {
|
||||
ElementVisionRenderer.TYPE -> mapOf("plotly".asName() to this)
|
||||
else -> super.content(target)
|
||||
}
|
||||
|
||||
public companion object : PluginFactory<PlotlyJSPlugin> {
|
||||
override val tag: PluginTag = PluginTag("vision.plotly.js", PluginTag.DATAFORGE_GROUP)
|
||||
|
||||
override fun build(context: Context, meta: Meta): PlotlyJSPlugin = PlotlyJSPlugin()
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package space.kscience.plotly
|
||||
|
||||
import kotlinx.html.div
|
||||
import kotlinx.html.stream.createHTML
|
||||
import kotlinx.html.style
|
||||
import org.jetbrains.kotlinx.jupyter.api.HTML
|
||||
import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
|
||||
import org.jetbrains.kotlinx.jupyter.api.libraries.resources
|
||||
import space.kscience.plotly.models.Trace
|
||||
import space.kscience.visionforge.html.HtmlFragment
|
||||
|
||||
public object PlotlyJupyterConfiguration {
|
||||
public var legacyMode: Boolean = false
|
||||
|
||||
/**
|
||||
* Switch plotly renderer to the legacy notebook mode (Jupyter classic)
|
||||
*/
|
||||
public fun notebook(): HtmlFragment {
|
||||
legacyMode = true
|
||||
return HtmlFragment {
|
||||
div {
|
||||
style = "color: blue;"
|
||||
+"Plotly notebook integration switched into the notebook mode."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun lab(): HtmlFragment {
|
||||
legacyMode = false
|
||||
return HtmlFragment {
|
||||
div {
|
||||
style = "color: blue;"
|
||||
+"Plotly notebook integration switched into the lab mode."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global plotly jupyter configuration
|
||||
*/
|
||||
public val Plotly.jupyter: PlotlyJupyterConfiguration
|
||||
get() = PlotlyJupyterConfiguration
|
||||
|
||||
public class PlotlyIntegration : JupyterIntegration() {
|
||||
|
||||
private fun renderPlot(plot: Plot) = if (PlotlyJupyterConfiguration.legacyMode) {
|
||||
HTML(plot.toHTMLPage(), true)
|
||||
} else {
|
||||
HTML(createHTML().div { plotly(plot, PlotlyConfig { responsive = true }) }, false)
|
||||
}
|
||||
|
||||
|
||||
override fun Builder.onLoaded() {
|
||||
|
||||
resources {
|
||||
js("plotly-kt") {
|
||||
url("https://cdn.plot.ly/plotly-2.29.1.min.js")
|
||||
classPath("js/plotly-kt.js")
|
||||
}
|
||||
}
|
||||
|
||||
repositories("https://repo.kotlin.link")
|
||||
|
||||
import(
|
||||
"space.kscience.plotly.*",
|
||||
"space.kscience.plotly.models.*",
|
||||
"space.kscience.dataforge.meta.*",
|
||||
"kotlinx.html.*"
|
||||
)
|
||||
|
||||
import("space.kscience.plotly.jupyter")
|
||||
|
||||
render<HtmlFragment> {
|
||||
HTML(it.toString())
|
||||
}
|
||||
|
||||
render<Plot> { plot ->
|
||||
renderPlot(plot)
|
||||
}
|
||||
|
||||
render<Trace> { trace ->
|
||||
renderPlot(
|
||||
Plotly.plot {
|
||||
traces(trace)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package space.kscience.plotly
|
||||
|
||||
import kotlinx.html.link
|
||||
import kotlinx.html.script
|
||||
import space.kscience.visionforge.html.HtmlFragment
|
||||
|
||||
|
||||
//public fun localBootstrap(basePath: Path) = HtmlFragment {
|
||||
// script {
|
||||
// type = "text/javascript"
|
||||
// src = checkOrStoreFile(
|
||||
// basePath,
|
||||
// Path.of(assetsDirectory + jQueryPath),
|
||||
// jQueryPath
|
||||
// ).toString()
|
||||
// }
|
||||
// script {
|
||||
// type = "text/javascript"
|
||||
// src = checkOrStoreFile(
|
||||
// basePath,
|
||||
// Path.of(assetsDirectory + bootstrapJsPath),
|
||||
// bootstrapJsPath
|
||||
// ).toString()
|
||||
// }
|
||||
// link {
|
||||
// rel = "stylesheet"
|
||||
// href = checkOrStoreFile(
|
||||
// basePath,
|
||||
// Path.of(assetsDirectory + bootstrapCssPath),
|
||||
// bootstrapCssPath
|
||||
// ).toString()
|
||||
// }
|
||||
//}
|
||||
|
||||
public val cdnBootstrap: HtmlFragment = HtmlFragment {
|
||||
script {
|
||||
src = "https://code.jquery.com/jquery-3.5.1.slim.min.js"
|
||||
integrity = "sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
|
||||
attributes["crossorigin"] = "anonymous"
|
||||
}
|
||||
script {
|
||||
src = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.bundle.min.js"
|
||||
integrity = "sha384-1CmrxMRARb6aLqgBO7yyAxTOQE2AKb9GfXnEo760AUcUmFx3ibVJJAzGytlQcNXd"
|
||||
attributes["crossorigin"] = "anonymous"
|
||||
}
|
||||
link {
|
||||
rel = "stylesheet"
|
||||
href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
|
||||
attributes["integrity"] = "sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
|
||||
attributes["crossorigin"] = "anonymous"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package space.kscience.plotly
|
||||
|
||||
import kotlinx.html.FlowContent
|
||||
import space.kscience.visionforge.html.HtmlFragment
|
||||
import java.awt.Desktop
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
@@ -51,49 +49,49 @@ public fun Plot.makeFile(
|
||||
) {
|
||||
val actualFile = path ?: Files.createTempFile("tempPlot", ".html")
|
||||
Files.createDirectories(actualFile.parent)
|
||||
Files.writeString(actualFile, toHTML(inferPlotlyHeader(path, resourceLocation), config = config))
|
||||
Files.writeString(actualFile, toHTMLPage(inferPlotlyHeader(path, resourceLocation), config = config))
|
||||
if (show) {
|
||||
Desktop.getDesktop().browse(actualFile.toFile().toURI())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The same as [Plot.makeFile].
|
||||
*/
|
||||
public fun PlotlyFragment.makeFile(
|
||||
path: Path? = null,
|
||||
show: Boolean = true,
|
||||
title: String = "Plotly.kt",
|
||||
resourceLocation: ResourceLocation = ResourceLocation.LOCAL,
|
||||
additionalHeaders: List<HtmlFragment> = emptyList(),
|
||||
) {
|
||||
toPage(
|
||||
title = title,
|
||||
headers = (additionalHeaders + inferPlotlyHeader(path, resourceLocation)).toTypedArray()
|
||||
).makeFile(path, show)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export a page html to a file.
|
||||
*/
|
||||
public fun PlotlyPage.makeFile(path: Path? = null, show: Boolean = true) {
|
||||
val actualFile = path ?: Files.createTempFile("tempPlot", ".html")
|
||||
Files.createDirectories(actualFile.parent)
|
||||
Files.writeString(actualFile, render())
|
||||
if (show) {
|
||||
Desktop.getDesktop().browse(actualFile.toFile().toURI())
|
||||
}
|
||||
}
|
||||
|
||||
public fun Plotly.display(pageBuilder: FlowContent.(renderer: PlotlyRenderer) -> Unit): Unit =
|
||||
fragment(pageBuilder).makeFile(null, true)
|
||||
///**
|
||||
// * The same as [Plot.makeFile].
|
||||
// */
|
||||
//public fun PlotlyFragment.makeFile(
|
||||
// path: Path? = null,
|
||||
// show: Boolean = true,
|
||||
// title: String = "Plotly.kt",
|
||||
// resourceLocation: ResourceLocation = ResourceLocation.LOCAL,
|
||||
// additionalHeaders: List<HtmlFragment> = emptyList(),
|
||||
//) {
|
||||
// toPage(
|
||||
// title = title,
|
||||
// headers = (additionalHeaders + inferPlotlyHeader(path, resourceLocation)).toTypedArray()
|
||||
// ).makeFile(path, show)
|
||||
//}
|
||||
//
|
||||
//
|
||||
///**
|
||||
// * Export a page html to a file.
|
||||
// */
|
||||
//public fun PlotlyPage.makeFile(path: Path? = null, show: Boolean = true) {
|
||||
// val actualFile = path ?: Files.createTempFile("tempPlot", ".html")
|
||||
// Files.createDirectories(actualFile.parent)
|
||||
// Files.writeString(actualFile, render())
|
||||
// if (show) {
|
||||
// Desktop.getDesktop().browse(actualFile.toFile().toURI())
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//public fun Plotly.display(pageBuilder: FlowContent.(renderer: PlotlyRenderer) -> Unit): Unit =
|
||||
// fragment(pageBuilder).makeFile(null, true)
|
||||
|
||||
/**
|
||||
* Select a file to save plot to using Swing form.
|
||||
*/
|
||||
@UnstablePlotlyAPI
|
||||
public fun selectFile(filter: FileNameExtensionFilter? = null): Path? {
|
||||
public fun Plotly.selectFile(filter: FileNameExtensionFilter? = null): Path? {
|
||||
val fileChooser = JFileChooser()
|
||||
fileChooser.dialogTitle = "Specify a file to save"
|
||||
if (filter != null) {
|
||||
|
||||
@@ -16,7 +16,7 @@ public fun Plot.makeFile(
|
||||
config: PlotlyConfig = PlotlyConfig(),
|
||||
) {
|
||||
FileSystem.SYSTEM.write(path, true) {
|
||||
writeUtf8(toHTML(cdnPlotlyHeader, config = config))
|
||||
writeUtf8(toHTMLPage(cdnPlotlyHeader, config = config))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# Module plotlykt-jupyter
|
||||
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `space.kscience:plotlykt-jupyter:0.7.1`.
|
||||
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
repositories {
|
||||
maven("https://repo.kotlin.link")
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("space.kscience:plotlykt-jupyter:0.7.1")
|
||||
}
|
||||
```
|
||||
@@ -1,18 +0,0 @@
|
||||
public final class space/kscience/plotly/PlotlyIntegration : org/jetbrains/kotlinx/jupyter/api/libraries/JupyterIntegration, space/kscience/plotly/PlotlyRenderer {
|
||||
public fun <init> ()V
|
||||
public fun onLoaded (Lorg/jetbrains/kotlinx/jupyter/api/libraries/JupyterIntegration$Builder;)V
|
||||
public fun renderPlot (Lkotlinx/html/FlowContent;Lspace/kscience/plotly/Plot;Ljava/lang/String;Lspace/kscience/plotly/PlotlyConfig;)Lspace/kscience/plotly/Plot;
|
||||
}
|
||||
|
||||
public final class space/kscience/plotly/PlotlyIntegrationKt {
|
||||
public static final fun getJupyter (Lspace/kscience/plotly/Plotly;)Lspace/kscience/plotly/PlotlyJupyterConfiguration;
|
||||
}
|
||||
|
||||
public final class space/kscience/plotly/PlotlyJupyterConfiguration {
|
||||
public static final field INSTANCE Lspace/kscience/plotly/PlotlyJupyterConfiguration;
|
||||
public final fun getLegacyMode ()Z
|
||||
public final fun lab ()Lspace/kscience/plotly/PlotlyHtmlFragment;
|
||||
public final fun notebook ()Lspace/kscience/plotly/PlotlyHtmlFragment;
|
||||
public final fun setLegacyMode (Z)V
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
kotlin("jupyter.api")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
val dataforgeVersion: String by rootProject.extra
|
||||
|
||||
kscience{
|
||||
jvm()
|
||||
jvmMain{
|
||||
api(projects.plotly.plotlyktCore)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.processJupyterApiResources{
|
||||
libraryProducers = listOf("space.kscience.plotly.PlotlyIntegration")
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
package space.kscience.plotly
|
||||
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.stream.createHTML
|
||||
import org.jetbrains.kotlinx.jupyter.api.HTML
|
||||
import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
|
||||
import org.jetbrains.kotlinx.jupyter.api.libraries.resources
|
||||
import space.kscience.visionforge.html.HtmlFragment
|
||||
|
||||
public object PlotlyJupyterConfiguration {
|
||||
public var legacyMode: Boolean = false
|
||||
|
||||
/**
|
||||
* Switch plotly renderer to the legacy notebook mode (Jupyter classic)
|
||||
*/
|
||||
public fun notebook(): HtmlFragment {
|
||||
legacyMode = true
|
||||
return HtmlFragment {
|
||||
div {
|
||||
style = "color: blue;"
|
||||
+"Plotly notebook integration switched into the notebook mode."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun lab(): HtmlFragment {
|
||||
legacyMode = false
|
||||
return HtmlFragment {
|
||||
div {
|
||||
style = "color: blue;"
|
||||
+"Plotly notebook integration switched into the lab mode."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global plotly jupyter configuration
|
||||
*/
|
||||
public val Plotly.jupyter: PlotlyJupyterConfiguration
|
||||
get() = PlotlyJupyterConfiguration
|
||||
|
||||
public class PlotlyIntegration : JupyterIntegration(), PlotlyRenderer {
|
||||
override fun FlowContent.renderPlot(plot: Plot, plotId: String, config: PlotlyConfig): Plot {
|
||||
div {
|
||||
id = plotId
|
||||
script {
|
||||
unsafe {
|
||||
//language=JavaScript
|
||||
+"""
|
||||
if(typeof Plotly !== "undefined"){
|
||||
Plotly.react(
|
||||
'$plotId',
|
||||
${plot.data.toJsonString()},
|
||||
${plot.layout.toJsonString()},
|
||||
${config.toJsonString()}
|
||||
);
|
||||
} else {
|
||||
console.error("Plotly not loaded")
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
}
|
||||
return plot
|
||||
}
|
||||
|
||||
private fun renderPlot(plot: Plot): String = createHTML().div {
|
||||
plot(plot, config = PlotlyConfig {
|
||||
responsive = true
|
||||
}, renderer = this@PlotlyIntegration)
|
||||
}
|
||||
|
||||
private fun renderFragment(fragment: PlotlyFragment): String = createHTML().div {
|
||||
with(fragment) {
|
||||
render(this@PlotlyIntegration)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderPage(page: PlotlyPage): String = page.copy(renderer = this@PlotlyIntegration).render()
|
||||
|
||||
override fun Builder.onLoaded() {
|
||||
|
||||
resources {
|
||||
js("plotly-kt") {
|
||||
url("https://cdn.plot.ly/plotly-2.29.1.min.js")
|
||||
classPath("js/plotly-kt.js")
|
||||
}
|
||||
}
|
||||
|
||||
repositories("https://repo.kotlin.link")
|
||||
|
||||
import(
|
||||
"space.kscience.plotly.*",
|
||||
"space.kscience.plotly.models.*",
|
||||
"space.kscience.dataforge.meta.*",
|
||||
"kotlinx.html.*"
|
||||
)
|
||||
|
||||
import("space.kscience.plotly.jupyter")
|
||||
|
||||
render<HtmlFragment> {
|
||||
HTML(it.toString())
|
||||
}
|
||||
|
||||
val renderer = this@PlotlyIntegration
|
||||
|
||||
render<Plot> { plot ->
|
||||
if (PlotlyJupyterConfiguration.legacyMode) {
|
||||
HTML(
|
||||
Plotly.page(renderer = renderer) {
|
||||
plot(renderer = renderer, plot = plot)
|
||||
}.render(),
|
||||
true
|
||||
)
|
||||
} else {
|
||||
HTML(renderPlot(plot), false)
|
||||
}
|
||||
}
|
||||
|
||||
render<PlotlyFragment> { fragment ->
|
||||
if (PlotlyJupyterConfiguration.legacyMode) {
|
||||
HTML(
|
||||
Plotly.page(renderer = renderer) { renderer ->
|
||||
fragment.render(this, renderer)
|
||||
}.render(), true
|
||||
)
|
||||
} else {
|
||||
HTML(renderFragment(fragment), false)
|
||||
}
|
||||
}
|
||||
|
||||
render<PlotlyPage> {
|
||||
HTML(renderPage(it), true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@ val trace1 = Trace(x1, y1) { name = "sin" }
|
||||
val trace2 = Trace(x1, y2) { name = "cos" }
|
||||
|
||||
|
||||
plot {
|
||||
plotly {
|
||||
traces(trace1, trace2)
|
||||
layout {
|
||||
title = "The plot above"
|
||||
@@ -18,7 +18,7 @@ hr()
|
||||
h1 { +"A custom separator" }
|
||||
hr()
|
||||
div {
|
||||
plot {
|
||||
plotly {
|
||||
traces(trace1, trace2)
|
||||
layout {
|
||||
title = "The plot below"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# Module plotlykt-server
|
||||
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `space.kscience:plotlykt-server:0.7.1`.
|
||||
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
repositories {
|
||||
maven("https://repo.kotlin.link")
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("space.kscience:plotlykt-server:0.7.1")
|
||||
}
|
||||
```
|
||||
@@ -1,100 +0,0 @@
|
||||
public final class space/kscience/plotly/server/MetaChangeCollector {
|
||||
public fun <init> ()V
|
||||
public final fun collect (Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/meta/Meta;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public final fun read (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class space/kscience/plotly/server/MetaChangeCollectorKt {
|
||||
public static final fun collectUpdates (Lspace/kscience/plotly/Plot;Ljava/lang/String;Lkotlinx/coroutines/CoroutineScope;I)Lkotlinx/coroutines/flow/Flow;
|
||||
}
|
||||
|
||||
public final class space/kscience/plotly/server/PlotlyServer : kotlinx/coroutines/CoroutineScope, space/kscience/dataforge/meta/Configurable {
|
||||
public static final field Companion Lspace/kscience/plotly/server/PlotlyServer$Companion;
|
||||
public static final field DEFAULT_PAGE Ljava/lang/String;
|
||||
public final fun getApplication ()Lio/ktor/server/application/Application;
|
||||
public fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext;
|
||||
public final fun getDataSourceHost ()Ljava/lang/String;
|
||||
public final fun getDataSourcePort ()Ljava/lang/Integer;
|
||||
public final fun getEmbedData ()Z
|
||||
public synthetic fun getMeta ()Lspace/kscience/dataforge/meta/MutableMeta;
|
||||
public fun getMeta ()Lspace/kscience/dataforge/meta/ObservableMutableMeta;
|
||||
public final fun getUpdateInterval ()I
|
||||
public final fun getUpdateMode ()Lspace/kscience/plotly/server/PlotlyUpdateMode;
|
||||
public final fun header (Lkotlin/jvm/functions/Function1;)V
|
||||
public final fun page (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;)V
|
||||
public final fun page (Lspace/kscience/plotly/PlotlyFragment;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V
|
||||
public static synthetic fun page$default (Lspace/kscience/plotly/server/PlotlyServer;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
|
||||
public static synthetic fun page$default (Lspace/kscience/plotly/server/PlotlyServer;Lspace/kscience/plotly/PlotlyFragment;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)V
|
||||
public final fun setDataSourceHost (Ljava/lang/String;)V
|
||||
public final fun setDataSourcePort (Ljava/lang/Integer;)V
|
||||
public final fun setEmbedData (Z)V
|
||||
public final fun setUpdateInterval (I)V
|
||||
public final fun setUpdateMode (Lspace/kscience/plotly/server/PlotlyUpdateMode;)V
|
||||
}
|
||||
|
||||
public final class space/kscience/plotly/server/PlotlyServer$Companion {
|
||||
public final fun getUPDATE_INTERVAL_KEY ()Lspace/kscience/dataforge/names/Name;
|
||||
public final fun getUPDATE_MODE_KEY ()Lspace/kscience/dataforge/names/Name;
|
||||
}
|
||||
|
||||
public final class space/kscience/plotly/server/PlotlyServerConfiguration : space/kscience/dataforge/meta/Scheme {
|
||||
public static final field INSTANCE Lspace/kscience/plotly/server/PlotlyServerConfiguration;
|
||||
public final fun getLegacyMode ()Z
|
||||
public final fun getPort ()I
|
||||
public final fun getUpdateInterval ()I
|
||||
public final fun notebook ()Lspace/kscience/plotly/PlotlyHtmlFragment;
|
||||
public final fun setLegacyMode (Z)V
|
||||
public final fun setPort (I)V
|
||||
public final fun setUpdateInterval (I)V
|
||||
}
|
||||
|
||||
public final class space/kscience/plotly/server/PlotlyServerIntegration : org/jetbrains/kotlinx/jupyter/api/libraries/JupyterIntegration {
|
||||
public fun <init> ()V
|
||||
public final fun isServerStarted ()Z
|
||||
public fun onLoaded (Lorg/jetbrains/kotlinx/jupyter/api/libraries/JupyterIntegration$Builder;)V
|
||||
}
|
||||
|
||||
public final class space/kscience/plotly/server/PlotlyServerIntegrationKt {
|
||||
public static final fun getJupyter (Lspace/kscience/plotly/Plotly;)Lspace/kscience/plotly/server/PlotlyServerConfiguration;
|
||||
}
|
||||
|
||||
public final class space/kscience/plotly/server/PlotlyServerKt {
|
||||
public static final fun close (Lio/ktor/server/engine/ApplicationEngine;)V
|
||||
public static final fun plot (Lspace/kscience/plotly/server/PlotlyServer;Ljava/lang/String;Lspace/kscience/plotly/PlotlyConfig;Lkotlin/jvm/functions/Function1;)V
|
||||
public static synthetic fun plot$default (Lspace/kscience/plotly/server/PlotlyServer;Ljava/lang/String;Lspace/kscience/plotly/PlotlyConfig;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
|
||||
public static final fun plotlyModule (Lio/ktor/server/application/Application;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lspace/kscience/plotly/server/PlotlyServer;
|
||||
public static synthetic fun plotlyModule$default (Lio/ktor/server/application/Application;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lspace/kscience/plotly/server/PlotlyServer;
|
||||
public static final fun pullUpdates (Lspace/kscience/plotly/server/PlotlyServer;I)Lspace/kscience/plotly/server/PlotlyServer;
|
||||
public static synthetic fun pullUpdates$default (Lspace/kscience/plotly/server/PlotlyServer;IILjava/lang/Object;)Lspace/kscience/plotly/server/PlotlyServer;
|
||||
public static final fun pushUpdates (Lspace/kscience/plotly/server/PlotlyServer;I)Lspace/kscience/plotly/server/PlotlyServer;
|
||||
public static synthetic fun pushUpdates$default (Lspace/kscience/plotly/server/PlotlyServer;IILjava/lang/Object;)Lspace/kscience/plotly/server/PlotlyServer;
|
||||
public static final fun serve (Lspace/kscience/plotly/Plotly;Lkotlinx/coroutines/CoroutineScope;Ljava/lang/String;ILkotlin/jvm/functions/Function1;)Lio/ktor/server/engine/ApplicationEngine;
|
||||
public static synthetic fun serve$default (Lspace/kscience/plotly/Plotly;Lkotlinx/coroutines/CoroutineScope;Ljava/lang/String;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/ktor/server/engine/ApplicationEngine;
|
||||
public static final fun show (Lio/ktor/server/engine/ApplicationEngine;)V
|
||||
}
|
||||
|
||||
public final class space/kscience/plotly/server/PlotlyUpdateMode : java/lang/Enum {
|
||||
public static final field NONE Lspace/kscience/plotly/server/PlotlyUpdateMode;
|
||||
public static final field PULL Lspace/kscience/plotly/server/PlotlyUpdateMode;
|
||||
public static final field PUSH Lspace/kscience/plotly/server/PlotlyUpdateMode;
|
||||
public static fun getEntries ()Lkotlin/enums/EnumEntries;
|
||||
public static fun valueOf (Ljava/lang/String;)Lspace/kscience/plotly/server/PlotlyUpdateMode;
|
||||
public static fun values ()[Lspace/kscience/plotly/server/PlotlyUpdateMode;
|
||||
}
|
||||
|
||||
public abstract class space/kscience/plotly/server/Update {
|
||||
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public final fun getId ()Ljava/lang/String;
|
||||
public abstract fun toJson ()Lkotlinx/serialization/json/JsonObject;
|
||||
}
|
||||
|
||||
public final class space/kscience/plotly/server/Update$Layout : space/kscience/plotly/server/Update {
|
||||
public fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;)V
|
||||
public fun toJson ()Lkotlinx/serialization/json/JsonObject;
|
||||
}
|
||||
|
||||
public final class space/kscience/plotly/server/Update$Trace : space/kscience/plotly/server/Update {
|
||||
public fun <init> (Ljava/lang/String;ILspace/kscience/dataforge/meta/Meta;)V
|
||||
public fun toJson ()Lkotlinx/serialization/json/JsonObject;
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import space.kscience.gradle.KScienceVersions
|
||||
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
kotlin("jupyter.api")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
val dataforgeVersion: String by rootProject.extra
|
||||
val ktorVersion = KScienceVersions.ktorVersion
|
||||
|
||||
kscience{
|
||||
jvm()
|
||||
useCoroutines()
|
||||
commonMain{
|
||||
api(projects.plotly.plotlyktCore)
|
||||
api("io.ktor:ktor-server-cio:$ktorVersion")
|
||||
api("io.ktor:ktor-server-html-builder:$ktorVersion")
|
||||
api("io.ktor:ktor-server-websockets:$ktorVersion")
|
||||
api("io.ktor:ktor-server-cors:$ktorVersion")
|
||||
api("space.kscience:dataforge-context:$dataforgeVersion")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.processJupyterApiResources{
|
||||
libraryProducers = listOf("space.kscience.plotly.server.PlotlyServerIntegration")
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package space.kscience.plotly.server
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import space.kscience.dataforge.meta.*
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.asName
|
||||
import space.kscience.plotly.Plot
|
||||
|
||||
|
||||
/**
|
||||
* A change collector that combines all emitted configuration changes until read, than drops all collected changes
|
||||
* and starts new batch.
|
||||
*/
|
||||
public class MetaChangeCollector {
|
||||
private val mutex = Mutex()
|
||||
private var state = MutableMeta()
|
||||
|
||||
public suspend fun collect(name: Name, newItem: Meta?) {
|
||||
mutex.withLock {
|
||||
state[name] = newItem
|
||||
}
|
||||
}
|
||||
|
||||
public suspend fun read(): Meta {
|
||||
return if (!state.isEmpty()) {
|
||||
mutex.withLock {
|
||||
state.seal().also {
|
||||
state = MutableMeta()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Meta.EMPTY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ObservableMeta.collectChanges(scope: CoroutineScope): MetaChangeCollector = MetaChangeCollector().apply {
|
||||
onChange(this) { name ->
|
||||
scope.launch {
|
||||
collect(name, get(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ObservableMeta.flowChanges(scope: CoroutineScope, updateInterval: Int): Flow<Meta> {
|
||||
val collector = collectChanges(scope)
|
||||
return flow {
|
||||
while (true) {
|
||||
delay(updateInterval.toLong())
|
||||
val meta = collector.read()
|
||||
if (!meta.isEmpty()) {
|
||||
emit(meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun Plot.collectUpdates(
|
||||
plotId: String,
|
||||
scope: CoroutineScope,
|
||||
updateInterval: Int,
|
||||
): Flow<Update> = meta.flowChanges(scope, updateInterval).transform { change ->
|
||||
change["layout"]?.let{ emit(Update.Layout(plotId, it))}
|
||||
change.getIndexed("data".asName()).forEach { (index, metaItem) ->
|
||||
emit(Update.Trace(plotId, index?.toInt() ?: 0, metaItem))
|
||||
}
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
package space.kscience.plotly.server
|
||||
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.engine.ApplicationEngine
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.html.respondHtml
|
||||
import io.ktor.server.http.content.staticResources
|
||||
import io.ktor.server.plugins.cors.routing.CORS
|
||||
import io.ktor.server.plugins.origin
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.response.respondText
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.websocket.WebSockets
|
||||
import io.ktor.server.websocket.webSocket
|
||||
import io.ktor.websocket.Frame
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.html.*
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import space.kscience.dataforge.meta.*
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.plotly.*
|
||||
import space.kscience.plotly.server.PlotlyServer.Companion.DEFAULT_PAGE
|
||||
import space.kscience.visionforge.html.HtmlFragment
|
||||
import space.kscience.visionforge.html.appendTo
|
||||
import java.awt.Desktop
|
||||
import java.net.URI
|
||||
import kotlin.collections.set
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
public enum class PlotlyUpdateMode {
|
||||
NONE,
|
||||
PUSH,
|
||||
PULL
|
||||
}
|
||||
|
||||
internal class ServerPlotlyRenderer(
|
||||
val baseUrl: Url,
|
||||
val updateMode: PlotlyUpdateMode,
|
||||
val updateInterval: Int,
|
||||
val embedData: Boolean,
|
||||
val plotCallback: (plotId: String, plot: Plot) -> Unit,
|
||||
) : PlotlyRenderer {
|
||||
override fun FlowContent.renderPlot(plot: Plot, plotId: String, config: PlotlyConfig): Plot {
|
||||
plotCallback(plotId, plot)
|
||||
div {
|
||||
id = plotId
|
||||
|
||||
val dataUrl = URLBuilder(baseUrl).apply {
|
||||
encodedPath = baseUrl.encodedPath + "/data/$plotId"
|
||||
}.build()
|
||||
script {
|
||||
if (embedData) {
|
||||
unsafe {
|
||||
//language=JavaScript
|
||||
+"""
|
||||
|
||||
plotlyConnect.makePlot(
|
||||
'$plotId',
|
||||
${plot.data.toJsonString()},
|
||||
${plot.layout.toJsonString()},
|
||||
$config
|
||||
);
|
||||
|
||||
|
||||
""".trimIndent()
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
//language=JavaScript
|
||||
+"""
|
||||
|
||||
plotlyConnect.createPlotFrom('$plotId','$dataUrl', $config);
|
||||
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
// starting plot updates if required
|
||||
when (updateMode) {
|
||||
PlotlyUpdateMode.PUSH -> {
|
||||
val wsUrl = URLBuilder(baseUrl).apply {
|
||||
protocol = URLProtocol.WS
|
||||
encodedPath = baseUrl.encodedPath + "/ws/$plotId"
|
||||
}.build()
|
||||
unsafe {
|
||||
//language=JavaScript
|
||||
+"""
|
||||
|
||||
plotlyConnect.startPush('$plotId', '$wsUrl');
|
||||
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
PlotlyUpdateMode.PULL -> {
|
||||
unsafe {
|
||||
//language=JavaScript
|
||||
+"""
|
||||
|
||||
plotlyConnect.startPull('$plotId', '$dataUrl', ${updateInterval});
|
||||
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
PlotlyUpdateMode.NONE -> {
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return plot
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class PlotlyServer internal constructor(
|
||||
private val routing: Routing, private val rootRoute: String,
|
||||
) : Configurable, CoroutineScope {
|
||||
|
||||
override val coroutineContext: CoroutineContext get() = routing.application.coroutineContext
|
||||
|
||||
override val meta: ObservableMutableMeta = ObservableMutableMeta()
|
||||
public var updateMode: PlotlyUpdateMode by meta.enum(PlotlyUpdateMode.PUSH, key = UPDATE_MODE_KEY)
|
||||
public var updateInterval: Int by meta.int(300, key = UPDATE_INTERVAL_KEY)
|
||||
public var embedData: Boolean by meta.boolean(false)
|
||||
|
||||
/**
|
||||
* An override for data (pull/push) service host. By default uses request host
|
||||
*/
|
||||
public var dataSourceHost: String? by meta.string()
|
||||
|
||||
/**
|
||||
* An override for data (pull/push) service port. By default uses request port
|
||||
*/
|
||||
public var dataSourcePort: Int? by meta.int()
|
||||
|
||||
internal val root by lazy { routing.createRouteFromPath(rootRoute) }
|
||||
|
||||
/**
|
||||
* a list of headers that should be applied to all pages
|
||||
*/
|
||||
private val globalHeaders: ArrayList<HtmlFragment> = ArrayList<HtmlFragment>()
|
||||
|
||||
public fun header(block: TagConsumer<*>.() -> Unit) {
|
||||
globalHeaders.add(HtmlFragment(block))
|
||||
}
|
||||
|
||||
internal fun Route.servePlotData(plots: Map<String, Plot>) {
|
||||
//Update websocket
|
||||
webSocket("ws/{id}") {
|
||||
val plotId: String = call.parameters["id"] ?: error("Plot id not defined")
|
||||
|
||||
application.log.debug("Opened server socket for $plotId")
|
||||
|
||||
val plot = plots[plotId] ?: error("Plot with id='$plotId' not registered")
|
||||
|
||||
try {
|
||||
plot.collectUpdates(plotId, this, updateInterval).collect { update: Update ->
|
||||
val json = update.toJson()
|
||||
outgoing.send(Frame.Text(JsonObject(json).toString()))
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
application.log.debug("Closed server socket for $plotId")
|
||||
}
|
||||
}
|
||||
//Plots in their json representation
|
||||
get("data/{id}") {
|
||||
val id: String = call.parameters["id"] ?: error("Plot id not defined")
|
||||
|
||||
val plot: Plot? = plots[id]
|
||||
if (plot == null) {
|
||||
call.respond(HttpStatusCode.NotFound, "Plot with id = $id not found")
|
||||
} else {
|
||||
call.respondText(
|
||||
plot.toJsonString(),
|
||||
contentType = ContentType.Application.Json,
|
||||
status = HttpStatusCode.OK
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun page(
|
||||
plotlyFragment: PlotlyFragment,
|
||||
route: String = DEFAULT_PAGE,
|
||||
title: String = "Plotly server page '$route'",
|
||||
headers: List<HtmlFragment> = emptyList(),
|
||||
) {
|
||||
root.apply {
|
||||
val plots = HashMap<String, Plot>()
|
||||
route(route) {
|
||||
servePlotData(plots)
|
||||
//filled pages
|
||||
get {
|
||||
val origin = call.request.origin
|
||||
val url = URLBuilder().apply {
|
||||
protocol = URLProtocol.createOrDefault(origin.scheme)
|
||||
//workaround for https://github.com/ktorio/ktor/issues/1663
|
||||
host = dataSourceHost
|
||||
?: if (origin.serverHost.startsWith("0:")) "[${origin.serverHost}]" else origin.serverHost
|
||||
port = dataSourcePort ?: origin.serverPort
|
||||
encodedPath = origin.uri
|
||||
}.build()
|
||||
call.respondHtml {
|
||||
// val normalizedRoute = if (rootRoute.endsWith("/")) {
|
||||
// rootRoute
|
||||
// } else {
|
||||
// "$rootRoute/"
|
||||
// }
|
||||
|
||||
head {
|
||||
meta {
|
||||
charset = "utf-8"
|
||||
(globalHeaders + headers).forEach {
|
||||
it.appendTo(consumer)
|
||||
}
|
||||
plotlyKtHeader.appendTo(consumer)
|
||||
}
|
||||
title(title)
|
||||
}
|
||||
body {
|
||||
val container = ServerPlotlyRenderer(
|
||||
url, updateMode, updateInterval, embedData
|
||||
) { plotId, plot ->
|
||||
plots[plotId] = plot
|
||||
}
|
||||
with(plotlyFragment) {
|
||||
render(container)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun page(
|
||||
route: String = DEFAULT_PAGE,
|
||||
title: String = "Plotly server page '$route'",
|
||||
headers: List<HtmlFragment> = emptyList(),
|
||||
content: FlowContent.(renderer: PlotlyRenderer) -> Unit,
|
||||
) {
|
||||
page(PlotlyFragment(content), route, title, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes the Ktor application environment to internal logic
|
||||
*/
|
||||
public val application: Application get() = routing.application
|
||||
|
||||
public companion object {
|
||||
public const val DEFAULT_PAGE: String = "/"
|
||||
public val UPDATE_MODE_KEY: Name = Name.parse("update.mode")
|
||||
public val UPDATE_INTERVAL_KEY: Name = Name.parse("update.interval")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attach plotly application to given server
|
||||
*/
|
||||
public fun Application.plotlyModule(route: String = DEFAULT_PAGE, block: PlotlyServer.() -> Unit = {}): PlotlyServer {
|
||||
if (pluginOrNull(WebSockets) == null) {
|
||||
install(WebSockets)
|
||||
}
|
||||
|
||||
routing {
|
||||
route(route) {
|
||||
staticResources("js", "js")
|
||||
}
|
||||
}
|
||||
|
||||
// val root: Route = feature(Routing).createRouteFromPath(route)
|
||||
return PlotlyServer(plugin(Routing), route).apply(block)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configure server to start sending updates in push mode. Does not affect loaded pages
|
||||
*/
|
||||
public fun PlotlyServer.pushUpdates(interval: Int = 100): PlotlyServer = apply {
|
||||
updateMode = PlotlyUpdateMode.PUSH
|
||||
updateInterval = interval
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure client to request regular updates from server. Pull updates are more expensive than push updates since
|
||||
* they contain the full plot data and server can't decide what to send.
|
||||
*/
|
||||
public fun PlotlyServer.pullUpdates(interval: Int = 500): PlotlyServer = apply {
|
||||
updateMode = PlotlyUpdateMode.PULL
|
||||
updateInterval = interval
|
||||
}
|
||||
|
||||
/**
|
||||
* Start static server (updates via reload)
|
||||
*/
|
||||
@Suppress("ExtractKtorModule")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
public fun Plotly.serve(
|
||||
scope: CoroutineScope = GlobalScope,
|
||||
host: String = "localhost",
|
||||
port: Int = 7777,
|
||||
block: PlotlyServer.() -> Unit,
|
||||
): ApplicationEngine = scope.embeddedServer(io.ktor.server.cio.CIO, port, host) {
|
||||
// install(CallLogging)
|
||||
install(CORS) {
|
||||
anyHost()
|
||||
}
|
||||
|
||||
plotlyModule(block = block)
|
||||
}.start()
|
||||
|
||||
/**
|
||||
* A shortcut to make a single plot at the default page
|
||||
*/
|
||||
public fun PlotlyServer.plot(
|
||||
plotId: String? = null,
|
||||
config: PlotlyConfig = PlotlyConfig(),
|
||||
plotBuilder: Plot.() -> Unit,
|
||||
) {
|
||||
page { plotly ->
|
||||
div {
|
||||
plot(plotId = plotId, config = config, renderer = plotly, builder = plotBuilder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun ApplicationEngine.show() {
|
||||
val connector = environment.connectors.first()
|
||||
val uri = URI("http", null, connector.host, connector.port, null, null, null)
|
||||
Desktop.getDesktop().browse(uri)
|
||||
}
|
||||
|
||||
public fun ApplicationEngine.close(): Unit = stop(1000, 5000)
|
||||
@@ -1,184 +0,0 @@
|
||||
package space.kscience.plotly.server
|
||||
|
||||
import io.ktor.http.URLBuilder
|
||||
import io.ktor.server.engine.ApplicationEngine
|
||||
import kotlinx.html.div
|
||||
import kotlinx.html.script
|
||||
import kotlinx.html.stream.createHTML
|
||||
import kotlinx.html.style
|
||||
import org.jetbrains.kotlinx.jupyter.api.HTML
|
||||
import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
|
||||
import org.jetbrains.kotlinx.jupyter.api.libraries.resources
|
||||
import org.slf4j.LoggerFactory
|
||||
import space.kscience.dataforge.meta.Scheme
|
||||
import space.kscience.dataforge.meta.boolean
|
||||
import space.kscience.dataforge.meta.int
|
||||
import space.kscience.plotly.*
|
||||
import space.kscience.visionforge.html.HtmlFragment
|
||||
|
||||
public object PlotlyServerConfiguration : Scheme() {
|
||||
public var port: Int by int(System.getProperty("space.kscience.plotly.port")?.toInt() ?: 8882)
|
||||
public var updateInterval: Int by int(100)
|
||||
|
||||
public var legacyMode: Boolean by boolean(false)
|
||||
|
||||
/**
|
||||
* Switch plotly renderer to the legacy notebook mode (Jupyter classic)
|
||||
*/
|
||||
public fun notebook(): HtmlFragment {
|
||||
legacyMode = true
|
||||
return HtmlFragment {
|
||||
div {
|
||||
style = "color: blue;"
|
||||
+"Plotly notebook integration switch into the legacy mode."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val plotlyKtHeader = HtmlFragment {
|
||||
script {
|
||||
src = "js/plotly-kt.js"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global plotly jupyter configuration
|
||||
*/
|
||||
public val Plotly.jupyter: PlotlyServerConfiguration
|
||||
get() = PlotlyServerConfiguration
|
||||
|
||||
public class PlotlyServerIntegration : JupyterIntegration() {
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
private var server: ApplicationEngine? = null
|
||||
|
||||
private val plots = HashMap<String, Plot>()
|
||||
|
||||
private var renderer: PlotlyRenderer = StaticPlotlyRenderer
|
||||
|
||||
public val isServerStarted: Boolean get() = server != null
|
||||
|
||||
|
||||
private fun start(): HtmlFragment = if (server != null) {
|
||||
HtmlFragment {
|
||||
div {
|
||||
style = "color: blue;"
|
||||
+"The server is already running on ${Plotly.jupyter.port}. It must be shut down first to be restarted."
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fun doStart(): HtmlFragment {
|
||||
server?.stop(1000, 1000)
|
||||
server = Plotly.serve(host = "0.0.0.0", port = Plotly.jupyter.port) {
|
||||
root.servePlotData(plots)
|
||||
}
|
||||
val serverUrl = URLBuilder(port = Plotly.jupyter.port).build()
|
||||
renderer = ServerPlotlyRenderer(
|
||||
serverUrl, PlotlyUpdateMode.PUSH,
|
||||
Plotly.jupyter.updateInterval,
|
||||
true
|
||||
) { plotId, plot ->
|
||||
plots[plotId] = plot
|
||||
}
|
||||
return HtmlFragment {
|
||||
div {
|
||||
style = "color: blue;"
|
||||
+"Started plotly server on ${Plotly.jupyter.port}"
|
||||
}
|
||||
}
|
||||
}
|
||||
Plotly.jupyter.meta.onChange(this) { name ->
|
||||
if (name.toString() != PlotlyServerConfiguration::legacyMode.name) {
|
||||
logger.info("Plotly server config parameter $name changed. Restarting server.")
|
||||
doStart()
|
||||
}
|
||||
}
|
||||
logger.info("Starting Plotly-kt data server with ${Plotly.jupyter}")
|
||||
doStart()
|
||||
}
|
||||
|
||||
private fun stop() {
|
||||
logger.info("Stopping Plotly-kt update server")
|
||||
server?.stop(1000, 1000)
|
||||
server = null
|
||||
}
|
||||
|
||||
private fun renderPlot(plot: Plot): String = createHTML().div {
|
||||
plot(plot, config = PlotlyConfig {
|
||||
responsive = true
|
||||
}, renderer = renderer)
|
||||
}
|
||||
|
||||
private fun renderFragment(fragment: PlotlyFragment): String = createHTML().div {
|
||||
with(fragment) {
|
||||
render(renderer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun Builder.onLoaded() {
|
||||
|
||||
resources {
|
||||
js("plotly-kt") {
|
||||
classPath("js/plotly-kt.js")
|
||||
}
|
||||
}
|
||||
|
||||
repositories("https://repo.kotlin.link")
|
||||
|
||||
import(
|
||||
"space.kscience.plotly.*",
|
||||
"space.kscience.plotly.models.*",
|
||||
"space.kscience.dataforge.meta.*",
|
||||
"kotlinx.html.*",
|
||||
"kotlinx.coroutines.*"
|
||||
)
|
||||
|
||||
import("space.kscience.plotly.server.jupyter")
|
||||
|
||||
render<HtmlFragment> {
|
||||
HTML(it.toString())
|
||||
}
|
||||
|
||||
render<Plot> { plot ->
|
||||
if (Plotly.jupyter.legacyMode) {
|
||||
HTML(
|
||||
Plotly.page(plotlyKtHeader, renderer = renderer) {
|
||||
plot(renderer = renderer, plot = plot)
|
||||
}.render(), true
|
||||
)
|
||||
} else {
|
||||
HTML(renderPlot(plot))
|
||||
}
|
||||
}
|
||||
|
||||
render<PlotlyFragment> { fragment ->
|
||||
if (Plotly.jupyter.legacyMode) {
|
||||
HTML(
|
||||
Plotly.page(plotlyKtHeader, renderer = renderer) { renderer ->
|
||||
fragment.render(this, renderer)
|
||||
}.render(), true
|
||||
)
|
||||
} else {
|
||||
HTML(renderFragment(fragment))
|
||||
}
|
||||
}
|
||||
|
||||
render<PlotlyPage> { page ->
|
||||
HTML(page.copy(headers = page.headers + plotlyKtHeader, renderer = renderer).render(), true)
|
||||
}
|
||||
|
||||
onLoaded {
|
||||
logger.info("Starting Plotly-kt data server with ${Plotly.jupyter}")
|
||||
val serverStart = start()
|
||||
display(HTML(serverStart.toString()), null)
|
||||
}
|
||||
|
||||
onShutdown {
|
||||
logger.info("Stopping Plotly-kt data server")
|
||||
Plotly.jupyter.meta.removeListener(this)
|
||||
display(HTML(stop().toString()), null)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package space.kscience.plotly.server
|
||||
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.toJson
|
||||
import space.kscience.plotly.Plotly
|
||||
|
||||
|
||||
/**
|
||||
* An update message for both data and layout
|
||||
*/
|
||||
public sealed class Update(public val id: String) {
|
||||
public abstract fun toJson(): JsonObject
|
||||
|
||||
public class Trace(id: String, private val trace: Int, private val content: Meta) : Update(id) {
|
||||
override fun toJson(): JsonObject = buildJsonObject {
|
||||
put("plotId", id)
|
||||
put("contentType", "trace")
|
||||
put("trace", trace)
|
||||
//patch json to adhere to plotly array in array specification
|
||||
val contentJson: JsonObject = content.toJson() as? JsonObject
|
||||
?: buildJsonObject { put("@value", content.toJson()) }
|
||||
val patchedJson = contentJson + Plotly.coordinateNames.associateWith { contentJson[it] }
|
||||
.filter { it.value != null }
|
||||
.mapValues { JsonArray(listOf(it.value!!)) }
|
||||
put("content", JsonObject(patchedJson))
|
||||
}
|
||||
}
|
||||
|
||||
public class Layout(id: String, private val content: Meta) : Update(id) {
|
||||
override fun toJson(): JsonObject = buildJsonObject {
|
||||
put("plotId", id)
|
||||
put("contentType", "layout")
|
||||
put("content", content.toJson())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ include(
|
||||
":visionforge-gdml",
|
||||
":cern-root-loader",
|
||||
":visionforge-server",
|
||||
":visionforge-plotly",
|
||||
// ":visionforge-plotly",
|
||||
":visionforge-tables",
|
||||
":visionforge-markdown",
|
||||
":demo:solid-showcase",
|
||||
@@ -62,8 +62,6 @@ include(
|
||||
":visionforge-jupyter:visionforge-jupyter-common",
|
||||
":plotly",
|
||||
":plotly:plotlykt-core",
|
||||
":plotly:plotlykt-jupyter",
|
||||
":plotly:plotlykt-server",
|
||||
":plotly:plotlykt-script",
|
||||
":plotly:examples",
|
||||
":plotly:examples:fx-demo",
|
||||
|
||||
@@ -6,15 +6,16 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||
|
||||
|
||||
@Serializable
|
||||
public abstract class AbstractVision : Vision {
|
||||
public abstract class AbstractVision(
|
||||
@SerialName("properties")
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@EncodeDefault(EncodeDefault.Mode.NEVER)
|
||||
private val propertiesInternal: MutableMeta = MutableMeta()
|
||||
): Vision {
|
||||
|
||||
@Transient
|
||||
override var parent: Vision? = null
|
||||
|
||||
@SerialName("properties")
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@EncodeDefault(EncodeDefault.Mode.NEVER)
|
||||
protected var propertiesInternal: MutableMeta = MutableMeta()
|
||||
|
||||
final override val properties: MutableVisionProperties by lazy {
|
||||
AbstractVisionProperties(this, propertiesInternal)
|
||||
|
||||
@@ -32,7 +32,7 @@ public interface MutableVisionContainer<in V : Vision> {
|
||||
public interface VisionChildren : VisionContainer<Vision> {
|
||||
public val parent: Vision?
|
||||
|
||||
public val keys: Set<NameToken>
|
||||
public val keys: Collection<NameToken>
|
||||
|
||||
public val values: Iterable<Vision> get() = keys.map { get(it)!! }
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import kotlinx.serialization.Transient
|
||||
import space.kscience.dataforge.meta.*
|
||||
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||
import space.kscience.dataforge.meta.descriptors.get
|
||||
import space.kscience.dataforge.misc.DFExperimental
|
||||
import space.kscience.dataforge.names.*
|
||||
|
||||
public interface VisionProperties : MetaProvider {
|
||||
@@ -108,12 +109,15 @@ private class VisionPropertiesItem(
|
||||
val inherit: Boolean? = null,
|
||||
val useStyles: Boolean? = null,
|
||||
val default: Meta? = null,
|
||||
) : MutableMeta {
|
||||
) : MutableTypedMeta<VisionPropertiesItem> {
|
||||
|
||||
override val self: VisionPropertiesItem get() = this
|
||||
|
||||
|
||||
val descriptor: MetaDescriptor? by lazy { properties.descriptor?.get(nodeName) }
|
||||
|
||||
|
||||
override val items: Map<NameToken, MutableMeta>
|
||||
override val items: Map<NameToken, VisionPropertiesItem>
|
||||
get() {
|
||||
val metaKeys = properties.own[nodeName]?.items?.keys ?: emptySet()
|
||||
val descriptorKeys = descriptor?.nodes?.map { NameToken(it.key) } ?: emptySet()
|
||||
@@ -141,7 +145,24 @@ private class VisionPropertiesItem(
|
||||
properties.setValue(nodeName, value)
|
||||
}
|
||||
|
||||
override fun getOrCreate(name: Name): MutableMeta = VisionPropertiesItem(
|
||||
//TODO remove with DataForge 0.9.1
|
||||
override fun get(name: Name): VisionPropertiesItem? {
|
||||
tailrec fun VisionPropertiesItem.find(name: Name): VisionPropertiesItem? = if (name.isEmpty()) {
|
||||
this
|
||||
} else {
|
||||
items[name.firstOrNull()!!]?.find(name.cutFirst())
|
||||
}
|
||||
|
||||
return self.find(name)
|
||||
}
|
||||
|
||||
|
||||
@DFExperimental
|
||||
override fun attach(name: Name, node: VisionPropertiesItem) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getOrCreate(name: Name): VisionPropertiesItem = VisionPropertiesItem(
|
||||
properties,
|
||||
nodeName + name,
|
||||
inherit,
|
||||
|
||||
@@ -20,7 +20,7 @@ kscience {
|
||||
)
|
||||
dependencies {
|
||||
api(projects.visionforgeSolid)
|
||||
api(projects.visionforgePlotly)
|
||||
api(projects.plotly.plotlyktCore)
|
||||
api(projects.visionforgeTables)
|
||||
api(projects.visionforgeMarkdown)
|
||||
api(projects.visionforgeJupyter)
|
||||
|
||||
Reference in New Issue
Block a user