Click here to Skip to main content
Click here to Skip to main content

WPF Chart Control With Pan, Zoom and More

By , 10 Dec 2012
 

Sample Image - swordfishcharts.png

Introduction

This article gives an overview of the code and algorithms I created while writing a chart control for .NET 3.0/WPF. My chart control is useful in that it includes Pan/Zoom, has a clipboard helper that copies the chart to the clipboard in a resolution that the user desires, adds data to the image in the clipboard so that the user can paste a bitmap image or paste tab separated chart line values into a spreadsheet. The cursor, when moved over the chart, shows the value of the nearest point, or the value at the centre of the cursor. Polygons and lines can be added by coordinate to the chart, and the chart has a transparent background allowing it to be overlaid over any pretty background required. To zoom into the chart, hold down the right mouse button and move the mouse up or to the right, then to pan around just drag the chart with the left mouse button.

While this chart control is quite basic right now, I intend to add other features as they are required such as databinding to static and dynamic data sources with dynamically giving the chart an animated appearance, pipe shaded bars for bar charts, and other fancy chart primitives. Updates to this control can be found at the Swordfish Charts project page on Sourceforge where I've released it under the BSD license.

Background

Over the years I've moved from ASM/C/DOS to C/Win32 to C++/MFC To C++/GTK/Linux to C#/Windows Forms and now to C#/WPF with WPF being the new User Interface API introduced for Windows Vista. For each GUI toolkit I've generally written my own charting control for displaying data, and here I present to you how I wrote one for WPF that I use for displaying data inside WPF business applications that I am working on.

Using the code

Using the chart involves adding the chart control to your XAML file, and then in the code behind you add lines to the chart control. First of all you will need to add a reference in your project to the Swordfish.WPF.Charts project. Then at the top of your XAML file for where you are placing the control, add the following namespace:

xmlns:SwordfishCharts="clr-namespace:Swordfish.WPF.Charts"

In the body of the XAML file add the chart control like so:

<SwordfishCharts:XYLineChart x:Name="xyLineChart" 
                    RenderTransformOrigin="0.5,0.5"/>

If you want a nice rounded background for the chart control as seen in the screenshot above, wrap the control in a border like this:

<Border x:Name="plotToCopy" BorderBrush="Black" BorderThickness="1" 
        CornerRadius="8" Background="#FFFFF0D0" Margin="0">
    <SwordfishCharts:XYLineChart x:Name="xyLineChart" 
        RenderTransformOrigin="0.5,0.5"/>
</Border>

The chart control has a property called Primitives to which you add ChartPrimitive objects. At the moment a ChartPrimitive represents a line, or a filled polygon. ChartPrimitive objects are rendered in the order that they appear in the Primitives collection. Points are added to a ChartPrimitive using either of these functions:

public void AddPoint(double x, double y);
public void AddPoint(Point point);
public void InsertPoint(Point point, int index);
public void AddSmoothHorizontalBar(double x, double y);

Once all of the required primitives have been added to the chart call XYLineChart.RedrawPlotLines(); to get the chart to recalculate it's display list. The chart control sets the axis limits automatically to show all of the data points.

In the source code download link at the top of this article, there is a class in the Swordfish.WPF.Charts project called TestPage that is implemented in the TestPage.xaml and TestPage.cs files. This test page overlays a clipboard interface over the chart control. The contents of TestPage.xaml can be seen below showing a CopyToClipboard being placed in the same grid cell as the XYLineChart control. Note that the border around the chart control is called "plotToCopy". When copying the chart to the clipboard I want to copy the decorative border as well, so I have labelled it as such.

<Grid x:Class="Swordfish.WPF.Charts.TestPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:SwordfishCharts="clr-namespace:Swordfish.WPF.Charts">
  <Border x:Name="plotToCopy" BorderBrush="Black" BorderThickness="1" 
        CornerRadius="8" Background="#FFFFF0D0" Margin="0">
    <SwordfishCharts:XYLineChart x:Name="xyLineChart" 
        RenderTransformOrigin="0.5,0.5"/>
  </Border>
  <Border HorizontalAlignment="Left" VerticalAlignment="Top" 
        Padding="1,0,1,0" BorderBrush="Black"
        Background="#A0005500" BorderThickness="0" CornerRadius="8">
    <SwordfishCharts:CopyToClipboard x:Name="copyToClipboard"/>
  </Border>
</Grid>

There is a static class called ChartUtilities that contains a function called CopyChartToClipboard that is used for copying the chart. Here's a list of the functions in ChartUtilities which may be of use to you:

/// <summary>
/// Copies the plotToCopy as a bitmap to the clipboard, and copies the
/// chartControl to the clipboard as tab separated values.
/// </summary>
/// <param name="width">Width of the bitmap to be created</param>
/// <param name="height">Height of the bitmap to be created</param>
public static void CopyChartToClipboard(FrameworkElement plotToCopy, 
    XYLineChart chartControl, double width, double height);

/// <summary>
/// Calculates the value as near to the input as possible, 
/// a power of 10 times 1,2, or 5
/// </summary>
/// <param name="optimalValue"> The value to get closest to</param>
/// <returns>The nearest value to the input value</returns>
public static double Closest_1_2_5_Pow10(double optimalValue);

/// <summary>
/// Calculates the closest possible value to the optimalValue passed
/// in, that can be obtained by multiplying one of the numbers in the
/// list by the baseValue to the power of any integer.
/// </summary>
/// <param name="optimalValue">The number to get closest to</param>
/// <param name="numbers">List of numbers to multiply by</param>
/// <param name="baseValue">The base value</param>
public static double ClosestValueInListTimesBaseToInteger
    (double optimalValue, double[] numbers, double baseValue);

/// <summary>
/// Gets the plot rectangle that is required to hold all the
/// lines in the primitive list
/// </summary>
/// <param name="primitiveList"></param>
public static Rect GetPlotRectangle(List<ChartPrimitive> primitiveList);

/// <summary>
/// Gets a nominally oversize rectangle that the plot will be drawn into
/// </summary>
public static Rect GetPlotRectangle(List<ChartPrimitive> 
                    primitiveList, double oversize);

/// <summary>
/// Converts a ChartLine object to a ChartPolygon object that has
/// one edge along the bottom Horizontal base line in the plot.
/// </summary>
public static ChartPrimitive ChartLineToBaseLinedPolygon
                        (ChartPrimitive chartLine);

/// <summary>
/// Takes two lines and creates a polygon between them
/// </summary>
public static ChartPrimitive LineDiffToPolygon
            (ChartPrimitive baseLine, ChartPrimitive topLine);

/// <summary>
/// Adds a set of lines to the chart for test purposes
/// </summary>
public static void AddTestLines(XYLineChart xyLineChart);

/// <summary>
/// Creates a 50% hatch patter for filling a polygon
/// </summary>
public static DrawingBrush CreateHatch50(Color color, Size blockSize);

/// <summary>
/// Copies a Framework Element to the clipboard as a bitmap
/// </summary>
/// <param name="copyTarget">The Framework Element to be copied</param>
/// <param name="width">The width of the bitmap</param>
/// <param name="height">The height of the bitmap</param>
public static void CopyFrameworkElementToClipboard
    (FrameworkElement copyTarget, double width, double height);

/// <summary>
/// Copies a Framework Element to a bitmap
/// </summary>
/// <param name="copyTarget">The Framework Element to be copied</param>
/// <param name="width">The width of the bitmap</param>
/// <param name="height">The height of the bitmap</param>
public static System.Drawing.Bitmap CopyFrameworkElementToBitmap
    (FrameworkElement copyTarget, double width, double height);

Under the Hood

Rendering the Chart

When the chart is rendered, the gridlines are drawn first, and then the chart lines and polygons are drawn on top. The gridlines are selected by picking the granularity required to get close to 100 pixels in the horizontal, and 75 pixels in the vertical (the values of 100 and 75 are set in the XYLineChart constructor). A function called Closest_1_2_5_Pow10 from ChartUtilities picks gridline granularity so that the gridline values are multiples of 1, 2 or 5 times the power of 10 that yields the nearest match. This function is implemented as follows:

double[] numbers = { 1.0, 2.0, 5.0 };
double baseValue = 10.0;
double multiplier = Math.Pow(baseValue, Math.Floor(Math.Log(optimalValue) / 
                            Math.Log(baseValue)));
double minimumDifference = baseValue * baseValue * multiplier;
double closestValue = 0.0;
double minimumNumber = baseValue * baseValue;

foreach (double number in numbers)
{
  double difference = Math.Abs(optimalValue - number * multiplier);
  if (difference < minimumDifference)
  {
    minimumDifference = difference;
    closestValue = number * multiplier;
  }
  if (number < minimumNumber)
  {
    minimumNumber = number;
  }
}

if (Math.Abs(optimalValue - minimumNumber * baseValue * multiplier) <
  Math.Abs(optimalValue - closestValue))
  closestValue = minimumNumber * baseValue * multiplier;

return closestValue;

In actual fact the current implementation of the chart control picks gridlines for the X axis that are multiples of powers of 12 as it has only been used for showing months on the X axis. So the entire implementation for working out the spacing looks like this:

if (maxXY.X != minXY.X)
  scaleX = (double)size.Width / (double)(maxXY.X - minXY.X);
if (maxXY.Y != minXY.Y)
  scaleY = (double)size.Height / (double)(maxXY.Y - minXY.Y);
double optimalSpacingX = optimalGridLineSpacing.X / scaleX;

double spacingX = ChartUtilities.ClosestValueInListTimesBaseToInteger(
  optimalSpacingX, new double[] { 1, 3, 6 }, 12.0);

if (spacingX < 2.0)
  spacingX = ChartUtilities.Closest_1_2_5_Pow10(optimalSpacingX);

double optimalSpacingY = optimalGridLineSpacing.Y / scaleY;
double spacingY = ChartUtilities.Closest_1_2_5_Pow10(optimalSpacingY);

where maxXY and minXY are the minimum and maximum values of all the primitives added to the chart. The gridlines are added to a PathFigure object as line segments as seen below. The boolean parameter in LineSegment just says whether a line should be drawn to that point or not, so the code is moving to the start point, and then drawing to the endpoint.

Point startPoint = new Point(xPos, size.Height);
Point endPoint = new Point(xPos, 0);

pathFigure.Segments.Add(new LineSegment(startPoint, false));
pathFigure.Segments.Add(new LineSegment(endPoint, true));

A PathFigure can be converted to PathGeometry which, as seen below, has a transform attached to it to allow it to be zoomed, panned, or even rotated.

Chart lines and polygons are added to the chart canvas in the XYLineChart.RenderPlotLines method seen below:

protected void RenderPlotLines(Canvas canvas)
{
  canvas.Children.Clear();
  foreach (ChartPrimitive primitive in primitiveList)
  {
    if (primitive.Points.Count > 0)
    {
      Path path = new Path();
      PathGeometry pathGeometry = new PathGeometry();
      pathGeometry.Transform = shapeTransform;

      if (primitive.Filled)
      {
        pathGeometry.AddGeometry(primitive.PathGeometry);
        path.Stroke = null;
        if (primitive.Dashed)
        {
          path.Fill = ChartUtilities.CreateHatch50(primitive.Color, 
                            new Size(2, 2));
        }
        else
          path.Fill = new SolidColorBrush(primitive.Color);
      }
      else
      {
        pathGeometry.AddGeometry(primitive.PathGeometry);
        path.Stroke = new SolidColorBrush(primitive.Color);
        path.StrokeThickness = primitive.LineThickness;
        path.Fill = null;
        if (primitive.Dashed)
          path.StrokeDashArray = new DoubleCollection(new double[] { 2, 2 });
      }
      path.Data = pathGeometry;
      path.Clip = chartClip;
      canvas.Children.Add(path);
    }
  }
}

Inspection of the above code shows that for each primitive, a Path object is added to the canvas. A Path has geometry in it, and to that geometry I attach a transform that I use to implement the pan and zoom. The path is clipped to the chart region so that the geometry isn't drawn outside the chart area when it is zoomed.

Zooming and Panning

The calculations for zooming and panning are done in the PanZoomCalculator class and are normalized to the dimensions of the chart, so a pan distance of 1 will pan the chart 1x the width of the chart which allows the chart to easily retain it's pan and zoom settings when it is resized.

Most of the work is done in the MouseMoved method. Panning is calculated like this:

Pan += PixelsMoved/Zoom/ChartSize 

which in the code looks like this:

if (isPanning)
{
  currentPan.X += (newPosition.X - lastPosition.X) / 
                        currentZoom.X / window.Width;
  currentPan.Y += (newPosition.Y - lastPosition.Y) / 
                        currentZoom.Y / window.Height;
}

Zooming uses a power function so that the zooming retains a linear feel to it as you zoom in and out of the chart. I've found that 1.002 to the power of the number of pixels moved seems to work nicely. So the algorithm looks like this:

Zoom *= Power(1.002, PixelsMoved) 

Zooming is done on the spot that the user right clicked the mouse. Normally the chart would zoom in on it's centre, so to zoom in on where the user clicked the chart needs to be panned as it is zoomed. The amount to pan is calculated like this:

Pan += (ChartSize/2 - ZoomStartPos) * (1/previousZoom - 1/Zoom)/ChartSize 

Which in code looks like this:

if (isZooming)
{
  Point oldZoom = currentZoom;

  currentZoom.X *= Math.Pow(1.002, newPosition.X - lastPosition.X);
  currentZoom.Y *= Math.Pow(1.002, -newPosition.Y + lastPosition.Y);
  currentZoom.X = Math.Max(1, currentZoom.X);
  currentZoom.Y = Math.Max(1, currentZoom.Y);

  currentPan.X +=
    (window.Width * .5 - zoomCentre.X) * (1/oldZoom.X - 1/currentZoom.X) / 
    window.Width;
  currentPan.Y +=
    (-window.Height * .5 - zoomCentre.Y) * (1/oldZoom.Y - 1/currentZoom.Y) / 
    window.Height;
}

Finally I limit the Pan so that the chart isn't panned out of site. I take the amount that the chart is bigger than if it wasn't zoomed, halve it as it can only be panned either way half of the size that it exceeds the normal size, and then I scale it by the zoom factor as the zoom factor is applied to the pan when the transform is calculated. So the algorithm looks like this:

Pan = Min(Pan, (Zoom-1)/2/Zoom) 

Which in code looks like this:

lastPosition = newPosition;

if (isPanning || isZooming)
{
  // Limit Pan
  Point maxPan = new Point();
  maxPan.X = .5*(currentZoom.X - 1) / (currentZoom.X);
  maxPan.Y = .5*(currentZoom.Y - 1) / (currentZoom.Y);
  currentPan.X = Math.Min(maxPan.X, currentPan.X);
  currentPan.X = Math.Max(-maxPan.X, currentPan.X);
  currentPan.Y = Math.Min(maxPan.Y, currentPan.Y);
  currentPan.Y = Math.Max(-maxPan.Y, currentPan.Y);

  if (Double.IsNaN(currentPan.X) || Double.IsNaN(currentPan.Y))
    currentPan = new Point(0f, 0f);
  if (Double.IsNaN(currentZoom.X) || Double.IsNaN(currentZoom.Y))
    currentZoom = new Point(1f, 1f);

  this.OnPanZoomChanged();
}

The Zoom and Pan are then applied to the chart geometry as a transform, which is calculated in the XYLineChart.SetChartTransform method which looks like this:

protected void SetChartTransform(double width, double height)
{
  Rect plotArea = ChartUtilities.GetPlotRectangle(primitiveList, 0.01f);

  minPoint = plotArea.Location;
  minPoint.Offset(-plotArea.Width * panZoomCalculator.Pan.X,
    plotArea.Height * panZoomCalculator.Pan.Y);
  minPoint.Offset(0.5 * plotArea.Width * (1 - 1 / panZoomCalculator.Zoom.X),
    0.5 * plotArea.Height * (1 - 1 / panZoomCalculator.Zoom.Y));

  maxPoint = minPoint;
  maxPoint.Offset(plotArea.Width / panZoomCalculator.Zoom.X,
    plotArea.Height / panZoomCalculator.Zoom.Y);

  Point plotScale = new Point();
  plotScale.X = (width / plotArea.Width) * panZoomCalculator.Zoom.X;
  plotScale.Y = (height / plotArea.Height) * panZoomCalculator.Zoom.Y;

  Matrix shapeMatrix = Matrix.Identity;
  shapeMatrix.Translate(-minPoint.X, -minPoint.Y);
  shapeMatrix.Scale(plotScale.X, plotScale.Y);
  shapeTransform.Matrix = shapeMatrix;
}

Putting the chart coordinates on the cursor

The chart coordinates on the cursor work in 2 modes. When the cursor is near a plot point it displays the coordinates of the point and draws a yellow circle around the point, otherwise the cursor just shows the coordinates of the crosshairs. Finding the closest point is handled by the ClosestPointPicker class which performs an optimized equivalent of a distance test on each transformed point and picks the closest.

Originally the crosshairs and coordinates were rendered to a bitmap that was converted to an icon and then set as a mouse cursor. This worked fine except that it caused a Security Exception if the chart control was used in an online XBAP application. To overcome this, I now turn off the cursor, and then render a software cursor in the Adorner Layer of the Chart Control. The code for displaying the coordinates on the actual mouse cursor can still be found in the MouseCursorCoordinateDrawer class which uses a WPFCursorFromBitmap class that handles removing the old Icon handle from system space everytime it is updated, otherwise you will find the system suddenly running out of space to put GDI objects and you will have to reboot your machine everytime you use the chart control. The Adorner version being used has a little bit of lag, but it's not too bad, and it's what the Adorner layer is for.

The AdornerCursorCoordinateDrawer inherits from Adorner. The adorner layer seems to exist on the chart control only when it's visible, so the AdornerCursorCoordinateDrawer is added to the chart control when it is made visible, and removed when the chart control is made not visible as such:

public XYLineChart()
{
  ...
  adorner = new AdornerCursorCoordinateDrawer
                (clippedPlotCanvas, shapeTransform);
  ...
}

void clippedPlotCanvas_IsVisibleChanged
            (object sender, DependencyPropertyChangedEventArgs e)
{
  if (this.IsVisible && adornerLayer == null)
  {
    adornerLayer = AdornerLayer.GetAdornerLayer(clippedPlotCanvas);
    adornerLayer.Add(adorner);
  }
  else if (adornerLayer != null)
  {
    adornerLayer.Remove(adorner);
    adornerLayer = null;
  }
}

The adorner draws the cursor in the OnRender method. When the cursor is locked onto the nearest point it draws the cursor like this:

// Draw the little circle around the lock point

Point point = elementTransform.Transform(lockPoint);
drawingContext.DrawEllipse(null, new Pen(blackBrush, 3), point, 2.5, 2.5);
drawingContext.DrawEllipse
    (null, new Pen(new SolidColorBrush(Colors.White), 2), point, 2.5, 2.5);

// Draw the big yellow circle

Pen yellowPen = new Pen(new SolidColorBrush(Colors.Yellow), 2);
Pen blackPen = new Pen(blackBrush, 3);
drawingContext.DrawEllipse(null, blackPen, mousePoint, radius, radius);
drawingContext.DrawEllipse(null, yellowPen, mousePoint, radius, radius);

Otherwise it draws the cursor like this:

// Draw the target symbol

Pen blackPen = new Pen(blackBrush, .7);
drawingContext.DrawEllipse(null, blackPen, mousePoint, radius, radius);
drawingContext.DrawLine(blackPen, new Point
                (mousePoint.X - radius * 1.6, mousePoint.Y),
  new Point(mousePoint.X - 2, mousePoint.Y));
drawingContext.DrawLine(blackPen, new Point
                (mousePoint.X + radius * 1.6, mousePoint.Y),
  new Point(mousePoint.X + 2, mousePoint.Y));
drawingContext.DrawLine(blackPen, new Point
                (mousePoint.X, mousePoint.Y - radius * 1.6),
  new Point(mousePoint.X, mousePoint.Y - 2));
drawingContext.DrawLine(blackPen, new Point
                (mousePoint.X, mousePoint.Y + radius * 1.6),
  new Point(mousePoint.X, mousePoint.Y + 2));

When adding text to the cursor, I first need to work out how many decimal places to use to display a value accurate down to one pixel. This is done by looking at the Log10 of the distance between 2 adjacent pixels after they have been transformed to the chart coordinates. The code for this looks like this:

// Works out the number of decimal places required to show the 
// difference between
// 2 pixels. E.g. if pixels are .1 apart then use 2 places etc
Rect rect = inverse.TransformBounds(new Rect(0, 0, 1, 1));

int xFigures = Math.Max(1,(int)(Math.Ceiling(-Math.Log10(rect.Width)) + .1));
int yFigures = Math.Max(1,(int)(Math.Ceiling(-Math.Log10(rect.Height)) + .1));

// Number of significant figures for the x coordinate
string xFormat = "#0." + new string('#', xFigures);
/// Number of significant figures for the y coordinate
string yFormat = "#0." + new string('#', yFigures);

The coordinate text is then drawn in a box like this:

string coordinateText = coordinate.X.ToString(xFormat) + "," + 
coordinate.Y.ToString(yFormat);
drawingContext.PushTransform(new ScaleTransform(1, -1));
FormattedText formattedText = new FormattedText
    (coordinateText, CultureInfo.CurrentCulture,
    FlowDirection.LeftToRight, new Typeface("Arial"), 10, blackBrush);
Pen textBoxPen = new Pen(new SolidColorBrush
                (Color.FromArgb(127, 255, 255, 255)), 1);

Rect textBoxRect = new Rect
    (new Point(mousePoint.X + radius * .7, -mousePoint.Y + radius * .7),
    new Size(formattedText.Width, formattedText.Height));
double diff = textBoxRect.Right + 3 - 
        ((FrameworkElement)AdornedElement).ActualWidth;

if (diff > 0)
  textBoxRect.Location = new Point(textBoxRect.Left - diff, textBoxRect.Top);

drawingContext.DrawRectangle(textBoxPen.Brush, textBoxPen, textBoxRect);
drawingContext.DrawText(formattedText, textBoxRect.Location);
drawingContext.Pop();

Copying to the clipboard

Currently the chart needs to be copied to the clipboard as a bitmap if it is to be pasted into any other app. I would anticipate that sometime in the future the chart could be copied as a XAML package, but at the moment nothing supports this clipboard format, which is a shame, because I would really like to be able to paste vector graphics objects into Microsoft Word 2007 documents. I tried going down the convert to VML route but I haven't finished that yet as there are some issues. So currently the chart is converted to a bitmap using the ChartUtilities.CopyFrameworkElementToBitmap method which looks like this:

public static System.Drawing.Bitmap CopyFrameworkElementToBitmap
    (FrameworkElement copyTarget, double width, double height)
{
  if (copyTarget == null)
    return new System.Drawing.Bitmap((int)width, (int)height);

  System.Drawing.Bitmap bitmap;
  // Convert from a WPF Bitmap Source to a Win32 Bitmap
  using (MemoryStream outStream = 
    CopyFrameworkElementToMemoryStream(copyTarget, width, height,
    new BmpBitmapEncoder()))
  {
    bitmap = new System.Drawing.Bitmap(outStream);
  }

  return bitmap;
}

That's not very exciting, is it. What it is doing is it is using ChartUtilities.CopyFrameworkElementToMemoryStream to save the chart control to a bitmap in a memory stream, and then it is loading that memory stream back into a System.Drawing.Bitmap. The CopyFrameworkElementToMemoryStream method is below. Note that it changes the DPI of the render target as a way of scaling the chart to the bitmap.

public static MemoryStream CopyFrameworkElementToMemoryStream
    (FrameworkElement copyTarget, double width, double height, 
    BitmapEncoder enc)
{
  // Store the Frameworks current layout transform, as this will be 
  // restored later
  Transform storedTransform = copyTarget.LayoutTransform;

  // Set the layout transform to unity to get the nominal width and height
  copyTarget.LayoutTransform = new ScaleTransform(1, 1);
  copyTarget.UpdateLayout();

  double baseHeight = copyTarget.ActualHeight;
  double baseWidth = copyTarget.ActualWidth;

  // Now scale the layout to fit the bitmap
  copyTarget.LayoutTransform =
    new ScaleTransform(baseWidth / width, baseHeight / height);
  copyTarget.UpdateLayout();

  // Render to a Bitmap Source, note that the DPI is changed for the
  // render target as a way of scaling the FrameworkElement
  RenderTargetBitmap rtb = new RenderTargetBitmap(
    (int)width,
    (int)height,
    96d * width / baseWidth,
    96d * height / baseHeight,
    PixelFormats.Default);

  rtb.Render(copyTarget);

  // Convert from a WPF Bitmap Source to a Win32 Bitmap
  MemoryStream outStream = new MemoryStream();
  enc.Frames.Add(BitmapFrame.Create(rtb));
  enc.Save(outStream);
  // Restore the Framework Element to it's previous state
  copyTarget.LayoutTransform = storedTransform;
  copyTarget.UpdateLayout();

  return outStream;
}

Points of Interest

I had a problem with the grid lines flickering when they were recalculated after the pan or zoom changed. This was fixed by adding them to the chart inside the MeasureOverride method.

I've found that it looks nice to add a filled background to lines added to the chart. So that lines aren't covered up by the polygons I add all the filled polygons first, and then add the lines on top.

History

  • 10th January 2007 - Initial article
  • 11th January 2007 - Fixed a bug found by Josh Smith where the adorner cursor wasn't being removed when the mouse went off the control during a pan
  • 10th December 2012 - Major changes so called it version 2. Added bar charts, axis overrides, zoom with mouse wheel, and an override library that uses Direct2D to render the chart.

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication

About the Author

John Stewien
Founder Cheesy Design
Taiwan Taiwan
Member
John graduated from the University of South Australia in 1997 with a Bachelor of Electronic Engineering Degree, and since then he has worked on hardware and software in many fields including Aerospace, Defence, and Medical giving him over 10 of years experience in C++ and C# programming. In 2009 John Started his own contracting company doing business between Taiwan and Australia.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
Questionhow to add referencememberMember 97236682 Jan '13 - 7:27 
QuestionX/Y axis resizable? [modified]membermashhur9 Dec '12 - 15:46 
AnswerRe: X/Y axis resizable?memberJohn Stewien10 Dec '12 - 0:10 
Hi mashhur,
 
It's been 3 years since I last updated this project on the net, but I did resurect it recently for a customer and made some changes. I was going to tidy up the changes before pushlishing them anywhere but the way things are going I'm going to be busy with work until at least August, so as my changes could help you I've decieded to upload a new version which currently can be found Here[^]. I've called it version 2.0 because there are significant changes.
 
The main thing that you need to change is that things like ChartPrimitiveXY are obtained from a factory pattern on the control, i.e ChartControl.CreateXY(), I'll explain why I've made this change below.
 
If you download source code archive swordfishchart-2.0.zip from the above link you will find the original chart control inside the Charts directory. I've changed the chart control so now that it can have different renderers, and hence requires factory methods for creating the primitives. To demonstrate this, inside the directory ChartsD2D is a version that uses Direct2D to hardware accelerate the rendering of the control. There are also has 3 helper libraries for interfacing to DirectX 10.
 
The solution file included loads all of these projects, so you can try the pure C# Sworsfish.Charts.NET, or the hardware accelerated Swordfish.NET.ChartsD2D. The chart control library compiles to an application that you can run directly, or reference from another application.
 
This code is all fairly new, so I hope there aren't any major problems with it. I'll also submit the new source archive to Code Project which will take them a day or 2 I guess to approve. I don't know when I'll get a chance to update the text of this article.
 
For the features you wanted:
- New version includes zoom on the mouse wheel
- New version has bar charts
- New version lets you oversize the chart rendering area by setting the Oversize property.
- New version has the ability to overide the x/y labels:
You pass a delegate/Func to XAxisLabelGenerator property or the YAxisLabelGenerator. This will pass you the x/y location of the grid line, and you return the string. The default is currently this x => x.ToString();

Hope this version works as well for you as the original version has so far.
 
- John
GeneralRe: X/Y axis resizable?membermashhur11 Dec '12 - 17:54 
GeneralMy vote of 5 [modified]membermashhur9 Dec '12 - 15:46 
QuestionThanks... F1 F1 F1memberamity200115 Jul '12 - 20:26 
AnswerRe: Thanks... F1 F1 F1memberJohn Stewien16 Jul '12 - 0:52 
QuestionHow to add?memberSam Duke24 May '12 - 3:13 
AnswerRe: How to add?memberJohn Stewien24 May '12 - 14:54 
GeneralRe: How to add?memberSam Duke27 May '12 - 4:46 
GeneralRe: How to add?memberSam Duke27 May '12 - 5:15 
GeneralRe: How to add?memberJohn Stewien27 May '12 - 15:03 
GeneralRe: How to add?memberSam Duke28 May '12 - 10:30 
QuestionPath geometry disappears on mouse movememberHari.net10 May '12 - 21:06 
GeneralMy vote of 5membermanoj kumar choubey20 Feb '12 - 0:19 
QuestionReducing Level of DetailmemberMember 847057314 Dec '11 - 22:51 
AnswerRe: Reducing Level of DetailmemberJohn Stewien14 Dec '11 - 23:13 
GeneralOne of the best.memberJasonD_S17 May '11 - 4:01 
GeneralRe: One of the best.memberJohn Stewien18 May '11 - 21:29 
GeneralNice articlememberanishkannan25 Apr '11 - 23:29 
GeneralRe: Nice articlememberJohn Stewien25 Apr '11 - 23:57 
GeneralnicememberCIDev23 Dec '10 - 5:34 
GeneralRe: nicememberJohn Stewien2 Jan '11 - 1:45 
GeneralAwesomememberDavid Roh11 May '10 - 23:14 
GeneralRe: AwesomememberJohn Stewien12 May '10 - 0:49 
GeneralLogarithmic Scalemembersentmandrake15 Oct '09 - 5:53 
GeneralRe: Logarithmic ScalememberJohn Stewien19 Oct '09 - 3:06 
GeneralRe: Logarithmic ScalememberHari.net10 May '12 - 20:51 
Generalthanksmembergj_code31 Aug '09 - 17:09 
GeneralRe: thanksmemberJohn Stewien1 Sep '09 - 1:10 
GeneralValue of each line on x and y axismembertinatran306 Aug '09 - 12:32 
GeneralRe: Value of each line on x and y axismemberJohn Stewien6 Aug '09 - 18:40 
Questiongraphical artefact [modified]membervincent7930 Jun '09 - 0:10 
AnswerRe: graphical artefactmemberJohn Stewien30 Jun '09 - 0:17 
Questionhelpmemberaprr8 Jun '09 - 5:30 
AnswerRe: helpmemberJohn Stewien9 Jun '09 - 20:22 
GeneralRe: helpmemberaprr15 Jun '09 - 23:53 
GeneralMemory leak and how to fix it.memberStefan Sch26 Feb '09 - 1:53 
GeneralRe: Memory leak and how to fix it.memberJohn Stewien3 Mar '09 - 23:09 
QuestionLabel the points?memberfastas24 Feb '09 - 21:55 
AnswerRe: Label the points?memberJohn Stewien25 Feb '09 - 3:47 
QuestionIs MeasureOverride the best way?memberNickB15 Jan '09 - 1:32 
AnswerRe: Is MeasureOverride the best way?memberJohn Stewien16 Jan '09 - 2:43 
QuestionIs this still being developed?memberdakehurst14 Aug '08 - 0:32 
AnswerRe: Is this still being developed?memberJohn Stewien14 Aug '08 - 2:14 
GeneralDrawing on the grid cellsmemberseccom5 Aug '08 - 23:32 
GeneralRe: Drawing on the grid cellsmemberJohn Stewien7 Aug '08 - 0:35 
GeneralRe: Drawing on the grid cellsmemberseccom10 Aug '08 - 21:27 
GeneralcopyToClipboard throwing exception [modified]memberklawonk5 Aug '08 - 5:47 
GeneralQuite interestingmembernewspicy15 Jul '08 - 8:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130516.1 | Last Updated 10 Dec 2012
Article Copyright 2007 by John Stewien
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid