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}