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

Upload folder using huggingface_hub

Browse files
modules/chart_engine/template/d3-js/type14_histogram/histogram_plain_chart_01.js ADDED
@@ -0,0 +1,467 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ REQUIREMENTS_BEGIN
3
+ {
4
+ "chart_type": "Histogram",
5
+ "chart_name": "histogram_plain_chart_01",
6
+ "required_fields": ["x", "y"],
7
+ "required_fields_type": [["temporal"], ["numerical"]],
8
+ "required_fields_range": [[15, 50], [0, "inf"]],
9
+ "required_fields_icons": [],
10
+ "required_other_icons": [],
11
+ "required_fields_colors": [],
12
+ "required_other_colors": ["primary", "secondary", "background"],
13
+ "supported_effects": ["gradient", "opacity"],
14
+ "min_height": 600,
15
+ "min_width": 800,
16
+ "background": "light",
17
+ "icon_mark": "none",
18
+ "icon_label": "none",
19
+ "has_x_axis": "yes",
20
+ "has_y_axis": "yes"
21
+ }
22
+ REQUIREMENTS_END
23
+ */
24
+
25
+
26
+ function makeChart(containerSelector, data) {
27
+ const colorResolver = chartUtils.color.resolver(data);
28
+ const jsonData = data;
29
+ const chartData = jsonData.data && Array.isArray(jsonData.data.data)
30
+ ? jsonData.data.data
31
+ : (Array.isArray(jsonData.data) ? jsonData.data : []);
32
+ const variables = jsonData.variables || {};
33
+ const typography = jsonData.typography || {
34
+ title: { font_family: "Georgia", font_size: "18px", font_weight: "bold" },
35
+ label: { font_family: "Arial", font_size: "14px", font_weight: "normal" },
36
+ description: { font_family: "Arial", font_size: "14px", font_weight: "normal" },
37
+ annotation: { font_family: "Arial", font_size: "12px", font_weight: "normal" }
38
+ };
39
+
40
+ d3.select(containerSelector).html("");
41
+
42
+ const width = Number(variables.width) || 625;
43
+ const height = Number(variables.height) || 486;
44
+ const primary = colorResolver.other("primary").value || "#6EAD38";
45
+ const textColor = colorResolver.text({ fallback: "#1E3020" }).value || "#1E3020";
46
+ const labelFamily = (typography.label && typography.label.font_family) || "Arial";
47
+ const annotationFamily = (typography.annotation && typography.annotation.font_family) || labelFamily;
48
+
49
+ const xChannel = chartUtils.schema.channel(jsonData, "x");
50
+ const yChannel = chartUtils.schema.channel(jsonData, "y");
51
+ const xField = xChannel.key || "period";
52
+ const yField = yChannel.key || "value";
53
+ const yUnit = yChannel.unit && yChannel.unit !== "none" ? yChannel.unit : "";
54
+
55
+ function parseTimeValue(value) {
56
+ const parsed = chartUtils.format.parseDate(value);
57
+ if (parsed instanceof Date && !Number.isNaN(parsed.getTime())) return parsed;
58
+ const match = String(value).match(/\d{4}/);
59
+ const year = match ? Number(match[0]) : NaN;
60
+ return Number.isFinite(year) ? new Date(year, 0, 1) : new Date(0);
61
+ }
62
+
63
+ function readableName(value) {
64
+ return String(value || "")
65
+ .replace(/[_-]+/g, " ")
66
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
67
+ .replace(/\s+/g, " ")
68
+ .trim();
69
+ }
70
+
71
+ function wrapWords(text, maxChars, maxLines) {
72
+ const words = String(text || "").split(/\s+/).filter(Boolean);
73
+ const lines = [];
74
+ let line = "";
75
+ words.forEach(word => {
76
+ const next = line ? `${line} ${word}` : word;
77
+ if (next.length > maxChars && line) {
78
+ lines.push(line);
79
+ line = word;
80
+ } else {
81
+ line = next;
82
+ }
83
+ });
84
+ if (line) lines.push(line);
85
+ return lines.slice(0, maxLines);
86
+ }
87
+
88
+ function intersects(a, b, pad = 0) {
89
+ return !(a.x + a.w + pad < b.x || b.x + b.w + pad < a.x ||
90
+ a.y + a.h + pad < b.y || b.y + b.h + pad < a.y);
91
+ }
92
+
93
+ function overlapArea(a, b, pad = 0) {
94
+ const x0 = Math.max(a.x - pad, b.x - pad);
95
+ const y0 = Math.max(a.y - pad, b.y - pad);
96
+ const x1 = Math.min(a.x + a.w + pad, b.x + b.w + pad);
97
+ const y1 = Math.min(a.y + a.h + pad, b.y + b.h + pad);
98
+ return Math.max(0, x1 - x0) * Math.max(0, y1 - y0);
99
+ }
100
+
101
+ const processedData = chartData
102
+ .map((d, i) => ({
103
+ raw: d,
104
+ time: parseTimeValue(d[xField]),
105
+ value: Number(d[yField]),
106
+ label: String(d[xField]),
107
+ order: i
108
+ }))
109
+ .filter(d => Number.isFinite(d.value));
110
+
111
+ processedData.sort((a, b) => a.time - b.time || a.order - b.order);
112
+ processedData.forEach((d, i) => { d.order = i; });
113
+
114
+ const svg = d3.select(containerSelector)
115
+ .append("svg")
116
+ .attr("width", "100%")
117
+ .attr("height", height)
118
+ .attr("viewBox", `0 0 ${width} ${height}`)
119
+ .attr("style", "max-width: 100%; height: auto;")
120
+ .attr("xmlns", "http://www.w3.org/2000/svg")
121
+ .attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
122
+
123
+ const defs = svg.append("defs");
124
+ const shadow = defs.append("filter")
125
+ .attr("id", "histogram-soft-panel-shadow")
126
+ .attr("x", "-10%")
127
+ .attr("y", "-10%")
128
+ .attr("width", "120%")
129
+ .attr("height", "120%");
130
+ shadow.append("feDropShadow")
131
+ .attr("dx", 0)
132
+ .attr("dy", 3)
133
+ .attr("stdDeviation", 4)
134
+ .attr("flood-color", "#2f4a21")
135
+ .attr("flood-opacity", 0.13);
136
+
137
+ const barGradient = defs.append("linearGradient")
138
+ .attr("id", "histogram-green-square-bar-gradient")
139
+ .attr("x1", "0%")
140
+ .attr("x2", "0%")
141
+ .attr("y1", "100%")
142
+ .attr("y2", "0%");
143
+ barGradient.append("stop").attr("offset", "0%").attr("stop-color", d3.color(primary)?.darker(0.45) || "#57932E");
144
+ barGradient.append("stop").attr("offset", "58%").attr("stop-color", primary);
145
+ barGradient.append("stop").attr("offset", "100%").attr("stop-color", d3.color(primary)?.brighter(0.55) || "#9BD257");
146
+
147
+ const panelX = Math.max(8, width * 0.025);
148
+ const panelY = Math.max(8, height * 0.035);
149
+ const panelW = width - panelX * 2;
150
+ const panelH = height - panelY - Math.max(10, height * 0.025);
151
+
152
+ svg.append("rect")
153
+ .attr("class", "histogram-reference-panel")
154
+ .attr("x", panelX)
155
+ .attr("y", panelY)
156
+ .attr("width", panelW)
157
+ .attr("height", panelH)
158
+ .attr("rx", Math.min(28, width * 0.035))
159
+ .attr("fill", "#FFFDF2")
160
+ .attr("stroke", "#DDE9C8")
161
+ .attr("stroke-width", 1.1)
162
+ .attr("filter", "url(#histogram-soft-panel-shadow)");
163
+
164
+ const unitPillW = Math.min(230, Math.max(145, panelW * 0.34));
165
+ const unitPillH = Math.max(30, Math.min(42, height * 0.075));
166
+ const unitPillX = panelX + Math.max(18, panelW * 0.07);
167
+ const unitPillY = panelY + Math.max(12, panelH * 0.035);
168
+ const unitGroup = svg.append("g")
169
+ .attr("class", "histogram-unit-pill")
170
+ .attr("transform", `translate(${unitPillX}, ${unitPillY})`);
171
+ unitGroup.append("rect")
172
+ .attr("width", unitPillW)
173
+ .attr("height", unitPillH)
174
+ .attr("rx", unitPillH / 2)
175
+ .attr("fill", "#F6F8EE")
176
+ .attr("stroke", "#DFE8CD");
177
+ unitGroup.append("circle")
178
+ .attr("class", "reserved-asset-slot unit-symbol-slot")
179
+ .attr("data-asset-slot", "unit-symbol-neutral")
180
+ .attr("data-reserved-slot", "unit_symbol_neutral_slot")
181
+ .attr("data-asset-source-policy", "neutral_chart_primitive")
182
+ .attr("data-no-embedded-raster", "true")
183
+ .attr("data-no-copied-reference-art", "true")
184
+ .attr("data-anchor", "unit-pill-left")
185
+ .attr("data-collision-rule", "inside unit pill and left of text")
186
+ .attr("data-bbox-policy", "cx/cy/r define visible neutral unit slot bbox")
187
+ .attr("cx", unitPillH / 2)
188
+ .attr("cy", unitPillH / 2)
189
+ .attr("r", unitPillH * 0.38)
190
+ .attr("fill", primary);
191
+ unitGroup.append("text")
192
+ .attr("x", unitPillH / 2)
193
+ .attr("y", unitPillH / 2 + 1)
194
+ .attr("text-anchor", "middle")
195
+ .attr("dominant-baseline", "middle")
196
+ .style("font-family", "Georgia, serif")
197
+ .style("font-weight", "700")
198
+ .style("font-size", `${Math.max(16, unitPillH * 0.55)}px`)
199
+ .style("fill", "#FFFFFF")
200
+ .text("$");
201
+ unitGroup.append("text")
202
+ .attr("x", unitPillH + 12)
203
+ .attr("y", unitPillH / 2 + 1)
204
+ .attr("dominant-baseline", "middle")
205
+ .style("font-family", labelFamily)
206
+ .style("font-weight", "800")
207
+ .style("font-size", `${Math.max(13, unitPillH * 0.36)}px`)
208
+ .style("fill", "#18381C")
209
+ .text(yUnit || readableName(yField));
210
+
211
+ const margin = {
212
+ top: unitPillY + unitPillH - panelY + Math.max(18, height * 0.035),
213
+ right: Math.max(18, width * 0.035),
214
+ bottom: Math.max(54, height * 0.12),
215
+ left: Math.max(76, width * 0.13)
216
+ };
217
+ const plotX = panelX + margin.left;
218
+ const plotY = panelY + margin.top;
219
+ const plotW = Math.max(120, panelW - margin.left - margin.right);
220
+ const plotH = Math.max(120, panelH - margin.top - margin.bottom);
221
+
222
+ svg.append("g")
223
+ .attr("class", "reserved-asset-slot header-illustration-policy")
224
+ .attr("data-asset-slot", "header-illustration")
225
+ .attr("data-reserved-slot", "header_illustration_slot")
226
+ .attr("data-slot-policy", "image2_or_retrieval_asset_reserved_only")
227
+ .attr("data-asset-source-policy", "image2_or_retrieval_asset;not-template-hardcoded")
228
+ .attr("data-no-embedded-raster", "true")
229
+ .attr("data-no-copied-reference-art", "true")
230
+ .attr("data-bbox", `${(width * 0.58).toFixed(2)},${(height * 0.08).toFixed(2)},${(width * 0.34).toFixed(2)},${(height * 0.22).toFixed(2)}`)
231
+ .attr("data-bbox-policy", "reference-ratio header safe-area metadata only; outer infographic layer emits the visible reserved slot")
232
+ .attr("data-mask-policy", "downstream image2/retrieval may place topic art only inside reserved header bbox; template emits no raster")
233
+ .attr("data-anchor", "outer-header-right-safe-area")
234
+ .attr("data-collision-rule", "owned by title/image layer; avoid title subtitle chart panel axes callout and canvas edge; chart template does not embed copied reference art");
235
+
236
+ if (!processedData.length) {
237
+ svg.append("text")
238
+ .attr("x", width / 2)
239
+ .attr("y", height / 2)
240
+ .attr("text-anchor", "middle")
241
+ .style("font-family", labelFamily)
242
+ .style("fill", textColor)
243
+ .text("No numeric data");
244
+ return svg.node();
245
+ }
246
+
247
+ const xScale = d3.scaleLinear()
248
+ .domain([0, Math.max(0, processedData.length - 1)])
249
+ .range([0, plotW]);
250
+ const yMax = d3.max(processedData, d => d.value) || 1;
251
+ const yScale = d3.scaleLinear()
252
+ .domain([0, yMax * 1.08])
253
+ .range([plotH, 0])
254
+ .nice();
255
+ const binWidth = processedData.length > 1 ? plotW / processedData.length : plotW;
256
+ const barWidth = Math.max(3, Math.min(12, binWidth * 0.42));
257
+
258
+ const chartGroup = svg.append("g")
259
+ .attr("class", "histogram-plot")
260
+ .attr("transform", `translate(${plotX}, ${plotY})`);
261
+
262
+ const yTicks = yScale.ticks(6);
263
+ chartGroup.append("g")
264
+ .attr("class", "histogram-grid")
265
+ .selectAll("line")
266
+ .data(yTicks)
267
+ .enter()
268
+ .append("line")
269
+ .attr("x1", 0)
270
+ .attr("x2", plotW)
271
+ .attr("y1", d => yScale(d))
272
+ .attr("y2", d => yScale(d))
273
+ .attr("stroke", "#C9D8BA")
274
+ .attr("stroke-width", 1)
275
+ .attr("stroke-dasharray", "4 4")
276
+ .attr("opacity", 0.82);
277
+
278
+ chartGroup.append("line")
279
+ .attr("class", "y-axis-line")
280
+ .attr("x1", 0)
281
+ .attr("x2", 0)
282
+ .attr("y1", 0)
283
+ .attr("y2", plotH)
284
+ .attr("stroke", d3.color(primary)?.darker(0.75) || "#2F6123")
285
+ .attr("stroke-width", 1.4);
286
+
287
+ chartGroup.append("line")
288
+ .attr("class", "x-axis-baseline")
289
+ .attr("x1", 0)
290
+ .attr("x2", plotW)
291
+ .attr("y1", plotH)
292
+ .attr("y2", plotH)
293
+ .attr("stroke", "#18381C")
294
+ .attr("stroke-width", 1.8);
295
+
296
+ chartGroup.append("g")
297
+ .attr("class", "y-axis-labels")
298
+ .selectAll("text")
299
+ .data(yTicks)
300
+ .enter()
301
+ .append("text")
302
+ .attr("x", -12)
303
+ .attr("y", d => yScale(d) + 4)
304
+ .attr("text-anchor", "end")
305
+ .style("font-family", "Georgia, serif")
306
+ .style("font-size", `${Math.max(12, Math.min(20, height * 0.035))}px`)
307
+ .style("font-weight", "700")
308
+ .style("fill", "#245A24")
309
+ .text(d => `$${chartUtils.format.autoText(d)}`);
310
+
311
+ const maxTicks = Math.max(5, Math.min(9, Math.floor(plotW / 68)));
312
+ const indexStep = Math.max(1, Math.ceil(processedData.length / maxTicks));
313
+ const tickIndices = [];
314
+ for (let i = 0; i < processedData.length; i += indexStep) tickIndices.push(i);
315
+ if (tickIndices[tickIndices.length - 1] !== processedData.length - 1) tickIndices.push(processedData.length - 1);
316
+ const minTickGap = Math.max(46, Math.min(62, plotW / 8));
317
+ const filteredTickIndices = tickIndices.filter((i, idx) => {
318
+ if (idx === 0 || i === processedData.length - 1) return true;
319
+ const prev = tickIndices.slice(0, idx).reverse().find(j => Math.abs(xScale(i) - xScale(j)) >= minTickGap);
320
+ return prev !== undefined;
321
+ }).filter((i, idx, arr) => idx === 0 || Math.abs(xScale(i) - xScale(arr[idx - 1])) >= minTickGap || i === processedData.length - 1);
322
+
323
+ chartGroup.append("g")
324
+ .attr("class", "x-axis-labels")
325
+ .selectAll("text")
326
+ .data(filteredTickIndices)
327
+ .enter()
328
+ .append("text")
329
+ .attr("x", i => xScale(i))
330
+ .attr("y", plotH + Math.max(18, height * 0.045))
331
+ .attr("text-anchor", (i, idx) => idx === 0 ? "start" : (idx === filteredTickIndices.length - 1 ? "end" : "middle"))
332
+ .style("font-family", labelFamily)
333
+ .style("font-size", `${Math.max(11, Math.min(14, height * 0.03))}px`)
334
+ .style("font-weight", "700")
335
+ .style("fill", "#2E3B31")
336
+ .text(i => chartUtils.format.date(processedData[i].time, { output: "%Y" }).text);
337
+
338
+ const barBoxes = processedData.map(d => ({
339
+ datum: d,
340
+ x: plotX + xScale(d.order) - barWidth / 2,
341
+ y: plotY + yScale(d.value),
342
+ w: barWidth,
343
+ h: plotH - yScale(d.value)
344
+ }));
345
+
346
+ chartGroup.selectAll(".bar")
347
+ .data(processedData)
348
+ .enter()
349
+ .append("rect")
350
+ .attr("class", "bar histogram-square-flat-bar")
351
+ .attr("data-x-value", d => d.label)
352
+ .attr("data-y-value", d => d.value)
353
+ .attr("x", d => xScale(d.order) - barWidth / 2)
354
+ .attr("y", d => yScale(d.value))
355
+ .attr("width", barWidth)
356
+ .attr("height", d => Math.max(0, plotH - yScale(d.value)))
357
+ .attr("rx", 0)
358
+ .attr("ry", 0)
359
+ .attr("fill", "url(#histogram-green-square-bar-gradient)")
360
+ .attr("stroke", "none");
361
+
362
+ const values = processedData.map(d => d.value);
363
+ const minValue = d3.min(values);
364
+ const maxValue = d3.max(values);
365
+ const firstValue = processedData[0].value;
366
+ const lastValue = processedData[processedData.length - 1].value;
367
+ const direction = lastValue - firstValue >= 0 ? "higher" : "lower";
368
+ const calloutW = Math.min(Math.max(154, plotW * 0.25), 180);
369
+ const calloutH = Math.min(Math.max(88, plotH * 0.28), 108);
370
+ const calloutCandidates = [
371
+ { x: plotX + plotW * 0.025, y: plotY + plotH * 0.055, zone: "plot" },
372
+ { x: plotX + plotW - calloutW - plotW * 0.06, y: plotY + plotH * 0.07, zone: "plot" },
373
+ { x: plotX + plotW * 0.025, y: plotY + plotH * 0.50, zone: "plot" },
374
+ { x: plotX + plotW - calloutW - plotW * 0.06, y: plotY + plotH * 0.48, zone: "plot" },
375
+ { x: plotX + plotW * 0.025, y: unitPillY + unitPillH + 9, zone: "panel-safe-band" },
376
+ { x: plotX + plotW - calloutW - plotW * 0.06, y: unitPillY + unitPillH + 9, zone: "panel-safe-band" }
377
+ ];
378
+ const fixedAvoid = [
379
+ { x: panelX, y: panelY, w: panelW, h: unitPillY + unitPillH - panelY + 6 },
380
+ { x: plotX - margin.left, y: plotY + plotH + 2, w: panelW, h: panelY + panelH - (plotY + plotH) }
381
+ ];
382
+ const scoredCallouts = calloutCandidates.map(c => {
383
+ const box = { x: c.x, y: c.y, w: calloutW, h: calloutH };
384
+ const insidePlot = box.x >= plotX + 2 && box.y >= plotY + 2 &&
385
+ box.x + box.w <= plotX + plotW - 2 && box.y + box.h <= plotY + plotH - 2;
386
+ const insidePanel = box.x >= panelX + 10 && box.y >= panelY + 10 &&
387
+ box.x + box.w <= panelX + panelW - 10 && box.y + box.h <= panelY + panelH - 10;
388
+ const inside = c.zone === "plot" ? insidePlot : insidePanel;
389
+ const fixedOverlap = fixedAvoid.reduce((sum, avoid) => sum + overlapArea(box, avoid, 4), 0);
390
+ const barOverlap = barBoxes.reduce((sum, bar) => sum + overlapArea(box, bar, 3), 0);
391
+ const tallBarOverlap = barBoxes
392
+ .filter(bar => bar.h > plotH * 0.45)
393
+ .reduce((sum, bar) => sum + overlapArea(box, bar, 4), 0);
394
+ return { box, zone: c.zone, inside, fixedOverlap, barOverlap, tallBarOverlap };
395
+ }).filter(d => d.inside && d.fixedOverlap === 0)
396
+ .sort((a, b) => (a.tallBarOverlap - b.tallBarOverlap) || (a.barOverlap - b.barOverlap));
397
+ const plotChoices = scoredCallouts.filter(d => d.zone === "plot");
398
+ const safeChoices = scoredCallouts.filter(d => d.zone !== "plot");
399
+ const bestPlot = plotChoices.length ? plotChoices[0] : null;
400
+ const maxPlotOverlap = 0;
401
+ const calloutChoice = bestPlot && bestPlot.tallBarOverlap === 0 && bestPlot.barOverlap <= maxPlotOverlap
402
+ ? bestPlot
403
+ : (safeChoices[0] || bestPlot || null);
404
+ const calloutBox = calloutChoice ? calloutChoice.box : null;
405
+
406
+ if (calloutBox && plotW > 260 && plotH > 180) {
407
+ const callout = svg.append("g")
408
+ .attr("class", "plot-callout floating-plot-relative")
409
+ .attr("data-coordinate-relationship", calloutChoice.zone === "plot" ? "floating_plot_relative" : "panel_safe_band_fallback")
410
+ .attr("data-anchor", "plot-bbox-with-bar-collision-avoidance-and-panel-fallback")
411
+ .attr("transform", `translate(${calloutBox.x}, ${calloutBox.y})`);
412
+ callout.append("rect")
413
+ .attr("width", calloutW)
414
+ .attr("height", calloutH)
415
+ .attr("rx", 14)
416
+ .attr("fill", "#F8FAEC")
417
+ .attr("stroke", "#C9DCB3")
418
+ .attr("stroke-width", 1.1);
419
+ callout.append("circle")
420
+ .attr("class", "reserved-asset-slot callout-badge-slot")
421
+ .attr("data-asset-slot", "callout-neutral-badge")
422
+ .attr("data-reserved-slot", "callout_neutral_badge_slot")
423
+ .attr("data-asset-source-policy", "neutral_chart_primitive")
424
+ .attr("data-no-embedded-raster", "true")
425
+ .attr("data-no-copied-reference-art", "true")
426
+ .attr("data-anchor", "callout-card-left")
427
+ .attr("data-collision-rule", "inside callout card and left of callout text")
428
+ .attr("data-bbox-policy", "cx/cy/r inside callout group define visible neutral badge bbox")
429
+ .attr("cx", 28)
430
+ .attr("cy", 30)
431
+ .attr("r", 18)
432
+ .attr("fill", primary)
433
+ .attr("opacity", 0.92);
434
+ callout.append("circle")
435
+ .attr("cx", 28)
436
+ .attr("cy", 30)
437
+ .attr("r", 6)
438
+ .attr("fill", "#FFFDF2")
439
+ .attr("opacity", 0.95);
440
+ callout.append("text")
441
+ .attr("x", 55)
442
+ .attr("y", 26)
443
+ .style("font-family", annotationFamily)
444
+ .style("font-size", `${Math.max(12, Math.min(17, height * 0.032))}px`)
445
+ .style("font-weight", "800")
446
+ .style("fill", "#245A24")
447
+ .text("Range in motion");
448
+ const body = `${readableName(yField)}: ${chartUtils.format.autoText(minValue)} to ${chartUtils.format.autoText(maxValue)}${yUnit ? ` ${yUnit}` : ""}. Latest ${direction}.`;
449
+ wrapWords(body, Math.max(18, Math.floor((calloutW - 32) / 7.2)), 3).forEach((line, i) => {
450
+ callout.append("text")
451
+ .attr("x", 18)
452
+ .attr("y", 58 + i * Math.max(14, height * 0.028))
453
+ .style("font-family", annotationFamily)
454
+ .style("font-size", `${Math.max(10, Math.min(13, height * 0.026))}px`)
455
+ .style("font-weight", "500")
456
+ .style("fill", "#253029")
457
+ .text(line);
458
+ });
459
+ }
460
+
461
+ svg.append("g")
462
+ .attr("class", "ignored-bottom-wrapper-policy")
463
+ .attr("data-ignored-reference-element", "bottom_takeaway_source_footer_rating_replacement_prose")
464
+ .attr("data-policy", "not-rendered");
465
+
466
+ return svg.node();
467
+ }