Skip to content

Commit

Permalink
Don't use empty to calculate bounding box
Browse files Browse the repository at this point in the history
The `BoundingBox` from Doodle does not the semantics we want for
calculating the bounding box of data. `BoundingBox` must always contain
the origin, which is not the case for data. `BoundingBox.empty` is a
point at the origin. Using it means we include the origin when
calculating the bounding box of data, which is incorrect.

There must be a least one `Layer` in a `Plot`, so use a `NonEmptySeq` to
represent this.
  • Loading branch information
noelwelsh committed Aug 19, 2024
1 parent 52a7f08 commit cd22563
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 29 deletions.
19 changes: 18 additions & 1 deletion core/shared/src/main/scala/chartreuse/Data.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,24 @@ enum Data[+A] {
case FromTraverse[F[_], A](data: F[A], traverse: Traverse[F]) extends Data[A]

def boundingBox(toPoint: A => Point): BoundingBox =
foldLeft(BoundingBox.empty)((bb, a) => bb.enclose(toPoint(a)))
this match {
case FromIterable(data) =>
if data.isEmpty then BoundingBox.empty
else {
val pt = toPoint(data.head)
val bb = BoundingBox(pt.x, pt.y, pt.x, pt.y)
data.tail.foldLeft(bb) { (bb, a) => bb.enclose(toPoint(a)) }
}
case FromTraverse(data, traverse) =>
traverse.reduceLeftToOption(data) { (a: A) =>
val pt = toPoint(a)
BoundingBox(pt.x, pt.y, pt.x, pt.y)
}((bb, a) => bb.enclose(toPoint(a))) match {
case Some(value) => value
case None => BoundingBox.empty
}

}

def foldLeft[B](z: B)(f: (B, A) => B): B =
this match {
Expand Down
11 changes: 5 additions & 6 deletions core/shared/src/main/scala/chartreuse/Plot.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package chartreuse

import cats.Id
import cats.data.NonEmptySeq
import chartreuse.component.Axis.*
import chartreuse.component.*
import chartreuse.theme.PlotTheme
Expand All @@ -30,7 +31,7 @@ import Plot.PlotAlg
* grid.
*/
final case class Plot[-Alg <: Algebra](
layers: List[Layer[?, Alg]],
layers: NonEmptySeq[Layer[?, Alg]],
plotTitle: String = "Plot Title",
xTitle: String = "X data",
yTitle: String = "Y data",
Expand All @@ -43,7 +44,7 @@ final case class Plot[-Alg <: Algebra](
annotations: List[Annotation] = List.empty[Annotation]
) {
def addLayer[Alg2 <: Algebra](layer: Layer[?, Alg2]): Plot[Alg & Alg2] = {
copy(layers = layer :: layers)
copy(layers = layer +: layers)
}

def addAnnotation(annotation: Annotation): Plot[Alg] = {
Expand Down Expand Up @@ -89,9 +90,7 @@ final case class Plot[-Alg <: Algebra](
height: Int,
theme: PlotTheme[Id] = PlotTheme.default
): Picture[Alg & PlotAlg, Unit] = {
val dataBoundingBox = layers.foldLeft(BoundingBox.empty) { (bb, layer) =>
bb.on(layer.boundingBox)
}
val dataBoundingBox = layers.map(_.boundingBox).reduceLeft(_.on(_))

val dataMinX = dataBoundingBox.left
val dataMaxX = dataBoundingBox.right
Expand Down Expand Up @@ -216,5 +215,5 @@ object Plot {

/** Utility constructor to create a `Plot` from a single layer. */
def apply[Alg <: Algebra](layer: Layer[?, Alg]): Plot[Alg] =
Plot(layers = List(layer))
Plot(layers = NonEmptySeq.one(layer))
}
5 changes: 3 additions & 2 deletions core/shared/src/main/scala/chartreuse/component/Legend.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package chartreuse.component

import cats.Id
import cats.data.NonEmptySeq
import chartreuse.Layer
import chartreuse.Plot.PlotAlg
import chartreuse.theme.PlotTheme
Expand All @@ -25,15 +26,15 @@ import doodle.core.*
import doodle.syntax.all.*

final case class Legend[-Alg <: Algebra](
layers: Seq[Layer[?, Alg]],
layers: NonEmptySeq[Layer[?, Alg]],
theme: PlotTheme[Id]
) {
def build(x: Double, y: Double): Picture[Alg & PlotAlg, Unit] = {
val circleRadius = 8
val legendMargin = 6

val legendContent =
(layers
(layers.iterator
.zip(theme.layerThemesIterator))
.foldLeft(empty[Alg & PlotAlg])((content, layerAndTheme) => {
// This code is not ideal, because we're recreating the themed value here,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package chartreuse.examples

import cats.data.NonEmptySeq
import cats.effect.unsafe.implicits.global
import chartreuse.*
import chartreuse.layout.*
Expand Down Expand Up @@ -303,7 +304,7 @@ object BahamasPopulation {

val plot =
Plot(
List(
NonEmptySeq.of(
line.toLayer(population).withLabel("Line"),
curve.toLayer(population).withLabel("Curve")
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package chartreuse.examples

import cats.data.NonEmptySeq
import cats.effect.unsafe.implicits.global
import chartreuse.*
import chartreuse.layout.Scatter
Expand All @@ -41,7 +42,7 @@ object PlotExample {
}

val plot: Plot[Basic] = Plot(
List.fill(5)(randomLayer),
NonEmptySeq.fromSeqUnsafe(List.fill(5)(randomLayer)),
grid = true
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package chartreuse.examples

import cats.data.NonEmptySeq
import cats.effect.unsafe.implicits.global
import chartreuse.*
import chartreuse.layout.*
Expand All @@ -36,30 +37,33 @@ object TemperatureAnomaly {
data.groupBy(_.year).filter((year, _) => year < 2023)

val layers =
dataByYear
.map((year, records) =>
Line
.default[Record]
.forThemeable(themeable =>
// Highlight the most recent years by themeing them. Other years become grey
if year < 2013 then
themeable.withStrokeColor(Themeable.Override(Some(Color.grey)))
else themeable
)
.toLayer(records.sortBy(_.month))(record =>
Point(record.month, record.anomaly)
)
.withLabel(year.toString)
)
.toList
.sortBy(_.label)
NonEmptySeq.fromSeqUnsafe(
dataByYear
.map((year, records) =>
Line
.default[Record]
.forThemeable(themeable =>
// Highlight the most recent years by themeing them. Other years become grey
if year < 2013 then
themeable.withStrokeColor(Themeable.Override(Some(Color.grey)))
else themeable
)
.toLayer(records.sortBy(_.month))(record =>
Point(record.month, record.anomaly)
)
.withLabel(year.toString)
)
.toSeq
.sortBy(_.label)
)

val plot = Plot(layers.toList)
val plot = Plot(layers)
.withPlotTitle(
"Global Average Temperature Anomaly (2022-2013 Highlighted)"
)
.withYTitle("°C anomaly from 1961-1990")
.withXTitle("Month")
.withXTicks(MajorTickLayout.Manual(Seq(1,2,3,4,5,6,7,8,9,10,11,12)))
.withLegend(false)

@JSExport
Expand Down

0 comments on commit cd22563

Please sign in to comment.