001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-present, by David Gilbert 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-present, by David Gilbert and Contributors. 031 * 032 * Original Author: David Gilbert; 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}