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!
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.
Kommentare