Skip to content
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

Fix handling of scales and add documentation #29

Merged
merged 10 commits into from
Sep 4, 2023
14 changes: 4 additions & 10 deletions core/shared/src/main/scala/chartreuse/Layer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,16 @@ import doodle.core.Point
final case class Layer[A, -Alg <: Algebra](
data: Data[A],
toPoint: A => Point,
scale: Scale,
layout: Layout[A, Alg],
label: String
) {
def draw(
width: Int,
height: Int,
scale: Bijection[Point, Point],
theme: LayoutTheme[Id]
): Picture[Alg, Unit] = {
val bb = data.boundingBox(toPoint)
val s = scale.build(bb, width, height)

layout.draw(data, toPoint, s, theme)
layout.draw(data, toPoint, scale, theme)
}

def boundingBox: BoundingBox =
Expand All @@ -58,9 +55,6 @@ final case class Layer[A, -Alg <: Algebra](
def withLayout[AAlg <: Algebra](layout: Layout[A, AAlg]): Layer[A, AAlg] =
this.copy(layout = layout)

def withScale(scale: Scale): Layer[A, Alg] =
this.copy(scale = scale)

def withToPoint(toPoint: A => Point): Layer[A, Alg] =
this.copy(toPoint = toPoint)

Expand All @@ -72,10 +66,10 @@ object Layer {
data: Data[A],
label: String = "Layer Label"
)(toPoint: A => Point): Layer[A, Shape] =
Layer(data, toPoint, Scale.linear, Layout.empty, label)
Layer(data, toPoint, Layout.empty, label)

def apply[A, Alg <: Algebra](data: Data[A], layout: Layout[A, Alg])(
toPoint: A => Point
): Layer[A, Alg] =
Layer(data, toPoint, Scale.linear, layout, "Layer Label")
Layer(data, toPoint, layout, "Layer Label")
}
1 change: 1 addition & 0 deletions core/shared/src/main/scala/chartreuse/Layout.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import doodle.algebra.Picture
import doodle.algebra.Shape
import doodle.core.Point

/** A `Layout` determines the visual appearance of data. */
trait Layout[A, -Alg <: Algebra] {

/** This should be the type of the concrete subclass that extends `Layout`. We
Expand Down
2 changes: 1 addition & 1 deletion core/shared/src/main/scala/chartreuse/Plot.scala
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ final case class Plot[-Alg <: Algebra](
val allLayers: Picture[Alg & PlotAlg, Unit] =
layers
.zip(theme.layerThemesIterator)
.map((layer, theme) => layer.draw(width, height, theme))
.map((layer, theme) => layer.draw(width, height, scale, theme))
.foldLeft(empty)(_ on _)

val allAnnotations: Picture[Alg & PlotAlg, Unit] =
Expand Down
19 changes: 12 additions & 7 deletions core/shared/src/main/scala/chartreuse/layout/Glyph.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,17 @@ trait Glyph[-A, Alg <: Algebra] {
def draw(data: A, theme: LayoutTheme[Id]): Picture[Alg, Unit]
}
object Glyph {
val circle: Glyph[Double, Shape & Style] =
new Glyph {
def draw(
data: Double,
theme: LayoutTheme[Id]
): Picture[Shape & Style, Unit] =
theme(doodle.syntax.shape.circle(data))

/** Create a `Glyph` from a function that produces a `Picture` given some
* input. The function does not need to do any themeing of its output.
*/
def apply[A, Alg <: Algebra](f: A => Picture[Alg, Unit]) =
new Glyph[A, Alg & Style] {
def draw(data: A, theme: LayoutTheme[Id]): Picture[Alg & Style, Unit] =
theme(f(data))
}

/** Create a `Glyph` that draws a circle of the given size. */
val circle: Glyph[Double, Shape & Style] =
apply((size: Double) => doodle.syntax.shape.circle(size))
}
13 changes: 13 additions & 0 deletions core/shared/src/main/scala/chartreuse/layout/Scatter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ import doodle.algebra.Picture
import doodle.core.Point
import doodle.syntax.all.*

/** A [[chartreuse.Layout]] that represents each data point as a small graphical
* mark, known as a [[chartreuse.layout.Glyph]].
*
* It is typically used to create scatter plots, but because the `Glyph` is
* parameterized by a `Double` value, which is interpreted as some measure of
* size, it can also be used to create bubble plots.
*/
final case class Scatter[
A,
Alg <: doodle.algebra.Algebra
Expand All @@ -38,6 +45,12 @@ final case class Scatter[
): Scatter[A, Alg] =
this.copy(themeable = f(themeable))

/** Change the function that determines the size of a data point to the given
* function.
*/
def withToSize(toSize: A => Double): Scatter[A, Alg] =
this.copy(toSize = toSize)

def draw(
data: Data[A],
toPoint: A => Point,
Expand Down
22 changes: 17 additions & 5 deletions docs/src/pages/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
# Chartreuse

Compositional data visualization for a functional world.
Chartreuse is a [Scala][scala] library for creating visualizations like the one below. Chartreuse builds on [Doodle][doodle], and provides a functional and compositional interface to quickly assembly visualizations from small reusable pieces.

@:doodle("temperature", "TemperatureAnomaly.draw")

## Related Work

- [ggplot2](https://ggplot2.tidyverse.org/)
- [AlgebraOfGraphics](https://aog.makie.org/stable/)
- [Matplotlib anatomy of a figure](https://matplotlib.org/stable/gallery/showcase/anatomy.html)
## Getting Started

To use Chartreuse in your project, add the following to your `build.sbt`:

```scala
libraryDependencies += "org.creativescala" %% "chartreuse" % "@VERSION@"
```


## API Documentation

See the ScalaDoc @:api(chartreuse.index) for API documentation.

[scala]: https://scala-lang.org/
[doodle]: https://creativescala.org/doodle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Visualizations in Chartreuse are built from small components. The most important

So creating a plot consists of creating each of the above components and combining them together. There are some shortcuts that simplify common cases.

Here's an example, taken from the [Quick Start](quick-start.md). We start with some data, which is always needed for a visualization.
Here's an example, taken from the [Quick Start](../quick-start.md). We start with some data, which is always needed for a visualization.

```scala mdoc:silent
import doodle.core.Point
Expand Down Expand Up @@ -74,7 +74,7 @@ layout

Builder methods *always* return a modified copy of the object they're called on,
so it's always safe to call a builder method even if you used a component in another place.
This is a core part of Chartreuse's design philosophy, as described in [Core Concepts](concepts.md)
This is a core part of Chartreuse's design philosophy, as described in [Core Concepts](../concepts.md)

In the example above we used `toLayer` and `toPlot` to convert types. These are convenience methods.
You can, for example, construct a `Plot` by calling its constructor but it's much simpler to type `.` and follow the auto-complete to turn a `Layer` into a `Plot`.
Expand Down
91 changes: 91 additions & 0 deletions docs/src/pages/creating/annotations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Annotations

Annotations allow you to highlight particular parts of the data.
There are two concepts in Chartreuse's annotation system:

- @:api(chartreuse.component.AnnotationType), which defines various types of annotations, each with its specific characteristics; and
- @:api(chartreuse.component.AnnotationPosition), which represents a specific positioning for annotations along with an arrow angle and placement logic.


## Types of annotations

Annotation types are found within the `chartreuse.component` package.
Currently, there are four different types of annotations:

- @:api(chartreuse.component.AnnotationType.Circle), a circle with user-specified radius;
- @:api(chartreuse.component.AnnotationType.CircleWithText), text along with a circle with user-specified radius;
- @:api(chartreuse.component.AnnotationType.Text), plain text; and
- @:api(chartreuse.component.AnnotationType.TextWithBox), text in a box.

An example of creating an `AnnotationType`:

```scala
val annotationType = AnnotationType.CircleWithText(15, "Something interesting happened here")
```


## Positioning the annotations

Each annotation in Chartreuse uses `AnnotationPosition` for positioning.

`AnnotationPosition` uses @:api(doodle.core.Landmark), @:api(doodle.core.Angle) and (@:api(doodle.core.Point), Double) => @:api(doodle.core.Point) to place an annotation:

- The `Landmark` is used as the reference point for annotation placement;
- The `Angle` is used to properly rotate the arrow indicating the annotation (More on the arrows in the following block); and
- The `(Point, Double) => Point` is a function that takes a base point and a margin offset, and returns the final annotation point. For example, `(pt, offset) => Point(pt.x - offset, pt.y + offset)` will place the annotation diagonally above and to the left of the point of interest.

Most users will use predefined annotation positioning options for ease of use (e.g. `AnnotationPosition.topLeft`).
But for more precise positioning it is possible to define all the parameters manually.


## Creating annotations

To create an annotation, it's enough to use the `apply` method of `Annotation`, which takes the point of interest and the `AnnotationType`:

```scala
Annotation(
Point(1950, 81651),
AnnotationType.Text("Rapid growth began here")
)
```

Annotations can be added to a plot with the `addAnnotation` method:

```scala
val annotatedPlot = plot
.addAnnotation(annotation)
```

By default, each annotation is placed in the center of the point of interest:

@:doodle(annotation-default, Annotations.drawDefault)

But the position can be adjusted using the `withAnnotationPosition` method:

```scala
Annotation(
Point(1950, 81651),
AnnotationType.Text("Rapid growth began here")
)
.withAnnotationPosition(AnnotationPosition.bottomRight)
```

Which will produce

@:doodle(annotation-with-positioning, Annotations.drawWithPositioning)

In addition, it's possible to draw an arrow between the text and the point of interest. `withArrow` method is used for this.
Arrows are placed automatically:

```scala
Annotation(
Point(1950, 81651),
AnnotationType.Text("Rapid growth began here")
)
.withAnnotationPosition(AnnotationPosition.bottomRight)
.withArrow()
```

Which will produce

@:doodle(annotation-with-positioning-and-arrow, Annotations.drawWithPositioningAndArrow)
6 changes: 6 additions & 0 deletions docs/src/pages/creating/directory.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
laika.navigationOrder = [
README.md
layouts.md
annotations.md
themes.md
]
26 changes: 26 additions & 0 deletions docs/src/pages/creating/layouts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Layouts

A @:api(chartreuse.Layout) determines how data is mapped to graphical output.
For example, a @:api(chartreuse.layout.Scatter) represents each data point as a small graphical mark known as a @:api(chartreuse.layout.Glyph), and is used to create [scatter plots][scatter-plot] and [bubble plots][bubble-plot].

## Line

The @:api(chartreuse.layout.Line) layout draws a straight line between each data point. The data is drawn in the order it is received, so you should probably ensure the data is sorted before it is drawn.


## Curve

The @:api(chartreuse.layout.Curve) layout interpolates a smooth curve that passes through each data point. Like @:api(chartreuse.layout.Line), the data is drawn in the order it is received, so you should probably ensure the data is sorted before it is drawn.

The curve is produced using the [Catmul-Rom method][catmul-rom], which is controlled by a `tension` parameter that determines how closely the interpolated curve follows the data.


## Scatter

The @:api(chartreuse.layout.Scatter) layout draws each data point as a small graphical mark known as a @:api(chartreuse.layout.Glyph).

The @:api(chartreuse.layout.Glyph) accepts a `Double` value, which is interpreted as the size of the data point being drawn. This allows the `Glyph` to vary in relation to the size, for example by creating a larger output when this value is larger. This `Double` value is in turn determined by the `toSize` parameter of `Scatter`, which can be changed in the usual way using the `withToSize` builder method. The default `toSize` ignores it's input and returns `5.0`.

[scatter-plot]: https://en.wikipedia.org/wiki/Scatter_plot
[bubble-plot]: https://en.wikipedia.org/wiki/Bubble_chart
[catmul-rom]: https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull%E2%80%93Rom_spline
File renamed without changes.
3 changes: 1 addition & 2 deletions docs/src/pages/directory.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ laika.navigationOrder = [
README.md
quick-start.md
concepts.md
creating-visualizations.md
themes.md
creating
examples.md
development.md
]
2 changes: 1 addition & 1 deletion docs/src/pages/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@

## Plot with an annotation

@:doodle("annotations", "Annotations.draw")
@:doodle("annotations", "Annotations.drawWithPositioningAndArrow")
Loading