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.
private static void ChartControl_MouseWheel(object sender, MouseEventArgs e)
{
if (Form.ModifierKeys == (Keys.Alt | Keys.Control))
{
ScaleViewZoom(ptrChartArea.AxisX, e.Delta);
ScaleViewZoom(ptrChartArea.AxisX2, e.Delta);
}
}
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.
private static void ChartContext_Opening(object sender, CancelEventArgs e)
{
ptrChartData.ChartToolSelect.DropDownItems.Clear();
ptrChartData.ChartToolSelect2.DropDownItems.Clear();
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)
{
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)
{
if (!ChartSeries.Contains(ptrChartData.Cursor1.SelectedChartSeries))
ptrChartData.Cursor1.SelectedChartSeries = ChartSeries[0];
if (!ChartSeries.Contains(ptrChartData.Cursor2.SelectedChartSeries))
ptrChartData.Cursor2.SelectedChartSeries = ChartSeries[0];
if (ChartSeries.Count > 1)
{
foreach (Series s in ChartSeries)
{
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;
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;
}
}
}
}
}
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.
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;
double xTarget = xAxis.PixelPositionToValue(e.Location.X);
double yTarget = yAxis.PixelPositionToValue(e.Location.Y);
DataPoint[] datas = series.Points.OrderBy(x => x.XValue).ToArray();
int iLower, iUpper;
iUpper = iLower = 0;
int estIndex = (int)(datas.Length * (xTarget - xMin) / (xMax - xMin));
if (datas[estIndex].XValue > xTarget)
{
for (int x = estIndex; x > 0; x--)
{
if (datas[x].XValue <= xTarget)
{
iLower = x;
iUpper = x + 1;
break;
}
}
}
else
{
for (int x = estIndex; x < datas.Length; x++)
{
if (datas[x].XValue >= xTarget)
{
iUpper = x;
iLower = x - 1;
break;
}
}
}
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:
- Sort data points (data) by X value.
- 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)
- 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.
- 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. - Data with shortest distance is the nearest data point to cursor.
- 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:
- Added function
IsZoomed
to check if any of the axis is zoomed - Added function
RemoveAnnotation
to remove annotation by name - Exposed function
SetChartControlState
as public
for changing chart control state programmatically
Bug Fixes
CursorLineWidth
and CursorDashStyle
does not effect cursor property 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