edu.caltech.ipac.firefly.visualize.graph.XYPlotWidget.java Source code

Java tutorial

Introduction

Here is the source code for edu.caltech.ipac.firefly.visualize.graph.XYPlotWidget.java

Source

/*
 * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt
 */
package edu.caltech.ipac.firefly.visualize.graph;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.*;
import com.googlecode.gchart.client.GChart;
import edu.caltech.ipac.firefly.core.Application;
import edu.caltech.ipac.firefly.core.GeneralCommand;
import edu.caltech.ipac.firefly.data.DecimateInfo;
import edu.caltech.ipac.firefly.data.Param;
import edu.caltech.ipac.firefly.data.TableServerRequest;
import edu.caltech.ipac.firefly.data.table.*;
import edu.caltech.ipac.firefly.resbundle.images.TableImages;
import edu.caltech.ipac.firefly.resbundle.images.VisIconCreator;
import edu.caltech.ipac.firefly.ui.GwtUtil;
import edu.caltech.ipac.firefly.ui.PopupPane;
import edu.caltech.ipac.firefly.ui.PopupUtil;
import edu.caltech.ipac.firefly.ui.ServerTask;
import edu.caltech.ipac.firefly.ui.table.DataSetTableModel;
import edu.caltech.ipac.firefly.ui.table.FilterToggle;
import edu.caltech.ipac.firefly.ui.table.ModelEventHandler;
import edu.caltech.ipac.firefly.ui.table.filter.FilterDialog;
import edu.caltech.ipac.firefly.ui.table.filter.FilterPanel;
import edu.caltech.ipac.firefly.util.MinMax;
import edu.caltech.ipac.firefly.util.PropertyChangeEvent;
import edu.caltech.ipac.firefly.util.PropertyChangeListener;
import edu.caltech.ipac.firefly.util.WebUtil;
import edu.caltech.ipac.util.StringUtils;

import java.util.*;
import java.util.logging.Level;

/**
 * @author tatianag
 */
public class XYPlotWidget extends XYPlotBasicWidget implements FilterToggle.FilterToggleSupport {

    /*
     * There are two main use cases for this widget:
     * 1. simple xy plot of two numeric columns of a relatively small dataset, like in spectrum preview
     * 2. xy plot view of a table
     * Selection and filtering can be supported for both scenarios.
     * However, for the sake of simplicity, only zoom is supported in the first case.
     * In the second case, whenever an area is selected, user is presented with 3 choices: zoom, select, or filter.
     * Another difference, is that in the first case, there is no "current dataset" (table headers were not
     * previously fetched), and the first server call will bring the whole table (up to max num points).
     * In the second case, only the requested columns are brought back.
     * It might be worth splitting this class into two, but the alternative is to treat these two cases
     * in the same way. I am not sure which is better.
     */
    public enum PlotMode {
        SIMPLE_PLOT, TABLE_VIEW
    }

    public static final boolean ENABLE_XY_CHARTS = Application.getInstance().getProperties()
            .getBooleanProperty("XYCharts.enableXYCharts", true);

    private static final String RUBBERBAND_HELP = " Rubber band zoom/select/filter — click and drag to select an area. ";
    private static final String SELECTION_BTNS_HELP = " Please see buttons at the top right for available actions. ";

    private static int MIN_ROWS_FOR_DECIMATION = 30000;

    private Selection _currentSelection = null;

    private PlotMode plotMode;
    private FilterToggle _filters;
    private DeckPanel zoomToggle;
    private DeckPanel selectToggle;
    private Widget _filterSelectedLink;
    private String _sourceFile = null;
    //private String _suggestedName = null;
    private boolean _suspendEvents = false;

    GChart.Curve _highlightedPoints;
    GChart.Curve _selectedPoints;
    private FilterDialog popoutFilters;

    private Image _loading = new Image(GwtUtil.LOADING_ICON_URL);

    /*
      We have two cases: when current data in table model is null (previews) and when it is not null (view)
      In the first case _tableModel.getTotalRows() returns 0, in the second case something else
     */
    private DataSetTableModel _tableModel = null;
    private PropertyChangeListener dsPropertyChangeListener;
    private ModelEventHandler dsModelEventHandler;

    // save serialized server request for the duration of server call
    // to avoid placing duplicate requests
    private String ongoingServerReqStr;
    private String lastServerReqStr = "";

    // parameters used in last serverCall

    public XYPlotWidget(XYPlotMeta meta) {
        super(meta);
        plotMode = _meta.isSpectrum() ? PlotMode.SIMPLE_PLOT : PlotMode.TABLE_VIEW;
    }

    @Override
    protected Widget getMenuBar() {
        FlowPanel menuBar = new FlowPanel();
        //GwtUtil.setStyle(menuBar, "borderBottom", "1px solid #bbbbbb");
        menuBar.setWidth("100%");

        HorizontalPanel left = new HorizontalPanel();
        left.setVerticalAlignment(HorizontalPanel.ALIGN_MIDDLE);
        left.setSpacing(10);
        GwtUtil.setStyle(left, "align", "left");

        HorizontalPanel rightBtnsPanel;
        rightBtnsPanel = new HorizontalPanel();
        rightBtnsPanel.setSpacing(10);
        GwtUtil.setStyle(rightBtnsPanel, "align", "center");
        GwtUtil.setStyle(rightBtnsPanel, "paddingRight", "20px");

        VisIconCreator ic = VisIconCreator.Creator.getInstance();

        left.add(GwtUtil.makeImageButton(new Image(ic.getSettings()), "Plot options and tools", new ClickHandler() {
            public void onClick(ClickEvent clickEvent) {
                showOptions();
            }
        }));

        Widget saveBtn = GwtUtil.makeImageButton(new Image(TableImages.Creator.getInstance().getSaveImage()),
                "Download data in IPAC table format", new ClickHandler() {
                    public void onClick(ClickEvent clickEvent) {
                        Frame f = Application.getInstance().getNullFrame();
                        String url;
                        if (_sourceFile.contains("://")) {
                            url = _sourceFile;
                        } else {
                            Param[] params = new Param[2];
                            //if (_suggestedName != null) {
                            //    params = new Param[3];
                            //    params[2] = new Param("return", _suggestedName);
                            //} else {
                            //    params = new Param[2];
                            //}
                            params[0] = new Param("file", _sourceFile);
                            params[1] = new Param("log", "true");
                            url = WebUtil.encodeUrl(GWT.getModuleBaseURL() + "servlet/Download", params);
                        }
                        f.setUrl(url);
                    }
                });

        if (plotMode.equals(PlotMode.TABLE_VIEW)) {

            // no save button in table view mode - user should use "Save" button on table
            // left.add(saveBtn);

            _filters = new FilterToggle(this);
            left.add(_filters);

            left.add(_loading);
            _loading.setVisible(false);

            zoomToggle = new DeckPanel();
            zoomToggle.setVisible(false);
            zoomToggle.add(GwtUtil.makeImageButton(new Image(ic.getZoomUpSmall()), "Zoom in the enclosed points",
                    new ClickHandler() {
                        public void onClick(ClickEvent clickEvent) {
                            if (_data != null) {
                                if (_currentSelection != null) {
                                    _selectionCurve.setVisible(false);
                                    int numPoints = _data.getNPoints(_currentSelection.xMinMax,
                                            _currentSelection.yMinMax);
                                    if (numPoints < 1) {
                                        _currentSelection = null;
                                        updateOnSelectionBtns();
                                        return;
                                    }
                                    _savedZoomSelection = new Selection(_currentSelection.xMinMax,
                                            _currentSelection.yMinMax);
                                    updateOnSelectionBtns();
                                    if (_data.isSampled()) {
                                        _meta.userMeta.setXLimits(_currentSelection.xMinMax);
                                        _meta.userMeta.setYLimits(_currentSelection.yMinMax);
                                        updateMeta(_meta, true);
                                    } else {
                                        setChartAxesForSelection(_currentSelection.xMinMax,
                                                _currentSelection.yMinMax);
                                        // clear previous limits, if any
                                        _meta.userMeta.setXLimits(null);
                                        _meta.userMeta.setYLimits(null);
                                    }
                                    _chart.update();
                                }
                            }
                        }
                    }));
            zoomToggle.add(GwtUtil.makeImageButton(new Image(ic.getZoomOriginalSmall()),
                    "Zoom out to original chart", new ClickHandler() {
                        public void onClick(ClickEvent clickEvent) {
                            if (_data != null) {
                                _savedZoomSelection = null;
                                if (XYPlotData.shouldSample(_dataSet.getSize())
                                        || _tableModel.getTotalRows() >= MIN_ROWS_FOR_DECIMATION) {
                                    _meta.userMeta.setXLimits(null);
                                    _meta.userMeta.setYLimits(null);
                                    updateMeta(_meta, false);
                                } else {
                                    setChartAxes();
                                }
                                updateOnSelectionBtns();
                                _chart.update();
                            }
                        }
                    }));
            zoomToggle.showWidget(1);
            rightBtnsPanel.add(zoomToggle);

            selectToggle = new DeckPanel();
            selectToggle.setVisible(false);
            selectToggle.add(GwtUtil.makeImageButton(new Image(ic.getSelectRows()), "Select enclosed points",
                    new ClickHandler() {
                        public void onClick(ClickEvent clickEvent) {
                            if (_currentSelection != null) {
                                _selectionCurve.setVisible(false);
                                setSelected(_currentSelection.xMinMax, _currentSelection.yMinMax);
                                updateOnSelectionBtns();
                            }
                        }
                    }));
            selectToggle.add(GwtUtil.makeImageButton(new Image(ic.getUnselectRows()),
                    "Unselect all selected points", new ClickHandler() {
                        public void onClick(ClickEvent clickEvent) {
                            if (_data != null) {
                                if (_selectedPoints != null) {
                                    _selectedPoints.clearPoints();
                                    _selectedPoints.setCurveData(null);
                                }
                                if (_tableModel.getCurrentData() != null) {
                                    _suspendEvents = true;
                                    _tableModel.getCurrentData().deselectAll();
                                    _suspendEvents = false;
                                }
                                updateOnSelectionBtns();
                                _chart.update();
                            }
                        }
                    }));
            selectToggle.showWidget(0);
            rightBtnsPanel.add(selectToggle);

            _filterSelectedLink = GwtUtil.makeImageButton(new Image(ic.getFilterIn()),
                    "Filter in the selected points", new ClickHandler() {
                        public void onClick(ClickEvent clickEvent) {
                            if (_currentSelection != null) {
                                _selectionCurve.setVisible(false);
                                setSelected(_currentSelection.xMinMax, _currentSelection.yMinMax);
                                filterSelected();
                                updateOnSelectionBtns();
                            }
                        }
                    });
            _filterSelectedLink.setVisible(false);
            rightBtnsPanel.add(_filterSelectedLink);
        } else {
            // no selection or filter options

            left.add(_loading);
            _loading.setVisible(false);

            rightBtnsPanel.add(saveBtn);
            rightBtnsPanel.add(GwtUtil.makeImageButton(new Image(ic.getZoomOriginalSmall()),
                    "Zoom out to original chart", new ClickHandler() {
                        public void onClick(ClickEvent clickEvent) {
                            if (_data != null) {
                                _savedZoomSelection = null;
                                setChartAxes();
                                _chart.update();
                                _actionHelp.setHTML(ZOOM_IN_HELP);
                            }
                        }
                    }));
        }
        left.add(_chartTitle);

        rightBtnsPanel.add(super.getPopoutToolbar());
        enableExpansionToolbarHiding();

        menuBar.add(GwtUtil.leftRightAlign(new Widget[] { left }, new Widget[] { rightBtnsPanel }));

        return menuBar;
    }

    public void makeNewChart(XYPlotMeta meta, final DataSetTableModel tableModel, String title) {
        _meta = meta;
        makeNewChart(tableModel, title);
    }

    public void makeNewChart(final DataSetTableModel tableModel, String title) {
        if (!tableModel.equals(_tableModel)) {
            if (_tableModel != null && dsModelEventHandler != null) {
                _tableModel.removeHandler(dsModelEventHandler);
                DataSet ds = _tableModel.getCurrentData();
                if (ds != null && dsPropertyChangeListener != null) {
                    ds.removePropertyChangeListener(dsPropertyChangeListener);
                }
            }
            _tableModel = tableModel;
            dsModelEventHandler = new ModelEventHandler() {

                public void onFailure(Throwable caught) {
                }

                public void onLoad(TableDataView result) {
                    if (result.getMeta().isLoaded() && isNewRequest()) {
                        onStaleData();
                    }
                }

                public void onStatusUpdated(TableDataView result) {
                    if (result.getMeta().isLoaded() && isNewRequest()) {
                        onStaleData();
                    }
                }

                public void onDataStale(DataSetTableModel model) {
                    // must be a better way to check that no table is connected
                    if (model.getHandlers().size() == 1) {
                        // standalone chart
                        onStaleData();
                    }
                }
            };
            _tableModel.addHandler(dsModelEventHandler);

            DataSet ds = _tableModel.getCurrentData();
            if (ds != null) {
                dsPropertyChangeListener = new PropertyChangeListener() {
                    public void propertyChange(PropertyChangeEvent pce) {
                        if (_data != null && !_suspendEvents
                                && (!_tableModel.isMaxRowsExceeded() || _meta.isMaxPointsSet())) {
                            if (pce.getPropertyName().equals(TableDataView.ROW_HIGHLIGHTED)) {
                                setHighlighted((Integer) pce.getNewValue());
                            } else if (pce.getPropertyName().equals(TableDataView.ROW_SELECT_ALL)
                                    || pce.getPropertyName().equals(TableDataView.ROW_DESELECT_ALL)) {
                                setSelected((SelectionInfo) pce.getNewValue());
                            } else if (pce.getPropertyName().equals(TableDataView.ROW_SELECTED)
                                    || pce.getPropertyName().equals(TableDataView.ROW_DESELECTED)) {
                                setSelected((SelectionInfo) pce.getOldValue());
                            }
                        }
                    }
                };
                ds.addPropertyChangeListener(dsPropertyChangeListener);
            }
            _suspendEvents = false;
        }
        _maskPane.hide();
        setupNewChart(title);
        doServerCall(_meta.getMaxPoints());
    }

    private boolean isNewRequest() {
        TableServerRequest currentReq = _tableModel.getRequest();
        String currentReqStr = (currentReq == null) ? null : serverRequestToString(currentReq);
        return ongoingServerReqStr == null || currentReqStr == null || !ongoingServerReqStr.equals(currentReqStr);
    }

    private String serverRequestToString(TableServerRequest req) {
        // remove page size and start index parameters
        return req.toString().replaceAll('&' + TableServerRequest.PAGE_SIZE + "=\\d+", "")
                .replaceAll('&' + TableServerRequest.START_IDX + "=\\d+", "");
    }

    private void onStaleData() {
        _meta.userMeta.setXLimits(null);
        _meta.userMeta.setYLimits(null);
        doServerCall(_meta.getMaxPoints());
        //updateStatusMessage();
    }

    private boolean isSelectingSupported() {
        return plotMode.equals(PlotMode.TABLE_VIEW) && _tableModel.getTotalRows() < MIN_ROWS_FOR_DECIMATION;
    }

    private void doServerCall(final int maxPoints) {

        final RequiredColsInfo requiredColsInfo = getRequiredColsInfo();
        _maskPane.hide();
        if (plotMode.equals(PlotMode.TABLE_VIEW)) {
            _filters.reinit();
        }
        _savedZoomSelection = null; // do not preserve zoomed selection

        removeCurrentChart();
        //GwtUtil.DockLayout.hideWidget(_dockPanel, _statusMessage);

        ServerTask task = new ServerTask<TableDataView>(_dockPanel, "Retrieving Data...", true) {

            Date start;

            DecimateInfo info = null;
            boolean logTime = false;

            public void onSuccess(TableDataView result) {
                try {
                    _dataSet = (DataSet) result;
                    if (_dataSet != null && logTime) {
                        GwtUtil.getClientLogger().log(Level.INFO, "XY Plot: retrieved " + _dataSet.getSize()
                                + " rows in " + ((new Date()).getTime() - start.getTime()) + "ms");
                    }

                    addData(_dataSet, _tableModel.getRequest());
                    //updateStatusMessage();
                } catch (Exception e) {
                    showMask(e.getMessage());
                } finally {
                    _loading.setVisible(false);
                    lastServerReqStr = ongoingServerReqStr + (info == null ? "" : info.toString()); // for now only used when getting decimated data
                    ongoingServerReqStr = null;
                }
            }

            @Override
            public void onFailure(Throwable throwable) {
                _dataSet = null;
                _loading.setVisible(false);
                ongoingServerReqStr = null;
                showMask(throwable.getMessage());
            }

            @Override
            public void onCancel(boolean byUser) {
                super.onCancel(byUser);
                _loading.setVisible(false);
                ongoingServerReqStr = null;
            }

            @Override
            public void doTask(AsyncCallback<TableDataView> passAlong) {
                start = new Date();
                TableServerRequest curRequest = _tableModel.getRequest();
                if (curRequest != null) {
                    ongoingServerReqStr = serverRequestToString(curRequest);
                }

                List<String> requiredCols = requiredColsInfo.requiredCols;
                if (plotMode.equals(PlotMode.TABLE_VIEW) && _tableModel.getTotalRows() >= MIN_ROWS_FOR_DECIMATION) {
                    info = new DecimateInfo();

                    if (_chart != null) {
                        if (_meta.userMeta != null && _meta.userMeta.samplingXBins > 0
                                && _meta.userMeta.samplingYBins > 0) {
                            info.setXyRatio(
                                    (float) _meta.userMeta.samplingXBins / (float) _meta.userMeta.samplingYBins);
                            info.setMaxPoints(_meta.userMeta.samplingXBins * _meta.userMeta.samplingYBins);
                        } else {
                            if (_meta.userMeta != null && _meta.userMeta.aspectRatio > 0) {
                                info.setXyRatio((float) _meta.userMeta.aspectRatio);
                            } else {
                                // otherwise use default xyRatio=1
                                info.setXyRatio(1);
                            }

                            // if we want to do client side resampling for resize,
                            // it makes sense to get more data from the server
                            // but if we don't resample on resize - no need to get
                            // more than we can plot.
                            //info.setMaxPoints(MIN_ROWS_FOR_DECIMATION);
                            info.setMaxPoints(6400);

                            // uncomment if we want to do only server side decimation
                            // this way a request will go to the server to recalculate bins
                            /*
                            if (_meta.getXSize()>0 && _meta.getYSize()>0) {
                            info.setXyRatio(((float)_meta.getXSize())/((float)_meta.getYSize()));
                            int maxPoints = (int)(_meta.getXSize()*_meta.getYSize()/25.0); // assuming 5 px symbol
                            if (maxPoints < 4) maxPoints = 4;
                            if (maxPoints > 6400) maxPoints = 6400;
                            info.setMaxPoints(maxPoints);
                            } else {
                            info.setMaxPoints(6400);
                            }
                            */
                        }
                    }

                    String xCol, yCol;
                    if (_meta.userMeta != null && _meta.userMeta.xColExpr != null) {
                        xCol = _meta.userMeta.xColExpr.getInput();
                        xCol = xCol.replaceAll(" ", "");
                    } else {
                        xCol = requiredCols.get(requiredColsInfo.xColIdx);
                    }

                    if (_meta.userMeta != null && _meta.userMeta.yColExpr != null) {
                        yCol = _meta.userMeta.yColExpr.getInput();
                        yCol = yCol.replaceAll(" ", "");
                    } else {
                        yCol = requiredCols.get(requiredColsInfo.yColIdx);
                    }
                    info.setxColumnName(xCol);
                    info.setyColumnName(yCol);
                    if (_meta.userMeta != null
                            && (_meta.userMeta.getXLimits() != null || _meta.userMeta.getYLimits() != null)) {
                        // set zooming limits
                        if (_meta.userMeta.hasXMin())
                            info.setXMin(_meta.userMeta.getXLimits().getMin());
                        if (_meta.userMeta.hasXMax())
                            info.setXMax(_meta.userMeta.getXLimits().getMax());
                        if (_meta.userMeta.hasYMin())
                            info.setYMin(_meta.userMeta.getYLimits().getMin());
                        if (_meta.userMeta.hasYMax())
                            info.setYMax(_meta.userMeta.getYLimits().getMax());
                    }
                    String currentServerReqStr = ongoingServerReqStr + info.toString();
                    if (!currentServerReqStr.equals(lastServerReqStr) || _dataSet == null) {
                        //GwtUtil.getClientLogger().log(Level.INFO, "XY Plot: server req - "+currentServerReqStr);
                        logTime = true;
                        _tableModel.getDecimatedAdHocData(passAlong, info);
                    } else {
                        //GwtUtil.getClientLogger().log(Level.INFO, "XY Plot: using previous server req results");
                        logTime = false;
                        onSuccess(_dataSet);
                        cancel(); // cancel the task, should do it after onSuccess
                    }
                } else {
                    logTime = false;
                    _tableModel.getAdHocData(passAlong, requiredCols, 0, maxPoints);
                }
            }
        };
        if (!_tableModel.isMaxRowsExceeded() || _meta.isMaxPointsSet()) {
            _loading.setVisible(true);
            task.start();
        }

    }

    private List<String> getRequiredCols() {
        RequiredColsInfo reqColumnsInfo = getRequiredColsInfo();
        return reqColumnsInfo.requiredCols;
    }

    private RequiredColsInfo getRequiredColsInfo() {
        final ArrayList<String> requiredCols = new ArrayList<String>();
        int xColIdx = -1;
        int yColIdx = -1;

        // Limit number of columns for bigger tables
        if (plotMode.equals(PlotMode.TABLE_VIEW) && _tableModel.getTotalRows() > 10) {
            ArrayList<String> cols = new ArrayList<String>();
            List<TableDataView.Column> allCols = _tableModel.getCurrentData().getColumns();
            for (TableDataView.Column c : allCols) {
                // interested only in numeric columns
                if (!c.getType().startsWith("c")) {
                    cols.add(c.getName());
                }
            }
            XYPlotMeta.UserMeta userMeta = _meta.userMeta;
            String c;
            if (userMeta != null && userMeta.xColExpr != null) {
                Set<String> cSet = userMeta.xColExpr.getParsedVariables();
                for (String s : cSet) {
                    if (!StringUtils.isEmpty(s))
                        requiredCols.add(s);
                }
            } else {
                c = _meta.findXColName(cols);
                if (!StringUtils.isEmpty(c)) {
                    xColIdx = requiredCols.size();
                    requiredCols.add(c);
                }
            }
            if (userMeta != null && userMeta.yColExpr != null) {
                Set<String> cSet = userMeta.yColExpr.getParsedVariables();
                for (String s : cSet) {
                    if (!StringUtils.isEmpty(s) && !requiredCols.contains(s))
                        requiredCols.add(s);
                }
            } else {
                c = _meta.findYColName(cols);
                if (!StringUtils.isEmpty(c) && !requiredCols.contains(c)) {
                    requiredCols.add(c);
                }
                yColIdx = requiredCols.indexOf(c);
            }
            c = _meta.findErrorColName(cols);
            if (!StringUtils.isEmpty(c) && !requiredCols.contains(c))
                requiredCols.add(c);
            c = _meta.findDefaultOrderColName(cols);
            if (!StringUtils.isEmpty(c) && !requiredCols.contains(c))
                requiredCols.add(c);
            if (requiredCols.size() == 0 && cols.size() > 2) {
                // get first two columns
                requiredCols.add(cols.get(0));
                xColIdx = 0;
                requiredCols.add(cols.get(1));
                yColIdx = 1;
            }
        }
        return new RequiredColsInfo(xColIdx, yColIdx, requiredCols);
    }

    private void addData(DataSet dataSet, TableServerRequest sreq) {
        // check if DOWNLOAD_SOURCE attribute present:
        String downloadSource;
        TableMeta tableMeta = dataSet.getMeta();
        if (!StringUtils.isEmpty(tableMeta)) {
            downloadSource = dataSet.getMeta().getAttribute("DOWNLOAD_SOURCE");

            if (StringUtils.isEmpty(downloadSource)) {
                // use TableServerRequest, if available, to get source table url
                if (sreq != null) {
                    _sourceFile = WebUtil.getTableSourceUrl(sreq);
                } else {
                    _sourceFile = dataSet.getMeta().getSource();
                }
            } else {
                _sourceFile = downloadSource;
            }
        }

        try {
            addData(_dataSet);
            if (_chart != null && plotMode.equals(PlotMode.TABLE_VIEW)) {
                updateOnSelectionBtns();
            }
            _selectionCurve = getSelectionCurve();
            if (optionsDialog != null && (optionsDialog.isVisible() || _meta.hasUserMeta())) {
                if (optionsDialog.setupError()) {
                    if (!optionsDialog.isVisible())
                        showOptionsDialog();
                }
            }
        } catch (Throwable e) {
            if (e.getMessage().indexOf("column is not found") > 0) {
                if (_chart != null) {
                    _chart.clearCurves();
                }
                showOptionsDialog();
            } else {
                showMask(e.getMessage());
            }
        } finally {
            resizeNow = true;
            onResize();
            resizeNow = false;
            _panel.setWidget(_cpanel);
        }

    }

    /*
    private void updateStatusMessage() {
    Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
        public void execute() {
            if (_data == null) {
                _statusMessage.setHTML("&nbsp;");
                return;
            }
            //info about data - rows retrieved, points plotted, etc
            _statusMessage.setHTML("&nbsp;&nbsp;" + getDataInfo());
                    (_data.isSampled() ? " b  zoom for better resolution" : ""));
                    //(_data.isSampled() ? " plotted with "+_data.getNumPointsInSample()+" symbols" : ""));
        }
    });
    }
    */

    // This information might be confusing to a user
    // however it is very useful for understanding what is going on.
    // We might want to allow to get it somehow, but there is no need
    // to display it constantly
    public String getDataInfo() {
        String tableInfo = (_dataSet.getTotalRows() == _data.getNumPointsRepresented()) ? ""
                : getTableInfo() + " - ";
        return tableInfo + _data.getNumPointsRepresented() + " data points "
                + (_savedZoomSelection != null ? " (zoomed)" : "")
                + (_data.isSampled() ? " plotted with " + _data.getNumPointsInSample() + " representative points"
                        : "");
    }

    public String getTableInfo() {
        if (_tableModel != null) {
            try {
                boolean filtered = _tableModel.getFilters().size() > 0;
                if (_tableModel.getTotalRows() > 0) {
                    boolean tableNotLoaded = !_tableModel.getCurrentData().getMeta().isLoaded();
                    int totalRows = _tableModel.getTotalRows();
                    boolean allPlotted = (totalRows <= _meta.getMaxPoints());
                    return _dataSet.getTotalRows() + (tableNotLoaded ? "+" : "")
                            + (allPlotted ? "" : " from " + totalRows) + (filtered ? " filtered" : "")
                            + " rows retrieved" + (allPlotted ? "" : " - maximum reached");
                } else if (_dataSet != null) {
                    boolean tableNotLoaded = !_dataSet.getMeta().isLoaded();
                    return _dataSet.getTotalRows() + (tableNotLoaded ? "+" : "") + (filtered ? " filtered" : "")
                            + " rows retrieved";
                }
            } catch (Exception e) {
                return "";
            }
        }
        return "";
    }

    @Override
    public void removeCurrentChart() {
        if (_chart != null) {

            // clears all curves
            super.removeCurrentChart();

            // to make sure highlights and selections are always on top
            _highlightedPoints = null;
            _selectedPoints = null;

            //_filterSelectedLink.setVisible(false);
            //selectToggle.showWidget(0);
        }
    }

    @Override
    protected void addMouseListeners() {

        super.addMouseListeners();

        if (plotMode.equals(PlotMode.TABLE_VIEW)) {
            _chart.addClickHandler(new ClickHandler() {
                public void onClick(ClickEvent clickEvent) {
                    if (_chart != null && _data != null) {
                        GChart.Curve.Point touchedPoint = _chart.getTouchedPoint();
                        if (touchedPoint != null) {
                            clickEvent.preventDefault();
                            clickEvent.stopPropagation();
                            setHighlighted(_chart.getTouchedPoint());
                        } else {
                            updateOnSelectionBtns();
                            _chart.update();
                        }
                    }
                }
            });
        }
    }

    @Override
    protected void enableHover(boolean enable) {
        super.enableHover(enable);
        if (_selectedPoints != null && _chart.getCurveIndex(_selectedPoints) >= 0) {
            _selectedPoints.getSymbol().setHoverSelectionEnabled(enable);
            _selectedPoints.getSymbol().setHoverAnnotationEnabled(enable);
        }
        if (_highlightedPoints != null && _chart.getCurveIndex(_highlightedPoints) >= 0) {
            _highlightedPoints.getSymbol().setHoverSelectionEnabled(enable);
            _highlightedPoints.getSymbol().setHoverAnnotationEnabled(enable);
        }
    }

    @Override
    protected void onSelection(MinMax xMinMax, MinMax yMinMax) {
        int numPoints = _data.getNPoints(xMinMax, yMinMax);
        if (numPoints > 0) {
            if (plotMode.equals(PlotMode.TABLE_VIEW)) {
                _selectionCurve.setVisible(true);
                _currentSelection = new Selection(xMinMax, yMinMax);
                Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
                    public void execute() {
                        showOnSelectionBtns();
                    }
                });
            } else {
                _selectionCurve.setVisible(false);
                if (_data.isSampled()) {
                    _meta.userMeta.setXLimits(xMinMax);
                    _meta.userMeta.setYLimits(yMinMax);
                    updateMeta(_meta, false);
                }
                setChartAxesForSelection(xMinMax, yMinMax);
                _actionHelp.setHTML(ZOOM_OUT_HELP);
                _chart.update();
            }
        } else {
            _selectionCurve.setVisible(false);
            if (plotMode.equals(PlotMode.TABLE_VIEW)) {
                updateOnSelectionBtns();
            }
            _chart.update();
        }
    }

    private void showOnSelectionBtns() {
        zoomToggle.showWidget(0);
        zoomToggle.setVisible(true);
        selectToggle.showWidget(0);
        if (isSelectingSupported()) {
            selectToggle.setVisible(true);
        } else {
            selectToggle.setVisible(false);
        }
        _filterSelectedLink.setVisible(true);
        _actionHelp.setHTML(SELECTION_BTNS_HELP);
    }

    private void updateOnSelectionBtns() {
        boolean unzoomed = false;
        if (_savedZoomSelection != null || _meta.userMeta.getXLimits() != null
                || _meta.userMeta.getYLimits() != null) {
            zoomToggle.showWidget(1);
            zoomToggle.setVisible(true);
        } else {
            zoomToggle.setVisible(false);
            unzoomed = true;
        }

        boolean unselected = false;
        if (isSelectingSupported()) {
            if (_selectedPoints != null && _chart.getCurveIndex(_selectedPoints) >= 0
                    && _selectedPoints.getNPoints() > 0) {
                selectToggle.showWidget(1);
                selectToggle.setVisible(true);
            } else {
                selectToggle.setVisible(false);
                unselected = true;
            }
        }

        _filterSelectedLink.setVisible(false);

        if (unzoomed && unselected) {
            _actionHelp.setHTML(RUBBERBAND_HELP);
        } else {
            _actionHelp.setHTML(SELECTION_BTNS_HELP);
        }
    }

    @Override
    public void updateMeta(final XYPlotMeta meta, final boolean preserveZoomSelection) {
        _loading.setVisible(true);
        Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
            public void execute() {
                try {
                    _meta = meta;
                    if (_chart != null) {
                        _chart.clearCurves();

                        // force to reevaluate chart size
                        reevaluateChartSize(true);

                        if (_dataSet != null) {
                            List<String> requiredCols;
                            //do we need server call to get a new dataset? always evaluates to true for decimated table
                            boolean serverCallNeeded = _dataSet.getSize() < _tableModel.getTotalRows()
                                    && _meta.getMaxPoints() > _dataSet.getSize();
                            if (!serverCallNeeded) {
                                requiredCols = getRequiredCols();
                                for (String c : requiredCols) {
                                    if (_dataSet.findColumn(c) == null) {
                                        serverCallNeeded = true;
                                        break;
                                    }
                                }
                            }

                            if (serverCallNeeded) {
                                doServerCall(_meta.getMaxPoints());
                            } else {
                                addData(_dataSet);
                                _selectionCurve = getSelectionCurve();
                                if (_savedZoomSelection != null && preserveZoomSelection) {
                                    setChartAxesForSelection(_savedZoomSelection.xMinMax,
                                            _savedZoomSelection.yMinMax);
                                } else {
                                    _savedZoomSelection = null;
                                }
                                if (plotMode.equals(PlotMode.TABLE_VIEW)) {
                                    updateOnSelectionBtns();
                                }
                                _loading.setVisible(false);
                                _chart.update();
                            }
                        }
                    }
                    //_meta.addUserColumnsToDefault();
                } catch (Throwable e) {
                    _loading.setVisible(false);
                    if (_chart != null) {
                        _chart.clearCurves();
                    }
                    PopupUtil.showError("Error", e.getMessage());
                }
            }
        });
    }

    @Override
    protected void setDefaultActionHelp() {
        _actionHelp.setHTML(plotMode.equals(PlotMode.TABLE_VIEW) ? RUBBERBAND_HELP : ZOOM_IN_HELP);
    }

    private void addData(DataSet dataSet) {
        super.addData(new XYPlotData(dataSet, _meta));

        //updateStatusMessage();

        // sync highlighted and selected with current dataset, if available
        if (_tableModel.getCurrentData() != null) {
            DataSet ds = _tableModel.getCurrentData();
            // set selected first, highlighted second - to show highlighted on top of selected
            setSelected(ds.getSelectionInfo());
            setHighlighted(ds.getHighlighted());
        }
    }

    @Override
    public List<TableDataView.Column> getColumns() {

        if (_tableModel != null) {
            try {
                if (_tableModel.getTotalRows() > 0) {
                    return _tableModel.getCurrentData().getColumns();
                } else if (_dataSet != null) {
                    return _dataSet.getColumns();
                } else {
                    return new ArrayList<TableDataView.Column>(0);
                }
            } catch (Exception e) {
                return new ArrayList<TableDataView.Column>(0);
            }
        }
        return new ArrayList<TableDataView.Column>(0);
    }

    @Override
    protected XYPlotData.Point getDataPoint(GChart.Curve.Point p) {
        if (_data != null && _mainCurves.size() > 0) {
            int curveIdx = p.getParent().getParent().getCurveIndex(p.getParent());
            int pointIdx = p.getParent().getPointIndex(p);

            if (isMainCurve(curveIdx)) {
                return _data.getPoint(curveIdx, pointIdx);
            } else if (_highlightedPoints != null && curveIdx == _chart.getCurveIndex(_highlightedPoints)) {
                return (XYPlotData.Point) _highlightedPoints.getCurveData();
            } else if (_selectedPoints != null && curveIdx == _chart.getCurveIndex(_selectedPoints)) {
                List<XYPlotData.Point> dataPoints = ((SelectedData) _selectedPoints.getCurveData()).getDataPoints();
                return dataPoints.get(pointIdx);
            }
        }
        return null;
    }

    private void setHighlighted(int rowIdx) {

        if (rowIdx < 0 || _data == null)
            return;
        DataSet currentData = _tableModel.getCurrentData();
        if (currentData != null) {
            try {
                TableData.Row highlightedRow = currentData.getModel().getRow(rowIdx - currentData.getStartingIdx());

                XYPlotData.Point point = _data.getPoint(_meta, highlightedRow);
                if (point != null) {
                    setHighlighted(point, _mainCurves.get(0), false);
                } else {
                    // it's possible that highlighted point is outside chart area
                    if (_highlightedPoints != null && _chart.getCurveIndex(_highlightedPoints) >= 0) {
                        // unhighlight
                        _highlightedPoints.clearPoints();
                        _highlightedPoints.setCurveData(null);
                        _chart.update();
                    }
                }
            } catch (Exception e) {
                GwtUtil.getClientLogger().log(Level.WARNING,
                        "XYPlotWidget.setHighlighted " + rowIdx + ": " + e.getMessage());
            }
        }
    }

    private void setHighlighted(GChart.Curve.Point p) {
        if (p == null)
            return;
        setHighlighted(getDataPoint(p), p.getParent(), true);

    }

    private void setHighlighted(XYPlotData.Point point, GChart.Curve referenceCurve, boolean updateModel) {
        if (point == null)
            return;

        boolean doHighlight = true; // we want to unhighlight when clicking on a highlighted point
        if (_highlightedPoints == null || _chart.getCurveIndex(_highlightedPoints) < 0) {
            _chart.addCurve();
            _highlightedPoints = _chart.getCurve();
            GChart.Symbol symbol = _highlightedPoints.getSymbol();
            symbol.setBorderColor("black");
            symbol.setBackgroundColor("yellow");
            symbol.setSymbolType(GChart.SymbolType.BOX_CENTER);
            symbol.setHoverSelectionEnabled(true);
            symbol.setHoverAnnotationEnabled(true);

            GChart.Symbol refSym = referenceCurve.getSymbol();
            symbol.setBrushHeight(refSym.getBrushHeight());
            symbol.setBrushWidth(refSym.getBrushWidth());
            symbol.setHoverSelectionWidth(refSym.getHoverSelectionWidth());
            symbol.setHoverSelectionHeight(refSym.getHoverSelectionHeight());
            symbol.setHoverSelectionBackgroundColor(symbol.getBackgroundColor());
            symbol.setHoverSelectionBorderColor(refSym.getBorderColor());
            symbol.setHoverAnnotationSymbolType(refSym.getHoverAnnotationSymbolType());
            symbol.setHoverLocation(refSym.getHoverLocation());
            symbol.setHoverYShift(refSym.getHoverYShift());
            symbol.setHovertextTemplate(refSym.getHovertextTemplate());
        } else {
            if (_highlightedPoints.getNPoints() > 0) {
                if (updateModel) {
                    GChart.Curve.Point currentHighlighted = _highlightedPoints.getPoint();
                    //XYPlotData.Point currentPoint = (XYPlotData.Point)_highlightedPoints.getCurveData();
                    if (point.getX() == _xScale.getScaled(currentHighlighted.getX())
                            && point.getY() == _yScale.getScaled(currentHighlighted.getY())) {
                        doHighlight = false; // unhighlight if a highlighted point is clicked again
                    }
                }
                // unhighlight
                _highlightedPoints.clearPoints();
                _highlightedPoints.setCurveData(null);
            }
        }

        // highlight
        if (doHighlight) {
            _highlightedPoints.setCurveData(point);
            _highlightedPoints.addPoint(_xScale.getScaled(point.getX()), _yScale.getScaled(point.getY()));
            //_highlightedPoints.getSymbol().setHovertextTemplate(p.getHovertext());
            if (updateModel && _tableModel.getCurrentData() != null) {
                _suspendEvents = true;
                _tableModel.getCurrentData().highlight(_data.getFullTableRowIdx(point.getRowIdx()));
                _suspendEvents = false;
            }
        }
        _chart.update();
    }

    public void setSelected(SelectionInfo selectionInfo) {
        if (selectionInfo == null || !isSelectingSupported()) {
            return;
        }
        if (selectionInfo.isSelectAll()) {
            List<XYPlotData.Point> dataPoints = new ArrayList<XYPlotData.Point>();
            for (XYPlotData.Curve curve : _data.getCurveData()) {
                dataPoints.addAll(curve.getPoints());
            }
            setSelected(new SelectedData(null, null, dataPoints), false);
        } else {
            if (selectionInfo.getSelected().size() == 0) {
                List<XYPlotData.Point> emptyList = new ArrayList<XYPlotData.Point>();
                setSelected(new SelectedData(null, null, emptyList), false);
            } else {
                List<XYPlotData.Point> dataPoints = new ArrayList<XYPlotData.Point>();
                for (XYPlotData.Curve curve : _data.getCurveData()) {
                    for (XYPlotData.Point pt : curve.getPoints()) {
                        if (selectionInfo.isSelected(pt.getRowIdx())) {
                            dataPoints.add(pt);
                        } else {
                            // select data point if it represents a selected row
                            List<Integer> representedRows = pt.getRepresentedRows();
                            if (representedRows != null) {
                                for (int i : representedRows) {
                                    if (selectionInfo.isSelected(i)) {
                                        dataPoints.add(pt);
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
                setSelected(new SelectedData(null, null, dataPoints), false);
            }
        }
    }

    private void setSelected(MinMax xMinMax, MinMax yMinMax) {

        double xMin = xMinMax.getMin();
        double xMax = xMinMax.getMax();
        double yMin = yMinMax.getMin();
        double yMax = yMinMax.getMax();

        double x, y;
        List<XYPlotData.Point> dataPoints = new ArrayList<XYPlotData.Point>();
        for (XYPlotData.Curve c : _data.getCurveData()) {
            for (XYPlotData.Point p : c.getPoints()) {
                x = p.getX();
                y = p.getY();
                if (x > xMin && x < xMax && y > yMin && y < yMax) {
                    dataPoints.add(p);
                }
            }
        }
        setSelected(new SelectedData(xMinMax, yMinMax, dataPoints), true);
    }

    private void setSelected(SelectedData selectedData, boolean updateModel) {

        if (_mainCurves.size() < 1)
            return;
        if (_selectedPoints == null || _chart.getCurveIndex(_selectedPoints) < 0) {
            _chart.addCurve();
            _selectedPoints = _chart.getCurve();
            GChart.Symbol symbol = _selectedPoints.getSymbol();
            symbol.setBorderColor("black");
            symbol.setBackgroundColor("#99ff33");
            symbol.setSymbolType(GChart.SymbolType.BOX_CENTER);

            GChart.Symbol refSym = _mainCurves.get(0).getSymbol();
            symbol.setBrushHeight(refSym.getBrushHeight());
            symbol.setBrushWidth(refSym.getBrushWidth());
            symbol.setHoverSelectionWidth(refSym.getHoverSelectionWidth());
            symbol.setHoverSelectionHeight(refSym.getHoverSelectionHeight());
            symbol.setHoverSelectionBackgroundColor(symbol.getBackgroundColor());
            symbol.setHoverSelectionBorderColor(refSym.getBorderColor());
            symbol.setHoverAnnotationSymbolType(refSym.getHoverAnnotationSymbolType());
            symbol.setHoverLocation(refSym.getHoverLocation());
            symbol.setHoverYShift(refSym.getHoverYShift());
            symbol.setHovertextTemplate(refSym.getHovertextTemplate());
            symbol.setHoverSelectionEnabled(true);
            symbol.setHoverAnnotationEnabled(true);
        } else {
            _selectedPoints.clearPoints();
            _selectedPoints.setCurveData(null);
            if (updateModel && _tableModel.getCurrentData() != null) {
                _suspendEvents = true;
                _tableModel.getCurrentData().deselectAll();
                _suspendEvents = false;
            }
        }

        double x, y;
        List<XYPlotData.Point> dataPoints = selectedData.getDataPoints();

        for (XYPlotData.Point p : dataPoints) {
            x = p.getX();
            y = p.getY();
            _selectedPoints.addPoint(_xScale.getScaled(x), _yScale.getScaled(y));
        }
        _selectedPoints.setCurveData(selectedData);

        // set selected rows
        if (dataPoints.size() > 0) {
            if (updateModel && _tableModel.getCurrentData() != null) {

                Integer[] selected = _data.getRepresentedRows(dataPoints);

                _suspendEvents = true;
                _tableModel.getCurrentData().select(selected);
                _suspendEvents = false;
            }
        } else {
            _filterSelectedLink.setVisible(false);
        }

        _chart.update();
    }

    private void filterSelected() {

        // can filter when there are some selected points and when both x and y are not expressions
        if (_data == null || _selectedPoints == null || _chart.getCurveIndex(_selectedPoints) < 0
                || _selectedPoints.getNPoints() < 1) {
            PopupUtil.showError("Nothing to filter", "Nothing selected");
            return;
        }

        if (_chart.getCurveIndex(_selectedPoints) >= 0 && _selectedPoints.getNPoints() > 0) {
            SelectedData selectedData = (SelectedData) _selectedPoints.getCurveData();

            String xCol, yCol;
            if (_meta.userMeta != null && _meta.userMeta.xColExpr != null) {
                xCol = _meta.userMeta.xColExpr.getInput();
                xCol = xCol.replaceAll(" ", "");
            } else {
                xCol = _data.getXCol();
            }

            if (_meta.userMeta != null && _meta.userMeta.yColExpr != null) {
                yCol = _meta.userMeta.yColExpr.getInput();
                yCol = yCol.replaceAll(" ", "");
            } else {
                yCol = _data.getYCol();
            }

            if (xCol != null && yCol != null) {
                MinMax xMinMax = selectedData.getXMinMax();
                MinMax yMinMax = selectedData.getYMinMax();
                if (xMinMax == null || yMinMax == null) {
                    PopupUtil.showError("Unable to filter", "No X/Y range is saved for the selected points.");
                    return;
                }

                List<String> currentFilters = _tableModel.getFilters();
                // remove filters, that would be overridden
                Iterator<String> iter = currentFilters.iterator();
                String f;
                while (iter.hasNext()) {
                    f = iter.next();
                    if (f.startsWith(xCol + " > ") || f.startsWith(xCol + " < ") || f.startsWith(yCol + " > ")
                            || f.startsWith(yCol + " < ")) {
                        iter.remove();
                    }
                }
                // add new filters
                currentFilters.add(xCol + " > " + XYPlotData.formatValue(xMinMax.getMin()));
                currentFilters.add(xCol + " < " + XYPlotData.formatValue(xMinMax.getMax()));
                currentFilters.add(yCol + " > " + XYPlotData.formatValue(yMinMax.getMin()));
                currentFilters.add(yCol + " < " + XYPlotData.formatValue(yMinMax.getMax()));
            }
            if (_tableModel.getCurrentData() != null) {
                _tableModel.getCurrentData().deselectAll();
            }
            _tableModel.fireDataStaleEvent();

            _filterSelectedLink.setVisible(false);

        } else {
            PopupUtil.showError("Unable to filter", "Unable to Filter");
        }
    }

    public void toggleFilters() {
        if (popoutFilters == null) {
            final FilterPanel filterPanel = new FilterPanel(getColumns());
            popoutFilters = new FilterDialog(_filters, filterPanel);
            popoutFilters.setApplyListener(new GeneralCommand("Apply") {
                @Override
                protected void doExecute() {
                    _tableModel.setFilters(filterPanel.getFilters());
                    if (_tableModel.getCurrentData() != null) {
                        _tableModel.getCurrentData().deselectAll();
                    }
                    _tableModel.fireDataStaleEvent();
                }
            });

        }
        if (popoutFilters.isVisible()) {
            popoutFilters.setVisible(false);
        } else {
            popoutFilters.getFilterPanel().setFilters(_tableModel.getFilters());
            popoutFilters.show(0, PopupPane.Align.BOTTOM_LEFT);
        }
    }

    public List<String> getFilters() {
        return _tableModel.getFilters();
    }

    public void clearFilters() {
        _tableModel.setFilters(null);
        if (_tableModel.getCurrentData() != null) {
            _tableModel.getCurrentData().deselectAll();
        }
        _tableModel.fireDataStaleEvent();
    }

    public static class SelectedData {
        MinMax _xMinMax;
        MinMax _yMinMax;
        List<XYPlotData.Point> _dataPoints;

        SelectedData(MinMax xMinMax, MinMax yMinMax, List<XYPlotData.Point> dataPoints) {
            _xMinMax = xMinMax;
            _yMinMax = yMinMax;
            _dataPoints = dataPoints;
        }

        MinMax getXMinMax() {
            return _xMinMax;
        }

        MinMax getYMinMax() {
            return _yMinMax;
        }

        List<XYPlotData.Point> getDataPoints() {
            return _dataPoints;
        }
    }

    private static class RequiredColsInfo {
        int xColIdx;
        int yColIdx;
        List<String> requiredCols;

        public RequiredColsInfo(int xColIdx, int yColIdx, List<String> requiredCols) {
            this.xColIdx = xColIdx;
            this.yColIdx = yColIdx;
            this.requiredCols = requiredCols;
        }
    }
}