Heute geht es mal um eine Leserfrage. Genauer geht es darum ob und wie man in einer ChartArea mehrere Serien mit unterschiedlichen Einheiten darstellen kann.

Der „einfache“ Weg

Zum ersten Teil der Frage lässt sich schon einmal sagen: Ja, aber standardmäßig eher eingeschränkt. Tatsächlich ist es so, dass eine ChartArea bis zu zwei unterschiedlich skalierte Y-Achsen haben kann. Dabei ist eine Achsenbeschriftung an der linken Seite des Diagramms angebracht und die zweite an der rechten.

Gesetzt wird die zu verwendende Achse jeweils an der Serie über die Property XAxisType bzw. YAxisType. Die Verwendung mehrerer Achsen ist somit nicht nur auf die Y-Achse begrenzt. Dafür aber leider auf die bereits erwähnte Maximalanzahl. Denn anstatt den Eigenschaften einen Index zu übergeben, wird eine Enumeration gesetzt und die hat, wer hätte es gedacht, nur zwei Einträge: Primary (also unten bzw. links) und Secondary (oben bzw. rechts).


Das Ergebnis ist nicht schön, aber selten. Denn woher soll der Nutzer nun bitteschön wissen für welche Serie welche Achsenbeschriftung gedacht ist? Hier hilft nur selbst Hand anzulegen. Dazu muss man folgende Stellen im Code setzen:

var color = Color.Orange;
chartArea.AxisY.Title = "C°";
chartArea.AxisY.TitleForeColor = color;
chartArea.AxisY.LabelStyle.ForeColor = color;
temperatureSeries.Color = color;
color = Color.Blue;
chartArea.AxisY2.Title = "m/s";
chartArea.AxisY2.TitleForeColor = color;
chartArea.AxisY2.LabelStyle.ForeColor = color;
speedSeries.Color = color;

Anfangs bin ich auf die Idee gekommen, doch einfach die Farbe der Serie zu nutzen um die Vordergrundfarben zu setzen. Dies ist aber nur möglich, wenn die Color-Property vorher auch initialisiert wurde. Setzt man diese nicht explizit, wird sie auch zur Laufzeit nicht gesetzt und hat damit einen Alphawert von 0, womit sie komplett transparent und damit unsichtbar ist!


Nun gut, richtig hübsch ist das Fabrizierte aber immer noch nicht. Sieht man genauer hin erkennt man nämlich, dass auf der rechten Seite ein Wert weniger an der Achse steht als auf der linken. Der Grund dafür ist, dass MS Chart Fließkommazahlen in der Beschriftung zu vermeiden versucht bzw. intern kein Abgleich zwischen der rechten und linken Achse gemacht wird. Dadurch wiederum überlagern sich die waagerechten Striche des Grids und stiften Verwirrung.

Ich habe verschiedene Dinge ausprobiert um das Grid sauber zu synchronisieren und bin dabei jedes Mal gescheitert. Weil man nur die Schrittweite der Achseneinteilung angeben kann, nicht aber die Anzahl der Einteilungen. Weiterhin kommt es zu einigem unerwarteten Verhalten, wenn man sich eine bestimmte Schrittweite anhand des Maximums und Minimums errechnet. Das hier auszuwälzen spare ich mir aber einfach mal und wünsche jedem Erfolg, der sich selbst versucht…

Aus diesem Grund kann ich nur anbieten entweder das Grid komplett abzuschalten oder dessen Aussehen zu ändern. Ersteres macht man über die Properties MajorGrid.Enabled der entsprechenden Achse, letzteres geschieht über MajorGrid.LineDashStyle bzw. MajorGrid.LineColor.

Der „kompliziertere“ Weg

Nun gut, das größte Problem ist aber in einigen Fällen nicht das Grid, sondern die Beschränkung auf maximal 2 Achsen. Die Idee ist also MS Chart so zu erweitern, dass es auch mehr Achsen pro ChartArea unterstützt. Nur wie vorgehen?

Eine Idee die mir dazu gekommen ist, war eine Überlagerung von ChartAreas. Also legt man eine Area über eine andere, macht den Hintergrund der überlagernden durchsichtig und verschiebt ihre Achse.

Klingt plausibel, scheitert aber daran, dass sich die Achsen einer Area nicht verschieben lassen. Es ist einfach nicht möglich sie zu entkoppeln, denn sie sind fester Bestandteil der ChartArea. Was also tun? Man nehme noch eine Area! In diese wird nur die Achse eingezeichnet. In eine andere wird nur der Graph, ohne Achse, gezeichnet und diese wird dann über die eigentliche ChartArea gelegt.

Der Code dafür sieht wie folgt aus und stammt weitestgehend aus den Beispielen die Microsoft liefert. Ich habe nur den Rückgabewert geändert, damit man die Achse später leichter anpassen kann.

public Axis 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 grid 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;
    // Adjust area position
    areaAxis.Position.X -= axisOffset;
    areaAxis.InnerPlotPosition.X += labelsSize;
    return areaAxis.AxisY;
}

Das allein reicht aber nicht. Immerhin nutzen wir die Ausgangsgröße der ChartArea um auf der linken Seite unsere Achsen anzuflanschen. Es ist also notwendig die eigentliche Area und ihren Inhalt etwas zu verschieben und genau das macht man mit den Properties Position und InnerPlotPosition.

chartArea.Position = new ElementPosition(25, 15, 58, 85);
chartArea.InnerPlotPosition = new ElementPosition(10, 0, 90, 90);
var speedAxis = CreateYAxis(chart, chartArea, speedSeries, 13, 8);
var rainAxis = CreateYAxis(chart, chartArea, rainSeries, 20, 8);

Das Resultat des ganzen kann dann wie folgt aussehen und wirkt zumindest auf mich schon etwas professioneller. Sollte man das Chart-Control im übergeordneten Container angedockt haben, funktioniert sogar die Größenänderung der Area, auch wenn wir absolute Werte für Position und InnerPlotPosition angegeben haben. Das liegt wiederum daran, dass die Angaben sich auf das Koordinatensystem des Chart-Controls beziehen, welches bei einer Größenänderung von diesem auch transformiert wird.

Zusammenfassung

So Herr Klaus, ich hoffe einmal Ihre Frage ausführlich genug beantwortet zu haben. Word zeigt mir immerhin schon Seite 5 an :) Ob Sie damit etwas anfangen können weiß ich leider nicht. Der Aufwand um Ihre Anforderung abzubilden ist demnach jedoch recht groß und sollte man mehr als 3 Achsen nutzen wollen, müsste man die Einstellungen des letzten Snippets entsprechend anpassen, ansonsten fliegen die Exceptions.

Dennoch kann man mit dem Workaround die gröbsten Probleme umgehen. Dass sich die Grids jedoch nicht besser synchronisieren lassen wurmt mich schon gewaltig.