Ray1ee01 commited on
Commit
6ef89e2
·
verified ·
1 Parent(s): bd556a3

Upload folder using huggingface_hub

Browse files
modules/chart_engine/template/d3-js/type60_small_multiples_of_radial_line_charts/small_multiple_radial_line_plain_chart_01.js ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Small Multiples of Radial Spline Charts",
5
+ "chart_name": "small_multiple_radial_spline_plain_chart_01",
6
+ "is_composite": false,
7
+ "required_fields": ["group", "x", "y"],
8
+ "required_fields_type": [["categorical"], ["categorical"], ["numerical"]],
9
+ "required_fields_range": [[2, 7], [3, 12], [0, "inf"]],
10
+ "required_fields_icons": [],
11
+ "required_other_icons": [],
12
+ "required_fields_colors": ["x"],
13
+ "required_other_colors": ["primary"],
14
+ "supported_effects": [],
15
+ "min_height": 400,
16
+ "min_width": 400,
17
+ "background": "no",
18
+ "icon_mark": "none",
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
+ const colorResolver = chartUtils.color.resolver(data);
28
+ // 计算文本宽度
29
+ const measureTextWidth = (text, fontSize) => {
30
+ return chartUtils.text.measure(null, text, { fontSize: fontSize }).width;
31
+ };
32
+
33
+ // 数据准备
34
+ const jsonData = data;
35
+ const chartData = jsonData.data.data;
36
+ const variables = jsonData.variables || {};
37
+ const typography = jsonData.typography || {
38
+ title: { font_family: "Arial", font_size: "18px", font_weight: "bold" },
39
+ label: { font_family: "Arial", font_size: "14px", font_weight: "normal" },
40
+ annotation: { font_family: "Arial", font_size: "12px", font_weight: "normal" }
41
+ };
42
+ const colors = jsonData.colors || {
43
+ text_color: "#333333",
44
+ field: {},
45
+ other: { primary: "#1f77b4" }
46
+ };
47
+ const dataColumns = chartUtils.schema.columns(jsonData);
48
+
49
+ // 清空容器
50
+ d3.select(containerSelector).html("");
51
+
52
+ // 尺寸和布局设置
53
+ const width = variables.width || 800;
54
+ const height = variables.height || 600;
55
+ const margin = { top: 120, right: 50, bottom: 80, left: 50 };
56
+
57
+ // 提取字段名
58
+ const groupField = chartUtils.schema.channel(jsonData, "group").key || chartUtils.schema.columnField(dataColumns, 0);
59
+ const categoryField = chartUtils.schema.channel(jsonData, "x").key || chartUtils.schema.columnField(dataColumns, 1);
60
+ const valueField = chartUtils.schema.channel(jsonData, "y").key || chartUtils.schema.columnField(dataColumns, 2);
61
+
62
+ // 数据处理
63
+ let groups = [...new Set(chartData.map(d => d[groupField]))];
64
+ if (groups.length > 6) groups = groups.slice(0, 6);
65
+
66
+ const groupedData = {};
67
+ const groupMaxValues = {};
68
+ groups.forEach(group => {
69
+ groupedData[group] = chartData.filter(d => d[groupField] === group);
70
+ groupMaxValues[group] = d3.max(groupedData[group], d => +d[valueField]);
71
+ });
72
+
73
+ const sortedGroups = [...groups].sort((a, b) => groupMaxValues[b] - groupMaxValues[a]);
74
+ const allValues = chartData.map(d => +d[valueField]);
75
+ const maxValue = d3.max(allValues);
76
+
77
+ // 获取所有类别并创建颜色比例尺
78
+ let allCategories = [];
79
+ groups.forEach(group => {
80
+ const categoriesInGroup = groupedData[group].map(d => d[categoryField]);
81
+ allCategories = [...allCategories, ...categoriesInGroup];
82
+ });
83
+ allCategories = [...new Set(allCategories)];
84
+
85
+ // 创建颜色比例尺
86
+ const categoryColorScale = colorResolver.scale(allCategories, { palette: "category10" });
87
+
88
+ // 创建SVG
89
+ const svg = d3.select(containerSelector)
90
+ .append("svg")
91
+ .attr("width", "100%")
92
+ .attr("height", height)
93
+ .attr("viewBox", `0 0 ${width} ${height}`)
94
+ .attr("style", "max-width: 100%; height: auto;")
95
+ .attr("xmlns", "http://www.w3.org/2000/svg")
96
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
97
+
98
+ const legendLayoutInfo = chartUtils.legend.layout(allCategories, {
99
+ color: cat => categoryColorScale(cat),
100
+ maxWidth: width - 100,
101
+ align: "center",
102
+ context: svg,
103
+ markerSize: 8,
104
+ labelGap: 6,
105
+ itemGap: 15,
106
+ rowGap: 8,
107
+ fontSize: 11,
108
+ fontFamily: typography.label.font_family,
109
+ });
110
+ chartUtils.legend.draw(svg, allCategories, {
111
+ color: cat => categoryColorScale(cat),
112
+ }, {
113
+ x: (width - legendLayoutInfo.width) / 2,
114
+ y: 20,
115
+ maxWidth: width - 100,
116
+ align: "center",
117
+ markerShape: "circle",
118
+ markerSize: 8,
119
+ labelGap: 6,
120
+ itemGap: 15,
121
+ rowGap: 8,
122
+ fontSize: 11,
123
+ fontFamily: typography.label.font_family,
124
+ textColor: colorResolver.text({ fallback: "#333333" }).value,
125
+ });
126
+ margin.top = 20 + legendLayoutInfo.height + 30;
127
+
128
+ // 计算网格布局
129
+ const numCharts = groups.length;
130
+ let rows_layout, cols;
131
+ if (numCharts <= 3) {
132
+ rows_layout = 1; cols = numCharts;
133
+ } else if (numCharts === 4) {
134
+ rows_layout = 2; cols = 2;
135
+ } else if (numCharts <= 8) {
136
+ rows_layout = 2; cols = Math.ceil(numCharts / 2);
137
+ } else {
138
+ rows_layout = 3; cols = Math.ceil(numCharts / 3);
139
+ }
140
+
141
+ const chartAreaWidth = width - margin.left - margin.right;
142
+ const chartAreaHeight = height - margin.top - margin.bottom;
143
+ const cellWidth = chartAreaWidth / cols;
144
+ const cellHeight = chartAreaHeight / rows_layout;
145
+
146
+ // 测量最长组标签
147
+ const groupFontSize = parseFloat(typography.label.font_size);
148
+ let maxGroupWidth = 0;
149
+ groups.forEach(group => {
150
+ const groupWidth = measureTextWidth(group, groupFontSize);
151
+ maxGroupWidth = Math.max(maxGroupWidth, groupWidth);
152
+ });
153
+
154
+ // 计算间距和半径
155
+ const innerCellWidth = cellWidth * 0.85;
156
+ const innerCellHeight = cellHeight * 0.85;
157
+ const radius = Math.min(innerCellWidth, innerCellHeight) / 2.2;
158
+
159
+ // 调整字体大小
160
+ const maxChartAreaWidth = radius * 2.4;
161
+ let groupScaleFactor = 1;
162
+ if (maxGroupWidth > maxChartAreaWidth) {
163
+ groupScaleFactor = maxChartAreaWidth / (maxGroupWidth + 3);
164
+ }
165
+ const adjustedGroupFontSize = `${Math.floor(groupFontSize * groupScaleFactor)}px`;
166
+
167
+ // 创建比例尺
168
+ const angleScale = d3.scalePoint()
169
+ .domain(allCategories)
170
+ .range([0, 2 * Math.PI - (2 * Math.PI / allCategories.length)]);
171
+
172
+ const radiusScale = d3.scaleLinear()
173
+ .domain([0, maxValue * 1.2])
174
+ .range([0, radius])
175
+ .nice();
176
+
177
+ // 雷达样条曲线生成器
178
+ const createSplineGenerator = (groupData) => {
179
+ return () => {
180
+ const points = allCategories.map(cat => {
181
+ const point = groupData.find(item => item[categoryField] === cat);
182
+ if (point) {
183
+ const angle = angleScale(cat) - Math.PI/2;
184
+ const distance = radiusScale(+point[valueField]);
185
+ return [distance * Math.cos(angle), distance * Math.sin(angle)];
186
+ }
187
+ return [0, 0];
188
+ });
189
+ return d3.line()
190
+ .curve(d3.curveCardinalClosed.tension(0.5))(points);
191
+ };
192
+ };
193
+
194
+ // 创建雷达图
195
+ let groupIndex = 0;
196
+ for (let row = 0; row < rows_layout; row++) {
197
+ const itemsInThisRow = row < rows_layout - 1 ? cols : numCharts - cols * (rows_layout - 1);
198
+ const rowOffset = (cols - itemsInThisRow) * cellWidth / 2;
199
+
200
+ for (let col = 0; col < itemsInThisRow; col++) {
201
+ if (groupIndex >= numCharts) break;
202
+
203
+ const group = sortedGroups[groupIndex];
204
+ const groupData = groupedData[group];
205
+
206
+ const chartCenterX = margin.left + rowOffset + (col + 0.5) * cellWidth;
207
+ const chartCenterY = margin.top + (row + 0.5) * cellHeight;
208
+
209
+ const chartGroup = svg.append("g")
210
+ .attr("transform", `translate(${chartCenterX}, ${chartCenterY})`);
211
+
212
+ // 绘制同心圆网格
213
+ const ticks = radiusScale.ticks(4);
214
+ chartGroup.selectAll(".circle-grid")
215
+ .data(ticks.slice(1))
216
+ .enter()
217
+ .append("circle")
218
+ .attr("class", "gridline")
219
+ .attr("cx", 0).attr("cy", 0)
220
+ .attr("r", d => radiusScale(d))
221
+ .attr("fill", "none")
222
+ .attr("stroke", colorResolver.text({ fallback: "#333" }).value)
223
+ .attr("stroke-width", 1)
224
+ .attr("stroke-dasharray", "3,3")
225
+ .attr("opacity", 0.3);
226
+
227
+ // 绘制径向轴线
228
+ chartGroup.selectAll(".axis-line")
229
+ .data(allCategories)
230
+ .enter()
231
+ .append("line")
232
+ .attr("class", "axis")
233
+ .attr("x1", 0).attr("y1", 0)
234
+ .attr("x2", d => radius * Math.cos(angleScale(d) - Math.PI/2))
235
+ .attr("y2", d => radius * Math.sin(angleScale(d) - Math.PI/2))
236
+ .attr("stroke", colorResolver.text({ fallback: "#333" }).value)
237
+ .attr("stroke-width", 1)
238
+ .attr("opacity", 0.5);
239
+
240
+ // 绘制雷达样条曲线(平滑曲线,无填充)
241
+ const splineGenerator = createSplineGenerator(groupData);
242
+ chartGroup.append("path")
243
+ .attr("class", "mark")
244
+ .attr("d", splineGenerator())
245
+ .attr("fill", "none")
246
+ .attr("stroke", colorResolver.other("primary").value)
247
+ .attr("stroke-width", 1.5)
248
+ .attr("stroke-linejoin", "round");
249
+
250
+ // 绘制数据点和标签
251
+ allCategories.forEach(cat => {
252
+ const point = groupData.find(item => item[categoryField] === cat);
253
+ if (point) {
254
+ const angle = angleScale(cat) - Math.PI/2;
255
+ const distance = radiusScale(+point[valueField]);
256
+ const pointColor = categoryColorScale(cat);
257
+
258
+ // 数据点
259
+ chartGroup.append("circle")
260
+ .attr("class", "mark")
261
+ .attr("cx", distance * Math.cos(angle))
262
+ .attr("cy", distance * Math.sin(angle))
263
+ .attr("r", 3)
264
+ .attr("fill", pointColor)
265
+ .attr("stroke", "#fff")
266
+ .attr("stroke-width", 1);
267
+
268
+ // 数值标签(所有点都添加,放在径向外侧)
269
+ const labelText = chartUtils.format.number(point[valueField]).text;
270
+ const labelDistance = Math.max(distance + 12, 24);
271
+ const textX = labelDistance * Math.cos(angle);
272
+ const textY = labelDistance * Math.sin(angle);
273
+
274
+ chartGroup.append("text")
275
+ .attr("class", "value")
276
+ .attr("x", textX).attr("y", textY)
277
+ .attr("text-anchor", "middle")
278
+ .attr("dominant-baseline", "middle")
279
+ .attr("font-size", typography.annotation.font_size)
280
+ .attr("font-weight", "bold")
281
+ .attr("fill", pointColor)
282
+ .text(labelText);
283
+ }
284
+ });
285
+
286
+ // 添加组标题
287
+ chartGroup.append("text")
288
+ .attr("class", "label")
289
+ .attr("x", 0).attr("y", -radius - 20)
290
+ .attr("text-anchor", "middle")
291
+ .style("font-family", typography.label.font_family)
292
+ .style("font-size", adjustedGroupFontSize)
293
+ .style("font-weight", "bold")
294
+ .style("fill", colorResolver.text({ fallback: "#333" }).value)
295
+ .text(group);
296
+
297
+ groupIndex++;
298
+ }
299
+ }
300
+
301
+ return svg.node();
302
+ }