001: /*************************************************************************
002: * *
003: * 1) This source code file, in unmodified form, and compiled classes *
004: * derived from it can be used and distributed without restriction, *
005: * including for commercial use. (Attribution is not required *
006: * but is appreciated.) *
007: * *
008: * 2) Modified versions of this file can be made and distributed *
009: * provided: the modified versions are put into a Java package *
010: * different from the original package, edu.hws; modified *
011: * versions are distributed under the same terms as the original; *
012: * and the modifications are documented in comments. (Modification *
013: * here does not include simply making subclasses that belong to *
014: * a package other than edu.hws, which can be done without any *
015: * restriction.) *
016: * *
017: * David J. Eck *
018: * Department of Mathematics and Computer Science *
019: * Hobart and William Smith Colleges *
020: * Geneva, New York 14456, USA *
021: * Email: eck@hws.edu WWW: http://math.hws.edu/eck/ *
022: * *
023: *************************************************************************/package edu.hws.jcm.draw;
024:
025: import edu.hws.jcm.data.NumUtils;
026: import java.awt.*;
027:
028: /**
029: * A set of horizontal and vertical axes that look OK and
030: * have reasonable, labeled tick marks. The number and spacing of tick
031: * marks changes depending on the scale on the axes. (The heuristics
032: * for computing this could use some improvement.)
033: */
034: public class Axes extends Drawable {
035:
036: /**
037: * Creates axes with no names on the axes.
038: */
039: public Axes() {
040: this (null, null);
041: }
042:
043: /**
044: * Creates axes with given names on the axes.
045: *
046: * @param xlabel Label for x axis. If the value is null, no label is drawn.
047: * @param ylabel Label for y axis. If the value is null, no label is drawn.
048: */
049: public Axes(String xLabel, String yLabel) {
050: this .xLabel = xLabel;
051: this .yLabel = yLabel;
052: }
053:
054: /**
055: * A constant that can be used in the setYAxisPosition() method to indicate the placement of the y-axis.
056: * The axis is placed at the top of the CoordinateRect.
057: */
058: public static final int TOP = 0;
059:
060: /**
061: * A constant that can be used in the setYAxisPosition() method to indicate the placement of the y-axs.
062: * The axis is placed at the bottom of the CoordinateRect.
063: */
064: public static final int BOTTOM = 1;
065:
066: /**
067: * A constant that can be used in the setXAxisPosition() method to indicate the placement of the x-axis.
068: * The axis is placed at the left edge of the CoordinateRect.
069: */
070: public static final int LEFT = 2;
071:
072: /**
073: * A constant that can be used in the setXAxisPosition() method to indicate the placement of the x-axis.
074: * The axis is placed at the right edge of the CoordinateRect.
075: */
076: public static final int RIGHT = 3;
077:
078: /**
079: * A constant that can be used in the setXAxisPosition() and setYAxisPosition() methods to indicate the placement of the axes.
080: * The axis is placed in the center of the CoordinateRect.
081: */
082: public static final int CENTER = 4;
083:
084: /**
085: * A constant that can be used in the setXAxisPosition() and setYAxisPosition() methods to indicate the placement of the axes.
086: * The axis is placed at its true x- or y-position, if that lies within the range of values shown on the CoordinateRect.
087: * Otherwise, it is placed along an edge of the CoordinateRect. This is the default value for axis placement.
088: */
089: public static final int SMART = 5;
090:
091: private int xAxisPosition = SMART;
092: private int yAxisPosition = SMART;
093:
094: private Color axesColor = new Color(0, 0, 180);
095:
096: private Color lightAxesColor = new Color(180, 180, 255); // Used if real axis is outside the draw rect
097: private Color labelColor = Color.black;
098:
099: private String xLabel = null;
100: private String yLabel = null;
101:
102: //------------------ Methods for getting/setting properties ----------------
103:
104: /**
105: * Get the color that is used for drawing the axes, when they are drawn in their true position.
106: */
107: public Color getAxesColor() {
108: return axesColor;
109: }
110:
111: /**
112: * Set the color that is used for drawing the axes, when they are drawn in their true position.
113: * The default is blue.
114: */
115: public void setAxesColor(Color c) {
116: if (c != null && !c.equals(axesColor)) {
117: axesColor = c;
118: needsRedraw();
119: }
120: }
121:
122: /**
123: * Get the color that is used for drawing an axis, when it is drawn along an edge of the CoordinateRect
124: * instead of in its proper x- or y-position.
125: */
126: public Color getLightAxesColor() {
127: return lightAxesColor;
128: }
129:
130: /**
131: * Get the color that is used for drawing an axis, when it is drawn along an edge of the CoordinateRect
132: * instead of in its proper x- or y-position. The default is a light blue.
133: */
134: public void setLightAxesColor(Color c) {
135: if (c != null && !c.equals(lightAxesColor)) {
136: lightAxesColor = c;
137: needsRedraw();
138: }
139: }
140:
141: /**
142: * Get the color that is used for drawing the labels on the x- and y-axes.
143: */
144: public Color getLabelColor() {
145: return labelColor;
146: }
147:
148: /**
149: * Set the color that is used for drawing the labels (usually the names of the variables) on the x- and y-axes.
150: * The default is black.
151: */
152: public void setLabelColor(Color c) {
153: if (c != null && !c.equals(labelColor)) {
154: labelColor = c;
155: if (xLabel != null || yLabel != null)
156: needsRedraw();
157: }
158: }
159:
160: /**
161: * Get the positioning constant that tells where the x-axis is drawn. This can be LEFT, RIGHT, CENTER, or SMART.
162: */
163: public int getXAxisPosition() {
164: return xAxisPosition;
165: }
166:
167: /**
168: * Set the positioning constant that tells where the x-axis is drawn. This can be LEFT, RIGHT, CENTER, or SMART.
169: * The default is SMART.
170: */
171: public void setXAxisPosition(int pos) {
172: if ((pos == TOP || pos == BOTTOM || pos == CENTER || pos == SMART)
173: && pos != xAxisPosition) {
174: xAxisPosition = pos;
175: needsRedraw();
176: }
177: }
178:
179: /**
180: * Get the positioning constant that tells where the y-axis is drawn. This can be TOP, BOTTOM, CENTER, or SMART.
181: */
182: public int getYAxisPosition() {
183: return yAxisPosition;
184: }
185:
186: /**
187: * Set the positioning constant that tells where the y-axis is drawn. This can be TOP, BOTTOM, CENTER, or SMART.
188: * The default is SMART.
189: */
190: public void setYAxisPosition(int pos) {
191: if ((pos == LEFT || pos == RIGHT || pos == CENTER || pos == SMART)
192: && pos != yAxisPosition) {
193: yAxisPosition = pos;
194: needsRedraw();
195: }
196: }
197:
198: /**
199: * Get the label that appears on the x-axis. If the value is null, no label appears.
200: */
201: public String getXLabel() {
202: return xLabel;
203: }
204:
205: /**
206: * Set the label that appears on the x-axis. If the value is null, no label appears. This is the default.
207: */
208: public void setXLabel(String s) {
209: xLabel = s;
210: needsRedraw();
211: }
212:
213: /**
214: * Get the label that appears on the y-axis. If the value is null, no label appears.
215: */
216: public String getYLabel() {
217: return yLabel;
218: }
219:
220: /**
221: * Set the label that appears on the y-axis. If the value is null, no label appears. This is the default.
222: */
223: public void setYLabel(String s) {
224: yLabel = s;
225: needsRedraw();
226: }
227:
228: //--------------------------------------------------------------------------
229:
230: /**
231: * Draw the axes. This is not meant to be called directly.
232: *
233: */
234: public void draw(Graphics g, boolean coordsChanged) {
235: if (coords == null)
236: return;
237:
238: if (coordsChanged || xTicks == null
239: || !g.getFont().equals(font)) { // The second test forces a setup() when the
240: // Axes object has been reloaded after serialization.
241: // The third test accounts for the fact that the
242: // font might have changed since the last time
243: // a setup() was done.
244: font = g.getFont();
245: FontMetrics fm = g.getFontMetrics(font);
246: setup(fm, coords.getXmin(), coords.getXmax(), coords
247: .getYmin(), coords.getYmax(), coords.getLeft(),
248: coords.getTop(), coords.getWidth(), coords
249: .getHeight(), coords.getGap());
250: }
251: doDraw(g, coords.getXmin(), coords.getXmax(), coords.getYmin(),
252: coords.getYmax(), coords.getLeft(), coords.getTop(),
253: coords.getWidth(), coords.getHeight(), coords.getGap());
254: }
255:
256: private void doDraw(Graphics g, double xmin, double xmax,
257: double ymin, double ymax, int left, int top, int width,
258: int height, int gap) {
259: // Draw axes using data computed by setup(). The parameters come from the CoordinateRect.
260: if (xAxisPosition == SMART && (ymax < 0 || ymin > 0))
261: g.setColor(lightAxesColor);
262: else
263: g.setColor(axesColor);
264: g.drawLine(left + gap, xAxisPixelPosition, left + width - gap
265: - 1, xAxisPixelPosition);
266: for (int i = 0; i < xTicks.length; i++) {
267: int a = (xAxisPixelPosition - 2 < top) ? xAxisPixelPosition
268: : xAxisPixelPosition - 2;
269: int b = (xAxisPixelPosition + 2 >= top + height) ? xAxisPixelPosition
270: : xAxisPixelPosition + 2;
271: g.drawLine(xTicks[i], a, xTicks[i], b);
272: }
273: for (int i = 0; i < xTickLabels.length; i++)
274: g.drawString(xTickLabels[i], xTickLabelPos[i][0],
275: xTickLabelPos[i][1]);
276: if (yAxisPosition == SMART && (xmax < 0 || xmin > 0))
277: g.setColor(lightAxesColor);
278: else
279: g.setColor(axesColor);
280: g.drawLine(yAxisPixelPosition, top + gap, yAxisPixelPosition,
281: top + height - gap - 1);
282: for (int i = 0; i < yTicks.length; i++) {
283: int a = (yAxisPixelPosition - 2 < left) ? yAxisPixelPosition
284: : yAxisPixelPosition - 2;
285: int b = (yAxisPixelPosition + 2 >= left + width) ? yAxisPixelPosition
286: : yAxisPixelPosition + 2;
287: g.drawLine(a, yTicks[i], b, yTicks[i]);
288: }
289: for (int i = 0; i < yTickLabels.length; i++)
290: g.drawString(yTickLabels[i], yTickLabelPos[i][0],
291: yTickLabelPos[i][1]);
292: g.setColor(labelColor);
293: if (xLabel != null)
294: g.drawString(xLabel, xLabel_x, xLabel_y);
295: if (yLabel != null)
296: g.drawString(yLabel, yLabel_x, yLabel_y);
297: }
298:
299: private transient int[] xTicks; // Data for drawing axes
300: private transient int[] yTicks;
301: private transient String[] xTickLabels;
302: private transient String[] yTickLabels;
303: private transient int[][] xTickLabelPos;
304: private transient int[][] yTickLabelPos;
305: private transient int xAxisPixelPosition, yAxisPixelPosition;
306: private transient int xLabel_x, xLabel_y, yLabel_x, yLabel_y;
307: private transient Font font;
308: private transient int ascent, descent, digitWidth;
309:
310: void setup(FontMetrics fm, double xmin, double xmax, double ymin,
311: double ymax, int left, int top, int width, int height,
312: int gap) {
313: // Set up all data for drawing the axes.
314: digitWidth = fm.charWidth('0');
315: ascent = fm.getAscent();
316: descent = fm.getDescent();
317: switch (xAxisPosition) {
318: case TOP:
319: xAxisPixelPosition = top + gap;
320: break;
321: case BOTTOM:
322: xAxisPixelPosition = top + height - gap - 1;
323: break;
324: case CENTER:
325: xAxisPixelPosition = top + height / 2;
326: break;
327: case SMART:
328: if (ymax < 0)
329: xAxisPixelPosition = top + gap;
330: else if (ymin > 0)
331: xAxisPixelPosition = top + height - gap - 1;
332: else
333: xAxisPixelPosition = top
334: + gap
335: + (int) ((height - 2 * gap - 1) * ymax / (ymax - ymin));
336: break;
337: }
338: switch (yAxisPosition) {
339: case LEFT:
340: yAxisPixelPosition = left + gap;
341: break;
342: case BOTTOM:
343: yAxisPixelPosition = left + width - gap - 1;
344: break;
345: case CENTER:
346: yAxisPixelPosition = left + width / 2;
347: break;
348: case SMART:
349: if (xmax < 0)
350: yAxisPixelPosition = left + width - gap - 1;
351: else if (xmin > 0)
352: yAxisPixelPosition = left + gap;
353: else
354: yAxisPixelPosition = left
355: + gap
356: - (int) ((width - 2 * gap - 1) * xmin / (xmax - xmin));
357: break;
358: }
359: if (xLabel != null) {
360: int size = fm.stringWidth(xLabel);
361: if (left + width - gap - size <= yAxisPixelPosition)
362: xLabel_x = left + gap;
363: else
364: xLabel_x = left + width - gap - size;
365: if (xAxisPixelPosition + 3 + ascent + descent + gap >= top
366: + height)
367: xLabel_y = xAxisPixelPosition - 4;
368: else
369: xLabel_y = xAxisPixelPosition + 3 + ascent;
370: }
371: if (yLabel != null) {
372: int size = fm.stringWidth(yLabel);
373: if (yAxisPixelPosition + 3 + size + gap > left + width)
374: yLabel_x = yAxisPixelPosition - size - 3;
375: else
376: yLabel_x = yAxisPixelPosition + 3;
377: if (top + ascent + descent + gap > xAxisPixelPosition)
378: yLabel_y = top + height - gap - descent;
379: else
380: yLabel_y = top + ascent + gap;
381: }
382: double start = fudgeStart(
383: ((xmax - xmin) * (yAxisPixelPosition - (left + gap)))
384: / (width - 2 * gap) + xmin,
385: 0.05 * (xmax - xmin));
386: int labelCt = (width - 2 * gap) / (10 * digitWidth);
387: if (labelCt <= 2)
388: labelCt = 3;
389: else if (labelCt > 20)
390: labelCt = 20;
391: double interval = fudge((xmax - xmin) / labelCt);
392: for (double mul = 1.5; mul < 4; mul += 0.5) {
393: if (fm.stringWidth(NumUtils.realToString(interval + start))
394: + digitWidth > (interval / (xmax - xmin))
395: * (width - 2 * gap)) // overlapping labels
396: interval = fudge(mul * (xmax - xmin) / labelCt);
397: else
398: break;
399: }
400: double[] label = new double[50];
401: labelCt = 0;
402: double x = start + interval;
403: double limit = left + width;
404: if (xLabel != null
405: && left + width - gap - fm.stringWidth(xLabel) > yAxisPixelPosition) // avoid overlap with xLabel
406: limit -= fm.stringWidth(xLabel) + gap + digitWidth;
407: while (labelCt < 50 && x <= xmax) {
408: if (left + gap + (width - 2 * gap) * (x - xmin)
409: / (xmax - xmin)
410: + fm.stringWidth(NumUtils.realToString(x)) / 2 > limit)
411: break;
412: label[labelCt] = x;
413: labelCt++;
414: x += interval;
415: }
416: x = start - interval;
417: limit = left;
418: if (xLabel != null
419: && left + width - gap - fm.stringWidth(xLabel) <= yAxisPixelPosition) // avoid overlap with xLabel
420: limit += fm.stringWidth(xLabel) + digitWidth;
421: while (labelCt < 50 && x >= xmin) {
422: if (left + gap + (width - 2 * gap) * (x - xmin)
423: / (xmax - xmin)
424: - fm.stringWidth(NumUtils.realToString(x)) / 2 < limit)
425: break;
426: label[labelCt] = x;
427: labelCt++;
428: x -= interval;
429: }
430: xTicks = new int[labelCt];
431: xTickLabels = new String[labelCt];
432: xTickLabelPos = new int[labelCt][2];
433: for (int i = 0; i < labelCt; i++) {
434: xTicks[i] = (int) (left + gap + (width - 2 * gap)
435: * (label[i] - xmin) / (xmax - xmin));
436: xTickLabels[i] = NumUtils.realToString(label[i]);
437: xTickLabelPos[i][0] = xTicks[i]
438: - fm.stringWidth(xTickLabels[i]) / 2;
439: if (xAxisPixelPosition - 4 - ascent >= top)
440: xTickLabelPos[i][1] = xAxisPixelPosition - 4;
441: else
442: xTickLabelPos[i][1] = xAxisPixelPosition + 4 + ascent;
443: }
444:
445: start = fudgeStart(ymax
446: - ((ymax - ymin) * (xAxisPixelPosition - (top + gap)))
447: / (height - 2 * gap), 0.05 * (ymax - ymin));
448: labelCt = (height - 2 * gap) / (5 * (ascent + descent));
449: if (labelCt <= 2)
450: labelCt = 3;
451: else if (labelCt > 20)
452: labelCt = 20;
453: interval = fudge((ymax - ymin) / labelCt);
454: labelCt = 0;
455: double y = start + interval;
456: limit = top + 8 + gap;
457: if (yLabel != null
458: && top + gap + ascent + descent <= xAxisPixelPosition) // avoid overlap with yLabel
459: limit = top + gap + ascent + descent;
460: while (labelCt < 50 && y <= ymax) {
461: if (top + gap + (height - 2 * gap) * (ymax - y)
462: / (ymax - ymin) - ascent / 2 < limit)
463: break;
464: label[labelCt] = y;
465: labelCt++;
466: y += interval;
467: }
468: y = start - interval;
469: limit = top + height - gap - 8;
470: if (yLabel != null
471: && top + gap + ascent + descent > xAxisPixelPosition) // avoid overlap with yLabel
472: limit = top + height - gap - ascent - descent;
473: while (labelCt < 50 && y >= ymin) {
474: if (top + gap + (height - 2 * gap) * (ymax - y)
475: / (ymax - ymin) + ascent / 2 > limit)
476: break;
477: label[labelCt] = y;
478: labelCt++;
479: y -= interval;
480: }
481: yTicks = new int[labelCt];
482: yTickLabels = new String[labelCt];
483: yTickLabelPos = new int[labelCt][2];
484: int w = 0; // max width of tick mark
485: for (int i = 0; i < labelCt; i++) {
486: yTickLabels[i] = NumUtils.realToString(label[i]);
487: int s = fm.stringWidth(yTickLabels[i]);
488: if (s > w)
489: w = s;
490: }
491: for (int i = 0; i < labelCt; i++) {
492: yTicks[i] = (int) (top + gap + (height - 2 * gap)
493: * (ymax - label[i]) / (ymax - ymin));
494: yTickLabelPos[i][1] = yTicks[i] + ascent / 2;
495: if (yAxisPixelPosition - 4 - w < left)
496: yTickLabelPos[i][0] = yAxisPixelPosition + 4;
497: else
498: yTickLabelPos[i][0] = yAxisPixelPosition - 4
499: - fm.stringWidth(yTickLabels[i]);
500: }
501: } // end setup()
502:
503: /**
504: * Translated directly from the Pascal version of xFunctions.
505: * Move x to a more "rounded" value; used for labeling axes.
506: *
507: * @param x the x coordinate used for labeling axes
508: * @return the rounded value of x
509: */
510: double fudge(double x) {
511: int i, digits;
512: double y;
513: if (Math.abs(x) < 0.0005 || Math.abs(x) > 500000)
514: return x;
515: else if (Math.abs(x) < 0.1 || Math.abs(x) > 5000) {
516: y = x;
517: digits = 0;
518: if (Math.abs(y) >= 1) {
519: while (Math.abs(y) >= 8.75) {
520: y = y / 10;
521: digits = digits + 1;
522: }
523: } else {
524: while (Math.abs(y) < 1) {
525: y = y * 10;
526: digits = digits - 1;
527: }
528: }
529: y = Math.round(y * 4) / 4;
530: if (digits > 0) {
531: for (int j = 0; j < digits; j++)
532: y = y * 10;
533: } else if (digits < 0) {
534: for (int j = 0; j < -digits; j++)
535: y = y / 10;
536: }
537: return y;
538: } else if (Math.abs(x) < 0.5)
539: return Math.round(10 * x) / 10.0;
540: else if (Math.abs(x) < 2.5)
541: return Math.round(2 * x) / 2.0;
542: else if (Math.abs(x) < 12)
543: return Math.round(x);
544: else if (Math.abs(x) < 120)
545: return Math.round(x / 10) * 10.0;
546: else if (Math.abs(x) < 1200)
547: return Math.round(x / 100) * 100.0;
548: else
549: return Math.round(x / 1000) * 1000.0;
550: }
551:
552: private double fudgeStart(double a, double diff) {
553: // Adapted from the Pascal version of xFunctions.
554: // Tries to find a "rounded value" within diff of a.
555: if (Math.abs(Math.round(a) - a) < diff)
556: return Math.round(a);
557: for (double x = 10; x <= 100000; x *= 10) {
558: double d = Math.round(a * x) / x;
559: if (Math.abs(d - a) < diff)
560: return d;
561: }
562: return a;
563: }
564:
565: } // end class Axes
|