PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.7
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.7
2.0.13 2.0.12 2.0.11 2.0.10 2.0.9 trunk 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8
blockenberg / blocks / chord-diagram / index.js

index.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.7, at blocks/chord-diagram/index.js

263 lines 16.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 ( function () {
2 const el = window.wp.element.createElement;
3 const { registerBlockType } = window.wp.blocks;
4 const { InspectorControls, useBlockProps, PanelColorSettings } = window.wp.blockEditor;
5 const { PanelBody, RangeControl, ToggleControl, TextControl, Button, ColorPicker, Popover } = window.wp.components;
6 const { __ } = window.wp.i18n;
7 const { useState } = window.wp.element;
8 function getTypographyControl() { return (window.bkbgTypographyControl || function () { return null; }); }
9 function getTypoCssVars() { return (window.bkbgTypoCssVars || function () { return {}; }); }
10 function _tv(typo, prefix) { var fn = getTypoCssVars(); return fn(typo || {}, prefix); }
11
12 const TAU = 2 * Math.PI;
13
14 // ── Arc path helper ───────────────────────────────────────────────────────
15 function arcPath( cx, cy, r, startAngle, endAngle, thickness ) {
16 const innerR = r - thickness;
17 const cos0 = Math.cos, sin0 = Math.sin;
18 const sx1 = cx + r * cos0( startAngle ), sy1 = cy + r * sin0( startAngle );
19 const sx2 = cx + r * cos0( endAngle ), sy2 = cy + r * sin0( endAngle );
20 const ix1 = cx + innerR * cos0( endAngle ), iy1 = cy + innerR * sin0( endAngle );
21 const ix2 = cx + innerR * cos0( startAngle ), iy2 = cy + innerR * sin0( startAngle );
22 const largeArc = ( endAngle - startAngle ) > Math.PI ? 1 : 0;
23 return `M ${ sx1 } ${ sy1 } A ${ r } ${ r } 0 ${ largeArc } 1 ${ sx2 } ${ sy2 } L ${ ix1 } ${ iy1 } A ${ innerR } ${ innerR } 0 ${ largeArc } 0 ${ ix2 } ${ iy2 } Z`;
24 }
25
26 // ── Chord path helper (ribbon between two arc segments) ───────────────────
27 function chordPath( cx, cy, r, sa0, sa1, ta0, ta1 ) {
28 const innerR = r;
29 const p = ( a ) => ( { x: cx + innerR * Math.cos( a ), y: cy + innerR * Math.sin( a ) } );
30 const s0 = p( sa0 ), s1 = p( sa1 ), t0 = p( ta0 ), t1 = p( ta1 );
31 return `M ${ s0.x } ${ s0.y } A ${ innerR } ${ innerR } 0 0 1 ${ s1.x } ${ s1.y } Q ${ cx } ${ cy } ${ t0.x } ${ t0.y } A ${ innerR } ${ innerR } 0 0 1 ${ t1.x } ${ t1.y } Q ${ cx } ${ cy } ${ s0.x } ${ s0.y } Z`;
32 }
33
34 // ── Main renderer ─────────────────────────────────────────────────────────
35 function renderChord( a ) {
36 const S = a.svgSize;
37 const cx = S / 2, cy = S / 2;
38 const R = S / 2 - 60; // outer radius (leave room for labels)
39 const inner = R - a.arcThickness;
40 const gapAngle = ( a.arcGap / 180 ) * Math.PI; // gap in radians
41 const groups = a.groups || [];
42 const matrix = a.matrix || [];
43 const n = groups.length;
44 if ( n < 2 ) return el( 'svg', { viewBox: `0 0 ${ S } ${ S }`, width: '100%' }, el( 'text', { x: cx, y: cy, textAnchor: 'middle', fill: '#9ca3af' }, __( 'Add at least 2 groups', 'blockenberg' ) ) );
45
46 // Row totals → group "size"
47 const totals = groups.map( ( _, i ) => {
48 const row = matrix[i] || [];
49 return row.reduce( ( s, v ) => s + ( v || 0 ), 0 );
50 } );
51 const grand = totals.reduce( ( s, v ) => s + v, 0 ) || 1;
52
53 // Angle spans per group
54 const totalAngle = TAU - n * gapAngle;
55 const spans = totals.map( t => ( t / grand ) * totalAngle );
56
57 // Start angles
58 const startAngles = [];
59 let angle = -Math.PI / 2; // start at top
60 for ( let i = 0; i < n; i++ ) {
61 startAngles.push( angle );
62 angle += spans[i] + gapAngle;
63 }
64
65 // Track sub-angle offsets for chords
66 const outOffset = startAngles.slice();
67
68 const svgEls = [];
69 svgEls.push( el( 'rect', { key: 'bg', x: 0, y: 0, width: S, height: S, fill: a.bgColor || '#ffffff', rx: 10 } ) );
70
71 // ── Draw chords (behind arcs) ─────────────────────────────────────────
72 const op = ( a.chordOpacity || 55 ) / 100;
73
74 for ( let i = 0; i < n; i++ ) {
75 for ( let j = i + 1; j < n; j++ ) {
76 const val = ( matrix[i] && matrix[i][j] ) || 0;
77 const valji = ( matrix[j] && matrix[j][i] ) || 0;
78 if ( val + valji === 0 ) continue;
79
80 // Source slice for i→j
81 const iFrac = spans[i] / totalAngle; // proportional
82 const jFrac = spans[j] / totalAngle;
83
84 const sa0 = outOffset[i];
85 const sa1 = sa0 + ( ( val / grand ) * totalAngle );
86 outOffset[i] = sa1;
87
88 const ta0 = outOffset[j];
89 const ta1 = ta0 + ( ( valji / grand ) * totalAngle );
90 outOffset[j] = ta1;
91
92 const d = chordPath( cx, cy, inner, sa0, sa1, ta0, ta1 );
93 svgEls.push( el( 'path', { key: `chord_${ i }_${ j }`, d, fill: groups[i].color || '#4f46e5', opacity: op } ) );
94 }
95 }
96
97 // ── Draw arcs (on top of chords) ──────────────────────────────────────
98 for ( let i = 0; i < n; i++ ) {
99 const sa = startAngles[i];
100 const ea = sa + spans[i];
101 const color = groups[i].color || '#4f46e5';
102 const d = arcPath( cx, cy, R, sa, ea, a.arcThickness );
103 svgEls.push( el( 'path', { key: `arc_${ i }`, d, fill: color } ) );
104
105 // Labels
106 if ( a.showLabels ) {
107 const midAngle = ( sa + ea ) / 2;
108 const labelR = R + 14;
109 const lx = cx + labelR * Math.cos( midAngle );
110 const ly = cy + labelR * Math.sin( midAngle );
111 const anchor = Math.cos( midAngle ) > 0.1 ? 'start' : Math.cos( midAngle ) < -0.1 ? 'end' : 'middle';
112
113 svgEls.push( el( 'text', {
114 key: `lbl_${ i }`, x: lx, y: ly,
115 textAnchor: anchor, dominantBaseline: 'middle',
116 fill: color, fontSize: a.labelFontSize, fontWeight: a.labelFontWeight, fontFamily: 'inherit',
117 }, groups[i].label || `Group ${ i + 1 }` ) );
118 }
119 }
120
121 return el( 'svg', {
122 viewBox: `0 0 ${ S } ${ S }`, width: '100%',
123 style: { display: 'block', maxWidth: S + 'px', margin: '0 auto' }
124 }, ...svgEls );
125 }
126
127 // ── Helpers ───────────────────────────────────────────────────────────────
128 function ensureMatrix( groups, matrix ) {
129 const n = groups.length;
130 return Array.from( { length: n }, ( _, i ) =>
131 Array.from( { length: n }, ( _, j ) =>
132 ( matrix[i] && matrix[i][j] !== undefined ) ? matrix[i][j] : 0
133 )
134 );
135 }
136
137 function addGroup( a, setAttributes ) {
138 const newGroups = [ ...a.groups, { label: `Group ${ a.groups.length + 1 }`, color: '#6b7280' } ];
139 const n = newGroups.length;
140 const newMatrix = Array.from( { length: n }, ( _, i ) =>
141 Array.from( { length: n }, ( _, j ) =>
142 ( a.matrix[i] && a.matrix[i][j] !== undefined ) ? a.matrix[i][j] : 0
143 )
144 );
145 setAttributes( { groups: newGroups, matrix: newMatrix } );
146 }
147
148 function removeGroup( a, setAttributes, idx ) {
149 const newGroups = a.groups.filter( ( _, i ) => i !== idx );
150 const newMatrix = a.matrix.filter( ( _, i ) => i !== idx ).map( row => row.filter( ( _, j ) => j !== idx ) );
151 setAttributes( { groups: newGroups, matrix: newMatrix } );
152 }
153
154 function updMatrix( a, setAttributes, i, j, val ) {
155 const m = ensureMatrix( a.groups, a.matrix );
156 m[i][j] = val;
157 setAttributes( { matrix: m } );
158 }
159
160 // ── Block ───────────────────────────────────────────────────────────────── /* ── colour-swatch + popover ── */
161 function BkbgColorSwatch(p) {
162 var st = useState(false), open = st[0], setOpen = st[1];
163 return el('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'4px 0', gap:'8px' } },
164 el('span', { style:{ fontSize:'12px', color:'#1e1e1e', flex:1, lineHeight:1.4 } }, p.label),
165 el('div', { style:{ position:'relative', flexShrink:0 } },
166 el('button', { type:'button', title: p.value||'none', onClick: function(){ setOpen(!open); },
167 style:{ width:'28px', height:'28px', borderRadius:'4px', border: open ? '2px solid #007cba' : '2px solid #ddd', cursor:'pointer', padding:0, display:'block', background: p.value||'#ffffff', flexShrink:0 } }),
168 open && el(Popover, { position:'bottom left', onClose: function(){ setOpen(false); } },
169 el('div', { style:{ padding:'8px' }, onMouseDown: function(e){ e.stopPropagation(); } },
170 el('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:'6px' } },
171 el('strong', { style:{ fontSize:'12px' } }, p.label),
172 el(Button, { icon:'no-alt', isSmall:true, onClick: function(){ setOpen(false); } })
173 ),
174 el(ColorPicker, { color: p.value, enableAlpha:true, onChange: p.onChange })
175 )
176 )
177 )
178 );
179 }
180 registerBlockType( 'blockenberg/chord-diagram', {
181 edit: function ( props ) {
182 const { attributes: a, setAttributes } = props;
183 const blockProps = useBlockProps( { className: 'bkbg-chord-wrap', style: Object.assign({}, _tv(a.typoTitle, '--bkbg-chord-tt-')) } );
184 const m = ensureMatrix( a.groups, a.matrix );
185
186 return el( 'div', blockProps,
187 el( InspectorControls, {},
188
189 el( PanelBody, { title: __( 'Chart Settings', 'blockenberg' ), initialOpen: true },
190 el( TextControl, { label: __( 'Title', 'blockenberg' ), value: a.title, onChange: v => setAttributes( { title: v } ) } ),
191 el( ToggleControl, { label: __( 'Show Title', 'blockenberg' ), checked: a.showTitle, onChange: v => setAttributes( { showTitle: v } ) } ),
192 el( ToggleControl, { label: __( 'Show Labels', 'blockenberg' ), checked: a.showLabels, onChange: v => setAttributes( { showLabels: v } ) } ),
193 el( RangeControl, { label: __( 'Canvas Size', 'blockenberg' ), value: a.svgSize, onChange: v => setAttributes( { svgSize: v } ), min: 280, max: 900, step: 20 } ),
194 el( RangeControl, { label: __( 'Arc Thickness', 'blockenberg' ), value: a.arcThickness, onChange: v => setAttributes( { arcThickness: v } ), min: 8, max: 60 } ),
195 el( RangeControl, { label: __( 'Arc Gap (°)', 'blockenberg' ), value: a.arcGap, onChange: v => setAttributes( { arcGap: v } ), min: 1, max: 30 } ),
196 el( RangeControl, { label: __( 'Chord Opacity %', 'blockenberg' ), value: a.chordOpacity, onChange: v => setAttributes( { chordOpacity: v } ), min: 10, max: 95 } ),
197 ),
198
199
200 el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false },
201 el(getTypographyControl(), { label: __('Title', 'blockenberg'), value: a.typoTitle, onChange: v => setAttributes( { typoTitle: v } ) }),
202 el( RangeControl, { label: __( 'Label Font Size', 'blockenberg' ), value: a.labelFontSize, onChange: v => setAttributes( { labelFontSize: v } ), min: 9, max: 22 } ),
203 el( SelectControl, { label: __( 'Label Font Weight', 'blockenberg' ), value: a.labelFontWeight, options: [
204 { label: 'Normal (400)', value: '400' },
205 { label: 'Medium (500)', value: '500' },
206 { label: 'Semi Bold (600)', value: '600' },
207 { label: 'Bold (700)', value: '700' },
208 { label: 'Extra Bold (800)', value: '800' },
209 ], onChange: v => setAttributes( { labelFontWeight: v } ) } )
210 ),
211 el( PanelColorSettings, {
212 title: __( 'Colors', 'blockenberg' ), initialOpen: false,
213 colorSettings: [
214 { label: __( 'Background', 'blockenberg' ), value: a.bgColor, onChange: v => setAttributes( { bgColor: v || '#ffffff' } ) },
215 { label: __( 'Title Color', 'blockenberg' ), value: a.titleColor, onChange: v => setAttributes( { titleColor: v || '#111827' } ) },
216 ]
217 } ),
218
219 el( PanelBody, { title: __( 'Groups', 'blockenberg' ), initialOpen: false },
220 el( Button, { variant: 'secondary', style: { marginBottom: 10 }, onClick: () => addGroup( a, setAttributes ) }, __( '+ Add Group', 'blockenberg' ) ),
221 a.groups.map( ( g, i ) =>
222 el( PanelBody, { key: i, title: g.label || `Group ${ i + 1 }`, initialOpen: false },
223 el( TextControl, { label: __( 'Label', 'blockenberg' ), value: g.label, onChange: v => setAttributes( { groups: a.groups.map( ( x, xi ) => xi === i ? { ...x, label: v } : x ) } ) } ),
224 el( BkbgColorSwatch, { label: __( 'Color', 'blockenberg' ), value: g.color, onChange: v => setAttributes( { groups: a.groups.map( ( x, xi ) => xi === i ? { ...x, color: v } : x ) } ) } ),
225 el( Button, { isDestructive: true, isSmall: true, onClick: () => removeGroup( a, setAttributes, i ) }, __( 'Remove Group', 'blockenberg' ) ),
226 )
227 ),
228 ),
229
230 el( PanelBody, { title: __( 'Relationship Matrix', 'blockenberg' ), initialOpen: false },
231 el( 'p', { style: { fontSize: 12, color: '#6b7280', marginBottom: 10 } }, __( 'Row → Column flow values. Diagonal (same group) = 0.', 'blockenberg' ) ),
232 a.groups.map( ( rowG, i ) =>
233 el( PanelBody, { key: i, title: `${ rowG.label || `Group ${ i + 1 }` } →`, initialOpen: false },
234 a.groups.map( ( colG, j ) =>
235 i === j ? null :
236 el( RangeControl, {
237 key: j,
238 label: `→ ${ colG.label || `Group ${ j + 1 }` }`,
239 value: m[i][j] || 0,
240 onChange: v => updMatrix( a, setAttributes, i, j, v ),
241 min: 0, max: 1000,
242 } )
243 )
244 )
245 ),
246 ),
247 ),
248
249 a.showTitle && a.title && el( 'h3', { className: 'bkbg-chord-title', style: { color: a.titleColor } }, a.title ),
250 el( 'div', { className: 'bkbg-chord-svg' }, renderChord( a ) ),
251 );
252 },
253
254 save: function ( { attributes: a } ) {
255 const blockProps = useBlockProps.save( { className: 'bkbg-chord-wrap', style: Object.assign({}, _tv(a.typoTitle, '--bkbg-chord-tt-')) } );
256 return el( 'div', blockProps,
257 a.showTitle && a.title ? el( 'h3', { className: 'bkbg-chord-title', style: { color: a.titleColor } }, a.title ) : null,
258 el( 'div', { className: 'bkbg-chord-svg' }, renderChord( a ) ),
259 );
260 },
261 } );
262 }() );
263