001: package prefuse.action.layout;
002:
003: import java.awt.geom.Rectangle2D;
004: import java.text.NumberFormat;
005: import java.util.Iterator;
006: import java.util.logging.Logger;
007:
008: import prefuse.Constants;
009: import prefuse.data.Schema;
010: import prefuse.data.query.ObjectRangeModel;
011: import prefuse.data.tuple.TupleSet;
012: import prefuse.data.util.Index;
013: import prefuse.util.MathLib;
014: import prefuse.util.PrefuseLib;
015: import prefuse.util.ui.ValuedRangeModel;
016: import prefuse.visual.VisualItem;
017: import prefuse.visual.VisualTable;
018:
019: /**
020: * Layout Action that positions axis grid lines and labels for a given
021: * range model.
022: *
023: * @author <a href="http://jheer.org">jeffrey heer</a>
024: */
025: public class AxisLabelLayout extends Layout {
026:
027: public static final String FRAC = "frac";
028: public static final String LABEL = "_label";
029: public static final String VALUE = "_value";
030:
031: private AxisLayout m_layout; // pointer to matching layout, if any
032: private ValuedRangeModel m_model;
033: private double m_lo, m_hi, m_prevlo, m_prevhi;
034:
035: private NumberFormat m_nf = NumberFormat.getInstance();
036: private int m_axis;
037: private boolean m_asc = true;
038: private int m_scale = Constants.LINEAR_SCALE;
039:
040: private double m_spacing; // desired spacing between axis labels
041:
042: /**
043: * Create a new AxisLabelLayout layout.
044: * @param group the data group of the axis lines and labels
045: * @param axis the axis type, either {@link prefuse.Constants#X_AXIS}
046: * or {@link prefuse.Constants#Y_AXIS}.
047: * @param values the range model that defines the span of the axis
048: */
049: public AxisLabelLayout(String group, int axis,
050: ValuedRangeModel values) {
051: this (group, axis, values, null);
052: }
053:
054: /**
055: * Create a new AxisLabelLayout layout.
056: * @param group the data group of the axis lines and labels
057: * @param axis the axis type, either {@link prefuse.Constants#X_AXIS}
058: * or {@link prefuse.Constants#Y_AXIS}.
059: * @param values the range model that defines the span of the axis
060: * @param bounds the layout bounds within which to place the axis marks
061: */
062: public AxisLabelLayout(String group, int axis,
063: ValuedRangeModel values, Rectangle2D bounds) {
064: super (group);
065: if (bounds != null)
066: setLayoutBounds(bounds);
067: m_model = values;
068: m_axis = axis;
069: m_spacing = 50;
070: }
071:
072: /**
073: * Create a new AxisLabelLayout layout.
074: * @param group the data group of the axis lines and labels
075: * @param layout an {@link AxisLayout} instance to model this layout after.
076: * The axis type and range model of the provided instance will be used.
077: */
078: public AxisLabelLayout(String group, AxisLayout layout) {
079: this (group, layout, null, 50);
080: }
081:
082: /**
083: * Create a new AxisLabelLayout layout.
084: * @param group the data group of the axis lines and labels
085: * @param layout an {@link AxisLayout} instance to model this layout after.
086: * The axis type and range model of the provided instance will be used.
087: * @param bounds the layout bounds within which to place the axis marks
088: */
089: public AxisLabelLayout(String group, AxisLayout layout,
090: Rectangle2D bounds) {
091: this (group, layout, bounds, 50);
092: }
093:
094: /**
095: * Create a new AxisLabelLayout layout.
096: * @param group the data group of the axis lines and labels
097: * @param layout an {@link AxisLayout} instance to model this layout after.
098: * The axis type and range model of the provided instance will be used.
099: * @param bounds the layout bounds within which to place the axis marks
100: * @param spacing the minimum spacing between axis labels
101: */
102: public AxisLabelLayout(String group, AxisLayout layout,
103: Rectangle2D bounds, double spacing) {
104: super (group);
105: if (bounds != null)
106: setLayoutBounds(bounds);
107: m_layout = layout;
108: m_model = layout.getRangeModel();
109: m_axis = layout.getAxis();
110: m_scale = layout.getScale();
111: m_spacing = spacing;
112: }
113:
114: // ------------------------------------------------------------------------
115:
116: /**
117: * Get the formatter used to format labels for numerical values.
118: * @return the <code>NumberFormat</code> used to format numerical labels.
119: */
120: public NumberFormat getNumberFormat() {
121: return m_nf;
122: }
123:
124: /**
125: * Set the formatter used to format labels for numerical values.
126: * @param nf the <code>NumberFormat</code> used to format numerical labels.
127: */
128: public void setNumberFormat(NumberFormat nf) {
129: m_nf = nf;
130: }
131:
132: /**
133: * Get the required minimum spacing between axis labels.
134: * @return the axis label spacing
135: */
136: public double getSpacing() {
137: return m_spacing;
138: }
139:
140: /**
141: * Set the required minimum spacing between axis labels.
142: * @param spacing the axis label spacing to use
143: */
144: public void setSpacing(double spacing) {
145: m_spacing = spacing;
146: }
147:
148: /**
149: * Returns the scale type used for the axis. This setting only applies
150: * for numerical data types (i.e., when axis values are from a
151: * <code>NumberValuedRange</code>).
152: * @return the scale type. One of
153: * {@link prefuse.Constants#LINEAR_SCALE},
154: * {@link prefuse.Constants#SQRT_SCALE}, or
155: * {@link Constants#LOG_SCALE}.
156: */
157: public int getScale() {
158: return m_scale;
159: }
160:
161: /**
162: * Sets the scale type used for the axis. This setting only applies
163: * for numerical data types (i.e., when axis values are from a
164: * <code>NumberValuedRange</code>).
165: * @param scale the scale type. One of
166: * {@link prefuse.Constants#LINEAR_SCALE},
167: * {@link prefuse.Constants#SQRT_SCALE}, or
168: * {@link Constants#LOG_SCALE}.
169: */
170: public void setScale(int scale) {
171: if (scale < 0 || scale >= Constants.SCALE_COUNT) {
172: throw new IllegalArgumentException(
173: "Unrecognized scale type: " + scale);
174: }
175: m_scale = scale;
176: }
177:
178: /**
179: * Indicates if the axis values should be presented in ascending order
180: * along the axis.
181: * @return true if data values increase as pixel coordinates increase,
182: * false if data values decrease as pixel coordinates increase.
183: */
184: public boolean isAscending() {
185: return m_asc;
186: }
187:
188: /**
189: * Sets if the axis values should be presented in ascending order
190: * along the axis.
191: * @param asc true if data values should increase as pixel coordinates
192: * increase, false if data values should decrease as pixel coordinates
193: * increase.
194: */
195: public void setAscending(boolean asc) {
196: m_asc = asc;
197: }
198:
199: /**
200: * Sets the range model used to layout this axis.
201: * @param model the range model
202: */
203: public void setRangeModel(ValuedRangeModel model) {
204: m_model = model;
205: }
206:
207: // ------------------------------------------------------------------------
208:
209: /**
210: * @see prefuse.action.GroupAction#run(double)
211: */
212: public void run(double frac) {
213: if (m_model == null && m_layout != null)
214: m_model = m_layout.getRangeModel();
215:
216: if (m_model == null) {
217: Logger.getLogger(this .getClass().getName()).warning(
218: "Axis labels missing a range model.");
219: return;
220: }
221:
222: VisualTable labels = getTable();
223:
224: // check the axis label group to see if we can get a
225: // more precise reading of the previous scale
226: Double dfrac = (Double) labels.getClientProperty(FRAC);
227: double fr = dfrac == null ? 1.0 : dfrac.doubleValue();
228: m_prevlo = m_prevlo + fr * (m_lo - m_prevlo);
229: m_prevhi = m_prevhi + fr * (m_hi - m_prevhi);
230:
231: // now compute the layout
232: if (m_model instanceof ObjectRangeModel) { // ordinal layout
233: // get the current high and low values
234: m_lo = m_model.getValue();
235: m_hi = m_lo + m_model.getExtent();
236:
237: // compute the layout
238: ordinalLayout(labels);
239: } else { // numerical layout
240: // get the current high and low values
241: m_lo = ((Number) m_model.getLowValue()).doubleValue();
242: m_hi = ((Number) m_model.getHighValue()).doubleValue();
243:
244: // compute the layout
245: switch (m_scale) {
246: case Constants.LOG_SCALE:
247: logLayout(labels);
248: break;
249: case Constants.SQRT_SCALE:
250: sqrtLayout(labels);
251: break;
252: case Constants.LINEAR_SCALE:
253: default:
254: linearLayout(labels);
255: }
256: }
257:
258: // get rid of any labels that are no longer being used
259: garbageCollect(labels);
260: }
261:
262: // ------------------------------------------------------------------------
263: // Quantitative Axis Layout
264:
265: /**
266: * Calculates a quantitative, linearly scaled layout.
267: */
268: protected void linearLayout(VisualTable labels) {
269: Rectangle2D b = getLayoutBounds();
270: double breadth = getBreadth(b);
271:
272: double span = m_hi - m_lo;
273: double pspan = m_prevhi - m_prevlo;
274: double vlo = 0;
275: if (m_lo >= 0) {
276: vlo = Math.pow(10, Math.floor(MathLib.log10(m_lo)));
277: } else {
278: vlo = -Math.pow(10, 1 + Math.floor(MathLib.log10(-m_lo)));
279: }
280: //if ( vlo == 10 || vlo == 1 || vlo == 0.1 ) vlo = 0;
281:
282: // mark previously visible labels
283: Iterator iter = labels.tuples();
284: while (iter.hasNext()) {
285: VisualItem item = (VisualItem) iter.next();
286: reset(item);
287: double v = item.getDouble(VALUE);
288: double x = span == 0 ? 0 : ((v - m_lo) / span) * breadth;
289: set(item, x, b);
290: }
291:
292: Index index = labels.index(VALUE);
293: double step = getLinearStep(span, span == 0 ? 0 : breadth
294: / span);
295: if (step == 0)
296: step = 1;
297: int r;
298:
299: for (double x, v = vlo; v <= m_hi; v += step) {
300: x = ((v - m_lo) / span) * breadth;
301: if (x < -0.5) {
302: continue;
303: } else if ((r = index.get(v)) >= 0) {
304: VisualItem item = labels.getItem(r);
305: item.setVisible(true);
306: item.setEndVisible(true);
307: } else {
308: VisualItem item = labels.addItem();
309: item.set(LABEL, m_nf.format(v));
310: item.setDouble(VALUE, v);
311: double f = pspan == 0 ? 0 : ((v - m_prevlo) / pspan);
312: if (f <= 0 || f >= 1.0)
313: item.setStartVisible(true);
314: set(item, f * breadth, b);
315: set(item, x, b);
316: }
317: }
318: }
319:
320: /**
321: * Calculates a quantitative, square root scaled layout.
322: */
323: protected void sqrtLayout(VisualTable labels) {
324: Rectangle2D b = getLayoutBounds();
325: double breadth = getBreadth(b);
326:
327: double span = m_hi - m_lo;
328: double splo = MathLib.safeSqrt(m_prevlo);
329: double spspan = MathLib.safeSqrt(m_prevhi) - splo;
330: double vlo = Math.pow(10, Math.floor(MathLib.safeLog10(m_lo)));
331: double slo = MathLib.safeSqrt(m_lo);
332: double sspan = MathLib.safeSqrt(m_hi) - slo;
333:
334: // mark previously visible labels
335: Iterator iter = labels.tuples();
336: while (iter.hasNext()) {
337: VisualItem item = (VisualItem) iter.next();
338: reset(item);
339: double v = item.getDouble(VALUE);
340: double x = span == 0 ? 0
341: : ((MathLib.safeSqrt(v) - slo) / sspan) * breadth;
342: set(item, x, b);
343: }
344:
345: Index index = labels.index(VALUE);
346: double step = getLinearStep(span, breadth / span);
347: if (step == 0)
348: step = 1;
349: int r;
350: for (double x, v = vlo; v <= m_hi; v += step) {
351: x = ((MathLib.safeSqrt(v) - slo) / sspan) * breadth;
352: if (x < -0.5) {
353: continue;
354: } else if ((r = index.get(v)) >= 0) {
355: VisualItem item = labels.getItem(r);
356: item.setVisible(true);
357: item.setEndVisible(true);
358: } else {
359: VisualItem item = labels.addItem();
360: item.set(LABEL, m_nf.format(v));
361: item.setDouble(VALUE, v);
362: double f = spspan == 0 ? 0
363: : ((MathLib.safeSqrt(v) - splo) / spspan);
364: if (f <= 0 || f >= 1.0) {
365: item.setStartVisible(true);
366: }
367: set(item, f * breadth, b);
368: set(item, x, b);
369: }
370: }
371: }
372:
373: /**
374: * Calculates a quantitative, logarithmically-scaled layout.
375: * TODO: This method is currently not working correctly.
376: */
377: protected void logLayout(VisualTable labels) {
378: Rectangle2D b = getLayoutBounds();
379: double breadth = getBreadth(b);
380:
381: labels.clear();
382:
383: // get span in log space
384: // get log of the difference
385: // if [0.1,1) round to .1's 0.1-->0.1
386: // if [1,10) round to 1's 1-->1
387: // if [10,100) round to 10's 10-->10
388: double llo = MathLib.safeLog10(m_lo);
389: double lhi = MathLib.safeLog10(m_hi);
390: double lspan = lhi - llo;
391:
392: double d = MathLib.log10(lhi - llo);
393: int e = (int) Math.floor(d);
394: int ilo = (int) Math.floor(llo);
395: int ihi = (int) Math.ceil(lhi);
396:
397: double start = Math.pow(10, ilo);
398: double end = Math.pow(10, ihi);
399: double step = start * Math.pow(10, e);
400: //System.out.println((hi-lo)+"\t"+e+"\t"+start+"\t"+end+"\t"+step);
401:
402: // TODO: catch infinity case if diff is zero
403: // figure out label cases better
404: for (double val, v = start, i = 0; v <= end; v += step, ++i) {
405: val = MathLib.safeLog10(v);
406: if (i != 0 && Math.abs(val - Math.round(val)) < 0.0001) {
407: i = 0;
408: step = 10 * step;
409: }
410: val = ((val - llo) / lspan) * breadth;
411: if (val < -0.5)
412: continue;
413:
414: VisualItem item = labels.addItem();
415: set(item, val, b);
416: String label = i == 0 ? m_nf.format(v) : null;
417: item.set(LABEL, label);
418: item.setDouble(VALUE, v);
419: }
420: }
421:
422: /**
423: * Get the "breadth" of a rectangle, based on the axis type.
424: */
425: protected double getBreadth(Rectangle2D b) {
426: switch (m_axis) {
427: case Constants.X_AXIS:
428: return b.getWidth();
429: default:
430: return b.getHeight();
431: }
432: }
433:
434: /**
435: * Adjust a value according to the current scale type.
436: */
437: protected double adjust(double v) {
438: switch (m_scale) {
439: case Constants.LOG_SCALE:
440: return Math.pow(10, v);
441: case Constants.SQRT_SCALE:
442: return v * v;
443: case Constants.LINEAR_SCALE:
444: default:
445: return v;
446: }
447: }
448:
449: /**
450: * Compute a linear step between axis marks.
451: */
452: protected double getLinearStep(double span, double scale) {
453: double log10 = Math.log(span) / Math.log(10);
454: double step = Math.pow(10, Math.floor(log10));
455:
456: double delta = step * scale / m_spacing;
457: if (delta > 20) {
458: step /= 20;
459: } else if (delta > 10) {
460: step /= 10;
461: } else if (delta > 5) {
462: step /= 5;
463: } else if (delta > 4) {
464: step /= 4;
465: } else if (delta > 2) {
466: step /= 2;
467: } else if (delta < 1) {
468: step *= 2;
469: }
470: return step;
471: }
472:
473: // ------------------------------------------------------------------------
474: // Ordinal Axis Layout
475:
476: /**
477: * Compute an ordinal layout of axis marks.
478: */
479: protected void ordinalLayout(VisualTable labels) {
480: ObjectRangeModel model = (ObjectRangeModel) m_model;
481: double span = m_hi - m_lo;
482: double pspan = m_prevhi - m_prevlo;
483:
484: Rectangle2D b = getLayoutBounds();
485: double breadth = getBreadth(b);
486: double scale = breadth / span;
487: int step = getOrdinalStep(span, scale);
488: if (step <= 0)
489: step = 1;
490:
491: // mark previously visible labels
492: Iterator iter = labels.tuples();
493: while (iter.hasNext()) {
494: VisualItem item = (VisualItem) iter.next();
495: reset(item);
496: double v = item.getDouble(VALUE);
497: double x = span == 0 ? 0 : ((v - m_lo) / span) * breadth;
498: set(item, x, b);
499: }
500:
501: Index index = labels.index(VALUE);
502:
503: // handle remaining labels
504: for (int r, v = (int) m_lo; v <= m_hi; v += step) {
505: if ((r = index.get((double) v)) >= 0) {
506: VisualItem item = labels.getItem(r);
507: item.set(VisualItem.LABEL, model.getObject(v)
508: .toString());
509: item.setVisible(true);
510: item.setEndVisible(true);
511: } else {
512: VisualItem item = labels.addItem();
513: item.set(VisualItem.LABEL, model.getObject(v)
514: .toString());
515: item.setDouble(VisualItem.VALUE, v);
516: double f = pspan == 0 ? 0 : ((v - m_prevlo) / pspan);
517: if (f <= 0 || f >= 1.0)
518: item.setStartVisible(true);
519: set(item, f * breadth, b);
520: set(item, (v - m_lo) * breadth / span, b);
521: }
522: }
523: }
524:
525: /**
526: * Compute an ordinal step between axis marks.
527: */
528: protected int getOrdinalStep(double span, double scale) {
529: return (scale >= m_spacing ? 1 : (int) Math.ceil(m_spacing
530: / scale));
531: }
532:
533: // ------------------------------------------------------------------------
534: // Auxiliary methods
535:
536: /**
537: * Set the layout values for an axis label item.
538: */
539: protected void set(VisualItem item, double xOrY, Rectangle2D b) {
540: switch (m_axis) {
541: case Constants.X_AXIS:
542: xOrY = m_asc ? xOrY + b.getMinX() : b.getMaxX() - xOrY;
543: PrefuseLib.updateDouble(item, VisualItem.X, xOrY);
544: PrefuseLib.updateDouble(item, VisualItem.Y, b.getMinY());
545: PrefuseLib.updateDouble(item, VisualItem.X2, xOrY);
546: PrefuseLib.updateDouble(item, VisualItem.Y2, b.getMaxY());
547: break;
548: case Constants.Y_AXIS:
549: xOrY = m_asc ? b.getMaxY() - xOrY - 1 : xOrY + b.getMinY();
550: PrefuseLib.updateDouble(item, VisualItem.X, b.getMinX());
551: PrefuseLib.updateDouble(item, VisualItem.Y, xOrY);
552: PrefuseLib.updateDouble(item, VisualItem.X2, b.getMaxX());
553: PrefuseLib.updateDouble(item, VisualItem.Y2, xOrY);
554: }
555: }
556:
557: /**
558: * Reset an axis label VisualItem
559: */
560: protected void reset(VisualItem item) {
561: item.setVisible(false);
562: item.setEndVisible(false);
563: item.setStartStrokeColor(item.getStrokeColor());
564: item.revertToDefault(VisualItem.STROKECOLOR);
565: item.revertToDefault(VisualItem.ENDSTROKECOLOR);
566: item.setStartTextColor(item.getTextColor());
567: item.revertToDefault(VisualItem.TEXTCOLOR);
568: item.revertToDefault(VisualItem.ENDTEXTCOLOR);
569: item.setStartFillColor(item.getFillColor());
570: item.revertToDefault(VisualItem.FILLCOLOR);
571: item.revertToDefault(VisualItem.ENDFILLCOLOR);
572: }
573:
574: /**
575: * Remove axis labels no longer being used.
576: */
577: protected void garbageCollect(VisualTable labels) {
578: Iterator iter = labels.tuples();
579: while (iter.hasNext()) {
580: VisualItem item = (VisualItem) iter.next();
581: if (!item.isStartVisible() && !item.isEndVisible()) {
582: labels.removeTuple(item);
583: }
584: }
585: }
586:
587: /**
588: * Create a new table for representing axis labels.
589: */
590: protected VisualTable getTable() {
591: TupleSet ts = m_vis.getGroup(m_group);
592: if (ts == null) {
593: Schema s = PrefuseLib.getAxisLabelSchema();
594: VisualTable vt = m_vis.addTable(m_group, s);
595: vt.index(VALUE);
596: return vt;
597: } else if (ts instanceof VisualTable) {
598: return (VisualTable) ts;
599: } else {
600: throw new IllegalStateException(
601: "Group already exists, not being used for labels");
602: }
603: }
604:
605: } // end of class AxisLabels
|