Low-Level Drawing API

Drawing Primitives

In EvilPlot, we represent everything that can be drawn to the screen as a Drawable.

A Drawable is simply a description of the scene, and constructing one does not actually render anything. There are only a handful of drawing primitives that exist within EvilPlot, and they can be divided into three categories: drawing, positioning, and style. The drawing primitives are the “leaves” of a scene. They represent concrete things like shapes and text. They are:

Line
Path
Rect
BorderRect
Disc
Wedge
Polygon
Text

Positioning Primitives

Positioning primitives alter where other Drawables are placed. Notably, Drawable scenes do not expose a notion of absolute coordinates.

Translate
Rotate
Scale
Affine

Styling Primitives

Styling primitives allow us to modify the appearance of scenes.

  • Style colors the inside of a Drawable
  • StrokeStyle colors the outline of a Drawable
  • StrokeWeight changes the thickness of the outline of a Drawable
  • LineDash applies a line style to the outline of a Drawable

Composition

Creating scenes is simply a matter of composing these primitives. For example, to place a blue rectangle next to a red circle, you might write:

import com.cibo.evilplot.geometry._
import com.cibo.evilplot.colors._
val rect = Style(Rect(400, 400), HTMLNamedColors.red)
Group(
  Seq(
    Style(
      Translate(Disc(200), x = rect.extent.width),
      HTMLNamedColors.blue
    ),
    rect
  )
)

The geometry package also provides convenient syntax for dealing with positioning and styling primitives. So, instead of nesting constructors like we did up there, we can do the following with the same result.

import com.cibo.evilplot.geometry._
import com.cibo.evilplot.colors._

val rect = Rect(40, 40) filled HTMLNamedColors.red
Disc(20) transX rect.extent.width filled HTMLNamedColors.blue behind rect

The drawing API gives us the power to describe all of the scenes involved in the plots that EvilPlot can create; at no point do plot implementations reach below it and onto the rendering target.

Positional Combinators

EvilPlot gives a higher-level vocabulary to refer to common geometric manipulations. In fact, above we just wrote a positional combinator called beside. The geometry package is full of similar combinators, so most of the time you’ll never have to manually think about shifting objects by their widths like we did above.

beside
behind
above

Alignment

Additionally, you can align an entire sequence of Drawables by calling one of the Align methods, which produce a new sequence with all elements aligned. Combining alignment with reducing using a binary position operator is especially helpful:

import com.cibo.evilplot.geometry._
import com.cibo.evilplot.colors.HTMLNamedColors._
import com.cibo.evilplot.numeric.Point

val aligned: Seq[Drawable] = Align.right(
  Polygon(Seq(Point(0, 30), Point(15, 0), Point(30, 30))) filled red,
  Rect(50, 50) filled blue,
  Disc(15) filled red,
)

aligned.reduce(_ below _)

The available alignment functions are:1.

Align.bottom
Align.right
Align.middle
Align.center

An example

With EvilPlot’s drawing combinators on our side, we’re well equipped to recreate the box example from above that we made using only primitives–except it will be far easier to reason about. Recall, first we created our elements: two lines and two boxes, then centered them vertically and placed them on top of each other.

import com.cibo.evilplot.geometry._
import com.cibo.evilplot.colors.HTMLNamedColors.{blue, white}

// Some values from the outside:
val width = 100
val topWhisker = 60
val bottomWhisker = 40
val upperToMiddle = 50
val middleToLower = 45
val strokeWidth = 3

Align.center(
  Line(topWhisker, strokeWidth).rotated(90),
  BorderRect.filled(width, upperToMiddle),
  BorderRect.filled(width, middleToLower),
  Line(bottomWhisker, strokeWidth).rotated(90) 
).reduce(_ above _)
 .colored(blue)
 .filled(white)

And in fact, if you were to look at the source code for EvilPlot’s default box plot, it would be almost identical.

Drawing to the screen

Of course, at some point we want to draw our scenes to the screen. To do that, we call draw()

trait Drawable {
  // ...

  def draw(ctx: RenderContext): Unit
}

The RenderContext is the interface between our drawing API and platform-specific rendering targets. For more information on how to obtain a RenderContext, see the docs page.

  1. A .reduce(_ above _) or .reduce(_ beside _) was applied to each of these so the examples didn’t stomp all over each other.