@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.
- 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/config/createUi.d.ts +21 -2
- package/dist/config/createUi.js +18 -1
- package/dist/config/createUi.js.map +1 -1
- package/dist/config/theme/preference.js +2 -1
- package/dist/config/theme/preference.js.map +1 -1
- package/dist/index.d.ts +2 -2
- 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/detached.js.map +1 -1
- package/dist/utils/request/use-request.d.ts +25 -1
- package/dist/utils/request/use-request.js +35 -3
- package/dist/utils/request/use-request.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/frame.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import { borderWidths,
|
|
1
|
+
import { borderWidths, t } from "../config/theme/tokens.js";
|
|
2
2
|
import { useLang } from "../config/context.js";
|
|
3
3
|
import { mergeAttrs, mergePartProps } from "../utils/props.js";
|
|
4
|
-
import { themeTransition } from "../utils/motion/animation.js";
|
|
5
4
|
import { visuallyHiddenStyles } from "../base/VisuallyHidden.js";
|
|
6
5
|
import { ButtonBase } from "../base/ButtonBase.js";
|
|
7
6
|
import { IconButton } from "../base/IconButton.js";
|
|
8
|
-
import { useIntersectionObserver } from "../utils/dom/intersection.js";
|
|
9
|
-
import { useEventListener } from "../utils/dom/event-listener.js";
|
|
10
7
|
import { useFormat } from "../config/lang/format.js";
|
|
11
8
|
import { Empty } from "../feedback/Empty.js";
|
|
12
9
|
import { Menu as Menu$1, MenuItem } from "../overlay/Menu.js";
|
|
13
10
|
import { exportChartData, exportChartImage } from "./export.js";
|
|
14
|
-
import { createVNode
|
|
11
|
+
import { createVNode } from "vue";
|
|
15
12
|
import { _component, mergeJsxProps, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
|
|
16
13
|
import { css } from "@zerodep-dev/vue-tsx-pro-tsx/styles";
|
|
17
14
|
import { Download, FileSpreadsheet, ImageDown } from "@lucide/icons";
|
|
@@ -19,7 +16,7 @@ import { Download, FileSpreadsheet, ImageDown } from "@lucide/icons";
|
|
|
19
16
|
/** 图例里的一项 */
|
|
20
17
|
/** 给读屏的数据表:列名与每一行(行标题加各列的文字) */
|
|
21
18
|
/** 各种图表共有的选项与部件属性 */
|
|
22
|
-
/** 图表共用的样式:root
|
|
19
|
+
/** 图表共用的样式:root、header、unit、legend、swatch、plot、tooltip… */
|
|
23
20
|
var chartStyles = css.create({
|
|
24
21
|
root: {
|
|
25
22
|
display: "flex",
|
|
@@ -27,35 +24,7 @@ var chartStyles = css.create({
|
|
|
27
24
|
gap: "_sm",
|
|
28
25
|
width: "100%",
|
|
29
26
|
minWidth: 0,
|
|
30
|
-
fontSize: "_sm"
|
|
31
|
-
"--vis-font-family": fonts.sans,
|
|
32
|
-
"--vis-axis-tick-label-font-size": fontSizes.xs,
|
|
33
|
-
"--vis-axis-tick-label-color": colors.textMuted,
|
|
34
|
-
"--vis-axis-label-color": colors.textMuted,
|
|
35
|
-
"--vis-axis-label-font-size": fontSizes.xs,
|
|
36
|
-
"--vis-axis-label-weight": fontWeights.normal,
|
|
37
|
-
"--vis-axis-grid-color": colors.borderMuted,
|
|
38
|
-
"--vis-axis-domain-color": colors.border,
|
|
39
|
-
"--vis-axis-tick-color": colors.border,
|
|
40
|
-
"--vis-crosshair-line-stroke-color": colors.borderStrong,
|
|
41
|
-
"--vis-crosshair-circle-stroke-color": colors.surface,
|
|
42
|
-
"--vis-tooltip-background-color": colors.surfaceRaised,
|
|
43
|
-
"--vis-tooltip-border-color": colors.borderMuted,
|
|
44
|
-
"--vis-tooltip-text-color": colors.text,
|
|
45
|
-
"--vis-tooltip-border-radius": radii.md,
|
|
46
|
-
"--vis-tooltip-box-shadow": shadows.md,
|
|
47
|
-
"--vis-tooltip-padding": `${space.sm} ${space.md}`,
|
|
48
|
-
"--vis-tooltip-backdrop-filter": "none",
|
|
49
|
-
"--vis-tooltip-transition-duration": durations.sm,
|
|
50
|
-
"--vis-grouped-bar-hover-stroke-width": "0",
|
|
51
|
-
"--vis-donut-background-color": colors.fill,
|
|
52
|
-
"--vis-donut-segment-stroke-width": borderWidths.md,
|
|
53
|
-
"--vis-donut-central-label-text-color": colors.text,
|
|
54
|
-
"--vis-donut-central-label-font-size": fontSizes.xl,
|
|
55
|
-
"--vis-donut-central-sub-label-text-color": colors.textMuted,
|
|
56
|
-
"--vis-donut-central-sub-label-font-size": fontSizes.xs,
|
|
57
|
-
"--vis-plotline-label-font-size": fontSizes.xs,
|
|
58
|
-
"--vis-plotline-label-color": colors.textMuted
|
|
27
|
+
fontSize: "_sm"
|
|
59
28
|
},
|
|
60
29
|
header: {
|
|
61
30
|
display: "flex",
|
|
@@ -134,7 +103,7 @@ var chartStyles = css.create({
|
|
|
134
103
|
}
|
|
135
104
|
});
|
|
136
105
|
/** 提示框的一行 */
|
|
137
|
-
/** 提示框的内容:标题(类目、扇区名)+
|
|
106
|
+
/** 提示框的内容:标题(类目、扇区名)+ 每个系列一行;放进 ChartTooltipBox 跟着指针显示 */
|
|
138
107
|
function chartTooltip(title, items) {
|
|
139
108
|
return createVNode("div", css.attrs(chartStyles.tooltip), [title && createVNode("div", css.attrs(chartStyles.tooltipTitle), [title]), items.map((item) => createVNode("div", mergeJsxProps({ "key": item.label }, css.attrs(chartStyles.tooltipRow)), [
|
|
140
109
|
createVNode("span", css.attrs(chartStyles.swatch, chartStyles.swatchColor(item.color)), null),
|
|
@@ -158,68 +127,6 @@ function useChartValueText(read) {
|
|
|
158
127
|
} : valueFormat);
|
|
159
128
|
};
|
|
160
129
|
}
|
|
161
|
-
/** 绘图区离视口多近就算看见了(px):滚到之前先画好,进来时已经是画好的 */
|
|
162
|
-
var SEEN_MARGIN = 200;
|
|
163
|
-
/**
|
|
164
|
-
* 绘图区进入过视口:之前不画,进来之后一直算看见(滚出去不拆掉)。一页很多图表时先画看得见的,
|
|
165
|
-
* 其余的滚到附近再画(地图、网络图绘制很重)。在 setup 里调用,绘制的 effect 里读返回的函数。
|
|
166
|
-
* 挂上时就在视口附近的立即算看见(不等观察器的第一次回调;没有布局的测试环境里框都在原点,也算看见);
|
|
167
|
-
* 打印前全部画上;没有 IntersectionObserver 的环境当作看见了。
|
|
168
|
-
*/
|
|
169
|
-
function usePlotSeen(plot) {
|
|
170
|
-
const seen = shallowRef(typeof IntersectionObserver === "undefined");
|
|
171
|
-
const near = (element) => {
|
|
172
|
-
const box = element.getBoundingClientRect();
|
|
173
|
-
return box.top < window.innerHeight + SEEN_MARGIN && box.bottom > -200 && box.left < window.innerWidth + SEEN_MARGIN && box.right > -200;
|
|
174
|
-
};
|
|
175
|
-
watch(plot, (element) => {
|
|
176
|
-
if (element && !seen.value && near(element)) seen.value = true;
|
|
177
|
-
}, {
|
|
178
|
-
immediate: true,
|
|
179
|
-
flush: "post"
|
|
180
|
-
});
|
|
181
|
-
useIntersectionObserver(() => seen.value ? null : plot(), (entries) => {
|
|
182
|
-
if (entries.some((entry) => entry.isIntersecting)) seen.value = true;
|
|
183
|
-
}, () => ({ rootMargin: `${SEEN_MARGIN}px` }));
|
|
184
|
-
useEventListener(() => window, "beforeprint", () => seen.value = true);
|
|
185
|
-
return () => seen.value;
|
|
186
|
-
}
|
|
187
|
-
/** 主题的 lg 时长(毫秒),减少动态效果时为 0;读不到(元素不在主题范围内)时不要动画 */
|
|
188
|
-
function chartDuration(element) {
|
|
189
|
-
try {
|
|
190
|
-
return themeTransition(element, { duration: "lg" }).duration ?? 0;
|
|
191
|
-
} catch {
|
|
192
|
-
return 0;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Unovis 每个容器都插一个 id 固定为 saturate 的滤镜(悬停高亮用),一页多张图时 id 重复;
|
|
197
|
-
* 改成每张图自己的 id,并让引用它的 CSS 变量指过去
|
|
198
|
-
*/
|
|
199
|
-
function uniqueHighlightFilter(element, id) {
|
|
200
|
-
const filter = element.querySelector("filter#saturate");
|
|
201
|
-
if (!filter) return;
|
|
202
|
-
filter.id = id;
|
|
203
|
-
element.querySelector("svg")?.style.setProperty("--highlight-filter-id", `url(#${id})`);
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Unovis 实例表:组件、坐标轴、十字线只建一次,之后 setConfig + update(保留实例才有过渡动画)。
|
|
207
|
-
* 容器 destroy 时会连带销毁这些实例:重建容器前 reset,下次取用时重新创建
|
|
208
|
-
*/
|
|
209
|
-
function createChartInstances() {
|
|
210
|
-
let instances = /* @__PURE__ */ new Map();
|
|
211
|
-
return {
|
|
212
|
-
/** 取一个实例:第一次调用时创建 */
|
|
213
|
-
get: (key, create) => {
|
|
214
|
-
if (!instances.has(key)) instances.set(key, create());
|
|
215
|
-
return instances.get(key);
|
|
216
|
-
},
|
|
217
|
-
/** 容器销毁了:清空,之后重新创建 */
|
|
218
|
-
reset() {
|
|
219
|
-
instances = /* @__PURE__ */ new Map();
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
130
|
/** ChartFrame 的属性:未声明的属性落到外层 div(role="figure")上 */
|
|
224
131
|
/** 图表的外层结构 */
|
|
225
132
|
var ChartFrame = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
@@ -355,6 +262,6 @@ var ChartFrame = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
355
262
|
]
|
|
356
263
|
});
|
|
357
264
|
//#endregion
|
|
358
|
-
export { ChartFrame,
|
|
265
|
+
export { ChartFrame, chartStyles, chartTooltip, useChartValueText };
|
|
359
266
|
|
|
360
267
|
//# sourceMappingURL=frame.js.map
|
package/dist/charts/frame.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frame.js","names":["_createVNode","_mergeProps","_vtp_readonlyProp","_vtp_restProps","Menu"],"sources":["../../src/charts/frame.tsx"],"sourcesContent":["/**\n * 各种图表共用的外层结构(XYChart、DonutChart、SankeyChart、ChordChart、FunnelChart、GraphChart):外层 figure、单位与图例、绘图区(加载中的骨架、空状态、Unovis 画布)、\n * 给读屏的数据表;还有提示框的内容、Unovis 实例表与主题变量。\n *\n * <ChartFrame unit=\"万元\" legend={items} hiddenKeys={hidden} onLegendToggle={toggle} loading={loading}\n * isEmpty={rows.length === 0} skeleton={…} table={{ columns, rows }} plotRef={(el) => (plot = el)} />\n *\n * 外观全走主题:Unovis 的样式由 --vis-* 变量控制,这里把它们指到 tokens 上;系列颜色是 CSS 变量,\n * 切换主题、改品牌色都不用重画。绘图区对读屏隐藏,数据由同样内容的表格读出。\n */\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport { css } from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport { shallowRef, watch, type VNodeChild } from 'vue';\nimport { Download, FileSpreadsheet, ImageDown } from '@lucide/icons';\nimport { ButtonBase, type ButtonBaseProps } from '../base/ButtonBase';\nimport { IconButton } from '../base/IconButton';\nimport { Menu, MenuItem } from '../overlay/Menu';\nimport { exportChartData, exportChartImage } from './export';\nimport { visuallyHiddenStyles } from '../base/VisuallyHidden';\nimport { useLang } from '../config/context';\nimport { useFormat, type NumericFormat } from '../config/lang/format';\nimport {\n borderWidths,\n colors,\n durations,\n fonts,\n fontSizes,\n fontWeights,\n radii,\n shadows,\n space,\n t,\n} from '../config/theme/tokens';\nimport { Empty } from '../feedback/Empty';\nimport type { NumericValue } from '../utils/data/numeric';\nimport { themeTransition } from '../utils/motion/animation';\nimport { useEventListener } from '../utils/dom/event-listener';\nimport { useIntersectionObserver } from '../utils/dom/intersection';\nimport { mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\n\n/** 图例里的一项 */\nexport interface ChartLegendItem {\n /** 系列(或扇区)的 key */\n key: string;\n /** 显示的名字 */\n label: string;\n /** CSS 颜色 */\n color: string;\n}\n\n/** 给读屏的数据表:列名与每一行(行标题加各列的文字) */\nexport interface ChartTable {\n /** 列名:第一列是行标题的列 */\n columns: readonly string[];\n /** 每一行:行标题加其余各列的文字 */\n rows: readonly { header: string; cells: readonly string[] }[];\n}\n\n/** 各种图表共有的选项与部件属性 */\nexport interface ChartFrameParts {\n /** 右上角的导出菜单:绘图区存成 PNG 图片、数据表存成 CSV */\n exportable?: boolean;\n /** 导出的文件名(不含扩展名);不写时用图表的名称(aria-label) */\n exportName?: string;\n /** 图表上方一行(单位与图例)的部件属性 */\n propsHeader?: PartProps<'div'>;\n /** 单位的部件属性 */\n propsUnit?: PartProps<'span'>;\n /** 图例的部件属性 */\n propsLegend?: PartProps<'div', 'role'>;\n /** 图例每一项(按钮)的部件属性 */\n propsLegendItem?: Omit<Partial<ButtonBaseProps>, 'aria-pressed' | 'onClick'>;\n /** 绘图区(Unovis 画在这里)的部件属性 */\n propsPlot?: PartProps<'div', 'aria-hidden'>;\n /** 给读屏的数据表的部件属性 */\n propsTable?: PartProps<'table'>;\n}\n\n/** 图表共用的样式:root(--vis-* 主题变量)、header、unit、legend、swatch、plot、tooltip… */\nexport const chartStyles = css.create({\n root: {\n display: 'flex',\n flexDirection: 'column',\n gap: '_sm',\n // 在居中、弹性布局的容器里也占满一行:绘图区按容器宽度自适应\n width: '100%',\n minWidth: 0,\n fontSize: '_sm',\n // Unovis 的外观变量:坐标轴、网格、十字线、提示框、标签都指到主题 tokens\n '--vis-font-family': fonts.sans,\n '--vis-axis-tick-label-font-size': fontSizes.xs,\n '--vis-axis-tick-label-color': colors.textMuted,\n '--vis-axis-label-color': colors.textMuted,\n '--vis-axis-label-font-size': fontSizes.xs,\n '--vis-axis-label-weight': fontWeights.normal,\n '--vis-axis-grid-color': colors.borderMuted,\n '--vis-axis-domain-color': colors.border,\n '--vis-axis-tick-color': colors.border,\n '--vis-crosshair-line-stroke-color': colors.borderStrong,\n '--vis-crosshair-circle-stroke-color': colors.surface,\n '--vis-tooltip-background-color': colors.surfaceRaised,\n '--vis-tooltip-border-color': colors.borderMuted,\n '--vis-tooltip-text-color': colors.text,\n '--vis-tooltip-border-radius': radii.md,\n '--vis-tooltip-box-shadow': shadows.md,\n '--vis-tooltip-padding': `${space.sm} ${space.md}`,\n '--vis-tooltip-backdrop-filter': 'none',\n '--vis-tooltip-transition-duration': durations.sm,\n '--vis-grouped-bar-hover-stroke-width': '0',\n // 环形图:底色、扇区之间的缝、中间的标签\n '--vis-donut-background-color': colors.fill,\n '--vis-donut-segment-stroke-width': borderWidths.md,\n '--vis-donut-central-label-text-color': colors.text,\n '--vis-donut-central-label-font-size': fontSizes.xl,\n '--vis-donut-central-sub-label-text-color': colors.textMuted,\n '--vis-donut-central-sub-label-font-size': fontSizes.xs,\n // 目标线:虚线,标签是小号的次要文字(颜色由各条线自己给)\n '--vis-plotline-label-font-size': fontSizes.xs,\n '--vis-plotline-label-color': colors.textMuted,\n },\n header: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: '_md',\n minHeight: '1.5em',\n },\n unit: { color: '_textMuted', fontSize: '_xs' },\n legend: {\n display: 'flex',\n flexWrap: 'wrap',\n justifyContent: 'flex-end',\n gap: `${t.space.xs} ${t.space.md}`,\n marginInlineStart: 'auto',\n },\n // 图例项:ButtonBase 给出焦点环与按下的反馈,这里只定文字\n legendItem: {\n display: 'inline-flex',\n alignItems: 'center',\n gap: '_xs',\n color: { default: '_textMuted', ':hover': '_text' },\n fontSize: '_xs',\n borderRadius: '_sm',\n },\n // 隐藏的系列:划线、色块变空心;不降低不透明度,文字仍要满足对比度\n legendItemHidden: { textDecorationLine: 'line-through' },\n swatch: {\n width: '0.625em',\n height: '0.625em',\n borderRadius: '_sm',\n flexShrink: 0,\n },\n swatchColor: (color: string) => ({ backgroundColor: color }),\n swatchHidden: (color: string) => ({\n backgroundColor: 'transparent',\n boxShadow: `inset 0 0 0 ${borderWidths.md} ${color}`,\n }),\n // contain: inline-size:画好的 SVG 写死了宽度,不能让它反过来撑住按内容定宽的外层(表格式的滚动区、\n // 起始对齐的弹性布局);不然容器变窄时图表还是原来那么宽,Unovis 量不到变化也就不重画\n plot: { position: 'relative', minWidth: 0, contain: 'inline-size' },\n // 点了能下钻:手形指针\n plotClickable: { cursor: 'pointer' },\n plotHeight: (height: string) => ({ height }),\n center: { display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' },\n tooltip: { display: 'flex', flexDirection: 'column', gap: '_xs', fontSize: '_xs' },\n tooltipTitle: { fontWeight: '_semibold' },\n tooltipRow: { display: 'flex', alignItems: 'center', gap: '_sm' },\n tooltipLabel: { color: '_textMuted', flexGrow: 1 },\n tooltipValue: {\n fontVariantNumeric: 'tabular-nums',\n fontWeight: '_medium',\n marginInlineStart: '_lg',\n },\n});\n\n/** 提示框的一行 */\nexport interface ChartTooltipItem {\n /** 系列或数值的名字 */\n label: string;\n /** 色块的 CSS 颜色;transparent 时不画色块 */\n color: string;\n /** 格式化好的数值 */\n value: string;\n}\n\n/** 提示框的内容:标题(类目、扇区名)+ 每个系列一行;Unovis 定位,Vue 渲染 */\nexport function chartTooltip(title: string, items: readonly ChartTooltipItem[]) {\n return (\n <div {...css.attrs(chartStyles.tooltip)}>\n {title && <div {...css.attrs(chartStyles.tooltipTitle)}>{title}</div>}\n {items.map((item) => (\n <div key={item.label} {...css.attrs(chartStyles.tooltipRow)}>\n <span {...css.attrs(chartStyles.swatch, chartStyles.swatchColor(item.color))} />\n <span {...css.attrs(chartStyles.tooltipLabel)}>{item.label}</span>\n <span {...css.attrs(chartStyles.tooltipValue)}>{item.value}</span>\n </div>\n ))}\n </div>\n );\n}\n\n/**\n * 图表里数值的文字(提示框、读屏数据表、扇区标签):按 valueFormat 格式化、带上单位;\n * 没有值时是语言包的 emptyValue。setup 里调用,返回的函数在渲染里用\n */\nexport function useChartValueText(read: () => { valueFormat?: NumericFormat; unit?: string }) {\n const lang = useLang();\n const format = useFormat();\n return (value: NumericValue | undefined) => {\n if (value === undefined) return lang.emptyValue;\n const { valueFormat, unit } = read();\n return format.number(value, unit ? { ...valueFormat, suffix: unit } : valueFormat);\n };\n}\n\n/** 绘图区离视口多近就算看见了(px):滚到之前先画好,进来时已经是画好的 */\nconst SEEN_MARGIN = 200;\n\n/**\n * 绘图区进入过视口:之前不画,进来之后一直算看见(滚出去不拆掉)。一页很多图表时先画看得见的,\n * 其余的滚到附近再画(地图、网络图绘制很重)。在 setup 里调用,绘制的 effect 里读返回的函数。\n * 挂上时就在视口附近的立即算看见(不等观察器的第一次回调;没有布局的测试环境里框都在原点,也算看见);\n * 打印前全部画上;没有 IntersectionObserver 的环境当作看见了。\n */\nexport function usePlotSeen(plot: () => HTMLElement | null | undefined): () => boolean {\n const seen = shallowRef(typeof IntersectionObserver === 'undefined');\n const near = (element: Element) => {\n const box = element.getBoundingClientRect();\n return (\n box.top < window.innerHeight + SEEN_MARGIN &&\n box.bottom > -SEEN_MARGIN &&\n box.left < window.innerWidth + SEEN_MARGIN &&\n box.right > -SEEN_MARGIN\n );\n };\n watch(\n plot,\n (element) => {\n if (element && !seen.value && near(element)) seen.value = true;\n },\n { immediate: true, flush: 'post' },\n );\n useIntersectionObserver(\n () => (seen.value ? null : plot()),\n (entries) => {\n if (entries.some((entry) => entry.isIntersecting)) seen.value = true;\n },\n () => ({ rootMargin: `${SEEN_MARGIN}px` }),\n );\n useEventListener(\n () => window,\n 'beforeprint',\n () => (seen.value = true),\n );\n return () => seen.value;\n}\n\n/** 主题的 lg 时长(毫秒),减少动态效果时为 0;读不到(元素不在主题范围内)时不要动画 */\nexport function chartDuration(element: Element): number {\n try {\n return themeTransition(element, { duration: 'lg' }).duration ?? 0;\n } catch {\n return 0;\n }\n}\n\n/**\n * Unovis 每个容器都插一个 id 固定为 saturate 的滤镜(悬停高亮用),一页多张图时 id 重复;\n * 改成每张图自己的 id,并让引用它的 CSS 变量指过去\n */\nexport function uniqueHighlightFilter(element: HTMLElement, id: string): void {\n const filter = element.querySelector<SVGFilterElement>('filter#saturate');\n if (!filter) return;\n filter.id = id;\n element.querySelector('svg')?.style.setProperty('--highlight-filter-id', `url(#${id})`);\n}\n\n/**\n * Unovis 实例表:组件、坐标轴、十字线只建一次,之后 setConfig + update(保留实例才有过渡动画)。\n * 容器 destroy 时会连带销毁这些实例:重建容器前 reset,下次取用时重新创建\n */\nexport function createChartInstances() {\n let instances = new Map<string, unknown>();\n return {\n /** 取一个实例:第一次调用时创建 */\n get: <C,>(key: string, create: () => C): C => {\n if (!instances.has(key)) instances.set(key, create());\n return instances.get(key) as C;\n },\n /** 容器销毁了:清空,之后重新创建 */\n reset() {\n instances = new Map();\n },\n };\n}\n\n/** ChartFrame 的属性:未声明的属性落到外层 div(role=\"figure\")上 */\nexport interface ChartFrameProps extends PartProps<'div', 'role' | 'aria-busy'>, ChartFrameParts {\n /** 单位:显示在左上方 */\n unit?: string;\n /** 第二数值轴的单位:显示在右上方(组合图) */\n unitEnd?: string;\n /** 绘图区的高度 */\n height: number | string;\n /** 图例的各项;空数组时不显示图例 */\n legend: readonly ChartLegendItem[];\n /** 图例里隐藏着的项 */\n hiddenKeys: readonly string[];\n /** 点了图例的一项 */\n onLegendToggle: (key: string) => void;\n /** 加载中:绘图区显示 skeleton,外层带 aria-busy */\n loading: boolean;\n /** 加载中显示的骨架 */\n skeleton: VNodeChild;\n /** 没有数据:绘图区显示空状态,没有数据表 */\n isEmpty: boolean;\n /** 空状态的内容,默认是“暂无数据” */\n empty?: VNodeChild;\n /** 给读屏的数据表 */\n table: ChartTable;\n /** 取得绘图区元素(Unovis 画在这里);加载、空状态之后重新出现时是新的元素 */\n plotRef?: (element: HTMLElement | null) => void;\n /** 绘图区里直接渲染的内容(不用 Unovis 的图,如漏斗图);同样对读屏隐藏 */\n plot?: VNodeChild;\n /** 绘图区下面的内容(缩放的滑块、地图的色带) */\n footer?: VNodeChild;\n /** 上方一行右端的操作(地图的缩放按钮),在导出菜单前面 */\n actions?: VNodeChild;\n /** 点了绘图区(下钻):写了它时指针是手形 */\n onPlotClick?: () => void;\n /** 图里的项能点(手形指针),点击由图自己处理(环形图的扇区、网络图的节点) */\n clickable?: boolean;\n}\n\n/** 图表的外层结构 */\nexport const ChartFrame = _component(\n ({\n unit,\n unitEnd,\n height,\n legend,\n hiddenKeys,\n onLegendToggle,\n loading,\n skeleton,\n isEmpty,\n empty,\n table,\n plotRef,\n footer,\n actions,\n plot,\n onPlotClick,\n clickable = false,\n propsHeader,\n propsUnit,\n propsLegend,\n propsLegendItem,\n propsPlot,\n propsTable,\n exportable = false,\n exportName,\n ...rest\n }: ChartFrameProps) => {\n const lang = useLang();\n const format = useFormat();\n let plotElement: HTMLElement | null = null;\n const fileName = () => exportName ?? rest['aria-label'] ?? lang.chartExportName;\n /** 导出菜单:图片要绘图区里有 SVG(漏斗图这类直接用 HTML 画的没有) */\n const exportMenu = () => (\n <Menu\n trigger={(props) => (\n <IconButton\n {...props}\n icon={Download}\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n aria-label={lang.chartExport}\n />\n )}\n >\n <MenuItem\n icon={ImageDown}\n onAction={() => {\n const svg = plotElement?.querySelector('svg');\n if (svg) void exportChartImage(svg, fileName());\n }}\n >\n {lang.chartExportImage}\n </MenuItem>\n <MenuItem\n icon={FileSpreadsheet}\n onAction={() => exportChartData(table, format, fileName())}\n >\n {lang.chartExportData}\n </MenuItem>\n </Menu>\n );\n return () => {\n const plotHeight = typeof height === 'number' ? `${height}px` : height;\n const body = loading ? (\n skeleton\n ) : isEmpty ? (\n <div {...css.attrs(chartStyles.center)}>\n {empty ?? <Empty size=\"sm\" title={lang.chartEmpty} />}\n </div>\n ) : null;\n return (\n <div\n {...mergeAttrs<'div'>({ sx: chartStyles.root }, rest, {\n role: 'figure',\n 'aria-busy': loading || undefined,\n })}\n >\n {(unit || unitEnd || legend.length > 0 || exportable || actions) && (\n <div {...mergeAttrs<'div'>({ sx: chartStyles.header }, propsHeader)}>\n {unit && (\n <span {...mergeAttrs<'span'>({ sx: chartStyles.unit }, propsUnit)}>{unit}</span>\n )}\n {legend.length > 0 && (\n <div\n {...mergeAttrs<'div'>({ sx: chartStyles.legend }, propsLegend, { role: 'group' })}\n >\n {legend.map((item) => {\n const off = hiddenKeys.includes(item.key);\n return (\n <ButtonBase\n key={item.key}\n {...mergePartProps<ButtonBaseProps>(\n {\n sx: [chartStyles.legendItem, off && chartStyles.legendItemHidden],\n ripple: false,\n },\n propsLegendItem,\n { 'aria-pressed': !off, onClick: () => onLegendToggle(item.key) },\n )}\n >\n <span\n {...css.attrs(\n chartStyles.swatch,\n off\n ? chartStyles.swatchHidden(item.color)\n : chartStyles.swatchColor(item.color),\n )}\n />\n {item.label}\n </ButtonBase>\n );\n })}\n </div>\n )}\n {unitEnd && (\n <span {...mergeAttrs<'span'>({ sx: chartStyles.unit }, propsUnit)}>{unitEnd}</span>\n )}\n {!isEmpty && !loading && actions}\n {exportable && !isEmpty && !loading && exportMenu()}\n </div>\n )}\n {/* key 不同:两种状态不复用同一个 div,否则 Unovis 画进去的 svg 会留在骨架、空状态里 */}\n {body ? (\n <div key=\"state\" {...css.attrs(chartStyles.plot, chartStyles.plotHeight(plotHeight))}>\n {body}\n </div>\n ) : (\n <div\n key=\"plot\"\n {...mergeAttrs<'div'>(\n {\n sx: [\n chartStyles.plot,\n chartStyles.plotHeight(plotHeight),\n (clickable || onPlotClick) && chartStyles.plotClickable,\n ],\n },\n propsPlot,\n {\n 'aria-hidden': true,\n ref: (element: unknown) => {\n plotElement = element instanceof HTMLElement ? element : null;\n plotRef?.(plotElement);\n },\n onClick: onPlotClick,\n },\n )}\n >\n {plot}\n </div>\n )}\n {!body && footer}\n {!isEmpty && (\n <table {...mergeAttrs<'table'>({ sx: visuallyHiddenStyles.hidden }, propsTable)}>\n <thead>\n <tr>\n {table.columns.map((column) => (\n <th key={column} scope=\"col\">\n {column}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {table.rows.map((row, position) => (\n <tr key={position}>\n <th scope=\"row\">{row.header}</th>\n {row.cells.map((cell, column) => (\n <td key={column}>{cell}</td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n )}\n </div>\n );\n };\n },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"frame.js","names":["_createVNode","_mergeProps","_vtp_readonlyProp","_vtp_restProps","Menu"],"sources":["../../src/charts/frame.tsx"],"sourcesContent":["/**\n * 各种图表共用的外层结构:外层 figure、单位与图例、绘图区(加载中的骨架、空状态、图表自己画的 SVG)、\n * 给读屏的数据表、导出菜单;还有提示框的内容与数值的文字。\n *\n * <ChartFrame unit=\"万元\" legend={items} hiddenKeys={hidden} onLegendToggle={toggle} loading={loading}\n * isEmpty={rows.length === 0} skeleton={…} table={{ columns, rows }} plotRef={area.ref} plot={<svg …/>} />\n *\n * 外观全走主题 tokens;系列颜色是 CSS 变量,切换主题、改品牌色都不用重画。\n * 绘图区对读屏隐藏,数据由同样内容的表格读出。\n */\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport { css } from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport type { VNodeChild } from 'vue';\nimport { Download, FileSpreadsheet, ImageDown } from '@lucide/icons';\nimport { ButtonBase, type ButtonBaseProps } from '../base/ButtonBase';\nimport { IconButton } from '../base/IconButton';\nimport { Menu, MenuItem } from '../overlay/Menu';\nimport { exportChartData, exportChartImage } from './export';\nimport { visuallyHiddenStyles } from '../base/VisuallyHidden';\nimport { useLang } from '../config/context';\nimport { useFormat, type NumericFormat } from '../config/lang/format';\nimport { borderWidths, t } from '../config/theme/tokens';\nimport { Empty } from '../feedback/Empty';\nimport type { NumericValue } from '../utils/data/numeric';\nimport { mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\n\n/** 图例里的一项 */\nexport interface ChartLegendItem {\n /** 系列(或扇区)的 key */\n key: string;\n /** 显示的名字 */\n label: string;\n /** CSS 颜色 */\n color: string;\n}\n\n/** 给读屏的数据表:列名与每一行(行标题加各列的文字) */\nexport interface ChartTable {\n /** 列名:第一列是行标题的列 */\n columns: readonly string[];\n /** 每一行:行标题加其余各列的文字 */\n rows: readonly { header: string; cells: readonly string[] }[];\n}\n\n/** 各种图表共有的选项与部件属性 */\nexport interface ChartFrameParts {\n /** 右上角的导出菜单:绘图区存成 PNG 图片、数据表存成 CSV */\n exportable?: boolean;\n /** 导出的文件名(不含扩展名);不写时用图表的名称(aria-label) */\n exportName?: string;\n /** 图表上方一行(单位与图例)的部件属性 */\n propsHeader?: PartProps<'div'>;\n /** 单位的部件属性 */\n propsUnit?: PartProps<'span'>;\n /** 图例的部件属性 */\n propsLegend?: PartProps<'div', 'role'>;\n /** 图例每一项(按钮)的部件属性 */\n propsLegendItem?: Omit<Partial<ButtonBaseProps>, 'aria-pressed' | 'onClick'>;\n /** 绘图区(SVG 画在这里)的部件属性 */\n propsPlot?: PartProps<'div', 'aria-hidden'>;\n /** 给读屏的数据表的部件属性 */\n propsTable?: PartProps<'table'>;\n}\n\n/** 图表共用的样式:root、header、unit、legend、swatch、plot、tooltip… */\nexport const chartStyles = css.create({\n root: {\n display: 'flex',\n flexDirection: 'column',\n gap: '_sm',\n // 在居中、弹性布局的容器里也占满一行:绘图区按容器宽度自适应\n width: '100%',\n minWidth: 0,\n fontSize: '_sm',\n },\n header: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: '_md',\n minHeight: '1.5em',\n },\n unit: { color: '_textMuted', fontSize: '_xs' },\n legend: {\n display: 'flex',\n flexWrap: 'wrap',\n justifyContent: 'flex-end',\n gap: `${t.space.xs} ${t.space.md}`,\n marginInlineStart: 'auto',\n },\n // 图例项:ButtonBase 给出焦点环与按下的反馈,这里只定文字\n legendItem: {\n display: 'inline-flex',\n alignItems: 'center',\n gap: '_xs',\n color: { default: '_textMuted', ':hover': '_text' },\n fontSize: '_xs',\n borderRadius: '_sm',\n },\n // 隐藏的系列:划线、色块变空心;不降低不透明度,文字仍要满足对比度\n legendItemHidden: { textDecorationLine: 'line-through' },\n swatch: {\n width: '0.625em',\n height: '0.625em',\n borderRadius: '_sm',\n flexShrink: 0,\n },\n swatchColor: (color: string) => ({ backgroundColor: color }),\n swatchHidden: (color: string) => ({\n backgroundColor: 'transparent',\n boxShadow: `inset 0 0 0 ${borderWidths.md} ${color}`,\n }),\n // contain: inline-size:画好的 SVG 写死了宽度,不能让它反过来撑住按内容定宽的外层(表格式的滚动区、\n // 起始对齐的弹性布局);不然容器变窄时图表还是原来那么宽,量不到变化也就不重画\n plot: { position: 'relative', minWidth: 0, contain: 'inline-size' },\n // 点了能下钻:手形指针\n plotClickable: { cursor: 'pointer' },\n plotHeight: (height: string) => ({ height }),\n center: { display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' },\n tooltip: { display: 'flex', flexDirection: 'column', gap: '_xs', fontSize: '_xs' },\n tooltipTitle: { fontWeight: '_semibold' },\n tooltipRow: { display: 'flex', alignItems: 'center', gap: '_sm' },\n tooltipLabel: { color: '_textMuted', flexGrow: 1 },\n tooltipValue: {\n fontVariantNumeric: 'tabular-nums',\n fontWeight: '_medium',\n marginInlineStart: '_lg',\n },\n});\n\n/** 提示框的一行 */\nexport interface ChartTooltipItem {\n /** 系列或数值的名字 */\n label: string;\n /** 色块的 CSS 颜色;transparent 时不画色块 */\n color: string;\n /** 格式化好的数值 */\n value: string;\n}\n\n/** 提示框的内容:标题(类目、扇区名)+ 每个系列一行;放进 ChartTooltipBox 跟着指针显示 */\nexport function chartTooltip(title: string, items: readonly ChartTooltipItem[]) {\n return (\n <div {...css.attrs(chartStyles.tooltip)}>\n {title && <div {...css.attrs(chartStyles.tooltipTitle)}>{title}</div>}\n {items.map((item) => (\n <div key={item.label} {...css.attrs(chartStyles.tooltipRow)}>\n <span {...css.attrs(chartStyles.swatch, chartStyles.swatchColor(item.color))} />\n <span {...css.attrs(chartStyles.tooltipLabel)}>{item.label}</span>\n <span {...css.attrs(chartStyles.tooltipValue)}>{item.value}</span>\n </div>\n ))}\n </div>\n );\n}\n\n/**\n * 图表里数值的文字(提示框、读屏数据表、扇区标签):按 valueFormat 格式化、带上单位;\n * 没有值时是语言包的 emptyValue。setup 里调用,返回的函数在渲染里用\n */\nexport function useChartValueText(read: () => { valueFormat?: NumericFormat; unit?: string }) {\n const lang = useLang();\n const format = useFormat();\n return (value: NumericValue | undefined) => {\n if (value === undefined) return lang.emptyValue;\n const { valueFormat, unit } = read();\n return format.number(value, unit ? { ...valueFormat, suffix: unit } : valueFormat);\n };\n}\n\n/** ChartFrame 的属性:未声明的属性落到外层 div(role=\"figure\")上 */\nexport interface ChartFrameProps extends PartProps<'div', 'role' | 'aria-busy'>, ChartFrameParts {\n /** 单位:显示在左上方 */\n unit?: string;\n /** 第二数值轴的单位:显示在右上方(组合图) */\n unitEnd?: string;\n /** 绘图区的高度 */\n height: number | string;\n /** 图例的各项;空数组时不显示图例 */\n legend: readonly ChartLegendItem[];\n /** 图例里隐藏着的项 */\n hiddenKeys: readonly string[];\n /** 点了图例的一项 */\n onLegendToggle: (key: string) => void;\n /** 加载中:绘图区显示 skeleton,外层带 aria-busy */\n loading: boolean;\n /** 加载中显示的骨架 */\n skeleton: VNodeChild;\n /** 没有数据:绘图区显示空状态,没有数据表 */\n isEmpty: boolean;\n /** 空状态的内容,默认是“暂无数据” */\n empty?: VNodeChild;\n /** 给读屏的数据表 */\n table: ChartTable;\n /** 取得绘图区元素(量大小用,见 usePlotArea);加载、空状态之后重新出现时是新的元素 */\n plotRef?: (element: HTMLElement | null) => void;\n /** 绘图区里的内容(图表画的 SVG、提示框,漏斗图的横条);同样对读屏隐藏 */\n plot?: VNodeChild;\n /** 绘图区下面的内容(缩放的滑块、地图的色带) */\n footer?: VNodeChild;\n /** 上方一行右端的操作(地图的缩放按钮),在导出菜单前面 */\n actions?: VNodeChild;\n /** 点了绘图区(下钻):写了它时指针是手形 */\n onPlotClick?: () => void;\n /** 图里的项能点(手形指针),点击由图自己处理(环形图的扇区、网络图的节点) */\n clickable?: boolean;\n}\n\n/** 图表的外层结构 */\nexport const ChartFrame = _component(\n ({\n unit,\n unitEnd,\n height,\n legend,\n hiddenKeys,\n onLegendToggle,\n loading,\n skeleton,\n isEmpty,\n empty,\n table,\n plotRef,\n footer,\n actions,\n plot,\n onPlotClick,\n clickable = false,\n propsHeader,\n propsUnit,\n propsLegend,\n propsLegendItem,\n propsPlot,\n propsTable,\n exportable = false,\n exportName,\n ...rest\n }: ChartFrameProps) => {\n const lang = useLang();\n const format = useFormat();\n let plotElement: HTMLElement | null = null;\n const fileName = () => exportName ?? rest['aria-label'] ?? lang.chartExportName;\n /** 导出菜单:图片要绘图区里有 SVG(漏斗图这类直接用 HTML 画的没有) */\n const exportMenu = () => (\n <Menu\n trigger={(props) => (\n <IconButton\n {...props}\n icon={Download}\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n aria-label={lang.chartExport}\n />\n )}\n >\n <MenuItem\n icon={ImageDown}\n onAction={() => {\n const svg = plotElement?.querySelector('svg');\n if (svg) void exportChartImage(svg, fileName());\n }}\n >\n {lang.chartExportImage}\n </MenuItem>\n <MenuItem\n icon={FileSpreadsheet}\n onAction={() => exportChartData(table, format, fileName())}\n >\n {lang.chartExportData}\n </MenuItem>\n </Menu>\n );\n return () => {\n const plotHeight = typeof height === 'number' ? `${height}px` : height;\n const body = loading ? (\n skeleton\n ) : isEmpty ? (\n <div {...css.attrs(chartStyles.center)}>\n {empty ?? <Empty size=\"sm\" title={lang.chartEmpty} />}\n </div>\n ) : null;\n return (\n <div\n {...mergeAttrs<'div'>({ sx: chartStyles.root }, rest, {\n role: 'figure',\n 'aria-busy': loading || undefined,\n })}\n >\n {(unit || unitEnd || legend.length > 0 || exportable || actions) && (\n <div {...mergeAttrs<'div'>({ sx: chartStyles.header }, propsHeader)}>\n {unit && (\n <span {...mergeAttrs<'span'>({ sx: chartStyles.unit }, propsUnit)}>{unit}</span>\n )}\n {legend.length > 0 && (\n <div\n {...mergeAttrs<'div'>({ sx: chartStyles.legend }, propsLegend, { role: 'group' })}\n >\n {legend.map((item) => {\n const off = hiddenKeys.includes(item.key);\n return (\n <ButtonBase\n key={item.key}\n {...mergePartProps<ButtonBaseProps>(\n {\n sx: [chartStyles.legendItem, off && chartStyles.legendItemHidden],\n ripple: false,\n },\n propsLegendItem,\n { 'aria-pressed': !off, onClick: () => onLegendToggle(item.key) },\n )}\n >\n <span\n {...css.attrs(\n chartStyles.swatch,\n off\n ? chartStyles.swatchHidden(item.color)\n : chartStyles.swatchColor(item.color),\n )}\n />\n {item.label}\n </ButtonBase>\n );\n })}\n </div>\n )}\n {unitEnd && (\n <span {...mergeAttrs<'span'>({ sx: chartStyles.unit }, propsUnit)}>{unitEnd}</span>\n )}\n {!isEmpty && !loading && actions}\n {exportable && !isEmpty && !loading && exportMenu()}\n </div>\n )}\n {/* key 不同:两种状态不复用同一个 div,绘图区重新出现时是新的元素,大小重新量 */}\n {body ? (\n <div key=\"state\" {...css.attrs(chartStyles.plot, chartStyles.plotHeight(plotHeight))}>\n {body}\n </div>\n ) : (\n <div\n key=\"plot\"\n {...mergeAttrs<'div'>(\n {\n sx: [\n chartStyles.plot,\n chartStyles.plotHeight(plotHeight),\n (clickable || onPlotClick) && chartStyles.plotClickable,\n ],\n },\n propsPlot,\n {\n 'aria-hidden': true,\n ref: (element: unknown) => {\n plotElement = element instanceof HTMLElement ? element : null;\n plotRef?.(plotElement);\n },\n onClick: onPlotClick,\n },\n )}\n >\n {plot}\n </div>\n )}\n {!body && footer}\n {!isEmpty && (\n <table {...mergeAttrs<'table'>({ sx: visuallyHiddenStyles.hidden }, propsTable)}>\n <thead>\n <tr>\n {table.columns.map((column) => (\n <th key={column} scope=\"col\">\n {column}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {table.rows.map((row, position) => (\n <tr key={position}>\n <th scope=\"row\">{row.header}</th>\n {row.cells.map((cell, column) => (\n <td key={column}>{cell}</td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n )}\n </div>\n );\n };\n },\n);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 地图投影:经纬度 → 平面坐标(y 向下,同屏幕),以及把多边形裁到能画的范围里。
|
|
3
|
+
* 公式同 d3-geo、d3-geo-projection(Mike Bostock,ISC);这里只做地图用到的几种,球面裁剪写成精简版:
|
|
4
|
+
*
|
|
5
|
+
* - 先旋转到以 center 为中心(经度方向;正射投影连纬度一起转);
|
|
6
|
+
* - 世界地图换了中央经线后,跨过反子午线(中心对面那条经线)的多边形要切开:经度展开成连续的,
|
|
7
|
+
* 在 [-180°, 180°] 的条带里裁剪(平移 ±360° 的副本也裁一遍),绕着极点的环(南极洲)先沿极点闭合;
|
|
8
|
+
* - 正射投影只画朝向观察者的半球:在三维里按平面裁剪,切口沿地平圈补上弧。
|
|
9
|
+
*
|
|
10
|
+
* const project = createProjection('conic', { center: [105, 35], bounds });
|
|
11
|
+
* project.polygon(rings) // 裁剪、投影后的环(坐标单位任意,画的时候再按绘图区缩放)
|
|
12
|
+
* project.point([121.47, 31.23]) // [x, y];在地球背面时为 null
|
|
13
|
+
*/
|
|
14
|
+
/** 内置的投影 */
|
|
15
|
+
export type MapProjectionName = 'conic' | 'mercator' | 'naturalEarth' | 'equalEarth' | 'equirectangular' | 'orthographic';
|
|
16
|
+
/**
|
|
17
|
+
* 自己的投影:[经度, 纬度](度)→ [x, y](y 向下);看不见时返回 null。d3-geo 的投影可以直接传进来
|
|
18
|
+
* (它本身就是这样的函数);自己的投影不做裁剪
|
|
19
|
+
*/
|
|
20
|
+
export type MapGeoProjection = (coordinates: [number, number]) => [number, number] | null;
|
|
21
|
+
/** 平面上的一个点 */
|
|
22
|
+
type Planar = [number, number];
|
|
23
|
+
/** createProjection 的选项 */
|
|
24
|
+
export interface ProjectionOptions {
|
|
25
|
+
/** 投影的中心 [经度, 纬度](度):中央经线;正射投影还用纬度 */
|
|
26
|
+
center: readonly [number, number];
|
|
27
|
+
/** 地理数据的范围 [西, 南, 东, 北]:圆锥投影的两条标准纬线取范围的 1/6 与 5/6 处 */
|
|
28
|
+
bounds: readonly [number, number, number, number];
|
|
29
|
+
}
|
|
30
|
+
/** 投影好的一组工具:多边形、点 */
|
|
31
|
+
export interface Projector {
|
|
32
|
+
/** 一个多边形(几个环,经纬度)裁剪、投影后的环(y 向下);整个看不见时是空数组 */
|
|
33
|
+
polygon(rings: readonly (readonly (readonly [number, number])[])[]): Planar[][];
|
|
34
|
+
/** 一个点;看不见时为 null */
|
|
35
|
+
point(coordinates: readonly [number, number]): Planar | null;
|
|
36
|
+
/** 正射投影:地球的轮廓(圆心与半径,投影坐标);其余投影没有 */
|
|
37
|
+
sphere?: {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
r: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** 按名字(或自己的投影)建投影(见文件开头) */
|
|
44
|
+
export declare function createProjection(projection: MapProjectionName | MapGeoProjection, { center, bounds }: ProjectionOptions): Projector;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
//#region src/charts/geo/projection.ts
|
|
2
|
+
var RADIANS = Math.PI / 180;
|
|
3
|
+
var HALF_PI = Math.PI / 2;
|
|
4
|
+
var TAU = Math.PI * 2;
|
|
5
|
+
/** 墨卡托到这个纬度为止(再往上无穷大):同网页地图的 ±85.05° */
|
|
6
|
+
var MERCATOR_LIMIT = Math.atan(Math.sinh(Math.PI));
|
|
7
|
+
var mercator = (lambda, phi) => {
|
|
8
|
+
const limited = Math.max(-MERCATOR_LIMIT, Math.min(MERCATOR_LIMIT, phi));
|
|
9
|
+
return [lambda, Math.log(Math.tan(Math.PI / 4 + limited / 2))];
|
|
10
|
+
};
|
|
11
|
+
var naturalEarth = (lambda, phi) => {
|
|
12
|
+
const phi2 = phi * phi;
|
|
13
|
+
const phi4 = phi2 * phi2;
|
|
14
|
+
return [lambda * (.8707 - .131979 * phi2 + phi4 * (-.013791 + phi4 * (.003971 * phi2 - .001529 * phi4))), phi * (1.007226 + phi2 * (.015085 + phi4 * (-.044475 + .028874 * phi2 - .005916 * phi4)))];
|
|
15
|
+
};
|
|
16
|
+
var equalEarth = (lambda, phi) => {
|
|
17
|
+
const [a1, a2, a3, a4] = [
|
|
18
|
+
1.340264,
|
|
19
|
+
-.081106,
|
|
20
|
+
893e-6,
|
|
21
|
+
.003796
|
|
22
|
+
];
|
|
23
|
+
const m = Math.sqrt(3) / 2;
|
|
24
|
+
const l = Math.asin(m * Math.sin(phi));
|
|
25
|
+
const l2 = l * l;
|
|
26
|
+
const l6 = l2 * l2 * l2;
|
|
27
|
+
return [lambda * Math.cos(l) / (m * (a1 + 3 * a2 * l2 + l6 * (7 * a3 + 9 * a4 * l2))), l * (a1 + a2 * l2 + l6 * (a3 + a4 * l2))];
|
|
28
|
+
};
|
|
29
|
+
/** 等积圆锥(Albers):两条标准纬线(弧度);两条纬线关于赤道对称时退化成等积圆柱 */
|
|
30
|
+
function conicEqualArea(phi0, phi1) {
|
|
31
|
+
const sy0 = Math.sin(phi0);
|
|
32
|
+
const n = (sy0 + Math.sin(phi1)) / 2;
|
|
33
|
+
if (Math.abs(n) < 1e-6) {
|
|
34
|
+
const cosPhi0 = Math.cos(phi0);
|
|
35
|
+
return (lambda, phi) => [lambda * cosPhi0, Math.sin(phi) / cosPhi0];
|
|
36
|
+
}
|
|
37
|
+
const c = 1 + sy0 * (2 * n - sy0);
|
|
38
|
+
const r0 = Math.sqrt(c) / n;
|
|
39
|
+
return (lambda, phi) => {
|
|
40
|
+
const r = Math.sqrt(Math.max(0, c - 2 * n * Math.sin(phi))) / n;
|
|
41
|
+
return [r * Math.sin(lambda * n), r0 - r * Math.cos(lambda * n)];
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** 经度归到 [-π, π] */
|
|
45
|
+
var wrap = (lambda) => lambda - TAU * Math.round(lambda / TAU);
|
|
46
|
+
var toVector = (lambda, phi) => [
|
|
47
|
+
Math.cos(phi) * Math.cos(lambda),
|
|
48
|
+
Math.cos(phi) * Math.sin(lambda),
|
|
49
|
+
Math.sin(phi)
|
|
50
|
+
];
|
|
51
|
+
/** 绕东西轴转过 -pitch:把纬度 pitch 处转到赤道上(同 d3 的 rotation 的 φ 分量) */
|
|
52
|
+
function tilt([x, y, z], pitch) {
|
|
53
|
+
const cos = Math.cos(pitch);
|
|
54
|
+
const sin = Math.sin(pitch);
|
|
55
|
+
return [
|
|
56
|
+
x * cos + z * sin,
|
|
57
|
+
y,
|
|
58
|
+
z * cos - x * sin
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
/** Sutherland–Hodgman:一个环裁到 keep(点) 为真的一侧,cross 求边与边界的交点 */
|
|
62
|
+
function clipRing(ring, keep, cross) {
|
|
63
|
+
const output = [];
|
|
64
|
+
ring.forEach((current, i) => {
|
|
65
|
+
const previous = ring[(i + ring.length - 1) % ring.length];
|
|
66
|
+
if (keep(current)) {
|
|
67
|
+
if (!keep(previous)) output.push(cross(previous, current));
|
|
68
|
+
output.push(current);
|
|
69
|
+
} else if (keep(previous)) output.push(cross(previous, current));
|
|
70
|
+
});
|
|
71
|
+
return output;
|
|
72
|
+
}
|
|
73
|
+
/** 去掉环末尾与开头重复的点(TopoJSON 的环是闭合的) */
|
|
74
|
+
var open = (ring) => {
|
|
75
|
+
const first = ring[0];
|
|
76
|
+
const last = ring.at(-1);
|
|
77
|
+
return first && last && ring.length > 1 && first[0] === last[0] && first[1] === last[1] ? ring.slice(0, -1) : [...ring];
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* 跨反子午线的环切开(经纬度都是弧度,经度已旋转到 [-π, π]):展开成连续的经度,绕极点的环沿极点闭合,
|
|
81
|
+
* 再在 [-π, π] 的条带里裁剪自己和平移 ±2π 的副本;裁出的边沿着反子午线每隔 2° 补点(投影后是曲线)
|
|
82
|
+
*/
|
|
83
|
+
function cutAntimeridian(ring) {
|
|
84
|
+
const unwrapped = [];
|
|
85
|
+
let offset = 0;
|
|
86
|
+
ring.forEach(([lambda, phi], i) => {
|
|
87
|
+
if (i > 0) {
|
|
88
|
+
const delta = lambda - ring[i - 1][0];
|
|
89
|
+
if (delta > Math.PI) offset -= TAU;
|
|
90
|
+
else if (delta < -Math.PI) offset += TAU;
|
|
91
|
+
}
|
|
92
|
+
unwrapped.push([lambda + offset, phi]);
|
|
93
|
+
});
|
|
94
|
+
if (Math.abs(offset) > Math.PI) {
|
|
95
|
+
const pole = unwrapped.reduce((sum, [, phi]) => sum + phi, 0) < 0 ? -HALF_PI : HALF_PI;
|
|
96
|
+
unwrapped.push([unwrapped.at(-1)[0], pole], [unwrapped[0][0], pole]);
|
|
97
|
+
}
|
|
98
|
+
const lambdas = unwrapped.map(([lambda]) => lambda);
|
|
99
|
+
const [min, max] = [Math.min(...lambdas), Math.max(...lambdas)];
|
|
100
|
+
if (min >= -Math.PI - 1e-9 && max <= Math.PI + 1e-9) return [unwrapped];
|
|
101
|
+
const atEdge = (edge) => (a, b) => [edge, a[1] + (edge - a[0]) / (b[0] - a[0]) * (b[1] - a[1])];
|
|
102
|
+
const pieces = [];
|
|
103
|
+
for (const shift of [
|
|
104
|
+
-TAU,
|
|
105
|
+
0,
|
|
106
|
+
TAU
|
|
107
|
+
]) {
|
|
108
|
+
if (max + shift <= -Math.PI || min + shift >= Math.PI) continue;
|
|
109
|
+
const clipped = clipRing(clipRing(unwrapped.map(([lambda, phi]) => [lambda + shift, phi]), ([lambda]) => lambda <= Math.PI, atEdge(Math.PI)), ([lambda]) => lambda >= -Math.PI, atEdge(-Math.PI));
|
|
110
|
+
if (clipped.length >= 3) pieces.push(densifyEdge(clipped));
|
|
111
|
+
}
|
|
112
|
+
return pieces;
|
|
113
|
+
}
|
|
114
|
+
/** 沿反子午线的边每隔 2° 补一个点 */
|
|
115
|
+
function densifyEdge(ring) {
|
|
116
|
+
const step = 2 * RADIANS;
|
|
117
|
+
const output = [];
|
|
118
|
+
ring.forEach((point, i) => {
|
|
119
|
+
const next = ring[(i + 1) % ring.length];
|
|
120
|
+
output.push(point);
|
|
121
|
+
if (!(Math.abs(Math.abs(point[0]) - Math.PI) < 1e-9 && point[0] === next[0])) return;
|
|
122
|
+
const count = Math.floor(Math.abs(next[1] - point[1]) / step);
|
|
123
|
+
for (let k = 1; k < count; k += 1) output.push([point[0], point[1] + (next[1] - point[1]) * k / count]);
|
|
124
|
+
});
|
|
125
|
+
return output;
|
|
126
|
+
}
|
|
127
|
+
/** 正射投影:环裁到朝向观察者的半球(x ≥ 0),切口沿地平圈补弧;返回投影后的平面坐标(y 向上) */
|
|
128
|
+
function clipHemisphere(ring) {
|
|
129
|
+
const clipped = clipRing(ring, ([x]) => x >= 0, (a, b) => {
|
|
130
|
+
const t = a[0] / (a[0] - b[0]);
|
|
131
|
+
const point = [
|
|
132
|
+
0,
|
|
133
|
+
a[1] + t * (b[1] - a[1]),
|
|
134
|
+
a[2] + t * (b[2] - a[2])
|
|
135
|
+
];
|
|
136
|
+
const length = Math.hypot(point[1], point[2]) || 1;
|
|
137
|
+
return [
|
|
138
|
+
0,
|
|
139
|
+
point[1] / length,
|
|
140
|
+
point[2] / length
|
|
141
|
+
];
|
|
142
|
+
});
|
|
143
|
+
const output = [];
|
|
144
|
+
clipped.forEach((point, i) => {
|
|
145
|
+
output.push([point[1], point[2]]);
|
|
146
|
+
const next = clipped[(i + 1) % clipped.length];
|
|
147
|
+
if (point[0] !== 0 || next[0] !== 0) return;
|
|
148
|
+
const from = Math.atan2(point[2], point[1]);
|
|
149
|
+
let delta = Math.atan2(next[2], next[1]) - from;
|
|
150
|
+
if (delta > Math.PI) delta -= TAU;
|
|
151
|
+
if (delta < -Math.PI) delta += TAU;
|
|
152
|
+
const count = Math.ceil(Math.abs(delta) / (4 * RADIANS));
|
|
153
|
+
for (let k = 1; k < count; k += 1) {
|
|
154
|
+
const angle = from + delta * k / count;
|
|
155
|
+
output.push([Math.cos(angle), Math.sin(angle)]);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return output;
|
|
159
|
+
}
|
|
160
|
+
/** 按名字(或自己的投影)建投影(见文件开头) */
|
|
161
|
+
function createProjection(projection, { center, bounds }) {
|
|
162
|
+
if (typeof projection === "function") return {
|
|
163
|
+
polygon: (rings) => rings.map((ring) => ring.flatMap((point) => {
|
|
164
|
+
const result = projection([point[0], point[1]]);
|
|
165
|
+
return result ? [result] : [];
|
|
166
|
+
})),
|
|
167
|
+
point: (point) => projection([point[0], point[1]])
|
|
168
|
+
};
|
|
169
|
+
const lambda0 = center[0] * RADIANS;
|
|
170
|
+
const flip = ([x, y]) => [x, -y];
|
|
171
|
+
if (projection === "orthographic") {
|
|
172
|
+
const pitch = center[1] * RADIANS;
|
|
173
|
+
const toView = (point) => tilt(toVector(point[0] * RADIANS - lambda0, point[1] * RADIANS), pitch);
|
|
174
|
+
return {
|
|
175
|
+
polygon: (rings) => rings.flatMap((ring) => {
|
|
176
|
+
const clipped = clipHemisphere(open(ring).map(toView));
|
|
177
|
+
return clipped.length >= 3 ? [clipped.map(flip)] : [];
|
|
178
|
+
}),
|
|
179
|
+
point: (point) => {
|
|
180
|
+
const [x, y, z] = toView(point);
|
|
181
|
+
return x < 0 ? null : [y, -z];
|
|
182
|
+
},
|
|
183
|
+
sphere: {
|
|
184
|
+
x: 0,
|
|
185
|
+
y: 0,
|
|
186
|
+
r: 1
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const [, south, , north] = bounds;
|
|
191
|
+
const raw = projection === "conic" ? conicEqualArea((south + (north - south) / 6) * RADIANS, (north - (north - south) / 6) * RADIANS) : projection === "mercator" ? mercator : projection === "equalEarth" ? equalEarth : projection === "naturalEarth" ? naturalEarth : (lambda, phi) => [lambda, phi];
|
|
192
|
+
const rotate = (point) => [wrap(point[0] * RADIANS - lambda0), point[1] * RADIANS];
|
|
193
|
+
return {
|
|
194
|
+
polygon: (rings) => rings.flatMap((ring) => cutAntimeridian(open(ring).map(rotate)).map((piece) => piece.map(([lambda, phi]) => flip(raw(lambda, phi))))),
|
|
195
|
+
point: (point) => flip(raw(...rotate(point)))
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
export { createProjection };
|
|
200
|
+
|
|
201
|
+
//# sourceMappingURL=projection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection.js","names":[],"sources":["../../../src/charts/geo/projection.ts"],"sourcesContent":["/**\n * 地图投影:经纬度 → 平面坐标(y 向下,同屏幕),以及把多边形裁到能画的范围里。\n * 公式同 d3-geo、d3-geo-projection(Mike Bostock,ISC);这里只做地图用到的几种,球面裁剪写成精简版:\n *\n * - 先旋转到以 center 为中心(经度方向;正射投影连纬度一起转);\n * - 世界地图换了中央经线后,跨过反子午线(中心对面那条经线)的多边形要切开:经度展开成连续的,\n * 在 [-180°, 180°] 的条带里裁剪(平移 ±360° 的副本也裁一遍),绕着极点的环(南极洲)先沿极点闭合;\n * - 正射投影只画朝向观察者的半球:在三维里按平面裁剪,切口沿地平圈补上弧。\n *\n * const project = createProjection('conic', { center: [105, 35], bounds });\n * project.polygon(rings) // 裁剪、投影后的环(坐标单位任意,画的时候再按绘图区缩放)\n * project.point([121.47, 31.23]) // [x, y];在地球背面时为 null\n */\n\n/** 内置的投影 */\nexport type MapProjectionName =\n 'conic' | 'mercator' | 'naturalEarth' | 'equalEarth' | 'equirectangular' | 'orthographic';\n\n/**\n * 自己的投影:[经度, 纬度](度)→ [x, y](y 向下);看不见时返回 null。d3-geo 的投影可以直接传进来\n * (它本身就是这样的函数);自己的投影不做裁剪\n */\nexport type MapGeoProjection = (coordinates: [number, number]) => [number, number] | null;\n\n/** 平面上的一个点 */\ntype Planar = [number, number];\n\nconst RADIANS = Math.PI / 180;\nconst HALF_PI = Math.PI / 2;\nconst TAU = Math.PI * 2;\n/** 墨卡托到这个纬度为止(再往上无穷大):同网页地图的 ±85.05° */\nconst MERCATOR_LIMIT = Math.atan(Math.sinh(Math.PI));\n\n/** 原始投影:弧度的经纬度 → 平面坐标(y 向上) */\ntype Raw = (lambda: number, phi: number) => Planar;\n\nconst mercator: Raw = (lambda, phi) => {\n const limited = Math.max(-MERCATOR_LIMIT, Math.min(MERCATOR_LIMIT, phi));\n return [lambda, Math.log(Math.tan(Math.PI / 4 + limited / 2))];\n};\n\nconst naturalEarth: Raw = (lambda, phi) => {\n const phi2 = phi * phi;\n const phi4 = phi2 * phi2;\n return [\n lambda *\n (0.8707 - 0.131979 * phi2 + phi4 * (-0.013791 + phi4 * (0.003971 * phi2 - 0.001529 * phi4))),\n phi * (1.007226 + phi2 * (0.015085 + phi4 * (-0.044475 + 0.028874 * phi2 - 0.005916 * phi4))),\n ];\n};\n\nconst equalEarth: Raw = (lambda, phi) => {\n const [a1, a2, a3, a4] = [1.340264, -0.081106, 0.000893, 0.003796];\n const m = Math.sqrt(3) / 2;\n const l = Math.asin(m * Math.sin(phi));\n const l2 = l * l;\n const l6 = l2 * l2 * l2;\n return [\n (lambda * Math.cos(l)) / (m * (a1 + 3 * a2 * l2 + l6 * (7 * a3 + 9 * a4 * l2))),\n l * (a1 + a2 * l2 + l6 * (a3 + a4 * l2)),\n ];\n};\n\n/** 等积圆锥(Albers):两条标准纬线(弧度);两条纬线关于赤道对称时退化成等积圆柱 */\nfunction conicEqualArea(phi0: number, phi1: number): Raw {\n const sy0 = Math.sin(phi0);\n const n = (sy0 + Math.sin(phi1)) / 2;\n if (Math.abs(n) < 1e-6) {\n const cosPhi0 = Math.cos(phi0);\n return (lambda, phi) => [lambda * cosPhi0, Math.sin(phi) / cosPhi0];\n }\n const c = 1 + sy0 * (2 * n - sy0);\n const r0 = Math.sqrt(c) / n;\n return (lambda, phi) => {\n const r = Math.sqrt(Math.max(0, c - 2 * n * Math.sin(phi))) / n;\n return [r * Math.sin(lambda * n), r0 - r * Math.cos(lambda * n)];\n };\n}\n\n/** 经度归到 [-π, π] */\nconst wrap = (lambda: number) => lambda - TAU * Math.round(lambda / TAU);\n\n/** 单位球上的点:x 朝向经纬度 (0, 0),z 朝北极 */\ntype Vector = [number, number, number];\n\nconst toVector = (lambda: number, phi: number): Vector => [\n Math.cos(phi) * Math.cos(lambda),\n Math.cos(phi) * Math.sin(lambda),\n Math.sin(phi),\n];\n\n/** 绕东西轴转过 -pitch:把纬度 pitch 处转到赤道上(同 d3 的 rotation 的 φ 分量) */\nfunction tilt([x, y, z]: Vector, pitch: number): Vector {\n const cos = Math.cos(pitch);\n const sin = Math.sin(pitch);\n return [x * cos + z * sin, y, z * cos - x * sin];\n}\n\n/** Sutherland–Hodgman:一个环裁到 keep(点) 为真的一侧,cross 求边与边界的交点 */\nfunction clipRing<P>(\n ring: readonly P[],\n keep: (point: P) => boolean,\n cross: (a: P, b: P) => P,\n): P[] {\n const output: P[] = [];\n ring.forEach((current, i) => {\n const previous = ring[(i + ring.length - 1) % ring.length]!;\n if (keep(current)) {\n if (!keep(previous)) output.push(cross(previous, current));\n output.push(current);\n } else if (keep(previous)) output.push(cross(previous, current));\n });\n return output;\n}\n\n/** 去掉环末尾与开头重复的点(TopoJSON 的环是闭合的) */\nconst open = <P extends readonly number[]>(ring: readonly P[]) => {\n const first = ring[0];\n const last = ring.at(-1);\n return first && last && ring.length > 1 && first[0] === last[0] && first[1] === last[1]\n ? ring.slice(0, -1)\n : [...ring];\n};\n\n/**\n * 跨反子午线的环切开(经纬度都是弧度,经度已旋转到 [-π, π]):展开成连续的经度,绕极点的环沿极点闭合,\n * 再在 [-π, π] 的条带里裁剪自己和平移 ±2π 的副本;裁出的边沿着反子午线每隔 2° 补点(投影后是曲线)\n */\nfunction cutAntimeridian(ring: readonly Planar[]): Planar[][] {\n const unwrapped: Planar[] = [];\n let offset = 0;\n ring.forEach(([lambda, phi], i) => {\n if (i > 0) {\n const delta = lambda - ring[i - 1]![0];\n if (delta > Math.PI) offset -= TAU;\n else if (delta < -Math.PI) offset += TAU;\n }\n unwrapped.push([lambda + offset, phi]);\n });\n if (Math.abs(offset) > Math.PI) {\n // 绕了极点一圈:从最后一点沿纬线走到极点,再回到第一点的经度\n const pole = unwrapped.reduce((sum, [, phi]) => sum + phi, 0) < 0 ? -HALF_PI : HALF_PI;\n unwrapped.push([unwrapped.at(-1)![0], pole], [unwrapped[0]![0], pole]);\n }\n const lambdas = unwrapped.map(([lambda]) => lambda);\n const [min, max] = [Math.min(...lambdas), Math.max(...lambdas)];\n if (min >= -Math.PI - 1e-9 && max <= Math.PI + 1e-9) return [unwrapped];\n const atEdge =\n (edge: number) =>\n (a: Planar, b: Planar): Planar => [\n edge,\n a[1] + ((edge - a[0]) / (b[0] - a[0])) * (b[1] - a[1]),\n ];\n const pieces: Planar[][] = [];\n for (const shift of [-TAU, 0, TAU]) {\n if (max + shift <= -Math.PI || min + shift >= Math.PI) continue;\n const shifted = unwrapped.map(([lambda, phi]): Planar => [lambda + shift, phi]);\n const clipped = clipRing(\n clipRing(shifted, ([lambda]) => lambda <= Math.PI, atEdge(Math.PI)),\n ([lambda]) => lambda >= -Math.PI,\n atEdge(-Math.PI),\n );\n if (clipped.length >= 3) pieces.push(densifyEdge(clipped));\n }\n return pieces;\n}\n\n/** 沿反子午线的边每隔 2° 补一个点 */\nfunction densifyEdge(ring: readonly Planar[]): Planar[] {\n const step = 2 * RADIANS;\n const output: Planar[] = [];\n ring.forEach((point, i) => {\n const next = ring[(i + 1) % ring.length]!;\n output.push(point);\n const onEdge = Math.abs(Math.abs(point[0]) - Math.PI) < 1e-9 && point[0] === next[0];\n if (!onEdge) return;\n const count = Math.floor(Math.abs(next[1] - point[1]) / step);\n for (let k = 1; k < count; k += 1)\n output.push([point[0], point[1] + ((next[1] - point[1]) * k) / count]);\n });\n return output;\n}\n\n/** 正射投影:环裁到朝向观察者的半球(x ≥ 0),切口沿地平圈补弧;返回投影后的平面坐标(y 向上) */\nfunction clipHemisphere(ring: readonly Vector[]): Planar[] {\n const clipped = clipRing(\n ring,\n ([x]) => x >= 0,\n (a, b): Vector => {\n const t = a[0] / (a[0] - b[0]);\n const point: Vector = [0, a[1] + t * (b[1] - a[1]), a[2] + t * (b[2] - a[2])];\n const length = Math.hypot(point[1], point[2]) || 1;\n return [0, point[1] / length, point[2] / length];\n },\n );\n const output: Planar[] = [];\n clipped.forEach((point, i) => {\n output.push([point[1], point[2]]);\n const next = clipped[(i + 1) % clipped.length]!;\n if (point[0] !== 0 || next[0] !== 0) return;\n // 两个交点之间沿地平圈走较短的那段弧\n const from = Math.atan2(point[2], point[1]);\n let delta = Math.atan2(next[2], next[1]) - from;\n if (delta > Math.PI) delta -= TAU;\n if (delta < -Math.PI) delta += TAU;\n const count = Math.ceil(Math.abs(delta) / (4 * RADIANS));\n for (let k = 1; k < count; k += 1) {\n const angle = from + (delta * k) / count;\n output.push([Math.cos(angle), Math.sin(angle)]);\n }\n });\n return output;\n}\n\n/** createProjection 的选项 */\nexport interface ProjectionOptions {\n /** 投影的中心 [经度, 纬度](度):中央经线;正射投影还用纬度 */\n center: readonly [number, number];\n /** 地理数据的范围 [西, 南, 东, 北]:圆锥投影的两条标准纬线取范围的 1/6 与 5/6 处 */\n bounds: readonly [number, number, number, number];\n}\n\n/** 投影好的一组工具:多边形、点 */\nexport interface Projector {\n /** 一个多边形(几个环,经纬度)裁剪、投影后的环(y 向下);整个看不见时是空数组 */\n polygon(rings: readonly (readonly (readonly [number, number])[])[]): Planar[][];\n /** 一个点;看不见时为 null */\n point(coordinates: readonly [number, number]): Planar | null;\n /** 正射投影:地球的轮廓(圆心与半径,投影坐标);其余投影没有 */\n sphere?: { x: number; y: number; r: number };\n}\n\n/** 按名字(或自己的投影)建投影(见文件开头) */\nexport function createProjection(\n projection: MapProjectionName | MapGeoProjection,\n { center, bounds }: ProjectionOptions,\n): Projector {\n if (typeof projection === 'function')\n return {\n polygon: (rings) =>\n rings.map((ring) =>\n ring.flatMap((point) => {\n const result = projection([point[0], point[1]]);\n return result ? [result] : [];\n }),\n ),\n point: (point) => projection([point[0], point[1]]),\n };\n const lambda0 = center[0] * RADIANS;\n const flip = ([x, y]: Planar): Planar => [x, -y];\n if (projection === 'orthographic') {\n const pitch = center[1] * RADIANS;\n const toView = (point: readonly [number, number]) =>\n tilt(toVector(point[0] * RADIANS - lambda0, point[1] * RADIANS), pitch);\n return {\n polygon: (rings) =>\n rings.flatMap((ring) => {\n const clipped = clipHemisphere(open(ring).map(toView));\n return clipped.length >= 3 ? [clipped.map(flip)] : [];\n }),\n point: (point) => {\n const [x, y, z] = toView(point);\n return x < 0 ? null : [y, -z];\n },\n sphere: { x: 0, y: 0, r: 1 },\n };\n }\n const [, south, , north] = bounds;\n const raw: Raw =\n projection === 'conic'\n ? conicEqualArea(\n (south + (north - south) / 6) * RADIANS,\n (north - (north - south) / 6) * RADIANS,\n )\n : projection === 'mercator'\n ? mercator\n : projection === 'equalEarth'\n ? equalEarth\n : projection === 'naturalEarth'\n ? naturalEarth\n : (lambda, phi) => [lambda, phi];\n const rotate = (point: readonly [number, number]): Planar => [\n wrap(point[0] * RADIANS - lambda0),\n point[1] * RADIANS,\n ];\n return {\n polygon: (rings) =>\n rings.flatMap((ring) =>\n cutAntimeridian(open(ring).map(rotate)).map((piece) =>\n piece.map(([lambda, phi]) => flip(raw(lambda, phi))),\n ),\n ),\n point: (point) => flip(raw(...rotate(point))),\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** TopoJSON 里的一个几何对象:多边形、多个多边形,或一组几何对象 */
|
|
2
|
+
export interface MapTopologyObject {
|
|
3
|
+
/** 类型:Polygon、MultiPolygon、GeometryCollection(其余类型不画) */
|
|
4
|
+
type: string;
|
|
5
|
+
/** 要素的 id:地图按它对应区域的数据 */
|
|
6
|
+
id?: string | number;
|
|
7
|
+
/** 要素的属性(名称等) */
|
|
8
|
+
properties?: Readonly<Record<string, unknown>> | null;
|
|
9
|
+
/** 多边形的环:弧的序号(Polygon 是两层数组,MultiPolygon 是三层) */
|
|
10
|
+
arcs?: unknown;
|
|
11
|
+
/** GeometryCollection 里的几何对象 */
|
|
12
|
+
geometries?: readonly MapTopologyObject[];
|
|
13
|
+
}
|
|
14
|
+
/** TopoJSON 拓扑:地理数据 */
|
|
15
|
+
export interface MapTopology {
|
|
16
|
+
/** 'Topology' */
|
|
17
|
+
type: string;
|
|
18
|
+
/** 各段弧:点的序列;有 transform 时是量化后逐点的差值 */
|
|
19
|
+
arcs: readonly (readonly (readonly number[])[])[];
|
|
20
|
+
/** 量化:坐标 = 整数 × scale + translate */
|
|
21
|
+
transform?: {
|
|
22
|
+
scale: readonly number[];
|
|
23
|
+
translate: readonly number[];
|
|
24
|
+
};
|
|
25
|
+
/** 经纬度范围 [西, 南, 东, 北] */
|
|
26
|
+
bbox?: readonly number[];
|
|
27
|
+
/** 要素组:名字 → 几何对象(通常是 GeometryCollection) */
|
|
28
|
+
objects: Readonly<Record<string, MapTopologyObject>>;
|
|
29
|
+
}
|
|
30
|
+
/** 经纬度(度):[经度, 纬度] */
|
|
31
|
+
export type GeoPosition = readonly [number, number];
|
|
32
|
+
/** 解出来的一个要素 */
|
|
33
|
+
export interface MapFeature {
|
|
34
|
+
/** 要素的 id(转成字符串) */
|
|
35
|
+
id: string;
|
|
36
|
+
/** 属性 */
|
|
37
|
+
properties: Readonly<Record<string, unknown>>;
|
|
38
|
+
/** 多边形:每个多边形是几个环,第一个是外环,其余是洞 */
|
|
39
|
+
polygons: GeoPosition[][][];
|
|
40
|
+
}
|
|
41
|
+
/** 要素组 name 里的要素(GeometryCollection 展开;单个几何对象当作一个要素) */
|
|
42
|
+
export declare function topologyFeatures(topology: MapTopology, name: string): MapFeature[];
|
|
43
|
+
/**
|
|
44
|
+
* 简化地理数据:去掉与前后两点围成的三角形面积不到 minArea(坐标单位的平方,经纬度是平方度)的点
|
|
45
|
+
* (Visvalingam,topojson-simplify)。相邻区域共用的边界一起简化,不会出现缝隙;各段弧的两端总保留。
|
|
46
|
+
* minArea 按 2 的整数次幂分档缓存:绘图区大小变一点不重算。
|
|
47
|
+
*/
|
|
48
|
+
export declare function simplifyTopology(topology: MapTopology, minArea: number): MapTopology;
|
|
49
|
+
/** TopoJSON 的经纬度范围 [西, 南, 东, 北]:有 bbox 用它,否则解出各段弧的坐标 */
|
|
50
|
+
export declare function topologyBounds(topology: MapTopology): [number, number, number, number];
|