/*******************************************************************************
 * This library 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 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 * 
 * Contact Info:
 * 	Bruce Donald
 * 	Duke University
 * 	Department of Computer Science
 * 	Levine Science Research Center (LSRC)
 * 	Durham
 * 	NC 27708-0129 
 * 	USA
 * 	brd@cs.duke.edu
 * 
 * Copyright (C) 2011 Jeffrey W. Martin and Bruce R. Donald
 * 
 * <signature of Bruce Donald>, April 2011
 * Bruce Donald, Professor of Computer Science
 ******************************************************************************/


package edu.duke.donaldLab.jdshot.disco;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Shape;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;

import org.jfree.chart.ChartTheme;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.StandardChartTheme;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.StandardBarPainter;
import org.jfree.chart.renderer.xy.StandardXYBarPainter;
import org.jfree.chart.renderer.xy.XYDifferenceRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.statistics.HistogramDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.ui.RectangleInsets;
import org.jfree.util.ShapeUtilities;

import edu.duke.donaldLab.jdshot.chart.ChartFactory;
import edu.duke.donaldLab.jdshot.chart.GeometryDataset;
import edu.duke.donaldLab.jdshot.chart.GeometryRenderer;
import edu.duke.donaldLab.jdshot.chart.GeometryType;
import edu.duke.donaldLab.jdshot.chart.SansonFlamsteedRenderer;
import edu.duke.donaldLab.jdshot.chart.Shapes;
import edu.duke.donaldLab.jdshot.disco.AnnulusIndex.AnnulusEntry;
import edu.duke.donaldLab.jdshot.disco.StructureScores.ScoreSample;
import edu.duke.donaldLab.jdshot.disco.ViolationCalculator.Violation;
import edu.duke.donaldLab.share.geom.Annulus2;
import edu.duke.donaldLab.share.geom.LineSegment2;
import edu.duke.donaldLab.share.geom.Vector2;
import edu.duke.donaldLab.share.geom.Vector3;
import edu.duke.donaldLab.share.io.Transformer;
import edu.duke.donaldLab.share.math.Quaternion;
import edu.duke.donaldLab.share.nmr.AlignmentTensor;
import edu.duke.donaldLab.share.nmr.AlignmentTensorAxis;
import edu.duke.donaldLab.share.nmr.Assignment;
import edu.duke.donaldLab.share.nmr.DistanceRestraint;
import edu.duke.donaldLab.share.protein.AtomAddressInternal;

public class Plotter
{
	/**************************
	 *   Definitions
	 **************************/
	
	public static final Color Red = new Color( 255, 66, 14 );
	public static final Color Green = new Color( 87, 157, 28 );
	public static final Color Blue = new Color( 0, 69, 134 );
	public static final Color MediumGrey = new Color( 180, 180, 180 );
	private static final Color[] Colors = { Blue, Red, Green, MediumGrey };
	
	
	/**************************
	 *   Constructors
	 **************************/
	
	static
	{
		org.jfree.chart.ChartFactory.setChartTheme( getDiscoTheme() );
	}
	
	
	/**************************
	 *   Static Functions
	 **************************/
	
	public static JFreeChart plotArrangement( List<FaceInfo> msrs, AnnulusIndex annulusIndex )
	{
		return plotArrangement( msrs, annulusIndex, null, false, (Vector2)null );
	}
	
	public static JFreeChart plotArrangement( List<FaceInfo> msrs, AnnulusIndex annulusIndex, AnnulusIndex untrustedAnnulusIndex )
	{
		return plotArrangement( msrs, annulusIndex, untrustedAnnulusIndex, false, (Vector2)null );
	}
	
	public static JFreeChart plotArrangement( List<FaceInfo> msrs, AnnulusIndex annulusIndex, boolean isZoomed )
	{
		return plotArrangement( msrs, annulusIndex, null, isZoomed, (Vector2)null );
	}
	
	public static JFreeChart plotArrangement( List<FaceInfo> msrs, AnnulusIndex annulusIndex, AnnulusIndex untrustedAnnulusIndex, boolean isZoomed )
	{
		return plotArrangement( msrs, annulusIndex, untrustedAnnulusIndex, isZoomed, (Vector2)null );
	}
	
	public static JFreeChart plotArrangement( List<FaceInfo> msrs, AnnulusIndex annulusIndex, boolean isZoomed, Vector3 referencePosition )
	{
		return plotArrangement( msrs, annulusIndex, isZoomed, new Vector2( referencePosition.x, referencePosition.y ) );
	}
	
	public static JFreeChart plotArrangement( List<FaceInfo> msrs, AnnulusIndex annulusIndex, boolean isZoomed, Vector2 referencePosition )
	{
		return plotArrangement( msrs, annulusIndex, null, isZoomed, referencePosition );
	}
	
	public static JFreeChart plotArrangement( List<FaceInfo> msrs, AnnulusIndex annulusIndex, AnnulusIndex untrustedAnnulusIndex, boolean isZoomed, Vector2 referencePosition )
	{
		GeometryDataset dataset = new GeometryDataset();
		GeometryRenderer renderer = new GeometryRenderer();
		dataset.addSeries( "Annuli", GeometryType.AnnuliUnion, collectAnnuli( annulusIndex ) );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.darkGray, new BasicStroke( 1.0f ) );
		renderer.setUseForBounds( dataset.getLastSeries(), !isZoomed );
		
		if( untrustedAnnulusIndex != null )
		{
			dataset.addSeries( "Untrusted Annuli", GeometryType.AnnuliUnion, collectAnnuli( untrustedAnnulusIndex ) );
			renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.gray, new BasicStroke( 1.0f ) );
			renderer.setUseForBounds( dataset.getLastSeries(), !isZoomed );
		}
		
		dataset.addSeries( "MSRs", GeometryType.Face, msrs );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.blue, new BasicStroke( 2.0f ) );
		
		if( referencePosition != null )
		{
			dataset.addSeries( "Reference Position", GeometryType.Point, Transformer.toArrayList( referencePosition ) );
			renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.magenta, new BasicStroke( 2.0f ) );
			renderer.setSeriesShape( dataset.getLastSeries(), Shapes.Star.getShape() );
		}
		
		return ChartFactory.createGeometryChart(
			"Angstroms",
			"Angstroms",
			renderer,
			dataset
		);
	}
	
	public static JFreeChart plotCompareMsrs( List<FaceInfo> msrsLeft, List<FaceInfo> msrsRight, AnnulusIndex annulusIndex, boolean isZoomed, Vector3 referencePosition )
	{
		return plotCompareMsrs( msrsLeft, msrsRight, annulusIndex, isZoomed, new Vector2( referencePosition.x, referencePosition.y ) );
	}
	
	public static JFreeChart plotCompareMsrs( List<FaceInfo> msrsLeft, List<FaceInfo> msrsRight, AnnulusIndex annulusIndex, boolean isZoomed, Vector2 referencePosition )
	{
		// collect the annuli
		ArrayList<ArrayList<Annulus2>> annuli = new ArrayList<ArrayList<Annulus2>>( annulusIndex.getNumRestraints() );
		for( Entry<DistanceRestraint<AtomAddressInternal>,HashMap<Assignment<AtomAddressInternal>,AnnulusEntry>> entry : annulusIndex )
		{
			ArrayList<Annulus2> group = new ArrayList<Annulus2>( entry.getValue().size() );
			for( AnnulusEntry annulusEntry : entry.getValue().values() )
			{
				group.add( annulusEntry.annulus );
			}
			annuli.add( group );
		}
		
		// build the dataset
		GeometryDataset dataset = new GeometryDataset();
		GeometryRenderer renderer = new GeometryRenderer();
		dataset.addSeries( "Annuli", GeometryType.AnnuliUnion, annuli );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.gray, new BasicStroke( 1.0f ) );
		renderer.setUseForBounds( dataset.getLastSeries(), !isZoomed );
		dataset.addSeries( "Left MSRs", GeometryType.Face, msrsLeft );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Blue, new BasicStroke( 2.0f ) );
		dataset.addSeries( "Right MSRs", GeometryType.Face, msrsRight );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Green, new BasicStroke( 2.0f ) );
		
		if( referencePosition != null )
		{
			dataset.addSeries( "Reference Position", GeometryType.Point, Transformer.toArrayList( referencePosition ) );
			renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.magenta, new BasicStroke( 2.0f ) );
			renderer.setSeriesShape( dataset.getLastSeries(), Shapes.Star.getShape() );
		}
		
		return ChartFactory.createGeometryChart(
			"Angstroms",
			"Angstroms",
			renderer,
			dataset
		);
	}
	
	public static JFreeChart plotMsrsOverlap( List<List<FaceInfo>> msrsStack, Vector2 referencePosition )
	{
		// build the dataset
		GeometryDataset dataset = new GeometryDataset();
		GeometryRenderer renderer = new GeometryRenderer();
		for( int i=0; i<msrsStack.size(); i++ )
		{
			// compute the color
			List<FaceInfo> msrs = msrsStack.get( i );
			double factor = (double)( msrsStack.size() - i - 1 ) / (double)( msrsStack.size() - 1 );
			Color color = new Color( (int)( 255.0 * ( 1.0 - factor ) ), 0, (int)( 255.0 * factor ) );
			dataset.addSeries( "MSRs", GeometryType.Face, msrs );
			renderer.setSeriesStyle( dataset.getLastSeries(), null, color, new BasicStroke( 2.0f ) );
		}
		
		dataset.addSeries( "Reference Position", GeometryType.Point, Transformer.toArrayList( referencePosition ) );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.magenta, new BasicStroke( 2.0f ) );
		renderer.setSeriesShape( dataset.getLastSeries(), Shapes.Star.getShape() );
		
		return ChartFactory.createGeometryChart(
			"Angstroms",
			"Angstroms",
			renderer,
			dataset
		);
	}
	
	public static JFreeChart plotTwoFaces( FaceInfo left, FaceInfo right )
	{
		// build the dataset
		GeometryDataset dataset = new GeometryDataset();
		GeometryRenderer renderer = new GeometryRenderer();
		dataset.addSeries( "Left", GeometryType.Face, Arrays.asList( left ) );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.blue, new BasicStroke( 2.0f ) );
		dataset.addSeries( "Right", GeometryType.Face, Arrays.asList( right ) );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.red, new BasicStroke( 1.0f ) );
		
		return ChartFactory.createGeometryChart(
			"Angstroms",
			"Angstroms",
			renderer,
			dataset
		);
	}
	
	public static JFreeChart plotSampling( List<FaceInfo> msrs, List<Vector2> sampledPoints )
	{
		// build the dataset
		GeometryDataset dataset = new GeometryDataset();
		GeometryRenderer renderer = new GeometryRenderer();
		dataset.addSeries( "MSRs", GeometryType.Face, msrs );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.blue, new BasicStroke( 2.0f ) );
		dataset.addSeries( "Sampled Points", GeometryType.Point, sampledPoints );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.red, new BasicStroke( 1.0f ) );
		
		return ChartFactory.createGeometryChart(
			"Angstroms",
			"Angstroms",
			renderer,
			dataset
		);
	}
	
	public static JFreeChart plotViolations( List<FaceInfo> msrs, List<Violation> violations, AnnulusIndex annulusIndex, List<DistanceRestraint<AtomAddressInternal>> restraints )
	{
		return plotViolations( msrs, violations, annulusIndex, restraints, false );
	}
	
	public static JFreeChart plotViolations( List<FaceInfo> msrs, List<Violation> violations, AnnulusIndex annulusIndex, List<DistanceRestraint<AtomAddressInternal>> restraints, boolean isZoomed )
	{
		// collect the annuli
		ArrayList<ArrayList<Annulus2>> annuli = new ArrayList<ArrayList<Annulus2>>( annulusIndex.getNumRestraints() );
		for( DistanceRestraint<AtomAddressInternal> restraint : restraints )
		{
			List<AnnulusEntry> entries = annulusIndex.getEntries( restraint );
			ArrayList<Annulus2> group = new ArrayList<Annulus2>( entries.size() );
			for( AnnulusEntry entry : entries )
			{
				group.add( entry.annulus );
			}
			annuli.add( group );
		}
		
		// collect the lines
		ArrayList<LineSegment2> lines = new ArrayList<LineSegment2>();
		for( Violation violation : violations )
		{
			lines.add( new LineSegment2( violation.annulusPoint, violation.facePoint ) );
		}
		
		// build the dataset
		GeometryDataset dataset = new GeometryDataset();
		GeometryRenderer renderer = new GeometryRenderer();
		dataset.addSeries( "Violated Restraints", GeometryType.AnnuliUnion, annuli );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.red, new BasicStroke( 1.0f ) );
		renderer.setUseForBounds( dataset.getLastSeries(), !isZoomed );
		dataset.addSeries( "MSRs", GeometryType.Face, msrs );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.blue, new BasicStroke( 2.0f ) );
		dataset.addSeries( "Violations", GeometryType.LineSegment, lines );
		renderer.setSeriesStyle(
			dataset.getLastSeries(),
			null,
			Color.black,
			new BasicStroke(
				1.0f,
				BasicStroke.CAP_ROUND,
				BasicStroke.JOIN_ROUND,
				10.0f,
				new float[] { 5.0f },
				0.0f
			)
		);
		
		return ChartFactory.createGeometryChart(
			"Angstroms",
			"Angstroms",
			renderer,
			dataset
		);
	}
	
	@SuppressWarnings("unchecked")
	public static JFreeChart plotAnnulusViolation( FaceInfo msr, Annulus2 annulus, double innerDist, double outerDist, Vector2 facePoint, Vector2 annulusPoint )
	{
		// build the dataset
		GeometryDataset dataset = new GeometryDataset();
		GeometryRenderer renderer = new GeometryRenderer();
		
		// draw the annulus
		dataset.addSeries( "Annulus", GeometryType.AnnuliUnion, Arrays.asList( Arrays.asList( annulus ) ) );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.darkGray, new BasicStroke( 1.0f ) );
		renderer.setUseForBounds( dataset.getLastSeries(), false );
		
		// draw the MSR
		dataset.addSeries( "MSRs", GeometryType.Face, Arrays.asList( msr ) );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.blue, new BasicStroke( 2.0f ) );
		
		// draw the radii
		Annulus2 boundAnnulus = new Annulus2( annulus.center, innerDist, outerDist );
		dataset.addSeries( "Annulus", GeometryType.AnnuliUnion, Arrays.asList( Arrays.asList( boundAnnulus ) ) );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.blue, new BasicStroke( 1.0f ) );
		renderer.setUseForBounds( dataset.getLastSeries(), false );
		
		// draw the points
		dataset.addSeries( "Points", GeometryType.Point, Arrays.asList( facePoint, annulusPoint, annulus.center ) );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.black, new BasicStroke( 2.0f ) );
		renderer.setSeriesShape( dataset.getLastSeries(), Shapes.Star.getShape() );
		
		return ChartFactory.createGeometryChart(
			"Angstroms",
			"Angstroms",
			renderer,
			dataset
		);
	}
	
	@SuppressWarnings("unchecked")
	public static JFreeChart plotAnnuliViolation( List<FaceInfo> msrs, ArrayList<AnnulusEntry> entries, Violation violation )
	{
		// build the dataset
		GeometryDataset dataset = new GeometryDataset();
		GeometryRenderer renderer = new GeometryRenderer();
		
		// collect the annuli
		ArrayList<Annulus2> annuli = new ArrayList<Annulus2>( entries.size() );
		for( AnnulusEntry entry : entries )
		{
			annuli.add( entry.annulus );
		}
		
		// draw the annuli
		dataset.addSeries( "Annuli", GeometryType.AnnuliUnion, Arrays.asList( annuli ) );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.darkGray, new BasicStroke( 1.0f ) );
		//renderer.setUseForBounds( dataset.getLastSeries(), false );
		
		// draw the MSRs
		dataset.addSeries( "MSRs", GeometryType.Face, msrs );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.blue, new BasicStroke( 2.0f ) );
		
		// draw the points
		dataset.addSeries( "Points", GeometryType.Point, Arrays.asList( violation.facePoint, violation.annulusPoint ) );
		renderer.setSeriesStyle( dataset.getLastSeries(), null, Color.black, new BasicStroke( 2.0f ) );
		renderer.setSeriesShape( dataset.getLastSeries(), Shapes.Star.getShape() );
		
		return ChartFactory.createGeometryChart(
			"Angstroms",
			"Angstroms",
			renderer,
			dataset
		);
	}
	
	public static JFreeChart plotStructureScores( List<ScoreSample> scoreSamples )
	{
		return plotStructureScores( scoreSamples, 10000.0 );
	}
	
	public static JFreeChart plotStructureScores( List<ScoreSample> scoreSamples, double outlierThreshold )
	{
		// build the dataset
		XYSeriesCollection dataset = new XYSeriesCollection();
		XYSeries series = new XYSeries( "Computed Structures" );
		int numOutliersOmitted = 0;
		for( ScoreSample sample : scoreSamples )
		{
			// ignore extreme outliers
			if( sample.energy > outlierThreshold )
			{
				numOutliersOmitted++;
				continue;
			}
			
			series.add( sample.energy, sample.restraintRmsd );
		}
		dataset.addSeries( series );
		
		// build the outliers text
		String outliersText = "";
		if( numOutliersOmitted > 0 )
		{
			outliersText = " - " + numOutliersOmitted + " severly clashing " + ( numOutliersOmitted == 1 ? "structure" : "structures" ) + " omitted";
		}
		
		JFreeChart chart = ChartFactory.createScatterPlot(
			"",
			"van der Waals Energy (kcal/mol)" + outliersText,
			"RMS distance restraint violation (\u212B)",
			dataset,
			PlotOrientation.VERTICAL,
			false,
			false,
			false
		);
		
		// set styles
		XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer)chart.getXYPlot().getRenderer();
		renderer.setUseOutlinePaint( true );
		renderer.setUseFillPaint( true );
		renderer.setSeriesOutlineStroke( 0, new BasicStroke( 1.0f ) );
		renderer.setSeriesOutlinePaint( 0, Color.black );
		renderer.setSeriesFillPaint( 0, Blue );
		renderer.setSeriesShape( 0, ShapeUtilities.createDiamond( 7.0f ) );
		
		return chart;
	}
	
	public static JFreeChart plotStructureScoresWithReference( List<Vector2> computed, Vector2 reference )
	{
		// build the dataset
		XYSeriesCollection dataset = new XYSeriesCollection();
		XYSeries series = new XYSeries( "Computed Structures" );
		for( Vector2 v : computed )
		{
			series.add( v.x, v.y );
		}
		dataset.addSeries( series );
		series = new XYSeries( "Reference Structure" );
		series.add( reference.x, reference.y );
		dataset.addSeries( series );
		
		JFreeChart chart = ChartFactory.createScatterPlot(
			"",
			"van der Waals Energy (kcal/mol)",
			"RMS distance restraint violation (\u212B)",
			dataset,
			PlotOrientation.VERTICAL,
			false,
			false,
			false
		);
		
		// set styles
		XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer)chart.getXYPlot().getRenderer();
		renderer.setUseOutlinePaint( true );
		renderer.setUseFillPaint( true );
		renderer.setSeriesOutlineStroke( 0, new BasicStroke( 1.0f ) );
		renderer.setSeriesOutlinePaint( 0, Color.black );
		renderer.setSeriesFillPaint( 0, Blue );
		renderer.setSeriesOutlineStroke( 1, new BasicStroke( 1.0f ) );
		renderer.setSeriesOutlinePaint( 1, Color.black );
		renderer.setSeriesFillPaint( 1, Red );
		renderer.setSeriesShape( 0, ShapeUtilities.createDiamond( 7.0f ) );
		renderer.setSeriesShape( 1, ShapeUtilities.createUpTriangle( 8.0f ) );
		
		return chart;
	}
	
	public static JFreeChart plotAlignmentTensors( List<AlignmentTensor> tensors )
	{
		// build the dataset
		XYSeriesCollection dataset = new XYSeriesCollection();
		XYSeries xaxis = new XYSeries( "X Axis" );
		XYSeries yaxis = new XYSeries( "Y Axis" );
		XYSeries zaxis = new XYSeries( "Z Axis" );
		dataset.addSeries( xaxis );
		dataset.addSeries( yaxis );
		dataset.addSeries( zaxis );
		double[] angles = new double[2];
		for( AlignmentTensor tensor : tensors )
		{
			tensor.getXAxis().toAngles( angles );
			xaxis.add( angles[0], angles[1] );
			tensor.getYAxis().toAngles( angles );
			yaxis.add( angles[0], angles[1] );
			tensor.getZAxis().toAngles( angles );
			zaxis.add( angles[0], angles[1] );
		}
		
		// set the colors and make the plot
		SansonFlamsteedRenderer renderer = new SansonFlamsteedRenderer();
		renderer.setSeriesFillPaint( 0, Color.red );
		renderer.setSeriesFillPaint( 1, Color.green );
		renderer.setSeriesFillPaint( 2, Color.blue );
		return ChartFactory.createSansonFlamsteedChart( "", dataset, renderer );
	}
	
	public static JFreeChart plotAlignmentTensorsAxis( List<AlignmentTensor> tensors, AlignmentTensorAxis axis )
	{
		// extract the Z axes and plot
		ArrayList<Vector3> axes = new ArrayList<Vector3>();
		for( AlignmentTensor tensor : tensors )
		{
			axes.add( tensor.getAxis( axis ) );
		}
		return plotOrientations( axes );
	}
	
	public static JFreeChart plotAlignmentTensorsAxis( List<AlignmentTensor> tensors, AlignmentTensor bestTensor, AlignmentTensorAxis axis )
	{
		// extract the Z axes and plot
		ArrayList<Vector3> axes = new ArrayList<Vector3>();
		for( AlignmentTensor tensor : tensors )
		{
			axes.add( tensor.getAxis( axis ) );
		}
		return plotOrientations( axes, bestTensor.getAxis( axis ) );
	}
	
	public static JFreeChart plotAlignmentTensorsAxisWithReference( List<AlignmentTensor> tensors, AlignmentTensor bestTensor, AlignmentTensorAxis axis, Vector3 referenceOrientation )
	{
		// extract the Z axes and plot
		ArrayList<List<Vector3>> manyAxes = new ArrayList<List<Vector3>>();
		
		// add the sampled axes
		ArrayList<Vector3> axes = new ArrayList<Vector3>();
		for( AlignmentTensor tensor : tensors )
		{
			axes.add( tensor.getAxis( axis ) );
		}
		manyAxes.add( axes );
		
		// add the reference axis
		axes = new ArrayList<Vector3>();
		axes.add( referenceOrientation );
		manyAxes.add( axes );
		
		return plotManyOrientationsCartesian( manyAxes, bestTensor.getAxis( axis ), new Color[] { MediumGrey, Red } );
	}
	
	public static JFreeChart plotOrientationSampling( List<AlignmentTensor> tensors, AlignmentTensor bestTensor, AlignmentTensorAxis axis, List<Vector3> sampledOrientations )
	{
		// extract the Z axes and plot
		ArrayList<Vector3> axes = new ArrayList<Vector3>();
		for( AlignmentTensor tensor : tensors )
		{
			axes.add( tensor.getAxis( axis ) );
		}
		
		ArrayList<List<Vector3>> orientations = new ArrayList<List<Vector3>>();
		orientations.add( axes );
		orientations.add( sampledOrientations );
		return plotManyOrientationsCartesian(
			orientations,
			bestTensor.getAxis( axis ),
			new Color[] { MediumGrey, Blue }
		);
	}
	
	public static JFreeChart plotOrientations( List<Vector3> orientations )
	{
		// build the dataset
		XYSeriesCollection dataset = new XYSeriesCollection();
		XYSeries series = new XYSeries( "" );
		dataset.addSeries( series );
		double[] angles = new double[2];
		for( Vector3 orientation : orientations )
		{
			orientation.toAngles( angles );
			series.add( angles[0], angles[1] );
		}
		
		// set the colors and make the plot
		SansonFlamsteedRenderer renderer = new SansonFlamsteedRenderer();
		renderer.setSeriesFillPaint( 0, Color.blue );
		return ChartFactory.createSansonFlamsteedChart( "", dataset, renderer );
	}
	
	public static JFreeChart plotAlignmentTensorsAxisWithReferenceAndSampling( List<AlignmentTensor> tensors, AlignmentTensor bestTensor, AlignmentTensorAxis axis, Vector3 referenceOrientation, List<Vector3> orientations )
	{
		// extract the Z axes and plot
		ArrayList<List<Vector3>> manyAxes = new ArrayList<List<Vector3>>();
		
		// add the sampled axes
		ArrayList<Vector3> axes = new ArrayList<Vector3>();
		for( AlignmentTensor tensor : tensors )
		{
			axes.add( tensor.getAxis( axis ) );
		}
		manyAxes.add( axes );
		
		// add the reference axis
		axes = new ArrayList<Vector3>();
		axes.add( referenceOrientation );
		manyAxes.add( axes );
		
		// add the sampled orientations
		manyAxes.add( orientations );
		
		return plotManyOrientationsCartesian( manyAxes, bestTensor.getAxis( axis ), new Color[] { MediumGrey, Red, Blue } );
	}
	
	public static JFreeChart plotOrientations( List<Vector3> orientations, Vector3 center )
	{
		List<List<Vector3>> list = new ArrayList<List<Vector3>>();
		list.add( orientations );
		return plotManyOrientations( list, center );
	}
	
	public static JFreeChart plotManyOrientations( List<List<Vector3>> orientations )
	{
		return plotManyOrientations( orientations, Vector3.getUnitX(), Colors );
	}
	
	public static JFreeChart plotManyOrientations( List<List<Vector3>> orientations, Vector3 center )
	{
		return plotManyOrientations( orientations, center, Colors );
	}
	
	public static JFreeChart plotManyOrientations( List<List<Vector3>> orientations, Vector3 center, Color[] colors )
	{
		// build the dataset
		XYSeriesCollection dataset = new XYSeriesCollection();
		double[] angles = new double[2];
		Quaternion rotation = new Quaternion();
		Quaternion.getRotation( rotation, center, Vector3.getUnitX() );
		SansonFlamsteedRenderer renderer = new SansonFlamsteedRenderer();
		
		Vector3 rotatedOrientation = new Vector3();
		for( int i=0; i<orientations.size(); i++ )
		{
			XYSeries series = new XYSeries( "" );
			dataset.addSeries( series );
			for( Vector3 orientation : orientations.get( i ) )
			{
				// rotate the vector
				rotatedOrientation.set( orientation );
				rotation.rotate( rotatedOrientation );
				
				// get the angles and add to the dataset
				rotatedOrientation.toAngles( angles );
				series.add( angles[0], angles[1] );
			}
			
			renderer.setSeriesFillPaint( i, getColor( colors, i ) );
		}
		
		// set the colors and make the plot
		return ChartFactory.createSansonFlamsteedChart( "", dataset, renderer );
	}
	
	public static JFreeChart plotOrientationsCartesian( List<Vector3> orientations, Vector3 center )
	{
		List<List<Vector3>> list = new ArrayList<List<Vector3>>();
		list.add( orientations );
		return plotManyOrientationsCartesian( list, center );
	}
	
	public static JFreeChart plotManyOrientationsCartesian( List<List<Vector3>> orientations, Vector3 center )
	{
		return plotManyOrientationsCartesian( orientations, center, Colors );
	}
	
	public static JFreeChart plotManyOrientationsCartesian( List<List<Vector3>> orientations, Vector3 center, Color[] colors )
	{
		// build the dataset
		GeometryDataset dataset = new GeometryDataset();
		GeometryRenderer renderer = new GeometryRenderer();
		
		double[] angles = new double[2];
		Quaternion rotation = new Quaternion();
		Quaternion.getRotation( rotation, center, Vector3.getUnitX() );
		Vector3 rotatedOrientation = new Vector3();
		
		for( int i=0; i<orientations.size(); i++ )
		{
			// build the series
			List<Vector2> sampledPoints = new ArrayList<Vector2>( orientations.size() );
			for( Vector3 orientation : orientations.get( i ) )
			{
				// rotate the vector to the y-axis (ish)
				rotatedOrientation.set( orientation );
				rotation.rotate( rotatedOrientation );
				
				// get the angles (convert to degrees)
				rotatedOrientation.toAngles( angles );
				sampledPoints.add( new Vector2( Math.toDegrees( angles[0] ), Math.toDegrees( angles[1] ) ) );
			}
			
			dataset.addSeries( "Series " + i, GeometryType.Point, sampledPoints );
			renderer.setSeriesStyle( dataset.getLastSeries(), getColor( colors, i ), null, null );
		}
		
		return ChartFactory.createGeometryChart(
			"Equatorial",
			"Polar",
			renderer,
			dataset
		);
	}
	
	public static JFreeChart plotAlignmentTensorAxisDeviation( List<AlignmentTensor> tensors, AlignmentTensor bestTensor, AlignmentTensorAxis axis )
	{
		Vector3 bestAxis = bestTensor.getAxis( axis );
		
		// convert to a list of deviation angles
		double maxDeviation = 0.0;
		double[] deviations = new double[tensors.size()];
		for( int i=0; i<tensors.size(); i++ )
		{
			deviations[i] = Math.toDegrees( Math.acos( bestAxis.getDot( tensors.get( i ).getAxis( axis ) ) ) );
			maxDeviation = Math.max( maxDeviation, deviations[i] );
		}
		
		// build the dataset
		HistogramDataset dataset = new HistogramDataset();
		dataset.addSeries( "", deviations, 16, 0, Math.ceil( maxDeviation ) );
		
		return ChartFactory.createHistogram(
            null,
            "Deviation angle (deg)",
            "Number of Axes",
            dataset
        );
	}
	
	public static JFreeChart plotRanges( List<Double> padPercents, List<Double> mins, List<Double> maxs )
	{
		// build the dataset
		XYSeriesCollection dataset = new XYSeriesCollection();
		XYSeries minSeries = new XYSeries( "Min" );
		XYSeries maxSeries = new XYSeries( "Max" );
        for( int i=0; i<padPercents.size(); i++ )
        {
        	minSeries.add( padPercents.get( i ), mins.get( i ) );
        	maxSeries.add( padPercents.get( i ), maxs.get( i ) );
        }
        dataset.addSeries( minSeries );
        dataset.addSeries( maxSeries );
        
        // build the chart
		final JFreeChart chart = ChartFactory.createXYChart(
			"",
			"Pad Percents", 
			"Distance",
			dataset
		);
		
		// set renderer
		final XYDifferenceRenderer renderer = new XYDifferenceRenderer( Color.red, Color.blue, true );
		chart.getXYPlot().setRenderer( renderer );
		
		// force zero to show in the y axis
		((NumberAxis)chart.getXYPlot().getDomainAxis()).setAutoRangeIncludesZero( true );
		
		// set styles
		Shape shape = ShapeUtilities.createDiamond( 4.0f );
		renderer.setSeriesStroke( 0, new BasicStroke( 2.0f ) );
		renderer.setSeriesStroke( 1, new BasicStroke( 2.0f ) );
		renderer.setSeriesShape( 0, shape );
		renderer.setSeriesShape( 1, shape );
		
        return chart;      
	}
	
	private static ArrayList<ArrayList<Annulus2>> collectAnnuli( AnnulusIndex annulusIndex )
	{
		ArrayList<ArrayList<Annulus2>> annuli = new ArrayList<ArrayList<Annulus2>>( annulusIndex.getNumRestraints() );
		for( Entry<DistanceRestraint<AtomAddressInternal>,HashMap<Assignment<AtomAddressInternal>,AnnulusEntry>> entry : annulusIndex )
		{
			ArrayList<Annulus2> group = new ArrayList<Annulus2>( entry.getValue().size() );
			for( AnnulusEntry annulusEntry : entry.getValue().values() )
			{
				group.add( annulusEntry.annulus );
			}
			annuli.add( group );
		}
		return annuli;
	}
	
	private static Color getColor( Color[] colors, int i )
	{
		return colors[ i % colors.length ];
	}
	
	private static ChartTheme getDiscoTheme( )
	{
		StandardChartTheme theme = new StandardChartTheme( "Disco" );
		theme.setPlotBackgroundPaint( Color.white );
		theme.setDomainGridlinePaint( Color.gray );
		theme.setRangeGridlinePaint( Color.gray );
		theme.setAxisOffset( new RectangleInsets( 0, 0, 0, 0 ) );
		theme.setExtraLargeFont( new Font( "SansSerif", theme.getExtraLargeFont().getStyle(), theme.getExtraLargeFont().getSize() ) );
		theme.setLargeFont( new Font( "SansSerif", theme.getLargeFont().getStyle(), theme.getLargeFont().getSize() ) );
		theme.setRegularFont( new Font( "SansSerif", theme.getRegularFont().getStyle(), theme.getRegularFont().getSize() ) );
		theme.setSmallFont( new Font( "SansSerif", theme.getSmallFont().getStyle(), theme.getSmallFont().getSize() ) );
		theme.setBarPainter( new StandardBarPainter() );
        theme.setXYBarPainter( new StandardXYBarPainter() );
        theme.setShadowVisible( false );
		return theme;
	}
}
