Swing Components Java

//package edu.purdue.touch.util;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.text.DecimalFormat;
import javax.swing.JPanel;
/**
 * 
 * 


 * Title: HeatMap
 * 


 * 
 * 


 * Description: HeatMap is a JPanel that displays a 2-dimensional array of data
 * using a selected color gradient scheme.
 * 


 * 


 * For specifying data, the first index into the double[][] array is the x-
 * coordinate, and the second index is the y-coordinate. In the constructor and
 * updateData method, the 'useGraphicsYAxis' parameter is used to control
 * whether the row y=0 is displayed at the top or bottom. Since the usual
 * graphics coordinate system has y=0 at the top, setting this parameter to true
 * will draw the y=0 row at the top, and setting the parameter to false will
 * draw the y=0 row at the bottom, like in a regular, mathematical coordinate
 * system. This parameter was added as a solution to the problem of
 * "Which coordinate system should we use? Graphics, or mathematical?", and
 * allows the user to choose either coordinate system. Because the HeatMap will
 * be plotting the data in a graphical manner, using the Java Swing framework
 * that uses the standard computer graphics coordinate system, the user's data
 * is stored internally with the y=0 row at the top.
 * 


 * 


 * There are a number of defined gradient types (look at the static fields), but
 * you can create any gradient you like by using either of the following
 * functions in the Gradient class:
 * 


     * 
  • public static Color[] createMultiGradient(Color[] colors, int numSteps)

  •  * 
  • public static Color[] createGradient(Color one, Color two, int numSteps)

  •  * 

 * You can then assign an arbitrary Color[] object to the HeatMap as follows:
 * 
 * 

 * myHeatMap.updateGradient(Gradient.createMultiGradient(new Color[] { Color.red,
 *     Color.white, Color.blue }, 256));
 * 

 * 
 * 


 * 
 * 


 * By default, the graph title, axis titles, and axis tick marks are not
 * displayed. Be sure to set the appropriate title before enabling them.
 * 


 * 
 * 
 * 


 * Copyright: Copyright (c) 2007, 2008
 * 


 * 
 * 


 * HeatMap is free software; you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 * 


 * 
 * 


 * HeatMap is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * 


 * 
 * 


 * You should have received a copy of the GNU General Public License along with
 * HeatMap; if not, write to the Free Software Foundation, Inc., 51 Franklin St,
 * Fifth Floor, Boston, MA 02110-1301 USA
 * 


 * 
 * @author Matthew Beckler (matthew@mbeckler.org)
 * @author Josh Hayes-Sheen (grey@grevian.org), Converted to use BufferedImage.
 * @author J. Keller (jpaulkeller@gmail.com), Added transparency (alpha)
 *         support, data ordering bug fix.
 * @version 1.6
 */
public class HeatMap extends JPanel {
  /**
   * 
   */
  private static final long serialVersionUID = 1L;
  private double[][] data;
  private int[][] dataColorIndices;
  // these four variables are used to print the axis labels
  private double xMin;
  private double xMax;
  private double yMin;
  private double yMax;
  private String title;
  private String xAxis;
  private String yAxis;
  private boolean drawTitle = false;
  private boolean drawXTitle = false;
  private boolean drawYTitle = false;
  private boolean drawLegend = false;
  private boolean drawXTicks = false;
  private boolean drawYTicks = false;
  private Color[] colors;
  private Color bg = Color.white;
  private Color fg = Color.black;
  private BufferedImage bufferedImage;
  private Graphics2D bufferedGraphics;
  /**
   * @param data
   *            The data to display, must be a complete array (non-ragged)
   * @param useGraphicsYAxis
   *            If true, the data will be displayed with the y=0 row at the
   *            top of the screen. If false, the data will be displayed with
   *            they=0 row at the bottom of the screen.
   * @param colors
   *            A variable of the type Color[]. See also
   *            {@link #createMultiGradient} and {@link #createGradient}.
   */
  public HeatMap(double[][] data, boolean useGraphicsYAxis, Color[] colors) {
    super();
    updateGradient(colors);
    updateData(data, useGraphicsYAxis);
    this.setPreferredSize(new Dimension(60 + data.length,
        60 + data[0].length));
    this.setDoubleBuffered(true);
    this.bg = Color.white;
    this.fg = Color.black;
    // this is the expensive function that draws the data plot into a
    // BufferedImage. The data plot is then cheaply drawn to the screen when
    // needed, saving us a lot of time in the end.
    drawData();
  }
  /**
   * Specify the coordinate bounds for the map. Only used for the axis labels,
   * which must be enabled seperately. Calls repaint() when finished.
   * 
   * @param xMin
   *            The lower bound of x-values, used for axis labels
   * @param xMax
   *            The upper bound of x-values, used for axis labels
   */
  public void setCoordinateBounds(double xMin, double xMax, double yMin,
      double yMax) {
    this.xMin = xMin;
    this.xMax = xMax;
    this.yMin = yMin;
    this.yMax = yMax;
    repaint();
  }
  /**
   * Specify the coordinate bounds for the X-range. Only used for the axis
   * labels, which must be enabled seperately. Calls repaint() when finished.
   * 
   * @param xMin
   *            The lower bound of x-values, used for axis labels
   * @param xMax
   *            The upper bound of x-values, used for axis labels
   */
  public void setXCoordinateBounds(double xMin, double xMax) {
    this.xMin = xMin;
    this.xMax = xMax;
    repaint();
  }
  /**
   * Specify the coordinate bounds for the X Min. Only used for the axis
   * labels, which must be enabled seperately. Calls repaint() when finished.
   * 
   * @param xMin
   *            The lower bound of x-values, used for axis labels
   */
  public void setXMinCoordinateBounds(double xMin) {
    this.xMin = xMin;
    repaint();
  }
  /**
   * Specify the coordinate bounds for the X Max. Only used for the axis
   * labels, which must be enabled seperately. Calls repaint() when finished.
   * 
   * @param xMax
   *            The upper bound of x-values, used for axis labels
   */
  public void setXMaxCoordinateBounds(double xMax) {
    this.xMax = xMax;
    repaint();
  }
  /**
   * Specify the coordinate bounds for the Y-range. Only used for the axis
   * labels, which must be enabled seperately. Calls repaint() when finished.
   * 
   * @param yMin
   *            The lower bound of y-values, used for axis labels
   * @param yMax
   *            The upper bound of y-values, used for axis labels
   */
  public void setYCoordinateBounds(double yMin, double yMax) {
    this.yMin = yMin;
    this.yMax = yMax;
    repaint();
  }
  /**
   * Specify the coordinate bounds for the Y Min. Only used for the axis
   * labels, which must be enabled seperately. Calls repaint() when finished.
   * 
   * @param yMin
   *            The lower bound of Y-values, used for axis labels
   */
  public void setYMinCoordinateBounds(double yMin) {
    this.yMin = yMin;
    repaint();
  }
  /**
   * Specify the coordinate bounds for the Y Max. Only used for the axis
   * labels, which must be enabled seperately. Calls repaint() when finished.
   * 
   * @param yMax
   *            The upper bound of y-values, used for axis labels
   */
  public void setYMaxCoordinateBounds(double yMax) {
    this.yMax = yMax;
    repaint();
  }
  /**
   * Updates the title. Calls repaint() when finished.
   * 
   * @param title
   *            The new title
   */
  public void setTitle(String title) {
    this.title = title;
    repaint();
  }
  /**
   * Updates the state of the title. Calls repaint() when finished.
   * 
   * @param drawTitle
   *            Specifies if the title should be drawn
   */
  public void setDrawTitle(boolean drawTitle) {
    this.drawTitle = drawTitle;
    repaint();
  }
  /**
   * Updates the X-Axis title. Calls repaint() when finished.
   * 
   * @param xAxisTitle
   *            The new X-Axis title
   */
  public void setXAxisTitle(String xAxisTitle) {
    this.xAxis = xAxisTitle;
    repaint();
  }
  /**
   * Updates the state of the X-Axis Title. Calls repaint() when finished.
   * 
   * @param drawXAxisTitle
   *            Specifies if the X-Axis title should be drawn
   */
  public void setDrawXAxisTitle(boolean drawXAxisTitle) {
    this.drawXTitle = drawXAxisTitle;
    repaint();
  }
  /**
   * Updates the Y-Axis title. Calls repaint() when finished.
   * 
   * @param yAxisTitle
   *            The new Y-Axis title
   */
  public void setYAxisTitle(String yAxisTitle) {
    this.yAxis = yAxisTitle;
    repaint();
  }
  /**
   * Updates the state of the Y-Axis Title. Calls repaint() when finished.
   * 
   * @param drawYAxisTitle
   *            Specifies if the Y-Axis title should be drawn
   */
  public void setDrawYAxisTitle(boolean drawYAxisTitle) {
    this.drawYTitle = drawYAxisTitle;
    repaint();
  }
  /**
   * Updates the state of the legend. Calls repaint() when finished.
   * 
   * @param drawLegend
   *            Specifies if the legend should be drawn
   */
  public void setDrawLegend(boolean drawLegend) {
    this.drawLegend = drawLegend;
    repaint();
  }
  /**
   * Updates the state of the X-Axis ticks. Calls repaint() when finished.
   * 
   * @param drawXTicks
   *            Specifies if the X-Axis ticks should be drawn
   */
  public void setDrawXTicks(boolean drawXTicks) {
    this.drawXTicks = drawXTicks;
    repaint();
  }
  /**
   * Updates the state of the Y-Axis ticks. Calls repaint() when finished.
   * 
   * @param drawYTicks
   *            Specifies if the Y-Axis ticks should be drawn
   */
  public void setDrawYTicks(boolean drawYTicks) {
    this.drawYTicks = drawYTicks;
    repaint();
  }
  /**
   * Updates the foreground color. Calls repaint() when finished.
   * 
   * @param fg
   *            Specifies the desired foreground color
   */
  public void setColorForeground(Color fg) {
    this.fg = fg;
    repaint();
  }
  /**
   * Updates the background color. Calls repaint() when finished.
   * 
   * @param bg
   *            Specifies the desired background color
   */
  public void setColorBackground(Color bg) {
    this.bg = bg;
    repaint();
  }
  /**
   * Updates the gradient used to display the data. Calls drawData() and
   * repaint() when finished.
   * 
   * @param colors
   *            A variable of type Color[]
   */
  public void updateGradient(Color[] colors) {
    this.colors = (Color[]) colors.clone();
    if (data != null) {
      updateDataColors();
      drawData();
      repaint();
    }
  }
  /**
   * This uses the current array of colors that make up the gradient, and
   * assigns a color index to each data point, stored in the dataColorIndices
   * array, which is used by the drawData() method to plot the points.
   */
  private void updateDataColors() {
    // We need to find the range of the data values,
    // in order to assign proper colors.
    double largest = Double.MIN_VALUE;
    double smallest = Double.MAX_VALUE;
    for (int x = 0; x < data.length; x++) {
      for (int y = 0; y < data[0].length; y++) {
        largest = Math.max(data[x][y], largest);
        smallest = Math.min(data[x][y], smallest);
      }
    }
    double range = largest - smallest;
    // dataColorIndices is the same size as the data array
    // It stores an int index into the color array
    dataColorIndices = new int[data.length][data[0].length];
    // assign a Color to each data point
    for (int x = 0; x < data.length; x++) {
      for (int y = 0; y < data[0].length; y++) {
        double norm = (data[x][y] - smallest) / range; // 0 < norm < 1
        int colorIndex = (int) Math.floor(norm * (colors.length - 1));
        dataColorIndices[x][y] = colorIndex;
      }
    }
  }
  /**
   * This function generates data that is not vertically-symmetric, which
   * makes it very useful for testing which type of vertical axis is being
   * used to plot the data. If the graphics Y-axis is used, then the lowest
   * values should be displayed at the top of the frame. If the non-graphics
   * (mathematical coordinate-system) Y-axis is used, then the lowest values
   * should be displayed at the bottom of the frame.
   * 
   * @return double[][] data values of a simple vertical ramp
   */
  public static double[][] generateRampTestData() {
    double[][] data = new double[10][10];
    for (int x = 0; x < 10; x++) {
      for (int y = 0; y < 10; y++) {
        data[x][y] = y;
      }
    }
    return data;
  }
  /**
   * This function generates an appropriate data array for display. It uses
   * the function: z = sin(x)*cos(y). The parameter specifies the number of
   * data points in each direction, producing a square matrix.
   * 
   * @param dimension
   *            Size of each side of the returned array
   * @return double[][] calculated values of z = sin(x)*cos(y)
   */
  public static double[][] generateSinCosData(int dimension) {
    if (dimension % 2 == 0) {
      dimension++; // make it better
    }
    double[][] data = new double[dimension][dimension];
    double sX, sY; // s for 'Scaled'
    for (int x = 0; x < dimension; x++) {
      for (int y = 0; y < dimension; y++) {
        sX = 2 * Math.PI * (x / (double) dimension); // 0 < sX < 2 * Pi
        sY = 2 * Math.PI * (y / (double) dimension); // 0 < sY < 2 * Pi
        data[x][y] = Math.sin(sX) * Math.cos(sY);
      }
    }
    return data;
  }
  /**
   * This function generates an appropriate data array for display. It uses
   * the function: z = Math.cos(Math.abs(sX) + Math.abs(sY)). The parameter
   * specifies the number of data points in each direction, producing a square
   * matrix.
   * 
   * @param dimension
   *            Size of each side of the returned array
   * @return double[][] calculated values of z = Math.cos(Math.abs(sX) +
   *         Math.abs(sY));
   */
  public static double[][] generatePyramidData(int dimension) {
    if (dimension % 2 == 0) {
      dimension++; // make it better
    }
    double[][] data = new double[dimension][dimension];
    double sX, sY; // s for 'Scaled'
    for (int x = 0; x < dimension; x++) {
      for (int y = 0; y < dimension; y++) {
        sX = 6 * (x / (double) dimension); // 0 < sX < 6
        sY = 6 * (y / (double) dimension); // 0 < sY < 6
        sX = sX - 3; // -3 < sX < 3
        sY = sY - 3; // -3 < sY < 3
        data[x][y] = Math.cos(Math.abs(sX) + Math.abs(sY));
      }
    }
    return data;
  }
  /**
   * Updates the data display, calls drawData() to do the expensive re-drawing
   * of the data plot, and then calls repaint().
   * 
   * @param data
   *            The data to display, must be a complete array (non-ragged)
   * @param useGraphicsYAxis
   *            If true, the data will be displayed with the y=0 row at the
   *            top of the screen. If false, the data will be displayed with
   *            the y=0 row at the bottom of the screen.
   */
  public void updateData(double[][] data, boolean useGraphicsYAxis) {
    this.data = new double[data.length][data[0].length];
    for (int ix = 0; ix < data.length; ix++) {
      for (int iy = 0; iy < data[0].length; iy++) {
        // we use the graphics Y-axis internally
        if (useGraphicsYAxis) {
          this.data[ix][iy] = data[ix][iy];
        } else {
          this.data[ix][iy] = data[ix][data[0].length - iy - 1];
        }
      }
    }
    updateDataColors();
    drawData();
    repaint();
  }
  /**
   * Creates a BufferedImage of the actual data plot.
   * 
   * After doing some profiling, it was discovered that 90% of the drawing
   * time was spend drawing the actual data (not on the axes or tick marks).
   * Since the Graphics2D has a drawImage method that can do scaling, we are
   * using that instead of scaling it ourselves. We only need to draw the data
   * into the bufferedImage on startup, or if the data or gradient changes.
   * This saves us an enormous amount of time. Thanks to Josh Hayes-Sheen
   * (grey@grevian.org) for the suggestion and initial code to use the
   * BufferedImage technique.
   * 
   * Since the scaling of the data plot will be handled by the drawImage in
   * paintComponent, we take the easy way out and draw our bufferedImage with
   * 1 pixel per data point. Too bad there isn't a setPixel method in the
   * Graphics2D class, it seems a bit silly to fill a rectangle just to set a
   * single pixel...
   * 
   * This function should be called whenever the data or the gradient changes.
   */
  private void drawData() {
    bufferedImage = new BufferedImage(data.length, data[0].length,
        BufferedImage.TYPE_INT_ARGB);
    bufferedGraphics = bufferedImage.createGraphics();
    for (int x = 0; x < data.length; x++) {
      for (int y = 0; y < data[0].length; y++) {
        bufferedGraphics.setColor(colors[dataColorIndices[x][y]]);
        bufferedGraphics.fillRect(x, y, 1, 1);
      }
    }
  }
  /**
   * The overridden painting method, now optimized to simply draw the data
   * plot to the screen, letting the drawImage method do the resizing. This
   * saves an extreme amount of time.
   */
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2d = (Graphics2D) g;
    int width = this.getWidth();
    int height = this.getHeight();
    this.setOpaque(true);
    // clear the panel
    g2d.setColor(bg);
    g2d.fillRect(0, 0, width, height);
    // draw the heat map
    if (bufferedImage == null) {
      // Ideally, we only to call drawData in the constructor, or if we
      // change the data or gradients. We include this just to be safe.
      drawData();
    }
    // The data plot itself is drawn with 1 pixel per data point, and the
    // drawImage method scales that up to fit our current window size. This
    // is very fast, and is much faster than the previous version, which
    // redrew the data plot each time we had to repaint the screen.
    g2d.drawImage(bufferedImage, 31, 31, width - 30, height - 30, 0, 0,
        bufferedImage.getWidth(), bufferedImage.getHeight(), null);
    // border
    g2d.setColor(fg);
    g2d.drawRect(30, 30, width - 60, height - 60);
    // title
    if (drawTitle && title != null) {
      g2d.drawString(title, (width / 2) - 4 * title.length(), 20);
    }
    // axis ticks - ticks start even with the bottom left coner, end very
    // close to end of line (might not be right on)
    int numXTicks = (width - 60) / 50;
    int numYTicks = (height - 60) / 50;
    String label = "";
    DecimalFormat df = new DecimalFormat("##.##");
    // Y-Axis ticks
    if (drawYTicks) {
      int yDist = (int) ((height - 60) / (double) numYTicks); // distance
                                  // between
                                  // ticks
      for (int y = 0; y <= numYTicks; y++) {
        g2d.drawLine(26, height - 30 - y * yDist, 30, height - 30 - y
            * yDist);
        label = df.format(((y / (double) numYTicks) * (yMax - yMin))
            + yMin);
        int labelY = height - 30 - y * yDist - 4 * label.length();
        // to get the text to fit nicely, we need to rotate the graphics
        g2d.rotate(Math.PI / 2);
        g2d.drawString(label, labelY, -14);
        g2d.rotate(-Math.PI / 2);
      }
    }
    // Y-Axis title
    if (drawYTitle && yAxis != null) {
      // to get the text to fit nicely, we need to rotate the graphics
      g2d.rotate(Math.PI / 2);
      g2d.drawString(yAxis, (height / 2) - 4 * yAxis.length(), -3);
      g2d.rotate(-Math.PI / 2);
    }
    // X-Axis ticks
    if (drawXTicks) {
      int xDist = (int) ((width - 60) / (double) numXTicks); // distance
                                  // between
                                  // ticks
      for (int x = 0; x <= numXTicks; x++) {
        g2d.drawLine(30 + x * xDist, height - 30, 30 + x * xDist,
            height - 26);
        label = df.format(((x / (double) numXTicks) * (xMax - xMin))
            + xMin);
        int labelX = (31 + x * xDist) - 4 * label.length();
        g2d.drawString(label, labelX, height - 14);
      }
    }
    // X-Axis title
    if (drawXTitle && xAxis != null) {
      g2d.drawString(xAxis, (width / 2) - 4 * xAxis.length(), height - 3);
    }
    // Legend
    if (drawLegend) {
      g2d.drawRect(width - 20, 30, 10, height - 60);
      for (int y = 0; y < height - 61; y++) {
        int yStart = height
            - 31
            - (int) Math.ceil(y
                * ((height - 60) / (colors.length * 1.0)));
        yStart = height - 31 - y;
        g2d.setColor(colors[(int) ((y / (double) (height - 60)) * (colors.length * 1.0))]);
        g2d.fillRect(width - 19, yStart, 9, 1);
      }
    }
  }
}