com.actelion.research.table.view.JVisualization.java Source code

Java tutorial

Introduction

Here is the source code for com.actelion.research.table.view.JVisualization.java

Source

/*
 * Copyright 2014 Actelion Pharmaceuticals Ltd., Gewerbestrasse 16, CH-4123 Allschwil, Switzerland
 *
 * This file is part of DataWarrior.
 * 
 * DataWarrior is free software: you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 * 
 * DataWarrior is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along with DataWarrior.
 * If not, see http://www.gnu.org/licenses/.
 *
 * @author Thomas Sander
 */

package com.actelion.research.table.view;

import java.awt.Color;
import java.awt.Component;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Rectangle2D;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Random;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import org.apache.commons.math.MathException;
import org.apache.commons.math.stat.inference.TTestImpl;

import com.actelion.research.chem.StereoMolecule;
import com.actelion.research.chem.descriptor.DescriptorConstants;
import com.actelion.research.chem.io.CompoundTableConstants;
import com.actelion.research.gui.JProgressDialog;
import com.actelion.research.table.CompoundListSelectionModel;
import com.actelion.research.table.CompoundRecord;
import com.actelion.research.table.CompoundTableEvent;
import com.actelion.research.table.CompoundTableHitlistEvent;
import com.actelion.research.table.CompoundTableHitlistHandler;
import com.actelion.research.table.CompoundTableHitlistListener;
import com.actelion.research.table.CompoundTableListener;
import com.actelion.research.table.CompoundTableModel;
import com.actelion.research.table.DetailPopupProvider;
import com.actelion.research.table.HighlightListener;
import com.actelion.research.table.MarkerLabelDisplayer;
import com.actelion.research.table.view.graph.RadialVisualizationNode;
import com.actelion.research.table.view.graph.TreeVisualizationNode;
import com.actelion.research.table.view.graph.VisualizationNode;
import com.actelion.research.util.ByteArrayComparator;
import com.actelion.research.util.ColorHelper;
import com.actelion.research.util.DoubleFormat;

public abstract class JVisualization extends JComponent implements CompoundTableListener, FocusableView,
        HighlightListener, MarkerLabelDisplayer, ListSelectionListener, MouseListener, MouseMotionListener,
        Printable, CompoundTableHitlistListener, VisualizationColorListener {
    private static final long serialVersionUID = 0x20100610;

    // taken from class com.actelion.research.gui.form.FormObjectBorder
    public static final Color DEFAULT_TITLE_BACKGROUND = new Color(224, 224, 255);
    protected static final Rectangle2D.Float DEPICTOR_RECT = new Rectangle2D.Float(0, 0, 32000, 32000);

    public static final int cColumnUnassigned = -1;
    public static final int cConnectionColumnConnectAll = -2;
    public static final int cConnectionColumnMeanAndMedian = -3;
    public static final int cConnectionColumnConnectCases = -4;

    public static final int cChartTypeScatterPlot = 0;
    public static final int cChartTypeWhiskerPlot = 1;
    public static final int cChartTypeBoxPlot = 2;
    public static final int cChartTypeBars = 3;
    public static final int cChartTypePies = 4;

    public static final String[] CHART_TYPE_NAME = { "Scatter Plot", "Whisker Plot", "Box Plot", "Bar Chart",
            "Pie Chart" };
    public static final String[] CHART_TYPE_CODE = { "scatter", "whiskers", "boxes", "bars", "pies" };

    public static final int cChartModeCount = 0;
    public static final int cChartModePercent = 1;
    public static final int cChartModeMean = 2;
    public static final int cChartModeMin = 3;
    public static final int cChartModeMax = 4;
    public static final int cChartModeSum = 5;

    public static final String[] CHART_MODE_NAME = { "Row Count", "Row Percentage", "Mean Value", "Minimum Value",
            "Maximum Value", "Sum of Values" };
    public static final String[] CHART_MODE_CODE = { "count", "percent", "mean", "min", "max", "sum" };

    public static final String[] SCALE_MODE_CODE = { "false", "true", "y", "x" };
    public static final int cScaleModeShowAll = 0;
    public static final int cScaleModeHideAll = 1;
    public static final int cScaleModeHideY = 2;
    public static final int cScaleModeHideX = 3;

    public static final int cTreeViewModeNone = 0;
    public static final int cTreeViewModeHTree = 1;
    public static final int cTreeViewModeVTree = 2;
    public static final int cTreeViewModeRadial = 3;

    public static final String[] TREE_VIEW_MODE_NAME = { "<none>", "Horizontal Tree", "Vertical Tree",
            "Radial Graph" };
    public static final String[] TREE_VIEW_MODE_CODE = { "none", "hTree", "vTree", "radial" };

    public static final int cMaxChartCategoryCount = 256; // this is for one axis
    public static final int cMaxTotalChartCategoryCount = 32768; // this is the product of all axis
    private static final float cBarSpacingFactor = 1.08f;

    public static final int cMaxCaseSeparationCategoryCount = 64; // this is for one axis
    public static final int cMaxSplitViewCount = 10000;

    protected static final String[] CHART_MODE_AXIS_TEXT = CHART_MODE_CODE;

    public static final String[] BOXPLOT_MEAN_MODE_TEXT = { "No Indicator", "Median Line", "Mean Line",
            "Mean & Median Lines", "Mean & Median Triangles" };
    public static final String[] BOXPLOT_MEAN_MODE_CODE = { "none", "median", "mean", "both", "triangles" };
    public static final int BOXPLOT_DEFAULT_MEAN_MODE = 3;
    protected static final int cBoxplotMeanModeMedian = 1;
    protected static final int cBoxplotMeanModeMean = 2;
    protected static final int cBoxplotMeanModeLines = 3;
    protected static final int cBoxplotMeanModeTriangles = 4;

    protected static final int AXIS_TYPE_UNASSIGNED = 0;
    protected static final int AXIS_TYPE_TEXT_CATEGORY = 1;
    protected static final int AXIS_TYPE_DOUBLE_CATEGORY = 2;
    protected static final int AXIS_TYPE_DOUBLE_VALUE = 3;

    protected static final float SCALE_LIGHT = 0.4f;
    protected static final float SCALE_MEDIUM = 0.7f;
    protected static final float SCALE_STRONG = 1.0f;

    private static final byte EXCLUSION_FLAG_ZOOM_0 = 0x01;
    private static final byte EXCLUSION_FLAG_NAN_0 = 0x08;
    private static final byte EXCLUSION_FLAGS_NAN = 7 * EXCLUSION_FLAG_NAN_0;
    private static final byte EXCLUSION_FLAG_DETAIL_GRAPH = 0x40;
    private static final byte EXCLUSION_FLAG_OTHER_NAN = (byte) 0x80; // used if a column's NAN values affect visibility, but column not assigned to axis

    // This is an Apple only solution and needs to be adapted to support high-res displays of other vendors
    private static float sRetinaFactor = -1f;

    protected CompoundTableModel mTableModel;
    protected float[] mAxisVisMin, mAxisVisMax;
    protected float[][] mAxisSimilarity;
    protected int[] mAxisIndex, mLabelColumn, mSplittingColumn, mCategoryVisibleCount;
    protected boolean[] mIsCategoryAxis;
    protected CategoryViewInfo mChartInfo;
    protected VisualizationPoint[] mPoint;
    protected VisualizationPoint mHighlightedPoint, mActivePoint;
    protected JVisualizationLegend mColorLegend, mShapeLegend, mSizeLegend;
    protected VisualizationColor mMarkerColor;
    protected VisualizationSplitter mSplitter;
    protected VisualizationPoint[] mConnectionLinePoint;
    protected TreeMap<byte[], VisualizationPoint> mConnectionLineMap;
    protected VisualizationNode[][] mTreeNodeList;
    protected float mAbsoluteMarkerSize, mRelativeMarkerSize, mMarkerLabelSize, mMarkerJittering, mRelativeFontSize,
            mAbsoluteConnectionLineWidth, mRelativeConnectionLineWidth, mCaseSeparationValue,
            mMarkerSizeZoomAdaption, mSplittingAspectRatio;
    protected boolean mOffImageValid, mCoordinatesValid, mMouseIsDown, mTouchFunctionActive,
            mLocalAffectsGlobalExclusion, mAddingToSelection, mMarkerSizeInversion, mSuppressGrid,
            mSuspendGlobalExclusion, mShowNaNValues, mBoxplotShowMeanAndMedianValues, mBoxplotShowPValue,
            mBoxplotShowFoldChange, mLabelsInTreeViewOnly, mTreeViewShowAll, mTreeViewIsDynamic, mIsFastRendering,
            mMarkerSizeProportional, mShowEmptyInSplitView;
    protected int mDataPoints, mMarkerSizeColumn, mMarkerShapeColumn, mFontHeight, mBoxplotMeanMode, mMouseX1,
            mMouseY1, mMouseX2, mMouseY2, mDimensions, mConnectionColumn, mConnectionOrderColumn, mChartColumn,
            mChartMode, mChartType, mPreferredChartType, mPValueColumn, mTreeViewRadius, mFocusHitlist,
            mCaseSeparationColumn, mCaseSeparationCategoryCount, mScaleMode, mTreeViewMode, mActiveExclusionFlags,
            mHVCount, mHVExclusionTag, mVisibleCategoryExclusionTag;
    protected String mPValueRefCategory;
    protected Random mRandom;
    protected StereoMolecule mLabelMolecule;

    private CompoundListSelectionModel mSelectionModel;
    private int mLocalExclusionFlagNo, mPreviousLocalExclusionFlagNo;
    private float[] mPruningBarLow, mPruningBarHigh, mSimilarityMarkerSize;
    private int[][] mVisibleCategoryFromCategory;
    private int[] mCategoryMin, mCategoryMax, mCombinedCategoryCount;
    private Color mViewBackground, mTitleBackground;
    private boolean mLassoSelecting, mRectangleSelecting, mApplyLocalExclusionScheduled, mSplitViewCountExceeded;
    private Polygon mLassoRegion;
    private DetailPopupProvider mDetailPopupProvider;

    /**
     * Checks HiDPI support for Java 7 and newer.
     *
     * @return factor != 1 if HiDPI feature is enabled.
     */
    public static float getContentScaleFactor() {
        /* with Apple-Java-6 this was:
        Object sContentScaleFactorObject = Toolkit.getDefaultToolkit().getDesktopProperty("apple.awt.contentScaleFactor");
         private static final float sRetinaFactor = (sContentScaleFactorObject == null) ? 1f : ((Float)sContentScaleFactorObject).floatValue();
        */
        if (sRetinaFactor != -1f)
            return sRetinaFactor;

        sRetinaFactor = 1f;

        GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
        final GraphicsDevice device = env.getDefaultScreenDevice();

        try {
            Field field = device.getClass().getDeclaredField("scale");
            if (field != null) {
                field.setAccessible(true);
                Object scale = field.get(device);

                if (scale instanceof Integer) {
                    sRetinaFactor = (Integer) scale;
                }
            }
        } catch (Throwable e) {
        }

        return sRetinaFactor;
    }

    public JVisualization(CompoundTableModel tableModel, CompoundListSelectionModel selectionModel,
            int dimensions) {
        mTableModel = tableModel;
        mSelectionModel = selectionModel;
        mDimensions = dimensions;

        addMouseListener(this);
        addMouseMotionListener(this);

        tableModel.addHighlightListener(this);
        selectionModel.addListSelectionListener(this);

        mPruningBarLow = new float[mDimensions];
        mPruningBarHigh = new float[mDimensions];

        mAxisVisMin = new float[mDimensions];
        mAxisVisMax = new float[mDimensions];
        mAxisIndex = new int[mDimensions];
        mIsCategoryAxis = new boolean[mDimensions];
        mSplittingColumn = new int[mDimensions];
        mPreviousLocalExclusionFlagNo = -1;
        mLocalExclusionFlagNo = -1;
        mLocalAffectsGlobalExclusion = true; // default
        mSuspendGlobalExclusion = true;
        mMarkerSizeZoomAdaption = 1.0f;

        mMarkerColor = new VisualizationColor(mTableModel, this);

        mLabelColumn = new int[MarkerLabelDisplayer.cPositionCode.length];

        mPreferredChartType = cChartTypeScatterPlot;
        mChartMode = cChartModeCount;
        mChartColumn = cColumnUnassigned;

        mCategoryMin = new int[mDimensions];
        mCategoryMax = new int[mDimensions];
        mCategoryVisibleCount = new int[mDimensions];
        mAxisSimilarity = new float[mDimensions][];

        mRandom = new Random();
        setToolTipText(""); // to switch on tool-tips
    }

    protected void initialize() {
        mRelativeFontSize = 1.0f;
        mRelativeMarkerSize = 1.0f;
        mMarkerSizeInversion = false;
        mMarkerSizeProportional = false;
        mMarkerSizeColumn = cColumnUnassigned;
        mMarkerShapeColumn = cColumnUnassigned;
        mMarkerJittering = 0.0f;
        mConnectionColumn = cColumnUnassigned;
        mConnectionOrderColumn = cColumnUnassigned;
        mRelativeConnectionLineWidth = 1.0f;
        mSplittingColumn[0] = cColumnUnassigned;
        mSplittingColumn[1] = cColumnUnassigned;
        mSplittingAspectRatio = 1.0f;
        mCaseSeparationColumn = cColumnUnassigned;
        mCaseSeparationValue = 0.5f;
        mPValueColumn = cColumnUnassigned;
        mBoxplotMeanMode = BOXPLOT_DEFAULT_MEAN_MODE;
        mSplitter = null;
        mShowEmptyInSplitView = true;
        mVisibleCategoryExclusionTag = -1;
        mHVExclusionTag = -1;
        mHighlightedPoint = null;
        mActivePoint = null;
        mCoordinatesValid = false;
        mColorLegend = null;
        mShapeLegend = null;
        mSizeLegend = null;
        mSuppressGrid = false;
        mScaleMode = cScaleModeShowAll;
        mFocusHitlist = cHitlistUnassigned;
        mLabelsInTreeViewOnly = false;
        for (int i = 0; i < MarkerLabelDisplayer.cPositionCode.length; i++) {
            mLabelColumn[i] = cColumnUnassigned;
        }
        mMarkerLabelSize = 1.0f;
        for (int axis = 0; axis < mDimensions; axis++) {
            mAxisIndex[axis] = cColumnUnassigned;
            initializeAxis(axis);
        }
        mShowNaNValues = true;
        mTitleBackground = DEFAULT_TITLE_BACKGROUND;
        mTreeViewMode = cTreeViewModeNone;
        mTreeViewRadius = 5;
        mTreeViewShowAll = true;
        determineChartType();
    }

    @Override
    public CompoundTableModel getTableModel() {
        return mTableModel;
    }

    public String getAxisTitle(int column) {
        return mTableModel.getColumnTitleExtended(column);
    }

    /*   protected int getAxisType(int axis) {
          if (mAxisIndex[axis] == -1)
     return AXIS_TYPE_UNASSIGNED;
          return mIsCategoryAxis[axis] ? AXIS_TYPE_TEXT_CATEGORY : AXIS_TYPE_DOUBLE_VALUE;
        
          if (mAxisIndex[axis] != -1) {
     if (!mTableModel.isColumnTypeDouble(mAxisIndex[axis])
      && !mTableModel.isDescriptorColumn(mAxisIndex[axis]))
        return AXIS_TYPE_TEXT_CATEGORY;
     if ((mChartType == cChartTypeBars
       || mChartType == cChartTypePies
       || ((mChartType == cChartTypeBoxPlot
        || mChartType == cChartTypeWhiskerPlot)
        && axis != mChartInfo.barAxis)
       || (mChartType == cChartTypeScatterPlot
        && mCaseSeparationColumn != cColumnUnassigned))
       && mTableModel.isColumnTypeCategory(mAxisIndex[axis]))
        return AXIS_TYPE_DOUBLE_CATEGORY;
     else
        return AXIS_TYPE_DOUBLE_VALUE;
     }
          else if (mChartType == cChartTypeBars && mChartInfo.barAxis == axis) {
     return AXIS_TYPE_DOUBLE_VALUE;
     }
          return AXIS_TYPE_UNASSIGNED;
          }*/

    public void cleanup() {
        mTableModel.removeHighlightListener(this);
        mSelectionModel.removeListSelectionListener(this);
        if (mLocalExclusionFlagNo != -1)
            mTableModel.freeCompoundFlag(mLocalExclusionFlagNo);
        mLocalExclusionFlagNo = -1;
    }

    public int getDimensionCount() {
        return mDimensions;
    }

    public void initializeDataPoints() {
        mDataPoints = mTableModel.getTotalRowCount();

        mPoint = new VisualizationPoint[mDataPoints];
        for (int i = 0; i < mDataPoints; i++) {
            CompoundRecord record = mTableModel.getTotalRecord(i);
            mPoint[record.getID()] = createVisualizationPoint(record);
        }

        updateActiveRow();
    }

    protected void drawSelectionOutline(Graphics g) {
        g.setColor(VisualizationColor.cSelectedColor);
        if (mRectangleSelecting)
            g.drawRect((mMouseX1 < mMouseX2) ? mMouseX1 : mMouseX2, (mMouseY1 < mMouseY2) ? mMouseY1 : mMouseY2,
                    Math.abs(mMouseX2 - mMouseX1), Math.abs(mMouseY2 - mMouseY1));

        if (mLassoSelecting)
            g.drawPolygon(mLassoRegion);
    }

    protected abstract VisualizationPoint createVisualizationPoint(CompoundRecord record);

    public abstract int getAvailableShapeCount();

    public abstract int print(Graphics g, PageFormat f, int pageIndex);

    public abstract void paintHighResolution(Graphics g, Rectangle bounds, float fontScaling, boolean transparentBG,
            boolean isPrinting);

    // methods needed by JVisualizationLegend
    protected abstract int getStringWidth(String s);

    protected abstract void setFontHeight(int h);

    protected abstract void setColor(Color c);

    protected abstract void drawLine(int x1, int y1, int x2, int y2);

    protected abstract void drawRect(int x, int y, int w, int h);

    protected abstract void fillRect(int x, int y, int w, int h);

    protected abstract void drawMarker(Color color, int shape, int size, int x, int y);

    protected abstract void drawString(String s, int x, int y);

    protected abstract void drawMolecule(StereoMolecule mol, Color color, Rectangle2D.Float rect, int mode,
            int maxAVBL);

    protected void setActivePoint(VisualizationPoint vp) {
        mActivePoint = vp;

        if (mTreeViewMode != cTreeViewModeNone)
            updateTreeViewGraph();

        for (int axis = 0; axis < mDimensions; axis++)
            if (mAxisIndex[axis] != cColumnUnassigned && mTableModel.isDescriptorColumn(mAxisIndex[axis]))
                setSimilarityValues(axis, -1);

        updateSimilarityMarkerSizes(-1);

        repaint();
    }

    private void setHighlightedPoint(VisualizationPoint vp) {
        mHighlightedPoint = vp;
        repaint();
    }

    /**
     * Determines whether radial chart view is selected and if conditions are such
     * that a radial chart is shown when a current record is defined, i.e. we have
     * connection lines defined by both a referencing and a referenced column.
     * @return true radial chart view can be displayed
     */
    public int getTreeViewMode() {
        if (mTreeViewMode != cTreeViewModeNone && mConnectionColumn != cColumnUnassigned
                && mConnectionColumn != cConnectionColumnConnectAll
                && mTableModel.findColumn(mTableModel.getColumnProperty(mConnectionColumn,
                        CompoundTableConstants.cColumnPropertyReferencedColumn)) != -1)
            return mTreeViewMode;
        return cTreeViewModeNone;
    }

    public int getTreeViewRadius() {
        return mTreeViewRadius;
    }

    public void setTreeViewMode(int mode, int radius, boolean showAll, boolean isDynamic) {
        if (mTreeViewMode != mode || (mTreeViewMode != cTreeViewModeNone
                && (mTreeViewRadius != radius || mTreeViewShowAll != showAll || mTreeViewIsDynamic != isDynamic))) {
            mTreeViewMode = mode;
            mTreeViewRadius = radius;
            mTreeViewShowAll = showAll;
            mTreeViewIsDynamic = isDynamic;
            updateTreeViewGraph();
        }
    }

    public boolean isTreeViewDynamic() {
        return mTreeViewIsDynamic;
    }

    public boolean isTreeViewShowAll() {
        return mTreeViewShowAll;
    }

    public boolean isTreeViewModeEnabled() {
        return getTreeViewMode() != cTreeViewModeNone;
    }

    public boolean isMarkerLabelsInTreeViewOnly() {
        return mLabelsInTreeViewOnly;
    }

    /**
     * Determines whether currently a tree view is displayed, which includes
     * empty tree views, if no root is chosen and not all rows are shown.
     * @return
     */
    protected boolean isTreeViewGraph() {
        return mTreeViewMode != cTreeViewModeNone && (mActivePoint != null || !mTreeViewShowAll)
                && mConnectionColumn != cColumnUnassigned && mConnectionColumn != cConnectionColumnConnectAll
                && mTableModel.findColumn(mTableModel.getColumnProperty(mConnectionColumn,
                        CompoundTableConstants.cColumnPropertyReferencedColumn)) != -1;
    }

    protected int compareConnectionLinePoints(VisualizationPoint p1, VisualizationPoint p2) {
        if (p1.hvIndex != p2.hvIndex)
            return (p1.hvIndex < p2.hvIndex) ? -1 : 1;
        if (isCaseSeparationDone()) {
            float v1 = p1.record.getDouble(mCaseSeparationColumn);
            float v2 = p2.record.getDouble(mCaseSeparationColumn);
            if (v1 != v2)
                return (v1 < v2) ? -1 : 1;
        }
        if (mConnectionColumn > cColumnUnassigned) {
            float v1 = p1.record.getDouble(mConnectionColumn);
            float v2 = p2.record.getDouble(mConnectionColumn);
            if (v1 != v2)
                return (v1 < v2) ? -1 : 1;
        }
        int connectionOrderColumn = (mConnectionOrderColumn == cColumnUnassigned) ? mAxisIndex[0]
                : mConnectionOrderColumn;
        if (connectionOrderColumn != cColumnUnassigned) {
            float v1 = p1.record.getDouble(connectionOrderColumn);
            float v2 = p2.record.getDouble(connectionOrderColumn);
            if (v1 != v2)
                return (v1 < v2) ? -1 : 1;
        }
        return 0;
    }

    protected boolean isConnectionLinePossible(VisualizationPoint p1, VisualizationPoint p2) {
        if (p1.hvIndex != p2.hvIndex
                || (isCaseSeparationDone()
                        && p1.record.getDouble(mCaseSeparationColumn) != p2.record.getDouble(mCaseSeparationColumn))
                || (mConnectionColumn != JVisualization.cConnectionColumnConnectAll
                        && p1.record.getDouble(mConnectionColumn) != p2.record.getDouble(mConnectionColumn)))
            return false;
        return true;
    }

    protected int getNextChangedConnectionLinePointIndex(int index1) {
        int index2 = index1 + 1;

        while (index2 < mConnectionLinePoint.length
                && compareConnectionLinePoints(mConnectionLinePoint[index1], mConnectionLinePoint[index2]) == 0)
            index2++;

        return index2;
    }

    private void updateTreeViewGraph() {
        boolean oldWasNull = (mTreeNodeList == null);
        mTreeNodeList = null;

        if (isTreeViewGraph()) {
            int referencedColumn = mTableModel.findColumn(mTableModel.getColumnProperty(mConnectionColumn,
                    CompoundTableConstants.cColumnPropertyReferencedColumn));
            int strengthColumn = mTableModel.findColumn(mTableModel.getColumnProperty(mConnectionColumn,
                    CompoundTableConstants.cColumnPropertyReferenceStrengthColumn));
            float min = 0;
            float max = 0;
            float dif = 0;
            if (strengthColumn != -1) {
                min = mTableModel.getMinimumValue(strengthColumn);
                max = mTableModel.getMaximumValue(strengthColumn);
                if (max == min) {
                    strengthColumn = -1;
                } else {
                    min -= 0.2 * (max - min);
                    dif = max - min;
                }
            }

            if (mConnectionLineMap == null)
                mConnectionLineMap = createReferenceMap(mConnectionColumn, referencedColumn);

            for (int i = 0; i < mDataPoints; i++)
                mPoint[i].exclusionFlags |= EXCLUSION_FLAG_DETAIL_GRAPH;

            if (mActivePoint == null || (mTreeViewIsDynamic && !mTableModel.isVisible(mActivePoint.record))) {
                mTreeNodeList = new VisualizationNode[0][];
            } else {
                mActivePoint.exclusionFlags &= ~EXCLUSION_FLAG_DETAIL_GRAPH;

                VisualizationNode[] rootShell = new VisualizationNode[1];
                rootShell[0] = createVisualizationNode(mActivePoint, null, 1.0f);

                ArrayList<VisualizationNode[]> shellList = new ArrayList<VisualizationNode[]>();
                shellList.add(rootShell);

                // create array lists for every shell
                for (int shell = 1; shell <= mTreeViewRadius; shell++) {
                    ArrayList<VisualizationNode> vpList = new ArrayList<VisualizationNode>();
                    for (VisualizationNode parent : shellList.get(shell - 1)) {
                        byte[] data = (byte[]) parent.getVisualizationPoint().record.getData(mConnectionColumn);
                        if (data != null) {
                            String[] entry = mTableModel.separateEntries(new String(data));
                            String[] strength = null;
                            if (strengthColumn != cColumnUnassigned) {
                                byte[] strengthData = (byte[]) parent.getVisualizationPoint().record
                                        .getData(strengthColumn);
                                if (strengthData != null) {
                                    strength = mTableModel.separateEntries(new String(strengthData));
                                    if (strength.length != entry.length)
                                        strength = null;
                                }
                            }
                            int firstChildIndex = vpList.size();
                            for (int i = 0; i < entry.length; i++) {
                                String ref = entry[i];
                                VisualizationPoint vp = mConnectionLineMap.get(ref.getBytes());
                                if (vp != null && (!mTreeViewIsDynamic || mTableModel.isVisible(vp.record))) {
                                    // if we don't have connection strength information and the child is already connected to another parent
                                    if (strengthColumn == cColumnUnassigned
                                            && (vp.exclusionFlags & EXCLUSION_FLAG_DETAIL_GRAPH) == 0)
                                        continue;

                                    float strengthValue = 1.0f;
                                    if (strength != null) {
                                        try {
                                            float value = Math.min(max, Math.max(min,
                                                    mTableModel.tryParseEntry(strength[i], strengthColumn)));
                                            strengthValue = Float.isNaN(value) ? 0.0f
                                                    : (float) ((value - min) / dif);
                                        } catch (NumberFormatException nfe) {
                                        }
                                    }

                                    VisualizationNode childNode = null;

                                    // if we have a strength value and the child is already connected compare strength values
                                    if ((vp.exclusionFlags & EXCLUSION_FLAG_DETAIL_GRAPH) == 0) {
                                        for (int j = 0; j < firstChildIndex; j++) {
                                            if (vpList.get(j).getVisualizationPoint() == vp) {
                                                if (vpList.get(j).getStrength() < strengthValue) {
                                                    childNode = vpList.get(j);
                                                    vpList.remove(childNode);
                                                    firstChildIndex--;
                                                    childNode.setParentNode(parent);
                                                    childNode.setStrength(strengthValue);
                                                }
                                                break;
                                            }
                                        }
                                        if (childNode == null)
                                            continue;
                                    } else {
                                        vp.exclusionFlags &= ~EXCLUSION_FLAG_DETAIL_GRAPH;
                                        childNode = createVisualizationNode(vp, parent, strengthValue);
                                    }

                                    int insertIndex = firstChildIndex;
                                    while (insertIndex < vpList.size()
                                            && childNode.getStrength() <= vpList.get(insertIndex).getStrength())
                                        insertIndex++;

                                    vpList.add(insertIndex, childNode);
                                }
                            }
                        }
                    }

                    if (vpList.size() == 0)
                        break;

                    shellList.add(vpList.toArray(new VisualizationNode[0]));
                }

                mTreeNodeList = shellList.toArray(new VisualizationNode[0][]);
            }
        }

        if (!oldWasNull && mTreeNodeList == null) {
            for (int i = 0; i < mDataPoints; i++)
                mPoint[i].exclusionFlags &= ~EXCLUSION_FLAG_DETAIL_GRAPH;
        }

        if (!oldWasNull || mTreeNodeList != null) {
            // if we have a tree without any nodes (no chosen root) we don't want to hide invisible rows from other views
            mActiveExclusionFlags = (mTreeNodeList != null && mTreeNodeList.length == 0) ? 0
                    : EXCLUSION_FLAG_DETAIL_GRAPH;
            applyLocalExclusion(false);
            invalidateOffImage(true);
        }
    }

    private VisualizationNode createVisualizationNode(VisualizationPoint vp, VisualizationNode parent,
            float strength) {
        if (mTreeViewMode == cTreeViewModeHTree || mTreeViewMode == cTreeViewModeVTree)
            return new TreeVisualizationNode(vp, (TreeVisualizationNode) parent, strength);
        if (mTreeViewMode == cTreeViewModeRadial)
            return new RadialVisualizationNode(vp, (RadialVisualizationNode) parent, strength);
        return null;
    }

    protected void invalidateOffImage(boolean invalidateCoordinates) {
        if (invalidateCoordinates)
            mCoordinatesValid = false;
        mOffImageValid = false;
        repaint();
    }

    protected void calculateLegend(Rectangle bounds, int fontHeight) {
        if (mMarkerSizeColumn != cColumnUnassigned && mChartType != cChartTypeBars
                && mChartType != cChartTypePies) {
            mSizeLegend = new JVisualizationLegend(this, mTableModel, mMarkerSizeColumn, null,
                    JVisualizationLegend.cLegendTypeSize);
            mSizeLegend.calculate(bounds, fontHeight);
            bounds.height -= mSizeLegend.getHeight();
        } else {
            mSizeLegend = null;
        }

        if (mMarkerShapeColumn != cColumnUnassigned && mChartType != cChartTypeBars
                && mChartType != cChartTypePies) {
            mShapeLegend = new JVisualizationLegend(this, mTableModel, mMarkerShapeColumn, null,
                    JVisualizationLegend.cLegendTypeShapeCategory);
            mShapeLegend.calculate(bounds, fontHeight);
            bounds.height -= mShapeLegend.getHeight();
        } else {
            mShapeLegend = null;
        }

        if (mMarkerColor.getColorColumn() != cColumnUnassigned) {
            mColorLegend = new JVisualizationLegend(this, mTableModel, mMarkerColor.getColorColumn(), mMarkerColor,
                    mMarkerColor.getColorListMode() == VisualizationColor.cColorListModeCategories
                            ? JVisualizationLegend.cLegendTypeColorCategory
                            : JVisualizationLegend.cLegendTypeColorDouble);
            mColorLegend.calculate(bounds, fontHeight);
            bounds.height -= mColorLegend.getHeight();
        } else {
            mColorLegend = null;
        }
    }

    protected void paintLegend(Rectangle bounds) {
        if (mColorLegend != null)
            mColorLegend.paint(bounds);

        if (mSizeLegend != null)
            mSizeLegend.paint(bounds);

        if (mShapeLegend != null)
            mShapeLegend.paint(bounds);
    }

    protected void paintTouchIcon(Graphics g) {
        if (mTouchFunctionActive) {
            g.setColor(Color.red);
            //      g.setFont(Font.);
            g.drawString("touch", 10, 20);
        }
    }

    public void setDetailPopupProvider(DetailPopupProvider p) {
        mDetailPopupProvider = p;
    }

    /**
     * This returns the <b>indended</b> case separation column that was defined with
     * setCaseSeparation(). Whether the view really separates cases by applying a case
     * specific shift to markers, bars or boxes, depends on other view settings.
     * This shift is not applied, if<br>
     * - the categories are already separated, because the same column is also assigned to an axis.<br>
     * - none of the axes is unassigned or assigned to another category column with a low number of categories.<br>
     * Call isCaseSeparationDone() to find out, whether the view applies a case specific shift.
     * @return
     */
    public int getCaseSeparationColumn() {
        return mCaseSeparationColumn;
    }

    public float getCaseSeparationValue() {
        return mCaseSeparationValue;
    }

    /**
     * Checks, whether a case separation column was defined and the view applies
     * a case specific shift to all markers, bars or boxes.
     * Case separation is not done, if the case separation column is also assigned to an axis.
     * Case separation is not possible if we have no category axis or unassigned axis.
     * @return true, if the view applies a case specific shift to markers, bars or boxes
     */
    public boolean isCaseSeparationDone() {
        if (mCaseSeparationColumn == cColumnUnassigned)
            return false;

        for (int axis = 0; axis < mDimensions; axis++)
            if (mAxisIndex[axis] == mCaseSeparationColumn)
                return false; // don't separate cases, if we have them separated on one axis anyway

        for (int axis = 0; axis < mDimensions; axis++)
            if (mAxisIndex[axis] == cColumnUnassigned || (mTableModel.isColumnTypeCategory(mAxisIndex[axis])
                    && mTableModel.getCategoryCount(mAxisIndex[axis]) <= cMaxCaseSeparationCategoryCount))
                return true;

        return false;
    }

    protected int getCaseSeparationAxis() {
        if (!isCaseSeparationDone())
            return -1;

        int preferredAxis = -1;
        int preferredCategoryCount = Integer.MAX_VALUE;
        for (int axis = 0; axis < mDimensions; axis++) {
            if ((mChartType != cChartTypeBoxPlot && mChartType != cChartTypeWhiskerPlot)
                    || ((BoxPlotViewInfo) mChartInfo).barAxis != axis) {
                int column = mAxisIndex[axis];
                if (column == cColumnUnassigned) {
                    if (preferredCategoryCount > 1) {
                        preferredAxis = axis;
                        preferredCategoryCount = 1;
                    }
                } else if (mTableModel.isColumnTypeCategory(column)
                        && mTableModel.getCategoryCount(column) <= cMaxCaseSeparationCategoryCount
                        && preferredCategoryCount > mTableModel.getCategoryCount(column)) {
                    preferredAxis = axis;
                    preferredCategoryCount = mTableModel.getCategoryCount(column);
                }
            }
        }
        return preferredAxis;
    }

    @Override
    public int getFocusHitlist() {
        return mFocusHitlist;
    }

    protected int getFocusFlag() {
        return (mFocusHitlist == cHitlistUnassigned) ? -1
                : (mFocusHitlist == cFocusOnSelection) ? CompoundRecord.cFlagSelected
                        : mTableModel.getHitlistHandler().getHitlistFlagNo(mFocusHitlist);
    }

    public float getFontSize() {
        return mRelativeFontSize;
    }

    public int getMarkerShapeColumn() {
        return mMarkerShapeColumn;
    }

    public float getJittering() {
        return mMarkerJittering;
    }

    public float getMarkerSize() {
        return mRelativeMarkerSize;
    }

    public int getMarkerSizeColumn() {
        return mMarkerSizeColumn;
    }

    public boolean getMarkerSizeInversion() {
        return mMarkerSizeInversion;
    }

    public boolean getMarkerSizeProportional() {
        return mMarkerSizeProportional;
    }

    public boolean isMarkerSizeZoomAdapted() {
        return !Float.isNaN(mMarkerSizeZoomAdaption);
    }

    public float getMarkerLabelSize() {
        return mMarkerLabelSize;
    }

    /**
     * Calculates the font size for drawing a marker label. Usually this depends on
     * default font size, general font size factor, marker label font size factor.
     * However, we have a central label instead of a marker, and if the marker size column
     * is set, then the central label's font size is defined by the general marker label size setting.
     * @param vp
     * @param position cMidCenter or other
     * @return
     */
    protected float getLabelFontSize(VisualizationPoint vp, int position, boolean isTreeView) {
        if (mMarkerSizeColumn != cColumnUnassigned && position == cMidCenter && !isTreeView) {
            float fontsize = getMarkerSize(vp) / 2.0f;
            float factor = 0.5f * (mMarkerLabelSize + 1.0f); // reduce effect by factor 2.0
            return Math.max(3f, mRelativeFontSize * factor * fontsize);
        }

        return mMarkerLabelSize * mFontHeight;
    }

    /**
     * Calculates the average bond length used for molecule scaling, if a label
     * displays the chemical structure. This method is based on getLabelFontSize()
     * and, thus, used the same logic.
     * @param vp
     * @param position cMidCenter or other
     * @return
     */
    protected float getLabelAVBL(VisualizationPoint vp, int position, boolean isTreeView) {
        return getLabelFontSize(vp, position, isTreeView) / 2f;
    }

    /**
     * Calculates the absolute marker size of the visualization point, which depends
     * on a view size specific base value (mAbsoluteMarkerSize), a user changeable factor
     * (mRelativeMarkerSize) and optionally another factor derived from a column value
     * defined to influence the marker size.
     * @param vp
     * @return
     */
    protected float getMarkerSize(VisualizationPoint vp) {
        if (mMarkerSizeColumn == cColumnUnassigned) {
            float size = Float.isNaN(mMarkerSizeZoomAdaption) ? mAbsoluteMarkerSize
                    : mAbsoluteMarkerSize * mMarkerSizeZoomAdaption;
            return validateSizeWithConnections(size);
        }

        if (mTableModel.isDescriptorColumn(mMarkerSizeColumn)) {
            return getMarkerSizeFromDescriptorSimilarity(
                    mSimilarityMarkerSize == null ? 0.2f : mSimilarityMarkerSize[vp.record.getID()]);
        }

        if (CompoundTableHitlistHandler.isHitlistColumn(mMarkerSizeColumn))
            return getMarkerSizeFromHitlistMembership(vp.record.isFlagSet(mTableModel.getHitlistHandler()
                    .getHitlistFlagNo(CompoundTableHitlistHandler.getHitlistFromColumn(mMarkerSizeColumn))));

        return getMarkerSizeFromValue(vp.record.getDouble(mMarkerSizeColumn));
    }

    protected float getMarkerSizeFromHitlistMembership(boolean isMember) {
        float size = (isMember ^ mMarkerSizeInversion) ? mAbsoluteMarkerSize * 1.2f : mAbsoluteMarkerSize * 0.6f;
        if (!Float.isNaN(mMarkerSizeZoomAdaption))
            size *= mMarkerSizeZoomAdaption;
        return validateSizeWithConnections(size);
    }

    protected float getMarkerSizeFromDescriptorSimilarity(float similarity) {
        float size = 2f * mAbsoluteMarkerSize * similarity;
        if (!Float.isNaN(mMarkerSizeZoomAdaption))
            size *= mMarkerSizeZoomAdaption;
        return validateSizeWithConnections(size);
    }

    protected float getMarkerSizeFromValue(float value) {
        float factor = getMarkerSizeVPFactor(value, mMarkerSizeColumn);
        if (!Float.isNaN(mMarkerSizeZoomAdaption))
            factor *= mMarkerSizeZoomAdaption;
        float size = (int) (factor * mAbsoluteMarkerSize);
        return validateSizeWithConnections(size);
    }

    /**
     * Calculates the marker size factor from the absolute(!) row value normalized
     * by the maximum of all absolute values. Returned is 2*sqrt(value/max)) assuming that
     * the factor is used on two marker dimensions to make the marker area proportional
     * to the value.
     * @param value
     * @param invert
     * @param valueColumn
     * @return 0.0 -> 2.0
     */
    protected float getMarkerSizeVPFactor(float value, int valueColumn) {
        if (mMarkerSizeProportional) {
            float min = mTableModel.getMinimumValue(valueColumn);
            float max = mTableModel.getMaximumValue(valueColumn);
            if (mTableModel.isLogarithmicViewMode(valueColumn)) {
                max = (float) Math.pow(10, max);
                value = (float) Math.pow(10, value);
            } else {
                max = Math.max(Math.abs(min), Math.abs(max));
                value = Math.abs(value);
            }
            return 2f * (float) Math.sqrt((0.01 + (mMarkerSizeInversion ? max - value : value) / max) / 1.01f);
        } else {
            float min = mTableModel.getMinimumValue(valueColumn);
            float max = mTableModel.getMaximumValue(valueColumn);
            return 2f * (float) Math
                    .sqrt((0.04 + (mMarkerSizeInversion ? max - value : value - min) / (max - min)) / 1.04f);
        }
    }

    /**
     * Make marker size not smaller than 1.5 unless<br>
     * - marker sizes are not modulated by a column value<br>
     * - and connection lines are shown that are thicker than the marker<br>
     * This allows reduce marker size to 0, if connection lines are shown.
     * @param updated size
     * @return
     */
    private float validateSizeWithConnections(float size) {
        if (size < 1.5) {
            if (mMarkerSizeColumn != cColumnUnassigned || mConnectionColumn == cColumnUnassigned
                    || mAbsoluteConnectionLineWidth < size)
                size = 1.5f;
        }

        return size;
    }

    public void colorChanged(VisualizationColor source) {
        if (source == mMarkerColor) {
            updateColorIndices();
        }
    }

    protected boolean isLegendLayoutValid(JVisualizationLegend legend, VisualizationColor color) {
        if (legend == null || color.getColorColumn() == cColumnUnassigned)
            return legend == null && color.getColorColumn() == cColumnUnassigned;

        return legend.layoutIsValid(color.getColorListMode() == VisualizationColor.cColorListModeCategories,
                color.getColorListSizeWithoutDefaults());
    }

    public VisualizationColor getMarkerColor() {
        return mMarkerColor;
    }

    private void updateColorIndices() {
        if (mMarkerColor.getColorColumn() == cColumnUnassigned) {
            for (int i = 0; i < mDataPoints; i++)
                mPoint[i].colorIndex = VisualizationColor.cDefaultDataColorIndex;
        } else if (CompoundTableHitlistHandler.isHitlistColumn(mMarkerColor.getColorColumn())) {
            int hitlistIndex = CompoundTableHitlistHandler.getHitlistFromColumn(mMarkerColor.getColorColumn());
            int flagNo = mTableModel.getHitlistHandler().getHitlistFlagNo(hitlistIndex);
            for (int i = 0; i < mDataPoints; i++)
                mPoint[i].colorIndex = (byte) (mPoint[i].record.isFlagSet(flagNo)
                        ? VisualizationColor.cSpecialColorCount
                        : VisualizationColor.cSpecialColorCount + 1);
        } else if (mTableModel.isDescriptorColumn(mMarkerColor.getColorColumn())) {
            setSimilarityColors(-1);
        } else if (mMarkerColor.getColorListMode() == VisualizationColor.cColorListModeCategories) {
            for (int i = 0; i < mDataPoints; i++)
                mPoint[i].colorIndex = (byte) (VisualizationColor.cSpecialColorCount
                        + mTableModel.getCategoryIndex(mMarkerColor.getColorColumn(), mPoint[i].record));
        } else if (mTableModel.isColumnTypeDouble(mMarkerColor.getColorColumn())) {
            float min = Float.isNaN(mMarkerColor.getColorMin())
                    ? mTableModel.getMinimumValue(mMarkerColor.getColorColumn())
                    : (mTableModel.isLogarithmicViewMode(mMarkerColor.getColorColumn()))
                            ? (float) Math.log10(mMarkerColor.getColorMin())
                            : mMarkerColor.getColorMin();
            float max = Float.isNaN(mMarkerColor.getColorMax())
                    ? mTableModel.getMaximumValue(mMarkerColor.getColorColumn())
                    : (mTableModel.isLogarithmicViewMode(mMarkerColor.getColorColumn()))
                            ? (float) Math.log10(mMarkerColor.getColorMax())
                            : mMarkerColor.getColorMax();

            //   1. colorMin is explicitly set; max is real max, but lower than min
            // or 2. colorMax is explicitly set; min is real min, but larger than max
            // first case is OK, second needs adaption below to be handled as indented
            if (min >= max)
                if (!Float.isNaN(mMarkerColor.getColorMax()))
                    min = Float.MIN_VALUE;

            for (int i = 0; i < mDataPoints; i++) {
                float value = mPoint[i].record.getDouble(mMarkerColor.getColorColumn());
                if (Float.isNaN(value))
                    mPoint[i].colorIndex = VisualizationColor.cMissingDataColorIndex;
                else if (value <= min)
                    mPoint[i].colorIndex = (byte) VisualizationColor.cSpecialColorCount;
                else if (value >= max)
                    mPoint[i].colorIndex = (byte) (mMarkerColor.getColorList().length - 1);
                else
                    mPoint[i].colorIndex = (byte) (0.5 + VisualizationColor.cSpecialColorCount
                            + (float) (mMarkerColor.getColorList().length - VisualizationColor.cSpecialColorCount
                                    - 1) * (value - min) / (max - min));
            }
        }

        invalidateOffImage(true);
    }

    /**
     * @param axis
     * @param rowID if != -1 then update this row only
     */
    private void setSimilarityValues(int axis, int rowID) {
        int column = mAxisIndex[axis];
        if (mActivePoint == null) {
            mAxisSimilarity[axis] = null;
        } else if (rowID != -1) {
            for (int i = 0; i < mDataPoints; i++) {
                if (mPoint[i].record.getID() == rowID) {
                    mAxisSimilarity[axis][rowID] = (float) mTableModel.getDescriptorSimilarity(mActivePoint.record,
                            mPoint[i].record, column);
                    break;
                }
            }
        } else {
            if (DescriptorConstants.DESCRIPTOR_Flexophore.shortName
                    .equals(mTableModel.getColumnSpecialType(column))) {
                // if we have the slow 3DPPMM2 then use a progress dialog and multi-threading
                mAxisSimilarity[axis] = null;
                Object descriptor = mActivePoint.record.getData(column);
                if (descriptor != null) {
                    Component c = this;
                    while (!(c instanceof Frame))
                        c = c.getParent();

                    String idcode = new String(
                            (byte[]) mActivePoint.record.getData(mTableModel.getParentColumn(column)));
                    mAxisSimilarity[axis] = createSimilarityListSMP(idcode, descriptor, column);
                }
            } else {
                mAxisSimilarity[axis] = new float[mDataPoints];
                for (int i = 0; i < mDataPoints; i++)
                    mAxisSimilarity[axis][mPoint[i].record.getID()] = (float) mTableModel
                            .getDescriptorSimilarity(mActivePoint.record, mPoint[i].record, column);
            }
        }
        invalidateOffImage(true);
    }

    /**
     * @param rowID if != -1 then update this row only
     */
    private void updateSimilarityMarkerSizes(int rowID) {
        if (mActivePoint == null || !mTableModel.isDescriptorColumn(mMarkerSizeColumn)) {
            mSimilarityMarkerSize = null;
        } else if (rowID != -1) {
            for (int i = 0; i < mDataPoints; i++) {
                if (mPoint[i].record.getID() == rowID) {
                    mSimilarityMarkerSize[rowID] = (float) mTableModel.getDescriptorSimilarity(mActivePoint.record,
                            mPoint[i].record, mMarkerSizeColumn);
                    break;
                }
            }
        } else {
            if (DescriptorConstants.DESCRIPTOR_Flexophore.shortName
                    .equals(mTableModel.getColumnSpecialType(mMarkerSizeColumn))) {
                // if we have the slow 3DPPMM2 then use a progress dialog and multi-threading
                mSimilarityMarkerSize = null;
                Object descriptor = mActivePoint.record.getData(mMarkerSizeColumn);
                if (descriptor != null) {
                    Component c = this;
                    while (!(c instanceof Frame))
                        c = c.getParent();

                    String idcode = new String(
                            (byte[]) mActivePoint.record.getData(mTableModel.getParentColumn(mMarkerSizeColumn)));
                    mSimilarityMarkerSize = createSimilarityListSMP(idcode, descriptor, mMarkerSizeColumn);
                }
            } else {
                mSimilarityMarkerSize = new float[mDataPoints];
                for (int i = 0; i < mDataPoints; i++)
                    mSimilarityMarkerSize[mPoint[i].record.getID()] = (float) mTableModel
                            .getDescriptorSimilarity(mActivePoint.record, mPoint[i].record, mMarkerSizeColumn);
            }
        }
    }

    /**
     * @param rowID if != -1 then update this row only
     */
    private void setSimilarityColors(int rowID) {
        if (mActivePoint == null) {
            for (int i = 0; i < mDataPoints; i++)
                mPoint[i].colorIndex = VisualizationColor.cDefaultDataColorIndex;
        } else {
            float min = Float.isNaN(mMarkerColor.getColorMin()) ? 0.0f : mMarkerColor.getColorMin();
            float max = Float.isNaN(mMarkerColor.getColorMax()) ? 1.0f : mMarkerColor.getColorMax();
            if (min >= max) {
                min = 0.0f;
                max = 1.0f;
            }

            int column = mMarkerColor.getColorColumn();
            float[] flexophoreSimilarity = null;
            if (rowID == -1 && DescriptorConstants.DESCRIPTOR_Flexophore.shortName
                    .equals(mTableModel.getColumnSpecialType(column))) {
                // if we have the slow 3DPPMM2 then use a progress dialog and multi-threading
                Object descriptor = mActivePoint.record.getData(column);
                if (descriptor != null) {
                    Component c = this;
                    while (!(c instanceof Frame))
                        c = c.getParent();

                    String idcode = new String(
                            (byte[]) mActivePoint.record.getData(mTableModel.getParentColumn(column)));
                    flexophoreSimilarity = createSimilarityListSMP(idcode, descriptor, column);
                    if (flexophoreSimilarity == null) { // cancelled
                        mMarkerColor.setColor(cColumnUnassigned);
                        return;
                    }
                }
            }

            for (int i = 0; i < mDataPoints; i++) {
                if (rowID == -1 || mActivePoint.record.getID() == rowID) {
                    float similarity = (flexophoreSimilarity != null) ? flexophoreSimilarity[i]
                            : mTableModel.getDescriptorSimilarity(mActivePoint.record, mPoint[i].record, column);
                    if (Float.isNaN(similarity))
                        mPoint[i].colorIndex = VisualizationColor.cMissingDataColorIndex;
                    else if (similarity <= min)
                        mPoint[i].colorIndex = (byte) VisualizationColor.cSpecialColorCount;
                    else if (similarity >= max)
                        mPoint[i].colorIndex = (byte) (mMarkerColor.getColorList().length - 1);
                    else
                        mPoint[i].colorIndex = (byte) (0.5 + VisualizationColor.cSpecialColorCount
                                + (float) (mMarkerColor.getColorList().length
                                        - VisualizationColor.cSpecialColorCount - 1) * (similarity - min)
                                        / (max - min));
                }
            }
        }
    }

    private float[] createSimilarityListSMP(String idcode, Object descriptor, int descriptorColumn) {
        float[] similarity = mTableModel.getSimilarityListFromCache(idcode, descriptorColumn);
        if (similarity != null)
            return similarity;

        Component c = this;
        while (!(c instanceof Frame))
            c = c.getParent();

        JProgressDialog progressDialog = new JProgressDialog((Frame) c) {
            private static final long serialVersionUID = 0x20110325;

            public void stopProgress() {
                super.stopProgress();
                close();
            }
        };

        mTableModel.createSimilarityListSMP(null, descriptor, idcode, descriptorColumn, progressDialog);

        progressDialog.setVisible(true);
        similarity = mTableModel.getSimilarityListSMP();

        return similarity;
    }

    /**
     * Calculates for the given category column, which of its categories have at least one visible
     * member in this view. To do so, this method updates mVisibleCategoryFromCategory[column].
     * @param valid category column
     * @return
     */
    protected int getVisibleCategoryCount(int column) {
        if (CompoundTableHitlistHandler.isHitlistColumn(column))
            return 2; // don't handle lists here

        if (mVisibleCategoryFromCategory == null)
            mVisibleCategoryFromCategory = new int[mTableModel.getTotalColumnCount()][];

        // If the tableModel's row visibility was changed since the last generation of visible categories
        // then we have to freshly generate.
        if (mVisibleCategoryExclusionTag != mTableModel.getExclusionTag()) {
            mVisibleCategoryExclusionTag = mTableModel.getExclusionTag();
            mVisibleCategoryFromCategory[column] = null;
        }

        if (mVisibleCategoryFromCategory[column] == null) {
            int categoryCount = mTableModel.getCategoryCount(column);
            int count = 0;
            boolean[] isVisibleCategory = new boolean[categoryCount];
            for (int i = 0; i < mDataPoints; i++) {
                if (isVisible(mPoint[i])) {
                    int category = mTableModel.getCategoryIndex(column, mPoint[i].record);
                    if (!isVisibleCategory[category]) {
                        isVisibleCategory[category] = true;
                        if (++count == categoryCount)
                            break;
                    }
                }
            }

            mVisibleCategoryFromCategory[column] = new int[categoryCount];
            int visibleCategory = 0;
            for (int i = 0; i < categoryCount; i++)
                mVisibleCategoryFromCategory[column][i] = isVisibleCategory[i] ? visibleCategory++ : -1;

            return count;
        }

        int count = 0;
        for (int i = 0; i < mVisibleCategoryFromCategory[column].length; i++)
            if (mVisibleCategoryFromCategory[column][i] != -1)
                count++;
        return count;
    }

    /**
     * Creates a list of those categories within the given column,
     * which have at least one visible member in this view.
     * @param column
     * @return
     */
    protected String[] getVisibleCategoryList(int column) {
        String[] category = mTableModel.getCategoryList(column);
        String[] visibleCategory = new String[getVisibleCategoryCount(column)];
        for (int i = 0; i < mVisibleCategoryFromCategory[column].length; i++)
            if (mVisibleCategoryFromCategory[column][i] != -1)
                visibleCategory[mVisibleCategoryFromCategory[column][i]] = category[i];
        return visibleCategory;
    }

    private void invalidateSplittingIndices() {
        mHVExclusionTag = -1;
        invalidateOffImage(true);
    }

    /**
     * Updates all rows assignments (hv-indexes) to split views, if they are invalid.
     * @return true, if these assignments were updated.
     */
    protected boolean validateSplittingIndices() {
        if (mHVExclusionTag == mTableModel.getExclusionTag())
            return false;

        mHVExclusionTag = mTableModel.getExclusionTag();

        int count1 = (mSplittingColumn[0] == cColumnUnassigned) ? 1
                : mShowEmptyInSplitView ? mTableModel.getCategoryCount(mSplittingColumn[0])
                        : getVisibleCategoryCount(mSplittingColumn[0]);
        int count2 = (mSplittingColumn[1] == cColumnUnassigned) ? 1
                : mShowEmptyInSplitView ? mTableModel.getCategoryCount(mSplittingColumn[1])
                        : getVisibleCategoryCount(mSplittingColumn[1]);
        mHVCount = Math.max(0, count1 * count2);

        mSplitViewCountExceeded = (mHVCount > cMaxSplitViewCount);
        if (mSplitViewCountExceeded)
            mHVCount = 1;

        if (mHVCount == 1) {
            for (int i = 0; i < mDataPoints; i++)
                mPoint[i].hvIndex = 0;
        } else if (mSplittingColumn[1] == cColumnUnassigned) {
            if (CompoundTableHitlistHandler.isHitlistColumn(mSplittingColumn[0])) {
                int flagNo = mTableModel.getHitlistHandler()
                        .getHitlistFlagNo(CompoundTableHitlistHandler.getHitlistFromColumn(mSplittingColumn[0]));
                for (int i = 0; i < mDataPoints; i++)
                    mPoint[i].hvIndex = mPoint[i].record.isFlagSet(flagNo) ? 0 : 1;
            } else if (mShowEmptyInSplitView) {
                for (int i = 0; i < mDataPoints; i++)
                    mPoint[i].hvIndex = mTableModel.getCategoryIndex(mSplittingColumn[0], mPoint[i].record);
            } else {
                for (int i = 0; i < mDataPoints; i++)
                    mPoint[i].hvIndex = mVisibleCategoryFromCategory[mSplittingColumn[0]][mTableModel
                            .getCategoryIndex(mSplittingColumn[0], mPoint[i].record)];
            }
        } else {
            int flagNo1 = -1;
            if (CompoundTableHitlistHandler.isHitlistColumn(mSplittingColumn[0]))
                flagNo1 = mTableModel.getHitlistHandler()
                        .getHitlistFlagNo(CompoundTableHitlistHandler.getHitlistFromColumn(mSplittingColumn[0]));

            int flagNo2 = -1;
            if (CompoundTableHitlistHandler.isHitlistColumn(mSplittingColumn[1]))
                flagNo2 = mTableModel.getHitlistHandler()
                        .getHitlistFlagNo(CompoundTableHitlistHandler.getHitlistFromColumn(mSplittingColumn[1]));

            for (int i = 0; i < mDataPoints; i++) {
                CompoundRecord record = mPoint[i].record;
                int index1 = (flagNo1 != -1) ? (record.isFlagSet(flagNo1) ? 0 : 1)
                        : mShowEmptyInSplitView ? mTableModel.getCategoryIndex(mSplittingColumn[0], record)
                                : mVisibleCategoryFromCategory[mSplittingColumn[0]][mTableModel
                                        .getCategoryIndex(mSplittingColumn[0], record)];
                int index2 = (flagNo2 != -1) ? (record.isFlagSet(flagNo2) ? 0 : 1)
                        : mShowEmptyInSplitView ? mTableModel.getCategoryIndex(mSplittingColumn[1], record)
                                : mVisibleCategoryFromCategory[mSplittingColumn[1]][mTableModel
                                        .getCategoryIndex(mSplittingColumn[1], record)];
                mPoint[i].hvIndex = index1 + index2 * count1;
            }
        }

        return true;
    }

    public Color getTitleBackground() {
        return mTitleBackground;
    }

    public void setTitleBackground(Color c) {
        if (!mTitleBackground.equals(c)) {
            mTitleBackground = c;
            invalidateOffImage(false);
        }
    }

    public Color getViewBackground() {
        return (mViewBackground == null) ? Color.WHITE : mViewBackground;
    }

    /**
     * Defines the background color
     * @param c (null for WHITE)
     * @return whether there was a change
     */
    public boolean setViewBackground(Color c) {
        if (Color.WHITE.equals(c))
            c = null;
        if ((c != null && !c.equals(mViewBackground)) || (c == null && mViewBackground != null)) {
            mViewBackground = c;
            invalidateOffImage(false);
            return true;
        }
        return false;
    }

    /**
     * Generates a neutral grey with given contrast to the current background.
     * @param contrast 0.0 (not visible) to 1.0 (full contrast)
     * @return
     */
    protected Color getContrastGrey(float contrast) {
        return getContrastGrey(contrast, mViewBackground);
    }

    /**
     * Generates a neutral grey with given contrast to given color.
     * @param contrast 0.0 (not visible) to 1.0 (full contrast)
     * @return
     */
    protected Color getContrastGrey(float contrast, Color color) {
        float brightness = ColorHelper.perceivedBrightness(color);
        float range = (brightness > 0.5) ? brightness : 1f - brightness;

        // enhance contrast for middle bright backgrounds
        contrast = (float) Math.pow(contrast, range);

        return (brightness > 0.5) ? Color.getHSBColor(0.0f, 0.0f, brightness - range * contrast)
                : Color.getHSBColor(0.0f, 0.0f, brightness + range * contrast);
    }

    @Override
    public void setFocusHitlist(int hitlistIndex) {
        if (mFocusHitlist != hitlistIndex) {
            mFocusHitlist = hitlistIndex;
            invalidateOffImage(mChartType != cChartTypeScatterPlot); // no of colors in mChartInfo needs to be updated
        }
    }

    /**
     * This is the user defined relative font size factor applied to marker and scale labels.
     * Note: Marker labels are also affected by setMarkerLabelSize().
     * @param size
     * @param isAdjusting
     */
    public void setFontSize(float size, boolean isAdjusting) {
        if (mRelativeFontSize != size) {
            mRelativeFontSize = size;
            invalidateOffImage(true);
        }
    }

    public void setJittering(float jittering, boolean isAdjusting) {
        if (mMarkerJittering != jittering) {
            mMarkerJittering = jittering;
            invalidateOffImage(true);
        }
    }

    public void setCaseSeparation(int column, float value, boolean isAdjusting) {
        if (column >= 0 && (!mTableModel.isColumnTypeCategory(column)
                || mTableModel.getCategoryCount(column) > cMaxCaseSeparationCategoryCount))
            column = cColumnUnassigned;

        if (mCaseSeparationColumn != column
                || (mCaseSeparationColumn != cColumnUnassigned && mCaseSeparationValue != value)) {
            mCaseSeparationColumn = column;
            mCaseSeparationValue = value;
            validateExclusion(determineChartType());
            invalidateOffImage(true);
        }
    }

    /**
     * Returns whether the user selected one or two category columns for view splitting.
     * If columns are selected, but the vast number of categories is preventing splitting,
     * then this returns true!
     * @return whether the view is configured for view splitting
     */
    public boolean isSplitViewConfigured() {
        return mSplittingColumn[0] != cColumnUnassigned;
    }

    /**
     * Returns whether the user selected one or two category columns for view splitting
     * and if the number of view categories don't exceed the maximum for rendering.
     * @return whether split views are rendered
     */
    protected boolean isSplitView() {
        return mSplittingColumn[0] != cColumnUnassigned && !mSplitViewCountExceeded;
    }

    public boolean isShowEmptyInSplitView() {
        return mShowEmptyInSplitView;
    }

    public int[] getSplittingColumns() {
        return mSplittingColumn;
    }

    public float getSplittingAspectRatio() {
        return mSplittingAspectRatio;
    }

    public void setSplittingColumns(int column1, int column2, float aspectRatio, boolean showEmptyViews) {
        if (column1 == cColumnUnassigned && column2 != cColumnUnassigned) {
            column1 = column2;
            column2 = cColumnUnassigned;
        }

        if (mSplittingColumn[0] != column1 || mSplittingColumn[1] != column2
                || (mSplittingColumn[0] != cColumnUnassigned && mSplittingAspectRatio != aspectRatio)
                || (mSplittingColumn[0] != cColumnUnassigned && mShowEmptyInSplitView != showEmptyViews)) {
            if ((column1 == cColumnUnassigned || mTableModel.isColumnTypeCategory(column1))
                    && (column2 == cColumnUnassigned || mTableModel.isColumnTypeCategory(column2))) {
                mSplittingColumn[0] = column1;
                mSplittingColumn[1] = column2;
                mSplittingAspectRatio = aspectRatio;
                mShowEmptyInSplitView = showEmptyViews;
                invalidateSplittingIndices();
            }
        }
    }

    /** Determine current mean/median setting for box and whisker plots
     * @return mode or BOXPLOT_DEFAULT_MEAN_MODE if not box/whisker plot
     */
    public int getBoxplotMeanMode() {
        return (mChartType == cChartTypeBoxPlot || mChartType == cChartTypeWhiskerPlot) ? mBoxplotMeanMode
                : BOXPLOT_DEFAULT_MEAN_MODE;
    }

    /**
     * Defines whether mean and/or median are have indicators in box/whisker plots.
     * @param mode
     */
    public void setBoxplotMeanMode(int mode) {
        if (mBoxplotMeanMode != mode) {
            mBoxplotMeanMode = mode;
            invalidateOffImage(false);
        }
    }

    /** Determines whether mean and/or median values are shown in a current box or whisker plot.
     *  Returns false if the current plot is neither box nor whisker plot. 
     * @return false if the current box or whisker plot shows 
     */
    public boolean isShowMeanAndMedianValues() {
        return (mChartType == cChartTypeBoxPlot || mChartType == cChartTypeWhiskerPlot)
                && mBoxplotShowMeanAndMedianValues;
    }

    /**
     * Defines whether mean and/or median are shown in box/whisker plots.
     * @param b
     */
    public void setShowMeanAndMedianValues(boolean b) {
        if (mBoxplotShowMeanAndMedianValues != b) {
            mBoxplotShowMeanAndMedianValues = b;
            invalidateOffImage(false);
        }
    }

    /** Determines whether p-values are shown in a current box or whisker plot.
     *  Returns false if the current plot is neither box nor whisker plot
     *  or if no proper p-value column is assigned.
     * @return false if the current box or whisker plot shows 
     */
    public boolean isShowPValue() {
        return (mChartType == cChartTypeBoxPlot || mChartType == cChartTypeWhiskerPlot) && mBoxplotShowPValue
                && isValidPValueColumn(mPValueColumn);
    }

    /**
     * Defines whether p-values are shown in box/whisker plots.
     * For showing p-values one also needs to call setPValueColumn()
     * @param b
     */
    public void setShowPValue(boolean b) {
        if (mBoxplotShowPValue != b) {
            mBoxplotShowPValue = b;
            if (isValidPValueColumn(mPValueColumn))
                invalidateOffImage(true); // to recalculate p-values
        }
    }

    /** Determines whether fold-change values are shown in a current box or whisker plot.
     *  Returns false if the current plot is neither box nor whisker plot
     *  or if no proper p-value column is assigned.
     * @return false if the current box or whisker plot shows 
     */
    public boolean isShowFoldChange() {
        return (mChartType == cChartTypeBoxPlot || mChartType == cChartTypeWhiskerPlot) && mBoxplotShowFoldChange
                && isValidPValueColumn(mPValueColumn);
    }

    /**
     * Defines whether fold-change values are shown in box/whisker plots.
     * For showing fold-change values one also needs to call setPValueColumn()
     * @param b
     */
    public void setShowFoldChange(boolean b) {
        if (mBoxplotShowFoldChange != b) {
            mBoxplotShowFoldChange = b;
            if (isValidPValueColumn(mPValueColumn))
                invalidateOffImage(true); // to recalculate fold change
        }
    }

    /**
     * Returns the currently applied p-value column.
     * @return
     */
    public int getPValueColumn() {
        return isValidPValueColumn(mPValueColumn) ? mPValueColumn : cColumnUnassigned;
    }

    /**
     * Returns the currently applied reference category for p-value calculation.
     * @return
     */
    public String getPValueRefCategory() {
        return (getPValueColumn() == cColumnUnassigned) ? null : mPValueRefCategory;
    }

    public boolean isValidPValueColumn(int column) {
        if (column == cColumnUnassigned)
            return false;

        if (mChartType == cChartTypeBoxPlot || mChartType == cChartTypeWhiskerPlot) {
            if (column == getCaseSeparationColumn())
                return true;

            if (isSplitView() && (column == mSplittingColumn[0] || column == mSplittingColumn[1]))
                return true;

            for (int axis = 0; axis < mDimensions; axis++)
                if (column == mAxisIndex[axis] && mCategoryVisibleCount[axis] >= 2 && axis != mChartInfo.barAxis)
                    return true;
        }

        return false;
    }

    public void setPValueColumn(int column, String refCategory) {
        if (column == cColumnUnassigned)
            refCategory = null;
        else if (refCategory == null)
            column = cColumnUnassigned;

        if (column != mPValueColumn || (refCategory != null && !refCategory.equals(mPValueRefCategory))) {
            mPValueColumn = column;
            mPValueRefCategory = refCategory;
            invalidateOffImage(true);
        }
    }

    /**
     * In fast rendering mode anti-aliasing is switched off
     * @return whether we are in fast render mode
     */
    public boolean isFastRendering() {
        return mIsFastRendering;
    }

    /**
     * In fast rendering mode anti-aliasing is switched off
     * @param v
     */
    public void setFastRendering(boolean v) {
        if (mIsFastRendering != v) {
            mIsFastRendering = v;
            invalidateOffImage(false);
        }
    }

    /**
     * Determines the type of the drawn chart and visible ranges on all axes.
     * This depends on the preferred chart type, on the columns being assigned
     * to the axes, the number of existing categories within these columns,
     * and the current pruning bar settings.
     * If any of these change, then this method needs to be called.
     * @return local exclusion update needs (EXCLUSION_FLAG_ZOOM_0 left shifted according to axis)
     */
    private int determineChartType() {
        boolean[] wasCatagoryAxis = new boolean[mDimensions];
        for (int axis = 0; axis < mDimensions; axis++)
            wasCatagoryAxis[axis] = mIsCategoryAxis[axis];

        mChartType = cChartTypeScatterPlot; // scatter plot is the default that is always possible
        for (int axis = 0; axis < mDimensions; axis++)
            mIsCategoryAxis[axis] = (mAxisIndex[axis] != cColumnUnassigned
                    && mTableModel.isColumnTypeCategory(mAxisIndex[axis])
                    && !mTableModel.isColumnTypeDouble(mAxisIndex[axis]));

        if (mPreferredChartType == cChartTypeScatterPlot) {
            int csAxis = getCaseSeparationAxis();
            if (csAxis != -1)
                mIsCategoryAxis[csAxis] = true;
        } else if (mPreferredChartType == cChartTypeBoxPlot || mPreferredChartType == cChartTypeWhiskerPlot) {
            int boxPlotDoubleAxis = determineBoxPlotDoubleAxis();
            if (boxPlotDoubleAxis != -1) {
                mChartType = mPreferredChartType;
                for (int axis = 0; axis < mDimensions; axis++)
                    mIsCategoryAxis[axis] = (axis != boxPlotDoubleAxis);
            }
        } else if (mPreferredChartType == cChartTypeBars || mPreferredChartType == cChartTypePies) {
            boolean allQualify = true;
            for (int axis = 0; axis < mDimensions; axis++)
                if (!qualifiesAsChartCategory(axis))
                    allQualify = false;

            if (allQualify) {
                int categoryCount = 1;
                for (int axis = 0; axis < mDimensions; axis++)
                    if (mAxisIndex[axis] != cColumnUnassigned)
                        categoryCount *= mTableModel.getCategoryCount(mAxisIndex[axis]);

                if (categoryCount <= cMaxTotalChartCategoryCount) {
                    mChartType = mPreferredChartType;
                    for (int axis = 0; axis < mDimensions; axis++)
                        mIsCategoryAxis[axis] = (mAxisIndex[axis] != cColumnUnassigned);
                }
            }
        }

        if (mTreeNodeList == null) { // no tree view
            mActiveExclusionFlags = 0xFF; // masks out NaN flags for axes that show categories
            for (int axis = 0; axis < mDimensions; axis++)
                if (mIsCategoryAxis[axis])
                    mActiveExclusionFlags &= ~(byte) (EXCLUSION_FLAG_NAN_0 << axis);

            updateBarAndPieNaNExclusion();
        } else if (mTreeNodeList.length == 0) { // we have an empty tree view
            mActiveExclusionFlags = 0;
        } else { // tree view with at least one node
            mActiveExclusionFlags = EXCLUSION_FLAG_DETAIL_GRAPH;
        }

        int localExclusionNeeds = 0;
        for (int axis = 0; axis < mDimensions; axis++) {
            if (mIsCategoryAxis[axis] != wasCatagoryAxis[axis])
                localExclusionNeeds |= (EXCLUSION_FLAG_ZOOM_0 << axis);

            calculateVisibleRange(axis);
        }

        return localExclusionNeeds;
    }

    private void updateBarAndPieNaNExclusion() {
        boolean excludeNaN = (mChartType == cChartTypeBars || mChartType == cChartTypePies)
                && (mChartMode != cChartModeCount && mChartMode != cChartModePercent)
                && mChartColumn != cColumnUnassigned && !mTableModel.isDescriptorColumn(mChartColumn);

        for (int i = 0; i < mDataPoints; i++) {
            if (excludeNaN && Float.isNaN(mPoint[i].record.getDouble(mChartColumn)))
                mPoint[i].exclusionFlags |= EXCLUSION_FLAG_DETAIL_GRAPH;
            else
                mPoint[i].exclusionFlags &= ~EXCLUSION_FLAG_DETAIL_GRAPH;
        }
    }

    /**
     * Updates the NaN and zoom flags of local exclusion for all axes where needed
     * and applies the local to the global exclusion, if local NaN or zoom flags were updated.
     * @param localExclusionNeeds EXCLUSION_FLAG_NAN_0 and EXCLUSION_FLAG_ZOOM_0 left shifted according to axis
     */
    private void validateExclusion(int localExclusionNeeds) {
        boolean found = false;
        for (int axis = 0; axis < mDimensions; axis++) {
            if ((localExclusionNeeds & (EXCLUSION_FLAG_NAN_0 << axis)) != 0) {
                found = true;
                initializeLocalExclusion(axis);
            }
            if (mAxisIndex[axis] != cColumnUnassigned
                    && (localExclusionNeeds & (EXCLUSION_FLAG_ZOOM_0 << axis)) != 0) {
                found = true;
                updateLocalZoomExclusion(axis);
            }
        }
        if (found)
            applyLocalExclusion(false);
    }

    /**
     * Determines based on table model information and current axis assignment,
     * which axis is the preferred double value axis.
     * @return 0,1,-1 for x-axis, y-axis, none
     */
    protected int determineBoxPlotDoubleAxis() {
        for (int i = 0; i < mDimensions; i++)
            if (mAxisIndex[i] != cColumnUnassigned
                    && (mTableModel.isDescriptorColumn(mAxisIndex[i])
                            || mTableModel.isColumnTypeDouble(mAxisIndex[i]))
                    && !mTableModel.isColumnTypeCategory(mAxisIndex[i]) && qualifyOtherAsChartCategory(i))
                return i;

        int minCategoryCount = Integer.MAX_VALUE;
        int minCountAxis = -1;
        for (int i = 0; i < mDimensions; i++) {
            if (mAxisIndex[i] != cColumnUnassigned && mTableModel.isColumnTypeDouble(mAxisIndex[i])
                    && qualifyOtherAsChartCategory(i)) {
                int categoryCount = 1;
                for (int axis = 0; axis < mDimensions; axis++)
                    if (axis != i && mAxisIndex[axis] != cColumnUnassigned)
                        categoryCount = mTableModel.getCategoryCount(mAxisIndex[axis]);

                if (minCategoryCount > categoryCount) {
                    minCategoryCount = categoryCount;
                    minCountAxis = i;
                }
            }
        }

        return minCountAxis;
    }

    private boolean qualifyOtherAsChartCategory(int axis) {
        for (int i = 0; i < mDimensions; i++)
            if (axis != i && !qualifiesAsChartCategory(i))
                return false;
        return true;
    }

    protected boolean qualifiesAsChartCategory(int axis) {
        return (mAxisIndex[axis] == cColumnUnassigned || (mTableModel.isColumnTypeCategory(mAxisIndex[axis])
                && mTableModel.getCategoryCount(mAxisIndex[axis]) <= cMaxChartCategoryCount));
    }

    /**
     * Calculates the visible range of the axis based on pruning bar settings,
     * current graph type, logarithmic view mode.
     * @param axis
     * @param low 0.0 >= value >= high
     * @param high low >= value >= 1.0
     * @return whether the visible range has been changed
     */
    private boolean calculateVisibleRange(int axis) {
        float visMin = -1.0f;
        float visMax = 1.0f;
        int column = mAxisIndex[axis];
        if (column != cColumnUnassigned) {
            if (mIsCategoryAxis[axis]) {
                int maxCategory = mTableModel.getCategoryCount(column) - 1;
                visMin = Math.round(mPruningBarLow[axis] * maxCategory) - 0.5f;
                visMax = Math.round(mPruningBarHigh[axis] * maxCategory) + 0.5f;
            } else if (mTableModel.isDescriptorColumn(column)) {
                visMin = mPruningBarLow[axis];
                visMax = mPruningBarHigh[axis];
            } else {
                float dataMin = mTableModel.getMinimumValue(column);
                float dataMax = mTableModel.getMaximumValue(column);
                float dataRange = dataMax - dataMin;
                visMin = (mPruningBarLow[axis] == 0.0f) ? dataMin : dataMin + mPruningBarLow[axis] * dataRange;
                visMax = (mPruningBarHigh[axis] == 1.0f) ? dataMax : dataMin + mPruningBarHigh[axis] * dataRange;
            }
        }

        if (visMin != mAxisVisMin[axis] || visMax != mAxisVisMax[axis]) {
            mAxisVisMin[axis] = visMin;
            mAxisVisMax[axis] = visMax;
            return true;
        }

        return false;
    }

    /**
     * Based on the currently selected visible range, the displayed chart type and axis usage,
     * this method calculates the pruning bar value from 0.0 to 1.0 to reflect visible range low value.
     * @param axis
     * @return low value of visible range normalized to span 0.0 to 1.0
     */
    public float getPruningBarLow(int axis) {
        int column = mAxisIndex[axis];
        if (column == cColumnUnassigned)
            return 0.0f;
        if (mTableModel.isDescriptorColumn(column))
            return mAxisVisMin[axis];
        if (mIsCategoryAxis[axis])
            return (float) Math.round(mAxisVisMin[axis] + 0.5f) / (float) mTableModel.getCategoryCount(column);
        else
            return (mAxisVisMin[axis] - mTableModel.getMinimumValue(column))
                    / (mTableModel.getMaximumValue(column) - mTableModel.getMinimumValue(column));
    }

    /**
     * Based on the currently selected visible range, the displayed chart type and axis usage,
     * this method calculates the pruning bar value from 0.0 to 1.0 to reflect visible range high value.
     * @param axis
     * @return high value of visible range normalized to span 0.0 to 1.0
     */
    public float getPruningBarHigh(int axis) {
        int column = mAxisIndex[axis];
        if (column == cColumnUnassigned)
            return 1.0f;
        if (mTableModel.isDescriptorColumn(column))
            return mAxisVisMax[axis];
        if (mIsCategoryAxis[axis])
            return (float) Math.round(mAxisVisMax[axis] + 0.5f) / (float) mTableModel.getCategoryCount(column);
        else
            return (mAxisVisMax[axis] - mTableModel.getMinimumValue(column))
                    / (mTableModel.getMaximumValue(column) - mTableModel.getMinimumValue(column));
    }

    private float calculateZoomState() {
        float zoomState = 1.0f;
        int axisCount = 0;
        for (int axis = 0; axis < mDimensions; axis++) {
            if (mAxisIndex[axis] != cColumnUnassigned) {
                zoomState *= Math.max(0.001f, mPruningBarHigh[axis] - mPruningBarLow[axis]);
                axisCount++;
            }
        }
        zoomState = (float) Math.pow(zoomState, 0.75f / (float) axisCount);
        return Math.min(100f, 1f / zoomState);
    }

    // TODO remove mCategoryMin,mCategoryMax,mCategoryVisibleCount
    protected void calculateCategoryCounts(int boxPlotDoubleAxis) {
        for (int axis = 0; axis < mDimensions; axis++) {
            int column = mAxisIndex[axis];
            if (column == cColumnUnassigned || axis == boxPlotDoubleAxis) {
                mCategoryMin[axis] = 0;
                mCategoryMax[axis] = 1;
                mCategoryVisibleCount[axis] = 1;
            } else {
                mCategoryMin[axis] = Math.round(mAxisVisMin[axis] + 0.5f);
                mCategoryMax[axis] = Math.round(mAxisVisMax[axis] + 0.5f);
                mCategoryVisibleCount[axis] = mCategoryMax[axis] - mCategoryMin[axis];
            }
        }

        if (isCaseSeparationDone())
            mCaseSeparationCategoryCount = mTableModel.getCategoryCount(mCaseSeparationColumn);
        else
            mCaseSeparationCategoryCount = 1;

        mCombinedCategoryCount = new int[1 + mDimensions];
        mCombinedCategoryCount[0] = mCaseSeparationCategoryCount;
        for (int axis = 0; axis < mDimensions; axis++)
            mCombinedCategoryCount[axis + 1] = mCombinedCategoryCount[axis] * mCategoryVisibleCount[axis];
    }

    /**
     * Based on axis column assignments and on hvIndices of VisualizationPoints
     * this method assigns all visible VisualizationPoints to bars/pies and to color categories
     * within these bars/pies. It also calculates relative bar/pie sizes.
     * @param hvCount
     */
    protected void calculateBarsOrPies() {
        calculateCategoryCounts(-1);

        Color[] colorList = mMarkerColor.getColorList();
        int focusFlagNo = getFocusFlag();
        int basicColorCount = colorList.length + 1;
        int colorCount = basicColorCount * ((focusFlagNo == -1) ? 1 : 2);

        int catCount = mCaseSeparationCategoryCount;
        for (int count : mCategoryVisibleCount)
            catCount *= count;

        mChartInfo = new CategoryViewInfo(mHVCount, catCount, colorCount);

        mChartInfo.barAxis = 0;
        for (int axis = 1; axis < mDimensions; axis++)
            if (mAxisIndex[axis] == cColumnUnassigned)
                mChartInfo.barAxis = axis;
            else if (mAxisIndex[mChartInfo.barAxis] != cColumnUnassigned
                    && mCategoryVisibleCount[axis] <= mCategoryVisibleCount[mChartInfo.barAxis])
                mChartInfo.barAxis = axis;

        for (int i = 0; i < colorList.length; i++)
            mChartInfo.color[i] = colorList[i];
        mChartInfo.color[colorList.length] = VisualizationColor.cSelectedColor;
        if (focusFlagNo != -1) {
            for (int i = 0; i < colorList.length; i++)
                mChartInfo.color[i + basicColorCount] = VisualizationColor.grayOutColor(colorList[i]);
            mChartInfo.color[colorList.length + basicColorCount] = VisualizationColor
                    .grayOutColor(VisualizationColor.cSelectedColor);
        }

        int visibleCount = 0;
        for (int i = 0; i < mDataPoints; i++) {
            if (isVisibleExcludeNaN(mPoint[i])) {
                int colorIndex = ((mPoint[i].record.getFlags() & CompoundRecord.cFlagMaskSelected) != 0
                        && mFocusHitlist != cFocusOnSelection) ? colorList.length : mPoint[i].colorIndex;
                if (focusFlagNo != -1 && !mPoint[i].record.isFlagSet(focusFlagNo))
                    colorIndex += basicColorCount;

                int cat = getChartCategoryIndex(mPoint[i]);
                mChartInfo.pointsInCategory[mPoint[i].hvIndex][cat]++;
                mChartInfo.pointsInColorCategory[mPoint[i].hvIndex][cat][colorIndex]++;
                visibleCount++;
                switch (mChartMode) {
                case cChartModeCount:
                case cChartModePercent:
                    mChartInfo.barValue[mPoint[i].hvIndex][cat]++;
                    break;
                case cChartModeMean:
                case cChartModeSum:
                    mChartInfo.barValue[mPoint[i].hvIndex][cat] += mPoint[i].record.getDouble(mChartColumn);
                    break;
                case cChartModeMin:
                case cChartModeMax:
                    float value = mPoint[i].record.getDouble(mChartColumn);
                    if (mChartInfo.pointsInCategory[mPoint[i].hvIndex][cat] == 1)
                        mChartInfo.barValue[mPoint[i].hvIndex][cat] = value;
                    else if (mChartMode == cChartModeMin)
                        mChartInfo.barValue[mPoint[i].hvIndex][cat] = Math
                                .min(mChartInfo.barValue[mPoint[i].hvIndex][cat], value);
                    else
                        mChartInfo.barValue[mPoint[i].hvIndex][cat] = Math
                                .max(mChartInfo.barValue[mPoint[i].hvIndex][cat], value);
                    break;
                }
            }
        }
        if (mChartMode == cChartModePercent)
            for (int i = 0; i < mHVCount; i++)
                for (int j = 0; j < catCount; j++)
                    mChartInfo.barValue[i][j] *= 100f / visibleCount;
        if (mChartMode == cChartModeMean)
            for (int i = 0; i < mHVCount; i++)
                for (int j = 0; j < catCount; j++)
                    if (mChartInfo.pointsInCategory[i][j] != 0)
                        mChartInfo.barValue[i][j] /= mChartInfo.pointsInCategory[i][j];

        int[][][] count = new int[mHVCount][catCount][colorCount];
        for (int hv = 0; hv < mHVCount; hv++)
            for (int cat = 0; cat < catCount; cat++)
                for (int color = 1; color < colorCount; color++)
                    count[hv][cat][color] = count[hv][cat][color - 1]
                            + mChartInfo.pointsInColorCategory[hv][cat][color - 1];
        for (int i = 0; i < mDataPoints; i++) {
            if (isVisibleExcludeNaN(mPoint[i])) {
                int colorIndex = (mPoint[i].record.isSelected() && mFocusHitlist != cFocusOnSelection)
                        ? colorList.length
                        : mPoint[i].colorIndex;
                if (focusFlagNo != -1 && !mPoint[i].record.isFlagSet(focusFlagNo))
                    colorIndex += basicColorCount;

                int hv = mPoint[i].hvIndex;
                int cat = getChartCategoryIndex(mPoint[i]);
                mPoint[i].chartGroupIndex = count[hv][cat][colorIndex];
                count[hv][cat][colorIndex]++;
            }
        }

        float dataMin = Float.POSITIVE_INFINITY;
        float dataMax = Float.NEGATIVE_INFINITY;
        for (int i = 0; i < mHVCount; i++) {
            for (int j = 0; j < catCount; j++) {
                if (mChartInfo.pointsInCategory[i][j] != 0) {
                    if (dataMin > mChartInfo.barValue[i][j])
                        dataMin = mChartInfo.barValue[i][j];
                    if (dataMax < mChartInfo.barValue[i][j])
                        dataMax = mChartInfo.barValue[i][j];
                }
            }
        }
        mChartInfo.barOrPieDataAvailable = (dataMin != Float.POSITIVE_INFINITY);

        if (mChartInfo.barOrPieDataAvailable) {
            switch (mChartMode) {
            case cChartModeCount:
            case cChartModePercent:
                mChartInfo.axisMin = 0.0f;
                mChartInfo.axisMax = dataMax * cBarSpacingFactor;
                mChartInfo.barBase = 0.0f;
                break;
            default:
                if (mTableModel.isLogarithmicViewMode(mChartColumn)) {
                    float dataRange = dataMax - dataMin;
                    mChartInfo.axisMin = dataMin - dataRange * (cBarSpacingFactor - 1.0f);
                    mChartInfo.axisMax = dataMax + dataRange * (cBarSpacingFactor - 1.0f);
                    mChartInfo.barBase = mChartInfo.axisMin;
                } else {
                    if (dataMin >= 0.0) {
                        mChartInfo.axisMin = 0.0f;
                        mChartInfo.axisMax = dataMax * cBarSpacingFactor;
                    } else if (dataMax <= 0.0) {
                        mChartInfo.axisMin = dataMin * cBarSpacingFactor;
                        mChartInfo.axisMax = 0.0f;
                    } else {
                        float dataRange = dataMax - dataMin;
                        float spacing = (cBarSpacingFactor - 1.0f) * dataRange / 2.0f;
                        mChartInfo.axisMax = dataMax + spacing;
                        mChartInfo.axisMin = dataMin - spacing;
                    }
                    mChartInfo.barBase = 0.0f;
                }
                break;
            }
        } else {
            mChartInfo.axisMin = 0.0f;
            mChartInfo.axisMax = cBarSpacingFactor;
            mChartInfo.barBase = 0.0f;
        }
        //System.out.println("calculateBarsOrPies() dataMin:"+mChartInfo.dataMin+" dataMax:"+mChartInfo.dataMax+" axisMin:"+mChartInfo.axisMin+" axisMax:"+mChartInfo.axisMax+" barBase:"+mChartInfo.barBase);
    }

    /**
     * Based on axis column assignments and on hvIndices of VisualizationPoints
     * this method assigns all visible VisualizationPoints to boxes and to color categories
     * within these boxes. It also calculates statistical parameters of all boxes.
     * @param hvCount
     */
    protected void calculateBoxPlot() {
        int doubleAxis = determineBoxPlotDoubleAxis();
        calculateCategoryCounts(doubleAxis);

        Color[] colorList = mMarkerColor.getColorList();
        int focusFlagNo = getFocusFlag();
        int basicColorCount = colorList.length + 1;
        int colorCount = basicColorCount * ((focusFlagNo == -1) ? 1 : 2);

        int catCount = mCaseSeparationCategoryCount;
        for (int axis = 0; axis < mDimensions; axis++)
            if (axis != doubleAxis)
                catCount *= mCategoryVisibleCount[axis];

        BoxPlotViewInfo boxPlotInfo = new BoxPlotViewInfo(mHVCount, catCount, colorCount);
        mChartInfo = boxPlotInfo;
        boxPlotInfo.barAxis = doubleAxis;

        // create array with all visible values separated by hv and cat
        int[][] vCount = new int[mHVCount][catCount];
        for (int i = 0; i < mDataPoints; i++) {
            if (isVisibleExcludeNaN(mPoint[i])) {
                int cat = getChartCategoryIndex(mPoint[i]);
                int hv = mPoint[i].hvIndex;
                vCount[hv][cat]++;
            }
        }
        double[][][] value = new double[mHVCount][catCount][];
        for (int hv = 0; hv < mHVCount; hv++)
            for (int cat = 0; cat < catCount; cat++)
                if (vCount[hv][cat] != 0)
                    value[hv][cat] = new double[vCount[hv][cat]];

        // fill in values
        vCount = new int[mHVCount][catCount];
        for (int i = 0; i < mDataPoints; i++) {
            if (isVisibleExcludeNaN(mPoint[i])) {
                int hv = mPoint[i].hvIndex;
                int cat = getChartCategoryIndex(mPoint[i]);
                float d = getValue(mPoint[i].record, boxPlotInfo.barAxis);
                boxPlotInfo.barValue[hv][cat] += d;
                value[hv][cat][vCount[hv][cat]] = d;
                vCount[hv][cat]++;
            }
        }

        boxPlotInfo.boxQ1 = new float[mHVCount][catCount];
        boxPlotInfo.median = new float[mHVCount][catCount];
        boxPlotInfo.boxQ3 = new float[mHVCount][catCount];
        boxPlotInfo.boxLAV = new float[mHVCount][catCount];
        boxPlotInfo.boxUAV = new float[mHVCount][catCount];

        // calculate statistical parameters from sorted values
        for (int hv = 0; hv < mHVCount; hv++) {
            for (int cat = 0; cat < catCount; cat++) {
                if (vCount[hv][cat] != 0) {
                    Arrays.sort(value[hv][cat]);
                    boxPlotInfo.boxQ1[hv][cat] = getPercentile(value[hv][cat], 0.25f);
                    boxPlotInfo.median[hv][cat] = getPercentile(value[hv][cat], 0.50f);
                    boxPlotInfo.boxQ3[hv][cat] = getPercentile(value[hv][cat], 0.75f);
                    boxPlotInfo.barValue[hv][cat] /= vCount[hv][cat];

                    // set lower and upper adjacent values
                    float iqr = boxPlotInfo.boxQ3[hv][cat] - boxPlotInfo.boxQ1[hv][cat];
                    float lowerLimit = boxPlotInfo.boxQ1[hv][cat] - 1.5f * iqr;
                    float upperLimit = boxPlotInfo.boxQ3[hv][cat] + 1.5f * iqr;
                    int i = 0;
                    while (value[hv][cat][i] < lowerLimit)
                        i++;
                    boxPlotInfo.boxLAV[hv][cat] = (float) value[hv][cat][i];
                    i = value[hv][cat].length - 1;
                    while (value[hv][cat][i] > upperLimit)
                        i--;
                    boxPlotInfo.boxUAV[hv][cat] = (float) value[hv][cat][i];

                    if (mChartType == cChartTypeWhiskerPlot)
                        boxPlotInfo.pointsInCategory[hv][cat] = vCount[hv][cat];
                }
            }
        }

        int pValueColumn = getPValueColumn();
        if (pValueColumn != cColumnUnassigned) {
            int categoryIndex = getCategoryIndex(pValueColumn, mPValueRefCategory);
            if (categoryIndex != -1) {
                if (mBoxplotShowFoldChange)
                    boxPlotInfo.foldChange = new float[mHVCount][catCount];
                if (mBoxplotShowPValue)
                    boxPlotInfo.pValue = new float[mHVCount][catCount];
                int[] individualIndex = new int[1 + mDimensions];
                for (int hv = 0; hv < mHVCount; hv++) {
                    for (int cat = 0; cat < catCount; cat++) {
                        if (vCount[hv][cat] != 0) {
                            int refHV = getReferenceHV(hv, pValueColumn, categoryIndex);
                            int refCat = getReferenceCat(cat, pValueColumn, categoryIndex, individualIndex);
                            if ((refHV != hv || refCat != cat) && vCount[refHV][refCat] != 0) {
                                if (mBoxplotShowFoldChange) {
                                    if (mTableModel.isLogarithmicViewMode(mAxisIndex[boxPlotInfo.barAxis]))
                                        boxPlotInfo.foldChange[hv][cat] = 3.321928094887363f
                                                * (boxPlotInfo.barValue[hv][cat]
                                                        - boxPlotInfo.barValue[refHV][refCat]); // this is the log2(fc)
                                    else
                                        boxPlotInfo.foldChange[hv][cat] = boxPlotInfo.barValue[hv][cat]
                                                / boxPlotInfo.barValue[refHV][refCat];
                                }
                                if (mBoxplotShowPValue) {
                                    try {
                                        boxPlotInfo.pValue[hv][cat] = (float) new TTestImpl().tTest(value[hv][cat],
                                                value[refHV][refCat]);
                                    } catch (IllegalArgumentException e) {
                                        boxPlotInfo.pValue[hv][cat] = Float.NaN;
                                    } catch (MathException e) {
                                        boxPlotInfo.pValue[hv][cat] = Float.NaN;
                                    }
                                }
                            } else {
                                if (mBoxplotShowFoldChange)
                                    boxPlotInfo.foldChange[hv][cat] = Float.NaN;
                                if (mBoxplotShowPValue)
                                    boxPlotInfo.pValue[hv][cat] = Float.NaN;
                            }
                        }
                    }
                }
            }
        }

        // for this chart type we need the statistical parameters, but no colored bar
        if (mChartType == cChartTypeWhiskerPlot)
            return;

        // create color list
        for (int i = 0; i < colorList.length; i++)
            boxPlotInfo.color[i] = colorList[i];
        boxPlotInfo.color[colorList.length] = VisualizationColor.cSelectedColor;
        if (focusFlagNo != -1) {
            for (int i = 0; i < colorList.length; i++)
                boxPlotInfo.color[i + basicColorCount] = VisualizationColor.grayOutColor(colorList[i]);
            boxPlotInfo.color[colorList.length + basicColorCount] = VisualizationColor
                    .grayOutColor(VisualizationColor.cSelectedColor);
        }

        boxPlotInfo.outlierCount = new int[mHVCount][catCount];

        for (int i = 0; i < mDataPoints; i++) {
            if (isVisibleExcludeNaN(mPoint[i])) {
                int cat = getChartCategoryIndex(mPoint[i]);
                int hv = mPoint[i].hvIndex;
                if (boxPlotInfo.isOutsideValue(hv, cat, getValue(mPoint[i].record, boxPlotInfo.barAxis))) {
                    boxPlotInfo.outlierCount[hv][cat]++;
                } else {
                    int colorIndex = ((mPoint[i].record.getFlags() & CompoundRecord.cFlagMaskSelected) != 0
                            && mFocusHitlist != cFocusOnSelection) ? colorList.length : mPoint[i].colorIndex;
                    if (focusFlagNo != -1 && !mPoint[i].record.isFlagSet(focusFlagNo))
                        colorIndex += basicColorCount;

                    boxPlotInfo.pointsInCategory[hv][cat]++;
                    boxPlotInfo.pointsInColorCategory[hv][cat][colorIndex]++;
                }
            }
        }

        int[][][] count = new int[mHVCount][catCount][colorCount];
        for (int hv = 0; hv < mHVCount; hv++)
            for (int cat = 0; cat < catCount; cat++)
                for (int color = 1; color < colorCount; color++)
                    count[hv][cat][color] = count[hv][cat][color - 1]
                            + boxPlotInfo.pointsInColorCategory[hv][cat][color - 1];
        for (int i = 0; i < mDataPoints; i++) {
            if (isVisible(mPoint[i])) {
                float v = getValue(mPoint[i].record, boxPlotInfo.barAxis);
                if (Float.isNaN(v)) {
                    mPoint[i].chartGroupIndex = -1;
                } else {
                    int hv = mPoint[i].hvIndex;
                    int cat = getChartCategoryIndex(mPoint[i]);
                    if (boxPlotInfo.isOutsideValue(hv, cat, v)) {
                        mPoint[i].chartGroupIndex = -1;
                    } else {
                        CompoundRecord record = mPoint[i].record;
                        int colorIndex = (record.isSelected() && mFocusHitlist != cFocusOnSelection)
                                ? colorList.length
                                : mPoint[i].colorIndex;
                        if (focusFlagNo != -1 && !record.isFlagSet(focusFlagNo))
                            colorIndex += basicColorCount;

                        mPoint[i].chartGroupIndex = count[hv][cat][colorIndex];
                        count[hv][cat][colorIndex]++;
                    }
                }
            }
        }
    }

    public String getStatisticalValues() {
        if (mChartType == cChartTypeScatterPlot)
            return "Incompatible chart type.";

        String[][] categoryList = new String[6][];
        int[] categoryColumn = new int[6];
        int categoryColumnCount = 0;

        if (isSplitView()) {
            for (int i = 0; i < 2; i++) {
                if (mSplittingColumn[i] != cColumnUnassigned) {
                    categoryColumn[categoryColumnCount] = mSplittingColumn[i];
                    categoryList[categoryColumnCount] = mTableModel.getCategoryList(mSplittingColumn[i]);
                    categoryColumnCount++;
                }
            }
        }
        for (int axis = 0; axis < mDimensions; axis++) {
            if (mIsCategoryAxis[axis]) {
                categoryColumn[categoryColumnCount] = mAxisIndex[axis];
                categoryList[categoryColumnCount] = new String[mCategoryVisibleCount[axis]];
                if (mAxisIndex[axis] == cColumnUnassigned) { // box/whisker plot with unassigned category axis
                    categoryList[categoryColumnCount][0] = "<All Rows>";
                } else {
                    String[] list = mTableModel.getCategoryList(categoryColumn[categoryColumnCount]);
                    for (int j = 0; j < mCategoryVisibleCount[axis]; j++)
                        categoryList[categoryColumnCount][j] = list[mCategoryMin[axis] + j];
                }
                categoryColumnCount++;
            }
        }
        if (isCaseSeparationDone()) {
            categoryColumn[categoryColumnCount] = mCaseSeparationColumn;
            categoryList[categoryColumnCount] = mTableModel.getCategoryList(mCaseSeparationColumn);
            categoryColumnCount++;
        }

        boolean includePValue = false;
        boolean includeFoldChange = false;
        int pValueColumn = getPValueColumn();
        int referenceCategoryIndex = -1;
        if (pValueColumn != cColumnUnassigned) {
            referenceCategoryIndex = getCategoryIndex(pValueColumn, mPValueRefCategory);
            if (referenceCategoryIndex != -1) {
                includePValue = mBoxplotShowPValue;
                includeFoldChange = mBoxplotShowFoldChange;
            }
        }

        StringWriter stringWriter = new StringWriter(1024);
        BufferedWriter writer = new BufferedWriter(stringWriter);

        try {
            // construct the title line
            for (int i = 0; i < categoryColumnCount; i++) {
                String columnTitle = (categoryColumn[i] == -1) ? "Category"
                        : mTableModel.getColumnTitleWithSpecialType(categoryColumn[i]);
                writer.append(columnTitle + "\t");
            }

            if (mChartType != cChartTypeBoxPlot)
                writer.append("Rows in Category");

            if ((mChartType == cChartTypeBars || mChartType == cChartTypePies) && mChartMode != cChartModeCount) {
                String name = mTableModel.getColumnTitleWithSpecialType(mChartColumn);
                writer.append((mChartMode == cChartModePercent) ? "\tPercent of Rows"
                        : (mChartMode == cChartModeMean) ? "\tMean of " + name
                                : (mChartMode == cChartModeMean) ? "\tSum of " + name
                                        : (mChartMode == cChartModeMin) ? "\tMinimum of " + name
                                                : (mChartMode == cChartModeMax) ? "\tMaximum of " + name : "");
            }

            if (mChartType == cChartTypeBoxPlot || mChartType == cChartTypeWhiskerPlot) {
                if (mChartType == cChartTypeBoxPlot) {
                    writer.append("Total Count");
                    writer.append("\tOutlier Count");
                }
                writer.append("\tMean Value");
                writer.append("\t1st Quartile");
                writer.append("\tMedian");
                writer.append("\t3rd Quartile");
                writer.append("\tLower Adjacent Limit");
                writer.append("\tUpper Adjacent Limit");
                /*            if (includeFoldChange || includePValue)      don't use additional column
                               writer.append("\tIs Reference Group");   */
                if (includeFoldChange)
                    writer.append(
                            mTableModel.isLogarithmicViewMode(mAxisIndex[((BoxPlotViewInfo) mChartInfo).barAxis])
                                    ? "\tlog2(Fold Change)"
                                    : "\tFold Change");
                if (includePValue)
                    writer.append("\tp-Value");
            }
            writer.newLine();

            int[] categoryIndex = new int[6];
            while (categoryIndex[0] < categoryList[0].length) {
                int columnIndex = 0;
                int hv = 0;
                if (isSplitView()) {
                    if (mSplittingColumn[0] != cColumnUnassigned)
                        hv += categoryIndex[columnIndex++];
                    if (mSplittingColumn[1] != cColumnUnassigned)
                        hv += categoryIndex[columnIndex++] * categoryList[0].length;
                }

                int cat = 0;
                for (int axis = 0; axis < mDimensions; axis++)
                    if (mIsCategoryAxis[axis])
                        cat += categoryIndex[columnIndex++] * mCombinedCategoryCount[axis];

                if (isCaseSeparationDone())
                    cat += categoryIndex[columnIndex++];

                if (mChartInfo.pointsInCategory[hv][cat] != 0) {
                    for (int i = 0; i < categoryColumnCount; i++) {
                        writer.append(categoryList[i][categoryIndex[i]]);
                        if ((includeFoldChange || includePValue) && pValueColumn == categoryColumn[i]
                                && mPValueRefCategory.equals(categoryList[i][categoryIndex[i]]))
                            writer.append(" (ref)");
                        writer.append("\t");
                    }

                    if (mChartType != cChartTypeBoxPlot)
                        writer.append("" + mChartInfo.pointsInCategory[hv][cat]);

                    if ((mChartType == cChartTypeBars || mChartType == cChartTypePies)
                            && mChartMode != cChartModeCount)
                        writer.append("\t" + formatValue(mChartInfo.barValue[hv][cat], mChartColumn));

                    if (mChartType == cChartTypeBoxPlot || mChartType == cChartTypeWhiskerPlot) {
                        BoxPlotViewInfo vi = (BoxPlotViewInfo) mChartInfo;
                        if (mChartType == cChartTypeBoxPlot) {
                            writer.append("" + (mChartInfo.pointsInCategory[hv][cat] + vi.outlierCount[hv][cat]));
                            writer.append("\t" + vi.outlierCount[hv][cat]);
                        }
                        int column = mAxisIndex[((BoxPlotViewInfo) mChartInfo).barAxis];
                        writer.append("\t" + formatValue(vi.barValue[hv][cat], column));
                        writer.append("\t" + formatValue(vi.boxQ1[hv][cat], column));
                        writer.append("\t" + formatValue(vi.median[hv][cat], column));
                        writer.append("\t" + formatValue(vi.boxQ3[hv][cat], column));
                        writer.append("\t" + formatValue(vi.boxLAV[hv][cat], column));
                        writer.append("\t" + formatValue(vi.boxUAV[hv][cat], column));
                        /*                  if (includeFoldChange || includePValue) {      don't use additional column
                                             int refHV = getReferenceHV(hv, pValueColumn, referenceCategoryIndex);
                                             int refCat = getReferenceCat(cat, pValueColumn, referenceCategoryIndex, new int[1+mDimensions]);
                                             writer.append("\t"+((hv==refHV && cat==refCat) ? "yes" : "no"));
                                             }   */
                        if (includeFoldChange) {
                            writer.append("\t");
                            if (!Float.isNaN(vi.foldChange[hv][cat]))
                                writer.append(new DecimalFormat("#.#####").format(vi.foldChange[hv][cat]));
                        }
                        if (includePValue) {
                            writer.append("\t");
                            if (!Float.isNaN(vi.pValue[hv][cat]))
                                writer.append(new DecimalFormat("#.#####").format(vi.pValue[hv][cat]));
                        }
                    }
                    writer.newLine();
                }

                // update category indices for next row
                for (int i = categoryColumnCount - 1; i >= 0; i--) {
                    if (++categoryIndex[i] < categoryList[i].length || i == 0)
                        break;

                    categoryIndex[i] = 0;
                }
            }
            writer.close();
        } catch (IOException ioe) {
        }

        return stringWriter.toString();
    }

    /**
     * Formats a numerical value for displaying it.
     * This includes proper rounding and potential de-logarithmization of the original value.
     * @param value is the logarithm, if the column is in logarithmic view mode
     * @param column the column this value refers to
     * @return
     */
    protected String formatValue(float value, int column) {
        if (mTableModel.isLogarithmicViewMode(column) && !Float.isNaN(value) && !Float.isInfinite(value))
            value = (float) Math.pow(10, value);
        return DoubleFormat.toString(value);
    }

    /**
     * Returns the correct value to apply, when positioning a VisualizationPoint
     * on an axis. This method resolves whether we have a dynamic value (e.g. from
     * a descriptor similarity calculation) or a static value from the CompoundRecord.
     * With ambiguous column types (category and double) it also considers, whether
     * to use the category index or the double value.
     * @param vp
     * @param axis
     * @return
     */
    protected float getValue(CompoundRecord record, int axis) {
        int column = mAxisIndex[axis];
        return mTableModel.isDescriptorColumn(column)
                ? (mAxisSimilarity[axis] == null ? 0.5f : mAxisSimilarity[axis][record.getID()])
                : (mIsCategoryAxis[axis]) ? mTableModel.getCategoryIndex(column, record) : record.getDouble(column);
    }

    protected TreeMap<byte[], VisualizationPoint> createReferenceMap(int referencingColumn, int referencedColumn) {
        // create list of referencing keys
        TreeSet<byte[]> set = new TreeSet<byte[]>(new ByteArrayComparator());
        for (VisualizationPoint vp : mPoint) {
            byte[] data = (byte[]) vp.record.getData(referencingColumn);
            if (data != null)
                for (String ref : mTableModel.separateEntries(new String(data)))
                    set.add(ref.getBytes());
        }

        // create map of existing and referenced VisualizationPoints
        TreeMap<byte[], VisualizationPoint> map = new TreeMap<byte[], VisualizationPoint>(
                new ByteArrayComparator());
        for (VisualizationPoint vp : mPoint) {
            byte[] key = (byte[]) vp.record.getData(referencedColumn);
            if (set.contains(key))
                map.put(key, vp);
        }

        return map;
    }

    public boolean getShowNaNValues() {
        return mShowNaNValues;
    }

    public void setShowNaNValues(boolean b) {
        if (mShowNaNValues != b) {
            mShowNaNValues = b;
            applyLocalExclusion(false);
            invalidateOffImage(true);
        }
    }

    public boolean isGridSuppressed() {
        return mSuppressGrid;
    }

    public void setSuppressGrid(boolean hideGrid) {
        if (mSuppressGrid != hideGrid) {
            mSuppressGrid = hideGrid;
            invalidateOffImage(false);
        }
    }

    public int getScaleMode() {
        return mScaleMode;
    }

    public void setScaleMode(int scaleMode) {
        if (mScaleMode != scaleMode) {
            mScaleMode = scaleMode;
            invalidateOffImage(true);
        }
    }

    public int getConnectionColumn() {
        // filter out connection types being incompatible with chart type
        if (mChartType == cChartTypeBoxPlot || mChartType == cChartTypeWhiskerPlot) {
            if (mConnectionColumn == cConnectionColumnConnectCases
                    || (mCaseSeparationCategoryCount != 1 && mConnectionColumn == mCaseSeparationColumn))
                return mConnectionColumn;
            for (int i = 0; i < mDimensions; i++)
                if (mConnectionColumn == mAxisIndex[i])
                    return mConnectionColumn;
            return cColumnUnassigned;
        } else {
            return mConnectionColumn != cConnectionColumnConnectCases ? mConnectionColumn : cColumnUnassigned;
        }
    }

    public int getConnectionOrderColumn() {
        return (mConnectionColumn == cColumnUnassigned || mChartType == cChartTypeBoxPlot
                || mChartType == cChartTypeWhiskerPlot) ? cColumnUnassigned : mConnectionOrderColumn;
    }

    public void setConnectionColumns(int column, int orderColumn) {
        if (column != cColumnUnassigned && column != cConnectionColumnConnectAll
                && column != cConnectionColumnConnectCases && !mTableModel.isColumnTypeCategory(column)
                && mTableModel.getColumnProperty(column,
                        CompoundTableConstants.cColumnPropertyReferencedColumn) == null)
            column = cColumnUnassigned;

        if (column == cColumnUnassigned || column == cConnectionColumnConnectCases || (column >= 0 && mTableModel
                .getColumnProperty(column, CompoundTableConstants.cColumnPropertyReferencedColumn) != null))
            orderColumn = cColumnUnassigned;

        if (mConnectionColumn != column || mConnectionOrderColumn != orderColumn) {
            invalidateConnectionLines();
            mConnectionColumn = column;
            mConnectionOrderColumn = orderColumn;
            invalidateOffImage(false);
            updateTreeViewGraph();
        }
    }

    public void setConnectionLineWidth(float width, boolean isAdjusting) {
        width = Math.min(4f, width);
        if (mRelativeConnectionLineWidth != width) {
            mRelativeConnectionLineWidth = width;
            invalidateOffImage(false);
        }
    }

    public float getConnectionLineWidth() {
        return mRelativeConnectionLineWidth;
    }

    private void invalidateConnectionLines() {
        mConnectionLinePoint = null;
        mConnectionLineMap = null;
    }

    public void setMarkerSize(float size, boolean isAdjusting) {
        if (mRelativeMarkerSize != size) {
            mRelativeMarkerSize = size;

            // if no connection lines are drawn then keep line width synchronized with marker size for potential use
            if (mConnectionColumn == cColumnUnassigned)
                mRelativeConnectionLineWidth = mRelativeMarkerSize;

            invalidateOffImage(true);
        }
    }

    public void setMarkerSizeColumn(int column) {
        if (mMarkerSizeColumn != column) {
            mMarkerSizeColumn = column;
            updateSimilarityMarkerSizes(-1);
            invalidateOffImage(true);
        }
    }

    public void setMarkerSizeInversion(boolean inversion) {
        if (mMarkerSizeInversion != inversion) {
            mMarkerSizeInversion = inversion;
            invalidateOffImage(false);
        }
    }

    public void setMarkerSizeProportional(boolean proportional) {
        if (mMarkerSizeProportional != proportional) {
            mMarkerSizeProportional = proportional;
            invalidateOffImage(false);
        }
    }

    public void setMarkerSizeZoomAdaption(boolean adapt) {
        if (Float.isNaN(mMarkerSizeZoomAdaption) == adapt) {
            if (adapt)
                mMarkerSizeZoomAdaption = calculateZoomState();
            else
                mMarkerSizeZoomAdaption = Float.NaN;
            invalidateOffImage(false);
        }
    }

    public void setMarkerLabelSize(float size, boolean isAdjusting) {
        if (mMarkerLabelSize != size) {
            mMarkerLabelSize = size;
            if (showAnyLabels()) {
                invalidateOffImage(false);
            }
        }
    }

    public void setMarkerShapeColumn(int column) {
        if (column >= 0 && (!mTableModel.isColumnTypeCategory(column)
                || mTableModel.getCategoryCount(column) > getAvailableShapeCount()))
            column = cColumnUnassigned;

        if (mMarkerShapeColumn != column) {
            mMarkerShapeColumn = column;
            updateShapeIndices();
        }
    }

    private void updateShapeIndices() {
        if (mMarkerShapeColumn == cColumnUnassigned)
            for (int i = 0; i < mDataPoints; i++)
                mPoint[i].shape = 0;
        else if (CompoundTableHitlistHandler.isHitlistColumn(mMarkerShapeColumn)) {
            int flagNo = mTableModel.getHitlistHandler()
                    .getHitlistFlagNo(CompoundTableHitlistHandler.getHitlistFromColumn(mMarkerShapeColumn));
            for (int i = 0; i < mDataPoints; i++)
                mPoint[i].shape = (byte) (mPoint[i].record.isFlagSet(flagNo) ? 0 : 1);
        } else {
            for (int i = 0; i < mDataPoints; i++)
                mPoint[i].shape = (byte) mTableModel.getCategoryIndex(mMarkerShapeColumn, mPoint[i].record);
        }

        invalidateOffImage(true);
    }

    public void setMarkerLabelsInTreeViewOnly(boolean inTreeViewOnly) {
        mLabelsInTreeViewOnly = inTreeViewOnly;
        invalidateOffImage(false);
    }

    public void setMarkerLabels(int[] columnAtPosition) {
        mLabelColumn = columnAtPosition;
        invalidateOffImage(false);
    }

    protected boolean showAnyLabels() {
        if (!mLabelsInTreeViewOnly || isTreeViewGraph())
            for (int i = 0; i < mLabelColumn.length; i++)
                if (mLabelColumn[i] != cColumnUnassigned)
                    return true;

        return false;
    }

    public int getMarkerLabelColumn(int position) {
        return mLabelColumn[position];
    }

    public int getColumnIndex(int axis) {
        return mAxisIndex[axis];
    }

    /**
     * Assigns the axis to the specified column or cColumnUnassigned.
     * The chart type is updated and the visible range set to the maximum.
     * Local record hiding of this axis is initialized and applied to the global
     * exclusion.
     * @param axis
     * @param index
     */
    public void setColumnIndex(int axis, int index) {
        if (mAxisIndex[axis] != index) {
            mAxisIndex[axis] = index;
            initializeAxis(axis);
            int exclusionNeeeds = (EXCLUSION_FLAG_NAN_0 << axis) | determineChartType();
            validateExclusion(exclusionNeeeds);
        }
    }

    public abstract int[] getSupportedChartTypes();

    public int getChartType() {
        return mChartType;
    }

    public int getPreferredChartType() {
        return mPreferredChartType;
    }

    public int getPreferredChartMode() {
        return mChartMode;
    }

    public int getPreferredChartColumn() {
        return mChartColumn;
    }

    public void setPreferredChartType(int type, int mode, int column) {
        if (mode == -1)
            mode = cChartModeCount;
        if (mode != cChartModeCount && mode != cChartModePercent && column == cColumnUnassigned)
            mode = cChartModeCount;
        if (mPreferredChartType != type || mChartColumn != column || mChartMode != mode) {
            mChartColumn = column;
            mPreferredChartType = type;
            mChartMode = mode;
            int exclusionNeeeds = determineChartType();
            validateExclusion(exclusionNeeeds);
            updateTreeViewGraph();
            invalidateOffImage(true);
        }
    }

    private int getReferenceHV(int hv, int categoryColumn, int categoryIndex) {
        if (!isSplitView())
            return 0;

        if (mSplittingColumn[1] == cColumnUnassigned)
            return (mSplittingColumn[0] == categoryColumn) ? categoryIndex : hv;

        int categoryCount = mTableModel.getCategoryCount(mSplittingColumn[0]);
        if (mSplittingColumn[0] == categoryColumn) {
            int index2 = hv / categoryCount;
            return categoryIndex + index2 * categoryCount;
        } else {
            int index1 = hv % categoryCount;
            return index1 + categoryIndex * categoryCount;
        }
    }

    private int getReferenceCat(int cat, int categoryColumn, int categoryIndex, int[] individualIndex) {
        for (int i = mDimensions; i > 0; i--) {
            individualIndex[i] = cat / mCombinedCategoryCount[i - 1];
            cat -= individualIndex[i] * mCombinedCategoryCount[i - 1];
        }
        individualIndex[0] = cat;

        if (mCaseSeparationCategoryCount != 1 && categoryColumn == mCaseSeparationColumn) {
            individualIndex[0] = categoryIndex;
        } else {
            for (int axis = 0; axis < mDimensions; axis++) {
                if (categoryColumn == mAxisIndex[axis]) {
                    individualIndex[axis + 1] = categoryIndex;
                    break;
                }
            }
        }

        int index = individualIndex[0];
        for (int axis = 0; axis < mDimensions; axis++)
            index += individualIndex[axis + 1] * mCombinedCategoryCount[axis];

        return index;
    }

    /**
     * Returns the category index on the axis, i.e. the visible(!) category list
     * index of the visualization point. If the row belong to a category being
     * zoomed out of the view, then -1 is returned.
     * @param axis
     * @param vp
     * @return visible category index or -1
     */
    protected int getCategoryIndex(int axis, VisualizationPoint vp) {
        if (mAxisIndex[axis] == cColumnUnassigned)
            return 0;

        int index = mTableModel.getCategoryIndex(mAxisIndex[axis], vp.record);

        return (index >= mCategoryMin[axis] && index < mCategoryMax[axis]) ? index - mCategoryMin[axis] : -1;
    }

    /**
     * Returns the index of value in the column's visible(!!!) category list.
     * If the column is shown on an axis and if this axis is zoomed in, then
     * this category index differs from the one of the CompoundTableModel.
     * @param column
     * @param value
     * @return category index or -1 if the category is scrolled out of view
     */
    private int getCategoryIndex(int column, String value) {
        if (column == mCaseSeparationColumn)
            return mTableModel.getCategoryIndex(column, value);

        if (column == mSplittingColumn[0] || column == mSplittingColumn[1])
            return mSplitViewCountExceeded ? 0 : mTableModel.getCategoryIndex(column, value);

        int axis = -1;
        for (int i = 0; i < mDimensions; i++) {
            if (mAxisIndex[i] == column) {
                axis = i;
                break;
            }
        }

        int index = mTableModel.getCategoryIndex(column, value);

        return (index >= mCategoryMin[axis] && index < mCategoryMax[axis]) ? index - mCategoryMin[axis] : -1;
    }

    /**
     * This method requires a valid chart type, which may be achieved by calling determineChartBasics()
     * @param axis
     * @return whether the data of the column assigned to this axis is considered category data
     */
    public boolean isCategoryAxis(int axis) {
        return mIsCategoryAxis[axis];
    }

    /**
     * Calculates one combined category index for VisualizationPoint
     * that includes individual categories from case separation and axis
     * categories. If the vp is zoomed out of the view, then -1 is returned.
     * @param vp
     * @return
     */
    protected int getChartCategoryIndex(VisualizationPoint vp) {
        int index = (mCombinedCategoryCount[0] == 1) ? 0
                : mTableModel.getCategoryIndex(mCaseSeparationColumn, vp.record);

        for (int axis = 0; axis < mDimensions; axis++) {
            if ((mChartType != cChartTypeBoxPlot && mChartType != cChartTypeWhiskerPlot)
                    || axis != mChartInfo.barAxis) {
                int axisIndex = getCategoryIndex(axis, vp);
                if (axisIndex == -1)
                    return -1;

                index += axisIndex * mCombinedCategoryCount[axis];
            }
        }

        return index;
    }

    protected float getPercentile(double[] value, double cutoff) {
        int index = (int) (cutoff * ((float) value.length - 0.999999));
        float percentile = (float) value[index];
        if (0.0001 + index < cutoff * (value.length - 1))
            percentile += cutoff * (value[index + 1] - value[index]);
        return percentile;
    }

    public void mouseClicked(MouseEvent e) {
        if ((e.getModifiers() & InputEvent.BUTTON1_MASK) != 0) {
            VisualizationPoint marker = findMarker(e.getX(), e.getY());
            if (mActivePoint != marker) {
                // don't allow root de-selection if we are in a dedicated tree view
                boolean isPureTreeView = isTreeViewGraph() && !mTreeViewShowAll;
                if (marker != null || !isPureTreeView)
                    mTableModel.setActiveRow(marker == null ? null : marker.record);
            }
        }
    }

    /**
     * This is the default implementation of locating a marker from screen coordinates.
     * It assumes a rectangular marker shape and relies on the getDistanceToMarker().
     * For complex marker shapes overwrite this method or getDistanceToMarker().
     * @param x
     * @param y
     * @return
     */
    public VisualizationPoint findMarker(int x, int y) {
        // inverted order to prefer markers that are in the front
        VisualizationPoint p = null;
        int minDistance = Integer.MAX_VALUE;
        for (int i = mDataPoints - 1; i >= 0; i--) {
            if (isVisible(mPoint[i])) {
                int dvp = getDistanceToMarker(mPoint[i], x, y);
                if (dvp == 0)
                    return mPoint[i];
                if (dvp < 4 && dvp < minDistance) {
                    p = mPoint[i];
                    dvp = minDistance;
                }
            }
        }

        return p;
    }

    /**
     * This method assumes a rectangular marker shape and uses the
     * VisualizationPount's width and height values.
     * May be overwritten to support complex marker shapes.
     * @param vp
     * @param x
     * @param y
     * @return
     */
    public int getDistanceToMarker(VisualizationPoint vp, int x, int y) {
        int dx = Math.abs(vp.screenX - x) - Math.round(vp.width / 2.0f);
        int dy = Math.abs(vp.screenY - y) - Math.round(vp.height / 2.0f);
        return Math.max(0, Math.max(dx, dy));
    }

    public void mousePressed(MouseEvent e) {
        mMouseX1 = mMouseX2 = e.getX();
        mMouseY1 = mMouseY2 = e.getY();
        mMouseIsDown = true;

        if (System.getProperty("touch") != null) {
            new Thread() {
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ie) {
                    }

                    if (Math.abs(mMouseX2 - mMouseX1) < 5 && Math.abs(mMouseY2 - mMouseY1) < 5 && mMouseIsDown) {
                        SwingUtilities.invokeLater(new Runnable() {
                            public void run() {
                                activateTouchFunction();
                            }
                        });
                    }
                }
            }.start();
        }

        mRectangleSelecting = false;
        mLassoSelecting = false;
        if (!handlePopupTrigger(e) && (e.getModifiers() & InputEvent.BUTTON3_MASK) == 0) {
            mAddingToSelection = e.isShiftDown();
            if (e.isAltDown())
                mRectangleSelecting = true;
            else {
                mLassoSelecting = true;
                mLassoRegion = new Polygon();
                mLassoRegion.addPoint(mMouseX1, mMouseY1);
                mLassoRegion.addPoint(mMouseX1, mMouseY1);
            }
        }
    }

    public void mouseReleased(MouseEvent e) {
        mMouseIsDown = false;
        if (!handlePopupTrigger(e)) {
            if (mRectangleSelecting) {
                int mouseX1, mouseX2, mouseY1, mouseY2;

                if (mMouseX1 < mMouseX2) {
                    mouseX1 = mMouseX1;
                    mouseX2 = mMouseX2;
                } else {
                    mouseX1 = mMouseX2;
                    mouseX2 = mMouseX1;
                }

                if (mMouseY1 < mMouseY2) {
                    mouseY1 = mMouseY1;
                    mouseY2 = mMouseY2;
                } else {
                    mouseY1 = mMouseY2;
                    mouseY2 = mMouseY1;
                }

                for (int i = 0; i < mDataPoints; i++) {
                    if (mPoint[i].screenX >= mouseX1 && mPoint[i].screenX <= mouseX2 && mPoint[i].screenY >= mouseY1
                            && mPoint[i].screenY <= mouseY2 && isVisible(mPoint[i]))
                        mPoint[i].record.setSelection(true);
                    else if (!mAddingToSelection)
                        mPoint[i].record.setSelection(false);
                }

                mRectangleSelecting = false;
                mSelectionModel.invalidate();
            } else if (mLassoSelecting) {
                for (int i = 0; i < mDataPoints; i++) {
                    if (mLassoRegion.contains(mPoint[i].screenX, mPoint[i].screenY) && isVisible(mPoint[i]))
                        mPoint[i].record.setSelection(true);
                    else if (!mAddingToSelection)
                        mPoint[i].record.setSelection(false);
                }

                mLassoSelecting = false;
                mSelectionModel.invalidate();
            }
        }
        if (mTouchFunctionActive) {
            mTouchFunctionActive = false;
            repaint();
        }
    }

    private void activateTouchFunction() {
        if (!showPopupMenu()) {
            mTouchFunctionActive = true;
            repaint();
        }
    }

    protected boolean isTouchFunctionActive() {
        return mTouchFunctionActive;
    }

    private boolean handlePopupTrigger(MouseEvent e) {
        if (e.isPopupTrigger())
            showPopupMenu();

        return false;
    }

    private boolean showPopupMenu() {
        if (mDetailPopupProvider != null && allowPopupMenu()) {
            CompoundRecord record = (mHighlightedPoint == null) ? null : mHighlightedPoint.record;
            JPopupMenu popup = mDetailPopupProvider.createPopupMenu(record, (VisualizationPanel) getParent(), -1);
            if (popup != null) {
                popup.show(this, mMouseX1, mMouseY1);
                return true;
            }
        }

        return false;
    }

    /**
     * May be overridden to allow popup menus depending on current state,
     * e.g. on mCurrentHighlighted being null
     * @return true if popup menu shall be shown
     */
    public boolean allowPopupMenu() {
        return true;
    }

    public void mouseEntered(MouseEvent e) {
        mMouseIsDown = false;
    }

    public void mouseExited(MouseEvent e) {
        mMouseIsDown = false;
    }

    public void mouseMoved(MouseEvent e) {
        VisualizationPoint marker = findMarker(e.getX(), e.getY());
        if (mHighlightedPoint != marker)
            mTableModel.setHighlightedRow((marker == null) ? null : marker.record);
    }

    public void mouseDragged(MouseEvent e) {
        mMouseX2 = e.getX();
        mMouseY2 = e.getY();

        if (mRectangleSelecting) {
            repaint();
        } else if (mLassoSelecting) {
            if ((Math.abs(mMouseX2 - mLassoRegion.xpoints[mLassoRegion.npoints - 1]) > 3)
                    || (Math.abs(mMouseY2 - mLassoRegion.ypoints[mLassoRegion.npoints - 1]) > 3)) {
                mLassoRegion.npoints--;
                mLassoRegion.addPoint(mMouseX2, mMouseY2);
                mLassoRegion.addPoint(mMouseX1, mMouseY1);
            }

            repaint();
        }
    }

    @Override
    public Point getToolTipLocation(MouseEvent e) {
        VisualizationPoint vp = findMarker(e.getX(), e.getY());
        return (vp != null) ? new Point(vp.screenX, vp.screenY) : null;
    }

    @Override
    public String getToolTipText(MouseEvent e) {
        VisualizationPoint vp = findMarker(e.getX(), e.getY());
        if (vp == null)
            return null;
        StringBuilder sb = new StringBuilder();
        for (int axis = 0; axis < mDimensions; axis++)
            addTooltipRow(vp.record, mAxisIndex[axis], mAxisSimilarity[axis], sb);

        addMarkerTooltips(vp, sb);

        sb.append("</html>");
        return sb.toString();
    }

    protected void addMarkerTooltips(VisualizationPoint vp, StringBuilder sb) {
        addTooltipRow(vp.record, mMarkerColor.getColorColumn(), null, sb);
        addTooltipRow(vp.record, mMarkerSizeColumn, null, sb);
        addTooltipRow(vp.record, mMarkerShapeColumn, null, sb);
    }

    protected void addTooltipRow(CompoundRecord record, int column, float[] similarity, StringBuilder sb) {
        if (column != cColumnUnassigned) {
            String title = null;
            String value = null;
            if (CompoundTableHitlistHandler.isHitlistColumn(column)) {
                int hitlistIndex = CompoundTableHitlistHandler.getHitlistFromColumn(column);
                int flagNo = mTableModel.getHitlistHandler().getHitlistFlagNo(hitlistIndex);
                title = record.isFlagSet(flagNo) ? "Member of '" : "Not member of '";
                value = mTableModel.getHitlistHandler().getHitlistName(hitlistIndex);
            } else {
                title = getAxisTitle(column) + ": ";
                if (mTableModel.isDescriptorColumn(column)) {
                    if (similarity != null)
                        value = DoubleFormat.toString(similarity[record.getID()]);
                    else
                        value = DoubleFormat.toString((mActivePoint == null) ? Double.NaN
                                : mTableModel.getDescriptorSimilarity(mActivePoint.record, record, column));
                } else if (mTableModel.getColumnSpecialType(column) != null) {
                    int idColumn = mTableModel.findColumn(mTableModel.getColumnProperty(column,
                            CompoundTableConstants.cColumnPropertyIdentifierColumn));
                    if (idColumn != -1)
                        value = mTableModel.encodeData(record, idColumn);
                } else {
                    value = mTableModel.encodeData(record, column);
                }
            }
            if (value != null) {
                sb.append((sb.length() == 0) ? "<html>" : "<br>");
                sb.append(title);
                sb.append(value);
            }
        }
    }

    public void compoundTableChanged(CompoundTableEvent e) {
        boolean needsUpdate = false;
        int exclusionNeeds = 0;

        if (e.getType() == CompoundTableEvent.cChangeExcluded) {
            mVisibleCategoryFromCategory = null;
            if ((mSplittingColumn[0] >= 0 || mSplittingColumn[1] >= 0) && !mShowEmptyInSplitView)
                invalidateSplittingIndices();
            if (mTreeViewIsDynamic && mTreeNodeList != null) {
                updateTreeViewGraph();
            }
        } else if (e.getType() == CompoundTableEvent.cChangeColumnData) {
            int column = e.getColumn();
            if (mVisibleCategoryFromCategory != null)
                mVisibleCategoryFromCategory[column] = null;

            for (int axis = 0; axis < mDimensions; axis++) {
                if (column == mAxisIndex[axis]) {
                    if (mTableModel.isDescriptorColumn(column))
                        setSimilarityValues(axis, e.getSpecifier());

                    exclusionNeeds = ((EXCLUSION_FLAG_NAN_0 | EXCLUSION_FLAG_ZOOM_0) << axis);
                    needsUpdate = true;
                }
            }
            if (mMarkerSizeColumn == column) {
                updateSimilarityMarkerSizes(e.getSpecifier());
                needsUpdate = true;
            }
            if (mMarkerShapeColumn == column) {
                if (!mTableModel.isColumnTypeCategory(column)
                        || mTableModel.getCategoryCount(column) > getAvailableShapeCount())
                    mMarkerShapeColumn = cColumnUnassigned;
                updateShapeIndices();
                needsUpdate = true;
            }
            if (mCaseSeparationColumn == column) {
                if (!mTableModel.isColumnTypeCategory(column)
                        || mTableModel.getCategoryCount(column) > cMaxCaseSeparationCategoryCount)
                    mCaseSeparationColumn = cColumnUnassigned;
                needsUpdate = true;
            }
            if (mSplittingColumn[0] == column || mSplittingColumn[1] == column) {
                if (!mTableModel.isColumnTypeCategory(column)) {
                    if (mSplittingColumn[0] == column) {
                        mSplittingColumn[0] = mSplittingColumn[1];
                        mSplittingColumn[1] = cColumnUnassigned;
                    } else {
                        mSplittingColumn[1] = cColumnUnassigned;
                    }
                }

                invalidateSplittingIndices();
            }
            for (int i = 0; i < mLabelColumn.length; i++) {
                if (mLabelColumn[i] == column)
                    needsUpdate = true;
            }
            if (mConnectionColumn == column || mConnectionOrderColumn == column) {
                invalidateConnectionLines();
                needsUpdate = true;
            }
        } else if (e.getType() == CompoundTableEvent.cAddRows || e.getType() == CompoundTableEvent.cDeleteRows) {
            initializeDataPoints();
            mVisibleCategoryFromCategory = null;
            for (int axis = 0; axis < mDimensions; axis++) {
                int column = mAxisIndex[axis];
                if (column != cColumnUnassigned) {
                    if (mTableModel.isDescriptorColumn(column))
                        setSimilarityValues(axis, -1);

                    exclusionNeeds |= ((EXCLUSION_FLAG_NAN_0 | EXCLUSION_FLAG_ZOOM_0) << axis);
                }
            }

            updateSimilarityMarkerSizes(-1);
            if (mMarkerShapeColumn >= 0) { // if not is unassigned or hitlist
                if (!mTableModel.isColumnTypeCategory(mMarkerShapeColumn))
                    mMarkerShapeColumn = cColumnUnassigned;
                updateShapeIndices();
            }
            if (mCaseSeparationColumn >= 0) {
                if (!mTableModel.isColumnTypeCategory(mCaseSeparationColumn)
                        || mTableModel.getCategoryCount(mCaseSeparationColumn) > cMaxCaseSeparationCategoryCount)
                    mCaseSeparationColumn = cColumnUnassigned;
                needsUpdate = true;
            }
            if (mSplittingColumn[0] >= 0 || mSplittingColumn[1] >= 0) { // if not is unassigned or hitlist
                if (mSplittingColumn[0] >= 0 && !mTableModel.isColumnTypeCategory(mSplittingColumn[0])) {
                    mSplittingColumn[0] = mSplittingColumn[1];
                    mSplittingColumn[1] = cColumnUnassigned;
                }
                if (mSplittingColumn[1] >= 0 && !mTableModel.isColumnTypeCategory(mSplittingColumn[1])) {
                    mSplittingColumn[1] = cColumnUnassigned;
                }
                invalidateSplittingIndices();
            }
            invalidateConnectionLines();
            needsUpdate = true;
        } else if (e.getType() == CompoundTableEvent.cAddColumns) {
            if (mVisibleCategoryFromCategory != null) {
                int[][] oldVisibleCategoryFromCategory = mVisibleCategoryFromCategory;
                mVisibleCategoryFromCategory = new int[mTableModel.getTotalColumnCount()][];
                for (int i = 0; i < oldVisibleCategoryFromCategory.length; i++)
                    mVisibleCategoryFromCategory[i] = oldVisibleCategoryFromCategory[i];
            }
        } else if (e.getType() == CompoundTableEvent.cRemoveColumns) {
            int[] columnMapping = e.getMapping();
            if (mVisibleCategoryFromCategory != null) {
                int[][] oldVisibleCategoryFromCategory = mVisibleCategoryFromCategory;
                mVisibleCategoryFromCategory = new int[mTableModel.getTotalColumnCount()][];
                for (int i = 0; i < columnMapping.length; i++)
                    if (columnMapping[i] != -1)
                        mVisibleCategoryFromCategory[columnMapping[i]] = oldVisibleCategoryFromCategory[i];
            }
            if (mMarkerSizeColumn >= 0) {
                mMarkerSizeColumn = columnMapping[mMarkerSizeColumn];
                if (mMarkerSizeColumn == cColumnUnassigned) {
                    mSizeLegend = null;
                    needsUpdate = true;
                }
            }
            if (mMarkerShapeColumn >= 0) {
                mMarkerShapeColumn = columnMapping[mMarkerShapeColumn];
                if (mMarkerShapeColumn == cColumnUnassigned) {
                    mShapeLegend = null;
                    updateShapeIndices();
                }
            }
            if (mCaseSeparationColumn >= 0) {
                mCaseSeparationColumn = columnMapping[mCaseSeparationColumn];
                if (mCaseSeparationColumn == cColumnUnassigned) {
                    needsUpdate = true;
                }
            }
            if (mSplittingColumn[0] >= 0) {
                mSplittingColumn[0] = columnMapping[mSplittingColumn[0]];
                boolean updateSplitting = (mSplittingColumn[0] == cColumnUnassigned);
                if (mSplittingColumn[1] >= 0) {
                    mSplittingColumn[1] = columnMapping[mSplittingColumn[1]];
                    if (mSplittingColumn[1] == cColumnUnassigned)
                        updateSplitting = true;
                }
                if (mSplittingColumn[0] == cColumnUnassigned && mSplittingColumn[1] != -1) {
                    mSplittingColumn[0] = mSplittingColumn[1];
                    mSplittingColumn[1] = cColumnUnassigned;
                }
                if (updateSplitting) {
                    invalidateSplittingIndices();
                    needsUpdate = true;
                }
            }
            if (mConnectionColumn >= 0) {
                mConnectionColumn = columnMapping[mConnectionColumn];
                if (mConnectionColumn == cColumnUnassigned) {
                    invalidateConnectionLines();
                    needsUpdate = true;
                }
            }
            if (mConnectionOrderColumn != cColumnUnassigned) {
                mConnectionOrderColumn = columnMapping[mConnectionOrderColumn];
                if (mConnectionOrderColumn == cColumnUnassigned) {
                    needsUpdate = true;
                }
            }
            for (int i = 0; i < mLabelColumn.length; i++) {
                if (mLabelColumn[i] >= 0) {
                    mLabelColumn[i] = columnMapping[mLabelColumn[i]];
                    if (mLabelColumn[i] == cColumnUnassigned)
                        needsUpdate = true;
                }
            }
            for (int axis = 0; axis < mDimensions; axis++) {
                if (mAxisIndex[axis] != cColumnUnassigned) {
                    mAxisIndex[axis] = columnMapping[mAxisIndex[axis]];
                    if (mAxisIndex[axis] == cColumnUnassigned) {
                        mAxisIndex[axis] = cColumnUnassigned;
                        needsUpdate = true;
                        initializeAxis(axis);
                        exclusionNeeds = (EXCLUSION_FLAG_NAN_0 << axis);
                    }
                }
            }
        } else if (e.getType() == CompoundTableEvent.cChangeActiveRow) {
            updateActiveRow();
            needsUpdate = true;
        } else if (e.getType() == CompoundTableEvent.cChangeColumnName) {
            invalidateOffImage(false);
        }

        if (mColorLegend != null)
            mColorLegend.compoundTableChanged(e);
        if (mSizeLegend != null)
            mSizeLegend.compoundTableChanged(e);
        if (mShapeLegend != null)
            mShapeLegend.compoundTableChanged(e);

        mMarkerColor.compoundTableChanged(e);

        validateExclusion(exclusionNeeds | determineChartType());

        if (needsUpdate)
            invalidateOffImage(true);
    }

    public void hitlistChanged(CompoundTableHitlistEvent e) {
        if (e.getType() == CompoundTableHitlistEvent.cDelete) {
            if (mFocusHitlist != cHitlistUnassigned) {
                if (mFocusHitlist == e.getHitlistIndex())
                    setFocusHitlist(cHitlistUnassigned);
                else if (mFocusHitlist > e.getHitlistIndex())
                    mFocusHitlist--;
            }
            if (CompoundTableHitlistHandler.isHitlistColumn(mMarkerSizeColumn)) {
                int hitlistIndex = CompoundTableHitlistHandler.getHitlistFromColumn(mMarkerSizeColumn);
                if (e.getHitlistIndex() == hitlistIndex) {
                    mMarkerSizeColumn = cColumnUnassigned;
                    invalidateOffImage(false);
                } else if (hitlistIndex > e.getHitlistIndex()) {
                    mMarkerSizeColumn = CompoundTableHitlistHandler.getColumnFromHitlist(hitlistIndex - 1);
                }
            }
            if (CompoundTableHitlistHandler.isHitlistColumn(mMarkerShapeColumn)) {
                int hitlistIndex = CompoundTableHitlistHandler.getHitlistFromColumn(mMarkerShapeColumn);
                if (e.getHitlistIndex() == hitlistIndex) {
                    mMarkerShapeColumn = cColumnUnassigned;
                    for (int i = 0; i < mDataPoints; i++)
                        mPoint[i].shape = 0;
                    invalidateOffImage(true);
                } else if (hitlistIndex > e.getHitlistIndex()) {
                    mMarkerShapeColumn = CompoundTableHitlistHandler.getColumnFromHitlist(hitlistIndex - 1);
                }
            }
            if (CompoundTableHitlistHandler.isHitlistColumn(mCaseSeparationColumn)) {
                int hitlistIndex = CompoundTableHitlistHandler.getHitlistFromColumn(mCaseSeparationColumn);
                if (e.getHitlistIndex() == hitlistIndex) {
                    mCaseSeparationColumn = cColumnUnassigned;
                    invalidateOffImage(true);
                } else if (hitlistIndex > e.getHitlistIndex()) {
                    mCaseSeparationColumn = CompoundTableHitlistHandler.getColumnFromHitlist(hitlistIndex - 1);
                }
            }
            boolean splittingChanged = false;
            if (CompoundTableHitlistHandler.isHitlistColumn(mSplittingColumn[0])) {
                int hitlistIndex = CompoundTableHitlistHandler.getHitlistFromColumn(mSplittingColumn[0]);
                if (e.getHitlistIndex() == hitlistIndex) {
                    mSplittingColumn[0] = cColumnUnassigned;
                    splittingChanged = true;
                } else if (hitlistIndex > e.getHitlistIndex()) {
                    mSplittingColumn[0] = CompoundTableHitlistHandler.getColumnFromHitlist(hitlistIndex - 1);
                }
            }
            if (CompoundTableHitlistHandler.isHitlistColumn(mSplittingColumn[1])) {
                int hitlistIndex = CompoundTableHitlistHandler.getHitlistFromColumn(mSplittingColumn[1]);
                if (e.getHitlistIndex() == hitlistIndex) {
                    mSplittingColumn[1] = cColumnUnassigned;
                    splittingChanged = true;
                } else if (hitlistIndex > e.getHitlistIndex()) {
                    mSplittingColumn[1] = CompoundTableHitlistHandler.getColumnFromHitlist(hitlistIndex - 1);
                }
            }
            if (splittingChanged) {
                if (mSplittingColumn[0] == cColumnUnassigned || mSplittingColumn[1] != cColumnUnassigned) {
                    mSplittingColumn[0] = mSplittingColumn[1];
                    mSplittingColumn[1] = cColumnUnassigned;
                }
                invalidateSplittingIndices();
            }
        } else if (e.getType() == CompoundTableHitlistEvent.cChange) {
            if (mFocusHitlist == e.getHitlistIndex()) {
                invalidateOffImage(false);
            }
            if (CompoundTableHitlistHandler.isHitlistColumn(mMarkerSizeColumn)) {
                int hitlistIndex = CompoundTableHitlistHandler.getHitlistFromColumn(mMarkerSizeColumn);
                if (e.getHitlistIndex() == hitlistIndex) {
                    invalidateOffImage(false);
                }
            }
            if (CompoundTableHitlistHandler.isHitlistColumn(mMarkerShapeColumn)) {
                int hitlistIndex = CompoundTableHitlistHandler.getHitlistFromColumn(mMarkerShapeColumn);
                if (e.getHitlistIndex() == hitlistIndex) {
                    int flagNo = mTableModel.getHitlistHandler().getHitlistFlagNo(hitlistIndex);
                    for (int i = 0; i < mDataPoints; i++)
                        mPoint[i].shape = (byte) (mPoint[i].record.isFlagSet(flagNo) ? 0 : 1);
                    invalidateOffImage(false);
                }
            }
            if (CompoundTableHitlistHandler.isHitlistColumn(mCaseSeparationColumn)) {
                int hitlistIndex = CompoundTableHitlistHandler.getHitlistFromColumn(mCaseSeparationColumn);
                if (e.getHitlistIndex() == hitlistIndex)
                    invalidateOffImage(true);
            }
            if ((CompoundTableHitlistHandler.isHitlistColumn(mSplittingColumn[0])
                    && e.getHitlistIndex() == CompoundTableHitlistHandler.getHitlistFromColumn(mSplittingColumn[0]))
                    || (CompoundTableHitlistHandler.isHitlistColumn(mSplittingColumn[1])
                            && e.getHitlistIndex() == CompoundTableHitlistHandler
                                    .getHitlistFromColumn(mSplittingColumn[1]))) {
                invalidateSplittingIndices();
            }
        }

        mMarkerColor.hitlistChanged(e);
    }

    public void valueChanged(ListSelectionEvent e) {
        if (!e.getValueIsAdjusting()) {
            invalidateOffImage(mChartType == cChartTypeBoxPlot || mChartType == cChartTypeBars
                    || mChartType == cChartTypePies);
        }
    }

    @Override
    public void highlightChanged(CompoundRecord record) {
        if (record != null && (mHighlightedPoint == null || mHighlightedPoint.record != record)) {
            for (int i = 0; i < mPoint.length; i++) {
                if (mPoint[i].record == record) {
                    setHighlightedPoint(mPoint[i]);
                    break;
                }
            }
        } else if (record == null && mHighlightedPoint != null) {
            setHighlightedPoint(null);
        }
    }

    public void updateVisibleRange(int axis, float low, float high, boolean isAdjusting) {
        if (axis < mDimensions && mAxisIndex[axis] != cColumnUnassigned) {
            mPruningBarLow[axis] = low;
            mPruningBarHigh[axis] = high;
            if (calculateVisibleRange(axis)) {
                updateLocalZoomExclusion(axis);
                applyLocalExclusion(isAdjusting);
                if (!Float.isNaN(mMarkerSizeZoomAdaption))
                    mMarkerSizeZoomAdaption = calculateZoomState();
                invalidateOffImage(true);
            }
        }
    }

    /**
     * This is used by find marker, assuming that a marker covers a
     * rectangular area. If the marker's area is not rectangular,
     * then findMarker() should be overridden for a smooth handling.
     * @param p
     * @return
     */
    protected float getMarkerWidth(VisualizationPoint p) {
        return mAbsoluteMarkerSize;
    }

    /**
     * This is used by find marker, assuming that a marker covers a
     * rectangular area. If the marker's area is not rectangular,
     * then findMarker() should be overridden for a smooth handling.
     * @param p
     * @return
     */
    protected float getMarkerHeight(VisualizationPoint p) {
        return mAbsoluteMarkerSize;
    }

    /**
     * Resets the visible range of the axis to the tablemodel's
     * min and max values and repaints.
     * @param axis
     */
    public void initializeAxis(int axis) {
        mPruningBarLow[axis] = 0.0f;
        mPruningBarHigh[axis] = 1.0f;

        mAxisSimilarity[axis] = null;

        int column = mAxisIndex[axis];

        if (column != cColumnUnassigned && mTableModel.isDescriptorColumn(column))
            setSimilarityValues(axis, -1);

        invalidateOffImage(true);
    }

    /**
     * Checks, whether this visualization point is visible in this view,
     * i.e. whether it is not excluded by filters, foreign views or local view
     * settings. Visualization points with a NaN value in one of the axis columns
     * are considered visible, if the showNaNValue option is on.
     * @param point
     * @return
     */
    protected boolean isVisible(VisualizationPoint point) {
        return (point.exclusionFlags & (mShowNaNValues ? ~EXCLUSION_FLAGS_NAN : mActiveExclusionFlags)) == 0
                && mTableModel.isVisible(point.record);
    }

    /**
     * Checks, whether this visualization point is visible in this view,
     * i.e. whether it is not excluded by filters, foreign views or local view
     * settings. Visualization points with a NaN value in one of the axis columns
     * are considered invisible, even if the showNaNValue option is on.
     * @param point
     * @return
     */
    protected boolean isVisibleExcludeNaN(VisualizationPoint point) {
        return (point.exclusionFlags & mActiveExclusionFlags) == 0 && mTableModel.isVisible(point.record);
    }

    /**
     * Checks, whether this visualization point is visible in this view,
     * i.e. whether it is not excluded by filters, foreign views or local view
     * settings. Visualization points with a NaN value in one of the axis columns
     * are considered visible, even if the showNaNValue option is off.
     * @param point
     * @return
     */
    protected boolean isVisibleIncludeNaN(VisualizationPoint point) {
        return (point.exclusionFlags & ~EXCLUSION_FLAGS_NAN) == 0 && mTableModel.isVisible(point.record);
    }

    /**
     * Checks, whether this visualization point is not zoomed out of this view
     * and not invisible because of another view, but contains a NaN value
     * on at least one axis that shows floating point values.
     * @param point
     * @return
     */
    protected boolean isVisibleAndNaN(VisualizationPoint point) {
        return (point.exclusionFlags & ~EXCLUSION_FLAGS_NAN) == 0
                && (point.exclusionFlags & EXCLUSION_FLAGS_NAN & mActiveExclusionFlags) != 0
                && mTableModel.isVisible(point.record);
    }

    /**
     * Checks, whether there is at least one axis showing floating point values
     * (not as categories) where this point has a NaN value in the associated column.
     * @param point
     * @return
     */
    protected boolean isNaN(VisualizationPoint point) {
        return (point.exclusionFlags & EXCLUSION_FLAGS_NAN & mActiveExclusionFlags) != 0;
    }

    /**
     * Sets axis related zoom flags to false and sets NaN flags depending on
     * whether the value is NaN regardless whether the axis is showing floats or categories.
     * @param axis
     */
    private void initializeLocalExclusion(int axis) {
        // flags 0-2: set if invisible due to view zooming
        // flags 3-5: set if invisible because of empty data (applies only for non-category )
        // flags 6  : set if invisible because point is not part of currently shown detail graph
        int column = mAxisIndex[axis];
        byte nanFlag = (byte) (EXCLUSION_FLAG_NAN_0 << axis);
        byte bothFlags = (byte) ((EXCLUSION_FLAG_ZOOM_0 | EXCLUSION_FLAG_NAN_0) << axis);

        // reset all flags and then
        // flag all records with empty data
        for (int i = 0; i < mDataPoints; i++) {
            mPoint[i].exclusionFlags &= ~bothFlags;
            if (column != -1 && !mTableModel.isDescriptorColumn(column)
                    && Float.isNaN(mPoint[i].record.getDouble(column)))
                mPoint[i].exclusionFlags |= nanFlag;
        }
    }

    /**
     * Updates the local exclusion flags of non-NAN row values to
     * reflect whether the value lies between the visible range of the axis.
     * Needs to be called after determineChartType().
     * @param axis
     */
    private void updateLocalZoomExclusion(int axis) {
        byte zoomFlag = (byte) (EXCLUSION_FLAG_ZOOM_0 << axis);
        byte nanFlag = (byte) (EXCLUSION_FLAG_NAN_0 << axis);
        for (int i = 0; i < mDataPoints; i++) {
            if (mIsCategoryAxis[axis] || (mPoint[i].exclusionFlags & nanFlag) == 0) {
                float theDouble = getValue(mPoint[i].record, axis);
                if (theDouble < mAxisVisMin[axis] || theDouble > mAxisVisMax[axis])
                    mPoint[i].exclusionFlags |= zoomFlag;
                else
                    mPoint[i].exclusionFlags &= ~zoomFlag;
            } else {
                mPoint[i].exclusionFlags &= ~zoomFlag;
            }
        }
    }

    private void updateGlobalExclusion() {
        if (mLocalAffectsGlobalExclusion && !mSuspendGlobalExclusion) {
            if (mLocalExclusionFlagNo == -1)
                mLocalExclusionFlagNo = mTableModel.getUnusedCompoundFlag(true);
        } else {
            mLocalExclusionFlagNo = -1;
        }
        applyLocalExclusion(false);
    }

    /**
     * Returns whether rows zoomed out of view or invisible rows because of NaN values
     * are also excluded from other views. The default is true.
     * @return whether this view's local exclusion also affects the global exclusion
     */
    public boolean getAffectGlobalExclusion() {
        return mLocalAffectsGlobalExclusion;
    }

    /**
     * Defines whether local exclusion is affecting global exclusion,
     * i.e. whether rows zoomed out of view or invisible rows because of NaN values
     * are also excluded from other views. The default is true.
     * @param v
     */
    public void setAffectGlobalExclusion(boolean v) {
        if (mLocalAffectsGlobalExclusion != v) {
            mLocalAffectsGlobalExclusion = v;
            updateGlobalExclusion();
        }
    }

    /**
     * Used to temporarily suspend the global record exclusion from the local one.
     * This is called when the view gets hidden or shown again.
     * @param suspend
     */
    public void setSuspendGlobalExclusion(boolean suspend) {
        if (mSuspendGlobalExclusion != suspend) {
            mSuspendGlobalExclusion = suspend;
            updateGlobalExclusion();
        }
    }

    /**
     * Set table model row exclusion flags according to local zooming/NAN-values
     * and trigger a TableModelEvent in case the global record visibility changes.
     * @param isAdjusting
     */
    private void applyLocalExclusion(final boolean isAdjusting) {
        // set "current view exclusion flags" in CompoundTableModel
        if (!mApplyLocalExclusionScheduled) {
            mApplyLocalExclusionScheduled = true;

            // in case applyLocalExclusion() is called in the cascade caused by a compoundTableChanged()
            // (e.g. with delete columns), then we must wait until all views have updated accordingly,
            // before interfering by spawning another compoundTableChanged() cascade...
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    mApplyLocalExclusionScheduled = false;

                    if (mLocalExclusionFlagNo != -1) {
                        boolean excludedRecordsFound = false;
                        long mask = mTableModel.convertCompoundFlagToMask(mLocalExclusionFlagNo);
                        for (int i = 0; i < mDataPoints; i++) {
                            if ((mPoint[i].exclusionFlags & mActiveExclusionFlags) == 0
                                    || (mShowNaNValues && (mPoint[i].exclusionFlags & ~EXCLUSION_FLAGS_NAN) == 0)) {
                                mPoint[i].record.clearFlags(mask);
                            } else {
                                mPoint[i].record.setFlags(mask);
                                excludedRecordsFound = true;
                            }
                        }

                        mTableModel.updateLocalExclusion(mLocalExclusionFlagNo, isAdjusting, excludedRecordsFound);
                    } else if (mPreviousLocalExclusionFlagNo != -1) {
                        mTableModel.freeCompoundFlag(mPreviousLocalExclusionFlagNo);
                    }

                    mPreviousLocalExclusionFlagNo = mLocalExclusionFlagNo;
                }
            });
        }
    }

    protected String createDateLabel(int theMarker, int exponent) {
        long time = theMarker;
        while (exponent < 0) {
            if (time % 10 != 0)
                return null;
            time /= 10;
            exponent++;
        }
        while (exponent > 0) {
            time *= 10;
            exponent--;
        }
        return DateFormat.getDateInstance().format(new Date(86400000 * time + 43200000));
    }

    protected void updateActiveRow() {
        CompoundRecord newActiveRow = mTableModel.getActiveRow();
        if (newActiveRow != null && (mActivePoint == null || mActivePoint.record != newActiveRow)) {
            for (int i = 0; i < mPoint.length; i++) {
                if (mPoint[i].record == newActiveRow) {
                    setActivePoint(mPoint[i]);
                    break;
                }
            }
        } else if (newActiveRow == null && mActivePoint != null) {
            setActivePoint(null);
        }
    }

    public boolean supportsMarkerLabelTable() {
        return false;
    }

    public boolean supportsMidPositionLabels() {
        return true;
    }

    public int getMarkerLabelTableEntryCount() {
        return 0;
    }

    public class FloatDimension {
        float width, height;
    }
}