
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.font.FontRenderContext;
import javax.swing.JComponent;
import javax.swing.JPanel;

/**  This is a simple class for creating basic 3D bar graphs of data.
 * @author Anthony L. Slade */
public class GraphCanvas extends JComponent {

  /** A basic test of this GraphCanvas class */
  public static void main( String[] args ) {
    GraphCanvas gc = new GraphCanvas( new String[] {"hi", "this is a longer name","another","and another"},
				      new int[] {5, 19, 1, 0, },
				      new Color[] {Color.blue,Color.red,Color.yellow,Color.white},
				      "Test Graph!");
    JPanel panel = new JPanel();
    panel.add(gc);
    javax.swing.JFrame frame = new javax.swing.JFrame("test frame");
    //frame.getContentPane().setLayout( new BorderLayout() );
    frame.getContentPane().add(gc);
    frame.setLocation(50,50);
    frame.pack(); frame.show();
    System.err.println("You should see the frame by now!");
  }

  /** Determines the angle of rotation before painting labels */
  public static final double DEFAULT_LABEL_ROTATION = Math.PI/5;
  /** The DIM_XXX fields determine spacing and sizing of various
   * characteristics of the graph.  The names should be relatively
   * self-explanatory. */
  public static final int DIM_MARGIN_TOP = 20;
  public static final int DIM_MARGIN_LEFT = 35;
  public static final int DIM_MARGIN_RIGHT = 20;
  public static final int DIM_MARGIN_BOTTOM = 5;
  public static final int DIM_BAR_WIDTH = 15;
  public static final int DIM_BAR_DEPTH_X = 5;
  public static final int DIM_BAR_DEPTH_Y = 5;
  public static final int DIM_SPACE = 15;
  public static final int DIM_GRAPH_Y = 200;
  /** This is a series of blank spaces used to pad labels when they
   * are being painted or measured. */
  public static final String LABEL_PAD = "    ";
  /** This constant helps to adjust the placement of some of the
   * labels. */
  public static final int LABEL_OFFSET = 2;
  /** The name of the font to use in this component */
  public static final String FONT_NAME = null; // null==>default
  /** The type of font to use in this component */
  public static final int FONT_TYPE = Font.PLAIN;
  /** The size of font to use in this component */
  public static final int FONT_SIZE = 10;

  /**
   * @param labels the names of the entries
   * @param values the initial values of the entries
   * @param colors the colors used to represent each entry
   * @param axisLabel the label to apply to the axis
   * @throws ArrayIndexOutOfBoundsException if the given arrays are
   * not matched in length
   */
  public GraphCanvas( String[] labels,
		      int[] values,
		      Color[] colors,
		      String axisLabel )
    throws ArrayIndexOutOfBoundsException {
    int numElements = labels.length;
    _labels = new String[numElements];
    System.arraycopy(labels,0,_labels,0,numElements);
    if ( values.length != numElements ) arrayMismatch();
    _values = new int[values.length];
    System.arraycopy(values,0,_values,0,numElements);
    if ( colors.length != numElements ) arrayMismatch();
    _colors = new Color[colors.length];
    System.arraycopy(colors,0,_colors,0,numElements);
    _axisLabel = axisLabel;
    _labelRotation = DEFAULT_LABEL_ROTATION;
    initializeComponent();
  }

  /** The main paint method. @param gr the Graphics2D object */
  public void paint( Graphics gr ) {
    super.paint(gr);
    if ( null == _fmet )
      _fmet = gr.getFontMetrics();
    gr.setFont( _font );
    Graphics2D g2 = (Graphics2D)gr;
    Paint originalPaint = g2.getPaint();

    // paint a cool gradient background
    g2.setPaint(new GradientPaint(0.0f,0.0f,Color.white,
				  0.0f,(float)_height,_brightGray));//Color.gray));
    g2.fillRect(0,0, getWidth(),getHeight());
    g2.translate(DIM_MARGIN_LEFT+DIM_SPACE,DIM_MARGIN_TOP);
    // paint a nifty look to accent the axes
    int xAxisLength = DIM_SPACE+ _values.length*(DIM_SPACE
						 +DIM_BAR_WIDTH
						 +DIM_BAR_DEPTH_X);
    g2.setPaint(new GradientPaint(0f,0f,Color.white,
				  0f,(float)DIM_GRAPH_Y,_darkGray));
    g2.fillPolygon(new int[] {0,0,3*DIM_BAR_DEPTH_X,
			      3*DIM_BAR_DEPTH_X},
		   new int[] {0,DIM_GRAPH_Y,
			      DIM_GRAPH_Y-3*DIM_BAR_DEPTH_Y,
			      -3*DIM_BAR_DEPTH_Y}, 4);
    g2.setPaint(new GradientPaint(0f,0f,_darkGray,
				  xAxisLength,0f,Color.white));
    g2.fillPolygon(new int[] {0,xAxisLength,
			      xAxisLength+3*DIM_BAR_DEPTH_X,
			      3*DIM_BAR_DEPTH_X},
		   new int[] {DIM_GRAPH_Y,DIM_GRAPH_Y,
			      DIM_GRAPH_Y-3*DIM_BAR_DEPTH_Y,
			      DIM_GRAPH_Y-3*DIM_BAR_DEPTH_Y}, 4);
    g2.translate(DIM_SPACE,0);
    // paint the bars for each of the values
    for ( int val = 0; val < _values.length; ++val ) {
      g2.translate(val*(DIM_SPACE+DIM_BAR_WIDTH+DIM_BAR_DEPTH_X),0);
      paintValue(g2,val);
      g2.translate(-val*(DIM_SPACE+DIM_BAR_WIDTH+DIM_BAR_DEPTH_X),0);
    }
    // paint the axes
    g2.setColor(Color.black);
    g2.drawLine(-DIM_SPACE,0,-DIM_SPACE,DIM_GRAPH_Y);
    g2.drawLine(-DIM_SPACE,DIM_GRAPH_Y,xAxisLength-DIM_SPACE,DIM_GRAPH_Y);
    g2.translate(-DIM_MARGIN_LEFT-DIM_SPACE,-DIM_MARGIN_TOP);

    // paint the label for the graph axis
    String maxLabel = ""+_maxValue;
    g2.drawString(maxLabel,
		  DIM_MARGIN_LEFT-_fmet.stringWidth(maxLabel),
		  DIM_MARGIN_TOP);
    g2.rotate(-Math.PI / 2);
    g2.drawString(_axisLabel,
		  -DIM_MARGIN_TOP-DIM_GRAPH_Y,
		  DIM_MARGIN_LEFT-LABEL_OFFSET);
    g2.rotate(Math.PI / 2);

    g2.setPaint(originalPaint);
  }

  /** Sets the value at the given index.  This method does not cause
   * the GraphCanvas to be repainted.  This might cause the axis to be
   * adjusted.
   * @param index the index of the value to update
   * @param value the new value */
  public void updateValue( int index, int value ) {
    _values[index] = value;
    if ( value > _maxValue ) {
      _maxValue = value;
      int maxMod10 = value % 10;
      if ( maxMod10 > 0 )
	_maxValue = _maxValue - maxMod10 + 10;
      _maxIndex = index;
    } else if ( index == _maxIndex
		&& value < _maxValue-10 ) {
      _maxValue = 10;
      for ( int ind = 0; ind < _values.length; ++ind ) {
	if ( _values[ind] > _maxValue ) {
	  _maxValue = _values[ind];
	  _maxIndex = ind;
	}
      }
      int maxMod10 = _maxValue % 10;
      if ( maxMod10 > 0 )
	_maxValue = _maxValue - maxMod10 + 10;
    }
  }

  /** Resets all of the values to zero.  This method does not cause
   * the GraphCanvas to be repainted. */
  public void resetValues() {
    for ( int vi = 0; vi < _values.length; ++vi ) {
      _values[vi] = 0;
    }
    _maxValue = 10;
    _maxIndex = 0;
  }

  /** Convenience method for throwing an
   * ArrayIndexOutOfBoundsException */
  private void arrayMismatch() {
    throw new ArrayIndexOutOfBoundsException("Arrays given to GraphCanvas must all have same size.");
  }

  /** Paints the bar and labels of the value at the given index
   * @param g2 the graphics object used to do the painting
   * @param index the index of the value to paint */
  private void paintValue( Graphics2D g2, int index ) {
    float height = (float)DIM_GRAPH_Y * ((float)_values[index]/(float)_maxValue);
    int barTop = DIM_GRAPH_Y-(int)height;
    // paint the bar
    g2.setPaint(new GradientPaint(0.0f,0.0f,_colors[index],
				  0.0f,(float)DIM_GRAPH_Y,_brightGray));
    g2.fillRect(0,barTop, DIM_BAR_WIDTH,(int)height);
    g2.setPaint(new GradientPaint(0.0f,0.0f,_colorsBrighter[index],
				  (float)DIM_BAR_WIDTH,0.0f,_colors[index]));
    g2.fillPolygon(new int[] {0, DIM_BAR_WIDTH,
			      DIM_BAR_WIDTH+DIM_BAR_DEPTH_X,DIM_BAR_DEPTH_X},
		   new int[] {barTop,barTop,
			      barTop-DIM_BAR_DEPTH_Y,barTop-DIM_BAR_DEPTH_Y},
		   4);
    g2.setPaint(new GradientPaint(0.0f,0.0f,_colorsDarker[index],
				  0.0f,(float)DIM_GRAPH_Y,_brightGray));
    g2.fillPolygon(new int[] {DIM_BAR_WIDTH,DIM_BAR_WIDTH,
			      DIM_BAR_WIDTH+DIM_BAR_DEPTH_X,
			      DIM_BAR_WIDTH+DIM_BAR_DEPTH_X},
		   new int[] {barTop,DIM_GRAPH_Y,
			      DIM_GRAPH_Y-DIM_BAR_DEPTH_Y,
			      barTop-DIM_BAR_DEPTH_Y},
		   4);
    // outline the bar
    g2.setColor(Color.black);
    g2.drawRect(0,barTop, DIM_BAR_WIDTH,(int)height);
    g2.drawPolyline(new int[] {0,DIM_BAR_DEPTH_X,
			       DIM_BAR_DEPTH_X+DIM_BAR_WIDTH,
			       DIM_BAR_DEPTH_X+DIM_BAR_WIDTH,
			       DIM_BAR_WIDTH},
		    new int[] {barTop,barTop-DIM_BAR_DEPTH_Y,
			       barTop-DIM_BAR_DEPTH_Y,
			       DIM_GRAPH_Y-DIM_BAR_DEPTH_Y,
			       DIM_GRAPH_Y}, 5);
    g2.drawLine(DIM_BAR_WIDTH,barTop,DIM_BAR_WIDTH+DIM_BAR_DEPTH_X,barTop-DIM_BAR_DEPTH_Y);

    // paint the angled label
    g2.rotate(_labelRotation,0d,(double)DIM_GRAPH_Y);
    g2.drawString(LABEL_PAD+_labels[index],0,DIM_GRAPH_Y);
    g2.rotate(-_labelRotation,0d,(double)DIM_GRAPH_Y);

    // paint the value on the bar at a right angle
    int valOffset = DIM_BAR_WIDTH+DIM_BAR_DEPTH_Y-_fmet.stringWidth(LABEL_PAD+_values[index]);
    if ( height < .1f*(float)DIM_GRAPH_Y ) {
      valOffset += (int)height;
      valOffset += (int)(0.1f*(float)DIM_GRAPH_Y);
    }
    g2.rotate(-Math.PI / 2,DIM_BAR_WIDTH,barTop);
    g2.drawString(""+_values[index],valOffset,barTop-LABEL_OFFSET);
    g2.setColor(Color.white);
    g2.drawString(""+_values[index],valOffset+1,barTop-LABEL_OFFSET-1);
    g2.rotate(Math.PI / 2,DIM_BAR_WIDTH,barTop);
  }

  /** Sets up several values used to paint this component.  This
   * method is usually only called once, on the first time the object
   * is painted. */
  private void initializeComponent() {
    _maxValue = 10;
    _maxIndex = 0;
    _colorsDarker = new Color[_colors.length];
    _colorsBrighter = new Color[_colors.length];
    double maxLabelY = 0.0, maxLabelX = 0.0;
    _font = new Font( FONT_NAME, FONT_TYPE, FONT_SIZE );
    FontRenderContext frc = new FontRenderContext(null,false,false);
    double cosRotation = Math.cos(_labelRotation);
    double sinRotation = Math.sin(_labelRotation);
    for ( int ind = 0; ind < _labels.length; ++ind ) {
      //int len = _fmet.stringWidth(LABEL_PAD+_labels[ind]+LABEL_PAD);
      int len =
	(int)(_font.getStringBounds(LABEL_PAD+_labels[ind]+LABEL_PAD,
				    frc).getWidth());
      double lY = sinRotation * (double)len;
      if ( lY > maxLabelY ) {
	maxLabelY = lY;
	maxLabelX = cosRotation * (double)len;
      }
      if ( _values[ind] > _maxValue ) {
	_maxValue = _values[ind];
	_maxIndex = ind;
      }
      _colorsDarker[ind] = _colors[ind].darker().darker();
      _colorsBrighter[ind] = _colors[ind].brighter().brighter();
    }
    int maxMod10 = _maxValue % 10;
    if ( maxMod10 > 0 )
      _maxValue = _maxValue - maxMod10 + 10;
    _height = DIM_MARGIN_BOTTOM + (int)maxLabelY
      + DIM_GRAPH_Y + DIM_MARGIN_TOP;
    _width = DIM_MARGIN_LEFT + DIM_MARGIN_RIGHT + DIM_SPACE + (int)maxLabelX
      + _values.length * (DIM_BAR_WIDTH + DIM_BAR_DEPTH_X + DIM_SPACE);
    Dimension size = new Dimension(_width,_height); 
    setMinimumSize( size );
    setPreferredSize( size );
    setMaximumSize( size );
    _brightGray = Color.gray.brighter();
    _darkGray = Color.gray.darker().darker();
  }

  /** The labels/names of the values */
  private String[] _labels;
  /** The values */
  private int[] _values;
  /** The colors to use for each of the values */
  private Color[] _colors;
  /** Used to create different shades for the 3D bars */
  private Color[] _colorsDarker;
  /** Used to create different shades for the 3D bars */
  private Color[] _colorsBrighter;
  /** The label for the vertical axis */
  private String _axisLabel;
  /** The maximum value displayed on the vertical axis */
  private int _maxValue;
  /** Indicates the index of the maximum value.  This is used to track
   * how big the graph's axis should be */
  private int _maxIndex;
  /** The amount that bar labels should be rotated */
  private double _labelRotation;
  /** The height of the component */
  private int _height;
  /** The width of the component */
  private int _width;
  /** A color of gray that we hang on to to avoid having to
   * recalculate it over and over */
  private Color _brightGray;
  /** A color of gray that we hang on to to avoid having to
   * recalculate it over and over */
  private Color _darkGray;
  /** The font used in this component */
  private Font _font;
  /** For convenience we hang on to the FontMetrics of the Graphics
   * context to help us determine the sizes of painted strings */
  private FontMetrics _fmet;

}// end class GraphCanvas

