1

I have a few problems trying to tame the x axis of my mschart.

I use the chart to plot multiple histograms simultaneously, and as you can see my x scale is a mess (see image below). My main question here is: how can I make it look as clean as the y axis. What I mean by "clean" is:

  • Make the "zero value" appear in the scale.
  • Put values that make sense for human readability, I mean, not in a format such as {0.000000000000}.

All the solutions I was able to find were some sort of DIY approach that doesn't work in all the variety of use cases that I am considering (or better: I am not able to make it work). My intuition says: "mschart is able to do it perfectly in the case of the Y axis. It must be also able to do it for the X axis!".

Edit: Sorry, I don't have enough reputation to post images. Here it is the graph in a imgur link =>

https://i.stack.imgur.com/iNjhg.png

On a side note, and given that as you can see in the graph almost all my values are concentrated around x=0, I wonder if there is any possibility to create a logarithmic scale for x... although I am quite aware that negative values are a problem, and 0 should be -Infinite.

Of course I could choose a better scale, but what I am trying to accomplish here is to create plenty of graphs automatically and not worry about them one by one. My solution was to cut a certain % of energy from the top and the bottom of the x axis (something like "start at min_x, go up, and once you have reached the 10% of the energy as a sum, this will be the new min_x. Same thing from max_x downwards."), but this solution still leaves a narrow spike around zero. I could go with higher values of cut energy, but I am afraid it wouldn't work in all cases...

Any hints? Thank you very much.

Edit2: this is an example of the type of series of this graph. As it is an histogram computed for N bins between x_min and x_max, the values of X are quite a mess.

-67.7591400146485,0
-66.0651615142823,0
-64.3711830139161,0
-62.6772045135498,0
-60.9832260131836,0
-59.2892475128174,0
-57.5952690124512,0
-55.901290512085,0
-54.2073120117188,0
-52.5133335113526,0
-50.8193550109863,0
-49.1253765106201,0
-47.4313980102539,0
-45.7374195098877,0
-44.0434410095215,0
-42.3494625091553,0
-40.6554840087891,0
-38.9615055084228,0
-37.2675270080566,0
-35.5735485076904,0
-33.8795700073242,0
-32.185591506958,0
-30.4916130065918,0
-28.7976345062256,0.000405350628293474
-27.1036560058594,0
-25.4096775054932,0
-23.7156990051269,0
-22.0217205047607,0.000405350628293474
-20.3277420043945,0.000810701256586948
-18.6337635040283,0.000810701256586948
-16.9397850036621,0.000810701256586948
-15.2458065032959,0.0016214025131739
-13.5518280029297,0.00121605188488042
-11.8578495025635,0.00283745439805432
-10.1638710021973,0.00364815565464126
-8.46989250183105,0.00405350628293474
-6.77591400146484,0.0105391163356303
-5.08193550109863,0.0121605188488042
-3.38795700073241,0.0186461289014998
-1.6939785003662,0.0283745439805432
7.54951656745106E-15,0.835022294284556
1.69397850036622,0.0413457640859343
3.38795700073243,0.0206728820429672
5.08193550109864,0.00608025942440211
6.77591400146485,0.00486420753952169
8.46989250183106,0.000810701256586948
10.1638710021973,0.000810701256586948
11.8578495025635,0.000405350628293474
13.5518280029297,0.00243210376976084
15.2458065032959,0.000810701256586948
16.9397850036621,0.000405350628293474
18.6337635040283,0
20.3277420043945,0
22.0217205047607,0
23.715699005127,0
25.4096775054932,0
27.1036560058594,0
28.7976345062256,0
30.4916130065918,0
32.185591506958,0

Edit3: so I found a solution that mixes this post and this other post.

Dim bestGuessInterval As Double = (maxGraphValue - minGraphValue) / numberOfPointsInGraph

newChartArea.AxisX.Interval = RoundToClosest(bestGuessInterval)

'It uses the custom function "RoundToClosest":

Private Function RoundToClosest(ByVal value As Double) As Double
    Dim digits As Integer = NumberOfZerosAfterDecimalPoint(value)        
    Return Math.Round(value, digits)
End Function

Private Function NumberOfZerosAfterDecimalPoint(ByVal value As Double) As Integer
    Dim numberAsString As String = value.ToString()        
    Dim charCounter As Integer = 0
    Dim digitsAfterPoint As Boolean = False
    For Each character As String In numberAsString
        If character = "." Then
            digitsAfterPoint = True
            charCounter += 1
        Else
            If CDbl(character) <> 0 Then
                Return charCounter
            Else
                If digitsAfterPoint Then
                    charCounter += 1
                End If
            End If
        End If
    Next
End Function
Community
  • 1
  • 1
Xavier Peña
  • 7,399
  • 9
  • 57
  • 99
  • OK, so... about the second question: I found that in mschart you can in fact set `myChartArea.AxisX.LogarithmBase = True`. Now I have to figure out how to deal with 0 and negative values so the graph still makes sense (I can not simply cut values<=0, the graph would be incomplete. Separate it in two different graphs? Not an optimal solution either...) – Xavier Peña Apr 01 '14 at 14:02
  • Regarding the second problem once again: I took a different approach. I select an automatic binWidth `Dim automaticBinWidth As Double = 3.49 * sigma * Math.Pow(totalNumberOfValues, -1 / 3)` (info [here](http://www.fmrib.ox.ac.uk/analysis/techrep/tr00mj2/tr00mj2/node24.html)) and I cut the max-min of the histogram at [-2*sigma, 2*sigma]. This sets a reasonable zoom on the interesting part of the histogram. Although I think I will still try to combine it with the x log scale for better accuracy. – Xavier Peña Apr 02 '14 at 08:12

1 Answers1

0

You want to set the Interval of the chart area. If you can read C#, check out the code below.

Area.AxisY.Interval = p_axisYInterval;
Area.AxisX.Interval = p_axisXInterval;

Here's a demo app

Create a new WinForms project in c#. Add a chart control named chart1 (default name) and a button named button1 (also default name). Double-click the button to wire up click-event for it.

Then copy-paste the entire code block below and run it. Click the button to add data to the charts and step/debug to see what/where things happens.

I just ripped 99% of this code out of a project I already had, so it's a bit messy, but it gets the point across.

EDIT

Updated my sample to do XY series instead of fixed X values and hardcoded some stuff just to see how it behaved.

Look at method where I have this code block shown below and change the bool flag to true or false and run it to see the change in behaviour. Basically MSChart will happily show 0 if the Maximum and Minimum are symmetric or if 0 happens to be one of the values hit by the Interval (ie. if Min = -10.0, Max = 5.0 and Interval = 1.0, then it'll show -10, -9, -8... 0, 1, 2, ... 5).

I hardcoded these values, but you will of course want to get them programmatically. That however is an exercise I leave to you. Enjoy!

// Make X-Interval Clean numbers
if (flag)
{
    // Play with these number, the hard part I suppose is calculating them programmatically.
    m_chart.ChartAreas[p_chartArea].AxisX.Minimum = -2;
    m_chart.ChartAreas[p_chartArea].AxisX.Interval = 0.5;
    m_chart.ChartAreas[p_chartArea].AxisX.Maximum = 1;
}
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
using Common.Extensions;
using Common.FluentValidation;
using InspectionStation.Classes.Adapters;
using InspectionStation.Classes.Components;
using InspectionStation.Interfaces.IComponents;
using System.Linq;
using System.Text;

namespace WindowsFormsApplication2
{
    public partial class Form1 : Form
    {
        public IScanResultsDisplay Single_Point_Lasers_Display { get; private set; }
        public Form1()
        {
            InitializeComponent();
            Single_Point_Lasers_Display = new ChartControl_To_IScanResultsDisplay_Adapter(this.chart1, Color.Silver);
        }

        int Count = 0;
        private void button1_Click(object sender, EventArgs e)
        {
            Console.WriteLine("Generate_new_graph_data");

            try
            {
                var foo = new double[720];
                var bar = new double[720];

                for (int i = 0; i < foo.Length; i++)
                {
                   foo [i] = (i - 460) / 567.5432;
                   bar[i] = Math.Sin((i + Count++ * Count++) / (float)Count++ / i);
                }

                Single_Point_Lasers_Display
                    .AddSeries("foo", 0, ScanKey.Inside_Scan, foo.ToList(), bar.ToList());

            }
            catch (Exception ex)
            {
                var sb = new StringBuilder();

                sb.AppendFormat("Exception: \"{0}\"", ex.GetType().FullName).AppendLine();
                sb.AppendFormat("Message: \"{0}\"", ex.Message).AppendLine();
                sb.AppendFormat("TargetSite: \"{0}\"", ex.TargetSite).AppendLine();
                sb.AppendFormat("Source: \"{0}\"", ex.Source).AppendLine();
                sb.AppendFormat("Stack_Trace: \"{0}\"", ex.StackTrace);

                Console.WriteLine(sb.ToString());
            }
        }
    }
}

namespace InspectionStation.Classes.Components
{
    public static class ScanKey
    {
        public const string Outside_Scan = "Outside Scan";
        public const string Inside_Scan = "Inside Scan";
    }
}

namespace InspectionStation.Interfaces.IComponents
{
    public interface IScanResultsDisplay
    {
        // Properties
        List<Color> Pallete { get; set; }

        // Methodsvoid
        void AddSeries(string p_seriesName, int p_groupID, string p_chartArea, List<double> p_seriesXData, List<double> p_seriesYData, double p_stripLineValue = default(double));
        void Clear();
    }
}

namespace InspectionStation.Classes.Adapters
{
    public class ChartControl_To_IScanResultsDisplay_Adapter : IScanResultsDisplay
    {
        // http://en.wikipedia.org/wiki/Adapter_pattern
        // Fields
        private Chart m_chart;
        private Dictionary<int, Tuple<List<Series>, List<HorizontalLineAnnotation>>> m_displayedSeries;

        // Properties
        public List<Color> Pallete { get; set; }

        // Constructor
        public ChartControl_To_IScanResultsDisplay_Adapter(Chart p_chart, Color p_lineColour, double p_axisYInterval = 0, double p_axisXInterval = 0)
        {
            m_chart = p_chart;
            var h = m_chart.Handle;

            m_displayedSeries = new Dictionary<int, Tuple<List<Series>, List<HorizontalLineAnnotation>>>();
            Pallete = new List<Color>() { Color.Black };

            m_chart.Series.Clear();
            m_chart.ChartAreas.Clear();
            m_chart.Legends.Clear();
            m_chart.ChartAreas.Add(ScanKey.Outside_Scan);
            m_chart.ChartAreas.Add(ScanKey.Inside_Scan);

            m_chart.ChartAreas[ScanKey.Outside_Scan].Position.X = 0;
            m_chart.ChartAreas[ScanKey.Outside_Scan].Position.Y = 0;
            m_chart.ChartAreas[ScanKey.Outside_Scan].Position.Width = 100;
            m_chart.ChartAreas[ScanKey.Outside_Scan].Position.Height = 50;

            m_chart.ChartAreas[ScanKey.Inside_Scan].Position.X = 0;
            m_chart.ChartAreas[ScanKey.Inside_Scan].Position.Y = 50;
            m_chart.ChartAreas[ScanKey.Inside_Scan].Position.Width = 100;
            m_chart.ChartAreas[ScanKey.Inside_Scan].Position.Height = 50;

            m_chart.ChartAreas[ScanKey.Outside_Scan].AxisX.LabelStyle.Enabled = false;
            m_chart.ChartAreas[ScanKey.Inside_Scan].AlignWithChartArea = ScanKey.Outside_Scan;

            foreach (var Area in m_chart.ChartAreas)
            {
                var AreaTitle = new Title(Area.Name, Docking.Top);
                AreaTitle.DockedToChartArea = Area.Name;
                m_chart.Titles.Add(AreaTitle);

                Area.AxisY.Interval = p_axisYInterval;
                Area.AxisX.Interval = p_axisXInterval;

                foreach (var Axes in Area.Axes)
                {
                    Axes.LabelAutoFitMaxFontSize = 5;
                    Axes.LabelAutoFitMinFontSize = 5;
                    Axes.IsLabelAutoFit = false;

                    AreaTitle.Font = Axes.LabelStyle.Font;
                    AreaTitle.ForeColor = p_lineColour;
                    Axes.LineColor = p_lineColour;
                    Axes.MinorGrid.LineColor = p_lineColour;
                    Axes.MajorGrid.LineColor = p_lineColour;
                }
            }
            HookEvents();
        }
        public void HookEvents()
        {
            m_chart
                .MouseClick += Chart_MouseClick;
        }

        // Event Handlers
        //[jwdebug("Hardcoded values for zoomming chart control.")]
        void Chart_MouseClick(object sender, MouseEventArgs e)
        {
            var XPos = (e.X * 100) / m_chart.Width;
            var YPos = (e.Y * 100) / m_chart.Height;

            //Log
            //    .FormattedLine(MessageScope.Integration, "Chart_MouseClick. e.Button = {0}, [X{1}, Y{2}]. m_chart.Height = {3} ", e.Button, XPos, YPos, m_chart.Height);

            //foreach (var Area in m_chart.ChartAreas)
            //{
            //    Log
            //        .FormattedLine(MessageScope.Integration, "Chart_MouseClick. Area.Name = {0}, [X{1}, Y{2}]. Area.Position.Height = {3} ", Area.Name, Area.Position.X, Area.Position.Y, Area.Position.Height);
            //}

            foreach (var Area in m_chart.ChartAreas)
            {
                if (e.Button == MouseButtons.Left)
                {
                    if (XPos < 33)
                    {
                        Area.AxisX.Minimum = 0;
                        Area.AxisX.Maximum = 240;
                    }
                    else if (XPos > 66)
                    {
                        Area.AxisX.Minimum = 240;
                        Area.AxisX.Maximum = 480;
                    }
                    else
                    {
                        Area.AxisX.Minimum = 480;
                        Area.AxisX.Maximum = 720;
                    }
                }
                else
                {
                    Area.AxisX.Minimum = 0;
                    Area.AxisX.Maximum = 720;
                }
            }
        }

        // Methods
        bool flag = true;
        public void AddSeries(string p_seriesName, int p_groupID, string p_chartArea, List<double> p_seriesXData, List<double> p_seriesYData, double p_limitLine = default(double))
        {
            Series SeriesData;
            HorizontalLineAnnotation Limit;

            // Get Series
            if (p_seriesXData != null && p_seriesYData != null)
            {
                SeriesData = new Series(p_seriesName);
                SeriesData.ChartArea = p_chartArea;
                SeriesData.ChartType = SeriesChartType.FastLine;
                SeriesData.Points.DataBindXY(p_seriesXData, p_seriesYData);

                // Make X-Interval Clean numbers
                if (flag)
                {
                    // Play with these number, the hard part I suppose is calculating them programmatically.
                    m_chart.ChartAreas[p_chartArea].AxisX.Minimum = -2;
                    m_chart.ChartAreas[p_chartArea].AxisX.Interval = 0.5;
                    m_chart.ChartAreas[p_chartArea].AxisX.Maximum = 1;
                }
            }
            else
                SeriesData = new Series();

            // Create Horizontal Line
            Limit = new HorizontalLineAnnotation();
            Limit.LineDashStyle = ChartDashStyle.Dash;
            Limit.LineColor = Color.Magenta;
            Limit.IsInfinitive = true;
            Limit.ClipToChartArea = p_chartArea;
            Limit.Y = p_limitLine;

            m_chart
                .SafeInvoke(() =>
                {
                    // Drop off old series by group ID, except for group 0
                    if (p_groupID != 0)
                    {
                        // Initialize a new Group if one does not exist
                        if (!m_displayedSeries.ContainsKey(p_groupID))
                            m_displayedSeries[p_groupID] =
                                new Tuple<List<Series>, List<HorizontalLineAnnotation>>(
                                    new List<Series>(),
                                    new List<HorizontalLineAnnotation>());

                        // Allow as many Series to be Displayed at a time as there are Colours in the Pallete list
                        var Count = m_displayedSeries[p_groupID].Item1.Count;
                        if (Count >= Pallete.Count)
                        {
                            // Items to Remove
                            Series
                                Oldest_Series = m_displayedSeries[p_groupID].Item1[Count - Pallete.Count];

                            HorizontalLineAnnotation
                                Oldest_Limit = m_displayedSeries[p_groupID].Item2[Count - Pallete.Count];

                            // Remove oldest Series and Limits from Chart by Object Reference
                            m_chart.Series.Remove(Oldest_Series);
                            m_chart.Annotations.Remove(Oldest_Limit);

                            // Prune Obsolete References
                            m_displayedSeries[p_groupID].Item1.RemoveAt(0);
                            m_displayedSeries[p_groupID].Item2.RemoveAt(0);
                        }

                        // Add new Object References to Dictionary
                        m_displayedSeries[p_groupID].Item1.Add(SeriesData);
                        m_displayedSeries[p_groupID].Item2.Add(Limit);

                        // Add new Stripline to Graph                        
                        Limit.AxisY = m_chart.ChartAreas[p_chartArea].AxisY;
                        if (p_limitLine != default(double))
                            m_chart.Annotations.Add(Limit);
                    }

                    // Add new series to Graph
                    m_chart.Series[p_seriesName] = SeriesData;

                    if (p_groupID == 0)
                        m_chart.Series[p_seriesName].Color = Color.Black;
                    else
                    {
                        for (int i = 0; i < m_chart.Series.Count; i++)
                            m_chart.Series[i].Color = Pallete[Pallete.Count.Span(0, i)];
                    }

                    foreach (var Area in m_chart.ChartAreas)
                        Area.RecalculateAxesScale();

                }, true);
        }

        public void Clear()
        {
            m_chart
                .SafeInvoke(() =>
                {
                    m_chart.Series.Clear();
                    m_chart.Annotations.Clear();
                    m_displayedSeries.Clear();
                });
        }
    }
}

namespace Common.Extensions
{
    public static partial class ExtensionMethods
    {
        /// <summary>
        /// Execute a method on the control's owning thread.
        /// </summary>
        /// <param name="p_control">The control that is being updated.</param>
        /// <param name="p_action">The method that updates uiElement.</param>
        /// <param name="p_synchronous">True to force synchronous execution of 
        /// updater.  False to allow asynchronous execution if the call is marshalled
        /// from a non-GUI thread.  If the method is called on the GUI thread,
        /// execution is always synchronous.</param>
        /// http://stackoverflow.com/q/714666
        public static void SafeInvoke(this Control p_control, Action p_action, bool p_synchronous = false)
        {
            p_control
                .CannotBeNull("p_control");

            if (p_control.InvokeRequired)
            {
                if (p_synchronous)
                    p_control.Invoke((Action)delegate { SafeInvoke(p_control, p_action, p_synchronous); });
                else
                    p_control.BeginInvoke((Action)delegate { SafeInvoke(p_control, p_action, p_synchronous); });
            }
            else
            {
                if (!p_control.IsHandleCreated)
                {
                    // The user is responsible for ensuring that the control has a valid handle
                    throw
                        new
                            InvalidOperationException("SafeInvoke on \"" + p_control.Name + "\" failed because the control had no handle.");

                    // jwdebug
                    // Only manually create handles when knowingly on the GUI thread such as a Form's Constructor
                    // Add the line below to generate a handle http://stackoverflow.com/a/3289692/1718702
                    // var h = this.Handle;
                }

                if (p_control.IsDisposed)
                    throw
                        new
                            ObjectDisposedException("Control is already disposed.");

                p_action.Invoke();
            }
        }
    }
}

namespace Common.Extensions
{
    public static partial class ExtensionMethods
    {
        /// <summary>
        /// Gets the index for an array relative to an anchor point, seamlessly crossing array boundaries in either direction.
        /// Returns calculated index value of an element within a collection as if the collection was a ring of contiguous elements (Ring Buffer).
        /// </summary>
        /// <param name="p_rollover">Index value after which the iterator should return back to zero.</param>
        /// <param name="p_anchor">A fixed or variable position to offset the iteration from.</param>
        /// <param name="p_offset">A fixed or variable position to offset from the anchor.</param>
        /// <returns>calculated index value of an element within a collection as if the collection was a ring of contiguous elements (Ring Buffer).</returns>
        public static int Span(this int p_rollover, int p_anchor, int p_offset)
        {
            // Prevent absolute value of `n` from being larger than count
            int n = (p_anchor + p_offset) % p_rollover;

            // If `n` is negative, then result is n less than rollover
            if (n < 0)
                n = n + p_rollover;

            return n;
        }
    }
}

namespace Common.FluentValidation
{
    public static partial class Validate
    {
        /// <summary>
        /// Validates the passed in parameter is not null, throwing a detailed exception message if the test fails.
        /// </summary>
        /// <param name="p_parameter">Parameter to validate.</param>
        /// <param name="p_name">Name of tested parameter to assist with debugging.</param>
        /// <exception cref="ArgumentNullException"></exception>
        public static void CannotBeNull(this object p_parameter, string p_name)
        {
            if (p_parameter == null)
                throw
                    new
                        ArgumentNullException(
                        string.Format("Parameter \"{0}\" cannot be null.",
                        p_name), default(Exception));
        }
    }
}
HodlDwon
  • 1,131
  • 1
  • 13
  • 30
  • Thank you for the answer. I tried to study the code you posted, and from what I think I grasp from the code, these `p_axisYInterval` and `p_axisXInterval` are set "manually": `new ChartControl_To_IScanResultsDisplay_Adapter(this.chart1, Color.Silver, p_axisYInterval:=1.0, p_axisXInterval:=64.0);`. What I was hoping to find is a way to set those dynamically, with an automatic choice of the best fit (in the case that no other option in mschart is able to do so). – Xavier Peña Apr 02 '14 at 08:04
  • @Tremor Well, you should be able to determine that based on your number of Bins and you can expose those settings as `Properties` on the interface instead of being fixed on construction. – HodlDwon Apr 02 '14 at 16:12
  • I am trying to do this right now, but it is quite hard to cover all the possible cases. In case it's of your interest: the first approach to this solution is written in the **Edit3** of my post. – Xavier Peña Apr 02 '14 at 16:30
  • 1
    @Tremor see my edit, I believe I've answered your original question now. (it's best to post a new question if you get stuck again on this like finding those values programmatically, instead of continually updating this one). – HodlDwon Apr 02 '14 at 20:24