Skip to content

Commit 8e24356

Browse files
committed
Reorder radar charts and enhance visualization system
- Move Task Category Distribution radar to first position after pie charts - Add Priority Category Distribution as second radar chart - Add Node Category Distribution as third radar chart - Create NodeDistributionRadar component with Indian red (#CD5C5C) connecting lines - Fix color consistency between pie and radar charts (Low: blue, Minimal: green) - Add unique radar colors for each chart type (brown, blue violet, Indian red) - Implement proper GraphQL variable naming to avoid conflicts - Add comprehensive legend systems with icons and task counts - Clean up unused variables and imports in ListView component
1 parent 80cb472 commit 8e24356

5 files changed

Lines changed: 257 additions & 20 deletions

File tree

packages/web/src/components/ListView.tsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import { useQuery } from '@apollo/client';
3434
import { useAuth } from '../contexts/AuthContext';
3535
import { useGraph } from '../contexts/GraphContext';
3636
import { TaskDistributionRadar } from './TaskDistributionRadar';
37+
import { PriorityDistributionRadar } from './PriorityDistributionRadar';
38+
import { NodeDistributionRadar } from './NodeDistributionRadar';
3739
import { GET_WORK_ITEMS } from '../lib/queries';
3840
import { EditNodeModal } from './EditNodeModal';
3941
import { DeleteNodeModal } from './DeleteNodeModal';
@@ -1200,9 +1202,7 @@ export function ListView() {
12001202
const path = createPath(percentage, cumulativePercentage);
12011203

12021204
// Use pre-calculated dynamic positions
1203-
const position = labelPositions[index];
1204-
const labelX = position.x;
1205-
const labelY = position.y;
1205+
// const position = labelPositions[index];
12061206

12071207
cumulativePercentage += percentage;
12081208

@@ -1452,7 +1452,7 @@ export function ListView() {
14521452
<PieChart
14531453
title=""
14541454
data={Object.entries(stats.typeStats)
1455-
.filter(([type, count]) => count > 0)
1455+
.filter(([, count]) => count > 0)
14561456
.map(([type, count]) => ({
14571457
label: formatLabel(type),
14581458
value: count,
@@ -1469,14 +1469,34 @@ export function ListView() {
14691469
</div>
14701470
</div>
14711471

1472+
{/* First Radar Row - Task Distribution Radar */}
1473+
<div className="mt-12">
1474+
<div className="flex flex-col items-center">
1475+
<h2 className="text-2xl font-bold text-white mb-4">Task Category Distribution</h2>
1476+
<div className="w-full">
1477+
<TaskDistributionRadar showLegend={false} />
1478+
</div>
1479+
</div>
1480+
</div>
1481+
1482+
</div>
1483+
1484+
{/* Second Radar Row - Priority Distribution Radar */}
1485+
<div className="mt-12">
1486+
<div className="flex flex-col items-center">
1487+
<h2 className="text-2xl font-bold text-white mb-4">Priority Category Distribution</h2>
1488+
<div className="w-full">
1489+
<PriorityDistributionRadar showLegend={false} />
1490+
</div>
1491+
</div>
14721492
</div>
14731493

1474-
{/* Fourth Row - Task Distribution Radar */}
1494+
{/* Third Radar Row - Node Distribution Radar */}
14751495
<div className="mt-12">
14761496
<div className="flex flex-col items-center">
1477-
<h2 className="text-2xl font-bold text-white mb-4">Task Category Distribution</h2>
1497+
<h2 className="text-2xl font-bold text-white mb-4">Node Category Distribution</h2>
14781498
<div className="w-full">
1479-
<TaskDistributionRadar showLegend={false} />
1499+
<NodeDistributionRadar showLegend={false} />
14801500
</div>
14811501
</div>
14821502
</div>
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { useMemo, useState } from 'react';
2+
import { ZoomIn, ZoomOut, RotateCcw, Layers, Trophy, Target, Sparkles, ListTodo, AlertTriangle, Lightbulb, Microscope } from 'lucide-react';
3+
import { RadarChart } from './RadarChart';
4+
import { useGraph } from '../contexts/GraphContext';
5+
import { useQuery, gql } from '@apollo/client';
6+
7+
const GET_NODE_DISTRIBUTION = gql`
8+
query GetNodeDistribution($graphId: ID) {
9+
workItems(where: { graph: { id: $graphId } }) {
10+
type
11+
}
12+
}
13+
`;
14+
15+
interface NodeDistributionRadarProps {
16+
className?: string;
17+
showLegend?: boolean;
18+
}
19+
20+
export function NodeDistributionRadar({ className = '', showLegend = true }: NodeDistributionRadarProps) {
21+
const { currentGraph } = useGraph();
22+
const [zoomLevel, setZoomLevel] = useState(1);
23+
const { data: queryData, loading, error } = useQuery(GET_NODE_DISTRIBUTION, {
24+
variables: { graphId: currentGraph?.id },
25+
skip: !currentGraph?.id
26+
});
27+
28+
const handleZoomIn = () => {
29+
setZoomLevel(prev => Math.min(prev + 0.1, 2));
30+
};
31+
32+
const handleZoomOut = () => {
33+
setZoomLevel(prev => Math.max(prev - 0.1, 0.5));
34+
};
35+
36+
const handleReset = () => {
37+
setZoomLevel(1);
38+
};
39+
40+
const radarData = useMemo(() => {
41+
if (!queryData?.workItems) return [];
42+
43+
// Count nodes by type
44+
const typeCounts: { [key: string]: number } = {};
45+
46+
queryData.workItems.forEach((item: any) => {
47+
const type = item.type;
48+
typeCounts[type] = (typeCounts[type] || 0) + 1;
49+
});
50+
51+
// Format label function
52+
const formatLabel = (type: string) => {
53+
switch(type) {
54+
case 'EPIC': return 'Epic';
55+
case 'MILESTONE': return 'Milestone';
56+
case 'OUTCOME': return 'Outcome';
57+
case 'FEATURE': return 'Feature';
58+
case 'TASK': return 'Task';
59+
case 'BUG': return 'Bug';
60+
case 'IDEA': return 'Idea';
61+
case 'RESEARCH': return 'Research';
62+
default: return type.charAt(0) + type.slice(1).toLowerCase();
63+
}
64+
};
65+
66+
// Node type colors and order matching the pie chart exactly
67+
const typeOrder = ['EPIC', 'MILESTONE', 'OUTCOME', 'FEATURE', 'TASK', 'BUG', 'IDEA', 'RESEARCH'];
68+
69+
const nodeTypeData = typeOrder
70+
.filter(type => typeCounts[type] > 0)
71+
.map(type => ({
72+
axis: formatLabel(type),
73+
value: typeCounts[type],
74+
color: type === 'EPIC' ? '#c084fc' :
75+
type === 'MILESTONE' ? '#fb923c' :
76+
type === 'OUTCOME' ? '#818cf8' :
77+
type === 'FEATURE' ? '#38bdf8' :
78+
type === 'TASK' ? '#4ade80' :
79+
type === 'BUG' ? '#ef4444' :
80+
type === 'IDEA' ? '#fde047' :
81+
type === 'RESEARCH' ? '#2dd4bf' : '#6b7280'
82+
}));
83+
84+
const maxValue = Math.max(...nodeTypeData.map(item => item.value), 1);
85+
86+
return nodeTypeData.map(item => ({
87+
...item,
88+
maxValue: maxValue
89+
}));
90+
}, [queryData]);
91+
92+
if (loading) {
93+
return (
94+
<div className={`${className} flex items-center justify-center h-64`}>
95+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-400" />
96+
</div>
97+
);
98+
}
99+
100+
if (error || !queryData) {
101+
return (
102+
<div className={`${className} flex items-center justify-center h-64 text-gray-400`}>
103+
<p>Unable to load node distribution data</p>
104+
</div>
105+
);
106+
}
107+
108+
if (radarData.length === 0) {
109+
return (
110+
<div className={`${className} flex items-center justify-center h-64 text-gray-400`}>
111+
<div className="text-center">
112+
<p className="mb-2">No node data found</p>
113+
<p className="text-sm">Create some work items to see the distribution</p>
114+
</div>
115+
</div>
116+
);
117+
}
118+
119+
return (
120+
<div className={className}>
121+
{/* Node Distribution */}
122+
<div className="bg-gray-800 border border-gray-600 rounded-lg p-6">
123+
<div className="mb-4">
124+
<h3 className="text-lg font-semibold text-white mb-2">Node Distribution</h3>
125+
</div>
126+
<div className="relative">
127+
{/* Zoom Controls */}
128+
<div className="absolute top-4 right-4 flex flex-col gap-2 z-10">
129+
<button
130+
onClick={handleZoomIn}
131+
className="p-2 bg-gray-600 hover:bg-gray-500 text-white rounded"
132+
title="Zoom In"
133+
>
134+
<ZoomIn className="h-4 w-4" />
135+
</button>
136+
<button
137+
onClick={handleZoomOut}
138+
className="p-2 bg-gray-600 hover:bg-gray-500 text-white rounded"
139+
title="Zoom Out"
140+
>
141+
<ZoomOut className="h-4 w-4" />
142+
</button>
143+
<button
144+
onClick={handleReset}
145+
className="p-2 bg-gray-600 hover:bg-gray-500 text-white rounded"
146+
title="Reset Zoom"
147+
>
148+
<RotateCcw className="h-4 w-4" />
149+
</button>
150+
</div>
151+
152+
<div className="flex justify-center">
153+
<div
154+
style={{
155+
transform: `scale(${zoomLevel})`,
156+
transformOrigin: 'center',
157+
transition: 'transform 0.2s ease-in-out'
158+
}}
159+
>
160+
<RadarChart
161+
data={radarData}
162+
width={500}
163+
height={500}
164+
margin={80}
165+
radarColor="#CD5C5C"
166+
/>
167+
</div>
168+
</div>
169+
</div>
170+
{showLegend && (
171+
<div className="mt-6">
172+
<h4 className="text-sm font-semibold text-gray-300 mb-3">Node Type Breakdown</h4>
173+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
174+
{radarData.map((item, index) => {
175+
const getIcon = (axis: string) => {
176+
switch(axis) {
177+
case 'Epic': return <Layers className="h-5 w-5" style={{ color: item.color || '#c084fc' }} />;
178+
case 'Milestone': return <Trophy className="h-5 w-5" style={{ color: item.color || '#fb923c' }} />;
179+
case 'Outcome': return <Target className="h-5 w-5" style={{ color: item.color || '#818cf8' }} />;
180+
case 'Feature': return <Sparkles className="h-5 w-5" style={{ color: item.color || '#38bdf8' }} />;
181+
case 'Task': return <ListTodo className="h-5 w-5" style={{ color: item.color || '#4ade80' }} />;
182+
case 'Bug': return <AlertTriangle className="h-5 w-5" style={{ color: item.color || '#ef4444' }} />;
183+
case 'Idea': return <Lightbulb className="h-5 w-5" style={{ color: item.color || '#fde047' }} />;
184+
case 'Research': return <Microscope className="h-5 w-5" style={{ color: item.color || '#2dd4bf' }} />;
185+
default: return <div className="w-4 h-4 rounded-full" style={{ backgroundColor: item.color || '#6b7280' }}></div>;
186+
}
187+
};
188+
189+
return (
190+
<div key={index} className="bg-gray-700 rounded p-3">
191+
<div className="flex items-center mb-2">
192+
{getIcon(item.axis)}
193+
<span className="text-gray-200 text-base ml-2">{item.axis}</span>
194+
</div>
195+
<div className="text-right">
196+
<span className="text-xl font-bold text-white">{item.value}</span>
197+
<span className="text-base text-gray-400 ml-1">nodes</span>
198+
</div>
199+
</div>
200+
);
201+
})}
202+
</div>
203+
</div>
204+
)}
205+
</div>
206+
</div>
207+
);
208+
}

packages/web/src/components/PriorityDistributionRadar.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ interface PriorityDistributionRadarProps {
2323
export function PriorityDistributionRadar({ className = '', showLegend = true }: PriorityDistributionRadarProps) {
2424
const { currentGraph } = useGraph();
2525
const [zoomLevel, setZoomLevel] = useState(1);
26-
const { data: priorityData, loading, error } = useQuery(GET_PRIORITY_DISTRIBUTION, {
26+
const { data: queryData, loading, error } = useQuery(GET_PRIORITY_DISTRIBUTION, {
2727
variables: { graphId: currentGraph?.id },
2828
skip: !currentGraph?.id
2929
});
@@ -41,7 +41,7 @@ export function PriorityDistributionRadar({ className = '', showLegend = true }:
4141
};
4242

4343
const radarData = useMemo(() => {
44-
if (!priorityData?.workItems) return [];
44+
if (!queryData?.workItems) return [];
4545

4646
// Calculate priority levels based on composite priority
4747
const priorityCounts = {
@@ -52,7 +52,7 @@ export function PriorityDistributionRadar({ className = '', showLegend = true }:
5252
minimal: 0
5353
};
5454

55-
priorityData.workItems.forEach((item: any) => {
55+
queryData.workItems.forEach((item: any) => {
5656
// Calculate composite priority (average of all priorities)
5757
const compositePriority = (
5858
(item.priorityExec || 0) +
@@ -79,8 +79,8 @@ export function PriorityDistributionRadar({ className = '', showLegend = true }:
7979
{ axis: 'Critical', value: priorityCounts.critical, color: '#ef4444' },
8080
{ axis: 'High', value: priorityCounts.high, color: '#f97316' },
8181
{ axis: 'Moderate', value: priorityCounts.moderate, color: '#eab308' },
82-
{ axis: 'Low', value: priorityCounts.low, color: '#22c55e' },
83-
{ axis: 'Minimal', value: priorityCounts.minimal, color: '#6b7280' }
82+
{ axis: 'Low', value: priorityCounts.low, color: '#3b82f6' },
83+
{ axis: 'Minimal', value: priorityCounts.minimal, color: '#22c55e' }
8484
].filter(item => item.value > 0); // Only show priorities with tasks
8585

8686
const maxValue = Math.max(...priorityData.map(item => item.value), 1);
@@ -89,7 +89,7 @@ export function PriorityDistributionRadar({ className = '', showLegend = true }:
8989
...item,
9090
maxValue: maxValue
9191
}));
92-
}, [priorityData]);
92+
}, [queryData]);
9393

9494
if (loading) {
9595
return (
@@ -99,7 +99,7 @@ export function PriorityDistributionRadar({ className = '', showLegend = true }:
9999
);
100100
}
101101

102-
if (error || !priorityData) {
102+
if (error || !queryData) {
103103
return (
104104
<div className={`${className} flex items-center justify-center h-64 text-gray-400`}>
105105
<p>Unable to load priority distribution data</p>
@@ -164,6 +164,7 @@ export function PriorityDistributionRadar({ className = '', showLegend = true }:
164164
width={500}
165165
height={500}
166166
margin={80}
167+
radarColor="#8A2BE2"
167168
/>
168169
</div>
169170
</div>
@@ -178,8 +179,8 @@ export function PriorityDistributionRadar({ className = '', showLegend = true }:
178179
case 'Critical': return <Flame className="h-5 w-5" style={{ color: item.color || '#ef4444' }} />;
179180
case 'High': return <Zap className="h-5 w-5" style={{ color: item.color || '#f97316' }} />;
180181
case 'Moderate': return <Triangle className="h-5 w-5" style={{ color: item.color || '#eab308' }} />;
181-
case 'Low': return <Circle className="h-5 w-5" style={{ color: item.color || '#22c55e' }} />;
182-
case 'Minimal': return <ArrowDown className="h-5 w-5" style={{ color: item.color || '#6b7280' }} />;
182+
case 'Low': return <Circle className="h-5 w-5" style={{ color: item.color || '#3b82f6' }} />;
183+
case 'Minimal': return <ArrowDown className="h-5 w-5" style={{ color: item.color || '#22c55e' }} />;
183184
default: return <div className="w-4 h-4 rounded-full" style={{ backgroundColor: item.color || '#4ade80' }}></div>;
184185
}
185186
};

packages/web/src/components/RadarChart.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ interface RadarDataPoint {
66
value: number;
77
maxValue: number;
88
color?: string;
9+
icon?: React.ReactNode;
910
}
1011

1112
interface RadarChartProps {
@@ -15,6 +16,7 @@ interface RadarChartProps {
1516
margin?: number;
1617
levels?: number;
1718
className?: string;
19+
radarColor?: string;
1820
}
1921

2022
export function RadarChart({
@@ -23,7 +25,8 @@ export function RadarChart({
2325
height = 300,
2426
margin = 50,
2527
levels = 5,
26-
className = ''
28+
className = '',
29+
radarColor = 'rgb(156, 163, 175)'
2730
}: RadarChartProps) {
2831
const svgRef = useRef<SVGSVGElement>(null);
2932

@@ -131,12 +134,16 @@ export function RadarChart({
131134
})
132135
.curve(d3.curveLinearClosed);
133136

134-
// Add the radar area
137+
// Add the radar area with custom color
138+
const fillColor = radarColor.startsWith('rgb')
139+
? radarColor.replace('rgb(', 'rgba(').replace(')', ', 0.1)')
140+
: `${radarColor}33`; // Add alpha for hex colors
141+
135142
g.append('path')
136143
.datum(data)
137144
.attr('d', radarLine)
138-
.attr('fill', 'rgba(128, 128, 0, 0.2)')
139-
.attr('stroke', '#808000')
145+
.attr('fill', fillColor)
146+
.attr('stroke', radarColor)
140147
.attr('stroke-width', 2);
141148

142149
// Add data points with status-based colors

0 commit comments

Comments
 (0)