├── project
├── build.properties
├── WebDeps.scala
├── Mima.scala
├── plugins.sbt
├── make-ghpages.sh
├── Deps.scala
└── Settings.scala
├── .gitignore
├── .github
├── dependabot.yml
├── scripts
│ └── gpg-setup.sh
└── workflows
│ └── ci.yml
├── core
├── shared
│ └── src
│ │ └── main
│ │ ├── scala
│ │ └── plotly
│ │ │ ├── element
│ │ │ ├── Cumulative.scala
│ │ │ ├── TextFont.scala
│ │ │ ├── Bins.scala
│ │ │ ├── SizeMode.scala
│ │ │ ├── GroupNorm.scala
│ │ │ ├── HoverLabelFont.scala
│ │ │ ├── Orientation.scala
│ │ │ ├── Dash.scala
│ │ │ ├── Ticks.scala
│ │ │ ├── Alignment.scala
│ │ │ ├── TickMode.scala
│ │ │ ├── HoverOn.scala
│ │ │ ├── ColorScale.scala
│ │ │ ├── Side.scala
│ │ │ ├── HoverLabel.scala
│ │ │ ├── HistFunc.scala
│ │ │ ├── BarTextPosition.scala
│ │ │ ├── ColorModel.scala
│ │ │ ├── BoxMean.scala
│ │ │ ├── Anchor.scala
│ │ │ ├── AxisAnchor.scala
│ │ │ ├── LineShape.scala
│ │ │ ├── OneOrSeq.scala
│ │ │ ├── AxisType.scala
│ │ │ ├── HistNorm.scala
│ │ │ ├── Fill.scala
│ │ │ ├── ScatterMode.scala
│ │ │ ├── BoxPoints.scala
│ │ │ ├── AxisReference.scala
│ │ │ ├── Color.scala
│ │ │ ├── Element.scala
│ │ │ ├── TextPosition.scala
│ │ │ ├── Line.scala
│ │ │ ├── HoverInfo.scala
│ │ │ ├── LocalDateTime.scala
│ │ │ ├── Marker.scala
│ │ │ ├── Error.scala
│ │ │ └── Symbol.scala
│ │ │ ├── layout
│ │ │ ├── RangeSlider.scala
│ │ │ ├── BoxMode.scala
│ │ │ ├── HoverMode.scala
│ │ │ ├── TraceOrder.scala
│ │ │ ├── Pattern.scala
│ │ │ ├── RowOrder.scala
│ │ │ ├── Grid.scala
│ │ │ ├── Ref.scala
│ │ │ ├── BarMode.scala
│ │ │ ├── Shape.scala
│ │ │ ├── Scene.scala
│ │ │ ├── Font.scala
│ │ │ ├── Margin.scala
│ │ │ ├── Annotation.scala
│ │ │ ├── Legend.scala
│ │ │ ├── Layout.scala
│ │ │ └── Axis.scala
│ │ │ ├── Range.scala
│ │ │ ├── Config.scala
│ │ │ ├── Sequence.scala
│ │ │ └── Trace.scala
│ │ ├── scala-2.12
│ │ └── plotly
│ │ │ └── MutableSequenceImplicitConversions.scala
│ │ └── scala-2.13
│ │ └── plotly
│ │ └── MutableSequenceImplicitConversions.scala
├── js
│ └── src
│ │ └── main
│ │ └── scala
│ │ └── plotly
│ │ └── element
│ │ └── PlotlyJavaTimeConversions.scala
└── jvm
│ └── src
│ └── main
│ └── scala
│ └── plotly
│ └── element
│ └── PlotlyJavaTimeConversions.scala
├── .gitmodules
├── .git-blame-ignore-revs
├── tests
└── src
│ └── test
│ ├── java
│ └── plotly
│ │ └── doc
│ │ ├── Document.java
│ │ ├── Plotly.java
│ │ └── NativeArrayWithDefault.java
│ └── scala
│ └── plotly
│ ├── SequenceTests.scala
│ ├── FieldTests.scala
│ ├── element
│ └── LocalDateTimeTests.scala
│ └── doc
│ ├── SchemaTests.scala
│ └── DocumentationTests.scala
├── render
├── jvm
│ └── src
│ │ ├── test
│ │ └── scala
│ │ │ └── plotly
│ │ │ └── ResourceTests.scala
│ │ └── main
│ │ └── scala
│ │ └── plotly
│ │ ├── internals
│ │ └── Properties.scala
│ │ └── Plotly.scala
├── shared
│ └── src
│ │ └── main
│ │ └── scala
│ │ └── plotly
│ │ ├── internals
│ │ ├── ArgonautCodecsExtra.scala
│ │ ├── BetterPrinter.scala
│ │ └── ArgonautCodecsInternals.scala
│ │ ├── Codecs.scala
│ │ └── Enumerate.scala
└── js
│ └── src
│ └── main
│ └── scala
│ └── plotly
│ └── Plotly.scala
├── .editorconfig
├── demo
└── src
│ └── main
│ ├── scala
│ └── plotly
│ │ └── demo
│ │ ├── DemoChart.scala
│ │ ├── bar
│ │ ├── BasicBarChart.scala
│ │ ├── GroupedBarChart.scala
│ │ ├── CustomizingIndividualBarColors.scala
│ │ ├── BarChartWithDirectLabels.scala
│ │ └── WaterfallBarChart.scala
│ │ ├── timeseries
│ │ └── TimeSeries.scala
│ │ ├── linecharts
│ │ ├── BasicLinePlot.scala
│ │ └── LineAndScatterPlot.scala
│ │ ├── horizontalbarcharts
│ │ ├── BasicHorizontalBarChart.scala
│ │ └── ColoredBarChart.scala
│ │ ├── heatmaps
│ │ ├── BasicHeatmap.scala
│ │ ├── CategoricalAxisHeatmap.scala
│ │ ├── CustomColorScaleHeatmap.scala
│ │ └── AnnotatedHeatmap.scala
│ │ ├── area
│ │ └── BasicOverlaidAreaChart.scala
│ │ ├── histogram
│ │ ├── BasicHistogram.scala
│ │ └── StyledBasicHistogram.scala
│ │ ├── bubblecharts
│ │ └── HoverOnTextBubbleChart.scala
│ │ ├── lineandscatter
│ │ └── CategoricalDotPlot.scala
│ │ └── Demo.scala
│ └── resources
│ └── index.html
├── .scalafmt.conf
├── joda-time
└── src
│ └── main
│ └── scala
│ └── plotly
│ └── Joda.scala
├── almond
└── src
│ └── main
│ └── scala
│ └── plotly
│ └── Almond.scala
└── README.md
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.10.0
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | .bsp/
3 | .idea/
4 | metals.sbt
5 | .vscode/
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Cumulative.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | import dataclass.data
4 |
5 | @data class Cumulative(enabled: Boolean)
6 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/TextFont.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | import dataclass.data
5 |
6 | @data class TextFont(family: String)
7 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "plotly-documentation"]
2 | path = plotly-documentation
3 | url = https://github.com/alexarchambault/plotly-documentation.git
4 | branch = source
5 |
--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # activate meaningful git annotations with:
2 | # git config blame.ignoreRevsFile .git-blame-ignore-revs
3 |
4 | # scalafmt
5 | 0ad886c35bdf9c8ad2bfb0501070b8b2ce810710
6 |
--------------------------------------------------------------------------------
/core/js/src/main/scala/plotly/element/PlotlyJavaTimeConversions.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | trait PlotlyJavaTimeConversions {
4 | // Empty since the java.time classes are not available in ScalaJS
5 | }
6 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Bins.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | import dataclass.data
4 |
5 | @data class Bins(
6 | start: Double,
7 | end: Double,
8 | size: Double
9 | )
10 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/RangeSlider.scala:
--------------------------------------------------------------------------------
1 | package plotly.layout
2 |
3 | import dataclass.data
4 | import plotly.Range
5 |
6 | @data(optionSetters = true) class RangeSlider(
7 | range: Option[Range] = None
8 | )
9 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/BoxMode.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package layout
3 |
4 | sealed abstract class BoxMode(val label: String) extends Product with Serializable
5 |
6 | object BoxMode {
7 | case object Group extends BoxMode("group")
8 | }
9 |
--------------------------------------------------------------------------------
/tests/src/test/java/plotly/doc/Document.java:
--------------------------------------------------------------------------------
1 | package plotly.doc;
2 |
3 | // Defining these from Java code, so that scalac doesn't give
4 | // those weird looking names under the hood.
5 | public interface Document {
6 | String getElementById(String id);
7 | }
8 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala-2.12/plotly/MutableSequenceImplicitConversions.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | trait MutableSequenceImplicitConversions {
4 | // Unneccessary in Scala 2.12, since the `Seq` alias refers to the supertype for mutable and immutable sequences
5 | }
6 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/HoverMode.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package layout
3 |
4 | sealed abstract class HoverMode(val label: String) extends Product with Serializable
5 |
6 | object HoverMode {
7 | case object Closest extends HoverMode("closest")
8 | }
9 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/TraceOrder.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package layout
3 |
4 | sealed abstract class TraceOrder(val label: String) extends Product with Serializable
5 |
6 | object TraceOrder {
7 | case object Reversed extends TraceOrder("reversed")
8 | }
9 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/SizeMode.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | sealed abstract class SizeMode(val label: String) extends Product with Serializable
4 |
5 | object SizeMode {
6 | case object Diameter extends SizeMode("diameter")
7 | case object Area extends SizeMode("area")
8 | }
9 |
--------------------------------------------------------------------------------
/render/jvm/src/test/scala/plotly/ResourceTests.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | import org.scalatest.propspec.AnyPropSpec
4 |
5 | class ResourceTests extends AnyPropSpec {
6 |
7 | property("plotly.min.js must be found in resources") {
8 | assert(Plotly.plotlyMinJs.nonEmpty)
9 | }
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/Pattern.scala:
--------------------------------------------------------------------------------
1 | package plotly.layout
2 |
3 | sealed abstract class Pattern(val label: String) extends Product with Serializable
4 |
5 | object Pattern {
6 | case object Independent extends Pattern("independent")
7 | case object Coupled extends Pattern("coupled")
8 | }
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | insert_final_newline = false
15 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/GroupNorm.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | sealed abstract class GroupNorm(val label: String) extends Product with Serializable
4 |
5 | object GroupNorm {
6 | case object Fraction extends GroupNorm("fraction")
7 | case object Percent extends GroupNorm("percent")
8 | }
9 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/HoverLabelFont.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | import dataclass.data
4 |
5 | @data(optionSetters = true) class HoverLabelFont(
6 | family: Option[OneOrSeq[String]] = None,
7 | size: Option[OneOrSeq[Double]] = None,
8 | color: Option[OneOrSeq[Color]] = None
9 | )
10 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/RowOrder.scala:
--------------------------------------------------------------------------------
1 | package plotly.layout
2 |
3 | sealed abstract class RowOrder(val label: String) extends Product with Serializable
4 |
5 | object RowOrder {
6 | case object TopToBottom extends RowOrder("top to bottom")
7 | case object BottomToTop extends RowOrder("bottom to top")
8 | }
9 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Orientation.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | sealed abstract class Orientation(val label: String) extends Product with Serializable
5 |
6 | object Orientation {
7 | case object Horizontal extends Orientation("h")
8 | case object Vertical extends Orientation("v")
9 | }
10 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Dash.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | sealed abstract class Dash(val label: String) extends Product with Serializable
5 |
6 | object Dash {
7 | case object Solid extends Dash("solid")
8 | case object DashDot extends Dash("dashdot")
9 | case object Dot extends Dash("dot")
10 | }
11 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Ticks.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | sealed abstract class Ticks(val label: String) extends Product with Serializable
5 |
6 | object Ticks {
7 | case object Outside extends Ticks("outside")
8 | case object Inside extends Ticks("inside")
9 | case object Empty extends Ticks("")
10 | }
11 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Alignment.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | sealed abstract class Alignment(val label: String) extends Product with Serializable
4 |
5 | object Alignment {
6 | case object Left extends Alignment("left")
7 | case object Right extends Alignment("right")
8 | case object Auto extends Alignment("auto")
9 | }
10 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/TickMode.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | sealed abstract class TickMode(val mode: String) extends Product with Serializable
4 |
5 | object TickMode {
6 | case object Auto extends TickMode("auto")
7 | case object Linear extends TickMode("linear")
8 | case object Array extends TickMode("array")
9 | }
10 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/Grid.scala:
--------------------------------------------------------------------------------
1 | package plotly.layout
2 |
3 | import dataclass.data
4 |
5 | @data(optionSetters = true) class Grid(
6 | rows: Option[Int] = None,
7 | columns: Option[Int] = None,
8 | pattern: Option[Pattern] = None,
9 | roworder: Option[RowOrder] = None,
10 | subplots: Option[Seq[Seq[String]]] = None
11 | )
12 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/HoverOn.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | sealed abstract class HoverOn(val label: String) extends Product with Serializable
4 |
5 | object HoverOn {
6 | case object Points extends HoverOn("points")
7 | case object Fills extends HoverOn("fills")
8 | case object PointsFill extends HoverOn("points+fills")
9 | }
10 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/ColorScale.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 | import dataclass.data
3 |
4 | sealed abstract class ColorScale extends Product with Serializable
5 |
6 | object ColorScale {
7 | @data class CustomScale(values: Seq[(Double, Color)]) extends ColorScale
8 | @data class NamedScale(name: String) extends ColorScale
9 | }
10 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/Ref.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package layout
3 |
4 | import plotly.element._
5 |
6 | sealed abstract class Ref(val label: String) extends Product with Serializable
7 |
8 | object Ref {
9 | case object Paper extends Ref("paper")
10 | case class Axis(underlying: AxisReference) extends Ref(underlying.label)
11 | }
12 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Side.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | sealed abstract class Side(val label: String) extends Product with Serializable
5 |
6 | object Side {
7 | case object Left extends Side("left")
8 | case object Right extends Side("right")
9 | case object Top extends Side("top")
10 | case object Bottom extends Side("bottom")
11 | }
12 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/DemoChart.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo
2 |
3 | import plotly.Trace
4 | import plotly.layout.Layout
5 |
6 | trait DemoChart {
7 | def plotlyDocUrl: String
8 | def id: String
9 | def source: String
10 | def data: Seq[Trace]
11 | def layout: Layout
12 | }
13 |
14 | trait NoLayoutDemoChart extends DemoChart {
15 | final def layout: Layout = null
16 | }
17 |
--------------------------------------------------------------------------------
/tests/src/test/java/plotly/doc/Plotly.java:
--------------------------------------------------------------------------------
1 | package plotly.doc;
2 |
3 | // Defining these from Java code, so that scalac doesn't give
4 | // those weird looking names under the hood.
5 | public interface Plotly {
6 | void newPlot(String div, Object data, Object layout, Object other);
7 | void newPlot(String div, Object data, Object layout);
8 | void newPlot(String div, Object data);
9 | }
10 |
--------------------------------------------------------------------------------
/.github/scripts/gpg-setup.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | # from https://github.com/coursier/apps/blob/f1d2bf568bf466a98569a85c3f23c5f3a8eb5360/.github/scripts/gpg-setup.sh
4 |
5 | echo $PGP_SECRET | base64 --decode | gpg --import --no-tty --batch --yes
6 |
7 | echo "allow-loopback-pinentry" >>~/.gnupg/gpg-agent.conf
8 | echo "pinentry-mode loopback" >>~/.gnupg/gpg.conf
9 |
10 | gpg-connect-agent reloadagent /bye
11 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/BarMode.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package layout
3 |
4 | sealed abstract class BarMode(val label: String) extends Product with Serializable
5 |
6 | object BarMode {
7 | case object Group extends BarMode("group")
8 | case object Stack extends BarMode("stack")
9 | case object Overlay extends BarMode("overlay")
10 | case object Relative extends BarMode("relative")
11 | }
12 |
--------------------------------------------------------------------------------
/project/WebDeps.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 |
3 | object WebDeps {
4 |
5 | object Versions {
6 | def plotlyJs = "1.54.1"
7 | }
8 |
9 | def bootstrap = "org.webjars.bower" % "bootstrap" % "3.4.1"
10 | def jquery = "org.webjars.bower" % "jquery" % "3.7.1"
11 | def plotlyJs = "org.webjars.bower" % "plotly.js" % Versions.plotlyJs
12 | def prism = "org.webjars.bower" % "prism" % "1.16.0"
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/render/shared/src/main/scala/plotly/internals/ArgonautCodecsExtra.scala:
--------------------------------------------------------------------------------
1 | package plotly.internals
2 |
3 | import argonaut.{DecodeJson, EncodeJson}
4 |
5 | trait ArgonautCodecsExtra {
6 |
7 | implicit def seqEncoder[T: EncodeJson]: EncodeJson[Seq[T]] =
8 | EncodeJson.of[Vector[T]].contramap(_.toVector)
9 | implicit def seqDecoder[T: DecodeJson]: DecodeJson[Seq[T]] =
10 | DecodeJson.of[Vector[T]].map(x => x)
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/HoverLabel.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | import dataclass.data
4 |
5 | @data(optionSetters = true) class HoverLabel(
6 | bgcolor: Option[OneOrSeq[Color]] = None,
7 | bordercolor: Option[OneOrSeq[Color]] = None,
8 | font: Option[HoverLabelFont] = None,
9 | align: Option[OneOrSeq[Alignment]] = None,
10 | namelength: Option[OneOrSeq[Int]] = None,
11 | uirevision: Option[Element] = None
12 | )
13 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/HistFunc.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | sealed abstract class HistFunc(val label: String) extends Product with Serializable
4 |
5 | object HistFunc {
6 |
7 | case object Count extends HistFunc("count")
8 | case object Sum extends HistFunc("sum")
9 | case object Average extends HistFunc("avg")
10 | case object Min extends HistFunc("min")
11 | case object Max extends HistFunc("max")
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/BarTextPosition.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | sealed abstract class BarTextPosition(val label: String) extends Product with Serializable
5 |
6 | object BarTextPosition {
7 | case object Inside extends BarTextPosition("inside")
8 | case object Outside extends BarTextPosition("outside")
9 | case object Auto extends BarTextPosition("auto")
10 | case object None extends BarTextPosition("none")
11 | }
12 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/ColorModel.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | sealed abstract class ColorModel(val label: String) extends Product with Serializable
4 |
5 | object ColorModel {
6 | case object RGB extends ColorModel("rgb")
7 | case object RGBA extends ColorModel("rgba")
8 | case object RGBA256 extends ColorModel("rgba256")
9 | case object HSL extends ColorModel("hsl")
10 | case object HSLA extends ColorModel("hsla")
11 | }
12 |
--------------------------------------------------------------------------------
/project/Mima.scala:
--------------------------------------------------------------------------------
1 | import com.typesafe.tools.mima.core._
2 | import com.typesafe.tools.mima.plugin.MimaPlugin
3 | import sbt._
4 | import sbt.Keys._
5 |
6 | import scala.sys.process._
7 |
8 | object Mima {
9 |
10 | lazy val renderFilters = Def.settings(
11 | MimaPlugin.autoImport.mimaBinaryIssueFilters ++= Seq(
12 | // users shouln't ever reference those
13 | ProblemFilters.exclude[Problem]("plotly.internals.shaded.*")
14 | )
15 | )
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/BoxMean.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | import dataclass.data
5 |
6 | sealed abstract class BoxMean extends Product with Serializable
7 |
8 | object BoxMean {
9 | @data class Bool(value: Boolean) extends BoxMean
10 | sealed abstract class Labeled(val label: String) extends BoxMean
11 |
12 | val True = Bool(true)
13 | val False = Bool(false)
14 |
15 | case object SD extends Labeled("sd")
16 | }
17 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Anchor.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | sealed abstract class Anchor(val label: String) extends Product with Serializable
5 |
6 | object Anchor {
7 | case object Left extends Anchor("left")
8 | case object Center extends Anchor("center")
9 | case object Right extends Anchor("right")
10 | case object Top extends Anchor("top")
11 | case object Middle extends Anchor("middle")
12 | case object Bottom extends Anchor("bottom")
13 | }
14 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/AxisAnchor.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | import dataclass.data
5 |
6 | sealed abstract class AxisAnchor(val label: String) extends Product with Serializable
7 |
8 | object AxisAnchor {
9 | @data class Reference(axisReference: AxisReference) extends AxisAnchor(axisReference.label)
10 | case object Free extends AxisAnchor("free")
11 | case object Y extends AxisAnchor("y")
12 | }
13 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/LineShape.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | sealed abstract class LineShape(val label: String) extends Product with Serializable
5 |
6 | object LineShape {
7 | case object Linear extends LineShape("linear")
8 | case object Spline extends LineShape("spline")
9 | case object VHV extends LineShape("vhv")
10 | case object HVH extends LineShape("hvh")
11 | case object VH extends LineShape("vh")
12 | case object HV extends LineShape("hv")
13 | }
14 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/OneOrSeq.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | import scala.language.implicitConversions
4 |
5 | sealed abstract class OneOrSeq[T] extends Product with Serializable
6 |
7 | object OneOrSeq {
8 | case class One[T](value: T) extends OneOrSeq[T]
9 | case class Sequence[T](seq: Seq[T]) extends OneOrSeq[T]
10 |
11 | implicit def fromOne[T](value: T): OneOrSeq[T] =
12 | One(value)
13 | implicit def fromSeq[T](seq: Seq[T]): OneOrSeq[T] =
14 | Sequence(seq)
15 | }
16 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/AxisType.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | sealed abstract class AxisType(val label: String) extends Product with Serializable
5 |
6 | object AxisType {
7 |
8 | /** Lets plotly guess from data */
9 | case object Default extends AxisType("-")
10 | case object Linear extends AxisType("linear")
11 | case object Log extends AxisType("log")
12 | case object Date extends AxisType("date")
13 | case object Category extends AxisType("category")
14 | }
15 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/HistNorm.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | sealed abstract class HistNorm(val label: String) extends Product with Serializable
4 |
5 | object HistNorm {
6 | case object Count extends HistNorm("count")
7 | case object Percent extends HistNorm("percent")
8 | case object Probability extends HistNorm("probability")
9 | case object Density extends HistNorm("density")
10 | case object ProbabilityDensity extends HistNorm("probability density")
11 | }
12 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12")
2 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.3")
3 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0")
4 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2")
5 | addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.2")
6 | addSbtPlugin("io.get-coursier" % "sbt-shading" % "2.1.4")
7 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
8 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Fill.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | sealed abstract class Fill(val label: String) extends Product with Serializable
5 |
6 | object Fill {
7 | case object None extends Fill("none")
8 | case object ToZeroX extends Fill("tozerox")
9 | case object ToZeroY extends Fill("tozeroy")
10 | case object ToNextX extends Fill("tonextx")
11 | case object ToNextY extends Fill("tonexty")
12 | case object ToSelf extends Fill("toself")
13 | case object ToNext extends Fill("tonext")
14 | }
15 |
--------------------------------------------------------------------------------
/.scalafmt.conf:
--------------------------------------------------------------------------------
1 | version = "3.8.1"
2 |
3 | runner.dialect = scala213
4 |
5 | preset = defaultWithAlign
6 |
7 | maxColumn = 120
8 |
9 | assumeStandardLibraryStripMargin = true
10 |
11 | align {
12 | arrowEnumeratorGenerator = true
13 | }
14 |
15 | newlines {
16 | penalizeSingleSelectMultiArgList = true
17 | }
18 |
19 | rewrite {
20 | rules = [Imports, PreferCurlyFors, RedundantParens]
21 | imports.sort = scalastyle
22 | }
23 |
24 | docstrings.wrap=no
25 |
26 | // only format files tracked by git
27 | project.git = true
28 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/Shape.scala:
--------------------------------------------------------------------------------
1 | package plotly.layout
2 |
3 | import dataclass.data
4 | import plotly.element.{Color, Line}
5 |
6 | @data(optionSetters = true) class Shape(
7 | `type`: Option[String] = None,
8 | xref: Option[String] = None,
9 | yref: Option[String] = None,
10 | x0: Option[String] = None,
11 | y0: Option[Double] = None,
12 | x1: Option[String] = None,
13 | y1: Option[Double] = None,
14 | fillcolor: Option[Color] = None,
15 | opacity: Option[Double] = None,
16 | line: Option[Line] = None
17 | )
18 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/Range.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | import plotly.element.LocalDateTime
4 |
5 | import scala.language.implicitConversions
6 |
7 | sealed abstract class Range extends Product with Serializable
8 |
9 | object Range {
10 | final case class Doubles(range: (Double, Double)) extends Range
11 | final case class DateTimes(range: (LocalDateTime, LocalDateTime)) extends Range
12 |
13 | implicit def fromDoubleTuple(t: (Double, Double)): Range =
14 | Doubles(t)
15 | implicit def fromDateTimes(t: (LocalDateTime, LocalDateTime)): Range =
16 | DateTimes(t)
17 | }
18 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/bar/BasicBarChart.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.bar
2 |
3 | import plotly._
4 | import plotly.demo.NoLayoutDemoChart
5 | import plotly.element._
6 |
7 | object BasicBarChart extends NoLayoutDemoChart {
8 |
9 | def plotlyDocUrl = "https://plot.ly/javascript/bar-charts/#basic-bar-chart"
10 | def id = "basic-bar-chart"
11 | def source = BasicBarChartSource.source
12 |
13 | // demo source start
14 |
15 | val data = Seq(
16 | Bar(
17 | Seq("giraffes", "orangutans", "monkeys"),
18 | Seq(20, 14, 23)
19 | )
20 | )
21 |
22 | // demo source end
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/timeseries/TimeSeries.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.timeseries
2 |
3 | import plotly.Scatter
4 | import plotly.demo.NoLayoutDemoChart
5 |
6 | object TimeSeries extends NoLayoutDemoChart {
7 |
8 | def plotlyDocUrl = "https://plot.ly/javascript/time-series/#date-strings"
9 | def id = "time-series-chart"
10 | def source = TimeSeriesSource.source
11 |
12 | // demo source start
13 |
14 | val data = Seq(
15 | Scatter(
16 | Seq("2013-10-04 22:23:00", "2013-11-04 22:23:00", "2013-12-04 22:23:00"),
17 | Seq(1, 3, 6)
18 | )
19 | )
20 |
21 | // demo source end
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/ScatterMode.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | import dataclass.data
5 |
6 | @data class ScatterMode(flags: Set[ScatterMode.Flag])
7 |
8 | object ScatterMode {
9 | def apply(flags: Flag*): ScatterMode =
10 | ScatterMode(flags.toSet)
11 |
12 | sealed abstract class Flag(val label: String) extends Product with Serializable
13 |
14 | case object Markers extends Flag("markers")
15 | case object Text extends Flag("text")
16 | case object Lines extends Flag("lines")
17 |
18 | val flags = Seq(Markers, Text, Lines)
19 | val flagMap = flags.map(m => m.label -> m).toMap
20 | }
21 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/BoxPoints.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | import dataclass.data
5 |
6 | sealed abstract class BoxPoints extends Product with Serializable
7 |
8 | object BoxPoints {
9 | @data class Bool(value: Boolean) extends BoxPoints
10 | sealed abstract class Labeled(val label: String) extends BoxPoints
11 |
12 | val False = Bool(false)
13 | val True = Bool(true)
14 |
15 | case object All extends Labeled("all")
16 | case object SuspectedOutliers extends Labeled("suspectedoutliers")
17 | case object Outliers extends Labeled("Outliers") // FIXME case?
18 | }
19 |
--------------------------------------------------------------------------------
/project/make-ghpages.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -ex
3 |
4 | if [ -e gh-pages ]; then
5 | echo "Error: gh-pages already exists." 1>&2
6 | exit 1
7 | fi
8 |
9 | ./sbt demo/fullOptJS
10 | mkdir gh-pages
11 |
12 | cp \
13 | demo/target/scala-2.13/plotly-demo-opt.js \
14 | demo/target/scala-2.13/plotly-demo-opt.js.map \
15 | demo/target/scala-2.13/plotly-demo-jsdeps.js \
16 | demo/target/scala-2.13/plotly-demo-jsdeps.min.js \
17 | gh-pages
18 |
19 | cat demo/target/scala-2.13/classes/index.html | \
20 | sed 's@\.\./plotly-demo-jsdeps\.js@plotly-demo-jsdeps.min.js@' | \
21 | sed 's@\.\./plotly-demo-fastopt\.js@plotly-demo-opt.js@' | \
22 | cat > gh-pages/index.html
23 |
--------------------------------------------------------------------------------
/render/jvm/src/main/scala/plotly/internals/Properties.scala:
--------------------------------------------------------------------------------
1 | package plotly.internals
2 |
3 | import java.util.{Properties => JProperties}
4 |
5 | object Properties {
6 |
7 | private lazy val props = {
8 | val p = new JProperties
9 | try {
10 | p.load(
11 | getClass.getClassLoader
12 | .getResourceAsStream("plotly/plotly-scala.properties")
13 | )
14 | } catch {
15 | case _: NullPointerException =>
16 | }
17 | p
18 | }
19 |
20 | lazy val plotlyJsVersion = props.getProperty("plotly-js-version")
21 | lazy val version = props.getProperty("version")
22 | lazy val commitHash = props.getProperty("commit-hash")
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/linecharts/BasicLinePlot.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.linecharts
2 |
3 | import plotly.Scatter
4 | import plotly.demo.NoLayoutDemoChart
5 |
6 | object BasicLinePlot extends NoLayoutDemoChart {
7 |
8 | def id = "basic-line-plot"
9 |
10 | def plotlyDocUrl = "https://plot.ly/javascript/line-charts/#basic-line-plot"
11 |
12 | def source = BasicLinePlotSource.source
13 |
14 | // demo source start
15 |
16 | val trace1 = Scatter(
17 | Seq(1, 2, 3, 4),
18 | Seq(10, 15, 13, 17)
19 | )
20 |
21 | val trace2 = Scatter(
22 | Seq(1, 2, 3, 4),
23 | Seq(16, 5, 11, 9)
24 | )
25 |
26 | val data = Seq(trace1, trace2)
27 |
28 | // demo source end
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/render/shared/src/main/scala/plotly/Codecs.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | import argonaut._
4 | import argonaut.ArgonautShapeless._
5 | import plotly.internals.ArgonautCodecsExtra
6 | import plotly.internals.ArgonautCodecsInternals._
7 | import plotly.layout._
8 |
9 | object Codecs extends ArgonautCodecsExtra {
10 |
11 | implicit val argonautEncodeTrace = EncodeJson.of[Trace]
12 | implicit val argonautDecodeTrace = DecodeJson.of[Trace]
13 |
14 | implicit val argonautEncodeLayout = EncodeJson.of[Layout]
15 | implicit val argonautDecodeLayout = DecodeJson.of[Layout]
16 |
17 | implicit val argonautEncodeConfig = EncodeJson.of[Config]
18 | implicit val argonautDecodeConfig = DecodeJson.of[Config]
19 | }
20 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/AxisReference.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | sealed abstract class AxisReference(val label: String) extends Product with Serializable
5 |
6 | object AxisReference {
7 | case object X extends AxisReference("x")
8 | case object X1 extends AxisReference("x1")
9 | case object X2 extends AxisReference("x2")
10 | case object X3 extends AxisReference("x3")
11 | case object X4 extends AxisReference("x4")
12 | case object Y extends AxisReference("y")
13 | case object Y1 extends AxisReference("y1")
14 | case object Y2 extends AxisReference("y2")
15 | case object Y3 extends AxisReference("y3")
16 | case object Y4 extends AxisReference("y4")
17 | }
18 |
--------------------------------------------------------------------------------
/tests/src/test/java/plotly/doc/NativeArrayWithDefault.java:
--------------------------------------------------------------------------------
1 | package plotly.doc;
2 |
3 | import org.mozilla.javascript.NativeArray;
4 |
5 | // This class is to override NativeArray#getDefaultValue. Ideally we would just do this in an anonymous class in Scala
6 | // code, but https://github.com/scala/bug/issues/11575 makes this impossible.
7 | class NativeArrayWithDefault extends NativeArray {
8 | private final Object defaultValue;
9 |
10 | public NativeArrayWithDefault(Object[] array, Object defaultValue) {
11 | super(array);
12 | this.defaultValue = defaultValue;
13 | }
14 |
15 | @Override
16 | public Object getDefaultValue(Class> hint) {
17 | return defaultValue;
18 | }
19 | }
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/Scene.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package layout
3 |
4 | import java.lang.{Boolean => JBoolean, Double => JDouble, Integer => JInt}
5 |
6 | import dataclass.data
7 | import plotly.element._
8 |
9 | @data(optionSetters = true) class Scene(
10 | xaxis: Option[Axis] = None,
11 | yaxis: Option[Axis] = None,
12 | zaxis: Option[Axis] = None
13 | )
14 |
15 | object Scene {
16 | @deprecated("Use Scene() and chain-call .with* methods on it instead", "0.8.0")
17 | def apply(
18 | xaxis: Axis = null,
19 | yaxis: Axis = null,
20 | zaxis: Axis = null
21 | ): Scene = new Scene(
22 | Option(xaxis),
23 | Option(yaxis),
24 | Option(zaxis)
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/horizontalbarcharts/BasicHorizontalBarChart.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.horizontalbarcharts
2 |
3 | import plotly.Bar
4 | import plotly.demo.NoLayoutDemoChart
5 | import plotly.element.Orientation
6 |
7 | object BasicHorizontalBarChart extends NoLayoutDemoChart {
8 |
9 | def plotlyDocUrl = "https://plot.ly/javascript/horizontal-bar-charts/#basic-horizontal-bar-chart"
10 | def id = "basic-horizontal-bar-chart"
11 | def source = BasicHorizontalBarChartSource.source
12 |
13 | // demo source start
14 |
15 | val data = Seq(
16 | Bar(Seq(20, 14, 23), Seq("giraffes", "orangutans", "monkeys"))
17 | .withOrientation(Orientation.Horizontal)
18 | )
19 |
20 | // demo source end
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/heatmaps/BasicHeatmap.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.heatmaps
2 |
3 | import plotly._
4 | import plotly.demo.NoLayoutDemoChart
5 | import plotly.element._
6 |
7 | object BasicHeatmap extends NoLayoutDemoChart {
8 |
9 | def plotlyDocUrl = "https://plot.ly/javascript/heatmaps/#basic-heatmap"
10 | def id = "basic-heatmap"
11 | def source = BasicHeatmapSource.source
12 |
13 | // demo source start
14 |
15 | val data = Seq(
16 | Heatmap()
17 | .withZ(
18 | Seq(
19 | Seq(1, 20, 30),
20 | Seq(20, 1, 60),
21 | Seq(30, 60, 1)
22 | )
23 | )
24 | .withColorscale(ColorScale.NamedScale("Portland"))
25 | )
26 |
27 | // demo source end
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Color.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | import dataclass.data
5 |
6 | sealed abstract class Color extends Product with Serializable
7 |
8 | object Color {
9 |
10 | @data class RGBA(r: Int, g: Int, b: Int, alpha: Double) extends Color
11 |
12 | @data class StringColor(color: String) extends Color
13 |
14 | object StringColor {
15 | val colors = Set(
16 | "black",
17 | "grey",
18 | "white",
19 | "fuchsia",
20 | "red",
21 | "blue",
22 | "cls", // ???
23 | "pink",
24 | "green",
25 | "magenta"
26 | )
27 | }
28 |
29 | @data class RGB(r: Int, g: Int, b: Int) extends Color
30 |
31 | @data class HSL(h: Int, s: Int, l: Int) extends Color
32 | }
33 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Element.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | import scala.language.implicitConversions
5 |
6 | sealed abstract class Element extends Product with Serializable
7 |
8 | object Element {
9 | final case class DoubleElement(value: Double) extends Element
10 | final case class StringElement(value: String) extends Element
11 |
12 | implicit def fromDouble(d: Double): Element =
13 | DoubleElement(d)
14 | implicit def fromFloat(f: Float): Element =
15 | DoubleElement(f.toDouble)
16 | implicit def fromLong(l: Long): Element =
17 | DoubleElement(l.toDouble)
18 | implicit def fromInt(n: Int): Element =
19 | DoubleElement(n.toDouble)
20 | implicit def fromString(s: String): Element =
21 | StringElement(s)
22 | }
23 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/area/BasicOverlaidAreaChart.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.area
2 |
3 | import plotly.Scatter
4 | import plotly.demo.NoLayoutDemoChart
5 | import plotly.element.Fill
6 |
7 | object BasicOverlaidAreaChart extends NoLayoutDemoChart {
8 |
9 | def plotlyDocUrl = "https://plot.ly/javascript/filled-area-plots/#basic-overlaid-area-chart"
10 | def id = "basic-overlaid-area-chart"
11 | def source = BasicOverlaidAreaChartSource.source
12 |
13 | // demo source start
14 |
15 | val trace1 = Scatter(Seq(1, 2, 3, 4), Seq(0, 2, 3, 5))
16 | .withFill(Fill.ToZeroY)
17 |
18 | val trace2 = Scatter(Seq(1, 2, 3, 4), Seq(3, 5, 1, 7))
19 | .withFill(Fill.ToNextY)
20 |
21 | val data = Seq(trace1, trace2)
22 |
23 | // demo source end
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/TextPosition.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | sealed abstract class TextPosition(val label: String) extends Product with Serializable
5 |
6 | object TextPosition {
7 | case object TopLeft extends TextPosition("top left")
8 | case object TopCenter extends TextPosition("top center")
9 | case object TopRight extends TextPosition("top right")
10 | case object MiddleLeft extends TextPosition("middle left")
11 | case object MiddleCenter extends TextPosition("middle center")
12 | case object MiddleRight extends TextPosition("middle right")
13 | case object BottomLeft extends TextPosition("bottom left")
14 | case object BottomCenter extends TextPosition("bottom center")
15 | case object BottomRight extends TextPosition("bottom right")
16 | }
17 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/bar/GroupedBarChart.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.bar
2 |
3 | import plotly._
4 | import plotly.demo.DemoChart
5 | import plotly.element._
6 | import plotly.layout._
7 |
8 | object GroupedBarChart extends DemoChart {
9 |
10 | def plotlyDocUrl = "https://plot.ly/javascript/bar-charts/#grouped-bar-chart"
11 | def id = "grouped-bar-chart"
12 | def source = GroupedBarChartSource.source
13 |
14 | // demo source start
15 |
16 | val trace1 = Bar(Seq("giraffes", "orangutans", "monkeys"), Seq(20, 14, 23))
17 | .withName("SF Zoo")
18 |
19 | val trace2 = Bar(Seq("giraffes", "orangutans", "monkeys"), Seq(12, 18, 29))
20 | .withName("LA Zoo")
21 |
22 | val data = Seq(trace1, trace2)
23 |
24 | val layout = Layout()
25 | .withBarmode(BarMode.Group)
26 |
27 | // demo source end
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/Config.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | import java.lang.{Boolean => JBoolean}
4 | import dataclass.data
5 |
6 | @data(optionSetters = true) class Config(
7 | editable: Option[Boolean] = None,
8 | responsive: Option[Boolean] = None,
9 | showEditInChartStudio: Option[Boolean] = None,
10 | plotlyServerURL: Option[String] = None
11 | )
12 |
13 | object Config {
14 | @deprecated("Use Config() and chain-call .with* methods on it instead", "0.8.0")
15 | def apply(
16 | editable: JBoolean = null,
17 | responsive: JBoolean = null,
18 | showEditInChartStudio: JBoolean = null,
19 | plotlyServerURL: String = null
20 | ): Config =
21 | new Config(
22 | Option(editable),
23 | Option(responsive),
24 | Option(showEditInChartStudio),
25 | Option(plotlyServerURL)
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/project/Deps.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 |
3 | import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._
4 |
5 | object Deps {
6 |
7 | import Def.setting
8 |
9 | def almondScalaApi = "sh.almond" %% "jupyter-api" % "0.13.14"
10 | def argonautShapeless = setting("com.github.alexarchambault" %%% "argonaut-shapeless_6.3" % "1.3.1")
11 | def dataClass = "io.github.alexarchambault" %% "data-class" % "0.2.6"
12 | def jodaTime = "joda-time" % "joda-time" % "2.12.7"
13 | def rhino = "org.mozilla" % "rhino" % "1.7.15"
14 | def scalajsDom = setting("org.scala-js" %%% "scalajs-dom" % "2.8.0")
15 | def scalatags = setting("com.lihaoyi" %%% "scalatags" % "0.13.1")
16 | def scalaTest = "org.scalatest" %% "scalatest" % "3.2.18"
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/heatmaps/CategoricalAxisHeatmap.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.heatmaps
2 |
3 | import plotly._
4 | import plotly.demo.NoLayoutDemoChart
5 | import plotly.element._
6 |
7 | object CategoricalAxisHeatmap extends NoLayoutDemoChart {
8 |
9 | def plotlyDocUrl = "https://plot.ly/javascript/heatmaps/#heatmap-with-categorical-axis-labels"
10 | def id = "categorical-axis-heatmap"
11 | def source = CategoricalAxisHeatmapSource.source
12 |
13 | // demo source start
14 |
15 | val data = Seq(
16 | Heatmap()
17 | .withZ(
18 | Seq(
19 | Seq(1, null.asInstanceOf[Int], 30, 50, 1),
20 | Seq(20, 1, 60, 80, 30),
21 | Seq(30, 60, 1, -10, 20)
22 | )
23 | )
24 | .withX(Seq("Monday", "Tuesday", "Wednesday", "Thursday", "Friday"))
25 | .withY(Seq("Morning", "Afternoon", "Evening"))
26 | )
27 |
28 | // demo source end
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/joda-time/src/main/scala/plotly/Joda.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | import org.joda.time._
4 | import plotly.Sequence.DateTimes
5 |
6 | import scala.language.implicitConversions
7 |
8 | object Joda {
9 |
10 | implicit def fromJodaLocalDates(seq: Seq[LocalDate]): Sequence =
11 | fromJodaLocalDateTimes {
12 | seq.map(_.toLocalDateTime(LocalTime.MIDNIGHT))
13 | }
14 |
15 | implicit def fromJodaDateTimes(seq: Seq[DateTime]): Sequence =
16 | fromJodaLocalDateTimes {
17 | seq.map(_.toLocalDateTime)
18 | }
19 |
20 | implicit def fromJodaLocalDateTimes(seq: Seq[LocalDateTime]): Sequence =
21 | DateTimes {
22 | seq.map { d =>
23 | plotly.element.LocalDateTime(
24 | d.getYear,
25 | d.getMonthOfYear,
26 | d.getDayOfMonth,
27 | d.getHourOfDay,
28 | d.getMinuteOfHour,
29 | d.getSecondOfMinute
30 | )
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/histogram/BasicHistogram.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.histogram
2 |
3 | import plotly.Histogram
4 | import plotly.demo.DemoChart
5 | import plotly.layout.Layout
6 |
7 | object BasicHistogram extends DemoChart {
8 |
9 | def plotlyDocUrl = "https://plotly.com/javascript/histograms/#basic-histogram"
10 | def id = this.getClass.getSimpleName.dropRight(1)
11 | def source = BasicHistogramSource.source
12 |
13 | // demo source start
14 |
15 | private val categoryCount = 50
16 |
17 | private val indices = LazyList.from(0).take(categoryCount)
18 | private val categories = indices.map(i => s"name-$i")
19 | private val values = indices.map(_ => math.random())
20 |
21 | val data = Seq(Histogram(values, categories))
22 |
23 | val layout = new Layout()
24 | .withTitle(id)
25 | .withShowlegend(false)
26 | .withHeight(400)
27 | .withWidth(600)
28 |
29 | // demo source end
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/tests/src/test/scala/plotly/SequenceTests.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | import org.scalatest.flatspec.AnyFlatSpec
4 |
5 | import scala.collection.mutable.ArrayBuffer
6 |
7 | class SequenceTests extends AnyFlatSpec {
8 |
9 | "The implicit sequence conversion" should "convert a List to a Sequence" in {
10 | assert((List(1, 2, 3): Sequence) === Sequence.Doubles(List(1d, 2d, 3d)))
11 | }
12 |
13 | it should "convert a mutable ArrayBuffer to a Sequence" in {
14 | assert((ArrayBuffer(1, 2, 3): Sequence) === Sequence.Doubles(List(1d, 2d, 3d)))
15 | }
16 |
17 | it should "convert a nested mutable ArrayBuffer to a Sequence" in {
18 | val mutableNestedDoubles: ArrayBuffer[ArrayBuffer[Double]] = ArrayBuffer(
19 | ArrayBuffer(1d, 2d),
20 | ArrayBuffer(3d, 4d)
21 | )
22 |
23 | val nestedDoublesList: List[List[Double]] = List(
24 | List(1d, 2d),
25 | List(3d, 4d)
26 | )
27 |
28 | assert((mutableNestedDoubles: Sequence) === Sequence.NestedDoubles(nestedDoublesList))
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Line.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | import dataclass.data
5 |
6 | import java.lang.{Double => JDouble}
7 |
8 | @data(optionSetters = true) class Line(
9 | shape: Option[LineShape] = None,
10 | color: Option[OneOrSeq[Color]] = None,
11 | width: Option[OneOrSeq[Double]] = None,
12 | dash: Option[Dash] = None,
13 | outliercolor: Option[Color] = None,
14 | outlierwidth: Option[Double] = None
15 | )
16 |
17 | object Line {
18 | @deprecated("Use Line() and chain-call .with* methods on it instead", "0.8.0")
19 | def apply(
20 | shape: LineShape = null,
21 | color: OneOrSeq[Color] = null,
22 | width: OneOrSeq[Double] = null,
23 | dash: Dash = null,
24 | outliercolor: Color = null,
25 | outlierwidth: JDouble = null
26 | ): Line =
27 | Line(
28 | Option(shape),
29 | Option(color),
30 | Option(width),
31 | Option(dash),
32 | Option(outliercolor),
33 | Option(outlierwidth).map(x => x: Double)
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/linecharts/LineAndScatterPlot.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.linecharts
2 |
3 | import plotly.Scatter
4 | import plotly.demo.DemoChart
5 | import plotly.element.ScatterMode
6 | import plotly.layout.Layout
7 |
8 | object LineAndScatterPlot extends DemoChart {
9 |
10 | def id = "line-and-scatter-plot"
11 |
12 | def plotlyDocUrl = "https://plot.ly/javascript/line-charts/#line-and-scatter-plot"
13 |
14 | def source = LineAndScatterPlotSource.source
15 |
16 | // demo source start
17 |
18 | val trace1 = Scatter(Seq(1, 2, 3, 4), Seq(10, 15, 13, 17))
19 | .withMode(ScatterMode(ScatterMode.Markers))
20 |
21 | val trace2 = Scatter(Seq(2, 3, 4, 5), Seq(16, 5, 11, 9))
22 | .withMode(ScatterMode(ScatterMode.Lines))
23 |
24 | val trace3 = Scatter(Seq(1, 2, 3, 4), Seq(12, 9, 15, 12))
25 | .withMode(ScatterMode(ScatterMode.Lines, ScatterMode.Markers))
26 |
27 | val data = Seq(trace1, trace2, trace3)
28 |
29 | val layout = Layout()
30 | .withTitle("Line and Scatter Plot")
31 |
32 | // demo source end
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/Font.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package layout
3 |
4 | import java.lang.{Integer => JInt}
5 |
6 | import dataclass.data
7 | import plotly.element._
8 |
9 | @data(optionSetters = true) class Font(
10 | size: Option[Int] = None,
11 | family: Option[String] = None,
12 | color: Option[Color] = None
13 | )
14 |
15 | object Font {
16 |
17 | def apply(size: Int, family: String, color: Color): Font =
18 | Font(Some(size), Some(family), Some(color))
19 |
20 | def apply(size: Int, family: String): Font =
21 | Font(Some(size), Some(family), None)
22 |
23 | def apply(size: Int): Font =
24 | Font(Some(size), None, None)
25 |
26 | def apply(color: Color): Font =
27 | Font(None, None, Some(color))
28 |
29 | @deprecated("Use Font() and chain-call .with* methods on it instead", "0.8.0")
30 | def apply(
31 | size: JInt = null,
32 | family: String = null,
33 | color: Color = null
34 | ): Font =
35 | Font(
36 | Option(size).map(x => x: Int),
37 | Option(family),
38 | Option(color)
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/HoverInfo.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | sealed abstract class HoverInfo extends Product with Serializable {
4 | def label: String
5 | }
6 |
7 | object HoverInfo {
8 |
9 | def all: HoverInfo = All
10 | def none: HoverInfo = None
11 | def skip: HoverInfo = Skip
12 | def apply(elements: Element*): HoverInfo =
13 | Combination(elements)
14 |
15 | sealed abstract class Element(override val label: String) extends HoverInfo
16 |
17 | case object X extends Element("x")
18 | case object Y extends Element("y")
19 | case object Z extends Element("z")
20 | case object Text extends Element("text")
21 | case object Name extends Element("name")
22 | case object Color extends Element("color")
23 |
24 | case object All extends HoverInfo {
25 | def label = "all"
26 | }
27 | val None = Combination(Nil)
28 | case object Skip extends HoverInfo {
29 | def label = "skip"
30 | }
31 |
32 | final case class Combination(elements: Seq[Element]) extends HoverInfo {
33 | def label: String = elements.map(_.label).mkString("+")
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/Margin.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package layout
3 |
4 | import java.lang.{Boolean => JBoolean, Integer => JInt}
5 | import dataclass.data
6 |
7 | @data(optionSetters = true) class Margin(
8 | autoexpand: Option[Boolean] = None,
9 | l: Option[Int] = None,
10 | r: Option[Int] = None,
11 | t: Option[Int] = None,
12 | b: Option[Int] = None,
13 | pad: Option[Int] = None
14 | )
15 |
16 | object Margin {
17 | def apply(l: Int, r: Int, t: Int, b: Int): Margin =
18 | Margin().withL(l).withR(r).withT(t).withB(b)
19 |
20 | @deprecated("Use Margin() and chain-call .with* methods on it instead", "0.8.0")
21 | def apply(
22 | autoexpand: JBoolean = null,
23 | l: JInt = null,
24 | r: JInt = null,
25 | t: JInt = null,
26 | b: JInt = null,
27 | pad: JInt = null
28 | ): Margin =
29 | Margin(
30 | Option(autoexpand).map(b => b: Boolean),
31 | Option(l).map(n => n: Int),
32 | Option(r).map(n => n: Int),
33 | Option(t).map(n => n: Int),
34 | Option(b).map(n => n: Int),
35 | Option(pad).map(n => n: Int)
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/bar/CustomizingIndividualBarColors.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.bar
2 |
3 | import plotly._
4 | import plotly.demo.DemoChart
5 | import plotly.element._
6 | import plotly.layout._
7 |
8 | object CustomizingIndividualBarColors extends DemoChart {
9 |
10 | def plotlyDocUrl = ""
11 | def id = "customizing-individual-bar-colors"
12 | def source = CustomizingIndividualBarColorsSource.source
13 |
14 | // demo source start
15 |
16 | val defaultColor = Color.RGBA(204, 204, 204, 1)
17 | val highlightColor = Color.RGBA(222, 45, 38, 0.8)
18 |
19 | val trace1 = Bar(
20 | Seq("Feature A", "Feature B", "Feature C", "Feature D", "Feature E"),
21 | Seq(20, 14, 23, 25, 22)
22 | )
23 | .withMarker(
24 | Marker()
25 | .withColor(
26 | Seq(
27 | defaultColor,
28 | highlightColor,
29 | defaultColor,
30 | defaultColor,
31 | defaultColor
32 | )
33 | )
34 | )
35 |
36 | val data = Seq(trace1)
37 |
38 | val layout = Layout()
39 | .withTitle("Least Used Feature")
40 |
41 | // demo source end
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/LocalDateTime.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | import dataclass.data
4 |
5 | import scala.util.Try
6 |
7 | @data class LocalDateTime(
8 | year: Int,
9 | month: Int,
10 | dayOfMonth: Int,
11 | hour: Int,
12 | minute: Int,
13 | second: Int
14 | ) {
15 | override def toString: String =
16 | f"$year-$month%02d-$dayOfMonth%02d $hour%02d:$minute%02d:$second%02d"
17 | }
18 |
19 | object LocalDateTime extends PlotlyJavaTimeConversions {
20 |
21 | private object IntStr {
22 | def unapply(s: String): Option[Int] =
23 | Try(s.toInt).toOption
24 | }
25 |
26 | def parse(s: String): Option[LocalDateTime] =
27 | s.split(' ') match {
28 | case Array(d, t) =>
29 | (d.split('-'), t.split(':')) match {
30 | case (Array(IntStr(y), IntStr(m), IntStr(d)), Array(IntStr(h), IntStr(min), IntStr(s))) =>
31 | Some(LocalDateTime(y, m, d, h, min, s))
32 | case (Array(IntStr(y), IntStr(m), IntStr(d)), Array(IntStr(h), IntStr(min))) =>
33 | Some(LocalDateTime(y, m, d, h, min, 0))
34 | case _ => None
35 | }
36 | case _ => None
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/demo/src/main/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | plotly-scala demos
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
32 |
33 |
34 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala-2.13/plotly/MutableSequenceImplicitConversions.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | import plotly.Sequence.{DateTimes, Doubles, NestedDoubles, NestedInts, Strings}
4 | import plotly.element.LocalDateTime
5 |
6 | import scala.collection.{Seq => BaseScalaSeq}
7 | import scala.language.implicitConversions
8 |
9 | trait MutableSequenceImplicitConversions {
10 |
11 | implicit def fromMutableDoubleSeq(s: BaseScalaSeq[Double]): Sequence =
12 | Doubles(s.toSeq)
13 | implicit def fromMutableFloatSeq(s: BaseScalaSeq[Float]): Sequence =
14 | Doubles(s.map(_.toDouble).toSeq)
15 | implicit def fromMutableIntSeq(s: BaseScalaSeq[Int]): Sequence =
16 | Doubles(s.map(_.toDouble).toSeq)
17 | implicit def fromMutableLongSeq(s: BaseScalaSeq[Long]): Sequence =
18 | Doubles(s.map(_.toDouble).toSeq)
19 | implicit def fromMutableNestedDoubleSeq(s: BaseScalaSeq[BaseScalaSeq[Double]]): Sequence =
20 | NestedDoubles(s.map(_.toSeq).toSeq)
21 | implicit def fromMutableNestedIntSeq(s: BaseScalaSeq[BaseScalaSeq[Int]]): Sequence =
22 | NestedInts(s.map(_.toSeq).toSeq)
23 | implicit def fromMutableStringSeq(s: BaseScalaSeq[String]): Sequence =
24 | Strings(s.toSeq)
25 | implicit def fromMutableDateTimes(seq: BaseScalaSeq[LocalDateTime]): Sequence =
26 | DateTimes(seq.toSeq)
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/horizontalbarcharts/ColoredBarChart.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.horizontalbarcharts
2 |
3 | import plotly.Bar
4 | import plotly.demo.DemoChart
5 | import plotly.element.{Color, Marker, Orientation}
6 | import plotly.layout.{BarMode, Layout}
7 |
8 | object ColoredBarChart extends DemoChart {
9 |
10 | def plotlyDocUrl = "https://plot.ly/javascript/horizontal-bar-charts/#colored-bar-chart"
11 | def id = "colored-bar-chart"
12 | def source = ColoredBarChartSource.source
13 |
14 | // demo source start
15 |
16 | val trace1 = Bar(Seq(20, 14, 23), Seq("giraffes", "orangutans", "monkeys"))
17 | .withName("SF Zoo")
18 | .withOrientation(Orientation.Horizontal)
19 | .withMarker(
20 | Marker()
21 | .withColor(Color.RGBA(55, 128, 191, 0.6))
22 | .withWidth(1)
23 | )
24 |
25 | val trace2 = Bar(Seq(12, 18, 29), Seq("giraffes", "orangutans", "monkeys"))
26 | .withName("LA Zoo")
27 | .withOrientation(Orientation.Horizontal)
28 | .withMarker(
29 | Marker()
30 | .withColor(Color.RGBA(255, 153, 51, 0.6))
31 | .withWidth(1)
32 | )
33 |
34 | val data = Seq(trace1, trace2)
35 |
36 | val layout = Layout()
37 | .withTitle("Colored Bar Chart")
38 | .withBarmode(BarMode.Stack)
39 |
40 | // demo source end
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/heatmaps/CustomColorScaleHeatmap.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.heatmaps
2 |
3 | import plotly._
4 | import plotly.demo.NoLayoutDemoChart
5 | import plotly.element._
6 |
7 | object CustomColorScaleHeatmap extends NoLayoutDemoChart {
8 |
9 | def plotlyDocUrl = "https://plot.ly/javascript/colorscales/#custom-colorscale-for-contour-plot"
10 | def id = "custom-colorscale-heatmap"
11 | def source = CustomColorScaleHeatmapSource.source
12 |
13 | // demo source start
14 |
15 | val data = Seq(
16 | Heatmap()
17 | .withZ(
18 | Seq(
19 | Seq(10.0, 10.625, 12.5, 15.625, 20.0),
20 | Seq(5.625, 6.25, 8.125, 11.25, 15.625),
21 | Seq(2.5, 3.125, 5.0, 8.125, 12.5),
22 | Seq(0.625, 1.25, 3.125, 6.25, 10.625),
23 | Seq(0.0, 0.625, 2.5, 5.625, 10.0)
24 | )
25 | )
26 | .withColorscale(
27 | ColorScale.CustomScale(
28 | Seq(
29 | (0, Color.RGB(166, 206, 227)),
30 | (0.25, Color.RGB(31, 120, 180)),
31 | (0.45, Color.RGB(178, 223, 138)),
32 | (0.65, Color.RGB(51, 160, 44)),
33 | (0.85, Color.RGB(251, 154, 153)),
34 | (1, Color.RGB(227, 26, 28))
35 | )
36 | )
37 | )
38 | )
39 |
40 | // demo source end
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/tests/src/test/scala/plotly/FieldTests.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | import argonaut.Json
4 | import org.scalatest.flatspec.AnyFlatSpec
5 | import org.scalatest.matchers.should.Matchers
6 |
7 | class FieldTests extends AnyFlatSpec with Matchers {
8 |
9 | def traceHasShowlegendField(trace: Trace): Unit = {
10 |
11 | val expectedField = Json.jBool(true)
12 |
13 | val field = Codecs
14 | .argonautEncodeTrace(trace)
15 | .obj
16 | .toList
17 | .flatMap(_.toList.filter(_._1 == "showlegend").map(_._2))
18 |
19 | assert(field === List(expectedField))
20 | }
21 |
22 | "Bar" should "have a showlegend field" in {
23 |
24 | val bar = Bar(1 to 10, 1 to 10)
25 | .withShowlegend(true)
26 |
27 | traceHasShowlegendField(bar)
28 | }
29 |
30 | "Box" should "have a showlegend field" in {
31 |
32 | val box = Box(1 to 10)
33 | .withShowlegend(true)
34 |
35 | traceHasShowlegendField(box)
36 | }
37 |
38 | "Histogram" should "have a showlegend field" in {
39 |
40 | val histogram = Histogram(1 to 10)
41 | .withShowlegend(true)
42 |
43 | traceHasShowlegendField(histogram)
44 | }
45 |
46 | "Scatter" should "have a showlegend field" in {
47 |
48 | val scatter = Scatter(1 to 10)
49 | .withShowlegend(true)
50 |
51 | traceHasShowlegendField(scatter)
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - master
6 | tags:
7 | - "v*"
8 | pull_request:
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | SCALA_VERSION: ["2.12.19", "2.13.14"]
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: coursier/cache-action@v6
20 | - uses: coursier/setup-action@v1.3.5
21 | with:
22 | jvm: 8
23 | - run: |
24 | sbtn ++$SCALA_VERSION test
25 | sbtn ++$SCALA_VERSION mimaReportBinaryIssues
26 | env:
27 | SCALA_VERSION: ${{ matrix.SCALA_VERSION }}
28 |
29 | publish:
30 | if: github.event_name == 'push'
31 | runs-on: ubuntu-latest
32 | steps:
33 | - uses: actions/checkout@v4
34 | - uses: coursier/cache-action@v6
35 | - uses: coursier/setup-action@v1.3.5
36 | with:
37 | jvm: 8
38 | - run: .github/scripts/gpg-setup.sh
39 | env:
40 | PGP_SECRET: ${{ secrets.PUBLISH_SECRET_KEY }}
41 | - name: Release
42 | run: sbtn ci-release
43 | env:
44 | PGP_PASSPHRASE: ${{ secrets.PUBLISH_SECRET_KEY_PASSWORD }}
45 | PGP_SECRET: ${{ secrets.PUBLISH_SECRET_KEY }}
46 | SONATYPE_PASSWORD: ${{ secrets.PUBLISH_PASSWORD }}
47 | SONATYPE_USERNAME: ${{ secrets.PUBLISH_USER }}
48 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/bubblecharts/HoverOnTextBubbleChart.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.bubblecharts
2 |
3 | import plotly.Scatter
4 | import plotly.demo.DemoChart
5 | import plotly.element.{Color, Marker, ScatterMode}
6 | import plotly.layout.Layout
7 |
8 | object HoverOnTextBubbleChart extends DemoChart {
9 |
10 | def plotlyDocUrl = "https://plot.ly/javascript/bubble-charts/#hover-text-on-bubble-charts"
11 | def id = "hover-on-text-bubble-chart"
12 | def source = HoverOnTextBubbleChartSource.source
13 |
14 | // demo source start
15 |
16 | val trace1 = Scatter(Seq(1, 2, 3, 4), Seq(10, 11, 12, 13))
17 | .withText(
18 | Seq(
19 | """A
20 | size = 40""",
21 | """B
22 | size = 60""",
23 | """C
24 | size = 80""",
25 | """D
26 | size = 100"""
27 | )
28 | )
29 | .withMode(ScatterMode(ScatterMode.Markers))
30 | .withMarker(
31 | Marker()
32 | .withColor(
33 | Seq(Color.RGB(93, 164, 214), Color.RGB(255, 144, 14), Color.RGB(44, 160, 101), Color.RGB(255, 65, 54))
34 | )
35 | .withSize(Seq(40, 60, 80, 100))
36 | )
37 |
38 | val data = Seq(trace1)
39 |
40 | val layout = Layout()
41 | .withTitle("Bubble Chart Hover Text")
42 | .withShowlegend(false)
43 | .withHeight(400)
44 | .withWidth(600)
45 |
46 | // demo source end
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Marker.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | import java.lang.{Double => JDouble}
5 |
6 | import dataclass.data
7 |
8 | @data(optionSetters = true) class Marker(
9 | size: Option[OneOrSeq[Int]] = None,
10 | color: Option[OneOrSeq[Color]] = None,
11 | opacity: Option[OneOrSeq[Double]] = None,
12 | line: Option[Line] = None,
13 | symbol: Option[OneOrSeq[Symbol]] = None,
14 | outliercolor: Option[Color] = None,
15 | sizeref: Option[Double] = None,
16 | sizemode: Option[SizeMode] = None,
17 | width: Option[OneOrSeq[Int]] = None
18 | )
19 |
20 | object Marker {
21 | @deprecated("Use Marker() and chain-call .with* methods on it instead", "0.8.0")
22 | def apply(
23 | size: OneOrSeq[Int] = null,
24 | color: OneOrSeq[Color] = null,
25 | opacity: OneOrSeq[Double] = null,
26 | line: Line = null,
27 | symbol: OneOrSeq[Symbol] = null,
28 | outliercolor: Color = null,
29 | sizeref: JDouble = null,
30 | sizemode: SizeMode = null,
31 | width: OneOrSeq[Int] = null
32 | ): Marker =
33 | Marker(
34 | Option(size),
35 | Option(color),
36 | Option(opacity),
37 | Option(line),
38 | Option(symbol),
39 | Option(outliercolor),
40 | Option(sizeref).map(d => d: Double),
41 | Option(sizemode),
42 | Option(width)
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/Sequence.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | import plotly.element.LocalDateTime
4 |
5 | import scala.language.implicitConversions
6 |
7 | sealed abstract class Sequence extends Product with Serializable
8 |
9 | object Sequence extends MutableSequenceImplicitConversions {
10 | final case class Doubles(seq: Seq[Double]) extends Sequence
11 | final case class NestedDoubles(seq: Seq[Seq[Double]]) extends Sequence
12 | final case class NestedInts(seq: Seq[Seq[Int]]) extends Sequence
13 | final case class Strings(seq: Seq[String]) extends Sequence
14 | final case class DateTimes(seq: Seq[LocalDateTime]) extends Sequence
15 |
16 | implicit def fromDoubleSeq(s: Seq[Double]): Sequence =
17 | Doubles(s)
18 | implicit def fromFloatSeq(s: Seq[Float]): Sequence =
19 | Doubles(s.map(_.toDouble))
20 | implicit def fromIntSeq(s: Seq[Int]): Sequence =
21 | Doubles(s.map(_.toDouble))
22 | implicit def fromLongSeq(s: Seq[Long]): Sequence =
23 | Doubles(s.map(_.toDouble))
24 | implicit def fromNestedDoubleSeq(s: Seq[Seq[Double]]): Sequence =
25 | NestedDoubles(s)
26 | implicit def fromNestedIntSeq(s: Seq[Seq[Int]]): Sequence =
27 | NestedInts(s)
28 | implicit def fromStringSeq(s: Seq[String]): Sequence =
29 | Strings(s)
30 | implicit def fromDateTimes(seq: Seq[LocalDateTime]): Sequence =
31 | DateTimes(seq)
32 | }
33 |
--------------------------------------------------------------------------------
/render/shared/src/main/scala/plotly/Enumerate.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | import shapeless.{:+:, ::, CNil, Coproduct, Generic, HList, HNil, Inl, Inr, Strict}
4 |
5 | sealed abstract class Enumerate[T] {
6 | def apply(): Seq[T]
7 | }
8 |
9 | object Enumerate {
10 | def apply[T](implicit enumerate: Enumerate[T]): Enumerate[T] = enumerate
11 |
12 | private def instance[T](values: => Seq[T]): Enumerate[T] =
13 | new Enumerate[T] {
14 | def apply() = values
15 | }
16 |
17 | implicit val boolean: Enumerate[Boolean] =
18 | instance(Seq(true, false))
19 |
20 | implicit val hnil: Enumerate[HNil] =
21 | instance(Seq(HNil))
22 | implicit def hcons[H, T <: HList](implicit
23 | head: Strict[Enumerate[H]],
24 | tail: Enumerate[T]
25 | ): Enumerate[H :: T] =
26 | instance {
27 | for {
28 | h <- head.value()
29 | t <- tail()
30 | } yield h :: t
31 | }
32 |
33 | implicit val cnil: Enumerate[CNil] =
34 | instance(Seq())
35 | implicit def ccons[H, T <: Coproduct](implicit
36 | head: Strict[Enumerate[H]],
37 | tail: Enumerate[T]
38 | ): Enumerate[H :+: T] =
39 | instance(head.value().map(Inl(_)) ++ tail().map(Inr(_)))
40 |
41 | implicit def generic[F, G](implicit
42 | gen: Generic.Aux[F, G],
43 | underlying: Strict[Enumerate[G]]
44 | ): Enumerate[F] =
45 | instance(underlying.value().map(gen.from))
46 | }
47 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/Annotation.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package layout
3 |
4 | import java.lang.{Boolean => JBoolean, Double => JDouble}
5 |
6 | import dataclass.data
7 | import plotly.element._
8 |
9 | @data(optionSetters = true) class Annotation(
10 | xref: Option[Ref] = None,
11 | yref: Option[Ref] = None,
12 | x: Option[Element] = None,
13 | y: Option[Element] = None,
14 | xanchor: Option[Anchor] = None,
15 | yanchor: Option[Anchor] = None,
16 | text: Option[Element] = None,
17 | font: Option[Font] = None,
18 | showarrow: Option[Boolean] = None,
19 | @since("0.8.0")
20 | ax: Option[Double] = None,
21 | ay: Option[Double] = None
22 | )
23 |
24 | object Annotation {
25 | @deprecated("Use Annotation() and chain-call .with* methods on it instead", "0.8.0")
26 | def apply(
27 | xref: Ref = null,
28 | yref: Ref = null,
29 | x: Element = null,
30 | y: Element = null,
31 | xanchor: Anchor = null,
32 | yanchor: Anchor = null,
33 | text: Element = null,
34 | font: Font = null,
35 | showarrow: JBoolean = null
36 | ): Annotation =
37 | Annotation(
38 | Option(xref),
39 | Option(yref),
40 | Option(x),
41 | Option(y),
42 | Option(xanchor),
43 | Option(yanchor),
44 | Option(text),
45 | Option(font),
46 | Option(showarrow).map(v => v: Boolean)
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/histogram/StyledBasicHistogram.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.histogram
2 |
3 | import plotly.Histogram
4 | import plotly.demo.DemoChart
5 | import plotly.layout.{Axis, Layout, RangeSlider}
6 | import plotly.element._
7 |
8 | object StyledBasicHistogram extends DemoChart {
9 |
10 | def plotlyDocUrl = "https://plotly.com/javascript/histograms/#basic-histogram"
11 | def id = this.getClass.getSimpleName.dropRight(1)
12 | def source = StyledBasicHistogramSource.source
13 |
14 | // demo source start
15 |
16 | private val categoryCount = 50
17 |
18 | private val indices = LazyList.from(0).take(categoryCount)
19 | private val categories = indices.map(i => s"name-$i for $id")
20 | private val values = indices.map(_ => math.random())
21 |
22 | val data = Seq(
23 | Histogram(values, categories)
24 | .withMarker(new Marker().withColor(Color.StringColor("#004A72")))
25 | .withHovertext(categories.map(c => s"$c with hover text"))
26 | )
27 |
28 | val xAxis = new Axis()
29 | .withRange((0d, 2d))
30 | .withRangeslider(RangeSlider())
31 |
32 | val yAxis = new Axis()
33 | .withTitle("Count")
34 | .withFixedrange(true)
35 | .withTickformat(".1f")
36 |
37 | val layout = new Layout()
38 | .withXaxis(xAxis)
39 | .withYaxis(yAxis)
40 | .withTitle(id)
41 | .withShowlegend(false)
42 | .withHeight(400)
43 | .withWidth(600)
44 |
45 | // demo source end
46 | }
47 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/Legend.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package layout
3 |
4 | import java.lang.{Double => JDouble}
5 |
6 | import dataclass._
7 | import plotly.element._
8 |
9 | @data(optionSetters = true) class Legend(
10 | x: Option[Double] = None,
11 | y: Option[Double] = None,
12 | traceorder: Option[TraceOrder] = None,
13 | yref: Option[Ref] = None,
14 | font: Option[Font] = None,
15 | bordercolor: Option[Color] = None,
16 | bgcolor: Option[Color] = None,
17 | xanchor: Option[Anchor] = None,
18 | yanchor: Option[Anchor] = None,
19 | @since
20 | orientation: Option[Orientation] = None
21 | )
22 |
23 | object Legend {
24 | @deprecated("Use Legend() and chain-call .with* methods on it instead", "0.8.0")
25 | def apply(
26 | x: JDouble = null,
27 | y: JDouble = null,
28 | traceorder: TraceOrder = null,
29 | yref: Ref = null,
30 | font: Font = null,
31 | bordercolor: Color = null,
32 | bgcolor: Color = null,
33 | xanchor: Anchor = null,
34 | yanchor: Anchor = null,
35 | orientation: Orientation = null
36 | ): Legend =
37 | Legend(
38 | Option(x).map(v => v: Double),
39 | Option(y).map(v => v: Double),
40 | Option(traceorder),
41 | Option(yref),
42 | Option(font),
43 | Option(bordercolor),
44 | Option(bgcolor),
45 | Option(xanchor),
46 | Option(yanchor),
47 | Option(orientation)
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/bar/BarChartWithDirectLabels.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.bar
2 |
3 | import plotly._
4 | import plotly.demo.DemoChart
5 | import plotly.element._
6 | import plotly.layout._
7 |
8 | object BarChartWithDirectLabels extends DemoChart {
9 |
10 | def plotlyDocUrl = "https://plot.ly/javascript/bar-charts/#bar-chart-with-direct-labels"
11 | def id = "bar-chart-with-direct-labels"
12 | def source = BarChartWithDirectLabelsSource.source
13 |
14 | // demo source start
15 |
16 | val xValue = Seq("Product A", "Product B", "Product C")
17 |
18 | val yValue = Seq(20, 14, 23)
19 |
20 | val trace1 = Bar(xValue, yValue)
21 | .withText(Seq("27% market share", "24% market share", "19% market share"))
22 | .withMarker(
23 | Marker()
24 | .withColor(Color.RGB(158, 202, 225))
25 | .withOpacity(0.6)
26 | .withLine(
27 | Line()
28 | .withColor(Color.RGB(8, 48, 107))
29 | .withWidth(1.5)
30 | )
31 | )
32 |
33 | val data = Seq(trace1)
34 |
35 | val annotations = xValue.zip(yValue).map { case (x, y) =>
36 | Annotation()
37 | .withX(x)
38 | .withY(y)
39 | .withText(y.toString)
40 | .withXanchor(Anchor.Center)
41 | .withYanchor(Anchor.Bottom)
42 | .withShowarrow(false)
43 | }
44 |
45 | val layout = Layout()
46 | .withTitle("January 2013 Sales Report")
47 | .withAnnotations(annotations)
48 |
49 | // demo source end
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/heatmaps/AnnotatedHeatmap.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.heatmaps
2 |
3 | import plotly._
4 | import plotly.demo.DemoChart
5 | import plotly.element._
6 | import plotly.layout._
7 |
8 | object AnnotatedHeatmap extends DemoChart {
9 |
10 | def plotlyDocUrl = "https://plot.ly/javascript/heatmaps/#annotated-heatmap"
11 | def id = "annotated-heatmap"
12 | def source = AnnotatedHeatmapSource.source
13 |
14 | // demo source start
15 |
16 | val x = Seq("A", "B", "C", "D", "E");
17 | val y = Seq("W", "X", "Y", "Z");
18 | val z = Seq(
19 | Seq(0.00, 0.00, 0.75, 0.75, 0.00),
20 | Seq(0.00, 0.00, 0.75, 0.75, 0.00),
21 | Seq(0.75, 0.75, 0.75, 0.75, 0.75),
22 | Seq(0.00, 0.00, 0.00, 0.75, 0.00)
23 | )
24 |
25 | val data = Seq(
26 | Heatmap(z, x, y)
27 | .withShowscale(false)
28 | .withColorscale(
29 | ColorScale.CustomScale(
30 | Seq(
31 | (0, Color.StringColor("#3D9970")),
32 | (1, Color.StringColor("#001f3f"))
33 | )
34 | )
35 | )
36 | )
37 |
38 | val layout = Layout()
39 | .withTitle("Annotated Heatmap")
40 | .withXaxis(Axis().withTicks(Ticks.Empty).withSide(Side.Top))
41 | .withYaxis(Axis().withTicks(Ticks.Empty).withTicksuffix(" "))
42 | .withAnnotations(
43 | for {
44 | (xv, xi) <- x.zipWithIndex
45 | (yv, yi) <- y.zipWithIndex
46 | } yield Annotation()
47 | .withX(xv)
48 | .withY(yv)
49 | .withXref(Ref.Axis(AxisReference.X1))
50 | .withYref(Ref.Axis(AxisReference.Y1))
51 | .withShowarrow(false)
52 | .withText(z(yi)(xi).toString)
53 | .withFont(Font(Color.StringColor("white")))
54 | )
55 |
56 | // demo source end
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/core/jvm/src/main/scala/plotly/element/PlotlyJavaTimeConversions.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | import java.time._
4 | import plotly.element.{LocalDateTime => PlotlyLocalDateTime}
5 |
6 | import scala.language.implicitConversions
7 |
8 | trait PlotlyJavaTimeConversions {
9 |
10 | implicit def fromJavaLocalDateTime(javaLocalDateTime: java.time.LocalDateTime): PlotlyLocalDateTime =
11 | PlotlyLocalDateTime(
12 | javaLocalDateTime.getYear,
13 | javaLocalDateTime.getMonthValue,
14 | javaLocalDateTime.getDayOfMonth,
15 | javaLocalDateTime.getHour,
16 | javaLocalDateTime.getMinute,
17 | javaLocalDateTime.getSecond
18 | )
19 |
20 | implicit def fromJavaInstant(javaInstant: Instant): PlotlyLocalDateTime =
21 | fromJavaLocalDateTime(javaInstant.atOffset(ZoneOffset.UTC).toLocalDateTime)
22 |
23 | implicit def fromJavaLocalDate(javaLocalDate: LocalDate): PlotlyLocalDateTime =
24 | fromJavaLocalDateTime(javaLocalDate.atStartOfDay)
25 |
26 | /** Implicit conversions in this object convert to `plotly.element.LocalDateTime` by simply dropping timezone/offset
27 | * information. This can lead to unexpected behaviour, particularly for datasets with varying offsets and timezones.
28 | * It will generally be safer to convert your data to `java.time.LocalDateTime` in the appropriate timezone/offset,
29 | * and then use the `PlotlyJavaTimeConversions.fromJavaLocalDateTime` implicit conversion.
30 | */
31 | object UnsafeImplicitConversions {
32 |
33 | implicit def fromJavaOffsetDateTime(javaOffsetDateTime: OffsetDateTime): PlotlyLocalDateTime =
34 | fromJavaLocalDateTime(javaOffsetDateTime.toLocalDateTime)
35 |
36 | implicit def fromJavaZonedDateTime(javaZonedDateTime: ZonedDateTime): PlotlyLocalDateTime =
37 | fromJavaLocalDateTime(javaZonedDateTime.toLocalDateTime)
38 |
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/tests/src/test/scala/plotly/element/LocalDateTimeTests.scala:
--------------------------------------------------------------------------------
1 | package plotly.element
2 |
3 | import org.scalatest.flatspec.AnyFlatSpec
4 | import plotly.element.LocalDateTime.UnsafeImplicitConversions._
5 | import plotly.element.{LocalDateTime => PlotlyLocalDateTime}
6 |
7 | class LocalDateTimeTests extends AnyFlatSpec {
8 |
9 | "JavaTime conversions" should "convert java.time.LocalDateTime to Plotly LocalDateTime" in {
10 | val javaLocalDateTime = java.time.LocalDateTime.parse("2020-04-18T14:52:52")
11 | val plotlyLocalDateTime = PlotlyLocalDateTime(2020, 4, 18, 14, 52, 52)
12 |
13 | assert((javaLocalDateTime: PlotlyLocalDateTime) === plotlyLocalDateTime)
14 | }
15 |
16 | it should "convert java.time.Instant to Plotly LocalDateTime using UTC" in {
17 | val javaInstant = java.time.Instant.parse("2020-04-18T14:52:52Z")
18 | val plotlyLocalDateTime = PlotlyLocalDateTime(2020, 4, 18, 14, 52, 52)
19 |
20 | assert((javaInstant: PlotlyLocalDateTime) === plotlyLocalDateTime)
21 | }
22 |
23 | it should "convert java.time.OffsetDateTime to Plotly LocalDateTime" in {
24 | val javaOffsetDateTime = java.time.OffsetDateTime.parse("2020-04-18T14:52:52+10:00")
25 | val plotlyLocalDateTime = PlotlyLocalDateTime(2020, 4, 18, 14, 52, 52)
26 |
27 | assert((javaOffsetDateTime: PlotlyLocalDateTime) === plotlyLocalDateTime)
28 | }
29 |
30 | it should "convert java.time.ZonedDateTime to Plotly LocalDateTime" in {
31 | val javaOffsetDateTime = java.time.ZonedDateTime.parse("2020-04-18T14:52:52+10:00[Australia/Melbourne]")
32 | val plotlyLocalDateTime = PlotlyLocalDateTime(2020, 4, 18, 14, 52, 52)
33 |
34 | assert((javaOffsetDateTime: PlotlyLocalDateTime) === plotlyLocalDateTime)
35 | }
36 |
37 | it should "convert java.time.LocalDate to Plotly LocalDateTime" in {
38 | val javaLocalDate = java.time.LocalDate.parse("2020-04-18")
39 | val plotlyLocalDateTime = PlotlyLocalDateTime(2020, 4, 18, 0, 0, 0)
40 |
41 | assert((javaLocalDate: PlotlyLocalDateTime) === plotlyLocalDateTime)
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Error.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 |
4 | import java.lang.{Boolean => JBoolean, Double => JDouble}
5 |
6 | import dataclass._
7 |
8 | sealed abstract class Error(val `type`: String) extends Product with Serializable
9 |
10 | object Error {
11 | @data(optionSetters = true) class Data(
12 | array: Seq[Double],
13 | @since
14 | visible: Option[Boolean] = None,
15 | symmetric: Option[Boolean] = None,
16 | arrayminus: Option[Seq[Double]] = None
17 | ) extends Error("data")
18 |
19 | object Data {
20 | @deprecated("Use Data(array) and chain-call .with* methods on it instead", "0.8.0")
21 | def apply(
22 | array: Seq[Double],
23 | visible: JBoolean = null,
24 | symmetric: JBoolean = null,
25 | arrayminus: Seq[Double] = null
26 | ): Data =
27 | Data(
28 | array,
29 | Option(visible).map(x => x: Boolean),
30 | Option(symmetric).map(x => x: Boolean),
31 | Option(arrayminus)
32 | )
33 | }
34 |
35 | @data(optionSetters = true) class Percent(
36 | value: Double,
37 | @since
38 | visible: Option[Boolean] = None,
39 | symmetric: Option[Boolean] = None,
40 | valueminus: Option[Double] = None
41 | ) extends Error("percent")
42 |
43 | object Percent {
44 | @deprecated("Use Percent(value) and chain-call .with* methods on it instead", "0.8.0")
45 | def apply(
46 | value: Double,
47 | visible: JBoolean = null,
48 | symmetric: JBoolean = null,
49 | valueminus: JDouble = null
50 | ): Percent =
51 | Percent(
52 | value,
53 | Option(visible).map(x => x: Boolean),
54 | Option(symmetric).map(x => x: Boolean),
55 | Option(valueminus).map(d => d: Double)
56 | )
57 | }
58 |
59 | @data(optionSetters = true) class Constant(
60 | value: Double,
61 | color: Option[String] = None,
62 | thickness: Option[Double] = None,
63 | opacity: Option[Double] = None,
64 | width: Option[Double] = None
65 | ) extends Error("constant")
66 |
67 | object Constant {
68 | @deprecated("Use Constant(value) and chain-call .with* methods on it instead", "0.8.0")
69 | def apply(
70 | value: Double,
71 | color: String = null,
72 | thickness: JDouble = null,
73 | opacity: JDouble = null,
74 | width: JDouble = null
75 | ): Constant =
76 | Constant(
77 | value,
78 | Option(color),
79 | Option(thickness).map(x => x: Double),
80 | Option(opacity).map(x => x: Double),
81 | Option(width).map(x => x: Double)
82 | )
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/bar/WaterfallBarChart.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.bar
2 |
3 | import plotly.Bar
4 | import plotly.demo.DemoChart
5 | import plotly.element.{Color, Line, Marker}
6 | import plotly.layout.{Annotation, BarMode, Font, Layout}
7 |
8 | object WaterfallBarChart extends DemoChart {
9 |
10 | def plotlyDocUrl = "https://plot.ly/javascript/bar-charts/#waterfall-bar-chart"
11 | def id = "waterfall-bar-chart"
12 | def source = WaterfallBarChartSource.source
13 |
14 | // demo source start
15 |
16 | val xData = Seq(
17 | "Product Revenue",
18 | "Services Revenue",
19 | "Total Revenue",
20 | "Fixed Costs",
21 | "Variable Costs",
22 | "Total Costs",
23 | "Total"
24 | )
25 |
26 | val yData = Seq(400, 660, 660, 590, 400, 400, 340)
27 |
28 | val textList = Seq("$430K", "$260K", "$690K", "$-120K", "$-200K", "$-320K", "$370K")
29 |
30 | // Base
31 |
32 | val trace1 = Bar(xData, Seq(0, 430, 0, 570, 370, 370, 0))
33 | .withMarker(
34 | Marker()
35 | .withColor(Color.RGBA(1, 1, 1, 0.0))
36 | )
37 |
38 | // Revenue
39 |
40 | val trace2 = Bar(xData, Seq(430, 260, 690, 0, 0, 0, 0))
41 | .withMarker(
42 | Marker()
43 | .withColor(Color.RGBA(55, 128, 191, 0.7))
44 | .withLine(
45 | Line()
46 | .withColor(Color.RGBA(55, 128, 191, 1.0))
47 | .withWidth(2.0)
48 | )
49 | )
50 |
51 | // Cost
52 |
53 | val trace3 = Bar(xData, Seq(0, 0, 0, 120, 200, 320, 0))
54 | .withMarker(
55 | Marker()
56 | .withColor(Color.RGBA(219, 64, 82, 0.7))
57 | .withLine(
58 | Line()
59 | .withColor(Color.RGBA(219, 64, 82, 1.0))
60 | .withWidth(2.0)
61 | )
62 | )
63 |
64 | // Profit
65 |
66 | val trace4 = Bar(xData, Seq(0, 0, 0, 0, 0, 0, 370))
67 | .withMarker(
68 | Marker()
69 | .withColor(Color.RGBA(50, 171, 96, 0.7))
70 | .withLine(
71 | Line()
72 | .withColor(Color.RGBA(50, 171, 96, 1.0))
73 | .withWidth(2.0)
74 | )
75 | )
76 |
77 | val data = Seq(trace1, trace2, trace3, trace4)
78 |
79 | val annotations = xData.zip(yData).zip(textList).map { case ((x, y), text) =>
80 | Annotation()
81 | .withX(x)
82 | .withY(y)
83 | .withText(text)
84 | .withFont(
85 | Font(
86 | family = "Arial",
87 | size = 14,
88 | color = Color.RGBA(245, 246, 249, 1)
89 | )
90 | )
91 | .withShowarrow(false)
92 | }
93 |
94 | val layout = Layout()
95 | .withTitle("Annual Profit 2015")
96 | .withBarmode(BarMode.Stack)
97 | .withPaper_bgcolor(Color.RGBA(245, 246, 249, 1))
98 | .withPlot_bgcolor(Color.RGBA(245, 246, 249, 1))
99 | .withWidth(600)
100 | .withHeight(400)
101 | .withShowlegend(false)
102 | .withAnnotations(annotations)
103 |
104 | // demo source end
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/lineandscatter/CategoricalDotPlot.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo.lineandscatter
2 |
3 | import plotly.Scatter
4 | import plotly.demo.DemoChart
5 | import plotly.element._
6 | import plotly.layout._
7 |
8 | object CategoricalDotPlot extends DemoChart {
9 |
10 | val id = "categorical-dot-plot"
11 |
12 | val plotlyDocUrl = "https://plot.ly/javascript/line-and-scatter/#categorical-dot-plot"
13 |
14 | def source = CategoricalDotPlotSource.source
15 |
16 | // demo source start
17 |
18 | val country = Seq(
19 | "Switzerland (2011)",
20 | "Chile (2013)",
21 | "Japan (2014)",
22 | "United States (2012)",
23 | "Slovenia (2014)",
24 | "Canada (2011)",
25 | "Poland (2010)",
26 | "Estonia (2015)",
27 | "Luxembourg (2013)",
28 | "Portugal (2011)"
29 | )
30 |
31 | val votingPop = Seq(
32 | 40.0, 45.7, 52.0, 53.6, 54.1, 54.2, 54.5, 54.7, 55.1, 56.6
33 | )
34 |
35 | val regVoters = Seq(
36 | 49.1, 42.0, 52.7, 84.3, 51.7, 61.1, 55.3, 64.2, 91.1, 58.9
37 | )
38 |
39 | val trace1 = Scatter(votingPop, country)
40 | .withMode(ScatterMode(ScatterMode.Markers))
41 | .withName("Percent of estimated voting age population")
42 | .withMarker(
43 | Marker()
44 | .withColor(Color.RGBA(156, 165, 196, 0.95))
45 | .withLine(
46 | Line()
47 | .withColor(Color.RGBA(156, 165, 196, 1.0))
48 | .withWidth(1.0)
49 | )
50 | .withSymbol(Symbol.Circle())
51 | .withSize(16)
52 | )
53 |
54 | val trace2 = Scatter(regVoters, country)
55 | .withMode(ScatterMode(ScatterMode.Markers))
56 | .withName("Percent of estimated registered voters")
57 | .withMarker(
58 | Marker()
59 | .withColor(Color.RGBA(204, 204, 204, 0.95))
60 | .withLine(
61 | Line()
62 | .withColor(Color.RGBA(217, 217, 217, 1.0))
63 | .withWidth(1.0)
64 | )
65 | .withSymbol(Symbol.Circle())
66 | .withSize(16)
67 | )
68 |
69 | val data = Seq(trace1, trace2)
70 |
71 | val layout = Layout()
72 | .withTitle("Votes cast for ten lowest voting age population in OECD countries")
73 | .withXaxis(
74 | Axis()
75 | .withShowgrid(false)
76 | .withShowline(true)
77 | .withLinecolor(Color.RGB(102, 102, 102))
78 | .withTitlefont(
79 | Font(Color.RGB(204, 204, 204))
80 | )
81 | .withTickfont(
82 | Font(Color.RGB(102, 102, 102))
83 | )
84 | .withAutotick(false)
85 | .withDtick(10.0)
86 | .withTicks(Ticks.Outside)
87 | .withTickcolor(Color.RGB(102, 102, 102))
88 | )
89 | .withMargin(
90 | Margin(
91 | l = 140,
92 | r = 40,
93 | b = 50,
94 | t = 80
95 | )
96 | )
97 | .withLegend(
98 | Legend()
99 | .withFont(Font(size = 10))
100 | .withYanchor(Anchor.Middle)
101 | .withXanchor(Anchor.Right)
102 | )
103 | .withWidth(600)
104 | .withHeight(400)
105 | .withPaper_bgcolor(Color.RGB(254, 247, 234))
106 | .withPlot_bgcolor(Color.RGB(254, 247, 234))
107 | .withHovermode(HoverMode.Closest)
108 |
109 | // demo source end
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/Layout.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package layout
3 |
4 | import java.lang.{Boolean => JBoolean, Double => JDouble, Integer => JInt}
5 |
6 | import dataclass.data
7 | import plotly.element._
8 |
9 | @data(optionSetters = true) class Layout(
10 | title: Option[String] = None,
11 | legend: Option[Legend] = None,
12 | width: Option[Int] = None,
13 | height: Option[Int] = None,
14 | showlegend: Option[Boolean] = None,
15 | xaxis: Option[Axis] = None,
16 | yaxis: Option[Axis] = None,
17 | xaxis1: Option[Axis] = None,
18 | xaxis2: Option[Axis] = None,
19 | xaxis3: Option[Axis] = None,
20 | xaxis4: Option[Axis] = None,
21 | yaxis1: Option[Axis] = None,
22 | yaxis2: Option[Axis] = None,
23 | yaxis3: Option[Axis] = None,
24 | yaxis4: Option[Axis] = None,
25 | barmode: Option[BarMode] = None,
26 | autosize: Option[Boolean] = None,
27 | margin: Option[Margin] = None,
28 | annotations: Option[Seq[Annotation]] = None,
29 | plot_bgcolor: Option[Color] = None,
30 | paper_bgcolor: Option[Color] = None,
31 | font: Option[Font] = None,
32 | bargap: Option[Double] = None,
33 | bargroupgap: Option[Double] = None,
34 | hovermode: Option[HoverMode] = None,
35 | boxmode: Option[BoxMode] = None,
36 | scene: Option[Scene] = None,
37 | @since("0.8.0")
38 | dragmode: Option[String] = None,
39 | shapes: Option[Seq[Shape]] = None,
40 | @since("0.8.2")
41 | grid: Option[Grid] = None
42 | )
43 |
44 | object Layout {
45 | @deprecated("Use Layout() and chain-call .with* methods on it instead", "0.8.0")
46 | def apply(
47 | title: String = null,
48 | legend: Legend = null,
49 | width: JInt = null,
50 | height: JInt = null,
51 | showlegend: JBoolean = null,
52 | xaxis: Axis = null,
53 | yaxis: Axis = null,
54 | xaxis1: Axis = null,
55 | xaxis2: Axis = null,
56 | xaxis3: Axis = null,
57 | xaxis4: Axis = null,
58 | yaxis1: Axis = null,
59 | yaxis2: Axis = null,
60 | yaxis3: Axis = null,
61 | yaxis4: Axis = null,
62 | barmode: BarMode = null,
63 | autosize: JBoolean = null,
64 | margin: Margin = null,
65 | annotations: Seq[Annotation] = null,
66 | plot_bgcolor: Color = null,
67 | paper_bgcolor: Color = null,
68 | font: Font = null,
69 | bargap: JDouble = null,
70 | bargroupgap: JDouble = null,
71 | hovermode: HoverMode = null,
72 | boxmode: BoxMode = null,
73 | scene: Scene = null
74 | ): Layout =
75 | new Layout(
76 | Option(title),
77 | Option(legend),
78 | Option(width).map(x => x),
79 | Option(height).map(x => x),
80 | Option(showlegend).map(x => x),
81 | Option(xaxis),
82 | Option(yaxis),
83 | Option(xaxis1),
84 | Option(xaxis2),
85 | Option(xaxis3),
86 | Option(xaxis4),
87 | Option(yaxis1),
88 | Option(yaxis2),
89 | Option(yaxis3),
90 | Option(yaxis4),
91 | Option(barmode),
92 | Option(autosize).map(x => x),
93 | Option(margin),
94 | Option(annotations),
95 | Option(plot_bgcolor),
96 | Option(paper_bgcolor),
97 | Option(font),
98 | Option(bargap).map(x => x),
99 | Option(bargroupgap).map(x => x),
100 | Option(hovermode),
101 | Option(boxmode),
102 | Option(scene)
103 | )
104 | }
105 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/element/Symbol.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package element
3 | import dataclass.data
4 |
5 | sealed abstract class Symbol(label0: String, idx0: Int) extends Product with Serializable {
6 | def idx: Int = idx0 + (if (open) 100 else 0)
7 | def label: String = label0 + (if (open) "-open" else "")
8 | def open: Boolean
9 | }
10 |
11 | object Symbol {
12 | sealed abstract class DotSymbol(label0: String, idx0: Int) extends Symbol(label0, idx0) {
13 | override def idx: Int = super.idx + (if (dot) 200 else 0)
14 | override def label: String = super.label + (if (dot) "-dot" else "")
15 | def dot: Boolean
16 | }
17 |
18 | @data class Circle(open: Boolean = false, dot: Boolean = false) extends DotSymbol("circle", 0)
19 | @data class Square(open: Boolean = false, dot: Boolean = false) extends DotSymbol("square", 1)
20 | @data class Diamond(open: Boolean = false, dot: Boolean = false) extends DotSymbol("diamond", 2)
21 | @data class Cross(open: Boolean = false, dot: Boolean = false) extends DotSymbol("cross", 3)
22 |
23 | /*
24 | "4" | "x" | "104" | "x-open" | "204" | "x-dot" | "304" | "x-open-dot"
25 | "5" | "triangle-up" | "105" | "triangle-up-open" | "205" | "triangle-up-dot" | "305" | "triangle-up-open-dot"
26 | "6" | "triangle-down" | "106" | "triangle-down-open" | "206" | "triangle-down-dot" | "306" | "triangle-down-open-dot"
27 | "7" | "triangle-left" | "107" | "triangle-left-open" | "207" | "triangle-left-dot" | "307" | "triangle-left-open-dot"
28 | "8" | "triangle-right" | "108" | "triangle-right-open" | "208" | "triangle-right-dot" | "308" | "triangle-right-open-dot"
29 | "9" | "triangle-ne" | "109" | "triangle-ne-open" | "209" | "triangle-ne-dot" | "309" | "triangle-ne-open-dot"
30 | "10" | "triangle-se" | "110" | "triangle-se-open" | "210" | "triangle-se-dot" | "310" | "triangle-se-open-dot"
31 | "11" | "triangle-sw" | "111" | "triangle-sw-open" | "211" | "triangle-sw-dot" | "311" | "triangle-sw-open-dot"
32 | "12" | "triangle-nw" | "112" | "triangle-nw-open" | "212" | "triangle-nw-dot" | "312" | "triangle-nw-open-dot"
33 | "13" | "pentagon" | "113" | "pentagon-open" | "213" | "pentagon-dot" | "313" | "pentagon-open-dot"
34 | "14" | "hexagon" | "114" | "hexagon-open" | "214" | "hexagon-dot" | "314" | "hexagon-open-dot"
35 | "15" | "hexagon2" | "115" | "hexagon2-open" | "215" | "hexagon2-dot" | "315" | "hexagon2-open-dot"
36 | "16" | "octagon" | "116" | "octagon-open" | "216" | "octagon-dot" | "316" | "octagon-open-dot"
37 | "17" | "star" | "117" | "star-open" | "217" | "star-dot" | "317" | "star-open-dot"
38 | "18" | "hexagram" | "118" | "hexagram-open" | "218" | "hexagram-dot" | "318" | "hexagram-open-dot"
39 | "19" | "star-triangle-up" | "119" | "star-triangle-up-open" | "219" | "star-triangle-up-dot" | "319" | "star-triangle-up-open-dot"
40 | "20" | "star-triangle-down" | "120" | "star-triangle-down-open" | "220" | "star-triangle-down-dot" | "320" | "star-triangle-down-open-dot"
41 | "21" | "star-square" | "121" | "star-square-open" | "221" | "star-square-dot" | "321" | "star-square-open-dot"
42 | "22" | "star-diamond" | "122" | "star-diamond-open" | "222" | "star-diamond-dot" | "322" | "star-diamond-open-dot"
43 | "23" | "diamond-tall" | "123" | "diamond-tall-open" | "223" | "diamond-tall-dot" | "323" | "diamond-tall-open-dot"
44 | "24" | "diamond-wide" | "124" | "diamond-wide-open" | "224" | "diamond-wide-dot" | "324" | "diamond-wide-open-dot"
45 | "25" | "hourglass" | "125" | "hourglass-open"
46 | "26" | "bowtie" | "126" | "bowtie-open"
47 | "27" | "circle-cross" | "127" | "circle-cross-open"
48 | "28" | "circle-x" | "128" | "circle-x-open"
49 | "29" | "square-cross" | "129" | "square-cross-open"
50 | "30" | "square-x" | "130" | "square-x-open"
51 | "31" | "diamond-cross" | "131" | "diamond-cross-open"
52 | "32" | "diamond-x" | "132" | "diamond-x-open"
53 | "33" | "cross-thin" | "133" | "cross-thin-open"
54 | "34" | "x-thin" | "134" | "x-thin-open"
55 | "35" | "asterisk" | "135" | "asterisk-open"
56 | "36" | "hash" | "136" | "hash-open" | "236" | "hash-dot" | "336" | "hash-open-dot"
57 | "37" | "y-up" | "137" | "y-up-open"
58 | "38" | "y-down" | "138" | "y-down-open"
59 | "39" | "y-left" | "139" | "y-left-open"
60 | "40" | "y-right" | "140" | "y-right-open"
61 | "41" | "line-ew" | "141" | "line-ew-open"
62 | "42" | "line-ns" | "142" | "line-ns-open"
63 | "43" | "line-ne" | "143" | "line-ne-open"
64 | "44" | "line-nw" | "144" | "line-nw-open
65 | */
66 | }
67 |
--------------------------------------------------------------------------------
/tests/src/test/scala/plotly/doc/SchemaTests.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package doc
3 |
4 | import java.io.File
5 | import java.nio.file.Files
6 |
7 | import argonaut.ArgonautShapeless._
8 | import argonaut.{DecodeJson, DecodeResult, Json, Parse}
9 | import org.scalatest.flatspec.AnyFlatSpec
10 | import org.scalatest.matchers.should.Matchers
11 | import plotly.element._
12 | import shapeless.Witness
13 |
14 | object SchemaTests {
15 |
16 | sealed abstract class Attribute extends Product with Serializable
17 |
18 | object Attribute {
19 | case class ConstantString(value: String) extends Attribute
20 |
21 | case class Flag(
22 | valType: Witness.`"flaglist"`.T,
23 | flags: List[String],
24 | description: String,
25 | role: String
26 | ) extends Attribute
27 |
28 | case class Enumerated(
29 | valType: Witness.`"enumerated"`.T,
30 | description: String,
31 | role: String,
32 | values: List[String]
33 | ) extends Attribute
34 |
35 | case class Other(json: Json) extends Attribute
36 |
37 | implicit val decode: DecodeJson[Attribute] =
38 | DecodeJson { c =>
39 | val constantString = c.as[String].map[Attribute](ConstantString(_))
40 | def flag = c.as[Flag].map[Attribute](x => x)
41 | def enumerated = c.as[Enumerated].map[Attribute](x => x)
42 | def other = DecodeResult.ok[Attribute](Other(c.focus))
43 |
44 | constantString.toOption
45 | .map(DecodeResult.ok)
46 | .orElse(flag.toOption.map(DecodeResult.ok))
47 | .orElse(enumerated.toOption.map(DecodeResult.ok))
48 | .getOrElse(other)
49 | }
50 | }
51 |
52 | case class Trace(
53 | description: Option[String],
54 | attributes: Map[String, Attribute]
55 | ) {
56 | def flagAttribute(name: String): Attribute.Flag =
57 | attributes.get(name) match {
58 | case None =>
59 | throw new NoSuchElementException(s"attribute $name")
60 | case Some(f: Attribute.Flag) =>
61 | f
62 | case Some(nonFlag) =>
63 | throw new Exception(s"Attribute $name not a flag ($nonFlag)")
64 | }
65 |
66 | def enumeratedAttribute(name: String): Attribute.Enumerated =
67 | attributes.get(name) match {
68 | case None =>
69 | throw new NoSuchElementException(s"attribute $name")
70 | case Some(f: Attribute.Enumerated) =>
71 | f
72 | case Some(nonFlag) =>
73 | throw new Exception(s"Attribute $name not a flag ($nonFlag)")
74 | }
75 | }
76 |
77 | case class Schema(
78 | traces: Map[String, Trace]
79 | )
80 |
81 | val schemaFile = new File("plotly-documentation/_data/plotschema.json")
82 |
83 | lazy val schema: Schema = {
84 |
85 | val schemaContent = new String(Files.readAllBytes(schemaFile.toPath), "UTF-8")
86 |
87 | val schemaJson = Parse.parse(schemaContent) match {
88 | case Left(error) =>
89 | throw new Exception(s"Cannot parse schema: $error")
90 | case Right(json) => json
91 | }
92 |
93 | schemaJson.as[Schema].toEither match {
94 | case Left(error) =>
95 | println(schemaJson.obj.map(_.fields).getOrElse(Nil).mkString("\n"))
96 | throw new Exception(s"Cannot decode schema: $error")
97 | case Right(schema) =>
98 | schema
99 | }
100 | }
101 |
102 | }
103 |
104 | class SchemaTests extends AnyFlatSpec with Matchers {
105 |
106 | private def compareValues(fromSchema: Set[String], fromLib: Set[String]): Unit = {
107 | val onlySchema = (fromSchema -- fromLib).toVector.sorted
108 | val onlyLib = (fromLib -- fromSchema).toVector.sorted
109 |
110 | assert(onlySchema.isEmpty, s"Only in schema: ${onlySchema.mkString(", ")}")
111 | assert(onlyLib.isEmpty, s"Only in lib: ${onlyLib.mkString(", ")}")
112 | }
113 |
114 | "Scatter mode flags" should "be exhaustive" in {
115 | val fromSchema = SchemaTests.schema
116 | .traces("scatter")
117 | .flagAttribute("mode")
118 | .flags
119 | .toSet
120 |
121 | val fromLib = ScatterMode.flags
122 | .map(_.label)
123 | .toSet
124 |
125 | compareValues(fromSchema, fromLib)
126 | }
127 |
128 | "Text position" should "be exhausitve" in {
129 | val fromSchema = SchemaTests.schema
130 | .traces("scatter")
131 | .enumeratedAttribute("textposition")
132 | .values
133 | .toSet
134 |
135 | val fromLib = Enumerate[TextPosition]
136 | .apply()
137 | .map(_.label)
138 | .toSet
139 |
140 | compareValues(fromSchema, fromLib)
141 | }
142 |
143 | }
144 |
--------------------------------------------------------------------------------
/demo/src/main/scala/plotly/demo/Demo.scala:
--------------------------------------------------------------------------------
1 | package plotly.demo
2 |
3 | import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
4 |
5 | import plotly.Plotly
6 |
7 | import org.scalajs.dom
8 |
9 | import scalatags.JsDom.all.{area => _, _}
10 |
11 | @JSExportTopLevel("Demo") object Demo {
12 |
13 | val demos = Seq(
14 | "Line Charts" -> Seq(
15 | linecharts.BasicLinePlot,
16 | linecharts.LineAndScatterPlot
17 | ),
18 | "Scatter Plots" -> Seq(
19 | lineandscatter.CategoricalDotPlot
20 | ),
21 | "Bar Charts" -> Seq(
22 | bar.BasicBarChart,
23 | bar.GroupedBarChart,
24 | bar.BarChartWithDirectLabels,
25 | bar.CustomizingIndividualBarColors,
26 | bar.WaterfallBarChart
27 | ),
28 | "Horizontal Bar Charts" -> Seq(
29 | horizontalbarcharts.BasicHorizontalBarChart,
30 | horizontalbarcharts.ColoredBarChart
31 | ),
32 | "Time Series" -> Seq(
33 | timeseries.TimeSeries
34 | ),
35 | "Bubble Charts" -> Seq(
36 | bubblecharts.HoverOnTextBubbleChart
37 | ),
38 | "Filled Area Plots" -> Seq(
39 | area.BasicOverlaidAreaChart
40 | ),
41 | "Heatmaps" -> Seq(
42 | heatmaps.BasicHeatmap,
43 | heatmaps.CategoricalAxisHeatmap,
44 | heatmaps.CustomColorScaleHeatmap,
45 | heatmaps.AnnotatedHeatmap
46 | ),
47 | "Histogram" -> Seq(
48 | histogram.BasicHistogram,
49 | histogram.StyledBasicHistogram
50 | )
51 | )
52 |
53 | def unindent(source: String): String = {
54 |
55 | val lines = source.linesIterator.toVector
56 | val nonEmptyLines: Vector[String] = lines.filter(_.exists(!_.isSpaceChar))
57 |
58 | if (nonEmptyLines.isEmpty)
59 | source
60 | else {
61 |
62 | val dropCount = LazyList
63 | .from(0)
64 | .takeWhile(idx => nonEmptyLines.forall(str => str(idx) == nonEmptyLines.head(idx)))
65 | .lastOption
66 | .fold(0)(_ + 1)
67 |
68 | if (dropCount == 0)
69 | source
70 | else
71 | lines
72 | .map(_.drop(dropCount))
73 | .mkString("\n")
74 | }
75 | }
76 |
77 | @JSExport def main(): Unit = {
78 |
79 | val mainDiv = dom.document.getElementById("demo")
80 |
81 | def chartTypeId(chartType: String) =
82 | chartType.toLowerCase.replace(' ', '-')
83 |
84 | val toc = div(
85 | style := "margin-bottom: 5em;",
86 | a(href := "https://github.com/alexarchambault/plotly-scala", h2("Sources")),
87 | h2("Examples"),
88 | div(
89 | style := "margin-left: 3em;",
90 | demos.map { case (chartType, chartDemos) =>
91 | val chartTypeId0 = chartTypeId(chartType)
92 | a(href := "#" + chartTypeId0, h3(chartType))
93 | }
94 | )
95 | )
96 |
97 | mainDiv.appendChild(toc.render)
98 |
99 | for ((chartType, chartDemos) <- demos) {
100 | Console.println(s"Rendering demos $chartType")
101 |
102 | val chartTypeId0 = chartTypeId(chartType)
103 |
104 | val chartTypeElem = h2(id := chartTypeId0, a(href := "#" + chartTypeId0, chartType))
105 |
106 | mainDiv.appendChild(chartTypeElem.render)
107 |
108 | for (demo <- chartDemos) {
109 | Console.println(s" Rendering demo ${demo.id}")
110 |
111 | val divId = s"demo-${demo.id}"
112 |
113 | val elem =
114 | div(
115 | id := demo.id,
116 | `class` := "panel panel-default",
117 | div(`class` := "panel-heading", a(href := "#" + demo.id, h4(demo.id))),
118 | div(
119 | `class` := "panel-body",
120 | div(
121 | `class` := "example-code",
122 | pre(
123 | code(
124 | `class` := "language-scala",
125 | s"""import plotly._
126 | |import plotly.element._${if (demo.layout == null) "" else "\nimport plotly.layout._"}
127 | """.stripMargin +
128 | unindent(demo.source) +
129 | s"""
130 | |
131 | |Plotly.plot("div-id", data${if (demo.layout == null) "" else ", layout"})""".stripMargin
132 | )
133 | )
134 | ),
135 | div(id := divId, `class` := "plot-box"),
136 | p(a(href := demo.plotlyDocUrl, "Original plotly example"))
137 | )
138 | )
139 |
140 | mainDiv.appendChild(elem.render)
141 |
142 | if (demo.layout == null)
143 | Plotly.plot(divId, demo.data)
144 | else
145 | Plotly.plot(divId, demo.data, demo.layout)
146 | }
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/layout/Axis.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package layout
3 |
4 | import java.lang.{Boolean => JBoolean, Double => JDouble, Integer => JInt}
5 |
6 | import dataclass.data
7 | import plotly.element._
8 |
9 | @data(optionSetters = true) class Axis(
10 | title: Option[String] = None,
11 | titlefont: Option[Font] = None,
12 | showgrid: Option[Boolean] = None,
13 | gridwidth: Option[Int] = None,
14 | gridcolor: Option[Color] = None,
15 | showline: Option[Boolean] = None,
16 | showticklabels: Option[Boolean] = None,
17 | linecolor: Option[Color] = None,
18 | linewidth: Option[Int] = None,
19 | autotick: Option[Boolean] = None,
20 | tickcolor: Option[Color] = None,
21 | tickwidth: Option[Int] = None,
22 | tickangle: Option[Double] = None,
23 | dtick: Option[Double] = None,
24 | ticklen: Option[Int] = None,
25 | tickfont: Option[Font] = None,
26 | tickprefix: Option[String] = None,
27 | ticksuffix: Option[String] = None,
28 | zeroline: Option[Boolean] = None,
29 | zerolinewidth: Option[Double] = None,
30 | zerolinecolor: Option[Color] = None,
31 | range: Option[Range] = None,
32 | autorange: Option[Boolean] = None,
33 | ticks: Option[Ticks] = None,
34 | domain: Option[Range] = None,
35 | side: Option[Side] = None,
36 | anchor: Option[AxisAnchor] = None,
37 | `type`: Option[AxisType] = None,
38 | overlaying: Option[AxisAnchor] = None,
39 | position: Option[Double] = None,
40 | tickmode: Option[TickMode] = None,
41 | tickvals: Option[Sequence] = None,
42 | ticktext: Option[Sequence] = None,
43 | nticks: Option[Int] = None,
44 | automargin: Option[Boolean] = None,
45 | @since("0.8.0")
46 | rangeslider: Option[RangeSlider] = None,
47 | @since("0.8.2")
48 | width: Option[Int] = None,
49 | height: Option[Int] = None,
50 | autosize: Option[Boolean] = None,
51 | @since("0.8.5")
52 | tickformat: Option[String] = None,
53 | fixedrange: Option[Boolean] = None
54 | )
55 |
56 | object Axis {
57 | @deprecated("Use Axis() and chain-call .with* methods on it instead", "0.8.0")
58 | def apply(
59 | title: String = null,
60 | titlefont: Font = null,
61 | showgrid: JBoolean = null,
62 | gridwidth: JInt = null,
63 | gridcolor: Color = null,
64 | showline: JBoolean = null,
65 | showticklabels: JBoolean = null,
66 | linecolor: Color = null,
67 | linewidth: JInt = null,
68 | autotick: JBoolean = null,
69 | tickcolor: Color = null,
70 | tickwidth: JInt = null,
71 | tickangle: JDouble = null,
72 | dtick: JDouble = null,
73 | ticklen: JInt = null,
74 | tickfont: Font = null,
75 | tickprefix: String = null,
76 | ticksuffix: String = null,
77 | zeroline: JBoolean = null,
78 | zerolinewidth: JDouble = null,
79 | zerolinecolor: Color = null,
80 | range: (Double, Double) = null,
81 | autorange: JBoolean = null,
82 | ticks: Ticks = null,
83 | domain: (Double, Double) = null,
84 | side: Side = null,
85 | anchor: AxisAnchor = null,
86 | `type`: AxisType = null,
87 | overlaying: AxisAnchor = null,
88 | position: JDouble = null,
89 | tickmode: TickMode = null,
90 | tickvals: Sequence = null,
91 | ticktext: Sequence = null,
92 | nticks: JInt = null,
93 | automargin: JBoolean = null
94 | ): Axis =
95 | Axis(
96 | Option(title),
97 | Option(titlefont),
98 | Option(showgrid).map(x => x: Boolean),
99 | Option(gridwidth).map(x => x: Int),
100 | Option(gridcolor),
101 | Option(showline).map(x => x: Boolean),
102 | Option(showticklabels).map(x => x: Boolean),
103 | Option(linecolor),
104 | Option(linewidth).map(x => x: Int),
105 | Option(autotick).map(x => x: Boolean),
106 | Option(tickcolor),
107 | Option(tickwidth).map(x => x: Int),
108 | Option(tickangle).map(x => x: Double),
109 | Option(dtick).map(x => x: Double),
110 | Option(ticklen).map(x => x: Int),
111 | Option(tickfont),
112 | Option(tickprefix),
113 | Option(ticksuffix),
114 | Option(zeroline).map(x => x: Boolean),
115 | Option(zerolinewidth).map(x => x: Double),
116 | Option(zerolinecolor),
117 | Option(range).map(x => x: Range),
118 | Option(autorange).map(x => x: Boolean),
119 | Option(ticks),
120 | Option(domain).map(x => x: Range),
121 | Option(side),
122 | Option(anchor),
123 | Option(`type`),
124 | Option(overlaying),
125 | Option(position).map(x => x: Double),
126 | Option(tickmode),
127 | Option(tickvals),
128 | Option(ticktext),
129 | Option(nticks).map(x => x: Int),
130 | Option(automargin).map(x => x: Boolean)
131 | )
132 | }
133 |
--------------------------------------------------------------------------------
/project/Settings.scala:
--------------------------------------------------------------------------------
1 | import com.jsuereth.sbtpgp._
2 | import sbt._
3 | import sbt.Keys._
4 |
5 | object Settings {
6 |
7 | lazy val customSourceGenerators = TaskKey[Seq[sbt.File]]("custom-source-generators")
8 |
9 | lazy val generateCustomSources = Seq(
10 | customSourceGenerators := {
11 | val dir = target.value
12 | val f = dir / "Properties.scala"
13 | dir.mkdirs()
14 |
15 | def gitCommit =
16 | sys.process.Process(Seq("git", "rev-parse", "HEAD")).!!.trim
17 |
18 | val w = new java.io.FileOutputStream(f)
19 | w.write(
20 | s"""package plotly.demo
21 | |
22 | |object Properties {
23 | |
24 | | val version = "${version.value}"
25 | | val commitHash = "$gitCommit"
26 | |
27 | |}
28 | """.stripMargin
29 | .getBytes("UTF-8")
30 | )
31 | w.close()
32 |
33 | println(s"Wrote $f")
34 |
35 | val files = new collection.mutable.ArrayBuffer[File]
36 | files += f
37 |
38 | val tq = "\"\"\""
39 |
40 | def process(destDir: File, pathComponents: Seq[String], file: File): Unit = {
41 | if (file.isDirectory) {
42 | val destDir0 = destDir / file.getName
43 | val pathComponents0 = pathComponents :+ file.getName
44 | for (f <- file.listFiles())
45 | process(destDir0, pathComponents0, f)
46 | } else {
47 | val lines = new String(java.nio.file.Files.readAllBytes(file.toPath), "UTF-8").linesIterator.toVector
48 |
49 | val demoLines = lines
50 | .dropWhile(!_.contains("demo source start"))
51 | .drop(1)
52 | .takeWhile(!_.contains("demo source end"))
53 | .map(l => l.replace(tq, tq + " + \"\\\"\\\"\\\"\" + " + tq))
54 |
55 | if (demoLines.nonEmpty) {
56 | val dest = destDir / (file.getName.stripSuffix(".scala") + "Source.scala")
57 | destDir.mkdirs()
58 | val w = new java.io.FileOutputStream(dest)
59 | w.write(
60 | s"""package plotly${pathComponents.map("." + _).mkString}
61 | |
62 | |object ${file.getName.stripSuffix(".scala")}Source {
63 | |
64 | | val source = $tq${demoLines.mkString("\n")}$tq
65 | |
66 | |}
67 | """.stripMargin
68 | .getBytes("UTF-8")
69 | )
70 | w.close()
71 |
72 | files += dest
73 | }
74 | }
75 | }
76 |
77 | process(dir / "plotly", Vector(), (Compile / scalaSource).value / "plotly" / "demo")
78 |
79 | files
80 | },
81 | (Compile / sourceGenerators) += customSourceGenerators.taskValue
82 | )
83 |
84 | private val scala212 = "2.12.19"
85 | private val scala213 = "2.13.14"
86 |
87 | private lazy val isAtLeastScala213 = Def.setting {
88 | import Ordering.Implicits._
89 | CrossVersion.partialVersion(scalaVersion.value).exists(_ >= (2, 13))
90 | }
91 |
92 | lazy val shared = Def.settings(
93 | crossScalaVersions := Seq(scala213, scala212),
94 | scalaVersion := scala213,
95 | resolvers += "jitpack" at "https://jitpack.io",
96 | libraryDependencies ++= {
97 | if (isAtLeastScala213.value) Nil
98 | else Seq(compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full))
99 | },
100 | scalacOptions ++= Seq("-deprecation", "-feature"),
101 | scalacOptions ++= {
102 | if (isAtLeastScala213.value) Seq("-Ymacro-annotations")
103 | else Nil
104 | }
105 | )
106 |
107 | lazy val plotlyPrefix = {
108 | name := "plotly-" + name.value
109 | }
110 |
111 | val gitLock = new Object
112 |
113 | def runCommand(cmd: Seq[String], dir: File): Unit = {
114 | val b = new ProcessBuilder(cmd: _*)
115 | b.directory(dir)
116 | b.inheritIO()
117 | val p = b.start()
118 | val retCode = p.waitFor()
119 | if (retCode != 0)
120 | sys.error(s"Command ${cmd.mkString(" ")} failed (return code $retCode)")
121 | }
122 |
123 | lazy val fetchTestData = {
124 | (Test / unmanagedResources) ++= {
125 | val log = streams.value.log
126 | val baseDir = (LocalRootProject / baseDirectory).value
127 | val testsPostsDir = baseDir / "plotly-documentation" / "_posts"
128 | if (!testsPostsDir.exists())
129 | gitLock.synchronized {
130 | if (!testsPostsDir.exists()) {
131 | val cmd = Seq("git", "submodule", "update", "--init", "--recursive", "--", "plotly-documentation")
132 | log.info("Fetching submodule plotly-documentation (this may take some time)")
133 | runCommand(cmd, baseDir)
134 | log.info("Successfully fetched submodule plotly-documentation")
135 | }
136 | }
137 | Nil
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/render/js/src/main/scala/plotly/Plotly.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | import argonaut.Argonaut._
4 | import argonaut.{EncodeJson, PrettyParams}
5 | import plotly.Codecs._
6 | import plotly.element.Color
7 | import plotly.internals.BetterPrinter
8 | import plotly.layout._
9 |
10 | import java.lang.{Boolean => JBoolean, Double => JDouble, Integer => JInt}
11 | import scala.scalajs.js
12 | import scala.scalajs.js.Dynamic.{global => g}
13 | import scala.scalajs.js.JSON
14 |
15 | object Plotly {
16 |
17 | private val printer = BetterPrinter(PrettyParams.nospace.copy(dropNullKeys = true))
18 |
19 | // Remove empty objects
20 | private def stripNulls[J: EncodeJson](value: J): js.Any = JSON.parse(printer.render(value.asJson))
21 |
22 | trait PlotlyDyn {
23 | def plotFn: js.Dynamic
24 |
25 | def apply(div: String, data: Seq[Trace], layout: Layout, config: Config): Unit =
26 | plotFn(
27 | div,
28 | stripNulls(data),
29 | stripNulls(layout),
30 | stripNulls(config)
31 | )
32 |
33 | def apply(div: String, data: Seq[Trace], layout: Layout): Unit =
34 | plotFn(
35 | div,
36 | stripNulls(data),
37 | stripNulls(layout)
38 | )
39 |
40 | def apply(div: String, data: Seq[Trace]): Unit =
41 | plotFn(div, stripNulls(data))
42 |
43 | def apply(div: String, data: Trace, layout: Layout): Unit =
44 | plotFn(div, stripNulls(data), stripNulls(layout))
45 |
46 | def apply(div: String, data: Trace): Unit =
47 | plotFn(div, stripNulls(data))
48 | }
49 |
50 | object newPlot extends PlotlyDyn {
51 | val plotFn: js.Dynamic = g.Plotly.newPlot
52 | }
53 |
54 | object plot extends PlotlyDyn {
55 | val plotFn: js.Dynamic = g.Plotly.newPlot
56 | }
57 |
58 | object react extends PlotlyDyn {
59 | val plotFn: js.Dynamic = g.Plotly.react
60 | }
61 |
62 | def relayout(div: String, layout: Layout): Unit =
63 | g.Plotly.relayout(div, stripNulls(layout))
64 |
65 | def purge(div: String): Unit =
66 | g.Plotly.purge(div)
67 |
68 | def validate(data: Seq[Trace], layout: Layout, config: Config): Unit =
69 | g.Plotly.validate(stripNulls(data), stripNulls(layout))
70 |
71 | implicit class TraceOps(val trace: Trace) extends AnyVal {
72 | def plot(div: String, layout: Layout): Unit =
73 | Plotly.plot(div, trace, layout)
74 |
75 | @deprecated("Create a Layout and call plot(div, layout) instead", "0.8.0")
76 | def plot(
77 | div: String,
78 | title: String = null,
79 | legend: Legend = null,
80 | width: JInt = null,
81 | height: JInt = null,
82 | showlegend: JBoolean = null,
83 | xaxis: Axis = null,
84 | yaxis: Axis = null,
85 | xaxis1: Axis = null,
86 | xaxis2: Axis = null,
87 | xaxis3: Axis = null,
88 | xaxis4: Axis = null,
89 | yaxis1: Axis = null,
90 | yaxis2: Axis = null,
91 | yaxis3: Axis = null,
92 | yaxis4: Axis = null,
93 | barmode: BarMode = null,
94 | autosize: JBoolean = null,
95 | margin: Margin = null,
96 | annotations: Seq[Annotation] = null,
97 | plot_bgcolor: Color = null,
98 | paper_bgcolor: Color = null,
99 | font: Font = null,
100 | bargap: JDouble = null,
101 | bargroupgap: JDouble = null,
102 | hovermode: HoverMode = null,
103 | boxmode: BoxMode = null
104 | ): Unit =
105 | plot(
106 | div,
107 | Layout(
108 | title,
109 | legend,
110 | width,
111 | height,
112 | showlegend,
113 | xaxis,
114 | yaxis,
115 | xaxis1,
116 | xaxis2,
117 | xaxis3,
118 | xaxis4,
119 | yaxis1,
120 | yaxis2,
121 | yaxis3,
122 | yaxis4,
123 | barmode,
124 | autosize,
125 | margin,
126 | annotations,
127 | plot_bgcolor,
128 | paper_bgcolor,
129 | font,
130 | bargap,
131 | bargroupgap,
132 | hovermode,
133 | boxmode
134 | )
135 | )
136 | }
137 |
138 | implicit class TraceSeqOps(val traces: Seq[Trace]) extends AnyVal {
139 | def plot(div: String, layout: Layout): Unit =
140 | Plotly.plot(div, traces, layout)
141 |
142 | @deprecated("Create a Layout and call plot(div, layout) instead", "0.8.0")
143 | def plot(
144 | div: String,
145 | title: String = null,
146 | legend: Legend = null,
147 | width: JInt = null,
148 | height: JInt = null,
149 | showlegend: JBoolean = null,
150 | xaxis: Axis = null,
151 | yaxis: Axis = null,
152 | xaxis1: Axis = null,
153 | xaxis2: Axis = null,
154 | xaxis3: Axis = null,
155 | xaxis4: Axis = null,
156 | yaxis1: Axis = null,
157 | yaxis2: Axis = null,
158 | yaxis3: Axis = null,
159 | yaxis4: Axis = null,
160 | barmode: BarMode = null,
161 | autosize: JBoolean = null,
162 | margin: Margin = null,
163 | annotations: Seq[Annotation] = null,
164 | plot_bgcolor: Color = null,
165 | paper_bgcolor: Color = null,
166 | font: Font = null,
167 | bargap: JDouble = null,
168 | bargroupgap: JDouble = null,
169 | hovermode: HoverMode = null,
170 | boxmode: BoxMode = null
171 | ): Unit =
172 | plot(
173 | div,
174 | Layout(
175 | title,
176 | legend,
177 | width,
178 | height,
179 | showlegend,
180 | xaxis,
181 | yaxis,
182 | xaxis1,
183 | xaxis2,
184 | xaxis3,
185 | xaxis4,
186 | yaxis1,
187 | yaxis2,
188 | yaxis3,
189 | yaxis4,
190 | barmode,
191 | autosize,
192 | margin,
193 | annotations,
194 | plot_bgcolor,
195 | paper_bgcolor,
196 | font,
197 | bargap,
198 | bargroupgap,
199 | hovermode,
200 | boxmode
201 | )
202 | )
203 | }
204 |
205 | }
206 |
--------------------------------------------------------------------------------
/render/shared/src/main/scala/plotly/internals/BetterPrinter.scala:
--------------------------------------------------------------------------------
1 | package plotly.internals
2 |
3 | import argonaut._
4 | import argonaut.PrettyParams.vectorMemo
5 |
6 | final case class BetterPrinter(params: PrettyParams) {
7 |
8 | import params._
9 |
10 | private def addIndentation(s: String): Int => String = {
11 | val lastNewLineIndex = s.lastIndexOf("\n")
12 | if (lastNewLineIndex < 0) { _ =>
13 | s
14 | } else {
15 | val afterLastNewLineIndex = lastNewLineIndex + 1
16 | val start = s.substring(0, afterLastNewLineIndex)
17 | val end = s.substring(afterLastNewLineIndex)
18 | n => start + indent * n + end
19 | }
20 | }
21 |
22 | private val openBraceText = "{"
23 | private val closeBraceText = "}"
24 | private val openArrayText = "["
25 | private val closeArrayText = "]"
26 | private val commaText = ","
27 | private val colonText = ":"
28 | private val nullText = "null"
29 | private val trueText = "true"
30 | private val falseText = "false"
31 | private val stringEnclosureText = "\""
32 |
33 | private val _lbraceLeft = addIndentation(lbraceLeft)
34 | private val _lbraceRight = addIndentation(lbraceRight)
35 | private val _rbraceLeft = addIndentation(rbraceLeft)
36 | private val _rbraceRight = addIndentation(rbraceRight)
37 | private val _lbracketLeft = addIndentation(lbracketLeft)
38 | private val _lbracketRight = addIndentation(lbracketRight)
39 | private val _rbracketLeft = addIndentation(rbracketLeft)
40 | private val _rbracketRight = addIndentation(rbracketRight)
41 | private val _lrbracketsEmpty = addIndentation(lrbracketsEmpty)
42 | private val _arrayCommaLeft = addIndentation(arrayCommaLeft)
43 | private val _arrayCommaRight = addIndentation(arrayCommaRight)
44 | private val _objectCommaLeft = addIndentation(objectCommaLeft)
45 | private val _objectCommaRight = addIndentation(objectCommaRight)
46 | private val _colonLeft = addIndentation(colonLeft)
47 | private val _colonRight = addIndentation(colonRight)
48 |
49 | private val lbraceMemo = vectorMemo { depth: Int =>
50 | "%s%s%s".format(_lbraceLeft(depth), openBraceText, _lbraceRight(depth + 1))
51 | }
52 | private val rbraceMemo = vectorMemo { depth: Int =>
53 | "%s%s%s".format(_rbraceLeft(depth), closeBraceText, _rbraceRight(depth + 1))
54 | }
55 |
56 | private val lbracketMemo = vectorMemo { depth: Int =>
57 | "%s%s%s".format(_lbracketLeft(depth), openArrayText, _lbracketRight(depth + 1))
58 | }
59 | private val rbracketMemo = vectorMemo { depth: Int =>
60 | "%s%s%s".format(_rbracketLeft(depth), closeArrayText, _rbracketRight(depth))
61 | }
62 | private val lrbracketsEmptyMemo = vectorMemo { depth: Int =>
63 | "%s%s%s".format(openArrayText, _lrbracketsEmpty(depth), closeArrayText)
64 | }
65 | private val arrayCommaMemo = vectorMemo { depth: Int =>
66 | "%s%s%s".format(_arrayCommaLeft(depth + 1), commaText, _arrayCommaRight(depth + 1))
67 | }
68 | private val objectCommaMemo = vectorMemo { depth: Int =>
69 | "%s%s%s".format(_objectCommaLeft(depth + 1), commaText, _objectCommaRight(depth + 1))
70 | }
71 | private val colonMemo = vectorMemo { depth: Int =>
72 | "%s%s%s".format(_colonLeft(depth + 1), colonText, _colonRight(depth + 1))
73 | }
74 |
75 | /** Returns a string representation of a pretty-printed JSON value.
76 | */
77 | def render(j: Json): String = {
78 |
79 | import Json._
80 | import StringEscaping._
81 |
82 | def appendJsonString(builder: StringBuilder, jsonString: String): StringBuilder = {
83 | for (c <- jsonString) {
84 | if (isNormalChar(c))
85 | builder += c
86 | else
87 | builder.append(escape(c))
88 | }
89 |
90 | builder
91 | }
92 |
93 | def encloseJsonString(builder: StringBuilder, jsonString: JsonString): StringBuilder = {
94 | appendJsonString(builder.append(stringEnclosureText), jsonString).append(stringEnclosureText)
95 | }
96 |
97 | def trav(builder: StringBuilder, depth: Int, k: Json): StringBuilder = {
98 |
99 | def lbrace(builder: StringBuilder): StringBuilder = {
100 | builder.append(lbraceMemo(depth))
101 | }
102 | def rbrace(builder: StringBuilder): StringBuilder = {
103 | builder.append(rbraceMemo(depth))
104 | }
105 | def lbracket(builder: StringBuilder): StringBuilder = {
106 | builder.append(lbracketMemo(depth))
107 | }
108 | def rbracket(builder: StringBuilder): StringBuilder = {
109 | builder.append(rbracketMemo(depth))
110 | }
111 | def lrbracketsEmpty(builder: StringBuilder): StringBuilder = {
112 | builder.append(lrbracketsEmptyMemo(depth))
113 | }
114 | def arrayComma(builder: StringBuilder): StringBuilder = {
115 | builder.append(arrayCommaMemo(depth))
116 | }
117 | def objectComma(builder: StringBuilder): StringBuilder = {
118 | builder.append(objectCommaMemo(depth))
119 | }
120 | def colon(builder: StringBuilder): StringBuilder = {
121 | builder.append(colonMemo(depth))
122 | }
123 |
124 | k.fold[StringBuilder](
125 | builder.append(nullText),
126 | bool => builder.append(if (bool) trueText else falseText),
127 | n =>
128 | n match {
129 | case JsonLong(x) => builder append x.toString
130 | case JsonDecimal(x) => builder append x
131 | case JsonBigDecimal(x) => builder append x.toString
132 | },
133 | s => encloseJsonString(builder, s),
134 | e =>
135 | if (e.isEmpty) {
136 | lrbracketsEmpty(builder)
137 | } else {
138 | rbracket(
139 | e.foldLeft((true, lbracket(builder))) { case ((firstElement, builder), subElement) =>
140 | val withComma = if (firstElement) builder else arrayComma(builder)
141 | val updatedBuilder = trav(withComma, depth + 1, subElement)
142 | (false, updatedBuilder)
143 | }._2
144 | )
145 | },
146 | o => {
147 | rbrace(
148 | (if (preserveOrder) o.toList else o.toMap)
149 | .foldLeft((true, lbrace(builder))) { case ((firstElement, builder), (key, value)) =>
150 | val ignoreKey = dropNullKeys && value.isNull
151 | if (ignoreKey) {
152 | (firstElement, builder)
153 | } else {
154 | val withComma = if (firstElement) builder else objectComma(builder)
155 | (false, trav(colon(encloseJsonString(withComma, key)), depth + 1, value))
156 | }
157 | }
158 | ._2
159 | )
160 | }
161 | )
162 | }
163 |
164 | trav(new StringBuilder(), 0, j).toString()
165 | }
166 |
167 | }
168 |
--------------------------------------------------------------------------------
/almond/src/main/scala/plotly/Almond.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | import java.lang.{Boolean => JBoolean, Double => JDouble, Integer => JInt}
4 |
5 | import almond.interpreter.api.{DisplayData, OutputHandler}
6 |
7 | import scala.util.Random
8 | import plotly.element._
9 | import plotly.layout._
10 |
11 | object Almond {
12 |
13 | object Internal {
14 | @volatile var initialized = false
15 | }
16 |
17 | def init(offline: Boolean = false)(implicit publish: OutputHandler): Unit = {
18 |
19 | // offline mode like in plotly-python
20 |
21 | val requireInit =
22 | if (offline)
23 | s"""define('plotly', function(require, exports, module) {
24 | | ${Plotly.plotlyMinJs}
25 | |});
26 | """.stripMargin
27 | else
28 | s"""require.config({
29 | | paths: {
30 | | d3: 'https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min',
31 | | plotly: 'https://cdn.plot.ly/plotly-${Plotly.plotlyVersion}.min',
32 | | jquery: 'https://code.jquery.com/jquery-3.3.1.min'
33 | | },
34 | |
35 | | shim: {
36 | | plotly: {
37 | | deps: ['d3', 'jquery'],
38 | | exports: 'plotly'
39 | | }
40 | | }
41 | |});
42 | """.stripMargin
43 |
44 | val html = s"""
45 |
52 | """
53 |
54 | Internal.initialized = true
55 |
56 | publish.html(html)
57 | }
58 |
59 | def plotJs(
60 | data: Seq[Trace],
61 | layout: Layout,
62 | config: Config,
63 | div: String = ""
64 | )(implicit
65 | publish: OutputHandler
66 | ): String = {
67 |
68 | val (div0, divPart) =
69 | if (div.isEmpty) {
70 | val d = randomDiv()
71 | (d, s"""""")
72 | } else
73 | (div, "")
74 |
75 | val baseJs = Plotly.jsSnippet(div0, data, layout, config)
76 | val json = Plotly.jsonSnippet(data, layout, config)
77 |
78 | val js =
79 | s"""require(['plotly'], function(Plotly) {
80 | | $baseJs
81 | |});
82 | """.stripMargin
83 |
84 | val data0 = DisplayData(
85 | data = Map(
86 | "text/html" ->
87 | s"""$divPart
88 | |
89 | """.stripMargin,
90 | "application/vnd.plotly.v1+json" -> json
91 | )
92 | )
93 |
94 | publish.display(data0)
95 |
96 | div0
97 | }
98 |
99 | def randomDiv(): String =
100 | almond.display.UpdatableDisplay.generateDiv("plot-")
101 |
102 | def plot(
103 | data: Seq[Trace],
104 | layout: Layout = Layout(),
105 | config: Config = Config(),
106 | div: String = ""
107 | )(implicit
108 | publish: OutputHandler
109 | ): String = {
110 |
111 | if (!Internal.initialized)
112 | Internal.synchronized {
113 | if (!Internal.initialized) {
114 | init()
115 | Internal.initialized = true
116 | }
117 | }
118 |
119 | plotJs(data, layout, config)
120 | }
121 |
122 | implicit class DataOps(val data: Trace) extends AnyVal {
123 |
124 | @deprecated("Create a Layout and / or a Config, and call one of the other plot methods instead", "0.8.0")
125 | def plot(
126 | title: String = null,
127 | legend: Legend = null,
128 | width: JInt = null,
129 | height: JInt = null,
130 | showlegend: JBoolean = null,
131 | xaxis: Axis = null,
132 | yaxis: Axis = null,
133 | xaxis1: Axis = null,
134 | xaxis2: Axis = null,
135 | xaxis3: Axis = null,
136 | xaxis4: Axis = null,
137 | yaxis1: Axis = null,
138 | yaxis2: Axis = null,
139 | yaxis3: Axis = null,
140 | yaxis4: Axis = null,
141 | barmode: BarMode = null,
142 | autosize: JBoolean = null,
143 | margin: Margin = null,
144 | annotations: Seq[Annotation] = null,
145 | plot_bgcolor: Color = null,
146 | paper_bgcolor: Color = null,
147 | font: Font = null,
148 | bargap: JDouble = null,
149 | bargroupgap: JDouble = null,
150 | hovermode: HoverMode = null,
151 | boxmode: BoxMode = null,
152 | editable: JBoolean = null,
153 | responsive: JBoolean = null,
154 | showEditInChartStudio: JBoolean = null,
155 | plotlyServerURL: String = null,
156 | div: String = ""
157 | )(implicit
158 | publish: OutputHandler
159 | ): String =
160 | plot(
161 | Layout(
162 | title,
163 | legend,
164 | width,
165 | height,
166 | showlegend,
167 | xaxis,
168 | yaxis,
169 | xaxis1,
170 | xaxis2,
171 | xaxis3,
172 | xaxis4,
173 | yaxis1,
174 | yaxis2,
175 | yaxis3,
176 | yaxis4,
177 | barmode,
178 | autosize,
179 | margin,
180 | annotations,
181 | plot_bgcolor,
182 | paper_bgcolor,
183 | font,
184 | bargap,
185 | bargroupgap,
186 | hovermode,
187 | boxmode
188 | ),
189 | Config()
190 | .withEditable(Option(editable).map[Boolean](identity))
191 | .withResponsive(Option(responsive).map[Boolean](identity))
192 | .withShowEditInChartStudio(Option(showEditInChartStudio).map[Boolean](identity))
193 | .withPlotlyServerURL(Option(plotlyServerURL)),
194 | div
195 | )
196 |
197 | def plot(
198 | layout: Layout,
199 | config: Config,
200 | div: String
201 | )(implicit
202 | publish: OutputHandler
203 | ): String =
204 | Almond.plot(Seq(data), layout, config, div = div)
205 |
206 | def plot()(implicit
207 | publish: OutputHandler
208 | ): String =
209 | plot(Layout(), Config(), "")
210 |
211 | def plot(
212 | layout: Layout
213 | )(implicit
214 | publish: OutputHandler
215 | ): String =
216 | plot(layout, Config(), "")
217 |
218 | def plot(
219 | config: Config
220 | )(implicit
221 | publish: OutputHandler
222 | ): String =
223 | plot(Layout(), config, "")
224 |
225 | def plot(
226 | layout: Layout,
227 | config: Config
228 | )(implicit
229 | publish: OutputHandler
230 | ): String =
231 | plot(layout, config, "")
232 | }
233 |
234 | implicit class DataSeqOps(val data: Seq[Trace]) extends AnyVal {
235 | def plot(
236 | title: String = null,
237 | legend: Legend = null,
238 | width: JInt = null,
239 | height: JInt = null,
240 | showlegend: JBoolean = null,
241 | xaxis: Axis = null,
242 | yaxis: Axis = null,
243 | xaxis1: Axis = null,
244 | xaxis2: Axis = null,
245 | xaxis3: Axis = null,
246 | xaxis4: Axis = null,
247 | yaxis1: Axis = null,
248 | yaxis2: Axis = null,
249 | yaxis3: Axis = null,
250 | yaxis4: Axis = null,
251 | barmode: BarMode = null,
252 | autosize: JBoolean = null,
253 | margin: Margin = null,
254 | annotations: Seq[Annotation] = null,
255 | plot_bgcolor: Color = null,
256 | paper_bgcolor: Color = null,
257 | font: Font = null,
258 | bargap: JDouble = null,
259 | bargroupgap: JDouble = null,
260 | hovermode: HoverMode = null,
261 | boxmode: BoxMode = null,
262 | editable: JBoolean = null,
263 | responsive: JBoolean = null,
264 | showEditInChartStudio: JBoolean = null,
265 | plotlyServerURL: String = null,
266 | div: String = ""
267 | )(implicit
268 | publish: OutputHandler
269 | ): String =
270 | plot(
271 | Layout(
272 | title,
273 | legend,
274 | width,
275 | height,
276 | showlegend,
277 | xaxis,
278 | yaxis,
279 | xaxis1,
280 | xaxis2,
281 | xaxis3,
282 | xaxis4,
283 | yaxis1,
284 | yaxis2,
285 | yaxis3,
286 | yaxis4,
287 | barmode,
288 | autosize,
289 | margin,
290 | annotations,
291 | plot_bgcolor,
292 | paper_bgcolor,
293 | font,
294 | bargap,
295 | bargroupgap,
296 | hovermode,
297 | boxmode
298 | ),
299 | Config()
300 | .withEditable(Option(editable).map[Boolean](identity))
301 | .withResponsive(Option(responsive).map[Boolean](identity))
302 | .withShowEditInChartStudio(Option(showEditInChartStudio).map[Boolean](identity))
303 | .withPlotlyServerURL(plotlyServerURL),
304 | div
305 | )
306 |
307 | def plot(
308 | layout: Layout,
309 | config: Config,
310 | div: String
311 | )(implicit
312 | publish: OutputHandler
313 | ): String =
314 | Almond.plot(data, layout, config, div = div)
315 | }
316 |
317 | }
318 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # plotly-scala
2 |
3 | Scala bindings for [plotly.js](https://plot.ly/javascript/)
4 |
5 | [](https://travis-ci.org/alexarchambault/plotly-scala)
6 | [](https://gitter.im/alexarchambault/plotly-scala?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
7 | [](https://maven-badges.herokuapp.com/maven-central/org.plotly-scala/plotly-render_2.13)
8 | [](http://javadoc-badge.appspot.com/org.plotly-scala/plotly-render_2.13)
9 |
10 | [Demo](https://alexarchambault.github.io/plotly-scala/)
11 |
12 | *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.
13 |
14 | 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.
15 |
16 | 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.
17 |
18 | It is published for both scala 2.12 and 2.13.
19 |
20 | ## Table of content
21 |
22 | 1. [Quick start](#quick-start)
23 | 2. [Rationale](#rationale)
24 | 3. [Internals](#internals)
25 | 4. [Supported features](#supported-features)
26 |
27 | ## Quick start
28 |
29 | ### From almond
30 |
31 | Add the `org.plotly-scala::plotly-almond:0.8.1` dependency to the notebook. (Latest version: [](https://maven-badges.herokuapp.com/maven-central/org.plotly-scala/plotly-render_2.13))
32 | Then initialize plotly-scala, and use it, like
33 | ```scala
34 | import $ivy.`org.plotly-scala::plotly-almond:0.8.1`
35 |
36 | import plotly._
37 | import plotly.element._
38 | import plotly.layout._
39 | import plotly.Almond._
40 |
41 | val (x, y) = Seq(
42 | "Banana" -> 10,
43 | "Apple" -> 8,
44 | "Grapefruit" -> 5
45 | ).unzip
46 |
47 | Bar(x, y).plot()
48 | ```
49 |
50 | #### JupyterLab
51 | 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:
52 | ```bash
53 | jupyter labextension install jupyterlab-plotly
54 | ```
55 |
56 | ### From scala-js
57 |
58 | Add the corresponding dependency to your project, like
59 | ```scala
60 | libraryDependencies += "org.plotly-scala" %%% "plotly-render" % "0.8.1"
61 | ```
62 | (Latest version: [](https://maven-badges.herokuapp.com/maven-central/org.plotly-scala/plotly-render_2.13))
63 |
64 | From your code, add some imports for plotly,
65 | ```scala
66 | import plotly._, element._, layout._, Plotly._
67 | ```
68 |
69 | Then define plots like
70 | ```scala
71 | val x = (0 to 100).map(_ * 0.1)
72 | val y1 = x.map(d => 2.0 * d + util.Random.nextGaussian())
73 | val y2 = x.map(math.exp)
74 |
75 | val plot = Seq(
76 | Scatter(x, y1).withName("Approx twice"),
77 | Scatter(x, y2).withName("Exp")
78 | )
79 | ```
80 | and plot them with
81 |
82 | ```scala
83 | val lay = Layout().withTitle("Curves")
84 | plot.plot("plot", lay) // attaches to div element with id 'plot'
85 | ```
86 |
87 |
88 | ### From Ammonite
89 |
90 | Load the corresponding dependency, and some imports, like
91 | ```scala
92 | import $ivy.`org.plotly-scala::plotly-render:0.8.1`
93 | import plotly._, element._, layout._, Plotly._
94 | ```
95 |
96 | Then plot things like
97 | ```scala
98 | val labels = Seq("Banana", "Banano", "Grapefruit")
99 | val valuesA = labels.map(_ => util.Random.nextGaussian())
100 | val valuesB = labels.map(_ => 0.5 + util.Random.nextGaussian())
101 |
102 | Seq(
103 | Bar(labels, valuesA, name = "A"),
104 | Bar(labels, valuesB, name = "B")
105 | ).plot(
106 | title = "Level"
107 | )
108 | ```
109 |
110 |
111 | ## Rationale
112 |
113 | Most high-level Javascript libraries for plotting have well designed APIs, enforcing immutability and almost relying on typed objects, although not explicitly. Yet, the few existing Scala libraries for plotting still try to mimick [matplotlib](http://matplotlib.org/) or Matlab, and have APIs requiring users to mutate things, in order to do plots. They also tend to lack a lot of features, especially compared to the current high-end Javascript plotting libraries. *plotly-scala* aims at filling this gap, by providing a reliable bridge from Scala towards the renowned [plotly.js](https://plot.ly/javascript/).
114 |
115 | ## Internals
116 |
117 | *plotly-scala* consists in a bunch of definitions, mostly case classes and sealed class hierarchies, closely following the API of plotly.js. It also contains JSON codecs for those, allowing to convert them to JSON that can be passed straightaway to plotly.js.
118 |
119 | Having the ability to convert these classes to JSON, the codecs can also go the other way around: from plotly.js-compatible JSON to plotly-scala Scala classes. This way of going is used by the tests of plotly-scala, to ensure that the examples of the plotly.js documentation, illustrating a wide range of the features of plotly.js, can be represented via the classes of plotly-scala. Namely, the Javascript examples of the documentation of plotly.js are run inside a [Rhino](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino) VM, with mocks of the plotly API. These mocks allow to keep the Javascript objects passed to the plotly.js API, and convert them to JSON. These JSON objects are then validated against the codecs of plotly-scala, to ensure that all their fields can be decoded by them. If these are fine, this gives a proof that all the features of the examples have a counterpart in plotly-scala.
120 |
121 | Internally, plotly-scala uses [circe](https://github.com/travisbrown/circe) (along with custom codec derivation mechanisms) to convert things to JSON, then render them. The circe objects don't appear in the plotly-scala API - circe is only used internally. The plotly-scala API only returns JSON strings, that can be passed to plotly.js. In subsequent versions, plotly-scala will likely try to shade circe and its dependencies, or switch to a more lightweight JSON library.
122 |
123 | ## Supported features
124 |
125 | plotly-scala supports the features illustrated in the following sections of the plotly.js documentation:
126 | - [Scatter Plots](https://plot.ly/javascript/line-and-scatter/),
127 | - [Bubble Charts](https://plot.ly/javascript/bubble-charts/),
128 | - [Line Charts](https://plot.ly/javascript/line-charts/),
129 | - [Bar Charts](https://plot.ly/javascript/bar-charts/),
130 | - [Horizontal Bar Charts](https://plot.ly/javascript/horizontal-bar-charts/),
131 | - [Filled Area Plots](https://plot.ly/javascript/filled-area-plots/),
132 | - [Time Series](https://plot.ly/javascript/time-series/),
133 | - [Subplots](https://plot.ly/javascript/subplots/),
134 | - [Multiple Axes](https://plot.ly/javascript/multiple-axes/),
135 | - [Histograms](https://plot.ly/javascript/histograms/),
136 | - [Log Plots](https://plot.ly/javascript/log-plot/),
137 | - [Image](https://plotly.com/javascript/reference/image/).
138 |
139 | Some of these are illustrated in the [demo](https://alexarchambault.github.io/plotly-scala/) page.
140 |
141 | ## Adding support for extra plotly.js features
142 |
143 | The following workflow can be followed to add support for extra sections of the plotly.js documentation:
144 | - 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,
145 | - 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),
146 | - running the tests with `sbt ~test`,
147 | - 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 | - add the required fields / class definitions, and possibly codecs, to have the added tests pass.
149 |
150 | ## About
151 |
152 | Battlefield tested since early 2016 at [Teads.tv](http://teads.tv)
153 |
154 | Released under the LGPL v3 license, copyright 2016-2019 Alexandre Archambault and contributors.
155 |
156 | Parts based on the original plotly.js API, which is copyright 2016 Plotly, Inc.
157 |
--------------------------------------------------------------------------------
/render/jvm/src/main/scala/plotly/Plotly.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | import java.io.{ByteArrayOutputStream, File, InputStream}
4 |
5 | import plotly.Codecs._
6 | import plotly.element.Color
7 | import plotly.layout._
8 | import java.lang.{Boolean => JBoolean, Double => JDouble, Integer => JInt}
9 | import java.nio.file.Files
10 |
11 | import argonaut.Argonaut._
12 | import argonaut.{Json, PrettyParams}
13 | import plotly.internals.{BetterPrinter, Properties}
14 |
15 | import scala.annotation.tailrec
16 |
17 | object Plotly {
18 |
19 | private val printer = BetterPrinter(PrettyParams.nospace.copy(dropNullKeys = true))
20 |
21 | def jsonSnippet(data: Seq[Trace], layout: Layout, config: Config): String = {
22 |
23 | val json = Json.obj(
24 | "data" -> data.toList.asJson,
25 | "layout" -> layout.asJson,
26 | "config" -> config.asJson
27 | )
28 |
29 | printer.render(json)
30 | }
31 |
32 | def jsSnippet(div: String, data: Seq[Trace], layout: Layout, config: Config): String = {
33 |
34 | val b = new StringBuilder
35 |
36 | b ++= "(function () {\n"
37 |
38 | for ((d, idx) <- data.zipWithIndex) {
39 | b ++= s" var data$idx = "
40 | b ++= printer.render(d.asJson)
41 | b ++= ";\n"
42 | }
43 |
44 | b ++= "\n "
45 | b ++= data.indices.map(idx => s"data$idx").mkString("var data = [", ", ", "];")
46 | b ++= "\n"
47 | b ++= " var layout = "
48 | b ++= printer.render(layout.asJson)
49 | b ++= ";\n var config = "
50 | b ++= printer.render(config.asJson)
51 | b ++= ";\n\n Plotly.plot('"
52 | b ++= div.replaceAll("'", "\\'")
53 | b ++= "', data, layout, config);\n"
54 |
55 | b ++= "})();"
56 |
57 | b.result()
58 | }
59 |
60 | private def readFully(is: InputStream): Array[Byte] = {
61 | val buffer = new ByteArrayOutputStream()
62 | val data = Array.ofDim[Byte](16384)
63 |
64 | var nRead = is.read(data, 0, data.length)
65 | while (nRead != -1) {
66 | buffer.write(data, 0, nRead)
67 | nRead = is.read(data, 0, data.length)
68 | }
69 |
70 | buffer.flush()
71 | buffer.toByteArray
72 | }
73 |
74 | def plotlyVersion: String =
75 | Properties.plotlyJsVersion
76 |
77 | def plotlyMinJs: String = {
78 | var is: InputStream = null
79 | try {
80 | is = getClass.getClassLoader.getResourceAsStream(
81 | s"META-INF/resources/webjars/plotly.js/$plotlyVersion/dist/plotly.min.js"
82 | )
83 | if (is == null)
84 | throw new Exception(s"plotly.min.js resource not found")
85 |
86 | new String(readFully(is), "UTF-8")
87 | } finally {
88 | if (is != null)
89 | is.close()
90 | }
91 | }
92 |
93 | def plot(
94 | path: String,
95 | traces: Seq[Trace],
96 | layout: Layout,
97 | config: Config = Config(),
98 | useCdn: Boolean = true,
99 | openInBrowser: Boolean = true,
100 | addSuffixIfExists: Boolean = true
101 | ): File = {
102 |
103 | val f0 = new File(path)
104 |
105 | val f =
106 | if (addSuffixIfExists) {
107 | lazy val name = f0.getName
108 | lazy val idx = name.lastIndexOf('.')
109 |
110 | lazy val (prefix, suffixOpt) =
111 | if (idx < 0)
112 | (name, None)
113 | else
114 | (name.take(idx), Some(name.drop(idx + 1)))
115 |
116 | def nameWithIndex(idx: Int) =
117 | s"$prefix-$idx${suffixOpt.fold("")("." + _)}"
118 |
119 | @tailrec
120 | def nonExisting(counter: Option[Int]): File = {
121 | val f = counter.fold(f0) { n =>
122 | new File(f0.getParentFile, nameWithIndex(n))
123 | }
124 |
125 | if (f.exists())
126 | nonExisting(counter.fold(Some(1))(n => Some(n + 1)))
127 | else
128 | f
129 | }
130 |
131 | nonExisting(None)
132 | } else if (f0.exists())
133 | throw new Exception(s"$f0 already exists")
134 | else
135 | f0
136 |
137 | val plotlyHeader =
138 | if (useCdn)
139 | s""""""
140 | else
141 | s""
142 |
143 | val divId = "chart"
144 |
145 | val html =
146 | s"""
147 | |
148 | |
149 | |
150 | |${layout.title.getOrElse("plotly chart")}
151 | |$plotlyHeader
152 | |
153 | |
154 | |
155 | |
158 | |
159 | |
160 | """.stripMargin
161 |
162 | Files.write(f.toPath, html.getBytes("UTF-8"))
163 |
164 | if (openInBrowser) {
165 | val cmdOpt = sys.props.get("os.name").map(_.toLowerCase) match {
166 | case Some("mac os x") =>
167 | Some(Seq("open", f.getAbsolutePath))
168 | case Some(win) if win.startsWith("windows") =>
169 | Some(Seq("cmd", s"start ${f.getAbsolutePath}"))
170 | case Some(lin) if lin.indexOf("linux") >= 0 =>
171 | Some(Seq("xdg-open", f.getAbsolutePath))
172 | case other =>
173 | None
174 | }
175 |
176 | cmdOpt match {
177 | case Some(cmd) =>
178 | sys.process.Process(cmd).!
179 | case None =>
180 | Console.err.println(s"Don't know how to open ${f.getAbsolutePath}")
181 | }
182 | }
183 |
184 | f
185 | }
186 |
187 | implicit class TraceOps(val trace: Trace) extends AnyVal {
188 | def plot(
189 | path: String,
190 | layout: Layout,
191 | useCdn: Boolean,
192 | openInBrowser: Boolean,
193 | addSuffixIfExists: Boolean
194 | ): Unit =
195 | Plotly.plot(
196 | path,
197 | Seq(trace),
198 | layout,
199 | useCdn = useCdn,
200 | openInBrowser = openInBrowser,
201 | addSuffixIfExists = addSuffixIfExists
202 | )
203 |
204 | @deprecated("Create a Layout and call plot(path, layout) instead", "0.8.0")
205 | def plot(
206 | path: String = "./plot.html",
207 | title: String = null,
208 | legend: Legend = null,
209 | width: JInt = null,
210 | height: JInt = null,
211 | showlegend: JBoolean = null,
212 | xaxis: Axis = null,
213 | yaxis: Axis = null,
214 | xaxis1: Axis = null,
215 | xaxis2: Axis = null,
216 | xaxis3: Axis = null,
217 | xaxis4: Axis = null,
218 | yaxis1: Axis = null,
219 | yaxis2: Axis = null,
220 | yaxis3: Axis = null,
221 | yaxis4: Axis = null,
222 | barmode: BarMode = null,
223 | autosize: JBoolean = null,
224 | margin: Margin = null,
225 | annotations: Seq[Annotation] = null,
226 | plot_bgcolor: Color = null,
227 | paper_bgcolor: Color = null,
228 | font: Font = null,
229 | bargap: JDouble = null,
230 | bargroupgap: JDouble = null,
231 | hovermode: HoverMode = null,
232 | boxmode: BoxMode = null,
233 | useCdn: Boolean = true,
234 | openInBrowser: Boolean = true,
235 | addSuffixIfExists: Boolean = true
236 | ): Unit =
237 | plot(
238 | path,
239 | Layout(
240 | title,
241 | legend,
242 | width,
243 | height,
244 | showlegend,
245 | xaxis,
246 | yaxis,
247 | xaxis1,
248 | xaxis2,
249 | xaxis3,
250 | xaxis4,
251 | yaxis1,
252 | yaxis2,
253 | yaxis3,
254 | yaxis4,
255 | barmode,
256 | autosize,
257 | margin,
258 | annotations,
259 | plot_bgcolor,
260 | paper_bgcolor,
261 | font,
262 | bargap,
263 | bargroupgap,
264 | hovermode,
265 | boxmode
266 | ),
267 | useCdn,
268 | openInBrowser,
269 | addSuffixIfExists
270 | )
271 |
272 | def plot(
273 | path: String,
274 | layout: Layout
275 | ): Unit =
276 | plot(
277 | path,
278 | layout,
279 | useCdn = true,
280 | openInBrowser = true,
281 | addSuffixIfExists = true
282 | )
283 | }
284 |
285 | implicit class TraceSeqOps(val traces: Seq[Trace]) extends AnyVal {
286 | def plot(
287 | path: String,
288 | layout: Layout,
289 | useCdn: Boolean,
290 | openInBrowser: Boolean,
291 | addSuffixIfExists: Boolean
292 | ): Unit =
293 | Plotly.plot(
294 | path,
295 | traces,
296 | layout,
297 | useCdn = useCdn,
298 | openInBrowser = openInBrowser,
299 | addSuffixIfExists = addSuffixIfExists
300 | )
301 |
302 | @deprecated("Create a Layout and call plot(path, layout) instead", "0.8.0")
303 | def plot(
304 | path: String = "./plot.html",
305 | title: String = null,
306 | legend: Legend = null,
307 | width: JInt = null,
308 | height: JInt = null,
309 | showlegend: JBoolean = null,
310 | xaxis: Axis = null,
311 | yaxis: Axis = null,
312 | xaxis1: Axis = null,
313 | xaxis2: Axis = null,
314 | xaxis3: Axis = null,
315 | xaxis4: Axis = null,
316 | yaxis1: Axis = null,
317 | yaxis2: Axis = null,
318 | yaxis3: Axis = null,
319 | yaxis4: Axis = null,
320 | barmode: BarMode = null,
321 | autosize: JBoolean = null,
322 | margin: Margin = null,
323 | annotations: Seq[Annotation] = null,
324 | plot_bgcolor: Color = null,
325 | paper_bgcolor: Color = null,
326 | font: Font = null,
327 | bargap: JDouble = null,
328 | bargroupgap: JDouble = null,
329 | hovermode: HoverMode = null,
330 | boxmode: BoxMode = null,
331 | useCdn: Boolean = true,
332 | openInBrowser: Boolean = true,
333 | addSuffixIfExists: Boolean = true
334 | ): Unit =
335 | plot(
336 | path,
337 | Layout(
338 | title,
339 | legend,
340 | width,
341 | height,
342 | showlegend,
343 | xaxis,
344 | yaxis,
345 | xaxis1,
346 | xaxis2,
347 | xaxis3,
348 | xaxis4,
349 | yaxis1,
350 | yaxis2,
351 | yaxis3,
352 | yaxis4,
353 | barmode,
354 | autosize,
355 | margin,
356 | annotations,
357 | plot_bgcolor,
358 | paper_bgcolor,
359 | font,
360 | bargap,
361 | bargroupgap,
362 | hovermode,
363 | boxmode
364 | ),
365 | useCdn,
366 | openInBrowser,
367 | addSuffixIfExists
368 | )
369 |
370 | def plot(
371 | path: String,
372 | layout: Layout
373 | ): Unit =
374 | plot(
375 | path,
376 | layout,
377 | useCdn = true,
378 | openInBrowser = true,
379 | addSuffixIfExists = true
380 | )
381 | }
382 |
383 | }
384 |
--------------------------------------------------------------------------------
/tests/src/test/scala/plotly/doc/DocumentationTests.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package doc
3 |
4 | import java.io.{ByteArrayOutputStream, File, InputStream}
5 | import java.lang.{Double => JDouble}
6 | import java.nio.file.Files
7 | import argonaut.Argonaut._
8 | import argonaut.{Json, Parse}
9 | import plotly.layout.Layout
10 | import org.mozilla.javascript._
11 | import org.scalatest.flatspec.AnyFlatSpec
12 | import org.scalatest.matchers.should.Matchers
13 | import plotly.element.HoverInfo
14 | import plotly.element.HoverInfo.{X, Y, Z}
15 | import plotly.element.ColorModel._
16 |
17 | import scala.util.matching.Regex
18 |
19 | object DocumentationTests {
20 |
21 | import plotly.Codecs._
22 |
23 | private def readFully(is: InputStream): Array[Byte] = {
24 | val buffer = new ByteArrayOutputStream()
25 | val data = Array.ofDim[Byte](16384)
26 |
27 | var nRead = is.read(data, 0, data.length)
28 | while (nRead != -1) {
29 | buffer.write(data, 0, nRead)
30 | nRead = is.read(data, 0, data.length)
31 | }
32 |
33 | buffer.flush()
34 | buffer.toByteArray
35 | }
36 |
37 | def load(path: String): String = {
38 |
39 | val cl = getClass.getClassLoader // resources should be in the same JAR as this, so same loader
40 | val resPath = s"plotly/doc/$path"
41 | val is = cl.getResourceAsStream(resPath)
42 |
43 | if (is == null)
44 | throw new NoSuchElementException(s"Resource $resPath")
45 |
46 | val res = readFully(is)
47 |
48 | new String(res, "UTF-8")
49 | }
50 |
51 | def resourceTrace(res: String): Trace = {
52 | val dataStr = load(res)
53 | val result = dataStr.decodeEither[Trace]
54 | result.getOrElse {
55 | throw new Exception(s"$res: $result")
56 | }
57 | }
58 |
59 | def resourceLayout(res: String): Layout = {
60 | val dataStr = load(res)
61 | val result = dataStr.decodeEither[Layout]
62 | result.getOrElse {
63 | throw new Exception(s"$res: $result")
64 | }
65 | }
66 |
67 | private class Plotly extends plotly.doc.Plotly {
68 |
69 | var dataOpt = Option.empty[Object]
70 | var layoutOpt = Option.empty[Object]
71 |
72 | def newPlot(div: String, data: Object, layout: Object, other: Object): Unit = {
73 | dataOpt = Option(data)
74 | layoutOpt = Option(layout)
75 | }
76 |
77 | def newPlot(div: String, data: Object, layout: Object): Unit = {
78 | dataOpt = Option(data)
79 | layoutOpt = Option(layout)
80 | }
81 |
82 | def newPlot(div: String, data: Object): Unit = {
83 | dataOpt = Option(data)
84 | }
85 |
86 | def plot(div: String, data: Object, layout: Object): Unit =
87 | newPlot(div, data, layout)
88 | def plot(div: String, data: Object): Unit =
89 | newPlot(div, data)
90 |
91 | def result(cx: Context, scope: ScriptableObject): (Seq[Json], Option[Json]) = {
92 | def stringify(obj: Object) =
93 | NativeJSON.stringify(cx, scope, obj, null, null).toString
94 |
95 | def jsonRepr(obj: Object): Json = {
96 | val jsonStr = stringify(obj)
97 | Parse
98 | .parse(jsonStr)
99 | .left
100 | .map { err =>
101 | throw new Exception(s"Cannot parse JSON: $err\n$jsonStr")
102 | }
103 | .merge
104 | }
105 |
106 | val data = dataOpt.map(jsonRepr) match {
107 | case None =>
108 | throw new NoSuchElementException("data not set")
109 | case Some(json) =>
110 | json.array.getOrElse {
111 | throw new Exception(s"data is not a JSON array\n${json.spaces2}")
112 | }
113 | }
114 |
115 | (data, layoutOpt.map(jsonRepr))
116 | }
117 | }
118 |
119 | private object Document extends plotly.doc.Document {
120 | // stub...
121 | def getElementById(id: String): String = id
122 | }
123 |
124 | private object Numeric {
125 | def linspace(from: Int, to: Int, count: Int) = {
126 | val step = (to - from).toDouble / (count - 1)
127 | new NativeArrayWithDefault((0 until count).map(n => from + n * step: JDouble).toArray[AnyRef], 0.0: JDouble)
128 | }
129 | def linspace(from: Double, to: Double, count: Int) = {
130 | val step = (to - from) / (count - 1)
131 | new NativeArrayWithDefault((0 until count).map(n => from + n * step: JDouble).toArray[AnyRef], 0.0: JDouble)
132 | }
133 | }
134 |
135 | def linspaceImpl(cx: Context, thisObj: Scriptable, args: Array[Object], funObj: Function): AnyRef =
136 | args.toSeq.map(x => x: Any) match {
137 | case Seq(from: Int, to: Int, step: Int) =>
138 | Numeric.linspace(from, to, step)
139 | case Seq(from: Double, to: Int, step: Int) =>
140 | Numeric.linspace(from, to.toDouble, step)
141 | case Seq(from: Double, to: Double, step: Int) =>
142 | Numeric.linspace(from, to, step)
143 | case other => throw new NoSuchElementException(s"linspace${other.mkString("(", ", ", ")")}")
144 | }
145 |
146 | def requireImpl(cx: Context, thisObj: Scriptable, args: Array[Object], funObj: Function): AnyRef =
147 | args match {
148 | case Array("linspace") => linspace(thisObj)
149 | case other => throw new NoSuchElementException(s"require${other.mkString("(", ", ", ")")}")
150 | }
151 |
152 | private def linspace(scope: Scriptable) = new FunctionObject(
153 | "linspace",
154 | classOf[DocumentationTests].getMethods.find(_.getName == "linspaceImpl").get,
155 | scope
156 | )
157 |
158 | private def require(scope: Scriptable) = new FunctionObject(
159 | "require",
160 | classOf[DocumentationTests].getMethods.find(_.getName == "requireImpl").get,
161 | scope
162 | )
163 |
164 | def plotlyDemoElements(demo: String): (Seq[Trace], Option[Layout]) = {
165 |
166 | val plotly = new Plotly
167 |
168 | val cx = Context.enter()
169 |
170 | val (rawDataElems, rawLayoutOpt) =
171 | try {
172 | val scope = cx.initStandardObjects()
173 | ScriptableObject.putProperty(scope, "Plotly", plotly)
174 | ScriptableObject.putProperty(scope, "document", Document)
175 | ScriptableObject.putProperty(scope, "numeric", Numeric)
176 | ScriptableObject.putProperty(scope, "require", require(scope))
177 | ScriptableObject.putProperty(scope, "linspace", linspace(scope))
178 | cx.evaluateString(scope, demo, "", 1, null)
179 | plotly.result(cx, scope)
180 | } catch {
181 | case e: org.mozilla.javascript.EvaluatorException =>
182 | println(s"Was running\n$demo\n\n")
183 | throw new Exception(s"Evaluation error at line ${e.lineNumber()} column ${e.columnNumber()}", e)
184 | } finally {
185 | Context.exit()
186 | }
187 |
188 | val decodeData0 = rawDataElems.map(json => json -> json.as[Trace].toEither)
189 |
190 | val dataErrors = decodeData0.collect { case (json, Left((err, h))) =>
191 | (json, err, h)
192 | }
193 |
194 | if (dataErrors.nonEmpty) {
195 | for ((json, err, h) <- dataErrors)
196 | Console.err.println(s"Decoding data: $err ($h)\n${json.spaces2}\n")
197 |
198 | throw new Exception("Error decoding data (see above messages)")
199 | }
200 |
201 | val data = decodeData0.collect { case (_, Right(data)) =>
202 | data
203 | }
204 |
205 | val decodeLayoutOpt = rawLayoutOpt.map(json => json -> json.as[Layout].toEither)
206 |
207 | val layoutOpt = decodeLayoutOpt.map {
208 | case (json, Left((err, h))) =>
209 | Console.err.println(s"Decoding layout: $err ($h)\n${json.spaces2}\n")
210 | throw new Exception("Error decoding layout (see above messages)")
211 |
212 | case (_, Right(layout)) => layout
213 | }
214 |
215 | (data, layoutOpt)
216 | }
217 |
218 | def stripFrontMatter(content: String): String = {
219 | val lines = content.linesIterator.toVector
220 | lines match {
221 | case Seq("---", remaining0 @ _*) =>
222 | val idx = remaining0.indexOf("---")
223 | if (idx >= 0)
224 | remaining0
225 | .drop(idx + 1)
226 | .mkString("\n")
227 | .replaceAll(Regex.quote("{%") + ".*" + Regex.quote("%}"), "")
228 | .replaceAll(Regex.quote("<"), "<")
229 | .replaceAll(Regex.quote(">"), ">")
230 | else
231 | throw new Exception(s"Unrecognized format:\n$content")
232 | case _ =>
233 | content
234 | }
235 | }
236 |
237 | }
238 |
239 | class DocumentationTests extends AnyFlatSpec with Matchers {
240 |
241 | import DocumentationTests._
242 |
243 | val dir = new File("plotly-documentation/_posts/plotly_js")
244 | val subDirNames = Seq(
245 | "basic/line_and_scatter",
246 | "basic/line-plots",
247 | "basic/bar",
248 | "basic/horizontal-bar",
249 | // TODO? Pie charts
250 | "financial/time-series",
251 | "financial/candlestick-charts",
252 | // "financial/ohlc",
253 | "basic/bubble",
254 | "basic/area",
255 | "fundamentals/sizing",
256 | // TODO? Gauge charts
257 | // TODO Multiple chart types (needs contour)
258 | // TODO Shapes (need mock of d3)
259 | "subplot/subplots",
260 | "subplot/multiple-axes",
261 | "subplot/insets",
262 | // TODO Responsive demo (only a demo, no new chart type / attributes)
263 | "statistical/error-bar",
264 | // TODO Continuous error bars
265 | "statistical/box",
266 | // TODO 2D Density plots
267 | "statistical/histogram",
268 | "scientific/heatmap",
269 | // TODO 2D Histograms
270 | // TODO Wind rose charts
271 | // TODO Contour plots
272 | // TODO Heatmaps
273 | // TODO Heatmap and contour colorscales
274 | // TODO Polar charts
275 | "scientific/log",
276 | // TODO Financial charts
277 | // TODO Maps
278 | "3d/3d-surface"
279 | )
280 |
281 | val subDirs = subDirNames.map(new File(dir, _))
282 |
283 | for {
284 | subDir <- subDirs
285 | post <- subDir.listFiles().sorted
286 | if !post.getName.startsWith(".")
287 | } {
288 | s"$subDir" should s"$post" in {
289 | val rawContent = new String(Files.readAllBytes(post.toPath), "UTF-8")
290 | val content = stripFrontMatter(rawContent)
291 | .replace("
", "\\n")
292 | .replace("", "\\n")
293 | .replace("(...size)", "(size[0])") // rhino doesn't seem to support the spead (...) operator
294 | .replace("desired_maximum_marker_size**2", "desired_maximum_marker_size*desired_maximum_marker_size")
295 | .replace(
296 | """function linspace(a,b,n) {
297 | return Plotly.d3.range(n).map(function(i){return a+i*(b-a)/(n-1);});
298 | }
299 | """,
300 | ""
301 | )
302 |
303 | if (content.contains("Plotly.d3.csv"))
304 | println(s"Ignoring $post (Plotly.d3.csv not implemented)")
305 | else {
306 |
307 | val lines = content.linesIterator.toVector
308 | .map(_.trim)
309 | .filter(_.nonEmpty)
310 |
311 | if (lines.nonEmpty)
312 | plotlyDemoElements(content)
313 | }
314 | }
315 | }
316 |
317 | it should "demo Image Trace" in {
318 | val js =
319 | """
320 | |var data = [
321 | | {
322 | | type: "image",
323 | | opacity: 0.1,
324 | | x0: 0.05,
325 | | y0: 0.05,
326 | | colormodel: "rgb",
327 | | hoverinfo: "x+y+z+color",
328 | | z: [[[255, 0, 0], [0, 255, 0], [0, 0, 255]]]
329 | | }
330 | |];
331 | |
332 | |var layout = {
333 | | width: 400,
334 | | height: 400,
335 | | title: "image with opacity 0.1"
336 | |};
337 | |
338 | |Plotly.newPlot('myDiv', data, layout);
339 | |""".stripMargin
340 | val (data, maybeLayout) = plotlyDemoElements(js)
341 | maybeLayout should ===(
342 | Some(
343 | Layout()
344 | .withWidth(400)
345 | .withHeight(400)
346 | .withTitle("image with opacity 0.1")
347 | )
348 | )
349 |
350 | data.headOption match {
351 | case Some(image) =>
352 | val colors = Seq(
353 | Seq(Seq(255d, 0d, 0d), Seq(0d, 255, 0), Seq(0d, 0, 255))
354 | )
355 | val expected = Image(z = colors)
356 | .withOpacity(0.1)
357 | .withX0(0.05)
358 | .withY0(0.05)
359 | .withHoverinfo(HoverInfo(X, Y, Z, HoverInfo.Color))
360 | .withColormodel(RGB)
361 |
362 | image should ===(expected)
363 | case None =>
364 | fail("data must contain an image trace")
365 | }
366 | }
367 |
368 | }
369 |
--------------------------------------------------------------------------------
/core/shared/src/main/scala/plotly/Trace.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 |
3 | import scala.language.implicitConversions
4 |
5 | import java.lang.{Boolean => JBoolean, Double => JDouble}
6 |
7 | import dataclass._
8 | import plotly.element._
9 |
10 | sealed abstract class Trace extends Product with Serializable
11 |
12 | @data(optionSetters = true) class Scatter(
13 | x: Option[Sequence] = None,
14 | y: Option[Sequence] = None,
15 | text: Option[OneOrSeq[String]] = None,
16 | mode: Option[ScatterMode] = None,
17 | marker: Option[Marker] = None,
18 | line: Option[Line] = None,
19 | textposition: Option[TextPosition] = None,
20 | textfont: Option[TextFont] = None,
21 | name: Option[String] = None,
22 | connectgaps: Option[Boolean] = None,
23 | xaxis: Option[AxisReference] = None,
24 | yaxis: Option[AxisReference] = None,
25 | fill: Option[Fill] = None,
26 | error_x: Option[Error] = None,
27 | error_y: Option[Error] = None,
28 | showlegend: Option[Boolean] = None,
29 | fillcolor: Option[OneOrSeq[Color]] = None,
30 | hoverinfo: Option[HoverInfo] = None,
31 | hoveron: Option[HoverOn] = None,
32 | stackgroup: Option[String] = None,
33 | groupnorm: Option[GroupNorm] = None,
34 | @since("0.8.2")
35 | hovertemplate: Option[OneOrSeq[String]] = None
36 | ) extends Trace
37 |
38 | object Scatter {
39 | def apply(x: Sequence, y: Sequence): Scatter =
40 | Scatter().withX(x).withY(y)
41 |
42 | def apply(y: Sequence): Scatter =
43 | Scatter().withY(y)
44 |
45 | @deprecated("Use Scatter() and chain-call .with* methods on it instead", "0.8.0")
46 | def apply(
47 | values: Sequence = null,
48 | secondValues: Sequence = null,
49 | text: OneOrSeq[String] = null,
50 | mode: ScatterMode = null,
51 | marker: Marker = null,
52 | line: Line = null,
53 | textposition: TextPosition = null,
54 | textfont: TextFont = null,
55 | name: String = null,
56 | connectgaps: JBoolean = null,
57 | xaxis: AxisReference = null,
58 | yaxis: AxisReference = null,
59 | fill: Fill = null,
60 | error_x: Error = null,
61 | error_y: Error = null,
62 | showlegend: JBoolean = null,
63 | fillcolor: OneOrSeq[Color] = null,
64 | hoverinfo: HoverInfo = null,
65 | hoveron: HoverOn = null,
66 | stackgroup: String = null,
67 | groupnorm: GroupNorm = null
68 | ): Scatter = {
69 |
70 | val (xOpt, yOpt) = Option(secondValues) match {
71 | case Some(y) => (Option(values), Some(y))
72 | case None => (None, Option(values))
73 | }
74 |
75 | Scatter(
76 | xOpt,
77 | yOpt,
78 | Option(text),
79 | Option(mode),
80 | Option(marker),
81 | Option(line),
82 | Option(textposition),
83 | Option(textfont),
84 | Option(name),
85 | Option(connectgaps).map(x => x: Boolean),
86 | Option(xaxis),
87 | Option(yaxis),
88 | Option(fill),
89 | Option(error_x),
90 | Option(error_y),
91 | Option(showlegend).map(b => b: Boolean),
92 | Option(fillcolor),
93 | Option(hoverinfo),
94 | Option(hoveron),
95 | Option(stackgroup),
96 | Option(groupnorm)
97 | )
98 | }
99 | }
100 |
101 | @data(optionSetters = true) class Box(
102 | y: Option[Sequence] = None,
103 | x: Option[Sequence] = None,
104 | boxpoints: Option[BoxPoints] = None,
105 | jitter: Option[Double] = None,
106 | pointpos: Option[Double] = None,
107 | name: Option[String] = None,
108 | marker: Option[Marker] = None,
109 | orientation: Option[Orientation] = None,
110 | whiskerwidth: Option[Double] = None,
111 | boxmean: Option[BoxMean] = None,
112 | fillcolor: Option[OneOrSeq[Color]] = None,
113 | line: Option[Line] = None,
114 | showlegend: Option[Boolean] = None,
115 | @since("0.8.2")
116 | hovertemplate: Option[OneOrSeq[String]] = None
117 | ) extends Trace
118 |
119 | object Box {
120 | def apply(y: Sequence): Box =
121 | Box().withY(y)
122 |
123 | def apply(y: Sequence, x: Sequence): Box =
124 | Box().withY(y).withX(x)
125 |
126 | @deprecated("Use Box() and chain-call .with* methods on it instead", "0.8.0")
127 | def apply(
128 | y: Sequence = null,
129 | x: Sequence = null,
130 | boxpoints: BoxPoints = null,
131 | jitter: JDouble = null,
132 | pointpos: JDouble = null,
133 | name: String = null,
134 | marker: Marker = null,
135 | orientation: Orientation = null,
136 | whiskerwidth: JDouble = null,
137 | boxmean: BoxMean = null,
138 | fillcolor: OneOrSeq[Color] = null,
139 | line: Line = null,
140 | showlegend: JBoolean = null
141 | ): Box =
142 | Box(
143 | Option(y),
144 | Option(x),
145 | Option(boxpoints),
146 | Option(jitter).map(d => d: Double),
147 | Option(pointpos).map(d => d: Double),
148 | Option(name),
149 | Option(marker),
150 | Option(orientation),
151 | Option(whiskerwidth).map(d => d: Double),
152 | Option(boxmean),
153 | Option(fillcolor),
154 | Option(line),
155 | Option(showlegend).map(b => b: Boolean)
156 | )
157 | }
158 |
159 | @data(optionSetters = true) class Image(
160 | z: Seq[Seq[Seq[Double]]],
161 | x0: Option[Element] = None,
162 | y0: Option[Element] = None,
163 | name: Option[String] = None,
164 | text: Option[Seq[String]] = None,
165 | opacity: Option[Double] = None,
166 | ids: Option[Seq[String]] = None,
167 | dx: Option[Double] = None,
168 | dy: Option[Double] = None,
169 | source: Option[String] = None,
170 | hoverinfo: Option[HoverInfo] = None,
171 | hovertemplate: Option[Seq[String]] = None,
172 | meta: Option[String] = None,
173 | customdata: Option[Seq[String]] = None,
174 | xaxis: Option[AxisReference] = None,
175 | yaxis: Option[AxisReference] = None,
176 | colormodel: Option[ColorModel] = None,
177 | zmax: Option[Seq[Double]] = None,
178 | zmin: Option[Seq[Double]] = None,
179 | hoverlabel: Option[HoverLabel] = None
180 | ) extends Trace
181 |
182 | @data(optionSetters = true) class Bar(
183 | x: Sequence,
184 | y: Sequence,
185 | @since
186 | name: Option[String] = None,
187 | text: Option[Seq[String]] = None,
188 | marker: Option[Marker] = None,
189 | orientation: Option[Orientation] = None,
190 | xaxis: Option[AxisReference] = None,
191 | yaxis: Option[AxisReference] = None,
192 | error_y: Option[Error] = None,
193 | showlegend: Option[Boolean] = None,
194 | hoverinfo: Option[HoverInfo] = None,
195 | textposition: Option[BarTextPosition] = None,
196 | opacity: Option[Double] = None,
197 | width: Option[OneOrSeq[Double]] = None,
198 | base: Option[OneOrSeq[Double]] = None,
199 | @since("0.8.2")
200 | hovertemplate: Option[OneOrSeq[String]] = None
201 | ) extends Trace
202 |
203 | object Bar {
204 | @deprecated("Use Bar() and chain-call .with* methods on it instead", "0.8.0")
205 | def apply(
206 | x: Sequence,
207 | y: Sequence,
208 | name: String = null,
209 | text: Seq[String] = null,
210 | marker: Marker = null,
211 | orientation: Orientation = null,
212 | xaxis: AxisReference = null,
213 | yaxis: AxisReference = null,
214 | error_y: Error = null,
215 | showlegend: JBoolean = null,
216 | hoverinfo: HoverInfo = null,
217 | textposition: BarTextPosition = null,
218 | opacity: JDouble = null,
219 | width: OneOrSeq[Double] = null,
220 | base: OneOrSeq[Double] = null
221 | ): Bar =
222 | Bar(
223 | x,
224 | y,
225 | Option(name),
226 | Option(text),
227 | Option(marker),
228 | Option(orientation),
229 | Option(xaxis),
230 | Option(yaxis),
231 | Option(error_y),
232 | Option(showlegend).map(b => b: Boolean),
233 | Option(hoverinfo),
234 | Option(textposition),
235 | Option(opacity).map(d => d: Double),
236 | Option(width),
237 | Option(base)
238 | )
239 | }
240 |
241 | @data(optionSetters = true) class Histogram(
242 | x: Option[Sequence] = None,
243 | y: Option[Sequence] = None,
244 | opacity: Option[Double] = None,
245 | name: Option[String] = None,
246 | autobinx: Option[Boolean] = None,
247 | marker: Option[Marker] = None,
248 | xbins: Option[Bins] = None,
249 | histnorm: Option[HistNorm] = None,
250 | showlegend: Option[Boolean] = None,
251 | cumulative: Option[Cumulative] = None,
252 | histfunc: Option[HistFunc] = None,
253 | @since("0.8.2")
254 | hovertemplate: Option[OneOrSeq[String]] = None,
255 | @since("0.8.5")
256 | yaxis: Option[String] = None,
257 | hovertext: Option[OneOrSeq[String]] = None
258 | ) extends Trace
259 |
260 | object Histogram {
261 | def apply(x: Sequence): Histogram =
262 | Histogram().withX(x)
263 |
264 | def apply(x: Sequence, y: Sequence): Histogram =
265 | Histogram().withX(x).withY(y)
266 |
267 | @deprecated("Use Histogram() and chain-call .with* methods on it instead", "0.8.0")
268 | def apply(
269 | x: Sequence = null,
270 | y: Sequence = null,
271 | opacity: JDouble = null,
272 | name: String = null,
273 | autobinx: JBoolean = null,
274 | marker: Marker = null,
275 | xbins: Bins = null,
276 | histnorm: HistNorm = null,
277 | showlegend: JBoolean = null,
278 | cumulative: Cumulative = null,
279 | histfunc: HistFunc = null
280 | ): Histogram =
281 | Histogram(
282 | Option(x),
283 | Option(y),
284 | Option(opacity).map(d => d: Double),
285 | Option(name),
286 | Option(autobinx).map(b => b: Boolean),
287 | Option(marker),
288 | Option(xbins),
289 | Option(histnorm),
290 | Option(showlegend).map(b => b: Boolean),
291 | Option(cumulative),
292 | Option(histfunc)
293 | )
294 | }
295 |
296 | @data(optionSetters = true) class Surface(
297 | x: Option[Sequence] = None,
298 | y: Option[Sequence] = None,
299 | z: Option[Sequence] = None,
300 | showscale: Option[Boolean] = None,
301 | opacity: Option[Double] = None,
302 | @since("0.8.2")
303 | hovertemplate: Option[OneOrSeq[String]] = None
304 | ) extends Trace
305 |
306 | object Surface {
307 | @deprecated("Use Surface() and chain-call .with* methods on it instead", "0.8.0")
308 | def apply(
309 | x: Sequence = null,
310 | y: Sequence = null,
311 | z: Sequence = null,
312 | showscale: JBoolean = null,
313 | opacity: JDouble = null
314 | ): Surface =
315 | Surface(
316 | Option(x),
317 | Option(y),
318 | Option(z),
319 | Option(showscale).map(b => b: Boolean),
320 | Option(opacity).map(d => d: Double)
321 | )
322 | }
323 |
324 | @data(optionSetters = true) class Heatmap(
325 | y: Option[Sequence] = None,
326 | x: Option[Sequence] = None,
327 | z: Option[Sequence] = None,
328 | autocolorscale: Option[Boolean] = None,
329 | colorscale: Option[ColorScale] = None,
330 | showscale: Option[Boolean] = None,
331 | name: Option[String] = None,
332 | @since("0.8.2")
333 | hovertemplate: Option[OneOrSeq[String]] = None,
334 | hoverongaps: Option[Boolean] = None
335 | ) extends Trace
336 |
337 | object Heatmap {
338 | def apply(z: Sequence): Heatmap =
339 | Heatmap().withZ(z)
340 |
341 | def apply(z: Sequence, x: Sequence, y: Sequence): Heatmap =
342 | Heatmap().withZ(z).withX(x).withY(y)
343 |
344 | @deprecated("Use Heatmap() and chain-call .with* methods on it instead", "0.8.0")
345 | def apply(
346 | y: Sequence = null,
347 | x: Sequence = null,
348 | z: Sequence = null,
349 | autocolorscale: JBoolean = null,
350 | colorscale: ColorScale = null,
351 | showscale: JBoolean = null,
352 | name: String = null
353 | ): Heatmap =
354 | Heatmap(
355 | Option(y),
356 | Option(x),
357 | Option(z),
358 | Option(autocolorscale).map(b => b: Boolean),
359 | Option(colorscale),
360 | Option(showscale).map(b => b: Boolean),
361 | Option(name)
362 | )
363 | }
364 |
365 | @data(optionSetters = true) class Candlestick(
366 | x: Option[Sequence] = None,
367 | close: Option[Sequence] = None,
368 | high: Option[Sequence] = None,
369 | low: Option[Sequence] = None,
370 | open: Option[Sequence] = None,
371 | decreasing: Option[Marker] = None,
372 | increasing: Option[Marker] = None,
373 | line: Option[Marker] = None,
374 | xaxis: Option[AxisReference] = None,
375 | yaxis: Option[AxisReference] = None
376 | ) extends Trace
377 |
378 | object Candlestick {
379 | @deprecated("Use Candlestick() and chain-call .with* methods on it instead", "0.8.0")
380 | def apply(
381 | x: Sequence = null,
382 | close: Sequence = null,
383 | high: Sequence = null,
384 | low: Sequence = null,
385 | open: Sequence = null,
386 | decreasing: Marker = null,
387 | increasing: Marker = null,
388 | line: Marker = null,
389 | xaxis: AxisReference = null,
390 | yaxis: AxisReference = null
391 | ): Candlestick =
392 | Candlestick(
393 | Option(x),
394 | Option(close),
395 | Option(high),
396 | Option(low),
397 | Option(open),
398 | Option(decreasing),
399 | Option(increasing),
400 | Option(line),
401 | Option(xaxis),
402 | Option(yaxis)
403 | )
404 | }
405 |
--------------------------------------------------------------------------------
/render/shared/src/main/scala/plotly/internals/ArgonautCodecsInternals.scala:
--------------------------------------------------------------------------------
1 | package plotly
2 | package internals
3 |
4 | import java.math.BigInteger
5 |
6 | import argonaut._
7 | import argonaut.Argonaut._
8 | import argonaut.ArgonautShapeless._
9 | import argonaut.derive._
10 | import shapeless._
11 |
12 | import scala.util.Try
13 | import plotly.element._
14 | import plotly.layout._
15 |
16 | object ArgonautCodecsInternals extends ArgonautCodecsExtra {
17 |
18 | sealed abstract class IsWrapper[W]
19 |
20 | implicit def isWrapperEncode[W, L <: HList, T](implicit
21 | ev: IsWrapper[W],
22 | gen: Generic.Aux[W, L],
23 | isHCons: ops.hlist.IsHCons.Aux[L, T, HNil],
24 | underlying: EncodeJson[T]
25 | ): EncodeJson[W] =
26 | EncodeJson { w =>
27 | val t = isHCons.head(gen.to(w))
28 | t.asJson
29 | }
30 |
31 | implicit def isWrapperDecode[W, L <: HList, T](implicit
32 | ev: IsWrapper[W],
33 | gen: Generic.Aux[W, L],
34 | isHCons: ops.hlist.IsHCons.Aux[L, T, HNil],
35 | underlying: DecodeJson[T]
36 | ): DecodeJson[W] =
37 | DecodeJson { c =>
38 | c.as[T]
39 | .map(t => gen.from((t :: HNil).asInstanceOf[L]) // FIXME
40 | )
41 | }
42 |
43 | implicit val boxMeanBoolIsWrapper: IsWrapper[BoxMean.Bool] = null
44 | implicit val boxPointsBoolIsWrapper: IsWrapper[BoxPoints.Bool] = null
45 | implicit val sequenceDoublesIsWrapper: IsWrapper[Sequence.Doubles] = null
46 | implicit val sequenceNestedDoublesIsWrapper: IsWrapper[Sequence.NestedDoubles] = null
47 | implicit val sequenceNestedIntsIsWrapper: IsWrapper[Sequence.NestedInts] = null
48 | implicit val sequenceStringsIsWrapper: IsWrapper[Sequence.Strings] = null
49 | implicit val sequenceDatetimesIsWrapper: IsWrapper[Sequence.DateTimes] = null
50 | implicit val rangeDoublesIsWrapper: IsWrapper[Range.Doubles] = null
51 | implicit val rangeDatetimesIsWrapper: IsWrapper[Range.DateTimes] = null
52 | implicit val doubleElementIsWrapper: IsWrapper[Element.DoubleElement] = null
53 | implicit val stringElementIsWrapper: IsWrapper[Element.StringElement] = null
54 | implicit def oneOrSeqOneIsWrapper[T]: IsWrapper[OneOrSeq.One[T]] = null
55 | implicit def oneOrSeqSequenceIsWrapper[T]: IsWrapper[OneOrSeq.Sequence[T]] = null
56 |
57 | def flagEncoder[T, F](flags: T => Set[F], label: F => String): EncodeJson[T] =
58 | EncodeJson { t =>
59 | val s = flags(t).toSeq match {
60 | case Seq() => "none"
61 | case nonEmpty => nonEmpty.map(label).mkString("+")
62 | }
63 |
64 | s.asJson
65 | }
66 |
67 | def flagDecoder[T, F](type0: String, map: Map[String, F], build: Set[F] => T): DecodeJson[T] =
68 | DecodeJson { c =>
69 | c.as[String].flatMap { s =>
70 | val flags =
71 | if (s == "none")
72 | DecodeResult.ok(Set.empty[F])
73 | else
74 | s.split('+').foldLeft[DecodeResult[Set[F]]](DecodeResult.ok(Set.empty[F])) { case (acc, f) =>
75 | for {
76 | acc0 <- acc
77 | f0 <-
78 | map
79 | .get(f)
80 | .fold[DecodeResult[F]](DecodeResult.fail(s"Unrecognized $type0: $f", c.history))(DecodeResult.ok)
81 | } yield acc0 + f0
82 | }
83 |
84 | flags.map(build)
85 | }
86 | }
87 |
88 | sealed abstract class IsEnum[-T] {
89 | def label(t: T): String
90 | }
91 |
92 | object IsEnum {
93 | def apply[T](implicit isEnum: IsEnum[T]): IsEnum[T] = isEnum
94 |
95 | def instance[T](f: T => String): IsEnum[T] =
96 | new IsEnum[T] {
97 | def label(t: T): String = f(t)
98 | }
99 | }
100 |
101 | implicit def isEnumEncoder[T: IsEnum]: EncodeJson[T] =
102 | EncodeJson.of[String].contramap(IsEnum[T].label)
103 |
104 | implicit def isEnumDecoder[T](implicit
105 | isEnum: IsEnum[T],
106 | enumerate: Enumerate[T],
107 | typeable: Typeable[T]
108 | ): DecodeJson[T] =
109 | DecodeJson {
110 | val underlying = DecodeJson.of[String]
111 | val map = enumerate().map(e => isEnum.label(e) -> e).toMap
112 | val name = typeable.describe // TODO split in words
113 |
114 | c =>
115 | underlying(c).flatMap { s =>
116 | map.get(s) match {
117 | case None => DecodeResult.fail(s"Unrecognized $name: '$s'", c.history)
118 | case Some(m) => DecodeResult.ok(m)
119 | }
120 | }
121 | }
122 |
123 | implicit val anchorIsEnum = IsEnum.instance[Anchor](_.label)
124 | implicit val refIsEnum = IsEnum.instance[Ref](_.label)
125 | implicit val axisAnchorIsEnum = IsEnum.instance[AxisAnchor](_.label)
126 | implicit val axisReferenceIsEnum = IsEnum.instance[AxisReference](_.label)
127 | implicit val axisTypeIsEnum = IsEnum.instance[AxisType](_.label)
128 | implicit val barModeIsEnum = IsEnum.instance[BarMode](_.label)
129 | implicit val boxModeIsEnum = IsEnum.instance[BoxMode](_.label)
130 | implicit val dashIsEnum = IsEnum.instance[Dash](_.label)
131 | implicit val fillIsEnum = IsEnum.instance[Fill](_.label)
132 | implicit val hoverModeIsEnum = IsEnum.instance[HoverMode](_.label)
133 | implicit val lineShapeIsEnum = IsEnum.instance[LineShape](_.label)
134 | implicit val orientationIsEnum = IsEnum.instance[Orientation](_.label)
135 | implicit val traceOrderIsEnum = IsEnum.instance[TraceOrder](_.label)
136 | implicit val boxMeanOtherIsEnum = IsEnum.instance[BoxMean.Labeled](_.label)
137 | implicit val boxPointsOtherIsEnum = IsEnum.instance[BoxPoints.Labeled](_.label)
138 | implicit val textPositionIsEnum = IsEnum.instance[TextPosition](_.label)
139 | implicit val barTextPositionIsEnum = IsEnum.instance[BarTextPosition](_.label)
140 | implicit val sideIsEnum = IsEnum.instance[Side](_.label)
141 | implicit val symbolIsEnum = IsEnum.instance[Symbol](_.label)
142 | implicit val ticksIsEnum = IsEnum.instance[Ticks](_.label)
143 | implicit val histNormIsEnum = IsEnum.instance[HistNorm](_.label)
144 | implicit val sizeModeIsEnum = IsEnum.instance[SizeMode](_.label)
145 | implicit val hoverOnIsEnum = IsEnum.instance[HoverOn](_.label)
146 | implicit val groupNormIsEnum = IsEnum.instance[GroupNorm](_.label)
147 | implicit val histFuncIsEnum = IsEnum.instance[HistFunc](_.label)
148 | implicit val tickModeIsEnum = IsEnum.instance[TickMode](_.mode)
149 | implicit val patternIsEnum = IsEnum.instance[Pattern](_.label)
150 | implicit val rowOrderIsEnum = IsEnum.instance[RowOrder](_.label)
151 | implicit val alignmentIsEnum = IsEnum.instance[Alignment](_.label)
152 | implicit val colorModelIsEnum = IsEnum.instance[ColorModel](_.label)
153 |
154 | def jsonSumDirectCodecFor(name: String): JsonSumCodec = new JsonSumCodec {
155 | def encodeEmpty: Nothing =
156 | throw new IllegalArgumentException(s"empty $name")
157 |
158 | def encodeField(fieldOrObj: Either[Json, (String, Json)]): Json =
159 | fieldOrObj match {
160 | case Left(other) => other
161 | case Right((_, content)) => content
162 | }
163 |
164 | def decodeEmpty(cursor: HCursor): DecodeResult[Nothing] =
165 | // FIXME Sometimes reports the wrong error (in case of two nested sum types)
166 | DecodeResult.fail(s"unrecognized $name", cursor.history)
167 |
168 | def decodeField[A](name: String, cursor: HCursor, decode: DecodeJson[A]): DecodeResult[Either[ACursor, A]] =
169 | DecodeResult.ok {
170 | val o = decode(cursor)
171 | o.toOption
172 | .toRight(ACursor.ok(cursor))
173 | }
174 | }
175 |
176 | case class JsonProductObjCodecNoEmpty(
177 | toJsonName: String => String = identity
178 | ) extends JsonProductCodec {
179 |
180 | private val underlying = JsonProductCodec.adapt(toJsonName)
181 |
182 | val encodeEmpty: Json = underlying.encodeEmpty
183 |
184 | def encodeField(field: (String, Json), obj: Json, default: => Option[Json]): Json =
185 | underlying.encodeField(field, obj, default)
186 |
187 | def decodeEmpty(cursor: HCursor): DecodeResult[Unit] =
188 | if (cursor.focus == Json.obj())
189 | DecodeResult.ok(())
190 | else
191 | DecodeResult.fail(
192 | s"Found extra fields: ${cursor.fields.toSeq.flatten.mkString(", ")}",
193 | cursor.history
194 | )
195 |
196 | def decodeField[A](
197 | name: String,
198 | cursor: HCursor,
199 | decode: DecodeJson[A],
200 | default: Option[A]
201 | ): DecodeResult[(A, ACursor)] = {
202 | val c = cursor.downField(toJsonName(name))
203 |
204 | def result = c.as(decode).map((_, if (c.succeeded) c.delete else ACursor.ok(cursor)))
205 |
206 | default match {
207 | case None => result
208 | case Some(d) =>
209 | if (c.succeeded)
210 | result
211 | else
212 | DecodeResult.ok((d, ACursor.ok(cursor)))
213 | }
214 | }
215 | }
216 |
217 | object JsonProductObjCodecNoEmpty {
218 | val default = JsonProductObjCodecNoEmpty()
219 | }
220 |
221 | implicit val encodeHoverInfo: EncodeJson[HoverInfo] =
222 | EncodeJson.of[String].contramap(_.label)
223 | implicit val decodeHoverInfo: DecodeJson[HoverInfo] =
224 | DecodeJson { c =>
225 | DecodeJson.of[String].apply(c).flatMap {
226 | case "all" => DecodeResult.ok(HoverInfo.All)
227 | case "skip" => DecodeResult.ok(HoverInfo.Skip)
228 | case "none" => DecodeResult.ok(HoverInfo.None)
229 | case combination =>
230 | val results = combination.split('+').map {
231 | case "x" => Right(HoverInfo.X)
232 | case "y" => Right(HoverInfo.Y)
233 | case "z" => Right(HoverInfo.Z)
234 | case "color" => Right(HoverInfo.Color)
235 | case "text" => Right(HoverInfo.Text)
236 | case "name" => Right(HoverInfo.Name)
237 | case other => Left(s"Unrecognized hover info element: $other")
238 | }
239 | if (results.exists(_.isLeft))
240 | DecodeResult.fail(
241 | s"Unrecognized hover info elements: ${results.flatMap(_.left.toSeq).mkString(", ")}",
242 | c.history
243 | )
244 | else
245 | DecodeResult.ok(HoverInfo(results.flatMap(_.toSeq).toIndexedSeq: _*))
246 | }
247 | }
248 |
249 | implicit def defaultJsonProductCodecFor[T]: JsonProductCodecFor[T] =
250 | JsonProductCodecFor(JsonProductObjCodecNoEmpty.default)
251 |
252 | implicit val encodeRGBA: EncodeJson[Color.RGBA] =
253 | EncodeJson.of[String].contramap(c => s"rgba(${c.r}, ${c.g}, ${c.b}, ${c.alpha})")
254 |
255 | implicit val decodeRGBA: DecodeJson[Color.RGBA] =
256 | DecodeJson { c =>
257 | c.as[String].flatMap { s =>
258 | if (s.startsWith("rgba(") && s.endsWith(")"))
259 | s.stripPrefix("rgba(").stripSuffix(")").split(',').map(_.trim) match {
260 | case Array(rStr, gStr, bStr, alphaStr) =>
261 | val res = for {
262 | r <- Try(rStr.toInt).toOption
263 | g <- Try(gStr.toInt).toOption
264 | b <- Try(bStr.toInt).toOption
265 | alpha <- Try(alphaStr.toDouble).toOption
266 | } yield DecodeResult.ok(Color.RGBA(r, g, b, alpha))
267 |
268 | res.getOrElse {
269 | DecodeResult.fail(s"Unrecognized RGBA color: '$s'", c.history)
270 | }
271 | case _ =>
272 | DecodeResult.fail(s"Unrecognized RGBA color: '$s'", c.history)
273 | }
274 | else
275 | DecodeResult.fail(s"Unrecognized RGBA color: '$s'", c.history)
276 | }
277 | }
278 |
279 | implicit val encodeStringColor: EncodeJson[Color.StringColor] =
280 | EncodeJson.of[String].contramap(_.color)
281 |
282 | implicit val decodeStringColor: DecodeJson[Color.StringColor] =
283 | DecodeJson {
284 | val underlying = DecodeJson.of[String]
285 | val map = Color.StringColor.colors.toVector
286 | .map(c => c -> Color.StringColor(c))
287 | .toMap
288 |
289 | c =>
290 | underlying(c).flatMap { s =>
291 | map.get(s) match {
292 | case None => DecodeResult.fail(s"Unrecognized color: '$s'", c.history)
293 | case Some(m) => DecodeResult.ok(m)
294 | }
295 | }
296 | }
297 |
298 | private val HexaColor3 = "#([0-9a-fA-F]{3})".r
299 | private val HexaColor6 = "#([0-9a-fA-F]{6})".r
300 |
301 | implicit val encodeRGB: EncodeJson[Color.RGB] =
302 | EncodeJson.of[String].contramap(c => s"rgb(${c.r}, ${c.g}, ${c.b})")
303 |
304 | implicit val decodeRGB: DecodeJson[Color.RGB] =
305 | DecodeJson { c =>
306 | val asString: DecodeResult[Color.RGB] = c.as[String].flatMap { s =>
307 | if (s.startsWith("rgb(") && s.endsWith(")"))
308 | s.stripPrefix("rgb(").stripSuffix(")").split(',').map(_.trim).map(s => Try(s.toInt).toOption) match {
309 | case Array(Some(r), Some(g), Some(b)) =>
310 | DecodeResult.ok(Color.RGB(r, g, b))
311 | case _ =>
312 | DecodeResult.fail(s"Unrecognized RGB color: '$s'", c.history)
313 | }
314 | else
315 | DecodeResult.fail(s"Unrecognized RGB color: '$s'", c.history)
316 | }
317 | def asInt: DecodeResult[Color.RGB] = c.as[Int].flatMap {
318 | case r if r >= 0 && r < 256 =>
319 | DecodeResult.ok(Color.RGB(r, 0, 0))
320 | case _ =>
321 | DecodeResult.fail(s"Unrecognized RGB color: ${c.focus}", c.history)
322 | }
323 |
324 | def parseHex(s: String, from: Int, until: Int) =
325 | new BigInteger(s.substring(from, until), 16).intValue()
326 | def asHexa: DecodeResult[Color.RGB] = c.as[String].flatMap {
327 | case HexaColor3(hex) =>
328 | val r = parseHex(hex, 0, 1)
329 | val g = parseHex(hex, 1, 2)
330 | val b = parseHex(hex, 2, 3)
331 |
332 | DecodeResult.ok(Color.RGB(r, g, b))
333 |
334 | case HexaColor6(hex) =>
335 | val r = parseHex(hex, 0, 2)
336 | val g = parseHex(hex, 2, 4)
337 | val b = parseHex(hex, 4, 6)
338 |
339 | DecodeResult.ok(Color.RGB(r, g, b))
340 |
341 | case other =>
342 | DecodeResult.fail(s"Unrecognized RGB color: $other", c.history)
343 | }
344 |
345 | asString.toOption
346 | .orElse(asInt.toOption)
347 | .fold(asHexa)(DecodeResult.ok)
348 | }
349 |
350 | private def decodeNum(s: String) = {
351 |
352 | val intOpt = Try(s.toInt).toOption
353 |
354 | val fromDouble = Try(s.toDouble).toOption
355 | .map(_.toInt)
356 |
357 | def fromPct =
358 | if (s.endsWith("%"))
359 | Try(s.stripSuffix("%").trim.toDouble).toOption
360 | .map(v => (256 * v).toInt)
361 | else
362 | None
363 |
364 | intOpt
365 | .orElse(fromDouble)
366 | .orElse(fromPct)
367 | }
368 |
369 | implicit val encodeHSL: EncodeJson[Color.HSL] =
370 | EncodeJson.of[String].contramap(c => s"hsl(${c.h}, ${c.s}, ${c.l})")
371 |
372 | implicit val decodeHSL: DecodeJson[Color.HSL] =
373 | DecodeJson { c =>
374 | c.as[String].flatMap { s =>
375 | if (s.startsWith("hsl(") && s.endsWith(")"))
376 | s.stripPrefix("hsl(").stripSuffix(")").split(',').map(_.trim).map(decodeNum) match {
377 | case Array(Some(h), Some(s), Some(l)) =>
378 | DecodeResult.ok(Color.HSL(h, s, l))
379 | case _ =>
380 | DecodeResult.fail(s"Unrecognized HSL color: '$s'", c.history)
381 | }
382 | else
383 | DecodeResult.fail(s"Unrecognized HSL color: '$s'", c.history)
384 | }
385 | }
386 |
387 | implicit val encodeNamedColorScale: EncodeJson[ColorScale.NamedScale] =
388 | EncodeJson.of[String].contramap(_.name)
389 |
390 | implicit val decodeNamedColorScale: DecodeJson[ColorScale.NamedScale] =
391 | DecodeJson { c =>
392 | c.as[String].flatMap { s =>
393 | // TODO: Add colorscale name enum?
394 | DecodeResult.ok(ColorScale.NamedScale(s))
395 | }
396 | }
397 |
398 | implicit val encodeCustomColorScale: EncodeJson[ColorScale.CustomScale] =
399 | EncodeJson.of[Json].contramap(_.values.toList.asJson)
400 |
401 | implicit val decodeCustomColorScale: DecodeJson[ColorScale.CustomScale] =
402 | DecodeJson { c =>
403 | c.as[Seq[(Double, Color)]].flatMap { s =>
404 | DecodeResult.ok(ColorScale.CustomScale(s))
405 | }
406 | }
407 |
408 | implicit val colorscaleJsonCodec: JsonSumCodecFor[ColorScale] =
409 | JsonSumCodecFor(jsonSumDirectCodecFor("colorscale"))
410 |
411 | implicit val elementJsonCodec: JsonSumCodecFor[Element] =
412 | JsonSumCodecFor(jsonSumDirectCodecFor("element"))
413 |
414 | implicit val sequenceJsonCodec: JsonSumCodecFor[Sequence] =
415 | JsonSumCodecFor(jsonSumDirectCodecFor("sequence"))
416 |
417 | implicit val rangeJsonCodec: JsonSumCodecFor[Range] =
418 | JsonSumCodecFor(jsonSumDirectCodecFor("range"))
419 |
420 | implicit val boxPointsJsonCodec: JsonSumCodecFor[BoxPoints] =
421 | JsonSumCodecFor(jsonSumDirectCodecFor("box points"))
422 |
423 | implicit val boxMeanJsonCodec: JsonSumCodecFor[BoxMean] =
424 | JsonSumCodecFor(jsonSumDirectCodecFor("box mean"))
425 |
426 | implicit def oneOrSeqJsonCodec[T]: JsonSumCodecFor[OneOrSeq[T]] =
427 | JsonSumCodecFor(jsonSumDirectCodecFor("one or sequence"))
428 |
429 | implicit val encodeScatterMode: EncodeJson[ScatterMode] =
430 | flagEncoder[ScatterMode, ScatterMode.Flag](_.flags, _.label)
431 |
432 | implicit val decodeScatterMode: DecodeJson[ScatterMode] =
433 | flagDecoder[ScatterMode, ScatterMode.Flag]("scatter mode", ScatterMode.flagMap, ScatterMode(_))
434 |
435 | implicit val encodeLocalDateTime: EncodeJson[LocalDateTime] =
436 | EncodeJson { dt =>
437 | dt.toString.asJson
438 | }
439 |
440 | implicit val decodeLocalDateTime: DecodeJson[LocalDateTime] =
441 | DecodeJson { c =>
442 | c.as[String].flatMap { s =>
443 | LocalDateTime.parse(s) match {
444 | case Some(dt) =>
445 | DecodeResult.ok(dt)
446 | case None =>
447 | DecodeResult.fail(
448 | s"Malformed date-time: '$s'",
449 | c.history
450 | )
451 | }
452 | }
453 | }
454 |
455 | implicit val encodeError: EncodeJson[Error] =
456 | EncodeJson { error =>
457 | val json = error match {
458 | case data: Error.Data => data.asJson
459 | case pct: Error.Percent => pct.asJson
460 | case cst: Error.Constant => cst.asJson
461 | }
462 |
463 | json.obj.fold(json)(o => Json.jObject(("type" -> error.`type`.asJson) +: o))
464 | }
465 |
466 | implicit val decodeError: DecodeJson[Error] =
467 | DecodeJson { c =>
468 | c.downField("type").success match {
469 | case None =>
470 | DecodeResult.fail("No type found", c.history)
471 | case Some(c1) =>
472 | val c0 = c1.delete
473 | c1.focus.as[String].flatMap {
474 | case "data" =>
475 | c0.as[Error.Data].map(e => e: Error)
476 | case "percent" =>
477 | c0.as[Error.Percent].map(e => e: Error)
478 | case "constant" =>
479 | c0.as[Error.Constant].map(e => e: Error)
480 | case unrecognized =>
481 | DecodeResult.fail(s"Unrecognized type: $unrecognized", c.history)
482 | }
483 | }
484 | }
485 |
486 | implicit val jsonSumCodecForColor: JsonSumCodecFor[Color] =
487 | JsonSumCodecFor(jsonSumDirectCodecFor("color"))
488 |
489 | case class WrappedFont(font: Font)
490 | val derivedFontDecoder = MkDecodeJson[Font].decodeJson
491 | lazy val wrappedFontDecoder = DecodeJson.of[WrappedFont].map(_.font)
492 |
493 | implicit lazy val decodeFont: DecodeJson[Font] =
494 | DecodeJson { c =>
495 | wrappedFontDecoder(c).toOption.fold(derivedFontDecoder(c))(DecodeResult.ok)
496 | }
497 |
498 | implicit val jsonCodecForTrace = JsonSumCodecFor[Trace](
499 | new JsonSumTypeFieldCodec {
500 | override def toTypeValue(name: String) = name.toLowerCase
501 |
502 | override def decodeField[A](name: String, cursor: HCursor, decode: DecodeJson[A]) = {
503 | val c = cursor.downField(typeField)
504 |
505 | c.as[String].toEither match {
506 | case Right(name0) if toTypeValue(name) == name0 =>
507 | c.delete.as(decode).map(Right(_))
508 | case Left(_) if name == "Scatter" => // assume scatter if no type found
509 | cursor.as(decode).map(Right(_))
510 | case _ =>
511 | DecodeResult.ok(Left(ACursor.ok(cursor)))
512 | }
513 | }
514 | }
515 | )
516 | }
517 |
--------------------------------------------------------------------------------