Skip to content

Adding Piechart support #291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions core/shared/src/main/scala/plotly/Trace.scala
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -179,6 +178,34 @@ object Box {
hoverlabel: Option[HoverLabel] = None
) extends Trace

/**
* See <a href="https://plotly.com/javascript/reference/pie">
* https://plotly.com/javascript/reference/pie
* </a> 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,
Expand Down
8 changes: 8 additions & 0 deletions core/shared/src/main/scala/plotly/element/pie/Direction.scala
Original file line number Diff line number Diff line change
@@ -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")
}
11 changes: 11 additions & 0 deletions core/shared/src/main/scala/plotly/element/pie/Domain.scala
Original file line number Diff line number Diff line change
@@ -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,
)
35 changes: 35 additions & 0 deletions core/shared/src/main/scala/plotly/element/pie/PieHoverInfo.scala
Original file line number Diff line number Diff line change
@@ -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("+")
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
10 changes: 10 additions & 0 deletions core/shared/src/main/scala/plotly/element/pie/PieTitleFont.scala
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
@@ -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")
}
9 changes: 9 additions & 0 deletions core/shared/src/main/scala/plotly/element/pie/Title.scala
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package plotly
package internals

import java.math.BigInteger

import argonaut._
import argonaut.Argonaut._
import argonaut.ArgonautShapeless._
Expand All @@ -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 {
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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)
Expand Down
62 changes: 60 additions & 2 deletions tests/src/test/scala/plotly/doc/DocumentationTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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")
}
}

}