// Copyright © 2007 John M Rusk (http://dotnet.agilekiwi.com)
//
// You may use this source code ("The Software") in any manner you wish,
// subject to the following conditions:
//
// (a) The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// (b) 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.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
// NOTE: ** this version has a small bug, in which the lowest point of the sparkline does not
// quite touch the bottom of the rendering area. I.e. the scaling is slightly "off", not
// quite scaling the sparkline far enough to touch the bottom of the rendering area. **
///
/// Basic generation of "sparklines" for .net WinForms. C# 2.0 and later.
/// See http://en.wikipedia.org/wiki/Sparkline for background to sparklines.
///
namespace AgileKiwi.Sparklines.WinForms
{
///
/// A Control that can be placed on a form
///
public class SparklineControl : Control
{
public SparklineControl()
{
Renderer.WidthChanged += RendererWidthChanged;
}
readonly SparklineRenderer _renderer = new SparklineRenderer();
///
/// Set properties on this object to control the sparkline
///
public SparklineRenderer Renderer
{
get { return _renderer; }
}
///
/// Sync our width to that calculated by the renederer (it drives the width)
///
void RendererWidthChanged(object sender, EventArgs e)
{
Width = Renderer.Width;
}
///
/// Sync renderer height to our height (we drive the height)
///
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
Renderer.Height = Height;
}
protected override void OnPaint(PaintEventArgs e)
{
Renderer.Render(e.Graphics);
}
}
///
/// Renders a sparkline to a given object. Can be used by
/// and can also be used in other contexts.
/// For info on sparklines, see http://www.edwardtufte.com/bboard/q-and-a-fetch-msg?msg_id=0001OR
///
public class SparklineRenderer
{
ICollection _dataElements = new List();
int _pixelsPerElement;
int _height;
Color _lineColor = Color.FromArgb(40, 40, 40);
Color _backgroundColor = Color.White;
public event EventHandler WidthChanged;
///
/// Number of pixels to use for each data element. Width of control is automatically set to
/// count of *
///
public int PixelsPerElement
{
get { return _pixelsPerElement; }
set
{
_pixelsPerElement = value;
OnWidthChanged();
}
}
///
/// Data displayed by the sparkline.
///
public ICollection DataElements
{
get { return _dataElements; }
set
{
_dataElements = value;
OnWidthChanged();
}
}
///
/// Height in pixels. Must be specified by user.
///
public int Height
{
get { return _height; }
set { _height = value; }
}
///
/// Width in pixels. Automaticall set to count of *
///
public int Width
{
get { return DataElements.Count * PixelsPerElement; }
}
void OnWidthChanged()
{
if (WidthChanged != null)
WidthChanged(this, EventArgs.Empty);
}
public Color LineColor
{
get { return _lineColor; }
set { _lineColor = value; }
}
public Color BackgroundColor
{
get { return _backgroundColor; }
set { _backgroundColor = value; }
}
///
/// Draw the sparkline
///
public void Render(Graphics g)
{
g.Clear(BackgroundColor);
SmoothingMode oldSmoothing = g.SmoothingMode;
PixelOffsetMode oldOffsetMode = g.PixelOffsetMode;
try
{
// these settings seem to give the best antialiasing
g.SmoothingMode = SmoothingMode.HighQuality;
g.PixelOffsetMode = PixelOffsetMode.Default;
DoRender(g);
}
finally
{
g.SmoothingMode = oldSmoothing;
g.PixelOffsetMode = oldOffsetMode;
}
}
void DoRender(Graphics g)
{
int? max;
int? min;
GetBounds(out max, out min);
if (min == null || max == null)
return; // no range of values to draw
using (Pen pen = new Pen(LineColor, -1))
// force single pixel width (see http://www.bobpowell.net/single_pixel_lines.htm )
{
float range = max.Value - min.Value;
float multiplier = Height / range;
Point? last = null;
int x = 0;
foreach (int? element in DataElements)
{
Point? current;
if (element == null)
{
current = null;
}
else
{
int y = (int)((element - min) * multiplier);
y = Height - y; // flip y co-ord to standard PC-style coordinates
current = new Point(x, y);
if (last == null)
g.DrawEllipse(pen, current.Value.X, current.Value.Y, 1,1); // draw "one pixel"
else
g.DrawLine(pen, last.Value, current.Value);
}
last = current;
x += PixelsPerElement;
}
}
}
void GetBounds(out int? max, out int? min)
{
min = null;
max = null;
foreach (int? element in DataElements)
{
if (element < (min ?? int.MaxValue))
min = element;
if (element > (max ?? int.MinValue))
max = element;
}
if (min > 0)
min = 0; // in this implementation, we are always tying our sparkline "y axis" to 0 at the bottom, if min is positive
}
}
}