001/* ======================================================
002 * JFreeChart : a chart library for the Java(tm) platform
003 * ======================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert and Contributors.
006 *
007 * Project Info:  https://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * -------------
028 * FlowPlot.java
029 * -------------
030 * (C) Copyright 2021-present, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   -;
034 */
035
036package org.jfree.chart.plot.flow;
037
038import java.awt.AlphaComposite;
039import java.awt.Color;
040import java.awt.Composite;
041import java.awt.Font;
042import java.awt.GradientPaint;
043import java.awt.Graphics2D;
044import java.awt.Paint;
045import java.awt.geom.Path2D;
046import java.awt.geom.Point2D;
047import java.awt.geom.Rectangle2D;
048import java.io.Serializable;
049import java.util.ArrayList;
050import java.util.HashMap;
051import java.util.List;
052import java.util.Map;
053import java.util.Objects;
054import org.jfree.chart.entity.EntityCollection;
055import org.jfree.chart.entity.FlowEntity;
056import org.jfree.chart.entity.NodeEntity;
057import org.jfree.chart.labels.FlowLabelGenerator;
058import org.jfree.chart.labels.StandardFlowLabelGenerator;
059import org.jfree.chart.plot.Plot;
060import org.jfree.chart.plot.PlotRenderingInfo;
061import org.jfree.chart.plot.PlotState;
062import org.jfree.chart.text.TextUtils;
063import org.jfree.chart.ui.RectangleInsets;
064import org.jfree.chart.ui.TextAnchor;
065import org.jfree.chart.ui.VerticalAlignment;
066import org.jfree.chart.util.Args;
067import org.jfree.chart.util.PaintUtils;
068import org.jfree.chart.util.PublicCloneable;
069import org.jfree.data.flow.FlowDataset;
070import org.jfree.data.flow.FlowDatasetUtils;
071import org.jfree.data.flow.FlowKey;
072import org.jfree.data.flow.NodeKey;
073
074/**
075 * A plot for visualising flows defined in a {@link FlowDataset}.  This enables
076 * the production of a type of Sankey chart.  The example shown here is 
077 * produced by the {@code FlowPlotDemo1.java} program included in the JFreeChart 
078 * Demo Collection:
079 * <img src="doc-files/FlowPlotDemo1.svg" width="600" height="400" alt="FlowPlotDemo1.svg">
080 * 
081 * @since 1.5.3
082 */
083public class FlowPlot extends Plot implements Cloneable, PublicCloneable, 
084        Serializable {
085
086    /** The source of data. */
087    private FlowDataset dataset;
088    
089    /** 
090     * The node width in Java 2D user-space units.
091     */
092    private double nodeWidth = 20.0;
093    
094    /** The gap between nodes (expressed as a percentage of the plot height). */
095    private double nodeMargin = 0.01;
096
097    /** 
098     * The percentage of the plot width to assign to a gap between the nodes
099     * and the flow representation. 
100     */
101    private double flowMargin = 0.005;
102    
103    /** 
104     * Stores colors for specific nodes - if there isn't a color in here for
105     * the node, the default node color will be used (unless the color swatch
106     * is active).
107     */
108    private Map<NodeKey, Color> nodeColorMap;
109
110    /** Node colors. */
111    private List<Color> nodeColorSwatch;
112    
113    /** A pointer into the color swatch. */
114    private int nodeColorSwatchPointer = 0;
115
116    /** The default node color if nothing is defined in the nodeColorMap. */
117    private Color defaultNodeColor;
118
119    /** Default node label font. */
120    private Font defaultNodeLabelFont;
121
122    /** Default node label paint. */
123    private Paint defaultNodeLabelPaint;
124
125    /** Default node label alignment. */
126    private VerticalAlignment nodeLabelAlignment;
127    
128    /** The x-offset for node labels. */
129    private double nodeLabelOffsetX;
130    
131    /** The y-offset for node labels. */
132    private double nodeLabelOffsetY;
133    
134    /** The tool tip generator - if null, no tool tips will be displayed. */
135    private FlowLabelGenerator toolTipGenerator; 
136    
137    /**
138     * Creates a new instance that will source data from the specified dataset.
139     * 
140     * @param dataset  the dataset. 
141     */
142    public FlowPlot(FlowDataset dataset) {
143        super();
144        this.dataset = dataset;
145        if (dataset != null) {
146            dataset.addChangeListener(this);
147        }
148        this.nodeColorMap = new HashMap<>();
149        this.nodeColorSwatch = new ArrayList<>();
150        this.defaultNodeColor = Color.GRAY;
151        this.defaultNodeLabelFont = new Font(Font.DIALOG, Font.BOLD, 12);
152        this.defaultNodeLabelPaint = Color.BLACK;
153        this.nodeLabelAlignment = VerticalAlignment.CENTER;
154        this.nodeLabelOffsetX = 2.0;
155        this.nodeLabelOffsetY = 2.0;
156        this.toolTipGenerator = new StandardFlowLabelGenerator();
157    }
158
159    /**
160     * Returns a string identifying the plot type.
161     * 
162     * @return A string identifying the plot type.
163     */
164    @Override
165    public String getPlotType() {
166        return "FlowPlot";
167    }
168
169    /**
170     * Returns a reference to the dataset.
171     * 
172     * @return A reference to the dataset (possibly {@code null}).
173     */
174    public FlowDataset getDataset() {
175        return this.dataset;
176    }
177
178    /**
179     * Sets the dataset for the plot and sends a change notification to all
180     * registered listeners.
181     * 
182     * @param dataset  the dataset ({@code null} permitted). 
183     */
184    public void setDataset(FlowDataset dataset) {
185        this.dataset = dataset;
186        fireChangeEvent();
187    }
188
189    /**
190     * Returns the node margin (expressed as a percentage of the available
191     * plotting space) which is the gap between nodes (sources or destinations).
192     * The initial (default) value is {@code 0.01} (1 percent).
193     * 
194     * @return The node margin. 
195     */
196    public double getNodeMargin() {
197        return this.nodeMargin;
198    }
199
200    /**
201     * Sets the node margin and sends a change notification to all registered
202     * listeners.
203     * 
204     * @param margin  the margin (expressed as a percentage). 
205     */
206    public void setNodeMargin(double margin) {
207        Args.requireNonNegative(margin, "margin");
208        this.nodeMargin = margin;
209        fireChangeEvent();
210    }
211    
212
213    /**
214     * Returns the flow margin.  This determines the gap between the graphic 
215     * representation of the nodes (sources and destinations) and the curved
216     * flow representation.  This is expressed as a percentage of the plot 
217     * width so that it remains proportional as the plot is resized.  The
218     * initial (default) value is {@code 0.005} (0.5 percent).
219     * 
220     * @return The flow margin. 
221     */
222    public double getFlowMargin() {
223        return this.flowMargin;
224    }
225    
226    /**
227     * Sets the flow margin and sends a change notification to all registered
228     * listeners.
229     * 
230     * @param margin  the margin (must be 0.0 or higher).
231     */
232    public void setFlowMargin(double margin) {
233        Args.requireNonNegative(margin, "margin");
234        this.flowMargin = margin;
235        fireChangeEvent();
236    }
237
238    /**
239     * Returns the width of the source and destination nodes, expressed in 
240     * Java2D user-space units.  The initial (default) value is {@code 20.0}.
241     * 
242     * @return The width. 
243     */
244    public double getNodeWidth() {
245        return this.nodeWidth;
246    }
247
248    /**
249     * Sets the width for the source and destination nodes and sends a change
250     * notification to all registered listeners.
251     * 
252     * @param width  the width. 
253     */
254    public void setNodeWidth(double width) {
255        this.nodeWidth = width;
256        fireChangeEvent();
257    }
258
259    /**
260     * Returns the list of colors that will be used to auto-populate the node
261     * colors when they are first rendered.  If the list is empty, no color 
262     * will be assigned to the node so, unless it is manually set, the default
263     * color will apply.  This method returns a copy of the list, modifying
264     * the returned list will not affect the plot.
265     * 
266     * @return The list of colors (possibly empty, but never {@code null}). 
267     */
268    public List<Color> getNodeColorSwatch() {
269        return new ArrayList<>(this.nodeColorSwatch);
270    }
271
272    /**
273     * Sets the color swatch for the plot.
274     * 
275     * @param colors  the list of colors ({@code null} not permitted). 
276     */
277    public void setNodeColorSwatch(List<Color> colors) {
278        Args.nullNotPermitted(colors, "colors");
279        this.nodeColorSwatch = colors;
280        
281    }
282    
283    /**
284     * Returns the fill color for the specified node.
285     * 
286     * @param nodeKey  the node key ({@code null} not permitted).
287     * 
288     * @return The fill color (possibly {@code null}).
289     */
290    public Color getNodeFillColor(NodeKey nodeKey) {
291        return this.nodeColorMap.get(nodeKey);
292    }
293    
294    /**
295     * Sets the fill color for the specified node and sends a change 
296     * notification to all registered listeners.
297     * 
298     * @param nodeKey  the node key ({@code null} not permitted).
299     * @param color  the fill color ({@code null} permitted).
300     */
301    public void setNodeFillColor(NodeKey nodeKey, Color color) {
302        this.nodeColorMap.put(nodeKey, color);
303        fireChangeEvent();
304    }
305    
306    /**
307     * Returns the default node color.  This is used when no specific node color
308     * has been specified.  The initial (default) value is {@code Color.GRAY}.
309     * 
310     * @return The default node color (never {@code null}). 
311     */
312    public Color getDefaultNodeColor() {
313        return this.defaultNodeColor;
314    }
315    
316    /**
317     * Sets the default node color and sends a change event to registered
318     * listeners.
319     * 
320     * @param color  the color ({@code null} not permitted). 
321     */
322    public void setDefaultNodeColor(Color color) {
323        Args.nullNotPermitted(color, "color");
324        this.defaultNodeColor = color;
325        fireChangeEvent();
326    }
327
328    /**
329     * Returns the default font used to display labels for the source and
330     * destination nodes.  The initial (default) value is 
331     * {@code Font(Font.DIALOG, Font.BOLD, 12)}.
332     * 
333     * @return The default font (never {@code null}). 
334     */
335    public Font getDefaultNodeLabelFont() {
336        return this.defaultNodeLabelFont;
337    }
338
339    /**
340     * Sets the default font used to display labels for the source and
341     * destination nodes and sends a change notification to all registered
342     * listeners.
343     * 
344     * @param font  the font ({@code null} not permitted). 
345     */
346    public void setDefaultNodeLabelFont(Font font) {
347        Args.nullNotPermitted(font, "font");
348        this.defaultNodeLabelFont = font;
349        fireChangeEvent();
350    }
351
352    /**
353     * Returns the default paint used to display labels for the source and
354     * destination nodes.  The initial (default) value is {@code Color.BLACK}.
355     * 
356     * @return The default paint (never {@code null}). 
357     */
358    public Paint getDefaultNodeLabelPaint() {
359        return this.defaultNodeLabelPaint;
360    }
361
362    /**
363     * Sets the default paint used to display labels for the source and
364     * destination nodes and sends a change notification to all registered
365     * listeners.
366     * 
367     * @param paint  the paint ({@code null} not permitted). 
368     */
369    public void setDefaultNodeLabelPaint(Paint paint) {
370        Args.nullNotPermitted(paint, "paint");
371        this.defaultNodeLabelPaint = paint;
372        fireChangeEvent();
373    }
374
375    /**
376     * Returns the vertical alignment of the node labels relative to the node.
377     * The initial (default) value is {@link VerticalAlignment#CENTER}.
378     * 
379     * @return The alignment (never {@code null}). 
380     */
381    public VerticalAlignment getNodeLabelAlignment() {
382        return this.nodeLabelAlignment;
383    }
384    
385    /**
386     * Sets the vertical alignment of the node labels and sends a change 
387     * notification to all registered listeners.
388     * 
389     * @param alignment  the new alignment ({@code null} not permitted). 
390     */
391    public void setNodeLabelAlignment(VerticalAlignment alignment) {
392        Args.nullNotPermitted(alignment, "alignment");
393        this.nodeLabelAlignment = alignment;
394        fireChangeEvent();
395    }
396    
397    /**
398     * Returns the x-offset for the node labels.
399     * 
400     * @return The x-offset for the node labels.
401     */
402    public double getNodeLabelOffsetX() {
403        return this.nodeLabelOffsetX;
404    }
405
406    /**
407     * Sets the x-offset for the node labels and sends a change notification
408     * to all registered listeners.
409     * 
410     * @param offsetX  the node label x-offset in Java2D units.
411     */
412    public void setNodeLabelOffsetX(double offsetX) {
413        this.nodeLabelOffsetX = offsetX;
414        fireChangeEvent();
415    }
416
417    /**
418     * Returns the y-offset for the node labels.
419     * 
420     * @return The y-offset for the node labels.
421     */
422    public double getNodeLabelOffsetY() {
423        return nodeLabelOffsetY;
424    }
425
426    /**
427     * Sets the y-offset for the node labels and sends a change notification
428     * to all registered listeners.
429     * 
430     * @param offsetY  the node label y-offset in Java2D units.
431     */
432    public void setNodeLabelOffsetY(double offsetY) {
433        this.nodeLabelOffsetY = offsetY;
434        fireChangeEvent();
435    }
436
437    /**
438     * Returns the tool tip generator that creates the strings that are 
439     * displayed as tool tips for the flows displayed in the plot.
440     * 
441     * @return The tool tip generator (possibly {@code null}). 
442     */
443    public FlowLabelGenerator getToolTipGenerator() {
444        return this.toolTipGenerator;
445    }
446    
447    /**
448     * Sets the tool tip generator and sends a change notification to all
449     * registered listeners.  If the generator is set to {@code null}, no tool 
450     * tips will be displayed for the flows.
451     * 
452     * @param generator  the new generator ({@code null} permitted). 
453     */
454    public void setToolTipGenerator(FlowLabelGenerator generator) {
455        this.toolTipGenerator = generator;
456        fireChangeEvent();
457    }
458
459    /**
460     * Draws the flow plot within the specified area of the supplied graphics
461     * target {@code g2}.
462     * 
463     * @param g2  the graphics target ({@code null} not permitted).
464     * @param area  the plot area ({@code null} not permitted).
465     * @param anchor  the anchor point (ignored).
466     * @param parentState  the parent state (ignored).
467     * @param info  the plot rendering info.
468     */
469    @Override
470    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, PlotState parentState, PlotRenderingInfo info) {
471        Args.nullNotPermitted(g2, "g2");
472        Args.nullNotPermitted(area, "area");
473 
474        EntityCollection entities = null;
475        if (info != null) {
476            info.setPlotArea(area);
477            entities = info.getOwner().getEntityCollection();
478        }
479        RectangleInsets insets = getInsets();
480        insets.trim(area);
481        if (info != null) {
482            info.setDataArea(area);
483        }
484        
485        // use default JFreeChart background handling
486        drawBackground(g2, area);
487
488        // we need to ensure there is space to show all the inflows and all 
489        // the outflows at each node group, so first we calculate the max
490        // flow space required - for each node in the group, consider the 
491        // maximum of the inflow and the outflow
492        double flow2d = Double.POSITIVE_INFINITY;
493        double nodeMargin2d = this.nodeMargin * area.getHeight();
494        int stageCount = this.dataset.getStageCount();
495        for (int stage = 0; stage < this.dataset.getStageCount(); stage++) {
496            List<Comparable> sources = this.dataset.getSources(stage);
497            int nodeCount = sources.size();
498            double flowTotal = 0.0;
499            for (Comparable source : sources) {
500                double inflow = FlowDatasetUtils.calculateInflow(this.dataset, source, stage);
501                double outflow = FlowDatasetUtils.calculateOutflow(this.dataset, source, stage);
502                flowTotal = flowTotal + Math.max(inflow, outflow);
503            }
504            if (flowTotal > 0.0) {
505                double availableH = area.getHeight() - (nodeCount - 1) * nodeMargin2d;
506                flow2d = Math.min(availableH / flowTotal, flow2d);
507            }
508            
509            if (stage == this.dataset.getStageCount() - 1) {
510                // check inflows to the final destination nodes...
511                List<Comparable> destinations = this.dataset.getDestinations(stage);
512                int destinationCount = destinations.size();
513                flowTotal = 0.0;
514                for (Comparable destination : destinations) {
515                    double inflow = FlowDatasetUtils.calculateInflow(this.dataset, destination, stage + 1);
516                    flowTotal = flowTotal + inflow;
517                }
518                if (flowTotal > 0.0) {
519                    double availableH = area.getHeight() - (destinationCount - 1) * nodeMargin2d;
520                    flow2d = Math.min(availableH / flowTotal, flow2d);
521                }
522            }
523        }
524
525        double stageWidth = (area.getWidth() - ((stageCount + 1) * this.nodeWidth)) / stageCount;
526        double flowOffset = area.getWidth() * this.flowMargin;
527        
528        Map<NodeKey, Rectangle2D> nodeRects = new HashMap<>();
529        boolean hasNodeSelections = FlowDatasetUtils.hasNodeSelections(this.dataset);
530        boolean hasFlowSelections = FlowDatasetUtils.hasFlowSelections(this.dataset);
531        
532        // iterate over all the stages, we can render the source node rects and
533        // the flows ... we should add the destination node rects last, then
534        // in a final pass add the labels
535        for (int stage = 0; stage < this.dataset.getStageCount(); stage++) {
536            
537            double stageLeft = area.getX() + (stage + 1) * this.nodeWidth + (stage * stageWidth);
538            double stageRight = stageLeft + stageWidth;
539            
540            // calculate the source node and flow rectangles
541            Map<FlowKey, Rectangle2D> sourceFlowRects = new HashMap<>();
542            double nodeY = area.getY();
543            for (Object s : this.dataset.getSources(stage)) {
544                Comparable source = (Comparable) s;
545                double inflow = FlowDatasetUtils.calculateInflow(dataset, source, stage);
546                double outflow = FlowDatasetUtils.calculateOutflow(dataset, source, stage);
547                double nodeHeight = (Math.max(inflow, outflow) * flow2d);
548                Rectangle2D nodeRect = new Rectangle2D.Double(stageLeft - nodeWidth, nodeY, nodeWidth, nodeHeight);
549                if (entities != null) {
550                    entities.add(new NodeEntity(new NodeKey<>(stage, source), nodeRect, source.toString()));                
551                }
552                nodeRects.put(new NodeKey<>(stage, source), nodeRect);
553                double y = nodeY;
554                for (Object d : this.dataset.getDestinations(stage)) {
555                    Comparable destination = (Comparable) d;
556                    Number flow = this.dataset.getFlow(stage, source, destination);
557                    if (flow != null) {
558                        double height = flow.doubleValue() * flow2d;
559                        Rectangle2D rect = new Rectangle2D.Double(stageLeft - nodeWidth, y, nodeWidth, height);
560                        sourceFlowRects.put(new FlowKey<>(stage, source, destination), rect);
561                        y = y + height;
562                    }
563                }
564                nodeY = nodeY + nodeHeight + nodeMargin2d;
565            }
566            
567            // calculate the destination rectangles
568            Map<FlowKey, Rectangle2D> destFlowRects = new HashMap<>();
569            nodeY = area.getY();
570            for (Object d : this.dataset.getDestinations(stage)) {
571                Comparable destination = (Comparable) d;
572                double inflow = FlowDatasetUtils.calculateInflow(dataset, destination, stage + 1);
573                double outflow = FlowDatasetUtils.calculateOutflow(dataset, destination, stage + 1);
574                double nodeHeight = Math.max(inflow, outflow) * flow2d;
575                nodeRects.put(new NodeKey<>(stage + 1, destination), new Rectangle2D.Double(stageRight, nodeY, nodeWidth, nodeHeight));
576                double y = nodeY;
577                for (Object s : this.dataset.getSources(stage)) {
578                    Comparable source = (Comparable) s;
579                    Number flow = this.dataset.getFlow(stage, source, destination);
580                    if (flow != null) {
581                        double height = flow.doubleValue() * flow2d;
582                        Rectangle2D rect = new Rectangle2D.Double(stageRight, y, nodeWidth, height);
583                        y = y + height;
584                        destFlowRects.put(new FlowKey<>(stage, source, destination), rect);
585                    }
586                }
587                nodeY = nodeY + nodeHeight + nodeMargin2d;
588            }
589        
590            for (Object s : this.dataset.getSources(stage)) {
591                Comparable source = (Comparable) s;
592                NodeKey nodeKey = new NodeKey<>(stage, source);
593                Rectangle2D nodeRect = nodeRects.get(nodeKey);
594                Color ncol = lookupNodeColor(nodeKey);
595                if (hasNodeSelections) {
596                    if (!Boolean.TRUE.equals(dataset.getNodeProperty(nodeKey, NodeKey.SELECTED_PROPERTY_KEY))) {
597                        int g = (ncol.getRed() + ncol.getGreen() + ncol.getBlue()) / 3;
598                        ncol = new Color(g, g, g, ncol.getAlpha());
599                    }
600                }
601                g2.setPaint(ncol);
602                g2.fill(nodeRect);
603                                
604                for (Object d : this.dataset.getDestinations(stage)) {
605                    Comparable destination = (Comparable) d;
606                    FlowKey flowKey = new FlowKey<>(stage, source, destination);
607                    Rectangle2D sourceRect = sourceFlowRects.get(flowKey);
608                    if (sourceRect == null) { 
609                        continue; 
610                    }
611                    Rectangle2D destRect = destFlowRects.get(flowKey);
612                
613                    Path2D connect = new Path2D.Double();
614                    connect.moveTo(sourceRect.getMaxX() + flowOffset, sourceRect.getMinY());
615                    connect.curveTo(stageLeft + stageWidth / 2.0, sourceRect.getMinY(), stageLeft + stageWidth / 2.0, destRect.getMinY(), destRect.getX() - flowOffset, destRect.getMinY());
616                    connect.lineTo(destRect.getX() - flowOffset, destRect.getMaxY());
617                    connect.curveTo(stageLeft + stageWidth / 2.0, destRect.getMaxY(), stageLeft + stageWidth / 2.0, sourceRect.getMaxY(), sourceRect.getMaxX() + flowOffset, sourceRect.getMaxY());
618                    connect.closePath();
619                    Color nc = lookupNodeColor(nodeKey);
620                    if (hasFlowSelections) {
621                        if (!Boolean.TRUE.equals(dataset.getFlowProperty(flowKey, FlowKey.SELECTED_PROPERTY_KEY))) {
622                            int g = (ncol.getRed() + ncol.getGreen() + ncol.getBlue()) / 3;
623                            nc = new Color(g, g, g, ncol.getAlpha());
624                        }
625                    }
626                    
627                    GradientPaint gp = new GradientPaint((float) sourceRect.getMaxX(), 0, nc, (float) destRect.getMinX(), 0, new Color(nc.getRed(), nc.getGreen(), nc.getBlue(), 128));
628                    Composite saved = g2.getComposite();
629                    g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.75f));
630                    g2.setPaint(gp);
631                    g2.fill(connect);
632                    if (entities != null) {
633                        String toolTip = null;
634                        if (this.toolTipGenerator != null) {
635                            toolTip = this.toolTipGenerator.generateLabel(this.dataset, flowKey);
636                        }
637                        entities.add(new FlowEntity(flowKey, connect, toolTip, ""));                
638                    }
639                    g2.setComposite(saved);
640                }
641                
642            }
643        }
644        
645        // now draw the destination nodes
646        int lastStage = this.dataset.getStageCount() - 1;
647        for (Object d : this.dataset.getDestinations(lastStage)) {
648            Comparable destination = (Comparable) d;
649            NodeKey nodeKey = new NodeKey<>(lastStage + 1, destination);
650            Rectangle2D nodeRect = nodeRects.get(nodeKey);
651            if (nodeRect != null) {
652                Color ncol = lookupNodeColor(nodeKey);
653                if (hasNodeSelections) {
654                    if (!Boolean.TRUE.equals(dataset.getNodeProperty(nodeKey, NodeKey.SELECTED_PROPERTY_KEY))) {
655                        int g = (ncol.getRed() + ncol.getGreen() + ncol.getBlue()) / 3;
656                        ncol = new Color(g, g, g, ncol.getAlpha());
657                    }
658                }
659                g2.setPaint(ncol);
660                g2.fill(nodeRect);
661                if (entities != null) {
662                    entities.add(new NodeEntity(new NodeKey<>(lastStage + 1, destination), nodeRect, destination.toString()));                
663                }
664            }
665        }
666        
667        // now draw all the labels over top of everything else
668        g2.setFont(this.defaultNodeLabelFont);
669        g2.setPaint(this.defaultNodeLabelPaint);
670        for (NodeKey key : nodeRects.keySet()) {
671            Rectangle2D r = nodeRects.get(key);
672            if (key.getStage() < this.dataset.getStageCount()) {
673                TextUtils.drawAlignedString(key.getNode().toString(), g2, 
674                        (float) (r.getMaxX() + flowOffset + this.nodeLabelOffsetX), 
675                        (float) labelY(r), TextAnchor.CENTER_LEFT);                
676            } else {
677                TextUtils.drawAlignedString(key.getNode().toString(), g2, 
678                        (float) (r.getX() - flowOffset - this.nodeLabelOffsetX), 
679                        (float) labelY(r), TextAnchor.CENTER_RIGHT);                
680            }
681        }
682    }
683    
684    /**
685     * Performs a lookup on the color for the specified node.
686     * 
687     * @param nodeKey  the node key ({@code null} not permitted).
688     * 
689     * @return The node color. 
690     */
691    protected Color lookupNodeColor(NodeKey nodeKey) {
692        Color result = this.nodeColorMap.get(nodeKey);
693        if (result == null) {
694            // if the color swatch is non-empty, we use it to autopopulate 
695            // the node colors...
696            if (!this.nodeColorSwatch.isEmpty()) {
697                // look through previous stages to see if this source key is already seen
698                for (int s = 0; s < nodeKey.getStage(); s++) {
699                    for (Object key : dataset.getSources(s)) {
700                        if (nodeKey.getNode().equals(key)) {
701                            Color color = this.nodeColorMap.get(new NodeKey<>(s, (Comparable) key));
702                            setNodeFillColor(nodeKey, color);
703                            return color;
704                        }
705                    }
706                }
707
708                result = this.nodeColorSwatch.get(Math.min(this.nodeColorSwatchPointer, this.nodeColorSwatch.size() - 1));
709                this.nodeColorSwatchPointer++;
710                if (this.nodeColorSwatchPointer > this.nodeColorSwatch.size() - 1) { 
711                    this.nodeColorSwatchPointer = 0;
712                }
713                setNodeFillColor(nodeKey, result);
714                return result;
715            } else {
716                result = this.defaultNodeColor;
717            }
718        }
719        return result;
720    }
721
722    /**
723     * Computes the y-coordinate for a node label taking into account the 
724     * current alignment settings.
725     * 
726     * @param r  the node rectangle.
727     * 
728     * @return The y-coordinate for the label. 
729     */
730    private double labelY(Rectangle2D r) {
731        if (this.nodeLabelAlignment == VerticalAlignment.TOP) {
732            return r.getY() + this.nodeLabelOffsetY;
733        } else if (this.nodeLabelAlignment == VerticalAlignment.BOTTOM) {
734            return r.getMaxY() - this.nodeLabelOffsetY;
735        } else {
736            return r.getCenterY();
737        }
738    }
739    
740    /**
741     * Tests this plot for equality with an arbitrary object.  Note that, for 
742     * the purposes of this equality test, the dataset is ignored.
743     * 
744     * @param obj  the object ({@code null} permitted).
745     * 
746     * @return A boolean. 
747     */
748    @Override
749    public boolean equals(Object obj) {
750        if (!(obj instanceof FlowPlot)) {
751            return false;
752        }
753        FlowPlot that = (FlowPlot) obj;
754        if (!this.defaultNodeColor.equals(that.defaultNodeColor)) {
755            return false;
756        }
757        if (!this.nodeColorMap.equals(that.nodeColorMap)) {
758            return false;
759        }
760        if (!this.nodeColorSwatch.equals(that.nodeColorSwatch)) {
761            return false;
762        }
763        if (!this.defaultNodeLabelFont.equals(that.defaultNodeLabelFont)) {
764            return false;
765        }
766        if (!PaintUtils.equal(this.defaultNodeLabelPaint, that.defaultNodeLabelPaint)) {
767            return false;
768        }
769        if (this.flowMargin != that.flowMargin) {
770            return false;
771        }
772        if (this.nodeMargin != that.nodeMargin) {
773            return false;
774        }
775        if (this.nodeWidth != that.nodeWidth) {
776            return false;
777        }
778        if (this.nodeLabelOffsetX != that.nodeLabelOffsetX) {
779            return false;
780        }
781        if (this.nodeLabelOffsetY != that.nodeLabelOffsetY) {
782            return false;
783        }
784        if (this.nodeLabelAlignment != that.nodeLabelAlignment) {
785            return false;
786        }
787        if (!Objects.equals(this.toolTipGenerator, that.toolTipGenerator)) {
788            return false;
789        }
790        return super.equals(obj);
791    }
792
793    /**
794     * Returns a hashcode for this instance.
795     * 
796     * @return A hashcode. 
797     */
798    @Override
799    public int hashCode() {
800        int hash = 3;
801        hash = 83 * hash + Long.hashCode(Double.doubleToLongBits(this.nodeWidth));
802        hash = 83 * hash + Long.hashCode(Double.doubleToLongBits(this.nodeMargin));
803        hash = 83 * hash + Long.hashCode(Double.doubleToLongBits(this.flowMargin));
804        hash = 83 * hash + Objects.hashCode(this.nodeColorMap);
805        hash = 83 * hash + Objects.hashCode(this.nodeColorSwatch);
806        hash = 83 * hash + Objects.hashCode(this.defaultNodeColor);
807        hash = 83 * hash + Objects.hashCode(this.defaultNodeLabelFont);
808        hash = 83 * hash + Objects.hashCode(this.defaultNodeLabelPaint);
809        hash = 83 * hash + Objects.hashCode(this.nodeLabelAlignment);
810        hash = 83 * hash + Long.hashCode(Double.doubleToLongBits(this.nodeLabelOffsetX));
811        hash = 83 * hash + Long.hashCode(Double.doubleToLongBits(this.nodeLabelOffsetY));
812        hash = 83 * hash + Objects.hashCode(this.toolTipGenerator);
813        return hash;
814    }
815
816    /**
817     * Returns an independent copy of this {@code FlowPlot} instance (note, 
818     * however, that the dataset is NOT cloned).
819     * 
820     * @return A close of this instance.
821     * 
822     * @throws CloneNotSupportedException if there is a problem cloning.
823     */
824    @Override
825    public Object clone() throws CloneNotSupportedException {
826        FlowPlot clone = (FlowPlot) super.clone();
827        clone.nodeColorMap = new HashMap<>(this.nodeColorMap);
828        return clone;
829    }
830
831}