/* Monte-Carlo simulation code for statistical physics Copyright (C) 2001-2004 Sylvain Reynal Département de Physique Ecole Nationale Supérieure de l'Electronique et de ses Applications (ENSEA) 6, avenue du Ponceau, F-95014 CERGY CEDEX et Laboratoire de Physique Théorique et Modélisation (LPTM) Université de Cergy-Pontoise - Site de Neuville F-95031 CERGY CEDEX Tel : 00 +33 130 736 245 Fax : 00 +33 130 736 667 e-mail : reynal@ensea.fr web page : http://www.ensea.fr/staff/reynal/ */ package fr.ensea.chart; import fr.ensea.Localizer; import JSci.awt.*; import java.awt.*; import java.awt.event.*; import java.util.*; import java.text.*; /** * An AWT-style plotter for Curve2D's, with double-buffering functionality. * Supports multiple curves and plot-styles, as well as axes, numbering and labelling. */ public class Plot2D extends DoubleBufferedCanvas implements UIConstants { DecoratedCurve2D[] curveArray = new DecoratedCurve2D[0]; // list of curves to be drawn /* --- axis and scales --- */ Point ptOrg = new Point(); // coordinates in pixels of the (0,0) point Point2D scale=new Point2D(); // scale factor along X- and Y- axis between model and screen coordinates Axis xAxis, yAxis; int leftMarginPx, bottomMarginPx, rightMarginPx, topMarginPx; // space around graph frame static final int DEFAULT_MARGIN=10; // in pixels int tickLengthPx = 10; // length of a tick mark, in pixels Dimension preferredSize; // component's pref size, in px. Rectangle paintCurvesClip; // clip set when painting curves only ; updated in dataChanged(); /* --- labels and colors --- */ String title, xAxisLabel; Color axesColor, gridColor; /* --- fonts --- */ /* package access from Axis */ FontMetrics fontMetrics; private int fontHeight; /** * Constructs a new graph with the default colors. */ public Plot2D() { this(Color.black, Color.white, Color.darkGray); } /** * Constructs a new graph with the given colors. */ public Plot2D(Color backGround, Color axesColor, Color gridColor) { setBackground(backGround); setForeground(Color.yellow); this.axesColor = axesColor; this.gridColor = gridColor; fontMetrics = Toolkit.getDefaultToolkit().getFontMetrics(new Font("SansSerif", Font.PLAIN, 10)); fontHeight = fontMetrics.getHeight(); bottomMarginPx = DEFAULT_MARGIN + fontHeight; topMarginPx = leftMarginPx = rightMarginPx = DEFAULT_MARGIN; xAxis=new Axis(Curve2D.X_AXIS); yAxis=new Axis(Curve2D.Y_AXIS); addComponentListener(new ComponentHandler()); addMouseListener(new MouseHandler()); } /////////////////////////////////////////////////////////////////////////////////////////// //// Curve management /////////////////////////////////////////////////////////////////////////////////////////// /** * Add the given curve to the list of curves */ public void addCurve(Curve2D c, String title, Color color, int plotStyle){ addDecoratedCurve(new DecoratedCurve2D(c, title, color, plotStyle)); } /** * Add the given curve to the list of curves */ public void addDecoratedCurve(DecoratedCurve2D c){ DecoratedCurve2D[] _curveArray = new DecoratedCurve2D[curveArray.length+1]; System.arraycopy(curveArray, 0, _curveArray, 0, curveArray.length); _curveArray[curveArray.length] = c; curveArray = _curveArray; dataChanged(); } public DecoratedCurve2D getDecoratedCurve(int idx){ return curveArray[idx]; } /** * Remove all the curves from this graph */ public void removeCurves(){ curveArray = new DecoratedCurve2D[0]; dataChanged(); } /** * Remove the last curve that was added to the graph */ public void removeLastCurve(){ if (curveArray.length==0) return; DecoratedCurve2D[] _curveArray = new DecoratedCurve2D[curveArray.length-1]; System.arraycopy(curveArray,0,_curveArray,0,_curveArray.length); curveArray = _curveArray; dataChanged(); } /** return the number of curves */ public int getCurvesCount(){ return curveArray.length; } /////////////////////////////////////////////////////////////////////////////////////////// //// Plot parameters /////////////////////////////////////////////////////////////////////////////////////////// /** * Set the title */ public void setTitle(String title){ this.title = title; if (title!=null) topMarginPx = fontHeight + DEFAULT_MARGIN; else topMarginPx = DEFAULT_MARGIN; dataChanged(); } /** * Returns the plot's title */ public String getTitle(){ return this.title; } /** * Set the X-axis label */ public void setXaxisLabel(String xAxisLabel){ this.xAxisLabel = xAxisLabel; if (xAxisLabel!=null) bottomMarginPx = 2 * fontHeight + 2 * DEFAULT_MARGIN; else bottomMarginPx = DEFAULT_MARGIN + fontHeight; dataChanged(); } /** * Returns the X-axis label * @return null if there's no x-axis label */ public String getXaxisLabel(){ return xAxisLabel; } public Axis xAxis(){ return this.xAxis; } public Axis yAxis(){ return this.yAxis; } /** * Set the color for the axes and the grid. */ public void setColors(Color axesColor, Color gridColor){ this.axesColor = axesColor; this.gridColor = gridColor; redraw(); } /** * Sets the plot style * @param curveIdx a curve index, or "-1" to change all curves at one. * @param style DOTS, LINES or STAIRS */ public void setPlotStyle(int curveIdx, int style){ if (curveIdx==-1){ for (int i=0; i Screen /////////////////////////////////////////////////////////////////////////////////////////// /** * Converts a data point to screen coordinates. */ public Point toScreenCoordinates(Point2D p) { return new Point((int)(ptOrg.x + scale.x * p.x), (int)(ptOrg.y - scale.y * p.y)); } /** * Converts a data point to screen coordinates. */ public Point toScreenCoordinates(double x, double y) { return new Point((int)(ptOrg.x + scale.x * x), (int)(ptOrg.y - scale.y * y)); } /** * Converts a screen point to data coordinates. */ public Point2D toModelCoordinates(Point p) { return new Point2D((p.x - ptOrg.x) / scale.x, (ptOrg.y - p.y) / scale.y); } /** * Converts a screen point to data coordinates. */ public Point2D toModelCoordinates(int x, int y) { return new Point2D((x - ptOrg.x) / scale.x, (ptOrg.y - y) / scale.y); } /** * Draws a line from p1 to p2, where the two given points are in model coordinates. */ private void drawLine(Graphics g, double p1x, double p1y, double p2x, double p2y){ Point pA = toScreenCoordinates(p1x, p1y); Point pB = toScreenCoordinates(p2x, p2y); g.drawLine(pA.x, pA.y, pB.x, pB.y); } /** * Draws a line from p1 to p2, where the two given points are in model coordinates. */ private void drawRect(Graphics g, double p1x, double p1y, double p2x, double p2y, boolean filled){ Point pA = toScreenCoordinates(p1x, p1y); Point pB = toScreenCoordinates(p2x, p2y); int x, y, w, h; if (pB.x > pA.x){ x = pA.x; w = pB.x - pA.x; } else { x = pB.x; w = pA.x - pB.x; } if (pB.y > pA.y){ y = pA.y; h = pB.y - pA.y; } else { y = pB.y; h = pA.y - pB.y; } if (filled) g.fillRect(x,y,w,h); else g.drawRect(x,y,w,h); } /** * Draws a line from p1 to p2, where the two given points are in model coordinates. */ private void drawDot(Graphics g, double p1x, double p1y){ Point pA = toScreenCoordinates(p1x, p1y); //g.drawLine(pA.x, pA.y, pA.x, pA.y); g.fillOval(pA.x-3, pA.y-3, 6,6); } /////////////////////////////////////////////////////////////////////////////////////////// //// Axis, ticks, margins and all that... /////////////////////////////////////////////////////////////////////////////////////////// /** * Invoke this method to signal that some curve's data have changed, e.g. implying that axis bounds * may have to be recomputed. */ public void dataChanged() { // possibly determine xAxis.min/Y and xAxis.max/Y from curves // + recompute "maxNumberingWidthPx" for each axis (string width in pixel of the largest label) xAxis.dataChanged(this); yAxis.dataChanged(this); // recompute horizontal margins from new extrema values (since they depend on the width of numbering labels) rightMarginPx = xAxis.maxNumberingWidthPx; // trailing part of rightmost x-axis numbering leftMarginPx = 12 + yAxis.maxNumberingWidthPx; int maxWidthOfYAxisLabels = 0; // maximum width in px of Y-axis labels (NOT numberings !) for (int i=0; i maxWidthOfYAxisLabels) maxWidthOfYAxisLabels = w; } leftMarginPx += (maxWidthOfYAxisLabels + 10); // recompute scaling factors and location of (0,0) point: scale.x = (getSize().width - leftMarginPx - rightMarginPx) / (xAxis.max - xAxis.min); scale.y = (getSize().height - bottomMarginPx - topMarginPx) / (yAxis.max - yAxis.min); ptOrg.x = (int)(- xAxis.min * scale.x + leftMarginPx); ptOrg.y = (int)(topMarginPx + yAxis.max * scale.y); // recompute xAxis.inc and yAxis.inc: xAxis.maxNumberingWidthPx=max width in pixels of x-axis labels // so xAxis.maxNumberingWidthPx/scale.x is the same width, yet in model coordinates // and we consider ? times this width to be the optimal distance b/w ticks: xAxis.inc = ceilMultiple125(xAxis.maxNumberingWidthPx * 2.0 / scale.x); yAxis.inc = ceilMultiple125(40.0/ scale.y); //System.out.println("xAxis.maxNumberingWidthPx="+xAxis.maxNumberingWidthPx+", scale.x="+scale.x+", xAxis.inc="+(xAxis.maxNumberingWidthPx * 2.0 / scale.x)+" -> "+xAxis.inc); // compute clip to frame area (used by paintCurves) this.paintCurvesClip = new Rectangle(toScreenCoordinates(xAxis.min, yAxis.max)); // upper left corner this.paintCurvesClip.width = (int)((xAxis.max-xAxis.min)*scale.x); this.paintCurvesClip.height = (int)((yAxis.max-yAxis.min)*scale.y); // repaint: super.redraw(); //System.out.println("========== Data changed ==========\n"+this.toString()); } /** * Ceils the given double to the lowest double (sub-)multiple of 1, 2 or 5, e.g. 0.12 -> 0.2 and 640 -> 1000. * This method is specially well suited for rounding x- or y-axis increments so that * they are multiple or 1, 2 or 5. * This works with negative number as well (same behaviour as Math.ceil(),except for the rounding to 1,2 or 5) * @return return the lowest multiple of 1, 2 or 5 that is greater than x */ private static double ceilMultiple125(double x) { if (Double.isNaN(x)) return 1.0; if (x==0) return x; double absx = Math.abs(x); // compute magnitude of integer part : int mag = (int)Math.floor(Math.log(absx)/Math.log(10)); // e.g. 12.45 -> 1 ; 900.1 -> 2 // and Math.pow(10,mag) yields : 12.45 -> 10 ; 900.1 -> 100 double nearestPowerOfTen = Math.pow(10,mag); // compute floored integer part : absx/10^mag lies within [1,10) ! int n = (int)Math.ceil(absx/nearestPowerOfTen); // following examples are for absx in [10,100), whereby nearestPowerOfTen = 10: // [10,20) -> 20 // [20,50) -> 50 // [50,100) -> 100 if (n < 1 || n > LOG_CEILED_125.length) { System.out.println("------------------------------------------------------------"); System.out.println("x="+x); System.out.println("absx="+absx); System.out.println("mag="+mag); System.out.println("nearestPowerOfTen="+nearestPowerOfTen); System.out.println("n="+n); return nearestPowerOfTen*10; } if (x>0) return (double)(LOG_CEILED_125[n-1]*nearestPowerOfTen); else return -(double)(LOG_FLOORED_125[n-1]*nearestPowerOfTen); } // 1 2 3 4 5 6 7 8 9 10 private final static int[] LOG_CEILED_125 = {1, 2, 5, 5, 5, 10, 10, 10, 10, 10}; // see ceilMultiple125 /** * Floors the given double to the highest double (sub-)multiple of 1, 2 or 5, e.g. 0.12 -> 0.1 and 640 -> 500. * This method is specially well suited for rounding x- or y-axis increments so that * they are multiple or 1, 2 or 5. * This works with negative number as well (same behaviour as Math.floor(),except for the rounding to 1,2 or 5) * @return return the highest multiple of 1, 2 or 5 that is less than x */ private static double floorMultiple125(double x) { double absx = Math.abs(x); // compute magnitude of integer part : int mag = (int)Math.floor(Math.log(absx)/Math.log(10)); // e.g. 12.45 -> 1 ; 900.1 -> 2 // and Math.pow(10,mag) yields : 12.45 -> 10 ; 900.1 -> 100 double nearestPowerOfTen = Math.pow(10,mag); // compute floored integer part : absx/10^mag lies within [1,10) ! int n = (int)Math.floor(absx/nearestPowerOfTen); // following examples are for absx in [10,100), whereby nearestPowerOfTen = 10: // [10,20) -> 10 // [20,50) -> 20 // [50,100) -> 50 if (x>0) return (double)(LOG_FLOORED_125[n-1]*nearestPowerOfTen); else return -(double)(LOG_CEILED_125[n-1]*nearestPowerOfTen); } private final static int[] LOG_FLOORED_125 = {1, 2, 2, 2, 5, 5, 5, 5, 5, 10}; // see floorMultiple125 /////////////////////////////////////////////////////////////////////////////////////////// //// Paint methods /////////////////////////////////////////////////////////////////////////////////////////// // paint into the offscreen buffer image: public void offscreenPaint(Graphics g){ paintGrid(g); paintCurves(g); paintFrame(g); } // paint curves private void paintCurves(Graphics g){ Rectangle oldClip=g.getClipBounds(); g.clipRect(paintCurvesClip.x, paintCurvesClip.y, paintCurvesClip.width, paintCurvesClip.height); // now paint curves: Point2D p1, p2; double xA,xB; for (int curveIdx=0; curveIdx < curveArray.length; curveIdx++){ if (!curveArray[curveIdx].isVisible()) continue; DecoratedCurve2D decorCurve = curveArray[curveIdx]; Curve2D curve = decorCurve.curve; g.setColor(decorCurve.getColor()); if (curve.getNbPoints()<2){ for (int i = 0; i < curve.getNbPoints(); i++) { p1 = curve.getPoint(i); drawDot(g,p1.x, p1.y); } continue; } switch (decorCurve.getPlotStyle()){ // ----------------------------------------------- case DecoratedCurve2D.LINES: p1 = curve.getPoint(0); for (int i = 1; i < curve.getNbPoints(); i++) { p2 = curve.getPoint(i); drawLine(g,p1.x, p1.y, p2.x, p2.y); p1 = p2; } break; // ----------------------------------------------- case DecoratedCurve2D.DOTS: for (int i = 0; i < curve.getNbPoints(); i++) { p1 = curve.getPoint(i); drawDot(g,p1.x, p1.y); } break; // ----------------------------------------------- case DecoratedCurve2D.DOTS_LINES: p1 = curve.getPoint(0); drawDot(g,p1.x, p1.y); for (int i = 1; i < curve.getNbPoints(); i++) { p2 = curve.getPoint(i); drawLine(g,p1.x, p1.y, p2.x, p2.y); drawDot(g,p2.x, p2.y); p1 = p2; } break; // ----------------------------------------------- case DecoratedCurve2D.STAIRS: p1 = curve.getPoint(0); for (int i = 1; i < curve.getNbPoints(); i++) { p2 = curve.getPoint(i); drawLine(g,p1.x, p1.y, p2.x, p1.y); drawLine(g,p2.x, p1.y, p2.x, p2.y); p1 = p2; } break; // ----------------------------------------------- case DecoratedCurve2D.BARS: // works well only if x values are ordered double yBaseline=0; // where to paint the bar's baseline ; y=0 is ok, except if it doesn't belong to the axis range ! if (yAxis.min > 0) yBaseline=yAxis.min; else if (yAxis.max < 0) yBaseline=yAxis.max; p1 = curve.getPoint(0); xA=p1.x; for (int i = 1; i < curve.getNbPoints(); i++) { p2 = curve.getPoint(i); xB=(p1.x+p2.x)/2.0; drawLine(g, xA, 0, xA, p1.y); drawLine(g, xA, p1.y, xB, p1.y); drawLine(g, xB, p1.y, xB, 0); p1 = p2; xA = xB; } drawLine(g, xA, 0, xA, p1.y); drawLine(g, xA, p1.y, p1.x, p1.y); drawLine(g, p1.x, p1.y, p1.x, 0); break; // ----------------------------------------------- case DecoratedCurve2D.BARS_FILLED: p1 = curve.getPoint(0); xA=p1.x; for (int i = 1; i < curve.getNbPoints(); i++) { p2 = curve.getPoint(i); xB=(p1.x+p2.x)/2.0; g.setColor(decorCurve.getColor()); drawRect(g, xA, 0, xB, p1.y,true); g.setColor(getBackground()); drawRect(g, xA, 0, xB, p1.y,false); p1 = p2; xA = xB; } g.setColor(decorCurve.getColor()); drawRect(g, xA, 0, p1.x, p1.y,true); g.setColor(getBackground()); drawRect(g, xA, 0, p1.x, p1.y,false); break; // ----------------------------------------------- case DecoratedCurve2D.SAMPLES: for (int i = 1; i < curve.getNbPoints(); i++) { p1 = curve.getPoint(i); drawLine(g, p1.x, 0, p1.x, p1.y); } break; default: } } g.setClip(oldClip); } // paint grid lines + axis numberings private void paintGrid(Graphics g){ String label; double tickLength; int strWidth; Point p; // draw vertical grid lines + x-axis numbering: tickLength = tickLengthPx / scale.y; for (double x=Math.ceil(xAxis.min/xAxis.inc) * xAxis.inc; x <= xAxis.max; x += xAxis.inc){ // grid: g.setColor(gridColor); drawLine(g, x, yAxis.min, x, yAxis.max); // in model coordinates ! // ticks: g.setColor(axesColor); drawLine(g, x, yAxis.min, x, yAxis.min+tickLength); drawLine(g, x, yAxis.max, x, yAxis.max-tickLength); // labels: label=String.valueOf(xAxis.numberingsFormatter.format(x)); strWidth = fontMetrics.stringWidth(label); p = toScreenCoordinates(x, yAxis.min); g.drawString(label, p.x - strWidth / 2, p.y + 5 + fontHeight); } // draw horizonal grid lines + y-axis numbering: tickLength = tickLengthPx / scale.x; for (double y=Math.ceil(yAxis.min/yAxis.inc) * yAxis.inc; y <= yAxis.max; y += yAxis.inc){ g.setColor(gridColor); drawLine(g, xAxis.min, y, xAxis.max, y); g.setColor(axesColor); drawLine(g, xAxis.min, y, xAxis.min+tickLength, y); drawLine(g, xAxis.max, y, xAxis.max-tickLength, y); // labels: label=String.valueOf(yAxis.numberingsFormatter.format(y)); strWidth = fontMetrics.stringWidth(label); p = toScreenCoordinates(xAxis.min, y); g.drawString(label, p.x - 10 - strWidth, p.y + fontHeight / 4); } } // paint frame, title and axis keys private void paintFrame(Graphics g){ String label; int strWidth; Point p; // draw frame p = toScreenCoordinates(xAxis.min, yAxis.max); g.setColor(axesColor); g.drawRect(p.x, p.y, (int)((xAxis.max-xAxis.min)*scale.x), (int)((yAxis.max-yAxis.min)*scale.y)); // draw title: if (title != null){ p = toScreenCoordinates((xAxis.min+xAxis.max)/2.0, yAxis.max); strWidth = fontMetrics.stringWidth(title); g.setColor(getForeground()); g.drawString(title, p.x-strWidth/2, p.y-8); } // draw x-axis key: if (xAxisLabel != null){ p = toScreenCoordinates((xAxis.min+xAxis.max)/2.0, yAxis.min); strWidth = fontMetrics.stringWidth(xAxisLabel); g.setColor(getForeground()); g.drawString(xAxisLabel, p.x - strWidth/2, p.y + 10 + 2 * fontHeight); } // draw y-axis keys: p = toScreenCoordinates(xAxis.min,(yAxis.max)); for (int i=0; i