Compare commits

...

2 Commits

Author SHA1 Message Date
3c9d74e8f6 Added basic support for stacked bars 2025-01-15 22:47:56 -08:00
9ce6586662 Added support for hierarchical bar graphs 2025-01-15 22:31:22 -08:00
9 changed files with 358 additions and 76 deletions

View File

@ -17,6 +17,14 @@
"style": "file:./style-index.css", "style": "file:./style-index.css",
"viewScript": "file:./view.js", "viewScript": "file:./view.js",
"attributes": { "attributes": {
"enableHierarchicalView": {
"type": "boolean",
"default": false
},
"selectedParentId": {
"type": "string",
"default": null
},
"chartColorSource": { "chartColorSource": {
"type": "string", "type": "string",
"default": "default" "default": "default"
@ -116,6 +124,10 @@
"gridOpacity": { "gridOpacity": {
"type": "number", "type": "number",
"default": 0.5 "default": 0.5
},
"enableStackedBars": {
"type": "boolean",
"default": false
} }
} }
} }

View File

@ -1 +1 @@
<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => 'd3e2bb3c261756c0085a'); <?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => '79dffd7b201fcf20bc7c');

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<?php return array('dependencies' => array(), 'version' => '70f08b60c296a36b16fe'); <?php return array('dependencies' => array(), 'version' => 'cd3eca3e743bd29f75fd');

File diff suppressed because one or more lines are too long

View File

@ -17,6 +17,14 @@
"style": "file:./style-index.css", "style": "file:./style-index.css",
"viewScript": "file:./view.js", "viewScript": "file:./view.js",
"attributes": { "attributes": {
"enableHierarchicalView": {
"type": "boolean",
"default": false
},
"selectedParentId": {
"type": "string",
"default": null
},
"chartColorSource": { "chartColorSource": {
"type": "string", "type": "string",
"default": "default" "default": "default"
@ -116,6 +124,10 @@
"gridOpacity": { "gridOpacity": {
"type": "number", "type": "number",
"default": 0.5 "default": 0.5
},
"enableStackedBars": {
"type": "boolean",
"default": false
} }
} }
} }

View File

@ -19,33 +19,108 @@ const BarGraph = ({
colorSource, colorSource,
defaultBarColor, defaultBarColor,
customColors, customColors,
barOpacity barOpacity,
enableHierarchicalView,
enableStackedBars,
selectedParentId,
setAttributes
}) => { }) => {
const svgRef = useRef(); const svgRef = useRef();
const containerRef = useRef(); const containerRef = useRef();
// Helper function to darken a color
const darkenColor = (color, amount) => {
const rgb = d3.color(color).rgb();
return d3.rgb(
Math.max(0, rgb.r - amount),
Math.max(0, rgb.g - amount),
Math.max(0, rgb.b - amount)
).toString();
};
// Helper function to get children of a node
const getChildren = (nodeId) => {
const children = [];
Object.entries(data).forEach(([dataset, items]) => {
items.forEach(item => {
if (item.parent === nodeId) {
children.push({
...item,
dataset,
color: item.color || defaultBarColor
});
}
});
});
return children;
};
useEffect(() => { useEffect(() => {
if (!data || !svgRef.current || !containerRef.current) return; if (!data || !svgRef.current || !containerRef.current) return;
// Clear previous content // Clear previous content
d3.select(svgRef.current).selectAll("*").remove(); d3.select(svgRef.current).selectAll("*").remove();
// Convert data to simple array // Filter data based on hierarchical view
const chartData = []; let displayData = data;
for (const [dataset, items] of Object.entries(data)) { if (enableHierarchicalView && !enableStackedBars) {
for (const item of items) { displayData = {};
chartData.push({ Object.entries(data).forEach(([dataset, items]) => {
dataset, const filteredItems = items.filter(item => item.parent === selectedParentId);
label: item.label, if (filteredItems.length > 0) {
value: parseInt(item.value, 10), displayData[dataset] = filteredItems;
color: item.color || '#FF6384' }
}); });
}
} }
// Convert data to array format
const chartDataArray = [];
Object.entries(displayData).forEach(([dataset, items]) => {
items.forEach(item => {
if (!enableStackedBars || item.parent === null) {
const baseColor = item.color || defaultBarColor;
const children = getChildren(item.id);
// Create the main bar
const barData = {
id: item.id,
dataset,
label: item.label || '',
value: parseFloat(item.value) || 0,
color: baseColor,
hasChildren: children.length > 0,
children: []
};
// If stacked bars are enabled, add children data
if (enableStackedBars && children.length > 0) {
let usedColors = new Set([baseColor]);
children.forEach((child, index) => {
let childColor = child.color || baseColor;
// If color is already used, darken it
while (usedColors.has(childColor)) {
childColor = darkenColor(childColor, 30);
}
usedColors.add(childColor);
barData.children.push({
id: child.id,
label: child.label,
value: parseFloat(child.value) || 0,
color: childColor
});
});
}
chartDataArray.push(barData);
}
});
});
// Get container dimensions // Get container dimensions
const containerWidth = containerRef.current.clientWidth; const containerWidth = containerRef.current.clientWidth;
const containerHeight = containerRef.current.clientHeight; const containerHeight = typeof height === 'number' ? height : 400;
// Basic dimensions with extra margin for axis labels // Basic dimensions with extra margin for axis labels
const margin = { const margin = {
@ -54,13 +129,14 @@ const BarGraph = ({
bottom: xAxisLabel ? 50 : 30, bottom: xAxisLabel ? 50 : 30,
left: yAxisLabel ? 60 : 40 left: yAxisLabel ? 60 : 40
}; };
const innerWidth = containerWidth - margin.left - margin.right; const innerWidth = Math.max(containerWidth - margin.left - margin.right, 0);
const innerHeight = containerHeight - margin.top - margin.bottom; const innerHeight = Math.max(containerHeight - margin.top - margin.bottom, 0);
// Create SVG // Create SVG
const svg = d3.select(svgRef.current) const svg = d3.select(svgRef.current)
.attr('width', containerWidth) .attr('width', containerWidth)
.attr('height', containerHeight); .attr('height', containerHeight)
.style('background-color', backgroundColor);
// Add title if present // Add title if present
if (title) { if (title) {
@ -78,12 +154,17 @@ const BarGraph = ({
// Create scales // Create scales
const xScale = d3.scaleBand() const xScale = d3.scaleBand()
.domain(chartData.map(d => d.label)) .domain(chartDataArray.map(d => d.label))
.range([0, innerWidth]) .range([0, innerWidth])
.padding(0.1); .padding(0.1);
const yScale = d3.scaleLinear() const yScale = d3.scaleLinear()
.domain([0, d3.max(chartData, d => d.value)]) .domain([0, d3.max(chartDataArray, d => {
if (enableStackedBars) {
return d.value + d.children.reduce((sum, child) => sum + child.value, 0);
}
return d.value;
}) || 100])
.range([innerHeight, 0]); .range([innerHeight, 0]);
// Get color for a datapoint based on color source // Get color for a datapoint based on color source
@ -92,7 +173,7 @@ const BarGraph = ({
case 'singleColor': case 'singleColor':
return defaultBarColor; return defaultBarColor;
case 'customColors': case 'customColors':
const customColor = customColors.find( const customColor = customColors?.find(
c => c.dataset === datapoint.dataset && c.label === datapoint.label c => c.dataset === datapoint.dataset && c.label === datapoint.label
); );
return customColor ? customColor.color : datapoint.color; return customColor ? customColor.color : datapoint.color;
@ -126,34 +207,111 @@ const BarGraph = ({
} }
// Add bars // Add bars
const bars = g.selectAll('rect') const barGroups = g.selectAll('.bar-group')
.data(chartData) .data(chartDataArray)
.enter() .enter()
.append('rect') .append('g')
.attr('x', d => xScale(d.label)) .attr('class', 'bar-group')
.attr('transform', d => `translate(${xScale(d.label)},0)`);
// Add main bars
barGroups.append('rect')
.attr('class', 'main-bar')
.attr('y', d => yScale(d.value)) .attr('y', d => yScale(d.value))
.attr('width', xScale.bandwidth()) .attr('width', xScale.bandwidth())
.attr('height', d => innerHeight - yScale(d.value)) .attr('height', d => innerHeight - yScale(d.value))
.attr('fill', d => getColor(d)) .attr('fill', d => getColor(d))
.style('opacity', barOpacity); .style('opacity', barOpacity)
.style('cursor', d => (d.hasChildren && !enableStackedBars) ? 'pointer' : 'default');
// Add stacked child bars if enabled
if (enableStackedBars) {
barGroups.each(function(d) {
let yOffset = d.value;
d.children.forEach(child => {
d3.select(this)
.append('rect')
.attr('class', 'child-bar')
.attr('y', () => yScale(yOffset + child.value))
.attr('width', xScale.bandwidth())
.attr('height', () => innerHeight - yScale(child.value))
.attr('fill', child.color)
.style('opacity', barOpacity);
yOffset += child.value;
});
});
}
// Add click handlers for hierarchical view
if (enableHierarchicalView && !enableStackedBars) {
barGroups.selectAll('.main-bar').on('click', function(event, d) {
if (d.hasChildren && setAttributes) {
setAttributes({ selectedParentId: d.id });
}
});
// Add back button if not at root level
if (selectedParentId !== null) {
svg.append('text')
.attr('x', margin.left)
.attr('y', margin.top / 2)
.attr('class', 'back-button')
.style('cursor', 'pointer')
.style('font-weight', 'bold')
.text('← Back')
.on('click', function() {
if (setAttributes) {
let parentParentId = null;
Object.values(data).forEach(items => {
const currentItem = items.find(item => item.id === selectedParentId);
if (currentItem) {
parentParentId = currentItem.parent;
}
});
setAttributes({ selectedParentId: parentParentId });
}
});
}
}
// Add bar values // Add bar values
if (showBarValues) { if (showBarValues) {
g.selectAll('.bar-value') barGroups.each(function(d) {
.data(chartData) // Add value for main bar
.enter() d3.select(this)
.append('text') .append('text')
.attr('class', 'bar-value') .attr('class', 'bar-value')
.attr('x', d => xScale(d.label) + xScale.bandwidth() / 2) .attr('x', xScale.bandwidth() / 2)
.attr('y', d => yScale(d.value) - 5) .attr('y', yScale(d.value) - 5)
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
.text(d => d.value); .text(d.value);
// Add values for stacked bars if enabled
if (enableStackedBars) {
let yOffset = d.value;
d.children.forEach(child => {
d3.select(this)
.append('text')
.attr('class', 'bar-value')
.attr('x', xScale.bandwidth() / 2)
.attr('y', yScale(yOffset + child.value) - 5)
.attr('text-anchor', 'middle')
.text(child.value);
yOffset += child.value;
});
}
});
} }
// Add X axis // Add X axis
g.append('g') g.append('g')
.attr('transform', `translate(0,${innerHeight})`) .attr('transform', `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale)); .call(d3.axisBottom(xScale))
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-45)');
// Add Y axis // Add Y axis
g.append('g') g.append('g')
@ -182,7 +340,7 @@ const BarGraph = ({
.text(yAxisLabel); .text(yAxisLabel);
} }
}, [data, width, height, title, showGridX, showGridY, xGridColor, yGridColor, xGridWidth, yGridWidth, showBarValues, xAxisLabel, yAxisLabel, colorSource, defaultBarColor, customColors, barOpacity]); }, [data, width, height, title, showGridX, showGridY, xGridColor, yGridColor, xGridWidth, yGridWidth, showBarValues, xAxisLabel, yAxisLabel, colorSource, defaultBarColor, customColors, barOpacity, enableHierarchicalView, enableStackedBars, selectedParentId]);
return ( return (
<div <div

View File

@ -12,7 +12,8 @@ import {
TabPanel, TabPanel,
TextControl, TextControl,
Button, Button,
Popover Popover,
TextareaControl
} from '@wordpress/components'; } from '@wordpress/components';
import {useState} from '@wordpress/element'; import {useState} from '@wordpress/element';
@ -50,7 +51,10 @@ export default function Edit({ attributes, setAttributes }) {
xAxisLabel, xAxisLabel,
yAxisLabel, yAxisLabel,
chartColorSource, chartColorSource,
chartCustomColors } = attributes; chartCustomColors,
enableHierarchicalView,
enableStackedBars,
selectedParentId } = attributes;
const blockProps = useBlockProps(); const blockProps = useBlockProps();
@ -163,6 +167,43 @@ export default function Edit({ attributes, setAttributes }) {
// Debug log to check chartData // Debug log to check chartData
console.log('Chart data:', chartData); console.log('Chart data:', chartData);
// Ensure chartData is an object
if (!attributes.chartData || typeof attributes.chartData !== 'object') {
setAttributes({ chartData: {} });
}
const updateChartData = (value) => {
try {
// Attempt to parse the JSON input
const parsedData = JSON.parse(value);
// Validate the structure
if (typeof parsedData === 'object' && parsedData !== null) {
// Check if the data follows our expected format
let isValid = true;
Object.entries(parsedData).forEach(([dataset, items]) => {
if (!Array.isArray(items)) {
isValid = false;
} else {
items.forEach(item => {
if (!item.id || typeof item.id !== 'string') {
isValid = false;
}
});
}
});
if (isValid) {
setAttributes({ chartData: parsedData });
} else {
console.error('Invalid data structure');
}
}
} catch (e) {
console.error('Invalid JSON:', e);
}
};
return ( return (
<div {...blockProps}> <div {...blockProps}>
<InspectorControls> <InspectorControls>
@ -192,6 +233,7 @@ export default function Edit({ attributes, setAttributes }) {
return ( return (
<Panel> <Panel>
<PanelBody title="Data Settings"> <PanelBody title="Data Settings">
<LCPDataSelector <LCPDataSelector
value={chartData} value={chartData}
onChange={handleDataChange} onChange={handleDataChange}
@ -199,7 +241,17 @@ export default function Edit({ attributes, setAttributes }) {
onDataSourceChange={handleDataSourceChange} onDataSourceChange={handleDataSourceChange}
/> />
</PanelBody> </PanelBody>
<PanelBody title="Chart Settings"> <PanelBody title={__('Chart Settings', 'lcp')} initialOpen={true}>
<ToggleControl
label={__('Enable Hierarchical View', 'lcp')}
checked={enableHierarchicalView}
onChange={(value) => setAttributes({ enableHierarchicalView: value })}
/>
<ToggleControl
label={__('Enable Stacked Bars', 'lcp')}
checked={enableStackedBars}
onChange={(value) => setAttributes({ enableStackedBars: value })}
/>
<ToggleControl <ToggleControl
label={__('Display Chart Title', 'lcp')} label={__('Display Chart Title', 'lcp')}
checked={displayChartTitle} checked={displayChartTitle}
@ -439,17 +491,19 @@ export default function Edit({ attributes, setAttributes }) {
showGridX={showGridX} showGridX={showGridX}
showGridY={showGridY} showGridY={showGridY}
yGridColor={yGridColor} yGridColor={yGridColor}
xGridColor={xGridColor} xGridColor={xGridColor}
xGridWidth={xGridWidth} xGridWidth={xGridWidth}
yGridWidth={yGridWidth} yGridWidth={yGridWidth}
title={displayChartTitle ? chartTitle : ''} title={displayChartTitle ? chartTitle : ''}
showSorting={showSorting}
showFiltering={showFiltering}
showBarValues={showBarValues} showBarValues={showBarValues}
xAxisLabel={xAxisLabel} xAxisLabel={xAxisLabel}
yAxisLabel={yAxisLabel} yAxisLabel={yAxisLabel}
colorSource={chartColorSource} colorSource={chartColorSource}
customColors={chartCustomColors} customColors={chartCustomColors}
enableHierarchicalView={enableHierarchicalView}
enableStackedBars={enableStackedBars}
selectedParentId={selectedParentId}
setAttributes={setAttributes}
/> />
) : ( ) : (
<div <div

View File

@ -23,12 +23,8 @@
import * as d3 from 'd3'; import * as d3 from 'd3';
window.addEventListener('load', function() { window.addEventListener('load', function() {
// Check if we have any bar graphs to render if (!window.lcpBarGraphData) return;
if (!window.lcpBarGraphData) {
return;
}
// Render each bar graph
Object.entries(window.lcpBarGraphData).forEach(([blockId, data]) => { Object.entries(window.lcpBarGraphData).forEach(([blockId, data]) => {
const { attributes } = data; const { attributes } = data;
const container = document.getElementById(blockId); const container = document.getElementById(blockId);
@ -38,36 +34,52 @@ window.addEventListener('load', function() {
}); });
}); });
function filterDataByParent(chartData, parentId) {
const filteredData = {};
Object.entries(chartData).forEach(([dataset, items]) => {
const filteredItems = items.filter(item => item.parent === parentId);
if (filteredItems.length > 0) {
filteredData[dataset] = filteredItems;
}
});
return filteredData;
}
function hasChildren(chartData, itemId) {
return Object.values(chartData).some(items =>
items.some(item => item.parent === itemId)
);
}
function renderBarGraph(container, attrs) { function renderBarGraph(container, attrs) {
// Clear any existing content
const graphContainer = container.querySelector('.bar-graph-container'); const graphContainer = container.querySelector('.bar-graph-container');
if (!graphContainer) return; if (!graphContainer) return;
graphContainer.innerHTML = ''; graphContainer.innerHTML = '';
// Get the actual width of the container
const containerWidth = graphContainer.clientWidth; const containerWidth = graphContainer.clientWidth;
// Set up dimensions
const margin = { top: 40, right: 20, bottom: 60, left: 60 }; const margin = { top: 40, right: 20, bottom: 60, left: 60 };
const width = containerWidth - margin.left - margin.right; const width = containerWidth - margin.left - margin.right;
const height = attrs.chartHeight - margin.top - margin.bottom; const height = attrs.chartHeight - margin.top - margin.bottom;
// Get data for current level
const currentData = filterDataByParent(attrs.chartData, attrs.selectedParentId);
// Convert data to array format // Convert data to array format
const chartDataArray = []; const chartDataArray = [];
if (attrs.chartData && typeof attrs.chartData === 'object') { Object.entries(currentData).forEach(([dataset, items]) => {
Object.entries(attrs.chartData).forEach(([dataset, items]) => { items.forEach(item => {
if (Array.isArray(items)) { chartDataArray.push({
items.forEach(item => { id: item.id,
chartDataArray.push({ dataset,
dataset, label: item.label || '',
label: item.label || '', value: parseFloat(item.value) || 0,
value: parseFloat(item.value) || 0, color: item.color || attrs.barColor,
color: item.color || attrs.barColor hasChildren: hasChildren(attrs.chartData, item.id)
}); });
});
}
}); });
} });
// Create SVG // Create SVG
const svg = d3.select(graphContainer) const svg = d3.select(graphContainer)
@ -105,7 +117,7 @@ function renderBarGraph(container, attrs) {
} }
}; };
// Add X grid // Add grids
if (attrs.showGridX) { if (attrs.showGridX) {
g.append('g') g.append('g')
.attr('class', 'grid x-grid') .attr('class', 'grid x-grid')
@ -117,7 +129,6 @@ function renderBarGraph(container, attrs) {
.tickFormat('')); .tickFormat(''));
} }
// Add Y grid
if (attrs.showGridY) { if (attrs.showGridY) {
g.append('g') g.append('g')
.attr('class', 'grid y-grid') .attr('class', 'grid y-grid')
@ -129,7 +140,7 @@ function renderBarGraph(container, attrs) {
} }
// Add bars // Add bars
g.selectAll('rect') const bars = g.selectAll('rect')
.data(chartDataArray) .data(chartDataArray)
.enter() .enter()
.append('rect') .append('rect')
@ -138,7 +149,45 @@ function renderBarGraph(container, attrs) {
.attr('width', xScale.bandwidth()) .attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.value)) .attr('height', d => height - yScale(d.value))
.attr('fill', d => getColor(d)) .attr('fill', d => getColor(d))
.style('opacity', attrs.barOpacity); .style('opacity', attrs.barOpacity)
.style('cursor', d => d.hasChildren ? 'pointer' : 'default');
// Add click handlers for bars
bars.on('click', function(event, d) {
if (d.hasChildren) {
const blockData = window.lcpBarGraphData[container.id];
if (blockData) {
blockData.attributes.selectedParentId = d.id;
renderBarGraph(container, blockData.attributes);
}
}
});
// Add back button if not at root level
if (attrs.selectedParentId !== null) {
svg.append('text')
.attr('x', margin.left)
.attr('y', margin.top / 2)
.attr('class', 'back-button')
.style('cursor', 'pointer')
.style('font-weight', 'bold')
.text('← Back')
.on('click', function() {
const blockData = window.lcpBarGraphData[container.id];
if (blockData) {
// Find current item's parent
let parentParentId = null;
Object.values(attrs.chartData).forEach(items => {
const currentItem = items.find(item => item.id === attrs.selectedParentId);
if (currentItem) {
parentParentId = currentItem.parent;
}
});
blockData.attributes.selectedParentId = parentParentId;
renderBarGraph(container, blockData.attributes);
}
});
}
// Add bar values // Add bar values
if (attrs.showBarValues) { if (attrs.showBarValues) {
@ -153,7 +202,7 @@ function renderBarGraph(container, attrs) {
.text(d => d.value); .text(d => d.value);
} }
// Add X axis // Add axes
g.append('g') g.append('g')
.attr('class', 'x-axis') .attr('class', 'x-axis')
.attr('transform', `translate(0,${height})`) .attr('transform', `translate(0,${height})`)
@ -164,7 +213,6 @@ function renderBarGraph(container, attrs) {
.attr('dy', '.15em') .attr('dy', '.15em')
.attr('transform', 'rotate(-45)'); .attr('transform', 'rotate(-45)');
// Add Y axis
g.append('g') g.append('g')
.attr('class', 'y-axis') .attr('class', 'y-axis')
.call(d3.axisLeft(yScale)); .call(d3.axisLeft(yScale));
@ -179,7 +227,7 @@ function renderBarGraph(container, attrs) {
.text(attrs.chartTitle); .text(attrs.chartTitle);
} }
// Add X axis label // Add axis labels
if (attrs.xAxisLabel) { if (attrs.xAxisLabel) {
svg.append('text') svg.append('text')
.attr('x', containerWidth / 2) .attr('x', containerWidth / 2)
@ -188,7 +236,6 @@ function renderBarGraph(container, attrs) {
.text(attrs.xAxisLabel); .text(attrs.xAxisLabel);
} }
// Add Y axis label
if (attrs.yAxisLabel) { if (attrs.yAxisLabel) {
svg.append('text') svg.append('text')
.attr('transform', 'rotate(-90)') .attr('transform', 'rotate(-90)')
@ -202,7 +249,6 @@ function renderBarGraph(container, attrs) {
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
const newWidth = graphContainer.clientWidth; const newWidth = graphContainer.clientWidth;
if (newWidth !== containerWidth) { if (newWidth !== containerWidth) {
// Clear and redraw
graphContainer.innerHTML = ''; graphContainer.innerHTML = '';
renderBarGraph(container, attrs); renderBarGraph(container, attrs);
} }