Adding Your Own Data Objects
You might find that the standard data objects don't fulfill all your requirements. For example, maybe you are going to analyze data recorded from a game engine. The format of the original data can't be expressed directly as a table. Although you could write a single operator that reads in data from a file and does all the translation and feature extraction, you may decide that it's best to split the task. Instead, create multiple operators -- one that can handle the new data object and one that extracts part of the data as a table. With this modularity, it is much easier to extend the mechanism later on and to optimize steps separately.
Defining the object class
First, you must define a new class that holds the information you need.
This class should preferably extend the ResultObjectAdapter
, an abstract class that already implements much non-specialized functionality.
If a data-containing class with important functionality already exists, it might be more convenient to directly implement the IOObject
interface.
An empty implementation of the ResultObjectAdapter
would look like this:
import com.rapidminer.operator.ResultObjectAdapter;
public class DemoDataIOObject extends ResultObjectAdapter {
private static final long serialVersionUID = 1725159059797569345L;
}
The above is only an empty object that holds no information except for the serialVersionUID
.
This constant is needed during serialization, when loading an instance of the new object from the repository, or when storing it.
Now, add some content:
import com.rapidminer.operator.ResultObjectAdapter;
public class DemoDataIOObject extends ResultObjectAdapter {
private static final long serialVersionUID = 1725159059797569345L;
private DemoData data;
public DemoDataIOObject(DemoData data) {
this.data = data;
}
public DemoData getDemoData() {
return data;
}
@Override
public String toString() {
return "Custom data specific string";
}
}
This class wraps an object of the class DemoData
as an IOObject
(through the ResultObjectAdapter
), thus making it possible to pass it between Altair RapidMiner operators and storing it in the Altair RapidMiner repository. By overwriting the toString
method of ResultObjectAdapter
already allows for simple visualization of data provided by the class inside Altair RapidMiner. More advanced visualizations are also possible by creating complex renderers (see below).
While it might be more complex to implement in real-world applications, this example helps to illustrate how things work in general. You want to extract attribute values from the demo data, which an operator can then store in a table. You need a mechanism to add data to the IOObject. For simplicity, assume you only have numerical attributes to save the effort of remembering the correct types of the data. Add a map for storing the values with an identifier as a local variable:
private Map<String, Double> valueMap = new HashMap<String, Double>();
Then we extend the DemoDataIOObject with two methods for accessing the map:
public void setValue(String identifier, double value) {
valueMap.put(identifier, value);
}
public Map<String, Double> getValueMap() {
return valueMap;
}
Configuration
To make your data object accessible, adapt the file ioobjectsNAME.xml
(in the resources folder), which contains some examples of how to define your IOObject
.This is how the DemoDataIOObject
is defined:
<ioobjects>
<ioobject
name="DemoDataIOObject"
class="com.rapidminer.operator.demo.DemoDataIOObject"
reportable="false"
icon="data.png">
<renderer>com.rapidminer.gui.renderer.DefaultTextRenderer</renderer>
</ioobject>
</ioobjects>
The ioobject
needs a name
parameter for identifying the object inside Altair RapidMiner, the path to the class
we just defined for the new object, as well as an option (reportable
) whether the object can be used by the Reporting Extension or not. Adding the optional attribute icon
allows to define the icon displayed next to the new data objects visualization in the result view tab of the renderer output. By setting the renderer visualization behavior can be defined. Shown is a simple text renderer, which calls the toString()
method of your IOObject
to display the object in the Results
perspective. Writing more complex renderers is described below.
Similarly to operators, you can also give your own data objects a color. The connection between two operators that exchange your data object will be colored in the assigned color. To control colors, change the file groupsNAME.properties
in the resources folder to add a line defining the color:
# IOObjects
io.com.rapidminer.operator.demo.DemoDataIOObject.color = #28C68C
Processing your own IOObjects
Using these methods, you can now implement your first operator, which extracts values of the DemoData
.
import java.util.List;
import com.rapidminer.operator.Operator;
import com.rapidminer.operator.OperatorDescription;
import com.rapidminer.operator.OperatorException;
import com.rapidminer.operator.ports.OutputPort;
/**
* Operator that generates {@link DemoData}
*/
public class GenerateDemoDataOperator extends Operator {
private OutputPort outputPortDemoData = getOutputPorts().createPort("demo data");
/**
* Operator to create {@link DemoData}
*
* @param description of the operator
*/
public GenerateDemoDataOperator(OperatorDescription description) {
super(description);
getTransformer().addGenerationRule(outputPortDemoData, DemoDataIOObject.class);
}
@Override
public void doWork() throws OperatorException {
DemoDataIOObject ioobject = new DemoDataIOObject(new DemoData());
List<Double> values = ioobject.getDemoData().getValues();
for (int i = 0; i < values.size(); i++) {
ioobject.setValue("" + (i + 1), values.get(i));
}
outputPortDemoData.deliver(ioobject);
}
}
The example fetches the values from the DemoData
object and sets the values of the IOObject
. Then, the output port delivers the DemoDataIOObject
, thus making it available for other operators or connecting it to other ports in general.
Of course, it's possible to build more complex constructions. You might, for example, use one super operator that handles your DemoData
with different inner operators. You could build operators that get DemoData
as input and transform them to a table. Every method of treating your own IOObjects is possible by combining what we have learned.
Looking into your IOObject - Custom Renderers
When building a process for your IOObjects
, it's an excellent idea to set breakpoints with the process and take a look at what's contained in the objects. To continue with the example above, it would be interesting to see which values have been extracted. If you set a breakpoint, Altair RapidMiner will display the result of the toString
method as the default fallback.
There is plenty of space you could fill with information about the object. How can you do this? The simplest approach is to override the toString
method of the IOObject
. However, it is better to override the toResultString
method, which, by default, only calls the toString
method.
Although text output has its advantages, writing Courier characters to the screen is a bit outdated. How do you add nice representations to the output as is done with nearly all core Altair RapidMiner IOObjects
?
Altair RapidMiner uses a renderer concept for displaying the various types of IOObjects
. So, you should implement a renderer for your DemoDataIOObject
.
You must implement the Renderer
interface for this purpose. Extend the AbstractRenderer
, which has most of the methods already implemented. Most of the methods are used for handling parameters, since renderers might have parameters, just as operators do. They are used during automatic object reporting and control the output. The handling of these parameters and their values is done by the abstract class, you just need to take their values into account when rendering. Here is the example code of a renderer for our DemoData.
import java.awt.Component;
import com.rapidminer.gui.renderer.AbstractRenderer;
import com.rapidminer.operator.IOContainer;
import com.rapidminer.report.Reportable;
public class DemoDataRenderer extends AbstractRenderer {
public static final String RENDERER_NAME = "demo_data_renderer";
@Override
public String getName() {
return RENDERER_NAME;
}
@Override
public Component getVisualizationComponent(Object renderable, IOContainer ioContainer) {
// TODO Auto-generated method stub
return null;
}
@Override
public Reportable createReportable(Object renderable, IOContainer ioContainer, int desiredWidth, int desiredHeight) {
// TODO Auto-generated method stub
return null;
}
}
The renderer has a name, defined by the getName() method. Best practice is to use a public static field variable to define the name of the renderer, as it is done by the RENDERER_NAME
field in the example.
The renderer name is used also as a key in configuration files, as we will see later.
Besides defining the renderer name, two methods have to be implemented. The method createReportable must return an object, that implements one of the sub interfaces of Reportable
. This is used if you want to report the object in anyway, e.g. in the Reporting Extension. Please have a look at the interfaces and some of the implementations in the core.
If you only want to visualize your object in the results panel of Altair RapidMiner you can safely return null here. But then you should implement the toString() method of the underlying IOObject
.
The second method getVisualizationComponent returns an arbitrary Java component used for displaying content in Swing. You have all the flexibility of Swing, as long as you return a Java component.
There are also some abstract subclasses of the AbstractRenderer
in Altair RapidMiner which you can use for the visualization. For example the AbstractTableModelTableRenderer
. As the name indicates, it shows a table based upon a table model. All you need to do is to return this table model:
import java.util.ArrayList;
import java.util.List;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import com.rapidminer.gui.renderer.AbstractTableModelTableRenderer;
import com.rapidminer.operator.IOContainer;
import com.rapidminer.operator.demo.DemoDataIOObject;
import com.rapidminer.tools.container.Pair;
public class NewDemoDataRenderer extends AbstractTableModelTableRenderer {
public static final String RENDERER_NAME = "demo_data_renderer";
@Override
public String getName() {
return RENDERER_NAME;
}
@Override
public TableModel getTableModel(Object renderable, IOContainer ioContainer, boolean isReporting) {
if (renderable instanceof DemoDataIOObject) {
DemoDataIOObject object = (DemoDataIOObject) renderable;
final List<Pair<String, Double>> values = new ArrayList<>();
// Fill the table value variable 'values' with entries from the DemoDataIOObject
for (String key : object.getValueMap().keySet()) {
values.add(new Pair<String, Double>(key, object.getValueMap().get(key)));
}
return new AbstractTableModel() {
private static final long serialVersionUID = 1L;
@Override
public int getColumnCount() {
return 2;
}
@Override
public String getColumnName(int column) {
if (column == 0) {
return "Id";
}
return "Value";
}
@Override
public int getRowCount() {
return values.size();
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
Pair<String, Double> pair = values.get(rowIndex);
if (columnIndex == 0) {
return pair.getFirst();
}
return pair.getSecond();
}
};
}
return new DefaultTableModel();
}
}
There are other convenience methods in the AbstractTableModelTableRenderer
for changing the appearance of the table. For example, the following methods change the behavior of the table by enabling or disabling certain features:
@Override
public boolean isSortable() {
return true;
}
@Override
public boolean isAutoresize() {
return false;
}
@Override
public boolean isColumnMovable() {
return true;
}
To use the new renderer, you must change the file ioobjectsNAME.xml
in the resources folder again. Just add the renderer before the default renderer:
<ioobjects>
<ioobject
name="DemoDataIOObject"
class="com.rapidminer.operator.demo.DemoDataIOObject"
reportable="false"
icon="data.png">
<renderer>com.rapidminer.gui.renderer.demo.DemoDataRenderer</renderer>
<renderer>com.rapidminer.gui.renderer.DefaultTextRenderer</renderer>
</ioobject>
</ioobjects>
You can now see the result of your efforts in building a table representation of the DemoData values.
You can add as many customized renderers as you want. Each one will appear as a renderer tab in the list of the left side of the panel.
You can further customize the appearance of your renderer by giving it a title, a tooltip and its own icon.
This is done in the GUI<NAME>.properties
file in the i18n
folder of your extension resources.
gui.cards.result_view.<renderer_name>.icon = <icon>.png
gui.cards.result_view.<renderer_name>.title = Demo data
gui.cards.result_view.<renderer_name>.tip = Shows the values of the DemoData as a table representation.
Use the same string as you specified in the RENDERER_NAME
field of your renderer class. Only icons with 32x32 pixels are supported. You can put your own icons in the corresponding folder of your extension or you can use the icons provided in the 32 pixels folder in Altair AI Studio Core.
Other useful GUI classes in the core are the following:
RapidMinerGUI
: gives you access to the Mainframe and other relevant structures of the GUI
DataViewerTable
: A useful renderer to visualize example sets.
ExtendedJTable
: Extends the JTable class and adds functionality like sorting, row highlighting etc.
ExtendedJScrollPane
: Improves scrolling behavior.
RendererService
: Gives you access to already existing renderers
Some more tips to adapt the look of your renderer to the look of Altair AI Studio:
- Set borders to null if possible, especially borders of scroll panes
- Most renderers should come with a white background -- tables and scroll panes often have children whose background color needs to be changed to white as well
- Most renderers provide some empty borders, e.g. with:
tableScrollPane.setBorder(BorderFactory.createEmptyBorder(15, 10, 10, 10));
- The IOContainer object, which is provided when the getVisualizationComponent method is called, gives you access to all other
IOObjects
available at the moment. This can be used if the rendering depends on properties of other objects.