diff --git a/core/shared/src/main/scala/plotly/Trace.scala b/core/shared/src/main/scala/plotly/Trace.scala index 95c9139..53a9de0 100755 --- a/core/shared/src/main/scala/plotly/Trace.scala +++ b/core/shared/src/main/scala/plotly/Trace.scala @@ -1,11 +1,10 @@ package plotly import scala.language.implicitConversions - -import java.lang.{ Boolean => JBoolean, Double => JDouble } - +import java.lang.{Boolean => JBoolean, Double => JDouble} import dataclass._ import plotly.element._ +import plotly.element.pie.{Direction, Domain, PieHoverInfo, PieTextPosition, Title} sealed abstract class Trace extends Product with Serializable @@ -179,6 +178,34 @@ object Box { hoverlabel: Option[HoverLabel] = None ) extends Trace +/** + * See + * https://plotly.com/javascript/reference/pie + * for a description of the params. + */ +@data(optionSetters = true) class Pie( + name: Option[String] = None, + title: Option[Title] = None, + showlegend: Option[Boolean] = None, + legendgroup: Option[String] = None, + opacity: Option[Double] = None, + ids: Option[Seq[String]] = None, + values: Option[Sequence] = None, + labels: Option[Sequence] = None, + pull: Option[OneOrSeq[Double]] = None, + text: Option[Seq[String]] = None, + textposition: Option[PieTextPosition] = None, + hovertext: Option[OneOrSeq[String]] = None, + hoverinfo: Option[PieHoverInfo] = None, + domain: Option[Domain] = None, + marker: Option[Marker] = None, + direction: Option[Direction] = None, + hole: Option[Double] = None, + hoverlabel: Option[HoverLabel] = None, + rotation: Option[Double] = None, + sort: Option[Boolean] = None, +) extends Trace + @data(optionSetters = true) class Bar( x: Sequence, y: Sequence, diff --git a/core/shared/src/main/scala/plotly/element/pie/Direction.scala b/core/shared/src/main/scala/plotly/element/pie/Direction.scala new file mode 100644 index 0000000..b4d6e90 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/pie/Direction.scala @@ -0,0 +1,8 @@ +package plotly.element.pie + +sealed abstract class Direction(val label: String) extends Product with Serializable + +object Direction { + case object Clockwise extends Direction("clockwise") + case object CounterClockwise extends Direction("counterclockwise") +} diff --git a/core/shared/src/main/scala/plotly/element/pie/Domain.scala b/core/shared/src/main/scala/plotly/element/pie/Domain.scala new file mode 100644 index 0000000..f0e5d13 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/pie/Domain.scala @@ -0,0 +1,11 @@ +package plotly.element.pie + +import dataclass.data +import plotly.Sequence + +@data(optionSetters = true) class Domain( + x: Option[Sequence] = None, + y: Option[Sequence] = None, + row: Option[Int] = None, + column: Option[Int] = None, +) \ No newline at end of file diff --git a/core/shared/src/main/scala/plotly/element/pie/PieHoverInfo.scala b/core/shared/src/main/scala/plotly/element/pie/PieHoverInfo.scala new file mode 100644 index 0000000..ce0c5be --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/pie/PieHoverInfo.scala @@ -0,0 +1,35 @@ +package plotly.element.pie + +sealed abstract class PieHoverInfo extends Product with Serializable { + def label: String +} + +object PieHoverInfo { + + def all: PieHoverInfo = All + def skip: PieHoverInfo = Skip + def none: PieHoverInfo = None + def apply(elements: Element*): PieHoverInfo = Combination(elements) + + sealed abstract class Element(override val label: String) extends PieHoverInfo + case object Text extends Element("text") + case object Name extends Element("name") + case object Percent extends Element("percent") + case object Value extends Element("value") + case object Label extends Element("label") + + + case object All extends PieHoverInfo { + def label = "all" + } + + case object Skip extends PieHoverInfo { + def label = "skip" + } + + val None: Combination = Combination(Nil) + + final case class Combination(elements: Seq[Element]) extends PieHoverInfo { + def label: String = elements.map(_.label).mkString("+") + } +} diff --git a/core/shared/src/main/scala/plotly/element/pie/PieTextPosition.scala b/core/shared/src/main/scala/plotly/element/pie/PieTextPosition.scala new file mode 100644 index 0000000..ffcffb8 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/pie/PieTextPosition.scala @@ -0,0 +1,10 @@ +package plotly.element.pie + +sealed abstract class PieTextPosition(val label: String) extends Product with Serializable + +object PieTextPosition { + case object Inside extends PieTextPosition("inside") + case object Outside extends PieTextPosition("outside") + case object Auto extends PieTextPosition("auto") + case object None extends PieTextPosition("none") +} diff --git a/core/shared/src/main/scala/plotly/element/pie/PieTitleFont.scala b/core/shared/src/main/scala/plotly/element/pie/PieTitleFont.scala new file mode 100644 index 0000000..4d46d12 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/pie/PieTitleFont.scala @@ -0,0 +1,10 @@ +package plotly.element.pie + +import dataclass.data +import plotly.element.{Color, OneOrSeq} + +@data(optionSetters = true) class PieTitleFont( + family: Option[OneOrSeq[String]] = None, + size: Option[OneOrSeq[Double]] = None, + color: Option[OneOrSeq[Color]] = None +) diff --git a/core/shared/src/main/scala/plotly/element/pie/PieTitlePosition.scala b/core/shared/src/main/scala/plotly/element/pie/PieTitlePosition.scala new file mode 100644 index 0000000..1756e07 --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/pie/PieTitlePosition.scala @@ -0,0 +1,13 @@ +package plotly.element.pie + +sealed abstract class PieTitlePosition(val label: String) extends Product with Serializable + +object PieTitlePosition { + case object TopLeft extends PieTitlePosition("top left") + case object TopCenter extends PieTitlePosition("top center") + case object TopRight extends PieTitlePosition("top right") + case object MiddleCenter extends PieTitlePosition("middle center") + case object BottomLeft extends PieTitlePosition("bottom left") + case object BottomCenter extends PieTitlePosition("bottom center") + case object BottomRight extends PieTitlePosition("bottom right") +} diff --git a/core/shared/src/main/scala/plotly/element/pie/Title.scala b/core/shared/src/main/scala/plotly/element/pie/Title.scala new file mode 100644 index 0000000..cf5671f --- /dev/null +++ b/core/shared/src/main/scala/plotly/element/pie/Title.scala @@ -0,0 +1,9 @@ +package plotly.element.pie + +import dataclass.data + +@data(optionSetters = true) class Title( + text: Option[String] = None, + font: Option[PieTitleFont] = None, + position: Option[PieTitlePosition] = None +) \ No newline at end of file diff --git a/render/shared/src/main/scala/plotly/internals/ArgonautCodecsInternals.scala b/render/shared/src/main/scala/plotly/internals/ArgonautCodecsInternals.scala index 6f1e270..aac6985 100755 --- a/render/shared/src/main/scala/plotly/internals/ArgonautCodecsInternals.scala +++ b/render/shared/src/main/scala/plotly/internals/ArgonautCodecsInternals.scala @@ -2,7 +2,6 @@ package plotly package internals import java.math.BigInteger - import argonaut._ import argonaut.Argonaut._ import argonaut.ArgonautShapeless._ @@ -11,6 +10,7 @@ import shapeless._ import scala.util.Try import plotly.element._ +import plotly.element.pie.{Direction, PieHoverInfo, PieTextPosition, PieTitlePosition} import plotly.layout._ object ArgonautCodecsInternals extends ArgonautCodecsExtra { @@ -151,6 +151,9 @@ object ArgonautCodecsInternals extends ArgonautCodecsExtra { implicit val rowOrderIsEnum = IsEnum.instance[RowOrder](_.label) implicit val alignmentIsEnum = IsEnum.instance[Alignment](_.label) implicit val colorModelIsEnum = IsEnum.instance[ColorModel](_.label) + implicit val directionIsEnum = IsEnum.instance[Direction](_.label) + implicit val pieTextPositionIsEnum = IsEnum.instance[PieTextPosition](_.label) + implicit val pieTitlePositionIsEnum = IsEnum.instance[PieTitlePosition](_.label) def jsonSumDirectCodecFor(name: String): JsonSumCodec = new JsonSumCodec { def encodeEmpty: Nothing = @@ -242,6 +245,32 @@ object ArgonautCodecsInternals extends ArgonautCodecsExtra { } } + implicit val encodePieHoverInfo: EncodeJson[PieHoverInfo] = + EncodeJson.of[String].contramap(_.label) + implicit val decodePieHoverInfo: DecodeJson[PieHoverInfo] = + DecodeJson { c => + DecodeJson.of[String].apply(c).flatMap { + case "all" => DecodeResult.ok(PieHoverInfo.All) + case "skip" => DecodeResult.ok(PieHoverInfo.Skip) + case "none" => DecodeResult.ok(PieHoverInfo.None) + case combination => + val results = combination.split('+').map { + case "percent" => Right(PieHoverInfo.Percent) + case "value" => Right(PieHoverInfo.Value) + case "label" => Right(PieHoverInfo.Label) + case "text" => Right(PieHoverInfo.Text) + case "name" => Right(PieHoverInfo.Name) + case other => Left(s"Unrecognized pie hover info element: $other") + } + if (results.exists(_.isLeft)) + DecodeResult.fail( + s"Unrecognized pie hover info elements: ${results.flatMap(_.left.toSeq).mkString(", ")}", + c.history + ) + else + DecodeResult.ok(PieHoverInfo(results.flatMap(_.toSeq).toIndexedSeq: _*)) + } + } implicit def defaultJsonProductCodecFor[T]: JsonProductCodecFor[T] = JsonProductCodecFor(JsonProductObjCodecNoEmpty.default) diff --git a/tests/src/test/scala/plotly/doc/DocumentationTests.scala b/tests/src/test/scala/plotly/doc/DocumentationTests.scala index b341bc0..e20b839 100755 --- a/tests/src/test/scala/plotly/doc/DocumentationTests.scala +++ b/tests/src/test/scala/plotly/doc/DocumentationTests.scala @@ -11,8 +11,9 @@ import org.mozilla.javascript._ import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import plotly.element.HoverInfo -import plotly.element.HoverInfo.{X,Y,Z} +import plotly.element.HoverInfo.{X, Y, Z} import plotly.element.ColorModel._ +import plotly.element.pie.Direction import scala.util.matching.Regex @@ -243,7 +244,7 @@ class DocumentationTests extends AnyFlatSpec with Matchers { "basic/line-plots", "basic/bar", "basic/horizontal-bar", - // TODO? Pie charts + "basic/pie", "financial/time-series", "financial/candlestick-charts", // "financial/ohlc", @@ -359,4 +360,61 @@ class DocumentationTests extends AnyFlatSpec with Matchers { } } + it should "demo Pie Trace" in { + val js = + """ + |var data = [ + | { + | type: "pie", + | name: "Best donuts", + | values: [30, 60, 40, 20, 50], + | labels: ["Vanilla", "Choco", "Strawberry", "Peanutbutter", "Cherry"], + | showlegend: true, + | opacity: 0.9, + | pull: 0.02, + | hole: 0.3, + | direction: "clockwise", + | sort: true, + | rotation: -50 + | } + |]; + | + |var layout = { + | width: 400, + | height: 400, + | title: "Tasty donut chart" + |}; + | + |Plotly.newPlot('myDonut', data, layout); + |""".stripMargin + + val (data, maybeLayout) = plotlyDemoElements(js) + + maybeLayout should === (Some( + new Layout() + .withWidth(400) + .withHeight(400) + .withTitle("Tasty donut chart") + )) + + data.headOption match { + case Some(image) => + val expected = Pie() + .withName("Best donuts") + .withValues(Seq(30, 60, 40, 20, 50)) + .withLabels(Seq("Vanilla", "Choco", "Strawberry", "Peanutbutter", "Cherry")) + .withShowlegend(true) + .withOpacity(0.9) + .withPull(0.02) + .withHole(0.3) + .withDirection(Direction.Clockwise) + .withSort(true) + .withRotation(-50) + + image should === (expected) + case None => + fail("data must contain an pie trace") + } + } + }