/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This program 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
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui.new_plotter.configuration;
import com.rapidminer.gui.new_plotter.ChartConfigurationException;
import com.rapidminer.gui.new_plotter.PlotConfigurationError;
import com.rapidminer.gui.new_plotter.configuration.DataTableColumn.ValueType;
import com.rapidminer.gui.new_plotter.configuration.DimensionConfig.PlotDimension;
import com.rapidminer.gui.new_plotter.configuration.LineFormat.LineStyle;
import com.rapidminer.gui.new_plotter.configuration.SeriesFormat.FillStyle;
import com.rapidminer.gui.new_plotter.configuration.SeriesFormat.IndicatorType;
import com.rapidminer.gui.new_plotter.configuration.SeriesFormat.ItemShape;
import com.rapidminer.gui.new_plotter.configuration.SeriesFormat.VisualizationType;
import com.rapidminer.gui.new_plotter.listener.AggregationWindowingListener;
import com.rapidminer.gui.new_plotter.listener.SeriesFormatListener;
import com.rapidminer.gui.new_plotter.listener.ValueSourceListener;
import com.rapidminer.gui.new_plotter.listener.events.DimensionConfigChangeEvent;
import com.rapidminer.gui.new_plotter.listener.events.SeriesFormatChangeEvent;
import com.rapidminer.gui.new_plotter.listener.events.ValueSourceChangeEvent;
import com.rapidminer.gui.new_plotter.listener.events.ValueSourceChangeEvent.ValueSourceChangeType;
import com.rapidminer.tools.I18N;
import com.rapidminer.tools.Ontology;
import com.rapidminer.tools.math.function.aggregation.AbstractAggregationFunction;
import com.rapidminer.tools.math.function.aggregation.AbstractAggregationFunction.AggregationFunctionType;
import com.rapidminer.tools.math.function.aggregation.AggregationFunction;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A source for actual plot values.
*
* Each ValueSource holds a reference to a DimensionConfig which configures the domain for this
* value source. Since all value sources share the same domain axis but hold individual domain
* configurations, there are some constraints for these configurations. Because of that the contract
* for the domain configuration is that all operations which change the value type or the categories
* of the domain must be passed through the {@link DomainConfigManager} of the the
* {@link PlotConfiguration} and may not be called directly on the object returned by
* getDomainConfig().
*
* @author Marius Helf, Nils Woehler
*/
public class ValueSource implements AggregationWindowingListener, SeriesFormatListener {
public enum SeriesUsageType {
MAIN_SERIES, // The main series, used for plotting the values of a series
INDICATOR_1, // Errors of the main series. If INDICATOR_2 is not set,
// symmetric error bars are generated based on INDICATOR_1.
// Otherwise INDICATOR_1 defines the upper error. The series
// must contain error values (like stdev or variance), not the
// absolute position of the error bars in the coordinate system.
INDICATOR_2, // Lower errors of the main series. If unset, a symmetric error
// from INDICATOR_1 is plotted.
}
private boolean useDomainGrouping;
private SeriesFormat format;
private Map<SeriesUsageType, DataTableColumn> dataTableColumnMap = new HashMap<SeriesUsageType, DataTableColumn>();
private List<WeakReference<ValueSourceListener>> listeners = new LinkedList<WeakReference<ValueSourceListener>>();
private Map<SeriesUsageType, AggregationFunction> aggregationFunctionMap = new HashMap<SeriesUsageType, AggregationFunction>();
private Map<SeriesUsageType, AggregationFunctionType> aggregationFunctionTypeMap = new HashMap<SeriesUsageType, AggregationFunctionType>();
private AggregationWindowing aggregationWindowing = new AggregationWindowing(0, 0, false);
private boolean useRelativeUtilities = true;
private String label;
private boolean autoNaming;
private final int Id;
private DomainConfigManager domainConfigManager;
/**
* Constructor for a default value source. Check if grouping is required by calling
* isGroupingRequired() at the destinated {@link RangeAxisConfig} parent.
*/
public ValueSource(PlotConfiguration plotConfiguration, DataTableColumn mainDataTableColumn,
AggregationFunctionType aggregationFunctionType, boolean grouped) {
this(plotConfiguration.getDomainConfigManager(), mainDataTableColumn, aggregationFunctionType, grouped,
plotConfiguration.getNextId());
}
/**
* Private C'tor that is used for cloning.
*/
private ValueSource(DomainConfigManager domainConfigManager, DataTableColumn mainDataTableColumn,
AggregationFunctionType aggregationFunctionType, boolean grouped, int Id) {
this.domainConfigManager = domainConfigManager;
this.Id = Id;
this.dataTableColumnMap.put(SeriesUsageType.MAIN_SERIES, mainDataTableColumn);
setAggregationFunction(SeriesUsageType.MAIN_SERIES, aggregationFunctionType);
setAggregationFunction(SeriesUsageType.INDICATOR_1, AggregationFunctionType.standard_deviation);
setAggregationFunction(SeriesUsageType.INDICATOR_2, AggregationFunctionType.standard_deviation);
aggregationWindowing.addAggregationWindowingListener(this);
format = new SeriesFormat();
format.addChangeListener(this);
setUseDomainGrouping(grouped);
this.setAutoNaming(true);
}
public AggregationFunction getAggregationFunction(SeriesUsageType usageType) {
return aggregationFunctionMap.get(usageType);
}
/**
* Returns the name of the {@link RangeAxisConfig}. Maybe <code>null</code>.
*/
public String getLabel() {
return label;
}
/**
* Sets the name of the plot range axis
*/
public void setLabel(String label) {
if (label == null ? label != this.label : !label.equals(this.label)) {
this.label = label;
fireValueSourceChanged(new ValueSourceChangeEvent(this, label));
}
}
private void setAutoLabelIfEnabled() {
if (isAutoNaming()) {
setLabel(getAutoLabel());
}
}
/**
* @return the autoName
*/
public boolean isAutoNaming() {
return autoNaming;
}
public String getAutoLabel() {
DataTableColumn dataTableColumn = getDataTableColumn(SeriesUsageType.MAIN_SERIES);
if (dataTableColumn != null) {
String label = dataTableColumn.getName();
if (isUsingDomainGrouping()) {
label = getAggregationFunctionType(SeriesUsageType.MAIN_SERIES) + "(" + label + ")";
}
return label;
} else {
return "-Empty-"; // TODO I18N
}
}
/**
* @param autoName
* the autoName to set
*/
public void setAutoNaming(boolean autoName) {
if (autoName != this.autoNaming) {
this.autoNaming = autoName;
setAutoLabelIfEnabled();
fireValueSourceChanged(new ValueSourceChangeEvent(this, ValueSourceChangeType.AUTO_NAMING, autoName));
}
}
public void setDataTableColumn(SeriesUsageType seriesUsage, DataTableColumn dataTableColumn)
throws ChartConfigurationException {
DataTableColumn oldDataTableColumn = dataTableColumnMap.get(seriesUsage);
if (dataTableColumn != null && !dataTableColumn.equals(oldDataTableColumn)) {
dataTableColumnMap.put(seriesUsage, dataTableColumn);
fireUsageTypeToColumnMapChanged(dataTableColumn, seriesUsage);
if (seriesUsage == SeriesUsageType.MAIN_SERIES) {
setAutoLabelIfEnabled();
}
} else if (dataTableColumn == null && dataTableColumn != oldDataTableColumn) {
if (seriesUsage == SeriesUsageType.MAIN_SERIES) {
throw new ChartConfigurationException("remove_main_domain_column");
}
dataTableColumnMap.remove(seriesUsage);
fireUsageTypeToColumnMapChanged(dataTableColumn, seriesUsage);
}
}
public void setAggregationFunction(SeriesUsageType seriesUsage, AggregationFunctionType functionType) {
if (functionType == null) {
aggregationFunctionMap.remove(seriesUsage);
if (seriesUsage == SeriesUsageType.MAIN_SERIES) {
setAutoLabelIfEnabled();
}
fireAggregationFunctionChanged(null, seriesUsage);
} else {
// return if aggregationFunctionName is equal to currently set name
AggregationFunction currentFunction = aggregationFunctionMap.get(seriesUsage);
String currentFunctionName = null;
if (currentFunction != null) {
currentFunctionName = currentFunction.getName();
}
if (functionType.toString().equals(currentFunctionName)) {
return;
}
AggregationFunction aggregationFunction = null;
try {
aggregationFunction = AbstractAggregationFunction.createAggregationFunction(functionType.toString());
} catch (InstantiationException e) {
throw new RuntimeException("Unknown aggregation function type " + functionType);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unknown aggregation function type " + functionType);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Unknown aggregation function type " + functionType);
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unknown aggregation function type " + functionType);
} catch (InvocationTargetException e) {
throw new RuntimeException("Unknown aggregation function type " + functionType);
}
aggregationFunctionMap.put(seriesUsage, aggregationFunction);
aggregationFunctionTypeMap.put(seriesUsage, functionType);
if (seriesUsage == SeriesUsageType.MAIN_SERIES) {
setAutoLabelIfEnabled();
}
fireAggregationFunctionChanged(aggregationFunction, seriesUsage);
}
}
public boolean isNominal() {
return getValueType() == ValueType.NOMINAL;
}
/**
* Returns the {@link ValueType} of the output values of this value source (which are not the
* necessarily the input values).
*/
public ValueType getValueType(SeriesUsageType usageType) {
if (!getDefinedUsageTypes().contains(usageType)) {
return ValueType.INVALID;
}
if (isUsingDomainGrouping()) {
ValueType valueType = getDataTableColumn(usageType).getValueType();
int rmInputValueType = ValueType.convertToRapidMinerOntology(valueType);
int rmOutputValueType = getAggregationFunction(usageType).getValueTypeOfResult(rmInputValueType);
ValueType valueTypeOfResult = ValueType.convertFromRapidMinerOntology(rmOutputValueType);
return valueTypeOfResult;
} else {
DataTableColumn dataTableColumn = dataTableColumnMap.get(usageType);
return dataTableColumn.getValueType();
}
}
/**
* Returns the associated domain config's value type.
*/
public ValueType getDomainConfigValueType() {
return getDomainConfig().getValueType();
}
public ValueType getValueType() {
return getValueType(SeriesUsageType.MAIN_SERIES);
}
public boolean isDate() {
return getValueType() == ValueType.DATE_TIME;
}
/**
* Returns true if the output values of this value source (not the necessarily the input values)
* are numerical, either because the input data itself is numerical, or the applied grouping and
* aggregation function results in numerical values.
*/
public boolean isNumerical() {
return getValueType() == ValueType.NUMERICAL;
}
public AggregationFunctionType getAggregationFunctionType(SeriesUsageType usageType) {
AggregationFunction aggregationFunction = aggregationFunctionMap.get(usageType);
if (aggregationFunction != null) {
return AggregationFunctionType.valueOf(aggregationFunction.getName());
} else {
return null;
}
}
public Set<SeriesUsageType> getDefinedUsageTypes() {
return dataTableColumnMap.keySet();
}
@Override
public String toString() {
if (label == null) {
return I18N.getGUILabel("plotter.unnamed_value_label");
}
return getLabel();
}
// public boolean useFormatFromDimensionConfig(PlotDimension dimension) {
// if (isUsingDomainGrouping()) {
// return false;
// }
// if (plotConfiguration.getDimensionConfig(dimension) != null) {
// return true;
// }
// return false;
// }
//
// public boolean hasSeriesValueForDimension(PlotDimension dimension) {
// DefaultDimensionConfig dimensionConfig = (DefaultDimensionConfig)
// plotConfiguration.getDimensionConfig(dimension);
// if (useSeriesFormatForDimension(dimension)) {
// return true;
// }
// if (dimensionConfig != null && !isUsingDomainGrouping()) {
// return false; // use value from DimensionConfig, e.g. different color for each item from
// ColorProvider
// }
// if (dimensionConfig != null && dimensionConfig.isGrouping()) {
// return true; // use value from DimensionConfig for this series
// }
//
// throw new
// RuntimeException("Should not happen, one of the above conditions should always be true.");
// }
public boolean useSeriesFormatForDimension(PlotConfiguration plotConfig, PlotDimension dimension) {
DefaultDimensionConfig dimensionConfig = (DefaultDimensionConfig) plotConfig.getDimensionConfig(dimension);
if (dimensionConfig == null) {
return true; // use value from SeriesFormat
}
if (!dimensionConfig.isGrouping() && isUsingDomainGrouping()) {
// aggregated, but not by given dimension
// --> use value from SeriesFormat (not possible to use e.g. color from ColorProvider
// for each item,
// because each item is aggregated from possibly many items with possibly different
// values for
// the given dimension)
return true;
}
return false;
}
public void addValueSourceListener(ValueSourceListener l) {
listeners.add(new WeakReference<ValueSourceListener>(l));
}
public void removeValueSourceListener(ValueSourceListener l) {
Iterator<WeakReference<ValueSourceListener>> it = listeners.iterator();
while (it.hasNext()) {
ValueSourceListener listener = it.next().get();
if (listener == null || listener == l) {
it.remove();
}
}
}
public SeriesFormat getSeriesFormat() {
return format;
}
public void setSeriesFormat(SeriesFormat format) {
if (format != this.format) {
if (this.format != null) {
format.removeChangeListener(this);
}
this.format = format;
if (format != null) {
format.addChangeListener(this);
}
}
}
private void fireValueSourceChanged(ValueSourceChangeEvent e) {
Iterator<WeakReference<ValueSourceListener>> it = listeners.iterator();
while (it.hasNext()) {
ValueSourceListener l = it.next().get();
if (l != null) {
l.valueSourceChanged(e);
} else {
it.remove();
}
}
}
private void fireAggregationFunctionChanged(AggregationFunction function, SeriesUsageType seriesUsage) {
if (function != null) {
fireValueSourceChanged(new ValueSourceChangeEvent(this, AggregationFunctionType.valueOf(function.getName()),
seriesUsage));
}
}
private void fireAggregationWindowingChanged() {
fireValueSourceChanged(new ValueSourceChangeEvent(this, aggregationWindowing));
}
private void fireUsageTypeToColumnMapChanged(DataTableColumn column, SeriesUsageType seriesUsage) {
fireValueSourceChanged(new ValueSourceChangeEvent(this, column, seriesUsage));
}
public void removeAllListeners() {
listeners.clear();
}
public DefaultDimensionConfig getDomainConfig() {
return domainConfigManager.getDomainConfig(useDomainGrouping);
}
@Override
public void seriesFormatChanged(SeriesFormatChangeEvent e) {
fireValueSourceChanged(new ValueSourceChangeEvent(this, e));
}
/**
* Returns true if this ValueSource delivers aggregated values of some kind.
*/
public boolean isUsingDomainGrouping() {
return useDomainGrouping;
}
/**
* Defines if this ValueSource uses the domain grouping provided by the
* {@link DomainConfigManager}.
*/
public void setUseDomainGrouping(boolean useDomainGrouping) {
if (this.useDomainGrouping != useDomainGrouping) {
this.useDomainGrouping = useDomainGrouping;
setAutoLabelIfEnabled();
fireValueSourceChanged(new ValueSourceChangeEvent(this, ValueSourceChangeType.USES_GROUPING, useDomainGrouping));
}
}
public void dimensionConfigChanged(DimensionConfigChangeEvent e) {
switch (e.getType()) {
case RESET:
case SORTING:
case COLUMN:
case GROUPING_CHANGED:
case RANGE:
fireValueSourceChanged(new ValueSourceChangeEvent(this));
break;
default:
}
}
/**
* Returns the {@link AggregationWindowing} for the domain dimension. Never returns null.
*/
public AggregationWindowing getAggregationWindowing() {
return aggregationWindowing;
}
public void setAggregationWindowing(AggregationWindowing aggregationWindowing) {
if (aggregationWindowing == null) {
throw new IllegalArgumentException("null not allowed for aggregationWindowing");
}
if (this.aggregationWindowing != aggregationWindowing) {
this.aggregationWindowing.removeAggregationWindowingListener(this);
this.aggregationWindowing = aggregationWindowing;
aggregationWindowing.addAggregationWindowingListener(this);
}
}
@Override
public void aggregationWindowingChanged(AggregationWindowing source) {
fireAggregationWindowingChanged();
}
public boolean isUsingRelativeIndicator() {
return useRelativeUtilities;
}
public void setUseRelativeUtilities(boolean relativeUtilities) {
if (relativeUtilities != this.useRelativeUtilities) {
this.useRelativeUtilities = relativeUtilities;
fireValueSourceChanged(new ValueSourceChangeEvent(this, ValueSourceChangeType.USE_RELATIVE_UTILITIES,
relativeUtilities));
}
}
@Override
public ValueSource clone() {
ValueSource clone = new ValueSource(domainConfigManager, null, null, useDomainGrouping, Id);
clone.setLabel(label);
clone.setAutoNaming(autoNaming);
clone.setSeriesFormat(format.clone());
clone.useRelativeUtilities = useRelativeUtilities;
clone.setAggregationWindowing(aggregationWindowing.clone());
for (Map.Entry<SeriesUsageType, AggregationFunctionType> entry : aggregationFunctionTypeMap.entrySet()) {
clone.setAggregationFunction(entry.getKey(), entry.getValue());
}
for (Map.Entry<SeriesUsageType, DataTableColumn> entry : dataTableColumnMap.entrySet()) {
try {
clone.setDataTableColumn(entry.getKey(), entry.getValue().clone());
} catch (ChartConfigurationException e) {
throw new RuntimeException("this should not happen");
}
}
return clone;
}
/**
* Returns the {@link DataTableColumn} associated with the specified {@link SeriesUsageType}.
*/
public DataTableColumn getDataTableColumn(SeriesUsageType seriesUsage) {
return dataTableColumnMap.get(seriesUsage);
}
public boolean doesAggregationFunctionSupportValueType(AggregationFunction function, ValueType valueType) {
switch (valueType) {
case DATE_TIME:
return function.supportsValueType(Ontology.DATE_TIME);
case NOMINAL:
return function.supportsValueType(Ontology.NOMINAL);
case NUMERICAL:
return function.supportsValueType(Ontology.NUMERICAL);
case INVALID:
case UNKNOWN:
return false;
default:
return false;
}
}
public List<PlotConfigurationError> getErrors() {
List<PlotConfigurationError> errors = new LinkedList<PlotConfigurationError>();
if (useDomainGrouping) {
for (SeriesUsageType usageType : getDefinedUsageTypes()) {
ValueType valueType = getDataTableColumn(usageType).getValueType();
AggregationFunction aggregationFunction = getAggregationFunction(usageType);
if (!doesAggregationFunctionSupportValueType(aggregationFunction, valueType)) {
errors.add(new PlotConfigurationError("value_type_not_supported_by_aggregation", this.toString(),
usageType, valueType, aggregationFunction.getName()));
}
}
}
// check that all series usage types have the same value type
ValueType valueType = getValueType();
if (format.getUtilityUsage() != IndicatorType.NONE && getDefinedUsageTypes().contains(SeriesUsageType.INDICATOR_1)) {
ValueType seriesValueType = getValueType(SeriesUsageType.INDICATOR_1);
if (seriesValueType != valueType) {
errors.add(new PlotConfigurationError("incompatible_utility_value_type", this.toString(),
SeriesUsageType.INDICATOR_1, valueType, seriesValueType));
}
}
if (format.getUtilityUsage() != IndicatorType.NONE && getDefinedUsageTypes().contains(SeriesUsageType.INDICATOR_2)) {
if (format.getUtilityUsage() != IndicatorType.DIFFERENCE) {
ValueType seriesValueType = getValueType(SeriesUsageType.INDICATOR_2);
if (seriesValueType != valueType) {
errors.add(new PlotConfigurationError("incompatible_utility_value_type", this.toString(),
SeriesUsageType.INDICATOR_2, valueType, seriesValueType));
}
}
}
return errors;
}
public List<PlotConfigurationError> getWarnings() {
List<PlotConfigurationError> warnings = new LinkedList<PlotConfigurationError>();
if (format.getSeriesType() == VisualizationType.LINES_AND_SHAPES) {
if (format.getItemShape() == ItemShape.NONE && format.getLineStyle() == LineStyle.NONE
&& format.getUtilityUsage() == IndicatorType.NONE) {
warnings.add(new PlotConfigurationError("invisible_format", this.toString()));
} else if (format.getItemShape() == ItemShape.NONE && format.getAreaFillStyle() == FillStyle.NONE) {
warnings.add(new PlotConfigurationError("invisible_format", this.toString()));
}
}
if (format.getUtilityUsage() == IndicatorType.NONE) {
if (getDataTableColumn(SeriesUsageType.INDICATOR_1) != null
|| getDataTableColumn(SeriesUsageType.INDICATOR_2) != null) {
warnings.add(new PlotConfigurationError("unused_utility_series", this.toString(), IndicatorType.NONE
.getName()));
}
} else if (format.getUtilityUsage() == IndicatorType.DIFFERENCE) {
if (getDataTableColumn(SeriesUsageType.INDICATOR_2) != null) {
warnings.add(new PlotConfigurationError("unused_secondary_utility_series", this.toString(),
IndicatorType.DIFFERENCE.getName()));
}
}
return warnings;
}
/**
* Returns true iff this {@link ValueSource} suggests to sample the data if it is large. Large
* is defined as being larger than the RapidMiner property rapidminer.gui.plotter.rows.maximum.<br>
*
* Currently this function returns true for non-aggregated scatter plots and false otherwise.
*/
public boolean isSamplingSuggested() {
if (!isUsingDomainGrouping()) {
return true;
} else {
return false;
}
}
/**
* @return the id
*/
public int getId() {
return Id;
}
/**
* Exchanges the domainConfigManager of this ValueSource. Should only be called directly after
* cloning.
*/
void setDomainConfigManager(DomainConfigManager domainConfigManager) {
this.domainConfigManager = domainConfigManager;
}
}