public static DefaultStatisticalCategoryDataset createDefaultStatisticalCategoryDataset(
      ValueSource valueSource, PlotInstance plotInstance) throws ChartPlottimeException {
    ValueSourceData valueSourceData = plotInstance.getPlotData().getValueSourceData(valueSource);
    assertMaxValueCountNotExceededOrThrowException(valueSourceData);
    GroupCellSeriesData dataForAllGroupCells = valueSourceData.getSeriesDataForAllGroupCells();
    DefaultStatisticalCategoryDataset dataset = new DefaultStatisticalCategoryDataset();
    DefaultDimensionConfig domainConfig = valueSource.getDomainConfig();
    DimensionConfigData domainConfigData =
        plotInstance.getPlotData().getDimensionConfigData(domainConfig);

    // Loop all group cells and add data to dataset
    for (GroupCellKeyAndData groupCellKeyAndData : dataForAllGroupCells) {
      GroupCellKey groupCellKey = groupCellKeyAndData.getKey();
      GroupCellData groupCellData = groupCellKeyAndData.getData();
      String seriesName =
          generateSeriesName(
              valueSource, groupCellKey, plotInstance.getCurrentPlotConfigurationClone());

      Map<PlotDimension, double[]> mainSeriesData =
          groupCellData.getDataForUsageType(SeriesUsageType.MAIN_SERIES);
      double[] xValues = mainSeriesData.get(PlotDimension.DOMAIN);
      double[] yValues = mainSeriesData.get(PlotDimension.VALUE);
      double[] yErrorValues = valueSourceData.getAbsoluteUtilityValues(groupCellKeyAndData, true);
      if (yErrorValues == null) {
        throw new ChartPlottimeException(
            "undefined_series", valueSource.toString(), SeriesUsageType.INDICATOR_1);
      }

      // this dataset does not support unsymmetric errors
      if (groupCellData.getDataForUsageType(SeriesUsageType.INDICATOR_2) != null) {
        throw new ChartPlottimeException(
            "unsymmetric_utility_not_supported", valueSource.toString());
      }

      int rowCount = xValues.length;

      // Loop all rows and add data to series
      double xValue;
      double yValue;
      double yErrorValue;
      String xString;
      for (int row = 0; row < rowCount; ++row) {
        xValue = xValues[row];
        xString = domainConfigData.getStringForValue(xValue);
        yValue = yValues[row];
        yErrorValue = yErrorValues[row] - yValue;

        dataset.add(yValue, yErrorValue, seriesName, xString);
      }
    }
    return dataset;
  }
  /**
   * @param valueSource
   * @param plotInstance
   * @param autoWidthFraction If this value is greater than 0, an auto width for the intervals is
   *     calculated such that the intervals nearest to each other touch. This value is then
   *     multiplied with the value of autoWidthFtraction. If unset, the intervals have width 0.
   * @param allowDuplicates
   * @param sortByDomain if true, the data is sorted by domain values (useful for bar and area
   *     charts)
   * @return
   * @throws ChartPlottimeException
   */
  public static XYSeriesCollection createXYSeriesCollection(
      ValueSource valueSource,
      PlotInstance plotInstance,
      double autoWidthFraction,
      boolean allowDuplicates,
      boolean sortByDomain)
      throws ChartPlottimeException {
    XYSeriesCollection xyDataset = new XYSeriesCollection();
    if (autoWidthFraction > 0) {
      xyDataset.setAutoWidth(true);
    } else {
      xyDataset.setAutoWidth(false);
      xyDataset.setIntervalWidth(0);
    }

    ValueSourceData valueSourceData = plotInstance.getPlotData().getValueSourceData(valueSource);
    assertMaxValueCountNotExceededOrThrowException(valueSourceData);
    GroupCellSeriesData dataForAllGroupCells = valueSourceData.getSeriesDataForAllGroupCells();

    // Loop over group cells and add data to dataset
    for (GroupCellKeyAndData groupCellKeyAndData : dataForAllGroupCells) {
      GroupCellKey groupCellKey = groupCellKeyAndData.getKey();
      GroupCellData groupCellData = groupCellKeyAndData.getData();

      String seriesName =
          generateSeriesName(
              valueSource, groupCellKey, plotInstance.getCurrentPlotConfigurationClone());

      XYSeries series = new XYSeries(seriesName, sortByDomain, allowDuplicates);
      Map<PlotDimension, double[]> dataForUsageType =
          groupCellData.getDataForUsageType(SeriesUsageType.MAIN_SERIES);
      int rowCount = dataForUsageType.get(PlotDimension.DOMAIN).length;
      double[] xValues = dataForUsageType.get(PlotDimension.DOMAIN);
      double[] yValues = dataForUsageType.get(PlotDimension.VALUE);

      try {
        // Loop over rows and add data to series
        for (int row = 0; row < rowCount; ++row) {
          double x = xValues[row];
          double y = yValues[row];
          if (!Double.isNaN(x)) {
            series.add(x, y);
          }
        }
      } catch (SeriesException e) {
        throw new ChartPlottimeException(
            "duplicate_value", valueSource.toString(), PlotDimension.DOMAIN.getName());
      }

      xyDataset.addSeries(series);
    }
    // intervals should not touch each other, so decrease auto width.
    if (xyDataset.getIntervalWidth() > 0) {
      xyDataset.setIntervalWidth(xyDataset.getIntervalWidth() * autoWidthFraction);
    }
    return xyDataset;
  }
  public static DefaultTableXYDataset createDefaultTableXYDataset(
      ValueSource valueSource, PlotInstance plotInstance) throws ChartPlottimeException {
    ValueSourceData valueSourceData = plotInstance.getPlotData().getValueSourceData(valueSource);
    assertMaxValueCountNotExceededOrThrowException(valueSourceData);
    GroupCellSeriesData dataForAllGroupCells = valueSourceData.getSeriesDataForAllGroupCells();
    DefaultTableXYDataset dataset = new DefaultTableXYDataset();

    // Loop all group cells and add data to dataset
    for (GroupCellKeyAndData groupCellKeyAndData : dataForAllGroupCells) {
      GroupCellKey groupCellKey = groupCellKeyAndData.getKey();
      GroupCellData groupCellData = groupCellKeyAndData.getData();

      Map<PlotDimension, double[]> dataForUsageType =
          groupCellData.getDataForUsageType(SeriesUsageType.MAIN_SERIES);
      double[] xValues = dataForUsageType.get(PlotDimension.DOMAIN);
      double[] yValues = dataForUsageType.get(PlotDimension.VALUE);
      int rowCount = xValues.length;

      String seriesName =
          generateSeriesName(
              valueSource, groupCellKey, plotInstance.getCurrentPlotConfigurationClone());

      XYSeries series = new XYSeries(seriesName, false, false);

      // Loop all rows and add data to series
      double x;
      double y;
      for (int row = 0; row < rowCount; ++row) {
        x = xValues[row];
        y = yValues[row];
        try {
          if (!Double.isNaN(x)) {
            series.add(
                x, y,
                false); // false means: do not notify. Since we are currently initializing, this is
                        // not necessary and saves us some fractions of a second
          }
        } catch (SeriesException e) {
          throw new ChartPlottimeException(
              "duplicate_value", valueSource.toString(), PlotDimension.DOMAIN.getName());
        }
      }
      dataset.addSeries(series);
    }
    dataset.setAutoWidth(true);
    return dataset;
  }
  private static void addSeriesToDefaultXYDataset(
      ValueSource valueSource, int seriesIdx, PlotInstance plotInstance, DefaultXYDataset dataset)
      throws ChartPlottimeException {
    final int xIdx = 0;
    final int yIdx = 1;

    ValueSourceData valueSourceData = plotInstance.getPlotData().getValueSourceData(valueSource);
    assertMaxValueCountNotExceededOrThrowException(valueSourceData);

    GroupCellSeriesData dataForAllGroupCells = valueSourceData.getSeriesDataForAllGroupCells();
    GroupCellKeyAndData groupCellKeyAndData =
        dataForAllGroupCells.getGroupCellKeyAndData(seriesIdx);

    GroupCellKey groupCellKey = groupCellKeyAndData.getKey();
    GroupCellData groupCellData = groupCellKeyAndData.getData();

    // create series name
    GroupCellKey groupCellKeyClone = (GroupCellKey) groupCellKey.clone();
    String seriesName =
        generateSeriesName(
            valueSource, groupCellKeyClone, plotInstance.getCurrentPlotConfigurationClone());
    String differenceName = "__&%" + seriesName + "%&__";

    Map<PlotDimension, double[]> mainData =
        groupCellData.getDataForUsageType(SeriesUsageType.MAIN_SERIES);
    double[] xValues = mainData.get(PlotDimension.DOMAIN);
    double[] yValues = mainData.get(PlotDimension.VALUE);

    double[][] mainSeries = new double[2][xValues.length];
    mainSeries[xIdx] = xValues;
    mainSeries[yIdx] = yValues;
    dataset.addSeries(seriesName, mainSeries);

    if (valueSource.getSeriesFormat().getUtilityUsage() == IndicatorType.DIFFERENCE) {
      double[] differenceValues =
          valueSourceData.getAbsoluteUtilityValues(groupCellKeyAndData, true);
      if (differenceValues == null) {
        throw new ChartPlottimeException(
            "undefined_series", valueSource.toString(), SeriesUsageType.INDICATOR_1);
      }
      double[][] differenceSeries = new double[2][xValues.length];
      differenceSeries[xIdx] = xValues;
      differenceSeries[yIdx] = differenceValues;
      dataset.addSeries(differenceName, differenceSeries);
    }
  }
  public static CategoryDataset createDefaultCategoryDataset(
      ValueSource valueSource,
      PlotInstance plotInstance,
      boolean fillWithZero,
      boolean allowValuesLessThanZero)
      throws ChartPlottimeException {
    ValueSourceData valueSourceData = plotInstance.getPlotData().getValueSourceData(valueSource);
    assertMaxValueCountNotExceededOrThrowException(valueSourceData);
    GroupCellSeriesData dataForAllGroupCells = valueSourceData.getSeriesDataForAllGroupCells();
    DefaultDimensionConfig domainConfig = valueSource.getDomainConfig();
    DimensionConfigData domainConfigData =
        plotInstance.getPlotData().getDimensionConfigData(domainConfig);
    DefaultCategoryDataset dataset = new DefaultCategoryDataset();

    for (GroupCellKeyAndData groupCellKeyAndData : dataForAllGroupCells) {

      GroupCellKey groupCellKey = groupCellKeyAndData.getKey();
      GroupCellData groupCellData = groupCellKeyAndData.getData();

      // create series name
      GroupCellKey groupCellKeyClone = (GroupCellKey) groupCellKey.clone();
      String seriesName =
          generateSeriesName(
              valueSource, groupCellKeyClone, plotInstance.getCurrentPlotConfigurationClone());

      Map<PlotDimension, double[]> dataForUsageType =
          groupCellData.getDataForUsageType(SeriesUsageType.MAIN_SERIES);
      double[] xValues = dataForUsageType.get(PlotDimension.DOMAIN);
      double[] yValues = dataForUsageType.get(PlotDimension.VALUE);

      int rowCount = xValues.length;
      for (int row = 0; row < rowCount; ++row) {
        double xValue = xValues[row];

        String xString = null;
        xString = domainConfigData.getStringForValue(xValue);
        Double y = yValues[row];
        if (!allowValuesLessThanZero && y < 0) {
          throw new ChartPlottimeException("illegal_zero_value", valueSource.toString());
        }

        if (xString != null) {
          dataset.addValue(y, seriesName, xString);
        }
      }
      if (fillWithZero) {
        for (int row = 0; row < rowCount; ++row) {
          double xValue = xValues[row];

          String xString = null;
          xString = domainConfigData.getStringForValue(xValue);
          Number value = dataset.getValue(seriesName, xString);
          if (value == null || Double.isNaN(value.doubleValue())) {
            dataset.addValue(0, seriesName, xString);
          }
        }
      }
    }

    return dataset;
  }
  /**
   * Creates a dataset which supports custom intervals on both axes.
   *
   * <p>Expects a grouping on the domain axis.
   *
   * @throws ChartPlottimeException
   */
  public static DefaultIntervalXYDataset createDefaultIntervalXYDataset(
      ValueSource valueSource, PlotInstance plotInstance, boolean createRangeIntervals)
      throws ChartPlottimeException {
    ValueSourceData valueSourceData = plotInstance.getPlotData().getValueSourceData(valueSource);
    assertMaxValueCountNotExceededOrThrowException(valueSourceData);

    GroupCellSeriesData dataForAllGroupCells = valueSourceData.getSeriesDataForAllGroupCells();

    DefaultIntervalXYDataset intervalDataset = new DefaultIntervalXYDataset();

    DefaultDimensionConfig domainConfig = valueSource.getDomainConfig();
    DimensionConfigData domainConfigData =
        plotInstance.getPlotData().getDimensionConfigData(domainConfig);

    // Loop all group cells and add data to dataset
    for (GroupCellKeyAndData groupCellKeyAndData : dataForAllGroupCells) {

      GroupCellKey groupCellKey = groupCellKeyAndData.getKey();
      GroupCellData groupCellData = groupCellKeyAndData.getData();

      // create series name
      GroupCellKey groupCellKeyClone = (GroupCellKey) groupCellKey.clone();
      groupCellKeyClone.removeRangeForDimension(
          PlotDimension.DOMAIN); // legend does not need X-group
      String seriesName =
          generateSeriesName(
              valueSource, groupCellKeyClone, plotInstance.getCurrentPlotConfigurationClone());

      List<ValueRange> domainValueGroups = domainConfigData.getGroupingModel();

      // Loop all rows and add data to series.
      // Remember that by definition one row in the groupCellData corresponds
      // to one group in xValueGroups (if the x-axis is grouped, which should
      // always be the case in this function).
      final int domainValueIdx = 0;
      final int domainLowerIdx = 1;
      final int domainUpperIdx = 2;
      final int rangeValueIdx = 3;
      final int rangeLowerIdx = 4;
      final int rangeUpperIdx = 5;
      Map<PlotDimension, double[]> dataForMainSeries =
          groupCellData.getDataForUsageType(SeriesUsageType.MAIN_SERIES);

      int rowCount = dataForMainSeries.get(PlotDimension.DOMAIN).length;
      double[] domainValues = dataForMainSeries.get(PlotDimension.DOMAIN);
      double[] rangeValues = dataForMainSeries.get(PlotDimension.VALUE);
      double[] upperErrorValues = null;
      double[] lowerErrorValues = null;

      upperErrorValues = valueSourceData.getAbsoluteUtilityValues(groupCellKeyAndData, true);
      lowerErrorValues = valueSourceData.getAbsoluteUtilityValues(groupCellKeyAndData, false);
      if (createRangeIntervals && upperErrorValues == null) {
        throw new ChartPlottimeException(
            "undefined_series", valueSource.toString(), SeriesUsageType.INDICATOR_1);
      }

      double[][] series = new double[6][rowCount];
      Iterator<ValueRange> domainGroupIterator = null;
      if (domainValueGroups != null) {
        domainGroupIterator = domainValueGroups.iterator();
      }

      double domainLower;
      double domainUpper;
      double domainValue;
      double rangeValue;
      double rangeUpper;
      double rangeLower;
      for (int row = 0; row < rowCount; ++row) {
        domainValue = domainValues[row];
        domainLower = domainValue;
        domainUpper = domainValue;
        if (domainGroupIterator != null) {
          ValueRange currentDomainGroup = domainGroupIterator.next();
          if (currentDomainGroup.definesUpperLowerBound()) {
            domainLower = currentDomainGroup.getLowerBound();
            domainUpper = currentDomainGroup.getUpperBound();
          }
        }
        rangeValue = rangeValues[row];
        rangeUpper = upperErrorValues != null ? upperErrorValues[row] : Double.NaN;
        rangeLower = lowerErrorValues != null ? lowerErrorValues[row] : Double.NaN;

        series[domainValueIdx][row] = domainValue;
        series[domainLowerIdx][row] = domainLower;
        series[domainUpperIdx][row] = domainUpper;
        series[rangeValueIdx][row] = rangeValue;
        series[rangeLowerIdx][row] = rangeLower;
        series[rangeUpperIdx][row] = rangeUpper;
      }

      intervalDataset.addSeries(seriesName, series);
    }
    return intervalDataset;
  }
  private CustomLegendItem createValueSourceLegendItem(
      PlotConfiguration plotConfig, ValueSource valueSource) {

    Set<PlotDimension> dimensions = new HashSet<PlotDimension>();
    for (PlotDimension dimension : PlotDimension.values()) {
      switch (dimension) {
        case DOMAIN:
        case VALUE:
          break;
        default:
          if (valueSource.useSeriesFormatForDimension(plotConfig, dimension)) {
            dimensions.add(dimension);
          }
      }
    }
    if (dimensions.isEmpty()) {
      return null;
    }

    SeriesFormat format = valueSource.getSeriesFormat();
    String description = "";
    String toolTipText = "";
    String urlText = "";
    boolean shapeVisible = true;
    Shape shape;
    boolean shapeFilled = true;
    Paint fillPaint = UNDEFINED_COLOR_PAINT;
    boolean shapeOutlineVisible = true;
    Paint outlinePaint = PlotConfiguration.DEFAULT_OUTLINE_COLOR;
    Stroke outlineStroke = DEFAULT_OUTLINE_STROKE;
    boolean lineVisible =
        format.getLineStyle() != LineStyle.NONE
            && format.getSeriesType() == SeriesFormat.VisualizationType.LINES_AND_SHAPES;

    // configure fill paint and line paint
    Paint linePaint;
    String label = valueSource.toString();
    if (label == null) {
      label = "";
    }
    if (dimensions.contains(PlotDimension.COLOR)) {
      Color color = format.getItemColor();
      fillPaint = format.getAreaFillPaint(color);
      linePaint = fillPaint;
    } else {
      if (format.getAreaFillStyle() == FillStyle.NONE) {
        fillPaint = new Color(0, 0, 0, 0);
        linePaint = fillPaint;
      } else if (format.getAreaFillStyle() == FillStyle.SOLID) {
        fillPaint = UNDEFINED_COLOR_PAINT;
        linePaint = UNDEFINED_LINE_COLOR;
      } else {
        fillPaint = format.getAreaFillPaint(UNDEFINED_COLOR);
        linePaint = fillPaint;
      }
    }

    VisualizationType seriesType = valueSource.getSeriesFormat().getSeriesType();
    if (seriesType == VisualizationType.LINES_AND_SHAPES) {
      if (dimensions.contains(PlotDimension.SHAPE)) {
        shape = format.getItemShape().getShape();
      } else if (dimensions.contains(PlotDimension.COLOR)) {
        shape = UNDEFINED_SHAPE;
      } else {
        shape = UNDEFINED_SHAPE_AND_COLOR;
      }

      if (dimensions.contains(PlotDimension.SIZE)) {
        AffineTransform transformation = new AffineTransform();
        double scalingFactor = format.getItemSize();
        transformation.scale(scalingFactor, scalingFactor);
        shape = transformation.createTransformedShape(shape);
      }
    } else if (seriesType == VisualizationType.BARS) {
      shape = BAR_SHAPE;
    } else if (seriesType == VisualizationType.AREA) {
      shape = AREA_SHAPE;
    } else {
      throw new RuntimeException("Unknown SeriesType. This should not happen.");
    }

    // configure line shape
    float lineLength = 0;
    if (lineVisible) {
      lineLength = format.getLineWidth();
      if (lineLength < 1) {
        lineLength = 1;
      }
      if (lineLength > 1) {
        lineLength = 1 + (float) Math.log(lineLength) / 2;
      }

      // line at least 30 pixels long, and show at least 2 iterations of stroke
      lineLength = Math.max(lineLength * 30, format.getStrokeLength() * 2);

      // line at least 2x longer than shape width
      if (shape != null) {
        lineLength = Math.max(lineLength, (float) shape.getBounds().getWidth() * 2f);
      }
    }

    // now create line shape and stroke
    Shape line = new Line2D.Float(0, 0, lineLength, 0);
    BasicStroke lineStroke = format.getStroke();
    if (lineStroke == null) {
      lineStroke = new BasicStroke();
    }

    // unset line ending decoration to prevent drawing errors in legend
    {
      BasicStroke s = lineStroke;
      lineStroke =
          new BasicStroke(
              s.getLineWidth(),
              BasicStroke.CAP_BUTT,
              BasicStroke.JOIN_ROUND,
              s.getMiterLimit(),
              s.getDashArray(),
              s.getDashPhase());
    }

    return new CustomLegendItem(
        label,
        description,
        toolTipText,
        urlText,
        shapeVisible,
        shape,
        shapeFilled,
        fillPaint,
        shapeOutlineVisible,
        outlinePaint,
        outlineStroke,
        lineVisible,
        line,
        lineStroke,
        linePaint);
  }