PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.2.2
AI Builder – Generate pages, blocks, images & translate with AI v2.2.2
2.7.10 2.7.9 2.7.8 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.3.10 All 122 releases
ai-builder / assets / js / src / editor-blocks / stats / index.js

index.js in AI Builder – Generate pages, blocks, images & translate with AI 2.2.2, at assets/js/src/editor-blocks/stats/index.js

518 lines 15.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 const { registerBlockType } = wp.blocks;
2 const { __ } = wp.i18n;
3 const {
4 PanelBody,
5 TextControl,
6 SelectControl,
7 Button,
8 Notice,
9 TextareaControl,
10 } = wp.components;
11 const { InspectorControls, useBlockProps } = wp.blockEditor || wp.editor;
12
13 import "./stats.css";
14
15 // Simple, common schemas
16 // bar/line: { labels:["Jan","Feb"], series:[{name:"A",data:[10,20]},{name:"B",data:[5,15]}] }
17 // pie: { labels:["A","B","C"], data:[30,50,20] }
18 const DEFAULT_DATA = {
19 labels: ["A", "B", "C"],
20 series: [{ name: "Series 1", data: [30, 50, 20] }],
21 };
22
23 registerBlockType("ai-builder/aibui-stats", {
24 title: __("Stats (AI Builder)", "ai-builder"),
25 icon: "chart-bar",
26 category: "widgets",
27 supports: { align: ["wide", "full"], html: false },
28 attributes: {
29 chartType: { type: "string", default: "bar" }, // bar | line | pie
30 title: { type: "string", default: "" },
31 xLabel: { type: "string", default: "" },
32 yLabel: { type: "string", default: "" },
33 dataset: { type: "string", default: JSON.stringify(DEFAULT_DATA) }, // JSON string for simplicity
34 color: { type: "string", default: "#4f46e5" },
35 },
36 edit: (props) => {
37 const { attributes, setAttributes } = props;
38 const { chartType, title, xLabel, yLabel, dataset, color } = attributes;
39 const blockProps = useBlockProps({ className: "aibui-stats" });
40
41 let parsed = DEFAULT_DATA;
42 try {
43 parsed = JSON.parse(dataset || "{}");
44 if (!parsed || typeof parsed !== "object") parsed = DEFAULT_DATA;
45 } catch (e) {
46 parsed = DEFAULT_DATA;
47 }
48
49 return (
50 <div {...blockProps}>
51 <InspectorControls>
52 <PanelBody title={__("Settings", "ai-builder")} initialOpen>
53 <SelectControl
54 label={__("Chart type", "ai-builder")}
55 value={chartType}
56 options={[
57 { label: "Bar", value: "bar" },
58 { label: "Line", value: "line" },
59 { label: "Pie", value: "pie" },
60 ]}
61 onChange={(val) => setAttributes({ chartType: val })}
62 />
63 <TextControl
64 label={__("Title", "ai-builder")}
65 value={title}
66 onChange={(val) => setAttributes({ title: val })}
67 />
68 <TextControl
69 label={__("X axis label", "ai-builder")}
70 value={xLabel}
71 onChange={(val) => setAttributes({ xLabel: val })}
72 />
73 <TextControl
74 label={__("Y axis label", "ai-builder")}
75 value={yLabel}
76 onChange={(val) => setAttributes({ yLabel: val })}
77 />
78 <TextControl
79 label={__("Primary color", "ai-builder")}
80 type="color"
81 value={color}
82 onChange={(val) => setAttributes({ color: val })}
83 />
84 <TextareaControl
85 label={__("Data (JSON)", "ai-builder")}
86 help={
87 chartType === "pie"
88 ? '{"labels":["A","B"],"data":[30,50]}'
89 : '{"labels":["Jan","Feb"],"series":[{"name":"A","data":[10,20]},{"name":"B","data":[5,15]}]}'
90 }
91 value={dataset}
92 onChange={(val) => setAttributes({ dataset: val })}
93 />
94 </PanelBody>
95 </InspectorControls>
96
97 <div className="aibui-stats-preview">
98 {title ? <h3 className="aibui-stats-title">{title}</h3> : null}
99 <StatsSVG
100 type={chartType}
101 data={parsed}
102 color={color}
103 xLabel={xLabel}
104 yLabel={yLabel}
105 />
106 {chartType !== "pie" &&
107 Array.isArray(parsed.series) &&
108 parsed.series.some((s) => s && s.name) ? (
109 <div className="aibui-stats-legend">
110 {parsed.series.map((s, si) =>
111 s && s.name ? (
112 <span key={si} className="aibui-stats-legend-item">
113 <span
114 className="aibui-stats-legend-dot"
115 style={{
116 backgroundColor: shade(color, si, parsed.series.length),
117 }}
118 ></span>
119 {s.name}
120 </span>
121 ) : null
122 )}
123 </div>
124 ) : null}
125 </div>
126 </div>
127 );
128 },
129 save: (props) => {
130 const { attributes } = props;
131 const { chartType, title, xLabel, yLabel, dataset, color } = attributes;
132 let parsed = DEFAULT_DATA;
133 try {
134 parsed = JSON.parse(dataset || "{}");
135 if (!parsed || typeof parsed !== "object") parsed = DEFAULT_DATA;
136 } catch (e) {
137 parsed = DEFAULT_DATA;
138 }
139 const blockProps = wp.blockEditor
140 ? wp.blockEditor.useBlockProps.save({ className: "aibui-stats" })
141 : {};
142 return (
143 <div {...blockProps}>
144 <div className="aibui-stats-preview">
145 {title ? <h3 className="aibui-stats-title">{title}</h3> : null}
146 <StatsSVG
147 type={chartType}
148 data={parsed}
149 color={color}
150 xLabel={xLabel}
151 yLabel={yLabel}
152 />
153 {chartType !== "pie" &&
154 Array.isArray(parsed.series) &&
155 parsed.series.some((s) => s && s.name) ? (
156 <div className="aibui-stats-legend">
157 {parsed.series.map((s, si) =>
158 s && s.name ? (
159 <span key={si} className="aibui-stats-legend-item">
160 <span
161 className="aibui-stats-legend-dot"
162 style={{
163 backgroundColor: shade(color, si, parsed.series.length),
164 }}
165 ></span>
166 {s.name}
167 </span>
168 ) : null
169 )}
170 </div>
171 ) : null}
172 </div>
173 </div>
174 );
175 },
176 });
177
178 function StatsSVG({ type, data, color, xLabel, yLabel }) {
179 const PAD_LEFT = 56; // wider to avoid clipping large y tick labels
180 const PAD_BOTTOM = 24;
181 const BASE_W = 200;
182 const H = 200;
183 const labelsCommon = Array.isArray(data.labels) ? data.labels : [];
184 const W =
185 type === "pie"
186 ? BASE_W
187 : Math.max(BASE_W, PAD_LEFT + 8 + labelsCommon.length * 48);
188
189 if (type === "pie") {
190 const labels = data.labels || [];
191 let values = (data.data || []).map((v) => Number(v) || 0);
192 // Fallback: if user switched from bar/line and kept previous schema
193 if (!values.length && Array.isArray(data.series) && data.series.length) {
194 values = (data.series[0].data || []).map((v) => Number(v) || 0);
195 }
196 // Still empty: render nothing but keep container
197 if (!values.length) {
198 return <svg viewBox={`0 0 ${W} ${H}`} className="aibui-stats-svg" />;
199 }
200 const total = values.reduce((s, v) => s + v, 0) || 1;
201 let acc = 0;
202 return (
203 <svg viewBox={`0 0 ${W} ${H}`} className="aibui-stats-svg">
204 {values.map((val, i) => {
205 const angle = (val / total) * Math.PI * 2;
206 const x1 = W / 2 + (W / 2) * Math.cos(acc);
207 const y1 = H / 2 + (H / 2) * Math.sin(acc);
208 acc += angle;
209 const x2 = W / 2 + (W / 2) * Math.cos(acc);
210 const y2 = H / 2 + (H / 2) * Math.sin(acc);
211 const large = angle > Math.PI ? 1 : 0;
212 const path = `M${W / 2},${H / 2} L${x1},${y1} A${W / 2},${
213 H / 2
214 } 0 ${large} 1 ${x2},${y2} z`;
215 return (
216 <path
217 key={i}
218 d={path}
219 fill={shade(color, i, values.length)}
220 data-label={labels[i] != null ? labels[i] : ""}
221 data-value={val}
222 data-series={""}
223 />
224 );
225 })}
226 {(() => {
227 let angleAcc = 0;
228 return values.map((val, i) => {
229 const angle = (val / total) * Math.PI * 2;
230 const mid = angleAcc + angle / 2;
231 angleAcc += angle;
232 const lx = W / 2 + (W / 3) * Math.cos(mid);
233 const ly = H / 2 + (H / 3) * Math.sin(mid);
234 const label = labels[i] != null ? labels[i] : "";
235 return (
236 <text
237 key={`t${i}`}
238 x={lx}
239 y={ly}
240 fontSize="10"
241 textAnchor="middle"
242 dominantBaseline="middle"
243 >
244 {label}
245 </text>
246 );
247 });
248 })()}
249 </svg>
250 );
251 }
252
253 const labels = labelsCommon;
254 const series = Array.isArray(data.series) ? data.series : [];
255 const maxVal = Math.max(
256 1,
257 ...series.flatMap((s) => s.data.map((v) => Number(v) || 0))
258 );
259 // Compute "nice" step and max (1/2/5 * 10^n) similar to d3.ticks
260 function niceStep(maxValue, tickCount) {
261 const raw = maxValue / Math.max(1, tickCount);
262 const power = Math.pow(10, Math.floor(Math.log10(raw)));
263 const fraction = raw / power;
264 let niceFraction;
265 if (fraction <= 1) niceFraction = 1;
266 else if (fraction <= 2) niceFraction = 2;
267 else if (fraction <= 5) niceFraction = 5;
268 else niceFraction = 10;
269 return niceFraction * power;
270 }
271 const yTicks = 4;
272 const step = niceStep(maxVal, yTicks);
273 const niceMax = step * yTicks;
274
275 if (type === "line") {
276 const xStep = (W - PAD_LEFT - 8) / Math.max(1, labels.length - 1);
277 return (
278 <svg viewBox={`0 0 ${W} ${H}`} className="aibui-stats-svg">
279 <line
280 x1={PAD_LEFT}
281 y1={H - PAD_BOTTOM}
282 x2={W - 8}
283 y2={H - PAD_BOTTOM}
284 stroke="#e5e7eb"
285 />
286 <line
287 x1={PAD_LEFT}
288 y1={8}
289 x2={PAD_LEFT}
290 y2={H - PAD_BOTTOM}
291 stroke="#e5e7eb"
292 />
293 {[...Array(yTicks + 1)].map((_, i) => {
294 const val = i * step;
295 const y = H - PAD_BOTTOM - (val / niceMax) * (H - PAD_BOTTOM - 20);
296 return (
297 <g key={`yt${i}`}>
298 <line
299 x1={PAD_LEFT - 3}
300 y1={y}
301 x2={PAD_LEFT}
302 y2={y}
303 stroke="#9ca3af"
304 />
305 <text
306 x={PAD_LEFT - 6}
307 y={y}
308 fontSize="9"
309 textAnchor="end"
310 dominantBaseline="middle"
311 >
312 {val}
313 </text>
314 </g>
315 );
316 })}
317 {labels.map((lab, i) => {
318 const x = PAD_LEFT + i * xStep;
319 return (
320 <text
321 key={`xl${i}`}
322 x={x}
323 y={H - PAD_BOTTOM + 12}
324 fontSize="9"
325 textAnchor="middle"
326 >
327 {lab}
328 </text>
329 );
330 })}
331 {xLabel ? (
332 <text
333 x={(W + PAD_LEFT) / 2}
334 y={H - 4}
335 fontSize="10"
336 textAnchor="middle"
337 >
338 {xLabel}
339 </text>
340 ) : null}
341 {yLabel ? (
342 <text
343 transform={`translate(12,${H / 2}) rotate(-90)`}
344 fontSize="10"
345 textAnchor="middle"
346 >
347 {yLabel}
348 </text>
349 ) : null}
350 {series.map((s, si) => {
351 const pts = (s.data || []).map((v, i) => {
352 const x =
353 PAD_LEFT +
354 (i / Math.max(1, labels.length - 1)) * (W - PAD_LEFT - 8);
355 const y =
356 H -
357 PAD_BOTTOM -
358 ((Number(v) || 0) / niceMax) * (H - PAD_BOTTOM - 20);
359 return `${x},${y}`;
360 });
361 return (
362 <g key={si}>
363 <polyline
364 fill="none"
365 stroke={shade(color, si, series.length)}
366 strokeWidth="2"
367 points={pts.join(" ")}
368 />
369 {(s.data || []).map((v, i) => {
370 const x =
371 PAD_LEFT +
372 (i / Math.max(1, labels.length - 1)) * (W - PAD_LEFT - 8);
373 const y =
374 H -
375 PAD_BOTTOM -
376 ((Number(v) || 0) / niceMax) * (H - PAD_BOTTOM - 20);
377 return (
378 <circle
379 key={`pt-${si}-${i}`}
380 cx={x}
381 cy={y}
382 r="3"
383 fill={shade(color, si, series.length)}
384 data-label={labels[i] != null ? labels[i] : ""}
385 data-value={Number(v) || 0}
386 data-series={s && s.name ? s.name : ""}
387 />
388 );
389 })}
390 </g>
391 );
392 })}
393 </svg>
394 );
395 }
396
397 // bar (grouped)
398 const groupWidth = (W - PAD_LEFT - 8) / Math.max(1, labels.length);
399 const barWidth = Math.max(2, (groupWidth - 8) / Math.max(1, series.length));
400 return (
401 <svg viewBox={`0 0 ${W} ${H}`} className="aibui-stats-svg">
402 <line
403 x1={PAD_LEFT}
404 y1={H - PAD_BOTTOM}
405 x2={W - 8}
406 y2={H - PAD_BOTTOM}
407 stroke="#e5e7eb"
408 />
409 <line
410 x1={PAD_LEFT}
411 y1={8}
412 x2={PAD_LEFT}
413 y2={H - PAD_BOTTOM}
414 stroke="#e5e7eb"
415 />
416 {[...Array(5)].map((_, i) => {
417 const val = i * step;
418 const y = H - PAD_BOTTOM - (val / niceMax) * (H - PAD_BOTTOM - 20);
419 return (
420 <g key={`ybt${i}`}>
421 <line
422 x1={PAD_LEFT - 3}
423 y1={y}
424 x2={PAD_LEFT}
425 y2={y}
426 stroke="#9ca3af"
427 />
428 <text
429 x={PAD_LEFT - 6}
430 y={y}
431 fontSize="9"
432 textAnchor="end"
433 dominantBaseline="middle"
434 >
435 {val}
436 </text>
437 </g>
438 );
439 })}
440 {labels.map((lab, li) => {
441 const x = PAD_LEFT + li * groupWidth + groupWidth / 2;
442 return (
443 <text
444 key={`xbl${li}`}
445 x={x}
446 y={H - PAD_BOTTOM + 12}
447 fontSize="9"
448 textAnchor="middle"
449 >
450 {lab}
451 </text>
452 );
453 })}
454 {xLabel ? (
455 <text
456 x={(W + PAD_LEFT) / 2}
457 y={H - 4}
458 fontSize="10"
459 textAnchor="middle"
460 >
461 {xLabel}
462 </text>
463 ) : null}
464 {yLabel ? (
465 <text
466 transform={`translate(12,${H / 2}) rotate(-90)`}
467 fontSize="10"
468 textAnchor="middle"
469 >
470 {yLabel}
471 </text>
472 ) : null}
473 {labels.map((lab, li) => {
474 return series.map((s, si) => {
475 const val = Number((s.data || [])[li]) || 0;
476 const h = (val / niceMax) * (H - PAD_BOTTOM - 20);
477 const x = PAD_LEFT + li * groupWidth + 4 + si * barWidth;
478 const y = H - PAD_BOTTOM - h;
479 return (
480 <rect
481 key={`${li}-${si}`}
482 x={x}
483 y={y}
484 width={barWidth}
485 height={h}
486 fill={shade(color, si, series.length)}
487 rx="3"
488 data-label={lab}
489 data-value={val}
490 data-series={s && s.name ? s.name : ""}
491 />
492 );
493 });
494 })}
495 </svg>
496 );
497 }
498
499 function shade(hex, index, total) {
500 try {
501 const amt = Math.round(((index / Math.max(1, total - 1)) * 2 - 1) * 20);
502 let col = hex.replace("#", "");
503 if (col.length === 3)
504 col = col
505 .split("")
506 .map((c) => c + c)
507 .join("");
508 let r = Math.max(0, Math.min(255, parseInt(col.substring(0, 2), 16) + amt));
509 let g = Math.max(0, Math.min(255, parseInt(col.substring(2, 4), 16) + amt));
510 let b = Math.max(0, Math.min(255, parseInt(col.substring(4, 6), 16) + amt));
511 return `#${r.toString(16).padStart(2, "0")}${g
512 .toString(16)
513 .padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
514 } catch (e) {
515 return hex;
516 }
517 }
518