/** * Copyright (C) 2010-14 diirt developers. See COPYRIGHT.TXT * All rights reserved. Use is subject to license terms. See LICENSE.TXT */ package org.diirt.graphene; import org.diirt.util.stats.Range; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.geom.Line2D; import java.awt.geom.Path2D; import java.util.Arrays; import java.util.Collections; import java.util.List; import static org.diirt.graphene.InterpolationScheme.CUBIC; import static org.diirt.graphene.InterpolationScheme.LINEAR; import static org.diirt.graphene.InterpolationScheme.NEAREST_NEIGHBOR; import static org.diirt.graphene.ReductionScheme.FIRST_MAX_MIN_LAST; import static org.diirt.graphene.ReductionScheme.NONE; import org.diirt.util.array.ArrayDouble; import org.diirt.util.array.ListDouble; import org.diirt.util.array.ListMath; import org.diirt.util.array.ListNumber; /** * The base class for all graph renderers. * * @author carcassi */ public abstract class Graph2DRenderer<T extends Graph2DRendererUpdate> { // WARNING: the following has been cause of continuous confusion, so // please do not touch any drawing code before you fully understand the following. // All the vairables marked as Coord are in unit of pixels. The precision // is subpixel, and 0 coord represent the ideal boundary before the first pixel. // When drawing a line, one has to pay special attention as to whether // the last pixel really need to be drawn: drawing from 0 to 5, for example, // may color 6 pixel which is not what is needed; 5 will be the boundary // between the 5th and 6th pixel, so only 5 pixels should be colored. // All the variables marked as Value are in unit of data to plot // The range of values for the plot // These match the xPlotCoordXxx protected double xPlotValueStart; protected double yPlotValueStart; protected double xPlotValueEnd; protected double yPlotValueEnd; // The pixel coordinates for the area protected int xAreaCoordStart; protected int yAreaCoordStart; protected int yAreaCoordEnd; protected int xAreaCoordEnd; // The pixel coordinates for the ranges // These match the xPlotValueXxx protected double xPlotCoordStart; protected double yPlotCoordStart; protected double yPlotCoordEnd; protected double xPlotCoordEnd; // The pixel size of the range (not of the plot area) protected double yPlotCoordHeight; protected double xPlotCoordWidth; /** * Creates a graph renderer. * * @param graphWidth the graph width * @param graphHeight the graph height */ public Graph2DRenderer(int graphWidth, int graphHeight) { this.imageWidth = graphWidth; this.imageHeight = graphHeight; } /** * The current height of the graph. * * @return the graph height */ public int getImageHeight() { return imageHeight; } /** * The current width of the graph. * * @return the graph width */ public int getImageWidth() { return imageWidth; } protected Graphics2D g; // Renderer external parameter // // Size of the image private int imageWidth; private int imageHeight; // Strategy for calculating the axis range private AxisRangeInstance xAxisRange = AxisRanges.display().createInstance(); private AxisRangeInstance yAxisRange = AxisRanges.display().createInstance(); // Strategy for generating labels and scaling value of the axis protected ValueScale xValueScale = ValueScales.linearScale(); protected ValueScale yValueScale = ValueScales.linearScale(); // Colors and fonts protected Color backgroundColor = Color.WHITE; protected Color labelColor = Color.BLACK; protected Color referenceLineColor = new Color(240, 240, 240); protected Font labelFont = FontUtil.getLiberationSansRegular(); // Image margins protected int bottomMargin = 2; protected int topMargin = 2; protected int leftMargin = 2; protected int rightMargin = 2; // area margins protected int bottomAreaMargin = 0; protected int topAreaMargin = 0; protected int leftAreaMargin = 0; protected int rightAreaMargin = 0; // Axis label margins protected int xLabelMargin = 3; protected int yLabelMargin = 3; // Margin for starting drawing from center of pixel protected double xPointMargin = 0.5; //Set as point (not area) by default protected double yPointMargin = 0.5; // Computed parameters, visible to outside // private Range xAggregatedRange; private Range yAggregatedRange; private Range xPlotRange; private Range yPlotRange; protected FontMetrics labelFontMetrics; protected ListDouble xReferenceCoords; protected ListDouble xReferenceValues; protected List<String> xReferenceLabels; protected ListDouble yReferenceCoords; protected ListDouble yReferenceValues; protected List<String> yReferenceLabels; private int xLabelMaxHeight; private int yLabelMaxWidth; private boolean xAsPoints = true; private boolean yAsPoints = true; /** * The current strategy to calculate the x range for the graph. * * @return the x axis range calculator */ public AxisRange getXAxisRange() { return xAxisRange.getAxisRange(); } /** * The current strategy to calculate the y range for the graph. * * @return the y axis range calculator */ public AxisRange getYAxisRange() { return yAxisRange.getAxisRange(); } /** * The aggregated range of all the data that has been rendered. * * @return the aggregated data x range */ public Range getXAggregatedRange() { return xAggregatedRange; } /** * The aggregated range of all the data that has been rendered. * * @return the aggregated data y range */ public Range getYAggregatedRange() { return yAggregatedRange; } /** * The range of the x axis in the last graph rendering. * * @return the x axis range */ public Range getXPlotRange() { return xPlotRange; } /** * The range of the y axis in the last graph rendering. * * @return the y axis range */ public Range getYPlotRange() { return yPlotRange; } /** * Applies the update to the renderer. * <p> * When sub-classing, one should re-implement this method by first calling it * and then applying all the updates specific to the sub-class. * * @param update the update to apply */ public void update(T update) { if (update.getImageHeight() != null) { imageHeight = update.getImageHeight(); } if (update.getImageWidth() != null) { imageWidth = update.getImageWidth(); } if (update.getXAxisRange() != null) { xAxisRange = update.getXAxisRange().createInstance(); } if (update.getYAxisRange() != null) { yAxisRange = update.getYAxisRange().createInstance(); } if (update.getXValueScale()!= null) { xValueScale = update.getXValueScale(); } if (update.getYValueScale() != null) { yValueScale = update.getYValueScale(); } if (update.getBackgroundColor() != null){ backgroundColor = update.getBackgroundColor(); } if (update.getLabelColor() != null){ labelColor = update.getLabelColor(); } if (update.getReferenceLineColor() != null){ referenceLineColor = update.getReferenceLineColor(); } if (update.getLabelFont() != null){ labelFont = update.getLabelFont(); } if (update.getBottomMargin() != null){ bottomMargin = update.getBottomMargin(); } if (update.getTopMargin() != null){ topMargin = update.getTopMargin(); } if (update.getLeftMargin() != null){ leftMargin = update.getLeftMargin(); } if (update.getRightMargin() != null){ rightMargin = update.getRightMargin(); } if (update.getBottomAreaMargin() != null){ bottomAreaMargin = update.getBottomAreaMargin(); } if (update.getTopAreaMargin() != null){ topAreaMargin = update.getTopAreaMargin(); } if (update.getLeftAreaMargin() != null){ leftAreaMargin = update.getLeftAreaMargin(); } if (update.getRightAreaMargin() != null){ rightAreaMargin = update.getRightAreaMargin(); } if (update.getXLabelMargin() != null){ xLabelMargin = update.getXLabelMargin(); } if (update.getYLabelMargin() != null){ yLabelMargin = update.getYLabelMargin(); } } static Range aggregateRange(Range dataRange, Range aggregatedRange) { if (aggregatedRange == null) { return dataRange; } else { return dataRange.combine(aggregatedRange); } } /** * Creates a new update for the given graph. * * @return a new update object */ public abstract T newUpdate(); /** * Given the new data ranges, calculates the new aggregated and plot * ranges. * * @param xDataRange the new data range for x * @param xDisplayRange the new display range for x * @param yDataRange the new data range for y * @param yDisplayRange the new display range for y */ protected void calculateRanges(Range xDataRange, Range xDisplayRange, Range yDataRange, Range yDisplayRange) { xPlotRange = xAxisRange.axisRange(xDataRange, xDisplayRange); yPlotRange = yAxisRange.axisRange(yDataRange, yDisplayRange); } /** * Draws the horizontal reference lines based on the calculated * graph area. */ protected void drawHorizontalReferenceLines() { g.setColor(referenceLineColor); g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); ListNumber yTicks = yReferenceCoords; for (int i = 0; i < yTicks.size(); i++) { Shape line = new Line2D.Double(xAreaCoordStart, yTicks.getDouble(i), xAreaCoordEnd - 1, yTicks.getDouble(i)); g.draw(line); } } /** *Draw reference lines that correspond to reference values. Reference lines are drawn on the exact pixel that represents a reference value. */ protected void drawVerticalReferenceLines() { g.setColor(referenceLineColor); g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); ListNumber xTicks = xReferenceCoords; for (int i = 0; i < xTicks.size(); i++) { Shape line = new Line2D.Double(xTicks.getDouble(i), yAreaCoordStart, xTicks.getDouble(i), yAreaCoordEnd - 1); g.draw(line); } } /** * Calculates: * <ul> * <li>The font for the labels</li> * <li>The margins based on labels</li> * </ul> */ protected void calculateLabels() { // Calculate horizontal axis references. If range is zero, use special logic if (!(xPlotRange.getMinimum() == xPlotRange.getMaximum())) { ValueAxis xAxis = xValueScale.references(xPlotRange, 2, Math.max(2, getImageWidth() / 60)); xReferenceLabels = Arrays.asList(xAxis.getTickLabels()); xReferenceValues = new ArrayDouble(xAxis.getTickValues()); } else { // TODO: use something better to format the number xReferenceLabels = Collections.singletonList(Double.toString(xPlotRange.getMinimum())); xReferenceValues = new ArrayDouble(xPlotRange.getMinimum()); } // Calculate vertical axis references. If range is zero, use special logic if (!(yPlotRange.getMinimum() == yPlotRange.getMaximum())) { ValueAxis yAxis = yValueScale.references(yPlotRange, 2, Math.max(2, getImageHeight() / 60)); yReferenceLabels = Arrays.asList(yAxis.getTickLabels()); yReferenceValues = new ArrayDouble(yAxis.getTickValues()); } else { // TODO: use something better to format the number yReferenceLabels = Collections.singletonList(Double.toString(yPlotRange.getMinimum())); yReferenceValues = new ArrayDouble(yPlotRange.getMinimum()); } labelFontMetrics = g.getFontMetrics(labelFont); // Compute x axis spacing xLabelMaxHeight = labelFontMetrics.getHeight() - labelFontMetrics.getLeading(); // Compute y axis spacing int[] yLabelWidths = new int[yReferenceLabels.size()]; yLabelMaxWidth = 0; for (int i = 0; i < yLabelWidths.length; i++) { yLabelWidths[i] = labelFontMetrics.stringWidth(yReferenceLabels.get(i)); yLabelMaxWidth = Math.max(yLabelMaxWidth, yLabelWidths[i]); } } /** * Calculates the graph area based on: * <ul> * <li>The image size</li> * <li>The plot ranges</li> * <li>The value scales</li> * <li>The font for the labels</li> * <li>The margins</li> * </ul> * * To calculate area based on labels, ensure that calculateGraphArea() is called * prior to calling calculateGraphAreaNoLabels(). */ protected void calculateGraphArea() { int areaFromBottom = bottomMargin + xLabelMaxHeight + xLabelMargin; int areaFromLeft = leftMargin + yLabelMaxWidth + yLabelMargin; xPlotValueStart = getXPlotRange().getMinimum(); xPlotValueEnd = getXPlotRange().getMaximum(); if (xPlotValueStart == xPlotValueEnd) { // If range is zero, fake a range xPlotValueStart -= 1.0; xPlotValueEnd += 1.0; } xAreaCoordStart = areaFromLeft; xAreaCoordEnd = getImageWidth() - rightMargin; xPlotCoordStart = xAreaCoordStart + leftAreaMargin + xPointMargin; xPlotCoordEnd = xAreaCoordEnd - rightAreaMargin - xPointMargin; xPlotCoordWidth = xPlotCoordEnd - xPlotCoordStart; yPlotValueStart = getYPlotRange().getMinimum(); yPlotValueEnd = getYPlotRange().getMaximum(); if (yPlotValueStart == yPlotValueEnd) { // If range is zero, fake a range yPlotValueStart -= 1.0; yPlotValueEnd += 1.0; } yAreaCoordStart = topMargin; yAreaCoordEnd = getImageHeight() - areaFromBottom; yPlotCoordStart = yAreaCoordStart + topAreaMargin + yPointMargin; yPlotCoordEnd = yAreaCoordEnd - bottomAreaMargin - yPointMargin; yPlotCoordHeight = yPlotCoordEnd - yPlotCoordStart; //Only calculates reference coordinates if calculateLabels() was called if (xReferenceValues != null) { double[] xRefCoords = new double[xReferenceValues.size()]; for (int i = 0; i < xRefCoords.length; i++) { xRefCoords[i] = scaledX(xReferenceValues.getDouble(i)); } xReferenceCoords = new ArrayDouble(xRefCoords); } if (yReferenceValues != null) { double[] yRefCoords = new double[yReferenceValues.size()]; for (int i = 0; i < yRefCoords.length; i++) { yRefCoords[i] = scaledY(yReferenceValues.getDouble(i)); } yReferenceCoords = new ArrayDouble(yRefCoords); } } /** * Draws the background with the background color. */ protected void drawBackground() { g.setColor(backgroundColor); g.fillRect(0, 0, getImageWidth(), getImageHeight()); } /** * Draw the calculated graph area. Draws the the reference * lines and the labels. */ protected void drawGraphArea() { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // When drawing the reference line, align them to the pixel drawVerticalReferenceLines(); drawHorizontalReferenceLines(); drawYLabels(); drawXLabels(); } private ScaledData scaleNoReduction(ListNumber xValues, ListNumber yValues) { return scaleNoReduction(xValues, yValues, 0); } private ScaledData scaleNoReduction(ListNumber xValues, ListNumber yValues, int dataStart) { ScaledData scaledData = new ScaledData(); int dataCount = xValues.size(); scaledData.scaledX = new double[dataCount]; scaledData.scaledY = new double[dataCount]; for (int i = 0; i < scaledData.scaledY.length; i++) { scaledData.scaledX[i] = scaledX(xValues.getDouble(i)); scaledData.scaledY[i] = scaledY(yValues.getDouble(i)); processScaledValue(dataStart + i, xValues.getDouble(i), yValues.getDouble(i), scaledData.scaledX[i], scaledData.scaledY[i]); } scaledData.end = dataCount; return scaledData; } private ScaledData scaleFirstMaxMinLastReduction(ListNumber xValues, ListNumber yValues, int dataStart) { // The number of points generated by this is about 4 times the // number of points on the x axis. If the number of points is less // than that, it's not worth it. Don't do the data reduction. if (xValues.size() < xPlotCoordWidth * 4) { return scaleNoReduction(xValues, yValues, dataStart); } ScaledData scaledData = new ScaledData(); scaledData.scaledX = new double[((int) xPlotCoordWidth + 1)*4 ]; scaledData.scaledY = new double[((int) xPlotCoordWidth + 1)*4]; int cursor = 0; int previousPixel = (int) scaledX(xValues.getDouble(0)); double last = scaledY(yValues.getDouble(0)); double min = last; double max = last; scaledData.scaledX[0] = previousPixel; scaledData.scaledY[0] = min; processScaledValue(dataStart, xValues.getDouble(0), yValues.getDouble(0), scaledX(xValues.getDouble(0)), last); cursor++; for (int i = 1; i < xValues.size(); i++) { double currentScaledX = scaledX(xValues.getDouble(i)); int currentPixel = (int) currentScaledX; if (currentPixel == previousPixel) { last = scaledY(yValues.getDouble(i)); min = MathIgnoreNaN.min(min, last); max = MathIgnoreNaN.max(max, last); processScaledValue(dataStart + i, xValues.getDouble(i), yValues.getDouble(i), currentScaledX, last); } else { scaledData.scaledX[cursor] = previousPixel; scaledData.scaledY[cursor] = max; cursor++; scaledData.scaledX[cursor] = previousPixel; scaledData.scaledY[cursor] = min; cursor++; scaledData.scaledX[cursor] = previousPixel; scaledData.scaledY[cursor] = last; cursor++; previousPixel = currentPixel; last = scaledY(yValues.getDouble(i)); min = last; max = last; scaledData.scaledX[cursor] = currentPixel; scaledData.scaledY[cursor] = last; cursor++; } } scaledData.scaledX[cursor] = previousPixel; scaledData.scaledY[cursor] = max; cursor++; scaledData.scaledX[cursor] = previousPixel; scaledData.scaledY[cursor] = min; cursor++; scaledData.end = cursor; return scaledData; } /** *Empty function, designed to be implemented in sub-classes. * <p>Used on every value in a dataset.</p> * * @param index the index of the value * @param valueX the x value * @param valueY the y value * @param scaledX the x pixel * @param scaledY the y pixel */ protected void processScaledValue(int index, double valueX, double valueY, double scaledX, double scaledY) { } private static class ScaledData { private double[] scaledX; private double[] scaledY; private int start; private int end; } /** * Draws an implicit line given the interpolation scheme and the x,y values. * The function will scale the values. * * @param xValues the x values * @param yValues the y values * @param interpolation the interpolation scheme */ protected void drawValueLine(ListNumber xValues, ListNumber yValues, InterpolationScheme interpolation) { ReductionScheme reductionScheme = ReductionScheme.NONE; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); ScaledData scaledData; switch (reductionScheme) { default: throw new IllegalArgumentException("Reduction scheme " + reductionScheme + " not supported"); case NONE: scaledData = scaleNoReduction(xValues, yValues); break; } // create path Path2D path; switch (interpolation) { default: case NEAREST_NEIGHBOR: path = nearestNeighbour(scaledData); break; case LINEAR: path = linearInterpolation(scaledData); break; case CUBIC: path = cubicInterpolation(scaledData); break; } // Draw the line g.draw(path); } /** * Draws an explicit line give the interpolation and reduction schemes, * the x values and the y values. The function will scale the values. * * @param xValues the x values * @param yValues the y values * @param interpolation the interpolation * @param reduction the reduction */ protected void drawValueExplicitLine(ListNumber xValues, ListNumber yValues, InterpolationScheme interpolation, ReductionScheme reduction) { ScaledData scaledData; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); // Narrow the data int start = org.diirt.util.array.ListNumbers.binarySearchValueOrLower(xValues, xPlotValueStart); int end = org.diirt.util.array.ListNumbers.binarySearchValueOrHigher(xValues, xPlotValueEnd); xValues = ListMath.limit(xValues, start, end + 1); yValues = ListMath.limit(yValues, start, end + 1); switch (reduction) { default: throw new IllegalArgumentException("Reduction scheme " + reduction + " not supported"); case NONE: scaledData = scaleNoReduction(xValues, yValues, start); break; case FIRST_MAX_MIN_LAST: scaledData = scaleFirstMaxMinLastReduction(xValues, yValues, start); break; } // create path Path2D path; switch (interpolation) { default: case NEAREST_NEIGHBOR: path = nearestNeighbour(scaledData); break; case LINEAR: path = linearInterpolation(scaledData); break; case CUBIC: path = cubicInterpolation(scaledData); break; } // Draw the line g.draw(path); } private static Path2D.Double nearestNeighbour(ScaledData scaledData) { double[] scaledX = scaledData.scaledX; double[] scaledY = scaledData.scaledY; int start = scaledData.start; int end = scaledData.end; Path2D.Double line = new Path2D.Double(); line.moveTo(scaledX[start], scaledY[start]); for (int i = 1; i < end; i++) { double halfX = scaledX[i - 1] + (scaledX[i] - scaledX[i - 1]) / 2; if (!java.lang.Double.isNaN(scaledY[i-1])) { line.lineTo(halfX, scaledY[i - 1]); if (!java.lang.Double.isNaN(scaledY[i])) line.lineTo(halfX, scaledY[i]); } else { line.moveTo(halfX, scaledY[i]); } } line.lineTo(scaledX[end - 1], scaledY[end - 1]); return line; } private static Path2D.Double linearInterpolation(ScaledData scaledData){ double[] scaledX = scaledData.scaledX; double[] scaledY = scaledData.scaledY; int start = scaledData.start; int end = scaledData.end; Path2D.Double line = new Path2D.Double(); for (int i = start; i < end; i++) { // Do I have a current value? if (!java.lang.Double.isNaN(scaledY[i])) { // Do I have a previous value? if (i != start && !java.lang.Double.isNaN(scaledY[i - 1])) { // Here I have both the previous value and the current value line.lineTo(scaledX[i], scaledY[i]); } else { // Don't have a previous value // Do I have a next value? if (i != end - 1 && !java.lang.Double.isNaN(scaledY[i + 1])) { // There is no value before, but there is a value after line.moveTo(scaledX[i], scaledY[i]); } else { // There is no value either before or after line.moveTo(scaledX[i] - 1, scaledY[i]); line.lineTo(scaledX[i] + 1, scaledY[i]); } } } } return line; } private static Path2D.Double cubicInterpolation(ScaledData scaledData){ double[] scaledX = scaledData.scaledX; double[] scaledY = scaledData.scaledY; int start = scaledData.start; int end = scaledData.end; Path2D.Double path = new Path2D.Double(); for (int i = start; i < end; i++) { double y1; double y2; double x1; double x2; double y0; double x0; double y3; double x3; double bx0; double by0; double bx3; double by3; double bdy0; double bdy3; double bx1; double by1; double bx2; double by2; //Do I have current value? if (!java.lang.Double.isNaN(scaledY[i])){ //Do I have previous value? if (i > start && !java.lang.Double.isNaN(scaledY[i - 1])) { //Do I have value two before? if (i > start + 1 && !java.lang.Double.isNaN(scaledY[i - 2])) { //Do I have next value? if (i != end - 1 && !java.lang.Double.isNaN(scaledY[i + 1])) { y2 = scaledY[i]; x2 = scaledX[i]; y0 = scaledY[i - 2]; x0 = scaledX[i - 2]; y3 = scaledY[i + 1]; x3 = scaledX[i + 1]; y1 = scaledY[i - 1]; x1 = scaledX[i - 1]; bx0 = x1; by0 = y1; bx3 = x2; by3 = y2; bdy0 = (y2 - y0) / (x2 - x0); bdy3 = (y3 - y1) / (x3 - x1); bx1 = bx0 + (x2 - x0) / 6.0; by1 = (bx1 - bx0) * bdy0 + by0; bx2 = bx3 - (x3 - x1) / 6.0; by2 = (bx2 - bx3) * bdy3 + by3; path.curveTo(bx1, by1, bx2, by2, bx3, by3); } else{//Have current, previous, two before, but not value after y2 = scaledY[i]; x2 = scaledX[i]; y1 = scaledY[i - 1]; x1 = scaledX[i - 1]; y0 = scaledY[i - 2]; x0 = scaledX[i - 2]; y3 = y2 + (y2 - y1) / 2; x3 = x2 + (x2 - x1) / 2; bx0 = x1; by0 = y1; bx3 = x2; by3 = y2; bdy0 = (y2 - y0) / (x2 - x0); bdy3 = (y3 - y1) / (x3 - x1); bx1 = bx0 + (x2 - x0) / 6.0; by1 = (bx1 - bx0) * bdy0 + by0; bx2 = bx3 - (x3 - x1) / 6.0; by2 = (bx2 - bx3) * bdy3 + by3; path.curveTo(bx1, by1, bx2, by2, bx3, by3); } } else if (i != end - 1 && !java.lang.Double.isNaN(scaledY[i + 1])) { //Have current , previous, and next, but not two before path.moveTo(scaledX[i - 1], scaledY[i - 1]); y2 = scaledY[i]; x2 = scaledX[i]; y1 = scaledY[i - 1]; x1 = scaledX[i - 1]; y0 = y1 - (y2 - y1) / 2; x0 = x1 - (x2 - x1) / 2; y3 = scaledY[i + 1]; x3 = scaledX[i + 1]; bx0 = x1; by0 = y1; bx3 = x2; by3 = y2; bdy0 = (y2 - y0) / (x2 - x0); bdy3 = (y3 - y1) / (x3 - x1); bx1 = bx0 + (x2 - x0) / 6.0; by1 = (bx1 - bx0) * bdy0 + by0; bx2 = bx3 - (x3 - x1) / 6.0; by2 = (bx2 - bx3) * bdy3 + by3; path.curveTo(bx1, by1, bx2, by2, bx3, by3); }else{//have current, previous, but not two before or next path.lineTo(scaledX[i], scaledY[i]); } //have current, but not previous }else{ // No previous value if (i != end - 1 && !java.lang.Double.isNaN(scaledY[i + 1])) { // If we have the next value, just move, we'll draw later path.moveTo(scaledX[i], scaledY[i]); } else { // If not, write a small horizontal line path.moveTo(scaledX[i] - 1, scaledY[i]); path.lineTo(scaledX[i] + 1, scaledY[i]); } } }else{ //do not have current // Do nothing } } return path; } private static final int MIN = 0; private static final int MAX = 1; private static void drawHorizontalReferencesLabel(Graphics2D graphics, FontMetrics metrics, String text, int yCenter, int[] drawRange, int xRight, boolean updateMin, boolean centeredOnly) { // If the center is not in the range, don't draw anything if (drawRange[MAX] < yCenter || drawRange[MIN] > yCenter) return; // If there is no space, don't draw anything if (drawRange[MAX] - drawRange[MIN] < metrics.getHeight()) return; Java2DStringUtilities.Alignment alignment = Java2DStringUtilities.Alignment.RIGHT; int targetY = yCenter; int halfHeight = metrics.getAscent() / 2; if (yCenter < drawRange[MIN] + halfHeight) { // Can't be drawn in the center if (centeredOnly) return; alignment = Java2DStringUtilities.Alignment.TOP_RIGHT; targetY = drawRange[MIN]; } else if (yCenter > drawRange[MAX] - halfHeight) { // Can't be drawn in the center if (centeredOnly) return; alignment = Java2DStringUtilities.Alignment.BOTTOM_RIGHT; targetY = drawRange[MAX]; } Java2DStringUtilities.drawString(graphics, alignment, xRight, targetY, text); if (updateMin) { drawRange[MAX] = targetY - metrics.getHeight(); } else { drawRange[MIN] = targetY + metrics.getHeight(); } } private static void drawVerticalReferenceLabel(Graphics2D graphics, FontMetrics metrics, String text, int xCenter, int[] drawRange, int yTop, boolean updateMin, boolean centeredOnly) { // If the center is not in the range, don't draw anything if (drawRange[MAX] < xCenter || drawRange[MIN] > xCenter) return; // If there is no space, don't draw anything if (drawRange[MAX] - drawRange[MIN] < metrics.getHeight()) return; Java2DStringUtilities.Alignment alignment = Java2DStringUtilities.Alignment.TOP; int targetX = xCenter; int halfWidth = metrics.stringWidth(text) / 2; if (xCenter < drawRange[MIN] + halfWidth) { // Can't be drawn in the center if (centeredOnly) return; alignment = Java2DStringUtilities.Alignment.TOP_LEFT; targetX = drawRange[MIN]; } else if (xCenter > drawRange[MAX] - halfWidth) { // Can't be drawn in the center if (centeredOnly) return; alignment = Java2DStringUtilities.Alignment.TOP_RIGHT; targetX = drawRange[MAX]; } Java2DStringUtilities.drawString(graphics, alignment, targetX, yTop, text); if (updateMin) { drawRange[MIN] = targetX + metrics.getHeight(); } else { drawRange[MAX] = targetX - metrics.getHeight(); } } /* * Scale the x value to the graph area. * * @param value the x value * @return the x position in the graph area */ protected final double scaledX(double value) { return xValueScale.scaleValue(value, xPlotValueStart, xPlotValueEnd, xPlotCoordStart, xPlotCoordEnd); } /** * Scale the y value to the graph area. * * @param value the y value * @return the y position in the graph area */ protected final double scaledY(double value) { return yValueScale.scaleValue(value, yPlotValueStart, yPlotValueEnd, yPlotCoordEnd, yPlotCoordStart); } /** * Sets the clip area to the actual graph area * * @param g the graphics context */ protected void setClip(Graphics2D g) { g.setClip(xAreaCoordStart, yAreaCoordStart, xAreaCoordEnd - xAreaCoordStart, yAreaCoordEnd - yAreaCoordStart); } /** * Draw the vertical labels based on the calculated graph area. */ protected void drawYLabels() { // Draw Y labels ListNumber yTicks = yReferenceCoords; if (yReferenceLabels != null && !yReferenceLabels.isEmpty()) { //g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); g.setColor(labelColor); g.setFont(labelFont); FontMetrics metrics = g.getFontMetrics(); // Draw first and last label int[] drawRange = new int[] {yAreaCoordStart, yAreaCoordEnd - 1}; int xRightLabel = (int) (xAreaCoordStart - yLabelMargin - 1); drawHorizontalReferencesLabel(g, metrics, yReferenceLabels.get(0), (int) Math.floor(yTicks.getDouble(0)), drawRange, xRightLabel, true, false); drawHorizontalReferencesLabel(g, metrics, yReferenceLabels.get(yReferenceLabels.size() - 1), (int) Math.floor(yTicks.getDouble(yReferenceLabels.size() - 1)), drawRange, xRightLabel, false, false); for (int i = 1; i < yReferenceLabels.size() - 1; i++) { drawHorizontalReferencesLabel(g, metrics, yReferenceLabels.get(i), (int) Math.floor(yTicks.getDouble(i)), drawRange, xRightLabel, true, false); } } } /** * Draw the horizontal labels based on the calculated graph area. */ protected void drawXLabels() { // Draw X labels ListNumber xTicks = xReferenceCoords; if (xReferenceLabels != null && !xReferenceLabels.isEmpty()) { //g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); g.setColor(labelColor); g.setFont(labelFont); FontMetrics metrics = g.getFontMetrics(); // Draw first and last label int[] drawRange = new int[] {xAreaCoordStart, xAreaCoordEnd - 1}; int yTop = (int) (yAreaCoordEnd + xLabelMargin); drawVerticalReferenceLabel(g, metrics, xReferenceLabels.get(0), (int) Math.floor(xTicks.getDouble(0)), drawRange, yTop, true, false); drawVerticalReferenceLabel(g, metrics, xReferenceLabels.get(xReferenceLabels.size() - 1), (int) Math.floor(xTicks.getDouble(xReferenceLabels.size() - 1)), drawRange, yTop, false, false); for (int i = 1; i < xReferenceLabels.size() - 1; i++) { drawVerticalReferenceLabel(g, metrics, xReferenceLabels.get(i), (int) Math.floor(xTicks.getDouble(i)), drawRange, yTop, true, false); } } } /** *Sets up a graph to start and end at the center of a pixel. * Consequently, all drawing done after using this method should assume that every point is on the center of a pixel. */ protected void setupDataAsPoints(){ setupXAsPoints(); setupYAsPoints(); } /** *Sets up the x-axis of a graph to start and end at the center of a pixel. */ protected void setupXAsPoints(){ xPointMargin = 0.5; } /** *Sets up the y-axis of a graph to start and end at the center of a pixel. */ protected void setupYAsPoints(){ yPointMargin = 0.5; } /** *Sets up a graph to start and end at the beginning border of a pixel. * After using this method, each point should be assumed to be at the top left corner of a pixel. */ protected void setupDataAsAreas(){ setupXAsAreas(); setupYAsAreas(); } /** *Sets up the x-axis of a graph to start and end at the left border of a pixel. */ protected void setupXAsAreas(){ xPointMargin = 0; } /** *Sets up the y-axis of a graph to start and end at the top border of a pixel. */ protected void setupYAsAreas(){ yPointMargin = 0; } }