├── 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 |
28 |
29 |
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 | [![Build Status](https://travis-ci.org/alexarchambault/plotly-scala.svg?branch=master)](https://travis-ci.org/alexarchambault/plotly-scala) 6 | [![Join the chat at https://gitter.im/alexarchambault/plotly-scala](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/alexarchambault/plotly-scala?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | [![Maven Central](https://img.shields.io/maven-central/v/org.plotly-scala/plotly-render_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/org.plotly-scala/plotly-render_2.13) 8 | [![ScalaDoc](http://javadoc-badge.appspot.com/org.plotly-scala/plotly-render_2.13.svg?label=scaladoc)](http://javadoc-badge.appspot.com/org.plotly-scala/plotly-render_2.13) 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: [![Maven Central](https://img.shields.io/maven-central/v/org.plotly-scala/plotly-render_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/org.plotly-scala/plotly-render_2.13)) 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: [![Maven Central](https://img.shields.io/maven-central/v/org.plotly-scala/plotly-render_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/org.plotly-scala/plotly-render_2.13)) 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 | --------------------------------------------------------------------------------