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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/charts/AreaChart.d.ts +1 -1
  2. package/dist/charts/AreaChart.js +6 -48
  3. package/dist/charts/AreaChart.js.map +1 -1
  4. package/dist/charts/BarChart.js +6 -49
  5. package/dist/charts/BarChart.js.map +1 -1
  6. package/dist/charts/ChordChart.d.ts +7 -2
  7. package/dist/charts/ChordChart.js +151 -60
  8. package/dist/charts/ChordChart.js.map +1 -1
  9. package/dist/charts/ComboChart.d.ts +2 -1
  10. package/dist/charts/ComboChart.js +17 -93
  11. package/dist/charts/ComboChart.js.map +1 -1
  12. package/dist/charts/DonutChart.d.ts +6 -1
  13. package/dist/charts/DonutChart.js +134 -51
  14. package/dist/charts/DonutChart.js.map +1 -1
  15. package/dist/charts/FunnelChart.js.map +1 -1
  16. package/dist/charts/GraphChart.d.ts +22 -10
  17. package/dist/charts/GraphChart.js +415 -175
  18. package/dist/charts/GraphChart.js.map +1 -1
  19. package/dist/charts/LineChart.d.ts +1 -29
  20. package/dist/charts/LineChart.js +6 -54
  21. package/dist/charts/LineChart.js.map +1 -1
  22. package/dist/charts/MapChart.d.ts +21 -48
  23. package/dist/charts/MapChart.js +426 -353
  24. package/dist/charts/MapChart.js.map +1 -1
  25. package/dist/charts/RadarChart.js.map +1 -1
  26. package/dist/charts/SankeyChart.d.ts +7 -2
  27. package/dist/charts/SankeyChart.js +164 -76
  28. package/dist/charts/SankeyChart.js.map +1 -1
  29. package/dist/charts/ScatterChart.d.ts +7 -1
  30. package/dist/charts/ScatterChart.js +45 -30
  31. package/dist/charts/ScatterChart.js.map +1 -1
  32. package/dist/charts/TimelineChart.d.ts +8 -1
  33. package/dist/charts/TimelineChart.js +141 -89
  34. package/dist/charts/TimelineChart.js.map +1 -1
  35. package/dist/charts/TreemapChart.d.ts +6 -0
  36. package/dist/charts/TreemapChart.js +174 -58
  37. package/dist/charts/TreemapChart.js.map +1 -1
  38. package/dist/charts/WaterfallChart.d.ts +6 -7
  39. package/dist/charts/WaterfallChart.js +46 -51
  40. package/dist/charts/WaterfallChart.js.map +1 -1
  41. package/dist/charts/XYChart.d.ts +44 -35
  42. package/dist/charts/XYChart.js +493 -281
  43. package/dist/charts/XYChart.js.map +1 -1
  44. package/dist/charts/axis-ticks.d.ts +1 -1
  45. package/dist/charts/axis-ticks.js +4 -5
  46. package/dist/charts/axis-ticks.js.map +1 -1
  47. package/dist/charts/flow.d.ts +4 -11
  48. package/dist/charts/flow.js +4 -4
  49. package/dist/charts/flow.js.map +1 -1
  50. package/dist/charts/frame.d.ts +6 -30
  51. package/dist/charts/frame.js +6 -99
  52. package/dist/charts/frame.js.map +1 -1
  53. package/dist/charts/geo/projection.d.ts +45 -0
  54. package/dist/charts/geo/projection.js +201 -0
  55. package/dist/charts/geo/projection.js.map +1 -0
  56. package/dist/charts/geo/topojson.d.ts +50 -0
  57. package/dist/charts/geo/topojson.js +119 -0
  58. package/dist/charts/geo/topojson.js.map +1 -0
  59. package/dist/charts/index.d.ts +21 -9
  60. package/dist/charts/index.js +19 -8
  61. package/dist/charts/label-layout.js.map +1 -1
  62. package/dist/charts/layout/chord.d.ts +44 -0
  63. package/dist/charts/layout/chord.js +45 -0
  64. package/dist/charts/layout/chord.js.map +1 -0
  65. package/dist/charts/layout/graph.d.ts +49 -0
  66. package/dist/charts/layout/graph.js +268 -0
  67. package/dist/charts/layout/graph.js.map +1 -0
  68. package/dist/charts/layout/sankey.d.ts +79 -0
  69. package/dist/charts/layout/sankey.js +200 -0
  70. package/dist/charts/layout/sankey.js.map +1 -0
  71. package/dist/charts/layout/treemap.d.ts +46 -0
  72. package/dist/charts/layout/treemap.js +152 -0
  73. package/dist/charts/layout/treemap.js.map +1 -0
  74. package/dist/charts/marks.d.ts +79 -0
  75. package/dist/charts/marks.js +231 -0
  76. package/dist/charts/marks.js.map +1 -0
  77. package/dist/charts/plot.d.ts +107 -0
  78. package/dist/charts/plot.js +231 -0
  79. package/dist/charts/plot.js.map +1 -0
  80. package/dist/charts/scale.d.ts +29 -0
  81. package/dist/charts/scale.js +244 -0
  82. package/dist/charts/scale.js.map +1 -0
  83. package/dist/charts/shape.d.ts +46 -0
  84. package/dist/charts/shape.js +131 -0
  85. package/dist/charts/shape.js.map +1 -0
  86. package/dist/charts/stack.d.ts +17 -0
  87. package/dist/charts/stack.js +35 -0
  88. package/dist/charts/stack.js.map +1 -0
  89. package/dist/charts/text-size.d.ts +12 -0
  90. package/dist/charts/text-size.js +32 -0
  91. package/dist/charts/text-size.js.map +1 -0
  92. package/dist/config/createUi.d.ts +21 -2
  93. package/dist/config/createUi.js +18 -1
  94. package/dist/config/createUi.js.map +1 -1
  95. package/dist/config/theme/preference.js +2 -1
  96. package/dist/config/theme/preference.js.map +1 -1
  97. package/dist/index.d.ts +2 -2
  98. package/dist/maps/china-provinces.js +35680 -0
  99. package/dist/maps/china-provinces.js.map +1 -0
  100. package/dist/maps/china.d.ts +13 -0
  101. package/dist/maps/china.js +8 -0
  102. package/dist/maps/china.js.map +1 -0
  103. package/dist/maps/index.d.ts +9 -0
  104. package/dist/maps/index.js +3 -0
  105. package/dist/maps/world-countries.js +22273 -0
  106. package/dist/maps/world-countries.js.map +1 -0
  107. package/dist/maps/world.d.ts +13 -0
  108. package/dist/maps/world.js +8 -0
  109. package/dist/maps/world.js.map +1 -0
  110. package/dist/overlay/detached.js.map +1 -1
  111. package/dist/utils/request/use-request.d.ts +25 -1
  112. package/dist/utils/request/use-request.js +35 -3
  113. package/dist/utils/request/use-request.js.map +1 -1
  114. package/package.json +7 -8
  115. package/dist/charts/single.d.ts +0 -41
  116. package/dist/charts/single.js +0 -66
  117. package/dist/charts/single.js.map +0 -1
@@ -1,43 +1,35 @@
1
+ import { svgNumber } from "../utils/data/number.js";
1
2
  import { toNumber } from "../utils/data/numeric.js";
2
- import { colors } from "../config/theme/tokens.js";
3
3
  import { useLang } from "../config/context.js";
4
4
  import { Button } from "../base/Button.js";
5
5
  import { useFormat } from "../config/lang/format.js";
6
6
  import { Skeleton } from "../feedback/Skeleton.js";
7
- import { useDetachedRender } from "../overlay/detached.js";
8
7
  import { Slider } from "../input/Slider.js";
8
+ import { ChartTooltipBox, chartAxis, chartSvgStyles, pointerIn, usePlotArea } from "./plot.js";
9
+ import { stackValues } from "./stack.js";
10
+ import { linearScale, linearTicks, niceDomain } from "./scale.js";
9
11
  import { alignedTicks } from "./axis-ticks.js";
10
- import { ChartFrame, chartDuration, chartTooltip, createChartInstances, uniqueHighlightFilter, useChartValueText, usePlotSeen } from "./frame.js";
12
+ import { ChartFrame, chartTooltip, useChartValueText } from "./frame.js";
11
13
  import { resolveColor, resolveSeries, seriesNumber, seriesValue } from "./series.js";
12
- import { computed, createVNode, shallowRef, useId } from "vue";
13
- import { _component, bindable, effect, mergeJsxProps, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
14
+ import { estimateTextWidth, truncateText } from "./text-size.js";
15
+ import { computed, createVNode, shallowRef } from "vue";
16
+ import { _component, bindable, mergeJsxProps, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
14
17
  import { css } from "@zerodep-dev/vue-tsx-pro-tsx/styles";
15
- import { Axis, AxisType, Crosshair, Plotband, PlotbandLabelPosition, Plotline, PlotlineLabelPosition, PlotlineLineStylePresets, Position, Scatter, Tooltip, XYContainer } from "@unovis/ts";
16
18
  //#region src/charts/XYChart.tsx
17
- /**
18
- * 直角坐标图表(柱状图、折线图、面积图)的公共部分:坐标轴、十字线与提示框、数值范围;外层结构(图例、单位、
19
- * 空与加载状态、给读屏的数据表)交给 ChartFrame。
20
- * 绘图交给 Unovis 的 XYContainer:组件实例只建一次,数据与配置变化时 update,卸载时 destroy;
21
- * 绘图区换了元素(加载、空状态之后重新出现)时容器连同全部实例一起重建。
22
- * 各种图只决定画什么(kind.components),坐标轴、十字线、提示框、配色都在这里统一。
23
- * 目标线(targets)用 Unovis 的 Plotline。点了下钻(onItemClick):有图形可点的图(柱子)按图形的点击事件
24
- * (kind.itemSelector),折线、面积图按十字线所在的那一行,散点图按悬停的点。
25
- *
26
- * 标注(annotations):类目上的事件画成竖线(Plotline),圈出某个系列的值画成空心圈带文字(Scatter);
27
- * 区间(bands)画成浅色的带(Plotband)。百分比堆叠(kind.percent)按每个类目里各系列的占比画。
28
- * 第二数值轴(kind.secondary,组合图):刻度与主轴对齐(alignedTicks),第二轴的系列换算到主轴的坐标里画,
29
- * 右侧的刻度文字是不裁剪的 Plotline 标签。
30
- */
31
19
  /** 目标线(参考线):数值轴上的一条虚线 */
32
20
  /** 标注:类目上发生的事(竖线),或圈出某个系列在这个类目上的值 */
33
21
  /** 区间:类目轴上的一段(试运行期、促销期),或数值轴上的一段(目标区间) */
34
22
  /** 第二数值轴(右侧):哪些系列画在它上面,它的格式与单位 */
35
23
  /** 加载中的骨架 */
36
24
  /** 直角坐标图表共有的属性;未声明的属性落到外层 div(role="figure")上,aria-label 是图表的名称 */
37
- /** 交给各种图的绘制上下文 */
38
- /** 一种图:画哪些组件,以及与坐标轴、十字线有关的差别 */
39
- /** 第二数值轴的刻度文字占的宽度(px) */
40
- var SECONDARY_AXIS_WIDTH = 48;
25
+ /** 交给各种图的绘制上下文:坐标都是 SVG 里的 px */
26
+ /** 一种图:画什么,以及与坐标轴、十字线有关的差别 */
27
+ /** 刻度文字的字号(px):主题的 xs */
28
+ var TICK_FONT = 12;
29
+ /** 刻度文字与绘图区的距离 */
30
+ var TICK_GAP = 8;
31
+ /** 散点图找最近的点时,离指针多远以内算悬停在点上(px) */
32
+ var POINT_REACH = 24;
41
33
  /** 骨架柱的高度(百分比):高低错落,看起来像一组数据 */
42
34
  var SKELETON_BARS = [
43
35
  55,
@@ -48,9 +40,9 @@ var SKELETON_BARS = [
48
40
  50,
49
41
  65
50
42
  ];
51
- /** XYChart 的样式:hideCrosshairLine 与加载中的骨架(外层结构的样式见 chartStyles) */
43
+ /** XYChart 的样式:缩放的滑块、加载中的骨架、标注(外层结构的样式见 chartStyles) */
52
44
  var xyChartStyles = css.create({
53
- hideCrosshairLine: { "--vis-crosshair-line-stroke-opacity": "0" },
45
+ svg: { touchAction: "pan-y" },
54
46
  skeleton: {
55
47
  display: "block",
56
48
  width: "100%",
@@ -84,7 +76,29 @@ var xyChartStyles = css.create({
84
76
  borderRadius: "_sm"
85
77
  },
86
78
  skeletonBarSize: (percent) => ({ height: percent }),
87
- skeletonBarSizeHorizontal: (percent) => ({ width: percent })
79
+ skeletonBarSizeHorizontal: (percent) => ({ width: percent }),
80
+ targetLine: {
81
+ strokeWidth: 1.5,
82
+ strokeDasharray: "6 4",
83
+ pointerEvents: "none"
84
+ },
85
+ eventLine: {
86
+ strokeWidth: 1,
87
+ strokeDasharray: "3 3",
88
+ pointerEvents: "none"
89
+ },
90
+ mark: {
91
+ fill: "none",
92
+ strokeWidth: 2,
93
+ pointerEvents: "none"
94
+ },
95
+ crosshairDot: {
96
+ stroke: "_surface",
97
+ strokeWidth: 2,
98
+ pointerEvents: "none"
99
+ },
100
+ bandFill: (color) => ({ fill: `color-mix(in srgb, ${color} 12%, transparent)` }),
101
+ textFill: (color) => ({ fill: color })
88
102
  });
89
103
  /** 加载中绘图区的骨架:柱状图是一排高低不一的柱(横向时是条),其余是一整块 */
90
104
  function chartSkeleton({ skeleton = "block", horizontal = false }) {
@@ -94,6 +108,9 @@ function chartSkeleton({ skeleton = "block", horizontal = false }) {
94
108
  "sx": [xyChartStyles.skeletonBar, horizontal ? xyChartStyles.skeletonBarSizeHorizontal(`${percent}%`) : xyChartStyles.skeletonBarSize(`${percent}%`)]
95
109
  }, null))]);
96
110
  }
111
+ /** 数值轴上约几格:每格不少于 45px(横着的轴 90px),2–6 格 */
112
+ var tickCount = (length, horizontal) => Math.max(2, Math.min(6, Math.round(length / (horizontal ? 90 : 45))));
113
+ /** 指针悬停的状态:行的位置、提示框的位置 */
97
114
  /** 直角坐标图表的公共实现(组件库内部用,见 BarChart、LineChart、AreaChart) */
98
115
  var XYChart = /*#__PURE__*/ _component((_props, _ctx) => {
99
116
  const _cache = [];
@@ -118,13 +135,13 @@ var XYChart = /*#__PURE__*/ _component((_props, _ctx) => {
118
135
  const zoom = readonlyProp(_props, "zoom", false);
119
136
  const zoomRange = bindable(_props, "_zoomRange", () => null, _ctx.emit);
120
137
  const kind = readonlyProp(_props, "kind");
121
- const sx = readonlyProp(_props, "sx");
122
138
  const rest = restProps(_props, _ctx, true);
123
139
  const lang = useLang();
124
140
  const format = useFormat();
125
- const id = useId();
141
+ const area = usePlotArea();
126
142
  const resolved = computed(() => resolveSeries(series.value));
127
143
  const visible = computed(() => resolved.value.filter((item) => !hiddenSeries.value.includes(item.key)));
144
+ let hover = shallowRef(null);
128
145
  const categoryText = (row) => row === void 0 ? "" : indexFormat.value ? indexFormat.value(row[index.value], row) : String(row[index.value] ?? "");
129
146
  const valueText = useChartValueText(() => ({
130
147
  valueFormat: valueFormat.value,
@@ -173,21 +190,9 @@ var XYChart = /*#__PURE__*/ _component((_props, _ctx) => {
173
190
  $stable: true
174
191
  })]);
175
192
  };
176
- const instances = createChartInstances();
177
- let tooltipElement;
178
- const renderDetached = useDetachedRender();
179
- let hovered = null;
180
- const hover = (row) => {
181
- hovered = row === void 0 ? null : {
182
- row,
183
- index: data.value.indexOf(row)
184
- };
185
- };
186
- /** 十字线的提示框:内容用 Vue 渲染进同一个元素,交给 Unovis 定位 */
187
- function renderTooltip(row) {
188
- tooltipElement ??= document.createElement("div");
189
- if (kind.value.pointTooltip) hover(row);
190
- renderDetached(chartTooltip(categoryText(row), kind.value.tooltipItems?.(row) ?? [...kind.value.continuousX ? [{
193
+ /** 提示框的内容:类目(或点的名字)与各系列的值 */
194
+ function tooltipContent(row) {
195
+ return chartTooltip(categoryText(row), kind.value.tooltipItems?.(row) ?? [...kind.value.continuousX ? [{
191
196
  label: kind.value.continuousX.label,
192
197
  color: "transparent",
193
198
  value: xText(row)
@@ -199,248 +204,466 @@ var XYChart = /*#__PURE__*/ _component((_props, _ctx) => {
199
204
  color: item.color,
200
205
  value: kind.value.percent && value !== void 0 && total > 0 ? `${seriesText(item.key, value)}(${percentText(toNumber(value) / total)})` : seriesText(item.key, value)
201
206
  };
202
- })]), tooltipElement);
203
- return tooltipElement;
207
+ })]);
204
208
  }
205
- let plot = shallowRef(null);
206
- const seen = usePlotSeen(() => plot.value);
207
- let container;
208
- let containerElement;
209
- effect(() => () => {
210
- container?.destroy();
209
+ /** 缩放:只画选中的一段(散点图的横轴是连续的数值,不按类目缩放) */
210
+ const range = () => zoom.value && !kind.value.continuousX ? zoomRange.value : null;
211
+ const shownRows = computed(() => {
212
+ const selected = range();
213
+ return selected ? data.value.slice(selected[0], selected[1] + 1) : data.value;
211
214
  });
212
- effect(() => {
213
- const range = zoom.value && !kind.value.continuousX ? zoomRange.value : null;
214
- const rows = range ? data.value.slice(range[0], range[1] + 1) : [...data.value];
215
+ /** 画图要的一切:比例尺、刻度、边距;随数据、属性、绘图区大小变 */
216
+ const layout = computed(() => {
217
+ const rows = shownRows.value;
218
+ const width = area.width;
219
+ const plotHeight = area.height;
220
+ const horizontal = kind.value.horizontal ?? false;
215
221
  const shown = visible.value;
216
- const element = plot.value;
217
- if (!element || loading.value || rows.length === 0 || !seen()) return;
218
- if (container && containerElement !== element) {
219
- container.destroy();
220
- container = void 0;
221
- instances.reset();
222
- }
223
222
  const primarySeries = shown.filter((item) => !onSecondary(item.key));
224
223
  const secondarySeries = shown.filter((item) => onSecondary(item.key));
225
224
  const totals = rows.map((row) => primarySeries.map((item) => seriesNumber(row, item.key) ?? 0));
226
225
  const values = [
227
- ...kind.value.domainValues ? kind.value.domainValues(rows, primarySeries) : kind.value.stacked ? totals.flatMap((row) => [row.reduce((sum, value) => sum + Math.max(value, 0), 0), row.reduce((sum, value) => sum + Math.min(value, 0), 0)]) : totals.flat(),
226
+ ...kind.value.domainValues ? kind.value.domainValues(rows, primarySeries) : kind.value.stacked ? totals.flatMap((row) => [row.reduce((sum, value) => sum + Math.max(value, 0), 0), row.reduce((sum, value) => sum + Math.min(value, 0), 0)]) : rows.flatMap((row) => primarySeries.flatMap((item) => seriesNumber(row, item.key) ?? [])),
228
227
  ...(targets.value ?? []).map((target) => toNumber(target.value)),
229
228
  ...(bands.value ?? []).filter((band) => band.axis === "value").flatMap((band) => [Number(band.from), Number(band.to)])
230
- ];
229
+ ].filter(Number.isFinite);
231
230
  const aligned = secondarySeries.length > 0 ? alignedTicks([...values, ...[valueDomain.value?.[0], valueDomain.value?.[1]].filter((end) => end !== void 0)], rows.flatMap((row) => secondarySeries.map((item) => seriesNumber(row, item.key) ?? 0))) : void 0;
232
- const domain = kind.value.percent ? [0, 1] : aligned ? aligned.primary.domain : [valueDomain.value?.[0] ?? (kind.value.zeroBaseline === false ? void 0 : Math.min(0, ...values)), valueDomain.value?.[1]];
233
- const x = kind.value.continuousX ? (row) => kind.value.continuousX.value(row) ?? NaN : (_row, position) => position;
234
- const y = (item) => {
235
- if (kind.value.percent) return (row) => {
236
- const value = seriesNumber(row, item.key);
231
+ const valueLength = horizontal ? width : plotHeight;
232
+ let domain;
233
+ let ticks;
234
+ if (kind.value.percent) {
235
+ domain = [0, 1];
236
+ ticks = linearTicks(0, 1, tickCount(valueLength, horizontal));
237
+ } else if (aligned) {
238
+ domain = aligned.primary.domain;
239
+ ticks = aligned.primary.ticks;
240
+ } else {
241
+ const fromZero = kind.value.zeroBaseline !== false;
242
+ const low = values.length ? Math.min(...values, ...fromZero ? [0] : []) : 0;
243
+ const high = values.length ? Math.max(...values, ...fromZero ? [0] : []) : 1;
244
+ const [autoLow, autoHigh] = low === high ? [low - 1, high + 1] : [low, high];
245
+ const nice = niceDomain([valueDomain.value?.[0] ?? autoLow, valueDomain.value?.[1] ?? autoHigh], tickCount(valueLength, horizontal));
246
+ domain = [valueDomain.value?.[0] ?? nice[0], valueDomain.value?.[1] ?? nice[1]];
247
+ ticks = linearTicks(domain[0], domain[1], tickCount(valueLength, horizontal));
248
+ }
249
+ const valueLabel = (tick) => kind.value.percent ? format.number(tick, {
250
+ style: "percent",
251
+ maximumFractionDigits: 0
252
+ }) : format.number(tick, valueFormat.value);
253
+ const valueLabels = ticks.map(valueLabel);
254
+ const secondaryLabels = aligned ? aligned.secondary.ticks.map((tick) => format.number(tick, kind.value.secondary?.format)) : [];
255
+ const continuous = kind.value.continuousX;
256
+ const xValues = continuous ? rows.map((row) => continuous.value(row)).filter((x) => x !== void 0) : [];
257
+ const xDomain = niceDomain(xValues.length ? [Math.min(...xValues), Math.max(...xValues)] : [0, 1], tickCount(width, true));
258
+ const xTicks = continuous ? linearTicks(xDomain[0], xDomain[1], tickCount(width, true)) : [];
259
+ const widest = (labels) => Math.max(0, ...labels.map((label) => estimateTextWidth(label, TICK_FONT)));
260
+ const categoryLabels = rows.map((row) => categoryText(row));
261
+ const padding = {
262
+ left: 0,
263
+ right: 0,
264
+ top: 0,
265
+ bottom: 0,
266
+ ...kind.value.padding
267
+ };
268
+ const bottomLabels = horizontal ? valueLabels : continuous ? xTicks.map(continuous.format) : categoryLabels;
269
+ const margin = horizontal ? {
270
+ left: Math.min(widest(categoryLabels), width * .3) + TICK_GAP,
271
+ right: Math.max(8, widest(valueLabels.slice(-1)) / 2),
272
+ top: 4,
273
+ bottom: 22
274
+ } : {
275
+ left: widest(valueLabels) + TICK_GAP,
276
+ right: aligned ? widest(secondaryLabels) + TICK_GAP : 8,
277
+ top: 8,
278
+ bottom: 22 + (continuous?.title ? 20 : 0)
279
+ };
280
+ if (!horizontal && bottomLabels.length && !kind.value.band) {
281
+ margin.left = Math.max(margin.left, widest(bottomLabels.slice(0, 1)) / 2 - padding.left);
282
+ margin.right = Math.max(margin.right, widest(bottomLabels.slice(-1)) / 2 - padding.right);
283
+ }
284
+ const plot = {
285
+ left: margin.left,
286
+ top: margin.top,
287
+ width: Math.max(0, width - margin.left - margin.right),
288
+ height: Math.max(0, plotHeight - margin.top - margin.bottom)
289
+ };
290
+ const [cStart, cEnd] = horizontal ? [plot.top + padding.top, plot.top + plot.height - padding.bottom] : [plot.left + padding.left, plot.left + plot.width - padding.right];
291
+ const count = rows.length;
292
+ const step = kind.value.band ? (cEnd - cStart) / Math.max(count, 1) : count > 1 ? (cEnd - cStart) / (count - 1) : cEnd - cStart;
293
+ const xScale = linearScale(xDomain, [cStart, cEnd]);
294
+ const category = continuous ? (row) => xScale(continuous.value(row) ?? NaN) : (_row, position) => kind.value.band ? cStart + step * (position + .5) : count > 1 ? cStart + step * position : (cStart + cEnd) / 2;
295
+ const valueScale = horizontal ? linearScale(domain, [plot.left + padding.left, plot.left + plot.width - padding.right]) : linearScale(domain, [plot.top + plot.height - padding.bottom, plot.top + padding.top]);
296
+ const valueOf = (item, row) => {
297
+ const value = seriesNumber(row, item.key);
298
+ if (value === void 0) return void 0;
299
+ if (kind.value.percent) {
237
300
  const total = rowTotal(row);
238
- return value === void 0 || total === 0 ? void 0 : value / total;
239
- };
240
- if (aligned && onSecondary(item.key)) return (row) => {
241
- const value = seriesNumber(row, item.key);
242
- return value === void 0 ? void 0 : aligned.toPrimary(value);
243
- };
244
- return (row) => seriesNumber(row, item.key);
301
+ return total === 0 ? void 0 : value / total;
302
+ }
303
+ return aligned && onSecondary(item.key) ? aligned.toPrimary(value) : value;
245
304
  };
246
- /** 类目在类目轴上的位置:散点图是横轴的数值,其余是第几行;找不到时 undefined */
247
- const positionOf = (at) => {
248
- if (kind.value.continuousX) return Number(at);
249
- const found = rows.findIndex((row) => row[index.value] === at);
250
- return found < 0 ? void 0 : found;
305
+ const stacked = kind.value.stacked ? shown.filter(kind.value.stackSeries ?? (() => true)) : [];
306
+ const segments = stackValues(stacked.map((item) => rows.map((row) => valueOf(item, row))));
307
+ const stacks = new Map(stacked.map((item, s) => [item.key, segments[s]]));
308
+ const every = continuous ? 1 : Math.max(1, Math.ceil((horizontal ? 16 : widest(categoryLabels) + 12) / Math.max(step, 1)));
309
+ const categoryTicks = continuous ? xTicks.map((tick) => ({
310
+ position: xScale(tick),
311
+ label: continuous.format(tick)
312
+ })) : rows.map((row, position) => ({
313
+ position: category(row, position),
314
+ label: position % every === 0 ? horizontal ? truncateText(categoryLabels[position], width * .3, TICK_FONT) : categoryLabels[position] : ""
315
+ }));
316
+ const valueTicks = ticks.map((tick, position) => ({
317
+ position: valueScale(tick),
318
+ label: valueLabels[position]
319
+ }));
320
+ const secondaryTicks = aligned ? aligned.primary.ticks.map((tick, position) => ({
321
+ position: valueScale(tick),
322
+ label: secondaryLabels[position]
323
+ })) : [];
324
+ return {
325
+ rows,
326
+ shown,
327
+ horizontal,
328
+ plot,
329
+ step,
330
+ domain,
331
+ category,
332
+ valueScale,
333
+ valueOf,
334
+ stacks,
335
+ categoryTicks,
336
+ valueTicks,
337
+ secondaryTicks
251
338
  };
252
- const categoryAxisName = kind.value.horizontal ? AxisType.Y : AxisType.X;
253
- const valueAxisName = kind.value.horizontal ? AxisType.X : AxisType.Y;
254
- const lines = (targets.value ?? []).map((target, position) => {
255
- const line = instances.get(`target-${position}`, () => new Plotline({}));
256
- const value = toNumber(target.value);
257
- const label = valueText(target.value);
258
- line.setConfig({
259
- axis: kind.value.horizontal ? "x" : "y",
260
- value,
261
- color: resolveColor(target.color ?? "danger", 0),
262
- lineWidth: 1.5,
263
- lineStyle: PlotlineLineStylePresets.Dash,
264
- labelText: target.label ? `${target.label} ${label}` : label,
265
- labelPosition: kind.value.horizontal ? PlotlineLabelPosition.TopRight : PlotlineLabelPosition.TopLeft,
266
- labelOffsetX: 4,
267
- labelOffsetY: 10
339
+ });
340
+ /** 一个类目在类目轴上的位置(标注、区间用):散点图是横轴的数值,其余是第几行;找不到时 undefined */
341
+ function positionOf(at) {
342
+ const { rows, category } = layout.value;
343
+ if (kind.value.continuousX) {
344
+ const row = rows.find((item) => kind.value.continuousX.value(item) === Number(at));
345
+ return row ? category(row, 0) : void 0;
346
+ }
347
+ const found = rows.findIndex((row) => row[index.value] === at);
348
+ return found < 0 ? void 0 : category(rows[found], found);
349
+ }
350
+ /** 指针移动:找出悬停的行(散点图是最近的点) */
351
+ function onPointermove(event) {
352
+ const svg = event.currentTarget;
353
+ const { x, y } = pointerIn(event, svg);
354
+ const { rows, plot, horizontal, category, valueScale, valueOf, shown, step } = layout.value;
355
+ if (!(x >= plot.left - 4 && x <= plot.left + plot.width + 4 && y >= plot.top - 4 && y <= plot.top + plot.height + 4) || rows.length === 0) {
356
+ hover.value = null;
357
+ return;
358
+ }
359
+ if (kind.value.pointTooltip) {
360
+ let best = null;
361
+ let distance = POINT_REACH;
362
+ rows.forEach((row, position) => {
363
+ for (const item of shown) {
364
+ const value = valueOf(item, row);
365
+ if (value === void 0) continue;
366
+ const [px, py] = [category(row, position), valueScale(value)];
367
+ const d = Math.hypot(px - x, py - y);
368
+ if (d < distance) {
369
+ distance = d;
370
+ best = {
371
+ index: position,
372
+ x: px,
373
+ y: py
374
+ };
375
+ }
376
+ }
268
377
  });
269
- return line;
378
+ hover.value = best;
379
+ return;
380
+ }
381
+ const along = horizontal ? y : x;
382
+ let nearest = 0;
383
+ rows.forEach((row, position) => {
384
+ if (Math.abs(category(row, position) - along) < Math.abs(category(rows[nearest], nearest) - along)) nearest = position;
270
385
  });
271
- const bandComponents = (bands.value ?? []).flatMap((band, position) => {
386
+ const center = category(rows[nearest], nearest);
387
+ if (Math.abs(center - along) > Math.max(step, 8)) {
388
+ hover.value = null;
389
+ return;
390
+ }
391
+ hover.value = horizontal ? {
392
+ index: nearest,
393
+ x,
394
+ y: center
395
+ } : {
396
+ index: nearest,
397
+ x: center,
398
+ y
399
+ };
400
+ }
401
+ /** 点击下钻:点在柱上按那根柱(data-index),其余按悬停的行 */
402
+ function onClick(event) {
403
+ if (!onItemClick.value) return;
404
+ const target = event.target?.closest?.("[data-index]");
405
+ const position = target ? Number(target.getAttribute("data-index")) : hover.value?.index;
406
+ if (position === void 0 || Number.isNaN(position)) return;
407
+ const offset = range()?.[0] ?? 0;
408
+ const row = layout.value.rows[position];
409
+ if (row !== void 0) onItemClick.value(row, offset + position);
410
+ }
411
+ /** 区间:浅色的带,文字在左下角(类目轴)或左上角(数值轴) */
412
+ function renderBands() {
413
+ const { plot, horizontal, step, valueScale } = layout.value;
414
+ return (bands.value ?? []).map((band, position) => {
272
415
  const onIndex = band.axis !== "value";
273
- const from = onIndex ? positionOf(band.from) : Number(band.from);
274
- const to = onIndex ? positionOf(band.to) : Number(band.to);
275
- if (from === void 0 || to === void 0) return [];
276
- const half = onIndex && !kind.value.continuousX ? .5 : 0;
277
- const plotband = instances.get(`band-${position}`, () => new Plotband({}));
278
- plotband.setConfig({
279
- axis: onIndex ? categoryAxisName : valueAxisName,
280
- from: Math.min(from, to) - half,
281
- to: Math.max(from, to) + half,
282
- color: `color-mix(in srgb, ${resolveColor(band.color ?? "neutral", 0)} 12%, transparent)`,
283
- labelText: band.label,
284
- labelPosition: onIndex ? PlotbandLabelPosition.BottomLeftInside : PlotbandLabelPosition.TopLeftInside,
285
- labelColor: colors.textMuted,
286
- labelOffsetX: 6,
287
- labelOffsetY: 6
288
- });
289
- return [plotband];
416
+ const color = resolveColor(band.color ?? "neutral", 0);
417
+ let from;
418
+ let to;
419
+ if (onIndex) {
420
+ const a = positionOf(band.from);
421
+ const b = positionOf(band.to);
422
+ if (a === void 0 || b === void 0) return null;
423
+ const half = kind.value.continuousX ? 0 : step / 2;
424
+ from = Math.min(a, b) - half;
425
+ to = Math.max(a, b) + half;
426
+ } else {
427
+ from = Math.min(valueScale(Number(band.from)), valueScale(Number(band.to)));
428
+ to = Math.max(valueScale(Number(band.from)), valueScale(Number(band.to)));
429
+ }
430
+ const alongCategory = onIndex !== horizontal;
431
+ const clampedFrom = Math.max(from, alongCategory ? plot.left : plot.top);
432
+ const clampedTo = Math.min(to, alongCategory ? plot.left + plot.width : plot.top + plot.height);
433
+ const rect = alongCategory ? {
434
+ x: clampedFrom,
435
+ y: plot.top,
436
+ width: clampedTo - clampedFrom,
437
+ height: plot.height
438
+ } : {
439
+ x: plot.left,
440
+ y: clampedFrom,
441
+ width: plot.width,
442
+ height: clampedTo - clampedFrom
443
+ };
444
+ if (rect.width <= 0 || rect.height <= 0) return null;
445
+ return createVNode("g", {
446
+ "key": `band-${position}`,
447
+ "data-part": "band"
448
+ }, [createVNode("rect", mergeJsxProps(rect, css.attrs(xyChartStyles.bandFill(color), chartSvgStyles.noPointer)), null), band.label && createVNode("text", mergeJsxProps({
449
+ "x": rect.x + 6,
450
+ "y": onIndex && !horizontal ? rect.y + rect.height - 6 : rect.y + 6,
451
+ "dominant-baseline": onIndex && !horizontal ? "auto" : "hanging"
452
+ }, css.attrs(chartSvgStyles.labelMuted, chartSvgStyles.noPointer)), [band.label])]);
453
+ });
454
+ }
455
+ /** 目标线与事件竖线 */
456
+ function renderLines() {
457
+ const { plot, horizontal, valueScale } = layout.value;
458
+ const right = plot.left + plot.width;
459
+ const bottom = plot.top + plot.height;
460
+ const targetLines = (targets.value ?? []).map((target, position) => {
461
+ const color = resolveColor(target.color ?? "danger", 0);
462
+ const at = svgNumber(valueScale(toNumber(target.value)));
463
+ const label = target.label ? `${target.label} ${valueText(target.value)}` : valueText(target.value);
464
+ return createVNode("g", {
465
+ "key": `target-${position}`,
466
+ "data-part": "target"
467
+ }, [horizontal ? createVNode("line", mergeJsxProps({
468
+ "x1": at,
469
+ "x2": at,
470
+ "y1": plot.top,
471
+ "y2": bottom
472
+ }, css.attrs(xyChartStyles.targetLine, chartSvgStyles.stroke(color))), null) : createVNode("line", mergeJsxProps({
473
+ "x1": plot.left,
474
+ "x2": right,
475
+ "y1": at,
476
+ "y2": at
477
+ }, css.attrs(xyChartStyles.targetLine, chartSvgStyles.stroke(color))), null), createVNode("text", mergeJsxProps({
478
+ "x": horizontal ? at + 4 : plot.left + 4,
479
+ "y": horizontal ? plot.top + 4 : at - 5,
480
+ "dominant-baseline": horizontal ? "hanging" : "auto"
481
+ }, css.attrs(chartSvgStyles.labelMuted, chartSvgStyles.halo, xyChartStyles.textFill(color), chartSvgStyles.noPointer)), [label])]);
290
482
  });
291
- const eventLines = (annotations.value ?? []).filter((item) => item.series === void 0).flatMap((item, position) => {
483
+ const events = (annotations.value ?? []).filter((item) => item.series === void 0).map((item, position) => {
292
484
  const at = positionOf(item.at);
293
- if (at === void 0) return [];
294
- const line = instances.get(`event-${position}`, () => new Plotline({}));
295
- line.setConfig({
296
- axis: categoryAxisName,
297
- value: at,
298
- color: resolveColor(item.color ?? "neutral", 0),
299
- lineWidth: 1,
300
- lineStyle: PlotlineLineStylePresets.ShortDash,
301
- labelText: item.label,
302
- labelPosition: kind.value.horizontal ? PlotlineLabelPosition.BottomRight : PlotlineLabelPosition.TopRight,
303
- labelOffsetX: 4,
304
- labelOffsetY: 4
305
- });
306
- return [line];
485
+ if (at === void 0) return null;
486
+ const color = resolveColor(item.color ?? "neutral", 0);
487
+ const p = svgNumber(at);
488
+ return createVNode("g", {
489
+ "key": `event-${position}`,
490
+ "data-part": "event"
491
+ }, [horizontal ? createVNode("line", mergeJsxProps({
492
+ "x1": plot.left,
493
+ "x2": right,
494
+ "y1": p,
495
+ "y2": p
496
+ }, css.attrs(xyChartStyles.eventLine, chartSvgStyles.stroke(color))), null) : createVNode("line", mergeJsxProps({
497
+ "x1": p,
498
+ "x2": p,
499
+ "y1": plot.top,
500
+ "y2": bottom
501
+ }, css.attrs(xyChartStyles.eventLine, chartSvgStyles.stroke(color))), null), createVNode("text", mergeJsxProps({
502
+ "x": horizontal ? right - 4 : p + 4,
503
+ "y": horizontal ? p - 4 : plot.top + 4,
504
+ "text-anchor": horizontal ? "end" : "start",
505
+ "dominant-baseline": horizontal ? "auto" : "hanging"
506
+ }, css.attrs(chartSvgStyles.labelMuted, chartSvgStyles.halo, chartSvgStyles.noPointer)), [item.label])]);
307
507
  });
308
- const marked = (annotations.value ?? []).filter((item) => item.series !== void 0 && !kind.value.horizontal);
309
- const markOf = (row) => {
310
- const at = kind.value.continuousX ? kind.value.continuousX.value(row) : rows.indexOf(row);
311
- return marked.find((item) => positionOf(item.at) === at && shown.some((series) => series.key === item.series));
312
- };
313
- /** 圈的位置:系列的值;堆叠时是它在这一摞里的顶端 */
314
- const markY = (row) => {
315
- const mark = markOf(row);
316
- if (!mark) return void 0;
317
- const series = shown.find((item) => item.key === mark.series);
318
- if (!kind.value.stacked || kind.value.percent) return y(series)(row);
319
- return shown.slice(0, shown.indexOf(series) + 1).reduce((sum, item) => sum + Math.max(y(item)(row) ?? 0, 0), 0);
320
- };
321
- const markColor = (row) => {
322
- const mark = markOf(row);
323
- const series = shown.find((item) => item.key === mark?.series);
324
- return mark?.color ? resolveColor(mark.color, 0) : series?.color ?? colors.textMuted;
325
- };
326
- const marks = instances.get("marks", () => new Scatter({
327
- x,
328
- y: () => void 0
329
- }));
330
- marks.setConfig({
331
- x,
332
- y: markY,
333
- size: 16,
334
- color: "transparent",
335
- strokeColor: markColor,
336
- strokeWidth: 2,
337
- label: (row) => markOf(row)?.label ?? "",
338
- labelPosition: Position.Top,
339
- labelColor: colors.text
508
+ return [...targetLines, ...events];
509
+ }
510
+ /** 一个系列在一行上的值的坐标:堆叠时是它在这一摞里的外端 */
511
+ function valuePosition(item, row, position) {
512
+ const segment = layout.value.stacks.get(item.key)?.[position];
513
+ if (segment) return segment.value === void 0 ? void 0 : segment.value < 0 ? segment.low : segment.high;
514
+ return layout.value.valueOf(item, row);
515
+ }
516
+ /** 标注圈出的值:空心圈,文字在上方 */
517
+ function renderMarks() {
518
+ if (layout.value.horizontal) return null;
519
+ return (annotations.value ?? []).filter((item) => item.series !== void 0).map((item, position) => {
520
+ const series = layout.value.shown.find((entry) => entry.key === item.series);
521
+ const rowIndex = kind.value.continuousX ? layout.value.rows.findIndex((row) => kind.value.continuousX.value(row) === Number(item.at)) : layout.value.rows.findIndex((row) => row[index.value] === item.at);
522
+ const row = layout.value.rows[rowIndex];
523
+ if (!series || row === void 0) return null;
524
+ const value = valuePosition(series, row, rowIndex);
525
+ if (value === void 0) return null;
526
+ const [x, y] = [svgNumber(layout.value.category(row, rowIndex)), svgNumber(layout.value.valueScale(value))];
527
+ const color = item.color ? resolveColor(item.color, 0) : series.color;
528
+ const half = estimateTextWidth(item.label, TICK_FONT) / 2;
529
+ const { left, width } = layout.value.plot;
530
+ const anchor = x + half > left + width ? "end" : x - half < left ? "start" : "middle";
531
+ const textX = anchor === "end" ? x + 8 : anchor === "start" ? x - 8 : x;
532
+ return createVNode("g", {
533
+ "key": `mark-${position}`,
534
+ "data-part": "mark"
535
+ }, [createVNode("circle", mergeJsxProps({
536
+ "cx": x,
537
+ "cy": y,
538
+ "r": 8
539
+ }, css.attrs(xyChartStyles.mark, chartSvgStyles.stroke(color))), null), createVNode("text", mergeJsxProps({
540
+ "x": textX,
541
+ "y": y - 14,
542
+ "text-anchor": anchor
543
+ }, css.attrs(chartSvgStyles.label, chartSvgStyles.halo, chartSvgStyles.noPointer)), [item.label])]);
340
544
  });
341
- const secondaryLabels = aligned ? aligned.secondary.ticks.map((tick, position) => {
342
- const label = instances.get(`secondary-${position}`, () => {
343
- const line = new Plotline({});
344
- line.clippable = false;
345
- return line;
346
- });
347
- label.setConfig({
348
- axis: "y",
349
- value: aligned.primary.ticks[position],
350
- color: "transparent",
351
- lineWidth: 0,
352
- labelText: format.number(tick, kind.value.secondary?.format),
353
- labelPosition: PlotlineLabelPosition.Right,
354
- labelOffsetX: -44,
355
- labelColor: colors.textMuted
356
- });
357
- return label;
358
- }) : [];
359
- const components = [
360
- ...bandComponents,
361
- ...kind.value.components({
362
- series: shown,
363
- x,
364
- y,
365
- instance: instances.get,
366
- events: kind.value.itemSelector && onItemClick.value ? { [kind.value.itemSelector]: { click: (row, _event, index) => {
367
- const position = data.value.indexOf(row);
368
- const at = position >= 0 ? position : index;
369
- onItemClick.value(data.value[at] ?? row, at);
370
- } } } : {}
545
+ }
546
+ /** 十字线:悬停的类目一条竖线,线上给各系列画圆点;柱状图铺一层浅底 */
547
+ function renderCrosshair(front) {
548
+ const current = hover.value;
549
+ if (!current || kind.value.pointTooltip) return null;
550
+ const { rows, plot, horizontal, step, category, valueScale } = layout.value;
551
+ const row = rows[current.index];
552
+ if (row === void 0) return null;
553
+ const center = category(row, current.index);
554
+ if (!front) {
555
+ if (kind.value.crosshairDots && !kind.value.band) return null;
556
+ const half = Math.max(step / 2, 1);
557
+ return horizontal ? createVNode("rect", mergeJsxProps({
558
+ "x": plot.left,
559
+ "y": center - half,
560
+ "width": plot.width,
561
+ "height": half * 2
562
+ }, css.attrs(chartSvgStyles.hoverBand)), null) : createVNode("rect", mergeJsxProps({
563
+ "x": center - half,
564
+ "y": plot.top,
565
+ "width": half * 2,
566
+ "height": plot.height
567
+ }, css.attrs(chartSvgStyles.hoverBand)), null);
568
+ }
569
+ if (!kind.value.crosshairDots) return null;
570
+ const dotted = kind.value.crosshairSeries ? layout.value.shown.filter(kind.value.crosshairSeries) : layout.value.shown;
571
+ const p = Math.round(center) + .5;
572
+ return createVNode("g", { "data-part": "crosshair" }, [!kind.value.band && createVNode("line", mergeJsxProps({
573
+ "x1": p,
574
+ "x2": p,
575
+ "y1": plot.top,
576
+ "y2": plot.top + plot.height
577
+ }, css.attrs(chartSvgStyles.crosshair)), null), dotted.map((item) => {
578
+ const value = valuePosition(item, row, current.index);
579
+ if (value === void 0) return null;
580
+ return createVNode("circle", mergeJsxProps({
581
+ "key": item.key,
582
+ "cx": svgNumber(center),
583
+ "cy": svgNumber(valueScale(value)),
584
+ "r": 4.5
585
+ }, css.attrs(xyChartStyles.crosshairDot, chartSvgStyles.fill(item.color))), null);
586
+ })]);
587
+ }
588
+ /** 绘图区的 SVG */
589
+ function renderPlot() {
590
+ const { rows, shown, horizontal, plot, step, domain, category, valueScale, valueOf, stacks } = layout.value;
591
+ const context = {
592
+ series: shown,
593
+ rows,
594
+ horizontal,
595
+ plot,
596
+ step,
597
+ domain,
598
+ category,
599
+ value: valueScale,
600
+ valueOf,
601
+ stacks,
602
+ point: (c, v) => horizontal ? [v, c] : [c, v],
603
+ hovered: hover.value?.index ?? null
604
+ };
605
+ const zero = valueScale(0);
606
+ const showZero = domain[0] < 0 && domain[1] > 0;
607
+ return createVNode("svg", mergeJsxProps({
608
+ "width": area.width,
609
+ "height": area.height
610
+ }, css.attrs(chartSvgStyles.svg, xyChartStyles.svg), {
611
+ "onPointermove": onPointermove,
612
+ "onPointerleave": _cache[3] || (_cache[3] = () => hover.value = null),
613
+ "onClick": onClick
614
+ }), [
615
+ renderBands(),
616
+ chartAxis({
617
+ side: horizontal ? "bottom" : "left",
618
+ ticks: layout.value.valueTicks,
619
+ plot,
620
+ grid: grid.value
371
621
  }),
372
- ...lines,
373
- ...eventLines,
374
- ...marked.length > 0 ? [marks] : [],
375
- ...secondaryLabels
376
- ];
377
- const xAxis = instances.get("xAxis", () => new Axis({ type: "x" }));
378
- const yAxis = instances.get("yAxis", () => new Axis({ type: "y" }));
379
- const crosshair = instances.get("crosshair", () => new Crosshair());
380
- const tooltipBox = instances.get("tooltip", () => new Tooltip());
381
- const [categoryAxis, valueAxis] = kind.value.horizontal ? [yAxis, xAxis] : [xAxis, yAxis];
382
- const continuous = kind.value.continuousX;
383
- categoryAxis.setConfig({
384
- type: kind.value.horizontal ? "y" : "x",
385
- tickValues: continuous ? void 0 : rows.map((_row, position) => position),
386
- numTicks: continuous ? 6 : void 0,
387
- tickFormat: continuous ? (tick) => continuous.format(Number(tick)) : (tick) => categoryText(rows[Number(tick)]),
388
- gridLine: continuous ? grid.value : false,
389
- tickLine: false,
390
- domainLine: false,
391
- tickTextHideOverlapping: true,
392
- tickPadding: 8,
393
- label: continuous?.title,
394
- labelMargin: 8
395
- });
396
- valueAxis.setConfig({
397
- type: kind.value.horizontal ? "x" : "y",
398
- numTicks: 5,
399
- tickValues: aligned?.primary.ticks,
400
- tickFormat: (tick) => kind.value.percent ? format.number(Number(tick), {
401
- style: "percent",
402
- maximumFractionDigits: 0
403
- }) : format.number(Number(tick), valueFormat.value),
404
- gridLine: grid.value,
405
- tickLine: false,
406
- domainLine: false,
407
- tickPadding: 8
408
- });
409
- const dotted = kind.value.crosshairSeries ? shown.filter(kind.value.crosshairSeries) : shown;
410
- crosshair.setConfig({
411
- x,
412
- ...kind.value.stacked ? { yStacked: dotted.map(y) } : { y: dotted.map(y) },
413
- template: (row) => renderTooltip(row),
414
- color: (_row, position) => dotted[position]?.color ?? "transparent",
415
- getCircles: kind.value.crosshairDots ? void 0 : () => [],
416
- hideWhenFarFromPointer: false,
417
- onCrosshairMove: (_x, row) => hover(row)
418
- });
419
- tooltipBox.setConfig({ triggers: kind.value.pointTooltip ? { [kind.value.pointTooltip]: (row) => renderTooltip(row) } : {} });
420
- const config = {
421
- components,
422
- xAxis,
423
- yAxis,
424
- crosshair: (tooltip.value || onItemClick.value && !kind.value.itemSelector) && !kind.value.pointTooltip ? crosshair : void 0,
425
- tooltip: tooltip.value ? tooltipBox : void 0,
426
- [kind.value.horizontal ? "xDomain" : "yDomain"]: domain,
427
- padding: aligned ? {
428
- ...kind.value.padding,
429
- right: Math.max(kind.value.padding?.right ?? 0, SECONDARY_AXIS_WIDTH)
430
- } : kind.value.padding ?? {},
431
- ariaLabel: null,
432
- duration: chartDuration(element),
433
- onRenderComplete: haloPlotlineLabels
622
+ chartAxis({
623
+ side: horizontal ? "left" : "bottom",
624
+ ticks: layout.value.categoryTicks,
625
+ plot,
626
+ grid: kind.value.continuousX ? grid.value : false
627
+ }),
628
+ layout.value.secondaryTicks.length > 0 && chartAxis({
629
+ side: "right",
630
+ ticks: layout.value.secondaryTicks,
631
+ plot
632
+ }),
633
+ kind.value.continuousX?.title && createVNode("text", mergeJsxProps({
634
+ "x": plot.left + plot.width / 2,
635
+ "y": area.height - 2,
636
+ "text-anchor": "middle"
637
+ }, css.attrs(chartSvgStyles.axisTitle)), [kind.value.continuousX.title]),
638
+ showZero && (horizontal ? createVNode("line", mergeJsxProps({
639
+ "x1": Math.round(zero) + .5,
640
+ "x2": Math.round(zero) + .5,
641
+ "y1": plot.top,
642
+ "y2": plot.top + plot.height
643
+ }, css.attrs(chartSvgStyles.baseline)), null) : createVNode("line", mergeJsxProps({
644
+ "x1": plot.left,
645
+ "x2": plot.left + plot.width,
646
+ "y1": Math.round(zero) + .5,
647
+ "y2": Math.round(zero) + .5
648
+ }, css.attrs(chartSvgStyles.baseline)), null)),
649
+ renderCrosshair(false),
650
+ kind.value.render(context),
651
+ renderLines(),
652
+ renderMarks(),
653
+ renderCrosshair(true)
654
+ ]);
655
+ }
656
+ const tip = () => {
657
+ const current = hover.value;
658
+ if (!tooltip.value || !current) return null;
659
+ const row = layout.value.rows[current.index];
660
+ return row === void 0 ? null : {
661
+ x: current.x,
662
+ y: current.y,
663
+ content: tooltipContent(row)
434
664
  };
435
- if (container) container.update(config, void 0, rows);
436
- else {
437
- container = new XYContainer(element, config, rows);
438
- containerElement = element;
439
- uniqueHighlightFilter(element, `${id}-highlight`);
440
- }
441
- });
665
+ };
442
666
  return () => createVNode(ChartFrame, mergeJsxProps(rest, {
443
- "sx": [css.select(() => !kind.value.crosshairDots && xyChartStyles.hideCrosshairLine, true), sx.value],
444
667
  "unit": kind.value.percent ? void 0 : unit.value,
445
668
  "unitEnd": kind.value.secondary?.unit,
446
669
  "height": height.value,
@@ -452,9 +675,6 @@ var XYChart = /*#__PURE__*/ _component((_props, _ctx) => {
452
675
  "isEmpty": data.value.length === 0,
453
676
  "empty": empty.value,
454
677
  "clickable": onItemClick.value != null,
455
- "onPlotClick": onItemClick.value && !kind.value.itemSelector ? () => {
456
- if (hovered) onItemClick.value(hovered.row, hovered.index);
457
- } : void 0,
458
678
  "table": kind.value.table?.(data.value) ?? {
459
679
  columns: [
460
680
  lang.chartCategory,
@@ -472,7 +692,12 @@ var XYChart = /*#__PURE__*/ _component((_props, _ctx) => {
472
692
  })]
473
693
  }))
474
694
  },
475
- "plotRef": _cache[3] || (_cache[3] = (element) => plot.value = element),
695
+ "plotRef": area.ref,
696
+ "plot": area.ready && shownRows.value.length > 0 ? [renderPlot(), createVNode(ChartTooltipBox, {
697
+ "key": "tooltip",
698
+ "tip": tip(),
699
+ "width": area.width
700
+ }, null)] : null,
476
701
  "footer": zoom.value && !kind.value.continuousX && data.value.length > 2 ? zoomBar() : void 0
477
702
  }), null);
478
703
  }, {
@@ -500,22 +725,9 @@ var XYChart = /*#__PURE__*/ _component((_props, _ctx) => {
500
725
  "zoom",
501
726
  "_zoomRange",
502
727
  "onUpdate:_zoomRange",
503
- "kind",
504
- "sx"
728
+ "kind"
505
729
  ]
506
730
  });
507
- /**
508
- * 目标线、事件竖线的文字描一圈表面色的边(同地图上地点的名字):压在柱子、折线上也看得清。
509
- * Unovis 的 Plotline 文字没有描边的配置项,渲染完成后补在元素上;导出图片时按计算样式一并带走
510
- */
511
- function haloPlotlineLabels(svg) {
512
- for (const label of svg.querySelectorAll("text[class*=\"plotline-label\"]")) {
513
- label.style.setProperty("paint-order", "stroke");
514
- label.style.setProperty("stroke", colors.surface);
515
- label.style.setProperty("stroke-width", "3px");
516
- label.style.setProperty("stroke-linejoin", "round");
517
- }
518
- }
519
731
  //#endregion
520
732
  export { XYChart, xyChartStyles };
521
733