Code Modifications

I've modified the Java Graph Class Library in a number of ways to make it more suitable for animated plots: double buffering is used to eliminate the flicker in the usual way; the plotting is speeded up by only drawing the new points each iteration; and changes are made to make the class more "thread friendly". The changes are in the following files.


Graph2D.java

The main changes are in the paint(g) method:

    public void paint(Graphics g) {
         int i;
 
         Rectangle r = bounds();
         if(r.width<=0) return;
         if(r.height<=0) return;
         
//       Make new graphics if bounds have changed or if clearAll true         
         if((! r.equals(r_save))  || clearAll) {
            newRectangle = true;
            r_g  =   new Rectangle(r.x,r.y,r.width,r.height);
            r_save = new Rectangle(r.x,r.y,r.width,r.height);             
            offScreenImage=createImage(r_g.width,r_g.height);
            if(offScreenGraphics != null) offScreenGraphics.dispose();
            offScreenGraphics=offScreenImage.getGraphics();
         }
         
//       Clear and reset axes etc if axes changed or new graphics
         if(axesChanged() || newRectangle) {
            newRectangle = false;
            resetAxes();           //tell Axis that axis change has been done
            if( !paintAll ) return;
            for (i=0; i<dataset.size(); i++) {
                 ((DataSet)dataset.elementAt(i)).plotAllPoints();
            }
 
//          Make new copy of offScreenGraphics for clipping etc.                        
            if(lg != null) {
                 lg.dispose();
            }            
            Color c = g.getColor();
            lg = offScreenGraphics.create();
            lg.setColor(getBackground());
            lg.fillRect(0,0,r.width,r.height);   
            lg.setColor(c);
 
            r.x      = borderLeft;
            r.y      = borderTop;
            r.width  -= borderLeft+borderRight;
            r.height -= borderBottom+borderTop;
 
            paintFirst(lg,r);
 
            if( !axis.isEmpty() ) r = drawAxis(lg, r);
            else   drawFrame(lg,r.x,r.y,r.width,r.height);
 
            if( !dataset.isEmpty() ) {
               for (i=0; i<dataset.size(); i++) {
                 ((DataSet)dataset.elementAt(i)).draw_legend(lg,r);
               }
            }
 
            lg.clipRect(r.x, r.y, r.width, r.height);
 
            datarect.x      = r.x;
            datarect.y      = r.y;
            datarect.width  = r.width;
            datarect.height = r.height;
        }
        
        paintBeforeData(lg,r);
        if( !dataset.isEmpty() ) {                
            for (i=0; i<dataset.size(); i++) {
                 ((DataSet)dataset.elementAt(i)).draw_data(lg,datarect);
            }
        }
        
        paintLast(lg,r);
        
        // Now draw offScreenImage to screen
        g.drawImage(offScreenImage,0,0,this);
    }


The update method is changed to simply call paint:

     public void update(Graphics g) {
          if( paintAll ) paint(g);
     }


There are also declarations at the top of the file:

/*
**  The offscreen graphics
*/
    protected Graphics lg = null;
    protected Image offScreenImage;
    protected Graphics offScreenGraphics=null;
/*
**  Size of graphics region
*/
    protected Rectangle r_save=null;
/*
**  Size of graphics region clipped by axes etc
*/
    protected Rectangle r_g=null;
/*
**  True if new graphics region
*/    
    protected boolean newRectangle = false;
 


and methods are added to communicate with Axis so that the graphics is reset when the axes change:

        protected boolean axesChanged() {
            boolean changed = false;
            Axis a;
            for (int i=0; i<axis.size(); i++) {
               a = ((Axis)axis.elementAt(i));
               if(a.changed) changed = true;
            }
            return changed;
        }
        
        protected void resetAxes() {
            Axis a;
            for (int i=0; i<axis.size(); i++) {
               a = ((Axis)axis.elementAt(i));
               a.changed = false;;
            }
        }


Axis.java

To implement the graphics resetting when axes are changed add to the declarations

      public boolean changed = true;

and change the method resetRange() to set changed

     public void resetRange() {
          double tryMinimum = getDataMin();
          double tryMaximum = getDataMax();
          if((tryMinimum != minimum || tryMaximum != maximum) &&
              ! force_end_labels) {
             minimum = tryMinimum;
             maximum = tryMaximum;
             changed = true;
          }
     }


In addtion there is a "thread bug" when using threads in animation: the minimum and maximum values of the axes might be changed by the a thread appending data between the time they are set and the data is plotted in the plotting thread. To fix this (at least when data is attached to the axes) change attachXdata to use d.getXmin() rather than d.xmin etc.

      protected void attachXdata( DataSet d ) {
 
            dataset.addElement(d);
            d.xaxis = this;
            if( dataset.size() == 1 ) {
                  minimum = d.getXmin();       // d.xmin -> d.getXmin  etc.
                  maximum = d.getXmax();       // MCC 6/28/96
            } else {                           // for safe thread operation
               if(minimum > d.getXmin()) minimum = d.getXmin();
               if(maximum < d.getXmax()) maximum = d.getXmax();
            }
 
      }

and similarly for attachYdata.


DataSet.java

The changes to DataSet fall into three parts:

To eliminate the "thread bug", when data is attached to an axis we don't want to reset xmin etc. in range() called when data is appended, since these variables are used in the plotting routines as the range of the axis, not a single data set. To correct this add the if statements to the last few lines:

//We don't want to set xmin here unless no axis defined
           if(xaxis==null) {
                xmin = dxmin;
                xmax = dxmax;
           }
           if(yaxis==null) {
                ymin = dymin;
                ymax = dymax;
           }
     }

To speed up the plotting we only want to draw the new data - the remainder of the plotted data is safely retained on the off-screen graphics. To do this define variables to keep track of the last points drawn:

      int previousPointLines = -1; 
      int previousPointMarkers = -1;

draw_lines() and draw_markers() are then extensively modified:

      protected void draw_lines(Graphics g, Rectangle w) {
          int i;
          int j;
          boolean inside0 = false;
          boolean inside1 = false;
          double x,y;
          int x0 = 0 , y0 = 0;
          int x1 = 0 , y1 = 0;
//     Calculate the clipping rectangle
          Rectangle clip = g.getClipRect();
          int xcmin = clip.x;
          int xcmax = clip.x + clip.width;
          int ycmin = clip.y;
          int ycmax = clip.y + clip.height;
 
//    Is there any data to draw? Sometimes the draw command will
//    will be called before any data has been placed in the class.
          if( data == null || data.length < 2 
              ||(length - previousPointLines) < 3) return;          
 
//          System.out.println("Drawing Data Lines!");
          int firstPoint;
          if(previousPointLines > 0) {
               firstPoint = previousPointLines -1;
          }
          else firstPoint = 0;
 
//    Is the first point inside the drawing region ?
          if( (inside0 = inside(data[firstPoint], 
                data[firstPoint+1])) ) {
 
              x0 = (int)(w.x + ((data[firstPoint]-xmin)/xrange)*w.width);
              y0 = (int)(w.y + (1.0 - (data[firstPoint+1]-ymin)/yrange)*w.height);
 
              if( x0 < xcmin || x0 > xcmax || 
                  y0 < ycmin || y0 > ycmax)  inside0 = false;
 
          }
 
          int newLength = length;
          for(i=firstPoint+2; i<newLength; i+=2) { 
 
//        Is this point inside the drawing region?
 
              inside1 = inside( data[i], data[i+1]);
             
//        If one point is inside the drawing region calculate the second point
              if ( inside1 || inside0 ) {
 
               x1 = (int)(w.x + ((data[i]-xmin)/xrange)*w.width);
               y1 = (int)(w.y + (1.0 - (data[i+1]-ymin)/yrange)*w.height);
 
               if( x1 < xcmin || x1 > xcmax || 
                   y1 < ycmin || y1 > ycmax)  inside1 = false;
 
              }
//        If the second point is inside calculate the first point if it
//        was outside
              if ( !inside0 && inside1 ) {
 
                x0 = (int)(w.x + ((data[i-2]-xmin)/xrange)*w.width);
                y0 = (int)(w.y + (1.0 - (data[i-1]-ymin)/yrange)*w.height);
 
              }
//        If either point is inside draw the segment
              if ( inside0 || inside1 )  {
                      g.drawLine(x0,y0,x1,y1);
              }
 
/*
**        The reason for the convolution above is to avoid calculating
**        the points over and over. Now just copy the second point to the
**        first and grab the next point
*/
              inside0 = inside1;
              x0 = x1;
              y0 = y1;
 
          }
          previousPointLines = newLength - 1;
      }
      protected void draw_markers(Graphics g, Rectangle w) {
          int x1,y1;
          int i;
//     Calculate the clipping rectangle
          Rectangle clip = g.getClipRect();
          int xcmin = clip.x;
          int xcmax = clip.x + clip.width;
          int ycmin = clip.y;
          int ycmax = clip.y + clip.height;
/*
**        Load the marker specified for this data
*/
          Vector m = g2d.getMarkerVector(marker);
 
 
          if( m == null) return;
 
//          System.out.println("Drawing Data Markers!");
          int firstPoint;
          if(previousPointMarkers > 0) {
               firstPoint = previousPointMarkers -1;
          }
          else firstPoint = 0;
 
          int newLength = length;
          for(i=firstPoint; i<newLength; i+=2) {
              if( inside( data[i], data[i+1]) ) {
 
                x1 = (int)(w.x + ((data[i]-xmin)/xrange)*w.width);
                y1 = (int)(w.y + (1.0 - (data[i+1]-ymin)/yrange)*w.height);
 
                if( x1 >= xcmin && x1 <= xcmax && 
                   y1 >= ycmin && y1 <= ycmax ) stroke_marker(m, g, x1, y1);
 
                }
          }
          previousPointMarkers = newLength -1;
      }

and a method is added to reset previousPointLines and previousPointMarkers so that all the data is then plotted

      public void plotAllPoints() {
            previousPointLines = -1;
            previousPointMarkers = -1;
      }


Finally, I've added a method (based on append) to add a single data point and check the new values directly against the minimum and maximum values (rather than all the other data) to see if the axes' ranges need to be changed:

public void appendPoint( double x, double y )  {
           double[] d = new double[2];
           d[0]=x;
           d[1]=y;
           int i;
           int k = 0;
           double tmp[];
 
           if(data == null) data = new double[increment];
 
//     Copy the data locally.
 
           if( 2+length < data.length ) {
               System.arraycopy(d, 0, data, length, 2);
               length += 2;
       } else {
               tmp = new double[2+length+increment];
 
               if( length != 0 ) {
                 System.arraycopy(data, 0, tmp, 0, length);
               }
               System.arraycopy(d, 0, tmp, length, 2);
 
               length += 2;
               data = tmp;
         }
 
//     Calculate the data range.
 
           if(x < dxmin) dxmin = x;
           if(x > dxmax) dxmax = x;
           if(y < dymin) dymin = y;
           if(y > dymax) dymax = y;
 
//     Update the range on Axis that this data is attached to
           if(xaxis != null) xaxis.resetRange();
           if(yaxis != null) yaxis.resetRange();
 
      }

[Return to Home Page]
Last modified 18 August, 2009
Michael Cross