Added support for zooming X-Axis on bar graph
This commit is contained in:
@ -17,6 +17,14 @@
|
||||
"style": "file:./style-index.css",
|
||||
"viewScript": "file:./view.js",
|
||||
"attributes": {
|
||||
"drillDownAnimation": {
|
||||
"type": "string",
|
||||
"default": "none"
|
||||
},
|
||||
"enableXZoom": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"enableHierarchicalView": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
@ -128,6 +136,10 @@
|
||||
"enableStackedBars": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"enableBarPopovers": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => 'e934cc621b90f4fd2b9c');
|
||||
<?php return array('dependencies' => array('react', 'react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => '9fe03dfe50c4ea8c8985');
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -17,6 +17,14 @@
|
||||
"style": "file:./style-index.css",
|
||||
"viewScript": "file:./view.js",
|
||||
"attributes": {
|
||||
"drillDownAnimation": {
|
||||
"type": "string",
|
||||
"default": "none"
|
||||
},
|
||||
"enableXZoom": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"enableHierarchicalView": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
@ -128,6 +136,10 @@
|
||||
"enableStackedBars": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"enableBarPopovers": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,32 +1,36 @@
|
||||
import { useEffect, useRef } from '@wordpress/element';
|
||||
import { useEffect, useRef, useState } from '@wordpress/element';
|
||||
import { Popover } from '@wordpress/components';
|
||||
import * as d3 from 'd3';
|
||||
|
||||
const BarGraph = ({
|
||||
data,
|
||||
width = '100%',
|
||||
height = '400px',
|
||||
backgroundColor,
|
||||
data,
|
||||
width = '100%',
|
||||
height = '400px',
|
||||
enableXZoom = false,
|
||||
backgroundColor = '#ffffff',
|
||||
defaultBarColor = '#007cba',
|
||||
barOpacity = 1,
|
||||
title,
|
||||
showGridX,
|
||||
showGridY,
|
||||
xGridColor,
|
||||
yGridColor,
|
||||
xGridWidth,
|
||||
yGridWidth,
|
||||
showBarValues,
|
||||
showBarValues = false,
|
||||
showGridX = false,
|
||||
showGridY = false,
|
||||
xGridColor = '#e0e0e0',
|
||||
yGridColor = '#e0e0e0',
|
||||
xGridWidth = 1,
|
||||
yGridWidth = 1,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
colorSource,
|
||||
defaultBarColor,
|
||||
customColors,
|
||||
barOpacity,
|
||||
colorSource = 'default',
|
||||
customColors = [],
|
||||
enableHierarchicalView,
|
||||
enableStackedBars,
|
||||
selectedParentId,
|
||||
enableBarPopovers,
|
||||
setAttributes
|
||||
}) => {
|
||||
const svgRef = useRef();
|
||||
const containerRef = useRef();
|
||||
const [hoveredBar, setHoveredBar] = useState(null);
|
||||
|
||||
// Helper function to darken a color
|
||||
const darkenColor = (color, amount) => {
|
||||
@ -38,23 +42,6 @@ const BarGraph = ({
|
||||
).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(() => {
|
||||
if (!data || !svgRef.current || !containerRef.current) return;
|
||||
|
||||
@ -63,166 +50,170 @@ const BarGraph = ({
|
||||
|
||||
// Filter data based on hierarchical view
|
||||
let displayData = data;
|
||||
if (enableHierarchicalView && !enableStackedBars) {
|
||||
displayData = {};
|
||||
Object.entries(data).forEach(([dataset, items]) => {
|
||||
const filteredItems = items.filter(item => item.parent === selectedParentId);
|
||||
if (filteredItems.length > 0) {
|
||||
displayData[dataset] = filteredItems;
|
||||
}
|
||||
});
|
||||
}
|
||||
let chartDataArray = [];
|
||||
|
||||
// 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: []
|
||||
};
|
||||
// Process data into flat array format
|
||||
Object.entries(data).forEach(([dataset, items]) => {
|
||||
if (Array.isArray(items)) {
|
||||
items.forEach(item => {
|
||||
// Only process items that match the current hierarchical level
|
||||
const isParentMatch = enableHierarchicalView ?
|
||||
(item.parent === selectedParentId) :
|
||||
(!item.parent || item.parent === null);
|
||||
|
||||
// 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
|
||||
if (isParentMatch) {
|
||||
const value = parseFloat(item.value);
|
||||
if (!isNaN(value)) { // Only add items with valid numeric values
|
||||
chartDataArray.push({
|
||||
dataset,
|
||||
label: item.label || '',
|
||||
value: value,
|
||||
color: item.color || defaultBarColor,
|
||||
id: item.id,
|
||||
popoverHtml: item.popoverHtml || '',
|
||||
hasChildren: Array.isArray(data[dataset]) &&
|
||||
data[dataset].some(child => child.parent === item.id)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
chartDataArray.push(barData);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get container dimensions
|
||||
const containerWidth = containerRef.current.clientWidth;
|
||||
const containerHeight = typeof height === 'number' ? height : 400;
|
||||
// If no valid data, set a default scale
|
||||
if (chartDataArray.length === 0) {
|
||||
chartDataArray = [{
|
||||
label: 'No Data',
|
||||
value: 0,
|
||||
color: defaultBarColor
|
||||
}];
|
||||
}
|
||||
|
||||
// Get container dimensions from the actual DOM element
|
||||
const containerWidth = containerRef.current.clientWidth;
|
||||
const containerHeight = containerRef.current.clientHeight;
|
||||
|
||||
// Basic dimensions with extra margin for axis labels
|
||||
const margin = {
|
||||
top: title ? 40 : 20,
|
||||
right: 20,
|
||||
bottom: xAxisLabel ? 50 : 30,
|
||||
left: yAxisLabel ? 60 : 40
|
||||
bottom: 60,
|
||||
left: 60
|
||||
};
|
||||
|
||||
const innerWidth = Math.max(containerWidth - margin.left - margin.right, 0);
|
||||
const innerHeight = Math.max(containerHeight - margin.top - margin.bottom, 0);
|
||||
|
||||
// Create SVG
|
||||
const svg = d3.select(svgRef.current)
|
||||
.attr('width', containerWidth)
|
||||
.attr('height', containerHeight)
|
||||
.attr('width', '100%')
|
||||
.attr('height', '100%')
|
||||
.attr('viewBox', `0 0 ${containerWidth} ${containerHeight}`)
|
||||
.attr('preserveAspectRatio', 'xMidYMid meet')
|
||||
.style('background-color', backgroundColor);
|
||||
|
||||
// Add title if present
|
||||
if (title) {
|
||||
svg.append('text')
|
||||
.attr('x', containerWidth / 2)
|
||||
.attr('y', 20)
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('font-size', '16px')
|
||||
.text(title);
|
||||
}
|
||||
|
||||
// Create main group
|
||||
const g = svg.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
// Create scales
|
||||
// Create scales with validated domains
|
||||
const xScale = d3.scaleBand()
|
||||
.domain(chartDataArray.map(d => d.label))
|
||||
.range([0, innerWidth])
|
||||
.padding(0.1);
|
||||
|
||||
// Find the maximum value with validation
|
||||
const maxValue = Math.max(
|
||||
d3.max(chartDataArray, d => d.value) || 0,
|
||||
1 // Ensure we always have a positive scale
|
||||
);
|
||||
|
||||
// Calculate a nice round maximum for the y-axis
|
||||
const yAxisMax = (() => {
|
||||
const magnitude = Math.pow(10, Math.floor(Math.log10(maxValue)));
|
||||
const normalized = maxValue / magnitude;
|
||||
return Math.ceil(normalized) * magnitude || 100; // Fallback to 100 if calculation results in 0
|
||||
})();
|
||||
|
||||
// Create y scale with validated domain
|
||||
const yScale = d3.scaleLinear()
|
||||
.domain([0, d3.max(chartDataArray, d => {
|
||||
if (enableStackedBars) {
|
||||
return d.value + d.children.reduce((sum, child) => sum + child.value, 0);
|
||||
}
|
||||
return d.value;
|
||||
}) || 100])
|
||||
.domain([0, yAxisMax])
|
||||
.range([innerHeight, 0]);
|
||||
|
||||
// Get color for a datapoint based on color source
|
||||
const getColor = (datapoint) => {
|
||||
switch (colorSource) {
|
||||
case 'singleColor':
|
||||
return defaultBarColor;
|
||||
case 'customColors':
|
||||
const customColor = customColors?.find(
|
||||
c => c.dataset === datapoint.dataset && c.label === datapoint.label
|
||||
);
|
||||
return customColor ? customColor.color : datapoint.color;
|
||||
case 'default':
|
||||
default:
|
||||
return datapoint.color;
|
||||
}
|
||||
};
|
||||
// Create y-axis with nice tick values
|
||||
const yAxis = d3.axisLeft(yScale)
|
||||
.tickSize(6)
|
||||
.ticks(10)
|
||||
.tickFormat(d3.format(",d")); // Format numbers with commas for thousands
|
||||
|
||||
// Add clip path
|
||||
const clipPath = svg.append('defs')
|
||||
.append('clipPath')
|
||||
.attr('id', 'clip')
|
||||
.append('rect')
|
||||
.attr('x', 0)
|
||||
.attr('y', 0)
|
||||
.attr('width', innerWidth)
|
||||
.attr('height', innerHeight);
|
||||
|
||||
// Create container for all zoomable content
|
||||
const zoomGroup = g.append('g')
|
||||
.attr('class', 'zoom-group')
|
||||
.attr('clip-path', 'url(#clip)');
|
||||
|
||||
// Add grids (behind bars)
|
||||
if (showGridY) {
|
||||
zoomGroup.append('g')
|
||||
.attr('class', 'grid y-grid')
|
||||
.style('color', yGridColor)
|
||||
.style('stroke-width', yGridWidth)
|
||||
.style('pointer-events', 'none')
|
||||
.call(d3.axisLeft(yScale)
|
||||
.tickSize(-innerWidth)
|
||||
.ticks(yAxis.ticks()[0]) // Use same number of ticks as y-axis
|
||||
.tickFormat(''));
|
||||
}
|
||||
|
||||
// Add X grid
|
||||
if (showGridX) {
|
||||
g.append('g')
|
||||
zoomGroup.append('g')
|
||||
.attr('class', 'grid x-grid')
|
||||
.attr('transform', `translate(0,${innerHeight})`)
|
||||
.style('color', xGridColor)
|
||||
.style('stroke-width', xGridWidth)
|
||||
.style('pointer-events', 'none')
|
||||
.call(d3.axisBottom(xScale)
|
||||
.tickSize(-innerHeight)
|
||||
.tickFormat(''));
|
||||
}
|
||||
|
||||
// Add Y grid
|
||||
if (showGridY) {
|
||||
g.append('g')
|
||||
.attr('class', 'grid y-grid')
|
||||
.style('color', yGridColor)
|
||||
.style('stroke-width', yGridWidth)
|
||||
.call(d3.axisLeft(yScale)
|
||||
.tickSize(-innerWidth)
|
||||
.tickFormat(''));
|
||||
}
|
||||
|
||||
// Add bars
|
||||
const barGroups = g.selectAll('.bar-group')
|
||||
// Create bar groups with validation
|
||||
const barGroups = zoomGroup.selectAll('.bar-group')
|
||||
.data(chartDataArray)
|
||||
.enter()
|
||||
.append('g')
|
||||
.join('g')
|
||||
.attr('class', 'bar-group')
|
||||
.attr('transform', d => `translate(${xScale(d.label)},0)`);
|
||||
|
||||
// Add main bars
|
||||
// Add bars with validation and click handling
|
||||
barGroups.append('rect')
|
||||
.attr('class', 'main-bar')
|
||||
.attr('y', d => yScale(d.value))
|
||||
.attr('class', 'bar')
|
||||
.attr('x', 0)
|
||||
.attr('y', d => isNaN(yScale(d.value)) ? yScale(0) : yScale(d.value))
|
||||
.attr('width', xScale.bandwidth())
|
||||
.attr('height', d => innerHeight - yScale(d.value))
|
||||
.attr('fill', d => getColor(d))
|
||||
.attr('height', d => {
|
||||
const height = innerHeight - (isNaN(yScale(d.value)) ? yScale(0) : yScale(d.value));
|
||||
return Math.max(0, height);
|
||||
})
|
||||
.attr('fill', d => d.color || defaultBarColor)
|
||||
.style('opacity', barOpacity)
|
||||
.style('cursor', d => (d.hasChildren && !enableStackedBars) ? 'pointer' : 'default');
|
||||
.style('cursor', d => {
|
||||
if (enableBarPopovers && d.popoverHtml) return 'pointer';
|
||||
return (d.hasChildren && enableHierarchicalView) ? 'pointer' : 'default';
|
||||
})
|
||||
.on('click', (event, d) => {
|
||||
if (d.hasChildren && enableHierarchicalView) {
|
||||
event.stopPropagation();
|
||||
setAttributes({ selectedParentId: d.id });
|
||||
}
|
||||
});
|
||||
|
||||
// Add stacked child bars if enabled
|
||||
if (enableStackedBars) {
|
||||
@ -242,87 +233,100 @@ const BarGraph = ({
|
||||
});
|
||||
}
|
||||
|
||||
// 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 with validation
|
||||
if (showBarValues) {
|
||||
barGroups.each(function(d) {
|
||||
// Add value for main bar
|
||||
d3.select(this)
|
||||
.append('text')
|
||||
.attr('class', 'bar-value')
|
||||
.attr('x', xScale.bandwidth() / 2)
|
||||
.attr('y', yScale(d.value) - 5)
|
||||
.attr('text-anchor', 'middle')
|
||||
.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;
|
||||
});
|
||||
}
|
||||
});
|
||||
barGroups.append('text')
|
||||
.attr('class', 'bar-value')
|
||||
.attr('x', xScale.bandwidth() / 2)
|
||||
.attr('y', d => {
|
||||
const y = yScale(d.value);
|
||||
return isNaN(y) ? yScale(0) - 5 : y - 5;
|
||||
})
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('fill', '#000')
|
||||
.style('font-size', '12px')
|
||||
.text(d => d3.format(',d')(d.value || 0));
|
||||
}
|
||||
|
||||
// Add X axis
|
||||
g.append('g')
|
||||
// Add X axis with rotated labels
|
||||
const xAxis = g.append('g')
|
||||
.attr('class', 'x-axis')
|
||||
.attr('transform', `translate(0,${innerHeight})`)
|
||||
.call(d3.axisBottom(xScale))
|
||||
.selectAll('text')
|
||||
.call(d3.axisBottom(xScale));
|
||||
|
||||
xAxis.selectAll('text')
|
||||
.style('text-anchor', 'end')
|
||||
.attr('dx', '-.8em')
|
||||
.attr('dy', '.15em')
|
||||
.attr('transform', 'rotate(-45)');
|
||||
|
||||
// Add Y axis
|
||||
g.append('g')
|
||||
.call(d3.axisLeft(yScale));
|
||||
// Add Y axis with nice ticks
|
||||
const yAxisGroup = g.append('g')
|
||||
.attr('class', 'y-axis')
|
||||
.call(yAxis);
|
||||
|
||||
// Add X axis label
|
||||
// Add zoom behavior
|
||||
const zoom = d3.zoom()
|
||||
.scaleExtent([1, 5]) // Minimum scale is 1 (original size)
|
||||
.extent([[0, 0], [innerWidth, innerHeight]])
|
||||
.translateExtent([[0, 0], [innerWidth, innerHeight]])
|
||||
.on('zoom', (event) => {
|
||||
const transform = event.transform;
|
||||
|
||||
// Calculate new scale for bars
|
||||
const newBandwidth = xScale.bandwidth() * transform.k;
|
||||
const newRange = [transform.x, transform.x + innerWidth * transform.k];
|
||||
|
||||
// Update bar positions
|
||||
barGroups.attr('transform', d => {
|
||||
const x = transform.x + (xScale(d.label) * transform.k);
|
||||
return `translate(${x},0)`;
|
||||
});
|
||||
|
||||
// Update bar widths
|
||||
barGroups.selectAll('rect')
|
||||
.attr('width', Math.max(newBandwidth, 1));
|
||||
|
||||
// Update bar value labels size and position
|
||||
if (showBarValues) {
|
||||
barGroups.selectAll('.bar-value')
|
||||
.attr('x', newBandwidth / 2)
|
||||
.style('font-size', `${Math.min(12 * transform.k, 24)}px`); // Scale font size with zoom, max 24px
|
||||
}
|
||||
|
||||
// Update x-axis
|
||||
xAxis.call(d3.axisBottom(xScale)
|
||||
.scale(d3.scaleBand()
|
||||
.domain(chartDataArray.map(d => d.label))
|
||||
.range(newRange)
|
||||
.padding(0.1)));
|
||||
|
||||
// Update x-grid if enabled
|
||||
if (showGridX) {
|
||||
zoomGroup.select('.x-grid')
|
||||
.attr('transform', `translate(0,${innerHeight})`)
|
||||
.call(d3.axisBottom(xScale)
|
||||
.scale(d3.scaleBand()
|
||||
.domain(chartDataArray.map(d => d.label))
|
||||
.range(newRange)
|
||||
.padding(0.1))
|
||||
.tickSize(-innerHeight)
|
||||
.tickFormat(''));
|
||||
}
|
||||
});
|
||||
|
||||
// Only add zoom behavior if enabled
|
||||
if (enableXZoom) {
|
||||
svg.call(zoom)
|
||||
.call(zoom.transform, d3.zoomIdentity); // Reset to initial transform
|
||||
}
|
||||
|
||||
// Add X axis label with proper spacing
|
||||
if (xAxisLabel) {
|
||||
g.append('text')
|
||||
.attr('class', 'x-axis-label')
|
||||
.attr('x', innerWidth / 2)
|
||||
.attr('y', innerHeight + 40)
|
||||
.attr('y', innerHeight + 25)
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('font-size', '12px')
|
||||
.text(xAxisLabel);
|
||||
@ -340,7 +344,43 @@ const BarGraph = ({
|
||||
.text(yAxisLabel);
|
||||
}
|
||||
|
||||
}, [data, width, height, title, showGridX, showGridY, xGridColor, yGridColor, xGridWidth, yGridWidth, showBarValues, xAxisLabel, yAxisLabel, colorSource, defaultBarColor, customColors, barOpacity, enableHierarchicalView, enableStackedBars, selectedParentId]);
|
||||
// Add "Back" button if we're in a child view
|
||||
if (enableHierarchicalView && selectedParentId) {
|
||||
const backButton = svg.append('g')
|
||||
.attr('class', 'back-button')
|
||||
.style('cursor', 'pointer')
|
||||
.on('click', () => {
|
||||
// Find the parent's parent
|
||||
let parentParentId = null;
|
||||
Object.values(data).forEach(items => {
|
||||
if (Array.isArray(items)) {
|
||||
const currentParent = items.find(item => item.id === selectedParentId);
|
||||
if (currentParent) {
|
||||
parentParentId = currentParent.parent;
|
||||
}
|
||||
}
|
||||
});
|
||||
setAttributes({ selectedParentId: parentParentId });
|
||||
});
|
||||
|
||||
backButton.append('rect')
|
||||
.attr('x', 10)
|
||||
.attr('y', 10)
|
||||
.attr('width', 60)
|
||||
.attr('height', 24)
|
||||
.attr('rx', 4)
|
||||
.attr('fill', '#f0f0f0')
|
||||
.attr('stroke', '#ccc');
|
||||
|
||||
backButton.append('text')
|
||||
.attr('x', 40)
|
||||
.attr('y', 26)
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('font-size', '12px')
|
||||
.text('← Back');
|
||||
}
|
||||
|
||||
}, [data, width, height, title, showGridX, showGridY, xGridColor, yGridColor, xGridWidth, yGridWidth, showBarValues, xAxisLabel, yAxisLabel, colorSource, defaultBarColor, customColors, barOpacity, enableHierarchicalView, enableStackedBars, selectedParentId, enableBarPopovers, enableXZoom]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -349,11 +389,34 @@ const BarGraph = ({
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
backgroundColor,
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<svg ref={svgRef}></svg>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
></svg>
|
||||
{hoveredBar && (
|
||||
<div
|
||||
className="bar-popover"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: hoveredBar.position.x,
|
||||
top: hoveredBar.position.y,
|
||||
transform: 'translate(-50%, -100%)',
|
||||
backgroundColor: 'white',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
||||
zIndex: 1000,
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: hoveredBar.data.popoverHtml }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -54,7 +54,10 @@ export default function Edit({ attributes, setAttributes }) {
|
||||
chartCustomColors,
|
||||
enableHierarchicalView,
|
||||
enableStackedBars,
|
||||
selectedParentId } = attributes;
|
||||
selectedParentId,
|
||||
enableBarPopovers,
|
||||
enableXZoom
|
||||
} = attributes;
|
||||
|
||||
const blockProps = useBlockProps();
|
||||
|
||||
@ -273,6 +276,12 @@ export default function Edit({ attributes, setAttributes }) {
|
||||
label={__('Chart Height', 'lcp')}
|
||||
value={chartHeight}
|
||||
onChange={(value) => setAttributes({ chartHeight: value })}
|
||||
/>
|
||||
<ToggleControl
|
||||
label={__('Enable X-Axis Zoom', 'lcp-data-blocks')}
|
||||
checked={enableXZoom}
|
||||
onChange={(value) => setAttributes({ enableXZoom: value })}
|
||||
help={__('Allow zooming and panning along the X-axis', 'lcp-data-blocks')}
|
||||
/>
|
||||
</PanelBody>
|
||||
<PanelBody title="Appearance">
|
||||
@ -429,7 +438,12 @@ export default function Edit({ attributes, setAttributes }) {
|
||||
if (tab.name === 'appearance') {
|
||||
return (
|
||||
<PanelBody>
|
||||
|
||||
<ToggleControl
|
||||
label={__('Enable Bar Popovers', 'lcp-data-blocks')}
|
||||
checked={enableBarPopovers}
|
||||
onChange={(value) => setAttributes({ enableBarPopovers: value })}
|
||||
help={__('Show HTML content when hovering over bars', 'lcp-data-blocks')}
|
||||
/>
|
||||
</PanelBody>
|
||||
);
|
||||
}
|
||||
@ -481,30 +495,58 @@ export default function Edit({ attributes, setAttributes }) {
|
||||
)}
|
||||
|
||||
{chartData && Object.keys(chartData).length > 0 ? (
|
||||
<BarGraph
|
||||
data={chartData}
|
||||
height={chartHeight}
|
||||
width={chartWidth}
|
||||
backgroundColor={backgroundColor}
|
||||
defaultBarColor={barColor}
|
||||
barOpacity={barOpacity}
|
||||
showGridX={showGridX}
|
||||
showGridY={showGridY}
|
||||
yGridColor={yGridColor}
|
||||
xGridColor={xGridColor}
|
||||
xGridWidth={xGridWidth}
|
||||
yGridWidth={yGridWidth}
|
||||
title={displayChartTitle ? chartTitle : ''}
|
||||
showBarValues={showBarValues}
|
||||
xAxisLabel={xAxisLabel}
|
||||
yAxisLabel={yAxisLabel}
|
||||
colorSource={chartColorSource}
|
||||
customColors={chartCustomColors}
|
||||
enableHierarchicalView={enableHierarchicalView}
|
||||
enableStackedBars={enableStackedBars}
|
||||
selectedParentId={selectedParentId}
|
||||
setAttributes={setAttributes}
|
||||
/>
|
||||
<>
|
||||
{selectedParentId && enableHierarchicalView && (
|
||||
<button
|
||||
onClick={() => setAttributes({ selectedParentId: null })}
|
||||
style={{
|
||||
marginBottom: '10px',
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#007cba',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
fontSize: '14px',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}}
|
||||
onMouseEnter={e => e.target.style.backgroundColor = '#006ba1'}
|
||||
onMouseLeave={e => e.target.style.backgroundColor = '#007cba'}
|
||||
>
|
||||
<span style={{ fontSize: '18px', lineHeight: 1 }}>←</span>
|
||||
Back to Parent View
|
||||
</button>
|
||||
)}
|
||||
<BarGraph
|
||||
data={chartData}
|
||||
width={chartWidth}
|
||||
height={chartHeight}
|
||||
enableXZoom={enableXZoom}
|
||||
backgroundColor={backgroundColor}
|
||||
defaultBarColor={barColor}
|
||||
barOpacity={barOpacity}
|
||||
showGridX={showGridX}
|
||||
showGridY={showGridY}
|
||||
yGridColor={yGridColor}
|
||||
xGridColor={xGridColor}
|
||||
xGridWidth={xGridWidth}
|
||||
yGridWidth={yGridWidth}
|
||||
title={displayChartTitle ? chartTitle : ''}
|
||||
showBarValues={showBarValues}
|
||||
xAxisLabel={xAxisLabel}
|
||||
yAxisLabel={yAxisLabel}
|
||||
colorSource={chartColorSource}
|
||||
customColors={chartCustomColors}
|
||||
enableHierarchicalView={enableHierarchicalView}
|
||||
enableStackedBars={enableStackedBars}
|
||||
selectedParentId={selectedParentId}
|
||||
enableBarPopovers={enableBarPopovers}
|
||||
setAttributes={setAttributes}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="lcp-bar-graph-placeholder"
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
Card,
|
||||
CardBody,
|
||||
Popover,
|
||||
TextareaControl
|
||||
} from '@wordpress/components';
|
||||
import { useState, useRef } from '@wordpress/element';
|
||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||
@ -22,6 +23,7 @@ const ItemTypes = {
|
||||
|
||||
const DatasetItem = ({ item, datasetKey, moveItem, updateItem, items, onDelete }) => {
|
||||
const [showColorPicker, setShowColorPicker] = useState(false);
|
||||
const [showHtmlEditor, setShowHtmlEditor] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
@ -133,6 +135,13 @@ const DatasetItem = ({ item, datasetKey, moveItem, updateItem, items, onDelete }
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isSmall
|
||||
onClick={() => setShowHtmlEditor(true)}
|
||||
>
|
||||
{__('HTML', 'lcp-data-blocks')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isDestructive
|
||||
@ -149,6 +158,23 @@ const DatasetItem = ({ item, datasetKey, moveItem, updateItem, items, onDelete }
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
{showHtmlEditor && (
|
||||
<Modal
|
||||
title={__('Edit HTML Content', 'lcp-data-blocks')}
|
||||
onRequestClose={() => setShowHtmlEditor(false)}
|
||||
>
|
||||
<div style={{ padding: '20px' }}>
|
||||
<TextareaControl
|
||||
value={item.popoverHtml || ''}
|
||||
onChange={(content) => {
|
||||
updateItem(datasetKey, item.id, 'popoverHtml', content);
|
||||
}}
|
||||
rows={10}
|
||||
style={{ width: '100%', minHeight: '200px', fontFamily: 'monospace' }}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
{/* Render child items */}
|
||||
{childItems.map(childItem => (
|
||||
<DatasetItem
|
||||
|
||||
105
package-lock.json
generated
105
package-lock.json
generated
@ -5,8 +5,11 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@tinymce/tinymce-react": "^5.1.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1"
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-simple-wysiwyg": "^3.2.0",
|
||||
"tinymce": "^7.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
@ -39,6 +42,20 @@
|
||||
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tinymce/tinymce-react": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-5.1.1.tgz",
|
||||
"integrity": "sha512-DQ0wpvnf/9z8RsOEAmrWZ1DN1PKqcQHfU+DpM3llLze7FHmxVtzuN8O+FYh0oAAF4stzAXwiCIVacfqjMwRieQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.6.2",
|
||||
"tinymce": "^7.0.0 || ^6.0.0 || ^5.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^17.0.1 || ^16.7.0",
|
||||
"react-dom": "^18.0.0 || ^17.0.1 || ^16.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dnd-core": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
|
||||
@ -65,12 +82,53 @@
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
||||
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -114,12 +172,35 @@
|
||||
"dnd-core": "^16.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-simple-wysiwyg": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-simple-wysiwyg/-/react-simple-wysiwyg-3.2.0.tgz",
|
||||
"integrity": "sha512-Iaqoe3GxnDKtx5otvM1TnYh7GrP+VxysI6QuqKwnbhkqSmo2MPm0xbZYsmNRmO7w4S5AyVlqprRu1MEImqj2sw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
@ -134,6 +215,22 @@
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinymce": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tinymce/-/tinymce-7.6.0.tgz",
|
||||
"integrity": "sha512-kUrklnD7H8JbpSDEGRh51GKK6Mrf+pR9neSDzUHvXKV+2oRtMB7sqfAtEOnM0/WKdstwaX0qoNCZNo2H1Y0EFA==",
|
||||
"license": "GPL-2.0-or-later"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@tinymce/tinymce-react": "^5.1.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1"
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-simple-wysiwyg": "^3.2.0",
|
||||
"tinymce": "^7.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user