| 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, SelectControl, ToggleControl, TextControl, Button } = window.wp.components; |
| 6 |
const { __ } = window.wp.i18n; |
| 7 |
|
| 8 |
var _tc, _tv; |
| 9 |
function getTypoControl() { return _tc || (_tc = window.bkbgTypographyControl); } |
| 10 |
function getTypoCssVars() { return _tv || (_tv = window.bkbgTypoCssVars); } |
| 11 |
|
| 12 |
/* ---- SVG pie math ---- */ |
| 13 |
function buildPieSlices(slices, cx, cy, r) { |
| 14 |
const total = slices.reduce((s, sl) => s + (Number(sl.value) || 0), 0); |
| 15 |
if (!total) return []; |
| 16 |
let ang = -Math.PI / 2; |
| 17 |
return slices.map(sl => { |
| 18 |
const frac = (Number(sl.value) || 0) / total; |
| 19 |
const sweep = frac * 2 * Math.PI; |
| 20 |
const end = ang + sweep; |
| 21 |
const x1 = cx + r * Math.cos(ang), y1 = cy + r * Math.sin(ang); |
| 22 |
const x2 = cx + r * Math.cos(end), y2 = cy + r * Math.sin(end); |
| 23 |
const large = sweep > Math.PI ? 1 : 0; |
| 24 |
const mid = ang + sweep / 2; |
| 25 |
const lx = cx + (r * 0.62) * Math.cos(mid); |
| 26 |
const ly = cy + (r * 0.62) * Math.sin(mid); |
| 27 |
const pct = Math.round(frac * 100); |
| 28 |
const path = `M ${cx} ${cy} L ${x1.toFixed(2)} ${y1.toFixed(2)} A ${r} ${r} 0 ${large} 1 ${x2.toFixed(2)} ${y2.toFixed(2)} Z`; |
| 29 |
ang = end; |
| 30 |
return { path, color: sl.color, label: sl.label, pct, lx: lx.toFixed(2), ly: ly.toFixed(2) }; |
| 31 |
}); |
| 32 |
} |
| 33 |
|
| 34 |
function renderSVG(a) { |
| 35 |
const half = a.size / 2; |
| 36 |
const r = half - 4; |
| 37 |
const slices = buildPieSlices(a.slices, half, half, r); |
| 38 |
return el('svg', { |
| 39 |
className: 'bkbg-piec-svg', |
| 40 |
width: a.size, height: a.size, |
| 41 |
viewBox: `0 0 ${a.size} ${a.size}`, |
| 42 |
}, |
| 43 |
slices.map((s, i) => |
| 44 |
el('path', { |
| 45 |
key: i, |
| 46 |
d: s.path, |
| 47 |
fill: s.color, |
| 48 |
stroke: a.strokeColor, |
| 49 |
strokeWidth: a.strokeWidth, |
| 50 |
}) |
| 51 |
), |
| 52 |
a.showLabels && a.showPercentages && slices.map((s, i) => |
| 53 |
s.pct > 5 && el('text', { |
| 54 |
key: 'lbl-' + i, |
| 55 |
x: s.lx, y: s.ly, |
| 56 |
textAnchor: 'middle', |
| 57 |
dominantBaseline: 'middle', |
| 58 |
fill: '#ffffff', |
| 59 |
fontSize: 13, |
| 60 |
fontWeight: 700, |
| 61 |
fontFamily: 'inherit', |
| 62 |
}, s.pct + '%') |
| 63 |
), |
| 64 |
); |
| 65 |
} |
| 66 |
|
| 67 |
function renderLegend(a) { |
| 68 |
return el('ul', { className: 'bkbg-piec-legend' }, |
| 69 |
a.slices.map((s, i) => |
| 70 |
el('li', { key: i, className: 'bkbg-piec-legend-item' }, |
| 71 |
el('span', { className: 'bkbg-piec-legend-dot', style: { background: s.color } }), |
| 72 |
el('span', { className: 'bkbg-piec-legend-label' }, s.label), |
| 73 |
) |
| 74 |
) |
| 75 |
); |
| 76 |
} |
| 77 |
|
| 78 |
function wrapStyle(a) { |
| 79 |
return { |
| 80 |
'--bkbg-piec-bg': a.bgColor, |
| 81 |
'--bkbg-piec-title-c': a.titleColor, |
| 82 |
'--bkbg-piec-sub-c': a.subtitleColor, |
| 83 |
'--bkbg-piec-legend-c': a.legendColor, |
| 84 |
'--bkbg-piec-radius': a.borderRadius + 'px', |
| 85 |
'--bkbg-piec-pt': a.paddingTop + 'px', |
| 86 |
'--bkbg-piec-pb': a.paddingBottom + 'px', |
| 87 |
}; |
| 88 |
} |
| 89 |
|
| 90 |
function wrapStyleLegacy(a) { |
| 91 |
return { |
| 92 |
'--bkbg-piec-bg': a.bgColor, |
| 93 |
'--bkbg-piec-title-c': a.titleColor, |
| 94 |
'--bkbg-piec-sub-c': a.subtitleColor, |
| 95 |
'--bkbg-piec-legend-c': a.legendColor, |
| 96 |
'--bkbg-piec-title-sz': a.titleSize + 'px', |
| 97 |
'--bkbg-piec-title-w': a.titleWeight, |
| 98 |
'--bkbg-piec-sub-sz': a.subtitleSize + 'px', |
| 99 |
'--bkbg-piec-legend-sz': a.legendSize + 'px', |
| 100 |
'--bkbg-piec-radius': a.borderRadius + 'px', |
| 101 |
'--bkbg-piec-pt': a.paddingTop + 'px', |
| 102 |
'--bkbg-piec-pb': a.paddingBottom + 'px', |
| 103 |
}; |
| 104 |
} |
| 105 |
|
| 106 |
const LEGEND_POSITIONS = [ |
| 107 |
{ label: 'Right', value: 'right' }, |
| 108 |
{ label: 'Bottom', value: 'bottom' }, |
| 109 |
{ label: 'Left', value: 'left' }, |
| 110 |
{ label: 'Top', value: 'top' }, |
| 111 |
]; |
| 112 |
const LAYOUTS = [ |
| 113 |
{ label: 'Side (chart + legend)', value: 'side' }, |
| 114 |
{ label: 'Stacked (chart on top)', value: 'stacked' }, |
| 115 |
]; |
| 116 |
const ALIGNS = [ |
| 117 |
{ label: 'Left', value: 'left' }, |
| 118 |
{ label: 'Center', value: 'center' }, |
| 119 |
{ label: 'Right', value: 'right' }, |
| 120 |
]; |
| 121 |
|
| 122 |
registerBlockType('blockenberg/pie-chart', { |
| 123 |
edit: function (props) { |
| 124 |
const { attributes: a, setAttributes } = props; |
| 125 |
|
| 126 |
function updateSlice(i, key, val) { |
| 127 |
const next = a.slices.slice(); |
| 128 |
next[i] = Object.assign({}, next[i], { [key]: val }); |
| 129 |
setAttributes({ slices: next }); |
| 130 |
} |
| 131 |
function removeSlice(i) { setAttributes({ slices: a.slices.filter((_, x) => x !== i) }); } |
| 132 |
function addSlice() { |
| 133 |
const colors = ['#6c3fb5','#3b82f6','#10b981','#f59e0b','#ef4444','#8b5cf6','#06b6d4']; |
| 134 |
setAttributes({ slices: [...a.slices, { label: 'New Slice', value: 10, color: colors[a.slices.length % colors.length] }] }); |
| 135 |
} |
| 136 |
|
| 137 |
const blockProps = useBlockProps((function () { |
| 138 |
var tv = getTypoCssVars(); |
| 139 |
var s = wrapStyle(a); |
| 140 |
Object.assign(s, tv(a.titleTypo, '--bkbg-piec-tt-')); |
| 141 |
Object.assign(s, tv(a.subtitleTypo, '--bkbg-piec-st-')); |
| 142 |
Object.assign(s, tv(a.legendTypo, '--bkbg-piec-lg-')); |
| 143 |
return { className: 'bkbg-piec-wrap bkbg-piec-layout--' + a.layout + ' bkbg-piec-title-align--' + a.titleAlign, style: s }; |
| 144 |
})()); |
| 145 |
|
| 146 |
return el('div', blockProps, |
| 147 |
el(InspectorControls, null, |
| 148 |
|
| 149 |
el(PanelBody, { title: __('Chart Data'), initialOpen: true }, |
| 150 |
a.slices.map((sl, i) => |
| 151 |
el('div', { key: i, style: { border: '1px solid #e2e8f0', borderRadius: 8, padding: '10px', marginBottom: 8 } }, |
| 152 |
el(TextControl, { label: __('Label'), value: sl.label, onChange: v => updateSlice(i, 'label', v) }), |
| 153 |
el(RangeControl, { label: __('Value'), value: sl.value, min: 1, max: 10000, onChange: v => updateSlice(i, 'value', v) }), |
| 154 |
el('div', { style: { marginBottom: 8 } }, |
| 155 |
el('label', { style: { fontSize: 11, fontWeight: 600, display: 'block', marginBottom: 4 } }, __('Color')), |
| 156 |
el('input', { type: 'color', value: sl.color, onChange: e => updateSlice(i, 'color', e.target.value), style: { width: '100%', height: 32, border: '1px solid #ccc', borderRadius: 4, cursor: 'pointer' } }), |
| 157 |
), |
| 158 |
a.slices.length > 2 && el(Button, { isDestructive: true, isSmall: true, onClick: () => removeSlice(i) }, __('Remove')), |
| 159 |
) |
| 160 |
), |
| 161 |
el(Button, { variant: 'secondary', onClick: addSlice, style: { width: '100%', justifyContent: 'center', marginTop: 8 } }, __('+ Add Slice')), |
| 162 |
), |
| 163 |
|
| 164 |
el(PanelBody, { title: __('Title & Labels'), initialOpen: false }, |
| 165 |
el(ToggleControl, { label: __('Show Title'), checked: a.showTitle, onChange: v => setAttributes({ showTitle: v }), __nextHasNoMarginBottom: true }), |
| 166 |
a.showTitle && el(TextControl, { label: __('Title'), value: a.title, onChange: v => setAttributes({ title: v }) }), |
| 167 |
el(ToggleControl, { label: __('Show Subtitle'), checked: a.showSubtitle, onChange: v => setAttributes({ showSubtitle: v }), __nextHasNoMarginBottom: true }), |
| 168 |
a.showSubtitle && el(TextControl, { label: __('Subtitle'), value: a.subtitle, onChange: v => setAttributes({ subtitle: v }) }), |
| 169 |
el(SelectControl, { label: __('Title Align'), value: a.titleAlign, options: ALIGNS, onChange: v => setAttributes({ titleAlign: v }) }), |
| 170 |
el(ToggleControl, { label: __('Show % Labels on Slices'), checked: a.showLabels && a.showPercentages, onChange: v => setAttributes({ showLabels: v, showPercentages: v }), __nextHasNoMarginBottom: true }), |
| 171 |
el(ToggleControl, { label: __('Show Legend'), checked: a.showLegend, onChange: v => setAttributes({ showLegend: v }), __nextHasNoMarginBottom: true }), |
| 172 |
a.showLegend && el(SelectControl, { label: __('Legend Position'), value: a.legendPosition, options: LEGEND_POSITIONS, onChange: v => setAttributes({ legendPosition: v }) }), |
| 173 |
), |
| 174 |
|
| 175 |
el(PanelBody, { title: __('Chart Dimensions'), initialOpen: false }, |
| 176 |
el(RangeControl, { label: __('Chart Diameter (px)'), value: a.size, min: 140, max: 500, onChange: v => setAttributes({ size: v }) }), |
| 177 |
el(RangeControl, { label: __('Stroke Width'), value: a.strokeWidth, min: 0, max: 8, onChange: v => setAttributes({ strokeWidth: v }) }), |
| 178 |
el(SelectControl, { label: __('Layout'), value: a.layout, options: LAYOUTS, onChange: v => setAttributes({ layout: v }) }), |
| 179 |
), |
| 180 |
|
| 181 |
el(PanelBody, { title: __('Spacing & Style'), initialOpen: false }, |
| 182 |
el(RangeControl, { label: __('Border Radius (px)'), value: a.borderRadius, min: 0, max: 32, onChange: v => setAttributes({ borderRadius: v }) }), |
| 183 |
el(RangeControl, { label: __('Padding Top (px)'), value: a.paddingTop, min: 0, max: 160, onChange: v => setAttributes({ paddingTop: v }) }), |
| 184 |
el(RangeControl, { label: __('Padding Bottom (px)'), value: a.paddingBottom, min: 0, max: 160, onChange: v => setAttributes({ paddingBottom: v }) }), |
| 185 |
|
| 186 |
), |
| 187 |
|
| 188 |
|
| 189 |
el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false }, |
| 190 |
el(getTypoControl(), { label: __('Title', 'blockenberg'), value: a.titleTypo, onChange: v => setAttributes({ titleTypo: v }) }), |
| 191 |
el(getTypoControl(), { label: __('Subtitle', 'blockenberg'), value: a.subtitleTypo, onChange: v => setAttributes({ subtitleTypo: v }) }), |
| 192 |
el(getTypoControl(), { label: __('Legend', 'blockenberg'), value: a.legendTypo, onChange: v => setAttributes({ legendTypo: v }) }) |
| 193 |
), |
| 194 |
el(PanelColorSettings, { |
| 195 |
title: __('Colors'), initialOpen: false, |
| 196 |
colorSettings: [ |
| 197 |
{ label: __('Background'), value: a.bgColor, onChange: v => setAttributes({ bgColor: v || '#ffffff' }) }, |
| 198 |
{ label: __('Title'), value: a.titleColor, onChange: v => setAttributes({ titleColor: v || '#0f172a' }) }, |
| 199 |
{ label: __('Subtitle'), value: a.subtitleColor, onChange: v => setAttributes({ subtitleColor: v || '#64748b' }) }, |
| 200 |
{ label: __('Legend Text'), value: a.legendColor, onChange: v => setAttributes({ legendColor: v || '#374151' }) }, |
| 201 |
{ label: __('Slice Stroke'), value: a.strokeColor, onChange: v => setAttributes({ strokeColor: v || '#ffffff' }) }, |
| 202 |
] |
| 203 |
}), |
| 204 |
), |
| 205 |
|
| 206 |
/* ---- Canvas ---- */ |
| 207 |
a.showTitle && el('h3', { className: 'bkbg-piec-title' }, a.title), |
| 208 |
a.showSubtitle && el('p', { className: 'bkbg-piec-subtitle' }, a.subtitle), |
| 209 |
el('div', { className: 'bkbg-piec-chart-row bkbg-piec-legend-pos--' + a.legendPosition }, |
| 210 |
renderSVG(a), |
| 211 |
a.showLegend && renderLegend(a), |
| 212 |
), |
| 213 |
); |
| 214 |
}, |
| 215 |
|
| 216 |
save: function ({ attributes: a }) { |
| 217 |
var tv = getTypoCssVars(); |
| 218 |
var s = wrapStyle(a); |
| 219 |
Object.assign(s, tv(a.titleTypo, '--bkbg-piec-tt-')); |
| 220 |
Object.assign(s, tv(a.subtitleTypo, '--bkbg-piec-st-')); |
| 221 |
Object.assign(s, tv(a.legendTypo, '--bkbg-piec-lg-')); |
| 222 |
return el('div', { |
| 223 |
className: 'bkbg-piec-wrap bkbg-piec-layout--' + a.layout + ' bkbg-piec-title-align--' + a.titleAlign, |
| 224 |
style: s, |
| 225 |
}, |
| 226 |
a.showTitle && el('h3', { className: 'bkbg-piec-title' }, a.title), |
| 227 |
a.showSubtitle && el('p', { className: 'bkbg-piec-subtitle' }, a.subtitle), |
| 228 |
el('div', { className: 'bkbg-piec-chart-row bkbg-piec-legend-pos--' + a.legendPosition }, |
| 229 |
renderSVG(a), |
| 230 |
a.showLegend && el('ul', { className: 'bkbg-piec-legend' }, |
| 231 |
a.slices.map((s, i) => |
| 232 |
el('li', { key: i, className: 'bkbg-piec-legend-item' }, |
| 233 |
el('span', { className: 'bkbg-piec-legend-dot', style: { background: s.color } }), |
| 234 |
el('span', { className: 'bkbg-piec-legend-label' }, s.label), |
| 235 |
) |
| 236 |
) |
| 237 |
), |
| 238 |
), |
| 239 |
); |
| 240 |
}, |
| 241 |
|
| 242 |
deprecated: [ |
| 243 |
{ |
| 244 |
attributes: { |
| 245 |
title: { type: 'string', default: 'Revenue by Channel' }, |
| 246 |
showTitle: { type: 'boolean', default: true }, |
| 247 |
subtitle: { type: 'string', default: 'Q1 2025 breakdown' }, |
| 248 |
showSubtitle: { type: 'boolean', default: true }, |
| 249 |
slices: { type: 'array', default: [{label:'Organic Search',value:42,color:'#6c3fb5'},{label:'Direct',value:28,color:'#3b82f6'},{label:'Referral',value:18,color:'#10b981'},{label:'Social Media',value:12,color:'#f59e0b'}] }, |
| 250 |
size: { type: 'integer', default: 280 }, |
| 251 |
showLabels: { type: 'boolean', default: true }, |
| 252 |
showPercentages: { type: 'boolean', default: true }, |
| 253 |
showLegend: { type: 'boolean', default: true }, |
| 254 |
legendPosition: { type: 'string', default: 'right' }, |
| 255 |
strokeWidth: { type: 'integer', default: 2 }, |
| 256 |
strokeColor: { type: 'string', default: '#ffffff' }, |
| 257 |
layout: { type: 'string', default: 'side' }, |
| 258 |
titleAlign: { type: 'string', default: 'center' }, |
| 259 |
borderRadius: { type: 'integer', default: 16 }, |
| 260 |
paddingTop: { type: 'integer', default: 48 }, |
| 261 |
paddingBottom: { type: 'integer', default: 48 }, |
| 262 |
titleSize: { type: 'integer', default: 24 }, |
| 263 |
titleWeight: { type: 'integer', default: 700 }, |
| 264 |
subtitleSize: { type: 'integer', default: 15 }, |
| 265 |
legendSize: { type: 'integer', default: 14 }, |
| 266 |
bgColor: { type: 'string', default: '#ffffff' }, |
| 267 |
titleColor: { type: 'string', default: '#0f172a' }, |
| 268 |
subtitleColor: { type: 'string', default: '#64748b' }, |
| 269 |
legendColor: { type: 'string', default: '#374151' }, |
| 270 |
titleFontWeight: { type: 'string', default: '700' }, |
| 271 |
subtitleFontWeight: { type: 'string', default: '400' }, |
| 272 |
}, |
| 273 |
save: function ({ attributes: a }) { |
| 274 |
return el('div', { |
| 275 |
className: 'bkbg-piec-wrap bkbg-piec-layout--' + a.layout + ' bkbg-piec-title-align--' + a.titleAlign, |
| 276 |
style: wrapStyleLegacy(a), |
| 277 |
}, |
| 278 |
a.showTitle && el('h3', { className: 'bkbg-piec-title' }, a.title), |
| 279 |
a.showSubtitle && el('p', { className: 'bkbg-piec-subtitle' }, a.subtitle), |
| 280 |
el('div', { className: 'bkbg-piec-chart-row bkbg-piec-legend-pos--' + a.legendPosition }, |
| 281 |
renderSVG(a), |
| 282 |
a.showLegend && el('ul', { className: 'bkbg-piec-legend' }, |
| 283 |
a.slices.map((s, i) => |
| 284 |
el('li', { key: i, className: 'bkbg-piec-legend-item' }, |
| 285 |
el('span', { className: 'bkbg-piec-legend-dot', style: { background: s.color } }), |
| 286 |
el('span', { className: 'bkbg-piec-legend-label', style: { color: a.legendColor, fontSize: a.legendSize + 'px' } }, s.label), |
| 287 |
) |
| 288 |
) |
| 289 |
), |
| 290 |
), |
| 291 |
); |
| 292 |
} |
| 293 |
} |
| 294 |
] |
| 295 |
}); |
| 296 |
}() ); |
| 297 |
|