kg.apc.charting.GraphPanelChart Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jmeter-plugins-cmn-jmeter Show documentation
Show all versions of jmeter-plugins-cmn-jmeter Show documentation
Various utility classes to ease development of plugins
The newest version!
package kg.apc.charting;
import kg.apc.charting.plotters.AbstractRowPlotter;
import kg.apc.charting.plotters.BarRowPlotter;
import kg.apc.charting.plotters.CSplineRowPlotter;
import kg.apc.charting.plotters.LineRowPlotter;
import kg.apc.jmeter.gui.CustomNumberRenderer;
import org.apache.jorphan.gui.NumberRenderer;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
public class GraphPanelChart
extends JComponent
implements ClipboardOwner {
//cache management
private static BufferedImage cache = null;
private static int cacheWitdh, cacheHeight;
private boolean cacheValid = false;
private static int cacheOwner = -1;
private int gpcId;
private static int uidGenerator = 0;
//plotters
BarRowPlotter barRowPlotter = null;
LineRowPlotter lineRowPlotter = null;
CSplineRowPlotter cSplineRowPlotter = null;
AbstractRowPlotter currentPlotter = null;
JPopupMenu popup = new JPopupMenu();
private static final String AD_TEXT = "jmeter-plugins.org";
private static final String NO_SAMPLES = "Waiting for samples...";
private static final int spacing = 5;
private static final int previewInset = 4;
/*
* Special type of graph were minY is forced to 0 and maxY is forced to 100
* to display percentage charts (eg cpu monitoring)
*/
public static final int CHART_PERCENTAGE = 0;
public static final int CHART_DEFAULT = -1;
private static final Logger log = LoggerFactory.getLogger(GraphPanelChart.class);
private Rectangle legendRect;
private Rectangle xAxisRect;
private Rectangle yAxisRect;
private Rectangle chartRect;
private static final Rectangle zeroRect = new Rectangle();
private AbstractMap rows;
private double maxYVal;
private double minYVal;
private long maxXVal;
private long minXVal;
private long currentXVal;
private static final int gridLinesCount = 10;
private NumberRenderer yAxisLabelRenderer;
private NumberRenderer xAxisLabelRenderer;
private long forcedMinX = -1;
private int chartType = CHART_DEFAULT;
// Message display in graphs. Used for perfmon error messages
private String errorMessage = null;
// Chart's gradient background end color
private final static Color gradientColor = new Color(229, 236, 246);
// Chart's Axis Color. For good results, use gradient color - (40, 40, 40)
private final static Color axisColor = new Color(189, 196, 206);
//save file path. We remember last folder used.
private static String savePath = null;
private ChartSettings chartSettings = new ChartSettings();
private CustomNumberRenderer nbFormatter = new CustomNumberRenderer("#,#00.#", ' ');
private final static int legendAdjust = 3;
public ChartSettings getChartSettings() {
return chartSettings;
}
private boolean reSetColors = false;
//hover info
private JWindow hoverWindow;
//gap in pixels relative to mouse pointer position
private final static int hoverGap = 20;
//force positionning between 2 clicks
private boolean forceHoverPosition = true;
private JTextField hoverLabel;
private int xHoverInfo = -1;
private int yHoverInfo = -1;
private HoverMotionListener motionListener = new HoverMotionListener();
public void setReSetColors(boolean reSetColors) {
this.reSetColors = reSetColors;
}
private String xAxisLabel = "X axis label";
private String yAxisLabel = "Y axis label";
private int precisionLabel = -1;
private int limitPointFactor = 1;
private boolean displayPrecision = false;
public void setDisplayPrecision(boolean displayPrecision) {
this.displayPrecision = displayPrecision;
}
public void setxAxisLabel(String xAxisLabel) {
this.xAxisLabel = xAxisLabel;
}
public void setYAxisLabel(String yAxisLabel) {
this.yAxisLabel = yAxisLabel;
}
public void setPrecisionLabel(int precision) {
this.precisionLabel = precision;
}
private void autoZoom_orig() {
//row zooming
if (!chartSettings.isExpendRows()) {
return;
}
for (Entry row : rows.entrySet()) {
double[] minMax = row.getValue().getMinMaxY(chartSettings.getMaxPointPerRow());
if (minMax[1] > 0) {
double zoomFactor = 1;
rowsZoomFactor.put(row.getKey(), zoomFactor);
while (minMax[1] * zoomFactor <= maxYVal) {
rowsZoomFactor.put(row.getKey(), zoomFactor);
zoomFactor = zoomFactor * 10;
}
} else {
rowsZoomFactor.put(row.getKey(), 1.0);
}
}
}
private String getXAxisLabel() {
String label;
if (!displayPrecision) {
label = xAxisLabel;
} else {
long granularity;
if (chartSettings.getMaxPointPerRow() <= 0) {
granularity = precisionLabel;
} else {
granularity = precisionLabel * limitPointFactor;
}
long min = granularity / 60000;
long sec = (granularity % 60000) / 1000;
long ms = (granularity % 60000) % 1000;
label = xAxisLabel + " (granularity:";
if (min > 0) {
label += " " + min + " min";
}
if (sec > 0 || (min > 0 && ms > 0)) {
if (min > 0) {
label += ",";
}
label += " " + sec + " sec";
}
if (ms > 0) {
if (sec > 0 || min > 0) {
label += ",";
}
label += " " + ms + " ms";
}
label += ")";
}
return label;
}
private boolean isPreview = false;
public void setIsPreview(boolean isPreview) {
this.isPreview = isPreview;
if (!isPreview) {
this.setComponentPopupMenu(popup);
} else {
this.setComponentPopupMenu(null);
}
}
//relative time
private long testStartTime = 0;
public void setTestStartTime(long time) {
testStartTime = time;
}
//row zooming
private HashMap rowsZoomFactor = new HashMap<>();
private static synchronized int getNextId() {
uidGenerator++;
return uidGenerator;
}
/**
* Creates new chart object with default parameters
*
* @param allowCsvExport if do it
* @param haveGUI if have it
*/
public GraphPanelChart(boolean allowCsvExport, boolean haveGUI) {
gpcId = getNextId();
setBackground(Color.white);
setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED, Color.lightGray, Color.darkGray));
yAxisLabelRenderer = new CustomNumberRenderer("#,###.#", ' ');
xAxisLabelRenderer = new CustomNumberRenderer("#,###.#", ' ');
legendRect = new Rectangle();
yAxisRect = new Rectangle();
xAxisRect = new Rectangle();
chartRect = new Rectangle();
//no need to register anything in non GUI mode
//second test required for unit test mode
if (haveGUI && !GraphicsEnvironment.isHeadless()) {
registerPopup(allowCsvExport);
hoverLabel = new JTextField();
hoverLabel.setEditable(false);
hoverLabel.setOpaque(false);
hoverLabel.setBorder(new BevelBorder(BevelBorder.RAISED));
hoverLabel.setFont(new java.awt.Font("Tahoma", 0, 11));
hoverWindow = new JWindow();
hoverWindow.setBackground(gradientColor);
hoverWindow.add(hoverLabel, BorderLayout.CENTER);
registerHoverInfo();
}
barRowPlotter = new BarRowPlotter(chartSettings, yAxisLabelRenderer);
lineRowPlotter = new LineRowPlotter(chartSettings, yAxisLabelRenderer);
cSplineRowPlotter = new CSplineRowPlotter(chartSettings, yAxisLabelRenderer);
}
public GraphPanelChart(boolean haveGUI) {
this(true, haveGUI);
}
public GraphPanelChart() {
this(false);
}
public boolean isModelContainsRow(AbstractGraphRow row) {
return rows.containsKey(row.getLabel());
}
public void setChartType(int type) {
chartType = type;
}
private boolean drawMessages(Graphics2D g) {
if (errorMessage != null) {
g.setColor(Color.RED);
g.drawString(errorMessage, g.getClipBounds().width / 2 - g.getFontMetrics(g.getFont()).stringWidth(errorMessage) / 2, g.getClipBounds().height / 2);
return true;
}
if (rows.isEmpty()) {
g.setColor(Color.BLACK);
g.drawString(NO_SAMPLES, g.getClipBounds().width / 2 - g.getFontMetrics(g.getFont()).stringWidth(NO_SAMPLES) / 2, g.getClipBounds().height / 2);
return true;
}
return false;
}
private void getMinMaxDataValues() {
maxXVal = 0L;
maxYVal = 0L;
minXVal = Long.MAX_VALUE;
minYVal = Double.MAX_VALUE;
Iterator> it = rows.entrySet().iterator();
Entry row;
AbstractGraphRow rowValue = null;
int barValue = 0;
while (it.hasNext()) {
row = it.next();
rowValue = row.getValue();
rowValue.setExcludeOutOfRangeValues(chartSettings.isPreventXAxisOverScaling());
if (!rowValue.isDrawOnChart()) {
continue;
}
if (rowValue.getMaxX() > maxXVal) {
maxXVal = rowValue.getMaxX();
}
if (rowValue.getMinX() < minXVal) {
minXVal = rowValue.getMinX();
}
double[] rowMinMaxY = rowValue.getMinMaxY(chartSettings.getMaxPointPerRow());
if (rowMinMaxY[1] > maxYVal) {
maxYVal = rowMinMaxY[1];
}
if (rowMinMaxY[0] < minYVal) {
//we draw only positives values
minYVal = rowMinMaxY[0] >= 0 ? rowMinMaxY[0] : 0;
}
if (rowValue.isDrawBar()) {
barValue = rowValue.getGranulationValue();
}
}
if (barValue > 0) {
maxXVal += barValue;
//find nice X steps
double barPerSquare = (double) (maxXVal - minXVal) / (barValue * gridLinesCount);
double step = Math.floor(barPerSquare) + 1;
maxXVal = (long) (minXVal + step * barValue * gridLinesCount);
}
//maxYVal *= 1 + (double) 1 / (double) gridLinesCount;
if (forcedMinX >= 0L) {
minXVal = forcedMinX;
}
//prevent X and Y axis not initialized in case of no row displayed
if (maxXVal == 0L
|| maxYVal == 0L
|| minXVal == Long.MAX_VALUE
|| minYVal == Double.MAX_VALUE) {
minYVal = 0;
maxYVal = 10;
//we take last known row to get x range
if (rowValue != null) {
maxXVal = rowValue.getMaxX();
minXVal = rowValue.getMinX();
}
} else if (chartSettings.isConfigOptimizeYAxis()) {
computeChartSteps();
} else {
minYVal = 0;
}
if (chartSettings.getForcedMaxY() > 0) {
maxYVal = Math.max(chartSettings.getForcedMaxY(), minYVal + 1);
}
}
/**
* compute minY and step value to have better readable charts
*/
private void computeChartSteps() {
//if special type
if (chartType == GraphPanelChart.CHART_PERCENTAGE) {
minYVal = 0;
maxYVal = 100;
return;
}
//try to find the best range...
//first, avoid special cases where maxY equal or close to minY
if (maxYVal - minYVal < 0.1) {
maxYVal = minYVal + 1;
}
//real step
double step = (maxYVal - minYVal) / gridLinesCount;
int pow = -1;
double factor = -1;
boolean found = false;
double testStep;
//find a step close to the real one
while (!found) {
pow++;
for (double f = 0; f < 10; f++) {
testStep = Math.pow(10, pow) * f;
if (testStep >= step) {
factor = f;
found = true;
break;
}
}
}
//first proposal
double foundStep = Math.pow(10, pow) * factor;
//we shift to the closest lower minval to align with the step
minYVal = minYVal - minYVal % foundStep;
//check if step is still good with minY trimmed. If not, use next factor.
if (minYVal + foundStep * gridLinesCount < maxYVal) {
foundStep = Math.pow(10, pow) * (factor + (pow > 0 ? 0.5 : 1));
}
//last visual optimization: find the optimal minYVal
double trim = 10;
while ((minYVal - minYVal % trim) + foundStep * gridLinesCount >= maxYVal && minYVal > 0) {
minYVal = minYVal - minYVal % trim;
trim = trim * 10;
}
//final calculation
maxYVal = minYVal + foundStep * gridLinesCount;
}
private void setDefaultDimensions(Graphics g) {
chartRect.setBounds(spacing, spacing, g.getClipBounds().width - spacing * 2, g.getClipBounds().height - spacing * 2);
legendRect.setBounds(zeroRect);
xAxisRect.setBounds(zeroRect);
yAxisRect.setBounds(zeroRect);
}
private int getYLabelsMaxWidth(FontMetrics fm) {
int ret = 0;
for (int i = 0; i <= gridLinesCount; i++) {
yAxisLabelRenderer.setValue((minYVal * gridLinesCount + i * (maxYVal - minYVal)) / gridLinesCount);
int current = fm.stringWidth(yAxisLabelRenderer.getText());
if (current > ret) {
ret = current;
}
}
return ret;
}
private void calculateYAxisDimensions(Graphics g) {
FontMetrics fm = g.getFontMetrics(g.getFont());
int axisWidth = getYLabelsMaxWidth(fm) + spacing * 3 + fm.getHeight();
yAxisRect.setBounds(chartRect.x, chartRect.y, axisWidth, chartRect.height);
if (!isPreview) {
chartRect.setBounds(chartRect.x + axisWidth, chartRect.y, chartRect.width - axisWidth, chartRect.height);
} else {
chartRect.setBounds(chartRect.x + previewInset, chartRect.y, chartRect.width, chartRect.height);
}
}
private void calculateXAxisDimensions(Graphics g) {
FontMetrics fm = g.getFontMetrics(g.getFont());
// we need to handle this and make Y axis wider
int axisHeight;
if (!isPreview) {
axisHeight = 2 * fm.getHeight() + spacing;//labels plus name
} else {
axisHeight = spacing;
}
xAxisLabelRenderer.setValue(maxXVal);
int axisEndSpace = fm.stringWidth(xAxisLabelRenderer.getText()) / 2;
xAxisRect.setBounds(chartRect.x, chartRect.y + chartRect.height - axisHeight, chartRect.width, axisHeight);
if (!isPreview) {
chartRect.setBounds(chartRect.x, chartRect.y, chartRect.width - axisEndSpace, chartRect.height - axisHeight);
} else {
chartRect.setBounds(chartRect.x, chartRect.y, chartRect.width - 2 * previewInset, chartRect.height - (previewInset + 1)); //+1 because layout take one pixel
}
yAxisRect.setBounds(yAxisRect.x, yAxisRect.y, yAxisRect.width, chartRect.height);
}
public void invalidateCache() {
cacheValid = false;
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
int witdh = this.getWidth();
int height = this.getHeight();
if (cacheHeight != height || cacheWitdh != witdh || gpcId != cacheOwner) {
cacheValid = false;
}
if (!cacheValid) {
if (cache == null || cacheHeight != height || cacheWitdh != witdh) {
cache = new BufferedImage(witdh, height, BufferedImage.TYPE_INT_ARGB);
}
Graphics2D g2d = cache.createGraphics();
g2d.setClip(0, 0, witdh, height);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
drawPanel(g2d);
cacheValid = true;
cacheHeight = height;
cacheWitdh = witdh;
cacheOwner = gpcId;
}
g.drawImage(cache, 0, 0, this);
}
private void drawPanel(Graphics2D g) {
drawPanel(g, true);
}
private void drawPanel(Graphics2D g, boolean drawHoverInfo) {
g.setColor(Color.white);
if (chartSettings.isDrawGradient()) {
GradientPaint gdp = new GradientPaint(0, 0, Color.white, 0, g.getClipBounds().height, gradientColor);
g.setPaint(gdp);
}
g.fillRect(0, 0, g.getClipBounds().width, g.getClipBounds().height);
paintAd(g);
if (drawMessages(g)) {
return;
}
setDefaultDimensions(g);
getMinMaxDataValues();
autoZoom_orig();
paintLegend(g);
calculateYAxisDimensions(g);
calculateXAxisDimensions(g);
paintYAxis(g);
paintXAxis(g);
paintChart(g);
if (drawHoverInfo) {
showHoverInfo();
}
}
private String getNiceNumber(int nb) {
nbFormatter.setValue(nb);
return nbFormatter.getText();
}
private void paintLegend(Graphics g) {
FontMetrics fm = g.getFontMetrics(g.getFont());
int rectH = fm.getHeight();
Iterator> it = rows.entrySet().iterator();
Entry row;
int currentX = chartRect.x;
int currentY = chartRect.y;
int legendHeight = it.hasNext() ? rectH + spacing : 0;
ColorsDispatcher colors = null;
if (reSetColors) {
colors = ColorsDispatcherFactory.getColorsDispatcher();
}
while (it.hasNext()) {
row = it.next();
//Color color = reSetColors ? colors.getNextColor() : row.getValue().getColor();
Color color = row.getValue().getColor() != null ? row.getValue().getColor() : colors.getNextColor();
if (!row.getValue().isShowInLegend() || !row.getValue().isDrawOnChart()) {
continue;
}
String rowLabel = row.getKey();
if (chartSettings.isExpendRows() && rowsZoomFactor.get(row.getKey()) != null) {
double zoomFactor = rowsZoomFactor.get(row.getKey());
if (zoomFactor != 1) {
if (zoomFactor > 1) {
int iZoomFactor = (int) zoomFactor;
rowLabel = rowLabel + " (x" + getNiceNumber(iZoomFactor) + ")";
} else {
rowLabel = rowLabel + " (x" + zoomFactor + ")";
}
}
}
// wrap row if overflowed
if (currentX + rectH + spacing / 2 + fm.stringWidth(rowLabel) > g.getClipBounds().width) {
currentY += rectH + spacing / 2;
legendHeight += rectH + spacing / 2;
currentX = chartRect.x;
}
// draw legend color box
g.setColor(color);
Composite oldComposite = null;
boolean isBarChart = row.getValue().isDrawBar() && chartSettings.getChartType() == ChartSettings.CHART_TYPE_DEFAULT || chartSettings.getChartType() == ChartSettings.CHART_TYPE_BAR;
if (isBarChart) {
oldComposite = ((Graphics2D) g).getComposite();
((Graphics2D) g).setComposite(chartSettings.getBarComposite());
}
Rectangle r = new Rectangle(currentX + legendAdjust + 1, currentY + legendAdjust + 1, rectH - 2 * legendAdjust, rectH - 2 * legendAdjust);
((Graphics2D) g).fill(r);
row.getValue().setLegendColorBox(r);
//g.fillRect(currentX+legendAdjust+1, currentY+legendAdjust+1, rectW-2*legendAdjust, rectH-2*legendAdjust);
if (isBarChart) {
((Graphics2D) g).setComposite(oldComposite);
}
g.setColor(color.darker());
g.drawRect(currentX + legendAdjust + 1, currentY + legendAdjust + 1, rectH - 2 * legendAdjust, rectH - 2 * legendAdjust);
g.setColor(Color.BLACK);
// draw legend item label
currentX += rectH + spacing / 2;
g.drawString(rowLabel, currentX, (int) (currentY + rectH * 0.9));
currentX += fm.stringWidth(rowLabel) + spacing;
}
legendRect.setBounds(chartRect.x, chartRect.y, chartRect.width, legendHeight);
chartRect.setBounds(chartRect.x, chartRect.y + legendHeight + spacing, chartRect.width, chartRect.height - legendHeight - spacing);
}
private void paintYAxis(Graphics g) {
FontMetrics fm = g.getFontMetrics(g.getFont());
String valueLabel;
int labelXPos;
int gridLineY;
// shift 2nd and more lines
int shift = 0;
// for strokes swapping
Stroke oldStroke = ((Graphics2D) g).getStroke();
//draw markers
g.setColor(axisColor);
for (int n = 0; n <= gridLinesCount; n++) {
gridLineY = chartRect.y + (int) ((gridLinesCount - n) * (double) chartRect.height / gridLinesCount);
g.drawLine(chartRect.x - 3, gridLineY, chartRect.x + 3, gridLineY);
}
for (int n = 0; n <= gridLinesCount; n++) {
//draw 2nd and more axis dashed and shifted
if (n != 0) {
((Graphics2D) g).setStroke(chartSettings.getDashStroke());
shift = 7;
}
gridLineY = chartRect.y + (int) ((gridLinesCount - n) * (double) chartRect.height / gridLinesCount);
// draw grid line with tick
g.setColor(axisColor);
g.drawLine(chartRect.x + shift, gridLineY, chartRect.x + chartRect.width, gridLineY);
g.setColor(Color.black);
// draw label
if (!isPreview) {
yAxisLabelRenderer.setValue((minYVal * gridLinesCount + n * (maxYVal - minYVal)) / gridLinesCount);
valueLabel = yAxisLabelRenderer.getText();
labelXPos = yAxisRect.x + yAxisRect.width - fm.stringWidth(valueLabel) - spacing - spacing / 2;
g.drawString(valueLabel, labelXPos, gridLineY + fm.getAscent() / 2);
}
}
if (!isPreview) {
Font oldFont = g.getFont();
g.setFont(g.getFont().deriveFont(Font.ITALIC));
// Create a rotation transformation for the font.
AffineTransform fontAT = new AffineTransform();
int delta = g.getFontMetrics(g.getFont()).stringWidth(yAxisLabel);
fontAT.rotate(-Math.PI / 2d);
g.setFont(g.getFont().deriveFont(fontAT));
g.drawString(yAxisLabel, yAxisRect.x + 15, yAxisRect.y + yAxisRect.height / 2 + delta / 2);
g.setFont(oldFont);
}
//restore stroke
((Graphics2D) g).setStroke(oldStroke);
}
private void paintXAxis(Graphics g) {
FontMetrics fm = g.getFontMetrics(g.getFont());
String valueLabel;
int labelXPos;
int gridLineX;
// shift 2nd and more lines
int shift = 0;
// for strokes swapping
Stroke oldStroke = ((Graphics2D) g).getStroke();
g.setColor(axisColor);
//draw markers
for (int n = 0; n <= gridLinesCount; n++) {
gridLineX = chartRect.x + (int) (n * ((double) chartRect.width / gridLinesCount));
g.drawLine(gridLineX, chartRect.y + chartRect.height - 3, gridLineX, chartRect.y + chartRect.height + 3);
}
for (int n = 0; n <= gridLinesCount; n++) {
//draw 2nd and more axis dashed and shifted
if (n != 0) {
((Graphics2D) g).setStroke(chartSettings.getDashStroke());
shift = 7;
}
gridLineX = chartRect.x + (int) (n * ((double) chartRect.width / gridLinesCount));
// draw grid line with tick
g.setColor(axisColor);
g.drawLine(gridLineX, chartRect.y + chartRect.height - shift, gridLineX, chartRect.y);
g.setColor(Color.black);
// draw label - keep decimal precision if range is too small
double labelValue = minXVal + n * (double) (maxXVal - minXVal) / gridLinesCount;
if ((maxXVal - minXVal < 2 * gridLinesCount) && (minXVal != maxXVal)) {
xAxisLabelRenderer.setValue(labelValue);
} else {
xAxisLabelRenderer.setValue((long) labelValue);
}
valueLabel = xAxisLabelRenderer.getText();
labelXPos = gridLineX - fm.stringWidth(valueLabel) / 2;
g.drawString(valueLabel, labelXPos, xAxisRect.y + fm.getAscent() + spacing);
}
Font oldFont = g.getFont();
g.setFont(g.getFont().deriveFont(Font.ITALIC));
//axis label
g.drawString(getXAxisLabel(), chartRect.x + chartRect.width / 2 - g.getFontMetrics(g.getFont()).stringWidth(getXAxisLabel()) / 2, xAxisRect.y + 2 * fm.getAscent() + spacing + 3);
g.setFont(oldFont);
//restore stroke
((Graphics2D) g).setStroke(oldStroke);
if (chartSettings.isDrawCurrentX()) {
gridLineX = chartRect.x + (int) ((currentXVal - minXVal) * (double) chartRect.width / (maxXVal - minXVal));
g.setColor(Color.GRAY);
g.drawLine(gridLineX, chartRect.y, gridLineX, chartRect.y + chartRect.height);
g.setColor(Color.black);
}
}
private void paintChart(Graphics g) {
g.setColor(Color.yellow);
Iterator> it;
ColorsDispatcher dispatcher = null;
if (reSetColors) {
dispatcher = ColorsDispatcherFactory.getColorsDispatcher();
}
//first we get the aggregate point factor if maxpoint is > 0;
limitPointFactor = 1;
if (chartSettings.getMaxPointPerRow() > 0) {
it = rows.entrySet().iterator();
while (it.hasNext()) {
Entry row = it.next();
int rowFactor = (int) Math.floor(row.getValue().size() / (double) chartSettings.getMaxPointPerRow()) + 1;
if (rowFactor > limitPointFactor) {
limitPointFactor = rowFactor;
}
}
}
//paint rows in 2 phases. Raws with draw label are drawn after to have label on top
it = rows.entrySet().iterator();
paintRows(g, dispatcher, it, false);
it = rows.entrySet().iterator();
paintRows(g, dispatcher, it, true);
}
private void paintRows(Graphics g, ColorsDispatcher dispatcher, Iterator> it, boolean rowsWithLabel) {
while (it.hasNext()) {
Entry row = it.next();
if (row.getValue().isDrawOnChart() && row.getValue().isDrawValueLabel() == rowsWithLabel) {
//eto Color color = reSetColors ? dispatcher.getNextColor() : row.getValue().getColor();
Color color = row.getValue().getColor() != null ? row.getValue().getColor() : dispatcher.getNextColor();
paintRow(g, row.getValue(), row.getKey(), color);
}
}
}
private void paintRow(Graphics g, AbstractGraphRow row, String rowLabel, Color color) {
if (row.isDrawLine() && chartSettings.getChartType() == ChartSettings.CHART_TYPE_DEFAULT
|| chartSettings.getChartType() == ChartSettings.CHART_TYPE_LINE) {
currentPlotter = lineRowPlotter;
} else if (row.isDrawBar() && chartSettings.getChartType() == ChartSettings.CHART_TYPE_DEFAULT
|| chartSettings.getChartType() == ChartSettings.CHART_TYPE_BAR) {
currentPlotter = barRowPlotter;
} else if (row.isDrawSpline() && chartSettings.getChartType() == ChartSettings.CHART_TYPE_DEFAULT
|| chartSettings.getChartType() == ChartSettings.CHART_TYPE_CSPLINE) {
currentPlotter = cSplineRowPlotter;
}
if (currentPlotter != null) {
double zoomFactor = 1;
if (chartSettings.isExpendRows() && rowsZoomFactor.get(rowLabel) != null) {
zoomFactor = rowsZoomFactor.get(rowLabel);
}
currentPlotter.setBoundsValues(chartRect, minXVal, maxXVal, minYVal, maxYVal);
currentPlotter.paintRow((Graphics2D) g, row, color, zoomFactor, limitPointFactor);
}
}
public void setRows(AbstractMap aRows) {
rows = aRows;
}
/**
* @param yAxisLabelRenderer the yAxisLabelRenderer to set
*/
public void setyAxisLabelRenderer(NumberRenderer yAxisLabelRenderer) {
this.yAxisLabelRenderer = yAxisLabelRenderer;
}
/**
* @param xAxisLabelRenderer the xAxisLabelRenderer to set
*/
public void setxAxisLabelRenderer(NumberRenderer xAxisLabelRenderer) {
this.xAxisLabelRenderer = xAxisLabelRenderer;
}
/**
* @param currentX the currentX to set
*/
public void setCurrentX(long currentX) {
this.currentXVal = currentX;
}
public void setForcedMinX(long minX) {
forcedMinX = minX;
}
//Paint the add the same color of the axis but with transparency
private void paintAd(Graphics2D g) {
Font oldFont = g.getFont();
g.setFont(g.getFont().deriveFont(10F));
g.setColor(axisColor);
Composite oldComposite = g.getComposite();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f));
g.drawString(AD_TEXT,
g.getClipBounds().width - g.getFontMetrics().stringWidth(AD_TEXT) - spacing,
g.getFontMetrics().getHeight() - spacing + 1);
g.setComposite(oldComposite);
g.setFont(oldFont);
}
/*
* Clear error messages
*/
public void clearErrorMessage() {
errorMessage = null;
}
/*
* Set error message if not null and not empty
* @param msg the error message to set
*/
public void setErrorMessage(String msg) {
if (msg != null && msg.trim().length() > 0) {
errorMessage = msg;
}
}
// Adding a popup menu to copy image in clipboard
@Override
public void lostOwnership(Clipboard clipboard, Transferable contents) {
// do nothing
}
private Image getImage() {
return getBufferedImage(this.getWidth(), this.getHeight());
}
private BufferedImage getBufferedImage(int w, int h) {
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setClip(0, 0, w, h);
drawPanel(g2, false);
return image;
}
/**
* Thanks to stephane.hoblingre
*/
private void registerPopup(boolean allowCsvExport) {
this.setComponentPopupMenu(popup);
JMenuItem itemCopy = new JMenuItem("Copy Image to Clipboard");
itemCopy.setIcon(new javax.swing.ImageIcon(getClass().getResource("/kg/apc/jmeter/img/copy.png")));
JMenuItem itemSave = new JMenuItem("Save Image as...");
itemSave.setIcon(new javax.swing.ImageIcon(getClass().getResource("/kg/apc/jmeter/img/save.png")));
JMenuItem itemExport = new JMenuItem("Export to CSV...");
itemExport.setIcon(new javax.swing.ImageIcon(getClass().getResource("/kg/apc/jmeter/img/export.png")));
itemCopy.addActionListener(new CopyAction());
itemSave.addActionListener(new SaveAction());
itemExport.addActionListener(new CsvExportAction());
popup.add(itemCopy);
popup.add(itemSave);
if (allowCsvExport) {
popup.addSeparator();
popup.add(itemExport);
}
}
private void registerHoverInfo() {
addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mousePressed(java.awt.event.MouseEvent evt) {
if (!isPreview && evt.getButton() == MouseEvent.BUTTON1) {
forceHoverPosition = true;
addMouseMotionListener(motionListener);
chartMouseMoved(evt);
}
}
@Override
public void mouseReleased(java.awt.event.MouseEvent evt) {
if (!isPreview) {
hideHoverInfo();
removeMouseMotionListener(motionListener);
}
}
@Override
public void mouseExited(java.awt.event.MouseEvent evt) {
if (!isPreview) {
hideHoverInfo();
}
}
});
}
private void hideHoverInfo() {
hoverWindow.setVisible(false);
xHoverInfo = -1;
yHoverInfo = -1;
}
private synchronized void showHoverInfo() {
if (isPreview
|| chartRect.width == 0 || chartRect.height == 0
|| xHoverInfo == -1 || yHoverInfo == -1) {
return;
}
long realX = minXVal + (maxXVal - minXVal) * (xHoverInfo - chartRect.x) / chartRect.width;
double realY = minYVal + (maxYVal - minYVal) * (chartRect.height - yHoverInfo + chartRect.y) / chartRect.height;
xAxisLabelRenderer.setValue(realX);
yAxisLabelRenderer.setValue(realY);
String hoverInfo = "(" + xAxisLabelRenderer.getText() + " ; " + yAxisLabelRenderer.getText() + ")";
hoverLabel.setText(hoverInfo);
int labelWidth = hoverLabel.getPreferredSize().width + 5;
int labelHeight = hoverLabel.getPreferredSize().height;
if (hoverWindow.getWidth() < labelWidth || hoverWindow.getHeight() < labelHeight) {
hoverWindow.setSize(labelWidth, labelHeight);
}
Point mousePos = MouseInfo.getPointerInfo().getLocation();
int hoverWindowX = mousePos.x + hoverGap;
int hoverWindowY = mousePos.y + hoverGap;
//we move window only if far from pointer to limit cpu
double deltaX = Math.abs(hoverWindow.getLocation().getX() - hoverWindowX);
double deltaY = Math.abs(hoverWindow.getLocation().getY() - hoverWindowY);
if (forceHoverPosition || deltaX >= hoverGap || deltaY >= hoverGap) {
//prevent out of screen
int correctedX = Math.min(hoverWindowX, Toolkit.getDefaultToolkit().getScreenSize().width - hoverWindow.getSize().width);
hoverWindow.setLocation(correctedX, hoverWindowY);
forceHoverPosition = false;
}
}
private void chartMouseMoved(java.awt.event.MouseEvent evt) {
int x = evt.getX();
int y = evt.getY();
if (x >= chartRect.x
&& x <= (chartRect.x + chartRect.width)
&& y >= chartRect.y
&& y <= (chartRect.y + chartRect.height)) {
xHoverInfo = x;
yHoverInfo = y;
showHoverInfo();
hoverWindow.setVisible(true);
} else {
hoverWindow.setVisible(false);
xHoverInfo = -1;
yHoverInfo = -1;
}
}
public void setUseRelativeTime(boolean selected) {
chartSettings.setUseRelativeTime(selected);
if (selected) {
setxAxisLabelRenderer(new DateTimeRenderer(DateTimeRenderer.HHMMSS, testStartTime));
} else {
setxAxisLabelRenderer(new DateTimeRenderer(DateTimeRenderer.HHMMSS));
}
}
private class CopyAction
implements ActionListener {
@Override
public void actionPerformed(final ActionEvent e) {
Clipboard clipboard = getToolkit().getSystemClipboard();
Transferable transferable = new Transferable() {
@Override
public Object getTransferData(DataFlavor flavor) {
if (isDataFlavorSupported(flavor)) {
return getImage();
}
return null;
}
@Override
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[]{
DataFlavor.imageFlavor
};
}
@Override
public boolean isDataFlavorSupported(DataFlavor flavor) {
return DataFlavor.imageFlavor.equals(flavor);
}
};
clipboard.setContents(transferable, GraphPanelChart.this);
}
}
public void saveGraphToPNG(File file, int w, int h) throws IOException {
log.info("Saving PNG to " + file.getAbsolutePath());
FileOutputStream fos = new FileOutputStream(file);
ImageIO.write(getBufferedImage(w, h), "png", fos);
fos.flush();
fos.close();
}
public void saveGraphToCSV(File file) throws IOException {
log.info("Saving CSV to " + file.getAbsolutePath());
GraphModelToCsvExporter exporter = new GraphModelToCsvExporter(rows, file, chartSettings.getConfigCsvSeparator(), xAxisLabel, xAxisLabelRenderer, chartSettings.getHideNonRepValLimit());
exporter.writeCsvFile();
}
private class SaveAction
implements ActionListener {
@Override
public void actionPerformed(final ActionEvent e) {
JFileChooser chooser = savePath != null ? new JFileChooser(new File(savePath)) : new JFileChooser(new File("."));
chooser.setFileFilter(new FileNameExtensionFilter("PNG Images", "png"));
int returnVal = chooser.showSaveDialog(GraphPanelChart.this);
if (returnVal == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
if (!file.getAbsolutePath().toUpperCase().endsWith(".PNG")) {
file = new File(file.getAbsolutePath() + ".png");
}
savePath = file.getParent();
boolean doSave = true;
if (file.exists()) {
int choice = JOptionPane.showConfirmDialog(GraphPanelChart.this, "Do you want to overwrite " + file.getAbsolutePath() + "?", "Save Image as", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
doSave = (choice == JOptionPane.YES_OPTION);
}
if (doSave) {
try {
saveGraphToPNG(file, getWidth(), getHeight());
} catch (IOException ex) {
JOptionPane.showConfirmDialog(GraphPanelChart.this, "Impossible to write the image to the file:\n" + ex.getMessage(), "Save Image as", JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE);
}
}
}
}
}
private class CsvExportAction
implements ActionListener {
@Override
public void actionPerformed(final ActionEvent e) {
JFileChooser chooser = savePath != null ? new JFileChooser(new File(savePath)) : new JFileChooser(new File("."));
chooser.setFileFilter(new FileNameExtensionFilter("CSV Files", "csv"));
int returnVal = chooser.showSaveDialog(GraphPanelChart.this);
if (returnVal == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
if (!file.getAbsolutePath().toUpperCase().endsWith(".CSV")) {
file = new File(file.getAbsolutePath() + ".csv");
}
savePath = file.getParent();
boolean doSave = true;
if (file.exists()) {
int choice = JOptionPane.showConfirmDialog(GraphPanelChart.this, "Do you want to overwrite " + file.getAbsolutePath() + "?", "Export to CSV File", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
doSave = (choice == JOptionPane.YES_OPTION);
}
if (doSave) {
try {
saveGraphToCSV(file);
} catch (IOException ex) {
JOptionPane.showConfirmDialog(GraphPanelChart.this, "Impossible to write the CSV file:\n" + ex.getMessage(), "Export to CSV File", JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE);
}
}
}
}
}
private class HoverMotionListener extends java.awt.event.MouseMotionAdapter implements Serializable {
@Override
public void mouseDragged(java.awt.event.MouseEvent evt) {
chartMouseMoved(evt);
}
}
}