18 Commits

Author SHA1 Message Date
2c55875f86 Add time to plotly trace values 2026-04-07 08:00:09 +03:00
c543536d2d Implement plotly-kt for wasm and add an example 2026-02-01 13:50:08 +03:00
efb05ae156 Fix plotly js test 2026-02-01 11:50:58 +03:00
ba4a8aa7c1 Update github actions 2026-01-31 22:13:37 +03:00
d069bd6047 Update github actions 2026-01-31 22:10:31 +03:00
0268ddbca0 Merge remote-tracking branch 'spc/dev' into dev 2026-01-31 20:27:25 +03:00
eacf977a0e add wasmJs target back 2026-01-31 20:27:08 +03:00
3c20583b5e Merge branch 'main' into dev 2026-01-10 20:03:11 +03:00
2bb20dea68 Update github actions 2026-01-10 19:48:35 +03:00
41949329ba Fix plugin version 2026-01-10 19:45:01 +03:00
d578efa17b Update readme 2026-01-10 19:31:56 +03:00
b6100bdce1 Fix DSLMarker annotations 2026-01-10 19:29:02 +03:00
636938087a Merge branch 'kotlin/2.3.0' into dev 2025-12-28 10:22:42 +03:00
9782721cb9 Update to stable Kotlin 2.3 2025-12-28 10:15:42 +03:00
29dacbdd2f Fix plotly axis title 2025-10-18 18:19:30 +03:00
6b83137bd0 Merge branch 'dev' into kotlin/2.3.0
# Conflicts:
#	gradle.properties
2025-10-15 22:15:03 +03:00
d48133f77f Kotlin 2.3 2025-10-14 16:44:08 +03:00
ad5ec93598 Merge pull request 'dev' (!83) from dev into master
Reviewed-on: #83
2025-03-21 11:10:27 +03:00
60 changed files with 647 additions and 180 deletions

View File

@@ -2,7 +2,7 @@ name: Gradle build
on:
push:
branches: [ dev, master ]
branches: [ dev, main ]
pull_request:
jobs:
@@ -10,15 +10,25 @@ jobs:
runs-on: windows-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3.5.1
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
java-version: '17'
java-version: '21'
distribution: 'liberica'
cache: 'gradle'
- name: Gradle Wrapper Validation
uses: gradle/wrapper-validation-action@v1.0.4
- name: Gradle Build
uses: gradle/gradle-build-action@v2.4.2
uses: gradle/gradle-build-action@v3
with:
arguments: test jvmTest
arguments: test jvmTest
- name: Publish Test Report
uses: mikepenz/action-junit-report@v6
if: ${{ !cancelled() }} # always run even if the previous step fails
with:
report_paths: '**/test-results/**/TEST-*.xml'
annotate_only: true
detailed_summary: true
flaky_summary: true
include_empty_in_summary: false
skip_success_summary: true

View File

@@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: 17
java-version: 21
distribution: liberica
- uses: gradle/gradle-build-action@v3
with:

View File

@@ -3,18 +3,35 @@
## Unreleased
### Added
- Plotly implementation and demo for Kotlin/Wasm
- Wasm targets for plotly-kt and visionforge-core/solid
- Time (Instant) based TraceValues in Plotly
### Changed
- Order of arguments in Plotly-kt for js plotDiv functions
### Deprecated
### Removed
### Fixed
- Fix the problem where property listeners do not react on property child node changa
- Plotly-kt js demo
### Security
## 0.5.1 - 2026-01-10
### Changed
- Kotlin 2.3
### Fixed
- Fix DSLMarker annotations for builders
- Flaky test
- Fix the problem where property listeners do not react on property child node changes
- Plotly plot title now writes proper field
## 0.5.0 - 2025-03-21
### Added

View File

@@ -5,14 +5,14 @@
[![Slack](https://img.shields.io/badge/slack-channel-green?logo=slack)](https://kotlinlang.slack.com/archives/CEXV2QWNM)
# DataForge Visualization Platform
# VisionForge platform
## Table of Contents
* [Introduction](#introduction)
* [Requirements](#requirements)
* [Features](#features)
* [About DataForge](#about-dataforge)
* [About VisionForge](#about-VisionForge)
* [Modules contained in this repository](#modules-contained-in-this-repository)
* [Visualization for External Systems](#visualization-for-external-systems)
* [Demonstrations](#demonstrations)
@@ -23,7 +23,7 @@
## Introduction
This repository contains a [DataForge](#about-dataforge)\-based framework
This repository contains a [VisionForge](#about-VisionForge) framework
used for visualization in various scientific applications.
The main framework's use case for now is 3D visualization for particle physics experiments.
@@ -41,16 +41,16 @@ JVM backend requires JDK 11 or later
The main framework's features for now include:
- 3D visualization of complex experimental set-ups
- Event display such as particle tracks, etc.
- Scales up to few hundred thousands of elements
- Camera move, rotate, zoom-in and zoom-out
- Scales up to hundreds of thousands of elements
- The camera moves, rotates, zoom-in and zoom-out
- Scene graph as an object tree with property editor
- Settings export and import
- Multiple platform support
## About DataForge
## About VisionForge
DataForge is a software framework for automated scientific data processing. DataForge Visualization
Platform uses some of the concepts and modules of DataForge, including: `Meta`, `Configuration`, `Context`,
[DataForge](https://git.sciprog.center/kscience/dataforge-core) is a software framework for automated scientific data processing. VisionForge
Platform uses some concepts and modules of DataForge, including: `Meta`, `Configuration`, `Context`,
`Provider`, and some others.
To learn more about DataForge, please consult the following URLs:
@@ -66,10 +66,6 @@ To learn more about DataForge, please consult the following URLs:
>
> **Maturity**: EXPERIMENTAL
### [demo](demo)
>
> **Maturity**: EXPERIMENTAL
### [plotly-kt](plotly-kt)
>
> **Maturity**: EXPERIMENTAL
@@ -131,10 +127,6 @@ To learn more about DataForge, please consult the following URLs:
>
> **Maturity**: EXPERIMENTAL
### [demo/playground](demo/playground)
>
> **Maturity**: EXPERIMENTAL
### [demo/sat-demo](demo/sat-demo)
>
> **Maturity**: EXPERIMENTAL
@@ -143,10 +135,6 @@ To learn more about DataForge, please consult the following URLs:
>
> **Maturity**: EXPERIMENTAL
### [plotly-kt/examples](plotly-kt/examples)
>
> **Maturity**: EXPERIMENTAL
### [plotly-kt/plotly-kt-core](plotly-kt/plotly-kt-core)
>
> **Maturity**: DEVELOPMENT
@@ -164,18 +152,6 @@ To learn more about DataForge, please consult the following URLs:
>
> **Maturity**: EXPERIMENTAL
### [plotly-kt/examples/compose-demo](plotly-kt/examples/compose-demo)
>
> **Maturity**: EXPERIMENTAL
### [plotly-kt/examples/js-demo](plotly-kt/examples/js-demo)
>
> **Maturity**: EXPERIMENTAL
### [plotly-kt/examples/native-demo](plotly-kt/examples/native-demo)
>
> **Maturity**: EXPERIMENTAL
**Class diagram:**

View File

@@ -1,3 +1,6 @@
@file:OptIn(ExperimentalAbiValidation::class)
import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import space.kscience.gradle.useApache2Licence
import space.kscience.gradle.useSPCTeam
@@ -7,11 +10,11 @@ plugins {
alias(spclibs.plugins.kotlinx.kover)
}
val dataforgeVersion by extra("0.10.1")
val dataforgeVersion by extra("0.10.2")
allprojects {
group = "space.kscience"
version = "0.6.0-dev-1"
version = "0.5.2-dev-1"
}
subprojects {
@@ -39,17 +42,23 @@ subprojects {
}
ksciencePublish {
kscienceProject {
pom("https://github.com/SciProgCentre/visionforge") {
useApache2Licence()
useSPCTeam()
}
repository("spc", "https://maven.sciprog.center/kscience")
central()
publishTo("spc", "https://maven.sciprog.center/kscience")
publishToCentral()
abiValidation {
filters{
exclude{
byNames.add("info.laht.threekt.**")
}
}
}
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
}
apiValidation {
ignoredPackages.add("info.laht.threekt")
}
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")

View File

@@ -6,7 +6,7 @@
## Artifact:
The Maven coordinates of this project are `space.kscience:cern-root-loader:0.5.0`.
The Maven coordinates of this project are `space.kscience:cern-root-loader:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
@@ -16,6 +16,6 @@ repositories {
}
dependencies {
implementation("space.kscience:cern-root-loader:0.5.0")
implementation("space.kscience:cern-root-loader:0.5.1")
}
```

View File

@@ -81,10 +81,12 @@ fun GravityDemo(context: Context) {
y = h
box(200, 5, 200, name = "floor") {
y = -2.5
}
}
box(200, 5, 200, name = "floor") {
y = -2.5
}
}
}

View File

@@ -12,11 +12,7 @@ kscience {
"muon-monitor.js",
development = false,
jvmConfig = {
binaries {
executable {
mainClass.set("ru.mipt.npm.muon.monitor.MMServerKt")
}
}
application("ru.mipt.npm.muon.monitor.MMServerKt")
},
browserConfig = {
commonWebpackConfig {

View File

@@ -12,7 +12,7 @@ repositories {
}
kotlin {
jvmToolchain(17)
jvmToolchain(21)
js {
useEsModules()
browser {

View File

@@ -49,7 +49,7 @@ suspend fun main() = serve(
//axes(200)
ambientLight {
color(Colors.white)
intensity = 3.0
intensity = 1.5
}
val platform = solidGroup("platform") {
cylinder(50, 5, name = "base")
@@ -88,7 +88,7 @@ suspend fun main() = serve(
val incRot = Quaternion.fromRotation(30.degrees, Float64Space3D.zAxis)
context.launch {
this@vision.context.launch {
var time: Long = 0L
while (isActive) {
with(QuaternionAlgebra) {

View File

@@ -13,14 +13,10 @@ kscience {
// useSerialization {
// json()
// }
jvm{
binaries {
executable {
mainClass.set("ru.mipt.npm.sat.SatServerKt")
}
}
jvm {
application("ru.mipt.npm.sat.SatServerKt")
}
jvmMain{
jvmMain {
implementation("io.ktor:ktor-server-cio")
implementation(projects.visionforgeThreejs.visionforgeThreejsServer)
implementation(spclibs.logback.classic)

View File

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

View File

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

View File

@@ -15,10 +15,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
### Fixed
- Fix legend orientation constants
### Security
## 0.5.1 - 2026-01-10
### Fixed
- Fix legend orientation constants
## 0.5.0 (package and versioning change!)
### Changed

View File

@@ -1,7 +1,10 @@
plugins{
id("org.jetbrains.changelog")
id("space.kscience.gradle.project")
}
kscienceProject{
readme {
readmeTemplate = file("docs/templates/README-TEMPLATE.md")
}
}
readme {
readmeTemplate = file("docs/templates/README-TEMPLATE.md")
}

View File

@@ -15,7 +15,7 @@ dependencies {
}
kotlin{
jvmToolchain(17)
jvmToolchain(21)
}
// A workaround for https://youtrack.jetbrains.com/issue/KT-44101

View File

@@ -12,7 +12,7 @@ repositories {
kotlin {
jvm()
jvmToolchain(17)
jvmToolchain(21)
sourceSets {
jvmMain {
dependencies {

View File

@@ -11,11 +11,14 @@ import kotlinx.html.style
import kotlinx.serialization.json.Json
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.Event
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.plotly.*
import space.kscience.plotly.events.PlotlyEventListenerType
import space.kscience.plotly.models.ScatterMode
import space.kscience.plotly.models.TraceType
import space.kscience.plotly.models.histogram
import space.kscience.plotly.models.scatter
import kotlin.random.Random
private fun onDomLoaded(block: (Event) -> Unit) {
@@ -35,7 +38,7 @@ fun main(): Unit = withCanvas {
div {
style = "height:50%; width=100%;"
h1 { +"Histogram demo" }
plotDiv {
plotDiv(Global) {
val rnd = Random(222)
histogram {
name = "Random data"
@@ -79,7 +82,7 @@ fun main(): Unit = withCanvas {
div {
style = "height:50%; width=100%;"
h1 { +"Dynamic trace demo" }
plotDiv {
plotDiv(Global) {
scatter {
x(1, 2, 3, 4)
y(10, 15, 13, 17)

View File

@@ -15,14 +15,14 @@ import space.kscience.plotly.models.geo.openStreetMap
import space.kscience.plotly.plot
import space.kscience.visionforge.plotly.serveSinglePage
import space.kscience.visionforge.server.openInBrowser
import java.net.URL
import java.net.URI
import kotlin.random.Random
suspend fun main() {
//downloading GeoJson
val geoJsonString =
URL("https://raw.githubusercontent.com/isellsoap/deutschlandGeoJSON/main/4_kreise/4_niedrig.geo.json").readText()
URI("https://raw.githubusercontent.com/isellsoap/deutschlandGeoJSON/main/4_kreise/4_niedrig.geo.json").toURL().readText()
// Filtering GeoJson features and creating new feature set
@@ -50,7 +50,7 @@ suspend fun main() {
locations.numbers = features.map { it.id!!.int }
// Set random values to locations
z.numbers = features.map { Random.nextDouble(1.0, 10.0) }
context.launch {
this@serveSinglePage.context.launch {
while (isActive) {
delay(300)
z.numbers = features.map { Random.nextDouble(1.0, 10.0) }

View File

@@ -10,7 +10,7 @@ import space.kscience.plotly.models.geo.json.GeoJsonFeatureCollection
import space.kscience.plotly.models.geo.json.combine
import space.kscience.plotly.models.geo.openStreetMap
import space.kscience.plotly.openInBrowser
import java.net.URL
import java.net.URI
import kotlin.random.Random
@@ -18,7 +18,7 @@ fun main() {
//downloading GeoJson
val geoJsonString =
URL("https://raw.githubusercontent.com/isellsoap/deutschlandGeoJSON/main/4_kreise/4_niedrig.geo.json").readText()
URI("https://raw.githubusercontent.com/isellsoap/deutschlandGeoJSON/main/4_kreise/4_niedrig.geo.json").toURL().readText()
// Filtering GeoJson features and creating new feature set

View File

@@ -0,0 +1,29 @@
@file:OptIn(ExperimentalWasmDsl::class)
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
kotlin("multiplatform")
}
repositories {
mavenCentral()
maven("https://repo.kotlin.link")
}
kotlin {
wasmJs {
browser()
binaries.executable()
}
sourceSets{
wasmJsMain{
dependencies{
implementation(projects.plotlyKt.plotlyKtCore)
implementation(spclibs.kotlinx.coroutines.core)
}
}
}
}

View File

@@ -0,0 +1,146 @@
package space.kscience.plotly.wasmjsdemo
import kotlinx.browser.document
import kotlinx.coroutines.*
import kotlinx.html.TagConsumer
import kotlinx.html.dom.append
import kotlinx.html.h1
import kotlinx.html.js.div
import kotlinx.html.style
import kotlinx.serialization.json.Json
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.Event
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.plotly.*
import space.kscience.plotly.models.ScatterMode
import space.kscience.plotly.models.TraceType
import space.kscience.plotly.models.histogram
import space.kscience.plotly.models.scatter
import kotlin.random.Random
private fun onDomLoaded(block: (Event) -> Unit) {
document.addEventListener("DOMContentLoaded", block)
}
private fun withCanvas(block: TagConsumer<Element>.() -> Unit) {
val element = document.getElementById("canvas") as? HTMLElement
?: error("Element with id 'canvas' not found on page")
println("element loaded")
element.append { block() }
}
@OptIn(DelicateCoroutinesApi::class)
fun main(): Unit = withCanvas {
div {
style = "height:50%; width=100%;"
h1 { +"Histogram demo" }
plotDiv(Global) {
val rnd = Random(222)
histogram {
name = "Random data"
GlobalScope.launch {
while (isActive) {
x.numbers = List(500) { rnd.nextDouble() }
delay(300)
}
}
}
layout {
bargap = 0.1
title {
text = "Basic Histogram"
font {
size = 20
color("black")
}
}
xaxis {
title {
text = "Value"
font {
size = 16
}
}
}
yaxis {
title {
text = "Count"
font {
size = 16
}
}
}
}
}
}
div {
style = "height:50%; width=100%;"
h1 { +"Dynamic trace demo" }
plotDiv(Global) {
scatter {
x(1, 2, 3, 4)
y(10, 15, 13, 17)
mode = ScatterMode.markers
type = TraceType.scatter
}
scatter {
x(2, 3, 4, 5)
y(10, 15, 13, 17)
mode = ScatterMode.lines
type = TraceType.scatter
GlobalScope.launch {
while (isActive) {
delay(500)
marker {
if (Random.nextBoolean()) {
color("magenta")
} else {
color("blue")
}
}
}
}
}
scatter {
x(1, 2, 3, 4)
y(12, 5, 2, 12)
mode = ScatterMode.`lines+markers`
type = TraceType.scatter
marker {
color("red")
}
}
layout {
title = "Line and Scatter Plot"
}
}
}
div {
style = "height:50%; width=100%;"
h1 { +"Deserialization" }
val plot = Plotly.plot {
scatter {
x(1, 2, 3, 4)
y(10, 15, 13, 17)
mode = ScatterMode.markers
type = TraceType.scatter
}
}
val serialized = plot.toJsonString()
println(serialized)
val deserialized = Plot(Json.decodeFromString(MetaSerializer, serialized))
plotDiv(plot = deserialized)
// plotDiv(plot = deserialized).on(PlotlyEventListenerType.CLICK){
// println(it.toString())
// }
}
}

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Kotlin/Wasm Example</title>
</head>
<body>
<script src="wasm-demo.js"></script>
<div id="canvas"></div>
</body>
<script type="application/javascript">
const unhandledError = (event, error) => {
if (error instanceof WebAssembly.CompileError) {
document.getElementById("warning").style.display = "initial";
// Hide a Scary Webpack Overlay which is less informative in this case.
const webpackOverlay = document.getElementById("webpack-dev-server-client-overlay");
if (webpackOverlay != null) webpackOverlay.style.display = "none";
}
}
addEventListener("error", (event) => unhandledError(event, event.error));
addEventListener("unhandledrejection", (event) => unhandledError(event, event.reason));
</script>
</html>

View File

@@ -6,7 +6,7 @@
## Artifact:
The Maven coordinates of this project are `space.kscience:plotly-kt-core:0.5.0`.
The Maven coordinates of this project are `space.kscience:plotly-kt-core:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
@@ -16,6 +16,6 @@ repositories {
}
dependencies {
implementation("space.kscience:plotly-kt-core:0.5.0")
implementation("space.kscience:plotly-kt-core:0.5.1")
}
```

View File

@@ -11,11 +11,9 @@ val plotlyVersion by extra("2.35.3")
//}
kscience {
// jvm()
// js()
fullStack(bundleName = "js/plotly-kt.js")
native()
// wasm()
wasmJs()
useSerialization()
commonMain {
@@ -29,12 +27,18 @@ kscience {
nativeMain {
implementation("com.squareup.okio:okio:3.3.0")
}
wasmJsMain {
api(npm("plotly.js", plotlyVersion))
api("org.jetbrains.kotlinx:kotlinx-browser:0.5.0")
}
}
//tasks.processJupyterApiResources {
// libraryProducers = listOf("space.kscience.plotly.PlotlyIntegration")
//}
kotlinJupyter {
integrations {
producer("space.kscience.plotly.PlotlyIntegration")
}
}
kotlin {
compilerOptions {

View File

@@ -8,7 +8,6 @@ import kotlinx.serialization.json.buildJsonObject
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.node
import space.kscience.dataforge.misc.DFBuilder
import space.kscience.dataforge.names.NameToken
import space.kscience.plotly.models.Layout
import space.kscience.plotly.models.Trace
@@ -18,7 +17,6 @@ import space.kscience.visionforge.*
* The main plot class.
*
*/
@DFBuilder
@Serializable
public class Plot : AbstractVision(), MutableVisionGroup<Trace> {

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

@@ -720,13 +720,11 @@ public fun <T : Scheme> Trace.scheme(
/**
* A base class for Plotly traces
*
* @param uid a unique identifier for this trace
*/
@Serializable
public open class Trace : AbstractVision(), MutableMetaProvider, MetaRepr {
override fun get(name: Name): MutableMeta? = properties.get(name)
override fun get(name: Name): MutableMeta? = properties[name]
override fun set(name: Name, node: Meta?) {
properties[name] = node

View File

@@ -2,6 +2,7 @@ package space.kscience.plotly.models
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import kotlin.time.Instant
/**
* Type-safe accessor class for values in the trace
@@ -27,6 +28,12 @@ public class TraceValues internal constructor(public val owner: MutableMetaProvi
this.value = value.map { it.asValue() }.asValue()
}
public var times: Iterable<Instant?>
get() = value?.list?.map { Instant.parseOrNull(it.string) } ?: emptyList()
set(value) {
this.value = value.map { it.toString().asValue() }.asValue()
}
/**
* Smart fill for trace values. The following types are accepted: [DoubleArray], [IntArray], [Array] of primitive or string,
* [Iterable] of primitive or string.
@@ -50,6 +57,10 @@ public class TraceValues internal constructor(public val owner: MutableMetaProvi
this.strings = strings.asList()
}
public operator fun invoke(vararg times: Instant) {
this.times = times.asList()
}
public operator fun invoke(lists: List<List<Number>>) {
this.value = lists.map { row -> row.map { it.asValue() }.asValue() }.asValue()
}

View File

@@ -61,9 +61,10 @@ public fun Element.plot(
trace.eventFlow.filterIsInstance<VisionPropertyChangedEvent>().onEach { event ->
val traceData = trace.toDynamic()
//need to wrap coordinates into an additional array because plotly API for some reason expects 2D arrays
Plotly.coordinateNames.forEach { coordinate ->
val data = traceData[coordinate]
if (traceData[coordinate] != null) {
if (data != null) {
traceData[coordinate] = arrayOf(data)
}
}
@@ -111,12 +112,24 @@ public class PlotlyElement(public val div: HTMLElement)
* Create a div element and render the plot in it
*/
@OptIn(DelicateCoroutinesApi::class)
@Deprecated("Change arguments positions", ReplaceWith("plotDiv(plot, plotlyConfig, scope)"))
public fun TagConsumer<HTMLElement>.plotDiv(
plotlyConfig: PlotlyConfig,
plot: Plot,
scope: CoroutineScope = plot.manager?.context ?: GlobalScope,
): PlotlyElement = PlotlyElement(div("plotly-kt-plot").apply { plot(plotlyConfig, plot) })
/**
* Create a div element and render the plot in it
*/
@OptIn(DelicateCoroutinesApi::class)
public fun TagConsumer<HTMLElement>.plotDiv(
plot: Plot,
plotlyConfig: PlotlyConfig = PlotlyConfig(),
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.
*/

View File

@@ -7,7 +7,6 @@ import okio.Path.Companion.toPath
/**
* Create a standalone html with the plot
* @param path the reference to html file. If null, create a temporary file
* @param show if true, start the browser after file is created
* @param config represents plotly frame configuration
*/
@UnstablePlotlyAPI

View File

@@ -0,0 +1,57 @@
@file:OptIn(ExperimentalWasmJsInterop::class)
package space.kscience.plotly
import org.w3c.dom.Element
import org.w3c.dom.events.MouseEvent
import kotlin.js.Promise
public external interface ToImgOpts {
public var format: JsString /* 'jpeg' | 'png' | 'webp' | 'svg' */
public var width: JsNumber
public var height: JsNumber
}
public external interface DownloadImgOpts {
public var format: JsString /* 'jpeg' | 'png' | 'webp' | 'svg' */
public var width: JsNumber
public var height: JsNumber
public var filename: JsString
}
@JsName("Plotly")
@JsModule("plotly.js/dist/plotly.js")
public external object PlotlyWasm {
public fun newPlot(
graphDiv: Element,
data: JsArray<JsAny> = definedExternally,
layout: JsAny = definedExternally,
config: JsAny = definedExternally
)
public fun react(
graphDiv: Element,
data: JsArray<JsAny> = definedExternally,
layout: JsAny = definedExternally,
config: JsAny = definedExternally
)
public fun update(
graphDiv: Element,
data: JsAny = definedExternally,
layout: JsAny = definedExternally
)
public fun restyle(graphDiv: Element, update: JsAny, traceIndices: JsArray<JsNumber>? = definedExternally)
public fun relayout(graphDiv: Element, update: JsAny)
public fun toImage(root: Element, opts: ToImgOpts): Promise<JsString>
public fun downloadImage(root: Element, opts: DownloadImgOpts): Promise<JsString>
}
public external interface PlotMouseEvent {
public val points: JsArray<JsAny>
public val event: MouseEvent
}

View File

@@ -0,0 +1,158 @@
@file:OptIn(ExperimentalWasmJsInterop::class)
package space.kscience.plotly
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.html.TagConsumer
import kotlinx.html.div
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import org.w3c.dom.Element
import org.w3c.dom.MutationObserver
import org.w3c.dom.MutationObserverInit
import org.w3c.dom.MutationRecord
import space.kscience.dataforge.meta.MetaRepr
import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.toJson
import space.kscience.plotly.Plotly.coordinateNames
import space.kscience.plotly.models.Trace
import space.kscience.visionforge.VisionGroupCompositionChangedEvent
import space.kscience.visionforge.VisionPropertyChangedEvent
@JsFun("s => JSON.parse(s)")
private external fun json(s: String): JsAny
private fun JsonElement.toWasmJs(): JsAny {
val string = toString()
return json(string)
}
@OptIn(ExperimentalSerializationApi::class)
private fun MetaRepr.toWasmJs(): JsAny = Json.encodeToJsonElement(MetaSerializer, toMeta()).toWasmJs()
private fun List<MetaRepr>.toWasmJs(): JsArray<JsAny> = map { it.toWasmJs() }.toJsArray()
@Suppress("UNUSED_PARAMETER")
private fun myMutationObserverInit(
childList: Boolean?,
attributes: Boolean?,
): MutationObserverInit = js("({ childList: childList, attributes: attributes})")
/**
* Attach a plot to this element or update the existing plot
*/
@OptIn(DelicateCoroutinesApi::class)
public fun Element.plot(
plotlyConfig: PlotlyConfig,
plot: Plot,
scope: CoroutineScope = plot.manager?.context ?: GlobalScope
) {
//send initial data
PlotlyWasm.react(
graphDiv = this,
data = plot.data.toWasmJs(),
layout = plot.layout.toWasmJs(),
config = plotlyConfig.toWasmJs()
)
//start updates
val listenJob = scope.launch {
plot.data.forEachIndexed { index, trace: Trace ->
trace.eventFlow.filterIsInstance<VisionPropertyChangedEvent>().onEach { event ->
val traceMeta = trace.toMeta()
//wrap coordinates into an additional array because plotly API for some reason expects 2D arrays
val traceJson = JsonObject(
traceMeta.items.map { (token, item) ->
val key = token.toStringUnescaped()
val valueUnwrapped = item.toJson()
val value = if (key in coordinateNames) JsonArray(listOf(valueUnwrapped)) else valueUnwrapped
key to value
}.toMap()
)
PlotlyWasm.restyle(this@plot, traceJson.toWasmJs(), listOf(index.toJsNumber()).toJsArray())
}.launchIn(this)
}
plot.eventFlow.onEach { event ->
when (event) {
is VisionGroupCompositionChangedEvent -> PlotlyWasm.react(this@plot, plot.data.toWasmJs())
is VisionPropertyChangedEvent -> PlotlyWasm.relayout(this@plot, plot.layout.toWasmJs())
else -> {
//ignore
}
}
}.launchIn(this)
}
//observe node removal to avoid memory leak
MutationObserver { records: JsArray<MutationRecord>, _ ->
if (records.toList().firstOrNull()?.removedNodes?.length != 0) {
listenJob.cancel()
}
}.observe(this, myMutationObserverInit(childList = true, attributes = false))
}
@Deprecated("Change arguments positions", ReplaceWith("plot(plotlyConfig, plot)"))
public fun Element.plot(plot: Plot, plotlyConfig: PlotlyConfig = PlotlyConfig()): Unit = plot(plotlyConfig, plot)
/**
* Create a plot in this element
*/
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: Element)
/**
* Create a div element and render the plot in it
*/
@OptIn(DelicateCoroutinesApi::class)
public fun TagConsumer<Element>.plotDiv(
plot: Plot,
plotlyConfig: PlotlyConfig = PlotlyConfig(),
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<Element>.plotDiv(
scope: CoroutineScope,
plotlyConfig: PlotlyConfig = PlotlyConfig(),
plotBuilder: Plot.() -> Unit,
): PlotlyElement = PlotlyElement(div("plotly-kt-plot").apply { plot(scope, plotlyConfig, plotBuilder) })
// TODO implement events
//@OptIn(ExperimentalSerializationApi::class)
//public fun PlotlyElement.on(eventType: PlotlyEventListenerType, block: MouseEvent.(PlotlyEvent) -> Unit) {
// div.addEventListener(eventType.eventType) { event: Event ->
// val eventData = PlotlyEvent(event.points.map {
// PlotlyEventPoint(
// curveNumber = it.curveNumber as Int,
// pointNumber = it.pointNumber as? Int,
// x = Value.of(it.x),
// y = Value.of(it.y),
// data = Json.decodeFromDynamic(it.data)
// )
// })
// event.event.block(eventData)
// }
//}

View File

@@ -2,3 +2,20 @@
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:plotly-kt-server:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
mavenCentral()
}
dependencies {
implementation("space.kscience:plotly-kt-server:0.5.1")
}
```

View File

@@ -68,5 +68,6 @@ include(
// ":plotly:examples:fx-demo",
":plotly-kt:examples:compose-demo",
":plotly-kt:examples:js-demo",
":plotly-kt:examples:native-demo"
":plotly-kt:examples:native-demo",
":plotly-kt:examples:wasm-demo"
)

View File

@@ -6,7 +6,7 @@
## Artifact:
The Maven coordinates of this project are `space.kscience:visionforge-compose-html:0.5.0`.
The Maven coordinates of this project are `space.kscience:visionforge-compose-html:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
@@ -16,6 +16,6 @@ repositories {
}
dependencies {
implementation("space.kscience:visionforge-compose-html:0.5.0")
implementation("space.kscience:visionforge-compose-html:0.5.1")
}
```

View File

@@ -6,7 +6,7 @@
## Artifact:
The Maven coordinates of this project are `space.kscience:visionforge-compose-multiplatform:0.5.0`.
The Maven coordinates of this project are `space.kscience:visionforge-compose-multiplatform:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
@@ -16,6 +16,6 @@ repositories {
}
dependencies {
implementation("space.kscience:visionforge-compose-multiplatform:0.5.0")
implementation("space.kscience:visionforge-compose-multiplatform:0.5.1")
}
```

View File

@@ -6,7 +6,7 @@
## Artifact:
The Maven coordinates of this project are `space.kscience:visionforge-core:0.5.0`.
The Maven coordinates of this project are `space.kscience:visionforge-core:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
@@ -16,6 +16,6 @@ repositories {
}
dependencies {
implementation("space.kscience:visionforge-core:0.5.0")
implementation("space.kscience:visionforge-core:0.5.1")
}
```

View File

@@ -8,7 +8,7 @@ kscience {
jvm()
js()
native()
// wasm()
wasmJs()
useCoroutines()
commonMain {
api("space.kscience:dataforge-context:$dataforgeVersion")

View File

@@ -16,14 +16,12 @@ private tailrec fun styleIsDefined(vision: Vision, reference: StyleReference): B
else -> styleIsDefined(vision.parent!!, reference)
}
@VisionBuilder
public fun MutableVision.useStyle(reference: StyleReference) {
//check that style is defined in a parent
//check(styleIsDefined(this, reference)) { "Style reference does not belong to a Vision parent" }
useStyle(reference.name)
}
@VisionBuilder
public fun MutableVision.style(
styleKey: String? = null,
builder: MutableMeta.() -> Unit,
@@ -33,7 +31,6 @@ public fun MutableVision.style(
StyleReference(this, styleName)
}
@VisionBuilder
public fun <T : Scheme> MutableVision.style(
spec: SchemeSpec<T>,
styleKey: String? = null,

View File

@@ -45,6 +45,7 @@ public interface VisionGroup<out V : Vision> : Vision, VisionContainer<V> {
// public val propertyName: Name
//) : VisionEvent
@VisionBuilder
public interface MutableVisionGroup<V : Vision> : VisionGroup<V>, MutableVision, MutableVisionContainer<V> {
/**
@@ -123,7 +124,6 @@ public class SimpleVisionGroup : AbstractVision(), MutableVisionGroup<Vision> {
}
}
@VisionBuilder
public inline fun MutableVisionContainer<Vision>.group(
name: NameToken? = null,
builder: SimpleVisionGroup.() -> Unit = {},
@@ -134,7 +134,6 @@ public inline fun MutableVisionContainer<Vision>.group(
/**
* Define a group with given [token], attach it to this parent and return it.
*/
@VisionBuilder
public inline fun MutableVisionContainer<Vision>.group(
token: String,
builder: SimpleVisionGroup.() -> Unit = {},

View File

@@ -27,7 +27,6 @@ public class VisionOfHtmlForm(
/**
* Create a [VisionOfHtmlForm] and bind this form to the id
*/
@HtmlTagMarker
public inline fun <T, C : TagConsumer<T>> C.visionOfForm(
vision: VisionOfHtmlForm,
action: String? = null,

View File

@@ -9,19 +9,13 @@ import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.*
import space.kscience.visionforge.html.VisionTagConsumer.Companion.DEFAULT_VISION_NAME
import space.kscience.visionforge.setAsRoot
import space.kscience.visionforge.visionManager
@DslMarker
public annotation class VisionDSL
/**
* A placeholder object to attach inline vision builders.
*/
@VisionDSL
@VisionBuilder
public class VisionOutput(override val context: Context, public val name: Name) : ContextAware {
public var meta: Meta = Meta.EMPTY
@@ -55,7 +49,7 @@ public fun VisionOutput.meta(metaRepr: MetaRepr) {
/**
* Modified [TagConsumer] that allows rendering output fragments and visions in them
*/
@VisionDSL
@VisionBuilder
public abstract class VisionTagConsumer<R>(
private val root: TagConsumer<R>,
public val visionManager: VisionManager,
@@ -111,7 +105,6 @@ public abstract class VisionTagConsumer<R>(
* Insert a vision in this HTML.
* TODO replace by multi-receiver
*/
@VisionDSL
public open fun <T> TagConsumer<T>.vision(
name: Name? = null,
buildOutput: VisionOutput.() -> Vision,
@@ -125,13 +118,11 @@ public abstract class VisionTagConsumer<R>(
/**
* TODO to be replaced by multi-receiver
*/
@VisionDSL
public fun <T> TagConsumer<T>.vision(
name: String?,
buildOutput: VisionOutput.() -> Vision,
): T = vision(name?.parseAsName(), buildOutput)
@VisionDSL
public open fun <T> TagConsumer<T>.vision(
vision: Vision,
name: Name? = null,

View File

@@ -75,7 +75,6 @@ private fun <T> TagConsumer<T>.vision(
* Insert a vision in this HTML.
*/
context(htmlContext: HtmlVisionContext)
@VisionDSL
public fun <T> TagConsumer<T>.vision(
name: Name? = null,
visionProvider: VisionOutput.() -> Vision,
@@ -90,7 +89,6 @@ public fun <T> TagConsumer<T>.vision(
* Insert a vision in this HTML.
*/
context(htmlContext: HtmlVisionContext)
@VisionDSL
public fun <T> TagConsumer<T>.vision(
name: String?,
visionProvider: VisionOutput.() -> Vision,

View File

@@ -1,10 +1,10 @@
package space.kscience.visionforge.meta
import kotlinx.coroutines.cancel
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 kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.int
@@ -13,14 +13,14 @@ import space.kscience.dataforge.meta.set
import space.kscience.visionforge.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.milliseconds
internal class PropertyFlowTest {
private val manager = Global.request(VisionManager)
@Test
fun testChildrenPropertyFlow() = runTest(timeout = 200.milliseconds) {
fun testChildrenPropertyFlow(): Unit = runBlocking {
val parent = MutableVisionGroup(manager) {
properties {
@@ -37,33 +37,30 @@ internal class PropertyFlowTest {
val child = parent.getVision("child") as MutableVisionGroup<*>
val changesFlow = child.flowProperty("test", inherited = true)
launch {
val changesFlow = child.flowProperty("test", inherited = true).stateIn(this)
val collectedValues = ArrayList<Int>(5)
assertEquals(22, child.readProperty("test", true).int)
changesFlow.onEach {
collectedValues.add(it.int!!)
}.launchIn(backgroundScope)
delay(10)
assertEquals(22, changesFlow.value.int)
delay(1)
assertEquals(22, child.readProperty("test", true).int)
parent.properties["test1"] = 88 // another property
parent.properties["test1"] = 88 // another property
child.properties.remove("test")
child.properties.remove("test")
assertEquals(11, child.readProperty("test", true).int)
delay(10)
assertEquals(11, changesFlow.value.int)
delay(1)
assertEquals(11, child.readProperty("test", true).int)
parent.properties["test"] = 33
assertEquals(33, child.readProperty("test", true).int)
parent.properties["test"] = 33
delay(1)
assertEquals(33, child.readProperty("test", true).int)
delay(10)
assertEquals(33, changesFlow.value.int)
advanceUntilIdle()
//assertEquals(listOf(22, 11, 33), collectedValues)
assertEquals(22, collectedValues.first())
assertEquals(33, collectedValues.last())
println("finished")
cancel()
}
}
}

View File

@@ -6,7 +6,7 @@
## Artifact:
The Maven coordinates of this project are `space.kscience:visionforge-gdml:0.5.0`.
The Maven coordinates of this project are `space.kscience:visionforge-gdml:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
@@ -16,6 +16,6 @@ repositories {
}
dependencies {
implementation("space.kscience:visionforge-gdml:0.5.0")
implementation("space.kscience:visionforge-gdml:0.5.1")
}
```

View File

@@ -7,6 +7,8 @@ kscience {
js {
binaries.library()
}
// native()
// wasmJs()
dependencies {
api(projects.visionforgeSolid)
api("space.kscience:gdml:0.5.0")
@@ -14,4 +16,9 @@ kscience {
dependencies(jvmTest) {
implementation(spclibs.logback.classic)
}
}
readme {
// TODO remove into a separate library
maturity = space.kscience.gradle.Maturity.DEPRECATED
}

View File

@@ -6,7 +6,7 @@ Common visionforge jupyter module
## Artifact:
The Maven coordinates of this project are `space.kscience:visionforge-jupyter:0.5.0`.
The Maven coordinates of this project are `space.kscience:visionforge-jupyter:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
@@ -16,6 +16,6 @@ repositories {
}
dependencies {
implementation("space.kscience:visionforge-jupyter:0.5.0")
implementation("space.kscience:visionforge-jupyter:0.5.1")
}
```

View File

@@ -6,7 +6,7 @@ Jupyter api artifact including all common modules
## Artifact:
The Maven coordinates of this project are `space.kscience:visionforge-jupyter-common:0.5.0`.
The Maven coordinates of this project are `space.kscience:visionforge-jupyter-common:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
@@ -16,6 +16,6 @@ repositories {
}
dependencies {
implementation("space.kscience:visionforge-jupyter-common:0.5.0")
implementation("space.kscience:visionforge-jupyter-common:0.5.1")
}
```

View File

@@ -36,10 +36,11 @@ kscience {
}
}
//tasks.processJupyterApiResources {
// libraryProducers = listOf("space.kscience.visionforge.jupyter.JupyterCommonIntegration")
//}
kotlinJupyter {
integrations {
producer("space.kscience.visionforge.jupyter.JupyterCommonIntegration")
}
}
readme {
maturity = space.kscience.gradle.Maturity.EXPERIMENTAL

View File

@@ -6,7 +6,7 @@
## Artifact:
The Maven coordinates of this project are `space.kscience:visionforge-markdown:0.5.0`.
The Maven coordinates of this project are `space.kscience:visionforge-markdown:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
@@ -16,6 +16,6 @@ repositories {
}
dependencies {
implementation("space.kscience:visionforge-markdown:0.5.0")
implementation("space.kscience:visionforge-markdown:0.5.1")
}
```

View File

@@ -6,7 +6,7 @@
## Artifact:
The Maven coordinates of this project are `space.kscience:visionforge-server:0.5.0`.
The Maven coordinates of this project are `space.kscience:visionforge-server:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
@@ -16,6 +16,6 @@ repositories {
}
dependencies {
implementation("space.kscience:visionforge-server:0.5.0")
implementation("space.kscience:visionforge-server:0.5.1")
}
```

View File

@@ -6,7 +6,7 @@
## Artifact:
The Maven coordinates of this project are `space.kscience:visionforge-solid:0.5.0`.
The Maven coordinates of this project are `space.kscience:visionforge-solid:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
@@ -16,6 +16,6 @@ repositories {
}
dependencies {
implementation("space.kscience:visionforge-solid:0.5.0")
implementation("space.kscience:visionforge-solid:0.5.1")
}
```

View File

@@ -8,7 +8,7 @@ kscience {
jvm()
js()
native()
// wasm()
wasmJs()
useSerialization {
json()
}

View File

@@ -84,7 +84,6 @@ public class Solids(meta: Meta) : VisionPlugin(meta), MutableVisionContainer<Sol
}
}
@VisionBuilder
public inline fun VisionOutput.solid(options: Canvas3DOptions? = null, block: SolidGroup.() -> Unit): SolidGroup {
requirePlugin(Solids)
options?.let {
@@ -97,6 +96,5 @@ public inline fun VisionOutput.solid(options: Canvas3DOptions? = null, block: So
}
}
@VisionBuilder
public inline fun VisionOutput.solid(options: Canvas3DOptions.() -> Unit, block: SolidGroup.() -> Unit): SolidGroup =
solid(Canvas3DOptions(options), block)

View File

@@ -6,7 +6,7 @@
## Artifact:
The Maven coordinates of this project are `space.kscience:visionforge-tables:0.5.0`.
The Maven coordinates of this project are `space.kscience:visionforge-tables:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
@@ -16,6 +16,6 @@ repositories {
}
dependencies {
implementation("space.kscience:visionforge-tables:0.5.0")
implementation("space.kscience:visionforge-tables:0.5.1")
}
```

View File

@@ -19,6 +19,8 @@ kscience {
}
}
}
native()
wasmJs()
useSerialization()
commonMain {

View File

@@ -6,7 +6,7 @@
## Artifact:
The Maven coordinates of this project are `space.kscience:visionforge-threejs:0.5.0`.
The Maven coordinates of this project are `space.kscience:visionforge-threejs:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
@@ -16,6 +16,6 @@ repositories {
}
dependencies {
implementation("space.kscience:visionforge-threejs:0.5.0")
implementation("space.kscience:visionforge-threejs:0.5.1")
}
```

View File

@@ -6,7 +6,7 @@
## Artifact:
The Maven coordinates of this project are `space.kscience:visionforge-threejs-server:0.5.0`.
The Maven coordinates of this project are `space.kscience:visionforge-threejs-server:0.5.1`.
**Gradle Kotlin DSL:**
```kotlin
@@ -16,6 +16,6 @@ repositories {
}
dependencies {
implementation("space.kscience:visionforge-threejs-server:0.5.0")
implementation("space.kscience:visionforge-threejs-server:0.5.1")
}
```