Working with tables

This notebook covers how to work with tables in a BeakerX Jupyter notebook, including built-in features as well as SciJava's org.scijava.table package.

In [1]:
%classpath config resolver scijava.public https://maven.scijava.org/content/groups/public
%classpath add mvn net.imagej imagej 2.0.0-rc-71
ij = new net.imagej.ImageJ()
"ImageJ v${ij.getVersion()} is ready to go."
Added new repo: scijava.public
Out[1]:
ImageJ v2.0.0-rc-71 is ready to go.

Built-in table rendering

BeakerX has some nice table rendering capabilities. You can create a simple table using a standard map data structure:

In [2]:
foodMap = [
    "apple": "fruit",
    "orange": "fruit",
    "broccoli": "vegetable",
    "milk": "dairy",
]

By using a list of maps, you can define column headers, as well as more than two columns:

In [3]:
foodListOfMaps = [
    ["Food": "apple",    "Category": "fruit",     "Calories": 95],
    ["Food": "orange",   "Category": "fruit",     "Calories": 45],
    ["Food": "broccoli", "Category": "vegetable", "Calories": 50],
    ["Food": "milk",     "Category": "dairy",     "Calories": 103],
]

Limitations of built-in BeakerX tables

BeakerX tables do not support complex rendering inside each table cell. For example:

In [4]:
import javax.imageio.ImageIO

// Image sources:
// - https://commons.wikimedia.org/wiki/File:Dark_apple.png
// - https://commons.wikimedia.org/wiki/File:Hassaku_fruit_and_cross_section.jpg
// - https://commons.wikimedia.org/wiki/File:Broccoli_DSC00862.png
// - https://commons.wikimedia.org/wiki/File:Milk_glass.jpg
// - https://commons.wikimedia.org/wiki/File:KS_California_strawberry_yogurt.JPG
foodListOfMaps.get(0).put("Image", ImageIO.read(new URL("https://upload.wikimedia.org/wikipedia/commons/thumb/3/32/Dark_apple.png/120px-Dark_apple.png")))
foodListOfMaps.get(1).put("Image", ImageIO.read(new URL("https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/Hassaku_fruit_and_cross_section.jpg/120px-Hassaku_fruit_and_cross_section.jpg")))
foodListOfMaps.get(2).put("Image", ImageIO.read(new URL("https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Broccoli_DSC00862.png/120px-Broccoli_DSC00862.png")))
foodListOfMaps.get(3).put("Image", ImageIO.read(new URL("https://upload.wikimedia.org/wikipedia/commons/thumb/0/0e/Milk_glass.jpg/106px-Milk_glass.jpg")))

// NB: To see BeakerX explode, run the cell with this line uncommented:
//listOfMaps
Out[4]:
null

SciJava Tables

SciJava offers a set of interfaces for tables in the org.scijava.table package. The base interface is Table, which is a List of typed Columns. These columns offer improved type safety and storage efficiency over the "maps" and "list of maps" approaches shown above. They can also used to render more complex data using the ij.notebook().display(...) methods.

Wrapping lists and maps as SciJava tables

The org.scijava.table.Tables utility class provides convenience methods for wrapping List and Map data structures as read-only SciJava Table objects:

In [5]:
import org.scijava.table.Tables
foodTable = Tables.wrap(foodListOfMaps, null)

Rendering table data using the ij.notebook().display(...) methods

To render Table objects nicely, they must be passed to the ij.notebook().display(Object) method:

In [6]:
ij.notebook().display((Object) foodTable)
Out[6]:
FoodCategoryCaloriesImage
applefruit95
orangefruit45
broccolivegetable50
milkdairy103

For convenience, there is a ij.notebook().display(List) method, eliminating the need for the Tables.wrap call here:

In [7]:
ij.notebook().display(foodListOfMaps)
Out[7]:
FoodCategoryCaloriesImage
applefruit95
orangefruit45
broccolivegetable50
milkdairy103

There is also a ij.notebook().display(Map) method:

In [8]:
ij.notebook().display(foodMap)
Out[8]:
applefruit
orangefruit
broccolivegetable
milkdairy

There are additional signatures of the display method for specifying row and/or column headers:

In [9]:
ij.notebook().display(foodMap, 'Category')
Out[9]:
 Category
applefruit
orangefruit
broccolivegetable
milkdairy
In [10]:
ij.notebook().display(foodListOfMaps, ['A', 'B', 'C', 'D'])
Out[10]:
 FoodCategoryCaloriesImage
Aapplefruit95
Borangefruit45
Cbroccolivegetable50
Dmilkdairy103

Creating a SciJava table from scratch

There is an API for creating SciJava Table objects without wrapping lists or maps. Using this API gives you control over the data type of the columns. The following column types are available:

  • ByteColumn stores each table cell as a primitive byte (int8).
  • ShortColumn stores each table cell as a primitive short (int16).
  • IntColumn stores each table cell as a primitive int (int32).
  • LongColumn stores each table cell as a primitive long (int64).
  • FloatColumn stores each table cell as a primitive float (float32).
  • DoubleColumn stores each table cell as a primitive double (float64).
  • BoolColumn stores each table cell as a primitive boolean.
  • CharColumn stores each table cell as a primitive char.
  • GenericColumn stores each table cell as an Object.

GenericColumn is the most flexible, but it uses an Object for every cell, which can be inefficient. For better space performance, it is encouraged to instead use column types with efficient storage as appropriate. For example, if you know a column will consist only of short values, then use a ShortColumn. The primitive numeric column types are built on SciJava's PrimitiveArray utility classes; e.g., DoubleColumn is a column backed by a DoubleArray, which is in turn backed by a double[] which grows dynamically as needed.

To illustrate this structure, here is an example which creates a SciJava table from scratch:

In [11]:
import org.scijava.table.DoubleColumn
import org.scijava.table.GenericColumn
import org.scijava.table.DefaultGenericTable

// Create two columns.
nameColumn = new GenericColumn("City")
populationColumn = new DoubleColumn("Population")

// Fill the columns with information about the largest cities in the world.
nameColumn.add("Karachi")
populationColumn.add(23500000d)
nameColumn.add("Bejing")
populationColumn.add(21516000d)
nameColumn.add("Sao Paolo")
populationColumn.add(21292893d)

// But actually, the largest city is Shanghai,
// so let's add it at the beginning of the table.
nameColumn.add(0, "Shanghai")
populationColumn.add(0, 24256800d)

// Create the table.
cities = new DefaultGenericTable()
cities.add(nameColumn)
cities.add(populationColumn)

ij.notebook().display((Object) cities)
Out[11]:
CityPopulation
Shanghai2.42568E7
Karachi2.35E7
Bejing2.1516E7
Sao Paolo2.1292893E7

Here is another way to create a table, using the set(int col, int row, T value) method:

In [12]:
import org.scijava.table.DefaultGenericTable
colCount = 7
rowCount = 5
spreadsheet = new DefaultGenericTable(colCount, rowCount)
for (col = 0; col < colCount; col++) {
    letter = (char) (col + 65) // 0->A, 1->B, etc.
    spreadsheet.setColumnHeader(col, "" + letter)
    for (row = 0; row < rowCount; row++) {
        data = "" + letter + (row + 1)
        spreadsheet.set(col, row, data)
    }
}
ij.notebook().display((Object) spreadsheet)
Out[12]:
ABCDEFG
A1B1C1D1E1F1G1
A2B2C2D2E2F2G2
A3B3C3D3E3F3G3
A4B4C4D4E4F4G4
A5B5C5D5E5F5G5

When created this way, the columns are all GenericColumn instances:

In [13]:
spreadsheet.stream().map{column -> [
    "Header": column.getHeader(),
    "Column type": column.getClass().getName(),
    "Data type": column.getType()
]}.collect()

Reading information from tables

Read out the header of the second column:

In [14]:
header = cities.get(1).getHeader()
Out[14]:
Population

Get a certain column.

In [15]:
populationColumn = cities.get("Population")
Out[15]:
[2.42568E7, 2.35E7, 2.1516E7, 2.1292893E7]

Get a value from the first line in the column.

In [16]:
populationOfLargestTown = populationColumn.get(0)
Out[16]:
2.42568E7

List of Table API methods

The Table interface provides many convenience methods. Here is a complete list:

In [17]:
ij.notebook().methods(cities)