CPF/CPF/Charts/PieChart.cs
2024-06-06 20:22:18 +08:00

458 lines
17 KiB
C#

using CPF.Controls;
using CPF.Drawing;
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.ComponentModel;
using CPF.Input;
namespace CPF.Charts
{
/// <summary>
/// 饼图
/// </summary>
[Description("饼图")]
public class PieChart : Control
{
public PieChart()
{
Data = new Collection<PieChartData>();
}
[PropertyChanged(nameof(Data))]
void OnDataChanged(object newValue, object oldValue, PropertyMetadataAttribute attribute)
{
var nv = newValue as Collection<PieChartData>;
if (nv != null)
{
foreach (var item in nv)
{
Nv_CollectionChanged(nv, new CollectionChangedEventArgs<PieChartData>(item, 0, null, CollectionChangedAction.Add));
}
nv.CollectionChanged += Nv_CollectionChanged;
}
else
{
var data = new Collection<PieChartData>();
Data = data;
var list = newValue as IEnumerable<PieChartData>;
if (list != null)
{
foreach (var item in list)
{
data.Add(item);
}
}
}
}
private void Nv_CollectionChanged(object sender, CollectionChangedEventArgs<PieChartData> e)
{
switch (e.Action)
{
case CollectionChangedAction.Add:
e.NewItem.PropertyChanged += NewItem_PropertyChanged;
break;
case CollectionChangedAction.Remove:
e.OldItem.PropertyChanged -= NewItem_PropertyChanged;
break;
case CollectionChangedAction.Replace:
e.NewItem.PropertyChanged += NewItem_PropertyChanged;
e.OldItem.PropertyChanged -= NewItem_PropertyChanged;
break;
}
InvalidateData();
}
private void NewItem_PropertyChanged(object sender, CPFPropertyChangedEventArgs e)
{
InvalidateData();
}
bool refreshData;
/// <summary>
/// 刷新数据,下次更新的时候更新
/// </summary>
public void InvalidateData()
{
if (!refreshData)
{
refreshData = true;
BeginInvoke(() =>
{
UpdateData();
});
}
}
public void UpdateData()
{
Invalidate();
var data = Data;
HasData = data.Count > 0;
refreshData = false;
var template = SerieTemplate;
if (seriesPanel && template != null)
{
seriesPanel.Children.Clear();
foreach (var item in Data)
{
var c = template.CreateElement();
c.DataContext = item;
c.Name = item.Name;
//c.Value = item.Value;
seriesPanel.Children.Add(c);
}
}
foreach (var item in paths)
{
item.Item1.Dispose();
}
paths.Clear();
foreach (var item in Children.Where(a => a.Tag == this && a is CPF.Shapes.Path).ToArray())
{
Children.Remove(item);
item.Dispose();
}
var padding = Padding;
//绘制值标签
double StartAngle = 0;
var ownerSize = ActualSize;
var bounds = new Rect(new Point(), ownerSize);
var sum = data.Sum(a => a.Value);
var ringWidth = RingWidth;
var r = Math.Min(ownerSize.Width - padding.Horizontal, ownerSize.Height - padding.Vertical) / 2;
if (r < 1)
{
return;
}
for (int i = 0; i < data.Count; i++)
{
var angle = data[i].Value * 360 / sum;
if (angle == 360)
{
angle = 359.999;
}
var centerX = padding.Left + (ownerSize.Width - padding.Horizontal) / 2;
var centerY = (padding.Top + (ownerSize.Height - padding.Vertical) / 2);
var path = new PathGeometry();
if (ringWidth.IsAuto)
{
path.BeginFigure(centerX, centerY);
}
else
{
var ww = Math.Max(1, Math.Min(ringWidth.GetActualValue(r), r));
path.BeginFigure((float)Math.Sin(StartAngle * Math.PI / 180) * (r - ww) + centerX, centerY - (float)Math.Cos(StartAngle * Math.PI / 180) * (r - ww));
}
path.LineTo((float)Math.Sin(StartAngle * Math.PI / 180) * r + centerX, centerY - (float)Math.Cos(StartAngle * Math.PI / 180) * r);
StartAngle += angle;
path.ArcTo(new Point((float)Math.Sin(StartAngle * Math.PI / 180) * r + centerX, centerY - (float)Math.Cos(StartAngle * Math.PI / 180) * r), new Size(r, r), (float)angle, true, angle > 180);
if (!ringWidth.IsAuto)
{
var ww = Math.Max(1, Math.Min(ringWidth.GetActualValue(r), r));
path.LineTo((float)Math.Sin(StartAngle * Math.PI / 180) * (r - ww) + centerX, centerY - (float)Math.Cos(StartAngle * Math.PI / 180) * (r - ww));
path.ArcTo(new Point((float)Math.Sin((StartAngle - angle) * Math.PI / 180) * (r - ww) + centerX, centerY - (float)Math.Cos((StartAngle - angle) * Math.PI / 180) * (r - ww)), new Size(r - ww, r - ww), (float)angle, false, angle > 180);
}
path.EndFigure(true);
Children.Add(new CPF.Shapes.Path
{
PresenterFor = this,
Name = data[i].Name,
Fill = data[i].Fill,
ZIndex = -1,
Data = path,
MarginLeft = -padding.Left,
MarginTop = -padding.Top,
Tag = this,
StrokeFill = null,
});
paths.Add((path, data[i]));
}
}
public IList<PieChartData> Data
{
get { return GetValue<IList<PieChartData>>(); }
set { SetValue(value); }
}
/// <summary>
/// 指示的线条填充
/// </summary>
[UIPropertyMetadata(typeof(ViewFill), "#aaa", UIPropertyOptions.AffectsRender)]
public ViewFill TipLineFill
{
get { return GetValue<ViewFill>(); }
set { SetValue(value); }
}
/// <summary>
/// 圆环宽度
/// </summary>
[Description("圆环宽度"), UIPropertyMetadata(typeof(FloatField), "auto", UIPropertyOptions.AffectsRender)]
public FloatField RingWidth
{
get { return GetValue<FloatField>(); }
set { SetValue(value); }
}
/// <summary>
/// 图例模板
/// </summary>
public UIElementTemplate<SerieItem> SerieTemplate
{
get { return GetValue<UIElementTemplate<SerieItem>>(); }
set { SetValue(value); }
}
/// <summary>
/// 是否有数据
/// </summary>
[Description("是否有数据")]
public bool HasData
{
get { return GetValue<bool>(); }
private set { SetValue(value); }
}
protected override void OnOverrideMetadata(OverrideMetadata overridePropertys)
{
base.OnOverrideMetadata(overridePropertys);
overridePropertys.Override(nameof(SerieTemplate), new PropertyMetadataAttribute((UIElementTemplate<SerieItem>)typeof(SerieItem)));
overridePropertys.Override(nameof(Padding), new UIPropertyMetadataAttribute(new Thickness(30, 30, 30, 20), UIPropertyOptions.AffectsMeasure));
}
protected override void InitializeComponent()
{
Children.Add(new WrapPanel
{
Name = "seriesPanel",
PresenterFor = this,
Orientation = Orientation.Horizontal,
MaxWidth = "100%",
MarginTop = -30,
});
Children.Add(new StackPanel
{
Name = "tipPanel",
PresenterFor = this,
Orientation = Orientation.Vertical,
Visibility = Visibility.Collapsed,
IsHitTestVisible = false,
Background = "#ffffff99",
BorderFill = "#00000055",
BorderStroke = new Stroke(1),
Children =
{
new TextBlock
{
Name="tipName",
PresenterFor=this,
MarginLeft=3,
},
new StackPanel
{
Name = "tipListPanel",
PresenterFor = this,
Orientation= Orientation.Vertical,
Margin="5",
}
}
});
}
Panel seriesPanel;
Panel tipPanel;
TextBlock tipName;
Panel tipListPanel;
protected override void OnInitializedAsync()
{
base.OnInitializedAsync();
seriesPanel = FindPresenterByName<Panel>("seriesPanel");
tipPanel = FindPresenterByName<Panel>("tipPanel");
tipName = FindPresenterByName<TextBlock>("tipName");
tipListPanel = FindPresenterByName<Panel>("tipListPanel");
}
//protected override void OnLayoutUpdated()
//{
// base.OnLayoutUpdated();
// InvalidateData();
//}
[PropertyChanged(nameof(ActualSize))]
void OnAcSizeChanged(object newValue, object oldValue, PropertyMetadataAttribute attribute)
{
InvalidateData();
}
List<(PathGeometry, PieChartData)> paths = new List<(PathGeometry, PieChartData)>();
protected override void OnRender(DrawingContext g)
{
base.OnRender(g);
//绘制值标签
var ownerSize = ActualSize;
var bounds = new Rect(new Point(), ownerSize);
double StartAngle = 0;
var data = Data;
var padding = Padding;
var sum = data.Sum(a => a.Value);
var ringWidth = RingWidth;
var r = Math.Min(ownerSize.Width - padding.Horizontal, ownerSize.Height - padding.Vertical) / 2;
for (int i = 0; i < data.Count; i++)
{
//using (var br = data[i].Fill.CreateBrush(bounds, Root.RenderScaling))
{
var angle = data[i].Value * 360 / sum;
var centerX = padding.Left + (ownerSize.Width - padding.Horizontal) / 2;
var centerY = (padding.Top + (ownerSize.Height - padding.Vertical) / 2);
//using (PathGeometry path = new PathGeometry())
//{
// if (ringWidth.IsAuto)
// {
// path.BeginFigure(centerX, centerY);
// }
// else
// {
// var ww = Math.Max(1, Math.Min(ringWidth.GetActualValue(r), r));
// path.BeginFigure((float)Math.Sin(StartAngle * Math.PI / 180) * (r - ww) + centerX, centerY - (float)Math.Cos(StartAngle * Math.PI / 180) * (r - ww));
// }
// path.LineTo((float)Math.Sin(StartAngle * Math.PI / 180) * r + centerX, centerY - (float)Math.Cos(StartAngle * Math.PI / 180) * r);
StartAngle += angle;
// path.ArcTo(new Point((float)Math.Sin(StartAngle * Math.PI / 180) * r + centerX, centerY - (float)Math.Cos(StartAngle * Math.PI / 180) * r), new Size(r, r), (float)angle, true, angle > 180);
// if (!ringWidth.IsAuto)
// {
// var ww = Math.Max(1, Math.Min(ringWidth.GetActualValue(r), r));
// path.LineTo((float)Math.Sin(StartAngle * Math.PI / 180) * (r - ww) + centerX, centerY - (float)Math.Cos(StartAngle * Math.PI / 180) * (r - ww));
// path.ArcTo(new Point((float)Math.Sin((StartAngle - angle) * Math.PI / 180) * (r - ww) + centerX, centerY - (float)Math.Cos((StartAngle - angle) * Math.PI / 180) * (r - ww)), new Size(r - ww, r - ww), (float)angle, false, angle > 180);
// }
// path.EndFigure(true);
// g.FillPath(br, path);
//}
var an = StartAngle - angle / 2;
var tipFill = TipLineFill;
var rr = 12;
var l = (float)Math.Sin(an * Math.PI / 180) * (r + rr) + centerX;
if (tipFill != null)
{
using (var tip = tipFill.CreateBrush(bounds, Root.RenderScaling))
{
var point = new Point((float)Math.Sin(an * Math.PI / 180) * (r + rr) + centerX, centerY - (float)Math.Cos(an * Math.PI / 180) * (r + rr));
g.DrawLine(new Stroke(1), tip, new Point((float)Math.Sin(an * Math.PI / 180) * r + centerX, centerY - (float)Math.Cos(an * Math.PI / 180) * r), point);
g.DrawLine(new Stroke(1), tip, point, new Point(point.X + (l > centerX ? 8 : -8), point.Y));
}
}
var fore = Foreground;
if (fore != null)
{
using (Font font = new Font(FontFamily, FontSize, FontStyle))
{
using (var f = fore.CreateBrush(bounds, Root.RenderScaling))
{
var str = data[i].Name;
var s = DrawingFactory.Default.MeasureString(str, font);
if (l > centerX)
{
g.DrawString(new Point(l + 8, centerY - (float)Math.Cos(an * Math.PI / 180) * (r + rr) - s.Height / 2), f, str, font);
}
else
{
g.DrawString(new Point(l - s.Width - 8, centerY - (float)Math.Cos(an * Math.PI / 180) * (r + rr) - s.Height / 2), f, str, font);
}
}
}
}
}
}
//foreach (var item in paths)
//{
// if (item.Item2.Fill != null)
// {
// using (var br = item.Item2.Fill.CreateBrush(bounds, Root.RenderScaling))
// {
// g.FillPath(br, item.Item1);
// }
// }
//}
}
/// <summary>
/// 通过坐标测试选中的数据
/// </summary>
/// <param name="point"></param>
/// <returns></returns>
public PieChartData HitTestData(Point point)
{
foreach (var item in paths)
{
if (item.Item1.Contains(point.X, point.Y))
{
return item.Item2;
}
}
return null;
}
PieChartData hoverChartData;
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.Handled)
{
return;
}
var hitTest = HitTestData(e.Location);
if (hitTest != null)
{
if (tipPanel)
{
if (hoverChartData != hitTest)
{
tipPanel.Visibility = Visibility.Visible;
tipListPanel.Children.Clear();
var template = SerieTemplate;
if (template != null)
{
var c = template.CreateElement();
c.DataContext = hitTest;
c.Name = hitTest.Name;
c.Value = hitTest.Value;
tipListPanel.Children.Add(c);
}
var padding = Padding;
tipPanel.MarginLeft = e.Location.X + 5 - padding.Left;
tipPanel.MarginTop = e.Location.Y + 5 - padding.Top;
}
}
}
else
{
if (tipPanel)
{
tipPanel.Visibility = Visibility.Collapsed;
}
}
hoverChartData = hitTest;
}
protected override void Dispose(bool disposing)
{
foreach (var item in paths)
{
item.Item1.Dispose();
}
paths.Clear();
base.Dispose(disposing);
}
}
}