Initial
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Node.js dependencies
|
||||
node_modules/
|
||||
176
blocks/bar-graph/build/block.json
Normal file
176
blocks/bar-graph/build/block.json
Normal file
@ -0,0 +1,176 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 3,
|
||||
"name": "lcp/bar-graph",
|
||||
"version": "1.0.0",
|
||||
"title": "Bar Graph",
|
||||
"category": "widgets",
|
||||
"icon": "chart-bar",
|
||||
"description": "Display data as a bar graph using D3.js",
|
||||
"supports": {
|
||||
"html": false
|
||||
},
|
||||
"attributes": {
|
||||
"columnTypes": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"ID": "lcpText",
|
||||
"Label": "lcpText",
|
||||
"Value": "lcpNumber",
|
||||
"Color": "lcpColor",
|
||||
"Content": "lcpText",
|
||||
"Parent": "lcpText"
|
||||
}
|
||||
},
|
||||
"enableStacked": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"enableHierachical": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"chartData": {
|
||||
"type": "array",
|
||||
"default": [
|
||||
{
|
||||
"ID": "lcpDatapoint-1",
|
||||
"Label": "Sample 1",
|
||||
"Value": 100,
|
||||
"Color": "#007cba",
|
||||
"Content": "<p>First item</p>",
|
||||
"Parent": ""
|
||||
},
|
||||
{
|
||||
"ID": "lcpDatapoint-2",
|
||||
"Label": "Sample 2",
|
||||
"Value": 50,
|
||||
"Color": "#ff0000",
|
||||
"Content": "<p>Second item</p>",
|
||||
"Parent": "lcpDatapoint-1"
|
||||
},
|
||||
{
|
||||
"ID": "lcpDatapoint-3",
|
||||
"Label": "Sample 3",
|
||||
"Value": 30,
|
||||
"Color": "#00ff00",
|
||||
"Content": "<p>Third item</p>",
|
||||
"Parent": ""
|
||||
},
|
||||
{
|
||||
"ID": "lcpDatapoint-4",
|
||||
"Label": "Sample 4",
|
||||
"Value": 20,
|
||||
"Color": "#0000ff",
|
||||
"Content": "<p>Fourth item</p>",
|
||||
"Parent": ""
|
||||
},
|
||||
{
|
||||
"ID": "lcpDatapoint-5",
|
||||
"Label": "Sample 5",
|
||||
"Value": 80,
|
||||
"Color": "#800080",
|
||||
"Content": "<p>Fifth item</p>",
|
||||
"Parent": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"chartHeight": {
|
||||
"type": "string",
|
||||
"default": "400px"
|
||||
},
|
||||
"chartWidth": {
|
||||
"type": "string",
|
||||
"default": "100%"
|
||||
},
|
||||
"idColumn": {
|
||||
"type": "string",
|
||||
"default": "ID"
|
||||
},
|
||||
"barColor": {
|
||||
"type": "string",
|
||||
"default": "#007cba"
|
||||
},
|
||||
"valueColumn": {
|
||||
"type": "string",
|
||||
"default": "Value"
|
||||
},
|
||||
"labelsColumn": {
|
||||
"type": "string",
|
||||
"default": "Label"
|
||||
},
|
||||
"colorColumn": {
|
||||
"type": "string",
|
||||
"default": "Color"
|
||||
},
|
||||
"popoverColumn": {
|
||||
"type": "string",
|
||||
"default": "Content"
|
||||
},
|
||||
"renderLegend": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"legendLocation": {
|
||||
"type": "string",
|
||||
"default": "top"
|
||||
},
|
||||
"legendAlignment": {
|
||||
"type": "string",
|
||||
"default": "left"
|
||||
},
|
||||
"allowDownloadImage": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"downloadImageMaxWidth": {
|
||||
"type": "string",
|
||||
"default": "2000px"
|
||||
},
|
||||
"allowDownloadCsv": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"allowDownloadJson": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"allowFilter": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"allowSorting": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"renderFooter": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"footerContent": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"chartTitle": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"chartSubtitle": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"toolbarLocation": {
|
||||
"type": "string",
|
||||
"default": "bottom"
|
||||
},
|
||||
"toolbarAlignment": {
|
||||
"type": "string",
|
||||
"default": "left"
|
||||
}
|
||||
},
|
||||
"textdomain": "lcp-visualize",
|
||||
"editorScript": "file:./index.js",
|
||||
"editorStyle": "file:./index.css",
|
||||
"style": "file:./style-index.css",
|
||||
"render": "render_block_lcp_bar_graph"
|
||||
}
|
||||
1
blocks/bar-graph/build/index.asset.php
Normal file
1
blocks/bar-graph/build/index.asset.php
Normal file
@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'react-dom', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => '32e6baadcfdb2c5e9a4b');
|
||||
5
blocks/bar-graph/build/index.css
Normal file
5
blocks/bar-graph/build/index.css
Normal file
File diff suppressed because one or more lines are too long
1
blocks/bar-graph/build/index.js
Normal file
1
blocks/bar-graph/build/index.js
Normal file
File diff suppressed because one or more lines are too long
1
blocks/bar-graph/build/style-index.css
Normal file
1
blocks/bar-graph/build/style-index.css
Normal file
@ -0,0 +1 @@
|
||||
.wp-block-lcp-bar-graph .lcp-bar-graph-container svg{overflow:visible}.wp-block-lcp-bar-graph .lcp-bar-graph-container text{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}
|
||||
22996
blocks/bar-graph/package-lock.json
generated
Normal file
22996
blocks/bar-graph/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
blocks/bar-graph/package.json
Normal file
22
blocks/bar-graph/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "lcp-bar-graph",
|
||||
"version": "1.0.0",
|
||||
"description": "Bar graph block using D3.js",
|
||||
"scripts": {
|
||||
"build": "wp-scripts build",
|
||||
"start": "wp-scripts start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wordpress/block-editor": "^12.0.0",
|
||||
"@wordpress/blocks": "^12.0.0",
|
||||
"@wordpress/components": "^25.0.0",
|
||||
"@wordpress/element": "^5.0.0",
|
||||
"@wordpress/i18n": "^4.0.0",
|
||||
"ag-grid-community": "^31.0.1",
|
||||
"ag-grid-react": "^31.0.1",
|
||||
"d3": "^7.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wordpress/scripts": "^26.0.0"
|
||||
}
|
||||
}
|
||||
141
blocks/bar-graph/src/block.json
Normal file
141
blocks/bar-graph/src/block.json
Normal file
@ -0,0 +1,141 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 3,
|
||||
"name": "lcp/bar-graph",
|
||||
"version": "1.0.0",
|
||||
"title": "Bar Graph",
|
||||
"category": "widgets",
|
||||
"icon": "chart-bar",
|
||||
"description": "Display data as a bar graph using D3.js",
|
||||
"supports": {
|
||||
"html": false
|
||||
},
|
||||
"attributes": {
|
||||
"columnTypes": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"ID": "lcpText",
|
||||
"Label": "lcpText",
|
||||
"Value": "lcpNumber",
|
||||
"Color": "lcpColor",
|
||||
"Content": "lcpText",
|
||||
"Parent": "lcpText"
|
||||
}
|
||||
},
|
||||
"enableStacked": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"enableHierachical": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"chartData": {
|
||||
"type": "array",
|
||||
"default": [
|
||||
{ "ID": "lcpDatapoint-1", "Label": "Sample 1", "Value": 100, "Color": "#007cba", "Content": "<p>First item</p>", "Parent": "" },
|
||||
{ "ID": "lcpDatapoint-2", "Label": "Sample 2", "Value": 50, "Color": "#ff0000", "Content": "<p>Second item</p>", "Parent": "lcpDatapoint-1" },
|
||||
{ "ID": "lcpDatapoint-3", "Label": "Sample 3", "Value": 30, "Color": "#00ff00", "Content": "<p>Third item</p>" , "Parent": ""},
|
||||
{ "ID": "lcpDatapoint-4", "Label": "Sample 4", "Value": 20, "Color": "#0000ff", "Content": "<p>Fourth item</p>" , "Parent": ""},
|
||||
{ "ID": "lcpDatapoint-5", "Label": "Sample 5", "Value": 80, "Color": "#800080", "Content": "<p>Fifth item</p>" , "Parent": ""}
|
||||
]
|
||||
},
|
||||
"chartHeight": {
|
||||
"type": "string",
|
||||
"default": "400px"
|
||||
},
|
||||
"chartWidth": {
|
||||
"type": "string",
|
||||
"default": "100%"
|
||||
},
|
||||
"idColumn": {
|
||||
"type": "string",
|
||||
"default": "ID"
|
||||
},
|
||||
"barColor": {
|
||||
"type": "string",
|
||||
"default": "#007cba"
|
||||
},
|
||||
"valueColumn": {
|
||||
"type": "string",
|
||||
"default": "Value"
|
||||
},
|
||||
"labelsColumn": {
|
||||
"type": "string",
|
||||
"default": "Label"
|
||||
},
|
||||
"colorColumn": {
|
||||
"type": "string",
|
||||
"default": "Color"
|
||||
},
|
||||
"popoverColumn": {
|
||||
"type": "string",
|
||||
"default": "Content"
|
||||
},
|
||||
"renderLegend": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"legendLocation": {
|
||||
"type": "string",
|
||||
"default": "top"
|
||||
},
|
||||
"legendAlignment": {
|
||||
"type": "string",
|
||||
"default": "left"
|
||||
},
|
||||
"allowDownloadImage": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"downloadImageMaxWidth": {
|
||||
"type": "string",
|
||||
"default": "2000px"
|
||||
},
|
||||
"allowDownloadCsv": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"allowDownloadJson": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"allowFilter": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"allowSorting": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"renderFooter": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"footerContent": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"chartTitle": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"chartSubtitle": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"toolbarLocation": {
|
||||
"type": "string",
|
||||
"default": "bottom"
|
||||
},
|
||||
"toolbarAlignment": {
|
||||
"type": "string",
|
||||
"default": "left"
|
||||
}
|
||||
},
|
||||
"textdomain": "lcp-visualize",
|
||||
"editorScript": "file:./index.js",
|
||||
"editorStyle": "file:./index.css",
|
||||
"style": "file:./style-index.css",
|
||||
"render": "render_block_lcp_bar_graph"
|
||||
}
|
||||
178
blocks/bar-graph/src/components/BarGraph.js
Normal file
178
blocks/bar-graph/src/components/BarGraph.js
Normal file
@ -0,0 +1,178 @@
|
||||
import { useEffect, useRef, useState } from '@wordpress/element';
|
||||
import { Popover } from '@wordpress/components';
|
||||
import * as d3 from 'd3';
|
||||
|
||||
const BarGraph = ({
|
||||
chartData = [],
|
||||
height = '400px',
|
||||
width = '100%',
|
||||
valueColumn = 'Value',
|
||||
labelsColumn = 'Label',
|
||||
colorColumn = 'Color',
|
||||
popoverColumn = 'Content',
|
||||
defaultBarColor = '#4CAF50'
|
||||
}) => {
|
||||
const d3Container = useRef(null);
|
||||
const [activePopover, setActivePopover] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!Array.isArray(chartData) || chartData.length === 0 || !d3Container.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the first row to determine fields
|
||||
const firstRow = chartData[0];
|
||||
const fields = Object.keys(firstRow);
|
||||
if (fields.length < 2) return;
|
||||
|
||||
// Clear any existing SVG
|
||||
d3.select(d3Container.current).selectAll('*').remove();
|
||||
|
||||
// Set the dimensions and margins of the graph
|
||||
const margin = { top: 30, right: 30, bottom: 70, left: 60 };
|
||||
const containerWidth = d3Container.current.clientWidth;
|
||||
const containerHeight = parseInt(height) || 400;
|
||||
const graphWidth = containerWidth - margin.left - margin.right;
|
||||
const graphHeight = containerHeight - margin.top - margin.bottom;
|
||||
|
||||
// Create the SVG container
|
||||
const svg = d3.select(d3Container.current)
|
||||
.append('svg')
|
||||
.attr('width', containerWidth)
|
||||
.attr('height', containerHeight)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
// X axis
|
||||
const x = d3.scaleBand()
|
||||
.range([0, graphWidth])
|
||||
.domain(chartData.map(d => String(d[labelsColumn])))
|
||||
.padding(0.2);
|
||||
|
||||
svg.append('g')
|
||||
.attr('transform', `translate(0,${graphHeight})`)
|
||||
.call(d3.axisBottom(x))
|
||||
.selectAll('text')
|
||||
.attr('transform', 'translate(-10,0)rotate(-45)')
|
||||
.style('text-anchor', 'end');
|
||||
|
||||
// Add X axis label
|
||||
svg.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('x', graphWidth / 2)
|
||||
.attr('y', graphHeight + margin.bottom - 10)
|
||||
.text(labelsColumn);
|
||||
|
||||
// Y axis
|
||||
const y = d3.scaleLinear()
|
||||
.domain([0, d3.max(chartData, d => {
|
||||
const val = Number(d[valueColumn]);
|
||||
return isNaN(val) ? 0 : val;
|
||||
})])
|
||||
.range([graphHeight, 0]);
|
||||
|
||||
svg.append('g')
|
||||
.call(d3.axisLeft(y));
|
||||
|
||||
// Add Y axis label
|
||||
svg.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('transform', 'rotate(-90)')
|
||||
.attr('y', -margin.left + 20)
|
||||
.attr('x', -graphHeight / 2)
|
||||
.text(valueColumn);
|
||||
|
||||
// Add the bars
|
||||
svg.selectAll('rect')
|
||||
.data(chartData)
|
||||
.join('rect')
|
||||
.attr('x', d => x(String(d[labelsColumn])))
|
||||
.attr('y', d => {
|
||||
const val = Number(d[valueColumn]);
|
||||
return y(isNaN(val) ? 0 : val);
|
||||
})
|
||||
.attr('width', x.bandwidth())
|
||||
.attr('height', d => {
|
||||
const val = Number(d[valueColumn]);
|
||||
return graphHeight - y(isNaN(val) ? 0 : val);
|
||||
})
|
||||
.attr('fill', d => colorColumn && d[colorColumn] ? d[colorColumn] : defaultBarColor)
|
||||
.on('mouseover', function(event, d) {
|
||||
// Highlight the bar
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('opacity', 0.8);
|
||||
|
||||
// Show value label
|
||||
svg.append('text')
|
||||
.attr('class', 'value-label')
|
||||
.attr('x', x(String(d[labelsColumn])) + x.bandwidth() / 2)
|
||||
.attr('y', y(Number(d[valueColumn])) - 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.text(d[valueColumn]);
|
||||
|
||||
// Show popover if content exists
|
||||
if (popoverColumn && d[popoverColumn]) {
|
||||
const rect = this.getBoundingClientRect();
|
||||
setActivePopover({
|
||||
content: d[popoverColumn],
|
||||
position: {
|
||||
top: rect.top + window.scrollY,
|
||||
left: rect.left + rect.width / 2 + window.scrollX,
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('mouseout', function() {
|
||||
// Remove highlight
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('opacity', 1);
|
||||
|
||||
// Remove value label
|
||||
svg.selectAll('.value-label').remove();
|
||||
|
||||
// Hide popover
|
||||
setActivePopover(null);
|
||||
});
|
||||
|
||||
}, [chartData, height, width, valueColumn, labelsColumn, colorColumn, popoverColumn, defaultBarColor]);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div
|
||||
className="d3-container"
|
||||
ref={d3Container}
|
||||
style={{
|
||||
width: width,
|
||||
height: height,
|
||||
backgroundColor: '#ffffff'
|
||||
}}
|
||||
/>
|
||||
{activePopover && (
|
||||
<Popover
|
||||
position="top"
|
||||
focusOnMount={false}
|
||||
onClose={() => setActivePopover(null)}
|
||||
anchorRect={{
|
||||
top: activePopover.position.top,
|
||||
left: activePopover.position.left,
|
||||
bottom: activePopover.position.top,
|
||||
right: activePopover.position.left,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ padding: '12px' }}
|
||||
dangerouslySetInnerHTML={{ __html: activePopover.content }}
|
||||
/>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BarGraph;
|
||||
43
blocks/bar-graph/src/dev-setup.js
Normal file
43
blocks/bar-graph/src/dev-setup.js
Normal file
@ -0,0 +1,43 @@
|
||||
// Enable React development mode for better error messages
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.debug('React running in development mode');
|
||||
}
|
||||
|
||||
// Add error boundary for better error handling
|
||||
import { Component } from '@wordpress/element';
|
||||
|
||||
export class ErrorBoundary extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null, errorInfo: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
console.error('React Error Boundary caught an error:', error, errorInfo);
|
||||
this.setState({
|
||||
error: error,
|
||||
errorInfo: errorInfo
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div>
|
||||
<h2>Something went wrong.</h2>
|
||||
<details style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{this.state.error && this.state.error.toString()}
|
||||
<br />
|
||||
{this.state.errorInfo.componentStack}
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
78
blocks/bar-graph/src/edit.js
Normal file
78
blocks/bar-graph/src/edit.js
Normal file
@ -0,0 +1,78 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
|
||||
import { PanelBody, ColorPicker } from '@wordpress/components';
|
||||
import './editor.scss';
|
||||
import BarGraph from './components/BarGraph';
|
||||
import LCPDatasetBuilder from '../../components/LCPDatasetBuilder';
|
||||
import LCPDimensionControl from '../../components/LCPDimensionControl';
|
||||
import LCPChartBlockSettings from '../../components/LCPChartBlockSettings';
|
||||
|
||||
export default function Edit({ attributes, setAttributes }) {
|
||||
const {
|
||||
chartHeight = '400px',
|
||||
chartWidth = '100%',
|
||||
barColor = '#007cba',
|
||||
chartData = [],
|
||||
valueColumn = 'Value',
|
||||
labelsColumn = 'Label',
|
||||
colorColumn = 'color',
|
||||
popoverColumn = 'content',
|
||||
|
||||
} = attributes;
|
||||
|
||||
const blockProps = useBlockProps();
|
||||
|
||||
const handleDatasetChange = (newData) => {
|
||||
setAttributes({ chartData: newData });
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div {...blockProps}>
|
||||
<InspectorControls>
|
||||
{/* Bar Graph-specific Settings */}
|
||||
<PanelBody title={__('Chart Settings', 'lcp-visualize')}>
|
||||
<LCPDatasetBuilder
|
||||
chartType="bar"
|
||||
chartData={chartData}
|
||||
onChange={handleDatasetChange}
|
||||
attributes={attributes}
|
||||
setAttributes={setAttributes}
|
||||
/>
|
||||
<LCPDimensionControl
|
||||
label={__('Chart Height', 'lcp-visualize')}
|
||||
value={chartHeight}
|
||||
onChange={(value) => setAttributes({ chartHeight: value })}
|
||||
/>
|
||||
<LCPDimensionControl
|
||||
label={__('Chart Width', 'lcp-visualize')}
|
||||
value={chartWidth}
|
||||
onChange={(value) => setAttributes({ chartWidth: value })}
|
||||
/>
|
||||
|
||||
</PanelBody>
|
||||
{/* Common Chart Settings */}
|
||||
<LCPChartBlockSettings
|
||||
attributes={attributes}
|
||||
setAttributes={setAttributes}
|
||||
/>
|
||||
|
||||
</InspectorControls>
|
||||
|
||||
<div className="lcp-bar-graph-container">
|
||||
<BarGraph
|
||||
chartData={chartData}
|
||||
height={chartHeight}
|
||||
width={chartWidth}
|
||||
valueColumn={valueColumn}
|
||||
labelsColumn={labelsColumn}
|
||||
colorColumn={colorColumn}
|
||||
popoverColumn={popoverColumn}
|
||||
defaultBarColor={barColor}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
124
blocks/bar-graph/src/editor.scss
Normal file
124
blocks/bar-graph/src/editor.scss
Normal file
@ -0,0 +1,124 @@
|
||||
.lcp-bar-graph-container {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.lcp-dataset-builder {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.lcp-dataset-modal {
|
||||
width: calc(100vw - 40px) !important;
|
||||
height: calc(100vh - 40px) !important;
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
|
||||
.components-modal__content {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.components-modal__header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.lcp-dataset-modal-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.lcp-dataset-modal-grid {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lcp-dataset-modal-sidebar {
|
||||
width: 300px;
|
||||
border-left: 1px solid #ddd;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.lcp-dataset-modal-sidebar-content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.lcp-dataset-column-settings {
|
||||
margin-top: 20px;
|
||||
|
||||
.components-base-control {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.components-base-control__label {
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lcp-dataset-modal-footer {
|
||||
padding: 16px;
|
||||
border-top: 1px solid #ddd;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.lcp-data-grid {
|
||||
height: 100%;
|
||||
|
||||
.ag-theme-alpine {
|
||||
height: 100%;
|
||||
--ag-header-height: 30px;
|
||||
--ag-header-foreground-color: #1e1e1e;
|
||||
--ag-header-background-color: #f0f0f0;
|
||||
--ag-row-hover-color: #f5f5f5;
|
||||
--ag-selected-row-background-color: rgba(0, 124, 186, 0.1);
|
||||
|
||||
.ag-header-cell {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lcp-grid-context-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
min-width: 160px;
|
||||
|
||||
.components-button {
|
||||
justify-content: flex-start;
|
||||
height: 36px;
|
||||
padding: 6px 12px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
&.is-destructive {
|
||||
color: #cc1818;
|
||||
|
||||
&:hover {
|
||||
color: #710d0d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
blocks/bar-graph/src/index.js
Normal file
12
blocks/bar-graph/src/index.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import 'ag-grid-community/styles/ag-grid.css';
|
||||
import 'ag-grid-community/styles/ag-theme-alpine.css';
|
||||
import './style.scss';
|
||||
import Edit from './edit';
|
||||
import metadata from './block.json';
|
||||
|
||||
registerBlockType(metadata.name, {
|
||||
...metadata,
|
||||
edit: Edit,
|
||||
save: () => null, // Dynamic block, rendered in PHP
|
||||
});
|
||||
11
blocks/bar-graph/src/style.scss
Normal file
11
blocks/bar-graph/src/style.scss
Normal file
@ -0,0 +1,11 @@
|
||||
.wp-block-lcp-bar-graph {
|
||||
.lcp-bar-graph-container {
|
||||
svg {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
text {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
}
|
||||
}
|
||||
191
blocks/components/LCPChartBlockSettings.js
Normal file
191
blocks/components/LCPChartBlockSettings.js
Normal file
@ -0,0 +1,191 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Icon, PanelBody, ToggleControl } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import LCPDimensionControl from './LCPDimensionControl';
|
||||
import LCPHTMLModal from './LCPHTMLModal.js';
|
||||
|
||||
const LCPChartBlockSettings = ({ attributes, setAttributes }) => {
|
||||
// Use `legendAlignment` from props (block attributes)
|
||||
const { renderLegend,
|
||||
legendLocation,
|
||||
legendAlignment,
|
||||
allowDownloadImage,
|
||||
downloadImageMaxWidth,
|
||||
allowDownloadCsv,
|
||||
allowDownloadJson,
|
||||
allowSorting,
|
||||
allowFiltering,
|
||||
footerContent,
|
||||
chartTitle,
|
||||
chartSubtitle,
|
||||
toolbarLocation,
|
||||
toolbarAlignment
|
||||
} = attributes;
|
||||
|
||||
// Set local state for legend location (for UI updates)
|
||||
const [selectedLegendLocation, setSelectedLegendLocation] = useState(legendLocation);
|
||||
|
||||
// Update block attribute when location changes
|
||||
useEffect(() => {
|
||||
setAttributes({ legendLocation: selectedLegendLocation });
|
||||
}, [selectedLegendLocation, setAttributes]);
|
||||
|
||||
// Set local state for alignment (for UI updates)
|
||||
const [selectedLegendAlignment, setSelectedLegendAlignment] = useState(legendAlignment);
|
||||
|
||||
// Update block attribute when alignment changes
|
||||
useEffect(() => {
|
||||
setAttributes({ legendAlignment: selectedLegendAlignment });
|
||||
}, [selectedLegendAlignment, setAttributes]);
|
||||
|
||||
// Set local state for alignment (for UI updates)
|
||||
const [selectedToolbarAlignment, setSelectedToolbarAlignment] = useState(toolbarAlignment);
|
||||
|
||||
// Update block attribute when alignment changes
|
||||
useEffect(() => {
|
||||
setAttributes({ toolbarAlignment: selectedToolbarAlignment });
|
||||
}, [selectedToolbarAlignment, setAttributes]);
|
||||
|
||||
|
||||
// Render the component
|
||||
return (
|
||||
<div>
|
||||
{/* Legend Settings Panel */}
|
||||
<PanelBody title={__('Legend', 'lcp')} initialOpen={false}>
|
||||
<ToggleControl
|
||||
label={__('Render Legend', 'lcp')}
|
||||
checked={renderLegend}
|
||||
onChange={(value) => setAttributes({ renderLegend: value })}
|
||||
/>
|
||||
{renderLegend && (
|
||||
<div>
|
||||
{/* Legend - Alignment Buttons */}
|
||||
<div>
|
||||
{/* Left Alignment Button */}
|
||||
<Button
|
||||
isPrimary={selectedLegendAlignment === 'left'}
|
||||
onClick={() => setSelectedLegendAlignment('left')}
|
||||
>
|
||||
{__('Left', 'lcp')}
|
||||
</Button>
|
||||
|
||||
{/* Center Alignment Button */}
|
||||
<Button
|
||||
isPrimary={selectedLegendAlignment === 'center'}
|
||||
onClick={() => setSelectedLegendAlignment('center')}
|
||||
>
|
||||
{__('Center', 'lcp')}
|
||||
</Button>
|
||||
|
||||
{/* Right Alignment Button */}
|
||||
<Button
|
||||
isPrimary={selectedLegendAlignment === 'right'}
|
||||
onClick={() => setSelectedLegendAlignment('right')}
|
||||
>
|
||||
{__('Right', 'lcp')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Legend - Location Buttons */}
|
||||
<div>
|
||||
{/* Top Location Button */}
|
||||
<Button
|
||||
isPrimary={selectedLegendLocation === 'top'}
|
||||
onClick={() => setSelectedLegendLocation('top')}
|
||||
>
|
||||
{__('Top', 'lcp')}
|
||||
</Button>
|
||||
|
||||
{/* Bottom Location Button */}
|
||||
<Button
|
||||
isPrimary={selectedLegendLocation === 'bottom'}
|
||||
onClick={() => setSelectedLegendLocation('bottom')}
|
||||
>
|
||||
{__('Bottom', 'lcp')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</PanelBody>
|
||||
|
||||
{/* Controls Settings Panel */}
|
||||
<PanelBody title={__('Controls', 'lcp')} initialOpen={false}>
|
||||
<div>
|
||||
{/* Left Alignment Button */}
|
||||
<Button
|
||||
isPrimary={selectedToolbarAlignment === 'left'}
|
||||
onClick={() => setSelectedToolbarAlignment('left')}
|
||||
>
|
||||
|
||||
{__('Left', 'lcp')}
|
||||
</Button>
|
||||
|
||||
{/* Center Alignment Button */}
|
||||
<Button
|
||||
isPrimary={selectedToolbarAlignment === 'center'}
|
||||
onClick={() => setSelectedToolbarAlignment('center')}
|
||||
>
|
||||
{__('Center', 'lcp')}
|
||||
</Button>
|
||||
|
||||
{/* Right Alignment Button */}
|
||||
<Button
|
||||
isPrimary={selectedToolbarAlignment === 'right'}
|
||||
onClick={() => setSelectedToolbarAlignment('right')}
|
||||
>
|
||||
{__('Right', 'lcp')}
|
||||
</Button>
|
||||
</div>
|
||||
<ToggleControl
|
||||
label={__('Allow Filtering', 'lcp')}
|
||||
checked={allowFiltering}
|
||||
onChange={(value) => setAttributes({ allowFiltering: value })}
|
||||
/>
|
||||
<ToggleControl
|
||||
label={__('Allow Sorting', 'lcp')}
|
||||
checked={allowSorting}
|
||||
onChange={(value) => setAttributes({ allowSorting: value })}
|
||||
/>
|
||||
<ToggleControl
|
||||
label={__('Allow Download Image', 'lcp')}
|
||||
checked={allowDownloadImage}
|
||||
onChange={(value) => setAttributes({ allowDownloadImage: value })}
|
||||
/>
|
||||
{allowDownloadImage && (
|
||||
<LCPDimensionControl
|
||||
unitTypes={['px']}
|
||||
label={__('Download Image Max Width', 'lcp')}
|
||||
value={downloadImageMaxWidth}
|
||||
onChange={(value) => setAttributes({ downloadImageMaxWidth: value })}
|
||||
/>
|
||||
)}
|
||||
<ToggleControl
|
||||
label={__('Allow Download CSV', 'lcp')}
|
||||
checked={allowDownloadCsv}
|
||||
onChange={(value) => setAttributes({ allowDownloadCsv: value })}
|
||||
/>
|
||||
<ToggleControl
|
||||
label={__('Allow Download JSON', 'lcp')}
|
||||
checked={allowDownloadJson}
|
||||
onChange={(value) => setAttributes({ allowDownloadJson: value })}
|
||||
/>
|
||||
</PanelBody>
|
||||
{/* Tooltips and Popups Panel */}
|
||||
<PanelBody title={__('Tooltips and Popups', 'lcp')} initialOpen={false}>
|
||||
<LCPHTMLModal />
|
||||
</PanelBody>
|
||||
{/* Header Settings Panel */}
|
||||
<PanelBody title={__('Header', 'lcp')} initialOpen={false}>
|
||||
|
||||
</PanelBody>
|
||||
{/* Footer Settings Panel */}
|
||||
<PanelBody title={__('Footer', 'lcp')} initialOpen={false}>
|
||||
<LCPHTMLModal />
|
||||
</PanelBody>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LCPChartBlockSettings;
|
||||
500
blocks/components/LCPDataGrid.js
Normal file
500
blocks/components/LCPDataGrid.js
Normal file
@ -0,0 +1,500 @@
|
||||
import { useMemo, useCallback, useRef, useState } from '@wordpress/element';
|
||||
import { Popover, Button, Modal, SelectControl } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { AgGridReact } from 'ag-grid-react';
|
||||
import 'ag-grid-community/styles/ag-grid.css';
|
||||
import 'ag-grid-community/styles/ag-theme-alpine.css';
|
||||
|
||||
const generateId = () => {
|
||||
const randomStr = Math.random().toString(36).substring(2, 8);
|
||||
return `lcpDatapoint-${randomStr}`;
|
||||
};
|
||||
|
||||
// Helper function to determine text color based on background
|
||||
const getContrastColor = (hexcolor) => {
|
||||
if (!hexcolor) return 'inherit';
|
||||
const r = parseInt(hexcolor.slice(1, 3), 16);
|
||||
const g = parseInt(hexcolor.slice(3, 5), 16);
|
||||
const b = parseInt(hexcolor.slice(5, 7), 16);
|
||||
const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
|
||||
return (yiq >= 128) ? 'black' : 'white';
|
||||
};
|
||||
|
||||
const ColorCellRenderer = (props) => {
|
||||
if (!props.value) return '';
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: props.value,
|
||||
color: getContrastColor(props.value),
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '4px',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
{props.value}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const ParentCellRenderer = (props) => {
|
||||
if (!props.data || !props.data.ID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create options from all available rows except current
|
||||
const options = [
|
||||
{ label: __('None', 'lcp-visualize'), value: '' }
|
||||
];
|
||||
|
||||
// Get all rows from the grid
|
||||
props.api.forEachNode(node => {
|
||||
if (node.data &&
|
||||
node.data.ID &&
|
||||
node.data.ID !== props.data.ID) {
|
||||
options.push({
|
||||
label: node.data.Label || node.data.ID,
|
||||
value: node.data.ID
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const handleChange = (newValue) => {
|
||||
if (props.context && props.context.handleParentChange) {
|
||||
props.context.handleParentChange(props.data, newValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectControl
|
||||
value={props.data.Parent || ''}
|
||||
options={options}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const ColumnHeaderComponent = (props) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [headerName, setHeaderName] = useState(props.displayName);
|
||||
|
||||
const dataTypeOptions = [
|
||||
{ label: __('Text', 'lcp'), value: 'lcpText' },
|
||||
{ label: __('Number', 'lcp'), value: 'lcpNumber' },
|
||||
{ label: __('Color', 'lcp'), value: 'lcpColor' },
|
||||
{ label: __('Date/Time', 'lcp'), value: 'lcpDate' }
|
||||
];
|
||||
|
||||
const handleTypeChange = (newType) => {
|
||||
props.setColumnType(props.column.colId, newType);
|
||||
};
|
||||
|
||||
const handleHeaderChange = (newHeader) => {
|
||||
if (props.onHeaderChange && newHeader !== props.displayName) {
|
||||
props.onHeaderChange(props.column.colId, newHeader);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
handleHeaderChange(headerName);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setHeaderName(props.displayName);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', height: '100%' }}>
|
||||
<span>{props.displayName}</span>
|
||||
<Button
|
||||
isSmall
|
||||
variant="tertiary"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
icon={
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ fill: 'currentColor' }}
|
||||
>
|
||||
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
{isModalOpen && (
|
||||
<Modal
|
||||
title={__('Column Settings', 'lcp')}
|
||||
onRequestClose={handleCancel}
|
||||
style={{ width: '400px' }}
|
||||
>
|
||||
<div style={{ padding: '20px' }}>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '8px' }}>
|
||||
{__('Column Header', 'lcp')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={headerName}
|
||||
onChange={(e) => setHeaderName(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #757575'
|
||||
}}
|
||||
disabled={props.column.colId === 'ID'}
|
||||
/>
|
||||
</div>
|
||||
<SelectControl
|
||||
label={__('Data Type', 'lcp')}
|
||||
value={props.currentType || 'lcpText'}
|
||||
options={dataTypeOptions}
|
||||
onChange={handleTypeChange}
|
||||
disabled={props.column.colId === 'ID'}
|
||||
/>
|
||||
<div style={{ marginTop: '20px', display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{__('Cancel', 'lcp')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
>
|
||||
{__('Save', 'lcp-visualize')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LCPDataGrid = ({
|
||||
chartData = [],
|
||||
onDataChange,
|
||||
columnTypes,
|
||||
onColumnTypeChange,
|
||||
attributes,
|
||||
setAttributes
|
||||
}) => {
|
||||
const gridRef = useRef();
|
||||
const [contextMenu, setContextMenu] = useState(null);
|
||||
|
||||
// Helper function to guess column type based on value
|
||||
const guessColumnType = useCallback((value) => {
|
||||
if (typeof value === 'number') return 'lcpNumber';
|
||||
if (typeof value === 'string') {
|
||||
if (/^#([0-9A-F]{3}){1,2}$/i.test(value)) return 'lcpColor';
|
||||
if (!isNaN(Date.parse(value))) return 'lcpDate';
|
||||
}
|
||||
return 'lcpText';
|
||||
}, []);
|
||||
|
||||
// Handle parent change
|
||||
const handleParentChange = useCallback((data, newParent) => {
|
||||
const newData = chartData.map(item =>
|
||||
item.ID === data.ID ? { ...item, Parent: newParent } : item
|
||||
);
|
||||
onDataChange(newData);
|
||||
}, [chartData, onDataChange]);
|
||||
|
||||
// Use columnTypes from attributes if available, otherwise use direct columnTypes prop
|
||||
const effectiveColumnTypes = attributes?.columnTypes || columnTypes || {};
|
||||
|
||||
// Convert any old column types to new format
|
||||
const migratedColumnTypes = useMemo(() => {
|
||||
const typeMap = {
|
||||
'textColumn': 'lcpText',
|
||||
'numericColumn': 'lcpNumber',
|
||||
'colorColumn': 'lcpColor',
|
||||
'dateColumn': 'lcpDate'
|
||||
};
|
||||
|
||||
return Object.entries(effectiveColumnTypes).reduce((acc, [key, value]) => {
|
||||
acc[key] = typeMap[value] || value;
|
||||
return acc;
|
||||
}, {});
|
||||
}, [effectiveColumnTypes]);
|
||||
|
||||
const handleColumnTypeChange = useCallback((colId, newType) => {
|
||||
if (setAttributes) {
|
||||
setAttributes({
|
||||
columnTypes: {
|
||||
...migratedColumnTypes,
|
||||
[colId]: newType
|
||||
}
|
||||
});
|
||||
} else if (onColumnTypeChange) {
|
||||
onColumnTypeChange(colId, newType);
|
||||
}
|
||||
}, [migratedColumnTypes, setAttributes, onColumnTypeChange]);
|
||||
|
||||
// Define column type configurations
|
||||
const gridColumnTypes = useMemo(() => ({
|
||||
lcpNumber: {
|
||||
filter: 'agNumberColumnFilter',
|
||||
filterParams: {
|
||||
buttons: ['apply', 'reset'],
|
||||
closeOnApply: true
|
||||
},
|
||||
valueParser: params => {
|
||||
if (!params.newValue) return '';
|
||||
const parsed = parseFloat(params.newValue);
|
||||
return isNaN(parsed) ? '' : parsed;
|
||||
}
|
||||
},
|
||||
lcpText: {
|
||||
filter: 'agTextColumnFilter',
|
||||
filterParams: {
|
||||
buttons: ['apply', 'reset'],
|
||||
closeOnApply: true
|
||||
}
|
||||
},
|
||||
lcpColor: {
|
||||
filter: 'agTextColumnFilter',
|
||||
filterParams: {
|
||||
buttons: ['apply', 'reset'],
|
||||
closeOnApply: true
|
||||
},
|
||||
cellRenderer: ColorCellRenderer,
|
||||
valueParser: params => {
|
||||
if (!params.newValue) return '';
|
||||
return /^#([0-9A-F]{3}){1,2}$/i.test(params.newValue) ?
|
||||
params.newValue.toUpperCase() : '';
|
||||
}
|
||||
},
|
||||
lcpDate: {
|
||||
filter: 'agDateColumnFilter',
|
||||
filterParams: {
|
||||
buttons: ['apply', 'reset'],
|
||||
closeOnApply: true
|
||||
},
|
||||
valueParser: params => {
|
||||
if (!params.newValue) return '';
|
||||
const date = new Date(params.newValue);
|
||||
return isNaN(date.getTime()) ? '' : date.toISOString();
|
||||
}
|
||||
}
|
||||
}), []);
|
||||
|
||||
const handleHeaderChange = useCallback((oldHeader, newHeader) => {
|
||||
if (oldHeader === newHeader || !chartData.length) return;
|
||||
|
||||
// Update the chartData with the new header
|
||||
const newData = chartData.map(row => {
|
||||
const { [oldHeader]: value, ...rest } = row;
|
||||
return {
|
||||
...rest,
|
||||
[newHeader]: value
|
||||
};
|
||||
});
|
||||
|
||||
onDataChange(newData);
|
||||
}, [chartData, onDataChange]);
|
||||
|
||||
// Generate column definitions based on chartData
|
||||
const columnDefs = useMemo(() => {
|
||||
if (!chartData.length) return [];
|
||||
|
||||
// Get all unique keys from the data
|
||||
const allKeys = Array.from(new Set(
|
||||
chartData.flatMap(item => Object.keys(item))
|
||||
)).filter(key => key !== 'Parent'); // Remove Parent from regular columns
|
||||
|
||||
// Create column definitions for regular columns
|
||||
const regularColumns = allKeys.map(key => {
|
||||
const sampleValue = chartData.find(item => item[key] !== undefined)?.[key];
|
||||
const defaultType = guessColumnType(sampleValue);
|
||||
const currentType = migratedColumnTypes[key] || defaultType;
|
||||
|
||||
return {
|
||||
field: key,
|
||||
headerName: key,
|
||||
hide: key === 'ID',
|
||||
headerComponent: ColumnHeaderComponent,
|
||||
headerComponentParams: {
|
||||
displayName: key,
|
||||
onHeaderChange: handleHeaderChange,
|
||||
setColumnType: handleColumnTypeChange,
|
||||
currentType: currentType
|
||||
},
|
||||
type: currentType,
|
||||
editable: key !== 'ID',
|
||||
sortable: true,
|
||||
filter: true
|
||||
};
|
||||
});
|
||||
|
||||
// Add Parent column with ParentCellRenderer
|
||||
const parentColumn = {
|
||||
field: 'Parent',
|
||||
headerName: __('Parent', 'lcp-visualize'),
|
||||
cellRenderer: ParentCellRenderer,
|
||||
cellRendererParams: {
|
||||
context: {
|
||||
handleParentChange
|
||||
}
|
||||
},
|
||||
editable: false,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
minWidth: 200
|
||||
};
|
||||
|
||||
return [...regularColumns, parentColumn];
|
||||
}, [chartData, migratedColumnTypes, handleColumnTypeChange, handleHeaderChange, guessColumnType, handleParentChange]);
|
||||
|
||||
const defaultColDef = useMemo(() => ({
|
||||
flex: 1,
|
||||
minWidth: 100,
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
resizable: true
|
||||
}), []);
|
||||
|
||||
const rowData = useMemo(() => {
|
||||
return chartData.map(point => ({
|
||||
...point,
|
||||
ID: point.ID || generateId()
|
||||
}));
|
||||
}, [chartData]);
|
||||
|
||||
const handleContextMenu = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
const target = event.target;
|
||||
const rowCell = target.closest('.ag-cell');
|
||||
|
||||
if (!rowCell) return;
|
||||
|
||||
const rect = rowCell.getBoundingClientRect();
|
||||
const rowNode = gridRef.current.api.getRowNode(rowCell.parentElement.getAttribute('row-index'));
|
||||
if (!rowNode || !rowNode.data || !rowNode.data.ID) return;
|
||||
|
||||
setContextMenu({
|
||||
type: 'row',
|
||||
position: { top: rect.bottom, left: rect.left },
|
||||
rowIndex: rowNode.rowIndex,
|
||||
totalRows: gridRef.current.api.getDisplayedRowCount()
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleRowAction = useCallback((action, rowIndex) => {
|
||||
const newData = [...chartData];
|
||||
|
||||
// Create empty row with all existing columns
|
||||
const emptyRow = { ID: generateId() };
|
||||
if (chartData.length > 0) {
|
||||
// Get all columns from existing data
|
||||
const columns = Array.from(new Set(
|
||||
chartData.flatMap(item => Object.keys(item))
|
||||
));
|
||||
|
||||
// Initialize each column with an appropriate empty value
|
||||
columns.forEach(col => {
|
||||
if (col === 'ID') return; // Skip Id as it's already set
|
||||
|
||||
// Get a sample value to determine appropriate empty value
|
||||
const sampleValue = chartData.find(item => item[col] !== undefined)?.[col];
|
||||
|
||||
switch (typeof sampleValue) {
|
||||
case 'number':
|
||||
emptyRow[col] = 0;
|
||||
break;
|
||||
case 'boolean':
|
||||
emptyRow[col] = false;
|
||||
break;
|
||||
default:
|
||||
emptyRow[col] = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'add-above':
|
||||
newData.splice(rowIndex - 1, 0, emptyRow);
|
||||
break;
|
||||
case 'add-below':
|
||||
newData.splice(rowIndex, 0, emptyRow);
|
||||
break;
|
||||
case 'delete':
|
||||
newData.splice(rowIndex - 1, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
onDataChange(newData);
|
||||
setContextMenu(null);
|
||||
}, [chartData, onDataChange]);
|
||||
|
||||
const handleCellValueChanged = useCallback((params) => {
|
||||
if (!params.data || !params.data.ID) return;
|
||||
|
||||
const newData = chartData.map(item =>
|
||||
item.ID === params.data.ID ? params.data : item
|
||||
);
|
||||
|
||||
onDataChange(newData);
|
||||
}, [chartData, onDataChange]);
|
||||
|
||||
return (
|
||||
<div className="ag-theme-alpine" style={{ height: '400px', width: '100%' }}>
|
||||
<AgGridReact
|
||||
ref={gridRef}
|
||||
rowData={rowData}
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
columnTypes={gridColumnTypes}
|
||||
onCellValueChanged={handleCellValueChanged}
|
||||
suppressRowClickSelection={true}
|
||||
rowSelection="multiple"
|
||||
animateRows={true}
|
||||
context={{
|
||||
handleParentChange
|
||||
}}
|
||||
/>
|
||||
{contextMenu && (
|
||||
<Popover
|
||||
position="bottom left"
|
||||
onClose={() => setContextMenu(null)}
|
||||
anchorRect={{
|
||||
top: contextMenu.mouseEvent.clientY,
|
||||
left: contextMenu.mouseEvent.clientX,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '8px' }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const selectedRows = gridRef.current.api.getSelectedRows();
|
||||
const newData = chartData.filter(row => !selectedRows.includes(row));
|
||||
onDataChange(newData);
|
||||
setContextMenu(null);
|
||||
}}
|
||||
>
|
||||
{__('Delete Selected Rows', 'lcp-visualize')}
|
||||
</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LCPDataGrid;
|
||||
201
blocks/components/LCPDatasetBuilder.js
Normal file
201
blocks/components/LCPDatasetBuilder.js
Normal file
@ -0,0 +1,201 @@
|
||||
import { useState, useEffect, useMemo } from '@wordpress/element';
|
||||
import { Button, Modal, SelectControl, ToggleControl } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import LCPDataGrid from './LCPDataGrid';
|
||||
|
||||
const LCPDatasetBuilder = ({ chartData = [], onChange, attributes, setAttributes, chartType }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [options, setOptions] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (chartData && chartData.length > 0) {
|
||||
const columns = Object.keys(chartData[0]).filter(key => key !== 'lcpId');
|
||||
setOptions(columns.map(col => ({ label: col, value: col })));
|
||||
}
|
||||
}, [chartData]);
|
||||
|
||||
// Get available columns from chartData
|
||||
const getAvailableColumns = () => {
|
||||
if (!chartData.length) return [];
|
||||
|
||||
// Get all unique keys from the data
|
||||
const columns = Array.from(new Set(
|
||||
chartData.flatMap(item => Object.keys(item))
|
||||
)).filter(key => key !== 'lcpId'); // Exclude lcpId column
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
// Get column options based on type filter
|
||||
const getColumnOptions = (columns, typeFilter = null) => {
|
||||
return columns
|
||||
.filter(col => {
|
||||
if (!typeFilter) return true;
|
||||
const columnType = attributes.columnTypes?.[col];
|
||||
return columnType === typeFilter;
|
||||
})
|
||||
.map(col => ({ label: col, value: col }));
|
||||
};
|
||||
|
||||
const handleDataChange = (newData) => {
|
||||
console.log('DatasetBuilder updating chartData:', newData);
|
||||
onChange(newData);
|
||||
};
|
||||
|
||||
const downloadCSV = () => {
|
||||
// Get all columns except lcpId
|
||||
const columns = getAvailableColumns().map(col => col.value);
|
||||
|
||||
// Create CSV header
|
||||
const header = columns.join(',');
|
||||
|
||||
// Create CSV rows
|
||||
const rows = chartData.map(row =>
|
||||
columns.map(col => {
|
||||
let value = row[col] || '';
|
||||
if (typeof value === 'string' && (value.includes(',') || value.includes('"'))) {
|
||||
value = `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return value;
|
||||
}).join(',')
|
||||
).join('\n');
|
||||
|
||||
const csv = `${header}\n${rows}`;
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', 'chart-data.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// Get default value for valueColumn
|
||||
const defaultValueColumn = useMemo(() => {
|
||||
return attributes.valueColumn || (options[0]?.value || '');
|
||||
}, [attributes.valueColumn, options]);
|
||||
|
||||
// Get all available columns
|
||||
const availableColumns = getAvailableColumns();
|
||||
|
||||
// Get all column options
|
||||
const allOptions = getColumnOptions(availableColumns);
|
||||
|
||||
// Get numeric column options for bar chart value
|
||||
const numericOptions = getColumnOptions(availableColumns, 'lcpNumber');
|
||||
|
||||
// Determine which options to use for value column
|
||||
const valueColumnOptions = chartType === 'bar' ? numericOptions : allOptions;
|
||||
|
||||
// Add "None" option for optional fields
|
||||
const optionalOptions = [{ label: __('None', 'lcp-visualize'), value: '' }, ...allOptions];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setIsOpen(true)}
|
||||
style={{ marginBottom: '10px', width: '100%' }}
|
||||
>
|
||||
{__('Edit Dataset', 'lcp-visualize')}
|
||||
</Button>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
onRequestClose={() => setIsOpen(false)}
|
||||
title="Dataset Builder"
|
||||
style={{ width: '90vw', height: '90vh' }}
|
||||
>
|
||||
<div style={{ height: 'calc(90vh - 40px)', padding: '20px', display: 'flex', gap: '20px', marginBottom: '20px' }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<LCPDataGrid
|
||||
chartData={chartData}
|
||||
onDataChange={handleDataChange}
|
||||
attributes={attributes}
|
||||
setAttributes={setAttributes}
|
||||
columnTypes={attributes.columnTypes}
|
||||
onColumnTypeChange={(field, type) => {
|
||||
const newColumnTypes = {
|
||||
...attributes.columnTypes,
|
||||
[field]: type
|
||||
};
|
||||
|
||||
setAttributes({
|
||||
columnTypes: newColumnTypes,
|
||||
// If we're changing from a numeric type and this is the value column, reset it
|
||||
valueColumn: field === attributes.valueColumn &&
|
||||
type !== 'lcpNumber' &&
|
||||
chartType === 'bar' ? '' : attributes.valueColumn
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{
|
||||
width: '300px',
|
||||
padding: '16px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px'
|
||||
}}>
|
||||
<SelectControl
|
||||
label={__('Value Column', 'lcp-visualize')}
|
||||
help={chartType === 'bar' ?
|
||||
__('Select a numeric column for bar heights', 'lcp-visualize') :
|
||||
__('Column to use for values', 'lcp-visualize')
|
||||
}
|
||||
value={attributes.valueColumn || ''}
|
||||
options={valueColumnOptions}
|
||||
onChange={(value) => setAttributes({ valueColumn: value })}
|
||||
/>
|
||||
{chartType === 'bar' && valueColumnOptions.length === 0 && (
|
||||
<div style={{ color: '#cc1818', marginTop: '4px' }}>
|
||||
{__('Please set at least one column type to Number to use as the value column', 'lcp-visualize')}
|
||||
</div>
|
||||
)}
|
||||
<SelectControl
|
||||
label={__('Labels Column', 'lcp-visualize')}
|
||||
help={__('Column to use for bar labels', 'lcp-visualize')}
|
||||
value={attributes.labelsColumn || (options[0]?.value || '')}
|
||||
options={options}
|
||||
onChange={(value) => setAttributes({ labelsColumn: value })}
|
||||
/>
|
||||
<SelectControl
|
||||
label={__('Color Column', 'lcp-visualize')}
|
||||
help={__('Column containing bar colors (optional)', 'lcp-visualize')}
|
||||
value={attributes.colorColumn || ''}
|
||||
options={optionalOptions}
|
||||
onChange={(value) => setAttributes({ colorColumn: value })}
|
||||
/>
|
||||
<SelectControl
|
||||
label={__('Popover Content Column', 'lcp-visualize')}
|
||||
help={__('Column containing HTML content for popovers (optional)', 'lcp-visualize')}
|
||||
value={attributes.popoverColumn || ''}
|
||||
options={optionalOptions}
|
||||
onChange={(value) => setAttributes({ popoverColumn: value })}
|
||||
/>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={downloadCSV}
|
||||
icon={
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" style={{ fill: 'currentColor' }}>
|
||||
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
{__('Download CSV', 'lcp-visualize')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LCPDatasetBuilder;
|
||||
55
blocks/components/LCPDimensionControl.js
Normal file
55
blocks/components/LCPDimensionControl.js
Normal file
@ -0,0 +1,55 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { SelectControl, TextControl } from '@wordpress/components';
|
||||
|
||||
const LCPDimensionControl = ({ label, value, onChange, unitTypes }) => {
|
||||
// Parse the current value into number and unit
|
||||
const parseValue = (val) => {
|
||||
const match = val?.match(/^(\d+)(.*)$/);
|
||||
return match ? {
|
||||
number: parseInt(match[1], 10),
|
||||
unit: match[2] || 'px'
|
||||
} : { number: 300, unit: 'px' };
|
||||
};
|
||||
|
||||
const { number, unit } = parseValue(value);
|
||||
|
||||
// Default units
|
||||
const allUnits = [
|
||||
{ label: 'Pixels (px)', value: 'px' },
|
||||
{ label: 'Percentage (%)', value: '%' },
|
||||
{ label: 'Viewport Width (vw)', value: 'vw' },
|
||||
{ label: 'Viewport Height (vh)', value: 'vh' }
|
||||
];
|
||||
|
||||
// Filter units based on unitTypes prop or use allUnits if unitTypes is not passed
|
||||
const units = unitTypes && unitTypes.length > 0
|
||||
? allUnits.filter(unit => unitTypes.includes(unit.value))
|
||||
: allUnits;
|
||||
|
||||
return (
|
||||
<div className="lcp-dimension-control">
|
||||
<label className="components-base-control__label">{label}</label>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start' }}>
|
||||
<TextControl
|
||||
type="number"
|
||||
value={number}
|
||||
onChange={(newNumber) => {
|
||||
onChange(`${newNumber}${unit}`);
|
||||
}}
|
||||
min={0}
|
||||
style={{ width: '80px' }}
|
||||
/>
|
||||
<SelectControl
|
||||
value={unit}
|
||||
options={units}
|
||||
onChange={(newUnit) => {
|
||||
onChange(`${number}${newUnit}`);
|
||||
}}
|
||||
style={{ minWidth: '100px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LCPDimensionControl;
|
||||
56
blocks/components/LCPHTMLModal.js
Normal file
56
blocks/components/LCPHTMLModal.js
Normal file
@ -0,0 +1,56 @@
|
||||
import { useState } from '@wordpress/element';
|
||||
import { Button, Modal, TextControl } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const LCPHTMLModal= () => {
|
||||
// State to control modal visibility and text value
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [textValue, setTextValue] = useState('');
|
||||
|
||||
// Open modal function
|
||||
const openModal = () => setIsModalOpen(true);
|
||||
|
||||
// Close modal function
|
||||
const closeModal = () => setIsModalOpen(false);
|
||||
|
||||
// Handle text change
|
||||
const handleTextChange = (newValue) => {
|
||||
setTextValue(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Button to trigger modal */}
|
||||
<Button isPrimary onClick={openModal}>
|
||||
{__('Open Modal', 'lcp')}
|
||||
</Button>
|
||||
|
||||
{/* Modal */}
|
||||
{isModalOpen && (
|
||||
<Modal
|
||||
title={__('Text Area Modal', 'lcp')}
|
||||
onRequestClose={closeModal}
|
||||
className="my-modal"
|
||||
>
|
||||
{/* Modal content */}
|
||||
<div>
|
||||
<TextControl
|
||||
label={__('Enter your text:', 'lcp')}
|
||||
value={textValue}
|
||||
onChange={handleTextChange}
|
||||
placeholder={__('Type here...', 'lcp')}
|
||||
style={{ width: '100%', minHeight: '100px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal footer */}
|
||||
<div style={{ marginTop: '16px', textAlign: 'right' }}>
|
||||
<Button onClick={closeModal}>{__('Close', 'lcp')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LCPHTMLModal;
|
||||
41
blocks/components/ParentCellRenderer.js
Normal file
41
blocks/components/ParentCellRenderer.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { SelectControl } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const ParentCellRenderer = (props) => {
|
||||
if (!props.data || !props.data.ID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create options from all available rows except current
|
||||
const options = [
|
||||
{ label: __('None', 'lcp-visualize'), value: '' }
|
||||
];
|
||||
|
||||
// Get all rows from the grid
|
||||
props.api.forEachNode(node => {
|
||||
if (node.data &&
|
||||
node.data.ID &&
|
||||
node.data.ID !== props.data.ID) {
|
||||
options.push({
|
||||
label: node.data.Label || node.data.ID,
|
||||
value: node.data.ID
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const handleChange = (newValue) => {
|
||||
if (props.context && props.context.handleParentChange) {
|
||||
props.context.handleParentChange(props.data, newValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectControl
|
||||
value={props.data.Parent || ''}
|
||||
options={options}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParentCellRenderer;
|
||||
22996
blocks/package-lock.json
generated
Normal file
22996
blocks/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
blocks/package.json
Normal file
22
blocks/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "lcp-bar-graph",
|
||||
"version": "1.0.0",
|
||||
"description": "Bar graph block using D3.js",
|
||||
"scripts": {
|
||||
"build": "wp-scripts build",
|
||||
"start": "wp-scripts start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wordpress/block-editor": "^12.0.0",
|
||||
"@wordpress/blocks": "^12.0.0",
|
||||
"@wordpress/components": "^25.0.0",
|
||||
"@wordpress/element": "^5.0.0",
|
||||
"@wordpress/i18n": "^4.0.0",
|
||||
"ag-grid-community": "^31.3.4",
|
||||
"ag-grid-react": "^31.0.1",
|
||||
"d3": "^7.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wordpress/scripts": "^26.0.0"
|
||||
}
|
||||
}
|
||||
75
lcp-visualize.php
Normal file
75
lcp-visualize.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: LCP Visualize
|
||||
* Description: Data visualization blocks using D3.js
|
||||
* Version: 1.0.0
|
||||
* Author: LCP
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
function lcp_visualize_register_blocks() {
|
||||
if (!function_exists('register_block_type')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$asset_file = include(plugin_dir_path(__FILE__) . 'blocks/bar-graph/build/index.asset.php');
|
||||
|
||||
wp_register_style(
|
||||
'ag-grid',
|
||||
'https://unpkg.com/ag-grid-community/dist/styles/ag-grid.css',
|
||||
[],
|
||||
'27.1.0'
|
||||
);
|
||||
|
||||
wp_register_style(
|
||||
'ag-grid-theme',
|
||||
'https://unpkg.com/ag-grid-community/dist/styles/ag-theme-balham.css',
|
||||
[],
|
||||
'27.1.0'
|
||||
);
|
||||
|
||||
wp_register_style(
|
||||
'lcp-bar-graph-editor',
|
||||
plugins_url('blocks/bar-graph/build/index.css', __FILE__),
|
||||
[],
|
||||
$asset_file['version']
|
||||
);
|
||||
|
||||
wp_register_script(
|
||||
'lcp-bar-graph',
|
||||
plugins_url('blocks/bar-graph/build/index.js', __FILE__),
|
||||
$asset_file['dependencies'],
|
||||
$asset_file['version']
|
||||
);
|
||||
|
||||
register_block_type('lcp/bar-graph', array(
|
||||
'editor_script' => 'lcp-bar-graph',
|
||||
'editor_style' => ['ag-grid', 'ag-grid-theme', 'lcp-bar-graph-editor'],
|
||||
'render_callback' => 'render_block_lcp_bar_graph'
|
||||
));
|
||||
}
|
||||
add_action('init', 'lcp_visualize_register_blocks');
|
||||
|
||||
function render_block_lcp_bar_graph($attributes) {
|
||||
wp_enqueue_style('ag-grid');
|
||||
wp_enqueue_style('ag-grid-theme');
|
||||
wp_enqueue_style('lcp-bar-graph-editor');
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes([
|
||||
'class' => 'wp-block-lcp-bar-graph'
|
||||
]);
|
||||
|
||||
$chart_data = $attributes['chartData'] ?? [];
|
||||
$chart_data_json = wp_json_encode($chart_data);
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div <?php echo $wrapper_attributes; ?>>
|
||||
<div class="lcp-bar-graph-container" data-chart='<?php echo esc_attr($chart_data_json); ?>'></div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
22974
package-lock.json
generated
Normal file
22974
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "lcp-visualize",
|
||||
"version": "1.0.0",
|
||||
"description": "Data visualization blocks using D3.js",
|
||||
"scripts": {
|
||||
"build": "wp-scripts build",
|
||||
"start": "wp-scripts start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wordpress/block-editor": "^12.0.0",
|
||||
"@wordpress/blocks": "^12.0.0",
|
||||
"@wordpress/components": "^25.0.0",
|
||||
"@wordpress/element": "^5.0.0",
|
||||
"@wordpress/i18n": "^4.0.0",
|
||||
"d3": "^7.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wordpress/scripts": "^26.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user