
com.xceptance.xlt.report.util.JFreeChartUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of xlt Show documentation
Show all versions of xlt Show documentation
XLT (Xceptance LoadTest) is an extensive load and performance test tool developed and maintained by Xceptance.
/*
* Copyright (c) 2005-2024 Xceptance Software Technologies GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.xceptance.xlt.report.util;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Rectangle2D.Double;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.LegendItem;
import org.jfree.chart.LegendItemCollection;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.LogarithmicAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.Marker;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.ValueMarker;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.StandardXYBarPainter;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.Range;
import org.jfree.data.time.MovingAverage;
import org.jfree.data.time.Second;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.time.TimeSeriesDataItem;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYIntervalDataItem;
import org.jfree.data.xy.XYIntervalSeries;
import org.jfree.data.xy.XYIntervalSeriesCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.luciad.imageio.webp.WebPWriteParam;
import com.xceptance.common.io.FileUtils;
import com.xceptance.xlt.common.XltConstants;
import com.xceptance.xlt.report.ReportGeneratorConfiguration.ChartCappingInfo;
import com.xceptance.xlt.report.ReportGeneratorConfiguration.ChartCappingInfo.ChartCappingMode;
import com.xceptance.xlt.report.ReportGeneratorConfiguration.ChartScale;
/**
* The JFreeChartUtils class simplifies generating charts via the JFreeChart library.
*/
public final class JFreeChartUtils
{
/**
* chart color sets
*/
public static class ColorSet
{
/**
* blue, ...
*/
public static final ColorSet AVERAGES = new ColorSet(COLOR_MOVING_AVERAGE, COLOR_MEDIAN, COLOR_MEAN);
/**
* blue, gray, magenta, green, red
*/
public static final ColorSet A = new ColorSet(Color.BLUE, MoreColors.GRAY.getColor(), Color.MAGENTA, MoreColors.GREEN.getColor(),
Color.RED);
/**
* black, light green, brown, steel blue, light gray
*/
public static final ColorSet B = new ColorSet(Color.BLACK, MoreColors.LIGHT_GREEN.getColor(), MoreColors.BROWN.getColor(),
MoreColors.STEEL_BLUE.getColor(), MoreColors.LIGHT_GRAY.getColor());
/**
* orange, cyan, pink, lilac, yellow
*/
public static final ColorSet C = new ColorSet(MoreColors.ORANGE.getColor(), Color.CYAN, Color.PINK, MoreColors.LILAC.getColor(),
Color.YELLOW);
private final List colors = new ArrayList(5);
public ColorSet(final Color... colors)
{
final int count = colors.length;
for (int i = 0; i < count; i++)
{
this.colors.add(colors[i]);
}
}
/**
* get the color from specified index
*
* @param index
* index of color in color set
* @return the color from specified index
* @throws IndexOutOfBoundsException
* if the index is out of range (index < 0 || index >= size())
*/
public Color get(final int index) throws IndexOutOfBoundsException
{
return colors.get(index);
}
/**
* get all colors from this set
*
* @return
*/
public List getColors()
{
return Collections.unmodifiableList(colors);
}
/**
* how many colors are in this set
*
* @return size of color set
*/
public int size()
{
return colors.size();
}
}
/**
* self defined colors
*/
public enum MoreColors
{
BROWN(0xB97A57),
GRAY(0xAAAAAA),
GREEN(0x00AA00),
LIGHT_GRAY(0x757575),
LIGHT_GREEN(0xB5E61D),
LILAC(0xC8BFE7),
ORANGE(0xFF9900),
STEEL_BLUE(0x7092BE);
private final Color color;
private MoreColors(final int color)
{
this.color = new Color(color);
}
public Color getColor()
{
return color;
}
}
/**
* The capping color in the charts (dark red).
*/
public static final Color COLOR_CAP = new Color(0xAA0000);
/**
* The color of error bars in the charts (red).
*/
public static final Color COLOR_ERROR = Color.RED;
/**
* The color of event bars in the charts (orange).
*/
public static final Color COLOR_EVENT = new Color(0xFFA500);
/**
* The color of histogram bars in the charts (gray-green).
*/
public static final Color COLOR_HISTOGRAM = new Color(0x82C17D);
/**
* The color of a standard value line in the charts (gray).
*/
public static final Color COLOR_LINE = new Color(0xAAAAAA);
/**
* The color of a dimmed value line in the charts (light gray).
*/
public static final Color COLOR_LINE_DIMMED = new Color(0xDDDDDD);
/**
* The color of a mean line in the charts (dark magenta).
*/
public static final Color COLOR_MEAN = new Color(0xCD3333);
/**
* The color of a median line in the charts (dark turquoise).
*/
public static final Color COLOR_MEDIAN = new Color(0x62C0E0);
/**
* The color of a moving average line in the charts (dark blue).
*/
public static final Color COLOR_MOVING_AVERAGE = new Color(0x1C1CBF);
/**
* The default chart theme.
*/
private static final XltChartTheme DEFAULT_CHART_THEME = new XltChartTheme();
/**
* The default title of time axes.
*/
private static final String DEFAULT_DATE_AXIS_TITLE = "Time";
/**
* The default title of value axes.
*/
private static final String DEFAULT_VALUE_AXIS_TITLE = "Values";
/**
* The shape of a line in the chart's legend.
*/
private static final Double LEGEND_LINE_SHAPE = new Rectangle2D.Double(-7.0, 0.0, 14.0, 1.0);
/**
* The logger.
*/
private static final Logger log = LoggerFactory.getLogger(JFreeChartUtils.class);
/**
* Maps a long value to its corresponding {@link Second} object.
*/
private static final Map secondsCache = new HashMap();
/**
* The watermark color.
*/
private static final Color WATERMARK_COLOR = new Color(0xABABAB);
/**
* The watermark text.
*/
private static final String WATERMARK_TEXT = "Xceptance LoadTest";
/**
* The compression factor, to use when creating WebP images, where 0 is fastest compression and 1 is highest
* compression (default: 0.0).
*/
private static float webpCompressionFactor = 0.0f;
/**
* The replacement value for negative/0 values when making a series fit for logarithmic axes.
*/
private static final double MIN_VALUE_FOR_LOGARITHMIC_AXES = 0.00000001;
/**
* Creates a new line plot and adds it to the given chart.
*
* @param chart
* the chart to modify
* @param rangeAxisTitle
* the title shown at the plot's range axis
* @param dataset
* the data to show
* @see #createCombinedPlotChart(String, long, long)
*/
public static void addLinePlotToCombinedPlotChart(final JFreeChart chart, final String rangeAxisTitle, final XYDataset dataset)
{
final CombinedDomainXYPlot combinedPlot = ((CombinedDomainXYPlot) chart.getPlot());
// choose a color set
final int subPlotCount = combinedPlot.getSubplots().size();
final int decider = subPlotCount % 3;
final ColorSet colorSet = decider == 0 ? ColorSet.A : decider == 1 ? ColorSet.B : ColorSet.C;
// create and add plot
final XYPlot plot = createLinePlot(dataset, null, rangeAxisTitle, colorSet);
combinedPlot.add(plot, 1);
}
/**
* Caps a plot at the given value on the range axis and draws a cap marker. Optionally, a cap item can be added to
* the legend.
*
* @param plot
* the plot to cap
* @param cappingValue
* the value to cap the plot at (must be greater than 0 to take effect)
* @param addLegendItem
* whether to add a legend item for the cap
*/
public static void capPlot(final XYPlot plot, final int cappingValue, final boolean addLegendItem)
{
// cap the plot if necessary
if (cappingValue > 0)
{
// cap the plot
plot.getRangeAxis().setUpperBound(cappingValue);
// draw a cap marker
final Marker capMarker = new ValueMarker(cappingValue);
capMarker.setAlpha(1.0f);
capMarker.setPaint(COLOR_CAP);
capMarker.setStroke(new BasicStroke(4.0f));
plot.addRangeMarker(capMarker);
// add a cap legend item
if (addLegendItem)
{
final LegendItemCollection legendItems = plot.getLegendItems();
final LegendItem capLegendItem = new LegendItem("Cap", COLOR_CAP);
capLegendItem.setShape(new Rectangle2D.Double(-7.0, 0.0, 14.0, 2.0));
legendItems.add(capLegendItem);
plot.setFixedLegendItems(legendItems);
}
}
}
/**
* Creates an average chart with the moving average, the median, and the mean, but not the actual values.
*
* @param seriesName
* the name of the series
* @param chartTitle
* the title of the chart
* @param yAxisTitle
* the title of the range axis
* @param valueSeries
* the value series
* @param averageValueSeries
* the average value series
* @param median
* the median of the values in the value series
* @param mean
* the mean of the values in the value series
* @param startTime
* chart start time
* @param endTime
* chart end time
*/
public static JFreeChart createAverageLineChart(final String seriesName, final String chartTitle, final String yAxisTitle,
final TimeSeries valueSeries, final TimeSeries averageValueSeries, final double median,
final double mean, final long startTime, final long endTime)
{
final TimeSeries medianSeries = new TimeSeries(seriesName + " (Median)");
final TimeSeries meanSeries = new TimeSeries(seriesName + " (Mean)");
final TimeSeriesCollection seriesCollection = new TimeSeriesCollection();
seriesCollection.addSeries(averageValueSeries);
seriesCollection.addSeries(medianSeries);
seriesCollection.addSeries(meanSeries);
// only add graphs for median / mean if there are some values
final int count = valueSeries.getItemCount();
if (count > 1)
{
final TimeSeriesDataItem firstItem = valueSeries.getDataItem(0);
final TimeSeriesDataItem lastItem = valueSeries.getDataItem(count - 1);
medianSeries.add(firstItem.getPeriod(), median);
medianSeries.add(lastItem.getPeriod(), median);
meanSeries.add(firstItem.getPeriod(), mean);
meanSeries.add(lastItem.getPeriod(), mean);
}
// create and customize the chart
final JFreeChart chart = createLineChart(chartTitle, yAxisTitle, seriesCollection, startTime, endTime, ColorSet.AVERAGES);
final NumberAxis axis = (NumberAxis) chart.getXYPlot().getRangeAxis();
axis.setAutoRangeIncludesZero(false);
axis.setStandardTickUnits(NumberAxis.createStandardTickUnits());
return chart;
}
/**
* Creates a new bar chart.
*
* @param chartTitle
* the chart title
* @param dataset
* the data to show
* @param rangeAxisTitle
* the title of the range
* @param barColor
* the bar color
* @param startTime
* chart start time
* @param endTime
* chart end time
* @return the chart
*/
public static JFreeChart createBarChart(final String chartTitle, final XYDataset dataset, final String rangeAxisTitle,
final Color barColor, final long startTime, final long endTime)
{
final DateAxis timeAxis = createTimeAxis(startTime, endTime);
final XYPlot barPlot = createBarPlot(dataset, timeAxis, rangeAxisTitle, barColor);
return createChart(chartTitle, barPlot);
}
/**
* Creates a new bar chart.
*
* @param chartTitle
* the chart title
* @param dataset
* the data to show
* @param rangeAxisTitle
* the title of the range
* @param barColor
* the bar color
* @param startTime
* chart start time
* @param endTime
* chart end time
* @param createLegend
* enable or disable the creation of a chart legend
* @return the chart
*/
public static JFreeChart createBarChart(final String chartTitle, final XYDataset dataset, final String rangeAxisTitle,
final Color barColor, final long startTime, final long endTime, boolean createLegend,
boolean createTimeAxis)
{
DateAxis timeAxis = new DateAxis();
if (createTimeAxis)
{
timeAxis = createTimeAxis(startTime, endTime);
}
updateDateAxisMinMaxTime(timeAxis, startTime, endTime);
NumberAxis rangeAxis = new NumberAxis();
if (rangeAxisTitle != null)
{
rangeAxis = new NumberAxis(rangeAxisTitle);
}
rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
final XYPlot barPlot = createBarPlot(dataset, timeAxis, rangeAxis, barColor);
return new JFreeChart(chartTitle, JFreeChart.DEFAULT_TITLE_FONT, barPlot, createLegend);
}
/**
* Creates a new bar plot with standard settings applied (number axis set).
*
* @param dataset
* the data to show
* @param domainAxis
* the domain axis (may be null
if no axis is to be shown)
* @param rangeAxis
* the range axis (may be null
if no axis is to be shown)
* @param barColor
* the bar color
* @return the bar plot
*/
public static XYPlot createBarPlot(final XYDataset dataset, final ValueAxis domainAxis, ValueAxis rangeAxis, final Color barColor)
{
return new XYPlot(dataset, domainAxis, rangeAxis, createBarRenderer(barColor));
}
/**
* Creates a new bar plot with standard settings applied (number axis set).
*
* @param dataset
* the data to show
* @param domainAxis
* the domain axis (may be null
if no axis is to be shown)
* @param rangeAxisTitle
* the title of the range axis
* @param barColor
* the bar color
* @return the bar plot
*/
public static XYPlot createBarPlot(final XYDataset dataset, final ValueAxis domainAxis, final String rangeAxisTitle,
final Color barColor)
{
return new XYPlot(dataset, domainAxis, createNumberAxis(rangeAxisTitle), createBarRenderer(barColor));
}
/**
* Creates a new bar renderer with standard settings applied (no shadow).
*
* @param barColor
* the bar color
* @return the bar renderer
*/
public static XYBarRenderer createBarRenderer(final Color barColor)
{
final XYBarRenderer barRenderer = new XYBarRenderer();
barRenderer.setBarPainter(new StandardXYBarPainter());
barRenderer.setSeriesPaint(0, barColor);
barRenderer.setShadowVisible(false);
return barRenderer;
}
/**
* Creates a basic line chart with only a time axis.
*
* @param chartTitle
* the chart title
* @param timeAxisTitle
* the x-axis title
* @param startTime
* chart start time
* @param endTime
* chart end time
* @return the chart
* @see #setAxisTimeSeriesCollection(JFreeChart, int, String, List)
*/
public static JFreeChart createBasicLineChart(final String chartTitle, final String timeAxisTitle, final long startTime,
final long endTime)
{
final DateAxis timeAxis = createTimeAxis(timeAxisTitle, startTime, endTime);
final XYLineAndShapeRenderer lineRenderer = createLineRenderer(ColorSet.A);
final XYPlot plot = new XYPlot(null, timeAxis, null, lineRenderer);
return createChart(chartTitle, plot);
}
/**
* Creates chart with the given title set and the passed plot embedded.
*
* @param chartTitle
* the chart title
* @param plot
* the plot
* @return the chart
*/
public static JFreeChart createChart(final String chartTitle, final Plot plot)
{
final JFreeChart jfreechart = new JFreeChart(chartTitle, plot);
return jfreechart;
}
/**
* Creates a combined plot with standard settings applied (time axis).
*
* @param startTime
* chart start time
* @param endTime
* chart end time
* @return the combined plot
*/
public static CombinedDomainXYPlot createCombinedPlot(final long startTime, final long endTime)
{
final CombinedDomainXYPlot combinedPlot = new CombinedDomainXYPlot(createTimeAxis(startTime, endTime));
combinedPlot.setGap(16.0);
return combinedPlot;
}
/**
* Creates an empty combined plot chart. Plots can later be added to this chart and will be shown one below the
* other.
*
* @param chartTitle
* the chart title
* @param startTime
* chart start time
* @param endTime
* chart end time
* @return the chart
* @see {@link JFreeChartUtils#addLinePlotToCombinedPlotChart(JFreeChart, String, TimeSeriesCollection)}
*/
public static JFreeChart createCombinedPlotChart(final String chartTitle, final long startTime, final long endTime)
{
return createChart(chartTitle, createCombinedPlot(startTime, endTime));
}
/**
* Creates a histogram plot for the given histogram series. The plot won't have any visible axes and is meant to be
* combined with other plots.
*
* @param histogramSeries
* the histogram series
* @param range
* the range to show on the range axis
* @param chartScale
* the chart scale to be used
* @param plotCappingValue
* the value at which to cap the plot
* @return the plot
*/
public static XYPlot createHistogramPlot(final XYIntervalSeries histogramSeries, final Range range, final ChartScale chartScale,
final int plotCappingValue)
{
// adjust the series if necessary
if (chartScale == ChartScale.LOGARITHMIC)
{
adjustSeriesForLogarithmicAxes(histogramSeries);
}
// data set
final XYIntervalSeriesCollection dataset = new XYIntervalSeriesCollection();
dataset.addSeries(histogramSeries);
// domain axis
final ValueAxis domainAxis = new NumberAxis();
domainAxis.setVisible(false);
// range axis
final NumberAxis rangeAxis = (chartScale == ChartScale.LOGARITHMIC) ? new LogarithmicAxis(null) : new NumberAxis();
rangeAxis.setVisible(false);
rangeAxis.setRange(range);
// bar renderer
final XYBarRenderer barRenderer = createBarRenderer(COLOR_HISTOGRAM);
barRenderer.setUseYInterval(true);
// plot
final XYPlot histogramPlot = new XYPlot(dataset, domainAxis, rangeAxis, barRenderer);
histogramPlot.setDomainGridlinesVisible(false);
capPlot(histogramPlot, plotCappingValue, true);
return histogramPlot;
}
/**
* Creates a chart from the passed time series and gives it the specified title.
*
* @param chartTitle
* the chart title
* @param rangeAxisTitle
* the name of the y-axis
* @param series
* the time series to show
* @param startTime
* the start time of the x-axis
* @param endTime
* the end time of the x-axis
* @param includeMovingAverage
* whether or not an additional moving average time series should be included
* @param percentage
* the percentaged amount of values for building the moving average
* @return the chart
*/
public static JFreeChart createLineChart(final String chartTitle, final String rangeAxisTitle, final TimeSeries series,
final long startTime, final long endTime, final boolean includeMovingAverage,
final int percentage)
{
return createLineChart(chartTitle, rangeAxisTitle, series, startTime, endTime, includeMovingAverage, percentage, true);
}
/**
* Creates a chart from the passed time series and gives it the specified title.
*
* @param chartTitle
* the chart title
* @param rangeAxisTitle
* the name of the y-axis
* @param series
* the time series to show
* @param startTime
* the start time of the x-axis
* @param endTime
* the end time of the x-axis
* @param includeMovingAverage
* whether or not an additional moving average time series should be included
* @param percentage
* the percentaged amount of values for building the moving average
* @param showDots
* whether to additionally visualize the values as dots
* @return the chart
*/
public static JFreeChart createLineChart(final String chartTitle, final String rangeAxisTitle, final TimeSeries series,
final long startTime, final long endTime, final boolean includeMovingAverage,
final int percentage, final boolean showDots)
{
final TimeSeries movingAverageSeries = includeMovingAverage ? createMovingAverageTimeSeries(series, percentage) : null;
return createLineChart(chartTitle, rangeAxisTitle, series, movingAverageSeries, startTime, endTime, showDots, ChartScale.LINEAR,
-1);
}
/**
* Creates a chart from the passed time series collection and gives it the specified title. Any time series in the
* collection is added to the chart. The chart legend will show the series names.
*
* @param chartTitle
* the chart title
* @param rangeAxisTitle
* the name of the y-axis
* @param series
* the time series to show
* @param movingAverageTimeSeries
* the moving average time series
* @param startTime
* the start time of the x-axis
* @param endTime
* the end time of the x-axis
* @param showDots
* whether to additionally visualize the values as dots
* @param chartScale
* the chart scale to use
* @param cappingValue
* the value at which to cap the chart
* @return the chart
*/
public static JFreeChart createLineChart(final String chartTitle, final String rangeAxisTitle, final TimeSeries timeSeries,
final TimeSeries movingAverageTimeSeries, final long startTime, final long endTime,
final boolean showDots, final ChartScale chartScale, final int cappingValue)
{
final DateAxis timeAxis = createTimeAxis(startTime, endTime);
final XYPlot linePlot = createLinePlot(timeSeries, movingAverageTimeSeries, timeAxis, rangeAxisTitle, showDots, chartScale,
cappingValue);
return createChart(chartTitle, linePlot);
}
/**
* Creates a line plot from the passed time series and average time series.
*
* @param series
* the series
* @param movingAverageSeries
* the moving average series
* @param domainAxis
* the domain axis (may be null
)
* @param rangeAxisTitle
* the name of the y-axis
* @param showDots
* whether to additionally visualize the values as dots
* @param chartScale
* the chart scale to use
* @param cappingValue
* the value at which to cap the chart
* @return the line plot
*/
public static XYPlot createLinePlot(final TimeSeries series, final TimeSeries movingAverageSeries, final ValueAxis domainAxis,
final String rangeAxisTitle, final boolean showDots, final ChartScale chartScale,
final int cappingValue)
{
// adjust time series if necessary
if (chartScale == ChartScale.LOGARITHMIC)
{
adjustSeriesForLogarithmicAxes(series);
adjustSeriesForLogarithmicAxes(movingAverageSeries);
}
// response time axis
final NumberAxis rangeAxis = chartScale == ChartScale.LOGARITHMIC ? new LogarithmicAxis(rangeAxisTitle)
: new NumberAxis(rangeAxisTitle);
rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
// response time plot
final XYPlot plot = new XYPlot(null, domainAxis, rangeAxis, null);
// moving average line renderer
if (movingAverageSeries != null)
{
final XYLineAndShapeRenderer movingAverageLineRenderer = createLineRenderer(COLOR_MOVING_AVERAGE);
plot.setRenderer(0, movingAverageLineRenderer);
plot.setDataset(0, new TimeSeriesCollection(movingAverageSeries));
}
// dots renderer
if (showDots)
{
final DotsRenderer dotsRenderer = new DotsRenderer();
dotsRenderer.setSeriesPaint(0, COLOR_LINE);
dotsRenderer.setDotWidth(2);
dotsRenderer.setDotHeight(2);
plot.setRenderer(1, dotsRenderer);
plot.setDataset(1, new MinMaxTimeSeriesCollection(series));
}
// line renderer
final Color lineColor = showDots ? COLOR_LINE_DIMMED : COLOR_LINE;
final XYLineAndShapeRenderer lineRenderer = createLineRenderer(lineColor);
lineRenderer.setSeriesVisibleInLegend(0, !showDots);
plot.setRenderer(2, lineRenderer);
plot.setDataset(2, new MinMaxTimeSeriesCollection(series));
// cap the plot if necessary
capPlot(plot, cappingValue, false);
return plot;
}
/**
* Creates a chart from the passed time series collection and gives it the specified title. Any time series in the
* collection is added to the chart. The chart legend will show the series names.
*
* @param chartTitle
* the chart title
* @param rangeAxisTitle
* the name of the y-axis
* @param collection
* the time series collection to show
* @param startTime
* minimum time of the chart
* @param endTime
* maximum time of the chart
* @return the chart
*/
public static JFreeChart createLineChart(final String chartTitle, final String rangeAxisTitle, final TimeSeriesCollection collection,
final long startTime, final long endTime)
{
return createLineChart(chartTitle, rangeAxisTitle, collection, startTime, endTime, ColorSet.A);
}
/**
* Creates a chart from the passed time series collection and gives it the specified title. Any time series in the
* collection is added to the chart. The chart legend will show the series names.
*
* @param chartTitle
* the chart title
* @param rangeAxisTitle
* the name of the y-axis
* @param dataset
* the time series collection to show
* @param startTime
* minimum time of the chart
* @param endTime
* maximum time of the chart
* @param colorSet
* the colors to use
* @return the chart
*/
public static JFreeChart createLineChart(final String chartTitle, final String rangeAxisTitle, final TimeSeriesCollection dataset,
final long startTime, final long endTime, final ColorSet colorSet)
{
final DateAxis timeAxis = createTimeAxis(startTime, endTime);
final XYPlot linePlot = createLinePlot(dataset, timeAxis, rangeAxisTitle, colorSet);
return createChart(chartTitle, linePlot);
}
/**
* Creates a line plot from the passed time series collection. Any time series in the collection is added to the
* plot.
*
* @param dataset
* the time series collection to show
* @param domainAxis
* the domain axis (may be null
)
* @param rangeAxisTitle
* the name of the y-axis
* @param colorSet
* the colors to use
* @return the chart
*/
public static XYPlot createLinePlot(final XYDataset dataset, final ValueAxis domainAxis, final String rangeAxisTitle,
final ColorSet colorSet)
{
return new XYPlot(dataset, domainAxis, createNumberAxis(rangeAxisTitle), createLineRenderer(colorSet));
}
/**
* Creates a new line renderer with standard settings applied. The line renderer is capable of rendering min/max
* values.
*
* @param colorSet
* the line colors (may be null
)
* @return the line renderer
*/
public static XYLineAndShapeRenderer createLineRenderer(final ColorSet colorSet)
{
// line renderer
final XYLineAndShapeRenderer renderer = new MinMaxRenderer(true, false);
renderer.setLegendLine(LEGEND_LINE_SHAPE);
// set the colors
if (colorSet != null)
{
int index = 0;
for (final Color color : colorSet.getColors())
{
renderer.setSeriesPaint(index++, color);
}
}
return renderer;
}
/**
* Creates a new line renderer with standard settings applied. The line renderer is capable of rendering min/max
* values.
*
* @param color
* the line color
* @return the line renderer
*/
public static XYLineAndShapeRenderer createLineRenderer(final Color color)
{
return createLineRenderer(new ColorSet(color));
}
/**
* Creates a "moving average" time series from the given time series.
*
* @param series
* the source series
* @param percentage
* the percentaged amount of values for building the moving average
* @return the time series
*/
public static TimeSeries createMovingAverageTimeSeries(final TimeSeries series, final int percentage)
{
// take the last X percent of the values
final int samples = Math.max(2, series.getItemCount() * percentage / 100);
// derive the name from the source series
final String avgSeriesName = series.getKey() + " (Moving Average)";
// return MovingAverage.createMovingAverage(series, avgSeriesName, samples, samples);
return MovingAverage.createPointMovingAverage(series, avgSeriesName, samples);
}
/**
* Creates a number axis with standard settings applied.
*
* @param axisTitle
* the axis title (in case of null
, a default title will be set)
* @return the axis
*/
public static NumberAxis createNumberAxis(String axisTitle)
{
if (StringUtils.isBlank(axisTitle))
{
axisTitle = DEFAULT_VALUE_AXIS_TITLE;
}
final NumberAxis numberAxis = new NumberAxis(axisTitle);
numberAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
return numberAxis;
}
/**
* Creates a placeholder chart and stores it to the specified directory. Actually the placeholder chart is an empty
* WebP file with the same dimensions as the regular charts.
*
* @param outputDir
* the directory to which to save the chart
*/
public static void createPlaceholderChart(final File outputDir, final int width, final int height)
{
final File outputFile = new File(outputDir, XltConstants.REPORT_CHART_PLACEHOLDER_FILENAME);
// create the image with the correct dimensions
final BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
// set white background
final Graphics2D graphics = bufferedImage.createGraphics();
graphics.setBackground((Color) DEFAULT_CHART_THEME.getChartBackgroundPaint());
graphics.clearRect(0, 0, width, height);
// draw some info text
final Font font = new Font("SansSerif", Font.BOLD, 32);
graphics.setFont(font);
final FontMetrics fontMetrics = graphics.getFontMetrics();
final int stringWidth = fontMetrics.stringWidth(XltConstants.REPORT_CHART_PLACEHOLDER_MESSAGE);
final int stringHeight = fontMetrics.getAscent();
graphics.setPaint(new Color(0xcccccc));
graphics.drawString(XltConstants.REPORT_CHART_PLACEHOLDER_MESSAGE, (width - stringWidth) / 2, height / 2 + stringHeight / 4);
graphics.dispose();
// finally save the image
saveImage(bufferedImage, outputFile);
}
/**
* Creates an invisible plot which can be used to fill up space in combined plots.
*
* @return the spacer plot
*/
public static XYPlot createSpacerPlot()
{
final XYPlot spacerPlot = new XYPlot();
spacerPlot.setForegroundAlpha(0);
spacerPlot.setBackgroundAlpha(0);
spacerPlot.setOutlineVisible(false);
return spacerPlot;
}
/**
* Creates a time axis with standard settings applied. This includes limiting the visible time range and appending
* the time zone to the axis title.
*
* @param startTime
* the minimum time to show
* @param endTime
* the maximum time to show
* @return the axis
*/
public static DateAxis createTimeAxis(final long startTime, final long endTime)
{
return createTimeAxis(DEFAULT_DATE_AXIS_TITLE, startTime, endTime);
}
/**
* Creates a time axis with standard settings applied. This includes limiting the visible time range and appending
* the time zone to the axis title.
*
* @param axisTitle
* the axis title (in case of null
, a default title will be set)
* @param startTime
* the minimum time to show
* @param endTime
* the maximum time to show
* @return the axis
*/
public static DateAxis createTimeAxis(String axisTitle, final long startTime, final long endTime)
{
final Date startDate = new Date(startTime);
// determine the time axis title
final TimeZone tz = TimeZone.getDefault();
if (StringUtils.isBlank(axisTitle))
{
axisTitle = DEFAULT_DATE_AXIS_TITLE;
}
axisTitle = axisTitle + " [" + tz.getDisplayName(tz.inDaylightTime(startDate), TimeZone.SHORT, Locale.US) + "]";
// create the time axis
final DateAxis timeAxis = new DateAxis(axisTitle);
updateDateAxisMinMaxTime(timeAxis, startTime, endTime);
return timeAxis;
}
private static void updateDateAxisMinMaxTime(final DateAxis timeAxis, final long startTime, final long endTime)
{
// set the visible time range
if (startTime > 0 && endTime < Long.MAX_VALUE)
{
// add 1% of the time range as right margin, so details drawn at the very edge will become visible
final long margin = (endTime - startTime) / 100;
timeAxis.setMinimumDate(new Date(startTime));
timeAxis.setMaximumDate(new Date(endTime + margin));
}
}
/**
* Computes the value at which the y-axis of a chart should been capped.
*
* @param cappingInfo
* the capping settings
* @param averageValue
* the average value of the data set
* @param maxValue
* the maximum value of the data set
* @return the capping value, or -1 if the chart should not be capped
*/
public static int getChartCappingValue(final ChartCappingInfo cappingInfo, final double averageValue, final int maxValue)
{
int effectiveCappingValue;
// first calculate the capping value according to the capping method
switch (cappingInfo.method)
{
case ABSOLUTE:
effectiveCappingValue = (int) cappingInfo.parameter;
break;
case NFOLD_OF_AVERAGE:
effectiveCappingValue = (int) (averageValue * cappingInfo.parameter);
break;
default:
effectiveCappingValue = -1;
break;
}
// now let the capping mode modify the capping value
if (effectiveCappingValue >= maxValue && cappingInfo.mode == ChartCappingMode.SMART)
{
effectiveCappingValue = -1;
}
return effectiveCappingValue;
}
/**
* Returns the {@link Second} object that corresponds to the given millisecond value. This method creates and caches
* a new object only if it has not been requested so far and returns a cached object in any subsequent call.
* Creating object is expensive.
*
* @param time
* the time in milliseconds
* @return the corresponding {@link Second} object
*/
public static synchronized Second getSecond(long time)
{
// rub out milliseconds -> we have seconds precision only
time = time / 1000 * 1000;
// lookup/create a Second for this time value
Second second = secondsCache.get(time);
if (second == null)
{
second = new Second(new Date(time));
secondsCache.put(time, second);
}
return second;
}
/**
* Saves the given chart in the WebP format to a file with the passed name in the specified directory.
*
* @param chart
* the chart
* @param name
* the file name (excluding the .webp extension)
* @param outputDir
* the target directory
* @param chartWidth
* the chart width
* @param chartHeight
* the chart height
*/
public static void saveChart(final JFreeChart chart, final String name, final File outputDir, final int chartWidth,
final int chartHeight)
{
final File outputFile = new File(outputDir, FileUtils.convertIllegalCharsInFileName(name) + ".webp");
saveChart(chart, outputFile, chartWidth, chartHeight);
}
/**
* Saves the given chart in the WebP format to a given file.
*
* @param chart
* the chart
* @param outputFile
* the target file
* @param chartWidth
* the chart width
* @param chartHeight
* the chart height
*/
public static void saveChart(final JFreeChart chart, final File outputFile, final int chartWidth, final int chartHeight)
{
// first of all apply the XLT chart theme to the chart
DEFAULT_CHART_THEME.apply(chart);
// brand chart
final BufferedImage bufferedImage = chart.createBufferedImage(chartWidth, chartHeight);
final Graphics2D g2d = bufferedImage.createGraphics();
// prepare watermark settings
g2d.setFont(DEFAULT_CHART_THEME.getSmallFont());
g2d.setColor(WATERMARK_COLOR);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// insert watermark
final FontMetrics fontMetrics = g2d.getFontMetrics();
final int textWidth = (int) fontMetrics.getStringBounds(WATERMARK_TEXT, g2d).getWidth();
final int x = chartWidth - (1 + 8 + textWidth);
final int y = 1 + 8 + fontMetrics.getAscent();
g2d.drawString(WATERMARK_TEXT, x, y);
g2d.dispose();
// finally save the image
saveImage(bufferedImage, outputFile);
}
/**
* Saves the given buffered image in the WebP format to a given file.
*
* @param bufferedImage
* the buffered image
* @param outputFile
* the target file
*/
private static void saveImage(final BufferedImage bufferedImage, final File outputFile)
{
// Encode image as webp using default settings and save it as webp file
final ImageWriter writer = ImageIO.getImageWritersByMIMEType("image/webp").next();
// Set parameters for lossless webp files
final WebPWriteParam writeParam = new WebPWriteParam(writer.getLocale());
// Notify encoder to consider WebPWriteParams
writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
// Set lossless compression
writeParam.setCompressionType(writeParam.getCompressionTypes()[WebPWriteParam.LOSSLESS_COMPRESSION]);
// Set quality of images
writeParam.setCompressionQuality(webpCompressionFactor);
try
{
// create parent directories if they don't exist
Files.createDirectories(Paths.get(outputFile.getParent()));
// save the image
try (final FileImageOutputStream fios = new FileImageOutputStream(outputFile))
{
writer.setOutput(fios);
writer.write(null, new IIOImage(bufferedImage, null, null), writeParam);
}
}
catch (final IOException e)
{
log.error("Failed to save chart to file: " + outputFile, e);
}
// clean up
writer.dispose();
}
/**
* Adds the given time series collection and axis title to the given chart.
*
* @param chart
* the chart to modify
* @param rangeAxisTitle
* the name of the y-axis
* @param seriesCollection
* the time series collection to show
*/
public static void setAxisTimeSeriesCollection(final JFreeChart chart, final int axisIndex, final String rangeAxisTitle,
final List seriesConfigurations)
{
final XYPlot plot = (XYPlot) chart.getPlot();
final int dsCount = plot.getDatasetCount();
// initialize renderer and series collection
final XYItemRenderer renderer = createLineRenderer((ColorSet) null);
final TimeSeriesCollection seriesCollection = new TimeSeriesCollection();
// setup renderer and build series collection
for (int index = 0; index < seriesConfigurations.size(); index++)
{
final TimeSeriesConfiguration seriesConfig = seriesConfigurations.get(index);
// add series to collection
seriesCollection.addSeries(seriesConfig.getTimeSeries());
// update renderer (line style)
final TimeSeriesConfiguration.Style style = seriesConfig.getStyle();
if (style != TimeSeriesConfiguration.Style.LINE)
{
final float lineWidth = 0.5f;
final float dash[] =
{
5.0f
};
final float dot[] =
{
lineWidth
};
BasicStroke stroke;
switch (style)
{
case DASH:
{
stroke = new BasicStroke(lineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f);
break;
}
case DOT:
{
stroke = new BasicStroke(lineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 2.0f, dot, 0.0f);
break;
}
default:
{
log.warn("unknown line style '" + style + "'. Use LINE style by default.");
stroke = new BasicStroke(lineWidth);
}
}
renderer.setSeriesStroke(index, stroke);
}
// define color
final ColorSet colorSet = axisIndex < 1 ? ColorSet.A : ColorSet.B;
final Color color = seriesConfig.getColor() != null ? seriesConfig.getColor()
: colorSet.get((index + colorSet.size()) % colorSet.size());
// update renderer (color)
renderer.setSeriesPaint(index, color);
}
// set y-Axis
final NumberAxis yAxis = createNumberAxis(rangeAxisTitle != null ? rangeAxisTitle : DEFAULT_VALUE_AXIS_TITLE);
// update plot
plot.setDataset(dsCount, seriesCollection);
plot.setRangeAxis(axisIndex == 1 ? 1 : 0, yAxis);
plot.mapDatasetToRangeAxis(dsCount, axisIndex == 1 ? 1 : 0);
plot.setRenderer(dsCount, renderer);
}
/**
* Creates a new time series with the given name from the passed min-max value set. The data items in the time
* series will be {@link DoubleMinMaxTimeSeriesDataItem} objects, so the minimum/maximum/count/accumulated value
* properties of a {@link DoubleMinMaxValue} will still be available.
*
* @param minMaxValueSet
* the source min-max value set
* @param timeSeriesName
* the name of the time series
* @return the time series
*/
public static TimeSeries toMinMaxTimeSeries(final DoubleMinMaxValueSet valueSet, final String timeSeriesName)
{
final TimeSeries timeSeries = new TimeSeries(timeSeriesName);
if (valueSet.getValueCount() > 0)
{
final DoubleMinMaxValue[] values = valueSet.getValues();
long time = valueSet.getMinimumTime();
final int timeIncrement = valueSet.getScale() * 1000;
for (int i = 0; i < values.length; i++)
{
final DoubleMinMaxValue value = values[i];
if (value != null)
{
final Second second = getSecond(time);
timeSeries.add(new DoubleMinMaxTimeSeriesDataItem(second, value));
}
time += timeIncrement;
}
}
return timeSeries;
}
/**
* Creates a new time series with the given name from the passed min-max value set. The data items in the time
* series will be {@link IntMinMaxTimeSeriesDataItem} objects, so the minimum/maximum/count/accumulated value
* properties of a {@link IntMinMaxValue} will still be available.
*
* @param minMaxValueSet
* the source min-max value set
* @param timeSeriesName
* the name of the time series
* @return the time series
*/
public static TimeSeries toMinMaxTimeSeries(final IntMinMaxValueSet minMaxValueSet, final String timeSeriesName)
{
final TimeSeries timeSeries = new TimeSeries(timeSeriesName);
if (minMaxValueSet.getValueCount() > 0)
{
final IntMinMaxValue[] values = minMaxValueSet.getValues();
long time = minMaxValueSet.getMinimumTime();
final int scale = minMaxValueSet.getScale();
for (int i = 0; i < values.length; i++)
{
final IntMinMaxValue value = values[i];
if (value != null)
{
final Second second = getSecond(time);
timeSeries.add(new IntMinMaxTimeSeriesDataItem(second, value));
}
time = time + scale * 1000;
}
}
return timeSeries;
}
/**
* Creates a new time series with the given name from the passed min-max value set. The data items in the time
* series will get the maximum value from the corresponding value in the value set. This is especially useful for
* bar charts.
*
* @param minMaxValueSet
* the source min-max value set
* @param timeSeriesName
* the name of the time series
* @return the time series
*/
public static TimeSeries toStandardTimeSeries(final IntMinMaxValueSet minMaxValueSet, final String timeSeriesName)
{
final TimeSeries timeSeries = new TimeSeries(timeSeriesName);
if (minMaxValueSet.getValueCount() > 0)
{
final IntMinMaxValue[] values = minMaxValueSet.getValues();
long time = minMaxValueSet.getMinimumTime();
final int scale = minMaxValueSet.getScale();
for (int i = 0; i < values.length; i++)
{
final IntMinMaxValue value = values[i];
if (value != null)
{
final Second second = getSecond(time);
timeSeries.add(second, value.getMaximumValue());
}
time = time + scale * 1000;
}
}
return timeSeries;
}
/**
* Creates a time series with rate values calculated from the given count/total count values sets. This method
* assumes that the count value set is always a sub set of the total count value set.
*
* @param countValueSet
* the count values
* @param totalCountValueSet
* the total count values
* @param minMaxValueSetSize
* the size to use for min-max value sets
* @return the rate time series
*/
public static TimeSeries calculateRateTimeSeries(final ValueSet countValueSet, final ValueSet totalCountValueSet,
final int minMaxValueSetSize, final String seriesName)
{
final TimeSeries rateTimeSeries = new TimeSeries(seriesName);
// leave early if there are no total count values
if (totalCountValueSet.getValueCount() == 0)
{
return rateTimeSeries;
}
// make the count value set the same size as the total count set by simply adding 0 values
countValueSet.addOrUpdateValue(totalCountValueSet.getMinimumTime(), 0);
countValueSet.addOrUpdateValue(totalCountValueSet.getMaximumTime(), 0);
// get start time (values are equal for both input value sets)
long time = totalCountValueSet.getMinimumTime();
// calculate the rate time series
final int[] counts = countValueSet.getValues();
final int[] totalCounts = totalCountValueSet.getValues();
for (int i = 0; i < counts.length; i++)
{
// create rate values only when there have been finished transactions
if (totalCounts[i] > 0)
{
final double rate = 100.0 * counts[i] / totalCounts[i];
rateTimeSeries.add(getSecond(time), rate);
}
time = time + 1000;
}
return rateTimeSeries;
}
/**
* Fixes the given time series such that negative or 0 values are replaced with a very small positive number so the
* time series can be used together with logarithmic axes.
*
* @param timeSeries
* the time series
*/
public static void adjustSeriesForLogarithmicAxes(final TimeSeries timeSeries)
{
for (int i = 0; i < timeSeries.getItemCount(); i++)
{
final TimeSeriesDataItem dataItem = timeSeries.getDataItem(i);
if ((double) dataItem.getValue() <= 0)
{
dataItem.setValue(MIN_VALUE_FOR_LOGARITHMIC_AXES);
}
}
}
/**
* Sets the compression factor (0 -> fastest compression, 1 -> highest compression) to use when creating Webp
* images.
*
* @param factor
* the compression factor (0 -> fastest compression, 1 -> highest compression)
*/
public static void setWebpCompressionFactor(final float factor)
{
if (0 <= factor && factor <= 1)
{
webpCompressionFactor = factor;
}
else
{
throw new IllegalArgumentException("The Webp compression factor must be between 0...1");
}
}
/**
* Returns the compression factor to use when creating Webp images.
*
* @return the compression factor (0 -> fastest compression, 1 -> highest compression)
*/
public static float getWebpCompressionFactor()
{
return webpCompressionFactor;
}
/**
* Fixes the given interval series such that negative or 0 values are replaced with a very small positive number so
* the interval series can be used together with logarithmic axes.
*
* @param intervalSeries
* the interval series
*/
public static void adjustSeriesForLogarithmicAxes(final XYIntervalSeries intervalSeries)
{
for (int i = 0; i < intervalSeries.getItemCount(); i++)
{
final XYIntervalDataItem dataItem = (XYIntervalDataItem) intervalSeries.getDataItem(i);
final java.lang.Double x = dataItem.getX();
double yLow = dataItem.getYLowValue();
double y = dataItem.getYLowValue();
double yHigh = dataItem.getYLowValue();
// check if the data item has to be adjusted
if (yLow <= 0 || y <= 0 || yHigh <= 0)
{
// remove the old data item
intervalSeries.remove(x);
// limit the y values
yLow = (yLow <= 0) ? MIN_VALUE_FOR_LOGARITHMIC_AXES : yLow;
y = (y <= 0) ? MIN_VALUE_FOR_LOGARITHMIC_AXES : y;
yHigh = (yHigh <= 0) ? MIN_VALUE_FOR_LOGARITHMIC_AXES : yHigh;
// add a new data item
intervalSeries.add(x, dataItem.getXLowValue(), dataItem.getXHighValue(), y, yLow, yHigh);
}
}
}
/**
* Private constructor to avoid object instantiation.
*/
private JFreeChartUtils()
{
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy