Subscribe:

Pages

Displaying Multi Column Data in wxRuby

This post is about using the ListCtrl widget within wxRuby to display data in a table format, using several columns naming the fields in the data. The most straightforward way to manage the data is to construct an underlying data model, and use the virtual listctrl style to have your data displayed dynamically. This method also efficiently displays large datasets.

To demonstrate the simplest case, I will display some data from a CSV text file. For example, the
letter recognition dataset from the UCI machine learning repository consists of two files. The file ending '.data' contains the raw data. Each line contains the class and attributes of an instance from the data set. Each instance can be represented using a Ruby struct. Taking the attribute names from the file ending '.names', I build the following struct:

LetterInstance = Struct.new(:letter, :x_box, :y_box, :width, :height,
:on_pixels, :x_bar, :y_bar, :x2bar, :y2bar,
:xybar, :x2ybar, :xy2bar, :x_ege, :xegvy,
:y_ege, :yegvx)

I define the Struct with the attribute names in the order that they occur in the CSV file, as this makes reading the CSV file simple. To read in the textfile to create an array of instances of this struct:


@@data = []

File.open("letter-recognition.data", "r") do |f|
f.each_line do |line|
@@data << LetterInstance.new(*line.strip.split(","))
end
end

Displaying the Data

Building the list control is almost trivial. I use two facts about Ruby Structs to help in constructing the table automatically. First, the method 'members' can be used to find all the field names within the Struct; these are used for the column headings.

For example: @@data[0].members.join(", ")
returns: "letter, x_box, y_box, width, height, on_pixels, x_bar, y_bar, x2bar, y2bar, xybar, x2ybar, xy2bar, x_ege, xegvy, y_ege, yegvx"

Second, fields within a Struct can be accessed using array indexes. So, @@data[0][0] returns the value in the 'letter' field of the first item.

The following few lines of code create a class for our own virtual list control, which extends Wx::ListCtrl. We are building a particular kind of list control, which uses 'report format' and a 'virtual list data model'. Other kinds exist, but are not covered here. The initialize method builds the columns automatically, and sets the number of items.
The method 'on_get_item_text' is the one which makes this virtual list control so easy to use. The method is called only when an item is to be displayed. It is given the index of the item to display, and the column number. Your method just has to return the string for this item and column: as our data is stored as an array of structs, the implementation is as simple as can be.
# -- show data in a ListCtrl

require 'wx'

class LetterDataList < Wx::ListCtrl
def initialize parent
super(parent, :style => Wx::LC_REPORT | Wx::LC_VIRTUAL)
return if @@data.nil? or @@data.empty?

# Directly retrieve field names in Struct to populate table with columns
@@data[0].members.each_with_index do |name, index|
insert_column(index, name.to_s)
end

set_item_count @@data.size
end

# use array like indexing of fields in Struct to
# directly retrieve data for display
def on_get_item_text(item, column)
@@data[item][column]
end
end

class LetterDisplay < Wx::Frame
def initialize
super(nil, :title => "Letter Recognition Data")

LetterDataList.new self
end
end

Wx::App.run do
LetterDisplay.new.show
end


A screenshot of the result:


Handling Events


The list control supports a number of events. The most useful are for catching double clicks on a row or change in the highlighted row. Double clicks are known as activating the item, and single clicks as selecting the item. The event evt_list_item_activated captures a double click, and the event evt_list_item_selected captures a change in the focussed item (through a single click or use of arrow keys). Both events are easy to capture and redirect to a method to handle them. The following code will just print out the item(s) now selected by walking through the list of selected indices obtained by calling selections:

class LetterDataList < Wx::ListCtrl
def initialize parent
super(parent, :style => Wx::LC_REPORT | Wx::LC_VIRTUAL)
return if @@data.nil? or @@data.empty?

# Directly retrieve field names in Struct to populate table with columns
@@data[0].members.each_with_index do |name, index|
insert_column(index, name.to_s)
end

evt_list_item_selected(self) { selected_item }

set_item_count @@data.size
end

# use array like indexing of fields in Struct to
# directly retrieve data for display
def on_get_item_text(item, column)
@@data[item][column]
end

def selected_item
get_selections.each do |selection|
puts @@data[selection]
end
end

Using jRuby from Netbeans

Netbeans is a great development platform which supports Ruby and jRuby. In this post, I have collected together the steps I needed to write and run a jRuby script with a library file, such as Weka or my own jChrest.

First, you must download and install the Netbeans platform. ( I started by downloading the version for Java SE alone.)


Second, you must add the plugin to use Ruby programs. To do this start up Netbeans, and select the 'plugins' option on the 'Tools' menu. Under the tab for 'Available plugins', select the 'Ruby' plugin and install it. (You may not need this step, if you downloaded a version with Ruby included in the previous step.)

Third, download and install jRuby. You need to download and unpack one of the 'binary' distributions. Take note of the 'bin' directory, as you need to know this to tell Netbeans about your jRuby distribution.

Fourth, start up Netbeans and create a new ruby project.

After clicking 'next' you need to specify the Ruby platform you want to use.

The first time we do this, we must tell Netbeans about our new jRuby installation, so click on 'Manage...' and select the 'jruby' executable in the 'bin' directory you installed in step 3. Once you've done this, Netbeans will remember your jruby installation for future projects.

Now, you can create some ruby code and click on 'Run Main Project' to see the output in the 'Output window'. Two things may need changing. First, to set a project to be the 'Main' project, the one which is run when you click the green triangle, you need to right click on the name of the project and select 'Set as Main Project'. Second, you may need to change the ruby platform or version. Again, right click on the name of the project and select 'Properties'. Click on the 'Run' option on the left hand side and you can change the Ruby Platform in the choice box, and add options to the interpreter. In the example below, you can see I have made jRuby use 1.9 mode:

Accessing Java Libraries

The final step in setting up the environment is to include a Java library within the project. For example, to work with Weka, you need to access the file weka.jar. To do this within Netbeans, go to the 'Java' option in the Properties dialog box shown above. Select 'Add JAR/Folder' and add the jar file to the classpath for this project. That's all there is to it. The only change I needed to make to my scripts was to remove the line 'require "weka"'; this is taken care of by adding the jar file to the project.


Plotting Multiple Data Sources in RSRuby

In this post, I look at plotting multiple data sources on a single graph from RSRuby, and also how to use Ruby to simplify the method of constructing such graphs.

Plotting Multiple Data Sources

Consider the table below as a typical example, which contains some data on craters on different planetary bodies of the solar system. There are three columns. The first gives the parent body (planet) of the crater, the second gives the diameter of the crater, and the third gives the depth of the crater. What we would like to see is a plot with the two axes holding the quantitative information, and the labels of the points showing the planet. (Note: these are not real data.)











Planetary BodyDiameter (km)Depth (km)
Moon3355.7
Moon663.0
Moon412.5
Mars501.0
Mars801.5
Ganymede200.8
Ganymede701.0
Ganymede800.9


In a previous post, I looked at plotting a single set of points. For example, to make a simple graph of diameter vs depth for the Moon's craters, you could use the following code:
require 'rsruby'

@@r = RSRuby.instance

@@r.png("mygraph.png")
@@r.plot([335,66,41], [5.7,3.0,2.5],
:xlab => "Diameter (km)", :ylab => "Depth (km)",
:main => "Crater diameter vs. depth",
:xlim => [0, 350], :ylim => [0, 6],
:pch => 15, :col => "blue")
@@r.eval_R("dev.off()")

The points are plotted using a symbol, controlled by the number passed to :pch. The range of numbers for :pch goes from 1 to 25. http://www.phaget4.org/R/plot.html has a diagram showing the effect of the different numbers, but it is good to experiment a little for your own graph.

To add more points to the graph, with different symbols, we use the 'points' command for each group of data, before the 'eval' command:
require 'rsruby'

@@r = RSRuby.instance

@@r.png("mygraph.png")
@@r.plot([335,66,41], [5.7,3.0,2.5],
:xlab => "Diameter (km)", :ylab => "Depth (km)",
:main => "Crater diameter vs. depth",
:xlim => [0, 350], :ylim => [0, 6],
:pch => 15, :col => "blue")
@@r.points([50,80], [1.0,1.5], :pch => 19, :col => "green")
@@r.points([20,70,80], [0.8,1.0,0.9], :pch => 4, :col => "red")
@@r.eval_R("dev.off()")

The resulting graph is:


Simplifying the Code

Automation is a big benefit of doing the above kind of process from Ruby. How can we simplify the code to make it as easy as possible to construct and save a graph from a new set of data? For example, if we extend the above table with another two attributes, we might want to create all possible 6 graphs, one for each pair of attributes. To make the call as easy as possible, we need to encapsulate the graph construction process within a single function with some suitable parameters.

First, we need a structure to represent the data in a single group. Ruby provides a Struct data structure just for such purposes. We create a Struct holding the label, points and display information for the group of data.
DataItems = Struct.new(:name, :x_points, :y_points, :pch, :col)

Then, the graph construction function can process an array of these structures, adding the information to the graph. The construction process adds all the data using the 'points' method in R. For this to work, you have to provide 'xlim' and 'ylim' in the parameters so the graph is drawn to the correct size. The 'plot_graph' method works by iterating through a list of provided DataItems, and adding the points to the graph using the display attributes. As an extra, there is a line to add a legend to the graph, again by collecting information from the DataItems array:
module L
R = RSRuby.instance

def L.plot_graph(filename, data, params)
R.png(filename, :res => 100)
R.plot([], [], params)
data.each do |items|
R.points(items.x_points, items.y_points, :pch => items.pch, :col => items.col)
end
R.legend("bottomright", data.collect {|items| items.name},
:col => data.collect {|items| items.col},
:pch => data.collect {|items| items.pch})
R.eval_R("dev.off()")
end
end

Finally, this is how to plot the same graph as above:
L.plot_graph("graph.png",
[DataItems.new("Moon", [335,66,41], [5.7,3.0,2.5], 15, "blue"),
DataItems.new("Mars", [50,80], [1.0,1.5], 19, "green"),
DataItems.new("Ganymede", [20,70,80], [0.8,1.0,0.9], 4, "red")],
:xlim => [0, 350],
:ylim => [0, 6],
:xlab => "Diameter (km)",
:ylab => "Depth (km)",
:main => "Crater diameter vs. depth")


There are still parts that could be included as parameters to the function, such as the location of the legend. The advantage of an approach such as the above is not to make it into a general function, but to provide a pattern to copy and modify in a particular case. With the above, the call to create a graph is a single method call, and that can make automation of graph construction a lot easier. For example, to create a whole range of graphs from a table of data.