Click here to Skip to main content
13,863,424 members
Click here to Skip to main content
Add your own
alternative version

Stats

2.6K views
116 downloads
4 bookmarked
Posted 10 Feb 2019
Licenced MIT

MSChart Extension - Zoom and Pan Control Version 2.2.0

, 10 Feb 2019
Rate this:
Please Sign up or sign in to vote.
MSChart Extension 2.2.0 Update with new features

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 limitation 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 type 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 take consideration both X and X2 Axis.

 

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, 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 belongs 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.

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 open, 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 item 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 eithe ChartToolSelect or ChartToolSelect2 to identify which cursor is activate 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 much interesting 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.

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 assending 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)
        {
            //Serch 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 find 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 point 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 with 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 as follow:

  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 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.

License

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

Share

About the Author

Code Artist
Technical Lead
Malaysia Malaysia
Official Page: www.codearteng.com

You may also be interested in...

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web02 | 2.8.190214.1 | Last Updated 10 Feb 2019
Article Copyright 2019 by Code Artist
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid