1. Setup

To use Picta in a Jupyter notebook with the Almond.sh kernel, you need to first register a new Jitpack repository as some essential components of Almond are not availble in Maven Central (simply copy and run the first cell below). You then need to import the library from Maven (second cell) and initialize the notebook to have nicer outputs (optional, third cell).

In [1]:
interp.repositories() ++= Seq(coursierapi.MavenRepository.of(
"https://jitpack.io"
))
In [2]:
import $ivy. `org.carbonateresearch::picta:0.1.1`
Out[2]:
import $ivy.$                                    
In [3]:
import org.carbonateresearch.picta.render.Html.initNotebook // required to initialize jupyter notebook mode
initNotebook() // stops ugly output
Out[3]:
import org.carbonateresearch.picta.render.Html.initNotebook // required to initialize jupyter notebook mode

1. Basics

The aim of the Picta library is to be a highly configurable and composable charting library for data exploration. The library takes a grammatical approach to chart construction, using a DSL for creating plots.

The following examples are aimed at demonstrating the libraries capabilities, and eventually make constructing charts using this library easy and intuitive.

Main Components

Picta graphs are constructed in a modular manner. The main components are:

  1. Canvas: This is is the top-level component for representing the display. A Canvas may consist of multiple charts.
  1. Chart: This is the component that corresponds to an actual chart (i.e. scatter, pie etc).
  1. Series: This is the data series that is plotted on a Chart. Series come in 3 types:

    • XY: This is series data for constructing a 2D chart
    • XYZ: This is series data for constructing a 3D chart
    • Map: This is series data that constructs a Map chart
  1. ChartLayout: This configures various parts of how the Chart is represented on screen.

Chart may occassionally also make use of the Config component for further configuration. Config specifies whether the plot is responsive.

The following examples in this notebook should provide a number of interactive examples to get used to the Picta library.

Importing the Library

Importing as below should bring in the most frequently used Components in the library.

In [4]:
import org.carbonateresearch.picta._

Create Some Dummy Data to Use in the Examples

Here we create some dummy data to be used in the examples for this notebook.

In [5]:
val x = List.range(0, 100).map(x => scala.util.Random.nextDouble() * 50)
val y = List.range(0, 100).map(x => scala.util.Random.nextDouble() * 50)
val z = List.range(0, 100).map(x => scala.util.Random.nextDouble() * 50)

2D Plots

Series that represent 2D data are represented by XY series types.

Scatter Plot

An example Scatter plot is below.

Note: This is just one way to call the library functions. Another way that uses function chaining will be shown immediately after this example.

In [6]:
// create the data series using the data from above
// 'asType SCATTER' transforms the series into a scatter chart
// 'drawStyle Markers' specifies the style of the markers the chart should have
// if a name is not provided to the series, Picta generates a random name to help keep track of the series in charts
// multiple series.
val series = XY(x, y) asType SCATTER drawStyle MARKERS

// create a chart by adding to it a data series. We set the title as 'First Chart'
val chart = Chart() addSeries series setTitle "My First Chart"

// if we have a single chart, then we do not need to use the Canvas and can simply call chart.plot
chart.plotInline

Simple costumization

This plot is nice, but could use some simple costumization to make it more attractive. First, the data series does not have a name and so the legend come with a generated arbitrary name (not very useful). Second, the X and Y axis don't have labels. Let's fix that.

In [7]:
val series = XY(x, y) asType SCATTER drawStyle MARKERS setName("x vs y") // Using 'setName' gives a name to our series 

// Note the use of the setXAxisTitle and setYAxisTitle compared to above
val chart = Chart() addSeries series setTitle "Chart with custom axis and label" setXAxisTitle("X values") setYAxisTitle("Y values")

chart.plotInline

Chaining methods using the '.dot' notation

Sometimes when we are specifying many options, it is quicker to use the IDE autocomplete and just chain the method calls using the familiar '.dot' notation. Throughout this book both the style above and the method chaining style will be used as and when convenient.

In [8]:
// sometimes we may specify many options. In such cases it is clearer to use function chaining as below:
val series = XY(x, y)
            .asType(SCATTER)
            .setName("Chained xy")
            .drawMarkers  // This is to help leverage the IDE autocompletion for quick scripting. Tap 'tab' twice to see options.

val chart = Chart()
            .addSeries(series)
            .setTitle("Using Method Chaining")
            // by specifying and X and Y axes we can give more rid of the zerolines
            .addAxes(Axis(X ,zeroline = false), Axis(Y ,zeroline = false))
            .setXAxisTitle("X values")
            .setYAxisTitle("Y values")

chart.plotInline

Bar Chart

Now using the same principles as above, let's create a Bar chart:

In [9]:
// First we create some data for the piechart
val x_bar = List("a", "b", "c")
val y_bar = List(10, 20, 300)

// again feed in the data that we want to represent as a Bar chart.
// Notice how we change 'asType SCATTER' to 'asType BAR'
val series = XY(x_bar, y_bar) asType BAR setName("My data")

// we can turn off the scrollzoom using setConfig
val chart = Chart() addSeries series setTitle "Bar Chart" setConfig(false, false) setXAxisTitle "Category" setYAxisTitle "Count"

chart.plotInline

Adding Multiple Series on the Same Axis

Sometimes we may want to plot multiple Series on the same axis to compare different data series.

Let's plot both of the above on the same pair of axis:

In [10]:
// we import the marker option which lets us specify the marker
import org.carbonateresearch.picta.options.Marker
import org.carbonateresearch.picta.SymbolShape._
In [11]:
// additional traces can simply be composed with an existing chart and added on
val series1 = XY(x, y) asType BAR setName "Bar"

// lets give the second series a red marker. Again we can 'compose' a marker using smaller components
val marker = Marker() setSymbol SQUARE_OPEN setColor "red"

val series2 = XY(x, y) asType SCATTER setName "Scatter" drawStyle MARKERS setMarker marker

// we not put brackets in the 'addSeries' function to ensure that addSeries picks up the right series'
val chart = Chart() addSeries(series1, series2) setTitle("Multiple series on one chart") setXAxisTitle "X values" setYAxisTitle "Y values"

chart.plotInline

Lets Change The Appearance of The Markers

We can control the appearance of the markers by setting the width

In [12]:
// we can pass in the size of the markers
val sizes = List.range(0, 100).map(x => 10)

// additional traces can simply be composed with an existing chart and added on
val series1 = XY(x, y) asType BAR setName "Bar"

// lets give the second series a red marker. Again we can 'compose' a marker using smaller components
val marker = (
    Marker() 
    // sets the marker shape
    setSymbol CIRCLE
    // sets the fill to red
    setColor "red" 
    // sets the outline to black, with width 2
    setLine(width = 2, "black")
    // set the size of the markers to 10 as per the list we created
    setSize(sizes)
) 

val series2 = XY(x, y) asType SCATTER setName "Scatter" drawStyle MARKERS setMarker marker

// we not put brackets in the 'addSeries' function to ensure that addSeries picks up the right series'
val chart = Chart() addSeries(series1, series2) setConfig(false, false) setXAxisTitle "X values" setYAxisTitle "Y values"

chart.plotInline

Adding Another Axes

Sometimes we may wish to plot multiple series on one chart, but using two different Axes. This may be because the values of the two series are very different, yet we want to compare them on the same plot. We can do this as follows:

In [13]:
val series1 = XY(x, y) asType SCATTER drawStyle MARKERS

// The following maps the series onto the second Y axis.
val series2 = (
    XY(x, z) 
    asType SCATTER 
    drawStyle MARKERS 
    setAxis Axis(Y, 2)
    setName("Series 2 using Y axis 2")
)

val series3 = series1.copy() setName("Series 3 using Y axis 1")
      
val chart = (
    Chart() 
    setTitle "Using Multiple Axes"
    // the following makes the chart unresponsive
    setConfig(false, false) 
    addSeries(series3, series2) 
    // the following tells the chart how to render the second Y Axis
    addAxes Axis(Y, position = 2, title = "Second y axis", overlaying = Axis(Y), side = RIGHT_SIDE)
    addAxes Axis(Y, 1, "First Y Axis")
    addAxes Axis(X, title = "X Axis")
) 

// this is just for illustration purposes, but we can also do the following
val canvas = Canvas() setChart(0, 0, chart)

canvas.plotInline

Customizing the Axes

As can be seen from some of the examples above, if you do a simple chart you can use the standard axis (X and Y) and costumise them in the chart object. But for more complex layout, we may would want to create new axis and name them.

We can do this as follows:

In [14]:
// We construct the axes and set their title
val xaxis = Axis(X, title = "x variable")
val yaxis = Axis(Y, title = "y variable")

// another way to do composition is to just add a bracket around the composition
val chart = (
    Chart()
    setTitle "Chart with Axes"
    addSeries(series1 setName("Data"))
    addAxes(xaxis, yaxis)
)

chart.plotInline

Quickly Configuring The Axes

Picta comes with a range of utility functions to make configuring the primary axes quick and easy

In [15]:
// Let's create 100 randome data points
val x = List.range(0, 100).map(x => x * scala.util.Random.nextDouble())
val y = List.range(0, 100).map(x => x * scala.util.Random.nextDouble())

Adjusting Axes Limits

In [16]:
val series = (
    XY(x, y)
    asType SCATTER
    drawStyle MARKERS
)

val chart = (
      Chart()
    addSeries series.setName("Data for Axis Limit example")
    setTitle "Axis.SetAxisRange"
    addAxes yaxis
    setXAxisLimits(-100, 100) // Here we set the axis limit from -100 to 100
    setYAxisLimits(100, -100) // Here, by using the minimum as 100 and the maximum as -100, we automatically reverse the axis
    setXAxisTitle("x axis")
    setYAxisTitle("y axis")
)

chart.plotInline

Adjusting Axis Ticks

In [17]:
val series = (
    XY(x, y)
    asType SCATTER
    drawStyle MARKERS
)

val chart = (
    Chart()
    addSeries series
    setTitle "Set Axis Ticks"
    addAxes yaxis
    setXAxisLimits(0, 100)
    setYAxisLimits(-100, 100)
    setXAxisTitle("my new x axis")
    setXAxisStartTick 5
    setXAxisTickGap 15
)

chart.plotInline

Setting Logarithmic Axes

In [18]:
val series = (
    XY(List(1, 2, 3), List(1.234, 5.2112, 2.44332))
    asType SCATTER
    drawStyle MARKERS
)

val chart = (
    Chart()
    addSeries series
    setTitle "Logarithmic Axes"
    addAxes(xaxis, yaxis)
    drawXAxisLog true // We set the 'drawXAxisLog' value to 'true' to draw it on a logarithmic scale
    drawYAxisLog true
)

chart.plotInline

Reversing axis

We can also reverse the axis by setting the appropriate 'drawXAxisReversed'

In [19]:
val series = (
    XY(x, y)
    asType SCATTER
    drawStyle MARKERS
)

val chart = (
    Chart()
    addSeries series
    setTitle "Reversed Axes"
    setXAxisTitle("x reversed")
    setYAxisTitle("y reversed")
    drawXAxisReversed true
    drawYAxisReversed true
)
chart.plotInline

Display options - MultiCharts

MultiCharts are a way to plot multiple axes on the same chart. This is seperate to Picta's grid system, which create a grid of subplot Chart's. The next series of examples will make this clearer.

In [20]:
// create the axes
val xaxis1 = Axis(X) setTitle "x1"
val yaxis1 = Axis(Y) setTitle "y1" // this is not strictly necessary, but if it is not added there will be a zeroline at y = 0

val xaxis2 = Axis(X, 2) setTitle "x2"
val yaxis2 = Axis(Y, 2) // this is not strictly necessary, but if it is not added there will be a zeroline at y = 0

val series1 = (
    XY(x = List(1, 2, 3), y = List(2, 4, 5))
    asType SCATTER
    drawStyle MARKERS
    setName("First series") //Here we don't specify the axis, so the standard axis will be used
)

val series2 = (
    XY(x = x, y = y) 
    asType SCATTER
    drawStyle MARKERS
    setName("Second series")
    setAxes (xaxis2, yaxis2) //Here we specify the axis for this plot, which are the second axis
)

val chart1 = Chart()
            .addSeries(series1, series2)
            .setTitle("Chart with Axis Composition")
            .addAxes(xaxis1, yaxis1, xaxis2, yaxis2)
            .setConfig(responsive=false)
            // This tells Picta that we want to actually subdivide this chart into multiple plots
            .asMultiChart(1, 2) //These correspond to the axis values given above

chart1.plotInline

Picta's Subplot System

While the above is useful, sometimes it is easier to plot independent charts in a subplot grid. This is where we can use the Picta subplot system.

In the subplots below, each subplot is an individual Chart. This means we can actually embed the above MultiChart inside another subplot. This way we can create nested subplots to showcase any data we need to.

Used creatively, this subplot system can be very useful for data exploration.

In [21]:
// first define the x-axes we will use in the plot
val ax1 = Axis(X, title = "x axis 1")
val ax2 = Axis(X, title = "x axis 2")
val ax3 = Axis(X, title = "x axis 3")
val ax4 = Axis(X, title = "x axis 4")

// first define the y-axes we will use in the plot
val ax6 = Axis(Y, title = "y axis 1")
val ax7 = Axis(Y, title = "y axis 2")
val ax8 = Axis(Y, title = "y axis 3")
val ax9 = Axis(Y, title = "y axis 4")

// it may be necessary to play around with the chart dimensions and margin  in order to ensure a good fit on screen.
val dim = 350

val chart1 = (
    Chart()
    setDimensions(width = dim, height = dim)
    addSeries XY(x, y).setName("a").drawMarkers
    addAxes(ax1, ax6)
    setMargin(l=50, r=30, t=50, b=50)
)

val chart2 = (
    Chart() 
    setDimensions(width = dim, height = dim)
    addSeries XY(x, y).setName("b").drawMarkers
    addAxes(ax2, ax7)
    setMargin(l=50, r=50, t=50, b=50)
)

val chart3 = (
    Chart()
    setDimensions(width = dim, height = dim)
    addSeries XY(x, y).setName("c").drawMarkers
    addAxes(ax3, ax8)
    setMargin(l=50, r=30, t=50, b=50)
)

val chart4 = (
    Chart()
    setDimensions(width = dim, height = dim)
    addSeries(series1, series2)
    setTitle "The MultiChart from Above"
    addAxes(xaxis1, yaxis1, xaxis2, yaxis2) // these axes come from above
    setConfig(responsive=false)
    asMultiChart(1, 2)
)

// The canvas has an underlying grid. By default the underlying grid is 1x1, but we can pass in the dimensions we 
// require by passing in parameters in the constructor.
Canvas(2, 2)
.setChart(0, 0, chart1)
.setChart(0, 1, chart2)
.setChart(1, 0, chart3)
.setChart(1, 1, chart4)
.plotInline

Error Bars

We can also add Error Bars to our plots.

Error bars come in two flavours:

  1. XError
  2. YError

Both of these has an associated mode that determines how the error bar is calculated for an individual point. The following modes can be specified:

  • DATA: The user passes in an array that specifies the per point error
  • PERCENT: The user passes in a Double that calculates the error as a percentage of the value of the point
  • CONSTANT: The user passes in a double that is the constant value for the error for all points
  • SQRT: This calculates the error as a square root of the point value.

The next few examples will demonstrate:

In [22]:
// First import the Error Bar options
import org.carbonateresearch.picta.options.{YError, CONSTANT, DATA, PERCENT, SQRT}
In [23]:
val dim = 400

val series1 = (
    XY(List(1, 2, 3), List(1.234, 5.2112, 2.44332))
    asType SCATTER
    drawStyle MARKERS
    // User specifies the error per point
    setErrorBars YError(mode = DATA, array = List(0.5, 0.5, 0.5))
)

val chart1 = (
    Chart()
    addSeries series1.setName("Error from provided data")
    setTitle "Per Point Specified Error"
    setDimensions(width = dim, height = dim)
)


val series2 = (
    XY(List(1, 2, 3), List(1.234, 5.2112, 2.44332))
    asType SCATTER
    drawStyle MARKERS
    // the error here is 10% of the corresponding y-value for the point
    setErrorBars YError(mode = PERCENT, value = 10.0)
)

val chart2 = (
    Chart()
    addSeries series2.setName("% error")
    setTitle "Percentage Error"
    setDimensions(width = dim, height = dim) 
)


val series3 = (
    XY(List(1, 2, 3), List(1.234, 5.2112, 2.44332))
    asType SCATTER
    drawStyle MARKERS
    // a constant error of 10 is applied to each point
    setErrorBars YError(mode = CONSTANT, value = 10.0)
    setName("Constant value")
)

val chart3 = (
    Chart()
    addSeries series3.setName("constant error")
    setTitle "Constant Error"
    setDimensions(width = dim, height = dim)
)


val series4 = (
    XY(List(1, 2, 3), List(1.234, 5.2112, 2.44332))
    asType SCATTER
    drawStyle MARKERS
    setErrorBars YError(mode = SQRT)
    setName("Sqrt")
)

val chart4 = (
    Chart()
    addSeries series4.setName("Sqrt")
    setTitle "Sqrt Error"
    // the error is set to the sqrt of the corresponding point
    setDimensions(width = dim, height = dim)
)

val canvas = Canvas(2, 2) addCharts (chart1, chart2, chart3, chart4)

canvas.plotInline

Other Types of Charts

The next examples will demonstrate how to create a variety of Charts. All of the examples below are composable with the Canvas subplot system.

Pie Chart

Pie charts can be created in two ways. The first way uses the PieElement component to compose a Piechart:

In [24]:
val a = PieElement(value=60, name="Residential")
val b = PieElement(value=20, name="Non-Residential")
val c = PieElement(value=20, name="Utility")

// we add a list of Pie Elements to an XY series as the list of Pie Elements gets deconstructed down into two series:
// X => values: [60, 20, 20]
// Y => labels: ["Residential", "Non-Residential", "Utility"]
// The labels become the series_name for each individual PieElement
// As we pass in a list of PieElements, we do not need to specify the type as a PIE
val series = XY(x=List(a, b, c))

val chart = Chart() addSeries series setTitle "Pie Elements"

chart.plotInline
In [25]:
// However, composing individual PieElements may be tedious if there is a lot of data and we know how to it all fits
// together. Picta provides a short hand to quickly create a Pie Chart using the methods we have seen previously
// In this case we pass the (value, name) pairs as two seperate lists. As before, the labels become an individual
// series name for each point; which is why the legend renders correctly.
val series = XY(x=List(60, 20, 20), y=List("Residential", "Non-Residential", "Utility")) asType PIE

val chart = Chart() addSeries series setConfig(false, false) setTitle "Pie Chart"

chart.plotInline

Histogram

In [26]:
val series = XY(x=x) asType HISTOGRAM

val xaxis = Axis(X, title = "x")
val yaxis = Axis(Y, title = "y")

val chart = (
    Chart() 
    addSeries series
    setTitle "Histogram with axes"
    addAxes(xaxis, yaxis)
)

Canvas()
.addCharts(chart)
.plotInline

Customizing colors for a Histogram

We can use the HistOptions class to further specify options for a histogram. For example, if we wanted to create a horizontally positioned histogram, we can do the following:

In [27]:
import org.carbonateresearch.picta.options.histogram.HistOptions
import org.carbonateresearch.picta.options.Line
In [28]:
// we can also compose customizations in much the same way:
val marker = Marker() setColor RGBA(255, 100, 102, 0.5) setLine Line()

// change xkey to y to get a horizontal histogram
val series = (
    XY(x) 
    asType HISTOGRAM
    setMarker marker
    // we can set histogram specific options using the setHistOptions method
    setHistOptions(orientation = HORIZONTAL)
)

val chart = (
    Chart() 
    addSeries series 
    setTitle "XY.Histogram.Color"
)

chart.plotInline

Cumulative Histogram

Sometimes a cumulative histogram may be desired. We can do this as follows:

In [29]:
// we can import a range of histnorms
import org.carbonateresearch.picta.options.histogram.{Cumulative, PERCENT, DENSITY, PROBABILITY_DENSITY, NUMBER}
In [30]:
val series = XY(x) asType HISTOGRAM setHistOptions(histnorm = NUMBER, cumulative = Cumulative(enabled=true))

val chart = Chart() addSeries series setTitle "Histogram - Cumulative"

chart.plotInline

Specifiying the Binning Function

We can also specify the binning function for a histogram.

In [31]:
import org.carbonateresearch.picta.options.histogram.{COUNT, SUM, AVG, MIN, MAX, HistOptions}
In [32]:
val x = List("Apples", "Apples", "Apples", "Oranges", "Bananas")
val y = List("5", "10", "3", "10", "5")

// we can also assign histOptions to a value and pass them to the setHistOptions method
val ho1 = HistOptions(histfunc = COUNT)
val ho2 = HistOptions(histfunc = SUM)
    
val t1 = XY(x = x, y = y) asType HISTOGRAM setHistOptions ho1
val t2 = XY(x = x, y = y) asType HISTOGRAM setHistOptions ho2

val chart = Chart() addSeries(t1, t2) setTitle "Histogram - Specify Binning Function"
val canvas = Canvas() addCharts chart
canvas.plotInline

2D Histogram Contour

In [33]:
val x = List.range(1, 50)
val y = x.map(x => x + scala.util.Random.nextDouble()*100)

val series = XY(x, y).asType(HISTOGRAM2DCONTOUR).drawMarkers

val chart = (
    Chart() addSeries series
    setTitle "2D Histogram Contour"
)

chart.plotInline

Adding additional axes

As the above is a density plot, adding histograms can be useful too.

We can add histograms as follows:

In [34]:
// import the 2d Density histogram options
import org.carbonateresearch.picta.options.histogram2d.Hist2dOptions
In [35]:
val ax1 = Axis(X, showgrid = false) setDomain(0.0, 0.85)
val ax2 = Axis(Y, showgrid = false) setDomain(0.0, 0.85)
val ax3 = Axis(X, position = 2, showgrid = false) setDomain(0.85, 1.0)
val ax4 = Axis(Y, position = 2, showgrid = false) setDomain(0.85, 1.0)

val marker = Marker() setColor RGB(102,0,0)
val series1 = XY(x, y) asType SCATTER drawStyle MARKERS setName "points" setMarker marker

val series2 = (
  XY(x, y)
    setName "density"
    asType HISTOGRAM2DCONTOUR
    setHist2dOptions(ncontours = 20, reversescale = false, showscale = true)
  )

val series3 = XY(x = x) asType HISTOGRAM setName "histogram" setAxes(ax1, ax4)
val series4 = (XY(y) setName "y density" asType HISTOGRAM setAxis ax3 setMarker marker
  setHistOptions (orientation = HORIZONTAL))

val layout = (ChartLayout("XY.Histogram2dContour.WithDensity", auto_size = false) setAxes(ax1, ax2, ax3, ax4))

val chart = Chart() addSeries(series1, series2, series3, series4) setChartLayout layout showLegend false

chart.plotInline

3D Charts

3D charts are constructed using a XYZ series as they take in 3 Series' to create a single point.

The Picta API does not accept nested lists, however the underlying Plotlyjs render does. In order to render any series that will make use of a nested list, we must flatten and provide the length of an element (before the list was flattened).

The following examples will make it clearer.

Contour

In [36]:
// lets create some dummy adata for the third dimension
val x = List(-9, -6, -5 , -3, -1)
val y = List(0, 1, 4, 5, 7)
val z = List(
    List(10, 10.625, 12.5, 15.625, 20),
    List(5.625, 6.25, 8.125, 11.25, 15.625),
    List(2.5, 3.125, 5.0, 8.125, 12.5),
    List(0.625, 1.25, 3.125, 6.25, 10.625),
    List(0, 0.625, 2.5, 5.625, 10)
)
In [37]:
// we flatten the nested list as we pass it into the Series constructor
val series = XYZ(x=x, y=y, z=z.flatten, n=z(0).length).asType(CONTOUR)

// set up the chart
val chart = Chart()
            .addSeries(series)
            .setTitle("Contour")

// plot the chart
chart.plotInline

Heatmap

In [38]:
// create a new nested list for the heatmap
val z = List.range(1, 101).map(e => e + scala.util.Random.nextDouble()*100).grouped(10).toList
In [39]:
// we get the length of an element of the nested list
val n = z(0).length

// we now flatten the list and pass it into Series constructor, as well as 'n', the length of an element so that the 
// heatmap dimensions are correctly constructed
val series = XYZ(z=z.flatten, n=n) asType HEATMAP

val chart = Chart().addSeries(series).setTitle("Heatmap")

chart.plotInline

Scatter3D

In [40]:
val x = List.range(1, 100)
val y = List.range(1, 100)
val z = List.range(1, 100).map(e => e + scala.util.Random.nextDouble()*100)

val series = XYZ(x, y, z).asType(SCATTER3D).drawStyle(MARKERS)
val chart1 = Chart() addSeries series setTitle "3D Scatter Chart" setConfig(false, false)
val canvas = Canvas() addCharts chart1
canvas.plotInline

3D Line

In [41]:
val x = List.range(1, 100)
val y = List.range(1, 100)
val z = List.range(1, 100).map(e => e + scala.util.Random.nextDouble()*100)

val series = XYZ(x, y, z).asType(SCATTER3D).drawStyle(LINES)
val chart1 = Chart() addSeries series setTitle "3D Line Chart" setConfig(false, false)
val canvas = Canvas() addCharts chart1
canvas.plotInline

Surface Plot

In [42]:
// 3d surface plot
val k = List(
    List(8.83,8.89,8.81,8.87,8.9,8.87),
    List(8.89,8.94,8.85,8.94,8.96,8.92),
    List(8.84,8.9,8.82,8.92,8.93,8.91),
    List(8.79,8.85,8.79,8.9,8.94,8.92),
    List(8.79,8.88,8.81,8.9,8.95,8.92),
    List(8.8,8.82,8.78,8.91,8.94,8.92),
    List(8.75,8.78,8.77,8.91,8.95,8.92),
    List(8.8,8.8,8.77,8.91,8.95,8.94),
    List(8.74,8.81,8.76,8.93,8.98,8.99),
    List(8.89,8.99,8.92,9.1,9.13,9.11),
    List(8.97,8.97,8.91,9.09,9.11,9.11),
    List(9.04,9.08,9.05,9.25,9.28,9.27),
    List(9,9.01,9,9.2,9.23,9.2),
    List(8.99,8.99,8.98,9.18,9.2,9.19),
    List(8.93,8.97,8.97,9.18,9.2,9.18)
  )

val series = XYZ(z=k.flatten, n = k(0).length) asType SURFACE setColorBar("Altitude", RIGHT_SIDE)
val chart2 = Chart() addSeries series setTitle "Surface"

chart2.plotInline

Third Dimension as Color

In [43]:
// multiple compositions can be used to create scatter charts with a color representing some third dimension
val series = (
    XY(x, y) 
    asType SCATTER 
    drawStyle MARKERS 
    setMarker marker 
    setColor z 
    setColorBar("3rd Dimension", RIGHT_SIDE)
)

val chart3 = Chart() addSeries series setTitle "Scatter With Color" showLegend false
chart3.plotInline

Subplot

The Subplot class can be used to generate subplots for an XYZ plots just as we did previously.

In [44]:
// it may be necessary to play around with the chart dimensions and margin  in order to ensure a good fit on screen.
val dim = 350

// The canvas has an underlying grid. By default the underlying grid is 1x1, but we can pass in the dimensions we 
// require by passing in parameters in the constructor.
Canvas(2, 2)
.setChart(0, 0, chart1.setDimensions(width = dim, height = dim).setMargin(l=50, r=50, t=50, b=50))
.setChart(0, 1, chart2.setDimensions(width = dim, height = dim)setMargin(l=50, r=50, t=50, b=50))
.setChart(1, 0, chart3.setDimensions(width = dim, height = dim)setMargin(l=50, r=50, t=50, b=50))
.setChart(1, 1, chart.setDimensions(width = dim, height = dim)setMargin(l=50, r=50, t=50, b=50))
.plotInline

Map

We can also create maps using the composition technique below.

In [45]:
import org.carbonateresearch.picta.options.{Margin, Line}
import org.carbonateresearch.picta.Map
In [46]:
// draw a line on the map that is red
val line = Line(width = 2) setColor "red"

// construct the map Series. It is an XY chart as it takes in a List of Longitude and Latitude
val series = Map(List(40.7127, 51.5072), List(-74.0059, 0.1275)) drawSymbol LINES drawLine line

// These are options that further specify the options for the map
val geo = MapOptions(landcolor = RGB(204, 204, 204), lakecolor = RGB(255, 255, 255))
              .setMapAxes(LatAxis(List(20, 60)), LongAxis(List(-100, 20)))

val chart = (
    Chart()
    addSeries series
    setConfig(false, false)
    setMapOptions geo
//     setMargin(l=0, r=0, t=0, b=0)
    showLegend false
    setTitle "Map"
)

chart.plotInline

Animated Charts

We can also create animated charts. This can be useful for tracking the evolution of a data over time.

All Series types should be supported.

2D Animated Chart

In [47]:
import org.carbonateresearch.picta.ColorOptions._

def genRangeRandomInt(min: Int = 0, max: Int = 10000) = min + (max - min) * scala.util.Random.nextInt()
def genRangeRandomDouble(min: Double = 0.0, max: Double = 10000.0) = min + (max - min) * scala.util.Random.nextDouble()

// creates random XY for testing purposes
def createXYSeries[T: Color]
(numberToCreate: Int, count: Int = 0, length: Int = 10): List[XY[Double, Double, T, T]] = {
    if (count == numberToCreate) Nil
    else {
      val xs = List.range(0, length).map(x => genRangeRandomDouble())
      val ys = xs.map(x => genRangeRandomDouble())
      val series = XY(x = xs, y = ys, name = "series " + count).drawMarkers
      series :: createXYSeries(numberToCreate, count + 1, length)
    }
}

def createXYZSeries(numberToCreate: Int, count: Int = 0, length: Int = 10): List[XYZ[Double, Double, Double]] = {
    if (count == numberToCreate) Nil
    else {
      val xs = List.range(0, length).map(x => genRangeRandomDouble())
      val ys = xs.map(x => genRangeRandomDouble())
      val zs = xs.map(x => genRangeRandomDouble())
      val series = XYZ(x = xs, y = ys, z = zs, name = "series " + count, `type` = SCATTER3D).drawMarkers
      series :: createXYZSeries(numberToCreate, count + 1, length)
    }
}
In [48]:
val xaxis = Axis(X, title = "X Variable") setLimits (0.0, 10000.0)
val yaxis = Axis(Y, title = "Y Variable") setLimits (0.0, 10000.0)

// we can also specifiy the underlying layout directly - sometimes this can be useful
val layout = ChartLayout("Animation XY") setAxes(xaxis, yaxis)

val series = createXYSeries(numberToCreate = 50, length = 30)

val chart = Chart(animated = true, transition_duration=100) setChartLayout layout addSeries series

chart.plotInline

Frame:

0

3D Animated Chart

In [49]:
val series = createXYZSeries(numberToCreate = 50, length = 30)

val chart = (
  Chart(animated = true)
  setTitle "Animation 3D"
  addSeries series
  setXAxisLimits(0, 1E4)
  setYAxisLimits(0, 1E4)
  setZAxisLimits(0, 1E4)
  drawZAxisLog true
)

chart.plotInline

Frame:

0

IO + Utility Functions

The library also comes with some basic CSV IO functions and a utility function for breaking down data

In [50]:
import org.carbonateresearch.picta.IO._
import org.carbonateresearch.picta.common.Utils.getSeriesbyCategory
In [51]:
val working_directory = getWorkingDirectory

println(working_directory)
/Users/fazi/Desktop/Final Project/picta
In [52]:
// by providing a path, we can read in a CSV
val filepath = working_directory + "/iris_csv.csv"

val data = readCSV(filepath)

// by default, CSV are read in as strings. However we can convert the individual columns to the correct format
val sepal_length = data("sepallength").map(_.toDouble)
val petal_width = data("petalwidth").map(_.toDouble)
val categories = data("class")
In [53]:
val series = XY(sepal_length, petal_width) asType SCATTER drawStyle MARKERS

val chart = Chart() addSeries series setTitle "Uninformative Chart"

chart.plotInline

The above chart is not very informative. However since we have the per data point category labels, we can use the utility function to display the data in the different clusters

In [54]:
val result = getSeriesbyCategory(categories, (sepal_length, petal_width))

val chart = Chart() addSeries result setTitle "Iris" showLegend true

chart.plotInline

Conus Integration

Picta was originally created to help plot data in CoNuS and with Spark. A couple of utility functions make wrangling CoNuS data easier. Below is an example taken from the conus repo and plotting some variables of interest.

In [55]:
import org.carbonateresearch.picta.conus.Utils._
import org.carbonateresearch.picta.{Canvas, Chart, XY}
import org.carbonateresearch.conus.common.SingleModelResults
import org.carbonateresearch.conus._

import math._
In [56]:
val simulator = new AlmondSimulator // We create an Almond specific simulator

// We will now create a ver simple 2D CoNuS model. The model has a dimension of 3x3 grids, and each grid is meant to represent
// about 100x100 meters of a field. We initialize the model with values ranging from 2.0 to 6.0. These represent the population 
// of rats living in each 100 sq meter of the field. We will run the simulation for 10 time step, each time step represents
// one generation. We assume a perfect parity between male and female rat, and we also assume that each couple will have 10
// babies per generation. In addition, we will simulate a death rate between 0 to 0.9 (0 to 90% of the population), assigned
// randomly at each timestep and for each square. A major simplification is that each cell (square in the field) has its own
// rat population, there is no movement of rats in between the different cells.

// In CoNuS, values that will be calculated are know as model variables. Let's set a few
val nbRats:ModelVariable[Int] = ModelVariable("Number of Rats",2,"Individuals") //Notice this is an Int
val deathRate:ModelVariable[Double] = ModelVariable("Death rate",0.0,"%")

// Let's initialise a few model conditions 
val numberOfSteps = 10

// And let's create a function that, given a rat population and a deathRate, calculates a new population 

def survivingRats(initialPopulation:Int, deathRate:Double): Int = {
    initialPopulation-math.floor(initialPopulation.toDouble*deathRate).toInt
}
17:16:20.229 [CoNuS-akka.actor.default-dispatcher-3] INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started
SLF4J: A number (1) of logging calls during the initialization phase have been intercepted and are
SLF4J: now being replayed. These are subject to the filtering rules of the underlying logging system.
SLF4J: See also http://www.slf4j.org/codes.html#replay
Variable Number of Rats with initial value of 2 Individuals defined
Variable Death rate with initial value of 0.0 % defined
In [57]:
// Now we can create our model, step by step
val ratPopulation = new SteppedModel(numberOfSteps,"Simplified rat population dynamics")
    .setGrid(3,3) // 9 cells
    .defineMathematicalModel( // In this super simple model we do only two things at each step
      deathRate =>> {(s:Step) => scala.util.Random.nextDouble()*0.9}, // calculate a death rate
      nbRats =>> {(s:Step) => {survivingRats(nbRats(s-1)+(nbRats(s-1)/2*10),deathRate(s))}} // calcuate the nb rats
    )
    .defineInitialModelConditions( // Now we need to determine the inital size of the population at each model grid
      PerCell(nbRats,List(
        (List(2),Seq(0,0)),
        (List(2),Seq(0,1)),
        (List(4),Seq(0,2)),
        (List(4),Seq(1,0)),
        (List(2),Seq(1,1)),
        (List(6),Seq(1,2)),
        (List(2),Seq(2,0)),
        (List(4),Seq(2,1)),
        (List(6),Seq(2,2)))))
A total of 1 unique models were defined, attempting to create a list now.
Models list successfully created.
Model characteristics
FeatureValue
NameSimplified rat population dynamics
Nb of steps10
Nb of models1
Nb grid cells9
Nb of operations per step2
Total nb of operations180
In [58]:
// Now we run the model
simulator.evaluate(ratPopulation)
Run progress:
#################################################################################################### 100.0%
Total runtime: 0 seconds
In [60]:
// grab the results from the Conus model
val model: SingleModelResults = simulator(ratPopulation)(0)

val generation = (0 until numberOfSteps-1).map(x=>x.toDouble).toList

// we can use the utility function to grab the series for a single variable
val deathRateSeries: List[Double] = getDataFromSingleModel(model, deathRate, List(0,0), numberOfSteps)

val xy1 = XY(generation, deathRateSeries) setName("Death rate")

// alternatively we can quickly get the same data for XY using the function below
//  val xy1 = getXYSeriesFromSingleModel(model, (age, d18Occ), List(0), numberOfSteps)

// lets also plot a second y variable
val yaxis2 = Axis(
    Y,
    position = 2, 
    title = "Nb rats", 
    overlaying = Axis(Y), // this ensures that the axis sits on a seperate axis
    side = RIGHT_SIDE, // this ensures the axis is on the right hand side
    tickformat = "0.0f" // this will keep formatting reasonable for display purposes
)

// we construct the second y variable;
val nbRatsSeries: List[Double] = getDataFromSingleModel(model, nbRats, List(0,0), numberOfSteps).map(x => x.toDouble)
val xy2 = XY(generation, nbRatsSeries) setAxis yaxis2 setName("Nb of rats")

// finally we can combine in a single chart
val chart = (
    Chart() 
    addSeries xy1
    addSeries xy2
    setTitle("Death rate vs nb of rats per generation for cell (0,0)") 
    addAxes(Axis(X, title="Generation"), Axis(Y, title="Death rate"), yaxis2)
)

val canvas = Canvas() addCharts chart

// When we plot the result, we can see the legend is in the wrong place and overlaying the axis - we can overcome this
// in the next example
canvas.plotInline()
Simplified rat population dynamics model #1
Timestep: 9
[from 0 to 9]
Cell coordinatesNumber of RatsDeath rate
(0, 0)27740.7546291128476154
(0, 1)55300.8643091295605385
(0, 2)946780.7451193238389766
(1, 0)57640.6696876234748471
(1, 1)275590.3002731251910419
(1, 2)44953100.11279058375097456
(2, 0)3530090.8680828644779888
(2, 1)288080.6719572479842593
(2, 2)42980.6894402371924573

Moving the Legend

We can also move the Legend and position it where we would like as follows:

In [61]:
import org.carbonateresearch.picta.options.AUTO
In [62]:
// finally we can combine in a single chart
val chart = (
    Chart() 
    addSeries xy1
    addSeries xy2
    setTitle("Death rate vs Nb of rats for cell (0,0)") 
    addAxes(Axis(X, title="Generation"), Axis(Y, title="Death rate"), yaxis2)
    setLegend(x = 0.5, y = -0.5, orientation = HORIZONTAL, xanchor = AUTO, yanchor = AUTO)
)

val canvas = Canvas() addCharts chart

canvas.plotInline()
In [63]:
val xaxis = Axis(X, title = "Generation") setLimits (0.0, 9.0)
val yaxis = Axis(Y, title = "Death Rate") setLimits(0.0, 1.0)

// lets also plot a second y variable
val yaxis2 = Axis(
    Y,
    position = 2, 
    title = "Nb rats",
    overlaying = Axis(Y), // this ensures that the axis sits on a seperate axis
    side = RIGHT_SIDE, // this ensures the axis is on the right hand side
    tickformat = "0.0f" // this will keep formatting reasonable for display purposes
).setLimits (0.0, 10000.0)

// we can also specifiy the underlying layout directly - sometimes this can be useful
val layout = ChartLayout("Animation XY with Multiple Series") setAxes(xaxis, yaxis, yaxis2)

val animation = 
    (0 to generation.size-1)
    .map(x => XY(generation.take(x+1), deathRateSeries.take(x+1)) setName "Death Rate")
    .toList

val animation2 = 
    (0 to generation.size-1)
    .map(x => XY(generation.take(x+1), nbRatsSeries.take(x+1)) setName "Nb of rats" setAxis yaxis2)
    .toList

val chart = (
    Chart(animated = true, transition_duration=100, animate_multiple_series = true)
    setChartLayout layout 
    addSeries animation
    addSeries animation2
    setLegend(x = 0.5, y = -0.5, orientation = HORIZONTAL, xanchor = AUTO, yanchor = AUTO)
) 

chart.plotInline

Frame:

0

In [64]:
val nbCol = (0 to 2).toList
val mySeries:List[Double] = nbCol.flatMap(r => {
    nbCol.map{c => getDataFromSingleModel(model, nbRats, List(r,c), numberOfSteps).last.toDouble}})

val series = XYZ(z=mySeries,n=3) asType HEATMAP

val chart = Chart().addSeries(series).setTitle("Nb of rats at time step 10")

chart.plotInline
In [65]:
val ratsAsSurface = XYZ(z=mySeries,n=3) asType SURFACE setColorBar("Rat Population", RIGHT_SIDE)

val ratsChart = Chart() addSeries ratsAsSurface setTitle "Surface"

ratsChart.plotInline
In [70]:
def createSeries:List[List[Double]] = {
    val nbCol = (0 to 2).toList
    val nestedList:List[List[Double]] = nbCol.flatMap(r => {
    nbCol.map{c => getDataFromSingleModel(model, nbRats, List(r,c), numberOfSteps).map(x=>x.toDouble)}})
    (0 to 9).map(x => (0 to 8).map(y => nestedList(y)(x)).toList).toList
}


val ratsAsSurface = createSeries.map(s => XYZ(z=s,n=3) asType SURFACE setColorBar("Rat Population", RIGHT_SIDE)) 

val ratsChart = Chart(animated = true, transition_duration=100) addSeries ratsAsSurface setTitle "Animated Surface" setZAxisLimits(0, 3E6)

ratsChart.plotInline

Frame:

0

In [ ]: