/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
Cyclos 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 2 of the License, or
(at your option) any later version.
Cyclos 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 Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.controls.reports.statistics.graphs;
import nl.strohalm.cyclos.entities.reports.StatisticalNumber;
import nl.strohalm.cyclos.utils.jfreeAsymmetric.AsymmetricStatisticalCategoryDataset;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.CategoryToPieDataset;
import org.jfree.data.category.DefaultIntervalCategoryDataset;
import org.jfree.data.general.Dataset;
import org.jfree.data.general.DatasetUtilities;
import org.jfree.util.TableOrder;
/**
* generates the dataset which is needed for the cewolf graphs, and takes care of needed manipulations of this dataset.<br>
* Is only called from StatisticalDataProducer; therefor the class has default and not public access. All of its methods are static.
*
* @author Rinke
*
*/
class GraphDatasetGenerator {
/**
* method calculates the scalefactor from the input dataset. Scaling means for example: if the numbers are in the range of millions (so: from 0 to
* 10 million), numbers 1 to 10 are shown on the y-axis, and the y-axis label indicates " (x 1.000.000)". Scaling is done per thousands.
*
* @param dataset
* @return the scaleFactor. A value of 1 correspondents with "x1000", a value of 2 with "x1.000.000".
*/
static byte calculateScaleFactor(final Object dataset) {
final CategoryDataset lDataset = (CategoryDataset) dataset;
final Number maxValue = DatasetUtilities.findMaximumRangeValue(lDataset);
return (byte) getThousands(maxValue.doubleValue());
}
/**
* Limits the series names for the graph to the indicated number of columns.<br>
* A table may have more columns than the graph has series (often, the growth column indicating the difference between the first two columns is
* shown in the table, but not in the graph).<br>
* This methods copies the columnHeaders to a String[] seriesNames, but strips the last columns. The original input param is not effected.
*
* @param tableSeries a <code>String[]</code> representing the original seriesNames as shown in the table. This may be the table column headers
* or the table row headers, depending if the data is "switched".
* @param numberOfColumnsToDrop if 1, it drops the last column; if 2, it drops the two last columns... etc...
* @return the seriesNames
*/
protected static String[] createGraphSeries(final String[] tableSeries, final int seriesCount) {
final String[] seriesNames = new String[seriesCount];
System.arraycopy(tableSeries, 0, seriesNames, 0, seriesCount);
return seriesNames;
}
/**
* generates the CategoryDataset for the graph from the raw input params
*
* @param tableData a <code>Number[][]</code> containing <code>StatisticalNumber</code>s, and representing the raw data
* @param seriesNames a <code>String[]</code> with the series names
* @param categories a <code>String[]</code> with the categories names
* @param hasErrorBars <code>true</code> if the graph should show error bars.
* @return a <code>CategoryDataset</code> which can be directly returned by <code>StatisticalDataProducer.getDataProducer</code>.
*/
static CategoryDataset generateDataset(final Number[][] tableData, final String[] seriesNames, final String[] categories, final boolean hasErrorBars) {
final int series = seriesNames.length;
final int cats = categories.length;
if (hasErrorBars) {
final AsymmetricStatisticalCategoryDataset ds = new AsymmetricStatisticalCategoryDataset();
for (int s = 0; s < series; s++) {
for (int c = 0; c < cats; c++) {
final StatisticalNumber data = (StatisticalNumber) tableData[s][c];
ds.add((data.isNull() ? null : data), data.getLowerBound(), data.getUpperBound(), seriesNames[s], categories[c]);
}
}
return ds;
}
final Number[][] endValues = new Number[series][cats];
final Number[][] startValues = new Number[series][cats];
for (int s = 0; s < series; s++) {
for (int c = 0; c < cats; c++) {
final Number value = tableData[s][c];
if (!((value.getClass() == StatisticalNumber.class) && ((StatisticalNumber) value).isNull())) {
endValues[s][c] = value;
startValues[s][c] = new Integer(0);
}
}
}
final CategoryDataset ds = new DefaultIntervalCategoryDataset(seriesNames, categories, startValues, endValues);
return ds;
}
/**
* generates a pie dataset by extracting it from a categoryDataset. It always takes the first column of this categoryDataset, so any surpluss
* columns are just ignored.
* @param keys a <code>String</code> array showing the legend Strings.
* @param tableData the two dimensional array which is the basis of the categoryDataset.
* @return a <code>PieDataset</code>.
*/
static Dataset generatePieDataset(final String[] keys, final Number[][] tableData) {
final CategoryDataset catDataset = GraphDatasetGenerator.generateDataset(tableData, keys, new String[] { "" }, false);
final CategoryToPieDataset dataset = new CategoryToPieDataset(catDataset, TableOrder.BY_COLUMN, 0);
return dataset;
}
/**
* gives the scaleFactor in thousands, for example for a graph axis. The axis can be scaled to reasonable numbers, where all numbers should be
* multiplied by 1000 to the power of the result. Example: if the number entered is 345, then the method returns 0. All numbers for the axis may
* be divided by 1000 to the power of 0. Example 2: if the number entered is 345000000, then the method returns 2. All numbers for the axis may
* divided by 1000 to the power of 2 (=1.000.000). Usually, the y-axis labels is appended with a string like "(x 1.000.000).
*
* @param number, usually the maximum value of a range
* @return a scaling factor, which is 1000 to the power of the result
*/
private static int getThousands(final double number) {
return (int) Math.floor(Math.log(number) / Math.log(1000));
}
/**
* scales the data for better presentation in the y-axis of the graph. For example: if the numbers are in the range of millions (so: from 0 to 10
* million), numbers 1 to 10 are shown on the y-axis, and the y-axis label indicates " (x 1.000.000)".
*
* @param dataset a <code>CategoryDataset</code> to be scaled.
* @param scaleFactor a <code>byte</code> indicating the factor by which it should be scaled. The real factor is Math.pow(1000, scaleFactor), so
* a scaleFactor of 1 means "x 1000".
* @param hasErrorBars a <code>boolean</code> if the graph should show error bars.
* @return a <code>CategoryDataset</code> which is scaled.
*/
static CategoryDataset scaleData(final CategoryDataset dataset, final byte scaleFactor, final boolean hasErrorBars) {
if (scaleFactor == 0) {
return dataset;
}
final double factor = Math.pow(1000, scaleFactor);
if (hasErrorBars) {
final AsymmetricStatisticalCategoryDataset scaledData = new AsymmetricStatisticalCategoryDataset();
for (int r = 0; r < dataset.getRowCount(); r++) {
for (int c = 0; c < dataset.getColumnCount(); c++) {
final StatisticalNumber dataPoint = (StatisticalNumber) StatisticalNumber.scale(dataset.getValue(r, c), factor);
final Comparable<?> columnKey = dataset.getColumnKey(c);
final Comparable<?> rowKey = dataset.getRowKey(r);
scaledData.add(dataPoint, (dataPoint == null) ? null : dataPoint.getLowerBound(), (dataPoint == null) ? null : dataPoint.getUpperBound(), rowKey, columnKey);
}
}
return scaledData;
}
final DefaultIntervalCategoryDataset scaledData = (DefaultIntervalCategoryDataset) dataset;
for (int r = 0; r < dataset.getRowCount(); r++) {
for (int c = 0; c < dataset.getColumnCount(); c++) {
final Number scaledValue = StatisticalNumber.scale(dataset.getValue(r, c), factor);
scaledData.setEndValue(r, dataset.getColumnKey(c), scaledValue);
}
}
return scaledData;
}
/**
* switches the x and y of a two dimensional array of numbers.
*
* @param input the input array
* @return a copy of the input array, but with rows and columns switched.
*/
static Number[][] switchXYData(final Number[][] input) {
final Number[][] switchedData = new Number[input[0].length][input.length];
for (int x = 0; x < input.length; x++) {
for (int y = 0; y < input[x].length; y++) {
switchedData[y][x] = input[x][y];
}
}
return switchedData;
}
private GraphDatasetGenerator() {
// do nothing private default constructor so it cannot be instantiated
}
}