Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add more Y-axes to MSChart with different scale at left or right side

I want to add 3 Y-axes for the chart with different scales.

I want to get one x axis and different y axis. I did it like below code but I want to show one y axis like in the 2nd image that I attached..

My C# code so far:

   private void checkBoxUseMultipleYAxis_CheckedChanged(object sender, EventArgs e)
    {
        if (checkBoxUseMultipleYAxis.Checked)
        {
            // Set custom chart area position
            chart1.ChartAreas["ChartArea1"].Position = new ElementPosition(25, 10, 68, 85);
            chart1.ChartAreas["ChartArea1"].InnerPlotPosition = new ElementPosition(10, 0, 90, 90);``



            // Create extra Y axis for second and third series
            CreateYAxis(chart1, chart1.ChartAreas["ChartArea1"], chart1.Series["Current"], 13, 8);
            CreateYAxis(chart1, chart1.ChartAreas["ChartArea1"], chart1.Series["Capacity"], 22, 8);
        }
        else
        {
            // Set default chart areas
            chart1.Series["Current"].ChartArea = "ChartArea1";
            chart1.Series["Capacity"].ChartArea = "ChartArea1";

            // Remove newly created series and chart areas
            while (chart1.Series.Count > 3)
            {
                chart1.Series.RemoveAt(3);
            }
            while (chart1.ChartAreas.Count > 1)
            {
                chart1.ChartAreas.RemoveAt(1);
            }

            // Set default chart are position to Auto
            chart1.ChartAreas["ChartArea1"].Position.Auto = true;
            chart1.ChartAreas["ChartArea1"].InnerPlotPosition.Auto = true;

        }
    }
 public void CreateYAxis(Chart chart, ChartArea area, Series series, float axisOffset, float labelsSize)
    {
        // Create new chart area for original series
        ChartArea areaSeries = chart.ChartAreas.Add("ChartArea_" + series.Name);
        areaSeries.BackColor = Color.Transparent;
        areaSeries.BorderColor = Color.Transparent;
        areaSeries.Position.FromRectangleF(area.Position.ToRectangleF());
        areaSeries.InnerPlotPosition.FromRectangleF(area.InnerPlotPosition.ToRectangleF());
        areaSeries.AxisX.MajorGrid.Enabled = false;
        areaSeries.AxisX.MajorTickMark.Enabled = false;
        areaSeries.AxisX.LabelStyle.Enabled = false;
        areaSeries.AxisY.MajorGrid.Enabled = false;
        areaSeries.AxisY.MajorTickMark.Enabled = false;
        areaSeries.AxisY.LabelStyle.Enabled = false;
        areaSeries.AxisY.IsStartedFromZero = area.AxisY.IsStartedFromZero;


        series.ChartArea = areaSeries.Name;

        // Create new chart area for axis
        ChartArea areaAxis = chart.ChartAreas.Add("AxisY_" + series.ChartArea);
        areaAxis.BackColor = Color.Transparent;
        areaAxis.BorderColor = Color.Transparent;
        areaAxis.Position.FromRectangleF(chart.ChartAreas[series.ChartArea].Position.ToRectangleF());
        areaAxis.InnerPlotPosition.FromRectangleF(chart.ChartAreas[series.ChartArea].InnerPlotPosition.ToRectangleF());

        // Create a copy of specified series
        Series seriesCopy = chart.Series.Add(series.Name + "_Copy");
        seriesCopy.ChartType = series.ChartType;
        foreach (DataPoint point in series.Points)
        {
            seriesCopy.Points.AddXY(point.XValue, point.YValues[0]);
        }

        // Hide copied series
        seriesCopy.IsVisibleInLegend = false;
        seriesCopy.Color = Color.Transparent;
        seriesCopy.BorderColor = Color.Transparent;
        seriesCopy.ChartArea = areaAxis.Name;

        // Disable drid lines & tickmarks
        areaAxis.AxisX.LineWidth = 0;
        areaAxis.AxisX.MajorGrid.Enabled = false;
        areaAxis.AxisX.MajorTickMark.Enabled = false;
        areaAxis.AxisX.LabelStyle.Enabled = false;
        areaAxis.AxisY.MajorGrid.Enabled = false;
        areaAxis.AxisY.IsStartedFromZero = area.AxisY.IsStartedFromZero;
        areaAxis.AxisY.LabelStyle.Font = area.AxisY.LabelStyle.Font;

        // Adjust area position
        areaAxis.Position.X -= axisOffset;
        areaAxis.InnerPlotPosition.X += labelsSize;

    }
    };

So I get output like this

but I want to adjust this output like this

like image 607
SNP Avatar asked Dec 11 '22 11:12

SNP


1 Answers

This is an interesting question with a piece of quite involved code which solves a problem that I never noticed..

Let's first talk about the basics.

In a chart you can add several series of data to the same area. As long as the ranges of y-values are more or less the same there is usually no problem. But if they aren't, the y-axis will scale so that all values will fit in the chart area. This means that those series with small ranges will get squashed. Here is an example:

enter image description here

In addition to the unreadable y-values for all but the orange series we also see that there is only one axis title; if it applies to all series, ok; but if it doesn't: best leave it out..

(Sometimes setting the y-axis to be logarithmic can help, but usually this would just confuse things and not help at all.)

This calls for more axes. In fact there is one extra axis built-in, right there just for the asking: Each chart area can have a primary y-axis to the left and another one, called 'secondary' AxisY2 to the right.

You can enable it and associate one series to it:

chart1.ChartAreas[0].AxisY2.Enabled = AxisEnabled.True;
chart1.Series[1].YAxisType = AxisType.Secondary;

This is fine and works well. But our example calls for more than 2 y-axes.. which is just what the code you found provides.

Let's have a look at the reults first:

enter image description here

This is nice; now we can see how the ranges differ from 0 - 30 to 0 - 120 , -20 - 30 and finally 0 - 1200.

But as all axes are added to the left they get further and further away from the plot area. Hence your question..

I found it easiest to expand on the code you found instead of writing a better version from scratch. This implies that most issues with the code are still there:

  • Not modularized
  • Routines depend on 'magic' values
  • Finding those values by trial and error is tedious

I have added two params to the CreateYAxis method; one sets the width of the added axis-areas and the other toggles adding them to the left or right.

Let's look at the result first: enter image description here

Now for the changed code:

public void CreateYAxis(Chart chart, ChartArea area, Series series, 
                        float axisX, float axisWidth, float labelsSize, bool alignLeft)
{

    chart.ApplyPaletteColors();  // (*)

    // Create new chart area for original series
    ChartArea areaSeries = chart.ChartAreas.Add("CAs_" + series.Name);
    areaSeries.BackColor = Color.Transparent;
    areaSeries.BorderColor = Color.Transparent;
    areaSeries.Position.FromRectangleF(area.Position.ToRectangleF());
    areaSeries.InnerPlotPosition.FromRectangleF(area.InnerPlotPosition.ToRectangleF());
    areaSeries.AxisX.MajorGrid.Enabled = false;
    areaSeries.AxisX.MajorTickMark.Enabled = false;
    areaSeries.AxisX.LabelStyle.Enabled = false;
    areaSeries.AxisY.MajorGrid.Enabled = false;
    areaSeries.AxisY.MajorTickMark.Enabled = false;
    areaSeries.AxisY.LabelStyle.Enabled = false;
    areaSeries.AxisY.IsStartedFromZero = area.AxisY.IsStartedFromZero;
    // associate series with new ca
    series.ChartArea = areaSeries.Name;

    // Create new chart area for axis
    ChartArea areaAxis = chart.ChartAreas.Add("CA_AxY_" + series.ChartArea);

    areaAxis.BackColor = Color.Transparent;
    areaAxis.BorderColor = Color.Transparent;
    RectangleF oRect = area.Position.ToRectangleF();
    areaAxis.Position = new ElementPosition(oRect.X, oRect.Y, axisWidth, oRect.Height);
    areaAxis.InnerPlotPosition
            .FromRectangleF(areaSeries.InnerPlotPosition.ToRectangleF());

    // Create a copy of specified series
    Series seriesCopy = chart.Series.Add(series.Name + "_Copy");
    seriesCopy.ChartType = series.ChartType;
    seriesCopy.YAxisType = alignLeft ? AxisType.Primary : AxisType.Secondary;  // (**)

    foreach (DataPoint point in series.Points)
    {
        seriesCopy.Points.AddXY(point.XValue, point.YValues[0]);
    }
    // Hide copied series
    seriesCopy.IsVisibleInLegend = false;
    seriesCopy.Color = Color.Transparent;
    seriesCopy.BorderColor = Color.Transparent;
    seriesCopy.ChartArea = areaAxis.Name;

    // Disable grid lines & tickmarks
    areaAxis.AxisX.LineWidth = 0;
    areaAxis.AxisX.MajorGrid.Enabled = false;
    areaAxis.AxisX.MajorTickMark.Enabled = false;
    areaAxis.AxisX.LabelStyle.Enabled = false;

    Axis areaAxisAxisY = alignLeft ? areaAxis.AxisY : areaAxis.AxisY2;   // (**)
    areaAxisAxisY.MajorGrid.Enabled = false;
    areaAxisAxisY.IsStartedFromZero = area.AxisY.IsStartedFromZero;
    areaAxisAxisY.LabelStyle.Font = area.AxisY.LabelStyle.Font;

    areaAxisAxisY.Title = series.Name;
    areaAxisAxisY.LineColor =  series.Color;    // (*)
    areaAxisAxisY.TitleForeColor = Color.DarkCyan;  // (*)

    // Adjust area position
    areaAxis.Position.X = axisX;
    areaAxis.InnerPlotPosition.X += labelsSize;
}

I have added a little code to make the axes have the series colors. (*) The alignLeft will, when false, pick the secondary axis instead of the primary one. (**)

The necessary numbers are used when calling the method in the checkbox event.

Here are the lines and numbers that were used for my screenshots:

First the normal one..

// Set custom chart area position
ChartArea ca = chart1.ChartAreas["ChartArea1"];
ca.Position = new ElementPosition(23, 10, 77, 85);
ca.InnerPlotPosition = new ElementPosition(12, 0, 67, 90);

 // Create extra Y axis for some series
 CreateYAxis(chart1, ca, chart1.Series["Current"], 5, 9, 8, true);
 CreateYAxis(chart1, ca, chart1.Series["Capacity"], 13, 9, 8, true);
 CreateYAxis(chart1, ca, chart1.Series["testing"], 21, 9, 8, true);

..then the one with one series axis added to the right:

// Set custom chart area position
ChartArea ca = chart1.ChartAreas["ChartArea1"];
ca .Position = new ElementPosition(15, 10, 83, 85);
ca .InnerPlotPosition = new ElementPosition(12, 0, 67, 90);

// Create extra Y axis for some series
CreateYAxis(chart1,ca , chart1.Series["Current"], 5, 9, 8, true);
CreateYAxis(chart1, ca , chart1.Series["Capacity"], 13, 9, 8, true);
CreateYAxis(chart1, ca , chart1.Series["testing"], 64, 21, 8, false);

Note that the else branch of the checkbox event tries to remove the extra chart areas. It has a hard-coded numer 3; this and the whole reversal code is not quite stable!

A short desciption about what the code itself does:

It adds two extra chart areas for each extra 'series axis':

  • One to associate the original series to. This one must always have the same position&size as the orginal chart area! Its purpose is to allow the graphics to scale to the maximum extent, which it will do, as no other series is associated with this new chart area. The graphics stays visible but all other parts like axes borders etc are invisible.

  • The other one will show the axis. Here everything but the axis is invisible; and to fill the axis the points from the original series are copied to a new series, which is associated with this chart area.

Final note about usage: The whole usage still depends on the numbers you use to lay out the chart areas! I wrote myself a little helper tool, which you can download if you are interested. Here is it at work:

enter image description here

like image 68
TaW Avatar answered Mar 09 '23 00:03

TaW