function H = geom_helpers() %GEOM_HELPERS 几何示意图共用的绘制函数。 % 只使用 MATLAB 默认图形对象与默认配色,不做自定义样式。 H.circle = @draw_circle; H.wedge = @draw_wedge; H.disk = @draw_disk; H.poly = @draw_poly; H.marker = @draw_marker; H.arrow = @draw_arrow; H.label_points = @label_points; H.text_width = @text_width; end % --------------------------------------------------------------------------- % 基本图元 % --------------------------------------------------------------------------- function h = draw_circle(ax, center, radius, color, lw, style) t = linspace(0, 2 * pi, 360); h = plot(ax, center(1) + radius * cos(t), center(2) + radius * sin(t), ... 'Color', color, 'LineWidth', lw, 'LineStyle', style); end function h = draw_wedge(ax, poly, color, alpha) h = patch(ax, 'XData', poly(:, 1), 'YData', poly(:, 2), 'FaceColor', color, ... 'FaceAlpha', alpha, 'EdgeColor', 'none'); end function h = draw_disk(ax, center, radius, color, alpha, edge_style) t = linspace(0, 2 * pi, 360); h = patch(ax, 'XData', center(1) + radius * cos(t), ... 'YData', center(2) + radius * sin(t), ... 'FaceColor', color, 'FaceAlpha', alpha, 'EdgeColor', color, ... 'LineWidth', 0.6, 'LineStyle', edge_style); end function h = draw_poly(ax, poly, face, alpha, edge, lw) h = patch(ax, 'XData', poly(:, 1), 'YData', poly(:, 2), 'FaceColor', face, ... 'FaceAlpha', alpha, 'EdgeColor', edge, 'LineWidth', lw); end function h = draw_marker(ax, p, color, size, filled) if filled h = plot(ax, p(1), p(2), 'o', 'MarkerSize', size, ... 'MarkerFaceColor', color, 'MarkerEdgeColor', 'none'); else h = plot(ax, p(1), p(2), 'o', 'MarkerSize', size, ... 'MarkerFaceColor', 'none', 'MarkerEdgeColor', color, ... 'LineWidth', 0.9); end end function h = draw_arrow(ax, p0, p1, color) %DRAW_ARROW 数据坐标箭头:直线 + 实心三角箭头。 % 箭头在"像素空间"里构造后换算回数据坐标,因此与坐标轴比例无关, % 也不依赖绘制时的 axes 位置(图例把坐标区挤小后箭头依然对准数据点)。 h = plot(ax, [p0(1) p1(1)], [p0(2) p1(2)], 'Color', color, 'LineWidth', 0.9); d = [p1(1) - p0(1), p1(2) - p0(2)]; [sx, sy] = px_scale(ax); % 每像素对应的数据单位 v = [d(1) / sx, d(2) / sy]; % 像素空间向量 len = hypot(v(1), v(2)); if len < 1e-9 return; end u = v / len; n = [-u(2), u(1)]; head = min(len * 0.6, 10); % 头长(像素) base = v - u * head; tri = [v; base + n * head * 0.42; base - n * head * 0.42] .* [sx, sy]; patch(ax, 'XData', p0(1) + tri(:, 1), 'YData', p0(2) + tri(:, 2), ... 'FaceColor', color, 'EdgeColor', 'none'); end function [sx, sy] = px_scale(ax) pos = ax.Position; fpos = ancestor(ax, 'figure').Position; sx = diff(ax.XLim) / (pos(3) * fpos(3)); sy = diff(ax.YLim) / (pos(4) * fpos(4)); end % --------------------------------------------------------------------------- % 标签自动避让 % --------------------------------------------------------------------------- function label_points(ax, pts, labels, color) %LABEL_POINTS 在散点旁放置标签:按候选方位打分选出与已放标签、标记点都不重叠的位置。 % color 可以是 1×3(所有标签同色)或 n×3(逐点配色)。 if isempty(pts) return; end if size(color, 1) == 1 color = repmat(color, numel(labels), 1); end xl = ax.XLim; yl = ax.YLim; gap = pixel_gap(ax, 5); % 标签与标记点之间的固定像素间距 mbox = marker_size(ax) * [-0.5 -0.5 1 1]; placed = zeros(0, 4); for i = 1:numel(labels) if pts(i, 1) < xl(1) || pts(i, 1) > xl(2) || ... pts(i, 2) < yl(1) || pts(i, 2) > yl(2) continue; % 视野外的点不标注 end sz = text_size(ax, labels{i}); cand = candidates(pts(i, :), sz, gap); best_pen = inf; best = cand(1, :); for k = 1:size(cand, 1) box = [cand(k, 1) - sz(1) / 2 - gap(1), cand(k, 2) - sz(2) / 2 - gap(2), ... sz(1) + 2 * gap(1), sz(2) + 2 * gap(2)]; pen = penalty(box, placed, pts, mbox, xl, yl); if pen < best_pen best_pen = pen; best = cand(k, :); end end text(ax, best(1), best(2), labels{i}, 'Color', color(i, :), ... 'HorizontalAlignment', 'center', 'VerticalAlignment', 'middle'); placed(end + 1, :) = [best(1) - sz(1) / 2 - gap(1), ... best(2) - sz(2) / 2 - gap(2), ... sz(1) + 2 * gap(1), sz(2) + 2 * gap(2)]; %#ok end end function cand = candidates(p, sz, gap) %CANDIDATES 标记点周围的候选标签中心:先近后远,四正方向 + 四斜角。 dx = gap(1) + sz(1) / 2; dy = gap(2) + sz(2) / 2; cand = [p(1) + dx, p(2); p(1) - dx, p(2) p(1), p(2) + dy; p(1), p(2) - dy p(1) + dx, p(2) + dy; p(1) - dx, p(2) + dy p(1) + dx, p(2) - dy; p(1) - dx, p(2) - dy p(1) + 2.2 * dx, p(2); p(1) - 2.2 * dx, p(2) p(1), p(2) + 2.2 * dy; p(1), p(2) - 2.2 * dy p(1) + 1.9 * dx, p(2) + 1.9 * dy; p(1) - 1.9 * dx, p(2) + 1.9 * dy p(1) + 1.9 * dx, p(2) - 1.9 * dy; p(1) - 1.9 * dx, p(2) - 1.9 * dy p(1) + 3.4 * dx, p(2); p(1) - 3.4 * dx, p(2) p(1), p(2) + 3.4 * dy; p(1), p(2) - 3.4 * dy]; end function g = pixel_gap(ax, npix) pos = ax.Position; fpos = ancestor(ax, 'figure').Position; g = [npix * diff(ax.XLim) / (pos(3) * fpos(3)), ... npix * diff(ax.YLim) / (pos(4) * fpos(4))]; end function pen = penalty(box, placed, pts, mbox, xl, yl) pen = 0; for j = 1:size(placed, 1) % 标签之间重叠代价最高 pen = pen + 12 * overlap(box, placed(j, :)); end for j = 1:size(pts, 1) % 其次避免压住标记点 mb = [pts(j, 1) + mbox(1), pts(j, 2) + mbox(2), mbox(3), mbox(4)]; pen = pen + 3 * overlap(box, mb); end dx = max(0, xl(1) - box(1)) + max(0, box(1) + box(3) - xl(2)); dy = max(0, yl(1) - box(2)) + max(0, box(2) + box(4) - yl(2)); pen = pen + 1e3 * (dx * box(4) + dy * box(3)); end function a = overlap(b1, b2) w = min(b1(1) + b1(3), b2(1) + b2(3)) - max(b1(1), b2(1)); h = min(b1(2) + b1(4), b2(2) + b2(4)) - max(b1(2), b2(2)); a = max(0, w) * max(0, h); end function sz = text_size(ax, str) %TEXT_SIZE 文本在数据坐标下的包围盒尺寸(用临时 text 对象的 Extent 量取)。 h = text(ax, 0, 0, str, 'Units', 'data', 'Visible', 'off', ... 'Interpreter', 'none'); e = get(h, 'Extent'); delete(h); sz = [e(3) e(4)] * 1.12; end function w = text_width(ax, str, fs) %TEXT_WIDTH 指定字号下文本在数据坐标下的宽度。 h = text(ax, 0, 0, str, 'Units', 'data', 'Visible', 'off', ... 'FontSize', fs, 'Interpreter', 'none'); e = get(h, 'Extent'); delete(h); w = e(3) * 1.04; end function s = marker_size(ax) xl = ax.XLim; yl = ax.YLim; pos = ax.Position; fpos = ancestor(ax, 'figure').Position; w_px = pos(3) * fpos(3); h_px = pos(4) * fpos(4); s = 8 * max(diff(xl) / w_px, diff(yl) / h_px); % 约 8 px end