Scala API to Plot Widgets

Overview

The Scala API for plot widgets maps the widgets' Java bean-style properties to Scala properties. It provides both type safety and a script-friendly syntax with the same techniques as ScalaFX.

For example, the operations getDisplayName and setDisplayName turn into a displayName getter and setter. In general, methods that expect a Java (or Groovy) List will accept a Scala Seq. Arrays should also work where a Seq is expected, although Array is not a subtype of Seq.

The Java/Groovy API frequently allows setting some property to be either a single value or a list of values. In those cases, there are typically two getters, one with a singular name (like the setter) and one with a plural name (e.g., getFill and getFills). The Scala API follows this pattern for consistency. If the single value might be undefined, the getter will be an Option type. If the list value is undefined, the API will map it to an empty Seq.

Usage

Groovy allows setting an arbitrary set of properties when constructing an object. To get similar terseness in creating Scala plot widgets, you can use the anonymous subclass initialization idiom. Instead of this:

val myLine = new Line()
myLine.x = 1 to 3
myLine.y = 5 to 15 by 5

you can use this:

val myLine = new Line {
  x = 1 to 3
  y = 5 to 15 by 5
}

This allows setting the properties without creating a named value for the new Line in many cases:

val plot = new Plot
plot.add(new Line { x = 1 to 3; y = (1 to 3) map (1.0 / _) })

Note that this syntax is not using named parameters. There is one important caveat about using this style: inside the body of the initializer, the names of the getters will shadow any identifiers in the outer scope. So, don't try this:

// THIS WON'T BEHAVE AS EXPECTED!
val x = List(1, 2, 3)
val myPoints = new Points {
  x = x
}

Instead of using the outer value of x, this will just invoke the getter for x.

Compatibility

In general, the Scala plot classes extend the corresponding Java classes. This means that the set<Property> methods are still available on the Scala objects. This may be useful if you need to deal with data that is not statically typed.


In [ ]:
val plot = new Plot()
val y1 = Seq(1.5, 1, 6, 5, 2, 8)
val cs = Seq(Color.black, Color.red, Color.gray, Color.green, Color.blue, Color.pink)
val ss = Seq(StrokeType.SOLID, StrokeType.SOLID, StrokeType.DASH, StrokeType.DOT, StrokeType.DASHDOT, StrokeType.LONGDASH)
plot.add(new Stems {
    y = y1
    color = cs
    style = ss
    width = 5
})

In [ ]:
val plot = new Plot { title = "Elo" }
var cs = new Color(255, 0, 0, 128)// transparent bars
//cs[3] = Color.red // set color of a single bar, solid colored bar
plot.add(new Bars {
    x = Seq(1, 2, 3, 4)
    y = Seq(3, 5, 2, 3, 7)
    color = cs
    outlineColor = Color.black
    width = 0.3
})

In [ ]:
val plot = new Plot { title = "Changing Point Size, Color, Shape" }
val y1 = Seq(6, 7, 12, 11, 8, 14)
val y2 = y1.map(x => x - 1)
val y3 = y1.map(x => x - 2)
val y4 = y1.map(x => x - 3)
plot.add(new Points { y = y1 })
plot.add(new Points {
    y = y2
    shape = ShapeType.CIRCLE
})
plot.add(new Points {
    y = y3
    size = 8.0
    shape = ShapeType.DIAMOND
})
plot.add(new Points {
    y = y4
    size = 12.0
    color = Color.orange
    outlineColor = Color.red
})

In [ ]:
val plot = new Plot { title = "Changing point properties with list" }
val cs = Seq(Color.black, Color.red, Color.orange, Color.green, Color.blue, Color.pink)
val ss = Seq(6.0, 9.0, 12.0, 15.0, 18.0, 21.0)
val fs = Seq(false, false, false, true, false, false)

val list = List(
    new Points {
        y = Seq(5,5,5,5,5,5)
        size = 12.0
        color = cs
    },
    new Points {
        y = Seq(4,4,4,4,4,4)
        size = 12.0
        color = Color.gray
        outlineColor = cs
    },
    new Points {
        y = Seq(3,3,3,3,3,3)
        size = 12
        color = Color.red
    },
    new Points {
        y = Seq(2,2,2,2,2,2)
        size = 12.0
        color = Color.black
        fill = fs
        outlineColor = Color.black
    }
)

plot.add(list)

In [ ]:
val plot = new Plot()
val ys = Seq(3, 5, 2, 3)
val x0 = Seq(0, 1, 2, 3)
val x1 = Seq(3, 4, 5, 8)
plot.add(new Area {
    x = x0
    y = ys
})
plot.add(new Area {
    x = x1
    y = ys
    color = new Color(128, 128, 128, 50)
    interpolation = 0
})

In [ ]:
val p = new Plot()
p.add(new Line {
    y = Seq(3, 6, 12, 24)
    displayName = "Median"
})
p.add(new Area {
    base = Seq(4, 8, 16, 32)
    y = Seq(2, 4, 8, 16)
    color = new Color(255, 0, 0, 50)
    displayName = "Q1 to Q3"
})

In [ ]:
val y1 = Seq(1,5,3,2,3)
val y2 = Seq(7,2,4,1,3)
val p = new Plot { title = "Plot with XYStacker"; initHeight = 200 }
val a1 = new Area { y = y1; displayName =  "y1" }
val a2 = new Area { y = y2; displayName = "y2" }
p.add(XYStacker.stack(Seq(a1, a2)))

In [ ]:
val p = new Plot()
p.add(new Line { y = Seq(-1, 1) })
p.add(new ConstantLine {
    x = 0
    y = 0.65
    style = StrokeType.DOT
    color = Color.blue
})
p.add(new ConstantLine {
    x = 0
    y = 1
    style = StrokeType.DASHDOT
    color = Color.blue
})
p.add(new ConstantLine {
    x = 1
    y = 0.4
    color = Color.gray
    width = 5
    showLabel = true
})

In [ ]:
val constBand = new ConstantBand { x = Seq(1, 2); y = Seq(1, 3) }
val lineVal = new Line { y = Seq(-3, 1, 3, 4, 5) }
val plot = new Plot()
plot.add(lineVal)
plot.add(constBand)

In [ ]:
val p = new Plot() 
p.add(new Line { x = Seq(-3, 1, 2, 4, 5); y = Seq(4, 2, 6, 1, 5) })
p.add(new ConstantBand { x = Seq(Double.NegativeInfinity, 1); color = new Color(128, 128, 128, 50) })
p.add(new ConstantBand { x = Seq(1, 2) })
p.add(new ConstantBand { x = Seq(4, Double.PositiveInfinity) })

In [ ]:
val plot = new Plot()
val xs = Seq(1,2,3,4,5,6,7,8,9,10)
val ys = Seq(8.6, 6.1, 7.4, 2.5, 0.4, 0.0, 0.5, 1.7, 8.4, 1)

plot.add(new Line {
    x = xs
    y = ys
})

def label(i: Int, ys: Seq[Double]): String = {
  val leftSign = Math.signum(ys(i) - ys(i - 1))
  val rightSign = Math.signum(ys(i + 1) - ys(i))
  (leftSign, rightSign) match {
    case (1, -1) => "max"
    case (-1, 1) => "min"
    case (1, _) => "rising"
    case (-1, _) => "falling"
    case _ => ""
  }
}

for (i <- 0 to xs.size) {
  if (i > 0 && i < xs.size - 1) {
    plot.add(new Text {
        x = xs(i)
        y = ys(i)
        text = label(i, ys)
        pointerAngle = -i/3.0
    })
  }
}

plot

In [ ]:
val ch = new Crosshair {
    color = new Color(255, 128, 5)
    width = 2
    style = StrokeType.DOT
}
val pp = new Plot {
    crosshair = ch
    omitCheckboxes = true
    legendLayout = LegendLayout.HORIZONTAL
    legendPosition = LegendPosition.TOP
}
def xs = Seq(1, 4, 6, 8, 10)
def ys = Seq(3, 6, 4, 5, 9)
pp.add(new Line {
    displayName = "Line"
    x = xs
    y = ys
    width = 3
})
pp.add(new Bars {
    displayName = "Bar"
    x = Seq(1,2,3,4,5,6,7,8,9,10)
    y = Seq(2, 2, 4, 4, 2, 2, 0, 2, 2, 4)
    width = 0.5
})
pp.add(new Points {
    x = xs
    y = ys
    size = 10
    // TODO: implement ToolTipBuilder
    toolTip = Seq("x = ","y = ")
})

In [ ]:
val rates = new CSV().readFile("../resources/data/interest-rates.csv")

new SimpleTimePlot {
    data = rates
    columns = Seq("y1", "y10")
    yLabel = "Price"
    displayNames = Seq("All", "1 Year", "10 Year")
}

In [ ]:
val points = 100
val logBase = 10

val xs = for (i <- 0 to points) yield i / 15.0
val expys = for (x <- xs) yield Math.exp(x)

val cplot = new CombinedPlot()
val logYPlot = new Plot {
    title = "Linear x, Log y"
    xLabel = "Log"
    logY=true
    yLogBase = logBase
}
logYPlot.add(new Line {
    displayName = "f(x) = exp(x)"
    x = xs
    y = expys
    width = 3.0f
})
logYPlot.add(new Line {
    displayName = "g(x) = x"
    x = xs
    y = xs
    width = 3.0f
})

cplot.add(logYPlot, 3)

val linearYPlot = new Plot {
    title = "Linear x, Linear y"
    xLabel = "Linear"
}
linearYPlot.add(new Line {
    displayName = "f(x) = exp(x)"
    x = xs
    y = expys
    width = 3.0f
})
linearYPlot.add(new Line {
    displayName = "g(x) = x"
    x = xs
    y = xs
    width = 3.0f
})
cplot.add(linearYPlot, 3)

cplot

In [ ]:
val points = 100
val logBase = 10
val xs = for (i <- 0 to points) yield i / 15.0
val expys = for (x <- xs) yield Math.exp(x)

val plot = new Plot { 
    title = "Log x, Log y"
    xLabel = "Log"
    yLabel = "Log"
    logX = true
    xLogBase = logBase
    logY = true
    yLogBase = logBase
}

plot.add(new Line {
    displayName = "f(x) = exp(x)"
    x = xs
    y = expys
    width = 3.0f
})
plot.add(new Line {
    displayName = "g(x) = x"
    x = xs
    y = xs
    width = 3.0f
})
plot

In [ ]:
import java.util.{Calendar, Date}
import java.util.SimpleTimeZone

val cal = Calendar.getInstance();
cal.add(Calendar.HOUR, -1)

val today = new Date()
val millis = today.getTime()
val hour = 1000 * 60 * 60;

val plot = new TimePlot {
    timeZone = new SimpleTimeZone(10800000, "America/New_York")
}

//list of milliseconds
plot.add(new Points {
  x = (0 to 10).map(x => millis + hour * x)
  y = (0 to 10)
  size = 10
  displayName = "milliseconds"
})

plot.add(new Points {
  x = (0 to 10).map(x => {cal.add(Calendar.HOUR, 1); cal.getTime()})
  y = (0 to 10)
  size = 5
  displayName = "date objects"
})

In [ ]:
import java.util.Date

val today  = new Date()
val millis = today.getTime()
val nanos  = millis * 1000 * 1000// g makes it arbitrary precision
val np = new NanoPlot()

np.add(new Points {
  x = (0 to 10).map(x => nanos + 7 * x)
  y = (0 to 10)
})

In [ ]:
import scala.util.Random

val r = new Random()
val p = new Plot {
    title = "Advanced Plot Styling"
    labelStyle = "font-size:32px; font-weight: bold; font-family: courier; fill: green;"
    gridLineStyle = "stroke: purple; stroke-width: 3;"
    titleStyle = "color: green;"
}

p.add(new Points {
    x = (1 to 1000).map(x => r.nextGaussian() * 10.0d)
    y = (1 to 1000).map(x => r.nextGaussian() * 20.0d)
})

In [ ]:
import java.nio.file.Files
import java.io.File

val picture: Array[Byte] = Files.readAllBytes(new File("../resources/img/widgetArch.png").toPath())

var p =  new Plot();
// x y width height are coordinates, opacity is a double in 0~1

// image can be loaded via bytes, filepath, or url
p.add(new Rasters {
    x = List(-10,3)
    y = List(3,1.5)
    height = List(6,5)
    width = List(10,8)
    opacity = List(1,0.5)
    dataString = picture
})

//p << new Rasters(x: -1, y: 4.5, width: 5, height: 8, opacity:0.5, filePath: "../resources/img/widgetArch.png");
p.add(new Rasters {
    x = List(-4)
    y = List(10.5)
    height = List(2)
    width = List(10)
    opacity = List(1)
    fileUrl = "https://www.twosigma.com/static/img/twosigma.png"
})

// a list of images!
def xs = List(-8, -5, -3, -2, -1, 1, 2, 4, 6, 8)
def ys = List(4, 5, 1, 2, 0 ,3, 6, 4, 5, 9)
def widths = List(1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
def opacities = List(0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1)
p.add(new Rasters {
    x = xs
    y = ys
    width = widths
    height = widths
    opacity = opacities
    fileUrl = "http://icons.iconarchive.com/icons/paomedia/small-n-flat/1024/sign-check-icon.png"
})

In [ ]:
val plot = new Plot { title = "Setting 2nd Axis bounds" }
val ys = List(0, 2, 4, 6, 15, 10)
val ys2 = List(-40, 50, 6, 4, 2, 0)
val ys3 = List(3, 6, 3, 6, 70, 6)
plot.add(new YAxis { label = "Spread" })
plot.add(new Line { y = ys })
plot.add(new Line { y = ys2; displayName = "Spread" })

plot.yAxes(1).bound = (1, 5)

plot

In [ ]: