14 Commits
master ... dev

37 changed files with 182 additions and 328 deletions

View File

@@ -11,6 +11,8 @@
### Removed
### Fixed
- Fix the problem where property listeners do not react on property child node changa
- Plotly plot title now writes proper field
### Security

View File

@@ -11,7 +11,7 @@ val dataforgeVersion by extra("0.10.1")
allprojects {
group = "space.kscience"
version = "0.5.0"
version = "0.6.0-dev-1"
}
subprojects {

View File

@@ -1,6 +1,6 @@
plugins {
kotlin("multiplatform")
kotlin("jupyter.api")
alias(spclibs.plugins.kotlin.jupyter.api)
id("com.gradleup.shadow") version "8.3.6"
}

View File

@@ -1,15 +1,13 @@
package space.kscience.visionforge.solid.demo
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import space.kscience.visionforge.html.startApplication
import space.kscience.visionforge.solid.x
import space.kscience.visionforge.solid.y
import kotlin.random.Random
@OptIn(DelicateCoroutinesApi::class)
fun main() {
startApplication { document ->
val element = document.getElementById("demo") ?: error("Element with id 'demo' not found on page")

View File

@@ -8,4 +8,4 @@ org.gradle.workers.max=4
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
kotlin.native.enableKlibsCrossCompilation=true
toolsVersion=0.17.1-kotlin-2.1.20
toolsVersion=0.19.2-kotlin-2.2.20

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
### Fixed
- Fix legend orientation constants
### Security

View File

@@ -20,7 +20,7 @@ kotlin{
// A workaround for https://youtrack.jetbrains.com/issue/KT-44101
val copyPlotlyResources by tasks.creating(Copy::class){
val copyPlotlyResources by tasks.registering(Copy::class){
dependsOn(":plotly-kt:plotly-kt-server:jvmProcessResources")
mustRunAfter(":plotly-kt:plotly-kt-server:jvmTestProcessResources")
from(project(":plotly-kt:plotly-kt-server").layout.buildDirectory.file("processedResources/jvm/main"))

View File

@@ -1,6 +1,6 @@
plugins {
id("space.kscience.gradle.mpp")
kotlin("jupyter.api")
alias(spclibs.plugins.kotlin.jupyter.api)
`maven-publish`
}
@@ -31,9 +31,9 @@ kscience {
}
}
tasks.processJupyterApiResources {
libraryProducers = listOf("space.kscience.plotly.PlotlyIntegration")
}
//tasks.processJupyterApiResources {
// libraryProducers = listOf("space.kscience.plotly.PlotlyIntegration")
//}
kotlin {

View File

@@ -80,8 +80,9 @@ public class Plot : AbstractVision(), MutableVisionGroup<Trace> {
*/
@UnstablePlotlyAPI
@JvmSynchronized
internal fun removeTrace(index: Int) {
public fun removeTrace(index: Int) {
_data.removeAt(index)
emitEvent(VisionGroupCompositionChangedEvent(NameToken("trace", _data.size.toString()), null))
}
override val descriptor: MetaDescriptor get() = Companion.descriptor

View File

@@ -8,9 +8,11 @@ import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.plotly.models.Trace
import space.kscience.visionforge.VisionBuilder
import space.kscience.visionforge.html.*
import space.kscience.visionforge.html.VisionOutput
import space.kscience.visionforge.html.VisionTagConsumer
import kotlin.js.JsName
/**
@@ -65,6 +67,18 @@ public class PlotlyConfig : Scheme() {
public var responsive: Boolean? by boolean()
public var imageFormat: String? by string(Name.parse("toImageButtonOptions.format"))
/**
* A list of class names applied to the output `div` in the generated HTML for the plot.
*
* This property allows customization of the CSS classes assigned to the `div` element
* that contains the rendered plot. It can be utilized to add custom styling or specific
* class-based behaviors to the output.
*
* By default, this property is initialized as an empty list and can be updated to include
* necessary class names as strings.
*/
public var classes: List<String> by stringList(default = emptyArray(), key = VisionTagConsumer.OUTPUT_DIV_CLASSES_KEY.asName())
public fun withEditorButton() {
showEditInChartStudio = true
plotlyServerURL = "https://chart-studio.plotly.com"
@@ -92,12 +106,14 @@ public inline fun VisionOutput.plotly(
return Plotly.plot(block)
}
//FIXME rework VisionTagConsumer toa a context
context(rootConsumer: VisionTagConsumer<*>)
public fun TagConsumer<*>.plot(
config: PlotlyConfig = PlotlyConfig(),
block: Plot.() -> Unit,
): Unit = with(rootConsumer) {
vision {
this@plot.vision {
plotly(config, block)
}
}

View File

@@ -45,7 +45,7 @@ public fun TagConsumer<*>.staticPlot(
config: PlotlyConfig = PlotlyConfig(),
plotId: String = "plotly[${Uuid.random()}]",
plot: Plot.() -> Unit
) = staticPlot(Plotly.plot(plot), config, plotId)
): Unit = staticPlot(Plotly.plot(plot), config, plotId)
/**
* Create an html (including headers) string from plot

View File

@@ -45,7 +45,7 @@ public class Axis : Scheme() {
public var title: String?
get() = meta["title.text"].string ?: meta["title"].string
set(value) {
meta["title"] = value?.asValue()
meta["title.text"] = value?.asValue()
}
/**

View File

@@ -6,14 +6,10 @@ import space.kscience.dataforge.meta.enum
import space.kscience.dataforge.meta.scheme
import space.kscience.plotly.numberGreaterThan
import space.kscience.plotly.numberInRange
import kotlin.js.JsName
public enum class LegendOrientation {
@JsName("v")
vertical,
@JsName("h")
horizontal
v,
h
}
public enum class XAnchor {
@@ -103,7 +99,7 @@ public class Legend : Scheme() {
* Sets the orientation of the legend (vertical/horizontal).
* Default: vertical.
*/
public var orientation: LegendOrientation by enum(LegendOrientation.vertical)
public var orientation: LegendOrientation by enum(LegendOrientation.v)
/**
* The order at which the legend items are displayed.

View File

@@ -1,167 +0,0 @@
package space.kscience.plotly
import kotlinx.browser.document
import kotlinx.browser.window
import org.w3c.dom.*
import org.w3c.dom.url.URL
import org.w3c.fetch.RequestInit
import kotlin.js.Promise
import kotlin.js.json
@JsExport
public object PlotlyConnect {
/**
* Wait for the Plotly library to be loaded
*/
private fun withPlotly(action: PlotlyJs.() -> Unit) {
if (jsTypeOf(PlotlyJs) !== "undefined") {
action(PlotlyJs);
} else {
val promiseOfPlotly: Promise<PlotlyJs> = window.asDynamic().promiseOfPlotly as Promise<PlotlyJs>
if (jsTypeOf(promiseOfPlotly) != "undefined") {
promiseOfPlotly.then { action(PlotlyJs) }
} else {
console.warn("Plotly not defined. Loading the script from CDN")
window.asDynamic().promiseOfPlotly = Promise<PlotlyJs> { resolve, reject ->
val plotlyLoaderScript = document.createElement("script") as HTMLScriptElement
plotlyLoaderScript.src = "https://cdn.plot.ly/plotly-2.29.0.min.js"
plotlyLoaderScript.type = "text/javascript"
plotlyLoaderScript.onload = {
resolve(PlotlyJs)
action(PlotlyJs)
}
plotlyLoaderScript.onerror = { error, _, _, _, _ ->
console.error(error);
reject(error as Throwable)
}
document.head?.appendChild(plotlyLoaderScript);
}
}
}
}
/**
* Request and parse json from given address
* @param url {URL}
* @param callback
*/
private fun getJSON(url: URL, callback: (dynamic) -> Unit) {
try {
window.fetch(
url,
RequestInit(
method = "GET",
headers = json("Accept" to "application/json")
)
).then { response ->
if (!response.ok) {
error("Fetch request failed with error: ${response.statusText}")
} else {
response.json().then(callback)
}
}.catch { error -> console.log(error) }
} catch (e: Exception) {
window.alert("Fetch of plot data failed with error: $e")
}
}
public fun makePlot(
graphDiv: Element,
data: Array<dynamic>,
layout: dynamic,
config: dynamic,
) {
withPlotly { newPlot(graphDiv, data, layout, config) }
}
/**
* Create a plot taking data from given url
* @param id {string} element id for plot
* @param from {URL} json server url
* @param config {object} plotly configuration
*/
public fun createPlotFrom(id: String, from: URL, config: dynamic = {}) {
getJSON(from) { json ->
val element = document.getElementById(id) as HTMLElement
withPlotly { newPlot(element, json.data, json.layout, config) }
}
}
/**
* Update a plot taking data from given url
* @param id {string} element id for plot
* @param from {URL} json server url
* @return {JSON}
*/
public fun updatePlotFrom(id: String, from: URL) {
getJSON(from) { json ->
val element = document.getElementById(id) as HTMLElement
withPlotly { react(element, json.data, json.layout) }
}
}
/**
* Start pull updates with regular requests from client side
* @param id {string}
* @param from
* @param millis
*/
public fun startPull(id: String, from: URL, millis: Int) {
window.setInterval({ updatePlotFrom(id, from) }, millis)
}
/**
* Start push updates via websocket
* @param id {string} element id for plot
* @param ws {URL} a websocket address
*/
public fun startPush(id: String, ws: String) {
val element = document.getElementById(id) as HTMLElement
val socket = WebSocket(ws)
socket.onopen = {
console.log("[Plotly.kt] A connection for plot with id = $id with server established on $ws")
}
socket.onclose = { event ->
event as CloseEvent
if (event.wasClean) {
console.log("The connection with server is closed")
} else {
console.log("The connection with server is broken") // Server process is dead
}
console.log("Code: ${event.code} case: ${event.reason}")
}
socket.onerror = { error ->
error as ErrorEvent
console.error("Plotly push update error: " + error.message)
socket.close()
}
socket.onmessage = { event ->
val json: dynamic = JSON.parse(event.data.toString())
if (json.plotId === id) {
if (json.contentType === "layout") {
withPlotly { relayout(element, json.content) }
} else if (json.contentType === "trace") {
withPlotly { restyle(element, json.content, json.trace) }
}
}
}
//gracefully close the socket just in case
window.onbeforeunload = {
console.log("Gracefully closing socket")
socket.close()
null
}
}
}
public fun main() {
window.asDynamic().plotlyConnect = PlotlyConnect
window.asDynamic().Plotly = PlotlyJs
}

View File

@@ -1,5 +1,6 @@
package space.kscience.plotly
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.filterIsInstance
@@ -32,7 +33,11 @@ private fun List<MetaRepr>.toDynamic(): Array<dynamic> = map { it.toDynamic() }.
* Attach a plot to this element or update the existing plot
*/
@OptIn(DelicateCoroutinesApi::class)
public fun Element.plot(plotlyConfig: PlotlyConfig = PlotlyConfig(), plot: Plot) {
public fun Element.plot(
plotlyConfig: PlotlyConfig,
plot: Plot,
scope: CoroutineScope = plot.manager?.context ?: GlobalScope
) {
// console.info("""
// Plotly.react(
@@ -51,7 +56,7 @@ public fun Element.plot(plotlyConfig: PlotlyConfig = PlotlyConfig(), plot: Plot)
)
//start updates
val listenJob = (plot.manager?.context ?: GlobalScope).launch {
val listenJob = scope.launch {
plot.data.forEachIndexed { index, trace ->
trace.eventFlow.filterIsInstance<VisionPropertyChangedEvent>().onEach { event ->
val traceData = trace.toDynamic()
@@ -92,27 +97,34 @@ public fun Element.plot(plot: Plot, plotlyConfig: PlotlyConfig = PlotlyConfig())
/**
* Create a plot in this element
*/
public inline fun Element.plot(plotlyConfig: PlotlyConfig = PlotlyConfig(), plotBuilder: Plot.() -> Unit) {
plot(plotlyConfig, Plot().apply(plotBuilder))
public inline fun Element.plot(
scope: CoroutineScope,
plotlyConfig: PlotlyConfig = PlotlyConfig(),
plotBuilder: Plot.() -> Unit
) {
plot(plotlyConfig, Plot().apply(plotBuilder), scope)
}
public class PlotlyElement(public val div: HTMLElement)
/**
* Create a div element and render plot in it
* Create a div element and render the plot in it
*/
@OptIn(DelicateCoroutinesApi::class)
public fun TagConsumer<HTMLElement>.plotDiv(
plotlyConfig: PlotlyConfig = PlotlyConfig(),
plotlyConfig: PlotlyConfig,
plot: Plot,
scope: CoroutineScope = plot.manager?.context ?: GlobalScope,
): PlotlyElement = PlotlyElement(div("plotly-kt-plot").apply { plot(plotlyConfig, plot) })
/**
* Render plot in the HTML element using direct plotly API.
*/
public inline fun TagConsumer<HTMLElement>.plotDiv(
scope: CoroutineScope,
plotlyConfig: PlotlyConfig = PlotlyConfig(),
plotBuilder: Plot.() -> Unit,
): PlotlyElement = PlotlyElement(div("plotly-kt-plot").apply { plot(plotlyConfig, plotBuilder) })
): PlotlyElement = PlotlyElement(div("plotly-kt-plot").apply { plot(scope, plotlyConfig, plotBuilder) })
@OptIn(ExperimentalSerializationApi::class)
public fun PlotlyElement.on(eventType: PlotlyEventListenerType, block: MouseEvent.(PlotlyEvent) -> Unit) {

View File

@@ -1,5 +1,6 @@
package space.kscience.plotly
import kotlinx.browser.window
import kotlinx.serialization.modules.SerializersModule
import org.w3c.dom.Element
import space.kscience.dataforge.context.Context
@@ -12,6 +13,7 @@ import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionPlugin
import space.kscience.visionforge.html.ElementVisionRenderer
import space.kscience.visionforge.html.JsVisionClient
import space.kscience.visionforge.html.renderAllVisions
public class PlotlyJsPlugin : VisionPlugin(), ElementVisionRenderer {
public val plotly: PlotlyPlugin by require(PlotlyPlugin)
@@ -39,10 +41,25 @@ public class PlotlyJsPlugin : VisionPlugin(), ElementVisionRenderer {
else -> super.content(target)
}
public companion object : PluginFactory<PlotlyJsPlugin> {
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()
public val defaultClient: JsVisionClient by lazy {
val context = Context("Plotly-kt") {
plugin(PlotlyJsPlugin)
}
context.plugins[PlotlyJsPlugin]!!.visionClient
}
}
}
public fun main() {
window.asDynamic().Plotly = PlotlyJs
window.asDynamic().renderAllPlots = {
PlotlyJsPlugin.defaultClient.renderAllVisions()
}
}

View File

@@ -1,5 +1,7 @@
package space.kscience.plotly
import kotlinx.html.script
import kotlinx.html.unsafe
import space.kscience.visionforge.html.*
import space.kscience.visionforge.visionManager
import java.awt.Desktop
@@ -53,6 +55,17 @@ public fun Plotly.makePageFile(
resourceLocation,
actualPath
),
"plotly-render" to HtmlFragment {
script {
defer = true
unsafe {
+"""
window.renderAllPlots()
""".trimIndent()
}
}
}
) + additionalHeaders
}
if (show) Desktop.getDesktop().browse(actualPath.toFile().toURI())

View File

@@ -2,6 +2,7 @@ plugins {
id("space.kscience.gradle.mpp")
alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
`maven-publish`
}
kscience {
@@ -26,6 +27,7 @@ kscience {
jvmMain {
api(projects.visionforgeServer)
api(project.dependencies.platform(spclibs.ktor.bom))
api("io.ktor:ktor-server-cio")
}
}

View File

@@ -22,7 +22,7 @@ kotlin {
jsMain {
dependencies {
api("app.softwork:bootstrap-compose:0.3.0")
api("app.softwork:bootstrap-compose:0.3.1")
api("app.softwork:bootstrap-compose-icons:0.3.0")
// implementation(npm("bootstrap", "5.3.3"))
// implementation(npm(" bootstrap-icons", "1.11.3"))

View File

@@ -28,9 +28,8 @@ public interface ComposeHtmlVisionRenderer : ElementVisionRenderer {
public companion object
}
/**
* A compose-html renderer for a vision of given type
* A compose-html renderer for a vision of a given type
*/
public inline fun <reified T : Vision> ComposeHtmlVisionRenderer(
acceptRating: Int = ElementVisionRenderer.DEFAULT_RATING,

View File

@@ -0,0 +1,18 @@
package space.kscience.visionforge.html
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.flow.map
import space.kscience.visionforge.Vision
import space.kscience.visionforge.flowProperty
import kotlin.reflect.KProperty1
@Composable
public fun <V : Vision, T> V.collectPropertyAsState(
property: KProperty1<V, T>,
propertyName: String = property.name,
): State<T> = flowProperty(propertyName)
.map { property.get(this@collectPropertyAsState) }
.collectAsState(property.get(this))

View File

@@ -7,7 +7,7 @@ val dataforgeVersion: String by rootProject.extra
kscience {
jvm()
js()
native {}
native()
// wasm()
useCoroutines()
commonMain {

View File

@@ -118,7 +118,7 @@ public interface MutableVision : Vision {
override suspend fun receiveEvent(event: VisionEvent) {
if (event is VisionChange) {
if (event.children?.isNotEmpty() == true) {
error("Vision is not a group")
error("Received vision group change event, but $this Vision does not handle children changes")
}
event.properties?.let {
updateProperties(it, Name.EMPTY)
@@ -130,7 +130,7 @@ public interface MutableVision : Vision {
name: Name,
inherited: Boolean = isInheritedProperty(name),
useStyles: Boolean = isStyledProperty(name),
): MutableMeta = properties.getOrCreate(name).withDefault { suffix->
): MutableMeta = properties.getOrCreate(name).withDefault { suffix ->
val propertyName = name + suffix
if (useStyles) getStyleProperty(propertyName)?.let { return@withDefault it }
if (inherited) parent?.readProperty(propertyName, inherited, useStyles)?.let { return@withDefault it }

View File

@@ -14,7 +14,6 @@ import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.VisionTagConsumer.Companion.DEFAULT_VISION_NAME
import space.kscience.visionforge.setAsRoot
import space.kscience.visionforge.visionManager
import kotlin.collections.set
@DslMarker
public annotation class VisionDSL
@@ -23,7 +22,7 @@ public annotation class VisionDSL
* A placeholder object to attach inline vision builders.
*/
@VisionDSL
public class VisionOutput(override val context: Context, public val name: Name): ContextAware {
public class VisionOutput(override val context: Context, public val name: Name) : ContextAware {
public var meta: Meta = Meta.EMPTY
private val requirements: MutableSet<PluginFactory<*>> = HashSet()
@@ -89,7 +88,8 @@ public abstract class VisionTagConsumer<R>(
+"Empty Vision output"
} else div {
id = resolveId(name)
classes = setOf(OUTPUT_CLASS)
classes = setOf(OUTPUT_CLASS, *(outputMeta[OUTPUT_DIV_CLASSES_KEY].stringList?.toTypedArray() ?: emptyArray()))
if (vision.parent == null) {
vision.setAsRoot(manager)
}
@@ -155,6 +155,8 @@ public abstract class VisionTagConsumer<R>(
public const val OUTPUT_META_CLASS: String = "visionforge-output-meta"
public const val OUTPUT_DATA_CLASS: String = "visionforge-output-data"
public const val OUTPUT_DIV_CLASSES_KEY: String = "classes"
public const val OUTPUT_FETCH_ATTRIBUTE: String = "data-output-fetch"
public const val OUTPUT_CONNECT_ATTRIBUTE: String = "data-output-connect"

View File

@@ -9,6 +9,7 @@ import space.kscience.dataforge.meta.Value
import space.kscience.dataforge.meta.descriptors.get
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.names.startsWith
import kotlin.reflect.KProperty1
private fun Vision.withAncestors(): List<Vision> = buildList {
@@ -42,7 +43,7 @@ public fun Vision.flowProperty(
}
combinedFlow.filterIsInstance<VisionPropertyChangedEvent>().collect { event ->
if (event.propertyName == propertyName || (useStyles && event.propertyName == Vision.STYLE_KEY)) {
if (event.propertyName.startsWith(propertyName) || (useStyles && event.propertyName == Vision.STYLE_KEY)) {
emit(readProperty(event.propertyName, inherited, useStyles))
}
}
@@ -89,7 +90,7 @@ public fun Vision.useProperty(
} else {
eventFlow
}.filterIsInstance<VisionPropertyChangedEvent>().onEach { event ->
if (event.propertyName == propertyName || (useStyles && event.propertyName == Vision.STYLE_KEY)) {
if (event.propertyName.startsWith(propertyName) || (useStyles && event.propertyName == Vision.STYLE_KEY)) {
callback(readProperty(event.propertyName, inherited, useStyles))
}
}.collect()
@@ -134,7 +135,7 @@ public fun <V : Vision, T> V.onPropertyChange(
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."),
callback: suspend V.(T) -> Unit,
): Job = inheritedEventFlow().filterIsInstance<VisionPropertyChangedEvent>().onEach {
if (it.propertyName.toString() == property.name) {
if (it.propertyName.startsWith(property.name)) {
callback(property.get(this))
}
}.launchIn(scope)

View File

@@ -1,10 +1,6 @@
package space.kscience.visionforge.meta
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.test.runTest
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.request
@@ -13,7 +9,6 @@ import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.get
import space.kscience.dataforge.names.parseAsName
import space.kscience.visionforge.*
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.milliseconds
@@ -93,51 +88,4 @@ internal class VisionPropertyTest {
subscription.cancel()
}
@Test
@Ignore
fun testChildrenPropertyFlow() = runTest(timeout = 500.milliseconds) {
val group = SimpleVisionGroup().apply {
properties {
"test" put 11
}
group("child") {
properties {
"test" put 22
}
}
}
val child = group.visions["child"] as MutableVision
val semaphore = Semaphore(1, 1)
val changesFlow = child.flowPropertyValue("test", inherited = true).map {
semaphore.release()
it!!.int
}
val collectedValues = ArrayList<Int>(5)
val collectorJob = changesFlow.onEach {
collectedValues.add(it)
}.launchIn(this)
assertEquals(22, child.readProperty("test", true).int)
semaphore.acquire()
child.properties.remove("test")
assertEquals(11, child.readProperty("test", true).int)
semaphore.acquire()
group.properties["test"] = 33
assertEquals(33, child.readProperty("test", true).int)
semaphore.acquire()
collectorJob.cancel()
assertEquals(listOf(22, 11, 33), collectedValues)
}
}

View File

@@ -3,6 +3,7 @@ package space.kscience.visionforge.meta
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.request
@@ -38,40 +39,31 @@ internal class PropertyFlowTest {
val changesFlow = child.flowProperty("test", inherited = true)
// child.inheritedEventFlow().filterIsInstance<VisionPropertyChangedEvent>().onEach { event ->
// println(event)
// delay(2)
// println(child.readProperty("test", inherited = true))
// }.launchIn(this)
val collectedValues = ArrayList<Int>(5)
val collectorJob = changesFlow.onEach {
changesFlow.onEach {
collectedValues.add(it.int!!)
}.launchIn(this)
}.launchIn(backgroundScope)
delay(2)
delay(1)
assertEquals(22, child.readProperty("test", true).int)
// assertEquals(1, collectedValues.size)
parent.properties["test1"] = 88 // another property
child.properties.remove("test")
delay(2)
delay(1)
assertEquals(11, child.readProperty("test", true).int)
// assertEquals(2, collectedValues.size)
parent.properties["test"] = 33
delay(2)
delay(1)
assertEquals(33, child.readProperty("test", true).int)
// assertEquals(3, collectedValues.size)
collectorJob.cancel()
assertEquals(listOf(22, 11, 33), collectedValues)
advanceUntilIdle()
//assertEquals(listOf(22, 11, 33), collectedValues)
assertEquals(22, collectedValues.first())
assertEquals(33, collectedValues.last())
println("finished")
}
}

View File

@@ -1,5 +1,7 @@
plugins {
id("space.kscience.gradle.mpp")
alias(spclibs.plugins.kotlin.jupyter.api)
}
description = "Common visionforge jupyter module"
@@ -7,7 +9,6 @@ description = "Common visionforge jupyter module"
kscience {
jvm()
js()
jupyterLibrary()
dependencies {
api(projects.visionforgeCore)
}
@@ -20,7 +21,6 @@ kscience {
}
}
readme {
maturity = space.kscience.gradle.Maturity.EXPERIMENTAL
}

View File

@@ -1,5 +1,6 @@
plugins {
id("space.kscience.gradle.mpp")
alias(spclibs.plugins.kotlin.jupyter.api)
}
description = "Jupyter api artifact including all common modules"
@@ -33,10 +34,13 @@ kscience {
jsMain {
implementation(projects.visionforgeThreejs)
}
jupyterLibrary("space.kscience.visionforge.jupyter.JupyterCommonIntegration")
}
//tasks.processJupyterApiResources {
// libraryProducers = listOf("space.kscience.visionforge.jupyter.JupyterCommonIntegration")
//}
readme {
maturity = space.kscience.gradle.Maturity.EXPERIMENTAL
}

View File

@@ -2,7 +2,7 @@ plugins {
id("space.kscience.gradle.mpp")
}
val kmathVersion = "0.4.1"
val kmathVersion = "0.4.2"
kscience {
jvm()

View File

@@ -117,7 +117,7 @@ public operator fun SolidGroup.get(name: Name): Solid? = getVision(name)
public operator fun SolidGroup.get(name: String): Solid? = getVision(name)
public operator fun SolidGroup.set(name: NameToken, value: Solid?) = setVision(name, value)
public operator fun SolidGroup.set(name: NameToken, value: Solid?): Unit = setVision(name, value)
public operator fun SolidGroup.set(name: Name, vision: Solid?) {
when (name.length) {
@@ -143,7 +143,7 @@ public operator fun SolidGroup.set(name: Name, vision: Solid?) {
}
}
public operator fun SolidGroup.set(name: String, vision: Solid?) = set(name.parseAsName(), vision)
public operator fun SolidGroup.set(name: String, vision: Solid?): Unit = set(name.parseAsName(), vision)
/**
* Add anonymous (auto-assigned name) child to a SolidGroup

View File

@@ -1,13 +1,11 @@
package space.kscience.visionforge.solid
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.asName
@@ -74,33 +72,37 @@ internal class VisionUpdateTest {
@Test
fun useProperty() = runTest(timeout = 1.seconds) {
withContext(Dispatchers.Default) {
val group = testSolids.solidGroup {
box(100, 100, 100)
}
val box = group.visions.values.first()
val collected = Channel<String?>(5)
box.useProperty(SolidMaterial.MATERIAL_COLOR_KEY) {
println(it.string)
collected.send(it.string)
}
delay(1)
group.color("red")
group.color("green")
box.color("blue")
assertEquals("blue", box.readProperty(SolidMaterial.MATERIAL_COLOR_KEY).string)
assertEquals("blue", box.color.string)
val list = collected.consumeAsFlow().take(4).toList()
assertEquals(null, list.first())
assertEquals("blue", list.last())
val group = testSolids.solidGroup {
box(100, 100, 100)
}
val box = group.visions.values.first()
val collected = Channel<String?>(5)
box.useProperty(
propertyName = SolidMaterial.MATERIAL_COLOR_KEY,
scope = backgroundScope
) {
println(it.string)
collected.send(it.string)
}
delay(1)
group.color("red")
group.color("green")
box.color("blue")
delay(1)
assertEquals("blue", box.readProperty(SolidMaterial.MATERIAL_COLOR_KEY).string)
assertEquals("blue", box.color.string)
val list = collected.consumeAsFlow().take(4).toList()
assertEquals(null, list.first())
assertEquals("blue", list.last())
}
}

View File

@@ -1,6 +1,6 @@
package space.kscience.visionforge.tables
import js.objects.jso
import js.objects.unsafeJso
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
import space.kscience.dataforge.context.AbstractPlugin
@@ -39,17 +39,17 @@ public class TableVisionJsPlugin : AbstractPlugin(), ElementVisionRenderer {
val table: VisionOfTable = (vision as? VisionOfTable)
?: error("VisionOfTable expected but ${vision::class} found")
val tableOptions = jso<Options> {
val tableOptions = unsafeJso<Options> {
columns = Array(table.headers.size + 1) {
if (it == 0) {
jso {
unsafeJso {
field = "@index"
title = "#"
resizable = false
}
} else {
val header = table.headers[it - 1]
jso {
unsafeJso {
field = header.name
title = header.properties.title ?: header.name
resizable = true

View File

@@ -1,7 +1,7 @@
package space.kscience.visionforge.solid.three
import js.objects.jso
import js.objects.unsafeJso
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.context.warn
import space.kscience.visionforge.onPropertyChange
@@ -18,7 +18,7 @@ public object ThreeLabelFactory : ThreeFactory<SolidLabel> {
override val type: KClass<in SolidLabel> get() = SolidLabel::class
override suspend fun build(three: ThreePlugin, vision: SolidLabel, observe: Boolean): Object3D {
val textGeo = TextBufferGeometry(vision.text, jso {
val textGeo = TextBufferGeometry(vision.text, unsafeJso {
font = vision.fontFamily
size = 20
height = 1

View File

@@ -1,6 +1,5 @@
package space.kscience.visionforge.three
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.html.*
import space.kscience.visionforge.solid.Solids
import java.awt.Desktop
@@ -9,7 +8,6 @@ import java.nio.file.Path
public val VisionPage.Companion.threeJsHeader: HtmlFragment get() = scriptHeader("js/visionforge-three.js")
@DFExperimental
public fun Solids.makeThreeJsFile(
path: Path? = null,
title: String = "VisionForge page",

View File

@@ -9,7 +9,6 @@ import kotlin.test.Test
class TestServerExtensions {
@Suppress("UNUSED_VARIABLE")
@Test
fun testServerHeader(){
val string = createHTML().apply {