Ray1ee01 commited on
Commit
86655e8
·
verified ·
1 Parent(s): 88c106a

Upload folder using huggingface_hub

Browse files
modules/chart_engine/template/d3-js/type38_radial_area_chart/radial_area_chart_grid_01.js ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Radial Area Chart",
5
+ "chart_name": "radial_area_chart_grid_01",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 12], [0, "inf"]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": [],
12
+ "required_other_colors": ["primary"],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 400,
16
+ "background": "light",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "no",
20
+ "has_y_axis": "no"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+ function makeChart(containerSelector, data) {
26
+ // 内联utils函数
27
+ const measureTextWidth = (text, fontSize) => {
28
+ return chartUtils.text.measure(null, text, { fontSize: fontSize }).width;
29
+ };
30
+
31
+ // 提取数据
32
+ const jsonData = data;
33
+ const chartData = jsonData.data.data;
34
+ const variables = jsonData.variables;
35
+ const colors = jsonData.colors || {};
36
+ const colorResolver = chartUtils.color.resolver(jsonData);
37
+ const dataColumns = chartUtils.schema.columns(jsonData);
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: 50, right: 50, bottom: 50, left: 50 };
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;")
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
+ const radius = Math.min(chartWidth, chartHeight) / 2;
65
+
66
+ const g = svg.append("g")
67
+ .attr("transform", `translate(${width/2}, ${height/2})`);
68
+
69
+ // 获取唯一类别和主色调
70
+ const categories = [...new Set(chartData.map(d => d[categoryField]))];
71
+ const mainColor = colorResolver.other("primary", { fallback: "#1f77b4" }).value;
72
+
73
+ // 创建比例尺
74
+ const angleScale = d3.scalePoint()
75
+ .domain(categories)
76
+ .range([0, 2 * Math.PI - (2 * Math.PI / categories.length)]);
77
+
78
+ const allValues = chartData.map(d => +d[valueField]);
79
+ const radiusScale = d3.scaleLinear()
80
+ .domain([Math.min(0, d3.min(allValues)), d3.max(allValues) * 1.2])
81
+ .range([0, radius])
82
+ .nice();
83
+
84
+ // 绘制网格线
85
+ const ticks = radiusScale.ticks(5);
86
+
87
+ // 绘制同心圆网格
88
+ g.selectAll(".gridline")
89
+ .data(ticks)
90
+ .enter()
91
+ .append("circle")
92
+ .attr("class", "gridline")
93
+ .attr("cx", 0)
94
+ .attr("cy", 0)
95
+ .attr("r", d => radiusScale(d))
96
+ .attr("fill", "none")
97
+ .attr("stroke", "#bbb")
98
+ .attr("stroke-width", 1)
99
+ .attr("stroke-dasharray", "4,4");
100
+
101
+ // 绘制径向轴线
102
+ g.selectAll(".axis")
103
+ .data(categories)
104
+ .enter()
105
+ .append("line")
106
+ .attr("class", "axis")
107
+ .attr("x1", 0)
108
+ .attr("y1", 0)
109
+ .attr("x2", d => radius * Math.cos(angleScale(d) - Math.PI/2))
110
+ .attr("y2", d => radius * Math.sin(angleScale(d) - Math.PI/2))
111
+ .attr("stroke", "#bbb")
112
+ .attr("stroke-width", 1);
113
+
114
+ // 添加类别标签
115
+ g.selectAll(".label")
116
+ .data(categories)
117
+ .enter()
118
+ .append("text")
119
+ .attr("class", "label")
120
+ .attr("x", d => (radius + 20) * Math.cos(angleScale(d) - Math.PI/2))
121
+ .attr("y", d => (radius + 20) * Math.sin(angleScale(d) - Math.PI/2))
122
+ .attr("text-anchor", d => {
123
+ const angle = angleScale(d);
124
+ if (Math.abs(angle) < 0.1 || Math.abs(angle - Math.PI) < 0.1) return "middle";
125
+ return angle > Math.PI ? "end" : "start";
126
+ })
127
+ .attr("dominant-baseline", d => {
128
+ const angle = angleScale(d);
129
+ if (Math.abs(angle) < 0.1 || Math.abs(angle - Math.PI) < 0.1) return "middle";
130
+ return angle < Math.PI ? "hanging" : "auto";
131
+ })
132
+ .attr("fill", "#333")
133
+ .attr("font-size", "16px")
134
+ .attr("font-weight", "bold")
135
+ .text(d => d);
136
+
137
+ // 添加刻度值标签
138
+ g.selectAll(".value")
139
+ .data(ticks)
140
+ .enter()
141
+ .append("text")
142
+ .attr("class", "value")
143
+ .attr("x", 5)
144
+ .attr("y", d => -radiusScale(d))
145
+ .attr("text-anchor", "start")
146
+ .attr("font-size", "14px")
147
+ .attr("fill", "#666")
148
+ .text(d => d);
149
+
150
+ // 创建���达路径
151
+ const points = categories.map(cat => {
152
+ const point = chartData.find(item => item[categoryField] === cat);
153
+ if (point) {
154
+ const angle = angleScale(cat) - Math.PI/2;
155
+ const distance = radiusScale(+point[valueField]);
156
+ return [distance * Math.cos(angle), distance * Math.sin(angle)];
157
+ }
158
+ return [0, 0];
159
+ });
160
+
161
+ // 绘制雷达面积
162
+ g.append("path")
163
+ .attr("class", "mark")
164
+ .attr("d", d3.line()(points) + "Z")
165
+ .attr("fill", mainColor)
166
+ .attr("fill-opacity", 0.2)
167
+ .attr("stroke", mainColor)
168
+ .attr("stroke-width", 6)
169
+ .attr("stroke-linejoin", "miter");
170
+
171
+ // 绘制数据点和数值标签
172
+ categories.forEach((cat, index) => {
173
+ const point = chartData.find(item => item[categoryField] === cat);
174
+ if (point) {
175
+ const angle = angleScale(cat) - Math.PI/2;
176
+ const distance = radiusScale(+point[valueField]);
177
+ const x = distance * Math.cos(angle);
178
+ const y = distance * Math.sin(angle);
179
+
180
+ // 数据点
181
+ g.append("circle")
182
+ .attr("class", "mark")
183
+ .attr("cx", x)
184
+ .attr("cy", y)
185
+ .attr("r", 6)
186
+ .attr("fill", mainColor)
187
+ .attr("stroke", "#fff")
188
+ .attr("stroke-width", 3);
189
+
190
+ // 数值标签
191
+ const labelText = chartUtils.format.number(point[valueField]).text;
192
+ const textWidth = measureTextWidth(labelText, 14);
193
+ const textX = index === 0 ? (distance + 30) * Math.cos(angle) - 20 : (distance + 30) * Math.cos(angle);
194
+ const textY = index === 0 ? (distance + 15) * Math.sin(angle) : (distance + 30) * Math.sin(angle);
195
+
196
+ // 标签背景
197
+ g.append("rect")
198
+ .attr("class", "background")
199
+ .attr("x", textX - textWidth/2 - 4)
200
+ .attr("y", textY - 8)
201
+ .attr("width", textWidth + 8)
202
+ .attr("height", 16)
203
+ .attr("fill", mainColor)
204
+ .attr("rx", 3);
205
+
206
+ // 标签文字
207
+ g.append("text")
208
+ .attr("class", "value")
209
+ .attr("x", textX)
210
+ .attr("y", textY)
211
+ .attr("text-anchor", "middle")
212
+ .attr("dominant-baseline", "middle")
213
+ .attr("font-size", "14px")
214
+ .attr("fill", "#fff")
215
+ .text(labelText);
216
+ }
217
+ });
218
+
219
+ return svg.node();
220
+ }
modules/chart_engine/template/d3-js/type38_radial_area_chart/radial_area_plain_chart_01.js ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Radial Area Chart",
5
+ "chart_name": "radial_area_plain_chart_01",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["categorical"], ["numerical"]],
8
+ "required_fields_range": [[3, 12], [0, "inf"]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": [],
12
+ "required_other_colors": ["primary"],
13
+ "supported_effects": [],
14
+ "min_height": 400,
15
+ "min_width": 400,
16
+ "background": "light",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "no",
20
+ "has_y_axis": "no"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+ function makeChart(containerSelector, data) {
26
+ const jsonData = data || {};
27
+ const sourceData = jsonData.data?.data || [];
28
+ const variables = jsonData.variables || {};
29
+ const typography = jsonData.typography || {};
30
+ const sourceColors = jsonData.colors || {};
31
+ const dataColumns = chartUtils.schema.columns(jsonData);
32
+ const colorResolver = chartUtils.color.resolver(jsonData);
33
+ const chartUtilsFormatSample = chartUtils.format.autoText;
34
+ const chartUtilsTextSample = chartUtils.text.estimate;
35
+ const chartUtilsStandard = {
36
+ schema: chartUtils.schema,
37
+ format: chartUtils.format,
38
+ text: chartUtils.text,
39
+ color: chartUtils.color,
40
+ legendLayout: chartUtils.legend.layout,
41
+ legendDraw: chartUtils.legend.draw,
42
+ random: chartUtils.random.generator(jsonData, "dev-wz-style")
43
+ };
44
+ const standardChannels = chartUtils.schema.channels(jsonData, {
45
+ x: { fallbackIndex: 0 },
46
+ y: { fallbackIndex: 1 },
47
+ y2: { fallbackIndex: 2 },
48
+ y3: { fallbackIndex: 3 },
49
+ size: { fallbackIndex: 2 },
50
+ group: { fallbackIndex: 2 },
51
+ group2: { fallbackIndex: 3 },
52
+ group3: { fallbackIndex: 4 }
53
+ });
54
+
55
+ d3.select(containerSelector).html("");
56
+
57
+ const categoryField = chartUtils.schema.column(dataColumns, 0).raw?.name;
58
+ const valueField = chartUtils.schema.column(dataColumns, 1).raw?.name;
59
+ const categoryType = chartUtils.schema.column(dataColumns, 0).raw?.data_type || "categorical";
60
+ const categoryUnit = (chartUtils.schema.column(dataColumns, 0).raw?.unit || "").trim();
61
+ const valueUnit = (chartUtils.schema.column(dataColumns, 1).raw?.unit || "").trim();
62
+ const width = variables.width || 600;
63
+ const height = variables.height || 600;
64
+
65
+ const fontFamily = typography.label?.font_family || typography.title?.font_family || "Arial, sans-serif";
66
+ const titleFont = typography.title?.font_family || fontFamily;
67
+ const textColor = sourceColors.text_color || "#1f2937";
68
+ const mutedText = "#6b7280";
69
+ const gridColor = "#d8e2ea";
70
+ const mainColor = sourceColors.other?.primary || "#2563eb";
71
+
72
+ const readableName = value => String(value || "")
73
+ .replace(/_/g, " ")
74
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
75
+ .replace(/\s+/g, " ")
76
+ .trim();
77
+
78
+ const categoryLabel = readableName(chartUtils.schema.column(dataColumns, 0).raw?.label || chartUtils.schema.column(dataColumns, 0).raw?.name || categoryField);
79
+ const valueLabel = readableName(chartUtils.schema.column(dataColumns, 1).raw?.label || chartUtils.schema.column(dataColumns, 1).raw?.name || valueField);
80
+
81
+ const svg = d3.select(containerSelector)
82
+ .append("svg")
83
+ .attr("width", "100%")
84
+ .attr("height", height)
85
+ .attr("viewBox", `0 0 ${width} ${height}`)
86
+ .attr("style", "max-width: 100%; height: auto;")
87
+ .attr("xmlns", "http://www.w3.org/2000/svg")
88
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink")
89
+ .attr("class", "radial-area-chart-root");
90
+
91
+ if (!categoryField || !valueField) {
92
+ svg.append("text")
93
+ .attr("x", width / 2)
94
+ .attr("y", height / 2)
95
+ .attr("text-anchor", "middle")
96
+ .attr("fill", textColor)
97
+ .style("font-family", fontFamily)
98
+ .style("font-size", "16px")
99
+ .text("Missing radial area fields");
100
+ return svg.node();
101
+ }
102
+
103
+ const parseTemporal = value => {
104
+ if (value instanceof Date) return value;
105
+ const numeric = Number(value);
106
+ if (Number.isFinite(numeric) && String(value).trim().length <= 4) {
107
+ return new Date(numeric, 0, 1);
108
+ }
109
+ const parsed = new Date(value);
110
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
111
+ };
112
+
113
+ const compactNumber = value => {
114
+ const abs = Math.abs(value);
115
+ const spec = abs >= 100 ? ".0f" : abs >= 10 ? ".1f" : ".2f";
116
+ if (abs >= 1000000000) return `${d3.format(spec)(value / 1000000000)}B`;
117
+ if (abs >= 1000000) return `${d3.format(spec)(value / 1000000)}M`;
118
+ if (abs >= 1000) return `${d3.format(spec)(value / 1000)}K`;
119
+ return d3.format(spec)(value).replace(/\.0$/, "");
120
+ };
121
+
122
+ const formatLocalValue = value => {
123
+ const formatted = compactNumber(value);
124
+ const currencyWithSuffix = valueUnit.match(/^([£$€¥])\s*([A-Za-z]+)$/);
125
+ if (valueUnit === "%") return `${formatted}%`;
126
+ if (currencyWithSuffix) return `${currencyWithSuffix[1]}${formatted}${currencyWithSuffix[2]}`;
127
+ if (/^[£$€¥]$/.test(valueUnit)) return `${valueUnit}${formatted}`;
128
+ if (valueUnit) return `${formatted} ${valueUnit}`;
129
+ return formatted;
130
+ };
131
+
132
+ const categoryMap = new Map();
133
+ sourceData.forEach(d => {
134
+ const category = String(d[categoryField]);
135
+ const value = Number(d[valueField]);
136
+ if (!category || !Number.isFinite(value) || value < 0) return;
137
+ if (!categoryMap.has(category)) {
138
+ categoryMap.set(category, { rawCategory: d[categoryField], values: [] });
139
+ }
140
+ categoryMap.get(category).values.push(value);
141
+ });
142
+ const grouped = Array.from(categoryMap.entries()).map(([category, entry]) => {
143
+ const value = d3.mean(entry.values);
144
+ return {
145
+ category,
146
+ rawCategory: entry.rawCategory,
147
+ value
148
+ };
149
+ });
150
+
151
+ if (grouped.length < 3) {
152
+ svg.append("text")
153
+ .attr("x", width / 2)
154
+ .attr("y", height / 2)
155
+ .attr("text-anchor", "middle")
156
+ .attr("fill", textColor)
157
+ .style("font-family", fontFamily)
158
+ .style("font-size", "16px")
159
+ .text("Not enough data");
160
+ return svg.node();
161
+ }
162
+
163
+ let chartData = grouped.slice();
164
+ if (categoryType === "temporal") {
165
+ chartData.sort((a, b) => {
166
+ const aDate = parseTemporal(a.rawCategory);
167
+ const bDate = parseTemporal(b.rawCategory);
168
+ return d3.ascending(aDate ? +aDate : Number(a.rawCategory), bDate ? +bDate : Number(b.rawCategory));
169
+ });
170
+ } else {
171
+ chartData.sort((a, b) => d3.descending(a.value, b.value) || d3.ascending(a.category, b.category));
172
+ }
173
+
174
+ const maxLabelLength = d3.max(chartData, d => d.category.length) || 0;
175
+ const needsKey = categoryType !== "temporal" && (maxLabelLength > 12 || chartData.length > 10);
176
+ chartData.forEach((d, index) => {
177
+ d.displayLabel = needsKey ? `R${index + 1}` : d.category;
178
+ });
179
+
180
+ const margin = {
181
+ top: 48,
182
+ right: needsKey ? 168 : 44,
183
+ bottom: 56,
184
+ left: 44
185
+ };
186
+ const plotWidth = width - margin.left - margin.right;
187
+ const plotHeight = height - margin.top - margin.bottom;
188
+ const radius = Math.max(80, Math.min(plotWidth, plotHeight) / 2 - 26);
189
+ const centerX = margin.left + plotWidth / 2;
190
+ const centerY = margin.top + plotHeight / 2 + 10;
191
+
192
+ const maxValue = d3.max(chartData, d => d.value) || 1;
193
+ const radiusScale = d3.scaleLinear()
194
+ .domain([0, maxValue])
195
+ .range([0, radius])
196
+ .nice(4);
197
+ const radiusTicks = radiusScale.ticks(4).filter(tick => tick > 0);
198
+ const maxTick = d3.max(radiusTicks) || maxValue;
199
+ radiusScale.domain([0, maxTick]);
200
+
201
+ const angleStep = (2 * Math.PI) / chartData.length;
202
+ const angleForIndex = index => index * angleStep - Math.PI / 2;
203
+
204
+ const pointFor = (d, index, distance = radiusScale(d.value)) => {
205
+ const angle = angleForIndex(index);
206
+ return {
207
+ x: distance * Math.cos(angle),
208
+ y: distance * Math.sin(angle),
209
+ angle
210
+ };
211
+ };
212
+
213
+ const pathPoints = chartData.map((d, index) => {
214
+ const point = pointFor(d, index);
215
+ return [point.x, point.y];
216
+ });
217
+
218
+ const g = svg.append("g")
219
+ .attr("class", "radial-area-plot")
220
+ .attr("data-tag", "chart")
221
+ .attr("transform", `translate(${centerX}, ${centerY})`);
222
+
223
+ radiusTicks.forEach(tick => {
224
+ g.append("circle")
225
+ .attr("class", "radial-grid-ring")
226
+ .attr("r", radiusScale(tick))
227
+ .attr("fill", "none")
228
+ .attr("stroke", gridColor)
229
+ .attr("stroke-width", 1);
230
+
231
+ g.append("text")
232
+ .attr("class", "radial-grid-label")
233
+ .attr("x", 6)
234
+ .attr("y", -radiusScale(tick) - 3)
235
+ .attr("fill", mutedText)
236
+ .style("font-family", fontFamily)
237
+ .style("font-size", "10px")
238
+ .text(formatLocalValue(tick));
239
+ });
240
+
241
+ chartData.forEach((d, index) => {
242
+ const outer = pointFor(d, index, radius);
243
+ const labelDistance = radius + 15;
244
+ const labelPoint = pointFor(d, index, labelDistance);
245
+ const cos = Math.cos(outer.angle);
246
+ const sin = Math.sin(outer.angle);
247
+
248
+ g.append("line")
249
+ .attr("class", "radial-axis-line")
250
+ .attr("x1", 0)
251
+ .attr("y1", 0)
252
+ .attr("x2", outer.x)
253
+ .attr("y2", outer.y)
254
+ .attr("stroke", "#cbd5df")
255
+ .attr("stroke-width", 1);
256
+
257
+ g.append("text")
258
+ .attr("class", "category-label")
259
+ .attr("x", labelPoint.x)
260
+ .attr("y", labelPoint.y)
261
+ .attr("text-anchor", Math.abs(cos) < 0.25 ? "middle" : (cos > 0 ? "start" : "end"))
262
+ .attr("dominant-baseline", Math.abs(sin) < 0.25 ? "middle" : (sin > 0 ? "hanging" : "auto"))
263
+ .attr("fill", textColor)
264
+ .style("font-family", fontFamily)
265
+ .style("font-size", needsKey ? "10px" : "11px")
266
+ .style("font-weight", "700")
267
+ .text(d.displayLabel);
268
+ });
269
+
270
+ g.append("path")
271
+ .attr("class", "radial-area-path")
272
+ .attr("data-tag", "mark")
273
+ .attr("d", d3.line().curve(d3.curveLinearClosed)(pathPoints))
274
+ .attr("fill", mainColor)
275
+ .attr("fill-opacity", 0.22)
276
+ .attr("stroke", mainColor)
277
+ .attr("stroke-width", 3)
278
+ .attr("stroke-linejoin", "round");
279
+
280
+ chartData.forEach((d, index) => {
281
+ const point = pointFor(d, index);
282
+ g.append("circle")
283
+ .attr("class", "radial-area-point")
284
+ .attr("data-tag", "mark")
285
+ .attr("cx", point.x)
286
+ .attr("cy", point.y)
287
+ .attr("r", 4.2)
288
+ .attr("fill", mainColor)
289
+ .attr("stroke", "#ffffff")
290
+ .attr("stroke-width", 2);
291
+ });
292
+
293
+ let maxIndex = 0;
294
+ let minIndex = 0;
295
+ chartData.forEach((d, index) => {
296
+ if (d.value > chartData[maxIndex].value) maxIndex = index;
297
+ if (d.value < chartData[minIndex].value) minIndex = index;
298
+ });
299
+ const extrema = new Set([maxIndex, minIndex]);
300
+ chartData.forEach((d, index) => {
301
+ if (!extrema.has(index)) return;
302
+ const point = pointFor(d, index, radiusScale(d.value) + 18);
303
+ const label = formatLocalValue(d.value);
304
+ const labelWidth = Math.max(34, label.length * 6.2);
305
+
306
+ g.append("rect")
307
+ .attr("class", "extreme-value-background")
308
+ .attr("x", point.x - labelWidth / 2 - 4)
309
+ .attr("y", point.y - 9)
310
+ .attr("width", labelWidth + 8)
311
+ .attr("height", 18)
312
+ .attr("rx", 4)
313
+ .attr("fill", "#ffffff")
314
+ .attr("stroke", mainColor)
315
+ .attr("stroke-width", 1);
316
+
317
+ g.append("text")
318
+ .attr("class", "value-label")
319
+ .attr("x", point.x)
320
+ .attr("y", point.y + 1)
321
+ .attr("text-anchor", "middle")
322
+ .attr("dominant-baseline", "middle")
323
+ .attr("fill", mainColor)
324
+ .style("font-family", fontFamily)
325
+ .style("font-size", "10px")
326
+ .style("font-weight", "700")
327
+ .text(label);
328
+ });
329
+
330
+ g.append("text")
331
+ .attr("class", "radial-axis-title")
332
+ .attr("x", 0)
333
+ .attr("y", radius + 42)
334
+ .attr("text-anchor", "middle")
335
+ .attr("fill", mutedText)
336
+ .style("font-family", titleFont)
337
+ .style("font-size", "11px")
338
+ .text(valueUnit ? `${valueLabel} (${valueUnit})` : valueLabel);
339
+
340
+ if (categoryType === "temporal") {
341
+ const first = chartData[0]?.category;
342
+ const last = chartData[chartData.length - 1]?.category;
343
+ g.append("text")
344
+ .attr("class", "category-context-label")
345
+ .attr("x", 0)
346
+ .attr("y", -radius - 28)
347
+ .attr("text-anchor", "middle")
348
+ .attr("fill", mutedText)
349
+ .style("font-family", fontFamily)
350
+ .style("font-size", "10px")
351
+ .text(`${categoryLabel}${categoryUnit ? ` (${categoryUnit})` : ""}: ${first}-${last}`);
352
+ }
353
+
354
+ if (needsKey) {
355
+ const key = svg.append("g")
356
+ .attr("class", "category-key")
357
+ .attr("transform", `translate(${width - margin.right + 16}, ${margin.top + 10})`);
358
+
359
+ key.append("text")
360
+ .attr("class", "category-key-title")
361
+ .attr("x", 0)
362
+ .attr("y", 0)
363
+ .attr("fill", textColor)
364
+ .style("font-family", titleFont)
365
+ .style("font-size", "11px")
366
+ .style("font-weight", "700")
367
+ .text(categoryLabel);
368
+
369
+ chartData.forEach((d, index) => {
370
+ const row = key.append("g")
371
+ .attr("class", "category-key-row")
372
+ .attr("transform", `translate(0, ${18 + index * 18})`);
373
+ const label = d.category.length > 21 ? `${d.category.slice(0, 20)}...` : d.category;
374
+
375
+ row.append("text")
376
+ .attr("class", "category-key-label")
377
+ .attr("x", 0)
378
+ .attr("y", 0)
379
+ .attr("fill", textColor)
380
+ .style("font-family", fontFamily)
381
+ .style("font-size", "9.5px")
382
+ .text(`${d.displayLabel} ${label}`);
383
+ });
384
+ }
385
+
386
+ return svg.node();
387
+ }