From 838c22cf4af44e5319438d21fa2bc869d2d731df Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Wed, 12 Jul 2023 22:09:48 +0200 Subject: [PATCH] This renders something --- build.sbt | 6 +- calico/src/main/scala/viz/package.scala | 96 ++++++++++++------- .../scala/livechart/LiveChartCalico.scala | 83 ++++++---------- raw_docs/ScalaVersions/scalaJS.md | 92 ++++++++++++++---- 4 files changed, 166 insertions(+), 111 deletions(-) diff --git a/build.sbt b/build.sbt index a6f9caa..d4e2d62 100644 --- a/build.sbt +++ b/build.sbt @@ -48,10 +48,6 @@ ThisBuild / tlSonatypeUseLegacyHost := false ThisBuild / tlCiReleaseBranches := Seq("main") ThisBuild / scalaVersion := scalaV -// ThisBuild / scalaJSLinkerConfig ~= ( -// _.withModuleKind(ModuleKind.ESModule) -// ) - lazy val generated = crossProject(JVMPlatform, JSPlatform) .in(file("generated")) .settings( @@ -65,7 +61,7 @@ lazy val generated = crossProject(JVMPlatform, JSPlatform) ) ) -lazy val root = tlCrossRootProject.aggregate(core, generated, laminarIntegration, unidocs, tests) +lazy val root = tlCrossRootProject.aggregate(core, generated, laminarIntegration,calicoIntegration, unidocs, tests) lazy val core = crossProject(JVMPlatform, JSPlatform) .in(file("core")) diff --git a/calico/src/main/scala/viz/package.scala b/calico/src/main/scala/viz/package.scala index e2c0da7..96cb90a 100644 --- a/calico/src/main/scala/viz/package.scala +++ b/calico/src/main/scala/viz/package.scala @@ -21,12 +21,21 @@ import scala.scalajs.js.annotation.* import org.scalajs.dom import scalajs.js.JSON +import calico.* +import calico.html.io.{*, given} +import calico.unsafe.given +import calico.syntax.* +import cats.effect.* +import fs2.* +import fs2.concurrent.* +import fs2.dom.* import viz.PlotTargets.doNothing import viz.extensions.* import viz.vega.facades.EmbedOptions import viz.vega.facades.VegaView import viz.vega.facades.EmbedResult +import cats.effect.IO import viz.vega.facades.Helpers.* @@ -63,40 +72,59 @@ object CalicoViz: * @param embedOpt * \- optionally, the embed options you wish to use */ - // def viewEmbed( - // chart: Spec, - // inDivOpt: Option[Div] = None, - // embedOpt: Option[EmbedOptions] = None - // ): (Div, Signal[Option[VegaView]]) = - // val specObj = JSON.parse(chart.spec).asInstanceOf[js.Object] - - // val (embeddedIn, embedResult) = (inDivOpt, embedOpt) match - // case (Some(thisDiv), Some(opts)) => - // val p: js.Promise[EmbedResult] = viz.vega.facades.VegaEmbed(thisDiv.ref, specObj, opts) - // (thisDiv, p) - // case (Some(thisDiv), None) => - // val specObj = JSON.parse(chart.spec).asInstanceOf[js.Object] - // val p: js.Promise[EmbedResult] = viz.vega.facades.VegaEmbed(thisDiv.ref, specObj, EmbedOptions) - // (thisDiv, p) - // case (None, Some(opts)) => - // val newDiv = div( - // width := "40vmin", - // height := "40vmin" - // ) - // val p: js.Promise[EmbedResult] = viz.vega.facades.VegaEmbed(newDiv.ref, specObj, opts) - // (newDiv, p) - // case (None, None) => - // val newDiv = div( - // width := "40vmin", - // height := "40vmin" - // ) - // val p: js.Promise[EmbedResult] = viz.vega.facades.VegaEmbed(newDiv.ref, specObj, EmbedOptions) - // (newDiv, p) - - // val viewSignal: Signal[Option[VegaView]] = Signal.fromJsPromise(embedResult).map(in => in.map(_.view)) - // // val viewSignal: Signal[Option[VegaView]] = Signal.fromValue(None) - - // (embeddedIn, viewSignal) + def viewEmbed( + chart: Spec, + inDivOpt: Option[Resource[IO, HtmlDivElement[IO]]] = None, + embedOpt: Option[EmbedOptions] = None + ): Resource[IO, (HtmlDivElement[IO], IO[VegaView])] = + + val specObj = JSON.parse(chart.spec).asInstanceOf[js.Object] + val tmp = (inDivOpt, embedOpt) match + case (Some(thisDiv), Some(opts)) => + thisDiv.map { (d: HtmlDivElement[IO]) => + val dCheat = d.asInstanceOf[org.scalajs.dom.html.Div] + dCheat.style.height = "40vmin" + dCheat.style.width = "40vmin" + val p: js.Promise[EmbedResult] = viz.vega.facades.VegaEmbed(d.asInstanceOf[org.scalajs.dom.html.Div], specObj, opts) + val pIop = IO.fromPromise(IO(p)) + (d, pIop.map(_.view)) + } + //case (Some(thisDiv), None) => ??? + // This case doesn't work + // thisDiv.flatMap { (d: HtmlDivElement[IO]) => + // val dCheat = d.asInstanceOf[org.scalajs.dom.html.Div] + // dCheat.style.height = "40vmin" + // dCheat.style.width = "40vmin" + // val p: js.Promise[EmbedResult] = viz.vega.facades.VegaEmbed(d.asInstanceOf[org.scalajs.dom.html.Div], specObj, opts) + // val pIop = IO.fromPromise[EmbedResult](IO(p)).toResource + // pIop.map(_.view).map((d, _)) + // } + case _ => ??? + // case (Some(thisDiv), None) => + // val specObj = JSON.parse(chart.spec).asInstanceOf[js.Object] + // val p: js.Promise[EmbedResult] = viz.vega.facades.VegaEmbed(thisDiv.ref, specObj, EmbedOptions) + // (thisDiv, p) + // case (None, Some(opts)) => + // val newDiv = div( + // width := "40vmin", + // height := "40vmin" + // ) + // val p: js.Promise[EmbedResult] = viz.vega.facades.VegaEmbed(newDiv.ref, specObj, opts) + // (newDiv, p) + // case (None, None) => + // val newDiv = div( + // width := "40vmin", + // height := "40vmin" + // ) + // val p: js.Promise[EmbedResult] = viz.vega.facades.VegaEmbed(newDiv.ref, specObj, EmbedOptions) + // (newDiv, p) + tmp + + // val viewSignal: IO[Option[VegaView]] = IO.fromPromise(embedResult).map(in => in.map(_.view)) + // // val viewSignal: Signal[Option[VegaView]] = Signal.fromValue(None) + + // (embeddedIn, viewSignal) + end viewEmbed // end viewEmbed /** Embed a chart in a div. This method is a good choice if you are not at all worried about performance (mostly you diff --git a/jsdocs/src/main/scala/livechart/LiveChartCalico.scala b/jsdocs/src/main/scala/livechart/LiveChartCalico.scala index 273ce19..7d928b4 100644 --- a/jsdocs/src/main/scala/livechart/LiveChartCalico.scala +++ b/jsdocs/src/main/scala/livechart/LiveChartCalico.scala @@ -4,6 +4,8 @@ import scala.scalajs.js import scala.scalajs.js.annotation.* //import scala.util.Random +import viz.extensions.* +import viz.vega.plots.{BarChart, given} import calico.* import calico.html.io.{*, given} import calico.unsafe.given @@ -13,21 +15,19 @@ import cats.effect.std.Random import fs2.* import fs2.concurrent.* import fs2.dom.* +import viz.vega.facades.EmbedOptions object MyCalicoApp extends IOWebApp: - def render: Resource[IO, HtmlElement[IO]] = - calicoChart -end MyCalicoApp + def render: Resource[IO, HtmlElement[IO]] = calicoChart -val dataSignal = SignallingRef[IO] - .of(List(2.4, 3.4, 5.1, -2.3)) +end MyCalicoApp -def calicoChart: Resource[IO, HtmlDivElement[IO]] = +def calicoChart: Resource[IO, HtmlElement[IO]] = SignallingRef[IO] .of(List(2.4, 3.4, 5.1, -2.3)) .product(Channel.unbounded[IO, Int]) .toResource - .flatMap { (data, diff) => + .flatMap { (data: SignallingRef[cats.effect.IO, List[Double]], diff) => div( p("We want to make it as easy as possible, to build a chart"), span("Here's a random data set: "), @@ -36,60 +36,33 @@ def calicoChart: Resource[IO, HtmlDivElement[IO]] = "Add a random number", onClick --> ( _.evalMap(_ => - //IO.println("clicked") >> - Random.scalaUtilRandom[IO].toResource.use{r => r.nextDouble.map(_* 5)} + Random.scalaUtilRandom[IO].toResource.use(r => r.nextDouble.map(_ * 5)) ).foreach(newD => val d = data.get IO.println(newD) >> - d.map(_ :+ newD).map(data.set).void + data.update(_ :+ newD).void ) ) ), - p("") - // child <-- data.signal.map { data => - // val barChart: BarChart = data.plotBarChart(List(viz.Utils.fillDiv)) - // LaminarViz.simpleEmbed(barChart) - // } - ) - } - -def Counter(label: String, initialStep: Int): Resource[IO, HtmlDivElement[IO]] = - SignallingRef[IO].of(initialStep).product(Channel.unbounded[IO, Int]).toResource.flatMap { (step, diff) => - - val allowedSteps = List(1, 2, 3, 5, 10) - - div( - p( - "Step: ", - select.withSelf { self => - ( - allowedSteps.map(step => option(value := step.toString, step.toString)), - value <-- step.map(_.toString), - onChange --> { - _.evalMap(_ => self.value.get).map(_.toIntOption).unNone.foreach(step.set) - } + p(""), + data.map { data => + val barChart: BarChart = data.plotBarChart( + List( + viz.Utils.fillDiv, + viz.Utils.removeYAxis + ) ) - } - ), - p( - label + ": ", - b(diff.stream.scanMonoid.map(_.toString).holdOptionResource), - " ", - button( - "-", - onClick --> { - _.evalMap(_ => step.get).map(-1 * _).foreach(diff.send(_).void) + val chartDiv = div("") + chartDiv.flatMap{ d => + // To my astonishment, this doesn't work... + /* val dCheat = d.asInstanceOf[org.scalajs.dom.html.Div] + dCheat.style.height = "40vmin" + dCheat.style.width = "40vmin" */ + // end yuck + + // I had to set the div size down in here. Then it worked. But I have no idea why. + viz.CalicoViz.viewEmbed(barChart, Some(chartDiv), Some(EmbedOptions)).map(_._1) } - ), - button( - "+", - onClick --> (_.evalMap(_ => step.get).foreach(diff.send(_).void)) - ) + } ) - ) - } - -val app: Resource[IO, HtmlDivElement[IO]] = div( - h1("Let's count!"), - Counter("Sheep", initialStep = 3) -) + } \ No newline at end of file diff --git a/raw_docs/ScalaVersions/scalaJS.md b/raw_docs/ScalaVersions/scalaJS.md index be35ab3..bca7b25 100644 --- a/raw_docs/ScalaVersions/scalaJS.md +++ b/raw_docs/ScalaVersions/scalaJS.md @@ -1,18 +1,9 @@ # Scala JS -The charts in these documents, are display using scala JS :-). - -What turns out to be really nice about scala JS support, is the seamless transition between exploration in a repl on the JVM, luxuriating in it's rapid feedback and typsafe tooling, and subsequent publication into a browser with scala JS. It's the same code! There is a only a little more ceremony than with a repl - we need to decide the charts position in the document. i.e. find it a parent. - -Gotcha : dedav ***does not include*** the underlying JS libraries out of it's box. - -I may list out some toy examples on the github readme. Here's one... -[Mill, Scala Js, Snowpack, Laminar, Dedav](https://github.com/Quafadas/scalajs-snowpack-example) - ## Scala JS UI frameworks It turns out, that scala JS Dom is simply a facade for the browser API. Dedav works, through providing a reference to a scala js dom Div element. -Due to how fundamental the statement above is, we implicitly support _all_ JS UI frameworks. It must be possible to coerce the DIV wrapper of your framework into a scala js dom Div. However, some frameworks have a little more polish... +Due to how fundamental the statement above is, we implicitly support _all_ JS UI frameworks. It must be possible to coerce the DIV wrapper of your framework into a scala js dom Div. However, as I use some frameworks myself, it's a little easier to get started ... ## Integrations @@ -20,7 +11,7 @@ Due to how fundamental the statement above is, we implicitly support _all_ JS UI See the `LaminarViz.simpleEmbed` function, to get started. It returns a div, which you can put, anywhere you want in your app. Here it's just added where it's constructed for the sake of simplicity. -The only constraint, is that it must have a well defined size and hieght. +The only constraint, is that the div must have a well defined size and height. ```scala mdoc:js import com.raquo.laminar.api.L._ @@ -158,7 +149,7 @@ object chartExample: chartDiv, p("You last clicked on : ", child.text <-- chartDataClickedBus.map(textIfObject)), p("You last hovered on : ", child.text <-- aSignalBus.map(textIfObject)), - p() + p(),p("") ) end apply end chartExample @@ -172,7 +163,71 @@ Finally, this sets out some low leverl building blocks. If you were to know they ### Calico -Most peculiar +```scala mdoc:js +import scala.scalajs.js +import scala.scalajs.js.annotation.* + +import viz.extensions.* +import viz.vega.plots.{BarChart, given} +import calico.* +import calico.html.io.{*, given} +import calico.unsafe.given +import calico.syntax.* +import cats.effect.* +import cats.effect.std.Random +import fs2.* +import fs2.concurrent.* +import fs2.dom.* +import viz.vega.facades.EmbedOptions + +calicoChart.renderInto(node.asInstanceOf[fs2.dom.Node[IO]]).useForever.unsafeRunAndForget() + +def calicoChart: Resource[IO, HtmlElement[IO]] = + SignallingRef[IO] + .of(List(2.4, 3.4, 5.1, -2.3)) + .product(Channel.unbounded[IO, Int]) + .toResource + .flatMap { (data: SignallingRef[cats.effect.IO, List[Double]], diff) => + div( + p("We want to make it as easy as possible, to build a chart"), + span("Here's a random data set: "), + data.map(in => p(in.mkString("[", ",", "]"))), + button( + "Add a random number", + onClick --> ( + _.evalMap(_ => + Random.scalaUtilRandom[IO].toResource.use(r => r.nextDouble.map(_ * 5)) + ).foreach(newD => + val d = data.get + IO.println(newD) >> + data.update(_ :+ newD).void + ) + ) + ), + p(""), + data.map { data => + val barChart: BarChart = data.plotBarChart( + List( + viz.Utils.fillDiv, + viz.Utils.removeYAxis + ) + ) + val chartDiv = div("") + chartDiv.flatMap{ d => + // To my astonishment, this doesn't work... + /* val dCheat = d.asInstanceOf[org.scalajs.dom.html.Div] + dCheat.style.height = "40vmin" + dCheat.style.width = "40vmin" */ + // end yuck + + // I had to set the div size down in here. Then it worked. + viz.CalicoViz.viewEmbed(barChart, Some(chartDiv), Some(EmbedOptions)).map(_._1) + } + } + ) + } + +``` ### MDoc Is how this documentation works. Setup mdoc with scalajs bundler, and include vega in the bundle. Read the source of this library :-). @@ -199,11 +254,14 @@ tlSiteHeliumConfig := { The github repo of this documentation is a successful example! -# Ecosystem support -We have two orthogonal problems +# Conclusion + +The charts in these documents, are displayed using scala JS :-). + +What turns out to be really nice about scala JS support, is the seamless transition between exploration in a repl on the JVM, luxuriating in it's rapid feedback and typsafe tooling, and subsequent publication into a browser with scala JS. It's the same code! There is a only a little more ceremony than with a repl - we need to decide the charts position in the document. i.e. find it a parent. + +Gotcha : dedav ***does not include*** the underlying JS libraries out of it's box. -1. How to we obtain the javascript libraries? -2. How to support the bouquet of scala JS UI frameworks? ## Javascript libraries The example dependency is set out above. It _should_ work with _any_ bundling solution, or even by directly embedding the dependancies in the header of the html. Your choice.