@zerodep-dev/vue-tsx-pro-ui 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/charts/AreaChart.d.ts +1 -1
  2. package/dist/charts/AreaChart.js +6 -48
  3. package/dist/charts/AreaChart.js.map +1 -1
  4. package/dist/charts/BarChart.js +6 -49
  5. package/dist/charts/BarChart.js.map +1 -1
  6. package/dist/charts/ChordChart.d.ts +7 -2
  7. package/dist/charts/ChordChart.js +151 -60
  8. package/dist/charts/ChordChart.js.map +1 -1
  9. package/dist/charts/ComboChart.d.ts +2 -1
  10. package/dist/charts/ComboChart.js +17 -93
  11. package/dist/charts/ComboChart.js.map +1 -1
  12. package/dist/charts/DonutChart.d.ts +6 -1
  13. package/dist/charts/DonutChart.js +134 -51
  14. package/dist/charts/DonutChart.js.map +1 -1
  15. package/dist/charts/FunnelChart.js.map +1 -1
  16. package/dist/charts/GraphChart.d.ts +22 -10
  17. package/dist/charts/GraphChart.js +415 -175
  18. package/dist/charts/GraphChart.js.map +1 -1
  19. package/dist/charts/LineChart.d.ts +1 -29
  20. package/dist/charts/LineChart.js +6 -54
  21. package/dist/charts/LineChart.js.map +1 -1
  22. package/dist/charts/MapChart.d.ts +21 -48
  23. package/dist/charts/MapChart.js +426 -353
  24. package/dist/charts/MapChart.js.map +1 -1
  25. package/dist/charts/RadarChart.js.map +1 -1
  26. package/dist/charts/SankeyChart.d.ts +7 -2
  27. package/dist/charts/SankeyChart.js +164 -76
  28. package/dist/charts/SankeyChart.js.map +1 -1
  29. package/dist/charts/ScatterChart.d.ts +7 -1
  30. package/dist/charts/ScatterChart.js +45 -30
  31. package/dist/charts/ScatterChart.js.map +1 -1
  32. package/dist/charts/TimelineChart.d.ts +8 -1
  33. package/dist/charts/TimelineChart.js +141 -89
  34. package/dist/charts/TimelineChart.js.map +1 -1
  35. package/dist/charts/TreemapChart.d.ts +6 -0
  36. package/dist/charts/TreemapChart.js +174 -58
  37. package/dist/charts/TreemapChart.js.map +1 -1
  38. package/dist/charts/WaterfallChart.d.ts +6 -7
  39. package/dist/charts/WaterfallChart.js +46 -51
  40. package/dist/charts/WaterfallChart.js.map +1 -1
  41. package/dist/charts/XYChart.d.ts +44 -35
  42. package/dist/charts/XYChart.js +493 -281
  43. package/dist/charts/XYChart.js.map +1 -1
  44. package/dist/charts/axis-ticks.d.ts +1 -1
  45. package/dist/charts/axis-ticks.js +4 -5
  46. package/dist/charts/axis-ticks.js.map +1 -1
  47. package/dist/charts/flow.d.ts +4 -11
  48. package/dist/charts/flow.js +4 -4
  49. package/dist/charts/flow.js.map +1 -1
  50. package/dist/charts/frame.d.ts +6 -30
  51. package/dist/charts/frame.js +6 -99
  52. package/dist/charts/frame.js.map +1 -1
  53. package/dist/charts/geo/projection.d.ts +45 -0
  54. package/dist/charts/geo/projection.js +201 -0
  55. package/dist/charts/geo/projection.js.map +1 -0
  56. package/dist/charts/geo/topojson.d.ts +50 -0
  57. package/dist/charts/geo/topojson.js +119 -0
  58. package/dist/charts/geo/topojson.js.map +1 -0
  59. package/dist/charts/index.d.ts +21 -9
  60. package/dist/charts/index.js +19 -8
  61. package/dist/charts/label-layout.js.map +1 -1
  62. package/dist/charts/layout/chord.d.ts +44 -0
  63. package/dist/charts/layout/chord.js +45 -0
  64. package/dist/charts/layout/chord.js.map +1 -0
  65. package/dist/charts/layout/graph.d.ts +49 -0
  66. package/dist/charts/layout/graph.js +268 -0
  67. package/dist/charts/layout/graph.js.map +1 -0
  68. package/dist/charts/layout/sankey.d.ts +79 -0
  69. package/dist/charts/layout/sankey.js +200 -0
  70. package/dist/charts/layout/sankey.js.map +1 -0
  71. package/dist/charts/layout/treemap.d.ts +46 -0
  72. package/dist/charts/layout/treemap.js +152 -0
  73. package/dist/charts/layout/treemap.js.map +1 -0
  74. package/dist/charts/marks.d.ts +79 -0
  75. package/dist/charts/marks.js +231 -0
  76. package/dist/charts/marks.js.map +1 -0
  77. package/dist/charts/plot.d.ts +107 -0
  78. package/dist/charts/plot.js +231 -0
  79. package/dist/charts/plot.js.map +1 -0
  80. package/dist/charts/scale.d.ts +29 -0
  81. package/dist/charts/scale.js +244 -0
  82. package/dist/charts/scale.js.map +1 -0
  83. package/dist/charts/shape.d.ts +46 -0
  84. package/dist/charts/shape.js +131 -0
  85. package/dist/charts/shape.js.map +1 -0
  86. package/dist/charts/stack.d.ts +17 -0
  87. package/dist/charts/stack.js +35 -0
  88. package/dist/charts/stack.js.map +1 -0
  89. package/dist/charts/text-size.d.ts +12 -0
  90. package/dist/charts/text-size.js +32 -0
  91. package/dist/charts/text-size.js.map +1 -0
  92. package/dist/config/createUi.d.ts +21 -2
  93. package/dist/config/createUi.js +18 -1
  94. package/dist/config/createUi.js.map +1 -1
  95. package/dist/config/theme/preference.js +2 -1
  96. package/dist/config/theme/preference.js.map +1 -1
  97. package/dist/index.d.ts +2 -2
  98. package/dist/maps/china-provinces.js +35680 -0
  99. package/dist/maps/china-provinces.js.map +1 -0
  100. package/dist/maps/china.d.ts +13 -0
  101. package/dist/maps/china.js +8 -0
  102. package/dist/maps/china.js.map +1 -0
  103. package/dist/maps/index.d.ts +9 -0
  104. package/dist/maps/index.js +3 -0
  105. package/dist/maps/world-countries.js +22273 -0
  106. package/dist/maps/world-countries.js.map +1 -0
  107. package/dist/maps/world.d.ts +13 -0
  108. package/dist/maps/world.js +8 -0
  109. package/dist/maps/world.js.map +1 -0
  110. package/dist/overlay/detached.js.map +1 -1
  111. package/dist/utils/request/use-request.d.ts +25 -1
  112. package/dist/utils/request/use-request.js +35 -3
  113. package/dist/utils/request/use-request.js.map +1 -1
  114. package/package.json +7 -8
  115. package/dist/charts/single.d.ts +0 -41
  116. package/dist/charts/single.js +0 -66
  117. package/dist/charts/single.js.map +0 -1
@@ -1,83 +1,115 @@
1
+ import { svgNumber } from "../utils/data/number.js";
1
2
  import { toNumber } from "../utils/data/numeric.js";
2
- import { colors, fontSizes, opacities } from "../config/theme/tokens.js";
3
+ import { colors, t } from "../config/theme/tokens.js";
3
4
  import { useLang } from "../config/context.js";
4
5
  import { useFormat } from "../config/lang/format.js";
5
6
  import { Skeleton } from "../feedback/Skeleton.js";
7
+ import { usePanZoom } from "../utils/dom/pan-zoom.js";
8
+ import { ChartTooltipBox, chartSvgStyles, pointerIn, usePlotArea } from "./plot.js";
6
9
  import { ChartFrame, chartTooltip, useChartValueText } from "./frame.js";
7
10
  import { resolveColor } from "./series.js";
8
- import { useSingleChart } from "./single.js";
9
- import { computed, createVNode, shallowRef } from "vue";
10
- import { _component, bindable, mergeJsxProps, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
11
+ import { estimateTextWidth, truncateText } from "./text-size.js";
12
+ import { graphLayout } from "./layout/graph.js";
13
+ import { computed, createVNode, ref, shallowRef } from "vue";
14
+ import { _component, bindable, effect, mergeJsxProps, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
11
15
  import { css } from "@zerodep-dev/vue-tsx-pro-tsx/styles";
12
- import { Graph, GraphLayoutType, GraphLinkArrowStyle, GraphLinkStyle, GraphNodeSelectionHighlightMode, GraphNodeShape } from "@unovis/ts";
13
16
  //#region src/charts/GraphChart.tsx
14
- /**
15
- * 网络图:节点与连线,看谁和谁有关系、关系有多强(案例与触发事件的匹配、团队与供应商的协作、依赖关系)。
16
- *
17
- * <GraphChart aria-label="协作关系" nodes={nodes} links={links} layout="dagre" groups={teams} />
18
- *
19
- * 节点是 { id, label?, sublabel?, group?, color?, shape?, size? },连线是 { source, target, value?, label?, dashed? }。
20
- * 布局交给 Unovis 的 Graph:force(默认,力导向)、dagre(分层,direction 定方向)、circular、concentric、parallel
21
- * (按 group 分列)、elk(分层且支持分组框,较大,用到时才加载)。布局异步算好再画,第一帧是空的。
22
- * 写了 groups 时同组的节点框在一起(dagre、elk、parallel 布局;力导向、环形布局里组只决定颜色,框会互相盖住);
23
- * 节点的颜色默认按组取色板,没有组时都是 chart1。
24
- * 连线写了 value 时按大小加粗(1–6px),悬停看“起点 → 终点”与数值。点节点选中它(selected 双向绑定),
25
- * 与它不相连的节点、连线变淡;点空白处取消。能缩放、拖动画布(zoom={false} 关掉),双击回到全图。
26
- * 给读屏的数据表每条连线一行;单位、加载与空状态同其他图表(见 ChartFrame)。
27
- */
28
17
  /** 节点的形状 */
29
18
  /** 布局 */
30
19
  /** 一个节点 */
31
20
  /** 一条连线 */
32
21
  /** 一组节点 */
33
22
  /** 未声明的属性落到外层 div(role="figure")上,aria-label 是图表的名称 */
34
- /** GraphChart 的样式:root(--vis-graph-* 主题变量)、skeleton */
23
+ /** GraphChart 的样式 */
35
24
  var graphChartStyles = css.create({
36
- root: {
37
- "--vis-graph-node-stroke-color": colors.surface,
38
- "--vis-graph-node-label-text-color": colors.text,
39
- "--vis-graph-node-label-font-size": fontSizes.xs,
40
- "--vis-graph-node-label-background": colors.surface,
41
- "--vis-graph-node-sublabel-text-color": colors.textMuted,
42
- "--vis-graph-node-sublabel-font-size": fontSizes.xs,
43
- "--vis-graph-node-selection-color": colors.accent,
44
- "--vis-graph-node-greyout-color": colors.surfaceMuted,
45
- "--vis-graph-node-greyout-opacity": opacities.disabled,
46
- "--vis-graph-link-stroke-color": colors.borderStrong,
47
- "--vis-graph-link-greyout-opacity": "0.25",
48
- "--vis-graph-link-label-background": colors.fill,
49
- "--vis-graph-link-label-text-color-dark": colors.text,
50
- "--vis-graph-link-label-font-size": fontSizes.xs,
51
- "--vis-graph-panel-fill-color": colors.surfaceMuted,
52
- "--vis-graph-panel-border-color": colors.borderMuted,
53
- "--vis-graph-panel-label-color": colors.textMuted,
54
- "--vis-graph-panel-label-background": colors.surface,
55
- "--vis-graph-panel-label-font-size": fontSizes.xs,
56
- "--vis-graph-panel-label-font-weight": "500"
57
- },
58
25
  skeleton: {
59
26
  display: "block",
60
27
  width: "100%",
61
28
  height: "100%"
29
+ },
30
+ viewport: {
31
+ position: "absolute",
32
+ inset: 0
33
+ },
34
+ background: { fill: "transparent" },
35
+ panel: {
36
+ fill: "_surfaceMuted",
37
+ strokeWidth: 1
38
+ },
39
+ panelLabel: {
40
+ fill: "_textMuted",
41
+ fontSize: "_xs",
42
+ fontWeight: "_medium"
43
+ },
44
+ link: {
45
+ fill: "none",
46
+ transition: `opacity ${t.durations.sm} ${t.easings.standard}`
47
+ },
48
+ linkDashed: { strokeDasharray: "5 4" },
49
+ linkLabelBox: { fill: "_fill" },
50
+ linkLabel: {
51
+ fill: "_text",
52
+ fontSize: "_xs"
53
+ },
54
+ node: { transition: `opacity ${t.durations.sm} ${t.easings.standard}` },
55
+ nodeShape: {
56
+ stroke: "_surface",
57
+ strokeWidth: 2
58
+ },
59
+ nodeSelected: {
60
+ stroke: "_accent",
61
+ strokeWidth: 3
62
+ },
63
+ nodeLabel: {
64
+ fill: "_text",
65
+ fontSize: "_xs",
66
+ fontWeight: "_medium"
67
+ },
68
+ nodeSublabel: {
69
+ fill: "_textMuted",
70
+ fontSize: "_xs"
71
+ },
72
+ clickable: { cursor: "pointer" },
73
+ wheelHint: {
74
+ position: "absolute",
75
+ top: "_sm",
76
+ insetInlineEnd: "_sm",
77
+ paddingBlock: "_xs",
78
+ paddingInline: "_sm",
79
+ borderRadius: "_md",
80
+ backgroundColor: "_surfaceRaised",
81
+ boxShadow: "_sm",
82
+ color: "_textMuted",
83
+ fontSize: "_xs",
84
+ pointerEvents: "none"
62
85
  }
63
86
  });
64
- var LAYOUTS = {
65
- force: GraphLayoutType.Force,
66
- dagre: GraphLayoutType.Dagre,
67
- circular: GraphLayoutType.Circular,
68
- concentric: GraphLayoutType.Concentric,
69
- parallel: GraphLayoutType.Parallel,
70
- elk: GraphLayoutType.Elk
71
- };
72
- var SHAPES = {
73
- circle: GraphNodeShape.Circle,
74
- square: GraphNodeShape.Square,
75
- hexagon: GraphNodeShape.Hexagon,
76
- triangle: GraphNodeShape.Triangle
77
- };
78
- /** 交给 Unovis 的节点:带上名称与颜色;Unovis 会往上写布局用的字段,每次都复制一份 */
79
- /** 交给 Unovis 的连线:数值转成 number 算粗细 */
80
- /** Unovis 布局后:连线的起点、终点换成了节点对象 */
87
+ /** 节点的默认大小、名字的行高、名字最多几个字宽(px) */
88
+ var NODE_SIZE = 28;
89
+ var LINE = 16;
90
+ var LABEL_MAX = 120;
91
+ /** 分组框四周、顶上(写组名)留的空 */
92
+ var PANEL_PAD = 16;
93
+ var PANEL_TITLE = 22;
94
+ /** 箭头的长与宽 */
95
+ var ARROW = 8;
96
+ /** 节点的形状:中心在原点、外接圆直径 size 的 SVG 路径 */
97
+ function shapePath(shape, size) {
98
+ const r = size / 2;
99
+ const polygon = (count, rotate, radius) => `${Array.from({ length: count }, (_, i) => {
100
+ const angle = rotate + i / count * Math.PI * 2;
101
+ return `${i ? "L" : "M"}${svgNumber(radius * Math.cos(angle))},${svgNumber(radius * Math.sin(angle))}`;
102
+ }).join("")}Z`;
103
+ if (shape === "square") {
104
+ const half = r * .86;
105
+ return `M${-half},${-half}H${half}V${half}H${-half}Z`;
106
+ }
107
+ if (shape === "hexagon") return polygon(6, 0, r);
108
+ if (shape === "triangle") return polygon(3, -Math.PI / 2, r * 1.1);
109
+ return `M${-r},0A${r},${r},0,1,1,${r},0A${r},${r},0,1,1,${-r},0Z`;
110
+ }
111
+ /** 画图用的节点:带上名称、颜色与位置 */
112
+ /** 悬停的节点或连线 */
81
113
  /** 网络图 */
82
114
  var GraphChart = /*#__PURE__*/ _component((_props, _ctx) => {
83
115
  const _cache = [];
@@ -96,7 +128,6 @@ var GraphChart = /*#__PURE__*/ _component((_props, _ctx) => {
96
128
  const loading = readonlyProp(_props, "loading", false);
97
129
  const empty = readonlyProp(_props, "empty");
98
130
  const onItemClick = readonlyProp(_props, "onItemClick");
99
- const sx = readonlyProp(_props, "sx");
100
131
  const rest = restProps(_props, _ctx, true);
101
132
  const lang = useLang();
102
133
  const format = useFormat();
@@ -104,20 +135,33 @@ var GraphChart = /*#__PURE__*/ _component((_props, _ctx) => {
104
135
  valueFormat: valueFormat.value,
105
136
  unit: unit.value
106
137
  }));
138
+ const area = usePlotArea();
139
+ const viewport = shallowRef(null);
140
+ let hovered = shallowRef(null);
141
+ let wheelHint = ref(false);
142
+ let hintTimer;
143
+ effect(() => () => clearTimeout(hintTimer));
144
+ const view = usePanZoom({
145
+ viewport: () => viewport.value,
146
+ min: .3,
147
+ max: 4,
148
+ disabled: () => !zoom.value,
149
+ filter: (event) => {
150
+ if (event instanceof WheelEvent && !event.ctrlKey && !event.metaKey) {
151
+ wheelHint.value = true;
152
+ clearTimeout(hintTimer);
153
+ hintTimer = setTimeout(() => wheelHint.value = false, 1500);
154
+ return false;
155
+ }
156
+ return !(event.type === "dblclick" && event.target.closest?.("[data-node]"));
157
+ }
158
+ });
107
159
  const groupColors = computed(() => new Map((groups.value ?? []).map((group, position) => [group.id, resolveColor(group.color, position)])));
108
- const resolved = computed(() => nodes.value.map((node, index) => ({
109
- ...node,
110
- label: node.label ?? node.id,
111
- fill: node.color ? resolveColor(node.color, 0) : (node.group && groupColors.value.get(node.group)) ?? resolveColor("chart1", 0),
112
- index
113
- })));
114
- const byId = computed(() => new Map(resolved.value.map((node) => [node.id, node])));
160
+ const byId = computed(() => new Map(nodes.value.map((node) => [node.id, node])));
115
161
  const labelOf = (id) => byId.value.get(id)?.label ?? id;
116
- const shownLinks = computed(() => links.value.filter((link) => byId.value.has(link.source) && byId.value.has(link.target)).map((link) => ({
117
- ...link,
118
- amount: link.value === void 0 ? void 0 : toNumber(link.value)
119
- })));
120
- const maxAmount = computed(() => Math.max(0, ...shownLinks.value.map((link) => link.amount ?? 0)));
162
+ const shownLinks = computed(() => links.value.filter((link) => byId.value.has(link.source) && byId.value.has(link.target)));
163
+ const amounts = computed(() => shownLinks.value.map((link) => link.value === void 0 ? void 0 : toNumber(link.value)));
164
+ const maxAmount = computed(() => Math.max(0, ...amounts.value.map((amount) => amount ?? 0)));
121
165
  const degree = computed(() => {
122
166
  const counts = /* @__PURE__ */ new Map();
123
167
  for (const link of shownLinks.value) {
@@ -126,112 +170,308 @@ var GraphChart = /*#__PURE__*/ _component((_props, _ctx) => {
126
170
  }
127
171
  return counts;
128
172
  });
129
- let plot = shallowRef(null);
130
- const chart = useSingleChart(() => ({
131
- element: plot.value,
132
- ready: !loading.value && nodes.value.length > 0,
133
- tooltip: tooltip.value,
134
- build: (instance) => {
135
- const graph = instance("graph", () => new Graph({}));
136
- const layered = layout.value === "dagre" || layout.value === "elk";
137
- const framed = layered || layout.value === "parallel";
138
- graph.setConfig({
139
- layoutType: LAYOUTS[layout.value],
140
- layoutNodeGroup: (node) => node.group ?? "",
141
- layoutGroupOrder: groups.value?.map((group) => group.id),
142
- dagreLayoutSettings: {
143
- rankdir: direction.value,
144
- nodesep: groups.value?.length ? 48 : 36,
145
- ranksep: groups.value?.length ? 150 : 96
146
- },
147
- layoutElkSettings: {
148
- "elk.direction": direction.value === "LR" ? "RIGHT" : "DOWN",
149
- "elk.spacing.nodeNode": "36",
150
- "elk.layered.spacing.nodeNodeBetweenLayers": "96"
151
- },
152
- layoutElkNodeGroups: groups.value?.length ? [(node) => node.group ?? ""] : void 0,
153
- forceLayoutSettings: {
154
- linkDistance: 90,
155
- charge: -600
156
- },
157
- nodeSize: (node) => node.size ?? 28,
158
- nodeShape: (node) => SHAPES[node.shape ?? "circle"],
159
- nodeFill: (node) => node.fill,
160
- nodeStrokeWidth: 2,
161
- nodeLabel: (node) => node.label,
162
- nodeSubLabel: (node) => node.sublabel ?? "",
163
- nodeLabelTrimLength: 16,
164
- nodeIcon: () => "",
165
- linkWidth: (link) => link.amount === void 0 || maxAmount.value === 0 ? 1.5 : 1 + 5 * link.amount / maxAmount.value,
166
- linkStroke: (link) => link.color ? resolveColor(link.color, 0) : void 0,
167
- linkStyle: (link) => link.dashed ? GraphLinkStyle.Dashed : GraphLinkStyle.Solid,
168
- linkArrow: arrows.value ?? layered ? GraphLinkArrowStyle.Single : void 0,
169
- linkLabel: (link) => link.label ? { text: link.label } : void 0,
170
- linkCurvature: layered ? 0 : .2,
171
- panels: (framed ? groups.value ?? [] : []).filter((group) => resolved.value.some((node) => node.group === group.id)).map((group) => ({
172
- nodes: resolved.value.filter((node) => node.group === group.id).map((node) => node.id),
173
- label: group.label,
174
- padding: 16,
175
- borderColor: groupColors.value.get(group.id)
176
- })),
177
- selectedNodeId: selected.value ?? void 0,
178
- nodeSelectionHighlightMode: GraphNodeSelectionHighlightMode.GreyoutNonConnected,
179
- disableZoom: !zoom.value,
180
- disableDrag: !zoom.value,
181
- disableBrush: true,
182
- fitViewPadding: {
183
- top: 48,
184
- right: 32,
185
- bottom: 32,
186
- left: 32
187
- },
188
- events: {
189
- [Graph.selectors.node]: { click: (node) => {
190
- selected.value = node.id;
191
- onItemClick.value?.(nodes.value[node.index], node.index);
192
- } },
193
- [Graph.selectors.background]: {
194
- click: () => selected.value = null,
195
- dblclick: () => graph.fitView()
196
- }
197
- }
198
- });
173
+ const layered = () => layout.value === "layered";
174
+ const framed = () => layered() || layout.value === "parallel";
175
+ /** 布局:节点的位置(未缩放,单位 px) */
176
+ const positions = computed(() => {
177
+ const inputs = nodes.value.map((node) => {
178
+ const size = node.size ?? NODE_SIZE;
179
+ const label = node.label ?? node.id;
180
+ const textWidth = Math.min(LABEL_MAX, Math.max(estimateTextWidth(label), estimateTextWidth(node.sublabel ?? "")));
181
+ return {
182
+ id: node.id,
183
+ group: node.group,
184
+ width: Math.max(size, textWidth) + 8,
185
+ height: size + 6 + LINE * (node.sublabel ? 2 : 1)
186
+ };
187
+ });
188
+ return graphLayout(inputs, shownLinks.value, {
189
+ layout: layout.value,
190
+ direction: direction.value,
191
+ groups: groups.value?.map((group) => group.id)
192
+ });
193
+ });
194
+ /** 放进绘图区:位置按绘图区缩放、居中(节点与文字不缩放),最多放大到 1.2 倍 */
195
+ const placed = computed(() => {
196
+ const items = nodes.value.map((node, index) => {
197
+ const position = positions.value.get(node.id) ?? {
198
+ x: 0,
199
+ y: 0
200
+ };
201
+ return {
202
+ ...node,
203
+ label: node.label ?? node.id,
204
+ fill: node.color ? resolveColor(node.color, 0) : (node.group && groupColors.value.get(node.group)) ?? resolveColor("chart1", 0),
205
+ index,
206
+ size: node.size ?? NODE_SIZE,
207
+ x: position.x,
208
+ y: position.y
209
+ };
210
+ });
211
+ if (!items.length) return items;
212
+ const xs = items.map((item) => item.x);
213
+ const ys = items.map((item) => item.y);
214
+ const [minX, maxX, minY, maxY] = [
215
+ Math.min(...xs),
216
+ Math.max(...xs),
217
+ Math.min(...ys),
218
+ Math.max(...ys)
219
+ ];
220
+ const frame = framed() && groups.value?.length ? PANEL_PAD : 0;
221
+ const room = {
222
+ x: LABEL_MAX / 2 + frame + 8,
223
+ top: NODE_SIZE / 2 + frame + (frame ? PANEL_TITLE : 0) + 8,
224
+ bottom: 46 + frame + 8
225
+ };
226
+ const width = area.width - room.x * 2;
227
+ const height = area.height - room.top - room.bottom;
228
+ const scale = Math.min(1.2, maxX > minX ? width / (maxX - minX) : 1.2, maxY > minY ? height / (maxY - minY) : 1.2);
229
+ const offsetX = room.x + (width - (maxX - minX) * scale) / 2 - minX * scale;
230
+ const offsetY = room.top + (height - (maxY - minY) * scale) / 2 - minY * scale;
231
+ return items.map((item) => ({
232
+ ...item,
233
+ x: item.x * scale + offsetX,
234
+ y: item.y * scale + offsetY
235
+ }));
236
+ });
237
+ const placedById = computed(() => new Map(placed.value.map((node) => [node.id, node])));
238
+ /** 选中节点时它与相连的节点、连线保持原样,其余变淡 */
239
+ const connected = computed(() => {
240
+ if (selected.value === null) return null;
241
+ const ids = /* @__PURE__ */ new Set([selected.value]);
242
+ for (const link of shownLinks.value) {
243
+ if (link.source === selected.value) ids.add(link.target);
244
+ if (link.target === selected.value) ids.add(link.source);
245
+ }
246
+ return ids;
247
+ });
248
+ /** 一条连线的路径、终点处的方向(画箭头)与中点(写标签) */
249
+ function linkGeometry(source, target, arrow) {
250
+ const tail = arrow ? ARROW : 0;
251
+ if (layered()) {
252
+ const horizontal = direction.value === "LR";
253
+ const [sx, sy] = horizontal ? [source.x + source.size / 2, source.y] : [source.x, source.y + source.size / 2];
254
+ const [tx, ty] = horizontal ? [target.x - target.size / 2 - tail, target.y] : [target.x, target.y - target.size / 2 - tail];
199
255
  return {
200
- component: graph,
201
- data: {
202
- nodes: resolved.value.map((node) => ({ ...node })),
203
- links: shownLinks.value.map((link) => ({ ...link }))
204
- },
205
- triggers: {
206
- [Graph.selectors.node]: (node) => chart.tooltip(chartTooltip(node.label, [...node.sublabel ? [{
207
- label: node.sublabel,
208
- color: "transparent",
209
- value: ""
210
- }] : [], {
211
- label: lang.chartDegree(degree.value.get(node.id) ?? 0),
212
- color: node.fill,
213
- value: ""
214
- }])),
215
- [Graph.selectors.link]: (link) => chart.tooltip(chartTooltip(lang.chartFlowLabel(link.source.label, link.target.label), [...link.value === void 0 ? [] : [{
216
- label: lang.chartValue,
217
- color: link.source.fill,
218
- value: valueText(link.value)
219
- }], ...link.label ? [{
220
- label: link.label,
221
- color: "transparent",
222
- value: ""
223
- }] : []]))
224
- }
256
+ d: horizontal ? `M${svgNumber(sx)},${svgNumber(sy)}C${svgNumber((sx + tx) / 2)},${svgNumber(sy)},${svgNumber((sx + tx) / 2)},${svgNumber(ty)},${svgNumber(tx)},${svgNumber(ty)}` : `M${svgNumber(sx)},${svgNumber(sy)}C${svgNumber(sx)},${svgNumber((sy + ty) / 2)},${svgNumber(tx)},${svgNumber((sy + ty) / 2)},${svgNumber(tx)},${svgNumber(ty)}`,
257
+ end: [tx, ty],
258
+ angle: horizontal ? 0 : Math.PI / 2,
259
+ middle: [(sx + tx) / 2, (sy + ty) / 2]
225
260
  };
226
261
  }
227
- }));
262
+ const dx = target.x - source.x;
263
+ const dy = target.y - source.y;
264
+ const length = Math.hypot(dx, dy) || 1;
265
+ const control = [(source.x + target.x) / 2 - dy / length * length * .12, (source.y + target.y) / 2 + dx / length * length * .12];
266
+ const toward = (from, to, distance) => {
267
+ const ux = to[0] - from[0];
268
+ const uy = to[1] - from[1];
269
+ const norm = Math.hypot(ux, uy) || 1;
270
+ return [from[0] + ux / norm * distance, from[1] + uy / norm * distance];
271
+ };
272
+ const start = toward([source.x, source.y], control, source.size / 2);
273
+ const end = toward([target.x, target.y], control, target.size / 2 + tail);
274
+ return {
275
+ d: `M${svgNumber(start[0])},${svgNumber(start[1])}Q${svgNumber(control[0])},${svgNumber(control[1])},${svgNumber(end[0])},${svgNumber(end[1])}`,
276
+ end,
277
+ angle: Math.atan2(target.y - control[1], target.x - control[0]),
278
+ middle: [(start[0] + 2 * control[0] + end[0]) / 4, (start[1] + 2 * control[1] + end[1]) / 4]
279
+ };
280
+ }
281
+ function onPointermove(event) {
282
+ const target = event.target.closest?.("[data-node],[data-link]");
283
+ if (!target) {
284
+ hovered.value = null;
285
+ return;
286
+ }
287
+ const { x, y } = pointerIn(event, viewport.value ?? event.currentTarget);
288
+ const node = target.getAttribute("data-node");
289
+ hovered.value = node === null ? {
290
+ kind: "link",
291
+ index: Number(target.getAttribute("data-link")),
292
+ x,
293
+ y
294
+ } : {
295
+ kind: "node",
296
+ index: Number(node),
297
+ x,
298
+ y
299
+ };
300
+ }
301
+ function onClick(event) {
302
+ const target = event.target.closest?.("[data-node]");
303
+ if (!target) {
304
+ selected.value = null;
305
+ return;
306
+ }
307
+ const node = placed.value[Number(target.getAttribute("data-node"))];
308
+ if (!node) return;
309
+ selected.value = node.id;
310
+ onItemClick.value?.(nodes.value[node.index], node.index);
311
+ }
312
+ function renderPanels() {
313
+ if (!framed()) return null;
314
+ return (groups.value ?? []).map((group) => {
315
+ const members = placed.value.filter((node) => node.group === group.id);
316
+ if (!members.length) return null;
317
+ const half = (node) => Math.max(node.size / 2, Math.min(LABEL_MAX, estimateTextWidth(node.label)) / 2);
318
+ const x0 = Math.min(...members.map((node) => node.x - half(node))) - PANEL_PAD;
319
+ const x1 = Math.max(...members.map((node) => node.x + half(node))) + PANEL_PAD;
320
+ const y0 = Math.min(...members.map((node) => node.y - node.size / 2)) - PANEL_PAD - PANEL_TITLE;
321
+ const y1 = Math.max(...members.map((node) => node.y + node.size / 2 + LINE * (node.sublabel ? 2 : 1) + 4)) + PANEL_PAD;
322
+ return createVNode("g", {
323
+ "key": group.id,
324
+ "data-part": "panel"
325
+ }, [createVNode("rect", mergeJsxProps({
326
+ "x": svgNumber(x0),
327
+ "y": svgNumber(y0),
328
+ "width": svgNumber(x1 - x0),
329
+ "height": svgNumber(y1 - y0),
330
+ "rx": 8
331
+ }, css.attrs(graphChartStyles.panel, chartSvgStyles.stroke(groupColors.value.get(group.id) ?? colors.borderMuted))), null), createVNode("text", mergeJsxProps({
332
+ "x": svgNumber(x0 + 10),
333
+ "y": svgNumber(y0 + PANEL_TITLE / 2 + 2),
334
+ "dominant-baseline": "central"
335
+ }, css.attrs(graphChartStyles.panelLabel)), [truncateText(group.label, x1 - x0 - 20)])]);
336
+ });
337
+ }
338
+ function renderLinks() {
339
+ const withArrows = arrows.value ?? layered();
340
+ return shownLinks.value.map((link, index) => {
341
+ const source = placedById.value.get(link.source);
342
+ const target = placedById.value.get(link.target);
343
+ if (source === target) return null;
344
+ const amount = amounts.value[index];
345
+ const width = amount === void 0 || maxAmount.value === 0 ? 1.5 : 1 + 5 * amount / maxAmount.value;
346
+ const color = link.color ? resolveColor(link.color, 0) : colors.borderStrong;
347
+ const { d, end, angle, middle } = linkGeometry(source, target, withArrows);
348
+ const faded = connected.value !== null && link.source !== selected.value && link.target !== selected.value;
349
+ const arrowSize = ARROW + width;
350
+ const arrowPath = () => {
351
+ const [ex, ey] = end;
352
+ const tipX = ex + Math.cos(angle) * arrowSize;
353
+ const tipY = ey + Math.sin(angle) * arrowSize;
354
+ const side = arrowSize * .55;
355
+ const nx = -Math.sin(angle) * side;
356
+ const ny = Math.cos(angle) * side;
357
+ return `M${svgNumber(tipX)},${svgNumber(tipY)}L${svgNumber(ex + nx)},${svgNumber(ey + ny)}L${svgNumber(ex - nx)},${svgNumber(ey - ny)}Z`;
358
+ };
359
+ return createVNode("g", mergeJsxProps({ "key": `${link.source}->${link.target}:${index}` }, css.attrs(graphChartStyles.node, faded && chartSvgStyles.dimmed)), [
360
+ createVNode("path", mergeJsxProps({
361
+ "data-part": "link",
362
+ "data-link": index,
363
+ "d": d,
364
+ "stroke-width": svgNumber(width)
365
+ }, css.attrs(graphChartStyles.link, chartSvgStyles.stroke(color), link.dashed && graphChartStyles.linkDashed)), null),
366
+ withArrows && createVNode("path", mergeJsxProps({ "d": arrowPath() }, css.attrs(chartSvgStyles.fill(color))), null),
367
+ link.label && createVNode("g", mergeJsxProps({ "transform": `translate(${svgNumber(middle[0])},${svgNumber(middle[1])})` }, css.attrs(chartSvgStyles.noPointer)), [createVNode("rect", mergeJsxProps({
368
+ "x": -estimateTextWidth(link.label) / 2 - 4,
369
+ "y": -9,
370
+ "width": estimateTextWidth(link.label) + 8,
371
+ "height": 18,
372
+ "rx": 4
373
+ }, css.attrs(graphChartStyles.linkLabelBox)), null), createVNode("text", mergeJsxProps({
374
+ "text-anchor": "middle",
375
+ "dominant-baseline": "central"
376
+ }, css.attrs(graphChartStyles.linkLabel)), [link.label])])
377
+ ]);
378
+ });
379
+ }
380
+ function renderNodes() {
381
+ return placed.value.map((node, index) => {
382
+ const faded = connected.value !== null && !connected.value.has(node.id);
383
+ const shape = node.shape ?? "circle";
384
+ return createVNode("g", mergeJsxProps({
385
+ "key": node.id,
386
+ "data-part": "node",
387
+ "data-node": index,
388
+ "transform": `translate(${svgNumber(node.x)},${svgNumber(node.y)})`
389
+ }, css.attrs(graphChartStyles.node, onItemClick.value && graphChartStyles.clickable, faded && chartSvgStyles.dimmed)), [
390
+ createVNode("path", mergeJsxProps({ "d": shapePath(shape, node.size) }, css.attrs(graphChartStyles.nodeShape, chartSvgStyles.fill(node.fill), selected.value === node.id && graphChartStyles.nodeSelected)), null),
391
+ createVNode("text", mergeJsxProps({
392
+ "y": node.size / 2 + 4,
393
+ "text-anchor": "middle",
394
+ "dominant-baseline": "hanging"
395
+ }, css.attrs(graphChartStyles.nodeLabel, chartSvgStyles.halo)), [truncateText(node.label, LABEL_MAX)]),
396
+ node.sublabel && createVNode("text", mergeJsxProps({
397
+ "y": node.size / 2 + 4 + LINE,
398
+ "text-anchor": "middle",
399
+ "dominant-baseline": "hanging"
400
+ }, css.attrs(graphChartStyles.nodeSublabel, chartSvgStyles.halo)), [truncateText(node.sublabel, LABEL_MAX)])
401
+ ]);
402
+ });
403
+ }
404
+ const tip = () => {
405
+ const current = hovered.value;
406
+ if (!tooltip.value || !current) return null;
407
+ if (current.kind === "node") {
408
+ const node = placed.value[current.index];
409
+ if (!node) return null;
410
+ return {
411
+ x: current.x,
412
+ y: current.y,
413
+ content: chartTooltip(node.label, [...node.sublabel ? [{
414
+ label: node.sublabel,
415
+ color: "transparent",
416
+ value: ""
417
+ }] : [], {
418
+ label: lang.chartDegree(degree.value.get(node.id) ?? 0),
419
+ color: node.fill,
420
+ value: ""
421
+ }])
422
+ };
423
+ }
424
+ const link = shownLinks.value[current.index];
425
+ if (!link) return null;
426
+ const source = placedById.value.get(link.source);
427
+ return {
428
+ x: current.x,
429
+ y: current.y,
430
+ content: chartTooltip(lang.chartFlowLabel(source.label, placedById.value.get(link.target).label), [...link.value === void 0 ? [] : [{
431
+ label: lang.chartValue,
432
+ color: source.fill,
433
+ value: valueText(link.value)
434
+ }], ...link.label ? [{
435
+ label: link.label,
436
+ color: "transparent",
437
+ value: ""
438
+ }] : []])
439
+ };
440
+ };
441
+ function renderPlot() {
442
+ return createVNode("div", mergeJsxProps(css.attrs(graphChartStyles.viewport), zoom.value ? view.viewportProps : {}, {
443
+ "ref": _cache[0] || (_cache[0] = (element) => viewport.value = element),
444
+ "onPointermove": _cache[1] || (_cache[1] = (event) => {
445
+ if (zoom.value) view.viewportProps.onPointermove(event);
446
+ onPointermove(event);
447
+ }),
448
+ "onPointerleave": _cache[2] || (_cache[2] = () => hovered.value = null)
449
+ }), [
450
+ createVNode("svg", mergeJsxProps({
451
+ "width": area.width,
452
+ "height": area.height
453
+ }, css.attrs(chartSvgStyles.svg), { "onClick": onClick }), [createVNode("rect", mergeJsxProps({
454
+ "data-part": "background",
455
+ "width": area.width,
456
+ "height": area.height
457
+ }, css.attrs(graphChartStyles.background)), null), createVNode("g", { "transform": `translate(${svgNumber(view.x)} ${svgNumber(view.y)}) scale(${svgNumber(view.scale)})` }, [createVNode("g", css.attrs(chartSvgStyles.fadeIn), [
458
+ renderPanels(),
459
+ renderLinks(),
460
+ renderNodes()
461
+ ])])]),
462
+ createVNode(ChartTooltipBox, {
463
+ "tip": tip(),
464
+ "width": area.width
465
+ }, null),
466
+ wheelHint.value && createVNode("span", css.attrs(graphChartStyles.wheelHint), [lang.chartZoomWheelHint])
467
+ ]);
468
+ }
228
469
  return () => createVNode(ChartFrame, mergeJsxProps(rest, {
229
- "sx": [graphChartStyles.root, sx.value],
230
470
  "unit": unit.value,
231
471
  "height": height.value,
232
- "legend": _cache[0] || (_cache[0] = []),
233
- "hiddenKeys": _cache[1] || (_cache[1] = []),
234
- "onLegendToggle": _cache[2] || (_cache[2] = () => {}),
472
+ "legend": _cache[3] || (_cache[3] = []),
473
+ "hiddenKeys": _cache[4] || (_cache[4] = []),
474
+ "onLegendToggle": _cache[5] || (_cache[5] = () => {}),
235
475
  "loading": loading.value,
236
476
  "skeleton": createVNode(Skeleton, { "sx": graphChartStyles.skeleton }, null),
237
477
  "isEmpty": nodes.value.length === 0,
@@ -244,7 +484,8 @@ var GraphChart = /*#__PURE__*/ _component((_props, _ctx) => {
244
484
  cells: [[link.value === void 0 ? "" : format.number(link.value, valueFormat.value), link.label ?? ""].filter(Boolean).join(" · ") || lang.emptyValue]
245
485
  }))
246
486
  },
247
- "plotRef": _cache[3] || (_cache[3] = (element) => plot.value = element)
487
+ "plotRef": area.ref,
488
+ "plot": area.ready ? renderPlot() : null
248
489
  }), null);
249
490
  }, {
250
491
  name: "GraphChart",
@@ -264,8 +505,7 @@ var GraphChart = /*#__PURE__*/ _component((_props, _ctx) => {
264
505
  "onUpdate:_selected",
265
506
  "loading",
266
507
  "empty",
267
- "onItemClick",
268
- "sx"
508
+ "onItemClick"
269
509
  ]
270
510
  });
271
511
  //#endregion