Thursday, December 02, 2010

BIRT Background/Watermark Images

Back in September I wrote a post about the BIRT Image report item. In that post I did not say much about creating background or watermarks for BIRT reports. In this post I will cover a few ways this can be accomplished.

One of the simplest approaches is to just add your watermark/background image to the resource folder that you have configured in the designer. Note that you will also need to add the image to the resource folder of the deployed environment. Once you have the image in the resource folder, select the style icon in the outline view and create a new style. Set the background image for the new style to the image you added to the resource folder. Next select the master page tab in the report editor and then right click on the canvas background and apply the new style.



The watermark can also be made to be dynamic. For example assume you want one watermark for pdf output and one for html. To do this you could add the second watermark to your resource folder and then use the following beforeFactory script to swap the image.



if( reportContext.getHttpServletRequest().getParameter("__format") == "pdf" ){
reportContext.getDesignHandle().findStyle("BackgroundStyle").backgroundImage = "watermarkpdf.png";
}else{
reportContext.getDesignHandle().findStyle("BackgroundStyle").backgroundImage = "watermarkhtml.png";
}




There is one drawback when using this approach. Master page content is always created in the generation phase of the BIRT pipeline, which means if you use the BIRT Viewer and export the report to another format the above code will not work. This is because the frameset mapping in the Viewer generates an rptdocument that can be rendered in additional formats without the need to re-execute the report. If the report is not re-executed the master page background image cannot be changed.

Another option which will facilitate exporting the report is to use a grid item instead of the master page for the watermark. For example assume there is one table in the report design. You can add a grid to the report that has one column and one cell. Set the height for the grid to be the size of your page (eg 11 in). Drag the table into the cell. Select the grid row and apply your background style.



Using the following onRender script of the grid row will allow you to change the style image at render time.



if( reportContext.getHttpServletRequest().getParameter("__format") == "pdf" ){
this.getStyle().backgroundImage = "watermarkpdf.png";
}





The examples used in this post are available on Birt-Exchange.

Monday, November 29, 2010

Last Day For EclipseCon Submissions

Tuesday is the last day for EclipseCon submissions.  This is a great time for all of you to share your experiences working with BIRT.

Thursday, October 21, 2010

BIRT Duplicate Rows

Currently BIRT supports suppressing duplicate columns values. To do this you can select the table column and in the general properties click the Suppress duplicates checkbox.



This will have an effect similar to the following image.


You will notice that the number of rows generated in the table has not changed, but no order number is repeated. If your table does contain duplicate rows, meaning each column in the table has the same data as the row before and you suppress duplicates on each column, the number of rows displayed by BIRT will be reduced. For example assume your table is tied to a dataset that has two columns and every row contains the same data.



If this dataset is bound to a table and both columns are set to suppress duplicates the following will be displayed.



The actual number of rows is not reduced but the table only shows the value for one row. In the generated HTML a TR element is created for each empty row with empty values. If you use this approach make sure to set the padding on the table row, cell, and data items to 0 or you will get unwanted space for empty rows. It is also important to realize that these empty rows are counted if you are use the page break interval property to handle page breaks. As a side note, when dealing with duplicate rows it is often better to allow the database to handle culling repeated rows, but in some cases like flat file data sources this may not be an option.

Another option that can be used to hide duplicate rows is scripting and a visibility expression.

Assume we have a table that shows order numbers. If we only want a particular order number to be shown once, we obviously could use a distinct on the query. If that is not an option you can use a script and a visibility expression to implement this requirement. On the table row’s onCreate method you could enter a script like:



var rowLast = this.getRowData().getColumnValue("ORDERNUMBER");
reportContext.setGlobalVariable("rl", rowLast);


And a visibility expression like:



if( row["ORDERNUMBER"] == reportContext.getGlobalVariable("rl") ){
true;
}else{
false;
}


This will cause the BIRT engine to only show the row if its current row order number is different from the previous row.



As with the previous examples and all visibility operations the underlying data is not altered. It is just not displayed. For example if you hide a table the dataset still executes. If you want to alter the underlying data you will need to filter the data either on the table or the actual dataset. This can be difficult when trying to get a rows previous value so I created a simple aggregate extension Plugin that will do this for you. It is based on Scott’s post on Optimistic sums. The extension point has been revised slightly since that post so take a look at the attached examples to see the source code. The extension point adds a previous value aggregate function to BIRT. To use it you can create a computed column on the dataset.



The function stores the last row value as entered in the Column expression. Once the computed column is added to the dataset you can then select the filters tab and enter a filter like:


This will filter the dataset as shown below.



The source for this post is available at Birt-Exchange. The download contains two reports (one that uses the aggregate function and one that was demonstrated earlier in the post), the exported new aggregate plugin, and a source project for the plugin.

Friday, October 15, 2010

BIRT Java Object Data Type

In addition to simple data types BIRT also supports a Java Object Data type. This type can be useful when writing reports that use scripted data sources that consume POJOs that contain a member object. For example assume you have an array of objects and each of these objects contains three member variables (a String, an Integer, and an Object).




Before BIRT supported the Java Object data type you could write a scripted data source that iterated the array and assigned a row variable for the string member and another for the integer variable. If you wanted to use the Object member you would need to call some method on the object that returned a BIRT primitive type. While this is generally not a drawback with a simple table, if you wanted to nest tables and use the inner object in another dataset, scripting this became very complex. Using the Java Object data type now makes this much easier. The only caveat to using the Java Object data type is that the object must be serializable. The reason for this is that BIRT caches datasets and the object will be cached just like any other column value.

To illustrate using the Java Object type, consider the following classes. The BIRTOuterJavaObject contains string, integer, and object member variables.



package test.birt.javaobject;

import java.io.Serializable;

public class BIRTOuterJavaObject implements Serializable {


private static final long serialVersionUID = 6530113212317618087L;
private String outerstring;
private int outerint;
private BIRTInnerJavaObject innerobj;

public String getOuterstring() {
return outerstring;
}

public void setOuterstring(String outerstring) {
this.outerstring = outerstring;
}

public int getOuterint() {
return outerint;
}

public void setOuterint(int outerint) {
this.outerint = outerint;
}

public BIRTInnerJavaObject getInnerobj() {
return innerobj;
}

public void setInnerobj(BIRTInnerJavaObject innerobj) {
this.innerobj = innerobj;
}

public BIRTOuterJavaObject() {

}
}



The BIRTInnerJavaObject is just a sample object that stores information about an automobile. It contains three member variables which are all strings.



package test.birt.javaobject;

import java.io.Serializable;

public class BIRTInnerJavaObject implements Serializable {

private static final long serialVersionUID = -4509250036928470517L;


private String make;
private String model;
private String year;
public String getMake() {
return make;
}
public void setMake(String make) {
this.make = make;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public String getYear() {
return year;
}
public void setYear(String year) {
this.year = year;
}
public BIRTInnerJavaObject() {
this.year = "2009";
this.make = "Chevrolet";
this.model = "Corvette";
}
public String toString(){
return "Make:--"+this.make+" Model:--"+this.model+" Year:--"+this.year;
}
}


The BIRTRow class sets up the array of objects that will be used in the report.


package test.birt.javaobject;

import java.util.ArrayList;

public class BIRTRow {

private ArrayList rows = null;

public BIRTRow() {
rows = new ArrayList();
rows.clear();
}
public void setupRows(){

BIRTOuterJavaObject row1o = new BIRTOuterJavaObject();
BIRTOuterJavaObject row2o = new BIRTOuterJavaObject();
BIRTOuterJavaObject row3o = new BIRTOuterJavaObject();

BIRTInnerJavaObject row1i = new BIRTInnerJavaObject();
BIRTInnerJavaObject row2i = new BIRTInnerJavaObject();
BIRTInnerJavaObject row3i = new BIRTInnerJavaObject();


row1i.setMake("Toyota");
row1i.setModel("Land Cuiser");
row1i.setYear("2008");
row1o.setOuterint(1);
row1o.setOuterstring("Outer Row 1 String");
row1o.setInnerobj(row1i);
rows.add(row1o);

row2i.setMake("Jeep");
row2i.setModel("Cherokee");
row2i.setYear("2002");
row2o.setOuterint(2);
row2o.setOuterstring("Outer Row 2 String");
row2o.setInnerobj(row2i);
rows.add(row2o);

row3i.setMake("Land Rover");
row3i.setModel("LR3");
row3i.setYear("2006");
row3o.setOuterint(3);
row3o.setOuterstring("Outer Row 3 String");
row3o.setInnerobj(row3i);
rows.add(row3o);

}
public ArrayList getRows(){
return rows;
}
}



The primary goal of the example is to nest two tables. The outer table will have three columns. One column for each of the member variables of the outer object(BIRTOuterJavaObject). The Inner BIRT table will contain three columns as well, one for each of the member variables of the inner object(BIRTInnerJavaObject). Note that in this example the inner object could have contained a list of objects but for simplicity it only contains one object. Because the inner dataset will only return one row of data the example could have been completed using just one data set. So the report will contain two datasets one for the outer object and one for the inner object. The inner object dataset will be parameterized so that when we nest the inner table it can get the object from the outer dataset. The inner dataset will only return one row of data.



The outer dataset scripted data set code is as follows:
Open Method:



importPackage(Packages.test.birt.javaobject);
myrows = new BIRTRow();
myrows.setupRows();
rowsiter = myrows.getRows().iterator();
icnt = 0;


The open method sets up the array of objects and gets an iterator for the list.
Fetch Method:


if( rowsiter.hasNext() )
{
var obj = rowsiter.next();
row["OuterString"] = obj.getOuterstring();
row["OuterInt"] = obj.getOuterint();
row["InnerObj"] = obj.getInnerobj();
icnt++;
return true;
} else {
return false;
}


The fetch method iterates over each of the outer objects and assigns the column values. Note that the InnerObj column is of type Java Object and will be an instance of BIRTInnerJavaObject.


The inner dataset has three string columns.



The inner dataset also has a parameter that we will set when the table is nested. The parameter is named JavaObjectParam and its default value is set to null.



The inner data set code is as follows:

Open method:


importPackage(Packages.test.birt.javaobject);
if( inputParams["JavaObjectParam"] == null ){
icnt=1;
}else{
icnt=0;
}



The open method just checks to see if the dataset parameter is set. If the parameter is null the dataset will not return any rows. If the parameter is set the dataset will only return one row which is an instance of BIRTInnerJavaObject.

Fetch Method:


if( icnt > 0 )return false
var obj = inputParams["JavaObjectParam"];
row["Make"] = obj.getMake();
row["Model"] = obj.getModel();
row["Year"] = obj.getYear();
icnt++;
return true;



In the fetch method we get the object from the parameter and call the getter methods for the object and assign them to the column values.

Once both datasets are created, you can drag the outer dataset to the report canvas which will create the first table. Next add a column to the first table and drag the second dataset to the new column which should create the second (nested) table. To make the inner table work properly the dataset parameter has to be bound to the outer tables InnerObj column. To do this select the inner table and click the binding tab in the properties view. Click the data set parameter binding button and set the value of the dataset parameter to row[“InnerObj”].



The Java Object Data type can also be consumed by a chart. Continuing with this example if you add a chart and bind it to the outer dataset you can specify getter methods for the series, categories, optional grouping, tooltip help, etc.



The completed report should have similar output to the following.



This example is available at BIRT-Exchange as two projects. One Java project and one BIRT Report project. To run the example import both projects to your workspace, open the BirtJavaObjectReport.rptdesign and preview the report.

If you are using Actuate BIRT 11, The Java Object data type can be used in conjunction with the new POJO ODA data source that allows the Report Designer to access and report off of POJO’s using a GUI designer. If you are interested in this approach take a look at this blog post and this video that show the driver in action. An example of using this new data source is available on BIRT-Exchange and you can try out Acutate BIRT 11 by downloading it here.

Wednesday, September 22, 2010

Format Negative Numbers

Just a simple little tip this week. I wanted to format numeric data with a custom format. I wanted positive numbers to look like this relatively standard format

 1,233.00

But I also wanted negative numbers to show up like:

 1,245.00 -

With the negative sign showing to the right of the number. 

At first this was a  this is a bit of a problem.  I figured out a way to do it using OnRender script, but it was kind of ugly.  Turns out the solution is quite a bit more simple. BIRT is using the java DecimalFormat for numeric data in the CustomFormat.  One of the accepted patterns for DecimalFormat is in the form of

 PositivePattern ; NegativePattern


So all you need to do is select Custom Format and enter the format as (see below)



 #,##0.00; #,###.00 -

Monday, September 13, 2010

Open Source Reporting Comparison - BIRT vs. Pentaho vs. Jasper

This is a post by Scott Rosenbaum of Innovent Solutions. It does not necessarily reflect the view of any of the other BIRT project members.


One of the things that I often get asked is how does BIRT compare to BrandX? For the last six years, I have been creating solutions using BIRT, so the easy answer would be just use BIRT. Unfortunately it is not that simple.
Innovent Solutions' corporate goal is Enabling Intelligent Decisions® and we use a variety of commercial and open source tools to achieve that goal. Over the last fifteen years, experience has taught us:

  • Analysis, communication, and project management are the keys to BI and reporting success
  • Any sufficiently mature technology can be successful in the right situation 
So how does BIRT compare to BrandX? requires a bit more than just use BIRT. Most of the answer depends on the business issue that we are trying to solve. But we also have to evaluate if BrandX has reached the point of being sufficiently mature.
Over the last three years three open source business intelligence and reporting projects have emerged as leaders. BIRT, JasperReports, and Pentaho dominate the open source business intelligence and reporting space. In my opinion, BIRT reached the minimum maturity level with its 2.2 release in 2007, since then the product has continued to mature through three annual releases (2.3, 2.5, 2.6).
So what about Jasper and Pentaho, how do their features compare to BIRT, and are they sufficiently mature? In an effort to come up with a better answer, Innovent Solutions has conducted a product comparison of the open source report development tools from each project. 
The full evaluation is available on the the Innovent Solutions web site (free, no registration). The starting point for the product comparison can be found at Product Comparison Overview. If you are interested in specific comparisons, we have one for BIRT versus Jasper and another for BIRT versus Pentaho. We also created a side by side feature comparison the Open Source Comparison Matrix (BIRT, Jasper, Pentaho).
NOTE: I would love to hear what you think of the Innovent comparison, but I would like to keep the focus of this blog on BIRT. For this reason I have turned off comments for this post. Comments about the comparison should be made from the Product Comparison Overiew page on the Innovent Solutions web site.

Wednesday, September 08, 2010

Beyond BIRT Basics - Roadshow

The advanced BIRT Roadshows will begin next week in Boston, New York and DC.
These events feature a free day of training on advanced BIRT topics, like understanding the event model, chart and report item scripting, implementing high performance reports, and making BIRT mobile.

The schedule for the month of September is listed below.

Upcoming Accelerated BIRT Roadshows
Boston – Tuesday, September 14
New York – Wednesday, September 15
DC – Thursday, September 16
Bay Area – Tuesday, September 28

If you are interested, sign up quickly, as space is limited.

Thursday, September 02, 2010

BIRT Image - Report Item

BIRT has many ways to include images within a report. Images can be used in BIRT styles, as watermarks, in text elements, and placed within the report using an image report item. In this post I will cover some of the details needed to work with images that are inserted using the Image report item.

The image report item can retrieve images in four different ways. 1 -Through a URI, 2 – as an image in the resource folder, 3 – as an embedded image, 4 -or by using a dynamic image. Each of these methods is described below with examples. In addition some of the examples use onCreate scripts written in JavaScript. While these examples use JavaScript, they could also have been written in Java.



URI Images
The first way is to retrieve an image with the image report item is to use a URI specification. This method is pretty straight forward and the value can be entered as a constant or as a JavaScript expression. Constants are processed faster by the engine but are harder to make dynamic. An example of a constant expression for a URI image would be as follows:


http://www.eclipse.org/eclipse.org-common/themes/Nova/images/eclipse.png


Note: Do not put quotes around the expression unless JavaScript Syntax is selected. An example of using a JavaScript expression is presented below.


if( params["dynamicimage"].value == true ){
"http://www.google.com/intl/en_ALL/images/srpr/logo1w.png";
}else{
"http://www.eclipse.org/eclipse.org-common/themes/Nova/images/eclipse.png";
}
This expression checks the value of the dynamicimage report parameter, and then based on its value, sets the value of the URI.
The URI can also be set using an onCreate event handler for the image report item. The syntax for this approach would look like:
this.URI = "http://tomcat.apache.org/images/tomcat.gif";

//for a local file use:
//this.URI = "file://C:/test/birtlogo.png";


Resource Folder Images
BIRT uses a resource folder for storing report libraries, style sheets, images, jars, js files, properties files or virtually any file that you will need access to at runtime. While in the design environment the resource folder location can be set using the Windows->Preferences->Report Design->Resource setting. This can be set for the entire workspace or on a per project basis. At runtime you can set the resource folder in the web.xml if you are using the viewer. If you are using the engine API you can set the resource folder using the EngineConfig class’ setResourcePath method. When using a resource folder image, all that is needed is to specify the image name as it is defined in the resource folder. You can also set a JavaScript expression for the image name. For example:


if( row["QUANTITYORDERED"] > 30 ){
"green.png";
}else if( row["QUANTITYORDERED"] > 25 && row["QUANTITYORDERED"] <= 30){
"yellow.png";
}else{
"red.png";
}


This expression checks the row data to determine which resource folder image should be rendered. The same type of checks can be made if you are using the onCreate script event for the image element.


var myqty = this.getRowData().getColumnValue("QUANTITYORDERED");
if( myqty > 30 ){
this.file = "green.png";
}else if( myqty > 25 && myqty <= 30){
this.file ="yellow.png";
}else{
this.file="red.png";
}


One thing to note in the above is that the column value QUANTITYORDERED is the binding column name, not the dataset column name. See the binding tab on the table in the attached example.
Images can also be placed in jar files within the resource folder. If your image exists in a jar file, you can use a script expression similar to the following to specify the image to retrieve.


var jarfile = reportContext.getResource("birtimages.jar");
myfulljarimage = "jar:"+jarfile.toString()+"!/green.png";
myfulljarimage;


The getResource method of the reportContext object is used to return the location of a file in the resource folder. Using the location of the file and the jar protocol, the image can be specified.

Embedded Images
BIRT allows images to be encoded directly into the xml report design. Images can be added by right clicking on the embedded images icon in the outline view of the report and selecting “New Embedded Image”. After selecting the image, the outline view is updated and the data for the image is encoded in to the design. You can also add embedded images to the report using the add image button of the image report item editor.



Once the images are embedded into the report, you can add the image report item to the desired location, choose the embedded image radial, select the image name and click the insert button.



If you wish to change the image dynamically, this can be done using an onCreate script. In the onCreate script specify the image name using the imageName property.


this.imageName = "eyellow.png";



Dynamic Images
Dynamic images allow Blob images to be inserted into the report. Typically this type of image is tied to a data set column through either the image’s dataset bindings, or the container element’s bindings (eg Table). The sample database, which is delivered as part of BIRT, contains a Blob type column in the PRODUCTLINES table. The example report used in the post has an example of using this column in conjunction with the image report item.



A developer can also use an onCreate event script to set the image data. When doing this, the image data should be in a byte[]. Presented below is an onCreate script that uses the ImageIO class to read a file, a URL, an image from the resource folder, or an image in a jar file in the resource folder. Uncomment the section of the script for the desired image location.


importPackage(Packages.java.io);
importPackage(Packages.java.lang);
importPackage(Packages.java.net);
importPackage(Packages.javax.imageio);

//File Based
//var myfile = new Packages.java.io.File("c:/test/green.png");
//var img = ImageIO.read(myfile);

//URL Based
//Jar image in resource folder
var jarfile = reportContext.getResource("birtimages.jar");
var myfulljarimagestr = "jar:"+jarfile.toString()+"!/red.png";
var myurl = new Packages.java.net.URL(myfulljarimagestr);

//Image in resource folder
//var myurl = reportContext.getResource("green.png");

//Image at url
//var myurl = new Packages.java.net.URL("http://www.eclipse.org/eclipse.org-common/themes/Nova/images/eclipse.png");

var img = ImageIO.read(myurl);
bas = new ByteArrayOutputStream();
ImageIO.write(img, "png", bas);
this.data = bas.toByteArray();





The example used for this post is available at Birt-Exchange. To setup the example, copy the birtimages.jar and the three supplied images to your resource folder.

Friday, August 20, 2010

BIRT Flash and Gadget Scripting

In my last post I blogged about how Chart Scripting works in BIRT. If you are using Actuate BIRT, you also will have access to Flash Charts and Gadgets. Scripting for these two report items is very similar to the standard Chart scripting model.



Flash Charts
Currently Flash charts support 14 script event triggers that can be hooked to make chart modifications.



The event firing order is shown below.



As you can see the order is very similar to that of normal BIRT charts. The before and after dataset filled events are fired first for each dataset (category and value). Next the beforeRendering event is fired and then the before and after draw series events are fired for each series. Between the before and after draw series events the before and after draw data point events are fired for each point in the series. The before and after draw series events (and the events contained in them) are fired for each series in the chart. After all series are processed, the before and after draw effect events are fired for each effect in the chart. Effects can be used to alter the chart at runtime, allowing font manipulation, blurring, glowing, shadowing, beveling, and animation of different objects of the chart. Effects are added to the chart using the effects editor.



Using the beforeDrawEffect event script you can alter any of the effects added to the chart. For example to turn off the shadow effect in the above chart you could use the following script.



function beforeDrawEffect( effect, fcsc )
{
var effectName = "MyTitleEffect";
if ( effectName.equals( effect.getAction().getName() ) ){
if( effect.getAction().getShadow() != null ){
effect.getAction().getShadow().setEnabled(false);
effect.getAction().getFont().setEnabled(false);
}
}
}



The getEffect().getAction().getName() call returns the name of the effect as defined in the effect editor. This can be used to determine which effect definition your script is operating on. The getAction allows access to the different effect features. Note that if the effect is not enabled the getter method for the effect will return null. In the above script we first check to see if the getShadow method returns null before turning it off.

Finally the afterRendering event is triggered.

These events function much the same way as described in my earlier post. You will notice that event function prototypes are just a little different. For example instead of a chart and icsc parameter you will get a flashChart and fcsc parameter in certain events. For practical purposes these are just extended chart and icsc objects and still provide most of the standard object features. For example to set the chart title to the value of a report parameter using the reportContext, you could use a script like the following.



function beforeRendering( flashChart, fcsc )
{
var parmChartTitle = fcsc.getExternalContext().getScriptable().getParameterValue("MyChartTitle");
flashChart.getChart().getTitle().getLabel().getCaption().setValue(parmChartTitle)
}



Flash Gadgets
BIRT Flash Gadget report items are used to display data in a unique dynamic graphic. Currently Actuate BIRT supports thermometer, cylinder, bullet, spark line, meter gauge or linear gauge gadgets. These gadgets also support server side scripting.



Twelve event triggers are supported and can be used to alter how the gadget is displayed. The events that are triggered will depend on the gadget and which features the gadget is using. For example this meter gadget:



Produces the following event order.



The beforeRendering event is called first and can be used to modify the flash gadget before it is rendered. Next before and after draw region events are trigger for each of the three regions in the meter gadget. This gadget contains two thresholds, so the before and after threshold events are triggered next for each threshold. The gadget also contains two needles so the before and after draw needle events are fired. The threshold label contains a font effect and the threshold areas are animated. This produces before and after draw effect triggers for each. Finally the afterRendering event is triggered.

Retrieving Gadget Values
BIRT Flash Gadgets use value definitions to link the gadget to a BIRT dataset. The definitions are assigned in the Gadget Editor. The runtime values for these definitions can be retrieved in script using the gadget object. For example, in the beforeRendering you could use the following script:


function beforeRendering( gadget, fgsc )
{
var vals=gadget.getResultValues();
for( i = 0; i < vals.size(); i++ ){
if( vals.get(i).getValue() > 20 ){
//do something
}
}
}


The number of result values will depend on the gadget. For example, In a Spark Line gadget there will be multiple values. In a meter gadget, there will be a result value for each needle value.

Region Events
You can customize the colors, transparency, labels, and range values of any region using the beforeRendering or beforeDrawRegion scripts. See the example below.


function beforeDrawRegion( region, fgsc )
{
if( region.getLabel().equals("A") ){
region.setColor("00ff00");
}
//Display value range of region.
region.setLabel( "(" + region.getStartValue() + ", " + region.getEndValue() + ")" );

}

function beforeRendering( gadget, fgsc )
{
//First Region
gadget.getRegion().get(0).setEndValue(25);
//Second Region
gadget.getRegion().get(1).setStartValue(25);




Threshold Events
Thresholds, like regions can also be customized using script. Virtually anything that is set in the designer can be changed in an event trigger. For example:


function beforeDrawThreshold( threshold, fgsc )
{
if( threshold.getLabel().equals("MyThreshold") ){
threshold.setShowValueInside(true);
threshold.setShowValueOnTop(false);
threshold.setStartValue(10);
threshold.setColor("0000ee");
// Display the value range of threshold.
threshold.setLabel( "(" + threshold.getStartValue() + ", " + threshold.getEndValue() + ")" );
}
}

function beforeRendering( gadget, fgsc )
{
gadget.getThresholds().get(0).setMarkerColor("ff0000");
}


In these scripts, we change the starting value, color, label and label location in the beforeDrawThreshold. The marker color is also changed in the beforeRendering script.

AddOn Events
BIRT Gadgets support the concept of an AddOn. An AddOn allows the designer to place images, text, and various shapes on top of the gadget. If the gadget contains an AddOn the before and afterDrawAddOn events will be triggered, allowing you to customize the AddOn at runtime. For example, the following script customizes two AddOns.


function beforeDrawAddOn( addOn, fgsc )
{
// Change add-on styles.
if ( addOn.getName().equals("TitleAddOn") )
{
addOn.setLabel( "Meter Gauge" );
addOn.setFontColor( "ff0000" );
addOn.setFontSize( 28 );
addOn.setY( 35 );
}
else if ( addOn.getName().equals( "ArcAddOn" ) )
{
addOn.setFillColor( "ffff00" );
}
}


AddOns can even be created and placed on the gadget at runtime. For example the following script adds a text AddOn to the gadget using the beforeRendering event.


function beforeRendering( gadget, fgsc )
{
importPackage( Packages.com.actuate.birt.flash.gadgets );
importPackage( Packages.com.actuate.birt.flash.gadgets.impl );

// Add a text add-on.
var textAddOn = GadgetsFactory.eINSTANCE.createTextAddOn();
textAddOn.setAddOnType( AddOnType.TEXT );
textAddOn.setName("DescAddOn");
textAddOn.setShowAddOn(true);
gadget.getAddOns().add( textAddOn );
gadget.setZOrderPosition(1);

textAddOn.setLabel("This sample demonstrates how to add an add-on by scripting.");
textAddOn.setX(2);
textAddOn.setY(20);
textAddOn.setFontSize( 12 );
textAddOn.setHorizontalAlignment( HorizontalAlignmentType.LEFT );
textAddOn.setVerticalAlignment( VerticalAlignmentType.TOP );
textAddOn.setWrap( true );
textAddOn.setTextBoxBorderColor("000000"); //Black
textAddOn.setTextBoxBackgroundColor("c0c0c0");
textAddOn.setWrapMaxWidth( 30 );
textAddOn.setWrapMaxHeight( 50 );

}


Needle Events
If your gadget contains a needle, the before and afterDrawNeedle events will be triggered. Using script the needle can be customized. The size, color, shape, and various other properties of the needle can be customized. These events are passed a NeedleWrapper instance, which can be used to get the specific needle type. For example the following two scripts differentiate between a normal gadget needle and a meter gadget needle.


function beforeDrawNeedle( needleWrapper, fgsc )
{
importPackage( Packages.com.actuate.birt.flash.gadgets );
importPackage( Packages.com.actuate.birt.flash.gadgets.impl );
// Change needle styles.
needleWrapper.getNeedle().setShape( Shape.DIAMOND );
needleWrapper.getNeedle().setFillColor("2cf83a");
needleWrapper.getNeedle().setBorderColor("007f00");
}

function beforeDrawNeedle( needleWrapper, fgsc )
{
//Set Needle size to 25%
needleWrapper.getMeterNeedle().setSize(25);
needleWrapper.getMeterNeedle().setBackgroundColor("ff0000");
}



Effect Events
The effect events were described above in the flash charting section.

Several examples used in this post are available at BIRT-Exchange.

Monday, August 16, 2010

BIRT For Perforce

I recently stumbled across a project that has BIRT reports about Perforce.  It looks like if you are using Perforce for your version control, you can now use BIRT reports to gain insight into that system.

http://public.perforce.com/wiki/BIRT_Reports

Thursday, August 12, 2010

BIRT Charting – Scripting Overview

The BIRT Chart Engine currently supports building and running charts within the BIRT designer or externally. The engine is fully extensible using the Eclipse extension mechanisms and supports client side interactivity and server side event scripting. Fourteen main chart types, such as bar charts, line charts, and scatter charts are supported. Additionally most chart types support many subtypes such as stacked bar charts, 2D with depth, or 3D charts. When using the chart engine within BIRT the charts can be rendered as PNG, JPG, BMP, or SVG.

The chart engine also supports a sophisticated server side event model that allows the developer to customize the charts dynamically. This post is an overview of most of the chart scripting events available.

If you are interested in learning more about client side chart scripting, see these posts:
ClientSide Script:
Calling Client Side JavaScript from a BIRT Chart
More on Chart Interactivity

General BIRT Server Side Scripting
BIRT Chart event scripting is based on the standard BIRT report scripting model. A primer for report scripting is available here. When the BIRT report engine processes reports they are executed in two phases (Generation and Presentation). The generation phase connects to data sources collects and processes the values and creates the report items. The presentation phase renders the content to the particular output (HTML, paginated HTML, PDF, PPT, XLS, DOC, or PS). These phases can be executed in one or two processes, depending on how you call the engine.

If you use the Report Engine API (RE API), the RunAndRenderTask is one process. To run it in two processes you would use a RunTask and then a RenderTask. If you are using the BIRT Viewer, the /Run and /Preview mappings use one process and the /frameset mapping uses two processes. This concept is very important to understand when scripting as it affects the order in which the events are triggered. When using one process the event order for a data item would look something like:

1st instance
onPrepare
onCreate
onRender
2nd instance
onCreate
onRender.

The same data item with two processes would look like:

1st instance
onPrepare
onCreate
2nd instance
onCreate

onRender first instance
onRender second instance

Using two processes offers many advantages. First a binary file called the report document (.rptdocument) is created which can be rendered many times at a later date without re-executing all the queries. It is also required to support TOC bookmarks and paginated HTML.

Chart Server Side Scripting
When using charts in a BIRT report, the engine supports over thirty events. These events can be used to change the data, interactivity, or presentation of the chart. All of these events are fired in the presentation phase (at render time) of the report and can be written in Java or JavaScript.

It is possible to change chart properties at generation time, but these changes must be made in a report event that happens prior to the chart being created, like the beforeFactory event. This approach is described here and here.

It is vital to understand when chart events are fired, if you plan on making script changes to your chart. Take for instance the following chart.



This is a standard Bar chart with a marker line and curve fitting line. If two processes are used to generate the report that contains this chart, the following event firing order will be used.




This diagram shows most of the available events, but not all events are fired for every chart type. For example before and afterDrawMarker events are only fired for chart types that support makers and these events are usually fired in between the before and afterDrawDataPoint scripts. The beforeDrawSeriesTitle event only fires for Pie charts.

Notice the chart events do not start until after the beforeRender report level event.

Chart Script Context
Before getting into each of the events, a good understanding of the chart script context object will be helpful. All BIRT Chart events are passed a chart script context object. This object will be labeled icsc in the function definition. You can use the chart script context object to get the current instance of the chart model by calling:


icsc.getChartInstance();



You can also use it to get the reportContext that is associated with all other report scripting events. See the scripting primer referenced above for more details. This object can be used to get parameter values, resource values, locales, the current http request, global variables, page variables, etc. To access these functions with a JavaScript event use the following:


rpCtx = icsc.getExternalContext().getScriptable();


This is equivalent to the reportContext variable. So to get a report parameter value, use:


rpCtx.getParameterValue(“MyParameter”);


or to access the http request use (this only works in the Viewer unless you put the http request object in the report engines App Context):


rptCtx.getHttpServletRequest().getRequestURL();


If you are using a Java Event handler use:


IReportContext rpCtx = (IReportContext)icsc.getExternalContext().getObject();


Data Set Filled Events
The first set of chart events that fire are the data set filled events. These events are fired when data organizing and generation of runtime series is occurring for the chart. In These events you can change many properties of the chart model, such as series expressions, grouping, labels, formatting, and series visibility. These events are fired once for the category series and once for each runtime series that is generated based on how the chart is designed. Generally you will get one runtime series per design time series and one additional one for the category series. So if you define a bar chart with product code on the x-axis and quantity ordered on the y-axis, this would produce two runtime series. Each series would contain an array of values. For example ProdA, ProdB, and ProdC may be the category series and 55, 98, and 32 may be the value series. If the chart also contained a line series then this would constitute on additional runtime series containing its values. If you use the optional grouping feature this could generate many runtime series, depending on the number of groups the optional grouping expression produced. In the above example, if the following expression was entered for the optional grouping.


if( row["QUANTITYORDERED"] > 50 ){
"high";
}else{
"low";
}


This would produce a total of three runtime series. 1 (ProdA,ProdB,ProdC), 2 ( 55, 98, null) and 3 (null ,null, 32).

Note that all series have to have the same number of points. In the afterDataSetFilled script these values can be check and or modified.

beforeGeneration Event
The beforeGeneration event is executed before chart building begins and after all series data points have been created. You can make changes to the chart model here and can even alter the datasets that the chart uses. In the data set filled events, many runtime series may have been generated. Every series in a chart will have a unique series identifier. You can specify this on the third tab of the chart wizard. If you use optional grouping and many runtime series are generated for each design time series, a new series identifier will be created for each runtime series. To interrogate these series use code similar to the following (Chart With Axis Example):



var xAxis = chart.getAxes().get(0);
var yAxis = xAxis.getAssociatedAxes().get(0);
var xSeriesDef = xAxis.getSeriesDefinitions().get(0);
var ySeriesDef = yAxis.getSeriesDefinitions().get(0);

var ySeries = ySeriesDef.getRunTimeSeries();
var numberofrunseries = ySeries.size();
//get the first runtime series identifier
ySeries.get(0).getSeriesIdentifier();



Computation Events
The before and afterComputations events are called next. These events are called just before and right after all the calculations for the chart’s rendering properties are processed. These computations include all bounds objects for the chart, the series rendering hints, and data point hints, which store the plot location and values for each series. Using these scripts these values can be modified before the chart is actually rendered.

afterGeneration/beforeRender Event
These events are fired after the chart is built but before it renders each part of the chart. When the chart engine is building it stores all information in the GeneratedChartState object. This object is passed to these methods and can be used to make changes before the chart is rendered. You can use the following code to get the plotComputations object that was populated in the computations event:


compu =gcs.getComputations();


You can even change how the chart engine processes interactivity triggers. For example assume you have a class that implements the IActionRenderer interface.



package my.action.handler;
import java.io.Serializable;
import org.eclipse.birt.chart.computation.DataPointHints;
import org.eclipse.birt.chart.event.StructureSource;
import org.eclipse.birt.chart.event.StructureType;
import org.eclipse.birt.chart.model.attribute.ActionType;
import org.eclipse.birt.chart.model.attribute.TooltipValue;
import org.eclipse.birt.chart.model.data.Action;
import org.eclipse.birt.chart.render.ActionRendererAdapter;
public class MyActionHandler extends ActionRendererAdapter implements Serializable{

private static final long serialVersionUID = 1169308658661902989L;

public void processAction( Action action, StructureSource source )
{
if ( ActionType.SHOW_TOOLTIP_LITERAL.equals( action.getType( ) ) )
{
TooltipValue tv = (TooltipValue) action.getValue( );
if ( StructureType.SERIES_DATA_POINT.equals( source.getType( ) ) )
{
final DataPointHints dph = (DataPointHints) source.getSource( );
String MyToolTip = "My Value is " + dph.getDisplayValue() + "--" +dph.getBaseDisplayValue();
tv.setText( MyToolTip );
}
}
}
}


You could instantiate an instance of this class in the beforeFactory event of the report:


importPackage(Packages.my.action.handler);
MyActionRenderer = new MyActionHandler();
reportContext.setPersistentGlobalVariable("myactionrenderer", MyActionRenderer);


And then set it in the beforeRender event like:


mar = icsc.getExternalContext().getScriptable().getPersistentGlobalVariable("myactionrenderer");
gcs.getRunTimeContext().setActionRenderer(mar);



DrawBlock Events
Charts in BIRT are made up of rendering areas called blocks. Each block will have properties such as rendering bounds, anchor points, outline properties, background properties, and child blocks. The whole chart is the main chart block and within this block is the title block, the plot block and the legend block.



The before and afterDraw block events are called multiple times, once for each chart part that constitutes a block.

The before and afterDraw block events are first called for the whole chart, the title block, plot block, and finally for the legend block. Nested within the plot block and legend block other events will fire as illustrated in the event diagram. The plot block events are fired once for each runtime series as well. So if you have 3 runtime series, the before and afterDrawBlock event will be called three times for the plot block.

To know which block the event is firing for you can use a script similar to:


if ( block.isLegend( ) )
{
block.getOutline( ).setVisible( true );
block.getOutline( ).getColor( ).set( 21,244,231 );
block.setBackground( ColorDefinitionImpl.YELLOW( ) );
block.setAnchor( Anchor.NORTH_LITERAL );
}
else if ( block.isPlot( ) )
{
}
else if ( block.isTitle( ) )
{
}
else if ( block.isCustom( ) ) //Main Block
{
}


You can use these events to modify the calculated bounds of any block, but this may result in unattractive charts. To change the location of the legend the following script could be used.


if (block.isLegend())
{
var wid = block.getBounds().getWidth();
var lft = block.getBounds().getLeft();

block.getBounds().setLeft(lft-20.00);
block.getBounds().setWidth(wid+20.00);
}


Marker Range and Line Events
The before and afterDrawMarker range and line events are called before any of the series are drawn. These events are only called for charts with axis that actually contain marker ranges (or lines). Using these events the marker ranges (or lines) can be modified. By default the location of the markers are set in the chart builder, but you can set the location using an index like:


markerLine.setValue(NumberDataElementImpl.create(intcountformaker));


The index is associated with the runtime series data point index and not the data point value. If the index is set to 0 the marker will be set to the first data point. You can use this in combination with other events like the afterDataSetFilled event to make the marker dynamic.

Draw Series Events
For each runtime series that is generated, the before and afterDraw series events will be fired. You can use the series identifier to determine which series is being processed. You can also check the specific type that of series that is being rendered.


if( series.getClass() == LineSeriesImpl ){
series.getLineAttributes().setThickness(5);
}
if( series.getClass() == BarSeriesImpl ){
if( series.getSeriesIdentifier() == “Series 1”){
}
}


Also note that this event is called for the category series.

Within these events you can modify the series before it is rendered. For example if you want to change a drill through hyperlink to point to a different report you could use the following script.


triglen = series.getTriggers().size();
if( triglen >0 ){
mytarget = series.getTriggers().get(0).getAction().getValue().getURLValues( ).get(0).getBaseUrl();
newtarget = mytarget.replace("reporttarget1.rptdesign", "reporttarget2.rptdesign");
series.getTriggers().get(0).getAction().getValue().getURLValues( ).get(0).setBaseUrl(newtarget);

}


You can also get all the data points for the series by using script like the following:


if( series.getClass() == BarSeriesImpl ){
var pts = isr.getSeriesRenderingHints().getDataPoints();
for( i=0; i< pts.length; i++ ){
var mydp = pts[i];
if( mydp.getOrthogonalValue() > 35 ){
//do something
}
}
}



DataPoint and Data Point Label Events
For each runtime series that is not a category series, the before and afterDrawDataPoint and before and afterDrawDataPointLabel events are fired. These occur between the before and afterDrawSeries events. These events can be used to modify how the data point and label are rendered.

The fill object passed to the before and afterDrawDataPoint events will be an instance of one of the following classes depending on how you have setup the palette.

GradientImpl
ColorDefinitionImpl
ImageImpl (EmbeddedImageImpl and PatternImageImpl extend ImageImpl)



It is important to know which type you are dealing with if you plan on making changes to the fill object. You can check the fill type with script similar to this:



if( fill.getClass().isAssignableFrom(GradientImpl)){
fill.setStartColor(ColorDefinitionImpl.create( 0, 0, 255 ));
fill.setEndColor(ColorDefinitionImpl.create(255,255,225 ));
}



You can set the fill object based on the data point value by using a script like (Assumes Color Fill):


val = dph.getOrthogonalValue();
if (val == 10){
fill.set(255, 0, 0);
}


Using the data point label scripts you can modify the label format and value.


var newlabel = label.getCaption().getValue().replace(",", "'");
label.getCaption().setValue(newlabel);

if( dph.getBaseDisplayValue() == 50 ){
label.setVisible(true);

label.getCaption().getFont().setBold(true);
label.getCaption().getFont().setSize(14);
label.getCaption().getFont().setName("Arial");
}else{
label.setVisible(false);
}



Curve Fitting Events
If the series contains a visible curve fitting line, the before and afterDrawFittingCurve events will be trigged after all the data point and data point label scripts have fired. You can use this event to modify the line attributes and label properties for the fitting curve.

Axis Title and Axis Label Events
After all series have been rendered the before and afterDrawAxisLabel events are fired for every label that appears on the axis. The before and afterDrawAxisTitle events are then fired. This process starts with the category axis and then proceeds for each value axis. You can use these events to modify the labels that are rendered. The axis type can be checked to determine which axis fired the event.


//LINEAR_LITERAL
//LOGARITHMIC_LITERAL
//TEXT_LITERAL
//DATE_TIME_LITERAL
if( context.getExternalContext().getScriptable().getParameterValue("NewParameter") == "date"){
if (axis.getType() == AxisType.TEXT_LITERAL)
{
value = label.getCaption().getValue();
var dtf = new SimpleDateFormat("MM/dd/yy");
var dt = new Date(value);
var fn = dtf.format(dt);
label.getCaption().setValue(fn);
}
}
You can also check the axis title to determine which axis fired the event.
if( axis.getTitle( ).getCaption( ).getValue( ) ==
"myYaxisTitle" )
{
label.getCaption( ).setColor(
ColorDefinitionImpl.BLUE( ) );
}
if ( axis.getType( ) == AxisType.DATE_TIME_LITERAL )
{
label.getCaption( ).setColor(
ColorDefinitionImpl.RED( ) );
}



Legend Item Events
The final set of events that are fired that can affect the chart are the before and afterDrawLegendItem events. These events can be used to customize the look of the chart legend.

This event is passed the LegendEntryRenderingHints (lerh) object and the bounds for the specific entry. The bounds object is for legend item graphic and not the text. Using these objects you can customize the legend. You can hide an entry using the bounds object.


if( lerh.getLabel().getCaption().getValue().compareToIgnoreCase("series3") == 0 ){
bounds.setHeight(0);
bounds.setWidth(0);
lerh.getLabel().setVisible(false);
}


You can modify the fill object using the lerh.getFill() function and modify the label using the lerh.getLabel() function.


importPackage( Packages.org.eclipse.birt.chart.model.attribute.impl );
label = lerh.getLabel();
labelString = label.getCaption().getValue();
if( labelString == "series2" ){
label.getCaption( ).getColor( ).set( 32, 168, 255 );
label.getCaption().getFont().setItalic(true);
label.getCaption().getFont().setRotation(5);
label.getCaption().getFont().setStrikethrough(true);
label.getCaption().getFont().setSize(12);
label.getCaption().getFont().setName("Arial");
label.getOutline().setVisible(true);
label.getOutline().setThickness(3);

var mycolor = ColorDefinitionImpl.BLUE();
r = mycolor.getRed();
g = mycolor.getGreen();
b = mycolor.getBlue();
lerh.getFill().set(r, g, b);

}



You can check the fill object type before modifying it as described in the DataPoint and Data Point Label Events described earlier.

Chart Scripting Examples
Here are some example reports/posts that illustrate using the chart scripting model.
BIRT Resizing Charts
BIRT - Reversing a Chart Scale
Resize Pie Chart based on optional grouping count
Add spacing to stacked bar optional grouped chart
Turning a Chart series off in script
Set Chart Axis Label Interval with Script
Using Chart Script to Define a specific series color
Chart Script to Alter Palette - Gradient Pie
Adjust BAR Location in Bar Chart

Tuesday, August 03, 2010

Functions and Controls Moves to Eclipse Labs

Don't you love it when technology just works?  I recently had this experience when I moved a couple of BIRT projects to Eclipse Labs.  I have moved all of my BIRT projects that were on Google Code over to the Eclipse Labs section of Google Code.  You can access them here:

The one question might be why create an update site that is separate from the project sites?  Well the short answer is that I want to make it easy for someone to get access to the projects we have created from a single site.  In addition, it is easy for me to add more content without having to modify the existing versions on the projects.

In the next couple of weeks, I hope to add a couple of new features to the update site that will make it easier to create team based reporting projects.


 

Monday, July 26, 2010

Whats the Difference Between dataSetRow["FIELD"] and row["FIELD"]

One of the most common questions for people that are new to BIRT is about how to ask data from the DataSet in the report.  The question is when building expressions should I use dataSetRow["FIELD"] or row["FIELD"]?

So let me see if I can set the record straight.  When data is acquired, it is acquired by a DataSet, so the following query in a JDBC DataSet will create a three field resultset:

select CITY, STATE, COUNTRY 
from CUSTOMERS


Any script or expressions written on the DataSet will be written to use the format

row["FIELD_NAME"];









So if we add a computed column to the DataSet called compCityState, the expression would look like this.

Once a computed column is created, you can reference that computed column using the same row syntax.  So in the OnFetch method you could add this message to log the value of the computed column.

Packages.java.lang.System.out.println(row["compCityState"]);

The other place that you can access variable on the DataSet is through ReportItem binding.  In most cases, this means Table Binding where you have attached a table to a DataSet.  In general, when you use BIRT you associate a Table with a DataSet.  We say that the Table is bound to the DataSet.

When a DataSet is dragged to the Layout editor, BIRT automatically does the data binding and creates a bound column for each field and computed column in the DataSet.  The following shows a table and its Binding.

You will notice that all of the expressions use the term dataSetRow, which means this value is pulled from the DataSet.  Now for the tricky bit, any field that is bound to a Table can be used in another table binding.  So if we wanted to create a field that has City, State and Country, we could either:
a) go back to the DataSet
b) add a new table binding

When you add a binding to a table you are brought to the expression builder.  In this first example, I am creating a table binding that gathers data straight from the DataSet.

Because I am referencing the DataSet directly the expression uses the dataSetRow syntax.

In the next example, I will use the previously established bindings to build the expression.  In the expression builder I will select Available Column Bindings and the Table and then select the fields.  You will also notice that the Table Binding has access to any of the previously bound fields and a special RowNumber field.  

The important thing is that the syntax to access those values is to use row["FIELD_NAME"]


So the short answer is, dataSetRow syntax references the original values from the DataSet, whereas the row syntax references the bound column values.

Now for a quick test, this should be easy.

Imagine that I change the expression of the CITY expression to look like this, and all else remains the same.

If the values for the row are:
City: Kalamazoo
State: MI
Country: USA

What will a DataItem that is showing rowCityStateZip display?
What will a DataItem that is showing tablCityStateCountry display?


The answers will be after a short shameless promotion of my companies BIRT training program.  We have designed a modular training program that is focused on teaching you how BIRT works.  You can take one module or all of them.  The focus of our training is on how BIRT works, so that when you finish you really understand what is going on in the product.

Our training can be taken on-site, or through remote sessions of between two and four hours each.  We are also happy to work with your team to customize the training to match your companies needs.   If you are interested, please visit our web site and have a look.

Answers:
rowCityStateZip:        { Kalamazoo }, MI USA
tablCityStateCountry:  Kalamazoo, MI USA

Remember, dataSetRow syntax (as used in tablCityStateCountry) goes back to the original data.  Row syntax use the table binding.

Monday, July 19, 2010

Manipulating Chart Legends in Event Handlers

Recently I had the opportunity to figure out how to manipulate sizing, positioning and text wrapping of chart legends so I thought I'd share what I learned.

If you have a chart with legend items that don't fit in the available space, BIRT will do one of two things, depending on the wrapping width option. (Wrapping width is found on the legend layout dialog). If the wrapping width is set to zero, BIRT will simply truncate the legend item text and optionally append an ellipsis. (The ellipsis option is located on the legend entries dialog).

If the wrapping width is set to a positive value, BIRT will word-wrap the text. Unfortunately when it does this, it doesn't check the vertical boundaries and long items can end up overlapping.




Another downside is you have to specify a hard width within which to wrap. It won't simply wrap based on the amount of space that's available. On top of that, my client had some special requirements about how the Thai characters should be wrapped. So I dug into the chart event handlers to see what I could do.

It turned out I could do a lot, but I had to pick the right event handler. My first thought was to use beforeDrawLegendItem. It looked perfect because of the Bounds parameter. However, it turned out that was not the bounds of the text area, but of the colored rectangle, which didn't help me much. Additionally I couldn't manipulate the size and position of the legend items within that handler and have it stick. The horses had already left the barn so to speak.

The beforeRendering handler was much better. In it I could manipulate the size, position and text of each legend item. Following are the the technical details.

The first argument to the event handler function is a GeneratedChartState object (which I'll call gcs). Using gcs.getRunTimeContext(), you can get the RunTimeContext object (rtc). Then using rtc.getLegendLayoutHints() you get the LegendLayoutHints object (llh) which tells you about the legend as a whole. Particularly llh.getLegendSize() tells you how big BIRT wants to make the legend. Finally llh.getLegendItemHints gets you an array of LegendItemHints (liha), which gives you access to each legend item. The number of legend items is liha.length. The text for each item is lih.getItemText().

So how much height is available for the whole legend? You can get that with gcs.getPlotComputations().getPlotBounds().getHeight(). The width of the legend is llh.getLegendSize().getWidth(). I didn't need to change the width so I didn't try to find out how to do that. I'm not sure how BIRT allocates horizontal space to the legend vs the chart itself. That may present itself as a future exercise.

For my project I had to wrap this text according to the space that was available, so I needed to be able to measure the pixel width of a string of characters. To do this you need to instantiate BIRTChartComputation. The constructor takes no arguments so it's simple. I'll call my instance bcc. Next you need the font height, which you can get from bcc.computeFontHeight. This method needs an IDisplayServer and a Label. You can get the IDisplayServer from gcs.getDisplayServer() and you can create a temporary Label with GObjectFactory.instance().createLabel(). The method with compute the font height based on the text in the label. Finally, you get the overall size of a string of text with bcc.computeLabelSize, which you pass an IDisplayServer, a Label and a font height. This is a fairly expensive operation so it's best to use a binary search to look for a wrap point.

Once a new version of the text has been computed, you can simply insert it back into the LegendItemHint using lih.setItemText(). Also the item height can be set with lih.itemHeight(). Finally, if you want an ellipsis to be appended you need to set the valid item length with lih.validItemLen() to the character position where you want the ellipsis to go. If you set it to zero or a value greater than the length of the label, no ellipsis will be appended.

Since I also needed to change the overall size of the legend, I wanted a setLegendSize method in LegendItemHints, but no such method exists. In fact, the class is mostly immutable. Fortunately it's possible to instantiate a new LegendItemHints with a new size and pass it to the RunTimeContext with rtc. setLegendLayoutHints().

An that's it. Fun, huh? Ok you can stop yawning. Here's what my new legend looks like:





I know you can't do everything in event handlers, but this is more than I thought I'd be able to do when I started. I was pleasantly surprised.