/* * Copyright (C) 2011 Laurent Caillette * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package org.novelang.nhovestone.report; import java.awt.BasicStroke; import java.awt.Color; import java.awt.GradientPaint; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.util.Collections; import java.util.List; import java.util.Map; import javax.imageio.ImageIO; import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import org.apache.batik.dom.GenericDOMImplementation; import org.apache.batik.svggen.SVGGraphics2D; import org.dom4j.DocumentException; import org.dom4j.io.OutputFormat; import org.dom4j.io.XMLWriter; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.chart.annotations.XYTextAnnotation; import org.jfree.chart.axis.AxisLocation; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYBarRenderer; import org.jfree.chart.renderer.xy.XYSplineRenderer; import org.jfree.chart.title.LegendTitle; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; import org.jfree.ui.RectangleEdge; import org.jfree.ui.TextAnchor; import org.w3c.dom.DOMImplementation; import org.w3c.dom.Document; import org.xml.sax.InputSource; import org.novelang.Version; import org.novelang.logger.Logger; import org.novelang.logger.LoggerFactory; import org.novelang.nhovestone.MeasurementBundle; import org.novelang.nhovestone.Termination; import org.novelang.nhovestone.scenario.TimeMeasurement; import org.novelang.nhovestone.scenario.TimeMeasurer; import org.novelang.novella.VectorImageTools; /** * Generates the graph representing {@link org.novelang.nhovestone.Scenario#getMeasurements()}. * * @author Laurent Caillette */ public class Grapher { private static final Logger LOGGER = LoggerFactory.getLogger( Grapher.class ) ; private static final int DEFAULT_WIDTH_PIXELS = 600; private static final int DEFAULT_HEIGHT_PIXELS = 300; private static final int DEFAULT_WIDTH_VECTORUNIT = 500 ; private static final int DEFAULT_HEIGHT_VECTORUNIT = 250 ; /** * Using pixels because it's the only way to control image size. * Otherwise the image display remains the same whatever the absolute size is, * and whatever is done to the viewport, including SVG scaling (but except rotating it with * fox:transform but this requires block-container with absolute positioning). * So we're depending on renderer's resolution here. */ private static final String VECTORUNIT = "px" ; private static final Charset CHARSET = Charsets.UTF_8; private Grapher() { } public static BufferedImage create( final List< Long > upsizings, final Map< Version, MeasurementBundle< TimeMeasurement > > measurements, final boolean showUpsizingCount ) { return create( upsizings, measurements, showUpsizingCount, DEFAULT_WIDTH_PIXELS, DEFAULT_HEIGHT_PIXELS ) ; } public static BufferedImage create( final List< Long > upsizings, final Map< Version, MeasurementBundle< TimeMeasurement > > measurements, final boolean showUpsizingCount, final int widthPixels, final int heightPixels ) { final JFreeChart chart = createChart( upsizings, measurements, showUpsizingCount ) ; return chart.createBufferedImage( widthPixels, heightPixels ) ; } public static void exportChartAsSvg( final File svgFile, final List< Long > upsizings, final Map< Version, MeasurementBundle< TimeMeasurement > > measurements ) throws IOException { final JFreeChart chart = createChart( upsizings, measurements, false ) ; exportChartAsSvg( svgFile, chart, DEFAULT_WIDTH_VECTORUNIT, DEFAULT_HEIGHT_VECTORUNIT, VECTORUNIT ) ; } /** * Exports a JFreeChart to a SVG file. * * @param svgFile the output file. * @param chart JFreeChart to export * @param width * @param height * @throws IOException if writing the svgFile fails. * * @author Dolf Trieschnig http://dolf.trieschnigg.nl/jfreechart */ public static void exportChartAsSvg( final File svgFile, final JFreeChart chart, final int width, final int height, final String dimensionUnit ) throws IOException { final DOMImplementation domImpl = GenericDOMImplementation.getDOMImplementation(); final Document w3cDocument = domImpl.createDocument( null, "svg", null ) ; final SVGGraphics2D svgGenerator = new SVGGraphics2D( w3cDocument ) ; final java.awt.geom.Rectangle2D bounds = new Rectangle( width, height ) ; chart.draw( svgGenerator, bounds ) ; // Don't know how to set properties of the root element. // Accessing to svgGenerator.getRoot() has no effect. // So we're taking the long way here. final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream() ; svgGenerator.stream( new OutputStreamWriter( byteArrayOutputStream, CHARSET ), true ) ; LOGGER.debug( "Streamed this document: \n", new String( byteArrayOutputStream.toByteArray(), CHARSET ) ) ; final org.dom4j.Document dom4jDocument ; try { dom4jDocument = VectorImageTools.loadSvgAsDom4jDocument( new InputSource( new ByteArrayInputStream( byteArrayOutputStream.toByteArray() ) ) ) ; } catch( DocumentException e ) { throw new RuntimeException( e ) ; } setSvgDimensions( dom4jDocument.getRootElement(), width, height, dimensionUnit ) ; final OutputFormat prettyPrint = OutputFormat.createPrettyPrint(); prettyPrint.setEncoding( CHARSET.name() ) ; final OutputStream outputStream = new FileOutputStream( svgFile ) ; try { final XMLWriter writer = new XMLWriter( outputStream, prettyPrint ) ; writer.write( dom4jDocument ) ; writer.flush() ; } finally { outputStream.close() ; } LOGGER.info( "Wrote '", svgFile.getAbsolutePath(), "'." ) ; } private static void setSvgDimensions( final org.dom4j.Element element, final int width, final int height, final String dimensionUnit ) { element.addAttribute( "width", width + dimensionUnit ) ; element.addAttribute( "height", height + dimensionUnit ) ; } private static JFreeChart createChart( final List< Long > upsizings, final Map< Version, MeasurementBundle< TimeMeasurement > > measurements, final boolean showUpsizingCount ) { final JFreeChart chart = ChartFactory.createXYLineChart( null, // chart title "Total source size (KiB)", // domain axis label null, // range axis label null, // data PlotOrientation.VERTICAL, // orientation true, // include legend false, // tooltips false // urls ) ; final XYPlot plot = ( XYPlot ) chart.getPlot() ; plot.setBackgroundPaint( BACKGROUND_GRADIENT_PAINT ) ; final List< Double > cumulatedUpsizings = cumulate( upsizings ) ; addMeasurementsDataset( plot, cumulatedUpsizings, measurements ); final NumberAxis domainAxis = ( NumberAxis ) plot.getDomainAxis() ; domainAxis.setStandardTickUnits( NumberAxis.createIntegerTickUnits() ) ; domainAxis.setAxisLinePaint( NULL_COLOR ) ; domainAxis.setAutoRangeStickyZero( false ) ; if( showUpsizingCount ) { final NumberAxis novellaCountAxis = new NumberAxis( "Novella count" ); novellaCountAxis.setLowerBound( 1.0 ) ; novellaCountAxis.setUpperBound( ( double ) cumulatedUpsizings.size() ) ; novellaCountAxis.setAxisLinePaint( NULL_COLOR ) ; plot.setDomainAxis( UPSIZINGS_KEY, novellaCountAxis ) ; plot.setDomainAxisLocation( UPSIZINGS_KEY, AxisLocation.TOP_OR_RIGHT ) ; } final XYSplineRenderer measurementsRenderer = new XYSplineRenderer() ; for( int serieIndex = 0 ; serieIndex < measurements.size() ; serieIndex ++ ) { measurementsRenderer.setSeriesShapesVisible( serieIndex, false ) ; final BasicStroke stroke = new BasicStroke( serieIndex == 0 ? 3.0f : 1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND ) ; measurementsRenderer.setSeriesStroke( serieIndex, stroke ) ; } measurementsRenderer.setSeriesFillPaint( 0, new Color( 255, 0, 0, 255 ) ) ; plot.setRenderer( MEASUREMENTS_KEY, measurementsRenderer ) ; final XYBarRenderer upsizingBarRenderer = new XYBarRenderer() ; upsizingBarRenderer.setSeriesFillPaint( 0, UPSIZING_GRADIENT_PAINT ) ; upsizingBarRenderer.setSeriesPaint( 0, COLOR_BACKGROUND_DARK ) ; plot.setRenderer( UPSIZINGS_KEY, upsizingBarRenderer ) ; final LegendTitle chartLegend = chart.getLegend() ; chartLegend.setBorder( 0.0, 0.0, 0.0, 0.0 ) ; chartLegend.setPosition( RectangleEdge.TOP ); return chart; } private static void addMeasurementsDataset( final XYPlot plot, final List< Double > cumulatedUpsizings, final Map< Version, MeasurementBundle< TimeMeasurement > > measurements ) { final List< Version > versions = Lists.newArrayList( measurements.keySet() ) ; Collections.sort( versions, Ordering.from( Version.COMPARATOR ).reverse() ) ; final XYSeriesCollection measurementsDataset = new XYSeriesCollection() ; for( final Version version : versions ) { final MeasurementBundle< TimeMeasurement > measurementBundle = measurements.get( version ) ; int callIndex = 0 ; final XYSeries series = new XYSeries( version.getName() ) ; for( final TimeMeasurement measurement : measurementBundle ) { series.add( ( double ) cumulatedUpsizings.get( ++ callIndex ), convertToYValue( measurement ) ) ; } measurementsDataset.addSeries( series ) ; addAnnotations( plot, measurementBundle ) ; } plot.setDataset( MEASUREMENTS_KEY, measurementsDataset ) ; final NumberAxis measurementRangeAxis = new NumberAxis( "Response time (seconds)" ) ; measurementRangeAxis.setAutoRange( true ) ; measurementRangeAxis.setAxisLinePaint( NULL_COLOR ) ; plot.setRangeAxis( MEASUREMENTS_KEY, measurementRangeAxis ) ; plot.mapDatasetToRangeAxis( MEASUREMENTS_KEY, MEASUREMENTS_KEY ) ; } private static List< Double > cumulate( final List< Long > upsizings ) { double sum = 0.0 ; final List< Double > cumulated = Lists.newArrayList() ; for( final Long upsizing : upsizings ) { sum += ( double ) ( upsizing ) ; cumulated.add( sum / 1024.0 ) ; } return Collections.unmodifiableList( cumulated ) ; } private static void addAnnotations( final XYPlot plot, final MeasurementBundle< TimeMeasurement > measurementBundle ) { final int measurementCount = measurementBundle.getMeasurementCount(); final String annotationText ; annotationText = calculateTerminationText( measurementBundle.getTermination() ) ; if( annotationText != null && measurementCount > 0 ) { final TimeMeasurement measurement = measurementBundle.getMeasurement( measurementCount - 1 ) ; if( measurement != null ) { final XYTextAnnotation annotation = new XYTextAnnotation( annotationText, ( double ) measurementCount, convertToYValue( measurement ) ) ; annotation.setTextAnchor( TextAnchor.BOTTOM_RIGHT ) ; plot.addAnnotation( annotation ) ; } } } private static String calculateTerminationText( final Termination termination ) { final String annotationText ; if( termination == TimeMeasurer.Terminations.STRAIN ) { annotationText = null ; } else { annotationText = termination.getName() ; } return annotationText; } private static double convertToYValue( final TimeMeasurement measurement ) { return ( double ) measurement.getTimeMilliseconds() / 1000.0; } private static final int MEASUREMENTS_KEY = 0 ; private static final int UPSIZINGS_KEY = 1 ; private static final Color COLOR_BACKGROUND_DARK = new Color( 136, 167, 189 ) ; private static final Color COLOR_BACKGROUND_LIGHT = new Color( 204, 237, 255 ) ; private static final GradientPaint BACKGROUND_GRADIENT_PAINT = new GradientPaint( 0.0f, 0.0f, COLOR_BACKGROUND_DARK, 0.0f, 0.0f, COLOR_BACKGROUND_LIGHT ); private static final Color COLOR_UPSIZING_DARK = new Color( 136, 167, 189, 20 ) ; private static final Color COLOR_UPSIZING_LIGHT = new Color( 204, 237, 255, 200 ) ; private static final GradientPaint UPSIZING_GRADIENT_PAINT = new GradientPaint( 0.0f, 0.0f, COLOR_UPSIZING_DARK, 0.0f, 0.0f, COLOR_UPSIZING_LIGHT ) ; private static final Color NULL_COLOR = new Color( 0, 0, 0, 0 ) ; static void writeBitmapImage( final File imageFile, final List< Long > upsizings, final Map< Version, MeasurementBundle<TimeMeasurement > > versionMap ) throws IOException { final BufferedImage image = create( upsizings, versionMap, false ) ; ImageIO.write( image, "png", imageFile ) ; LOGGER.info( "Wrote '", imageFile.getAbsolutePath(), "'." ) ; } }