using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Windows.Forms; namespace CustomChartControl { public class TimeVoltageChart : Control { private List dataPoints; private float zoomFactorX = 1.0f; // X축 줌 팩터 private float zoomFactorY = 1.0f; // Y축 줌 팩터 private PointF panOffset = PointF.Empty; private Point lastMousePosition; private bool isDragging = false; private DateTime[] _timeData; private float[] _voltageData; private bool _showDataPoints = false; // 기존 필드 추가 private bool isSelecting = false; // 영역 선택 중 여부 private Point selectionStart; // 영역 선택 시작점 private Rectangle selectionRectangle; // 선택 영역 public TimeVoltageChart() { this.DoubleBuffered = true; this.dataPoints = new List(); this.MouseWheel += TimeVoltageChart_MouseWheel; this.MouseDown += TimeVoltageChart_MouseDown; this.MouseMove += TimeVoltageChart_MouseMove; this.MouseUp += TimeVoltageChart_MouseUp; } public DateTime[] TimeData { get => _timeData; set { _timeData = value; UpdateDataPoints(); } } public float[] VoltageData { get => _voltageData; set { _voltageData = value; UpdateDataPoints(); } } public bool ShowDataPoints { get => _showDataPoints; set { _showDataPoints = value; Invalidate(); // 속성 변경 시 차트 다시 그리기 } } public void AdjustScaleX() { if (_timeData != null && _timeData.Length > 0) { // X축 자동 스케일링 float totalSeconds = (float)(_timeData[_timeData.Length - 1] - _timeData[0]).TotalSeconds; zoomFactorX = Width / totalSeconds; panOffset.X = 0; // 초기화 Invalidate(); // 변경 후 다시 그리기 } } public void AdjustScaleY() { if (_voltageData != null && _voltageData.Length > 0) { // Y축 자동 스케일링 float minY = float.MaxValue; float maxY = float.MinValue; foreach (var voltage in _voltageData) { if (voltage < minY) minY = voltage; if (voltage > maxY) maxY = voltage; } float range = maxY - minY; if (range == 0) range = 1; zoomFactorY = Height / range; panOffset.Y = -minY * zoomFactorY; // 초기화 Invalidate(); // 변경 후 다시 그리기 } } private void UpdateDataPoints() { dataPoints.Clear(); if (_timeData != null && _voltageData != null && _timeData.Length == _voltageData.Length) { DateTime startTime = _timeData[0]; for (int i = 0; i < _timeData.Length; i++) { float timeOffset = (float)(_timeData[i] - startTime).TotalSeconds; dataPoints.Add(new PointF(timeOffset, _voltageData[i])); } Invalidate(); // 화면을 갱신하여 차트를 다시 그리도록 함 } } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); DrawGrid(e.Graphics); DrawData(e.Graphics); DrawBorder(e.Graphics); DrawZoomLevel(e.Graphics); if (isSelecting && selectionRectangle.Width > 0 && selectionRectangle.Height > 0) { using (var selectionPen = new Pen(Color.Gray, 1)) { selectionPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash; e.Graphics.DrawRectangle(selectionPen, selectionRectangle); } } } private void DrawZoomLevel(Graphics g) { string zoomLevelText = $"Zoom Level - X: {zoomFactorX:F2}, Y: {zoomFactorY:F2}"; Font zoomLevelFont = new Font("Arial", 10, FontStyle.Bold); Brush zoomLevelBrush = new SolidBrush(Color.Black); SizeF textSize = g.MeasureString(zoomLevelText, zoomLevelFont); float x = Width - textSize.Width - 60; // 오른쪽 여백을 줄이기 float y = 10; // 상단 여백 10 g.DrawString(zoomLevelText, zoomLevelFont, zoomLevelBrush, x, y); } private void DrawBorder(Graphics g) { int leftMargin = 50; // 왼쪽 Y축 라벨을 위한 공간 int rightMargin = 50; // 오른쪽 여백 int bottomMargin = 30; // X축 라벨을 위한 공간 Pen borderPen = new Pen(Color.Black, 1); // 테두리 색상 및 두께 Rectangle borderRect = new Rectangle(leftMargin, 0, Width - leftMargin - rightMargin, Height - bottomMargin - 1); // 테두리 영역 g.DrawRectangle(borderPen, borderRect); } private void DrawGrid(Graphics g) { int gridSpacingX = 60; // X축 그리드 간격 int gridSpacingY = 20; // Y축 그리드 간격 int leftMargin = 50; // 왼쪽 Y축 라벨을 위한 공간 int rightMargin = 50; // 오른쪽 여백 int bottomMargin = 30; // X축 라벨을 위한 공간 Pen gridPen = new Pen(Color.LightGray); Brush labelBrush = new SolidBrush(Color.Black); Font labelFont = new Font("Arial", 8); if (_timeData != null && _timeData.Length > 0) { DateTime minTime = _timeData[0]; DateTime maxTime = _timeData[_timeData.Length - 1]; // 마지막 요소 double totalSeconds = (maxTime - minTime).TotalSeconds; float visibleStartTime = (0 - panOffset.X) / zoomFactorX; float visibleEndTime = (Width - leftMargin - rightMargin - panOffset.X) / zoomFactorX; for (int x = leftMargin; x < Width - rightMargin; x += gridSpacingX) { float timeOffset = (x - leftMargin - panOffset.X) / zoomFactorX; if (timeOffset < visibleStartTime || timeOffset > visibleEndTime) continue; DateTime time = minTime.AddSeconds(timeOffset); g.DrawLine(gridPen, x, 0, x, Height - bottomMargin); // 테두리 위에 맞추기 using (StringFormat sf = new StringFormat()) { sf.LineAlignment = StringAlignment.Near; sf.Alignment = StringAlignment.Center; g.DrawString(time.ToString("yy-MM-dd\nHH:mm:ss"), labelFont, labelBrush, new PointF(x, Height - bottomMargin), sf); } } } if (_voltageData != null && _voltageData.Length > 0) { float minY = float.MaxValue; float maxY = float.MinValue; foreach (var voltage in _voltageData) { if (voltage < minY) minY = voltage; if (voltage > maxY) maxY = voltage; } float range = maxY - minY; if (range == 0) range = 1; using (StringFormat sfY = new StringFormat()) { sfY.LineAlignment = StringAlignment.Center; sfY.Alignment = StringAlignment.Far; // Y축 라벨을 오른쪽 정렬 for (int y = 0; y < Height - bottomMargin; y += gridSpacingY) // 테두리 위에 맞추기 { float voltageValue = minY + (Height - y - panOffset.Y) / zoomFactorY * range / (Height - bottomMargin); // 모든 Y축 눈금 라벨을 표시 g.DrawLine(gridPen, leftMargin, y, Width - rightMargin, y); // 패딩 추가 g.DrawString(voltageValue.ToString("F2"), labelFont, labelBrush, new PointF(leftMargin - 10, y), sfY); } } } } private void DrawData(Graphics g) { if (dataPoints.Count < 2) return; int leftMargin = 50; int rightMargin = 50; int bottomMargin = 30; int minPixelDistance = 3; // 최소 픽셀 거리 설정 Pen dataPen = new Pen(Color.Blue); Brush pointBrush = new SolidBrush(Color.Red); // 데이터 포인트의 원 색상 float visibleStartTime = (0 - panOffset.X) / zoomFactorX; float visibleEndTime = (Width - leftMargin - rightMargin - panOffset.X) / zoomFactorX; PointF prevPoint = TransformPoint(dataPoints[0]); PointF lastDrawnPoint = prevPoint; bool skipNextPoint = false; // 포인트 간 평균 간격 계산 float totalWidth = Width - leftMargin - rightMargin; float avgPixelDistance = totalWidth / dataPoints.Count; int pointRadius = Math.Max(1, (int)(avgPixelDistance / 4)); // 밀집도에 따른 포인트 반지름 조정 for (int i = 1; i < dataPoints.Count; i++) { float timeOffset = dataPoints[i].X; if (timeOffset < visibleStartTime || timeOffset > visibleEndTime) { prevPoint = TransformPoint(dataPoints[i]); continue; } PointF currPoint = TransformPoint(dataPoints[i]); if (currPoint.X >= leftMargin && currPoint.X <= Width - rightMargin) { PointF clippedPrev = ClipToChartArea(prevPoint, leftMargin, Width - rightMargin, bottomMargin, Height); PointF clippedCurr = ClipToChartArea(currPoint, leftMargin, Width - rightMargin, bottomMargin, Height); // 선 연결을 유지하되, 포인트를 그리지 않을 때만 스킵 if (skipNextPoint || Math.Abs(clippedCurr.X - lastDrawnPoint.X) >= minPixelDistance) { g.DrawLine(dataPen, clippedPrev, clippedCurr); lastDrawnPoint = clippedCurr; skipNextPoint = false; if (clippedCurr.X > leftMargin && clippedCurr.X < Width - rightMargin && clippedCurr.Y > 0 && clippedCurr.Y < Height - bottomMargin) { g.FillEllipse(pointBrush, clippedCurr.X - pointRadius, clippedCurr.Y - pointRadius, pointRadius * 2, pointRadius * 2); } } else { skipNextPoint = true; } } prevPoint = currPoint; } } private PointF ClipToChartArea(PointF point, int leftMargin, int rightLimit, int bottomMargin, int topLimit) { // X축 클리핑 if (point.X < leftMargin) point.X = leftMargin; if (point.X > rightLimit) point.X = rightLimit; // Y축 클리핑 if (point.Y < 0) point.Y = 0; if (point.Y > topLimit - bottomMargin) point.Y = topLimit - bottomMargin; return point; } private PointF TransformPoint(PointF point) { int leftMargin = 50; int bottomMargin = 30; return new PointF( (point.X * zoomFactorX + panOffset.X) + leftMargin, Height - ((point.Y * zoomFactorY - panOffset.Y) + bottomMargin)); } private void TimeVoltageChart_MouseWheel(object sender, MouseEventArgs e) { float zoomChange; if (ModifierKeys.HasFlag(Keys.Control)) { // Y축 줌 레벨 조정 zoomChange = e.Delta > 0 ? 1.1f : 0.9f; float newZoomFactorY = zoomFactorY * zoomChange; if (CanZoomY(newZoomFactorY)) { AdjustPanOffsetY(newZoomFactorY, e.Delta > 0); zoomFactorY = newZoomFactorY; } } else { // X축 줌 레벨 조정 zoomChange = e.Delta > 0 ? 1.1f : 0.9f; float newZoomFactorX = zoomFactorX * zoomChange; if (CanZoomX(newZoomFactorX)) { AdjustPanOffsetX(newZoomFactorX, e.Delta > 0); zoomFactorX = newZoomFactorX; } } Invalidate(); } private void AdjustPanOffsetX(float newZoomFactorX, bool zoomingIn) { float totalDataTime = (float)(_timeData[_timeData.Length - 1] - _timeData[0]).TotalSeconds; float currentVisibleStart = -panOffset.X / zoomFactorX; float currentVisibleEnd = (Width - panOffset.X) / zoomFactorX; float newVisibleStart, newVisibleEnd; if (zoomingIn) { newVisibleStart = currentVisibleStart + (currentVisibleEnd - currentVisibleStart) * 0.1f; newVisibleEnd = currentVisibleEnd - (currentVisibleEnd - currentVisibleStart) * 0.1f; } else { newVisibleStart = currentVisibleStart - (currentVisibleEnd - currentVisibleStart) * 0.1f; newVisibleEnd = currentVisibleEnd + (currentVisibleEnd - currentVisibleStart) * 0.1f; } if (newVisibleStart < 0) { panOffset.X = 0; } else if (newVisibleEnd > totalDataTime) { panOffset.X = -(totalDataTime * newZoomFactorX - Width); } else { panOffset.X = -newVisibleStart * newZoomFactorX; } } private void AdjustPanOffsetY(float newZoomFactorY, bool zoomingIn) { float minY = float.MaxValue; float maxY = float.MinValue; foreach (var voltage in _voltageData) { if (voltage < minY) minY = voltage; if (voltage > maxY) maxY = voltage; } float dataRange = maxY - minY; float visibleRange = (Height - 70) / newZoomFactorY; // 상하 여백 고려 float visibleStartY = -panOffset.Y / zoomFactorY; float visibleEndY = visibleStartY + visibleRange; // Ensure the visible range is within the data range if (visibleStartY < minY) { visibleStartY = minY; visibleEndY = visibleStartY + visibleRange; } if (visibleEndY > maxY) { visibleEndY = maxY; visibleStartY = visibleEndY - visibleRange; } // Adjust panOffset.Y to ensure the data is visible within the screen bounds panOffset.Y = -visibleStartY * newZoomFactorY; } private bool CanZoomX(float newZoomFactorX) { float totalDataTime = (float)(_timeData[_timeData.Length - 1] - _timeData[0]).TotalSeconds; float visibleTime = (Width - 100) / newZoomFactorX; // 좌우 여백 고려 float marginFactor = 0.05f; // 5% 여백 float minVisibleTime = 1.0f; // 최소 가시 시간 return visibleTime < totalDataTime * (1 + marginFactor) && visibleTime > minVisibleTime; } private bool CanZoomY(float newZoomFactorY) { float minY = float.MaxValue; float maxY = float.MinValue; foreach (var voltage in _voltageData) { if (voltage < minY) minY = voltage; if (voltage > maxY) maxY = voltage; } float dataRange = maxY - minY; float visibleRange = (Height - 70) / newZoomFactorY; // 상하 여백 고려 float marginFactor = 0.05f; // 5% 여백 float minVisibleRange = 0.1f; // 최소 가시 범위 return visibleRange < dataRange * (1 + marginFactor) && visibleRange > minVisibleRange; } private void TimeVoltageChart_MouseDown(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { isDragging = true; lastMousePosition = e.Location; } else if (e.Button == MouseButtons.Right) { isSelecting = true; selectionStart = e.Location; selectionRectangle = new Rectangle(e.Location, Size.Empty); } } private void TimeVoltageChart_MouseMove(object sender, MouseEventArgs e) { if (isDragging) { panOffset.X += e.X - lastMousePosition.X; panOffset.Y += e.Y - lastMousePosition.Y; // Y축 이동 방향 수정 lastMousePosition = e.Location; Invalidate(); } else if (isSelecting) { var endPoint = e.Location; selectionRectangle = new Rectangle( Math.Min(selectionStart.X, endPoint.X), Math.Min(selectionStart.Y, endPoint.Y), Math.Abs(selectionStart.X - endPoint.X), Math.Abs(selectionStart.Y - endPoint.Y)); Invalidate(); } } private void TimeVoltageChart_MouseUp(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { isDragging = false; } else if (e.Button == MouseButtons.Right) { isSelecting = false; // 선택한 영역만큼 확대 if (selectionRectangle.Width > 0 && selectionRectangle.Height > 0) { float newZoomFactorX = (Width - 100) / (float)selectionRectangle.Width; // 여백을 고려한 줌 float newZoomFactorY = (Height - 30) / (float)selectionRectangle.Height; // 여백을 고려한 줌 // 확대할 영역의 좌표를 차트 좌표로 변환 float selectedMinX = (selectionRectangle.Left - 50 - panOffset.X) / zoomFactorX; float selectedMaxX = (selectionRectangle.Right - 50 - panOffset.X) / zoomFactorX; float selectedMinY = (Height - selectionRectangle.Bottom - panOffset.Y) / zoomFactorY; float selectedMaxY = (Height - selectionRectangle.Top - panOffset.Y) / zoomFactorY; zoomFactorX = newZoomFactorX; zoomFactorY = newZoomFactorY; panOffset.X = -selectedMinX * zoomFactorX + 50; panOffset.Y = -selectedMinY * zoomFactorY; Invalidate(); } } } } }