Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Blackberry

Creating a XY Chart/Plot as a BlackBerry Custom Field

4.20/5 (2 votes)
17 Jun 2009GPL35 min read 42.4K  
Extend a field to make into a chart/plot field

Introduction 

  Plotting an XY set is a very common task, a description of the main steps for implementing such a module for the BlackBerry platform is outlined in this article.

Data collected or digested by a given application is more meaningful when displayed as a chart, for instance while developing a Bluetooth app that sampled data from an external device the requirement was to log and plot the data both in real-time and from previous logs, displaying this to the end-users as charts made it much more appealing  than showing them flashing numbers with maxs/mins being updated and they could easily remember trends of the data.

In the Blackberry world Apps can be either a Midlet app which to some extent could be device independent or Device app (CLDC) where all of their available APIs are utilized to build a BlackBerry only application similar to the core apps like calendar, browser and so on.

For the chart element outlined here the choice was a CLDC application

Background   

  In the BlackBerry camp to interact with users the basic entity is the Field, e.g. buttons, labels,dropdowns etc. are defined as Fields (something like VisualStudio controls for our MS refugees) . Since the BlackBerry development platform is Java(actually J2ME) it is very well structured and all of the features from Java such as inheritance, subclassing, etc. can be used extensively. A custom Field fits the requirement for our Chart element.

Within the available BlackBerry UI classes the Mainscreen class is the most commonly used  to interact with the user, within this Mainscreen Fields are dropped  to make up a GUI interface and a basic set of pre-cooked Fields are available with standard properties and methods.

These fields have some basic behaviour as described on the API documentation but of course they need to be extended. Generic fields cannot be located side by side as by default they take all the space horizontally, the real state for a PDA screen needs to be carefully allocated and it is important to optimize it,  to overcome this default behaviour the Manager class allows custom positioning and sizing of fields. 


1. The Chart Custom Field implementation  

   To implement our chart CustomField we will need a Chart class and a chart Manager class.

   The chart class carries out the scaling, coloring and data digesting.  This charting element is comprised of a grid where the data will be plotted, a scale/transpose method so that the data points can be coherently mapped onto the available screen space and a data digesting routine to loop through the data points.

1.1 Defining the chart Custom Field 

   The chart Custom Field will be instantiated and positioned on the MainScreen  The code shows the extension of the basic Field class.

import net.rim.device.api.ui.*;
.
.  more classes as needed
.
import java.util.*;


public class drawCharty extends Field implements DrawStyle
{
    
  private int fieldWidth, fieldHeight, xCoord, yCoord;
  private int[] drawSizes;
  

  public drawCharty(int xVal, int yVal, int wVal, int hVal)
  {
    //Create a non focusable field.
    super(Field.NON_FOCUSABLE);

    //Set the x and y coordinates.
    xCoord = xVal;
    yCoord = yVal;

    //width and height.
    fieldWidth = wVal; 
    fieldHeight = hVal; 
    
  } // --- end of constructor ---

  //Get/Set the  coordinates and dimensions of the field.
  public void setXCoord(int xVal)
  {
    if (xVal != xCoord)
    {
      xCoord = xVal;
      invalidate();
    }
  }
  //Get the x coordinate of the field.
  public int getXCoord()
  {
    return xCoord;
  }

  .
  . Similar for the Ycoord, width and height
  .

1.2 Drawing the grid for plotting

   Once the basic field is defined we need to define the space where the data points will be drawn, a chart will usually have a label and some range for its data points. In the Code shown the Vector argument contains objects of the form [maxValue, Label] upon completion the draw_sizes array will have the location and dimension of the plotting area.  

 // these_labels has [maxValue, text]  entries
 public void draw_the_grid(Graphics this_g, Vector these_labels) 
 {

    double rounded_max;
    double rounded_min;
    Object curElt;
    String[] cur_elt_array;
    int left_margin_d, right_margin_d;

    // work out the margins -- only 2 items to avoid confusion 
    switch(these_labels.size() )
    {
        //set it up for 1-elt chart
        case 1:  //work out margin space according to max value to be plotteed
               curElt = these_labels.elementAt(0);
               cur_elt_array  = (String[]) curElt;
               rounded_max  =       Math.ceil(Double.valueOf(cur_elt_array[0]).doubleValue() );
                left_margin_d = getFont().getAdvance(Double.toString(rounded_max));

               //keep the position for later drawing
               int[] tmp_draw_sizes = {2 + left_margin_d, 25,fieldWidth - 2 + left_margin_d ,fieldHeight - 25 -5};
               drawSizes = tmp_draw_sizes; //keep it for later processing

               break;

        case 2:  //same as above but now left and right margins

               curElt = these_labels.elementAt(0);
               cur_elt_array  = (String[]) curElt;
               rounded_max  = Math.ceil(Double.valueOf(cur_elt_array[0]).doubleValue() );
                left_margin_d = getFont().getAdvance(Double.toString(rounded_max));

               curElt = these_labels.elementAt(1);
               cur_elt_array  = (String[]) curElt;
               rounded_max  = Math.ceil(Double.valueOf(cur_elt_array[0]).doubleValue() );
                right_margin_d = getFont().getAdvance(Double.toString(rounded_max));

               //keep the position for later drawing
               int[] tmp_draw_sizes1 = {2 + left_margin_d, 25,fieldWidth -  left_margin_d -right_margin_d -4 , fieldHeight - 25 -5};
               drawSizes = tmp_draw_sizes1; //keep it for later processing

               break;
        default:
                  //error on allowed dimensions
    }

    //with the margins worked out draw the plotting grid
    this_g.fillRoundRect(drawSizes[0], drawSizes[1], drawSizes[2], drawSizes[3],1,1);

    this_g.setColor( Color.LIGHTGREY );

    this_g.drawRect(drawSizes[0],drawSizes[1],drawSizes[2],drawSizes[3]);

    // draw the mesh 
    for(int i=1; i < 5 ; i++)
    {
           this_g.drawLine(drawSizes[0], drawSizes[1] + (i * drawSizes[3] / 5), drawSizes[0] + drawSizes[2], drawSizes[1] + (i * drawSizes[3] / 5));
           this_g.drawLine(drawSizes[0]+ (i * drawSizes[2] / 5), drawSizes[1], drawSizes[0] + (i * drawSizes[2] / 5), drawSizes[1] + drawSizes[3]);
    }


   //print the labels and we are good 
    switch(these_labels.size() )
    {
        case 1:
               this_g.setColor( Color.RED );
               curElt = these_labels.elementAt(0);
               cur_elt_array  = (String[]) curElt;
                rounded_max  = Math.ceil(Double.valueOf(cur_elt_array[0]).doubleValue() );
                print_axis_values_4_grid(this_g, "mph" , Double.toString(rounded_max) , "0", cur_elt_array[1] , 2 ,0 );

               break;
        case 2:
               this_g.setColor( Color.RED );
               curElt = these_labels.elementAt(0);
               cur_elt_array  = (String[]) curElt;
                rounded_max  = Math.ceil(Double.valueOf(cur_elt_array[0]).doubleValue() );
                print_axis_values_4_grid(this_g, "mph" , Double.toString(rounded_max) , "0", cur_elt_array[1] , 2 ,0  );

               this_g.setColor( Color.BLUE );
               curElt = these_labels.elementAt(1);
               cur_elt_array  = (String[]) curElt;
                rounded_max  = Math.ceil(Double.valueOf(cur_elt_array[0]).doubleValue() );
                print_axis_values_4_grid(this_g, "sec" , Double.toString(rounded_max) , "0", cur_elt_array[1] , drawSizes[0] + drawSizes[2] +3 ,1  );
               break;

   }  // --- ed of draw_the_grid ---

1.3. Overriding the Paint event

   The Paint event is where we can customize our Field

public void paint(Graphics graphics)
{
    //set background of our field
    graphics.setColor( Color.AQUA );
    graphics.fillRoundRect(0, 0, getWidth(), getHeight(), 15, 15);
    graphics.setColor( Color.LIGHTBLUE );
    graphics.drawRoundRect(0, 0, getWidth(), getHeight(), 15, 15);

    graphics.setColor( Color.BEIGE );

    // labels_vector arg has [maxVal, label] objects
    draw_the_grid(graphics , labels_vector); 

} // --- end of paint---

1.4. Instantiating the class and the current output

   The code below shows the instantiation. If we were to stop here the output will look like in the figure

  .
  .  
  .
  LabelField title = new LabelField("Charty CustomField V 0.6", LabelField.ELLIPSIS
  setTitle(title);

  add(new LabelField("Label before charty",0, -1, Field.FIELD_HCENTER));

  //instantiate the class
  drawCharty  charty = new drawCharty(35,10, 200,120);
  add(charty);

  add(new LabelField("Label after charty",0, -1, Field.FIELD_HCENTER));
    
 

BB_custfield_3.jpg 

Here we can see that even though we are passing the coordinates and dimension of our class it is not being placed properly , the Manager class will take care of that.  

 

1.5. Plotting and scaling  

The data points need a proper mapping from the data ranges to the screen coordinates, this is accomplished with the scale method shown below. Looping through the data points and calling a drawLine between two successive points will wrap up our Chart CustomField. The data points are passed as a Vector containing the data and our Paint event will now call the plot_array_list .  

    private XYPoint  scale_point(int this_x , double this_y  , XYPoint drawPoint , 
         int scr_x  , int scr_y  , int scr_width  , int src_height  , 
         double maxX  , double minX  , double  maxY  , double minY  ) 
    {
        int temp_x, temp_y;
        XYPoint temp = new XYPoint();   
        
        if (maxY == minY)  //skip bad data
            return null;

        //don't touch it if bad data
        try
        {
        //if (! ( this_x == null) )
                temp_x = scr_x + (int)( ((double)this_x - minX) * ((double)scr_width / (maxX - minX)) );
                temp_y = scr_y + (int)( (maxY - this_y) * ((double)src_height / (maxY - minY)) );
             
                temp.x = temp_x;
                temp.y= temp_y;
                drawPoint = temp;
                
        } 
        catch  (Exception e)
        {
     
           return (null);
        }
        
        return temp;
        
    } // --- end of scale_point --

        public boolean plot_array_list(Graphics this_g, Vector this_array_list , Vector these_labels , String this_title  )  
        {
             int lRow ;
             int nParms;
             int  i, points_2_plot, shifted_idx ; 
             int prev_x, prev_y ;
             int cur_x=0, cur_y=0 ; 
             //Dim ShowMarker As Object
             XYPoint cur_point = new XYPoint();
     cur_point.set(0,0);
     
             double cur_maxX, cur_minX, cur_maxY=20, cur_minY=0, cur_rangeY;
             int cur_start_x, cur_points_2_plot; 
   
             Object curElt;  
             String[] cur_elt_array;
     
             draw_the_grid(this_g, these_labels);  

             try 
             {
                   points_2_plot = this_array_list.size();
    
                   //'Work it out so it prints the scrolled section if required
                        cur_start_x = 0;
                        cur_points_2_plot = points_2_plot;
                        cur_maxX = cur_points_2_plot;
                        cur_minX = 0;
  
                   //'Create the plot points for this series from the ChartPoints array:
   
                   curElt = this_array_list.elementAt(0);
                   cur_elt_array  = (String[]) curElt;
                   
                   //the lines plotted have to look good
                    this_g.setDrawingStyle(Graphics.DRAWSTYLE_AALINES, true);
                   
                      
                   //skip 1st elt of array as it is the x-axis value 
                   for(  nParms = 1 ; nParms < cur_elt_array.length ; nParms++ )
                   {
                       curElt = these_labels.elementAt(nParms -1);
                       cur_elt_array  = (String[]) curElt;
                       cur_maxY= Integer.parseInt(cur_elt_array[0]);
  
                       curElt = this_array_list.elementAt(cur_start_x);
                       cur_elt_array  = (String[]) curElt;
    
                       cur_point = scale_point(Integer.parseInt(cur_elt_array[0]), Double.parseDouble(cur_elt_array[nParms ] ), cur_point, 
                              drawSizes[0], drawSizes[1], drawSizes[2], drawSizes[3], 
                              cur_maxX, cur_minX, cur_maxY, cur_minY);                        
                       cur_x = cur_point.x;
                       cur_y = cur_point.y;
                       
                       prev_x = cur_x;
                       prev_y = cur_y;

                       switch (nParms)
                       {
                          case 1:
                            this_g.setColor( Color.RED );
                            break;
                          case 2:
                            this_g.setColor( Color.BLUE );
                            break;
                       }

                       //go and plot this parm 
                       for (lRow = cur_start_x +1 ; lRow< cur_start_x + cur_points_2_plot ; lRow++)
                       {
                            curElt = this_array_list.elementAt(lRow);
                            cur_elt_array  = (String[]) curElt;
        
                            if (cur_elt_array[0] == null) 
                                 cur_elt_array[0] = "0";
 
                            if (! (cur_elt_array[nParms ] == null ) )   //skip bad one
                            {                  
  
                               cur_point = scale_point(Integer.parseInt(cur_elt_array[0]), Double.parseDouble( cur_elt_array[nParms ]), cur_point,  
                                    drawSizes[0], drawSizes[1], drawSizes[2], drawSizes[3], 
                                    cur_maxX, cur_minX, cur_maxY, cur_minY);
    
                               cur_x = cur_point.x;
                               cur_y = cur_point.y;
 
                               this_g.drawLine( prev_x, prev_y, cur_x, cur_y); 
                               prev_x = cur_x;
                               prev_y = cur_y;
                                                        
                            } //  if end of this_array(lRow, nParms - 1)<> nothing
                                 
                   } // end of for lrow
                               
               } // end of for nParmns


            invalidate();
            return( true);
        }
        catch (Exception e)
        {
            return( false);
            //Console.out some message for debugging
        }

    } // --- end of plot_array_list ---

With this the plotting is carried out and the output now looks like the figure below

BB_custfield_3.jpg 

 

2. Defining our Manager class for the Chart Custom Field  

Our output is almost complete the only issue is positioning the Field, the companion Manager class completes the implementation

import net.rim.device.api.ui.*;
.
.  more classes as needed
.
import java.util.*;


// add a custom manager to handle this 
public class chartyManager extends Manager 
{
    
    int myHeight;
    int myWidth;
    
  public chartyManager()
  {
    //Disable scrolling in this manager.
    super(Manager.NO_HORIZONTAL_SCROLL |
      Manager.NO_VERTICAL_SCROLL);
  }

  //Override sublayout.
  protected void sublayout(int width, int height) {
    drawCharty field;
    int x_pos=0, y_pos=0;

    //Loop thru total number of fields within
    //this manager. In our case only our chart Field will be here
    int numberOfFields = getFieldCount();

    for (int i = 0; i < numberOfFields; i++)
    {
      //Get the field. 
      field = (drawCharty)getField(i); 

      //Obtain the custom x and y coordinates for
      setPositionChild(field, field.getXCoord(), field.getYCoord());

      x_pos=field.getXCoord();
      y_pos=field.getYCoord();


      //Layout the field.
      layoutChild(field, field.getPreferredWidth(), field.getPreferredHeight());
      width = field.getPreferredWidth();
      height = field.getPreferredHeight();
      myHeight=field.getPreferredHeight();
      myWidth=field.getPreferredWidth();

    }
    //Set the manager's extent
    
       setExtent(width + x_pos, height + y_pos );
  }


  public int getPreferredWidth()
  {
    return myWidth;  
  }

  public int getPreferredHeight()
  {
    return myHeight ; 
  }
}

3. Instantiating the manager class and the final output  

So now we can position the chart anywhere as set by the coordinates. The final syntax below.
  .
  .  
  .
  LabelField title = new LabelField("Charty CustomField V 0.6", LabelField.ELLIPSIS
  setTitle(title);

  add(new LabelField("Label before charty",0, -1, Field.FIELD_HCENTER));

  //instantiate the class
  drawCharty  charty = new drawCharty(35,10, 200,120);
  // and its manager class 
  chartyManager myManager = new chartyManager();
  myManager.add(charty);
  add (myManager);


  add(new LabelField("Label after charty",0, -1, Field.FIELD_HCENTER));
The output

    

Final Thoughts

The classes shown work rather well, when we needed to do real-time plotting a slight modification of the routine was used where instead of a Vector a Queue data structure was used since we needed to be able plot the data as it came in as well as having a scrolling feature but the basic components were the same. Many more modifications can be applied depending on the requirements.

If instead of a plot a graphic intensive application is required like a RPG(Role Playing Game) the basic concept is still the same the background image is equivalent to the grid and the plot is the gamer navigating through the background image(for instance through a maze) in such case optimization should be looked very carefully since when overlapping bitmaps and making such computations it will be very CPU intensive and the computations should be kept Integer as much as possible as the float conversions eat up the CPU made even worse when all pixels have to go through some math mill. The basic technique of double buffering should be definitely considered for such graphic intensive apps.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)