package prefuse.action.assignment;
import java.util.logging.Logger;
import prefuse.Constants;
import prefuse.data.tuple.TupleSet;
import prefuse.util.DataLib;
import prefuse.util.MathLib;
import prefuse.util.PrefuseLib;
import prefuse.visual.VisualItem;
/**
* <p>
* Assignment Action that assigns size values for a group of items based upon
* a data field. This action can be used to automatically vary item's on screen
* sizes proportionally to an underlying data value. Sizes can be assigned along
* a continuous scale, or can be binned into discrete size groups. Both 1D
* (length) and 2D (area) encodings are supported by this function.
* 2D is assumed by default; use the setIs2DArea method to change this.</p>
*
* <p>
* The size assignments for numerical data are continuous by default, but can
* be binned into a few discrete steps (see {@link #setBinCount(int)}).
* Quantitative data can also be sized on different numerical scales. The
* default scale is a linear scale (specified by
* {@link Constants#LINEAR_SCALE}), but logarithmic and square root scales can
* be used (specified by {@link Constants#LOG_SCALE} and
* {@link Constants#SQRT_SCALE} respectively. Finally, the scale can be broken
* into quantiles, reflecting the statistical distribution of the values rather
* than just the total data value range, using the
* {@link Constants#QUANTILE_SCALE} value. For the quantile scale to work, you
* also need to specify the number of bins to use (see
* {@link #setBinCount(int)}). This value will determine the number of
* quantiles that the data should be divided into.
* </p>
*
* <p>
* By default, the maximum size value is determined automatically from the
* data, faithfully representing the scale differences between data values.
* However, this can sometimes result in very large differences. For
* example, if the minimum data value is 1.0 and the largest is 200.0, the
* largest items will be 200 times larger than the smallest. While
* accurate, this may not result in the most readable display. To correct
* these cases, use the {@link #setMaximumSize(double)} method to manually
* set the range of allowed sizes. By default, the minimum size value is
* 1.0. This too can be changed using the {@link #setMinimumSize(double)}
* method.
* </p>
*
* @author <a href="http://jheer.org">jeffrey heer</a>
*/
public class DataSizeAction extends SizeAction {
protected static final double NO_SIZE = Double.NaN;
protected String m_dataField;
protected double m_minSize = 1;
protected double m_sizeRange;
protected int m_scale = Constants.LINEAR_SCALE;
protected int m_bins = Constants.CONTINUOUS;
protected boolean m_inferBounds = true;
protected boolean m_inferRange = true;
protected boolean m_is2DArea = true;
protected double[] m_dist;
protected int m_tempScale;
/**
* Create a new DataSizeAction.
* @param group the data group to process
* @param field the data field to base size assignments on
*/
public DataSizeAction(String group, String field) {
super(group, NO_SIZE);
m_dataField = field;
}
/**
* Create a new DataSizeAction.
* @param group the data group to process
* @param field the data field to base size assignments on
* @param bins the number of discrete size values to use
*/
public DataSizeAction(String group, String field, int bins) {
this(group, field, bins, Constants.LINEAR_SCALE);
}
/**
* Create a new DataSizeAction.
* @param group the data group to process
* @param field the data field to base size assignments on
* @param bins the number of discrete size values to use
* @param scale the scale type to use. One of
* {@link prefuse.Constants#LINEAR_SCALE},
* {@link prefuse.Constants#LOG_SCALE},
* {@link prefuse.Constants#SQRT_SCALE}, or
* {@link prefuse.Constants#QUANTILE_SCALE}. If a quantile scale is
* used, the number of bins must be greater than zero.
*/
public DataSizeAction(String group, String field, int bins, int scale) {
super(group, NO_SIZE);
m_dataField = field;
setScale(scale);
setBinCount(bins);
}
// ------------------------------------------------------------------------
/**
* Returns the data field used to encode size values.
* @return the data field that is mapped to size values
*/
public String getDataField() {
return m_dataField;
}
/**
* Set the data field used to encode size values.
* @param field the data field to map to size values
*/
public void setDataField(String field) {
m_dataField = field;
}
/**
* Returns the scale type used for encoding size values from the data.
* @return the scale type. One of
* {@link prefuse.Constants#LINEAR_SCALE},
* {@link prefuse.Constants#LOG_SCALE},
* {@link prefuse.Constants#SQRT_SCALE},
* {@link prefuse.Constants#QUANTILE_SCALE}.
*/
public int getScale() {
return m_scale;
}
/**
* Set the scale (linear, square root, or log) to use for encoding size
* values from the data.
* @param scale the scale type to use. This value should be one of
* {@link prefuse.Constants#LINEAR_SCALE},
* {@link prefuse.Constants#SQRT_SCALE},
* {@link prefuse.Constants#LOG_SCALE},
* {@link prefuse.Constants#QUANTILE_SCALE}.
* If {@link prefuse.Constants#QUANTILE_SCALE} is used, the number of
* bins to use must also be specified to a value greater than zero using
* the {@link #setBinCount(int)} method.
*/
public void setScale(int scale) {
if ( scale < 0 || scale >= Constants.SCALE_COUNT )
throw new IllegalArgumentException(
"Unrecognized scale value: "+scale);
m_scale = scale;
}
/**
* Returns the number of "bins" or distinct categories of sizes
* @return the number of bins.
*/
public int getBinCount() {
return m_bins;
}
/**
* Sets the number of "bins" or distinct categories of sizes
* @param count the number of bins to set. The value
* {@link Constants#CONTINUOUS} indicates not to use any binning. If the
* scale type set using the {@link #setScale(int)} method is
* {@link Constants#QUANTILE_SCALE}, the bin count <strong>must</strong>
* be greater than zero.
*/
public void setBinCount(int count) {
if ( m_scale == Constants.QUANTILE_SCALE && count <= 0 ) {
throw new IllegalArgumentException(
"The quantile scale can not be used without binning. " +
"Use a bin value greater than zero.");
}
m_bins = count;
}
/**
* Indicates if the size values set by this function represent 2D areas.
* That is, if the size is a 2D area or a 1D length. The size value will
* be scaled appropriately to facilitate better perception of size
* differences.
* @return true if this instance is configured for area sizes, false for
* length sizes.
* @see prefuse.util.PrefuseLib#getSize2D(double)
*/
public boolean is2DArea() {
return m_is2DArea;
}
/**
* Sets if the size values set by this function represent 2D areas.
* That is, if the size is a 2D area or a 1D length. The size value will
* be scaled appropriately to facilitate better perception of size
* differences.
* @param isArea true to configure this instance for area sizes, false for
* length sizes
* @see prefuse.util.PrefuseLib#getSize2D(double)
*/
public void setIs2DArea(boolean isArea) {
m_is2DArea = isArea;
}
/**
* Gets the size assigned to the lowest-valued data items, typically 1.0.
* @return the size for the lowest-valued data items
*/
public double getMinimumSize() {
return m_minSize;
}
/**
* Sets the size assigned to the lowest-valued data items. By default,
* this value is 1.0.
* @param size the new size for the lowest-valued data items
*/
public void setMinimumSize(double size) {
if ( Double.isInfinite(size) ||
Double.isNaN(size) ||
size <= 0 )
{
throw new IllegalArgumentException("Minimum size value must be a "
+ "finite number greater than zero.");
}
if ( m_inferRange ) {
m_sizeRange += m_minSize - size;
}
m_minSize = size;
}
/**
* Gets the maximum size value that will be assigned by this action. By
* default, the maximum size value is determined automatically from the
* data, faithfully representing the scale differences between data values.
* However, this can sometimes result in very large differences. For
* example, if the minimum data value is 1.0 and the largest is 200.0, the
* largest items will be 200 times larger than the smallest. While
* accurate, this may not result in the most readable display. To correct
* these cases, use the {@link #setMaximumSize(double)} method to manually
* set the range of allowed sizes.
* @return the current maximum size. For the returned value to accurately
* reflect the size range used by this action, either the action must
* have already been run (allowing the value to be automatically computed)
* or the maximum size must have been explicitly set.
*/
public double getMaximumSize() {
return m_minSize + m_sizeRange;
}
/**
* Set the maximum size value that will be assigned by this action. By
* default, the maximum size value is determined automatically from the
* data, faithfully representing the scale differences between data values.
* However, this can sometimes result in very large differences. For
* example, if the minimum data value is 1.0 and the largest is 200.0, the
* largest items will be 200 times larger than the smallest. While
* accurate, this may not result in the most readable display. To correct
* these cases, use the {@link #setMaximumSize(double)} method to manually
* set the range of allowed sizes.
* @param maxSize the maximum size to use. If this value is less than or
* equal to zero, infinite, or not a number (NaN) then the input value
* will be ignored and instead automatic inference of the size range
* will be performed.
*/
public void setMaximumSize(double maxSize) {
if ( Double.isInfinite(maxSize) ||
Double.isNaN(maxSize) ||
maxSize <= 0 )
{
m_inferRange = true;
} else {
m_inferRange = false;
m_sizeRange = maxSize-m_minSize;
}
}
/**
* This operation is not supported by the DataSizeAction type.
* Calling this method will result in a thrown exception.
* @see prefuse.action.assignment.SizeAction#setDefaultSize(double)
* @throws UnsupportedOperationException
*/
public void setDefaultSize(double defaultSize) {
throw new UnsupportedOperationException();
}
// ------------------------------------------------------------------------
/**
* @see prefuse.action.EncoderAction#setup()
*/
protected void setup() {
TupleSet ts = m_vis.getGroup(m_group);
// cache the scale value in case it gets changed due to error
m_tempScale = m_scale;
if ( m_inferBounds ) {
if ( m_scale == Constants.QUANTILE_SCALE && m_bins > 0 ) {
double[] values =
DataLib.toDoubleArray(ts.tuples(), m_dataField);
m_dist = MathLib.quantiles(m_bins, values);
} else {
// check for non-binned quantile scale error
if ( m_scale == Constants.QUANTILE_SCALE ) {
Logger.getLogger(getClass().getName()).warning(
"Can't use quantile scale with no binning. " +
"Defaulting to linear scale. Set the bin value " +
"greater than zero to use a quantile scale.");
m_scale = Constants.LINEAR_SCALE;
}
m_dist = new double[2];
m_dist[0]= DataLib.min(ts, m_dataField).getDouble(m_dataField);
m_dist[1]= DataLib.max(ts, m_dataField).getDouble(m_dataField);
}
if ( m_inferRange ) {
m_sizeRange = m_dist[m_dist.length-1]/m_dist[0] - m_minSize;
}
}
}
/**
* @see prefuse.action.EncoderAction#finish()
*/
protected void finish() {
// reset scale in case it needed to be changed due to errors
m_scale = m_tempScale;
}
/**
* @see prefuse.action.assignment.SizeAction#getSize(prefuse.visual.VisualItem)
*/
public double getSize(VisualItem item) {
// check for any cascaded rules first
double size = super.getSize(item);
if ( !Double.isNaN(size) ) {
return size;
}
// otherwise perform data-driven assignment
double v = item.getDouble(m_dataField);
double f = MathLib.interp(m_scale, v, m_dist);
if ( m_bins < 1 ) {
// continuous scale
v = m_minSize + f * m_sizeRange;
} else {
// binned sizes
int bin = f < 1.0 ? (int)(f*m_bins) : m_bins-1;
v = m_minSize + bin*(m_sizeRange/(m_bins-1));
}
// return the size value. if this action is configured to return
// 2-dimensional sizes (ie area rather than length) then the
// size value is appropriately scaled first
return m_is2DArea ? PrefuseLib.getSize2D(v) : v;
}
} // end of class DataSizeAction