package net.iponweb.disthene.reader.graph; import net.iponweb.disthene.reader.beans.TimeSeries; import net.iponweb.disthene.reader.beans.TimeSeriesOption; import net.iponweb.disthene.reader.exceptions.LogarithmicScaleNotAllowed; import net.iponweb.disthene.reader.graphite.utils.GraphiteUtils; import net.iponweb.disthene.reader.handler.parameters.ImageParameters; import net.iponweb.disthene.reader.handler.parameters.RenderParameters; import org.apache.log4j.Logger; import org.joda.time.DateTime; import org.joda.time.Seconds; import org.joda.time.format.DateTimeFormat; import javax.imageio.ImageIO; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * @author Andrei Ivanov * <p/> * This class and those below in hierarchy are pure translations from graphite-web Python code. * This will probably changed some day. But for now reverse engineering the logic is too comaplicated. */ public abstract class Graph { final static Logger logger = Logger.getLogger(Graph.class); private static final double[] PRETTY_VALUES = {0.1, 0.2, 0.25, 0.5, 1.0, 1.2, 1.25, 1.5, 2.0, 2.25, 2.5}; protected ImageParameters imageParameters; protected RenderParameters renderParameters; protected List<DecoratedTimeSeries> data = new ArrayList<>(); protected List<DecoratedTimeSeries> dataLeft = new ArrayList<>(); protected List<DecoratedTimeSeries> dataRight = new ArrayList<>(); protected boolean secondYAxis = false; protected int xMin; protected int xMax; protected int yMin; protected int yMax; protected int graphWidth; protected int graphHeight; protected long startTime = Long.MAX_VALUE; protected long endTime = Long.MIN_VALUE; protected DateTime startDateTime; protected DateTime endDateTime; protected double yStep; protected double yBottom; protected double yTop; protected double ySpan; protected double yScaleFactor; protected double yStepL; protected double yStepR; protected double yBottomL; protected double yBottomR; protected double yTopL; protected double yTopR; protected double ySpanL; protected double ySpanR; protected double yScaleFactorL; protected double yScaleFactorR; protected List<Double> yLabelValues; protected List<String> yLabels; protected int yLabelWidth; protected List<Double> yLabelValuesL; protected List<Double> yLabelValuesR; protected List<String> yLabelsL; protected List<String> yLabelsR; protected int yLabelWidthL; protected int yLabelWidthR; protected double xScaleFactor; protected XAxisConfig xAxisConfig; protected long xLabelStep; protected long xMinorGridStep; protected long xMajorGridStep; protected BufferedImage image; protected Graphics2D g2d; public static Graph getInstance(GraphType type, RenderParameters renderParameters, List<TimeSeries> data) { if (type.equals(GraphType.PIE)) { return new PieGraph(renderParameters, data); } else { return new LineGraph(renderParameters, data); } } public Graph(RenderParameters renderParameters, List<TimeSeries> data) { this.renderParameters = renderParameters; this.imageParameters = renderParameters.getImageParameters(); for (TimeSeries ts : data) { this.data.add(new DecoratedTimeSeries(ts)); } xMin = imageParameters.getMargin() + 10; xMax = imageParameters.getWidth() - imageParameters.getMargin(); yMin = imageParameters.getMargin(); yMax = imageParameters.getHeight() - imageParameters.getMargin(); image = new BufferedImage(imageParameters.getWidth(), imageParameters.getHeight(), BufferedImage.TYPE_INT_ARGB); g2d = image.createGraphics(); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); g2d.setPaint(imageParameters.getBackgroundColor()); g2d.fillRect(0, 0, imageParameters.getWidth(), imageParameters.getHeight()); } public abstract byte[] drawGraph() throws LogarithmicScaleNotAllowed; protected byte[] getBytes() { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { ImageIO.write(image, "png", baos); return baos.toByteArray(); } catch (IOException e) { logger.error(e); return new byte[0]; } } protected void drawText(int x, int y, String text, HorizontalAlign horizontalAlign, VerticalAlign verticalAlign) { drawText(x, y, text, imageParameters.getFont(), imageParameters.getForegroundColor(), horizontalAlign, verticalAlign, 0); } protected void drawText(int x, int y, String text, Font font, Color color, HorizontalAlign horizontalAlign, VerticalAlign verticalAlign) { drawText(x, y, text, font, color, horizontalAlign, verticalAlign, 0); } protected void drawText(int x, int y, String text, Font font, Color color, HorizontalAlign horizontalAlign, VerticalAlign verticalAlign, double rotate) { g2d.setPaint(color); g2d.setFont(font); FontMetrics fontMetrics = g2d.getFontMetrics(font); int textWidth = fontMetrics.stringWidth(text); int horizontal, vertical; switch (horizontalAlign) { case RIGHT: horizontal = textWidth; break; case CENTER: horizontal = textWidth / 2; break; default: horizontal = 0; break; } switch (verticalAlign) { case MIDDLE: vertical = fontMetrics.getHeight() / 2 - fontMetrics.getDescent(); break; case BOTTOM: vertical = -fontMetrics.getDescent(); break; case BASELINE: vertical = 0; break; default: vertical = fontMetrics.getAscent(); } double angle = Math.toRadians(rotate); AffineTransform orig = g2d.getTransform(); // g2d.rotate(angle, x, y); g2d.rotate(angle, x - Math.sin(Math.toRadians(angle) * vertical), y + Math.cos(Math.toRadians(angle) * vertical)); g2d.drawString(text, x - horizontal, y + vertical); g2d.setTransform(orig); } protected void drawVerticalTitle(Boolean alignRight) { Font font = new Font(imageParameters.getFont().getName(), imageParameters.getFont().getStyle(), (int) (imageParameters.getFont().getSize() + Math.log(imageParameters.getFont().getSize()))); FontMetrics fontMetrics = g2d.getFontMetrics(font); int lineHeight = fontMetrics.getHeight(); if (alignRight) { int x = xMax - lineHeight; int y = imageParameters.getHeight() / 2; String[] split = imageParameters.getVerticalTitle().split("\n"); for (String line : split) { drawText(x, y, line, font, imageParameters.getForegroundColor(), HorizontalAlign.CENTER, VerticalAlign.BASELINE, -90); x -= lineHeight; } xMax = x - imageParameters.getMargin() - lineHeight; } else { int x = xMin + lineHeight; int y = imageParameters.getHeight() / 2; String[] split = imageParameters.getVerticalTitle().split("\n"); for (String line : split) { drawText(x, y, line, font, imageParameters.getForegroundColor(), HorizontalAlign.CENTER, VerticalAlign.BASELINE, -90); x += lineHeight; } xMin = x + imageParameters.getMargin() + lineHeight; } } protected void drawTitle() { int y = yMin; int x = imageParameters.getWidth() / 2; Font font = new Font(imageParameters.getFont().getName(), imageParameters.getFont().getStyle(), (int) (imageParameters.getFont().getSize() + Math.log(imageParameters.getFont().getSize()))); FontMetrics fontMetrics = g2d.getFontMetrics(font); int lineHeight = fontMetrics.getHeight(); String[] split = imageParameters.getTitle().split("\n"); for (String line : split) { drawText(x, y, line, font, imageParameters.getForegroundColor(), HorizontalAlign.CENTER, VerticalAlign.TOP); y += lineHeight; } if (imageParameters.getyAxisSide().equals(ImageParameters.Side.RIGHT)) { yMin = y; } else { yMin = y + imageParameters.getMargin(); } } protected void drawLegend(List<String> legends, List<Color> colors, List<Boolean> secondYAxes, boolean uniqueLegend) { // remove duplicate names List<String> legendsUnique = new ArrayList<>(); List<Color> colorsUnique = new ArrayList<>(); List<Boolean> secondYAxesUnique = new ArrayList<>(); if (uniqueLegend) { for (int i = 0; i < legends.size(); i++) { if (!legendsUnique.contains(legends.get(i))) { legendsUnique.add(legends.get(i)); colorsUnique.add(colors.get(i)); secondYAxesUnique.add(secondYAxes.get(i)); } } legends = legendsUnique; colors = colorsUnique; secondYAxes = secondYAxesUnique; } FontMetrics fontMetrics = g2d.getFontMetrics(imageParameters.getFont()); // Check if there's enough room to use two columns boolean rightSideLabels = false; int padding = 5; String longestLegend = Collections.max(legends, new Comparator<String>() { @Override public int compare(String s1, String s2) { return s1.length() - s2.length(); } }); // Double it to check if there's enough room for 2 columns String testSizeName = longestLegend + " " + longestLegend; int testBoxSize = fontMetrics.getHeight() - 1; int testWidth = fontMetrics.stringWidth(testSizeName) + 2 * (testBoxSize + padding); if (testWidth + 50 < imageParameters.getWidth()) { rightSideLabels = true; } if (secondYAxis && rightSideLabels) { int boxSize = fontMetrics.getHeight() - 1; int lineHeight = fontMetrics.getHeight() + 1; int labelWidth = fontMetrics.stringWidth(longestLegend) + 2 * (boxSize + padding); int columns = (int) Math.max(1, Math.floor((imageParameters.getWidth() - xMin) / labelWidth)); int numRight = 0; for (Boolean b : secondYAxes) { if (b) numRight++; } int numberOfLines = Math.max(legends.size() - numRight, numRight); columns = (int) Math.floor(columns / 2.0); if (columns < 1) columns = 1; int legendHeight = (int) (Math.max(1, ((double) numberOfLines / columns)) * (lineHeight + padding)); yMax -= legendHeight; int x = xMin; int y = yMax + 2 * padding; int n = 0; int xRight = xMax - xMin; int yRight = y; int nRight = 0; for (int i = 0; i < legends.size(); i++) { g2d.setPaint(colors.get(i)); if (secondYAxes.get(i)) { nRight++; g2d.fillRect(xRight - padding, yRight, boxSize, boxSize); g2d.setPaint(ColorTable.DARK_GRAY); g2d.drawRect(xRight - padding, yRight, boxSize, boxSize); drawText(xRight - boxSize, yRight, legends.get(i), imageParameters.getFont(), imageParameters.getForegroundColor(), HorizontalAlign.RIGHT, VerticalAlign.TOP); xRight -= labelWidth; if (nRight % columns == 0) { xRight = xMax - xMin; yRight += lineHeight; } } else { n++; g2d.fillRect(x, y, boxSize, boxSize); g2d.setPaint(ColorTable.DARK_GRAY); g2d.drawRect(x, y, boxSize, boxSize); drawText(x + boxSize + padding, y, legends.get(i), imageParameters.getFont(), imageParameters.getForegroundColor(), HorizontalAlign.LEFT, VerticalAlign.TOP); x += labelWidth; if (n % columns == 0) { x = xMin; y += lineHeight; } } } } else { int boxSize = fontMetrics.getHeight() - 1; int lineHeight = fontMetrics.getHeight() + 1; int labelWidth = fontMetrics.stringWidth(longestLegend) + 2 * (boxSize + padding); int columns = (int) Math.floor(imageParameters.getWidth() / labelWidth); if (columns < 1) columns = 1; int numberOfLines = (int) Math.ceil((double) legends.size() / columns); int legendHeight = numberOfLines * (lineHeight + padding); yMax -= legendHeight; g2d.setStroke(new BasicStroke(1f)); int x = xMin; int y = yMax + (2 * padding); for (int i = 0; i < legends.size(); i++) { if (secondYAxes.get(i)) { g2d.setPaint(colors.get(i)); g2d.fillRect(x + labelWidth + padding, y, boxSize, boxSize); g2d.setPaint(ColorTable.DARK_GRAY); g2d.drawRect(x + labelWidth + padding, y, boxSize, boxSize); drawText(x + labelWidth, y, legends.get(i), imageParameters.getFont(), imageParameters.getForegroundColor(), HorizontalAlign.RIGHT, VerticalAlign.TOP); x += labelWidth; } else { g2d.setPaint(colors.get(i)); g2d.fillRect(x, y, boxSize, boxSize); g2d.setPaint(ColorTable.DARK_GRAY); g2d.drawRect(x, y, boxSize, boxSize); drawText(x + boxSize + padding, y, legends.get(i), imageParameters.getFont(), imageParameters.getForegroundColor(), HorizontalAlign.LEFT, VerticalAlign.TOP); x += labelWidth; } if ((i + 1) % columns == 0) { x = xMin; y += lineHeight; } } } } protected void consolidateDataPoints() { int numberOfPixels = (int) (xMax - xMin - imageParameters.getLineWidth() - 1); graphWidth = (int) (xMax - xMin - imageParameters.getLineWidth() - 1); for (DecoratedTimeSeries ts : data) { double numberOfDataPoints = ts.getValues().length; double divisor = ts.getValues().length - 1; double bestXStep = numberOfPixels / divisor; if (bestXStep < imageParameters.getMinXStep()) { int drawableDataPoints = numberOfPixels / imageParameters.getMinXStep(); double pointsPerPixel = Math.ceil(numberOfDataPoints / drawableDataPoints); ts.setValuesPerPoint((int) pointsPerPixel); ts.setxStep((numberOfPixels * pointsPerPixel) / numberOfDataPoints); } else { ts.setxStep(bestXStep); } } } protected void setupTwoYAxes() throws LogarithmicScaleNotAllowed { List<DecoratedTimeSeries> seriesWithMissingValuesL = new ArrayList<>(); List<DecoratedTimeSeries> seriesWithMissingValuesR = new ArrayList<>(); for (DecoratedTimeSeries ts : dataLeft) { for (Double value : ts.getValues()) { if (value == null) { seriesWithMissingValuesL.add(ts); break; } } } for (DecoratedTimeSeries ts : dataRight) { for (Double value : ts.getValues()) { if (value == null) { seriesWithMissingValuesR.add(ts); break; } } } double yMinValueL = Double.POSITIVE_INFINITY; double yMinValueR = Double.POSITIVE_INFINITY; double yMaxValueL; double yMaxValueR; if (imageParameters.isDrawNullAsZero() && seriesWithMissingValuesL.size() > 0) { yMinValueL = 0; } else { for (DecoratedTimeSeries ts : dataLeft) { if (!ts.hasOption(TimeSeriesOption.DRAW_AS_INFINITE)) { double mm = GraphUtils.safeMin(ts); yMinValueL = mm < yMinValueL ? mm : yMinValueL; } } } if (imageParameters.isDrawNullAsZero() && seriesWithMissingValuesR.size() > 0) { yMinValueR = 0; } else { for (DecoratedTimeSeries ts : dataRight) { if (!ts.hasOption(TimeSeriesOption.DRAW_AS_INFINITE)) { double mm = GraphUtils.safeMin(ts); yMinValueR = mm < yMinValueR ? mm : yMinValueR; } } } yMaxValueL = GraphUtils.safeMax(dataLeft); yMaxValueR = GraphUtils.safeMax(dataRight); /* if (getStackedData(dataLeft).size() > 0) { yMaxValueL = GraphUtils.maxSum(dataLeft); } else { yMaxValueL = GraphUtils.safeMax(dataLeft); } */ /* if (getStackedData(dataRight).size() > 0) { yMaxValueR = GraphUtils.maxSum(dataRight); } else { yMaxValueR = GraphUtils.safeMax(dataRight); } */ if (yMinValueL == Double.POSITIVE_INFINITY) { yMinValueL = 0.0; } if (yMinValueR == Double.POSITIVE_INFINITY) { yMinValueR = 0.0; } if (imageParameters.getyMaxLeft() < Double.POSITIVE_INFINITY) { yMaxValueL = imageParameters.getyMaxLeft(); } if (imageParameters.getyMaxRight() < Double.POSITIVE_INFINITY) { yMaxValueR = imageParameters.getyMaxRight(); } if (imageParameters.getyMinLeft() > Double.NEGATIVE_INFINITY) { yMinValueL = imageParameters.getyMinLeft(); } if (imageParameters.getyMinRight() > Double.NEGATIVE_INFINITY) { yMinValueR = imageParameters.getyMinRight(); } if (yMaxValueL <= yMinValueL) { yMaxValueL = yMinValueL + 1; } if (yMaxValueR <= yMinValueR) { yMaxValueR = yMinValueR + 1; } double yVarianceL = yMaxValueL - yMinValueL; double yVarianceR = yMaxValueR - yMinValueR; double orderL = Math.log10(yVarianceL); double orderR = Math.log10(yVarianceR); double orderFactorL = Math.pow(10, Math.floor(orderL)); double orderFactorR = Math.pow(10, Math.floor(orderR)); double vL = yVarianceL / orderFactorL; double vR = yVarianceR / orderFactorR; double distance = Double.POSITIVE_INFINITY; double prettyValueL = PRETTY_VALUES[0]; double prettyValueR = PRETTY_VALUES[0]; for (int i = 0; i < imageParameters.getyDivisors().size(); i++) { double q = vL / imageParameters.getyDivisors().get(i); double p = GraphUtils.closest(q, PRETTY_VALUES); if (Math.abs(q - p) < distance) { distance = Math.abs(q - p); prettyValueL = p; } } distance = Double.POSITIVE_INFINITY; for (int i = 0; i < imageParameters.getyDivisors().size(); i++) { double q = vR / imageParameters.getyDivisors().get(i); double p = GraphUtils.closest(q, PRETTY_VALUES); if (Math.abs(q - p) < distance) { distance = Math.abs(q - p); prettyValueR = p; } } yStepL = prettyValueL * orderFactorL; yStepR = prettyValueR * orderFactorR; if (imageParameters.getyStepLeft() < Double.POSITIVE_INFINITY) { yStepL = imageParameters.getyStepLeft(); } if (imageParameters.getyStepRight() < Double.POSITIVE_INFINITY) { yStepR = imageParameters.getyStepRight(); } yBottomL = yStepL * Math.floor(yMinValueL / yStepL); yBottomR = yStepR * Math.floor(yMinValueR / yStepR); yTopL = yStepL * Math.ceil(yMaxValueL / yStepL); yTopR = yStepR * Math.ceil(yMaxValueR / yStepR); if (imageParameters.getLogBase() != 0 && yMaxValueL > 0) { yBottomL = Math.pow(imageParameters.getLogBase(), Math.floor(Math.log(yMinValueL) / Math.log(imageParameters.getLogBase()))); yTopL = Math.pow(imageParameters.getLogBase(), Math.ceil(Math.log(yMaxValueL) / Math.log(imageParameters.getLogBase()))); } else if (imageParameters.getLogBase() != 0 && yMinValueL <= 0) { throw new LogarithmicScaleNotAllowed("Logarithmic scale specified with a dataset with a minimum value less than or equal to zero"); } if (imageParameters.getLogBase() != 0 && yMaxValueR > 0) { yBottomR = Math.pow(imageParameters.getLogBase(), Math.floor(Math.log(yMinValueR) / Math.log(imageParameters.getLogBase()))); yTopR = Math.pow(imageParameters.getLogBase(), Math.ceil(Math.log(yMaxValueR) / Math.log(imageParameters.getLogBase()))); } else if (imageParameters.getLogBase() != 0 && yMinValueR <= 0) { throw new LogarithmicScaleNotAllowed("Logarithmic scale specified with a dataset with a minimum value less than or equal to zero"); } if (imageParameters.getyMaxLeft() < Double.POSITIVE_INFINITY) { yTopL = imageParameters.getyMaxLeft(); } if (imageParameters.getyMaxRight() < Double.POSITIVE_INFINITY) { yTopR = imageParameters.getyMaxRight(); } if (imageParameters.getyMinLeft() > Double.NEGATIVE_INFINITY) { yBottomL = imageParameters.getyMinLeft(); } if (imageParameters.getyMinRight() > Double.NEGATIVE_INFINITY) { yBottomR = imageParameters.getyMinRight(); } ySpanL = yTopL - yBottomL; ySpanR = yTopR - yBottomR; if (ySpanL == 0) { yTopL++; ySpanL++; } if (ySpanR == 0) { yTopR++; ySpanR++; } graphHeight = yMax - yMin; yScaleFactorL = graphHeight / ySpanL; yScaleFactorR = graphHeight / ySpanR; // Round the values a bit yBottomR = GraphiteUtils.magicRound(yBottomR); yTopR = GraphiteUtils.magicRound(yTopR); yStepR = GraphiteUtils.magicRound(yStepR); yBottomL = GraphiteUtils.magicRound(yBottomL); yTopL = GraphiteUtils.magicRound(yTopL); yStepL = GraphiteUtils.magicRound(yStepL); yLabelValuesL = getYLabelValues(yBottomL, yTopL, yStepL); yLabelValuesR = getYLabelValues(yBottomR, yTopR, yStepR); FontMetrics fontMetrics = g2d.getFontMetrics(imageParameters.getFont()); yLabelsL = new ArrayList<>(); yLabelsR = new ArrayList<>(); for (Double value : yLabelValuesL) { String label = makeLabel(value, yStepL, ySpanL); yLabelsL.add(label); if (fontMetrics.stringWidth(label) > yLabelWidthL) yLabelWidthL = fontMetrics.stringWidth(label); } for (Double value : yLabelValuesR) { String label = makeLabel(value, yStepR, ySpanR); yLabelsR.add(label); if (fontMetrics.stringWidth(label) > yLabelWidthR) yLabelWidthR = fontMetrics.stringWidth(label); } int xxMin = (int) (imageParameters.getMargin() + (yLabelWidthL * 1.15)); if (xMin < xxMin) { xMin = xxMin; } int xxMax = (int) (imageParameters.getWidth() - (yLabelWidthR * 1.15)); if (xMax >= xxMax) { xMax = xxMax; } } protected void setupYAxis() throws LogarithmicScaleNotAllowed { List<DecoratedTimeSeries> seriesWithMissingValues = new ArrayList<>(); for (DecoratedTimeSeries ts : data) { for (Double value : ts.getValues()) { if (value == null) { seriesWithMissingValues.add(ts); break; } } } double yMinValue = Double.POSITIVE_INFINITY; for (DecoratedTimeSeries ts : data) { if (!ts.hasOption(TimeSeriesOption.DRAW_AS_INFINITE)) { double mm = GraphUtils.safeMin(ts); yMinValue = mm < yMinValue ? mm : yMinValue; } } if (yMinValue > 0 && imageParameters.isDrawNullAsZero() && seriesWithMissingValues.size() > 0) { yMinValue = 0; } double yMaxValue; yMaxValue = GraphUtils.safeMax(data); /* if (getStackedData(data).size() > 0) { yMaxValue = GraphUtils.maxSum(data); } else { yMaxValue = GraphUtils.safeMax(data); } */ if (yMaxValue < 0 && imageParameters.isDrawNullAsZero() && seriesWithMissingValues.size() > 0) { yMaxValue = 0; } if (yMinValue == Double.POSITIVE_INFINITY) { yMinValue = 0; } if (yMaxValue == Double.NEGATIVE_INFINITY) { yMaxValue = 0; } if (imageParameters.getyMax() != Double.POSITIVE_INFINITY) { yMaxValue = imageParameters.getyMax(); } if (imageParameters.getyMin() != Double.NEGATIVE_INFINITY) { yMinValue = imageParameters.getyMin(); } if (yMaxValue <= yMinValue) { yMaxValue = yMinValue + 1; } double yVariance = yMaxValue - yMinValue; double order; double orderFactor; order = Math.log10(yVariance); orderFactor = Math.pow(10, Math.floor(order)); double v = yVariance / orderFactor; double distance = Double.POSITIVE_INFINITY; double prettyValue = PRETTY_VALUES[0]; for (int i = 0; i < imageParameters.getyDivisors().size(); i++) { double q = v / imageParameters.getyDivisors().get(i); double p = GraphUtils.closest(q, PRETTY_VALUES); if (Math.abs(q - p) < distance) { distance = Math.abs(q - p); prettyValue = p; } } yStep = prettyValue * orderFactor; if (imageParameters.getyStep() < Double.POSITIVE_INFINITY) { yStep = imageParameters.getyStep(); } yBottom = yStep * Math.floor(yMinValue / yStep); yTop = yStep * Math.ceil(yMaxValue / yStep); if (imageParameters.getLogBase() != 0 && yMaxValue > 0) { yBottom = Math.pow(imageParameters.getLogBase(), Math.floor(Math.log(yMinValue) / Math.log(imageParameters.getLogBase()))); yTop = Math.pow(imageParameters.getLogBase(), Math.ceil(Math.log(yMaxValue) / Math.log(imageParameters.getLogBase()))); } else if (imageParameters.getLogBase() != 0 && yMinValue <= 0) { throw new LogarithmicScaleNotAllowed("Logarithmic scale specified with a dataset with a minimum value less than or equal to zero"); } if (imageParameters.getyMax() != Double.POSITIVE_INFINITY) { yTop = imageParameters.getyMax(); } if (imageParameters.getyMin() != Double.NEGATIVE_INFINITY) { yBottom = imageParameters.getyMin(); } ySpan = yTop - yBottom; if (ySpan == 0) { yTop++; ySpan++; } graphHeight = yMax - yMin; yScaleFactor = graphHeight / ySpan; // Round the values a bit yBottom = GraphiteUtils.magicRound(yBottom); yTop = GraphiteUtils.magicRound(yTop); yStep = GraphiteUtils.magicRound(yStep); if (!imageParameters.isHideAxes()) { yLabelValues = getYLabelValues(yBottom, yTop, yStep); yLabels = new ArrayList<>(); yLabelWidth = 0; FontMetrics fontMetrics = g2d.getFontMetrics(imageParameters.getFont()); for (Double value : yLabelValues) { String label = makeLabel(value); yLabels.add(label); if (fontMetrics.stringWidth(label) > yLabelWidth) yLabelWidth = fontMetrics.stringWidth(label); } if (!imageParameters.isHideYAxis()) { if (imageParameters.getyAxisSide().equals(ImageParameters.Side.LEFT)) { int xxMin = (int) (imageParameters.getMargin() + yLabelWidth * 1.15); if (xMin < xxMin) { xMin = xxMin; } } else { int xxMax = imageParameters.getWidth() - imageParameters.getMargin() - (int) (yLabelWidth * 1.15); if (xMax >= xxMax) { xMax = xxMax; } } } } else { yLabelValues = new ArrayList<>(); yLabels = new ArrayList<>(); yLabelWidth = 0; } } protected void setupXAxis() { startDateTime = new DateTime(startTime * 1000, renderParameters.getTz()); endDateTime = new DateTime(endTime * 1000, renderParameters.getTz()); double secondsPerPixel = (endTime - startTime) / (double) graphWidth; xScaleFactor = (double) graphWidth / (endTime - startTime); xAxisConfig = XAxisConfigProvider.getXAxisConfig(secondsPerPixel, endTime - startTime); xLabelStep = xAxisConfig.getLabelUnit() * xAxisConfig.getLabelStep(); xMinorGridStep = (long) (xAxisConfig.getMinorGridUnit() * xAxisConfig.getMinorGridStep()); xMajorGridStep = xAxisConfig.getMajorGridUnit() * xAxisConfig.getMajorGridStep(); } protected void drawLabels() { // Draw the Y-labels if (!imageParameters.isHideYAxis()) { if (!secondYAxis) { for (int i = 0; i < yLabelValues.size(); i++) { int x; if (imageParameters.getyAxisSide().equals(ImageParameters.Side.LEFT)) { x = (int) (xMin - (yLabelWidth * 0.15)); } else { x = (int) (xMax + (yLabelWidth * 0.15)); } int y = getYCoord(yLabelValues.get(i)); if (y < 0) y = 0; if (imageParameters.getyAxisSide().equals(ImageParameters.Side.LEFT)) { drawText(x, y, yLabels.get(i), HorizontalAlign.RIGHT, VerticalAlign.MIDDLE); } else { drawText(x, y, yLabels.get(i), HorizontalAlign.LEFT, VerticalAlign.MIDDLE); } } } else { for (int i = 0; i < yLabelValuesL.size(); i++) { int x = (int) (xMin - (yLabelWidthL * 0.15)); int y = getYCoordLeft(yLabelValuesL.get(i)); if (y < 0) y = 0; drawText(x, y, yLabelsL.get(i), HorizontalAlign.RIGHT, VerticalAlign.MIDDLE); } for (int i = 0; i < yLabelValuesR.size(); i++) { int x = (int) (xMax + (yLabelWidthR * 0.15)); int y = getYCoordRight(yLabelValuesR.get(i)); if (y < 0) y = 0; drawText(x, y, yLabelsR.get(i), HorizontalAlign.LEFT, VerticalAlign.MIDDLE); } } } // Draw the X-labels long labelDt = 0; long labelDelta = 1; if (xAxisConfig.getLabelUnit() == XAxisConfigProvider.SEC) { labelDt = startTime - startTime % xAxisConfig.getLabelStep(); labelDelta = (long) xAxisConfig.getLabelStep(); } else if (xAxisConfig.getLabelUnit() == XAxisConfigProvider.MIN) { DateTime tdt = new DateTime(startTime * 1000, renderParameters.getTz()); labelDt = tdt.withSecondOfMinute(0).withMinuteOfHour(tdt.getMinuteOfHour() - (tdt.getMinuteOfHour() % xAxisConfig.getLabelStep())).getMillis() / 1000; labelDelta = (long) xAxisConfig.getLabelStep() * 60; } else if (xAxisConfig.getLabelUnit() == XAxisConfigProvider.HOUR) { DateTime tdt = new DateTime(startTime * 1000, renderParameters.getTz()); labelDt = tdt.withSecondOfMinute(0).withMinuteOfHour(0).withHourOfDay(tdt.getHourOfDay() - (tdt.getHourOfDay() % xAxisConfig.getLabelStep())).getMillis() / 1000; labelDelta = (long) xAxisConfig.getLabelStep() * 60 * 60; } else if (xAxisConfig.getLabelUnit() == XAxisConfigProvider.DAY) { DateTime tdt = new DateTime(startTime * 1000, renderParameters.getTz()); labelDt = tdt.withSecondOfMinute(0).withMinuteOfHour(0).withHourOfDay(0).getMillis() / 1000; labelDelta = (long) xAxisConfig.getLabelStep() * 60 * 60 * 24; } while (labelDt < startTime) labelDt += labelDelta; DateTime ddt = new DateTime(labelDt * 1000, renderParameters.getTz()); FontMetrics fontMetrics = g2d.getFontMetrics(imageParameters.getFont()); while (ddt.isBefore(endDateTime)) { String label = ddt.toString(DateTimeFormat.forPattern(xAxisConfig.getFormat())); int x = (int) (xMin + (Seconds.secondsBetween(startDateTime, ddt).getSeconds() * xScaleFactor)); int y = yMax + fontMetrics.getMaxAscent(); drawText(x, y, label, HorizontalAlign.CENTER, VerticalAlign.TOP); ddt = ddt.plusSeconds((int) labelDelta); } } protected void drawGridLines() { g2d.setStroke(new BasicStroke(0f)); //Horizontal grid lines int leftSide = xMin; int rightSide = xMax; List<Double> labelValues = secondYAxis ? yLabelValuesL : yLabelValues; for (int i = 0; i < labelValues.size(); i++) { g2d.setColor(imageParameters.getMajorGridLineColor()); int y = secondYAxis ? getYCoordLeft(labelValues.get(i)) : getYCoord(labelValues.get(i)); if (y < 0) continue; g2d.drawLine(leftSide, y, rightSide, y); // draw minor gridlines if this isn't the last label g2d.setColor(imageParameters.getMinorGridLineColor()); if (imageParameters.getMinorY() >= 1 && i < (labelValues.size() - 1)) { double distance = ((labelValues.get(i + 1) - labelValues.get(i)) / (1 + imageParameters.getMinorY())); for (int minor = 0; minor < imageParameters.getMinorY(); minor++) { double minorValue = (labelValues.get(i) + ((1 + minor) * distance)); int yTopFactor = imageParameters.getLogBase() != 0 ? (int) (imageParameters.getLogBase() * imageParameters.getLogBase()) : 1; if (secondYAxis) { if (minorValue > yTopFactor * yTopL) continue; } else { if (minorValue > yTopFactor * yTop) continue; } int yMinor = secondYAxis ? getYCoordLeft(minorValue) : getYCoord(minorValue); if (yMinor < 0) continue; g2d.drawLine(leftSide, yMinor, rightSide, yMinor); } } } // Vertical grid lines int top = yMin; int bottom = yMax; long dt = 0; long delta = 1; if (xAxisConfig.getMinorGridUnit() == XAxisConfigProvider.SEC) { dt = startTime - (long) (startTime % xAxisConfig.getMinorGridStep()); delta = (long) xAxisConfig.getMinorGridStep(); } else if (xAxisConfig.getMinorGridUnit() == XAxisConfigProvider.MIN) { DateTime tdt = new DateTime(startTime * 1000, renderParameters.getTz()); dt = tdt.withSecondOfMinute(0).withMinuteOfHour((int) (tdt.getMinuteOfHour() - (tdt.getMinuteOfHour() % xAxisConfig.getMinorGridStep()))).getMillis() / 1000; delta = (long) xAxisConfig.getMinorGridStep() * 60; } else if (xAxisConfig.getMinorGridUnit() == XAxisConfigProvider.HOUR) { DateTime tdt = new DateTime(startTime * 1000, renderParameters.getTz()); dt = tdt.withSecondOfMinute(0).withMinuteOfHour(0).withHourOfDay((int) (tdt.getHourOfDay() - (tdt.getHourOfDay() % xAxisConfig.getMinorGridStep()))).getMillis() / 1000; delta = (long) xAxisConfig.getMinorGridStep() * 60 * 60; } else if (xAxisConfig.getMinorGridUnit() == XAxisConfigProvider.DAY) { DateTime tdt = new DateTime(startTime * 1000, renderParameters.getTz()); dt = tdt.withSecondOfMinute(0).withMinuteOfHour(0).withHourOfDay(0).getMillis() / 1000; delta = (long) xAxisConfig.getMinorGridStep() * 60 * 60 * 24; } while (dt < startTime) dt += delta; DateTime ddt = new DateTime(dt * 1000, renderParameters.getTz()); g2d.setColor(imageParameters.getMinorGridLineColor()); while (ddt.isBefore(endDateTime)) { int x = (int) (xMin + (Seconds.secondsBetween(startDateTime, ddt).getSeconds() * xScaleFactor)); if (x < xMax) { g2d.drawLine(x, bottom, x, top); } ddt = ddt.plusSeconds((int) delta); } // Now we do the major grid lines g2d.setColor(imageParameters.getMajorGridLineColor()); long majorDt = 0; long majorDelta = 1; if (xAxisConfig.getMajorGridUnit() == XAxisConfigProvider.SEC) { majorDt = startTime - startTime % xAxisConfig.getMajorGridStep(); majorDelta = (long) xAxisConfig.getMajorGridStep(); } else if (xAxisConfig.getMajorGridUnit() == XAxisConfigProvider.MIN) { DateTime tdt = new DateTime(startTime * 1000, renderParameters.getTz()); majorDt = tdt.withSecondOfMinute(0).withMinuteOfHour(tdt.getMinuteOfHour() - (tdt.getMinuteOfHour() % xAxisConfig.getMajorGridStep())).getMillis() / 1000; majorDelta = (long) xAxisConfig.getMajorGridStep() * 60; } else if (xAxisConfig.getMajorGridUnit() == XAxisConfigProvider.HOUR) { DateTime tdt = new DateTime(startTime * 1000, renderParameters.getTz()); majorDt = tdt.withSecondOfMinute(0).withMinuteOfHour(0).withHourOfDay(tdt.getHourOfDay() - (tdt.getHourOfDay() % xAxisConfig.getMajorGridStep())).getMillis() / 1000; majorDelta = (long) xAxisConfig.getMajorGridStep() * 60 * 60; } else if (xAxisConfig.getMajorGridUnit() == XAxisConfigProvider.DAY) { DateTime tdt = new DateTime(startTime * 1000, renderParameters.getTz()); majorDt = tdt.withSecondOfMinute(0).withMinuteOfHour(0).withHourOfDay(0).getMillis() / 1000; majorDelta = (long) xAxisConfig.getMajorGridStep() * 60 * 60 * 24; } while (majorDt < startTime) majorDt += majorDelta; ddt = new DateTime(majorDt * 1000, renderParameters.getTz()); while (ddt.isBefore(endDateTime)) { int x = (int) (xMin + (Seconds.secondsBetween(startDateTime, ddt).getSeconds() * xScaleFactor)); if (x < xMax) { g2d.drawLine(x, bottom, x, top); } ddt = ddt.plusSeconds((int) majorDelta); } //Draw side borders for our graph area g2d.drawLine(xMax, bottom, xMax, top); g2d.drawLine(xMin, bottom, xMin, top); } private void drawLines(List<DecoratedTimeSeries> timeSeriesList) { Rectangle rectangle = new Rectangle(xMin, yMin, xMax - xMin + 1, yMax - yMin + 1); g2d.clip(rectangle); for (DecoratedTimeSeries ts : timeSeriesList) { g2d.setStroke(getStroke(ts)); g2d.setColor(getColor(ts)); GeneralPath path = new GeneralPath(); double x = xMin; int y; Double[] values = ts.getConsolidatedValues(); int consecutiveNulls = 0; boolean allNullsSoFar = true; for (Double value : values) { Double adjustedValue = value; if (adjustedValue == null && imageParameters.isDrawNullAsZero()) adjustedValue = 0.; if (adjustedValue == null) { /* if (consecutiveNulls == 0) { path.lineTo(x, y); } */ x += ts.getxStep(); consecutiveNulls++; continue; } if (secondYAxis) { if (ts.hasOption(TimeSeriesOption.SECOND_Y_AXIS)) { y = getYCoordRight(adjustedValue); } else { y = getYCoordLeft(adjustedValue); } } else { y = getYCoord(adjustedValue); } y = y < 0 ? 0 : y; if (path.getCurrentPoint() == null) { path.moveTo(x, y); } if (ts.hasOption(TimeSeriesOption.DRAW_AS_INFINITE) && adjustedValue > 0) { path.moveTo((int) x, yMax); path.lineTo((int) x, yMin); x += ts.getxStep(); continue; } if (imageParameters.getLineMode().equals(ImageParameters.LineMode.SLOPE)) { if (consecutiveNulls > 0) { path.moveTo(x, y); } path.lineTo(x, y); } else if (imageParameters.getLineMode().equals(ImageParameters.LineMode.STAIRCASE)) { if (consecutiveNulls > 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } path.lineTo(x + ts.getxStep(), y); } else if (imageParameters.getLineMode().equals(ImageParameters.LineMode.CONNECTED)) { if (consecutiveNulls > imageParameters.getConnectedLimit() || allNullsSoFar) { path.moveTo(x, y); allNullsSoFar = false; } path.lineTo((int) x, y); } consecutiveNulls = 0; x += ts.getxStep(); } g2d.draw(path); } } private void drawStacked(List<DecoratedTimeSeries> timeSeriesList) { if (timeSeriesList.size() == 0) return; Shape savedClip = g2d.getClip(); g2d.clip(new Rectangle(xMin, yMin, xMax - xMin, yMax - yMin)); for (DecoratedTimeSeries ts : timeSeriesList) { // We will be constructing general path for each time series GeneralPath path = new GeneralPath(); path.moveTo(xMin, yMax); g2d.setPaint(getPaint(ts)); double x = xMin; double startX = x; int y = yMax; Double[] values = ts.getConsolidatedValues(); int consecutiveNulls = 0; boolean allNullsSoFar = true; for (Double value : values) { Double adjustedValue = value; if (value == null && imageParameters.isDrawNullAsZero()) adjustedValue = 0.; if (adjustedValue == null) { if (consecutiveNulls == 0) { path.lineTo(x, y); if (secondYAxis) { if (ts.hasOption(TimeSeriesOption.SECOND_Y_AXIS)) { fillAreaAndClip(path, x, startX, getYCoordRight(0)); } else { fillAreaAndClip(path, x, startX, getYCoordLeft(0)); } } else { fillAreaAndClip(path, x, startX, getYCoord(0)); } } x += ts.getxStep(); consecutiveNulls++; } else { if (secondYAxis) { if (ts.hasOption(TimeSeriesOption.SECOND_Y_AXIS)) { y = getYCoordRight(adjustedValue); } else { y = getYCoordLeft(adjustedValue); } } else { y = getYCoord(adjustedValue); } y = y < 0 ? 0 : y; if (consecutiveNulls > 0) startX = x; if (imageParameters.getLineMode().equals(ImageParameters.LineMode.STAIRCASE)) { if (consecutiveNulls > 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } x += ts.getxStep(); path.lineTo(x, y); } else if (imageParameters.getLineMode().equals(ImageParameters.LineMode.SLOPE)) { if (consecutiveNulls > 0) { path.moveTo(x, y); } path.lineTo(x, y); x += ts.getxStep(); } else if (imageParameters.getLineMode().equals(ImageParameters.LineMode.CONNECTED)) { if (consecutiveNulls > imageParameters.getConnectedLimit() || allNullsSoFar) { path.moveTo(x, y); allNullsSoFar = false; } path.lineTo(x, y); x += ts.getxStep(); } consecutiveNulls = 0; } } double xPos; if (imageParameters.getLineMode().equals(ImageParameters.LineMode.STAIRCASE)) { xPos = x; } else { xPos = x - ts.getxStep(); } if (consecutiveNulls == 0) { if (secondYAxis) { if (ts.hasOption(TimeSeriesOption.SECOND_Y_AXIS)) { fillAreaAndClip(path, xPos, startX, getYCoordRight(0)); } else { fillAreaAndClip(path, xPos, startX, getYCoordLeft(0)); } } else { fillAreaAndClip(path, xPos, startX, getYCoord(0)); } } } g2d.setClip(savedClip); } protected void fillAreaAndClip(GeneralPath path, double x, double startX, int yTo) { GeneralPath pattern = new GeneralPath(path); path.lineTo(x, yTo); path.lineTo(startX, yTo); path.closePath(); g2d.fill(path); pattern.lineTo(x, yTo); pattern.lineTo(xMax, yTo); pattern.lineTo(xMax, yMin); pattern.lineTo(xMin, yMin); pattern.lineTo(xMin, yTo); pattern.lineTo(startX, yTo); pattern.lineTo(x, yTo); pattern.lineTo(xMax, yTo); pattern.lineTo(xMax, yMax); pattern.lineTo(xMin, yMax); pattern.lineTo(xMin, yTo); pattern.lineTo(startX, yTo); pattern.closePath(); g2d.clip(pattern); } protected void drawData() { g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); drawStacked(getStackedData(data)); drawLines(getLineData(data)); } private List<DecoratedTimeSeries> getLineData(List<DecoratedTimeSeries> data) { List<DecoratedTimeSeries> result = new ArrayList<>(); for (DecoratedTimeSeries ts : data) { if (!ts.hasOption(TimeSeriesOption.STACKED)) { result.add(ts); } } return result; } protected List<DecoratedTimeSeries> getStackedData(List<DecoratedTimeSeries> data) { List<DecoratedTimeSeries> result = new ArrayList<>(); for (DecoratedTimeSeries ts : data) { if (ts.hasOption(TimeSeriesOption.STACKED)) { result.add(ts); } } return result; } private int getYCoordRight(double value) { double highestValue = yLabelValuesR.size() > 0 ? Collections.max(yLabelValuesR) : yTopR; double lowestValue = yLabelValuesR.size() > 0 ? Collections.min(yLabelValuesR) : yBottomR; int pixelRange = yMax - yMin; double relativeValue = value - lowestValue; double valueRange = highestValue - lowestValue; if (imageParameters.getLogBase() != 0) { if (value < 0) { return -1; } relativeValue = Math.log(value) / Math.log(imageParameters.getLogBase()) - Math.log(lowestValue) / Math.log(imageParameters.getLogBase()); valueRange = Math.log(highestValue) / Math.log(imageParameters.getLogBase()) - Math.log(lowestValue) / Math.log(imageParameters.getLogBase()); } double pixelToValueRatio = pixelRange / valueRange; double valueInPixels = pixelToValueRatio * relativeValue; return (int) (yMax - valueInPixels); } private int getYCoordLeft(double value) { double highestValue = yLabelValuesL.size() > 0 ? Collections.max(yLabelValuesL) : yTopL; double lowestValue = yLabelValuesL.size() > 0 ? Collections.min(yLabelValuesL) : yBottomL; int pixelRange = yMax - yMin; double relativeValue = value - lowestValue; double valueRange = highestValue - lowestValue; if (imageParameters.getLogBase() != 0) { if (value < 0) { return -1; } relativeValue = Math.log(value) / Math.log(imageParameters.getLogBase()) - Math.log(lowestValue) / Math.log(imageParameters.getLogBase()); valueRange = Math.log(highestValue) / Math.log(imageParameters.getLogBase()) - Math.log(lowestValue) / Math.log(imageParameters.getLogBase()); } double pixelToValueRatio = pixelRange / valueRange; double valueInPixels = pixelToValueRatio * relativeValue; return (int) (yMax - valueInPixels); } private int getYCoord(double value) { double highestValue = yLabelValues.size() > 0 ? Collections.max(yLabelValues) : yTop; double lowestValue = yLabelValues.size() > 0 ? Collections.min(yLabelValues) : yBottom; int pixelRange = yMax - yMin; double relativeValue = value - lowestValue; double valueRange = highestValue - lowestValue; if (imageParameters.getLogBase() != 0) { if (value < 0) { return -1; } relativeValue = Math.log(value) / Math.log(imageParameters.getLogBase()) - Math.log(lowestValue) / Math.log(imageParameters.getLogBase()); valueRange = Math.log(highestValue) / Math.log(imageParameters.getLogBase()) - Math.log(lowestValue) / Math.log(imageParameters.getLogBase()); } double pixelToValueRatio = pixelRange / valueRange; double valueInPixels = pixelToValueRatio * relativeValue; return (int) (yMax - valueInPixels); } private List<Double> getYLabelValues(double min, double max, double step) { if (imageParameters.getLogBase() != 0) { return logRange(imageParameters.getLogBase(), min, max); } else { return fRange(step, min, max); } } protected String makeLabel(double value, double step, double span) { double tmpValue = GraphiteUtils.formatUnitValue(value, step, imageParameters.getyUnitSystem()); String prefix = GraphiteUtils.formatUnitPrefix(value, step, imageParameters.getyUnitSystem()); double ySpan = GraphiteUtils.formatUnitValue(span, step, imageParameters.getyUnitSystem()); String spanPrefix = GraphiteUtils.formatUnitPrefix(span, step, imageParameters.getyUnitSystem()); value = tmpValue; if (value < 0.1) { return value + " " + prefix; } else if (value < 1.0) { return String.format("%.2f %s", value, prefix); } if (ySpan > 10 || !spanPrefix.equals(prefix)) { return String.format("%s %s", value, prefix); } else if (ySpan > 3) { return String.format("%.1f %s", value, prefix); } else if (ySpan > 0.1) { return String.format("%.2f %s", value, prefix); } else { return value + prefix; } } protected String makeLabel(double value) { return makeLabel(value, yStep, ySpan); } private List<Double> logRange(double base, double min, double max) { List<Double> result = new ArrayList<>(); double current = min; if (min > 0) { current = Math.floor(Math.log(min) / Math.log(base)); } double factor = current; while (current < max) { current = Math.pow(base, factor); result.add(current); factor++; } return result; } // todo: this "magic rounding" is a complete atrocity - fix it! private List<Double> fRange(double step, double min, double max) { List<Double> result = new ArrayList<>(); BigDecimal bf = BigDecimal.valueOf(min); BigDecimal bMax = BigDecimal.valueOf(max); BigDecimal bMin = BigDecimal.valueOf(min); BigDecimal bStep = BigDecimal.valueOf(step); while (bf.compareTo(bMax) <= 0) { result.add(GraphiteUtils.magicRound(bf).doubleValue()); bf = bf.add(bStep); if (bf.compareTo(bMin) == 0) { result.add(max); break; } } return result; } private Color getColor(DecoratedTimeSeries timeSeries) { if (timeSeries.hasOption(TimeSeriesOption.INVISIBLE)) { return ColorTable.INVISIBLE; } Color c = (Color) timeSeries.getOption(TimeSeriesOption.COLOR); return new Color(c.getRed(), c.getGreen(), c.getBlue(), timeSeries.hasOption(TimeSeriesOption.ALPHA) ? (int) ((Float) timeSeries.getOption(TimeSeriesOption.ALPHA) * 255) : 255); } private Color getPaint(DecoratedTimeSeries timeSeries) { if (timeSeries.hasOption(TimeSeriesOption.INVISIBLE)) { return ColorTable.INVISIBLE; } Color c = (Color) timeSeries.getOption(TimeSeriesOption.COLOR); return new Color(c.getRed(), c.getGreen(), c.getBlue(), (int)((float) (imageParameters.getAreaAlpha() * 255))); } private Stroke getStroke(DecoratedTimeSeries timeSeries) { float lineWidth; if (timeSeries.hasOption(TimeSeriesOption.LINE_WIDTH)) { lineWidth = (float) timeSeries.getOption(TimeSeriesOption.LINE_WIDTH); } else { lineWidth = imageParameters.getLineWidth().floatValue(); } boolean isDashed = false; float dashLength = 0f; if (timeSeries.hasOption(TimeSeriesOption.DASHED)) { isDashed = true; dashLength = (float) timeSeries.getOption(TimeSeriesOption.DASHED); } if (isDashed) { return new BasicStroke(lineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{dashLength, dashLength}, 0.0f); } else { return new BasicStroke(lineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); } } //todo: move enums to separate classes? protected enum HorizontalAlign { LEFT, CENTER, RIGHT } protected enum VerticalAlign { TOP, MIDDLE, BOTTOM, BASELINE } public enum GraphType { LINE, PIE } public enum PieMode { AVERAGE, MAXIMUM, MINIMUM } public enum PieLabelsStyle { PERCENT, NUMBER, NONE } public enum PieLabelsOrientation { HORIZONTAL, ROTATED } }