【问题标题】:mschart: my x axis scale is a messmschart:我的 x 轴刻度是一团糟
【发布时间】:2014-04-01 13:27:32
【问题描述】:

我在尝试驯服我的 mschart 的 x 轴时遇到了一些问题。

我使用图表同时绘制多个直方图,如您所见,我的 x 比例是一团糟(见下图)。我的主要问题是:如何让它看起来像 y 轴一样干净。我所说的“干净”是指:

  • 使“零值”出现在刻度中。
  • 我的意思是,输入对人类可读性有意义的值,而不是像 {0.000000000000} 这样的格式。

我能够找到的所有解决方案都是某种 DIY 方法,它不适用于我正在考虑的所有各种用例(或者更好的是:我无法使其工作)。我的直觉是:“mschart在Y轴的情况下可以做到完美,X轴也一定可以做到!”。

编辑:抱歉,我没有足够的声望来发布图片。这是imgur链接中的图表=>

http://imgur.com/bpeYDFK.png

附带说明,鉴于您在图中可以看到,我的所有值几乎都集中在 x=0 附近,我想知道是否有可能为 x 创建一个对数刻度......虽然我很知道负值是个问题,0 应该是 -Infinite。

当然我可以选择更好的比例,但我在这里想要完成的是自动创建大量图表,而不用担心它们一个一个。我的解决方案是从 x 轴的顶部和底部削减一定百分比的能量(类似于“从 min_x 开始,向上,一旦你达到总能量的 10%,这将是新的 min_x。从 max_x 向下也是一样。”),但是这个解决方案仍然在零附近留下一个狭窄的尖峰。我可以选择更高的切割能量值,但恐怕它不会在所有情况下都有效......

有什么提示吗?非常感谢。

Edit2:这是此图系列类型的示例。由于它是针对 x_min 和 x_max 之间的 N 个 bin 计算的直方图,因此 X 的值相当混乱。

-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:所以我找到了一个混合this post 和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

【问题讨论】:

  • 好的,所以...关于第二个问题:我发现在 mschart 中您实际上可以设置myChartArea.AxisX.LogarithmBase = True。现在我必须弄清楚如何处理 0 和负值,所以图表仍然有意义(我不能简单地切割值
  • 再次谈到第二个问题:我采取了不同的方法。我选择了一个自动 binWidth Dim automaticBinWidth As Double = 3.49 * sigma * Math.Pow(totalNumberOfValues, -1 / 3)(信息 here),然后在 [-2*sigma, 2*sigma] 处切割直方图的最大最小值。这为直方图的有趣部分设置了合理的缩放比例。虽然我认为我仍然会尝试将它与 x 对数刻度结合起来以获得更好的准确性。

标签: vb.net c#-4.0 charts mschart


【解决方案1】:

您要设置图表区域的Interval。如果您会阅读 C#,请查看下面的代码。

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

这是一个演示应用

在 c# 中创建一个新的 WinForms 项目。添加一个名为 chart1(默认名称)的图表控件和一个名为 button1(也是默认名称)的按钮。双击按钮为它连接点击事件。

然后复制粘贴下面的整个代码块并运行它。单击该按钮可将数据添加到图表并进行单步调试/调试以查看发生了什么/在何处发生。

我刚刚从我已经拥有的项目中提取了 99% 的代码,所以它有点乱,但它明白了重点。

编辑

更新了我的示例以执行 XY 系列而不是固定的 X 值,并硬编码了一些内容以查看其行为方式。

查看下面显示的此代码块的方法,并将 bool flag 更改为 true 或 false 并运行它以查看行为的变化。如果Maximum 和Minimum 对称 或者如果0 恰好是Interval 命中的值之一(即。如果 Min = -10.0, Max = 5.0 和 Interval = 1.0,那么它将显示 -10, -9, -8... 0, 1, 2, ... 5)。

我对这些值进行了硬编码,但您当然希望以编程方式获取它们。然而,这是我留给你的练习。享受吧!

// 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));
        }
    }
}

【讨论】:

  • 感谢您的回答。我试图研究您发布的代码,从我认为我从代码中掌握的内容来看,这些 p_axisYInterval 和 p_axisXInterval 是“手动”设置的:new ChartControl_To_IScanResultsDisplay_Adapter(this.chart1, Color.Silver, p_axisYInterval:=1.0, p_axisXInterval:=64.0);。我希望找到一种动态设置它们的方法,自动选择最合适的(如果 mschart 中没有其他选项能够这样做)。
  • @Tremor 好吧,您应该能够根据您的 Bins 数量来确定,您可以在界面上将这些设置公开为Properties,而不是固定在构造上。
  • 我现在正在尝试这样做,但很难涵盖所有可能的情况。如果您感兴趣:此解决方案的第一种方法写在我的帖子的 Edit3 中。
  • @Tremor 看到我的编辑,我相信我现在已经回答了你原来的问题。 (如果您再次陷入困境,最好发布一个新问题,例如以编程方式查找这些值,而不是不断更新这个问题)。
猜你喜欢
  • 1970-01-01
  • 2012-12-12
  • 2012-12-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-04-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多