Search code examples
c#.netwinformslivecharts

How do I get the Axis size, in pixels, from a LiveCharts Axis in order to determine a custom number of Separator steps on the axis?


I am using the WinForms version of LiveCharts to plot a series of data, as a LineSeries, in my .NET application. I would like to be able to adjust the number of 'ticks' shown on the X axis depending on the length of the axis in pixels. So far, I haven't been able to determine what the length of the axis actually is. On inspection at runtime, the Axis object has a Height and a Width of NaN so my method fails to calculate a correct number of ticks (or steps).

So, my question is, how do I get the correct size of the Axis object, as shown on screen, in pixels?

My code so far:

Form method calling Chart class methods to get number of ticks based on a desired pixel spacing and the set the tick spacing on the axis:

private static void UpdateChainageTickSpacing()
{
    Axis chainageAxis = Chart.GetAxisByName(AxisOrientation.X, "Chainage");
    int numTicks = Chart.GetNumTicksByPixelSpacing(chainageAxis, 40);
    Chart.SetTickSpacing(chainageAxis, numTicks, 0);
}

In Chart class:

public static void SetTickSpacing(Axis axis, int numTicks, int precision)
{
    double maxValue = double.MinValue;
    double minValue = double.MaxValue;

    foreach (Series series in AllSeries)
    {
        maxValue = Math.Max(GetMaxValue(series, GetAxisOrientation(axis)), maxValue);
        minValue = Math.Min(GetMinValue(series, GetAxisOrientation(axis)), minValue);
    }

    double range = maxValue - minValue;
    double spacing = range / numTicks;
    spacing = Math.Round(spacing / 10, precision) * 10;
    axis.Separator.Step = spacing;
}

public static int GetNumTicksByPixelSpacing(Axis axis, int pixelSpacing)
{
    double pixelSize;

    if (GetAxisOrientation(axis) == AxisOrientation.X)
    {
        int index = GetAxisIndexByName(GetAxisCollection(AxisOrientation.X), axis.Name);
        pixelSize = cartesianChart.AxisX[index].Width;   // this returns NaN
    }
    else
    {
        int index = GetAxisIndexByName(GetAxisCollection(AxisOrientation.Y), axis.Name);
        pixelSize = cartesianChart.AxisY[index].Height;   // this returns NaN
    }

    return (int)Math.Round(pixelSize / pixelSpacing);
}

Solution

  • The chart has two Canvas elements: the parent Canvas contains chart elements like the legends, sections etc. and another child Canvas as the actual plot area to host the plotted graph.

    The axis lengths (x and y) are equal to the plot area's dimensions (width and height).

    You can access the plot canvas by referencing the parent Canvas via the CartesianChart.Content property:

    You need to wait until all elements have been added to the plot canvas to get it's final size.

    The chart itself doesn't expose an event that notifies when the content layout has completed. So you have to listen to the UIElement.LayoutUpdated event of the plot canvas.

    To achieve this, you need to subscribe to the Canvas.LayoutUpdated event from either a Window.ContentRendered or Window.Loaded event handler. Since you want to ignore as many redundant layout updates as possible, the Window.ContentRendered event, which is raised last, is the best in this scenario.

    UIElement.LayoutUpdated is raised during every layout pass like resize or Canvas.Children collection manipulation like add/move/remove a child element, which is quite often and therefore needs some optimization to reduce redundant re-calculations.

    MainWindow.xaml

    <Window>
      <CartesianChart x:Name="Chart" />
    </Window>
    

    MainWindow.xaml.cs

    partial class MainWindow: Window
    {
      public MainWindow()
      {
        InitializeComponent();
        
        this.ContentRendered += OnContentRendered;
      }
    
      private void OnContentRendered(object sender, EventArgs e)
      {
        this.ContentRendered -= OnContentRendered;
    
        var canvas = this.Chart.Content as Canvas;
        var plotCanvas = canvas.Children.OfType<Canvas>().FirstOrDefault();
        plotCanvas.LayoutUpdated += GetAxisXLengthOnLayoutUpdated;
      }
    
      // Will be called very often 
      // (on every layout pass of the Canvas sender like on resize or add/move/remove child)
      private void GetAxisXLengthOnLayoutUpdated(object sender, EventArgs e)
      {
        var plotCanvas = sender as Canvas;
    
        // If this is a one time operation, unsubscribe from LayoutUpdated event
        plotCanvas.LayoutUpdated -= GetAxisXLengthOnLayoutUpdated;
    
        // The length of the x-axis is equal to the final width of the plot area
        var actualAxisXLength = plotCanvas.ActualWidth;
      }
    }