SunnyUI/SunnyUI/Charts/UIBarChart.cs

576 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/******************************************************************************
* SunnyUI 开源控件库、工具类库、扩展类库、多页面开发框架。
* CopyRight (C) 2012-2022 ShenYongHua(沈永华).
* QQ群56829229 QQ17612584 EMailSunnyUI@QQ.Com
*
* Blog: https://www.cnblogs.com/yhuse
* Gitee: https://gitee.com/yhuse/SunnyUI
* GitHub: https://github.com/yhuse/SunnyUI
*
* SunnyUI.dll can be used for free under the GPL-3.0 license.
* If you use this code, please keep this note.
* 如果您使用此代码,请保留此说明。
******************************************************************************
* 文件名称: UIBarChart.cs
* 文件说明: 柱状图
* 当前版本: V3.1
* 创建日期: 2020-06-06
*
* 2020-06-06: V2.2.5 增加文件说明
* 2020-08-21: V2.2.7 可设置柱状图最小宽度
* 2021-07-22: V3.0.5 增加更新数据的方法
* 2021-01-01: V3.0.9 增加柱子上显示数值
* 2022-03-08: V3.1.1 增加X轴文字倾斜
* 2022-05-27: V3.1.9 重写Y轴坐标显示
* 2022-07-29: V3.2.2 数据显示的小数位数重构调整至数据序列 Series.DecimalPlaces
* 2022-07-30: V3.2.2 坐标轴的小数位数重构调整至坐标轴标签 AxisLabel.DecimalPlaces
******************************************************************************/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Windows.Forms;
namespace Sunny.UI
{
/// <summary>
/// 柱状图
/// </summary>
[ToolboxItem(true)]
public class UIBarChart : UIChart
{
private bool NeedDraw;
/// <summary>
/// 重载控件尺寸变更
/// </summary>
/// <param name="e">参数</param>
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
CalcData();
}
/// <summary>
/// 刷新显示
/// </summary>
public override void Refresh()
{
base.Refresh();
if (Option != null)
{
SetOption(Option);
}
CalcData();
}
/// <summary>
/// 更新数据
/// </summary>
/// <param name="seriesName">序列名称</param>
/// <param name="index">序号</param>
/// <param name="value">值</param>
public void Update(string seriesName, int index, double value)
{
var series = Option[seriesName];
if (series != null)
{
series.Update(index, value);
}
}
UILinearScale YScale = new UILinearScale();
/// <summary>
/// 计算数据用于显示
/// </summary>
protected override void CalcData()
{
Bars.Clear();
NeedDraw = false;
if (Option == null || Option.Series == null || Option.SeriesCount == 0) return;
if (DrawSize.Width <= 0 || DrawSize.Height <= 0) return;
if (Option.XAxis.Data.Count == 0) return;
NeedDraw = true;
DrawBarWidth = DrawSize.Width * 1.0f / Option.XAxis.Data.Count;
double min = double.MaxValue;
double max = double.MinValue;
foreach (var series in Option.Series)
{
if (series.Data.Count > 0)
{
min = Math.Min(min, series.Data.Min());
max = Math.Max(max, series.Data.Max());
}
}
if (min > 0 && max > 0 && !Option.YAxis.Scale) min = 0;
if (min < 0 && max < 0 && !Option.YAxis.Scale) max = 0;
if (!Option.YAxis.MaxAuto) max = Option.YAxis.Max;
if (!Option.YAxis.MinAuto) min = Option.YAxis.Min;
if (Option.YAxis.MaxAuto && Option.YAxis.MinAuto)
{
if (min > 0) min = 0;
if (max < 0) max = 0;
if (min.IsZero() && !max.IsZero())
{
max = max * 1.2;
}
if (max.IsZero() && !min.IsZero())
{
min = min * 1.2;
}
if (!max.IsZero() && !min.IsZero())
{
max = max * 1.2;
min = min * 1.2;
}
}
if ((max - min).IsZero())
{
if (min.IsZero())
{
max = 100;
min = 0;
}
else if (max > 0)
{
max = max * 2;
min = 0;
}
else
{
max = 0;
min = min * 2;
}
}
YScale.Max = max;
YScale.Min = min;
YScale.AxisChange();
YAxisStart = YScale.Min;
YAxisEnd = YScale.Max;
YAxisInterval = YScale.Step;
double[] YLabels = YScale.CalcLabels();
float[] labels = YScale.CalcYPixels(YLabels, DrawOrigin.Y, DrawSize.Height);
YAxisDecimalCount = YScale.Format.Replace("F", "").ToInt();
float x1 = DrawBarWidth / (Option.SeriesCount * 2 + Option.SeriesCount + 1);
float x2 = x1 * 2;
for (int i = 0; i < Option.SeriesCount; i++)
{
float barX = DrawOrigin.X;
var series = Option.Series[i];
Bars.TryAdd(i, new List<BarInfo>());
for (int j = 0; j < series.Data.Count; j++)
{
Color color = ChartStyle.GetColor(i);
if (series.Colors.Count > 0 && j >= 0 && j < series.Colors.Count)
color = series.Colors[j];
float xx = barX + x1 * (i + 1) + x2 * i + x1;
float ww = Math.Min(x2, series.MaxWidth);
xx -= ww / 2.0f;
float YZeroPos = YScale.CalcYPixel(0, DrawOrigin.Y, DrawSize.Height);
float VPos = YScale.CalcYPixel(series.Data[j], DrawOrigin.Y, DrawSize.Height);
if (VPos <= YZeroPos)
{
Bars[i].Add(new BarInfo()
{
Rect = new RectangleF(xx, VPos, ww, (YZeroPos - VPos)),
Value = series.Data[j],
Color = color,
Top = true,
Series = series,
});
}
else
{
Bars[i].Add(new BarInfo()
{
Rect = new RectangleF(xx, YZeroPos, ww, (VPos - YZeroPos)),
Value = series.Data[j],
Color = color,
Top = false,
Series = series,
});
}
barX += DrawBarWidth;
}
}
if (Option.ToolTip != null)
{
for (int i = 0; i < Option.XAxis.Data.Count; i++)
{
string str = Option.XAxis.Data[i];
foreach (var series in Option.Series)
{
str += '\n';
str += series.Name + " : " + series.Data[i].ToString("F" + series.DecimalPlaces);
}
Bars[0][i].Tips = str;
}
}
}
private Point DrawOrigin => new Point(Option.Grid.Left, Height - Option.Grid.Bottom);
private Size DrawSize => new Size(Width - Option.Grid.Left - Option.Grid.Right, Height - Option.Grid.Top - Option.Grid.Bottom);
private Rectangle DrawRect => new Rectangle(Option.Grid.Left, Option.Grid.Top, DrawSize.Width, DrawSize.Height);
private int selectIndex = -1;
private float DrawBarWidth;
private double YAxisStart;
private double YAxisEnd;
private double YAxisInterval;
private int YAxisDecimalCount;
private readonly ConcurrentDictionary<int, List<BarInfo>> Bars = new ConcurrentDictionary<int, List<BarInfo>>();
[DefaultValue(-1), Browsable(false)]
private int SelectIndex
{
get => selectIndex;
set
{
if (Option.ToolTip != null && selectIndex != value)
{
selectIndex = value;
Invalidate();
}
if (selectIndex < 0) tip.Visible = false;
}
}
/// <summary>
/// 重载鼠标移动事件
/// </summary>
/// <param name="e">鼠标参数</param>
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
try
{
if (!Option.ToolTip.Visible) return;
if (e.Location.X > Option.Grid.Left && e.Location.X < Width - Option.Grid.Right
&& e.Location.Y > Option.Grid.Top &&
e.Location.Y < Height - Option.Grid.Bottom)
{
SelectIndex = (int)((e.Location.X - Option.Grid.Left) / DrawBarWidth);
}
else
{
SelectIndex = -1;
}
if (SelectIndex >= 0 && Bars.Count > 0)
{
if (tip.Text != Bars[0][selectIndex].Tips)
{
tip.Text = Bars[0][selectIndex].Tips;
tip.Size = new Size((int)Bars[0][selectIndex].Size.Width + 4, (int)Bars[0][selectIndex].Size.Height + 4);
}
int x = e.Location.X + 15;
int y = e.Location.Y + 20;
if (e.Location.X + 15 + tip.Width > Width - Option.Grid.Right)
x = e.Location.X - tip.Width - 2;
if (e.Location.Y + 20 + tip.Height > Height - Option.Grid.Bottom)
y = e.Location.Y - tip.Height - 2;
tip.Left = x;
tip.Top = y;
if (!tip.Visible) tip.Visible = Bars[0][selectIndex].Tips.IsValid();
}
}
catch (Exception exception)
{
Console.WriteLine(exception);
}
}
/// <summary>
/// 图表参数
/// </summary>
[Browsable(false), DefaultValue(null)]
public UIBarOption Option
{
get
{
UIOption option = BaseOption ?? EmptyOption;
return (UIBarOption)option;
}
}
/// <summary>
/// 默认创建空的图表参数
/// </summary>
protected override void CreateEmptyOption()
{
if (emptyOption != null) return;
UIBarOption option = new UIBarOption();
option.Title = new UITitle();
option.Title.Text = "SunnyUI";
option.Title.SubText = "BarChart";
//设置Legend
option.Legend = new UILegend();
option.Legend.Orient = UIOrient.Horizontal;
option.Legend.Top = UITopAlignment.Top;
option.Legend.Left = UILeftAlignment.Left;
option.Legend.AddData("Bar1");
option.Legend.AddData("Bar2");
var series = new UIBarSeries();
series.Name = "Bar1";
series.AddData(1);
series.AddData(5);
series.AddData(2);
series.AddData(4);
series.AddData(3);
option.Series.Add(series);
series = new UIBarSeries();
series.Name = "Bar2";
series.AddData(2);
series.AddData(1);
series.AddData(5);
series.AddData(3);
series.AddData(4);
option.Series.Add(series);
option.XAxis.Data.Add("Mon");
option.XAxis.Data.Add("Tue");
option.XAxis.Data.Add("Wed");
option.XAxis.Data.Add("Thu");
option.XAxis.Data.Add("Fri");
option.ToolTip = new UIBarToolTip();
option.ToolTip.AxisPointer.Type = UIAxisPointerType.Shadow;
emptyOption = option;
}
/// <summary>
/// 绘制图表参数
/// </summary>
/// <param name="g">绘制图面</param>
protected override void DrawOption(Graphics g)
{
if (Option == null) return;
if (!NeedDraw) return;
if (Option.ToolTip != null && Option.ToolTip.AxisPointer.Type == UIAxisPointerType.Shadow) DrawToolTip(g);
DrawSeries(g, Option.Series);
DrawAxis(g);
DrawTitle(g, Option.Title);
if (Option.ToolTip != null && Option.ToolTip.AxisPointer.Type == UIAxisPointerType.Line) DrawToolTip(g);
DrawLegend(g, Option.Legend);
DrawAxisScales(g);
}
/// <summary>
/// 绘制工具提示
/// </summary>
/// <param name="g">绘制图面</param>
protected virtual void DrawToolTip(Graphics g)
{
if (selectIndex < 0) return;
if (Option.ToolTip.AxisPointer.Type == UIAxisPointerType.Line)
{
float x = DrawOrigin.X + SelectIndex * DrawBarWidth + DrawBarWidth / 2.0f;
g.DrawLine(ChartStyle.ToolTipShadowColor, x, DrawOrigin.Y, x, Option.Grid.Top);
}
if (Option.ToolTip.AxisPointer.Type == UIAxisPointerType.Shadow)
{
float x = DrawOrigin.X + SelectIndex * DrawBarWidth;
g.FillRectangle(ChartStyle.ToolTipShadowColor, x, Option.Grid.Top, DrawBarWidth, Height - Option.Grid.Top - Option.Grid.Bottom);
}
}
/// <summary>
/// 绘制坐标轴
/// </summary>
/// <param name="g">绘制图面</param>
protected virtual void DrawAxis(Graphics g)
{
g.FillRectangle(FillColor, Option.Grid.Left, 1, Width - Option.Grid.Left - Option.Grid.Right, Option.Grid.Top);
g.FillRectangle(FillColor, Option.Grid.Left, Height - Option.Grid.Bottom, Width - Option.Grid.Left - Option.Grid.Right, Option.Grid.Bottom - 1);
if (YAxisStart >= 0) g.DrawLine(ForeColor, DrawOrigin, new Point(DrawOrigin.X + DrawSize.Width, DrawOrigin.Y));
if (YAxisEnd <= 0) g.DrawLine(ForeColor, new Point(DrawOrigin.X, Option.Grid.Top), new Point(DrawOrigin.X + DrawSize.Width, Option.Grid.Top));
g.DrawLine(ForeColor, DrawOrigin, new Point(DrawOrigin.X, DrawOrigin.Y - DrawSize.Height));
g.DrawLine(ForeColor, DrawOrigin, new Point(Width - Option.Grid.Right, DrawOrigin.Y));
if (Option.XAxis.AxisTick.Show)
{
float start = DrawOrigin.X + DrawBarWidth / 2.0f;
for (int i = 0; i < Option.XAxis.Data.Count; i++)
{
g.DrawLine(ForeColor, start, DrawOrigin.Y, start, DrawOrigin.Y + Option.XAxis.AxisTick.Length);
start += DrawBarWidth;
}
}
if (Option.XAxis.AxisLabel.Show)
{
float start = DrawOrigin.X + DrawBarWidth / 2.0f;
foreach (var data in Option.XAxis.Data)
{
SizeF sf = g.MeasureString(data, TempFont);
int angle = (Option.XAxis.AxisLabel.Angle + 36000) % 360;
if (angle > 0 && angle <= 90)
g.DrawString(data, TempFont, ForeColor, new PointF(start, DrawOrigin.Y + Option.XAxis.AxisTick.Length),
new StringFormat() { Alignment = StringAlignment.Far }, (3600 - Option.XAxis.AxisLabel.Angle) % 360);
else
g.DrawString(data, TempFont, ForeColor, start - sf.Width / 2.0f, DrawOrigin.Y + Option.XAxis.AxisTick.Length); start += DrawBarWidth;
}
SizeF sfname = g.MeasureString(Option.XAxis.Name, TempFont);
g.DrawString(Option.XAxis.Name, TempFont, ForeColor, DrawOrigin.X + (DrawSize.Width - sfname.Width) / 2.0f, DrawOrigin.Y + Option.XAxis.AxisTick.Length + sfname.Height);
}
double[] YLabels = YScale.CalcLabels();
float[] labels = YScale.CalcYPixels(YLabels, DrawOrigin.Y, DrawSize.Height);
for (int i = 0; i < labels.Length; i++)
{
if (labels[i] > DrawOrigin.Y) continue;
if (labels[i] < Option.Grid.Top) continue;
if (Option.YAxis.AxisTick.Show)
{
g.DrawLine(ForeColor, DrawOrigin.X, labels[i], DrawOrigin.X - Option.YAxis.AxisTick.Length, labels[i]);
if (!YLabels[i].EqualsDouble(0))
{
using (Pen pn = new Pen(ForeColor))
{
pn.DashStyle = DashStyle.Dash;
pn.DashPattern = new float[] { 3, 3 };
g.DrawLine(pn, DrawOrigin.X, labels[i], Width - Option.Grid.Right, labels[i]);
}
}
else
{
g.DrawLine(ForeColor, DrawOrigin.X, labels[i], Width - Option.Grid.Right, labels[i]);
}
}
if (Option.YAxis.AxisLabel.Show)
{
string label = YLabels[i].ToString(Option.YAxis.AxisLabel.DecimalPlaces >= 0 ? "F" + Option.YAxis.AxisLabel.DecimalPlaces : YScale.Format);
SizeF sf = g.MeasureString(label, TempFont);
g.DrawString(label, TempFont, ForeColor, DrawOrigin.X - Option.YAxis.AxisTick.Length - sf.Width, labels[i] - sf.Height / 2.0f);
}
}
}
private void DrawAxisScales(Graphics g)
{
foreach (var line in Option.YAxisScaleLines)
{
double ymin = YAxisStart * YAxisInterval;
double ymax = YAxisEnd * YAxisInterval;
float pos = YScale.CalcYPixel(line.Value, DrawOrigin.Y, DrawSize.Height);
if (pos <= Option.Grid.Top || pos >= Height - Option.Grid.Bottom) continue;
using (Pen pn = new Pen(line.Color, line.Size))
{
g.DrawLine(pn, DrawOrigin.X, pos, Width - Option.Grid.Right, pos);
}
SizeF sf = g.MeasureString(line.Name, TempFont);
if (line.Left == UILeftAlignment.Left)
g.DrawString(line.Name, TempFont, line.Color, DrawOrigin.X + 4, pos - 2 - sf.Height);
if (line.Left == UILeftAlignment.Center)
g.DrawString(line.Name, TempFont, line.Color, DrawOrigin.X + (Width - Option.Grid.Left - Option.Grid.Right - sf.Width) / 2, pos - 2 - sf.Height);
if (line.Left == UILeftAlignment.Right)
g.DrawString(line.Name, TempFont, line.Color, Width - sf.Width - 4 - Option.Grid.Right, pos - 2 - sf.Height);
}
}
/// <summary>
/// 绘制序列
/// </summary>
/// <param name="g">绘制图面</param>
/// <param name="series">序列</param>
protected virtual void DrawSeries(Graphics g, List<UIBarSeries> series)
{
if (series == null || series.Count == 0) return;
for (int i = 0; i < Bars.Count; i++)
{
var bars = Bars[i];
foreach (var info in bars)
{
g.FillRectangle(info.Color, info.Rect);
if (Option.ShowValue)
{
string value = info.Value.ToString("F" + info.Series.DecimalPlaces);
SizeF sf = g.MeasureString(value, TempFont);
if (info.Top)
{
float top = info.Rect.Top - sf.Height;
if (top > Option.Grid.Top)
{
g.DrawString(value, TempFont, info.Color, info.Rect.Center().X - sf.Width / 2, top);
}
}
else
{
if (info.Rect.Bottom + sf.Height + Option.Grid.Bottom < Height)
{
g.DrawString(value, TempFont, info.Color, info.Rect.Center().X - sf.Width / 2, info.Rect.Bottom);
}
}
}
}
}
for (int i = 0; i < Option.XAxis.Data.Count; i++)
{
Bars[0][i].Size = g.MeasureString(Bars[0][i].Tips, TempFont);
}
}
private class BarInfo
{
public RectangleF Rect { get; set; }
public string Tips { get; set; }
public SizeF Size { get; set; }
public Color Color { get; set; }
public double Value { get; set; }
public bool Top { get; set; }
public UIBarSeries Series { get; set; }
}
}
}