幾個圖表控制元件關於熱力圖顯示的調研筆記

wzwyc發表於2024-12-06

InteractiveDataDisplay.WPF

這是微軟出的一個開源的曲線圖控制元件,目前已經沒有更新了,而且只支援.NET Framework,不支援.NET Core平臺。

安裝

Install-Package InteractiveDataDisplay.WPF

前臺程式碼

<Window
    x:Class="HeatmapGraphDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:d3="clr-namespace:InteractiveDataDisplay.WPF;assembly=InteractiveDataDisplay.WPF"
    xmlns:local="clr-namespace:HeatmapGraphDemo"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <d3:Chart>
            <d3:Chart.Title>
                <TextBlock Margin="0,5,0,5" FontSize="18">Heatmap sample</TextBlock>
            </d3:Chart.Title>
            <!--<d3:HeatmapGraph x:Name="heatmap" Palette="Yellow,Blue,Orange" />-->
            <d3:HeatmapGraph x:Name="heatmap" />
        </d3:Chart>

        <Button
            Grid.Row="1"
            Click="Button_Click"
            Content="載入" />
    </Grid>
</Window>

後臺程式碼

using MiniExcelLibs;
using System.Diagnostics;
using System.Linq;
using System.Windows;

namespace HeatmapGraphDemo
{
    /// <summary>
    /// MainWindow.xaml 的互動邏輯
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var items = MiniExcel.Query<DataInfo>("test2.xlsx");
            items = items.OrderBy(s => s.Longitude).ThenBy(s => s.Latitude).ToList();
            var x = items.Select(s => s.Longitude).Distinct().ToArray();
            var y = items.Select(s => s.Latitude).Distinct().ToArray();
            var values = items.Select(s => s.AccessCycle).ToArray();
            Debug.Assert(x.Length * y.Length == values.Length);

            var f = new double[x.Length, y.Length];
            for (int i = 0; i < x.Length; i++)
            {
                for (int j = 0; j < y.Length; j++)
                {
                    f[i, j] = values[i * y.Length + j];
                }
            }

            heatmap.Plot(f, x, y);
        }
    }

    public class DataInfo
    {
        public double Longitude { get; set; }
        public double Latitude { get; set; }
        public double AccessCycle { get; set; }
    }
}

最終效果

image

試用總結

作為一個古老的控制元件,但是單單熱力圖這個功能,實現得相當靠譜。
生成熱力圖的時候,會同時要求提供X,Y,Z三個引數組。而且X,Y不要求均勻間隔,如果之間的間隔不均勻的時候,會自動進行插值。
生成的熱力圖最終圖片有點像一張平滑處理過的圖片。

LiveCharts2

也是一個開源的圖表控制元件。LiveCharts2目前還在RC版本,沒有正式釋出。支援的平臺很多,支援.NET FRAMEWORK和.NET CORE,除了支援WPF,也支援Avalonia這些平臺,能夠用來做跨平臺的專案。

安裝

因為還不是正式版,需要跟上具體的版本號安裝。

Install-Package LiveChartsCore.SkiaSharpView.WPF -Version 2.0.0-rc4.5

前臺介面

<Window
    x:Class="LiveChartDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:LiveChartDemo"
    xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">

    <Grid>
        <!--<lvc:CartesianChart
            Series="{Binding Series}"
            XAxes="{Binding XAxes}"
            YAxes="{Binding YAxes}" />-->
        <lvc:CartesianChart Series="{Binding Series}" />
    </Grid>
</Window>

後臺程式碼

using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.SkiaSharpView;
using MiniExcelLibs;
using SkiaSharp;

namespace LiveChartDemo
{
    public class MainWindowViewModel2
    {
        public ISeries[] Series { get; set; } = [
            new HeatSeries<WeightedPoint>
        {
            HeatMap = [
                SKColors.Blue.AsLvcColor(), // the first element is the "coldest"
                SKColors.Red.AsLvcColor() // the last element is the "hottest"
            ]
        }
        ];

        //public ICartesianAxis[] XAxes { get; set; } = [
        //    new Axis
        //{
        //    Labels = ["Charles", "Richard", "Ana", "Mari"]
        //}
        //];

        //public ICartesianAxis[] YAxes { get; set; } = [
        //    new Axis
        //{
        //    Labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
        //}
        //];

        public MainWindowViewModel2()
        {
            var items = MiniExcel.Query<DataInfo>("test2.xlsx").ToList();

            var ser = Series.First();
            var list = new List<WeightedPoint>();
            foreach (var item in items)
            {
                list.Add(new WeightedPoint(item.Longitude, item.Latitude, item.AccessCycle));
            }
            ser.Values = list;
        }
    }

    public class DataInfo
    {
        public double Longitude { get; set; }
        public double Latitude { get; set; }
        public double AccessCycle { get; set; }
    }
}

試用總結

間隔均勻的熱力圖也沒有啥問題。遇到間隔不均勻的資料,會出現中間的空白,不會像上一個控制元件一樣,自動去補齊。
image

OxyPlot

也是開源的圖表控制元件,目前也差不多是我們團隊裡面使用最多的圖表控制元件。跟LiveChart2一樣,支援.NET FRAMEWORK和.NET CORE,支援WPF和Avalonia這些平臺,可以用來做跨平臺開發專案。效能也不錯。

安裝

Install-Package OxyPlot.SkiaSharp.Wpf

前臺程式碼

<Window
    x:Class="OxyPlotDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:OxyPlotDemo"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:oxy="http://oxyplot.org/skiawpf"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <oxy:PlotView x:Name="plot" />
    </Grid>
</Window>

後臺程式碼

using MiniExcelLibs;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;
using System.Diagnostics;
using System.Windows;

namespace OxyPlotDemo
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            plot.Model = GetPlotModel();
        }

        private PlotModel GetPlotModel()
        {
            var items = MiniExcel.Query<DataInfo>("test2.xlsx").ToList();
            items = items.OrderBy(s => s.Longitude).ThenBy(s => s.Latitude).ToList();
            var xvalues = items.Select(s => s.Longitude).Distinct().ToArray();
            var yvalues = items.Select(s => s.Latitude).Distinct().ToArray();
            var zvalues = items.Select(s => s.AccessCycle).ToArray();
            var min = zvalues.Min();
            var max = zvalues.Max();
            var len = max - min;
            Debug.Assert(xvalues.Length * yvalues.Length == zvalues.Length);

            var model = new PlotModel { Title = "Heatmap" };

            // Color axis (the X and Y axes are generated automatically)
            model.Axes.Add(new LinearColorAxis
            {
                Palette = OxyPalettes.Rainbow(100)
            });

            //// generate 2d normal distribution
            var data = new double[xvalues.Length, yvalues.Length];
            for (int x = 0; x < xvalues.Length; ++x)
            {
                for (int y = 0; y < yvalues.Length; ++y)
                {
                    data[y, x] = zvalues[y * xvalues.Length + x];
                }
            }

            var heatMapSeries = new HeatMapSeries
            {
                X0 = xvalues.Min(),
                X1 = xvalues.Max(),
                Y0 = yvalues.Min(),
                Y1 = yvalues.Max(),
                Interpolate = true,
                RenderMethod = HeatMapRenderMethod.Bitmap,
                Data = data
            };

            model.Series.Add(heatMapSeries);

            return model;
        }
    }

    public class DataInfo
    {
        public double Longitude { get; set; }
        public double Latitude { get; set; }
        public double AccessCycle { get; set; }
    }
}

試用總結

常規的熱力圖顯示也沒有問題。但是OxyPlot預設每個點之間的X和Y的間隔是均勻的。它的X軸和Y軸的區間是透過最大值和最小值去控制的,然後傳進去的點,它會預設當作最大值和最小值之間均分的,所以如果點是不均勻的,最終顯示的圖會有期望的圖有差異。

下面這張圖是X和Y值間隔均勻的影像。應該是符合預期的。
image

下面是特意把最後一個點的Y軸從40改成100的情況,看起來跟40的貌似沒啥區別,但這是不符合預期的。其實是應該像InteractiveDataDisplay.WPF顯示的那樣才對。
image

ScottPlot

這個控制元件貌似也有不少人使用,也聽到不少人推薦。但我們團隊用得不多,主要因為不熟,官方的示例和文件感覺有點偏簡單,一些我們平常經常用的功能,或者開發過程中遇到的一些問題不好解決,然後本身可用的控制元件較多,所以這個控制元件就用得比較少。

安裝

Install-Package ScottPlot.WPF

前臺程式碼

<Window
    x:Class="ScottPlotDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:ScottPlotDemo"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:wpf="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <wpf:WpfPlot Name="plot" />

        <Button
            Grid.Row="1"
            Click="Button_Click"
            Content="載入" />
    </Grid>
</Window>

後臺程式碼

using MiniExcelLibs;
using ScottPlot;
using System.Diagnostics;
using System.Windows;

namespace ScottPlotDemo
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var items = MiniExcel.Query<DataInfo>("test.xlsx").ToList();
            items = items.OrderBy(s => s.Longitude).ThenBy(s => s.Latitude).ToList();
            var x = items.Select(s => s.Longitude).Distinct().ToArray();
            var y = items.Select(s => s.Latitude).Distinct().ToArray();
            var values = items.Select(s => s.AccessCycle).ToArray();
            var min = values.Min();
            var max = values.Max();
            var len = max - min;
            Debug.Assert(x.Length * y.Length == values.Length);
            var f = new Coordinates3d[x.Length, y.Length];
            for (int i = 0; i < x.Length; i++)
            {
                for (int j = 0; j < y.Length; j++)
                {
                    var info = f[i, j];
                    var data = items[i * y.Length + j];
                    info.X = data.Longitude;
                    info.Y = data.Latitude;
                    info.Z = data.AccessCycle;
                }
            }

            //double[,] f2 = SampleData.MonaLisa();
            var map = plot.Plot.Add.Heatmap(f);
            map.Colormap = new ScottPlot.Colormaps.Turbo();
            plot.Plot.Add.ColorBar(map);

            plot.Plot.Axes.AutoScale();
            plot.Refresh();
        }
    }

    public class DataInfo
    {
        public double Longitude { get; set; }
        public double Latitude { get; set; }
        public double AccessCycle { get; set; }
    }
}

試用總結

熱力圖是畫出來了,但是顯示的圖片完全一個顏色的,沒有深淺的對比,嘗試了不少方法,貌似都沒折騰出來。
提供了蒙娜麗莎的測試資料,貌似可以顯示出來,但是自己的資料顯示就一個色的。

最後的總結

熱力圖,大部分的圖表控制元件都是預設X和Y值間隔均勻的情況,並且大部分的圖表控制元件,傳點的時候,都是隻傳Z值的數值進去的。其實這也是熱力圖比較常規的使用方式。
不均勻的是不是顯示成散點圖會更合理一點?

相關文章