-
Notifications
You must be signed in to change notification settings - Fork 981
Closed
Labels
Description
Issue:
If the axis is marked as inverted and its TickGenerator property is set to DateTimeAutomatic, no ticks will be generated.
I found that DateTimeAutomatic::Regenerate(...) can't deal good with inverted axis. If the axis is inverted, parameter range is inverted and range.Span is negative. Negative range.Span leads to the problem.
I figure out a quick fix here:
// DateTimeAutomatic.cs
public class DateTimeAutomatic : IDateTimeTickGenerator
{
// ...
public void Regenerate(CoordinateRange range, Edge edge, PixelLength size, SKPaint paint, LabelStyle labelStyle)
{
var rangeSpan = Math.Abs(range.Span); //patch: solving the problem about negative value dule to inverted axis
if (rangeSpan >= TimeSpan.MaxValue.Days || double.IsNaN(rangeSpan) || double.IsInfinity(rangeSpan))
{
// cases of extreme zoom (10,000 years)
Ticks = [];
return;
}
TimeSpan span = TimeSpan.FromDays(rangeSpan);
ITimeUnit? timeUnit = GetAppropriateTimeUnit(span);
// estimate the size of the largest tick label for this unit this unit
int maxExpectedTickLabelWidth = (int)Math.Max(16, span.TotalDays / MaxTickCount);
int tickLabelHeight = 12;
PixelSize tickLabelBounds = new(maxExpectedTickLabelWidth, tickLabelHeight);
double coordinatesPerPixel = rangeSpan / size.Length;
while (true)
{
// determine the ideal spacing to use between ticks
double increment = coordinatesPerPixel * tickLabelBounds.Width / timeUnit.MinSize.TotalDays;
int? niceIncrement = LeastMemberGreaterThan(increment, timeUnit.Divisors);
if (niceIncrement is null)
{
timeUnit = TheseTimeUnits.FirstOrDefault(t => t.MinSize > timeUnit.MinSize);
if (timeUnit is not null)
continue;
timeUnit = TheseTimeUnits.Last();
niceIncrement = (int)Math.Ceiling(increment);
}
TimeUnit = timeUnit;
// attempt to generate the ticks given these conditions
(List<Tick>? ticks, PixelSize? largestTickLabelSize) = GenerateTicks(range, timeUnit, niceIncrement.Value, tickLabelBounds, paint, labelStyle);
// if ticks were returned, use them
if (ticks is not null)
{
Ticks = [.. ticks];
return;
}
// if no ticks were returned it means the conditions were too dense and tick labels
// overlapped, so expand the tick label bounds and try again.
if (largestTickLabelSize is not null)
{
tickLabelBounds = tickLabelBounds.Max(largestTickLabelSize.Value);
tickLabelBounds = new PixelSize(tickLabelBounds.Width + 10, tickLabelBounds.Height + 10);
continue;
}
throw new InvalidOperationException($"{nameof(ticks)} and {nameof(largestTickLabelSize)} are both null");
}
}
// ...
}ScottPlot Version:
5.0.54
Code Sample:
MainWindow.axaml.cs
using System;
using System.Linq;
using System.Net.Sockets;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using ScottPlot;
using ScottPlot.Avalonia;
using ScottPlot.Colormaps;
using ScottPlot.Plottables;
using ScottPlot.TickGenerators;
using Range = ScottPlot.Range;
namespace scottHeatmapTest.Views;
public partial class MainWindow : Window
{
private AvaPlot _avaPlot; // ScottPlot 控件
private Heatmap _heatmap; // 热力图对象
private double[,] _data; // 热力图数据
private const int DataWidth = 100; // 距离轴数据点数量
private const int DataHeight = 100; // 时间轴数据点数量
private int _currentIndex = DataHeight - 1; // 当前数据行索引(从最下方开始)
private string[] _timeLabels = new string[DataHeight]; // 存储每一行的时间标签
private readonly Crosshair _crosshair;
// private Task _task;
public MainWindow()
{
InitializeComponent();
// 获取 AvaPlot 控件
_avaPlot = this.FindControl<AvaPlot>("AvaPlot");
// 初始化数据
_data = new double[DataHeight, DataWidth];
// 初始化热力图
_heatmap = _avaPlot.Plot.Add.Heatmap(_data); // ScottPlot 5.x 的 API 变化
_heatmap.ManualRange = new Range(0, 255);
_heatmap.Colormap = new ScottPlot.Colormaps.Turbo(); // 设置颜色映射
_heatmap.CellHeight = DateTime.MinValue.AddSeconds(5).ToOADate() - DateTime.MinValue.ToOADate();
_avaPlot.Plot.Axes.SetLimitsY(bottom: 1.5, top: -1.5);
_avaPlot.Plot.Axes.AutoScaler.InvertedY = true;
_avaPlot.Plot.Axes.Left.TickGenerator = new MyDateTimeAutomatic();
// 设置横轴和纵轴标签
_avaPlot.Plot.Axes.Title.Label.Text = "Heatmap Example";
_avaPlot.Plot.Axes.Bottom.Label.Text = "Distance (m)";
_avaPlot.Plot.Axes.Left.Label.Text = "Time";
// 十字线
_crosshair = _avaPlot.Plot.Add.Crosshair(0, 0);
_crosshair.TextColor = Colors.White;
_crosshair.TextBackgroundColor = _crosshair.HorizontalLine.Color;
_avaPlot.PointerMoved += (s, e) =>
{
var pt = e.GetCurrentPoint(null).Position;
// Pixel mousePixel = new(e.X, e.Y);
// Coordinates mouseCoordinates = formsPlot1.Plot.GetCoordinates(mousePixel);
var mouseCoordinates = _avaPlot.Plot.GetCoordinates((float)pt.X, (float)pt.Y);
_crosshair.Position = _avaPlot.Plot.GetCoordinates((float)pt.X, (float)pt.Y);
_crosshair.VerticalLine.Text = $"{mouseCoordinates.X:N3}";
_crosshair.HorizontalLine.Text = $"{DateTime.FromOADate(mouseCoordinates.Y)}";
_avaPlot.Refresh();
};
// 添加颜色条
var colorbar = _avaPlot.Plot.Add.ColorBar(_heatmap);
colorbar.Axis.Label.Text = "Intensity";
// 启动 TCP 数据接收任务
if (!Design.IsDesignMode)
_ = ReceiveDataFromTcp();
}
private async Task ReceiveDataFromTcp()
{
using var client = new TcpClient("127.0.0.1", 5000); // 替换为你的 TCP 服务器地址和端口
using var stream = client.GetStream();
using var reader = new System.IO.StreamReader(stream, System.Text.Encoding.UTF8);
while (true)
{
var line = await reader.ReadLineAsync(); // 读取一行数据
if (line != null)
{
var values = line.Split(',').Select(double.Parse).ToArray(); // 假设数据是以逗号分隔的浮点数
UpdateHeatmap(values);
}
}
}
private void UpdateHeatmap(double[] newData)
{
// 旧数据上移(FIFO 操作)
for (int i = 0; i < DataHeight - 1; i++)
{
for (int j = 0; j < DataWidth; j++)
{
_data[i, j] = _data[i + 1, j]; // 将下一行的数据复制到当前行
}
_timeLabels[i] = _timeLabels[i + 1]; // 时间标签也上移
}
// 将新数据添加到最下方
for (int j = 0; j < DataWidth; j++)
{
_data[DataHeight - 1, j] = newData[j];
}
var now = DateTime.Now;
_heatmap.Position = new CoordinateRect(
_heatmap.Position.Value.XRange,
new CoordinateRange(now.ToOADate()- DataHeight * _heatmap.CellHeight, now.ToOADate() ));
_avaPlot.Plot.Axes.SetLimitsY(bottom: now.ToOADate() , top: now.ToOADate() - DataHeight * _heatmap.CellHeight);
Console.WriteLine($"_avaPlot.Plot.Axes.AutoScaler.InvertedY = {_avaPlot.Plot.Axes.AutoScaler.InvertedY}");
Console.WriteLine($"_avaPlot.Plot.Axes.Left.IsInverted = {_avaPlot.Plot.Axes.Left.IsInverted()}");
Console.WriteLine($"_heatmap.Position.Value.YRange.IsInverted = {_heatmap.Position.Value.YRange.IsInverted}");
// _avaPlot.Plot.Axes.AutoScale(null,true);
_avaPlot.Plot.Axes.AutoScaleY();
Console.WriteLine($"{now}");
if (_avaPlot.IsPointerOver)
{
// var topLevel = GetTopLevel(this);
//
// var pt = e.GetCurrentPoint(null).Position;
// // Pixel mousePixel = new(e.X, e.Y);
// // Coordinates mouseCoordinates = formsPlot1.Plot.GetCoordinates(mousePixel);
// var mouseCoordinates = _avaPlot.Plot.GetCoordinates((float)pt.X, (float)pt.Y);
// _crosshair.Position = _avaPlot.Plot.GetCoordinates((float)pt.X, (float)pt.Y);
// _crosshair.VerticalLine.Text = $"{mouseCoordinates.X:N3}";
// _crosshair.HorizontalLine.Text = $"{DateTime.FromOADate(mouseCoordinates.Y)}";
}
// 刷新热力图
_heatmap.Update();
_avaPlot.Refresh();
}
}server.py
import socket
import time
import random
import signal
import sys
# 定义服务器地址和端口
HOST = '127.0.0.1'
PORT = 5000
# 定义浮点数的最大值
MAX_FLOAT = 255.0
# 处理Ctrl+C信号
def signal_handler(sig, frame):
print('Server is shutting down...')
sys.exit(0)
# 生成浮点数列表
def generate_floats(start, end):
return [random.uniform(start, end) for _ in range(100)]
# 主函数
def main():
# 创建TCP socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((HOST, PORT))
server_socket.listen(1)
print(f'Server is listening on {HOST}:{PORT}...')
# 注册信号处理函数
signal.signal(signal.SIGINT, signal_handler)
try:
while True:
# 等待客户端连接
client_socket, client_address = server_socket.accept()
print(f'Connected by {client_address}')
start_range = 0
end_range = 99
try:
while True:
# 生成浮点数列表
floats = generate_floats(start_range, end_range)
# 将浮点数列表转换为字符串
data = ','.join(map(str, floats)) + '\n'
# 发送数据给客户端
client_socket.sendall(data.encode('utf-8'))
print(f'Sent: {data.strip()}')
# 更新浮点数范围
start_range += 1
end_range += 1
if end_range > MAX_FLOAT:
start_range = 0
end_range = 99
# 等待5秒
time.sleep(5)
except ConnectionResetError:
print('Client disconnected.')
finally:
client_socket.close()
print('Connection closed.')
except KeyboardInterrupt:
print('Server is shutting down...')
finally:
server_socket.close()
if __name__ == '__main__':
main()Reactions are currently unavailable