nplot/src/HistogramPlot.cs

409 lines
14 KiB
C#

/*
* NPlot - A charting library for .NET
*
* HistogramPlot.cs
* Copyright (C) 2003-2006 Matt Howlett and others.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections;
using System.Drawing;
namespace NPlot
{
/// <summary>
/// Provides ability to draw histogram plots.
/// </summary>
public class HistogramPlot : BaseSequencePlot, IPlot, ISequencePlot
{
private double baseOffset_;
private float baseWidth_ = 1.0f;
private bool center_ = true;
private bool isStacked_;
private Pen pen_ = new Pen(Color.Black);
private IRectangleBrush rectangleBrush_ = new RectangleBrushes.Solid(Color.Black);
private HistogramPlot stackedTo_;
/// <summary>
/// Set/Get the brush to use if the histogram is filled.
/// </summary>
public IRectangleBrush RectangleBrush
{
get { return rectangleBrush_; }
set { rectangleBrush_ = value; }
}
/// <summary>
/// Whether or not the histogram columns will be filled.
/// </summary>
public bool Filled { get; set; }
/// <summary>
/// The width of the histogram bar as a proportion of the data spacing
/// (in range 0.0 - 1.0).
/// </summary>
public float BaseWidth
{
get { return baseWidth_; }
set
{
if (value > 0.0 && value <= 1.0)
{
baseWidth_ = value;
}
else
{
throw new NPlotException("Base width must be between 0.0 and 1.0");
}
}
}
/// <summary>
/// If true, each histogram column will be centered on the associated abscissa value.
/// If false, each histogram colum will be drawn between the associated abscissa value, and the next abscissa value.
/// Default value is true.
/// </summary>
public bool Center
{
set { center_ = value; }
get { return center_; }
}
/// <summary>
/// If this histogram plot has another stacked on top, this will be true. Else false.
/// </summary>
public bool IsStacked
{
get { return isStacked_; }
}
/// <summary>
/// The pen used to draw the plot
/// </summary>
public Pen Pen
{
get { return pen_; }
set { pen_ = value; }
}
/// <summary>
/// The color of the pen used to draw lines in this plot.
/// </summary>
public Color Color
{
set
{
if (pen_ != null)
{
pen_.Color = value;
}
else
{
pen_ = new Pen(value);
}
}
get { return pen_.Color; }
}
/// <summary>
/// Horizontal position of histogram columns is offset by this much (in world coordinates).
/// </summary>
public double BaseOffset
{
set { baseOffset_ = value; }
get { return baseOffset_; }
}
/// <summary>
/// Renders the histogram.
/// </summary>
/// <param name="g">The Graphics surface on which to draw</param>
/// <param name="xAxis">The X-Axis to draw against.</param>
/// <param name="yAxis">The Y-Axis to draw against.</param>
public void Draw(Graphics g, PhysicalAxis xAxis, PhysicalAxis yAxis)
{
SequenceAdapter data =
new SequenceAdapter(DataSource, DataMember, OrdinateData, AbscissaData);
float yoff;
for (int i = 0; i < data.Count; ++i)
{
// (1) determine the top left hand point of the bar (assuming not centered)
PointD p1 = data[i];
if (double.IsNaN(p1.X) || double.IsNaN(p1.Y))
continue;
// (2) determine the top right hand point of the bar (assuming not centered)
PointD p2;
if (i + 1 != data.Count)
{
p2 = data[i + 1];
if (double.IsNaN(p2.X) || double.IsNaN(p2.Y))
continue;
p2.Y = p1.Y;
}
else if (i != 0)
{
p2 = data[i - 1];
if (double.IsNaN(p2.X) || double.IsNaN(p2.Y))
continue;
double offset = p1.X - p2.X;
p2.X = p1.X + offset;
p2.Y = p1.Y;
}
else
{
double offset = 1.0f;
p2.X = p1.X + offset;
p2.Y = p1.Y;
}
// (3) now account for plots this may be stacked on top of.
HistogramPlot currentPlot = this;
yoff = 0.0f;
double yval = 0.0f;
while (currentPlot.isStacked_)
{
SequenceAdapter stackedToData = new SequenceAdapter(
currentPlot.stackedTo_.DataSource,
currentPlot.stackedTo_.DataMember,
currentPlot.stackedTo_.OrdinateData,
currentPlot.stackedTo_.AbscissaData);
yval += stackedToData[i].Y;
yoff = yAxis.WorldToPhysical(yval, false).Y;
p1.Y += stackedToData[i].Y;
p2.Y += stackedToData[i].Y;
currentPlot = currentPlot.stackedTo_;
}
// (4) now account for centering
if (center_)
{
double offset = (p2.X - p1.X)/2.0f;
p1.X -= offset;
p2.X -= offset;
}
// (5) now account for BaseOffset (shift of bar sideways).
p1.X += baseOffset_;
p2.X += baseOffset_;
// (6) now get physical coordinates of top two points.
PointF xPos1 = xAxis.WorldToPhysical(p1.X, false);
PointF yPos1 = yAxis.WorldToPhysical(p1.Y, false);
PointF xPos2 = xAxis.WorldToPhysical(p2.X, false);
//PointF yPos2 = yAxis.WorldToPhysical(p2.Y, false);
if (isStacked_)
{
currentPlot = this;
while (currentPlot.isStacked_)
{
currentPlot = currentPlot.stackedTo_;
}
baseWidth_ = currentPlot.baseWidth_;
}
float width = xPos2.X - xPos1.X;
float height;
if (isStacked_)
{
height = -yPos1.Y + yoff;
}
else
{
height = -yPos1.Y + yAxis.PhysicalMin.Y;
}
float xoff = (1.0f - baseWidth_)/2.0f*width;
Rectangle r = new Rectangle((int) (xPos1.X + xoff), (int) yPos1.Y, (int) (width - 2*xoff), (int) height);
if (Filled)
{
if (r.Height != 0 && r.Width != 0)
{
// room for optimization maybe.
g.FillRectangle(rectangleBrush_.Get(r), r);
}
}
g.DrawRectangle(Pen, r.X, r.Y, r.Width, r.Height);
}
}
/// <summary>
/// Returns an x-axis that is suitable for drawing this plot.
/// </summary>
/// <returns>A suitable x-axis.</returns>
public Axis SuggestXAxis()
{
SequenceAdapter data =
new SequenceAdapter(DataSource, DataMember, OrdinateData, AbscissaData);
Axis a = data.SuggestXAxis();
if (data.Count == 0)
{
return a;
}
PointD p1;
PointD p2;
PointD p3;
PointD p4;
if (data.Count < 2)
{
p1 = data[0];
p1.X -= 1.0;
p2 = data[0];
p3 = p1;
p4 = p2;
}
else
{
p1 = data[0];
p2 = data[1];
p3 = data[data.Count - 2];
p4 = data[data.Count - 1];
}
double offset1;
double offset2;
if (!center_)
{
offset1 = 0.0f;
offset2 = p4.X - p3.X;
}
else
{
offset1 = (p2.X - p1.X)/2.0f;
offset2 = (p4.X - p3.X)/2.0f;
}
a.WorldMin -= offset1;
a.WorldMax += offset2;
return a;
}
/// <summary>
/// Returns a y-axis that is suitable for drawing this plot.
/// </summary>
/// <returns>A suitable y-axis.</returns>
public Axis SuggestYAxis()
{
if (isStacked_)
{
double tmpMax = 0.0f;
ArrayList adapterList = new ArrayList();
HistogramPlot currentPlot = this;
do
{
adapterList.Add(new SequenceAdapter(
currentPlot.DataSource,
currentPlot.DataMember,
currentPlot.OrdinateData,
currentPlot.AbscissaData)
);
} while ((currentPlot = currentPlot.stackedTo_) != null);
SequenceAdapter[] adapters =
(SequenceAdapter[]) adapterList.ToArray(typeof (SequenceAdapter));
for (int i = 0; i < adapters[0].Count; ++i)
{
double tmpHeight = 0.0f;
for (int j = 0; j < adapters.Length; ++j)
{
tmpHeight += adapters[j][i].Y;
}
tmpMax = Math.Max(tmpMax, tmpHeight);
}
Axis a = new LinearAxis(0.0f, tmpMax);
// TODO make 0.08 a parameter.
a.IncreaseRange(0.08);
return a;
}
else
{
SequenceAdapter data =
new SequenceAdapter(DataSource, DataMember, OrdinateData, AbscissaData);
return data.SuggestYAxis();
}
}
/// <summary>
/// Draws a representation of this plot in the legend.
/// </summary>
/// <param name="g">The graphics surface on which to draw.</param>
/// <param name="startEnd">A rectangle specifying the bounds of the area in the legend set aside for drawing.</param>
public void DrawInLegend(Graphics g, Rectangle startEnd)
{
if (Filled)
{
g.FillRectangle(rectangleBrush_.Get(startEnd), startEnd);
}
g.DrawRectangle(Pen, startEnd.X, startEnd.Y, startEnd.Width, startEnd.Height);
}
/// <summary>
/// Stack the histogram to another HistogramPlot.
/// </summary>
public void StackedTo(HistogramPlot hp)
{
SequenceAdapter data =
new SequenceAdapter(DataSource, DataMember, OrdinateData, AbscissaData);
SequenceAdapter hpData =
new SequenceAdapter(hp.DataSource, hp.DataMember, hp.OrdinateData, hp.AbscissaData);
if (hp != null)
{
isStacked_ = true;
if (hpData.Count != data.Count)
{
throw new NPlotException("Can stack HistogramPlot data only with the same number of datapoints.");
}
for (int i = 0; i < data.Count; ++i)
{
if (data[i].X != hpData[i].X)
{
throw new NPlotException("Can stack HistogramPlot data only with the same X coordinates.");
}
if (hpData[i].Y < 0.0f)
{
throw new NPlotException("Can stack HistogramPlot data only with positive Y coordinates.");
}
}
}
stackedTo_ = hp;
}
}
}