/**
* 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.data;
import com.rapidminer.datatable.DataTable;
import com.rapidminer.datatable.DataTableRow;
import com.rapidminer.gui.new_plotter.ConfigurationChangeResponse;
import com.rapidminer.gui.new_plotter.PlotConfigurationError;
import com.rapidminer.gui.new_plotter.PlotConfigurationQuickFix;
import com.rapidminer.gui.new_plotter.StaticDebug;
import com.rapidminer.gui.new_plotter.configuration.DataTableColumn;
import com.rapidminer.gui.new_plotter.configuration.DataTableColumn.ValueType;
import com.rapidminer.gui.new_plotter.configuration.DefaultDimensionConfig;
import com.rapidminer.gui.new_plotter.configuration.DimensionConfig;
import com.rapidminer.gui.new_plotter.configuration.DimensionConfig.PlotDimension;
import com.rapidminer.gui.new_plotter.configuration.PlotConfiguration;
import com.rapidminer.gui.new_plotter.listener.events.DimensionConfigChangeEvent;
import com.rapidminer.gui.new_plotter.listener.events.DimensionConfigChangeEvent.DimensionConfigChangeType;
import com.rapidminer.gui.new_plotter.utility.CategoricalColorProvider;
import com.rapidminer.gui.new_plotter.utility.CategoricalSizeProvider;
import com.rapidminer.gui.new_plotter.utility.ColorProvider;
import com.rapidminer.gui.new_plotter.utility.ContinuousColorProvider;
import com.rapidminer.gui.new_plotter.utility.ContinuousSizeProvider;
import com.rapidminer.gui.new_plotter.utility.NumericalValueRange;
import com.rapidminer.gui.new_plotter.utility.ShapeProvider;
import com.rapidminer.gui.new_plotter.utility.SizeProvider;
import com.rapidminer.gui.new_plotter.utility.ValueRange;
import com.rapidminer.tools.I18N;
import com.rapidminer.tools.LogService;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
/**
* @author Marius Helf, Nils Woehler
*
*/
public class DimensionConfigData {
private PlotInstance plotInstance;
private SizeProvider sizeProvider = null;
private ColorProvider colorProvider = null;
private ShapeProvider shapeProvider = null;
private transient double cachedMinValue = Double.NaN;
private transient double cachedMaxValue = Double.NaN;
private transient double cachedMinGroupValue = Double.NaN;
private transient double cachedMaxGroupValue = Double.NaN;
private transient List<ValueRange> cachedValueGroups = null;
private transient List<Double> cachedValues = null;
private transient List<Double> cachedDistinctValues = null;
private DataTableColumnIndex columnIdx;
private DimensionConfigChangeEvent lastProcessedEvent = null;
private int dimensionConfigId;
public DimensionConfigData(PlotInstance plotInstance, DefaultDimensionConfig dimensionConfig) {
this.dimensionConfigId = dimensionConfig.getId();
this.plotInstance = plotInstance;
DataTable dataTable = plotInstance.getPlotData().getSortedDataTableWithoutImplicitUpdate();
this.columnIdx = new DataTableColumnIndex(getDimensionConfig().getDataTableColumn(), dataTable);
}
/**
* The returned values are sorted and filtered according to the selected sort order and value
* range.
*/
public List<Double> getValues() {
if (cachedValues == null) {
updateValueCache();
}
return cachedValues;
}
protected void updateShapeProvider() {
if (getDimensionConfig().isNominal()) {
List<Double> categoryList = new LinkedList<Double>();
if (getDimensionConfig().isGrouping()) {
for (ValueRange valueGroup : getGroupingModel()) {
categoryList.add(valueGroup.getValue());
}
} else {
categoryList = getDistinctValues();
}
setShapeProvider(new ShapeProvider(categoryList));
} else {
setShapeProvider(null);
}
}
private void setShapeProvider(ShapeProvider shapeProvider) {
if (this.shapeProvider != shapeProvider) {
this.shapeProvider = shapeProvider;
}
}
/**
* If isNominal() returns true, returns a string for the given value. If isGrouping() returns
* true, value must be an integer and this function returns the toString() method of the
* valueGroup with value==valueGroup.getValue(). If no grouping is found with
* value==valueGroup.getValue() return <code>null</code>. If isGrouping() returns false, calls
* the mapIndex() function of the underlying DataTable with value.
*
* If isNominal() returns false, this functions returns null.
*/
public String getStringForValue(double value) {
if (getDimensionConfig().isNominal()) {
if (getDimensionConfig().isGrouping()) {
List<ValueRange> groupingModel = getGroupingModel();
for (ValueRange range : groupingModel) {
// if range's value is equal to value or both are NaN (i.e. missing value):
if (range.getValue() == value || (Double.isNaN(range.getValue()) && Double.isNaN(value))) {
return range.toString();
}
}
return null;
} else {
DataTable dataTable = plotInstance.getPlotData().getValueMappingDataTable();
int columnIdx = DataTableColumn.getColumnIndex(dataTable, getDimensionConfig().getDataTableColumn());
String valueString;
if (Double.isNaN(value)) {
valueString = I18N.getGUILabel("plotter.unknown_value_label");
} else {
valueString = dataTable.mapIndex(columnIdx, (int) value);
}
return valueString;
}
} else {
return null;
}
}
private void invalidateFormatProviders() {
colorProvider = null;
shapeProvider = null;
sizeProvider = null;
}
private void updateGroupingModel() {
DefaultDimensionConfig dimensionConfig = getDimensionConfig();
double upperBound = Double.POSITIVE_INFINITY;
double lowerBound = Double.NEGATIVE_INFINITY;
if (dimensionConfig.isUsingUserDefinedUpperBound()) {
upperBound = dimensionConfig.getUserDefinedUpperBound();
}
if (dimensionConfig.isUsingUserDefinedLowerBound()) {
lowerBound = dimensionConfig.getUserDefinedLowerBound();
}
cachedValueGroups = dimensionConfig.getGrouping().getGroupingModel(plotInstance.getPlotData().getDataTable(),
upperBound, lowerBound);
int maxAllowedValueCount = PlotConfiguration.getMaxAllowedValueCount();
if (cachedValueGroups.size() > maxAllowedValueCount) {
ConfigurationChangeResponse response = new ConfigurationChangeResponse();
response.addError(new PlotConfigurationError("too_many_values_in_plot", dimensionConfig.getDimension().getName()));
plotInstance.getMasterOfDesaster().registerConfigurationChangeResponse(response);
cachedValueGroups = new LinkedList<ValueRange>();
cachedMinGroupValue = 0;
cachedMaxGroupValue = 1;
return;
}
cachedMinGroupValue = Double.POSITIVE_INFINITY;
cachedMaxGroupValue = Double.NEGATIVE_INFINITY;
for (ValueRange group : cachedValueGroups) {
double value = group.getValue();
if (value < cachedMinGroupValue) {
cachedMinGroupValue = value;
}
if (value > cachedMaxGroupValue) {
cachedMaxGroupValue = value;
}
}
}
/**
* May not contain null values. If there are no value groups (because this dimension is not
* grouping), this function returns null.
*
* Classes implementing this method are strongly advised to cache to list of value ranges, since
* this method might be called quite often, and the calculation of the grouping might be quite
* expensive.
*/
public List<ValueRange> getGroupingModel() {
if (getDimensionConfig().isGrouping()) {
if (cachedValueGroups == null) {
updateGroupingModel();
}
return cachedValueGroups;
} else {
return null;
}
}
public ColorProvider getColorProvider() {
if (colorProvider == null) {
updateColorProvider();
}
return colorProvider;
}
public SizeProvider getSizeProvider() {
if (sizeProvider == null) {
updateSizeProvider();
}
return sizeProvider;
}
private void setColorProvider(ColorProvider colorProvider) {
this.colorProvider = colorProvider;
}
/**
* @return a sorted list of all distinct values in this domain.
*/
public List<Double> getDistinctValues() {
if (cachedDistinctValues == null) {
cachedDistinctValues = new LinkedList<Double>();
if (getDimensionConfig().isGrouping()) {
List<ValueRange> valueRanges = getGroupingModel();
for (ValueRange range : valueRanges) {
if (range != null) {
cachedDistinctValues.add(range.getValue());
} else {
cachedDistinctValues.add(Double.NaN);
}
}
} else {
Set<Double> distinctValuesSet = new HashSet<Double>();
for (Double value : getValues()) {
// Set.add() returns true if the element was NOT present before the function
// call
if (distinctValuesSet.add(value)) {
cachedDistinctValues.add(value);
}
}
}
Collections.sort(cachedDistinctValues);
}
return cachedDistinctValues;
}
public ShapeProvider getShapeProvider() {
if (shapeProvider == null) {
updateShapeProvider();
}
return shapeProvider;
}
public void clearCache() {
cachedValues = null;
cachedValueGroups = null;
cachedDistinctValues = null;
cachedMinValue = Double.NaN;
cachedMaxValue = Double.NaN;
cachedMinGroupValue = Double.NaN;
cachedMaxGroupValue = Double.NaN;
columnIdx.invalidate();
invalidateFormatProviders();
}
/**
* Updates the cache of ungrouped values.
*/
protected void updateValueCache() {
DataTable dataTable = plotInstance.getPlotData().getDataTable();
cachedValues = new LinkedList<Double>();
// return if we don't have a valid column
if (columnIdx.getIndex() == -1) {
cachedMinValue = Double.NaN;
cachedMaxValue = Double.NaN;
return;
}
cachedMinValue = Double.POSITIVE_INFINITY;
cachedMaxValue = Double.NEGATIVE_INFINITY;
boolean useUserDefinedRange = getDimensionConfig().isUsingUserDefinedLowerBound()
|| getDimensionConfig().isUsingUserDefinedUpperBound();
// get user defined range (if necessary; only one bound may be required)
ValueRange userDefinedRange = null;
if (useUserDefinedRange) {
userDefinedRange = getDimensionConfig().getUserDefinedRangeClone(dataTable);
if (userDefinedRange instanceof NumericalValueRange) {
// set unused bounds to INFINITY
NumericalValueRange numericalUserDefinedRange = (NumericalValueRange) userDefinedRange;
if (!getDimensionConfig().isUsingUserDefinedLowerBound()) {
numericalUserDefinedRange.setLowerBound(Double.NEGATIVE_INFINITY);
}
if (!getDimensionConfig().isUsingUserDefinedUpperBound()) {
numericalUserDefinedRange.setUpperBound(Double.POSITIVE_INFINITY);
}
userDefinedRange = numericalUserDefinedRange;
}
}
for (DataTableRow row : dataTable) {
if (!useUserDefinedRange || userDefinedRange.keepRow(row)) {
Double value = row.getValue(columnIdx.getIndex());
// add value to value cache
cachedValues.add(value);
// update min/max values
if (!Double.isInfinite(value) && !Double.isNaN(value)) {
if (cachedMinValue > value) {
cachedMinValue = value;
}
if (cachedMaxValue < value) {
cachedMaxValue = value;
}
}
}
}
if (cachedMaxValue < cachedMinValue) {
boolean maxInfinite = Double.isInfinite(cachedMaxValue);
boolean minInfinite = Double.isInfinite(cachedMinValue);
if (maxInfinite || minInfinite) {
if (maxInfinite) {
cachedMaxValue = Double.POSITIVE_INFINITY;
}
if (minInfinite) {
cachedMinValue = Double.NEGATIVE_INFINITY;
}
} else {
cachedMaxValue = cachedMinValue + 1;
}
}
}
public NumericalValueRange getEffectiveRange() {
if (getDimensionConfig().isNominal()) {
return null;
}
double effectiveMin;
double effectiveMax;
if (!getDimensionConfig().isUsingUserDefinedLowerBound() || !getDimensionConfig().isUsingUserDefinedUpperBound()) {
if (Double.isNaN(cachedMaxValue)) {
updateValueCache();
debug(getDimensionConfig().getDimension() + " min: " + cachedMinValue);
debug(getDimensionConfig().getDimension() + " max: " + cachedMaxValue);
}
effectiveMin = cachedMinValue;
effectiveMax = cachedMaxValue;
if (!getDimensionConfig().isUsingUserDefinedLowerBound() && !getDimensionConfig().isUsingUserDefinedUpperBound()) {
return new NumericalValueRange(effectiveMin, effectiveMax, columnIdx.getIndex(), true, true);
} else if (getDimensionConfig().isUsingUserDefinedLowerBound()) {
effectiveMin = getDimensionConfig().getUserDefinedLowerBound();
if (effectiveMin > effectiveMax) {
effectiveMax = effectiveMin + 1;
}
return new NumericalValueRange(effectiveMin, effectiveMax, columnIdx.getIndex(), true, true);
} else {
effectiveMax = getDimensionConfig().getUserDefinedUpperBound();
if (effectiveMin > effectiveMax) {
effectiveMin = effectiveMax - 1;
}
return new NumericalValueRange(effectiveMin, effectiveMax, columnIdx.getIndex(), true, true);
}
} else {
return (NumericalValueRange) getDimensionConfig().getUserDefinedRangeClone(
plotInstance.getPlotData().getOriginalDataTable());
}
}
protected void updateColorProvider() {
if (getDimensionConfig().isNominal()) {
List<Double> categoryList;
categoryList = getDistinctValues();
setColorProvider(new CategoricalColorProvider(plotInstance, categoryList, 255));
} else {
setColorProvider(new ContinuousColorProvider(plotInstance, getMinValue(), getMaxValue(), 255,
getDimensionConfig().isLogarithmic()));
}
}
protected void updateSizeProvider() {
PlotConfiguration plotConfiguration = plotInstance.getCurrentPlotConfigurationClone();
if (getDimensionConfig().isNominal()) {
List<Double> categoryList;
categoryList = getDistinctValues();
setSizeProvider(new CategoricalSizeProvider(categoryList, plotConfiguration.getMinShapeSize(),
plotConfiguration.getMaxShapeSize()));
} else {
double minValue = getMinValue();
double maxValue = getMaxValue();
setSizeProvider(new ContinuousSizeProvider(minValue, maxValue, plotConfiguration.getMinShapeSize(),
plotConfiguration.getMaxShapeSize(), getDimensionConfig().isLogarithmic()));
}
}
private void setSizeProvider(SizeProvider sizeProvider) {
this.sizeProvider = sizeProvider;
}
/**
* If getRange() is a NumericalValueRange, returns the same as getRange().getLowerBound(). Else
* returns the smallest value in getAllValues(), excluding NEG_INFINITY and NaNs.
*/
public double getMinValue() {
if (getDimensionConfig().isGrouping()) {
if (Double.isNaN(cachedMinGroupValue)) {
updateGroupingModel();
}
return cachedMinGroupValue;
} else {
if (Double.isNaN(cachedMinValue)) {
updateValueCache();
}
return getEffectiveRange().getLowerBound();
}
}
/**
* If getRange() is not null, returns the same as getRange().getUpperBound(). Else returns the
* greatest value in getAllValues(), excluding POS_INFINITY and NaNs.
*/
public double getMaxValue() {
if (getDimensionConfig().isGrouping()) {
if (Double.isNaN(cachedMaxGroupValue)) {
updateGroupingModel();
}
return cachedMaxGroupValue;
} else {
if (Double.isNaN(cachedMaxValue)) {
updateValueCache();
}
return getEffectiveRange().getUpperBound();
}
}
public int getValueCount() {
if (getDimensionConfig().isGrouping()) {
return getGroupingModel().size();
} else {
return getValues().size();
}
}
public boolean hasDuplicateValues() {
int distinctValueCount = getDistinctValues().size();
return distinctValueCount < getValueCount();
}
public boolean isLogarithmicPossible() {
if (getMinValue() < 0.0) {
return false;
}
if (getDimensionConfig().isNominal()) {
return false;
}
return true;
}
public int getDistinctValueCount() {
return getDistinctValues().size();
}
public void dimensionConfigChanged(DimensionConfigChangeEvent e) {
if (e == null || e == lastProcessedEvent) {
return;
}
lastProcessedEvent = e;
// update dimension config to the one of the current clone
PlotDimension dimension = e.getSource().getDimension();
DimensionConfig dimConf = plotInstance.getCurrentPlotConfigurationClone().getDimensionConfig(dimension);
if (dimConf == null) {
debug("DimensionConfigData: ### ATTENTION #### DimensionConfig for dimension " + dimension
+ " is null! Meta change event?");
return;
}
switch (e.getType()) {
case COLUMN:
columnIdx.setDataTableColumn(dimConf.getDataTableColumn());
case RESET:
case GROUPING_CHANGED:
case RANGE:
case SORTING:
case SCALING:
clearCache();
invalidateFormatProviders();
break;
case COLOR_SCHEME:
debug("Color scheme has changed. " + this + " invalidate format providers");
invalidateFormatProviders();
break;
default:
}
}
private void debug(String string) {
StaticDebug.debug("DimensionConfigData: " + getDimensionConfig().getDimension() + " " + string);
}
public List<PlotConfigurationError> getErrors() {
List<PlotConfigurationError> errorList = new LinkedList<PlotConfigurationError>();
if (cachedValueGroups != null) {
plotInstance.getCurrentPlotConfigurationClone();
if (cachedValueGroups.size() > PlotConfiguration.getMaxAllowedValueCount()) {
errorList.add(new PlotConfigurationError("too_many_values_in_plot", getDimensionConfig().getDimension()
.getName()));
}
}
if (getDimensionConfig().getDimension() == PlotDimension.SHAPE) {
ShapeProvider shapeProvider = getShapeProvider();
if (shapeProvider != null && shapeProvider.maxCategoryCount() < getDistinctValues().size()) {
PlotConfigurationError error = new PlotConfigurationError("too_many_values_in_dimension",
PlotDimension.SHAPE.getName(), getDistinctValues().size(), shapeProvider.maxCategoryCount());
errorList.add(error);
}
}
if (!getDimensionConfig().isNominal() && getDimensionConfig().isLogarithmic() && getMinValue() <= 0.0) {
PlotConfigurationError error = new PlotConfigurationError("log_axis_contains_zero", getDimensionConfig()
.getDimension().getName());
PlotConfigurationQuickFix quickFix = new PlotConfigurationQuickFix(new DimensionConfigChangeEvent(
getDimensionConfig(), getDimensionConfig().getDimension(), false, DimensionConfigChangeType.SCALING));
error.addQuickFix(quickFix);
errorList.add(error);
}
return errorList;
}
public List<PlotConfigurationError> getWarnings() {
List<PlotConfigurationError> warnings = new LinkedList<PlotConfigurationError>();
// check if there are enough defined colors for all categories, or if we use darker.darker
if (getDimensionConfig() != null) {
if (getDimensionConfig().getDimension() == PlotDimension.COLOR) {
if (getDimensionConfig().getValueType() == ValueType.NOMINAL) {
ColorProvider colorProvider = getColorProvider();
int categoryCount = getDistinctValueCount();
int colorListSize = plotInstance.getCurrentPlotConfigurationClone().getActiveColorScheme().getColors()
.size();
if (colorProvider != null && colorListSize < categoryCount) {
PlotConfigurationError warning = new PlotConfigurationError("darken_category_colors", categoryCount,
colorListSize);
warnings.add(warning);
}
}
}
PlotData plotData = plotInstance.getPlotData();
if ((getDimensionConfig().isUsingUserDefinedLowerBound() || getDimensionConfig().isUsingUserDefinedUpperBound())
&& plotData.isDataTableValid() && plotData.getDataTable().getRowNumber() == 0) {
warnings.add(new PlotConfigurationError("user_range_includes_no_data", getDimensionConfig().getDimension()
.getName()));
}
} else {
LogService.getRoot().log(Level.WARNING,
"com.rapidminer.gui.new_plotter.data.DimensionConfigData.null_dimension_config");
}
return warnings;
}
public int getColumnIdx() {
dimensionConfigChanged(lastProcessedEvent);
return columnIdx.getIndex();
}
@Override
public String toString() {
return "DimensionConfigData for " + getDimensionConfig().toString();
}
public void setDataTable(DataTable dataTable) {
columnIdx.setDataTable(dataTable);
}
/**
* @return The dimension config with the id of this {@link DimensionConfigData} of the
* {@link PlotInstance#getCurrentPlotConfigurationClone()}.
*/
public DefaultDimensionConfig getDimensionConfig() {
return plotInstance.getCurrentPlotConfigurationClone().getDefaultDimensionConfigById(dimensionConfigId);
}
/**
* @param getDimensionConfig
* () the getDimensionConfig() to set
*/
// private void setDimensionConfig(DefaultDimensionConfig getDimensionConfig()) {
// if(getDimensionConfig() != null && getDimensionConfig().getId() ==
// this.getDimensionConfig().getId()) {
// this.getDimensionConfig() = getDimensionConfig();
// } else {
// throw new
// IllegalArgumentException("Trying to set getDimensionConfig() on dimensionConfigData with different id (or null) - this should not happen");
// }
// }
}