Ray1ee01 commited on
Commit
3f342b2
·
verified ·
1 Parent(s): 0ac599f

Upload folder using huggingface_hub

Browse files
modules/chart_engine/template/d3-js/pyramid_funnel/funnel_chart_01.js ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Funnel Chart",
5
+ "chart_name": "funnel_chart_01",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 10], [0, 100]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 400,
16
+ "background": "light",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "no",
20
+ "has_y_axis": "no"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+ function makeChart(containerSelector, data) {
26
+ // 提取数据
27
+ const jsonData = data;
28
+ const chartData = jsonData.data.data;
29
+ const variables = jsonData.variables;
30
+ const typography = jsonData.typography;
31
+ const colors = jsonData.colors || {};
32
+ const colorResolver = chartUtils.color.resolver(jsonData);
33
+ const dataColumns = chartUtils.schema.columns(jsonData);
34
+ const images = jsonData.images || {};
35
+
36
+ // 清空容器
37
+ d3.select(containerSelector).html("");
38
+
39
+ // 获取字段名
40
+ const categoryField = chartUtils.schema.columnField(dataColumns, 0);
41
+ const valueField = chartUtils.schema.columnField(dataColumns, 1);
42
+
43
+ // 按值从大到小排序数据(大的在顶部)
44
+ const sortedData = [...chartData].sort((a, b) => +b[valueField] - +a[valueField]);
45
+
46
+ // 计算总和以获取百分比
47
+ const total = d3.sum(sortedData, d => +d[valueField]);
48
+
49
+ // 为每个数据点添加百分比
50
+ sortedData.forEach(d => {
51
+ d.percent = (+d[valueField] / total) * 100;
52
+ });
53
+ let max_percent = 0;
54
+ let min_percent = 100;
55
+ sortedData.forEach(d => {
56
+ max_percent = Math.max(max_percent, d.percent);
57
+ min_percent = Math.min(min_percent, d.percent);
58
+ });
59
+
60
+ // 设置尺寸和边距
61
+ const width = variables.width;
62
+ const height = variables.height;
63
+ const margin = { top: 40, right: 120, bottom: 40, left: 60 };
64
+
65
+ // 创建SVG
66
+ const svg = d3.select(containerSelector)
67
+ .append("svg")
68
+ .attr("width", "100%")
69
+ .attr("height", height)
70
+ .attr("viewBox", `0 0 ${width} ${height}`)
71
+ .attr("style", "max-width: 100%; height: auto;")
72
+ .attr("xmlns", "http://www.w3.org/2000/svg")
73
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
74
+
75
+ // 创建图表区域
76
+ const chartWidth = width - margin.left - margin.right;
77
+ const chartHeight = height - margin.top - margin.bottom;
78
+
79
+ const g = svg.append("g")
80
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
81
+
82
+ // 计算漏斗图的最大宽度和高度
83
+ const maxFunnelWidth = chartWidth * 0.8;
84
+ const funnelHeight = chartHeight * 0.8;
85
+
86
+ // 计算每个部分的高度(均等分配)
87
+ const sectionHeight = funnelHeight / sortedData.length;
88
+
89
+ // 创建宽度比例尺
90
+ const widthScale = d3.scaleLinear()
91
+ .domain([0, max_percent])
92
+ .range([0, maxFunnelWidth]);
93
+
94
+ // 计算每个部分的宽度
95
+ const sectionWidths = sortedData.map(d => widthScale(d.percent));
96
+
97
+ // 计算垂直居中的偏移量
98
+ const verticalOffset = (chartHeight - funnelHeight) / 2;
99
+
100
+ // 绘制漏斗图的每一层
101
+ sortedData.forEach((d, i) => {
102
+ // 获取颜色
103
+ const color = colorResolver.field(d[categoryField], i, { palette: "category10" }).value;
104
+
105
+ // 计算当前层的宽度
106
+ const topWidth = sectionWidths[i];
107
+
108
+ // 计算下一层的宽度(如果是最后一层,则使用当前宽度的80%)
109
+ const bottomWidth = i < sortedData.length - 1
110
+ ? sectionWidths[i + 1]
111
+ : topWidth * 0.8;
112
+
113
+ // 计算当前层的位置
114
+ const sectionY = i * sectionHeight + verticalOffset;
115
+
116
+ // 绘制梯形
117
+ const points = [
118
+ [chartWidth / 2 - topWidth / 2, sectionY],
119
+ [chartWidth / 2 + topWidth / 2, sectionY],
120
+ [chartWidth / 2 + bottomWidth / 2, sectionY + sectionHeight],
121
+ [chartWidth / 2 - bottomWidth / 2, sectionY + sectionHeight]
122
+ ];
123
+
124
+ g.append("polygon")
125
+ .attr("points", points.map(p => p.join(",")).join(" "))
126
+ .attr("fill", color);
127
+
128
+ // 添加标签
129
+ g.append("text")
130
+ .attr("x", chartWidth / 2 + topWidth / 2 + 10)
131
+ .attr("y", sectionY + sectionHeight / 2)
132
+ .attr("dominant-baseline", "middle")
133
+ .attr("font-size", "14px")
134
+ .attr("font-weight", "bold")
135
+ .text(`${d[categoryField]} ${chartUtils.format.percent(d.percent, { digits: 0 }).text}`);
136
+ });
137
+
138
+ return svg.node();
139
+ }
modules/chart_engine/template/d3-js/pyramid_funnel/funnel_chart_02.js ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Funnel Chart",
5
+ "chart_name": "funnel_chart_02",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 10], [0, 100]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 400,
16
+ "background": "light",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "no",
20
+ "has_y_axis": "no"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+ function makeChart(containerSelector, data) {
26
+ // 提取数据
27
+ const jsonData = data;
28
+ const chartData = jsonData.data.data;
29
+ const variables = jsonData.variables;
30
+ const typography = jsonData.typography;
31
+ const colors = jsonData.colors || {};
32
+ const colorResolver = chartUtils.color.resolver(jsonData);
33
+ const dataColumns = chartUtils.schema.columns(jsonData);
34
+ const images = jsonData.images || {};
35
+
36
+ // 清空容器
37
+ d3.select(containerSelector).html("");
38
+
39
+ // 获取字段名
40
+ const categoryField = chartUtils.schema.columnField(dataColumns, 0);
41
+ const valueField = chartUtils.schema.columnField(dataColumns, 1);
42
+
43
+ // 按值从大到小排序数据(大的在顶部)
44
+ const sortedData = [...chartData].sort((a, b) => +b[valueField] - +a[valueField]);
45
+
46
+ // 计算总和以获取百分比
47
+ const total = d3.sum(sortedData, d => +d[valueField]);
48
+
49
+ // 为每个数据点添加百分比
50
+ sortedData.forEach(d => {
51
+ d.percent = (+d[valueField] / total) * 100;
52
+ });
53
+
54
+ // 设置尺寸和边距
55
+ const width = variables.width;
56
+ const height = variables.height;
57
+ const margin = { top: 40, right: 120, bottom: 40, left: 60 };
58
+
59
+ // 创建SVG
60
+ const svg = d3.select(containerSelector)
61
+ .append("svg")
62
+ .attr("width", "100%")
63
+ .attr("height", height)
64
+ .attr("viewBox", `0 0 ${width} ${height}`)
65
+ .attr("style", "max-width: 100%; height: auto;")
66
+ .attr("xmlns", "http://www.w3.org/2000/svg")
67
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
68
+
69
+ // 创建图表区域
70
+ const chartWidth = width - margin.left - margin.right;
71
+ const chartHeight = height - margin.top - margin.bottom;
72
+
73
+ const g = svg.append("g")
74
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
75
+
76
+ // 计算漏斗图的最大宽度和高度
77
+ const maxFunnelWidth = chartWidth * 0.8;
78
+ const funnelHeight = chartHeight * 0.8;
79
+
80
+ // 计算每个部分的高度(均等分配)
81
+ const sectionHeight = funnelHeight / sortedData.length;
82
+
83
+ // 创建宽度比例尺
84
+ const widthScale = d3.scaleLinear()
85
+ .domain([0, 100])
86
+ .range([0, maxFunnelWidth]);
87
+
88
+ // 计算每个部分的宽度
89
+ const sectionWidths = sortedData.map(d => widthScale(d.percent));
90
+
91
+ // 计算垂直居中的偏移量
92
+ const verticalOffset = (chartHeight - funnelHeight) / 2;
93
+
94
+ const y_padding = 5;
95
+
96
+ // 绘制漏斗图的每一层
97
+ sortedData.forEach((d, i) => {
98
+ // 获取颜色
99
+ const color = colorResolver.field(d[categoryField], i, { palette: "category10" }).value;
100
+
101
+ // 计算当前层的宽度
102
+ const topWidth = sectionWidths[i];
103
+
104
+ // 计算下一层的宽度(如果是最后一层,则使用当前宽度的80%)
105
+ const bottomWidth = i < sortedData.length - 1
106
+ ? sectionWidths[i + 1]
107
+ : topWidth * 0.8;
108
+
109
+ // 计算当前层的位置
110
+ const sectionY = i * sectionHeight + verticalOffset;
111
+
112
+ // 绘制梯形
113
+ const points = [
114
+ [chartWidth / 2 - topWidth / 2, sectionY + y_padding * i],
115
+ [chartWidth / 2 + topWidth / 2, sectionY + y_padding * i],
116
+ [chartWidth / 2 + bottomWidth / 2, sectionY + sectionHeight + y_padding * i],
117
+ [chartWidth / 2 - bottomWidth / 2, sectionY + sectionHeight + y_padding * i]
118
+ ];
119
+
120
+ g.append("polygon")
121
+ .attr("points", points.map(p => p.join(",")).join(" "))
122
+ .attr("fill", color);
123
+
124
+ // 添加标签
125
+ g.append("text")
126
+ .attr("x", chartWidth / 2 + topWidth / 2 + 10)
127
+ .attr("y", sectionY + sectionHeight / 2 + y_padding * i)
128
+ .attr("dominant-baseline", "middle")
129
+ .attr("font-size", "14px")
130
+ .attr("font-weight", "bold")
131
+ .text(`${d[categoryField]} ${chartUtils.format.percent(d.percent, { digits: 0 }).text}`);
132
+ });
133
+
134
+ return svg.node();
135
+ }
modules/chart_engine/template/d3-js/pyramid_funnel/pyramid_chart_01.js ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Pyramid Chart",
5
+ "chart_name": "pyramid_chart_01",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 10], [0, 100]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 400,
16
+ "background": "light",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "no",
20
+ "has_y_axis": "no"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+ function makeChart(containerSelector, data) {
26
+ // 提取数据
27
+ const jsonData = data;
28
+ const chartData = jsonData.data.data;
29
+ const variables = jsonData.variables;
30
+ const typography = jsonData.typography;
31
+ const colors = jsonData.colors || {};
32
+ const colorResolver = chartUtils.color.resolver(jsonData);
33
+ const dataColumns = chartUtils.schema.columns(jsonData);
34
+ const images = jsonData.images || {};
35
+
36
+ // 清空容器
37
+ d3.select(containerSelector).html("");
38
+
39
+ // 获取字段名
40
+ const categoryField = chartUtils.schema.columnField(dataColumns, 0);
41
+ const valueField = chartUtils.schema.columnField(dataColumns, 1);
42
+
43
+ // 按值从小到大排序数据(小的在顶部)
44
+ const sortedData = [...chartData].sort((a, b) => +a[valueField] - +b[valueField]);
45
+
46
+ // 计算总和以获取百分比
47
+ const total = d3.sum(sortedData, d => +d[valueField]);
48
+
49
+ // 为每个数据点添加百分比和累积百分比
50
+ let cumulativePercent = 0;
51
+ sortedData.forEach(d => {
52
+ d.percent = (+d[valueField] / total) * 100;
53
+ d.cumulativePercentStart = cumulativePercent;
54
+ cumulativePercent += d.percent;
55
+ d.cumulativePercentEnd = cumulativePercent;
56
+ });
57
+
58
+ // 设置尺寸和边距
59
+ const width = variables.width;
60
+ const height = variables.height;
61
+ const margin = { top: 40, right: 120, bottom: 40, left: 60 };
62
+
63
+ // 创建SVG
64
+ const svg = d3.select(containerSelector)
65
+ .append("svg")
66
+ .attr("width", "100%")
67
+ .attr("height", height)
68
+ .attr("viewBox", `0 0 ${width} ${height}`)
69
+ .attr("style", "max-width: 100%; height: auto;")
70
+ .attr("xmlns", "http://www.w3.org/2000/svg")
71
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
72
+
73
+ // 创建图表区域
74
+ const chartWidth = width - margin.left - margin.right;
75
+ const chartHeight = height - margin.top - margin.bottom;
76
+
77
+ const g = svg.append("g")
78
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
79
+
80
+ // 计算金字塔的最大宽度(底部)和高度
81
+ const maxPyramidWidth = chartWidth * 0.6;
82
+ const pyramidHeight = chartHeight * 0.6; // 使用90%的高度,留出上下空间
83
+
84
+ // 计算面积比例
85
+ // 金字塔总面积
86
+ const totalArea = maxPyramidWidth * pyramidHeight / 2;
87
+
88
+ // 计算每个部分的高度(基于面积比例)
89
+ let currentHeight = 0;
90
+ const sections = [];
91
+
92
+ sortedData.forEach((d, i) => {
93
+ // 该部分应占的面积比例
94
+ const areaRatio = d.percent / 100;
95
+ const sectionArea = totalArea * areaRatio;
96
+
97
+ // 计算该部分的高度
98
+ // 对于梯形,面积 = (上底+下底) * 高 / 2
99
+ // 我们需要求解高度,已知面积和下底(上一部分的上底)
100
+
101
+ // 首先计算该部分在整个三角形中的相对位置
102
+ const bottomPosition = currentHeight / pyramidHeight;
103
+ // 正三角形:底部宽,顶部窄
104
+ const bottomWidth = maxPyramidWidth * bottomPosition;
105
+
106
+ // 求解该部分的高度
107
+ // 设高度为h,则上底 = maxPyramidWidth * (currentHeight + h) / pyramidHeight
108
+ // 面积方程:sectionArea = (bottomWidth + topWidth) * h / 2
109
+
110
+ // 简化后的二次方程:
111
+ // h^2 * (maxPyramidWidth / (2 * pyramidHeight)) + h * bottomWidth - 2 * sectionArea = 0
112
+
113
+ const a = maxPyramidWidth / (2 * pyramidHeight);
114
+ const b = bottomWidth;
115
+ const c = -2 * sectionArea;
116
+
117
+ // 使用求根公式
118
+ const h = (-b + Math.sqrt(b*b - 4*a*c)) / (2*a);
119
+
120
+ // 计算该部分的上底宽度
121
+ const topPosition = (currentHeight + h) / pyramidHeight;
122
+ const topWidth = maxPyramidWidth * topPosition;
123
+
124
+ sections.push({
125
+ data: d,
126
+ bottomY: currentHeight,
127
+ topY: currentHeight + h,
128
+ bottomWidth: bottomWidth,
129
+ topWidth: topWidth
130
+ });
131
+
132
+ currentHeight += h;
133
+ });
134
+
135
+ // 计算垂直居中的偏移量
136
+ const verticalOffset = (chartHeight - pyramidHeight) / 2;
137
+
138
+ // 绘制金字塔的每一层
139
+ sections.forEach((section, i) => {
140
+ const d = section.data;
141
+
142
+ // 获取颜色
143
+ const color = colorResolver.field(d[categoryField], i, { palette: "category10" }).value;
144
+
145
+ // 绘制梯形 - 添加垂直偏移
146
+ const points = [
147
+ [chartWidth / 2 - section.topWidth / 2, section.topY + verticalOffset],
148
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset],
149
+ [chartWidth / 2 + section.bottomWidth / 2, section.bottomY + verticalOffset],
150
+ [chartWidth / 2 - section.bottomWidth / 2, section.bottomY + verticalOffset]
151
+ ];
152
+
153
+ g.append("polygon")
154
+ .attr("points", points.map(p => p.join(",")).join(" "))
155
+ .attr("fill", color);
156
+
157
+ // 计算标签位置,避免重叠 - 添加垂直偏移
158
+ const labelY = (section.topY + section.bottomY) / 2 + verticalOffset;
159
+ const labelX = chartWidth / 2 + Math.max(section.topWidth, section.bottomWidth) / 2 + 10;
160
+
161
+ // 添加标签
162
+ g.append("text")
163
+ .attr("x", labelX)
164
+ .attr("y", labelY)
165
+ .attr("dominant-baseline", "middle")
166
+ .attr("font-size", "14px")
167
+ .attr("font-weight", "bold")
168
+ .text(`${d[categoryField]} ${chartUtils.format.percent(d.percent, { digits: 0 }).text}`);
169
+ });
170
+
171
+ return svg.node();
172
+ }
modules/chart_engine/template/d3-js/pyramid_funnel/pyramid_chart_01_dark.js ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Pyramid Chart",
5
+ "chart_name": "pyramid_chart_01_dark",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 10], [0, 100]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 400,
16
+ "background": "dark",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "no",
20
+ "has_y_axis": "no"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+ function makeChart(containerSelector, data) {
26
+ const colorResolver = chartUtils.color.resolver(data);
27
+ // 提取数据
28
+ const jsonData = data;
29
+ const chartData = jsonData.data.data;
30
+ const variables = jsonData.variables;
31
+ const typography = jsonData.typography;
32
+ const colors = jsonData.colors_dark || {
33
+ text_color: "#000000",
34
+ other: { primary: "#4682B4", secondary: "#FF7F50" }
35
+ };
36
+ const dataColumns = chartUtils.schema.columns(jsonData);
37
+ const images = jsonData.images || {};
38
+
39
+ // 清空容器
40
+ d3.select(containerSelector).html("");
41
+
42
+ // 获取字段名
43
+ const categoryField = chartUtils.schema.columnField(dataColumns, 0);
44
+ const valueField = chartUtils.schema.columnField(dataColumns, 1);
45
+
46
+ // 按值从小到大排序数据(小的在顶部)
47
+ const sortedData = [...chartData].sort((a, b) => +a[valueField] - +b[valueField]);
48
+
49
+ // 计算总和以获取百分比
50
+ const total = d3.sum(sortedData, d => +d[valueField]);
51
+
52
+ // 为每个数据点添加百分比和累积百分比
53
+ let cumulativePercent = 0;
54
+ sortedData.forEach(d => {
55
+ d.percent = (+d[valueField] / total) * 100;
56
+ d.cumulativePercentStart = cumulativePercent;
57
+ cumulativePercent += d.percent;
58
+ d.cumulativePercentEnd = cumulativePercent;
59
+ });
60
+
61
+ // 设置尺寸和边距
62
+ const width = variables.width;
63
+ const height = variables.height;
64
+ const margin = { top: 40, right: 120, bottom: 40, left: 60 };
65
+
66
+ // 创建SVG
67
+ const svg = d3.select(containerSelector)
68
+ .append("svg")
69
+ .attr("width", "100%")
70
+ .attr("height", height)
71
+ .attr("viewBox", `0 0 ${width} ${height}`)
72
+ .attr("style", "max-width: 100%; height: auto;")
73
+ .attr("xmlns", "http://www.w3.org/2000/svg")
74
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
75
+
76
+ // 创建图表区域
77
+ const chartWidth = width - margin.left - margin.right;
78
+ const chartHeight = height - margin.top - margin.bottom;
79
+
80
+ const g = svg.append("g")
81
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
82
+
83
+ // 计算金字塔的最大宽度(底部)和高度
84
+ const maxPyramidWidth = chartWidth * 0.6;
85
+ const pyramidHeight = chartHeight * 0.6; // 使用90%的高度,留出上下空间
86
+
87
+ // 计算面积比例
88
+ // 金字塔总面积
89
+ const totalArea = maxPyramidWidth * pyramidHeight / 2;
90
+
91
+ // 计算每个部分的高度(基于面积比例)
92
+ let currentHeight = 0;
93
+ const sections = [];
94
+
95
+ sortedData.forEach((d, i) => {
96
+ // 该部分应占的面积比例
97
+ const areaRatio = d.percent / 100;
98
+ const sectionArea = totalArea * areaRatio;
99
+
100
+ // 计算该部分的高度
101
+ // 对于梯形,面积 = (上底+下底) * 高 / 2
102
+ // 我们需要求解高度,已知面积和下底(上一部分的上底)
103
+
104
+ // 首先计算该部分在整个三角形中的相对位置
105
+ const bottomPosition = currentHeight / pyramidHeight;
106
+ // 正三角形:底部宽,顶部窄
107
+ const bottomWidth = maxPyramidWidth * bottomPosition;
108
+
109
+ // 求解该部分的高度
110
+ // 设高度为h,则上底 = maxPyramidWidth * (currentHeight + h) / pyramidHeight
111
+ // 面积方程:sectionArea = (bottomWidth + topWidth) * h / 2
112
+
113
+ // 简化后的二次方程:
114
+ // h^2 * (maxPyramidWidth / (2 * pyramidHeight)) + h * bottomWidth - 2 * sectionArea = 0
115
+
116
+ const a = maxPyramidWidth / (2 * pyramidHeight);
117
+ const b = bottomWidth;
118
+ const c = -2 * sectionArea;
119
+
120
+ // 使用求根公式
121
+ const h = (-b + Math.sqrt(b*b - 4*a*c)) / (2*a);
122
+
123
+ // 计算该部分的上底宽度
124
+ const topPosition = (currentHeight + h) / pyramidHeight;
125
+ const topWidth = maxPyramidWidth * topPosition;
126
+
127
+ sections.push({
128
+ data: d,
129
+ bottomY: currentHeight,
130
+ topY: currentHeight + h,
131
+ bottomWidth: bottomWidth,
132
+ topWidth: topWidth
133
+ });
134
+
135
+ currentHeight += h;
136
+ });
137
+
138
+ // 计算垂直居中的偏移量
139
+ const verticalOffset = (chartHeight - pyramidHeight) / 2;
140
+
141
+ // 绘制金字塔的每一层
142
+ sections.forEach((section, i) => {
143
+ const d = section.data;
144
+
145
+ // 获取颜色
146
+ const color = colorResolver.field(d[categoryField], i, { palette: "category10" }).value;
147
+
148
+ // 绘制梯形 - 添加垂直偏移
149
+ const points = [
150
+ [chartWidth / 2 - section.topWidth / 2, section.topY + verticalOffset],
151
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset],
152
+ [chartWidth / 2 + section.bottomWidth / 2, section.bottomY + verticalOffset],
153
+ [chartWidth / 2 - section.bottomWidth / 2, section.bottomY + verticalOffset]
154
+ ];
155
+
156
+ g.append("polygon")
157
+ .attr("points", points.map(p => p.join(",")).join(" "))
158
+ .attr("fill", color);
159
+
160
+ // 计算标签位置,避免重叠 - 添加垂直偏移
161
+ const labelY = (section.topY + section.bottomY) / 2 + verticalOffset;
162
+ const labelX = chartWidth / 2 + Math.max(section.topWidth, section.bottomWidth) / 2 + 10;
163
+
164
+ // 添加标签
165
+ g.append("text")
166
+ .attr("x", labelX)
167
+ .attr("y", labelY)
168
+ .attr("dominant-baseline", "middle")
169
+ .attr("font-size", "14px")
170
+ .attr("font-weight", "bold")
171
+ .attr("fill", "#FFFFFF")
172
+ .text(`${d[categoryField]} ${chartUtils.format.percent(d.percent, { digits: 0 }).text}`);
173
+ });
174
+
175
+ return svg.node();
176
+ }
modules/chart_engine/template/d3-js/pyramid_funnel/pyramid_chart_02.js ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Pyramid Chart",
5
+ "chart_name": "pyramid_chart_02",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 10], [0, 100]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 400,
16
+ "background": "light",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "no",
20
+ "has_y_axis": "no"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+ function makeChart(containerSelector, data) {
26
+ // 提取数据
27
+ const jsonData = data;
28
+ const chartData = jsonData.data.data;
29
+ const variables = jsonData.variables;
30
+ const typography = jsonData.typography;
31
+ const colors = jsonData.colors || {};
32
+ const colorResolver = chartUtils.color.resolver(jsonData);
33
+ const dataColumns = chartUtils.schema.columns(jsonData);
34
+ const images = jsonData.images || {};
35
+
36
+ // 清空容器
37
+ d3.select(containerSelector).html("");
38
+
39
+ // 获取字段名
40
+ const categoryField = chartUtils.schema.columnField(dataColumns, 0);
41
+ const valueField = chartUtils.schema.columnField(dataColumns, 1);
42
+
43
+ // 按值从小到大排序数据(小的在顶部)
44
+ const sortedData = [...chartData].sort((a, b) => +a[valueField] - +b[valueField]);
45
+
46
+ // 计算总和以获取百分比
47
+ const total = d3.sum(sortedData, d => +d[valueField]);
48
+
49
+ // 为每个数据点添加百分比和累积百分比
50
+ let cumulativePercent = 0;
51
+ sortedData.forEach(d => {
52
+ d.percent = (+d[valueField] / total) * 100;
53
+ d.cumulativePercentStart = cumulativePercent;
54
+ cumulativePercent += d.percent;
55
+ d.cumulativePercentEnd = cumulativePercent;
56
+ });
57
+
58
+ // 设置尺寸和边距
59
+ const width = variables.width;
60
+ const height = variables.height;
61
+ const margin = { top: 40, right: 120, bottom: 40, left: 60 };
62
+
63
+ // 创建SVG
64
+ const svg = d3.select(containerSelector)
65
+ .append("svg")
66
+ .attr("width", "100%")
67
+ .attr("height", height)
68
+ .attr("viewBox", `0 0 ${width} ${height}`)
69
+ .attr("style", "max-width: 100%; height: auto;")
70
+ .attr("xmlns", "http://www.w3.org/2000/svg")
71
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
72
+
73
+ // 创建图表区域
74
+ const chartWidth = width - margin.left - margin.right;
75
+ const chartHeight = height - margin.top - margin.bottom;
76
+
77
+ const g = svg.append("g")
78
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
79
+
80
+ // 计算金字塔的最大宽度(底部)和高度
81
+ const maxPyramidWidth = chartWidth * 0.6;
82
+ const pyramidHeight = chartHeight * 0.6; // 使用90%的高度,留出上下空间
83
+
84
+ // 计算面积比例
85
+ // 金字塔总面积
86
+ const totalArea = maxPyramidWidth * pyramidHeight / 2;
87
+
88
+ // 计算每个部分的高度(基于面积比例)
89
+ let currentHeight = 0;
90
+ const sections = [];
91
+
92
+ sortedData.forEach((d, i) => {
93
+ // 该部分应占的面积比例
94
+ const areaRatio = d.percent / 100;
95
+ const sectionArea = totalArea * areaRatio;
96
+
97
+ // 计算该部分的高度
98
+ // 对于梯形,面积 = (上底+下底) * 高 / 2
99
+ // 我们需要求解高度,已知面积和下底(上一部分的上底)
100
+
101
+ // 首先计算该部分在整个三角形中的相对位置
102
+ const bottomPosition = currentHeight / pyramidHeight;
103
+ // 正三角形:底部宽,顶部窄
104
+ const bottomWidth = maxPyramidWidth * bottomPosition;
105
+
106
+ // 求解该部分的高度
107
+ // 设高度为h,则上底 = maxPyramidWidth * (currentHeight + h) / pyramidHeight
108
+ // 面积方程:sectionArea = (bottomWidth + topWidth) * h / 2
109
+
110
+ // 简化后的二次方程:
111
+ // h^2 * (maxPyramidWidth / (2 * pyramidHeight)) + h * bottomWidth - 2 * sectionArea = 0
112
+
113
+ const a = maxPyramidWidth / (2 * pyramidHeight);
114
+ const b = bottomWidth;
115
+ const c = -2 * sectionArea;
116
+
117
+ // 使用求根公式
118
+ const h = (-b + Math.sqrt(b*b - 4*a*c)) / (2*a);
119
+
120
+ // 计算该部分的上底宽度
121
+ const topPosition = (currentHeight + h) / pyramidHeight;
122
+ const topWidth = maxPyramidWidth * topPosition;
123
+
124
+ sections.push({
125
+ data: d,
126
+ bottomY: currentHeight,
127
+ topY: currentHeight + h,
128
+ bottomWidth: bottomWidth,
129
+ topWidth: topWidth
130
+ });
131
+
132
+ currentHeight += h;
133
+ });
134
+
135
+ // 计算垂直居中的偏移量
136
+ const verticalOffset = (chartHeight - pyramidHeight) / 2;
137
+
138
+ const y_padding = 5;
139
+
140
+ // 绘制金字塔的每一层
141
+ sections.forEach((section, i) => {
142
+ const d = section.data;
143
+
144
+ // 获取颜色
145
+ const color = colorResolver.field(d[categoryField], i, { palette: "category10" }).value;
146
+
147
+ // 绘制梯形 - 添加垂直偏移
148
+ const points = [
149
+ [chartWidth / 2 - section.topWidth / 2, section.topY + verticalOffset + y_padding * i],
150
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset + y_padding * i],
151
+ [chartWidth / 2 + section.bottomWidth / 2, section.bottomY + verticalOffset + y_padding * i],
152
+ [chartWidth / 2 - section.bottomWidth / 2, section.bottomY + verticalOffset + y_padding * i]
153
+ ];
154
+
155
+ g.append("polygon")
156
+ .attr("points", points.map(p => p.join(",")).join(" "))
157
+ .attr("fill", color);
158
+
159
+ // 计算标签位置,避免重叠 - 添加垂直偏移
160
+ const labelY = (section.topY + section.bottomY) / 2 + verticalOffset + y_padding * i;
161
+ const labelX = chartWidth / 2 + Math.max(section.topWidth, section.bottomWidth) / 2 + 10;
162
+
163
+ // 添加标签
164
+ g.append("text")
165
+ .attr("x", labelX)
166
+ .attr("y", labelY)
167
+ .attr("dominant-baseline", "middle")
168
+ .attr("font-size", "14px")
169
+ .attr("font-weight", "bold")
170
+ .text(`${d[categoryField]} ${chartUtils.format.percent(d.percent, { digits: 0 }).text}`);
171
+ });
172
+
173
+ return svg.node();
174
+ }
modules/chart_engine/template/d3-js/pyramid_funnel/pyramid_chart_03.js ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Pyramid Chart",
5
+ "chart_name": "pyramid_chart_03",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 10], [0, 100]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 400,
16
+ "background": "light",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "no",
20
+ "has_y_axis": "no"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+ function makeChart(containerSelector, data) {
26
+ // 提取数据
27
+ const jsonData = data;
28
+ const chartData = jsonData.data.data;
29
+ const variables = jsonData.variables;
30
+ const typography = jsonData.typography;
31
+ const colors = jsonData.colors || {};
32
+ const colorResolver = chartUtils.color.resolver(jsonData);
33
+ const dataColumns = chartUtils.schema.columns(jsonData);
34
+ const images = jsonData.images || {};
35
+
36
+ // 清空容器
37
+ d3.select(containerSelector).html("");
38
+
39
+ // 获取字段名
40
+ const categoryField = chartUtils.schema.columnField(dataColumns, 0);
41
+ const valueField = chartUtils.schema.columnField(dataColumns, 1);
42
+
43
+ // 按值从小到大排序数据(小的在顶部)
44
+ const sortedData = [...chartData].sort((a, b) => +a[valueField] - +b[valueField]);
45
+
46
+ // 计算总和以获取百分比
47
+ const total = d3.sum(sortedData, d => +d[valueField]);
48
+
49
+ // 为每个数据点添加百分比和累积百分比
50
+ let cumulativePercent = 0;
51
+ sortedData.forEach(d => {
52
+ d.percent = (+d[valueField] / total) * 100;
53
+ d.cumulativePercentStart = cumulativePercent;
54
+ cumulativePercent += d.percent;
55
+ d.cumulativePercentEnd = cumulativePercent;
56
+ });
57
+
58
+ // 设置尺寸和边距
59
+ const width = variables.width;
60
+ const height = variables.height;
61
+ const margin = { top: 40, right: 120, bottom: 40, left: 60 };
62
+
63
+ // 创建SVG
64
+ const svg = d3.select(containerSelector)
65
+ .append("svg")
66
+ .attr("width", "100%")
67
+ .attr("height", height)
68
+ .attr("viewBox", `0 0 ${width} ${height}`)
69
+ .attr("style", "max-width: 100%; height: auto;")
70
+ .attr("xmlns", "http://www.w3.org/2000/svg")
71
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
72
+
73
+ // 创建图表区域
74
+ const chartWidth = width - margin.left - margin.right;
75
+ const initialChartHeightForLegend = height - margin.top - margin.bottom;
76
+
77
+ // ---------- 图例逻辑 (借鉴 Voronoi Treemap / function_modules.js) ----------
78
+ const uniqueCategories = [...new Set(sortedData.map(d => d[categoryField]))];
79
+
80
+ const canvas = document.createElement('canvas');
81
+ const ctx = canvas.getContext('2d');
82
+ function getTextWidthHelper(text, fontFamily, fontSize, fontWeight) {
83
+ ctx.font = `${fontWeight || 'normal'} ${fontSize}px ${fontFamily || 'Arial'}`;
84
+ return chartUtils.text.contextWidth(ctx, text);
85
+ }
86
+
87
+ let legendBlockHeight = 0;
88
+ const legendLines = [];
89
+ const paddingBelowLegendToChart = 20; // 图例和金字塔之间的间距增加
90
+ const minSvgGlobalTopPadding = 15;
91
+ let legendItemMaxHeight = 0;
92
+ let interLineVerticalPadding = parseFloat(typography.label?.line_spacing || '6');
93
+ let legendInterItemSpacing = 10;
94
+
95
+ // 获取颜色函数,与后续金字塔层颜色逻辑保持一致
96
+ const getColorForLegend = (category, index) => {
97
+ return colorResolver.field(category, index, { palette: "category10" }).value;
98
+ };
99
+
100
+ if (uniqueCategories && uniqueCategories.length > 0) {
101
+ const legendColorRectWidth = 12;
102
+ const legendColorRectHeight = 12;
103
+ const legendIconWidth = typography.label?.icon_size || 16;
104
+ const legendIconHeight = typography.label?.icon_size || 16;
105
+ const legendPaddingRectIcon = 4;
106
+ const legendPaddingIconText = 4;
107
+
108
+ const legendFontFamily = typography.label?.font_family || 'Arial';
109
+ const legendFontSize = parseFloat(typography.label?.font_size || '12');
110
+ const legendFontWeight = typography.label?.font_weight || 'normal';
111
+
112
+ legendItemMaxHeight = Math.max(legendColorRectHeight, legendIconHeight, legendFontSize);
113
+
114
+ const legendItemsData = uniqueCategories.map((catName, index) => {
115
+ const text = String(catName);
116
+ const color = getColorForLegend(catName, sortedData.findIndex(d => d[categoryField] === catName)); // 确保颜色与金字塔层一致
117
+ const iconUrl = images.field && images.field[catName] ? images.field[catName] : null;
118
+ const textWidth = getTextWidthHelper(text, legendFontFamily, legendFontSize, legendFontWeight);
119
+
120
+ let itemVisualWidth = legendColorRectWidth;
121
+ if (iconUrl) {
122
+ itemVisualWidth += legendPaddingRectIcon + legendIconWidth + legendPaddingIconText;
123
+ } else {
124
+ itemVisualWidth += legendPaddingRectIcon; // 如果没有图标,颜色块和文本之间仍有间距
125
+ }
126
+ itemVisualWidth += textWidth;
127
+ return { text, color, iconUrl, textWidth, visualWidth: itemVisualWidth };
128
+ });
129
+
130
+ const legendLayout = chartUtils.legend.wrapItems(
131
+ legendItemsData.map(item => ({ ...item, width: item.visualWidth, height: legendItemMaxHeight })),
132
+ {
133
+ maxWidth: chartWidth,
134
+ itemGap: legendInterItemSpacing,
135
+ rowGap: interLineVerticalPadding,
136
+ itemHeight: legendItemMaxHeight,
137
+ }
138
+ );
139
+ legendLines.push(...legendLayout.rows.map(row => ({
140
+ items: row.items,
141
+ totalVisualWidth: row.width,
142
+ })));
143
+ legendBlockHeight = legendLayout.height;
144
+ }
145
+
146
+ let effectiveMarginTop = margin.top;
147
+ let legendStartY = minSvgGlobalTopPadding;
148
+
149
+ if (legendBlockHeight > 0) {
150
+ legendStartY = Math.max(minSvgGlobalTopPadding, margin.top); // 确保图例从至少 margin.top 或 minPadding 开始
151
+ effectiveMarginTop = legendStartY + legendBlockHeight + paddingBelowLegendToChart;
152
+ } else {
153
+ effectiveMarginTop = Math.max(margin.top, minSvgGlobalTopPadding);
154
+ }
155
+
156
+ // 实际用于金字塔绘制的 chartHeight
157
+ const chartHeight = height - effectiveMarginTop - margin.bottom;
158
+
159
+ // 创建图表区域 (g),使用新的 effectiveMarginTop
160
+ const g = svg.append("g")
161
+ .attr("transform", `translate(${margin.left}, ${effectiveMarginTop})`);
162
+
163
+ // ---------- 绘制图例 (如果存在) ----------
164
+ if (legendBlockHeight > 0 && legendLines.length > 0) {
165
+ const legendContainerGroup = svg.append("g") // 注意:图例容器直接附加到svg,其Y坐标由legendStartY决定
166
+ .attr("class", "custom-legend-container")
167
+ .attr("transform", `translate(0, ${legendStartY})`);
168
+
169
+ let currentLineBaseY = 0;
170
+ const currentLegendInterItemSpacing = legendInterItemSpacing;
171
+
172
+ legendLines.forEach((line) => {
173
+ const lineRenderStartX = margin.left + (chartWidth - line.totalVisualWidth) / 2; // 水平居中于chartWidth区域
174
+ const lineCenterY = currentLineBaseY + legendItemMaxHeight / 2;
175
+ let currentItemDrawX = lineRenderStartX;
176
+
177
+ const legendColorRectWidth = 12;
178
+ const legendColorRectHeight = 12;
179
+ const legendIconWidth = typography.label?.icon_size || 16;
180
+ const legendIconHeight = typography.label?.icon_size || 16;
181
+ const legendPaddingRectIcon = 4;
182
+ const legendPaddingIconText = 4;
183
+ const legendFontFamily = typography.label?.font_family || 'Arial';
184
+ const legendFontSize = parseFloat(typography.label?.font_size || '12');
185
+ const legendFontWeight = typography.label?.font_weight || 'normal';
186
+
187
+ line.items.forEach((item, itemIndex) => {
188
+ legendContainerGroup.append("rect")
189
+ .attr("x", currentItemDrawX)
190
+ .attr("y", currentLineBaseY + (legendItemMaxHeight - legendColorRectHeight) / 2)
191
+ .attr("width", legendColorRectWidth).attr("height", legendColorRectHeight)
192
+ .attr("rx", 3).attr("ry", 3) // 圆角矩形
193
+ .attr("fill", item.color).attr("fill-opacity", 0.85);
194
+ currentItemDrawX += legendColorRectWidth;
195
+ if (item.iconUrl) {
196
+ currentItemDrawX += legendPaddingRectIcon;
197
+ legendContainerGroup.append("image").attr("xlink:href", item.iconUrl)
198
+ .attr("x", currentItemDrawX)
199
+ .attr("y", currentLineBaseY + (legendItemMaxHeight - legendIconHeight) / 2)
200
+ .attr("width", legendIconWidth).attr("height", legendIconHeight)
201
+ .attr("preserveAspectRatio", "xMidYMid meet");
202
+ currentItemDrawX += legendIconWidth + legendPaddingIconText;
203
+ } else {
204
+ currentItemDrawX += legendPaddingRectIcon;
205
+ }
206
+ legendContainerGroup.append("text").attr("x", currentItemDrawX).attr("y", lineCenterY)
207
+ .attr("dominant-baseline", "middle")
208
+ .style("font-family", legendFontFamily).style("font-size", `${legendFontSize}px`)
209
+ .style("font-weight", legendFontWeight).style("fill", colorResolver.text({ fallback: "#333333" }).value || typography.label?.font_color || "#333333")
210
+ .text(item.text);
211
+ currentItemDrawX += item.textWidth;
212
+ if (itemIndex < line.items.length - 1) {
213
+ currentItemDrawX += (line.items[itemIndex+1].visualWidth > 0 ? (currentLegendInterItemSpacing || 10) : 0) ;
214
+ }
215
+ });
216
+ currentLineBaseY += legendItemMaxHeight + interLineVerticalPadding;
217
+ });
218
+ }
219
+
220
+ // 计算金字塔的最大宽度(底部)和高度,使用调整后的 chartHeight
221
+ const maxPyramidWidth = chartWidth * 0.6;
222
+ const pyramidHeight = chartHeight * 0.8; // 使用 chartHeight 的80%,给上下留些空间
223
+
224
+ // 计算面积比例
225
+ // 金字塔总面积
226
+ const totalArea = maxPyramidWidth * pyramidHeight / 2;
227
+
228
+ // 计算每个部分的高度(基于面积比例)
229
+ let currentHeight = 0;
230
+ const sections = [];
231
+
232
+ sortedData.forEach((d, i) => {
233
+ // 该部分应占的面积比例
234
+ const areaRatio = d.percent / 100;
235
+ const sectionArea = totalArea * areaRatio;
236
+
237
+ // 计算该部分的高度
238
+ // 对于梯形,面积 = (上底+下底) * 高 / 2
239
+ // 我们需要求解高度,已知面积和下底(上一部分的上底)
240
+
241
+ // 首先计算该部分在整个三角形中的相对位置
242
+ const bottomPosition = currentHeight / pyramidHeight;
243
+ // 正三角形:底部宽,顶部窄
244
+ const bottomWidth = maxPyramidWidth * bottomPosition;
245
+
246
+ // 求解该部分的高度
247
+ // 设高度为h,则上底 = maxPyramidWidth * (currentHeight + h) / pyramidHeight
248
+ // 面积方程:sectionArea = (bottomWidth + topWidth) * h / 2
249
+
250
+ // 简化后的二次方程:
251
+ // h^2 * (maxPyramidWidth / (2 * pyramidHeight)) + h * bottomWidth - 2 * sectionArea = 0
252
+
253
+ const a = maxPyramidWidth / (2 * pyramidHeight);
254
+ const b = bottomWidth;
255
+ const c = -2 * sectionArea;
256
+
257
+ // 使用求根公式
258
+ const h = (-b + Math.sqrt(b*b - 4*a*c)) / (2*a);
259
+
260
+ // 计算该部分的上底宽度
261
+ const topPosition = (currentHeight + h) / pyramidHeight;
262
+ const topWidth = maxPyramidWidth * topPosition;
263
+
264
+ sections.push({
265
+ data: d,
266
+ bottomY: currentHeight,
267
+ topY: currentHeight + h,
268
+ bottomWidth: bottomWidth,
269
+ topWidth: topWidth
270
+ });
271
+
272
+ currentHeight += h;
273
+ });
274
+
275
+ // 计算垂直居中的偏移量
276
+ const verticalOffset = (chartHeight - pyramidHeight) / 2;
277
+
278
+ // 绘制金字塔的每一层
279
+ sections.forEach((section, i) => {
280
+ const d = section.data;
281
+
282
+ // 获取颜色 (与图例逻辑中的 getColorForLegend 对应)
283
+ const color = getColorForLegend(d[categoryField], i); // 使用 i (排序后的索引) 作为后备
284
+
285
+ // 绘制梯形 - 添加垂直偏移
286
+ const points = [
287
+ [chartWidth / 2 - section.topWidth / 2, section.topY + verticalOffset],
288
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset],
289
+ [chartWidth / 2 + section.bottomWidth / 2, section.bottomY + verticalOffset],
290
+ [chartWidth / 2 - section.bottomWidth / 2, section.bottomY + verticalOffset]
291
+ ];
292
+
293
+ g.append("polygon")
294
+ .attr("points", points.map(p => p.join(",")).join(" "))
295
+ .attr("fill", color);
296
+
297
+ // 计算标签位置,避免重叠 - 添加垂直偏移
298
+ const labelY = (section.topY + section.bottomY) / 2 + verticalOffset;
299
+ const labelX = chartWidth / 2 + Math.max(section.topWidth, section.bottomWidth) / 2 + 10;
300
+
301
+ // 修改标签为只显示百分比
302
+ g.append("text")
303
+ .attr("x", labelX)
304
+ .attr("y", labelY)
305
+ .attr("dominant-baseline", "middle")
306
+ .attr("font-size", typography.label?.font_size || "12px") // 使用label字号
307
+ .attr("font-weight", typography.label?.font_weight || "normal")
308
+ .attr("fill", colorResolver.text({ fallback: "#333333" }).value || typography.label?.font_color || "#333333")
309
+ .text(chartUtils.format.percent(d.percent, { digits: 0 }).text);
310
+ });
311
+
312
+ return svg.node();
313
+ }
modules/chart_engine/template/d3-js/pyramid_funnel/pyramid_diagram_01.js ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Pyramid Diagram",
5
+ "chart_name": "pyramid_diagram_01",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 10], [0, 100]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 400,
16
+ "background": "light",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "no",
20
+ "has_y_axis": "no"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+ function makeChart(containerSelector, data) {
26
+ // 提取数据
27
+ const jsonData = data;
28
+ const chartData = jsonData.data.data;
29
+ const variables = jsonData.variables;
30
+ const typography = jsonData.typography;
31
+ const colors = jsonData.colors || {};
32
+ const colorResolver = chartUtils.color.resolver(jsonData);
33
+ const dataColumns = chartUtils.schema.columns(jsonData);
34
+ const images = jsonData.images || {};
35
+
36
+ // 清空容器
37
+ d3.select(containerSelector).html("");
38
+
39
+ // 获取字段名
40
+ const categoryField = chartUtils.schema.columnField(dataColumns, 0);
41
+ const valueField = chartUtils.schema.columnField(dataColumns, 1);
42
+
43
+ // 按值从小到大排序数据(小的在顶部)
44
+ const sortedData = [...chartData].sort((a, b) => +a[valueField] - +b[valueField]);
45
+
46
+ // 计算总和以获取百分比
47
+ const total = d3.sum(sortedData, d => +d[valueField]);
48
+
49
+ // 为每个数据点添加百分比和累积百分比
50
+ let cumulativePercent = 0;
51
+ sortedData.forEach(d => {
52
+ d.percent = (+d[valueField] / total) * 100;
53
+ d.cumulativePercentStart = cumulativePercent;
54
+ cumulativePercent += d.percent;
55
+ d.cumulativePercentEnd = cumulativePercent;
56
+ });
57
+
58
+ // 设置尺寸和边距
59
+ const width = variables.width;
60
+ const height = variables.height;
61
+ const margin = { top: 40, right: 120, bottom: 40, left: 60 };
62
+
63
+ // 创建SVG
64
+ const svg = d3.select(containerSelector)
65
+ .append("svg")
66
+ .attr("width", "100%")
67
+ .attr("height", height)
68
+ .attr("viewBox", `0 0 ${width} ${height}`)
69
+ .attr("style", "max-width: 100%; height: auto;")
70
+ .attr("xmlns", "http://www.w3.org/2000/svg")
71
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
72
+
73
+ // 创建图表区域
74
+ const chartWidth = width - margin.left - margin.right;
75
+ const chartHeight = height - margin.top - margin.bottom;
76
+
77
+ const g = svg.append("g")
78
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
79
+
80
+ // 计算金字塔的最大宽度(底部)和高度
81
+ const maxPyramidWidth = chartWidth * 0.6;
82
+ const pyramidHeight = chartHeight * 0.6; // 使用90%的高度,留出上下空间
83
+
84
+ // 计算面积比例
85
+ // 金字塔总面积
86
+ const totalArea = maxPyramidWidth * pyramidHeight / 2;
87
+
88
+ // 计算每个部分的高度(基于面积比例)
89
+ let currentHeight = 0;
90
+ const sections = [];
91
+
92
+ sortedData.forEach((d, i) => {
93
+ // 该部分应占的面积比例
94
+ const areaRatio = d.percent / 100;
95
+ const sectionArea = totalArea * areaRatio;
96
+
97
+ // 计算该部分的高度
98
+ // 对于梯形,面积 = (上底+下底) * 高 / 2
99
+ // 我们需要求解高度,已知面积和下底(上一部分的上底)
100
+
101
+ // 首先计算该部分在整个三角形中的相对位置
102
+ const bottomPosition = currentHeight / pyramidHeight;
103
+ // 正三角形:底部宽,顶部窄
104
+ const bottomWidth = maxPyramidWidth * bottomPosition;
105
+
106
+ // 求解该部分的高度
107
+ // 设高度为h,则上底 = maxPyramidWidth * (currentHeight + h) / pyramidHeight
108
+ // 面积方程:sectionArea = (bottomWidth + topWidth) * h / 2
109
+
110
+ // 简化后的二次方程:
111
+ // h^2 * (maxPyramidWidth / (2 * pyramidHeight)) + h * bottomWidth - 2 * sectionArea = 0
112
+
113
+ const a = maxPyramidWidth / (2 * pyramidHeight);
114
+ const b = bottomWidth;
115
+ const c = -2 * sectionArea;
116
+
117
+ // 使用求根公式
118
+ const h = (-b + Math.sqrt(b*b - 4*a*c)) / (2*a);
119
+
120
+ // 计算该部分的上底宽度
121
+ const topPosition = (currentHeight + h) / pyramidHeight;
122
+ const topWidth = maxPyramidWidth * topPosition;
123
+
124
+ sections.push({
125
+ data: d,
126
+ bottomY: currentHeight,
127
+ topY: currentHeight + h,
128
+ bottomWidth: bottomWidth,
129
+ topWidth: topWidth
130
+ });
131
+
132
+ currentHeight += h;
133
+ });
134
+
135
+ // 计算垂直居中的偏移量
136
+ const verticalOffset = (chartHeight - pyramidHeight) / 2;
137
+
138
+ // 绘制金字塔的每一层
139
+ sections.forEach((section, i) => {
140
+ const d = section.data;
141
+
142
+ // ��取颜色
143
+ const color = colorResolver.field(d[categoryField], i, { palette: "category10" }).value;
144
+
145
+ // 绘制梯形 - 添加垂直偏移
146
+ const points = [
147
+ [chartWidth / 2 - section.topWidth / 2, section.topY + verticalOffset],
148
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset],
149
+ [chartWidth / 2 + section.bottomWidth / 2, section.bottomY + verticalOffset],
150
+ [chartWidth / 2 - section.bottomWidth / 2, section.bottomY + verticalOffset]
151
+ ];
152
+
153
+ g.append("polygon")
154
+ .attr("points", points.map(p => p.join(",")).join(" "))
155
+ .attr("fill", color);
156
+
157
+ // 计算标签位置,避免重叠 - 添加垂直偏移
158
+ const labelY = (section.topY + section.bottomY) / 2 + verticalOffset;
159
+ const labelX = chartWidth / 2;
160
+
161
+ const textWidth = chartUtils.text.measure(null, d[categoryField], { fontSize: 14 }).width + 20;
162
+ if (textWidth > (section.topWidth + section.bottomWidth) / 2) {
163
+ // 添加一个暗色透明背景
164
+ g.append("rect")
165
+ .attr("x", labelX - textWidth / 2)
166
+ .attr("y", labelY - 15)
167
+ .attr("width", textWidth)
168
+ .attr("height", 30)
169
+ .attr("fill", "rgba(0, 0, 0, 0.3)")
170
+ .attr("rx", 5)
171
+ .attr("ry", 5);
172
+ }
173
+
174
+ // 添加标签
175
+ g.append("text")
176
+ .attr("x", labelX)
177
+ .attr("y", labelY)
178
+ .attr("text-anchor", "middle")
179
+ .attr("dominant-baseline", "middle")
180
+ .attr("font-size", "14px")
181
+ .attr("font-weight", "bold")
182
+ .attr("fill", "white")
183
+ .text(chartUtils.format.category(d[categoryField]).text);
184
+ });
185
+
186
+ return svg.node();
187
+ }
modules/chart_engine/template/d3-js/pyramid_funnel/pyramid_diagram_01_3d.js ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Pyramid Diagram",
5
+ "chart_name": "pyramid_diagram_01_3d",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 10], [0, 100]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 400,
16
+ "background": "light",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "no",
20
+ "has_y_axis": "no"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+ function makeChart(containerSelector, data) {
26
+ // 提取数据
27
+ const jsonData = data;
28
+ const chartData = jsonData.data.data;
29
+ const variables = jsonData.variables;
30
+ const typography = jsonData.typography;
31
+ const colors = jsonData.colors || {};
32
+ const colorResolver = chartUtils.color.resolver(jsonData);
33
+ const dataColumns = chartUtils.schema.columns(jsonData);
34
+ const images = jsonData.images || {};
35
+
36
+ // 清空容器
37
+ d3.select(containerSelector).html("");
38
+
39
+ // 获取字段名
40
+ const categoryField = chartUtils.schema.columnField(dataColumns, 0);
41
+ const valueField = chartUtils.schema.columnField(dataColumns, 1);
42
+
43
+ // 按值从大到小排序数据(大的在底部)
44
+ const sortedData = [...chartData].sort((a, b) => +b[valueField] - +a[valueField]);
45
+
46
+ // 计算总和以获取百分比
47
+ const total = d3.sum(sortedData, d => +d[valueField]);
48
+
49
+ // 为每个数据点添加百分比和累积百分比
50
+ let cumulativePercent = 0;
51
+ sortedData.forEach(d => {
52
+ d.percent = (+d[valueField] / total) * 100;
53
+ d.cumulativePercentStart = cumulativePercent;
54
+ cumulativePercent += d.percent;
55
+ d.cumulativePercentEnd = cumulativePercent;
56
+ });
57
+
58
+ // 设置尺寸和边距
59
+ const width = variables.width;
60
+ const height = variables.height;
61
+ const margin = { top: 40, right: 120, bottom: 40, left: 160 }; // 增加左侧边距
62
+
63
+ // 创建SVG
64
+ const svg = d3.select(containerSelector)
65
+ .append("svg")
66
+ .attr("width", "100%")
67
+ .attr("height", height)
68
+ .attr("viewBox", `0 0 ${width} ${height}`)
69
+ .attr("style", "max-width: 100%; height: auto;")
70
+ .attr("xmlns", "http://www.w3.org/2000/svg")
71
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
72
+
73
+ // 添加渐变和阴影定义
74
+ const defs = svg.append("defs");
75
+
76
+ // 添加阴影滤镜
77
+ const filter = defs.append("filter")
78
+ .attr("id", "shadow")
79
+ .attr("x", "-50%")
80
+ .attr("y", "-50%")
81
+ .attr("width", "200%")
82
+ .attr("height", "200%");
83
+
84
+ filter.append("feOffset")
85
+ .attr("result", "offOut")
86
+ .attr("in", "SourceGraphic")
87
+ .attr("dx", 5)
88
+ .attr("dy", 5);
89
+
90
+ filter.append("feGaussianBlur")
91
+ .attr("result", "blurOut")
92
+ .attr("in", "offOut")
93
+ .attr("stdDeviation", 3);
94
+
95
+ filter.append("feBlend")
96
+ .attr("in", "SourceGraphic")
97
+ .attr("in2", "blurOut")
98
+ .attr("mode", "normal");
99
+
100
+ // 创建图表区域
101
+ const chartWidth = width - margin.left - margin.right;
102
+ const chartHeight = height - margin.top - margin.bottom;
103
+
104
+ const g = svg.append("g")
105
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
106
+
107
+ // 计算金字塔的最大宽度(底部)和高度
108
+ const maxPyramidWidth = chartWidth * 0.6;
109
+ const pyramidHeight = chartHeight * 0.8; // 使用80%的高度,留出上下空间
110
+
111
+ // 计算面积比例
112
+ // 金字塔总面积
113
+ const totalArea = maxPyramidWidth * pyramidHeight / 2;
114
+
115
+ // 计算每个部分的高度(基于面积比例)
116
+ let currentHeight = 0;
117
+ const sections = [];
118
+
119
+ // 计算所有部分的总高度,使高度与数据成正比
120
+ const totalValue = d3.sum(sortedData, d => +d[valueField]);
121
+ const totalHeight = pyramidHeight;
122
+
123
+ sortedData.forEach((d, i) => {
124
+ // 根据数值在总和中的比例计算高度
125
+ const heightRatio = +d[valueField] / totalValue;
126
+ const sectionHeight = heightRatio * totalHeight;
127
+
128
+ // 计算该部分在金字塔中的位置
129
+ const bottomY = currentHeight;
130
+ const topY = currentHeight + sectionHeight;
131
+
132
+ // 根据当前高度位置计算宽度(线性变化)
133
+ const bottomPosition = bottomY / totalHeight;
134
+ const topPosition = topY / totalHeight;
135
+
136
+ // 底部宽度大,顶部宽度小
137
+ const bottomWidth = maxPyramidWidth * (1 - bottomPosition);
138
+ const topWidth = maxPyramidWidth * (1 - topPosition);
139
+
140
+ sections.push({
141
+ data: d,
142
+ bottomY: bottomY,
143
+ topY: topY,
144
+ bottomWidth: bottomWidth,
145
+ topWidth: topWidth
146
+ });
147
+
148
+ currentHeight += sectionHeight;
149
+ });
150
+
151
+ // 计算垂直居中的偏移量
152
+ const verticalOffset = (chartHeight - pyramidHeight) / 2;
153
+
154
+ // 绘制金字塔的左侧标签
155
+ sections.forEach((section, i) => {
156
+ const d = section.data;
157
+ const labelY = (section.topY + section.bottomY) / 2 + verticalOffset;
158
+
159
+ // 添加左侧的类别标签(x标签)
160
+ g.append("text")
161
+ .attr("x", -20) // 位于图形左侧
162
+ .attr("y", labelY)
163
+ .attr("text-anchor", "end") // 右对齐
164
+ .attr("dominant-baseline", "middle")
165
+ .attr("font-size", "14px")
166
+ .attr("font-weight", "bold")
167
+ .attr("fill", "#333")
168
+ .text(`${d[categoryField]}`);
169
+
170
+ // 添加连接线
171
+ g.append("line")
172
+ .attr("x1", -15)
173
+ .attr("y1", labelY)
174
+ .attr("x2", chartWidth / 2 - section.bottomWidth / 2 - 5)
175
+ .attr("y2", labelY)
176
+ .attr("stroke", "#999")
177
+ .attr("stroke-width", 1)
178
+ .attr("stroke-dasharray", "3,3");
179
+ });
180
+
181
+ // 绘制金字塔的每一层
182
+ sections.forEach((section, i) => {
183
+ const d = section.data;
184
+
185
+ // 获取颜色
186
+ const baseColor = colorResolver.field(d[categoryField], i, { palette: "category10" }).value;
187
+
188
+ // 为3D效果创建较深的颜色
189
+ const darkerColor = colorResolver.variant(baseColor, { mode: "darker", amount: 0.8 });
190
+
191
+ // 为每个截面创建渐变
192
+ const gradientId = `gradient-${i}`;
193
+ const gradient = defs.append("linearGradient")
194
+ .attr("id", gradientId)
195
+ .attr("x1", "0%")
196
+ .attr("y1", "0%")
197
+ .attr("x2", "100%")
198
+ .attr("y2", "0%");
199
+
200
+ gradient.append("stop")
201
+ .attr("offset", "0%")
202
+ .attr("stop-color", darkerColor.toString());
203
+
204
+ gradient.append("stop")
205
+ .attr("offset", "100%")
206
+ .attr("stop-color", baseColor);
207
+
208
+ // 绘制主梯形 - 添加垂直偏移
209
+ const points = [
210
+ [chartWidth / 2 - section.topWidth / 2, section.topY + verticalOffset],
211
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset],
212
+ [chartWidth / 2 + section.bottomWidth / 2, section.bottomY + verticalOffset],
213
+ [chartWidth / 2 - section.bottomWidth / 2, section.bottomY + verticalOffset]
214
+ ];
215
+
216
+ // 主体部分
217
+ g.append("polygon")
218
+ .attr("points", points.map(p => p.join(",")).join(" "))
219
+ .attr("fill", `url(#${gradientId})`)
220
+ .attr("filter", "url(#shadow)");
221
+
222
+ // 判断是否为底部三角形(最后一个元素,索引为0)
223
+ const isBottom = (i === 0);
224
+
225
+ // 右侧面(3D效果)- 对底部三角形特殊处理
226
+ let sidePoints;
227
+
228
+ if (isBottom) {
229
+ // 底部三角形的侧面不应超出底部
230
+ sidePoints = [
231
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset],
232
+ [chartWidth / 2 + section.topWidth / 2 + 15, section.topY + verticalOffset + 10],
233
+ [chartWidth / 2 + section.bottomWidth / 2, section.bottomY + verticalOffset]
234
+ ];
235
+ } else {
236
+ // 其他层的侧面正常绘制
237
+ sidePoints = [
238
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset],
239
+ [chartWidth / 2 + section.topWidth / 2 + 15, section.topY + verticalOffset + 10],
240
+ [chartWidth / 2 + section.bottomWidth / 2 + 15, section.bottomY + verticalOffset + 10],
241
+ [chartWidth / 2 + section.bottomWidth / 2, section.bottomY + verticalOffset]
242
+ ];
243
+ }
244
+
245
+ g.append("polygon")
246
+ .attr("points", sidePoints.map(p => p.join(",")).join(" "))
247
+ .attr("fill", darkerColor.toString());
248
+
249
+ // 仅为最顶部的部分添加顶面
250
+ if (i === sections.length - 1) {
251
+ // 顶部面(仅最顶层部分)
252
+ const topPoints = [
253
+ [chartWidth / 2 - section.topWidth / 2, section.topY + verticalOffset],
254
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset],
255
+ [chartWidth / 2 + section.topWidth / 2 + 15, section.topY + verticalOffset + 10],
256
+ [chartWidth / 2 - section.topWidth / 2 + 15, section.topY + verticalOffset + 10]
257
+ ];
258
+
259
+ g.append("polygon")
260
+ .attr("points", topPoints.map(p => p.join(",")).join(" "))
261
+ .attr("fill", chartUtils.color.variant(darkerColor, { mode: "brighter", amount: 0.5 }));
262
+ }
263
+ });
264
+
265
+ // 在梯形内部添加数据标签
266
+ sections.forEach((section, i) => {
267
+ const d = section.data;
268
+ const centerY = (section.topY + section.bottomY) / 2 + verticalOffset;
269
+ const centerX = chartWidth / 2;
270
+
271
+ // 计算该部分占比的百分比文本
272
+ const percentText = `${chartUtils.format.autoText(+d[valueField])} (${chartUtils.format.percent(d.percent).text})`;
273
+
274
+ // 添加数值和百分比标签
275
+ g.append("text")
276
+ .attr("x", centerX)
277
+ .attr("y", centerY)
278
+ .attr("text-anchor", "middle")
279
+ .attr("dominant-baseline", "middle")
280
+ .attr("font-size", "14px")
281
+ .attr("font-weight", "bold")
282
+ .attr("fill", "white")
283
+ .text(percentText);
284
+ });
285
+
286
+ // 辅助函数:估算文本宽度
287
+ function measureTextWidth(text, fontSize) {
288
+ return chartUtils.text.estimate(text, { fontSize: fontSize, factor: 0.6 }).width;
289
+ }
290
+
291
+ return svg.node();
292
+ }
modules/chart_engine/template/d3-js/pyramid_funnel/pyramid_diagram_01_3d_dark.js ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Pyramid Diagram",
5
+ "chart_name": "pyramid_diagram_01_3d_dark",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 10], [0, 100]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 400,
16
+ "background": "dark",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "no",
20
+ "has_y_axis": "no"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+ function makeChart(containerSelector, data) {
26
+ const colorResolver = chartUtils.color.resolver(data);
27
+ // 提取数据
28
+ const jsonData = data;
29
+ const chartData = jsonData.data.data;
30
+ const variables = jsonData.variables;
31
+ const typography = jsonData.typography;
32
+ const colors = jsonData.colors_dark || {
33
+ text_color: "#000000",
34
+ other: { primary: "#4682B4", secondary: "#FF7F50" }
35
+ };
36
+ const dataColumns = chartUtils.schema.columns(jsonData);
37
+ const images = jsonData.images || {};
38
+
39
+ // 清空容器
40
+ d3.select(containerSelector).html("");
41
+
42
+ // 获取字段名
43
+ const categoryField = chartUtils.schema.columnField(dataColumns, 0);
44
+ const valueField = chartUtils.schema.columnField(dataColumns, 1);
45
+
46
+ // 按值从大到小排序数据(大的在底部)
47
+ const sortedData = [...chartData].sort((a, b) => +b[valueField] - +a[valueField]);
48
+
49
+ // 计算总和以获取百分比
50
+ const total = d3.sum(sortedData, d => +d[valueField]);
51
+
52
+ // 为每个数据点添加百分比和累积百分比
53
+ let cumulativePercent = 0;
54
+ sortedData.forEach(d => {
55
+ d.percent = (+d[valueField] / total) * 100;
56
+ d.cumulativePercentStart = cumulativePercent;
57
+ cumulativePercent += d.percent;
58
+ d.cumulativePercentEnd = cumulativePercent;
59
+ });
60
+
61
+ // 设置尺寸和边距
62
+ const width = variables.width;
63
+ const height = variables.height;
64
+ const margin = { top: 40, right: 120, bottom: 40, left: 160 }; // 增加左侧边距
65
+
66
+ // 创建SVG
67
+ const svg = d3.select(containerSelector)
68
+ .append("svg")
69
+ .attr("width", "100%")
70
+ .attr("height", height)
71
+ .attr("viewBox", `0 0 ${width} ${height}`)
72
+ .attr("style", "max-width: 100%; height: auto;")
73
+ .attr("xmlns", "http://www.w3.org/2000/svg")
74
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
75
+
76
+ // 添加渐变和阴影定义
77
+ const defs = svg.append("defs");
78
+
79
+ // 添加阴影滤镜
80
+ const filter = defs.append("filter")
81
+ .attr("id", "shadow")
82
+ .attr("x", "-50%")
83
+ .attr("y", "-50%")
84
+ .attr("width", "200%")
85
+ .attr("height", "200%");
86
+
87
+ filter.append("feOffset")
88
+ .attr("result", "offOut")
89
+ .attr("in", "SourceGraphic")
90
+ .attr("dx", 5)
91
+ .attr("dy", 5);
92
+
93
+ filter.append("feGaussianBlur")
94
+ .attr("result", "blurOut")
95
+ .attr("in", "offOut")
96
+ .attr("stdDeviation", 3);
97
+
98
+ filter.append("feBlend")
99
+ .attr("in", "SourceGraphic")
100
+ .attr("in2", "blurOut")
101
+ .attr("mode", "normal");
102
+
103
+ // 创建图表区域
104
+ const chartWidth = width - margin.left - margin.right;
105
+ const chartHeight = height - margin.top - margin.bottom;
106
+
107
+ const g = svg.append("g")
108
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
109
+
110
+ // 计算金字塔的最大宽度(底部)和高度
111
+ const maxPyramidWidth = chartWidth * 0.6;
112
+ const pyramidHeight = chartHeight * 0.8; // 使用80%的高度,留出上下空间
113
+
114
+ // 计算面积比例
115
+ // 金字塔总面积
116
+ const totalArea = maxPyramidWidth * pyramidHeight / 2;
117
+
118
+ // 计算每个部分的高度(基于面积比例)
119
+ let currentHeight = 0;
120
+ const sections = [];
121
+
122
+ // 计算所有部分的总高度,使高度与数据成正比
123
+ const totalValue = d3.sum(sortedData, d => +d[valueField]);
124
+ const totalHeight = pyramidHeight;
125
+
126
+ sortedData.forEach((d, i) => {
127
+ // 根据数值在总和中的比例计算高度
128
+ const heightRatio = +d[valueField] / totalValue;
129
+ const sectionHeight = heightRatio * totalHeight;
130
+
131
+ // 计算该部分在金字塔中的位置
132
+ const bottomY = currentHeight;
133
+ const topY = currentHeight + sectionHeight;
134
+
135
+ // 根据当前高度位置计算宽度(线性变化)
136
+ const bottomPosition = bottomY / totalHeight;
137
+ const topPosition = topY / totalHeight;
138
+
139
+ // 底部宽度大,顶部宽度小
140
+ const bottomWidth = maxPyramidWidth * (1 - bottomPosition);
141
+ const topWidth = maxPyramidWidth * (1 - topPosition);
142
+
143
+ sections.push({
144
+ data: d,
145
+ bottomY: bottomY,
146
+ topY: topY,
147
+ bottomWidth: bottomWidth,
148
+ topWidth: topWidth
149
+ });
150
+
151
+ currentHeight += sectionHeight;
152
+ });
153
+
154
+ // 计算垂直居中的偏移量
155
+ const verticalOffset = (chartHeight - pyramidHeight) / 2;
156
+
157
+ // 绘制金字塔的左侧标签
158
+ sections.forEach((section, i) => {
159
+ const d = section.data;
160
+ const labelY = (section.topY + section.bottomY) / 2 + verticalOffset;
161
+
162
+ // 添加左侧的类别标签(x标签)
163
+ g.append("text")
164
+ .attr("x", -20) // 位于图形左侧
165
+ .attr("y", labelY)
166
+ .attr("text-anchor", "end") // 右对齐
167
+ .attr("dominant-baseline", "middle")
168
+ .attr("font-size", "14px")
169
+ .attr("font-weight", "bold")
170
+ .attr("fill", "#ffffff")
171
+ .text(`${d[categoryField]}`);
172
+
173
+ // 添加连接线
174
+ g.append("line")
175
+ .attr("x1", -15)
176
+ .attr("y1", labelY)
177
+ .attr("x2", chartWidth / 2 - section.bottomWidth / 2 - 5)
178
+ .attr("y2", labelY)
179
+ .attr("stroke", "#999")
180
+ .attr("stroke-width", 1)
181
+ .attr("stroke-dasharray", "3,3");
182
+ });
183
+
184
+ // 绘制金字塔的每一层
185
+ sections.forEach((section, i) => {
186
+ const d = section.data;
187
+
188
+ // 获取颜色
189
+ const baseColor = colorResolver.field(d[categoryField], i, { palette: "category10" }).value;
190
+
191
+ // 为3D效果创建较深的颜色
192
+ const darkerColor = colorResolver.variant(baseColor, { mode: "darker", amount: 0.8 });
193
+
194
+ // 为每个截面创建渐变
195
+ const gradientId = `gradient-${i}`;
196
+ const gradient = defs.append("linearGradient")
197
+ .attr("id", gradientId)
198
+ .attr("x1", "0%")
199
+ .attr("y1", "0%")
200
+ .attr("x2", "100%")
201
+ .attr("y2", "0%");
202
+
203
+ gradient.append("stop")
204
+ .attr("offset", "0%")
205
+ .attr("stop-color", darkerColor.toString());
206
+
207
+ gradient.append("stop")
208
+ .attr("offset", "100%")
209
+ .attr("stop-color", baseColor);
210
+
211
+ // 绘制主梯形 - 添加垂直偏移
212
+ const points = [
213
+ [chartWidth / 2 - section.topWidth / 2, section.topY + verticalOffset],
214
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset],
215
+ [chartWidth / 2 + section.bottomWidth / 2, section.bottomY + verticalOffset],
216
+ [chartWidth / 2 - section.bottomWidth / 2, section.bottomY + verticalOffset]
217
+ ];
218
+
219
+ // 主体部分
220
+ g.append("polygon")
221
+ .attr("points", points.map(p => p.join(",")).join(" "))
222
+ .attr("fill", `url(#${gradientId})`)
223
+ .attr("filter", "url(#shadow)");
224
+
225
+ // 判断是否为底部三角形(最后一个元素,索引为0)
226
+ const isBottom = (i === 0);
227
+
228
+ // 右侧面(3D效果)- 对底部三角形特殊处理
229
+ let sidePoints;
230
+
231
+ if (isBottom) {
232
+ // 底部三角形的侧面不应超出底部
233
+ sidePoints = [
234
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset],
235
+ [chartWidth / 2 + section.topWidth / 2 + 15, section.topY + verticalOffset + 10],
236
+ [chartWidth / 2 + section.bottomWidth / 2, section.bottomY + verticalOffset]
237
+ ];
238
+ } else {
239
+ // 其他层的侧面正常绘制
240
+ sidePoints = [
241
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset],
242
+ [chartWidth / 2 + section.topWidth / 2 + 15, section.topY + verticalOffset + 10],
243
+ [chartWidth / 2 + section.bottomWidth / 2 + 15, section.bottomY + verticalOffset + 10],
244
+ [chartWidth / 2 + section.bottomWidth / 2, section.bottomY + verticalOffset]
245
+ ];
246
+ }
247
+
248
+ g.append("polygon")
249
+ .attr("points", sidePoints.map(p => p.join(",")).join(" "))
250
+ .attr("fill", darkerColor.toString());
251
+
252
+ // 仅为最顶部的部分添加顶面
253
+ if (i === sections.length - 1) {
254
+ // 顶部面(仅最顶层部分)
255
+ const topPoints = [
256
+ [chartWidth / 2 - section.topWidth / 2, section.topY + verticalOffset],
257
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset],
258
+ [chartWidth / 2 + section.topWidth / 2 + 15, section.topY + verticalOffset + 10],
259
+ [chartWidth / 2 - section.topWidth / 2 + 15, section.topY + verticalOffset + 10]
260
+ ];
261
+
262
+ g.append("polygon")
263
+ .attr("points", topPoints.map(p => p.join(",")).join(" "))
264
+ .attr("fill", chartUtils.color.variant(darkerColor, { mode: "brighter", amount: 0.5 }));
265
+ }
266
+ });
267
+
268
+ // 在梯形内部添加数据标签
269
+ sections.forEach((section, i) => {
270
+ const d = section.data;
271
+ const centerY = (section.topY + section.bottomY) / 2 + verticalOffset;
272
+ const centerX = chartWidth / 2;
273
+
274
+ // 计算该部分占比的百分比文本
275
+ const percentText = `${chartUtils.format.autoText(+d[valueField])} (${chartUtils.format.percent(d.percent).text})`;
276
+
277
+ // 添加数值和百分比标签
278
+ g.append("text")
279
+ .attr("x", centerX)
280
+ .attr("y", centerY)
281
+ .attr("text-anchor", "middle")
282
+ .attr("dominant-baseline", "middle")
283
+ .attr("font-size", "14px")
284
+ .attr("font-weight", "bold")
285
+ .attr("fill", "white")
286
+ .text(percentText);
287
+ });
288
+
289
+ // 辅助函数:估算文本宽度
290
+ function measureTextWidth(text, fontSize) {
291
+ return chartUtils.text.estimate(text, { fontSize: fontSize, factor: 0.6 }).width;
292
+ }
293
+
294
+ return svg.node();
295
+ }
modules/chart_engine/template/d3-js/pyramid_funnel/pyramid_diagram_01_hand.js ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Pyramid Diagram",
5
+ "chart_name": "pyramid_diagram_01_hand",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 10], [0, 100]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 400,
16
+ "background": "light",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "no",
20
+ "has_y_axis": "no"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+ function makeChart(containerSelector, data) {
26
+ // 提取数据
27
+ const jsonData = data;
28
+ const chartData = jsonData.data.data;
29
+ const variables = jsonData.variables;
30
+ const typography = jsonData.typography;
31
+ const colors = jsonData.colors || {};
32
+ const colorResolver = chartUtils.color.resolver(jsonData);
33
+ const dataColumns = chartUtils.schema.columns(jsonData);
34
+ const images = jsonData.images || {};
35
+
36
+ // 清空容器
37
+ d3.select(containerSelector).html("");
38
+
39
+ // 获取字段名
40
+ const categoryField = chartUtils.schema.columnField(dataColumns, 0);
41
+ const valueField = chartUtils.schema.columnField(dataColumns, 1);
42
+
43
+ // 按值从小到大排序数据(小的在顶部)
44
+ const sortedData = [...chartData].sort((a, b) => +a[valueField] - +b[valueField]);
45
+
46
+ // 计算总和以获取百分比
47
+ const total = d3.sum(sortedData, d => +d[valueField]);
48
+
49
+ // 为每个数据点添加百分比和累积百分比
50
+ let cumulativePercent = 0;
51
+ sortedData.forEach(d => {
52
+ d.percent = (+d[valueField] / total) * 100;
53
+ d.cumulativePercentStart = cumulativePercent;
54
+ cumulativePercent += d.percent;
55
+ d.cumulativePercentEnd = cumulativePercent;
56
+ });
57
+
58
+ // 设置尺寸和边距
59
+ const width = variables.width;
60
+ const height = variables.height;
61
+ const margin = { top: 40, right: 120, bottom: 40, left: 60 };
62
+
63
+ // 创建SVG
64
+ const svg = d3.select(containerSelector)
65
+ .append("svg")
66
+ .attr("width", "100%")
67
+ .attr("height", height)
68
+ .attr("viewBox", `0 0 ${width} ${height}`)
69
+ .attr("style", "max-width: 100%; height: auto;")
70
+ .attr("xmlns", "http://www.w3.org/2000/svg")
71
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
72
+
73
+ // 创建图表区域
74
+ const chartWidth = width - margin.left - margin.right;
75
+ const chartHeight = height - margin.top - margin.bottom;
76
+
77
+ const g = svg.append("g")
78
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
79
+
80
+ // 计算金字塔的最大宽度(底部)和高度
81
+ const maxPyramidWidth = chartWidth * 0.6;
82
+ const pyramidHeight = chartHeight * 0.6; // 使用90%的高度,留出上下空间
83
+
84
+ // 计算面积比例
85
+ // 金字塔总面积
86
+ const totalArea = maxPyramidWidth * pyramidHeight / 2;
87
+
88
+ // 计算每个部分的高度(基于面积比例)
89
+ let currentHeight = 0;
90
+ const sections = [];
91
+
92
+ sortedData.forEach((d, i) => {
93
+ // 该部分应占的面积比例
94
+ const areaRatio = d.percent / 100;
95
+ const sectionArea = totalArea * areaRatio;
96
+
97
+ // 计算该部分的高度
98
+ // 对于梯形,面积 = (上底+下底) * 高 / 2
99
+ // 我们需要求解高度,已知面积和下底(上一部分的上底)
100
+
101
+ // 首先计算该部分在整个三角形中的相对位置
102
+ const bottomPosition = currentHeight / pyramidHeight;
103
+ // 正三角形:底部宽,顶部窄
104
+ const bottomWidth = maxPyramidWidth * bottomPosition;
105
+
106
+ // 求解该部分的高度
107
+ // 设高度为h,则上底 = maxPyramidWidth * (currentHeight + h) / pyramidHeight
108
+ // 面积方程:sectionArea = (bottomWidth + topWidth) * h / 2
109
+
110
+ // 简化后的二次方程:
111
+ // h^2 * (maxPyramidWidth / (2 * pyramidHeight)) + h * bottomWidth - 2 * sectionArea = 0
112
+
113
+ const a = maxPyramidWidth / (2 * pyramidHeight);
114
+ const b = bottomWidth;
115
+ const c = -2 * sectionArea;
116
+
117
+ // 使用求根公式
118
+ const h = (-b + Math.sqrt(b*b - 4*a*c)) / (2*a);
119
+
120
+ // 计算该部分的上底宽度
121
+ const topPosition = (currentHeight + h) / pyramidHeight;
122
+ const topWidth = maxPyramidWidth * topPosition;
123
+
124
+ sections.push({
125
+ data: d,
126
+ bottomY: currentHeight,
127
+ topY: currentHeight + h,
128
+ bottomWidth: bottomWidth,
129
+ topWidth: topWidth
130
+ });
131
+
132
+ currentHeight += h;
133
+ });
134
+
135
+ // 计算垂直居中的偏移量
136
+ const verticalOffset = (chartHeight - pyramidHeight) / 2;
137
+
138
+ // 绘制金字塔的每一层
139
+ sections.forEach((section, i) => {
140
+ const d = section.data;
141
+
142
+ // 获取颜色
143
+ const color = colorResolver.field(d[categoryField], i, { palette: "category10" }).value;
144
+
145
+ // 绘制梯形 - 添加垂直偏移
146
+ const points = [
147
+ [chartWidth / 2 - section.topWidth / 2, section.topY + verticalOffset],
148
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset],
149
+ [chartWidth / 2 + section.bottomWidth / 2, section.bottomY + verticalOffset],
150
+ [chartWidth / 2 - section.bottomWidth / 2, section.bottomY + verticalOffset]
151
+ ];
152
+
153
+ g.append("polygon")
154
+ .attr("points", points.map(p => p.join(",")).join(" "))
155
+ .attr("fill", color);
156
+
157
+ // 计算标签位置,避免重叠 - 添加垂直偏移
158
+ const labelY = (section.topY + section.bottomY) / 2 + verticalOffset;
159
+ const labelX = chartWidth / 2;
160
+
161
+ // 数据标签位置(在金字塔中心)
162
+ g.append("text")
163
+ .attr("x", labelX)
164
+ .attr("y", labelY)
165
+ .attr("text-anchor", "middle")
166
+ .attr("dominant-baseline", "middle")
167
+ .attr("font-size", "14px")
168
+ .attr("font-weight", "bold")
169
+ .attr("fill", "white")
170
+ .text(`${chartUtils.format.autoText(+d[valueField])}`);
171
+
172
+ // 类别标签(在左侧)
173
+ g.append("text")
174
+ .attr("x", chartWidth / 2 - Math.max(section.topWidth, section.bottomWidth) / 2 - 10)
175
+ .attr("y", labelY)
176
+ .attr("text-anchor", "end")
177
+ .attr("dominant-baseline", "middle")
178
+ .attr("font-size", "14px")
179
+ .attr("font-weight", "bold")
180
+ .attr("fill", colorResolver.field(d[categoryField], i, { palette: "category10" }).value)
181
+ .text(`${d[categoryField]}`);
182
+ });
183
+
184
+ const roughness = 1;
185
+ const bowing = 2;
186
+ const fillStyle = "hachure";
187
+ const randomize = false;
188
+ const pencilFilter = false;
189
+
190
+ const svgConverter = new svg2roughjs.Svg2Roughjs(containerSelector);
191
+ svgConverter.pencilFilter = pencilFilter;
192
+ svgConverter.randomize = randomize;
193
+ svgConverter.svg = svg.node();
194
+ svgConverter.roughConfig = {
195
+ bowing,
196
+ roughness,
197
+ fillStyle
198
+ };
199
+ svgConverter.sketch();
200
+ // Remove the first SVG element if it exists
201
+ const firstSvg = document.querySelector(`${containerSelector} svg`);
202
+ if (firstSvg) {
203
+ firstSvg.remove();
204
+ }
205
+
206
+ return svg.node();
207
+ }
modules/chart_engine/template/d3-js/pyramid_funnel/pyramid_diagram_02.js ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Pyramid Diagram",
5
+ "chart_name": "pyramid_diagram_02",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 10], [0, 100]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 400,
16
+ "background": "light",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "no",
20
+ "has_y_axis": "no"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+ function makeChart(containerSelector, data) {
26
+ // 提取数据
27
+ const jsonData = data;
28
+ const chartData = jsonData.data.data;
29
+ const variables = jsonData.variables;
30
+ const typography = jsonData.typography;
31
+ const colors = jsonData.colors || {};
32
+ const colorResolver = chartUtils.color.resolver(jsonData);
33
+ const dataColumns = chartUtils.schema.columns(jsonData);
34
+ const images = jsonData.images || {};
35
+
36
+ // 清空容器
37
+ d3.select(containerSelector).html("");
38
+
39
+ // 获取字段名
40
+ const categoryField = chartUtils.schema.columnField(dataColumns, 0);
41
+ const valueField = chartUtils.schema.columnField(dataColumns, 1);
42
+
43
+ // 按值从小到大排序数据(小的在顶部)
44
+ const sortedData = [...chartData].sort((a, b) => +a[valueField] - +b[valueField]);
45
+
46
+ // 计算总和以获取百分比
47
+ const total = d3.sum(sortedData, d => +d[valueField]);
48
+
49
+ // 为每个数据点添加百分比和累积百分比
50
+ let cumulativePercent = 0;
51
+ sortedData.forEach(d => {
52
+ d.percent = (+d[valueField] / total) * 100;
53
+ d.cumulativePercentStart = cumulativePercent;
54
+ cumulativePercent += d.percent;
55
+ d.cumulativePercentEnd = cumulativePercent;
56
+ });
57
+
58
+ // 设置尺寸和边距
59
+ const width = variables.width;
60
+ const height = variables.height;
61
+ const margin = { top: 40, right: 120, bottom: 40, left: 60 };
62
+
63
+ // 创建SVG
64
+ const svg = d3.select(containerSelector)
65
+ .append("svg")
66
+ .attr("width", "100%")
67
+ .attr("height", height)
68
+ .attr("viewBox", `0 0 ${width} ${height}`)
69
+ .attr("style", "max-width: 100%; height: auto;")
70
+ .attr("xmlns", "http://www.w3.org/2000/svg")
71
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
72
+
73
+ // 创建图表区域
74
+ const chartWidth = width - margin.left - margin.right;
75
+ const chartHeight = height - margin.top - margin.bottom;
76
+
77
+ const g = svg.append("g")
78
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
79
+
80
+ // 创建金属光泽的渐变定义
81
+ const defs = svg.append("defs");
82
+
83
+ // 计算金字塔的最大宽度(底部)和高度
84
+ const maxPyramidWidth = chartWidth * 0.6;
85
+ const pyramidHeight = chartHeight * 0.6; // 使用90%的高度,留出上下空间
86
+
87
+ // 计算面积比例
88
+ // 金字塔总面积
89
+ const totalArea = maxPyramidWidth * pyramidHeight / 2;
90
+
91
+ // 计算每个部分的高度(基于面积比例)
92
+ let currentHeight = 0;
93
+ const sections = [];
94
+
95
+ sortedData.forEach((d, i) => {
96
+ // 该部分应占的面积比例
97
+ const areaRatio = d.percent / 100;
98
+ const sectionArea = totalArea * areaRatio;
99
+
100
+ // 计算该部分的高度
101
+ // 对于梯形,面积 = (上底+下底) * 高 / 2
102
+ // 我们需要求解高度,已知面积和下底(上一部分的上底)
103
+
104
+ // 首先计算该部分在整个三角形中的相对位置
105
+ const bottomPosition = currentHeight / pyramidHeight;
106
+ // 正三角形:底部宽,顶部窄
107
+ const bottomWidth = maxPyramidWidth * bottomPosition;
108
+
109
+ // 求解该部分的高度
110
+ // 设高度为h,则上底 = maxPyramidWidth * (currentHeight + h) / pyramidHeight
111
+ // 面积方程:sectionArea = (bottomWidth + topWidth) * h / 2
112
+
113
+ // 简化后的二次方程:
114
+ // h^2 * (maxPyramidWidth / (2 * pyramidHeight)) + h * bottomWidth - 2 * sectionArea = 0
115
+
116
+ const a = maxPyramidWidth / (2 * pyramidHeight);
117
+ const b = bottomWidth;
118
+ const c = -2 * sectionArea;
119
+
120
+ // 使用求根公式
121
+ const h = (-b + Math.sqrt(b*b - 4*a*c)) / (2*a);
122
+
123
+ // 计算该部分的上底宽度
124
+ const topPosition = (currentHeight + h) / pyramidHeight;
125
+ const topWidth = maxPyramidWidth * topPosition;
126
+
127
+ sections.push({
128
+ data: d,
129
+ bottomY: currentHeight,
130
+ topY: currentHeight + h,
131
+ bottomWidth: bottomWidth,
132
+ topWidth: topWidth
133
+ });
134
+
135
+ currentHeight += h;
136
+ });
137
+
138
+ // 计算垂直居中的偏移量
139
+ const verticalOffset = (chartHeight - pyramidHeight) / 2;
140
+
141
+ // 绘制金字塔的每一层
142
+ sections.forEach((section, i) => {
143
+ const d = section.data;
144
+
145
+ // 获取颜色
146
+ const color = colorResolver.field(d[categoryField], i, { palette: "category10" }).value;
147
+
148
+ // 为每个部分创建金属光泽渐变
149
+ const gradientId = `metallic-gradient-${i}`;
150
+ const gradient = defs.append("linearGradient")
151
+ .attr("id", gradientId)
152
+ .attr("x1", "0%")
153
+ .attr("y1", "0%")
154
+ .attr("x2", "100%")
155
+ .attr("y2", "100%");
156
+
157
+ // 解析颜色以创建金属效果
158
+ const baseColor = d3.rgb(color);
159
+ const lighterColor = d3.rgb(
160
+ Math.min(255, baseColor.r + 60),
161
+ Math.min(255, baseColor.g + 60),
162
+ Math.min(255, baseColor.b + 60)
163
+ );
164
+ const darkerColor = d3.rgb(
165
+ Math.max(0, baseColor.r - 30),
166
+ Math.max(0, baseColor.g - 30),
167
+ Math.max(0, baseColor.b - 30)
168
+ );
169
+
170
+ // 添加渐变停止点来创建金属效果
171
+ gradient.append("stop")
172
+ .attr("offset", "0%")
173
+ .attr("stop-color", lighterColor.toString());
174
+ gradient.append("stop")
175
+ .attr("offset", "30%")
176
+ .attr("stop-color", color);
177
+ gradient.append("stop")
178
+ .attr("offset", "70%")
179
+ .attr("stop-color", color);
180
+ gradient.append("stop")
181
+ .attr("offset", "100%")
182
+ .attr("stop-color", darkerColor.toString());
183
+
184
+ const y_padding = 5;
185
+
186
+ // 绘制梯形 - 添加垂直偏移
187
+ const points = [
188
+ [chartWidth / 2 - section.topWidth / 2, section.topY + verticalOffset + y_padding * i],
189
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset + y_padding * i],
190
+ [chartWidth / 2 + section.bottomWidth / 2, section.bottomY + verticalOffset + y_padding * i],
191
+ [chartWidth / 2 - section.bottomWidth / 2, section.bottomY + verticalOffset + y_padding * i]
192
+ ];
193
+
194
+ g.append("polygon")
195
+ .attr("points", points.map(p => p.join(",")).join(" "))
196
+ .attr("fill", `url(#${gradientId})`)
197
+ .attr("stroke", darkerColor.toString())
198
+ .attr("stroke-width", 0.5);
199
+
200
+ // 计算标签位置,避免重叠 - 添加垂直偏移
201
+ const labelY = (section.topY + section.bottomY) / 2 + verticalOffset + y_padding * i;
202
+ const labelX = chartWidth / 2;
203
+
204
+ const textWidth = chartUtils.text.measure(null, d[categoryField], { fontSize: 14 }).width + 20;
205
+ if (textWidth > (section.topWidth + section.bottomWidth) / 2) {
206
+ // 添加一个暗色透明背景
207
+ g.append("rect")
208
+ .attr("x", labelX - textWidth / 2)
209
+ .attr("y", labelY - 15)
210
+ .attr("width", textWidth)
211
+ .attr("height", 30)
212
+ .attr("fill", "rgba(0, 0, 0, 0.3)")
213
+ .attr("rx", 5)
214
+ .attr("ry", 5);
215
+ }
216
+
217
+ // 添加标签
218
+ g.append("text")
219
+ .attr("x", labelX)
220
+ .attr("y", labelY)
221
+ .attr("text-anchor", "middle")
222
+ .attr("dominant-baseline", "middle")
223
+ .attr("font-size", "14px")
224
+ .attr("font-weight", "bold")
225
+ .attr("fill", "white")
226
+ .text(`${d[categoryField]}`);
227
+
228
+ // 添加数值标签
229
+ g.append("text")
230
+ .attr("x", chartWidth / 2 + Math.max(section.topWidth, section.bottomWidth) / 2 + 10)
231
+ .attr("y", labelY)
232
+ .attr("text-anchor", "start")
233
+ .attr("dominant-baseline", "middle")
234
+ .attr("font-size", "12px")
235
+ .attr("font-weight", "bold")
236
+ .text(`${chartUtils.format.autoText(+d[valueField])} (${chartUtils.format.percent(d.percent).text})`);
237
+ });
238
+
239
+ return svg.node();
240
+ }
modules/chart_engine/template/d3-js/pyramid_funnel/pyramid_diagram_02_hand.js ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Pyramid Diagram",
5
+ "chart_name": "pyramid_diagram_02_hand",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 10], [0, 100]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 400,
16
+ "background": "dark",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "no",
20
+ "has_y_axis": "no"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+ function makeChart(containerSelector, data) {
26
+ // 提取数据
27
+ const jsonData = data;
28
+ const chartData = jsonData.data.data;
29
+ const variables = jsonData.variables;
30
+ const typography = jsonData.typography;
31
+ const colors = jsonData.colors_dark || {};
32
+ const colorResolver = chartUtils.color.resolver(jsonData);
33
+ const dataColumns = chartUtils.schema.columns(jsonData);
34
+ const images = jsonData.images || {};
35
+
36
+ // 清空容器
37
+ d3.select(containerSelector).html("");
38
+
39
+ // 获取字段名
40
+ const categoryField = chartUtils.schema.columnField(dataColumns, 0);
41
+ const valueField = chartUtils.schema.columnField(dataColumns, 1);
42
+
43
+ // 按值从小到大排序数据(小的在顶部)
44
+ const sortedData = [...chartData].sort((a, b) => +a[valueField] - +b[valueField]);
45
+
46
+ // 计算总和以获取百分比
47
+ const total = d3.sum(sortedData, d => +d[valueField]);
48
+
49
+ // 为每个数据点添加百分比和累积百分比
50
+ let cumulativePercent = 0;
51
+ sortedData.forEach(d => {
52
+ d.percent = (+d[valueField] / total) * 100;
53
+ d.cumulativePercentStart = cumulativePercent;
54
+ cumulativePercent += d.percent;
55
+ d.cumulativePercentEnd = cumulativePercent;
56
+ });
57
+
58
+ // 设置尺寸和边距
59
+ const width = variables.width;
60
+ const height = variables.height;
61
+ const margin = { top: 40, right: 120, bottom: 40, left: 60 };
62
+
63
+ // 创建SVG
64
+ const svg = d3.select(containerSelector)
65
+ .append("svg")
66
+ .attr("width", "100%")
67
+ .attr("height", height)
68
+ .attr("viewBox", `0 0 ${width} ${height}`)
69
+ .attr("style", "max-width: 100%; height: auto;")
70
+ .attr("xmlns", "http://www.w3.org/2000/svg")
71
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
72
+
73
+ // 创建图表区域
74
+ const chartWidth = width - margin.left - margin.right;
75
+ const chartHeight = height - margin.top - margin.bottom;
76
+
77
+ const g = svg.append("g")
78
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
79
+
80
+ // 计算金字塔的最大宽度(底部)和高度
81
+ const maxPyramidWidth = chartWidth * 0.6;
82
+ const pyramidHeight = chartHeight * 0.6; // 使用90%的高度,留出上下空间
83
+
84
+ // 计算面积比例
85
+ // 金字塔总面积
86
+ const totalArea = maxPyramidWidth * pyramidHeight / 2;
87
+
88
+ // 计算每个部分的高度(基于面积比例)
89
+ let currentHeight = 0;
90
+ const sections = [];
91
+
92
+ sortedData.forEach((d, i) => {
93
+ // 该部分应占的面积比例
94
+ const areaRatio = d.percent / 100;
95
+ const sectionArea = totalArea * areaRatio;
96
+
97
+ // 计算该部分的高度
98
+ // 对于梯形,面积 = (上底+下底) * 高 / 2
99
+ // 我们需要求解高度,已知面积和下底(上一部分的上底)
100
+
101
+ // 首先计算该部分在整个三角形中的相对位置
102
+ const bottomPosition = currentHeight / pyramidHeight;
103
+ // 正三角形:底部宽,顶部窄
104
+ const bottomWidth = maxPyramidWidth * bottomPosition;
105
+
106
+ // 求解该部分的高度
107
+ // 设高度为h,则上底 = maxPyramidWidth * (currentHeight + h) / pyramidHeight
108
+ // 面积方程:sectionArea = (bottomWidth + topWidth) * h / 2
109
+
110
+ // 简化后的二次方程:
111
+ // h^2 * (maxPyramidWidth / (2 * pyramidHeight)) + h * bottomWidth - 2 * sectionArea = 0
112
+
113
+ const a = maxPyramidWidth / (2 * pyramidHeight);
114
+ const b = bottomWidth;
115
+ const c = -2 * sectionArea;
116
+
117
+ // 使用求根公式
118
+ const h = (-b + Math.sqrt(b*b - 4*a*c)) / (2*a);
119
+
120
+ // 计算该部分的上底宽度
121
+ const topPosition = (currentHeight + h) / pyramidHeight;
122
+ const topWidth = maxPyramidWidth * topPosition;
123
+
124
+ sections.push({
125
+ data: d,
126
+ bottomY: currentHeight,
127
+ topY: currentHeight + h,
128
+ bottomWidth: bottomWidth,
129
+ topWidth: topWidth
130
+ });
131
+
132
+ currentHeight += h;
133
+ });
134
+
135
+ // 计算垂直居中的偏移量
136
+ const verticalOffset = (chartHeight - pyramidHeight) / 2;
137
+
138
+ // 绘制金字塔的每一层
139
+ sections.forEach((section, i) => {
140
+ const d = section.data;
141
+
142
+ // 获取颜色
143
+ const color = colorResolver.field(d[categoryField], i, { palette: "category10" }).value;
144
+
145
+ // 绘制梯形 - 添加垂直偏移
146
+ const points = [
147
+ [chartWidth / 2 - section.topWidth / 2, section.topY + verticalOffset],
148
+ [chartWidth / 2 + section.topWidth / 2, section.topY + verticalOffset],
149
+ [chartWidth / 2 + section.bottomWidth / 2, section.bottomY + verticalOffset],
150
+ [chartWidth / 2 - section.bottomWidth / 2, section.bottomY + verticalOffset]
151
+ ];
152
+
153
+ g.append("polygon")
154
+ .attr("points", points.map(p => p.join(",")).join(" "))
155
+ .attr("fill", color);
156
+
157
+ // 计算标签位置,避免重叠 - 添加垂直偏移
158
+ const labelY = (section.topY + section.bottomY) / 2 + verticalOffset;
159
+ const labelX = chartWidth / 2;
160
+
161
+ // 数据标签位置(在金字塔中心)
162
+ g.append("text")
163
+ .attr("x", labelX)
164
+ .attr("y", labelY)
165
+ .attr("text-anchor", "middle")
166
+ .attr("dominant-baseline", "middle")
167
+ .attr("font-size", "14px")
168
+ .attr("font-weight", "bold")
169
+ .attr("fill", "white")
170
+ .text(`${chartUtils.format.autoText(+d[valueField])}`);
171
+
172
+ // 类别标签(在左侧)
173
+ g.append("text")
174
+ .attr("x", chartWidth / 2 + Math.max(section.topWidth, section.bottomWidth) / 2 + 10)
175
+ .attr("y", labelY)
176
+ .attr("text-anchor", "start")
177
+ .attr("dominant-baseline", "middle")
178
+ .attr("font-size", "14px")
179
+ .attr("font-weight", "bold")
180
+ .attr("fill", colorResolver.field(d[categoryField], i, { palette: "category10" }).value)
181
+ .text(`${d[categoryField]}`);
182
+ });
183
+
184
+ const roughness = 1;
185
+ const bowing = 2;
186
+ const fillStyle = "hachure";
187
+ const randomize = false;
188
+ const pencilFilter = false;
189
+
190
+ const svgConverter = new svg2roughjs.Svg2Roughjs(containerSelector);
191
+ svgConverter.pencilFilter = pencilFilter;
192
+ svgConverter.randomize = randomize;
193
+ svgConverter.svg = svg.node();
194
+ svgConverter.roughConfig = {
195
+ bowing,
196
+ roughness,
197
+ fillStyle
198
+ };
199
+ svgConverter.sketch();
200
+ // Remove the first SVG element if it exists
201
+ const firstSvg = document.querySelector(`${containerSelector} svg`);
202
+ if (firstSvg) {
203
+ firstSvg.remove();
204
+ }
205
+
206
+ return svg.node();
207
+ }