Package org.perf4j.chart

Source Code of org.perf4j.chart.GoogleChartGenerator

/* Copyright (c) 2008-2009 HomeAway, Inc.
* All rights reserved.  http://www.perf4j.org
*
* 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 org.perf4j.chart;

import org.perf4j.GroupedTimingStatistics;
import org.perf4j.TimingStatistics;
import org.perf4j.helpers.StatsValueRetriever;

import java.util.*;
import java.net.URLEncoder;
import java.io.UnsupportedEncodingException;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.text.DecimalFormatSymbols;

/**
* This implementation of StatisticsChartGenerator creates a chart URL in the format expected by the Google Chart API.
*
* @see <a href="http://code.google.com/apis/chart/">Google Chart API</a>
* @author Alex Devine
*/
public class GoogleChartGenerator implements StatisticsChartGenerator {
    /**
     * The DEFAULT_BASE_URL points to Google's charting server at chart.apis.google.com.
     */
    public static final String DEFAULT_BASE_URL = "http://chart.apis.google.com/chart?";

    /**
     * The maximum supported chart size is 300,000 pixels per the Google Chart API.
     */
    public static final int MAX_POSSIBLE_CHART_SIZE = 300000;

    /**
     * The default chart width is 750 pixels.
     */
    public static final int DEFAULT_CHART_WIDTH = 750;

    /**
     * The default chart height is 400 pixels.
     */
    public static final int DEFAULT_CHART_HEIGHT = 400;

    /**
     * The default hex color codes used for the individual data series displayed on the chart.
     */
    public static final String[] DEFAULT_SERIES_COLORS = {
            "ff0000", //red
            "00ff00", //green
            "0000ff", //blue
            "00ffff", //cyan
            "ff00ff", //magenta
            "ffff00", //yellow
            "000000", //black
            "d2b48c", //tan
            "ffa500", //orange
            "a020f0" //purple
    };

    private StatsValueRetriever valueRetriever;
    private String baseUrl;
    private LinkedList<GroupedTimingStatistics> data = new LinkedList<GroupedTimingStatistics>();
    private int width = DEFAULT_CHART_WIDTH;
    private int height = DEFAULT_CHART_HEIGHT;
    private int maxDataPoints = DEFAULT_MAX_DATA_POINTS;
    private Set<String> enabledTags = null;

    // --- Constructors ---

    /**
     * Default constructor creates a chart that displays mean execution values and uses the default Google Chart URL.
     */
    public GoogleChartGenerator() {
        this(StatsValueRetriever.MEAN_VALUE_RETRIEVER, DEFAULT_BASE_URL);
    }

    /**
     * Creates a chart that uses the specified StatsValueRetriever to determine which values from the
     * TimingStatistic object to display. For example, a chart could be used to display mean values, transactions
     * per second, etc.
     *
     * @param statsValueRetriever The StatsPerTagDataValueExtractor that determines which value to display.
     */
    public GoogleChartGenerator(StatsValueRetriever statsValueRetriever) {
        this(statsValueRetriever, DEFAULT_BASE_URL);
    }

    /**
     * Creates a chart that uses the specified StatsValueRetriever to determine which values from the
     * StatsPerTag object to display, and also allows the base chart URL to be overridden from the Google default.
     *
     * @param valueRetriever Determines which value (such as mean/min/max/etc) from the TimingStatistic to display on
     *                       the chart
     * @param baseUrl        A value to override for the default base URL of "http://chart.apis.google.com/chart?"
     */
    public GoogleChartGenerator(StatsValueRetriever valueRetriever, String baseUrl) {
        this.valueRetriever = valueRetriever;
        this.baseUrl = baseUrl;
    }

    // --- Bean properties ---

    /**
     * Gets the width of the chart that will be displayed
     *
     * @return The width of the chart in pixels, defaults to 750.
     */
    public int getWidth() {
        return width;
    }

    /**
     * Sets the width of the chart in pixels. Note that the Google Charting API currently only supports a maximum
     * of 300,000 pixels for display, so width X height must be less than 300,000.
     *
     * @param width the width of the chart in pixels.
     */
    public void setWidth(int width) {
        this.width = width;
    }

    /**
     * Gets the height of the chart that will be displayed
     *
     * @return The height of the chart in pixels, defaults to 400.
     */
    public int getHeight() {
        return height;
    }

    /**
     * Sets the height of the chart in pixels. Note that the Google Charting API currently only supports a maximum
     * of 300,000 pixels for display, so width X height must be less than 300,000.
     *
     * @param height the height of the chart in pixels.
     */
    public void setHeight(int height) {
        this.height = height;
    }

    /**
     * Gets the set of tag names for which values will be displayed on the chart. Each tag is represented as a
     * separate series on the chart.
     *
     * @return The set of enabled tag names, or null if ALL tags found in the GroupedTimingStatistics data will be
     * displayed.
     */
    public Set<String> getEnabledTags() {
        return enabledTags;
    }

    /**
     * Sets the set of tag names for which values will be displayed on the chart.
     *
     * @param enabledTags The set of enabled tag names. If this method is not called, or if enabledTags is null,
     *                    then ALL tags from the GroupedTimingStatistics data will be displayed on the chart.
     */
    public void setEnabledTags(Set<String> enabledTags) {
        this.enabledTags = enabledTags;
    }

    /**
     * Gets the maximum number of data points to display on a chart. If <tt>appendData</tt> is called more than
     * this number of times, then only the last maxDataPoints data items will be shown in any generated charts.
     *
     * @return the maximum number of data points that will be displayed
     */
    public int getMaxDataPoints() {
        return maxDataPoints;
    }

    /**
     * Sets the maximum number of data points to display on a chart.
     *
     * @param maxDataPoints The maximum number of data points.
     */
    public void setMaxDataPoints(int maxDataPoints) {
        this.maxDataPoints = maxDataPoints;
    }

    // --- Data methods ---

    public List<GroupedTimingStatistics> getData() {
        return Collections.unmodifiableList(this.data);
    }

    public synchronized void appendData(GroupedTimingStatistics statistics) {
        if (this.data.size() >= this.maxDataPoints) {
            this.data.removeFirst();
        }
        this.data.add(statistics);
    }

    public synchronized String getChartUrl() {
        if (width * height > MAX_POSSIBLE_CHART_SIZE || width * height <= 0) {
            throw new IllegalArgumentException("The chart size must be between 0 and " + MAX_POSSIBLE_CHART_SIZE
                                               + " pixels. Current size is " + width + " x " + height);
        }

        StringBuilder retVal = new StringBuilder(baseUrl);

        //we use an x/y chart
        retVal.append("cht=lxy");

        //set the size and title
        retVal.append("&chtt=").append(encodeUrl(valueRetriever.getValueName()));
        retVal.append("&chs=").append(width).append("x").append(height);

        //specify the axes that will have labels
        retVal.append("&chxt=x,x,y");

        //convert the data to google chart params
        retVal.append(generateGoogleChartParams());

        return retVal.toString();
    }

    // --- helper methods ---

    /**
     * Helper method takes the list of data values and converts them to a String suitable for appending to a Google
     * Chart URL.
     *
     * @return the chart parameters that encode all of the data necessary to display the chart.
     */
    @SuppressWarnings("unchecked")
    protected String generateGoogleChartParams() {
        long minTimeValue = Long.MAX_VALUE;
        long maxTimeValue = Long.MIN_VALUE;
        double maxDataValue = Double.MIN_VALUE;
        //this map stores all the data series. The key is the tag name (each tag represents a single series) and the
        //value contains two lists of numbers - the first list contains the X values for each point (which is time in
        //milliseconds) and the second list contains the y values, which are the data values pulled from dataWindows.
        Map<String, List<Number>[]> tagsToXDataAndYData = new TreeMap<String, List<Number>[]>();

        for (GroupedTimingStatistics groupedTimingStatistics : data) {
            Map<String, TimingStatistics> statsByTag = groupedTimingStatistics.getStatisticsByTag();
            long windowStartTime = groupedTimingStatistics.getStartTime();
            long windowLength = groupedTimingStatistics.getStopTime() - windowStartTime;
            //keep track of the min/max time value, this is needed for scaling the chart parameters
            minTimeValue = Math.min(minTimeValue, windowStartTime);
            maxTimeValue = Math.max(maxTimeValue, windowStartTime);

            for (Map.Entry<String, TimingStatistics> tagWithData : statsByTag.entrySet()) {
                String tag = tagWithData.getKey();
                if (this.enabledTags == null || this.enabledTags.contains(tag)) {
                    //get the corresponding value from tagsToXDataAndYData
                    List<Number>[] xAndYData = tagsToXDataAndYData.get(tagWithData.getKey());
                    if (xAndYData == null) {
                        tagsToXDataAndYData.put(tag, xAndYData = new List[]{new ArrayList<Number>(),
                                                                            new ArrayList<Number>()});
                    }

                    //the x data is the start time of the window, the y data is the value
                    Number yValue = this.valueRetriever.getStatsValue(tagWithData.getValue(), windowLength);
                    xAndYData[0].add(windowStartTime);
                    xAndYData[1].add(yValue);

                    //update the max data value, which is needed for scaling
                    maxDataValue = Math.max(maxDataValue, yValue.doubleValue());
                }
            }
        }

        //if it's empty, there's nothing to display
        if (tagsToXDataAndYData.isEmpty()) {
            return "";
        }

        //set up the axis labels - we use the US decimal format locale to ensure the decimal separator is . and not ,
        DecimalFormat decimalFormat = new DecimalFormat("##0.0", new DecimalFormatSymbols(Locale.US));
        SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
        dateFormat.setTimeZone(GroupedTimingStatistics.getTimeZone());

        //the y-axis label goes from 0 to the maximum data value
        String axisRangeParam = "&chxr=2,0," + decimalFormat.format(maxDataValue);

        //for the x-axis (time) labels, ideally we want one label for each data window, but support a maximum of 10
        //labels so the chart doesn't get too crowded
        int stepSize = this.data.size() / 10 + 1;
        StringBuilder timeAxisLabels = new StringBuilder("&chxl=0:");
        StringBuilder timeAxisLabelPositions = new StringBuilder("&chxp=0");

        for (Iterator<GroupedTimingStatistics> iter = data.iterator(); iter.hasNext();) {
            GroupedTimingStatistics groupedTimingStatistics = iter.next();
            long windowStartTime = groupedTimingStatistics.getStartTime();
            String label = dateFormat.format(new Date(windowStartTime));
            double position = 100.0 * (windowStartTime - minTimeValue) / (maxTimeValue - minTimeValue);
            timeAxisLabels.append("|").append(label);
            timeAxisLabelPositions.append(",").append(decimalFormat.format(position));

            //skip over some windows if stepSize is greater than 1
            for (int i = 1; i < stepSize && iter.hasNext(); i++) {
                iter.next();
            }
        }

        //this next line appends a "Time" label in the middle of the bottom of the X axis
        timeAxisLabels.append("|1:|Time");
        timeAxisLabelPositions.append("|1,50");

        //display the gridlines
        double xAxisGridlineStepSize = this.data.size() > 2 ? 100.0 / (this.data.size() - 1) : 50.0;
        String gridlinesParam = "&chg=" + decimalFormat.format(xAxisGridlineStepSize) + ",10";

        //at this point we should be able to normalize the data to 0 - 100 as required by the google chart API
        StringBuilder chartDataParam = new StringBuilder("&chd=t:");
        StringBuilder chartColorsParam = new StringBuilder("&chco=");
        StringBuilder chartShapeMarkerParam = new StringBuilder("&chm=");
        StringBuilder chartLegendParam = new StringBuilder("&chdl=");

        //this loop is run once for each tag, i.e. each data series to be displayed on the chart
        int i = 0;
        for (Iterator<Map.Entry<String, List<Number>[]>> iter = tagsToXDataAndYData.entrySet().iterator();
             iter.hasNext(); i++) {
            Map.Entry<String, List<Number>[]> tagWithXAndYData = iter.next();

            //data param
            List<Number> xValues = tagWithXAndYData.getValue()[0];
            chartDataParam.append(numberValuesToGoogleDataSeriesParam(xValues, minTimeValue, maxTimeValue));
            chartDataParam.append("|");

            List<Number> yValues = tagWithXAndYData.getValue()[1];
            chartDataParam.append(numberValuesToGoogleDataSeriesParam(yValues, 0, maxDataValue));

            //color param
            String color = DEFAULT_SERIES_COLORS[i % DEFAULT_SERIES_COLORS.length];
            chartColorsParam.append(color);

            //the shape marker param puts a diamond (the d) at each data point (the -1) of size 5 pixels.
            chartShapeMarkerParam.append("d,").append(color).append(",").append(i).append(",-1,5.0");

            //legend param
            chartLegendParam.append(tagWithXAndYData.getKey());

            if (iter.hasNext()) {
                chartDataParam.append("|");
                chartColorsParam.append(",");
                chartShapeMarkerParam.append("|");
                chartLegendParam.append("|");
            }
        }

        return chartDataParam.toString()
               + chartColorsParam
               + chartShapeMarkerParam
               + chartLegendParam
               + axisRangeParam
               + timeAxisLabels
               + timeAxisLabelPositions
               + gridlinesParam;
    }

    /**
     * This helper method is used to normalize a list of data values from 0 - 100 as required by the Google Chart
     * Data API, and from this data it constructs the series data URL param.
     *
     * @param values           the values to be normalized
     * @param minPossibleValue the minimum possible value for the values
     * @param maxPossibleValue the maximmum possible value for the values
     * @return A Google Chart API data series using normal text encoding (see the Chart API docs)
     */
    protected String numberValuesToGoogleDataSeriesParam(List<Number> values,
                                                         double minPossibleValue, double maxPossibleValue) {
        StringBuilder retVal = new StringBuilder();

        double valueRange = maxPossibleValue - minPossibleValue;
        DecimalFormat formatter = new DecimalFormat("##0.0", new DecimalFormatSymbols(Locale.US));

        for (Iterator<Number> iter = values.iterator(); iter.hasNext();) {
            Number value = iter.next();
            double normalizedNumber = 100.0 * (value.doubleValue() - minPossibleValue) / valueRange;
            retVal.append(formatter.format(normalizedNumber));
            if (iter.hasNext()) {
                retVal.append(",");
            }
        }

        return retVal.toString();
    }

    /**
     * Helper method encodes a string use as a URL parameter value.
     *
     * @param string the string to encode
     * @return the encoded string
     */
    protected String encodeUrl(String string) {
        try {
            return URLEncoder.encode(string, "UTF-8");
        } catch (UnsupportedEncodingException uee) {
            //can't happen;
            return string;
        }
    }

}
TOP

Related Classes of org.perf4j.chart.GoogleChartGenerator

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.