PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.14
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.14
2.0.14 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 / treemap-chart / index.js

index.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.14, at blocks/treemap-chart/index.js

268 lines 15.1 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
9 // Resolve the shared components lazily without defining globals for each block.
10 function _tc(props, key, label) {
11 var Control = window.bkbgTypographyControl;
12 return Control ? el(Control, {
13 label: label,
14 value: props.attributes[key] || {},
15 onChange: function (value) { var update = {}; update[key] = value; props.setAttributes(update); }
16 }) : null;
17 }
18 function _tvf(typo, prefix) {
19 var cssVars = window.bkbgTypoCssVars;
20 return cssVars ? cssVars(typo || {}, prefix) : {};
21 }
22 function getTypoControl(props, attrName, label) { return _tc(props, attrName, label); }
23
24 // ── Binary partition treemap algorithm ───────────────────────────────────
25 // Recursively splits the rectangle between items based on value ratio.
26 // Alternates split direction (horizontal → vertical) based on aspect ratio.
27 function partition( items, x, y, w, h ) {
28 if ( !items || items.length === 0 ) return [];
29 if ( items.length === 1 ) return [ { ...items[0], x, y, w, h } ];
30
31 const total = items.reduce( ( s, it ) => s + ( it.value || 0 ), 0 );
32 if ( total === 0 ) return [];
33
34 // Find balanced split index (left total ≥ right total)
35 let cumul = 0, splitIdx = Math.floor( items.length / 2 );
36 for ( let i = 0; i < items.length - 1; i++ ) {
37 cumul += ( items[i].value || 0 );
38 if ( cumul * 2 >= total ) { splitIdx = i + 1; break; }
39 }
40
41 const leftItems = items.slice( 0, splitIdx );
42 const rightItems = items.slice( splitIdx );
43 const leftTotal = leftItems.reduce( ( s, it ) => s + ( it.value || 0 ), 0 );
44 const ratio = leftTotal / total;
45
46 if ( w >= h ) {
47 // Split horizontally
48 const leftW = w * ratio;
49 return [
50 ...partition( leftItems, x, y, leftW, h ),
51 ...partition( rightItems, x + leftW, y, w - leftW, h ),
52 ];
53 } else {
54 // Split vertically
55 const topH = h * ratio;
56 return [
57 ...partition( leftItems, x, y, w, topH ),
58 ...partition( rightItems, x, y + topH, w, h - topH ),
59 ];
60 }
61 }
62
63 // ── Apply gap inset to a cell ─────────────────────────────────────────────
64 function inset( cell, gap ) {
65 const half = gap / 2;
66 return {
67 x: cell.x + half, y: cell.y + half,
68 w: Math.max( 0, cell.w - gap ),
69 h: Math.max( 0, cell.h - gap ),
70 };
71 }
72
73 // ── Helper to check if text fits in a cell ───────────────────────────────
74 function fits( str, cell, fontSize ) {
75 const charW = fontSize * 0.65;
76 return cell.w >= str.length * charW && cell.h >= fontSize * 2;
77 }
78
79 // ── Render treemap SVG ───────────────────────────────────────────────────
80 function renderTreemap( a ) {
81 const W = a.svgWidth, H = a.svgHeight;
82 const sorted = [ ...( a.items || [] ) ].sort( ( a, b ) => ( b.value || 0 ) - ( a.value || 0 ) );
83 const cells = partition( sorted, 0, 0, W, H );
84 const total = sorted.reduce( ( s, it ) => s + ( it.value || 0 ), 0 );
85
86 const svgEls = [];
87
88 // Background
89 svgEls.push( el( 'rect', { key: 'bg', x: 0, y: 0, width: W, height: H, fill: a.bgColor || '#ffffff', rx: 6 } ) );
90
91 cells.forEach( ( cell, i ) => {
92 const pad = inset( cell, a.gap );
93 const rx = a.cornerRadius;
94 const pct = total > 0 ? ( ( cell.value / total ) * 100 ).toFixed( 1 ) : '0';
95 const labelVisible = a.showLabels && fits( cell.label || '', pad, a.labelFontSize );
96 const valueVisible = a.showValues && pad.h >= a.labelFontSize * 2.5 && pad.w > 24;
97
98 // Cell background
99 svgEls.push( el( 'rect', {
100 key: 'r' + i,
101 x: pad.x, y: pad.y, width: pad.w, height: pad.h,
102 fill: cell.color || '#4f46e5', rx,
103 } ) );
104
105 // Overlay for text contrast
106 svgEls.push( el( 'rect', {
107 key: 'ov' + i,
108 x: pad.x, y: pad.y, width: pad.w, height: pad.h,
109 fill: 'rgba(0,0,0,0.12)', rx,
110 } ) );
111
112 if ( labelVisible ) {
113 const midX = pad.x + pad.w / 2;
114 const midY = pad.y + pad.h / 2;
115 const offset = valueVisible ? -( a.labelFontSize * 0.7 ) : 0;
116
117 svgEls.push( el( 'text', {
118 key: 'lbl' + i,
119 x: midX, y: midY + offset,
120 textAnchor: 'middle', dominantBaseline: 'middle',
121 fill: '#ffffff', fontSize: a.labelFontSize, fontWeight: 700,
122 fontFamily: 'inherit',
123 }, cell.label || '' ) );
124 }
125
126 if ( valueVisible ) {
127 const midX = pad.x + pad.w / 2;
128 const midY = pad.y + pad.h / 2;
129 const offset = labelVisible ? a.valueFontSize * 0.9 : 0;
130 const text = a.showPercent ? pct + '%' : ( cell.value || 0 );
131
132 svgEls.push( el( 'text', {
133 key: 'val' + i,
134 x: midX, y: midY + offset,
135 textAnchor: 'middle', dominantBaseline: 'middle',
136 fill: 'rgba(255,255,255,0.85)', fontSize: a.valueFontSize,
137 fontFamily: 'inherit',
138 }, String( text ) ) );
139 }
140 } );
141
142 return el( 'svg', {
143 viewBox: `0 0 ${ W } ${ H }`, width: '100%',
144 style: { display: 'block', maxWidth: W + 'px', margin: '0 auto' }
145 }, ...svgEls );
146 }
147
148 // ── Legend row ────────────────────────────────────────────────────────────
149 function renderLegend( a ) {
150 const total = ( a.items || [] ).reduce( ( s, it ) => s + ( it.value || 0 ), 0 );
151 const sorted = [ ...( a.items || [] ) ].sort( ( a, b ) => ( b.value || 0 ) - ( a.value || 0 ) );
152 return el( 'div', { className: 'bkbg-tm-legend' },
153 sorted.map( ( item, i ) => {
154 const pct = total > 0 ? ( ( item.value / total ) * 100 ).toFixed( 1 ) : '0';
155 return el( 'div', { key: i, className: 'bkbg-tm-legend-item' },
156 el( 'span', { className: 'bkbg-tm-swatch', style: { background: item.color } } ),
157 el( 'span', { className: 'bkbg-tm-legend-label' }, item.label || '' ),
158 el( 'span', { className: 'bkbg-tm-legend-val' }, `${ item.value } (${ pct }%)` ),
159 );
160 } )
161 );
162 }
163
164 // ── Block registration ──────────────────────────────────────────────────── /* ── colour-swatch + popover ── */
165 function BkbgColorSwatch(p) {
166 var st = useState(false), open = st[0], setOpen = st[1];
167 return el('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'4px 0', gap:'8px' } },
168 el('span', { style:{ fontSize:'12px', color:'#1e1e1e', flex:1, lineHeight:1.4 } }, p.label),
169 el('div', { style:{ position:'relative', flexShrink:0 } },
170 el('button', { type:'button', title: p.value||'none', onClick: function(){ setOpen(!open); },
171 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 } }),
172 open && el(Popover, { position:'bottom left', onClose: function(){ setOpen(false); } },
173 el('div', { style:{ padding:'8px' }, onMouseDown: function(e){ e.stopPropagation(); } },
174 el('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:'6px' } },
175 el('strong', { style:{ fontSize:'12px' } }, p.label),
176 el(Button, { icon:'no-alt', isSmall:true, onClick: function(){ setOpen(false); } })
177 ),
178 el(ColorPicker, { color: p.value, enableAlpha:true, onChange: p.onChange })
179 )
180 )
181 )
182 );
183 }
184 registerBlockType( 'blockenberg/treemap-chart', {
185 edit: function ( props ) {
186 const { attributes: a, setAttributes } = props;
187 const blockProps = ( function () {
188 var s = {};
189 Object.assign( s, _tvf( a.titleTypo, '--bktmc-tt-' ) );
190 return useBlockProps( { className: 'bkbg-treemap-wrap', style: Object.keys( s ).length ? s : undefined } );
191 } )();
192
193 function updItem( idx, field, val ) {
194 const next = a.items.map( ( it, i ) => i === idx ? { ...it, [field]: val } : it );
195 setAttributes( { items: next } );
196 }
197
198 return el( 'div', blockProps,
199 el( InspectorControls, {},
200
201 el( PanelBody, { title: __( 'Chart Settings', 'blockenberg' ), initialOpen: true },
202 el( TextControl, { label: __( 'Title', 'blockenberg' ), value: a.title, onChange: v => setAttributes( { title: v } ) } ),
203 el( ToggleControl, { label: __( 'Show Title', 'blockenberg' ), checked: a.showTitle, onChange: v => setAttributes( { showTitle: v } ) } ),
204 el( ToggleControl, { label: __( 'Show Labels', 'blockenberg' ), checked: a.showLabels, onChange: v => setAttributes( { showLabels: v } ) } ),
205 el( ToggleControl, { label: __( 'Show Values', 'blockenberg' ), checked: a.showValues, onChange: v => setAttributes( { showValues: v } ) } ),
206 el( ToggleControl, { label: __( 'Show as Percent', 'blockenberg' ), checked: a.showPercent, onChange: v => setAttributes( { showPercent: v } ) } ),
207 ),
208
209 el( PanelBody, { title: __( 'Canvas & Style', 'blockenberg' ), initialOpen: false },
210 el( RangeControl, { label: __( 'Width', 'blockenberg' ), value: a.svgWidth, onChange: v => setAttributes( { svgWidth: v } ), min: 300, max: 1200, step: 50 } ),
211 el( RangeControl, { label: __( 'Height', 'blockenberg' ), value: a.svgHeight, onChange: v => setAttributes( { svgHeight: v } ), min: 200, max: 800, step: 50 } ),
212 el( RangeControl, { label: __( 'Cell Gap', 'blockenberg' ), value: a.gap, onChange: v => setAttributes( { gap: v } ), min: 0, max: 16 } ),
213 el( RangeControl, { label: __( 'Corner Radius', 'blockenberg' ), value: a.cornerRadius, onChange: v => setAttributes( { cornerRadius: v } ), min: 0, max: 20 } ),
214 ),
215
216
217 el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false },
218 getTypoControl( props, 'titleTypo', __( 'Title', 'blockenberg' ) ),
219 el( RangeControl, { label: __( 'Label Font Size', 'blockenberg' ), value: a.labelFontSize, onChange: v => setAttributes( { labelFontSize: v } ), min: 8, max: 24 } ),
220 el( RangeControl, { label: __( 'Value Font Size', 'blockenberg' ), value: a.valueFontSize, onChange: v => setAttributes( { valueFontSize: v } ), min: 8, max: 20 } )
221 ),
222 el( PanelColorSettings, {
223 title: __( 'Colors', 'blockenberg' ),
224 initialOpen: false,
225 colorSettings: [
226 { label: __( 'Background', 'blockenberg' ), value: a.bgColor, onChange: v => setAttributes( { bgColor: v || '#ffffff' } ) },
227 { label: __( 'Title Color', 'blockenberg' ), value: a.titleColor, onChange: v => setAttributes( { titleColor: v || '#111827' } ) },
228 ]
229 } ),
230
231 el( PanelBody, { title: __( 'Data Items', 'blockenberg' ), initialOpen: false },
232 el( Button, {
233 variant: 'secondary', style: { marginBottom: 10 },
234 onClick: () => setAttributes( { items: [ ...a.items, { label: 'New Item', value: 5, color: '#6b7280' } ] } )
235 }, __( '+ Add Item', 'blockenberg' ) ),
236
237 a.items.map( ( item, i ) =>
238 el( PanelBody, { key: i, title: ( item.label || `Item ${ i + 1 }` ) + ` (${ item.value })`, initialOpen: false },
239 el( TextControl, { label: __( 'Label', 'blockenberg' ), value: item.label, onChange: v => updItem( i, 'label', v ) } ),
240 el( BkbgColorSwatch, { label: __( 'Color', 'blockenberg' ), value: item.color, onChange: v => updItem( i, 'color', v ) } ),
241 el( RangeControl, { label: __( 'Value', 'blockenberg' ), value: item.value, onChange: v => updItem( i, 'value', v ), min: 1, max: 1000 } ),
242 el( Button, { isDestructive: true, isSmall: true, onClick: () => setAttributes( { items: a.items.filter( ( _, x ) => x !== i ) } ) }, __( 'Remove', 'blockenberg' ) ),
243 )
244 ),
245 ),
246 ),
247
248 a.showTitle && a.title && el( 'h3', { className: 'bkbg-tm-title', style: { color: a.titleColor } }, a.title ),
249 el( 'div', { className: 'bkbg-tm-svg' }, renderTreemap( a ) ),
250 renderLegend( a ),
251 );
252 },
253
254 save: function ( { attributes: a } ) {
255 const blockProps = ( function () {
256 var tv = _tvf( a.titleTypo, '--bktmc-tt-' );
257 var parts = []; Object.keys( tv ).forEach( function ( k ) { parts.push( k + ':' + tv[k] ); } );
258 return useBlockProps.save( { className: 'bkbg-treemap-wrap', style: parts.length ? parts.join( ';' ) : undefined } );
259 } )();
260 return el( 'div', blockProps,
261 a.showTitle && a.title ? el( 'h3', { className: 'bkbg-tm-title', style: { color: a.titleColor } }, a.title ) : null,
262 el( 'div', { className: 'bkbg-tm-svg' }, renderTreemap( a ) ),
263 renderLegend( a ),
264 );
265 },
266 } );
267 }() );
268