直方图将柱状图替换为面积图,优化密集数据显示效果,Y轴刻度自动取整支持 K/M 缩写,X 轴根据数据范围自动设置。

This commit is contained in:
QI Mingxuan
2026-05-21 10:37:28 +08:00
parent ef83a7637a
commit d7c027b732
3 changed files with 131 additions and 20 deletions
@@ -13,7 +13,7 @@ namespace XP.Common.Controls.ImageHistogram
internal sealed class ChartRenderer
{
private readonly RadCartesianChart _chart;
private readonly BarSeries _barSeries;
private readonly ScatterAreaSeries _areaSeries;
private readonly LinearAxis _xAxis;
/// <summary>
@@ -25,12 +25,12 @@ namespace XP.Common.Controls.ImageHistogram
/// 构造函数,接收 RadCartesianChart 实例 | Constructor, receives RadCartesianChart instance
/// </summary>
/// <param name="chart">图表控件实例 | Chart control instance</param>
/// <param name="barSeries">柱状图系列 | Bar series</param>
/// <param name="areaSeries">面积图系列 | Area series</param>
/// <param name="xAxis">X 轴 | X axis</param>
public ChartRenderer(RadCartesianChart chart, BarSeries barSeries, LinearAxis xAxis)
public ChartRenderer(RadCartesianChart chart, ScatterAreaSeries areaSeries, LinearAxis xAxis)
{
_chart = chart ?? throw new ArgumentNullException(nameof(chart));
_barSeries = barSeries ?? throw new ArgumentNullException(nameof(barSeries));
_areaSeries = areaSeries ?? throw new ArgumentNullException(nameof(areaSeries));
_xAxis = xAxis ?? throw new ArgumentNullException(nameof(xAxis));
}
@@ -64,6 +64,8 @@ namespace XP.Common.Controls.ImageHistogram
// 设置 X 轴范围 | Set X axis range
_xAxis.Minimum = 0;
_xAxis.Maximum = xAxisMax;
// 根据范围自动设置刻度间隔(保持 4-5 个刻度)| Auto set major step based on range (keep 4-5 ticks)
_xAxis.MajorStep = xAxisMax <= 255 ? 64 : 16384;
// 构建数据点 | Build data points
var dataPoints = new List<HistogramDataPoint>();
@@ -103,7 +105,7 @@ namespace XP.Common.Controls.ImageHistogram
}
// 更新图表数据 | Update chart data
_barSeries.ItemsSource = dataPoints;
_areaSeries.ItemsSource = dataPoints;
// 设置 Y 轴范围 | Set Y axis range
UpdateYAxis(displayData, isLogarithmic);
@@ -114,7 +116,7 @@ namespace XP.Common.Controls.ImageHistogram
/// </summary>
public void Clear()
{
_barSeries.ItemsSource = null;
_areaSeries.ItemsSource = null;
// X 轴范围重置为 0-255 | Reset X axis range to 0-255
_xAxis.Minimum = 0;
@@ -131,9 +133,9 @@ namespace XP.Common.Controls.ImageHistogram
{
get
{
if (_barSeries.ItemsSource is ICollection<HistogramDataPoint> collection)
if (_areaSeries.ItemsSource is ICollection<HistogramDataPoint> collection)
return collection.Count;
if (_barSeries.ItemsSource is IEnumerable<HistogramDataPoint> enumerable)
if (_areaSeries.ItemsSource is IEnumerable<HistogramDataPoint> enumerable)
return enumerable.Count();
return 0;
}
@@ -173,7 +175,48 @@ namespace XP.Common.Controls.ImageHistogram
if (maxValue == 0)
maxValue = 1;
SetYAxisRange(0, maxValue, isLogarithmic);
// 计算取整的 MajorStep(约 4 个刻度,对齐到 K/M 整数倍)| Calculate rounded MajorStep (~4 ticks, aligned to K/M multiples)
if (_chart.VerticalAxis is LinearAxis linearAxis)
{
long rawStep = maxValue / 4;
long step = RoundStepToNice(rawStep);
if (step < 1) step = 1;
linearAxis.MajorStep = step;
// 将最大值向上取整到 step 的整数倍 | Round max up to multiple of step
long roundedMax = ((maxValue / step) + 1) * step;
SetYAxisRange(0, roundedMax, isLogarithmic);
}
else
{
SetYAxisRange(0, maxValue, isLogarithmic);
}
}
/// <summary>
/// 将步长取整为"好看"的数值(1, 2, 5 的倍数 × 10^n| Round step to "nice" value (multiples of 1, 2, 5 × 10^n)
/// 例如:123456 → 100000350000 → 500000780000 → 1000000
/// </summary>
private static long RoundStepToNice(long rawStep)
{
if (rawStep <= 0) return 1;
// 找到数量级 | Find order of magnitude
double magnitude = Math.Pow(10, Math.Floor(Math.Log10(rawStep)));
double normalized = rawStep / magnitude;
// 取整到 1, 2, 5 中最近的 | Round to nearest of 1, 2, 5
double niceNormalized;
if (normalized <= 1.5)
niceNormalized = 1;
else if (normalized <= 3.5)
niceNormalized = 2;
else if (normalized <= 7.5)
niceNormalized = 5;
else
niceNormalized = 10;
return (long)(niceNormalized * magnitude);
}
/// <summary>
@@ -0,0 +1,49 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace XP.Common.Controls.ImageHistogram
{
/// <summary>
/// 频次标签转换器:将大数值转为 K/M 缩写格式 | Frequency label converter: converts large values to K/M abbreviation format
/// 例如:500000 → "500K"1500000 → "1.5M"800 → "800"
/// </summary>
internal sealed class FrequencyLabelConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null) return "0";
double num;
if (value is double d)
num = d;
else if (value is decimal dec)
num = (double)dec;
else if (!double.TryParse(value.ToString(), out num))
return value.ToString() ?? "0";
if (num >= 1_000_000)
{
double mValue = num / 1_000_000.0;
return mValue == Math.Floor(mValue)
? $"{(int)mValue}M"
: $"{mValue:0.#}M";
}
if (num >= 1_000)
{
double kValue = num / 1_000.0;
return kValue == Math.Floor(kValue)
? $"{(int)kValue}K"
: $"{kValue:0.#}K";
}
return $"{(int)num}";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
@@ -3,39 +3,58 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions"
xmlns:local="clr-namespace:XP.Common.Controls.ImageHistogram"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="400">
<UserControl.Resources>
<local:FrequencyLabelConverter x:Key="FreqConverter"/>
</UserControl.Resources>
<Grid>
<!-- 图表控件 | Chart control -->
<telerik:RadCartesianChart x:Name="HistogramChart">
<telerik:RadCartesianChart x:Name="HistogramChart" Padding="0">
<!-- 禁用 Telerik 自带的无数据提示 | Disable Telerik built-in empty content -->
<telerik:RadCartesianChart.EmptyContent>
<TextBlock/>
</telerik:RadCartesianChart.EmptyContent>
<!-- X 轴:灰度级别 | X Axis: Gray Level -->
<!-- X 轴:灰度级别(缩小字体,控制刻度数量,K/M 缩写)| X Axis: Gray Level -->
<telerik:RadCartesianChart.HorizontalAxis>
<telerik:LinearAxis x:Name="XAxis"
Minimum="0"
Maximum="255"
Title="灰度级别"/>
MajorStep="64"
FontSize="9">
<telerik:LinearAxis.LabelTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource FreqConverter}}"/>
</DataTemplate>
</telerik:LinearAxis.LabelTemplate>
</telerik:LinearAxis>
</telerik:RadCartesianChart.HorizontalAxis>
<!-- Y 轴:像素频次(默认线性| Y Axis: Pixel Frequency (default linear) -->
<!-- Y 轴:像素频次(K/M 缩写标签| Y Axis: Pixel Frequency (K/M abbreviation labels) -->
<telerik:RadCartesianChart.VerticalAxis>
<telerik:LinearAxis x:Name="YAxis"
Minimum="0"
Maximum="1"
Title="频次"/>
FontSize="9">
<telerik:LinearAxis.LabelTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource FreqConverter}}"/>
</DataTemplate>
</telerik:LinearAxis.LabelTemplate>
</telerik:LinearAxis>
</telerik:RadCartesianChart.VerticalAxis>
<!-- 柱状图系列 | Bar Series -->
<!-- 面积图系列(适合密集直方图数据)| Area Series (suitable for dense histogram data) -->
<telerik:RadCartesianChart.Series>
<telerik:BarSeries x:Name="HistogramBarSeries"
ValueBinding="Frequency"
CategoryBinding="GrayLevel"
ShowLabels="False"/>
<telerik:ScatterAreaSeries x:Name="HistogramBarSeries"
XValueBinding="GrayLevel"
YValueBinding="Frequency"
Fill="#7F2196F3"
Stroke="#FF2196F3"
StrokeThickness="1"/>
</telerik:RadCartesianChart.Series>
</telerik:RadCartesianChart>
@@ -44,7 +63,7 @@
Text="{loc:Localization Histogram_NoData}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="14"
FontSize="12"
Foreground="#9E9E9E"
Visibility="Visible"/>
</Grid>