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).
interp.repositories() ++= Seq(coursierapi.MavenRepository.of(
"https://jitpack.io"
))
import $ivy. `org.carbonateresearch::picta:0.1.1`
import org.carbonateresearch.picta.render.Html.initNotebook // required to initialize jupyter notebook mode
initNotebook() // stops ugly output
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.
Picta graphs are constructed in a modular manner. The main components are:
Canvas
: This is is the top-level component for representing the display. A Canvas
may consist of multiple charts.Chart
: This is the component that corresponds to an actual chart (i.e. scatter, pie etc).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 chartXYZ
: This is series data for constructing a 3D chartMap
: This is series data that constructs a Map chartChartLayout
: 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 as below should bring in the most frequently used Components in the library.
import org.carbonateresearch.picta._
Here we create some dummy data to be used in the examples for this notebook.
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)
// 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
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.
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
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.
// 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
Now using the same principles as above, let's create a Bar
chart:
// 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
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:
// we import the marker option which lets us specify the marker
import org.carbonateresearch.picta.options.Marker
import org.carbonateresearch.picta.SymbolShape._
// 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
We can control the appearance of the markers by setting the width
// 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
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:
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
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:
// 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
Picta comes with a range of utility functions to make configuring the primary axes quick and easy
// 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())
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
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
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
We can also reverse the axis by setting the appropriate 'drawXAxisReversed'
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
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.
// 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
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.
// 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
We can also add Error Bars to our plots.
Error bars come in two flavours:
XError
YError
Both of these has an associated mode
that determines how the error bar is calculated for an individual point. The following mode
s can be specified:
DATA
: The user passes in an array that specifies the per point errorPERCENT
: The user passes in a Double that calculates the error as a percentage of the value of the pointCONSTANT
: The user passes in a double that is the constant value for the error for all pointsSQRT
: This calculates the error as a square root of the point value.The next few examples will demonstrate:
// First import the Error Bar options
import org.carbonateresearch.picta.options.{YError, CONSTANT, DATA, PERCENT, SQRT}
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
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
// 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
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
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:
import org.carbonateresearch.picta.options.histogram.HistOptions
import org.carbonateresearch.picta.options.Line
// 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
Sometimes a cumulative histogram may be desired. We can do this as follows:
// we can import a range of histnorms
import org.carbonateresearch.picta.options.histogram.{Cumulative, PERCENT, DENSITY, PROBABILITY_DENSITY, NUMBER}
val series = XY(x) asType HISTOGRAM setHistOptions(histnorm = NUMBER, cumulative = Cumulative(enabled=true))
val chart = Chart() addSeries series setTitle "Histogram - Cumulative"
chart.plotInline
We can also specify the binning function for a histogram.
import org.carbonateresearch.picta.options.histogram.{COUNT, SUM, AVG, MIN, MAX, HistOptions}
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
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
As the above is a density plot, adding histograms can be useful too.
We can add histograms as follows:
// import the 2d Density histogram options
import org.carbonateresearch.picta.options.histogram2d.Hist2dOptions
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 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.
// 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)
)
// 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
// 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
// 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
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
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
// 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
// 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
The Subplot class can be used to generate subplots for an XYZ plots just as we did previously.
// 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
We can also create maps using the composition technique below.
import org.carbonateresearch.picta.options.{Margin, Line}
import org.carbonateresearch.picta.Map
// 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
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)
}
}
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
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
The library also comes with some basic CSV IO functions and a utility function for breaking down data
import org.carbonateresearch.picta.IO._
import org.carbonateresearch.picta.common.Utils.getSeriesbyCategory
val working_directory = getWorkingDirectory
println(working_directory)
// 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")
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
val result = getSeriesbyCategory(categories, (sepal_length, petal_width))
val chart = Chart() addSeries result setTitle "Iris" showLegend true
chart.plotInline
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.
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._
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
}
// 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)))))
// Now we run the model
simulator.evaluate(ratPopulation)
// 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()
We can also move the Legend and position it where we would like as follows:
import org.carbonateresearch.picta.options.AUTO
// 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()
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
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
val ratsAsSurface = XYZ(z=mySeries,n=3) asType SURFACE setColorBar("Rat Population", RIGHT_SIDE)
val ratsChart = Chart() addSeries ratsAsSurface setTitle "Surface"
ratsChart.plotInline
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