/* * Copyright 2010 Tom Castle (www.tc33.org) * Licensed under GNU Lesser General Public License * * This file is part of JHeatChart - the heat maps charting api for Java. * * JHeatChart is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * JHeatChart is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with JHeatChart. If not, see <http://www.gnu.org/licenses/>. */ package net.seninp.util; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.Iterator; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.FileImageOutputStream; /** * The <code>HeatChart</code> class describes a chart which can display 3-dimensions of values - x,y * and z, where x and y are the usual 2-dimensional axis and z is portrayed by colour intensity. * Heat charts are sometimes known as heat maps. * * <p> * Use of this chart would typically involve 3 steps: * <ol> * <li>Construction of a new instance, providing the necessary z-values.</li> * <li>Configure the visual settings.</li> * <li>A call to either <code>getChartImage()</code> or <code>saveToFile(String)</code>.</li> * </ol> * * <h3>Instantiation</h3> * <p> * Construction of a new <code>HeatChart</code> instance is through its one constructor which takes * a 2-dimensional array of <tt>doubles</tt> which should contain the z-values for the chart. * Consider this array to be the grid of values which will instead be represented as colours in the * chart. * * <p> * Setting of the x-values and y-values which are displayed along the appropriate axis is optional, * and by default will simply display the values 0 to n-1, where n is the number of rows or columns. * Otherwise, the x/y axis values can be set with the <code>setXValues</code> and <code>setYValues * </code> methods. Both methods are overridden with two forms: * * <h4>Object axis values</h4> * * <p> * The simplist way to set the axis values is to use the methods which take an array of Object[]. * This array must have the same length as the number of columns for setXValues and same as the * number of rows for setYValues. The string representation of the objects will then be used as the * axis values. * * <h4>Offset and Interval</h4> * * <p> * This is convenient way of defining numerical values along the axis. One of the two methods takes * an interval and an offset for either the x or y axis. These parameters supply the necessary * information to describe the values based upon the z-value indexes. The quantity of x-values and * y-values is already known from the lengths of the z-values array dimensions. Then the offset * parameters indicate what the first value will be, with the intervals providing the increment from * one column or row to the next. * * <p> * <strong>Consider an example:</strong> <blockquote> * * <pre> * double[][] zValues = new double[][] { { 1.2, 1.3, 1.5 }, { 1.0, 1.1, 1.6 }, { 0.7, 0.9, 1.3 } }; * * double xOffset = 1.0; * double yOffset = 0.0; * double xInterval = 1.0; * double yInterval = 2.0; * * chart.setXValues(xOffset, xInterval); * chart.setYValues(yOffset, yInterval); * </pre> * * </blockquote> * * <p> * In this example, the z-values range from 0.7 to 1.6. The x-values range from the xOffset value * 1.0 to 4.0, which is calculated as the number of x-values multiplied by the xInterval, shifted by * the xOffset of 1.0. The y-values are calculated in the same way to give a range of values from * 0.0 to 6.0. * * <h3>Configuration</h3> * <p> * This step is optional. By default the heat chart will be generated without a title or labels on * the axis, and the colouring of the heat map will be in grayscale. A large range of configuration * options are available to customise the chart. All customisations are available through simple * accessor methods. See the javadoc of each of the methods for more information. * * <h3>Output</h3> * <p> * The generated heat chart can be obtained in two forms, using the following methods: * <ul> * <li><strong>getChartImage()</strong> - The chart will be returned as a <code>BufferedImage</code> * object that can be used in any number of ways, most notably it can be inserted into a Swing * component, for use in a GUI application.</li> * <li><strong>saveToFile(File)</strong> - The chart will be saved to the file system at the file * location specified as a parameter. The image format that the image will be saved in is derived * from the extension of the file name.</li> * </ul> * * <strong>Note:</strong> The chart image will not actually be created until either saveToFile(File) * or getChartImage() are called, and will be regenerated on each successive call. */ public class HeatChart { /** * A basic logarithmic scale value of 0.3. */ public static final double SCALE_LOGARITHMIC = 0.3; /** * The linear scale value of 1.0. */ public static final double SCALE_LINEAR = 1.0; /** * A basic exponential scale value of 3.0. */ public static final double SCALE_EXPONENTIAL = 3; // x, y, z data values. private double[][] zValues; private Object[] xValues; private Object[] yValues; private boolean xValuesHorizontal; private boolean yValuesHorizontal; // General chart settings. private Dimension cellSize; private Dimension chartSize; private int margin; private Color backgroundColour; // Title settings. private String title; private Font titleFont; private Color titleColour; private Dimension titleSize; private int titleAscent; // Axis settings. private int axisThickness; private Color axisColour; private Font axisLabelsFont; private Color axisLabelColour; private String xAxisLabel; private String yAxisLabel; private Color axisValuesColour; private Font axisValuesFont; // The font size will be considered the maximum font size - it may be // smaller if needed to fit in. private int xAxisValuesFrequency; private int yAxisValuesFrequency; private boolean showXAxisValues; private boolean showYAxisValues; // Generated axis properties. private int xAxisValuesHeight; private int xAxisValuesWidthMax; private int yAxisValuesHeight; private int yAxisValuesAscent; private int yAxisValuesWidthMax; private Dimension xAxisLabelSize; private int xAxisLabelDescent; private Dimension yAxisLabelSize; private int yAxisLabelAscent; // Heat map colour settings. private Color highValueColour; private Color lowValueColour; // How many RGB steps there are between the high and low colours. // private int colourValueDistance; private double lowValue; private double highValue; // Key co-ordinate positions. private Point heatMapTL; private Point heatMapBR; private Point heatMapC; // Heat map dimensions. private Dimension heatMapSize; // Control variable for mapping z-values to colours. private double colourScale; /** * Constructs a heatmap for the given z-values against x/y-values that by default will be the * values 0 to n-1, where n is the number of columns or rows. * * @param zValues the z-values, where each element is a row of z-values in the resultant heat * chart. */ public HeatChart(double[][] zValues) { this(zValues, min(zValues), max(zValues)); } /** * Constructs a heatmap for the given z-values against x/y-values that by default will be the * values 0 to n-1, where n is the number of columns or rows. * * @param zValues the z-values, where each element is a row of z-values in the resultant heat * chart. * @param low the minimum possible value, which may or may not appear in the z-values. * @param high the maximum possible value, which may or may not appear in the z-values. */ public HeatChart(double[][] zValues, double low, double high) { this.zValues = zValues; this.lowValue = low; this.highValue = high; // Default x/y-value settings. setXValues(0, 1); setYValues(0, 1); // Default chart settings. this.cellSize = new Dimension(20, 20); // this.margin = 10; this.margin = 10; this.backgroundColour = Color.WHITE; // Default title settings. this.title = null; // this.titleFont = new Font("Sans-Serif", Font.BOLD, 16); this.titleFont = new Font("Sans-Serif", Font.BOLD, 12); this.titleColour = Color.BLACK; // Default axis settings. this.xAxisLabel = null; this.yAxisLabel = null; this.axisThickness = 2; this.axisColour = Color.BLACK; // this.axisLabelsFont = new Font("Sans-Serif", Font.PLAIN, 12); this.axisLabelsFont = new Font("Sans-Serif", Font.PLAIN, 10); this.axisLabelColour = Color.BLACK; this.axisValuesColour = Color.BLACK; // this.axisValuesFont = new Font("Sans-Serif", Font.PLAIN, 10); this.axisValuesFont = new Font("Sans-Serif", Font.PLAIN, 8); this.xAxisValuesFrequency = 1; this.xAxisValuesHeight = 0; this.xValuesHorizontal = false; // this.showXAxisValues = true; this.showXAxisValues = false; // this.showYAxisValues = true; this.showYAxisValues = false; this.yAxisValuesFrequency = 1; this.yAxisValuesHeight = 0; this.yValuesHorizontal = true; // Default heatmap settings. this.highValueColour = Color.BLACK; this.lowValueColour = Color.WHITE; this.colourScale = SCALE_LINEAR; // updateColourDistance(); } /** * Returns the low value. This is the value at which the low value colour will be applied. * * @return the low value. */ public double getLowValue() { return lowValue; } /** * Returns the high value. This is the value at which the high value colour will be applied. * * @return the high value. */ public double getHighValue() { return highValue; } /** * Returns the 2-dimensional array of z-values currently in use. Each element is a double array * which represents one row of the heat map, or all the z-values for one y-value. * * @return an array of the z-values in current use, that is, those values which will define the * colour of each cell in the resultant heat map. */ public double[][] getZValues() { return zValues; } /** * Replaces the z-values array. See the {@link #setZValues(double[][], double, double)} method for * an example of z-values. The smallest and largest values in the array are used as the minimum * and maximum values respectively. * * @param zValues the array to replace the current array with. The number of elements in each * inner array must be identical. */ public void setZValues(double[][] zValues) { setZValues(zValues, min(zValues), max(zValues)); } /** * Replaces the z-values array. The number of elements should match the number of y-values, with * each element containing a double array with an equal number of elements that matches the number * of x-values. Use this method where the minimum and maximum values possible are not contained * within the dataset. * * @param zValues the array to replace the current array with. The number of elements in each * inner array must be identical. * @param low the minimum possible value, which may or may not appear in the z-values. * @param high the maximum possible value, which may or may not appear in the z-values. */ public void setZValues(double[][] zValues, double low, double high) { this.zValues = zValues; this.lowValue = low; this.highValue = high; } /** * Sets the x-values which are plotted along the x-axis. The x-values are calculated based upon * the indexes of the z-values array: * * <pre> * * {@code * * <pre> * x-value = x-offset + (column-index * x-interval) * </pre> * * } * * </pre> * * <p> * The x-interval defines the gap between each x-value and the x-offset is applied to each value * to offset them all from zero. * * <p> * Alternatively the x-values can be set more directly with the <code>setXValues(Object[])</code> * method. * * @param xOffset an offset value to be applied to the index of each z-value element. * @param xInterval an interval that will separate each x-value item. */ public void setXValues(double xOffset, double xInterval) { // Update the x-values according to the offset and interval. xValues = new Object[zValues[0].length]; for (int i = 0; i < zValues[0].length; i++) { xValues[i] = xOffset + (i * xInterval); } } /** * Sets the x-values which are plotted along the x-axis. The given x-values array must be the same * length as the z-values array has columns. Each of the x-values elements will be displayed * according to their toString representation. * * @param xValues an array of elements to be displayed as values along the x-axis. */ public void setXValues(Object[] xValues) { this.xValues = xValues; } /** * Sets the y-values which are plotted along the y-axis. The y-values are calculated based upon * the indexes of the z-values array: * * <pre> * * {@code * * <pre> * y-value = y-offset + (column-index * y-interval) * </pre> * * } * </pre> * * <p> * The y-interval defines the gap between each y-value and the y-offset is applied to each value * to offset them all from zero. * * <p> * Alternatively the y-values can be set more directly with the <code>setYValues(Object[])</code> * method. * * @param yOffset an offset value to be applied to the index of each z-value element. * @param yInterval an interval that will separate each y-value item. */ public void setYValues(double yOffset, double yInterval) { // Update the y-values according to the offset and interval. yValues = new Object[zValues.length]; for (int i = 0; i < zValues.length; i++) { yValues[i] = yOffset + (i * yInterval); } } /** * Sets the y-values which are plotted along the y-axis. The given y-values array must be the same * length as the z-values array has columns. Each of the y-values elements will be displayed * according to their toString representation. * * @param yValues an array of elements to be displayed as values along the y-axis. */ public void setYValues(Object[] yValues) { this.yValues = yValues; } /** * Returns the x-values which are currently set to display along the x-axis. The array that is * returned is either that which was explicitly set with <code>setXValues(Object[])</code> or that * was generated from the offset and interval that were given to * <code>setXValues(double, double)</code>, in which case the object type of each element will be * <code>Double</code>. * * @return an array of the values that are to be displayed along the x-axis. */ public Object[] getXValues() { return xValues; } /** * Returns the y-values which are currently set to display along the y-axis. The array that is * returned is either that which was explicitly set with <code>setYValues(Object[])</code> or that * was generated from the offset and interval that were given to * <code>setYValues(double, double)</code>, in which case the object type of each element will be * <code>Double</code>. * * @return an array of the values that are to be displayed along the y-axis. */ public Object[] getYValues() { return yValues; } /** * Sets whether the text of the values along the x-axis should be drawn horizontally * left-to-right, or vertically top-to-bottom. * * @param xValuesHorizontal true if x-values should be drawn horizontally, false if they should be * drawn vertically. */ public void setXValuesHorizontal(boolean xValuesHorizontal) { this.xValuesHorizontal = xValuesHorizontal; } /** * Returns whether the text of the values along the x-axis are to be drawn horizontally * left-to-right, or vertically top-to-bottom. * * @return true if the x-values will be drawn horizontally, false if they will be drawn * vertically. */ public boolean isXValuesHorizontal() { return xValuesHorizontal; } /** * Sets whether the text of the values along the y-axis should be drawn horizontally * left-to-right, or vertically top-to-bottom. * * @param yValuesHorizontal true if y-values should be drawn horizontally, false if they should be * drawn vertically. */ public void setYValuesHorizontal(boolean yValuesHorizontal) { this.yValuesHorizontal = yValuesHorizontal; } /** * Returns whether the text of the values along the y-axis are to be drawn horizontally * left-to-right, or vertically top-to-bottom. * * @return true if the y-values will be drawn horizontally, false if they will be drawn * vertically. */ public boolean isYValuesHorizontal() { return yValuesHorizontal; } /** * Sets the width of each individual cell that constitutes a value in x,y,z data space. By setting * the cell width, any previously set chart width will be overwritten with a value calculated * based upon this value and the number of cells in there are along the x-axis. * * @param cellWidth the new width to use for each individual data cell. * @deprecated As of release 0.6, replaced by {@link #setCellSize(Dimension)} */ @Deprecated public void setCellWidth(int cellWidth) { setCellSize(new Dimension(cellWidth, cellSize.height)); } /** * Returns the width of each individual data cell that constitutes a value in the x,y,z space. * * @return the width of each cell. * @deprecated As of release 0.6, replaced by {@link #getCellSize} */ @Deprecated public int getCellWidth() { return cellSize.width; } /** * Sets the height of each individual cell that constitutes a value in x,y,z data space. By * setting the cell height, any previously set chart height will be overwritten with a value * calculated based upon this value and the number of cells in there are along the y-axis. * * @param cellHeight the new height to use for each individual data cell. * @deprecated As of release 0.6, replaced by {@link #setCellSize(Dimension)} */ @Deprecated public void setCellHeight(int cellHeight) { setCellSize(new Dimension(cellSize.width, cellHeight)); } /** * Returns the height of each individual data cell that constitutes a value in the x,y,z space. * * @return the height of each cell. * @deprecated As of release 0.6, replaced by {@link #getCellSize()} */ @Deprecated public int getCellHeight() { return cellSize.height; } /** * Sets the size of each individual cell that constitutes a value in x,y,z data space. By setting * the cell size, any previously set chart size will be overwritten with a value calculated based * upon this value and the number of cells along each axis. * * @param cellSize the new size to use for each individual data cell. * @since 0.6 */ public void setCellSize(Dimension cellSize) { this.cellSize = cellSize; } /** * Returns the size of each individual data cell that constitutes a value in the x,y,z space. * * @return the size of each individual data cell. * @since 0.6 */ public Dimension getCellSize() { return cellSize; } /** * Returns the width of the chart in pixels as calculated according to the cell dimensions, chart * margin and other size settings. * * @return the width in pixels of the chart image to be generated. * @deprecated As of release 0.6, replaced by {@link #getChartSize()} */ @Deprecated public int getChartWidth() { return chartSize.width; } /** * Returns the height of the chart in pixels as calculated according to the cell dimensions, chart * margin and other size settings. * * @return the height in pixels of the chart image to be generated. * @deprecated As of release 0.6, replaced by {@link #getChartSize()} */ @Deprecated public int getChartHeight() { return chartSize.height; } /** * Returns the size of the chart in pixels as calculated according to the cell dimensions, chart * margin and other size settings. * * @return the size in pixels of the chart image to be generated. * @since 0.6 */ public Dimension getChartSize() { return chartSize; } /** * Returns the String that will be used as the title of any successive calls to generate a chart. * * @return the title of the chart. */ public String getTitle() { return title; } /** * Sets the String that will be used as the title of any successive calls to generate a chart. The * title will be displayed centralised horizontally at the top of any generated charts. * * <p> * If the title is set to <tt>null</tt> then no title will be displayed. * * <p> * Defaults to null. * * @param title the chart title to set. */ public void setTitle(String title) { this.title = title; } /** * Returns the String that will be displayed as a description of the x-axis in any generated * charts. * * @return the display label describing the x-axis. */ public String getXAxisLabel() { return xAxisLabel; } /** * Sets the String that will be displayed as a description of the x-axis in any generated charts. * The label will be displayed horizontally central of the x-axis bar. * * <p> * If the xAxisLabel is set to <tt>null</tt> then no label will be displayed. * * <p> * Defaults to null. * * @param xAxisLabel the label to be displayed describing the x-axis. */ public void setXAxisLabel(String xAxisLabel) { this.xAxisLabel = xAxisLabel; } /** * Returns the String that will be displayed as a description of the y-axis in any generated * charts. * * @return the display label describing the y-axis. */ public String getYAxisLabel() { return yAxisLabel; } /** * Sets the String that will be displayed as a description of the y-axis in any generated charts. * The label will be displayed horizontally central of the y-axis bar. * * <p> * If the yAxisLabel is set to <tt>null</tt> then no label will be displayed. * * <p> * Defaults to null. * * @param yAxisLabel the label to be displayed describing the y-axis. */ public void setYAxisLabel(String yAxisLabel) { this.yAxisLabel = yAxisLabel; } /** * Returns the width of the margin in pixels to be left as empty space around the heat map * element. * * @return the size of the margin to be left blank around the edge of the chart. */ public int getChartMargin() { return margin; } /** * Sets the width of the margin in pixels to be left as empty space around the heat map element. * If a title is set then half the margin will be directly above the title and half directly below * it. Where axis labels are set then the axis labels may sit partially in the margin. * * <p> * Defaults to 20 pixels. * * @param margin the new margin to be left as blank space around the heat map. */ public void setChartMargin(int margin) { this.margin = margin; } /** * Returns an object that represents the colour to be used as the background for the whole chart. * * @return the colour to be used to fill the chart background. */ public Color getBackgroundColour() { return backgroundColour; } /** * Sets the colour to be used on the background of the chart. A transparent background can be set * by setting a background colour with an alpha value. The transparency will only be effective * when the image is saved as a png or gif. * * <p> * Defaults to <code>Color.WHITE</code>. * * @param backgroundColour the new colour to be set as the background fill. */ public void setBackgroundColour(Color backgroundColour) { if (backgroundColour == null) { backgroundColour = Color.WHITE; } this.backgroundColour = backgroundColour; } /** * Returns the <code>Font</code> that describes the visual style of the title. * * @return the Font that will be used to render the title. */ public Font getTitleFont() { return titleFont; } /** * Sets a new <code>Font</code> to be used in rendering the chart's title String. * * <p> * Defaults to Sans-Serif, BOLD, 16 pixels. * * @param titleFont the Font that should be used when rendering the chart title. */ public void setTitleFont(Font titleFont) { this.titleFont = titleFont; } /** * Returns the <code>Color</code> that represents the colour the title text should be painted in. * * @return the currently set colour to be used in painting the chart title. */ public Color getTitleColour() { return titleColour; } /** * Sets the <code>Color</code> that describes the colour to be used for the chart title String. * * <p> * Defaults to <code>Color.BLACK</code>. * * @param titleColour the colour to paint the chart's title String. */ public void setTitleColour(Color titleColour) { this.titleColour = titleColour; } /** * Returns the width of the axis bars in pixels. Both axis bars have the same thickness. * * @return the thickness of the axis bars in pixels. */ public int getAxisThickness() { return axisThickness; } /** * Sets the width of the axis bars in pixels. Both axis bars use the same thickness. * * <p> * Defaults to 2 pixels. * * @param axisThickness the thickness to use for the axis bars in any newly generated charts. */ public void setAxisThickness(int axisThickness) { this.axisThickness = axisThickness; } /** * Returns the colour that is set to be used for the axis bars. Both axis bars use the same * colour. * * @return the colour in use for the axis bars. */ public Color getAxisColour() { return axisColour; } /** * Sets the colour to be used on the axis bars. Both axis bars use the same colour. * * <p> * Defaults to <code>Color.BLACK</code>. * * @param axisColour the colour to be set for use on the axis bars. */ public void setAxisColour(Color axisColour) { this.axisColour = axisColour; } /** * Returns the font that describes the visual style of the labels of the axis. Both axis' labels * use the same font. * * @return the font used to define the visual style of the axis labels. */ public Font getAxisLabelsFont() { return axisLabelsFont; } /** * Sets the font that describes the visual style of the axis labels. Both axis' labels use the * same font. * * <p> * Defaults to Sans-Serif, PLAIN, 12 pixels. * * @param axisLabelsFont the font to be used to define the visual style of the axis labels. */ public void setAxisLabelsFont(Font axisLabelsFont) { this.axisLabelsFont = axisLabelsFont; } /** * Returns the current colour of the axis labels. Both labels use the same colour. * * @return the colour of the axis label text. */ public Color getAxisLabelColour() { return axisLabelColour; } /** * Sets the colour of the text displayed as axis labels. Both labels use the same colour. * * <p> * Defaults to Color.BLACK. * * @param axisLabelColour the colour to use for the axis label text. */ public void setAxisLabelColour(Color axisLabelColour) { this.axisLabelColour = axisLabelColour; } /** * Returns the font which describes the visual style of the axis values. The axis values are those * values displayed alongside the axis bars at regular intervals. Both axis use the same font. * * @return the font in use for the axis values. */ public Font getAxisValuesFont() { return axisValuesFont; } /** * Sets the font which describes the visual style of the axis values. The axis values are those * values displayed alongside the axis bars at regular intervals. Both axis use the same font. * * <p> * Defaults to Sans-Serif, PLAIN, 10 pixels. * * @param axisValuesFont the font that should be used for the axis values. */ public void setAxisValuesFont(Font axisValuesFont) { this.axisValuesFont = axisValuesFont; } /** * Returns the colour of the axis values as they will be painted along the axis bars. Both axis * use the same colour. * * @return the colour of the values displayed along the axis bars. */ public Color getAxisValuesColour() { return axisValuesColour; } /** * Sets the colour to be used for the axis values as they will be painted along the axis bars. * Both axis use the same colour. * * <p> * Defaults to Color.BLACK. * * @param axisValuesColour the new colour to be used for the axis bar values. */ public void setAxisValuesColour(Color axisValuesColour) { this.axisValuesColour = axisValuesColour; } /** * Returns the frequency of the values displayed along the x-axis. The frequency is how many * columns in the x-dimension have their value displayed. A frequency of 2 would mean every other * column has a value shown and a frequency of 3 would mean every third column would be given a * value. * * @return the frequency of the values displayed against columns. */ public int getXAxisValuesFrequency() { return xAxisValuesFrequency; } /** * Sets the frequency of the values displayed along the x-axis. The frequency is how many columns * in the x-dimension have their value displayed. A frequency of 2 would mean every other column * has a value and a frequency of 3 would mean every third column would be given a value. * * <p> * Defaults to 1. Every column is given a value. * * @param axisValuesFrequency the frequency of the values displayed against columns, where 1 is * every column and 2 is every other column. */ public void setXAxisValuesFrequency(int axisValuesFrequency) { this.xAxisValuesFrequency = axisValuesFrequency; } /** * Returns the frequency of the values displayed along the y-axis. The frequency is how many rows * in the y-dimension have their value displayed. A frequency of 2 would mean every other row has * a value and a frequency of 3 would mean every third row would be given a value. * * @return the frequency of the values displayed against rows. */ public int getYAxisValuesFrequency() { return yAxisValuesFrequency; } /** * Sets the frequency of the values displayed along the y-axis. The frequency is how many rows in * the y-dimension have their value displayed. A frequency of 2 would mean every other row has a * value and a frequency of 3 would mean every third row would be given a value. * * <p> * Defaults to 1. Every row is given a value. * * @param axisValuesFrequency the frequency of the values displayed against rows, where 1 is every * row and 2 is every other row. */ public void setYAxisValuesFrequency(int axisValuesFrequency) { yAxisValuesFrequency = axisValuesFrequency; } /** * Returns whether axis values are to be shown at all for the x-axis. * * <p> * If axis values are not shown then more space is allocated to the heat map. * * @return true if the x-axis values will be displayed, false otherwise. */ public boolean isShowXAxisValues() { // TODO Could get rid of these flags and use a frequency of -1 to signal no values. return showXAxisValues; } /** * Sets whether axis values are to be shown at all for the x-axis. * * <p> * If axis values are not shown then more space is allocated to the heat map. * * <p> * Defaults to true. * * @param showXAxisValues true if x-axis values should be displayed, false if they should be * hidden. */ public void setShowXAxisValues(boolean showXAxisValues) { this.showXAxisValues = showXAxisValues; } /** * Returns whether axis values are to be shown at all for the y-axis. * * <p> * If axis values are not shown then more space is allocated to the heat map. * * @return true if the y-axis values will be displayed, false otherwise. */ public boolean isShowYAxisValues() { return showYAxisValues; } /** * Sets whether axis values are to be shown at all for the y-axis. * * <p> * If axis values are not shown then more space is allocated to the heat map. * * <p> * Defaults to true. * * @param showYAxisValues true if y-axis values should be displayed, false if they should be * hidden. */ public void setShowYAxisValues(boolean showYAxisValues) { this.showYAxisValues = showYAxisValues; } /** * Returns the colour that is currently to be displayed for the heat map cells with the highest * z-value in the dataset. * * <p> * The full colour range will go through each RGB step between the high value colour and the low * value colour. * * @return the colour in use for cells of the highest z-value. */ public Color getHighValueColour() { return highValueColour; } /** * Sets the colour to be used to fill cells of the heat map with the highest z-values in the * dataset. * * <p> * The full colour range will go through each RGB step between the high value colour and the low * value colour. * * <p> * Defaults to Color.BLACK. * * @param highValueColour the colour to use for cells of the highest z-value. */ public void setHighValueColour(Color highValueColour) { this.highValueColour = highValueColour; // updateColourDistance(); } /** * Returns the colour that is currently to be displayed for the heat map cells with the lowest * z-value in the dataset. * * <p> * The full colour range will go through each RGB step between the high value colour and the low * value colour. * * @return the colour in use for cells of the lowest z-value. */ public Color getLowValueColour() { return lowValueColour; } /** * Sets the colour to be used to fill cells of the heat map with the lowest z-values in the * dataset. * * <p> * The full colour range will go through each RGB step between the high value colour and the low * value colour. * * <p> * Defaults to Color.WHITE. * * @param lowValueColour the colour to use for cells of the lowest z-value. */ public void setLowValueColour(Color lowValueColour) { this.lowValueColour = lowValueColour; // updateColourDistance(); } /** * Returns the scale that is currently in use to map z-value to colour. A value of 1.0 will give a * <strong>linear</strong> scale, which will spread the distribution of colours evenly amoungst * the full range of represented z-values. A value of greater than 1.0 will give an * <strong>exponential</strong> scale that will produce greater emphasis for the separation * between higher values and a value between 0.0 and 1.0 will provide a * <strong>logarithmic</strong> scale, with greater separation of low values. * * @return the scale factor that is being used to map from z-value to colour. */ public double getColourScale() { return colourScale; } /** * Sets the scale that is currently in use to map z-value to colour. A value of 1.0 will give a * <strong>linear</strong> scale, which will spread the distribution of colours evenly amoungst * the full range of represented z-values. A value of greater than 1.0 will give an * <strong>exponential</strong> scale that will produce greater emphasis for the separation * between higher values and a value between 0.0 and 1.0 will provide a * <strong>logarithmic</strong> scale, with greater separation of low values. Values of 0.0 or * less are illegal. * * <p> * Defaults to a linear scale value of 1.0. * * @param colourScale the scale that should be used to map from z-value to colour. */ public void setColourScale(double colourScale) { this.colourScale = colourScale; } /* * Calculate and update the field for the distance between the low colour and high colour. The * distance is the number of steps between one colour and the other using an RGB coding with 0-255 * values for each of red, green and blue. So the maximum colour distance is 255 + 255 + 255. */ // private void updateColourDistance() { // int r1 = lowValueColour.getRed(); // int g1 = lowValueColour.getGreen(); // int b1 = lowValueColour.getBlue(); // int r2 = highValueColour.getRed(); // int g2 = highValueColour.getGreen(); // int b2 = highValueColour.getBlue(); // // colourValueDistance = Math.abs(r1 - r2); // colourValueDistance += Math.abs(g1 - g2); // colourValueDistance += Math.abs(b1 - b2); // } /** * Generates a new chart <code>Image</code> based upon the currently held settings and then * attempts to save that image to disk, to the location provided as a File parameter. The image * type of the saved file will equal the extension of the filename provided, so it is essential * that a suitable extension be included on the file name. * * <p> * All supported <code>ImageIO</code> file types are supported, including PNG, JPG and GIF. * * <p> * No chart will be generated until this or the related <code>getChartImage()</code> method are * called. All successive calls will result in the generation of a new chart image, no caching is * used. * * @param outputFile the file location that the generated image file should be written to. The * File must have a suitable filename, with an extension of a valid image format (as supported by * <code>ImageIO</code>). * @throws IOException if the output file's filename has no extension or if there the file is * unable to written to. Reasons for this include a non-existant file location (check with the * File exists() method on the parent directory), or the permissions of the write location may be * incorrect. */ public void saveToFile(File outputFile) throws IOException { String filename = outputFile.getName(); int extPoint = filename.lastIndexOf('.'); if (extPoint < 0) { throw new IOException("Illegal filename, no extension used."); } // Determine the extension of the filename. String ext = filename.substring(extPoint + 1); // Handle jpg without transparency. if (ext.toLowerCase().equals("jpg") || ext.toLowerCase().equals("jpeg")) { BufferedImage chart = (BufferedImage) getChartImage(false); // Save our graphic. saveGraphicJpeg(chart, outputFile, 1.0f); } else { BufferedImage chart = (BufferedImage) getChartImage(true); ImageIO.write(chart, ext, outputFile); } } private void saveGraphicJpeg(BufferedImage chart, File outputFile, float quality) throws IOException { // Setup correct compression for jpeg. Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName("jpeg"); ImageWriter writer = (ImageWriter) iter.next(); ImageWriteParam iwp = writer.getDefaultWriteParam(); iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); iwp.setCompressionQuality(quality); // Output the image. FileImageOutputStream output = new FileImageOutputStream(outputFile); writer.setOutput(output); IIOImage image = new IIOImage(chart, null, null); writer.write(null, image, iwp); writer.dispose(); } /** * Generates and returns a new chart <code>Image</code> configured according to this object's * currently held settings. The given parameter determines whether transparency should be enabled * for the generated image. * * <p> * No chart will be generated until this or the related <code>saveToFile(File)</code> method are * called. All successive calls will result in the generation of a new chart image, no caching is * used. * * @param alpha whether to enable transparency. * @return A newly generated chart <code>Image</code>. The returned image is a * <code>BufferedImage</code>. */ public Image getChartImage(boolean alpha) { // Calculate all unknown dimensions. measureComponents(); updateCoordinates(); // Determine image type based upon whether require alpha or not. // Using BufferedImage.TYPE_INT_ARGB seems to break on jpg. int imageType = (alpha ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR); // Create our chart image which we will eventually draw everything on. BufferedImage chartImage = new BufferedImage(chartSize.width, chartSize.height, imageType); Graphics2D chartGraphics = chartImage.createGraphics(); // Use anti-aliasing where ever possible. chartGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Set the background. chartGraphics.setColor(backgroundColour); chartGraphics.fillRect(0, 0, chartSize.width, chartSize.height); // Draw the title. drawTitle(chartGraphics); // Draw the heatmap image. drawHeatMap(chartGraphics, zValues); // Draw the axis labels. drawXLabel(chartGraphics); drawYLabel(chartGraphics); // Draw the axis bars. drawAxisBars(chartGraphics); // Draw axis values. drawXValues(chartGraphics); drawYValues(chartGraphics); return chartImage; } /** * Generates and returns a new chart <code>Image</code> configured according to this object's * currently held settings. By default the image is generated with no transparency. * * <p> * No chart will be generated until this or the related <code>saveToFile(File)</code> method are * called. All successive calls will result in the generation of a new chart image, no caching is * used. * * @return A newly generated chart <code>Image</code>. The returned image is a * <code>BufferedImage</code>. */ public Image getChartImage() { return getChartImage(false); } /* * Calculates all unknown component dimensions. */ private void measureComponents() { // TODO This would be a good place to check that all settings have sensible values or throw // illegal state exception. // TODO Put this somewhere so it only gets created once. BufferedImage chartImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); Graphics2D tempGraphics = chartImage.createGraphics(); // Calculate title dimensions. if (title != null) { tempGraphics.setFont(titleFont); FontMetrics metrics = tempGraphics.getFontMetrics(); titleSize = new Dimension(metrics.stringWidth(title), metrics.getHeight()); titleAscent = metrics.getAscent(); } else { titleSize = new Dimension(0, 0); } // Calculate x-axis label dimensions. if (xAxisLabel != null) { tempGraphics.setFont(axisLabelsFont); FontMetrics metrics = tempGraphics.getFontMetrics(); xAxisLabelSize = new Dimension(metrics.stringWidth(xAxisLabel), metrics.getHeight()); xAxisLabelDescent = metrics.getDescent(); } else { xAxisLabelSize = new Dimension(0, 0); } // Calculate y-axis label dimensions. if (yAxisLabel != null) { tempGraphics.setFont(axisLabelsFont); FontMetrics metrics = tempGraphics.getFontMetrics(); yAxisLabelSize = new Dimension(metrics.stringWidth(yAxisLabel), metrics.getHeight()); yAxisLabelAscent = metrics.getAscent(); } else { yAxisLabelSize = new Dimension(0, 0); } // Calculate x-axis value dimensions. if (showXAxisValues) { tempGraphics.setFont(axisValuesFont); FontMetrics metrics = tempGraphics.getFontMetrics(); xAxisValuesHeight = metrics.getHeight(); xAxisValuesWidthMax = 0; for (Object o : xValues) { int w = metrics.stringWidth(o.toString()); if (w > xAxisValuesWidthMax) { xAxisValuesWidthMax = w; } } } else { xAxisValuesHeight = 0; } // Calculate y-axis value dimensions. if (showYAxisValues) { tempGraphics.setFont(axisValuesFont); FontMetrics metrics = tempGraphics.getFontMetrics(); yAxisValuesHeight = metrics.getHeight(); yAxisValuesAscent = metrics.getAscent(); yAxisValuesWidthMax = 0; for (Object o : yValues) { int w = metrics.stringWidth(o.toString()); if (w > yAxisValuesWidthMax) { yAxisValuesWidthMax = w; } } } else { yAxisValuesHeight = 0; } // Calculate heatmap dimensions. int heatMapWidth = (zValues[0].length * cellSize.width); int heatMapHeight = (zValues.length * cellSize.height); heatMapSize = new Dimension(heatMapWidth, heatMapHeight); int yValuesHorizontalSize = 0; if (yValuesHorizontal) { yValuesHorizontalSize = yAxisValuesWidthMax; } else { yValuesHorizontalSize = yAxisValuesHeight; } int xValuesVerticalSize = 0; if (xValuesHorizontal) { xValuesVerticalSize = xAxisValuesHeight; } else { xValuesVerticalSize = xAxisValuesWidthMax; } // Calculate chart dimensions. int chartWidth = heatMapWidth + (2 * margin) + yAxisLabelSize.height + yValuesHorizontalSize + axisThickness; int chartHeight = heatMapHeight + (2 * margin) + xAxisLabelSize.height + xValuesVerticalSize + titleSize.height + axisThickness; chartSize = new Dimension(chartWidth, chartHeight); } /* * Calculates the co-ordinates of some key positions. */ private void updateCoordinates() { // Top-left of heat map. int x = margin + axisThickness + yAxisLabelSize.height; x += (yValuesHorizontal ? yAxisValuesWidthMax : yAxisValuesHeight); int y = titleSize.height + margin; heatMapTL = new Point(x, y); // Top-right of heat map. x = heatMapTL.x + heatMapSize.width; y = heatMapTL.y + heatMapSize.height; heatMapBR = new Point(x, y); // Centre of heat map. x = heatMapTL.x + (heatMapSize.width / 2); y = heatMapTL.y + (heatMapSize.height / 2); heatMapC = new Point(x, y); } /* * Draws the title String on the chart if title is not null. */ private void drawTitle(Graphics2D chartGraphics) { if (title != null) { // Strings are drawn from the baseline position of the leftmost char. int yTitle = (margin / 2) + titleAscent; int xTitle = (chartSize.width / 2) - (titleSize.width / 2); chartGraphics.setFont(titleFont); chartGraphics.setColor(titleColour); chartGraphics.drawString(title, xTitle, yTitle); } } /* * Creates the actual heatmap element as an image, that can then be drawn onto a chart. */ private void drawHeatMap(Graphics2D chartGraphics, double[][] data) { // Calculate the available size for the heatmap. int noYCells = data.length; int noXCells = data[0].length; // double dataMin = min(data); // double dataMax = max(data); BufferedImage heatMapImage = new BufferedImage(heatMapSize.width, heatMapSize.height, BufferedImage.TYPE_INT_ARGB); Graphics2D heatMapGraphics = heatMapImage.createGraphics(); for (int x = 0; x < noXCells; x++) { for (int y = 0; y < noYCells; y++) { // Set colour depending on zValues. heatMapGraphics.setColor(getCellColour(data[y][x], lowValue, highValue)); int cellX = x * cellSize.width; int cellY = y * cellSize.height; heatMapGraphics.fillRect(cellX, cellY, cellSize.width, cellSize.height); } } // Draw the heat map onto the chart. chartGraphics.drawImage(heatMapImage, heatMapTL.x, heatMapTL.y, heatMapSize.width, heatMapSize.height, null); } /* * Draws the x-axis label string if it is not null. */ private void drawXLabel(Graphics2D chartGraphics) { if (xAxisLabel != null) { // Strings are drawn from the baseline position of the leftmost char. int yPosXAxisLabel = chartSize.height - (margin / 2) - xAxisLabelDescent; // TODO This will need to be updated if the y-axis values/label can be moved to the right. int xPosXAxisLabel = heatMapC.x - (xAxisLabelSize.width / 2); chartGraphics.setFont(axisLabelsFont); chartGraphics.setColor(axisLabelColour); chartGraphics.drawString(xAxisLabel, xPosXAxisLabel, yPosXAxisLabel); } } /* * Draws the y-axis label string if it is not null. */ private void drawYLabel(Graphics2D chartGraphics) { if (yAxisLabel != null) { // Strings are drawn from the baseline position of the leftmost char. int yPosYAxisLabel = heatMapC.y + (yAxisLabelSize.width / 2); int xPosYAxisLabel = (margin / 2) + yAxisLabelAscent; chartGraphics.setFont(axisLabelsFont); chartGraphics.setColor(axisLabelColour); // Create 270 degree rotated transform. AffineTransform transform = chartGraphics.getTransform(); AffineTransform originalTransform = (AffineTransform) transform.clone(); transform.rotate(Math.toRadians(270), xPosYAxisLabel, yPosYAxisLabel); chartGraphics.setTransform(transform); // Draw string. chartGraphics.drawString(yAxisLabel, xPosYAxisLabel, yPosYAxisLabel); // Revert to original transform before rotation. chartGraphics.setTransform(originalTransform); } } /* * Draws the bars of the x-axis and y-axis. */ private void drawAxisBars(Graphics2D chartGraphics) { if (axisThickness > 0) { chartGraphics.setColor(axisColour); // Draw x-axis. int x = heatMapTL.x - axisThickness; int y = heatMapBR.y; int width = heatMapSize.width + axisThickness; int height = axisThickness; chartGraphics.fillRect(x, y, width, height); // Draw y-axis. x = heatMapTL.x - axisThickness; y = heatMapTL.y; width = axisThickness; height = heatMapSize.height; chartGraphics.fillRect(x, y, width, height); } } /* * Draws the x-values onto the x-axis if showXAxisValues is set to true. */ private void drawXValues(Graphics2D chartGraphics) { if (!showXAxisValues) { return; } chartGraphics.setColor(axisValuesColour); for (int i = 0; i < xValues.length; i++) { if (i % xAxisValuesFrequency != 0) { continue; } String xValueStr = xValues[i].toString(); chartGraphics.setFont(axisValuesFont); FontMetrics metrics = chartGraphics.getFontMetrics(); int valueWidth = metrics.stringWidth(xValueStr); if (xValuesHorizontal) { // Draw the value with whatever font is now set. int valueXPos = (i * cellSize.width) + ((cellSize.width / 2) - (valueWidth / 2)); valueXPos += heatMapTL.x; int valueYPos = heatMapBR.y + metrics.getAscent() + 1; chartGraphics.drawString(xValueStr, valueXPos, valueYPos); } else { int valueXPos = heatMapTL.x + (i * cellSize.width) + ((cellSize.width / 2) + (xAxisValuesHeight / 2)); int valueYPos = heatMapBR.y + axisThickness + valueWidth; // Create 270 degree rotated transform. AffineTransform transform = chartGraphics.getTransform(); AffineTransform originalTransform = (AffineTransform) transform.clone(); transform.rotate(Math.toRadians(270), valueXPos, valueYPos); chartGraphics.setTransform(transform); // Draw the string. chartGraphics.drawString(xValueStr, valueXPos, valueYPos); // Revert to original transform before rotation. chartGraphics.setTransform(originalTransform); } } } /* * Draws the y-values onto the y-axis if showYAxisValues is set to true. */ private void drawYValues(Graphics2D chartGraphics) { if (!showYAxisValues) { return; } chartGraphics.setColor(axisValuesColour); for (int i = 0; i < yValues.length; i++) { if (i % yAxisValuesFrequency != 0) { continue; } String yValueStr = yValues[i].toString(); chartGraphics.setFont(axisValuesFont); FontMetrics metrics = chartGraphics.getFontMetrics(); int valueWidth = metrics.stringWidth(yValueStr); if (yValuesHorizontal) { // Draw the value with whatever font is now set. int valueXPos = margin + yAxisLabelSize.height + (yAxisValuesWidthMax - valueWidth); int valueYPos = heatMapTL.y + (i * cellSize.height) + (cellSize.height / 2) + (yAxisValuesAscent / 2); chartGraphics.drawString(yValueStr, valueXPos, valueYPos); } else { int valueXPos = margin + yAxisLabelSize.height + yAxisValuesAscent; int valueYPos = heatMapTL.y + (i * cellSize.height) + (cellSize.height / 2) + (valueWidth / 2); // Create 270 degree rotated transform. AffineTransform transform = chartGraphics.getTransform(); AffineTransform originalTransform = (AffineTransform) transform.clone(); transform.rotate(Math.toRadians(270), valueXPos, valueYPos); chartGraphics.setTransform(transform); // Draw the string. chartGraphics.drawString(yValueStr, valueXPos, valueYPos); // Revert to original transform before rotation. chartGraphics.setTransform(originalTransform); } } } /* * Determines what colour a heat map cell should be based upon the cell values. */ private Color getCellColour(double data, double min, double max) { double range = max - min; double position = data - min; // What proportion of the way through the possible values is that. double percentPosition = position / range; // Which colour group does that put us in. // int colourPosition = getColourPosition(percentPosition); int r = (int) (255 * red(percentPosition * 2 - 1)); int g = (int) (255 * green(percentPosition * 2 - 1)); int b = (int) (255 * blue(percentPosition * 2 - 1)); // int r = lowValueColour.getRed(); // int g = lowValueColour.getGreen(); // int b = lowValueColour.getBlue(); // // // Make n shifts of the colour, where n is the colourPosition. // for (int i = 0; i < colourPosition; i++) { // int rDistance = r - highValueColour.getRed(); // int gDistance = g - highValueColour.getGreen(); // int bDistance = b - highValueColour.getBlue(); // // if ((Math.abs(rDistance) >= Math.abs(gDistance)) // && (Math.abs(rDistance) >= Math.abs(bDistance))) { // // Red must be the largest. // r = changeColourValue(r, rDistance); // } // else if (Math.abs(gDistance) >= Math.abs(bDistance)) { // // Green must be the largest. // g = changeColourValue(g, gDistance); // } // else { // // Blue must be the largest. // b = changeColourValue(b, bDistance); // } // } return new Color(r, g, b); } /* * Returns how many colour shifts are required from the lowValueColour to get to the correct * colour position. The result will be different depending on the colour scale used: LINEAR, * LOGARITHMIC, EXPONENTIAL. */ // private int getColourPosition(double percentPosition) { // return (int) Math.round(colourValueDistance * Math.pow(percentPosition, colourScale)); // } // private int changeColourValue(int colourValue, int colourDistance) { // if (colourDistance < 0) { // return colourValue + 1; // } // else if (colourDistance > 0) { // return colourValue - 1; // } // else { // // This shouldn't actually happen here. // return colourValue; // } // } /** * Finds and returns the maximum value in a 2-dimensional array of doubles. * * @param values the data to use. * * @return the largest value in the array. */ public static double max(double[][] values) { double max = 0; for (int i = 0; i < values.length; i++) { for (int j = 0; j < values[i].length; j++) { max = (values[i][j] > max) ? values[i][j] : max; } } return max; } /** * Finds and returns the minimum value in a 2-dimensional array of doubles. * * @param values the data to use. * * @return the smallest value in the array. */ public static double min(double[][] values) { double min = Double.MAX_VALUE; for (int i = 0; i < values.length; i++) { for (int j = 0; j < values[i].length; j++) { min = (values[i][j] < min) ? values[i][j] : min; } } return min; } // the part below is taken from // http://stackoverflow.com/questions/7706339/grayscale-to-red-green-blue-matlab-jet-color-scale // // double interpolate(double val, double y0, double x0, double y1, double x1) { return (val - x0) * (y1 - y0) / (x1 - x0) + y0; } double base(double val) { if (val <= -0.75) return 0; else if (val <= -0.25) return interpolate(val, 0.0, -0.75, 1.0, -0.25); else if (val <= 0.25) return 1.0; else if (val <= 0.75) return interpolate(val, 1.0, 0.25, 0.0, 0.75); else return 0.0; } double red(double gray) { return base(gray - 0.5); } double green(double gray) { return base(gray); } double blue(double gray) { return base(gray + 0.5); } }