/* * PyramidGraph.java * * --Copyright notice-- * * Copyright (c) Andy Evans. * http://www.geog.leeds.ac.uk/people/a.evans/ * This software is licensed under 'The Artistic License' which can be found at * the Open Source Initiative website at... * http://www.opensource.org/licenses/artistic-license.php * Please note that the optional Clause 8 does not apply to this code. * * The Standard Version source code, and associated documentation can be found at... * Evans, A.J. (2001) 'PyramidGraph' Centre for Computational Geography * [online] http://www.ccg.leeds.ac.uk/software/pyramidgraph/ * If you happen to use this code on a project and fancy referencing it, * that would be great. * * --End of Copyright notice-- * */ import java.applet.*; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; import java.net.*; /** * This class provides an Applet containing an arbitary number of pyramid graphs.

* It requires the graph class PyramidCanvas and a number of datafiles or PARAM blocks. The files * are supplied to the Applet using the following PARAM tags (default values given)...

*

* Text files should be comma or space separated tables. The first line should be a label * for the file (unused), then labels for the bars. Only the labels from the first file are * used, but they should be identically present in all files. Subsequent lines should be * a label for that dataset and then values for the bars. Again, only the labels in the * first file will be used, but the labels should be the same in all files. The scrollbar * will go between the first and last labels (which will be shown), and the current scrollbar * value will be shown as a label (labelPrefix + current label).

* Two example files...

* Male,16-24,25-44,45-54,55-59,60-64,65+,
* 1971,3.0,6.5,3.2,1.5,1.3,0.6,
* 1981,3.2,7.1,3.0,1.4,1.0,0.3,
* 1991,3.1,8.1,3.0,1.1,0.8,0.3,
* 1997,2.4,8.1,3.4,1.1,0.7,0.3,
* 2001,2.4,8.2,3.4,1.3,0.7,0.3,
* 2011,2.8,7.3,3.9,1.3,0.9,0.3,
*

* Female,16-24,25-44,45-54,55-59,60-64,65+,
* 1971,2.3,3.5,2.1,0.9,0.5,0.3,
* 1981,2.7,4.6,2.1,0.9,0.4,0.2,
* 1991,2.6,6.1,2.4,0.8,0.3,0.2,
* 1997,2.0,6.4,2.9,0.8,0.4,0.2,
* 2001,2.1,6.4,3.0,0.9,0.4,0.2,
* 2011,2.3,6.2,3.6,1.0,0.7,0.2,
*

* Given these files, the left side of the graph would begin by showing the male values * for 1971, the left female. As the scrollbar was shifted to the right, subsequent years * would be displayed. The scrollbar scale would run from 1971 to 2011, and if the labelPrefix * was "Now = " the label would initially display "Now = 1971".

* Note that any number of files can be shown. They will pair up in the order given. Single * files remaining will show as bargraphs. Note also that the height of the Applet vs. the * barHeight PARAM fixes the gap between the bars. If you don't want gaps, set the Applet height * smaller. *

*

* As an alternative the data can be supplied in the HTML as PARAM block, using the following * PARAMS... *

*

* The above example, would be presented, thus... *

* <PARAM name="numberOfDataBlocks" value="2">
* <PARAM name="data-1-1" value="Male,16-24,25-44,45-54,55-59,60-64,65+,>
* <PARAM name="data-1-2" value="1971,3.0,6.5,3.2,1.5,1.3,0.6,">
* <PARAM name="data-1-3" value="1981,3.2,7.1,3.0,1.4,1.0,0.3,">
* <PARAM name="data-1-4" value="1991,3.1,8.1,3.0,1.1,0.8,0.3,">
* <PARAM name="data-1-5" value="1997,2.4,8.1,3.4,1.1,0.7,0.3,">
* <PARAM name="data-1-6" value="2001,2.4,8.2,3.4,1.3,0.7,0.3,">
* <PARAM name="data-1-7" value="2011,2.8,7.3,3.9,1.3,0.9,0.3,">
* <PARAM name="data-2-1" value="Female,16-24,25-44,45-54,55-59,60-64,65+,">
* <PARAM name="data-2-2" value="1971,2.3,3.5,2.1,0.9,0.5,0.3,">
* <PARAM name="data-2-3" value="1981,2.7,4.6,2.1,0.9,0.4,0.2,">
* <PARAM name="data-2-4" value="1991,2.6,6.1,2.4,0.8,0.3,0.2,">
* <PARAM name="data-2-5" value="1997,2.0,6.4,2.9,0.8,0.4,0.2,">
* <PARAM name="data-2-6" value="2001,2.1,6.4,3.0,0.9,0.4,0.2,">
* <PARAM name="data-2-7" value="2011,2.3,6.2,3.6,1.0,0.7,0.2,">
* * @version 1.2 * @author Andrew Evans **/ public class PyramidGraph extends Applet implements AdjustmentListener { private String message = null; // For errors. private double[][][] values = null; // For the values. private String[][] columnText = null; // For the bar labels. private String[][] rowText = null; // For the scrollbar labels. private String prefix = null; // For the current scrollbar position label. private PyramidCanvas pyramidCanvas = null; // To show the graphs. private Scrollbar scrollbar = null; // To alter data shown. private int barHeight = 10; // Height of bars. private double max = 0.0; // Max value in data - used to determine scales. private double min = 0.0; // Min value in data - used to determine scales. private String biggestLabel = ""; // Max bar label - used to determine spacing on left of graph. private Color foreground = null; // Color for lines and text. private Color background = null; // Color for background. private Color[] colours = null; // Colors for data boxes. /** * Applet started in init.

* Pulls in data and sets up look. First * row of data displayed. **/ public void start() { setUpData(); setUpLook(); } /** * Method pulls in PARAMs, particually the filenames.

* Calls setValues with a list of filenames to pull in data. **/ private void setUpData() { // Get colours for foreground and background. foreground = getColor("foreground"); background = getColor("background"); setBackground(background); // Get the prefix for the label. prefix = getParameter("labelPrefix"); if (prefix == null) prefix = ""; // Get the height for the bars. String temp = getParameter("barHeight"); if (temp == null) { barHeight = 10; } else { try { barHeight = (new Integer(temp)).intValue(); } catch (NumberFormatException nfe) { message = "Setup error: barHeight PARAM must be an integer."; repaint(); } } // Get maximum graph axis point. temp = getParameter("max"); if (temp != null) { try { max = (new Double(temp)).doubleValue(); } catch (NumberFormatException nfe) { max = 0; message = "Setup error: max PARAM must be a number."; repaint(); } } // Get minimum graph axis point. temp = getParameter("min"); if (temp != null) { try { min = (new Double(temp)).doubleValue(); } catch (NumberFormatException nfe) { min = 0.0; message = "Setup error: min PARAM must be a number."; repaint(); } } // Try and get the number of PARAM blocks holding data. temp = getParameter("numberOfDataBlocks"); int numOfDataBlocksInt = 0; if (temp != null) { try { numOfDataBlocksInt = (new Integer(temp)).intValue(); } catch (NumberFormatException nfe) { numOfDataBlocksInt = 0; message = "Setup error: numberOfDataBlocks PARAM must be an integer."; repaint(); } } // Either start the data block reading, or... if (numOfDataBlocksInt != 0) { parseParamData(numOfDataBlocksInt); // Note that this sets up the colours too. } else { // ...get the list of filenames and their colours. String numOfFiles = getParameter("numberOfFiles"); if (numOfFiles == null) { message = "Setup error: numberOfFiles PARAM missing."; repaint(); } else { try { int numberOfFiles = (new Integer(numOfFiles)).intValue(); String[] files = new String[numberOfFiles]; colours = new Color[numberOfFiles]; for (int a = 0; a < numberOfFiles; a++) { files[a] = getParameter("file" + (a + 1)); if (files[a].equals(null)) { message = "Setup error: expected to find file" + (a + 1) + " on the basis \n of the numberOfFiles PARAM."; repaint(); } else { } colours[a] = getColor("colour" + (a + 1)); } // End looping through numberOfFiles. setValues(files); } catch (NumberFormatException nfe) { message = "Setup error: numberofFiles PARAM must be an integer."; repaint(); } } // End of if numOfFiles PARAM exists. } } // End setUpData(). /** * This method parses data from PARAM blocks. * See examples at the top of this document for format. * @param numberOfDataBlocks The number of half pyramids you want. **/ private void parseParamData(int numberOfDataBlocks) { values = new double[numberOfDataBlocks][][]; columnText = new String[numberOfDataBlocks][]; rowText = new String[numberOfDataBlocks][]; colours = new Color[numberOfDataBlocks]; String temp = null; // String to read lines into. StringTokenizer st = null; try { for (int i = 0; i < numberOfDataBlocks; i++) { colours[i] = getColor("colour" + (i + 1)); // First, check each block's dimensions. int rows = 0; int columns = 0; boolean first = true; while ((temp = getParameter("data-" + (i + 1) + "-" + (rows + 1))) != null) { if (first == true) { first = false; st = new StringTokenizer(temp," ,"); while (st.hasMoreTokens()) { st.nextToken(); columns++; } } rows++; } // Having counted the rows and columns, close and read in the data. // Set the size of the arrays to contain the data. values[i] = new double[rows -1][columns - 1]; columnText[i] = new String[columns]; rowText[i] = new String[rows - 1]; rows = 0; // Set flag for first line (bar labels). first = true; // Read lines. while ((temp = getParameter("data-" + (i + 1) + "-" + (rows + 1))) != null) { st = new StringTokenizer(temp," ,"); columns = 0; while (st.hasMoreTokens()) { if (first == true) { // If first line, read the bar labels. columnText[i][columns] = st.nextToken(); if (columnText[i][columns].length() > biggestLabel.length()) biggestLabel = columnText[i][columns]; } else { // If not first line. if (columns == 0) { // If first column, read dataset label. rowText[i][rows - 1] = st.nextToken(); } else { // If none of the above, read data. double t = (new Double(st.nextToken())).doubleValue(); values[i][rows - 1][columns - 1] = t; if (values[i][rows - 1][columns - 1] > max) max = values[i][rows - 1][columns - 1]; if (values[i][rows - 1][columns - 1] < min) min = values[i][rows - 1][columns - 1]; } // End if first column. } // End if first line. columns++; } // End reading across line. first = false; rows++; } // End reading lines from one block. } // End reading all blocks. } catch (Exception ioe) { message = "Setup error: there is an error in the PARAM data blocks."; repaint(); } } // End of parseParamData. /** * Method takes in an array of filenames, and drags the data out of them.

* The filenames are got from PARAMs in the setUpData() method. **/ private void setValues(String[] files) { int numOfFiles = files.length; values = new double[numOfFiles][][]; columnText = new String[numOfFiles][]; rowText = new String[numOfFiles][]; String temp = null; // String to read lines into. StringTokenizer st = null; try { for (int i = 0; i < numOfFiles; i++) { // First, check each file's dimensions. URL url = new URL(getCodeBase().toString() + files[i]); URLConnection urlc = url.openConnection(); BufferedReader br = new BufferedReader(new InputStreamReader(urlc.getInputStream())); int rows = 0; int columns = 0; boolean first = true; while ((temp = br.readLine()) != null) { if (first == true) { first = false; st = new StringTokenizer(temp," ,"); while (st.hasMoreTokens()) { st.nextToken(); columns++; } } rows++; } // Having counted the rows and columns, close and reopen the file at the beginning. br.close(); URL url2 = new URL(getCodeBase().toString() + files[i]); URLConnection urlc2 = url2.openConnection(); BufferedReader br2 = new BufferedReader(new InputStreamReader(urlc2.getInputStream())); // Set the size of the arrays to contain the data. values[i] = new double[rows -1][columns - 1]; columnText[i] = new String[columns]; rowText[i] = new String[rows - 1]; rows = 0; // Set flag for first line (bar labels). first = true; // Read lines. while ((temp = br2.readLine()) != null) { st = new StringTokenizer(temp," ,"); columns = 0; while (st.hasMoreTokens()) { if (first == true) { // If first line, read the bar labels. columnText[i][columns] = st.nextToken(); if (columnText[i][columns].length() > biggestLabel.length()) biggestLabel = columnText[i][columns]; } else { // If not first line. if (columns == 0) { // If first column, read dataset label. rowText[i][rows - 1] = st.nextToken(); } else { // If none of the above, read data. values[i][rows - 1][columns - 1] = (new Double(st.nextToken())).doubleValue(); if (values[i][rows - 1][columns - 1] > max) max = values[i][rows - 1][columns -1]; } // End if first column. } // End if first line. columns++; } // End reading across line. first = false; rows++; } // End reading lines from one file. // Close file. br2.close(); } // End reading all files. } catch (IOException ioe) {ioe.printStackTrace();} } // End setup(). /** * Method sets up look of Applet.

* Sets up a gridbagLayout with a PyramidCanvas object in the top * cell, and a scrollbar all the way across the one below. **/ private void setUpLook() { GridBagLayout gridBag = new GridBagLayout(); GridBagConstraints c = new GridBagConstraints(); // Setup constraints for PyramidCanvas. c.fill = GridBagConstraints.NONE; c.ipadx = 0; c.ipady = 0; c.gridx = 0; c.gridy = 0; c.gridwidth = 1; c.gridheight = 1; c.anchor = GridBagConstraints.SOUTHWEST; c.weightx = 1; c.weighty = 1; c.insets = new Insets(5,5,5,5); pyramidCanvas = new PyramidCanvas(background); // Continue setting up the layout. pyramidCanvas.setSize(this.getSize().width, this.getSize().height - 40); setLayout(gridBag); pyramidCanvas.setVisible(true); gridBag.setConstraints(pyramidCanvas, c); // Get size for various components. This doesn't appear to work // for Canvas, so we get them here, and pass the sizes to the PyramidCanvas. this.getGraphics().setFont(new Font("Serif",Font.PLAIN,12)); int widthStart = (getGraphics().getFontMetrics()).stringWidth(biggestLabel); int maxWidth = (getGraphics().getFontMetrics()).stringWidth(Double.toString(max)); int zeroWidth = (getGraphics().getFontMetrics()).stringWidth("0.0"); int labelWidth = (getGraphics().getFontMetrics()).stringWidth(rowText[0][rowText[0].length - 1]); int prefixAndLabelWidth = (getGraphics().getFontMetrics()).stringWidth(prefix + rowText[0][rowText[0].length - 1]); // Setup the PyramidCanvas. pyramidCanvas.setUp(values, columnText, rowText, prefix, colours, foreground, background, barHeight, max, min, widthStart, maxWidth, zeroWidth, labelWidth, prefixAndLabelWidth); add(pyramidCanvas); // Set constraints for scrollbar. c.fill = GridBagConstraints.HORIZONTAL; c.gridx = 0; c.gridy = 1; c.gridwidth = 1; c.gridheight = 1; c.anchor = GridBagConstraints.NORTHWEST; c.weightx = 0; c.weighty = 0; scrollbar = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, 0, values[0].length); scrollbar.setSize(300,20); scrollbar.setVisible(true); scrollbar.addAdjustmentListener(this); // Check the colour of the background, and set the background colour of // the scrollbar appropriately. if (((background.getRed() + background.getGreen() + background.getBlue()) / 3) > 127) scrollbar.setBackground(background.brighter()); else scrollbar.setBackground(background.darker()); gridBag.setConstraints(scrollbar, c); add(scrollbar); } // End of setUpLook(); /** * This method is enacted when the scrollbar is moved.

* It calls the setValue method of the PyramidCanvas with the scrollbar value and * then repaints it. This displays the appropriate dataset. **/ public void adjustmentValueChanged(AdjustmentEvent ae) { pyramidCanvas.setValue(scrollbar.getValue()); pyramidCanvas.repaint(); } /** * Update overridden to prevent flickering. **/ public void update() { Graphics g = getGraphics(); paint(g); } /** * The paint method is overridden to repaint the image and histogram canvases.

* The method also paints error messages on screen when necessary. **/ public void paint(Graphics g) { // If there's an error message, print it on screen. if (message != null) { g.setColor(new Color(0, 0, 0)); g.drawString(message,10,10); } } // End of paint(Graphics g). /** * This method returns a colour from a PARAM.

* The PARAM value should be a hash symbol and then a * standard web hex colour. If the PARAM is missing * white is returned for everything except the PARAM * "foreground", which returns black. **/ private Color getColor(String param) { String colorString = getParameter(param); if (colorString == null) { if (param.equals("forground")) return new Color(0,0,0); else return new Color(255,255,255); } else { try { int red = (Integer.decode("0x" + colorString.substring(1,3))).intValue(); int green = (Integer.decode("0x" + colorString.substring(3,5))).intValue(); int blue = (Integer.decode("0x" + colorString.substring(5))).intValue(); return new Color(red, green, blue); } catch (NumberFormatException nfe) { displayError(param + " PARAM not a hex number"); if (param.equals("forground")) return new Color(0,0,0); else return new Color(255,255,255); } } } // End of getColor(String param). /** * Method for dealing with errors.

* At present these are sent to the Java console and a warning is displayed * in the applet. **/ private void displayError(String errorString) { System.out.println(errorString); message = "PARAM error: see Java console"; repaint(); } // End of displayError(String errorString). // End of PyramidGraph class. }