/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.util; import hudson.model.AbstractBuild; import hudson.tasks.junit.History; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.NumberAxis; import org.jfree.data.category.CategoryDataset; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import java.awt.Font; import java.io.IOException; /** * Chart generation utility code around JFreeChart. * * @see StackedAreaRenderer2 * @see DataSetBuilder * @see ShiftedCategoryAxis * @author Kohsuke Kawaguchi */ public class ChartUtil { /** * Can be used as a graph label. Only displays numbers. */ public static final class NumberOnlyBuildLabel implements Comparable<NumberOnlyBuildLabel> { //TODO: review and check whether we can do it private public final AbstractBuild build; public AbstractBuild getBuild() { return build; } public NumberOnlyBuildLabel(AbstractBuild build) { this.build = build; } public int compareTo(NumberOnlyBuildLabel that) { return this.build.number-that.build.number; } @Override public boolean equals(Object o) { if(!(o instanceof NumberOnlyBuildLabel)) return false; NumberOnlyBuildLabel that = (NumberOnlyBuildLabel) o; return build==that.build; } @Override public int hashCode() { return build.hashCode(); } @Override public String toString() { return build.getDisplayName(); } } /** * @deprecated * Use {@code awtProblemCause!=null} instead. As of 1.267. */ public static boolean awtProblem = false; /** * See issue 93. Detect an error in X11 and handle it gracefully. */ public static Throwable awtProblemCause = null; /** * Generates the graph in PNG format and sends that to the response. * * @param defaultSize * The size of the picture to be generated. These values can be overridden * by the query paramter 'width' and 'height' in the request. * @deprecated as of 1.320 * Bind {@link Graph} to the URL space. See {@link History} as an example (note that doing so involves * a bit of URL structure change.) */ public static void generateGraph(StaplerRequest req, StaplerResponse rsp, JFreeChart chart, Area defaultSize) throws IOException { generateGraph(req,rsp,chart,defaultSize.width, defaultSize.height); } /** * Generates the graph in PNG format and sends that to the response. * * @param defaultW * @param defaultH * The size of the picture to be generated. These values can be overridden * by the query paramter 'width' and 'height' in the request. * @deprecated as of 1.320 * Bind {@link Graph} to the URL space. See {@link History} as an example (note that doing so involves * a bit of URL structure change.) */ public static void generateGraph(StaplerRequest req, StaplerResponse rsp, final JFreeChart chart, int defaultW, int defaultH) throws IOException { new Graph(-1,defaultW,defaultH) { protected JFreeChart createGraph() { return chart; } }.doPng(req,rsp); } /** * Generates the clickable map info and sends that to the response. * * @deprecated as of 1.320 * Bind {@link Graph} to the URL space. See {@link History} as an example (note that doing so involves * a bit of URL structure change.) */ public static void generateClickableMap(StaplerRequest req, StaplerResponse rsp, JFreeChart chart, Area defaultSize) throws IOException { generateClickableMap(req,rsp,chart,defaultSize.width,defaultSize.height); } /** * Generates the clickable map info and sends that to the response. * * @deprecated as of 1.320 * Bind {@link Graph} to the URL space. See {@link History} as an example (note that doing so involves * a bit of URL structure change.) */ public static void generateClickableMap(StaplerRequest req, StaplerResponse rsp, final JFreeChart chart, int defaultW, int defaultH) throws IOException { new Graph(-1,defaultW,defaultH) { protected JFreeChart createGraph() { return chart; } }.doMap(req,rsp); } /** * Adjusts the Y-axis so that abnormally large value won't spoil the whole chart * by making everything look virtually 0. * * <p> * The algorithm is based on <a href="http://en.wikipedia.org/wiki/Chebyshev%27s_inequality">Chebyshev's inequality</a>, * which states that given any number sequence, nore more than 1/(N^2) values are more than N x stddev away * from the average. * * <p> * So the algorithm is to set Y-axis range so that we can see all data points that are within N x stddev * of the average. Most of the time, Cebyshev's inequality is very conservative, so it shouldn't do * much harm. * * <p> * When the algorithm does kick in, however, we can kick out at most 1 in N^2 data points. * (So for example if N=3 then we can "fix" the graph as long as we only have less than 1/(3*3)=11.111...% bad data. * * <p> * Also see issue #1246. */ public static void adjustChebyshev(CategoryDataset dataset, NumberAxis yAxis) { // first compute E(X) and Var(X) double sum=0,sum2=0; final int nColumns = dataset.getColumnCount(); final int nRows = dataset.getRowCount(); for (int i=0; i<nRows; i++ ) { Comparable rowKey = dataset.getRowKey(i); for( int j=0; j<nColumns; j++) { Comparable columnKey = dataset.getColumnKey(j); double n = dataset.getValue(rowKey,columnKey).doubleValue(); sum += n; sum2 +=n*n; } } double average = sum/(nColumns*nRows); double stddev = Math.sqrt(sum2/(nColumns*nRows)-average*average); double rangeMin = average-stddev*CHEBYSHEV_N; double rangeMax = average+stddev*CHEBYSHEV_N; // now see if there are any data points that fall outside (rangeMin,rangeMax) boolean found = false; double min=0,max=0; for (int i=0; i<nRows; i++ ) { Comparable rowKey = dataset.getRowKey(i); for( int j=0; j<nColumns; j++) { Comparable columnKey = dataset.getColumnKey(j); double n = dataset.getValue(rowKey,columnKey).doubleValue(); if(n<rangeMin || rangeMax<n) { found = true; continue; // ignore this value } min = Math.min(min,n); max = Math.max(max,n); } } if(!found) return; // no adjustment was necessary // some values fell outside the range, so adjust the Y-axis // if we are ever to extend this method to handle negative value ranges correctly, // the code after this needs modifications min = Math.min(0,min); // always include 0 in the graph max += yAxis.getUpperMargin()*(max-min); yAxis.setRange(min,max); } public static double CHEBYSHEV_N = 3; static { try { new Font("SansSerif",Font.BOLD,18).toString(); } catch (Throwable t) { awtProblemCause = t; awtProblem = true; } } }