package prefuse.action.layout; import java.awt.geom.Rectangle2D; import java.text.NumberFormat; import java.util.Iterator; import java.util.logging.Logger; import prefuse.Constants; import prefuse.data.Schema; import prefuse.data.query.ObjectRangeModel; import prefuse.data.tuple.TupleSet; import prefuse.data.util.Index; import prefuse.util.MathLib; import prefuse.util.PrefuseLib; import prefuse.util.ui.ValuedRangeModel; import prefuse.visual.VisualItem; import prefuse.visual.VisualTable; /** * Layout Action that positions axis grid lines and labels for a given * range model. * * @author <a href="http://jheer.org">jeffrey heer</a> */ public class AxisLabelLayout extends Layout { public static final String FRAC = "frac"; public static final String LABEL = "_label"; public static final String VALUE = "_value"; private AxisLayout m_layout; // pointer to matching layout, if any private ValuedRangeModel m_model; private double m_lo, m_hi, m_prevlo, m_prevhi; private NumberFormat m_nf = NumberFormat.getInstance(); private int m_axis; private boolean m_asc = true; private int m_scale = Constants.LINEAR_SCALE; private double m_spacing; // desired spacing between axis labels /** * Create a new AxisLabelLayout layout. * @param group the data group of the axis lines and labels * @param axis the axis type, either {@link prefuse.Constants#X_AXIS} * or {@link prefuse.Constants#Y_AXIS}. * @param values the range model that defines the span of the axis */ public AxisLabelLayout(String group, int axis, ValuedRangeModel values) { this(group, axis, values, null); } /** * Create a new AxisLabelLayout layout. * @param group the data group of the axis lines and labels * @param axis the axis type, either {@link prefuse.Constants#X_AXIS} * or {@link prefuse.Constants#Y_AXIS}. * @param values the range model that defines the span of the axis * @param bounds the layout bounds within which to place the axis marks */ public AxisLabelLayout(String group, int axis, ValuedRangeModel values, Rectangle2D bounds) { super(group); if ( bounds != null ) setLayoutBounds(bounds); m_model = values; m_axis = axis; m_spacing = 50; } /** * Create a new AxisLabelLayout layout. * @param group the data group of the axis lines and labels * @param layout an {@link AxisLayout} instance to model this layout after. * The axis type and range model of the provided instance will be used. */ public AxisLabelLayout(String group, AxisLayout layout) { this(group, layout, null, 50); } /** * Create a new AxisLabelLayout layout. * @param group the data group of the axis lines and labels * @param layout an {@link AxisLayout} instance to model this layout after. * The axis type and range model of the provided instance will be used. * @param bounds the layout bounds within which to place the axis marks */ public AxisLabelLayout(String group, AxisLayout layout, Rectangle2D bounds) { this(group, layout, bounds, 50); } /** * Create a new AxisLabelLayout layout. * @param group the data group of the axis lines and labels * @param layout an {@link AxisLayout} instance to model this layout after. * The axis type and range model of the provided instance will be used. * @param bounds the layout bounds within which to place the axis marks * @param spacing the minimum spacing between axis labels */ public AxisLabelLayout(String group, AxisLayout layout, Rectangle2D bounds, double spacing) { super(group); if ( bounds != null ) setLayoutBounds(bounds); m_layout = layout; m_model = layout.getRangeModel(); m_axis = layout.getAxis(); m_scale = layout.getScale(); m_spacing = spacing; } // ------------------------------------------------------------------------ /** * Get the formatter used to format labels for numerical values. * @return the <code>NumberFormat</code> used to format numerical labels. */ public NumberFormat getNumberFormat() { return m_nf; } /** * Set the formatter used to format labels for numerical values. * @param nf the <code>NumberFormat</code> used to format numerical labels. */ public void setNumberFormat(NumberFormat nf) { m_nf = nf; } /** * Get the required minimum spacing between axis labels. * @return the axis label spacing */ public double getSpacing() { return m_spacing; } /** * Set the required minimum spacing between axis labels. * @param spacing the axis label spacing to use */ public void setSpacing(double spacing) { m_spacing = spacing; } /** * Returns the scale type used for the axis. This setting only applies * for numerical data types (i.e., when axis values are from a * <code>NumberValuedRange</code>). * @return the scale type. One of * {@link prefuse.Constants#LINEAR_SCALE}, * {@link prefuse.Constants#SQRT_SCALE}, or * {@link Constants#LOG_SCALE}. */ public int getScale() { return m_scale; } /** * Sets the scale type used for the axis. This setting only applies * for numerical data types (i.e., when axis values are from a * <code>NumberValuedRange</code>). * @param scale the scale type. One of * {@link prefuse.Constants#LINEAR_SCALE}, * {@link prefuse.Constants#SQRT_SCALE}, or * {@link Constants#LOG_SCALE}. */ public void setScale(int scale) { if ( scale < 0 || scale >= Constants.SCALE_COUNT ) { throw new IllegalArgumentException( "Unrecognized scale type: "+scale); } m_scale = scale; } /** * Indicates if the axis values should be presented in ascending order * along the axis. * @return true if data values increase as pixel coordinates increase, * false if data values decrease as pixel coordinates increase. */ public boolean isAscending() { return m_asc; } /** * Sets if the axis values should be presented in ascending order * along the axis. * @param asc true if data values should increase as pixel coordinates * increase, false if data values should decrease as pixel coordinates * increase. */ public void setAscending(boolean asc) { m_asc = asc; } /** * Sets the range model used to layout this axis. * @param model the range model */ public void setRangeModel(ValuedRangeModel model) { m_model = model; } // ------------------------------------------------------------------------ /** * @see prefuse.action.GroupAction#run(double) */ public void run(double frac) { if ( m_model == null && m_layout != null ) m_model = m_layout.getRangeModel(); if ( m_model == null ) { Logger.getLogger(this.getClass().getName()) .warning("Axis labels missing a range model."); return; } VisualTable labels = getTable(); // check the axis label group to see if we can get a // more precise reading of the previous scale Double dfrac = (Double)labels.getClientProperty(FRAC); double fr = dfrac==null ? 1.0 : dfrac.doubleValue(); m_prevlo = m_prevlo + fr*(m_lo-m_prevlo); m_prevhi = m_prevhi + fr*(m_hi-m_prevhi); // now compute the layout if ( m_model instanceof ObjectRangeModel ) { // ordinal layout // get the current high and low values m_lo = m_model.getValue(); m_hi = m_lo + m_model.getExtent(); // compute the layout ordinalLayout(labels); } else { // numerical layout // get the current high and low values m_lo = ((Number)m_model.getLowValue()).doubleValue(); m_hi = ((Number)m_model.getHighValue()).doubleValue(); // compute the layout switch ( m_scale ) { case Constants.LOG_SCALE: logLayout(labels); break; case Constants.SQRT_SCALE: sqrtLayout(labels); break; case Constants.LINEAR_SCALE: default: linearLayout(labels); } } // get rid of any labels that are no longer being used garbageCollect(labels); } // ------------------------------------------------------------------------ // Quantitative Axis Layout /** * Calculates a quantitative, linearly scaled layout. */ protected void linearLayout(VisualTable labels) { Rectangle2D b = getLayoutBounds(); double breadth = getBreadth(b); double span = m_hi-m_lo; double pspan = m_prevhi-m_prevlo; double vlo = 0; if ( m_lo >= 0 ) { vlo = Math.pow(10, Math.floor(MathLib.log10(m_lo))); } else { vlo = -Math.pow(10, 1+Math.floor(MathLib.log10(-m_lo))); } //if ( vlo == 10 || vlo == 1 || vlo == 0.1 ) vlo = 0; // mark previously visible labels Iterator iter = labels.tuples(); while ( iter.hasNext() ) { VisualItem item = (VisualItem)iter.next(); reset(item); double v = item.getDouble(VALUE); double x = span==0 ? 0 : ((v-m_lo)/span)*breadth; set(item, x, b); } Index index = labels.index(VALUE); double step = getLinearStep(span, span==0 ? 0 : breadth/span); if ( step == 0 ) step = 1; int r; for ( double x, v=vlo; v<=m_hi; v+=step ) { x = ((v-m_lo)/span)*breadth; if ( x < -0.5 ) { continue; } else if ( (r=index.get(v)) >= 0 ) { VisualItem item = labels.getItem(r); item.setVisible(true); item.setEndVisible(true); } else { VisualItem item = labels.addItem(); item.set(LABEL, m_nf.format(v)); item.setDouble(VALUE, v); double f = pspan==0 ? 0 : ((v-m_prevlo)/pspan); if ( f <= 0 || f >= 1.0 ) item.setStartVisible(true); set(item, f*breadth, b); set(item, x, b); } } } /** * Calculates a quantitative, square root scaled layout. */ protected void sqrtLayout(VisualTable labels) { Rectangle2D b = getLayoutBounds(); double breadth = getBreadth(b); double span = m_hi-m_lo; double splo = MathLib.safeSqrt(m_prevlo); double spspan = MathLib.safeSqrt(m_prevhi)-splo; double vlo = Math.pow(10, Math.floor(MathLib.safeLog10(m_lo))); double slo = MathLib.safeSqrt(m_lo); double sspan = MathLib.safeSqrt(m_hi)-slo; // mark previously visible labels Iterator iter = labels.tuples(); while ( iter.hasNext() ) { VisualItem item = (VisualItem)iter.next(); reset(item); double v = item.getDouble(VALUE); double x = span==0 ? 0 : ((MathLib.safeSqrt(v)-slo)/sspan)*breadth; set(item, x, b); } Index index = labels.index(VALUE); double step = getLinearStep(span, breadth/span); if ( step == 0 ) step = 1; int r; for ( double x, v=vlo; v<=m_hi; v+=step ) { x = ((MathLib.safeSqrt(v)-slo)/sspan)*breadth; if ( x < -0.5 ) { continue; } else if ( (r=index.get(v)) >= 0 ) { VisualItem item = labels.getItem(r); item.setVisible(true); item.setEndVisible(true); } else { VisualItem item = labels.addItem(); item.set(LABEL, m_nf.format(v)); item.setDouble(VALUE, v); double f = spspan==0 ? 0 : ((MathLib.safeSqrt(v)-splo)/spspan); if ( f <= 0 || f >= 1.0 ) { item.setStartVisible(true); } set(item, f*breadth, b); set(item, x, b); } } } /** * Calculates a quantitative, logarithmically-scaled layout. * TODO: This method is currently not working correctly. */ protected void logLayout(VisualTable labels) { Rectangle2D b = getLayoutBounds(); double breadth = getBreadth(b); labels.clear(); // get span in log space // get log of the difference // if [0.1,1) round to .1's 0.1-->0.1 // if [1,10) round to 1's 1-->1 // if [10,100) round to 10's 10-->10 double llo = MathLib.safeLog10(m_lo); double lhi = MathLib.safeLog10(m_hi); double lspan = lhi - llo; double d = MathLib.log10(lhi-llo); int e = (int)Math.floor(d); int ilo = (int)Math.floor(llo); int ihi = (int)Math.ceil(lhi); double start = Math.pow(10,ilo); double end = Math.pow(10, ihi); double step = start * Math.pow(10, e); //System.out.println((hi-lo)+"\t"+e+"\t"+start+"\t"+end+"\t"+step); // TODO: catch infinity case if diff is zero // figure out label cases better for ( double val, v=start, i=0; v<=end; v+=step, ++i ) { val = MathLib.safeLog10(v); if ( i != 0 && Math.abs(val-Math.round(val)) < 0.0001 ) { i = 0; step = 10*step; } val = ((val-llo)/lspan)*breadth; if ( val < -0.5 ) continue; VisualItem item = labels.addItem(); set(item, val, b); String label = i==0 ? m_nf.format(v) : null; item.set(LABEL, label); item.setDouble(VALUE, v); } } /** * Get the "breadth" of a rectangle, based on the axis type. */ protected double getBreadth(Rectangle2D b) { switch ( m_axis ) { case Constants.X_AXIS: return b.getWidth(); default: return b.getHeight(); } } /** * Adjust a value according to the current scale type. */ protected double adjust(double v) { switch ( m_scale ) { case Constants.LOG_SCALE: return Math.pow(10,v); case Constants.SQRT_SCALE: return v*v; case Constants.LINEAR_SCALE: default: return v; } } /** * Compute a linear step between axis marks. */ protected double getLinearStep(double span, double scale) { double log10 = Math.log(span)/Math.log(10); double step = Math.pow(10, Math.floor(log10)); double delta = step * scale / m_spacing; if (delta > 20) { step /= 20; } else if (delta > 10) { step /= 10; } else if (delta > 5) { step /= 5; } else if (delta > 4) { step /= 4; } else if (delta > 2) { step /= 2; } else if (delta < 1) { step *= 2; } return step; } // ------------------------------------------------------------------------ // Ordinal Axis Layout /** * Compute an ordinal layout of axis marks. */ protected void ordinalLayout(VisualTable labels) { ObjectRangeModel model = (ObjectRangeModel)m_model; double span = m_hi-m_lo; double pspan = m_prevhi-m_prevlo; Rectangle2D b = getLayoutBounds(); double breadth = getBreadth(b); double scale = breadth/span; int step = getOrdinalStep(span, scale); if ( step <= 0 ) step = 1; // mark previously visible labels Iterator iter = labels.tuples(); while ( iter.hasNext() ) { VisualItem item = (VisualItem)iter.next(); reset(item); double v = item.getDouble(VALUE); double x = span==0 ? 0 : ((v-m_lo)/span)*breadth; set(item, x, b); } Index index = labels.index(VALUE); // handle remaining labels for ( int r, v=(int)m_lo; v<=m_hi; v+=step ) { if ( (r=index.get((double)v)) >= 0 ) { VisualItem item = labels.getItem(r); item.set(VisualItem.LABEL, model.getObject(v).toString()); item.setVisible(true); item.setEndVisible(true); } else { VisualItem item = labels.addItem(); item.set(VisualItem.LABEL, model.getObject(v).toString()); item.setDouble(VisualItem.VALUE, v); double f = pspan==0 ? 0 : ((v-m_prevlo)/pspan); if ( f <= 0 || f >= 1.0 ) item.setStartVisible(true); set(item, f*breadth, b); set(item, (v-m_lo)*breadth/span, b); } } } /** * Compute an ordinal step between axis marks. */ protected int getOrdinalStep(double span, double scale) { return (scale >= m_spacing ? 1 : (int)Math.ceil(m_spacing/scale)); } // ------------------------------------------------------------------------ // Auxiliary methods /** * Set the layout values for an axis label item. */ protected void set(VisualItem item, double xOrY, Rectangle2D b) { switch ( m_axis ) { case Constants.X_AXIS: xOrY = m_asc ? xOrY + b.getMinX() : b.getMaxX() - xOrY; PrefuseLib.updateDouble(item, VisualItem.X, xOrY); PrefuseLib.updateDouble(item, VisualItem.Y, b.getMinY()); PrefuseLib.updateDouble(item, VisualItem.X2, xOrY); PrefuseLib.updateDouble(item, VisualItem.Y2, b.getMaxY()); break; case Constants.Y_AXIS: xOrY = m_asc ? b.getMaxY() - xOrY - 1 : xOrY + b.getMinY(); PrefuseLib.updateDouble(item, VisualItem.X, b.getMinX()); PrefuseLib.updateDouble(item, VisualItem.Y, xOrY); PrefuseLib.updateDouble(item, VisualItem.X2, b.getMaxX()); PrefuseLib.updateDouble(item, VisualItem.Y2, xOrY); } } /** * Reset an axis label VisualItem */ protected void reset(VisualItem item) { item.setVisible(false); item.setEndVisible(false); item.setStartStrokeColor(item.getStrokeColor()); item.revertToDefault(VisualItem.STROKECOLOR); item.revertToDefault(VisualItem.ENDSTROKECOLOR); item.setStartTextColor(item.getTextColor()); item.revertToDefault(VisualItem.TEXTCOLOR); item.revertToDefault(VisualItem.ENDTEXTCOLOR); item.setStartFillColor(item.getFillColor()); item.revertToDefault(VisualItem.FILLCOLOR); item.revertToDefault(VisualItem.ENDFILLCOLOR); } /** * Remove axis labels no longer being used. */ protected void garbageCollect(VisualTable labels) { Iterator iter = labels.tuples(); while ( iter.hasNext() ) { VisualItem item = (VisualItem)iter.next(); if ( !item.isStartVisible() && !item.isEndVisible() ) { labels.removeTuple(item); } } } /** * Create a new table for representing axis labels. */ protected VisualTable getTable() { TupleSet ts = m_vis.getGroup(m_group); if ( ts == null ) { Schema s = PrefuseLib.getAxisLabelSchema(); VisualTable vt = m_vis.addTable(m_group, s); vt.index(VALUE); return vt; } else if ( ts instanceof VisualTable ) { return (VisualTable)ts; } else { throw new IllegalStateException( "Group already exists, not being used for labels"); } } } // end of class AxisLabels