Plots
Underdog's Plots module renders different types of charts and adds integration with rest of Underdog's modules.
Introduction
Echarts DSL
Underdog plots uses the Apache Echarts under the hood, so the idea is to try to be able to render whatever is possible in Echarts. To accomplish that this project creates a Groovy DSL mimicking the Echarts Option object. You can access the DSL when customizing the chart.
At the moment the support of the Echarts Option object is limited but we aim to improve that overtime.
Basic properties
For every chart we must provide methods containing the following properties:
- data entry as list of numbers
- data entry as Series
- chart title
- chart subtitle
When the method is receiving data as List instances:
- X coordinate label (by default is X)
- Y coordinate label (by default is Y)
In methods receiving Series objects the name of the X and Y coordinate will be taken from the Series' name passed as parameter.
There could be extra properties added to specific charts depending on how practical these properties are when dealing which that type of charts.
On top of that, every plot has a customize(Closure)
which allows to customize the chart following Echarts documentation using a Groovy DSL.
Customizing chart
All default methods provide a limited setup of the chart via the mandatory attributes we saw previously.To access the full Groovy Echarts DSL we can always access the Options#customize(Closure) method rendering the chart calling the Options#show().
Return Options
All plotting methods return a memento.plots.charts.Options
instance which represents the Echarts Options object.
Line
Simple
Here is a simple line chart:
def line = Underdog.plots()
.line(
// You can use a **range or a list** for X axis
2000..2010,
// You can use a **range or a list** for the Y axis
[10, 15, 18, 3, 5, 9, 10, 11, 12, 10],
// Optional attributes
title: "Wins of Team A",
subtitle: "Between years 2000 - 2010",
xLabel: "Years",
yLabel: "Wins"
)
line.show()
There are methods adapted for using Underdog's Series objects:
// load data
def df = Underdog.df().read_csv(baseballPath)
// filter & aggregate & sort
df = df[df['Team'] == 'BOS']
.agg(W: 'sum')
.by('year')
.sort_values(by: 'year')
// show
def plot = Underdog.plots()
.line(
// using `year` series for X axis
df['year'],
// renaming series to `Wins X Years` and using it for Y axis
df['Sum [W]'].rename('Wins X Years'),
title: "Wins of 'BOS' team over time")
plot.show()
In any method using Underdog's Series there is no attribute for changing the xLabel or the yLabel as it takes the Series' name. To change the label you can rename the Series' name as mentioned in the example.
N-lines
Map<String, List<Number>> data = [
// A list of lists of 2 elements [[x1, y1], [x2, y2],..., [xn, yn]]
A: [[2000, 13],[2001, 5], [2002, 7], [2003, 10], [2004,6]],
B: [[2000, 5], [2001, 6], [2002, 7], [2003, 8], [2004, 9]],
// Using [listX, listY]transpose()` == [[x1, y1], [x2, y2],..., [xn, yn]]
C: [2000..2004, 3..7].transpose()
]
def plot = Underdog.plots()
.lines(
data,
title: "Progress of Teams A, B, C",
subtitle: "Between years 2000 - 2010",
xLabel: "Years",
yLabel: "Wins"
)
plot.show()
Customize
As in any chart once we've created our Options object and before calling show()
we can use the Options#customize()
method to customize the chart using Groovy's Echart DSL.
def plot = Underdog.plots()
.lines(
dataFrame,
title: "Team comparison (BOS, ATL, CIN)",
subtitle: "Years 2000-2004",
xLabel: "Years",
yLabel: "Wins"
).customize {
// Adding legend in the top right corner
legend {
top("10%")
right('15%')
show(true)
}
// Adding tooltip of type `axis`
tooltip {
trigger('axis')
}
}
plot.show()
In this occasion we are adding the chart legend and positioning it to the top-right side of the chart.
Bar
Simple
def plot = Underdog.plots()
.bar(
1..12,
[10, 12, 18, 3, 0, 20, 10, 12, 18, 3, 0, 10],
title: "Using bars",
xLabel: "Months",
yLabel: "Indicator"
)
plot.show()
Histogram
// generating data
def random = new Random()
def distribution = (0..1_000).collect { random.nextGaussian(0, 50) }
// plot
def plot = Underdog
.plots()
.histogram(
distribution,
title: "Distribution",
subtitle: "mean: 0 / stddev: 50",
bins: 10
)
plot.show()
Scatter
Simple
// numbers from 0 to 99
// You can use a "range or a list" for X axis
def xs = 0..<100
// 100 random numbers
// You can use a "range or a list" for the Y axis
def ys = (0..<100).collect { new Random().nextInt(100) }
// plot
def plot = Underdog.plots()
.scatter(
xs,
ys,
title: "Random Numbers") // Optional attributes
plot.show()
Here's the same example but using Underdog's series for X and Y axes. Given the dataframe instance df
and series xs
and ys
:
Plots.plots()
.scatter(
// using a series for x axis and renaming it to X
df['xs'].rename('X'),
// using another series for y axis and renaming it to Y
df['ys'].rename('Y'),
title: "Random Numbers")
.show()
Graphs
Simple
Here's a simple graph representation just showing 2 edges connecting 3 vertices. We only have to pass an instance
of a graph to the graph(...)
function:
// create instance of Graph
def friends = Graphs.graph(String) {
edge('Robert', 'Thelma', relation: 'friend')
edge('Robert', 'Troy', relation: 'friend')
}
// show plot
def plot = Underdog.plots().graph(friends)
plot.show()
Directed graph
If the graph we're passing to the plots().graph(...)
function is a directed graph, the direction of the edges will
show up:
def friends = Graphs.digraph(String) {
edge('Robert', 'Thelma', relation: 'friend')
edge('Robert', 'Troy', relation: 'friend')
}
def plot = Underdog.plots().graph(friends)
plot.show()
Edge labels
If we want to show the edges labels we can do so by setting the showEdgeLabel
parameter to true
:
def friends = Graphs.digraph(String) {
edge('Robert', 'Thelma', relation: 'friend')
edge('Robert', 'Troy', relation: 'friend')
}
def plot = Plots.plots().graph(friends, showEdgeLabel: true)
plot.show()
Showing paths
Sometimes we may want highlight a given path between vertices. We can use the parameter paths
which receives a list
of paths to highlight any number of paths:
def friends = Graphs.digraph(String) {
edge('Robert', 'Thelma', relation: 'friend')
edge('Robert', 'Troy', relation: 'friend')
}
def friendship = friends.shortestPath('Robert', 'Troy')
def plot = Underdog.plots().graph(
friends,
paths: [friendship],
showEdgeLabel: true)
plot.show()
Graph domain
To go one step further and play with vertices sizes and colors, we can use the graph chart domain classes. These classes are:
underdog.plots.charts.Graph.Node
: represents a node graphically (size, color, label)underdog.plots.charts.Graph.Edge
: represents an edge graphically (width, color, label)
List<Graph.Node> nodes = [
new Graph.Node(id: "robert", name: "Robert", symbolSize: 75),
new Graph.Node(id: "thelma", name: "Thelma", symbolSize: 40),
new Graph.Node(id: "troy", name: "Troy", symbolSize: 40)
]
List<Graph.Edge> edges = [
new Graph.Edge(
source: "robert",
target: "thelma",
color: "green",
width: 2,
value: "bff"),
new Graph.Edge(
source: "robert",
target: "troy",
color: "red",
width: 10,
value: "friend")
]
def plot = Underdog.plots().graph(
nodes,
edges,
showEdgeLabel: true)
plot.show()
Customize
As any of the charts in Underdog, we can use the customize(...)
method of any chart to customize the chart by using
the Groovy Echarts DSL:
def friends = Graphs.digraph(String) {
edge('Robert', 'Thelma', relation: 'friend')
edge('Robert', 'Troy', relation: 'friend')
}
def plot = Underdog.plots()
.graph(friends, showEdgeLabel: true)
.customize {
title {
text "New title"
subtext("New subtitle")
top("bottom")
left("right")
}
}
plot.show()
Pie
Wikipedia
According to Wikipedia a pie chart (or a circle chart) is a circular statistical graphic which is divided into slices to illustrate numerical proportion.
In a pie chart, the arc length of each slice (and consequently its central angle and area) is proportional to the quantity it represents.
Simple
To create a minimal representation of a Pie we must provide at least a collection of the labels of each partition, and another collection with the values of each partition:
def plot = Underdog
.plots()
.pie(
('A'..'D'), // slice labels
[9,5,6,4] // slice values
)
plot.show()
Color mapping
In some situations the color of each partition is really meaningful. For example, it would be strange to represent a
group of race teams and represent the team Ferrari (which historically is red) with other color than red. In order
to map the colors to each partition we can provide a map of entries of type partitionLabel: color
to the parameter colorMap
:
// Colors matching the labels
def COLORS = [
"Red Bull": "#101864",
"Ferrari": "#b03641",
"Mclaren": "#d26f30",
"Mercedes": "#505c62"
]
def plot = Underdog.plots()
.pie(
// Labels
["Red Bull", "Ferrari", "Mclaren", "Mercedes"],
// Values
[9,5,6,4],
// Passing color mappings
colorMap: COLORS,
title: "Top 4 Teams F1(tm) 2024 season",
subtitle: "Total number of driver victories per team"
)
plot.show()
Dataframe
In order to use an Underdog's dataframe we have to make sure that the name of the series should match
the names: names
, values
, colors
.
// source map
def df = [
names: ('A'..'D'),
values: (10..40).by(10),
colors: ['red', 'pink', 'yellow', 'lightblue']
].toDataFrame("dataframe")
// passing dataframe to pie plot and show it
def plot = Underdog.plots().pie(df)
plot.show()
You can also use Underdog's Series following the same rules:
// given a dataframe
def df = [
A: ('D'..'G'),
B: (110..140).by(10),
C: ['orange', 'gray', 'lightgray', 'blue']
].toDataFrame("dataframe")
// we can pass series
def plot = Underdog
.plots()
.pie(
// using series "A" and renaming it to "names"
df['A'].rename("names"),
// using series "B" and renaming it to "values"
df['B'].rename("values"),
// using series "C" and renaming it to "colors"
df['C'].rename("colors")
)
plot.show()
Radar
According to Wikipedia A radar chart is a graphical method of displaying multivariate data in the form of a two-dimensional chart of three or more quantitative variables represented on axes starting from the same point
Simple
def plot = Underdog
.plots()
.radar(
["power", "consumption", "price"], // Name of the categories
[200, 10, 100000], // Maximum values for each category
[150, 5, 54_350] // Actual value for each category
)
plot.show()
Extensions
Dataframe
Underdog.df()
// Dataframe created
.from(X: 10..<20, Y: [1, 3, 9, 3, 19, 10, 11, 4, 14, 20], "dataframe name")
// Plots extensions for dataframe add plots methods such as `scatter()`
.scatter()
.show()
Graphs
TODO