Microsoft Chart Control(5) - Stacked 차트의 이해 ASP.NET/.NET

이번 글에서는 Stacked 차트에 대해서 좀 더 자세히 살펴보겠습니다. 단순하게 Line이나 Spline 형태의 그래프가 누적되는 것이 Stacked 차트이기 때문에 간단한 문제이기는 하지만 실제로 사용되는 데이터를 가지고 Stacked 차트를 만들다 보면 몇 가지 난점이 있습니다. 사실 이러한 부분은 어떤 식으로 Stacked 차트에 접근하느냐에 따라서 겪지 않을 수도 있지만 제가 Stacked 차트를 사용할 때 겪었던 어려움을 중심으로 살펴보겠습니다.

 

우선 다짜고짜 스크린샷 부터 보여드립니다.

 


 

위와 같은 그래프는 어떤 식으로 구성하면 될까요? 이해를 돕자면 위 그래프는 스타크래프트 대회인 위너스리그의 각 선수의 승수를 팀 별로 모아서 보여주는 StackedBar 그래프입니다. 잠시 글 읽기를 멈추고 '저 그래프를 그리기 위해서는 어떤 형태의 자료구조가 있을 것이고 그걸 몇 개의 Series에 어떻게 바인딩하면 되겠구나' 하는 생각을 해보시기 바랍니다. 어느 정도 생각이 정리 되었나요? 제 경우에 처음으로 저 문제를 해결할 때 당연히 12개의 Series를 만들고 각 Series를 하나의 팀에 바인딩하는 형식을 생각했습니다. 그 결과 나온 바인딩 코드는 다음과 같습니다.

 

        int[][] values = new int[12][];

 

        values[0] = new int[] { 11, 6, 5, 4, 4, 3, 3, 2, 1 };

        values[1] = new int[] { 17, 15, 3, 2, 1 };

        values[2] = new int[] { 20, 4, 4, 4, 3, 1, 1, 1 };

        values[3] = new int[] { 18, 10, 3, 1, 1, 1 };

        values[4] = new int[] { 12, 8, 8, 5, 1, 1 };

        values[5] = new int[] { 9, 8, 7, 4, 3, 2, 1 };

        values[6] = new int[] { 12, 8, 8, 4, 2 };

        values[7] = new int[] { 12, 6, 5, 3, 2, 1, 1 };

        values[8] = new int[] { 7, 7, 6, 5, 4, 3 };

        values[9] = new int[] { 13, 8, 4, 3 };

        values[10] = new int[] { 16, 10, 3, 3 };

        values[11] = new int[] { 11, 9, 1, 1, 1 };

 

        for (int i = 0; i < 12; i++)

        {

            for (int j = 0; j < values[i].Length; j++)

            {

                Chart1.Series[i].Points.AddY(values[i][j]);

            }

        }

 

깔끔하게 직관적으로 바인딩 되지 않습니까? 핫핫핫.... 그리고 빌드를 해보고(이상없이 빌드가 되었습니다.) 실행을 해보니... 바로 런타임 에러가 발생합니다. 이 시점에 코드를 보고 '? 저거 이상한데?' 라고 말하셨다면 그 분은 글쓴이 보다 센스가 훨씬 좋으신 분이 아닐까 생각되네요. 다시 한번 Line 그래프를 떠올려보면 하나의 Series의 값이 추가될 때마다 X축 방향으로 추가 됩니다. , StackedBar 차트를 그릴 때는(X Y축이 90도로 틀어진 형태이므로) 각각의 팀이 하나의 Series가 되는 것이 아니라 각 팀의 몇 번째 선수가 Series가 됩니다. 각 팀의 1번 선수가 첫 번째 Series 2번 선수가 두 번째 Series 하는 식으로 구성되는 것이지요. 그 결과 코드를 구성하는 논리가 좀 더 복잡해 졌습니다. Series의 수를 확정하기 위해서는 각 배열 중에 가장 많은 아이템을 가지는 배열의 크기를 알아야 하기 때문이죠.(가장 많은 선수가 있는 팀의 선수 숫자가 Series의 숫자가 되겠죠?) 그 결과 바인딩 코드는 다음처럼 변경 됩니다.

 

        int[][] values = new int[12][];

 

        values[0] = new int[] { 11, 6, 5, 4, 4, 3, 3, 2, 1 };

        values[1] = new int[] { 17, 15, 3, 2, 1 };

        values[2] = new int[] { 20, 4, 4, 4, 3, 1, 1, 1 };

        values[3] = new int[] { 18, 10, 3, 1, 1, 1 };

        values[4] = new int[] { 12, 8, 8, 5, 1, 1 };

        values[5] = new int[] { 9, 8, 7, 4, 3, 2, 1 };

        values[6] = new int[] { 12, 8, 8, 4, 2 };

        values[7] = new int[] { 12, 6, 5, 3, 2, 1, 1 };

        values[8] = new int[] { 7, 7, 6, 5, 4, 3 };

        values[9] = new int[] { 13, 8, 4, 3 };

        values[10] = new int[] { 16, 10, 3, 3 };

        values[11] = new int[] { 11, 9, 1, 1, 1 };

 

        int maxNum = 0;       

 

        for (int i = 0; i < 12; i++)

        {

            if (values[i].Length > maxNum)

                maxNum = values[i].Length;

        }

 

        for (int i = 0; i < maxNum; i++)

        {

            Series series = new Series();

 

            series.ChartType = SeriesChartType.StackedBar;

            series.BorderColor = Color.FromArgb(60, Color.Black);

            series.BorderWidth = 1;

            series.Color = colors[i];           

 

            Chart1.Series.Add(series);

        }

 

        for (int i = 0; i < 12; i++)

        {

            for (int j = 0; j < maxNum; j++)

            {

                if (values[i].Length > j)

                {

                    Chart1.Series[j].Points.AddXY(i, values[i][j]);

                }

            }

        }

 

우선 각 배열을 순회하면서 가장 큰 길이의 배열을 찾고 그 길이만큼의 Series를 생성합니다. 그리고 배열을 순회하면서 AddXY() 메서드를 이용해 각각의 값을 추가해 주었습니다. AddXY() 메서드를 이용하면 X축값과 Y축값을 지정해 줄 수 있기 때문에 원하는 정확한 위치에 값을 바인딩 할 수 있습니다. 이러한 바인딩 코드를 이용한 그래프의 결과는 다음과 같습니다.

 


 

뭔가 이상하죠? 얼핏 봐서는 맞는 논리인 듯 싶지만 결과를 보면 중간중간 막대가 띄워져 있기도 하고 뭔가 바인딩 되는 위치 자체도 부정확합니다. 이 때문에 MSDN이나 ASP.NET 공식사이트에 구글을 통해서 문의해 봤습니다. 그 결과 돌아온 내용은 '값이 없으면 0으로 바인딩 하세요. Empty Point는 명시적으로 Double.NaN 이나 0등의 값으로 바인딩 해줘야 합니다.' 라는 대답이 돌아왔습니다. 이어지는 글 중에 Empty Point에 대한 내용이 있을 예정이지만. 여기서 간단하게 설명하자면 그래프 중간에 내용이 없는 부분이 있다면 Double.NaN(아얘 없는 값, 기본적으로 Series Point값은 Double형이 사용됩니다. 지금까지는 대부분 int형의 값을 바인딩하고 있지만요.)이나 0을 바인딩해주어야 한다는 뜻입니다. 값이 없다고 그냥 넘어가면 안 된다는 의미입니다. 결국 모든 배열의 높이가 동일해야 정상적인 그래프가 그려질 수 있습니다. 하지만 Jagged 배열을 사용한 것처럼 저런 형태의 데이터가 언제나 고른 높이를 가질 수는 없는 노릇이기 때문에 값이 없는 경우에는 0을 바인딩 하는 것으로 코드를 수정하였습니다. 그 결과 만들어진 코드의 결과가 이 글에서 가장 먼저 보여드린 그래프입니다. 전체 코드를 살펴보겠습니다. 우선 HTML 코드를 살펴보죠.

 

               <asp:CHART id="Chart1" runat="server" Height="400px" Width="500px" BackColor="white" BorderDashStyle="Solid" ImageType="Png" BorderWidth="2" BorderColor="26, 59, 105">

                   <chartareas>

                             <asp:ChartArea Name="ChartArea1" BorderColor="64, 64, 64, 64" BorderDashStyle="Solid" BackSecondaryColor="Transparent" BackColor="64, 165, 191, 228">

                                        <area3dstyle Rotation="15" Inclination="15" WallWidth="0" />

                                        <position Y="3" Height="92" Width="92" X="2"></position>

                                        <axisy LineColor="64, 64, 64, 64"  LabelAutoFitMaxFontSize="8">

                                                  <LabelStyle Font="맑은 고딕, 8.25pt, style=Bold" />

                                                  <MajorGrid LineColor="64, 64, 64, 64" />

                                        </axisy>

                                        <axisx LineColor="64, 64, 64, 64"  LabelAutoFitMaxFontSize="8">

                                                  <LabelStyle Font="맑은 고딕, 8.25pt, style=Bold" />

                                                  <MajorGrid LineColor="64, 64, 64, 64" />

                                        </axisx>

                             </asp:ChartArea>

                   </chartareas>

        </asp:CHART> 

 

이렇게 HTML 코드를 구성합니다. .cs 코드는 다음과 같습니다.

 

    protected void Page_Load(object sender, System.EventArgs e)

    {

        Color[] colors = new Color[16];

 

        int alpha = 250;

 

        colors[0] = Color.FromArgb(alpha, 26, 83, 255);

        colors[1] = Color.FromArgb(alpha, 83, 26, 255);

        colors[2] = Color.FromArgb(alpha, 198, 26, 255);

        colors[3] = Color.FromArgb(alpha, 255, 26, 198);

        colors[4] = Color.FromArgb(alpha, 26, 198, 255);

        colors[5] = Color.FromArgb(alpha, 87, 129, 255);

        colors[6] = Color.FromArgb(alpha, 148, 175, 255);

        colors[7] = Color.FromArgb(alpha, 255, 26, 83);

        colors[8] = Color.FromArgb(alpha, 26, 255, 198);

        colors[9] = Color.FromArgb(alpha, 255, 228, 148);

        colors[10] = Color.FromArgb(alpha, 255, 213, 87);

        colors[11] = Color.FromArgb(alpha, 255, 83, 26);

        colors[12] = Color.FromArgb(alpha, 26, 255, 83);

        colors[13] = Color.FromArgb(alpha, 83, 255, 26);

        colors[14] = Color.FromArgb(alpha, 198, 255, 26);

        colors[15] = Color.FromArgb(alpha, 255, 198, 26);

 

        Chart1.PaletteCustomColors = colors;

 

        int[][] values = new int[12][];

 

        values[0] = new int[] { 11, 6, 5, 4, 4, 3, 3, 2, 1 };

        values[1] = new int[] { 17, 15, 3, 2, 1 };

        values[2] = new int[] { 20, 4, 4, 4, 3, 1, 1, 1 };

        values[3] = new int[] { 18, 10, 3, 1, 1, 1 };

        values[4] = new int[] { 12, 8, 8, 5, 1, 1 };

        values[5] = new int[] { 9, 8, 7, 4, 3, 2, 1 };

        values[6] = new int[] { 12, 8, 8, 4, 2 };

        values[7] = new int[] { 12, 6, 5, 3, 2, 1, 1 };

        values[8] = new int[] { 7, 7, 6, 5, 4, 3 };

        values[9] = new int[] { 13, 8, 4, 3 };

        values[10] = new int[] { 16, 10, 3, 3 };

        values[11] = new int[] { 11, 9, 1, 1, 1 };

 

        int maxNum = 0;       

 

        for (int i = 0; i < 12; i++)

        {

            if (values[i].Length > maxNum)

                maxNum = values[i].Length;

        }

 

        for (int i = 0; i < maxNum; i++)

        {

            Series series = new Series();

 

            series.ChartType = SeriesChartType.StackedBar;

            series.BorderColor = Color.FromArgb(60, Color.Black);

            series.BorderWidth = 1;

            series.Color = colors[i];           

 

            Chart1.Series.Add(series);

        }

 

        for (int i = 0; i < 12; i++)

        {

            for (int j = 0; j < maxNum; j++)

            {

                if (values[i].Length > j)

                {

                    Chart1.Series[j].Points.AddY(values[i][j]);

                }

                else

                {

                    Chart1.Series[j].Points.AddY(0);

                }

            }

        }

 

        Chart1.ChartAreas["ChartArea1"].Area3DStyle.Enable3D = true;

        Chart1.ChartAreas["ChartArea1"].AxisX.Interval = 1;

 

        Chart1.Series[0].Points[0].AxisLabel = "CJ";

        Chart1.Series[0].Points[1].AxisLabel = "화승";

        Chart1.Series[0].Points[2].AxisLabel = "SKT";

        Chart1.Series[0].Points[3].AxisLabel = "KTF";

        Chart1.Series[0].Points[4].AxisLabel = "웅진";

        Chart1.Series[0].Points[5].AxisLabel = "STX";

        Chart1.Series[0].Points[6].AxisLabel = "온게임넷";

        Chart1.Series[0].Points[7].AxisLabel = "삼성";

        Chart1.Series[0].Points[8].AxisLabel = "위메이드";

        Chart1.Series[0].Points[9].AxisLabel = "MBCgame";

        Chart1.Series[0].Points[10].AxisLabel = "Estro";

        Chart1.Series[0].Points[11].AxisLabel = "공군";

    }

 

색 지정 부분과 X축에 팀 이름을 넣는 부분이 들어갔기 때문에 좀 코드가 길어졌기는 하지만 코드의 내용 자체는 크게 어려운 부분이 없을 것으로 생각합니다. 수정한 마지막 코드에서는 if구문을 이용해서 값이 있을 때는 해당하는 위치의 배열의 값을 AddY() 메서드로 바인딩해주고 값이 없을 때는 AddY메서드에 0을 넣어서 바인딩 해주는 것을 확인할 수 있습니다.

 

여기서 확인할 수 있는 부분은

 

첫째, Series X축의 영역으로 진행된다.(그게 Bar차트처럼 90도로 회전하더라도 X축개념은 동일하다는 점)

 

둘째, 값이 없는 부분이더라도 반드시 채워준다.

 

입니다. 값이 없는 부분에 대한 처리는 이어질 강좌에서 좀 더 자세히 다뤄보겠습니다. 첫번째 부분이 이해가 되지 않으신다면 같은 색으로 되어 있는 부분이 하나의 Series다 라고 이해하시면 되겠습니다.

 

그럼 다음으로 StackedColumn100차트를 다시 살펴보겠습니다. 네 번째 강좌에서 보여드린 StackedColumn100 그래프가 좀 안타까운 형태여서 이런걸 하는 건 아닙니다.(믿어주세요) 뭐 그렇다고 해도 지금 보여드릴게 별게 없긴 하군요....

 

변경될 부분은 .cs 코드에서 Series의 차트 형태를 결정하는 부분 뿐입니다. .cs 코드의 Series 생성 부분을 다음처럼 변경합니다.

 

        for (int i = 0; i < maxNum; i++)

        {

            Series series = new Series();

 

            series.ChartType = SeriesChartType.StackedColumn100;   

            series.BorderColor = Color.FromArgb(60, Color.Black);

            series.BorderWidth = 1;

            series.Color = colors[i];

 

            Chart1.Series.Add(series);

        }

 

변한 부분은 series.ChartType의 값이 SeriesChartType.StackedBar에서 SeriesChartType.StackedColumn100이 된 것 뿐입니다. HTML 코드 .cs 코드 모두 동일합니다. 그 결과는 다음과 같습니다.


 

 

앞에서 살펴본 좀 구리구리한 StackedColumn100 그래프보다는 좀 더 멋진 그래프가 되었습니다. 이런 형태로 Stacked 차트를 Stacked~100 차트로 변경하면 절대적인 수를 비교하는 그래프에서 전체 중에 한 개체의 비율을 비교하는 그래프로 쉽게 변환이 가능합니다.

 

이번에는 StackedArea차트로 변경해 봄으로서 앞에서 살펴본 Stacked 차트의 성질에 대해서 더 확실히 알아보겠습니다. .cs 코드를 다음처럼 변경합니다.

 

        for (int i = 0; i < maxNum; i++)

        {

            Series series = new Series();

 

            series.ChartType = SeriesChartType.StackedArea;

            series.BorderColor = Color.FromArgb(60, Color.Black);

            series.BorderWidth = 1;

            series.Color = colors[i];

 

            Chart1.Series.Add(series);

        }

 

변경한 부분은 ChartType StackedArea로 바꾼 것뿐입니다. HTML에서는 변경 점이 전혀 없습니다. 이에 의한 결과는 다음과 같습니다.

 


 

그래프를 보시면 Stacked 차트에서 하나의 Series가 어떻게 구성되는지, 그리고 그래프가 쌓일 때 어떤 형식으로 쌓이는지 더 명확하게 구분이 되실 거라고 생각합니다. 여기에서 보면 0으로 설정하는 부분 역시 완전히 빈 값이 아닌 같은 Series에서 이어지는 한 점임을 알 수 있습니다.

 

이번 글에서 살펴본 내용은 사실 StackedArea차트를 만들어보지 않은 상태에서 StackedColumn이나 StackedBar 차트를 다루면서 생길 수 있는 오해에 관련된 내용이었습니다. 어떻게 보면 제가 그래프를 만들다가 생긴 삽질의 기록인 셈입니다. 이번 글을 통해서 Series에 대한 개념을 확실히 잡으셨으면 좋겠네요. 그럼 즐프하세요~


덧글

댓글 입력 영역