Skip to content

Commit 331a327

Browse files
authored
Merge pull request #3 from node-red/feature/comparison-mode
feat: add comparison mode for side-by-side segment analysis
2 parents eb3abb2 + 6172d68 commit 331a327

16 files changed

Lines changed: 1571 additions & 1027 deletions

public/node_red_survey.duckdb

512 KB
Binary file not shown.

src/App.jsx

Lines changed: 692 additions & 950 deletions
Large diffs are not rendered by default.

src/components/ChartHeader.jsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { useState, useCallback, memo } from 'react';
22
import {
33
serializeFiltersToURL,
44
getFullURLWithFiltersState,
5+
getFullURLWithComparisonState,
56
generateSectionId,
67
} from '../utils/url-utils';
7-
import { useFilters } from '../contexts/FilterContext';
8+
import { useFilters, useComparison } from '../contexts/FilterContext';
89
import Tooltip from './Tooltip';
910
import { getTooltipPosition } from '../utils/tooltip-utils';
1011

@@ -24,6 +25,8 @@ const ChartHeader = ({ title, compact = false, className }) => {
2425

2526
// Get filters from context - this is the source of truth
2627
const filters = useFilters();
28+
// Get comparison state from context
29+
const { comparisonMode, filtersA, filtersB } = useComparison();
2730
const sectionId = generateSectionId(title);
2831

2932
const handleAnchorClick = useCallback((e) => {
@@ -32,19 +35,30 @@ const ChartHeader = ({ title, compact = false, className }) => {
3235

3336
if (!sectionId) return;
3437

38+
let newHash, fullUrl;
39+
40+
if (comparisonMode) {
41+
// Comparison mode - include both filter sets
42+
const comparisonState = { comparisonMode: true, filtersA, filtersB };
43+
newHash = serializeFiltersToURL(null, sectionId, comparisonState);
44+
fullUrl = getFullURLWithComparisonState(comparisonState, sectionId);
45+
} else {
46+
// Normal mode
47+
newHash = serializeFiltersToURL(filters, sectionId);
48+
fullUrl = getFullURLWithFiltersState(filters, sectionId);
49+
}
50+
3551
// Build hash from React state (source of truth, not potentially stale URL)
36-
const newHash = serializeFiltersToURL(filters, sectionId);
3752
window.history.pushState(null, '', newHash);
3853

3954
// Copy full URL with current filters from React state
40-
const fullUrl = getFullURLWithFiltersState(filters, sectionId);
4155
navigator.clipboard.writeText(fullUrl).then(() => {
4256
setCopied(true);
4357
setTimeout(() => setCopied(false), 1500);
4458
}).catch(() => {
4559
// Fallback: just update the hash without clipboard notification
4660
});
47-
}, [sectionId, filters]);
61+
}, [sectionId, filters, comparisonMode, filtersA, filtersB]);
4862

4963
const handleMouseEnter = useCallback((e) => {
5064
if (!copied) {

src/components/ChoroplethMap.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ const ChoroplethMap = ({ questionId, questionTitle, filters, _color, wasmService
107107
// Once we have data, keep showing it while loading new filtered data
108108
if (loading && data.length === 0) {
109109
return (
110-
<div className="bg-white rounded-lg shadow-sm border border-gray-300 flex">
110+
<div className="bg-white rounded-[5px] overflow-hidden shadow-sm border border-gray-300 flex">
111111
<div className="flex items-center justify-center w-8 min-w-[32px] text-sm text-gray-600 bg-gray-100 border-r border-gray-300">
112112
<svg
113113
width="20"
@@ -135,7 +135,7 @@ const ChoroplethMap = ({ questionId, questionTitle, filters, _color, wasmService
135135

136136
if (error) {
137137
return (
138-
<div className="bg-white rounded-lg shadow-sm border border-gray-300 flex">
138+
<div className="bg-white rounded-[5px] overflow-hidden shadow-sm border border-gray-300 flex">
139139
<div className="flex items-center justify-center w-8 min-w-[32px] text-sm text-gray-600 bg-gray-100 border-r border-gray-300">
140140
<svg
141141
width="20"
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { memo } from 'react';
2+
3+
/**
4+
* Wrapper component for comparison mode.
5+
* Renders the same chart component twice side-by-side with different filter configurations.
6+
*
7+
* @param {Object} props
8+
* @param {React.ComponentType} props.ChartComponent - The chart component to render
9+
* @param {Object} props.chartProps - Props to pass to the chart component (excluding filters)
10+
* @param {Object} props.filtersA - Filter configuration for column A
11+
* @param {Object} props.filtersB - Filter configuration for column B
12+
*/
13+
const ComparisonChartWrapper = (props) => {
14+
const {
15+
ChartComponent,
16+
chartProps,
17+
filtersA,
18+
filtersB,
19+
} = props;
20+
21+
return (
22+
<div className="flex gap-4">
23+
{/* Column A */}
24+
<div className="flex-1 min-w-[500px]">
25+
<div className="border-l-2 border-blue-500 pl-3 relative">
26+
<span className="absolute -top-5 -left-px -translate-x-1/2 inline-flex items-center justify-center w-5 h-5 rounded-full bg-blue-500 text-white text-xs font-bold">
27+
A
28+
</span>
29+
<ChartComponent {...chartProps} filters={filtersA} />
30+
</div>
31+
</div>
32+
33+
{/* Column B */}
34+
<div className="flex-1 min-w-[500px]">
35+
<div className="border-l-2 border-orange-500 pl-3 relative">
36+
<span className="absolute -top-5 -left-px -translate-x-1/2 inline-flex items-center justify-center w-5 h-5 rounded-full bg-orange-500 text-white text-xs font-bold">
37+
B
38+
</span>
39+
<ChartComponent {...chartProps} filters={filtersB} />
40+
</div>
41+
</div>
42+
</div>
43+
);
44+
};
45+
46+
export default memo(ComparisonChartWrapper);

0 commit comments

Comments
 (0)