242 lines
7.5 KiB
C#
242 lines
7.5 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Windows;
|
||
using System.Windows.Controls;
|
||
using System.Windows.Media;
|
||
using System.Windows.Shapes;
|
||
using _29WxOrderWpf.Ctrls.Statistics.Models;
|
||
|
||
namespace _29WxOrderWpf.Ctrls.Statistics.Charts
|
||
{
|
||
/// <summary>
|
||
/// 销售趋势折线图(轻量自绘,无第三方依赖)。
|
||
/// 绑定 <see cref="Points"/>(IList<RevenueTrendPoint>)即可,数据变化时自动重绘。
|
||
/// 鼠标悬停在数据点圆圈上时显示该点数据(日期 / 销售额 / 订单数)。
|
||
/// </summary>
|
||
public partial class RevenueLineChart : UserControl
|
||
{
|
||
public static readonly DependencyProperty PointsProperty =
|
||
DependencyProperty.Register("Points", typeof(IList<RevenueTrendPoint>), typeof(RevenueLineChart),
|
||
new PropertyMetadata(null, OnRenderPropertyChanged));
|
||
|
||
/// <summary>趋势数据点</summary>
|
||
public IList<RevenueTrendPoint> Points
|
||
{
|
||
get { return (IList<RevenueTrendPoint>)GetValue(PointsProperty); }
|
||
set { SetValue(PointsProperty, value); }
|
||
}
|
||
|
||
public static readonly DependencyProperty LineBrushProperty =
|
||
DependencyProperty.Register("LineBrush", typeof(Brush), typeof(RevenueLineChart),
|
||
new PropertyMetadata(new SolidColorBrush(Color.FromRgb(22, 119, 255)), OnRenderPropertyChanged));
|
||
|
||
/// <summary>折线颜色</summary>
|
||
public Brush LineBrush
|
||
{
|
||
get { return (Brush)GetValue(LineBrushProperty); }
|
||
set { SetValue(LineBrushProperty, value); }
|
||
}
|
||
|
||
public static readonly DependencyProperty LineThicknessProperty =
|
||
DependencyProperty.Register("LineThickness", typeof(double), typeof(RevenueLineChart),
|
||
new PropertyMetadata(2d, OnRenderPropertyChanged));
|
||
|
||
/// <summary>折线粗细</summary>
|
||
public double LineThickness
|
||
{
|
||
get { return (double)GetValue(LineThicknessProperty); }
|
||
set { SetValue(LineThicknessProperty, value); }
|
||
}
|
||
|
||
public RevenueLineChart()
|
||
{
|
||
InitializeComponent();
|
||
Loaded += delegate { Render(); };
|
||
SizeChanged += delegate { Render(); };
|
||
}
|
||
|
||
private static void OnRenderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||
{
|
||
((RevenueLineChart)d).Render();
|
||
}
|
||
|
||
private void Render()
|
||
{
|
||
LayoutRoot.Children.Clear();
|
||
var points = Points;
|
||
if (points == null || points.Count == 0 || ActualWidth <= 20 || ActualHeight <= 20)
|
||
return;
|
||
|
||
const double marginLeft = 52;
|
||
const double marginRight = 16;
|
||
const double marginTop = 18;
|
||
const double marginBottom = 30;
|
||
|
||
double plotWidth = Math.Max(10, ActualWidth - marginLeft - marginRight);
|
||
double plotHeight = Math.Max(10, ActualHeight - marginTop - marginBottom);
|
||
|
||
var values = points.Select(p => (double)p.SalesAmount).ToList();
|
||
double maxValue = values.Max();
|
||
if (maxValue <= 0) maxValue = 1;
|
||
double yMax = maxValue * 1.15;
|
||
|
||
var lineBrush = LineBrush ?? new SolidColorBrush(Color.FromRgb(22, 119, 255));
|
||
Color lineColor = lineBrush is SolidColorBrush ? ((SolidColorBrush)lineBrush).Color : Color.FromRgb(22, 119, 255);
|
||
|
||
int count = points.Count;
|
||
double stepX = count <= 1 ? 0 : plotWidth / (count - 1);
|
||
|
||
Func<int, double> getX = i => marginLeft + stepX * i;
|
||
Func<double, double> getY = v => marginTop + plotHeight * (1 - v / yMax);
|
||
|
||
// 1. 横向网格线 + Y 轴刻度
|
||
for (int k = 0; k <= 4; k++)
|
||
{
|
||
double ratio = k / 4.0;
|
||
double y = marginTop + plotHeight * ratio;
|
||
double v = yMax * (1 - ratio);
|
||
|
||
LayoutRoot.Children.Add(new Line
|
||
{
|
||
X1 = marginLeft, Y1 = y, X2 = marginLeft + plotWidth, Y2 = y,
|
||
Stroke = new SolidColorBrush(Color.FromRgb(238, 238, 238)),
|
||
StrokeThickness = 1
|
||
});
|
||
|
||
var yLabel = new TextBlock
|
||
{
|
||
Text = FormatValue(v),
|
||
FontSize = 11,
|
||
Foreground = new SolidColorBrush(Color.FromRgb(153, 153, 153)),
|
||
Width = marginLeft - 6,
|
||
TextAlignment = TextAlignment.Right
|
||
};
|
||
LayoutRoot.Children.Add(yLabel);
|
||
Canvas.SetLeft(yLabel, 0);
|
||
Canvas.SetTop(yLabel, y - 8);
|
||
}
|
||
|
||
// 2. 面积填充
|
||
if (count > 1)
|
||
{
|
||
var figure = new PathFigure { StartPoint = new Point(getX(0), getY(values[0])) };
|
||
for (int i = 1; i < count; i++)
|
||
figure.Segments.Add(new LineSegment { Point = new Point(getX(i), getY(values[i])) });
|
||
figure.Segments.Add(new LineSegment { Point = new Point(getX(count - 1), marginTop + plotHeight) });
|
||
figure.Segments.Add(new LineSegment { Point = new Point(getX(0), marginTop + plotHeight) });
|
||
figure.IsClosed = true;
|
||
|
||
var gradient = new LinearGradientBrush
|
||
{
|
||
StartPoint = new Point(0, 0),
|
||
EndPoint = new Point(0, 1)
|
||
};
|
||
gradient.GradientStops.Add(new GradientStop(Color.FromArgb(64, lineColor.R, lineColor.G, lineColor.B), 0));
|
||
gradient.GradientStops.Add(new GradientStop(Color.FromArgb(8, lineColor.R, lineColor.G, lineColor.B), 1));
|
||
|
||
LayoutRoot.Children.Add(new Path
|
||
{
|
||
Data = new PathGeometry { Figures = { figure } },
|
||
Fill = gradient
|
||
});
|
||
}
|
||
|
||
// 3. 折线
|
||
var polylinePoints = new PointCollection();
|
||
for (int i = 0; i < count; i++)
|
||
polylinePoints.Add(new Point(getX(i), getY(values[i])));
|
||
|
||
LayoutRoot.Children.Add(new Polyline
|
||
{
|
||
Points = polylinePoints,
|
||
Stroke = lineBrush,
|
||
StrokeThickness = LineThickness,
|
||
StrokeLineJoin = PenLineJoin.Round
|
||
});
|
||
|
||
// 4. 数据点(点较多时省略;悬停显示该点数据)
|
||
if (count <= 31)
|
||
{
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var dot = new Ellipse
|
||
{
|
||
ToolTip = CreateTooltip(points[i])
|
||
};
|
||
dot. Style = (Style)FindResource("LineEllipse");
|
||
//// 悬停时高亮圆点
|
||
//dot.MouseEnter += (s, e) => dot.Fill = lineBrush;
|
||
//dot.MouseLeave += (s, e) => dot.Fill = Brushes.White;
|
||
|
||
LayoutRoot.Children.Add(dot);
|
||
Canvas.SetLeft(dot, getX(i) - 3.5);
|
||
Canvas.SetTop(dot, getY(values[i]) - 3.5);
|
||
}
|
||
}
|
||
|
||
// 5. X 轴日期刻度(最多显示 8 个)
|
||
int labelStep = Math.Max(1, (int)Math.Ceiling(count / 8.0));
|
||
for (int i = 0; i < count; i += labelStep)
|
||
{
|
||
var tb = new TextBlock
|
||
{
|
||
Text = points[i].DateText,
|
||
FontSize = 11,
|
||
Foreground = new SolidColorBrush(Color.FromRgb(153, 153, 153)),
|
||
TextAlignment = TextAlignment.Center,
|
||
Width = 60
|
||
};
|
||
LayoutRoot.Children.Add(tb);
|
||
double x = getX(i) - 30;
|
||
x = Math.Max(0, Math.Min(ActualWidth - 60, x));
|
||
Canvas.SetLeft(tb, x);
|
||
Canvas.SetTop(tb, marginTop + plotHeight + 6);
|
||
}
|
||
}
|
||
|
||
/// <summary>生成数据点悬浮提示(日期 / 销售额 / 订单数)</summary>
|
||
private static object CreateTooltip(RevenueTrendPoint p)
|
||
{
|
||
var panel = new StackPanel();
|
||
var txtBrush = new SolidColorBrush(Color.FromRgb(51, 51, 51));
|
||
panel.Children.Add(new TextBlock
|
||
{
|
||
Text = p.Date.ToString("yyyy-MM-dd"),
|
||
FontSize = 11,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = txtBrush
|
||
});
|
||
panel.Children.Add(new TextBlock
|
||
{
|
||
Text = "销售额:" + FormatValue((double)p.SalesAmount) + "元",
|
||
FontSize = 11,
|
||
Foreground = txtBrush
|
||
});
|
||
panel.Children.Add(new TextBlock
|
||
{
|
||
Text = "订单数:" + p.OrderCount + " 单",
|
||
FontSize = 11,
|
||
Foreground = txtBrush
|
||
});
|
||
|
||
return new Border
|
||
{
|
||
Background = Brushes.White,
|
||
CornerRadius = new CornerRadius(4),
|
||
Padding = new Thickness(8, 5, 8, 5),
|
||
Child = panel
|
||
};
|
||
}
|
||
|
||
private static string FormatValue(double value)
|
||
{
|
||
double abs = Math.Abs(value);
|
||
if (abs >= 100000000)
|
||
return (value / 100000000d).ToString("0.00") + "亿";
|
||
if (abs >= 10000)
|
||
return (value / 10000d).ToString("0.00") + "万";
|
||
return value.ToString("#,##0.00");
|
||
}
|
||
}
|
||
} |