refactored DreamCharts to consolidate chart rendering logic into reusable renderChart function; improved chart resize handling for better responsiveness and maintainability

Signed-off-by: Matthias Puchstein <matthias@puchstein.bayern>
This commit is contained in:
2025-07-17 03:56:10 +02:00
parent 936a2fa1ee
commit 618e5b442e

View File

@@ -16,96 +16,101 @@ export const EEGChart: React.FC<{ chipInput: ChipInput }> = ({chipInput}) => {
{name: 'Delta', values: chipInput.eeg.delta} {name: 'Delta', values: chipInput.eeg.delta}
]; ];
// Clear previous chart // Function to render the chart
d3.select(chartRef.current).selectAll('*').remove(); const renderChart = () => {
if (!chartRef.current) return;
// Clear previous chart
d3.select(chartRef.current).selectAll('*').remove();
const margin = {top: 20, right: 20, bottom: 30, left: 50}; const margin = {top: 20, right: 20, bottom: 30, left: 50};
const width = chartRef.current.clientWidth - margin.left - margin.right; const width = chartRef.current.clientWidth - margin.left - margin.right;
const height = chartRef.current.clientHeight - margin.top - margin.bottom; const height = chartRef.current.clientHeight - margin.top - margin.bottom;
const svg = d3.select(chartRef.current) const svg = d3.select(chartRef.current)
.append('svg') .append('svg')
.attr('width', width + margin.left + margin.right) .attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom) .attr('height', height + margin.top + margin.bottom)
.append('g') .append('g')
.attr('transform', `translate(${margin.left},${margin.top})`); .attr('transform', `translate(${margin.left},${margin.top})`);
// X scale // X scale
const x = d3.scaleLinear() const x = d3.scaleLinear()
.domain([0, chipInput.eeg.alpha.length - 1]) .domain([0, chipInput.eeg.alpha.length - 1])
.range([0, width]); .range([0, width]);
// Y scale // Y scale
const y = d3.scaleLinear() const y = d3.scaleLinear()
.domain([0, d3.max(eegData.flatMap(d => d.values)) || 50]) .domain([0, d3.max(eegData.flatMap(d => d.values)) || 50])
.range([height, 0]); .range([height, 0]);
// Line generator // Line generator
const line = d3.line<number>() const line = d3.line<number>()
.x((_d, i) => x(i)) .x((_d, i) => x(i))
.y(d => y(d)) .y(d => y(d))
.curve(d3.curveMonotoneX); .curve(d3.curveMonotoneX);
// Color scale // Color scale
const color = d3.scaleOrdinal<string>() const color = d3.scaleOrdinal<string>()
.domain(eegData.map(d => d.name)) .domain(eegData.map(d => d.name))
.range(['#8884d8', '#82ca9d', '#ffc658', '#ff8042']); .range(['#8884d8', '#82ca9d', '#ffc658', '#ff8042']);
// Add X axis // Add X axis
svg.append('g') svg.append('g')
.attr('transform', `translate(0,${height})`) .attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x)) .call(d3.axisBottom(x))
.selectAll('text') .selectAll('text')
.style('fill', 'var(--text)'); .style('fill', 'var(--text)');
// Add Y axis // Add Y axis
svg.append('g') svg.append('g')
.call(d3.axisLeft(y)) .call(d3.axisLeft(y))
.selectAll('text') .selectAll('text')
.style('fill', 'var(--text)'); .style('fill', 'var(--text)');
// Add lines // Add lines
eegData.forEach(d => { eegData.forEach(d => {
svg.append('path') svg.append('path')
.datum(d.values) .datum(d.values)
.attr('fill', 'none') .attr('fill', 'none')
.attr('stroke', color(d.name)) .attr('stroke', color(d.name))
.attr('stroke-width', 1.5) .attr('stroke-width', 1.5)
.attr('d', line); .attr('d', line);
}); });
// Add legend at the top right // Add legend at the top right
const legendItemHeight = 20; // Height allocated for each legend item const legendItemHeight = 20; // Height allocated for each legend item
const legend = svg.append('g') const legend = svg.append('g')
.attr('font-family', 'sans-serif') .attr('font-family', 'sans-serif')
.attr('font-size', 10) .attr('font-size', 10)
.attr('text-anchor', 'end') .attr('text-anchor', 'end')
.selectAll('g') .selectAll('g')
.data(eegData) .data(eegData)
.enter().append('g') .enter().append('g')
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`); .attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
legend.append('rect') legend.append('rect')
.attr('x', -15) .attr('x', -15)
.attr('width', 15) .attr('width', 15)
.attr('height', 15) .attr('height', 15)
.attr('fill', d => color(d.name)); .attr('fill', d => color(d.name));
legend.append('text') legend.append('text')
.attr('x', -20) .attr('x', -20)
.attr('y', 7.5) .attr('y', 7.5)
.attr('dy', '0.32em') .attr('dy', '0.32em')
.style('fill', 'var(--text)') .style('fill', 'var(--text)')
.text(d => d.name); .text(d => d.name);
};
// Initial render
renderChart();
// Handle resize // Handle resize
const handleResize = () => { const handleResize = () => {
if (!chartRef.current) return; if (!chartRef.current) return;
renderChart();
// Re-render chart on resize
d3.select(chartRef.current).selectAll('*').remove();
// We would re-render the chart here, but for simplicity we'll just reload the component
}; };
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
@@ -127,98 +132,103 @@ export const VitalsChart: React.FC<{ chipInput: ChipInput }> = ({chipInput}) =>
{name: 'HRV', values: chipInput.hrv} {name: 'HRV', values: chipInput.hrv}
]; ];
// Clear previous chart // Function to render the chart
d3.select(chartRef.current).selectAll('*').remove(); const renderChart = () => {
if (!chartRef.current) return;
// Clear previous chart
d3.select(chartRef.current).selectAll('*').remove();
const margin = {top: 20, right: 20, bottom: 30, left: 50}; const margin = {top: 20, right: 20, bottom: 30, left: 50};
const width = chartRef.current.clientWidth - margin.left - margin.right; const width = chartRef.current.clientWidth - margin.left - margin.right;
const height = chartRef.current.clientHeight - margin.top - margin.bottom; const height = chartRef.current.clientHeight - margin.top - margin.bottom;
const svg = d3.select(chartRef.current) const svg = d3.select(chartRef.current)
.append('svg') .append('svg')
.attr('width', width + margin.left + margin.right) .attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom) .attr('height', height + margin.top + margin.bottom)
.append('g') .append('g')
.attr('transform', `translate(${margin.left},${margin.top})`); .attr('transform', `translate(${margin.left},${margin.top})`);
// X scale // X scale
const x = d3.scaleLinear() const x = d3.scaleLinear()
.domain([0, chipInput.puls.length - 1]) .domain([0, chipInput.puls.length - 1])
.range([0, width]); .range([0, width]);
// Y scale // Y scale
const minValue = d3.min(vitalsData.flatMap(d => d.values)) || 0; const minValue = d3.min(vitalsData.flatMap(d => d.values)) || 0;
const maxValue = d3.max(vitalsData.flatMap(d => d.values)) || 100; const maxValue = d3.max(vitalsData.flatMap(d => d.values)) || 100;
const y = d3.scaleLinear() const y = d3.scaleLinear()
.domain([Math.max(0, minValue - 10), maxValue + 10]) .domain([Math.max(0, minValue - 10), maxValue + 10])
.range([height, 0]); .range([height, 0]);
// Line generator // Line generator
const line = d3.line<number>() const line = d3.line<number>()
.x((_d, i) => x(i)) .x((_d, i) => x(i))
.y(d => y(d)) .y(d => y(d))
.curve(d3.curveMonotoneX); .curve(d3.curveMonotoneX);
// Color scale // Color scale
const color = d3.scaleOrdinal<string>() const color = d3.scaleOrdinal<string>()
.domain(vitalsData.map(d => d.name)) .domain(vitalsData.map(d => d.name))
.range(['#ff6b6b', '#48dbfb']); .range(['#ff6b6b', '#48dbfb']);
// Add X axis // Add X axis
svg.append('g') svg.append('g')
.attr('transform', `translate(0,${height})`) .attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x)) .call(d3.axisBottom(x))
.selectAll('text') .selectAll('text')
.style('fill', 'var(--text)'); .style('fill', 'var(--text)');
// Add Y axis // Add Y axis
svg.append('g') svg.append('g')
.call(d3.axisLeft(y)) .call(d3.axisLeft(y))
.selectAll('text') .selectAll('text')
.style('fill', 'var(--text)'); .style('fill', 'var(--text)');
// Add lines // Add lines
vitalsData.forEach(d => { vitalsData.forEach(d => {
svg.append('path') svg.append('path')
.datum(d.values) .datum(d.values)
.attr('fill', 'none') .attr('fill', 'none')
.attr('stroke', color(d.name)) .attr('stroke', color(d.name))
.attr('stroke-width', 1.5) .attr('stroke-width', 1.5)
.attr('d', line); .attr('d', line);
}); });
// Add legend at the top right // Add legend at the top right
const legendItemHeight = 20; // Height allocated for each legend item const legendItemHeight = 20; // Height allocated for each legend item
const legend = svg.append('g') const legend = svg.append('g')
.attr('font-family', 'sans-serif') .attr('font-family', 'sans-serif')
.attr('font-size', 10) .attr('font-size', 10)
.attr('text-anchor', 'end') .attr('text-anchor', 'end')
.selectAll('g') .selectAll('g')
.data(vitalsData) .data(vitalsData)
.enter().append('g') .enter().append('g')
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`); .attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
legend.append('rect') legend.append('rect')
.attr('x', -15) .attr('x', -15)
.attr('width', 15) .attr('width', 15)
.attr('height', 15) .attr('height', 15)
.attr('fill', d => color(d.name)); .attr('fill', d => color(d.name));
legend.append('text') legend.append('text')
.attr('x', -20) .attr('x', -20)
.attr('y', 7.5) .attr('y', 7.5)
.attr('dy', '0.32em') .attr('dy', '0.32em')
.style('fill', 'var(--text)') .style('fill', 'var(--text)')
.text(d => d.name); .text(d => d.name);
};
// Initial render
renderChart();
// Handle resize // Handle resize
const handleResize = () => { const handleResize = () => {
if (!chartRef.current) return; if (!chartRef.current) return;
renderChart();
// Re-render chart on resize
d3.select(chartRef.current).selectAll('*').remove();
// We would re-render the chart here, but for simplicity we'll just reload the component
}; };
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
@@ -239,92 +249,97 @@ export const MovementChart: React.FC<{ chipInput: ChipInput }> = ({chipInput}) =
{name: 'Bewegung', values: chipInput.bewegung.map(v => v * 100)} // Scale movement for better visibility {name: 'Bewegung', values: chipInput.bewegung.map(v => v * 100)} // Scale movement for better visibility
]; ];
// Clear previous chart // Function to render the chart
d3.select(chartRef.current).selectAll('*').remove(); const renderChart = () => {
if (!chartRef.current) return;
// Clear previous chart
d3.select(chartRef.current).selectAll('*').remove();
const margin = {top: 20, right: 20, bottom: 30, left: 50}; const margin = {top: 20, right: 20, bottom: 30, left: 50};
const width = chartRef.current.clientWidth - margin.left - margin.right; const width = chartRef.current.clientWidth - margin.left - margin.right;
const height = chartRef.current.clientHeight - margin.top - margin.bottom; const height = chartRef.current.clientHeight - margin.top - margin.bottom;
const svg = d3.select(chartRef.current) const svg = d3.select(chartRef.current)
.append('svg') .append('svg')
.attr('width', width + margin.left + margin.right) .attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom) .attr('height', height + margin.top + margin.bottom)
.append('g') .append('g')
.attr('transform', `translate(${margin.left},${margin.top})`); .attr('transform', `translate(${margin.left},${margin.top})`);
// X scale // X scale
const x = d3.scaleLinear() const x = d3.scaleLinear()
.domain([0, chipInput.bewegung.length - 1]) .domain([0, chipInput.bewegung.length - 1])
.range([0, width]); .range([0, width]);
// Y scale // Y scale
const y = d3.scaleLinear() const y = d3.scaleLinear()
.domain([0, d3.max(bewegungData[0].values) || 100]) .domain([0, d3.max(bewegungData[0].values) || 100])
.range([height, 0]); .range([height, 0]);
// Line generator // Line generator
const line = d3.line<number>() const line = d3.line<number>()
.x((_d, i) => x(i)) .x((_d, i) => x(i))
.y(d => y(d)) .y(d => y(d))
.curve(d3.curveMonotoneX); .curve(d3.curveMonotoneX);
// Color scale // Color scale
const color = '#1dd1a1'; // Use the same color as before for consistency const color = '#1dd1a1'; // Use the same color as before for consistency
// Add X axis // Add X axis
svg.append('g') svg.append('g')
.attr('transform', `translate(0,${height})`) .attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x)) .call(d3.axisBottom(x))
.selectAll('text') .selectAll('text')
.style('fill', 'var(--text)'); .style('fill', 'var(--text)');
// Add Y axis // Add Y axis
svg.append('g') svg.append('g')
.call(d3.axisLeft(y)) .call(d3.axisLeft(y))
.selectAll('text') .selectAll('text')
.style('fill', 'var(--text)'); .style('fill', 'var(--text)');
// Add line // Add line
svg.append('path') svg.append('path')
.datum(bewegungData[0].values) .datum(bewegungData[0].values)
.attr('fill', 'none') .attr('fill', 'none')
.attr('stroke', color) .attr('stroke', color)
.attr('stroke-width', 1.5) .attr('stroke-width', 1.5)
.attr('d', line); .attr('d', line);
// Add legend at the top right // Add legend at the top right
const legendItemHeight = 20; // Height allocated for each legend item const legendItemHeight = 20; // Height allocated for each legend item
const legend = svg.append('g') const legend = svg.append('g')
.attr('font-family', 'sans-serif') .attr('font-family', 'sans-serif')
.attr('font-size', 10) .attr('font-size', 10)
.attr('text-anchor', 'end') .attr('text-anchor', 'end')
.selectAll('g') .selectAll('g')
.data(bewegungData) .data(bewegungData)
.enter().append('g') .enter().append('g')
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`); .attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
legend.append('rect') legend.append('rect')
.attr('x', -15) .attr('x', -15)
.attr('width', 15) .attr('width', 15)
.attr('height', 15) .attr('height', 15)
.attr('fill', () => color); .attr('fill', () => color);
legend.append('text') legend.append('text')
.attr('x', -20) .attr('x', -20)
.attr('y', 7.5) .attr('y', 7.5)
.attr('dy', '0.32em') .attr('dy', '0.32em')
.style('fill', 'var(--text)') .style('fill', 'var(--text)')
.text(d => d.name); .text(d => d.name);
};
// Initial render
renderChart();
// Handle resize // Handle resize
const handleResize = () => { const handleResize = () => {
if (!chartRef.current) return; if (!chartRef.current) return;
renderChart();
// Re-render chart on resize
d3.select(chartRef.current).selectAll('*').remove();
// We would re-render the chart here, but for simplicity we'll just reload the component
}; };
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);