[WIP] gradual merge of Plotly

This commit is contained in:
2024-06-05 21:46:29 +03:00
parent 4f49901351
commit aba3742b56
47 changed files with 426 additions and 1278 deletions

View File

@@ -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)
}

View File

@@ -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")

View File

@@ -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)
}
}

View File

@@ -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"

View File

@@ -10,7 +10,6 @@ repositories {
}
dependencies {
implementation(project(":plotly:plotlykt-server"))
implementation("no.tornado:tornadofx:1.7.20")
implementation(spclibs.logback.classic)
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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)
}
}
}

View File

@@ -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() {

View File

@@ -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"

View File

@@ -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)
}
}

View File

@@ -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"))
}

View File

@@ -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"

View File

@@ -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()

View File

@@ -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

View File

@@ -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())

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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())
}
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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)"))

View File

@@ -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()
}
}

View File

@@ -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)
}
)
}
}
}

View File

@@ -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"
}
}

View File

@@ -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) {

View File

@@ -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))
}
}

View File

@@ -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")
}
```

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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"

View File

@@ -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")
}
```

View File

@@ -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;
}

View File

@@ -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")
}

View File

@@ -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))
}
}

View File

@@ -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)

View File

@@ -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)
}
}
}

View File

@@ -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())
}
}
}

View File

@@ -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",

View File

@@ -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)

View File

@@ -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)!! }

View File

@@ -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,

View File

@@ -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)