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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/charts/AreaChart.d.ts +1 -1
  2. package/dist/charts/AreaChart.js +6 -48
  3. package/dist/charts/AreaChart.js.map +1 -1
  4. package/dist/charts/BarChart.js +6 -49
  5. package/dist/charts/BarChart.js.map +1 -1
  6. package/dist/charts/ChordChart.d.ts +7 -2
  7. package/dist/charts/ChordChart.js +151 -60
  8. package/dist/charts/ChordChart.js.map +1 -1
  9. package/dist/charts/ComboChart.d.ts +2 -1
  10. package/dist/charts/ComboChart.js +17 -93
  11. package/dist/charts/ComboChart.js.map +1 -1
  12. package/dist/charts/DonutChart.d.ts +6 -1
  13. package/dist/charts/DonutChart.js +134 -51
  14. package/dist/charts/DonutChart.js.map +1 -1
  15. package/dist/charts/FunnelChart.js.map +1 -1
  16. package/dist/charts/GraphChart.d.ts +22 -10
  17. package/dist/charts/GraphChart.js +415 -175
  18. package/dist/charts/GraphChart.js.map +1 -1
  19. package/dist/charts/LineChart.d.ts +1 -29
  20. package/dist/charts/LineChart.js +6 -54
  21. package/dist/charts/LineChart.js.map +1 -1
  22. package/dist/charts/MapChart.d.ts +21 -48
  23. package/dist/charts/MapChart.js +426 -353
  24. package/dist/charts/MapChart.js.map +1 -1
  25. package/dist/charts/RadarChart.js.map +1 -1
  26. package/dist/charts/SankeyChart.d.ts +7 -2
  27. package/dist/charts/SankeyChart.js +164 -76
  28. package/dist/charts/SankeyChart.js.map +1 -1
  29. package/dist/charts/ScatterChart.d.ts +7 -1
  30. package/dist/charts/ScatterChart.js +45 -30
  31. package/dist/charts/ScatterChart.js.map +1 -1
  32. package/dist/charts/TimelineChart.d.ts +8 -1
  33. package/dist/charts/TimelineChart.js +141 -89
  34. package/dist/charts/TimelineChart.js.map +1 -1
  35. package/dist/charts/TreemapChart.d.ts +6 -0
  36. package/dist/charts/TreemapChart.js +174 -58
  37. package/dist/charts/TreemapChart.js.map +1 -1
  38. package/dist/charts/WaterfallChart.d.ts +6 -7
  39. package/dist/charts/WaterfallChart.js +46 -51
  40. package/dist/charts/WaterfallChart.js.map +1 -1
  41. package/dist/charts/XYChart.d.ts +44 -35
  42. package/dist/charts/XYChart.js +493 -281
  43. package/dist/charts/XYChart.js.map +1 -1
  44. package/dist/charts/axis-ticks.d.ts +1 -1
  45. package/dist/charts/axis-ticks.js +4 -5
  46. package/dist/charts/axis-ticks.js.map +1 -1
  47. package/dist/charts/flow.d.ts +4 -11
  48. package/dist/charts/flow.js +4 -4
  49. package/dist/charts/flow.js.map +1 -1
  50. package/dist/charts/frame.d.ts +6 -30
  51. package/dist/charts/frame.js +6 -99
  52. package/dist/charts/frame.js.map +1 -1
  53. package/dist/charts/geo/projection.d.ts +45 -0
  54. package/dist/charts/geo/projection.js +201 -0
  55. package/dist/charts/geo/projection.js.map +1 -0
  56. package/dist/charts/geo/topojson.d.ts +50 -0
  57. package/dist/charts/geo/topojson.js +119 -0
  58. package/dist/charts/geo/topojson.js.map +1 -0
  59. package/dist/charts/index.d.ts +21 -9
  60. package/dist/charts/index.js +19 -8
  61. package/dist/charts/label-layout.js.map +1 -1
  62. package/dist/charts/layout/chord.d.ts +44 -0
  63. package/dist/charts/layout/chord.js +45 -0
  64. package/dist/charts/layout/chord.js.map +1 -0
  65. package/dist/charts/layout/graph.d.ts +49 -0
  66. package/dist/charts/layout/graph.js +268 -0
  67. package/dist/charts/layout/graph.js.map +1 -0
  68. package/dist/charts/layout/sankey.d.ts +79 -0
  69. package/dist/charts/layout/sankey.js +200 -0
  70. package/dist/charts/layout/sankey.js.map +1 -0
  71. package/dist/charts/layout/treemap.d.ts +46 -0
  72. package/dist/charts/layout/treemap.js +152 -0
  73. package/dist/charts/layout/treemap.js.map +1 -0
  74. package/dist/charts/marks.d.ts +79 -0
  75. package/dist/charts/marks.js +231 -0
  76. package/dist/charts/marks.js.map +1 -0
  77. package/dist/charts/plot.d.ts +107 -0
  78. package/dist/charts/plot.js +231 -0
  79. package/dist/charts/plot.js.map +1 -0
  80. package/dist/charts/scale.d.ts +29 -0
  81. package/dist/charts/scale.js +244 -0
  82. package/dist/charts/scale.js.map +1 -0
  83. package/dist/charts/shape.d.ts +46 -0
  84. package/dist/charts/shape.js +131 -0
  85. package/dist/charts/shape.js.map +1 -0
  86. package/dist/charts/stack.d.ts +17 -0
  87. package/dist/charts/stack.js +35 -0
  88. package/dist/charts/stack.js.map +1 -0
  89. package/dist/charts/text-size.d.ts +12 -0
  90. package/dist/charts/text-size.js +32 -0
  91. package/dist/charts/text-size.js.map +1 -0
  92. package/dist/config/createUi.d.ts +21 -2
  93. package/dist/config/createUi.js +18 -1
  94. package/dist/config/createUi.js.map +1 -1
  95. package/dist/config/theme/preference.js +2 -1
  96. package/dist/config/theme/preference.js.map +1 -1
  97. package/dist/index.d.ts +2 -2
  98. package/dist/maps/china-provinces.js +35680 -0
  99. package/dist/maps/china-provinces.js.map +1 -0
  100. package/dist/maps/china.d.ts +13 -0
  101. package/dist/maps/china.js +8 -0
  102. package/dist/maps/china.js.map +1 -0
  103. package/dist/maps/index.d.ts +9 -0
  104. package/dist/maps/index.js +3 -0
  105. package/dist/maps/world-countries.js +22273 -0
  106. package/dist/maps/world-countries.js.map +1 -0
  107. package/dist/maps/world.d.ts +13 -0
  108. package/dist/maps/world.js +8 -0
  109. package/dist/maps/world.js.map +1 -0
  110. package/dist/overlay/detached.js.map +1 -1
  111. package/dist/utils/request/use-request.d.ts +25 -1
  112. package/dist/utils/request/use-request.js +35 -3
  113. package/dist/utils/request/use-request.js.map +1 -1
  114. package/package.json +7 -8
  115. package/dist/charts/single.d.ts +0 -41
  116. package/dist/charts/single.js +0 -66
  117. package/dist/charts/single.js.map +0 -1
@@ -0,0 +1,107 @@
1
+ import { css } from '@zerodep-dev/vue-tsx-pro-tsx/styles';
2
+ import { type VNodeChild } from 'vue';
3
+ /**
4
+ * 绘图区进入过视口:之前不画,进来之后一直算看见(滚出去不拆掉)。一页很多图表时先画看得见的,
5
+ * 其余的滚到附近再画(地图要投影几万个点、网络图要迭代布局)。在 setup 里调用,渲染时读返回的函数。
6
+ * 挂上时就在视口附近的立即算看见(不等观察器的第一次回调;没有布局的测试环境里框都在原点,也算看见);
7
+ * 打印前全部画上;没有 IntersectionObserver 的环境当作看见了。
8
+ */
9
+ export declare function usePlotSeen(plot: () => HTMLElement | null | undefined): () => boolean;
10
+ /** 绘图区里画图形的矩形(SVG 坐标,px) */
11
+ export interface PlotRect {
12
+ /** 左 */
13
+ left: number;
14
+ /** 上 */
15
+ top: number;
16
+ /** 宽 */
17
+ width: number;
18
+ /** 高 */
19
+ height: number;
20
+ }
21
+ /** usePlotArea 的返回值 */
22
+ export interface PlotArea {
23
+ /** 交给 ChartFrame 的 plotRef */
24
+ ref: (element: HTMLElement | null) => void;
25
+ /** 绘图区元素 */
26
+ readonly element: HTMLElement | null;
27
+ /** 宽(px) */
28
+ readonly width: number;
29
+ /** 高(px) */
30
+ readonly height: number;
31
+ /** 可以画了:量到了大小,也进入过视口 */
32
+ readonly ready: boolean;
33
+ }
34
+ /** 绘图区的元素、大小与可见性(在 setup 里调用);没有布局的环境(服务端、测试)里大小是 0,不画 */
35
+ export declare function usePlotArea(): PlotArea;
36
+ /** 事件在元素里的坐标(px) */
37
+ export declare function pointerIn(event: {
38
+ clientX: number;
39
+ clientY: number;
40
+ }, element: Element): {
41
+ x: number;
42
+ y: number;
43
+ };
44
+ /** 图表 SVG 的共用样式 */
45
+ export declare const chartSvgStyles: Readonly<{
46
+ readonly svg: css.CompiledStyles;
47
+ readonly grid: css.CompiledStyles;
48
+ readonly baseline: css.CompiledStyles;
49
+ readonly tick: css.CompiledStyles;
50
+ readonly axisTitle: css.CompiledStyles;
51
+ readonly crosshair: css.CompiledStyles;
52
+ readonly hoverBand: css.CompiledStyles;
53
+ readonly halo: css.CompiledStyles;
54
+ readonly label: css.CompiledStyles;
55
+ readonly labelMuted: css.CompiledStyles;
56
+ readonly labelStrong: css.CompiledStyles;
57
+ readonly noPointer: css.CompiledStyles;
58
+ readonly fadeIn: css.CompiledStyles;
59
+ readonly dimmable: css.CompiledStyles;
60
+ readonly dimmed: css.CompiledStyles;
61
+ readonly fill: (color: string) => Readonly<[css.CompiledStyles, css.InlineStyles]>;
62
+ readonly stroke: (color: string) => Readonly<[css.CompiledStyles, css.InlineStyles]>;
63
+ }>;
64
+ /** 坐标轴上的一个刻度 */
65
+ export interface AxisTick {
66
+ /** 位置:竖轴是纵坐标,横轴是横坐标(px) */
67
+ position: number;
68
+ /** 文字;空串时只画网格线 */
69
+ label: string;
70
+ }
71
+ /** chartAxis 的参数 */
72
+ export interface ChartAxisOptions {
73
+ /** 刻度文字在绘图区的哪一边 */
74
+ side: 'left' | 'right' | 'bottom';
75
+ /** 刻度 */
76
+ ticks: readonly AxisTick[];
77
+ /** 绘图区 */
78
+ plot: PlotRect;
79
+ /** 画网格线(穿过绘图区) */
80
+ grid?: boolean;
81
+ /** 刻度文字与绘图区的距离,默认 8 */
82
+ gap?: number;
83
+ }
84
+ /** 坐标轴:刻度文字,可带网格线(键是位置,更新时文字原地换) */
85
+ export declare function chartAxis({ side, ticks, plot, grid, gap, }: ChartAxisOptions): VNodeChild;
86
+ /** 一个提示框:指针在绘图区里的位置与内容 */
87
+ export interface ChartTip {
88
+ /** 横坐标(px,相对绘图区) */
89
+ x: number;
90
+ /** 纵坐标 */
91
+ y: number;
92
+ /** 内容(chartTooltip 生成) */
93
+ content: VNodeChild;
94
+ }
95
+ /** 提示框的样式 */
96
+ export declare const chartTooltipStyles: Readonly<{
97
+ box: css.CompiledStyles;
98
+ hidden: css.CompiledStyles;
99
+ }>;
100
+ /**
101
+ * 跟着指针的提示框:放在指针右侧,过了绘图区的一半放到左侧;纵向以指针为中线。
102
+ * 消失时保留上一次的内容淡出。给读屏的是数据表,提示框和绘图区一样对读屏隐藏
103
+ */
104
+ export declare const ChartTooltipBox: import("@zerodep-dev/vue-tsx-pro-tsx").TsxComponent<{
105
+ tip: ChartTip | null;
106
+ width: number;
107
+ }>;
@@ -0,0 +1,231 @@
1
+ import { svgNumber } from "../utils/data/number.js";
2
+ import { t } from "../config/theme/tokens.js";
3
+ import { useResizeObserver } from "../utils/dom/resize.js";
4
+ import { useIntersectionObserver } from "../utils/dom/intersection.js";
5
+ import { useEventListener } from "../utils/dom/event-listener.js";
6
+ import { createVNode, shallowRef, watch } from "vue";
7
+ import { _component, mergeJsxProps, readonlyProp } from "@zerodep-dev/vue-tsx-pro-tsx";
8
+ import { css } from "@zerodep-dev/vue-tsx-pro-tsx/styles";
9
+ //#region src/charts/plot.tsx
10
+ /** 绘图区离视口多近就算看见了(px):滚到之前先画好,进来时已经是画好的 */
11
+ var SEEN_MARGIN = 200;
12
+ /**
13
+ * 绘图区进入过视口:之前不画,进来之后一直算看见(滚出去不拆掉)。一页很多图表时先画看得见的,
14
+ * 其余的滚到附近再画(地图要投影几万个点、网络图要迭代布局)。在 setup 里调用,渲染时读返回的函数。
15
+ * 挂上时就在视口附近的立即算看见(不等观察器的第一次回调;没有布局的测试环境里框都在原点,也算看见);
16
+ * 打印前全部画上;没有 IntersectionObserver 的环境当作看见了。
17
+ */
18
+ function usePlotSeen(plot) {
19
+ const seen = shallowRef(typeof IntersectionObserver === "undefined");
20
+ const near = (element) => {
21
+ const box = element.getBoundingClientRect();
22
+ return box.top < window.innerHeight + SEEN_MARGIN && box.bottom > -200 && box.left < window.innerWidth + SEEN_MARGIN && box.right > -200;
23
+ };
24
+ watch(plot, (element) => {
25
+ if (element && !seen.value && near(element)) seen.value = true;
26
+ }, {
27
+ immediate: true,
28
+ flush: "post"
29
+ });
30
+ useIntersectionObserver(() => seen.value ? null : plot(), (entries) => {
31
+ if (entries.some((entry) => entry.isIntersecting)) seen.value = true;
32
+ }, () => ({ rootMargin: `${SEEN_MARGIN}px` }));
33
+ useEventListener(() => window, "beforeprint", () => seen.value = true);
34
+ return () => seen.value;
35
+ }
36
+ /** 绘图区里画图形的矩形(SVG 坐标,px) */
37
+ /** usePlotArea 的返回值 */
38
+ /** 绘图区的元素、大小与可见性(在 setup 里调用);没有布局的环境(服务端、测试)里大小是 0,不画 */
39
+ function usePlotArea() {
40
+ const element = shallowRef(null);
41
+ const size = shallowRef({
42
+ width: 0,
43
+ height: 0
44
+ });
45
+ const seen = usePlotSeen(() => element.value);
46
+ useResizeObserver(() => element.value, (entries) => {
47
+ const box = entries.at(-1).contentRect;
48
+ if (box.width !== size.value.width || box.height !== size.value.height) size.value = {
49
+ width: box.width,
50
+ height: box.height
51
+ };
52
+ });
53
+ return {
54
+ ref: (value) => element.value = value,
55
+ get element() {
56
+ return element.value;
57
+ },
58
+ get width() {
59
+ return size.value.width;
60
+ },
61
+ get height() {
62
+ return size.value.height;
63
+ },
64
+ get ready() {
65
+ return seen() && size.value.width > 0 && size.value.height > 0;
66
+ }
67
+ };
68
+ }
69
+ /** 事件在元素里的坐标(px) */
70
+ function pointerIn(event, element) {
71
+ const box = element.getBoundingClientRect();
72
+ return {
73
+ x: event.clientX - box.left,
74
+ y: event.clientY - box.top
75
+ };
76
+ }
77
+ /** 进场:淡入;还有从 0 长出来的(柱、点),时长取主题的 lg */
78
+ var enter = `${t.durations.lg} ${t.easings.standard}`;
79
+ /** 图表 SVG 的共用样式 */
80
+ var chartSvgStyles = css.create({
81
+ svg: {
82
+ display: "block",
83
+ overflow: "visible",
84
+ userSelect: "none",
85
+ WebkitUserSelect: "none"
86
+ },
87
+ grid: {
88
+ stroke: "_borderMuted",
89
+ strokeWidth: 1,
90
+ shapeRendering: "crispEdges"
91
+ },
92
+ baseline: {
93
+ stroke: "_border",
94
+ strokeWidth: 1,
95
+ shapeRendering: "crispEdges"
96
+ },
97
+ tick: {
98
+ fill: "_textMuted",
99
+ fontSize: "_xs"
100
+ },
101
+ axisTitle: {
102
+ fill: "_textMuted",
103
+ fontSize: "_xs"
104
+ },
105
+ crosshair: {
106
+ stroke: "_borderStrong",
107
+ strokeWidth: 1,
108
+ strokeDasharray: "4 3",
109
+ pointerEvents: "none",
110
+ shapeRendering: "crispEdges"
111
+ },
112
+ hoverBand: {
113
+ fill: "_fill",
114
+ pointerEvents: "none"
115
+ },
116
+ halo: {
117
+ paintOrder: "stroke",
118
+ stroke: "_surface",
119
+ strokeWidth: 3,
120
+ strokeLinejoin: "round"
121
+ },
122
+ label: {
123
+ fill: "_text",
124
+ fontSize: "_xs"
125
+ },
126
+ labelMuted: {
127
+ fill: "_textMuted",
128
+ fontSize: "_xs"
129
+ },
130
+ labelStrong: {
131
+ fill: "_text",
132
+ fontSize: "_xs",
133
+ fontWeight: "_medium"
134
+ },
135
+ noPointer: { pointerEvents: "none" },
136
+ fadeIn: {
137
+ opacity: {
138
+ default: 1,
139
+ "@starting-style": 0
140
+ },
141
+ transition: `opacity ${enter}`
142
+ },
143
+ dimmable: { transition: `opacity ${t.durations.sm} ${t.easings.standard}` },
144
+ dimmed: { opacity: .25 },
145
+ fill: (color) => ({ fill: color }),
146
+ stroke: (color) => ({ stroke: color })
147
+ });
148
+ /** 坐标轴上的一个刻度 */
149
+ /** chartAxis 的参数 */
150
+ /** 坐标轴:刻度文字,可带网格线(键是位置,更新时文字原地换) */
151
+ function chartAxis({ side, ticks, plot, grid = false, gap = 8 }) {
152
+ const right = plot.left + plot.width;
153
+ const bottom = plot.top + plot.height;
154
+ return createVNode("g", null, [grid && ticks.map((tick) => {
155
+ const p = Math.round(tick.position) + .5;
156
+ return side === "bottom" ? createVNode("line", mergeJsxProps({
157
+ "key": `g${tick.position}`,
158
+ "x1": p,
159
+ "x2": p,
160
+ "y1": plot.top,
161
+ "y2": bottom
162
+ }, css.attrs(chartSvgStyles.grid)), null) : createVNode("line", mergeJsxProps({
163
+ "key": `g${tick.position}`,
164
+ "x1": plot.left,
165
+ "x2": right,
166
+ "y1": p,
167
+ "y2": p
168
+ }, css.attrs(chartSvgStyles.grid)), null);
169
+ }), ticks.map((tick) => tick.label === "" ? null : side === "bottom" ? createVNode("text", mergeJsxProps({
170
+ "key": `t${tick.position}`,
171
+ "x": svgNumber(tick.position),
172
+ "y": bottom + gap,
173
+ "text-anchor": "middle",
174
+ "dominant-baseline": "hanging"
175
+ }, css.attrs(chartSvgStyles.tick)), [tick.label]) : createVNode("text", mergeJsxProps({
176
+ "key": `t${tick.position}`,
177
+ "x": side === "left" ? plot.left - gap : right + gap,
178
+ "y": svgNumber(tick.position),
179
+ "text-anchor": side === "left" ? "end" : "start",
180
+ "dominant-baseline": "central"
181
+ }, css.attrs(chartSvgStyles.tick)), [tick.label]))]);
182
+ }
183
+ /** 一个提示框:指针在绘图区里的位置与内容 */
184
+ /** 提示框的样式 */
185
+ var chartTooltipStyles = css.create({
186
+ box: {
187
+ position: "absolute",
188
+ top: 0,
189
+ left: 0,
190
+ zIndex: 1,
191
+ boxSizing: "border-box",
192
+ paddingBlock: "_sm",
193
+ paddingInline: "_md",
194
+ borderWidth: "_xs",
195
+ borderStyle: "solid",
196
+ borderColor: "_borderMuted",
197
+ borderRadius: "_md",
198
+ backgroundColor: "_surfaceRaised",
199
+ color: "_text",
200
+ boxShadow: "_md",
201
+ whiteSpace: "nowrap",
202
+ pointerEvents: "none",
203
+ opacity: 1,
204
+ transition: `opacity ${t.durations.sm} ${t.easings.standard}, transform ${t.durations.sm} ${t.easings.standard}`
205
+ },
206
+ hidden: { opacity: 0 }
207
+ });
208
+ /**
209
+ * 跟着指针的提示框:放在指针右侧,过了绘图区的一半放到左侧;纵向以指针为中线。
210
+ * 消失时保留上一次的内容淡出。给读屏的是数据表,提示框和绘图区一样对读屏隐藏
211
+ */
212
+ var ChartTooltipBox = /*#__PURE__*/ _component((_props, _ctx) => {
213
+ const tip = readonlyProp(_props, "tip");
214
+ const width = readonlyProp(_props, "width");
215
+ let last = null;
216
+ return () => {
217
+ if (tip.value) last = tip.value;
218
+ const shown = tip.value ?? last;
219
+ if (!shown) return null;
220
+ const flip = shown.x > width.value / 2;
221
+ const left = flip ? shown.x - 14 : shown.x + 14;
222
+ return createVNode("div", mergeJsxProps({ "data-part": "tooltip" }, css.attrs(chartTooltipStyles.box, !tip.value && chartTooltipStyles.hidden), { "style": { transform: `translate(${Math.round(left)}px, ${Math.round(shown.y)}px) translate(${flip ? "-100%" : "0"}, -50%)` } }), [shown.content]);
223
+ };
224
+ }, {
225
+ name: "ChartTooltipBox",
226
+ props: ["tip", "width"]
227
+ });
228
+ //#endregion
229
+ export { ChartTooltipBox, chartAxis, chartSvgStyles, chartTooltipStyles, pointerIn, usePlotArea, usePlotSeen };
230
+
231
+ //# sourceMappingURL=plot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plot.js","names":["_createVNode","_mergeProps","_vtp_readonlyProp"],"sources":["../../src/charts/plot.tsx"],"sourcesContent":["/**\n * 绘图区的公共部分:量出绘图区的大小(ResizeObserver),SVG 图形的共用样式(网格、刻度文字、十字线、描边的标签、进场),\n * 坐标轴的刻度与网格线,跟着指针的提示框。各种图表在 ChartFrame 的绘图区里用它们画自己的 SVG。\n *\n * const area = usePlotArea();\n * <ChartFrame plotRef={area.ref} plot={area.ready && <svg width={area.width} height={area.height}>…</svg>} />\n * chartAxis({ side: 'bottom', ticks, plot, grid: true })\n * <ChartTooltipBox tip={tip} width={area.width} />\n *\n * 颜色、字号都是主题 tokens(切换主题不用重画);动画的时长是 durations(“减少动态效果”时为 0)。\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 { t } from '../config/theme/tokens';\nimport { useEventListener } from '../utils/dom/event-listener';\nimport { useIntersectionObserver } from '../utils/dom/intersection';\nimport { useResizeObserver } from '../utils/dom/resize';\nimport { svgNumber } from '../utils/data/number';\n\n/** 绘图区离视口多近就算看见了(px):滚到之前先画好,进来时已经是画好的 */\nconst SEEN_MARGIN = 200;\n\n/**\n * 绘图区进入过视口:之前不画,进来之后一直算看见(滚出去不拆掉)。一页很多图表时先画看得见的,\n * 其余的滚到附近再画(地图要投影几万个点、网络图要迭代布局)。在 setup 里调用,渲染时读返回的函数。\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/** 绘图区里画图形的矩形(SVG 坐标,px) */\nexport interface PlotRect {\n /** 左 */\n left: number;\n /** 上 */\n top: number;\n /** 宽 */\n width: number;\n /** 高 */\n height: number;\n}\n\n/** usePlotArea 的返回值 */\nexport interface PlotArea {\n /** 交给 ChartFrame 的 plotRef */\n ref: (element: HTMLElement | null) => void;\n /** 绘图区元素 */\n readonly element: HTMLElement | null;\n /** 宽(px) */\n readonly width: number;\n /** 高(px) */\n readonly height: number;\n /** 可以画了:量到了大小,也进入过视口 */\n readonly ready: boolean;\n}\n\n/** 绘图区的元素、大小与可见性(在 setup 里调用);没有布局的环境(服务端、测试)里大小是 0,不画 */\nexport function usePlotArea(): PlotArea {\n const element = shallowRef<HTMLElement | null>(null);\n const size = shallowRef({ width: 0, height: 0 });\n const seen = usePlotSeen(() => element.value);\n useResizeObserver(\n () => element.value,\n (entries) => {\n const box = entries.at(-1)!.contentRect;\n if (box.width !== size.value.width || box.height !== size.value.height)\n size.value = { width: box.width, height: box.height };\n },\n );\n return {\n ref: (value) => (element.value = value),\n get element() {\n return element.value;\n },\n get width() {\n return size.value.width;\n },\n get height() {\n return size.value.height;\n },\n get ready() {\n return seen() && size.value.width > 0 && size.value.height > 0;\n },\n };\n}\n\n/** 事件在元素里的坐标(px) */\nexport function pointerIn(event: { clientX: number; clientY: number }, element: Element) {\n const box = element.getBoundingClientRect();\n return { x: event.clientX - box.left, y: event.clientY - box.top };\n}\n\n/** 进场:淡入;还有从 0 长出来的(柱、点),时长取主题的 lg */\nconst enter = `${t.durations.lg} ${t.easings.standard}`;\n\n/** 图表 SVG 的共用样式 */\nexport const chartSvgStyles = css.create({\n svg: { display: 'block', overflow: 'visible', userSelect: 'none', WebkitUserSelect: 'none' },\n grid: { stroke: '_borderMuted', strokeWidth: 1, shapeRendering: 'crispEdges' },\n // 零线(数值轴有负数时)\n baseline: { stroke: '_border', strokeWidth: 1, shapeRendering: 'crispEdges' },\n tick: { fill: '_textMuted', fontSize: '_xs' },\n axisTitle: { fill: '_textMuted', fontSize: '_xs' },\n // 十字线:指针所在类目的竖线\n crosshair: {\n stroke: '_borderStrong',\n strokeWidth: 1,\n strokeDasharray: '4 3',\n pointerEvents: 'none',\n shapeRendering: 'crispEdges',\n },\n // 柱状图悬停的类目:整格一层浅底\n hoverBand: { fill: '_fill', pointerEvents: 'none' },\n // 文字描一圈表面色的边:压在图形上也看得清\n halo: {\n paintOrder: 'stroke',\n stroke: '_surface',\n strokeWidth: 3,\n strokeLinejoin: 'round',\n },\n label: { fill: '_text', fontSize: '_xs' },\n labelMuted: { fill: '_textMuted', fontSize: '_xs' },\n labelStrong: { fill: '_text', fontSize: '_xs', fontWeight: '_medium' },\n noPointer: { pointerEvents: 'none' },\n fadeIn: {\n opacity: { default: 1, '@starting-style': 0 },\n transition: `opacity ${enter}`,\n },\n // 悬停别的图形时,其余的变淡\n dimmable: { transition: `opacity ${t.durations.sm} ${t.easings.standard}` },\n dimmed: { opacity: 0.25 },\n fill: (color: string) => ({ fill: color }),\n stroke: (color: string) => ({ stroke: color }),\n});\n\n/** 坐标轴上的一个刻度 */\nexport interface AxisTick {\n /** 位置:竖轴是纵坐标,横轴是横坐标(px) */\n position: number;\n /** 文字;空串时只画网格线 */\n label: string;\n}\n\n/** chartAxis 的参数 */\nexport interface ChartAxisOptions {\n /** 刻度文字在绘图区的哪一边 */\n side: 'left' | 'right' | 'bottom';\n /** 刻度 */\n ticks: readonly AxisTick[];\n /** 绘图区 */\n plot: PlotRect;\n /** 画网格线(穿过绘图区) */\n grid?: boolean;\n /** 刻度文字与绘图区的距离,默认 8 */\n gap?: number;\n}\n\n/** 坐标轴:刻度文字,可带网格线(键是位置,更新时文字原地换) */\nexport function chartAxis({\n side,\n ticks,\n plot,\n grid = false,\n gap = 8,\n}: ChartAxisOptions): VNodeChild {\n const right = plot.left + plot.width;\n const bottom = plot.top + plot.height;\n return (\n <g>\n {grid &&\n ticks.map((tick) => {\n const p = Math.round(tick.position) + 0.5;\n return side === 'bottom' ? (\n <line\n key={`g${tick.position}`}\n x1={p}\n x2={p}\n y1={plot.top}\n y2={bottom}\n {...css.attrs(chartSvgStyles.grid)}\n />\n ) : (\n <line\n key={`g${tick.position}`}\n x1={plot.left}\n x2={right}\n y1={p}\n y2={p}\n {...css.attrs(chartSvgStyles.grid)}\n />\n );\n })}\n {ticks.map((tick) =>\n tick.label === '' ? null : side === 'bottom' ? (\n <text\n key={`t${tick.position}`}\n x={svgNumber(tick.position)}\n y={bottom + gap}\n text-anchor=\"middle\"\n dominant-baseline=\"hanging\"\n {...css.attrs(chartSvgStyles.tick)}\n >\n {tick.label}\n </text>\n ) : (\n <text\n key={`t${tick.position}`}\n x={side === 'left' ? plot.left - gap : right + gap}\n y={svgNumber(tick.position)}\n text-anchor={side === 'left' ? 'end' : 'start'}\n dominant-baseline=\"central\"\n {...css.attrs(chartSvgStyles.tick)}\n >\n {tick.label}\n </text>\n ),\n )}\n </g>\n );\n}\n\n/** 一个提示框:指针在绘图区里的位置与内容 */\nexport interface ChartTip {\n /** 横坐标(px,相对绘图区) */\n x: number;\n /** 纵坐标 */\n y: number;\n /** 内容(chartTooltip 生成) */\n content: VNodeChild;\n}\n\n/** 提示框的样式 */\nexport const chartTooltipStyles = css.create({\n box: {\n position: 'absolute',\n top: 0,\n left: 0,\n zIndex: 1,\n boxSizing: 'border-box',\n paddingBlock: '_sm',\n paddingInline: '_md',\n borderWidth: '_xs',\n borderStyle: 'solid',\n borderColor: '_borderMuted',\n borderRadius: '_md',\n backgroundColor: '_surfaceRaised',\n color: '_text',\n boxShadow: '_md',\n whiteSpace: 'nowrap',\n pointerEvents: 'none',\n opacity: 1,\n transition: `opacity ${t.durations.sm} ${t.easings.standard}, transform ${t.durations.sm} ${t.easings.standard}`,\n },\n hidden: { opacity: 0 },\n});\n\n/**\n * 跟着指针的提示框:放在指针右侧,过了绘图区的一半放到左侧;纵向以指针为中线。\n * 消失时保留上一次的内容淡出。给读屏的是数据表,提示框和绘图区一样对读屏隐藏\n */\nexport const ChartTooltipBox = _component(\n ({ tip, width }: { tip: ChartTip | null; width: number }) => {\n let last: ChartTip | null = null;\n return () => {\n if (tip) last = tip;\n const shown = tip ?? last;\n if (!shown) return null;\n const flip = shown.x > width / 2;\n const left = flip ? shown.x - 14 : shown.x + 14;\n return (\n <div\n data-part=\"tooltip\"\n {...css.attrs(chartTooltipStyles.box, !tip && chartTooltipStyles.hidden)}\n style={{\n transform: `translate(${Math.round(left)}px, ${Math.round(shown.y)}px) translate(${flip ? '-100%' : '0'}, -50%)`,\n }}\n >\n {shown.content}\n </div>\n );\n };\n },\n);\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 图表的比例尺与刻度:数值 → 像素的线性比例尺,“好看”的刻度与取整的范围(d3-array 的 ticks、d3-scale 的 nice 的精简版),
3
+ * 时间刻度(月初、周、日、整点,同 d3 的 scaleTime().ticks)。
4
+ *
5
+ * const y = linearScale(niceDomain([0, 236]), [200, 0]); // 数值轴:0 在下沿
6
+ * linearTicks(0, 250, 5) // [0, 50, 100, 150, 200, 250]
7
+ * timeTicks(start, end, 6) // 跨几个月时是各月的 1 日
8
+ */
9
+ /** 线性比例尺:数值 → 坐标,invert 反过来 */
10
+ export interface LinearScale {
11
+ /** 数值换成坐标 */
12
+ (value: number): number;
13
+ /** 数值的范围 [起, 止] */
14
+ readonly domain: readonly [number, number];
15
+ /** 坐标的范围 [起, 止](数值轴的起点通常在下方,坐标是从大到小) */
16
+ readonly range: readonly [number, number];
17
+ /** 坐标换回数值 */
18
+ invert(position: number): number;
19
+ }
20
+ /** 线性比例尺;domain 两端相同时取中间的坐标 */
21
+ export declare function linearScale(domain: readonly [number, number], range: readonly [number, number]): LinearScale;
22
+ /** 约 count 格的刻度步长:正数是步长本身,负数 -k 表示步长 1 / k(同 d3 的 tickIncrement) */
23
+ export declare function tickIncrement(start: number, stop: number, count: number): number;
24
+ /** [start, stop] 里约 count 格的“好看”刻度:1、2、5 乘 10 的幂的整数倍,从小到大 */
25
+ export declare function linearTicks(start: number, stop: number, count?: number): number[];
26
+ /** 范围的两端扩到刻度上(同 d3 的 linear.nice):[0, 236] → [0, 250] */
27
+ export declare function niceDomain(domain: readonly [number, number], count?: number): [number, number];
28
+ /** [start, stop](毫秒)之间约 count 个整齐的时间刻度:整秒、整分、整点、日、周日、月初、年初(本地时间) */
29
+ export declare function timeTicks(start: number, stop: number, count?: number): Date[];
@@ -0,0 +1,244 @@
1
+ //#region src/charts/scale.ts
2
+ /** 线性比例尺;domain 两端相同时取中间的坐标 */
3
+ function linearScale(domain, range) {
4
+ const [d0, d1] = domain;
5
+ const [r0, r1] = range;
6
+ const span = d1 - d0;
7
+ const scale = ((value) => span === 0 ? (r0 + r1) / 2 : r0 + (value - d0) / span * (r1 - r0));
8
+ return Object.assign(scale, {
9
+ domain,
10
+ range,
11
+ invert: (position) => r1 === r0 ? d0 : d0 + (position - r0) / (r1 - r0) * span
12
+ });
13
+ }
14
+ var E10 = Math.sqrt(50);
15
+ var E5 = Math.sqrt(10);
16
+ var E2 = Math.sqrt(2);
17
+ /**
18
+ * 刻度的整数区间 [i1, i2] 与步长:inc 为正时刻度是 i × inc;为负时是 i / -inc
19
+ * (小数步长用除法表示,0.1 × 3 这类乘法会带进浮点误差)
20
+ */
21
+ function tickSpec(start, stop, count) {
22
+ const step = (stop - start) / Math.max(0, count);
23
+ const power = Math.floor(Math.log10(step));
24
+ const error = step / 10 ** power;
25
+ const factor = error >= E10 ? 10 : error >= E5 ? 5 : error >= E2 ? 2 : 1;
26
+ let i1;
27
+ let i2;
28
+ let inc;
29
+ if (power < 0) {
30
+ inc = 10 ** -power / factor;
31
+ i1 = Math.round(start * inc);
32
+ i2 = Math.round(stop * inc);
33
+ if (i1 / inc < start) i1 += 1;
34
+ if (i2 / inc > stop) i2 -= 1;
35
+ inc = -inc;
36
+ } else {
37
+ inc = 10 ** power * factor;
38
+ i1 = Math.round(start / inc);
39
+ i2 = Math.round(stop / inc);
40
+ if (i1 * inc < start) i1 += 1;
41
+ if (i2 * inc > stop) i2 -= 1;
42
+ }
43
+ if (i2 < i1 && count >= .5 && count < 2) return tickSpec(start, stop, count * 2);
44
+ return [
45
+ i1,
46
+ i2,
47
+ inc
48
+ ];
49
+ }
50
+ /** 约 count 格的刻度步长:正数是步长本身,负数 -k 表示步长 1 / k(同 d3 的 tickIncrement) */
51
+ function tickIncrement(start, stop, count) {
52
+ return tickSpec(start, stop, count)[2];
53
+ }
54
+ /** [start, stop] 里约 count 格的“好看”刻度:1、2、5 乘 10 的幂的整数倍,从小到大 */
55
+ function linearTicks(start, stop, count = 5) {
56
+ if (!(count > 0) || !Number.isFinite(start) || !Number.isFinite(stop)) return [];
57
+ if (start === stop) return [start];
58
+ const [low, high] = start < stop ? [start, stop] : [stop, start];
59
+ const [i1, i2, inc] = tickSpec(low, high, count);
60
+ if (!(i2 >= i1)) return [];
61
+ return Array.from({ length: i2 - i1 + 1 }, (_, i) => inc < 0 ? (i1 + i) / -inc : (i1 + i) * inc);
62
+ }
63
+ /** 范围的两端扩到刻度上(同 d3 的 linear.nice):[0, 236] → [0, 250] */
64
+ function niceDomain(domain, count = 5) {
65
+ let [start, stop] = domain[0] <= domain[1] ? domain : [domain[1], domain[0]];
66
+ let previous;
67
+ for (let guard = 0; guard < 10; guard += 1) {
68
+ const step = tickIncrement(start, stop, count);
69
+ if (step === previous || !Number.isFinite(step)) break;
70
+ if (step > 0) {
71
+ start = Math.floor(start / step) * step;
72
+ stop = Math.ceil(stop / step) * step;
73
+ } else if (step < 0) {
74
+ start = Math.ceil(start * step) / step;
75
+ stop = Math.floor(stop * step) / step;
76
+ } else break;
77
+ previous = step;
78
+ }
79
+ return [start, stop];
80
+ }
81
+ var SECOND = 1e3;
82
+ var MINUTE = SECOND * 60;
83
+ var HOUR = MINUTE * 60;
84
+ var DAY = HOUR * 24;
85
+ var WEEK = DAY * 7;
86
+ var MONTH = DAY * 30;
87
+ var YEAR = DAY * 365;
88
+ /** 可选的间隔:单位、几个单位一格、大约多少毫秒(同 d3 的 tickIntervals) */
89
+ var TIME_INTERVALS = [
90
+ [
91
+ "second",
92
+ 1,
93
+ SECOND
94
+ ],
95
+ [
96
+ "second",
97
+ 5,
98
+ 5 * SECOND
99
+ ],
100
+ [
101
+ "second",
102
+ 15,
103
+ 15 * SECOND
104
+ ],
105
+ [
106
+ "second",
107
+ 30,
108
+ 30 * SECOND
109
+ ],
110
+ [
111
+ "minute",
112
+ 1,
113
+ MINUTE
114
+ ],
115
+ [
116
+ "minute",
117
+ 5,
118
+ 5 * MINUTE
119
+ ],
120
+ [
121
+ "minute",
122
+ 15,
123
+ 15 * MINUTE
124
+ ],
125
+ [
126
+ "minute",
127
+ 30,
128
+ 30 * MINUTE
129
+ ],
130
+ [
131
+ "hour",
132
+ 1,
133
+ HOUR
134
+ ],
135
+ [
136
+ "hour",
137
+ 3,
138
+ 3 * HOUR
139
+ ],
140
+ [
141
+ "hour",
142
+ 6,
143
+ 6 * HOUR
144
+ ],
145
+ [
146
+ "hour",
147
+ 12,
148
+ 12 * HOUR
149
+ ],
150
+ [
151
+ "day",
152
+ 1,
153
+ DAY
154
+ ],
155
+ [
156
+ "day",
157
+ 2,
158
+ 2 * DAY
159
+ ],
160
+ [
161
+ "week",
162
+ 1,
163
+ WEEK
164
+ ],
165
+ [
166
+ "month",
167
+ 1,
168
+ MONTH
169
+ ],
170
+ [
171
+ "month",
172
+ 3,
173
+ 3 * MONTH
174
+ ],
175
+ [
176
+ "year",
177
+ 1,
178
+ YEAR
179
+ ]
180
+ ];
181
+ /** 本地时间里这个单位的起点:月取 1 日 0 点、周取周日 0 点 */
182
+ function floorTime(date, unit) {
183
+ const result = new Date(date);
184
+ if (unit === "second") result.setMilliseconds(0);
185
+ else if (unit === "minute") result.setSeconds(0, 0);
186
+ else if (unit === "hour") result.setMinutes(0, 0, 0);
187
+ else {
188
+ result.setHours(0, 0, 0, 0);
189
+ if (unit === "week") result.setDate(result.getDate() - result.getDay());
190
+ if (unit === "month") result.setDate(1);
191
+ if (unit === "year") result.setMonth(0, 1);
192
+ }
193
+ return result;
194
+ }
195
+ /** 往后数一个单位(按日历走:跨夏令时、大小月都对) */
196
+ function nextTime(date, unit) {
197
+ const result = new Date(date);
198
+ if (unit === "second") result.setSeconds(result.getSeconds() + 1);
199
+ else if (unit === "minute") result.setMinutes(result.getMinutes() + 1);
200
+ else if (unit === "hour") result.setHours(result.getHours() + 1);
201
+ else if (unit === "day") result.setDate(result.getDate() + 1);
202
+ else if (unit === "week") result.setDate(result.getDate() + 7);
203
+ else if (unit === "month") result.setMonth(result.getMonth() + 1);
204
+ else result.setFullYear(result.getFullYear() + 1);
205
+ return result;
206
+ }
207
+ /** 几个单位一格时按哪个字段取整:每 3 个月取 1、4、7、10 月,每 6 小时取 0、6、12、18 点 */
208
+ function timeField(date, unit) {
209
+ if (unit === "second") return date.getSeconds();
210
+ if (unit === "minute") return date.getMinutes();
211
+ if (unit === "hour") return date.getHours();
212
+ if (unit === "day") return date.getDate() - 1;
213
+ if (unit === "month") return date.getMonth();
214
+ if (unit === "year") return date.getFullYear();
215
+ return 0;
216
+ }
217
+ /** [start, stop](毫秒)之间约 count 个整齐的时间刻度:整秒、整分、整点、日、周日、月初、年初(本地时间) */
218
+ function timeTicks(start, stop, count = 6) {
219
+ if (!Number.isFinite(start) || !Number.isFinite(stop) || stop < start) return [];
220
+ const target = (stop - start) / count;
221
+ const index = TIME_INTERVALS.findIndex(([, , duration]) => duration > target);
222
+ let unit;
223
+ let every;
224
+ if (index < 0) {
225
+ unit = "year";
226
+ every = Math.max(1, tickIncrement(start / YEAR, stop / YEAR, count));
227
+ } else if (index === 0) {
228
+ unit = "second";
229
+ every = 1;
230
+ } else {
231
+ const [before, after] = [TIME_INTERVALS[index - 1], TIME_INTERVALS[index]];
232
+ [unit, every] = target / before[2] < after[2] / target ? before : after;
233
+ }
234
+ const ticks = [];
235
+ for (let date = floorTime(new Date(start), unit); date.getTime() <= stop;) {
236
+ if (date.getTime() >= start && timeField(date, unit) % every === 0) ticks.push(date);
237
+ date = nextTime(date, unit);
238
+ }
239
+ return ticks;
240
+ }
241
+ //#endregion
242
+ export { linearScale, linearTicks, niceDomain, tickIncrement, timeTicks };
243
+
244
+ //# sourceMappingURL=scale.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scale.js","names":[],"sources":["../../src/charts/scale.ts"],"sourcesContent":["/**\n * 图表的比例尺与刻度:数值 → 像素的线性比例尺,“好看”的刻度与取整的范围(d3-array 的 ticks、d3-scale 的 nice 的精简版),\n * 时间刻度(月初、周、日、整点,同 d3 的 scaleTime().ticks)。\n *\n * const y = linearScale(niceDomain([0, 236]), [200, 0]); // 数值轴:0 在下沿\n * linearTicks(0, 250, 5) // [0, 50, 100, 150, 200, 250]\n * timeTicks(start, end, 6) // 跨几个月时是各月的 1 日\n */\n\n/** 线性比例尺:数值 → 坐标,invert 反过来 */\nexport interface LinearScale {\n /** 数值换成坐标 */\n (value: number): number;\n /** 数值的范围 [起, 止] */\n readonly domain: readonly [number, number];\n /** 坐标的范围 [起, 止](数值轴的起点通常在下方,坐标是从大到小) */\n readonly range: readonly [number, number];\n /** 坐标换回数值 */\n invert(position: number): number;\n}\n\n/** 线性比例尺;domain 两端相同时取中间的坐标 */\nexport function linearScale(\n domain: readonly [number, number],\n range: readonly [number, number],\n): LinearScale {\n const [d0, d1] = domain;\n const [r0, r1] = range;\n const span = d1 - d0;\n const scale = ((value: number) =>\n span === 0 ? (r0 + r1) / 2 : r0 + ((value - d0) / span) * (r1 - r0)) as LinearScale;\n return Object.assign(scale, {\n domain,\n range,\n invert: (position: number) => (r1 === r0 ? d0 : d0 + ((position - r0) / (r1 - r0)) * span),\n });\n}\n\n// 步长取 1、2、5 乘 10 的幂:误差超过这几个界时换大一档(同 d3-array)\nconst E10 = Math.sqrt(50);\nconst E5 = Math.sqrt(10);\nconst E2 = Math.sqrt(2);\n\n/**\n * 刻度的整数区间 [i1, i2] 与步长:inc 为正时刻度是 i × inc;为负时是 i / -inc\n * (小数步长用除法表示,0.1 × 3 这类乘法会带进浮点误差)\n */\nfunction tickSpec(start: number, stop: number, count: number): [number, number, number] {\n const step = (stop - start) / Math.max(0, count);\n const power = Math.floor(Math.log10(step));\n const error = step / 10 ** power;\n const factor = error >= E10 ? 10 : error >= E5 ? 5 : error >= E2 ? 2 : 1;\n let i1: number;\n let i2: number;\n let inc: number;\n if (power < 0) {\n inc = 10 ** -power / factor;\n i1 = Math.round(start * inc);\n i2 = Math.round(stop * inc);\n if (i1 / inc < start) i1 += 1;\n if (i2 / inc > stop) i2 -= 1;\n inc = -inc;\n } else {\n inc = 10 ** power * factor;\n i1 = Math.round(start / inc);\n i2 = Math.round(stop / inc);\n if (i1 * inc < start) i1 += 1;\n if (i2 * inc > stop) i2 -= 1;\n }\n if (i2 < i1 && count >= 0.5 && count < 2) return tickSpec(start, stop, count * 2);\n return [i1, i2, inc];\n}\n\n/** 约 count 格的刻度步长:正数是步长本身,负数 -k 表示步长 1 / k(同 d3 的 tickIncrement) */\nexport function tickIncrement(start: number, stop: number, count: number): number {\n return tickSpec(start, stop, count)[2];\n}\n\n/** [start, stop] 里约 count 格的“好看”刻度:1、2、5 乘 10 的幂的整数倍,从小到大 */\nexport function linearTicks(start: number, stop: number, count = 5): number[] {\n if (!(count > 0) || !Number.isFinite(start) || !Number.isFinite(stop)) return [];\n if (start === stop) return [start];\n const [low, high] = start < stop ? [start, stop] : [stop, start];\n const [i1, i2, inc] = tickSpec(low, high, count);\n if (!(i2 >= i1)) return [];\n return Array.from({ length: i2 - i1 + 1 }, (_, i) =>\n inc < 0 ? (i1 + i) / -inc : (i1 + i) * inc,\n );\n}\n\n/** 范围的两端扩到刻度上(同 d3 的 linear.nice):[0, 236] → [0, 250] */\nexport function niceDomain(domain: readonly [number, number], count = 5): [number, number] {\n let [start, stop] = domain[0] <= domain[1] ? domain : [domain[1], domain[0]];\n let previous: number | undefined;\n for (let guard = 0; guard < 10; guard += 1) {\n const step = tickIncrement(start, stop, count);\n if (step === previous || !Number.isFinite(step)) break;\n if (step > 0) {\n start = Math.floor(start / step) * step;\n stop = Math.ceil(stop / step) * step;\n } else if (step < 0) {\n start = Math.ceil(start * step) / step;\n stop = Math.floor(stop * step) / step;\n } else break;\n previous = step;\n }\n return [start, stop];\n}\n\n/** 时间刻度的单位 */\ntype TimeUnit = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';\n\nconst SECOND = 1000;\nconst MINUTE = SECOND * 60;\nconst HOUR = MINUTE * 60;\nconst DAY = HOUR * 24;\nconst WEEK = DAY * 7;\nconst MONTH = DAY * 30;\nconst YEAR = DAY * 365;\n\n/** 可选的间隔:单位、几个单位一格、大约多少毫秒(同 d3 的 tickIntervals) */\nconst TIME_INTERVALS: readonly (readonly [TimeUnit, number, number])[] = [\n ['second', 1, SECOND],\n ['second', 5, 5 * SECOND],\n ['second', 15, 15 * SECOND],\n ['second', 30, 30 * SECOND],\n ['minute', 1, MINUTE],\n ['minute', 5, 5 * MINUTE],\n ['minute', 15, 15 * MINUTE],\n ['minute', 30, 30 * MINUTE],\n ['hour', 1, HOUR],\n ['hour', 3, 3 * HOUR],\n ['hour', 6, 6 * HOUR],\n ['hour', 12, 12 * HOUR],\n ['day', 1, DAY],\n ['day', 2, 2 * DAY],\n ['week', 1, WEEK],\n ['month', 1, MONTH],\n ['month', 3, 3 * MONTH],\n ['year', 1, YEAR],\n];\n\n/** 本地时间里这个单位的起点:月取 1 日 0 点、周取周日 0 点 */\nfunction floorTime(date: Date, unit: TimeUnit): Date {\n const result = new Date(date);\n if (unit === 'second') result.setMilliseconds(0);\n else if (unit === 'minute') result.setSeconds(0, 0);\n else if (unit === 'hour') result.setMinutes(0, 0, 0);\n else {\n result.setHours(0, 0, 0, 0);\n if (unit === 'week') result.setDate(result.getDate() - result.getDay());\n if (unit === 'month') result.setDate(1);\n if (unit === 'year') result.setMonth(0, 1);\n }\n return result;\n}\n\n/** 往后数一个单位(按日历走:跨夏令时、大小月都对) */\nfunction nextTime(date: Date, unit: TimeUnit): Date {\n const result = new Date(date);\n if (unit === 'second') result.setSeconds(result.getSeconds() + 1);\n else if (unit === 'minute') result.setMinutes(result.getMinutes() + 1);\n else if (unit === 'hour') result.setHours(result.getHours() + 1);\n else if (unit === 'day') result.setDate(result.getDate() + 1);\n else if (unit === 'week') result.setDate(result.getDate() + 7);\n else if (unit === 'month') result.setMonth(result.getMonth() + 1);\n else result.setFullYear(result.getFullYear() + 1);\n return result;\n}\n\n/** 几个单位一格时按哪个字段取整:每 3 个月取 1、4、7、10 月,每 6 小时取 0、6、12、18 点 */\nfunction timeField(date: Date, unit: TimeUnit): number {\n if (unit === 'second') return date.getSeconds();\n if (unit === 'minute') return date.getMinutes();\n if (unit === 'hour') return date.getHours();\n if (unit === 'day') return date.getDate() - 1;\n if (unit === 'month') return date.getMonth();\n if (unit === 'year') return date.getFullYear();\n return 0;\n}\n\n/** [start, stop](毫秒)之间约 count 个整齐的时间刻度:整秒、整分、整点、日、周日、月初、年初(本地时间) */\nexport function timeTicks(start: number, stop: number, count = 6): Date[] {\n if (!Number.isFinite(start) || !Number.isFinite(stop) || stop < start) return [];\n const target = (stop - start) / count;\n const index = TIME_INTERVALS.findIndex(([, , duration]) => duration > target);\n let unit: TimeUnit;\n let every: number;\n if (index < 0) {\n // 比一年还长:年份按“好看”的步长\n unit = 'year';\n every = Math.max(1, tickIncrement(start / YEAR, stop / YEAR, count));\n } else if (index === 0) {\n unit = 'second';\n every = 1;\n } else {\n // 取与目标间隔之比更接近 1 的那一档\n const [before, after] = [TIME_INTERVALS[index - 1]!, TIME_INTERVALS[index]!];\n [unit, every] = target / before[2] < after[2] / target ? before : after;\n }\n const ticks: Date[] = [];\n for (let date = floorTime(new Date(start), unit); date.getTime() <= stop;) {\n if (date.getTime() >= start && timeField(date, unit) % every === 0) ticks.push(date);\n date = nextTime(date, unit);\n }\n return ticks;\n}\n"],"mappings":"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"}