Ray1ee01 commited on
Commit
6fb3f80
·
verified ·
1 Parent(s): eca9c1a

Upload folder using huggingface_hub

Browse files
modules/chart_engine/template/d3-js/type23_span_chart/span_plain_chart_01.js ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Span Chart",
5
+ "chart_name": "span_plain_chart_01",
6
+ "chart_for": "comparison",
7
+ "is_composite": false,
8
+ "required_fields": ["x", "y", "group"],
9
+ "required_fields_type": [["categorical"], ["numerical"], ["categorical"]],
10
+ "required_fields_range": [[2, 20], [0, "inf"], [2, 2]],
11
+ "required_fields_icons": [],
12
+ "required_other_icons": [],
13
+ "required_fields_colors": ["group"],
14
+ "required_other_colors": ["primary"],
15
+ "supported_effects": [],
16
+ "min_height": 400,
17
+ "min_width": 400,
18
+ "background": "dark",
19
+ "icon_mark": "none",
20
+ "icon_label": "side",
21
+ "has_x_axis": "no",
22
+ "has_y_axis": "no"
23
+ }
24
+ REQUIREMENTS_END
25
+ */
26
+
27
+ // 使用D3.js实现水平span图表
28
+ function makeChart(containerSelector, data) {
29
+ // ---------- 1. 数据准备 ----------
30
+ const jsonData = data;
31
+ const chartData = jsonData.data.data;
32
+ const variables = jsonData.variables || {};
33
+ const typography = jsonData.typography || {
34
+ title: { font_family: "Arial", font_size: "18px", font_weight: "bold" },
35
+ label: { font_family: "Arial", font_size: "12px", font_weight: "normal" },
36
+ description: { font_family: "Arial", font_size: "14px", font_weight: "normal" },
37
+ annotation: { font_family: "Arial", font_size: "12px", font_weight: "normal" }
38
+ };
39
+ const colors = jsonData.colors_dark || {};
40
+ const colorResolver = chartUtils.color.resolver(jsonData);
41
+ const dataColumns = chartUtils.schema.columns(jsonData);
42
+
43
+ d3.select(containerSelector).html("");
44
+
45
+ // ---------- 2. 尺寸和布局设置 ----------
46
+ const width = variables.width || 800;
47
+ const height = variables.height || 600;
48
+ const margin = { top: 90, right: 80, bottom: 60, left: 120 };
49
+
50
+ // ---------- 3. 提取字段信息 ----------
51
+ const dimensionField = chartUtils.schema.channel(jsonData, "x").key;
52
+ const valueField = chartUtils.schema.channel(jsonData, "y").key;
53
+ const groupField = chartUtils.schema.channel(jsonData, "group").key;
54
+
55
+ // 数值格式化函数
56
+
57
+ // ---------- 4. 数据处理 ----------
58
+ const dimensions = [...new Set(chartData.map(d => d[dimensionField]))];
59
+ const groups = [...new Set(chartData.map(d => d[groupField]))];
60
+
61
+ // Sparse dimensions are valid; dimensions with fewer than two points are skipped below.
62
+
63
+ // ---------- 5. 计算内部绘图区域 ----------
64
+ const innerWidth = width - margin.left - margin.right;
65
+ const innerHeight = height - margin.top - margin.bottom;
66
+
67
+ // ---------- 6. 创建SVG容器 ----------
68
+ const svg = d3.select(containerSelector)
69
+ .append("svg")
70
+ .attr("width", "100%")
71
+ .attr("height", height)
72
+ .attr("viewBox", `0 0 ${width} ${height}`)
73
+ .attr("style", "max-width: 100%; height: auto;");
74
+
75
+ const defs = svg.append("defs");
76
+
77
+ // 创建背景渐变
78
+ const bgGradient = defs.append("linearGradient")
79
+ .attr("id", "background-gradient")
80
+ .attr("x1", "0%").attr("y1", "0%")
81
+ .attr("x2", "100%").attr("y2", "100%");
82
+
83
+ bgGradient.append("stop").attr("offset", "0%").attr("stop-color", "#4a0032");
84
+ bgGradient.append("stop").attr("offset", "100%").attr("stop-color", "#1c3b6e");
85
+
86
+ // 添加背景
87
+ svg.append("rect")
88
+ .attr("width", width).attr("height", height)
89
+ .attr("class", "background")
90
+ .attr("fill", "url(#background-gradient)");
91
+
92
+ // ---------- 7. 创建比例尺 ----------
93
+ const yScale = d3.scaleBand()
94
+ .domain(dimensions)
95
+ .range([0, innerHeight])
96
+ .padding(0.2);
97
+
98
+ const minValue = d3.min(chartData, d => +d[valueField]);
99
+ const maxValue = d3.max(chartData, d => +d[valueField]);
100
+ const xScale = d3.scaleLinear()
101
+ .domain([Math.min(minValue, 0) * 1.15, maxValue * 1.1])
102
+ .range([0, innerWidth]);
103
+
104
+ // 颜色比例尺
105
+ const colorScale = d3.scaleOrdinal()
106
+ .domain(groups)
107
+ .range(groups.map((group, i) => {
108
+ if (colorResolver.field(group, 0, { fallbackKey: "primary" }).value) return colorResolver.field(group, 0, { fallbackKey: "primary" }).value;
109
+ return i === 0 ? "#1d3c6f" : "#6fa0d8";
110
+ }));
111
+ chartUtils.legend.centered(svg, groups, {
112
+ colorScale,
113
+ colorResolver,
114
+ colors,
115
+ }, width / 2, margin.top - 25, {
116
+ markerShape: "circle",
117
+ markerSize: 15,
118
+ labelGap: 5,
119
+ itemGap: 20,
120
+ fontSize: typography.label.font_size,
121
+ fontFamily: typography.label.font_family,
122
+ fontWeight: typography.label.font_weight,
123
+ textColor: "#ffffff",
124
+ markerRadius: variables.has_rounded_corners ? 2 : 0,
125
+ });
126
+
127
+ // ---------- 9. 创建主图表组 ----------
128
+ const g = svg.append("g")
129
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
130
+
131
+ // ---------- 10. 添加网格线 ----------
132
+ const xTicks = xScale.ticks(8);
133
+
134
+ g.selectAll(".gridline")
135
+ .data(xTicks)
136
+ .enter().append("line")
137
+ .attr("class", "gridline")
138
+ .attr("x1", d => xScale(d)).attr("y1", 0)
139
+ .attr("x2", d => xScale(d)).attr("y2", innerHeight)
140
+ .attr("stroke", "rgba(255, 255, 255, 0.15)")
141
+ .attr("stroke-width", 1);
142
+
143
+ // 添加X轴刻度值
144
+ g.selectAll(".value")
145
+ .data(xTicks)
146
+ .enter().append("text")
147
+ .attr("class", "value")
148
+ .attr("x", d => xScale(d))
149
+ .attr("y", innerHeight + 20)
150
+ .attr("text-anchor", "middle")
151
+ .style("font-family", "Arial")
152
+ .style("font-size", "12px")
153
+ .style("fill", "white")
154
+ .text(d => chartUtils.format.scaledText(d));
155
+
156
+ // ---------- 11. 绘制条形和标记 ----------
157
+ dimensions.forEach(dimension => {
158
+ const dimensionData = chartData.filter(d => d[dimensionField] === dimension);
159
+
160
+ if (dimensionData.length > 0) {
161
+ const barHeight = yScale.bandwidth();
162
+ const labelY = yScale(dimension) + barHeight / 2;
163
+
164
+ // 添加维度标签
165
+ g.append("text")
166
+ .attr("x", -5).attr("y", labelY).attr("dy", "0.35em")
167
+ .attr("text-anchor", "end")
168
+ .attr("class", "label")
169
+ .style("font-family", typography.label.font_family)
170
+ .style("font-size", typography.label.font_size)
171
+ .style("fill", "#ffffff")
172
+ .text(dimension);
173
+
174
+ // 处理数据点
175
+ const pointData = groups.map(group => {
176
+ const dataPoint = dimensionData.find(d => d[groupField] === group);
177
+ if (dataPoint) {
178
+ return {
179
+ group: group,
180
+ value: +dataPoint[valueField],
181
+ x: xScale(+dataPoint[valueField]),
182
+ y: yScale(dimension)
183
+ };
184
+ }
185
+ return null;
186
+ }).filter(d => d !== null);
187
+
188
+ pointData.sort((a, b) => a.value - b.value);
189
+
190
+ // 绘制条形和数据点
191
+ if (pointData.length >= 2) {
192
+ const startPoint = pointData[0];
193
+ const endPoint = pointData[1];
194
+
195
+ // 绘制条形
196
+ g.append("rect")
197
+ .attr("x", startPoint.x)
198
+ .attr("y", startPoint.y + barHeight * 0.25)
199
+ .attr("width", endPoint.x - startPoint.x)
200
+ .attr("height", barHeight * 0.5)
201
+ .attr("class", "mark")
202
+ .attr("fill", colorResolver.other("primary", { fallback: "#6fa0d8" }).value);
203
+
204
+ // 添加数据点圆圈
205
+ pointData.forEach(point => {
206
+ g.append("circle")
207
+ .attr("cx", point.x)
208
+ .attr("cy", point.y + barHeight / 2)
209
+ .attr("r", 6)
210
+ .attr("class", "mark")
211
+ .attr("fill", colorScale(point.group))
212
+ .attr("stroke", "white")
213
+ .attr("stroke-width", 2);
214
+ });
215
+
216
+ // 左边数值标签
217
+ g.append("text")
218
+ .attr("x", startPoint.x - 8)
219
+ .attr("y", startPoint.y + barHeight / 2)
220
+ .attr("dy", "0.35em")
221
+ .attr("text-anchor", "end")
222
+ .attr("class", "value")
223
+ .style("font-family", typography.annotation.font_family)
224
+ .style("font-size", `${Math.min(16, Math.max(barHeight * 0.4, 12))}px`)
225
+ .style("font-weight", "bold")
226
+ .style("fill", colorResolver.text({ fallback: "#ffffff" }).value)
227
+ .text(chartUtils.format.scaledText(startPoint.value));
228
+
229
+ // 右边数值标签
230
+ g.append("text")
231
+ .attr("x", endPoint.x + 8)
232
+ .attr("y", endPoint.y + barHeight / 2)
233
+ .attr("dy", "0.35em")
234
+ .attr("text-anchor", "start")
235
+ .attr("class", "value")
236
+ .style("font-family", typography.annotation.font_family)
237
+ .style("font-size", `${Math.min(16, Math.max(barHeight * 0.4, 12))}px`)
238
+ .style("font-weight", "bold")
239
+ .style("fill", colorResolver.text({ fallback: "#ffffff" }).value)
240
+ .text(chartUtils.format.scaledText(endPoint.value));
241
+ }
242
+ }
243
+ });
244
+
245
+ return svg.node();
246
+ }
modules/chart_engine/template/d3-js/type23_span_chart/span_plain_chart_02.js ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Span Chart",
5
+ "chart_name": "span_plain_chart_02",
6
+ "chart_for": "comparison",
7
+ "is_composite": false,
8
+ "required_fields": ["x", "y", "group"],
9
+ "required_fields_type": [["categorical"], ["numerical"], ["categorical"]],
10
+ "required_fields_range": [[2, 20], [0, "inf"], [2, 2]],
11
+ "required_fields_icons": [],
12
+ "required_other_icons": [],
13
+ "required_fields_colors": ["group"],
14
+ "required_other_colors": ["primary"],
15
+ "supported_effects": [],
16
+ "min_height": 400,
17
+ "min_width": 400,
18
+ "background": "dark",
19
+ "icon_mark": "none",
20
+ "icon_label": "side",
21
+ "has_x_axis": "no",
22
+ "has_y_axis": "no"
23
+ }
24
+ REQUIREMENTS_END
25
+ */
26
+
27
+ // 使用D3.js实现垂直span图表
28
+ function makeChart(containerSelector, data) {
29
+ // ---------- 1. 数据准备 ----------
30
+ const jsonData = data;
31
+ const chartData = jsonData.data.data;
32
+ const variables = jsonData.variables || {};
33
+ const typography = jsonData.typography || {
34
+ title: { font_family: "Arial", font_size: "18px", font_weight: "bold" },
35
+ label: { font_family: "Arial", font_size: "12px", font_weight: "normal" },
36
+ description: { font_family: "Arial", font_size: "14px", font_weight: "normal" },
37
+ annotation: { font_family: "Arial", font_size: "12px", font_weight: "normal" }
38
+ };
39
+ const colors = jsonData.colors_dark || {};
40
+ const colorResolver = chartUtils.color.resolver(jsonData);
41
+ const dataColumns = chartUtils.schema.columns(jsonData);
42
+
43
+ d3.select(containerSelector).html("");
44
+
45
+ // ---------- 2. 尺寸和布局设置 ----------
46
+ const width = variables.width || 800;
47
+ const height = variables.height || 600;
48
+ const margin = { top: 90, right: 60, bottom: 80, left: 60 };
49
+
50
+ // ---------- 3. 提取字段信息 ----------
51
+ const dimensionField = chartUtils.schema.channel(jsonData, "x").key;
52
+ const valueField = chartUtils.schema.channel(jsonData, "y").key;
53
+ const groupField = chartUtils.schema.channel(jsonData, "group").key;
54
+
55
+ // 数值格式化函数
56
+
57
+ // ---------- 4. 数据处理 ----------
58
+ const dimensions = [...new Set(chartData.map(d => d[dimensionField]))];
59
+ const groups = [...new Set(chartData.map(d => d[groupField]))];
60
+
61
+ // Sparse dimensions are valid; dimensions with fewer than two points are skipped below.
62
+
63
+ // ---------- 5. 计算内部绘图区域 ----------
64
+ const innerWidth = width - margin.left - margin.right;
65
+ const innerHeight = height - margin.top - margin.bottom;
66
+
67
+ // ---------- 6. 创建SVG容器 ----------
68
+ const svg = d3.select(containerSelector)
69
+ .append("svg")
70
+ .attr("width", "100%")
71
+ .attr("height", height)
72
+ .attr("viewBox", `0 0 ${width} ${height}`)
73
+ .attr("style", "max-width: 100%; height: auto;");
74
+
75
+ const defs = svg.append("defs");
76
+
77
+ // 创建背景渐变
78
+ const bgGradient = defs.append("linearGradient")
79
+ .attr("id", "background-gradient")
80
+ .attr("x1", "0%").attr("y1", "0%")
81
+ .attr("x2", "100%").attr("y2", "100%");
82
+
83
+ bgGradient.append("stop").attr("offset", "0%").attr("stop-color", "#4a0032");
84
+ bgGradient.append("stop").attr("offset", "100%").attr("stop-color", "#1c3b6e");
85
+
86
+ // 添加背景
87
+ svg.append("rect")
88
+ .attr("width", width).attr("height", height)
89
+ .attr("class", "background")
90
+ .attr("fill", "url(#background-gradient)");
91
+
92
+ // ---------- 7. 创建比例尺 ----------
93
+ const xScale = d3.scaleBand()
94
+ .domain(dimensions)
95
+ .range([0, innerWidth])
96
+ .padding(0.2);
97
+
98
+ const minValue = d3.min(chartData, d => +d[valueField]);
99
+ const maxValue = d3.max(chartData, d => +d[valueField]);
100
+ const yScale = d3.scaleLinear()
101
+ .domain([Math.min(minValue, 0) * 1.15, maxValue * 1.1])
102
+ .range([innerHeight, 0]);
103
+
104
+ // 颜色比例尺
105
+ const colorScale = d3.scaleOrdinal()
106
+ .domain(groups)
107
+ .range(groups.map((group, i) => {
108
+ if (colorResolver.field(group, 0, { fallbackKey: "primary" }).value) return colorResolver.field(group, 0, { fallbackKey: "primary" }).value;
109
+ return i === 0 ? "#1d3c6f" : "#6fa0d8";
110
+ }));
111
+ chartUtils.legend.centered(svg, groups, {
112
+ colorScale,
113
+ colorResolver,
114
+ colors,
115
+ }, width / 2, margin.top - 25, {
116
+ markerShape: "circle",
117
+ markerSize: 15,
118
+ labelGap: 5,
119
+ itemGap: 20,
120
+ fontSize: typography.label.font_size,
121
+ fontFamily: typography.label.font_family,
122
+ fontWeight: typography.label.font_weight,
123
+ textColor: "#ffffff",
124
+ markerRadius: variables.has_rounded_corners ? 2 : 0,
125
+ });
126
+
127
+ // ---------- 9. 创建主图表组 ----------
128
+ const g = svg.append("g")
129
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
130
+
131
+ // ---------- 10. 添加网格线 ----------
132
+ const yTicks = yScale.ticks(8);
133
+
134
+ g.selectAll(".gridline")
135
+ .data(yTicks)
136
+ .enter().append("line")
137
+ .attr("class", "gridline")
138
+ .attr("x1", 0).attr("y1", d => yScale(d))
139
+ .attr("x2", innerWidth).attr("y2", d => yScale(d))
140
+ .attr("stroke", "rgba(255, 255, 255, 0.15)")
141
+ .attr("stroke-width", 1);
142
+
143
+ // 添加Y轴刻度值
144
+ g.selectAll(".value")
145
+ .data(yTicks)
146
+ .enter().append("text")
147
+ .attr("class", "value")
148
+ .attr("x", -10)
149
+ .attr("y", d => yScale(d))
150
+ .attr("text-anchor", "end")
151
+ .attr("dy", "0.35em")
152
+ .style("font-family", "Arial")
153
+ .style("font-size", "12px")
154
+ .style("fill", "white")
155
+ .text(d => chartUtils.format.scaledText(d));
156
+
157
+ // ---------- 11. 绘制条形和标记 ----------
158
+ // 计算适合的字体大小
159
+ const calculateFontSize = (text, maxWidth, baseFontSize) => {
160
+ const canvas = document.createElement('canvas');
161
+ const ctx = canvas.getContext('2d');
162
+ let fontSize = baseFontSize;
163
+
164
+ do {
165
+ ctx.font = `${typography.label.font_weight} ${fontSize}px ${typography.label.font_family}`;
166
+ if (chartUtils.text.contextWidth(ctx, text) <= maxWidth) break;
167
+ fontSize -= 0.5;
168
+ } while (fontSize > 10);
169
+
170
+ return fontSize;
171
+ };
172
+
173
+ // 文本自动换行函数 - 顶行对齐
174
+ const wrapText = (textElement, str, maxWidth) => {
175
+ const words = str.split(/\s+/).reverse();
176
+ const lines = [];
177
+ let currentLine = [];
178
+ let word;
179
+
180
+ textElement.text(null);
181
+
182
+ if (words.length > 1) {
183
+ while (word = words.pop()) {
184
+ currentLine.push(word);
185
+ const testWidth = chartUtils.text.measure(null, currentLine.join(" "), {
186
+ fontFamily: textElement.style("font-family"),
187
+ fontSize: parseFloat(textElement.style("font-size")),
188
+ fontWeight: textElement.style("font-weight")
189
+ }).width;
190
+ if (testWidth > maxWidth && currentLine.length > 1) {
191
+ currentLine.pop();
192
+ lines.push(currentLine.join(" "));
193
+ currentLine = [word];
194
+ }
195
+ }
196
+ if (currentLine.length > 0) lines.push(currentLine.join(" "));
197
+ } else {
198
+ lines.push(str);
199
+ }
200
+
201
+ // 顶行对齐:第一行在原始Y位置
202
+ const lineHeight = 1.1;
203
+
204
+ lines.forEach((line, i) => {
205
+ textElement.append("tspan")
206
+ .attr("x", textElement.attr("x"))
207
+ .attr("dy", (i === 0 ? 0 : lineHeight) + "em")
208
+ .text(line);
209
+ });
210
+ };
211
+
212
+ // 预计算统一字体大小
213
+ const baseFontSize = parseInt(typography.label.font_size) || 12;
214
+ const longestDimension = dimensions.reduce((a, b) =>
215
+ a.toString().length > b.toString().length ? a : b, "").toString();
216
+ const maxLabelWidth = xScale.bandwidth() * 0.8;
217
+ const uniformFontSize = calculateFontSize(longestDimension, maxLabelWidth, baseFontSize);
218
+
219
+ dimensions.forEach(dimension => {
220
+ const dimensionData = chartData.filter(d => d[dimensionField] === dimension);
221
+
222
+ if (dimensionData.length > 0) {
223
+ const barWidth = xScale.bandwidth();
224
+ const labelX = xScale(dimension) + barWidth / 2;
225
+
226
+ // 添加维度标签 - 使用统一字体大小和换行
227
+ const labelText = g.append("text")
228
+ .attr("x", labelX).attr("y", innerHeight + 25)
229
+ .attr("text-anchor", "middle")
230
+ .attr("class", "label")
231
+ .style("font-family", typography.label.font_family)
232
+ .style("font-size", `${uniformFontSize}px`)
233
+ .style("font-weight", typography.label.font_weight)
234
+ .style("fill", colorResolver.text({ fallback: "#333333" }).value)
235
+ .text(dimension);
236
+
237
+ // 检查是否需要换行
238
+ if (chartUtils.text.measure(g, dimension, {
239
+ fontFamily: typography.label.font_family,
240
+ fontSize: uniformFontSize,
241
+ fontWeight: typography.label.font_weight
242
+ }).width > maxLabelWidth) {
243
+ wrapText(labelText, dimension.toString(), maxLabelWidth);
244
+ }
245
+
246
+ // 处理数据点
247
+ const pointData = groups.map(group => {
248
+ const dataPoint = dimensionData.find(d => d[groupField] === group);
249
+ if (dataPoint) {
250
+ return {
251
+ group: group,
252
+ value: +dataPoint[valueField],
253
+ x: xScale(dimension),
254
+ y: yScale(+dataPoint[valueField])
255
+ };
256
+ }
257
+ return null;
258
+ }).filter(d => d !== null);
259
+
260
+ pointData.sort((a, b) => a.value - b.value);
261
+
262
+ // 绘制条形和数据点
263
+ if (pointData.length >= 2) {
264
+ const startPoint = pointData[0];
265
+ const endPoint = pointData[1];
266
+
267
+ // 绘制条形
268
+ g.append("rect")
269
+ .attr("x", startPoint.x + barWidth * 0.25)
270
+ .attr("y", endPoint.y)
271
+ .attr("width", barWidth * 0.5)
272
+ .attr("height", startPoint.y - endPoint.y)
273
+ .attr("class", "mark")
274
+ .attr("fill", colorResolver.other("primary", { fallback: "#6fa0d8" }).value);
275
+
276
+ // 添加数据点圆圈
277
+ pointData.forEach(point => {
278
+ g.append("circle")
279
+ .attr("cx", point.x + barWidth / 2)
280
+ .attr("cy", point.y)
281
+ .attr("r", 6)
282
+ .attr("class", "mark")
283
+ .attr("fill", colorScale(point.group))
284
+ .attr("stroke", "white")
285
+ .attr("stroke-width", 2);
286
+ });
287
+
288
+ // 下方数值标签
289
+ g.append("text")
290
+ .attr("x", startPoint.x + barWidth / 2)
291
+ .attr("y", startPoint.y + 8)
292
+ .attr("dy", "0.7em")
293
+ .attr("text-anchor", "middle")
294
+ .attr("class", "value")
295
+ .style("font-family", typography.annotation.font_family)
296
+ .style("font-size", `${Math.min(16, Math.max(barWidth * 0.4, 12))}px`)
297
+ .style("font-weight", "bold")
298
+ .style("fill", colorResolver.text({ fallback: "#ffffff" }).value)
299
+ .text(chartUtils.format.scaledText(startPoint.value));
300
+
301
+ // 上方数值标签
302
+ g.append("text")
303
+ .attr("x", endPoint.x + barWidth / 2)
304
+ .attr("y", endPoint.y - 8)
305
+ .attr("dy", "-0.2em")
306
+ .attr("text-anchor", "middle")
307
+ .attr("class", "value")
308
+ .style("font-family", typography.annotation.font_family)
309
+ .style("font-size", `${Math.min(16, Math.max(barWidth * 0.4, 12))}px`)
310
+ .style("font-weight", "bold")
311
+ .style("fill", colorResolver.text({ fallback: "#ffffff" }).value)
312
+ .text(chartUtils.format.scaledText(endPoint.value));
313
+ }
314
+ }
315
+ });
316
+
317
+ return svg.node();
318
+ }