@@ -86,6 +86,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
86
86
/>
87
87
</g >
88
88
</g >
89
+ <g v-if =" groupCycle" class =" graph-subgraph-container" >
90
+ <g v-for =" (subgraph, key) in subgraphs"
91
+ :key =" key" >
92
+ <GraphSubgraph
93
+ v-if =" subgraph.label!='margin'"
94
+ :subgraph =" subgraph" />
95
+ </g >
96
+ </g >
89
97
</g >
90
98
</svg >
91
99
</div >
@@ -105,6 +113,7 @@ import {
105
113
import SubscriptionQuery from ' @/model/SubscriptionQuery.model'
106
114
// import CylcTreeCallback from '@/services/treeCallback'
107
115
import GraphNode from ' @/components/cylc/GraphNode.vue'
116
+ import GraphSubgraph from ' @/components/cylc/GraphSubgraph.vue'
108
117
import ViewToolbar from ' @/components/cylc/ViewToolbar.vue'
109
118
import {
110
119
posToPath ,
@@ -118,7 +127,8 @@ import {
118
127
mdiArrowCollapse ,
119
128
mdiArrowExpand ,
120
129
mdiRefresh ,
121
- mdiFileRotateRight
130
+ mdiFileRotateRight ,
131
+ mdiVectorSelection
122
132
} from ' @mdi/js'
123
133
124
134
// NOTE: Use TaskProxies not nodesEdges{nodes} to list nodes as this is what
@@ -219,6 +229,7 @@ export default {
219
229
220
230
components: {
221
231
GraphNode,
232
+ GraphSubgraph,
222
233
ViewToolbar
223
234
},
224
235
@@ -251,11 +262,19 @@ export default {
251
262
*/
252
263
const spacing = useInitialOptions (' spacing' , { props, emit }, 1.5 )
253
264
265
+ /**
266
+ * The group by cycle point toggle state.
267
+ * If true the graph nodes will be grouped by cycle point
268
+ * @type {import('vue').Ref<boolean>}
269
+ */
270
+ const groupCycle = useInitialOptions (' groupCycle' , { props, emit }, false )
271
+
254
272
return {
255
273
jobTheme: useJobTheme (),
256
274
transpose,
257
275
autoRefresh,
258
- spacing
276
+ spacing,
277
+ groupCycle
259
278
}
260
279
},
261
280
@@ -268,6 +287,7 @@ export default {
268
287
// the nodes end edges we render to the graph
269
288
graphNodes: [],
270
289
graphEdges: [],
290
+ subgraphs: {},
271
291
// the svg transformations to apply to each node to apply the layout
272
292
// generated by graphviz
273
293
nodeTransformations: {},
@@ -361,6 +381,13 @@ export default {
361
381
icon: mdiArrowCollapse,
362
382
action: ' callback' ,
363
383
callback: this .decreaseSpacing
384
+ },
385
+ {
386
+ title: ' Group by cycle point' ,
387
+ icon: mdiVectorSelection,
388
+ action: ' toggle' ,
389
+ value: this .groupCycle ,
390
+ key: ' groupCycle'
364
391
}
365
392
]
366
393
}
@@ -483,7 +510,20 @@ export default {
483
510
}
484
511
return ret
485
512
},
486
- getDotCode (nodeDimensions , nodes , edges ) {
513
+ /**
514
+ * Get the nodes binned by cycle point
515
+ *
516
+ * @param {Object[]} nodes
517
+ * @returns {{ [dateTime: string]: Object[] nodes }} mapping of node to their cycle point.
518
+ */
519
+ getCycles (nodes ) {
520
+ if (! this .groupCycle ) return
521
+ return nodes .reduce ((x , y ) => {
522
+ (x[y .node .cyclePoint ] || = []).push (y)
523
+ return x
524
+ }, {})
525
+ },
526
+ getDotCode (nodeDimensions , nodes , edges , cycles ) {
487
527
// return GraphViz dot code for the given nodes, edges and dimensions
488
528
const ret = [' digraph {' ]
489
529
let spacing = this .spacing
@@ -526,6 +566,27 @@ export default {
526
566
]
527
567
` )
528
568
}
569
+
570
+ if (this .groupCycle ) {
571
+ // Loop over the subgraphs
572
+ Object .keys (cycles).forEach ((key , i ) => {
573
+ // Loop over the nodes that are included in the subraph
574
+ const nodeFormattedArray = cycles[key].map (a => ` "${ a .id } "` )
575
+ ret .push (`
576
+ subgraph cluster_margin_${ i}
577
+ {
578
+ margin=100.0
579
+ label="margin"
580
+ subgraph cluster_${ i} {${ nodeFormattedArray} ;\n
581
+ label = "${ key} ";\n
582
+ fontsize = "70px"
583
+ style=dashed
584
+ margin=60.0
585
+ }
586
+ }` )
587
+ })
588
+ }
589
+
529
590
if (this .transpose ) {
530
591
// left-right orientation
531
592
// route edges from anywhere on the node of the source task to anywhere
@@ -542,6 +603,8 @@ export default {
542
603
}
543
604
}
544
605
ret .push (' }' )
606
+ console .log (" ret.join('\n ')" )
607
+ console .log (ret .join (' \n ' ))
545
608
return ret .join (' \n ' )
546
609
},
547
610
hashGraph (nodes , edges ) {
@@ -602,6 +665,8 @@ export default {
602
665
return
603
666
}
604
667
668
+ const cycles = this .getCycles (nodes)
669
+
605
670
// compute the graph ID
606
671
const graphID = this .hashGraph (nodes, edges)
607
672
if (this .graphID === graphID) {
@@ -646,7 +711,7 @@ export default {
646
711
647
712
// layout the graph
648
713
try {
649
- await this .layout (nodes, edges, nodeDimensions)
714
+ await this .layout (nodes, edges, nodeDimensions, cycles )
650
715
} catch (e) {
651
716
// something went wrong, allow the layout to retry later
652
717
this .graphID = null
@@ -689,26 +754,42 @@ export default {
689
754
* @param {Object[]} edges
690
755
* @param {{ [id: string]: SVGRect }} nodeDimensions
691
756
*/
692
- async layout (nodes , edges , nodeDimensions ) {
757
+ async layout (nodes , edges , nodeDimensions , cycles ) {
693
758
// generate the GraphViz dot code
694
- const dotCode = this .getDotCode (nodeDimensions, nodes, edges)
759
+ const dotCode = this .getDotCode (nodeDimensions, nodes, edges, cycles )
695
760
696
761
// run the layout algorithm
697
762
const jsonString = (await this .graphviz ).layout (dotCode, ' json' )
698
763
const json = JSON .parse (jsonString)
699
764
765
+ this .subgraphs = {}
700
766
// update graph node positions
701
767
for (const obj of json .objects ) {
702
- const [x , y ] = obj .pos .split (' ,' )
703
- const bbox = nodeDimensions[obj .name ]
704
- // translations:
705
- // 1. The graphviz node coordinates
706
- // 2. Centers the node on this coordinate
707
- // TODO convert (2) to maths OR fix it to avoid recomputation?
708
- this .nodeTransformations [obj .name ] = `
768
+ if (obj .bb ) {
769
+ // if the object is a subgraph
770
+ const [left , bottom , right , top ] = obj .bb .split (' ,' )
771
+ this .subgraphs [obj .name ] = {
772
+ x: left,
773
+ y: - top,
774
+ width: right - left,
775
+ height: top - bottom,
776
+ label: obj .label
777
+ }
778
+ } else {
779
+ console .log (" obj.name" )
780
+ console .log (obj .name )
781
+ // else the object is a node
782
+ const [x , y ] = obj .pos .split (' ,' )
783
+ const bbox = nodeDimensions[obj .name ]
784
+ // translations:
785
+ // 1. The graphviz node coordinates
786
+ // 2. Centers the node on this coordinate
787
+ // TODO convert (2) to maths OR fix it to avoid recomputation?
788
+ this .nodeTransformations [obj .name ] = `
709
789
translate(${ x} , -${ y} )
710
790
translate(-${ bbox .width / 2 } , -${ bbox .height / 2 } )
711
791
`
792
+ }
712
793
}
713
794
// update edge paths
714
795
this .graphEdges = json .edges ? .map (edge => posToPath (edge .pos )) ?? []
@@ -741,6 +822,11 @@ export default {
741
822
if (! this .autoRefresh ) {
742
823
this .updateTimer ()
743
824
}
825
+ },
826
+ groupCycle () {
827
+ // refresh the graph when group by cycle point option is changed
828
+ this .graphID = null
829
+ this .refresh ()
744
830
}
745
831
}
746
832
}
0 commit comments