@@ -49,7 +49,7 @@ const LAYOUT_FACTORIES: Record<LayoutType, LayoutFactory> = {
4949 'radial-layout' : ( options ) => new RadialLayout ( options ) ,
5050 'hive-plot-layout' : ( options ) => new HivePlotLayout ( options ) ,
5151 'force-multi-graph-layout' : ( options ) => new ForceMultiGraphLayout ( options ) ,
52- 'd3-dag-layout' : ( ) => new D3DagLayout ( ) ,
52+ 'd3-dag-layout' : ( options ) => new D3DagLayout ( options ) ,
5353} ;
5454
5555
@@ -97,6 +97,10 @@ export const useLoading = (engine) => {
9797export function App ( props ) {
9898 const [ selectedExample , setSelectedExample ] = useState < ExampleDefinition | undefined > ( DEFAULT_EXAMPLE ) ;
9999 const [ selectedLayout , setSelectedLayout ] = useState < LayoutType > ( DEFAULT_LAYOUT ) ;
100+ const [ collapseEnabled , setCollapseEnabled ] = useState ( true ) ;
101+ const [ dagChainSummary , setDagChainSummary ] = useState <
102+ { chainIds : string [ ] ; collapsedIds : string [ ] }
103+ | null > ( null ) ;
100104
101105 const graphData = useMemo ( ( ) => selectedExample ?. data ( ) , [ selectedExample ] ) ;
102106 const layoutOptions = useMemo (
@@ -117,6 +121,7 @@ export function App(props) {
117121 } , [ selectedLayout , layoutOptions ] ) ;
118122 const engine = useMemo ( ( ) => ( graph && layout ? new GraphEngine ( { graph, layout} ) : null ) , [ graph , layout ] ) ;
119123 const isFirstMount = useRef ( true ) ;
124+ const dagLayout = layout instanceof D3DagLayout ? ( layout as D3DagLayout ) : null ;
120125
121126 useLayoutEffect ( ( ) => {
122127 if ( ! engine ) {
@@ -166,6 +171,95 @@ export function App(props) {
166171 const [ { isLoading} , loadingDispatch ] = useLoading ( engine ) as any ;
167172
168173 const selectedStyles = selectedExample ?. style ;
174+ const isDagLayout = selectedLayout === 'd3-dag-layout' ;
175+ const totalChainCount = dagChainSummary ?. chainIds . length ?? 0 ;
176+ const collapsedChainCount = dagChainSummary ?. collapsedIds . length ?? 0 ;
177+ const collapseAllDisabled =
178+ ! collapseEnabled || ! dagChainSummary || dagChainSummary . chainIds . length === 0 ;
179+ const expandAllDisabled =
180+ ! collapseEnabled || ! dagChainSummary || dagChainSummary . collapsedIds . length === 0 ;
181+
182+ useEffect ( ( ) => {
183+ if ( isDagLayout ) {
184+ setCollapseEnabled ( true ) ;
185+ }
186+ } , [ isDagLayout , selectedExample ] ) ;
187+
188+ useEffect ( ( ) => {
189+ if ( ! dagLayout ) {
190+ return ;
191+ }
192+ dagLayout . setPipelineOptions ( { collapseLinearChains : collapseEnabled } ) ;
193+ if ( ! collapseEnabled ) {
194+ dagLayout . setCollapsedChains ( [ ] ) ;
195+ }
196+ } , [ dagLayout , collapseEnabled ] ) ;
197+
198+ useEffect ( ( ) => {
199+ if ( ! engine || ! dagLayout ) {
200+ setDagChainSummary ( isDagLayout ? { chainIds : [ ] , collapsedIds : [ ] } : null ) ;
201+ return ;
202+ }
203+
204+ const updateChainSummary = ( ) => {
205+ const chainIds : string [ ] = [ ] ;
206+ const collapsedIds : string [ ] = [ ] ;
207+
208+ for ( const node of engine . getNodes ( ) ) {
209+ const chainId = node . getPropertyValue ( 'collapsedChainId' ) ;
210+ const nodeIds = node . getPropertyValue ( 'collapsedNodeIds' ) ;
211+ const representativeId = node . getPropertyValue ( 'collapsedChainRepresentativeId' ) ;
212+ const isCollapsed = Boolean ( node . getPropertyValue ( 'isCollapsedChain' ) ) ;
213+
214+ if (
215+ chainId !== null &&
216+ chainId !== undefined &&
217+ Array . isArray ( nodeIds ) &&
218+ nodeIds . length > 1 &&
219+ representativeId === node . getId ( )
220+ ) {
221+ const chainKey = String ( chainId ) ;
222+ chainIds . push ( chainKey ) ;
223+ if ( isCollapsed ) {
224+ collapsedIds . push ( chainKey ) ;
225+ }
226+ }
227+ }
228+
229+ setDagChainSummary ( { chainIds, collapsedIds} ) ;
230+ } ;
231+
232+ updateChainSummary ( ) ;
233+
234+ const handleLayoutChange = ( ) => updateChainSummary ( ) ;
235+ const handleLayoutDone = ( ) => updateChainSummary ( ) ;
236+
237+ engine . addEventListener ( 'onLayoutChange' , handleLayoutChange ) ;
238+ engine . addEventListener ( 'onLayoutDone' , handleLayoutDone ) ;
239+
240+ return ( ) => {
241+ engine . removeEventListener ( 'onLayoutChange' , handleLayoutChange ) ;
242+ engine . removeEventListener ( 'onLayoutDone' , handleLayoutDone ) ;
243+ } ;
244+ } , [ engine , dagLayout , isDagLayout ] ) ;
245+
246+ const handleToggleCollapseEnabled = useCallback ( ( ) => {
247+ setCollapseEnabled ( ( value ) => ! value ) ;
248+ } , [ ] ) ;
249+
250+ const handleCollapseAll = useCallback ( ( ) => {
251+ if ( ! collapseEnabled || ! dagLayout || ! dagChainSummary ) {
252+ return ;
253+ }
254+ dagLayout . setCollapsedChains ( dagChainSummary . chainIds ) ;
255+ } , [ collapseEnabled , dagLayout , dagChainSummary ] ) ;
256+
257+ const handleExpandAll = useCallback ( ( ) => {
258+ if ( ! collapseEnabled || ! dagLayout ) {
259+ return ;
260+ }
261+ dagLayout . setCollapsedChains ( [ ] ) ;
262+ } , [ collapseEnabled , dagLayout ] ) ;
169263
170264 const fitBounds = useCallback ( ( ) => {
171265 if ( ! engine ) {
@@ -330,7 +424,88 @@ export function App(props) {
330424 examples = { EXAMPLES }
331425 defaultExample = { DEFAULT_EXAMPLE }
332426 onExampleChange = { handleExampleChange }
333- />
427+ >
428+ { isDagLayout ? (
429+ < section style = { { marginBottom : '0.5rem' , fontSize : '0.875rem' , lineHeight : 1.5 } } >
430+ < h3 style = { { margin : '0 0 0.5rem' , fontSize : '0.875rem' , fontWeight : 600 , color : '#0f172a' } } >
431+ Collapsed chains
432+ </ h3 >
433+ < p style = { { margin : '0 0 0.75rem' , color : '#334155' } } >
434+ Linear chains collapse to a single node marked with plus and minus icons. Use these controls to
435+ expand or collapse all chains. Individual chains remain interactive on the canvas.
436+ </ p >
437+ < div style = { { display : 'flex' , flexDirection : 'column' , gap : '0.5rem' } } >
438+ < div
439+ style = { {
440+ display : 'flex' ,
441+ justifyContent : 'space-between' ,
442+ fontSize : '0.8125rem' ,
443+ color : '#475569'
444+ } }
445+ >
446+ < span > Status</ span >
447+ < span >
448+ { collapsedChainCount } / { totalChainCount } collapsed
449+ </ span >
450+ </ div >
451+ < div style = { { display : 'flex' , gap : '0.5rem' , flexWrap : 'wrap' } } >
452+ < button
453+ type = "button"
454+ onClick = { handleToggleCollapseEnabled }
455+ style = { {
456+ background : collapseEnabled ? '#4c6ef5' : '#1f2937' ,
457+ color : '#ffffff' ,
458+ border : 'none' ,
459+ borderRadius : '0.375rem' ,
460+ padding : '0.375rem 0.75rem' ,
461+ cursor : 'pointer' ,
462+ fontFamily : 'inherit' ,
463+ fontSize : '0.8125rem'
464+ } }
465+ >
466+ { collapseEnabled ? 'Disable collapse' : 'Enable collapse' }
467+ </ button >
468+ < button
469+ type = "button"
470+ onClick = { handleCollapseAll }
471+ disabled = { collapseAllDisabled }
472+ style = { {
473+ background : '#2563eb' ,
474+ color : '#ffffff' ,
475+ border : 'none' ,
476+ borderRadius : '0.375rem' ,
477+ padding : '0.375rem 0.75rem' ,
478+ cursor : collapseAllDisabled ? 'not-allowed' : 'pointer' ,
479+ fontFamily : 'inherit' ,
480+ fontSize : '0.8125rem' ,
481+ opacity : collapseAllDisabled ? 0.5 : 1
482+ } }
483+ >
484+ Collapse all
485+ </ button >
486+ < button
487+ type = "button"
488+ onClick = { handleExpandAll }
489+ disabled = { expandAllDisabled }
490+ style = { {
491+ background : '#16a34a' ,
492+ color : '#ffffff' ,
493+ border : 'none' ,
494+ borderRadius : '0.375rem' ,
495+ padding : '0.375rem 0.75rem' ,
496+ cursor : expandAllDisabled ? 'not-allowed' : 'pointer' ,
497+ fontFamily : 'inherit' ,
498+ fontSize : '0.8125rem' ,
499+ opacity : expandAllDisabled ? 0.5 : 1
500+ } }
501+ >
502+ Expand all
503+ </ button >
504+ </ div >
505+ </ div >
506+ </ section >
507+ ) : null }
508+ </ ControlPanel >
334509 </ aside >
335510 </ div >
336511 ) ;
0 commit comments