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 UserPreferences {

    private static final UserPreferences INSTANCE = new UserPreferences();

	private static final Preferences PREFS = Preferences.userNodeForPackage(UserPreferences.class);

	public static final int IMPORT_FROM_HOMEDIR = 0;

	public static final int IMPORT_FROM_LASTDIR = 1;

	public static final int IMPORT_FROM_USERDIR = 2;

	public static final int LOCALE_US = 0;
	public static final int LOCALE_DE = 1;

    private UserPreferences() {
	}

	public static int getInt(Key key, int defaultValue){
	    return PREFS.getInt(key.getId(), defaultValue);
    }

    public static void putInt(Key key, int value) {
	    PREFS.putInt(key.getId(), value);
    }

    public static boolean getBoolean(Key key, boolean defaultValue){
        return PREFS.getBoolean(key.getId(), defaultValue);
    }

    public static void putBoolean(Key key, boolean value) {
        PREFS.putBoolean(key.getId(), value);
    }

    public static double getDouble(Key key, double defaultValue){
        return PREFS.getDouble(key.getId(), defaultValue);
    }

    public static void putDouble(Key key, double value) {
        PREFS.putDouble(key.getId(), value);
    }

    public static String getString(Key key, String defaultValue){
        return PREFS.get(key.getId(), defaultValue);
    }

    public static void putString(Key key, String value) {
        PREFS.put(key.getId(), value);
    }

	public static Color getColor(Key key, Color defaultValue) {
		int r = PREFS.getInt(key.getId() + "Red", defaultValue.getRed());
		int g = PREFS.getInt(key.getId() + "Green", defaultValue.getGreen());
		int b = PREFS.getInt(key.getId() + "Blue", defaultValue.getBlue());
		int a = PREFS.getInt(key.getId() + "Alpha", defaultValue.getAlpha());
		return new Color(r, g, b);
	}

	public static void putColor(Key key, Color value) {
		PREFS.putInt(key.getId() + "Red", value.getRed());
		PREFS.putInt(key.getId() + "Green", value.getGreen());
		PREFS.putInt(key.getId() + "Blue", value.getBlue());
		PREFS.putInt(key.getId() + "Alpha", value.getAlpha());
	}

	public static UserPreferences getInstance() {
		return INSTANCE;
	}

	public int getScatterChart2DDisplayMode() {
		return PREFS.getInt(Key.SCATTER_CHART_2D_DISPLAY_MODE.getId(), ScatterPlot2D.SHOW_ALL_DESIGNS);
	}

	public void setScatterChart2DDisplayMode(int ScatterChart2DDisplayMode) {
		PREFS.putInt(Key.SCATTER_CHART_2D_DISPLAY_MODE.getId(), ScatterChart2DDisplayMode);
	}

	public boolean isScatterChart2DAutofitX() {
		return PREFS.getBoolean(Key.SCATTER_CHART_2D_AUTOFIT_X.getId(), true);
	}

	public void setScatterChart2DAutofitX(boolean scatterChart2DAutofitX) {
		PREFS.putBoolean(Key.SCATTER_CHART_2D_AUTOFIT_X.getId(), scatterChart2DAutofitX);
	}

	public boolean isScatterChart2DAutofitY() {
		return PREFS.getBoolean(Key.SCATTER_CHART_2D_AUTOFIT_Y.getId(), true);
	}

	public void setScatterChart2DAutofitY(boolean scatterChart2DAutofitY) {
		PREFS.putBoolean(Key.SCATTER_CHART_2D_AUTOFIT_Y.getId(), scatterChart2DAutofitY);
	}


	public int getScatterChart2DAxisTitleFontsizeX() {
		return PREFS.getInt(Key.SCATTER_CHART_2D_AXIS_TITLE_FONTSIZE_X.getId(), 20);
	}

	public void setScatterChart2DAxisTitleFontsizeX(int scatterChart2DAxisTitleFontsizeX) {
		PREFS.putInt(Key.SCATTER_CHART_2D_AXIS_TITLE_FONTSIZE_X.getId(), scatterChart2DAxisTitleFontsizeX);
	}

	public int getScatterChart2DAxisTitleFontsizeY() {
		return PREFS.getInt(Key.SCATTER_CHART_2D_AXIS_TITLE_FONTSIZE_Y.getId(), 20);
	}

	public void setScatterChart2DAxisTitleFontsizeY(int scatterChart2DAxisTitleFontsizeY) {
		PREFS.putInt(Key.SCATTER_CHART_2D_AXIS_TITLE_FONTSIZE_Y.getId(), scatterChart2DAxisTitleFontsizeY);
	}

	public int getScatterChart2DTicCountX() {
		return PREFS.getInt(Key.SCATTER_CHART_2D_TIC_COUNT_X.getId(), 2);
	}

	public void setScatterChart2DTicCountX(int scatterChart2DTicCountX) {
		PREFS.putInt(Key.SCATTER_CHART_2D_TIC_COUNT_X.getId(), scatterChart2DTicCountX);
	}

	public int getScatterChart2DTicCountY() {
		return PREFS.getInt(Key.SCATTER_CHART_2D_TIC_COUNT_Y.getId(), 2);
	}

	public void setScatterChart2DTicCountY(int scatterChart2DTicCountY) {
		PREFS.putInt(Key.SCATTER_CHART_2D_TIC_COUNT_Y.getId(), scatterChart2DTicCountY);
	}

	public int getScatterChart2DTicLabelFontsizeX() {
		return PREFS.getInt(Key.SCATTER_CHART_2D_TIC_LABEL_FONTSIZE_X.getId(), 12);
	}

	public void setScatterChart2DTicLabelFontsizeX(int scatterChart2DTicLabelFontsizeX) {
		PREFS.putInt(Key.SCATTER_CHART_2D_TIC_LABEL_FONTSIZE_X.getId(), scatterChart2DTicLabelFontsizeX);
	}

	public int getScatterChart2DTicLabelFontsizeY() {
		return PREFS.getInt(Key.SCATTER_CHART_2D_TIC_LABEL_FONTSIZE_Y.getId(), 12);
	}

	public void setScatterChart2DTicLabelFontsizeY(int scatterChart2DTicLabelFontsizeY) {
		PREFS.putInt(Key.SCATTER_CHART_2D_TIC_LABEL_FONTSIZE_Y.getId(), scatterChart2DTicLabelFontsizeY);
	}

	public int getScatterChart2DDataPointSize() {
		return PREFS.getInt(Key.SCATTER_CHART_2D_DATA_POINT_SIZE.getId(), 3);
	}

	public void setScatterChart2DDataPointSize(int scatterChart2DDataPointSize) {
		PREFS.putInt(Key.SCATTER_CHART_2D_DATA_POINT_SIZE.getId(), scatterChart2DDataPointSize);
	}

	public Color getScatterChart2DForegroundColor() {
		int r = PREFS.getInt(Key.SCATTER_CHART_2D_FOREGROUND_COLOR.getId() + "Red", 0);
		int g = PREFS.getInt(Key.SCATTER_CHART_2D_FOREGROUND_COLOR.getId() + "Green", 0);
		int b = PREFS.getInt(Key.SCATTER_CHART_2D_FOREGROUND_COLOR.getId() + "Blue", 0);
		return new Color(r, g, b);
	}

	public void setScatterChart2DForegroundColor(Color scatterChart2DForegroundColor) {
		PREFS.putInt(Key.SCATTER_CHART_2D_FOREGROUND_COLOR.getId() + "Red", scatterChart2DForegroundColor.getRed());
		PREFS.putInt(Key.SCATTER_CHART_2D_FOREGROUND_COLOR.getId() + "Green", scatterChart2DForegroundColor.getGreen());
		PREFS.putInt(Key.SCATTER_CHART_2D_FOREGROUND_COLOR.getId() + "Blue", scatterChart2DForegroundColor.getBlue());
	}

	public Color getScatterChart2DBackgroundColor() {
		int r = PREFS.getInt(Key.SCATTER_CHART_2D_BACKGROUND_COLOR.getId() + "Red", 255);
		int g = PREFS.getInt(Key.SCATTER_CHART_2D_BACKGROUND_COLOR.getId() + "Green", 255);
		int b = PREFS.getInt(Key.SCATTER_CHART_2D_BACKGROUND_COLOR.getId() + "Blue", 255);
		return new Color(r, g, b);
	}

	public void setScatterChart2DBackgroundColor(Color scatterChart2DBackgroundColor) {
		PREFS.putInt(Key.SCATTER_CHART_2D_BACKGROUND_COLOR.getId() + "Red", scatterChart2DBackgroundColor.getRed());
		PREFS.putInt(Key.SCATTER_CHART_2D_BACKGROUND_COLOR.getId() + "Green", scatterChart2DBackgroundColor.getGreen());
		PREFS.putInt(Key.SCATTER_CHART_2D_BACKGROUND_COLOR.getId() + "Blue", scatterChart2DBackgroundColor.getBlue());
	}

	public Color getScatterChart2DActiveDesignColor() {
		int r = PREFS.getInt(Key.SCATTER_CHART_2D_ACTIVE_DESIGN_COLOR.getId() + "Red", 0);
		int g = PREFS.getInt(Key.SCATTER_CHART_2D_ACTIVE_DESIGN_COLOR.getId() + "Green", 150);
		int b = PREFS.getInt(Key.SCATTER_CHART_2D_ACTIVE_DESIGN_COLOR.getId() + "Blue", 0);
		return new Color(r, g, b);
	}

	public void setScatterChart2DActiveDesignColor(Color scatterChart2DActiveDesignColor) {
		PREFS.putInt(Key.SCATTER_CHART_2D_ACTIVE_DESIGN_COLOR.getId() + "Red", scatterChart2DActiveDesignColor.getRed());
		PREFS.putInt(Key.SCATTER_CHART_2D_ACTIVE_DESIGN_COLOR.getId() + "Green", scatterChart2DActiveDesignColor.getGreen());
		PREFS.putInt(Key.SCATTER_CHART_2D_ACTIVE_DESIGN_COLOR.getId() + "Blue", scatterChart2DActiveDesignColor.getBlue());
	}

	public Color getScatterChart2DSelectedDesignColor() {
		int r = PREFS.getInt(Key.SCATTER_CHART_2D_SELECTED_DESIGN_COLOR.getId() + "Red", 0);
		int g = PREFS.getInt(Key.SCATTER_CHART_2D_SELECTED_DESIGN_COLOR.getId() + "Green", 0);
		int b = PREFS.getInt(Key.SCATTER_CHART_2D_SELECTED_DESIGN_COLOR.getId() + "Blue", 150);
		return new Color(r, g, b);
	}

	public void setScatterChart2DSelectedDesignColor(Color scatterChart2DSelectedDesignColor) {
		PREFS.putInt(Key.SCATTER_CHART_2D_SELECTED_DESIGN_COLOR.getId() + "Red", scatterChart2DSelectedDesignColor.getRed());
		PREFS.putInt(Key.SCATTER_CHART_2D_SELECTED_DESIGN_COLOR.getId() + "Green", scatterChart2DSelectedDesignColor.getGreen());
		PREFS.putInt(Key.SCATTER_CHART_2D_SELECTED_DESIGN_COLOR.getId() + "Blue", scatterChart2DSelectedDesignColor.getBlue());
	}

	public int getParallelCoordinatesAxisTicLabelDigitCount(){
		return PREFS.getInt(Key.PARALLEL_COORDINATES_AXIS_TIC_LABEL_DIGIT_COUNT.getId(), 3);
	}

	public Color getParallelCoordinatesActiveDesignDefaultColor() {
		int r = PREFS.getInt(Key.ACTIVE_DESIGN_DEFAULT_COLOR.getId() + "Red", 0);
		int g = PREFS.getInt(Key.ACTIVE_DESIGN_DEFAULT_COLOR.getId() + "Green", 150);
		int b = PREFS.getInt(Key.ACTIVE_DESIGN_DEFAULT_COLOR.getId() + "Blue", 0);
		int a = PREFS.getInt(Key.ACTIVE_DESIGN_DEFAULT_COLOR.getId() + "Alpha", 128);
		return new Color(r, g, b, a);
	}

	public void setParallelCoordinatesActiveDesignDefaultColor(Color activeDesignDefaultColor) {
		PREFS.putInt(Key.ACTIVE_DESIGN_DEFAULT_COLOR.getId() + "Red", activeDesignDefaultColor.getRed());
		PREFS.putInt(Key.ACTIVE_DESIGN_DEFAULT_COLOR.getId() + "Green", activeDesignDefaultColor.getGreen());
		PREFS.putInt(Key.ACTIVE_DESIGN_DEFAULT_COLOR.getId() + "Blue", activeDesignDefaultColor.getBlue());
		PREFS.putInt(Key.ACTIVE_DESIGN_DEFAULT_COLOR.getId() + "Alpha", activeDesignDefaultColor.getAlpha());
	}

	public boolean isAntiAliasing() {
		return PREFS.getBoolean(Key.ANTI_ALIASING.getId(), true);
	}
	
	public boolean isUseAlpha() {
		return PREFS.getBoolean(Key.USE_ALPHA.getId(), false);
	}

	public int getDirToImportFrom() {
		return PREFS.getInt(Key.DIRECTORY_TO_IMPORT_FROM.getId(), IMPORT_FROM_LASTDIR);
	}

	public void setDirToImportFrom(int dirToImportFrom) {
		PREFS.putInt(Key.DIRECTORY_TO_IMPORT_FROM.getId(), dirToImportFrom);
	}

	public String getLastFile() {
		return PREFS.get(Key.LAST_FILE_BROWSING_DIRECTORY.getId(), System.getProperty("user.home"));
	}

	public void setLastFile(String lastFileBrowsingDirectory) {
		PREFS.put(Key.LAST_FILE_BROWSING_DIRECTORY.getId(), lastFileBrowsingDirectory);
	}

	public String getHomeDir() {
		return System.getProperty("user.home");
	}

	public String getUserDir() {
		return PREFS.get(Key.USER_DIR.getId(), getHomeDir());
	}

	public void setUserDir(String userDir) {
		PREFS.put(Key.USER_DIR.getId(), userDir);
	}

	public String getCurrentDir() {
		switch (this.getDirToImportFrom()) {
			case (IMPORT_FROM_LASTDIR): {
				return this.getLastFile();
			}
			case (IMPORT_FROM_USERDIR): {
				return this.getUserDir();
			}
			case (IMPORT_FROM_HOMEDIR):
				// deliberate fallthrough
			default: {
				return this.getHomeDir();
			}
		}

	}

	public String getDelimiter() {
		return PREFS.get(Key.DELIMITER.getId(), "\\s");
	}

	public void setDelimiter(String delimiter) {
		PREFS.put(Key.DELIMITER.getId(), delimiter);
	}

	public boolean isTreatConsecutiveAsOne() {
		return PREFS.getBoolean(Key.TREAT_CONSECUTIVE_AS_ONE.getId(), true);
	}

	public void setTreatConsecutiveAsOne(boolean treatConsecutiveAsOne) {
		PREFS.putBoolean(Key.TREAT_CONSECUTIVE_AS_ONE.getId(), treatConsecutiveAsOne);
	}

	public String getOtherDelimiter() {
		return PREFS.get(Key.OTHER_DELIMITER.getId(), "");
	}

	public void setOtherDelimiter(String otherDelimiter) {
		PREFS.put(Key.OTHER_DELIMITER.getId(), otherDelimiter);
	}

	public Locale getLocale() {
		int locale = PREFS.getInt(Key.LOCALE.getId(), LOCALE_US);
		switch (locale) {
			case (LOCALE_DE): {
				return Locale.GERMANY;
			}
			default: {
				return Locale.US;
			}
		}
	}

	public void setLocale(int locale) {
		PREFS.putInt(Key.LOCALE.getId(), locale);
	}

	public void resetToDefault() {
		try {
			PREFS.clear();
		} catch (BackingStoreException e) {
			e.printStackTrace();
		}
	}
} 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 Cluster implements Serializable {

	static final long serialVersionUID = 1L;
	private String name;
	private Color activeDesignColor;
	private Color activeDesignColorNoAlpha;
	private boolean active = true;
	private int lineThickness = 1;
	private int uniqueId;
	private transient List<ClusterListener> clusterListeners;
	public Cluster(String name, int uniqueId) {
		this.name = name;
		this.uniqueId = uniqueId;
		this.activeDesignColor = UserPreferences.getInstance().getParallelCoordinatesActiveDesignDefaultColor();
		this.activeDesignColorNoAlpha = new Color(this.activeDesignColor.getRed(), this.activeDesignColor.getGreen(), this.activeDesignColor.getBlue());
		initTransientData();
	}

	public Color getActiveDesignColor(boolean useAlpha) {
		return useAlpha ? activeDesignColor : activeDesignColorNoAlpha;
	}

	public void setActiveDesignColor(Color activeDesignColor) {
		this.activeDesignColor = activeDesignColor;
		this.activeDesignColorNoAlpha = new Color(this.activeDesignColor.getRed(), this.activeDesignColor.getGreen(), this.activeDesignColor.getBlue());
		this.fireColorChanged();
	}

	public int getLineThickness() {
		return lineThickness;
	}

	public void setLineThickness(int lineThickness) {
		this.lineThickness = lineThickness;
	}

	public String getName() {
		return name;
	}

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

	public boolean isActive() {
		return active;
	}

	public void setActive(boolean active) {
		this.active = active;
	}

	public Cluster duplicate() {
		Cluster duplication = new Cluster(this.name, this.uniqueId);
		duplication.setActive(this.active);
		duplication.setActiveDesignColor(this.activeDesignColor);
		duplication.setLineThickness(this.lineThickness);
		return duplication;
	}

	public boolean copySettingsTo(Cluster cluster) {
		boolean changed = !Objects.equals(cluster.name, this.name) ||
				cluster.active != this.active ||
				!Objects.equals(cluster.activeDesignColor, this.activeDesignColor) ||
				cluster.lineThickness != this.lineThickness;
		if (!changed) {
			return false;
		}
		cluster.setName(this.name);
		cluster.setActive(this.active);
		cluster.setActiveDesignColor(this.activeDesignColor);
		cluster.setLineThickness(this.lineThickness);
		return true;
	}

	public int getUniqueId() {
		return uniqueId;
	}
	
	public void addClusterListener(ClusterListener l){
		this.clusterListeners.add(l);
	}
	
	public void removeClusterListener(ClusterListener l){
		this.clusterListeners.remove(l);
	}
	
	private void fireNameChanged(){
		for(ClusterListener l : this.clusterListeners){
			l.onNameChanged(this);
		}
	}

	private void fireColorChanged(){
		for(ClusterListener l : this.clusterListeners){
			l.onColorChanged(this);
		}
	}

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Cluster cluster = (Cluster) o;
        return uniqueId == cluster.uniqueId;
    }

    @Override
    public int hashCode() {
        return Objects.hash(uniqueId);
    }

	void initTransientData() {
		this.clusterListeners = new ArrayList<>();
	}
} public class ClusterFactory {
    private int nextUid = 0;

    public ClusterFactory() {
    }

    public Cluster newCluster(List<Cluster> currentBuffer) {
        String newClusterName = getUniqueClusterName(currentBuffer);
        return new Cluster(newClusterName, nextUid++);
    }

    public String getUniqueClusterName(List<Cluster> currentBuffer) {
        String name = "Cluster 1";
        int id = 1;
        while (!isNameUnique(name, currentBuffer))
            name = "Cluster " + (id++);
        return name;
    }

    private boolean isNameUnique(String name, List<Cluster> currentBuffer) {
        boolean unique = true;
        for (int i = 0; i < currentBuffer.size(); i++) {
            if (name.equals(currentBuffer.get(i).getName())) {
                unique = false;
                break;
            }
        }
        return unique;
    }
} public class ClusterSet implements Serializable {

	static final long serialVersionUID = 1L;
	private DataSheet dataSheet;
	private List<Cluster> clusters = new LinkedList<>();

    public ClusterSet() {
    }

    public ClusterSet(DataSheet dataSheet) {
        this();
		this.dataSheet = dataSheet;
	}

	public void newCluster(ClusterFactory factory){
        Cluster newCluster = factory.newCluster(this.clusters);
        this.clusters.add(newCluster);
        this.dataSheet.onClustersUpdated(Collections.emptyList(), Collections.singletonList(newCluster), Collections.emptyList());
    }

	public Cluster getCluster(int i) {
		return this.clusters.get(i);
	}

	public Cluster getCluster(String clusterName) {
        for (Cluster cluster : this.clusters) {
            if (cluster.getName().equals(clusterName)) {
                return cluster;
            }
        }
		throw new IllegalArgumentException("Could not find cluster " + clusterName);
	}

	public Optional<Cluster> findClusterById(int id) {
        return this.clusters.stream()
                .filter(c -> c.getUniqueId() == id)
                .findAny();
    }

	public int getClusterCount() {
		return this.clusters.size();
	}

    public ClusterTableModel createTableModel(ClusterFactory factory) {
        return new ClusterTableModel(clusters, factory);
    }

    public void removeCluster(Cluster cluster) {
        boolean removed = this.clusters.remove(cluster);
        if (removed) {
            this.dataSheet.onClustersUpdated(Collections.emptyList(), Collections.emptyList(), Collections.singletonList(cluster));
        }
    }

    public List<Cluster> getClusters() {
        return clusters;
    }

    public void applyClustersBuffer(List<Cluster> clustersBuffer) {
        List<Cluster> removedClusters = new ArrayList<>();
        List<Cluster> changedClusters = new ArrayList<>();
        List<Cluster> addedClusters = new ArrayList<>();
        Iterator<Cluster> iterator = this.clusters.iterator();
        while(iterator.hasNext()){
            Cluster next = iterator.next();
            if(!clustersBuffer.contains(next)) {
                removedClusters.add(next);
                iterator.remove();
            }
        }

        for (Cluster bufferedCluster : clustersBuffer) {
            Cluster existing = this.clusters.stream()
                    .filter(b -> b.equals(bufferedCluster))
                    .findAny()
                    .orElse(null);
            if (existing == null) {
                Cluster added = bufferedCluster.duplicate();
                this.clusters.add(added);
                addedClusters.add(added);
            } else {
                boolean changed = bufferedCluster.copySettingsTo(existing);
                if (changed) {
                    changedClusters.add(existing);
                }
            }
        }
        this.dataSheet.onClustersUpdated(changedClusters, addedClusters, removedClusters);
    }

    public void initTransientData() {
        for (Cluster cluster : this.clusters) {
            cluster.initTransientData();
        }
    }
} 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 ColorChoiceButton extends JButton {

	private Color currentColor;
	private final int width;
	private final int height;
	
	public ColorChoiceButton(Color currentColor) {
		this(currentColor, null, 16, 16);
	}

	public ColorChoiceButton(Color currentColor, @Nullable String actionCommand) {
		this(currentColor, actionCommand, 16, 16);
	}

	public ColorChoiceButton(Color currentColor, @Nullable String actionCommand, int width, int height) {
		super();
		this.currentColor = currentColor;
		this.width = width;
		this.height = height;
		if (actionCommand != null) {
			this.setActionCommand(actionCommand);
		}
		this.setLayout(new GridLayout(1, 1));
		this.setPreferredSize(new Dimension(this.width, this.height));
	}

	@Override
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		g.setColor(this.currentColor);
		g.fillRect(0, 0, this.width, this.height);

	}

	public void setCurrentColor(Color currentColor) {
		this.currentColor = currentColor;
		this.repaint();
	}

	public Color getCurrentColor() {
		return currentColor;
	}

	public void setAlpha(int alpha){
		this.currentColor = new Color(this.currentColor.getRed(), this.currentColor.getGreen(), this.currentColor.getBlue(), alpha);
		this.repaint();
	}
} public class ParallelCoordinatesChartPanel extends ChartPanel implements MouseMotionListener, MouseListener, MouseWheelListener {
	static final long serialVersionUID = 5L;
	private final Main mainWindow;
	private final ChartFrame chartFrame;
	private final ParallelCoordinatesChart chart;
	private BufferedImage bufferedImage;
	private Filter draggedFilter;
	private Axis draggedAxis;
	private int dragStartX;
	private int dragStartY;
	private int dragCurrentX;
	private int dragOffsetY;
	private boolean dragSelecting = false;
	private final HashSet<Integer> hoverList;
	private final Map<int[], HashSet<Integer>> lineMap;

	public ParallelCoordinatesChartPanel(Main mainWindow, ChartFrame chartFrame, ParallelCoordinatesChart chart) {
		super(chart);
		this.mainWindow = mainWindow;
		this.chartFrame = chartFrame;
		this.chart = chart;
		this.addMouseListener(this);
		this.addMouseMotionListener(this);
		this.addMouseWheelListener(this);

		this.lineMap = new LinkedHashMap<>();
		this.hoverList = new HashSet<>();
		this.chart.addListener(source -> repaint());
	}

	public void paintComponent(Graphics g) {
		Graphics cg;
		BufferedImage canvas;
		if(this.chart.isAntiAliasing() || this.chart.isUseAlpha() ){
			canvas = new BufferedImage(this.getWidth(), this.getHeight(), BufferedImage.TYPE_INT_ARGB);
			cg = canvas.getGraphics();
		}
		else{
			canvas = null;
			cg = g;
		}
		if(this.chart.isAntiAliasing() ){
			Graphics2D graphics2D = (Graphics2D) cg;
			graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		}

		super.paintComponent(cg);

		if (this.draggedAxis != null) {
			cg.drawImage(this.bufferedImage, 0, 0, this);
			cg.setColor(new Color(100, 100, 100));
			int yPosition = chart.getAxisTopPos();
			int xPosition = this.dragCurrentX;
			cg.drawLine(xPosition - 1, yPosition, xPosition - 1, yPosition + (chart.getAxisHeight()));
			cg.drawLine(xPosition, yPosition, xPosition, yPosition + (chart.getAxisHeight()));
			cg.drawLine(xPosition + 1, yPosition, xPosition + 1, yPosition + (chart.getAxisHeight()));
		} else if (this.dragSelecting) {
			cg.drawImage(this.bufferedImage, 0, 0, this);
			this.drawSelection(cg);
		} else {
			this.drawDesigns(cg);
			this.drawAxes(cg);
		}

		if(canvas != null){
			g.drawImage(canvas, 0, 0, null);
		}
	}

	private void drawSelection(Graphics g) {
		if (dragSelecting) {
			Rectangle rec = getSelectionRectangle();

			g.setColor(new Color(50, 50, 50, 150));
			g.drawRect(rec.x, rec.y, rec.width, rec.height);

			g.setColor(new Color(50, 50, 50, 50));
			g.fillRect(rec.x, rec.y, rec.width, rec.height);
		}
	}
	
	private Rectangle getSelectionRectangle() {
		return pointsToRectangle(dragStartX, dragStartY, dragCurrentX, dragOffsetY);
	}

	private Rectangle pointsToRectangle(int x1, int y1, int x2, int y2) {
		if (x1 <= x2 && y1 >= y2) {
			// Quadrant 1
			return new Rectangle(x1, y2, x2 - x1, y1 - y2);
		} else if (x1 > x2 && y1 > y2) {
			// Quadrant 2
			return new Rectangle(x2, y2, x1 - x2, y1 - y2);
		} else if (x1 > x2 && y1 < y2) {
			// Quadrant 3
			return new Rectangle(x2, y1, x1 - x2, y2 - y1);
		} else {
			// Quadrant 4
			return new Rectangle(x1, y1, x2 - x1, y2 - y1);
		}
	}

	/**
	 * Returns the axis (actually its id) that is currently in the drag zone
	 * @param x the x coordinate where to look
	 * @return the axis id 
	 */
    private int withinAxisDragZone(int x) {
		for (int i = 0; i < chart.getAxisCount(); i++) {
			Axis axis = chart.getAxis(i);
			int axisPos = axis.getUpperFilter().getXPos();

			if (axis.isActive() && x >= axisPos - 10 && x < axisPos + 10) {
				return i;
			}
		}
		return -1;
	}

	private void drawDesigns(final Graphics g) {
		if(chart.isAntiAliasing()){
			Graphics2D graphics2D = (Graphics2D) g;
			graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		}
		ParallelCoordinatesChart chart = ((ParallelCoordinatesChart) getChart());
		int axisTopPos = chart.getAxisTopPos();
		int designLabelFontSize = chart.getDesignLabelFontSize();
		int axisCount = chart.getAxisCount();
		double[] axisRanges = new double[axisCount];
		int[] axisHeights = new int[axisCount];
		int[] axisWidths = new int[axisCount];
		double[] axisMaxValues = new double[axisCount];
		double[] axisMinValues = new double[axisCount];
		boolean[] axisActiveFlags = new boolean[axisCount];
		boolean[] axisInversionFlags = new boolean[axisCount];
		for (int i = 0; i < axisCount; i++) // read all the display settings and put them into arrays to improve rendering speed of the chart
		{
			axisRanges[i] = chart.getAxis(i).getMax() - chart.getAxis(i).getMin();
			axisHeights[i] = chart.getAxisHeight();
			axisWidths[i] = chart.getAxis(i).getWidth();
			axisMaxValues[i] = chart.getAxis(i).getMax();
			axisMinValues[i] = chart.getAxis(i).getMin();
			axisActiveFlags[i] = chart.getAxis(i).isActive();
			axisInversionFlags[i] = chart.getAxis(i).isAxisInverted();
		}
		Color activeDesignColor = chart.getActiveDesignColor();
		Color activeDesignColorNoAlpha = chart.getActiveDesignColorNoAlpha();
		Color filteredDesignColor = chart.getFilteredDesignColor();
		Color filteredDesignColorNoAlpha = chart.getFilteredDesignColorNoAlpha();
		Color selectedDesignColor = chart.getSelectedDesignColor();
		boolean showFilteredDesigns = chart.isShowFilteredDesigns();
		int chartLineThickness = chart.getLineThickness();
		boolean showOnlySelectedDesigns = chart.isShowOnlySelectedDesigns();
		int selectedDesignsLineThickness = chart.getSelectedDesignsLineThickness();

		this.lineMap.clear();
		boolean useAlpha = chart.isUseAlpha();
		List<Design> highlightedDesigns = new ArrayList<>();
		DataSheet dataSheet = mainWindow.getDataSheet();
		for (int designIndex = 0; designIndex < dataSheet.getDesignCount(); designIndex++) {
			Design currentDesign = dataSheet.getDesign(designIndex);
			if (!currentDesign.isInsideBounds(chart)){
				continue;
			}
			boolean currentDesignClusterActive = true;
			if (currentDesign.getCluster() != null) {
				currentDesignClusterActive = currentDesign.getCluster().isActive();
			}

			boolean currentDesignActive = currentDesign.isActive(chart);

			boolean displayDesign;

			if (showOnlySelectedDesigns) {
				displayDesign = currentDesign.isSelected() && (currentDesignActive || showFilteredDesigns) && (currentDesignClusterActive);
			} else {
				displayDesign = (currentDesignActive || showFilteredDesigns) && (currentDesignClusterActive);
			}

			if (displayDesign) {
				if ((showOnlySelectedDesigns || !currentDesign.isSelected())&&(!hoverList.contains(currentDesign.getId()))) {
					g.setColor(chart.getDesignColor(currentDesign, currentDesignActive, useAlpha, activeDesignColor, activeDesignColorNoAlpha, filteredDesignColor, filteredDesignColorNoAlpha));
					int lineThickness;
					if (currentDesign.getCluster() != null) {
						lineThickness = currentDesign.getCluster().getLineThickness();
					} else {
						lineThickness = chartLineThickness;
					}
					drawDesign(g, chart, axisTopPos, designLabelFontSize, axisCount, axisRanges, axisHeights, axisWidths, axisMaxValues, axisMinValues, axisActiveFlags,
                            axisInversionFlags, currentDesign, lineThickness);
				} else {
                    highlightedDesigns.add(currentDesign);
				}
			}
		}
        for (Design highlightedDesign : highlightedDesigns) {
			g.setColor(selectedDesignColor);
			drawDesign(g, chart, axisTopPos, designLabelFontSize, axisCount, axisRanges, axisHeights, axisWidths, axisMaxValues, axisMinValues, axisActiveFlags,
                            axisInversionFlags, highlightedDesign, selectedDesignsLineThickness);

        }
	}

    private void drawDesign(Graphics g, ParallelCoordinatesChart chart, int axisTopPos, int designLabelFontSize, int axisCount, double[] axisRanges, int[] axisHeights,
							int[] axisWidths, double[] axisMaxValues, double[] axisMinValues, boolean[] axisActiveFlags, boolean[] axisInversionFlags, Design currentDesign,
							int lineThickness) {
        int xPositionCurrent = getMarginLeft();
        int yPositionCurrent = axisTopPos;
        int xPositionLast = xPositionCurrent;
        int yPositionLast;
        boolean firstAxisDrawn = false;
        for (int i = 0; i < axisCount; i++) {
            int yPosition = axisTopPos;
            if (axisActiveFlags[i]) {
                Parameter parameter = this.mainWindow.getDataSheet().getParameter(i);
                double value = currentDesign.getDoubleValue(parameter);

                int yPositionRelToBottom;
                if (axisRanges[i] == 0) {
                    yPositionRelToBottom = (int) (axisHeights[i] * 0.5);
                } else {
                    double ratio;
                    if (axisInversionFlags[i]) {
                        ratio = (axisMaxValues[i] - value) / axisRanges[i];
                    } else {
                        ratio = (value - axisMinValues[i]) / axisRanges[i];
                    }
                    yPositionRelToBottom = (int) (axisHeights[i] * ratio);
                }

                yPositionLast = yPositionCurrent;
                yPositionCurrent = yPosition + (axisHeights[i]) - yPositionRelToBottom;

                FontMetrics fm = g.getFontMetrics();
                g.setFont(new Font("SansSerif", Font.PLAIN, designLabelFontSize));
                if (firstAxisDrawn) {
                    xPositionCurrent = xPositionCurrent + (int) (axisWidths[i] * 0.5);

                    if (lineThickness == 0) {
                        g.drawLine(xPositionLast - 3, yPositionLast, xPositionLast + 3, yPositionLast);
                        g.drawLine(xPositionCurrent - 3, yPositionCurrent, xPositionCurrent + 3, yPositionCurrent);
                    } else {
                        for (int t = 1; t <= lineThickness; t++) {
                            int deltaY = -(t / 2) * (2 * (t % 2) - 1);
                            g.drawLine(xPositionLast, yPositionLast + deltaY, xPositionCurrent, yPositionCurrent + deltaY);
                            int[] key = {xPositionLast, yPositionLast + deltaY, xPositionCurrent, yPositionCurrent + deltaY};

                            lineMap.computeIfAbsent(key, k -> new HashSet<>())
                                    .add(currentDesign.getId());

                        }
                    }

                } else {
                    firstAxisDrawn = true;
                    if (chart.isShowDesignIDs()) {
                        g.drawString(Integer.toString(currentDesign.getId()), xPositionCurrent - 5 - fm.stringWidth(Integer.toString(currentDesign.getId())), yPositionCurrent + (int) (0.5 * chart.getAxis(i).getTicLabelFontSize()));
                    }
                }
                xPositionLast = xPositionCurrent;
                xPositionCurrent = xPositionCurrent + (int) (axisWidths[i] * 0.5);
            }
        }
    }

	private void drawAxes(Graphics g) {
		ParallelCoordinatesChart chart = ((ParallelCoordinatesChart) this.getChart());
		int xPosition = this.getMarginLeft();
		int yPosition = chart.getAxisTopPos();
		FontMetrics fm = g.getFontMetrics();
		Axis lastAxis = null;
		Axis currentAxis;
		int drawnAxisCount = 0;
		Color filterColor = chart.getFilterColor();
        DataSheet dataSheet = mainWindow.getDataSheet();
        for (int i = 0; i < chart.getAxisCount(); i++) {
			if (chart.getAxis(i).isActive()) {
				// axes
				currentAxis = chart.getAxis(i);
				if (null != lastAxis) {
					xPosition = xPosition + (int) (lastAxis.getWidth() * 0.5) + (int) (currentAxis.getWidth() * 0.5);
				}

				String axisLabel = currentAxis.getName();
				int slenX = fm.stringWidth(axisLabel);
				g.setFont(new Font("SansSerif", Font.PLAIN, currentAxis.getAxisLabelFontSize()));

				int yLabelOffset = 0;
				if (chart.isVerticallyOffsetAxisLabels()) {
					yLabelOffset = ((drawnAxisCount++) % 2) * (chart.getMaxAxisLabelFontSize() + chart.getAxisLabelVerticalDistance());
				}

				// Axis Mainlabel Drawing

				g.setColor(currentAxis.getAxisLabelFontColor());
				g.drawString(axisLabel, xPosition - (int) (0.5 * slenX), chart.getMaxAxisLabelFontSize() + chart.getTopMargin() + yLabelOffset);

				g.setColor(currentAxis.getAxisColor());
				g.drawLine(xPosition, yPosition, xPosition, yPosition + (chart.getAxisHeight()));

				// Filters

				Filter uf = currentAxis.getUpperFilter();
				Filter lf = currentAxis.getLowerFilter();

				uf.setXPos(xPosition);
				lf.setXPos(xPosition);

				// lower Filter triangle Drawing
				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)
			);
		}

	}

	public void mouseEntered(MouseEvent e) {
	}

	public void mouseExited(MouseEvent e) {
		this.hoverList.clear();
		this.repaint();
	}

	public void mousePressed(MouseEvent e) {
		ParallelCoordinatesChart chart = ((ParallelCoordinatesChart) this.getChart());
		dragStartX = e.getX();
		dragStartY = e.getY();
        DataSheet dataSheet = mainWindow.getDataSheet();
        for (int i = 0; i < chart.getAxisCount(); i++) {
			Filter uf = chart.getAxis(i).getUpperFilter();
			Filter lf = chart.getAxis(i).getLowerFilter();
			if // check whether the drag operation started on the upper filter
			(chart.getAxis(i).isActive() && dragStartY >= uf.getYPos(dataSheet) - chart.getFilterHeight() && dragStartY <= uf.getYPos(dataSheet) && dragStartX >= uf.getXPos() - chart.getFilterWidth() && dragStartX <= uf.getXPos() + chart.getFilterWidth()) {
				this.draggedFilter = uf;
				this.dragOffsetY = uf.getYPos(dataSheet) - dragStartY;
				setCursor(new Cursor(Cursor.MOVE_CURSOR));
			} else if // check whether the drag operation started on the lower
						// filter
			(chart.getAxis(i).isActive() && dragStartY >= lf.getYPos(dataSheet) && dragStartY <= lf.getYPos(dataSheet) + chart.getFilterHeight() && dragStartX >= lf.getXPos() - chart.getFilterWidth() && dragStartX <= lf.getXPos() + chart.getFilterWidth()) {
				this.draggedFilter = lf;
				this.dragOffsetY = lf.getYPos(dataSheet) - dragStartY;
				setCursor(new Cursor(Cursor.MOVE_CURSOR));
			}
		}
		if (this.draggedFilter == null){
			this.storeBufferedImage();
			if ( e.getButton() == 1 && withinAxisDragZone(e.getX()) > -1) {
				this.getAxisAtLocation(dragStartX).ifPresent(axis -> {
					this.draggedAxis = axis;
					setCursor(new Cursor(Cursor.MOVE_CURSOR));
				});
			} else if (e.getButton() == 1) {
				this.hoverList.clear();
				this.dragSelecting = true;
			}
		}
	}

	public void mouseReleased(MouseEvent e) {
		setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
        DataSheet dataSheet = this.mainWindow.getDataSheet();

		boolean repaintRequired = false;
		if (this.draggedFilter != null) {
			repaintRequired = true;
			this.draggedFilter = null;
		}
		if (this.draggedAxis != null) {
			repaintRequired = true;
			int newIndex = this.getNewAxisIndexAtLocation(e.getX() - 1); // column index starts at  one, param index at 0
			DataSheetTableColumnModel cm = (DataSheetTableColumnModel) this.mainWindow.getDataSheetTablePanel().getDataTable().getColumnModel();
			int currentIndex = dataSheet.getParameterIndex(this.draggedAxis.getName()) + 1;
			if (newIndex > currentIndex) {
				cm.moveColumn(currentIndex, newIndex);
			} else if (newIndex < currentIndex) {
				cm.moveColumn(currentIndex, newIndex + 1);
			}
			this.draggedAxis = null;
		}
		if (dragSelecting) {
			repaintRequired = false;
			dragSelecting = false;

			Rectangle rec = getSelectionRectangle();

			dragStartX = dragStartY = dragCurrentX = dragOffsetY = 0;

			if (rec.x > 1 && rec.y > 1) {
				for (int i = 0; i < chart.getAxisCount(); i++) {
					if(!chart.getAxis(i).isActive()){
						continue;
					}
					Filter uf = chart.getAxis(i).getUpperFilter();
					Filter lf = chart.getAxis(i).getLowerFilter();

					if (rec.contains(uf.getXPos(), rec.y + 1)) {
						uf.setYPos(rec.y, dataSheet);
						lf.setYPos(rec.y + rec.height, dataSheet);
					}
				}
			}
		}
		if (repaintRequired) {
			this.mainWindow.repaintAllChartFrames();
		}
	}

	public void mouseMoved(MouseEvent e) {
		boolean needRepaint = false;

		if (withinAxisDragZone(e.getX()) > -1) {
			setCursor(new Cursor(Cursor.HAND_CURSOR));
		} else {
			setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
		}

		if (this.hoverList.size() > 0) {
			needRepaint = true;
		}

		this.hoverList.clear();

		if (withinAxisDragZone(e.getX()) < 0) {
			needRepaint = needRepaint || updateHoverList(e.getX(), e.getY());
		}

		if (needRepaint) {
			this.repaint();
		}
	}

	/**
	 * updates the hover list
	 * @param x the current mouse x position
	 * @param y the current mouse y position
	 * @return whether lines were added to the list
	 */
	private boolean updateHoverList(int x, int y) {
		boolean designsFound = false;
		this.hoverList.clear();
		for (Map.Entry<int[], HashSet<Integer>> mapEntry : this.lineMap.entrySet()) {
			int[] coords = mapEntry.getKey();
			HashSet<Integer> designIDs = mapEntry.getValue();

			int xStart = coords[0];
			int yStart = coords[1];
			int xEnd = coords[2];
			int yEnd = coords[3];

			if (x > xStart && x < xEnd && Math.min(yStart, yEnd) < y && Math.max(yStart, yEnd) > y) {
				int xDelta = xEnd - xStart;
				int xMiddle = xStart + (xDelta / 2);

				int yDelta = Math.abs(yEnd - yStart) / 2;
				int yMiddle = yEnd > yStart ? yStart + yDelta : yEnd + yDelta;

				int designCount = designIDs.size();
				int yOffset = (int) ((100. * (double) designCount) / (double) this.chart.getDataSheet().getDesignCount());

				int[] xPoints = new int[4];
				int[] yPoints = new int[4];

				// start
				xPoints[0] = xStart;
				yPoints[0] = yStart;

				// top
				xPoints[1] = xMiddle;
				yPoints[1] = yMiddle - yOffset - 2;

				// end
				xPoints[2] = xEnd;
				yPoints[2] = yEnd;

				// low
				xPoints[3] = xMiddle;
				yPoints[3] = yMiddle + yOffset + 2;

				Polygon poly = new Polygon(xPoints, yPoints, xPoints.length);

				if (poly.contains(x, y)) {
					this.hoverList.addAll(designIDs);
					designsFound = true;
				}
			}
		}
		return designsFound;
	}

	public void mouseDragged(MouseEvent e) {
        DataSheet dataSheet = this.mainWindow.getDataSheet();
		if (this.draggedFilter != null) {
			// try to make the filter follow the drag operation, but always keep
			// it within axis boundaries and opposite filter
			this.draggedFilter.setYPos(Math.max(Math.min(e.getY() + this.dragOffsetY, this.draggedFilter.getLowestPos(dataSheet)), this.draggedFilter.getHighestPos(dataSheet)), dataSheet);
		} else if (this.draggedAxis != null) {
			this.dragCurrentX = e.getX();
			this.repaint();
		} else if (dragSelecting) {
			this.dragCurrentX = e.getX();
			this.dragOffsetY = e.getY();
			this.repaint();
		}
	}

	@Override
	public void mouseWheelMoved(MouseWheelEvent e) {
		int modifier = e.getModifiers();
		if (modifier == 0) {
			this.chart.incrementAxisWidth(-e.getUnitsToScroll());
		} else if (modifier == 2) {
			int x = e.getX();
			this.getAxisAtLocation(x).ifPresent(axis ->
					axis.setWidth(Math.max(0, axis.getWidth() - e.getUnitsToScroll()))
			);
		} else if (modifier == 8) {
			int x = e.getX();
			this.getAxisAtLocation(x).ifPresent(axis ->
					axis.setTicCount(Math.max(2, axis.getTicCount() - e.getWheelRotation()), mainWindow.getDataSheet())
			);
		}
		this.repaint();
	}

	private void storeBufferedImage() {
		this.bufferedImage = new BufferedImage(chartFrame.getChartPanel().getWidth(), chartFrame.getChartPanel().getHeight(), BufferedImage.TYPE_INT_ARGB);
		Graphics g = this.bufferedImage.createGraphics();
		this.paintComponent(g);
		g.dispose();
	}

} public class ParallelCoordinatesChartSidebarPanel extends SidebarPanel {

	private static final Color CLUSTER_NAME_TEXT_FIELD_EDITING_COLOR = new Color(255, 205, 51);
	private static final Color CLUSTER_NAME_TEXT_FIELD_STANDARD_COLOR = new Color(230, 230, 230);
	private ParallelChartSidebarActionListener cmd;
	private ColorChoiceButton activeDesignColorButton;
	private JSlider activeDesignAlphaSlider;
	private JButton newClusterButton;
	private JScrollPane clusterScrollPane;
	private JPanel clusterPanel;
	private Map<Cluster, JSlider> clusterAlphaSliders;
	private Map<ClusterListener, Cluster> clusterListeners;

	public ParallelCoordinatesChartSidebarPanel(Main mainWindow, ChartFrame chartFrame, ChartPanel chartPanel, ParallelCoordinatesChart chart) {
		super(mainWindow, chartFrame, chartPanel, chart);
	}

	@Override
	protected void buildPanel(JPanel parentPanel) {
		// create components
		ParallelCoordinatesChart chart = (ParallelCoordinatesChart) getChart();
		parentPanel.setLayout(new BorderLayout());
		JPanel generalControlsPanel = new JPanel(new FlowLayout(FlowLayout.LEADING));
		generalControlsPanel.setPreferredSize(new Dimension(195, 140));
		parentPanel.add(generalControlsPanel, BorderLayout.NORTH);

		JPanel activeDesignColorButtonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
		JLabel activeDesignColorLabel = new JLabel("Active Design Color");
		activeDesignColorButtonPanel.add(activeDesignColorLabel);
		this.activeDesignColorButton = new ColorChoiceButton(chart.getDefaultDesignColor(true, chart.isUseAlpha()), "Active Design Color");
		this.activeDesignColorButton.addActionListener(e -> cmd.changeActiveDesignColor(chart));
		activeDesignColorButtonPanel.add(activeDesignColorButton);
		chart.getChartSettingsGroup().getColorSetting(Key.ACTIVE_DESIGN_DEFAULT_COLOR).addListener(
				(source, transaction) -> activeDesignColorButton.setCurrentColor(source.get())
		);
		generalControlsPanel.add(activeDesignColorButtonPanel);

		JPanel activeDesignAlphaSliderLabelPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
		JPanel activeDesignAlphaSliderPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
		activeDesignAlphaSliderLabelPanel.setPreferredSize(new Dimension(180, 20));
		activeDesignAlphaSliderPanel.setPreferredSize(new Dimension(180, 30));
		generalControlsPanel.add(activeDesignAlphaSliderLabelPanel);
		generalControlsPanel.add(activeDesignAlphaSliderPanel);
		JLabel activeDesignAlphaLabel = new JLabel("Transparency");
		activeDesignAlphaSliderLabelPanel.add(activeDesignAlphaLabel);
		this.activeDesignAlphaSlider = new JSlider(JSlider.HORIZONTAL, 0, 255, 0);
		this.activeDesignAlphaSlider.addChangeListener(e -> cmd.setActiveDesignColorAlpha(this.activeDesignAlphaSlider, chart));
		this.activeDesignAlphaSlider.setName("activeDesignAlphaSlider");
		this.activeDesignAlphaSlider.setPreferredSize(new Dimension(120, 30));
		chart.getChartSettingsGroup().getBooleanSetting(Key.USE_ALPHA).addListener(
				(source, transaction) -> {
					boolean enabled = source.get();
					setAlphaSlidersEnabled(enabled);
					chart.setUseAlpha(enabled);
				}
		);
		activeDesignAlphaSliderPanel.add(activeDesignAlphaSlider);

		JPanel addClusterButtonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
		addClusterButtonPanel.setPreferredSize(new Dimension(180, 35));
		newClusterButton = new JButton("Add Cluster");
		newClusterButton.addActionListener(e -> cmd.addCluster());
		addClusterButtonPanel.add(newClusterButton);
		generalControlsPanel.add(addClusterButtonPanel);

		parentPanel.add(clusterScrollPane, BorderLayout.CENTER);
		JPanel contentPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING));
		clusterScrollPane.setViewportView(contentPanel);

		this.clusterPanel = new JPanel(new GridLayout(0, 1));
		contentPanel.add(this.clusterPanel);

		this.activeDesignColorButton.setCurrentColor(chart.getDefaultDesignColor(true, chart.isUseAlpha()));
		if (chart.isUseAlpha())
			this.activeDesignAlphaSlider.setValue(chart.getDefaultDesignColor(true, chart.isUseAlpha()).getAlpha());
		else {
			this.activeDesignAlphaSlider.setValue(255);
			this.activeDesignAlphaSlider.setEnabled(false);
		}

		linkColorButtonToSlider(this.activeDesignColorButton, this.activeDesignAlphaSlider);
		this.updateClusterList(getMainWindow().getCurrentClusterSet());
		this.validate();
	}

	public ColorChoiceButton getActiveDesignColorButton() {
		return activeDesignColorButton;
	}

	public JSlider getActiveDesignAlphaSlider() {
		return activeDesignAlphaSlider;
	}

	public JSlider getClusterAlphaSlider(Cluster cluster) {
		if (this.clusterAlphaSliders.containsKey(cluster)) {
			return this.clusterAlphaSliders.get(cluster);
		}
		return null;
	}

	public void updateClusterList(ClusterSet clusterSet) {
		int clusterCount = clusterSet.getClusterCount();

		for (ClusterListener l : this.clusterListeners.keySet()) {
			this.clusterListeners.get(l).removeClusterListener(l);
		}
		this.clusterPanel.removeAll();
		this.clusterPanel.setLayout(new GridLayout(clusterCount, 1));
		this.clusterAlphaSliders.clear();

		for (int i = 0; i < clusterCount; i++) {
			final Cluster cluster = clusterSet.getCluster(i);
			JPanel wrapperPanel = new JPanel(new GridLayout(5, 1));

			final JTextField clusterNameTextField = new JTextField();
			clusterNameTextField.setEditable(true);
			clusterNameTextField.setBackground(CLUSTER_NAME_TEXT_FIELD_STANDARD_COLOR);
			clusterNameTextField.setToolTipText("Click to edit cluster name");
			clusterNameTextField.addActionListener(event ->
					handleUpdatedClusterName(clusterNameTextField, cluster, event.getActionCommand())
			);

			clusterNameTextField.addFocusListener(new FocusListener() {

				@Override
				public void focusLost(FocusEvent arg0) {
					clusterNameTextField.setBackground(CLUSTER_NAME_TEXT_FIELD_STANDARD_COLOR);
					clusterNameTextField.setToolTipText("Click to edit cluster name");
					handleUpdatedClusterName(clusterNameTextField, cluster, clusterNameTextField.getText());
				}

				@Override
				public void focusGained(FocusEvent arg0) {
					clusterNameTextField.setBackground(CLUSTER_NAME_TEXT_FIELD_EDITING_COLOR);
					clusterNameTextField.setToolTipText(null);
				}
			});
			// final JLabel clusterNameLabel = new JLabel();
			wrapperPanel.add(clusterNameTextField);

			JPanel clusterButtonPanel = new JPanel(new BorderLayout());
			wrapperPanel.add(clusterButtonPanel);

			final ColorChoiceButton clusterColorButton = new ColorChoiceButton(cluster.getActiveDesignColor(true), "clusterColor", 20, 25);
			clusterColorButton.addActionListener(e -> cmd.changeClusterColor(clusterColorButton, cluster));
			clusterButtonPanel.add(clusterColorButton, BorderLayout.WEST);

			final JButton removeClusterButton = new JButton("Remove");
			removeClusterButton.addActionListener(e -> cmd.removeCluster(cluster));
			clusterButtonPanel.add(removeClusterButton, BorderLayout.CENTER);

			final JButton applyClusterButton = new JButton("Apply");
			applyClusterButton.addActionListener(e -> cmd.applySettings((ParallelCoordinatesChart) getChart(), cluster));
			clusterButtonPanel.add(applyClusterButton, BorderLayout.EAST);

			JLabel clusterAlphaLabel = new JLabel("Transparency");
			wrapperPanel.add(clusterAlphaLabel);

			final JSlider clusterAlphaSlider = new JSlider(JSlider.HORIZONTAL, 0, 255, cluster.getActiveDesignColor(true).getAlpha());
			this.clusterAlphaSliders.put(cluster, clusterAlphaSlider);
			clusterAlphaSlider.setPreferredSize(new Dimension(150, 10));
			clusterAlphaSlider.addChangeListener(e -> cmd.setClusterAlpha(clusterAlphaSlider, cluster));
			wrapperPanel.add(clusterAlphaSlider);

			linkColorButtonToSlider(clusterColorButton, clusterAlphaSlider);

			final JCheckBox clusterActiveCheckbox = new JCheckBox("Active");
			clusterActiveCheckbox.addActionListener(e -> cmd.toggleClusterActive(clusterActiveCheckbox, cluster));
			clusterActiveCheckbox.setSelected(cluster.isActive());
			wrapperPanel.add(clusterActiveCheckbox);

			updateElementNames(cluster.getName(), clusterNameTextField, clusterColorButton, removeClusterButton, applyClusterButton, clusterAlphaSlider, clusterActiveCheckbox);

			ClusterListener listener = new ClusterListener() {
				@Override
				public void onNameChanged(Cluster source) {
					updateElementNames(source.getName(), clusterNameTextField, clusterColorButton, removeClusterButton, applyClusterButton, clusterAlphaSlider, clusterActiveCheckbox);
					revalidate();
					repaint();
				}

				@Override
				public void onColorChanged(Cluster source) {
					updateElementColor(clusterColorButton, source.getActiveDesignColor(getChart().isUseAlpha()));
					revalidate();
					repaint();
				}
			};
			cluster.addClusterListener(listener);

			this.clusterListeners.put(listener, cluster);
			this.clusterPanel.add(wrapperPanel);
		}

		this.revalidate();
		this.repaint();
	}

	private void linkColorButtonToSlider(final ColorChoiceButton colorButton, final JSlider clusterAlphaSlider) {
		clusterAlphaSlider.addChangeListener(event -> {
			int value = Math.min(clusterAlphaSlider.getValue(), 255);
			colorButton.setAlpha(value);
		});
	}

	private void updateElementNames(String name, JTextField clusterNameLabel, JButton clusterColorButton, JButton removeClusterButton, JButton applyClusterButton, JSlider clusterAlphaSlider, JCheckBox clusterActiveCheckbox) {
		if (!clusterNameLabel.getText().equals(name)) {
			clusterNameLabel.setText(name);
		}
		clusterColorButton.setName(name);
		removeClusterButton.setName(name);
		applyClusterButton.setName(name);
		clusterAlphaSlider.setName(name);
		clusterActiveCheckbox.setName(name);
	}

	private void updateElementColor(ColorChoiceButton clusterColorButton, Color color) {
		clusterColorButton.setCurrentColor(color);
	}

	private void handleUpdatedClusterName(JTextField source, Cluster cluster, String newName) {
		if (newName == null || newName.isEmpty()) {
			JOptionPane.showMessageDialog(this, "Invalid cluster name: \"" + newName + "\"", "Error on renaming cluster", JOptionPane.ERROR_MESSAGE);
			source.setText(cluster.getName());
		}
		if (Objects.equals(newName, cluster.getName())) {
			return;
		}
		cluster.setName(newName);
	}

	@Override
	protected void initialize() {
		clusterScrollPane = new JScrollPane();
		clusterScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
		clusterListeners = new HashMap<>();
		clusterAlphaSliders = new LinkedHashMap<>(10);
		this.cmd = new ParallelChartSidebarActionListener(getMainWindow(), this, (ParallelCoordinatesChartPanel) getChartPanel(), getMainWindow().getClusterFactory());
	}

	public void setAlphaSlidersEnabled(boolean enabled) {
		this.activeDesignAlphaSlider.setEnabled(enabled);
		for (JSlider slider : this.clusterAlphaSliders.values()) {
			slider.setEnabled(enabled);
		}
	}
}
import javax.swing.JCheckBox;
import javax.swing.JColorChooser;
import javax.swing.JSlider;
import java.awt.Color;
import org.xdat.Main;
import org.xdat.UserPreferences;
import org.xdat.chart.ParallelCoordinatesChart;
import org.xdat.data.Cluster;
import org.xdat.data.ClusterFactory;
import org.xdat.data.ClusterSet;
import org.xdat.data.DataSheet;
import org.xdat.gui.controls.ColorChoiceButton;
import org.xdat.gui.panels.ParallelCoordinatesChartPanel;
import org.xdat.gui.panels.ParallelCoordinatesChartSidebarPanel;
/*
 *  Copyright 2014, Enguerrand de Rochefort
 * 
 * This file is part of xdat.
 *
 * xdat is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * xdat is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with xdat.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */

package org.xdat.actionListeners.chartFrames;



public class ParallelChartSidebarActionListener {
	private final ParallelCoordinatesChartSidebarPanel sidePanel;
	private final Main mainWindow;
	private final ParallelCoordinatesChartPanel chartPanel;
	private final ClusterFactory clusterFactory;
	private Color activeDesignColor;

	public ParallelChartSidebarActionListener(Main mainWindow, ParallelCoordinatesChartSidebarPanel panel, ParallelCoordinatesChartPanel chartPanel, ClusterFactory clusterFactory) {
		this.mainWindow = mainWindow;
		this.sidePanel = panel;
		this.chartPanel = chartPanel;
		this.activeDesignColor = ((ParallelCoordinatesChart) chartPanel.getChart()).getDefaultDesignColor(true,  chartPanel.getChart().isUseAlpha());
		this.clusterFactory = clusterFactory;
	}

	public void changeClusterColor(ColorChoiceButton source, Cluster cluster) {