diff --git a/demo/Demo.tsx b/demo/Demo.tsx index 676a618..0b0c7ae 100644 --- a/demo/Demo.tsx +++ b/demo/Demo.tsx @@ -5,6 +5,7 @@ import { Markers } from '@demo/edges'; import { BreadthFirstIteratorPage, DepthFirstIteratorPage, + EquivalentNodesPage, IndexPage, PlanarizationPage, TopologicalIteratorPage, @@ -20,6 +21,7 @@ export const Demo = () => { } /> } /> } /> + } /> } /> diff --git a/demo/src/assets/equivalent.svg b/demo/src/assets/equivalent.svg new file mode 100644 index 0000000..febffee --- /dev/null +++ b/demo/src/assets/equivalent.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/demo/src/contexts/DirectedAcyclicGraph/types.tsx b/demo/src/contexts/DirectedAcyclicGraph/types.tsx index 3a374d9..47863ed 100644 --- a/demo/src/contexts/DirectedAcyclicGraph/types.tsx +++ b/demo/src/contexts/DirectedAcyclicGraph/types.tsx @@ -4,13 +4,17 @@ import { } from '@xyflow/react'; type Data = { - root?: boolean; - ignored?: boolean; + /** General */ loading?: boolean; active?: boolean; inactive?: boolean; disabled?: boolean; group?: string; + + /** Demo case specific */ + root?: boolean; + ignored?: boolean; + equivalenceClassNumber?: number; }; export type NodeData = Omit, 'id'>; diff --git a/demo/src/nodes/ClassifiedNode/index.module.css b/demo/src/nodes/ClassifiedNode/index.module.css new file mode 100644 index 0000000..ed0a783 --- /dev/null +++ b/demo/src/nodes/ClassifiedNode/index.module.css @@ -0,0 +1,5 @@ +.marker { + position: absolute; + width: 60%; + height: 60%; +} diff --git a/demo/src/nodes/ClassifiedNode/index.tsx b/demo/src/nodes/ClassifiedNode/index.tsx new file mode 100644 index 0000000..ad7ab75 --- /dev/null +++ b/demo/src/nodes/ClassifiedNode/index.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { NodeToolbar, Position, useReactFlow } from '@xyflow/react'; + +import { NodeProps } from '@demo/contexts'; + +import { BaseNode } from '../BaseNode'; + +import { EquivalenceClassMarker } from './marker'; + +import S from './index.module.css'; + +export const ClassifiedNode = ({ id, selected, data }: NodeProps) => { + const { deleteElements } = useReactFlow(); + + return ( + <> + + + + + {data.equivalenceClassNumber !== undefined ? ( + + ) : ( + id + )} + + + ); +}; diff --git a/demo/src/nodes/ClassifiedNode/marker.tsx b/demo/src/nodes/ClassifiedNode/marker.tsx new file mode 100644 index 0000000..45ffed4 --- /dev/null +++ b/demo/src/nodes/ClassifiedNode/marker.tsx @@ -0,0 +1,61 @@ +import React, { memo } from 'react'; + +import * as T from './types'; + +/** Variant count of each feature should be unique prime number */ +/** 2 */ +const stroke = [0, 64]; + +/** 5 */ +const shapes = [ + + + , + + + , + + + , + + + , + + + , +]; + +/** 7 */ +const colors = [ + '#ff3f3f', + '#ffcc06', + '#0a9ad7', + '#9ad70a', + '#d70a9a', + '#3a329f', + '#111111', +]; + +export const EquivalenceClassMarker = memo( + ({ num, className }: T.EquivalenceClassMarkerProps) => { + return ( +
+ {shapes[num % shapes.length]} +
+ ); + }, +); diff --git a/demo/src/nodes/ClassifiedNode/types.tsx b/demo/src/nodes/ClassifiedNode/types.tsx new file mode 100644 index 0000000..1aa9a3e --- /dev/null +++ b/demo/src/nodes/ClassifiedNode/types.tsx @@ -0,0 +1,4 @@ +export interface EquivalenceClassMarkerProps { + num: number; + className?: string; +} diff --git a/demo/src/nodes/index.tsx b/demo/src/nodes/index.tsx index a0955ff..f178a36 100644 --- a/demo/src/nodes/index.tsx +++ b/demo/src/nodes/index.tsx @@ -1,6 +1,7 @@ import { DeletableNode } from './DeletableNode'; import { AnyFirstIteratorNode } from './AnyFirstIteratorNode'; import { PartitionNode } from './PartitionNode'; +import { ClassifiedNode } from './ClassifiedNode'; export enum Shape { CIRCLE = 'circle', @@ -8,17 +9,20 @@ export enum Shape { } export const nodeTypes = { - Deletable: DeletableNode, AnyFirstIterator: AnyFirstIteratorNode, + Classified: ClassifiedNode, + Deletable: DeletableNode, Partition: PartitionNode, }; export const nodeShapes: Record = { - Deletable: Shape.CIRCLE, AnyFirstIterator: Shape.CIRCLE, + Classified: Shape.CIRCLE, + Deletable: Shape.CIRCLE, Partition: Shape.RECTANGLE, }; export * from './AnyFirstIteratorNode'; +export * from './ClassifiedNode'; export * from './DeletableNode'; export * from './PartitionNode'; diff --git a/demo/src/pages/DepthFirstIterator/card.tsx b/demo/src/pages/DepthFirstIterator/card.tsx index 1704a4f..31e066b 100644 --- a/demo/src/pages/DepthFirstIterator/card.tsx +++ b/demo/src/pages/DepthFirstIterator/card.tsx @@ -8,7 +8,12 @@ import React, { } from 'react'; import { Card, Radio } from '@demo/components'; -import { NodeData, getIgnored, getRootNode, useDAGContext } from '@demo/contexts'; +import { + NodeData, + getIgnored, + getRootNode, + useDAGContext, +} from '@demo/contexts'; import { NodeWithData } from '@demo/hooks'; import S from './index.module.css'; diff --git a/demo/src/pages/EquivalentNodes/card.tsx b/demo/src/pages/EquivalentNodes/card.tsx new file mode 100644 index 0000000..08fc905 --- /dev/null +++ b/demo/src/pages/EquivalentNodes/card.tsx @@ -0,0 +1,111 @@ +import { + getEquivalentNodesByParents, + getEquivalentNodesByChildren, + getEquivalentNodes, +} from '@self/dag'; +import React, { useCallback, useEffect, useState } from 'react'; + +import { Card, Radio } from '@demo/components'; +import { NodeData, useDAGContext } from '@demo/contexts'; + +import * as T from './types'; +import S from './index.module.css'; +import { NodeWithData } from '@demo/hooks'; + +const availableEquivalenceBy = [ + T.EquivalenceBy.PARENTS, + T.EquivalenceBy.CHILDREN, + T.EquivalenceBy.BOTH, +]; + +export const EquivalentNodesCard = () => { + const [instance, dag] = useDAGContext(); + + const [equivalenceBy, setEquivalenceBy] = useState< + T.EquivalenceBy | undefined + >(); + + const groupByEquivalenceClass = useCallback( + () => + dag.batch(() => { + if (equivalenceBy == undefined) return; + + let projection; + switch (equivalenceBy) { + case T.EquivalenceBy.PARENTS: + projection = getEquivalentNodesByParents( + instance, + new Set(instance.nodes), + ); + break; + case T.EquivalenceBy.CHILDREN: + projection = getEquivalentNodesByChildren( + instance, + new Set(instance.nodes), + ); + break; + case T.EquivalenceBy.BOTH: + projection = getEquivalentNodes(instance, new Set(instance.nodes)); + break; + } + + let classNumber = 0; + const classified = new Set>>(); + for (const [, cls] of projection.entries()) { + if (classified.has(cls)) continue; + + for (const node of cls) { + dag.replace(node.id, { + ...node.data, + data: { ...node.data, equivalenceClassNumber: classNumber }, + }); + } + classified.add(cls); + ++classNumber; + } + }), + [instance, equivalenceBy], + ); + + useEffect( + () => () => + [...instance.nodes].forEach((node) => + dag.replace(node.id, (data) => ({ + ...data, + data: { ...data.data, equivalenceClassNumber: undefined }, + })), + ), + [], + ); + + return ( + + Equivalent Nodes + + + + Choose node equivalence type: +
+ {availableEquivalenceBy.map((value) => ( + setEquivalenceBy(value)} + style={{ pointerEvents: 'all' }} + > + {value} + + ))} +
+
+
+
+
+ ); +}; diff --git a/demo/src/pages/EquivalentNodes/index.module.css b/demo/src/pages/EquivalentNodes/index.module.css new file mode 100644 index 0000000..b1b83a9 --- /dev/null +++ b/demo/src/pages/EquivalentNodes/index.module.css @@ -0,0 +1,5 @@ +.equivalence-by { + display: flex; + flex-flow: row wrap; + gap: 4px; +} diff --git a/demo/src/pages/EquivalentNodes/index.tsx b/demo/src/pages/EquivalentNodes/index.tsx new file mode 100644 index 0000000..81cc0c6 --- /dev/null +++ b/demo/src/pages/EquivalentNodes/index.tsx @@ -0,0 +1,71 @@ +import React, { useMemo } from 'react'; +import { Background, Controls, ReactFlow } from '@xyflow/react'; + +import { Panel } from '@demo/components'; +import { useDAGContext } from '@demo/contexts'; +import { nodeTypes } from '@demo/nodes'; +import { + edgeId, + useChangeHandlers, + useConnectionHandlers, + useMerged, + useSelection, +} from '@demo/hooks'; +import { DefaultConnectionLine, edgeTypes } from '@demo/edges'; + +import { EquivalentNodesCard } from './card'; + +export const EquivalentNodesPage = () => { + const [selected, onSelectionChange] = useSelection(); + const [instance, dag] = useDAGContext(); + const nodes = useMemo( + () => + [...instance.nodes].map((node) => ({ + id: node.id, + ...node.data, + type: 'Classified', + selected: selected.has(node.id), + })), + + [instance], + ); + const edges = useMemo( + () => + [...instance.edges].map(([tail, head]) => ({ + id: edgeId(tail.id, head.id), + type: 'Default', + source: tail.id, + target: head.id, + selected: selected.has(edgeId(tail.id, head.id)), + })), + [instance], + ); + + const [onNodesChange, onEdgesChange] = useChangeHandlers(dag); + const [onConnect, onConnectEnd] = useConnectionHandlers(dag); + + const onNodesChangeMerged = useMerged(onSelectionChange, onNodesChange); + const onEdgesChangeMerged = useMerged(onSelectionChange, onEdgesChange); + + return ( + + + + + + + + ); +}; diff --git a/demo/src/pages/EquivalentNodes/types.tsx b/demo/src/pages/EquivalentNodes/types.tsx new file mode 100644 index 0000000..43e64d5 --- /dev/null +++ b/demo/src/pages/EquivalentNodes/types.tsx @@ -0,0 +1,5 @@ +export const enum EquivalenceBy { + PARENTS = 'by parents', + CHILDREN = 'by children', + BOTH = 'by parents and children', +} diff --git a/demo/src/pages/index.tsx b/demo/src/pages/index.tsx index 20a627d..a545518 100644 --- a/demo/src/pages/index.tsx +++ b/demo/src/pages/index.tsx @@ -4,6 +4,7 @@ import { Background, Controls, MarkerType, ReactFlow } from '@xyflow/react'; import TopsortLogo from '@demo/assets/topsort.svg'; import DfsLogo from '@demo/assets/dfs.svg'; import BfsLogo from '@demo/assets/bfs.svg'; +import EquivalentLogo from '@demo/assets/equivalent.svg'; import PlanarizationLogo from '@demo/assets/planarization.svg'; import { Panel, Navigation } from '@demo/components'; @@ -31,6 +32,11 @@ export const DemoNavigation = ({ className }: { className?: string }) => ( BFS + + + Equivalent + + Planarize Multipartite @@ -97,4 +103,5 @@ export const IndexPage = () => { export * from './TopologicalIterator'; export * from './BreadthFirstIterator'; export * from './DepthFirstIterator'; +export * from './EquivalentNodes'; export * from './Planarization';