/* Copyright 2000 Charles G. Wright * This software may be distributed under the terms of the * GNU General Public License. * * $Id: GRAph.java,v 1.1 2000-06-15 10:14:58-05 chuckles Exp chuckles $ */ import java.awt.*; import java.awt.event.*; import java.io.*; import java.text.*; /** This class implements a 2D graph capable of * graphing several data sets */ public class GRAph extends Canvas { private DecimalFormat fmt1 = new DecimalFormat("#,##0.##"); /** Used to specify X axis mode (alternatively, NORMAL). */ public final static int TIME = 1; /** Used to specify X axis mode (alternatively, TIME). */ public final static int NORMAL = 0; // determines the minimum spacing between tics and grid lines private final static int MIN_TIC_SPACE_X = 30; private final static int MIN_TIC_SPACE_Y = 20; private final static int[] TIME_TIC_INTERVALS = {1, 2, 3, 6, 12, 24, 48, 96, 192, 384, 720, 1440, 2880}; private final static int[] TIME_TIC_INTERVAL_TYPES = {0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2}; private final static int[] TIME_TIC_UNIT_INTERVALS = {1, 2, 3, 6, 12, 1, 2, 4, 8, 16, 1, 2, 4}; private final static double[] X_TIC_INTERVALS = {0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0}; private final static double[] Y_TIC_INTERVALS = {0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0}; /** Array giving the starting day number in the year for * the first day of each of the 12 months. */ public final static int[] MONTH_DAYS = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}; //---------------------------------------------------------- // public instance variables //---------------------------------------------------------- /** Defines the mode of the X axis display, NORMAL or TIME.*/ public int xAxisMode = NORMAL; /** Defines left boundary of the graph. Uses the same units as the data being plotted. */ public double xmin; /** Defines right boundary of the graph. Uses the same units as the data being plotted. */ public double xmax; /** Defines bottom boundary of the graph. Uses the same units as the data being plotted. */ public double ymin; /** Defines top boundary of the graph. Uses the same units as the data being plotted. */ public double ymax; /** Pixels between top of graph and top of window.*/ public int margin_top = 20; /** Pixels between bottom of graph and bottom of window.*/ public int margin_bottom = 40; /** Pixels between left side of graph and left side of window.*/ public int margin_left = 30; /** Pixels between right side of graph and right side of window.*/ public int margin_right = 20; /** Graph title */ public String graph_title; //------------------------------------------------------------- // grid variables - declaration and default values. //------------------------------------------------------------- /** Interval between grid lines in X direction. Units are the same as X data. */ public double xGridInterval = 1.0; /** Interval between grid lines in Y direction. Units are the same as Y data. */ public double yGridInterval = 1.0; /** Position of grid origin. Units are the same as X data. */ public double xGridOrigin = 0.0; /** Position of grid origin. Units are the same as Y data.*/ public double yGridOrigin = 0.0; /** Size of the tic marks on the X and Y axes, in pixels. */ public int ticSize = 5; /** Boolean variable setting grid on or off. */ public boolean gridOn = false; //-------------------------------------------------------------- // private instance variables //-------------------------------------------------------------- // Virtual to physical conversion factors // These are recomputed before displaying anything. private double xscale, xoffset, yscale, yoffset; // width and height of plot area (less margin) private int w, h; /** Three dimensional array of data points to be plotted. * Holds an arbitrary number of sets of x-y data. * first index selects the data set number. * Second index selects x (0) or y (1) * Third index selects the data value. * There must be 1-1 correspondence between x and y values, * but the number of data points may vary across data sets. */ public double[][][] graph_data; // data points converted to actual display positions private int[][][] display_data; // Array of colors for the lines to be plotted. // 0 - background, 1 - axes & labels, 2 - grid color, 3 - plot 0 color, etc. public Color[] graph_colors; private boolean is_blank = true; //---------------------------------------- // cursor //---------------------------------------- private boolean cursorOn = false; private int cursorX = 0; //---------------------------------------------------------------- // constructor //---------------------------------------------------------------- /** Constructor for GRAph data object. * Sets up a blank canvas, with colors initialized for plotting. * Nothing is actually plotted. */ public GRAph(Color[] graph_colors){ this.graph_colors = new Color[graph_colors.length]; System.arraycopy(graph_colors, 0, this.graph_colors, 0, graph_colors.length); setBackground(graph_colors[0]); this.addMouseListener(new mouseHandler()); this.addMouseMotionListener(new mouseMotionHandler()); this.addKeyListener(new keyHandler()); } /** Used to draw a graph. * xmin, xmax, ymin, ymax define the * bounds of the graph area, in units of plotted data. * Data is a 3 dimensional array containing arbitrary number * of sets of x-y points: first dimension selects data set, * second specifies x (0)or y (1), and third is an array of * x or y data. For each set of data being graphed, both * x and y values must exist and be of the same size. Different * data sets may be of different size. */ public void graphit(double xmin, double xmax, double ymin, double ymax, double graph_data[][][]){ // Set bounds this.xmin = xmin; this.xmax = xmax; this.ymin = ymin; this.ymax = ymax; // remember that we are using the original data array, not a copy this.graph_data = graph_data; // Create display array of same dimensions as // display data. // Note that the array is empty until we fill it. display_data = new int[graph_data.length][][]; for (int i = 0; i < graph_data.length; i++){ display_data[i] = new int[2][graph_data[i][1].length]; } is_blank = false; } /** If invoked without the bounds being specified, autoscale() is first invoked to set plotting bounds. */ public void graphit(double graph_data[][][]){ //autoscale(graph_data); // Call graphit with the values that we just set. */ graphit(xmin, xmax, ymin, ymax, graph_data); } //--------------------------------------------------------------- // paint the graph //--------------------------------------------------------------- /** This routine is called whenever something changes, to render the graph. */ public void paint(Graphics g){ int i; if (is_blank == false){ ScrollPane parent = (ScrollPane)getParent(); setSize(parent.getViewportSize()); setBackground(graph_colors[0]); computeScaleFactors(); //System.out.println("inside GRAph paint() .. b"); // scale plot data to be displayed for (i = 0; i < graph_data.length; i++){ for (int k = 0; k < graph_data[i][1].length; k++){ display_data[i][0][k] = getXReal(graph_data[i][0][k]); display_data[i][1][k] = getYReal(graph_data[i][1][k]); } } // Now we have data ready to be displayed. Need to put it // onto the display canvas. // Write the border, grid, etc. g.setClip(0, 0, getSize().width, getSize().height); drawTics(g); // also draws grid and labels g.setColor(graph_colors[1]); g.drawRect(margin_left, margin_right, w, h); g.setClip(margin_left, margin_right, w, h); for (i = 0; i < graph_data.length; i++){ g.setColor(graph_colors[i + 3]); g.drawPolyline(display_data[i][0], display_data[i][1], display_data[i][1].length); } } } //-------------------------------------------------------------------------------- // drawing the grid, tic marks, and labels //-------------------------------------------------------------------------------- // calculates a reasonable time interval (in hours), based // on xmin, xmax, and window size. // Use the static class variable MinTicSpace to determine // what tic interval can reasonably be displayed. private double calcTicInterval(double[] intervals, double scale, int min_tic_spaces){ // start at 1 and work up: 2, 4, ... int i; double abs_scale = Math.abs(scale); for(i = 0; i < (intervals.length - 1); i++){ if(abs_scale * intervals[i] >= min_tic_spaces) break; } //System.out.println("Scale = " + abs_scale + " chose interval of " + intervals[i]); return(intervals[i]); } /** draw a tic on the X axis, label it, and draw a grid line, if appropriate. */ private void drawXTic(Graphics g, double xpos){ int tic_position = getXReal(xpos); g.setColor(graph_colors[1]); g.drawString(String.valueOf(xpos), tic_position, h + margin_top + 12); g.drawLine(tic_position, h + margin_top, tic_position, h + margin_top + ticSize); if (gridOn){ g.setColor(graph_colors[2]); g.drawLine(tic_position, margin_top, tic_position, margin_top + h); } } /** draw a tic on the Y axis, label it, and draw a grid line, if appropriate. */ private void drawYTic(Graphics g, double ypos){ String label = fmt1.format(ypos); int tic_position = getYReal(ypos); g.setColor(graph_colors[1]); g.drawString(label, margin_left - g.getFontMetrics().stringWidth(label) - 3, tic_position); g.drawLine(margin_left, tic_position, margin_left - ticSize, tic_position); if (gridOn){ g.setColor(graph_colors[2]); g.drawLine(margin_left, tic_position, margin_left + w, tic_position); } } /** Draw a set of NORMAL mode x-axis marks, labels, and grid marks (if appropriate).*/ private void drawXTics(Graphics g){ double xpos; double tic_interval = calcTicInterval(X_TIC_INTERVALS, xscale, MIN_TIC_SPACE_X); for (xpos = xGridOrigin; xpos <= xmax; xpos += tic_interval){ if (xpos > xmin){ drawXTic(g, xpos); } } for (xpos = xGridOrigin; xpos >= xmin; xpos -= tic_interval){ if (xpos < xmax) { drawXTic(g, xpos); } } } /** Draw a set of Y-axis tics, labels, and grid marks (if appropriate).*/ private void drawYtics(Graphics g){ double ypos; double tic_interval = calcTicInterval(Y_TIC_INTERVALS, yscale, MIN_TIC_SPACE_Y); for (ypos = yGridOrigin; ypos <= ymax; ypos += tic_interval){ if (ypos > ymin){ drawYTic(g, ypos); } } for (ypos = yGridOrigin; ypos >= ymin; ypos -= tic_interval){ if (ypos < ymax){ drawYTic(g, ypos); } } } private void drawTimeTic(Graphics g, int xpos, String label){ //System.out.println("Putting hour tic for " + xpos + " hour = " + hour); int tic_position_real = getXReal((double)xpos); g.setColor(graph_colors[1]); g.drawLine(tic_position_real, h + margin_top, tic_position_real, h + margin_top + ticSize); g.drawString(label, tic_position_real + 2, h + margin_top + 12); if (gridOn){ g.setColor(graph_colors[2]); g.drawLine(tic_position_real, margin_top, tic_position_real, margin_top + h); } } private void drawHourTic(Graphics g, int xpos, int hour){ //System.out.println("Putting hour tic for " + xpos + " hour = " + hour); int tic_position_real = getXReal((double)xpos); g.setColor(graph_colors[1]); g.drawLine(tic_position_real, h + margin_top, tic_position_real, h + margin_top + ticSize); g.drawString(String.valueOf(hour), tic_position_real + 2, h + margin_top + 12); if (gridOn){ g.setColor(graph_colors[2]); g.drawLine(tic_position_real, margin_top, tic_position_real, margin_top + h); } } /** Draw a tic mark for a day marking */ private void drawDayTic(Graphics g, int xpos, int day){ int yoff = 15; int tic_position_real = getXReal((double)xpos); g.setColor(graph_colors[1]); g.drawLine(tic_position_real, h + margin_top + yoff, tic_position_real, h + margin_top + yoff + ticSize); g.drawString(String.valueOf(day + 1), tic_position_real + 2, h + margin_top + 24 + ticSize); } /** draw a tic mark and label for an hour marking */ private void drawMonthTic(Graphics g, int xpos, int month, boolean do_tic){ // System.out.println("Putting month tic for " + xpos + " month = " + month); int yoff = 30; g.setColor(graph_colors[1]); int tic_position_real = getXReal((double)xpos); if(do_tic){ g.drawLine(tic_position_real, h + margin_top + yoff, tic_position_real, h + margin_top + yoff + ticSize); } g.drawString(TMYdata.MONTHS[month], tic_position_real + 2, h + margin_top + 36 + ticSize); } /** Draw a set of tics for time-marked axis. */ public void drawTimeTics(Graphics g){ int tic_position; int tic_interval; int first_tic, last_tic; int month = 0; int hour; int day_of_year; int day_of_month = 0; int tic_interval_days; int first_tic_day; int i; int tic_interval_type; int tic_unit_interval; boolean drew_month_tic = false; // calculate a tic interval (in hours) for(i = 0; i < (TIME_TIC_INTERVALS.length - 1); i++){ //System.out.println(xscale + " " + TIME_TIC_INTERVALS[i] + " " + MIN_TIC_SPACE_X); if(xscale * TIME_TIC_INTERVALS[i] >= MIN_TIC_SPACE_X) break; } tic_interval = TIME_TIC_INTERVALS[i]; tic_interval_type = TIME_TIC_INTERVAL_TYPES[i]; tic_unit_interval = TIME_TIC_UNIT_INTERVALS[i]; int tic_offset = 0; if (tic_interval <= 24){ drew_month_tic = false; //System.out.println("xmin: " + xmin); // for tic intervals of 1 day or less, we don't have to worry about month boundaries... if ((((int)xmin) % tic_interval) != 0) tic_offset = 1; first_tic = ((((int)xmin) / tic_interval) + tic_offset) * tic_interval; //last_tic = ((int)xmax / tic_interval) * tic_interval; for(tic_position = first_tic; tic_position <= (int)xmax; tic_position += tic_interval){ if ((hour = (tic_position % 24)) == 0){ day_of_year = tic_position / 24; for (month = 0; month <= 11; month++){ //System.out.println(day_of_year + " " + month + " " + MONTH_DAYS[month+1]); if (day_of_year < MONTH_DAYS[month+1]){ if(day_of_year == MONTH_DAYS[month]){ drawMonthTic(g, tic_position, month, true); drew_month_tic = true; } break; } } day_of_month = day_of_year - MONTH_DAYS[month]; drawDayTic(g, tic_position, day_of_month); } drawHourTic(g, tic_position, hour); } if(drew_month_tic == false) { drawMonthTic(g, (int)(xmax + xmin) / 2, month, false); } }else{ // do some fancy stuff if we are likely to miss month boundaries. tic_interval_days = tic_interval / 24; day_of_year = (int)xmin / 24; // find initial month and day of month - linear search for (month = 0; month <= 11; month++){ if (day_of_year <= MONTH_DAYS[month+1]) break; } day_of_month = day_of_year - MONTH_DAYS[month]; // Figure out which day the first tic will be on // first_tic_day is in days of year. //System.out.println("Month = " + month + " day of month = " + day_of_month + // "day of year = " + day_of_year); if((day_of_month % tic_interval_days) != 0) tic_offset = 1; first_tic_day = (((day_of_month / tic_interval_days) + tic_offset) * tic_interval_days) + MONTH_DAYS[month]; for (int tic_day = first_tic_day; (tic_day * 24) < (int)xmax;){ // If we are too close to the end of the month for a tic, bump to the next month if ((tic_interval_type == 1) && (tic_day >(MONTH_DAYS[month+1] - 2))){ tic_day = MONTH_DAYS[month+1]; day_of_month = 0; month++; } if(tic_interval_type == 1){ drawTimeTic(g, tic_day * 24, String.valueOf(day_of_month)); tic_day += tic_interval_days; day_of_month += tic_interval_days; }else{ drawTimeTic(g, tic_day * 24, TMYdata.MONTHS[month]); // increment by correct number of months for(int j = 0; j < tic_unit_interval; j++){ tic_day += TMYdata.MONTHLENGTH[month]; month++; } } } } } /** draw a complete set of tic marks, labels and grid (if appropriate). */ private void drawTics(Graphics g){ double xpos, ypos; Color orig_color; orig_color = g.getColor(); if (xAxisMode == NORMAL){ drawXTics(g); }else{ drawTimeTics(g); } drawYtics(g); g.setColor(orig_color); } //------------------------------------------ // //------------------------------------------ /** Find the index of the entry in "values" that most closely * matches "data" */ private int getDataIndexAtX(double data, double[] values){ // binary search // assume values[] is in ascending order int i1 = 0; int i2 = values.length - 1; boolean finished = false; int itest = i1 + i2 / 2; for (int iterations = 0; (i1 != itest) && (iterations < 10); iterations++){ if (values[itest] <= data){ i1 = itest; }else{ i2 = itest; } itest = (i1 + i2) / 2; } return itest; } /** Returns the data for whatever field number at the cursor. * xy = 0 indicates x value, 1 inticates y value*/ private double[] getDataAtCursor(int fieldnum){ double[] data = new double[2]; int dataIndex = getDataIndexAtX(getXData(cursorX), graph_data[fieldnum][0]); data[0] = graph_data[fieldnum][0][dataIndex]; data[1] = graph_data[fieldnum][1][dataIndex]; return(data); } //---------------------------------------------------------------- //Coordinate transformations //---------------------------------------------------------------- // Scaling functions to convert input coordinates // into display coordinates. /** Given an x data value, compute the position at which * it should be displayed. */ private int getXReal(double xin){ return ((int)((xin * xscale) + xoffset)); } private double getXData(int xreal){ return ((double)xreal - xoffset) / xscale; } /** Given a y data value, compute the position at which * it should be displayed. */ private int getYReal(double yin){ return (int)((yin * yscale) + yoffset); } private double getYData(int yreal){ return ((double)yreal - yoffset) / yscale; } // Always called prior to redrawing screen. private void computeScaleFactors(){ w = getSize().width - (margin_left + margin_right); xscale = w / (xmax - xmin); //xoffset = ((margin_left * (xmax + xmin)) - (getSize().width * xmin))/ (xmax - xmin); xoffset = margin_left - (w * xmin/(xmax - xmin)); h = getSize().height - (margin_top + margin_bottom); yscale = h / (ymin - ymax); //yoffset = ((margin_top * (ymin + ymax)) - (getSize().height * ymax)) / (ymin - ymax); yoffset = margin_top - (h * ymax / (ymin - ymax)); } public void autoscaleX(double graph_data[][][]){ xmax = xmin = graph_data[0][0][0]; for (int i = 0; i < graph_data.length; i++){ for (int k = 0; k < graph_data[i][0].length; k++) { if (graph_data[i][0][k] < xmin) xmin = graph_data[i][0][k]; if (graph_data[i][0][k] > xmax) xmax = graph_data[i][0][k]; } } } public void autoscaleY(double graph_data[][][]){ ymax = ymin = graph_data[0][1][0]; for (int i = 0; i < graph_data.length; i++){ for (int k = 0; k < graph_data[i][0].length; k++) { if (graph_data[i][1][k] < ymin) ymin = graph_data[i][1][k]; if (graph_data[i][1][k] > ymax) ymax = graph_data[i][1][k]; } } } public void autoscale(double graph_data[][][]){ autoscaleX(graph_data); autoscaleY(graph_data); } /** Inner class providing calendar functions. */ public class monthDay{ public int month; public int day; // constructor monthDay(int month, int day){ this.month = month; this.day = day; } // constructor, setting values from day of year public monthDay(int day_of_year){ setMonthDay(day_of_year); } public void setMonthDay(int day_of_year){ int day; int month; for (month = 0; month < 11; month++) if (day_of_year < MONTH_DAYS[month+1]) break; this.month = month; this.day = day_of_year - MONTH_DAYS[month]; } public int getMonth(){ return(month); } public int getDay(){ return(day); } public int getDayOfYear(){ return(MONTH_DAYS[month] + day); } public int getDaysInMonth(){ return(MONTH_DAYS[month+1] - MONTH_DAYS[month]); } public int getDaysLeftInMonth(){ return(MONTH_DAYS[month+1] - MONTH_DAYS[month] - day); } /** Increments the month. Wraps after month 11. Returns the next month.*/ public int nextMonth(){ if(month == 11){ month = 0; }else{ month++; } return month; } /** increments the day, and if necessary the month. If at the end of the * month, day count wraps to zero. Returns the incremented day number.*/ public int nextDay(){ if (day < (MONTH_DAYS[month+1] - 1)){ day++; }else{ day = 0; nextMonth(); } return(day); } } // 2 functions: mouse press turns on a cursor, which highlights // a vertical line, and retrieves the values of the graph at that // value (display them on the screen?). // Second function: if control key is pressed, mouse drag highlights // an area, which will then be expanded. Each old display ahould be // pushed onto a stack, for retrieval (I think this just sets xmin // and xmax, not doing anything to the data). class mouseHandler extends MouseAdapter { // mouseAdapter provides the required, (but empty) methods // mouseClicked(MouseEvent), mouseEntered(MouseEvent) // mouseExited(MouseEvent), mousePressed(MouseEvent) // to implement a mouseListener public void mousePressed(MouseEvent e){ Graphics g = e.getComponent().getGraphics(); //g.setXORMode(e.getComponent().getBackground()); g.setXORMode(Color.white); cursorX = e.getX(); g.drawLine(cursorX, margin_top, cursorX, margin_top + h); cursorOn = true; double[] cursordata = getDataAtCursor(0); System.out.println("Cursor: X = " + cursordata[0] + " Data value = " + fmt1.format(cursordata[1])); if (e.getModifiers() == InputEvent.BUTTON1_MASK){ System.out.println("Button 1 was pressed"); } } public void mouseReleased(MouseEvent e){ Graphics g = e.getComponent().getGraphics(); cursorX = e.getX(); g.setXORMode(Color.white); g.drawLine(cursorX, margin_top, cursorX, margin_top + h); g.setPaintMode(); cursorOn = false; } } class mouseMotionHandler extends MouseMotionAdapter { // MouseMotionAdapter provides the required, but empty methods // mouseDragged(MouseEvent), mouseMoved(MouseEvent) // to implement a mouseMotionListener public void mouseDragged(MouseEvent e){ Graphics g = e.getComponent().getGraphics(); // undraw previous line. g.setXORMode(Color.white); g.drawLine(cursorX, margin_top, cursorX, margin_top + h); // draw new line. cursorX = e.getX(); g.drawLine(cursorX, margin_top, cursorX, margin_top + h); double[] cursordata = getDataAtCursor(0); System.out.println("Cursor: X = " + cursordata[0] + " Data value = " + fmt1.format(cursordata[1])); } } class keyHandler extends KeyAdapter { //keyPressed(KeyEvent), keyReleased(KeyEvent), keyTyped(KeyEvent) public void keyTyped(KeyEvent e){ System.out.println("Key typed"); } public void keyPressed(KeyEvent e){ System.out.println("key pressed"); if(e.getKeyCode() == KeyEvent.VK_CONTROL){ System.out.println("Control Key Pressed"); } } public void keyReleased(KeyEvent e){ System.out.println("key released"); if(e.getKeyCode() == KeyEvent.VK_CONTROL){ System.out.println("Control Key Released"); } } } }