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