package org.basex.gui.view.plot; import static org.basex.util.Token.*; import org.basex.data.Data; import org.basex.index.StatsType; import org.basex.index.Stats; import org.basex.util.Array; import org.basex.util.hash.TokenSet; /** * Axis component of the scatter plot visualization. * * @author BaseX Team 2005-12, BSD License * @author Lukas Kircher */ final class PlotAxis { /** Text length limit for text to number transformation. */ private static final int TEXTLENGTH = 11; /** Plot data reference. */ private final PlotData plotData; /** Tag reference to selected attribute. */ int attrID; /** Number of different categories for x attribute. */ int nrCats; /** Data type. */ StatsType type; /** Coordinates of items. */ double[] co = {}; /** First label to be drawn after minimum label. */ double startvalue; /** The first category for text data. */ byte[] firstCat; /** The last category for text data. */ byte[] lastCat; /** Number of captions to display. */ int nrCaptions; /** Step for axis caption. */ double actlCaptionStep; /** Calculated caption step, view size not considered for calculation. */ private double calculatedCaptionStep; /** Minimum value in case selected attribute is numerical. */ double min; /** Maximum value in case selected attribute is numerical. */ double max; /** Axis uses logarithmic scale. */ boolean log; /** True if attribute is a tag, false if attribute. */ private boolean tag; /** Ln of min. */ private double logMin; /** Ln of max. */ private double logMax; /** * Constructor. * @param data plot data reference */ PlotAxis(final PlotData data) { plotData = data; } /** * (Re)Initializes axis. */ private void initialize() { tag = false; type = StatsType.INTEGER; co = new double[0]; nrCats = -1; firstCat = EMPTY; lastCat = EMPTY; nrCaptions = 0; actlCaptionStep = -1; calculatedCaptionStep = -1; min = Integer.MIN_VALUE; max = Integer.MAX_VALUE; startvalue = 0; } /** * Called if the user has changed the caption of the axis. If a new * attribute was selected the positions of the plot items are recalculated. * @param attribute attribute selected by the user * @return true if new attribute was selected */ boolean setAxis(final String attribute) { if(attribute == null) return false; initialize(); byte[] b = token(attribute); tag = !contains(b, '@'); b = delete(b, '@'); final Data data = plotData.context.data(); attrID = (tag ? data.tagindex : data.atnindex).id(b); refreshAxis(); return true; } /** * Refreshes item list and coordinates if the selection has changed. So far * only numerical data is considered for plotting. If the selected attribute * is of kind TEXT, it is treated as INT. */ void refreshAxis() { final Data data = plotData.context.data(); final Stats key = tag ? data.tagindex.stat(attrID) : data.atnindex.stat(attrID); if(key == null) return; type = key.type; if(type == null) return; if(type == StatsType.CATEGORY) type = StatsType.TEXT; final int[] items = plotData.pres; if(items.length < 1) return; co = new double[items.length]; final byte[][] vals = new byte[items.length][]; for(int i = 0; i < items.length; ++i) { byte[] value = getValue(items[i]); if(type == StatsType.TEXT && value.length > TEXTLENGTH) { value = substring(value, 0, TEXTLENGTH); } vals[i] = lc(value); } if(type == StatsType.TEXT) textToNum(vals); else { minMax(vals); // calculations for axis labeling if(!log) prepareLinAxis(); // coordinates for TEXT already calculated in textToNum() for(int i = 0; i < vals.length; ++i) co[i] = calcPosition(vals[i]); } } /** * TEXT data is transformed to categories, meaning each unique string forms * a category. The text values of the items are first sorted. The coordinate * for an item is then calculated as the position of the text value of this * item in the sorted category set. * @param vals item values */ private void textToNum(final byte[][] vals) { // get sorted indexes for values final int[] tmpI = Array.createOrder(vals, false, true); final int vl = vals.length; int i = 0; // find first non-empty value // empty string is treated as non existing value -> coordinate = -1 while(i < vl && vals[i].length == 0) co[tmpI[i++]] = -1; // count number of unique values nrCats = new TokenSet(vals).size(); if(i > 0) --nrCats; // get first/last category for axis caption firstCat = i < vl ? vals[i] : EMPTY; lastCat = i < vl ? vals[vl - 1] : EMPTY; // number of current category/position of item value in ordered text set int p = 0; while(i < vl) { // next string category to be tested final byte[] b = vals[i]; // l: highest index in sorted array for value b int l = i; // determining highest index of value/category b while(l < vl && eq(vals[l], b)) ++l; // calculating positions for all items with value b in current category while(i < l) { // centering items if only a single category exists (.5d) final double d = nrCats != 1 ? 1.0d / (nrCats - 1) * p : .5d; co[tmpI[i++]] = d; } ++p; } } /** * Calculates the relative position of an item in the plot for a given value. * The position for a TEXT value of an item is calculated in * {@link #textToNum}. * @param value item value * @return relative x or y value of the item */ private double calcPosition(final byte[] value) { if(value.length == 0) { return -1; } double range = max - min; if(range == 0) return 0.5d; final double d = toDouble(value); if(!log) return 1 / range * (d - min); // calculate position on a logarithmic scale. to display negative // values on a logarithmic scale, three cases are to be distinguished: // 0. both extreme values are greater or equal 0. // 1. the minimum value is smaller 0, hence the axis is 'mirrored' at 0. // 2. both extreme values are smaller 0; axis is also 'mirrored' and // values above the max value are not displayed. range = logMax - logMin; // case 1 if(min < 0 && max >= 0) { // p is the portion of the range between minimum value and zero compared // to the range between zero and the maximum value. // (needed for mirroring, s.a.) final double p = 1 / (logMin + logMax) * logMin; if(d == 0) return p; if(d < 0) return 1.0d - (1 - p) - 1.0d / logMin * ln(d) * p; return p + 1.0d / logMax * ln(d) * (1 - p); } // case 2 and 0 return 1 / range * (ln(d) - logMin); } /** * Calculates relative coordinate for a given value. * @param value given value * @return relative coordinate */ double calcPosition(final double value) { return calcPosition(token(value)); } /** * Calculates base e logarithm for the given value. * @param d value * @return base e logarithm for d */ private static double ln(final double d) { return d == 0 ? 0 : Math.log1p(Math.abs(d)); } /** * Returns the value for the specified pre value. * @param pre pre value * @return item value */ byte[] getValue(final int pre) { final Data data = plotData.context.data(); final int limit = pre + data.size(pre, Data.ELEM); for(int p = pre; p < limit; ++p) { final int kind = data.kind(p); if((kind == Data.ELEM && tag || kind == Data.ATTR && !tag) && attrID == data.name(p)) return data.atom(p); } return EMPTY; } /** * Determines the extreme values of the current data set. * @param vals values of plotted nodes */ private void minMax(final byte[][] vals) { min = Integer.MAX_VALUE; max = Integer.MIN_VALUE; int i = -1; boolean b = false; while(++i < vals.length) { if(vals[i].length > 0) { b = true; final double d = toDouble(vals[i]); if(d < min) min = d; if(d > max) max = d; } } if(!b) { min = 0; max = 0; } if(log) { logMin = ln(min); logMax = ln(max); } } /** * Executes some calculations to support a dynamic axis labeling for a * linear scale. */ private void prepareLinAxis() { // range as driving force for following calculations, no matter if INT // or DBL ... whatsoever double range = Math.abs(max - min); if(range == 0) return; // small ranges between min and max value if(range < 1) { final double dec = 1.0d / range; double pow = (int) (Math.floor(Math.log10(dec) + .5d) + 1) * 2; final double fac = (int) Math.pow(10, pow); final double tmin = min * fac; final double tmax = max * fac; range = Math.abs(tmax - tmin); pow = range < 10 ? 0 : (int) Math.floor(Math.log10(range) + .5d) - 1; calculatedCaptionStep = (int) Math.pow(10, pow); calculatedCaptionStep /= fac; return; } final int pow = range < 10 ? 0 : (int) Math.floor(Math.log10(range) + .5d) - 1; calculatedCaptionStep = (int) Math.pow(10, pow); } /** * Calculates axis caption depending on view width / height. * @param space space of view axis available for captions */ void calcCaption(final int space) { if(type == StatsType.DOUBLE || type == StatsType.INTEGER) { final double range = Math.abs(max - min); if(range == 0) { nrCaptions = 1; return; } // labeling for logarithmic scale if(log) { startvalue = min; nrCaptions = 3; return; } // labeling for linear scale final boolean dbl = type == StatsType.DOUBLE; actlCaptionStep = calculatedCaptionStep; nrCaptions = (int) (range / actlCaptionStep) + 1; while(2 * nrCaptions * PlotView.CAPTIONWHITESPACE * 3 < space && (dbl || actlCaptionStep % 2 == 0)) { actlCaptionStep /= 2; nrCaptions = (int) (range / actlCaptionStep); } while(nrCaptions * PlotView.CAPTIONWHITESPACE * 3 > space) { actlCaptionStep *= 2; nrCaptions = (int) (range / actlCaptionStep); } // calculate first value to be drawn startvalue = min + actlCaptionStep - min % actlCaptionStep; if(startvalue - min < actlCaptionStep / 4) startvalue += actlCaptionStep; // type == TEXT / CAT } else { nrCaptions = space / (PlotView.CAPTIONWHITESPACE * 3); if(nrCaptions > nrCats) nrCaptions = nrCats; actlCaptionStep = 1.0d / (nrCaptions - 1); } } }