Add configurable heatmap color ranges

This commit is contained in:
2026-06-30 23:41:41 +09:00
parent 46e3c22431
commit 74306da88b
5 changed files with 590 additions and 49 deletions

141
mbn_stock/ColorRangeRule.cs Normal file
View File

@@ -0,0 +1,141 @@
using System;
using System.ComponentModel;
using System.Drawing;
using System.Globalization;
namespace mbn_stock
{
public class ColorRangeRule : INotifyPropertyChanged
{
private decimal _fromRate;
private decimal _toRate;
private string _colorHex = "#808080";
public event PropertyChangedEventHandler PropertyChanged;
public decimal FromRate
{
get { return _fromRate; }
set
{
if (_fromRate == value)
{
return;
}
_fromRate = value;
OnPropertyChanged("FromRate");
}
}
public decimal ToRate
{
get { return _toRate; }
set
{
if (_toRate == value)
{
return;
}
_toRate = value;
OnPropertyChanged("ToRate");
}
}
public string ColorHex
{
get { return _colorHex; }
set
{
string normalized = NormalizeColorHex(value);
if (_colorHex == normalized)
{
return;
}
_colorHex = normalized;
OnPropertyChanged("ColorHex");
}
}
public decimal MinRate
{
get { return Math.Min(FromRate, ToRate); }
}
public decimal MaxRate
{
get { return Math.Max(FromRate, ToRate); }
}
public Color DrawingColor
{
get
{
Color color;
return TryParseColor(ColorHex, out color) ? color : Color.Gray;
}
}
public bool Contains(decimal value)
{
return value >= MinRate && value < MaxRate;
}
public static bool TryParseColor(string text, out Color color)
{
color = Color.Empty;
try
{
string normalized = NormalizeColorHex(text);
if (string.IsNullOrEmpty(normalized))
{
return false;
}
color = ColorTranslator.FromHtml(normalized);
return true;
}
catch (Exception)
{
return false;
}
}
public static string NormalizeColorHex(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return "#808080";
}
string normalized = text.Trim();
if (!normalized.StartsWith("#", StringComparison.Ordinal))
{
normalized = "#" + normalized;
}
if (normalized.Length == 7)
{
int value;
if (int.TryParse(normalized.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value))
{
return normalized.ToUpperInvariant();
}
}
return normalized;
}
private void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}

View File

@@ -32,12 +32,25 @@ namespace mbn_stock
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle3 = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle4 = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle5 = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle6 = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle7 = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle8 = new System.Windows.Forms.DataGridViewCellStyle();
this._splitContainer = new System.Windows.Forms.SplitContainer();
this._leftPanel = new System.Windows.Forms.Panel();
this._grid = new System.Windows.Forms.DataGridView();
this._nameColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this._capColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this._rateColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this._colorRulePanel = new System.Windows.Forms.Panel();
this._colorRuleGrid = new System.Windows.Forms.DataGridView();
this._fromRateColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this._toRateColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this._colorHexColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this._colorPreviewColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this._colorRuleHeaderPanel = new System.Windows.Forms.Panel();
this._colorRuleTitleLabel = new System.Windows.Forms.Label();
this._resetColorRulesButton = new System.Windows.Forms.Button();
this._toolbar = new System.Windows.Forms.FlowLayoutPanel();
this._randomButton = new System.Windows.Forms.Button();
this._saveButton = new System.Windows.Forms.Button();
@@ -50,36 +63,40 @@ namespace mbn_stock
this._splitContainer.SuspendLayout();
this._leftPanel.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this._grid)).BeginInit();
this._colorRulePanel.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this._colorRuleGrid)).BeginInit();
this._colorRuleHeaderPanel.SuspendLayout();
this._toolbar.SuspendLayout();
this._previewPanel.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this._previewBox)).BeginInit();
this.SuspendLayout();
//
//
// _splitContainer
//
//
this._splitContainer.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(210)))), ((int)(((byte)(214)))), ((int)(((byte)(220)))));
this._splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
this._splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel1;
this._splitContainer.Location = new System.Drawing.Point(0, 0);
this._splitContainer.Name = "_splitContainer";
//
//
// _splitContainer.Panel1
//
//
this._splitContainer.Panel1.Controls.Add(this._leftPanel);
this._splitContainer.Panel1MinSize = 420;
//
//
// _splitContainer.Panel2
//
//
this._splitContainer.Panel2.Controls.Add(this._previewPanel);
this._splitContainer.Panel2MinSize = 520;
this._splitContainer.Size = new System.Drawing.Size(1280, 760);
this._splitContainer.SplitterDistance = 500;
this._splitContainer.TabIndex = 0;
//
//
// _leftPanel
//
//
this._leftPanel.BackColor = System.Drawing.Color.White;
this._leftPanel.Controls.Add(this._grid);
this._leftPanel.Controls.Add(this._colorRulePanel);
this._leftPanel.Controls.Add(this._toolbar);
this._leftPanel.Controls.Add(this._statusLabel);
this._leftPanel.Dock = System.Windows.Forms.DockStyle.Fill;
@@ -88,9 +105,9 @@ namespace mbn_stock
this._leftPanel.Padding = new System.Windows.Forms.Padding(14);
this._leftPanel.Size = new System.Drawing.Size(500, 760);
this._leftPanel.TabIndex = 0;
//
//
// _grid
//
//
this._grid.AllowUserToAddRows = false;
this._grid.AllowUserToDeleteRows = false;
this._grid.AllowUserToResizeRows = false;
@@ -122,24 +139,24 @@ namespace mbn_stock
this._grid.RowHeadersVisible = false;
this._grid.RowTemplate.Height = 23;
this._grid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.CellSelect;
this._grid.Size = new System.Drawing.Size(472, 656);
this._grid.Size = new System.Drawing.Size(472, 446);
this._grid.TabIndex = 1;
this._grid.CellEndEdit += new System.Windows.Forms.DataGridViewCellEventHandler(this.Grid_CellEndEdit);
this._grid.CellParsing += new System.Windows.Forms.DataGridViewCellParsingEventHandler(this.Grid_CellParsing);
this._grid.CellValidating += new System.Windows.Forms.DataGridViewCellValidatingEventHandler(this.Grid_CellValidating);
this._grid.DataError += new System.Windows.Forms.DataGridViewDataErrorEventHandler(this.Grid_DataError);
//
//
// _nameColumn
//
//
this._nameColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this._nameColumn.DataPropertyName = "Name";
this._nameColumn.FillWeight = 130F;
this._nameColumn.HeaderText = "업종";
this._nameColumn.Name = "_nameColumn";
this._nameColumn.ReadOnly = true;
//
//
// _capColumn
//
//
this._capColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this._capColumn.DataPropertyName = "MarketCap";
dataGridViewCellStyle3.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleRight;
@@ -148,9 +165,9 @@ namespace mbn_stock
this._capColumn.FillWeight = 90F;
this._capColumn.HeaderText = "시가총액";
this._capColumn.Name = "_capColumn";
//
//
// _rateColumn
//
//
this._rateColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this._rateColumn.DataPropertyName = "ChangeRate";
dataGridViewCellStyle4.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleRight;
@@ -159,9 +176,134 @@ namespace mbn_stock
this._rateColumn.FillWeight = 90F;
this._rateColumn.HeaderText = "등락률(%)";
this._rateColumn.Name = "_rateColumn";
//
//
// _colorRulePanel
//
this._colorRulePanel.Controls.Add(this._colorRuleGrid);
this._colorRulePanel.Controls.Add(this._colorRuleHeaderPanel);
this._colorRulePanel.Dock = System.Windows.Forms.DockStyle.Bottom;
this._colorRulePanel.Location = new System.Drawing.Point(14, 506);
this._colorRulePanel.Name = "_colorRulePanel";
this._colorRulePanel.Padding = new System.Windows.Forms.Padding(0, 8, 0, 0);
this._colorRulePanel.Size = new System.Drawing.Size(472, 210);
this._colorRulePanel.TabIndex = 2;
//
// _colorRuleGrid
//
this._colorRuleGrid.AllowUserToResizeRows = false;
this._colorRuleGrid.AutoGenerateColumns = false;
this._colorRuleGrid.BackgroundColor = System.Drawing.Color.White;
this._colorRuleGrid.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
dataGridViewCellStyle5.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(238)))), ((int)(((byte)(241)))), ((int)(((byte)(245)))));
dataGridViewCellStyle5.Font = new System.Drawing.Font("Malgun Gothic", 9.5F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
dataGridViewCellStyle5.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(30)))), ((int)(((byte)(35)))), ((int)(((byte)(45)))));
this._colorRuleGrid.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle5;
this._colorRuleGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this._colorRuleGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this._fromRateColumn,
this._toRateColumn,
this._colorHexColumn,
this._colorPreviewColumn});
dataGridViewCellStyle6.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle6.BackColor = System.Drawing.SystemColors.Window;
dataGridViewCellStyle6.Font = new System.Drawing.Font("Malgun Gothic", 9.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
dataGridViewCellStyle6.ForeColor = System.Drawing.SystemColors.ControlText;
dataGridViewCellStyle6.SelectionBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(218)))), ((int)(((byte)(232)))), ((int)(((byte)(255)))));
dataGridViewCellStyle6.SelectionForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(20)))), ((int)(((byte)(24)))), ((int)(((byte)(32)))));
dataGridViewCellStyle6.WrapMode = System.Windows.Forms.DataGridViewTriState.False;
this._colorRuleGrid.DefaultCellStyle = dataGridViewCellStyle6;
this._colorRuleGrid.Dock = System.Windows.Forms.DockStyle.Fill;
this._colorRuleGrid.EnableHeadersVisualStyles = false;
this._colorRuleGrid.Location = new System.Drawing.Point(0, 40);
this._colorRuleGrid.MultiSelect = false;
this._colorRuleGrid.Name = "_colorRuleGrid";
this._colorRuleGrid.RowHeadersVisible = false;
this._colorRuleGrid.RowTemplate.Height = 23;
this._colorRuleGrid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.CellSelect;
this._colorRuleGrid.Size = new System.Drawing.Size(472, 170);
this._colorRuleGrid.TabIndex = 1;
this._colorRuleGrid.CellDoubleClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.ColorRuleGrid_CellDoubleClick);
this._colorRuleGrid.CellEndEdit += new System.Windows.Forms.DataGridViewCellEventHandler(this.ColorRuleGrid_CellEndEdit);
this._colorRuleGrid.CellFormatting += new System.Windows.Forms.DataGridViewCellFormattingEventHandler(this.ColorRuleGrid_CellFormatting);
this._colorRuleGrid.CellParsing += new System.Windows.Forms.DataGridViewCellParsingEventHandler(this.ColorRuleGrid_CellParsing);
this._colorRuleGrid.CellValidating += new System.Windows.Forms.DataGridViewCellValidatingEventHandler(this.ColorRuleGrid_CellValidating);
this._colorRuleGrid.DataError += new System.Windows.Forms.DataGridViewDataErrorEventHandler(this.ColorRuleGrid_DataError);
this._colorRuleGrid.DefaultValuesNeeded += new System.Windows.Forms.DataGridViewRowEventHandler(this.ColorRuleGrid_DefaultValuesNeeded);
this._colorRuleGrid.RowsRemoved += new System.Windows.Forms.DataGridViewRowsRemovedEventHandler(this.ColorRuleGrid_RowsRemoved);
//
// _fromRateColumn
//
this._fromRateColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this._fromRateColumn.DataPropertyName = "FromRate";
dataGridViewCellStyle7.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleRight;
dataGridViewCellStyle7.Format = "0.##";
this._fromRateColumn.DefaultCellStyle = dataGridViewCellStyle7;
this._fromRateColumn.FillWeight = 60F;
this._fromRateColumn.HeaderText = "시작%";
this._fromRateColumn.Name = "_fromRateColumn";
//
// _toRateColumn
//
this._toRateColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this._toRateColumn.DataPropertyName = "ToRate";
dataGridViewCellStyle8.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleRight;
dataGridViewCellStyle8.Format = "0.##";
this._toRateColumn.DefaultCellStyle = dataGridViewCellStyle8;
this._toRateColumn.FillWeight = 60F;
this._toRateColumn.HeaderText = "종료%";
this._toRateColumn.Name = "_toRateColumn";
//
// _colorHexColumn
//
this._colorHexColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this._colorHexColumn.DataPropertyName = "ColorHex";
this._colorHexColumn.FillWeight = 82F;
this._colorHexColumn.HeaderText = "색상";
this._colorHexColumn.Name = "_colorHexColumn";
//
// _colorPreviewColumn
//
this._colorPreviewColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this._colorPreviewColumn.FillWeight = 38F;
this._colorPreviewColumn.HeaderText = "보기";
this._colorPreviewColumn.Name = "_colorPreviewColumn";
this._colorPreviewColumn.ReadOnly = true;
//
// _colorRuleHeaderPanel
//
this._colorRuleHeaderPanel.Controls.Add(this._colorRuleTitleLabel);
this._colorRuleHeaderPanel.Controls.Add(this._resetColorRulesButton);
this._colorRuleHeaderPanel.Dock = System.Windows.Forms.DockStyle.Top;
this._colorRuleHeaderPanel.Location = new System.Drawing.Point(0, 8);
this._colorRuleHeaderPanel.Name = "_colorRuleHeaderPanel";
this._colorRuleHeaderPanel.Size = new System.Drawing.Size(472, 32);
this._colorRuleHeaderPanel.TabIndex = 0;
//
// _colorRuleTitleLabel
//
this._colorRuleTitleLabel.Dock = System.Windows.Forms.DockStyle.Fill;
this._colorRuleTitleLabel.Font = new System.Drawing.Font("Malgun Gothic", 9.5F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
this._colorRuleTitleLabel.Location = new System.Drawing.Point(0, 0);
this._colorRuleTitleLabel.Name = "_colorRuleTitleLabel";
this._colorRuleTitleLabel.Size = new System.Drawing.Size(362, 32);
this._colorRuleTitleLabel.TabIndex = 0;
this._colorRuleTitleLabel.Text = "등락률 색상 규칙";
this._colorRuleTitleLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// _resetColorRulesButton
//
this._resetColorRulesButton.Dock = System.Windows.Forms.DockStyle.Right;
this._resetColorRulesButton.Location = new System.Drawing.Point(362, 0);
this._resetColorRulesButton.Name = "_resetColorRulesButton";
this._resetColorRulesButton.Size = new System.Drawing.Size(110, 32);
this._resetColorRulesButton.TabIndex = 1;
this._resetColorRulesButton.Text = "기본 색상";
this._resetColorRulesButton.UseVisualStyleBackColor = true;
this._resetColorRulesButton.Click += new System.EventHandler(this.ResetColorRulesButton_Click);
//
//
// _toolbar
//
//
this._toolbar.Controls.Add(this._randomButton);
this._toolbar.Controls.Add(this._saveButton);
this._toolbar.Dock = System.Windows.Forms.DockStyle.Top;
@@ -171,9 +313,9 @@ namespace mbn_stock
this._toolbar.Size = new System.Drawing.Size(472, 46);
this._toolbar.TabIndex = 0;
this._toolbar.WrapContents = false;
//
//
// _randomButton
//
//
this._randomButton.Location = new System.Drawing.Point(3, 3);
this._randomButton.Name = "_randomButton";
this._randomButton.Size = new System.Drawing.Size(110, 32);
@@ -181,9 +323,9 @@ namespace mbn_stock
this._randomButton.Text = "랜덤 값";
this._randomButton.UseVisualStyleBackColor = true;
this._randomButton.Click += new System.EventHandler(this.RandomButton_Click);
//
//
// _saveButton
//
//
this._saveButton.Location = new System.Drawing.Point(119, 3);
this._saveButton.Name = "_saveButton";
this._saveButton.Size = new System.Drawing.Size(110, 32);
@@ -191,9 +333,9 @@ namespace mbn_stock
this._saveButton.Text = "PNG 저장";
this._saveButton.UseVisualStyleBackColor = true;
this._saveButton.Click += new System.EventHandler(this.SaveButton_Click);
//
//
// _statusLabel
//
//
this._statusLabel.Dock = System.Windows.Forms.DockStyle.Bottom;
this._statusLabel.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(80)))), ((int)(((byte)(86)))), ((int)(((byte)(96)))));
this._statusLabel.Location = new System.Drawing.Point(14, 716);
@@ -201,9 +343,9 @@ namespace mbn_stock
this._statusLabel.Size = new System.Drawing.Size(472, 30);
this._statusLabel.TabIndex = 2;
this._statusLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
//
// _previewPanel
//
//
this._previewPanel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(29)))), ((int)(((byte)(32)))), ((int)(((byte)(39)))));
this._previewPanel.Controls.Add(this._previewBox);
this._previewPanel.Dock = System.Windows.Forms.DockStyle.Fill;
@@ -212,9 +354,9 @@ namespace mbn_stock
this._previewPanel.Padding = new System.Windows.Forms.Padding(14);
this._previewPanel.Size = new System.Drawing.Size(776, 760);
this._previewPanel.TabIndex = 0;
//
//
// _previewBox
//
//
this._previewBox.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(20)))), ((int)(((byte)(22)))), ((int)(((byte)(28)))));
this._previewBox.Dock = System.Windows.Forms.DockStyle.Fill;
this._previewBox.Location = new System.Drawing.Point(14, 14);
@@ -223,9 +365,9 @@ namespace mbn_stock
this._previewBox.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
this._previewBox.TabIndex = 0;
this._previewBox.TabStop = false;
//
//
// Form1
//
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 17F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(245)))), ((int)(((byte)(246)))), ((int)(((byte)(248)))));
@@ -241,6 +383,9 @@ namespace mbn_stock
this._splitContainer.ResumeLayout(false);
this._leftPanel.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this._grid)).EndInit();
this._colorRulePanel.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this._colorRuleGrid)).EndInit();
this._colorRuleHeaderPanel.ResumeLayout(false);
this._toolbar.ResumeLayout(false);
this._previewPanel.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this._previewBox)).EndInit();
@@ -256,6 +401,15 @@ namespace mbn_stock
private System.Windows.Forms.DataGridViewTextBoxColumn _nameColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn _capColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn _rateColumn;
private System.Windows.Forms.Panel _colorRulePanel;
private System.Windows.Forms.DataGridView _colorRuleGrid;
private System.Windows.Forms.DataGridViewTextBoxColumn _fromRateColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn _toRateColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn _colorHexColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn _colorPreviewColumn;
private System.Windows.Forms.Panel _colorRuleHeaderPanel;
private System.Windows.Forms.Label _colorRuleTitleLabel;
private System.Windows.Forms.Button _resetColorRulesButton;
private System.Windows.Forms.FlowLayoutPanel _toolbar;
private System.Windows.Forms.Button _randomButton;
private System.Windows.Forms.Button _saveButton;

View File

@@ -12,6 +12,7 @@ namespace mbn_stock
public partial class Form1 : Form
{
private readonly BindingList<SectorHeatMapItem> _items = new BindingList<SectorHeatMapItem>();
private readonly BindingList<ColorRangeRule> _colorRules = new BindingList<ColorRangeRule>();
private readonly Random _random = new Random();
private bool _loading;
@@ -25,7 +26,9 @@ namespace mbn_stock
}
_grid.DataSource = _items;
_colorRuleGrid.DataSource = _colorRules;
LoadDefaultItems();
LoadDefaultColorRules();
RandomizeValues();
}
@@ -78,6 +81,20 @@ namespace mbn_stock
_loading = false;
}
private void LoadDefaultColorRules()
{
_loading = true;
_colorRules.Clear();
foreach (ColorRangeRule rule in HeatMapRenderer.CreateDefaultColorRules())
{
_colorRules.Add(rule);
}
_loading = false;
_colorRuleGrid.Refresh();
}
private void RandomizeValues()
{
_loading = true;
@@ -109,7 +126,7 @@ namespace mbn_stock
})
.ToList();
Bitmap newImage = HeatMapRenderer.Render(snapshot);
Bitmap newImage = HeatMapRenderer.Render(snapshot, _colorRules);
Image oldImage = _previewBox.Image;
_previewBox.Image = newImage;
@@ -120,10 +137,10 @@ namespace mbn_stock
_statusLabel.Text = string.Format(
CultureInfo.CurrentCulture,
"출력 {0:N0} x {1:N0}px / 색상 기준 ±{2:N0}%",
"출력 {0:N0} x {1:N0}px / 색상 규칙 {2:N0}",
HeatMapRenderer.ExportWidth,
HeatMapRenderer.ExportHeight,
HeatMapRenderer.MaxRatePercent);
_colorRules.Count);
}
private void RandomButton_Click(object sender, EventArgs e)
@@ -150,15 +167,21 @@ namespace mbn_stock
return;
}
using (Bitmap bitmap = HeatMapRenderer.Render(_items))
using (Bitmap bitmap = HeatMapRenderer.Render(_items, _colorRules))
{
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();
@@ -214,6 +237,131 @@ namespace mbn_stock
_statusLabel.Text = "입력값을 확인해 주세요.";
}
private void ColorRuleGrid_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0 ||
(_colorRuleGrid.Columns[e.ColumnIndex] != _colorHexColumn &&
_colorRuleGrid.Columns[e.ColumnIndex] != _colorPreviewColumn))
{
return;
}
ColorRangeRule rule = _colorRuleGrid.Rows[e.RowIndex].DataBoundItem as ColorRangeRule;
if (rule == null)
{
return;
}
using (ColorDialog dialog = new ColorDialog())
{
dialog.FullOpen = true;
dialog.Color = rule.DrawingColor;
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
rule.ColorHex = ToHex(dialog.Color);
_colorRuleGrid.Refresh();
RefreshPreview();
}
}
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)
{
return;
}
ColorRangeRule rule = _colorRuleGrid.Rows[e.RowIndex].DataBoundItem as ColorRangeRule;
if (rule == null)
{
return;
}
e.Value = string.Empty;
e.CellStyle.BackColor = rule.DrawingColor;
e.CellStyle.SelectionBackColor = rule.DrawingColor;
e.FormattingApplied = true;
}
private void ColorRuleGrid_CellParsing(object sender, DataGridViewCellParsingEventArgs e)
{
string propertyName = _colorRuleGrid.Columns[e.ColumnIndex].DataPropertyName;
if (propertyName == "FromRate" || propertyName == "ToRate")
{
decimal value;
string text = Convert.ToString(e.Value, CultureInfo.CurrentCulture);
if (TryParseDecimal(text, out value))
{
e.Value = value;
e.ParsingApplied = true;
}
}
else if (propertyName == "ColorHex")
{
e.Value = ColorRangeRule.NormalizeColorHex(Convert.ToString(e.Value, CultureInfo.CurrentCulture));
e.ParsingApplied = true;
}
}
private void ColorRuleGrid_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
{
string propertyName = _colorRuleGrid.Columns[e.ColumnIndex].DataPropertyName;
string text = Convert.ToString(e.FormattedValue, CultureInfo.CurrentCulture);
if (propertyName == "FromRate" || propertyName == "ToRate")
{
decimal value;
if (!TryParseDecimal(text, out value))
{
e.Cancel = true;
_colorRuleGrid.Rows[e.RowIndex].ErrorText = "숫자를 입력하세요.";
return;
}
}
else if (propertyName == "ColorHex")
{
Color color;
if (!ColorRangeRule.TryParseColor(text, out color))
{
e.Cancel = true;
_colorRuleGrid.Rows[e.RowIndex].ErrorText = "#RRGGBB 형식으로 입력하세요.";
return;
}
}
_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;
e.Row.Cells[_toRateColumn.Index].Value = 0m;
e.Row.Cells[_colorHexColumn.Index].Value = "#808080";
}
private void ColorRuleGrid_RowsRemoved(object sender, DataGridViewRowsRemovedEventArgs e)
{
RefreshPreview();
}
private static bool TryParseDecimal(string text, out decimal value)
{
value = 0m;
@@ -227,5 +375,10 @@ namespace mbn_stock
return decimal.TryParse(normalized, NumberStyles.Number, CultureInfo.CurrentCulture, out value)
|| decimal.TryParse(normalized, NumberStyles.Number, CultureInfo.InvariantCulture, out value);
}
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

@@ -20,10 +20,20 @@ namespace mbn_stock
public static Bitmap Render(IEnumerable<SectorHeatMapItem> sourceItems)
{
return Render(sourceItems, new Size(ExportWidth, ExportHeight));
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());
}
public static Bitmap Render(IEnumerable<SectorHeatMapItem> sourceItems, Size size, IEnumerable<ColorRangeRule> colorRules)
{
Bitmap bitmap = new Bitmap(size.Width, size.Height, PixelFormat.Format32bppArgb);
@@ -39,6 +49,7 @@ namespace mbn_stock
.Where(item => item != null && item.MarketCap > 0m)
.OrderByDescending(item => item.MarketCap)
.ToList();
List<ColorRangeRule> rules = NormalizeColorRules(colorRules);
if (items.Count == 0)
{
@@ -48,12 +59,35 @@ namespace mbn_stock
RectangleF canvas = new RectangleF(0, 0, size.Width, size.Height);
List<TreeMapNode> nodes = BuildNodes(items, canvas);
DrawTiles(graphics, nodes);
DrawTiles(graphics, nodes, rules);
}
return bitmap;
}
public static List<ColorRangeRule> CreateDefaultColorRules()
{
return new List<ColorRangeRule>
{
CreateColorRule(-30m, -20m, "#0046BE"),
CreateColorRule(-20m, -10m, "#1F66C8"),
CreateColorRule(-10m, 0m, "#5F8FD8"),
CreateColorRule(0m, 10m, "#D85F5F"),
CreateColorRule(10m, 20m, "#D82C2C"),
CreateColorRule(20m, 30m, "#B60000")
};
}
private static ColorRangeRule CreateColorRule(decimal fromRate, decimal toRate, string colorHex)
{
return new ColorRangeRule
{
FromRate = fromRate,
ToRate = toRate,
ColorHex = colorHex
};
}
private static List<TreeMapNode> BuildNodes(IList<SectorHeatMapItem> items, RectangleF bounds)
{
decimal total = items.Sum(item => item.MarketCap);
@@ -133,7 +167,7 @@ namespace mbn_stock
return splitIndex;
}
private static void DrawTiles(Graphics graphics, IList<TreeMapNode> nodes)
private static void DrawTiles(Graphics graphics, IList<TreeMapNode> nodes, IList<ColorRangeRule> colorRules)
{
foreach (TreeMapNode node in nodes)
{
@@ -143,7 +177,7 @@ namespace mbn_stock
continue;
}
Color fill = GetFillColor(node.Item.ChangeRate);
Color fill = GetFillColor(node.Item.ChangeRate, colorRules);
using (Brush brush = new SolidBrush(fill))
using (Pen borderPen = new Pen(Color.FromArgb(70, 255, 255, 255), 1.5f))
{
@@ -174,14 +208,13 @@ namespace mbn_stock
textBounds = Inset(tileBounds, 2f);
}
Color textColor = GetReadableTextColor(fill);
string rateText = FormatRate(item.ChangeRate);
float fontSize = FindFittingFontSize(graphics, item.Name, rateText, textBounds);
float rateFontSize = Math.Max(8f, fontSize * 0.78f);
using (Font nameFont = new Font("Malgun Gothic", fontSize, FontStyle.Bold, GraphicsUnit.Pixel))
using (Font rateFont = new Font("Malgun Gothic", rateFontSize, FontStyle.Bold, GraphicsUnit.Pixel))
using (Brush textBrush = new SolidBrush(textColor))
using (Brush textBrush = new SolidBrush(Color.White))
using (StringFormat centerFormat = new StringFormat())
{
centerFormat.Alignment = StringAlignment.Center;
@@ -227,7 +260,53 @@ namespace mbn_stock
return 8f;
}
private static Color GetFillColor(decimal changeRate)
private static Color GetFillColor(decimal changeRate, IList<ColorRangeRule> colorRules)
{
if (colorRules != null && colorRules.Count > 0)
{
Color ruleColor;
if (TryGetRuleColor(changeRate, colorRules, out ruleColor))
{
return ruleColor;
}
}
return GetDefaultFillColor(changeRate);
}
private static bool TryGetRuleColor(decimal changeRate, IList<ColorRangeRule> colorRules, out Color color)
{
color = Color.Empty;
for (int index = 0; index < colorRules.Count; index++)
{
ColorRangeRule rule = colorRules[index];
if (rule.Contains(changeRate))
{
color = rule.DrawingColor;
return true;
}
}
ColorRangeRule firstRule = colorRules.First();
ColorRangeRule lastRule = colorRules.Last();
if (changeRate < firstRule.MinRate)
{
color = firstRule.DrawingColor;
return true;
}
if (changeRate >= lastRule.MaxRate)
{
color = lastRule.DrawingColor;
return true;
}
return false;
}
private static Color GetDefaultFillColor(decimal changeRate)
{
double intensity = Math.Min(1d, Math.Abs(decimal.ToDouble(changeRate)) / decimal.ToDouble(MaxRatePercent));
@@ -244,6 +323,25 @@ namespace mbn_stock
return Color.FromArgb(226, 228, 232);
}
private static List<ColorRangeRule> NormalizeColorRules(IEnumerable<ColorRangeRule> colorRules)
{
List<ColorRangeRule> rules = colorRules == null
? new List<ColorRangeRule>()
: colorRules
.Where(rule => rule != null && rule.MinRate != rule.MaxRate && IsValidColorRule(rule))
.OrderBy(rule => rule.MinRate)
.ThenBy(rule => rule.MaxRate)
.ToList();
return rules.Count == 0 ? CreateDefaultColorRules() : rules;
}
private static bool IsValidColorRule(ColorRangeRule rule)
{
Color color;
return ColorRangeRule.TryParseColor(rule.ColorHex, out color);
}
private static Color Interpolate(Color from, Color to, double amount)
{
amount = Math.Max(0d, Math.Min(1d, amount));
@@ -253,12 +351,6 @@ namespace mbn_stock
return Color.FromArgb(r, g, b);
}
private static Color GetReadableTextColor(Color background)
{
double luminance = (0.299d * background.R) + (0.587d * background.G) + (0.114d * background.B);
return luminance < 145d ? Color.White : Color.FromArgb(24, 28, 36);
}
private static string FormatRate(decimal value)
{
string format = value > 0m ? "+0.00'%'"

View File

@@ -46,6 +46,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="ColorRangeRule.cs" />
<Compile Include="Form1.cs">
<SubType>Form</SubType>
</Compile>