Added basics of LCPLegend and more controls for blocks. Added boilerplate for lcp/line-graph
This commit is contained in:
@ -1,11 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Icon, PanelBody, ToggleControl } from '@wordpress/components';
|
||||
import { Button, Icon, PanelBody, ToggleControl, TextControl } 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)
|
||||
// Attributes from parent edit.js file
|
||||
const { renderLegend,
|
||||
legendLocation,
|
||||
legendAlignment,
|
||||
@ -18,38 +18,58 @@ const LCPChartBlockSettings = ({ attributes, setAttributes }) => {
|
||||
footerContent,
|
||||
chartTitle,
|
||||
chartSubtitle,
|
||||
toolbarLocation,
|
||||
toolbarAlignment
|
||||
toolbarLocation = 'top',
|
||||
toolbarAlignment,
|
||||
showXAxisLabel,
|
||||
xAxisLabel,
|
||||
showYAxisLabel,
|
||||
yAxisLabel,
|
||||
renderXGrid,
|
||||
renderYGrid,
|
||||
xGridColor,
|
||||
yGridColor,
|
||||
legendFontSize
|
||||
} = attributes;
|
||||
|
||||
// Set local state for legend location (for UI updates)
|
||||
// Set local state for legend location and alignment (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
|
||||
// Update block attributes when location or alignment changes
|
||||
useEffect(() => {
|
||||
setAttributes({ legendAlignment: selectedLegendAlignment });
|
||||
}, [selectedLegendAlignment, setAttributes]);
|
||||
setAttributes({ legendLocation: selectedLegendLocation, legendAlignment: selectedLegendAlignment });
|
||||
}, [selectedLegendLocation, selectedLegendAlignment, setAttributes]);
|
||||
|
||||
// Set local state for alignment (for UI updates)
|
||||
// Set local state for toolbar location and alignment (for UI updates)
|
||||
const [selectedToolbarLocation, setSelectedToolbarLocation] = useState(toolbarLocation);
|
||||
const [selectedToolbarAlignment, setSelectedToolbarAlignment] = useState(toolbarAlignment);
|
||||
|
||||
// Update block attribute when alignment changes
|
||||
// Update block attributes when toolbar location or alignment changes
|
||||
useEffect(() => {
|
||||
setAttributes({ toolbarAlignment: selectedToolbarAlignment });
|
||||
}, [selectedToolbarAlignment, setAttributes]);
|
||||
setAttributes({ toolbarLocation: selectedToolbarLocation, toolbarAlignment: selectedToolbarAlignment });
|
||||
}, [selectedToolbarLocation, selectedToolbarAlignment, setAttributes]);
|
||||
|
||||
|
||||
// Render the component
|
||||
return (
|
||||
<div>
|
||||
{/* Grid Settings Panel */}
|
||||
<PanelBody title={__('Grid', 'lcp')} initialOpen={false}>
|
||||
{/* Render X-Grid */}
|
||||
<ToggleControl
|
||||
label={__('Show X Grid', 'lcp')}
|
||||
checked={renderXGrid}
|
||||
onChange={(value) =>
|
||||
setAttributes({ renderXGrid: value })}
|
||||
/>
|
||||
{/* Render Y-Grid */}
|
||||
<ToggleControl
|
||||
label={__('Show Y Grid', 'lcp')}
|
||||
checked={renderYGrid}
|
||||
onChange={(value) =>
|
||||
setAttributes({ renderYGrid: value })}
|
||||
/>
|
||||
</PanelBody>
|
||||
{/* Legend Settings Panel */}
|
||||
<PanelBody title={__('Legend', 'lcp')} initialOpen={false}>
|
||||
<ToggleControl
|
||||
@ -103,15 +123,72 @@ const LCPChartBlockSettings = ({ attributes, setAttributes }) => {
|
||||
>
|
||||
{__('Bottom', 'lcp')}
|
||||
</Button>
|
||||
<LCPDimensionControl
|
||||
unitTypes={['px']}
|
||||
label={__('Legend Font Size', 'lcp')}
|
||||
value={legendFontSize}
|
||||
onChange={(value) => setAttributes({ legendFontSize: value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</PanelBody>
|
||||
|
||||
{/* Labels Settings Panel */}
|
||||
<PanelBody title={__('Labels', 'lcp')} initialOpen={false}>
|
||||
{/* Show X-Axis Label*/}
|
||||
<ToggleControl
|
||||
label={__('Show X-Axis', 'lcp')}
|
||||
checked={showXAxisLabel}
|
||||
onChange={(value) => setAttributes({ showXAxisLabel: value })}
|
||||
/>
|
||||
{/* X-Axis Label */}
|
||||
<TextControl
|
||||
label={__('X Axis Label', 'lcp')}
|
||||
value={xAxisLabel}
|
||||
onChange={(value) => setAttributes({ xAxisLabel: value })}
|
||||
placeholder={__('Enter X-axis label...', 'lcp')}
|
||||
/>
|
||||
{/* Show Y-Axis */}
|
||||
<ToggleControl
|
||||
label={__('Show Y-Axis Label', 'lcp')}
|
||||
checked={showYAxisLabel}
|
||||
onChange={(value) => setAttributes({ showYAxisLabel: value })}
|
||||
/>
|
||||
{/* Y-Axis Label */}
|
||||
<TextControl
|
||||
label={__('Y Axis Label', 'lcp')}
|
||||
value={yAxisLabel}
|
||||
onChange={(value) => setAttributes({ xAxisLabel: value })}
|
||||
placeholder={__('Enter X-axis label', 'lcp')}
|
||||
/>
|
||||
</PanelBody>
|
||||
{/* Controls Settings Panel */}
|
||||
<PanelBody title={__('Controls', 'lcp')} initialOpen={false}>
|
||||
<div>
|
||||
{/* Top Location Button */}
|
||||
<Button
|
||||
isPrimary={selectedToolbarLocation === 'top'}
|
||||
onClick={() => setSelectedToolbarLocation('top')}
|
||||
>
|
||||
{__('Top', 'lcp')}
|
||||
</Button>
|
||||
|
||||
{/* Bottom Location Button */}
|
||||
<Button
|
||||
isPrimary={selectedToolbarLocation === 'bottom'}
|
||||
onClick={() => setSelectedToolbarLocation('bottom')}
|
||||
>
|
||||
{__('Bottom', 'lcp')}
|
||||
</Button>
|
||||
<LCPDimensionControl
|
||||
unitTypes={['px']}
|
||||
label={__('Legend Font Size', 'lcp')}
|
||||
value={legendFontSize}
|
||||
onChange={(value) => setAttributes({ legendFontSize: value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{/* Left Alignment Button */}
|
||||
<Button
|
||||
@ -172,6 +249,7 @@ const LCPChartBlockSettings = ({ attributes, setAttributes }) => {
|
||||
onChange={(value) => setAttributes({ allowDownloadJson: value })}
|
||||
/>
|
||||
</PanelBody>
|
||||
|
||||
{/* Tooltips and Popups Panel */}
|
||||
<PanelBody title={__('Tooltips and Popups', 'lcp')} initialOpen={false}>
|
||||
<LCPHTMLModal />
|
||||
|
||||
@ -379,37 +379,51 @@ const LCPDataGrid = ({
|
||||
event.preventDefault();
|
||||
const target = event.target;
|
||||
const rowCell = target.closest('.ag-cell');
|
||||
const headerCell = target.closest('.ag-header-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()
|
||||
});
|
||||
if (rowCell) {
|
||||
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: event.clientY,
|
||||
left: event.clientX,
|
||||
},
|
||||
rowIndex: rowNode.rowIndex,
|
||||
totalRows: gridRef.current.api.getDisplayedRowCount()
|
||||
});
|
||||
} else if (headerCell) {
|
||||
const rect = headerCell.getBoundingClientRect();
|
||||
const colId = headerCell.getAttribute('col-id');
|
||||
if (!colId) return;
|
||||
|
||||
setContextMenu({
|
||||
type: 'column',
|
||||
position: {
|
||||
top: event.clientY,
|
||||
left: event.clientX,
|
||||
},
|
||||
colId: colId
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleRowAction = useCallback((action, rowIndex) => {
|
||||
const handleRowAction = useCallback((action) => {
|
||||
const newData = [...chartData];
|
||||
const rowIndex = contextMenu.rowIndex;
|
||||
|
||||
// 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
|
||||
if (col === 'ID') return;
|
||||
const sampleValue = chartData.find(item => item[col] !== undefined)?.[col];
|
||||
|
||||
switch (typeof sampleValue) {
|
||||
@ -427,19 +441,59 @@ const LCPDataGrid = ({
|
||||
|
||||
switch (action) {
|
||||
case 'add-above':
|
||||
newData.splice(rowIndex - 1, 0, emptyRow);
|
||||
break;
|
||||
case 'add-below':
|
||||
newData.splice(rowIndex, 0, emptyRow);
|
||||
break;
|
||||
case 'add-below':
|
||||
newData.splice(rowIndex + 1, 0, emptyRow);
|
||||
break;
|
||||
case 'delete':
|
||||
newData.splice(rowIndex - 1, 1);
|
||||
newData.splice(rowIndex, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
onDataChange(newData);
|
||||
setContextMenu(null);
|
||||
}, [chartData, onDataChange]);
|
||||
}, [chartData, onDataChange, contextMenu]);
|
||||
|
||||
const handleColumnAction = useCallback((action) => {
|
||||
if (!contextMenu || !contextMenu.colId) return;
|
||||
|
||||
const newData = [...chartData];
|
||||
const colId = contextMenu.colId;
|
||||
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
// Remove the column from all rows
|
||||
newData.forEach(row => {
|
||||
const { [colId]: removed, ...rest } = row;
|
||||
Object.assign(row, rest);
|
||||
});
|
||||
break;
|
||||
case 'add-left':
|
||||
case 'add-right':
|
||||
// Get all existing column names
|
||||
const existingColumns = Object.keys(chartData[0] || {});
|
||||
|
||||
// Find the next available column number
|
||||
let nextNumber = 1;
|
||||
while (existingColumns.includes(`Column${nextNumber}`)) {
|
||||
nextNumber++;
|
||||
}
|
||||
|
||||
const newColId = `Column${nextNumber}`;
|
||||
const insertIndex = columnDefs.findIndex(col => col.field === colId);
|
||||
const finalIndex = action === 'add-right' ? insertIndex + 1 : insertIndex;
|
||||
|
||||
// Add the new column to all rows
|
||||
newData.forEach(row => {
|
||||
row[newColId] = '';
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
onDataChange(newData);
|
||||
setContextMenu(null);
|
||||
}, [chartData, onDataChange, contextMenu, columnDefs]);
|
||||
|
||||
const handleCellValueChanged = useCallback((params) => {
|
||||
if (!params.data || !params.data.ID) return;
|
||||
@ -451,8 +505,15 @@ const LCPDataGrid = ({
|
||||
onDataChange(newData);
|
||||
}, [chartData, onDataChange]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="ag-theme-alpine" style={{ height: '400px', width: '100%' }}>
|
||||
<div
|
||||
className="ag-theme-alpine"
|
||||
style={{ height: '400px', width: '100%' }}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
|
||||
<AgGridReact
|
||||
ref={gridRef}
|
||||
rowData={rowData}
|
||||
@ -472,24 +533,57 @@ const LCPDataGrid = ({
|
||||
position="bottom left"
|
||||
onClose={() => setContextMenu(null)}
|
||||
anchorRect={{
|
||||
top: contextMenu.mouseEvent.clientY,
|
||||
left: contextMenu.mouseEvent.clientX,
|
||||
top: contextMenu.position.top,
|
||||
left: contextMenu.position.left,
|
||||
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 style={{ padding: '8px', display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
{contextMenu.type === 'row' && (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleRowAction('add-above')}
|
||||
>
|
||||
{__('Add Row Above', 'lcp-visualize')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleRowAction('add-below')}
|
||||
>
|
||||
{__('Add Row Below', 'lcp-visualize')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleRowAction('delete')}
|
||||
>
|
||||
{__('Delete Row', 'lcp-visualize')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{contextMenu.type === 'column' && (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleColumnAction('add-left')}
|
||||
>
|
||||
{__('Add Column Left', 'lcp-visualize')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleColumnAction('add-right')}
|
||||
>
|
||||
{__('Add Column Right', 'lcp-visualize')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleColumnAction('delete')}
|
||||
>
|
||||
{__('Delete Column', 'lcp-visualize')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
92
blocks/components/LCPDataUploader.js
Normal file
92
blocks/components/LCPDataUploader.js
Normal file
@ -0,0 +1,92 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
const LCPDataUploader = ({ onJsonDataUpdate }) => {
|
||||
const [error, setError] = useState(null);
|
||||
const [csvFile, setCsvFile] = useState(null);
|
||||
|
||||
const handleFileUpload = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
setCsvFile(file);
|
||||
|
||||
// Clear any previous errors
|
||||
setError(null);
|
||||
|
||||
// Log the file info for debugging
|
||||
console.log('Selected file:', file);
|
||||
|
||||
// Parse the CSV using PapaParse
|
||||
Papa.parse(file, {
|
||||
complete: (result) => {
|
||||
console.log('CSV parsing result:', result); // Log the result object
|
||||
try {
|
||||
const jsonData = convertCsvToJson(result.data);
|
||||
console.log('Converted JSON:', jsonData); // Log the converted JSON data
|
||||
onJsonDataUpdate(jsonData); // Pass the parsed JSON data to the parent component
|
||||
} catch (parseError) {
|
||||
console.error('Error during CSV conversion:', parseError);
|
||||
setError('Error during CSV conversion');
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error parsing CSV:', error);
|
||||
setError('Error parsing CSV file');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to convert CSV rows into JSON format
|
||||
const convertCsvToJson = (csvData) => {
|
||||
console.log('Converting CSV to JSON. Raw CSV data:', csvData);
|
||||
|
||||
// Get the headers from the first row of the CSV data
|
||||
const headers = csvData[0];
|
||||
|
||||
// Define a new array to hold the processed JSON
|
||||
const jsonData = [];
|
||||
|
||||
// Process the rows (skip the first row as it contains headers)
|
||||
csvData.slice(1).forEach((row, index) => {
|
||||
const obj = {};
|
||||
|
||||
// Map each column in the row to an object key based on the headers
|
||||
headers.forEach((header, headerIndex) => {
|
||||
obj[header] = row[headerIndex] || ''; // Set the value, default to an empty string if not available
|
||||
});
|
||||
|
||||
// Add a unique ID to each row (optional, just to ensure uniqueness)
|
||||
obj.ID = `lcpDatapoint-${index}`;
|
||||
|
||||
console.log('Processed row:', obj); // Log each row's conversion
|
||||
jsonData.push(obj);
|
||||
});
|
||||
|
||||
console.log('Final JSON Data:', jsonData);
|
||||
return jsonData;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button isPrimary onClick={() => document.getElementById('csv-upload-input').click()}>
|
||||
{__('Upload CSV', 'lcp')}
|
||||
</Button>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="csv-upload-input"
|
||||
style={{ display: 'none' }}
|
||||
accept=".csv"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
{csvFile && <p>{__('File selected: ', 'lcp')}{csvFile.name}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LCPDataUploader;
|
||||
@ -1,12 +1,19 @@
|
||||
import { useState, useEffect, useMemo } from '@wordpress/element';
|
||||
import { useState, useEffect, useMemo, useCallback } from '@wordpress/element';
|
||||
import { 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([]);
|
||||
|
||||
// Function to update chartData attribute with parsed JSON data from CSV
|
||||
const handleJsonDataUpdate = (newData) => {
|
||||
console.log('Received new data from CSV:', newData);
|
||||
setAttributes({ chartData: newData });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (chartData && chartData.length > 0) {
|
||||
const columns = Object.keys(chartData[0]).filter(key => key !== 'lcpId');
|
||||
@ -42,9 +49,11 @@ const LCPDatasetBuilder = ({ chartData = [], onChange, attributes, setAttributes
|
||||
onChange(newData);
|
||||
};
|
||||
|
||||
const downloadCSV = () => {
|
||||
// Get all columns except lcpId
|
||||
const columns = getAvailableColumns().map(col => col.value);
|
||||
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(',');
|
||||
@ -53,14 +62,18 @@ const LCPDatasetBuilder = ({ chartData = [], onChange, attributes, setAttributes
|
||||
const rows = chartData.map(row =>
|
||||
columns.map(col => {
|
||||
let value = row[col] || '';
|
||||
if (typeof value === 'string' && (value.includes(',') || value.includes('"'))) {
|
||||
// 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');
|
||||
@ -70,7 +83,7 @@ const LCPDatasetBuilder = ({ chartData = [], onChange, attributes, setAttributes
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
}, [chartData]);
|
||||
|
||||
// Get default value for valueColumn
|
||||
const defaultValueColumn = useMemo(() => {
|
||||
@ -163,6 +176,7 @@ const LCPDatasetBuilder = ({ chartData = [], onChange, attributes, setAttributes
|
||||
options={options}
|
||||
onChange={(value) => setAttributes({ labelsColumn: value })}
|
||||
/>
|
||||
<LCPDataUploader onJsonDataUpdate={handleJsonDataUpdate} />
|
||||
<SelectControl
|
||||
label={__('Color Column', 'lcp-visualize')}
|
||||
help={__('Column containing bar colors (optional)', 'lcp-visualize')}
|
||||
|
||||
117
blocks/components/LCPLegend.js
Normal file
117
blocks/components/LCPLegend.js
Normal file
@ -0,0 +1,117 @@
|
||||
import { useEffect, useRef, useState } from '@wordpress/element';
|
||||
import * as d3 from 'd3';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const LCPLegend = ({
|
||||
items = [],
|
||||
itemSize = 20,
|
||||
spacing = 5,
|
||||
startX = 20,
|
||||
startY = 20,
|
||||
fontFamily = 'Arial',
|
||||
fontSize = 14
|
||||
}) => {
|
||||
const svgRef = useRef(null);
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
|
||||
// Update the container's width when it changes
|
||||
useEffect(() => {
|
||||
if (svgRef.current) {
|
||||
setContainerWidth(svgRef.current.clientWidth);
|
||||
}
|
||||
}, [svgRef.current]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!items.length || !svgRef.current || containerWidth === 0) return;
|
||||
|
||||
// Clear any existing content
|
||||
d3.select(svgRef.current).selectAll("*").remove();
|
||||
|
||||
// Variables for item dimensions and row management
|
||||
const rowHeight = itemSize + spacing;
|
||||
let currentX = 0;
|
||||
let currentY = 0;
|
||||
|
||||
// Calculate the maximum number of items that can fit per row
|
||||
const itemWidth = itemSize + spacing + fontSize * 0.6; // Item width (rect + label)
|
||||
const itemsPerRow = Math.floor(containerWidth / itemWidth); // Items per row based on container width
|
||||
|
||||
// Create the SVG container
|
||||
const svg = d3.select(svgRef.current)
|
||||
.attr('width', '100%')
|
||||
.style('height', null) // Let height be dynamic
|
||||
.style('overflow', 'visible');
|
||||
|
||||
// Create the legend items group
|
||||
const legend = svg.append('g')
|
||||
.attr('class', 'legend-group')
|
||||
.attr('transform', `translate(${startX}, ${startY})`);
|
||||
|
||||
// Loop over items to position them
|
||||
items.forEach((item, i) => {
|
||||
// Calculate the width of each item (rect + label)
|
||||
const labelWidth = item.Label.length * fontSize * 0.6; // Approximate label width
|
||||
const itemWidth = itemSize + spacing + labelWidth; // Total width of the item (rect + label)
|
||||
|
||||
// If the item doesn't fit in the current row, move to the next row
|
||||
if (i % itemsPerRow === 0 && i !== 0) {
|
||||
currentX = 0; // Reset X position
|
||||
currentY += rowHeight; // Move down to next row
|
||||
}
|
||||
|
||||
// Create the rectangle
|
||||
legend.append('rect')
|
||||
.attr('x', currentX)
|
||||
.attr('y', currentY)
|
||||
.attr('width', itemSize)
|
||||
.attr('height', itemSize)
|
||||
.style('fill', item.color || '#cccccc');
|
||||
|
||||
// Create the label
|
||||
legend.append('text')
|
||||
.attr('x', currentX + itemSize + spacing)
|
||||
.attr('y', currentY + itemSize / 2)
|
||||
.text(item.Label || '')
|
||||
.style('font-family', fontFamily)
|
||||
.style('font-size', `${fontSize}px`)
|
||||
.style('dominant-baseline', 'middle')
|
||||
.style('fill', '#333333');
|
||||
|
||||
// Update the current X position for the next item
|
||||
currentX += itemWidth;
|
||||
});
|
||||
|
||||
// Calculate the height of the SVG element based on the number of rows
|
||||
const totalRows = Math.ceil(items.length / itemsPerRow);
|
||||
const calculatedHeight = totalRows * rowHeight + startY * 2;
|
||||
|
||||
// Set the correct height based on totalRows
|
||||
svg.attr('height', calculatedHeight);
|
||||
|
||||
}, [items, containerWidth, itemSize, spacing, startX, startY, fontFamily, fontSize]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={svgRef}
|
||||
className="lcp-legend"
|
||||
style={{
|
||||
width: '100%' // Ensure width is 100% of the parent container
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
LCPLegend.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
Label: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired
|
||||
})),
|
||||
itemSize: PropTypes.number,
|
||||
spacing: PropTypes.number,
|
||||
startX: PropTypes.number,
|
||||
startY: PropTypes.number,
|
||||
fontFamily: PropTypes.string,
|
||||
fontSize: PropTypes.number
|
||||
};
|
||||
|
||||
export default LCPLegend;
|
||||
Reference in New Issue
Block a user