@zerodep-dev/vue-tsx-pro-ui 0.3.2 → 0.4.0
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.
- package/dist/charts/AreaChart.d.ts +1 -1
- package/dist/charts/AreaChart.js +6 -48
- package/dist/charts/AreaChart.js.map +1 -1
- package/dist/charts/BarChart.js +6 -49
- package/dist/charts/BarChart.js.map +1 -1
- package/dist/charts/ChordChart.d.ts +7 -2
- package/dist/charts/ChordChart.js +151 -60
- package/dist/charts/ChordChart.js.map +1 -1
- package/dist/charts/ComboChart.d.ts +2 -1
- package/dist/charts/ComboChart.js +17 -93
- package/dist/charts/ComboChart.js.map +1 -1
- package/dist/charts/DonutChart.d.ts +6 -1
- package/dist/charts/DonutChart.js +134 -51
- package/dist/charts/DonutChart.js.map +1 -1
- package/dist/charts/FunnelChart.js.map +1 -1
- package/dist/charts/GraphChart.d.ts +22 -10
- package/dist/charts/GraphChart.js +415 -175
- package/dist/charts/GraphChart.js.map +1 -1
- package/dist/charts/LineChart.d.ts +1 -29
- package/dist/charts/LineChart.js +6 -54
- package/dist/charts/LineChart.js.map +1 -1
- package/dist/charts/MapChart.d.ts +21 -48
- package/dist/charts/MapChart.js +426 -353
- package/dist/charts/MapChart.js.map +1 -1
- package/dist/charts/RadarChart.js.map +1 -1
- package/dist/charts/SankeyChart.d.ts +7 -2
- package/dist/charts/SankeyChart.js +164 -76
- package/dist/charts/SankeyChart.js.map +1 -1
- package/dist/charts/ScatterChart.d.ts +7 -1
- package/dist/charts/ScatterChart.js +45 -30
- package/dist/charts/ScatterChart.js.map +1 -1
- package/dist/charts/TimelineChart.d.ts +8 -1
- package/dist/charts/TimelineChart.js +141 -89
- package/dist/charts/TimelineChart.js.map +1 -1
- package/dist/charts/TreemapChart.d.ts +6 -0
- package/dist/charts/TreemapChart.js +174 -58
- package/dist/charts/TreemapChart.js.map +1 -1
- package/dist/charts/WaterfallChart.d.ts +6 -7
- package/dist/charts/WaterfallChart.js +46 -51
- package/dist/charts/WaterfallChart.js.map +1 -1
- package/dist/charts/XYChart.d.ts +44 -35
- package/dist/charts/XYChart.js +493 -281
- package/dist/charts/XYChart.js.map +1 -1
- package/dist/charts/axis-ticks.d.ts +1 -1
- package/dist/charts/axis-ticks.js +4 -5
- package/dist/charts/axis-ticks.js.map +1 -1
- package/dist/charts/flow.d.ts +4 -11
- package/dist/charts/flow.js +4 -4
- package/dist/charts/flow.js.map +1 -1
- package/dist/charts/frame.d.ts +6 -30
- package/dist/charts/frame.js +6 -99
- package/dist/charts/frame.js.map +1 -1
- package/dist/charts/geo/projection.d.ts +45 -0
- package/dist/charts/geo/projection.js +201 -0
- package/dist/charts/geo/projection.js.map +1 -0
- package/dist/charts/geo/topojson.d.ts +50 -0
- package/dist/charts/geo/topojson.js +119 -0
- package/dist/charts/geo/topojson.js.map +1 -0
- package/dist/charts/index.d.ts +21 -9
- package/dist/charts/index.js +19 -8
- package/dist/charts/label-layout.js.map +1 -1
- package/dist/charts/layout/chord.d.ts +44 -0
- package/dist/charts/layout/chord.js +45 -0
- package/dist/charts/layout/chord.js.map +1 -0
- package/dist/charts/layout/graph.d.ts +49 -0
- package/dist/charts/layout/graph.js +268 -0
- package/dist/charts/layout/graph.js.map +1 -0
- package/dist/charts/layout/sankey.d.ts +79 -0
- package/dist/charts/layout/sankey.js +200 -0
- package/dist/charts/layout/sankey.js.map +1 -0
- package/dist/charts/layout/treemap.d.ts +46 -0
- package/dist/charts/layout/treemap.js +152 -0
- package/dist/charts/layout/treemap.js.map +1 -0
- package/dist/charts/marks.d.ts +79 -0
- package/dist/charts/marks.js +231 -0
- package/dist/charts/marks.js.map +1 -0
- package/dist/charts/plot.d.ts +107 -0
- package/dist/charts/plot.js +231 -0
- package/dist/charts/plot.js.map +1 -0
- package/dist/charts/scale.d.ts +29 -0
- package/dist/charts/scale.js +244 -0
- package/dist/charts/scale.js.map +1 -0
- package/dist/charts/shape.d.ts +46 -0
- package/dist/charts/shape.js +131 -0
- package/dist/charts/shape.js.map +1 -0
- package/dist/charts/stack.d.ts +17 -0
- package/dist/charts/stack.js +35 -0
- package/dist/charts/stack.js.map +1 -0
- package/dist/charts/text-size.d.ts +12 -0
- package/dist/charts/text-size.js +32 -0
- package/dist/charts/text-size.js.map +1 -0
- package/dist/display/VirtualList.d.ts +1 -2
- package/dist/display/VirtualList.js +11 -21
- package/dist/display/VirtualList.js.map +1 -1
- package/dist/input/Combobox.d.ts +1 -1
- package/dist/input/Combobox.js.map +1 -1
- package/dist/input/ListPopover.d.ts +1 -1
- package/dist/input/ListPopover.js.map +1 -1
- package/dist/input/Listbox.d.ts +1 -2
- package/dist/input/Listbox.js +7 -19
- package/dist/input/Listbox.js.map +1 -1
- package/dist/input/Select.d.ts +1 -1
- package/dist/input/Select.js.map +1 -1
- package/dist/maps/china-provinces.js +35680 -0
- package/dist/maps/china-provinces.js.map +1 -0
- package/dist/maps/china.d.ts +13 -0
- package/dist/maps/china.js +8 -0
- package/dist/maps/china.js.map +1 -0
- package/dist/maps/index.d.ts +9 -0
- package/dist/maps/index.js +3 -0
- package/dist/maps/world-countries.js +22273 -0
- package/dist/maps/world-countries.js.map +1 -0
- package/dist/maps/world.d.ts +13 -0
- package/dist/maps/world.js +8 -0
- package/dist/maps/world.js.map +1 -0
- package/dist/overlay/CommandPalette.d.ts +1 -1
- package/dist/overlay/CommandPalette.js.map +1 -1
- package/dist/overlay/detached.js +14 -1
- package/dist/overlay/detached.js.map +1 -1
- package/dist/patterns/QueryTable.js +0 -2
- package/dist/patterns/QueryTable.js.map +1 -1
- package/dist/table/DataTable.js +1 -1
- package/dist/table/DataTable.js.map +1 -1
- package/dist/table/DataTableView.d.ts +0 -3
- package/dist/table/DataTableView.js +32 -26
- package/dist/table/DataTableView.js.map +1 -1
- package/dist/table/table-styles.d.ts +1 -1
- package/dist/table/table-styles.js +0 -13
- package/dist/table/table-styles.js.map +1 -1
- package/package.json +7 -8
- package/dist/charts/single.d.ts +0 -41
- package/dist/charts/single.js +0 -66
- package/dist/charts/single.js.map +0 -1
package/dist/charts/MapChart.js
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
|
+
import { svgNumber } from "../utils/data/number.js";
|
|
1
2
|
import { toNumber } from "../utils/data/numeric.js";
|
|
2
|
-
import { colors } 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 { IconButton } from "../base/IconButton.js";
|
|
5
|
-
import {
|
|
6
|
+
import { useReducedMotion } from "../utils/dom/media-query.js";
|
|
6
7
|
import { useFormat } from "../config/lang/format.js";
|
|
7
8
|
import { Skeleton } from "../feedback/Skeleton.js";
|
|
9
|
+
import { usePanZoom } from "../utils/dom/pan-zoom.js";
|
|
10
|
+
import { ChartTooltipBox, chartSvgStyles, pointerIn, usePlotArea } from "./plot.js";
|
|
8
11
|
import { ChartFrame, chartTooltip } from "./frame.js";
|
|
9
12
|
import { resolveColor } from "./series.js";
|
|
10
|
-
import {
|
|
13
|
+
import { estimateTextWidth } from "./text-size.js";
|
|
14
|
+
import { createProjection } from "./geo/projection.js";
|
|
15
|
+
import { simplifyTopology, topologyBounds, topologyFeatures } from "./geo/topojson.js";
|
|
11
16
|
import { placeLabels, rectsOverlap } from "./label-layout.js";
|
|
12
17
|
import { computed, createTextVNode, createVNode, ref, shallowRef } from "vue";
|
|
13
18
|
import { _component, effect, mergeJsxProps, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
|
|
14
19
|
import { css } from "@zerodep-dev/vue-tsx-pro-tsx/styles";
|
|
15
20
|
import { Maximize, Minus, Plus } from "@lucide/icons";
|
|
16
|
-
import { MapProjection, TopoJSONMap } from "@unovis/ts";
|
|
17
|
-
import { presimplify, simplify } from "topojson-simplify";
|
|
18
21
|
//#region src/charts/MapChart.tsx
|
|
19
22
|
/**
|
|
20
23
|
* 地图:按区域着色(分省的销售额、各国的采购额),标出地点(工厂、供应商)与它们之间的流向。
|
|
21
|
-
* 地理数据是 TopoJSON
|
|
24
|
+
* 地理数据是 TopoJSON:`@zerodep-dev/vue-tsx-pro-ui/maps` 带着中国省份与世界各国,也可以用自己的。
|
|
22
25
|
*
|
|
23
|
-
* import {
|
|
26
|
+
* import { chinaProvinces } from '@zerodep-dev/vue-tsx-pro-ui/maps';
|
|
24
27
|
* <MapChart
|
|
25
28
|
* aria-label="各省销售额"
|
|
26
|
-
* topology={
|
|
29
|
+
* topology={chinaProvinces}
|
|
27
30
|
* feature="provinces"
|
|
28
31
|
* areas={[{ id: 'CN-JS', label: '江苏', value: 623 }, …]} // id 是地图要素的 id
|
|
29
32
|
* points={[{ id: 'sh', label: '上海组装', longitude: 121.47, latitude: 31.23, value: 860 }]}
|
|
@@ -32,49 +35,77 @@ import { presimplify, simplify } from "topojson-simplify";
|
|
|
32
35
|
* areaLabels
|
|
33
36
|
* />
|
|
34
37
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
38
|
+
* 投影(见 createProjection):默认按地理数据的范围选——一个国家、一个地区用以它为中心的等积圆锥投影
|
|
39
|
+
* (中国地图常用的 Albers,正着、不歪),跨了大半个地球时用 Natural Earth;也可以指定 mercator、equalEarth、
|
|
40
|
+
* equirectangular、orthographic(地球仪)或自己的投影函数(d3-geo 的投影可以直接传)。
|
|
41
|
+
* 地理数据先按绘图区的像素简化(看不出的点去掉),再投影、按绘图区缩放居中。
|
|
37
42
|
*
|
|
38
43
|
* 着色:sequential(默认)按数值深浅;diverging 以 midpoint 为界两头两种颜色(同比的涨跌);
|
|
39
44
|
* bins 分成几档(分级着色,图例是几个色块),不写时连续着色(图例是色带);区域写了 color 时按它(分类着色)。
|
|
40
45
|
* 没有数据的区域是底色。地点:value 决定大小(面积按比例),color、shape 决定颜色与形状;名字自动避让
|
|
41
46
|
* (换方位、挤时往外退并画引线,数值大的先放,放不下的悬停看;压住名字、地点的区域名藏起来);
|
|
42
|
-
* links 是地点之间的流向,粗细按 value,flow
|
|
43
|
-
* zoomable 时可以拖动、按住 Ctrl
|
|
47
|
+
* links 是地点之间的流向,粗细按 value,flow 时有流动的粒子(“减少动态效果”时不播)。
|
|
48
|
+
* zoomable 时可以拖动、按住 Ctrl 滚轮缩放,右上角有放大、缩小、复位的按钮;缩放时区域放大,地点与文字保持原来的大小。
|
|
49
|
+
* 悬停看名称与数值;读屏的数据表列出各区域、地点与流向。
|
|
44
50
|
*/
|
|
45
51
|
/** 一个区域的数据 */
|
|
46
52
|
/** 一个地点 */
|
|
47
53
|
/** 两个地点之间的流向 */
|
|
48
54
|
/** 投影的预设 */
|
|
49
|
-
/** d3 的投影(TopoJSONMap 的 projection) */
|
|
50
55
|
/** 未声明的属性落到外层 div(role="figure")上;aria-label 是图表的名称 */
|
|
51
56
|
/** MapChart 的样式 */
|
|
52
57
|
var mapChartStyles = css.create({
|
|
53
|
-
root: {
|
|
54
|
-
"--vis-map-feature-color": colors.fill,
|
|
55
|
-
"--vis-map-boundary-color": colors.surface,
|
|
56
|
-
"--vis-map-point-label-text-color-dark": colors.text,
|
|
57
|
-
"--vis-map-point-label-text-color-light": colors.text,
|
|
58
|
-
"--vis-map-area-label-text-color": colors.text,
|
|
59
|
-
"--vis-map-area-label-font-size": "10px",
|
|
60
|
-
"--vis-map-area-label-font-weight": "400",
|
|
61
|
-
"--vis-map-point-default-stroke-color": colors.surface,
|
|
62
|
-
"--vis-map-point-default-stroke-width": "1.5px"
|
|
63
|
-
},
|
|
64
58
|
skeleton: {
|
|
65
59
|
display: "block",
|
|
66
60
|
width: "100%",
|
|
67
61
|
height: "100%"
|
|
68
62
|
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
viewport: {
|
|
64
|
+
position: "absolute",
|
|
65
|
+
inset: 0
|
|
66
|
+
},
|
|
67
|
+
feature: {
|
|
68
|
+
stroke: "_surface",
|
|
69
|
+
strokeWidth: .8,
|
|
70
|
+
fillRule: "evenodd",
|
|
71
|
+
transition: `fill ${t.durations.md} ${t.easings.standard}`
|
|
72
|
+
},
|
|
73
|
+
featureHover: {
|
|
74
|
+
fill: "none",
|
|
75
|
+
stroke: "_text",
|
|
76
|
+
strokeWidth: 1.5,
|
|
75
77
|
pointerEvents: "none"
|
|
76
78
|
},
|
|
77
|
-
|
|
79
|
+
sphere: {
|
|
80
|
+
fill: "_surfaceMuted",
|
|
81
|
+
stroke: "_border",
|
|
82
|
+
strokeWidth: 1
|
|
83
|
+
},
|
|
84
|
+
link: {
|
|
85
|
+
fill: "none",
|
|
86
|
+
strokeOpacity: .7,
|
|
87
|
+
strokeLinecap: "round",
|
|
88
|
+
pointerEvents: "none"
|
|
89
|
+
},
|
|
90
|
+
particle: { pointerEvents: "none" },
|
|
91
|
+
point: {
|
|
92
|
+
stroke: "_surface",
|
|
93
|
+
strokeWidth: 1.5
|
|
94
|
+
},
|
|
95
|
+
clickable: { cursor: "pointer" },
|
|
96
|
+
pointLabel: {
|
|
97
|
+
fill: "_text",
|
|
98
|
+
fontSize: "11px",
|
|
99
|
+
fontWeight: "_semibold"
|
|
100
|
+
},
|
|
101
|
+
areaLabel: {
|
|
102
|
+
fill: "_text",
|
|
103
|
+
fontSize: "10px"
|
|
104
|
+
},
|
|
105
|
+
leader: {
|
|
106
|
+
stroke: "_textMuted",
|
|
107
|
+
strokeWidth: 1
|
|
108
|
+
},
|
|
78
109
|
zoom: {
|
|
79
110
|
display: "inline-flex",
|
|
80
111
|
alignItems: "center",
|
|
@@ -116,81 +147,30 @@ var mapChartStyles = css.create({
|
|
|
116
147
|
},
|
|
117
148
|
swatchColor: (color) => ({ backgroundColor: color })
|
|
118
149
|
});
|
|
119
|
-
/**
|
|
120
|
-
var
|
|
121
|
-
|
|
122
|
-
var svgElement = (name) => document.createElementNS("http://www.w3.org/2000/svg", name);
|
|
150
|
+
/** 地点名字的字号、区域名的字号(px) */
|
|
151
|
+
var POINT_FONT = 11;
|
|
152
|
+
var AREA_FONT = 10;
|
|
123
153
|
/** 混色:基色占 percent,其余透明(落在底色上) */
|
|
124
154
|
var tint = (color, percent) => `color-mix(in srgb, ${color} ${Math.round(percent)}%, transparent)`;
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
* 每次更新就不必把整份 TopoJSON 再拷一遍
|
|
130
|
-
*/
|
|
131
|
-
var SharedTopology = class {};
|
|
132
|
-
/** 简化过的地理数据:TopoJSON → 档位 → 结果 */
|
|
133
|
-
var simplified = /* @__PURE__ */ new WeakMap();
|
|
134
|
-
/**
|
|
135
|
-
* 简化地理数据:去掉与前后两点围成的三角形面积不到 minArea(坐标单位的平方,经纬度是平方度)的点
|
|
136
|
-
* (Visvalingam,topojson-simplify)。相邻区域共用的边界一起简化,不会出现缝隙;各段弧的两端总保留。
|
|
137
|
-
* minArea 按 2 的整数次幂分档缓存:绘图区大小变一点不重算。
|
|
138
|
-
*/
|
|
139
|
-
function simplifyTopology(topology, minArea) {
|
|
140
|
-
if (!(minArea > 0) || !Number.isFinite(minArea)) return topology;
|
|
141
|
-
const level = 2 ** Math.floor(Math.log2(minArea));
|
|
142
|
-
let byLevel = simplified.get(topology);
|
|
143
|
-
if (!byLevel) simplified.set(topology, byLevel = /* @__PURE__ */ new Map());
|
|
144
|
-
let result = byLevel.get(level);
|
|
145
|
-
if (!result) {
|
|
146
|
-
let weighted = presimplified.get(topology);
|
|
147
|
-
if (!weighted) presimplified.set(topology, weighted = presimplify(topology));
|
|
148
|
-
result = Object.assign(new SharedTopology(), simplify(weighted, level));
|
|
149
|
-
byLevel.set(level, result);
|
|
150
|
-
}
|
|
151
|
-
return result;
|
|
155
|
+
/** 一组环写成路径;坐标保留一位小数(像素) */
|
|
156
|
+
function ringsPath(rings) {
|
|
157
|
+
const round = (value) => Math.round(value * 10) / 10;
|
|
158
|
+
return rings.map((ring) => `M${ring.map(([x, y]) => `${round(x)},${round(y)}`).join("L")}Z`).join("");
|
|
152
159
|
}
|
|
153
|
-
/**
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
south,
|
|
160
|
-
east,
|
|
161
|
-
north
|
|
162
|
-
];
|
|
160
|
+
/** 地点的标记:中心在 (x, y)、半径 r 的路径 */
|
|
161
|
+
function markerPath(shape, x, y, r) {
|
|
162
|
+
const [cx, cy] = [svgNumber(x), svgNumber(y)];
|
|
163
|
+
if (shape === "square") {
|
|
164
|
+
const half = svgNumber(r * .9);
|
|
165
|
+
return `M${cx - half},${cy - half}h${half * 2}v${half * 2}h${-half * 2}Z`;
|
|
163
166
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
let [west, south, east, north] = [
|
|
168
|
-
Infinity,
|
|
169
|
-
Infinity,
|
|
170
|
-
-Infinity,
|
|
171
|
-
-Infinity
|
|
172
|
-
];
|
|
173
|
-
for (const arc of topology.arcs) {
|
|
174
|
-
let x = 0;
|
|
175
|
-
let y = 0;
|
|
176
|
-
for (const position of arc) {
|
|
177
|
-
x = quantized ? x + position[0] : position[0];
|
|
178
|
-
y = quantized ? y + position[1] : position[1];
|
|
179
|
-
const longitude = x * scaleX + translateX;
|
|
180
|
-
const latitude = y * scaleY + translateY;
|
|
181
|
-
west = Math.min(west, longitude);
|
|
182
|
-
east = Math.max(east, longitude);
|
|
183
|
-
south = Math.min(south, latitude);
|
|
184
|
-
north = Math.max(north, latitude);
|
|
185
|
-
}
|
|
167
|
+
if (shape === "triangle") {
|
|
168
|
+
const h = svgNumber(r * 1.15);
|
|
169
|
+
return `M${cx},${svgNumber(cy - h)}L${svgNumber(cx + h)},${svgNumber(cy + h * .75)}L${svgNumber(cx - h)},${svgNumber(cy + h * .75)}Z`;
|
|
186
170
|
}
|
|
187
|
-
return
|
|
188
|
-
west,
|
|
189
|
-
south,
|
|
190
|
-
east,
|
|
191
|
-
north
|
|
192
|
-
];
|
|
171
|
+
return `M${svgNumber(cx - r)},${cy}A${r},${r},0,1,1,${svgNumber(cx + r)},${cy}A${r},${r},0,1,1,${svgNumber(cx - r)},${cy}Z`;
|
|
193
172
|
}
|
|
173
|
+
/** 悬停的区域或地点 */
|
|
194
174
|
/** 地图 */
|
|
195
175
|
var MapChart = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
196
176
|
const _cache = [];
|
|
@@ -217,12 +197,15 @@ var MapChart = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
217
197
|
const tooltip = readonlyProp(_props, "tooltip", true);
|
|
218
198
|
const onAreaClick = readonlyProp(_props, "onAreaClick");
|
|
219
199
|
const onPointClick = readonlyProp(_props, "onPointClick");
|
|
220
|
-
const sx = readonlyProp(_props, "sx");
|
|
221
200
|
const rest = restProps(_props, _ctx, true);
|
|
222
201
|
const lang = useLang();
|
|
223
202
|
const format = useFormat();
|
|
203
|
+
const area = usePlotArea();
|
|
204
|
+
const reducedMotion = useReducedMotion();
|
|
205
|
+
const viewport = shallowRef(null);
|
|
206
|
+
let hovered = shallowRef(null);
|
|
224
207
|
const bounds = computed(() => topologyBounds(topology.value));
|
|
225
|
-
const numbers = computed(() => areas.value.flatMap((
|
|
208
|
+
const numbers = computed(() => areas.value.flatMap((item) => item.value === void 0 ? [] : [toNumber(item.value)]));
|
|
226
209
|
const extent = computed(() => numbers.value.length ? [Math.min(...numbers.value), Math.max(...numbers.value)] : null);
|
|
227
210
|
const largest = (values) => Math.max(0, ...values.map((item) => item.value === void 0 ? 0 : toNumber(item.value)));
|
|
228
211
|
const valueText = (value) => value === void 0 ? lang.emptyValue : format.number(value, unit.value ? {
|
|
@@ -230,6 +213,8 @@ var MapChart = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
230
213
|
suffix: unit.value
|
|
231
214
|
} : valueFormat.value);
|
|
232
215
|
const numberText = (value) => format.number(value, valueFormat.value);
|
|
216
|
+
const areaById = computed(() => new Map(areas.value.map((item) => [item.id, item])));
|
|
217
|
+
const pointById = computed(() => new Map(points.value.map((point) => [point.id, point])));
|
|
233
218
|
/** 数值 → 颜色:sequential 从 12% 到 100%;diverging 两头各自从分界往外加深;bins 时按档取 */
|
|
234
219
|
function colorOf(value) {
|
|
235
220
|
const [low, high] = extent.value ?? [0, 0];
|
|
@@ -244,129 +229,155 @@ var MapChart = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
244
229
|
if (bins.value) ratio = Math.min(Math.floor(ratio * bins.value), bins.value - 1) / Math.max(bins.value - 1, 1);
|
|
245
230
|
return tint(resolveColor(color.value, 0), 12 + ratio * 88);
|
|
246
231
|
}
|
|
247
|
-
const areaColor = (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
center.value?.[1],
|
|
266
|
-
...bounds.value
|
|
267
|
-
];
|
|
268
|
-
if (key.some((item, index) => item !== mapKey[index])) {
|
|
269
|
-
if (mapKey.length) {
|
|
270
|
-
map?.destroy();
|
|
271
|
-
generation += 1;
|
|
232
|
+
const areaColor = (item) => !item ? void 0 : item.color ? resolveColor(item.color, 0) : item.value === void 0 ? void 0 : colorOf(toNumber(item.value));
|
|
233
|
+
const pointMax = computed(() => largest(points.value));
|
|
234
|
+
const radiusOf = (point) => point.value === void 0 || pointMax.value === 0 ? 4 : 3 + Math.sqrt(toNumber(point.value) / pointMax.value) * 5;
|
|
235
|
+
let wheelHint = ref(false);
|
|
236
|
+
let hintTimer;
|
|
237
|
+
effect(() => () => clearTimeout(hintTimer));
|
|
238
|
+
const view = usePanZoom({
|
|
239
|
+
viewport: () => viewport.value,
|
|
240
|
+
min: 1,
|
|
241
|
+
max: 12,
|
|
242
|
+
step: 1.5,
|
|
243
|
+
disabled: () => !zoomable.value,
|
|
244
|
+
filter: (event) => {
|
|
245
|
+
if (event instanceof WheelEvent && !event.ctrlKey && !event.metaKey) {
|
|
246
|
+
wheelHint.value = true;
|
|
247
|
+
clearTimeout(hintTimer);
|
|
248
|
+
hintTimer = setTimeout(() => wheelHint.value = false, 1500);
|
|
249
|
+
return false;
|
|
272
250
|
}
|
|
273
|
-
|
|
274
|
-
mapProjection = createProjection();
|
|
251
|
+
return true;
|
|
275
252
|
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const height = plot.value?.clientHeight ?? 0;
|
|
280
|
-
if (!width || !height) return topology.value;
|
|
253
|
+
});
|
|
254
|
+
/** 投影:预设按中心旋转;不写时按范围选 */
|
|
255
|
+
const projector = computed(() => {
|
|
281
256
|
const [west, south, east, north] = bounds.value;
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return links.value.find((link) => link.from === source.id && link.to === target.id);
|
|
290
|
-
};
|
|
291
|
-
let map;
|
|
292
|
-
let plot = shallowRef(null);
|
|
293
|
-
const pointMax = computed(() => largest(points.value));
|
|
294
|
-
const radiusOf = (point) => point.value === void 0 || pointMax.value === 0 ? 4 : 3 + Math.sqrt(toNumber(point.value) / pointMax.value) * 5;
|
|
295
|
-
let labelFrame = 0;
|
|
296
|
-
/** 画完、缩放之后,下一帧重排地点的标签 */
|
|
297
|
-
function scheduleLabels() {
|
|
298
|
-
cancelAnimationFrame(labelFrame);
|
|
299
|
-
labelFrame = requestAnimationFrame(layoutLabels);
|
|
300
|
-
}
|
|
301
|
-
effect(() => () => cancelAnimationFrame(labelFrame));
|
|
257
|
+
const middle = center.value ?? [(west + east) / 2, (south + north) / 2];
|
|
258
|
+
const name = projection.value ?? (east - west > 180 ? "naturalEarth" : "conic");
|
|
259
|
+
return createProjection(name, {
|
|
260
|
+
center: middle,
|
|
261
|
+
bounds: bounds.value
|
|
262
|
+
});
|
|
263
|
+
});
|
|
302
264
|
/**
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
* 按屏幕像素算:地点那一组的坐标乘缩放级别
|
|
265
|
+
* 区域的路径:按绘图区里一像素对应的经纬度简化(三角形不到一平方像素的点去掉;能缩放时留到放大一倍也看不出折角),
|
|
266
|
+
* 投影后按绘图区缩放、居中。地点、连线用同一个换算(toScreen)
|
|
306
267
|
*/
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
268
|
+
const geometry = computed(() => {
|
|
269
|
+
const { width, height: plotHeight } = area;
|
|
270
|
+
const [west, south, east, north] = bounds.value;
|
|
271
|
+
const degreesPerPixel = width && plotHeight ? Math.max((east - west) / width, (north - south) / plotHeight) / (zoomable.value ? 2 : 1) : 0;
|
|
272
|
+
const projected = topologyFeatures(simplifyTopology(topology.value, degreesPerPixel ** 2), feature.value).map((item) => ({
|
|
273
|
+
feature: item,
|
|
274
|
+
rings: item.polygons.flatMap((polygon) => projector.value.polygon(polygon)).filter((ring) => ring.length > 2)
|
|
275
|
+
}));
|
|
276
|
+
let [x0, y0, x1, y1] = [
|
|
277
|
+
Infinity,
|
|
278
|
+
Infinity,
|
|
279
|
+
-Infinity,
|
|
280
|
+
-Infinity
|
|
281
|
+
];
|
|
282
|
+
for (const item of projected) for (const ring of item.rings) for (const [x, y] of ring) {
|
|
283
|
+
x0 = Math.min(x0, x);
|
|
284
|
+
x1 = Math.max(x1, x);
|
|
285
|
+
y0 = Math.min(y0, y);
|
|
286
|
+
y1 = Math.max(y1, y);
|
|
287
|
+
}
|
|
288
|
+
const fits = Number.isFinite(x0) && x1 > x0 && y1 > y0;
|
|
289
|
+
const k = fits ? Math.min((width - 16) / (x1 - x0), (plotHeight - 16) / (y1 - y0)) : 1;
|
|
290
|
+
const tx = fits ? (width - (x1 - x0) * k) / 2 - x0 * k : 0;
|
|
291
|
+
const ty = fits ? (plotHeight - (y1 - y0) * k) / 2 - y0 * k : 0;
|
|
292
|
+
const toScreen = ([x, y]) => [x * k + tx, y * k + ty];
|
|
293
|
+
return {
|
|
294
|
+
toScreen,
|
|
295
|
+
sphere: projector.value.sphere && {
|
|
296
|
+
...projector.value.sphere,
|
|
297
|
+
cx: tx,
|
|
298
|
+
cy: ty,
|
|
299
|
+
r: projector.value.sphere.r * k
|
|
300
|
+
},
|
|
301
|
+
shapes: projected.map((item) => {
|
|
302
|
+
const screen = item.rings.map((ring) => ring.map(toScreen));
|
|
303
|
+
let best;
|
|
304
|
+
for (const ring of screen) {
|
|
305
|
+
const xs = ring.map(([x]) => x);
|
|
306
|
+
const ys = ring.map(([, y]) => y);
|
|
307
|
+
const box = {
|
|
308
|
+
x: Math.min(...xs),
|
|
309
|
+
y: Math.min(...ys),
|
|
310
|
+
width: Math.max(...xs) - Math.min(...xs),
|
|
311
|
+
height: Math.max(...ys) - Math.min(...ys)
|
|
312
|
+
};
|
|
313
|
+
if (!best || box.width * box.height > best.size) best = {
|
|
314
|
+
box,
|
|
315
|
+
size: box.width * box.height
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
id: item.feature.id,
|
|
320
|
+
d: ringsPath(screen),
|
|
321
|
+
box: best?.box
|
|
322
|
+
};
|
|
323
|
+
})
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
/** 缩放后屏幕上的位置:地点、连线、文字不随缩放变大,只跟着移动 */
|
|
327
|
+
const zoomed = ([x, y]) => [x * view.scale + view.x, y * view.scale + view.y];
|
|
328
|
+
/** 地点在屏幕上的位置;看不见(地球背面)的不画 */
|
|
329
|
+
const placedPoints = computed(() => points.value.flatMap((point, index) => {
|
|
330
|
+
const projected = projector.value.point([point.longitude, point.latitude]);
|
|
331
|
+
if (!projected) return [];
|
|
332
|
+
const [x, y] = zoomed(geometry.value.toScreen(projected));
|
|
333
|
+
return [{
|
|
334
|
+
point,
|
|
335
|
+
index,
|
|
336
|
+
x,
|
|
337
|
+
y,
|
|
338
|
+
r: radiusOf(point)
|
|
339
|
+
}];
|
|
340
|
+
}));
|
|
341
|
+
/** 地点的名字:placeLabels 挪到放得下的方位(挤时画引线);压住名字、地点的区域名藏起来 */
|
|
342
|
+
const labels = computed(() => {
|
|
343
|
+
const anchors = placedPoints.value.map((item) => ({
|
|
344
|
+
x: item.x,
|
|
345
|
+
y: item.y,
|
|
346
|
+
radius: item.r,
|
|
347
|
+
width: estimateTextWidth(item.point.label, POINT_FONT) + 6,
|
|
348
|
+
height: 19.75,
|
|
349
|
+
priority: item.point.value === void 0 ? 0 : toNumber(item.point.value)
|
|
350
|
+
}));
|
|
351
|
+
const areaBoxes = areaLabels.value ? geometry.value.shapes.flatMap((shape) => {
|
|
352
|
+
const info = areaById.value.get(shape.id);
|
|
353
|
+
if (!info || !shape.box) return [];
|
|
354
|
+
const width = estimateTextWidth(info.label, AREA_FONT);
|
|
355
|
+
const box = {
|
|
356
|
+
x: shape.box.x * view.scale + view.x,
|
|
357
|
+
y: shape.box.y * view.scale + view.y,
|
|
358
|
+
width: shape.box.width * view.scale,
|
|
359
|
+
height: shape.box.height * view.scale
|
|
352
360
|
};
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
x:
|
|
356
|
-
y:
|
|
357
|
-
width
|
|
358
|
-
height:
|
|
361
|
+
if (box.width < width + 4 || box.height < 14) return [];
|
|
362
|
+
const rect = {
|
|
363
|
+
x: box.x + box.width / 2 - width / 2,
|
|
364
|
+
y: box.y + box.height / 2 - AREA_FONT / 2,
|
|
365
|
+
width,
|
|
366
|
+
height: AREA_FONT
|
|
359
367
|
};
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
height: svg.clientHeight
|
|
366
|
-
} : void 0;
|
|
368
|
+
return [{
|
|
369
|
+
label: info.label,
|
|
370
|
+
rect
|
|
371
|
+
}];
|
|
372
|
+
}) : [];
|
|
367
373
|
const placements = placeLabels(anchors, {
|
|
368
|
-
bounds
|
|
369
|
-
|
|
374
|
+
bounds: view.scale === 1 ? {
|
|
375
|
+
x: 0,
|
|
376
|
+
y: 0,
|
|
377
|
+
width: area.width,
|
|
378
|
+
height: area.height
|
|
379
|
+
} : void 0,
|
|
380
|
+
soft: areaBoxes.map((item) => item.rect)
|
|
370
381
|
});
|
|
371
382
|
const taken = anchors.map((anchor) => ({
|
|
372
383
|
x: anchor.x - anchor.radius,
|
|
@@ -374,128 +385,191 @@ var MapChart = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
374
385
|
width: anchor.radius * 2,
|
|
375
386
|
height: anchor.radius * 2
|
|
376
387
|
}));
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
taken.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
388
|
+
for (const placement of placements) if (placement) taken.push(placement.box);
|
|
389
|
+
return {
|
|
390
|
+
points: placements.map((placement, index) => ({
|
|
391
|
+
placement,
|
|
392
|
+
anchor: anchors[index],
|
|
393
|
+
item: placedPoints.value[index]
|
|
394
|
+
})),
|
|
395
|
+
areas: areaBoxes.filter((item) => !taken.some((rect) => rectsOverlap(rect, item.rect)))
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
/** 连线:两个地点之间往上拱起的弧,粗细按数值 */
|
|
399
|
+
const linkShapes = computed(() => {
|
|
400
|
+
const linkMax = largest(links.value);
|
|
401
|
+
const byId = new Map(placedPoints.value.map((item) => [item.point.id, item]));
|
|
402
|
+
return links.value.flatMap((link, index) => {
|
|
403
|
+
const from = byId.get(link.from);
|
|
404
|
+
const to = byId.get(link.to);
|
|
405
|
+
if (!from || !to) return [];
|
|
406
|
+
const dx = to.x - from.x;
|
|
407
|
+
const dy = to.y - from.y;
|
|
408
|
+
const length = Math.hypot(dx, dy) || 1;
|
|
409
|
+
const [nx, ny] = dx >= 0 ? [dy / length, -dx / length] : [-dy / length, dx / length];
|
|
410
|
+
const bend = length * .2;
|
|
411
|
+
const cx = (from.x + to.x) / 2 + nx * bend;
|
|
412
|
+
const cy = (from.y + to.y) / 2 + ny * bend;
|
|
413
|
+
return [{
|
|
414
|
+
link,
|
|
415
|
+
index,
|
|
416
|
+
length,
|
|
417
|
+
d: `M${svgNumber(from.x)},${svgNumber(from.y)}Q${svgNumber(cx)},${svgNumber(cy)},${svgNumber(to.x)},${svgNumber(to.y)}`,
|
|
418
|
+
width: link.value === void 0 || linkMax === 0 ? 1.5 : 1 + toNumber(link.value) / linkMax * 4
|
|
419
|
+
}];
|
|
391
420
|
});
|
|
421
|
+
});
|
|
422
|
+
function onPointermove(event) {
|
|
423
|
+
const target = event.target.closest?.("[data-feature],[data-point]");
|
|
424
|
+
if (!target) {
|
|
425
|
+
hovered.value = null;
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const { x, y } = pointerIn(event, viewport.value ?? event.currentTarget);
|
|
429
|
+
const point = target.getAttribute("data-point");
|
|
430
|
+
hovered.value = point === null ? {
|
|
431
|
+
kind: "feature",
|
|
432
|
+
index: Number(target.getAttribute("data-feature")),
|
|
433
|
+
x,
|
|
434
|
+
y
|
|
435
|
+
} : {
|
|
436
|
+
kind: "point",
|
|
437
|
+
index: Number(point),
|
|
438
|
+
x,
|
|
439
|
+
y
|
|
440
|
+
};
|
|
392
441
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
line.style.strokeWidth = `${1 / zoom}px`;
|
|
405
|
-
return line;
|
|
442
|
+
function onClick(event) {
|
|
443
|
+
const target = event.target.closest?.("[data-feature],[data-point]");
|
|
444
|
+
if (!target) return;
|
|
445
|
+
const point = target.getAttribute("data-point");
|
|
446
|
+
if (point !== null) {
|
|
447
|
+
onPointClick.value?.(points.value[Number(point)]);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const shape = geometry.value.shapes[Number(target.getAttribute("data-feature"))];
|
|
451
|
+
const info = shape && areaById.value.get(shape.id);
|
|
452
|
+
if (info) onAreaClick.value?.(info);
|
|
406
453
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
454
|
+
function renderPlot() {
|
|
455
|
+
const { width, height: plotHeight } = area;
|
|
456
|
+
const { shapes, sphere } = geometry.value;
|
|
457
|
+
const current = hovered.value;
|
|
458
|
+
const hoveredShape = current?.kind === "feature" ? shapes[current.index] : void 0;
|
|
459
|
+
return createVNode("svg", mergeJsxProps({
|
|
460
|
+
"width": width,
|
|
461
|
+
"height": plotHeight
|
|
462
|
+
}, css.attrs(chartSvgStyles.svg), { "onClick": onClick }), [
|
|
463
|
+
createVNode("g", mergeJsxProps({
|
|
464
|
+
"data-part": "features",
|
|
465
|
+
"transform": `translate(${svgNumber(view.x)} ${svgNumber(view.y)}) scale(${svgNumber(view.scale)})`
|
|
466
|
+
}, css.attrs(chartSvgStyles.fadeIn)), [
|
|
467
|
+
sphere && createVNode("circle", mergeJsxProps({
|
|
468
|
+
"cx": sphere.cx,
|
|
469
|
+
"cy": sphere.cy,
|
|
470
|
+
"r": sphere.r
|
|
471
|
+
}, css.attrs(mapChartStyles.sphere)), null),
|
|
472
|
+
shapes.map((shape, index) => {
|
|
473
|
+
const info = areaById.value.get(shape.id);
|
|
474
|
+
return createVNode("path", mergeJsxProps({
|
|
475
|
+
"key": `${shape.id}:${index}`,
|
|
476
|
+
"data-part": "feature",
|
|
477
|
+
"data-feature": index,
|
|
478
|
+
"d": shape.d,
|
|
479
|
+
"vector-effect": "non-scaling-stroke"
|
|
480
|
+
}, css.attrs(mapChartStyles.feature, chartSvgStyles.fill(areaColor(info) ?? colors.fill), info && onAreaClick.value && mapChartStyles.clickable)), null);
|
|
481
|
+
}),
|
|
482
|
+
hoveredShape && createVNode("path", mergeJsxProps({
|
|
483
|
+
"d": hoveredShape.d,
|
|
484
|
+
"vector-effect": "non-scaling-stroke"
|
|
485
|
+
}, css.attrs(mapChartStyles.featureHover)), null)
|
|
486
|
+
]),
|
|
487
|
+
createVNode("g", { "data-part": "links" }, [linkShapes.value.map((shape) => {
|
|
488
|
+
const tone = resolveColor(shape.link.color ?? "neutral", 0);
|
|
489
|
+
return createVNode("path", mergeJsxProps({
|
|
490
|
+
"key": shape.index,
|
|
491
|
+
"data-part": "link",
|
|
492
|
+
"d": shape.d,
|
|
493
|
+
"stroke-width": svgNumber(shape.width)
|
|
494
|
+
}, css.attrs(mapChartStyles.link, chartSvgStyles.stroke(tone))), null);
|
|
495
|
+
}), flow.value && !reducedMotion.value && linkShapes.value.flatMap((shape) => {
|
|
496
|
+
const count = Math.max(1, Math.round(shape.length / 120));
|
|
497
|
+
const duration = Math.max(1.5, shape.length / 60);
|
|
498
|
+
const tone = resolveColor(shape.link.color ?? "accent", 0);
|
|
499
|
+
return Array.from({ length: count }, (_, k) => createVNode("circle", mergeJsxProps({
|
|
500
|
+
"key": `${shape.index}:${k}`,
|
|
501
|
+
"r": 2
|
|
502
|
+
}, css.attrs(mapChartStyles.particle, chartSvgStyles.fill(tone))), [createVNode("animateMotion", mergeJsxProps({
|
|
503
|
+
"dur": `${svgNumber(duration)}s`,
|
|
504
|
+
"begin": `${svgNumber(-(duration * k) / count)}s`,
|
|
505
|
+
"repeatCount": "indefinite"
|
|
506
|
+
}, { path: shape.d }), null)]));
|
|
507
|
+
})]),
|
|
508
|
+
createVNode("g", { "data-part": "points" }, [placedPoints.value.map((item) => createVNode("path", mergeJsxProps({
|
|
509
|
+
"key": item.point.id,
|
|
510
|
+
"data-part": "point",
|
|
511
|
+
"data-point": item.index,
|
|
512
|
+
"d": markerPath(item.point.shape, item.x, item.y, item.r)
|
|
513
|
+
}, css.attrs(mapChartStyles.point, chartSvgStyles.fill(resolveColor(item.point.color ?? "danger", 0)), onPointClick.value && mapChartStyles.clickable)), null))]),
|
|
514
|
+
createVNode("g", mergeJsxProps({ "data-part": "area-labels" }, css.attrs(chartSvgStyles.noPointer)), [labels.value.areas.map((item) => createVNode("text", mergeJsxProps({
|
|
515
|
+
"key": item.label,
|
|
516
|
+
"x": svgNumber(item.rect.x + item.rect.width / 2),
|
|
517
|
+
"y": svgNumber(item.rect.y + item.rect.height / 2),
|
|
518
|
+
"text-anchor": "middle",
|
|
519
|
+
"dominant-baseline": "central"
|
|
520
|
+
}, css.attrs(mapChartStyles.areaLabel, chartSvgStyles.halo)), [item.label]))]),
|
|
521
|
+
createVNode("g", mergeJsxProps({ "data-part": "point-labels" }, css.attrs(chartSvgStyles.noPointer)), [labels.value.points.map(({ placement, anchor, item }) => {
|
|
522
|
+
if (!placement) return null;
|
|
523
|
+
const { box, leader } = placement;
|
|
524
|
+
const toX = Math.min(Math.max(anchor.x, box.x), box.x + box.width);
|
|
525
|
+
const toY = Math.min(Math.max(anchor.y, box.y), box.y + box.height);
|
|
526
|
+
const length = Math.hypot(toX - anchor.x, toY - anchor.y) || 1;
|
|
527
|
+
return createVNode("g", { "key": item.point.id }, [leader && createVNode("line", mergeJsxProps({
|
|
528
|
+
"x1": svgNumber(anchor.x + (toX - anchor.x) / length * anchor.radius),
|
|
529
|
+
"y1": svgNumber(anchor.y + (toY - anchor.y) / length * anchor.radius),
|
|
530
|
+
"x2": svgNumber(toX),
|
|
531
|
+
"y2": svgNumber(toY)
|
|
532
|
+
}, css.attrs(mapChartStyles.leader)), null), createVNode("text", mergeJsxProps({
|
|
533
|
+
"x": svgNumber(box.x + box.width / 2),
|
|
534
|
+
"y": svgNumber(box.y + box.height / 2),
|
|
535
|
+
"text-anchor": "middle",
|
|
536
|
+
"dominant-baseline": "central"
|
|
537
|
+
}, css.attrs(mapChartStyles.pointLabel, chartSvgStyles.halo)), [item.point.label])]);
|
|
538
|
+
})])
|
|
539
|
+
]);
|
|
540
|
+
}
|
|
541
|
+
const tip = () => {
|
|
542
|
+
const current = hovered.value;
|
|
543
|
+
if (!tooltip.value || !current) return null;
|
|
544
|
+
if (current.kind === "point") {
|
|
545
|
+
const point = points.value[current.index];
|
|
546
|
+
if (!point) return null;
|
|
473
547
|
return {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
triggers: {
|
|
482
|
-
[TopoJSONMap.selectors.feature]: (shape) => shape.data ? chart.tooltip(chartTooltip(shape.data.label, [{
|
|
483
|
-
label: lang.chartValue,
|
|
484
|
-
color: areaColor(shape.data) ?? "transparent",
|
|
485
|
-
value: valueText(shape.data.value)
|
|
486
|
-
}])) : void 0,
|
|
487
|
-
[TopoJSONMap.selectors.point]: (point) => chart.tooltip(chartTooltip(point.label, [{
|
|
488
|
-
label: lang.chartValue,
|
|
489
|
-
color: resolveColor(point.color ?? "danger", 0),
|
|
490
|
-
value: valueText(point.value)
|
|
491
|
-
}]))
|
|
492
|
-
}
|
|
548
|
+
x: current.x,
|
|
549
|
+
y: current.y,
|
|
550
|
+
content: chartTooltip(point.label, [{
|
|
551
|
+
label: lang.chartValue,
|
|
552
|
+
color: resolveColor(point.color ?? "danger", 0),
|
|
553
|
+
value: valueText(point.value)
|
|
554
|
+
}])
|
|
493
555
|
};
|
|
494
556
|
}
|
|
495
|
-
|
|
557
|
+
const shape = geometry.value.shapes[current.index];
|
|
558
|
+
const info = shape && areaById.value.get(shape.id);
|
|
559
|
+
if (!info) return null;
|
|
560
|
+
return {
|
|
561
|
+
x: current.x,
|
|
562
|
+
y: current.y,
|
|
563
|
+
content: chartTooltip(info.label, [{
|
|
564
|
+
label: lang.chartValue,
|
|
565
|
+
color: areaColor(info) ?? "transparent",
|
|
566
|
+
value: valueText(info.value)
|
|
567
|
+
}])
|
|
568
|
+
};
|
|
569
|
+
};
|
|
496
570
|
/** 图例:分档时每档一个色块与范围,连续时一条色带两端写最小最大 */
|
|
497
571
|
function legend() {
|
|
498
|
-
if (!extent.value || areas.value.every((
|
|
572
|
+
if (!extent.value || areas.value.every((item) => item.color)) return null;
|
|
499
573
|
const [low, high] = extent.value;
|
|
500
574
|
if (bins.value) {
|
|
501
575
|
const step = (high - low) / bins.value;
|
|
@@ -521,32 +595,22 @@ var MapChart = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
521
595
|
createVNode("span", null, [numberText(high)])
|
|
522
596
|
]);
|
|
523
597
|
}
|
|
524
|
-
let wheelHint = ref(false);
|
|
525
|
-
let hintTimer;
|
|
526
|
-
useEventListener(() => zoomable.value ? plot.value : null, "wheel", (event) => {
|
|
527
|
-
if (event.ctrlKey || event.metaKey) return;
|
|
528
|
-
event.stopPropagation();
|
|
529
|
-
wheelHint.value = true;
|
|
530
|
-
clearTimeout(hintTimer);
|
|
531
|
-
hintTimer = setTimeout(() => wheelHint.value = false, 1500);
|
|
532
|
-
}, { capture: true });
|
|
533
|
-
effect(() => () => clearTimeout(hintTimer));
|
|
534
598
|
/** 缩放按钮:放大、缩小、复位;前面是滚轮的提示 */
|
|
535
599
|
const zoomButtons = () => createVNode("span", css.attrs(mapChartStyles.zoom), [wheelHint.value && createVNode("span", mergeJsxProps(css.attrs(mapChartStyles.wheelHint), { "role": "status" }), [lang.chartZoomWheelHint]), [
|
|
536
600
|
[
|
|
537
601
|
Plus,
|
|
538
602
|
lang.chartZoomIn,
|
|
539
|
-
() =>
|
|
603
|
+
() => view.zoomIn()
|
|
540
604
|
],
|
|
541
605
|
[
|
|
542
606
|
Minus,
|
|
543
607
|
lang.chartZoomOut,
|
|
544
|
-
() =>
|
|
608
|
+
() => view.zoomOut()
|
|
545
609
|
],
|
|
546
610
|
[
|
|
547
611
|
Maximize,
|
|
548
612
|
lang.chartZoomFit,
|
|
549
|
-
() =>
|
|
613
|
+
() => view.reset()
|
|
550
614
|
]
|
|
551
615
|
].map(([icon, label, action]) => createVNode(IconButton, {
|
|
552
616
|
"key": label,
|
|
@@ -558,7 +622,6 @@ var MapChart = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
558
622
|
"onClick": action
|
|
559
623
|
}, null))]);
|
|
560
624
|
return () => createVNode(ChartFrame, mergeJsxProps(rest, {
|
|
561
|
-
"sx": [mapChartStyles.root, sx.value],
|
|
562
625
|
"unit": unit.value,
|
|
563
626
|
"height": height.value,
|
|
564
627
|
"legend": _cache[0] || (_cache[0] = []),
|
|
@@ -573,16 +636,16 @@ var MapChart = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
573
636
|
"table": {
|
|
574
637
|
columns: [lang.chartCategory, unit.value ? `${lang.chartValue}(${unit.value})` : lang.chartValue],
|
|
575
638
|
rows: [
|
|
576
|
-
...areas.value.map((
|
|
577
|
-
header:
|
|
578
|
-
value:
|
|
639
|
+
...areas.value.map((item) => ({
|
|
640
|
+
header: item.label,
|
|
641
|
+
value: item.value
|
|
579
642
|
})),
|
|
580
643
|
...points.value.map((point) => ({
|
|
581
644
|
header: point.label,
|
|
582
645
|
value: point.value
|
|
583
646
|
})),
|
|
584
647
|
...links.value.map((link) => ({
|
|
585
|
-
header: lang.chartFlowLabel(
|
|
648
|
+
header: lang.chartFlowLabel(pointById.value.get(link.from)?.label ?? link.from, pointById.value.get(link.to)?.label ?? link.to),
|
|
586
649
|
value: link.value
|
|
587
650
|
}))
|
|
588
651
|
].map((row) => ({
|
|
@@ -590,7 +653,18 @@ var MapChart = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
590
653
|
cells: [row.value === void 0 ? lang.emptyValue : format.number(row.value, valueFormat.value)]
|
|
591
654
|
}))
|
|
592
655
|
},
|
|
593
|
-
"plotRef":
|
|
656
|
+
"plotRef": area.ref,
|
|
657
|
+
"plot": area.ready ? createVNode("div", mergeJsxProps(css.attrs(mapChartStyles.viewport), zoomable.value ? view.viewportProps : {}, {
|
|
658
|
+
"ref": _cache[3] || (_cache[3] = (element) => viewport.value = element),
|
|
659
|
+
"onPointermove": _cache[4] || (_cache[4] = (event) => {
|
|
660
|
+
if (zoomable.value) view.viewportProps.onPointermove(event);
|
|
661
|
+
onPointermove(event);
|
|
662
|
+
}),
|
|
663
|
+
"onPointerleave": _cache[5] || (_cache[5] = () => hovered.value = null)
|
|
664
|
+
}), [renderPlot(), createVNode(ChartTooltipBox, {
|
|
665
|
+
"tip": tip(),
|
|
666
|
+
"width": area.width
|
|
667
|
+
}, null)]) : null
|
|
594
668
|
}), null);
|
|
595
669
|
}, {
|
|
596
670
|
name: "MapChart",
|
|
@@ -617,11 +691,10 @@ var MapChart = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
617
691
|
"empty",
|
|
618
692
|
"tooltip",
|
|
619
693
|
"onAreaClick",
|
|
620
|
-
"onPointClick"
|
|
621
|
-
"sx"
|
|
694
|
+
"onPointClick"
|
|
622
695
|
]
|
|
623
696
|
});
|
|
624
697
|
//#endregion
|
|
625
|
-
export { MapChart, mapChartStyles
|
|
698
|
+
export { MapChart, mapChartStyles };
|
|
626
699
|
|
|
627
700
|
//# sourceMappingURL=MapChart.js.map
|