Files
29WxOrder/29WxOrderWpf/Ctrls/Statistics/Charts/RevenueLineChart.xaml.cs
T

242 lines
7.5 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.
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&lt;RevenueTrendPoint&gt;)即可,数据变化时自动重绘。
/// 鼠标悬停在数据点圆圈上时显示该点数据(日期 / 销售额 / 订单数)。
/// </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");
}
}
}