| 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 |
|