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