// Plot.java package net.sf.gogui.tools.statistics; import java.awt.Stroke; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Point; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.text.DecimalFormat; import javax.imageio.ImageIO; import net.sf.gogui.gui.GuiUtil; import net.sf.gogui.util.Table; import net.sf.gogui.util.TableUtil; /** Produce a PNG plot from table data. */ public class Plot { public Plot(int imgWidth, int imgHeight, Color color, int precision) { m_precision = precision; m_color = color; m_imgWidth = imgWidth; m_imgHeight = imgHeight; } public void plot(File file, Table table, String columnX, String columnY, String errorColumn) throws IOException { int type = BufferedImage.TYPE_INT_RGB; BufferedImage image = new BufferedImage(m_imgWidth, m_imgHeight, type); m_graphics2D = image.createGraphics(); GuiUtil.setAntiAlias(m_graphics2D); Font font = m_graphics2D.getFont(); if (font != null) { font = font.deriveFont((float)(font.getSize() * 0.8)); m_graphics2D.setFont(font); } m_metrics = m_graphics2D.getFontMetrics(); m_fontHeight = m_metrics.getHeight(); m_left = 4 * m_fontHeight; if (m_title == null) m_top = (int)(m_fontHeight * 0.5); else m_top = (int)(m_fontHeight * 1.7); m_right = m_imgWidth - (int)(m_fontHeight * 0.5); m_bottom = m_imgHeight - (int)(m_fontHeight * 1.5); m_width = m_right - m_left; m_height = m_bottom - m_top; initScale(table, columnX, columnY); drawBackground(); drawGrid(); drawData(table, columnX, columnY, errorColumn, m_withBars); m_graphics2D.dispose(); ImageIO.write(image, "png", file); } /** Set number format for x-axis. @param format The format. */ public void setFormatX(DecimalFormat format) { m_formatX = format; } /** Set number format for y-axis. @param format The format. */ public void setFormatY(DecimalFormat format) { m_formatY = format; } /** Set plot style to bars. Default is plotting points connected by lines. @param barWidth The width of each bar. */ public void setPlotStyleBars(double barWidth) { m_withBars = true; m_barWidth = barWidth; } /** Don't connect plotted points with lines. */ public void setPlotStyleNoLines() { m_noLines = true; } /** Disable drawing the zero axis for the y-coordinates. */ public void setNoPlotYZero() { m_plotYZero = false; } /** Enable drawing of solid lines at certain x-intervals. NOTE: The new implementation does no longer use solid lines, but changes white and gray background color at the solid line interval. @param solidLineInterval The interval for the solid lines. */ public void setSolidLineInterval(double solidLineInterval) { m_solidLineInterval = solidLineInterval; m_useSolidLineInterval = true; } /** Set x-label intervals. By default, every x-tic (grid line) gets an x-label. NOTE: Misleading name, should be tics per x-label. @param xLabelPerTic The number of tics per x-label. */ public void setXLabelPerTic(int xLabelPerTic) { m_xLabelPerTic = xLabelPerTic; } /** Plot only x labels for 0 and 1. */ public void setXLabelsBool() { m_xLabelsBool = true; setXMin(-5); setXMax(5); setXTics(1); } /** Set maximum x value. @param max The maximum. */ public void setXMax(double max) { m_maxX = max; m_autoXMax = false; } /** Set minimum x value. @param min The minimum. */ public void setXMin(double min) { m_minX = min; m_autoXMin = false; } /** Set x-tics. Sets the grid line distance for the x-axis. @param tics The distance. */ public void setXTics(double tics) { m_xTics = tics; m_autoXTics = false; } /** Set maximum y value. @param max The maximum. */ public void setYMax(double max) { m_maxY = max; m_autoYMax = false; } /** Set minimum x value. @param min The minimum. */ public void setYMin(double min) { m_minY = min; m_autoYMin = false; } /** Set y-tics. Sets the grid line distance for the y-axis. @param tics The distance. */ public void setYTics(double tics) { m_yTics = tics; m_autoYTics = false; } /** Set plot title. @param title The title. */ public void setTitle(String title) { m_title = title; } private boolean m_autoXMax = true; private boolean m_autoXMin = true; private boolean m_autoXTics = true; private boolean m_autoYMin = true; private boolean m_autoYMax = true; private boolean m_autoYTics = true; private boolean m_noLines = false; private boolean m_onlyBoolValues; private boolean m_onlyIntValuesX; private boolean m_onlyIntValuesY; private boolean m_plotYZero = true; private boolean m_useSolidLineInterval = false; private boolean m_withBars; private boolean m_xLabelsBool; private int m_fontHeight; private int m_bottom; private int m_height; private final int m_imgHeight; private final int m_imgWidth; private int m_left; private final int m_precision; private int m_right; private int m_top; private int m_width; private int m_xLabelPerTic = 1; private double m_barWidth; private double m_minX; private double m_maxX; private double m_minY; private double m_maxY; private double m_solidLineInterval; private double m_xRange; private double m_xTics; private double m_xTicsMin; private double m_yRange; private double m_yTics; private double m_yTicsMin; private final Color m_color; private DecimalFormat m_formatX; private DecimalFormat m_formatY; private FontMetrics m_metrics; private Graphics2D m_graphics2D; private String m_title; private void drawBackground() { m_graphics2D.setColor(Color.decode("#e0e0e0")); m_graphics2D.fillRect(0, 0, m_imgWidth, m_imgHeight); m_graphics2D.setColor(Color.WHITE); m_graphics2D.fillRect(m_left, m_top, m_width, m_height); m_graphics2D.setColor(Color.BLACK); if (m_title != null) { int width = m_metrics.stringWidth(m_title) + 10; int height = (int)(m_fontHeight * 1.4); int x = m_left + (m_width - width) / 2; int y = (m_top - height) / 2; m_graphics2D.setColor(Color.decode("#ffffe1")); m_graphics2D.fillRect(x, y, width, height); m_graphics2D.setColor(Color.DARK_GRAY); m_graphics2D.drawRect(x, y, width, height); drawString(m_title, m_left + m_width / 2, m_top / 2); } } private void drawData(Table table, String columnX, String columnY, String errorColumn, boolean withBars) { m_graphics2D.setColor(m_color); Point last = null; int barWidthPixels = getPoint(m_barWidth, 0).x - getPoint(0, 0).x - 2; for (int row = 0; row < table.getNumberRows(); ++row) { try { double x = table.getDouble(columnX, row); double y = table.getDouble(columnY, row); Point point = getPoint(x, y); if (withBars) { Point bottom = getPoint(x, 0); m_graphics2D.fillRect(point.x - barWidthPixels / 2 + 1, point.y, barWidthPixels, bottom.y - point.y); } else if (last != null && ! m_noLines) m_graphics2D.drawLine(last.x, last.y, point.x, point.y); if (errorColumn != null) { double err = table.getDouble(errorColumn, row); Point top = getPoint(x, y + err); Point bottom = getPoint(x, y - err); m_graphics2D.drawLine(top.x, top.y, bottom.x, bottom.y); } if (! withBars) m_graphics2D.fillRect(point.x - 1, point.y - 1, 3, 3); last = point; } catch (Table.InvalidElement e) { last = null; } catch (Table.InvalidLocation e) { last = null; } } } private void drawGrid() { Stroke oldStroke = m_graphics2D.getStroke(); Stroke dottedStroke = new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1f, new float[] {2f}, 0f); if (m_useSolidLineInterval) { double min = (int)(m_xTicsMin / m_solidLineInterval) * m_solidLineInterval; int n = 0; for (double x = min; x < m_maxX; x += m_solidLineInterval, ++n) { Point bottom = getPoint(x, m_minY); Point top = getPoint(x, m_maxY); if (n % 2 == 0) { m_graphics2D.setColor(Color.decode("#f0f0f0")); Point right = getPoint(x + m_solidLineInterval, m_maxY); m_graphics2D.fillRect(top.x, top.y, Math.min(right.x - top.x, m_right - top.x), bottom.y - top.y); } } } m_graphics2D.setStroke(dottedStroke); for (double x = m_xTicsMin; x < m_maxX; x += m_xTics) { Point bottom = getPoint(x, m_minY); Point top = getPoint(x, m_maxY); m_graphics2D.setColor(Color.LIGHT_GRAY); m_graphics2D.drawLine(top.x, top.y, bottom.x, bottom.y); } m_graphics2D.setStroke(oldStroke); m_graphics2D.setStroke(dottedStroke); for (double y = m_yTicsMin; y < m_maxY; y += m_yTics) { Point left = getPoint(m_minX, y); Point right = getPoint(m_maxX, y); m_graphics2D.setColor(Color.LIGHT_GRAY); m_graphics2D.drawLine(left.x, left.y, right.x, right.y); } m_graphics2D.setStroke(oldStroke); m_graphics2D.setColor(Color.GRAY); if (m_minX <= 0 && m_maxX >= 0) { Point top = getPoint(0, m_minY); Point bottom = getPoint(0, m_maxY); m_graphics2D.drawLine(top.x, top.y, bottom.x, bottom.y); } if (m_minY <= 0 && m_maxY >= 0) { Point left = getPoint(m_minX, 0); Point right = getPoint(m_maxX, 0); m_graphics2D.drawLine(left.x, left.y, right.x, right.y); } if (m_formatX == null) m_formatX = getFormat(m_onlyIntValuesX); if (m_formatY == null) m_formatY = getFormat(m_onlyIntValuesY); for (double x = m_xTicsMin; x < m_maxX; x += m_xLabelPerTic * m_xTics) { if (m_xLabelsBool && Math.round(x) != 0 && Math.round(x) != 1) continue; Point bottom = getPoint(x, m_minY); String label; label = m_formatX.format(x); m_graphics2D.setColor(Color.GRAY); m_graphics2D.drawLine(bottom.x, bottom.y, bottom.x, bottom.y + 3); m_graphics2D.setColor(Color.BLACK); drawString(label, bottom.x, m_bottom + (m_imgHeight - m_bottom) / 2); } for (double y = m_yTicsMin; y < m_maxY; y += m_yTics) { if (! m_plotYZero && Math.round(y) == 0) continue; Point point = getPoint(m_minX, y); String label; label = m_formatY.format(y); m_graphics2D.setColor(Color.GRAY); m_graphics2D.drawLine(point.x, point.y, point.x - 3, point.y); m_graphics2D.setColor(Color.BLACK); drawStringRightAlign(label, m_left - 5, point.y); } m_graphics2D.setColor(Color.LIGHT_GRAY); m_graphics2D.drawRect(m_left, m_top, m_width, m_height); m_graphics2D.setColor(Color.GRAY); m_graphics2D.drawLine(m_left, m_top, m_left, m_bottom); m_graphics2D.drawLine(m_left, m_bottom, m_right, m_bottom); m_graphics2D.setStroke(oldStroke); } private void drawString(String string, int x, int y) { FontMetrics metrics = m_graphics2D.getFontMetrics(); int width = metrics.stringWidth(string); int height = m_fontHeight; m_graphics2D.drawString(string, x - width / 2, y + height / 2); } private void drawStringRightAlign(String string, int x, int y) { FontMetrics metrics = m_graphics2D.getFontMetrics(); int width = metrics.stringWidth(string); int height = m_fontHeight; m_graphics2D.drawString(string, x - width, y + height / 2); } private DecimalFormat getFormat(boolean onlyIntValues) { DecimalFormat format = new DecimalFormat(); format.setGroupingUsed(false); if (onlyIntValues) format.setMaximumFractionDigits(0); else format.setMaximumFractionDigits(m_precision); return format; } private Point getPoint(double x, double y) { int intX = (int)(m_left + (x - m_minX) / m_xRange * m_width); int intY = (int)(m_bottom - (y - m_minY) / m_yRange * m_height); return new Point(intX, intY); } /** Find tics interval. Tries to respect maxNumberTics, as long as there are at least two visible tics. */ private double getTics(double range, int maxNumberTics) { maxNumberTics = Math.max(maxNumberTics, 2); double maxTics = range / 2.1; // Make sure 2 tics are visible double tics = 1; if (range / maxNumberTics < 1) { while (range / (tics / 2) < maxNumberTics || tics > maxTics) { tics /= 2; if (range / (tics / 2) > maxNumberTics && tics < maxTics) break; tics /= 2; if (range / (tics / 2.5) > maxNumberTics && tics < maxTics) break; tics /= 2.5; } } else { while (range / tics > maxNumberTics && tics * 2 < maxTics) { tics *= 2; if (range / tics <= maxNumberTics || tics * 2.5 > maxTics) break; tics *= 2.5; if (range / tics <= maxNumberTics || tics * 2 > maxTics) break; tics *= 2; } } return tics; } private double getTicsMin(double tics, double min) { double result = (int)(min / tics) * tics; if (result < min) result += tics; return result; } private void initScale(Table table, String columnX, String columnY) { double minX = Double.MAX_VALUE; double maxX = -Double.MAX_VALUE; double minY = Double.MAX_VALUE; double maxY = -Double.MAX_VALUE; m_onlyBoolValues = true; m_onlyIntValuesX = true; m_onlyIntValuesY = true; for (int row = 0; row < table.getNumberRows(); ++row) { try { String xValue = table.get(columnX, row); String yValue = table.get(columnY, row); if (xValue == null || yValue == null || ! TableUtil.isNumberValue(yValue)) continue; if (! TableUtil.isBoolValue(yValue)) m_onlyBoolValues = false; if (! TableUtil.isIntValue(xValue)) m_onlyIntValuesX = false; if (! TableUtil.isIntValue(yValue)) m_onlyIntValuesY = false; double x = Double.parseDouble(xValue); double y = Double.parseDouble(yValue); minX = Math.min(minX, x); maxX = Math.max(maxX, x); minY = Math.min(minY, y); maxY = Math.max(maxY, y); } catch (Table.InvalidLocation e) { } } initScaleX(minX, maxX); initScaleY(minY, maxY); } private void initScaleX(double min, double max) { if (m_autoXMin) m_minX = min - 0.05 * (max - min); if (m_autoXMax) m_maxX = max + 0.05 * (max - m_minX); // Try to inlude 0 in plot if (m_minX > 0 && m_minX < 0.3 * m_maxX) m_minX = 0; // Avoid empty ranges if (m_maxX - m_minX < Double.MIN_VALUE) { m_minX -= 1.1; m_maxX += 1.1; } m_xRange = m_maxX - m_minX; if (m_autoXTics) { double absMax = Math.max(Math.abs(m_minX), Math.abs(m_maxX)); final double log10 = Math.log(10); int maxLength = (int)(Math.log(absMax) / log10) + m_precision + 3; int maxPixels = (int)(maxLength * (0.7 * m_fontHeight)); int numberTics = m_width / maxPixels; m_xTics = getTics(m_xRange, numberTics); } if (m_onlyIntValuesX) m_xTics = Math.max(1, m_xTics); m_xTicsMin = getTicsMin(m_xTics, m_minX); } private void initScaleY(double min, double max) { if (m_autoYMin) { if (m_onlyBoolValues) m_minY = 0; else m_minY = min; } if (m_autoYMax) { if (m_onlyBoolValues) m_maxY = 1.1; else m_maxY = max + 0.05 * (max - m_minY); } // Try to inlude 0 in plot if (m_autoYMin && m_minY > 0 && m_minY < 0.3 * m_maxY) m_minY = 0; // Avoid empty ranges if (m_maxY - m_minY < Double.MIN_VALUE) { m_minY -= 1.1; m_maxY += 1.1; } m_yRange = m_maxY - m_minY; if (m_autoYTics) { if (m_onlyBoolValues) { m_yTics = 1; m_yTicsMin = 0; } else { int maxNumberTics = (int)(m_height / (1.5 * m_fontHeight)); m_yTics = getTics(m_yRange, maxNumberTics); if (m_onlyIntValuesY) m_yTics = Math.max(1, m_yTics); } } if (! m_onlyBoolValues) m_yTicsMin = getTicsMin(m_yTics, m_minY); } }