1   /****************************************************************************
2    * This demo file is part of yFiles for Java 2.14.
3    * Copyright (c) 2000-2017 by yWorks GmbH, Vor dem Kreuzberg 28,
4    * 72070 Tuebingen, Germany. All rights reserved.
5    * 
6    * yFiles demo files exhibit yFiles for Java functionalities. Any redistribution
7    * of demo files in source code or binary form, with or without
8    * modification, is not permitted.
9    * 
10   * Owners of a valid software license for a yFiles for Java version that this
11   * demo is shipped with are allowed to use the demo source code as basis
12   * for their own yFiles for Java powered applications. Use of such programs is
13   * governed by the rights and conditions as set out in the yFiles for Java
14   * license agreement.
15   * 
16   * THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY EXPRESS OR IMPLIED
17   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18   * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
19   * NO EVENT SHALL yWorks BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
21   * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22   * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23   * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24   * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26   *
27   ***************************************************************************/
28  package demo.layout.hierarchic;
29  
30  import demo.view.DemoBase;
31  import y.base.DataProvider;
32  import y.base.Edge;
33  import y.base.EdgeCursor;
34  import y.base.EdgeMap;
35  import y.base.Node;
36  import y.base.NodeCursor;
37  import y.geom.YPoint;
38  import y.layout.AbstractLayoutStage;
39  import y.layout.LayoutGraph;
40  import y.layout.LayoutOrientation;
41  import y.layout.Layouter;
42  import y.layout.PortConstraint;
43  import y.layout.hierarchic.AsIsLayerer;
44  import y.layout.hierarchic.IncrementalHierarchicLayouter;
45  import y.layout.hierarchic.incremental.AsIsSequencer;
46  import y.layout.labeling.SALabeling;
47  import y.util.Maps;
48  import y.view.Graph2D;
49  import y.view.Graph2DLayoutExecutor;
50  
51  import javax.swing.AbstractAction;
52  import javax.swing.JCheckBox;
53  import javax.swing.JToolBar;
54  import javax.swing.event.ChangeEvent;
55  import javax.swing.event.ChangeListener;
56  import java.awt.EventQueue;
57  import java.awt.event.ActionEvent;
58  import java.util.Locale;
59  
60  /**
61   * This demo demonstrates how a Sankey diagram can be produced by means of {@link IncrementalHierarchicLayouter}.
62   * <p>
63   *   Sankey diagrams are used for visualizing flow information, e.g. energy, material or cost transfers. The 
64   *   thickness of the edges is proportional to the flow quantity and help identifying the dominant flows and how much 
65   *   each edge contributes to the overall flow.
66   * </p>
67   * <p>
68   *   The example diagram included in this demo visualizes a voters' migration flow over three elections
69   *   (the three columns of the diagram). The flow is depicted from left to right.
70   * </p>
71   * <p>
72   *   The user can select from the checkbox in the toolbar whether or not the layout algorithm should use the existing 
73   *   drawing as sketch for the next layout run. If not selected, the layout algorithm reduces the number of
74   *   edge crossings.
75   * </p>
76   * 
77   */
78  public class SankeyDemo extends DemoBase {
79  
80    private boolean fromSketchEnabled = true;
81    
82    public SankeyDemo(){
83      this(null);
84    }
85  
86    public SankeyDemo(final String helpFilePath) {
87      addHelpPane(helpFilePath);
88      loadGraph("resource/voter-migration.graphml");
89      doLayout();
90    } 
91  
92    protected JToolBar createToolBar() {
93      final JToolBar toolBar = super.createToolBar();
94      toolBar.addSeparator();
95  
96      final JCheckBox fromSketch = new JCheckBox("Use drawing as sketch");
97      fromSketch.setSelected(true);
98      fromSketch.addChangeListener(new ChangeListener() {
99        public void stateChanged(ChangeEvent e) {
100         fromSketchEnabled = !fromSketchEnabled;
101       }
102     });
103     toolBar.add(fromSketch);
104 
105     toolBar.add(createActionControl(new AbstractAction(
106         "Layout", SHARED_LAYOUT_ICON) {
107       public void actionPerformed(ActionEvent e) {
108         doLayout();
109       }
110     }));
111     
112     return toolBar;
113   }
114 
115   /**
116    * Configures and runs the layout algorithm for Sankey diagrams.
117    */
118   private void doLayout() {
119     final Graph2D graph = view.getGraph2D();
120     final EdgeMap thicknessMap = Maps.createHashedEdgeMap();
121     for (EdgeCursor ec = graph.edges(); ec.ok(); ec.next()) {
122       final Edge edge = ec.edge();
123       // edge thickness is determined by the line width also stored in edge realizer
124       final double thickness = graph.getRealizer(edge).getLineType().getLineWidth();
125       thicknessMap.setDouble(edge, thickness);
126     }
127     graph.addDataProvider(IncrementalHierarchicLayouter.EDGE_THICKNESS_DPKEY, thicknessMap);
128 
129     // configures the layout algorithm
130     final IncrementalHierarchicLayouter layouter = new IncrementalHierarchicLayouter();
131     final byte layoutOrientation = LayoutOrientation.LEFT_TO_RIGHT;
132     layouter.setLayoutOrientation(layoutOrientation);
133     layouter.setNodeToNodeDistance(50);
134     layouter.getEdgeLayoutDescriptor().setMinimumFirstSegmentLength(150);
135     layouter.getEdgeLayoutDescriptor().setMinimumLastSegmentLength(150);
136     if (fromSketchEnabled) {
137       layouter.setFromScratchLayerer(new AsIsLayerer());
138       layouter.setFromScratchSequencer(new AsIsSequencer());
139     }
140     
141     // a port border gap ratio of zero means that ports can be placed directly on the corners of the nodes
142     final double portBorderRatio = 0;
143     layouter.getNodeLayoutDescriptor().setPortBorderGapRatios(portBorderRatio);
144     
145     // configures the labeling algorithm to save space
146     final SALabeling labelLayouter = (SALabeling) layouter.getLabelLayouter();
147     labelLayouter.setPlaceNodeLabels(false);
148     labelLayouter.setRemoveNodeOverlaps(true);
149     layouter.setLabelLayouterEnabled(true);
150 
151     // for sankey diagrams, the nodes should be adjusted to the incoming/outgoing flow (enlarged if necessary)
152     // -> use NodeResizingStage for that purpose
153     final NodeResizingStage nodeResizingStage = new NodeResizingStage(layouter);
154     nodeResizingStage.setLayoutOrientation(layoutOrientation);
155     nodeResizingStage.setPortBorderGapRatio(portBorderRatio);
156 
157     // executes the layout
158     final Graph2DLayoutExecutor executor = new Graph2DLayoutExecutor();
159     executor.doLayout(view, nodeResizingStage);
160 
161     graph.removeDataProvider(IncrementalHierarchicLayouter.EDGE_THICKNESS_DPKEY);
162   }
163 
164   /**
165    * LayoutStage that ensures that the size of the nodes is large enough such that
166    * all edges can be placed without overlaps.
167    */
168   private static class NodeResizingStage extends AbstractLayoutStage {
169     private double minimumPortDistance;
170     private double portBorderGapRatio;
171     private byte layoutOrientation;
172 
173     public NodeResizingStage(Layouter coreLayouter) {
174       super(coreLayouter);
175     }
176 
177     /**
178      * Returns the main orientation of the layout. Should be the same value as for the associated core layout
179      * algorithm.
180      */
181     public byte getLayoutOrientation() {
182       return layoutOrientation;
183     }
184 
185     /**
186      * Specifies the main orientation of the layout. Should be the same value as for the associated core layout
187      * algorithm.
188      *
189      * @param layoutOrientation one of the default layout orientations
190      */
191     public void setLayoutOrientation(byte layoutOrientation) {
192       this.layoutOrientation = layoutOrientation;
193     }
194 
195     /**
196      * Returns the port border gap ratio for the port distribution at the sides of the nodes.
197      * Should be the same value as for the associated core layout algorithm.
198      */
199     public double getPortBorderGapRatio() {
200       return portBorderGapRatio;
201     }
202 
203     /**
204      * Specifies the port border gap ratio for the port distribution at the sides of the nodes. Should be the same value
205      * as for the associated core layout algorithm.
206      *
207      * @param portBorderGapRatio the given ratio
208      */
209     public void setPortBorderGapRatio(double portBorderGapRatio) {
210       this.portBorderGapRatio = portBorderGapRatio;
211     }
212 
213     /**
214      * Returns the minimum distance between two ports on the same node side.
215      */
216     public double getMinimumPortDistance() {
217       return minimumPortDistance;
218     }
219 
220     /**
221      * Specifies the minimum distance between two ports on the same node side.
222      *
223      * @param minimumPortDistance the minimum distance
224      */
225     public void setMinimumPortDistance(double minimumPortDistance) {
226       this.minimumPortDistance = minimumPortDistance;
227     }
228 
229     public boolean canLayout(LayoutGraph graph) {
230       return canLayoutCore(graph);
231     }
232 
233     public void doLayout(LayoutGraph graph) {
234       for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
235         //adjust the node size
236         adjustNodeSizes(nc.node(), graph);
237       }
238 
239       doLayoutCore(graph);
240     }
241 
242     private void adjustNodeSizes(Node node, LayoutGraph graph) {
243       double width = graph.getWidth(node);
244       double height = graph.getHeight(node);
245 
246       final double inEdgeSpace = calcRequiredSpace(node.inEdges(), graph);
247       final double outEdgeSpace = calcRequiredSpace(node.outEdges(), graph);
248       if (layoutOrientation == LayoutOrientation.TOP_TO_BOTTOM
249           || layoutOrientation == LayoutOrientation.BOTTOM_TO_TOP) {
250         //we have to enlarge the width such that the in-/out-edges can be placed side by side without overlaps
251         width = Math.max(width, inEdgeSpace);
252         width = Math.max(width, outEdgeSpace);
253       } else {
254         //we have to enlarge the height such that the in-/out-edges can be placed side by side without overlaps
255         height = Math.max(height, inEdgeSpace);
256         height = Math.max(height, outEdgeSpace);
257       }
258 
259       // adjust size for edges with strong port constraints
260       final DataProvider edgeThicknessDP = graph.getDataProvider(IncrementalHierarchicLayouter.EDGE_THICKNESS_DPKEY);
261       if (edgeThicknessDP != null) {
262         for (EdgeCursor ec = node.edges(); ec.ok(); ec.next()) {
263           final Edge edge = ec.edge();
264           final double thickness = edgeThicknessDP.getDouble(edge);
265 
266           final PortConstraint spc = PortConstraint.getSPC(graph, edge);
267           if (edge.source() == node && spc != null && spc.isStrong()) {
268             final YPoint sourcePoint = graph.getSourcePointRel(edge);
269             width = Math.max(width, Math.abs(sourcePoint.getX()) * 2 + thickness);
270             height = Math.max(height, Math.abs(sourcePoint.getY()) * 2 + thickness);
271           }
272 
273           final PortConstraint tpc = PortConstraint.getTPC(graph, edge);
274           if (edge.target() == node && tpc != null && tpc.isStrong()) {
275             final YPoint targetPoint = graph.getTargetPointRel(edge);
276             width = Math.max(width, Math.abs(targetPoint.getX()) * 2 + thickness);
277             height = Math.max(height, Math.abs(targetPoint.getY()) * 2 + thickness);
278           }
279         }
280       }
281 
282       graph.setSize(node, width, height);
283     }
284 
285     /**
286      * Calculates the space required when placing the given edge side by side without overlaps and considering
287      * the specified minimum port distance and edge thickness.
288      */
289     private double calcRequiredSpace(EdgeCursor edgesOnSideCur, LayoutGraph graph) {
290       double requiredSpace = 0;
291       final DataProvider edgeThicknessDP = graph.getDataProvider(IncrementalHierarchicLayouter.EDGE_THICKNESS_DPKEY);
292       for (EdgeCursor ec = edgesOnSideCur; ec.ok(); ec.next()) {
293         final Edge edge = ec.edge();
294         final double thickness = (edgeThicknessDP == null) ? 0 : edgeThicknessDP.getDouble(edge);
295         requiredSpace += Math.max(thickness, 1);
296       }
297       requiredSpace += (edgesOnSideCur.size() - 1) * getMinimumPortDistance();
298       requiredSpace += 2 * getPortBorderGapRatio() * getMinimumPortDistance();
299       return requiredSpace;
300     }
301   }
302 
303   /**
304    * Launches the demo.
305    */
306   public static void main(String[] args) {
307     EventQueue.invokeLater(new Runnable() {
308       public void run() {
309         Locale.setDefault(Locale.ENGLISH);
310         initLnF();
311         (new SankeyDemo("resource/sankeydemohelp.html")).start("Sankey Demo");
312       }
313     });
314   }
315 }
316