Ray1ee01 commited on
Commit
7be5861
·
verified ·
1 Parent(s): e134901

Upload folder using huggingface_hub

Browse files
modules/chart_engine/template/d3-js/treemap/treemap_01.js ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Treemap",
5
+ "chart_name": "treemap_01",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 20], [0, "inf"]],
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": 600,
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 width = variables.width;
45
+ const height = variables.height;
46
+ const margin = { top: 10, right: 10, bottom: 10, left: 10 };
47
+
48
+ // 创建SVG
49
+ const svg = d3.select(containerSelector)
50
+ .append("svg")
51
+ .attr("width", "100%")
52
+ .attr("height", height)
53
+ .attr("viewBox", `0 0 ${width} ${height}`)
54
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
55
+ .attr("xmlns", "http://www.w3.org/2000/svg")
56
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
57
+
58
+ // 创建图表区域
59
+ const chartWidth = width - margin.left - margin.right;
60
+ const chartHeight = height - margin.top - margin.bottom;
61
+
62
+ const g = svg.append("g")
63
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
64
+
65
+ // 准备层次结构数据
66
+ // 将扁平数据转换为层次结构
67
+ const hierarchyData = {
68
+ name: "root",
69
+ children: []
70
+ };
71
+
72
+ // 按类别分组数据
73
+ const groupedData = d3.group(chartData, d => d[categoryField]);
74
+
75
+ // 将分组数据转换为层次结构
76
+ groupedData.forEach((values, category) => {
77
+ // 计算该类别的总值
78
+ const total = d3.sum(values, d => +d[valueField]);
79
+
80
+ hierarchyData.children.push({
81
+ name: category,
82
+ value: total
83
+ });
84
+ });
85
+
86
+ // 创建颜色比例尺
87
+ const colorScale = d => colorResolver.field(d, hierarchyData.children.findIndex(item => item.name === d), { palette: "tableau10" }).value;
88
+
89
+ // 计算树图布局
90
+ const root = d3.treemap()
91
+ .size([chartWidth, chartHeight])
92
+ .padding(3)
93
+ .round(true)
94
+ (d3.hierarchy(hierarchyData)
95
+ .sum(d => d.value)
96
+ .sort((a, b) => b.value - a.value));
97
+
98
+ // 为每个叶子节点创建一个单元格
99
+ const leaf = g.selectAll("g")
100
+ .data(root.leaves())
101
+ .join("g")
102
+ .attr("transform", d => `translate(${d.x0},${d.y0})`);
103
+
104
+ // 添加矩形
105
+ leaf.append("rect")
106
+ .attr("width", d => Math.max(0, d.x1 - d.x0))
107
+ .attr("height", d => Math.max(0, d.y1 - d.y0))
108
+ .attr("fill", d => {
109
+ // 获取类别名称
110
+ const category = d.data.name;
111
+ return colorScale(category);
112
+ })
113
+ .attr("fill-opacity", 0.8)
114
+ .attr("stroke", "#fff");
115
+
116
+ // 为每个矩形添加工具提示
117
+ const format = value => chartUtils.format.autoText(+value);
118
+ leaf.append("title")
119
+ .text(d => `${d.data.name}: ${format(d.value)}`);
120
+
121
+ // 添加文本标签
122
+ leaf.append("text")
123
+ .attr("x", 4)
124
+ .attr("y", 14)
125
+ .attr("fill", "#fff")
126
+ .attr("font-size", "12px")
127
+ .attr("font-weight", "bold")
128
+ .text(d => d.data.name)
129
+ .each(function(d) {
130
+ // 检查文本是否适合矩形
131
+ const rectWidth = d.x1 - d.x0;
132
+ const maxTextWidth = rectWidth - 8;
133
+ const measureLabelWidth = value => chartUtils.text.measure(null, value, {
134
+ fontSize: 12,
135
+ fontWeight: "bold"
136
+ }).width;
137
+
138
+ // 如果文本太长,截断它
139
+ const text = d3.select(this);
140
+ let textContent = text.text();
141
+ let displayText = textContent;
142
+ while (measureLabelWidth(displayText) > maxTextWidth && textContent.length > 0) {
143
+ textContent = textContent.slice(0, -1);
144
+ displayText = textContent + "...";
145
+ text.text(displayText);
146
+ }
147
+ });
148
+
149
+ // 添加值标签
150
+ leaf.append("text")
151
+ .attr("x", 4)
152
+ .attr("y", 30)
153
+ .attr("fill", "#fff")
154
+ .attr("fill-opacity", 0.7)
155
+ .attr("font-size", "10px")
156
+ .text(d => format(d.value))
157
+ .each(function(d) {
158
+ // 检查文本是否适合矩形
159
+ const textWidth = chartUtils.text.measure(null, format(d.value), {
160
+ fontSize: 10
161
+ }).width;
162
+ const rectWidth = d.x1 - d.x0;
163
+
164
+ if (textWidth > rectWidth - 8 || (d.y1 - d.y0) < 40) {
165
+ // 如果文本太长或矩形太小,隐藏它
166
+ d3.select(this).style("display", "none");
167
+ }
168
+ });
169
+
170
+ return svg.node();
171
+ }
modules/chart_engine/template/d3-js/treemap/treemap_01_dark.js ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Treemap",
5
+ "chart_name": "treemap_01_dark",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 20], [0, "inf"]],
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": 600,
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 width = variables.width;
48
+ const height = variables.height;
49
+ const margin = { top: 10, right: 10, bottom: 10, left: 10 };
50
+
51
+ // 创建SVG
52
+ const svg = d3.select(containerSelector)
53
+ .append("svg")
54
+ .attr("width", "100%")
55
+ .attr("height", height)
56
+ .attr("viewBox", `0 0 ${width} ${height}`)
57
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
58
+ .attr("xmlns", "http://www.w3.org/2000/svg")
59
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
60
+
61
+ // 创建图表区域
62
+ const chartWidth = width - margin.left - margin.right;
63
+ const chartHeight = height - margin.top - margin.bottom;
64
+
65
+ const g = svg.append("g")
66
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
67
+
68
+ // 准备层次结构数据
69
+ // 将扁平数据转换为层次结构
70
+ const hierarchyData = {
71
+ name: "root",
72
+ children: []
73
+ };
74
+
75
+ // 按类别分组数据
76
+ const groupedData = d3.group(chartData, d => d[categoryField]);
77
+
78
+ // 将分组数据转换为层次结构
79
+ groupedData.forEach((values, category) => {
80
+ // 计算该类别的总值
81
+ const total = d3.sum(values, d => +d[valueField]);
82
+
83
+ hierarchyData.children.push({
84
+ name: category,
85
+ value: total
86
+ });
87
+ });
88
+
89
+ // 创建颜色比例尺
90
+ const colorScale = d => colorResolver.field(d, hierarchyData.children.findIndex(item => item.name === d), { palette: "tableau10" }).value;
91
+
92
+ // 计算树图布局
93
+ const root = d3.treemap()
94
+ .size([chartWidth, chartHeight])
95
+ .padding(3)
96
+ .round(true)
97
+ (d3.hierarchy(hierarchyData)
98
+ .sum(d => d.value)
99
+ .sort((a, b) => b.value - a.value));
100
+
101
+ // 为每个叶子节点创建一个单元格
102
+ const leaf = g.selectAll("g")
103
+ .data(root.leaves())
104
+ .join("g")
105
+ .attr("transform", d => `translate(${d.x0},${d.y0})`);
106
+
107
+ // 添加矩形
108
+ leaf.append("rect")
109
+ .attr("width", d => Math.max(0, d.x1 - d.x0))
110
+ .attr("height", d => Math.max(0, d.y1 - d.y0))
111
+ .attr("fill", d => {
112
+ // 获取类别名称
113
+ const category = d.data.name;
114
+ return colorScale(category);
115
+ })
116
+ .attr("fill-opacity", 0.8)
117
+ .attr("stroke", "#fff");
118
+
119
+ // 为每个矩形添加工具提示
120
+ const format = value => chartUtils.format.autoText(+value);
121
+ leaf.append("title")
122
+ .text(d => `${d.data.name}: ${format(d.value)}`);
123
+
124
+ // 添加文本标签
125
+ leaf.append("text")
126
+ .attr("x", 4)
127
+ .attr("y", 14)
128
+ .attr("fill", "#fff")
129
+ .attr("font-size", "12px")
130
+ .attr("font-weight", "bold")
131
+ .text(d => d.data.name)
132
+ .each(function(d) {
133
+ // 检查文本是否适合矩形
134
+ const rectWidth = d.x1 - d.x0;
135
+ const maxTextWidth = rectWidth - 8;
136
+ const measureLabelWidth = value => chartUtils.text.measure(null, value, {
137
+ fontSize: 12,
138
+ fontWeight: "bold"
139
+ }).width;
140
+
141
+ // 如果文本太长,截断它
142
+ const text = d3.select(this);
143
+ let textContent = text.text();
144
+ let displayText = textContent;
145
+ while (measureLabelWidth(displayText) > maxTextWidth && textContent.length > 0) {
146
+ textContent = textContent.slice(0, -1);
147
+ displayText = textContent + "...";
148
+ text.text(displayText);
149
+ }
150
+ });
151
+
152
+ // 添加值标签
153
+ leaf.append("text")
154
+ .attr("x", 4)
155
+ .attr("y", 30)
156
+ .attr("fill", "#fff")
157
+ .attr("fill-opacity", 0.7)
158
+ .attr("font-size", "10px")
159
+ .text(d => format(d.value))
160
+ .each(function(d) {
161
+ // 检查文本是否适合矩形
162
+ const textWidth = chartUtils.text.measure(null, format(d.value), {
163
+ fontSize: 10
164
+ }).width;
165
+ const rectWidth = d.x1 - d.x0;
166
+
167
+ if (textWidth > rectWidth - 8 || (d.y1 - d.y0) < 40) {
168
+ // 如果文本太长或矩形太小,隐藏它
169
+ d3.select(this).style("display", "none");
170
+ }
171
+ });
172
+
173
+ return svg.node();
174
+ }
modules/chart_engine/template/d3-js/treemap/treemap_02.js ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Treemap",
5
+ "chart_name": "treemap_02",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 20], [0, "inf"]],
9
+ "required_fields_icons": ["x"],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 600,
16
+ "background": "light",
17
+ "icon_mark": "overlay",
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
+ const valueUnit = chartUtils.schema.column(dataColumns, 1).unit;
44
+
45
+ // 设置尺寸和边距
46
+ const width = variables.width;
47
+ const height = variables.height;
48
+ const margin = { top: 10, right: 10, bottom: 10, left: 10 };
49
+
50
+ // 创建SVG
51
+ const svg = d3.select(containerSelector)
52
+ .append("svg")
53
+ .attr("width", "100%")
54
+ .attr("height", height)
55
+ .attr("viewBox", `0 0 ${width} ${height}`)
56
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
57
+ .attr("xmlns", "http://www.w3.org/2000/svg")
58
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
59
+
60
+ // 创建图表区域
61
+ const chartWidth = width - margin.left - margin.right;
62
+ const chartHeight = height - margin.top - margin.bottom;
63
+
64
+ const g = svg.append("g")
65
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
66
+
67
+ // 准备层次结构数据
68
+ // 将扁平数据转换为层次结构
69
+ const hierarchyData = {
70
+ name: "root",
71
+ children: []
72
+ };
73
+
74
+ // 按类别分组数据
75
+ const groupedData = d3.group(chartData, d => d[categoryField]);
76
+
77
+ // 将分组数据转换为层次结构
78
+ groupedData.forEach((values, category) => {
79
+ // 计算该类别的总值
80
+ const total = d3.sum(values, d => +d[valueField]);
81
+
82
+ hierarchyData.children.push({
83
+ name: category,
84
+ value: total
85
+ });
86
+ });
87
+
88
+ // 创建颜色比例尺
89
+ const colorScale = d => colorResolver.field(d, hierarchyData.children.findIndex(item => item.name === d), { palette: "tableau10" }).value;
90
+
91
+ // 计算树图布局
92
+ const root = d3.treemap()
93
+ .size([chartWidth, chartHeight])
94
+ .padding(8)
95
+ .round(true)
96
+ (d3.hierarchy(hierarchyData)
97
+ .sum(d => d.value)
98
+ .sort((a, b) => b.value - a.value));
99
+
100
+ // 为每个叶子节点创建一个单元格
101
+ const leaf = g.selectAll("g")
102
+ .data(root.leaves())
103
+ .join("g")
104
+ .attr("transform", d => `translate(${d.x0},${d.y0})`);
105
+
106
+ // 添加矩形
107
+ leaf.append("rect")
108
+ .attr("width", d => Math.max(0, d.x1 - d.x0))
109
+ .attr("height", d => Math.max(0, d.y1 - d.y0))
110
+ .attr("rx", 8)
111
+ .attr("ry", 8)
112
+ .attr("fill", d => {
113
+ // 获取类别名称
114
+ const category = d.data.name;
115
+ return colorScale(category);
116
+ })
117
+ .attr("fill-opacity", 0.8)
118
+ .attr("stroke", "none");
119
+
120
+ // 为每个矩形添加工具提示
121
+ const format = value => chartUtils.format.autoText(+value);
122
+ leaf.append("title")
123
+ .text(d => `${d.data.name}: ${format(d.value)}`);
124
+
125
+ // 创建包含标签和图标的组
126
+ const labelGroup = leaf.append("g")
127
+ .attr("transform", "translate(12, 12)"); // 增加边距
128
+ const measureCategoryLabelWidth = value => chartUtils.text.measure(null, value, {
129
+ fontSize: 20,
130
+ fontWeight: "bold"
131
+ }).width;
132
+ const measureValueLabelWidth = value => chartUtils.text.measure(null, value, {
133
+ fontSize: 16
134
+ }).width;
135
+
136
+ // 添加类别标签 (x label) - 变大并放在左上角,使用白色
137
+ labelGroup.append("text")
138
+ .attr("class", "category-label")
139
+ .attr("x", 0)
140
+ .attr("y", 18)
141
+ .attr("fill", "#ffffff")
142
+ .attr("font-size", "20px") // 字体变得更大
143
+ .attr("font-weight", "bold") // 使用粗体
144
+ .text(d => d.data.name)
145
+ .each(function(d) {
146
+ // 检查文本是否适合矩形(考虑图标宽度)
147
+ const rectWidth = d.x1 - d.x0 - 48; // 减去图标宽度和更多间距
148
+ const maxTextWidth = rectWidth - 12;
149
+
150
+ // 如果文本太长,截断它
151
+ const text = d3.select(this);
152
+ let textContent = text.text();
153
+ let displayText = textContent;
154
+ while (measureCategoryLabelWidth(displayText) > maxTextWidth && textContent.length > 0) {
155
+ textContent = textContent.slice(0, -1);
156
+ displayText = textContent + "...";
157
+ text.text(displayText);
158
+ }
159
+ });
160
+
161
+ // 添加图标(在x label右侧)
162
+ labelGroup.each(function(d) {
163
+ const g = d3.select(this);
164
+ const categoryName = d.data.name;
165
+
166
+ // 获取图标
167
+ if (images.field && images.field[categoryName]) {
168
+ const textWidth = measureCategoryLabelWidth(g.select(".category-label").text());
169
+
170
+ // 添加白色填充的圆形背景
171
+ g.append("circle")
172
+ .attr("cx", textWidth + 30) // 放在文本右侧,向右移动10px
173
+ .attr("cy", 10) // 向下移动10px
174
+ .attr("r", 22) // 半径增加到22 (17+5)
175
+ .attr("fill", "#ffffff") // 纯白色填充
176
+ .attr("fill-opacity", 0.75) // 75%透明度
177
+ .attr("stroke", "none"); // 移除描边
178
+
179
+ g.append("image")
180
+ .attr("x", textWidth + 14) // 放在文本右侧,向右移动10px
181
+ .attr("y", -6) // 调整y位置,向下移动10px
182
+ .attr("width", 32)
183
+ .attr("height", 32)
184
+ .attr("xlink:href", images.field[categoryName]);
185
+ }
186
+ });
187
+
188
+ // 添加值标签 (data label) - 放在x label下方,使用黑色,添加单位
189
+ labelGroup.append("text")
190
+ .attr("x", 0)
191
+ .attr("y", 42) // 放在x label下方
192
+ .attr("fill", "#000000") // 使用黑色
193
+ .attr("font-size", "16px") // 字体变大
194
+ .text(d => `${format(d.value)}${valueUnit}`) // 添加单位
195
+ .each(function(d) {
196
+ // 检查文本是否适合矩形
197
+ const textWidth = measureValueLabelWidth(`${format(d.value)}${valueUnit}`);
198
+ const rectWidth = d.x1 - d.x0;
199
+
200
+ if (textWidth > rectWidth - 24 || (d.y1 - d.y0) < 70) {
201
+ // 如果文本太长或矩形太小,隐藏它
202
+ d3.select(this).style("display", "none");
203
+ }
204
+ });
205
+
206
+ return svg.node();
207
+ }
modules/chart_engine/template/d3-js/treemap/treemap_03.js ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Treemap",
5
+ "chart_name": "treemap_03",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 20], [0, "inf"]],
9
+ "required_fields_icons": ["x"],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 600,
16
+ "background": "light",
17
+ "icon_mark": "overlay",
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
+ const valueUnit = chartUtils.schema.column(dataColumns, 1).unit;
44
+
45
+ // 设置尺寸和边距
46
+ const width = variables.width;
47
+ const height = variables.height;
48
+ const margin = { top: 10, right: 10, bottom: 10, left: 10 };
49
+
50
+ // 创建SVG
51
+ const svg = d3.select(containerSelector)
52
+ .append("svg")
53
+ .attr("width", "100%")
54
+ .attr("height", height)
55
+ .attr("viewBox", `0 0 ${width} ${height}`)
56
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
57
+ .attr("xmlns", "http://www.w3.org/2000/svg")
58
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
59
+
60
+ // 创建图表区域
61
+ const chartWidth = width - margin.left - margin.right;
62
+ const chartHeight = height - margin.top - margin.bottom;
63
+
64
+ const g = svg.append("g")
65
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
66
+
67
+ // 准备层次结构数据
68
+ // 将扁平数据转换为层次结构
69
+ const hierarchyData = {
70
+ name: "root",
71
+ children: []
72
+ };
73
+
74
+ // 按类别分组数据
75
+ const groupedData = d3.group(chartData, d => d[categoryField]);
76
+
77
+ // 将分组数据转换为层次结构
78
+ groupedData.forEach((values, category) => {
79
+ // 计算该类别的总值
80
+ const total = d3.sum(values, d => +d[valueField]);
81
+
82
+ hierarchyData.children.push({
83
+ name: category,
84
+ value: total
85
+ });
86
+ });
87
+
88
+ // 创建颜色比例尺
89
+ const colorScale = d => colorResolver.field(d, hierarchyData.children.findIndex(item => item.name === d), { palette: "tableau10" }).value;
90
+
91
+ // 创建渐变定义
92
+ const defs = svg.append("defs");
93
+
94
+ // 为每个类别创建一个金属光泽渐变
95
+ hierarchyData.children.forEach((category, i) => {
96
+ const baseColor = colorScale(category.name);
97
+ const gradientId = `metallic-gradient-${i}`;
98
+
99
+ // 创建线性渐变
100
+ const gradient = defs.append("linearGradient")
101
+ .attr("id", gradientId)
102
+ .attr("x1", "0%")
103
+ .attr("y1", "0%")
104
+ .attr("x2", "100%")
105
+ .attr("y2", "100%");
106
+
107
+ // 金属光泽效果的渐变停止点
108
+ gradient.append("stop")
109
+ .attr("offset", "0%")
110
+ .attr("stop-color", colorResolver.variant(baseColor, { mode: "brighter", amount: 1.5 }))
111
+ .attr("stop-opacity", 0.8);
112
+
113
+ gradient.append("stop")
114
+ .attr("offset", "45%")
115
+ .attr("stop-color", baseColor)
116
+ .attr("stop-opacity", 0.9);
117
+
118
+ gradient.append("stop")
119
+ .attr("offset", "55%")
120
+ .attr("stop-color", baseColor)
121
+ .attr("stop-opacity", 0.9);
122
+
123
+ gradient.append("stop")
124
+ .attr("offset", "100%")
125
+ .attr("stop-color", colorResolver.variant(baseColor, { mode: "darker", amount: 1.2 }))
126
+ .attr("stop-opacity", 0.8);
127
+ });
128
+
129
+ // 计算树图布局
130
+ const root = d3.treemap()
131
+ .size([chartWidth, chartHeight])
132
+ .padding(5)
133
+ .round(true)
134
+ (d3.hierarchy(hierarchyData)
135
+ .sum(d => d.value)
136
+ .sort((a, b) => b.value - a.value));
137
+
138
+ // 为每个叶子节点创建一个单元格
139
+ const leaf = g.selectAll("g")
140
+ .data(root.leaves())
141
+ .join("g")
142
+ .attr("transform", d => `translate(${d.x0},${d.y0})`);
143
+
144
+ // 添加矩形
145
+ leaf.append("rect")
146
+ .attr("width", d => Math.max(0, d.x1 - d.x0))
147
+ .attr("height", d => Math.max(0, d.y1 - d.y0))
148
+ .attr("fill", d => {
149
+ // 获���类别名称
150
+ const category = d.data.name;
151
+ const index = hierarchyData.children.findIndex(item => item.name === category);
152
+ return `url(#metallic-gradient-${index})`;
153
+ })
154
+ .attr("stroke", "none");
155
+
156
+ // 为每个矩形添加工具提示
157
+ const format = value => chartUtils.format.autoText(+value);
158
+ leaf.append("title")
159
+ .text(d => `${d.data.name}: ${format(d.value)}`);
160
+
161
+ // 创建包含标签和图标的组
162
+ const labelGroup = leaf.append("g")
163
+ .attr("transform", "translate(12, 12)"); // 增加边距
164
+
165
+ // 添加类别标签 (x label) - 变大并放在左上角,使用白色
166
+ labelGroup.append("text")
167
+ .attr("class", "category-label")
168
+ .attr("x", 0)
169
+ .attr("y", 18)
170
+ .attr("fill", "#ffffff")
171
+ .attr("font-size", "20px") // 字体变得更大
172
+ .attr("font-weight", "bold") // 使用粗体
173
+ .text(d => d.data.name)
174
+ .each(function(d) {
175
+ // 检查文本是否适合矩形(考虑图标宽度)
176
+ const rectWidth = d.x1 - d.x0 - 48; // 减去图标宽度和更多间距
177
+ const maxTextWidth = rectWidth - 12;
178
+ const measureLabelWidth = value => chartUtils.text.measure(null, value, {
179
+ fontSize: 20,
180
+ fontWeight: "bold"
181
+ }).width;
182
+
183
+ // 如果文本太长,截断它
184
+ const text = d3.select(this);
185
+ let textContent = text.text();
186
+ let displayText = textContent;
187
+ while (measureLabelWidth(displayText) > maxTextWidth && textContent.length > 0) {
188
+ textContent = textContent.slice(0, -1);
189
+ displayText = textContent + "...";
190
+ text.text(displayText);
191
+ }
192
+ });
193
+
194
+ // 添加图标(在矩形中央)
195
+ leaf.each(function(d) {
196
+ const g = d3.select(this);
197
+ const categoryName = d.data.name;
198
+ const boxWidth = d.x1 - d.x0;
199
+ const boxHeight = d.y1 - d.y0;
200
+
201
+ // 获取图标
202
+ if (images.field && images.field[categoryName]) {
203
+ const iconSize = 48; // 增大图标尺寸
204
+
205
+ // 添加白色填充的圆形背景
206
+ g.append("circle")
207
+ .attr("cx", boxWidth / 2)
208
+ .attr("cy", boxHeight / 2)
209
+ .attr("r", iconSize / 2 + 5) // 半径为图标尺寸的一半加上边距
210
+ .attr("fill", "#ffffff")
211
+ .attr("fill-opacity", 0.75)
212
+ .attr("stroke", "none");
213
+
214
+ g.append("image")
215
+ .attr("x", (boxWidth - iconSize) / 2)
216
+ .attr("y", (boxHeight - iconSize) / 2)
217
+ .attr("width", iconSize)
218
+ .attr("height", iconSize)
219
+ .attr("xlink:href", images.field[categoryName]);
220
+ }
221
+ });
222
+
223
+ // 添加值标签 (data label) - 放在矩形下方
224
+ leaf.append("text")
225
+ .attr("x", d => (d.x1 - d.x0) / 2)
226
+ .attr("y", d => (d.y1 - d.y0) - 15) // 放在矩形底部上方15px位置
227
+ .attr("text-anchor", "middle")
228
+ .attr("fill", "#ffffff")
229
+ .each(function(d) {
230
+ const rectWidth = d.x1 - d.x0;
231
+ // 根据矩形宽度调整字体大小
232
+ let fontSize = 16;
233
+ if (rectWidth < 120) {
234
+ fontSize = 12;
235
+ } else if (rectWidth < 80) {
236
+ fontSize = 10;
237
+ }
238
+ d3.select(this)
239
+ .attr("font-size", `${fontSize}px`)
240
+ .attr("font-weight", "bold")
241
+ .text(`${format(d.value)}${valueUnit}`);
242
+
243
+ // 检查文本是否适合矩形
244
+ const textWidth = chartUtils.text.measure(null, `${format(d.value)}${valueUnit}`, {
245
+ fontSize,
246
+ fontWeight: "bold"
247
+ }).width;
248
+ if (textWidth > rectWidth - 20 || (d.y1 - d.y0) < 70) {
249
+ // 如果文本太长或矩形太小,隐藏它
250
+ d3.select(this).style("display", "none");
251
+ }
252
+ });
253
+
254
+ return svg.node();
255
+ }
modules/chart_engine/template/d3-js/treemap/treemap_04.js ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Treemap",
5
+ "chart_name": "treemap_04",
6
+ "required_fields": ["x", "y", "group"],
7
+ "required_fields_type": [["categorical"], ["numerical"], ["categorical"]],
8
+ "required_fields_range": [[3, 20], [0, "inf"], [2, 6]],
9
+ "required_fields_icons": ["x"],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["group"],
12
+ "hierarchy": ["group"],
13
+ "required_other_colors": [],
14
+ "supported_effects": [],
15
+ "min_height": 600,
16
+ "min_width": 800,
17
+ "background": "light",
18
+ "icon_mark": "overlay",
19
+ "icon_label": "none",
20
+ "has_x_axis": "no",
21
+ "has_y_axis": "no"
22
+ }
23
+ REQUIREMENTS_END
24
+ */
25
+
26
+ function makeChart(containerSelector, 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 || {};
33
+ const colorResolver = chartUtils.color.resolver(jsonData);
34
+ const dataColumns = chartUtils.schema.columns(jsonData);
35
+ const images = jsonData.images || {};
36
+
37
+ // 清空容器
38
+ d3.select(containerSelector).html("");
39
+
40
+ // 获取字段名
41
+ const categoryField = chartUtils.schema.columnField(dataColumns, 0);
42
+ const valueField = chartUtils.schema.columnField(dataColumns, 1);
43
+ const groupField = chartUtils.schema.columnField(dataColumns, 2); // 获取group字段
44
+ // 获取单位
45
+ const valueUnit = chartUtils.schema.column(dataColumns, 1).unit;
46
+
47
+ // 设置尺寸和边距
48
+ const width = variables.width;
49
+ const height = variables.height;
50
+ const margin = { top: 10, right: 10, bottom: 50, left: 10 }; // 增加底部边距为图例留出空间
51
+
52
+ // 创建SVG
53
+ const svg = d3.select(containerSelector)
54
+ .append("svg")
55
+ .attr("width", "100%")
56
+ .attr("height", height)
57
+ .attr("viewBox", `0 0 ${width} ${height}`)
58
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
59
+ .attr("xmlns", "http://www.w3.org/2000/svg")
60
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
61
+
62
+ // 创建图表区域
63
+ const chartWidth = width - margin.left - margin.right;
64
+ const chartHeight = height - margin.top - margin.bottom;
65
+
66
+ const g = svg.append("g")
67
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
68
+
69
+ // 准备层次结构数据
70
+ // 将扁平数据转换为层次结构
71
+ const hierarchyData = {
72
+ name: "root",
73
+ children: []
74
+ };
75
+
76
+ // 按组别分组数据
77
+ const groupedData = d3.group(chartData, d => d[groupField]);
78
+
79
+ // 获取所有唯一的组
80
+ const groups = Array.from(groupedData.keys());
81
+
82
+ // 将分组数据转换为层次结构
83
+ groupedData.forEach((values, group) => {
84
+ // 按类别再次分组
85
+ const categoryGroups = d3.group(values, d => d[categoryField]);
86
+
87
+ const children = [];
88
+ categoryGroups.forEach((catValues, category) => {
89
+ // 计算该类别的总值
90
+ const total = d3.sum(catValues, d => +d[valueField]);
91
+
92
+ children.push({
93
+ name: category,
94
+ group: group,
95
+ value: total
96
+ });
97
+ });
98
+
99
+ hierarchyData.children.push({
100
+ name: group,
101
+ children: children
102
+ });
103
+ });
104
+
105
+ // 创建颜色比例尺
106
+ const colorScale = d => colorResolver.field(d, groups.indexOf(d), { palette: "tableau10" }).value;
107
+
108
+ // 计算树图布局
109
+ const root = d3.treemap()
110
+ .size([chartWidth, chartHeight])
111
+ .padding(5)
112
+ .round(true)
113
+ .tile(d3.treemapSquarify.ratio(1.6)) // 直接在squarify上设置比例
114
+ (d3.hierarchy(hierarchyData)
115
+ .sum(d => d.value)
116
+ .sort((a, b) => b.value - a.value));
117
+
118
+ // 为每个叶子节点创建一个单元格
119
+ const leaf = g.selectAll("g.leaf")
120
+ .data(root.leaves())
121
+ .join("g")
122
+ .attr("class", "leaf")
123
+ .attr("transform", d => `translate(${d.x0},${d.y0})`);
124
+
125
+ // 添加矩形
126
+ leaf.append("rect")
127
+ .attr("width", d => Math.max(0, d.x1 - d.x0))
128
+ .attr("height", d => Math.max(0, d.y1 - d.y0))
129
+ .attr("rx", 8)
130
+ .attr("ry", 8)
131
+ .attr("fill", d => {
132
+ // 获取组名称
133
+ const group = d.data.group;
134
+ return colorScale(group);
135
+ })
136
+ .attr("fill-opacity", 0.8)
137
+ .attr("stroke", "none");
138
+
139
+ // 为每个矩形添加工具提示
140
+ const format = value => chartUtils.format.autoText(+value);
141
+ leaf.append("title")
142
+ .text(d => `${d.data.name} (${d.data.group}): ${format(d.value)}`);
143
+
144
+ // 创建包含标签和图标的组
145
+ const labelGroup = leaf.append("g")
146
+ .attr("transform", "translate(12, 12)"); // 增加边距
147
+ const measureCategoryLabelWidth = value => chartUtils.text.measure(null, value, {
148
+ fontSize: 16,
149
+ fontWeight: "bold"
150
+ }).width;
151
+ const measureValueLabelWidth = value => chartUtils.text.measure(null, value, {
152
+ fontSize: 16
153
+ }).width;
154
+
155
+ // 添加类别标签 (x label) - 变大并放在左上角,使用白色
156
+ labelGroup.append("text")
157
+ .attr("class", "category-label")
158
+ .attr("x", 0)
159
+ .attr("y", 18)
160
+ .attr("fill", "#ffffff")
161
+ .attr("font-size", "16px") // 减小字体大小
162
+ .attr("font-weight", "bold") // 使用粗体
163
+ .text(d => d.data.name)
164
+ .each(function(d) {
165
+ const rectWidth = d.x1 - d.x0;
166
+ const rectHeight = d.y1 - d.y0;
167
+ const text = d3.select(this);
168
+ const textWidth = measureCategoryLabelWidth(text.text());
169
+
170
+ // 检查矩形是否过窄,但高度足够
171
+ if (rectWidth < 70 && rectHeight > 120 && textWidth > rectWidth - 12) {
172
+ // 对于窄而高的矩形,使用垂直文本
173
+ text.attr("transform", "rotate(90)")
174
+ .attr("x", 18) // 现在x成为垂直位置
175
+ .attr("y", -5); // 现在y成为水平位置(负值向左移动)
176
+
177
+ // 重新检查文本是否适合高度
178
+ if (textWidth > rectHeight - 20) {
179
+ // 如果文本太长,截断它
180
+ let textContent = text.text();
181
+ let displayText = textContent;
182
+ while (measureCategoryLabelWidth(displayText) > rectHeight - 20 && textContent.length > 0) {
183
+ textContent = textContent.slice(0, -1);
184
+ displayText = textContent + "...";
185
+ text.text(displayText);
186
+ }
187
+ }
188
+ } else if (textWidth > rectWidth - 12) {
189
+ // 如果文本太长,截断它
190
+ let textContent = text.text();
191
+ let displayText = textContent;
192
+ while (measureCategoryLabelWidth(displayText) > rectWidth - 12 && textContent.length > 0) {
193
+ textContent = textContent.slice(0, -1);
194
+ displayText = textContent + "...";
195
+ text.text(displayText);
196
+ }
197
+ }
198
+ });
199
+
200
+ // 添加图标(在x label右侧)
201
+ labelGroup.each(function(d) {
202
+ const g = d3.select(this);
203
+ const categoryName = d.data.name;
204
+ const rectWidth = d.x1 - d.x0;
205
+ const rectHeight = d.y1 - d.y0;
206
+ const textElement = g.select(".category-label").node();
207
+ const isVertical = textElement && textElement.getAttribute("transform") && textElement.getAttribute("transform").includes("rotate(90)");
208
+
209
+ // 获取图标,只在矩形足够宽或足够高且文本为垂直时显示
210
+ if (images.field && images.field[categoryName] &&
211
+ ((rectWidth > 100 && !isVertical) || (isVertical && rectHeight > 120))) {
212
+ const textWidth = textElement ? measureCategoryLabelWidth(d3.select(textElement).text()) : 0;
213
+
214
+ if (isVertical) {
215
+ // 垂直布局时,图标放在下方
216
+ g.append("circle")
217
+ .attr("cx", 18) // 与垂直文本同样的x值
218
+ .attr("cy", textWidth + 20) // 文本高度 + 一些间距
219
+ .attr("r", 16)
220
+ .attr("fill", "#ffffff")
221
+ .attr("fill-opacity", 0.75);
222
+
223
+ g.append("image")
224
+ .attr("x", 2) // 居中放置
225
+ .attr("y", textWidth + 4) // 文本高度 + 一些间距
226
+ .attr("width", 32)
227
+ .attr("height", 32)
228
+ .attr("xlink:href", images.field[categoryName]);
229
+ } else {
230
+ // 水平布局
231
+ g.append("circle")
232
+ .attr("cx", textWidth + 30)
233
+ .attr("cy", 10)
234
+ .attr("r", 22)
235
+ .attr("fill", "#ffffff")
236
+ .attr("fill-opacity", 0.75)
237
+ .attr("stroke", "none");
238
+
239
+ g.append("image")
240
+ .attr("x", textWidth + 14)
241
+ .attr("y", -6)
242
+ .attr("width", 32)
243
+ .attr("height", 32)
244
+ .attr("xlink:href", images.field[categoryName]);
245
+ }
246
+ }
247
+ });
248
+
249
+ // 添加值标签 (data label) - 放在x label下方,使用黑色,添加单位
250
+ labelGroup.append("text")
251
+ .attr("x", 0)
252
+ .attr("y", 42) // 放在x label下方
253
+ .attr("fill", "#000000") // 使用黑色
254
+ .attr("font-size", "16px") // 字体变大
255
+ .text(d => `${format(d.value)}${valueUnit}`) // 添加单位
256
+ .each(function(d) {
257
+ // 检查文本是否适合矩形
258
+ const textWidth = measureValueLabelWidth(`${format(d.value)}${valueUnit}`);
259
+ const rectWidth = d.x1 - d.x0;
260
+ const parentGroup = d3.select(this.parentNode);
261
+ const categoryLabel = parentGroup.select(".category-label").node();
262
+ const isVertical = categoryLabel && categoryLabel.getAttribute("transform") && categoryLabel.getAttribute("transform").includes("rotate(90)");
263
+
264
+ // 如果类别标签是垂直的,值标签也应该调整
265
+ if (isVertical) {
266
+ d3.select(this)
267
+ .attr("transform", "rotate(90)")
268
+ .attr("x", 50) // 在垂直类别标签下方
269
+ .attr("y", -5); // 相同的水平位置
270
+
271
+ if (textWidth > d.y1 - d.y0 - 70 || rectWidth < 50) {
272
+ // 如果文本太长或矩形太窄,隐藏它
273
+ d3.select(this).style("display", "none");
274
+ }
275
+ } else if (textWidth > rectWidth - 24 || (d.y1 - d.y0) < 70) {
276
+ // 如果文本太长或矩形太小,隐藏它
277
+ d3.select(this).style("display", "none");
278
+ }
279
+ });
280
+
281
+ chartUtils.legend.draw(svg, groups, {
282
+ color: d => colorScale(d),
283
+ markerShape: "rect",
284
+ markerWidth: 20,
285
+ markerHeight: 20,
286
+ markerSize: 20,
287
+ markerRadius: 4,
288
+ markerOpacity: 0.8,
289
+ labelGap: 5,
290
+ itemGap: 18,
291
+ rowGap: 8,
292
+ itemHeight: 20,
293
+ itemPaddingEnd: 0,
294
+ maxWidth: chartWidth,
295
+ align: "center",
296
+ x: margin.left,
297
+ y: height - margin.bottom + 10,
298
+ fontSize: 14,
299
+ textColor: "#333333",
300
+ className: "legend",
301
+ });
302
+
303
+ return svg.node();
304
+ }
modules/chart_engine/template/d3-js/treemap/treemap_05.js ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Treemap",
5
+ "chart_name": "treemap_05_hand",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 20], [0, "inf"]],
9
+ "required_fields_icons": ["x"],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 600,
16
+ "background": "light",
17
+ "icon_mark": "overlay",
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
+ const valueUnit = chartUtils.schema.column(dataColumns, 1).unit;
44
+
45
+ // 设置尺寸和边距
46
+ const width = variables.width;
47
+ const height = variables.height;
48
+ const margin = { top: 10, right: 10, bottom: 10, left: 10 };
49
+
50
+ // 创建SVG
51
+ const svg = d3.select(containerSelector)
52
+ .append("svg")
53
+ .attr("width", "100%")
54
+ .attr("height", height)
55
+ .attr("viewBox", `0 0 ${width} ${height}`)
56
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
57
+ .attr("xmlns", "http://www.w3.org/2000/svg")
58
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
59
+
60
+ // 创建图表区域
61
+ const chartWidth = width - margin.left - margin.right;
62
+ const chartHeight = height - margin.top - margin.bottom;
63
+
64
+ const g = svg.append("g")
65
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
66
+
67
+ // 准备层次结构数据
68
+ // 将扁平数据转换为层次结构
69
+ const hierarchyData = {
70
+ name: "root",
71
+ children: []
72
+ };
73
+
74
+ // 按类别分组数据
75
+ const groupedData = d3.group(chartData, d => d[categoryField]);
76
+
77
+ // 将分组数据转换为层次结构
78
+ groupedData.forEach((values, category) => {
79
+ // 计算该类别的总值
80
+ const total = d3.sum(values, d => +d[valueField]);
81
+
82
+ hierarchyData.children.push({
83
+ name: category,
84
+ value: total
85
+ });
86
+ });
87
+
88
+ // 创建颜色比例尺
89
+ const colorScale = d => colorResolver.field(d, hierarchyData.children.findIndex(item => item.name === d), { palette: "tableau10" }).value;
90
+
91
+ // 计算树图布局
92
+ const root = d3.treemap()
93
+ .size([chartWidth, chartHeight])
94
+ .padding(12)
95
+ .round(true)
96
+ (d3.hierarchy(hierarchyData)
97
+ .sum(d => d.value)
98
+ .sort((a, b) => b.value - a.value));
99
+
100
+ // 为每个叶子节点创建一个单元格
101
+ const leaf = g.selectAll("g")
102
+ .data(root.leaves())
103
+ .join("g")
104
+ .attr("transform", d => `translate(${d.x0},${d.y0})`);
105
+
106
+ // 添加矩形
107
+ leaf.append("rect")
108
+ .attr("width", d => Math.max(0, d.x1 - d.x0))
109
+ .attr("height", d => Math.max(0, d.y1 - d.y0))
110
+ .attr("rx", 3)
111
+ .attr("ry", 3)
112
+ .attr("fill", d => {
113
+ // 获取类别名称
114
+ const category = d.data.name;
115
+ return colorScale(category);
116
+ })
117
+ .attr("fill-opacity", 0.8)
118
+ .attr("stroke", "none");
119
+
120
+ // 为每个矩形添加工具提示
121
+ const format = value => chartUtils.format.autoText(+value);
122
+ leaf.append("title")
123
+ .text(d => `${d.data.name}: ${format(d.value)}`);
124
+
125
+ // 创建包含标签和图标的组
126
+ const labelGroup = leaf.append("g")
127
+ .attr("transform", "translate(12, 12)"); // 增加边距
128
+ const measureCategoryLabelWidth = value => chartUtils.text.measure(null, value, {
129
+ fontSize: 20,
130
+ fontWeight: "bold"
131
+ }).width;
132
+ const measureValueLabelWidth = value => chartUtils.text.measure(null, value, {
133
+ fontSize: 16
134
+ }).width;
135
+
136
+ // 添加类别标签 (x label) - 变大并放在左上角,使用白色
137
+ labelGroup.append("text")
138
+ .attr("class", "category-label")
139
+ .attr("x", 0)
140
+ .attr("y", 18)
141
+ .attr("fill", "#ffffff")
142
+ .attr("font-size", "20px") // 字体变得更大
143
+ .attr("font-weight", "bold") // 使用粗体
144
+ .text(d => d.data.name)
145
+ .each(function(d) {
146
+ // 检查文本是否适合矩形(考虑图标宽度)
147
+ const rectWidth = d.x1 - d.x0 - 48; // 减去图标宽度和更多间距
148
+ const maxTextWidth = rectWidth - 12;
149
+
150
+ // 如果文本太长,截断它
151
+ const text = d3.select(this);
152
+ let textContent = text.text();
153
+ let displayText = textContent;
154
+ while (measureCategoryLabelWidth(displayText) > maxTextWidth && textContent.length > 0) {
155
+ textContent = textContent.slice(0, -1);
156
+ displayText = textContent + "...";
157
+ text.text(displayText);
158
+ }
159
+ });
160
+
161
+ // 添加图标(在x label右侧)
162
+ labelGroup.each(function(d) {
163
+ const g = d3.select(this);
164
+ const categoryName = d.data.name;
165
+
166
+ // 获取图标
167
+ if (images.field && images.field[categoryName]) {
168
+ const textWidth = measureCategoryLabelWidth(g.select(".category-label").text());
169
+
170
+ // 添加白色填充的圆形背景
171
+ g.append("circle")
172
+ .attr("cx", textWidth + 30) // 放在文本右侧,向右移动10px
173
+ .attr("cy", 10) // 向下移动10px
174
+ .attr("r", 22) // 半径增加到22 (17+5)
175
+ .attr("fill", "#ffffff") // 纯白色填充
176
+ .attr("fill-opacity", 0.75) // 75%透明度
177
+ .attr("stroke", "none"); // 移除描边
178
+
179
+ g.append("image")
180
+ .attr("x", textWidth + 14) // 放在文本右侧,向右移动10px
181
+ .attr("y", -6) // 调整y位置,向下移动10px
182
+ .attr("width", 32)
183
+ .attr("height", 32)
184
+ .attr("xlink:href", images.field[categoryName]);
185
+ }
186
+ });
187
+
188
+ // 添加值标签 (data label) - 放在x label下方,使用黑色,添加单位
189
+ labelGroup.append("text")
190
+ .attr("x", 0)
191
+ .attr("y", 42) // 放在x label下方
192
+ .attr("fill", "#000000") // 使用黑色
193
+ .attr("font-size", "16px") // 字体变大
194
+ .text(d => `${format(d.value)}${valueUnit}`) // 添加单位
195
+ .each(function(d) {
196
+ // 检查文本是否适合矩形
197
+ const textWidth = measureValueLabelWidth(`${format(d.value)}${valueUnit}`);
198
+ const rectWidth = d.x1 - d.x0;
199
+
200
+ if (textWidth > rectWidth - 24 || (d.y1 - d.y0) < 70) {
201
+ // 如果文本太长或矩形太小,隐藏它
202
+ d3.select(this).style("display", "none");
203
+ }
204
+ });
205
+
206
+ const roughness = 1;
207
+ const bowing = 2;
208
+ const fillStyle = "solid";
209
+ const randomize = false;
210
+ const pencilFilter = false;
211
+
212
+ const svgConverter = new svg2roughjs.Svg2Roughjs(containerSelector);
213
+ svgConverter.pencilFilter = pencilFilter;
214
+ svgConverter.randomize = randomize;
215
+ svgConverter.svg = svg.node();
216
+ svgConverter.roughConfig = {
217
+ bowing,
218
+ roughness,
219
+ fillStyle
220
+ };
221
+ svgConverter.sketch();
222
+ // Remove the first SVG element if it exists
223
+ const firstSvg = document.querySelector(`${containerSelector} svg`);
224
+ if (firstSvg) {
225
+ firstSvg.remove();
226
+ }
227
+
228
+ return svg.node();
229
+ }
modules/chart_engine/template/d3-js/treemap/treemap_05_dark.js ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Treemap",
5
+ "chart_name": "treemap_05_dark",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 20], [0, "inf"]],
9
+ "required_fields_icons": ["x"],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 600,
16
+ "background": "dark",
17
+ "icon_mark": "overlay",
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
+ const valueUnit = chartUtils.schema.column(dataColumns, 1).unit;
47
+
48
+ // 设置尺寸和边距
49
+ const width = variables.width;
50
+ const height = variables.height;
51
+ const margin = { top: 10, right: 10, bottom: 10, left: 10 };
52
+
53
+ // 创建SVG
54
+ const svg = d3.select(containerSelector)
55
+ .append("svg")
56
+ .attr("width", "100%")
57
+ .attr("height", height)
58
+ .attr("viewBox", `0 0 ${width} ${height}`)
59
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
60
+ .attr("xmlns", "http://www.w3.org/2000/svg")
61
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
62
+
63
+ // 创建图表区域
64
+ const chartWidth = width - margin.left - margin.right;
65
+ const chartHeight = height - margin.top - margin.bottom;
66
+
67
+ const g = svg.append("g")
68
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
69
+
70
+ // 准备层次结构数据
71
+ // 将扁平数据转换为层次结构
72
+ const hierarchyData = {
73
+ name: "root",
74
+ children: []
75
+ };
76
+
77
+ // 按类别分组数据
78
+ const groupedData = d3.group(chartData, d => d[categoryField]);
79
+
80
+ // 将分组数据转换为层次结构
81
+ groupedData.forEach((values, category) => {
82
+ // 计算该类别的总值
83
+ const total = d3.sum(values, d => +d[valueField]);
84
+
85
+ hierarchyData.children.push({
86
+ name: category,
87
+ value: total
88
+ });
89
+ });
90
+
91
+ // 创建颜色比例尺
92
+ const colorScale = d => colorResolver.field(d, hierarchyData.children.findIndex(item => item.name === d), { palette: "tableau10" }).value;
93
+
94
+ // 计算树图布局
95
+ const root = d3.treemap()
96
+ .size([chartWidth, chartHeight])
97
+ .padding(12)
98
+ .round(true)
99
+ (d3.hierarchy(hierarchyData)
100
+ .sum(d => d.value)
101
+ .sort((a, b) => b.value - a.value));
102
+
103
+ // 为每个叶子节点创建一个单元格
104
+ const leaf = g.selectAll("g")
105
+ .data(root.leaves())
106
+ .join("g")
107
+ .attr("transform", d => `translate(${d.x0},${d.y0})`);
108
+
109
+ // 添加矩形
110
+ leaf.append("rect")
111
+ .attr("width", d => Math.max(0, d.x1 - d.x0))
112
+ .attr("height", d => Math.max(0, d.y1 - d.y0))
113
+ .attr("rx", 3)
114
+ .attr("ry", 3)
115
+ .attr("fill", d => {
116
+ // 获取类别名称
117
+ const category = d.data.name;
118
+ return colorScale(category);
119
+ })
120
+ .attr("fill-opacity", 0.8)
121
+ .attr("stroke", "none");
122
+
123
+ // 为每个矩形添加工具提示
124
+ const format = value => chartUtils.format.autoText(+value);
125
+ leaf.append("title")
126
+ .text(d => `${d.data.name}: ${format(d.value)}`);
127
+
128
+ // 创建包含标签和图标的组
129
+ const labelGroup = leaf.append("g")
130
+ .attr("transform", "translate(12, 12)"); // 增加边距
131
+ const measureCategoryLabelWidth = value => chartUtils.text.measure(null, value, {
132
+ fontSize: 20,
133
+ fontWeight: "bold"
134
+ }).width;
135
+ const measureValueLabelWidth = value => chartUtils.text.measure(null, value, {
136
+ fontSize: 16
137
+ }).width;
138
+
139
+ // 添加类别标签 (x label) - 变大并放在左上角,使用白色
140
+ labelGroup.append("text")
141
+ .attr("class", "category-label")
142
+ .attr("x", 0)
143
+ .attr("y", 18)
144
+ .attr("fill", "#ffffff")
145
+ .attr("font-size", "20px") // 字体变得更大
146
+ .attr("font-weight", "bold") // 使用粗体
147
+ .text(d => d.data.name)
148
+ .each(function(d) {
149
+ // 检查文本是否适合矩形(考虑图标宽度)
150
+ const rectWidth = d.x1 - d.x0 - 48; // 减去图标宽度和更多间距
151
+ const maxTextWidth = rectWidth - 12;
152
+
153
+ // 如果文本太长,截断它
154
+ const text = d3.select(this);
155
+ let textContent = text.text();
156
+ let displayText = textContent;
157
+ while (measureCategoryLabelWidth(displayText) > maxTextWidth && textContent.length > 0) {
158
+ textContent = textContent.slice(0, -1);
159
+ displayText = textContent + "...";
160
+ text.text(displayText);
161
+ }
162
+ });
163
+
164
+ // 添加图标(在x label右侧)
165
+ labelGroup.each(function(d) {
166
+ const g = d3.select(this);
167
+ const categoryName = d.data.name;
168
+
169
+ // 获取图标
170
+ if (images.field && images.field[categoryName]) {
171
+ const textWidth = measureCategoryLabelWidth(g.select(".category-label").text());
172
+
173
+ // 添加白色填充的圆形背景
174
+ g.append("circle")
175
+ .attr("cx", textWidth + 30) // 放在文本右侧,向右移动10px
176
+ .attr("cy", 10) // 向下移动10px
177
+ .attr("r", 22) // 半径增加到22 (17+5)
178
+ .attr("fill", "#ffffff") // 纯白色填充
179
+ .attr("fill-opacity", 0.75) // 75%透明度
180
+ .attr("stroke", "none"); // 移除描边
181
+
182
+ g.append("image")
183
+ .attr("x", textWidth + 14) // 放在文本右侧,向右移动10px
184
+ .attr("y", -6) // 调整y位置,向下移动10px
185
+ .attr("width", 32)
186
+ .attr("height", 32)
187
+ .attr("xlink:href", images.field[categoryName]);
188
+ }
189
+ });
190
+
191
+ // 添加值标签 (data label) - 放在x label下方,使用黑色,添加单位
192
+ labelGroup.append("text")
193
+ .attr("x", 0)
194
+ .attr("y", 42) // 放在x label下方
195
+ .attr("fill", "#ffffff")
196
+ .attr("font-size", "16px") // 字体变大
197
+ .text(d => `${format(d.value)}${valueUnit}`) // 添加单位
198
+ .each(function(d) {
199
+ // 检查文本是否适合矩形
200
+ const textWidth = measureValueLabelWidth(`${format(d.value)}${valueUnit}`);
201
+ const rectWidth = d.x1 - d.x0;
202
+
203
+ if (textWidth > rectWidth - 24 || (d.y1 - d.y0) < 70) {
204
+ // 如果文本太长或矩形太小,隐藏它
205
+ d3.select(this).style("display", "none");
206
+ }
207
+ });
208
+
209
+ const roughness = 1;
210
+ const bowing = 2;
211
+ const fillStyle = "solid";
212
+ const randomize = false;
213
+ const pencilFilter = false;
214
+
215
+ const svgConverter = new svg2roughjs.Svg2Roughjs(containerSelector);
216
+ svgConverter.pencilFilter = pencilFilter;
217
+ svgConverter.randomize = randomize;
218
+ svgConverter.svg = svg.node();
219
+ svgConverter.roughConfig = {
220
+ bowing,
221
+ roughness,
222
+ fillStyle
223
+ };
224
+ svgConverter.sketch();
225
+ // Remove the first SVG element if it exists
226
+ const firstSvg = document.querySelector(`${containerSelector} svg`);
227
+ if (firstSvg) {
228
+ firstSvg.remove();
229
+ }
230
+
231
+ return svg.node();
232
+ }
modules/chart_engine/template/d3-js/treemap/treemap_06.js ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Treemap",
5
+ "chart_name": "treemap_06_hand",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 20], [0, "inf"]],
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": 600,
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
+
35
+ // 清空容器
36
+ d3.select(containerSelector).html("");
37
+
38
+ // 获取字段名
39
+ const categoryField = chartUtils.schema.columnField(dataColumns, 0);
40
+ const valueField = chartUtils.schema.columnField(dataColumns, 1);
41
+ // 获取单位
42
+ const valueUnit = chartUtils.schema.column(dataColumns, 1).unit;
43
+
44
+ // 设置尺寸和边距
45
+ const width = variables.width;
46
+ const height = variables.height;
47
+ const margin = { top: 10, right: 10, bottom: 10, left: 10 };
48
+
49
+ // 创建SVG
50
+ const svg = d3.select(containerSelector)
51
+ .append("svg")
52
+ .attr("width", "100%")
53
+ .attr("height", height)
54
+ .attr("viewBox", `0 0 ${width} ${height}`)
55
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
56
+ .attr("xmlns", "http://www.w3.org/2000/svg")
57
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
58
+
59
+ // 创建图表区域
60
+ const chartWidth = width - margin.left - margin.right;
61
+ const chartHeight = height - margin.top - margin.bottom;
62
+
63
+ const g = svg.append("g")
64
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
65
+
66
+ // 准备层次结构数据
67
+ // 将扁平数据转换为层次结构
68
+ const hierarchyData = {
69
+ name: "root",
70
+ children: []
71
+ };
72
+
73
+ // 按类别分组数据
74
+ const groupedData = d3.group(chartData, d => d[categoryField]);
75
+
76
+ // 将分组数据转换为层次结构
77
+ groupedData.forEach((values, category) => {
78
+ // 计算该类别的总值
79
+ const total = d3.sum(values, d => +d[valueField]);
80
+
81
+ hierarchyData.children.push({
82
+ name: category,
83
+ value: total
84
+ });
85
+ });
86
+
87
+ // 创建颜色比例尺
88
+ const colorScale = d => colorResolver.field(d, hierarchyData.children.findIndex(item => item.name === d), { palette: "tableau10" }).value;
89
+
90
+ // 计算树图布局
91
+ const root = d3.treemap()
92
+ .size([chartWidth, chartHeight])
93
+ .padding(3)
94
+ .round(true)
95
+ (d3.hierarchy(hierarchyData)
96
+ .sum(d => d.value)
97
+ .sort((a, b) => b.value - a.value));
98
+
99
+ // 为每个叶子节点创建一个单元格
100
+ const leaf = g.selectAll("g")
101
+ .data(root.leaves())
102
+ .join("g")
103
+ .attr("transform", d => `translate(${d.x0},${d.y0})`);
104
+
105
+ // 添加矩形
106
+ leaf.append("rect")
107
+ .attr("width", d => Math.max(0, d.x1 - d.x0))
108
+ .attr("height", d => Math.max(0, d.y1 - d.y0))
109
+ .attr("rx", 3)
110
+ .attr("ry", 3)
111
+ .attr("fill", d => {
112
+ // 获取类别名称
113
+ const category = d.data.name;
114
+ return colorScale(category);
115
+ })
116
+ .attr("fill-opacity", 0.8)
117
+ .attr("stroke", "none");
118
+
119
+ // 为每个矩形添加工具提示
120
+ const format = value => chartUtils.format.autoText(+value);
121
+ leaf.append("title")
122
+ .text(d => `${d.data.name}: ${format(d.value)}`);
123
+
124
+ // 创建包含标签的组
125
+ const labelGroup = leaf.append("g")
126
+ .attr("transform", "translate(12, 12)"); // 增加边距
127
+
128
+ // 计算每个矩形的大小,用于自适应标签显示
129
+ leaf.each(function(d) {
130
+ d.rectWidth = d.x1 - d.x0 - 24; // 可用宽度
131
+ d.rectHeight = d.y1 - d.y0 - 24; // 可用高度
132
+ });
133
+
134
+ // 添加类别标签背景 (x label背景)
135
+ labelGroup.append("rect")
136
+ .attr("class", "category-label-bg")
137
+ .attr("x", -6)
138
+ .attr("y", 0)
139
+ .attr("rx", 3)
140
+ .attr("ry", 3)
141
+ .attr("fill", "#ffffff")
142
+ .attr("fill-opacity", 0.75)
143
+ .attr("width", d => Math.min(d.rectWidth, 200)) // 限制最大宽度
144
+ .attr("height", d => {
145
+ // 根据矩形大小调整背景高度
146
+ const rectSize = Math.min(d.rectWidth, d.rectHeight);
147
+ if (rectSize < 50) return 20; // 非常小的矩形
148
+ if (rectSize < 80) return 24; // 较小的矩形
149
+ return 28; // 正常大小的矩形
150
+ });
151
+
152
+ // 计算适合的字体大小函数
153
+ function calculateFontSize(d) {
154
+ const rectSize = Math.min(d.rectWidth, d.rectHeight);
155
+ if (rectSize < 40) return 8; // 非常小的矩形
156
+ if (rectSize < 60) return 10; // 较小的矩形
157
+ if (rectSize < 80) return 12; // 中等大小的矩形
158
+ if (rectSize < 100) return 14; // 较大的矩形
159
+ if (rectSize < 150) return 16; // 大矩形
160
+ return 20; // 非常大的矩形
161
+ }
162
+
163
+ // 添加类别标签 (x label) - 自适应大小并放在左上角,使用黑色
164
+ labelGroup.append("text")
165
+ .attr("class", "category-label")
166
+ .attr("x", 0)
167
+ .attr("y", d => {
168
+ const fontSize = calculateFontSize(d);
169
+ return Math.min(18, fontSize + 4); // 根据字体大小调整y位置
170
+ })
171
+ .attr("fill", "#000000")
172
+ .attr("font-weight", "bold")
173
+ .each(function(d) {
174
+ const fontSize = calculateFontSize(d);
175
+ const measureCategoryText = value => chartUtils.text.measure(null, value, {
176
+ fontSize,
177
+ fontWeight: "bold"
178
+ });
179
+ d3.select(this).attr("font-size", `${fontSize}px`);
180
+
181
+ // 处理文本自适应显示
182
+ const text = d3.select(this);
183
+ const maxWidth = Math.max(30, d.rectWidth - 12);
184
+ const textContent = d.data.name;
185
+ d.categoryLabelHeight = measureCategoryText(textContent).height;
186
+
187
+ // 设置原始文本进行测量
188
+ text.text(textContent);
189
+
190
+ // 检查是否需要换行或截断
191
+ if (measureCategoryText(textContent).width > maxWidth) {
192
+ // 尝试分词并换行显示
193
+ const words = textContent.split(/\s+/);
194
+ // 如果只有一个词,则截断显示
195
+ if (words.length <= 1) {
196
+ let displayText = textContent;
197
+ let renderedText = displayText;
198
+ // 即使是非常小的矩形也尝试显示尽可能多的文本
199
+ while (measureCategoryText(renderedText).width > maxWidth && displayText.length > 1) {
200
+ displayText = displayText.slice(0, -1);
201
+ renderedText = displayText + "...";
202
+ text.text(renderedText);
203
+ }
204
+ } else {
205
+ // 多个词尝试换行显示
206
+ text.text(null); // 清空文本
207
+ let line = [];
208
+ let lineNumber = 0;
209
+ const lineHeight = fontSize * 1.1;
210
+ let tspan = text.append("tspan")
211
+ .attr("x", 0)
212
+ .attr("dy", 0);
213
+
214
+ // 一个一个词添加并检查是否需要换行
215
+ words.forEach((word, i) => {
216
+ line.push(word);
217
+ tspan.text(line.join(" "));
218
+
219
+ if (measureCategoryText(line.join(" ")).width > maxWidth) {
220
+ if (line.length === 1) {
221
+ // 单词太长,需要截断
222
+ let wordToFit = line[0];
223
+ let renderedWord = wordToFit;
224
+ tspan.text("");
225
+ while (wordToFit.length > 1) {
226
+ tspan.text(renderedWord);
227
+ if (measureCategoryText(renderedWord).width <= maxWidth) break;
228
+ wordToFit = wordToFit.slice(0, -1);
229
+ renderedWord = wordToFit + "...";
230
+ tspan.text(renderedWord);
231
+ }
232
+ } else {
233
+ // 回退一个词,换行
234
+ line.pop();
235
+ tspan.text(line.join(" "));
236
+ line = [word];
237
+ lineNumber++;
238
+
239
+ // 最多显示2行
240
+ if (lineNumber >= 2) {
241
+ tspan.text(tspan.text() + "...");
242
+ return;
243
+ }
244
+
245
+ tspan = text.append("tspan")
246
+ .attr("x", 0)
247
+ .attr("dy", lineHeight)
248
+ .text(word);
249
+ }
250
+ }
251
+ });
252
+
253
+ // 调整背景高度以适应多行文本
254
+ if (lineNumber > 0) {
255
+ const bgRect = d3.select(this.parentNode).select(".category-label-bg");
256
+ bgRect.attr("height", (lineNumber + 1) * lineHeight + 4);
257
+ d.categoryLabelHeight = (lineNumber + 1) * lineHeight;
258
+ }
259
+ }
260
+ }
261
+ });
262
+
263
+ // 添加值标签背景 (data label背景)
264
+ labelGroup.append("rect")
265
+ .attr("class", "value-label-bg")
266
+ .attr("x", -6)
267
+ .attr("y", function(d) {
268
+ // 计算y位置,考虑类别标签可能是多行的情况
269
+ const categoryHeight = d.categoryLabelHeight || chartUtils.text.measure(null, d.data.name, {
270
+ fontSize: calculateFontSize(d),
271
+ fontWeight: "bold"
272
+ }).height;
273
+ return categoryHeight + 6;
274
+ })
275
+ .attr("rx", 3)
276
+ .attr("ry", 3)
277
+ .attr("fill", "#ffffff")
278
+ .attr("fill-opacity", 0.75)
279
+ .each(function(d) {
280
+ // 确定值标签背景的宽度和高度
281
+ const fontSize = Math.max(8, calculateFontSize(d) - 4); // 值标签字体比类别标签小
282
+ const valueText = `${format(d.value)}${valueUnit}`;
283
+
284
+ const parentNode = d3.select(this.parentNode);
285
+ const textWidth = chartUtils.text.measure(parentNode, valueText, { fontSize }).width;
286
+
287
+ const labelWidth = Math.min(textWidth + 12, d.rectWidth);
288
+ d3.select(this)
289
+ .attr("width", labelWidth)
290
+ .attr("height", fontSize + 4);
291
+
292
+ // 如果矩形太小,隐藏值标签
293
+ if (d.rectHeight < 50) {
294
+ d3.select(this).style("display", "none");
295
+ }
296
+ });
297
+
298
+ // 添加值标签 (data label) - 放在x label下方,使用黑色,添加单位
299
+ labelGroup.append("text")
300
+ .attr("class", "value-label")
301
+ .attr("x", 0)
302
+ .attr("fill", "#000000")
303
+ .each(function(d) {
304
+ // 获取类别标签的高度,用于定位值标签
305
+ const categoryHeight = d.categoryLabelHeight || chartUtils.text.measure(null, d.data.name, {
306
+ fontSize: calculateFontSize(d),
307
+ fontWeight: "bold"
308
+ }).height;
309
+
310
+ // 设置字体大小和y位置
311
+ const fontSize = Math.max(8, calculateFontSize(d) - 4); // 值标签字体比类别标签小
312
+ d3.select(this)
313
+ .attr("font-size", `${fontSize}px`)
314
+ .attr("y", categoryHeight + fontSize + 6)
315
+ .text(`${format(d.value)}${valueUnit}`);
316
+
317
+ // 如果矩形太小,隐藏值标签
318
+ if (d.rectHeight < 50) {
319
+ d3.select(this).style("display", "none");
320
+ d3.select(this.parentNode).select(".value-label-bg").style("display", "none");
321
+ }
322
+ });
323
+
324
+ const roughness = 1;
325
+ const bowing = 2;
326
+ const fillStyle = "hachure";
327
+ const randomize = false;
328
+ const pencilFilter = false;
329
+
330
+ const svgConverter = new svg2roughjs.Svg2Roughjs(containerSelector);
331
+ svgConverter.pencilFilter = pencilFilter;
332
+ svgConverter.randomize = randomize;
333
+ svgConverter.svg = svg.node();
334
+ svgConverter.roughConfig = {
335
+ bowing,
336
+ roughness,
337
+ fillStyle
338
+ };
339
+ svgConverter.sketch();
340
+ // Remove the first SVG element if it exists
341
+ const firstSvg = document.querySelector(`${containerSelector} svg`);
342
+ if (firstSvg) {
343
+ firstSvg.remove();
344
+ }
345
+
346
+ return svg.node();
347
+ }
modules/chart_engine/template/d3-js/treemap/voronoi_treemap_circle_01.js ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Voronoi Treemap",
5
+ "chart_name": "voronoi_treemap_plain_chart_01",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[5, 40], [0, "inf"]],
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": 600,
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 width = variables.width;
45
+ const height = variables.height;
46
+ const margin = { top: 10, right: 10, bottom: 10, left: 10 };
47
+
48
+ // 创建SVG
49
+ const svg = d3.select(containerSelector)
50
+ .append("svg")
51
+ .attr("width", "100%")
52
+ .attr("height", height)
53
+ .attr("viewBox", `0 0 ${width} ${height}`)
54
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
55
+ .attr("xmlns", "http://www.w3.org/2000/svg")
56
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
57
+
58
+ // 创建图表区域
59
+ const chartWidth = width - margin.left - margin.right;
60
+ const chartHeight = height - margin.top - margin.bottom;
61
+
62
+ const g = svg.append("g")
63
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
64
+
65
+ // 准备数据
66
+ const processedData = chartData.map(d => ({
67
+ name: d[categoryField],
68
+ weight: d[valueField]
69
+ }));
70
+
71
+ // 创建颜色比例尺
72
+ const colorScale = d => {
73
+ if (colorResolver.field(d, 0, { fallbackKey: "primary" }).value) {
74
+ return colorResolver.field(d, 0, { fallbackKey: "primary" }).value;
75
+ }
76
+ const uniqueCategories = [...new Set(chartData.map(d => d[categoryField]))];
77
+ return chartUtils.color.palette(uniqueCategories.indexOf(d), { palette: "tableau10" });
78
+ };
79
+
80
+ // 计算圆形裁剪区域
81
+ const radius = Math.min(chartWidth, chartHeight) / 2;
82
+ const centerX = chartWidth / 2;
83
+ const centerY = chartHeight / 2;
84
+
85
+ // 创建圆形裁剪多边形 - 使用多边形近似圆形
86
+ const numPoints = 50;
87
+ const clip = [];
88
+ for (let i = 0; i < numPoints; i++) {
89
+ const angle = (i / numPoints) * 2 * Math.PI;
90
+ clip.push([
91
+ centerX + radius * Math.cos(angle),
92
+ centerY + radius * Math.sin(angle)
93
+ ]);
94
+ }
95
+
96
+ // 创建 Voronoi Map 模拟
97
+ const simulation = d3.voronoiMapSimulation(processedData)
98
+ .weight(d => d.weight)
99
+ .clip(clip)
100
+ .stop();
101
+
102
+ // 运行模拟直到结束 - 限制迭代次数以避免超时
103
+ let state = simulation.state();
104
+ let iterations = 0;
105
+ const maxIterations = 300; // 限制最大迭代次数
106
+
107
+ while (!state.ended && iterations < maxIterations) {
108
+ simulation.tick();
109
+ state = simulation.state();
110
+ iterations++;
111
+ }
112
+
113
+ // 获取最终的多边形
114
+ const polygons = state.polygons;
115
+
116
+ // 绘制多边形
117
+ const cells = g.selectAll("g.cell")
118
+ .data(polygons)
119
+ .enter()
120
+ .append("g")
121
+ .attr("class", "cell");
122
+
123
+ // 添加单元格
124
+ cells.append("path")
125
+ .attr("d", d => {
126
+ return "M" + d.join("L") + "Z";
127
+ })
128
+ .attr("fill", d => {
129
+ try {
130
+ return colorScale(d.site.originalObject.data.originalData.name);
131
+ } catch (e) {
132
+ console.error("Error accessing color data:", e);
133
+ return "#ccc"; // 默认颜色
134
+ }
135
+ })
136
+ .attr("fill-opacity", 0.8)
137
+ .attr("stroke", "none")
138
+
139
+ // 添加文本标签
140
+ cells.append("text")
141
+ .attr("x", d => {
142
+ try {
143
+ return d3.polygonCentroid(d)[0];
144
+ } catch (e) {
145
+ console.error("Error calculating centroid:", e);
146
+ return 0;
147
+ }
148
+ })
149
+ .attr("y", d => {
150
+ try {
151
+ return d3.polygonCentroid(d)[1];
152
+ } catch (e) {
153
+ console.error("Error calculating centroid:", e);
154
+ return 0;
155
+ }
156
+ })
157
+ .attr("text-anchor", "middle")
158
+ .attr("dominant-baseline", "middle")
159
+ .attr("fill", "#fff")
160
+ .attr("font-size", "16px")
161
+ .attr("font-weight", "bold")
162
+ .text(d => {
163
+ try {
164
+ return d.site.originalObject.data.originalData.name;
165
+ } catch (e) {
166
+ console.error("Error accessing name data:", e);
167
+ return "";
168
+ }
169
+ })
170
+ .each(function(d) {
171
+ try {
172
+ // 计算多边形的边界框
173
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
174
+ d.forEach(point => {
175
+ minX = Math.min(minX, point[0]);
176
+ minY = Math.min(minY, point[1]);
177
+ maxX = Math.max(maxX, point[0]);
178
+ maxY = Math.max(maxY, point[1]);
179
+ });
180
+
181
+ const boxWidth = maxX - minX;
182
+ const boxHeight = maxY - minY;
183
+
184
+ // 检查文本是否适合单元格
185
+ const textSelection = d3.select(this);
186
+ const textWidth = chartUtils.text.measure(null, this.textContent || "", {
187
+ fontFamily: "sans-serif",
188
+ fontSize: parseFloat(textSelection.attr("font-size")) || 12,
189
+ fontWeight: textSelection.attr("font-weight") || "normal"
190
+ }).width;
191
+
192
+ if (textWidth > boxWidth * 0.8 || boxHeight < 30) {
193
+ // 如果文本太长或单元格太小,缩小字体而不是隐藏
194
+ d3.select(this)
195
+ .attr("font-size", "10px")
196
+ .text(function() {
197
+ const origText = this.textContent;
198
+ // 如果文本超过10个字符且单元格很小,尝试换行
199
+ if (origText.length > 10 && boxHeight < 40) {
200
+ const midPoint = Math.floor(origText.length / 2);
201
+ // 找到最近的空格或标点符号
202
+ let breakPoint = midPoint;
203
+ const punctuation = [' ', ',', '。', '、', ',', '.'];
204
+ let minDistance = origText.length;
205
+
206
+ for (let i = 0; i < origText.length; i++) {
207
+ if (punctuation.includes(origText[i])) {
208
+ const distance = Math.abs(i - midPoint);
209
+ if (distance < minDistance) {
210
+ minDistance = distance;
211
+ breakPoint = i;
212
+ }
213
+ }
214
+ }
215
+
216
+ // 如果找到合适的断点就换行,否则直接在中间换行
217
+ if (punctuation.includes(origText[breakPoint])) {
218
+ return origText.substring(0, breakPoint + 1) + '\n' + origText.substring(breakPoint + 1);
219
+ } else {
220
+ return origText.substring(0, midPoint) + '\n' + origText.substring(midPoint);
221
+ }
222
+ }
223
+ return origText;
224
+ })
225
+ .attr("dy", function() {
226
+ // 如果文本包含换行符,调整垂直位置
227
+ return this.textContent.includes('\n') ? "-0.5em" : "0";
228
+ });
229
+
230
+ // 如果文本包含换行符,创建第二行
231
+ if (d3.select(this).text().includes('\n')) {
232
+ const lines = d3.select(this).text().split('\n');
233
+ d3.select(this).text(lines[0]);
234
+
235
+ // 添加第二行
236
+ g.append("text")
237
+ .attr("x", d3.polygonCentroid(d)[0])
238
+ .attr("y", d3.polygonCentroid(d)[1])
239
+ .attr("text-anchor", "middle")
240
+ .attr("dominant-baseline", "middle")
241
+ .attr("fill", "#fff")
242
+ .attr("font-size", "10px")
243
+ .attr("font-weight", "bold")
244
+ .attr("dy", "1em")
245
+ .text(lines[1]);
246
+ }
247
+ }
248
+ } catch (e) {
249
+ console.error("Error in text sizing:", e);
250
+ // 即使出错也不隐藏文本,而是显示小号字体
251
+ d3.select(this).attr("font-size", "8px");
252
+ }
253
+ });
254
+
255
+ // 添加值标签
256
+ const format = value => chartUtils.format.autoText(+value);
257
+ cells.append("text")
258
+ .attr("x", d => {
259
+ try {
260
+ return d3.polygonCentroid(d)[0];
261
+ } catch (e) {
262
+ return 0;
263
+ }
264
+ })
265
+ .attr("y", d => {
266
+ try {
267
+ // 根据分类标签是否换行来调整位置
268
+ const name = d.site.originalObject.data.originalData.name;
269
+ const offset = name.length > 10 ? 25 : 15;
270
+ return d3.polygonCentroid(d)[1] + offset;
271
+ } catch (e) {
272
+ return 0;
273
+ }
274
+ })
275
+ .attr("text-anchor", "middle")
276
+ .attr("dominant-baseline", "middle")
277
+ .attr("fill", "#fff")
278
+ .attr("fill-opacity", 0.7)
279
+ .attr("font-size", "14px")
280
+ .text(d => {
281
+ try {
282
+ return format(d.site.originalObject.data.originalData.weight);
283
+ } catch (e) {
284
+ console.error("Error accessing weight data:", e);
285
+ return "";
286
+ }
287
+ })
288
+ .each(function(d) {
289
+ try {
290
+ // 计算多边形的边界框
291
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
292
+ d.forEach(point => {
293
+ minX = Math.min(minX, point[0]);
294
+ minY = Math.min(minY, point[1]);
295
+ maxX = Math.max(maxX, point[0]);
296
+ maxY = Math.max(maxY, point[1]);
297
+ });
298
+
299
+ const boxWidth = maxX - minX;
300
+ const boxHeight = maxY - minY;
301
+
302
+ // 检查文本是否适合单元格
303
+ const textSelection = d3.select(this);
304
+ const textWidth = chartUtils.text.measure(null, this.textContent || "", {
305
+ fontFamily: "sans-serif",
306
+ fontSize: parseFloat(textSelection.attr("font-size")) || 12,
307
+ fontWeight: textSelection.attr("font-weight") || "normal"
308
+ }).width;
309
+
310
+ if (textWidth > boxWidth * 0.8 || boxHeight < 40) {
311
+ // 如果单元格很小,缩小字体而不是隐藏文本
312
+ d3.select(this)
313
+ .attr("font-size", "8px")
314
+ .attr("fill-opacity", 0.9);
315
+ }
316
+ } catch (e) {
317
+ console.error("Error in value sizing:", e);
318
+ // 即使出错也不隐藏文本,而是显示小号字体
319
+ d3.select(this).attr("font-size", "8px");
320
+ }
321
+ });
322
+
323
+ return svg.node();
324
+ }
modules/chart_engine/template/d3-js/treemap/voronoi_treemap_circle_01_dark.js ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Voronoi Treemap(Circle)",
5
+ "chart_name": "voronoi_treemap_circle_01_dark",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[5, 40], [0, "inf"]],
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": 600,
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 width = variables.width;
48
+ const height = variables.height;
49
+ const margin = { top: 10, right: 10, bottom: 10, left: 10 };
50
+
51
+ // 创建SVG
52
+ const svg = d3.select(containerSelector)
53
+ .append("svg")
54
+ .attr("width", "100%")
55
+ .attr("height", height)
56
+ .attr("viewBox", `0 0 ${width} ${height}`)
57
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
58
+ .attr("xmlns", "http://www.w3.org/2000/svg")
59
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
60
+
61
+ // 创建图表区域
62
+ const chartWidth = width - margin.left - margin.right;
63
+ const chartHeight = height - margin.top - margin.bottom;
64
+
65
+ const g = svg.append("g")
66
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
67
+
68
+ // 准备数据
69
+ const processedData = chartData.map(d => ({
70
+ name: d[categoryField],
71
+ weight: d[valueField]
72
+ }));
73
+
74
+ // 创建颜色比例尺
75
+ const colorScale = d => {
76
+ if (colorResolver.field(d, 0, { fallbackKey: "primary" }).value) {
77
+ return colorResolver.field(d, 0, { fallbackKey: "primary" }).value;
78
+ }
79
+ const uniqueCategories = [...new Set(chartData.map(d => d[categoryField]))];
80
+ return chartUtils.color.palette(uniqueCategories.indexOf(d), { palette: "tableau10" });
81
+ };
82
+
83
+ // 计算圆形裁剪区域
84
+ const radius = Math.min(chartWidth, chartHeight) / 2;
85
+ const centerX = chartWidth / 2;
86
+ const centerY = chartHeight / 2;
87
+
88
+ // 创建圆形裁剪多边形 - 使用多边形近似圆形
89
+ const numPoints = 50;
90
+ const clip = [];
91
+ for (let i = 0; i < numPoints; i++) {
92
+ const angle = (i / numPoints) * 2 * Math.PI;
93
+ clip.push([
94
+ centerX + radius * Math.cos(angle),
95
+ centerY + radius * Math.sin(angle)
96
+ ]);
97
+ }
98
+
99
+ // 创建 Voronoi Map 模拟
100
+ const simulation = d3.voronoiMapSimulation(processedData)
101
+ .weight(d => d.weight)
102
+ .clip(clip)
103
+ .stop();
104
+
105
+ // 运行模拟直到结束 - 限制迭代次数以避免超时
106
+ let state = simulation.state();
107
+ let iterations = 0;
108
+ const maxIterations = 300; // 限制最大迭代次数
109
+
110
+ while (!state.ended && iterations < maxIterations) {
111
+ simulation.tick();
112
+ state = simulation.state();
113
+ iterations++;
114
+ }
115
+
116
+ // 获取最终的多边形
117
+ const polygons = state.polygons;
118
+
119
+ // 绘制多边形
120
+ const cells = g.selectAll("g.cell")
121
+ .data(polygons)
122
+ .enter()
123
+ .append("g")
124
+ .attr("class", "cell");
125
+
126
+ // 添加单元格
127
+ cells.append("path")
128
+ .attr("d", d => {
129
+ return "M" + d.join("L") + "Z";
130
+ })
131
+ .attr("fill", d => {
132
+ try {
133
+ return colorScale(d.site.originalObject.data.originalData.name);
134
+ } catch (e) {
135
+ console.error("Error accessing color data:", e);
136
+ return "#ccc"; // 默认颜色
137
+ }
138
+ })
139
+ .attr("fill-opacity", 0.8)
140
+ .attr("stroke", "none")
141
+
142
+ // 添加文本标签
143
+ cells.append("text")
144
+ .attr("x", d => {
145
+ try {
146
+ return d3.polygonCentroid(d)[0];
147
+ } catch (e) {
148
+ console.error("Error calculating centroid:", e);
149
+ return 0;
150
+ }
151
+ })
152
+ .attr("y", d => {
153
+ try {
154
+ return d3.polygonCentroid(d)[1];
155
+ } catch (e) {
156
+ console.error("Error calculating centroid:", e);
157
+ return 0;
158
+ }
159
+ })
160
+ .attr("text-anchor", "middle")
161
+ .attr("dominant-baseline", "middle")
162
+ .attr("fill", "#fff")
163
+ .attr("font-size", "16px")
164
+ .attr("font-weight", "bold")
165
+ .text(d => {
166
+ try {
167
+ return d.site.originalObject.data.originalData.name;
168
+ } catch (e) {
169
+ console.error("Error accessing name data:", e);
170
+ return "";
171
+ }
172
+ })
173
+ .each(function(d) {
174
+ try {
175
+ // 计算多边形的边界框
176
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
177
+ d.forEach(point => {
178
+ minX = Math.min(minX, point[0]);
179
+ minY = Math.min(minY, point[1]);
180
+ maxX = Math.max(maxX, point[0]);
181
+ maxY = Math.max(maxY, point[1]);
182
+ });
183
+
184
+ const boxWidth = maxX - minX;
185
+ const boxHeight = maxY - minY;
186
+
187
+ // 检查文本是否适合单元格
188
+ const textSelection = d3.select(this);
189
+ const textWidth = chartUtils.text.measure(null, this.textContent || "", {
190
+ fontFamily: "sans-serif",
191
+ fontSize: parseFloat(textSelection.attr("font-size")) || 12,
192
+ fontWeight: textSelection.attr("font-weight") || "normal"
193
+ }).width;
194
+
195
+ if (textWidth > boxWidth * 0.8 || boxHeight < 30) {
196
+ // 如果文本太长或单元格太小,缩小字体而不是隐藏
197
+ d3.select(this)
198
+ .attr("font-size", "10px")
199
+ .text(function() {
200
+ const origText = this.textContent;
201
+ // 如果文本超过10个字符且单元格很小,尝试换行
202
+ if (origText.length > 10 && boxHeight < 40) {
203
+ const midPoint = Math.floor(origText.length / 2);
204
+ // 找到最近的空格或标点符号
205
+ let breakPoint = midPoint;
206
+ const punctuation = [' ', ',', '。', '、', ',', '.'];
207
+ let minDistance = origText.length;
208
+
209
+ for (let i = 0; i < origText.length; i++) {
210
+ if (punctuation.includes(origText[i])) {
211
+ const distance = Math.abs(i - midPoint);
212
+ if (distance < minDistance) {
213
+ minDistance = distance;
214
+ breakPoint = i;
215
+ }
216
+ }
217
+ }
218
+
219
+ // 如果找到合适的断点就换行,否则直接在中间换行
220
+ if (punctuation.includes(origText[breakPoint])) {
221
+ return origText.substring(0, breakPoint + 1) + '\n' + origText.substring(breakPoint + 1);
222
+ } else {
223
+ return origText.substring(0, midPoint) + '\n' + origText.substring(midPoint);
224
+ }
225
+ }
226
+ return origText;
227
+ })
228
+ .attr("dy", function() {
229
+ // 如果文本包含换行符,调整垂直位置
230
+ return this.textContent.includes('\n') ? "-0.5em" : "0";
231
+ });
232
+
233
+ // 如果文本包含换行符,创建第二行
234
+ if (d3.select(this).text().includes('\n')) {
235
+ const lines = d3.select(this).text().split('\n');
236
+ d3.select(this).text(lines[0]);
237
+
238
+ // 添加第二行
239
+ g.append("text")
240
+ .attr("x", d3.polygonCentroid(d)[0])
241
+ .attr("y", d3.polygonCentroid(d)[1])
242
+ .attr("text-anchor", "middle")
243
+ .attr("dominant-baseline", "middle")
244
+ .attr("fill", "#fff")
245
+ .attr("font-size", "10px")
246
+ .attr("font-weight", "bold")
247
+ .attr("dy", "1em")
248
+ .text(lines[1]);
249
+ }
250
+ }
251
+ } catch (e) {
252
+ console.error("Error in text sizing:", e);
253
+ // 即使出错也不隐藏文本,而是显示小号字体
254
+ d3.select(this).attr("font-size", "8px");
255
+ }
256
+ });
257
+
258
+ // 添加值标签
259
+ const format = value => chartUtils.format.autoText(+value);
260
+ cells.append("text")
261
+ .attr("x", d => {
262
+ try {
263
+ return d3.polygonCentroid(d)[0];
264
+ } catch (e) {
265
+ return 0;
266
+ }
267
+ })
268
+ .attr("y", d => {
269
+ try {
270
+ // 根据分类标签是否换行来调整位置
271
+ const name = d.site.originalObject.data.originalData.name;
272
+ const offset = name.length > 10 ? 25 : 15;
273
+ return d3.polygonCentroid(d)[1] + offset;
274
+ } catch (e) {
275
+ return 0;
276
+ }
277
+ })
278
+ .attr("text-anchor", "middle")
279
+ .attr("dominant-baseline", "middle")
280
+ .attr("fill", "#fff")
281
+ .attr("fill-opacity", 0.7)
282
+ .attr("font-size", "14px")
283
+ .text(d => {
284
+ try {
285
+ return format(d.site.originalObject.data.originalData.weight);
286
+ } catch (e) {
287
+ console.error("Error accessing weight data:", e);
288
+ return "";
289
+ }
290
+ })
291
+ .each(function(d) {
292
+ try {
293
+ // 计算多边形的边界框
294
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
295
+ d.forEach(point => {
296
+ minX = Math.min(minX, point[0]);
297
+ minY = Math.min(minY, point[1]);
298
+ maxX = Math.max(maxX, point[0]);
299
+ maxY = Math.max(maxY, point[1]);
300
+ });
301
+
302
+ const boxWidth = maxX - minX;
303
+ const boxHeight = maxY - minY;
304
+
305
+ // 检查文本是否适合单元格
306
+ const textSelection = d3.select(this);
307
+ const textWidth = chartUtils.text.measure(null, this.textContent || "", {
308
+ fontFamily: "sans-serif",
309
+ fontSize: parseFloat(textSelection.attr("font-size")) || 12,
310
+ fontWeight: textSelection.attr("font-weight") || "normal"
311
+ }).width;
312
+
313
+ if (textWidth > boxWidth * 0.8 || boxHeight < 40) {
314
+ // 如果单元格很小,缩小字体而不是隐藏文本
315
+ d3.select(this)
316
+ .attr("font-size", "8px")
317
+ .attr("fill-opacity", 0.9);
318
+ }
319
+ } catch (e) {
320
+ console.error("Error in value sizing:", e);
321
+ // 即使出错也不隐藏文本,而是显示小号字体
322
+ d3.select(this).attr("font-size", "8px");
323
+ }
324
+ });
325
+
326
+ return svg.node();
327
+ }
modules/chart_engine/template/d3-js/treemap/voronoi_treemap_circle_02.js ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Voronoi Treemap(Circle)",
5
+ "chart_name": "voronoi_treemap_circle_02",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[5, 40], [0, "inf"]],
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": 600,
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 width = variables.width;
45
+ const height = variables.height;
46
+ const margin = { top: 10, right: 10, bottom: 10, left: 10 };
47
+
48
+ // 创建SVG
49
+ const svg = d3.select(containerSelector)
50
+ .append("svg")
51
+ .attr("width", "100%")
52
+ .attr("height", height)
53
+ .attr("viewBox", `0 0 ${width} ${height}`)
54
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
55
+ .attr("xmlns", "http://www.w3.org/2000/svg")
56
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
57
+
58
+ // 创建图表区域
59
+ const chartWidth = width - margin.left - margin.right;
60
+ const chartHeight = height - margin.top - margin.bottom;
61
+
62
+ const g = svg.append("g")
63
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
64
+
65
+ // 准备数据
66
+ const processedData = chartData.map(d => ({
67
+ name: d[categoryField],
68
+ weight: d[valueField]
69
+ }));
70
+
71
+ // 创建颜色比例尺
72
+ const colorScale = d => {
73
+ if (colorResolver.field(d, 0, { fallbackKey: "primary" }).value) {
74
+ return colorResolver.field(d, 0, { fallbackKey: "primary" }).value;
75
+ }
76
+ const uniqueCategories = [...new Set(chartData.map(d => d[categoryField]))];
77
+ return chartUtils.color.palette(uniqueCategories.indexOf(d), { palette: "tableau10" });
78
+ };
79
+
80
+ // 计算圆形裁剪区域
81
+ const radius = Math.min(chartWidth, chartHeight) / 2;
82
+ const centerX = chartWidth / 2;
83
+ const centerY = chartHeight / 2;
84
+
85
+ // 创建圆形裁剪多边形 - 使用多边形近似圆形
86
+ const numPoints = 50;
87
+ const clip = [];
88
+ for (let i = 0; i < numPoints; i++) {
89
+ const angle = (i / numPoints) * 2 * Math.PI;
90
+ clip.push([
91
+ centerX + radius * Math.cos(angle),
92
+ centerY + radius * Math.sin(angle)
93
+ ]);
94
+ }
95
+
96
+ // 创建 Voronoi Map 模拟
97
+ const simulation = d3.voronoiMapSimulation(processedData)
98
+ .weight(d => d.weight)
99
+ .clip(clip)
100
+ .stop();
101
+
102
+ // 运行模拟直到结束 - 限制迭代次数以避免超时
103
+ let state = simulation.state();
104
+ let iterations = 0;
105
+ const maxIterations = 300; // 限制最大迭代次数
106
+
107
+ while (!state.ended && iterations < maxIterations) {
108
+ simulation.tick();
109
+ state = simulation.state();
110
+ iterations++;
111
+ }
112
+
113
+ // 获取最终的多边形
114
+ const polygons = state.polygons;
115
+
116
+ // 绘制多边形
117
+ const cells = g.selectAll("g.cell")
118
+ .data(polygons)
119
+ .enter()
120
+ .append("g")
121
+ .attr("class", "cell");
122
+
123
+ // 添加单元格
124
+ cells.append("path")
125
+ .attr("d", d => {
126
+ return "M" + d.join("L") + "Z";
127
+ })
128
+ .attr("fill", d => {
129
+ try {
130
+ return colorScale(d.site.originalObject.data.originalData.name);
131
+ } catch (e) {
132
+ console.error("Error accessing color data:", e);
133
+ return "#ccc"; // 默认颜色
134
+ }
135
+ })
136
+ .attr("fill-opacity", 0.8)
137
+ .attr("stroke", "none")
138
+
139
+ // 添加文本标签
140
+ cells.append("text")
141
+ .attr("x", d => {
142
+ try {
143
+ return d3.polygonCentroid(d)[0];
144
+ } catch (e) {
145
+ console.error("Error calculating centroid:", e);
146
+ return 0;
147
+ }
148
+ })
149
+ .attr("y", d => {
150
+ try {
151
+ return d3.polygonCentroid(d)[1];
152
+ } catch (e) {
153
+ console.error("Error calculating centroid:", e);
154
+ return 0;
155
+ }
156
+ })
157
+ .attr("text-anchor", "middle")
158
+ .attr("dominant-baseline", "middle")
159
+ .attr("fill", "#fff")
160
+ .attr("font-size", "16px")
161
+ .attr("font-weight", "bold")
162
+ .text(d => {
163
+ try {
164
+ return d.site.originalObject.data.originalData.name;
165
+ } catch (e) {
166
+ console.error("Error accessing name data:", e);
167
+ return "";
168
+ }
169
+ })
170
+ .each(function(d) {
171
+ try {
172
+ // 计算多边形的边界框
173
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
174
+ d.forEach(point => {
175
+ minX = Math.min(minX, point[0]);
176
+ minY = Math.min(minY, point[1]);
177
+ maxX = Math.max(maxX, point[0]);
178
+ maxY = Math.max(maxY, point[1]);
179
+ });
180
+
181
+ const boxWidth = maxX - minX;
182
+ const boxHeight = maxY - minY;
183
+
184
+ // 检查文本是否适合单元格
185
+ const textSelection = d3.select(this);
186
+ const textWidth = chartUtils.text.measure(null, this.textContent || "", {
187
+ fontFamily: "sans-serif",
188
+ fontSize: parseFloat(textSelection.attr("font-size")) || 12,
189
+ fontWeight: textSelection.attr("font-weight") || "normal"
190
+ }).width;
191
+
192
+ if (textWidth > boxWidth * 0.8 || boxHeight < 30) {
193
+ // 如果文本太长或单元格太小,缩小字体而不是隐藏
194
+ d3.select(this)
195
+ .attr("font-size", "10px")
196
+ .text(function() {
197
+ const origText = this.textContent;
198
+ // 如果文本超过10个字符且单元格很小,尝试换行
199
+ if (origText.length > 10 && boxHeight < 40) {
200
+ const midPoint = Math.floor(origText.length / 2);
201
+ // 找到最近的空格或标点符号
202
+ let breakPoint = midPoint;
203
+ const punctuation = [' ', ',', '。', '、', ',', '.'];
204
+ let minDistance = origText.length;
205
+
206
+ for (let i = 0; i < origText.length; i++) {
207
+ if (punctuation.includes(origText[i])) {
208
+ const distance = Math.abs(i - midPoint);
209
+ if (distance < minDistance) {
210
+ minDistance = distance;
211
+ breakPoint = i;
212
+ }
213
+ }
214
+ }
215
+
216
+ // 如果找到合适的断点就换行,否则直接在中间换行
217
+ if (punctuation.includes(origText[breakPoint])) {
218
+ return origText.substring(0, breakPoint + 1) + '\n' + origText.substring(breakPoint + 1);
219
+ } else {
220
+ return origText.substring(0, midPoint) + '\n' + origText.substring(midPoint);
221
+ }
222
+ }
223
+ return origText;
224
+ })
225
+ .attr("dy", function() {
226
+ // 如果文本包含换行符,调整垂直位置
227
+ return this.textContent.includes('\n') ? "-0.5em" : "0";
228
+ });
229
+
230
+ // 如果文本包含换行符,创建第二行
231
+ if (d3.select(this).text().includes('\n')) {
232
+ const lines = d3.select(this).text().split('\n');
233
+ d3.select(this).text(lines[0]);
234
+
235
+ // 添加第二行
236
+ g.append("text")
237
+ .attr("x", d3.polygonCentroid(d)[0])
238
+ .attr("y", d3.polygonCentroid(d)[1])
239
+ .attr("text-anchor", "middle")
240
+ .attr("dominant-baseline", "middle")
241
+ .attr("fill", "#fff")
242
+ .attr("font-size", "10px")
243
+ .attr("font-weight", "bold")
244
+ .attr("dy", "1em")
245
+ .text(lines[1]);
246
+ }
247
+ }
248
+ } catch (e) {
249
+ console.error("Error in text sizing:", e);
250
+ // 即使出错也不隐藏文本,而是显示小号字体
251
+ d3.select(this).attr("font-size", "8px");
252
+ }
253
+ });
254
+
255
+ // 添加值标签
256
+ const format = value => chartUtils.format.autoText(+value);
257
+ cells.append("text")
258
+ .attr("x", d => {
259
+ try {
260
+ return d3.polygonCentroid(d)[0];
261
+ } catch (e) {
262
+ return 0;
263
+ }
264
+ })
265
+ .attr("y", d => {
266
+ try {
267
+ // 根据分类标签是否换行来调整位置
268
+ const name = d.site.originalObject.data.originalData.name;
269
+ const offset = name.length > 10 ? 25 : 15;
270
+ return d3.polygonCentroid(d)[1] + offset;
271
+ } catch (e) {
272
+ return 0;
273
+ }
274
+ })
275
+ .attr("text-anchor", "middle")
276
+ .attr("dominant-baseline", "middle")
277
+ .attr("fill", "#fff")
278
+ .attr("fill-opacity", 0.7)
279
+ .attr("font-size", "14px")
280
+ .text(d => {
281
+ try {
282
+ return format(d.site.originalObject.data.originalData.weight);
283
+ } catch (e) {
284
+ console.error("Error accessing weight data:", e);
285
+ return "";
286
+ }
287
+ })
288
+ .each(function(d) {
289
+ try {
290
+ // 计算多边形的边界框
291
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
292
+ d.forEach(point => {
293
+ minX = Math.min(minX, point[0]);
294
+ minY = Math.min(minY, point[1]);
295
+ maxX = Math.max(maxX, point[0]);
296
+ maxY = Math.max(maxY, point[1]);
297
+ });
298
+
299
+ const boxWidth = maxX - minX;
300
+ const boxHeight = maxY - minY;
301
+
302
+ // 检查文本是否适合单元格
303
+ const textSelection = d3.select(this);
304
+ const textWidth = chartUtils.text.measure(null, this.textContent || "", {
305
+ fontFamily: "sans-serif",
306
+ fontSize: parseFloat(textSelection.attr("font-size")) || 12,
307
+ fontWeight: textSelection.attr("font-weight") || "normal"
308
+ }).width;
309
+
310
+ if (textWidth > boxWidth * 0.8 || boxHeight < 40) {
311
+ // 如果单元格很小,缩小字体而不是隐藏文本
312
+ d3.select(this)
313
+ .attr("font-size", "8px")
314
+ .attr("fill-opacity", 0.9);
315
+ }
316
+ } catch (e) {
317
+ console.error("Error in value sizing:", e);
318
+ // 即使出错也不隐藏文本,而是显示小号字体
319
+ d3.select(this).attr("font-size", "8px");
320
+ }
321
+ });
322
+
323
+ return svg.node();
324
+ }
modules/chart_engine/template/d3-js/treemap/voronoi_treemap_circle_03.js ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Voronoi Treemap(Circle)",
5
+ "chart_name": "voronoi_treemap_circle_03_hand",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[5, 40], [0, "inf"]],
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": 600,
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 width = variables.width;
45
+ const height = variables.height;
46
+ const margin = { top: 10, right: 10, bottom: 10, left: 10 };
47
+
48
+ // 创建SVG
49
+ const svg = d3.select(containerSelector)
50
+ .append("svg")
51
+ .attr("width", "100%")
52
+ .attr("height", height)
53
+ .attr("viewBox", `0 0 ${width} ${height}`)
54
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
55
+ .attr("xmlns", "http://www.w3.org/2000/svg")
56
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
57
+
58
+ // 创建图表区域
59
+ const chartWidth = width - margin.left - margin.right;
60
+ const chartHeight = height - margin.top - margin.bottom;
61
+
62
+ const g = svg.append("g")
63
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
64
+
65
+ // 准备数据
66
+ const processedData = chartData.map(d => ({
67
+ name: d[categoryField],
68
+ weight: d[valueField]
69
+ }));
70
+
71
+ // 创建颜色比例尺
72
+ const colorScale = d => {
73
+ if (colorResolver.field(d, 0, { fallbackKey: "primary" }).value) {
74
+ return colorResolver.field(d, 0, { fallbackKey: "primary" }).value;
75
+ }
76
+ const uniqueCategories = [...new Set(chartData.map(d => d[categoryField]))];
77
+ return chartUtils.color.palette(uniqueCategories.indexOf(d), { palette: "tableau10" });
78
+ };
79
+
80
+ // 计算圆形裁剪区域
81
+ const radius = Math.min(chartWidth, chartHeight) / 2;
82
+ const centerX = chartWidth / 2;
83
+ const centerY = chartHeight / 2;
84
+
85
+ // 创建圆形裁剪多边形 - 使用多边形近似圆形
86
+ const numPoints = 50;
87
+ const clip = [];
88
+ for (let i = 0; i < numPoints; i++) {
89
+ const angle = (i / numPoints) * 2 * Math.PI;
90
+ clip.push([
91
+ centerX + radius * Math.cos(angle),
92
+ centerY + radius * Math.sin(angle)
93
+ ]);
94
+ }
95
+
96
+ // 创建 Voronoi Map 模拟
97
+ const simulation = d3.voronoiMapSimulation(processedData)
98
+ .weight(d => d.weight)
99
+ .clip(clip)
100
+ .stop();
101
+
102
+ // 运行模拟直到结束 - 限制迭代次数以避免超时
103
+ let state = simulation.state();
104
+ let iterations = 0;
105
+ const maxIterations = 300; // 限制最大迭代次数
106
+
107
+ while (!state.ended && iterations < maxIterations) {
108
+ simulation.tick();
109
+ state = simulation.state();
110
+ iterations++;
111
+ }
112
+
113
+ // 获取最终的多边形
114
+ const polygons = state.polygons;
115
+
116
+ // 绘制多边形
117
+ const cells = g.selectAll("g.cell")
118
+ .data(polygons)
119
+ .enter()
120
+ .append("g")
121
+ .attr("class", "cell");
122
+
123
+ // 添加单元格
124
+ cells.append("path")
125
+ .attr("d", d => {
126
+ return "M" + d.join("L") + "Z";
127
+ })
128
+ .attr("fill", d => {
129
+ try {
130
+ return colorScale(d.site.originalObject.data.originalData.name);
131
+ } catch (e) {
132
+ console.error("Error accessing color data:", e);
133
+ return "#ccc"; // 默认颜色
134
+ }
135
+ })
136
+ .attr("fill-opacity", 0.8)
137
+ .attr("stroke", "none")
138
+
139
+ // 添加文本标签
140
+ cells.append("text")
141
+ .attr("x", d => {
142
+ try {
143
+ return d3.polygonCentroid(d)[0];
144
+ } catch (e) {
145
+ console.error("Error calculating centroid:", e);
146
+ return 0;
147
+ }
148
+ })
149
+ .attr("y", d => {
150
+ try {
151
+ return d3.polygonCentroid(d)[1];
152
+ } catch (e) {
153
+ console.error("Error calculating centroid:", e);
154
+ return 0;
155
+ }
156
+ })
157
+ .attr("text-anchor", "middle")
158
+ .attr("dominant-baseline", "middle")
159
+ .attr("fill", "#fff")
160
+ .attr("font-size", "16px")
161
+ .attr("font-weight", "bold")
162
+ .text(d => {
163
+ try {
164
+ return d.site.originalObject.data.originalData.name;
165
+ } catch (e) {
166
+ console.error("Error accessing name data:", e);
167
+ return "";
168
+ }
169
+ })
170
+ .each(function(d) {
171
+ try {
172
+ // 计算多边形的边界框
173
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
174
+ d.forEach(point => {
175
+ minX = Math.min(minX, point[0]);
176
+ minY = Math.min(minY, point[1]);
177
+ maxX = Math.max(maxX, point[0]);
178
+ maxY = Math.max(maxY, point[1]);
179
+ });
180
+
181
+ const boxWidth = maxX - minX;
182
+ const boxHeight = maxY - minY;
183
+
184
+ // 检查文本是否适合单元格
185
+ const text = d3.select(this);
186
+ const measureTextWidth = () => chartUtils.text.measure(null, text.text(), {
187
+ fontFamily: "sans-serif",
188
+ fontSize: parseFloat(text.attr("font-size")) || 12,
189
+ fontWeight: text.attr("font-weight") || "normal"
190
+ }).width;
191
+ const textWidth = measureTextWidth();
192
+
193
+ // 不再隐藏文本,而是调整大小
194
+ if (textWidth > boxWidth * 0.8) {
195
+ // 如果文本太长,逐步缩小字体
196
+ let fontSize = 14;
197
+ text.attr("font-size", `${fontSize}px`);
198
+
199
+ while (measureTextWidth() > boxWidth * 0.9 && fontSize > 8) {
200
+ fontSize -= 1;
201
+ text.attr("font-size", `${fontSize}px`);
202
+ }
203
+
204
+ // 如果字体已经很小但仍然太宽,尝试换行显示
205
+ if (measureTextWidth() > boxWidth * 0.9) {
206
+ const originalText = text.text();
207
+ if (originalText.length > 3) {
208
+ // 分成两行
209
+ const midPoint = Math.ceil(originalText.length / 2);
210
+ const firstLine = originalText.substring(0, midPoint);
211
+ const secondLine = originalText.substring(midPoint);
212
+
213
+ // 清除原始文本
214
+ text.text("");
215
+
216
+ // 添加第一行
217
+ text.append("tspan")
218
+ .attr("x", d3.polygonCentroid(d)[0])
219
+ .attr("dy", "-0.3em")
220
+ .text(firstLine);
221
+
222
+ // 添加第二行
223
+ text.append("tspan")
224
+ .attr("x", d3.polygonCentroid(d)[0])
225
+ .attr("dy", "1.2em")
226
+ .text(secondLine);
227
+ }
228
+ }
229
+ }
230
+ } catch (e) {
231
+ console.error("Error in text sizing:", e);
232
+ }
233
+ });
234
+
235
+ // 添加值标签
236
+ const format = value => chartUtils.format.autoText(+value);
237
+ cells.append("text")
238
+ .attr("class", "value-label")
239
+ .attr("x", d => {
240
+ try {
241
+ return d3.polygonCentroid(d)[0];
242
+ } catch (e) {
243
+ return 0;
244
+ }
245
+ })
246
+ .attr("y", d => {
247
+ try {
248
+ return d3.polygonCentroid(d)[1] + 15;
249
+ } catch (e) {
250
+ return 0;
251
+ }
252
+ })
253
+ .attr("text-anchor", "middle")
254
+ .attr("dominant-baseline", "middle")
255
+ .attr("fill", "#fff")
256
+ .attr("fill-opacity", 0.7)
257
+ .attr("font-size", "14px")
258
+ .text(d => {
259
+ try {
260
+ return format(d.site.originalObject.data.originalData.weight);
261
+ } catch (e) {
262
+ console.error("Error accessing weight data:", e);
263
+ return "";
264
+ }
265
+ })
266
+ .each(function(d) {
267
+ try {
268
+ // 计算多边形的边界框
269
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
270
+ d.forEach(point => {
271
+ minX = Math.min(minX, point[0]);
272
+ minY = Math.min(minY, point[1]);
273
+ maxX = Math.max(maxX, point[0]);
274
+ maxY = Math.max(maxY, point[1]);
275
+ });
276
+
277
+ const boxWidth = maxX - minX;
278
+ const boxHeight = maxY - minY;
279
+
280
+ // 检查文本是否适合单元格
281
+ const text = d3.select(this);
282
+ const measureTextWidth = () => chartUtils.text.measure(null, text.text(), {
283
+ fontFamily: "sans-serif",
284
+ fontSize: parseFloat(text.attr("font-size")) || 12,
285
+ fontWeight: text.attr("font-weight") || "normal"
286
+ }).width;
287
+ const textWidth = measureTextWidth();
288
+
289
+ // 确保值标签显示
290
+ if (textWidth > boxWidth * 0.8) {
291
+ // 如果值标签太长,调整字体大小
292
+ let fontSize = 12;
293
+ text.attr("font-size", `${fontSize}px`);
294
+
295
+ // 降低字体大小
296
+ while (measureTextWidth() > boxWidth * 0.85 && fontSize > 7) {
297
+ fontSize -= 1;
298
+ text.attr("font-size", `${fontSize}px`);
299
+ }
300
+
301
+ // 如果单元格很小,调整y位置,避免与名称标签重叠
302
+ const nameLabel = d3.select(this.parentNode).select("text:not(.value-label)");
303
+
304
+ // 如果单元名称使用了两行显示(有tspan元素),则需要下移值标签
305
+ if (nameLabel.selectAll("tspan").size() > 0) {
306
+ text.attr("y", d => {
307
+ try {
308
+ return d3.polygonCentroid(d)[1] + 25;
309
+ } catch (e) {
310
+ return 0;
311
+ }
312
+ });
313
+ }
314
+ }
315
+ } catch (e) {
316
+ console.error("Error in value sizing:", e);
317
+ }
318
+ });
319
+
320
+ const roughness = 1;
321
+ const bowing = 2;
322
+ const fillStyle = "solid";
323
+ const randomize = false;
324
+ const pencilFilter = false;
325
+
326
+ const svgConverter = new svg2roughjs.Svg2Roughjs(containerSelector);
327
+ svgConverter.pencilFilter = pencilFilter;
328
+ svgConverter.randomize = randomize;
329
+ svgConverter.svg = svg.node();
330
+ svgConverter.roughConfig = {
331
+ bowing,
332
+ roughness,
333
+ fillStyle
334
+ };
335
+ svgConverter.sketch();
336
+ // Remove the first SVG element if it exists
337
+ const firstSvg = document.querySelector(`${containerSelector} svg`);
338
+ if (firstSvg) {
339
+ firstSvg.remove();
340
+ }
341
+
342
+ return svg.node();
343
+ }
modules/chart_engine/template/d3-js/treemap/voronoi_treemap_circle_04.js ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Voronoi Treemap(Circle)",
5
+ "chart_name": "voronoi_treemap_circle_04",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[5, 40], [0, "inf"]],
9
+ "required_fields_icons": ["x"],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 600,
16
+ "background": "light",
17
+ "icon_mark": "none",
18
+ "icon_label": "side",
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 width = variables.width;
45
+ const height = variables.height;
46
+ const initialMargin = { // 引入初始边距,图例将影响顶边距
47
+ top: variables.margin?.top ?? 10,
48
+ right: variables.margin?.right ?? 10,
49
+ bottom: variables.margin?.bottom ?? 10,
50
+ left: variables.margin?.left ?? 10
51
+ };
52
+
53
+ // 创建SVG
54
+ const svg = d3.select(containerSelector)
55
+ .append("svg")
56
+ .attr("width", "100%")
57
+ .attr("height", height)
58
+ .attr("viewBox", `0 0 ${width} ${height}`)
59
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
60
+ .attr("xmlns", "http://www.w3.org/2000/svg")
61
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
62
+
63
+ // chartWidth 用于图例换行计算,在图例逻辑之前定义
64
+ const chartWidth = width - initialMargin.left - initialMargin.right;
65
+ // chartHeight 和主图表区域的 g 将在图例计算后定义
66
+
67
+ // 准备数据 (在图例和主图表逻辑都会用到)
68
+ const processedData = chartData.map(d => ({
69
+ name: d[categoryField],
70
+ weight: d[valueField]
71
+ }));
72
+
73
+ const colorScale = d => {
74
+ if (colorResolver.field(d, 0, { fallbackKey: "primary" }).value) {
75
+ return colorResolver.field(d, 0, { fallbackKey: "primary" }).value;
76
+ }
77
+ const localUniqueCategories = [...new Set(chartData.map(d => d[categoryField]))];
78
+ return chartUtils.color.palette(localUniqueCategories.indexOf(d), { palette: "tableau10" });
79
+ };
80
+
81
+ // ---------- 图例逻辑 (借鉴 rectangle_03) ----------
82
+ const uniqueCategories = [...new Set(chartData.map(d => d[categoryField]))];
83
+
84
+ const canvas = document.createElement('canvas');
85
+ const ctx = canvas.getContext('2d');
86
+ function getTextWidthHelper(text, fontFamily, fontSize, fontWeight) {
87
+ ctx.font = `${fontWeight || 'normal'} ${fontSize}px ${fontFamily || 'Arial'}`;
88
+ return chartUtils.text.contextWidth(ctx, text);
89
+ }
90
+
91
+ let legendBlockHeight = 0;
92
+ const legendLines = [];
93
+ const paddingBelowLegendToChart = 15;
94
+ const minSvgGlobalTopPadding = 10;
95
+ let legendItemMaxHeight = 0;
96
+ let interLineVerticalPadding = parseFloat(typography.label?.line_spacing || '6');
97
+ let legendInterItemSpacing = 10;
98
+
99
+ if (uniqueCategories && uniqueCategories.length > 0 && images) {
100
+ const legendColorRectWidth = 12;
101
+ const legendColorRectHeight = 12;
102
+ const legendIconWidth = typography.label?.icon_size || 16;
103
+ const legendIconHeight = typography.label?.icon_size || 16;
104
+ const legendPaddingRectIcon = 4;
105
+ const legendPaddingIconText = 4;
106
+
107
+ const legendFontFamily = typography.label?.font_family || 'Arial';
108
+ const legendFontSize = parseFloat(typography.label?.font_size || '12');
109
+ const legendFontWeight = typography.label?.font_weight || 'normal';
110
+
111
+ legendItemMaxHeight = Math.max(legendColorRectHeight, legendIconHeight, legendFontSize);
112
+
113
+ const legendItemsData = uniqueCategories.map(catName => {
114
+ const text = String(catName);
115
+ const color = colorScale(catName);
116
+ const iconUrl = images.field && images.field[catName] ? images.field[catName] : null;
117
+ const textWidth = getTextWidthHelper(text, legendFontFamily, legendFontSize, legendFontWeight);
118
+
119
+ let itemVisualWidth = legendColorRectWidth;
120
+ if (iconUrl) {
121
+ itemVisualWidth += legendPaddingRectIcon + legendIconWidth + legendPaddingIconText;
122
+ } else {
123
+ itemVisualWidth += legendPaddingRectIcon;
124
+ }
125
+ itemVisualWidth += textWidth;
126
+ return { text, color, iconUrl, textWidth, visualWidth: itemVisualWidth };
127
+ });
128
+
129
+ const legendLayout = chartUtils.legend.wrapItems(
130
+ legendItemsData.map(item => ({ ...item, width: item.visualWidth, height: legendItemMaxHeight })),
131
+ {
132
+ maxWidth: chartWidth,
133
+ itemGap: legendInterItemSpacing,
134
+ rowGap: interLineVerticalPadding,
135
+ itemHeight: legendItemMaxHeight,
136
+ }
137
+ );
138
+ legendLines.push(...legendLayout.rows.map(row => ({
139
+ items: row.items,
140
+ totalVisualWidth: row.width,
141
+ })));
142
+ legendBlockHeight = legendLayout.height;
143
+ }
144
+
145
+ let effectiveMarginTop;
146
+ let legendStartY = minSvgGlobalTopPadding;
147
+ if (legendBlockHeight > 0) {
148
+ effectiveMarginTop = legendStartY + legendBlockHeight + paddingBelowLegendToChart;
149
+ } else {
150
+ effectiveMarginTop = Math.max(initialMargin.top, minSvgGlobalTopPadding);
151
+ }
152
+
153
+ // 重新计算主图表区域的 chartHeight 和 centerY (用于圆形裁剪)
154
+ const mainChartHeight = height - effectiveMarginTop - initialMargin.bottom; // Renamed to mainChartHeight to avoid conflict if chartHeight is used above for legend width context
155
+
156
+ if (mainChartHeight <= 0) {
157
+ console.warn("Voronoi Treemap (Circle): Chart height is not positive after accommodating legend and margins.");
158
+ // return null; // Or handle error appropriately
159
+ }
160
+
161
+ // ---------- 绘制图例 (如果存在) ----------
162
+ if (legendBlockHeight > 0 && legendLines.length > 0) {
163
+ const legendContainerGroup = svg.append("g")
164
+ .attr("class", "custom-legend-container")
165
+ .attr("transform", `translate(0, ${legendStartY})`);
166
+ let currentLineBaseY = 0;
167
+ const currentLegendInterItemSpacing = legendInterItemSpacing;
168
+
169
+ legendLines.forEach((line) => {
170
+ const lineRenderStartX = initialMargin.left + (chartWidth - line.totalVisualWidth) / 2;
171
+ const lineCenterY = currentLineBaseY + legendItemMaxHeight / 2;
172
+ let currentItemDrawX = lineRenderStartX;
173
+ const legendColorRectWidth = 12;
174
+ const legendColorRectHeight = 12;
175
+ const legendIconWidth = typography.label?.icon_size || 16;
176
+ const legendIconHeight = typography.label?.icon_size || 16;
177
+ const legendPaddingRectIcon = 4;
178
+ const legendPaddingIconText = 4;
179
+ const legendFontFamily = typography.label?.font_family || 'Arial';
180
+ const legendFontSize = parseFloat(typography.label?.font_size || '12');
181
+ const legendFontWeight = typography.label?.font_weight || 'normal';
182
+
183
+ line.items.forEach((item, itemIndex) => {
184
+ legendContainerGroup.append("rect")
185
+ .attr("x", currentItemDrawX)
186
+ .attr("y", currentLineBaseY + (legendItemMaxHeight - legendColorRectHeight) / 2)
187
+ .attr("width", legendColorRectWidth).attr("height", legendColorRectHeight)
188
+ .attr("fill", item.color).attr("fill-opacity", 0.85);
189
+ currentItemDrawX += legendColorRectWidth;
190
+ if (item.iconUrl) {
191
+ currentItemDrawX += legendPaddingRectIcon;
192
+ legendContainerGroup.append("image").attr("xlink:href", item.iconUrl)
193
+ .attr("x", currentItemDrawX)
194
+ .attr("y", currentLineBaseY + (legendItemMaxHeight - legendIconHeight) / 2)
195
+ .attr("width", legendIconWidth).attr("height", legendIconHeight)
196
+ .attr("preserveAspectRatio", "xMidYMid meet");
197
+ currentItemDrawX += legendIconWidth + legendPaddingIconText;
198
+ } else {
199
+ currentItemDrawX += legendPaddingRectIcon;
200
+ }
201
+ legendContainerGroup.append("text").attr("x", currentItemDrawX).attr("y", lineCenterY)
202
+ .attr("dominant-baseline", "middle")
203
+ .style("font-family", legendFontFamily).style("font-size", `${legendFontSize}px`)
204
+ .style("font-weight", legendFontWeight).style("fill", colorResolver.text({ fallback: "#333333" }).value || typography.label?.font_color || "#333333")
205
+ .text(item.text);
206
+ currentItemDrawX += item.textWidth;
207
+ if (itemIndex < line.items.length - 1) {
208
+ currentItemDrawX += (line.items[itemIndex+1].visualWidth > 0 ? (currentLegendInterItemSpacing || 10) : 0) ;
209
+ }
210
+ });
211
+ currentLineBaseY += legendItemMaxHeight + interLineVerticalPadding;
212
+ });
213
+ }
214
+
215
+ // ---------- 创建主图表绘图区域 (g) ----------
216
+ const g = svg.append("g")
217
+ .attr("transform", `translate(${initialMargin.left}, ${effectiveMarginTop})`);
218
+
219
+ // 计算圆形裁剪区域 - 使用新的 mainChartHeight 和 chartWidth (for radius calculation if width constrained)
220
+ // chartWidth for the main drawing area is the same as for legend (width - initialMargin.left - initialMargin.right)
221
+ const mainChartContentWidth = chartWidth;
222
+ const radius = Math.min(mainChartContentWidth, mainChartHeight) / 2;
223
+ const centerX = mainChartContentWidth / 2;
224
+ const centerY = mainChartHeight / 2;
225
+
226
+ // 创建圆形裁剪多边形 - 使用多边形近似圆形
227
+ const numPoints = 50;
228
+ const clip = [];
229
+ for (let i = 0; i < numPoints; i++) {
230
+ const angle = (i / numPoints) * 2 * Math.PI;
231
+ clip.push([
232
+ centerX + radius * Math.cos(angle),
233
+ centerY + radius * Math.sin(angle)
234
+ ]);
235
+ }
236
+
237
+ // 创建 Voronoi Map 模拟
238
+ const simulation = d3.voronoiMapSimulation(processedData)
239
+ .weight(d => d.weight)
240
+ .clip(clip)
241
+ .stop();
242
+
243
+ // 运行模拟直到结束 - 限制迭代次数以避免超时
244
+ let state = simulation.state();
245
+ let iterations = 0;
246
+ const maxIterations = 300; // 限制最大迭代次数
247
+
248
+ while (!state.ended && iterations < maxIterations) {
249
+ simulation.tick();
250
+ state = simulation.state();
251
+ iterations++;
252
+ }
253
+
254
+ // 获取最终的多边形
255
+ const polygons = state.polygons;
256
+
257
+ // 绘制多边形
258
+ const cells = g.selectAll("g.cell")
259
+ .data(polygons)
260
+ .enter()
261
+ .append("g")
262
+ .attr("class", "cell");
263
+
264
+ // 添加单元格
265
+ cells.append("path")
266
+ .attr("d", d => {
267
+ return "M" + d.join("L") + "Z";
268
+ })
269
+ .attr("fill", d => {
270
+ try {
271
+ return colorScale(d.site.originalObject.data.originalData.name);
272
+ } catch (e) {
273
+ console.error("Error accessing color data:", e);
274
+ return "#ccc"; // 默认颜色
275
+ }
276
+ })
277
+ .attr("fill-opacity", 0.8)
278
+ .attr("stroke", "none")
279
+
280
+ // 添加值标签 (调整为只显示数值,并适应单元格)
281
+ const format = value => chartUtils.format.autoText(+value);
282
+ cells.append("text")
283
+ .attr("class", "value-label-cell")
284
+ .attr("x", d => {
285
+ try { return d3.polygonCentroid(d)[0]; } catch (e) { console.error("Centroid error for value:", e); return 0; }
286
+ })
287
+ .attr("y", d => {
288
+ try { return d3.polygonCentroid(d)[1]; } catch (e) { console.error("Centroid error for value:", e); return 0; }
289
+ })
290
+ .attr("text-anchor", "middle")
291
+ .attr("dominant-baseline", "middle")
292
+ .attr("fill", "#ffffff")
293
+ .attr("font-size", typography.value?.font_size || "12px") // 调整基础字体大小
294
+ .attr("font-weight", typography.value?.font_weight || "normal")
295
+ .text(d => {
296
+ try { return format(d.site.originalObject.data.originalData.weight); } catch (e) { console.error("Weight data error:", e); return ""; }
297
+ })
298
+ .each(function(d) {
299
+ try {
300
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
301
+ d.forEach(point => {
302
+ minX = Math.min(minX, point[0]);
303
+ minY = Math.min(minY, point[1]);
304
+ maxX = Math.max(maxX, point[0]);
305
+ maxY = Math.max(maxY, point[1]);
306
+ });
307
+ const boxWidth = maxX - minX;
308
+ const boxHeight = maxY - minY;
309
+ const textSelection = d3.select(this);
310
+ const textWidth = chartUtils.text.measure(null, this.textContent || "", {
311
+ fontFamily: "sans-serif",
312
+ fontSize: parseFloat(textSelection.attr("font-size")) || 12,
313
+ fontWeight: textSelection.attr("font-weight") || "normal"
314
+ }).width;
315
+ const currentFontSize = parseFloat(this.getAttribute("font-size"));
316
+
317
+ // 如果文本太宽或太高,尝试缩小字体,最小到8px
318
+ if ((textWidth > boxWidth * 0.9 || currentFontSize > boxHeight * 0.7) && currentFontSize > 8) {
319
+ this.setAttribute("font-size", Math.max(8, currentFontSize - 2) + "px");
320
+ // 递归调用以再次检查,或直接隐藏如果还是太大
321
+ const newTextWidth = chartUtils.text.measure(null, this.textContent || "", {
322
+ fontFamily: "sans-serif",
323
+ fontSize: parseFloat(this.getAttribute("font-size")) || 12,
324
+ fontWeight: textSelection.attr("font-weight") || "normal"
325
+ }).width;
326
+ const newFontSize = parseFloat(this.getAttribute("font-size"));
327
+ if (newTextWidth > boxWidth * 0.9 || newFontSize > boxHeight * 0.7) {
328
+ d3.select(this).style("display", "none");
329
+ }
330
+ } else if (textWidth > boxWidth * 0.9 || currentFontSize > boxHeight * 0.7) {
331
+ d3.select(this).style("display", "none"); // 如果已经是8px还太大,则隐藏
332
+ }
333
+ } catch (e) {
334
+ console.error("Error in value label sizing:", e);
335
+ d3.select(this).style("display", "none"); // 出错则隐藏
336
+ }
337
+ });
338
+
339
+ return svg.node();
340
+ }
modules/chart_engine/template/d3-js/treemap/voronoi_treemap_rectangle_01.js ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Voronoi Treemap(Rectangle)",
5
+ "chart_name": "voronoi_treemap_rectangle_01",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 40], [0, "inf"]],
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": 600,
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 width = variables.width;
45
+ const height = variables.height;
46
+ const margin = { top: 10, right: 10, bottom: 10, left: 10 };
47
+
48
+ // 创建SVG
49
+ const svg = d3.select(containerSelector)
50
+ .append("svg")
51
+ .attr("width", "100%")
52
+ .attr("height", height)
53
+ .attr("viewBox", `0 0 ${width} ${height}`)
54
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
55
+ .attr("xmlns", "http://www.w3.org/2000/svg")
56
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
57
+
58
+ // 创建图表区域
59
+ const chartWidth = width - margin.left - margin.right;
60
+ const chartHeight = height - margin.top - margin.bottom;
61
+
62
+ const g = svg.append("g")
63
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
64
+
65
+ // 准备数据
66
+ const processedData = chartData.map(d => ({
67
+ name: d[categoryField],
68
+ weight: d[valueField]
69
+ }));
70
+
71
+ // 创建颜色比例尺
72
+ const colorScale = d => {
73
+ if (colorResolver.field(d, 0, { fallbackKey: "primary" }).value) {
74
+ return colorResolver.field(d, 0, { fallbackKey: "primary" }).value;
75
+ }
76
+ const uniqueCategories = [...new Set(chartData.map(d => d[categoryField]))];
77
+ return chartUtils.color.palette(uniqueCategories.indexOf(d), { palette: "tableau10" });
78
+ };
79
+
80
+ // 定义裁剪多边形(矩形)
81
+ const clip = [
82
+ [0, 0],
83
+ [0, chartHeight],
84
+ [chartWidth, chartHeight],
85
+ [chartWidth, 0]
86
+ ];
87
+
88
+ // 创建 Voronoi Map 模拟
89
+ const simulation = d3.voronoiMapSimulation(processedData)
90
+ .weight(d => d.weight)
91
+ .clip(clip)
92
+ .stop();
93
+
94
+ // 运行模拟直到结束
95
+ let state = simulation.state();
96
+ while (!state.ended) {
97
+ simulation.tick();
98
+ state = simulation.state();
99
+ }
100
+
101
+ // 获取最终的多边形
102
+ const polygons = state.polygons;
103
+
104
+ // 绘制多边形
105
+ const cells = g.selectAll("g")
106
+ .data(polygons)
107
+ .enter()
108
+ .append("g");
109
+
110
+ // 添加单元格
111
+ cells.append("path")
112
+ .attr("d", d => {
113
+ return "M" + d.join("L") + "Z";
114
+ })
115
+ .attr("fill", d => colorScale(d.site.originalObject.data.originalData.name))
116
+ .attr("fill-opacity", 0.8)
117
+ .attr("stroke", "none")
118
+
119
+ // 为每个单元格添加工具提示
120
+ const format = value => chartUtils.format.autoText(+value);
121
+
122
+ // 添加文本标签
123
+ cells.append("text")
124
+ .attr("x", d => d3.polygonCentroid(d)[0])
125
+ .attr("y", d => d3.polygonCentroid(d)[1])
126
+ .attr("text-anchor", "middle")
127
+ .attr("dominant-baseline", "middle")
128
+ .attr("fill", "#fff")
129
+ .attr("font-size", "16px")
130
+ .attr("font-weight", "bold")
131
+ .text(d => d.site.originalObject.data.originalData.name)
132
+ .each(function(d) {
133
+ // 计算多边形的边界框
134
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
135
+ d.forEach(point => {
136
+ minX = Math.min(minX, point[0]);
137
+ minY = Math.min(minY, point[1]);
138
+ maxX = Math.max(maxX, point[0]);
139
+ maxY = Math.max(maxY, point[1]);
140
+ });
141
+
142
+ const boxWidth = maxX - minX;
143
+ const boxHeight = maxY - minY;
144
+
145
+ // 检查文本是否适合单元格
146
+ const textSelection = d3.select(this);
147
+ const textWidth = chartUtils.text.measure(null, this.textContent || "", {
148
+ fontFamily: "sans-serif",
149
+ fontSize: parseFloat(textSelection.attr("font-size")) || 12,
150
+ fontWeight: textSelection.attr("font-weight") || "normal"
151
+ }).width;
152
+
153
+ if (textWidth > boxWidth * 0.8 || boxHeight < 30) {
154
+ // 如果文本太长或单元格太小,在文本下方添加暗色透明框
155
+ // 创建一个新的g元素来包含背景框和文本
156
+ const textGroup = d3.select(this.parentNode)
157
+ .append("g")
158
+ .raise(); // 将整个组提升到最上层
159
+
160
+ // 添加背景框
161
+ const padding = 4;
162
+ textGroup.append("rect")
163
+ .attr("x", d3.polygonCentroid(d)[0] - textWidth/2 - padding)
164
+ .attr("y", d3.polygonCentroid(d)[1] - 10)
165
+ .attr("width", textWidth + padding * 2)
166
+ .attr("height", 35)
167
+ .attr("fill", "rgba(0,0,0,0.3)")
168
+ .attr("rx", 3);
169
+
170
+ // 将原始文本移动到新组中
171
+ d3.select(this).remove();
172
+ textGroup.append("text")
173
+ .attr("x", d3.polygonCentroid(d)[0])
174
+ .attr("y", d3.polygonCentroid(d)[1])
175
+ .attr("text-anchor", "middle")
176
+ .attr("dominant-baseline", "middle")
177
+ .attr("fill", "#fff")
178
+ .attr("font-size", "16px")
179
+ .attr("font-weight", "bold")
180
+ .text(d.site.originalObject.data.originalData.name);
181
+ }
182
+ });
183
+
184
+ // 添加值标签
185
+ cells.append("text")
186
+ .attr("x", d => d3.polygonCentroid(d)[0])
187
+ .attr("y", d => d3.polygonCentroid(d)[1] + 15)
188
+ .attr("text-anchor", "middle")
189
+ .attr("dominant-baseline", "middle")
190
+ .attr("fill", "#fff")
191
+ .attr("fill-opacity", 0.7)
192
+ .attr("font-size", "14px")
193
+ .text(d => format(d.site.originalObject.data.originalData.weight))
194
+ .each(function(d) {
195
+ // 计算多边形的边界框
196
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
197
+ d.forEach(point => {
198
+ minX = Math.min(minX, point[0]);
199
+ minY = Math.min(minY, point[1]);
200
+ maxX = Math.max(maxX, point[0]);
201
+ maxY = Math.max(maxY, point[1]);
202
+ });
203
+
204
+ const boxWidth = maxX - minX;
205
+ const boxHeight = maxY - minY;
206
+
207
+ // 检查文本是否适合单元格
208
+ const textSelection = d3.select(this);
209
+ const textWidth = chartUtils.text.measure(null, this.textContent || "", {
210
+ fontFamily: "sans-serif",
211
+ fontSize: parseFloat(textSelection.attr("font-size")) || 12,
212
+ fontWeight: textSelection.attr("font-weight") || "normal"
213
+ }).width;
214
+
215
+ if (textWidth > boxWidth * 0.8 || boxHeight < 40) {
216
+ // 如果文本太长或单元格太小,隐藏它
217
+ d3.select(this).style("display", "none");
218
+ }
219
+ });
220
+
221
+ return svg.node();
222
+ }
modules/chart_engine/template/d3-js/treemap/voronoi_treemap_rectangle_02.js ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Voronoi Treemap(Rectangle)",
5
+ "chart_name": "voronoi_treemap_rectangle_02",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 20], [0, "inf"]],
9
+ "required_fields_icons": ["x"],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 600,
16
+ "background": "light",
17
+ "icon_mark": "icon",
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 width = variables.width;
45
+ const height = variables.height;
46
+ const margin = { top: 10, right: 10, bottom: 10, left: 10 };
47
+
48
+ // 创建SVG
49
+ const svg = d3.select(containerSelector)
50
+ .append("svg")
51
+ .attr("width", "100%")
52
+ .attr("height", height)
53
+ .attr("viewBox", `0 0 ${width} ${height}`)
54
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
55
+ .attr("xmlns", "http://www.w3.org/2000/svg")
56
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
57
+
58
+ // 创建图表区域
59
+ const chartWidth = width - margin.left - margin.right;
60
+ const chartHeight = height - margin.top - margin.bottom;
61
+
62
+ const g = svg.append("g")
63
+ .attr("transform", `translate(${margin.left}, ${margin.top})`);
64
+
65
+ // 准备数据
66
+ const processedData = chartData.map(d => ({
67
+ name: d[categoryField],
68
+ weight: d[valueField]
69
+ }));
70
+
71
+ // 创建颜色比例尺
72
+ const colorScale = d => {
73
+ if (colorResolver.field(d, 0, { fallbackKey: "primary" }).value) {
74
+ return colorResolver.field(d, 0, { fallbackKey: "primary" }).value;
75
+ }
76
+ const uniqueCategories = [...new Set(chartData.map(d => d[categoryField]))];
77
+ return chartUtils.color.palette(uniqueCategories.indexOf(d), { palette: "tableau10" });
78
+ };
79
+
80
+ // 定义裁剪多边形(矩形)
81
+ const clip = [
82
+ [0, 0],
83
+ [0, chartHeight],
84
+ [chartWidth, chartHeight],
85
+ [chartWidth, 0]
86
+ ];
87
+
88
+ // 创建 Voronoi Map 模拟
89
+ const simulation = d3.voronoiMapSimulation(processedData)
90
+ .weight(d => d.weight)
91
+ .clip(clip)
92
+ .stop();
93
+
94
+ // 运行模拟直到结束
95
+ let state = simulation.state();
96
+ while (!state.ended) {
97
+ simulation.tick();
98
+ state = simulation.state();
99
+ }
100
+
101
+ // 获取最终的多边形
102
+ const polygons = state.polygons;
103
+
104
+ // 定义纹理模式
105
+ // 创建纹理定义
106
+ const defs = svg.append("defs");
107
+
108
+ // 创建几种不同的纹理图案
109
+ const patternTypes = [
110
+ { id: "pattern1", d: "M5,0 l5,10 l-10,0 z" }, // 三角形
111
+ { id: "pattern2", d: "M0,0 l10,0 l0,10 l-10,0 z" }, // 方块
112
+ { id: "pattern3", d: "M0,5 a5,5 0 1,0 10,0 a5,5 0 1,0 -10,0" }, // 圆圈
113
+ { id: "pattern4", d: "M0,0 l5,5 l-5,5 z" }, // 另一种三角形
114
+ { id: "pattern5", d: "M0,0 l5,10 l5,-10 z" } // 菱形
115
+ ];
116
+
117
+ // 为每个类别创建唯一的纹理
118
+ const uniqueCategories = [...new Set(chartData.map(d => d[categoryField]))];
119
+ uniqueCategories.forEach((category, i) => {
120
+ const color = colorScale(category);
121
+ const patternType = patternTypes[i % patternTypes.length];
122
+
123
+ // 创建纹理图案
124
+ const pattern = defs.append("pattern")
125
+ .attr("id", `pattern-${category.replace(/\s+/g, '-').toLowerCase()}`)
126
+ .attr("width", 10)
127
+ .attr("height", 10)
128
+ .attr("patternUnits", "userSpaceOnUse")
129
+ .attr("patternTransform", "rotate(45)");
130
+
131
+ // 背景
132
+ pattern.append("rect")
133
+ .attr("width", 10)
134
+ .attr("height", 10)
135
+ .attr("fill", color)
136
+ .attr("fill-opacity", 0.75);
137
+
138
+ // 纹理元素
139
+ pattern.append("path")
140
+ .attr("d", patternType.d)
141
+ .attr("fill", "white")
142
+ .attr("fill-opacity", 0.2);
143
+ });
144
+
145
+ // 绘制多边形
146
+ const cells = g.selectAll("g")
147
+ .data(polygons)
148
+ .enter()
149
+ .append("g");
150
+
151
+ // 添��单元格
152
+ cells.append("path")
153
+ .attr("d", d => {
154
+ return "M" + d.join("L") + "Z";
155
+ })
156
+ .attr("fill", d => {
157
+ const categoryName = d.site.originalObject.data.originalData.name;
158
+ return `url(#pattern-${categoryName.replace(/\s+/g, '-').toLowerCase()})`;
159
+ })
160
+ .attr("stroke", d => colorScale(d.site.originalObject.data.originalData.name))
161
+ .attr("stroke-width", 1)
162
+ .attr("stroke-opacity", 0.5);
163
+
164
+ // 为每个单元格添加工具提示
165
+ const format = value => chartUtils.format.autoText(+value);
166
+
167
+ // 为足够大的区域添加图标
168
+ cells.each(function(d) {
169
+ // 计算多边形的边界框
170
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
171
+ d.forEach(point => {
172
+ minX = Math.min(minX, point[0]);
173
+ minY = Math.min(minY, point[1]);
174
+ maxX = Math.max(maxX, point[0]);
175
+ maxY = Math.max(maxY, point[1]);
176
+ });
177
+
178
+ const boxWidth = maxX - minX;
179
+ const boxHeight = maxY - minY;
180
+ const categoryValue = d.site.originalObject.data.originalData.name;
181
+
182
+ // 检查区域是否足够大以添加图标(至少50x50)
183
+ if (boxWidth >= 50 && boxHeight >= 50 && images.field && images.field[categoryValue]) {
184
+ const iconSize = 32;
185
+ const centerX = d3.polygonCentroid(d)[0];
186
+ const centerY = d3.polygonCentroid(d)[1] - 15; // 稍微上移,为标签留出空间
187
+
188
+ d3.select(this).append("image")
189
+ .attr("xlink:href", images.field[categoryValue])
190
+ .attr("x", centerX - iconSize/2)
191
+ .attr("y", centerY - iconSize/2)
192
+ .attr("width", iconSize)
193
+ .attr("height", iconSize);
194
+ }
195
+ });
196
+
197
+ // 添加文本标签
198
+ cells.append("text")
199
+ .attr("x", d => d3.polygonCentroid(d)[0])
200
+ .attr("y", d => d3.polygonCentroid(d)[1] + 20) // 下移标签,为图标留出空间
201
+ .attr("text-anchor", "middle")
202
+ .attr("dominant-baseline", "middle")
203
+ .attr("fill", "#fff")
204
+ .attr("font-size", "16px")
205
+ .attr("font-weight", "bold")
206
+ .text(d => d.site.originalObject.data.originalData.name)
207
+ .each(function(d) {
208
+ // 计算多边形的边界框
209
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
210
+ d.forEach(point => {
211
+ minX = Math.min(minX, point[0]);
212
+ minY = Math.min(minY, point[1]);
213
+ maxX = Math.max(maxX, point[0]);
214
+ maxY = Math.max(maxY, point[1]);
215
+ });
216
+
217
+ const boxWidth = maxX - minX;
218
+ const boxHeight = maxY - minY;
219
+
220
+ // 检查文本是否适合单元格
221
+ const textSelection = d3.select(this);
222
+ const textWidth = chartUtils.text.measure(null, this.textContent || "", {
223
+ fontFamily: "sans-serif",
224
+ fontSize: parseFloat(textSelection.attr("font-size")) || 12,
225
+ fontWeight: textSelection.attr("font-weight") || "normal"
226
+ }).width;
227
+
228
+ if (textWidth > boxWidth * 0.8 || boxHeight < 30) {
229
+ // 如果文本太长或单元格太小,在文本下方添加暗色透明框
230
+ // 创建一个新的g元素来包含背景框和文本
231
+ const textGroup = d3.select(this.parentNode)
232
+ .append("g")
233
+ .raise(); // 将整个组提升到最上层
234
+
235
+ // 添加背景框
236
+ const padding = 4;
237
+ textGroup.append("rect")
238
+ .attr("x", d3.polygonCentroid(d)[0] - textWidth/2 - padding)
239
+ .attr("y", d3.polygonCentroid(d)[1] - 10)
240
+ .attr("width", textWidth + padding * 2)
241
+ .attr("height", 35)
242
+ .attr("fill", "rgba(0,0,0,0.3)")
243
+ .attr("rx", 3);
244
+
245
+ // 将原始文本移动到新组中
246
+ d3.select(this).remove();
247
+ textGroup.append("text")
248
+ .attr("x", d3.polygonCentroid(d)[0])
249
+ .attr("y", d3.polygonCentroid(d)[1])
250
+ .attr("text-anchor", "middle")
251
+ .attr("dominant-baseline", "middle")
252
+ .attr("fill", "#fff")
253
+ .attr("font-size", "16px")
254
+ .attr("font-weight", "bold")
255
+ .text(d.site.originalObject.data.originalData.name);
256
+ }
257
+ });
258
+
259
+ // 添加值标签
260
+ cells.append("text")
261
+ .attr("x", d => d3.polygonCentroid(d)[0])
262
+ .attr("y", d => d3.polygonCentroid(d)[1] + 35) // 进一步下移值标签
263
+ .attr("text-anchor", "middle")
264
+ .attr("dominant-baseline", "middle")
265
+ .attr("fill", "#fff")
266
+ .attr("fill-opacity", 0.7)
267
+ .attr("font-size", "14px")
268
+ .text(d => format(d.site.originalObject.data.originalData.weight))
269
+ .each(function(d) {
270
+ // 计算多边形的边界框
271
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
272
+ d.forEach(point => {
273
+ minX = Math.min(minX, point[0]);
274
+ minY = Math.min(minY, point[1]);
275
+ maxX = Math.max(maxX, point[0]);
276
+ maxY = Math.max(maxY, point[1]);
277
+ });
278
+
279
+ const boxWidth = maxX - minX;
280
+ const boxHeight = maxY - minY;
281
+
282
+ // 检查文本是否适合单元格
283
+ const textSelection = d3.select(this);
284
+ const textWidth = chartUtils.text.measure(null, this.textContent || "", {
285
+ fontFamily: "sans-serif",
286
+ fontSize: parseFloat(textSelection.attr("font-size")) || 12,
287
+ fontWeight: textSelection.attr("font-weight") || "normal"
288
+ }).width;
289
+
290
+ if (textWidth > boxWidth * 0.8 || boxHeight < 40) {
291
+ // 如果文本太长或单元格太小,隐藏它
292
+ d3.select(this).style("display", "none");
293
+ }
294
+ });
295
+
296
+ return svg.node();
297
+ }
modules/chart_engine/template/d3-js/treemap/voronoi_treemap_rectangle_03.js ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Voronoi Treemap(Rectangle)",
5
+ "chart_name": "voronoi_treemap_rectangle_03",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 40], [0, "inf"]],
9
+ "required_fields_icons": ["x"],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": ["x"],
12
+ "required_other_colors": [],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 600,
16
+ "background": "light",
17
+ "icon_mark": "none",
18
+ "icon_label": "side",
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 width = variables.width;
45
+ const height = variables.height;
46
+ const initialMargin = { // 初始边距,图例将影响顶边距
47
+ top: variables.margin?.top ?? 10,
48
+ right: variables.margin?.right ?? 10,
49
+ bottom: variables.margin?.bottom ?? 10,
50
+ left: variables.margin?.left ?? 10
51
+ };
52
+
53
+ // 创建SVG
54
+ const svg = d3.select(containerSelector)
55
+ .append("svg")
56
+ .attr("width", "100%")
57
+ .attr("height", height)
58
+ .attr("viewBox", `0 0 ${width} ${height}`)
59
+ .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
60
+ .attr("xmlns", "http://www.w3.org/2000/svg")
61
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
62
+
63
+ // chartWidth is needed for legend wrapping calculation
64
+ const chartWidth = width - initialMargin.left - initialMargin.right;
65
+
66
+ // 准备数据 (This processedData and colorScale are used by the *correct* chart logic later)
67
+ const processedData = chartData.map(d => ({
68
+ name: d[categoryField],
69
+ weight: d[valueField]
70
+ }));
71
+
72
+ const colorScale = d => {
73
+ if (colorResolver.field(d, 0, { fallbackKey: "primary" }).value) {
74
+ return colorResolver.field(d, 0, { fallbackKey: "primary" }).value;
75
+ }
76
+ const localUniqueCategories = [...new Set(chartData.map(d => d[categoryField]))]; // Use local var to avoid conflict if uniqueCategories is defined globally for legend
77
+ return chartUtils.color.palette(localUniqueCategories.indexOf(d), { palette: "tableau10" });
78
+ };
79
+
80
+ // ---------- 新增图例 (uniqueCategories is defined here for legend) ----------
81
+ const uniqueCategories = [...new Set(chartData.map(d => d[categoryField]))];
82
+
83
+ // 辅助函数:使用 canvas 估算文本宽度
84
+ const canvas = document.createElement('canvas');
85
+ const ctx = canvas.getContext('2d');
86
+ function getTextWidthHelper(text, fontFamily, fontSize, fontWeight) {
87
+ ctx.font = `${fontWeight || 'normal'} ${fontSize}px ${fontFamily || 'Arial'}`;
88
+ return chartUtils.text.contextWidth(ctx, text);
89
+ }
90
+
91
+ // ---------- 图例计算与布局 (在主图表布局之前) ----------
92
+ let legendBlockHeight = 0;
93
+ const legendLines = [];
94
+ const paddingBelowLegendToChart = 15;
95
+ const minSvgGlobalTopPadding = 10; // SVG顶部到图例顶部的最小间距
96
+ let legendItemMaxHeight = 0; // 将在下面计算
97
+ let interLineVerticalPadding = parseFloat(typography.label?.line_spacing || '6'); // 图例行间距,移到外部并提供默认值
98
+ let legendInterItemSpacing = 10; // 默认的图例项间距,移到外部
99
+
100
+ if (uniqueCategories && uniqueCategories.length > 0 && images) { // 确保images已定义
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
+ interLineVerticalPadding = parseFloat(typography.label?.line_spacing || '6');
114
+
115
+
116
+ const legendItemsData = uniqueCategories.map(catName => {
117
+ const text = String(catName);
118
+ const color = colorScale(catName);
119
+ const iconUrl = images.field && images.field[catName] ? images.field[catName] : null;
120
+ const textWidth = getTextWidthHelper(text, legendFontFamily, legendFontSize, legendFontWeight);
121
+
122
+ let itemVisualWidth = legendColorRectWidth;
123
+ if (iconUrl) {
124
+ itemVisualWidth += legendPaddingRectIcon + legendIconWidth + legendPaddingIconText;
125
+ } else {
126
+ itemVisualWidth += legendPaddingRectIcon;
127
+ }
128
+ itemVisualWidth += textWidth;
129
+
130
+ return { text, color, iconUrl, textWidth, visualWidth: itemVisualWidth };
131
+ });
132
+
133
+ const legendLayout = chartUtils.legend.wrapItems(
134
+ legendItemsData.map(item => ({ ...item, width: item.visualWidth, height: legendItemMaxHeight })),
135
+ {
136
+ maxWidth: chartWidth,
137
+ itemGap: legendInterItemSpacing,
138
+ rowGap: interLineVerticalPadding,
139
+ itemHeight: legendItemMaxHeight,
140
+ }
141
+ );
142
+ legendLines.push(...legendLayout.rows.map(row => ({
143
+ items: row.items,
144
+ totalVisualWidth: row.width,
145
+ })));
146
+ legendBlockHeight = legendLayout.height;
147
+ }
148
+
149
+ // ---------- 根据图例调整边距和图表尺寸 ----------
150
+ let effectiveMarginTop;
151
+ let legendStartY = minSvgGlobalTopPadding; // 图例将从这里开始绘制Y坐标
152
+
153
+ if (legendBlockHeight > 0) {
154
+ // 图例存在,主图表内容在其下方
155
+ effectiveMarginTop = legendStartY + legendBlockHeight + paddingBelowLegendToChart;
156
+ } else {
157
+ // 没有图例,使用初始上边距或最小上边距
158
+ effectiveMarginTop = Math.max(initialMargin.top, minSvgGlobalTopPadding);
159
+ }
160
+
161
+ // 现在计算主绘图区域的 chartHeight
162
+ const chartHeight = height - effectiveMarginTop - initialMargin.bottom;
163
+
164
+ if (chartHeight <= 0) {
165
+ console.warn("Voronoi Treemap: Chart height is not positive after accommodating legend and margins. SVG might be too short.");
166
+ // You might want to stop execution or display an error if chartHeight is not positive
167
+ // For example:
168
+ // d3.select(containerSelector).html("<p style='color:red;'>Error: Not enough height for the chart and legend.</p>");
169
+ // return null; // Or svg.node() if you still want to return an empty SVG
170
+ }
171
+
172
+ // ---------- 绘制图例 (如果存在) ----------
173
+ if (legendBlockHeight > 0 && legendLines.length > 0) {
174
+ const legendContainerGroup = svg.append("g")
175
+ .attr("class", "custom-legend-container")
176
+ .attr("transform", `translate(0, ${legendStartY})`); // 图例容器的Y偏移
177
+
178
+ let currentLineBaseY = 0; // Y坐标相对于 legendContainerGroup
179
+
180
+ // 在图例绘制作用域内重新获取或确认常量值,以确保它们是可用的
181
+ const currentLegendInterItemSpacing = legendInterItemSpacing; // 使用已在外部定义的 legendInterItemSpacing
182
+
183
+ legendLines.forEach((line) => {
184
+ // 每行在 chartWidth 内水平居中,因此X起始位置需要加上 initialMargin.left
185
+ const lineRenderStartX = initialMargin.left + (chartWidth - line.totalVisualWidth) / 2;
186
+ const lineCenterY = currentLineBaseY + legendItemMaxHeight / 2;
187
+ let currentItemDrawX = lineRenderStartX;
188
+
189
+ const legendColorRectWidth = 12; // 从外部作用域或重新定义
190
+ const legendColorRectHeight = 12;
191
+ const legendIconWidth = typography.label?.icon_size || 16;
192
+ const legendIconHeight = typography.label?.icon_size || 16;
193
+ const legendPaddingRectIcon = 4;
194
+ const legendPaddingIconText = 4;
195
+ const legendFontFamily = typography.label?.font_family || 'Arial';
196
+ const legendFontSize = parseFloat(typography.label?.font_size || '12');
197
+ const legendFontWeight = typography.label?.font_weight || 'normal';
198
+
199
+
200
+ line.items.forEach((item, itemIndex) => {
201
+ legendContainerGroup.append("rect")
202
+ .attr("x", currentItemDrawX)
203
+ .attr("y", currentLineBaseY + (legendItemMaxHeight - legendColorRectHeight) / 2)
204
+ .attr("width", legendColorRectWidth)
205
+ .attr("height", legendColorRectHeight)
206
+ .attr("fill", item.color)
207
+ .attr("fill-opacity", 0.85);
208
+ currentItemDrawX += legendColorRectWidth;
209
+
210
+ if (item.iconUrl) {
211
+ currentItemDrawX += legendPaddingRectIcon;
212
+ legendContainerGroup.append("image")
213
+ .attr("xlink:href", item.iconUrl)
214
+ .attr("x", currentItemDrawX)
215
+ .attr("y", currentLineBaseY + (legendItemMaxHeight - legendIconHeight) / 2)
216
+ .attr("width", legendIconWidth)
217
+ .attr("height", legendIconHeight)
218
+ .attr("preserveAspectRatio", "xMidYMid meet");
219
+ currentItemDrawX += legendIconWidth;
220
+ currentItemDrawX += legendPaddingIconText;
221
+ } else {
222
+ currentItemDrawX += legendPaddingRectIcon;
223
+ }
224
+
225
+ legendContainerGroup.append("text")
226
+ .attr("x", currentItemDrawX)
227
+ .attr("y", lineCenterY)
228
+ .attr("dominant-baseline", "middle")
229
+ .style("font-family", legendFontFamily)
230
+ .style("font-size", `${legendFontSize}px`)
231
+ .style("font-weight", legendFontWeight)
232
+ .style("fill", colorResolver.text({ fallback: "#333333" }).value || typography.label?.font_color || "#333333")
233
+ .text(item.text);
234
+
235
+ currentItemDrawX += item.textWidth;
236
+
237
+ if (itemIndex < line.items.length - 1) {
238
+ currentItemDrawX += (line.items[itemIndex+1].visualWidth > 0 ? (currentLegendInterItemSpacing || 10) : 0) ; // 使用在当前作用域确认的 currentLegendInterItemSpacing
239
+ }
240
+ });
241
+ currentLineBaseY += legendItemMaxHeight + interLineVerticalPadding;
242
+ });
243
+ }
244
+
245
+ // ---------- 创建主图表绘图区域 (g) ----------
246
+ const g = svg.append("g")
247
+ .attr("transform", `translate(${initialMargin.left}, ${effectiveMarginTop})`);
248
+
249
+ // 定义裁剪多边形(矩形)- 使用计算后的 chartWidth 和 chartHeight
250
+ const clip = [
251
+ [0, 0],
252
+ [0, chartHeight],
253
+ [chartWidth, chartHeight],
254
+ [chartWidth, 0]
255
+ ];
256
+
257
+ // 创建 Voronoi Map 模拟
258
+ const simulation = d3.voronoiMapSimulation(processedData)
259
+ .weight(d => d.weight)
260
+ .clip(clip)
261
+ .stop();
262
+
263
+ // 运行模拟直到结束
264
+ let state = simulation.state();
265
+ while (!state.ended) {
266
+ simulation.tick();
267
+ state = simulation.state();
268
+ }
269
+
270
+ // 获取最终的多边形
271
+ const polygons = state.polygons;
272
+
273
+ // 绘制多边形
274
+ const cells = g.selectAll("g")
275
+ .data(polygons)
276
+ .enter()
277
+ .append("g");
278
+
279
+ // 添加单元格
280
+ cells.append("path")
281
+ .attr("d", d => {
282
+ return "M" + d.join("L") + "Z";
283
+ })
284
+ .attr("fill", d => colorScale(d.site.originalObject.data.originalData.name))
285
+ .attr("fill-opacity", 0.8)
286
+ .attr("stroke", "none");
287
+
288
+ // 为每个单元格添加工具提示 (格式化函数)
289
+ const format = value => chartUtils.format.autoText(+value);
290
+
291
+ // 修改值标签,使其成为单元格中唯一的主要文本
292
+ cells.append("text")
293
+ .attr("class", "value-label-cell")
294
+ .attr("x", d => d3.polygonCentroid(d)[0])
295
+ .attr("y", d => d3.polygonCentroid(d)[1]) // 居中Y
296
+ .attr("text-anchor", "middle")
297
+ .attr("dominant-baseline", "middle") // 垂直居中
298
+ .attr("fill", "#ffffff") // 使用主题或默认白色
299
+ .attr("font-size", typography.label?.font_size || "16px") // 字体稍大
300
+ .attr("font-weight", typography.label?.font_weight || "bold") // 加粗
301
+ .text(d => format(d.site.originalObject.data.originalData.weight))
302
+ .each(function(d) {
303
+ // 计算多边形的边界框
304
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
305
+ d.forEach(point => {
306
+ minX = Math.min(minX, point[0]);
307
+ minY = Math.min(minY, point[1]);
308
+ maxX = Math.max(maxX, point[0]);
309
+ maxY = Math.max(maxY, point[1]);
310
+ });
311
+
312
+ const boxWidth = maxX - minX;
313
+ const boxHeight = maxY - minY;
314
+
315
+ // 检查文本是否适合单元格
316
+ const textSelection = d3.select(this);
317
+ const textWidth = chartUtils.text.measure(null, this.textContent || "", {
318
+ fontFamily: "sans-serif",
319
+ fontSize: parseFloat(textSelection.attr("font-size")) || 12,
320
+ fontWeight: textSelection.attr("font-weight") || "normal"
321
+ }).width;
322
+ const textHeight = parseFloat(this.getAttribute("font-size")); // 获取字体大小
323
+
324
+ if (textWidth > boxWidth * 0.9 || textHeight > boxHeight * 0.8) { // 检查宽度和高度
325
+ // 如果文本太长或太高,隐藏它
326
+ d3.select(this).style("display", "none");
327
+ }
328
+ });
329
+
330
+ return svg.node();
331
+ }