mirror of https://github.com/mhowlett/nplot.git
409 lines
14 KiB
C#
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;
|
|
}
|
|
}
|
|
} |