public class Main extends JFrame {
	public static final long serialVersionUID = 10L;
	private MainMenuBar mainMenuBar;
	private Session currentSession;
	private List<ChartFrame> chartFrames = new LinkedList<>();
	private final BuildProperties buildProperties;
	private final ClusterFactory clusterFactory = new ClusterFactory();
	private final DatasheetListener datasheetListener;
	private final List<DatasheetListener> subListeners = new CopyOnWriteArrayList<>();
	private DataSheetTablePanel dataSheetTablePanel;
	private final List<ParallelChartFrameComboModel> comboModels = new LinkedList<>();
	private final SettingsGroup generalSettingsGroup;
	private final SettingsGroup parallelCoordinatesAxisSettingsGroup;

	private static final List<String> LOOK_AND_FEEL_ORDER_OF_PREF = Arrays.asList(
			"com.sun.java.swing.plaf.gtk.GTKLookAndFeel",
			"com.sun.java.swing.plaf.windows.WindowsLookAndFeel",
			"javax.swing.plaf.nimbus.NimbusLookAndFeel"
	);

	public Main() {
		super("xdat   -   Untitled");
		generalSettingsGroup = SettingsGroupFactory.buildGeneralParallelCoordinatesChartSettingsGroup();
		for (Setting<?> value : generalSettingsGroup.getSettings().values()) {
			value.addListener((source, transaction) -> source.setCurrentToDefault());
		}
		parallelCoordinatesAxisSettingsGroup = SettingsGroupFactory.buildParallelCoordinatesChartAxisSettingsGroup();
		for (Setting<?> value : parallelCoordinatesAxisSettingsGroup.getSettings().values()) {
			value.addListener((source, transaction) -> source.setCurrentToDefault());
		}

		this.buildProperties = new BuildProperties();
		this.datasheetListener = new DatasheetListener() {
			@Override
			public void onClustersChanged() {
				repaintAllChartFrames();
				fireSubListeners(DatasheetListener::onClustersChanged);
			}

			@Override
			public void onDataPanelUpdateRequired() {
				updateDataPanel();
				fireSubListeners(DatasheetListener::onDataPanelUpdateRequired);
			}

			@Override
			public void onDataChanged(boolean[] autoFitRequired, boolean[] filterResetRequired, boolean[] applyFiltersRequired) {
				final ProgressMonitor progressMonitor = new ProgressMonitor(Main.this, "", "Rebuilding charts", 0, getDataSheet().getParameterCount() - 1);
				progressMonitor.setMillisToPopup(0);

				for (int i = 0; i < getDataSheet().getParameterCount(); i++) {
					final int progress = i;
					if (progressMonitor.isCanceled()) {
						break;
					}
					SwingUtilities.invokeLater(() ->
							progressMonitor.setProgress(progress));
					if (autoFitRequired[i]) {
						autofitAxisAllChartFrames(i);
					}
					if (filterResetRequired[i]) {
						resetFiltersOnAxisAllChartFrames(i);
					}
					if (applyFiltersRequired[i]) {
						refilterAllChartFrames(i);
					}
				}

				progressMonitor.close();

				repaintAllChartFrames();

				fireSubListeners(l -> l.onDataChanged(autoFitRequired, filterResetRequired, applyFiltersRequired));
			}
		};
		this.currentSession = new Session();
		this.addWindowListener(new WindowClosingAdapter(true));
		this.mainMenuBar = new MainMenuBar(this);
		this.setJMenuBar(this.mainMenuBar);
		ImageIcon img = new ImageIcon(Main.class.getClassLoader().getResource("org/xdat/images/icon.png"));
		this.setIconImage(img.getImage());

		GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
		Dimension screenSize;
		try {
			screenSize = new Dimension(ge.getScreenDevices()[0].getDisplayMode().getWidth(), ge.getScreenDevices()[0].getDisplayMode().getHeight());
		} catch (Exception e) {
			try {
				screenSize = getToolkit().getScreenSize();
			} catch (HeadlessException e1) {
				screenSize = new Dimension(1200, 800);
			}
		}
		setLocation((int) (0.25 * screenSize.width), (int) (0.25 * screenSize.height));
		setSize((int) (0.5 * screenSize.width), (int) (0.5 * screenSize.height));
		setVisible(true);
	}

	public void initialiseDataPanel() {
		SwingUtilities.invokeLater(new Runnable() {

			@Override
			public void run() {
				if (dataSheetTablePanel == null) {
					dataSheetTablePanel = new DataSheetTablePanel(Main.this);
					setContentPane(dataSheetTablePanel);
					// this.repaint();
					dataSheetTablePanel.revalidate();
				} else if (currentSession.getCurrentDataSheet() != null) {
					dataSheetTablePanel.initialiseDataSheetTableModel();
					dataSheetTablePanel.revalidate();
				} else {
					setContentPane(new JPanel());
					repaint();
				}
			}
		});
	}

	public void updateDataPanel() {
		SwingUtilities.invokeLater(() -> {
			if (dataSheetTablePanel == null) {
				dataSheetTablePanel = new DataSheetTablePanel(Main.this);
				setContentPane(dataSheetTablePanel);
				dataSheetTablePanel.revalidate();
			} else if (currentSession.getCurrentDataSheet() != null) {
				dataSheetTablePanel.updateRunsTableModel();
				dataSheetTablePanel.revalidate();
			} else {
				setContentPane(new JPanel());
				repaint();
			}
		});
	}

	/**
	 * The main method.
	 *
	 * @param args
	 *            the command line arguments (not used)
	 */
	public static void main(String[] args) {
		setLookAndFeel();
		new Main();
	}

	private static void setLookAndFeel() {
		for (String lnf : LOOK_AND_FEEL_ORDER_OF_PREF) {
			try {
				UIManager.setLookAndFeel(lnf);
				break;
			} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ignored) {
			}
		}
	}

	public DataSheet getDataSheet() {
		return this.currentSession.getCurrentDataSheet();
	}

	public void setDataSheet(DataSheet dataSheet) {
		DataSheet currentDataSheet = this.currentSession.getCurrentDataSheet();
		if(currentDataSheet != null) {
			currentDataSheet.removeListener(this.datasheetListener);
		}
		this.currentSession.setCurrentDataSheet(dataSheet);
		if (dataSheet == null) {
			this.remove(this.dataSheetTablePanel);
		} else {
			dataSheet.addListener(this.datasheetListener);
		}
		this.initialiseDataPanel();
		this.repaint();
		this.rebuildAllChartFrames();
	}

	public ClusterSet getCurrentClusterSet() {
        return this.currentSession.getCurrentClusterSet();
    }

    public void setCurrentClusterSet(ClusterSet clusterSet) {
        this.currentSession.setCurrentClusterSet(clusterSet);
    }

	public Session getCurrentSession() {
		return currentSession;
	}

	public void setCurrentSession(Session currentSession) {
		this.currentSession = currentSession;
	}

	public void addChartFrame(ChartFrame chartFrame) {
		this.chartFrames.add(chartFrame);
		this.addChartToComboboxes(chartFrame);
	}

	public void removeChartFrame(ChartFrame chartFrame) {
		this.chartFrames.remove(chartFrame);
		this.currentSession.removeChart(chartFrame.getChart());
		this.removeChartFromComboboxes(chartFrame);
	}

	public void removeParameter(String paramName) {
		getCurrentSession().removeParameter(paramName);
	}

	public ChartFrame getChartFrame(int index) {
		return this.chartFrames.get(index);
	}

	public ChartFrame getChartFrame(String title) {
		for (int i = 0; i < this.getChartFrameCount(); i++) {
			if (title.equals(this.getChartFrame(i).getTitle()))
				return this.getChartFrame(i);
		}
		throw new RuntimeException("No chart found with title " + title);
	}

	public int getChartFrameCount() {
		return this.chartFrames.size();
	}

	public int getUniqueChartId(Class chartClass) {
		int id = 0;
		for (int i = 0; i < this.getChartFrameCount(); i++) {

			if (chartClass.equals(this.chartFrames.get(i).getChart().getClass())) {
				id = this.chartFrames.get(i).getChart().getID();
			}
		}
		return ++id;
	}

	public void disposeAllChartFrames() {
		for (int i = this.chartFrames.size() - 1; i >= 0; i--) {
			this.removeChartFromComboboxes(this.chartFrames.get(i));
			this.chartFrames.get(i).dispose();
		}
		this.chartFrames.clear();
		this.currentSession.clearAllCharts();
	}

	private void rebuildAllChartFrames() {
		SwingUtilities.invokeLater(() -> {
			List<Chart> chartsSnapshot = new ArrayList<>(currentSession.getCharts());
			disposeAllChartFrames();
			for (Chart chart : chartsSnapshot) {
				try {
					ChartFrame newFrame = new ChartFrame(Main.this, chart);
					chartFrames.add(newFrame);
					currentSession.addChart(chart);
				} catch (NoParametersDefinedException e) {
					JOptionPane.showMessageDialog(Main.this, "Cannot create chart when no parameters are defined.", "No parameters defined!", JOptionPane.ERROR_MESSAGE);
				}
			}
		});
	}

	public void repaintAllChartFrames() {
		repaintAllChartFrames(new ArrayList<>());
	}

	private void repaintAllChartFrames(final List<ChartFrame> exclusionList) {
		SwingUtilities.invokeLater(() -> {
			for (ChartFrame cf : chartFrames) {
				if (!exclusionList.contains(cf)) {
					cf.repaint();
				}
			}
		});
	}

	public void refilterAllChartFrames(int columnIndex) {
		for (int i = 0; i < this.chartFrames.size(); i++) {
			Chart c = this.chartFrames.get(i).getChart();
			if (c.getClass().equals(ParallelCoordinatesChart.class)) {
				((ParallelCoordinatesChart) c).getAxis(columnIndex).applyFilters(getDataSheet());
			}
		}
	}

	public void autofitAxisAllChartFrames(int axisIndex) {
		for (int i = 0; i < this.chartFrames.size(); i++) {
			Chart c = this.chartFrames.get(i).getChart();
			if (c.getClass().equals(ParallelCoordinatesChart.class)) {
				((ParallelCoordinatesChart) c).getAxis(axisIndex).autofit(getDataSheet());
			}
		}
	}

	public void resetFiltersOnAxisAllChartFrames(int axisIndex) {
		for (int i = 0; i < this.chartFrames.size(); i++) {
			Chart c = this.chartFrames.get(i).getChart();
			if (c.getClass().equals(ParallelCoordinatesChart.class)) {
				((ParallelCoordinatesChart) c).getAxis(axisIndex).resetFilters(getDataSheet());
			}
		}
	}

	public void loadSession(String pathToFile) {
		try {
			this.disposeAllChartFrames();
			this.currentSession = Session.readFromFile(pathToFile);

			this.setTitle("xdat   -   " + pathToFile);

			for (Chart chart : getCurrentSession().getCharts()) {
				try {
					new ChartFrame(this, chart);
				} catch (NoParametersDefinedException e) {
					JOptionPane.showMessageDialog(this, "Cannot create chart when no parameters are defined.", "No parameters defined!", JOptionPane.ERROR_MESSAGE);
				}
			}

			if (this.currentSession.getCurrentDataSheet() != null) {
				this.getMainMenuBar().setItemsRequiringDataSheetEnabled(true);
			} else {
				this.initialiseDataPanel();
				this.getMainMenuBar().setItemsRequiringDataSheetEnabled(false);
			}

		} catch (InvalidClassException | ClassNotFoundException e) {
			JOptionPane.showMessageDialog(this, "The file " + pathToFile + " is not a proper xdat version " + buildProperties.getVersion() + " Session file", "Load Session", JOptionPane.OK_OPTION);
		} catch (IOException e) {
			JOptionPane.showMessageDialog(this, "Error on loading session: " + e.getMessage(), "Load Session", JOptionPane.ERROR_MESSAGE);
		}

	}

	public void saveSessionAs(String pathToFile) {
		try {
			this.currentSession.saveToFile(pathToFile);
		} catch (IOException e) {
			JOptionPane.showMessageDialog(this, "IOException on saving session: " + e.getMessage(), "Save Session", JOptionPane.ERROR_MESSAGE);
		}
	}

	public MainMenuBar getMainMenuBar() {
		return mainMenuBar;
	}

	public DataSheetTablePanel getDataSheetTablePanel() {
		return dataSheetTablePanel;
	}

	public void addChartToComboboxes(ChartFrame chartFrame) {
		for (int i = 0; i < this.comboModels.size(); i++) {
			if (chartFrame.getChart().getClass().equals(ParallelCoordinatesChart.class)) {
				comboModels.get(i).addElement(chartFrame.getChart().getTitle());
			}
		}
	}

	public void removeChartFromComboboxes(ChartFrame chartFrame) {
		for (int i = 0; i < this.comboModels.size(); i++) {
			if (chartFrame.getChart().getClass().equals(ParallelCoordinatesChart.class)) {
				comboModels.get(i).removeElement(chartFrame.getChart().getTitle());
			}
		}
	}

	public void registerComboModel(ParallelChartFrameComboModel comboModel) {
		this.comboModels.add(comboModel);
	}

	public void unRegisterComboModel(ParallelChartFrameComboModel comboModel) {
		this.comboModels.remove(comboModel);
	}

	public ClusterFactory getClusterFactory() {
		return clusterFactory;
	}

	public SettingsGroup getGeneralSettingsGroup() {
		return generalSettingsGroup;
	}

	public SettingsGroup getParallelCoordinatesAxisSettingsGroup() {
		return parallelCoordinatesAxisSettingsGroup;
	}

	public void addDataSheetSubListener(DatasheetListener listener) {
		this.subListeners.add(listener);
	}

	public void removeDataSheetSubListener(DatasheetListener listener) {
		this.subListeners.remove(listener);
	}

	private void fireSubListeners(Consumer<DatasheetListener> action) {
		for (DatasheetListener l : this.subListeners) {
			action.accept(l);
		}
	}

	public void fireClustersChanged() {
		this.datasheetListener.onClustersChanged();
	}

	public void fireDataPanelUpdateRequired() {
		this.datasheetListener.onDataPanelUpdateRequired();
	}

	public void fireOnDataChanged(boolean[] axisAutofitRequired, boolean[] axisResetFilterRequired, boolean[] axisApplyFiltersRequired) {
		this.datasheetListener.onDataChanged(axisAutofitRequired, axisResetFilterRequired, axisApplyFiltersRequired);
	}
} public class Axis implements Serializable {
	static final long serialVersionUID = 7L;
	private ParallelCoordinatesChart chart;
	private Parameter parameter;
	private Filter upperFilter;
	private Filter lowerFilter;
	private SettingsGroup settings;

	public Axis(DataSheet dataSheet, ParallelCoordinatesChart chart, Parameter parameter) {
		this.settings = SettingsGroupFactory.buildParallelCoordinatesChartAxisSettingsGroup();
		this.chart = chart;
		this.parameter = parameter;
		initialiseSettings(dataSheet);
		if (this.settings.getBoolean(Key.PARALLEL_COORDINATES_AUTO_FIT_AXIS)) {
			autofit(dataSheet);
		}
		IntegerSetting ticLabelDigitCountSetting = this.settings.getIntegerSetting(Key.PARALLEL_COORDINATES_AXIS_TIC_LABEL_DIGIT_COUNT);
		ticLabelDigitCountSetting.set(parameter.getTicLabelDigitCount());
		ticLabelDigitCountSetting.addListener((source, transaction) -> parameter.setTicLabelDigitCount(source.get()));
		this.settings.getBooleanSetting(Key.PARALLEL_COORDINATES_AXIS_INVERTED).addListener((source, transaction) ->
				onAxisInverted(dataSheet)
		);

		this.settings.getBooleanSetting(Key.PARALLEL_COORDINATES_FILTER_INVERTED).addListener((source, transaction) ->
				applyFilters(dataSheet)
		);

	}

	private void initialiseSettings(DataSheet dataSheet) {
		dataSheet.evaluateBoundsForAllDesigns(this.chart);
	}

	void resetSettingsToDefault(DataSheet dataSheet) {
		initialiseSettings(dataSheet);
		autofit(dataSheet);
	}

	void addFilters(DataSheet dataSheet) {
		this.upperFilter = new Filter(dataSheet, this, Filter.UPPER_FILTER);
		this.lowerFilter = new Filter(dataSheet, this, Filter.LOWER_FILTER);
	}

	public void autofit(DataSheet dataSheet) {
		this.setMax(dataSheet.getMaxValueOf(this.parameter), dataSheet);
		this.setMin(dataSheet.getMinValueOf(this.parameter), dataSheet);
		dataSheet.evaluateBoundsForAllDesigns(this.chart);
	}

	public Color getAxisLabelFontColor() {
		return this.settings.getColor(Key.PARALLEL_COORDINATES_AXIS_LABEL_FONT_COLOR);
	}

	public int getAxisLabelFontSize() {
		return this.settings.getInteger(Key.PARALLEL_COORDINATES_AXIS_LABEL_FONT_SIZE);
	}

	public void setAxisLabelFontSize(int axisLabelFontSize, DataSheet dataSheet) {
		double[] upperFilterValues = new double[this.chart.getAxisCount()];
		double[] lowerFilterValues = new double[this.chart.getAxisCount()];
		for (int i = 0; i < this.chart.getAxisCount(); i++) {
			Axis axis = this.chart.getAxis(i);
			lowerFilterValues[i] = axis.getLowerFilter().getValue();
			upperFilterValues[i] = axis.getUpperFilter().getValue();
		}
		this.settings.getIntegerSetting(Key.PARALLEL_COORDINATES_AXIS_LABEL_FONT_SIZE).set(axisLabelFontSize);
		for (int i = 0; i < this.chart.getAxisCount(); i++) {
			Axis axis = this.chart.getAxis(i);
			axis.getLowerFilter().setValue(lowerFilterValues[i], dataSheet);
			axis.getUpperFilter().setValue(upperFilterValues[i], dataSheet);
		}
	}

	public double getMax() {
		if (!this.parameter.isNumeric())
			return this.parameter.getDiscreteLevelCount() - 1;
		else
			return this.settings.getDouble(Key.PARALLEL_COORDINATES_AXIS_DEFAULT_MAX);
	}

	public void setMax(double max, DataSheet dataSheet) {
		this.settings.getDoubleSetting(Key.PARALLEL_COORDINATES_AXIS_DEFAULT_MAX).set(max);
		dataSheet.evaluateBoundsForAllDesigns(this.chart);
	}

	public double getMin() {
		if (!this.parameter.isNumeric())
			return 0.0;
		else
			return this.settings.getDouble(Key.PARALLEL_COORDINATES_AXIS_DEFAULT_MIN);
	}

	public void setMin(double min, DataSheet dataSheet) {
		this.settings.getDoubleSetting(Key.PARALLEL_COORDINATES_AXIS_DEFAULT_MIN).set(min);
		dataSheet.evaluateBoundsForAllDesigns(this.chart);
	}

	public double getRange() {
		if (this.parameter.isNumeric())
			return getMax() - getMin();
		else
			return this.parameter.getDiscreteLevelCount() - 1;
	}

	public int getTicCount() {
		if (this.parameter.isNumeric() && this.getRange() > 0)
			return this.settings.getInteger(Key.PARALLEL_COORDINATES_AXIS_TIC_COUNT);
		else if (this.parameter.isNumeric())
			return 1;
		else
			return this.parameter.getDiscreteLevelCount();
	}

	public void setTicCount(int ticCount, DataSheet dataSheet) {
		this.settings.getIntegerSetting(Key.PARALLEL_COORDINATES_AXIS_TIC_COUNT).set(ticCount);
		if (ticCount < 2) {
			this.applyFilters(dataSheet);
		}
	}

	public int getTicLabelFontSize() {
		return settings.getInteger(Key.TIC_LABEL_FONT_SIZE);
	}

	public String getTicLabelFormat() {
        return this.parameter.getTicLabelFormat();
	}

	public int getTicLength() {
		return settings.getInteger(Key.PARALLEL_COORDINATES_AXIS_TIC_LENGTH);
	}

	public int getWidth() {
		return settings.getInteger(Key.PARALLEL_COORDINATES_AXIS_WIDTH);
	}

	public void setWidth(int width) {
		this.getSettings().getIntegerSetting(Key.PARALLEL_COORDINATES_AXIS_WIDTH).set(width);
	}

	public boolean isActive() {
		return settings.getBoolean(Key.PARALLEL_COORDINATES_AXIS_ACTIVE);
	}

	public void setActive(boolean active) {
		this.settings.getBooleanSetting(Key.PARALLEL_COORDINATES_AXIS_ACTIVE).set(active);
	}

	public Parameter getParameter() {
		return parameter;
	}

	public void setParameter(Parameter parameter) {
		this.parameter = parameter;
	}

	public Color getAxisColor() {
		return this.settings.getColor(Key.PARALLEL_COORDINATES_AXIS_COLOR);
	}

	public Color getAxisTicLabelFontColor() {
		return this.settings.getColor(Key.PARALLEL_COORDINATES_AXIS_LABEL_FONT_COLOR);
	}

	public String getName() {
		return this.parameter.getName();
	}

	public Filter getLowerFilter() {
		return lowerFilter;
	}

	public Filter getUpperFilter() {
		return upperFilter;
	}

	public Filter getMinimumFilter() {
		if (this.isAxisInverted())
			return this.upperFilter;
		else
			return lowerFilter;
	}

	public Filter getMaximumFilter() {
		if (this.isAxisInverted())
			return this.lowerFilter;
		else
			return this.upperFilter;
	}

	public ParallelCoordinatesChart getChart() {
		return chart;
	}

	public boolean isFilterInverted() {
		return this.settings.getBoolean(Key.PARALLEL_COORDINATES_FILTER_INVERTED);
	}

	public boolean isAxisInverted() {
		return this.settings.getBoolean(Key.PARALLEL_COORDINATES_AXIS_INVERTED);
	}

	public void setAxisInverted(boolean axisInverted, DataSheet dataSheet) {
		this.settings.getBooleanSetting(Key.PARALLEL_COORDINATES_AXIS_INVERTED).set(axisInverted);
		onAxisInverted(dataSheet);
	}

	private void onAxisInverted(DataSheet dataSheet) {
		double maxFilterValue = this.getMaximumFilter().getValue();
		double minFilterValue = this.getMinimumFilter().getValue();
		this.getMaximumFilter().setValue(minFilterValue, dataSheet);
		this.getMinimumFilter().setValue(maxFilterValue, dataSheet);
	}

	public boolean isAutoFit() {
		if (this.parameter.isNumeric()) {
			return this.settings.getBoolean(Key.PARALLEL_COORDINATES_AUTO_FIT_AXIS);
		} else {
			return true;
		}
	}

	public void setAutoFit(boolean autoFit) {
		this.settings.getBooleanSetting(Key.PARALLEL_COORDINATES_AUTO_FIT_AXIS).set(autoFit);
	}

	public void setFilterAsNewRange(DataSheet dataSheet) {
		// log("setFilterAsNewRange: current range: "+this.getMin()+" and "+this.getMax()+" for axis "+this.getName());
		// log("setFilterAsNewRange:filterPositions: "+this.lowerFilter.getValue()+" and "+this.upperFilter.getValue()+" for axis "+this.getName());
		this.setAutoFit(false);
		double minFilterValue = this.getMinimumFilter().getValue();
		double maxFilterValue = this.getMaximumFilter().getValue();
		this.setMin(minFilterValue, dataSheet);
		this.setMax(maxFilterValue, dataSheet);
		// log("setFilterAsNewRange: new range set to "+this.getMin()+" and "+this.getMax()+" for axis "+this.getName());
		this.resetFilters(dataSheet);
	}

	public void applyFilters(DataSheet dataSheet) {
		this.upperFilter.apply(dataSheet);
		this.lowerFilter.apply(dataSheet);
	}

	public void resetFilters(DataSheet dataSheet) {
		this.upperFilter.reset(dataSheet);
		this.lowerFilter.reset(dataSheet);
	}

	public void initTransientData(){
		this.settings.getSettings().values().forEach(Setting::initTransientData);
	}

	public SettingsGroup getSettings() {
		return this.settings;
	}

	@Override
	public String toString() {
		return parameter.getName();
	}
} public class Filter implements Serializable {
	static final long serialVersionUID = 2L;
	public static final int UPPER_FILTER = 0;
	public static final int LOWER_FILTER = 1;
	public static final double FILTER_TOLERANCE = 0.00001;
	private int filterType;
	private Axis axis;
	private int xPos;
	private double value;
	public Filter(DataSheet dataSheet, Axis axis, int filterType) {
		this.axis = axis;
		this.filterType = filterType;
		this.reset(dataSheet);
	}

	public double getValue() {
		return this.value;
	}

	public void setValue(double value, DataSheet dataSheet) {
		this.value = value;
		apply(dataSheet);
	}

	public int getXPos() {
		return xPos;
	}

	public void setXPos(int pos) {
		this.xPos = pos;
	}

	public int getYPos(DataSheet dataSheet) {
		if (this.axis.getTicCount() == 1)
			return this.getAxis().getChart().getAxisTopPos() + (int) (this.getAxis().getChart().getAxisHeight() * 0.5);
		else {
			double upperLimit = this.axis.getMax();
			double lowerLimit = this.axis.getMin();
			if (value > upperLimit)
				value = upperLimit;
			else if (value < lowerLimit)
				value = lowerLimit;
			double valueRange = upperLimit - lowerLimit;

			int topPos = this.axis.getChart().getAxisTopPos() - 1;
			int bottomPos = topPos + this.getAxis().getChart().getAxisHeight() + 1;
			double posRange = bottomPos - topPos;
			double ratio;
			int yPos;
			if (axis.isAxisInverted()) {
				ratio = (value - lowerLimit) / valueRange;
				yPos = topPos + (int) (posRange * ratio);
			} else {
				ratio = (value - lowerLimit) / valueRange;
				yPos = bottomPos - (int) (posRange * ratio);
			}
			return yPos;
		}
	}

	public void setYPos(int pos, DataSheet dataSheet) {
		double upperLimit = this.axis.getMax();
		// log("getValue: upperLimit of filter "+filterType+": "+upperLimit);
		double lowerLimit = this.axis.getMin();
		// log("getValue: lowerLimit of filter "+filterType+": "+lowerLimit);
		double valueRange = upperLimit - lowerLimit;
		int topPos = this.axis.getChart().getAxisTopPos() - 1;
		int bottomPos = topPos + this.getAxis().getChart().getAxisHeight() + 1;
		double posRange = bottomPos - topPos;
		double ratio;
		if (axis.isAxisInverted())
			ratio = (pos - topPos) / posRange;
		else
			ratio = (bottomPos - pos) / posRange;
		this.value = lowerLimit + valueRange * ratio;
		apply(dataSheet);
	}

	public int getHighestPos(DataSheet dataSheet) {
		int pos;
		if (this.getFilterType() == Filter.UPPER_FILTER) {
			pos = this.getAxis().getChart().getAxisTopPos();
		} else if (this.getFilterType() == Filter.LOWER_FILTER) {
			pos = this.getAxis().getUpperFilter().getYPos(dataSheet);
		} else {
			pos = this.getAxis().getChart().getAxisTopPos();
		}
		return pos - 1;
	}

	public int getLowestPos(DataSheet dataSheet) {
		int pos;
		if (this.getFilterType() == Filter.UPPER_FILTER) {
			pos = this.getAxis().getLowerFilter().getYPos(dataSheet);
		} else if (this.getFilterType() == Filter.LOWER_FILTER) {
			pos = this.getAxis().getChart().getAxisTopPos() + this.getAxis().getChart().getAxisHeight();
		} else {
			pos = this.getAxis().getChart().getAxisTopPos() + this.getAxis().getChart().getAxisHeight();
		}
		return pos + 1;
	}

	public int getFilterType() {
		return filterType;
	}

	public Axis getAxis() {
		return axis;
	}

	/**
	 * Applies the Filter to all designs.
	 * <p>
	 * This method has to be called every time this Filter value is modified.
	 * This allows to check the Filtering state of a Design with respect to a
	 * Filter only when there is actually a reason for checking rather then upon
	 * every repaint.
	 * <p>
	 * This implementation was chosen because the check if a design is filtered
	 * would become to CPU expensive otherwise because each Filter would have to
	 * be checked for each design every time the Chart is repainted.
	 * <p>
     */
	public void apply(DataSheet dataSheet) {
		Parameter param = this.axis.getParameter();
		double tolerance = this.getAxis().getRange() * FILTER_TOLERANCE;
		if (tolerance <= 0) {
			tolerance = FILTER_TOLERANCE;
		}
		double value = this.getValue();

		for (Design design : dataSheet.getDesigns()) {
			if (this.filterType == UPPER_FILTER && axis.isFilterInverted() && axis.isAxisInverted()) {
				if (design.getDoubleValue(param) - tolerance > value)
					design.setActive(this, false);
				else
					design.setActive(this, true);
			} else if (this.filterType == LOWER_FILTER && axis.isFilterInverted() && axis.isAxisInverted()) {
				if (design.getDoubleValue(param) + tolerance < value)
					design.setActive(this, false);
				else
					design.setActive(this, true);
			} else if (this.filterType == UPPER_FILTER && axis.isAxisInverted()) {
				if (design.getDoubleValue(param) + tolerance < value)
					design.setActive(this, false);
				else
					design.setActive(this, true);
			} else if (this.filterType == LOWER_FILTER && axis.isAxisInverted()) {
				if (design.getDoubleValue(param) - tolerance > value)
					design.setActive(this, false);
				else
					design.setActive(this, true);
			} else if (this.filterType == UPPER_FILTER && axis.isFilterInverted()) {
				if (design.getDoubleValue(param) + tolerance < value)
					design.setActive(this, false);
				else
					design.setActive(this, true);
			} else if (this.filterType == LOWER_FILTER && axis.isFilterInverted()) {
				if (design.getDoubleValue(param) - tolerance > value)
					design.setActive(this, false);
				else
					design.setActive(this, true);
			} else if (this.filterType == UPPER_FILTER) {
				if (design.getDoubleValue(param) - tolerance > value)
					design.setActive(this, false);
				else
					design.setActive(this, true);
			} else if (this.filterType == LOWER_FILTER) {
				if (design.getDoubleValue(param) + tolerance < value)
					design.setActive(this, false);
				else
					design.setActive(this, true);
			}
		}
		dataSheet.fireOnDataChanged(false, false, false);
	}

	public void reset(DataSheet dataSheet) {
		if (this.filterType == UPPER_FILTER && this.axis.isAxisInverted()) {
			this.setValue(this.axis.getMax(), dataSheet);
		} else if (this.filterType == UPPER_FILTER) {
			double value = this.axis.getMax();
			this.setValue(value, dataSheet);
		} else if (this.filterType == LOWER_FILTER && this.axis.isAxisInverted()) {
			this.setValue(this.axis.getMax(), dataSheet);
		} else {
			this.setValue(this.axis.getMin(), dataSheet);
		}
	}

} public class ParallelCoordinatesChart extends Chart implements Serializable {

	static final long serialVersionUID = 4;
	private static final int BOTTOM_PADDING = 60;
	private int topMargin = 10;
	private List<Axis> axes = new LinkedList<>();
	private SettingsGroup chartSettings;
	public ParallelCoordinatesChart(DataSheet dataSheet, ProgressMonitor progressMonitor, int id) {
		super(dataSheet, id);
		this.chartSettings = SettingsGroupFactory.buildGeneralParallelCoordinatesChartSettingsGroup();
		this.setLocation(new Point(100, 100));
		this.setFrameSize(new Dimension(1280, 800));
		progressMonitor.setMaximum(dataSheet.getParameterCount() - 1);
		progressMonitor.setNote("Building Chart...");
		for (int i = 0; i < dataSheet.getParameterCount() && !progressMonitor.isCanceled(); i++) {
			Axis newAxis = new Axis(dataSheet, this, dataSheet.getParameter(i));
			this.addAxis(newAxis);
			progressMonitor.setProgress(i);
		}

		if (!progressMonitor.isCanceled()) {
			progressMonitor.setNote("Building Filters...");
			progressMonitor.setProgress(0);
			for (int i = 0; i < dataSheet.getParameterCount() && !progressMonitor.isCanceled(); i++) {
				this.axes.get(i).addFilters(dataSheet);
				progressMonitor.setProgress(i);
			}
		}
		Stream.concat(
				this.chartSettings.getSettings().values().stream(),
				axes.stream()
						.map(Axis::getSettings)
						.flatMap(g -> g.getSettings().values().stream())
		).forEach(s ->
				s.addListener((source, transaction) ->
						handleSettingChange(this::fireChanged, transaction))
		);
	}

	private void handleSettingChange(Runnable changeHandler, @Nullable SettingsTransaction transaction) {
		if (transaction == null) {
			changeHandler.run();
		} else {
			transaction.handleOnce(changeHandler);
		}
	}

	public String getTitle() {
		return "Parallel Coordinates Chart " + this.getID();
	}

	public int getWidth() {
		int width = 0;
		if (this.getAxis(0).isActive()) {
			width = width + (int) (0.5 * this.getAxis(0).getWidth());
		}
		for (int i = 1; i < this.getAxisCount(); i++) {
			if (this.getAxis(i).isActive()) {
				width = width + this.getAxis(i).getWidth();
			}
		}
		return width;
	}

	public int getHeight() {
		return getAxisTopPos() + getAxisHeight();
	}

	public int getAxisMaxWidth() {
		int width = 0;
		for (int i = 0; i < this.getAxisCount(); i++) {
			if (this.getAxis(i).isActive()) {
				if (width < this.getAxis(i).getWidth()) {
					width = this.getAxis(i).getWidth();
				}
			}
		}
		return width;
	}

	public void incrementAxisWidth(int deltaWidth) {
		for (Axis axis : axes) {
			axis.setWidth(Math.max(0, axis.getWidth() + deltaWidth));
		}
	}

	public int getAxisTopPos() {
		boolean verticallyOffsetAxisLabels = this.chartSettings.getBoolean(Key.PARALLEL_COORDINATES_VERTICALLY_OFFSET_AXIS_LABELS);
		int topPos;
		if (verticallyOffsetAxisLabels) {
			topPos = 2 * getMaxAxisLabelFontSize() + this.getAxisLabelVerticalDistance() + this.getTopMargin() * 2 + this.getFilterHeight();
		} else {
			topPos = getMaxAxisLabelFontSize() + this.getTopMargin() * 2 + this.getFilterHeight();
		}
		return topPos;
	}

	public Axis getAxis(int index) {
		return axes.get(index);
	}

	public Axis getAxis(String parameterName) {
		for (Axis axis : this.axes) {
			if (parameterName.equals(axis.getParameter().getName())) {
				return axis;
			}
		}
		throw new IllegalArgumentException("Axis " + parameterName + " not found");
	}

	public int getMaxAxisLabelFontSize() {
		int maxAxisLabelFontSize = 0;
		for (Axis axis : axes) {
			if (maxAxisLabelFontSize < axis.getAxisLabelFontSize()) {
				maxAxisLabelFontSize = axis.getAxisLabelFontSize();
			}
		}
		return maxAxisLabelFontSize;
	}

	public int getAxisCount() {
		return axes.size();
	}

	public int getAxisLabelVerticalDistance() {
		return chartSettings.getInteger(Key.PARALLEL_COORDINATES_LABELS_VERTICAL_DISTANCE);
	}

	public boolean isVerticallyOffsetAxisLabels() {
		return chartSettings.getBoolean(Key.PARALLEL_COORDINATES_VERTICALLY_OFFSET_AXIS_LABELS);
	}

	public void addAxis(Axis axis) {
		this.axes.add(axis);
	}

	public void removeAxis(String parameterName) {
		for (int i = 0; i < this.axes.size(); i++) {
			if (parameterName.equals(this.axes.get(i).getParameter().getName())) {
				this.axes.remove(i);
				return;
			}
		}
		throw new IllegalArgumentException("Axis " + parameterName + " not found");
	}

	public void moveAxis(int oldIndex, int newIndex) {
		Axis axis = this.axes.remove(oldIndex);
		this.axes.add(newIndex, axis);
	}

	public int getAxisHeight() {
		return this.getFrameSize().height - this.getAxisTopPos() - BOTTOM_PADDING;
	}

	public int getDesignLabelFontSize() {
		return chartSettings.getInteger(Key.DESIGN_LABEL_FONT_SIZE);
	}

	public int getLineThickness() {
		return chartSettings.getInteger(Key.LINE_THICKNESS);
	}

	public void setActiveDesignColor(Color newColor) {
		chartSettings.getColorSetting(Key.ACTIVE_DESIGN_DEFAULT_COLOR).set(newColor);
	}

	public Color getActiveDesignColor() {
		return chartSettings.getColor(Key.ACTIVE_DESIGN_DEFAULT_COLOR);
	}

	public Color getActiveDesignColorNoAlpha() {
		return new Color(getActiveDesignColor().getRGB());
	}

	public Color getFilteredDesignColor() {
		return chartSettings.getColor(Key.IN_ACTIVE_DESIGN_DEFAULT_COLOR);
	}

	public Color getFilteredDesignColorNoAlpha() {
		return new Color(getFilteredDesignColor().getRGB());
	}

	public Color getDesignColor(Design design, boolean designActive, boolean useAlpha, Color activeDesignColor, Color activeDesignColorNoAlpha, Color filteredDesignColor, Color filteredDesignColorNoAlpha) // design active is function argument to improve  performance
	{
		if (designActive && design.hasGradientColor()) {
			return design.getGradientColor();
		} else if (designActive && design.getCluster() != null) {
			return design.getCluster().getActiveDesignColor(useAlpha);
		} else if (designActive) {
			return useAlpha ? activeDesignColor : activeDesignColorNoAlpha;
		}
		else {
			return useAlpha ? filteredDesignColor : filteredDesignColorNoAlpha;
		}
	}

	public int getSelectedDesignsLineThickness() {
		return chartSettings.getInteger(Key.SELECTED_DESIGN_LINE_THICKNESS);
	}

	public Color getDefaultDesignColor(boolean designActive, boolean useAlpha) {
		if (designActive)
			return useAlpha ? getActiveDesignColor() : getActiveDesignColorNoAlpha();
		else
			return useAlpha ? getFilteredDesignColor() : getFilteredDesignColorNoAlpha();
	}

	public Color getSelectedDesignColor() {
		return chartSettings.getColor(Key.SELECTED_DESIGN_DEFAULT_COLOR);
	}

	public boolean isShowDesignIDs() {
		return chartSettings.getBoolean(Key.SHOW_DESIGN_IDS);
	}

	public boolean isShowFilteredDesigns() {
		return chartSettings.getBoolean(Key.SHOW_FILTERED_DESIGNS);
	}

	public boolean isShowOnlySelectedDesigns() {
		return chartSettings.getBoolean(Key.PARALLEL_COORDINATES_SHOW_ONLY_SELECTED_DESIGNS);
	}

	public Color getFilterColor() {
		return chartSettings.getColor(Key.PARALLEL_COORDINATES_FILTER_COLOR);
	}

	public int getTopMargin() {
		return topMargin;
	}

	public void resetDisplaySettingsToDefault(DataSheet dataSheet) {
		this.chartSettings.resetToDefault();
		for (Axis axis : axes) {
			axis.resetSettingsToDefault(dataSheet);
		}
	}

	public Color getBackGroundColor() {
		return chartSettings.getColor(Key.PARALLEL_CHART_BACKGROUND_COLOR);
	}

	@Override
	public void setBackGroundColor(Color backGroundColor) {
		this.chartSettings.getColorSetting(Key.PARALLEL_CHART_BACKGROUND_COLOR).set(backGroundColor);
	}

	public int getFilterHeight() {
		return chartSettings.getInteger(Key.PARALLEL_COORDINATES_FILTER_HEIGHT);
	}

	public int getFilterWidth() {
		return chartSettings.getInteger(Key.PARALLEL_COORDINATES_FILTER_WIDTH);
	}

	@Override
	public void initTransientDataImpl() {
		this.chartSettings.initTransientData();
		this.axes.forEach(Axis::initTransientData);
	}

	public SettingsGroup getChartSettingsGroup() {
		return this.chartSettings;
	}

	public List<Axis> getAxes() {
		return Collections.unmodifiableList(this.axes);
	}
} public class DataSheet implements Serializable, ListModel {

	static final long serialVersionUID = 8;
	private List<Design> data = new ArrayList<>();
	private Map<Integer, Design> designIdsMap = new HashMap<>();
	private List<Parameter> parameters = new LinkedList<>();
	private transient List<ListDataListener> listDataListener;
	private transient List<DatasheetListener> listeners;
	private String delimiter;

	public void initTransientData() {
		this.listDataListener = new ArrayList<>();
		this.listeners = new ArrayList<>();
	}

	public DataSheet(String pathToInputFile, boolean dataHasHeaders, Main mainWindow, ProgressMonitor progressMonitor) throws IOException {
		initTransientData();
		UserPreferences userPreferences = UserPreferences.getInstance();
		this.delimiter = userPreferences.getDelimiter();
		if (userPreferences.isTreatConsecutiveAsOne())
			this.delimiter = this.delimiter + "+";
		importData(pathToInputFile, dataHasHeaders, progressMonitor);
		boolean continueChecking = true;
		for (Parameter parameter : this.parameters) {
			if (parameter.isMixed(this) && continueChecking) {
				int userAction = JOptionPane.showConfirmDialog(mainWindow, "Parameter " + parameter.getName() + " has numeric values in some designs\n" + "and non-numerical values in others. \nThis will result in the parameter being treated as a \n" + "non-numeric parameter. \n" + "If this is incorrect it is recommended to find the design(s)\n" + "with non-numeric values and correct or remove them.\n\n" + "Press Ok to continue checking parameters or Cancel to\n" + "suppress further warnings.", "Mixed Parameter Warning", JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
				continueChecking = (userAction == JOptionPane.OK_OPTION);
			}
			parameter.setTicLabelDigitCount(userPreferences.getParallelCoordinatesAxisTicLabelDigitCount());
		}
	}

	private void importData(String pathToInputFile, boolean dataHasHeaders, ProgressMonitor progressMonitor) throws IOException {
		List<Design> buffer = new ArrayList<>();
		if (this.data != null) {
			buffer = new ArrayList<>(this.data);
		}
		int lineCount = getLineCount(pathToInputFile);
		progressMonitor.setMaximum(lineCount);

		BufferedReader f;
		String line;
		int idCounter = 1;
		f = new BufferedReader(new FileReader(pathToInputFile));
		line = (f.readLine()).trim();

		String[] lineElements = line.split(this.delimiter);
		if (dataHasHeaders) {
			// if data has headers read the parameter names from the first line
			for (String lineElement : lineElements) {
				this.parameters.add(new Parameter(this.getUniqueParameterName(lineElement), this));
			}
		} else {
			// if data does not have headers read the first Design from the first line and create default Parameter names
			Design newDesign = new Design(idCounter++);
			for (int i = 0; i < lineElements.length; i++) {
				this.parameters.add(new Parameter("Parameter " + (i + 1), this));
				newDesign.setValue(this.parameters.get(i), lineElements[i], this);

			}
			this.data.add(newDesign);
			this.designIdsMap.put(newDesign.getId(), newDesign);
			progressMonitor.setProgress(idCounter - 1);
		}
		try {
			readDesignsFromFile(progressMonitor, f, idCounter);
		} catch (IOException e) {

			this.data = buffer;
			throw e;
		}
		f.close();
		if (progressMonitor.isCanceled()) {
			this.data = buffer;
		}

		for (Parameter parameter : this.parameters) {
			parameter.updateDiscreteLevels(this);
		}
	}

	public void updateData(String pathToInputFile, boolean dataHasHeaders, ProgressMonitor progressMonitor, ClusterSet clusterSet) throws IOException, InconsistentDataException {
		int lineCount = getLineCount(pathToInputFile);
		progressMonitor.setMaximum(lineCount);

		BufferedReader f;
		String line;
		int idCounter = 1;
		f = new BufferedReader(new FileReader(pathToInputFile));

		// check datasheet to be read for consistency
		line = (f.readLine()).trim();
		String[] lineElements = line.split(this.delimiter);
		if (lineElements.length != this.getParameterCount()) {
			f.close();
			throw new InconsistentDataException(pathToInputFile);
		}

		List<Design> buffer = new ArrayList<>(this.data);
		Map<Integer, Design> idbuffer = new HashMap<>(this.designIdsMap);
		// clusterId -> designHashes
		Map<Integer, Set<Integer>> clustersToDesignHashes = computeClusterDesignHashes(this.data);
		this.data.clear();
		this.designIdsMap.clear();

		// if data has headers read the parameter names from the first line
		if (dataHasHeaders){
			for (Parameter parameter : this.parameters) {
				parameter.setName(null);
			}
			for (int i = 0; i < lineElements.length; i++) {
				this.parameters.get(i).setName(this.getUniqueParameterName(lineElements[i]));
			}
			// if data does not have headers read the first Design from the first line and create default Parameter names
		} else {
			Design newDesign = new Design(idCounter++);
			for (int i = 0; i < this.parameters.size(); i++) {
				if (lineElements.length <= i) {
					newDesign.setValue(this.parameters.get(i), "-", this);
				} else {
					this.parameters.get(i).setName("Parameter " + (i + 1));
					newDesign.setValue(this.parameters.get(i), lineElements[i], this);
				}

			}
			this.data.add(newDesign);
			this.designIdsMap.put(newDesign.getId(), newDesign);
		}

		try {
			readDesignsFromFile(progressMonitor, f, idCounter);
		} catch (IOException e) {
			this.data = buffer;
			this.designIdsMap = idbuffer;
			throw e;
		}
		f.close();
		if (progressMonitor.isCanceled()) {
			this.data = buffer;
			this.designIdsMap = idbuffer;
		}

		for (Parameter parameter : this.parameters) {
			parameter.updateNumeric(this);
			parameter.updateDiscreteLevels(this);
		}

		restoreClustersFromHashes(clustersToDesignHashes, this.data, clusterSet);

		fireOnDataChanged(initialiseBooleanArray(true), initialiseBooleanArray(true), initialiseBooleanArray(true));
		fireDataPanelUpdateRequired();

	}

	private void restoreClustersFromHashes(Map<Integer, Set<Integer>> clustersToDesignHashes, List<Design> designs, ClusterSet clusterSet) {
		for (Map.Entry<Integer, Set<Integer>> entry : clustersToDesignHashes.entrySet()) {
			Integer clusterId = entry.getKey();
			Set<Integer> designHashes = entry.getValue();
			clusterSet.findClusterById(clusterId).ifPresent(cluster -> {
				for (Design design : designs) {
					if (designHashes.contains(design.computeValuesHash(parameters))) {
						design.setCluster(cluster);
					}
				}
			});
		}
	}

	private Map<Integer, Set<Integer>> computeClusterDesignHashes(List<Design> designs) {
		Map<Integer, Set<Integer>> clustersToDesignHashes = new HashMap<>();
		for (Design design : designs) {
			Cluster cluster = design.getCluster();
			if(cluster != null) {
				clustersToDesignHashes
						.computeIfAbsent(cluster.getUniqueId(), k -> new HashSet<>())
						.add(design.computeValuesHash(parameters));
			}
		}
		return clustersToDesignHashes;
	}

	private void readDesignsFromFile(ProgressMonitor progressMonitor, BufferedReader f, int idCounter) throws IOException {
		String line;
		String[] lineElements;
		while ((line = f.readLine()) != null && !progressMonitor.isCanceled()) // read all  subsequent lines  into Designs
		{
			progressMonitor.setProgress(idCounter - 1);
			Design newDesign;
			lineElements = line.split(this.delimiter);
			if (lineElements.length > 0) {
				newDesign = new Design(idCounter++);
				boolean newDesignContainsValues = false;
				for (String lineElement : lineElements) {
					if (lineElement.length() > 0 && (!lineElement.equals("\\s"))) {
						newDesignContainsValues = true; // found non-empty empty
						break;
					}
				}
				if (newDesignContainsValues) {
					for (int i = 0; i < this.parameters.size(); i++) {
						if (lineElements.length <= i || lineElements[i].length() <= 0 || lineElements[i].equals("\\s")) {
							newDesign.setValue(this.parameters.get(i), "-", this);
						} else {
							newDesign.setValue(this.parameters.get(i), lineElements[i], this);
						}
					}
					this.data.add(newDesign);
					this.designIdsMap.put(newDesign.getId(), newDesign);
				}
			}
		}
	}

	public void setValueAt(Object newValue, int rowIndex, int columnIndex) {
		Parameter parameter = this.parameters.get(columnIndex - 1);
		boolean previousNumeric = parameter.isNumeric();
		boolean[] axisAutofitRequired = initialiseBooleanArray(false);
		boolean[] axisResetFilterRequired = initialiseBooleanArray(false);
		boolean[] axisApplyFiltersRequired = initialiseBooleanArray(false);
		this.data.get(rowIndex).setValue(parameters.get(columnIndex - 1), newValue.toString(), this);

		if (!previousNumeric) {
			parameter.updateDiscreteLevels(this);
		}

		Optional<Float> parsed = NumberParser.parseNumber(newValue.toString());
		boolean parsable = parsed.isPresent();

		if(previousNumeric && !parsable) {
			parameter.setNumeric(false, this);
		} else if (!previousNumeric && parsable) {
			parameter.updateNumeric(this);
		}
		if (previousNumeric != parameter.isNumeric()) {
			axisAutofitRequired[columnIndex - 1] = true;
			axisResetFilterRequired[columnIndex - 1] = true;
		}

		axisApplyFiltersRequired[columnIndex - 1] = true;
		fireOnDataChanged(axisAutofitRequired, axisResetFilterRequired, axisApplyFiltersRequired);
	}

	private int getLineCount(String pathToInputFile) throws IOException {
		BufferedReader f;
		f = new BufferedReader(new FileReader(pathToInputFile));
		int lineCount = 0;
		while ((f.readLine()) != null)
			lineCount++;
		f.close();
		return lineCount;
	}

	public Design getDesign(int i) {
		return this.data.get(i);
	}

	public Design getDesignByID(int id) {
		return this.designIdsMap.get(id);
	}

	public void removeDesigns(int[] designsToRemove) {
		boolean[] axisAutofitRequired = initialiseBooleanArray(false);
		boolean[] axisResetFilterRequired = initialiseBooleanArray(false);
		boolean[] axisApplyFiltersRequired = initialiseBooleanArray(false);

		boolean[] discrete = new boolean[this.parameters.size()]; // check which parameters are discrete
		for (int i = 0; i < this.parameters.size(); i++) {
			discrete[i] = !this.parameters.get(i).isNumeric();
		}

		for (int i = designsToRemove.length - 1; i >= 0; i--) {
			Design removedDesign = data.remove(designsToRemove[i]);
			this.designIdsMap.remove(removedDesign.getId());
			// check if that makes any non-numeric parameter numeric
			for (Parameter parameter : this.parameters) {
				if (!parameter.isNumeric()) {
					String string = removedDesign.getStringValue(parameter);
					if (!NumberParser.parseNumber(string).isPresent()) {
						parameter.updateNumeric(this);
						parameter.updateDiscreteLevels(this);
					}
				}
			}
		}

		for (int i = 0; i < this.parameters.size(); i++) {
			axisAutofitRequired[i] = discrete[i] && this.parameters.get(i).isNumeric();
			axisApplyFiltersRequired[i] = true;
		}

		fireOnDataChanged(axisAutofitRequired, axisResetFilterRequired, axisApplyFiltersRequired);
		fireDataPanelUpdateRequired();
	}

	public int getParameterCount() {
		return this.parameters.size();
	}

	public String getParameterName(int index) {
		if (index >= this.parameters.size() || index < 0)
			throw new IllegalArgumentException("Invalid Index " + index);
		return this.parameters.get(index).getName();
	}

	public Parameter getParameter(int index) {
		if (index >= this.parameters.size() || index < 0)
			throw new IllegalArgumentException("Invalid Index " + index);
		return this.parameters.get(index);
	}

	public Parameter getParameter(String parameterName) {
		for (Parameter parameter : this.parameters) {
			if (parameterName.equals(parameter.getName())) {
				return parameter;
			}
		}
		throw new IllegalArgumentException("Parameter " + parameterName + " not found");
	}

	public int getParameterIndex(String parameterName) {
		for (int i = 0; i < this.parameters.size(); i++) {
			if (parameterName.equals(this.parameters.get(i).getName())) {
				return i;
			}
		}
		throw new IllegalArgumentException("Parameter " + parameterName + " not found");
	}

    public boolean parameterExists(Parameter param) {
		return this.parameters.contains(param);
	}

	public double getMaxValueOf(Parameter param) {
		if (param.isNumeric()) {
			double max = Double.NEGATIVE_INFINITY;
			for (Design datum : this.data) {
				if (max < datum.getDoubleValue(param)) {
					max = datum.getDoubleValue(param);
				}
			}
			return max;
		} else {
			return param.getDiscreteLevelCount() - 1;
		}
	}

	public double getMinValueOf(Parameter param) {
		if (param.isNumeric()) {
			double min = Double.POSITIVE_INFINITY;
			for (Design datum : this.data) {
				if (min > datum.getDoubleValue(param)) {
					min = datum.getDoubleValue(param);
				}
			}
			return min;
		} else {
			return 0.0;
		}
	}

	public int getDesignCount() {
		return this.data.size();
	}

	private String getUniqueParameterName(String nameSuggestion) {
		String name = nameSuggestion;
		int id = 2;
		while (!isNameUnique(name)) {
			name = nameSuggestion + " (" + (id++) + ")";
		}
		return name;
	}

	private boolean isNameUnique(String name) {
		boolean unique = true;
		for (Parameter parameter : this.parameters) {
			if (name.equals(parameter.getName())) {
				unique = false;
				break;
			}
		}
		return unique;
	}

	public void evaluateBoundsForAllDesigns(ParallelCoordinatesChart chart) {
		for (int i = 0; i < this.getDesignCount(); i++) {
			this.data.get(i).evaluateBounds(chart, this);
		}
	}

	public void moveParameter(int oldIndex, int newIndex) {
		Parameter param = this.parameters.remove(oldIndex);
		this.parameters.add(newIndex, param);
	}

	private boolean[] initialiseBooleanArray(boolean value) {
		boolean[] array = new boolean[this.getParameterCount()];
		for (int i = 0; i < this.getParameterCount(); i++) {
			array[i] = value;
		}
		return array;
	}

	@Override
	public void addListDataListener(ListDataListener l) {
		listDataListener.add(l);
	}

	@Override
	public Object getElementAt(int index) {
		return this.parameters.get(index).getName();
	}

	@Override
	public int getSize() {
		return this.parameters.size();
	}

	@Override
	public void removeListDataListener(ListDataListener l) {
		listDataListener.remove(l);
	}

	public void addListener(DatasheetListener l) {
		this.listeners.add(l);
	}

	public void removeListener(DatasheetListener l) {
		this.listeners.remove(l);
	}

	private void fireListeners(Consumer<DatasheetListener> action) {
		for (DatasheetListener listener : this.listeners) {
			action.accept(listener);
		}
	}

	void onClustersUpdated(List<Cluster> changed, List<Cluster> added, List<Cluster> removed) {
		for (Cluster removedCluster : removed) {
			for (int j = 0; j < getDesignCount(); j++) {
				if (removedCluster.equals(getDesign(j).getCluster())) {
					getDesign(j).setCluster(null);
				}
			}
		}
		if (changed.isEmpty() && added.isEmpty() && removed.isEmpty()) {
			return;
		}
		fireClustersChanged();
	}

	public Collection<Design> getDesigns() {
		return Collections.unmodifiableList(this.data);
	}

	public List<Parameter> getParameters() {
		return Collections.unmodifiableList(this.parameters);
	}

	public void fireClustersChanged() {
		fireListeners(DatasheetListener::onClustersChanged);
	}

	public void fireDataPanelUpdateRequired() {
		fireListeners(DatasheetListener::onDataPanelUpdateRequired);
	}

	public void fireOnDataChanged(boolean axisAutofitRequired, boolean axisResetFilterRequired, boolean axisApplyFiltersRequired) {
		boolean[] autofit = new boolean[parameters.size()];
		boolean[] resetFilter = new boolean[parameters.size()];
		boolean[] applyFilters = new boolean[parameters.size()];
		Arrays.fill(autofit, axisAutofitRequired);
		Arrays.fill(resetFilter, axisResetFilterRequired);
		Arrays.fill(applyFilters, axisApplyFiltersRequired);
		fireOnDataChanged(autofit, resetFilter, applyFilters);
	}

	public void fireOnDataChanged(boolean[] axisAutofitRequired, boolean[] axisResetFilterRequired, boolean[] axisApplyFiltersRequired) {
		fireListeners(l -> l.onDataChanged(axisAutofitRequired, axisResetFilterRequired, axisApplyFiltersRequired));
	}
} public class Design implements Serializable {

	static final long serialVersionUID = 4L;
	private Map<Parameter, String> stringParameterValues = new HashMap<>(0, 1);
	private Map<Parameter, Float> numericalParameterValues = new HashMap<>(0, 1);
	private int id;
	private Cluster cluster;
	private Map<Filter, Boolean> activationMap = new HashMap<>();
	private boolean insideBounds;
	private boolean selected = false;
	private Color gradientColor = null;
	public Design(int id) {
		this.id = id;
	}

	public void setValue(Parameter param, String parameterValue, DataSheet dataSheet) {
		Optional<Float> parsed = NumberParser.parseNumber(parameterValue);
		if (parsed.isPresent()) {
			this.numericalParameterValues.put(param, parsed.get());
			this.stringParameterValues.remove(param);
		} else {
			param.setNumeric(false, dataSheet);
			this.stringParameterValues.put(param, parameterValue);
			this.numericalParameterValues.remove(param);
		}
	}

	public double getDoubleValue(Parameter param) {
		if (stringParameterValues.containsKey(param)) {
			return param.getDoubleValueOf(stringParameterValues.get(param));
		} else if (numericalParameterValues.containsKey(param) && param.isNumeric()) {
			return (numericalParameterValues.get(param));
		} else if (numericalParameterValues.containsKey(param)) {
			return param.getDoubleValueOf(Float.toString(numericalParameterValues.get(param)));
		} else {
			throw new IllegalArgumentException("Unknown parameter " + param.getName());
		}
	}

	public String getStringValue(Parameter param) {
		if (stringParameterValues.containsKey(param)) {
			return (stringParameterValues.get(param));
		} else if (numericalParameterValues.containsKey(param)) {
			return Float.toString(numericalParameterValues.get(param));
		} else {
			throw new IllegalArgumentException("Unknown parameter " + param.getName());
		}
	}

	public boolean isActive(ParallelCoordinatesChart chart) {
		for (int i = 0; i < chart.getAxisCount(); i++) {
			Filter uf = chart.getAxis(i).getUpperFilter();
			Filter lf = chart.getAxis(i).getLowerFilter();
			if (!this.activationMap.containsKey(uf)) {
				this.activationMap.put(uf, true);
			}
			if (!this.activationMap.containsKey(lf)) {
				this.activationMap.put(lf, true);
			}
			if (chart.getAxis(i).isFilterInverted()) {
				if (!(this.activationMap.get(uf) || this.activationMap.get(lf)))
					return false;
			} else {
				if (!this.activationMap.get(uf))
					return false;
				if (!this.activationMap.get(lf))
					return false;
			}
		}
		return true;
	}

	public void setActive(Filter filter, boolean active) {
		this.activationMap.put(filter, active);
	}

	public void evaluateBounds(ParallelCoordinatesChart chart, DataSheet dataSheet) {
		this.insideBounds = true;
		for (int i = 0; i < chart.getAxisCount(); i++) {
			if (!isInsideBounds(chart.getAxis(i), dataSheet)) {
				this.insideBounds = false;
				return;
			}
		}
	}

	private boolean isInsideBounds(Axis axis, DataSheet dataSheet) {
		double value = this.getDoubleValue(axis.getParameter());
		double max = axis.getMax();
		double min = axis.getMin();
		return min <= value && value <= max;
	}

	public boolean isInsideBounds(ParallelCoordinatesChart chart) {
		return this.insideBounds;
	}

	public boolean isSelected() {
		return this.selected;
	}

	public void setSelected(boolean selected) {
		this.selected = selected;
	}

	public int getId() {
		return id;
	}

	public Cluster getCluster() {
		return cluster;
	}

	public void setCluster(Cluster cluster) {
		this.cluster = cluster;
	}

	public Color getGradientColor() {
		return gradientColor;
	}

	public void setAxisGradientColor(Color gradientColor) {
		this.gradientColor = gradientColor;
	}
	
	public void removeAxisGradientColor() {
		this.gradientColor = null;
	}

	public boolean hasGradientColor() {
		return (this.gradientColor != null);
	}

	public int computeValuesHash(List<Parameter> parameters) {
		return Objects.hash(parameters.stream()
				.map(p -> new ParameterValue(p, getStringValue(p)))
				.toArray());
	}

	private static class ParameterValue {
		private final Parameter parameter;
		private final String value;

		private ParameterValue(Parameter parameter, String value) {
			this.parameter = parameter;
			this.value = value;
		}

		@Override
		public boolean equals(Object o) {
			if (this == o) return true;
			if (o == null || getClass() != o.getClass()) return false;
			ParameterValue that = (ParameterValue) o;
			return Objects.equals(parameter, that.parameter) &&
					Objects.equals(value, that.value);
		}

		@Override
		public int hashCode() {
			return Objects.hash(parameter, value);
		}
	}
} public class Parameter implements Serializable {
	static final long serialVersionUID = 4L;
	private String name;
	private boolean numeric = true;
	private TreeSet<String> discreteLevels = new TreeSet<>(new ReverseStringComparator());
    private int ticLabelDigitCount = 3;
	public Parameter(String name, DataSheet dataSheet) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	/**
	 * Checks whether the parameter is mixed. A parameter is mixed if at least
	 * one design has a numeric value and at least one design has a non-numeric
	 * value.
	 * 
	 * @return true, if the parameter is mixed
	 * @param dataSheet
	 */
	boolean isMixed(DataSheet dataSheet) {
		return getType(dataSheet) == ParameterType.MIXED;
	}

	private ParameterType getType(DataSheet dataSheet){
		boolean foundNumericValue = false;
		boolean foundStringValue = false;
        for (Design design : dataSheet.getDesigns()) {
            String string = design.getStringValue(this);
            if(NumberParser.parseNumber(string).isPresent()){
                foundNumericValue = true;
            } else {
                foundStringValue = true;
            }
            if(foundNumericValue && foundStringValue) {
                return ParameterType.MIXED;
            }
        }
        if (foundStringValue) {
            return ParameterType.NON_NUMERIC;
        } else if (foundNumericValue) {
            return ParameterType.NUMERIC;
        }
        // This can only happen if we have no designs. Consider the parameter numeric in this case
        return ParameterType.NUMERIC;
	}

	public boolean isNumeric() {
		return numeric;
	}

	void setNumeric(boolean numeric, DataSheet dataSheet) {
		if (numeric == this.numeric) {
			return;
		}
		this.numeric = numeric;
		updateDiscreteLevels(dataSheet);
	}

	/**
	 * Gets a numeric representation of a string value for this parameter.
	 * <p>
	 * If the parameter is numeric, an attempt is made to parse the string as a
	 * Double. If this attempt fails, the parameter is not  considered numeric anymore,
	 * but is transformed into a discrete parameter.
	 * <p>
	 * If the parameter is not numeric, the string is looked up in the TreeSet
	 * discreteLevels that should contain all discrete values (that is Strings)
	 * that were found in the data sheet for this parameter. If the value is not
	 * found it is added as a new discrete level for this parameter. The treeSet
	 * is then searched again in order to get the correct index of the new
	 * discrete level.
	 * <p>
	 * If this second search does not yield the result, something unexpected has
	 * gone wrong and a CorruptDataException is thrown.
	 * 
	 * @param string
	 *            the string
	 * @return the numeric representation of the given string
	 */
	double getDoubleValueOf(String string) {
		if (this.numeric) {
			Optional<Float> parsed = NumberParser.parseNumber(string);
			if(parsed.isPresent()) {
				return parsed.get();
			}
		}

		int index = 0;
		Iterator<String> it = discreteLevels.iterator();
		while (it.hasNext()) {
			if (string.equalsIgnoreCase(it.next())) {
				return index;
			}
			index++;
		}

		// String not found, add it to discrete levels
		this.discreteLevels.add(string);
		index = 0;
		it = discreteLevels.iterator();
		while (it.hasNext()) {
			String next = it.next();
			if (string.equalsIgnoreCase(next)) {
				return index;
			}
			index++;
		}
		throw new CorruptDataException(this);
	}

	/**
	 * Gets the string representation of a given double value for this
	 * parameter.
	 * <p>
	 * If the parameter is numeric, the provided value is simply converted to a
	 * String and returned.
	 * <p>
	 * If it is discrete, the double value is casted to an Integer value and
	 * this value is used as an index to look up the corresponding discrete
	 * value string in the TreeSet discreteLevels.
	 * <p>
	 * If no value is found for the given index the data is assumed to be
	 * corrupt and a CorruptDataException is thrown.
	 * 
	 * @param value
	 *            the numeric value
	 * @return the string representation of the given double value for this
	 *         parameter.
	 */
	public String getStringValueOf(double value) {
		if (this.numeric) {
			return Double.toString(value);
		} else {
			int index = (int) value;
			int currentIndex = 0;
			Iterator<String> it = discreteLevels.iterator();
			while (it.hasNext()) {
				String next = it.next();
				if (currentIndex == index) {
					return next;
				}
				currentIndex++;
			}
			throw new CorruptDataException(this);
		}
	}

	public int getDiscreteLevelCount() {
		if (this.isNumeric()) {
			throw new RuntimeException("Parameter " + this.name + " is numeric!");
		} else {
			// log("getDiscreteLevelCount returning size "+this.discreteLevels.size());
			return this.discreteLevels.size();
		}
	}

	void updateNumeric(DataSheet dataSheet) {
		this.setNumeric(getType(dataSheet) == ParameterType.NUMERIC, dataSheet);

	}

	void updateDiscreteLevels(DataSheet dataSheet){
	    this.discreteLevels.clear();
	    if (isNumeric()) {
	        return;
        }
	    this.discreteLevels.addAll(dataSheet.getDesigns()
                .stream()
                .map(d -> d.getStringValue(this))
                .collect(Collectors.toSet()));
    }


    public void setTicLabelDigitCount(int value) {
        this.ticLabelDigitCount = value;
    }

    public int getTicLabelDigitCount(){
        return this.ticLabelDigitCount;
    }

    public String getTicLabelFormat() {
        int digitCount = getTicLabelDigitCount();
        return "%"+(digitCount+1)+"."+digitCount+"f";
    }

	public int getLongestTicLabelStringLength(FontMetrics fm, String numberFormat, DataSheet dataSheet) {
		if (this.isNumeric()) {
			double minValue = Double.POSITIVE_INFINITY;
			double maxValue = Double.NEGATIVE_INFINITY;

			for (int i = 0; i < dataSheet.getDesignCount(); i++) {
				if (dataSheet.getDesign(i).getDoubleValue(this) > maxValue)
					maxValue = dataSheet.getDesign(i).getDoubleValue(this);
				if (dataSheet.getDesign(i).getDoubleValue(this) < minValue)
					minValue = dataSheet.getDesign(i).getDoubleValue(this);
			}
			int minLength = fm.stringWidth(String.format(numberFormat, minValue));
			int maxLength = fm.stringWidth(String.format(numberFormat, maxValue));
			return Math.max(minLength, maxLength);
		} else {
			int length = 0;
			for (int i = 0; i < this.getDiscreteLevelCount(); i++) {
				int tempLength = fm.stringWidth(this.getStringValueOf(i));
				if (tempLength > length)
					length = tempLength;
			}
			return length;
		}
	}

	@Override
	public String toString() {
		return name;
	}

	private static class ReverseStringComparator implements Comparator<String>, Serializable {
		static final long serialVersionUID = 0L;
		public int compare(String s1, String s2) {
			return (s2.compareToIgnoreCase(s1));
		}
	}
} public class ChartFrame extends JFrame implements ComponentListener {

	private Main mainWindow;
	private Chart chart;
	private ChartPanel chartPanel;
	private final List<Runnable> closeHooks = new ArrayList<>();

	public ChartFrame(Main mainWindow, Chart chart) throws NoParametersDefinedException {
		super(chart.getTitle());
		if (mainWindow.getDataSheet().getParameterCount() < 1) {
			throw new NoParametersDefinedException();
		}
		addWindowListener(new ChartFrameClosingAdapter(this, mainWindow));
		this.mainWindow = mainWindow;

		this.setIconImage(mainWindow.getIconImage());
		this.setPreferredSize(chart.getFrameSize());

		this.chart = chart;
		mainWindow.addChartFrame(this);
		this.setLayout(new GridLayout(1, 1));

		if (chart.getClass().equals(ParallelCoordinatesChart.class)) {
			JPanel wrapper = new JPanel();
			wrapper.setLayout(new BorderLayout());

			this.chartPanel = new ParallelCoordinatesChartPanel(mainWindow, this, (ParallelCoordinatesChart) this.chart);
			this.setJMenuBar(new ParallelCoordinateChartFrameMenuBar(mainWindow, this, (ParallelCoordinatesChart) this.chart));
			JScrollPane scrollPane = new JScrollPane(this.chartPanel, JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);

			ParallelCoordinatesChartSidebarPanel sidebar = new ParallelCoordinatesChartSidebarPanel(mainWindow, this, this.chartPanel, (ParallelCoordinatesChart)chart);

			wrapper.add(scrollPane, BorderLayout.CENTER);
			wrapper.add(sidebar, BorderLayout.EAST);
			closeHooks.add(sidebar::onClosed);
			this.add(wrapper);
		} else if (chart.getClass().equals(ScatterChart2D.class)) {
			this.chartPanel = new ScatterChart2DPanel(mainWindow, (ScatterChart2D) this.chart);
			this.setJMenuBar(new ScatterChart2DFrameMenuBar(mainWindow, this, (ScatterChart2D) chart));
			this.add(this.chartPanel, BorderLayout.CENTER);
		} else {
			throw new RuntimeException("Unknown Chart Type!");
		}
		this.closeHooks.add(() -> mainWindow.removeChartFrame(this));

		this.setLocation(this.chart.getLocation());
		this.getContentPane().setPreferredSize(this.chart.getFrameSize());
		this.pack();
		this.addComponentListener(this);
		setVisible(true);
	}

	public Chart getChart() {
		return chart;
	}

	public ChartPanel getChartPanel() {
		return chartPanel;
	}

	public void dispose() {
		closeHooks.forEach(Runnable::run);
		super.dispose();
	}

	public Main getMainWindow() {
		return mainWindow;
	}

	public void componentHidden(ComponentEvent arg0) {
	}

	public void componentMoved(ComponentEvent arg0) {
		this.chart.setLocation(this.getLocation());
	}

	public void componentResized(ComponentEvent arg0) {
		this.chart.setFrameSize(this.getContentPane().getSize());
	}

	public void componentShown(ComponentEvent arg0) {
	}
} public class ParallelCoordinatesContextMenu extends JPopupMenu {

	public ParallelCoordinatesContextMenu(Main mainWindow, ChartFrame chartFrame, Axis axis) {
		super();
		this.add(new JLabel(" Axis settings: " + axis.getName()));
		this.add(new Separator());

		ParallelChartContextMenuActionListener cmd = new ParallelChartContextMenuActionListener(mainWindow, chartFrame, axis);

		JMenuItem setFilterAsNewRangeMenuItem = new JMenuItem("Set current filter as new range");
		if (axis.isFilterInverted() || !axis.getParameter().isNumeric())
			setFilterAsNewRangeMenuItem.setEnabled(false);
		else
			setFilterAsNewRangeMenuItem.addActionListener(cmd::setCurrentFiltersAsNewRange);
		this.add(setFilterAsNewRangeMenuItem);

		JMenuItem resetFilterMenuItem = new JMenuItem("Reset filter to axis range");
		resetFilterMenuItem.addActionListener(cmd::resetFilter);
		this.add(resetFilterMenuItem);

		JMenuItem autofitMenuItem = new JMenuItem("Autofit");
		if (!axis.getParameter().isNumeric())
			autofitMenuItem.setEnabled(false);
		else
			autofitMenuItem.addActionListener(cmd::autofit);
		this.add(autofitMenuItem);
		
		JMenuItem markValuesMenuItem = new JMenuItem("Apply color gradient");
		markValuesMenuItem.addActionListener(cmd::applyColorGradient);
		this.add(markValuesMenuItem);

		JMenuItem invertAxisMenuItem = new JMenuItem("Invert Axis                                ");
		invertAxisMenuItem.addActionListener(cmd::invertAxis);
		this.add(invertAxisMenuItem);
		
		JMenuItem moveColumnLeftMenuItem = new JMenuItem("Move left                               ");
		this.addCustomHint(moveColumnLeftMenuItem, "(Left-click & drag)");
		moveColumnLeftMenuItem.addActionListener(cmd::moveAxisLeft);
		this.add(moveColumnLeftMenuItem);

		JMenuItem moveColumnRightMenuItem = new JMenuItem("Move right                               ");
		this.addCustomHint(moveColumnRightMenuItem, "(Left-click & drag)");
		moveColumnRightMenuItem.addActionListener(cmd::moveAxisRight);
		this.add(moveColumnRightMenuItem);

		JMenuItem hideMenuItem = new JMenuItem("Hide axis                               ");
		hideMenuItem.addActionListener(cmd::hideAxis);
		this.add(hideMenuItem);

		JMenuItem addTicMenuItem = new JMenuItem("Add tic                               ");
		this.addCustomHint(addTicMenuItem, "(Alt+Mouse Whl Up)");
		if (!axis.getParameter().isNumeric())
			addTicMenuItem.setEnabled(false);
		else
			addTicMenuItem.addActionListener(cmd::addTic);
		this.add(addTicMenuItem);

		JMenuItem removeTicMenuItem = new JMenuItem("Remove tic                               ");
		this.addCustomHint(removeTicMenuItem, "(Alt+Mouse Whl Down)");
		if (!axis.getParameter().isNumeric())
			removeTicMenuItem.setEnabled(false);
		else
			removeTicMenuItem.addActionListener(cmd::removeTic);
		this.add(removeTicMenuItem);

		JMenuItem increaseDistanceThisAxisMenuItem = new JMenuItem("Increase spacing                               ");
		this.addCustomHint(increaseDistanceThisAxisMenuItem, "(Ctrl+Mouse Whl Up)");
		increaseDistanceThisAxisMenuItem.addActionListener(e -> cmd.addDistanceThisAxis(10));
		this.add(increaseDistanceThisAxisMenuItem);

		JMenuItem reduceDistanceThisAxisMenuItem = new JMenuItem("Reduce spacing                                ");
		this.addCustomHint(reduceDistanceThisAxisMenuItem, "(Ctrl+Mouse Whl Down)");
		increaseDistanceThisAxisMenuItem.addActionListener(e -> cmd.addDistanceThisAxis(-10));
		this.add(reduceDistanceThisAxisMenuItem);

		this.addSeparator();

		JMenuItem resetAllFiltersMenuItem = new JMenuItem("Reset all filters to axis range");
		resetAllFiltersMenuItem.addActionListener(cmd::resetAllFilters);
		this.add(resetAllFiltersMenuItem);

		JMenuItem increaseDistanceAllAxesMenuItem = new JMenuItem("Increase spacing all axes                               ");
		this.addCustomHint(increaseDistanceAllAxesMenuItem, "(Mouse Whl Up)");
		increaseDistanceAllAxesMenuItem.addActionListener(cmd::increaseDistanceAllAxes);
		this.add(increaseDistanceAllAxesMenuItem);

		JMenuItem reduceDistanceAllAxesMenuItem = new JMenuItem("Reduce spacing all axes                                ");
		this.addCustomHint(reduceDistanceAllAxesMenuItem, "(Mouse Whl Down)");
		reduceDistanceAllAxesMenuItem.addActionListener(cmd::reduceDistanceAllAxes);
		this.add(reduceDistanceAllAxesMenuItem);
		
		JMenuItem unmarkValuesMenuItem = new JMenuItem("Reset all color gradients");
		unmarkValuesMenuItem.addActionListener(cmd::resetColorGradient);
		this.add(unmarkValuesMenuItem);
	}

	private void addCustomHint(JMenuItem item, String hint) {
		item.setLayout(new BorderLayout());
		JLabel label = new JLabel(hint + " ");
		label.setFont(new Font("SansSerif", Font.PLAIN, 12));
		label.setForeground(new Color(140, 140, 180));
		item.add(label, BorderLayout.EAST);
	}
} public class DataSheetTableColumnModel extends DefaultTableColumnModel {

	private Main mainWindow;

	public DataSheetTableColumnModel(Main mainWindow) {
		super();
		this.mainWindow = mainWindow;
		this.setColumnMargin(1);
	}

	public boolean getColumnSelectionAllowed() {
		return false;
	}

	public void moveColumn(int src, int tar) {
		if (src != 0 && tar != 0) {
			super.moveColumn(src, tar);
			if (src != tar) {
				this.mainWindow.getDataSheet().moveParameter(src - 1, tar - 1);
				for (int i = 0; i < this.getColumnCount(); i++) {
					this.getColumn(i).setModelIndex(i);
				}

				Collection<Chart> charts = this.mainWindow.getCurrentSession().getCharts();
				for (Chart chart : charts) {
					if (chart instanceof ParallelCoordinatesChart) {
						((ParallelCoordinatesChart) charts).moveAxis(src - 1, tar - 1);
					}
				}
				this.mainWindow.repaintAllChartFrames();
			}
		}
	}

	public void removeColumn(TableColumn arg0) {
	}
}
import org.xdat.Main;
import org.xdat.chart.Axis;
import org.xdat.chart.Filter;
import org.xdat.chart.ParallelCoordinatesChart;
import org.xdat.data.DataSheet;
import org.xdat.data.Design;
import org.xdat.data.Parameter;
import org.xdat.gui.frames.ChartFrame;
import org.xdat.gui.menus.parallelCoordinatesChart.ParallelCoordinatesContextMenu;
import org.xdat.gui.tables.DataSheetTableColumnModel;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
				g.setColor(filterColor);
				g.drawLine(uf.getXPos(), uf.getYPos(dataSheet), uf.getXPos() - chart.getFilterWidth(), uf.getYPos(dataSheet) - chart.getFilterHeight());
				g.drawLine(uf.getXPos(), uf.getYPos(dataSheet), uf.getXPos() + chart.getFilterWidth(), uf.getYPos(dataSheet) - chart.getFilterHeight());
				g.drawLine(uf.getXPos() - chart.getFilterWidth(), uf.getYPos(dataSheet) - chart.getFilterHeight(), uf.getXPos() + chart.getFilterWidth(), uf.getYPos(dataSheet) - chart.getFilterHeight());

				// upper Filter triangle Drawing

				g.drawLine(lf.getXPos(), lf.getYPos(dataSheet), lf.getXPos() - chart.getFilterWidth(), lf.getYPos(dataSheet) + chart.getFilterHeight());
				g.drawLine(lf.getXPos(), lf.getYPos(dataSheet), lf.getXPos() + chart.getFilterWidth(), lf.getYPos(dataSheet) + chart.getFilterHeight());
				g.drawLine(lf.getXPos() - chart.getFilterWidth(), lf.getYPos(dataSheet) + chart.getFilterHeight(), lf.getXPos() + chart.getFilterWidth(), lf.getYPos(dataSheet) + chart.getFilterHeight());

				g.setFont(new Font("SansSerif", Font.PLAIN, currentAxis.getTicLabelFontSize()));
				// log("Font size: "+currentAxis.getTicLabelFontSize());
				if ((uf == this.draggedFilter || lf == this.draggedFilter) && currentAxis.getParameter().isNumeric()) {
					g.drawString(String.format(currentAxis.getTicLabelFormat(), this.draggedFilter.getValue()), this.draggedFilter.getXPos() + chart.getFilterWidth() + 4, this.draggedFilter.getYPos(dataSheet) - chart.getFilterHeight());
				}

				// Filteraxis Drawing

				if (null != lastAxis) {
					g.drawLine(lastAxis.getUpperFilter().getXPos(), lastAxis.getUpperFilter().getYPos(dataSheet), uf.getXPos(), uf.getYPos(dataSheet));
					g.drawLine(lastAxis.getLowerFilter().getXPos(), lastAxis.getLowerFilter().getYPos(dataSheet), lf.getXPos(), lf.getYPos(dataSheet));
				}

				// tics

				int ticSize = currentAxis.getTicLength();
				int ticCount = currentAxis.getTicCount();
				double ticSpacing; // must be double to avoid large round off
									// errors
				if (ticCount > 1)
					ticSpacing = chart.getAxisHeight() / ((double) (ticCount - 1));
				else
					ticSpacing = 0;
				double axisRange = currentAxis.getRange();
				double ticValueDifference = axisRange / ((double) (ticCount - 1));
				for (int ticID = 0; ticID < ticCount; ticID++) {
					int currentTicYPos;
					if (currentAxis.isAxisInverted())
						currentTicYPos = yPosition + chart.getAxisHeight() - (int) (ticID * ticSpacing);
					else
						currentTicYPos = yPosition + (int) (ticID * ticSpacing);
					g.setColor(currentAxis.getAxisColor());
					if (ticCount > 1)
						g.drawLine(xPosition, currentTicYPos, xPosition + ticSize, currentTicYPos);
					else
						g.drawLine(xPosition, yPosition + (chart.getAxisHeight() / 2), xPosition + ticSize, yPosition + (chart.getAxisHeight() / 2));

					g.setColor(currentAxis.getAxisTicLabelFontColor());

					String ticLabel;
					g.setFont(new Font("SansSerif", Font.PLAIN, currentAxis.getTicLabelFontSize()));
					if (currentAxis.getParameter().isNumeric()) {
						double ticValue;
						if (ticCount > 1) {
							ticValue = currentAxis.getMax() - ticValueDifference * ticID;
							ticLabel = String.format(currentAxis.getTicLabelFormat(), ticValue);
							g.drawString(ticLabel, xPosition + ticSize + 7, currentTicYPos + (int) (0.5 * currentAxis.getTicLabelFontSize()));
						} else {
							ticValue = currentAxis.getMax();
							ticLabel = String.format(currentAxis.getTicLabelFormat(), ticValue);
							g.drawString(ticLabel, xPosition + 2 * ticSize, yPosition + chart.getAxisHeight() / 2 + (int) (0.5 * currentAxis.getTicLabelFontSize()));
						}

					} else {
						if (ticCount > 1) {
							ticLabel = currentAxis.getParameter().getStringValueOf(currentAxis.getMax() - ticValueDifference * ticID);
							g.drawString(ticLabel, xPosition + 2 * ticSize, currentTicYPos + (int) (0.5 * currentAxis.getTicLabelFontSize()));
						} else {
							ticLabel = currentAxis.getParameter().getStringValueOf(currentAxis.getMax());
							g.drawString(ticLabel, xPosition + 2 * ticSize, yPosition + chart.getAxisHeight() / 2 + (int) (0.5 * currentAxis.getTicLabelFontSize()));
						}
					}
				}

				lastAxis = currentAxis;
			}
		}
	}

	private Optional<Axis> getAxisAtLocation(int x)  {
		for (int i = 0; i < this.chart.getAxisCount(); i++) {
			Filter uf = this.chart.getAxis(i).getUpperFilter();
			if (this.chart.getAxis(i).isActive() && x >= uf.getXPos() - 0.5 * this.chart.getAxis(i).getWidth() && x < uf.getXPos() + 0.5 * this.chart.getAxis(i).getWidth()) {
				return Optional.of(this.chart.getAxis(i));
			}
		}
		return Optional.empty();
	}

	private int getNewAxisIndexAtLocation(int x) {
		for (int i = 0; i < this.chart.getAxisCount(); i++) {
			Filter uf = this.chart.getAxis(i).getUpperFilter();
			if (this.chart.getAxis(i).isActive() && x < uf.getXPos()) {
				return i;
			}
		}
		return this.chart.getAxisCount();
	}

	public void mouseClicked(MouseEvent e) {
		if (e.getButton() == 1 && updateHoverList(e.getX(), e.getY())) {
			List<Integer> newSelection = new ArrayList<>();
			DataSheet dataSheet = this.chart.getDataSheet();
			for(Integer designId : this.hoverList){
				Design d = dataSheet.getDesignByID(designId);
				d.setSelected(!d.isSelected());
			}
			
			for(int i=0; i<dataSheet.getDesignCount(); i++){
				if(dataSheet.getDesign(i).isSelected())
					newSelection.add(i);
			}
			
			this.mainWindow.getDataSheetTablePanel().setSelectedRows(newSelection);
		} else if (e.getButton() == 3) {
			int x = e.getX();
			int y = e.getY();

			this.getAxisAtLocation(x).ifPresent(axis ->
					(new ParallelCoordinatesContextMenu(this.mainWindow, this.chartFrame, axis)).show(this, x, y)