All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.kostikiadis.charts.MultiAxisLineChart Maven / Gradle / Ivy

Go to download

The Open Tool Development Kit provides packages and classes for easy implementation of Java tools or applications. Originally designed for test supporting software.

The newest version!
/*
 * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.kostikiadis.charts;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javafx.animation.FadeTransition;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.scene.shape.StrokeLineJoin;
import javafx.scene.chart.Axis;

/**
 * Line Chart plots a line connecting the data points in a series. The data
 * points themselves can be represented by symbols optionally. Line charts are
 * usually used to view data trends over time or category.
 * 
 * @param  the data type of the first axis
 * @param  the data type of the second axis
 * 
 * @since JavaFX 2.0
 */
public class MultiAxisLineChart extends MultiAxisChart {

	// -------------- PRIVATE FIELDS ------------------------------------------

	/**
	 * A multiplier for the Y values that we store for each series, it is used to
	 * animate in a new series
	 */
	private Map, DoubleProperty> seriesYMultiplierMap = new HashMap<>();
	private Legend legend = new Legend();
	private Timeline dataRemoveTimeline;
	@SuppressWarnings("unused")
	private Series seriesOfDataRemoved = null;
	@SuppressWarnings("unused")
	private Data dataItemBeingRemoved = null;
	private FadeTransition fadeSymbolTransition = null;
	private Map, Double> XYValueMap = new HashMap, Double>();
	private Timeline seriesRemoveTimeline = null;

	// -------------- PUBLIC PROPERTIES ----------------------------------------

	/**
	 * When true, CSS styleable symbols are created for any data items that don't
	 * have a symbol node specified.
	 */
	private BooleanProperty createSymbols = new StyleableBooleanProperty(true) {
		@Override
		protected void invalidated() {
			for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) {
				Series series = getData().get(seriesIndex);
				for (int itemIndex = 0; itemIndex < series.getData().size(); itemIndex++) {
					Data item = series.getData().get(itemIndex);
					Node symbol = item.getNode();
					if (get() && symbol == null) { // create any symbols
						symbol = createSymbol(series, getData().indexOf(series), item, itemIndex);
						getPlotChildren().add(symbol);
					} else if (!get() && symbol != null) { // remove symbols
						getPlotChildren().remove(symbol);
						symbol = null;
						item.setNode(null);
					}
				}
			}
			requestChartLayout();
		}

		@Override
		public Object getBean() {
			return MultiAxisLineChart.this;
		}

		@Override
		public String getName() {
			return "createSymbols";
		}

		@Override
		public CssMetaData, Boolean> getCssMetaData() {
			return null;
		}
	};

	/**
	 * Indicates whether symbols for data points will be created or not.
	 *
	 * @return true if symbols for data points will be created and false otherwise.
	 */
	public final boolean getCreateSymbols() {
		return createSymbols.getValue();
	}

	public final void setCreateSymbols(boolean value) {
		createSymbols.setValue(value);
	}

	public final BooleanProperty createSymbolsProperty() {
		return createSymbols;
	}

	/**
	 * Indicates whether the data passed to MultiAxisLineChart should be sorted by
	 * natural order of one of the axes. If this is set to
	 * {@link SortingPolicy#NONE}, the order in {@link #dataProperty()} will be
	 * used.
	 *
	 * @since JavaFX 8u40
	 * @see SortingPolicy
	 * @defaultValue SortingPolicy#X_AXIS
	 */
	private ObjectProperty axisSortingPolicy = new ObjectPropertyBase(SortingPolicy.X_AXIS) {
		@Override
		protected void invalidated() {
			requestChartLayout();
		}

		@Override
		public Object getBean() {
			return MultiAxisLineChart.this;
		}

		@Override
		public String getName() {
			return "axisSortingPolicy";
		}

	};

	public final SortingPolicy getAxisSortingPolicy() {
		return axisSortingPolicy.getValue();
	}

	public final void setAxisSortingPolicy(SortingPolicy value) {
		axisSortingPolicy.setValue(value);
	}

	public final ObjectProperty axisSortingPolicyProperty() {
		return axisSortingPolicy;
	}

	// -------------- CONSTRUCTORS ----------------------------------------------

	/**
	 * Construct a new MultiAxisLineChart with the given axis.
	 *
	 * @param xAxis  The x axis to use
	 * @param y1Axis The y1 axis to use
	 * @param y2Axis The y2 axis to use
	 *
	 */
	public MultiAxisLineChart(Axis xAxis, Axis y1Axis, Axis y2Axis) {
		this(xAxis, y1Axis, y2Axis, FXCollections.>observableArrayList());
	}

	/**
	 * Construct a new MultiAxisLineChart with the given axis and data.
	 *
	 * @param xAxis  The x axis to use
	 * @param y1Axis The y axis to use
	 * @param y2Axis The second y axis to use
	 * @param data   The data to use, this is the actual list used so any changes to
	 *               it will be reflected in the chart
	 */
	public MultiAxisLineChart(Axis xAxis, Axis y1Axis, Axis y2Axis, ObservableList> data) {
		super(xAxis, y1Axis, y2Axis);
		setLegend(legend);
		setData(data);
	}

	// --------------
	// METHODS-------------------------------------------------------------

	@Override
	protected void updateAxisRange() {
		final Axis xa = getXAxis();
		final Axis y1a = getY1Axis();
		final Axis y2a = getY2Axis();

		List xData = null;
		List y1Data = null;
		List y2Data = null;

		if (xa.isAutoRanging())
			xData = new ArrayList();
		if (y1a.isAutoRanging())
			y1Data = new ArrayList();
		if (y2a != null && y2a.isAutoRanging())
			y2Data = new ArrayList();

		if (xData != null || y1Data != null) {
			for (MultiAxisChart.Series series : getData()) {
				for (Data data : series.getData()) {
					if (xData != null)
						xData.add(data.getXValue());
					if (y1Data != null && (data.getExtraValue() == null || (int) data.getExtraValue() == Y1_AXIS)) {
						y1Data.add(data.getYValue());
					} else if (y2Data != null) {
						if (y2a == null)
							throw new NullPointerException("Y2 Axis is not defined.");
						y2Data.add(data.getYValue());
					}
				}
			}
			// RT-32838 No need to invalidate range if there is one data item - whose value
			// is zero.
			if (xData != null && !(xData.size() == 1 && getXAxis().toNumericValue(xData.get(0)) == 0)) {
				xa.invalidateRange(xData);
			}
			if (y1Data != null && !(y1Data.size() == 1 && getY1Axis().toNumericValue(y1Data.get(0)) == 0)) {
				y1a.invalidateRange(y1Data);
			}
			if (y2Data != null && !(y2Data.size() == 1 && getY2Axis().toNumericValue(y2Data.get(0)) == 0)) {
				y2a.invalidateRange(y2Data);
			}

		}
	}

	@Override
	protected void dataItemAdded(final Series series, int itemIndex, final Data item) {
		final Node symbol = createSymbol(series, getData().indexOf(series), item, itemIndex);

		if (symbol != null)
			getPlotChildren().add(symbol);

	}

	@Override
	protected void dataItemRemoved(final Data item, final Series series) {
		final Node symbol = item.getNode();

		if (symbol != null) {
			symbol.focusTraversableProperty().unbind();
		}

		// remove item from sorted list
		@SuppressWarnings("unused")
		int itemIndex = series.getItemIndex(item);

		item.setSeries(null);
		if (symbol != null)
			getPlotChildren().remove(symbol);
		removeDataItemFromDisplay(series, item);

		// Note: better animation here, point should move from old position to new
		// position at center point between prev and next symbols
	}

	@Override
	protected void dataItemChanged(Data item) {
	}

	@Override
	protected void seriesChanged(Change> c) {
		// Update style classes for all series lines and symbols
		// Note: is there a more efficient way of doing this?
		for (int i = 0; i < getDataSize(); i++) {
			final Series s = getData().get(i);
			Node seriesNode = s.getNode();
			if (seriesNode != null)
				seriesNode.getStyleClass().setAll("chart-series-line", "series" + i, s.defaultColorStyleClass);
		}
	}

	@Override
	protected void seriesAdded(Series series, int seriesIndex) {
		// create new path for series
		Path seriesLine = new Path();
		seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL);
		series.setNode(seriesLine);
		// create series Y multiplier
		DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier");
		seriesYMultiplierMap.put(series, seriesYAnimMultiplier);
		// handle any data already in series

		seriesYAnimMultiplier.setValue(1d);

		getPlotChildren().add(seriesLine);

		for (int j = 0; j < series.getData().size(); j++) {
			Data item = series.getData().get(j);
			final Node symbol = createSymbol(series, seriesIndex, item, j);
			if (symbol != null) {

				getPlotChildren().add(symbol);

			}
		}

	}

	private void updateDefaultColorIndex(final Series series) {
		int clearIndex = seriesColorMap.get(series);
		series.getNode().getStyleClass().remove(DEFAULT_COLOR + clearIndex);
		for (int j = 0; j < series.getData().size(); j++) {
			final Node node = series.getData().get(j).getNode();
			if (node != null) {
				node.getStyleClass().remove(DEFAULT_COLOR + clearIndex);
			}
		}
	}

	@Override
	protected void seriesRemoved(final Series series) {
		updateDefaultColorIndex(series);
		// remove all symbol nodes
		seriesYMultiplierMap.remove(series);

		getPlotChildren().remove(series.getNode());
		for (Data d : series.getData())
			getPlotChildren().remove(d.getNode());
		removeSeriesFromDisplay(series);

	}

	@Override
	protected void layoutPlotChildren() {
		List constructedPath = new ArrayList<>(getDataSize());
		for (int seriesIndex = 0; seriesIndex < getDataSize(); seriesIndex++) {
			Series series = getData().get(seriesIndex);
			final DoubleProperty seriesYAnimMultiplier = seriesYMultiplierMap.get(series);
			if (series.getNode() instanceof Path) {
				final ObservableList seriesLine = ((Path) series.getNode()).getElements();
				seriesLine.clear();
				constructedPath.clear();
				for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) {
					Data item = it.next();
					double x = getXAxis().getDisplayPosition(item.getCurrentX());

					double y = -1;
					if (item.getExtraValue() == null || (int) item.getExtraValue() == MultiAxisChart.Y1_AXIS) {
						y = getY1Axis().getDisplayPosition(getY1Axis().toRealValue(getY1Axis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue()));
					} else {
						if (getY2Axis() != null) {
							if (getY2Axis().isVisible()) {
								y = getY2Axis().getDisplayPosition(getY2Axis().toRealValue(getY2Axis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue()));
							} else {
								continue;
							}
						} else {
							throw new NullPointerException("Y2 axis is not defined.");
						}
					}

					if (Double.isNaN(x) || Double.isNaN(y)) {
						int nextIdx = series.getData().indexOf(item) + 1;
						if (nextIdx < series.getDataSize()) {
							Data next = series.getData().get(nextIdx);
							double nextX = getXAxis().getDisplayPosition(next.getXValue());
							double nextY = 0;
							if ((int) item.getExtraValue() == MultiAxisChart.Y1_AXIS) {
								nextY = getY1Axis().getDisplayPosition(getY1Axis().toRealValue(getY1Axis().toNumericValue(next.getYValue())));
							} else if ((int) item.getExtraValue() == MultiAxisChart.Y2_AXIS) {
								nextY = getY2Axis().getDisplayPosition(getY2Axis().toRealValue(getY2Axis().toNumericValue(next.getYValue())));
							}
							constructedPath.add(new MoveTo(nextX, nextY));
						}
					} else {
						constructedPath.add(new LineTo(x, y));
					}

					Node symbol = item.getNode();
					if (symbol != null) {
						final double w = symbol.prefWidth(-1);
						final double h = symbol.prefHeight(-1);
						symbol.resizeRelocate(x - (w / 2), y - (h / 2), w, h);
					}
				}
				switch (getAxisSortingPolicy()) {
				case X_AXIS:
					Collections.sort(constructedPath, (e1, e2) -> Double.compare(getX(e1), getX(e2)));
					break;
				case Y_AXIS:
					Collections.sort(constructedPath, (e1, e2) -> Double.compare(getY(e1), getY(e2)));
					break;
				case NONE:
					break;
				default:
					break;
				}

				if (!constructedPath.isEmpty()) {
					PathElement first = constructedPath.get(0);
					seriesLine.add(new MoveTo(getX(first), getY(first)));
					seriesLine.addAll(constructedPath);
				}
			}
		}
	}

	@Override
	@SuppressWarnings({ "rawtypes", "unchecked" })
	void dataBeingRemovedIsAdded(Data item, Series series) {
		if (fadeSymbolTransition != null) {
			fadeSymbolTransition.setOnFinished(null);
			fadeSymbolTransition.stop();
		}
		if (dataRemoveTimeline != null) {
			dataRemoveTimeline.setOnFinished(null);
			dataRemoveTimeline.stop();
		}
		final Node symbol = item.getNode();
		if (symbol != null)
			getPlotChildren().remove(symbol);

		item.setSeries(null);
		removeDataItemFromDisplay(series, item);

		// restore values to item
		Double value = XYValueMap.get(item);
		if (value != null) {
			item.setYValue(value);
			item.setCurrentY(value);
		}
		XYValueMap.clear();
	}

	@Override
	void seriesBeingRemovedIsAdded(Series series) {
		if (seriesRemoveTimeline != null) {
			seriesRemoveTimeline.setOnFinished(null);
			seriesRemoveTimeline.stop();
			getPlotChildren().remove(series.getNode());
			for (Data d : series.getData())
				getPlotChildren().remove(d.getNode());
			removeSeriesFromDisplay(series);
		}
	}

	private Node createSymbol(Series series, int seriesIndex, final Data item, int itemIndex) {
		Node symbol = item.getNode();
		// check if symbol has already been created
		if (symbol == null && getCreateSymbols()) {
			symbol = new StackPane();
			symbol.setAccessibleRole(AccessibleRole.TEXT);
			symbol.setAccessibleRoleDescription("Point");
			symbol.focusTraversableProperty().bind(Platform.accessibilityActiveProperty());
			item.setNode(symbol);
		}
		// set symbol styles
		if (symbol != null)
			symbol.getStyleClass().addAll("chart-line-symbol", "series" + seriesIndex, "data" + itemIndex, series.defaultColorStyleClass);
		return symbol;
	}

	/**
	 * This is called whenever a series is added or removed and the legend needs to
	 * be updated
	 */
	@Override
	protected void updateLegend() {
		legend.getItems().clear();
		if (getData() != null) {
			for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) {
				Series series = getData().get(seriesIndex);
				Legend.LegendItem legenditem = new Legend.LegendItem(series.getName());
				legenditem.getSymbol().getStyleClass().addAll("chart-line-symbol", "series" + seriesIndex, series.defaultColorStyleClass);

				legend.getItems().add(legenditem);
			}
		}
		if (legend.getItems().size() > 0) {
			if (getLegend() == null) {
				setLegend(legend);
			}
		} else {
			setLegend(null);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @since JavaFX 8.0
	 */
	@Override
	public List> getCssMetaData() {
		return getClassCssMetaData();
	}

	/**
	 * This enum defines a policy for {@link #axisSortingPolicyProperty()}.
	 * 
	 * @since JavaFX 8u40
	 */
	public static enum SortingPolicy {
		/**
		 * TODO
		 */
		NONE,
		/**
		 * The data is ordered by x axis.
		 */
		X_AXIS,
		/**
		 * The data is ordered by y axis.
		 */
		Y_AXIS
	}

	public double getX(PathElement element) {
		if (element instanceof LineTo) {
			return getX((LineTo) element);
		} else if (element instanceof MoveTo) {
			return getX((MoveTo) element);
		} else {
			throw new IllegalArgumentException(element + " is not a valid type");
		}
	}

	public double getX(LineTo element) {
		return element.getX();
	}

	public double getX(MoveTo element) {
		return element.getX();
	}

	public double getY(PathElement element) {
		if (element instanceof LineTo) {
			return getY((LineTo) element);
		} else if (element instanceof MoveTo) {
			return getY((MoveTo) element);
		} else {
			throw new IllegalArgumentException(element + " is not a valid type");
		}
	}

	public double getY(LineTo element) {
		return element.getY();
	}

	public double getY(MoveTo element) {
		return element.getY();
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy