diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..82da0b16 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..be28ae10 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,5 @@ +# activate meaningful git annotations with: +# git config blame.ignoreRevsFile .git-blame-ignore-revs + +# scalafmt +0ad886c35bdf9c8ad2bfb0501070b8b2ce810710 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..5ace4600 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/scripts/gpg-setup.sh b/.github/scripts/gpg-setup.sh new file mode 100755 index 00000000..ad58f407 --- /dev/null +++ b/.github/scripts/gpg-setup.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +# from https://github.com/coursier/apps/blob/f1d2bf568bf466a98569a85c3f23c5f3a8eb5360/.github/scripts/gpg-setup.sh + +echo $PGP_SECRET | base64 --decode | gpg --import --no-tty --batch --yes + +echo "allow-loopback-pinentry" >>~/.gnupg/gpg-agent.conf +echo "pinentry-mode loopback" >>~/.gnupg/gpg.conf + +gpg-connect-agent reloadagent /bye diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..93b98f62 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI +on: + push: + branches: + - master + tags: + - "v*" + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + SCALA_VERSION: ["2.12.19", "2.13.14"] + steps: + - uses: actions/checkout@v4 + - uses: coursier/cache-action@v6 + - uses: coursier/setup-action@v1.3.5 + with: + jvm: 8 + - run: | + sbtn ++$SCALA_VERSION test + sbtn ++$SCALA_VERSION mimaReportBinaryIssues + env: + SCALA_VERSION: ${{ matrix.SCALA_VERSION }} + + publish: + if: github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: coursier/cache-action@v6 + - uses: coursier/setup-action@v1.3.5 + with: + jvm: 8 + - run: .github/scripts/gpg-setup.sh + env: + PGP_SECRET: ${{ secrets.PUBLISH_SECRET_KEY }} + - name: Release + run: sbtn ci-release + env: + PGP_PASSPHRASE: ${{ secrets.PUBLISH_SECRET_KEY_PASSWORD }} + PGP_SECRET: ${{ secrets.PUBLISH_SECRET_KEY }} + SONATYPE_PASSWORD: ${{ secrets.PUBLISH_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.PUBLISH_USER }} diff --git a/.gitignore b/.gitignore index 2f7896d1..aeb2de6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ target/ +.bsp/ +.idea/ +metals.sbt +.vscode/ diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 00000000..26c5bcea --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,27 @@ +version = "3.8.1" + +runner.dialect = scala213 + +preset = defaultWithAlign + +maxColumn = 120 + +assumeStandardLibraryStripMargin = true + +align { + arrowEnumeratorGenerator = true +} + +newlines { + penalizeSingleSelectMultiArgList = true +} + +rewrite { + rules = [Imports, PreferCurlyFors, RedundantParens] + imports.sort = scalastyle +} + +docstrings.wrap=no + +// only format files tracked by git +project.git = true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2579e8c3..00000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: scala -scala: -- 2.11.8 -- 2.12.1 -jdk: -- oraclejdk8 -script: sbt -batch ++${TRAVIS_SCALA_VERSION} test $(if [[ "$TRAVIS_PULL_REQUEST" == - "false" && ${JAVA_HOME} == $(jdk_switcher home oraclejdk8) && "${TRAVIS_BRANCH}" - == "master" ]]; then echo "publish"; fi) -cache: - directories: - - $HOME/.coursier - - $HOME/.ivy2/cache - - $HOME/.sbt -branches: - only: - - master -env: - global: - - secure: kuxKHPyXy1Bem0JkzkPtWgGmEOIknL5gHDD9kHTn1M7bwgGhqu8KT0AIeHo+h6iJZeALXnXfFZaF1YpOSEq/tIS33PLd5iHrcfmM86vXB7ZFLWBtGh3xytQ7TkvMlP8Xu7vJTcXdJw/1zKW7GJW+9AMK8zrpcBPWhnX/S+Bq1YLya1yxbOJ+qXx0ToL7Bn4AKp4Mucu8HiZ1eVPXXn1aGiCGBDBQDA0EzZvjpZeuJCIUjtr+B5KB4pRF+JHXzQJZeT4jh5zivBr79kPt2gbLK9YfQMLv5a5W2cl8GNv81lQCHsKPXHaQSLXoVz1JSgnfXqJYSXQqZx5DKlsXOgKwg5jzJdJehhEFnPL5gpIM9zLgRhamUea+c4CeP/WzuaR7mKTqovkpXpRfBnHcSXIyTqAhRx7DapWLeIsijSty5arHf0sY9fMeTvUgwoQpPq6VMl0Z5iGo/dQewnruqKIbmXW2NeJE4RMF70DDapLUPgtl8xRsGiEdes9pGKqjn7qj5+9I/fiA3M5SBJqckQfFrnVPbqC+lGnziPa9tG1c7cKXT5tbQDqc8G4K0wazH0c6lDHiwtiMmIGNCTn+PlFf7gEdJ6G5Bsk4TlE/8yqWcCtOUragK6E9pxGaEwCiJTHzXq3NnPByLIFBV/Cz9PpAJze1CeJ4qLPsoHniICKN8j4= - - secure: GXYsqWjiG4t85rt+O4oSWHIg20NGvqQ27QjuhfiU+0sIqsbopMqZhwwKdRDebz7DwpJ3yFkxqchAps2qJjuOMPByLpTIFiNXwBvN/HX+qh/+lTq/qurFcepfKtsgJwrVly5IPaf5Er5jPALiongBt8iJ7LcrOGLYF40GTqYza4VdUMYywEhsd2LJ+mfUjTvLbfrKMwfvfk7nxBK8PLOYqPaX75Q85P5Y8CjArzIpTVh7agQzM9UAKd8HlRKRo2tPYrs7GPFczVqR+SZJONXUf/HcaEhTxOaeC6jfKrDldetFxY+gwD6+T5q4g5bnzEsdzTgmIDYpO/FWB+mNPsnNs1lZiBPi0JpPM0ZoJ93CBun2+CDLTLXlvnHHEpen4LK8ZwUg+Qkel5rJ+ncIHwYQxChv5KdjGgTgaLaFgPbZp6V3wsjlexOKhI+TKG+dJ3hYoZTVwzPi7giPkvNScwExX1L94zmJcEUaTqfF+c4tSX/3xYj4wgIuNqA6c+nUOfPNhkLiEmb9obfFiOCrwkY+LwS/dgOpQNXS7CJGnbut6YBQj+ixUxYiaoUiVhg/OInCUgaHJCtBhh8QBV33tGlq705j023VFMPgTjkW5PzW4BlfYsS0rre4uZzSwtgmxmL2Mq2Hr4d+SywqVFIP81+z16tFHwSh/vO7mw5jSE4wxb8= diff --git a/README.md b/README.md index 6191c85f..b63cac2a 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,19 @@ Scala bindings for [plotly.js](https://plot.ly/javascript/) [![Build Status](https://travis-ci.org/alexarchambault/plotly-scala.svg?branch=master)](https://travis-ci.org/alexarchambault/plotly-scala) [![Join the chat at https://gitter.im/alexarchambault/plotly-scala](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/alexarchambault/plotly-scala?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Maven Central](https://img.shields.io/maven-central/v/org.plotly-scala/core_2.11.svg)](https://maven-badges.herokuapp.com/maven-central/org.plotly-scala/core_2.11) -[![ScalaDoc](http://javadoc-badge.appspot.com/org.plotly-scala/core_2.11.svg?label=scaladoc)](http://javadoc-badge.appspot.com/org.plotly-scala/core_2.11) +[![Maven Central](https://img.shields.io/maven-central/v/org.plotly-scala/plotly-render_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/org.plotly-scala/plotly-render_2.13) +[![ScalaDoc](http://javadoc-badge.appspot.com/org.plotly-scala/plotly-render_2.13.svg?label=scaladoc)](http://javadoc-badge.appspot.com/org.plotly-scala/plotly-render_2.13) [Demo](https://alexarchambault.github.io/plotly-scala/) *plotly-scala* is a Scala library able to output JSON that can be passed to [plotly.js](https://plot.ly/javascript/). Its classes closely follow the API of plotly.js, so that one can use plotly-scala by following the [documentation](https://plot.ly/javascript/) of plotly.js. These classes can be converted to JSON, that can be fed directly to plotly.js. -It can be used from [jupyter-scala](https://github.com/alexarchambault/jupyter-scala), from scala-js, or from a Scala REPL like [Ammonite](https://github.com/lihaoyi/Ammonite), to plot things straightaway in the browser. +It can be used from [almond](https://github.com/jupyter-scala/jupyter-scala/tree/develop), from scala-js, or from a Scala REPL like [Ammonite](https://github.com/lihaoyi/Ammonite), to plot things straightaway in the browser. It runs demos of the plotly.js documentation during its tests, to ensure that it is fine with all their features. That allows it to reliably cover a wide range of the plotly.js features - namely, all the examples of the supported sections of the plotly.js documentation are guaranteed to be fine. +It is published for both scala 2.12 and 2.13. + ## Table of content 1. [Quick start](#quick-start) @@ -24,18 +26,17 @@ It runs demos of the plotly.js documentation during its tests, to ensure that it ## Quick start -### From jupyter-scala +### From almond -Simply add the `org.plotly-scala::plotly-jupyter-scala:0.3.1` dependency to the notebook, initialize plotly-scala, and use it, like +Add the `org.plotly-scala::plotly-almond:0.8.1` dependency to the notebook. (Latest version: [![Maven Central](https://img.shields.io/maven-central/v/org.plotly-scala/plotly-render_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/org.plotly-scala/plotly-render_2.13)) +Then initialize plotly-scala, and use it, like ```scala -import $ivy.`org.plotly-scala::plotly-jupyter-scala:0.3.1` +import $ivy.`org.plotly-scala::plotly-almond:0.8.1` import plotly._ import plotly.element._ import plotly.layout._ -import plotly.JupyterScala._ - -plotly.JupyterScala.init() +import plotly.Almond._ val (x, y) = Seq( "Banana" -> 10, @@ -46,14 +47,19 @@ val (x, y) = Seq( Bar(x, y).plot() ``` +#### JupyterLab +If you're using [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/), you have to install [jupyterlab-plotly](https://plotly.com/python/getting-started/#jupyterlab-support-python-35) to enable support for rendering Plotly charts: +```bash +jupyter labextension install jupyterlab-plotly +``` + ### From scala-js Add the corresponding dependency to your project, like ```scala -libraryDependencies += "org.plotly-scala" %%% "plotly-render" % "0.3.1" +libraryDependencies += "org.plotly-scala" %%% "plotly-render" % "0.8.1" ``` - -Note that there are no version published for scala 2.10 yet, because of the limitation of case classes to 22 members with it. +(Latest version: [![Maven Central](https://img.shields.io/maven-central/v/org.plotly-scala/plotly-render_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/org.plotly-scala/plotly-render_2.13)) From your code, add some imports for plotly, ```scala @@ -62,24 +68,20 @@ import plotly._, element._, layout._, Plotly._ Then define plots like ```scala -val x = 0.0 to 10.0 by 0.1 +val x = (0 to 100).map(_ * 0.1) val y1 = x.map(d => 2.0 * d + util.Random.nextGaussian()) val y2 = x.map(math.exp) val plot = Seq( - Scatter( - x, y1, name = "Approx twice" - ), - Scatter( - x, y2, name = "Exp" - ) + Scatter(x, y1).withName("Approx twice"), + Scatter(x, y2).withName("Exp") ) ``` and plot them with + ```scala -plot.plot( - title = "Curves" -) +val lay = Layout().withTitle("Curves") +plot.plot("plot", lay) // attaches to div element with id 'plot' ``` @@ -87,7 +89,7 @@ plot.plot( Load the corresponding dependency, and some imports, like ```scala -import $ivy.`org.plotly-scala::plotly-render:0.3.1` +import $ivy.`org.plotly-scala::plotly-render:0.8.1` import plotly._, element._, layout._, Plotly._ ``` @@ -131,14 +133,15 @@ plotly-scala supports the features illustrated in the following sections of the - [Subplots](https://plot.ly/javascript/subplots/), - [Multiple Axes](https://plot.ly/javascript/multiple-axes/), - [Histograms](https://plot.ly/javascript/histograms/), -- [Log Plots](https://plot.ly/javascript/log-plot/). +- [Log Plots](https://plot.ly/javascript/log-plot/), +- [Image](https://plotly.com/javascript/reference/image/). Some of these are illustrated in the [demo](https://alexarchambault.github.io/plotly-scala/) page. ## Adding support for extra plotly.js features The following workflow can be followed to add support for extra sections of the plotly.js documentation: -- find the corresponding directory in the [source](https://github.com/plotly/documentation/tree/gh-pages/_posts/plotly_js) of the plotly.js documentation. These directories can also be found in the sources of plotly-scala, under `plotly-documentation/_posts/plotly_js`, if its repository has been cloned with the `--recursive` option, +- find the corresponding directory in the [source](https://github.com/alexarchambault/plotly-documentation/tree/eae136bb920c7542654a5e13cff04a0de175a08d/) of the plotly.js documentation. These directories can also be found in the sources of plotly-scala, under `plotly-documentation/_posts/plotly_js`, if its repository has been cloned with the `--recursive` option, - enabling testing of the corresponding documentation section examples in the `DocumentationTests` class, around [this line](https://github.com/alexarchambault/plotly-scala/blob/master/tests/src/test/scala/plotly/doc/DocumentationTests.scala#L224), - running the tests with `sbt ~test`, - fixing the possible Javascript typos in the plotly-documentation submodule in the plotly-scala sources, so that the enabled JS snippets run fine with Rhino from the tests, then committing these fixes, either to [https://github.com/alexarchambault/plotly-documentation](`alexarchambault/plotly-documentation`) or [https://github.com/plotly/documentation](`plotly/documentation`), @@ -148,6 +151,6 @@ The following workflow can be followed to add support for extra sections of the Battlefield tested since early 2016 at [Teads.tv](http://teads.tv) -Released under the LGPL v3 license, copyright 2016 Alexandre Archambault. +Released under the LGPL v3 license, copyright 2016-2019 Alexandre Archambault and contributors. Parts based on the original plotly.js API, which is copyright 2016 Plotly, Inc. diff --git a/almond/src/main/scala/plotly/Almond.scala b/almond/src/main/scala/plotly/Almond.scala new file mode 100644 index 00000000..41e7bb95 --- /dev/null +++ b/almond/src/main/scala/plotly/Almond.scala @@ -0,0 +1,317 @@ +package plotly + +import java.lang.{Boolean => JBoolean, Double => JDouble, Integer => JInt} + +import almond.interpreter.api.{DisplayData, OutputHandler} + +import scala.util.Random +import plotly.element._ +import plotly.layout._ + +object Almond { + + object Internal { + @volatile var initialized = false + } + + def init(offline: Boolean = false)(implicit publish: OutputHandler): Unit = { + + // offline mode like in plotly-python + + val requireInit = + if (offline) + s"""define('plotly', function(require, exports, module) { + | ${Plotly.plotlyMinJs} + |}); + """.stripMargin + else + s"""require.config({ + | paths: { + | d3: 'https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min', + | plotly: 'https://cdn.plot.ly/plotly-${Plotly.plotlyVersion}.min', + | jquery: 'https://code.jquery.com/jquery-3.3.1.min' + | }, + | + | shim: { + | plotly: { + | deps: ['d3', 'jquery'], + | exports: 'plotly' + | } + | } + |}); + """.stripMargin + + val html = s""" + + """ + + Internal.initialized = true + + publish.html(html) + } + + def plotJs( + data: Seq[Trace], + layout: Layout, + config: Config, + div: String = "" + )(implicit + publish: OutputHandler + ): String = { + + val (div0, divPart) = + if (div.isEmpty) { + val d = randomDiv() + (d, s"""
""") + } else + (div, "") + + val baseJs = Plotly.jsSnippet(div0, data, layout, config) + val json = Plotly.jsonSnippet(data, layout, config) + + val js = + s"""require(['plotly'], function(Plotly) { + | $baseJs + |}); + """.stripMargin + + val data0 = DisplayData( + data = Map( + "text/html" -> + s"""$divPart + | + """.stripMargin, + "application/vnd.plotly.v1+json" -> json + ) + ) + + publish.display(data0) + + div0 + } + + def randomDiv(): String = + almond.display.UpdatableDisplay.generateDiv("plot-") + + def plot( + data: Seq[Trace], + layout: Layout = Layout(), + config: Config = Config(), + div: String = "" + )(implicit + publish: OutputHandler + ): String = { + + if (!Internal.initialized) + Internal.synchronized { + if (!Internal.initialized) { + init() + Internal.initialized = true + } + } + + plotJs(data, layout, config) + } + + implicit class DataOps(val data: Trace) extends AnyVal { + + @deprecated("Create a Layout and / or a Config, and call one of the other plot methods instead", "0.8.0") + def plot( + title: String = null, + legend: Legend = null, + width: JInt = null, + height: JInt = null, + showlegend: JBoolean = null, + xaxis: Axis = null, + yaxis: Axis = null, + xaxis1: Axis = null, + xaxis2: Axis = null, + xaxis3: Axis = null, + xaxis4: Axis = null, + yaxis1: Axis = null, + yaxis2: Axis = null, + yaxis3: Axis = null, + yaxis4: Axis = null, + barmode: BarMode = null, + autosize: JBoolean = null, + margin: Margin = null, + annotations: Seq[Annotation] = null, + plot_bgcolor: Color = null, + paper_bgcolor: Color = null, + font: Font = null, + bargap: JDouble = null, + bargroupgap: JDouble = null, + hovermode: HoverMode = null, + boxmode: BoxMode = null, + editable: JBoolean = null, + responsive: JBoolean = null, + showEditInChartStudio: JBoolean = null, + plotlyServerURL: String = null, + div: String = "" + )(implicit + publish: OutputHandler + ): String = + plot( + Layout( + title, + legend, + width, + height, + showlegend, + xaxis, + yaxis, + xaxis1, + xaxis2, + xaxis3, + xaxis4, + yaxis1, + yaxis2, + yaxis3, + yaxis4, + barmode, + autosize, + margin, + annotations, + plot_bgcolor, + paper_bgcolor, + font, + bargap, + bargroupgap, + hovermode, + boxmode + ), + Config() + .withEditable(Option(editable).map[Boolean](identity)) + .withResponsive(Option(responsive).map[Boolean](identity)) + .withShowEditInChartStudio(Option(showEditInChartStudio).map[Boolean](identity)) + .withPlotlyServerURL(Option(plotlyServerURL)), + div + ) + + def plot( + layout: Layout, + config: Config, + div: String + )(implicit + publish: OutputHandler + ): String = + Almond.plot(Seq(data), layout, config, div = div) + + def plot()(implicit + publish: OutputHandler + ): String = + plot(Layout(), Config(), "") + + def plot( + layout: Layout + )(implicit + publish: OutputHandler + ): String = + plot(layout, Config(), "") + + def plot( + config: Config + )(implicit + publish: OutputHandler + ): String = + plot(Layout(), config, "") + + def plot( + layout: Layout, + config: Config + )(implicit + publish: OutputHandler + ): String = + plot(layout, config, "") + } + + implicit class DataSeqOps(val data: Seq[Trace]) extends AnyVal { + def plot( + title: String = null, + legend: Legend = null, + width: JInt = null, + height: JInt = null, + showlegend: JBoolean = null, + xaxis: Axis = null, + yaxis: Axis = null, + xaxis1: Axis = null, + xaxis2: Axis = null, + xaxis3: Axis = null, + xaxis4: Axis = null, + yaxis1: Axis = null, + yaxis2: Axis = null, + yaxis3: Axis = null, + yaxis4: Axis = null, + barmode: BarMode = null, + autosize: JBoolean = null, + margin: Margin = null, + annotations: Seq[Annotation] = null, + plot_bgcolor: Color = null, + paper_bgcolor: Color = null, + font: Font = null, + bargap: JDouble = null, + bargroupgap: JDouble = null, + hovermode: HoverMode = null, + boxmode: BoxMode = null, + editable: JBoolean = null, + responsive: JBoolean = null, + showEditInChartStudio: JBoolean = null, + plotlyServerURL: String = null, + div: String = "" + )(implicit + publish: OutputHandler + ): String = + plot( + Layout( + title, + legend, + width, + height, + showlegend, + xaxis, + yaxis, + xaxis1, + xaxis2, + xaxis3, + xaxis4, + yaxis1, + yaxis2, + yaxis3, + yaxis4, + barmode, + autosize, + margin, + annotations, + plot_bgcolor, + paper_bgcolor, + font, + bargap, + bargroupgap, + hovermode, + boxmode + ), + Config() + .withEditable(Option(editable).map[Boolean](identity)) + .withResponsive(Option(responsive).map[Boolean](identity)) + .withShowEditInChartStudio(Option(showEditInChartStudio).map[Boolean](identity)) + .withPlotlyServerURL(plotlyServerURL), + div + ) + + def plot( + layout: Layout, + config: Config, + div: String + )(implicit + publish: OutputHandler + ): String = + Almond.plot(data, layout, config, div = div) + } + +} diff --git a/build.sbt b/build.sbt index 503281d3..e5e34c5c 100644 --- a/build.sbt +++ b/build.sbt @@ -1,72 +1,142 @@ +import java.nio.charset.StandardCharsets +import java.nio.file.Files -import Aliases._ import Settings._ +import sbtcrossproject.CrossPlugin.autoImport.crossProject -lazy val core = crossProject +inThisBuild( + List( + organization := "org.plotly-scala", + homepage := Some(url("https://github.com/alexarchambault/plotly-scala")), + licenses := Seq("LGPL 3.0" -> url("http://opensource.org/licenses/LGPL-3.0")), + developers := List( + Developer( + "alexarchambault", + "Alexandre Archambault", + "", + url("https://github.com/alexarchambault") + ) + ) + ) +) + +val previousVersions = Set.empty[String] +lazy val mimaSettings = Def.settings( + mimaPreviousArtifacts := previousVersions.map(organization.value %% moduleName.value % _) +) + +lazy val core = crossProject(JVMPlatform, JSPlatform) + .jsConfigure(_.disablePlugins(MimaPlugin)) .settings( shared, - plotlyPrefix + plotlyPrefix, + libraryDependencies += Deps.dataClass % Provided + ) + .jvmSettings( + mimaSettings ) lazy val coreJvm = core.jvm -lazy val coreJs = core.js +lazy val coreJs = core.js lazy val `joda-time` = project .dependsOn(coreJvm) .settings( shared, + mimaSettings, plotlyPrefix, - libs += Deps.jodaTime + libraryDependencies += Deps.jodaTime ) -lazy val `circe-simple-generic` = crossProject - .settings( - shared, - libs ++= Seq( - Deps.circeCore.value, - Deps.circeParser.value, - Deps.shapeless.value, - Deps.scalacheckShapeless.value % "test" - ), - utest - ) - .jsSettings( - scalaJSStage in Global := FastOptStage - ) - -lazy val circeSimpleGenericJvm = `circe-simple-generic`.jvm -lazy val circeSimpleGenericJs = `circe-simple-generic`.js - -lazy val render = crossProject - .dependsOn(core, `circe-simple-generic`) +lazy val render = crossProject(JVMPlatform, JSPlatform) + .jvmConfigure(_.enablePlugins(ShadingPlugin)) + .jsConfigure(_.disablePlugins(MimaPlugin)) + .dependsOn(core) .settings( shared, plotlyPrefix ) .jvmSettings( - libs += WebDeps.plotlyJs + mimaSettings, + mimaCurrentClassfiles := shadedPackageBin.value, + Mima.renderFilters, + shadedModules += Deps.argonautShapeless.value.module, + shadingRules ++= { + val shadeUnder = "plotly.internals.shaded" + val shadeNamespaces = Seq("argonaut", "macrocompat", "shapeless") + for (ns <- shadeNamespaces) + yield ShadingRule.moveUnder(ns, shadeUnder), + }, + validNamespaces += "plotly", + libraryDependencies ++= Seq( + Deps.argonautShapeless.value, + // depending on that one so that it doesn't get shaded + "org.scala-lang" % "scala-reflect" % scalaVersion.value, + WebDeps.plotlyJs, + Deps.scalaTest % "test" + ), + (Compile / resourceGenerators) += Def.task { + import sys.process._ + + val log = state.value.log + + val dir = (Compile / classDirectory).value / "plotly" + val ver = version.value + + val f = dir / "plotly-scala.properties" + dir.mkdirs() + + val props = Seq( + "plotly-js-version" -> WebDeps.Versions.plotlyJs, + "version" -> ver, + "commit-hash" -> Seq("git", "rev-parse", "HEAD").!!.trim + ) + + val b = props + .map { case (k, v) => + assert(!v.contains("\n"), s"Invalid ${"\\n"} character in property $k") + s"$k=$v" + } + .mkString("\n") + .getBytes(StandardCharsets.UTF_8) + + val currentContentOpt = Some(f.toPath) + .filter(Files.exists(_)) + .map(p => Files.readAllBytes(p)) + + if (currentContentOpt.forall(b0 => !java.util.Arrays.equals(b, b0))) { + val w = new java.io.FileOutputStream(f) + w.write(b) + w.close() + + log.info(s"Wrote $f") + } + + Nil + } ) .jsSettings( - libs ++= Seq( - Deps.circeScalaJs.value, + libraryDependencies ++= Seq( + Deps.argonautShapeless.value, Deps.scalajsDom.value ) ) lazy val renderJvm = render.jvm -lazy val renderJs = render.js +lazy val renderJs = render.js lazy val demo = project - .enablePlugins(ScalaJSPlugin) + .enablePlugins(JSDependenciesPlugin, ScalaJSPlugin) + .disablePlugins(MimaPlugin) .dependsOn(renderJs) .settings( shared, - dontPublish, + (publish / skip) := true, plotlyPrefix, - test in Test := (), - testOnly in Test := (), - libs += Deps.scalatags.value, + (Test / test) := {}, + (Test / testOnly) := {}, + libraryDependencies += Deps.scalatags.value, jsDependencies ++= Seq( WebDeps.plotlyJs .intransitive() @@ -105,44 +175,28 @@ lazy val demo = project ) lazy val tests = project + .disablePlugins(MimaPlugin) .dependsOn(coreJvm, renderJvm) .settings( shared, - dontPublish, + (publish / skip) := true, plotlyPrefix, - libs ++= Seq( - Deps.circeLiteral.value % "test", + fetchTestData, + libraryDependencies ++= Seq( Deps.scalaTest % "test", - Deps.rhino % "test" + Deps.rhino % "test" ) ) -lazy val `jupyter-scala` = project +lazy val almond = project .dependsOn(coreJvm, renderJvm) .settings( shared, + mimaSettings, plotlyPrefix, - libs ++= Seq( - Deps.jupyterScalaApi % "provided" - ) + libraryDependencies += Deps.almondScalaApi % "provided" ) - -lazy val `plotly-scala` = project - .in(file(".")) - .aggregate( - coreJvm, - coreJs, - `joda-time`, - circeSimpleGenericJvm, - circeSimpleGenericJs, - renderJvm, - renderJs, - demo, - tests, - `jupyter-scala` - ) - .settings( - shared, - dontPublish - ) +crossScalaVersions := Nil +(publish / skip) := true +disablePlugins(MimaPlugin) diff --git a/circe-simple-generic/README.md b/circe-simple-generic/README.md deleted file mode 100644 index 03ea17e1..00000000 --- a/circe-simple-generic/README.md +++ /dev/null @@ -1 +0,0 @@ -Simple port of [argonaut-shapeless](https://github.com/alexarchambault/argonaut-shapeless) to circe diff --git a/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/DerivedInstances.scala b/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/DerivedInstances.scala deleted file mode 100644 index 2077bdc1..00000000 --- a/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/DerivedInstances.scala +++ /dev/null @@ -1,21 +0,0 @@ -package io.circe.simplegeneric.derive - -import shapeless.{ LowPriority, Strict } -import io.circe.{ Decoder, Encoder } - -trait DerivedInstances { - - implicit def derivedEncoder[T] - (implicit - ev: LowPriority, - underlying: Strict[MkEncoder[T]] - ): Encoder[T] = - underlying.value.encoder - - implicit def derivedDecoder[T] - (implicit - ev: LowPriority, - underlying: Strict[MkDecoder[T]] - ): Decoder[T] = - underlying.value.decoder -} diff --git a/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/JsonProductCodec.scala b/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/JsonProductCodec.scala deleted file mode 100644 index 5b3ab782..00000000 --- a/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/JsonProductCodec.scala +++ /dev/null @@ -1,62 +0,0 @@ -package io.circe.simplegeneric -package derive - -import io.circe.{ ACursor, Decoder, HCursor, Json } - -abstract class JsonProductCodec { - def encodeEmpty: Json - def encodeField(field: (String, Json), obj: Json, default: => Option[Json]): Json - - def decodeEmpty(cursor: HCursor): Decoder.Result[Unit] - def decodeField[A](name: String, cursor: HCursor, decode: Decoder[A], default: Option[A]): Decoder.Result[(A, ACursor)] -} - -object JsonProductCodec { - val obj: JsonProductCodec = new JsonProductObjCodec - def adapt(f: String => String): JsonProductCodec = new JsonProductObjCodec { - override def toJsonName(name: String) = f(name) - } -} - -abstract class JsonProductCodecFor[P] { - def codec: JsonProductCodec -} - -object JsonProductCodecFor { - def apply[S](codec0: JsonProductCodec): JsonProductCodecFor[S] = - new JsonProductCodecFor[S] { - def codec = codec0 - } - - implicit def default[T]: JsonProductCodecFor[T] = - JsonProductCodecFor(JsonProductCodec.obj) -} - -class JsonProductObjCodec extends JsonProductCodec { - - def toJsonName(name: String): String = name - - val encodeEmpty: Json = Json.obj() - def encodeField(field: (String, Json), obj: Json, default: => Option[Json]): Json = { - val (name, content) = field - if (default.toSeq.contains(content)) - obj - else - obj.mapObject((toJsonName(name) -> content) +: _) - } - - def decodeEmpty(cursor: HCursor): Decoder.Result[Unit] = Right(()) - def decodeField[A](name: String, cursor: HCursor, decode: Decoder[A], default: Option[A]): Decoder.Result[(A, ACursor)] = { - val c = cursor.downField(toJsonName(name)) - def result = c.as(decode).right.map((_, ACursor.ok(cursor))) - - default match { - case None => result - case Some(d) => - if (c.succeeded) - result - else - Right((d, ACursor.ok(cursor))) - } - } -} diff --git a/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/JsonSumCodec.scala b/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/JsonSumCodec.scala deleted file mode 100644 index d20ac78c..00000000 --- a/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/JsonSumCodec.scala +++ /dev/null @@ -1,93 +0,0 @@ -package io.circe.simplegeneric -package derive - -import io.circe._ - -abstract class JsonSumCodec { - def encodeEmpty: Nothing - def encodeField(fieldOrObj: Either[Json, (String, Json)]): Json - - def decodeEmpty(cursor: HCursor): Decoder.Result[Nothing] - def decodeField[A](name: String, cursor: HCursor, decode: Decoder[A]): Decoder.Result[Either[ACursor, A]] -} - -abstract class JsonSumCodecFor[S] { - def codec: JsonSumCodec -} - -object JsonSumCodecFor { - def apply[S](codec0: JsonSumCodec): JsonSumCodecFor[S] = - new JsonSumCodecFor[S] { - def codec = codec0 - } - - implicit def default[T]: JsonSumCodecFor[T] = - JsonSumCodecFor(JsonSumCodec.obj) -} - -object JsonSumCodec { - val obj: JsonSumCodec = new JsonSumObjCodec - val typeField: JsonSumCodec = new JsonSumTypeFieldCodec -} - -class JsonSumObjCodec extends JsonSumCodec { - - def toJsonName(name: String): String = name - - def encodeEmpty: Nothing = - throw new IllegalArgumentException("empty") - def encodeField(fieldOrObj: Either[Json, (String, Json)]): Json = - fieldOrObj match { - case Left(other) => other - case Right((name, content)) => - Json.obj(toJsonName(name) -> content) - } - - def decodeEmpty(cursor: HCursor): Decoder.Result[Nothing] = - Left(DecodingFailure( - s"unrecognized type(s): ${cursor.fields.getOrElse(Nil).mkString(", ")}", - cursor.history - )) - def decodeField[A](name: String, cursor: HCursor, decode: Decoder[A]): Decoder.Result[Either[ACursor, A]] = - cursor.downField(toJsonName(name)).either match { - case Left(_) => - Right(Left(ACursor.ok(cursor))) - case Right(content) => - decode(content).right.map(Right(_)) - } -} - -class JsonSumTypeFieldCodec extends JsonSumCodec { - - def typeField: String = "type" - - def toTypeValue(name: String) = name - - def encodeEmpty: Nothing = - throw new IllegalArgumentException("empty") - def encodeField(fieldOrObj: Either[Json, (String, Json)]): Json = - fieldOrObj match { - case Left(other) => other - case Right((name, content)) => - content.mapObject((typeField -> Json.fromString(toTypeValue(name))) +: _) - } - - def decodeEmpty(cursor: HCursor): Decoder.Result[Nothing] = - Left(DecodingFailure( - cursor.downField(typeField).focus match { - case None => "no type found" - case Some(type0) => s"unrecognized type: $type0" - }, - cursor.history - )) - def decodeField[A](name: String, cursor: HCursor, decode: Decoder[A]): Decoder.Result[Either[ACursor, A]] = { - val c = cursor.downField(typeField) - - c.as[String] match { - case Right(name0) if toTypeValue(name) == name0 => - c.delete.as(decode).right.map(Right(_)) - case _ => - Right(Left(ACursor.ok(cursor))) - } - } -} diff --git a/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/MkDecoder.scala b/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/MkDecoder.scala deleted file mode 100644 index 387e4095..00000000 --- a/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/MkDecoder.scala +++ /dev/null @@ -1,182 +0,0 @@ -package io.circe.simplegeneric -package derive - -import shapeless._ -import shapeless.labelled.{FieldType, field} -import io.circe.Decoder - -abstract class MkDecoder[T] { - def decoder: Decoder[T] -} - -object MkDecoder { - def apply[T](implicit decoder: MkDecoder[T]): MkDecoder[T] = decoder - - implicit def product[P] - (implicit - underlying: ProductDecoder[P], - codecFor: JsonProductCodecFor[P] - ): MkDecoder[P] = - new MkDecoder[P] { - def decoder = underlying(codecFor.codec) - } - - implicit def sum[S] - (implicit - underlying: SumDecoder[S], - codecFor: JsonSumCodecFor[S] - ): MkDecoder[S] = - new MkDecoder[S] { - def decoder = underlying(codecFor.codec) - } -} - -abstract class ProductDecoder[P] { - def apply(productCodec: JsonProductCodec): Decoder[P] -} - -object ProductDecoder { - def apply[P](implicit decoder: ProductDecoder[P]): ProductDecoder[P] = decoder - - def instance[P](f: JsonProductCodec => Decoder[P]): ProductDecoder[P] = - new ProductDecoder[P] { - def apply(productCodec: JsonProductCodec) = - f(productCodec) - } - - // Re-enable by making a dummy HList of defaults made of Option[_] - // implicit def record[R <: HList] - // (implicit - // underlying: HListProductDecoder[R] - // ): ProductDecoder[R] = - // instance { productCodec => - // underlying(productCodec) - // } - - implicit def generic[P, L <: HList, D <: HList] - (implicit - gen: LabelledGeneric.Aux[P, L], - defaults: Default.AsOptions.Aux[P, D], - underlying: Lazy[HListProductDecoder[L, D]] - ): ProductDecoder[P] = - instance { productCodec => - underlying.value(productCodec, defaults()) - .map(gen.from) - } -} - -abstract class HListProductDecoder[L <: HList, D <: HList] { - def apply(productCodec: JsonProductCodec, defaults: D): Decoder[L] -} - -object HListProductDecoder { - def apply[L <: HList, D <: HList](implicit decoder: HListProductDecoder[L, D]): HListProductDecoder[L, D] = - decoder - - def instance[L <: HList, D <: HList](f: (JsonProductCodec, D) => Decoder[L]): HListProductDecoder[L, D] = - new HListProductDecoder[L, D] { - def apply(productCodec: JsonProductCodec, defaults: D) = - f(productCodec, defaults) - } - - implicit val hnil: HListProductDecoder[HNil, HNil] = - instance { (productCodec, defaults) => - Decoder.instance { c => - productCodec - .decodeEmpty(c) - .right - .map(_ => HNil) - } - } - - implicit def hcons[K <: Symbol, H, T <: HList, TD <: HList] - (implicit - key: Witness.Aux[K], - headDecode: Strict[Decoder[H]], - tailDecode: HListProductDecoder[T, TD] - ): HListProductDecoder[FieldType[K, H] :: T, Option[H] :: TD] = - instance { (productCodec, defaults) => - lazy val tailDecode0 = tailDecode(productCodec, defaults.tail) - - Decoder.instance { c => - for { - x <- productCodec.decodeField(key.value.name, c, headDecode.value, defaults.head).right - t <- x._2.as(tailDecode0).right - } yield field[K](x._1) :: t - } - } -} - -abstract class CoproductSumDecoder[C <: Coproduct] { - def apply(sumCodec: JsonSumCodec): Decoder[C] -} - -object CoproductSumDecoder { - def apply[C <: Coproduct](implicit decoder: CoproductSumDecoder[C]): CoproductSumDecoder[C] = - decoder - - def instance[C <: Coproduct](f: JsonSumCodec => Decoder[C]): CoproductSumDecoder[C] = - new CoproductSumDecoder[C] { - def apply(sumCodec: JsonSumCodec) = - f(sumCodec) - } - - implicit val cnil: CoproductSumDecoder[CNil] = - instance { sumCodec => - Decoder.instance { c => - sumCodec - .decodeEmpty(c) - .right - .map(t => t: CNil) - } - } - - implicit def ccons[K <: Symbol, H, T <: Coproduct] - (implicit - key: Witness.Aux[K], - headDecode: Lazy[Decoder[H]], - tailDecode: CoproductSumDecoder[T] - ): CoproductSumDecoder[FieldType[K, H] :+: T] = - instance { sumCodec => - lazy val tailDecode0 = tailDecode(sumCodec) - - Decoder.instance { c => - sumCodec.decodeField(key.value.name, c, headDecode.value).right.flatMap { - case Left(tailCursor) => tailCursor.as(tailDecode0).right.map(Inr(_)) - case Right(h) => Right(Inl(field[K](h))) - } - } - } -} - -abstract class SumDecoder[S] { - def apply(sumCodec: JsonSumCodec): Decoder[S] -} - -object SumDecoder { - def apply[S](implicit decoder: SumDecoder[S]): SumDecoder[S] = decoder - - def instance[S](f: JsonSumCodec => Decoder[S]): SumDecoder[S] = - new SumDecoder[S] { - def apply(sumCodec: JsonSumCodec) = - f(sumCodec) - } - - implicit def union[U <: Coproduct] - (implicit - underlying: CoproductSumDecoder[U] - ): SumDecoder[U] = - instance { sumCodec => - underlying(sumCodec) - } - - implicit def generic[S, C <: Coproduct] - (implicit - gen: LabelledGeneric.Aux[S, C], - underlying: Strict[CoproductSumDecoder[C]] - ): SumDecoder[S] = - instance { sumCodec => - underlying.value(sumCodec) - .map(gen.from) - } -} diff --git a/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/MkEncoder.scala b/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/MkEncoder.scala deleted file mode 100644 index 0ab101a6..00000000 --- a/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/derive/MkEncoder.scala +++ /dev/null @@ -1,181 +0,0 @@ -package io.circe.simplegeneric -package derive - -import io.circe.Encoder -import shapeless._ -import shapeless.labelled.FieldType - -abstract class MkEncoder[T] { - def encoder: Encoder[T] -} - -object MkEncoder { - def apply[T](implicit encoder: MkEncoder[T]): MkEncoder[T] = encoder - - implicit def product[P] - (implicit - underlying: Strict[ProductEncoder[P]], - codecFor: JsonProductCodecFor[P] - ): MkEncoder[P] = - new MkEncoder[P] { - def encoder = underlying.value(codecFor.codec) - } - - implicit def sum[S] - (implicit - underlying: Strict[SumEncoder[S]], - codecFor: JsonSumCodecFor[S] - ): MkEncoder[S] = - new MkEncoder[S] { - def encoder = underlying.value(codecFor.codec) - } -} - -abstract class ProductEncoder[P] { - def apply(productCodec: JsonProductCodec): Encoder[P] -} - -object ProductEncoder { - def apply[P](implicit encoder: ProductEncoder[P]): ProductEncoder[P] = encoder - - def instance[P](f: JsonProductCodec => Encoder[P]): ProductEncoder[P] = - new ProductEncoder[P] { - def apply(productCodec: JsonProductCodec) = - f(productCodec) - } - - // TODO Generate an HList made of Option[...] as to use as default - // implicit def record[R <: HList] - // (implicit - // underlying: HListProductEncoder[R] - // ): ProductEncoder[R] = - // instance { productCodec => - // underlying(productCodec) - // } - - implicit def generic[P, L <: HList, D <: HList] - (implicit - gen: LabelledGeneric.Aux[P, L], - defaults: Default.AsOptions.Aux[P, D], - underlying: Lazy[HListProductEncoder[L, D]] - ): ProductEncoder[P] = - instance { productCodec => - underlying.value(productCodec, defaults()) - .contramap(gen.to) - } -} - -abstract class HListProductEncoder[L <: HList, D <: HList] { - def apply(productCodec: JsonProductCodec, defaults: D): Encoder[L] -} - -object HListProductEncoder { - def apply[L <: HList, D <: HList](implicit encoder: HListProductEncoder[L, D]): HListProductEncoder[L, D] = - encoder - - def instance[L <: HList, D <: HList](f: (JsonProductCodec, D) => Encoder[L]): HListProductEncoder[L, D] = - new HListProductEncoder[L, D] { - def apply(productCodec: JsonProductCodec, defaults: D) = - f(productCodec, defaults) - } - - implicit val hnil: HListProductEncoder[HNil, HNil] = - instance { (productCodec, _) => - Encoder.instance { _ => - productCodec.encodeEmpty - } - } - - implicit def hcons[K <: Symbol, H, T <: HList, TD <: HList] - (implicit - key: Witness.Aux[K], - headEncode: Strict[Encoder[H]], - tailEncode: HListProductEncoder[T, TD] - ): HListProductEncoder[FieldType[K, H] :: T, Option[H] :: TD] = - instance { (productCodec, defaults) => - lazy val defaultOpt = defaults.head.map(headEncode.value(_)) - lazy val tailEncode0 = tailEncode(productCodec, defaults.tail) - - Encoder.instance { l => - productCodec.encodeField( - key.value.name -> headEncode.value(l.head), - tailEncode0(l.tail), - defaultOpt - ) - } - } -} - - -abstract class SumEncoder[S] { - def apply(sumCodec: JsonSumCodec): Encoder[S] -} - -object SumEncoder { - def apply[S](implicit encoder: SumEncoder[S]): SumEncoder[S] = encoder - - def instance[S](f: JsonSumCodec => Encoder[S]): SumEncoder[S] = - new SumEncoder[S] { - def apply(sumCodec: JsonSumCodec) = - f(sumCodec) - } - - implicit def union[U <: Coproduct] - (implicit - underlying: CoproductSumEncoder[U] - ): SumEncoder[U] = - instance { sumCodec => - underlying(sumCodec) - } - - implicit def generic[S, C <: Coproduct] - (implicit - gen: LabelledGeneric.Aux[S, C], - underlying: Strict[CoproductSumEncoder[C]] - ): SumEncoder[S] = - instance { sumCodec => - underlying.value(sumCodec) - .contramap(gen.to) - } -} - -abstract class CoproductSumEncoder[C <: Coproduct] { - def apply(sumCodec: JsonSumCodec): Encoder[C] -} - -object CoproductSumEncoder { - def apply[C <: Coproduct](implicit encoder: CoproductSumEncoder[C]): CoproductSumEncoder[C] = - encoder - - def instance[C <: Coproduct](f: JsonSumCodec => Encoder[C]): CoproductSumEncoder[C] = - new CoproductSumEncoder[C] { - def apply(sumCodec: JsonSumCodec) = - f(sumCodec) - } - - implicit val cnil: CoproductSumEncoder[CNil] = - instance { sumCodec => - Encoder.instance { c => - sumCodec.encodeEmpty - } - } - - implicit def ccons[K <: Symbol, H, T <: Coproduct] - (implicit - key: Witness.Aux[K], - headEncode: Lazy[Encoder[H]], - tailEncode: CoproductSumEncoder[T] - ): CoproductSumEncoder[FieldType[K, H] :+: T] = - instance { sumCodec => - lazy val tailEncode0 = tailEncode(sumCodec) - - Encoder.instance { - case Inl(h) => - sumCodec.encodeField( - Right(key.value.name -> headEncode.value(h)) - ) - case Inr(r) => - tailEncode0(r) - } - } -} diff --git a/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/package.scala b/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/package.scala deleted file mode 100644 index fe53cde4..00000000 --- a/circe-simple-generic/shared/src/main/scala/io/circe/simplegeneric/package.scala +++ /dev/null @@ -1,5 +0,0 @@ -package io.circe - -import io.circe.simplegeneric.derive.DerivedInstances - -package object simplegeneric extends DerivedInstances diff --git a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/DefaultTests.scala b/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/DefaultTests.scala deleted file mode 100644 index 2e53803d..00000000 --- a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/DefaultTests.scala +++ /dev/null @@ -1,45 +0,0 @@ -package io.circe.simplegeneric - -import io.circe.{ Decoder, Encoder, Json } -import utest._ - -object DefaultTests extends TestSuite { - - case class WithDefaults( - i: Int, - s: String = "b" - ) - - val tests = TestSuite { - 'simple - { - val encoder = Encoder[WithDefaults] - val decoder = Decoder[WithDefaults] - - val value0 = WithDefaults(2, "a") - val json0 = encoder(value0) - val expectedJson0 = Json.obj( - "i" -> Json.fromInt(2), - "s" -> Json.fromString("a") - ) - - assert(json0 == expectedJson0) - - val value1 = WithDefaults(2) - val json1 = encoder(value1) - val expectedJson1 = Json.obj( - "i" -> Json.fromInt(2) - ) - - assert(json1 == expectedJson1) - - val result0 = decoder.decodeJson(expectedJson0) - val expectedResult0 = Right(value0) - assert(result0 == expectedResult0) - - val result1 = decoder.decodeJson(expectedJson1) - val expectedResult1 = Right(value1) - assert(result1 == expectedResult1) - } - } - -} diff --git a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/Definitions.scala b/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/Definitions.scala deleted file mode 100644 index d8fa4c46..00000000 --- a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/Definitions.scala +++ /dev/null @@ -1,38 +0,0 @@ -package io.circe.simplegeneric - -import io.circe.Json - -// These case classes / ADTs were originally the same as in scalacheck-shapeless - -/* - * We should have codecs for these - */ -case object Empty -case class EmptyCC() -case class Simple(i: Int, s: String, blah: Boolean) -case class Composed(foo: Simple, other: String) -case class TwiceComposed(foo: Simple, bar: Composed, v: Int) -case class ComposedOptList(fooOpt: Option[Simple], other: String, l: List[TwiceComposed]) - -case class OI(oi: Option[Int]) - -case class SimpleWithJs(i: Int, s: String, v: Json) - -case class NowThree(s: String, i: Int, n: Double) - -sealed trait Base -case class BaseIS(i: Int, s: String) extends Base -case class BaseDB(d: Double, b: Boolean) extends Base -case class BaseLast(c: Simple) extends Base - -/* - * We should *not* have codecs for these - */ -trait NoArbitraryType -case class ShouldHaveNoArb(n: NoArbitraryType, i: Int) -case class ShouldHaveNoArbEither(s: String, i: Int, n: NoArbitraryType) - -sealed trait BaseNoArb -case class BaseNoArbIS(i: Int, s: String) extends BaseNoArb -case class BaseNoArbDB(d: Double, b: Boolean) extends BaseNoArb -case class BaseNoArbN(n: NoArbitraryType) extends BaseNoArb diff --git a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/LowPriorityTests.scala b/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/LowPriorityTests.scala deleted file mode 100644 index 2b97f396..00000000 --- a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/LowPriorityTests.scala +++ /dev/null @@ -1,153 +0,0 @@ -package io.circe.simplegeneric - -import shapeless._ - -import utest._ - -object LowPriorityTests extends TestSuite { - - object Simple { - trait TC[T] { - def prop: Boolean - } - - object TC { - def apply[T](implicit tc: TC[T]): TC[T] = tc - - implicit val intTC: TC[Int] = new TC[Int] { def prop = true } - } - - case class CC(s: String) - - object CC { - implicit val ccTC: TC[CC] = new TC[CC] { def prop = true } - } - - case class CC2(s: String) - - object Extra { - implicit def extraTC[T](implicit notFound: LowPriority): TC[T] = - new TC[T] { def prop = false } - } - } - - object WithIgnoring { - trait TC[T] { - def prop: Option[Boolean] - } - - trait LowPriTC { - implicit def anyTC[T]: TC[T] = new TC[T] { def prop = None } - } - - object TC extends LowPriTC { - def apply[T](implicit tc: TC[T]): TC[T] = tc - - implicit val intTC: TC[Int] = new TC[Int] { def prop = Some(true) } - } - - case class CC(s: String) - - object CC { - implicit val ccTC: TC[CC] = new TC[CC] { def prop = Some(true) } - } - - case class CC2(s: String) - - object Extra { - implicit def extraTC[T](implicit notFound: LowPriority.Ignoring[Witness.`"anyTC"`.T]): TC[T] = - new TC[T] { def prop = Some(false) } - } - } - - - def simpleInt = { - import Simple._ - import Extra._ - TC[Int] - } - def simpleCC = { - import Simple._ - import Extra._ - TC[CC] - } - def simpleString = { - import Simple._ - import Extra._ - TC[String] - } - def simpleCC2 = { - import Simple._ - import Extra._ - TC[CC2] - } - def simpleCC2_0 = { - import Simple._ - import Extra._ - - { - implicit val cc2TC: TC[CC2] = new TC[CC2] { def prop = true } - TC[CC2] - } - } - - def withIgnoringInt = { - import WithIgnoring._ - import Extra._ - TC[Int] - } - def withIgnoringCC = { - import WithIgnoring._ - import Extra._ - TC[CC] - } - def withIgnoringString = { - import WithIgnoring._ - import Extra._ - TC[String] - } - def withIgnoringCC2 = { - import WithIgnoring._ - import Extra._ - TC[CC2] - } - def withIgnoringCC2_0 = { - import WithIgnoring._ - import Extra._ - - { - implicit val cc2TC: TC[CC2] = new TC[CC2] { def prop = Some(true) } - TC[CC2] - } - } - - val tests = TestSuite { - 'simple - { - // `Extra` provides extra implicit instances of `TC[T]` - // We check here that these do not take precedence over the already existing implicit instances. - - assert(simpleInt.prop) - assert(simpleCC.prop) - assert(!simpleString.prop) - assert(!simpleCC2.prop) - - assert(simpleCC2_0.prop) - } - - 'withIgnoring - { - import WithIgnoring._ - import Extra._ - - // `Extra` provides extra implicit instances of `TC[T]` - // We check here that these do not take precedence over the already existing implicit instances. - - assert(withIgnoringInt.prop == Some(true)) - assert(withIgnoringCC.prop == Some(true)) - assert(withIgnoringString.prop == Some(false)) - assert(withIgnoringCC2.prop == Some(false)) - - assert(withIgnoringCC2_0.prop == Some(true)) - } - } - -} diff --git a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/PriorityTests.scala b/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/PriorityTests.scala deleted file mode 100644 index 0ce5ee1f..00000000 --- a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/PriorityTests.scala +++ /dev/null @@ -1,25 +0,0 @@ -package io.circe.simplegeneric - -import io.circe.Decoder -import utest._ - -object PriorityTests extends TestSuite { - import PriorityTestsDefn._ - - val tests = TestSuite { - 'dontOverride - { - Decoder[CC] match { - case _: Flag => - case _ => throw new Exception(s"Default Decoder was overridden") - } - } - - 'doOverride - { - Decoder[CC2] match { - case _: Flag => throw new Exception(s"Can't happen") - case _ => - } - } - } - -} diff --git a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/PriorityTestsDefn.scala b/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/PriorityTestsDefn.scala deleted file mode 100644 index 791446a7..00000000 --- a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/PriorityTestsDefn.scala +++ /dev/null @@ -1,20 +0,0 @@ -package io.circe.simplegeneric - -import io.circe.{Decoder, HCursor} - -object PriorityTestsDefn { - trait Flag - - case class CC(s: String, i: Int) - - object CC { - implicit val decode: Decoder[CC] = new Decoder[CC] with Flag { - def apply(c: HCursor) = - Decoder[(String, Int)].apply(c).right.map { case (s, i) => - CC(s, i) - } - } - } - - case class CC2(i: Int, s: String) -} diff --git a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/ProductEncodeTests.scala b/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/ProductEncodeTests.scala deleted file mode 100644 index 4060a7c0..00000000 --- a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/ProductEncodeTests.scala +++ /dev/null @@ -1,237 +0,0 @@ -package io.circe.simplegeneric - -import utest._ -import org.scalacheck.{ Arbitrary, Prop } -import org.scalacheck.Shapeless._ -import shapeless._ -import io.circe.simplegeneric.derive.{ HListProductEncoder, JsonProductCodecFor, MkEncoder, ProductEncoder } -import io.circe.{ Encoder, Json, KeyEncoder } -import io.circe.syntax._ -import Util._ - - -object ProductEncodeTests extends TestSuite { - - case class WrappedMap(m: Map[String, Json]) - - lazy val expectedEmptyEncoder = - MkEncoder.product( - ProductEncoder.generic( - LabelledGeneric[Empty.type], - Default.AsOptions[Empty.type], - HListProductEncoder.hnil - ), - JsonProductCodecFor.default - ).encoder - - lazy val expectedEmptyCCEncoder = - MkEncoder.product( - ProductEncoder.generic( - LabelledGeneric[EmptyCC], - Default.AsOptions[EmptyCC], - HListProductEncoder.hnil - ), - JsonProductCodecFor.default - ).encoder - - lazy val expectedSimpleEncoder = - MkEncoder.product( - ProductEncoder.generic( - LabelledGeneric[Simple], - Default.AsOptions[Simple], - HListProductEncoder.hcons( - Witness('i), - Encoder.encodeInt, - HListProductEncoder.hcons( - Witness('s), - Encoder.encodeString, - HListProductEncoder.hcons( - Witness('blah), - Encoder.encodeBoolean, - HListProductEncoder.hnil - ) - ) - ) - ), - JsonProductCodecFor.default - ).encoder - - lazy val expectedComposedEncoder = - MkEncoder.product( - ProductEncoder.generic( - LabelledGeneric[Composed], - Default.AsOptions[Composed], - HListProductEncoder.hcons( - Witness('foo), - expectedSimpleEncoder, - HListProductEncoder.hcons( - Witness('other), - Encoder.encodeString, - HListProductEncoder.hnil - ) - ) - ), - JsonProductCodecFor.default - ).encoder - - lazy val expectedSimpleWithJsEncoder = - MkEncoder.product( - ProductEncoder.generic( - LabelledGeneric[SimpleWithJs], - Default.AsOptions[SimpleWithJs], - HListProductEncoder.hcons( - Witness('i), - Encoder.encodeInt, - HListProductEncoder.hcons( - Witness('s), - Encoder.encodeString, - HListProductEncoder.hcons( - Witness('v), - Encoder.encodeJson, - HListProductEncoder.hnil - ) - ) - ) - ), - JsonProductCodecFor.default - ).encoder - - lazy val expectedWrappedMapEncoder = - MkEncoder.product( - ProductEncoder.generic( - LabelledGeneric[WrappedMap], - Default.AsOptions[WrappedMap], - HListProductEncoder.hcons( - Witness('m), - Encoder.encodeMapLike[Map, String, Json](KeyEncoder.encodeKeyString, Encoder.encodeJson), - HListProductEncoder.hnil - ) - ), - JsonProductCodecFor.default - ).encoder - - lazy val expectedOIEncoder = - MkEncoder.product( - ProductEncoder.generic( - LabelledGeneric[OI], - Default.AsOptions[OI], - HListProductEncoder.hcons( - Witness('oi), - Encoder.encodeOption[Int](Encoder.encodeInt), - HListProductEncoder.hnil - ) - ), - JsonProductCodecFor.default - ).encoder - - - def compareEncoders[T: Arbitrary](first: Encoder[T], second: Encoder[T]): Unit = - Prop.forAll{ - t: T => - first(t) == second(t) - }.validate - - def jsonIs[T: Encoder](t: T, json: Json): Unit = { - assert(t.asJson == json) - } - - - val tests = TestSuite { - - 'codec { - 'empty - { - compareEncoders(Encoder[Empty.type], expectedEmptyEncoder) - } - - 'emptyCC - { - compareEncoders(Encoder[EmptyCC], expectedEmptyCCEncoder) - } - - 'simple - { - compareEncoders(Encoder[Simple], expectedSimpleEncoder) - } - - 'composed - { - compareEncoders(Encoder[Composed], expectedComposedEncoder) - } - - // Disabled, Arbitrary Json generation seems to take forever - // 'simpleWithJs - { - // compareEncoders(Encoder[SimpleWithJs], expectedSimpleWithJsEncoder) - // } - - // Looks like not enough WrappedMap can be generated - // 'wrappedMap - { - // val arb = Gen.resize(1000, Arbitrary.arbitrary[WrappedMap]) - // compareEncoders(Encoder[WrappedMap], expectedWrappedMapEncoder)(Arbitrary(arb)) - // } - - 'withOption - { - compareEncoders(Encoder[OI], expectedOIEncoder) - } - } - - 'output { - 'empty - { - jsonIs(Empty, Json.obj()) - } - - 'emptyCC - { - jsonIs(EmptyCC(), Json.obj()) - } - - 'simple - { - jsonIs( - Simple(41, "aa", blah = false), - Json.obj( - "i" -> Json.fromInt(41), - "s" -> Json.fromString("aa"), - "blah" -> Json.fromBoolean(false) - ) - ) - } - - 'composed - { - jsonIs( - Composed(Simple(41, "aa", blah = false), "bbb"), - Json.obj( - "foo" -> Json.obj( - "i" -> Json.fromInt(41), - "s" -> Json.fromString("aa"), - "blah" -> Json.fromBoolean(false) - ), - "other" -> Json.fromString("bbb") - ) - ) - } - - 'simpleWithJs - { - jsonIs( - SimpleWithJs(41, "aa", Json.arr(Json.fromInt(10), Json.obj("a" -> Json.fromBoolean(true)))), - Json.obj( - "i" -> Json.fromInt(41), - "s" -> Json.fromString("aa"), - "v" -> Json.arr(Json.fromInt(10), Json.obj("a" -> Json.fromBoolean(true))) - ) - ) - } - - 'wrappedMap - { - jsonIs( - WrappedMap(Map( - "aa" -> Json.arr(Json.fromInt(10), Json.obj("a" -> Json.fromBoolean(true))), - "bb" -> Json.obj("c" -> Json.fromBoolean(false)) - )), - Json.obj( - "m" -> Json.obj( - "aa" -> Json.arr(Json.fromInt(10), Json.obj("a" -> Json.fromBoolean(true))), - "bb" -> Json.obj("c" -> Json.fromBoolean(false)) - ) - ) - ) - } - } - - } - -} diff --git a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/ShapelessTests.scala b/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/ShapelessTests.scala deleted file mode 100644 index 24c540a1..00000000 --- a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/ShapelessTests.scala +++ /dev/null @@ -1,87 +0,0 @@ -package io.circe.simplegeneric - -import org.scalacheck.{ Arbitrary, Prop } -import shapeless.test.illTyped -import utest._ -import Util._ -import io.circe.{ Decoder, Encoder, Json, parser => Parse } -import io.circe.syntax._ - - -object ShapelessTests extends TestSuite { - private def toFromJson[T: Encoder : Decoder](t: T): Decoder.Result[T] = t.asJson.as[T] - - private def sameAfterBeforeSerialization[T: Arbitrary : Encoder : Decoder]: Unit = - Prop.forAll { - t: T => - toFromJson(t) == Right(t) - }.validate - - import org.scalacheck.Shapeless._ - - val tests = TestSuite { - 'serializeDeserialize { - 'empty - { - sameAfterBeforeSerialization[Empty.type] - } - - 'emptyCC - { - sameAfterBeforeSerialization[EmptyCC] - } - - 'simple - { - sameAfterBeforeSerialization[Simple] - } - - 'composed - { - sameAfterBeforeSerialization[Composed] - } - - 'twiceComposed - { - sameAfterBeforeSerialization[TwiceComposed] - } - - 'composedOptList - { - sameAfterBeforeSerialization[ComposedOptList] - } - - 'nowThree - { - sameAfterBeforeSerialization[NowThree] - } - - 'oi - { - sameAfterBeforeSerialization[OI] - } - - 'oiLoose - { - val json = Parse.parse("{}").right.toOption.get - // assert macro crashes if result is substituted by its value below - val result = json.as[OI] - assert(result == Right(OI(None))) - } - - 'base - { - sameAfterBeforeSerialization[Base] - } - - 'simpleWithJsDummy - { - Encoder[Json] - Decoder[Json] - Encoder[SimpleWithJs] - Decoder[SimpleWithJs] - // Arbitrary[SimpleWithJs] doesn't seem fine - // sameAfterBeforeSerialization[SimpleWithJs] - } - } - } - - illTyped(" Encoder[NoArbitraryType] ") - illTyped(" Decoder[NoArbitraryType] ") - illTyped(" Encoder[ShouldHaveNoArb] ") - illTyped(" Decoder[ShouldHaveNoArb] ") - illTyped(" Encoder[ShouldHaveNoArbEither] ") - illTyped(" Decoder[ShouldHaveNoArbEither] ") - illTyped(" Encoder[BaseNoArb] ") - illTyped(" Decoder[BaseNoArb] ") - -} diff --git a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/SumEncodeTests.scala b/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/SumEncodeTests.scala deleted file mode 100644 index ec080953..00000000 --- a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/SumEncodeTests.scala +++ /dev/null @@ -1,148 +0,0 @@ -package io.circe.simplegeneric - -import utest._ -import org.scalacheck.Shapeless._ -import shapeless._ -import derive._ -import io.circe.{Encoder, Json} - - -object SumEncodeTests extends TestSuite { - import ProductEncodeTests.{ compareEncoders, jsonIs } - - lazy val expectedBaseISEncoder = - MkEncoder.product( - ProductEncoder.generic( - LabelledGeneric[BaseIS], - Default.AsOptions[BaseIS], - Lazy( - HListProductEncoder.hcons( - Witness('i), - Strict(Encoder.encodeInt), - HListProductEncoder.hcons( - Witness('s), - Strict(Encoder.encodeString), - HListProductEncoder.hnil - ) - ) - ) - ), - JsonProductCodecFor.default - ).encoder - - lazy val expectedBaseDBEncoder = - MkEncoder.product( - ProductEncoder.generic( - LabelledGeneric[BaseDB], - Default.AsOptions[BaseDB], - Lazy( - HListProductEncoder.hcons( - Witness('d), - Strict(Encoder.encodeDouble), - HListProductEncoder.hcons( - Witness('b), - Strict(Encoder.encodeBoolean), - HListProductEncoder.hnil - ) - ) - ) - ), - JsonProductCodecFor.default - ).encoder - - lazy val expectedBaseLastEncoder = - MkEncoder.product( - ProductEncoder.generic( - LabelledGeneric[BaseLast], - Default.AsOptions[BaseLast], - Lazy( - HListProductEncoder.hcons( - Witness('c), - Strict(ProductEncodeTests.expectedSimpleEncoder), - HListProductEncoder.hnil - ) - ) - ), - JsonProductCodecFor.default - ).encoder - - lazy val expectedBaseEncoder = expectedBaseEncoderFor(JsonSumCodecFor.default) - lazy val expectedBaseEncoderTypeField = expectedBaseEncoderFor(JsonSumCodecFor(JsonSumCodec.typeField)) - def expectedBaseEncoderFor(codecFor: JsonSumCodecFor[Base]) = - MkEncoder.sum( - SumEncoder.generic( - LabelledGeneric[Base], - CoproductSumEncoder.ccons( - Witness('BaseDB), - expectedBaseDBEncoder, - CoproductSumEncoder.ccons( - Witness('BaseIS), - expectedBaseISEncoder, - CoproductSumEncoder.ccons( - Witness('BaseLast), - expectedBaseLastEncoder, - CoproductSumEncoder.cnil - ) - ) - ) - ), - codecFor - ).encoder - - val derivedBaseEncoderTypeField = { - implicit val codecFor = JsonSumCodecFor[Base](JsonSumCodec.typeField) - Encoder[Base] - } - - val tests = TestSuite { - - 'codec { - 'base - { - compareEncoders(Encoder[Base], expectedBaseEncoder) - } - - 'baseTypeField - { - compareEncoders(derivedBaseEncoderTypeField, expectedBaseEncoderTypeField) - } - } - - 'output { - 'base - { - jsonIs( - BaseLast(Simple(41, "aa", blah = false)): Base, - Json.obj( - "BaseLast" -> Json.obj( - "c" -> Json.obj( - "i" -> Json.fromInt(41), - "s" -> Json.fromString("aa"), - "blah" -> Json.fromBoolean(false) - ) - ) - ) - ) - - jsonIs( - BaseIS(43, "aa"): Base, - Json.obj( - "BaseIS" -> Json.obj( - "i" -> Json.fromInt(43), - "s" -> Json.fromString("aa") - ) - ) - ) - - jsonIs( - BaseDB(3.2, false): Base, - Json.obj( - "BaseDB" -> Json.obj( - "d" -> Json.fromDoubleOrString(3.2), - "b" -> Json.fromBoolean(false) - ) - ) - ) - } - } - - } - -} diff --git a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/Util.scala b/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/Util.scala deleted file mode 100644 index 16fa8c61..00000000 --- a/circe-simple-generic/shared/src/test/scala/io/circe/simplegeneric/Util.scala +++ /dev/null @@ -1,15 +0,0 @@ -package io.circe.simplegeneric - -import org.scalacheck.{ Prop, Test } -import utest._ - -object Util { - - implicit class PropExtensions(val prop: Prop) extends AnyVal { - def validate: Unit = { - val result = Test.check(Test.Parameters.default, prop) - assert(result.passed) - } - } - -} diff --git a/core/js/src/main/scala/plotly/element/PlotlyJavaTimeConversions.scala b/core/js/src/main/scala/plotly/element/PlotlyJavaTimeConversions.scala new file mode 100644 index 00000000..1115fc91 --- /dev/null +++ b/core/js/src/main/scala/plotly/element/PlotlyJavaTimeConversions.scala @@ -0,0 +1,5 @@ +package plotly.element + +trait PlotlyJavaTimeConversions { + // Empty since the java.time classes are not available in ScalaJS +} diff --git a/core/jvm/src/main/scala/plotly/element/PlotlyJavaTimeConversions.scala b/core/jvm/src/main/scala/plotly/element/PlotlyJavaTimeConversions.scala new file mode 100644 index 00000000..bf0d166a --- /dev/null +++ b/core/jvm/src/main/scala/plotly/element/PlotlyJavaTimeConversions.scala @@ -0,0 +1,41 @@ +package plotly.element + +import java.time._ +import plotly.element.{LocalDateTime => PlotlyLocalDateTime} + +import scala.language.implicitConversions + +trait PlotlyJavaTimeConversions { + + implicit def fromJavaLocalDateTime(javaLocalDateTime: java.time.LocalDateTime): PlotlyLocalDateTime = + PlotlyLocalDateTime( + javaLocalDateTime.getYear, + javaLocalDateTime.getMonthValue, + javaLocalDateTime.getDayOfMonth, + javaLocalDateTime.getHour, + javaLocalDateTime.getMinute, + javaLocalDateTime.getSecond + ) + + implicit def fromJavaInstant(javaInstant: Instant): PlotlyLocalDateTime = + fromJavaLocalDateTime(javaInstant.atOffset(ZoneOffset.UTC).toLocalDateTime) + + implicit def fromJavaLocalDate(javaLocalDate: LocalDate): PlotlyLocalDateTime = + fromJavaLocalDateTime(javaLocalDate.atStartOfDay) + + /** Implicit conversions in this object convert to `plotly.element.LocalDateTime` by simply dropping timezone/offset + * information. This can lead to unexpected behaviour, particularly for datasets with varying offsets and timezones. + * It will generally be safer to convert your data to `java.time.LocalDateTime` in the appropriate timezone/offset, + * and then use the `PlotlyJavaTimeConversions.fromJavaLocalDateTime` implicit conversion. + */ + object UnsafeImplicitConversions { + + implicit def fromJavaOffsetDateTime(javaOffsetDateTime: OffsetDateTime): PlotlyLocalDateTime = + fromJavaLocalDateTime(javaOffsetDateTime.toLocalDateTime) + + implicit def fromJavaZonedDateTime(javaZonedDateTime: ZonedDateTime): PlotlyLocalDateTime = + fromJavaLocalDateTime(javaZonedDateTime.toLocalDateTime) + + } + +} diff --git a/core/shared/src/main/scala-2.12/plotly/MutableSequenceImplicitConversions.scala b/core/shared/src/main/scala-2.12/plotly/MutableSequenceImplicitConversions.scala new file mode 100644 index 00000000..0e4650ce --- /dev/null +++ b/core/shared/src/main/scala-2.12/plotly/MutableSequenceImplicitConversions.scala @@ -0,0 +1,5 @@ +package plotly + +trait MutableSequenceImplicitConversions { + // Unneccessary in Scala 2.12, since the `Seq` alias refers to the supertype for mutable and immutable sequences +} diff --git a/core/shared/src/main/scala-2.13/plotly/MutableSequenceImplicitConversions.scala b/core/shared/src/main/scala-2.13/plotly/MutableSequenceImplicitConversions.scala new file mode 100644 index 00000000..c8edf055 --- /dev/null +++ b/core/shared/src/main/scala-2.13/plotly/MutableSequenceImplicitConversions.scala @@ -0,0 +1,28 @@ +package plotly + +import plotly.Sequence.{DateTimes, Doubles, NestedDoubles, NestedInts, Strings} +import plotly.element.LocalDateTime + +import scala.collection.{Seq => BaseScalaSeq} +import scala.language.implicitConversions + +trait MutableSequenceImplicitConversions { + + implicit def fromMutableDoubleSeq(s: BaseScalaSeq[Double]): Sequence = + Doubles(s.toSeq) + implicit def fromMutableFloatSeq(s: BaseScalaSeq[Float]): Sequence = + Doubles(s.map(_.toDouble).toSeq) + implicit def fromMutableIntSeq(s: BaseScalaSeq[Int]): Sequence = + Doubles(s.map(_.toDouble).toSeq) + implicit def fromMutableLongSeq(s: BaseScalaSeq[Long]): Sequence = + Doubles(s.map(_.toDouble).toSeq) + implicit def fromMutableNestedDoubleSeq(s: BaseScalaSeq[BaseScalaSeq[Double]]): Sequence = + NestedDoubles(s.map(_.toSeq).toSeq) + implicit def fromMutableNestedIntSeq(s: BaseScalaSeq[BaseScalaSeq[Int]]): Sequence = + NestedInts(s.map(_.toSeq).toSeq) + implicit def fromMutableStringSeq(s: BaseScalaSeq[String]): Sequence = + Strings(s.toSeq) + implicit def fromMutableDateTimes(seq: BaseScalaSeq[LocalDateTime]): Sequence = + DateTimes(seq.toSeq) + +} diff --git a/core/shared/src/main/scala/plotly/Config.scala b/core/shared/src/main/scala/plotly/Config.scala new file mode 100755 index 00000000..a886516b --- /dev/null +++ b/core/shared/src/main/scala/plotly/Config.scala @@ -0,0 +1,27 @@ +package plotly + +import java.lang.{Boolean => JBoolean} +import dataclass.data + +@data(optionSetters = true) class Config( + editable: Option[Boolean] = None, + responsive: Option[Boolean] = None, + showEditInChartStudio: Option[Boolean] = None, + plotlyServerURL: Option[String] = None +) + +object Config { + @deprecated("Use Config() and chain-call .with* methods on it instead", "0.8.0") + def apply( + editable: JBoolean = null, + responsive: JBoolean = null, + showEditInChartStudio: JBoolean = null, + plotlyServerURL: String = null + ): Config = + new Config( + Option(editable), + Option(responsive), + Option(showEditInChartStudio), + Option(plotlyServerURL) + ) +} diff --git a/core/shared/src/main/scala/plotly/Range.scala b/core/shared/src/main/scala/plotly/Range.scala new file mode 100644 index 00000000..c4221cc1 --- /dev/null +++ b/core/shared/src/main/scala/plotly/Range.scala @@ -0,0 +1,17 @@ +package plotly + +import plotly.element.LocalDateTime + +import scala.language.implicitConversions + +sealed abstract class Range extends Product with Serializable + +object Range { + final case class Doubles(range: (Double, Double)) extends Range + final case class DateTimes(range: (LocalDateTime, LocalDateTime)) extends Range + + implicit def fromDoubleTuple(t: (Double, Double)): Range = + Doubles(t) + implicit def fromDateTimes(t: (LocalDateTime, LocalDateTime)): Range = + DateTimes(t) +} diff --git a/core/shared/src/main/scala/plotly/Sequence.scala b/core/shared/src/main/scala/plotly/Sequence.scala old mode 100644 new mode 100755 index c3d0ff90..f40077fa --- a/core/shared/src/main/scala/plotly/Sequence.scala +++ b/core/shared/src/main/scala/plotly/Sequence.scala @@ -2,12 +2,16 @@ package plotly import plotly.element.LocalDateTime +import scala.language.implicitConversions + sealed abstract class Sequence extends Product with Serializable -object Sequence { - final case class Doubles(seq: Seq[Double]) extends Sequence - final case class Strings(seq: Seq[String]) extends Sequence - final case class DateTimes(seq: Seq[LocalDateTime]) extends Sequence +object Sequence extends MutableSequenceImplicitConversions { + final case class Doubles(seq: Seq[Double]) extends Sequence + final case class NestedDoubles(seq: Seq[Seq[Double]]) extends Sequence + final case class NestedInts(seq: Seq[Seq[Int]]) extends Sequence + final case class Strings(seq: Seq[String]) extends Sequence + final case class DateTimes(seq: Seq[LocalDateTime]) extends Sequence implicit def fromDoubleSeq(s: Seq[Double]): Sequence = Doubles(s) @@ -17,6 +21,10 @@ object Sequence { Doubles(s.map(_.toDouble)) implicit def fromLongSeq(s: Seq[Long]): Sequence = Doubles(s.map(_.toDouble)) + implicit def fromNestedDoubleSeq(s: Seq[Seq[Double]]): Sequence = + NestedDoubles(s) + implicit def fromNestedIntSeq(s: Seq[Seq[Int]]): Sequence = + NestedInts(s) implicit def fromStringSeq(s: Seq[String]): Sequence = Strings(s) implicit def fromDateTimes(seq: Seq[LocalDateTime]): Sequence = diff --git a/core/shared/src/main/scala/plotly/Trace.scala b/core/shared/src/main/scala/plotly/Trace.scala old mode 100644 new mode 100755 index 402856de..a8a8d03c --- a/core/shared/src/main/scala/plotly/Trace.scala +++ b/core/shared/src/main/scala/plotly/Trace.scala @@ -2,54 +2,74 @@ package plotly import scala.language.implicitConversions -import java.lang.{ Boolean => JBoolean, Double => JDouble } +import java.lang.{Boolean => JBoolean, Double => JDouble} +import dataclass._ import plotly.element._ sealed abstract class Trace extends Product with Serializable -final case class Scatter( - x: Option[Sequence], - y: Option[Sequence], - text: Option[Seq[String]], - mode: Option[ScatterMode], - marker: Option[Marker], - line: Option[Line], - textposition: Option[TextPosition], - textfont: Option[TextFont], - name: Option[String], - connectgaps: Option[Boolean], - xaxis: Option[AxisReference], - yaxis: Option[AxisReference], - fill: Option[Fill], - error_x: Option[Error], - error_y: Option[Error], - showlegend: Option[Boolean] +@data(optionSetters = true) class Scatter( + x: Option[Sequence] = None, + y: Option[Sequence] = None, + text: Option[OneOrSeq[String]] = None, + mode: Option[ScatterMode] = None, + marker: Option[Marker] = None, + line: Option[Line] = None, + textposition: Option[TextPosition] = None, + textfont: Option[TextFont] = None, + name: Option[String] = None, + connectgaps: Option[Boolean] = None, + xaxis: Option[AxisReference] = None, + yaxis: Option[AxisReference] = None, + fill: Option[Fill] = None, + error_x: Option[Error] = None, + error_y: Option[Error] = None, + showlegend: Option[Boolean] = None, + fillcolor: Option[OneOrSeq[Color]] = None, + hoverinfo: Option[HoverInfo] = None, + hoveron: Option[HoverOn] = None, + stackgroup: Option[String] = None, + groupnorm: Option[GroupNorm] = None, + @since("0.8.2") + hovertemplate: Option[OneOrSeq[String]] = None ) extends Trace object Scatter { + def apply(x: Sequence, y: Sequence): Scatter = + Scatter().withX(x).withY(y) + + def apply(y: Sequence): Scatter = + Scatter().withY(y) + + @deprecated("Use Scatter() and chain-call .with* methods on it instead", "0.8.0") def apply( - values: Sequence = null, - secondValues: Sequence = null, - text: Seq[String] = null, - mode: ScatterMode = null, - marker: Marker = null, - line: Line = null, - textposition: TextPosition = null, - textfont: TextFont = null, - name: String = null, - connectgaps: JBoolean = null, - xaxis: AxisReference = null, - yaxis: AxisReference = null, - fill: Fill = null, - error_x: Error = null, - error_y: Error = null, - showlegend: JBoolean = null + values: Sequence = null, + secondValues: Sequence = null, + text: OneOrSeq[String] = null, + mode: ScatterMode = null, + marker: Marker = null, + line: Line = null, + textposition: TextPosition = null, + textfont: TextFont = null, + name: String = null, + connectgaps: JBoolean = null, + xaxis: AxisReference = null, + yaxis: AxisReference = null, + fill: Fill = null, + error_x: Error = null, + error_y: Error = null, + showlegend: JBoolean = null, + fillcolor: OneOrSeq[Color] = null, + hoverinfo: HoverInfo = null, + hoveron: HoverOn = null, + stackgroup: String = null, + groupnorm: GroupNorm = null ): Scatter = { val (xOpt, yOpt) = Option(secondValues) match { case Some(y) => (Option(values), Some(y)) - case None => (None, Option(values)) + case None => (None, Option(values)) } Scatter( @@ -62,91 +82,142 @@ object Scatter { Option(textposition), Option(textfont), Option(name), - Option(connectgaps) .map(x => x: Boolean), + Option(connectgaps).map(x => x: Boolean), Option(xaxis), Option(yaxis), Option(fill), Option(error_x), Option(error_y), - Option(showlegend) .map(b => b: Boolean) + Option(showlegend).map(b => b: Boolean), + Option(fillcolor), + Option(hoverinfo), + Option(hoveron), + Option(stackgroup), + Option(groupnorm) ) } } -case class Box( - y: Option[Sequence], - x: Option[Sequence], - boxpoints: Option[BoxPoints], - jitter: Option[Double], - pointpos: Option[Double], - name: Option[String], - marker: Option[Marker], - orientation: Option[Orientation], - whiskerwidth: Option[Double], - boxmean: Option[BoxMean], - fillcolor: Option[OneOrSeq[Color]], - line: Option[Line], - showlegend: Option[Boolean] +@data(optionSetters = true) class Box( + y: Option[Sequence] = None, + x: Option[Sequence] = None, + boxpoints: Option[BoxPoints] = None, + jitter: Option[Double] = None, + pointpos: Option[Double] = None, + name: Option[String] = None, + marker: Option[Marker] = None, + orientation: Option[Orientation] = None, + whiskerwidth: Option[Double] = None, + boxmean: Option[BoxMean] = None, + fillcolor: Option[OneOrSeq[Color]] = None, + line: Option[Line] = None, + showlegend: Option[Boolean] = None, + @since("0.8.2") + hovertemplate: Option[OneOrSeq[String]] = None ) extends Trace object Box { + def apply(y: Sequence): Box = + Box().withY(y) + + def apply(y: Sequence, x: Sequence): Box = + Box().withY(y).withX(x) + + @deprecated("Use Box() and chain-call .with* methods on it instead", "0.8.0") def apply( - y: Sequence = null, - x: Sequence = null, - boxpoints: BoxPoints = null, - jitter: JDouble = null, - pointpos: JDouble = null, - name: String = null, - marker: Marker = null, - orientation: Orientation = null, - whiskerwidth: JDouble = null, - boxmean: BoxMean = null, - fillcolor: OneOrSeq[Color] = null, - line: Line = null, - showlegend: JBoolean = null + y: Sequence = null, + x: Sequence = null, + boxpoints: BoxPoints = null, + jitter: JDouble = null, + pointpos: JDouble = null, + name: String = null, + marker: Marker = null, + orientation: Orientation = null, + whiskerwidth: JDouble = null, + boxmean: BoxMean = null, + fillcolor: OneOrSeq[Color] = null, + line: Line = null, + showlegend: JBoolean = null ): Box = Box( Option(y), Option(x), Option(boxpoints), - Option(jitter) .map(d => d: Double), - Option(pointpos) .map(d => d: Double), + Option(jitter).map(d => d: Double), + Option(pointpos).map(d => d: Double), Option(name), Option(marker), Option(orientation), - Option(whiskerwidth) .map(d => d: Double), + Option(whiskerwidth).map(d => d: Double), Option(boxmean), Option(fillcolor), Option(line), - Option(showlegend) .map(b => b: Boolean) + Option(showlegend).map(b => b: Boolean) ) } -final case class Bar( - x: Sequence, - y: Sequence, - name: Option[String], - text: Option[Seq[String]], - marker: Option[Marker], - orientation: Option[Orientation], - xaxis: Option[AxisReference], - yaxis: Option[AxisReference], - error_y: Option[Error], - showlegend: Option[Boolean] +@data(optionSetters = true) class Image( + z: Seq[Seq[Seq[Double]]], + x0: Option[Element] = None, + y0: Option[Element] = None, + name: Option[String] = None, + text: Option[Seq[String]] = None, + opacity: Option[Double] = None, + ids: Option[Seq[String]] = None, + dx: Option[Double] = None, + dy: Option[Double] = None, + source: Option[String] = None, + hoverinfo: Option[HoverInfo] = None, + hovertemplate: Option[Seq[String]] = None, + meta: Option[String] = None, + customdata: Option[Seq[String]] = None, + xaxis: Option[AxisReference] = None, + yaxis: Option[AxisReference] = None, + colormodel: Option[ColorModel] = None, + zmax: Option[Seq[Double]] = None, + zmin: Option[Seq[Double]] = None, + hoverlabel: Option[HoverLabel] = None +) extends Trace + +@data(optionSetters = true) class Bar( + x: Sequence, + y: Sequence, + @since + name: Option[String] = None, + text: Option[Seq[String]] = None, + marker: Option[Marker] = None, + orientation: Option[Orientation] = None, + xaxis: Option[AxisReference] = None, + yaxis: Option[AxisReference] = None, + error_y: Option[Error] = None, + showlegend: Option[Boolean] = None, + hoverinfo: Option[HoverInfo] = None, + textposition: Option[BarTextPosition] = None, + opacity: Option[Double] = None, + width: Option[OneOrSeq[Double]] = None, + base: Option[OneOrSeq[Double]] = None, + @since("0.8.2") + hovertemplate: Option[OneOrSeq[String]] = None ) extends Trace object Bar { + @deprecated("Use Bar() and chain-call .with* methods on it instead", "0.8.0") def apply( - x: Sequence, - y: Sequence, - name: String = null, - text: Seq[String] = null, - marker: Marker = null, - orientation: Orientation = null, - xaxis: AxisReference = null, - yaxis: AxisReference = null, - error_y: Error = null, - showlegend: JBoolean = null + x: Sequence, + y: Sequence, + name: String = null, + text: Seq[String] = null, + marker: Marker = null, + orientation: Orientation = null, + xaxis: AxisReference = null, + yaxis: AxisReference = null, + error_y: Error = null, + showlegend: JBoolean = null, + hoverinfo: HoverInfo = null, + textposition: BarTextPosition = null, + opacity: JDouble = null, + width: OneOrSeq[Double] = null, + base: OneOrSeq[Double] = null ): Bar = Bar( x, @@ -158,43 +229,176 @@ object Bar { Option(xaxis), Option(yaxis), Option(error_y), - Option(showlegend).map(b => b: Boolean) + Option(showlegend).map(b => b: Boolean), + Option(hoverinfo), + Option(textposition), + Option(opacity).map(d => d: Double), + Option(width), + Option(base) ) } -case class Histogram( - x: Option[Sequence], - y: Option[Sequence], - opacity: Option[Double], - name: Option[String], - autobinx: Option[Boolean], - marker: Option[Marker], - xbins: Option[Bins], - histnorm: Option[HistNorm], - showlegend: Option[Boolean] +@data(optionSetters = true) class Histogram( + x: Option[Sequence] = None, + y: Option[Sequence] = None, + opacity: Option[Double] = None, + name: Option[String] = None, + autobinx: Option[Boolean] = None, + marker: Option[Marker] = None, + xbins: Option[Bins] = None, + histnorm: Option[HistNorm] = None, + showlegend: Option[Boolean] = None, + cumulative: Option[Cumulative] = None, + histfunc: Option[HistFunc] = None, + @since("0.8.2") + hovertemplate: Option[OneOrSeq[String]] = None, + @since("0.8.5") + yaxis: Option[String] = None, + hovertext: Option[OneOrSeq[String]] = None ) extends Trace object Histogram { + def apply(x: Sequence): Histogram = + Histogram().withX(x) + + def apply(x: Sequence, y: Sequence): Histogram = + Histogram().withX(x).withY(y) + + @deprecated("Use Histogram() and chain-call .with* methods on it instead", "0.8.0") def apply( - x: Sequence = null, - y: Sequence = null, - opacity: JDouble = null, - name: String = null, - autobinx: JBoolean = null, - marker: Marker = null, - xbins: Bins = null, - histnorm: HistNorm = null, - showlegend: JBoolean = null + x: Sequence = null, + y: Sequence = null, + opacity: JDouble = null, + name: String = null, + autobinx: JBoolean = null, + marker: Marker = null, + xbins: Bins = null, + histnorm: HistNorm = null, + showlegend: JBoolean = null, + cumulative: Cumulative = null, + histfunc: HistFunc = null ): Histogram = Histogram( Option(x), Option(y), - Option(opacity) .map(d => d: Double), + Option(opacity).map(d => d: Double), Option(name), - Option(autobinx) .map(b => b: Boolean), + Option(autobinx).map(b => b: Boolean), Option(marker), Option(xbins), Option(histnorm), - Option(showlegend) .map(b => b: Boolean) + Option(showlegend).map(b => b: Boolean), + Option(cumulative), + Option(histfunc) + ) +} + +@data(optionSetters = true) class Surface( + x: Option[Sequence] = None, + y: Option[Sequence] = None, + z: Option[Sequence] = None, + showscale: Option[Boolean] = None, + opacity: Option[Double] = None, + @since("0.8.2") + hovertemplate: Option[OneOrSeq[String]] = None +) extends Trace + +object Surface { + @deprecated("Use Surface() and chain-call .with* methods on it instead", "0.8.0") + def apply( + x: Sequence = null, + y: Sequence = null, + z: Sequence = null, + showscale: JBoolean = null, + opacity: JDouble = null + ): Surface = + Surface( + Option(x), + Option(y), + Option(z), + Option(showscale).map(b => b: Boolean), + Option(opacity).map(d => d: Double) + ) +} + +@data(optionSetters = true) class Heatmap( + y: Option[Sequence] = None, + x: Option[Sequence] = None, + z: Option[Sequence] = None, + autocolorscale: Option[Boolean] = None, + colorscale: Option[ColorScale] = None, + showscale: Option[Boolean] = None, + name: Option[String] = None, + @since("0.8.2") + hovertemplate: Option[OneOrSeq[String]] = None, + hoverongaps: Option[Boolean] = None +) extends Trace + +object Heatmap { + def apply(z: Sequence): Heatmap = + Heatmap().withZ(z) + + def apply(z: Sequence, x: Sequence, y: Sequence): Heatmap = + Heatmap().withZ(z).withX(x).withY(y) + + @deprecated("Use Heatmap() and chain-call .with* methods on it instead", "0.8.0") + def apply( + y: Sequence = null, + x: Sequence = null, + z: Sequence = null, + autocolorscale: JBoolean = null, + colorscale: ColorScale = null, + showscale: JBoolean = null, + name: String = null + ): Heatmap = + Heatmap( + Option(y), + Option(x), + Option(z), + Option(autocolorscale).map(b => b: Boolean), + Option(colorscale), + Option(showscale).map(b => b: Boolean), + Option(name) + ) +} + +@data(optionSetters = true) class Candlestick( + x: Option[Sequence] = None, + close: Option[Sequence] = None, + high: Option[Sequence] = None, + low: Option[Sequence] = None, + open: Option[Sequence] = None, + decreasing: Option[Marker] = None, + increasing: Option[Marker] = None, + line: Option[Marker] = None, + xaxis: Option[AxisReference] = None, + yaxis: Option[AxisReference] = None +) extends Trace + +object Candlestick { + @deprecated("Use Candlestick() and chain-call .with* methods on it instead", "0.8.0") + def apply( + x: Sequence = null, + close: Sequence = null, + high: Sequence = null, + low: Sequence = null, + open: Sequence = null, + decreasing: Marker = null, + increasing: Marker = null, + line: Marker = null, + xaxis: AxisReference = null, + yaxis: AxisReference = null + ): Candlestick = + Candlestick( + Option(x), + Option(close), + Option(high), + Option(low), + Option(open), + Option(decreasing), + Option(increasing), + Option(line), + Option(xaxis), + Option(yaxis) ) } diff --git a/core/shared/src/main/scala/plotly/element/Alignment.scala b/core/shared/src/main/scala/plotly/element/Alignment.scala new file mode 100644 index 00000000..c5043917 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/Alignment.scala @@ -0,0 +1,9 @@ +package plotly.element + +sealed abstract class Alignment(val label: String) extends Product with Serializable + +object Alignment { + case object Left extends Alignment("left") + case object Right extends Alignment("right") + case object Auto extends Alignment("auto") +} diff --git a/core/shared/src/main/scala/plotly/element/Anchor.scala b/core/shared/src/main/scala/plotly/element/Anchor.scala index a3ed33f1..c1204e0f 100644 --- a/core/shared/src/main/scala/plotly/element/Anchor.scala +++ b/core/shared/src/main/scala/plotly/element/Anchor.scala @@ -4,10 +4,10 @@ package element sealed abstract class Anchor(val label: String) extends Product with Serializable object Anchor { - case object Left extends Anchor("left") + case object Left extends Anchor("left") case object Center extends Anchor("center") - case object Right extends Anchor("right") - case object Top extends Anchor("top") + case object Right extends Anchor("right") + case object Top extends Anchor("top") case object Middle extends Anchor("middle") case object Bottom extends Anchor("bottom") } diff --git a/core/shared/src/main/scala/plotly/element/AxisAnchor.scala b/core/shared/src/main/scala/plotly/element/AxisAnchor.scala index 3a777148..0ba83157 100644 --- a/core/shared/src/main/scala/plotly/element/AxisAnchor.scala +++ b/core/shared/src/main/scala/plotly/element/AxisAnchor.scala @@ -1,9 +1,12 @@ package plotly package element +import dataclass.data + sealed abstract class AxisAnchor(val label: String) extends Product with Serializable object AxisAnchor { - case class Reference(axisReference: AxisReference) extends AxisAnchor(axisReference.label) - case object Free extends AxisAnchor("free") + @data class Reference(axisReference: AxisReference) extends AxisAnchor(axisReference.label) + case object Free extends AxisAnchor("free") + case object Y extends AxisAnchor("y") } diff --git a/core/shared/src/main/scala/plotly/element/AxisReference.scala b/core/shared/src/main/scala/plotly/element/AxisReference.scala index fd07a01a..8a326d79 100644 --- a/core/shared/src/main/scala/plotly/element/AxisReference.scala +++ b/core/shared/src/main/scala/plotly/element/AxisReference.scala @@ -4,12 +4,12 @@ package element sealed abstract class AxisReference(val label: String) extends Product with Serializable object AxisReference { - case object X extends AxisReference("x") + case object X extends AxisReference("x") case object X1 extends AxisReference("x1") case object X2 extends AxisReference("x2") case object X3 extends AxisReference("x3") case object X4 extends AxisReference("x4") - case object Y extends AxisReference("y") + case object Y extends AxisReference("y") case object Y1 extends AxisReference("y1") case object Y2 extends AxisReference("y2") case object Y3 extends AxisReference("y3") diff --git a/core/shared/src/main/scala/plotly/element/AxisType.scala b/core/shared/src/main/scala/plotly/element/AxisType.scala index 417c560e..5c7325f8 100644 --- a/core/shared/src/main/scala/plotly/element/AxisType.scala +++ b/core/shared/src/main/scala/plotly/element/AxisType.scala @@ -4,10 +4,11 @@ package element sealed abstract class AxisType(val label: String) extends Product with Serializable object AxisType { + /** Lets plotly guess from data */ - case object Default extends AxisType("-") - case object Linear extends AxisType("linear") - case object Log extends AxisType("log") - case object Date extends AxisType("date") + case object Default extends AxisType("-") + case object Linear extends AxisType("linear") + case object Log extends AxisType("log") + case object Date extends AxisType("date") case object Category extends AxisType("category") } diff --git a/core/shared/src/main/scala/plotly/element/BarTextPosition.scala b/core/shared/src/main/scala/plotly/element/BarTextPosition.scala new file mode 100644 index 00000000..964d4995 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/BarTextPosition.scala @@ -0,0 +1,11 @@ +package plotly +package element + +sealed abstract class BarTextPosition(val label: String) extends Product with Serializable + +object BarTextPosition { + case object Inside extends BarTextPosition("inside") + case object Outside extends BarTextPosition("outside") + case object Auto extends BarTextPosition("auto") + case object None extends BarTextPosition("none") +} diff --git a/core/shared/src/main/scala/plotly/element/Bins.scala b/core/shared/src/main/scala/plotly/element/Bins.scala index f580cf14..1a2b0382 100644 --- a/core/shared/src/main/scala/plotly/element/Bins.scala +++ b/core/shared/src/main/scala/plotly/element/Bins.scala @@ -1,3 +1,9 @@ package plotly.element -case class Bins(start: Double, end: Double, size: Double) +import dataclass.data + +@data class Bins( + start: Double, + end: Double, + size: Double +) diff --git a/core/shared/src/main/scala/plotly/element/BoxMean.scala b/core/shared/src/main/scala/plotly/element/BoxMean.scala index ad1948aa..f1e9a70e 100644 --- a/core/shared/src/main/scala/plotly/element/BoxMean.scala +++ b/core/shared/src/main/scala/plotly/element/BoxMean.scala @@ -1,13 +1,15 @@ package plotly package element +import dataclass.data + sealed abstract class BoxMean extends Product with Serializable object BoxMean { - case class Bool(value: Boolean) extends BoxMean + @data class Bool(value: Boolean) extends BoxMean sealed abstract class Labeled(val label: String) extends BoxMean - val True = Bool(true) + val True = Bool(true) val False = Bool(false) case object SD extends Labeled("sd") diff --git a/core/shared/src/main/scala/plotly/element/BoxPoints.scala b/core/shared/src/main/scala/plotly/element/BoxPoints.scala index 3dc67fdb..09d82326 100644 --- a/core/shared/src/main/scala/plotly/element/BoxPoints.scala +++ b/core/shared/src/main/scala/plotly/element/BoxPoints.scala @@ -1,16 +1,18 @@ package plotly package element +import dataclass.data + sealed abstract class BoxPoints extends Product with Serializable object BoxPoints { - case class Bool(value: Boolean) extends BoxPoints + @data class Bool(value: Boolean) extends BoxPoints sealed abstract class Labeled(val label: String) extends BoxPoints val False = Bool(false) - val True = Bool(true) + val True = Bool(true) - case object All extends Labeled("all") + case object All extends Labeled("all") case object SuspectedOutliers extends Labeled("suspectedoutliers") - case object Outliers extends Labeled("Outliers") // FIXME case? + case object Outliers extends Labeled("Outliers") // FIXME case? } diff --git a/core/shared/src/main/scala/plotly/element/Color.scala b/core/shared/src/main/scala/plotly/element/Color.scala index c4ccca39..84f508f2 100644 --- a/core/shared/src/main/scala/plotly/element/Color.scala +++ b/core/shared/src/main/scala/plotly/element/Color.scala @@ -1,13 +1,15 @@ package plotly package element +import dataclass.data + sealed abstract class Color extends Product with Serializable object Color { - final case class RGBA(r: Int, g: Int, b: Int, alpha: Double) extends Color + @data class RGBA(r: Int, g: Int, b: Int, alpha: Double) extends Color - final case class StringColor(color: String) extends Color + @data class StringColor(color: String) extends Color object StringColor { val colors = Set( @@ -15,11 +17,16 @@ object Color { "grey", "white", "fuchsia", - "red" + "red", + "blue", + "cls", // ??? + "pink", + "green", + "magenta" ) } - final case class RGB(r: Int, g: Int, b: Int) extends Color + @data class RGB(r: Int, g: Int, b: Int) extends Color - final case class HSL(h: Int, s: Int, l: Int) extends Color -} \ No newline at end of file + @data class HSL(h: Int, s: Int, l: Int) extends Color +} diff --git a/core/shared/src/main/scala/plotly/element/ColorModel.scala b/core/shared/src/main/scala/plotly/element/ColorModel.scala new file mode 100644 index 00000000..cec1d9a8 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/ColorModel.scala @@ -0,0 +1,11 @@ +package plotly.element + +sealed abstract class ColorModel(val label: String) extends Product with Serializable + +object ColorModel { + case object RGB extends ColorModel("rgb") + case object RGBA extends ColorModel("rgba") + case object RGBA256 extends ColorModel("rgba256") + case object HSL extends ColorModel("hsl") + case object HSLA extends ColorModel("hsla") +} diff --git a/core/shared/src/main/scala/plotly/element/ColorScale.scala b/core/shared/src/main/scala/plotly/element/ColorScale.scala new file mode 100755 index 00000000..c99d9474 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/ColorScale.scala @@ -0,0 +1,9 @@ +package plotly.element +import dataclass.data + +sealed abstract class ColorScale extends Product with Serializable + +object ColorScale { + @data class CustomScale(values: Seq[(Double, Color)]) extends ColorScale + @data class NamedScale(name: String) extends ColorScale +} diff --git a/core/shared/src/main/scala/plotly/element/Cumulative.scala b/core/shared/src/main/scala/plotly/element/Cumulative.scala new file mode 100644 index 00000000..4a64d110 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/Cumulative.scala @@ -0,0 +1,5 @@ +package plotly.element + +import dataclass.data + +@data class Cumulative(enabled: Boolean) diff --git a/core/shared/src/main/scala/plotly/element/Element.scala b/core/shared/src/main/scala/plotly/element/Element.scala index a6ea4e5c..d7de92b4 100644 --- a/core/shared/src/main/scala/plotly/element/Element.scala +++ b/core/shared/src/main/scala/plotly/element/Element.scala @@ -1,6 +1,8 @@ package plotly package element +import scala.language.implicitConversions + sealed abstract class Element extends Product with Serializable object Element { diff --git a/core/shared/src/main/scala/plotly/element/Error.scala b/core/shared/src/main/scala/plotly/element/Error.scala index e16f3f96..4f0ae7ab 100644 --- a/core/shared/src/main/scala/plotly/element/Error.scala +++ b/core/shared/src/main/scala/plotly/element/Error.scala @@ -1,24 +1,28 @@ package plotly package element -import java.lang.{ Boolean => JBoolean, Double => JDouble } +import java.lang.{Boolean => JBoolean, Double => JDouble} + +import dataclass._ sealed abstract class Error(val `type`: String) extends Product with Serializable object Error { - case class Data( - array: Seq[Double], - visible: Option[Boolean], - symmetric: Option[Boolean], - arrayminus: Option[Seq[Double]] + @data(optionSetters = true) class Data( + array: Seq[Double], + @since + visible: Option[Boolean] = None, + symmetric: Option[Boolean] = None, + arrayminus: Option[Seq[Double]] = None ) extends Error("data") object Data { + @deprecated("Use Data(array) and chain-call .with* methods on it instead", "0.8.0") def apply( - array: Seq[Double], - visible: JBoolean = null, - symmetric: JBoolean = null, - arrayminus: Seq[Double] = null + array: Seq[Double], + visible: JBoolean = null, + symmetric: JBoolean = null, + arrayminus: Seq[Double] = null ): Data = Data( array, @@ -28,19 +32,21 @@ object Error { ) } - case class Percent( - value: Double, - visible: Option[Boolean], - symmetric: Option[Boolean], - valueminus: Option[Double] + @data(optionSetters = true) class Percent( + value: Double, + @since + visible: Option[Boolean] = None, + symmetric: Option[Boolean] = None, + valueminus: Option[Double] = None ) extends Error("percent") object Percent { + @deprecated("Use Percent(value) and chain-call .with* methods on it instead", "0.8.0") def apply( - value: Double, - visible: JBoolean = null, - symmetric: JBoolean = null, - valueminus: JDouble = null + value: Double, + visible: JBoolean = null, + symmetric: JBoolean = null, + valueminus: JDouble = null ): Percent = Percent( value, @@ -50,21 +56,22 @@ object Error { ) } - case class Constant( - value: Double, - color: Option[String], - thickness: Option[Double], - opacity: Option[Double], - width: Option[Double] + @data(optionSetters = true) class Constant( + value: Double, + color: Option[String] = None, + thickness: Option[Double] = None, + opacity: Option[Double] = None, + width: Option[Double] = None ) extends Error("constant") object Constant { + @deprecated("Use Constant(value) and chain-call .with* methods on it instead", "0.8.0") def apply( - value: Double, - color: String = null, - thickness: JDouble = null, + value: Double, + color: String = null, + thickness: JDouble = null, opacity: JDouble = null, - width: JDouble = null + width: JDouble = null ): Constant = Constant( value, diff --git a/core/shared/src/main/scala/plotly/element/Fill.scala b/core/shared/src/main/scala/plotly/element/Fill.scala index 9ad76c24..438cc58c 100644 --- a/core/shared/src/main/scala/plotly/element/Fill.scala +++ b/core/shared/src/main/scala/plotly/element/Fill.scala @@ -4,6 +4,11 @@ package element sealed abstract class Fill(val label: String) extends Product with Serializable object Fill { + case object None extends Fill("none") + case object ToZeroX extends Fill("tozerox") case object ToZeroY extends Fill("tozeroy") + case object ToNextX extends Fill("tonextx") case object ToNextY extends Fill("tonexty") + case object ToSelf extends Fill("toself") + case object ToNext extends Fill("tonext") } diff --git a/core/shared/src/main/scala/plotly/element/GroupNorm.scala b/core/shared/src/main/scala/plotly/element/GroupNorm.scala new file mode 100644 index 00000000..03deecbc --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/GroupNorm.scala @@ -0,0 +1,8 @@ +package plotly.element + +sealed abstract class GroupNorm(val label: String) extends Product with Serializable + +object GroupNorm { + case object Fraction extends GroupNorm("fraction") + case object Percent extends GroupNorm("percent") +} diff --git a/core/shared/src/main/scala/plotly/element/HistFunc.scala b/core/shared/src/main/scala/plotly/element/HistFunc.scala new file mode 100644 index 00000000..4aabca56 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/HistFunc.scala @@ -0,0 +1,13 @@ +package plotly.element + +sealed abstract class HistFunc(val label: String) extends Product with Serializable + +object HistFunc { + + case object Count extends HistFunc("count") + case object Sum extends HistFunc("sum") + case object Average extends HistFunc("avg") + case object Min extends HistFunc("min") + case object Max extends HistFunc("max") + +} diff --git a/core/shared/src/main/scala/plotly/element/HistNorm.scala b/core/shared/src/main/scala/plotly/element/HistNorm.scala index 694062fc..fa619ce7 100644 --- a/core/shared/src/main/scala/plotly/element/HistNorm.scala +++ b/core/shared/src/main/scala/plotly/element/HistNorm.scala @@ -3,9 +3,9 @@ package plotly.element sealed abstract class HistNorm(val label: String) extends Product with Serializable object HistNorm { - case object Count extends HistNorm("count") - case object Percent extends HistNorm("percent") - case object Probability extends HistNorm("probability") - case object Density extends HistNorm("density") + case object Count extends HistNorm("count") + case object Percent extends HistNorm("percent") + case object Probability extends HistNorm("probability") + case object Density extends HistNorm("density") case object ProbabilityDensity extends HistNorm("probability density") } diff --git a/core/shared/src/main/scala/plotly/element/HoverInfo.scala b/core/shared/src/main/scala/plotly/element/HoverInfo.scala new file mode 100644 index 00000000..de8d8b26 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/HoverInfo.scala @@ -0,0 +1,36 @@ +package plotly.element + +sealed abstract class HoverInfo extends Product with Serializable { + def label: String +} + +object HoverInfo { + + def all: HoverInfo = All + def none: HoverInfo = None + def skip: HoverInfo = Skip + def apply(elements: Element*): HoverInfo = + Combination(elements) + + sealed abstract class Element(override val label: String) extends HoverInfo + + case object X extends Element("x") + case object Y extends Element("y") + case object Z extends Element("z") + case object Text extends Element("text") + case object Name extends Element("name") + case object Color extends Element("color") + + case object All extends HoverInfo { + def label = "all" + } + val None = Combination(Nil) + case object Skip extends HoverInfo { + def label = "skip" + } + + final case class Combination(elements: Seq[Element]) extends HoverInfo { + def label: String = elements.map(_.label).mkString("+") + } + +} diff --git a/core/shared/src/main/scala/plotly/element/HoverLabel.scala b/core/shared/src/main/scala/plotly/element/HoverLabel.scala new file mode 100644 index 00000000..15342160 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/HoverLabel.scala @@ -0,0 +1,12 @@ +package plotly.element + +import dataclass.data + +@data(optionSetters = true) class HoverLabel( + bgcolor: Option[OneOrSeq[Color]] = None, + bordercolor: Option[OneOrSeq[Color]] = None, + font: Option[HoverLabelFont] = None, + align: Option[OneOrSeq[Alignment]] = None, + namelength: Option[OneOrSeq[Int]] = None, + uirevision: Option[Element] = None +) diff --git a/core/shared/src/main/scala/plotly/element/HoverLabelFont.scala b/core/shared/src/main/scala/plotly/element/HoverLabelFont.scala new file mode 100644 index 00000000..7b839530 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/HoverLabelFont.scala @@ -0,0 +1,9 @@ +package plotly.element + +import dataclass.data + +@data(optionSetters = true) class HoverLabelFont( + family: Option[OneOrSeq[String]] = None, + size: Option[OneOrSeq[Double]] = None, + color: Option[OneOrSeq[Color]] = None +) diff --git a/core/shared/src/main/scala/plotly/element/HoverOn.scala b/core/shared/src/main/scala/plotly/element/HoverOn.scala new file mode 100644 index 00000000..6cbc68f4 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/HoverOn.scala @@ -0,0 +1,9 @@ +package plotly.element + +sealed abstract class HoverOn(val label: String) extends Product with Serializable + +object HoverOn { + case object Points extends HoverOn("points") + case object Fills extends HoverOn("fills") + case object PointsFill extends HoverOn("points+fills") +} diff --git a/core/shared/src/main/scala/plotly/element/Line.scala b/core/shared/src/main/scala/plotly/element/Line.scala index 7a72aaf7..05168d43 100644 --- a/core/shared/src/main/scala/plotly/element/Line.scala +++ b/core/shared/src/main/scala/plotly/element/Line.scala @@ -1,25 +1,28 @@ package plotly package element -import java.lang.{ Double => JDouble } +import dataclass.data -final case class Line( - shape: Option[LineShape], - color: Option[OneOrSeq[Color]], - width: Option[OneOrSeq[Double]], - dash: Option[Dash], - outliercolor: Option[Color], - outlierwidth: Option[Double] +import java.lang.{Double => JDouble} + +@data(optionSetters = true) class Line( + shape: Option[LineShape] = None, + color: Option[OneOrSeq[Color]] = None, + width: Option[OneOrSeq[Double]] = None, + dash: Option[Dash] = None, + outliercolor: Option[Color] = None, + outlierwidth: Option[Double] = None ) object Line { + @deprecated("Use Line() and chain-call .with* methods on it instead", "0.8.0") def apply( - shape: LineShape = null, - color: OneOrSeq[Color] = null, - width: OneOrSeq[Double] = null, - dash: Dash = null, - outliercolor: Color = null, - outlierwidth: JDouble = null + shape: LineShape = null, + color: OneOrSeq[Color] = null, + width: OneOrSeq[Double] = null, + dash: Dash = null, + outliercolor: Color = null, + outlierwidth: JDouble = null ): Line = Line( Option(shape), @@ -27,6 +30,6 @@ object Line { Option(width), Option(dash), Option(outliercolor), - Option(outlierwidth) .map(x => x: Double) + Option(outlierwidth).map(x => x: Double) ) } diff --git a/core/shared/src/main/scala/plotly/element/LineShape.scala b/core/shared/src/main/scala/plotly/element/LineShape.scala index 70637e52..d9a98618 100644 --- a/core/shared/src/main/scala/plotly/element/LineShape.scala +++ b/core/shared/src/main/scala/plotly/element/LineShape.scala @@ -6,8 +6,8 @@ sealed abstract class LineShape(val label: String) extends Product with Serializ object LineShape { case object Linear extends LineShape("linear") case object Spline extends LineShape("spline") - case object VHV extends LineShape("vhv") - case object HVH extends LineShape("hvh") - case object VH extends LineShape("vh") - case object HV extends LineShape("hv") + case object VHV extends LineShape("vhv") + case object HVH extends LineShape("hvh") + case object VH extends LineShape("vh") + case object HV extends LineShape("hv") } diff --git a/core/shared/src/main/scala/plotly/element/LocalDateTime.scala b/core/shared/src/main/scala/plotly/element/LocalDateTime.scala index 8b0709ae..e140fa5a 100644 --- a/core/shared/src/main/scala/plotly/element/LocalDateTime.scala +++ b/core/shared/src/main/scala/plotly/element/LocalDateTime.scala @@ -1,25 +1,26 @@ package plotly.element +import dataclass.data + import scala.util.Try -case class LocalDateTime( - year: Int, - month: Int, - dayOfMonth: Int, - hour: Int, - minute: Int, - second: Int +@data class LocalDateTime( + year: Int, + month: Int, + dayOfMonth: Int, + hour: Int, + minute: Int, + second: Int ) { override def toString: String = f"$year-$month%02d-$dayOfMonth%02d $hour%02d:$minute%02d:$second%02d" } -object LocalDateTime { +object LocalDateTime extends PlotlyJavaTimeConversions { private object IntStr { def unapply(s: String): Option[Int] = - Try(s.toInt) - .toOption + Try(s.toInt).toOption } def parse(s: String): Option[LocalDateTime] = @@ -28,6 +29,8 @@ object LocalDateTime { (d.split('-'), t.split(':')) match { case (Array(IntStr(y), IntStr(m), IntStr(d)), Array(IntStr(h), IntStr(min), IntStr(s))) => Some(LocalDateTime(y, m, d, h, min, s)) + case (Array(IntStr(y), IntStr(m), IntStr(d)), Array(IntStr(h), IntStr(min))) => + Some(LocalDateTime(y, m, d, h, min, 0)) case _ => None } case _ => None diff --git a/core/shared/src/main/scala/plotly/element/Marker.scala b/core/shared/src/main/scala/plotly/element/Marker.scala index d30825cc..5efb90b9 100644 --- a/core/shared/src/main/scala/plotly/element/Marker.scala +++ b/core/shared/src/main/scala/plotly/element/Marker.scala @@ -1,31 +1,34 @@ package plotly package element -import java.lang.{ Double => JDouble } +import java.lang.{Double => JDouble} -final case class Marker( - size: Option[OneOrSeq[Int]], - color: Option[OneOrSeq[Color]], - opacity: Option[OneOrSeq[Double]], - line: Option[Line], - symbol: Option[OneOrSeq[Symbol]], - outliercolor: Option[Color], - sizeref: Option[Double], - sizemode: Option[SizeMode], - width: Option[OneOrSeq[Int]] +import dataclass.data + +@data(optionSetters = true) class Marker( + size: Option[OneOrSeq[Int]] = None, + color: Option[OneOrSeq[Color]] = None, + opacity: Option[OneOrSeq[Double]] = None, + line: Option[Line] = None, + symbol: Option[OneOrSeq[Symbol]] = None, + outliercolor: Option[Color] = None, + sizeref: Option[Double] = None, + sizemode: Option[SizeMode] = None, + width: Option[OneOrSeq[Int]] = None ) object Marker { + @deprecated("Use Marker() and chain-call .with* methods on it instead", "0.8.0") def apply( - size: OneOrSeq[Int] = null, - color: OneOrSeq[Color] = null, - opacity: OneOrSeq[Double] = null, - line: Line = null, - symbol: OneOrSeq[Symbol] = null, - outliercolor: Color = null, - sizeref: JDouble = null, - sizemode: SizeMode = null, - width: OneOrSeq[Int] = null + size: OneOrSeq[Int] = null, + color: OneOrSeq[Color] = null, + opacity: OneOrSeq[Double] = null, + line: Line = null, + symbol: OneOrSeq[Symbol] = null, + outliercolor: Color = null, + sizeref: JDouble = null, + sizemode: SizeMode = null, + width: OneOrSeq[Int] = null ): Marker = Marker( Option(size), diff --git a/core/shared/src/main/scala/plotly/element/OneOrSeq.scala b/core/shared/src/main/scala/plotly/element/OneOrSeq.scala index fb3582eb..c4c32a05 100644 --- a/core/shared/src/main/scala/plotly/element/OneOrSeq.scala +++ b/core/shared/src/main/scala/plotly/element/OneOrSeq.scala @@ -1,9 +1,11 @@ package plotly.element +import scala.language.implicitConversions + sealed abstract class OneOrSeq[T] extends Product with Serializable object OneOrSeq { - case class One[T](value: T) extends OneOrSeq[T] + case class One[T](value: T) extends OneOrSeq[T] case class Sequence[T](seq: Seq[T]) extends OneOrSeq[T] implicit def fromOne[T](value: T): OneOrSeq[T] = diff --git a/core/shared/src/main/scala/plotly/element/ScatterMode.scala b/core/shared/src/main/scala/plotly/element/ScatterMode.scala index 93d5b3e6..0747e800 100644 --- a/core/shared/src/main/scala/plotly/element/ScatterMode.scala +++ b/core/shared/src/main/scala/plotly/element/ScatterMode.scala @@ -1,7 +1,9 @@ package plotly package element -case class ScatterMode(flags: Set[ScatterMode.Flag]) +import dataclass.data + +@data class ScatterMode(flags: Set[ScatterMode.Flag]) object ScatterMode { def apply(flags: Flag*): ScatterMode = @@ -10,9 +12,9 @@ object ScatterMode { sealed abstract class Flag(val label: String) extends Product with Serializable case object Markers extends Flag("markers") - case object Text extends Flag("text") - case object Lines extends Flag("lines") + case object Text extends Flag("text") + case object Lines extends Flag("lines") - val flags = Seq(Markers, Text, Lines) + val flags = Seq(Markers, Text, Lines) val flagMap = flags.map(m => m.label -> m).toMap } diff --git a/core/shared/src/main/scala/plotly/element/Side.scala b/core/shared/src/main/scala/plotly/element/Side.scala index cd37d3ba..0e0137ac 100644 --- a/core/shared/src/main/scala/plotly/element/Side.scala +++ b/core/shared/src/main/scala/plotly/element/Side.scala @@ -4,8 +4,8 @@ package element sealed abstract class Side(val label: String) extends Product with Serializable object Side { - case object Left extends Side("left") - case object Right extends Side("right") - case object Top extends Side("top") + case object Left extends Side("left") + case object Right extends Side("right") + case object Top extends Side("top") case object Bottom extends Side("bottom") } diff --git a/core/shared/src/main/scala/plotly/element/SizeMode.scala b/core/shared/src/main/scala/plotly/element/SizeMode.scala index 578e5971..7a453ff6 100644 --- a/core/shared/src/main/scala/plotly/element/SizeMode.scala +++ b/core/shared/src/main/scala/plotly/element/SizeMode.scala @@ -4,5 +4,5 @@ sealed abstract class SizeMode(val label: String) extends Product with Serializa object SizeMode { case object Diameter extends SizeMode("diameter") - case object Area extends SizeMode("area") + case object Area extends SizeMode("area") } diff --git a/core/shared/src/main/scala/plotly/element/Symbol.scala b/core/shared/src/main/scala/plotly/element/Symbol.scala index 14ae03d9..ede2a32b 100644 --- a/core/shared/src/main/scala/plotly/element/Symbol.scala +++ b/core/shared/src/main/scala/plotly/element/Symbol.scala @@ -1,23 +1,24 @@ package plotly package element +import dataclass.data sealed abstract class Symbol(label0: String, idx0: Int) extends Product with Serializable { - def idx: Int = idx0 + (if (open) 100 else 0) - def label: String = label0 + (if (open) "-open" else "") + def idx: Int = idx0 + (if (open) 100 else 0) + def label: String = label0 + (if (open) "-open" else "") def open: Boolean } object Symbol { sealed abstract class DotSymbol(label0: String, idx0: Int) extends Symbol(label0, idx0) { - override def idx: Int = super.idx + (if (dot) 200 else 0) + override def idx: Int = super.idx + (if (dot) 200 else 0) override def label: String = super.label + (if (dot) "-dot" else "") def dot: Boolean } - case class Circle(open: Boolean = false, dot: Boolean = false) extends DotSymbol("circle", 0) - case class Square(open: Boolean = false, dot: Boolean = false) extends DotSymbol("square", 1) - case class Diamond(open: Boolean = false, dot: Boolean = false) extends DotSymbol("diamond", 2) - case class Cross(open: Boolean = false, dot: Boolean = false) extends DotSymbol("cross", 3) + @data class Circle(open: Boolean = false, dot: Boolean = false) extends DotSymbol("circle", 0) + @data class Square(open: Boolean = false, dot: Boolean = false) extends DotSymbol("square", 1) + @data class Diamond(open: Boolean = false, dot: Boolean = false) extends DotSymbol("diamond", 2) + @data class Cross(open: Boolean = false, dot: Boolean = false) extends DotSymbol("cross", 3) /* "4" | "x" | "104" | "x-open" | "204" | "x-dot" | "304" | "x-open-dot" @@ -61,5 +62,5 @@ object Symbol { "42" | "line-ns" | "142" | "line-ns-open" "43" | "line-ne" | "143" | "line-ne-open" "44" | "line-nw" | "144" | "line-nw-open - */ + */ } diff --git a/core/shared/src/main/scala/plotly/element/TextFont.scala b/core/shared/src/main/scala/plotly/element/TextFont.scala index 13f16697..ac2a9fbc 100644 --- a/core/shared/src/main/scala/plotly/element/TextFont.scala +++ b/core/shared/src/main/scala/plotly/element/TextFont.scala @@ -1,4 +1,6 @@ package plotly package element -final case class TextFont(family: String) +import dataclass.data + +@data class TextFont(family: String) diff --git a/core/shared/src/main/scala/plotly/element/TickMode.scala b/core/shared/src/main/scala/plotly/element/TickMode.scala new file mode 100644 index 00000000..2cfb97d1 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/TickMode.scala @@ -0,0 +1,9 @@ +package plotly.element + +sealed abstract class TickMode(val mode: String) extends Product with Serializable + +object TickMode { + case object Auto extends TickMode("auto") + case object Linear extends TickMode("linear") + case object Array extends TickMode("array") +} diff --git a/core/shared/src/main/scala/plotly/element/Ticks.scala b/core/shared/src/main/scala/plotly/element/Ticks.scala old mode 100644 new mode 100755 index 9142bd72..bd2256a5 --- a/core/shared/src/main/scala/plotly/element/Ticks.scala +++ b/core/shared/src/main/scala/plotly/element/Ticks.scala @@ -5,4 +5,6 @@ sealed abstract class Ticks(val label: String) extends Product with Serializable object Ticks { case object Outside extends Ticks("outside") + case object Inside extends Ticks("inside") + case object Empty extends Ticks("") } diff --git a/core/shared/src/main/scala/plotly/layout/Annotation.scala b/core/shared/src/main/scala/plotly/layout/Annotation.scala index b8a1dae3..a12d873b 100644 --- a/core/shared/src/main/scala/plotly/layout/Annotation.scala +++ b/core/shared/src/main/scala/plotly/layout/Annotation.scala @@ -1,33 +1,38 @@ package plotly package layout -import java.lang.{ Boolean => JBoolean } +import java.lang.{Boolean => JBoolean, Double => JDouble} +import dataclass.data import plotly.element._ -final case class Annotation( - xref: Option[Ref], - yref: Option[Ref], - x: Option[Element], - y: Option[Element], - xanchor: Option[Anchor], - yanchor: Option[Anchor], - text: Option[Element], - font: Option[Font], - showarrow: Option[Boolean] +@data(optionSetters = true) class Annotation( + xref: Option[Ref] = None, + yref: Option[Ref] = None, + x: Option[Element] = None, + y: Option[Element] = None, + xanchor: Option[Anchor] = None, + yanchor: Option[Anchor] = None, + text: Option[Element] = None, + font: Option[Font] = None, + showarrow: Option[Boolean] = None, + @since("0.8.0") + ax: Option[Double] = None, + ay: Option[Double] = None ) object Annotation { + @deprecated("Use Annotation() and chain-call .with* methods on it instead", "0.8.0") def apply( - xref: Ref = null, - yref: Ref = null, - x: Element = null, - y: Element = null, - xanchor: Anchor = null, - yanchor: Anchor = null, - text: Element = null, - font: Font = null, - showarrow: JBoolean = null + xref: Ref = null, + yref: Ref = null, + x: Element = null, + y: Element = null, + xanchor: Anchor = null, + yanchor: Anchor = null, + text: Element = null, + font: Font = null, + showarrow: JBoolean = null ): Annotation = Annotation( Option(xref), diff --git a/core/shared/src/main/scala/plotly/layout/Axis.scala b/core/shared/src/main/scala/plotly/layout/Axis.scala old mode 100644 new mode 100755 index a96dc5a5..b6c0bf2c --- a/core/shared/src/main/scala/plotly/layout/Axis.scala +++ b/core/shared/src/main/scala/plotly/layout/Axis.scala @@ -1,100 +1,132 @@ package plotly package layout -import java.lang.{ Integer => JInt, Double => JDouble, Boolean => JBoolean } +import java.lang.{Boolean => JBoolean, Double => JDouble, Integer => JInt} +import dataclass.data import plotly.element._ -final case class Axis( - title: Option[String], - titlefont: Option[Font], - showgrid: Option[Boolean], - gridwidth: Option[Int], - gridcolor: Option[Color], - showline: Option[Boolean], - showticklabels: Option[Boolean], - linecolor: Option[Color], - linewidth: Option[Int], - autotick: Option[Boolean], - tickcolor: Option[Color], - tickwidth: Option[Int], - tickangle: Option[Double], - dtick: Option[Double], - ticklen: Option[Int], - tickfont: Option[Font], - zeroline: Option[Boolean], - zerolinewidth: Option[Double], - zerolinecolor: Option[Color], - range: Option[(Double, Double)], - autorange: Option[Boolean], - ticks: Option[Ticks], - domain: Option[(Double, Double)], - side: Option[Side], - anchor: Option[AxisAnchor], - `type`: Option[AxisType], - overlaying: Option[AxisAnchor], - position: Option[Double] +@data(optionSetters = true) class Axis( + title: Option[String] = None, + titlefont: Option[Font] = None, + showgrid: Option[Boolean] = None, + gridwidth: Option[Int] = None, + gridcolor: Option[Color] = None, + showline: Option[Boolean] = None, + showticklabels: Option[Boolean] = None, + linecolor: Option[Color] = None, + linewidth: Option[Int] = None, + autotick: Option[Boolean] = None, + tickcolor: Option[Color] = None, + tickwidth: Option[Int] = None, + tickangle: Option[Double] = None, + dtick: Option[Double] = None, + ticklen: Option[Int] = None, + tickfont: Option[Font] = None, + tickprefix: Option[String] = None, + ticksuffix: Option[String] = None, + zeroline: Option[Boolean] = None, + zerolinewidth: Option[Double] = None, + zerolinecolor: Option[Color] = None, + range: Option[Range] = None, + autorange: Option[Boolean] = None, + ticks: Option[Ticks] = None, + domain: Option[Range] = None, + side: Option[Side] = None, + anchor: Option[AxisAnchor] = None, + `type`: Option[AxisType] = None, + overlaying: Option[AxisAnchor] = None, + position: Option[Double] = None, + tickmode: Option[TickMode] = None, + tickvals: Option[Sequence] = None, + ticktext: Option[Sequence] = None, + nticks: Option[Int] = None, + automargin: Option[Boolean] = None, + @since("0.8.0") + rangeslider: Option[RangeSlider] = None, + @since("0.8.2") + width: Option[Int] = None, + height: Option[Int] = None, + autosize: Option[Boolean] = None, + @since("0.8.5") + tickformat: Option[String] = None, + fixedrange: Option[Boolean] = None ) object Axis { + @deprecated("Use Axis() and chain-call .with* methods on it instead", "0.8.0") def apply( - title: String = null, - titlefont: Font = null, - showgrid: JBoolean = null, - gridwidth: JInt = null, - gridcolor: Color = null, - showline: JBoolean = null, - showticklabels: JBoolean = null, - linecolor: Color = null, - linewidth: JInt = null, - autotick: JBoolean = null, - tickcolor: Color = null, - tickwidth: JInt = null, - tickangle: JDouble = null, - dtick: JDouble = null, - ticklen: JInt = null, - tickfont: Font = null, - zeroline: JBoolean = null, - zerolinewidth: JDouble = null, - zerolinecolor: Color = null, - range: (Double, Double) = null, - autorange: JBoolean = null, - ticks: Ticks = null, - domain: (Double, Double) = null, - side: Side = null, - anchor: AxisAnchor = null, - `type`: AxisType = null, - overlaying: AxisAnchor = null, - position: JDouble = null + title: String = null, + titlefont: Font = null, + showgrid: JBoolean = null, + gridwidth: JInt = null, + gridcolor: Color = null, + showline: JBoolean = null, + showticklabels: JBoolean = null, + linecolor: Color = null, + linewidth: JInt = null, + autotick: JBoolean = null, + tickcolor: Color = null, + tickwidth: JInt = null, + tickangle: JDouble = null, + dtick: JDouble = null, + ticklen: JInt = null, + tickfont: Font = null, + tickprefix: String = null, + ticksuffix: String = null, + zeroline: JBoolean = null, + zerolinewidth: JDouble = null, + zerolinecolor: Color = null, + range: (Double, Double) = null, + autorange: JBoolean = null, + ticks: Ticks = null, + domain: (Double, Double) = null, + side: Side = null, + anchor: AxisAnchor = null, + `type`: AxisType = null, + overlaying: AxisAnchor = null, + position: JDouble = null, + tickmode: TickMode = null, + tickvals: Sequence = null, + ticktext: Sequence = null, + nticks: JInt = null, + automargin: JBoolean = null ): Axis = Axis( Option(title), Option(titlefont), - Option(showgrid) .map(x => x: Boolean), - Option(gridwidth) .map(x => x: Int), + Option(showgrid).map(x => x: Boolean), + Option(gridwidth).map(x => x: Int), Option(gridcolor), - Option(showline) .map(x => x: Boolean), - Option(showticklabels) .map(x => x: Boolean), + Option(showline).map(x => x: Boolean), + Option(showticklabels).map(x => x: Boolean), Option(linecolor), - Option(linewidth) .map(x => x: Int), - Option(autotick) .map(x => x: Boolean), + Option(linewidth).map(x => x: Int), + Option(autotick).map(x => x: Boolean), Option(tickcolor), - Option(tickwidth) .map(x => x: Int), - Option(tickangle) .map(x => x: Double), - Option(dtick) .map(x => x: Double), - Option(ticklen) .map(x => x: Int), + Option(tickwidth).map(x => x: Int), + Option(tickangle).map(x => x: Double), + Option(dtick).map(x => x: Double), + Option(ticklen).map(x => x: Int), Option(tickfont), - Option(zeroline) .map(x => x: Boolean), - Option(zerolinewidth) .map(x => x: Double), + Option(tickprefix), + Option(ticksuffix), + Option(zeroline).map(x => x: Boolean), + Option(zerolinewidth).map(x => x: Double), Option(zerolinecolor), - Option(range), - Option(autorange) .map(x => x: Boolean), + Option(range).map(x => x: Range), + Option(autorange).map(x => x: Boolean), Option(ticks), - Option(domain), + Option(domain).map(x => x: Range), Option(side), Option(anchor), Option(`type`), Option(overlaying), - Option(position) .map(x => x: Double) + Option(position).map(x => x: Double), + Option(tickmode), + Option(tickvals), + Option(ticktext), + Option(nticks).map(x => x: Int), + Option(automargin).map(x => x: Boolean) ) } diff --git a/core/shared/src/main/scala/plotly/layout/BarMode.scala b/core/shared/src/main/scala/plotly/layout/BarMode.scala index 202a7e37..1d58c827 100644 --- a/core/shared/src/main/scala/plotly/layout/BarMode.scala +++ b/core/shared/src/main/scala/plotly/layout/BarMode.scala @@ -4,7 +4,8 @@ package layout sealed abstract class BarMode(val label: String) extends Product with Serializable object BarMode { - case object Group extends BarMode("group") - case object Stack extends BarMode("stack") - case object Overlay extends BarMode("overlay") -} \ No newline at end of file + case object Group extends BarMode("group") + case object Stack extends BarMode("stack") + case object Overlay extends BarMode("overlay") + case object Relative extends BarMode("relative") +} diff --git a/core/shared/src/main/scala/plotly/layout/Font.scala b/core/shared/src/main/scala/plotly/layout/Font.scala index a632df2f..30789cd0 100644 --- a/core/shared/src/main/scala/plotly/layout/Font.scala +++ b/core/shared/src/main/scala/plotly/layout/Font.scala @@ -1,21 +1,36 @@ package plotly package layout -import plotly.element._ +import java.lang.{Integer => JInt} -import java.lang.{ Integer => JInt } +import dataclass.data +import plotly.element._ -final case class Font( - size: Option[Int], - family: Option[String], - color: Option[Color] +@data(optionSetters = true) class Font( + size: Option[Int] = None, + family: Option[String] = None, + color: Option[Color] = None ) object Font { + + def apply(size: Int, family: String, color: Color): Font = + Font(Some(size), Some(family), Some(color)) + + def apply(size: Int, family: String): Font = + Font(Some(size), Some(family), None) + + def apply(size: Int): Font = + Font(Some(size), None, None) + + def apply(color: Color): Font = + Font(None, None, Some(color)) + + @deprecated("Use Font() and chain-call .with* methods on it instead", "0.8.0") def apply( - size: JInt = null, - family: String = null, - color: Color = null + size: JInt = null, + family: String = null, + color: Color = null ): Font = Font( Option(size).map(x => x: Int), diff --git a/core/shared/src/main/scala/plotly/layout/Grid.scala b/core/shared/src/main/scala/plotly/layout/Grid.scala new file mode 100644 index 00000000..67c892b6 --- /dev/null +++ b/core/shared/src/main/scala/plotly/layout/Grid.scala @@ -0,0 +1,11 @@ +package plotly.layout + +import dataclass.data + +@data(optionSetters = true) class Grid( + rows: Option[Int] = None, + columns: Option[Int] = None, + pattern: Option[Pattern] = None, + roworder: Option[RowOrder] = None, + subplots: Option[Seq[Seq[String]]] = None +) diff --git a/core/shared/src/main/scala/plotly/layout/Layout.scala b/core/shared/src/main/scala/plotly/layout/Layout.scala index 5839c1f9..4dbcf707 100644 --- a/core/shared/src/main/scala/plotly/layout/Layout.scala +++ b/core/shared/src/main/scala/plotly/layout/Layout.scala @@ -1,67 +1,76 @@ package plotly package layout -import java.lang.{ Integer => JInt, Double => JDouble, Boolean => JBoolean } +import java.lang.{Boolean => JBoolean, Double => JDouble, Integer => JInt} +import dataclass.data import plotly.element._ -final case class Layout( - title: Option[String], - legend: Option[Legend], - width: Option[Int], - height: Option[Int], - showlegend: Option[Boolean], - xaxis: Option[Axis], - yaxis: Option[Axis], - xaxis1: Option[Axis], - xaxis2: Option[Axis], - xaxis3: Option[Axis], - xaxis4: Option[Axis], - yaxis1: Option[Axis], - yaxis2: Option[Axis], - yaxis3: Option[Axis], - yaxis4: Option[Axis], - barmode: Option[BarMode], - autosize: Option[Boolean], - margin: Option[Margin], - annotations: Option[Seq[Annotation]], - plot_bgcolor: Option[Color], - paper_bgcolor: Option[Color], - font: Option[Font], - bargap: Option[Double], - bargroupgap: Option[Double], - hovermode: Option[HoverMode], - boxmode: Option[BoxMode] +@data(optionSetters = true) class Layout( + title: Option[String] = None, + legend: Option[Legend] = None, + width: Option[Int] = None, + height: Option[Int] = None, + showlegend: Option[Boolean] = None, + xaxis: Option[Axis] = None, + yaxis: Option[Axis] = None, + xaxis1: Option[Axis] = None, + xaxis2: Option[Axis] = None, + xaxis3: Option[Axis] = None, + xaxis4: Option[Axis] = None, + yaxis1: Option[Axis] = None, + yaxis2: Option[Axis] = None, + yaxis3: Option[Axis] = None, + yaxis4: Option[Axis] = None, + barmode: Option[BarMode] = None, + autosize: Option[Boolean] = None, + margin: Option[Margin] = None, + annotations: Option[Seq[Annotation]] = None, + plot_bgcolor: Option[Color] = None, + paper_bgcolor: Option[Color] = None, + font: Option[Font] = None, + bargap: Option[Double] = None, + bargroupgap: Option[Double] = None, + hovermode: Option[HoverMode] = None, + boxmode: Option[BoxMode] = None, + scene: Option[Scene] = None, + @since("0.8.0") + dragmode: Option[String] = None, + shapes: Option[Seq[Shape]] = None, + @since("0.8.2") + grid: Option[Grid] = None ) object Layout { + @deprecated("Use Layout() and chain-call .with* methods on it instead", "0.8.0") def apply( - title: String = null, - legend: Legend = null, - width: JInt = null, - height: JInt = null, - showlegend: JBoolean = null, - xaxis: Axis = null, - yaxis: Axis = null, - xaxis1: Axis = null, - xaxis2: Axis = null, - xaxis3: Axis = null, - xaxis4: Axis = null, - yaxis1: Axis = null, - yaxis2: Axis = null, - yaxis3: Axis = null, - yaxis4: Axis = null, - barmode: BarMode = null, - autosize: JBoolean = null, - margin: Margin = null, + title: String = null, + legend: Legend = null, + width: JInt = null, + height: JInt = null, + showlegend: JBoolean = null, + xaxis: Axis = null, + yaxis: Axis = null, + xaxis1: Axis = null, + xaxis2: Axis = null, + xaxis3: Axis = null, + xaxis4: Axis = null, + yaxis1: Axis = null, + yaxis2: Axis = null, + yaxis3: Axis = null, + yaxis4: Axis = null, + barmode: BarMode = null, + autosize: JBoolean = null, + margin: Margin = null, annotations: Seq[Annotation] = null, - plot_bgcolor: Color = null, - paper_bgcolor: Color = null, - font: Font = null, - bargap: JDouble = null, - bargroupgap: JDouble = null, - hovermode: HoverMode = null, - boxmode: BoxMode = null + plot_bgcolor: Color = null, + paper_bgcolor: Color = null, + font: Font = null, + bargap: JDouble = null, + bargroupgap: JDouble = null, + hovermode: HoverMode = null, + boxmode: BoxMode = null, + scene: Scene = null ): Layout = new Layout( Option(title), @@ -89,6 +98,7 @@ object Layout { Option(bargap).map(x => x), Option(bargroupgap).map(x => x), Option(hovermode), - Option(boxmode) + Option(boxmode), + Option(scene) ) } diff --git a/core/shared/src/main/scala/plotly/layout/Legend.scala b/core/shared/src/main/scala/plotly/layout/Legend.scala index 2db2163a..daac773f 100644 --- a/core/shared/src/main/scala/plotly/layout/Legend.scala +++ b/core/shared/src/main/scala/plotly/layout/Legend.scala @@ -1,33 +1,38 @@ package plotly package layout -import java.lang.{ Double => JDouble } +import java.lang.{Double => JDouble} +import dataclass._ import plotly.element._ -final case class Legend( - x: Option[Double], - y: Option[Double], - traceorder: Option[TraceOrder], - yref: Option[Ref], - font: Option[Font], - bordercolor: Option[Color], - bgcolor: Option[Color], - xanchor: Option[Anchor], - yanchor: Option[Anchor] +@data(optionSetters = true) class Legend( + x: Option[Double] = None, + y: Option[Double] = None, + traceorder: Option[TraceOrder] = None, + yref: Option[Ref] = None, + font: Option[Font] = None, + bordercolor: Option[Color] = None, + bgcolor: Option[Color] = None, + xanchor: Option[Anchor] = None, + yanchor: Option[Anchor] = None, + @since + orientation: Option[Orientation] = None ) object Legend { + @deprecated("Use Legend() and chain-call .with* methods on it instead", "0.8.0") def apply( - x: JDouble = null, - y: JDouble = null, - traceorder: TraceOrder = null, - yref: Ref = null, - font: Font = null, - bordercolor: Color = null, - bgcolor: Color = null, - xanchor: Anchor = null, - yanchor: Anchor = null + x: JDouble = null, + y: JDouble = null, + traceorder: TraceOrder = null, + yref: Ref = null, + font: Font = null, + bordercolor: Color = null, + bgcolor: Color = null, + xanchor: Anchor = null, + yanchor: Anchor = null, + orientation: Orientation = null ): Legend = Legend( Option(x).map(v => v: Double), @@ -38,6 +43,7 @@ object Legend { Option(bordercolor), Option(bgcolor), Option(xanchor), - Option(yanchor) + Option(yanchor), + Option(orientation) ) -} \ No newline at end of file +} diff --git a/core/shared/src/main/scala/plotly/layout/Margin.scala b/core/shared/src/main/scala/plotly/layout/Margin.scala index 76730d51..386e4aa3 100644 --- a/core/shared/src/main/scala/plotly/layout/Margin.scala +++ b/core/shared/src/main/scala/plotly/layout/Margin.scala @@ -1,29 +1,37 @@ package plotly package layout -import java.lang.{ Integer => JInt, Boolean => JBoolean } +import java.lang.{Boolean => JBoolean, Integer => JInt} +import dataclass.data -final case class Margin( - autoexpand: Option[Boolean], - l: Option[Int], - r: Option[Int], - t: Option[Int], - b: Option[Int] +@data(optionSetters = true) class Margin( + autoexpand: Option[Boolean] = None, + l: Option[Int] = None, + r: Option[Int] = None, + t: Option[Int] = None, + b: Option[Int] = None, + pad: Option[Int] = None ) object Margin { + def apply(l: Int, r: Int, t: Int, b: Int): Margin = + Margin().withL(l).withR(r).withT(t).withB(b) + + @deprecated("Use Margin() and chain-call .with* methods on it instead", "0.8.0") def apply( - autoexpand: JBoolean = null, - l: JInt = null, - r: JInt = null, - t: JInt = null, - b: JInt = null + autoexpand: JBoolean = null, + l: JInt = null, + r: JInt = null, + t: JInt = null, + b: JInt = null, + pad: JInt = null ): Margin = Margin( Option(autoexpand).map(b => b: Boolean), Option(l).map(n => n: Int), Option(r).map(n => n: Int), Option(t).map(n => n: Int), - Option(b).map(n => n: Int) + Option(b).map(n => n: Int), + Option(pad).map(n => n: Int) ) } diff --git a/core/shared/src/main/scala/plotly/layout/Pattern.scala b/core/shared/src/main/scala/plotly/layout/Pattern.scala new file mode 100644 index 00000000..f778371a --- /dev/null +++ b/core/shared/src/main/scala/plotly/layout/Pattern.scala @@ -0,0 +1,8 @@ +package plotly.layout + +sealed abstract class Pattern(val label: String) extends Product with Serializable + +object Pattern { + case object Independent extends Pattern("independent") + case object Coupled extends Pattern("coupled") +} diff --git a/core/shared/src/main/scala/plotly/layout/RangeSlider.scala b/core/shared/src/main/scala/plotly/layout/RangeSlider.scala new file mode 100644 index 00000000..db3af2c9 --- /dev/null +++ b/core/shared/src/main/scala/plotly/layout/RangeSlider.scala @@ -0,0 +1,8 @@ +package plotly.layout + +import dataclass.data +import plotly.Range + +@data(optionSetters = true) class RangeSlider( + range: Option[Range] = None +) diff --git a/core/shared/src/main/scala/plotly/layout/Ref.scala b/core/shared/src/main/scala/plotly/layout/Ref.scala index bbb30ebf..382dbd9d 100644 --- a/core/shared/src/main/scala/plotly/layout/Ref.scala +++ b/core/shared/src/main/scala/plotly/layout/Ref.scala @@ -6,6 +6,6 @@ import plotly.element._ sealed abstract class Ref(val label: String) extends Product with Serializable object Ref { - case object Paper extends Ref("paper") + case object Paper extends Ref("paper") case class Axis(underlying: AxisReference) extends Ref(underlying.label) } diff --git a/core/shared/src/main/scala/plotly/layout/RowOrder.scala b/core/shared/src/main/scala/plotly/layout/RowOrder.scala new file mode 100644 index 00000000..9a3b5b1d --- /dev/null +++ b/core/shared/src/main/scala/plotly/layout/RowOrder.scala @@ -0,0 +1,8 @@ +package plotly.layout + +sealed abstract class RowOrder(val label: String) extends Product with Serializable + +object RowOrder { + case object TopToBottom extends RowOrder("top to bottom") + case object BottomToTop extends RowOrder("bottom to top") +} diff --git a/core/shared/src/main/scala/plotly/layout/Scene.scala b/core/shared/src/main/scala/plotly/layout/Scene.scala new file mode 100644 index 00000000..52ab727d --- /dev/null +++ b/core/shared/src/main/scala/plotly/layout/Scene.scala @@ -0,0 +1,26 @@ +package plotly +package layout + +import java.lang.{Boolean => JBoolean, Double => JDouble, Integer => JInt} + +import dataclass.data +import plotly.element._ + +@data(optionSetters = true) class Scene( + xaxis: Option[Axis] = None, + yaxis: Option[Axis] = None, + zaxis: Option[Axis] = None +) + +object Scene { + @deprecated("Use Scene() and chain-call .with* methods on it instead", "0.8.0") + def apply( + xaxis: Axis = null, + yaxis: Axis = null, + zaxis: Axis = null + ): Scene = new Scene( + Option(xaxis), + Option(yaxis), + Option(zaxis) + ) +} diff --git a/core/shared/src/main/scala/plotly/layout/Shape.scala b/core/shared/src/main/scala/plotly/layout/Shape.scala new file mode 100644 index 00000000..c9b4bd2b --- /dev/null +++ b/core/shared/src/main/scala/plotly/layout/Shape.scala @@ -0,0 +1,17 @@ +package plotly.layout + +import dataclass.data +import plotly.element.{Color, Line} + +@data(optionSetters = true) class Shape( + `type`: Option[String] = None, + xref: Option[String] = None, + yref: Option[String] = None, + x0: Option[String] = None, + y0: Option[Double] = None, + x1: Option[String] = None, + y1: Option[Double] = None, + fillcolor: Option[Color] = None, + opacity: Option[Double] = None, + line: Option[Line] = None +) diff --git a/demo/src/main/resources/index.html b/demo/src/main/resources/index.html index 47aaa5ca..1f7b03d9 100644 --- a/demo/src/main/resources/index.html +++ b/demo/src/main/resources/index.html @@ -32,7 +32,7 @@ diff --git a/demo/src/main/scala/plotly/demo/Demo.scala b/demo/src/main/scala/plotly/demo/Demo.scala old mode 100644 new mode 100755 index c2da9e63..dbca5966 --- a/demo/src/main/scala/plotly/demo/Demo.scala +++ b/demo/src/main/scala/plotly/demo/Demo.scala @@ -1,14 +1,14 @@ package plotly.demo -import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel} import plotly.Plotly import org.scalajs.dom -import scalatags.JsDom.all._ +import scalatags.JsDom.all.{area => _, _} -@JSExport object Demo { +@JSExportTopLevel("Demo") object Demo { val demos = Seq( "Line Charts" -> Seq( @@ -37,20 +37,31 @@ import scalatags.JsDom.all._ ), "Filled Area Plots" -> Seq( area.BasicOverlaidAreaChart + ), + "Heatmaps" -> Seq( + heatmaps.BasicHeatmap, + heatmaps.CategoricalAxisHeatmap, + heatmaps.CustomColorScaleHeatmap, + heatmaps.AnnotatedHeatmap + ), + "Histogram" -> Seq( + histogram.BasicHistogram, + histogram.StyledBasicHistogram ) ) def unindent(source: String): String = { - val lines = source.lines.toVector - val nonEmptyLines = lines.filter(_.exists(!_.isSpaceChar)) + val lines = source.linesIterator.toVector + val nonEmptyLines: Vector[String] = lines.filter(_.exists(!_.isSpaceChar)) if (nonEmptyLines.isEmpty) source else { - val dropCount = Stream.from(0) - .takeWhile(idx => nonEmptyLines.forall(_(idx) == nonEmptyLines.head(idx))) + val dropCount = LazyList + .from(0) + .takeWhile(idx => nonEmptyLines.forall(str => str(idx) == nonEmptyLines.head(idx))) .lastOption .fold(0)(_ + 1) @@ -76,10 +87,9 @@ import scalatags.JsDom.all._ h2("Examples"), div( style := "margin-left: 3em;", - demos.map { - case (chartType, chartDemos) => - val chartTypeId0 = chartTypeId(chartType) - a(href := "#" + chartTypeId0, h3(chartType)) + demos.map { case (chartType, chartDemos) => + val chartTypeId0 = chartTypeId(chartType) + a(href := "#" + chartTypeId0, h3(chartType)) } ) ) @@ -91,37 +101,34 @@ import scalatags.JsDom.all._ val chartTypeId0 = chartTypeId(chartType) - val chartTypeElem = h2(id := chartTypeId0, - a(href := "#" + chartTypeId0, - chartType - ) - ) + val chartTypeElem = h2(id := chartTypeId0, a(href := "#" + chartTypeId0, chartType)) mainDiv.appendChild(chartTypeElem.render) for (demo <- chartDemos) { - Console.println(s"Rendering demo ${demo.id}") + Console.println(s" Rendering demo ${demo.id}") val divId = s"demo-${demo.id}" val elem = - div(id := demo.id, `class` := "panel panel-default", - div(`class` := "panel-heading", - a(href := "#" + demo.id, - h4(demo.id) - ) - ), - div(`class` := "panel-body", - div(`class` := "example-code", + div( + id := demo.id, + `class` := "panel panel-default", + div(`class` := "panel-heading", a(href := "#" + demo.id, h4(demo.id))), + div( + `class` := "panel-body", + div( + `class` := "example-code", pre( - code(`class` := "language-scala", - s"""import plotly._ - |import plotly.element._${if (demo.layout == null) "" else "\nimport plotly.layout._"} + code( + `class` := "language-scala", + s"""import plotly._ + |import plotly.element._${if (demo.layout == null) "" else "\nimport plotly.layout._"} """.stripMargin + - unindent(demo.source) + - s""" - | - |Plotly.plot("div-id", data${if (demo.layout == null) "" else ", layout"})""".stripMargin + unindent(demo.source) + + s""" + | + |Plotly.plot("div-id", data${if (demo.layout == null) "" else ", layout"})""".stripMargin ) ) ), diff --git a/demo/src/main/scala/plotly/demo/area/BasicOverlaidAreaChart.scala b/demo/src/main/scala/plotly/demo/area/BasicOverlaidAreaChart.scala index c4fa3adc..b458edca 100644 --- a/demo/src/main/scala/plotly/demo/area/BasicOverlaidAreaChart.scala +++ b/demo/src/main/scala/plotly/demo/area/BasicOverlaidAreaChart.scala @@ -7,22 +7,16 @@ import plotly.element.Fill object BasicOverlaidAreaChart extends NoLayoutDemoChart { def plotlyDocUrl = "https://plot.ly/javascript/filled-area-plots/#basic-overlaid-area-chart" - def id = "basic-overlaid-area-chart" - def source = BasicOverlaidAreaChartSource.source + def id = "basic-overlaid-area-chart" + def source = BasicOverlaidAreaChartSource.source // demo source start - - val trace1 = Scatter( - Seq(1, 2, 3, 4), - Seq(0, 2, 3, 5), - fill = Fill.ToZeroY - ) - - val trace2 = Scatter( - Seq(1, 2, 3, 4), - Seq(3, 5, 1, 7), - fill = Fill.ToNextY - ) + + val trace1 = Scatter(Seq(1, 2, 3, 4), Seq(0, 2, 3, 5)) + .withFill(Fill.ToZeroY) + + val trace2 = Scatter(Seq(1, 2, 3, 4), Seq(3, 5, 1, 7)) + .withFill(Fill.ToNextY) val data = Seq(trace1, trace2) diff --git a/demo/src/main/scala/plotly/demo/bar/BarChartWithDirectLabels.scala b/demo/src/main/scala/plotly/demo/bar/BarChartWithDirectLabels.scala index a68ac204..c80a38ea 100644 --- a/demo/src/main/scala/plotly/demo/bar/BarChartWithDirectLabels.scala +++ b/demo/src/main/scala/plotly/demo/bar/BarChartWithDirectLabels.scala @@ -8,8 +8,8 @@ import plotly.layout._ object BarChartWithDirectLabels extends DemoChart { def plotlyDocUrl = "https://plot.ly/javascript/bar-charts/#bar-chart-with-direct-labels" - def id = "bar-chart-with-direct-labels" - def source = BarChartWithDirectLabelsSource.source + def id = "bar-chart-with-direct-labels" + def source = BarChartWithDirectLabelsSource.source // demo source start @@ -17,38 +17,34 @@ object BarChartWithDirectLabels extends DemoChart { val yValue = Seq(20, 14, 23) - val trace1 = Bar( - xValue, - yValue, - text = Seq("27% market share", "24% market share", "19% market share"), - marker = Marker( - color = Color.RGB(158, 202, 225), - opacity = 0.6, - line = Line( - color = Color.RGB(8, 48, 107), - width = 1.5 - ) + val trace1 = Bar(xValue, yValue) + .withText(Seq("27% market share", "24% market share", "19% market share")) + .withMarker( + Marker() + .withColor(Color.RGB(158, 202, 225)) + .withOpacity(0.6) + .withLine( + Line() + .withColor(Color.RGB(8, 48, 107)) + .withWidth(1.5) + ) ) - ) val data = Seq(trace1) - val annotations = xValue.zip(yValue).map { - case (x, y) => - Annotation( - x = x, - y = y, - text = y.toString, - xanchor = Anchor.Center, - yanchor = Anchor.Bottom, - showarrow = false - ) + val annotations = xValue.zip(yValue).map { case (x, y) => + Annotation() + .withX(x) + .withY(y) + .withText(y.toString) + .withXanchor(Anchor.Center) + .withYanchor(Anchor.Bottom) + .withShowarrow(false) } - val layout = Layout( - title = "January 2013 Sales Report", - annotations = annotations - ) + val layout = Layout() + .withTitle("January 2013 Sales Report") + .withAnnotations(annotations) // demo source end diff --git a/demo/src/main/scala/plotly/demo/bar/BasicBarChart.scala b/demo/src/main/scala/plotly/demo/bar/BasicBarChart.scala index 1d1e29a7..99b69eaf 100644 --- a/demo/src/main/scala/plotly/demo/bar/BasicBarChart.scala +++ b/demo/src/main/scala/plotly/demo/bar/BasicBarChart.scala @@ -7,8 +7,8 @@ import plotly.element._ object BasicBarChart extends NoLayoutDemoChart { def plotlyDocUrl = "https://plot.ly/javascript/bar-charts/#basic-bar-chart" - def id = "basic-bar-chart" - def source = BasicBarChartSource.source + def id = "basic-bar-chart" + def source = BasicBarChartSource.source // demo source start diff --git a/demo/src/main/scala/plotly/demo/bar/CustomizingIndividualBarColors.scala b/demo/src/main/scala/plotly/demo/bar/CustomizingIndividualBarColors.scala index d33bc196..c4c61487 100644 --- a/demo/src/main/scala/plotly/demo/bar/CustomizingIndividualBarColors.scala +++ b/demo/src/main/scala/plotly/demo/bar/CustomizingIndividualBarColors.scala @@ -8,29 +8,35 @@ import plotly.layout._ object CustomizingIndividualBarColors extends DemoChart { def plotlyDocUrl = "" - def id = "customizing-individual-bar-colors" - def source = CustomizingIndividualBarColorsSource.source + def id = "customizing-individual-bar-colors" + def source = CustomizingIndividualBarColorsSource.source // demo source start - val defaultColor = Color.RGBA(204,204,204,1) - val highlightColor = Color.RGBA(222,45,38,0.8) + val defaultColor = Color.RGBA(204, 204, 204, 1) + val highlightColor = Color.RGBA(222, 45, 38, 0.8) val trace1 = Bar( Seq("Feature A", "Feature B", "Feature C", "Feature D", "Feature E"), - Seq(20, 14, 23, 25, 22), - marker = Marker( - color = Seq( - defaultColor, highlightColor, defaultColor, defaultColor, defaultColor - ) - ) + Seq(20, 14, 23, 25, 22) ) + .withMarker( + Marker() + .withColor( + Seq( + defaultColor, + highlightColor, + defaultColor, + defaultColor, + defaultColor + ) + ) + ) val data = Seq(trace1) - val layout = Layout( - title = "Least Used Feature" - ) + val layout = Layout() + .withTitle("Least Used Feature") // demo source end diff --git a/demo/src/main/scala/plotly/demo/bar/GroupedBarChart.scala b/demo/src/main/scala/plotly/demo/bar/GroupedBarChart.scala index d244d192..dca36376 100644 --- a/demo/src/main/scala/plotly/demo/bar/GroupedBarChart.scala +++ b/demo/src/main/scala/plotly/demo/bar/GroupedBarChart.scala @@ -8,28 +8,21 @@ import plotly.layout._ object GroupedBarChart extends DemoChart { def plotlyDocUrl = "https://plot.ly/javascript/bar-charts/#grouped-bar-chart" - def id = "grouped-bar-chart" - def source = GroupedBarChartSource.source + def id = "grouped-bar-chart" + def source = GroupedBarChartSource.source // demo source start - val trace1 = Bar( - Seq("giraffes", "orangutans", "monkeys"), - Seq(20, 14, 23), - name = "SF Zoo" - ) + val trace1 = Bar(Seq("giraffes", "orangutans", "monkeys"), Seq(20, 14, 23)) + .withName("SF Zoo") - val trace2 = Bar( - Seq("giraffes", "orangutans", "monkeys"), - Seq(12, 18, 29), - name = "LA Zoo" - ) + val trace2 = Bar(Seq("giraffes", "orangutans", "monkeys"), Seq(12, 18, 29)) + .withName("LA Zoo") val data = Seq(trace1, trace2) - val layout = Layout( - barmode = BarMode.Group - ) + val layout = Layout() + .withBarmode(BarMode.Group) // demo source end diff --git a/demo/src/main/scala/plotly/demo/bar/WaterfallBarChart.scala b/demo/src/main/scala/plotly/demo/bar/WaterfallBarChart.scala index f52d96b3..27c8c69f 100644 --- a/demo/src/main/scala/plotly/demo/bar/WaterfallBarChart.scala +++ b/demo/src/main/scala/plotly/demo/bar/WaterfallBarChart.scala @@ -8,8 +8,8 @@ import plotly.layout.{Annotation, BarMode, Font, Layout} object WaterfallBarChart extends DemoChart { def plotlyDocUrl = "https://plot.ly/javascript/bar-charts/#waterfall-bar-chart" - def id = "waterfall-bar-chart" - def source = WaterfallBarChartSource.source + def id = "waterfall-bar-chart" + def source = WaterfallBarChartSource.source // demo source start @@ -27,85 +27,79 @@ object WaterfallBarChart extends DemoChart { val textList = Seq("$430K", "$260K", "$690K", "$-120K", "$-200K", "$-320K", "$370K") - //Base + // Base - val trace1 = Bar( - x = xData, - y = Seq(0, 430, 0, 570, 370, 370, 0), - marker = Marker( - color = Color.RGBA(1, 1, 1, 0.0) + val trace1 = Bar(xData, Seq(0, 430, 0, 570, 370, 370, 0)) + .withMarker( + Marker() + .withColor(Color.RGBA(1, 1, 1, 0.0)) ) - ) - - //Revenue - val trace2 = Bar( - xData, - Seq(430, 260, 690, 0, 0, 0, 0), - marker = Marker( - color = Color.RGBA(55, 128, 191, 0.7), - line = Line( - color = Color.RGBA(55, 128, 191, 1.0), - width = 2.0 - ) + // Revenue + + val trace2 = Bar(xData, Seq(430, 260, 690, 0, 0, 0, 0)) + .withMarker( + Marker() + .withColor(Color.RGBA(55, 128, 191, 0.7)) + .withLine( + Line() + .withColor(Color.RGBA(55, 128, 191, 1.0)) + .withWidth(2.0) + ) ) - ) - - //Cost - val trace3 = Bar( - xData, - Seq(0, 0, 0, 120, 200, 320, 0), - marker = Marker( - color = Color.RGBA(219, 64, 82, 0.7), - line = Line( - color = Color.RGBA(219, 64, 82, 1.0), - width = 2.0 - ) + // Cost + + val trace3 = Bar(xData, Seq(0, 0, 0, 120, 200, 320, 0)) + .withMarker( + Marker() + .withColor(Color.RGBA(219, 64, 82, 0.7)) + .withLine( + Line() + .withColor(Color.RGBA(219, 64, 82, 1.0)) + .withWidth(2.0) + ) ) - ) - - //Profit - val trace4 = Bar( - xData, - Seq(0, 0, 0, 0, 0, 0, 370), - marker = Marker( - color = Color.RGBA(50,171, 96, 0.7), - line = Line( - color = Color.RGBA(50, 171, 96, 1.0), - width = 2.0 - ) + // Profit + + val trace4 = Bar(xData, Seq(0, 0, 0, 0, 0, 0, 370)) + .withMarker( + Marker() + .withColor(Color.RGBA(50, 171, 96, 0.7)) + .withLine( + Line() + .withColor(Color.RGBA(50, 171, 96, 1.0)) + .withWidth(2.0) + ) ) - ) val data = Seq(trace1, trace2, trace3, trace4) - val annotations = xData.zip(yData).zip(textList).map { - case ((x, y), text) => - Annotation( - x = x, - y = y, - text = text, - font = Font( + val annotations = xData.zip(yData).zip(textList).map { case ((x, y), text) => + Annotation() + .withX(x) + .withY(y) + .withText(text) + .withFont( + Font( family = "Arial", size = 14, color = Color.RGBA(245, 246, 249, 1) - ), - showarrow = false + ) ) + .withShowarrow(false) } - val layout = Layout( - title = "Annual Profit 2015", - barmode = BarMode.Stack, - paper_bgcolor = Color.RGBA(245, 246, 249, 1), - plot_bgcolor = Color.RGBA(245, 246, 249, 1), - width = 600, - height = 400, - showlegend = false, - annotations = annotations - ) + val layout = Layout() + .withTitle("Annual Profit 2015") + .withBarmode(BarMode.Stack) + .withPaper_bgcolor(Color.RGBA(245, 246, 249, 1)) + .withPlot_bgcolor(Color.RGBA(245, 246, 249, 1)) + .withWidth(600) + .withHeight(400) + .withShowlegend(false) + .withAnnotations(annotations) // demo source end diff --git a/demo/src/main/scala/plotly/demo/bubblecharts/HoverOnTextBubbleChart.scala b/demo/src/main/scala/plotly/demo/bubblecharts/HoverOnTextBubbleChart.scala index d2feb562..b12e665c 100644 --- a/demo/src/main/scala/plotly/demo/bubblecharts/HoverOnTextBubbleChart.scala +++ b/demo/src/main/scala/plotly/demo/bubblecharts/HoverOnTextBubbleChart.scala @@ -8,35 +8,41 @@ import plotly.layout.Layout object HoverOnTextBubbleChart extends DemoChart { def plotlyDocUrl = "https://plot.ly/javascript/bubble-charts/#hover-text-on-bubble-charts" - def id = "hover-on-text-bubble-chart" - def source = HoverOnTextBubbleChartSource.source + def id = "hover-on-text-bubble-chart" + def source = HoverOnTextBubbleChartSource.source // demo source start - - val trace1 = Scatter( - Seq(1, 2, 3, 4), - Seq(10, 11, 12, 13), - text = Seq("""A - size = 40""", """B - size = 60""", """C - size = 80""", """D - size = 100"""), - mode = ScatterMode(ScatterMode.Markers), - marker = Marker( - color = Seq(Color.RGB(93, 164, 214), Color.RGB(255, 144, 14), Color.RGB(44, 160, 101), Color.RGB(255, 65, 54)), - size = Seq(40, 60, 80, 100) + + val trace1 = Scatter(Seq(1, 2, 3, 4), Seq(10, 11, 12, 13)) + .withText( + Seq( + """A + size = 40""", + """B + size = 60""", + """C + size = 80""", + """D + size = 100""" + ) + ) + .withMode(ScatterMode(ScatterMode.Markers)) + .withMarker( + Marker() + .withColor( + Seq(Color.RGB(93, 164, 214), Color.RGB(255, 144, 14), Color.RGB(44, 160, 101), Color.RGB(255, 65, 54)) + ) + .withSize(Seq(40, 60, 80, 100)) ) - ) val data = Seq(trace1) - val layout = Layout( - title = "Bubble Chart Hover Text", - showlegend = false, - height = 400, - width = 600 - ) - + val layout = Layout() + .withTitle("Bubble Chart Hover Text") + .withShowlegend(false) + .withHeight(400) + .withWidth(600) + // demo source end } diff --git a/demo/src/main/scala/plotly/demo/heatmaps/AnnotatedHeatmap.scala b/demo/src/main/scala/plotly/demo/heatmaps/AnnotatedHeatmap.scala new file mode 100755 index 00000000..c8cbb035 --- /dev/null +++ b/demo/src/main/scala/plotly/demo/heatmaps/AnnotatedHeatmap.scala @@ -0,0 +1,58 @@ +package plotly.demo.heatmaps + +import plotly._ +import plotly.demo.DemoChart +import plotly.element._ +import plotly.layout._ + +object AnnotatedHeatmap extends DemoChart { + + def plotlyDocUrl = "https://plot.ly/javascript/heatmaps/#annotated-heatmap" + def id = "annotated-heatmap" + def source = AnnotatedHeatmapSource.source + + // demo source start + + val x = Seq("A", "B", "C", "D", "E"); + val y = Seq("W", "X", "Y", "Z"); + val z = Seq( + Seq(0.00, 0.00, 0.75, 0.75, 0.00), + Seq(0.00, 0.00, 0.75, 0.75, 0.00), + Seq(0.75, 0.75, 0.75, 0.75, 0.75), + Seq(0.00, 0.00, 0.00, 0.75, 0.00) + ) + + val data = Seq( + Heatmap(z, x, y) + .withShowscale(false) + .withColorscale( + ColorScale.CustomScale( + Seq( + (0, Color.StringColor("#3D9970")), + (1, Color.StringColor("#001f3f")) + ) + ) + ) + ) + + val layout = Layout() + .withTitle("Annotated Heatmap") + .withXaxis(Axis().withTicks(Ticks.Empty).withSide(Side.Top)) + .withYaxis(Axis().withTicks(Ticks.Empty).withTicksuffix(" ")) + .withAnnotations( + for { + (xv, xi) <- x.zipWithIndex + (yv, yi) <- y.zipWithIndex + } yield Annotation() + .withX(xv) + .withY(yv) + .withXref(Ref.Axis(AxisReference.X1)) + .withYref(Ref.Axis(AxisReference.Y1)) + .withShowarrow(false) + .withText(z(yi)(xi).toString) + .withFont(Font(Color.StringColor("white"))) + ) + + // demo source end + +} diff --git a/demo/src/main/scala/plotly/demo/heatmaps/BasicHeatmap.scala b/demo/src/main/scala/plotly/demo/heatmaps/BasicHeatmap.scala new file mode 100755 index 00000000..1d6d61bd --- /dev/null +++ b/demo/src/main/scala/plotly/demo/heatmaps/BasicHeatmap.scala @@ -0,0 +1,29 @@ +package plotly.demo.heatmaps + +import plotly._ +import plotly.demo.NoLayoutDemoChart +import plotly.element._ + +object BasicHeatmap extends NoLayoutDemoChart { + + def plotlyDocUrl = "https://plot.ly/javascript/heatmaps/#basic-heatmap" + def id = "basic-heatmap" + def source = BasicHeatmapSource.source + + // demo source start + + val data = Seq( + Heatmap() + .withZ( + Seq( + Seq(1, 20, 30), + Seq(20, 1, 60), + Seq(30, 60, 1) + ) + ) + .withColorscale(ColorScale.NamedScale("Portland")) + ) + + // demo source end + +} diff --git a/demo/src/main/scala/plotly/demo/heatmaps/CategoricalAxisHeatmap.scala b/demo/src/main/scala/plotly/demo/heatmaps/CategoricalAxisHeatmap.scala new file mode 100755 index 00000000..240497b2 --- /dev/null +++ b/demo/src/main/scala/plotly/demo/heatmaps/CategoricalAxisHeatmap.scala @@ -0,0 +1,30 @@ +package plotly.demo.heatmaps + +import plotly._ +import plotly.demo.NoLayoutDemoChart +import plotly.element._ + +object CategoricalAxisHeatmap extends NoLayoutDemoChart { + + def plotlyDocUrl = "https://plot.ly/javascript/heatmaps/#heatmap-with-categorical-axis-labels" + def id = "categorical-axis-heatmap" + def source = CategoricalAxisHeatmapSource.source + + // demo source start + + val data = Seq( + Heatmap() + .withZ( + Seq( + Seq(1, null.asInstanceOf[Int], 30, 50, 1), + Seq(20, 1, 60, 80, 30), + Seq(30, 60, 1, -10, 20) + ) + ) + .withX(Seq("Monday", "Tuesday", "Wednesday", "Thursday", "Friday")) + .withY(Seq("Morning", "Afternoon", "Evening")) + ) + + // demo source end + +} diff --git a/demo/src/main/scala/plotly/demo/heatmaps/CustomColorScaleHeatmap.scala b/demo/src/main/scala/plotly/demo/heatmaps/CustomColorScaleHeatmap.scala new file mode 100755 index 00000000..5fa21f4e --- /dev/null +++ b/demo/src/main/scala/plotly/demo/heatmaps/CustomColorScaleHeatmap.scala @@ -0,0 +1,42 @@ +package plotly.demo.heatmaps + +import plotly._ +import plotly.demo.NoLayoutDemoChart +import plotly.element._ + +object CustomColorScaleHeatmap extends NoLayoutDemoChart { + + def plotlyDocUrl = "https://plot.ly/javascript/colorscales/#custom-colorscale-for-contour-plot" + def id = "custom-colorscale-heatmap" + def source = CustomColorScaleHeatmapSource.source + + // demo source start + + val data = Seq( + Heatmap() + .withZ( + Seq( + Seq(10.0, 10.625, 12.5, 15.625, 20.0), + Seq(5.625, 6.25, 8.125, 11.25, 15.625), + Seq(2.5, 3.125, 5.0, 8.125, 12.5), + Seq(0.625, 1.25, 3.125, 6.25, 10.625), + Seq(0.0, 0.625, 2.5, 5.625, 10.0) + ) + ) + .withColorscale( + ColorScale.CustomScale( + Seq( + (0, Color.RGB(166, 206, 227)), + (0.25, Color.RGB(31, 120, 180)), + (0.45, Color.RGB(178, 223, 138)), + (0.65, Color.RGB(51, 160, 44)), + (0.85, Color.RGB(251, 154, 153)), + (1, Color.RGB(227, 26, 28)) + ) + ) + ) + ) + + // demo source end + +} diff --git a/demo/src/main/scala/plotly/demo/histogram/BasicHistogram.scala b/demo/src/main/scala/plotly/demo/histogram/BasicHistogram.scala new file mode 100644 index 00000000..b6e9ba4d --- /dev/null +++ b/demo/src/main/scala/plotly/demo/histogram/BasicHistogram.scala @@ -0,0 +1,31 @@ +package plotly.demo.histogram + +import plotly.Histogram +import plotly.demo.DemoChart +import plotly.layout.Layout + +object BasicHistogram extends DemoChart { + + def plotlyDocUrl = "https://plotly.com/javascript/histograms/#basic-histogram" + def id = this.getClass.getSimpleName.dropRight(1) + def source = BasicHistogramSource.source + + // demo source start + + private val categoryCount = 50 + + private val indices = LazyList.from(0).take(categoryCount) + private val categories = indices.map(i => s"name-$i") + private val values = indices.map(_ => math.random()) + + val data = Seq(Histogram(values, categories)) + + val layout = new Layout() + .withTitle(id) + .withShowlegend(false) + .withHeight(400) + .withWidth(600) + + // demo source end + +} diff --git a/demo/src/main/scala/plotly/demo/histogram/StyledBasicHistogram.scala b/demo/src/main/scala/plotly/demo/histogram/StyledBasicHistogram.scala new file mode 100644 index 00000000..7dbd86fb --- /dev/null +++ b/demo/src/main/scala/plotly/demo/histogram/StyledBasicHistogram.scala @@ -0,0 +1,46 @@ +package plotly.demo.histogram + +import plotly.Histogram +import plotly.demo.DemoChart +import plotly.layout.{Axis, Layout, RangeSlider} +import plotly.element._ + +object StyledBasicHistogram extends DemoChart { + + def plotlyDocUrl = "https://plotly.com/javascript/histograms/#basic-histogram" + def id = this.getClass.getSimpleName.dropRight(1) + def source = StyledBasicHistogramSource.source + + // demo source start + + private val categoryCount = 50 + + private val indices = LazyList.from(0).take(categoryCount) + private val categories = indices.map(i => s"name-$i for $id") + private val values = indices.map(_ => math.random()) + + val data = Seq( + Histogram(values, categories) + .withMarker(new Marker().withColor(Color.StringColor("#004A72"))) + .withHovertext(categories.map(c => s"$c with hover text")) + ) + + val xAxis = new Axis() + .withRange((0d, 2d)) + .withRangeslider(RangeSlider()) + + val yAxis = new Axis() + .withTitle("Count") + .withFixedrange(true) + .withTickformat(".1f") + + val layout = new Layout() + .withXaxis(xAxis) + .withYaxis(yAxis) + .withTitle(id) + .withShowlegend(false) + .withHeight(400) + .withWidth(600) + + // demo source end +} diff --git a/demo/src/main/scala/plotly/demo/horizontalbarcharts/BasicHorizontalBarChart.scala b/demo/src/main/scala/plotly/demo/horizontalbarcharts/BasicHorizontalBarChart.scala index 0d66876d..a9d852ee 100644 --- a/demo/src/main/scala/plotly/demo/horizontalbarcharts/BasicHorizontalBarChart.scala +++ b/demo/src/main/scala/plotly/demo/horizontalbarcharts/BasicHorizontalBarChart.scala @@ -7,16 +7,15 @@ import plotly.element.Orientation object BasicHorizontalBarChart extends NoLayoutDemoChart { def plotlyDocUrl = "https://plot.ly/javascript/horizontal-bar-charts/#basic-horizontal-bar-chart" - def id = "basic-horizontal-bar-chart" - def source = BasicHorizontalBarChartSource.source + def id = "basic-horizontal-bar-chart" + def source = BasicHorizontalBarChartSource.source // demo source start - val data = Seq(Bar( - Seq(20, 14, 23), - Seq("giraffes", "orangutans", "monkeys"), - orientation = Orientation.Horizontal - )) + val data = Seq( + Bar(Seq(20, 14, 23), Seq("giraffes", "orangutans", "monkeys")) + .withOrientation(Orientation.Horizontal) + ) // demo source end diff --git a/demo/src/main/scala/plotly/demo/horizontalbarcharts/ColoredBarChart.scala b/demo/src/main/scala/plotly/demo/horizontalbarcharts/ColoredBarChart.scala index 914581c0..ad55631e 100644 --- a/demo/src/main/scala/plotly/demo/horizontalbarcharts/ColoredBarChart.scala +++ b/demo/src/main/scala/plotly/demo/horizontalbarcharts/ColoredBarChart.scala @@ -8,39 +8,34 @@ import plotly.layout.{BarMode, Layout} object ColoredBarChart extends DemoChart { def plotlyDocUrl = "https://plot.ly/javascript/horizontal-bar-charts/#colored-bar-chart" - def id = "colored-bar-chart" - def source = ColoredBarChartSource.source + def id = "colored-bar-chart" + def source = ColoredBarChartSource.source // demo source start - val trace1 = Bar( - Seq(20, 14, 23), - Seq("giraffes", "orangutans", "monkeys"), - name = "SF Zoo", - orientation = Orientation.Horizontal, - marker = Marker( - color = Color.RGBA(55, 128, 191, 0.6), - width = 1 + val trace1 = Bar(Seq(20, 14, 23), Seq("giraffes", "orangutans", "monkeys")) + .withName("SF Zoo") + .withOrientation(Orientation.Horizontal) + .withMarker( + Marker() + .withColor(Color.RGBA(55, 128, 191, 0.6)) + .withWidth(1) ) - ) - - val trace2 = Bar( - Seq(12, 18, 29), - Seq("giraffes", "orangutans", "monkeys"), - name = "LA Zoo", - orientation = Orientation.Horizontal, - marker = Marker( - color = Color.RGBA(255, 153, 51, 0.6), - width = 1 + + val trace2 = Bar(Seq(12, 18, 29), Seq("giraffes", "orangutans", "monkeys")) + .withName("LA Zoo") + .withOrientation(Orientation.Horizontal) + .withMarker( + Marker() + .withColor(Color.RGBA(255, 153, 51, 0.6)) + .withWidth(1) ) - ) val data = Seq(trace1, trace2) - val layout = Layout( - title = "Colored Bar Chart", - barmode = BarMode.Stack - ) + val layout = Layout() + .withTitle("Colored Bar Chart") + .withBarmode(BarMode.Stack) // demo source end diff --git a/demo/src/main/scala/plotly/demo/lineandscatter/CategoricalDotPlot.scala b/demo/src/main/scala/plotly/demo/lineandscatter/CategoricalDotPlot.scala index 08f4489a..72925dfe 100644 --- a/demo/src/main/scala/plotly/demo/lineandscatter/CategoricalDotPlot.scala +++ b/demo/src/main/scala/plotly/demo/lineandscatter/CategoricalDotPlot.scala @@ -36,76 +36,75 @@ object CategoricalDotPlot extends DemoChart { 49.1, 42.0, 52.7, 84.3, 51.7, 61.1, 55.3, 64.2, 91.1, 58.9 ) - val trace1 = Scatter( - votingPop, - country, - mode = ScatterMode(ScatterMode.Markers), - name = "Percent of estimated voting age population", - marker = Marker( - color = Color.RGBA(156, 165, 196, 0.95), - line = Line( - color = Color.RGBA(156, 165, 196, 1.0), - width = 1.0 - ), - symbol = Symbol.Circle(), - size = 16 + val trace1 = Scatter(votingPop, country) + .withMode(ScatterMode(ScatterMode.Markers)) + .withName("Percent of estimated voting age population") + .withMarker( + Marker() + .withColor(Color.RGBA(156, 165, 196, 0.95)) + .withLine( + Line() + .withColor(Color.RGBA(156, 165, 196, 1.0)) + .withWidth(1.0) + ) + .withSymbol(Symbol.Circle()) + .withSize(16) ) - ) - val trace2 = Scatter( - regVoters, - country, - mode = ScatterMode(ScatterMode.Markers), - name = "Percent of estimated registered voters", - marker = Marker( - color = Color.RGBA(204, 204, 204, 0.95), - line = Line( - color = Color.RGBA(217, 217, 217, 1.0), - width = 1.0 - ), - symbol = Symbol.Circle(), - size = 16 + val trace2 = Scatter(regVoters, country) + .withMode(ScatterMode(ScatterMode.Markers)) + .withName("Percent of estimated registered voters") + .withMarker( + Marker() + .withColor(Color.RGBA(204, 204, 204, 0.95)) + .withLine( + Line() + .withColor(Color.RGBA(217, 217, 217, 1.0)) + .withWidth(1.0) + ) + .withSymbol(Symbol.Circle()) + .withSize(16) ) - ) val data = Seq(trace1, trace2) - val layout = Layout( - title = "Votes cast for ten lowest voting age population in OECD countries", - xaxis = Axis( - showgrid = false, - showline = true, - linecolor = Color.RGB(102, 102, 102), - titlefont = Font( - color = Color.RGB(204, 204, 204) - ), - tickfont = Font( - color = Color.RGB(102, 102, 102) - ), - autotick = false, - dtick = 10.0, - ticks = Ticks.Outside, - tickcolor = Color.RGB(102, 102, 102) - ), - margin = Margin( - l = 140, - r = 40, - b = 50, - t = 80 - ), - legend = Legend( - font = Font( - size = 10 - ), - yanchor = Anchor.Middle, - xanchor = Anchor.Right - ), - width = 600, - height = 400, - paper_bgcolor = Color.RGB(254, 247, 234), - plot_bgcolor = Color.RGB(254, 247, 234), - hovermode = HoverMode.Closest - ) + val layout = Layout() + .withTitle("Votes cast for ten lowest voting age population in OECD countries") + .withXaxis( + Axis() + .withShowgrid(false) + .withShowline(true) + .withLinecolor(Color.RGB(102, 102, 102)) + .withTitlefont( + Font(Color.RGB(204, 204, 204)) + ) + .withTickfont( + Font(Color.RGB(102, 102, 102)) + ) + .withAutotick(false) + .withDtick(10.0) + .withTicks(Ticks.Outside) + .withTickcolor(Color.RGB(102, 102, 102)) + ) + .withMargin( + Margin( + l = 140, + r = 40, + b = 50, + t = 80 + ) + ) + .withLegend( + Legend() + .withFont(Font(size = 10)) + .withYanchor(Anchor.Middle) + .withXanchor(Anchor.Right) + ) + .withWidth(600) + .withHeight(400) + .withPaper_bgcolor(Color.RGB(254, 247, 234)) + .withPlot_bgcolor(Color.RGB(254, 247, 234)) + .withHovermode(HoverMode.Closest) // demo source end diff --git a/demo/src/main/scala/plotly/demo/linecharts/LineAndScatterPlot.scala b/demo/src/main/scala/plotly/demo/linecharts/LineAndScatterPlot.scala index c4a73856..e20eff5a 100644 --- a/demo/src/main/scala/plotly/demo/linecharts/LineAndScatterPlot.scala +++ b/demo/src/main/scala/plotly/demo/linecharts/LineAndScatterPlot.scala @@ -15,29 +15,19 @@ object LineAndScatterPlot extends DemoChart { // demo source start - val trace1 = Scatter( - Seq(1, 2, 3, 4), - Seq(10, 15, 13, 17), - mode = ScatterMode(ScatterMode.Markers) - ) - - val trace2 = Scatter( - Seq(2, 3, 4, 5), - Seq(16, 5, 11, 9), - mode = ScatterMode(ScatterMode.Lines) - ) - - val trace3 = Scatter( - Seq(1, 2, 3, 4), - Seq(12, 9, 15, 12), - mode = ScatterMode(ScatterMode.Lines, ScatterMode.Markers) - ) + val trace1 = Scatter(Seq(1, 2, 3, 4), Seq(10, 15, 13, 17)) + .withMode(ScatterMode(ScatterMode.Markers)) + + val trace2 = Scatter(Seq(2, 3, 4, 5), Seq(16, 5, 11, 9)) + .withMode(ScatterMode(ScatterMode.Lines)) + + val trace3 = Scatter(Seq(1, 2, 3, 4), Seq(12, 9, 15, 12)) + .withMode(ScatterMode(ScatterMode.Lines, ScatterMode.Markers)) val data = Seq(trace1, trace2, trace3) - val layout = Layout( - title = "Line and Scatter Plot" - ) + val layout = Layout() + .withTitle("Line and Scatter Plot") // demo source end diff --git a/demo/src/main/scala/plotly/demo/timeseries/TimeSeries.scala b/demo/src/main/scala/plotly/demo/timeseries/TimeSeries.scala index 803a730e..6fdced95 100644 --- a/demo/src/main/scala/plotly/demo/timeseries/TimeSeries.scala +++ b/demo/src/main/scala/plotly/demo/timeseries/TimeSeries.scala @@ -6,8 +6,8 @@ import plotly.demo.NoLayoutDemoChart object TimeSeries extends NoLayoutDemoChart { def plotlyDocUrl = "https://plot.ly/javascript/time-series/#date-strings" - def id = "time-series-chart" - def source = TimeSeriesSource.source + def id = "time-series-chart" + def source = TimeSeriesSource.source // demo source start diff --git a/joda-time/src/main/scala/plotly/Joda.scala b/joda-time/src/main/scala/plotly/Joda.scala index 56ed11c8..f22aade3 100644 --- a/joda-time/src/main/scala/plotly/Joda.scala +++ b/joda-time/src/main/scala/plotly/Joda.scala @@ -3,6 +3,8 @@ package plotly import org.joda.time._ import plotly.Sequence.DateTimes +import scala.language.implicitConversions + object Joda { implicit def fromJodaLocalDates(seq: Seq[LocalDate]): Sequence = diff --git a/jupyter-scala/src/main/scala/plotly/JupyterScala.scala b/jupyter-scala/src/main/scala/plotly/JupyterScala.scala deleted file mode 100644 index b2a0155c..00000000 --- a/jupyter-scala/src/main/scala/plotly/JupyterScala.scala +++ /dev/null @@ -1,242 +0,0 @@ -package plotly - -import jupyter.api.Publish - -import java.lang.{ Integer => JInt, Double => JDouble, Boolean => JBoolean } - -import scala.util.Random - -import plotly.element._ -import plotly.layout._ - -object JupyterScala { - - def init(offline: Boolean = false)(implicit publish: Publish): Unit = { - - // offline mode like in plotly-python - - val requireInit = - if (offline) - s"""define('plotly', function(require, exports, module) { - | ${Plotly.plotlyMinJs} - |}); - """ - else - """require.config({ - | paths: { - | d3: 'https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min', - | plotly: 'https://cdn.plot.ly/plotly-1.12.0.min' - | }, - | - | shim: { - | plotly: { - | deps: ['d3', 'jquery'], - | exports: 'plotly' - | } - | } - |}); - """.stripMargin - - val html = s""" - - """ - - publish.html(html) - } - - def plotJs( - div: String, - data: Seq[Trace], - layout: Layout - )(implicit - publish: Publish - ): Unit = { - - val baseJs = Plotly.jsSnippet(div, data, layout) - - val js = - s"""requirejs(["plotly"], function(Plotly) { - | $baseJs - |}); - """.stripMargin - publish.js(js) - } - - def randomDiv() = "plot-" + math.abs(Random.nextInt().toLong) - - def plot( - data: Seq[Trace], - layout: Layout = Layout(), - div: String = "" - )(implicit - publish: Publish - ): String = { - - val div0 = - if (div.isEmpty) - randomDiv() - else - div - - if (div.isEmpty) - publish.html(s"""
""") - - plotJs(div0, data, layout) - - div0 - } - - implicit class DataOps(val data: Trace) extends AnyVal { - - def plot( - title: String = null, - legend: Legend = null, - width: JInt = null, - height: JInt = null, - showlegend: JBoolean = null, - xaxis: Axis = null, - yaxis: Axis = null, - xaxis1: Axis = null, - xaxis2: Axis = null, - xaxis3: Axis = null, - xaxis4: Axis = null, - yaxis1: Axis = null, - yaxis2: Axis = null, - yaxis3: Axis = null, - yaxis4: Axis = null, - barmode: BarMode = null, - autosize: JBoolean = null, - margin: Margin = null, - annotations: Seq[Annotation] = null, - plot_bgcolor: Color = null, - paper_bgcolor: Color = null, - font: Font = null, - bargap: JDouble = null, - bargroupgap: JDouble = null, - hovermode: HoverMode = null, - boxmode: BoxMode = null, - div: String = "" - )(implicit - publish: Publish - ): String = - plot( - Layout( - title, - legend, - width, - height, - showlegend, - xaxis, - yaxis, - xaxis1, - xaxis2, - xaxis3, - xaxis4, - yaxis1, - yaxis2, - yaxis3, - yaxis4, - barmode, - autosize, - margin, - annotations, - plot_bgcolor, - paper_bgcolor, - font, - bargap, - bargroupgap, - hovermode, - boxmode - ), - div - ) - - def plot( - layout: Layout, - div: String - )(implicit - publish: Publish - ): String = - JupyterScala.plot(Seq(data), layout, div = div) - } - - implicit class DataSeqOps(val data: Seq[Trace]) extends AnyVal { - def plot( - title: String = null, - legend: Legend = null, - width: JInt = null, - height: JInt = null, - showlegend: JBoolean = null, - xaxis: Axis = null, - yaxis: Axis = null, - xaxis1: Axis = null, - xaxis2: Axis = null, - xaxis3: Axis = null, - xaxis4: Axis = null, - yaxis1: Axis = null, - yaxis2: Axis = null, - yaxis3: Axis = null, - yaxis4: Axis = null, - barmode: BarMode = null, - autosize: JBoolean = null, - margin: Margin = null, - annotations: Seq[Annotation] = null, - plot_bgcolor: Color = null, - paper_bgcolor: Color = null, - font: Font = null, - bargap: JDouble = null, - bargroupgap: JDouble = null, - hovermode: HoverMode = null, - boxmode: BoxMode = null, - div: String = "" - )(implicit - publish: Publish - ): String = - plot( - Layout( - title, - legend, - width, - height, - showlegend, - xaxis, - yaxis, - xaxis1, - xaxis2, - xaxis3, - xaxis4, - yaxis1, - yaxis2, - yaxis3, - yaxis4, - barmode, - autosize, - margin, - annotations, - plot_bgcolor, - paper_bgcolor, - font, - bargap, - bargroupgap, - hovermode, - boxmode - ), - div - ) - - def plot( - layout: Layout, - div: String - )(implicit - publish: Publish - ): String = - JupyterScala.plot(data, layout, div = div) - } - -} diff --git a/plotly-documentation b/plotly-documentation index 49469d3e..20b627cf 160000 --- a/plotly-documentation +++ b/plotly-documentation @@ -1 +1 @@ -Subproject commit 49469d3e4f1295dc383384535d11573928134a6c +Subproject commit 20b627cf49db521172221c28788509621bc9449e diff --git a/project/Aliases.scala b/project/Aliases.scala deleted file mode 100644 index 12acbf45..00000000 --- a/project/Aliases.scala +++ /dev/null @@ -1,9 +0,0 @@ - -import sbt._ -import sbt.Keys._ - -object Aliases { - - def libs = libraryDependencies - -} diff --git a/project/Deps.scala b/project/Deps.scala index 2fbedcbc..704a7ae2 100644 --- a/project/Deps.scala +++ b/project/Deps.scala @@ -1,28 +1,18 @@ - import sbt._ -import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ +import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._ object Deps { import Def.setting - private val jupyterScalaVersion = "0.4.0" - private val circeVersion = "0.6.1" - - - def circeCore = setting("io.circe" %%% "circe-core" % circeVersion) - def circeLiteral = setting("io.circe" %% "circe-literal" % circeVersion) - def circeScalaJs = setting("io.circe" %%% "circe-scalajs" % circeVersion) - def circeParser = setting("io.circe" %%% "circe-parser" % circeVersion) - def jodaTime = "joda-time" % "joda-time" % "2.9.1" - def jupyterScalaApi = "org.jupyter-scala" % "scala-api" % jupyterScalaVersion cross CrossVersion.full - def rhino = "org.mozilla" % "rhino" % "1.7.7.1" - def shapeless = setting("com.chuusai" %%% "shapeless" % "2.3.2") - def scalacheckShapeless = setting("com.github.alexarchambault" %%% "scalacheck-shapeless_1.13" % "1.1.7") - def scalajsDom = setting("org.scala-js" %%% "scalajs-dom" % "0.9.3") - def scalatags = setting("com.lihaoyi" %%% "scalatags" % "0.6.7") - def scalaTest = "org.scalatest" %% "scalatest" % "3.0.4" - def utest = setting("com.lihaoyi" %%% "utest" % "0.5.4") + def almondScalaApi = "sh.almond" %% "jupyter-api" % "0.13.14" + def argonautShapeless = setting("com.github.alexarchambault" %%% "argonaut-shapeless_6.3" % "1.3.1") + def dataClass = "io.github.alexarchambault" %% "data-class" % "0.2.6" + def jodaTime = "joda-time" % "joda-time" % "2.12.7" + def rhino = "org.mozilla" % "rhino" % "1.7.15" + def scalajsDom = setting("org.scala-js" %%% "scalajs-dom" % "2.8.0") + def scalatags = setting("com.lihaoyi" %%% "scalatags" % "0.13.1") + def scalaTest = "org.scalatest" %% "scalatest" % "3.2.18" } diff --git a/project/Mima.scala b/project/Mima.scala new file mode 100644 index 00000000..ba0d0a1d --- /dev/null +++ b/project/Mima.scala @@ -0,0 +1,17 @@ +import com.typesafe.tools.mima.core._ +import com.typesafe.tools.mima.plugin.MimaPlugin +import sbt._ +import sbt.Keys._ + +import scala.sys.process._ + +object Mima { + + lazy val renderFilters = Def.settings( + MimaPlugin.autoImport.mimaBinaryIssueFilters ++= Seq( + // users shouln't ever reference those + ProblemFilters.exclude[Problem]("plotly.internals.shaded.*") + ) + ) + +} diff --git a/project/Settings.scala b/project/Settings.scala index d0d91b8e..765b1b1f 100644 --- a/project/Settings.scala +++ b/project/Settings.scala @@ -1,17 +1,15 @@ - +import com.jsuereth.sbtpgp._ import sbt._ import sbt.Keys._ -import Aliases._ - object Settings { lazy val customSourceGenerators = TaskKey[Seq[sbt.File]]("custom-source-generators") lazy val generateCustomSources = Seq( customSourceGenerators := { - var dir = target.value - val f = dir / "Properties.scala" + val dir = target.value + val f = dir / "Properties.scala" dir.mkdirs() def gitCommit = @@ -27,8 +25,7 @@ object Settings { | val commitHash = "$gitCommit" | |} - """ - .stripMargin + """.stripMargin .getBytes("UTF-8") ) w.close() @@ -42,14 +39,12 @@ object Settings { def process(destDir: File, pathComponents: Seq[String], file: File): Unit = { if (file.isDirectory) { - val destDir0 = destDir / file.getName + val destDir0 = destDir / file.getName val pathComponents0 = pathComponents :+ file.getName for (f <- file.listFiles()) process(destDir0, pathComponents0, f) } else { - val lines = new String(java.nio.file.Files.readAllBytes(file.toPath), "UTF-8") - .linesIterator - .toVector + val lines = new String(java.nio.file.Files.readAllBytes(file.toPath), "UTF-8").linesIterator.toVector val demoLines = lines .dropWhile(!_.contains("demo source start")) @@ -69,8 +64,7 @@ object Settings { | val source = $tq${demoLines.mkString("\n")}$tq | |} - """ - .stripMargin + """.stripMargin .getBytes("UTF-8") ) w.close() @@ -80,72 +74,67 @@ object Settings { } } - process(dir / "plotly", Vector(), scalaSource.in(Compile).value / "plotly" / "demo") + process(dir / "plotly", Vector(), (Compile / scalaSource).value / "plotly" / "demo") files }, - sourceGenerators.in(Compile) += customSourceGenerators.taskValue + (Compile / sourceGenerators) += customSourceGenerators.taskValue ) - lazy val shared = Seq( - organization := "org.plotly-scala", - scalacOptions ++= { - if (scalaBinaryVersion.value == "2.12") - Seq() - else - Seq("-target:jvm-1.7") - }, - resolvers ++= Seq( - "Webjars Bintray" at "https://dl.bintray.com/webjars/maven/", - Resolver.sonatypeRepo("releases") - ), - publishMavenStyle := true, - licenses := Seq("LGPL 3.0" -> url("http://opensource.org/licenses/LGPL-3.0")), - homepage := Some(url("https://github.com/alexarchambault/plotly-scala")), - pomExtra := { - - scm:git:github.com/alexarchambault/plotly-scala.git - scm:git:git@github.com:alexarchambault/plotly-scala.git - github.com/alexarchambault/plotly-scala.git - - - - alexarchambault - Alexandre Archambault - https://github.com/alexarchambault - - - }, - publishTo := { - val nexus = "https://oss.sonatype.org/" - if (isSnapshot.value) - Some("snapshots" at nexus + "content/repositories/snapshots") - else - Some("releases" at nexus + "service/local/staging/deploy/maven2") + private val scala212 = "2.12.19" + private val scala213 = "2.13.14" + + private lazy val isAtLeastScala213 = Def.setting { + import Ordering.Implicits._ + CrossVersion.partialVersion(scalaVersion.value).exists(_ >= (2, 13)) + } + + lazy val shared = Def.settings( + crossScalaVersions := Seq(scala213, scala212), + scalaVersion := scala213, + resolvers += "jitpack" at "https://jitpack.io", + libraryDependencies ++= { + if (isAtLeastScala213.value) Nil + else Seq(compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full)) }, - credentials ++= { - Seq("SONATYPE_USER", "SONATYPE_PASS").map(sys.env.get) match { - case Seq(Some(user), Some(pass)) => - Seq(Credentials("Sonatype Nexus Repository Manager", "oss.sonatype.org", user, pass)) - case _ => - Seq() - } + scalacOptions ++= Seq("-deprecation", "-feature"), + scalacOptions ++= { + if (isAtLeastScala213.value) Seq("-Ymacro-annotations") + else Nil } ) - lazy val dontPublish = Seq( - publish := (), - publishLocal := (), - publishArtifact := false - ) - lazy val plotlyPrefix = { name := "plotly-" + name.value } - lazy val utest = Seq( - libs += Deps.utest.value % "test", - testFrameworks += new TestFramework("utest.runner.Framework") - ) + val gitLock = new Object + + def runCommand(cmd: Seq[String], dir: File): Unit = { + val b = new ProcessBuilder(cmd: _*) + b.directory(dir) + b.inheritIO() + val p = b.start() + val retCode = p.waitFor() + if (retCode != 0) + sys.error(s"Command ${cmd.mkString(" ")} failed (return code $retCode)") + } + lazy val fetchTestData = { + (Test / unmanagedResources) ++= { + val log = streams.value.log + val baseDir = (LocalRootProject / baseDirectory).value + val testsPostsDir = baseDir / "plotly-documentation" / "_posts" + if (!testsPostsDir.exists()) + gitLock.synchronized { + if (!testsPostsDir.exists()) { + val cmd = Seq("git", "submodule", "update", "--init", "--recursive", "--", "plotly-documentation") + log.info("Fetching submodule plotly-documentation (this may take some time)") + runCommand(cmd, baseDir) + log.info("Successfully fetched submodule plotly-documentation") + } + } + Nil + } + } } diff --git a/project/WebDeps.scala b/project/WebDeps.scala index cc98d920..9baf2e9f 100644 --- a/project/WebDeps.scala +++ b/project/WebDeps.scala @@ -1,13 +1,14 @@ - import sbt._ object WebDeps { - private val plotlyVersion = "1.24.1" + object Versions { + def plotlyJs = "1.54.1" + } - def bootstrap = "org.webjars.bower" % "bootstrap" % "3.3.6" - def jquery = "org.webjars.bower" % "jquery" % "2.2.4" - def plotlyJs = "org.webjars.bower" % "plotly.js" % plotlyVersion - def prism = "org.webjars.bower" % "prism" % "1.5.0" + def bootstrap = "org.webjars.bower" % "bootstrap" % "3.4.1" + def jquery = "org.webjars.bower" % "jquery" % "3.7.1" + def plotlyJs = "org.webjars.bower" % "plotly.js" % Versions.plotlyJs + def prism = "org.webjars.bower" % "prism" % "1.16.0" } diff --git a/project/build.properties b/project/build.properties index c091b86c..081fdbbc 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.16 +sbt.version=1.10.0 diff --git a/project/make-ghpages.sh b/project/make-ghpages.sh index 7a2239df..276e5ba5 100755 --- a/project/make-ghpages.sh +++ b/project/make-ghpages.sh @@ -6,17 +6,17 @@ if [ -e gh-pages ]; then exit 1 fi -sbt demo/fullOptJS +./sbt demo/fullOptJS mkdir gh-pages cp \ - demo/target/scala-2.11/plotly-demo-opt.js \ - demo/target/scala-2.11/plotly-demo-opt.js.map \ - demo/target/scala-2.11/plotly-demo-jsdeps.js \ - demo/target/scala-2.11/plotly-demo-jsdeps.min.js \ + demo/target/scala-2.13/plotly-demo-opt.js \ + demo/target/scala-2.13/plotly-demo-opt.js.map \ + demo/target/scala-2.13/plotly-demo-jsdeps.js \ + demo/target/scala-2.13/plotly-demo-jsdeps.min.js \ gh-pages -cat demo/target/scala-2.11/classes/index.html | \ +cat demo/target/scala-2.13/classes/index.html | \ sed 's@\.\./plotly-demo-jsdeps\.js@plotly-demo-jsdeps.min.js@' | \ sed 's@\.\./plotly-demo-fastopt\.js@plotly-demo-opt.js@' | \ cat > gh-pages/index.html diff --git a/project/plugins.sbt b/project/plugins.sbt index 32d019ec..dc710d5d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,7 @@ -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.20") -addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0") -addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.1.18") -addSbtPlugin("com.dwijnand" % "sbt-travisci" % "1.1.1") - -addSbtCoursier +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") +addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.3") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") +addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.2") +addSbtPlugin("io.get-coursier" % "sbt-shading" % "2.1.4") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") diff --git a/project/project/plugins.sbt b/project/project/plugins.sbt deleted file mode 100644 index 9ca984e9..00000000 --- a/project/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC12") diff --git a/render/js/src/main/scala/plotly/Plotly.scala b/render/js/src/main/scala/plotly/Plotly.scala index 12f01c1f..fa246d5d 100644 --- a/render/js/src/main/scala/plotly/Plotly.scala +++ b/render/js/src/main/scala/plotly/Plotly.scala @@ -1,84 +1,106 @@ package plotly -import io.circe.{Json, Printer} -import io.circe.syntax._ - -import scala.scalajs.js -import scala.scalajs.js.Dynamic.{global => g} -import scala.scalajs.js.JSON +import argonaut.Argonaut._ +import argonaut.{EncodeJson, PrettyParams} import plotly.Codecs._ import plotly.element.Color +import plotly.internals.BetterPrinter import plotly.layout._ + import java.lang.{Boolean => JBoolean, Double => JDouble, Integer => JInt} +import scala.scalajs.js +import scala.scalajs.js.Dynamic.{global => g} +import scala.scalajs.js.JSON object Plotly { - private val printer = Printer.noSpaces.copy(dropNullKeys = true) - private def stripNulls(json: Json): js.Any = { - // Remove empty objects - JSON.parse(printer.pretty(json)) + private val printer = BetterPrinter(PrettyParams.nospace.copy(dropNullKeys = true)) + + // Remove empty objects + private def stripNulls[J: EncodeJson](value: J): js.Any = JSON.parse(printer.render(value.asJson)) + + trait PlotlyDyn { + def plotFn: js.Dynamic + + def apply(div: String, data: Seq[Trace], layout: Layout, config: Config): Unit = + plotFn( + div, + stripNulls(data), + stripNulls(layout), + stripNulls(config) + ) + + def apply(div: String, data: Seq[Trace], layout: Layout): Unit = + plotFn( + div, + stripNulls(data), + stripNulls(layout) + ) + + def apply(div: String, data: Seq[Trace]): Unit = + plotFn(div, stripNulls(data)) + + def apply(div: String, data: Trace, layout: Layout): Unit = + plotFn(div, stripNulls(data), stripNulls(layout)) + + def apply(div: String, data: Trace): Unit = + plotFn(div, stripNulls(data)) } - def plot(div: String, data: Seq[Trace], layout: Layout): Unit = { - g.Plotly.plot( - div, - stripNulls(data.asJson), - stripNulls(layout.asJson) - ) + object newPlot extends PlotlyDyn { + val plotFn: js.Dynamic = g.Plotly.newPlot } - def plot(div: String, data: Seq[Trace]): Unit = { - g.Plotly.plot( - div, - stripNulls(data.asJson) - ) + object plot extends PlotlyDyn { + val plotFn: js.Dynamic = g.Plotly.newPlot } - def plot(div: String, data: Trace, layout: Layout): Unit = - g.Plotly.plot( - div, - stripNulls(data.asJson), - stripNulls(layout.asJson) - ) + object react extends PlotlyDyn { + val plotFn: js.Dynamic = g.Plotly.react + } + + def relayout(div: String, layout: Layout): Unit = + g.Plotly.relayout(div, stripNulls(layout)) + + def purge(div: String): Unit = + g.Plotly.purge(div) - def plot(div: String, data: Trace): Unit = - g.Plotly.plot( - div, - stripNulls(data.asJson) - ) + def validate(data: Seq[Trace], layout: Layout, config: Config): Unit = + g.Plotly.validate(stripNulls(data), stripNulls(layout)) implicit class TraceOps(val trace: Trace) extends AnyVal { def plot(div: String, layout: Layout): Unit = Plotly.plot(div, trace, layout) + @deprecated("Create a Layout and call plot(div, layout) instead", "0.8.0") def plot( - div: String, - title: String = null, - legend: Legend = null, - width: JInt = null, - height: JInt = null, - showlegend: JBoolean = null, - xaxis: Axis = null, - yaxis: Axis = null, - xaxis1: Axis = null, - xaxis2: Axis = null, - xaxis3: Axis = null, - xaxis4: Axis = null, - yaxis1: Axis = null, - yaxis2: Axis = null, - yaxis3: Axis = null, - yaxis4: Axis = null, - barmode: BarMode = null, - autosize: JBoolean = null, - margin: Margin = null, + div: String, + title: String = null, + legend: Legend = null, + width: JInt = null, + height: JInt = null, + showlegend: JBoolean = null, + xaxis: Axis = null, + yaxis: Axis = null, + xaxis1: Axis = null, + xaxis2: Axis = null, + xaxis3: Axis = null, + xaxis4: Axis = null, + yaxis1: Axis = null, + yaxis2: Axis = null, + yaxis3: Axis = null, + yaxis4: Axis = null, + barmode: BarMode = null, + autosize: JBoolean = null, + margin: Margin = null, annotations: Seq[Annotation] = null, - plot_bgcolor: Color = null, - paper_bgcolor: Color = null, - font: Font = null, - bargap: JDouble = null, - bargroupgap: JDouble = null, - hovermode: HoverMode = null, - boxmode: BoxMode = null + plot_bgcolor: Color = null, + paper_bgcolor: Color = null, + font: Font = null, + bargap: JDouble = null, + bargroupgap: JDouble = null, + hovermode: HoverMode = null, + boxmode: BoxMode = null ): Unit = plot( div, @@ -117,34 +139,35 @@ object Plotly { def plot(div: String, layout: Layout): Unit = Plotly.plot(div, traces, layout) + @deprecated("Create a Layout and call plot(div, layout) instead", "0.8.0") def plot( - div: String, - title: String = null, - legend: Legend = null, - width: JInt = null, - height: JInt = null, - showlegend: JBoolean = null, - xaxis: Axis = null, - yaxis: Axis = null, - xaxis1: Axis = null, - xaxis2: Axis = null, - xaxis3: Axis = null, - xaxis4: Axis = null, - yaxis1: Axis = null, - yaxis2: Axis = null, - yaxis3: Axis = null, - yaxis4: Axis = null, - barmode: BarMode = null, - autosize: JBoolean = null, - margin: Margin = null, + div: String, + title: String = null, + legend: Legend = null, + width: JInt = null, + height: JInt = null, + showlegend: JBoolean = null, + xaxis: Axis = null, + yaxis: Axis = null, + xaxis1: Axis = null, + xaxis2: Axis = null, + xaxis3: Axis = null, + xaxis4: Axis = null, + yaxis1: Axis = null, + yaxis2: Axis = null, + yaxis3: Axis = null, + yaxis4: Axis = null, + barmode: BarMode = null, + autosize: JBoolean = null, + margin: Margin = null, annotations: Seq[Annotation] = null, - plot_bgcolor: Color = null, - paper_bgcolor: Color = null, - font: Font = null, - bargap: JDouble = null, - bargroupgap: JDouble = null, - hovermode: HoverMode = null, - boxmode: BoxMode = null + plot_bgcolor: Color = null, + paper_bgcolor: Color = null, + font: Font = null, + bargap: JDouble = null, + bargroupgap: JDouble = null, + hovermode: HoverMode = null, + boxmode: BoxMode = null ): Unit = plot( div, diff --git a/render/jvm/src/main/scala/plotly/Plotly.scala b/render/jvm/src/main/scala/plotly/Plotly.scala index 9801bc05..0677a092 100644 --- a/render/jvm/src/main/scala/plotly/Plotly.scala +++ b/render/jvm/src/main/scala/plotly/Plotly.scala @@ -1,24 +1,35 @@ package plotly -import java.io.{ ByteArrayOutputStream, File, InputStream } - -import io.circe.Printer -import io.circe.syntax._ +import java.io.{ByteArrayOutputStream, File, InputStream} import plotly.Codecs._ import plotly.element.Color import plotly.layout._ - -import java.lang.{ Boolean => JBoolean, Double => JDouble, Integer => JInt } +import java.lang.{Boolean => JBoolean, Double => JDouble, Integer => JInt} import java.nio.file.Files +import argonaut.Argonaut._ +import argonaut.{Json, PrettyParams} +import plotly.internals.{BetterPrinter, Properties} + import scala.annotation.tailrec object Plotly { - private val printer = Printer.noSpaces.copy(dropNullKeys = true) + private val printer = BetterPrinter(PrettyParams.nospace.copy(dropNullKeys = true)) + + def jsonSnippet(data: Seq[Trace], layout: Layout, config: Config): String = { - def jsSnippet(div: String, data: Seq[Trace], layout: Layout): String = { + val json = Json.obj( + "data" -> data.toList.asJson, + "layout" -> layout.asJson, + "config" -> config.asJson + ) + + printer.render(json) + } + + def jsSnippet(div: String, data: Seq[Trace], layout: Layout, config: Config): String = { val b = new StringBuilder @@ -26,7 +37,7 @@ object Plotly { for ((d, idx) <- data.zipWithIndex) { b ++= s" var data$idx = " - b ++= printer.pretty(d.asJson) + b ++= printer.render(d.asJson) b ++= ";\n" } @@ -34,10 +45,12 @@ object Plotly { b ++= data.indices.map(idx => s"data$idx").mkString("var data = [", ", ", "];") b ++= "\n" b ++= " var layout = " - b ++= printer.pretty(layout.asJson) + b ++= printer.render(layout.asJson) + b ++= ";\n var config = " + b ++= printer.render(config.asJson) b ++= ";\n\n Plotly.plot('" b ++= div.replaceAll("'", "\\'") - b ++= "', data, layout);\n" + b ++= "', data, layout, config);\n" b ++= "})();" @@ -46,7 +59,7 @@ object Plotly { private def readFully(is: InputStream): Array[Byte] = { val buffer = new ByteArrayOutputStream() - val data = Array.ofDim[Byte](16384) + val data = Array.ofDim[Byte](16384) var nRead = is.read(data, 0, data.length) while (nRead != -1) { @@ -58,12 +71,15 @@ object Plotly { buffer.toByteArray } - val plotlyVersion = "1.12.0" // FIXME Get from build.sbt + def plotlyVersion: String = + Properties.plotlyJsVersion def plotlyMinJs: String = { var is: InputStream = null try { - is = getClass.getClassLoader.getResourceAsStream(s"META-INF/resources/webjars/plotly.js/$plotlyVersion/dist/plotly.min.js") + is = getClass.getClassLoader.getResourceAsStream( + s"META-INF/resources/webjars/plotly.js/$plotlyVersion/dist/plotly.min.js" + ) if (is == null) throw new Exception(s"plotly.min.js resource not found") @@ -75,12 +91,13 @@ object Plotly { } def plot( - path: String, - traces: Seq[Trace], - layout: Layout, - useCdn: Boolean = true, - openInBrowser: Boolean = true, - addSuffixIfExists: Boolean = true + path: String, + traces: Seq[Trace], + layout: Layout, + config: Config = Config(), + useCdn: Boolean = true, + openInBrowser: Boolean = true, + addSuffixIfExists: Boolean = true ): File = { val f0 = new File(path) @@ -88,7 +105,7 @@ object Plotly { val f = if (addSuffixIfExists) { lazy val name = f0.getName - lazy val idx = name.lastIndexOf('.') + lazy val idx = name.lastIndexOf('.') lazy val (prefix, suffixOpt) = if (idx < 0) @@ -129,13 +146,14 @@ object Plotly { s""" | | + | |${layout.title.getOrElse("plotly chart")} |$plotlyHeader | | |
| | | @@ -168,11 +186,11 @@ object Plotly { implicit class TraceOps(val trace: Trace) extends AnyVal { def plot( - path: String, - layout: Layout, - useCdn: Boolean, - openInBrowser: Boolean, - addSuffixIfExists: Boolean + path: String, + layout: Layout, + useCdn: Boolean, + openInBrowser: Boolean, + addSuffixIfExists: Boolean ): Unit = Plotly.plot( path, @@ -182,38 +200,39 @@ object Plotly { openInBrowser = openInBrowser, addSuffixIfExists = addSuffixIfExists ) - + + @deprecated("Create a Layout and call plot(path, layout) instead", "0.8.0") def plot( - path: String = "./plot.html", - title: String = null, - legend: Legend = null, - width: JInt = null, - height: JInt = null, - showlegend: JBoolean = null, - xaxis: Axis = null, - yaxis: Axis = null, - xaxis1: Axis = null, - xaxis2: Axis = null, - xaxis3: Axis = null, - xaxis4: Axis = null, - yaxis1: Axis = null, - yaxis2: Axis = null, - yaxis3: Axis = null, - yaxis4: Axis = null, - barmode: BarMode = null, - autosize: JBoolean = null, - margin: Margin = null, - annotations: Seq[Annotation] = null, - plot_bgcolor: Color = null, - paper_bgcolor: Color = null, - font: Font = null, - bargap: JDouble = null, - bargroupgap: JDouble = null, - hovermode: HoverMode = null, - boxmode: BoxMode = null, - useCdn: Boolean = true, - openInBrowser: Boolean = true, - addSuffixIfExists: Boolean = true + path: String = "./plot.html", + title: String = null, + legend: Legend = null, + width: JInt = null, + height: JInt = null, + showlegend: JBoolean = null, + xaxis: Axis = null, + yaxis: Axis = null, + xaxis1: Axis = null, + xaxis2: Axis = null, + xaxis3: Axis = null, + xaxis4: Axis = null, + yaxis1: Axis = null, + yaxis2: Axis = null, + yaxis3: Axis = null, + yaxis4: Axis = null, + barmode: BarMode = null, + autosize: JBoolean = null, + margin: Margin = null, + annotations: Seq[Annotation] = null, + plot_bgcolor: Color = null, + paper_bgcolor: Color = null, + font: Font = null, + bargap: JDouble = null, + bargroupgap: JDouble = null, + hovermode: HoverMode = null, + boxmode: BoxMode = null, + useCdn: Boolean = true, + openInBrowser: Boolean = true, + addSuffixIfExists: Boolean = true ): Unit = plot( path, @@ -249,15 +268,27 @@ object Plotly { openInBrowser, addSuffixIfExists ) + + def plot( + path: String, + layout: Layout + ): Unit = + plot( + path, + layout, + useCdn = true, + openInBrowser = true, + addSuffixIfExists = true + ) } implicit class TraceSeqOps(val traces: Seq[Trace]) extends AnyVal { def plot( - path: String, - layout: Layout, - useCdn: Boolean, - openInBrowser: Boolean, - addSuffixIfExists: Boolean + path: String, + layout: Layout, + useCdn: Boolean, + openInBrowser: Boolean, + addSuffixIfExists: Boolean ): Unit = Plotly.plot( path, @@ -267,38 +298,39 @@ object Plotly { openInBrowser = openInBrowser, addSuffixIfExists = addSuffixIfExists ) - + + @deprecated("Create a Layout and call plot(path, layout) instead", "0.8.0") def plot( - path: String = "./plot.html", - title: String = null, - legend: Legend = null, - width: JInt = null, - height: JInt = null, - showlegend: JBoolean = null, - xaxis: Axis = null, - yaxis: Axis = null, - xaxis1: Axis = null, - xaxis2: Axis = null, - xaxis3: Axis = null, - xaxis4: Axis = null, - yaxis1: Axis = null, - yaxis2: Axis = null, - yaxis3: Axis = null, - yaxis4: Axis = null, - barmode: BarMode = null, - autosize: JBoolean = null, - margin: Margin = null, - annotations: Seq[Annotation] = null, - plot_bgcolor: Color = null, - paper_bgcolor: Color = null, - font: Font = null, - bargap: JDouble = null, - bargroupgap: JDouble = null, - hovermode: HoverMode = null, - boxmode: BoxMode = null, - useCdn: Boolean = true, - openInBrowser: Boolean = true, - addSuffixIfExists: Boolean = true + path: String = "./plot.html", + title: String = null, + legend: Legend = null, + width: JInt = null, + height: JInt = null, + showlegend: JBoolean = null, + xaxis: Axis = null, + yaxis: Axis = null, + xaxis1: Axis = null, + xaxis2: Axis = null, + xaxis3: Axis = null, + xaxis4: Axis = null, + yaxis1: Axis = null, + yaxis2: Axis = null, + yaxis3: Axis = null, + yaxis4: Axis = null, + barmode: BarMode = null, + autosize: JBoolean = null, + margin: Margin = null, + annotations: Seq[Annotation] = null, + plot_bgcolor: Color = null, + paper_bgcolor: Color = null, + font: Font = null, + bargap: JDouble = null, + bargroupgap: JDouble = null, + hovermode: HoverMode = null, + boxmode: BoxMode = null, + useCdn: Boolean = true, + openInBrowser: Boolean = true, + addSuffixIfExists: Boolean = true ): Unit = plot( path, @@ -334,6 +366,18 @@ object Plotly { openInBrowser, addSuffixIfExists ) + + def plot( + path: String, + layout: Layout + ): Unit = + plot( + path, + layout, + useCdn = true, + openInBrowser = true, + addSuffixIfExists = true + ) } } diff --git a/render/jvm/src/main/scala/plotly/internals/Properties.scala b/render/jvm/src/main/scala/plotly/internals/Properties.scala new file mode 100644 index 00000000..a129698d --- /dev/null +++ b/render/jvm/src/main/scala/plotly/internals/Properties.scala @@ -0,0 +1,24 @@ +package plotly.internals + +import java.util.{Properties => JProperties} + +object Properties { + + private lazy val props = { + val p = new JProperties + try { + p.load( + getClass.getClassLoader + .getResourceAsStream("plotly/plotly-scala.properties") + ) + } catch { + case _: NullPointerException => + } + p + } + + lazy val plotlyJsVersion = props.getProperty("plotly-js-version") + lazy val version = props.getProperty("version") + lazy val commitHash = props.getProperty("commit-hash") + +} diff --git a/render/jvm/src/test/scala/plotly/ResourceTests.scala b/render/jvm/src/test/scala/plotly/ResourceTests.scala new file mode 100644 index 00000000..a8abafe5 --- /dev/null +++ b/render/jvm/src/test/scala/plotly/ResourceTests.scala @@ -0,0 +1,11 @@ +package plotly + +import org.scalatest.propspec.AnyPropSpec + +class ResourceTests extends AnyPropSpec { + + property("plotly.min.js must be found in resources") { + assert(Plotly.plotlyMinJs.nonEmpty) + } + +} diff --git a/render/shared/src/main/scala/plotly/Codecs.scala b/render/shared/src/main/scala/plotly/Codecs.scala index a003372d..c612afac 100644 --- a/render/shared/src/main/scala/plotly/Codecs.scala +++ b/render/shared/src/main/scala/plotly/Codecs.scala @@ -1,467 +1,19 @@ package plotly -import java.math.BigInteger - -import io.circe.{ Error => _, _ } -import io.circe.simplegeneric._ -import io.circe.simplegeneric.derive._ -import io.circe.syntax._ - -import shapeless._ - -import scala.util.Try - -import plotly.element._ +import argonaut._ +import argonaut.ArgonautShapeless._ +import plotly.internals.ArgonautCodecsExtra +import plotly.internals.ArgonautCodecsInternals._ import plotly.layout._ -object Codecs { - - object Internals { - - sealed abstract class IsWrapper[W] - - implicit def isWrapperEncode[W, L <: HList, T] - (implicit - ev: IsWrapper[W], - gen: Generic.Aux[W, L], - isHCons: ops.hlist.IsHCons.Aux[L, T, HNil], - underlying: Encoder[T] - ): Encoder[W] = - Encoder.instance { w => - val t = isHCons.head(gen.to(w)) - t.asJson - } - - implicit def isWrapperDecode[W, L <: HList, T] - (implicit - ev: IsWrapper[W], - gen: Generic.Aux[W, L], - isHCons: ops.hlist.IsHCons.Aux[L, T, HNil], - underlying: Decoder[T] - ): Decoder[W] = - Decoder.instance { c => - c.as[T].right.map(t => - gen.from((t :: HNil).asInstanceOf[L]) // FIXME - ) - } - - implicit val boxMeanBoolIsWrapper: IsWrapper[BoxMean.Bool] = null - implicit val boxPointsBoolIsWrapper: IsWrapper[BoxPoints.Bool] = null - implicit val sequenceDoublesIsWrapper: IsWrapper[Sequence.Doubles] = null - implicit val sequenceStringsIsWrapper: IsWrapper[Sequence.Strings] = null - implicit val sequenceDatetimesIsWrapper: IsWrapper[Sequence.DateTimes] = null - implicit val doubleElementIsWrapper: IsWrapper[Element.DoubleElement] = null - implicit val stringElementIsWrapper: IsWrapper[Element.StringElement] = null - implicit def oneOrSeqOneIsWrapper[T]: IsWrapper[OneOrSeq.One[T]] = null - implicit def oneOrSeqSequenceIsWrapper[T]: IsWrapper[OneOrSeq.Sequence[T]] = null - - def flagEncoder[T, F](flags: T => Set[F], label: F => String): Encoder[T] = - Encoder.instance { t => - val s = flags(t).toSeq match { - case Seq() => "none" - case nonEmpty => nonEmpty.map(label).mkString("+") - } - - s.asJson - } - - def flagDecoder[T, F](type0: String, map: Map[String, F], build: Set[F] => T): Decoder[T] = - Decoder.instance { c => - c.as[String].right.flatMap { s => - val flags = - if (s == "none") - Right(Set.empty[F]) - else - s.split('+').foldLeft[Decoder.Result[Set[F]]](Right(Set.empty[F])) { - case (acc, f) => - for { - acc0 <- acc.right - f0 <- map.get(f).fold[Decoder.Result[F]](Left(DecodingFailure(s"Unrecognized $type0: $f", c.history)))(Right(_)).right - } yield acc0 + f0 - } - - flags.right.map(build) - } - } - - sealed abstract class IsEnum[-T] { - def label(t: T): String - } - - object IsEnum { - def apply[T](implicit isEnum: IsEnum[T]): IsEnum[T] = isEnum - - def instance[T](f: T => String): IsEnum[T] = - new IsEnum[T] { - def label(t: T) = f(t) - } - } - - implicit def isEnumEncoder[T: IsEnum]: Encoder[T] = - Encoder[String].contramap(IsEnum[T].label) - - implicit def isEnumDecoder[T] - (implicit - isEnum: IsEnum[T], - enum: Enumerate[T], - typeable: Typeable[T] - ): Decoder[T] = - Decoder.instance { - val underlying = Decoder[String] - val map = enum().map(e => isEnum.label(e) -> e).toMap - val name = typeable.describe // TODO split in words - - c => - underlying(c).right.flatMap { s => - map.get(s) match { - case None => Left(DecodingFailure(s"Unrecognized $name: '$s'", c.history)) - case Some(m) => Right(m) - } - } - } - - implicit val anchorIsEnum = IsEnum.instance[Anchor](_.label) - implicit val refIsEnum = IsEnum.instance[Ref](_.label) - implicit val axisAnchorIsEnum = IsEnum.instance[AxisAnchor](_.label) - implicit val axisReferenceIsEnum = IsEnum.instance[AxisReference](_.label) - implicit val axisTypeIsEnum = IsEnum.instance[AxisType](_.label) - implicit val barModeIsEnum = IsEnum.instance[BarMode](_.label) - implicit val boxModeIsEnum = IsEnum.instance[BoxMode](_.label) - implicit val dashIsEnum = IsEnum.instance[Dash](_.label) - implicit val fillIsEnum = IsEnum.instance[Fill](_.label) - implicit val hoverModeIsEnum = IsEnum.instance[HoverMode](_.label) - implicit val lineShapeIsEnum = IsEnum.instance[LineShape](_.label) - implicit val orientationIsEnum = IsEnum.instance[Orientation](_.label) - implicit val traceOrderIsEnum = IsEnum.instance[TraceOrder](_.label) - implicit val boxMeanOtherIsEnum = IsEnum.instance[BoxMean.Labeled](_.label) - implicit val boxPointsOtherIsEnum = IsEnum.instance[BoxPoints.Labeled](_.label) - implicit val textPositionIsEnum = IsEnum.instance[TextPosition](_.label) - implicit val sideIsEnum = IsEnum.instance[Side](_.label) - implicit val symbolIsEnum = IsEnum.instance[Symbol](_.label) - implicit val ticksIsEnum = IsEnum.instance[Ticks](_.label) - implicit val histNormIsEnum = IsEnum.instance[HistNorm](_.label) - implicit val sizeModeIsEnum = IsEnum.instance[SizeMode](_.label) - - def jsonSumDirectCodecFor(name: String): JsonSumCodec = new JsonSumCodec { - def encodeEmpty: Nothing = - throw new IllegalArgumentException(s"empty $name") - - def encodeField(fieldOrObj: Either[Json, (String, Json)]): Json = - fieldOrObj match { - case Left(other) => other - case Right((_, content)) => content - } - - def decodeEmpty(cursor: HCursor): Decoder.Result[Nothing] = - // FIXME Sometimes reports the wrong error (in case of two nested sum types) - Left(DecodingFailure(s"unrecognized $name", cursor.history)) - - def decodeField[A](name: String, cursor: HCursor, decode: Decoder[A]): Decoder.Result[Either[ACursor, A]] = - Right { - val o = decode(cursor) - o.right.toOption - .toRight(ACursor.ok(cursor)) - } - } - - case class JsonProductObjCodecNoEmpty( - toJsonName: String => String = identity - ) extends JsonProductCodec { - - private val underlying = JsonProductCodec.adapt(toJsonName) - - val encodeEmpty: Json = underlying.encodeEmpty - - def encodeField(field: (String, Json), obj: Json, default: => Option[Json]): Json = - underlying.encodeField(field, obj, default) - - def decodeEmpty(cursor: HCursor): Decoder.Result[Unit] = - if (cursor.focus == Json.obj()) - Right(()) - else - Left(DecodingFailure( - s"Found extra fields: ${cursor.fields.toSeq.flatten.mkString(", ")}", - cursor.history - )) - - def decodeField[A](name: String, cursor: HCursor, decode: Decoder[A], default: Option[A]): Decoder.Result[(A, ACursor)] = { - val c = cursor.downField(toJsonName(name)) - - def result = c.as(decode).right.map((_, if (c.succeeded) c.delete else cursor.acursor)) - - default match { - case None => result - case Some(d) => - if (c.succeeded) - result - else - Right((d, ACursor.ok(cursor))) - } - } - } - - object JsonProductObjCodecNoEmpty { - val default = JsonProductObjCodecNoEmpty() - } - - - implicit def defaultJsonProductCodecFor[T]: JsonProductCodecFor[T] = - JsonProductCodecFor(JsonProductObjCodecNoEmpty.default) - - implicit val encodeRGBA: Encoder[Color.RGBA] = - Encoder[String].contramap(c => s"rgba(${c.r}, ${c.g}, ${c.b}, ${c.alpha})") - - implicit val decodeRGBA: Decoder[Color.RGBA] = - Decoder.instance { c => - c.as[String].right.flatMap { s => - if (s.startsWith("rgba(") && s.endsWith(")")) - s.stripPrefix("rgba(").stripSuffix(")").split(',').map(_.trim) match { - case Array(rStr, gStr, bStr, alphaStr) => - val res = for { - r <- Try(rStr.toInt).toOption - g <- Try(gStr.toInt).toOption - b <- Try(bStr.toInt).toOption - alpha <- Try(alphaStr.toDouble).toOption - } yield Right(Color.RGBA(r, g, b, alpha)) - - res.getOrElse { - Left(DecodingFailure(s"Unrecognized RGBA color: '$s'", c.history)) - } - case _ => - Left(DecodingFailure(s"Unrecognized RGBA color: '$s'", c.history)) - } - else - Left(DecodingFailure(s"Unrecognized RGBA color: '$s'", c.history)) - } - } - - implicit val encodeStringColor: Encoder[Color.StringColor] = - Encoder[String].contramap(_.color) - - implicit val decodeStringColor: Decoder[Color.StringColor] = - Decoder.instance { - val underlying = Decoder[String] - val map = Color.StringColor.colors - .toVector - .map(c => c -> Color.StringColor(c)) - .toMap - - c => - underlying(c).right.flatMap { s => - map.get(s) match { - case None => Left(DecodingFailure(s"Unrecognized color: '$s'", c.history)) - case Some(m) => Right(m) - } - } - } - - private val HexaColor3 = "#([0-9a-fA-F]{3})".r - private val HexaColor6 = "#([0-9a-fA-F]{6})".r - - implicit val encodeRGB: Encoder[Color.RGB] = - Encoder[String].contramap(c => s"rgb(${c.r}, ${c.g}, ${c.b})") - - implicit val decodeRGB: Decoder[Color.RGB] = - Decoder.instance { c => - val asString: Decoder.Result[Color.RGB] = c.as[String].right.flatMap { s => - if (s.startsWith("rgb(") && s.endsWith(")")) - s.stripPrefix("rgb(").stripSuffix(")").split(',').map(_.trim).map(s => Try(s.toInt).toOption) match { - case Array(Some(r), Some(g), Some(b)) => - Right(Color.RGB(r, g, b)) - case _ => - Left(DecodingFailure(s"Unrecognized RGB color: '$s'", c.history)) - } - else - Left(DecodingFailure(s"Unrecognized RGB color: '$s'", c.history)) - } - def asInt: Decoder.Result[Color.RGB] = c.as[Int].right.flatMap { - case r if r >= 0 && r < 256 => - Right(Color.RGB(r, 0, 0)) - case _ => - Left(DecodingFailure(s"Unrecognized RGB color: ${c.focus}", c.history)) - } - - def parseHex(s: String, from: Int, until: Int) = - new BigInteger(s.substring(from, until), 16).intValue() - def asHexa: Decoder.Result[Color.RGB] = c.as[String].right.flatMap { - case HexaColor3(hex) => - val r = parseHex(hex, 0, 1) - val g = parseHex(hex, 1, 2) - val b = parseHex(hex, 2, 3) - - Right(Color.RGB(r, g, b)) - - case HexaColor6(hex) => - val r = parseHex(hex, 0, 2) - val g = parseHex(hex, 2, 4) - val b = parseHex(hex, 4, 6) - - Right(Color.RGB(r, g, b)) - - case other => - Left(DecodingFailure(s"Unrecognized RGB color: $other", c.history)) - } - - asString - .right - .toOption - .orElse(asInt.right.toOption) - .map(Right(_)) - .getOrElse(asHexa) - } - - private def decodeNum(s: String) = { - - val intOpt = Try(s.toInt) - .toOption - - val fromDouble = Try(s.toDouble) - .toOption - .map(_.toInt) - - def fromPct = - if (s.endsWith("%")) - Try(s.stripSuffix("%").trim.toDouble) - .toOption - .map(v => (256 * v).toInt) - else - None - - intOpt - .orElse(fromDouble) - .orElse(fromPct) - } - - implicit val encodeHSL: Encoder[Color.HSL] = - Encoder[String].contramap(c => s"hsl(${c.h}, ${c.s}, ${c.l})") - - implicit val decodeHSL: Decoder[Color.HSL] = - Decoder.instance { c => - c.as[String].right.flatMap { s => - if (s.startsWith("hsl(") && s.endsWith(")")) - s.stripPrefix("hsl(").stripSuffix(")").split(',').map(_.trim).map(decodeNum) match { - case Array(Some(h), Some(s), Some(l)) => - Right(Color.HSL(h, s, l)) - case _ => - Left(DecodingFailure(s"Unrecognized HSL color: '$s'", c.history)) - } - else - Left(DecodingFailure(s"Unrecognized HSL color: '$s'", c.history)) - } - } - - implicit val elementJsonCodec: JsonSumCodecFor[Element] = - JsonSumCodecFor(jsonSumDirectCodecFor("element")) - - implicit val sequenceJsonCodec: JsonSumCodecFor[Sequence] = - JsonSumCodecFor(jsonSumDirectCodecFor("sequence")) - - implicit val boxPointsJsonCodec: JsonSumCodecFor[BoxPoints] = - JsonSumCodecFor(jsonSumDirectCodecFor("box points")) - - implicit val boxMeanJsonCodec: JsonSumCodecFor[BoxMean] = - JsonSumCodecFor(jsonSumDirectCodecFor("box mean")) - - implicit def oneOrSeqJsonCodec[T]: JsonSumCodecFor[OneOrSeq[T]] = - JsonSumCodecFor(jsonSumDirectCodecFor("one or sequence")) - - implicit val encodeScatterMode: Encoder[ScatterMode] = - flagEncoder[ScatterMode, ScatterMode.Flag](_.flags, _.label) - - implicit val decodeScatterMode: Decoder[ScatterMode] = - flagDecoder[ScatterMode, ScatterMode.Flag]("scatter mode", ScatterMode.flagMap, ScatterMode(_)) - - implicit val encodeLocalDateTime: Encoder[LocalDateTime] = - Encoder.instance { dt => - dt.toString.asJson - } - - implicit val decodeLocalDateTime: Decoder[LocalDateTime] = - Decoder.instance { c => - c.as[String].right.flatMap { s => - LocalDateTime.parse(s) match { - case Some(dt) => - Right(dt) - case None => - Left(DecodingFailure( - s"Malformed date-time: '$s'", - c.history - )) - } - } - } - - implicit val encodeError: Encoder[Error] = - Encoder.instance { error => - val json = error match { - case data: Error.Data => data.asJson - case pct: Error.Percent => pct.asJson - case cst: Error.Constant => cst.asJson - } - - json.mapObject(("type" -> error.`type`.asJson) +: _) - } - - implicit val decodeError: Decoder[Error] = - Decoder.instance { c => - c.downField("type").either match { - case Left(c0) => - Left(DecodingFailure("No type found", c0.history)) - case Right(c1) => - val c0 = c1.delete - c1.focus.as[String].right.flatMap { - case "data" => - c0.as[Error.Data].right.map(e => e: Error) - case "percent" => - c0.as[Error.Percent].right.map(e => e: Error) - case "constant" => - c0.as[Error.Constant].right.map(e => e: Error) - case unrecognized => - Left(DecodingFailure(s"Unrecognized type: $unrecognized", c.history)) - } - } - } - - implicit val jsonSumCodecForColor: JsonSumCodecFor[Color] = - JsonSumCodecFor(jsonSumDirectCodecFor("color")) - - - case class WrappedFont(font: Font) - val derivedFontDecoder = MkDecoder[Font].decoder - lazy val wrappedFontDecoder = Decoder[WrappedFont].map(_.font) - - implicit lazy val decodeFont: Decoder[Font] = - Decoder.instance { - c => - wrappedFontDecoder(c).right.toOption.fold(derivedFontDecoder(c))(Right(_)) - } - - implicit val jsonCodecForTrace = JsonSumCodecFor[Trace]( - new JsonSumTypeFieldCodec { - override def toTypeValue(name: String) = name.toLowerCase - - override def decodeField[A](name: String, cursor: HCursor, decode: Decoder[A]) = { - val c = cursor.downField(typeField) - - c.as[String] match { - case Right(name0) if toTypeValue(name) == name0 => - c.delete.as(decode).right.map(Right(_)) - case Left(_) if name == "Scatter" => // assume scatter if no type found - cursor.as(decode).right.map(Right(_)) - case _ => - Right(Left(ACursor.ok(cursor))) - } - } - } - ) - } - - import Internals._ +object Codecs extends ArgonautCodecsExtra { - implicit val encodeTrace = Encoder[Trace] - implicit val decodeTrace = Decoder[Trace] + implicit val argonautEncodeTrace = EncodeJson.of[Trace] + implicit val argonautDecodeTrace = DecodeJson.of[Trace] - implicit val encodeLayout = Encoder[Layout] - implicit val decodeLayout = Decoder[Layout] + implicit val argonautEncodeLayout = EncodeJson.of[Layout] + implicit val argonautDecodeLayout = DecodeJson.of[Layout] + implicit val argonautEncodeConfig = EncodeJson.of[Config] + implicit val argonautDecodeConfig = DecodeJson.of[Config] } diff --git a/render/shared/src/main/scala/plotly/Enumerate.scala b/render/shared/src/main/scala/plotly/Enumerate.scala index edb0cb06..2f1e97f9 100644 --- a/render/shared/src/main/scala/plotly/Enumerate.scala +++ b/render/shared/src/main/scala/plotly/Enumerate.scala @@ -1,13 +1,13 @@ package plotly -import shapeless.{ :+:, ::, CNil, Coproduct, Generic, HList, HNil, Inl, Inr, Strict } +import shapeless.{:+:, ::, CNil, Coproduct, Generic, HList, HNil, Inl, Inr, Strict} sealed abstract class Enumerate[T] { def apply(): Seq[T] } object Enumerate { - def apply[T](implicit enum: Enumerate[T]): Enumerate[T] = enum + def apply[T](implicit enumerate: Enumerate[T]): Enumerate[T] = enumerate private def instance[T](values: => Seq[T]): Enumerate[T] = new Enumerate[T] { @@ -19,11 +19,10 @@ object Enumerate { implicit val hnil: Enumerate[HNil] = instance(Seq(HNil)) - implicit def hcons[H, T <: HList] - (implicit - head: Strict[Enumerate[H]], - tail: Enumerate[T] - ): Enumerate[H :: T] = + implicit def hcons[H, T <: HList](implicit + head: Strict[Enumerate[H]], + tail: Enumerate[T] + ): Enumerate[H :: T] = instance { for { h <- head.value() @@ -33,17 +32,15 @@ object Enumerate { implicit val cnil: Enumerate[CNil] = instance(Seq()) - implicit def ccons[H, T <: Coproduct] - (implicit - head: Strict[Enumerate[H]], - tail: Enumerate[T] - ): Enumerate[H :+: T] = + implicit def ccons[H, T <: Coproduct](implicit + head: Strict[Enumerate[H]], + tail: Enumerate[T] + ): Enumerate[H :+: T] = instance(head.value().map(Inl(_)) ++ tail().map(Inr(_))) - implicit def generic[F, G] - (implicit - gen: Generic.Aux[F, G], - underlying: Strict[Enumerate[G]] - ): Enumerate[F] = + implicit def generic[F, G](implicit + gen: Generic.Aux[F, G], + underlying: Strict[Enumerate[G]] + ): Enumerate[F] = instance(underlying.value().map(gen.from)) } diff --git a/render/shared/src/main/scala/plotly/internals/ArgonautCodecsExtra.scala b/render/shared/src/main/scala/plotly/internals/ArgonautCodecsExtra.scala new file mode 100644 index 00000000..53f8681b --- /dev/null +++ b/render/shared/src/main/scala/plotly/internals/ArgonautCodecsExtra.scala @@ -0,0 +1,12 @@ +package plotly.internals + +import argonaut.{DecodeJson, EncodeJson} + +trait ArgonautCodecsExtra { + + implicit def seqEncoder[T: EncodeJson]: EncodeJson[Seq[T]] = + EncodeJson.of[Vector[T]].contramap(_.toVector) + implicit def seqDecoder[T: DecodeJson]: DecodeJson[Seq[T]] = + DecodeJson.of[Vector[T]].map(x => x) + +} diff --git a/render/shared/src/main/scala/plotly/internals/ArgonautCodecsInternals.scala b/render/shared/src/main/scala/plotly/internals/ArgonautCodecsInternals.scala new file mode 100755 index 00000000..af2ebc6a --- /dev/null +++ b/render/shared/src/main/scala/plotly/internals/ArgonautCodecsInternals.scala @@ -0,0 +1,516 @@ +package plotly +package internals + +import java.math.BigInteger + +import argonaut._ +import argonaut.Argonaut._ +import argonaut.ArgonautShapeless._ +import argonaut.derive._ +import shapeless._ + +import scala.util.Try +import plotly.element._ +import plotly.layout._ + +object ArgonautCodecsInternals extends ArgonautCodecsExtra { + + sealed abstract class IsWrapper[W] + + implicit def isWrapperEncode[W, L <: HList, T](implicit + ev: IsWrapper[W], + gen: Generic.Aux[W, L], + isHCons: ops.hlist.IsHCons.Aux[L, T, HNil], + underlying: EncodeJson[T] + ): EncodeJson[W] = + EncodeJson { w => + val t = isHCons.head(gen.to(w)) + t.asJson + } + + implicit def isWrapperDecode[W, L <: HList, T](implicit + ev: IsWrapper[W], + gen: Generic.Aux[W, L], + isHCons: ops.hlist.IsHCons.Aux[L, T, HNil], + underlying: DecodeJson[T] + ): DecodeJson[W] = + DecodeJson { c => + c.as[T] + .map(t => gen.from((t :: HNil).asInstanceOf[L]) // FIXME + ) + } + + implicit val boxMeanBoolIsWrapper: IsWrapper[BoxMean.Bool] = null + implicit val boxPointsBoolIsWrapper: IsWrapper[BoxPoints.Bool] = null + implicit val sequenceDoublesIsWrapper: IsWrapper[Sequence.Doubles] = null + implicit val sequenceNestedDoublesIsWrapper: IsWrapper[Sequence.NestedDoubles] = null + implicit val sequenceNestedIntsIsWrapper: IsWrapper[Sequence.NestedInts] = null + implicit val sequenceStringsIsWrapper: IsWrapper[Sequence.Strings] = null + implicit val sequenceDatetimesIsWrapper: IsWrapper[Sequence.DateTimes] = null + implicit val rangeDoublesIsWrapper: IsWrapper[Range.Doubles] = null + implicit val rangeDatetimesIsWrapper: IsWrapper[Range.DateTimes] = null + implicit val doubleElementIsWrapper: IsWrapper[Element.DoubleElement] = null + implicit val stringElementIsWrapper: IsWrapper[Element.StringElement] = null + implicit def oneOrSeqOneIsWrapper[T]: IsWrapper[OneOrSeq.One[T]] = null + implicit def oneOrSeqSequenceIsWrapper[T]: IsWrapper[OneOrSeq.Sequence[T]] = null + + def flagEncoder[T, F](flags: T => Set[F], label: F => String): EncodeJson[T] = + EncodeJson { t => + val s = flags(t).toSeq match { + case Seq() => "none" + case nonEmpty => nonEmpty.map(label).mkString("+") + } + + s.asJson + } + + def flagDecoder[T, F](type0: String, map: Map[String, F], build: Set[F] => T): DecodeJson[T] = + DecodeJson { c => + c.as[String].flatMap { s => + val flags = + if (s == "none") + DecodeResult.ok(Set.empty[F]) + else + s.split('+').foldLeft[DecodeResult[Set[F]]](DecodeResult.ok(Set.empty[F])) { case (acc, f) => + for { + acc0 <- acc + f0 <- + map + .get(f) + .fold[DecodeResult[F]](DecodeResult.fail(s"Unrecognized $type0: $f", c.history))(DecodeResult.ok) + } yield acc0 + f0 + } + + flags.map(build) + } + } + + sealed abstract class IsEnum[-T] { + def label(t: T): String + } + + object IsEnum { + def apply[T](implicit isEnum: IsEnum[T]): IsEnum[T] = isEnum + + def instance[T](f: T => String): IsEnum[T] = + new IsEnum[T] { + def label(t: T): String = f(t) + } + } + + implicit def isEnumEncoder[T: IsEnum]: EncodeJson[T] = + EncodeJson.of[String].contramap(IsEnum[T].label) + + implicit def isEnumDecoder[T](implicit + isEnum: IsEnum[T], + enumerate: Enumerate[T], + typeable: Typeable[T] + ): DecodeJson[T] = + DecodeJson { + val underlying = DecodeJson.of[String] + val map = enumerate().map(e => isEnum.label(e) -> e).toMap + val name = typeable.describe // TODO split in words + + c => + underlying(c).flatMap { s => + map.get(s) match { + case None => DecodeResult.fail(s"Unrecognized $name: '$s'", c.history) + case Some(m) => DecodeResult.ok(m) + } + } + } + + implicit val anchorIsEnum = IsEnum.instance[Anchor](_.label) + implicit val refIsEnum = IsEnum.instance[Ref](_.label) + implicit val axisAnchorIsEnum = IsEnum.instance[AxisAnchor](_.label) + implicit val axisReferenceIsEnum = IsEnum.instance[AxisReference](_.label) + implicit val axisTypeIsEnum = IsEnum.instance[AxisType](_.label) + implicit val barModeIsEnum = IsEnum.instance[BarMode](_.label) + implicit val boxModeIsEnum = IsEnum.instance[BoxMode](_.label) + implicit val dashIsEnum = IsEnum.instance[Dash](_.label) + implicit val fillIsEnum = IsEnum.instance[Fill](_.label) + implicit val hoverModeIsEnum = IsEnum.instance[HoverMode](_.label) + implicit val lineShapeIsEnum = IsEnum.instance[LineShape](_.label) + implicit val orientationIsEnum = IsEnum.instance[Orientation](_.label) + implicit val traceOrderIsEnum = IsEnum.instance[TraceOrder](_.label) + implicit val boxMeanOtherIsEnum = IsEnum.instance[BoxMean.Labeled](_.label) + implicit val boxPointsOtherIsEnum = IsEnum.instance[BoxPoints.Labeled](_.label) + implicit val textPositionIsEnum = IsEnum.instance[TextPosition](_.label) + implicit val barTextPositionIsEnum = IsEnum.instance[BarTextPosition](_.label) + implicit val sideIsEnum = IsEnum.instance[Side](_.label) + implicit val symbolIsEnum = IsEnum.instance[Symbol](_.label) + implicit val ticksIsEnum = IsEnum.instance[Ticks](_.label) + implicit val histNormIsEnum = IsEnum.instance[HistNorm](_.label) + implicit val sizeModeIsEnum = IsEnum.instance[SizeMode](_.label) + implicit val hoverOnIsEnum = IsEnum.instance[HoverOn](_.label) + implicit val groupNormIsEnum = IsEnum.instance[GroupNorm](_.label) + implicit val histFuncIsEnum = IsEnum.instance[HistFunc](_.label) + implicit val tickModeIsEnum = IsEnum.instance[TickMode](_.mode) + implicit val patternIsEnum = IsEnum.instance[Pattern](_.label) + implicit val rowOrderIsEnum = IsEnum.instance[RowOrder](_.label) + implicit val alignmentIsEnum = IsEnum.instance[Alignment](_.label) + implicit val colorModelIsEnum = IsEnum.instance[ColorModel](_.label) + + def jsonSumDirectCodecFor(name: String): JsonSumCodec = new JsonSumCodec { + def encodeEmpty: Nothing = + throw new IllegalArgumentException(s"empty $name") + + def encodeField(fieldOrObj: Either[Json, (String, Json)]): Json = + fieldOrObj match { + case Left(other) => other + case Right((_, content)) => content + } + + def decodeEmpty(cursor: HCursor): DecodeResult[Nothing] = + // FIXME Sometimes reports the wrong error (in case of two nested sum types) + DecodeResult.fail(s"unrecognized $name", cursor.history) + + def decodeField[A](name: String, cursor: HCursor, decode: DecodeJson[A]): DecodeResult[Either[ACursor, A]] = + DecodeResult.ok { + val o = decode(cursor) + o.toOption + .toRight(ACursor.ok(cursor)) + } + } + + case class JsonProductObjCodecNoEmpty( + toJsonName: String => String = identity + ) extends JsonProductCodec { + + private val underlying = JsonProductCodec.adapt(toJsonName) + + val encodeEmpty: Json = underlying.encodeEmpty + + def encodeField(field: (String, Json), obj: Json, default: => Option[Json]): Json = + underlying.encodeField(field, obj, default) + + def decodeEmpty(cursor: HCursor): DecodeResult[Unit] = + if (cursor.focus == Json.obj()) + DecodeResult.ok(()) + else + DecodeResult.fail( + s"Found extra fields: ${cursor.fields.toSeq.flatten.mkString(", ")}", + cursor.history + ) + + def decodeField[A]( + name: String, + cursor: HCursor, + decode: DecodeJson[A], + default: Option[A] + ): DecodeResult[(A, ACursor)] = { + val c = cursor.downField(toJsonName(name)) + + def result = c.as(decode).map((_, if (c.succeeded) c.delete else ACursor.ok(cursor))) + + default match { + case None => result + case Some(d) => + if (c.succeeded) + result + else + DecodeResult.ok((d, ACursor.ok(cursor))) + } + } + } + + object JsonProductObjCodecNoEmpty { + val default = JsonProductObjCodecNoEmpty() + } + + implicit val encodeHoverInfo: EncodeJson[HoverInfo] = + EncodeJson.of[String].contramap(_.label) + implicit val decodeHoverInfo: DecodeJson[HoverInfo] = + DecodeJson { c => + DecodeJson.of[String].apply(c).flatMap { + case "all" => DecodeResult.ok(HoverInfo.All) + case "skip" => DecodeResult.ok(HoverInfo.Skip) + case "none" => DecodeResult.ok(HoverInfo.None) + case combination => + val results = combination.split('+').map { + case "x" => Right(HoverInfo.X) + case "y" => Right(HoverInfo.Y) + case "z" => Right(HoverInfo.Z) + case "color" => Right(HoverInfo.Color) + case "text" => Right(HoverInfo.Text) + case "name" => Right(HoverInfo.Name) + case other => Left(s"Unrecognized hover info element: $other") + } + if (results.exists(_.isLeft)) + DecodeResult.fail( + s"Unrecognized hover info elements: ${results.flatMap(_.left.toSeq).mkString(", ")}", + c.history + ) + else + DecodeResult.ok(HoverInfo(results.flatMap(_.toSeq).toIndexedSeq: _*)) + } + } + + implicit def defaultJsonProductCodecFor[T]: JsonProductCodecFor[T] = + JsonProductCodecFor(JsonProductObjCodecNoEmpty.default) + + implicit val encodeRGBA: EncodeJson[Color.RGBA] = + EncodeJson.of[String].contramap(c => s"rgba(${c.r}, ${c.g}, ${c.b}, ${c.alpha})") + + implicit val decodeRGBA: DecodeJson[Color.RGBA] = + DecodeJson { c => + c.as[String].flatMap { s => + if (s.startsWith("rgba(") && s.endsWith(")")) + s.stripPrefix("rgba(").stripSuffix(")").split(',').map(_.trim) match { + case Array(rStr, gStr, bStr, alphaStr) => + val res = for { + r <- Try(rStr.toInt).toOption + g <- Try(gStr.toInt).toOption + b <- Try(bStr.toInt).toOption + alpha <- Try(alphaStr.toDouble).toOption + } yield DecodeResult.ok(Color.RGBA(r, g, b, alpha)) + + res.getOrElse { + DecodeResult.fail(s"Unrecognized RGBA color: '$s'", c.history) + } + case _ => + DecodeResult.fail(s"Unrecognized RGBA color: '$s'", c.history) + } + else + DecodeResult.fail(s"Unrecognized RGBA color: '$s'", c.history) + } + } + + implicit val encodeStringColor: EncodeJson[Color.StringColor] = + EncodeJson.of[String].contramap(_.color) + + implicit val decodeStringColor: DecodeJson[Color.StringColor] = + DecodeJson { + val underlying = DecodeJson.of[String] + val map = Color.StringColor.colors.toVector + .map(c => c -> Color.StringColor(c)) + .toMap + + c => + underlying(c).flatMap { s => + map.get(s) match { + case None => DecodeResult.fail(s"Unrecognized color: '$s'", c.history) + case Some(m) => DecodeResult.ok(m) + } + } + } + + private val HexaColor3 = "#([0-9a-fA-F]{3})".r + private val HexaColor6 = "#([0-9a-fA-F]{6})".r + + implicit val encodeRGB: EncodeJson[Color.RGB] = + EncodeJson.of[String].contramap(c => s"rgb(${c.r}, ${c.g}, ${c.b})") + + implicit val decodeRGB: DecodeJson[Color.RGB] = + DecodeJson { c => + val asString: DecodeResult[Color.RGB] = c.as[String].flatMap { s => + if (s.startsWith("rgb(") && s.endsWith(")")) + s.stripPrefix("rgb(").stripSuffix(")").split(',').map(_.trim).map(s => Try(s.toInt).toOption) match { + case Array(Some(r), Some(g), Some(b)) => + DecodeResult.ok(Color.RGB(r, g, b)) + case _ => + DecodeResult.fail(s"Unrecognized RGB color: '$s'", c.history) + } + else + DecodeResult.fail(s"Unrecognized RGB color: '$s'", c.history) + } + def asInt: DecodeResult[Color.RGB] = c.as[Int].flatMap { + case r if r >= 0 && r < 256 => + DecodeResult.ok(Color.RGB(r, 0, 0)) + case _ => + DecodeResult.fail(s"Unrecognized RGB color: ${c.focus}", c.history) + } + + def parseHex(s: String, from: Int, until: Int) = + new BigInteger(s.substring(from, until), 16).intValue() + def asHexa: DecodeResult[Color.RGB] = c.as[String].flatMap { + case HexaColor3(hex) => + val r = parseHex(hex, 0, 1) + val g = parseHex(hex, 1, 2) + val b = parseHex(hex, 2, 3) + + DecodeResult.ok(Color.RGB(r, g, b)) + + case HexaColor6(hex) => + val r = parseHex(hex, 0, 2) + val g = parseHex(hex, 2, 4) + val b = parseHex(hex, 4, 6) + + DecodeResult.ok(Color.RGB(r, g, b)) + + case other => + DecodeResult.fail(s"Unrecognized RGB color: $other", c.history) + } + + asString.toOption + .orElse(asInt.toOption) + .fold(asHexa)(DecodeResult.ok) + } + + private def decodeNum(s: String) = { + + val intOpt = Try(s.toInt).toOption + + val fromDouble = Try(s.toDouble).toOption + .map(_.toInt) + + def fromPct = + if (s.endsWith("%")) + Try(s.stripSuffix("%").trim.toDouble).toOption + .map(v => (256 * v).toInt) + else + None + + intOpt + .orElse(fromDouble) + .orElse(fromPct) + } + + implicit val encodeHSL: EncodeJson[Color.HSL] = + EncodeJson.of[String].contramap(c => s"hsl(${c.h}, ${c.s}, ${c.l})") + + implicit val decodeHSL: DecodeJson[Color.HSL] = + DecodeJson { c => + c.as[String].flatMap { s => + if (s.startsWith("hsl(") && s.endsWith(")")) + s.stripPrefix("hsl(").stripSuffix(")").split(',').map(_.trim).map(decodeNum) match { + case Array(Some(h), Some(s), Some(l)) => + DecodeResult.ok(Color.HSL(h, s, l)) + case _ => + DecodeResult.fail(s"Unrecognized HSL color: '$s'", c.history) + } + else + DecodeResult.fail(s"Unrecognized HSL color: '$s'", c.history) + } + } + + implicit val encodeNamedColorScale: EncodeJson[ColorScale.NamedScale] = + EncodeJson.of[String].contramap(_.name) + + implicit val decodeNamedColorScale: DecodeJson[ColorScale.NamedScale] = + DecodeJson { c => + c.as[String].flatMap { s => + // TODO: Add colorscale name enum? + DecodeResult.ok(ColorScale.NamedScale(s)) + } + } + + implicit val encodeCustomColorScale: EncodeJson[ColorScale.CustomScale] = + EncodeJson.of[Json].contramap(_.values.toList.asJson) + + implicit val decodeCustomColorScale: DecodeJson[ColorScale.CustomScale] = + DecodeJson { c => + c.as[Seq[(Double, Color)]].flatMap { s => + DecodeResult.ok(ColorScale.CustomScale(s)) + } + } + + implicit val colorscaleJsonCodec: JsonSumCodecFor[ColorScale] = + JsonSumCodecFor(jsonSumDirectCodecFor("colorscale")) + + implicit val elementJsonCodec: JsonSumCodecFor[Element] = + JsonSumCodecFor(jsonSumDirectCodecFor("element")) + + implicit val sequenceJsonCodec: JsonSumCodecFor[Sequence] = + JsonSumCodecFor(jsonSumDirectCodecFor("sequence")) + + implicit val rangeJsonCodec: JsonSumCodecFor[Range] = + JsonSumCodecFor(jsonSumDirectCodecFor("range")) + + implicit val boxPointsJsonCodec: JsonSumCodecFor[BoxPoints] = + JsonSumCodecFor(jsonSumDirectCodecFor("box points")) + + implicit val boxMeanJsonCodec: JsonSumCodecFor[BoxMean] = + JsonSumCodecFor(jsonSumDirectCodecFor("box mean")) + + implicit def oneOrSeqJsonCodec[T]: JsonSumCodecFor[OneOrSeq[T]] = + JsonSumCodecFor(jsonSumDirectCodecFor("one or sequence")) + + implicit val encodeScatterMode: EncodeJson[ScatterMode] = + flagEncoder[ScatterMode, ScatterMode.Flag](_.flags, _.label) + + implicit val decodeScatterMode: DecodeJson[ScatterMode] = + flagDecoder[ScatterMode, ScatterMode.Flag]("scatter mode", ScatterMode.flagMap, ScatterMode(_)) + + implicit val encodeLocalDateTime: EncodeJson[LocalDateTime] = + EncodeJson { dt => + dt.toString.asJson + } + + implicit val decodeLocalDateTime: DecodeJson[LocalDateTime] = + DecodeJson { c => + c.as[String].flatMap { s => + LocalDateTime.parse(s) match { + case Some(dt) => + DecodeResult.ok(dt) + case None => + DecodeResult.fail( + s"Malformed date-time: '$s'", + c.history + ) + } + } + } + + implicit val encodeError: EncodeJson[Error] = + EncodeJson { error => + val json = error match { + case data: Error.Data => data.asJson + case pct: Error.Percent => pct.asJson + case cst: Error.Constant => cst.asJson + } + + json.obj.fold(json)(o => Json.jObject(("type" -> error.`type`.asJson) +: o)) + } + + implicit val decodeError: DecodeJson[Error] = + DecodeJson { c => + c.downField("type").success match { + case None => + DecodeResult.fail("No type found", c.history) + case Some(c1) => + val c0 = c1.delete + c1.focus.as[String].flatMap { + case "data" => + c0.as[Error.Data].map(e => e: Error) + case "percent" => + c0.as[Error.Percent].map(e => e: Error) + case "constant" => + c0.as[Error.Constant].map(e => e: Error) + case unrecognized => + DecodeResult.fail(s"Unrecognized type: $unrecognized", c.history) + } + } + } + + implicit val jsonSumCodecForColor: JsonSumCodecFor[Color] = + JsonSumCodecFor(jsonSumDirectCodecFor("color")) + + case class WrappedFont(font: Font) + val derivedFontDecoder = MkDecodeJson[Font].decodeJson + lazy val wrappedFontDecoder = DecodeJson.of[WrappedFont].map(_.font) + + implicit lazy val decodeFont: DecodeJson[Font] = + DecodeJson { c => + wrappedFontDecoder(c).toOption.fold(derivedFontDecoder(c))(DecodeResult.ok) + } + + implicit val jsonCodecForTrace = JsonSumCodecFor[Trace]( + new JsonSumTypeFieldCodec { + override def toTypeValue(name: String) = name.toLowerCase + + override def decodeField[A](name: String, cursor: HCursor, decode: DecodeJson[A]) = { + val c = cursor.downField(typeField) + + c.as[String].toEither match { + case Right(name0) if toTypeValue(name) == name0 => + c.delete.as(decode).map(Right(_)) + case Left(_) if name == "Scatter" => // assume scatter if no type found + cursor.as(decode).map(Right(_)) + case _ => + DecodeResult.ok(Left(ACursor.ok(cursor))) + } + } + } + ) +} diff --git a/render/shared/src/main/scala/plotly/internals/BetterPrinter.scala b/render/shared/src/main/scala/plotly/internals/BetterPrinter.scala new file mode 100644 index 00000000..80120787 --- /dev/null +++ b/render/shared/src/main/scala/plotly/internals/BetterPrinter.scala @@ -0,0 +1,167 @@ +package plotly.internals + +import argonaut._ +import argonaut.PrettyParams.vectorMemo + +final case class BetterPrinter(params: PrettyParams) { + + import params._ + + private def addIndentation(s: String): Int => String = { + val lastNewLineIndex = s.lastIndexOf("\n") + if (lastNewLineIndex < 0) { _ => + s + } else { + val afterLastNewLineIndex = lastNewLineIndex + 1 + val start = s.substring(0, afterLastNewLineIndex) + val end = s.substring(afterLastNewLineIndex) + n => start + indent * n + end + } + } + + private val openBraceText = "{" + private val closeBraceText = "}" + private val openArrayText = "[" + private val closeArrayText = "]" + private val commaText = "," + private val colonText = ":" + private val nullText = "null" + private val trueText = "true" + private val falseText = "false" + private val stringEnclosureText = "\"" + + private val _lbraceLeft = addIndentation(lbraceLeft) + private val _lbraceRight = addIndentation(lbraceRight) + private val _rbraceLeft = addIndentation(rbraceLeft) + private val _rbraceRight = addIndentation(rbraceRight) + private val _lbracketLeft = addIndentation(lbracketLeft) + private val _lbracketRight = addIndentation(lbracketRight) + private val _rbracketLeft = addIndentation(rbracketLeft) + private val _rbracketRight = addIndentation(rbracketRight) + private val _lrbracketsEmpty = addIndentation(lrbracketsEmpty) + private val _arrayCommaLeft = addIndentation(arrayCommaLeft) + private val _arrayCommaRight = addIndentation(arrayCommaRight) + private val _objectCommaLeft = addIndentation(objectCommaLeft) + private val _objectCommaRight = addIndentation(objectCommaRight) + private val _colonLeft = addIndentation(colonLeft) + private val _colonRight = addIndentation(colonRight) + + private val lbraceMemo = vectorMemo { depth: Int => + "%s%s%s".format(_lbraceLeft(depth), openBraceText, _lbraceRight(depth + 1)) + } + private val rbraceMemo = vectorMemo { depth: Int => + "%s%s%s".format(_rbraceLeft(depth), closeBraceText, _rbraceRight(depth + 1)) + } + + private val lbracketMemo = vectorMemo { depth: Int => + "%s%s%s".format(_lbracketLeft(depth), openArrayText, _lbracketRight(depth + 1)) + } + private val rbracketMemo = vectorMemo { depth: Int => + "%s%s%s".format(_rbracketLeft(depth), closeArrayText, _rbracketRight(depth)) + } + private val lrbracketsEmptyMemo = vectorMemo { depth: Int => + "%s%s%s".format(openArrayText, _lrbracketsEmpty(depth), closeArrayText) + } + private val arrayCommaMemo = vectorMemo { depth: Int => + "%s%s%s".format(_arrayCommaLeft(depth + 1), commaText, _arrayCommaRight(depth + 1)) + } + private val objectCommaMemo = vectorMemo { depth: Int => + "%s%s%s".format(_objectCommaLeft(depth + 1), commaText, _objectCommaRight(depth + 1)) + } + private val colonMemo = vectorMemo { depth: Int => + "%s%s%s".format(_colonLeft(depth + 1), colonText, _colonRight(depth + 1)) + } + + /** Returns a string representation of a pretty-printed JSON value. + */ + def render(j: Json): String = { + + import Json._ + import StringEscaping._ + + def appendJsonString(builder: StringBuilder, jsonString: String): StringBuilder = { + for (c <- jsonString) { + if (isNormalChar(c)) + builder += c + else + builder.append(escape(c)) + } + + builder + } + + def encloseJsonString(builder: StringBuilder, jsonString: JsonString): StringBuilder = { + appendJsonString(builder.append(stringEnclosureText), jsonString).append(stringEnclosureText) + } + + def trav(builder: StringBuilder, depth: Int, k: Json): StringBuilder = { + + def lbrace(builder: StringBuilder): StringBuilder = { + builder.append(lbraceMemo(depth)) + } + def rbrace(builder: StringBuilder): StringBuilder = { + builder.append(rbraceMemo(depth)) + } + def lbracket(builder: StringBuilder): StringBuilder = { + builder.append(lbracketMemo(depth)) + } + def rbracket(builder: StringBuilder): StringBuilder = { + builder.append(rbracketMemo(depth)) + } + def lrbracketsEmpty(builder: StringBuilder): StringBuilder = { + builder.append(lrbracketsEmptyMemo(depth)) + } + def arrayComma(builder: StringBuilder): StringBuilder = { + builder.append(arrayCommaMemo(depth)) + } + def objectComma(builder: StringBuilder): StringBuilder = { + builder.append(objectCommaMemo(depth)) + } + def colon(builder: StringBuilder): StringBuilder = { + builder.append(colonMemo(depth)) + } + + k.fold[StringBuilder]( + builder.append(nullText), + bool => builder.append(if (bool) trueText else falseText), + n => + n match { + case JsonLong(x) => builder append x.toString + case JsonDecimal(x) => builder append x + case JsonBigDecimal(x) => builder append x.toString + }, + s => encloseJsonString(builder, s), + e => + if (e.isEmpty) { + lrbracketsEmpty(builder) + } else { + rbracket( + e.foldLeft((true, lbracket(builder))) { case ((firstElement, builder), subElement) => + val withComma = if (firstElement) builder else arrayComma(builder) + val updatedBuilder = trav(withComma, depth + 1, subElement) + (false, updatedBuilder) + }._2 + ) + }, + o => { + rbrace( + (if (preserveOrder) o.toList else o.toMap) + .foldLeft((true, lbrace(builder))) { case ((firstElement, builder), (key, value)) => + val ignoreKey = dropNullKeys && value.isNull + if (ignoreKey) { + (firstElement, builder) + } else { + val withComma = if (firstElement) builder else objectComma(builder) + (false, trav(colon(encloseJsonString(withComma, key)), depth + 1, value)) + } + } + ._2 + ) + } + ) + } + + trav(new StringBuilder(), 0, j).toString() + } + +} diff --git a/sbt b/sbt new file mode 100755 index 00000000..9fc15a12 --- /dev/null +++ b/sbt @@ -0,0 +1,704 @@ +#!/usr/bin/env bash +# +# A more capable sbt runner, coincidentally also called sbt. +# Author: Paul Phillips +# https://github.com/paulp/sbt-extras +# +# Generated from http://www.opensource.org/licenses/bsd-license.php +# Copyright (c) 2011, Paul Phillips. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +set -o pipefail + +declare -r sbt_release_version="1.4.0" +declare -r sbt_unreleased_version="1.4.0" + +declare -r latest_213="2.13.3" +declare -r latest_212="2.12.12" +declare -r latest_211="2.11.12" +declare -r latest_210="2.10.7" +declare -r latest_29="2.9.3" +declare -r latest_28="2.8.2" + +declare -r buildProps="project/build.properties" + +declare -r sbt_launch_ivy_release_repo="https://repo.typesafe.com/typesafe/ivy-releases" +declare -r sbt_launch_ivy_snapshot_repo="https://repo.scala-sbt.org/scalasbt/ivy-snapshots" +declare -r sbt_launch_mvn_release_repo="https://repo.scala-sbt.org/scalasbt/maven-releases" +declare -r sbt_launch_mvn_snapshot_repo="https://repo.scala-sbt.org/scalasbt/maven-snapshots" + +declare -r default_jvm_opts_common="-Xms512m -Xss2m -XX:MaxInlineLevel=18" +declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy -Dsbt.coursier.home=project/.coursier" + +declare -r default_coursier_launcher_version="1.2.22" +declare coursier_launcher_version="default" + +declare sbt_jar sbt_dir sbt_create sbt_version sbt_script sbt_new +declare sbt_explicit_version +declare verbose noshare batch trace_level + +declare java_cmd="java" +declare sbt_launch_dir="$HOME/.sbt/launchers" +declare sbt_launch_repo + +# pull -J and -D options to give to java. +declare -a java_args scalac_args sbt_commands residual_args + +# args to jvm/sbt via files or environment variables +declare -a extra_jvm_opts extra_sbt_opts + +echoerr() { echo >&2 "$@"; } +vlog() { [[ -n "$verbose" ]] && echoerr "$@"; } +die() { + echo "Aborting: $*" + exit 1 +} + +setTrapExit() { + # save stty and trap exit, to ensure echo is re-enabled if we are interrupted. + SBT_STTY="$(stty -g 2>/dev/null)" + export SBT_STTY + + # restore stty settings (echo in particular) + onSbtRunnerExit() { + [ -t 0 ] || return + vlog "" + vlog "restoring stty: $SBT_STTY" + stty "$SBT_STTY" + } + + vlog "saving stty: $SBT_STTY" + trap onSbtRunnerExit EXIT +} + +# this seems to cover the bases on OSX, and someone will +# have to tell me about the others. +get_script_path() { + local path="$1" + [[ -L "$path" ]] || { + echo "$path" + return + } + + local -r target="$(readlink "$path")" + if [[ "${target:0:1}" == "/" ]]; then + echo "$target" + else + echo "${path%/*}/$target" + fi +} + +script_path="$(get_script_path "${BASH_SOURCE[0]}")" +declare -r script_path +script_name="${script_path##*/}" +declare -r script_name + +init_default_option_file() { + local overriding_var="${!1}" + local default_file="$2" + if [[ ! -r "$default_file" && "$overriding_var" =~ ^@(.*)$ ]]; then + local envvar_file="${BASH_REMATCH[1]}" + if [[ -r "$envvar_file" ]]; then + default_file="$envvar_file" + fi + fi + echo "$default_file" +} + +sbt_opts_file="$(init_default_option_file SBT_OPTS .sbtopts)" +sbtx_opts_file="$(init_default_option_file SBTX_OPTS .sbtxopts)" +jvm_opts_file="$(init_default_option_file JVM_OPTS .jvmopts)" + +build_props_sbt() { + [[ -r "$buildProps" ]] && + grep '^sbt\.version' "$buildProps" | tr '=\r' ' ' | awk '{ print $2; }' +} + +set_sbt_version() { + sbt_version="${sbt_explicit_version:-$(build_props_sbt)}" + [[ -n "$sbt_version" ]] || sbt_version=$sbt_release_version + export sbt_version +} + +url_base() { + local version="$1" + + case "$version" in + 0.7.*) echo "https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/simple-build-tool" ;; + 0.10.*) echo "$sbt_launch_ivy_release_repo" ;; + 0.11.[12]) echo "$sbt_launch_ivy_release_repo" ;; + 0.*-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) # ie "*-yyyymmdd-hhMMss" + echo "$sbt_launch_ivy_snapshot_repo" ;; + 0.*) echo "$sbt_launch_ivy_release_repo" ;; + *-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]) # ie "*-yyyymmddThhMMss" + echo "$sbt_launch_mvn_snapshot_repo" ;; + *) echo "$sbt_launch_mvn_release_repo" ;; + esac +} + +make_url() { + local version="$1" + + local base="${sbt_launch_repo:-$(url_base "$version")}" + + case "$version" in + 0.7.*) echo "$base/sbt-launch-0.7.7.jar" ;; + 0.10.*) echo "$base/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; + 0.11.[12]) echo "$base/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; + 0.*) echo "$base/org.scala-sbt/sbt-launch/$version/sbt-launch.jar" ;; + *) echo "$base/org/scala-sbt/sbt-launch/$version/sbt-launch-${version}.jar" ;; + esac +} + +make_coursier_url () { + local version="$1" + + echo "https://github.com/coursier/sbt-launcher/releases/download/v$version/csbt" +} + +enable_coursier () { + if [[ -z "$coursier_launcher_version" ]]; then + coursier_launcher_version="$default_coursier_launcher_version" + fi +} + +addJava() { + vlog "[addJava] arg = '$1'" + java_args+=("$1") +} +addSbt() { + vlog "[addSbt] arg = '$1'" + sbt_commands+=("$1") +} +addScalac() { + vlog "[addScalac] arg = '$1'" + scalac_args+=("$1") +} +addResidual() { + vlog "[residual] arg = '$1'" + residual_args+=("$1") +} + +addResolver() { addSbt "set resolvers += $1"; } + +addDebugger() { addJava "-Xdebug" && addJava "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1"; } + +setThisBuild() { + vlog "[addBuild] args = '$*'" + local key="$1" && shift + addSbt "set $key in ThisBuild := $*" +} +setScalaVersion() { + [[ "$1" == *"-SNAPSHOT" ]] && addResolver 'Resolver.sonatypeRepo("snapshots")' + addSbt "++ $1" +} +setJavaHome() { + java_cmd="$1/bin/java" + setThisBuild javaHome "_root_.scala.Some(file(\"$1\"))" + export JAVA_HOME="$1" + export JDK_HOME="$1" + export PATH="$JAVA_HOME/bin:$PATH" +} + +getJavaVersion() { + local -r str=$("$1" -version 2>&1 | grep -E -e '(java|openjdk) version' | awk '{ print $3 }' | tr -d '"') + + # java -version on java8 says 1.8.x + # but on 9 and 10 it's 9.x.y and 10.x.y. + if [[ "$str" =~ ^1\.([0-9]+)(\..*)?$ ]]; then + echo "${BASH_REMATCH[1]}" + elif [[ "$str" =~ ^([0-9]+)(\..*)?$ ]]; then + echo "${BASH_REMATCH[1]}" + elif [[ -n "$str" ]]; then + echoerr "Can't parse java version from: $str" + fi +} + +checkJava() { + # Warn if there is a Java version mismatch between PATH and JAVA_HOME/JDK_HOME + + [[ -n "$JAVA_HOME" && -e "$JAVA_HOME/bin/java" ]] && java="$JAVA_HOME/bin/java" + [[ -n "$JDK_HOME" && -e "$JDK_HOME/lib/tools.jar" ]] && java="$JDK_HOME/bin/java" + + if [[ -n "$java" ]]; then + pathJavaVersion=$(getJavaVersion java) + homeJavaVersion=$(getJavaVersion "$java") + if [[ "$pathJavaVersion" != "$homeJavaVersion" ]]; then + echoerr "Warning: Java version mismatch between PATH and JAVA_HOME/JDK_HOME, sbt will use the one in PATH" + echoerr " Either: fix your PATH, remove JAVA_HOME/JDK_HOME or use -java-home" + echoerr " java version from PATH: $pathJavaVersion" + echoerr " java version from JAVA_HOME/JDK_HOME: $homeJavaVersion" + fi + fi +} + +java_version() { + local -r version=$(getJavaVersion "$java_cmd") + vlog "Detected Java version: $version" + echo "$version" +} + +# MaxPermSize critical on pre-8 JVMs but incurs noisy warning on 8+ +default_jvm_opts() { + local -r v="$(java_version)" + if [[ $v -ge 10 ]]; then + echo "$default_jvm_opts_common -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler" + elif [[ $v -ge 8 ]]; then + echo "$default_jvm_opts_common" + else + echo "-XX:MaxPermSize=384m $default_jvm_opts_common" + fi +} + +execRunner() { + # print the arguments one to a line, quoting any containing spaces + vlog "# Executing command line:" && { + for arg; do + if [[ -n "$arg" ]]; then + if printf "%s\n" "$arg" | grep -q ' '; then + printf >&2 "\"%s\"\n" "$arg" + else + printf >&2 "%s\n" "$arg" + fi + fi + done + vlog "" + } + + setTrapExit + + if [[ -n "$batch" ]]; then + "$@" /dev/null 2>&1; then + curl --fail --silent --location "$url" --output "$jar" + elif command -v wget >/dev/null 2>&1; then + wget -q -O "$jar" "$url" + fi + } && [[ -r "$jar" ]] +} + +acquire_sbt_jar() { + + # if none of the options touched coursier_launcher_version, use the coursier + # launcher with sbt >= 0.13.8 + if [[ "$coursier_launcher_version" = "default" ]]; then + case "$sbt_version" in + 0.13.[89] | 0.13.1[0-9] | 1.* ) coursier_launcher_version="$default_coursier_launcher_version" ;; + * ) coursier_launcher_version="" ;; + esac + fi + + { + if [[ -z "$coursier_launcher_version" ]]; then + sbt_jar="$(jar_file "$sbt_version")" + else + sbt_jar="$(jar_file "coursier_$coursier_launcher_version")" + fi + + [[ -r "$sbt_jar" ]] + } || { + sbt_jar="$HOME/.ivy2/local/org.scala-sbt/sbt-launch/$sbt_version/jars/sbt-launch.jar" + [[ -z "$coursier_launcher_version" && -r "$sbt_jar" ]] + } || { + if [[ -z "$coursier_launcher_version" ]]; then + sbt_jar="$(jar_file "$sbt_version")" + jar_url="$(make_url "$sbt_version")" + + download_url "${jar_url}" "${sbt_jar}" \ + "Downloading sbt launcher for ${sbt_version}:" + + case "${sbt_version}" in + 0.*) + vlog "SBT versions < 1.0 do not have published MD5 checksums, skipping check" + echo "" + ;; + *) verify_sbt_jar "${sbt_jar}" ;; + esac + else + sbt_jar="$(jar_file "coursier_$coursier_launcher_version")" + download_url "$(make_coursier_url "$coursier_launcher_version")" "$sbt_jar" \ + "Downloading coursier sbt launcher ${coursier_launcher_version}:" + fi + } +} + +verify_sbt_jar() { + local jar="${1}" + local md5="${jar}.md5" + md5url="$(make_url "${sbt_version}").md5" + + echoerr "Downloading sbt launcher ${sbt_version} md5 hash:" + echoerr " From ${md5url}" + echoerr " To ${md5}" + + download_url "${md5url}" "${md5}" >/dev/null 2>&1 + + if command -v md5sum >/dev/null 2>&1; then + if echo "$(cat "${md5}") ${jar}" | md5sum -c -; then + rm -rf "${md5}" + return 0 + else + echoerr "Checksum does not match" + return 1 + fi + elif command -v md5 >/dev/null 2>&1; then + if [ "$(md5 -q "${jar}")" == "$(cat "${md5}")" ]; then + rm -rf "${md5}" + return 0 + else + echoerr "Checksum does not match" + return 1 + fi + elif command -v openssl >/dev/null 2>&1; then + if [ "$(openssl md5 -r "${jar}" | awk '{print $1}')" == "$(cat "${md5}")" ]; then + rm -rf "${md5}" + return 0 + else + echoerr "Checksum does not match" + return 1 + fi + else + echoerr "Could not find an MD5 command" + return 1 + fi +} + +usage() { + set_sbt_version + cat < display stack traces with a max of frames (default: -1, traces suppressed) + -debug-inc enable debugging log for the incremental compiler + -no-colors disable ANSI color codes + -sbt-create start sbt even if current directory contains no sbt project + -sbt-dir path to global settings/plugins directory (default: ~/.sbt/) + -sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11+) + -ivy path to local Ivy repository (default: ~/.ivy2) + -no-share use all local caches; no sharing + -offline put sbt in offline mode + -jvm-debug Turn on JVM debugging, open at the given port. + -batch Disable interactive mode + -prompt Set the sbt prompt; in expr, 's' is the State and 'e' is Extracted + -script Run the specified file as a scala script + -coursier use a coursier-based launcher rather than an official sbt launcher (default) + -mainline use the mainline sbt launcher + + # sbt version (default: sbt.version from $buildProps if present, otherwise $sbt_release_version) + -sbt-version use the specified version of sbt (default: $sbt_release_version) + -sbt-force-latest force the use of the latest release of sbt: $sbt_release_version + -sbt-dev use the latest pre-release version of sbt: $sbt_unreleased_version + -sbt-jar use the specified jar as the sbt launcher + -sbt-launch-dir directory to hold sbt launchers (default: $sbt_launch_dir) + -sbt-launch-repo repo url for downloading sbt launcher jar (default: $(url_base "$sbt_version")) + + # scala version (default: as chosen by sbt) + -28 use $latest_28 + -29 use $latest_29 + -210 use $latest_210 + -211 use $latest_211 + -212 use $latest_212 + -213 use $latest_213 + -scala-home use the scala build at the specified directory + -scala-version use the specified version of scala + -binary-version use the specified scala version when searching for dependencies + + # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) + -java-home alternate JAVA_HOME + + # passing options to the jvm - note it does NOT use JAVA_OPTS due to pollution + # The default set is used if JVM_OPTS is unset and no -jvm-opts file is found + $(default_jvm_opts) + JVM_OPTS environment variable holding either the jvm args directly, or + the reference to a file containing jvm args if given path is prepended by '@' (e.g. '@/etc/jvmopts') + Note: "@"-file is overridden by local '.jvmopts' or '-jvm-opts' argument. + -jvm-opts file containing jvm args (if not given, .jvmopts in project root is used if present) + -Dkey=val pass -Dkey=val directly to the jvm + -J-X pass option -X directly to the jvm (-J is stripped) + + # passing options to sbt, OR to this runner + SBT_OPTS environment variable holding either the sbt args directly, or + the reference to a file containing sbt args if given path is prepended by '@' (e.g. '@/etc/sbtopts') + Note: "@"-file is overridden by local '.sbtopts' or '-sbt-opts' argument. + -sbt-opts file containing sbt args (if not given, .sbtopts in project root is used if present) + -S-X add -X to sbt's scalacOptions (-S is stripped) + + # passing options exclusively to this runner + SBTX_OPTS environment variable holding either the sbt-extras args directly, or + the reference to a file containing sbt-extras args if given path is prepended by '@' (e.g. '@/etc/sbtxopts') + Note: "@"-file is overridden by local '.sbtxopts' or '-sbtx-opts' argument. + -sbtx-opts file containing sbt-extras args (if not given, .sbtxopts in project root is used if present) +EOM + exit 0 +} + +process_args() { + require_arg() { + local type="$1" + local opt="$2" + local arg="$3" + + if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then + die "$opt requires <$type> argument" + fi + } + while [[ $# -gt 0 ]]; do + case "$1" in + -h | -help) usage ;; + -v) verbose=true && shift ;; + -d) addSbt "--debug" && shift ;; + -w) addSbt "--warn" && shift ;; + -q) addSbt "--error" && shift ;; + -x) shift ;; # currently unused + -trace) require_arg integer "$1" "$2" && trace_level="$2" && shift 2 ;; + -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;; + + -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;; + -sbt-create) sbt_create=true && shift ;; + -sbt-dir) require_arg path "$1" "$2" && sbt_dir="$2" && shift 2 ;; + -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;; + -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;; + -no-share) noshare=true && shift ;; + -offline) addSbt "set offline in Global := true" && shift ;; + -jvm-debug) require_arg port "$1" "$2" && addDebugger "$2" && shift 2 ;; + -batch) batch=true && shift ;; + -prompt) require_arg "expr" "$1" "$2" && setThisBuild shellPrompt "(s => { val e = Project.extract(s) ; $2 })" && shift 2 ;; + -script) require_arg file "$1" "$2" && sbt_script="$2" && addJava "-Dsbt.main.class=sbt.ScriptMain" && shift 2 ;; + + -sbt-version) require_arg version "$1" "$2" && sbt_explicit_version="$2" && shift 2 ;; + -sbt-force-latest) sbt_explicit_version="$sbt_release_version" && shift ;; + -sbt-dev) sbt_explicit_version="$sbt_unreleased_version" && shift ;; + -sbt-jar) require_arg path "$1" "$2" && sbt_jar="$2" && shift 2 ;; + -sbt-launch-dir) require_arg path "$1" "$2" && sbt_launch_dir="$2" && shift 2 ;; + -sbt-launch-repo) require_arg path "$1" "$2" && sbt_launch_repo="$2" && shift 2 ;; + + -28) setScalaVersion "$latest_28" && shift ;; + -29) setScalaVersion "$latest_29" && shift ;; + -210) setScalaVersion "$latest_210" && shift ;; + -211) setScalaVersion "$latest_211" && shift ;; + -212) setScalaVersion "$latest_212" && shift ;; + -213) setScalaVersion "$latest_213" && shift ;; + + -coursier) enable_coursier && shift ;; + -mainline) coursier_launcher_version="" && shift ;; + -coursier-version) require_arg version "$1" "$2" && coursier_launcher_version="$2" && shift 2 ;; + + -scala-version) require_arg version "$1" "$2" && setScalaVersion "$2" && shift 2 ;; + -binary-version) require_arg version "$1" "$2" && setThisBuild scalaBinaryVersion "\"$2\"" && shift 2 ;; + -scala-home) require_arg path "$1" "$2" && setThisBuild scalaHome "_root_.scala.Some(file(\"$2\"))" && shift 2 ;; + -java-home) require_arg path "$1" "$2" && setJavaHome "$2" && shift 2 ;; + -sbt-opts) require_arg path "$1" "$2" && sbt_opts_file="$2" && shift 2 ;; + -sbtx-opts) require_arg path "$1" "$2" && sbtx_opts_file="$2" && shift 2 ;; + -jvm-opts) require_arg path "$1" "$2" && jvm_opts_file="$2" && shift 2 ;; + + -D*) addJava "$1" && shift ;; + -J*) addJava "${1:2}" && shift ;; + -S*) addScalac "${1:2}" && shift ;; + + new) sbt_new=true && : ${sbt_explicit_version:=$sbt_release_version} && addResidual "$1" && shift ;; + + *) addResidual "$1" && shift ;; + esac + done +} + +# process the direct command line arguments +process_args "$@" + +# skip #-styled comments and blank lines +readConfigFile() { + local end=false + until $end; do + read -r || end=true + [[ $REPLY =~ ^# ]] || [[ -z $REPLY ]] || echo "$REPLY" + done <"$1" +} + +# if there are file/environment sbt_opts, process again so we +# can supply args to this runner +if [[ -r "$sbt_opts_file" ]]; then + vlog "Using sbt options defined in file $sbt_opts_file" + while read -r opt; do extra_sbt_opts+=("$opt"); done < <(readConfigFile "$sbt_opts_file") +elif [[ -n "$SBT_OPTS" && ! ("$SBT_OPTS" =~ ^@.*) ]]; then + vlog "Using sbt options defined in variable \$SBT_OPTS" + IFS=" " read -r -a extra_sbt_opts <<<"$SBT_OPTS" +else + vlog "No extra sbt options have been defined" +fi + +# if there are file/environment sbtx_opts, process again so we +# can supply args to this runner +if [[ -r "$sbtx_opts_file" ]]; then + vlog "Using sbt options defined in file $sbtx_opts_file" + while read -r opt; do extra_sbt_opts+=("$opt"); done < <(readConfigFile "$sbtx_opts_file") +elif [[ -n "$SBTX_OPTS" && ! ("$SBTX_OPTS" =~ ^@.*) ]]; then + vlog "Using sbt options defined in variable \$SBTX_OPTS" + IFS=" " read -r -a extra_sbt_opts <<<"$SBTX_OPTS" +else + vlog "No extra sbt options have been defined" +fi + +[[ -n "${extra_sbt_opts[*]}" ]] && process_args "${extra_sbt_opts[@]}" + +# reset "$@" to the residual args +set -- "${residual_args[@]}" +argumentCount=$# + +# set sbt version +set_sbt_version + +checkJava + +# only exists in 0.12+ +setTraceLevel() { + case "$sbt_version" in + "0.7."* | "0.10."* | "0.11."*) echoerr "Cannot set trace level in sbt version $sbt_version" ;; + *) setThisBuild traceLevel "$trace_level" ;; + esac +} + +# set scalacOptions if we were given any -S opts +[[ ${#scalac_args[@]} -eq 0 ]] || addSbt "set scalacOptions in ThisBuild += \"${scalac_args[*]}\"" + +[[ -n "$sbt_explicit_version" && -z "$sbt_new" ]] && addJava "-Dsbt.version=$sbt_explicit_version" +vlog "Detected sbt version $sbt_version" + +if [[ -n "$sbt_script" ]]; then + residual_args=("$sbt_script" "${residual_args[@]}") +else + # no args - alert them there's stuff in here + ((argumentCount > 0)) || { + vlog "Starting $script_name: invoke with -help for other options" + residual_args=(shell) + } +fi + +# verify this is an sbt dir, -create was given or user attempts to run a scala script +[[ -r ./build.sbt || -d ./project || -n "$sbt_create" || -n "$sbt_script" || -n "$sbt_new" ]] || { + cat < hint) { + return defaultValue; + } +} \ No newline at end of file diff --git a/tests/src/test/java/plotly/doc/Plotly.java b/tests/src/test/java/plotly/doc/Plotly.java new file mode 100644 index 00000000..4c9fa66f --- /dev/null +++ b/tests/src/test/java/plotly/doc/Plotly.java @@ -0,0 +1,9 @@ +package plotly.doc; + +// Defining these from Java code, so that scalac doesn't give +// those weird looking names under the hood. +public interface Plotly { + void newPlot(String div, Object data, Object layout, Object other); + void newPlot(String div, Object data, Object layout); + void newPlot(String div, Object data); +} diff --git a/tests/src/test/scala/plotly/FieldTests.scala b/tests/src/test/scala/plotly/FieldTests.scala index 3e34a33a..7bcb80d4 100644 --- a/tests/src/test/scala/plotly/FieldTests.scala +++ b/tests/src/test/scala/plotly/FieldTests.scala @@ -1,57 +1,52 @@ package plotly -import io.circe.Json -import org.scalatest.{FlatSpec, Matchers} +import argonaut.Json +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers -class FieldTests extends FlatSpec with Matchers { +class FieldTests extends AnyFlatSpec with Matchers { def traceHasShowlegendField(trace: Trace): Unit = { - val expectedField = Json.fromBoolean(true) + val expectedField = Json.jBool(true) - val json = Codecs.encodeTrace(trace) - val field = json.findAllByKey("showlegend") + val field = Codecs + .argonautEncodeTrace(trace) + .obj + .toList + .flatMap(_.toList.filter(_._1 == "showlegend").map(_._2)) assert(field === List(expectedField)) } "Bar" should "have a showlegend field" in { - val bar = Bar( - 1 to 10, - 1 to 10, - showlegend = true - ) + val bar = Bar(1 to 10, 1 to 10) + .withShowlegend(true) traceHasShowlegendField(bar) } "Box" should "have a showlegend field" in { - val box = Box( - 1 to 10, - showlegend = true - ) + val box = Box(1 to 10) + .withShowlegend(true) traceHasShowlegendField(box) } "Histogram" should "have a showlegend field" in { - val histogram = Histogram( - 1 to 10, - showlegend = true - ) + val histogram = Histogram(1 to 10) + .withShowlegend(true) traceHasShowlegendField(histogram) } "Scatter" should "have a showlegend field" in { - val scatter = Scatter( - 1 to 10, - showlegend = true - ) + val scatter = Scatter(1 to 10) + .withShowlegend(true) traceHasShowlegendField(scatter) } diff --git a/tests/src/test/scala/plotly/SequenceTests.scala b/tests/src/test/scala/plotly/SequenceTests.scala new file mode 100644 index 00000000..80f44413 --- /dev/null +++ b/tests/src/test/scala/plotly/SequenceTests.scala @@ -0,0 +1,31 @@ +package plotly + +import org.scalatest.flatspec.AnyFlatSpec + +import scala.collection.mutable.ArrayBuffer + +class SequenceTests extends AnyFlatSpec { + + "The implicit sequence conversion" should "convert a List to a Sequence" in { + assert((List(1, 2, 3): Sequence) === Sequence.Doubles(List(1d, 2d, 3d))) + } + + it should "convert a mutable ArrayBuffer to a Sequence" in { + assert((ArrayBuffer(1, 2, 3): Sequence) === Sequence.Doubles(List(1d, 2d, 3d))) + } + + it should "convert a nested mutable ArrayBuffer to a Sequence" in { + val mutableNestedDoubles: ArrayBuffer[ArrayBuffer[Double]] = ArrayBuffer( + ArrayBuffer(1d, 2d), + ArrayBuffer(3d, 4d) + ) + + val nestedDoublesList: List[List[Double]] = List( + List(1d, 2d), + List(3d, 4d) + ) + + assert((mutableNestedDoubles: Sequence) === Sequence.NestedDoubles(nestedDoublesList)) + } + +} diff --git a/tests/src/test/scala/plotly/doc/DocumentationTests.scala b/tests/src/test/scala/plotly/doc/DocumentationTests.scala old mode 100644 new mode 100755 index 0efdb867..c22ceb28 --- a/tests/src/test/scala/plotly/doc/DocumentationTests.scala +++ b/tests/src/test/scala/plotly/doc/DocumentationTests.scala @@ -1,29 +1,28 @@ package plotly package doc -import java.lang.{ Double => JDouble } - -import java.io.{ ByteArrayOutputStream, File, InputStream } +import java.io.{ByteArrayOutputStream, File, InputStream} +import java.lang.{Double => JDouble} import java.nio.file.Files - -import io.circe.{ DecodingFailure, Json, parser => Parse } -import io.circe.syntax._ - +import argonaut.Argonaut._ +import argonaut.{Json, Parse} +import plotly.layout.Layout import org.mozilla.javascript._ +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import plotly.element.HoverInfo +import plotly.element.HoverInfo.{X, Y, Z} +import plotly.element.ColorModel._ import scala.util.matching.Regex -import org.scalatest.{ FlatSpec, Matchers } - -import plotly.layout.Layout - object DocumentationTests { import plotly.Codecs._ private def readFully(is: InputStream): Array[Byte] = { val buffer = new ByteArrayOutputStream() - val data = Array.ofDim[Byte](16384) + val data = Array.ofDim[Byte](16384) var nRead = is.read(data, 0, data.length) while (nRead != -1) { @@ -35,12 +34,11 @@ object DocumentationTests { buffer.toByteArray } - def load(path: String): String = { - val cl = getClass.getClassLoader // resources should be in the same JAR as this, so same loader + val cl = getClass.getClassLoader // resources should be in the same JAR as this, so same loader val resPath = s"plotly/doc/$path" - val is = cl.getResourceAsStream(resPath) + val is = cl.getResourceAsStream(resPath) if (is == null) throw new NoSuchElementException(s"Resource $resPath") @@ -52,25 +50,30 @@ object DocumentationTests { def resourceTrace(res: String): Trace = { val dataStr = load(res) - val result = Parse.parse(dataStr).right.flatMap(_.as[Trace]) - result.right.getOrElse { + val result = dataStr.decodeEither[Trace] + result.getOrElse { throw new Exception(s"$res: $result") } } def resourceLayout(res: String): Layout = { val dataStr = load(res) - val result = Parse.parse(dataStr).right.flatMap(_.as[Layout]) - result.right.getOrElse { + val result = dataStr.decodeEither[Layout] + result.getOrElse { throw new Exception(s"$res: $result") } } - private class Plotly { + private class Plotly extends plotly.doc.Plotly { - var dataOpt = Option.empty[Object] + var dataOpt = Option.empty[Object] var layoutOpt = Option.empty[Object] + def newPlot(div: String, data: Object, layout: Object, other: Object): Unit = { + dataOpt = Option(data) + layoutOpt = Option(layout) + } + def newPlot(div: String, data: Object, layout: Object): Unit = { dataOpt = Option(data) layoutOpt = Option(layout) @@ -80,22 +83,31 @@ object DocumentationTests { dataOpt = Option(data) } + def plot(div: String, data: Object, layout: Object): Unit = + newPlot(div, data, layout) + def plot(div: String, data: Object): Unit = + newPlot(div, data) + def result(cx: Context, scope: ScriptableObject): (Seq[Json], Option[Json]) = { def stringify(obj: Object) = NativeJSON.stringify(cx, scope, obj, null, null).toString def jsonRepr(obj: Object): Json = { val jsonStr = stringify(obj) - Parse.parse(jsonStr).left.map { err => - throw new Exception(s"Cannot parse JSON: $err\n$jsonStr") - }.merge + Parse + .parse(jsonStr) + .left + .map { err => + throw new Exception(s"Cannot parse JSON: $err\n$jsonStr") + } + .merge } val data = dataOpt.map(jsonRepr) match { case None => throw new NoSuchElementException("data not set") case Some(json) => - json.asArray.getOrElse { + json.array.getOrElse { throw new Exception(s"data is not a JSON array\n${json.spaces2}") } } @@ -104,18 +116,19 @@ object DocumentationTests { } } - private object Document { + private object Document extends plotly.doc.Document { // stub... def getElementById(id: String): String = id } - + private object Numeric { def linspace(from: Int, to: Int, count: Int) = { val step = (to - from).toDouble / (count - 1) - new NativeArray((0 until count).map(n => from + n * step: JDouble).toArray[AnyRef]) { - override def getDefaultValue(hint: Class[_]) = - 0.0: JDouble - } + new NativeArrayWithDefault((0 until count).map(n => from + n * step: JDouble).toArray[AnyRef], 0.0: JDouble) + } + def linspace(from: Double, to: Double, count: Int) = { + val step = (to - from) / (count - 1) + new NativeArrayWithDefault((0 until count).map(n => from + n * step: JDouble).toArray[AnyRef], 0.0: JDouble) } } @@ -123,13 +136,17 @@ object DocumentationTests { args.toSeq.map(x => x: Any) match { case Seq(from: Int, to: Int, step: Int) => Numeric.linspace(from, to, step) + case Seq(from: Double, to: Int, step: Int) => + Numeric.linspace(from, to.toDouble, step) + case Seq(from: Double, to: Double, step: Int) => + Numeric.linspace(from, to, step) case other => throw new NoSuchElementException(s"linspace${other.mkString("(", ", ", ")")}") } def requireImpl(cx: Context, thisObj: Scriptable, args: Array[Object], funObj: Function): AnyRef = args match { case Array("linspace") => linspace(thisObj) - case other => throw new NoSuchElementException(s"require${other.mkString("(", ", ", ")")}") + case other => throw new NoSuchElementException(s"require${other.mkString("(", ", ", ")")}") } private def linspace(scope: Scriptable) = new FunctionObject( @@ -150,23 +167,28 @@ object DocumentationTests { val cx = Context.enter() - val (rawDataElems, rawLayoutOpt) = try { - val scope = cx.initStandardObjects() - ScriptableObject.putProperty(scope, "Plotly", plotly) - ScriptableObject.putProperty(scope, "document", Document) - ScriptableObject.putProperty(scope, "numeric", Numeric) - ScriptableObject.putProperty(scope, "require", require(scope)) - cx.evaluateString(scope, demo, "", 1, null) - plotly.result(cx, scope) - } finally { - Context.exit() - } + val (rawDataElems, rawLayoutOpt) = + try { + val scope = cx.initStandardObjects() + ScriptableObject.putProperty(scope, "Plotly", plotly) + ScriptableObject.putProperty(scope, "document", Document) + ScriptableObject.putProperty(scope, "numeric", Numeric) + ScriptableObject.putProperty(scope, "require", require(scope)) + ScriptableObject.putProperty(scope, "linspace", linspace(scope)) + cx.evaluateString(scope, demo, "", 1, null) + plotly.result(cx, scope) + } catch { + case e: org.mozilla.javascript.EvaluatorException => + println(s"Was running\n$demo\n\n") + throw new Exception(s"Evaluation error at line ${e.lineNumber()} column ${e.columnNumber()}", e) + } finally { + Context.exit() + } - val decodeData0 = rawDataElems.map(json => json -> json.as[Trace]) + val decodeData0 = rawDataElems.map(json => json -> json.as[Trace].toEither) - val dataErrors = decodeData0.collect { - case (json, Left(DecodingFailure(err, h))) => - (json, err, h) + val dataErrors = decodeData0.collect { case (json, Left((err, h))) => + (json, err, h) } if (dataErrors.nonEmpty) { @@ -176,14 +198,14 @@ object DocumentationTests { throw new Exception("Error decoding data (see above messages)") } - val data = decodeData0.collect { - case (_, Right(data)) => data + val data = decodeData0.collect { case (_, Right(data)) => + data } - val decodeLayoutOpt = rawLayoutOpt.map(json => json -> json.as[Layout]) + val decodeLayoutOpt = rawLayoutOpt.map(json => json -> json.as[Layout].toEither) val layoutOpt = decodeLayoutOpt.map { - case (json, Left(DecodingFailure(err, h))) => + case (json, Left((err, h))) => Console.err.println(s"Decoding layout: $err ($h)\n${json.spaces2}\n") throw new Exception("Error decoding layout (see above messages)") @@ -214,61 +236,132 @@ object DocumentationTests { } -class DocumentationTests extends FlatSpec with Matchers { +class DocumentationTests extends AnyFlatSpec with Matchers { import DocumentationTests._ val dir = new File("plotly-documentation/_posts/plotly_js") val subDirNames = Seq( - "line_and_scatter", - "line-plots", - "bar", - "horizontal-bar", + "basic/line_and_scatter", + "basic/line-plots", + "basic/bar", + "basic/horizontal-bar", // TODO? Pie charts - "time-series", - "bubble", - "area", + "financial/time-series", + "financial/candlestick-charts", +// "financial/ohlc", + "basic/bubble", + "basic/area", + "fundamentals/sizing", // TODO? Gauge charts // TODO Multiple chart types (needs contour) // TODO Shapes (need mock of d3) - "subplots", - "multiple-axes", - "insets", + "subplot/subplots", + "subplot/multiple-axes", + "subplot/insets", // TODO Responsive demo (only a demo, no new chart type / attributes) - "error-bar", + "statistical/error-bar", // TODO Continuous error bars - "box", + "statistical/box", // TODO 2D Density plots - "histogram", + "statistical/histogram", + "scientific/heatmap", // TODO 2D Histograms // TODO Wind rose charts // TODO Contour plots // TODO Heatmaps // TODO Heatmap and contour colorscales // TODO Polar charts - "log" + "scientific/log", // TODO Financial charts // TODO Maps - // TODO 3D charts + "3d/3d-surface" ) val subDirs = subDirNames.map(new File(dir, _)) for { subDir <- subDirs - post <- subDir.listFiles().sorted + post <- subDir.listFiles().sorted + if !post.getName.startsWith(".") } { s"$subDir" should s"$post" in { val rawContent = new String(Files.readAllBytes(post.toPath), "UTF-8") val content = stripFrontMatter(rawContent) - val lines = content - .linesIterator - .toVector - .map(_.trim) - .filter(_.nonEmpty) - - if (lines.nonEmpty) - plotlyDemoElements(stripFrontMatter(rawContent)) + .replace("
", "\\n") + .replace("
", "\\n") + .replace("(...size)", "(size[0])") // rhino doesn't seem to support the spead (...) operator + .replace("desired_maximum_marker_size**2", "desired_maximum_marker_size*desired_maximum_marker_size") + .replace( + """function linspace(a,b,n) { + return Plotly.d3.range(n).map(function(i){return a+i*(b-a)/(n-1);}); +} +""", + "" + ) + + if (content.contains("Plotly.d3.csv")) + println(s"Ignoring $post (Plotly.d3.csv not implemented)") + else { + + val lines = content.linesIterator.toVector + .map(_.trim) + .filter(_.nonEmpty) + + if (lines.nonEmpty) + plotlyDemoElements(content) + } + } + } + + it should "demo Image Trace" in { + val js = + """ + |var data = [ + | { + | type: "image", + | opacity: 0.1, + | x0: 0.05, + | y0: 0.05, + | colormodel: "rgb", + | hoverinfo: "x+y+z+color", + | z: [[[255, 0, 0], [0, 255, 0], [0, 0, 255]]] + | } + |]; + | + |var layout = { + | width: 400, + | height: 400, + | title: "image with opacity 0.1" + |}; + | + |Plotly.newPlot('myDiv', data, layout); + |""".stripMargin + val (data, maybeLayout) = plotlyDemoElements(js) + maybeLayout should ===( + Some( + Layout() + .withWidth(400) + .withHeight(400) + .withTitle("image with opacity 0.1") + ) + ) + + data.headOption match { + case Some(image) => + val colors = Seq( + Seq(Seq(255d, 0d, 0d), Seq(0d, 255, 0), Seq(0d, 0, 255)) + ) + val expected = Image(z = colors) + .withOpacity(0.1) + .withX0(0.05) + .withY0(0.05) + .withHoverinfo(HoverInfo(X, Y, Z, HoverInfo.Color)) + .withColormodel(RGB) + + image should ===(expected) + case None => + fail("data must contain an image trace") } } diff --git a/tests/src/test/scala/plotly/doc/SchemaTests.scala b/tests/src/test/scala/plotly/doc/SchemaTests.scala index 13d7b3b5..a865475e 100644 --- a/tests/src/test/scala/plotly/doc/SchemaTests.scala +++ b/tests/src/test/scala/plotly/doc/SchemaTests.scala @@ -4,15 +4,12 @@ package doc import java.io.File import java.nio.file.Files -import org.scalatest.{ FlatSpec, Matchers } - -import io.circe.{ Decoder, Json, parser => Parser } -import io.circe.simplegeneric._ -import io.circe.literal._ - -import shapeless.Witness - +import argonaut.ArgonautShapeless._ +import argonaut.{DecodeJson, DecodeResult, Json, Parse} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers import plotly.element._ +import shapeless.Witness object SchemaTests { @@ -22,38 +19,39 @@ object SchemaTests { case class ConstantString(value: String) extends Attribute case class Flag( - valType: Witness.`"flaglist"`.T, - flags: Seq[String], - description: String, - role: String + valType: Witness.`"flaglist"`.T, + flags: List[String], + description: String, + role: String ) extends Attribute case class Enumerated( - valType: Witness.`"enumerated"`.T, - description: String, - role: String, - values: Seq[String] + valType: Witness.`"enumerated"`.T, + description: String, + role: String, + values: List[String] ) extends Attribute case class Other(json: Json) extends Attribute - implicit val decode: Decoder[Attribute] = - Decoder.instance { c => - val constantString = c.as[String].right.map[Attribute](ConstantString(_)) - def flag = c.as[Flag].right.map[Attribute](x => x) - def enumerated = c.as[Enumerated].right.map[Attribute](x => x) - def other = Right(Other(c.focus)) - - constantString.right.toOption.map(Right(_)) - .orElse(flag.right.toOption.map(Right(_))) - .orElse(enumerated.right.toOption.map(Right(_))) + implicit val decode: DecodeJson[Attribute] = + DecodeJson { c => + val constantString = c.as[String].map[Attribute](ConstantString(_)) + def flag = c.as[Flag].map[Attribute](x => x) + def enumerated = c.as[Enumerated].map[Attribute](x => x) + def other = DecodeResult.ok[Attribute](Other(c.focus)) + + constantString.toOption + .map(DecodeResult.ok) + .orElse(flag.toOption.map(DecodeResult.ok)) + .orElse(enumerated.toOption.map(DecodeResult.ok)) .getOrElse(other) } } case class Trace( - description: Option[String], - attributes: Map[String, Attribute] + description: Option[String], + attributes: Map[String, Attribute] ) { def flagAttribute(name: String): Attribute.Flag = attributes.get(name) match { @@ -77,11 +75,7 @@ object SchemaTests { } case class Schema( - traces: Map[String, Trace] - ) - - case class SchemaFile( - schema: Schema + traces: Map[String, Trace] ) val schemaFile = new File("plotly-documentation/_data/plotschema.json") @@ -90,28 +84,28 @@ object SchemaTests { val schemaContent = new String(Files.readAllBytes(schemaFile.toPath), "UTF-8") - val schemaJson = Parser.parse(schemaContent) match { + val schemaJson = Parse.parse(schemaContent) match { case Left(error) => throw new Exception(s"Cannot parse schema: $error") case Right(json) => json } - schemaJson.as[SchemaFile] match { + schemaJson.as[Schema].toEither match { case Left(error) => - println(schemaJson.asObject.map(_.fields).getOrElse(Nil).mkString("\n")) + println(schemaJson.obj.map(_.fields).getOrElse(Nil).mkString("\n")) throw new Exception(s"Cannot decode schema: $error") - case Right(schemaFile) => - schemaFile.schema + case Right(schema) => + schema } } } -class SchemaTests extends FlatSpec with Matchers { +class SchemaTests extends AnyFlatSpec with Matchers { private def compareValues(fromSchema: Set[String], fromLib: Set[String]): Unit = { val onlySchema = (fromSchema -- fromLib).toVector.sorted - val onlyLib = (fromLib -- fromSchema).toVector.sorted + val onlyLib = (fromLib -- fromSchema).toVector.sorted assert(onlySchema.isEmpty, s"Only in schema: ${onlySchema.mkString(", ")}") assert(onlyLib.isEmpty, s"Only in lib: ${onlyLib.mkString(", ")}") @@ -138,7 +132,8 @@ class SchemaTests extends FlatSpec with Matchers { .values .toSet - val fromLib = Enumerate[TextPosition].apply() + val fromLib = Enumerate[TextPosition] + .apply() .map(_.label) .toSet @@ -146,4 +141,3 @@ class SchemaTests extends FlatSpec with Matchers { } } - diff --git a/tests/src/test/scala/plotly/element/LocalDateTimeTests.scala b/tests/src/test/scala/plotly/element/LocalDateTimeTests.scala new file mode 100644 index 00000000..251305f2 --- /dev/null +++ b/tests/src/test/scala/plotly/element/LocalDateTimeTests.scala @@ -0,0 +1,44 @@ +package plotly.element + +import org.scalatest.flatspec.AnyFlatSpec +import plotly.element.LocalDateTime.UnsafeImplicitConversions._ +import plotly.element.{LocalDateTime => PlotlyLocalDateTime} + +class LocalDateTimeTests extends AnyFlatSpec { + + "JavaTime conversions" should "convert java.time.LocalDateTime to Plotly LocalDateTime" in { + val javaLocalDateTime = java.time.LocalDateTime.parse("2020-04-18T14:52:52") + val plotlyLocalDateTime = PlotlyLocalDateTime(2020, 4, 18, 14, 52, 52) + + assert((javaLocalDateTime: PlotlyLocalDateTime) === plotlyLocalDateTime) + } + + it should "convert java.time.Instant to Plotly LocalDateTime using UTC" in { + val javaInstant = java.time.Instant.parse("2020-04-18T14:52:52Z") + val plotlyLocalDateTime = PlotlyLocalDateTime(2020, 4, 18, 14, 52, 52) + + assert((javaInstant: PlotlyLocalDateTime) === plotlyLocalDateTime) + } + + it should "convert java.time.OffsetDateTime to Plotly LocalDateTime" in { + val javaOffsetDateTime = java.time.OffsetDateTime.parse("2020-04-18T14:52:52+10:00") + val plotlyLocalDateTime = PlotlyLocalDateTime(2020, 4, 18, 14, 52, 52) + + assert((javaOffsetDateTime: PlotlyLocalDateTime) === plotlyLocalDateTime) + } + + it should "convert java.time.ZonedDateTime to Plotly LocalDateTime" in { + val javaOffsetDateTime = java.time.ZonedDateTime.parse("2020-04-18T14:52:52+10:00[Australia/Melbourne]") + val plotlyLocalDateTime = PlotlyLocalDateTime(2020, 4, 18, 14, 52, 52) + + assert((javaOffsetDateTime: PlotlyLocalDateTime) === plotlyLocalDateTime) + } + + it should "convert java.time.LocalDate to Plotly LocalDateTime" in { + val javaLocalDate = java.time.LocalDate.parse("2020-04-18") + val plotlyLocalDateTime = PlotlyLocalDateTime(2020, 4, 18, 0, 0, 0) + + assert((javaLocalDate: PlotlyLocalDateTime) === plotlyLocalDateTime) + } + +} diff --git a/version.sbt b/version.sbt deleted file mode 100644 index dfd7e193..00000000 --- a/version.sbt +++ /dev/null @@ -1 +0,0 @@ -version in ThisBuild := "0.3.4-SNAPSHOT"