Skip to content

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:

simple line
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:

Series
// 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

lines using collections
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.

Customize
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

simple bar
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

simple 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

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:

simple series
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:

simple graph
// 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:

directed graph
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:

edge labels
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:

show 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)

domain classes
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:

customization
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:

building pie
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:

color mapping
// 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.

dataframe
// 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:

series
// 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

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

dataframe extensions
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