Skip to content

Commit 3e7d935

Browse files
committed
Enhance glow effects with type-specific colors and fix edge arrow consistency
- Change glow effects from generic green to type-specific colors: * Node glows use node type colors (OUTCOME, TASK, MILESTONE) * Edge glows use relationship type colors (DEPENDENCY, BLOCKS, RELATES_TO) - Fix edge arrow fill colors to match relationship types instead of source node colors - Remove CSS edge color overrides to ensure JavaScript-applied relationship colors take precedence - All visual indicators now maintain color consistency with their semantic types
1 parent 69eed4b commit 3e7d935

2 files changed

Lines changed: 148 additions & 98 deletions

File tree

packages/web/src/components/InteractiveGraphVisualization.tsx

Lines changed: 147 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -247,33 +247,165 @@ export function InteractiveGraphVisualization() {
247247
if (!svgRef.current) return;
248248

249249
const svg = d3.select(svgRef.current);
250+
const defs = svg.select('defs');
250251

251252
// Remove glow and reset properties from all elements first
252253
svg.selectAll('.node-bg').style('filter', null);
253254
svg.selectAll('.edge')
254255
.style('filter', null)
255256
.attr('stroke-width', (d: any) => (d.strength || 0.8) * 3); // Reset to normal thickness
256257

257-
// Apply glow to active node if nodeMenu is visible
258+
// Apply type-specific glow to active node if nodeMenu is visible
258259
if (nodeMenu.visible && nodeMenu.node) {
260+
const nodeTypeConfig = getTypeConfig(nodeMenu.node.type as WorkItemType);
261+
const nodeColor = nodeTypeConfig.hexColor;
262+
const filterId = `node-glow-${nodeMenu.node.type.toLowerCase()}`;
263+
264+
// Remove existing filter and create new one with node's type color
265+
defs.select(`#${filterId}`).remove();
266+
267+
const nodeGlowFilter = defs.append('filter')
268+
.attr('id', filterId)
269+
.attr('x', '-100%')
270+
.attr('y', '-100%')
271+
.attr('width', '300%')
272+
.attr('height', '300%');
273+
274+
// Convert hex to RGB values for feColorMatrix
275+
const hexToRgb = (hex: string) => {
276+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
277+
return result ? {
278+
r: parseInt(result[1], 16) / 255,
279+
g: parseInt(result[2], 16) / 255,
280+
b: parseInt(result[3], 16) / 255
281+
} : { r: 0.06, g: 0.73, b: 0.51 }; // fallback green
282+
};
283+
284+
const rgb = hexToRgb(nodeColor);
285+
nodeGlowFilter.append('feColorMatrix')
286+
.attr('in', 'SourceGraphic')
287+
.attr('type', 'matrix')
288+
.attr('values', `0 0 0 0 ${rgb.r} 0 0 0 0 ${rgb.g} 0 0 0 0 ${rgb.b} 0 0 0 1 0`);
289+
290+
const blur = nodeGlowFilter.append('feGaussianBlur')
291+
.attr('stdDeviation', '15')
292+
.attr('result', 'coloredBlur');
293+
294+
blur.append('animate')
295+
.attr('attributeName', 'stdDeviation')
296+
.attr('values', '10;20;10')
297+
.attr('dur', '2s')
298+
.attr('repeatCount', 'indefinite');
299+
300+
const feMerge = nodeGlowFilter.append('feMerge');
301+
feMerge.append('feMergeNode').attr('in', 'coloredBlur');
302+
feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
303+
304+
// Apply the type-specific glow filter
259305
svg.selectAll('.node-bg')
260306
.filter((d: any) => d && d.id === nodeMenu.node?.id)
261-
.style('filter', 'url(#dialog-glow)');
307+
.style('filter', `url(#${filterId})`);
262308
}
263309

264-
// Apply stronger glow to active edge if editingEdge OR showEdgeDetails is visible
310+
// Apply relationship-specific glow to active edge if editingEdge is visible
265311
if (editingEdge && editingEdge.edge) {
312+
const relationshipConfig = getRelationshipConfig(editingEdge.edge.type as RelationshipType);
313+
const edgeColor = relationshipConfig.hexColor;
314+
const edgeFilterId = `edge-glow-${editingEdge.edge.type.toLowerCase()}`;
315+
316+
// Remove existing filter and create new one with relationship color
317+
defs.select(`#${edgeFilterId}`).remove();
318+
319+
const edgeGlowFilter = defs.append('filter')
320+
.attr('id', edgeFilterId)
321+
.attr('x', '-150%')
322+
.attr('y', '-150%')
323+
.attr('width', '400%')
324+
.attr('height', '400%');
325+
326+
const hexToRgb = (hex: string) => {
327+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
328+
return result ? {
329+
r: parseInt(result[1], 16) / 255,
330+
g: parseInt(result[2], 16) / 255,
331+
b: parseInt(result[3], 16) / 255
332+
} : { r: 0.06, g: 0.73, b: 0.51 }; // fallback green
333+
};
334+
335+
const rgb = hexToRgb(edgeColor);
336+
edgeGlowFilter.append('feColorMatrix')
337+
.attr('in', 'SourceGraphic')
338+
.attr('type', 'matrix')
339+
.attr('values', `0 0 0 0 ${rgb.r} 0 0 0 0 ${rgb.g} 0 0 0 0 ${rgb.b} 0 0 0 1 0`);
340+
341+
const edgeBlur = edgeGlowFilter.append('feGaussianBlur')
342+
.attr('stdDeviation', '25')
343+
.attr('result', 'coloredBlur');
344+
345+
edgeBlur.append('animate')
346+
.attr('attributeName', 'stdDeviation')
347+
.attr('values', '15;35;15')
348+
.attr('dur', '1.5s')
349+
.attr('repeatCount', 'indefinite');
350+
351+
const edgeFeMerge = edgeGlowFilter.append('feMerge');
352+
edgeFeMerge.append('feMergeNode').attr('in', 'coloredBlur');
353+
edgeFeMerge.append('feMergeNode').attr('in', 'SourceGraphic');
354+
266355
svg.selectAll('.edge')
267356
.filter((d: any) => d && d.id === editingEdge.edge?.id)
268-
.style('filter', 'url(#edge-dialog-glow)')
357+
.style('filter', `url(#${edgeFilterId})`)
269358
.attr('stroke-width', 12); // Also make the edge thicker
270359
}
271360

272-
// Also apply glow to edge when showing edge details
361+
// Also apply relationship-specific glow to edge when showing edge details
273362
if (showEdgeDetails && selectedEdge) {
363+
const relationshipConfig = getRelationshipConfig(selectedEdge.type as RelationshipType);
364+
const edgeColor = relationshipConfig.hexColor;
365+
const edgeFilterId = `edge-glow-${selectedEdge.type.toLowerCase()}`;
366+
367+
// Remove existing filter and create new one with relationship color
368+
defs.select(`#${edgeFilterId}`).remove();
369+
370+
const edgeGlowFilter = defs.append('filter')
371+
.attr('id', edgeFilterId)
372+
.attr('x', '-150%')
373+
.attr('y', '-150%')
374+
.attr('width', '400%')
375+
.attr('height', '400%');
376+
377+
const hexToRgb = (hex: string) => {
378+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
379+
return result ? {
380+
r: parseInt(result[1], 16) / 255,
381+
g: parseInt(result[2], 16) / 255,
382+
b: parseInt(result[3], 16) / 255
383+
} : { r: 0.06, g: 0.73, b: 0.51 }; // fallback green
384+
};
385+
386+
const rgb = hexToRgb(edgeColor);
387+
edgeGlowFilter.append('feColorMatrix')
388+
.attr('in', 'SourceGraphic')
389+
.attr('type', 'matrix')
390+
.attr('values', `0 0 0 0 ${rgb.r} 0 0 0 0 ${rgb.g} 0 0 0 0 ${rgb.b} 0 0 0 1 0`);
391+
392+
const edgeBlur = edgeGlowFilter.append('feGaussianBlur')
393+
.attr('stdDeviation', '25')
394+
.attr('result', 'coloredBlur');
395+
396+
edgeBlur.append('animate')
397+
.attr('attributeName', 'stdDeviation')
398+
.attr('values', '15;35;15')
399+
.attr('dur', '1.5s')
400+
.attr('repeatCount', 'indefinite');
401+
402+
const edgeFeMerge = edgeGlowFilter.append('feMerge');
403+
edgeFeMerge.append('feMergeNode').attr('in', 'coloredBlur');
404+
edgeFeMerge.append('feMergeNode').attr('in', 'SourceGraphic');
405+
274406
svg.selectAll('.edge')
275407
.filter((d: any) => d && d.id === selectedEdge.id)
276-
.style('filter', 'url(#edge-dialog-glow)')
408+
.style('filter', `url(#${edgeFilterId})`)
277409
.attr('stroke-width', 12); // Same thickness as relationship editing
278410
}
279411
}, [nodeMenu.visible, nodeMenu.node?.id, editingEdge?.edge?.id, showEdgeDetails, selectedEdge?.id]);
@@ -1079,10 +1211,7 @@ export function InteractiveGraphVisualization() {
10791211
return classes;
10801212
})
10811213
.attr('stroke', (d: WorkItemEdge) => {
1082-
// Force bright green for active dialog
1083-
if (editingEdge && editingEdge.edge && editingEdge.edge.id === d.id) {
1084-
return '#10b981'; // Bright green
1085-
}
1214+
// Use relationship color for active dialog (same as normal)
10861215
const config = getRelationshipConfig(d.type as RelationshipType);
10871216
return config.hexColor;
10881217
})
@@ -1163,65 +1292,6 @@ export function InteractiveGraphVisualization() {
11631292
// Add arrowhead markers for middle of edges
11641293
const defs = svg.append('defs');
11651294

1166-
// Create pulsing glow filter for active dialogs (nodes)
1167-
const glowFilter = defs.append('filter')
1168-
.attr('id', 'dialog-glow')
1169-
.attr('x', '-100%')
1170-
.attr('y', '-100%')
1171-
.attr('width', '300%')
1172-
.attr('height', '300%');
1173-
1174-
// Create a bright green glow that pulses
1175-
glowFilter.append('feColorMatrix')
1176-
.attr('in', 'SourceGraphic')
1177-
.attr('type', 'matrix')
1178-
.attr('values', '0 0 0 0 0.06 0 0 0 0 0.73 0 0 0 0 0.51 0 0 0 1 0'); // bright green
1179-
1180-
const blur = glowFilter.append('feGaussianBlur')
1181-
.attr('stdDeviation', '15')
1182-
.attr('result', 'coloredBlur');
1183-
1184-
// Add animation to the blur intensity for pulsing effect
1185-
blur.append('animate')
1186-
.attr('attributeName', 'stdDeviation')
1187-
.attr('values', '10;20;10')
1188-
.attr('dur', '2s')
1189-
.attr('repeatCount', 'indefinite');
1190-
1191-
// Merge blur with original
1192-
const feMerge = glowFilter.append('feMerge');
1193-
feMerge.append('feMergeNode').attr('in', 'coloredBlur');
1194-
feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
1195-
1196-
// Create stronger glow filter specifically for edges
1197-
const edgeGlowFilter = defs.append('filter')
1198-
.attr('id', 'edge-dialog-glow')
1199-
.attr('x', '-150%')
1200-
.attr('y', '-150%')
1201-
.attr('width', '400%')
1202-
.attr('height', '400%');
1203-
1204-
// Much stronger glow for edges
1205-
edgeGlowFilter.append('feColorMatrix')
1206-
.attr('in', 'SourceGraphic')
1207-
.attr('type', 'matrix')
1208-
.attr('values', '0 0 0 0 0.06 0 0 0 0 0.73 0 0 0 0 0.51 0 0 0 1 0'); // bright green
1209-
1210-
const edgeBlur = edgeGlowFilter.append('feGaussianBlur')
1211-
.attr('stdDeviation', '25')
1212-
.attr('result', 'coloredBlur');
1213-
1214-
// Stronger pulsing animation for edges
1215-
edgeBlur.append('animate')
1216-
.attr('attributeName', 'stdDeviation')
1217-
.attr('values', '15;35;15')
1218-
.attr('dur', '1.5s')
1219-
.attr('repeatCount', 'indefinite');
1220-
1221-
// Merge blur with original for edges
1222-
const edgeFeMerge = edgeGlowFilter.append('feMerge');
1223-
edgeFeMerge.append('feMergeNode').attr('in', 'coloredBlur');
1224-
edgeFeMerge.append('feMergeNode').attr('in', 'SourceGraphic');
12251295

12261296
// Create different arrowhead colors for each edge type
12271297
RELATIONSHIP_OPTIONS.forEach((option) => {
@@ -1437,13 +1507,15 @@ export function InteractiveGraphVisualization() {
14371507
return '#1f2937'; // Dark background consistent with theme
14381508
})
14391509
.attr('stroke', (d: WorkItem) => {
1440-
// Force bright green with glow for active dialog
1510+
// Use node type color for active dialog
14411511
if (nodeMenu.visible && nodeMenu.node && nodeMenu.node.id === d.id) {
1442-
return '#10b981'; // Bright green
1512+
const typeConfig = getTypeConfig(d.type as WorkItemType);
1513+
return typeConfig.hexColor;
14431514
}
1444-
// Highlight selected node with bright border
1515+
// Highlight selected node with type color border
14451516
if (selectedNode && selectedNode.id === d.id) {
1446-
return '#10b981'; // Bright green for selected node
1517+
const typeConfig = getTypeConfig(d.type as WorkItemType);
1518+
return typeConfig.hexColor;
14471519
}
14481520
if (d.status === 'COMPLETED' || d.status === 'Completed' || d.status === 'Done' || d.status === 'DONE') {
14491521
return '#4b5563';
@@ -1855,22 +1927,12 @@ export function InteractiveGraphVisualization() {
18551927
.attr('class', 'arrow')
18561928
.attr('d', 'M-16,-8 L0,0 L-16,8 L-8,0 Z')
18571929
.attr('fill', (d: WorkItemEdge) => {
1858-
// Use the source node's color for the arrow
1859-
const sourceNode = nodes.find(n => n.id === (typeof d.source === 'string' ? d.source : (d.source as any)?.id));
1860-
if (sourceNode) {
1861-
return getNodeColor(sourceNode);
1862-
}
1863-
// Fallback to edge type color if node not found
1930+
// Use the relationship color for consistency with edge stroke
18641931
const config = getRelationshipConfig(d.type as RelationshipType);
18651932
return config.hexColor;
18661933
})
18671934
.attr('stroke', (d: WorkItemEdge) => {
1868-
// Use the source node's color for the arrow stroke
1869-
const sourceNode = nodes.find(n => n.id === (typeof d.source === 'string' ? d.source : (d.source as any)?.id));
1870-
if (sourceNode) {
1871-
return getNodeColor(sourceNode);
1872-
}
1873-
// Fallback to edge type color if node not found
1935+
// Always use relationship color for consistency
18741936
const config = getRelationshipConfig(d.type as RelationshipType);
18751937
return config.hexColor;
18761938
})

packages/web/src/index.css

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -225,19 +225,7 @@ svg text:not(.completion-indicator) {
225225
}
226226

227227
.edge {
228-
@apply stroke-current opacity-60;
229-
}
230-
231-
.edge-dependency {
232-
@apply stroke-blue-500;
233-
}
234-
235-
.edge-blocks {
236-
@apply stroke-red-500;
237-
}
238-
239-
.edge-relates {
240-
@apply stroke-gray-400;
228+
opacity: 0.6;
241229
}
242230

243231
/* Priority indicator styles */

0 commit comments

Comments
 (0)