classdef InteractiveDataPlotter < handle
properties
% 数据存储
dataMap
fig
axes
% UI 组件句柄
keyListbox
btnPlotRefresh
btnMultiPlot
btnSaveCSV
% 多图管理
multiPlotFigures = gobjects(0) % 初始化为空图形对象数组
% 选择模式
selectionMode = 'single' % 默认单选模式
% ===== 新增属性 =====
% 缩放控制
zoomMode = 0; % 0=自由, 1=水平, 2=垂直
zoomButtons % 缩放按钮句柄
% 参考线管理
refLines = matlab.graphics.primitive.Line.empty % 参考线对象数组
% 多点标记
dataPoints = struct('x', {}, 'y', {}, 'handle', {}, 'index', {}, 'dataKey', {}) % 修正字段
snapRadius = 0.01 % 吸附半径(相对坐标)
% 鼠标交互状态
currentDraggingLine % 当前拖拽的参考线
pointSelectionMode = false % 点标记模式标志
end
methods
function obj = InteractiveDataPlotter(dataMap)
% === 数据验证 ===
if ~isa(dataMap, 'containers.Map')
error('输入必须是 containers.Map 对象');
end
if isempty(dataMap)
error('输入Map不能为空');
end
% 检查所有值是否为数值向量
allKeys = keys(dataMap);
for i = 1:length(allKeys)
value = dataMap(allKeys{i});
if ~isnumeric(value) || ~isvector(value)
error('键 "%s" 的值必须是数值向量', allKeys{i});
end
end
obj.dataMap = dataMap;
obj.createMainUI();
obj.updateAddButtonState(); % 修复4: 初始化按钮状态
end
% 创建UI主界面
function createMainUI(obj)
obj.fig = uifigure('Name', '交互式数据绘图器', ...
'Position', [100, 100, 1200, 700], ...
'AutoResizeChildren', 'off');
% === 网格布局重构 ===
mainGrid = uigridlayout(obj.fig, [3, 3]); % 改为3行
mainGrid.RowHeight = {'1x', 100, 60};
mainGrid.ColumnWidth = {220, '1x', 220};
mainGrid.RowSpacing = 10;
mainGrid.ColumnSpacing = 15;
% === 左侧面板: 数据集选择 ===
dataPanel = uipanel(mainGrid, 'Title', '数据集选择');
dataPanel.Layout.Row = 1;
dataPanel.Layout.Column = 1;
dataGrid = uigridlayout(dataPanel, [4, 1]);
dataGrid.RowHeight = {25, '1x', 35, 35};
dataGrid.RowSpacing = 8;
uilabel(dataGrid, 'Text', '选择数据集:', ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left');
% ===== 修复2: 初始化模式同步 =====
if strcmp(obj.selectionMode, 'single')
multiSelect = 'off';
else
multiSelect = 'on';
end
obj.keyListbox = uilistbox(dataGrid, ...
'Items', keys(obj.dataMap), ...
'Multiselect', multiSelect, ... % 与selectionMode同步
'ValueChangedFcn', @(src,evt)obj.plotSelectedData());
modeGrid = uigridlayout(dataGrid, [1, 2]);
modeGrid.Padding = [0, 0, 0, 0];
modeGrid.ColumnWidth = {'1x', '1x'};
uibutton(modeGrid, 'Text', '单选模式', ...
'ButtonPushedFcn', @(src,evt)obj.setSelectionMode('single'));
uibutton(modeGrid, 'Text', '多选模式', ...
'ButtonPushedFcn', @(src,evt)obj.setSelectionMode('multi'));
% === 中央面板: 主绘图区 ===
plotPanel = uipanel(mainGrid, 'Title', '数据可视化');
plotPanel.Layout.Row = 1;
plotPanel.Layout.Column = 2;
plotGrid = uigridlayout(plotPanel, [1,1]);
obj.axes = uiaxes(plotGrid);
title(obj.axes, '数据可视化');
grid(obj.axes, 'on');
hold(obj.axes, 'on');
% === 右侧面板: 多图控制区 ===
multiPanel = uipanel(mainGrid, 'Title', '多图管理');
multiPanel.Layout.Row = 1;
multiPanel.Layout.Column = 3;
multiGrid = uigridlayout(multiPanel, [5, 1]);
multiGrid.RowHeight = {25, 40, 40, 40, '1x'};
multiGrid.RowSpacing = 10;
uilabel(multiGrid, 'Text', '操作:', ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left');
% === 缩放控制工具栏 ===
zoomPanel = uipanel(mainGrid, 'Title', '缩放控制');
zoomPanel.Layout.Row = 2; % 新位置
zoomPanel.Layout.Column = [1, 3]; % 跨列
zoomGrid = uigridlayout(zoomPanel, [1, 5]);
zoomGrid.ColumnWidth = repmat({'1x'}, 1, 5);
zoomGrid.Padding = [5, 10, 5, 10];
obj.zoomButtons = gobjects(3,1);
obj.zoomButtons(1) = uibutton(zoomGrid, ...
'Text', '自由缩放', ...
'ButtonPushedFcn', @(src,evt)obj.setZoomMode(0));
obj.zoomButtons(2) = uibutton(zoomGrid, ...
'Text', '水平缩放', ...
'ButtonPushedFcn', @(src,evt)obj.setZoomMode(1));
obj.zoomButtons(3) = uibutton(zoomGrid, ...
'Text', '垂直缩放', ...
'ButtonPushedFcn', @(src,evt)obj.setZoomMode(2));
uibutton(zoomGrid, ...
'Text', '重置缩放', ...
'ButtonPushedFcn', @(src,evt)resetview(obj.axes)); % 修复函数名
uibutton(zoomGrid, ...
'Text', '重置视图', ...
'ButtonPushedFcn', @(src,evt)view(obj.axes, 0, 90));
% === 参考线工具栏 ===
toolPanel = uipanel(mainGrid, 'Title', '数据工具');
toolPanel.Layout.Row = 2; % 新位置
toolPanel.Layout.Column = 1;
toolGrid = uigridlayout(toolPanel, [1, 6]);
toolGrid.ColumnWidth = repmat({'1x'}, 1, 6);
toolGrid.Padding = [5, 10, 5, 10];
uibutton(toolGrid, ...
'Text', '添加水平线', ...
'ButtonPushedFcn', @(src,evt)obj.addReferenceLine('horizontal'));
uibutton(toolGrid, ...
'Text', '添加垂直线', ...
'ButtonPushedFcn', @(src,evt)obj.addReferenceLine('vertical'));
uibutton(toolGrid, ...
'Text', '标记数据点', ...
'ButtonPushedFcn', @(src,evt)obj.enablePointSelection());
uibutton(toolGrid, ...
'Text', '清除参考线', ...
'ButtonPushedFcn', @(src,evt)obj.clearReferenceLines());
uibutton(toolGrid, ...
'Text', '清除标记点', ...
'ButtonPushedFcn', @(src,evt)obj.clearDataPoints());
uibutton(toolGrid, ...
'Text', '吸附设置', ...
'ButtonPushedFcn', @(src,evt)obj.configureSnapRadius());
% === 启用坐标交互 (R2024a兼容) ===
try
disableDefaultInteractivity(obj.axes);
obj.axes.Interactions = [zoomInteraction, panInteraction];
axtoolbar(obj.axes, {'zoomin', 'zoomout', 'pan', 'restoreview'});
catch
% 旧版本兼容
zoom(obj.axes, 'on');
pan(obj.axes, 'on');
end
% === 鼠标回调设置 ===
obj.fig.WindowButtonDownFcn = @(src,evt)obj.onMouseDown(src,evt);
obj.fig.WindowButtonMotionFcn = @(src,evt)obj.onMouseMove(src,evt);
obj.fig.WindowButtonUpFcn = @(src,evt)obj.onMouseUp(src,evt);
% === 修复1: 使用有效图标或移除图标 ===
try
% 方案1: 使用MATLAB内置图标名称
obj.btnMultiPlot = uibutton(multiGrid, ...
'Text', '创建多图窗口', ...
'Icon', 'add', ... % MATLAB内置图标名称
'ButtonPushedFcn', @(src,evt)obj.createMultiPlot());
catch
% 方案2: 创建简单的加号图标
iconData = zeros(16, 16, 3); % 创建16x16空白图像
iconData(8, 3:14, :) = 1; % 水平线 (白色)
iconData(3:14, 8, :) = 1; % 垂直线 (白色)
obj.btnMultiPlot = uibutton(multiGrid, ...
'Text', '创建多图窗口', ...
'Icon', iconData, ... % 直接使用图像数据
'ButtonPushedFcn', @(src,evt)obj.createMultiPlot());
end
uibutton(multiGrid, 'Text', '添加选中项', ...
'ButtonPushedFcn', @(src,evt)obj.addToActiveMultiPlot(), ...
'Enable', 'off', ...
'Tag', 'addToMultiBtn');
obj.btnSaveCSV = uibutton(multiGrid, ...
'Text', '保存为CSV', ...
'ButtonPushedFcn', @(src,evt)obj.saveToCSV());
% === 底部按钮区 ===
btnPanel = uipanel(mainGrid, 'BorderType', 'none');
btnPanel.Layout.Row = 3;
btnPanel.Layout.Column = [1, 3];
btnGrid = uigridlayout(btnPanel, [1, 4]);
btnGrid.ColumnWidth = {'1x', '1x', '1x', '1x'};
btnGrid.Padding = [5, 10, 5, 10];
uibutton(btnGrid, 'Text', '清除图表', ...
'ButtonPushedFcn', @(src,evt)cla(obj.axes));
obj.btnPlotRefresh = uibutton(btnGrid, 'Text', '刷新数据', ...
'ButtonPushedFcn', @(src,evt)obj.plotAllData(), ...
'BackgroundColor', [0.3, 0.6, 1], ...
'FontColor', 'white');
uibutton(btnGrid, 'Text', '导出图表', ...
'ButtonPushedFcn', @(src,evt)obj.exportPlot(), ...
'Tag', 'exportPlotBtn');
uibutton(btnGrid, 'Text', '帮助', ...
'ButtonPushedFcn', @(src,evt)obj.showHelp(), ...
'Tag', 'helpBtn');
% 延迟初始绘图
drawnow; % 使用drawnow替代pause
obj.plotAllData();
end
function setSelectionMode(obj, mode)
obj.selectionMode = mode;
if strcmp(mode, 'single')
obj.keyListbox.Multiselect = 'off';
% 单选时自动选择第一项
if ~isempty(obj.keyListbox.Items)
obj.keyListbox.Value = obj.keyListbox.Items{1};
end
else
obj.keyListbox.Multiselect = 'on';
end
obj.plotSelectedData();
end
function plotAllData(obj)
% === 安全检测 ===
if ~isvalid(obj.axes), return; end
cla(obj.axes);
keys = obj.dataMap.keys();
if isempty(keys)
title(obj.axes, '无可用数据');
return;
end
for i = 1:length(keys)
key = keys{i};
if isKey(obj.dataMap, key)
data = obj.dataMap(key);
if isempty(data)
warning('键 "%s" 的数据为空,跳过绘图', key);
continue;
end
if isrow(data), data = data'; end
plot(obj.axes, data, 'DisplayName', key);
end
end
if ~isempty(allchild(obj.axes))
legend(obj.axes, 'show', 'Interpreter', 'none');
else
title(obj.axes, '无有效数据可显示');
end
end
function plotSelectedData(obj)
if ~isvalid(obj.axes), return; end
cla(obj.axes);
selectedKeys = obj.keyListbox.Value;
if isempty(selectedKeys), return; end
% 统一转换为元胞数组
if ischar(selectedKeys)
selectedKeys = {selectedKeys};
elseif isstring(selectedKeys)
selectedKeys = cellstr(selectedKeys);
end
for i = 1:length(selectedKeys)
key = selectedKeys{i};
if isKey(obj.dataMap, key)
data = obj.dataMap(key);
if isrow(data), data = data'; end
plot(obj.axes, data, 'DisplayName', key);
end
end
if ~isempty(selectedKeys)
legend(obj.axes, 'show', 'Interpreter', 'none');
end
end
function saveToCSV(obj)
[filename, pathname] = uiputfile('*.csv', '保存CSV文件');
if isequal(filename, 0) || isequal(pathname, 0), return; end
fullPath = fullfile(pathname, filename);
keys = obj.dataMap.keys();
maxLength = max(cellfun(@(k)numel(obj.dataMap(k)), keys));
T = table();
for i = 1:length(keys)
key = keys{i};
% ===== 修复3: 确保列向量处理 =====
data = obj.dataMap(key);
if isrow(data), data = data'; end
data = data(:); % 强制转换为列向量
if length(data) < maxLength
data = [data; nan(maxLength-length(data), 1)];
end
T.(key) = data(1:maxLength);
end
writetable(T, fullPath);
uialert(obj.fig, sprintf('数据已保存到:\n%s', fullPath), '保存成功', 'Icon', 'success');
end
function createMultiPlot(obj)
multiFig = uifigure('Name', '多图视图', ...
'Position', [300, 300, 1000, 600], ...
'CloseRequestFcn', @(src,evt)obj.onMultiPlotClose(src));
obj.multiPlotFigures(end+1) = multiFig;
obj.updateAddButtonState();
multiGrid = uigridlayout(multiFig, [3, 1]);
multiGrid.RowHeight = {50, '1x', '1x'};
multiGrid.RowSpacing = 10;
% 控制区
ctrlPanel = uipanel(multiGrid, 'BorderType', 'none');
ctrlGrid = uigridlayout(ctrlPanel, [1, 6]);
ctrlGrid.ColumnWidth = {120, '1x', 120, 120};
% === 修复2: 添加按钮的稳健图标处理 ===
try
% 方案1: 使用内置图标
addBtn = uibutton(ctrlGrid, 'Text', '添加数据集', ...
'Icon', 'add', ... % MATLAB内置图标
'ButtonPushedFcn', @(src,evt)obj.addToMultiPlot(multiFig));
catch
try
% 方案2: 使用自定义图像文件
iconPath = fullfile(matlabroot, 'toolbox', 'matlab', 'icons', 'addicon.gif');
if exist(iconPath, 'file')
addBtn = uibutton(ctrlGrid, 'Text', '添加数据集', ...
'Icon', iconPath, ...
'ButtonPushedFcn', @(src,evt)obj.addToMultiPlot(multiFig));
else
% 方案3: 创建替代文本按钮
addBtn = uibutton(ctrlGrid, 'Text', '+ 添加数据集', ...
'BackgroundColor', [0.1, 0.7, 0.2], ...
'FontColor', 'white', ...
'FontWeight', 'bold', ...
'ButtonPushedFcn', @(src,evt)obj.addToMultiPlot(multiFig));
end
catch
% 方案4: 纯文本按钮
addBtn = uibutton(ctrlGrid, 'Text', '添加数据集', ...
'ButtonPushedFcn', @(src,evt)obj.addToMultiPlot(multiFig));
end
end
% 数据选择下拉框
dataDropdown = uidropdown(ctrlGrid, 'Items', keys(obj.dataMap), ...
'Tag', 'multiPlotDropdown');
uibutton(ctrlGrid, 'Text', '清除所有', ...
'ButtonPushedFcn', @(src,evt)obj.clearMultiPlot(multiFig));
uibutton(ctrlGrid, 'Text', '保存视图', ...
'ButtonPushedFcn', @(src,evt)obj.saveMultiPlot(multiFig));
% 列表区
listPanel = uipanel(multiGrid, 'Title', '已选数据集');
listGrid = uigridlayout(listPanel, [1,1]);
listbox = uilistbox(listGrid, 'Items', {}, ...
'Multiselect', 'on');
% 绘图区
plotPanel = uipanel(multiGrid, 'Title', '多图叠加显示');
plotGrid = uigridlayout(plotPanel, [1,1]);
ax = uiaxes(plotGrid);
hold(ax, 'on');
grid(ax, 'on');
% 存储句柄
multiFig.UserData = struct(...
'axesHandle', ax, ...
'listboxHandle', listbox, ...
'dropdownHandle', dataDropdown);
end
function updateAddButtonState(obj)
addBtns = findobj(obj.fig, 'Tag', 'addToMultiBtn');
if ~isempty(obj.multiPlotFigures) && all(isvalid(obj.multiPlotFigures))
set(addBtns, 'Enable', 'on');
else
set(addBtns, 'Enable', 'off');
% ===== 修复6: 清理无效引用 =====
obj.multiPlotFigures(~isvalid(obj.multiPlotFigures)) = [];
end
end
function addToActiveMultiPlot(obj)
if isempty(obj.multiPlotFigures) || ~any(isvalid(obj.multiPlotFigures))
uialert(obj.fig, '请先创建多图窗口', '无活动窗口');
return;
end
activeFig = obj.multiPlotFigures(end);
if isvalid(activeFig)
obj.addToMultiPlot(activeFig);
end
end
function addToMultiPlot(obj, multiFig)
% ===== 修复5: 增加安全检测 =====
if ~isvalid(multiFig) || ~isfield(multiFig.UserData, 'listboxHandle')
return;
end
ud = multiFig.UserData;
selectedKeys = obj.keyListbox.Value;
if isempty(selectedKeys), return; end
if ischar(selectedKeys)
selectedKeys = {selectedKeys};
end
currentItems = ud.listboxHandle.Items;
if isempty(currentItems)
currentItems = {};
end
newItems = setdiff(selectedKeys, currentItems);
if ~isempty(newItems)
ud.listboxHandle.Items = [currentItems; newItems(:)];
obj.updateMultiPlot(multiFig);
end
end
function updateMultiPlot(obj, multiFig)
% ===== 修复5: 增加安全检测 =====
if ~isvalid(multiFig) || ~isfield(multiFig.UserData, 'axesHandle')
return;
end
ud = multiFig.UserData;
cla(ud.axesHandle);
selectedKeys = ud.listboxHandle.Items;
if isempty(selectedKeys), return; end
if ischar(selectedKeys)
selectedKeys = {selectedKeys};
end
for i = 1:length(selectedKeys)
key = selectedKeys{i};
if isKey(obj.dataMap, key)
data = obj.dataMap(key);
if isrow(data), data = data'; end
plot(ud.axesHandle, data, 'DisplayName', key);
end
end
legend(ud.axesHandle, 'show', 'Interpreter', 'none');
end
function clearMultiPlot(obj, multiFig)
if ~isvalid(multiFig), return; end
if isfield(multiFig.UserData, 'axesHandle') && isvalid(multiFig.UserData.axesHandle)
cla(multiFig.UserData.axesHandle);
legend(multiFig.UserData.axesHandle, 'off');
end
if isfield(multiFig.UserData, 'listboxHandle') && isvalid(multiFig.UserData.listboxHandle)
multiFig.UserData.listboxHandle.Items = {};
end
end
% ===== 新增缩放功能方法 =====
function setZoomMode(obj, mode)
obj.zoomMode = mode;
% 更新按钮状态
for i = 1:3
if mode == (i-1)
obj.zoomButtons(i).BackgroundColor = [0.7, 0.9, 1];
obj.zoomButtons(i).FontWeight = 'bold';
else
obj.zoomButtons(i).BackgroundColor = [0.96, 0.96, 0.96];
obj.zoomButtons(i).FontWeight = 'normal';
end
end
% R2024a兼容缩放设置
try
zoomHandle = zoom(obj.fig);
set(zoomHandle, 'Motion', 'both', 'Enable', 'on');
switch mode
case 0 % 自由缩放
set(zoomHandle, 'Motion', 'both');
case 1 % 水平缩放
set(zoomHandle, 'Motion', 'horizontal');
case 2 % 垂直缩放
set(zoomHandle, 'Motion', 'vertical');
end
catch
% 旧版本备用方案
zoom(obj.axes, 'on');
end
end
% ===== 新增参考线功能方法 =====
function addReferenceLine(obj, orientation)
% 获取当前轴范围
xlims = obj.axes.XLim;
ylims = obj.axes.YLim;
% 创建参考线(修复变量名冲突)
switch orientation
case 'horizontal'
y = mean(ylims); % 默认放在中间
refLine = line(obj.axes, xlims, [y, y], ... % 变量名改为 refLine
'Color', [0, 0.6, 0], 'LineWidth', 1.5, ...
'PickableParts', 'visible', ...
'Tag', 'HReferenceLine');
% 添加数据标签(兼容 R2024a)
try
dt = datatip(refLine, 'DataIndex', 1);
dt.Location = 'northwest';
catch
text(obj.axes, xlims(1), y, sprintf('Y=%.4f', y), ...
'Color', [0, 0.6, 0], 'FontWeight', 'bold');
end
case 'vertical'
x = mean(xlims);
refLine = line(obj.axes, [x, x], ylims, ... % 变量名改为 refLine
'Color', [0.8, 0, 0.2], 'LineWidth', 1.5, ...
'PickableParts', 'visible', ...
'Tag', 'VReferenceLine');
% 添加数据标签(兼容 R2024a)
try
dt = datatip(refLine, 'DataIndex', 1);
dt.Location = 'northeast';
catch
text(obj.axes, x, ylims(2), sprintf('X=%.4f', x), ...
'Color', [0.8, 0, 0.2], 'FontWeight', 'bold');
end
end
% 存储线条对象(使用新变量名)
obj.refLines(end+1) = refLine;
% 开启拖拽功能
set(refLine, 'ButtonDownFcn', @(src,evt)obj.startDrag(src));
end
function startDrag(obj, src)
obj.currentDraggingLine = src;
% 获取当前线段的坐标数据
obj.originalPosition = get(src, {'XData', 'YData'});
end
function clearReferenceLines(obj)
delete(obj.refLines);
obj.refLines = matlab.graphics.primitive.Line.empty;
end
% ===== 新增数据点标记方法 =====
function enablePointSelection(obj)
% 设置标记模式
obj.pointSelectionMode = true;
set(obj.fig, 'Pointer', 'crosshair');
uialert(obj.fig, '点击图中位置标记数据点', '标记模式', 'Icon', 'info');
end
function markDataPoint(obj, x, y)
% 查找最近的数据点
[closestX, closestY, dataKey, pointIndex] = obj.findNearestDataPoint(x, y);
% 创建标记点
marker = scatter(obj.axes, closestX, closestY, 100, ...
'Marker', 'd', 'MarkerEdgeColor', 'k', ...
'MarkerFaceColor', [1, 0.8, 0.2], ...
'LineWidth', 1.5, ...
'Tag', 'DataMarker');
% 添加数据标签
dt = datatip(marker, 'DataIndex', 1);
dt.FontSize = 10;
dt.FontWeight = 'bold';
dt.String = {sprintf('数据集: %s', dataKey), ...
sprintf('索引: %d', pointIndex), ...
sprintf('值: %.4f', closestY)};
% 存储标记点信息
obj.dataPoints(end+1) = struct(...
'x', closestX, ...
'y', closestY, ...
'handle', marker, ...
'index', pointIndex, ...
'dataKey', dataKey);
end
function clearDataPoints(obj)
for i = 1:length(obj.dataPoints)
if isvalid(obj.dataPoints(i).handle)
delete(obj.dataPoints(i).handle);
end
end
obj.dataPoints = struct('x', {}, 'y', {}, 'handle', {}, 'index', {});
end
function configureSnapRadius(obj)
answer = inputdlg('设置吸附半径 (0.01-0.1):', '吸附灵敏度', 1, {num2str(obj.snapRadius)});
if ~isempty(answer)
newRadius = str2double(answer{1});
if ~isnan(newRadius) && newRadius > 0 && newRadius <= 0.1
obj.snapRadius = newRadius;
end
end
end
% ===== 新增吸附查找方法 =====
function [closestX, closestY, dataKey, pointIndex] = findNearestDataPoint(obj, x, y)
% 获取当前轴范围
xRange = range(obj.axes.XLim);
yRange = range(obj.axes.YLim);
% 初始化最近点变量
minDistance = inf;
closestX = NaN;
closestY = NaN;
dataKey = '';
pointIndex = 0;
% 遍历所有可见数据
visibleLines = findobj(obj.axes, 'Type', 'line', 'Visible', 'on');
for i = 1:length(visibleLines)
lineData = visibleLines(i);
xData = lineData.XData;
yData = lineData.YData;
% 仅处理有显示名称的数据集
if isempty(lineData.DisplayName) || strcmp(lineData.DisplayName, '')
continue;
end
% 计算点到数据集的归一化距离
normDistances = sqrt(...
((xData - x)/xRange).^2 + ...
((yData - y)/yRange).^2);
% 查找最小距离点
[dist, idx] = min(normDistances);
% 检查是否在吸附半径内
if dist < obj.snapRadius && dist < minDistance
minDistance = dist;
closestX = xData(idx);
closestY = yData(idx);
dataKey = lineData.DisplayName;
pointIndex = idx;
end
end
% 如果没有找到吸附点,使用原始坐标
if isnan(closestX)
closestX = x;
closestY = y;
dataKey = '手动标记';
pointIndex = 0;
end
end
% ===== 新增鼠标事件处理方法 =====
function onMouseDown(obj, src, event)
% 获取当前点坐标
cp = obj.axes.CurrentPoint(1,1:2);
x = cp(1);
y = cp(2);
% 检查是否在标记模式
if isfield(obj, 'pointSelectionMode') && obj.pointSelectionMode
obj.markDataPoint(x, y);
obj.pointSelectionMode = false;
set(obj.fig, 'Pointer', 'arrow');
return;
end
% 检查是否点击了参考线
hitObject = hittest(obj.fig);
if any(hitObject == obj.refLines)
obj.currentDraggingLine = hitObject;
obj.originalPosition = get(hitObject, {'XData', 'YData'});
return;
end
end
function onMouseMove(obj, src, event) % 添加两个额外参数占位
% 处理参考线拖拽
if ~isempty(obj.currentDraggingLine) && isvalid(obj.currentDraggingLine)
cp = obj.axes.CurrentPoint(1,1:2);
x = cp(1);
y = cp(2);
tag = get(obj.currentDraggingLine, 'Tag');
if strcmp(tag, 'HReferenceLine')
% 更新水平线位置
set(obj.currentDraggingLine, 'YData', [y, y]);
% 更新数据标签
dt = findobj(obj.currentDraggingLine, 'Type', 'datatip');
if ~isempty(dt)
dt.Position = [mean(obj.axes.XLim), y];
dt.String = sprintf('Y = %.4f', y);
end
elseif strcmp(tag, 'VReferenceLine')
% 更新垂直线位置
set(obj.currentDraggingLine, 'XData', [x, x]);
% 更新数据标签
dt = findobj(obj.currentDraggingLine, 'Type', 'datatip');
if ~isempty(dt)
dt.Position = [x, mean(obj.axes.YLim)];
dt.String = sprintf('X = %.4f', x);
end
end
end
end
% ===== 新增onMouseUp方法 =====
function onMouseUp(obj, src, event)
% 释放当前拖拽的参考线
if ~isempty(obj.currentDraggingLine) && isvalid(obj.currentDraggingLine)
obj.currentDraggingLine = [];
% 更新参考线标签位置
obj.updateReferenceLineLabels();
end
% 清除点标记模式
obj.pointSelectionMode = false;
set(obj.fig, 'Pointer', 'arrow');
end
% ===== 辅助方法:更新参考线标签 =====
function updateReferenceLineLabels(obj)
for i = 1:length(obj.refLines)
tag = obj.refLines(i).Tag;
if strcmp(tag, 'HReferenceLine')
% 更新水平线标签
y = mean(obj.refLines(i).YData);
textObjs = findobj(obj.refLines(i), 'Type', 'text');
if ~isempty(textObjs)
textObjs.String = sprintf('Y=%.4f', y);
textObjs.Position(2) = y;
end
elseif strcmp(tag, 'VReferenceLine')
% 更新垂直线标签
x = mean(obj.refLines(i).XData);
textObjs = findobj(obj.refLines(i), 'Type', 'text');
if ~isempty(textObjs)
textObjs.String = sprintf('X=%.4f', x);
textObjs.Position(1) = x;
end
end
end
end
function onMultiPlotClose(obj, fig)
if isvalid(fig)
delete(fig);
end
% ===== 修复6: 完全移除引用 =====
validIdx = isvalid(obj.multiPlotFigures) & (obj.multiPlotFigures ~= fig);
obj.multiPlotFigures = obj.multiPlotFigures(validIdx);
obj.updateAddButtonState();
end
% 预留接口保持不变
function exportPlot(obj)
end
function showHelp(obj)
end
function saveMultiPlot(obj, fig)
end
end
end
出现了参考线,但是无法拖动,而且无法自由的选择参考线出现的位置
解决代码中存在的问题,写出更新后的完整代码!!!
最新发布