Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

MSChart Extension - Zoom and Pan Control Version 2.2.0

5.00/5 (1 vote)
10 Feb 2019MIT4 min read 12K   1.1K  
MSChart Extension 2.2.0 Update with new features
MSChart Extension, an extension class for Microsoft Chart (MSChart) control in Visual Studio for WinForms applications is a tool published to overcome some limitations of the original MSChart.

NOTE: Binary and source file updated to version 2.2.4 with several bug fixes.

Introduction

MSChart Extension is an extension class for Microsoft Chart (MSChart) control in Visual Studio for WinForms applications. The tool was first published on July 2012 with the intention to overcome some of the limitations from the original MSChart. If you are new to MSChart Extension, we recommend you to read the previous articles listed below first.

These articles including this is created as technical sharing as well as documentation for this library.

Known Issue

  • MSChart Extensions is designed for chart type with X and Y Axis, the extension method will not work with some of the chart types such as Radar and Pie.
  • It is known that zoom does not works properly with Log Axis. We decided to disable extensions functions for chart with LOG Axis.
  • Date Time Axis - Zoom may not behave correctly for chart where XAxis values in DateTime format.

What's New

Scroll Zoom

Zoom along X-Axis can now be done by pressing the CTRL + ALT Key while moving the Mouse wheel to zoom in / out X-Axis of the chart. This is the simplified version of the Mouser Wheel Zoom function with Y-Axis remain untouched. The code takes into consideration both X and X2 Axis.

C#
private static void ChartControl_MouseWheel(object sender, MouseEventArgs e)
{
    //Some codes...

    if (Form.ModifierKeys == (Keys.Alt | Keys.Control))
    {
        //Mouse Wheel Zoom: X Axis
        ScaleViewZoom(ptrChartArea.AxisX, e.Delta);
        ScaleViewZoom(ptrChartArea.AxisX2, e.Delta);
    }
    
    //More codes...
}

Series Selection for Chart Cursor

From Version 2.2.0 onward, the user can now select which series to use for chart cursor 1 and 2. Prior to Version 2.2.0, both chart cursors is only works with primary X-Axis and Y-Axis. An additional drop down menu is added to "Select - Cursor n" menu to let user to choose which series to use by chart cursor. This drop down menu will not exist if the selected chart area contains only one axis.

The additional drop down menu is created in context menu opening event.
Drop down menu for "Select - Cursor n" menu is cleared. Next, we filter out series which belong to selected chart area using foreach loop. If series count is only 1, drop down menu is not created and chart cursor will always use the one and only one series in chart. Otherwise, a drop down menu is created for user to select which series to use for each chart cursor. On top of that, a Clear Cursors... function is added to remove all cursors from chart.

C#
private static void ChartContext_Opening(object sender, CancelEventArgs e)
{
    //Some code removed...

    ptrChartData.ChartToolSelect.DropDownItems.Clear();
    ptrChartData.ChartToolSelect2.DropDownItems.Clear();

    //Add Series to Context Menu
    List<Series> ChartSeries = new List<Series>();
    SeriesCollection chartSeries = ((Chart)menuStrip.SourceControl).Series;
    if (ptrChartData.ActiveChartArea != null)
    {
        ToolStripSeparator separator = new ToolStripSeparator();
        menuStrip.Items.Add(separator);
        separator.Tag = "Series";

        foreach (Series ptrSeries in chartSeries)
        {
            if (ptrSeries.ChartArea != ptrChartData.ActiveChartArea.Name) continue;

            ChartSeries.Add(ptrSeries);
            if (ptrChartData.Option.ContextMenuAllowToHideSeries) //Option to show / 
                                                                  //hide series controls
            {
                ToolStripItem ptrItem = menuStrip.Items.Add(ptrSeries.Name);
                ToolStripMenuItem ptrMenuItem = (ToolStripMenuItem)ptrItem;
                ptrMenuItem.Checked = ptrSeries.Enabled;
                ptrItem.Tag = "Series";
            }
        }
    
        if (ChartSeries.Count == 1)
        {
            ptrChartData.Cursor1.SelectedChartSeries = ChartSeries[0];
            ptrChartData.Cursor2.SelectedChartSeries = ChartSeries[0];
        }
        else if (chartSeries.Count > 1)
        {
            //Default cursor to first chart series if previous selected series not exist.
            if (!ChartSeries.Contains(ptrChartData.Cursor1.SelectedChartSeries)) 
                ptrChartData.Cursor1.SelectedChartSeries = ChartSeries[0];
            if (!ChartSeries.Contains(ptrChartData.Cursor2.SelectedChartSeries)) 
                ptrChartData.Cursor2.SelectedChartSeries = ChartSeries[0];

            //Populate Context Menu for user to select series for each Chart Cursor.
            if (ChartSeries.Count > 1)
            {
                foreach (Series s in ChartSeries)
                {
                    //Cursor 1
                    ToolStripMenuItem ptrItem = 
                    ptrChartData.ChartToolSelect.DropDownItems.Add(s.Name) 
                                 as ToolStripMenuItem;
                    ptrItem.Tag = ptrChartData.ChartToolSelect;
                    ptrItem.Click += ChartToolSelect_SeriesChanged;
                    if (s == ptrChartData.Cursor1.SelectedChartSeries) ptrItem.Checked = true;

                    //Cursor 2
                    ptrItem = ptrChartData.ChartToolSelect2.DropDownItems.Add(s.Name) 
                              as ToolStripMenuItem;
                    ptrItem.Tag = ptrChartData.ChartToolSelect2;
                    ptrItem.Click += ChartToolSelect_SeriesChanged;
                    if (s == ptrChartData.Cursor2.SelectedChartSeries) ptrItem.Checked = true;
                }
            }
        }
    }//Active Chart Area

    //Some Code Removed...
}

Since the drop down menu is recreated each time chart context menu is opened, it's important to remember the last selected series for each chart cursor somewhere else. A new property named SelectedChartSeries is added to ChartCursor class to store the last selected cursor.

All the menu items in context menu will trigger the event ChartContext_ItemClicked when clicked. However, the drop down menu which we added to chart cursors does not trigger this event. Hence, we subscribed to the Click event for this newly added menu item. The Tag property of the series menu added to chart cursor is set to either ChartToolSelect or ChartToolSelect2 to identify which cursor is activated when serving the click event.

Selecting series for selected chart cursor is not solely to identify which X and Y Axis to use, but also preparation for the next feature.

Snap Cursor to Nearest Data Point

The chart data is always very interesting to compare to those empty area of the chart. Hence, it's important for the chart cursor to snap to the nearest data point of the selected series for more precise analysis. This feature is first introduced in this Version 2.2.0. Should anyone prefer the old way of how chart cursor works, simply set the SnapCursorToData in ChartOption to set chart cursor free again.

This search function is implemented in SnapToNearestData function.

C#
private static void SnapToNearestData
(object sender, Series series, Axis xAxis, Axis yAxis, MouseEventArgs e,
        ref double XResult, ref double YResult)
    {
        XResult = YResult = Double.MaxValue;

        Chart ptrChart = (Chart)sender;
        ChartData ptrChartData = ChartTool[ptrChart];
        ChartArea ptrChartArea = ChartTool[ptrChart].ActiveChartArea;

        double xMin = xAxis.Minimum;
        double xMax = xAxis.Maximum;

        //Mouser Pointer Value
        double xTarget = xAxis.PixelPositionToValue(e.Location.X);
        double yTarget = yAxis.PixelPositionToValue(e.Location.Y);

        //Sort data point ascending by X-Values
        DataPoint[] datas = series.Points.OrderBy(x => x.XValue).ToArray();

        //Get nearest data points
        int iLower, iUpper;
        iUpper = iLower = 0;
        int estIndex = (int)(datas.Length * (xTarget - xMin) / (xMax - xMin));

        //iLower --> XValue < xTarget
        //iUpper --> XValue > xTarget
        if (datas[estIndex].XValue > xTarget)
        {
            //Search Down
            for (int x = estIndex; x > 0; x--)
            {
                if (datas[x].XValue <= xTarget)
                {
                    iLower = x;
                    iUpper = x + 1;
                    break;
                }
            }
        }
        else //datas[estIndex].XValue < xTarget
        {
            //Search Up
            for (int x = estIndex; x < datas.Length; x++)
            {
                if (datas[x].XValue >= xTarget)
                {
                    iUpper = x;
                    iLower = x - 1;
                    break;
                }
            }
        }

        //Distance = x^2 + y^2
        double distLower = Math.Pow(datas[iLower].XValue - xTarget, 2) + 
                           Math.Pow(datas[iLower].YValues[0] - yTarget, 2);
        double distUpper = Math.Pow(datas[iUpper].XValue - xTarget, 2) + 
                           Math.Pow(datas[iUpper].YValues[0] - yTarget, 2);

        if (distLower > distUpper)
        {
            XResult = datas[iUpper].XValue;
            YResult = datas[iUpper].YValues[0];
        }
        else
        {
            XResult = datas[iLower].XValue;
            YResult = datas[iLower].YValues[0];
        }
    }

The nearest data point to cursor is found using the following method:

  1. Sort data points (data) by X value.
  2. Assume that data is evenly distributed along X-Axis, estimate the nearest data index based on x value of cursor, where:
    index = Data_Count x ( Cursor_X_Value - X_Minimum ) / ( X_Maximum - X_Minimum)
  3. Find 2 data points where 1 with x value less than cursor's x value and another with x value greater than cursor's x value.
  4. Calculate distance of each point to chart cursor where distance d = sqrt(dx^2 + dy^2)
    Note: We omitted the square root function since we are not interested in the actual distance.
  5. Data with shortest distance is the nearest data point to cursor.
  6. Draw cursor based on the X and Y value of selected data point.

Some More New Functions

Some other minor changes included together with this release are as follows:

  1. Added function IsZoomed to check if any of the axis is zoomed
  2. Added function RemoveAnnotation to remove annotation by name
  3. Exposed function SetChartControlState as public for changing chart control state programmatically

Bug Fixes

  1. CursorLineWidth and CursorDashStyle does not effect cursor property
  2. ZoomChanged event does not trigger on Mouse Scroll function

Project Repository

This is an active project where the source code is available in GitHub, while the library is released as NuGet Package which can be easily included in Visual Studio project via NuGet Package Manager.

History

  • 10th February, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License