Add explanatory comments to heatmap code

This commit is contained in:
2026-07-01 10:09:24 +09:00
parent 74306da88b
commit 0d36c11ea4
5 changed files with 59 additions and 4 deletions

View File

@@ -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;

View File

@@ -9,6 +9,7 @@ using System.Windows.Forms;
namespace mbn_stock
{
// 업종 데이터와 색상 규칙을 편집하고 히트맵 이미지를 미리보기/저장하는 테스트 폼입니다.
public partial class Form1 : Form
{
private readonly BindingList<SectorHeatMapItem> _items = new BindingList<SectorHeatMapItem>();
@@ -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);

View File

@@ -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<SectorHeatMapItem> sourceItems)
{
return Render(sourceItems, new Size(ExportWidth, ExportHeight), CreateDefaultColorRules());
}
// 기본 이미지 크기에 사용자가 넘긴 색상 규칙을 적용해 히트맵을 렌더링합니다.
public static Bitmap Render(IEnumerable<SectorHeatMapItem> sourceItems, IEnumerable<ColorRangeRule> colorRules)
{
return Render(sourceItems, new Size(ExportWidth, ExportHeight), colorRules);
}
// 지정한 이미지 크기와 기본 색상 규칙으로 히트맵을 렌더링합니다.
public static Bitmap Render(IEnumerable<SectorHeatMapItem> sourceItems, Size size)
{
return Render(sourceItems, size, CreateDefaultColorRules());
}
// 지정한 이미지 크기와 색상 규칙으로 전체 히트맵 Bitmap을 생성합니다.
public static Bitmap Render(IEnumerable<SectorHeatMapItem> sourceItems, Size size, IEnumerable<ColorRangeRule> colorRules)
{
Bitmap bitmap = new Bitmap(size.Width, size.Height, PixelFormat.Format32bppArgb);
@@ -65,6 +70,7 @@ namespace mbn_stock
return bitmap;
}
// 초기 화면에서 사용할 기본 등락률 구간 색상 규칙을 만듭니다.
public static List<ColorRangeRule> CreateDefaultColorRules()
{
return new List<ColorRangeRule>
@@ -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<TreeMapNode> BuildNodes(IList<SectorHeatMapItem> items, RectangleF bounds)
{
decimal total = items.Sum(item => item.MarketCap);
@@ -105,6 +113,7 @@ namespace mbn_stock
return result;
}
// 노드 목록을 면적 균형이 맞도록 좌우 또는 상하로 재귀 분할합니다.
private static void LayoutBinary(IList<TreeMapNode> nodes, RectangleF bounds, List<TreeMapNode> result)
{
if (nodes.Count == 0)
@@ -145,6 +154,7 @@ namespace mbn_stock
LayoutBinary(secondGroup, bottomBounds, result);
}
// 현재 노드 목록을 두 그룹으로 나눌 가장 균형 잡힌 지점을 찾습니다.
private static int FindBalancedSplit(IList<TreeMapNode> nodes, double totalArea)
{
double target = totalArea / 2d;
@@ -167,6 +177,7 @@ namespace mbn_stock
return splitIndex;
}
// 계산된 각 사각형 영역을 색상으로 채우고 텍스트를 그립니다.
private static void DrawTiles(Graphics graphics, IList<TreeMapNode> nodes, IList<ColorRangeRule> 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<ColorRangeRule> colorRules)
{
if (colorRules != null && colorRules.Count > 0)
@@ -274,6 +288,7 @@ namespace mbn_stock
return GetDefaultFillColor(changeRate);
}
// 등락률이 속한 색상 규칙을 찾고 범위를 벗어나면 양 끝 색상으로 보정합니다.
private static bool TryGetRuleColor(decimal changeRate, IList<ColorRangeRule> 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<ColorRangeRule> NormalizeColorRules(IEnumerable<ColorRangeRule> colorRules)
{
List<ColorRangeRule> 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;

View File

@@ -6,11 +6,10 @@ using System.Windows.Forms;
namespace mbn_stock
{
// WinForms 애플리케이션을 시작하고 메인 폼을 여는 진입점입니다.
static class Program
{
/// <summary>
/// 해당 애플리케이션의 주 진입점입니다.
/// </summary>
// 애플리케이션 기본 스타일을 설정한 뒤 Form1을 실행합니다.
[STAThread]
static void Main()
{

View File

@@ -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;