From 0d36c11ea4048e129cda95d480b6948038255a98 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Wed, 1 Jul 2026 10:09:24 +0900 Subject: [PATCH] Add explanatory comments to heatmap code --- mbn_stock/ColorRangeRule.cs | 5 +++++ mbn_stock/Form1.cs | 26 +++++++++++++++++++++++++- mbn_stock/HeatMapRenderer.cs | 25 +++++++++++++++++++++++++ mbn_stock/Program.cs | 5 ++--- mbn_stock/SectorHeatMapItem.cs | 2 ++ 5 files changed, 59 insertions(+), 4 deletions(-) diff --git a/mbn_stock/ColorRangeRule.cs b/mbn_stock/ColorRangeRule.cs index 970db1a..b69bb0e 100644 --- a/mbn_stock/ColorRangeRule.cs +++ b/mbn_stock/ColorRangeRule.cs @@ -5,6 +5,7 @@ using System.Globalization; namespace mbn_stock { + // 등락률 구간과 해당 구간에 사용할 색상 값을 보관하는 바인딩용 모델입니다. public class ColorRangeRule : INotifyPropertyChanged { private decimal _fromRate; @@ -78,11 +79,13 @@ namespace mbn_stock } } + // 입력한 등락률이 이 색상 구간에 포함되는지 확인합니다. public bool Contains(decimal value) { return value >= MinRate && value < MaxRate; } + // #RRGGBB 문자열을 실제 Drawing Color로 변환할 수 있는지 확인합니다. public static bool TryParseColor(string text, out Color color) { color = Color.Empty; @@ -104,6 +107,7 @@ namespace mbn_stock } } + // 사용자가 입력한 색상 문자열을 #RRGGBB 형태에 가깝게 정리합니다. public static string NormalizeColorHex(string text) { if (string.IsNullOrWhiteSpace(text)) @@ -129,6 +133,7 @@ namespace mbn_stock return normalized; } + // DataGridView 바인딩에 변경 사실을 알려 화면을 갱신합니다. private void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; diff --git a/mbn_stock/Form1.cs b/mbn_stock/Form1.cs index fab9d5f..e5835c3 100644 --- a/mbn_stock/Form1.cs +++ b/mbn_stock/Form1.cs @@ -9,6 +9,7 @@ using System.Windows.Forms; namespace mbn_stock { + // 업종 데이터와 색상 규칙을 편집하고 히트맵 이미지를 미리보기/저장하는 테스트 폼입니다. public partial class Form1 : Form { private readonly BindingList _items = new BindingList(); @@ -16,6 +17,7 @@ namespace mbn_stock private readonly Random _random = new Random(); private bool _loading; + // 디자이너 UI를 초기화하고 런타임에서는 기본 데이터와 색상 규칙을 연결합니다. public Form1() { InitializeComponent(); @@ -32,6 +34,7 @@ namespace mbn_stock RandomizeValues(); } + // 미리보기 이미지 리소스를 해제해 Bitmap 핸들이 남지 않게 정리합니다. protected override void OnFormClosed(FormClosedEventArgs e) { if (_previewBox != null && _previewBox.Image != null) @@ -43,6 +46,7 @@ namespace mbn_stock base.OnFormClosed(e); } + // 국내 증시 업종 테스트용 기본 행을 그리드에 채웁니다. private void LoadDefaultItems() { _loading = true; @@ -81,6 +85,7 @@ namespace mbn_stock _loading = false; } + // 렌더러가 제공하는 기본 등락률 색상 구간을 그리드에 채웁니다. private void LoadDefaultColorRules() { _loading = true; @@ -95,6 +100,7 @@ namespace mbn_stock _colorRuleGrid.Refresh(); } + // 각 업종의 시가총액과 등락률을 테스트용 임의 값으로 갱신합니다. private void RandomizeValues() { _loading = true; @@ -110,6 +116,7 @@ namespace mbn_stock RefreshPreview(); } + // 현재 업종 데이터와 색상 규칙으로 1920x1080 히트맵 미리보기를 다시 그립니다. private void RefreshPreview() { if (_loading || _previewBox == null) @@ -143,11 +150,13 @@ namespace mbn_stock _colorRules.Count); } + // 랜덤 값 버튼 클릭 시 테스트 데이터를 다시 생성합니다. private void RandomButton_Click(object sender, EventArgs e) { RandomizeValues(); } + // PNG 저장 버튼 클릭 시 현재 설정으로 이미지를 렌더링해 파일로 저장합니다. private void SaveButton_Click(object sender, EventArgs e) { if (!_grid.EndEdit()) @@ -172,21 +181,24 @@ namespace mbn_stock bitmap.Save(dialog.FileName, ImageFormat.Png); } - _statusLabel.Text = "저장 완료: " + dialog.FileName; + _statusLabel.Text = "저장 완료: " + dialog.FileName; } } + // 기본 색상 버튼 클릭 시 등락률 색상 규칙을 초기값으로 되돌립니다. private void ResetColorRulesButton_Click(object sender, EventArgs e) { LoadDefaultColorRules(); RefreshPreview(); } + // 업종 그리드 편집이 끝나면 미리보기를 갱신합니다. private void Grid_CellEndEdit(object sender, DataGridViewCellEventArgs e) { RefreshPreview(); } + // 업종 그리드의 숫자 입력값을 decimal로 변환합니다. private void Grid_CellParsing(object sender, DataGridViewCellParsingEventArgs e) { if (_grid.Columns[e.ColumnIndex].DataPropertyName == "Name") @@ -204,6 +216,7 @@ namespace mbn_stock } } + // 업종 그리드에 잘못된 숫자나 0 이하 시가총액이 들어오지 않게 검증합니다. private void Grid_CellValidating(object sender, DataGridViewCellValidatingEventArgs e) { if (_grid.Columns[e.ColumnIndex].DataPropertyName == "Name") @@ -231,12 +244,14 @@ namespace mbn_stock _grid.Rows[e.RowIndex].ErrorText = string.Empty; } + // 업종 그리드 바인딩 오류가 앱 예외로 번지지 않도록 상태 메시지로 처리합니다. private void Grid_DataError(object sender, DataGridViewDataErrorEventArgs e) { e.ThrowException = false; _statusLabel.Text = "입력값을 확인해 주세요."; } + // 색상 값 또는 미리보기 칸을 더블클릭하면 색상 선택 창을 엽니다. private void ColorRuleGrid_CellDoubleClick(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0 || @@ -268,12 +283,14 @@ namespace mbn_stock } } + // 색상 규칙 편집이 끝나면 표 색상과 미리보기를 갱신합니다. private void ColorRuleGrid_CellEndEdit(object sender, DataGridViewCellEventArgs e) { _colorRuleGrid.Invalidate(); RefreshPreview(); } + // 색상 미리보기 칸을 실제 색상 배경으로 표시합니다. private void ColorRuleGrid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (e.RowIndex < 0 || _colorRuleGrid.Columns[e.ColumnIndex] != _colorPreviewColumn) @@ -293,6 +310,7 @@ namespace mbn_stock e.FormattingApplied = true; } + // 색상 규칙 그리드의 숫자와 색상 문자열을 모델 값으로 변환합니다. private void ColorRuleGrid_CellParsing(object sender, DataGridViewCellParsingEventArgs e) { string propertyName = _colorRuleGrid.Columns[e.ColumnIndex].DataPropertyName; @@ -315,6 +333,7 @@ namespace mbn_stock } } + // 색상 규칙 구간값과 #RRGGBB 색상 형식을 검증합니다. private void ColorRuleGrid_CellValidating(object sender, DataGridViewCellValidatingEventArgs e) { string propertyName = _colorRuleGrid.Columns[e.ColumnIndex].DataPropertyName; @@ -344,12 +363,14 @@ namespace mbn_stock _colorRuleGrid.Rows[e.RowIndex].ErrorText = string.Empty; } + // 색상 규칙 바인딩 오류가 앱 예외로 번지지 않도록 상태 메시지로 처리합니다. private void ColorRuleGrid_DataError(object sender, DataGridViewDataErrorEventArgs e) { e.ThrowException = false; _statusLabel.Text = "색상 규칙 입력값을 확인해 주세요."; } + // 새 색상 규칙 행을 추가할 때 기본값을 넣습니다. private void ColorRuleGrid_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e) { e.Row.Cells[_fromRateColumn.Index].Value = 0m; @@ -357,11 +378,13 @@ namespace mbn_stock e.Row.Cells[_colorHexColumn.Index].Value = "#808080"; } + // 색상 규칙 행이 삭제되면 남은 규칙으로 미리보기를 다시 그립니다. private void ColorRuleGrid_RowsRemoved(object sender, DataGridViewRowsRemovedEventArgs e) { RefreshPreview(); } + // 퍼센트 기호가 붙은 문자열도 decimal 숫자로 읽을 수 있게 변환합니다. private static bool TryParseDecimal(string text, out decimal value) { value = 0m; @@ -376,6 +399,7 @@ namespace mbn_stock || decimal.TryParse(normalized, NumberStyles.Number, CultureInfo.InvariantCulture, out value); } + // ColorDialog에서 선택한 색상을 #RRGGBB 문자열로 변환합니다. private static string ToHex(Color color) { return string.Format(CultureInfo.InvariantCulture, "#{0:X2}{1:X2}{2:X2}", color.R, color.G, color.B); diff --git a/mbn_stock/HeatMapRenderer.cs b/mbn_stock/HeatMapRenderer.cs index a1c8b24..bf1588f 100644 --- a/mbn_stock/HeatMapRenderer.cs +++ b/mbn_stock/HeatMapRenderer.cs @@ -9,6 +9,7 @@ using System.Linq; namespace mbn_stock { + // 업종별 데이터를 시가총액 면적 기반 트리맵 이미지로 렌더링하는 정적 유틸리티입니다. public static class HeatMapRenderer { public const int ExportWidth = 1920; @@ -18,21 +19,25 @@ namespace mbn_stock private const float TileGap = 6f; private static readonly float TileRadius = 0f; + // 기본 이미지 크기와 기본 색상 규칙으로 히트맵을 렌더링합니다. public static Bitmap Render(IEnumerable sourceItems) { return Render(sourceItems, new Size(ExportWidth, ExportHeight), CreateDefaultColorRules()); } + // 기본 이미지 크기에 사용자가 넘긴 색상 규칙을 적용해 히트맵을 렌더링합니다. public static Bitmap Render(IEnumerable sourceItems, IEnumerable colorRules) { return Render(sourceItems, new Size(ExportWidth, ExportHeight), colorRules); } + // 지정한 이미지 크기와 기본 색상 규칙으로 히트맵을 렌더링합니다. public static Bitmap Render(IEnumerable sourceItems, Size size) { return Render(sourceItems, size, CreateDefaultColorRules()); } + // 지정한 이미지 크기와 색상 규칙으로 전체 히트맵 Bitmap을 생성합니다. public static Bitmap Render(IEnumerable sourceItems, Size size, IEnumerable colorRules) { Bitmap bitmap = new Bitmap(size.Width, size.Height, PixelFormat.Format32bppArgb); @@ -65,6 +70,7 @@ namespace mbn_stock return bitmap; } + // 초기 화면에서 사용할 기본 등락률 구간 색상 규칙을 만듭니다. public static List CreateDefaultColorRules() { return new List @@ -78,6 +84,7 @@ namespace mbn_stock }; } + // 기본 색상 규칙 목록에 넣을 한 구간을 생성합니다. private static ColorRangeRule CreateColorRule(decimal fromRate, decimal toRate, string colorHex) { return new ColorRangeRule @@ -88,6 +95,7 @@ namespace mbn_stock }; } + // 시가총액 비중을 전체 캔버스 면적으로 환산해 배치 노드를 만듭니다. private static List BuildNodes(IList items, RectangleF bounds) { decimal total = items.Sum(item => item.MarketCap); @@ -105,6 +113,7 @@ namespace mbn_stock return result; } + // 노드 목록을 면적 균형이 맞도록 좌우 또는 상하로 재귀 분할합니다. private static void LayoutBinary(IList nodes, RectangleF bounds, List result) { if (nodes.Count == 0) @@ -145,6 +154,7 @@ namespace mbn_stock LayoutBinary(secondGroup, bottomBounds, result); } + // 현재 노드 목록을 두 그룹으로 나눌 가장 균형 잡힌 지점을 찾습니다. private static int FindBalancedSplit(IList nodes, double totalArea) { double target = totalArea / 2d; @@ -167,6 +177,7 @@ namespace mbn_stock return splitIndex; } + // 계산된 각 사각형 영역을 색상으로 채우고 텍스트를 그립니다. private static void DrawTiles(Graphics graphics, IList nodes, IList colorRules) { foreach (TreeMapNode node in nodes) @@ -200,6 +211,7 @@ namespace mbn_stock } } + // 업종명과 등락률을 사각형 중앙에 흰색 텍스트로 그립니다. private static void DrawTileText(Graphics graphics, SectorHeatMapItem item, RectangleF tileBounds, Color fill) { RectangleF textBounds = Inset(tileBounds, Math.Max(10f, Math.Min(tileBounds.Width, tileBounds.Height) * 0.07f)); @@ -235,6 +247,7 @@ namespace mbn_stock } } + // 업종명과 등락률이 사각형 안에 들어가도록 최대 폰트 크기를 찾습니다. private static float FindFittingFontSize(Graphics graphics, string name, string rateText, RectangleF bounds) { float maxSize = Math.Min(96f, Math.Max(12f, Math.Min(bounds.Width * 0.18f, bounds.Height * 0.34f))); @@ -260,6 +273,7 @@ namespace mbn_stock return 8f; } + // 색상 규칙에 맞는 채움색을 찾고, 없으면 기본 빨강/파랑 계산으로 대체합니다. private static Color GetFillColor(decimal changeRate, IList colorRules) { if (colorRules != null && colorRules.Count > 0) @@ -274,6 +288,7 @@ namespace mbn_stock return GetDefaultFillColor(changeRate); } + // 등락률이 속한 색상 규칙을 찾고 범위를 벗어나면 양 끝 색상으로 보정합니다. private static bool TryGetRuleColor(decimal changeRate, IList colorRules, out Color color) { color = Color.Empty; @@ -306,6 +321,7 @@ namespace mbn_stock return false; } + // 색상 규칙이 없을 때 ±30% 기준으로 빨강/파랑 농도를 계산합니다. private static Color GetDefaultFillColor(decimal changeRate) { double intensity = Math.Min(1d, Math.Abs(decimal.ToDouble(changeRate)) / decimal.ToDouble(MaxRatePercent)); @@ -323,6 +339,7 @@ namespace mbn_stock return Color.FromArgb(226, 228, 232); } + // 비어 있거나 잘못된 색상 규칙을 제거하고 등락률 시작값 기준으로 정렬합니다. private static List NormalizeColorRules(IEnumerable colorRules) { List rules = colorRules == null @@ -336,12 +353,14 @@ namespace mbn_stock return rules.Count == 0 ? CreateDefaultColorRules() : rules; } + // 색상 규칙이 실제 렌더링에 사용할 수 있는 색상인지 확인합니다. private static bool IsValidColorRule(ColorRangeRule rule) { Color color; return ColorRangeRule.TryParseColor(rule.ColorHex, out color); } + // 두 색상 사이를 amount 비율로 보간해 중간 색상을 만듭니다. private static Color Interpolate(Color from, Color to, double amount) { amount = Math.Max(0d, Math.Min(1d, amount)); @@ -351,6 +370,7 @@ namespace mbn_stock return Color.FromArgb(r, g, b); } + // 등락률 숫자를 +1.23% 또는 -1.23% 형태의 문자열로 바꿉니다. private static string FormatRate(decimal value) { string format = value > 0m ? "+0.00'%'" @@ -360,6 +380,7 @@ namespace mbn_stock return value.ToString(format, CultureInfo.InvariantCulture); } + // 사각형 안쪽에 여백을 적용한 새 영역을 계산합니다. private static RectangleF Inset(RectangleF rectangle, float padding) { float horizontal = Math.Min(padding, rectangle.Width / 2f - 0.5f); @@ -382,6 +403,7 @@ namespace mbn_stock Math.Max(1f, rectangle.Height - vertical * 2f)); } + // 둥근 모서리 옵션을 사용할 때 필요한 사각형 경로를 만듭니다. private static GraphicsPath CreateRoundedRectangle(RectangleF bounds, float radius) { float diameter = radius * 2f; @@ -394,6 +416,7 @@ namespace mbn_stock return path; } + // 렌더링 가능한 업종 데이터가 없을 때 안내 문구를 그립니다. private static void DrawEmptyMessage(Graphics graphics, RectangleF bounds) { using (Font font = new Font("Malgun Gothic", 40f, FontStyle.Bold, GraphicsUnit.Pixel)) @@ -406,8 +429,10 @@ namespace mbn_stock } } + // 트리맵 배치 중 한 업종의 면적과 최종 사각형 영역을 함께 보관합니다. private sealed class TreeMapNode { + // 업종 데이터와 비율 기반 면적을 묶어 배치 노드를 만듭니다. public TreeMapNode(SectorHeatMapItem item, double area) { Item = item; diff --git a/mbn_stock/Program.cs b/mbn_stock/Program.cs index 95849dc..f3a16cc 100644 --- a/mbn_stock/Program.cs +++ b/mbn_stock/Program.cs @@ -6,11 +6,10 @@ using System.Windows.Forms; namespace mbn_stock { + // WinForms 애플리케이션을 시작하고 메인 폼을 여는 진입점입니다. static class Program { - /// - /// 해당 애플리케이션의 주 진입점입니다. - /// + // 애플리케이션 기본 스타일을 설정한 뒤 Form1을 실행합니다. [STAThread] static void Main() { diff --git a/mbn_stock/SectorHeatMapItem.cs b/mbn_stock/SectorHeatMapItem.cs index 458abaf..4753cfb 100644 --- a/mbn_stock/SectorHeatMapItem.cs +++ b/mbn_stock/SectorHeatMapItem.cs @@ -2,6 +2,7 @@ using System.ComponentModel; namespace mbn_stock { + // 업종명, 시가총액, 등락률을 한 행으로 보관하는 히트맵 입력 모델입니다. public class SectorHeatMapItem : INotifyPropertyChanged { private string _name; @@ -55,6 +56,7 @@ namespace mbn_stock } } + // DataGridView 바인딩에 변경 사실을 알려 화면을 갱신합니다. private void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged;