강좌와 팁

MsChart 좌우 멀티축 구현 날짜:2021-7-4 11:25:18 조회수:73
작성자 : 소엔
포인트 : 1580
가입일 : 2020-02-02 00:09:14
방문횟수 : 109
글 203개, 댓글 64개
소개 : SoEn 운영자입니다.
작성글 보기
쪽지 보내기
두 개의 다른 물리량을 하나의 차트에 나타낼 때는 축을 분리하는 것이 좋다. 다음 예제는 2개의 온도와 1개의 습도를 같은 차트에 표시한다.

private void Form1_Load(object sender, EventArgs e)
{
     chart1.Series[0].Name = "온도";
     chart1.Series[0].Points.DataBindY(new int[] { 22, 25, 20, 28, 26, 29 });
     chart1.Series[0].ChartType = SeriesChartType.Line;
     chart1.Series[0].BorderWidth = 2;
     chart1.Series.Add("온도2");
     chart1.Series[1].Points.DataBindY(new int[] { 18, 18, 16, 19, 22, 17 });
     chart1.Series[1].ChartType = SeriesChartType.Line;
     chart1.Series[1].BorderWidth = 2;
     chart1.Series.Add("습도");
     chart1.Series[2].Points.DataBindY(new int[] { 65, 50, 58, 55, 60, 52 });
     chart1.Series[2].ChartType = SeriesChartType.Line;
     chart1.Series[2].BorderWidth = 2;

     chart1.ChartAreas[0].AxisX.MajorGrid.LineColor = Color.Gainsboro;
     chart1.ChartAreas[0].AxisY.MajorGrid.LineColor = Color.Gainsboro;
}

세 개의 시리즈를 추가하고 그리드는 흐릿하게 표시했다. 온도는 20도 안팎, 온도2는 10도 후반대를 왔다 갔다 하고 습도는 50%대 부근에 있다.



값의 변화를 파악하는데 별 무리는 없다. 그러나 온도끼리는 상대적인 크기를 쉽게 비교할 수 있지만 습도는 쌩판 다른 물리량이어서 크기를 비교하는 것이 별 의미가 없다.
온도는 섭씨이고 습도는 백분율이어서 단위가 달라 잘못 보면 무척 헷갈려 보인다. 다만 같은 차트에 있다 보니 상대적인 비교가 가능하고 변화 추이에 대한 상관 관계도 살펴볼 수 있다. 둘을 각각의 차트로 분리해 버리면 이런 비교는 어려워진다.
그나마 둘 다 값의 범위가 100 이내여서 한눈에 보이는데 온도가 수만도 범위에 있다면 백분율인 습도는 바닥에 깔려서 보이지도 않을 뿐더러 거의 직선 형태여서 값의 변화를 파악하기도 어렵다.
이럴 때는 축만 분리하여 왼쪽축은 온도로, 오른쪽 축은 습도로 표시하면 시인성도 좋고 동일 시점에 대해 두 물리량을 각각 비교해 볼 수 있다. 전체 코드는 다음과 같다.
 
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;

namespace chartTest
{
     public partial class Form1 : Form
     {
          public Form1()
          {
               InitializeComponent();
          }

          private void Form1_Load(object sender, EventArgs e)
          {
              chart1.Series[0].Name = "온도";
              chart1.Series[0].Points.DataBindY(new int[] { 22, 25, 20, 28, 26, 29 });
              chart1.Series[0].ChartType = SeriesChartType.Line;
              chart1.Series[0].BorderWidth = 2;
              chart1.Series.Add("온도2");
              chart1.Series[1].Points.DataBindY(new int[] { 18, 18, 16, 19, 22, 17 });
              chart1.Series[1].ChartType = SeriesChartType.Line;
              chart1.Series[1].BorderWidth = 2;
              chart1.Series.Add("습도");
              chart1.Series[2].Points.DataBindY(new int[] { 65, 50, 58, 55, 60, 52 });
              chart1.Series[2].ChartType = SeriesChartType.Line;
              chart1.Series[2].BorderWidth = 2;

              chart1.ChartAreas[0].AxisX.MajorGrid.LineColor = Color.Gainsboro;
              chart1.ChartAreas[0].AxisY.MajorGrid.LineColor = Color.Gainsboro;

              // 범례가 있고 좌우 Y축을 모두 표시한 스타일의 차트 좌표를 구한다.
              // 두 에리어를 똑같은 영역에 겹쳐 놓아야 한다.
              ElementPosition pos = new ElementPosition(3, 3, 80, 94);
              ElementPosition inner = new ElementPosition(6.48f, 2.79f, 87.02f, 88.46f);
              ChartArea areaTemp = chart1.ChartAreas[0];
              areaTemp.Position = pos;
              areaTemp.InnerPlotPosition = inner;

              // 습도용 에리어를 만든다.
              ChartArea areaHumid = chart1.ChartAreas.Add("humid");

              areaHumid.BackColor = Color.Transparent;
              areaHumid.BorderColor = Color.Transparent;
              areaHumid.Position.FromRectangleF(pos.ToRectangleF());
              areaHumid.InnerPlotPosition.FromRectangleF(inner.ToRectangleF());
              // 그리드는 모두 숨기고 AxisY2의 레이블만 남긴다.
              areaHumid.AxisX.MajorGrid.Enabled = false;
              areaHumid.AxisX.MajorTickMark.Enabled = false;
              areaHumid.AxisX.LabelStyle.Enabled = false;
               areaHumid.AxisY.MajorGrid.Enabled = false;
              areaHumid.AxisY.MajorTickMark.Enabled = false;
              areaHumid.AxisY.LabelStyle.Enabled = false;
              areaHumid.AxisY2.MajorGrid.Enabled = false;

              // 보조축과 틱마크만 보인다.
              areaHumid.AxisY.Enabled = AxisEnabled.False;
              areaHumid.AxisY2.Enabled = AxisEnabled.True;
              areaHumid.AxisY2.MajorTickMark.Enabled = true;

              // 좌우 마진을 숨기고 0부터가 아닌 범위에 맞게
              areaTemp.AxisX.IsMarginVisible = false;
              areaHumid.AxisX.IsMarginVisible = false;
              areaTemp.AxisY.IsStartedFromZero = false;
              areaHumid.AxisY.IsStartedFromZero = false;
              areaHumid.AxisY2.IsStartedFromZero = false;

              // 단위를 붙인다. %는 *100해 버리므로 작은따옴표로 감싸 문자 자체만 붙인다.
              areaTemp.AxisY.LabelStyle.Format = "#℃";
              areaHumid.AxisY2.LabelStyle.Format = "#'%'";

              // 습도 시리즈를 새로 만든 에리어로 옮긴다.
              chart1.Series[2].ChartArea = areaHumid.Name;
          }
     }
}

실행 결과부터 보자. 왼쪽 Y축에는 온도가 표시되고 두 개의 온도는 이 축을 기준으로 표시된다. 온도만 나타내니 축의 범위가 좁아 변화량을 더 크게 관찰할 수 있다. 오른쪽 Y축에는 습도를 표시하며 빨간색의 습도는 이 축을 기준으로 한다.




서로 다른 두 물리량을 같은 차트에 표시하면서도 축이 달라 헷갈리지 않고 각 축이 충분한 범위를 상세히 표현할 수 있다. 다만 어떤 시리즈가 어떤 축을 기준으로 하는지 색상과 범례로 명확히 표시해야 한다. 온도는 왼쪽 축, 습도는 오른쪽 축임을 사용자가 알 수 있어야 한다.  
차트 컨트롤이 X, Y 축 각각에 대해 주축, 보조축을 지원하기 때문에 좌우로 또는 상하로 두 개씩의 축을 그릴 수 있다. 그러나 주축과 보조축은 양쪽으로 표시만 할 뿐 범위를 다르게 줄 수는 없어 하나의 에리어만으로는 두 개의 물리량을 동시에 그릴 수 없다.
좌우축의 범위가 다른 차트를 그리려면 에리어를 분할해야 한다. 두 에리어를 같은 영역에 포개어 표시하되 하나는 주축만, 하나는 보조축만 사용하는 식이다. 두 에리어를 정확히 같은 영역에 겹쳐 배치하기 위해 원하는 차트 스타일로 영역의 크기를 조사한다.
(3, 3, 80, 94) 영역 크기는 차트의 보조축을 활성화하고 범례를 준 후 실행중에 버튼 클릭 등의 이벤트에서 실제 조사한 값이며 inner 영역도 마찬가지 방법으로 조사했다. 차트의 스타일이 바뀌면 영역의 크기도 달라진다.
영역 크기를 따로 조사하여 사용하는 대신 실행중에 에리어의 Position, innerPlotPosition 속성을 직접 조사하여 적용할 수도 있다. 그러나 Form_Load 시점에는 차트 영역이 아직 초기화되지 않아 모두 0의 값을 가지며 차트를 완전히 그린 후에나 정확한 값을 알 수 있다.
그래서 실행중에 차트 영역을 조사한 후 이를 적용했다. 일단 주축에 차트를 그린 후 에리어를 분할하는 것도 가능한데 이렇게 되면 깜박임이 발생한다. 세 시리즈를 한 에리어에 배치했다가 습도 에리어가 분리되는 모습이 보여 깔끔하지 못하다. 좀 더 좋은 방법이 있을 것도 같은데 아직 완벽한 해결책을 찾지 못했다.
영역을 결정한 후 humid라는 이름으로 에리어를 하나 더 생성한다. 이 에리어는 습도만 표시할 것이므로 온도와 같은 영역에 놓되 배경 색상은 투명하게 설정하고 X, Y축의 그리드나 틱 마크도 가지지 않는다. 다만 보조축만 표시하되 보조축의 그리드도 숨기고 레이블과 틱마크만 표시한다.
각 축의 마진과 0부터 시작 옵션은 필요에 따라 선택하되 X 마진은 숨기고 Y 마진만 표시하는 것이 딱 보기 좋다. 가급적 좁은 영역을 상세히 보기 위해 0부터 시작은 해제하되 humid 에리어의 경우 숨겨진 Y축과 보조축 Y2의 옵션을 일치시켜야 시리즈와 레이블의 범위가 같아진다.
온도의 Y축 레이블은 ℃로 표시하고 습도의 Y2축 레이블은 %로 표시하되 그냥 % 기호만 쓰면 자동으로 *100해 버리므로 %가 단순한 문자임을 지정하기 위해 작은 따옴표로 감싼다. 여기까지 모든 준비가 끝났으면 습도 시리즈의 에리어를 areaHumid로 변경하여 주 에리어에서 제외한다. 두 에리어를 분리해 보면 다음과 같다.



각 에리어별로 출력하는 시리즈를 분리해 놓고 두 에리어를 같이 영역에 겹치면 하나의 차트로 보인다. 습도 차트에는 X축 레이블, 틱마크, 그리드가 없고 심지어 바탕 배경색까지 투명해서 온도 차트 위에 살포시 얹힌다.
만약 3개 이상의 물리값을 같은 차트에 출력하려면 좌우로 또 다른 에리어를 겹치고 그 위에 축을 각각 표시하면 된다. 각 에리어는 시리즈를 표시하는 영역은 공유하되 축을 위한 에리어를 하나씩 더 만들어 일정 주기로 띄어가며 배치해야 한다. 이에 대해서는 mschart 샘플에 예제가 있으므로 참고하도록 하자.



이 방식도 에리어가 여러 개 겹쳐 있을 뿐 기본 원리는 같다. 코드가 좀 복잡하고 나열적이라 지저분해 보이는 경향이 있고 실행중에 멀티 Y를 토글하려면 에리어를 분리 및 통합하는 처리가 필요해 좀 까다로울 뿐이다. 실무에서 이런 차트를 만들어 봤는데 예상외로 유지, 보수가 어렵다. 이런 차트를 자유 자재로 그리려면 위 예제의 코드를 완벽하게 이해하고 있어야 한다.






 



개발자의 천국 SoEn

목록보기 삭제 수정 신고 스크랩

소엔 7월5일 9:51:02  

꼭 필요한 것은 아니지만 습도 에리어의 X축도 굳이 표시할 필요가 없네요. 어차피 온도축의 X축과 공유하니까요. 다음 코드를 더 추가하면 됩니다.

areaHumid.AxisX.Enabled = AxisEnabled.False;

물론 이 코드가 없어도 축이 겹칠 뿐 별 이상은 없습니다. 원칙적으로는 불필요하다는 얘기입니다. 


로그인하셔야 댓글을 달 수 있습니다.