* UILineChart:继续优化

This commit is contained in:
Sunny 2020-10-05 22:52:51 +08:00
parent 552d683df3
commit 1e09ae19b0
10 changed files with 416 additions and 52 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -33,7 +33,7 @@
this.uiImageButton2 = new Sunny.UI.UIImageButton();
this.uiImageButton1 = new Sunny.UI.UIImageButton();
this.uiLine1 = new Sunny.UI.UILine();
this.uiLineChart1 = new Sunny.UI.UILineChart();
this.LineChart = new Sunny.UI.UILineChart();
this.PagePanel.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.uiImageButton3)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.uiImageButton2)).BeginInit();
@ -47,7 +47,7 @@
this.PagePanel.Controls.Add(this.uiImageButton2);
this.PagePanel.Controls.Add(this.uiImageButton1);
this.PagePanel.Controls.Add(this.uiLine1);
this.PagePanel.Controls.Add(this.uiLineChart1);
this.PagePanel.Controls.Add(this.LineChart);
this.PagePanel.Size = new System.Drawing.Size(800, 539);
//
// uiSymbolButton1
@ -62,6 +62,7 @@
this.uiSymbolButton1.Symbol = 61952;
this.uiSymbolButton1.TabIndex = 34;
this.uiSymbolButton1.Text = "数据";
this.uiSymbolButton1.Click += new System.EventHandler(this.uiSymbolButton1_Click);
//
// uiImageButton3
//
@ -113,18 +114,19 @@
this.uiLine1.Text = "UIBarChart";
this.uiLine1.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// uiLineChart1
// LineChart
//
this.uiLineChart1.FillColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244)))));
this.uiLineChart1.Font = new System.Drawing.Font("微软雅黑", 12F);
this.uiLineChart1.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(54)))), ((int)(((byte)(54)))), ((int)(((byte)(54)))));
this.uiLineChart1.Location = new System.Drawing.Point(30, 48);
this.uiLineChart1.MinimumSize = new System.Drawing.Size(1, 1);
this.uiLineChart1.Name = "uiLineChart1";
this.uiLineChart1.Option = null;
this.uiLineChart1.Size = new System.Drawing.Size(670, 400);
this.uiLineChart1.TabIndex = 35;
this.uiLineChart1.Text = "uiLineChart1";
this.LineChart.FillColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244)))));
this.LineChart.Font = new System.Drawing.Font("微软雅黑", 12F);
this.LineChart.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(54)))), ((int)(((byte)(54)))), ((int)(((byte)(54)))));
this.LineChart.Location = new System.Drawing.Point(30, 48);
this.LineChart.MinimumSize = new System.Drawing.Size(1, 1);
this.LineChart.Name = "LineChart";
this.LineChart.Option = null;
this.LineChart.Size = new System.Drawing.Size(670, 400);
this.LineChart.TabIndex = 35;
this.LineChart.Text = "uiLineChart1";
this.LineChart.PointValue += new Sunny.UI.UILineChart.OnPointValue(this.LineChart_PointValue);
//
// FLineChart
//
@ -149,6 +151,6 @@
private UIImageButton uiImageButton2;
private UIImageButton uiImageButton1;
private UILine uiLine1;
private UILineChart uiLineChart1;
private UILineChart LineChart;
}
}

View File

@ -1,4 +1,8 @@
namespace Sunny.UI.Demo.Charts
using System;
using System.Drawing;
using System.Text;
namespace Sunny.UI.Demo.Charts
{
public partial class FLineChart : UITitlePage
{
@ -6,5 +10,63 @@
{
InitializeComponent();
}
private void uiSymbolButton1_Click(object sender, System.EventArgs e)
{
UILineOption option = new UILineOption();
option.Title = new UITitle();
option.Title.Text = "SunnyUI";
option.Title.SubText = "LineChart";
option.XAxisType = UIAxisType.Time;
var series = option.AddSeries(new UILineSeries("Line1"));
DateTime dt = new DateTime(2020, 10, 4);
series.Add(dt.AddHours(0), 1.2);
series.Add(dt.AddHours(0.1), 2.2);
series.Add(dt.AddHours(0.2), 3.2);
series.Add(dt.AddHours(0.3), 4.2);
series.Add(dt.AddHours(0.4), 3.2);
series.Add(dt.AddHours(0.5), 2.2);
series.Symbol = UILinePointSymbol.Square;
series.SymbolSize = 4;
series.SymbolLineWidth = 2;
series.SymbolColor = Color.Red;
series = option.AddSeries(new UILineSeries("Line2"));
series.Add(dt.AddHours(0.3), 3.3);
series.Add(dt.AddHours(0.4), 2.3);
series.Add(dt.AddHours(0.5), 2.3);
series.Add(dt.AddHours(0.6), 1.3);
series.Add(dt.AddHours(0.7), 2.3);
series.Add(dt.AddHours(0.8), 4.3);
series.Symbol = UILinePointSymbol.Star;
series.SymbolSize = 4;
series.SymbolLineWidth = 2;
series.SymbolColor = Color.Red;
series.Smooth = true;
// option.XAxis.Min = new DateTimeInt64(dt.AddDays(-1));
// option.XAxis.Max = new DateTimeInt64(dt.AddDays(1));
// option.XAxis.MaxAuto = false;
// option.XAxis.MinAuto = false;
option.XAxis.Name = "数值";
option.YAxis.Name = "数值";
LineChart.SetOption(option);
}
private void LineChart_PointValue(object sender, System.Collections.Generic.List<UILineSelectPoint> points)
{
StringBuilder sb = new StringBuilder();
foreach (var point in points)
{
sb.Append(point.Name + ", " + point.Index + ", " + point.X + ", " + point.Y);
sb.Append('\n');
}
Console.WriteLine(sb.ToString());
}
}
}

View File

@ -62,8 +62,11 @@ namespace Sunny.UI
double max = double.MinValue;
foreach (var series in BarOption.Series)
{
min = Math.Min(min, series.Data.Min());
max = Math.Max(max, series.Data.Max());
if (series.Data.Count > 0)
{
min = Math.Min(min, series.Data.Min());
max = Math.Max(max, series.Data.Max());
}
}
if (min > 0 && max > 0 && !BarOption.YAxis.Scale) min = 0;

View File

@ -159,15 +159,31 @@ namespace Sunny.UI
public event DoFormatter Formatter;
public string GetLabel(double value, int index)
public string GetLabel(double value, int index, UIAxisType axisType = UIAxisType.Value)
{
return Formatter != null ? Formatter?.Invoke(value, index) : value.ToString("F" + DecimalCount);
switch (axisType)
{
case UIAxisType.Value:
return Formatter != null ? Formatter?.Invoke(value, index) : value.ToString("F" + DecimalCount);
case UIAxisType.Time:
DateTimeInt64 dt = new DateTimeInt64((long)value);
return Formatter != null ? Formatter?.Invoke(dt, index) : (DateTimeFormat.IsNullOrEmpty() ? dt.ToString() : dt.ToString(DateTimeFormat));
case UIAxisType.Category:
return Formatter != null ? Formatter?.Invoke(value, index) : value.ToString("F0");
}
return value.ToString("F2");
}
/// <summary>
/// 小数位个数Formatter不为空时以Formatter为准
/// </summary>
public int DecimalCount { get; set; } = 0;
/// <summary>
/// 日期格式化字符串Formatter不为空时以Formatter为准
/// </summary>
public string DateTimeFormat { get; set; } = "HH:mm";
}
public class UIAxisTick

View File

@ -1,9 +1,10 @@
using Sunny.UI.Charts;
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Windows.Forms;
namespace Sunny.UI
{
@ -42,13 +43,13 @@ namespace Sunny.UI
foreach (var series in LineOption.Series.Values)
{
series.Points.Clear();
series.ClearPoints();
for (int i = 0; i < series.XData.Count; i++)
{
float x = (float)(series.XData[i] - XAxisStart) * 1.0f * DrawSize.Width / (XAxisEnd - XAxisStart);
float y = (float)(series.YData[i] - YAxisStart) * 1.0f * DrawSize.Height / (YAxisEnd - YAxisStart);
series.Points.Add(new PointF(DrawOrigin.X + x, DrawOrigin.Y - y));
float x = (float)((series.XData[i] - XAxisStart * XAxisInterval) * 1.0f * DrawSize.Width / XAxisInterval / (XAxisEnd - XAxisStart));
float y = (float)((series.YData[i] - YAxisStart * YAxisInterval) * 1.0f * DrawSize.Height / YAxisInterval / (YAxisEnd - YAxisStart));
series.AddPoint(new PointF(DrawOrigin.X + x, DrawOrigin.Y - y));
}
}
@ -62,8 +63,11 @@ namespace Sunny.UI
double max = double.MinValue;
foreach (var series in LineOption.Series.Values)
{
min = Math.Min(min, series.YData.Min());
max = Math.Max(max, series.YData.Max());
if (series.DataCount > 0)
{
min = Math.Min(min, series.YData.Min());
max = Math.Max(max, series.YData.Max());
}
}
if (min > 0 && max > 0 && !LineOption.YAxis.Scale) min = 0;
@ -93,8 +97,8 @@ namespace Sunny.UI
max = Math.Max(max, series.XData.Max());
}
if (min > 0 && max > 0 && !LineOption.XAxis.Scale) min = 0;
if (min < 0 && max < 0 && !LineOption.XAxis.Scale) max = 0;
if (min > 0 && max > 0 && !LineOption.XAxis.Scale && LineOption.XAxisType == UIAxisType.Value) min = 0;
if (min < 0 && max < 0 && !LineOption.XAxis.Scale && LineOption.XAxisType == UIAxisType.Value) max = 0;
if (!LineOption.XAxis.MaxAuto) max = LineOption.XAxis.Max;
if (!LineOption.XAxis.MinAuto) min = LineOption.XAxis.Min;
@ -104,12 +108,23 @@ namespace Sunny.UI
min = 0;
}
UIChartHelper.CalcDegreeScale(min, max, LineOption.XAxis.SplitNumber,
out int startX, out int endX, out double intervalX);
if (LineOption.XAxisType == UIAxisType.Value || LineOption.XAxisType == UIAxisType.Category)
{
UIChartHelper.CalcDegreeScale(min, max, LineOption.XAxis.SplitNumber,
out int startX, out int endX, out double intervalX);
XAxisStart = startX;
XAxisEnd = endX;
XAxisInterval = intervalX;
}
XAxisStart = startX;
XAxisEnd = endX;
XAxisInterval = intervalX;
if (LineOption.XAxisType == UIAxisType.Time)
{
UIChartHelper.CalcDateTimeDegreeScale(min, max, LineOption.XAxis.SplitNumber,
out int startX, out int endX, out double intervalX);
XAxisStart = startX;
XAxisEnd = endX;
XAxisInterval = intervalX;
}
}
[Browsable(false)]
@ -131,15 +146,31 @@ namespace Sunny.UI
option.Title.Text = "SunnyUI";
option.Title.SubText = "LineChart";
option.AddSeries(new UILineSeries("Line"));
option.AddData("Line", 0, 1);
option.AddData("Line", 1, 2);
option.AddData("Line", 2, 3);
option.AddData("Line", 3, 4);
option.AddData("Line", 4, 3);
option.AddData("Line", 5, 2);
var series = option.AddSeries(new UILineSeries("Line1"));
series.Add(0, 1.2);
series.Add(1.1, 2.2);
series.Add(2.2, 3.2);
series.Add(3.3, 4.2);
series.Add(4.4, 3.2);
series.Add(5.5, 2.2);
series.Symbol = UILinePointSymbol.Square;
series.SymbolSize = 4;
series.SymbolLineWidth = 1;
series.SymbolColor = Color.Red;
option.XAxis.Name = "日期";
series = option.AddSeries(new UILineSeries("Line2"));
series.Add(0.3, 3.3);
series.Add(1.3, 2.3);
series.Add(2.3, 2.3);
series.Add(3.3, 1.3);
series.Add(4.3, 2.3);
series.Add(5.3, 4.3);
series.Symbol = UILinePointSymbol.Plus;
series.SymbolSize = 4;
series.SymbolLineWidth = 1;
series.SymbolColor = Color.Red;
option.XAxis.Name = "数值";
option.YAxis.Name = "数值";
emptyOption = option;
@ -204,7 +235,7 @@ namespace Sunny.UI
float wmax = 0;
for (int i = XAxisStart; i <= XAxisEnd; i++)
{
string label = LineOption.XAxis.AxisLabel.GetLabel(i * XAxisInterval, idx);
string label = LineOption.XAxis.AxisLabel.GetLabel(i * XAxisInterval, idx, LineOption.XAxisType);
SizeF sf = g.MeasureString(label, SubFont);
wmax = Math.Max(wmax, sf.Width);
g.DrawString(label, SubFont, ChartStyle.ForeColor, start - sf.Width / 2.0f,
@ -277,10 +308,80 @@ namespace Sunny.UI
Color color = series.Color;
if (!series.CustomColor) color = ChartStyle.GetColor(idx);
if (series.Smooth)
g.DrawCurve(color, series.Points.ToArray(), true);
else
g.DrawLines(series.Color, series.Points.ToArray(), true);
using (Pen pen = new Pen(color, series.Width))
{
g.SetHighQuality();
if (series.Smooth)
g.DrawCurve(pen, series.Points.ToArray());
else
g.DrawLines(pen, series.Points.ToArray());
g.SetDefaultQuality();
}
if (series.Symbol != UILinePointSymbol.None)
{
using (Brush br = new SolidBrush(ChartStyle.BackColor))
using (Pen pn = new Pen(series.SymbolColor, series.SymbolLineWidth))
{
foreach (var p in series.Points)
{
switch (series.Symbol)
{
case UILinePointSymbol.Square:
g.FillRectangle(br, p.X - series.SymbolSize, p.Y - series.SymbolSize, series.SymbolSize * 2, series.SymbolSize * 2);
g.DrawRectangle(pn, p.X - series.SymbolSize, p.Y - series.SymbolSize, series.SymbolSize * 2, series.SymbolSize * 2);
break;
case UILinePointSymbol.Diamond:
{
PointF pt1 = new PointF(p.X - series.SymbolSize, p.Y);
PointF pt2 = new PointF(p.X, p.Y - series.SymbolSize);
PointF pt3 = new PointF(p.X + series.SymbolSize, p.Y);
PointF pt4 = new PointF(p.X, p.Y + series.SymbolSize);
PointF[] pts = { pt1, pt2, pt3, pt4, pt1 };
g.SetHighQuality();
GraphicsPath path = pts.Path();
g.FillPath(br, path);
g.DrawPath(pn, path);
path.Dispose();
}
break;
case UILinePointSymbol.Triangle:
{
PointF pt1 = new PointF(p.X, p.Y - series.SymbolSize);
PointF pt2 = new PointF(p.X - series.SymbolSize * 0.866f, p.Y + series.SymbolSize * 0.5f);
PointF pt3 = new PointF(p.X + series.SymbolSize * 0.866f, p.Y + series.SymbolSize * 0.5f);
PointF[] pts = { pt1, pt2, pt3, pt1 };
g.SetHighQuality();
GraphicsPath path = pts.Path();
g.FillPath(br, path);
g.DrawPath(pn, path);
path.Dispose();
}
break;
case UILinePointSymbol.Circle:
g.SetHighQuality();
g.FillEllipse(br, p.X - series.SymbolSize, p.Y - series.SymbolSize, series.SymbolSize * 2, series.SymbolSize * 2);
g.DrawEllipse(pn, p.X - series.SymbolSize, p.Y - series.SymbolSize, series.SymbolSize * 2, series.SymbolSize * 2);
break;
case UILinePointSymbol.Plus:
g.DrawLine(pn, p.X - series.SymbolSize, p.Y, p.X + series.SymbolSize, p.Y);
g.DrawLine(pn, p.X, p.Y - series.SymbolSize, p.X, p.Y + series.SymbolSize);
break;
case UILinePointSymbol.Star:
g.SetHighQuality();
g.DrawLine(pn, p.X, p.Y - series.SymbolSize, p.X, p.Y + series.SymbolSize);
g.DrawLine(pn, p.X - series.SymbolSize * 0.866f, p.Y + series.SymbolSize * 0.5f,
p.X + series.SymbolSize * 0.866f, p.Y - series.SymbolSize * 0.5f);
g.DrawLine(pn, p.X - series.SymbolSize * 0.866f, p.Y - series.SymbolSize * 0.5f,
p.X + series.SymbolSize * 0.866f, p.Y + series.SymbolSize * 0.5f);
break;
}
}
}
g.SetDefaultQuality();
}
idx++;
}
@ -309,5 +410,67 @@ namespace Sunny.UI
g.DrawString(line.Name, SubFont, line.Color, Width - sf.Width - 4 - LineOption.Grid.Right, pos - 2 - sf.Height);
}
}
private readonly List<UILineSelectPoint> selectPoints = new List<UILineSelectPoint>();
private readonly List<UILineSelectPoint> selectPointsTemp = new List<UILineSelectPoint>();
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (!NeedDraw) return;
selectPointsTemp.Clear();
foreach (var series in LineOption.Series.Values)
{
if (series.GetNearestPoint(e.Location, 4, out double x, out double y, out int index))
{
UILineSelectPoint point = new UILineSelectPoint();
point.Name = series.Name;
point.Index = index;
point.X = x;
point.Y = y;
selectPointsTemp.Add(point);
}
}
bool isNew = false;
if (selectPointsTemp.Count != selectPoints.Count)
{
isNew = true;
}
else
{
Dictionary<string, UILineSelectPoint> points = selectPoints.ToDictionary(p => p.Name);
foreach (var point in selectPointsTemp)
{
if (!points.ContainsKey(point.Name))
{
isNew = true;
break;
}
if (points[point.Name].Index != point.Index)
{
isNew = true;
break;
}
}
}
if (isNew)
{
selectPoints.Clear();
foreach (var point in selectPointsTemp)
{
selectPoints.Add(point);
}
PointValue?.Invoke(this, selectPoints);
}
}
public delegate void OnPointValue(object sender, List<UILineSelectPoint> points);
public event OnPointValue PointValue;
}
}

View File

@ -3,7 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
namespace Sunny.UI.Charts
namespace Sunny.UI
{
public sealed class UILineOption : UIOption, IDisposable
{
@ -29,10 +29,20 @@ namespace Sunny.UI.Charts
public readonly List<UIScaleLine> XAxisScaleLines = new List<UIScaleLine>();
public readonly List<UIScaleLine> YAxisScaleLines = new List<UIScaleLine>();
public void AddSeries(UILineSeries series)
public UILineSeries AddSeries(UILineSeries series)
{
if (series.Name.IsNullOrEmpty()) return;
if (series.Name.IsNullOrEmpty()) return null;
Series.TryAdd(series.Name, series);
return series;
}
public UILineSeries AddSeries(string name)
{
if (name.IsNullOrEmpty()) return null;
UILineSeries series = new UILineSeries(name);
Series.TryAdd(series.Name, series);
return series;
}
public void AddData(string name, double x, double y)
@ -150,11 +160,13 @@ namespace Sunny.UI.Charts
{
public string Name { get; private set; }
public float Width { get; set; } = 1;
public float Width { get; set; } = 2;
public Color Color { get; set; }
public UILinePointSymbol Symbol { get; set; } = UILinePointSymbol.None;
public int SymbolSize { get; set; } = 1;
public int SymbolSize { get; set; } = 4;
public int SymbolLineWidth { get; set; } = 1;
public Color SymbolColor { get; set; }
@ -181,13 +193,47 @@ namespace Sunny.UI.Charts
public readonly List<PointF> Points = new List<PointF>();
private readonly List<double> PointsX = new List<double>();
private readonly List<double> PointsY = new List<double>();
public int DataCount => XData.Count;
public bool GetNearestPoint(Point p, int offset, out double x, out double y, out int index)
{
index = PointsX.BinarySearchNearIndex(p.X);
if (p.X >= PointsX[index] - offset && p.X <= PointsX[index] + offset &&
p.Y >= PointsY[index] - offset && p.Y <= PointsY[index] + offset)
{
x = XData[index];
y = YData[index];
return true;
}
x = 0;
y = 0;
return false;
}
public void Clear()
{
XData.Clear();
YData.Clear();
ClearPoints();
}
public void ClearPoints()
{
Points.Clear();
PointsX.Clear();
PointsY.Clear();
}
public void AddPoint(PointF point)
{
Points.Add(point);
PointsX.Add(point.X);
PointsY.Add(point.Y);
}
public void Add(double x, double y)
@ -221,4 +267,15 @@ namespace Sunny.UI.Charts
Plus,
Star
}
public struct UILineSelectPoint
{
public string Name { get; set; }
public int Index { get; set; }
public double X { get; set; }
public double Y { get; set; }
}
}

View File

@ -238,6 +238,67 @@ namespace Sunny.UI
degree_end = end2;
}
/// <summary>
/// 计算刻度
/// 起始值必须小于结束值
/// </summary>
/// <param name="start">起始值</param>
/// <param name="end">结束值</param>
/// <param name="expect_num">期望刻度数量,实际数接近此数</param>
/// <param name="degree_start">刻度起始值,须乘以间隔使用</param>
/// <param name="degree_end">刻度结束值,须乘以间隔使用</param>
/// <param name="degree_gap">刻度间隔</param>
public static void CalcDateTimeDegreeScale(double start, double end, int expect_num,
out int degree_start, out int degree_end, out double degree_gap)
{
if (start >= end)
{
throw new Exception("起始值必须小于结束值");
}
double differ = end - start;
double differ_gap = differ / (expect_num - 1); //35, 4.6, 0.27
double exponent = Math.Log10(differ_gap) - 1; //0.54, -0.34, -1.57
int _exponent = (int)exponent; //0, 0=>-1, -1=>-2
if (exponent < 0 && Math.Abs(exponent) > 1e-8)
{
_exponent--;
}
int step = (int)(differ_gap / Math.Pow(10, _exponent)); //35, 46, 27
int[] fix_steps = new int[] { 10, 20, 30, 60, 120 };
int fix_step = 10; //25, 50, 25
for (int i = fix_steps.Length - 1; i >= 1; i--)
{
if (step > (fix_steps[i] + fix_steps[i - 1]) / 2)
{
fix_step = fix_steps[i];
break;
}
}
degree_gap = fix_step * Math.Pow(10, _exponent); //25, 5, 0.25
double start1 = start / degree_gap;
int start2 = (int)start1;
if (start1 < 0 && Math.Abs(start1 - start2) > 1e-8)
{
start2--;
}
degree_start = start2;
double end1 = end / degree_gap;
int end2 = (int)end1;
if (end1 >= 0 && Math.Abs(end1 - end2) > 1e-8)
{
end2++;
}
degree_end = end2;
}
/// <summary>
/// 计算刻度
/// 起始值必须小于结束值