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

Upload folder using huggingface_hub

Browse files
modules/chart_engine/template/d3-js/type22_dumbbell_plot/dumbbell_plot_plain_chart_01.js ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Dumbbell Plot",
5
+ "chart_name": "dumbbell_plot_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": ["shadow", "radius_corner", "gradient", "stroke", "spacing"],
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实现水平分裂条形图
28
+ function makeChart(containerSelector, data) {
29
+ // ---------- 1. 数据准备 ----------
30
+
31
+ // 提取数据和配置
32
+ const jsonData = data; // 完整JSON数据对象
33
+ const chartData = jsonData.data.data; // 实际数据点数组
34
+ const variables = jsonData.variables || {}; // 图表配置
35
+ const typography = jsonData.typography || { // 字体设置,如未提供则使用默认值
36
+ title: { font_family: "Arial", font_size: "18px", font_weight: "bold" },
37
+ label: { font_family: "Arial", font_size: "12px", font_weight: "normal" },
38
+ description: { font_family: "Arial", font_size: "14px", font_weight: "normal" },
39
+ annotation: { font_family: "Arial", font_size: "12px", font_weight: "normal" }
40
+ };
41
+ const colors = jsonData.colors || { text_color: "#333333" }; // 颜色设置
42
+ const colorResolver = chartUtils.color.resolver(jsonData);
43
+ const images = jsonData.images || { field: {}, other: {} }; // 图像设置
44
+ const dataColumns = chartUtils.schema.columns(jsonData); // 数据列定义
45
+
46
+ // 设置视觉效果变量的默认值
47
+ variables.has_rounded_corners = variables.has_rounded_corners || false;
48
+ variables.has_shadow = variables.has_shadow || false;
49
+ variables.has_gradient = variables.has_gradient || false;
50
+ variables.has_stroke = variables.has_stroke || false;
51
+ variables.has_spacing = variables.has_spacing || false;
52
+
53
+ // 清空容器
54
+ d3.select(containerSelector).html("");
55
+
56
+ // ---------- 2. 尺寸和布局设置 ----------
57
+
58
+ // 设置图表总尺寸
59
+ const width = variables.width || 800;
60
+ const height = variables.height || 600;
61
+
62
+ // 设置边距
63
+ const margin = {
64
+ top: 90, // 顶部空间用于标题
65
+ right: 20, // 右侧边距用于值标签
66
+ bottom: 60, // 底部边距
67
+ left: 100 // 左侧用于维度标签
68
+ };
69
+
70
+ // ---------- 3. 提取字段名称和单位 ----------
71
+
72
+ // 根据数据列顺序提取字段名称
73
+ const dimensionField = chartUtils.schema.channel(jsonData, "x").key;
74
+ const valueField = chartUtils.schema.channel(jsonData, "y").key;
75
+ const groupField = chartUtils.schema.channel(jsonData, "group").key;
76
+
77
+ // 获取字段单位(如果存在)
78
+ let dimensionUnit = "";
79
+ let valueUnit = "";
80
+ let groupUnit = "";
81
+
82
+ if (chartUtils.schema.channel(jsonData, "x").unit !== "none") {
83
+ dimensionUnit = chartUtils.schema.channel(jsonData, "x").unit;
84
+ }
85
+
86
+ //if (chartUtils.schema.channel(jsonData, "y").unit !== "none") {
87
+ // valueUnit = chartUtils.schema.channel(jsonData, "y").unit;
88
+ //}
89
+ if (chartUtils.schema.channel(jsonData, "group").unit !== "none") {
90
+ groupUnit = chartUtils.schema.channel(jsonData, "group").unit;
91
+ }
92
+
93
+ // 数值格式化函数
94
+
95
+ // ---------- 4. 数据处理 ----------
96
+
97
+ // 获取唯一的维度值和组值
98
+ const dimensions = [...new Set(chartData.map(d => d[dimensionField]))];
99
+ const groups = [...new Set(chartData.map(d => d[groupField]))];
100
+
101
+ // 如有需要可以对维度进行排序 - 当前使用原始顺序
102
+
103
+ // ---------- 5. 计算标签宽度 ----------
104
+
105
+ // 创建临时SVG以测量文本宽度
106
+ const tempSvg = d3.select(containerSelector)
107
+ .append("svg")
108
+ .attr("width", 0)
109
+ .attr("height", 0)
110
+ .style("visibility", "hidden");
111
+
112
+ // 初始填充值(将根据条形高度进行调整)
113
+ const flagPadding = 5;
114
+
115
+ // 计算最大维度标签宽度
116
+ let maxLabelWidth = 0;
117
+ dimensions.forEach(dimension => {
118
+ // 格式化维度名称(如果存在单位则添加)
119
+ const formattedDimension = dimensionUnit ?
120
+ `${dimension}${dimensionUnit}` :
121
+ `${dimension}`;
122
+
123
+ const textWidth = chartUtils.text.measure(tempSvg, formattedDimension, {
124
+ fontFamily: typography.label.font_family,
125
+ fontSize: typography.label.font_size,
126
+ fontWeight: typography.label.font_weight
127
+ }).width;
128
+ const totalWidth = textWidth;
129
+
130
+ maxLabelWidth = Math.max(maxLabelWidth, totalWidth);
131
+ });
132
+
133
+ // 计算值标签的最大宽度
134
+ let maxValueWidth = 0;
135
+ chartData.forEach(d => {
136
+ const formattedValue = valueUnit ?
137
+ `${d[valueField]}${valueUnit}` :
138
+ `${chartUtils.format.autoText(+d[valueField])}`;
139
+
140
+ const textWidth = chartUtils.text.measure(tempSvg, formattedValue, {
141
+ fontFamily: typography.annotation.font_family,
142
+ fontSize: typography.annotation.font_size,
143
+ fontWeight: typography.annotation.font_weight
144
+ }).width;
145
+
146
+ maxValueWidth = Math.max(maxValueWidth, textWidth);
147
+ });
148
+ // 删除临时SVG
149
+ tempSvg.remove();
150
+
151
+ // 根据标签宽度调整左边距(添加一些边距)
152
+ margin.left = Math.max(margin.left, maxLabelWidth + 10);
153
+ margin.right = Math.max(margin.right, maxValueWidth + 10);
154
+
155
+ // 计算内部绘图区域大小
156
+ const innerWidth = width - margin.left - margin.right;
157
+ const innerHeight = height - margin.top - margin.bottom;
158
+
159
+ // ---------- 6. 创建SVG容器 ----------
160
+ const svg = d3.select(containerSelector)
161
+ .append("svg")
162
+ .attr("width", "100%")
163
+ .attr("height", height)
164
+ .attr("viewBox", `0 0 ${width} ${height}`)
165
+ .attr("style", "max-width: 100%; height: auto;")
166
+ .attr("xmlns", "http://www.w3.org/2000/svg")
167
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
168
+
169
+ // 首先创建defs元素(必须在使用前创建)
170
+ const defs = svg.append("defs");
171
+
172
+ // 创建背景渐变
173
+ const bgGradient = defs.append("linearGradient")
174
+ .attr("id", "background-gradient")
175
+ .attr("x1", "0%")
176
+ .attr("y1", "0%")
177
+ .attr("x2", "100%")
178
+ .attr("y2", "100%");
179
+
180
+ bgGradient.append("stop")
181
+ .attr("offset", "0%")
182
+ .attr("stop-color", "#4a0032");
183
+
184
+ bgGradient.append("stop")
185
+ .attr("offset", "100%")
186
+ .attr("stop-color", "#1c3b6e");
187
+
188
+ // 添加渐变背景(在所有其他元素之前)
189
+ svg.append("rect")
190
+ .attr("width", width)
191
+ .attr("height", height)
192
+ .attr("class", "background")
193
+ .attr("fill", "url(#background-gradient)")
194
+ .attr("opacity", 1.0);
195
+
196
+ // 添加阴影滤镜(如果启用)
197
+ if (variables.has_shadow) {
198
+ const filter = defs.append("filter")
199
+ .attr("id", "shadow")
200
+ .attr("filterUnits", "userSpaceOnUse")
201
+ .attr("width", "200%")
202
+ .attr("height", "200%");
203
+
204
+ filter.append("feGaussianBlur")
205
+ .attr("in", "SourceAlpha")
206
+ .attr("stdDeviation", 3);
207
+
208
+ filter.append("feOffset")
209
+ .attr("dx", 2)
210
+ .attr("dy", 2)
211
+ .attr("result", "offsetblur");
212
+
213
+ const feMerge = filter.append("feMerge");
214
+ feMerge.append("feMergeNode");
215
+ feMerge.append("feMergeNode").attr("in", "SourceGraphic");
216
+ }
217
+
218
+ // ---------- 7. 创建坐标轴比例尺 ----------
219
+
220
+ // 计算额外的条形间距(如果启用)
221
+ const barPadding = variables.has_spacing ? 0.3 : 0.2;
222
+
223
+ // Y轴比例尺(用于维度) - 使用原始顺序
224
+ const yScale = d3.scaleBand()
225
+ .domain(dimensions)
226
+ .range([0, innerHeight])
227
+ .padding(barPadding);
228
+
229
+ // X轴比例尺(用于值)
230
+ // 找到最小和最大值来设置范围
231
+ const minValue = d3.min(chartData, d => +d[valueField]);
232
+ const maxValue = d3.max(chartData, d => +d[valueField]);
233
+
234
+ // 修改X轴范围确保最小值和最大值都在刻度范围内
235
+ // 这里使用更大的倍数以确保有足够的空间
236
+ const xScale = d3.scaleLinear()
237
+ .domain([Math.min(minValue, 0) * 1.15, maxValue + 5])
238
+ .range([0, innerWidth]);
239
+
240
+
241
+ // 颜色比例尺(用于组)
242
+ const colorScale = colorResolver.scale(groups, { colors: ["#1d3c6f", "#6fa0d8"] });
243
+ chartUtils.legend.centered(svg, groups, {
244
+ colorScale,
245
+ colorResolver,
246
+ colors,
247
+ }, width / 2, margin.top - 25, {
248
+ markerShape: "circle",
249
+ markerSize: 15.0,
250
+ labelGap: 5,
251
+ itemGap: 10,
252
+ maxWidth: width - 10,
253
+ fitToWidth: true,
254
+ minFontSize: 8,
255
+ fontSize: typography.label.font_size,
256
+ fontFamily: typography.label.font_family,
257
+ fontWeight: typography.label.font_weight,
258
+ textColor: "#ffffff",
259
+ });
260
+ // ---------- 9. 创建主图表组 ----------
261
+
262
+ const g = svg.append("g")
263
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
264
+
265
+
266
+
267
+ // ---------- 10. 添加网格线(Y轴) ----------
268
+
269
+ // 添加垂直网格线(刻度标记)
270
+ const xTicks = xScale.ticks(10);
271
+
272
+ // 添加网格线
273
+ g.selectAll(".grid-line")
274
+ .data(xTicks)
275
+ .enter()
276
+ .append("line")
277
+ .attr("class", "grid-line")
278
+ .attr("x1", d => xScale(d))
279
+ .attr("y1", 0)
280
+ .attr("x2", d => xScale(d))
281
+ .attr("y2", innerHeight)
282
+ .attr("stroke", "rgba(255, 255, 255, 0.15)")
283
+ .attr("stroke-width", 1);
284
+
285
+ // 在底部添加X轴值
286
+ g.selectAll(".x-tick-label")
287
+ .data(xTicks)
288
+ .enter()
289
+ .append("text")
290
+ .attr("class", "x-tick-label")
291
+ .attr("x", d => xScale(d))
292
+ .attr("y", innerHeight + 20)
293
+ .attr("text-anchor", "middle")
294
+ .style("font-family", "Arial")
295
+ .style("font-size", "12px")
296
+ .style("fill", "white")
297
+ .text(d => chartUtils.format.autoText(d));
298
+
299
+ // ---------- 11. 添加交替行背景 ----------
300
+
301
+ if (jsonData.variation && jsonData.variation.background === "styled") {
302
+ dimensions.forEach((dimension, i) => {
303
+
304
+ g.append("rect")
305
+ .attr("x", -margin.left + 10)
306
+ .attr("y", yScale(dimension))
307
+ .attr("width", innerWidth + margin.left + margin.right - 20)
308
+ .attr("height", yScale.bandwidth())
309
+ .attr("class", "background")
310
+ .attr("fill", "rgba(255, 255, 255, 0.05)")
311
+ .attr("rx", 4)
312
+ .attr("ry", 4);
313
+
314
+ });
315
+ }
316
+
317
+ // ---------- 12. 绘制条形和标记 ----------
318
+ const barHeight = yScale.bandwidth();
319
+ // 找到最右侧的刻度位置,用于连接线终点
320
+ const rightmostTickX = xScale(xTicks[xTicks.length - 1]);
321
+
322
+ dimensions.forEach(dimension => {
323
+ // 获取此维度的所有数据点
324
+ const dimensionData = chartData.filter(d => d[dimensionField] === dimension);
325
+
326
+ if (dimensionData.length > 0) {
327
+ const barHeight = yScale.bandwidth();
328
+ const labelY = yScale(dimension) + barHeight / 2;
329
+
330
+ // 添加维度标签
331
+ g.append("text")
332
+ .attr("x", -5)
333
+ .attr("y", labelY)
334
+ .attr("dy", "0.35em")
335
+ .attr("text-anchor", "end")
336
+ .style("font-family", typography.label.font_family)
337
+ .style("font-size", typography.label.font_size)
338
+ .style("font-weight", typography.label.font_weight)
339
+ .style("fill", "#ffffff")
340
+ .text(dimension);
341
+
342
+ // 两个组的数据
343
+ const pointData = groups.map(group => {
344
+ const dataPoint = dimensionData.find(d => d[groupField] === group);
345
+ if (dataPoint) {
346
+ return {
347
+ group: group,
348
+ value: +dataPoint[valueField],
349
+ x: xScale(+dataPoint[valueField]),
350
+ y: yScale(dimension) + barHeight / 2
351
+ };
352
+ }
353
+ return null;
354
+ }).filter(d => d !== null);
355
+
356
+ // 绘制连接线(从最低值数据点到右边缘)
357
+ if (pointData.length > 0) {
358
+ // 找到具有最低值的数据点
359
+ const lowestValueData = pointData.reduce((lowest, current) =>
360
+ current.value < lowest.value ? current : lowest, pointData[0]);
361
+
362
+ if (lowestValueData) {
363
+ // 绘制连接线 - 更粗并精确到最右侧刻度线
364
+ g.append("rect")
365
+ .attr("x", lowestValueData.x)
366
+ .attr("y", lowestValueData.y - 2) // 更粗的线
367
+ .attr("width", rightmostTickX - lowestValueData.x)
368
+ .attr("height", 4) // 更粗的线
369
+ .attr("fill", "#ffffff");
370
+ }
371
+ }
372
+
373
+ // 添加每个数据点的标记(圆圈)
374
+ pointData.forEach((point, i) => {
375
+ // 绘制标记圆圈
376
+ g.append("circle")
377
+ .attr("cx", point.x)
378
+ .attr("cy", point.y)
379
+ .attr("r", 8)
380
+ .attr("fill", colorScale(point.group))
381
+ .attr("stroke", "white")
382
+ .attr("stroke-width", 2);
383
+
384
+ // 添加值标签 - 修改这里保留小数点和小数部分
385
+ const formattedValue = valueUnit ?
386
+ `${chartUtils.format.autoText(point.value)}${valueUnit}` :
387
+ `${chartUtils.format.autoText(point.value)}`;
388
+
389
+ const labelFontSize = Math.min(20, Math.max(barHeight * 0.6, parseFloat(typography.annotation.font_size)));
390
+ const textBBox = chartUtils.text.measure(svg, formattedValue, {
391
+ fontFamily: typography.annotation.font_family,
392
+ fontSize: labelFontSize,
393
+ fontWeight: "bold",
394
+ });
395
+ const textPadding = 8; // 文本两侧的小填充
396
+ const labelWidth = textBBox.width + textPadding * 2;
397
+ const labelHeight = textBBox.height + textPadding;
398
+
399
+ // 创建样式化值标签 - 移到圆圈右侧
400
+ g.append("rect")
401
+ .attr("x", point.x + 12) // 从圆圈右侧开始
402
+ .attr("y", point.y - labelHeight/2) // 垂直居中
403
+ .attr("width", labelWidth)
404
+ .attr("height", labelHeight)
405
+ .attr("rx", 6)
406
+ .attr("ry", 6)
407
+ .attr("fill", colorScale(point.group))
408
+ .attr("stroke", "white")
409
+ .attr("stroke-width", 1);
410
+
411
+ // 修改文本位置
412
+ g.append("text")
413
+ .attr("x", point.x + 12 + labelWidth/2) // 文本在矩形中央
414
+ .attr("y", point.y)
415
+ .attr("dy", "0.35em")
416
+ .attr("text-anchor", "middle")
417
+ .style("font-family", typography.annotation.font_family)
418
+ .style("font-size", `${Math.min(20,Math.max(barHeight * 0.6, parseFloat(typography.annotation.font_size)))}px`)
419
+ .style("font-weight", "bold")
420
+ .style("fill", "white")
421
+ .text(formattedValue);
422
+ });
423
+ }
424
+ });
425
+
426
+ // 返回SVG节点
427
+ return svg.node();
428
+ }
modules/chart_engine/template/d3-js/type22_dumbbell_plot/dumbbell_plot_plain_chart_02.js ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Dumbbell Plot",
5
+ "chart_name": "dumbbell_plot_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, 12], [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": ["shadow", "radius_corner", "gradient", "stroke", "spacing"],
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实现水平分裂条形图
28
+ function makeChart(containerSelector, data) {
29
+ // ---------- 1. 数据准备 ----------
30
+
31
+ // 提取数据和配置
32
+ const jsonData = data; // 完整JSON数据对象
33
+ const chartData = jsonData.data.data; // 实际数据点数组
34
+ const variables = jsonData.variables || {}; // 图表配置
35
+ const typography = jsonData.typography || { // 字体设置,如未提供则使用默认值
36
+ title: { font_family: "Arial", font_size: "18px", font_weight: "bold" },
37
+ label: { font_family: "Arial", font_size: "12px", font_weight: "normal" },
38
+ description: { font_family: "Arial", font_size: "14px", font_weight: "normal" },
39
+ annotation: { font_family: "Arial", font_size: "12px", font_weight: "normal" }
40
+ };
41
+ const colors = jsonData.colors || { text_color: "#333333" }; // 颜色设置
42
+ const colorResolver = chartUtils.color.resolver(jsonData);
43
+ const images = jsonData.images || { field: {}, other: {} }; // 图像设置
44
+ const dataColumns = chartUtils.schema.columns(jsonData); // 数据列定义
45
+
46
+ // 设置视觉效果变量的默认值
47
+ variables.has_rounded_corners = variables.has_rounded_corners || false;
48
+ variables.has_shadow = variables.has_shadow || false;
49
+ variables.has_gradient = variables.has_gradient || false;
50
+ variables.has_stroke = variables.has_stroke || false;
51
+ variables.has_spacing = variables.has_spacing || false;
52
+
53
+ // 清空容器
54
+ d3.select(containerSelector).html("");
55
+
56
+ // ---------- 2. 尺寸和布局设置 ----------
57
+
58
+ // 设置图表总尺寸
59
+ const width = variables.width || 800;
60
+ const height = variables.height || 600;
61
+
62
+ // 设置边距
63
+ const margin = {
64
+ top: 90, // 顶部空间用于标题
65
+ right: 100, // 右侧边距用于值标签
66
+ bottom: 60, // 底部边距
67
+ left: 20 // 左侧用于维度标签
68
+ };
69
+
70
+ // ---------- 3. 提取字段名称和单位 ----------
71
+
72
+ // 根据数据列顺序提取字段名称
73
+ const dimensionField = chartUtils.schema.channel(jsonData, "x").key;
74
+ const valueField = chartUtils.schema.channel(jsonData, "y").key;
75
+ const groupField = chartUtils.schema.channel(jsonData, "group").key;
76
+
77
+ // 获取字段单位(如果存在)
78
+ let dimensionUnit = "";
79
+ let valueUnit = "";
80
+ let groupUnit = "";
81
+
82
+ if (chartUtils.schema.channel(jsonData, "x").unit !== "none") {
83
+ dimensionUnit = chartUtils.schema.channel(jsonData, "x").unit;
84
+ }
85
+
86
+ if (chartUtils.schema.channel(jsonData, "y").unit !== "none") {
87
+ valueUnit = chartUtils.schema.channel(jsonData, "y").unit;
88
+ }
89
+ if (chartUtils.schema.channel(jsonData, "group").unit !== "none") {
90
+ groupUnit = chartUtils.schema.channel(jsonData, "group").unit;
91
+ }
92
+
93
+ // ---------- 4. 数据处理 ----------
94
+
95
+ // 获取唯一的维度值和组值
96
+ const dimensions = [...new Set(chartData.map(d => d[dimensionField]))];
97
+ const groups = [...new Set(chartData.map(d => d[groupField]))];
98
+
99
+ // 如有需要可以对维度进行排序 - 当前使用原始顺序
100
+
101
+ // ---------- 5. 计算标签宽度 ----------
102
+
103
+ // 创建临时SVG以测量文本宽度
104
+ const tempSvg = d3.select(containerSelector)
105
+ .append("svg")
106
+ .attr("width", 0)
107
+ .attr("height", 0)
108
+ .style("visibility", "hidden");
109
+
110
+ // 初始填充值(将根据条形高度进行调整)
111
+ const flagPadding = 5;
112
+
113
+ // 计算最大维度标签宽度
114
+ let maxLabelWidth = 0;
115
+ dimensions.forEach(dimension => {
116
+ // 格式化维度名称(如果存在单位则添加)
117
+ const formattedDimension = dimensionUnit ?
118
+ `${dimension}${dimensionUnit}` :
119
+ `${dimension}`;
120
+
121
+ const textWidth = chartUtils.text.measure(tempSvg, formattedDimension, {
122
+ fontFamily: typography.label.font_family,
123
+ fontSize: typography.label.font_size,
124
+ fontWeight: typography.label.font_weight
125
+ }).width;
126
+ const totalWidth = textWidth;
127
+
128
+ maxLabelWidth = Math.max(maxLabelWidth, totalWidth);
129
+ });
130
+
131
+ // 计算值标签的最大宽度
132
+ let maxValueWidth = 0;
133
+ chartData.forEach(d => {
134
+ const formattedValue = valueUnit ?
135
+ `${d[valueField]}${valueUnit}` :
136
+ `${chartUtils.format.autoText(+d[valueField])}`;
137
+
138
+ const textWidth = chartUtils.text.measure(tempSvg, formattedValue, {
139
+ fontFamily: typography.annotation.font_family,
140
+ fontSize: typography.annotation.font_size,
141
+ fontWeight: typography.annotation.font_weight
142
+ }).width;
143
+
144
+ maxValueWidth = Math.max(maxValueWidth, textWidth);
145
+ });
146
+ // 删除临时SVG
147
+ tempSvg.remove();
148
+
149
+ // 根据标签宽度调整左边距(添加一些边距)
150
+ margin.left = Math.max(margin.left, maxLabelWidth + 10);
151
+ margin.right = Math.max(margin.right, maxValueWidth + 10);
152
+
153
+ // 计算内部绘图区域大小
154
+ const innerWidth = width - margin.left - margin.right;
155
+ const innerHeight = height - margin.top - margin.bottom;
156
+
157
+ // ---------- 6. 创建SVG容器 ----------
158
+ const svg = d3.select(containerSelector)
159
+ .append("svg")
160
+ .attr("width", "100%")
161
+ .attr("height", height)
162
+ .attr("viewBox", `0 0 ${width} ${height}`)
163
+ .attr("style", "max-width: 100%; height: auto;")
164
+ .attr("xmlns", "http://www.w3.org/2000/svg")
165
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
166
+
167
+ // 首先创建defs元素(必须在使用前创建)
168
+ const defs = svg.append("defs");
169
+
170
+ // 创建背景渐变
171
+ const bgGradient = defs.append("linearGradient")
172
+ .attr("id", "background-gradient")
173
+ .attr("x1", "0%")
174
+ .attr("y1", "0%")
175
+ .attr("x2", "100%")
176
+ .attr("y2", "100%");
177
+
178
+ bgGradient.append("stop")
179
+ .attr("offset", "0%")
180
+ .attr("stop-color", "#4a0032");
181
+
182
+ bgGradient.append("stop")
183
+ .attr("offset", "100%")
184
+ .attr("stop-color", "#1c3b6e");
185
+
186
+ // 添加渐变背景(在所有其他元素之前)
187
+ svg.append("rect")
188
+ .attr("width", width)
189
+ .attr("height", height)
190
+ .attr("class", "background")
191
+ .attr("fill", "url(#background-gradient)")
192
+ .attr("opacity", 1.0);
193
+
194
+ // 添加阴影滤镜(如果启用)
195
+ if (variables.has_shadow) {
196
+ const filter = defs.append("filter")
197
+ .attr("id", "shadow")
198
+ .attr("filterUnits", "userSpaceOnUse")
199
+ .attr("width", "200%")
200
+ .attr("height", "200%");
201
+
202
+ filter.append("feGaussianBlur")
203
+ .attr("in", "SourceAlpha")
204
+ .attr("stdDeviation", 3);
205
+
206
+ filter.append("feOffset")
207
+ .attr("dx", 2)
208
+ .attr("dy", 2)
209
+ .attr("result", "offsetblur");
210
+
211
+ const feMerge = filter.append("feMerge");
212
+ feMerge.append("feMergeNode");
213
+ feMerge.append("feMergeNode").attr("in", "SourceGraphic");
214
+ }
215
+
216
+ // ---------- 7. 创建坐标轴比例尺 ----------
217
+
218
+ // 计算额外的条形间距(如果启用)
219
+ const barPadding = variables.has_spacing ? 0.3 : 0.2;
220
+
221
+ // X轴比例尺(用于维度) - 使用原始顺序
222
+ const xScale = d3.scaleBand()
223
+ .domain(dimensions)
224
+ .range([0, innerWidth])
225
+ .padding(barPadding);
226
+
227
+ // Y轴比例尺(用于值)
228
+ // 找到最小和最大值来设置范围
229
+ const minValue = d3.min(chartData, d => +d[valueField]);
230
+ const maxValue = d3.max(chartData, d => +d[valueField]);
231
+
232
+ // 修改Y轴范围确保最小值和最大值都在刻度范围内
233
+ const yScale = d3.scaleLinear()
234
+ .domain([Math.min(minValue, 0) * 1.15, maxValue + 5])
235
+ .range([innerHeight, 0]);
236
+
237
+
238
+ // 颜色比例尺(用于组)
239
+ const colorScale = colorResolver.scale(groups, { colors: ["#1d3c6f", "#6fa0d8"] });
240
+ chartUtils.legend.centered(svg, groups, {
241
+ colorScale,
242
+ colorResolver,
243
+ colors,
244
+ }, width / 2, margin.top - 25, {
245
+ markerShape: "circle",
246
+ markerSize: 15.0,
247
+ labelGap: 5,
248
+ itemGap: 10,
249
+ maxWidth: width - 10,
250
+ fitToWidth: true,
251
+ minFontSize: 8,
252
+ fontSize: typography.label.font_size,
253
+ fontFamily: typography.label.font_family,
254
+ fontWeight: typography.label.font_weight,
255
+ textColor: "#ffffff",
256
+ });
257
+ // ---------- 9. 创建主图表组 ----------
258
+
259
+ const g = svg.append("g")
260
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
261
+
262
+
263
+
264
+ // ---------- 10. 添加网格线(X轴) ----------
265
+
266
+ // 添加水平网格线(刻度标记)
267
+ const yTicks = yScale.ticks(10);
268
+
269
+ // 添加网格线
270
+ g.selectAll(".grid-line")
271
+ .data(yTicks)
272
+ .enter()
273
+ .append("line")
274
+ .attr("class", "grid-line")
275
+ .attr("x1", 0)
276
+ .attr("y1", d => yScale(d))
277
+ .attr("x2", innerWidth)
278
+ .attr("y2", d => yScale(d))
279
+ .attr("stroke", "rgba(255, 255, 255, 0.15)")
280
+ .attr("stroke-width", 1);
281
+
282
+ // 在左侧添加Y轴值
283
+ g.selectAll(".y-tick-label")
284
+ .data(yTicks)
285
+ .enter()
286
+ .append("text")
287
+ .attr("class", "y-tick-label")
288
+ .attr("x", -10)
289
+ .attr("y", d => yScale(d))
290
+ .attr("text-anchor", "end")
291
+ .attr("dy", "0.35em")
292
+ .style("font-family", "Arial")
293
+ .style("font-size", "12px")
294
+ .style("fill", "white")
295
+ .text(d => d);
296
+
297
+ // ---------- 11. 添加交替列背景 ----------
298
+
299
+ if (jsonData.variation && jsonData.variation.background === "styled") {
300
+ dimensions.forEach((dimension, i) => {
301
+ g.append("rect")
302
+ .attr("x", xScale(dimension))
303
+ .attr("y", 0)
304
+ .attr("width", xScale.bandwidth())
305
+ .attr("height", innerHeight)
306
+ .attr("class", "background")
307
+ .attr("fill", "rgba(255, 255, 255, 0.05)")
308
+ .attr("rx", 4)
309
+ .attr("ry", 4);
310
+ });
311
+ }
312
+
313
+ // ---------- 12. 绘制条形和标记 ----------
314
+ const barWidth = xScale.bandwidth();
315
+ // 找到最上方的刻度位置,用于连接线终点
316
+ const topmostTickY = yScale(yTicks[yTicks.length - 1]);
317
+
318
+ dimensions.forEach(dimension => {
319
+ // 获取此维度的所有数据点
320
+ const dimensionData = chartData.filter(d => d[dimensionField] === dimension);
321
+
322
+ if (dimensionData.length > 0) {
323
+ const barWidth = xScale.bandwidth();
324
+ const labelX = xScale(dimension) + barWidth / 2;
325
+
326
+ // 添加维度标签
327
+ g.append("text")
328
+ .attr("x", labelX)
329
+ .attr("y", innerHeight + 20)
330
+ .attr("text-anchor", "middle")
331
+ .style("font-family", typography.label.font_family)
332
+ .style("font-size", typography.label.font_size)
333
+ .style("font-weight", typography.label.font_weight)
334
+ .style("fill", "#ffffff")
335
+ .text(dimension);
336
+
337
+ // 两个组的数据
338
+ const pointData = groups.map(group => {
339
+ const dataPoint = dimensionData.find(d => d[groupField] === group);
340
+ if (dataPoint) {
341
+ return {
342
+ group: group,
343
+ value: +dataPoint[valueField],
344
+ x: xScale(dimension) + barWidth / 2,
345
+ y: yScale(+dataPoint[valueField])
346
+ };
347
+ }
348
+ return null;
349
+ }).filter(d => d !== null);
350
+
351
+ // 绘制连接线(从最低值数据点到顶部边缘)
352
+ if (pointData.length > 0) {
353
+ // 找到具有最低值的数据点
354
+ const lowestValueData = pointData.reduce((lowest, current) =>
355
+ current.value < lowest.value ? current : lowest, pointData[0]);
356
+
357
+ if (lowestValueData) {
358
+ // 绘制连接线 - 更粗并精确到最上方刻度线
359
+ g.append("rect")
360
+ .attr("x", lowestValueData.x - 2) // 更粗的线
361
+ .attr("y", topmostTickY)
362
+ .attr("width", 4) // 更粗的线
363
+ .attr("height", lowestValueData.y - topmostTickY)
364
+ .attr("fill", "#ffffff");
365
+ }
366
+ }
367
+
368
+ // 添加每个数据点的标记(圆圈)
369
+ pointData.forEach((point, i) => {
370
+ // 绘制标记圆圈
371
+ g.append("circle")
372
+ .attr("cx", point.x)
373
+ .attr("cy", point.y)
374
+ .attr("r", 8)
375
+ .attr("fill", colorScale(point.group))
376
+ .attr("stroke", "white")
377
+ .attr("stroke-width", 2);
378
+
379
+ // 添加值标签
380
+ const formattedValue = chartUtils.format.fixed(point.value, 1, { unit: valueUnit }).text;
381
+
382
+ const labelFontSize = Math.min(20, Math.max(barWidth * 0.6, parseFloat(typography.annotation.font_size)));
383
+ const textBBox = chartUtils.text.measure(svg, formattedValue, {
384
+ fontFamily: typography.annotation.font_family,
385
+ fontSize: labelFontSize,
386
+ fontWeight: "bold",
387
+ });
388
+ const textPadding = 8; // 文本两侧的小填充
389
+ const labelWidth = textBBox.width + textPadding * 2;
390
+ const labelHeight = textBBox.height + textPadding;
391
+
392
+ // 创建样式化值标签 - 移到圆圈上方
393
+ g.append("rect")
394
+ .attr("x", point.x - labelWidth/2) // 水平居中
395
+ .attr("y", point.y - labelHeight - 12) // 在圆圈上方
396
+ .attr("width", labelWidth)
397
+ .attr("height", labelHeight)
398
+ .attr("rx", 6)
399
+ .attr("ry", 6)
400
+ .attr("fill", colorScale(point.group))
401
+ .attr("stroke", "white")
402
+ .attr("stroke-width", 1);
403
+
404
+ // 修改文本位置
405
+ g.append("text")
406
+ .attr("x", point.x) // 文本在矩形中央
407
+ .attr("y", point.y - labelHeight/2 - 12)
408
+ .attr("dy", "0.35em")
409
+ .attr("text-anchor", "middle")
410
+ .style("font-family", typography.annotation.font_family)
411
+ .style("font-size", `${Math.min(20,Math.max(barWidth * 0.6, parseFloat(typography.annotation.font_size)))}px`)
412
+ .style("font-weight", "bold")
413
+ .style("fill", "white")
414
+ .text(formattedValue);
415
+ });
416
+ }
417
+ });
418
+
419
+ // 返回SVG节点
420
+ return svg.node();
421
+ }