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 / waffle-chart / index.js

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

198 lines 12.5 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 var _tc, _tvf;
10 Object.defineProperty(window, '_tc', { get: function () { return _tc || (_tc = window.bkbgTypographyControl); } });
11 Object.defineProperty(window, '_tvf', { get: function () { return _tvf || (_tvf = window.bkbgTypoCssVars); } });
12 function getTypoControl(label, key, attrs, setA) { return _tc(label, key, attrs, setA); }
13 function getTypoCssVars(attrs) {
14 var v = {};
15 _tvf(v, 'titleTypo', attrs, '--bkwc-tt-');
16 _tvf(v, 'legendTypo', attrs, '--bkwc-lg-');
17 return v;
18 }
19
20 const COLS = 10, ROWS = 10, TOTAL = COLS * ROWS;
21
22 // ── Build cell color map from segments ────────────────────────────────────
23 // Fills cells from bottom-left to top-right, row by row from bottom.
24 function buildCellMap( segments ) {
25 const total = segments.reduce( ( s, seg ) => s + ( seg.value || 0 ), 0 ) || 100;
26
27 // Allocate exact cells per segment using largest-remainder method
28 const raw = segments.map( seg => ( ( seg.value || 0 ) / total ) * TOTAL );
29 const floor = raw.map( Math.floor );
30 let remaining = TOTAL - floor.reduce( ( s, v ) => s + v, 0 );
31
32 const fracs = raw.map( ( v, i ) => ( { i, frac: v - floor[i] } ) )
33 .sort( ( a, b ) => b.frac - a.frac );
34 for ( let k = 0; k < remaining; k++ ) floor[ fracs[k].i ]++;
35
36 // Build cell array [100] with segment index (or -1 for empty)
37 const cells = new Array( TOTAL ).fill( -1 );
38 let cursor = 0;
39 segments.forEach( ( _, segIdx ) => {
40 for ( let c = 0; c < floor[segIdx]; c++ ) {
41 if ( cursor < TOTAL ) cells[cursor++] = segIdx;
42 }
43 } );
44 return cells;
45 }
46
47 // ── Render waffle grid (SVG) ──────────────────────────────────────────────
48 function renderWaffle( a ) {
49 const { cellSize: CS, cellGap: CG, cellRadius: CR } = a;
50 const segments = a.segments || [];
51 const cells = buildCellMap( segments );
52
53 const total = segments.reduce( ( s, seg ) => s + ( seg.value || 0 ), 0 ) || 100;
54
55 const W = COLS * CS + ( COLS - 1 ) * CG;
56 const H = ROWS * CS + ( ROWS - 1 ) * CG;
57
58 const svgEls = [];
59
60 // Background
61 svgEls.push( el( 'rect', { key: 'bg', x: 0, y: 0, width: W, height: H, fill: a.bgColor || '#ffffff', rx: 8 } ) );
62
63 // Cells — rendered bottom-to-top (row 0 = bottom)
64 cells.forEach( ( segIdx, cellIdx ) => {
65 const col = cellIdx % COLS;
66 const row = ROWS - 1 - Math.floor( cellIdx / COLS ); // flip so row 0 is at bottom
67 const x = col * ( CS + CG );
68 const y = row * ( CS + CG );
69 const fill = segIdx >= 0 ? ( segments[segIdx].color || '#4f46e5' ) : ( a.emptyColor || '#f3f4f6' );
70
71 svgEls.push( el( 'rect', {
72 key: 'c' + cellIdx,
73 x, y, width: CS, height: CS,
74 fill, rx: CR,
75 } ) );
76 } );
77
78 return el( 'svg', {
79 viewBox: `0 0 ${ W } ${ H }`, width: '100%',
80 style: { display: 'block', maxWidth: W + 'px', margin: '0 auto' }
81 }, ...svgEls );
82 }
83
84 // ── Render legend ─────────────────────────────────────────────────────────
85 function renderLegend( a ) {
86 const segments = a.segments || [];
87 const total = segments.reduce( ( s, seg ) => s + ( seg.value || 0 ), 0 ) || 100;
88
89 return el( 'div', { className: 'bkbg-wc-legend' },
90 segments.map( ( seg, i ) => {
91 const pct = ( ( seg.value || 0 ) / total * 100 ).toFixed( 1 );
92 return el( 'div', { key: i, className: 'bkbg-wc-legend-item' },
93 el( 'span', { className: 'bkbg-wc-swatch', style: { background: seg.color || '#4f46e5' } } ),
94 el( 'span', { className: 'bkbg-wc-seg-label', style: { color: a.textColor } }, seg.label || '' ),
95 el( 'span', { className: 'bkbg-wc-seg-val', style: { color: seg.color || '#4f46e5' } },
96 a.showPercent ? pct + '%'
97 : a.showValues ? String( seg.value || 0 )
98 : pct + '%'
99 ),
100 );
101 } )
102 );
103 }
104
105 // ── Helpers ───────────────────────────────────────────────────────────────
106 function updSeg( setAttributes, segments, idx, field, val ) {
107 setAttributes( { segments: segments.map( ( s, i ) => i === idx ? { ...s, [field]: val } : s ) } );
108 }
109
110 // ── Block ───────────────────────────────────────────────────────────────── /* ── colour-swatch + popover ── */
111 function BkbgColorSwatch(p) {
112 var st = useState(false), open = st[0], setOpen = st[1];
113 return el('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'4px 0', gap:'8px' } },
114 el('span', { style:{ fontSize:'12px', color:'#1e1e1e', flex:1, lineHeight:1.4 } }, p.label),
115 el('div', { style:{ position:'relative', flexShrink:0 } },
116 el('button', { type:'button', title: p.value||'none', onClick: function(){ setOpen(!open); },
117 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 } }),
118 open && el(Popover, { position:'bottom left', onClose: function(){ setOpen(false); } },
119 el('div', { style:{ padding:'8px' }, onMouseDown: function(e){ e.stopPropagation(); } },
120 el('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:'6px' } },
121 el('strong', { style:{ fontSize:'12px' } }, p.label),
122 el(Button, { icon:'no-alt', isSmall:true, onClick: function(){ setOpen(false); } })
123 ),
124 el(ColorPicker, { color: p.value, enableAlpha:true, onChange: p.onChange })
125 )
126 )
127 )
128 );
129 }
130 registerBlockType( 'blockenberg/waffle-chart', {
131 edit: function ( props ) {
132 const { attributes: a, setAttributes } = props;
133 const blockProps = useBlockProps( { className: 'bkbg-waffle-wrap', style: getTypoCssVars( a ) } );
134 const segs = a.segments || [];
135
136 return el( 'div', blockProps,
137 el( InspectorControls, {},
138
139 el( PanelBody, { title: __( 'Chart Settings', 'blockenberg' ), initialOpen: true },
140 el( TextControl, { label: __( 'Title', 'blockenberg' ), value: a.title, onChange: v => setAttributes( { title: v } ) } ),
141 el( ToggleControl, { label: __( 'Show Title', 'blockenberg' ), checked: a.showTitle, onChange: v => setAttributes( { showTitle: v } ) } ),
142 el( ToggleControl, { label: __( 'Show Legend', 'blockenberg' ), checked: a.showLegend, onChange: v => setAttributes( { showLegend: v } ) } ),
143 el( ToggleControl, { label: __( 'Show as Percent', 'blockenberg' ), checked: a.showPercent, onChange: v => setAttributes( { showPercent: v, showValues: v ? false : a.showValues } ) } ),
144 el( ToggleControl, { label: __( 'Show Raw Values', 'blockenberg' ), checked: a.showValues, onChange: v => setAttributes( { showValues: v, showPercent: v ? false : a.showPercent } ) } ),
145 ),
146
147 el( PanelBody, { title: __( 'Grid Appearance', 'blockenberg' ), initialOpen: false },
148 el( RangeControl, { label: __( 'Cell Gap (px)', 'blockenberg' ), value: a.cellGap, onChange: v => setAttributes( { cellGap: v } ), min: 0, max: 16 } ),
149 el( RangeControl, { label: __( 'Cell Radius (px)', 'blockenberg' ), value: a.cellRadius, onChange: v => setAttributes( { cellRadius: v } ), min: 0, max: 20 } ),
150 ),
151
152
153 el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false },
154 el( RangeControl, { label: __( 'Cell Size (px)', 'blockenberg' ), value: a.cellSize, onChange: v => setAttributes( { cellSize: v } ), min: 14, max: 60 } ),
155 getTypoControl( __( 'Title', 'blockenberg' ), 'titleTypo', a, setAttributes ),
156 getTypoControl( __( 'Legend', 'blockenberg' ), 'legendTypo', a, setAttributes ),
157 ),
158 el( PanelColorSettings, {
159 title: __( 'Colors', 'blockenberg' ), initialOpen: false,
160 colorSettings: [
161 { label: __( 'Background', 'blockenberg' ), value: a.bgColor, onChange: v => setAttributes( { bgColor: v || '#ffffff' } ) },
162 { label: __( 'Empty Cell', 'blockenberg' ), value: a.emptyColor, onChange: v => setAttributes( { emptyColor: v || '#f3f4f6' } ) },
163 { label: __( 'Title Color', 'blockenberg' ), value: a.titleColor, onChange: v => setAttributes( { titleColor: v || '#111827' } ) },
164 { label: __( 'Text Color', 'blockenberg' ), value: a.textColor, onChange: v => setAttributes( { textColor: v || '#374151' } ) },
165 ]
166 } ),
167
168 el( PanelBody, { title: __( 'Segments', 'blockenberg' ), initialOpen: false },
169 el( 'p', { style: { fontSize: 12, color: '#6b7280', marginBottom: 8 } }, __( 'Values are normalized to 100 cells total automatically.', 'blockenberg' ) ),
170 el( Button, { variant: 'secondary', style: { marginBottom: 10 }, onClick: () => setAttributes( { segments: [ ...segs, { label: 'New Segment', value: 10, color: '#6b7280' } ] } ) }, __( '+ Add Segment', 'blockenberg' ) ),
171 segs.map( ( seg, i ) =>
172 el( PanelBody, { key: i, title: `${ seg.label || `Seg ${ i + 1 }` } (${ seg.value })`, initialOpen: false },
173 el( TextControl, { label: __( 'Label', 'blockenberg' ), value: seg.label, onChange: v => updSeg( setAttributes, segs, i, 'label', v ) } ),
174 el( RangeControl, { label: __( 'Value', 'blockenberg' ), value: seg.value, onChange: v => updSeg( setAttributes, segs, i, 'value', v ), min: 0, max: 10000 } ),
175 el( BkbgColorSwatch, { label: __( 'Color', 'blockenberg' ), value: seg.color, onChange: v => updSeg( setAttributes, segs, i, 'color', v ) } ),
176 el( Button, { isDestructive: true, isSmall: true, onClick: () => setAttributes( { segments: segs.filter( ( _, x ) => x !== i ) } ) }, __( 'Remove', 'blockenberg' ) ),
177 )
178 ),
179 ),
180 ),
181
182 a.showTitle && a.title && el( 'h3', { className: 'bkbg-wc-title', style: { color: a.titleColor } }, a.title ),
183 el( 'div', { className: 'bkbg-wc-grid' }, renderWaffle( a ) ),
184 a.showLegend && renderLegend( a ),
185 );
186 },
187
188 save: function ( { attributes: a } ) {
189 const blockProps = useBlockProps.save( { className: 'bkbg-waffle-wrap', style: getTypoCssVars( a ) } );
190 return el( 'div', blockProps,
191 a.showTitle && a.title ? el( 'h3', { className: 'bkbg-wc-title', style: { color: a.titleColor } }, a.title ) : null,
192 el( 'div', { className: 'bkbg-wc-grid' }, renderWaffle( a ) ),
193 a.showLegend ? renderLegend( a ) : null,
194 );
195 },
196 } );
197 }() );
198