This commit is contained in:
Jeremy Rangel
2025-01-21 18:41:51 -08:00
commit dd4bd0caf5
26 changed files with 70922 additions and 0 deletions

View 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"
}

View 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');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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"
}

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

View 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;
}
}

View 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>
);
}

View 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;
}
}
}
}

View 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
});

View 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;
}
}
}