org.jfree.chart.plot.flow.FlowPlot Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jfreechart Show documentation
Show all versions of jfreechart Show documentation
JFreeChart is a class library, written in Java, for generating charts.
Utilising the Java2D API, it supports a wide range of chart types including
bar charts, pie charts, line charts, XY-plots, time series plots, Sankey charts
and more.
/* ===========================================================
* JFreeChart : a free chart library for the Java(tm) platform
* ===========================================================
*
* (C) Copyright 2000-present, by David Gilbert and Contributors.
*
* Project Info: http://www.jfree.org/jfreechart/index.html
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
* [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.]
*
* -------------
* FlowPlot.java
* -------------
* (C) Copyright 2021-present, by David Gilbert and Contributors.
*
* Original Author: David Gilbert;
* Contributor(s): -;
*
*/
package org.jfree.chart.plot.flow;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.entity.FlowEntity;
import org.jfree.chart.entity.NodeEntity;
import org.jfree.chart.labels.FlowLabelGenerator;
import org.jfree.chart.labels.StandardFlowLabelGenerator;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.PlotState;
import org.jfree.chart.text.TextUtils;
import org.jfree.chart.ui.RectangleInsets;
import org.jfree.chart.ui.TextAnchor;
import org.jfree.chart.ui.VerticalAlignment;
import org.jfree.chart.util.Args;
import org.jfree.chart.util.PaintUtils;
import org.jfree.chart.util.PublicCloneable;
import org.jfree.data.flow.FlowDataset;
import org.jfree.data.flow.FlowDatasetUtils;
import org.jfree.data.flow.FlowKey;
import org.jfree.data.flow.NodeKey;
/**
* A plot for visualising flows defined in a {@link FlowDataset}. This enables
* the production of a type of Sankey chart. The example shown here is
* produced by the {@code FlowPlotDemo1.java} program included in the JFreeChart
* Demo Collection:
*
*
* @since 1.5.3
*/
public class FlowPlot extends Plot implements Cloneable, PublicCloneable,
Serializable {
/** The source of data. */
private FlowDataset dataset;
/**
* The node width in Java 2D user-space units.
*/
private double nodeWidth = 20.0;
/** The gap between nodes (expressed as a percentage of the plot height). */
private double nodeMargin = 0.01;
/**
* The percentage of the plot width to assign to a gap between the nodes
* and the flow representation.
*/
private double flowMargin = 0.005;
/**
* Stores colors for specific nodes - if there isn't a color in here for
* the node, the default node color will be used (unless the color swatch
* is active).
*/
private Map nodeColorMap;
private List nodeColorSwatch;
/** A pointer into the color swatch. */
private int nodeColorSwatchPointer = 0;
/** The default node color if nothing is defined in the nodeColorMap. */
private Color defaultNodeColor;
private Font defaultNodeLabelFont;
private Paint defaultNodeLabelPaint;
private VerticalAlignment nodeLabelAlignment;
/** The x-offset for node labels. */
private double nodeLabelOffsetX;
/** The y-offset for node labels. */
private double nodeLabelOffsetY;
/** The tool tip generator - if null, no tool tips will be displayed. */
private FlowLabelGenerator toolTipGenerator;
/**
* Creates a new instance that will source data from the specified dataset.
*
* @param dataset the dataset.
*/
public FlowPlot(FlowDataset dataset) {
this.dataset = dataset;
if (dataset != null) {
dataset.addChangeListener(this);
}
this.nodeColorMap = new HashMap<>();
this.nodeColorSwatch = new ArrayList<>();
this.defaultNodeColor = Color.GRAY;
this.defaultNodeLabelFont = new Font(Font.DIALOG, Font.BOLD, 12);
this.defaultNodeLabelPaint = Color.BLACK;
this.nodeLabelAlignment = VerticalAlignment.CENTER;
this.nodeLabelOffsetX = 2.0;
this.nodeLabelOffsetY = 2.0;
this.toolTipGenerator = new StandardFlowLabelGenerator();
}
/**
* Returns a string identifying the plot type.
*
* @return A string identifying the plot type.
*/
@Override
public String getPlotType() {
return "FlowPlot";
}
/**
* Returns a reference to the dataset.
*
* @return A reference to the dataset (possibly {@code null}).
*/
public FlowDataset getDataset() {
return this.dataset;
}
/**
* Sets the dataset for the plot and sends a change notification to all
* registered listeners.
*
* @param dataset the dataset ({@code null} permitted).
*/
public void setDataset(FlowDataset dataset) {
this.dataset = dataset;
fireChangeEvent();
}
/**
* Returns the node margin (expressed as a percentage of the available
* plotting space) which is the gap between nodes (sources or destinations).
* The initial (default) value is {@code 0.01} (1 percent).
*
* @return The node margin.
*/
public double getNodeMargin() {
return this.nodeMargin;
}
/**
* Sets the node margin and sends a change notification to all registered
* listeners.
*
* @param margin the margin (expressed as a percentage).
*/
public void setNodeMargin(double margin) {
Args.requireNonNegative(margin, "margin");
this.nodeMargin = margin;
fireChangeEvent();
}
/**
* Returns the flow margin. This determines the gap between the graphic
* representation of the nodes (sources and destinations) and the curved
* flow representation. This is expressed as a percentage of the plot
* width so that it remains proportional as the plot is resized. The
* initial (default) value is {@code 0.005} (0.5 percent).
*
* @return The flow margin.
*/
public double getFlowMargin() {
return this.flowMargin;
}
/**
* Sets the flow margin and sends a change notification to all registered
* listeners.
*
* @param margin the margin (must be 0.0 or higher).
*/
public void setFlowMargin(double margin) {
Args.requireNonNegative(margin, "margin");
this.flowMargin = margin;
fireChangeEvent();
}
/**
* Returns the width of the source and destination nodes, expressed in
* Java2D user-space units. The initial (default) value is {@code 20.0}.
*
* @return The width.
*/
public double getNodeWidth() {
return this.nodeWidth;
}
/**
* Sets the width for the source and destination nodes and sends a change
* notification to all registered listeners.
*
* @param width the width.
*/
public void setNodeWidth(double width) {
this.nodeWidth = width;
fireChangeEvent();
}
/**
* Returns the list of colors that will be used to auto-populate the node
* colors when they are first rendered. If the list is empty, no color
* will be assigned to the node so, unless it is manually set, the default
* color will apply. This method returns a copy of the list, modifying
* the returned list will not affect the plot.
*
* @return The list of colors (possibly empty, but never {@code null}).
*/
public List getNodeColorSwatch() {
return new ArrayList<>(this.nodeColorSwatch);
}
/**
* Sets the color swatch for the plot.
*
* @param colors the list of colors ({@code null} not permitted).
*/
public void setNodeColorSwatch(List colors) {
Args.nullNotPermitted(colors, "colors");
this.nodeColorSwatch = colors;
}
/**
* Returns the fill color for the specified node.
*
* @param nodeKey the node key ({@code null} not permitted).
*
* @return The fill color (possibly {@code null}).
*/
public Color getNodeFillColor(NodeKey nodeKey) {
return this.nodeColorMap.get(nodeKey);
}
/**
* Sets the fill color for the specified node and sends a change
* notification to all registered listeners.
*
* @param nodeKey the node key ({@code null} not permitted).
* @param color the fill color ({@code null} permitted).
*/
public void setNodeFillColor(NodeKey nodeKey, Color color) {
this.nodeColorMap.put(nodeKey, color);
fireChangeEvent();
}
/**
* Returns the default node color. This is used when no specific node color
* has been specified. The initial (default) value is {@code Color.GRAY}.
*
* @return The default node color (never {@code null}).
*/
public Color getDefaultNodeColor() {
return this.defaultNodeColor;
}
/**
* Sets the default node color and sends a change event to registered
* listeners.
*
* @param color the color ({@code null} not permitted).
*/
public void setDefaultNodeColor(Color color) {
Args.nullNotPermitted(color, "color");
this.defaultNodeColor = color;
fireChangeEvent();
}
/**
* Returns the default font used to display labels for the source and
* destination nodes. The initial (default) value is
* {@code Font(Font.DIALOG, Font.BOLD, 12)}.
*
* @return The default font (never {@code null}).
*/
public Font getDefaultNodeLabelFont() {
return this.defaultNodeLabelFont;
}
/**
* Sets the default font used to display labels for the source and
* destination nodes and sends a change notification to all registered
* listeners.
*
* @param font the font ({@code null} not permitted).
*/
public void setDefaultNodeLabelFont(Font font) {
Args.nullNotPermitted(font, "font");
this.defaultNodeLabelFont = font;
fireChangeEvent();
}
/**
* Returns the default paint used to display labels for the source and
* destination nodes. The initial (default) value is {@code Color.BLACK}.
*
* @return The default paint (never {@code null}).
*/
public Paint getDefaultNodeLabelPaint() {
return this.defaultNodeLabelPaint;
}
/**
* Sets the default paint used to display labels for the source and
* destination nodes and sends a change notification to all registered
* listeners.
*
* @param paint the paint ({@code null} not permitted).
*/
public void setDefaultNodeLabelPaint(Paint paint) {
Args.nullNotPermitted(paint, "paint");
this.defaultNodeLabelPaint = paint;
fireChangeEvent();
}
/**
* Returns the vertical alignment of the node labels relative to the node.
* The initial (default) value is {@link VerticalAlignment#CENTER}.
*
* @return The alignment (never {@code null}).
*/
public VerticalAlignment getNodeLabelAlignment() {
return this.nodeLabelAlignment;
}
/**
* Sets the vertical alignment of the node labels and sends a change
* notification to all registered listeners.
*
* @param alignment the new alignment ({@code null} not permitted).
*/
public void setNodeLabelAlignment(VerticalAlignment alignment) {
Args.nullNotPermitted(alignment, "alignment");
this.nodeLabelAlignment = alignment;
fireChangeEvent();
}
/**
* Returns the x-offset for the node labels.
*
* @return The x-offset for the node labels.
*/
public double getNodeLabelOffsetX() {
return this.nodeLabelOffsetX;
}
/**
* Sets the x-offset for the node labels and sends a change notification
* to all registered listeners.
*
* @param offsetX the node label x-offset in Java2D units.
*/
public void setNodeLabelOffsetX(double offsetX) {
this.nodeLabelOffsetX = offsetX;
fireChangeEvent();
}
/**
* Returns the y-offset for the node labels.
*
* @return The y-offset for the node labels.
*/
public double getNodeLabelOffsetY() {
return nodeLabelOffsetY;
}
/**
* Sets the y-offset for the node labels and sends a change notification
* to all registered listeners.
*
* @param offsetY the node label y-offset in Java2D units.
*/
public void setNodeLabelOffsetY(double offsetY) {
this.nodeLabelOffsetY = offsetY;
fireChangeEvent();
}
/**
* Returns the tool tip generator that creates the strings that are
* displayed as tool tips for the flows displayed in the plot.
*
* @return The tool tip generator (possibly {@code null}).
*/
public FlowLabelGenerator getToolTipGenerator() {
return this.toolTipGenerator;
}
/**
* Sets the tool tip generator and sends a change notification to all
* registered listeners. If the generator is set to {@code null}, no tool
* tips will be displayed for the flows.
*
* @param generator the new generator ({@code null} permitted).
*/
public void setToolTipGenerator(FlowLabelGenerator generator) {
this.toolTipGenerator = generator;
fireChangeEvent();
}
/**
* Draws the flow plot within the specified area of the supplied graphics
* target {@code g2}.
*
* @param g2 the graphics target ({@code null} not permitted).
* @param area the plot area ({@code null} not permitted).
* @param anchor the anchor point (ignored).
* @param parentState the parent state (ignored).
* @param info the plot rendering info.
*/
@Override
public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, PlotState parentState, PlotRenderingInfo info) {
Args.nullNotPermitted(g2, "g2");
Args.nullNotPermitted(area, "area");
EntityCollection entities = null;
if (info != null) {
info.setPlotArea(area);
entities = info.getOwner().getEntityCollection();
}
RectangleInsets insets = getInsets();
insets.trim(area);
if (info != null) {
info.setDataArea(area);
}
// use default JFreeChart background handling
drawBackground(g2, area);
// we need to ensure there is space to show all the inflows and all
// the outflows at each node group, so first we calculate the max
// flow space required - for each node in the group, consider the
// maximum of the inflow and the outflow
double flow2d = Double.POSITIVE_INFINITY;
double nodeMargin2d = this.nodeMargin * area.getHeight();
int stageCount = this.dataset.getStageCount();
for (int stage = 0; stage < this.dataset.getStageCount(); stage++) {
List sources = this.dataset.getSources(stage);
int nodeCount = sources.size();
double flowTotal = 0.0;
for (Comparable source : sources) {
double inflow = FlowDatasetUtils.calculateInflow(this.dataset, source, stage);
double outflow = FlowDatasetUtils.calculateOutflow(this.dataset, source, stage);
flowTotal = flowTotal + Math.max(inflow, outflow);
}
if (flowTotal > 0.0) {
double availableH = area.getHeight() - (nodeCount - 1) * nodeMargin2d;
flow2d = Math.min(availableH / flowTotal, flow2d);
}
if (stage == this.dataset.getStageCount() - 1) {
// check inflows to the final destination nodes...
List destinations = this.dataset.getDestinations(stage);
int destinationCount = destinations.size();
flowTotal = 0.0;
for (Comparable destination : destinations) {
double inflow = FlowDatasetUtils.calculateInflow(this.dataset, destination, stage + 1);
flowTotal = flowTotal + inflow;
}
if (flowTotal > 0.0) {
double availableH = area.getHeight() - (destinationCount - 1) * nodeMargin2d;
flow2d = Math.min(availableH / flowTotal, flow2d);
}
}
}
double stageWidth = (area.getWidth() - ((stageCount + 1) * this.nodeWidth)) / stageCount;
double flowOffset = area.getWidth() * this.flowMargin;
Map nodeRects = new HashMap<>();
boolean hasNodeSelections = FlowDatasetUtils.hasNodeSelections(this.dataset);
boolean hasFlowSelections = FlowDatasetUtils.hasFlowSelections(this.dataset);
// iterate over all the stages, we can render the source node rects and
// the flows ... we should add the destination node rects last, then
// in a final pass add the labels
for (int stage = 0; stage < this.dataset.getStageCount(); stage++) {
double stageLeft = area.getX() + (stage + 1) * this.nodeWidth + (stage * stageWidth);
double stageRight = stageLeft + stageWidth;
// calculate the source node and flow rectangles
Map sourceFlowRects = new HashMap<>();
double nodeY = area.getY();
for (Object s : this.dataset.getSources(stage)) {
Comparable source = (Comparable) s;
double inflow = FlowDatasetUtils.calculateInflow(dataset, source, stage);
double outflow = FlowDatasetUtils.calculateOutflow(dataset, source, stage);
double nodeHeight = (Math.max(inflow, outflow) * flow2d);
Rectangle2D nodeRect = new Rectangle2D.Double(stageLeft - nodeWidth, nodeY, nodeWidth, nodeHeight);
if (entities != null) {
entities.add(new NodeEntity(new NodeKey<>(stage, source), nodeRect, source.toString()));
}
nodeRects.put(new NodeKey<>(stage, source), nodeRect);
double y = nodeY;
for (Object d : this.dataset.getDestinations(stage)) {
Comparable destination = (Comparable) d;
Number flow = this.dataset.getFlow(stage, source, destination);
if (flow != null) {
double height = flow.doubleValue() * flow2d;
Rectangle2D rect = new Rectangle2D.Double(stageLeft - nodeWidth, y, nodeWidth, height);
sourceFlowRects.put(new FlowKey<>(stage, source, destination), rect);
y = y + height;
}
}
nodeY = nodeY + nodeHeight + nodeMargin2d;
}
// calculate the destination rectangles
Map destFlowRects = new HashMap<>();
nodeY = area.getY();
for (Object d : this.dataset.getDestinations(stage)) {
Comparable destination = (Comparable) d;
double inflow = FlowDatasetUtils.calculateInflow(dataset, destination, stage + 1);
double outflow = FlowDatasetUtils.calculateOutflow(dataset, destination, stage + 1);
double nodeHeight = Math.max(inflow, outflow) * flow2d;
nodeRects.put(new NodeKey<>(stage + 1, destination), new Rectangle2D.Double(stageRight, nodeY, nodeWidth, nodeHeight));
double y = nodeY;
for (Object s : this.dataset.getSources(stage)) {
Comparable source = (Comparable) s;
Number flow = this.dataset.getFlow(stage, source, destination);
if (flow != null) {
double height = flow.doubleValue() * flow2d;
Rectangle2D rect = new Rectangle2D.Double(stageRight, y, nodeWidth, height);
y = y + height;
destFlowRects.put(new FlowKey<>(stage, source, destination), rect);
}
}
nodeY = nodeY + nodeHeight + nodeMargin2d;
}
for (Object s : this.dataset.getSources(stage)) {
Comparable source = (Comparable) s;
NodeKey nodeKey = new NodeKey<>(stage, source);
Rectangle2D nodeRect = nodeRects.get(nodeKey);
Color ncol = lookupNodeColor(nodeKey);
if (hasNodeSelections) {
if (!Boolean.TRUE.equals(dataset.getNodeProperty(nodeKey, NodeKey.SELECTED_PROPERTY_KEY))) {
int g = (ncol.getRed() + ncol.getGreen() + ncol.getBlue()) / 3;
ncol = new Color(g, g, g, ncol.getAlpha());
}
}
g2.setPaint(ncol);
g2.fill(nodeRect);
for (Object d : this.dataset.getDestinations(stage)) {
Comparable destination = (Comparable) d;
FlowKey flowKey = new FlowKey<>(stage, source, destination);
Rectangle2D sourceRect = sourceFlowRects.get(flowKey);
if (sourceRect == null) {
continue;
}
Rectangle2D destRect = destFlowRects.get(flowKey);
Path2D connect = new Path2D.Double();
connect.moveTo(sourceRect.getMaxX() + flowOffset, sourceRect.getMinY());
connect.curveTo(stageLeft + stageWidth / 2.0, sourceRect.getMinY(), stageLeft + stageWidth / 2.0, destRect.getMinY(), destRect.getX() - flowOffset, destRect.getMinY());
connect.lineTo(destRect.getX() - flowOffset, destRect.getMaxY());
connect.curveTo(stageLeft + stageWidth / 2.0, destRect.getMaxY(), stageLeft + stageWidth / 2.0, sourceRect.getMaxY(), sourceRect.getMaxX() + flowOffset, sourceRect.getMaxY());
connect.closePath();
Color nc = lookupNodeColor(nodeKey);
if (hasFlowSelections) {
if (!Boolean.TRUE.equals(dataset.getFlowProperty(flowKey, FlowKey.SELECTED_PROPERTY_KEY))) {
int g = (ncol.getRed() + ncol.getGreen() + ncol.getBlue()) / 3;
nc = new Color(g, g, g, ncol.getAlpha());
}
}
GradientPaint gp = new GradientPaint((float) sourceRect.getMaxX(), 0, nc, (float) destRect.getMinX(), 0, new Color(nc.getRed(), nc.getGreen(), nc.getBlue(), 128));
Composite saved = g2.getComposite();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.75f));
g2.setPaint(gp);
g2.fill(connect);
if (entities != null) {
String toolTip = null;
if (this.toolTipGenerator != null) {
toolTip = this.toolTipGenerator.generateLabel(this.dataset, flowKey);
}
entities.add(new FlowEntity(flowKey, connect, toolTip, ""));
}
g2.setComposite(saved);
}
}
}
// now draw the destination nodes
int lastStage = this.dataset.getStageCount() - 1;
for (Object d : this.dataset.getDestinations(lastStage)) {
Comparable destination = (Comparable) d;
NodeKey nodeKey = new NodeKey<>(lastStage + 1, destination);
Rectangle2D nodeRect = nodeRects.get(nodeKey);
if (nodeRect != null) {
Color ncol = lookupNodeColor(nodeKey);
if (hasNodeSelections) {
if (!Boolean.TRUE.equals(dataset.getNodeProperty(nodeKey, NodeKey.SELECTED_PROPERTY_KEY))) {
int g = (ncol.getRed() + ncol.getGreen() + ncol.getBlue()) / 3;
ncol = new Color(g, g, g, ncol.getAlpha());
}
}
g2.setPaint(ncol);
g2.fill(nodeRect);
if (entities != null) {
entities.add(new NodeEntity(new NodeKey<>(lastStage + 1, destination), nodeRect, destination.toString()));
}
}
}
// now draw all the labels over top of everything else
g2.setFont(this.defaultNodeLabelFont);
g2.setPaint(this.defaultNodeLabelPaint);
for (NodeKey key : nodeRects.keySet()) {
Rectangle2D r = nodeRects.get(key);
if (key.getStage() < this.dataset.getStageCount()) {
TextUtils.drawAlignedString(key.getNode().toString(), g2,
(float) (r.getMaxX() + flowOffset + this.nodeLabelOffsetX),
(float) labelY(r), TextAnchor.CENTER_LEFT);
} else {
TextUtils.drawAlignedString(key.getNode().toString(), g2,
(float) (r.getX() - flowOffset - this.nodeLabelOffsetX),
(float) labelY(r), TextAnchor.CENTER_RIGHT);
}
}
}
/**
* Performs a lookup on the color for the specified node.
*
* @param nodeKey the node key ({@code null} not permitted).
*
* @return The node color.
*/
protected Color lookupNodeColor(NodeKey nodeKey) {
Color result = this.nodeColorMap.get(nodeKey);
if (result == null) {
// if the color swatch is non-empty, we use it to autopopulate
// the node colors...
if (!this.nodeColorSwatch.isEmpty()) {
// look through previous stages to see if this source key is already seen
for (int s = 0; s < nodeKey.getStage(); s++) {
for (Object key : dataset.getSources(s)) {
if (nodeKey.getNode().equals(key)) {
Color color = this.nodeColorMap.get(new NodeKey<>(s, (Comparable) key));
setNodeFillColor(nodeKey, color);
return color;
}
}
}
result = this.nodeColorSwatch.get(Math.min(this.nodeColorSwatchPointer, this.nodeColorSwatch.size() - 1));
this.nodeColorSwatchPointer++;
if (this.nodeColorSwatchPointer > this.nodeColorSwatch.size() - 1) {
this.nodeColorSwatchPointer = 0;
}
setNodeFillColor(nodeKey, result);
return result;
} else {
result = this.defaultNodeColor;
}
}
return result;
}
/**
* Computes the y-coordinate for a node label taking into account the
* current alignment settings.
*
* @param r the node rectangle.
*
* @return The y-coordinate for the label.
*/
private double labelY(Rectangle2D r) {
if (this.nodeLabelAlignment == VerticalAlignment.TOP) {
return r.getY() + this.nodeLabelOffsetY;
} else if (this.nodeLabelAlignment == VerticalAlignment.BOTTOM) {
return r.getMaxY() - this.nodeLabelOffsetY;
} else {
return r.getCenterY();
}
}
/**
* Tests this plot for equality with an arbitrary object. Note that, for
* the purposes of this equality test, the dataset is ignored.
*
* @param obj the object ({@code null} permitted).
*
* @return A boolean.
*/
@Override
public boolean equals(Object obj) {
if (!(obj instanceof FlowPlot)) {
return false;
}
FlowPlot that = (FlowPlot) obj;
if (!this.defaultNodeColor.equals(that.defaultNodeColor)) {
return false;
}
if (!this.nodeColorMap.equals(that.nodeColorMap)) {
return false;
}
if (!this.nodeColorSwatch.equals(that.nodeColorSwatch)) {
return false;
}
if (!this.defaultNodeLabelFont.equals(that.defaultNodeLabelFont)) {
return false;
}
if (!PaintUtils.equal(this.defaultNodeLabelPaint, that.defaultNodeLabelPaint)) {
return false;
}
if (this.flowMargin != that.flowMargin) {
return false;
}
if (this.nodeMargin != that.nodeMargin) {
return false;
}
if (this.nodeWidth != that.nodeWidth) {
return false;
}
if (this.nodeLabelOffsetX != that.nodeLabelOffsetX) {
return false;
}
if (this.nodeLabelOffsetY != that.nodeLabelOffsetY) {
return false;
}
if (this.nodeLabelAlignment != that.nodeLabelAlignment) {
return false;
}
if (!Objects.equals(this.toolTipGenerator, that.toolTipGenerator)) {
return false;
}
return super.equals(obj);
}
/**
* Returns a hashcode for this instance.
*
* @return A hashcode.
*/
@Override
public int hashCode() {
int hash = 3;
hash = 83 * hash + (int) (Double.doubleToLongBits(this.nodeWidth) ^ (Double.doubleToLongBits(this.nodeWidth) >>> 32));
hash = 83 * hash + (int) (Double.doubleToLongBits(this.nodeMargin) ^ (Double.doubleToLongBits(this.nodeMargin) >>> 32));
hash = 83 * hash + (int) (Double.doubleToLongBits(this.flowMargin) ^ (Double.doubleToLongBits(this.flowMargin) >>> 32));
hash = 83 * hash + Objects.hashCode(this.nodeColorMap);
hash = 83 * hash + Objects.hashCode(this.nodeColorSwatch);
hash = 83 * hash + Objects.hashCode(this.defaultNodeColor);
hash = 83 * hash + Objects.hashCode(this.defaultNodeLabelFont);
hash = 83 * hash + Objects.hashCode(this.defaultNodeLabelPaint);
hash = 83 * hash + Objects.hashCode(this.nodeLabelAlignment);
hash = 83 * hash + (int) (Double.doubleToLongBits(this.nodeLabelOffsetX) ^ (Double.doubleToLongBits(this.nodeLabelOffsetX) >>> 32));
hash = 83 * hash + (int) (Double.doubleToLongBits(this.nodeLabelOffsetY) ^ (Double.doubleToLongBits(this.nodeLabelOffsetY) >>> 32));
hash = 83 * hash + Objects.hashCode(this.toolTipGenerator);
return hash;
}
/**
* Returns an independent copy of this {@code FlowPlot} instance (note,
* however, that the dataset is NOT cloned).
*
* @return A close of this instance.
*
* @throws CloneNotSupportedException
*/
@Override
public Object clone() throws CloneNotSupportedException {
FlowPlot clone = (FlowPlot) super.clone();
clone.nodeColorMap = new HashMap<>(this.nodeColorMap);
return clone;
}
}