Skip to content

Commit

Permalink
Add segmented view, update map layout, refactor graph components.
Browse files Browse the repository at this point in the history
  • Loading branch information
brysonjbest committed Dec 17, 2024
1 parent e503c97 commit 815b7b6
Show file tree
Hide file tree
Showing 17 changed files with 654 additions and 411 deletions.
72 changes: 32 additions & 40 deletions app/components/RuleRelationsDisplay/RuleRelationsDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,41 @@
import { useEffect, useRef, useState, RefObject } from "react";
import { useEffect, useRef, useState, RefObject, createContext } from "react";
import { CategoryObject } from "@/app/types/ruleInfo";
import { RuleMapRule } from "@/app/types/rulemap";
import styles from "@/app/components/RuleRelationsDisplay/RuleRelationsDisplay.module.css";
import { RuleMapRule, RuleNode } from "@/app/types/rulemap";
import styles from "./RuleRelationsDisplay.module.css";
import { RuleGraphControls } from "./subcomponents/RuleGraphControls";
import { useRuleGraph } from "../../hooks/useRuleGraph";
import { DescriptionManager } from "./subcomponents/DescriptionManager";
import { RuleModalProvider } from "../../contexts/RuleModalContext";
import { RuleGraph } from "./subcomponents/RuleGraph";

interface RuleModalContextType {
selectedRule: RuleNode | null;
openModal: (rule: RuleNode) => void;
closeModal: () => void;
}

export const RuleModalContext = createContext<RuleModalContextType | null>(null);

export interface RuleGraphProps {
rules: RuleMapRule[];
categories: CategoryObject[];
searchTerm?: string;
setSearchTerm?: (value: string) => void;
embeddedCategory?: string;
width?: number;
height?: number;
filter?: string;
filter?: string | string[];
location?: Location;
basicLegend?: boolean;
}

export interface GraphContentProps {
rules: RuleMapRule[];
svgRef: RefObject<SVGSVGElement>;
dimensions: { width: number; height: number };
searchTerm: string;
categoryFilter: string | undefined;
showDraftRules: boolean;
}

// Renders the actual SVG graph visualization using D3.js
// Separated from the main component for better readability
// and to utilize the modal context provider
function GraphContent({ rules, svgRef, dimensions, searchTerm, categoryFilter, showDraftRules }: GraphContentProps) {
useRuleGraph({
rules,
svgRef,
dimensions,
searchTerm,
categoryFilter,
showDraftRules,
});

return <svg ref={svgRef} className={styles.svg} />;
}

/**
* Manages the visualization of rule relationships in a graph format
* Includes search, category filtering and draft rules toggle
*/
export default function RuleRelationsGraph({
rules,
categories,
searchTerm = "",
setSearchTerm,
width = 1000,
height = 1000,
filter,
Expand All @@ -59,10 +45,16 @@ export default function RuleRelationsGraph({
const containerRef = useRef<HTMLDivElement>(null);
const svgRef = useRef<SVGSVGElement>(null);
const [dimensions, setDimensions] = useState({ width, height });
const [searchTerm, setSearchTerm] = useState("");
const [isLegendMinimized, setIsLegendMinimized] = useState(true);
const [categoryFilter, setCategoryFilter] = useState(filter || undefined);
const [categoryFilter, setCategoryFilter] = useState(filter && filter?.length > 0 ? filter : undefined);
const [showDraftRules, setShowDraftRules] = useState(true);
const [selectedRule, setSelectedRule] = useState<RuleNode | null>(null);

const modalContext: RuleModalContextType = {
selectedRule,
openModal: (rule: RuleNode) => setSelectedRule(rule),
closeModal: () => setSelectedRule(null),
};

/**
* Sets up a ResizeObserver to handle responsive sizing
Expand All @@ -81,16 +73,16 @@ export default function RuleRelationsGraph({
}, []);

useEffect(() => {
if (filter) {
if (filter && filter?.length > 0) {
setCategoryFilter(filter);
}
}, [filter]);

const handleSearchChange = (value: string) => {
setSearchTerm(value);
setSearchTerm && setSearchTerm(value);
};

const handleCategoryChange = (value: string) => {
const handleCategoryChange = (value: string | string[]) => {
setCategoryFilter(value);
};

Expand All @@ -103,13 +95,13 @@ export default function RuleRelationsGraph({
};

const handleClearFilters = () => {
setSearchTerm("");
setSearchTerm && setSearchTerm("");
setCategoryFilter("");
};

return (
<div ref={containerRef} className={styles.container}>
<RuleModalProvider>
<RuleModalContext.Provider value={modalContext}>
<RuleGraphControls
searchTerm={searchTerm}
categoryFilter={categoryFilter}
Expand All @@ -125,7 +117,7 @@ export default function RuleRelationsGraph({
location={location}
basicLegend={basicLegend}
/>
<GraphContent
<RuleGraph
rules={rules}
svgRef={svgRef}
dimensions={dimensions}
Expand All @@ -134,7 +126,7 @@ export default function RuleRelationsGraph({
showDraftRules={showDraftRules}
/>
<DescriptionManager />
</RuleModalProvider>
</RuleModalContext.Provider>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useContext } from "react";
import { RuleDescription } from "./RuleDescription";
import { useRuleModal } from "../../../contexts/RuleModalContext";
import Modal from "antd/es/modal/Modal";
import { RuleModalContext } from "../RuleRelationsDisplay";

// Manages the display of the rule description modal
export function DescriptionManager() {
const { selectedRule, closeModal } = useRuleModal();
const context = useContext(RuleModalContext);
if (!context || !context.selectedRule) return null;

if (!selectedRule) return null;
const { selectedRule, closeModal } = context;

return (
<Modal
Expand Down
58 changes: 33 additions & 25 deletions app/components/RuleRelationsDisplay/subcomponents/RuleFilters.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,54 @@
import { RuleNode, RuleLink } from "@/app/types/rulemap";

// Returns nodes for a specific category
// Returns nodes for categories
// If category is empty, returns all nodes
// If showDraftRules is false, only returns published nodes
// Also returns all parent rules and child rules of the matching nodes
export const getNodesForCategory = (
nodes: RuleNode[],
category: string | undefined,
category: string | string[] | undefined,
showDraftRules: boolean,
getAllParentRules: (nodeId: number) => Set<number>,
getAllChildRules: (nodeId: number) => Set<number>
): Set<number> => {
const matchingNodes = new Set<number>();

nodes.forEach((node) => {
if (!showDraftRules && !node.isPublished) return;
if (!category) {
nodes.forEach((node) => {
if (showDraftRules || node.isPublished) {
matchingNodes.add(node.id);
}
});
return matchingNodes;
}

if (!category) {
matchingNodes.add(node.id);
return;
}
const categories = Array.isArray(category) ? category : [category];

if (node.filepath?.includes(category)) {
matchingNodes.add(node.id);
categories.forEach((cat) => {
nodes.forEach((node) => {
if (!showDraftRules && !node.isPublished) return;

const parentRules = getAllParentRules(node.id);
const childRules = getAllChildRules(node.id);
if (node.filepath?.includes(cat)) {
matchingNodes.add(node.id);

parentRules.forEach((id) => {
const parentNode = nodes.find((n) => n.id === id);
if (parentNode && (showDraftRules || parentNode.isPublished)) {
matchingNodes.add(id);
}
});
const parentRules = getAllParentRules(node.id);
const childRules = getAllChildRules(node.id);

childRules.forEach((id) => {
const childNode = nodes.find((n) => n.id === id);
if (childNode && (showDraftRules || childNode.isPublished)) {
matchingNodes.add(id);
}
});
}
parentRules.forEach((id) => {
const parentNode = nodes.find((n) => n.id === id);
if (parentNode && (showDraftRules || parentNode.isPublished)) {
matchingNodes.add(id);
}
});

childRules.forEach((id) => {
const childNode = nodes.find((n) => n.id === id);
if (childNode && (showDraftRules || childNode.isPublished)) {
matchingNodes.add(id);
}
});
}
});
});

return matchingNodes;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
import { useEffect, RefObject, useRef } from "react";
import { useEffect, RefObject, useRef, useContext } from "react";
import * as d3 from "d3";
import { RuleMapRule, RuleNode, RuleLink } from "@/app/types/rulemap";
import { useGraphTraversal } from "@/app/utils/graphUtils";
import {
getNodesForCategory,
isNodeVisible,
isLinkVisible,
} from "../components/RuleRelationsDisplay/subcomponents/RuleFilters";
import { GraphNavigation } from "../components/RuleRelationsDisplay/subcomponents/GraphNavigation";
import { RuleNodesGroup } from "../components/RuleRelationsDisplay/subcomponents/RuleNodesGroup";
import { useRuleModal } from "../contexts/RuleModalContext";
} from "@/app/components/RuleRelationsDisplay/subcomponents/RuleFilters";
import { GraphNavigation } from "@/app/components/RuleRelationsDisplay/subcomponents/GraphNavigation";
import { RuleNodesGroup } from "@/app/components/RuleRelationsDisplay/subcomponents/RuleNodesGroup";
import { RuleModalContext } from "@/app/components/RuleRelationsDisplay/RuleRelationsDisplay";
import styles from "@/app/components/RuleRelationsDisplay/RuleRelationsDisplay.module.css";

interface UseRuleGraphProps {
interface RuleGraphProps {
rules: RuleMapRule[];
svgRef: RefObject<SVGSVGElement>;
dimensions: { width: number; height: number };
searchTerm: string;
categoryFilter: string | undefined;
categoryFilter?: string | string[];
showDraftRules: boolean;
}

// Manages the rule graph visualization using D3.js
export const useRuleGraph = ({
rules,
svgRef,
dimensions,
searchTerm,
categoryFilter,
showDraftRules,
}: UseRuleGraphProps) => {
const { openModal } = useRuleModal();
const useRuleGraph = ({ rules, svgRef, dimensions, searchTerm, categoryFilter, showDraftRules }: RuleGraphProps) => {
const context = useContext(RuleModalContext);
if (!context) {
throw new Error("RuleGraph must be used within a RuleModalContext Provider");
}
const { openModal } = context;

const graphFunctionsRef = useRef<{
getAllParentRules: (id: number) => Set<number>;
Expand Down Expand Up @@ -115,22 +113,55 @@ export const useRuleGraph = ({
// eslint-disable-next-line react-hooks/rules-of-hooks
graphFunctionsRef.current = useGraphTraversal(links);

// Create simulation with base forces and constraints
// Forces and constraints can be modified to adjust graph layout
// Calculate total children for each node to weigh the layout
// More children/grandchildren = higher y position
const getChildCount = (nodeId: number, visited = new Set<number>()): number => {
if (visited.has(nodeId)) return 0;
visited.add(nodeId);

const directChildren = links
.filter((link) => link.source === nodeId)
.map((link) => (link.target as any).id || link.target);

let count = directChildren.length;
directChildren.forEach((childId) => {
count += getChildCount(childId, visited);
});

return count;
};

// Calculate and store Child counts for all nodes
const childrenCount = new Map<number, number>();
nodes.forEach((node) => {
childrenCount.set(node.id, getChildCount(node.id));
});

// Get the maximum children count for scaling
const maxChildren = Math.max(...Array.from(childrenCount.values()));
const simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3
.forceLink(links)
.id((d: any) => d.id)
.distance(150)
.distance(200)
.strength(0.5)
)
.force("charge", d3.forceManyBody().strength(-500).distanceMax(500))
.force("x", d3.forceX().strength(0.03))
.force("y", d3.forceY().strength(0.03))
.force("collision", d3.forceCollide().radius(60));
.force("charge", d3.forceManyBody().strength(-800).distanceMax(600))
.force("x", d3.forceX().strength(0.05))
.force(
"y",
d3
.forceY()
.y((d: any) => {
const children = childrenCount.get(d.id) || 0;
return -300 + 600 * (1 - children / maxChildren);
})
.strength(0.2)
)
.force("collision", d3.forceCollide().radius(80));

// Draw links
const link = containerGroup
Expand All @@ -150,11 +181,12 @@ export const useRuleGraph = ({
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("markerWidth", 8)
.attr("markerHeight", 8)
.attr("orient", "auto")
.append("path")
.attr("fill", "#999")
.attr("stroke-width", 1)
.attr("d", "M0,-5L10,0L0,5");

// Highlights parent and child rules, and links between them when a node is selected
Expand Down Expand Up @@ -322,3 +354,17 @@ export const useRuleGraph = ({
[rules, dimensions, searchTerm, categoryFilter, showDraftRules]
);
};

// GraphContent component that renders the SVG element as a functional component
export function RuleGraph({ rules, svgRef, dimensions, searchTerm, categoryFilter, showDraftRules }: RuleGraphProps) {
useRuleGraph({
rules,
svgRef,
dimensions,
searchTerm,
categoryFilter,
showDraftRules,
});

return <svg ref={svgRef} className={styles.svg} />;
}
Loading

0 comments on commit 815b7b6

Please sign in to comment.