Ray1ee01 commited on
Commit
d6b711f
·
verified ·
1 Parent(s): 06b1b95

Upload folder using huggingface_hub

Browse files
modules/chart_engine/template/d3-js/type19_horizontal_bar_chart_with_ circle/horizontal_bar_chart_with_circle_plain_01.js ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Horizontal Bar Chart With Circle",
5
+ "chart_name": "horizontal_bar_chart_with_circle_plain_01",
6
+ "is_composite": true,
7
+ "required_fields": ["x", "y", "y2"],
8
+ "required_fields_type": [
9
+ ["categorical"],
10
+ ["numerical"],
11
+ ["numerical"]
12
+ ],
13
+ "required_fields_range": [[2, 30], [0, "inf"], [0, "inf"]],
14
+ "required_fields_icons": ["x"],
15
+ "required_other_icons": [],
16
+ "required_fields_colors": [],
17
+ "required_other_colors": ["primary", "secondary"],
18
+ "supported_effects": ["radius_corner", "spacing"],
19
+ "min_height": 400,
20
+ "min_width": 400,
21
+ "background": "no",
22
+ "icon_mark": "none",
23
+ "icon_label": "side",
24
+ "has_x_axis": "no",
25
+ "has_y_axis": "no"
26
+ }
27
+ REQUIREMENTS_END
28
+ */
29
+
30
+ // 水平条形图与比例圆复合图表实现 - 使用D3.js
31
+ function makeChart(containerSelector, data) {
32
+ const colorResolver = chartUtils.color.resolver(data);
33
+ // ---------- 1. 数据准备阶段 ----------
34
+
35
+ // 提取数据和配置
36
+ const jsonData = data;
37
+
38
+ // 获取实际数据数组 - 处理不同的数据结构形式
39
+ let chartData = [];
40
+ if (Array.isArray(jsonData.data)) {
41
+ chartData = jsonData.data;
42
+ } else if (jsonData.data && Array.isArray(jsonData.data.data)) {
43
+ chartData = jsonData.data.data;
44
+ }
45
+
46
+ // 提取其他配置
47
+ const variables = jsonData.variables || {};
48
+ const typography = jsonData.typography || {
49
+ title: { font_family: "Arial", font_size: "28px", font_weight: 700 },
50
+ label: { font_family: "Arial", font_size: "16px", font_weight: 500 },
51
+ description: { font_family: "Arial", font_size: "16px", font_weight: 500 },
52
+ annotation: { font_family: "Arial", font_size: "12px", font_weight: 400 }
53
+ };
54
+ const colors = jsonData.colors || {
55
+ text_color: "#000000",
56
+ background_color: "#FFFFFF",
57
+ other: { primary: "#4269d0", secondary: "#4269d0" },
58
+ available_colors: ["#4269d0", "#6cc5b0", "#3ca951", "#ff8ab7", "#a463f2", "#97bbf5"]
59
+ };
60
+ const images = jsonData.images || { field: {}, other: {} };
61
+ const dataColumns = chartUtils.schema.columns(jsonData);
62
+ const titles = jsonData.titles || {};
63
+
64
+ // 设置视觉效果变量的默认值
65
+ variables.has_rounded_corners = false;
66
+ variables.has_spacing = variables.has_spacing !== undefined ? variables.has_spacing : false;
67
+
68
+ // 清空容器
69
+ d3.select(containerSelector).html("");
70
+
71
+ // ---------- 2. 提取字段名和单位 ----------
72
+
73
+ // 根据数据列提取字段名
74
+ const dimensionField = chartUtils.schema.channel(jsonData, "x").key || "Country";
75
+ const valueField1 = chartUtils.schema.channel(jsonData, "y").key || "Crypto Ownership Percentage";
76
+ const valueField2 = chartUtils.schema.channel(jsonData, "y2").key || "Number of Owners";
77
+
78
+ // 获取字段单位
79
+ let valueUnit1 = chartUtils.schema.channel(jsonData, "y").unit === "none" ? "" :
80
+ chartUtils.schema.channel(jsonData, "y").unit;
81
+ let valueUnit2 = chartUtils.schema.channel(jsonData, "y2").unit === "none"? "" :
82
+ chartUtils.schema.channel(jsonData, "y2").unit;
83
+ valueUnit1 = valueUnit1 ? valueUnit1 : "";
84
+ valueUnit2 = valueUnit2 ? valueUnit2 : "";
85
+
86
+ // 列标题
87
+ const columnTitle1 = chartUtils.schema.channel(jsonData, "y").key || "Crypto Ownership Percentage";
88
+ const columnTitle2 = chartUtils.schema.channel(jsonData, "y2").key || "Number of Owners";
89
+
90
+ // ---------- 3. 数据处理 ----------
91
+
92
+ // 按第一个数值字段降序排序数据
93
+ const sortedData = [...chartData].sort((a, b) => +b[valueField1] - +a[valueField1]);
94
+ const sortedDimensions = sortedData.map(d => d[dimensionField]);
95
+
96
+ // 添加数值格式化函数
97
+
98
+ // ---------- 4. 文本测量 ----------
99
+
100
+ const labelMeasureOptions = {
101
+ fontFamily: typography.label.font_family,
102
+ fontSize: typography.label.font_size,
103
+ fontWeight: typography.label.font_weight
104
+ };
105
+
106
+ // 4.2 测量每个标签的宽度并找出最大值
107
+ const labelWidths = sortedDimensions.map(d => {
108
+ return chartUtils.text.measure(null, d.toUpperCase(), labelMeasureOptions).width;
109
+ });
110
+
111
+ // 4.3 测量数值标签的最大宽度
112
+ const valueWidths = sortedData.map(d => {
113
+ return chartUtils.text.measure(null, `${chartUtils.format.autoText(+d[valueField1])}${valueUnit1}`, labelMeasureOptions).width;
114
+ });
115
+
116
+ // 4.4 测量第二个数值标签的最大宽度
117
+ const value2Widths = sortedData.map(d => {
118
+ return chartUtils.text.measure(null, `${chartUtils.format.autoText(+d[valueField2])}${valueUnit2}`, labelMeasureOptions).width;
119
+ });
120
+
121
+ const maxLabelWidth = Math.max(...labelWidths);
122
+ const maxValueWidth = Math.max(...valueWidths);
123
+ const maxValue2Width = Math.max(...value2Widths);
124
+ // ---------- 5. 尺��和布局设置 ----------
125
+
126
+ // 图标尺寸
127
+ const flagWidth = 24;
128
+ const flagHeight = 24;
129
+ const flagMargin = 10;
130
+ const textIconGap = 10; // 标签文本和图标之间的间距
131
+ const valueLabelGap = 5; // 条形和值标签之间的间距
132
+
133
+ // 基于内容计算合适的图表总宽度
134
+ const leftSectionWidth = maxLabelWidth + flagWidth + flagMargin + textIconGap;
135
+
136
+ // 设置图表总尺寸
137
+ const minWidth = variables.width || 800;
138
+ const height = variables.height || 600;
139
+
140
+ // 设置边距
141
+ const margin = {
142
+ top: 100, // 顶部留出标题空间
143
+ right: 20, // 大幅增加右侧边距,确保圆形完全显示
144
+ bottom: 60, // 底部边距
145
+ left: Math.max(20, leftSectionWidth + 20) // 确保左侧边距足够容纳标签和图标
146
+ };
147
+
148
+ // 计算内部绘图区域尺寸
149
+ const innerWidth = minWidth - margin.left - margin.right;
150
+ const innerHeight = height - margin.top - margin.bottom;
151
+
152
+ // 设置条形图和圆形图的分配比例 - 条形图占更多空间
153
+ const barChartRatio = 0.8;
154
+ const circleChartRatio = 0.2;
155
+
156
+ // 计算条形图的最大宽度,保证值标签显示
157
+ const barChartWidth = innerWidth * barChartRatio - maxValueWidth - valueLabelGap;
158
+
159
+ // 计算圆形图部分的宽度
160
+ const circleChartWidth = innerWidth * circleChartRatio;
161
+
162
+ // 计算圆形的最大直径,确保不溢出右侧边距
163
+ const maxCircleDiameter = Math.min(circleChartWidth * 0.8, 80);
164
+
165
+ // 计算实际总宽度
166
+ const width = margin.left + barChartWidth + maxValueWidth + valueLabelGap + circleChartWidth + margin.right;
167
+
168
+ // ---------- 6. 创建比例尺 ----------
169
+
170
+ // 计算条形的额外间距
171
+ const barPadding = variables.has_spacing ? 0.3 : 0.2;
172
+
173
+ // Y轴比例尺(用于维度)
174
+ const yScale = d3.scaleBand()
175
+ .domain(sortedDimensions)
176
+ .range([0, innerHeight])
177
+ .padding(barPadding);
178
+
179
+ // 条形高度
180
+ const barHeight = yScale.bandwidth();
181
+
182
+ // X轴比例尺(用于第一个数值)- 条形图
183
+ const xScale = d3.scaleLinear()
184
+ .domain([0, d3.max(chartData, d => +d[valueField1]) * 1.05]) // 添加5%边距
185
+ .range([0, barChartWidth]);
186
+
187
+ // 圆形面积比例尺(用于第二个数值)
188
+ const maxValue2 = d3.max(chartData, d => +d[valueField2]);
189
+
190
+ // 计算最小和最大半径
191
+ const minRadius = Math.min(barHeight * 0.4, maxCircleDiameter * 0.2);
192
+ const maxRadius = Math.min(barHeight * (1 + barPadding/2), maxCircleDiameter/2);
193
+
194
+ const radiusScale = d3.scaleSqrt()
195
+ .domain([0, maxValue2])
196
+ .range([minRadius, maxRadius]);
197
+
198
+ // ---------- 7. 创建SVG容器 ----------
199
+
200
+ const svg = d3.select(containerSelector)
201
+ .append("svg")
202
+ .attr("width", "100%")
203
+ .attr("height", height)
204
+ .attr("viewBox", `0 0 ${width} ${height}`)
205
+ .attr("style", "max-width: 100%; height: auto;")
206
+ .attr("xmlns", "http://www.w3.org/2000/svg")
207
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
208
+
209
+ // ---------- 8. 添加标题 ----------
210
+
211
+ // 获取条形图颜色和圆形图颜色用于图例
212
+ const barColor = colorResolver.field(valueField1, 0, { fallbackKey: "primary" }).value;
213
+
214
+ const circleColor = colorResolver.field(valueField2, 0, { fallbackKey: "secondary" }).value;
215
+
216
+ // 标题宽度是总宽度的一半
217
+ const titleWidth = width / 2;
218
+
219
+ // 设置图例大小和间距
220
+ const legendSize = 16; // 图例方块/圆形大小
221
+ const legendMargin = 8; // 图例与文本间距
222
+ const legendTextOffset = legendSize + legendMargin; // 文本起始位置偏移
223
+
224
+ // 修改:左侧列标题 - 左对齐,占据图表一半宽度,支持自动换行
225
+ const leftTitleGroup = svg.append("g")
226
+ .attr("transform", `translate(0, ${margin.top - 30})`);
227
+
228
+ // 添加方形图例
229
+ leftTitleGroup.append("rect")
230
+ .attr("x", 0)
231
+ .attr("y", 0)
232
+ .attr("width", legendSize)
233
+ .attr("height", legendSize)
234
+ .attr("fill", barColor);
235
+
236
+ const leftTitle = leftTitleGroup.append("text")
237
+ .attr("x", legendTextOffset) // 从图例右侧开始
238
+ .attr("y", legendSize / 2) // 垂直居中对齐文本
239
+ .attr("dy", "0.35em")
240
+ .attr("text-anchor", "start") // 左对齐
241
+ .style("font-family", typography.description.font_family)
242
+ .style("font-size", typography.description.font_size)
243
+ .style("font-weight", typography.description.font_weight)
244
+ .style("fill", colorResolver.text({ fallback: "#333333" }).value);
245
+
246
+ // 为长文本实现换行
247
+ wrapText(leftTitle, columnTitle1, titleWidth - legendTextOffset, 1.2);
248
+
249
+ // 修改:右侧列标题 - 右对齐,占据图表一半宽度,圆形图例位于标题文本左侧
250
+ const rightTitleGroup = svg.append("g")
251
+ .attr("transform", `translate(0, ${margin.top - 30})`);
252
+
253
+ // 首先添加标题文本 - 右对齐到图表右边缘
254
+ const rightTitle = rightTitleGroup.append("text")
255
+ .attr("x", width) // 位于图表最右侧
256
+ .attr("y", legendSize / 2)
257
+ .attr("dy", "0.35em")
258
+ .attr("text-anchor", "end") // 右对齐
259
+ .style("font-family", typography.description.font_family)
260
+ .style("font-size", typography.description.font_size)
261
+ .style("font-weight", typography.description.font_weight)
262
+ .style("fill", colorResolver.text({ fallback: "#333333" }).value)
263
+ .text(columnTitle2);
264
+
265
+ // 测量文本宽度以便正确放置图例
266
+ const rightTitleWidth = chartUtils.text.measure(rightTitleGroup, columnTitle2, {
267
+ fontFamily: typography.description.font_family,
268
+ fontSize: typography.description.font_size,
269
+ fontWeight: typography.description.font_weight
270
+ }).width;
271
+
272
+ // 添加圆形图例 - 位于文本左侧
273
+ rightTitleGroup.append("circle")
274
+ .attr("cx", width - rightTitleWidth - legendMargin - 5)
275
+ .attr("cy", legendSize / 2)
276
+ .attr("r", legendSize/2)
277
+ .attr("fill", circleColor);
278
+
279
+ // 文本换行函数 - 只用于左侧标题,右侧标题现在不需要换行
280
+ function wrapText(text, str, width, lineHeight) {
281
+ const words = str.split(/\s+/).reverse();
282
+ let word;
283
+ let line = [];
284
+ let lineNumber = 0;
285
+ const y = text.attr("y");
286
+ const dy = parseFloat(text.attr("dy") || 0);
287
+ const textMeasureOptions = {
288
+ fontFamily: text.style("font-family") || typography.description.font_family,
289
+ fontSize: parseFloat(text.style("font-size")) || parseFloat(typography.description.font_size),
290
+ fontWeight: text.style("font-weight") || typography.description.font_weight
291
+ };
292
+ const measureTextWidth = value => chartUtils.text.measure(null, value, textMeasureOptions).width;
293
+ let tspan = text.text(null).append("tspan")
294
+ .attr("x", text.attr("x"))
295
+ .attr("y", y)
296
+ .attr("dy", dy + "em");
297
+
298
+ while (word = words.pop()) {
299
+ line.push(word);
300
+ tspan.text(line.join(" "));
301
+ if (measureTextWidth(line.join(" ")) > width) {
302
+ line.pop();
303
+ tspan.text(line.join(" "));
304
+ line = [word];
305
+ tspan = text.append("tspan")
306
+ .attr("x", text.attr("x"))
307
+ .attr("y", y)
308
+ .attr("dy", ++lineNumber * lineHeight + dy + "em")
309
+ .text(word);
310
+ }
311
+ }
312
+ }
313
+
314
+ // ---------- 9. 创建主图表组 ----------
315
+
316
+ const g = svg.append("g")
317
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
318
+
319
+ // ---------- 10. 绘制元素 ----------
320
+
321
+ // 获取条形图颜色
322
+ const getBarColor = () => {
323
+ return colorResolver.field(valueField1, 0, { fallback: colorResolver.other("primary").value }).value;
324
+ };
325
+
326
+ // 获取圆形图颜色
327
+ const getCircleColor = () => {
328
+ return colorResolver.field(valueField2, 0, { fallback: colorResolver.other("secondary").value }).value;
329
+ };
330
+
331
+ // 为每个维度绘制内容
332
+ sortedDimensions.forEach((dimension, index) => {
333
+ try {
334
+ const dataPoint = chartData.find(d => d[dimensionField] === dimension);
335
+
336
+ if (!dataPoint) {
337
+ console.error(`No data found for dimension: ${dimension}`);
338
+ return;
339
+ }
340
+
341
+ // 检查数据值是否有效
342
+ if (isNaN(+dataPoint[valueField1]) || isNaN(+dataPoint[valueField2])) {
343
+ console.error(`Invalid data values for ${dimension}: ${dataPoint[valueField1]}, ${dataPoint[valueField2]}`);
344
+ return;
345
+ }
346
+
347
+ const y = yScale(dimension);
348
+ if (typeof y !== 'number') {
349
+ console.error(`Invalid y position for dimension: ${dimension}`);
350
+ return;
351
+ }
352
+
353
+ const barHeight = yScale.bandwidth();
354
+ const centerY = y + barHeight / 2;
355
+ const barWidthValue = xScale(+dataPoint[valueField1]);
356
+
357
+ // 1. 添加国家/地区名称 - 右对齐
358
+ // 标签位置在最左侧,右对齐
359
+ const labelX = -flagWidth - flagMargin - textIconGap;
360
+
361
+ g.append("text")
362
+ .attr("x", labelX)
363
+ .attr("y", centerY)
364
+ .attr("dy", "0.35em")
365
+ .attr("text-anchor", "end") // 右对齐
366
+ .style("font-family", typography.label.font_family)
367
+ .style("font-size", typography.label.font_size)
368
+ .style("font-weight", typography.label.font_weight)
369
+ .style("fill", colorResolver.text({ fallback: "#333333" }).value)
370
+ .text(dimension.toUpperCase());
371
+
372
+ // 2. 添加国家/地区图标 - 修改:直接使用矩形图标
373
+ if (images.field && images.field[dimension]) {
374
+ // 创建一个组来包含图像
375
+ const iconGroup = g.append("g")
376
+ .attr("transform", `translate(${-flagWidth - flagMargin}, ${centerY - flagHeight/2})`);
377
+
378
+ // 直接添加国家/地区图标,不使用圆形裁剪
379
+ iconGroup.append("image")
380
+ .attr("x", 0)
381
+ .attr("y", 0)
382
+ .attr("width", flagWidth)
383
+ .attr("height", flagHeight)
384
+ .attr("preserveAspectRatio","xMidYMid meet")
385
+ .attr("xlink:href", images.field[dimension]);
386
+ }
387
+
388
+ // 3. 绘制条形 - 只使用矩形
389
+ g.append("rect")
390
+ .attr("x", 0)
391
+ .attr("y", y)
392
+ .attr("width", barWidthValue)
393
+ .attr("height", barHeight)
394
+ .attr("fill", getBarColor())
395
+ .attr("opacity", 0.9);
396
+
397
+ // 4. 添加条形数值标签
398
+ const formattedValue1 = `${chartUtils.format.autoText(+dataPoint[valueField1])}${valueUnit1}`;
399
+
400
+ g.append("text")
401
+ .attr("x", barWidthValue + valueLabelGap)
402
+ .attr("y", centerY)
403
+ .attr("dy", "0.35em")
404
+ .attr("text-anchor", "start")
405
+ .style("font-family", typography.label.font_family)
406
+ .style("font-size", typography.label.font_size)
407
+ .style("font-weight", typography.label.font_weight)
408
+ .style("fill", colorResolver.text({ fallback: "#333333" }).value)
409
+ .text(formattedValue1);
410
+
411
+ // 5. 计算圆形位置
412
+ // 确保圆形居中放置
413
+ const circleX = barChartWidth + maxValueWidth + valueLabelGap + circleChartWidth / 2;
414
+ const circleRadius = radiusScale(+dataPoint[valueField2]);
415
+
416
+ // 绘制圆形
417
+ g.append("circle")
418
+ .attr("cx", circleX)
419
+ .attr("cy", centerY)
420
+ .attr("r", circleRadius)
421
+ .attr("fill", getCircleColor())
422
+ .attr("opacity", 0.7);
423
+
424
+ // 6. 添加圆形数值标签
425
+ const formattedValue2 = `${chartUtils.format.autoText(+dataPoint[valueField2])}${valueUnit2}`;
426
+ g.append("text")
427
+ .attr("x", circleX)
428
+ .attr("y", centerY)
429
+ .attr("dy", "0.35em")
430
+ .attr("text-anchor", "middle")
431
+ .style("font-family", typography.label.font_family)
432
+ .style("font-size", typography.label.font_size)
433
+ .style("font-weight", typography.label.font_weight)
434
+ .style("fill", colorResolver.text({ fallback: "#333333" }).value)
435
+ .text(formattedValue2);
436
+
437
+ } catch (error) {
438
+ console.error(`Error rendering chart element for ${dimension}:`, error);
439
+ }
440
+ });
441
+
442
+ // 返回SVG节点
443
+ return svg.node();
444
+ }
modules/chart_engine/template/d3-js/type19_horizontal_bar_chart_with_ circle/horizontal_bar_chart_with_circle_plain_02.js ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Horizontal Bar Chart With Circle",
5
+ "chart_name": "horizontal_bar_chart_with_circle_plain_02",
6
+ "is_composite": true,
7
+ "required_fields": ["x", "y", "y2"],
8
+ "required_fields_type": [
9
+ ["categorical"],
10
+ ["numerical"],
11
+ ["numerical"]
12
+ ],
13
+ "required_fields_range": [[2, 30], [0, "inf"], [0, "inf"]],
14
+ "required_fields_icons": ["x"],
15
+ "required_other_icons": [],
16
+ "required_fields_colors": [],
17
+ "required_other_colors": ["primary", "secondary"],
18
+ "supported_effects": ["radius_corner", "spacing"],
19
+ "min_height": 400,
20
+ "min_width": 400,
21
+ "background": "no",
22
+ "icon_mark": "none",
23
+ "icon_label": "side",
24
+ "has_x_axis": "no",
25
+ "has_y_axis": "no"
26
+ }
27
+ REQUIREMENTS_END
28
+ */
29
+
30
+ // 水平条形图与比例圆复合图表实现 - 使用D3.js
31
+ function makeChart(containerSelector, data) {
32
+ const colorResolver = chartUtils.color.resolver(data);
33
+ // ---------- 1. 数据准备阶段 ----------
34
+
35
+ // 提取数据和配置
36
+ const jsonData = data;
37
+
38
+ // 获取实际数据数组 - 处理不同的数据结构形式
39
+ let chartData = [];
40
+ if (Array.isArray(jsonData.data)) {
41
+ chartData = jsonData.data;
42
+ } else if (jsonData.data && Array.isArray(jsonData.data.data)) {
43
+ chartData = jsonData.data.data;
44
+ }
45
+
46
+ // 提取其他配置
47
+ const variables = jsonData.variables || {};
48
+ const typography = jsonData.typography || {
49
+ title: { font_family: "Arial", font_size: "28px", font_weight: 700 },
50
+ label: { font_family: "Arial", font_size: "16px", font_weight: 500 },
51
+ description: { font_family: "Arial", font_size: "16px", font_weight: 500 },
52
+ annotation: { font_family: "Arial", font_size: "12px", font_weight: 400 }
53
+ };
54
+ const colors = jsonData.colors || {
55
+ text_color: "#000000",
56
+ background_color: "#FFFFFF",
57
+ other: { primary: "#4269d0", secondary: "#4269d0" },
58
+ available_colors: ["#4269d0", "#6cc5b0", "#3ca951", "#ff8ab7", "#a463f2", "#97bbf5"]
59
+ };
60
+ const images = jsonData.images || { field: {}, other: {} };
61
+ const dataColumns = chartUtils.schema.columns(jsonData);
62
+ const titles = jsonData.titles || {};
63
+
64
+ // 设置视觉效果变量的默认值
65
+ variables.has_rounded_corners = false;
66
+ variables.has_spacing = variables.has_spacing !== undefined ? variables.has_spacing : false;
67
+
68
+ // 清空容器
69
+ d3.select(containerSelector).html("");
70
+
71
+ // ---------- 2. 提取字段名和单位 ----------
72
+
73
+ // 根据数据列提取字段名
74
+ const dimensionField = chartUtils.schema.channel(jsonData, "x").key || "Country";
75
+ const valueField1 = chartUtils.schema.channel(jsonData, "y").key || "Crypto Ownership Percentage";
76
+ const valueField2 = chartUtils.schema.channel(jsonData, "y2").key || "Number of Owners";
77
+
78
+ // 获取字段单位
79
+ let valueUnit1 = chartUtils.schema.channel(jsonData, "y").unit === "none" ? "" :
80
+ chartUtils.schema.channel(jsonData, "y").unit;
81
+ let valueUnit2 = chartUtils.schema.channel(jsonData, "y2").unit === "none"? "" :
82
+ chartUtils.schema.channel(jsonData, "y2").unit;
83
+ valueUnit1 = valueUnit1 ? valueUnit1 : "";
84
+ valueUnit2 = valueUnit2 ? valueUnit2 : "";
85
+
86
+ // 列标题
87
+ const columnTitle1 = chartUtils.schema.channel(jsonData, "y").key || "Crypto Ownership Percentage";
88
+ const columnTitle2 = chartUtils.schema.channel(jsonData, "y2").key || "Number of Owners";
89
+
90
+ // ---------- 3. 数据处理 ----------
91
+
92
+ // 按第一个数值字段降序排序数据
93
+ const sortedData = [...chartData].sort((a, b) => +b[valueField1] - +a[valueField1]);
94
+ const sortedDimensions = sortedData.map(d => d[dimensionField]);
95
+
96
+ // 添加数值格式化函数
97
+
98
+ // ---------- 4. 文本测量 ----------
99
+
100
+ const labelMeasureOptions = {
101
+ fontFamily: typography.label.font_family,
102
+ fontSize: typography.label.font_size,
103
+ fontWeight: typography.label.font_weight
104
+ };
105
+
106
+ // 4.2 测量每个标签的宽度并找出最大值
107
+ const labelWidths = sortedDimensions.map(d => {
108
+ return chartUtils.text.measure(null, d.toUpperCase(), labelMeasureOptions).width;
109
+ });
110
+
111
+ // 4.3 测量数值标签的最大宽度
112
+ const valueWidths = sortedData.map(d => {
113
+ return chartUtils.text.measure(null, `${chartUtils.format.autoText(+d[valueField1])}${valueUnit1}`, labelMeasureOptions).width;
114
+ });
115
+
116
+ // 4.4 测量第二个数值标签的最大宽度
117
+ const value2Widths = sortedData.map(d => {
118
+ return chartUtils.text.measure(null, `${chartUtils.format.autoText(+d[valueField2])}${valueUnit2}`, labelMeasureOptions).width;
119
+ });
120
+
121
+ const maxLabelWidth = Math.max(...labelWidths);
122
+ const maxValueWidth = Math.max(...valueWidths);
123
+ const maxValue2Width = Math.max(...value2Widths);
124
+ // ---------- 5. 尺��和布局设置 ----------
125
+
126
+ // 图标尺寸
127
+ const flagWidth = 24;
128
+ const flagHeight = 24;
129
+ const flagMargin = 10;
130
+ const textIconGap = 10; // 标签文本和图标之间的间距
131
+ const valueLabelGap = 5; // 条形和值标签之间的间距
132
+
133
+ // 基于内容计算合适的图表总宽度
134
+ const leftSectionWidth = maxLabelWidth + flagWidth + flagMargin + textIconGap;
135
+
136
+ // 设置图表总尺寸
137
+ const minWidth = variables.width || 800;
138
+ const height = variables.height || 600;
139
+
140
+ // 设置边距
141
+ const margin = {
142
+ top: 100, // 顶部留出标题空间
143
+ right: 20, // 大幅增加右侧边距,确保圆形完全显示
144
+ bottom: 60, // 底部边距
145
+ left: Math.max(20, leftSectionWidth + 20) // 确保左侧边距足够容纳标签和图标
146
+ };
147
+
148
+ // 计算内部绘图区域尺寸
149
+ const innerWidth = minWidth - margin.left - margin.right;
150
+ const innerHeight = height - margin.top - margin.bottom;
151
+
152
+ // 设置条形图和圆形图的分配比例 - 条形图占更多空间
153
+ const barChartRatio = 0.8;
154
+ const circleChartRatio = 0.2;
155
+
156
+ // 计算条形图的最大宽度,保证值标签显示
157
+ const barChartWidth = innerWidth * barChartRatio - maxValueWidth - valueLabelGap;
158
+
159
+ // 计算圆形图部分的宽度
160
+ const circleChartWidth = innerWidth * circleChartRatio;
161
+
162
+ // 计算圆形的最大直径,确保不溢出右侧边距
163
+ const maxCircleDiameter = Math.min(circleChartWidth * 0.8, 80);
164
+
165
+ // 计算实际总宽度
166
+ const width = margin.left + barChartWidth + maxValueWidth + valueLabelGap + circleChartWidth + margin.right;
167
+
168
+ // ---------- 6. 创建比例尺 ----------
169
+
170
+ // 计算条形的额外间距
171
+ const barPadding = variables.has_spacing ? 0.3 : 0.2;
172
+
173
+ // Y轴比例尺(用于维度)
174
+ const yScale = d3.scaleBand()
175
+ .domain(sortedDimensions)
176
+ .range([0, innerHeight])
177
+ .padding(barPadding);
178
+
179
+ // 条形高度
180
+ const barHeight = yScale.bandwidth();
181
+
182
+ // X轴比例尺(用于第一个数值)- 条形图
183
+ const xScale = d3.scaleLinear()
184
+ .domain([0, d3.max(chartData, d => +d[valueField1]) * 1.05]) // 添加5%边距
185
+ .range([0, barChartWidth]);
186
+
187
+ // 圆形面积比例尺(用于第二个数值)
188
+ const maxValue2 = d3.max(chartData, d => +d[valueField2]);
189
+
190
+ // 计算最小和最大半径
191
+ const minRadius = Math.min(barHeight * 0.4, maxCircleDiameter * 0.2);
192
+ const maxRadius = Math.min(barHeight * (1 + barPadding/2), maxCircleDiameter/2);
193
+
194
+ const radiusScale = d3.scaleSqrt()
195
+ .domain([0, maxValue2])
196
+ .range([minRadius, maxRadius]);
197
+
198
+ // ---------- 7. 创建SVG容器 ----------
199
+
200
+ const svg = d3.select(containerSelector)
201
+ .append("svg")
202
+ .attr("width", "100%")
203
+ .attr("height", height)
204
+ .attr("viewBox", `0 0 ${width} ${height}`)
205
+ .attr("style", "max-width: 100%; height: auto;")
206
+ .attr("xmlns", "http://www.w3.org/2000/svg")
207
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
208
+
209
+ // ---------- 8. 添加标题 ----------
210
+
211
+ // 获取条形图颜色和圆形图颜色用于图例
212
+ const barColor = colorResolver.field(valueField1, 0, { fallbackKey: "primary" }).value;
213
+
214
+ const circleColor = colorResolver.field(valueField2, 0, { fallbackKey: "secondary" }).value;
215
+
216
+ // 标题宽度是总宽度的一半
217
+ const titleWidth = width / 2;
218
+
219
+ // 设置图例大小和间距
220
+ const legendSize = 16; // 图例方块/圆形大小
221
+ const legendMargin = 8; // 图例与文本间距
222
+ const legendTextOffset = legendSize + legendMargin; // 文本起始位置偏移
223
+
224
+ // 修改:左侧列标题 - 左对齐,占据图表一半宽度,支持自动换行
225
+ const leftTitleGroup = svg.append("g")
226
+ .attr("transform", `translate(0, ${margin.top - 30})`);
227
+
228
+ // 添加方形图例
229
+ leftTitleGroup.append("rect")
230
+ .attr("x", 0)
231
+ .attr("y", 0)
232
+ .attr("width", legendSize)
233
+ .attr("height", legendSize)
234
+ .attr("fill", barColor);
235
+
236
+ const leftTitle = leftTitleGroup.append("text")
237
+ .attr("x", legendTextOffset) // 从图例右侧开始
238
+ .attr("y", legendSize / 2) // 垂直居中对齐文本
239
+ .attr("dy", "0.35em")
240
+ .attr("text-anchor", "start") // 左对齐
241
+ .style("font-family", typography.description.font_family)
242
+ .style("font-size", typography.description.font_size)
243
+ .style("font-weight", typography.description.font_weight)
244
+ .style("fill", colorResolver.text({ fallback: "#333333" }).value);
245
+
246
+ // 为长文本实现换行
247
+ wrapText(leftTitle, columnTitle1, titleWidth - legendTextOffset, 1.2);
248
+
249
+ // 修改:右侧列标题 - 右对齐,占据图表一半宽度,圆形图例位于标题文本左侧
250
+ const rightTitleGroup = svg.append("g")
251
+ .attr("transform", `translate(0, ${margin.top - 30})`);
252
+
253
+ // 首先添加标题文本 - 右对齐到图表右边缘
254
+ const rightTitle = rightTitleGroup.append("text")
255
+ .attr("x", width) // 位于图表最右侧
256
+ .attr("y", legendSize / 2)
257
+ .attr("dy", "0.35em")
258
+ .attr("text-anchor", "end") // 右对齐
259
+ .style("font-family", typography.description.font_family)
260
+ .style("font-size", typography.description.font_size)
261
+ .style("font-weight", typography.description.font_weight)
262
+ .style("fill", colorResolver.text({ fallback: "#333333" }).value)
263
+ .text(columnTitle2);
264
+
265
+ // 测量文本宽度以便正确放置图例
266
+ const rightTitleWidth = chartUtils.text.measure(rightTitleGroup, columnTitle2, {
267
+ fontFamily: typography.description.font_family,
268
+ fontSize: typography.description.font_size,
269
+ fontWeight: typography.description.font_weight
270
+ }).width;
271
+
272
+ // 添加圆形图例 - 位于文本左侧
273
+ rightTitleGroup.append("circle")
274
+ .attr("cx", width - rightTitleWidth - legendMargin - 5)
275
+ .attr("cy", legendSize / 2)
276
+ .attr("r", legendSize/2)
277
+ .attr("fill", circleColor);
278
+
279
+ // 文本换行函数 - 只用于左侧标题,右侧标题现在不需要换行
280
+ function wrapText(text, str, width, lineHeight) {
281
+ const words = str.split(/\s+/).reverse();
282
+ let word;
283
+ let line = [];
284
+ let lineNumber = 0;
285
+ const y = text.attr("y");
286
+ const dy = parseFloat(text.attr("dy") || 0);
287
+ const textMeasureOptions = {
288
+ fontFamily: text.style("font-family") || typography.description.font_family,
289
+ fontSize: parseFloat(text.style("font-size")) || parseFloat(typography.description.font_size),
290
+ fontWeight: text.style("font-weight") || typography.description.font_weight
291
+ };
292
+ const measureTextWidth = value => chartUtils.text.measure(null, value, textMeasureOptions).width;
293
+ let tspan = text.text(null).append("tspan")
294
+ .attr("x", text.attr("x"))
295
+ .attr("y", y)
296
+ .attr("dy", dy + "em");
297
+
298
+ while (word = words.pop()) {
299
+ line.push(word);
300
+ tspan.text(line.join(" "));
301
+ if (measureTextWidth(line.join(" ")) > width) {
302
+ line.pop();
303
+ tspan.text(line.join(" "));
304
+ line = [word];
305
+ tspan = text.append("tspan")
306
+ .attr("x", text.attr("x"))
307
+ .attr("y", y)
308
+ .attr("dy", ++lineNumber * lineHeight + dy + "em")
309
+ .text(word);
310
+ }
311
+ }
312
+ }
313
+
314
+ // ---------- 9. 创建主图表组 ----------
315
+
316
+ const g = svg.append("g")
317
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
318
+
319
+ // ---------- 10. 绘制元素 ----------
320
+
321
+ // 获取条形图颜色
322
+ const getBarColor = () => {
323
+ return colorResolver.field(valueField1, 0, { fallback: colorResolver.other("primary").value }).value;
324
+ };
325
+
326
+ // 获取圆形图颜色
327
+ const getCircleColor = () => {
328
+ return colorResolver.field(valueField2, 0, { fallback: colorResolver.other("secondary").value }).value;
329
+ };
330
+
331
+ // 为每个维度绘制内容
332
+ sortedDimensions.forEach((dimension, index) => {
333
+ try {
334
+ const dataPoint = chartData.find(d => d[dimensionField] === dimension);
335
+
336
+ if (!dataPoint) {
337
+ console.error(`No data found for dimension: ${dimension}`);
338
+ return;
339
+ }
340
+
341
+ // 检查数据值是否有效
342
+ if (isNaN(+dataPoint[valueField1]) || isNaN(+dataPoint[valueField2])) {
343
+ console.error(`Invalid data values for ${dimension}: ${dataPoint[valueField1]}, ${dataPoint[valueField2]}`);
344
+ return;
345
+ }
346
+
347
+ const y = yScale(dimension);
348
+ if (typeof y !== 'number') {
349
+ console.error(`Invalid y position for dimension: ${dimension}`);
350
+ return;
351
+ }
352
+
353
+ const barHeight = yScale.bandwidth();
354
+ const centerY = y + barHeight / 2;
355
+ const barWidthValue = xScale(+dataPoint[valueField1]);
356
+
357
+ // 1. 添加国家/地区名称 - 右对齐
358
+ // 标签位置在最左侧,右对齐
359
+ const labelX = -flagWidth - flagMargin - textIconGap;
360
+
361
+ g.append("text")
362
+ .attr("x", labelX)
363
+ .attr("y", centerY)
364
+ .attr("dy", "0.35em")
365
+ .attr("text-anchor", "end") // 右对齐
366
+ .style("font-family", typography.label.font_family)
367
+ .style("font-size", typography.label.font_size)
368
+ .style("font-weight", typography.label.font_weight)
369
+ .style("fill", colorResolver.text({ fallback: "#333333" }).value)
370
+ .text(dimension.toUpperCase());
371
+
372
+ // 2. 添加国家/地区图标 - 修改:直接使用矩形图标
373
+ if (images.field && images.field[dimension]) {
374
+ // 创建一个组来包含图像
375
+ const iconGroup = g.append("g")
376
+ .attr("transform", `translate(${-flagWidth - flagMargin}, ${centerY - flagHeight/2})`);
377
+
378
+ // 直接添加国家/地区图标,不使用圆形裁剪
379
+ iconGroup.append("image")
380
+ .attr("x", 0)
381
+ .attr("y", 0)
382
+ .attr("width", flagWidth)
383
+ .attr("height", flagHeight)
384
+ .attr("preserveAspectRatio","xMidYMid meet")
385
+ .attr("xlink:href", images.field[dimension]);
386
+ }
387
+
388
+ // 3. 绘制条形 - 只使用矩形
389
+ g.append("rect")
390
+ .attr("x", 0)
391
+ .attr("y", y)
392
+ .attr("width", barWidthValue)
393
+ .attr("height", barHeight)
394
+ .attr("fill", getBarColor())
395
+ .attr("rx",barHeight/2)
396
+ .attr("ry",barHeight/2)
397
+ .attr("opacity", 0.9);
398
+
399
+ // 4. 添加条形数值标签
400
+ const formattedValue1 = `${chartUtils.format.autoText(+dataPoint[valueField1])}${valueUnit1}`;
401
+
402
+ g.append("text")
403
+ .attr("x", barWidthValue + valueLabelGap)
404
+ .attr("y", centerY)
405
+ .attr("dy", "0.35em")
406
+ .attr("text-anchor", "start")
407
+ .style("font-family", typography.label.font_family)
408
+ .style("font-size", typography.label.font_size)
409
+ .style("font-weight", typography.label.font_weight)
410
+ .style("fill", colorResolver.text({ fallback: "#333333" }).value)
411
+ .text(formattedValue1);
412
+
413
+ // 5. 计算圆形位置
414
+ // 确保圆形居中放置
415
+ const circleX = barChartWidth + maxValueWidth + valueLabelGap + circleChartWidth / 2;
416
+ const circleRadius = radiusScale(+dataPoint[valueField2]);
417
+
418
+ // 绘制圆形
419
+ g.append("circle")
420
+ .attr("cx", circleX)
421
+ .attr("cy", centerY)
422
+ .attr("r", circleRadius)
423
+ .attr("fill", getCircleColor())
424
+ .attr("opacity", 0.7);
425
+
426
+ // 6. 添加圆形数值标签
427
+ const formattedValue2 = `${chartUtils.format.autoText(+dataPoint[valueField2])}${valueUnit2}`;
428
+ g.append("text")
429
+ .attr("x", circleX)
430
+ .attr("y", centerY)
431
+ .attr("dy", "0.35em")
432
+ .attr("text-anchor", "middle")
433
+ .style("font-family", typography.label.font_family)
434
+ .style("font-size", typography.label.font_size)
435
+ .style("font-weight", typography.label.font_weight)
436
+ .style("fill", colorResolver.text({ fallback: "#333333" }).value)
437
+ .text(formattedValue2);
438
+
439
+ } catch (error) {
440
+ console.error(`Error rendering chart element for ${dimension}:`, error);
441
+ }
442
+ });
443
+
444
+ // 返回SVG节点
445
+ return svg.node();
446
+ }