/* * Sun Public License * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is available at http://www.sun.com/ * * The Original Code is the SLAMD Distributed Load Generation Engine. * The Initial Developer of the Original Code is Neil A. Wilson. * Portions created by Neil A. Wilson are Copyright (C) 2004-2010. * Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): Neil A. Wilson */ package com.slamd.stat; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Polygon; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.image.BufferedImage; import java.io.FileOutputStream; import java.text.DecimalFormat; import java.util.Arrays; import java.util.HashMap; import com.sun.media.jai.codec.ImageCodec; import com.sun.media.jai.codec.ImageEncoder; /** * This class provides a mechanism for generating graphs based on sets of data * obtained during job processing. Note that a maximum of 30 data sets can be * represented on a single graph. * * * @author Neil A. Wilson */ public class StatGrapher { /** * The set of colors that will be used when producing the graphs. */ public static final Color[] COLORS = new Color[] { new Color(0xFF, 0x20, 0x20), // Red new Color(0x20, 0xFF, 0x20), // Green new Color(0x00, 0x66, 0xCC), // Blue new Color(0xFF, 0xFF, 0x20), // Yellow new Color(0x20, 0xFF, 0xFF), // Cyan new Color(0x90, 0x80, 0x70), // Brown new Color(0xFF, 0x20, 0xFF), // Magenta new Color(0xA0, 0xA0, 0xA0), // Mid Gray new Color(0xFF, 0xA0, 0x40), // Orange new Color(0x90, 0x68, 0xC0), // Purple new Color(0x80, 0x00, 0x00), // Dark Red new Color(0x00, 0x80, 0x00), // Dark Green new Color(0x00, 0x00, 0x80), // Dark Blue new Color(0x80, 0x80, 0x00), // Dark Yellow new Color(0x00, 0x80, 0x80), // Dark Cyan new Color(0x60, 0x50, 0x40), // Dark Brown new Color(0x80, 0x00, 0x80), // Dark Magenta new Color(0x80, 0x80, 0x80), // Dark Gray new Color(0xC0, 0x58, 0x00), // Dark Orange new Color(0x60, 0x40, 0x80), // Dark Purple new Color(0xFF, 0x80, 0x80), // Light Red new Color(0x80, 0xFF, 0x80), // Light Green new Color(0x80, 0x80, 0XFF), // Light Blue new Color(0xFF, 0xFF, 0x80), // Light Yellow new Color(0x80, 0xFF, 0xFF), // Light Cyan new Color(0xC0, 0xB0, 0xA0), // Light Brown new Color(0xFF, 0xC0, 0xFF), // Light Magenta new Color(0xC3, 0xC3, 0xC3), // Light Gray new Color(0xFF, 0xA8, 0x58), // Light Orange new Color(0xC0, 0x80, 0xFF), // Light Purple new Color(0xFF, 0x00, 0x00), // Bright Red new Color(0x00, 0xFF, 0x00), // Bright Green new Color(0x00, 0x00, 0xFF), // Bright Blue new Color(0xFF, 0xFF, 0x00), // Bright Yellow new Color(0x00, 0xFF, 0xFF), // Bright Cyan new Color(0x66, 0x66, 0x00), // Bright Brown new Color(0xFF, 0x00, 0xFF), // Bright Magenta new Color(0xCC, 0XCC, 0xCC), // Bright Gray new Color(0x66, 0x00, 0x00), // Really Dark Red new Color(0x00, 0x66, 0x00), // Really Dark Green new Color(0x00, 0x00, 0x66), // Really Dark Blue new Color(0x66, 0x66, 0x00), // Really Dark Yellow new Color(0x00, 0x66, 0x66), // Really Dark Cyan new Color(0x66, 0x00, 0x66), // Really Dark Magenta new Color(0x33, 0x33, 0x33), // Really Dark Gray new Color(0xFF, 0xC0, 0xC0), // Light Red (Washed Out) new Color(0xC0, 0xFF, 0xC0), // Light Green (Washed Out) new Color(0xC0, 0xC0, 0xFF), // Light Blue (Washed Out) new Color(0xFF, 0xFF, 0xC0), // Light Yellow (Washed Out) new Color(0xC0, 0xFF, 0xFF), // Light Cyan (Washed Out) }; // Indicates whether the lower bound of the graph should be zero rather than // calculated based on the information in the data set. private boolean baseAtZero; // Indicates whether line graphs should be flat between data points rather // than directly connected. private boolean flatBetweenPoints; // Indicates whether zero values present in the data set should be ignored. private boolean ignoreZeroValues; // Indicates whether a line showing the average value will be included in the // graph. private boolean includeAverage; // Indicates whether the graph should include horizontal grid lines. private boolean includeHorizontalGrid; // Indicates whether the graph should include a legend on the right side. private boolean includeLegend; // Indicates whether the graph should include a regression line. private boolean includeRegression; // Indicates whether the graph should include vertical grid lines. private boolean includeVerticalGrid; // Indicates whether the pie graph should show the percentage for each // category. protected boolean showPercentages; // The decimal format that will be used to format numeric values for display. private DecimalFormat decimalFormat; // The maximum value in the provided data sets. private double max; // The minimum value in the provided data sets. private double min; // The maximum value that will be displayed on the graph. private double graphMax; // The minimum value that will be displayed on the graph. private double graphMin; // The difference between graphMax and graphMin. private double graphSpan; // The sum of all the x values provided. private double sx; // The sum of the squares of all the x values provided. private double sxx; // The sum of the products of each x and y value provided. private double sxy; // The sum of all the y values provided. private double sy; // The sum of the squares of all the y values provided. private double syy; // The data to be graphed. private double[][] data; // The average values to use for data sets when generating stacked bar graphs. private double[][] dataSetAverages; // The number of data values provided. private int n; // The total width of the drawable area. private final int width; // The total height of the drawable area. private final int height; // The height of the graph area. private int graphHeight; // The X coordinate for the graph's origin. private int originX; // The Y coordinate for the graph's origin. private int originY; // The X coordinate for the lower right corner of the graph. private int lowerRightX; // The Y coordinate for the upper left corner of the graph. private int upperLeftY; // The number of seconds held in the data sets. private int numSeconds; // The number of seconds to use when starting the graph. private int startSeconds; // The collection interval used by the stat trackers. private int[] collectionIntervals; // The caption to include at the top of the graph. private final String graphTitle; // The caption to include along the horizontal axis. private String horizontalAxisTitle; // The caption to include at the top of the legend. private String legendTitle; // The caption to include along the vertical axis. private String verticalAxisTitle; // The set of labels to use for each data set (makes it possible to create the // legend along the right side. private String[] dataSetLabels; // The set of data set names for use in stacked bar graphs. private String[] dataSetNames; // The set of category names to use when generating stacked bar graphs. private String[][] categoryNames; /** * Creates a new stat grapher with the specified information. * * @param width The width of the drawable area to create. * @param height The height of the drawable area to create. * @param graphTitle The caption to include at the top of the graph. */ public StatGrapher(final int width, final int height, final String graphTitle) { this.width = width; this.height = height; this.graphTitle = graphTitle; this.horizontalAxisTitle = "Elapsed Time (seconds)"; this.verticalAxisTitle = ""; this.includeLegend = false; this.legendTitle = ""; this.includeHorizontalGrid = false; this.includeVerticalGrid = false; this.flatBetweenPoints = false; dataSetNames = new String[0]; categoryNames = new String[0][]; dataSetAverages = new double[0][]; includeAverage = false; decimalFormat = new DecimalFormat("0.00"); data = new double[0][]; collectionIntervals = new int[0]; dataSetLabels = new String[0]; max = -Double.MAX_VALUE; min = Double.MAX_VALUE; numSeconds = 0; startSeconds = 0; n = 0; sx = 0.0; sy = 0.0; sxx = 0.0; syy = 0.0; sxy = 0.0; } /** * Adds the specified data to the set of information that will be graphed. * * @param dataValues The of data to include in the set of * information that will be graphed. * @param collectionInterval The collection interval associated with this * data set. * @param dataSetLabel The label to use for this data set in the * legend. */ public void addDataSet(final double[] dataValues, final int collectionInterval, final String dataSetLabel) { final double[][] newData = new double[data.length+1][]; final int[] newIntervals = new int[collectionIntervals.length+1]; final String[] newLabels = new String[dataSetLabels.length+1]; System.arraycopy(data, 0, newData, 0, data.length); System.arraycopy(collectionIntervals, 0, newIntervals, 0, collectionIntervals.length); System.arraycopy(dataSetLabels, 0, newLabels, 0, dataSetLabels.length); newData[data.length] = dataValues; newIntervals[collectionIntervals.length] = collectionInterval; newLabels[dataSetLabels.length] = dataSetLabel; data = newData; collectionIntervals = newIntervals; dataSetLabels = newLabels; if (((dataValues.length + 1) * collectionInterval) > numSeconds) { numSeconds = ((dataValues.length + 1) * collectionInterval); } for (int i=0; i < dataValues.length; i++) { final double value = dataValues[i]; if (! (ignoreZeroValues && Double.isNaN(value))) { if (value > max) { max = value; } if (value < min) { min = value; } sx += (collectionInterval*i); sy += value; sxx += (collectionInterval*collectionInterval*i*i); syy += (value*value); sxy += (collectionInterval*i*value); n++; } } } /** * Adds the specified data for use in generating a stacked bar graph. * * @param dataSetName The overall name of the data set. * @param categoryNames The names of the categories in the data set. * @param categoryAverages The average values for each category of the data * set. */ public void addStackedBarGraphDataSet(final String dataSetName, final String[] categoryNames, final double[] categoryAverages) { final String[] newSetNames = new String[dataSetNames.length+1]; System.arraycopy(dataSetNames, 0, newSetNames, 0, dataSetNames.length); newSetNames[dataSetNames.length] = dataSetName; dataSetNames = newSetNames; final String[][] newCatNames = new String[this.categoryNames.length+1][]; System.arraycopy(this.categoryNames, 0, newCatNames, 0, this.categoryNames.length); newCatNames[this.categoryNames.length] = categoryNames; this.categoryNames = newCatNames; final double[][] newAverages = new double[dataSetAverages.length+1][]; System.arraycopy(dataSetAverages, 0, newAverages, 0, dataSetAverages.length); newAverages[dataSetAverages.length] = categoryAverages; dataSetAverages = newAverages; min = 0.0; double total = 0.0; for (int i=0; i < categoryAverages.length; i++) { total += categoryAverages[i]; } if (total > max) { max = total; } } /** * Indicates whether line graphs should have a flat horizontal line followed * by a vertical line between data points, or if the points should be directly * connected. * * @param flatBetweenPoints Indicates whether line graphs should have a flat * horizontal line followed by a vertical line * between data points. */ public void setFlatBetweenPoints(final boolean flatBetweenPoints) { this.flatBetweenPoints = flatBetweenPoints; } /** * Indicates whether the graph should ignore data intervals where the value * for that interval is zero. Note that if this is to be used, it should be * set before any calls to <CODE>addDataSet</CODE> are made. * * @param ignoreZeroValues Indicates whether the graph should ignore data * intervals where the value for that interval is * zero. */ public void setIgnoreZeroValues(final boolean ignoreZeroValues) { this.ignoreZeroValues = ignoreZeroValues; } /** * Indicates whether the graph should include a line that indicates the * average of all values provided. * * @param includeAverage Indicates whether the graph should include a line * that indicates the average of all values provided. */ public void setIncludeAverage(final boolean includeAverage) { this.includeAverage = includeAverage; } /** * Indicates whether the graph should include a trend line based on a linear * regression calculation of all the values. * * @param includeRegression Indicates whether the graph should include a * regression line. */ public void setIncludeRegression(final boolean includeRegression) { this.includeRegression = includeRegression; } /** * Indicates whether the lower bound of the graph should be at zero or should * be dynamically calculated based on information in the data set. * * @param baseAtZero Indicates whether the lower bound of the graph should * be at zero or should be dynamically calculated based on * information in the data set. */ public void setBaseAtZero(final boolean baseAtZero) { this.baseAtZero = baseAtZero; } /** * Indicates whether the generated graph should include a legend. * * @param includeLegend Indicates whether the generated graph should * include a legend. * @param legendTitle The title to use for the legend if it is included. */ public void setIncludeLegend(final boolean includeLegend, final String legendTitle) { this.includeLegend = includeLegend; this.legendTitle = legendTitle; } /** * Indicates whether the generated graph should include horizontal grid lines. * * @param includeHorizontalGrid Indicates whether the generated graph should * include horizontal grid lines. */ public void setIncludeHorizontalGrid(final boolean includeHorizontalGrid) { this.includeHorizontalGrid = includeHorizontalGrid; } /** * Indicates whether the generated graph should include horizontal grid lines. * * @param includeVerticalGrid Indicates whether the generated graph should * include vertical grid lines. */ public void setIncludeVerticalGrid(final boolean includeVerticalGrid) { this.includeVerticalGrid = includeVerticalGrid; } /** * Indicates whether the generated pie graph should show the percentages for * each category. * * @param showPercentages Indicates whether the generated pie graph should * show the percentages for each category. */ public void setShowPercentages(final boolean showPercentages) { this.showPercentages = showPercentages; } /** * Specifies the title to be used for the horizontal axis of the generated * graph. * * @param horizontalAxisTitle The title to be used for the horizontal axis * of the generated graph. */ public void setHorizontalAxisTitle(final String horizontalAxisTitle) { this.horizontalAxisTitle = horizontalAxisTitle; } /** * Specifies the title to be used for the vertical axis of the generated * graph. * * @param verticalAxisTitle The title to be used for the vertical axis of * the generated graph. */ public void setVerticalAxisTitle(final String verticalAxisTitle) { this.verticalAxisTitle = verticalAxisTitle; } /** * Specifies the number of seconds into the test that the graph starts. * * @param startSeconds The number of seconds into the test that the graph * starts. */ public void setStartSeconds(final int startSeconds) { this.startSeconds = startSeconds; } /** * Generates a pie graph based on the information that has been provided. * * @param categoryNames The names of the categories of each of the * elements. * @param occurrencesPerCategory The number of occurrences of the tracked * event in each category. * * @return A buffered image containing the generated pie graph. */ public BufferedImage generatePieGraph(final String[] categoryNames, final int[] occurrencesPerCategory) { // Calculate percentages based on the information provided. int totalOccurrences = 0; final String[] names = new String[categoryNames.length]; System.arraycopy(categoryNames, 0, names, 0, categoryNames.length); for (int i=0; i < occurrencesPerCategory.length; i++) { totalOccurrences += occurrencesPerCategory[i]; } final double[] percentages = new double[occurrencesPerCategory.length]; for (int i=0; i < percentages.length; i++) { percentages[i] = 1.0 * occurrencesPerCategory[i] / totalOccurrences; if (showPercentages) { names[i] += " (" + decimalFormat.format(percentages[i] * 100) + "%)"; } } // Convert those percentages to degrees. int totalDegrees = 0; final double[] dblDegrees = new double[percentages.length]; final int[] degrees = new int[percentages.length]; for (int i=0; i < percentages.length; i++) { dblDegrees[i] = 360.0 * percentages[i]; degrees[i] = (int) Math.round(360.0 * percentages[i]); totalDegrees += degrees[i]; } // Because the number of degrees in the arc must be an integer, rounding // errors can occur that make the total number of degrees in the pie greater // or less than 360. If that occurs, then "play with" the numbers a little // until we get something that rounds to 360. Although I haven't done the // math, it's probably possible for this to cause an infinite loop. To // prevent that from occurring, limit the number of times the numbers will // be nudged. double fudgeFactor = 0.1; int iterations = 0; while ((totalDegrees != 360) && (iterations < 20)) { if (totalDegrees > 360) { totalDegrees = 0; for (int i=0; i < degrees.length; i++) { dblDegrees[i] -= fudgeFactor; degrees[i] = (int) Math.round(dblDegrees[i]); totalDegrees += degrees[i]; } } else { totalDegrees = 0; for (int i=0; i < degrees.length; i++) { dblDegrees[i] += fudgeFactor; degrees[i] = (int) Math.round(dblDegrees[i]); totalDegrees += degrees[i]; } } fudgeFactor /= 2; iterations++; } // Create the image and get the graphics context. final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED); final Graphics2D g = image.createGraphics(); // Configure the graph to enable antialiasing. final HashMap<RenderingHints.Key,Object> renderingHints = new HashMap<RenderingHints.Key,Object>(3); renderingHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); renderingHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.addRenderingHints(renderingHints); // Give the graph a white background and use black to add text and the axes. g.setColor(Color.white); g.fillRect(0, 0, width, height); g.setColor(Color.black); // Set the fonts to use for the grapher. final Font titleFont = selectFont(g, "SansSerif", Font.BOLD, 18, graphTitle, width); final Font legendTitleFont = new Font("SansSerif", Font.BOLD, 12); final Font legendItemFont = new Font("SansSerif", Font.PLAIN, 10); final FontMetrics titleMetrics = g.getFontMetrics(titleFont); final FontMetrics legendTitleMetrics = g.getFontMetrics(legendTitleFont); final FontMetrics legendItemMetrics = g.getFontMetrics(legendItemFont); // Draw the title at the top of the graph. g.setFont(titleFont); final int captionWidth = titleMetrics.stringWidth(graphTitle); int x = (width - captionWidth) / 2; int y = titleMetrics.getHeight() + 2; g.drawString(graphTitle, x, y); // Add the legend along the side. int legendWidth = legendTitleMetrics.getHeight() + 2; if (includeLegend) { final int captionHeight = legendTitleMetrics.getHeight(); final int captionAscent = legendItemMetrics.getAscent(); int maxLabelWidth = legendTitleMetrics.stringWidth(legendTitle); for (int i=0; i < names.length; i++) { final int labelWidth = legendItemMetrics.stringWidth(names[i]) + captionHeight + 2; if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } } legendWidth += maxLabelWidth + 6; final int legendHeight = ((names.length+1) * captionHeight) + 4; // Draw the caption at the top of the legend. int labelX = width - ((maxLabelWidth+legendTitleMetrics.stringWidth(legendTitle))/2) - 6; int labelY = ((height - legendHeight) / 2) + captionHeight + 2; g.setFont(legendTitleFont); g.drawString(legendTitle, labelX, labelY); g.setFont(legendItemFont); labelY += captionHeight; // Draw the legend entries using the appropriate colors. labelX = width - maxLabelWidth - 6; for (int i=0; i < names.length; i++) { g.setColor(COLORS[i % COLORS.length]); g.fillRect(labelX, labelY-captionAscent, captionAscent, captionAscent); g.setColor(Color.black); g.drawString(names[i], labelX + captionHeight + 2, labelY); labelY += captionHeight; } g.setColor(Color.black); g.drawRect((labelX - 2), ((height - legendHeight) / 2), (maxLabelWidth + 4), legendHeight); } // Determine the bounds to use for the pie. final int usableWidth = width - legendWidth; final int usableHeight = height - legendTitleMetrics.getHeight() - 2; final int size = Math.min(usableWidth, usableHeight) * 9 / 10; x = (usableWidth - size) / 2; y = ((usableHeight - size) / 2) + legendTitleMetrics.getHeight() + 2; // Draw the whole circle using the first color to hide any gaps that might // still remain. g.setColor(COLORS[0]); g.fillOval(x, y, size, size); // Generate the pie. int startAngle = 0; for (int i=0; i < degrees.length; i++) { g.setColor(COLORS[i % COLORS.length]); g.fillArc(x, y, size, size, startAngle, degrees[i]); startAngle += degrees[i]; } // Return the completed image return image; } /** * Generates a line graph based on the information that has been provided. * * @return A buffered image containing the generated line graph. */ public BufferedImage generateLineGraph() { // Create the image and get the graphics context. final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED); final Graphics2D g = image.createGraphics(); // Configure the graph to enable antialiasing. final HashMap<RenderingHints.Key,Object> renderingHints = new HashMap<RenderingHints.Key,Object>(3); renderingHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); renderingHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.addRenderingHints(renderingHints); // Give the graph a white background and use black to add text and the axes. g.setColor(Color.white); g.fillRect(0, 0, width, height); g.setColor(Color.black); // Set the fonts to use for the grapher. final Font titleFont = selectFont(g, "SansSerif", Font.BOLD, 18, graphTitle, width); final Font axisLabelFont = selectFont(g, "SansSerif", Font.PLAIN, 12, verticalAxisTitle, height); final Font tickLabelFont = new Font("SansSerif", Font.PLAIN, 10); final Font legendTitleFont = new Font("SansSerif", Font.BOLD, 12); final Font legendItemFont = new Font("SansSerif", Font.PLAIN, 10); final FontMetrics titleMetrics = g.getFontMetrics(titleFont); final FontMetrics axisLabelMetrics = g.getFontMetrics(axisLabelFont); final FontMetrics tickLabelMetrics = g.getFontMetrics(tickLabelFont); final FontMetrics legendTitleMetrics = g.getFontMetrics(legendTitleFont); final FontMetrics legendItemMetrics = g.getFontMetrics(legendItemFont); // Calculate the real max and min values to use for the Y axis. There will // be a 5% border on the top and bottom. Note that if all values were the // same for the entire span, we need to "adjust" the numbers so that the // graph will render. double span = max - min; if (span < 0.00001) { span += 1.0; max += 0.5; if (min >= 0.5) { min -= 0.5; } } graphMax = max / 0.95; if (baseAtZero) { graphMin = 0; } else { graphMin = min - (graphMax * 0.05); // In case the graph min would be negative where the real min doesn't go // below zero, then make the graph min zero. if ((graphMin < 0) && (min >= 0)) { graphMin = 0; } // Otherwise, see if we can decrease the minimum by a little to make it a // relatively nice number. Do this by finding the largest power of ten // that is less than or equal to the span and making the minimum value in // the graph a multiple of that power of ten. graphSpan = graphMax - graphMin; final int largestPowerOfTen = (int) (Math.log(graphSpan) / Math.log(10)); if (largestPowerOfTen > 0) { graphMin = ((int) (graphMin / Math.pow(10, largestPowerOfTen))) * Math.pow(10, largestPowerOfTen); } } graphSpan = graphMax - graphMin; // The top of the graph will be just below the bottom of the title, and // the bottom of the graph will be twice the distance from the bottom of the // image. upperLeftY = titleMetrics.getHeight() + 5; originY = height - (2 * axisLabelMetrics.getHeight()) - tickLabelMetrics.getHeight(); graphHeight = originY - upperLeftY + 1; // Add the legend along the side. int legendWidth = axisLabelMetrics.getHeight() + 2; if (includeLegend) { int maxLabelWidth = legendTitleMetrics.stringWidth(legendTitle); for (int i=0; i < dataSetLabels.length; i++) { final int labelWidth = legendItemMetrics.stringWidth(dataSetLabels[i]) + legendItemMetrics.getHeight() + 2; if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } } legendWidth += maxLabelWidth + 6; final int legendHeight = 4 + legendTitleMetrics.getHeight() + (dataSetLabels.length * legendItemMetrics.getHeight()); // Draw the caption at the top of the legend. int labelX = width - ((maxLabelWidth + legendTitleMetrics.stringWidth(legendTitle)) / 2) - 6; int labelY = ((height - legendHeight) / 2) + legendTitleMetrics.getAscent() + 2; g.setFont(legendTitleFont); g.drawString(legendTitle, labelX, labelY); labelY += legendTitleMetrics.getHeight(); g.setFont(legendItemFont); // Draw the legend entries using the appropriate colors. labelX = width - maxLabelWidth - 6; for (int i=0; i < dataSetLabels.length; i++) { final int captionAscent = legendItemMetrics.getAscent(); final int captionHeight = legendItemMetrics.getHeight(); g.setColor(COLORS[i % COLORS.length]); g.fillRect(labelX, labelY-captionAscent, captionAscent, captionAscent); g.setColor(Color.black); g.drawString(dataSetLabels[i], labelX + captionHeight + 2, labelY); labelY += captionHeight; } g.setColor(Color.black); g.drawRect((labelX - 2), ((height - legendHeight) / 2), (maxLabelWidth + 4), legendHeight); } // The graph should be centered horizontally, so make the right border the // same distance from the image edge as the left border. lowerRightX = width - legendWidth; // Draw the vertical axis caption. It should be rotated 90 degrees, which // makes for some funky math. Just trust that this works. g.rotate(1.5*Math.PI); int captionWidth = axisLabelMetrics.stringWidth(verticalAxisTitle); g.setFont(axisLabelFont); g.drawString(verticalAxisTitle, -((height+captionWidth)/2), (axisLabelMetrics.getHeight()-2)); g.rotate(Math.PI/2); // Figure out how many labels to draw along the vertical axis. It will // basically be double-spaced, so figure out how many labels could fit along // the vertical axis and divide by 2. final int numVerticalLabels = graphHeight / tickLabelMetrics.getHeight() / 2; // If there is space for more vertical labels than there are integers, then // go up by integer values. Otherwise, use the calculated number of labels. int labelX = 5 + axisLabelMetrics.getHeight(); int labelY = originY; int maxLabelWidth = 0; g.setFont(tickLabelFont); if (numVerticalLabels > graphSpan) { if (numVerticalLabels > (2 * graphSpan)) { final double increment = graphSpan / numVerticalLabels; for (double value = graphMin; value < graphMax; value += increment) { labelY = valueYToGraphY(value); final String label = decimalFormat.format(value); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } else { for (double value = graphMin; value < graphMax; value++) { labelY = valueYToGraphY(value); final String label = decimalFormat.format(value); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels); for (double valueY = graphMin; valueY < graphMax; valueY += increment) { labelY = valueYToGraphY(valueY); final String label = decimalFormat.format(valueY); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } // Draw the vertical axis and add the tick marks along its side originX = labelX + maxLabelWidth + 5; g.setColor(new Color(0xF1, 0xF1, 0xF1)); g.fillRect(originX, upperLeftY, (lowerRightX - originX), (originY - upperLeftY)); g.setColor(Color.BLACK); g.drawLine(originX, upperLeftY, originX, originY); labelY = originY; if (numVerticalLabels > graphSpan) { if (numVerticalLabels > (2 * graphSpan)) { final double increment = graphSpan / numVerticalLabels; for (double value = graphMin; value < graphMax; value += increment) { labelY = valueYToGraphY(value); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } else { for (double value = graphMin; value < graphMax; value++) { labelY = valueYToGraphY(value); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels); for (double valueY = graphMin; valueY < graphMax; valueY += increment) { labelY = valueYToGraphY(valueY); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } // We know that the number of intervals will go from startSeconds to // startSeconds+n, so the width of startSeconds+n should be roughly equal to // the greatest width. final String label = String.valueOf(startSeconds+numSeconds); maxLabelWidth = tickLabelMetrics.stringWidth(label); final int maxHorizontalLabels = (lowerRightX - originX) / maxLabelWidth / 2; int secondsPerInterval; if ((numSeconds > maxHorizontalLabels) && (maxHorizontalLabels > 0)) { secondsPerInterval = numSeconds / maxHorizontalLabels; if ((secondsPerInterval % collectionIntervals[0]) != 0) { secondsPerInterval = (secondsPerInterval / collectionIntervals[0] + 1) * collectionIntervals[0]; } } else { secondsPerInterval = 1; } // Draw the labels at the bottom of the horizontal axis. The tick marks // can also be added at the same time. labelY = originY + tickLabelMetrics.getHeight() + 2; for (int value = 0; value < numSeconds; value += secondsPerInterval) { final String valueString = String.valueOf(startSeconds+value); labelX = valueXToGraphX(value); final int width = tickLabelMetrics.stringWidth(valueString); g.drawString(valueString, (labelX-(width/2)), labelY); if (includeVerticalGrid && (value != 0)) { g.setColor(Color.LIGHT_GRAY); g.drawLine(labelX, originY, labelX, upperLeftY); g.setColor(Color.BLACK); } g.drawLine(labelX, (originY - 2), labelX, (originY + 2)); } // Now draw the horizontal axis. g.drawLine(originX, originY, lowerRightX, originY); // Add the caption at the top of the image g.setFont(titleFont); captionWidth = titleMetrics.stringWidth(graphTitle); int captionX = ((lowerRightX - originX - captionWidth) / 2) + originX; int captionY = titleMetrics.getAscent() + 2; g.drawString(graphTitle, captionX, captionY); g.setFont(new Font("SansSerif", Font.PLAIN, 14)); // Add the horizontal axis caption at the bottom of the image captionWidth = axisLabelMetrics.stringWidth(horizontalAxisTitle); captionX = ((lowerRightX - originX - captionWidth) / 2) + originX; captionY = (height - axisLabelMetrics.getDescent() - 2); g.setFont(axisLabelFont); g.drawString(horizontalAxisTitle, captionX, captionY); // Iterate through the data and make the line graphs. Make sure that we // don't draw more data sets than we have colors available. final Stroke defaultStroke = g.getStroke(); g.setStroke(new BasicStroke(2.0f)); for (int i=0; ((i < data.length) && (i < COLORS.length)); i++) { g.setColor(COLORS[i]); int j, x1, x2, y1, y2; for (j=0; ((j < data[i].length) && ignoreZeroValues && Double.isNaN(data[i][j])); j++); if (j >= data[i].length) { continue; } x1 = valueXToGraphX((j+1)*collectionIntervals[i]); y1 = valueYToGraphY(data[i][j]); for (j=j+1; j < data[i].length; j++) { if (! (ignoreZeroValues && Double.isNaN(data[i][j]))) { x2 = valueXToGraphX((j+1) * collectionIntervals[i]); y2 = valueYToGraphY(data[i][j]); if (flatBetweenPoints) { g.drawLine(x1, y1, x2, y1); g.drawLine(x2, y1, x2, y2); } else { g.drawLine(x1, y1, x2, y2); } x1 = x2; y1 = y2; } } } // Draw a line with the average value, if specified. g.setStroke(defaultStroke); g.setColor(Color.BLACK); if (includeAverage) { final int y = valueYToGraphY(sy/n); g.drawLine(originX, y, lowerRightX, y); } // Draw the regression line, if specified. if (includeRegression) { final double b = (sxy - (sx*sy)/n) / (sxx - (sx*sx)/n); final double a = (sy - b*sx) / n; final int y1 = valueYToGraphY(a); final int y2 = valueYToGraphY(a + b*(numSeconds-1)); g.drawLine(originX, y1, lowerRightX, y2); } return image; } /** * Generates a line graph by plotting the provided data and connecting those * points with lines. * * @param xValues The sets of x coordinates for the data to graph. * @param yValues The sets of y coordinates for the data to graph. * @param labels The labels to use in the legend. * @param drawPoints Indicates whether the individual data points should be * clearly marked with dots. * @param baseXAtZero Indicates whether to start the x coordinates at zero * or the first x coordinate. * * @return A buffered image containing the generated line graph. */ public BufferedImage generateXYLineGraph(final double[][] xValues, final double[][] yValues, final String[] labels, final boolean drawPoints, final boolean baseXAtZero) { // Create the image and get the graphics context. final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED); final Graphics2D g = image.createGraphics(); // Configure the graph to enable antialiasing. final HashMap<RenderingHints.Key,Object> renderingHints = new HashMap<RenderingHints.Key,Object>(3); renderingHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); renderingHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.addRenderingHints(renderingHints); // Give the graph a white background and use black to add text and the axes. g.setColor(Color.white); g.fillRect(0, 0, width, height); g.setColor(Color.black); // If there was no data provided, or if the amount of data provided isn't // suited for graphing, then return an empty image. if ((xValues == null) || (xValues.length == 0) || (yValues == null) || (yValues.length == 0) || (yValues.length != xValues.length)) { if ((xValues == null) || (yValues == null) || (xValues.length == 0) || (yValues.length == 0)) { System.err.println("Unable to generate XY line graph -- No data " + "provided for x and/or y coordinates."); } else { System.err.println("Unable to generate XY line graph -- Number of X " + "coordinates does not match number of Y " + "coordinates."); } return image; } // Set the fonts to use for the grapher. final Font titleFont = selectFont(g, "SansSerif", Font.BOLD, 18, graphTitle, width); final Font axisLabelFont = selectFont(g, "SansSerif", Font.PLAIN, 12, verticalAxisTitle, height); final Font tickLabelFont = new Font("SansSerif", Font.PLAIN, 10); final Font legendTitleFont = new Font("SansSerif", Font.BOLD, 12); final Font legendItemFont = new Font("SansSerif", Font.PLAIN, 10); final FontMetrics titleMetrics = g.getFontMetrics(titleFont); final FontMetrics axisLabelMetrics = g.getFontMetrics(axisLabelFont); final FontMetrics tickLabelMetrics = g.getFontMetrics(tickLabelFont); final FontMetrics legendTitleMetrics = g.getFontMetrics(legendTitleFont); final FontMetrics legendItemMetrics = g.getFontMetrics(legendItemFont); // Calculate the real max and min values to use for the Y axis. There will // be a 5% border on the top and bottom. Note that if all values were the // same for the entire span, we need to "adjust" the numbers so that the // graph will render. double xMax = xValues[0][0]; double xMin = xValues[0][0]; double yMax = yValues[0][0]; double yMin = yValues[0][0]; double yAvg = 0.0; int yCount = 0; for (int i=0; i < xValues.length; i++) { if (xValues[i].length != yValues[i].length) { System.err.println("Unable to generate XY line graph -- Number of " + "x and y coordinates are not the same."); return image; } for (int j=0; j < xValues[i].length; j++) { if (xValues[i][j] > xMax) { xMax = xValues[i][j]; } if (xValues[i][j] < xMin) { if (j == 0) { xMin = xValues[i][j]; } else { System.err.println("Unable to generate XY line graph -- X " + "coordinates are not in increasing order."); return image; } } if (yValues[i][j] > yMax) { yMax = yValues[i][j]; } if (yValues[i][j] < yMin) { yMin = yValues[i][j]; } yAvg += yValues[i][j]; yCount++; } } yAvg = yAvg / yCount; double ySpan = (yMax - yMin); if (ySpan < 0.00001) { ySpan += 1.0; yMax += 0.5; if (yMin >= 0.5) { yMin -= 0.5; } } if (baseXAtZero) { if (xMin < 0) { System.err.println("Unable to generate XY line graph -- Smallest x " + "coordinate is less than zero."); return image; } xMin = 0.0; } final double xSpan = (xMax - xMin); if (xSpan < 0.00001) { System.err.println("Unable to generate XY line graph -- Span of x " + "values is too small."); return image; } graphMax = yMax / 0.95; if (baseAtZero) { graphMin = 0; } else { graphMin = yMin - (graphMax * 0.05); // In case the graph min would be negative where the real min doesn't go // below zero, then make the graph min zero. if ((graphMin < 0) && (yMin >= 0)) { graphMin = 0; } // Otherwise, see if we can decrease the minimum by a little to make it a // relatively nice number. Do this by finding the largest power of ten // that is less than or equal to the span and making the minimum value in // the graph a multiple of that power of ten. graphSpan = graphMax - graphMin; final int largestPowerOfTen = (int) (Math.log(graphSpan) / Math.log(10)); if (largestPowerOfTen > 0) { graphMin = ((int) (graphMin / Math.pow(10, largestPowerOfTen))) * Math.pow(10, largestPowerOfTen); } } graphSpan = graphMax - graphMin; // The top of the graph will be just below the bottom of the caption, and // the bottom of the graph will be twice the distance from the bottom of the // image. upperLeftY = titleMetrics.getHeight() + 5; originY = height - (2*upperLeftY); graphHeight = originY - upperLeftY + 1; int legendWidth = axisLabelMetrics.getHeight() + 2; if (includeLegend) { int maxLabelWidth = legendTitleMetrics.stringWidth(legendTitle); for (int i=0; i < labels.length; i++) { final int labelWidth = legendItemMetrics.stringWidth(labels[i]) + legendItemMetrics.getHeight() + 2; if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } } legendWidth += maxLabelWidth + 6; final int legendHeight = 4 + legendTitleMetrics.getHeight() + (dataSetLabels.length * legendItemMetrics.getHeight()); // Draw the caption at the top of the legend. int labelX = width - ((maxLabelWidth+legendTitleMetrics.stringWidth(legendTitle))/2) - 6; int labelY = ((height - legendHeight) / 2) + legendTitleMetrics.getHeight() + 2; g.setFont(legendTitleFont); g.drawString(legendTitle, labelX, labelY); g.setFont(legendItemFont); labelY += legendTitleMetrics.getHeight(); // Draw the legend entries using the appropriate colors. labelX = width - maxLabelWidth - 6; for (int i=0; i < labels.length; i++) { final int captionAscent = legendItemMetrics.getAscent(); final int captionHeight = legendItemMetrics.getHeight(); g.setColor(COLORS[i % COLORS.length]); g.fillRect(labelX, labelY-captionAscent, captionAscent, captionAscent); g.setColor(Color.black); g.drawString(labels[i], labelX + captionHeight + 2, labelY); labelY += captionHeight; } g.setColor(Color.black); g.drawRect((labelX - 2), ((height - legendHeight) / 2), (maxLabelWidth + 4), legendHeight); } // The graph should be centered horizontally, so make the right border the // same distance from the image edge as the left border. lowerRightX = width - legendWidth; // Draw the vertical axis caption. It should be rotated 90 degrees, which // makes for some funky math. Just trust that this works. g.rotate(1.5*Math.PI); int captionWidth = axisLabelMetrics.stringWidth(verticalAxisTitle); g.setFont(axisLabelFont); g.drawString(verticalAxisTitle, -((height+captionWidth)/2), (axisLabelMetrics.getHeight()-2)); g.setFont(tickLabelFont); g.rotate(Math.PI/2); // Figure out how many labels to draw along the vertical axis. It will // basically be double-spaced, so figure out how many labels could fit along // the vertical axis and divide by 2. final int numVerticalLabels = graphHeight / tickLabelMetrics.getHeight() / 2; // If there is space for more vertical labels than there are integers, then // go up by integer values. Otherwise, use the calculated number of labels. int labelX = 5 + axisLabelMetrics.getHeight(); int labelY = originY; int maxLabelWidth = 0; if (numVerticalLabels > graphSpan) { if (numVerticalLabels > (2 * graphSpan)) { final double increment = graphSpan / numVerticalLabels; for (double value = graphMin; value < graphMax; value += increment) { labelY = valueYToGraphY(value); final String label = decimalFormat.format(value); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } else { for (double value = graphMin; value < graphMax; value++) { labelY = valueYToGraphY(value); final String label = decimalFormat.format(value); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels); for (double valueY = graphMin; valueY < graphMax; valueY += increment) { labelY = valueYToGraphY(valueY); final String label = decimalFormat.format(valueY); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } // Draw the vertical axis and add the tick marks along its side originX = labelX + maxLabelWidth + 5; g.setColor(new Color(0xF1, 0xF1, 0xF1)); g.fillRect(originX, upperLeftY, (lowerRightX - originX), (originY - upperLeftY)); g.setColor(Color.BLACK); g.drawLine(originX, upperLeftY, originX, originY); labelY = originY; if (numVerticalLabels > graphSpan) { if (numVerticalLabels > (2 * graphSpan)) { final double increment = graphSpan / numVerticalLabels; for (double value = graphMin; value < graphMax; value += increment) { labelY = valueYToGraphY(value); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } else { for (double value = graphMin; value < graphMax; value++) { labelY = valueYToGraphY(value); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels); for (double valueY = graphMin; valueY < graphMax; valueY += increment) { labelY = valueYToGraphY(valueY); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } // We know that the x values are in increasing order, so the greatest width // should be roughly equal to the width of the last value. We'll use the // same logic to determine the label increment as we used for the vertical // axis. final String label = decimalFormat.format(xMax); maxLabelWidth = tickLabelMetrics.stringWidth(label); final int maxHorizontalLabels = (lowerRightX - originX) / maxLabelWidth / 2; final double secondsPerInterval = chooseVerticalLabelIncrement(maxHorizontalLabels, xSpan); // Draw the labels at the bottom of the horizontal axis. The tick marks // can also be added at the same time. labelY = originY + tickLabelMetrics.getHeight() + 2; for (double value = 0.0; value < xMax; value += secondsPerInterval) { final String valueString = decimalFormat.format(xMin+value); labelX = valueXToGraphX(value, xMax); final int width = tickLabelMetrics.stringWidth(valueString); g.drawString(valueString, (labelX-(width/2)), labelY); if (includeVerticalGrid && (value != 0)) { g.setColor(Color.LIGHT_GRAY); g.drawLine(labelX, originY, labelX, upperLeftY); g.setColor(Color.BLACK); } g.drawLine(labelX, (originY - 2), labelX, (originY + 2)); } // Now draw the horizontal axis. g.drawLine(originX, originY, lowerRightX, originY); // Add the caption at the top of the image captionWidth = titleMetrics.stringWidth(graphTitle); int captionX = ((lowerRightX - originX - captionWidth) / 2) + originX; int captionY = titleMetrics.getAscent() + 2; g.setFont(titleFont); g.drawString(graphTitle, captionX, captionY); // Add the horizontal axis caption at the bottom of the image captionWidth = axisLabelMetrics.stringWidth(horizontalAxisTitle); captionX = ((lowerRightX - originX - captionWidth) / 2) + originX; captionY = (height - 2); g.setFont(axisLabelFont); g.drawString(horizontalAxisTitle, captionX, captionY); // Iterate through the data and generate the graph. final Stroke defaultStroke = g.getStroke(); g.setStroke(new BasicStroke(2.0f)); for (int i=0; i < xValues.length; i++) { g.setColor(COLORS[i]); double x1 = xValues[i][0]; double y1 = yValues[i][0]; for (int j=1; j < xValues[i].length; j++) { final double x2 = xValues[i][j]; final double y2 = yValues[i][j]; final int graphX1 = valueXToGraphX(x1, xMax); final int graphX2 = valueXToGraphX(x2, xMax); final int graphY1 = valueYToGraphY(y1); final int graphY2 = valueYToGraphY(y2); if (flatBetweenPoints) { g.drawLine(graphX1, graphY1, graphX2, graphY1); g.drawLine(graphX2, graphY1, graphX2, graphY2); } else { g.drawLine(graphX1, graphY1, graphX2, graphY2); } if (drawPoints) { g.fillOval(graphX1-2, graphY1-2, 5, 5); g.fillOval(graphX2-2, graphY2-2, 5, 5); } x1 = x2; y1 = y2; } } // Draw a line with the average value, if specified. g.setStroke(defaultStroke); g.setColor(Color.BLACK); if (includeAverage) { final int y = valueYToGraphY(yAvg); g.drawLine(originX, y, lowerRightX, y); } return image; } /** * Generates a stacked area graph based on the information that has been * provided. Note that all data points provided must have exactly the same * number of elements. * * @return A buffered image containing the generated stacked area graph. */ public BufferedImage generateStackedAreaGraph() { // Create the image and get the graphics context. final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED); final Graphics2D g = image.createGraphics(); // Configure the graph to enable antialiasing. final HashMap<RenderingHints.Key,Object> renderingHints = new HashMap<RenderingHints.Key,Object>(3); renderingHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); renderingHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.addRenderingHints(renderingHints); // Give the graph a white background and use black to add text and the axes. g.setColor(Color.white); g.fillRect(0, 0, width, height); g.setColor(Color.black); // Set the fonts to use for the grapher. final Font titleFont = selectFont(g, "SansSerif", Font.BOLD, 18, graphTitle, width); final Font axisLabelFont = selectFont(g, "SansSerif", Font.PLAIN, 12, verticalAxisTitle, height); final Font tickLabelFont = new Font("SansSerif", Font.PLAIN, 10); final Font legendTitleFont = new Font("SansSerif", Font.BOLD, 12); final Font legendItemFont = new Font("SansSerif", Font.PLAIN, 10); final FontMetrics titleMetrics = g.getFontMetrics(titleFont); final FontMetrics axisLabelMetrics = g.getFontMetrics(axisLabelFont); final FontMetrics tickLabelMetrics = g.getFontMetrics(tickLabelFont); final FontMetrics legendTitleMetrics = g.getFontMetrics(legendTitleFont); final FontMetrics legendItemMetrics = g.getFontMetrics(legendItemFont); // Since we are doing a stacked graph, we need to add all the values // together for each data point and use that as the max. The min will // always be zero. min = 0.0; max = 0.0; for (int i=0; i < data[0].length; i++) { double total = 0.0; for (int j=0; j < data.length; j++) { total += data[j][i]; } if (total > max) { max = total; } } // Calculate the real max and min values to use for the Y axis. There will // be a 5% border on the top and bottom. Note that if all values were the // same for the entire span, we need to "adjust" the numbers so that the // graph will render. double span = max - min; if (span < 0.00001) { span += 1.0; max += 0.5; if (min >= 0.5) { min -= 0.5; } } graphMax = max / 0.95; if (baseAtZero) { graphMin = 0; } else { graphMin = min - (graphMax * 0.05); // In case the graph min would be negative where the real min doesn't go // below zero, then make the graph min zero. if ((graphMin < 0) && (min >= 0)) { graphMin = 0; } // Otherwise, see if we can decrease the minimum by a little to make it a // relatively nice number. Do this by finding the largest power of ten // that is less than or equal to the span and making the minimum value in // the graph a multiple of that power of ten. graphSpan = graphMax - graphMin; final int largestPowerOfTen = (int) (Math.log(graphSpan) / Math.log(10)); if (largestPowerOfTen > 0) { graphMin = ((int) (graphMin / Math.pow(10, largestPowerOfTen))) * Math.pow(10, largestPowerOfTen); } } graphSpan = graphMax - graphMin; // The top of the graph will be just below the bottom of the caption, and // the bottom of the graph will be twice the distance from the bottom of the // image. upperLeftY = titleMetrics.getHeight() + 5; originY = height - (2 * axisLabelMetrics.getHeight()) - tickLabelMetrics.getHeight(); graphHeight = originY - upperLeftY + 1; // Add the legend along the side. int legendWidth = axisLabelMetrics.getHeight() + 2; if (includeLegend) { int maxLabelWidth = legendTitleMetrics.stringWidth(legendTitle); for (int i=0; i < dataSetLabels.length; i++) { final int labelWidth = legendItemMetrics.stringWidth(dataSetLabels[i]) + legendItemMetrics.getHeight() + 2; if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } } legendWidth += maxLabelWidth + 6; final int legendHeight = 4 + legendTitleMetrics.getHeight() + (dataSetLabels.length * legendItemMetrics.getHeight()); // Draw the caption at the top of the legend. int labelX = width - ((maxLabelWidth + legendTitleMetrics.stringWidth(legendTitle)) / 2) - 6; int labelY = ((height - legendHeight) / 2) + legendTitleMetrics.getAscent() + 2; g.setFont(legendTitleFont); g.drawString(legendTitle, labelX, labelY); g.setFont(legendItemFont); labelY += legendTitleMetrics.getHeight(); // Draw the legend entries using the appropriate colors. labelX = width - maxLabelWidth - 6; for (int i=0; i < dataSetLabels.length; i++) { final int captionAscent = legendItemMetrics.getAscent(); final int captionHeight = legendItemMetrics.getHeight(); g.setColor(COLORS[i % COLORS.length]); g.fillRect(labelX, labelY-captionAscent, captionAscent, captionAscent); g.setColor(Color.black); g.drawString(dataSetLabels[i], labelX + captionHeight + 2, labelY); labelY += captionHeight; } g.setColor(Color.black); g.drawRect((labelX - 2), ((height - legendHeight) / 2), (maxLabelWidth + 4), legendHeight); } // The graph should be centered horizontally, so make the right border the // same distance from the image edge as the left border. lowerRightX = width - legendWidth; // Draw the vertical axis caption. It should be rotated 90 degrees, which // makes for some funky math. Just trust that this works. g.rotate(1.5*Math.PI); int captionWidth = axisLabelMetrics.stringWidth(verticalAxisTitle); g.setFont(axisLabelFont); g.drawString(verticalAxisTitle, -((height+captionWidth)/2), (axisLabelMetrics.getHeight()-2)); g.setFont(tickLabelFont); g.rotate(Math.PI/2); // Figure out how many labels to draw along the vertical axis. It will // basically be double-spaced, so figure out how many labels could fit along // the vertical axis and divide by 2. final int numVerticalLabels = graphHeight / tickLabelMetrics.getHeight() / 2; // If there is space for more vertical labels than there are integers, then // go up by integer values. Otherwise, use the calculated number of labels. int labelX = 5 + axisLabelMetrics.getHeight(); int labelY = originY; int maxLabelWidth = 0; if (numVerticalLabels > graphSpan) { if (numVerticalLabels > (2 * graphSpan)) { final double increment = graphSpan / numVerticalLabels; for (double value = graphMin; value < graphMax; value += increment) { labelY = valueYToGraphY(value); final String label = decimalFormat.format(value); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } else { for (double value = graphMin; value < graphMax; value++) { labelY = valueYToGraphY(value); final String label = decimalFormat.format(value); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels); for (double valueY = graphMin; valueY < graphMax; valueY += increment) { labelY = valueYToGraphY(valueY); final String label = decimalFormat.format(valueY); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } // Draw the vertical axis and add the tick marks along its side originX = labelX + maxLabelWidth + 5; g.drawLine(originX, upperLeftY, originX, originY); labelY = originY; if (numVerticalLabels > graphSpan) { if (numVerticalLabels > (2 * graphSpan)) { final double increment = graphSpan / numVerticalLabels; for (double value = graphMin; value < graphMax; value += increment) { labelY = valueYToGraphY(value); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } else { for (double value = graphMin; value < graphMax; value++) { labelY = valueYToGraphY(value); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels); for (double valueY = graphMin; valueY < graphMax; valueY += increment) { labelY = valueYToGraphY(valueY); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } // We know that the number of intervals will go from zero to n, so the // width of n should be roughly equal to the greatest width. final String label = String.valueOf(numSeconds); maxLabelWidth = tickLabelMetrics.stringWidth(label); final int maxHorizontalLabels = (lowerRightX - originX) / maxLabelWidth / 2; int secondsPerInterval; if ((numSeconds > maxHorizontalLabels) && (maxHorizontalLabels > 0)) { secondsPerInterval = numSeconds / maxHorizontalLabels; if ((secondsPerInterval % collectionIntervals[0]) != 0) { secondsPerInterval = (secondsPerInterval / collectionIntervals[0] + 1) * collectionIntervals[0]; } } else { secondsPerInterval = 1; } // Draw the labels at the bottom of the horizontal axis. The tick marks // can also be added at the same time. labelY = originY + tickLabelMetrics.getHeight() + 2; for (int value = 0; value < numSeconds; value += secondsPerInterval) { final String valueString = String.valueOf(value); labelX = valueXToGraphX(value); final int width = tickLabelMetrics.stringWidth(valueString); g.drawString(valueString, (labelX-(width/2)), labelY); if (includeVerticalGrid && (value != 0)) { g.setColor(Color.LIGHT_GRAY); g.drawLine(labelX, originY, labelX, upperLeftY); g.setColor(Color.BLACK); } g.drawLine(labelX, (originY - 2), labelX, (originY + 2)); } // Now draw the horizontal axis. g.drawLine(originX, originY, lowerRightX, originY); // Add the caption at the top of the image captionWidth = titleMetrics.stringWidth(graphTitle); int captionX = ((lowerRightX - originX - captionWidth) / 2) + originX; int captionY = titleMetrics.getAscent() + 2; g.setFont(titleFont); g.drawString(graphTitle, captionX, captionY); // Add the horizontal axis caption at the bottom of the image captionWidth = axisLabelMetrics.stringWidth(horizontalAxisTitle); captionX = ((lowerRightX - originX - captionWidth) / 2) + originX; captionY = (height - 2); g.setFont(axisLabelFont); g.drawString(horizontalAxisTitle, captionX, captionY); // Iterate through the data and make the area graphs. Make sure that we // don't draw more data sets than we have colors available. final double[] currentTotals = new double[data[0].length]; for (int i=0; ((i < data.length) && (i < COLORS.length)); i++) { g.setColor(COLORS[i]); int j, o1, o2, x1, x2, y1, y2; for (j=0; ((j < data[i].length) && ignoreZeroValues && Double.isNaN(data[i][j])); j++); if (j >= data[i].length) { continue; } x1 = valueXToGraphX((j+1)*collectionIntervals[i]); y1 = valueYToGraphY(data[i][j] + currentTotals[j]); o1 = valueYToGraphY(currentTotals[j]); currentTotals[j] += data[i][j]; for (j=j+1; j < data[i].length; j++) { if (! (ignoreZeroValues && Double.isNaN(data[i][j]))) { x2 = valueXToGraphX((j+1) * collectionIntervals[i]); y2 = valueYToGraphY(data[i][j] + currentTotals[j]); o2 = valueYToGraphY(currentTotals[j]); currentTotals[j] += data[i][j]; final int[] xPoints = new int[] { x1, x2, x2, x1 }; final int[] yPoints = new int[] { y1, y2, o2, o1 }; g.fill(new Polygon(xPoints, yPoints, 4)); x1 = x2; y1 = y2; o1 = o2; } } } // Draw a line with the average value, if specified. if (includeAverage) { final int y = valueYToGraphY(sy/n); g.drawLine(originX, y, lowerRightX, y); } // Draw the regression line, if specified. if (includeRegression) { final double b = (sxy - (sx*sy)/n) / (sxx - (sx*sx)/n); final double a = (sy - b*sx) / n; final int y1 = valueYToGraphY(a); final int y2 = valueYToGraphY(a + b*(numSeconds-1)); g.drawLine(originX, y1, lowerRightX, y2); } return image; } /** * Generates a line graph that can be used to overlay data for two different * statistics. The left X axis will be used for the first statistic and the * right X axis will be used for the second. * * @param label1 The label for the first statistic. * @param caption1 The caption that should be used for the * vertical axis for the first statistic. * @param values1 The actual data values for the first * statistic. * @param interval1 The collection interval used when gathering * the data for the first statistic. * @param label2 The label for the second statistic. * @param caption2 The caption that should be used for the * vertical axis for the second statistic. * @param values2 The actual data values for the second * statistic. * @param interval2 The collection interval used when gathering * the data for the second statistic. * @param useSameAxis Indicates whether both statistics should be * graphed along the same axis. * @param horizontalAxisCaption The caption that should be displayed along * the horizontal axis for this graph. * @param jobIDs The set of job IDs that correspond to each * data point in each value set. This should * be <CODE>null</CODE> unless each data point * represents a different job. * * @return A buffered image containing the generated line graph. */ public BufferedImage generateDualLineGraph(final String label1, final String caption1, final double[] values1, final int interval1, final String label2, final String caption2, final double[] values2, final int interval2, final boolean useSameAxis, final String horizontalAxisCaption, final String[] jobIDs) { // Create the image and get the graphics context. final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED); final Graphics2D g = image.createGraphics(); // Configure the graph to enable antialiasing. final HashMap<RenderingHints.Key,Object> renderingHints = new HashMap<RenderingHints.Key,Object>(3); renderingHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); renderingHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.addRenderingHints(renderingHints); // Give the graph a white background and use black to add text and the axes. g.setColor(Color.white); g.fillRect(0, 0, width, height); g.setColor(Color.black); // Set the fonts to use for the grapher. final Font titleFont = selectFont(g, "SansSerif", Font.BOLD, 18, graphTitle, width); final Font axisLabelFont = selectFont(g, "SansSerif", Font.PLAIN, 12, verticalAxisTitle, height); final Font tickLabelFont = new Font("SansSerif", Font.PLAIN, 10); final Font legendTitleFont = new Font("SansSerif", Font.BOLD, 12); final Font legendItemFont = new Font("SansSerif", Font.PLAIN, 10); final FontMetrics titleMetrics = g.getFontMetrics(titleFont); final FontMetrics axisLabelMetrics = g.getFontMetrics(axisLabelFont); final FontMetrics tickLabelMetrics = g.getFontMetrics(tickLabelFont); final FontMetrics legendTitleMetrics = g.getFontMetrics(legendTitleFont); final FontMetrics legendItemMetrics = g.getFontMetrics(legendItemFont); // Find the maximum and minimum for each data set. double max1 = -Double.MAX_VALUE; double min1 = Double.MAX_VALUE; for (int i=0; i < values1.length; i++) { if (values1[i] > max1) { max1 = values1[i]; } if (values1[i] < min1) { min1 = values1[i]; } } double max2 = -Double.MAX_VALUE; double min2 = Double.MAX_VALUE; for (int i=0; i < values2.length; i++) { if (values2[i] > max2) { max2 = values2[i]; } if (values2[i] < min2) { min2 = values2[i]; } } final double max = Math.max(max1, max2); final double min = Math.min(min1, min2); // Calculate the real max and min value to use for the axes of each data // set. double span1 = max1 - min1; if (span1 < 0.00001) { span1 += 1.0; max1 += 0.5; if (min1 >= 0.5) { min1 -= 0.5; } } double span2 = max2 - min2; if (span2 < 0.00001) { span2 += 1.0; max2 += 0.5; if (min2 >= 0.5) { min2 -= 0.5; } } final double graphMax1 = max1 / 0.95; double graphMin1; double graphSpan1; if (baseAtZero) { graphMin1 = 0; } else { graphMin1 = min1 - (graphMax1 * 0.05); if ((graphMin1 < 0) && (min1 >= 0)) { graphMin1 = 0; } graphSpan1 = graphMax1 - graphMin1; final int largestPowerOfTen = (int) (Math.log(graphSpan1) / Math.log(10)); if (largestPowerOfTen > 0) { graphMin1 = ((int) (graphMin1 / Math.pow(10, largestPowerOfTen))) * Math.pow(10, largestPowerOfTen); } } graphSpan1 = graphMax1 - graphMin1; final double graphMax2 = max2 / 0.95; double graphMin2; double graphSpan2; if (baseAtZero) { graphMin2 = 0; } else { graphMin2 = min2 - (graphMax2 * 0.05); if ((graphMin2 < 0) && (min2 >= 0)) { graphMin2 = 0; } graphSpan2 = graphMax2 - graphMin2; final int largestPowerOfTen = (int) (Math.log(graphSpan2) / Math.log(10)); if (largestPowerOfTen > 0) { graphMin2 = ((int) (graphMin2 / Math.pow(10, largestPowerOfTen))) * Math.pow(10, largestPowerOfTen); } } graphSpan2 = graphMax2 - graphMin2; graphMax = Math.max(graphMax1, graphMax2); graphMin = Math.min(graphMin1, graphMin2); graphSpan = graphMax - graphMin; // The top of the graph will be just below the bottom of the caption, and // the bottom of the graph will be twice the distance from the bottom of the // image. upperLeftY = titleMetrics.getHeight() + 5; originY = height - (2 * axisLabelMetrics.getHeight()) - tickLabelMetrics.getHeight(); graphHeight = originY - upperLeftY + 1; // Add the legend along the side. int legendWidth = axisLabelMetrics.getHeight() + 2; if (includeLegend) { int maxLabelWidth = legendTitleMetrics.stringWidth(legendTitle); int labelWidth = legendItemMetrics.stringWidth(label1) + legendItemMetrics.getHeight() + 2; if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } labelWidth = legendItemMetrics.stringWidth(label2) + legendItemMetrics.getHeight() + 2; if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } legendWidth += maxLabelWidth + 6; int legendHeight = 4 + legendTitleMetrics.getHeight() + (2 * legendItemMetrics.getHeight()); if ((jobIDs != null) && (jobIDs.length > 0)) { // If we should include job ID information in the legend, then we need // to adjust our numbers to accommodate them. for (int i=0; i < jobIDs.length; i++) { labelWidth = legendItemMetrics.stringWidth((i+1) + " -- " + jobIDs[i]); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } } final int captionHeight = legendItemMetrics.getHeight(); legendHeight += (jobIDs.length + 2) * captionHeight; legendWidth = captionHeight + 2 + maxLabelWidth + 6; } // Draw the caption at the top of the legend. int labelX = width - ((maxLabelWidth+legendTitleMetrics.stringWidth(legendTitle))/2) - 6; int labelY = ((height - legendHeight) / 2) + legendTitleMetrics.getHeight() + 2; g.setFont(legendTitleFont); g.drawString(legendTitle, labelX, labelY); g.setFont(legendItemFont); // Draw the legend entries using the appropriate colors. final int legendItemAscent = legendItemMetrics.getAscent(); final int legendItemHeight = legendItemMetrics.getHeight(); labelX = width - maxLabelWidth - 6; labelY += legendItemHeight; g.setColor(COLORS[0]); g.fillRect(labelX, labelY-legendItemAscent, legendItemAscent, legendItemAscent); g.setColor(Color.black); g.drawString(label1, labelX + legendItemHeight + 2, labelY); labelY += legendItemHeight; g.setColor(COLORS[1]); g.fillRect(labelX, labelY-legendItemAscent, legendItemAscent, legendItemAscent); g.setColor(Color.black); g.drawString(label2, labelX + legendItemHeight + 2, labelY); g.setColor(Color.black); if ((jobIDs != null) && (jobIDs.length > 0)) { labelY += (2 * legendItemHeight); labelWidth = legendItemMetrics.stringWidth("Job IDs"); labelX = width - 6 - ((maxLabelWidth + labelWidth) / 2); g.drawString("Job IDs", labelX, labelY); labelX = width - maxLabelWidth - 6; for (int i=0; i < jobIDs.length; i++) { labelY += legendItemHeight; g.drawString((i+1) + " -- " + jobIDs[i], labelX, labelY); } } g.drawRect((labelX - 2), ((height - legendHeight) / 2), (maxLabelWidth + 4), legendHeight); } // Draw the left and right vertical axis captions. They should be rotated // 90 degrees, which will make for some funky math. Just trust that this // works. g.setFont(axisLabelFont); if (! useSameAxis) { g.rotate(1.5*Math.PI); int captionWidth = axisLabelMetrics.stringWidth(caption1); g.drawString(caption1, -((height+captionWidth)/2), (axisLabelMetrics.getHeight()-2)); captionWidth = axisLabelMetrics.stringWidth(caption2); g.drawString(caption2, -((height+captionWidth)/2), (width-legendWidth)); g.rotate(Math.PI/2); } // The vertical axis will be different depending on whether a single or two // different axes are to be included. g.setFont(tickLabelFont); lowerRightX = width - legendWidth - 5; if (useSameAxis) { // Figure out how many labels to draw along the vertical axis. It will // basically be double-spaced, so figure out how many labels could fit // along the vertical axis and divide by two. final int captionHeight = tickLabelMetrics.getHeight(); final int numVerticalLabels = graphHeight / captionHeight / 2; // If there is space for more vertical labels than there are integers, // then go up by integer values. Otherwise, use the calculated number of // labels. final int labelX = 5 + captionHeight; int labelY = originY; int maxLabelWidth = 0; if (numVerticalLabels > graphSpan) { if (numVerticalLabels > (2 * graphSpan)) { final double increment = graphSpan / numVerticalLabels; for (double value = graphMin; value < graphMax; value += increment) { labelY = valueYToGraphY(value); final String label = decimalFormat.format(value); if (tickLabelMetrics.stringWidth(label) > maxLabelWidth) { maxLabelWidth = tickLabelMetrics.stringWidth(label); } g.drawString(label, labelX, (labelY+(tickLabelMetrics.getHeight()/2))); } } else { for (double value = graphMin; value < graphMax; value++) { labelY = valueYToGraphY(value); final String label = decimalFormat.format(value); if (tickLabelMetrics.stringWidth(label) > maxLabelWidth) { maxLabelWidth = tickLabelMetrics.stringWidth(label); } g.drawString(label, labelX, (labelY+(tickLabelMetrics.getHeight()/2))); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels); for (double valueY = graphMin; valueY < graphMax; valueY += increment) { labelY = valueYToGraphY(valueY); final String label = decimalFormat.format(valueY); if (tickLabelMetrics.stringWidth(label) > maxLabelWidth) { maxLabelWidth = tickLabelMetrics.stringWidth(label); } g.drawString(label, labelX, (labelY+(tickLabelMetrics.getHeight()/2))); } } // Draw the vertical axis and add the tick marks along its side originX = labelX + maxLabelWidth + 5; g.setColor(new Color(0xF1, 0xF1, 0xF1)); g.fillRect(originX, upperLeftY, (lowerRightX - originX), (originY - upperLeftY)); g.setColor(Color.BLACK); g.drawLine(originX, upperLeftY, originX, originY); labelY = originY; if (numVerticalLabels > graphSpan) { if (numVerticalLabels > (2 * graphSpan)) { final double increment = graphSpan / numVerticalLabels; for (double value = graphMin; value < graphMax; value += increment) { labelY = valueYToGraphY(value); if (value > graphMin) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } else { for (double value = graphMin; value < graphMax; value++) { labelY = valueYToGraphY(value); g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels); for (double valueY = graphMin; valueY < graphMax; valueY += increment) { labelY = valueYToGraphY(valueY); if (valueY > graphMin) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } } else { // Figure out how many labels to draw along the left vertical axis. It // will basically be double-spaced, so figure out how many labels could // fit along the vertical axis and divide by two. final int captionHeight = tickLabelMetrics.getHeight(); final int halfHeight = captionHeight / 2; final int numVerticalLabels = graphHeight / captionHeight / 2; // If there is space for more vertical labels than there are integers, // then go up by integer values. Otherwise, use the calculated number of // labels. int labelX = 5 + captionHeight; int labelY = originY; int maxLabelWidth = 0; if (numVerticalLabels > graphSpan1) { if (numVerticalLabels > (2 * graphSpan1)) { final double increment = graphSpan1 / numVerticalLabels; for (double value = graphMin1; value < graphMax1; value += increment) { labelY = valueYToGraphY(value, graphMin1, graphSpan1); final String label = decimalFormat.format(value); if (tickLabelMetrics.stringWidth(label) > maxLabelWidth) { maxLabelWidth = tickLabelMetrics.stringWidth(label); } g.drawString(label, labelX, (labelY+halfHeight)); } } else { for (double value = graphMin1; value < graphMax1; value++) { labelY = valueYToGraphY(value, graphMin1, graphSpan1); final String label = decimalFormat.format(value); if (tickLabelMetrics.stringWidth(label) > maxLabelWidth) { maxLabelWidth = tickLabelMetrics.stringWidth(label); } g.drawString(label, labelX, (labelY+halfHeight)); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels, graphSpan1); for (double valueY = graphMin1; valueY < graphMax1; valueY += increment) { labelY = valueYToGraphY(valueY, graphMin1, graphSpan1); final String label = decimalFormat.format(valueY); if (tickLabelMetrics.stringWidth(label) > maxLabelWidth) { maxLabelWidth = tickLabelMetrics.stringWidth(label); } g.drawString(label, labelX, (labelY+halfHeight)); } } // Figure out the maximum label width for labels on the right side. labelY = originY; int maxRightLabelWidth = 0; if (numVerticalLabels > graphSpan2) { if (numVerticalLabels > (2 * graphSpan2)) { final double increment = graphSpan2 / numVerticalLabels; for (double value = graphMin2; value < graphMax2; value += increment) { labelY = valueYToGraphY(value, graphMin2, graphSpan2); final String label = decimalFormat.format(value); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxRightLabelWidth) { maxRightLabelWidth = labelWidth; } } } else { for (double value = graphMin2; value < graphMax2; value++) { labelY = valueYToGraphY(value, graphMin2, graphSpan2); final String label = decimalFormat.format(value); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxRightLabelWidth) { maxRightLabelWidth = labelWidth; } } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels, graphSpan2); for (double valueY = graphMin2; valueY < graphMax2; valueY += increment) { labelY = valueYToGraphY(valueY, graphMin2, graphSpan2); final String label = decimalFormat.format(valueY); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxRightLabelWidth) { maxRightLabelWidth = labelWidth; } } } // Draw the left vertical axis and add the tick marks along its side originX = labelX + maxLabelWidth + 5; lowerRightX = width - legendWidth - captionHeight - maxRightLabelWidth - 5; g.setColor(new Color(0xF1, 0xF1, 0xF1)); g.fillRect(originX, upperLeftY, (lowerRightX - originX), (originY - upperLeftY)); g.setColor(COLORS[0]); g.drawLine(originX, upperLeftY, originX, originY); labelY = originY; if (numVerticalLabels > graphSpan1) { if (numVerticalLabels > (2 * graphSpan1)) { final double increment = graphSpan1 / numVerticalLabels; for (double value = graphMin1; value < graphMax1; value += increment) { labelY = valueYToGraphY(value, graphMin1, graphSpan1); if (value > graphMin1) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(COLORS[0]); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } else { for (double value = graphMin1; value < graphMax1; value++) { labelY = valueYToGraphY(value, graphMin1, graphSpan1); if (value > graphMin1) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(COLORS[0]); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels, graphSpan1); for (double valueY = graphMin1; valueY < graphMax1; valueY += increment) { labelY = valueYToGraphY(valueY, graphMin1, graphSpan1); if (valueY > graphMin1) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(COLORS[0]); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } g.setColor(Color.black); // Figure out how many labels to draw along the right vertical axis. If // there is space for more vertical labels than there are integers, then // go up by integer values. Otherwise, use the calculated number of // labels. labelX = width - legendWidth - captionHeight; labelY = originY; if (numVerticalLabels > graphSpan2) { if (numVerticalLabels > (2 * graphSpan2)) { final double increment = graphSpan2 / numVerticalLabels; for (double value = graphMin2; value < graphMax2; value += increment) { labelY = valueYToGraphY(value, graphMin2, graphSpan2); final String label = decimalFormat.format(value); final int labelWidth = tickLabelMetrics.stringWidth(label); g.drawString(label, labelX-labelWidth, (labelY+halfHeight)); } } else { for (double value = graphMin2; value < graphMax2; value++) { labelY = valueYToGraphY(value, graphMin2, graphSpan2); final String label = decimalFormat.format(value); final int labelWidth = tickLabelMetrics.stringWidth(label); g.drawString(label, labelX-labelWidth, (labelY+halfHeight)); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels, graphSpan2); for (double valueY = graphMin2; valueY < graphMax2; valueY += increment) { labelY = valueYToGraphY(valueY, graphMin2, graphSpan2); final String label = decimalFormat.format(valueY); final int labelWidth = tickLabelMetrics.stringWidth(label); g.drawString(label, labelX-labelWidth, (labelY+halfHeight)); } } // Draw the right vertical axis and add the tick marks along its side g.setColor(COLORS[1]); g.drawLine(lowerRightX, upperLeftY, lowerRightX, originY); labelY = originY; if (numVerticalLabels > graphSpan2) { if (numVerticalLabels > (2 * graphSpan2)) { final double increment = graphSpan2 / numVerticalLabels; for (double value = graphMin2; value < graphMax2; value += increment) { labelY = valueYToGraphY(value, graphMin2, graphSpan2); g.drawLine((lowerRightX - 2), labelY, (lowerRightX + 2), labelY); } } else { for (double value = graphMin2; value < graphMax2; value++) { labelY = valueYToGraphY(value, graphMin2, graphSpan2); g.drawLine((lowerRightX - 2), labelY, (lowerRightX + 2), labelY); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels, graphSpan2); for (double valueY = graphMin2; valueY < graphMax2; valueY += increment) { labelY = valueYToGraphY(valueY, graphMin2, graphSpan2); g.drawLine((lowerRightX - 2), labelY, (lowerRightX + 2), labelY); } } g.setColor(Color.black); } // We know that the number of intervals will go from startSeconds to // startSeconds+n, so the width of startSeconds+n should be roughly equal to // the greatest width. numSeconds = Math.max((interval1*values1.length), (interval2*values2.length)); final String label = String.valueOf(startSeconds+numSeconds); final int maxLabelWidth = tickLabelMetrics.stringWidth(label); final int maxHorizontalLabels = (lowerRightX - originX) / maxLabelWidth / 2; int secondsPerInterval; if ((numSeconds > maxHorizontalLabels) && (maxHorizontalLabels > 0)) { secondsPerInterval = numSeconds / maxHorizontalLabels; if ((secondsPerInterval % interval1) != 0) { secondsPerInterval = (secondsPerInterval / interval1 + 1) * interval1; } } else { secondsPerInterval = 1; } // Draw the labels at the bottom of the horizontal axis. The tick marks // can also be added at the same time. final int labelY = originY + tickLabelMetrics.getHeight() + 2; for (int value = 0; value < numSeconds; value += secondsPerInterval) { final String valueString = String.valueOf(startSeconds+value); final int labelX = valueXToGraphX(value); final int width = tickLabelMetrics.stringWidth(valueString); if (value > 0) { g.setColor(Color.LIGHT_GRAY); g.drawLine(labelX, originY, labelX, upperLeftY); g.setColor(Color.BLACK); } g.drawString(valueString, (labelX-(width/2)), labelY); g.drawLine(labelX, (originY - 2), labelX, (originY + 2)); } // Now draw the horizontal axis. g.drawLine(originX, originY, lowerRightX, originY); // Add the caption at the top of the image int captionWidth = titleMetrics.stringWidth(graphTitle); int captionX = ((lowerRightX - originX - captionWidth) / 2) + originX; int captionY = titleMetrics.getHeight() + 2; g.setFont(titleFont); g.drawString(graphTitle, captionX, captionY); // Add the horizontal axis caption at the bottom of the image captionWidth = axisLabelMetrics.stringWidth(horizontalAxisCaption); captionX = ((lowerRightX - originX - captionWidth) / 2) + originX; captionY = (height - 2); g.setFont(axisLabelFont); g.drawString(horizontalAxisCaption, captionX, captionY); // Actually draw the graph data. This will be different based on whether // one or two axes will be used. final Stroke defaultStroke = g.getStroke(); g.setStroke(new BasicStroke(2.0f)); if (useSameAxis) { // Iterate through the first set of data and make the line graph. g.setColor(COLORS[0]); int j, x1, x2, y1, y2; for (j=0; ((j < values1.length) && ignoreZeroValues && Double.isNaN(values1[j])); j++); x1 = valueXToGraphX(j*interval1); y1 = valueYToGraphY(values1[j]); for (j=j+1; j < values1.length; j++) { if (! (ignoreZeroValues && Double.isNaN(values1[j]))) { x2 = valueXToGraphX((j+1) * interval1); y2 = valueYToGraphY(values1[j]); if (flatBetweenPoints) { g.drawLine(x1, y1, x2, y1); g.drawLine(x2, y1, x2, y2); } else { g.drawLine(x1, y1, x2, y2); } x1 = x2; y1 = y2; } } // Iterate through the second set of data and make the line graph. g.setColor(COLORS[1]); for (j=0; ((j < values2.length) && ignoreZeroValues && Double.isNaN(values2[j])); j++); x1 = valueXToGraphX(j*interval2); y1 = valueYToGraphY(values2[j]); for (j=j+1; j < values2.length; j++) { if (! (ignoreZeroValues && Double.isNaN(values2[j]))) { x2 = valueXToGraphX((j+1) * interval2); y2 = valueYToGraphY(values2[j]); if (flatBetweenPoints) { g.drawLine(x1, y1, x2, y1); g.drawLine(x2, y1, x2, y2); } else { g.drawLine(x1, y1, x2, y2); } x1 = x2; y1 = y2; } } } else { // Iterate through the first set of data and make the line graph. g.setColor(COLORS[0]); int j, x1, x2, y1, y2; for (j=0; ((j < values1.length) && ignoreZeroValues && Double.isNaN(values1[j])); j++); if (j < values1.length) { x1 = valueXToGraphX((j+1)*interval1); y1 = valueYToGraphY(values1[j], graphMin1, graphSpan1); for (j=j+1; j < values1.length; j++) { if (! (ignoreZeroValues && Double.isNaN(values1[j]))) { x2 = valueXToGraphX((j+1) * interval1); y2 = valueYToGraphY(values1[j], graphMin1, graphSpan1); if (flatBetweenPoints) { g.drawLine(x1, y1, x2, y1); g.drawLine(x2, y1, x2, y2); } else { g.drawLine(x1, y1, x2, y2); } x1 = x2; y1 = y2; } } } // Iterate through the second set of data and make the line graph. g.setColor(COLORS[1]); for (j=0; ((j < values2.length) && ignoreZeroValues && Double.isNaN(values2[j])); j++); if (j < values2.length) { x1 = valueXToGraphX((j+1)*interval2); y1 = valueYToGraphY(values2[j], graphMin2, graphSpan2); for (j=j+1; j < values2.length; j++) { if (! (ignoreZeroValues && Double.isNaN(values2[j]))) { x2 = valueXToGraphX((j+1) * interval2); y2 = valueYToGraphY(values2[j], graphMin2, graphSpan2); if (flatBetweenPoints) { g.drawLine(x1, y1, x2, y1); g.drawLine(x2, y1, x2, y2); } else { g.drawLine(x1, y1, x2, y2); } x1 = x2; y1 = y2; } } } } g.setStroke(defaultStroke); return image; } /** * Generates a bar graph based on the information that has been provided. * * @return A buffered image containing the generated bar graph. */ public BufferedImage generateBarGraph() { // Create the image and get the graphics context. final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED); final Graphics2D g = image.createGraphics(); // Configure the graph to enable antialiasing. final HashMap<RenderingHints.Key,Object> renderingHints = new HashMap<RenderingHints.Key,Object>(3); renderingHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); renderingHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.addRenderingHints(renderingHints); // Give the graph a white background and use black to add text and the axes. g.setColor(Color.white); g.fillRect(0, 0, width, height); g.setColor(Color.black); // Set the fonts to use for the grapher. // Set the fonts to use for the grapher. final Font titleFont = selectFont(g, "SansSerif", Font.BOLD, 18, graphTitle, width); final Font axisLabelFont = selectFont(g, "SansSerif", Font.PLAIN, 12, verticalAxisTitle, height); final Font tickLabelFont = new Font("SansSerif", Font.PLAIN, 10); final Font legendTitleFont = new Font("SansSerif", Font.BOLD, 12); final Font legendItemFont = new Font("SansSerif", Font.PLAIN, 10); final FontMetrics titleMetrics = g.getFontMetrics(titleFont); final FontMetrics axisLabelMetrics = g.getFontMetrics(axisLabelFont); final FontMetrics tickLabelMetrics = g.getFontMetrics(tickLabelFont); final FontMetrics legendTitleMetrics = g.getFontMetrics(legendTitleFont); final FontMetrics legendItemMetrics = g.getFontMetrics(legendItemFont); // We're working with the averages for each data set rather than the // maximums, so calculate them. double maxAverage = 0.0; final double[] averages = new double[data.length]; for (int i=0; i < averages.length; i++) { if (data[i].length == 0) { averages[i] = 0.0; } else { double sum = 0.0; for (int j=0; j < data[i].length; j++) { sum += data[i][j]; } averages[i] = sum / data[i].length; if (averages[i] > maxAverage) { maxAverage = averages[i]; } } } // Bar graphs will always be based at zero, so there is not much work // necessary to calculate the values. final double span = maxAverage; graphMax = maxAverage / 0.9; graphMin = 0; graphSpan = graphMax; // The top of the graph will be just below the bottom of the caption, and // the bottom of the graph will be the same distance from the bottom of the // image. upperLeftY = titleMetrics.getHeight() + 5; originY = height - (2 * axisLabelMetrics.getHeight()) - tickLabelMetrics.getHeight(); graphHeight = originY - upperLeftY + 1; // Add the legend along the side. int legendWidth = axisLabelMetrics.getHeight() + 2; if (includeLegend) { int maxLabelWidth = legendTitleMetrics.stringWidth(legendTitle); for (int i=0; i < dataSetLabels.length; i++) { final int labelWidth = legendItemMetrics.stringWidth(dataSetLabels[i]) + legendItemMetrics.getHeight() + 2; if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } } legendWidth += maxLabelWidth + 6; final int legendHeight = 4 + legendTitleMetrics.getHeight() + (dataSetLabels.length * legendItemMetrics.getHeight()); // Draw the caption at the top of the legend. int labelX = width - ((maxLabelWidth + legendTitleMetrics.stringWidth(legendTitle)) / 2) - 6; int labelY = ((height - legendHeight) / 2) + legendTitleMetrics.getAscent() + 2; g.setFont(legendTitleFont); g.drawString(legendTitle, labelX, labelY); g.setFont(legendItemFont); labelY += legendTitleMetrics.getHeight(); // Draw the legend entries using the appropriate colors. labelX = width - maxLabelWidth - 6; for (int i=0; i < dataSetLabels.length; i++) { final int captionAscent = legendItemMetrics.getAscent(); final int captionHeight = legendItemMetrics.getHeight(); g.setColor(COLORS[i % COLORS.length]); g.fillRect(labelX, labelY-captionAscent, captionAscent, captionAscent); g.setColor(Color.black); g.drawString(dataSetLabels[i], labelX + captionHeight + 2, labelY); labelY += captionHeight; } g.setColor(Color.black); g.drawRect((labelX - 2), ((height - legendHeight) / 2), (maxLabelWidth + 4), legendHeight); } // The graph should be centered horizontally, so make the right border the // same distance from the image edge as the left border. lowerRightX = width - legendWidth; // Draw the vertical axis caption. It should be rotated 90 degrees, which // makes for some funky math. Just trust that this works. g.rotate(1.5*Math.PI); int captionWidth = axisLabelMetrics.stringWidth(verticalAxisTitle); g.setFont(axisLabelFont); g.drawString(verticalAxisTitle, -((height+captionWidth)/2), (axisLabelMetrics.getHeight()-2)); g.setFont(tickLabelFont); g.rotate(Math.PI/2); // Figure out how many labels to draw along the vertical axis. It will // basically be double-spaced, so figure out how many labels could fit along // the vertical axis and divide by 2. final int numVerticalLabels = graphHeight / tickLabelMetrics.getHeight() / 2; // If there is space for more vertical labels than there are integers, then // go up by integer values. Otherwise, use the calculated number of labels. int labelX = 5 + axisLabelMetrics.getHeight(); int labelY = originY; int maxLabelWidth = 0; if (numVerticalLabels > graphSpan) { if (numVerticalLabels > (2 * graphSpan)) { final double increment = graphSpan / numVerticalLabels; for (double value = graphMin; value < graphMax; value += increment) { labelY = valueYToGraphY(value); final String label = decimalFormat.format(value); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } else { for (double value = graphMin; value < graphMax; value++) { labelY = valueYToGraphY(value); final String label = decimalFormat.format(value); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels); for (double valueY = graphMin; valueY < graphMax; valueY += increment) { labelY = valueYToGraphY(valueY); final String label = decimalFormat.format(valueY); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } // Draw the vertical axis and add the tick marks along its side originX = labelX + maxLabelWidth + 5; g.drawLine(originX, upperLeftY, originX, originY); labelY = originY; if (numVerticalLabels > graphSpan) { if (numVerticalLabels > (2 * graphSpan)) { final double increment = graphSpan / numVerticalLabels; for (double value = graphMin; value < graphMax; value += increment) { labelY = valueYToGraphY(value); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } else { for (double value = graphMin; value < graphMax; value++) { labelY = valueYToGraphY(value); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels); for (double valueY = graphMin; valueY < graphMax; valueY += increment) { labelY = valueYToGraphY(valueY); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } // Now draw the horizontal axis. g.drawLine(originX, originY, lowerRightX, originY); // Add the caption at the top of the image captionWidth = titleMetrics.stringWidth(graphTitle); final int captionX = ((lowerRightX - originX - captionWidth) / 2) + originX; final int captionY = titleMetrics.getHeight() + 2; g.setFont(titleFont); g.drawString(graphTitle, captionX, captionY); // Figure out how wide each bar should be. final int numBars = data.length; if (numBars == 0) { return null; } final int barWidth = (lowerRightX - originX) / numBars; int barX = originX + (((lowerRightX - originX) - (numBars * barWidth)) / 2); // Finally, iterate through the data and make the bar graphs. for (int i=0; i < data.length; i++) { if (i < COLORS.length) { g.setColor(COLORS[i]); } else { g.setColor(COLORS[i % COLORS.length]); } //Draw the filled rectangle. final int barY = valueYToGraphY(averages[i]); g.fillRect(barX, barY, barWidth, (originY - barY)); g.setColor(Color.DARK_GRAY); g.drawRect(barX, barY, barWidth, (originY - barY)); barX += barWidth; } return image; } /** * Generates a stacked bar graph based on the information that has been * provided. * * @return A buffered image containing the generated stacked bar graph. */ public BufferedImage generateStackedBarGraph() { // Create the image and get the graphics context. final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED); final Graphics2D g = image.createGraphics(); // Configure the graph to enable antialiasing. final HashMap<RenderingHints.Key,Object> renderingHints = new HashMap<RenderingHints.Key,Object>(3); renderingHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); renderingHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.addRenderingHints(renderingHints); // Give the graph a white background and use black to add text and the axes. g.setColor(Color.white); g.fillRect(0, 0, width, height); g.setColor(Color.black); // Set the fonts to use for the grapher. final Font titleFont = selectFont(g, "SansSerif", Font.BOLD, 18, graphTitle, width); final Font axisLabelFont = selectFont(g, "SansSerif", Font.PLAIN, 12, verticalAxisTitle, height); final Font tickLabelFont = new Font("SansSerif", Font.PLAIN, 10); final Font legendTitleFont = new Font("SansSerif", Font.BOLD, 12); final Font legendItemFont = new Font("SansSerif", Font.PLAIN, 10); final FontMetrics titleMetrics = g.getFontMetrics(titleFont); final FontMetrics axisLabelMetrics = g.getFontMetrics(axisLabelFont); final FontMetrics tickLabelMetrics = g.getFontMetrics(tickLabelFont); final FontMetrics legendTitleMetrics = g.getFontMetrics(legendTitleFont); final FontMetrics legendItemMetrics = g.getFontMetrics(legendItemFont); // Bar graphs will always be based at zero, so there is not much work // necessary to calculate the values. final double span = max; graphMax = max + (span/20); graphMin = 0; graphSpan = graphMax; // The top of the graph will be just below the bottom of the caption, and // the bottom of the graph will be the same distance from the bottom of the // image. upperLeftY = titleMetrics.getHeight() + 5; originY = height - (2 * axisLabelMetrics.getHeight()) - tickLabelMetrics.getHeight(); graphHeight = originY - upperLeftY + 1; // Add the legend along the side. int legendHeightItems = 0; int legendWidth = axisLabelMetrics.getHeight() + 2; if (includeLegend) { int maxLabelWidth = legendTitleMetrics.stringWidth(legendTitle); for (int i=0; i < dataSetNames.length; i++) { for (int j=0; j < categoryNames[i].length; j++) { legendHeightItems++; final String label = dataSetNames[i] + " - " + categoryNames[i][j]; final int labelWidth = legendItemMetrics.stringWidth(dataSetLabels[i]) + legendItemMetrics.getHeight() + 2; if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } } } legendWidth += maxLabelWidth + 6; final int legendHeight = 4 + legendTitleMetrics.getHeight() + (dataSetLabels.length * legendItemMetrics.getHeight()); // Draw the caption at the top of the legend. int labelX = width - ((maxLabelWidth + legendTitleMetrics.stringWidth(legendTitle)) / 2) - 6; int labelY = ((height - legendHeight) / 2) + legendTitleMetrics.getAscent() + 2; g.setFont(legendTitleFont); g.drawString(legendTitle, labelX, labelY); labelY += legendTitleMetrics.getHeight(); g.setFont(legendItemFont); // Draw the legend entries using the appropriate colors. labelX = width - maxLabelWidth - 6; int colorSlot = 0; for (int i=0; i < dataSetNames.length; i++) { for (int j=0; j < categoryNames[i].length; j++) { final int captionAscent = legendItemMetrics.getAscent(); final int captionHeight = legendItemMetrics.getHeight(); g.setColor(COLORS[colorSlot++ % COLORS.length]); g.fillRect(labelX, labelY-captionAscent, captionAscent, captionAscent); g.setColor(Color.black); final String label = dataSetNames[i] + " - " + categoryNames[i][j]; g.drawString(label, labelX + captionHeight + 2, labelY); labelY += captionHeight; } } g.setColor(Color.black); g.drawRect((labelX - 2), ((height - legendHeight) / 2), (maxLabelWidth + 4), legendHeight); } // The graph should be centered horizontally, so make the right border the // same distance from the image edge as the left border. lowerRightX = width - legendWidth; // Draw the vertical axis caption. It should be rotated 90 degrees, which // makes for some funky math. Just trust that this works. g.rotate(1.5*Math.PI); int captionWidth = axisLabelMetrics.stringWidth(verticalAxisTitle); g.setFont(axisLabelFont); g.drawString(verticalAxisTitle, -((height+captionWidth)/2), (axisLabelMetrics.getHeight()-2)); g.setFont(tickLabelFont); g.rotate(Math.PI/2); // Figure out how many labels to draw along the vertical axis. It will // basically be double-spaced, so figure out how many labels could fit along // the vertical axis and divide by 2. final int numVerticalLabels = graphHeight / tickLabelMetrics.getHeight() / 2; // If there is space for more vertical labels than there are integers, then // go up by integer values. Otherwise, use the calculated number of labels. int labelX = 5 + axisLabelMetrics.getHeight(); int labelY = originY; int maxLabelWidth = 0; if (numVerticalLabels > graphSpan) { if (numVerticalLabels > (2 * graphSpan)) { final double increment = graphSpan / numVerticalLabels; for (double value = graphMin; value < graphMax; value += increment) { labelY = valueYToGraphY(value); final String label = decimalFormat.format(value); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } else { for (double value = graphMin; value < graphMax; value++) { labelY = valueYToGraphY(value); final String label = decimalFormat.format(value); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels); for (double valueY = graphMin; valueY < graphMax; valueY += increment) { labelY = valueYToGraphY(valueY); final String label = decimalFormat.format(valueY); final int labelWidth = tickLabelMetrics.stringWidth(label); if (labelWidth > maxLabelWidth) { maxLabelWidth = labelWidth; } g.drawString(label, labelX, (labelY + (tickLabelMetrics.getHeight()/2))); } } // Draw the vertical axis and add the tick marks along its side originX = labelX + maxLabelWidth + 5; g.drawLine(originX, upperLeftY, originX, originY); labelY = originY; if (numVerticalLabels > graphSpan) { if (numVerticalLabels > (2 * graphSpan)) { final double increment = graphSpan / numVerticalLabels; for (double value = graphMin; value < graphMax; value += increment) { labelY = valueYToGraphY(value); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } else { for (double value = graphMin; value < graphMax; value++) { labelY = valueYToGraphY(value); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } } else { final double increment = chooseVerticalLabelIncrement(numVerticalLabels); for (double valueY = graphMin; valueY < graphMax; valueY += increment) { labelY = valueYToGraphY(valueY); if (includeHorizontalGrid) { g.setColor(Color.LIGHT_GRAY); g.drawLine(originX, labelY, lowerRightX, labelY); g.setColor(Color.BLACK); } g.drawLine((originX - 2), labelY, (originX + 2), labelY); } } // Now draw the horizontal axis. g.drawLine(originX, originY, lowerRightX, originY); // Add the caption at the top of the image captionWidth = titleMetrics.stringWidth(graphTitle); final int captionX = ((lowerRightX - originX - captionWidth) / 2) + originX; final int captionY = titleMetrics.getHeight() + 2; g.setFont(titleFont); g.drawString(graphTitle, captionX, captionY); // Figure out how wide each bar should be. final int numBars = dataSetNames.length; if (numBars == 0) { return null; } final int barWidth = (lowerRightX - originX) / numBars; int barX = originX + (((lowerRightX - originX) - (numBars * barWidth)) / 2); // Finally, iterate through the data and make the bar graphs. int colorSlot = 0; for (int i=0; i < dataSetAverages.length; i++) { int lastY = originY; int barY = 0; double lastValue = 0.0; for (int j=0; j < dataSetAverages[i].length; j++) { g.setColor(COLORS[colorSlot++ % COLORS.length]); barY = valueYToGraphY(lastValue + dataSetAverages[i][j]); g.fillRect(barX, barY, barWidth, (lastY - barY)); lastY = barY; lastValue += dataSetAverages[i][j]; } g.setColor(Color.DARK_GRAY); g.drawRect(barX, barY, barWidth, (originY - barY)); barX += barWidth; } return image; } /** * Selects an appropriate font to use based on the provided information. * * @param g The graphics context to use. * @param fontName The name of the font to use. * @param style The style to use for the font. * @param desiredSize The desired size for the font. * @param text The text that will be displayed. * @param maxLength The maximum length available for the font. * * @return The font that should be used. */ private static Font selectFont(final Graphics2D g, final String fontName, final int style, final int desiredSize, final String text, final int maxLength) { final Font f = new Font(fontName, style, desiredSize); final FontMetrics fm = g.getFontMetrics(f); if (fm.stringWidth(text) < maxLength) { return f; } else { return selectFont(g, fontName, style, (desiredSize - 1), text, maxLength); } } /** * This method attempts to determine an appropriate increment between labels * on the vertical axis. It has a preference for whole numbers if possible. * * @param numVerticalLabels The maximum number of labels that may be used * along the vertical axis. The vertical span of * the graph divided by the returned increment may * not exceed this value. * * @return The increment that should be used between labels on the vertical * axis of the generated line graph. */ private double chooseVerticalLabelIncrement(final int numVerticalLabels) { // Determine whether the graph span is larger or smaller than 1. If larger // than 1, then we'll try to use whole numbers. If smaller than 1, then // obviously we can't so just do it the cheap way. if (graphSpan > 1) { // Determine the increment size if we wanted to break things up into // evenly-sized increments. final double roughIncrement = graphSpan / numVerticalLabels; // If the rough increment is itself less than 1, then we won't bother // with anything complex on it. if (roughIncrement < 1) { return roughIncrement; } // Determine the largest power of ten that is less than or equal to this // rough increment. This can be found using a base 10 logarithm. // However, since Java doesn't provide a function for finding a base 10 // logarithm, then we'll have to use a base e logarithm and divide the // result by the log base e of 10. final int largestPowerOfTen = (int) (Math.log(roughIncrement) / Math.log(10)); // OK. Now we at least have a decent starting point. This last part is // too complicated to comment, so either figure it out for yourself or // just trust that it does what we want. final int refinedIncrement = ((int) (roughIncrement / Math.pow(10, largestPowerOfTen))) * ((int) Math.pow(10, largestPowerOfTen)); final double fudgeFactor = 5 * Math.pow(10, (largestPowerOfTen - 1)); if ((refinedIncrement + fudgeFactor) < roughIncrement) { return (refinedIncrement + fudgeFactor); } else { return refinedIncrement; } } else { return graphSpan / numVerticalLabels; } } /** * This method attempts to determine an appropriate increment between labels * on the vertical axis. It has a preference for whole numbers if possible. * * @param numVerticalLabels The maximum number of labels that may be used * along the vertical axis. The vertical span of * the graph divided by the returned increment may * not exceed this value. * @param graphSpan The span of values covered along the vertical * axis. * * @return The increment that should be used between labels on the vertical * axis of the generated line graph. */ private static double chooseVerticalLabelIncrement( final int numVerticalLabels, final double graphSpan) { // Determine whether the graph span is larger or smaller than 1. If larger // than 1, then we'll try to use whole numbers. If smaller than 1, then // obviously we can't so just do it the cheap way. if (graphSpan > 1) { // Determine the increment size if we wanted to break things up into // evenly-sized increments. final double roughIncrement = graphSpan / numVerticalLabels; // If the rough increment is itself less than 1, then we won't bother // with anything complex on it. if (roughIncrement < 1) { return roughIncrement; } // Determine the largest power of ten that is less than or equal to this // rough increment. This can be found using a base 10 logarithm. // However, since Java doesn't provide a function for finding a base 10 // logarithm, then we'll have to use a base e logarithm and divide the // result by the log base e of 10. final int largestPowerOfTen = (int) (Math.log(roughIncrement) / Math.log(10)); // OK. Now we at least have a decent starting point. This last part is // too complicated to comment, so either figure it out for yourself or // just trust that it does what we want. final int refinedIncrement = ((int) (roughIncrement / Math.pow(10, largestPowerOfTen))) * ((int) Math.pow(10, largestPowerOfTen)); final double fudgeFactor = 5 * Math.pow(10, (largestPowerOfTen - 1)); if ((refinedIncrement + fudgeFactor) < roughIncrement) { return (refinedIncrement + fudgeFactor); } else { return refinedIncrement; } } else { return graphSpan / numVerticalLabels; } } /** * Converts the provided X coordinate to the value along the horizontal axis * to which it corresponds. * * @param graphX The X coordinate of the point in the image. * * @return The value that corresponds to the provided X coordinate. */ private int graphXToValueX(final int graphX) { final int distFromOrigin = graphX - originX; final double fractionOfTotal = 1.0 * distFromOrigin / (lowerRightX - originX); return (int) (fractionOfTotal * numSeconds); } /** * Converts the provided value along the horizontal axis to an X coordinate in * the graph image. * * @param valueX The value along the horizontal axis for which to retrieve * the X coordinate. * * @return The X coordinate that corresponds to the specified value. */ private int valueXToGraphX(final int valueX) { final double fractionOfTotal = 1.0 * valueX / numSeconds; final int distFromOrigin = (int) (fractionOfTotal * (lowerRightX - originX)); return originX + distFromOrigin; } /** * Converts the provided value along the horizontal axis to an X coordinate in * the graph image. * * @param xValue The value along the horizontal axis for which to retrieve * the X coordinate. * @param xMax The maximum x value of any data point to be graphed. * * @return The X coordinate that corresponds to the specified value. */ private int valueXToGraphX(final double xValue, final double xMax) { final double fractionOfTotal = xValue / xMax; final int distFromOrigin = (int) (fractionOfTotal * (lowerRightX - originX)); return originX + distFromOrigin; } /** * Converts the provided Y coordinate to the value along the vertical axis to * which it corresponds. * * @param graphY The Y coordinate of the point in the image. * * @return The value that corresponds to the provided Y coordinate. */ private double graphYToValueY(final int graphY) { final int distFromOrigin = originY - graphY; final double fractionOfTotal = 1.0 * distFromOrigin / (originY - upperLeftY); final double increaseOverGraphMin = fractionOfTotal * graphSpan; return graphMin + increaseOverGraphMin; } /** * Converts the provided Y coordinate to the value along the specific vertical * axis to which it corresponds. This is to be used when the left and right * vertical axes represent different ranges of values. * * @param graphY The Y coordinate of the point in the image. * @param graphMin The minimum value for the vertical axis. * @param graphSpan The span for the vertical axis. * * @return The value that corresponds to the provided Y coordinate. */ private double graphYToValueY(final int graphY, final double graphMin, final double graphSpan) { final int distFromOrigin = originY - graphY; final double fractionOfTotal = 1.0 * distFromOrigin / (originY - upperLeftY); final double increaseOverGraphMin = fractionOfTotal * graphSpan; return graphMin + increaseOverGraphMin; } /** * Converts the provided value along the vertical axis to a Y coordinate in * the graph image. * * @param valueY The value along the vertical axis for which to retrieve * the Y coordinate. * * @return The Y coordinate that corresponds to the specified value. */ private int valueYToGraphY(final double valueY) { final double increaseOverGraphMin = valueY - graphMin; final double fractionOfTotal = increaseOverGraphMin / graphSpan; final int distFromOrigin = (int) (fractionOfTotal * (originY - upperLeftY)); return originY - distFromOrigin; } /** * Converts the provided value along the vertical axis to a Y coordinate in * the graph image. This is to be used when the left and right vertical axes * represent different ranges of values. * * @param valueY The value along the vertical axis for which to retrieve * the Y coordinate. * @param graphMin The minimum value for the vertical axis. * @param graphSpan The span for the vertical axis. * * @return The Y coordinate that corresponds to the specified value. */ private int valueYToGraphY(final double valueY, final double graphMin, final double graphSpan) { final double increaseOverGraphMin = valueY - graphMin; final double fractionOfTotal = increaseOverGraphMin / graphSpan; final int distFromOrigin = (int) (fractionOfTotal * (originY - upperLeftY)); return originY - distFromOrigin; } /** * Provides a simple test program, which displays the set of colors that will * be used to generate the graphs. * * @param args The command-line arguments provided to this program. * * @throws Exception If a problem occurs while generating the test image. */ public static void main(final String[] args) throws Exception { final String[] categoryNames = new String[COLORS.length]; for (int i=0; i < categoryNames.length; i++) { categoryNames[i] = "Color " + i; } final int[] numOccurrences = new int[categoryNames.length]; Arrays.fill(numOccurrences, 1); final StatGrapher grapher = new StatGrapher(1280, 1024, "Color Test"); grapher.setIncludeLegend(true, "Color Map"); final BufferedImage image = grapher.generatePieGraph(categoryNames, numOccurrences); final FileOutputStream outputStream = new FileOutputStream("test.png"); final ImageEncoder encoder = ImageCodec.createImageEncoder("png", outputStream, null); encoder.encode(image); outputStream.flush(); outputStream.close(); System.out.println("Wrote color test to test.png"); } }