Ray1ee01 commited on
Commit
5db3106
·
verified ·
1 Parent(s): 3a7a505

Upload folder using huggingface_hub

Browse files
modules/chart_engine/template/d3-js/type13_pictorial_percentage_bar_chart_01/pictorial_percentage_bar_plain_chart_01.js ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Pictorial Percentage Bar Chart",
5
+ "chart_name": "pictorial_percentage_bar_plain_chart_01",
6
+ "is_composite": false,
7
+ "required_fields": ["x", "y"],
8
+ "required_fields_type": [["categorical"], ["numerical"]],
9
+ "required_fields_range": [[2, 10], [0, "inf"]],
10
+ "required_fields_icons": ["x"],
11
+ "required_other_icons": [],
12
+ "required_fields_colors": [],
13
+ "required_other_colors": [],
14
+ "supported_effects": [],
15
+ "min_height": 400,
16
+ "min_width": 400,
17
+ "background": "no",
18
+ "icon_mark": "none",
19
+ "icon_label": "side",
20
+ "has_x_axis": "no",
21
+ "has_y_axis": "no"
22
+ }
23
+ REQUIREMENTS_END
24
+ */
25
+
26
+ // 实现花瓶形状的柱状图 - 使用D3.js
27
+ function makeChart(containerSelector, data) {
28
+ const colorResolver = chartUtils.color.resolver(data);
29
+ // ---------- 1. 数据准备 ----------
30
+ // 提取数据和配置
31
+ const jsonData = data; // 完整的JSON数据对象
32
+ const chartData = jsonData.data.data || []; // 图表数据
33
+ const variables = jsonData.variables || {}; // 图表配置,如果不存在则使用空对象
34
+ const typography = jsonData.typography || { // 字体设置,如果不存在则使用默认值
35
+ title: { font_family: "Arial", font_size: "28px", font_weight: 700 },
36
+ label: { font_family: "Arial", font_size: "16px", font_weight: 500 },
37
+ description: { font_family: "Arial", font_size: "16px", font_weight: 500 },
38
+ annotation: { font_family: "Arial", font_size: "12px", font_weight: 400 }
39
+ };
40
+ const colors = jsonData.colors || {
41
+ text_color: "#000000",
42
+ other: { primary: "#4682B4", secondary: "#FF7F50" }
43
+ };
44
+ const images = jsonData.images || { field: {}, other: {} }; // 图像(国旗等)
45
+ const dataColumns = chartUtils.schema.columns(jsonData); // 使用data_columns
46
+
47
+
48
+ // 清空容器
49
+ d3.select(containerSelector).html("");
50
+
51
+ // ---------- 2. 提取字段名称 ----------
52
+ let xField, yField;
53
+
54
+ // 安全地提取字段名称
55
+ const xChannel = chartUtils.schema.channel(jsonData, "x");
56
+ const yChannel = chartUtils.schema.channel(jsonData, "y");
57
+
58
+ xField = xChannel.key;
59
+ yField = yChannel.key;
60
+
61
+ // 获取字段单位(如果存在)
62
+ let dimensionUnit = "";
63
+ let valueUnit = ""; // 默认为百分比
64
+
65
+ if (chartUtils.schema.channel(jsonData, "x").unit !== "none") {
66
+ dimensionUnit = chartUtils.schema.channel(jsonData, "x").unit;
67
+ }
68
+
69
+ if (chartUtils.schema.channel(jsonData, "y").unit !== "none") {
70
+ valueUnit = chartUtils.schema.channel(jsonData, "y").unit;
71
+ }
72
+ // 添加数值格式化函数
73
+ // ---------- 3. 数据处理 ----------
74
+ // 按照y值降序排序数据
75
+ const sortedData = [...chartData].sort((a, b) => b[yField] - a[yField]);
76
+
77
+ // ---------- 4. 尺寸和布局设置 ----------
78
+ const width = variables.width || 800;
79
+ const height = variables.height || 600;
80
+
81
+ // 边距: 上, 右, 下, 左
82
+ const margin = {
83
+ top: 120, // 减少顶部空间
84
+ right: 30,
85
+ bottom: 80, // 国旗和标签的空间
86
+ left: 30
87
+ };
88
+
89
+ // 计算实际绘图区域尺寸
90
+ const innerWidth = width - margin.left - margin.right;
91
+ const innerHeight = height - margin.top - margin.bottom;
92
+
93
+ // ---------- 5. 创建SVG容器 ----------
94
+ const svg = d3.select(containerSelector)
95
+ .append("svg")
96
+ .attr("width", width)
97
+ .attr("height", height)
98
+ .attr("viewBox", `0 0 ${width} ${height}`)
99
+ .attr("style", "max-width: 100%; height: auto;")
100
+ .attr("xmlns", "http://www.w3.org/2000/svg")
101
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
102
+
103
+
104
+ // 创建主图表组
105
+ const chart = svg.append("g")
106
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
107
+
108
+ // ---------- 6. 创建比例尺 ----------
109
+ // X轴比例尺(瓶子之间的间隔)
110
+ const xScale = d3.scaleBand()
111
+ .domain(sortedData.map(d => d[xField]))
112
+ .range([0, innerWidth])
113
+ .padding(0.2);
114
+
115
+ // Y轴比例尺(瓶子的高度)
116
+ const yMax = d3.max(sortedData, d => +d[yField]);
117
+ const yScale = d3.scaleLinear()
118
+ .domain([0, yMax * 1.1]) // 添加10%的空间
119
+ .range([innerHeight, 0]);
120
+
121
+ // ---------- 7. 创建瓶子和蒙版 ----------
122
+ // 定义瓶子的宽度和高度 (不修改这部分)
123
+ const bottleWidth = xScale.bandwidth();
124
+ const bottleHeight = Math.max(bottleWidth * 3, innerHeight * 0.5);
125
+
126
+ // 获取瓶子的主要颜色
127
+ const bottleColor = colorResolver.other("primary", { fallback: "#4CAF50" }).value;
128
+
129
+ // 定义国旗和文本的空间
130
+ const flagTextHeight = 50;
131
+
132
+ // 计算瓶子底部应在的位置 - 让瓶子底部接近国旗
133
+ const bottleBottomY = innerHeight - 10; // 瓶子底部位置,紧贴国旗上方
134
+ const bottleTopY = bottleBottomY - bottleHeight; // 瓶子顶部位置
135
+
136
+ // 创建一个数组来存储所有的值标签元素引用
137
+ const valueLabels = [];
138
+
139
+ const bottleImageSVG = `<svg viewBox="0 0 200 500" xmlns="http://www.w3.org/2000/svg">
140
+ <path d="M60,280 L60,450 Q100,480 140,450 L140,280 Z" fill="#8BC34A"/>
141
+
142
+ <path d="M60,230 L60,280 L140,280 L140,230 Z" fill="#FDD835"/>
143
+ <circle cx="100" cy="255" r="25" fill="#FDD835"/>
144
+
145
+ <path d="M75,50 Q100,40 125,50 L125,70 Q100,80 75,70 Z" fill="#555555"/>
146
+ <path d="M80,70 L60,230 L140,230 L120,70 Z" fill="#555555"/>
147
+
148
+ <path d="M80,70 Q100,60 120,70 L120,70 Q100,80 80,70 Z" fill="#555555"/>
149
+ </svg>`;
150
+
151
+ // 辅助函数:将字符串转换为 base64
152
+ function svgToBase64(svg) {
153
+ // 移除所有换行符和多余的空格
154
+ const cleanSvg = svg.replace(/\s+/g, ' ').trim();
155
+ // 使用 encodeURIComponent 处理 Unicode 字符
156
+ const encoded = encodeURIComponent(cleanSvg);
157
+ return 'data:image/svg+xml;base64,' + btoa(unescape(encoded));
158
+ }
159
+
160
+ // 将 SVG 转换为 base64
161
+ const bottleImageBase64 = svgToBase64(bottleImageSVG);
162
+
163
+ // 从SVG的viewBox中获取实际比例
164
+ const svgViewBox = bottleImageSVG.match(/viewBox="([^"]+)"/)[1].split(' ').map(Number);
165
+ const svgWidth = svgViewBox[2];
166
+ const svgHeight = svgViewBox[3];
167
+ const svgAspectRatio = svgHeight / svgWidth;
168
+
169
+ // 计算基础间距(在 makeChart 函数开始处添加)
170
+ const baseBottleWidth = xScale.bandwidth();
171
+ const baseBottleHeight = baseBottleWidth * svgAspectRatio;
172
+ const spacingAfterBottle = baseBottleWidth * 0.1; // 瓶子和图标之间的间距
173
+ const spacingBetweenIconLabel = baseBottleWidth * 0.15; // 图标和标签之间的间距
174
+
175
+ // 开始添加瓶子图像和蒙版
176
+ chart.selectAll(".bottle-group")
177
+ .data(sortedData)
178
+ .enter()
179
+ .append("g")
180
+ .attr("class", "bottle-group")
181
+ .attr("transform", d => `translate(${xScale(d[xField])}, ${bottleTopY})`)
182
+ .each(function(d, i) {
183
+ const group = d3.select(this);
184
+ const bottleWidth = xScale.bandwidth();
185
+ // 根据SVG的实际比例计算瓶子高度
186
+ const bottleHeight = bottleWidth * svgAspectRatio;
187
+ // 判断是否为最大值
188
+ const isMaxValue = +d[yField] === yMax;
189
+ const bottlePercentage = isMaxValue ? 1 : Math.max(0, (+d[yField] / yMax));
190
+
191
+ // 定义裁剪路径
192
+ const clipId = `clip-${i}`;
193
+ group.append("clipPath")
194
+ .attr("id", clipId)
195
+ .append("rect")
196
+ .attr("x", 0)
197
+ .attr("y", bottleHeight * (1 - bottlePercentage))
198
+ .attr("width", bottleWidth)
199
+ .attr("height", bottleHeight * bottlePercentage);
200
+
201
+ // 添加半透明的背景瓶子
202
+ group.append("image")
203
+ .attr("xlink:href", bottleImageBase64)
204
+ .attr("width", bottleWidth)
205
+ .attr("height", bottleHeight)
206
+ .attr("preserveAspectRatio", "xMidYMid meet")
207
+ .style("opacity", 0.15);
208
+
209
+ // 添加裁剪后的前景瓶子
210
+ group.append("image")
211
+ .attr("xlink:href", bottleImageBase64)
212
+ .attr("width", bottleWidth)
213
+ .attr("height", bottleHeight)
214
+ .attr("preserveAspectRatio", "xMidYMid meet")
215
+ .attr("clip-path", `url(#${clipId})`);
216
+
217
+ // 计算标签方块的大小和位置
218
+ const squareSize = bottleWidth * 0.6;
219
+ const squareX = (bottleWidth - squareSize) / 2;
220
+ const squareY = bottleHeight * (2/3) - (squareSize / 2);
221
+
222
+ // 添加正方形背景
223
+ group.append("rect")
224
+ .attr("class", "value-bg")
225
+ .attr("x", squareX)
226
+ .attr("y", squareY)
227
+ .attr("width", squareSize)
228
+ .attr("height", squareSize)
229
+ .attr("fill", "#f5f5f5")
230
+ .attr("opacity", 0.8)
231
+ .attr("rx", 5)
232
+ .attr("ry", 5);
233
+
234
+ // 格式化值
235
+
236
+ const formattedValue = `${chartUtils.format.autoText(+d[yField])}${valueUnit}`;
237
+ // 我们将在下面计算实际字体大小,先使用基础大小创建文本元素
238
+ const baseValueFontSize = 16;
239
+
240
+ // 添加数值文本但先不设置字体大小
241
+ const valueText = group.append("text")
242
+ .attr("class", "value-label")
243
+ .attr("x", bottleWidth / 2)
244
+ .attr("y", squareY + (squareSize * 0.6)) // 先设置一个位置,稍后会调整
245
+ .attr("text-anchor", "middle")
246
+ .style("font-family", typography.label.font_family)
247
+ .style("font-weight", "bold")
248
+ .style("fill", "#333")
249
+ .style("font-size", `${baseValueFontSize}px`) // 先设置基础字体大小
250
+ .text(formattedValue);
251
+
252
+ // 将值标签添加到数组中,以便后续统一调整
253
+ valueLabels.push({
254
+ element: valueText,
255
+ value: formattedValue,
256
+ squareSize: squareSize
257
+ });
258
+ });
259
+
260
+ // ---------- 8. 添加国旗和国家名称 (修改后) ----------
261
+ const baseLabelFontSize = 14; // 基础字体大小
262
+ // 文本换行辅助函数 (添加 alignment 参数)
263
+ function wrapText(text, str, width, lineHeight = 1.1, alignment = 'middle') {
264
+ const words = str.split(/\s+/).reverse(); // 按空格分割单词
265
+ let word;
266
+ let line = [];
267
+ let lineNumber = 0;
268
+ const initialY = parseFloat(text.attr("y")); // 获取原始y坐标
269
+ const initialX = parseFloat(text.attr("x")); // 获取原始x坐标
270
+ const actualFontSize = parseFloat(text.style("font-size")); // 获取实际应用的字体大小
271
+
272
+ text.text(null); // 清空现有文本
273
+
274
+ let tspans = []; // 存储最终要渲染的行
275
+
276
+ // 优先按单词换行
277
+ if (words.length > 1) {
278
+ let currentLine = [];
279
+ while (word = words.pop()) {
280
+ currentLine.push(word);
281
+ const isOverflow = chartUtils.text.measure(null, currentLine.join(" "), {
282
+ fontFamily: text.style("font-family"),
283
+ fontSize: parseFloat(text.style("font-size")),
284
+ fontWeight: text.style("font-weight")
285
+ }).width > width;
286
+
287
+ if (isOverflow && currentLine.length > 1) {
288
+ currentLine.pop(); // 回退一个词
289
+ tspans.push(currentLine.join(" ")); // 添加完成的行
290
+ currentLine = [word]; // 新行以当前词开始
291
+ lineNumber++;
292
+ }
293
+ }
294
+ // 添加最后一行
295
+ if (currentLine.length > 0) {
296
+ tspans.push(currentLine.join(" "));
297
+ }
298
+ } else { // 如果没有空格或只有一个词,则按字符换行
299
+ const chars = str.split('');
300
+ let currentLine = '';
301
+ for (let i = 0; i < chars.length; i++) {
302
+ const nextLine = currentLine + chars[i];
303
+ const isOverflow = chartUtils.text.measure(null, nextLine, {
304
+ fontFamily: text.style("font-family"),
305
+ fontSize: parseFloat(text.style("font-size")),
306
+ fontWeight: text.style("font-weight")
307
+ }).width > width;
308
+
309
+ if (isOverflow && currentLine.length > 0) { // 如果加了新字符就超长了,并且当前行不为空
310
+ tspans.push(currentLine); // 添加当前行
311
+ currentLine = chars[i]; // 新行从这个字符开始
312
+ lineNumber++;
313
+ } else {
314
+ currentLine = nextLine; // 没超长就继续加字符
315
+ }
316
+ }
317
+ // 添加最后一行
318
+ if (currentLine.length > 0) {
319
+ tspans.push(currentLine);
320
+ }
321
+ }
322
+
323
+ // 计算总行数
324
+ const totalLines = tspans.length;
325
+ let startDy = 0;
326
+
327
+ // 根据对齐方式计算起始偏移
328
+ if (alignment === 'middle') {
329
+ // 垂直居中:向上移动半行*(总行数-1)
330
+ startDy = -( (totalLines - 1) * lineHeight / 2);
331
+ } else if (alignment === 'bottom') {
332
+ // 底部对齐:计算总高度,向上移动 总高度 - 单行高度(近似)
333
+ // 注意:em单位是相对于字体大小的,这里用 lineHeight * actualFontSize 近似计算像素高度
334
+ const totalHeightEm = totalLines * lineHeight;
335
+ startDy = -(totalHeightEm - lineHeight); // 将底部对齐到原始y
336
+ }
337
+ // 如果是 'top' 对齐,startDy 保持为 0,即第一行基线在原始y位置
338
+ // 其他对齐方式(如 'top')可以保持 startDy 为 0
339
+
340
+ // 创建所有行的tspan元素
341
+ tspans.forEach((lineText, i) => {
342
+ text.append("tspan")
343
+ .attr("x", initialX) // x坐标与父<text>相同
344
+ .attr("dy", (i === 0 ? startDy : lineHeight) + "em") // 第一行应用起始偏移,后续行应用行高
345
+ .text(lineText);
346
+ });
347
+ }
348
+ // 计算所有维度标签在基础字体大小下的宽度
349
+ const dimensionWidths = sortedData.map(d => chartUtils.text.measure(svg, d[xField], {
350
+ fontFamily: typography.label.font_family,
351
+ fontSize: baseLabelFontSize
352
+ }).width);
353
+
354
+ const maxAvailableWidth = xScale.bandwidth() * 1.2;
355
+
356
+ // 计算是否需要缩小字体以及缩放比例
357
+ const maxDimensionWidth = Math.max(...dimensionWidths);
358
+ const labelScaleFactor = maxDimensionWidth > maxAvailableWidth ? maxAvailableWidth / maxDimensionWidth : 1;
359
+
360
+ // 确定最终的统一字体大小(增大1.5倍)
361
+ const finalLabelFontSize = baseLabelFontSize * labelScaleFactor * 1.5;
362
+
363
+ // 开始添加国旗和标签组
364
+ chart.selectAll(".flag-label-group")
365
+ .data(sortedData)
366
+ .enter()
367
+ .append("g")
368
+ .attr("class", "flag-label-group")
369
+ // 根据实际瓶子高度调整位置
370
+ .attr("transform", d => `translate(${xScale(d[xField]) + baseBottleWidth / 2}, ${bottleTopY + baseBottleHeight + spacingAfterBottle})`)
371
+ .each(function(d, i) {
372
+ const group = d3.select(this);
373
+
374
+ // 计算图标尺寸
375
+ const iconWidth = baseBottleWidth;
376
+ const iconHeight = iconWidth * 0.75;
377
+
378
+ // 添加国旗(如果数据中提供了图片字段和对应的值)
379
+ if (images.field && images.field[d[xField]]) {
380
+ group.append("image")
381
+ .attr("xlink:href", images.field[d[xField]])
382
+ .attr("x", -iconWidth / 2)
383
+ .attr("y", 0)
384
+ .attr("width", iconWidth)
385
+ .attr("height", iconHeight)
386
+ .attr("preserveAspectRatio", "xMidYMid meet");
387
+ }
388
+
389
+ // 添加国家名称文本
390
+ const label = group.append("text")
391
+ .attr("class", "country-label")
392
+ .attr("x", 0)
393
+ .attr("y", iconHeight + spacingBetweenIconLabel)
394
+ .attr("text-anchor", "middle")
395
+ .style("font-family", typography.label.font_family)
396
+ .style("font-size", `${finalLabelFontSize}px`)
397
+ .style("fill", colorResolver.text({ fallback: "#333333" }).value)
398
+ .text(d[xField]);
399
+
400
+ // 如果需要换行,重新应用换行
401
+ const labelMaxWidth = baseBottleWidth * 1.05;
402
+ if (chartUtils.text.measure(chart, d[xField], {
403
+ fontFamily: typography.label.font_family,
404
+ fontSize: finalLabelFontSize
405
+ }).width > labelMaxWidth) {
406
+ wrapText(label, d[xField].toString(), labelMaxWidth, 1.1, 'top');
407
+ }
408
+ });
409
+
410
+ // ---------- 9. 统一调整所有值标签的字体大小 ----------
411
+ // 创建临时文本元素来测量值标签文本宽度
412
+
413
+ const baseValueFontSize = 16;
414
+ // 计算所有值标签在基础字体大小下的实际宽度
415
+ const valueTextWidths = valueLabels.map(item => {
416
+ return chartUtils.text.measure(svg, item.value, {
417
+ fontFamily: typography.label.font_family,
418
+ fontSize: `${baseValueFontSize}px`,
419
+ fontWeight: "bold"
420
+ }).width;
421
+ });
422
+
423
+ // 计算每个标签所需的缩放比例(方形宽度 / 文本宽度)
424
+ const valueScaleFactors = valueLabels.map((item, i) => {
425
+ // 使用60%的方形宽度作为最大可用宽度
426
+ const maxWidth = item.squareSize * 1; // 留一些边距
427
+ return maxWidth / valueTextWidths[i];
428
+ });
429
+
430
+ // 取最小的缩放比例,确保所有值标签使用相同的字体大小
431
+ const minValueScaleFactor = Math.min(...valueScaleFactors, 1); // 不要放大,只缩小
432
+
433
+ // 计算最终的统一字体大小
434
+ const finalValueFontSize = Math.min(baseValueFontSize * minValueScaleFactor, baseValueFontSize);
435
+
436
+ // 应用统一的字体大小到所有值标签
437
+ valueLabels.forEach(item => {
438
+ item.element
439
+ .style("font-size", `${finalValueFontSize}px`);
440
+ });
441
+
442
+ // 返回SVG节点
443
+ return svg.node();
444
+ }