import { useState, useEffect, useMemo, useCallback } from '@wordpress/element'; import { TextControl, Button, Modal, SelectControl, ToggleControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import LCPDataGrid from './LCPDataGrid'; import LCPDataUploader from './LCPDataUploader'; const LCPDatasetBuilder = ({ chartData = [], onChange, attributes, setAttributes, chartType }) => { const [isOpen, setIsOpen] = useState(false); const [options, setOptions] = useState([]); // List of CSS color names const CSS_COLOR_NAMES = [ 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen' ]; // Helper function to check if a string is a valid hex color const isHexColor = (str) => { return /^#([0-9A-F]{3}){1,2}$/i.test(str) || /^#[0-9A-F]{6}$/i.test(str); }; // Helper function to check if a string is a valid color name const isColorName = (str) => { return CSS_COLOR_NAMES.includes(str.toLowerCase()); }; // Helper function to guess column type based on value const guessColumnType = useCallback((value) => { if (value === null || value === undefined) return 'text'; // Handle numeric values if (typeof value === 'number') return 'number'; if (typeof value === 'string') { const trimmedValue = value.trim(); // Try parsing as number first if (!isNaN(trimmedValue) && !isNaN(parseFloat(trimmedValue))) { return 'number'; } // Check for colors (hex or named) if (isHexColor(trimmedValue) || isColorName(trimmedValue)) { return 'lcpColor'; } // Check for date if (!isNaN(Date.parse(trimmedValue))) { return 'lcpDate'; } // Check for HTML if (/<[a-z][\s\S]*>/i.test(trimmedValue)) { return 'html'; } } return 'text'; }, []); // Function to analyze and update column types const analyzeColumnTypes = useCallback((data) => { if (!data || !data.length) return {}; // Get all unique keys except special columns const keys = Array.from(new Set( data.flatMap(item => Object.keys(item)) )).filter(key => !['ID', 'Parent'].includes(key)); // For each key, collect all values and determine type const newTypes = keys.reduce((acc, key) => { const values = data.map(item => item[key]).filter(v => v != null); // Try to determine the most appropriate type const types = values.map(guessColumnType); // If any value is a number, treat the whole column as number if (types.includes('number')) { acc[key] = 'number'; } else { // Otherwise use the most common type const typeCounts = types.reduce((counts, type) => { counts[type] = (counts[type] || 0) + 1; return counts; }, {}); acc[key] = Object.entries(typeCounts) .sort(([,a], [,b]) => b - a)[0][0]; } return acc; }, {}); return newTypes; }, [guessColumnType]); // Function to update chartData attribute with parsed JSON data from CSV const handleJsonDataUpdate = (newData) => { console.log('Received new data from CSV:', newData); // Update chartData setAttributes({ chartData: newData }); // Analyze and update column types const newTypes = analyzeColumnTypes(newData); setAttributes({ columnTypes: newTypes }); }; // Handle Edit Dataset button click const handleEditClick = useCallback(() => { // Update column types before opening the editor if (chartData?.length > 0) { const newTypes = analyzeColumnTypes(chartData); setAttributes({ columnTypes: newTypes }); } setIsOpen(true); }, [chartData, analyzeColumnTypes, setAttributes]); useEffect(() => { if (chartData && chartData.length > 0) { const columns = getAvailableColumns(); setOptions(columns.map(col => ({ label: col.name, value: col.key }))); } }, [chartData]); // Get available columns from chartData const getAvailableColumns = () => { if (!chartData.length) return []; // Get the first row which contains our column names const columnNames = Object.entries(chartData[0]) .filter(([key]) => !['ID', 'Parent'].includes(key)) .map(([key, value]) => ({ key, name: value })); return columnNames; }; // 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.key]; // Handle both 'number' and 'lcpNumber' for backward compatibility if (typeFilter === 'number') { return columnType === 'number'; } return columnType === typeFilter; }) .map(col => ({ label: col.name, // Use the column name from first row value: col.key // Use the field key for internal reference })); }; const handleDataChange = (newData) => { console.log('DatasetBuilder updating chartData:', newData); onChange(newData); }; const downloadCSV = useCallback(() => { if (!chartData || chartData.length === 0) return; // Get all columns except internal ones const columns = Object.keys(chartData[0]).filter(key => !['ID', 'Parent'].includes(key)); // Create CSV header const header = columns.join(','); // Create CSV rows const rows = chartData.map(row => columns.map(col => { let value = row[col] || ''; // Handle special characters in CSV if (typeof value === 'string' && (value.includes(',') || value.includes('"') || value.includes('\n'))) { value = `"${value.replace(/"/g, '""')}"`; } return value; }).join(',') ).join('\n'); // Combine header and rows const csv = `${header}\n${rows}`; // Create and trigger download 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); }, [chartData]); // 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, 'number'); // 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 (