1 /* 2 * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. 3 * 4 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 5 * 6 * The contents of this file are subject to the terms of either the Universal Permissive License 7 * v 1.0 as shown at http://oss.oracle.com/licenses/upl 8 * 9 * or the following license: 10 * 11 * Redistribution and use in source and binary forms, with or without modification, are permitted 12 * provided that the following conditions are met: 13 * 14 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions 15 * and the following disclaimer. 16 * 17 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of 18 * conditions and the following disclaimer in the documentation and/or other materials provided with 19 * the distribution. 20 * 21 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to 22 * endorse or promote products derived from this software without specific prior written permission. 23 * 24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 25 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 26 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 27 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 30 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY 31 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 */ 33 package org.openjdk.jmc.ui.charts; 34 35 import java.awt.BasicStroke; 36 import java.awt.Color; 37 import java.awt.FontMetrics; 38 import java.awt.Graphics2D; 39 import java.awt.Paint; 40 import java.awt.Polygon; 41 import java.awt.Rectangle; 42 import java.awt.RenderingHints; 43 import java.awt.Shape; 44 import java.awt.Stroke; 45 import java.awt.TexturePaint; 46 import java.awt.geom.AffineTransform; 47 import java.awt.image.BufferedImage; 48 49 import org.openjdk.jmc.common.unit.IFormatter; 50 import org.openjdk.jmc.common.unit.IIncrementalFormatter; 51 import org.openjdk.jmc.common.unit.IQuantity; 52 import org.openjdk.jmc.common.unit.IRange; 53 import org.openjdk.jmc.common.unit.QuantityRange; 54 import org.openjdk.jmc.ui.UIPlugin; 55 import org.openjdk.jmc.ui.preferences.PreferenceConstants; 56 57 public class AWTChartToolkit { 58 59 public static interface IColorProvider<T> { 60 61 Color getColor(T o); 62 } 63 64 private static final int PLOT_RADIUS = 2; 65 private static final int TICK_LINE = 3; 66 private static final int TICK_SIZE = 6; 67 private static final Stroke DASH_STROKE = new BasicStroke(.5f, 0, 0, 1.0f, new float[] {4, 3}, 0); 68 private static final BasicStroke EXTRAPOLATION_STROKE = new BasicStroke(1f, BasicStroke.CAP_ROUND, 69 BasicStroke.JOIN_ROUND, 1f, new float[] {3f, 2f}, 1f); 70 private static final Paint EXTRAPOLATION_PAINT; 71 // The amount of pixels at the top of the yAxis not to draw 72 private static final int Y_AXIS_TOP_SPACE = 1; 73 // The size of the arrow (real width/height will be ARROW_SIZE * 2 - 1) 74 private static final int ARROW_SIZE = 3; 75 76 private static boolean USE_AA = UIPlugin.getDefault().getPreferenceStore() 77 .getBoolean(PreferenceConstants.P_ANTI_ALIASING); 78 79 static { 80 BufferedImage bi = new BufferedImage(5, 5, BufferedImage.TYPE_INT_RGB); 81 Graphics2D big = bi.createGraphics(); 82 big.setColor(new Color(255, 255, 255)); 83 big.fillRect(0, 0, 5, 5); 84 big.setColor(new Color(200, 200, 200)); 85 big.drawLine(0, 0, 5, 5); 86 Rectangle rect = new Rectangle(0, 0, 5, 5); 87 EXTRAPOLATION_PAINT = new TexturePaint(bi, rect); 88 89 UIPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(e -> USE_AA = (boolean) e.getNewValue()); 90 } 91 92 public static <T> IColorProvider<T> staticColor(final Color color) { 93 return new IColorProvider<T>() { 94 95 @Override 96 public Color getColor(T o) { 97 return color; 98 } 99 100 }; 101 } 102 103 private static Object getAntiAliasingHint() { 104 return USE_AA ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF; 105 } 106 107 /** 108 * Draw a horizontal dashed extrapolation line and optionally a striped fill area below. Please 109 * observe that all the coordinates should be in the actual drawable area, at least roughly, to 110 * avoid <b>huge</b> performance issues on some machines, including some Macs. 111 * 112 * @param ctx 113 * @param x1 114 * @param y1 115 * @param x2 116 * @param y2 117 * @param fill 118 */ 119 private static void drawExtrapolation(Graphics2D ctx, int x1, int y1, int x2, int y2, boolean fill) { 120 int x = Math.min(x1, x2); 121 int y = Math.min(y1, y2); 122 int width = Math.abs(x2 - x1); 123 int heigth = Math.abs(y2 - y1); 124 if (fill) { 125 Paint p = ctx.getPaint(); 126 ctx.setPaint(EXTRAPOLATION_PAINT); 127 ctx.fillRect(x, y, width, heigth); 128 ctx.setPaint(p); 129 } 130 Stroke oldStroke = ctx.getStroke(); 131 ctx.setStroke(EXTRAPOLATION_STROKE); 132 /* 133 * On OS X 10.11, at least, these coordinates must be clamped to the visible bounds. 134 * Otherwise it may use huge resources and take time proportional to width (and possibly 135 * height). That is, _seconds_ with moderate zooming, and possibly much worse due to memory 136 * usage. 137 */ 138 ctx.drawLine(x, y + heigth, x + width, y + heigth); 139 ctx.setStroke(oldStroke); 140 } 141 142 public static void drawPlot(Graphics2D ctx, IXYDisplayableSet<?, ?> points, int height, boolean fill) { 143 Object oldAntiAliasing = ctx.getRenderingHint(RenderingHints.KEY_ANTIALIASING); 144 ctx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, getAntiAliasingHint()); 145 int diameter = PLOT_RADIUS * 2; 146 for (int i = 0; i < points.getSize(); i++) { 147 double yCoord = points.getPixelY(i); 148 if (!Double.isNaN(yCoord)) { 149 int x = (int) points.getPixelX(i) - PLOT_RADIUS; 150 int y = height - 1 - (int) yCoord - PLOT_RADIUS; 151 if (fill) { 152 ctx.fillOval(x, y, diameter + 1, diameter + 1); 153 } else { 154 ctx.drawOval(x, y, diameter, diameter); 155 } 156 } 157 } 158 ctx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldAntiAliasing); 159 } 160 161 public static void drawLineChart( 162 Graphics2D ctx, IXYDisplayableSet<?, ?> points, int width, int height, boolean fill) { 163 Object oldAntiAliasing = ctx.getRenderingHint(RenderingHints.KEY_ANTIALIASING); 164 ctx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, getAntiAliasingHint()); 165 166 AffineTransform oldTransform = ctx.getTransform(); 167 // Flipping integer coordinates 0 to (height - 1). May need to rethink for HiDPI. 168 ctx.scale(1, -1); 169 ctx.translate(0, 1 - height); 170 171 Polygon p = getLineChart(points); 172 int lastPoint = p.npoints - 1; 173 /* 174 * On OS X 10.11, at least, these coordinates must be clamped to the visible bounds. 175 * Otherwise it may use huge resources and take time proportional to width (and possibly 176 * height). That is, _seconds_ with moderate zooming, and possibly much worse due to memory 177 * usage. 178 */ 179 if (p.xpoints[0] > 0) { 180 drawExtrapolation(ctx, Math.min(p.xpoints[0], width), p.ypoints[0], 0, 0, fill); 181 } 182 if (p.xpoints[lastPoint] < width) { 183 drawExtrapolation(ctx, Math.max(p.xpoints[lastPoint], 0), p.ypoints[lastPoint], width, 0, fill); 184 } 185 186 if (fill) { 187 p.ypoints[0] = 0; 188 p.ypoints[lastPoint] = 0; 189 ctx.fillPolygon(p); 190 ctx.setPaint(Color.BLACK); 191 ctx.drawPolygon(p); 192 } else { 193 ctx.drawPolyline(p.xpoints, p.ypoints, p.npoints); 194 } 195 196 ctx.setTransform(oldTransform); 197 ctx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldAntiAliasing); 198 } 199 200 private static Polygon getLineChart(IXYDisplayableSet<?, ?> points) { 201 int maxCoordinates = points.getSize() + 2; 202 int[] xs = new int[maxCoordinates]; 203 int[] ys = new int[maxCoordinates]; 204 205 int index = 1; 206 for (int i = 0; i < points.getSize(); i++) { 207 double yCoord = points.getPixelY(i); 208 if (!Double.isNaN(yCoord)) { 209 xs[index] = (int) points.getPixelX(i); 210 ys[index] = (int) yCoord; 211 index++; 212 } 213 } 214 xs[0] = xs[1]; 215 ys[0] = ys[1]; 216 xs[index] = xs[index - 1]; 217 ys[index] = ys[index - 1]; 218 return new Polygon(xs, ys, index > 1 ? ++index : 0); 219 } 220 221 private static Polygon getRightAngleChart(IXYDisplayableSet<?, ?> points, int width) { 222 int maxCoordinates = points.getSize() * 2 + 2; 223 int[] xs = new int[maxCoordinates]; 224 int[] ys = new int[maxCoordinates]; 225 226 int index = 0; 227 int currentY = 0; 228 for (int i = 0; i < points.getSize(); i++) { 229 double yCoord = points.getPixelY(i); 230 int nextY = Double.isNaN(yCoord) ? 0 : (int) yCoord; 231 if (nextY != currentY) { 232 int x = (int) points.getPixelX(i); 233 xs[index] = x; 234 ys[index] = currentY; 235 index++; 236 xs[index] = x; 237 ys[index] = nextY; 238 index++; 239 currentY = nextY; 240 } 241 } 242 if (index > 0) { 243 xs[index] = width - 1; 244 ys[index] = currentY; 245 index++; 246 xs[index] = width - 1; 247 ys[index] = 0; 248 index++; 249 } 250 return new Polygon(xs, ys, index); 251 } 252 253 public static void drawRightAngleChart(Graphics2D ctx, IXYDisplayableSet<?, ?> points, int width, int height) { 254 Polygon p = getRightAngleChart(points, width); 255 256 AffineTransform oldTransform = ctx.getTransform(); 257 ctx.scale(1, -1); 258 ctx.translate(0, -height); 259 260 ctx.fillPolygon(p); 261 ctx.drawPolyline(p.xpoints, p.ypoints, p.npoints); 262 263 ctx.setTransform(oldTransform); 264 } 265 266 public static <T> void drawBarChart( 267 Graphics2D ctx, IXYDisplayableSet<T[], ?> points, IColorProvider<? super T> cp, int width, int height) { 268 AffineTransform oldTransform = ctx.getTransform(); 269 ctx.scale(1, -1); 270 ctx.translate(0, -height); 271 272 Paint paint = ctx.getPaint(); 273 T[] payload = points.getPayload(); 274 275 for (int i = 0; i < points.getSize(); i++) { 276 int barHeight = (int) points.getPixelY(i); 277 int x1 = (int) points.getPixelX(i); 278 int x2 = (int) points.getPixelX(i + 1); 279 int barWidth = x2 - x1; 280 if (barWidth > 10) { 281 barWidth -= 4; 282 x1 += 2; 283 } 284 // FIXME: Draw with gradient fill? 285 ctx.setPaint(cp == null ? paint : cp.getColor((payload == null) ? null : payload[i])); 286 ctx.fillRect(x1, 0, barWidth, barHeight); 287 ctx.setPaint(Color.GRAY); 288 ctx.drawRect(x1, 0, barWidth, barHeight); 289 } 290 ctx.setTransform(oldTransform); 291 } 292 293 public static void drawAxis( 294 Graphics2D ctx, SubdividedQuantityRange range, int axisPos, boolean labelAhead, int labelLimit, 295 boolean vertical) { 296 int axisSize = range.getPixelExtent(); 297 FontMetrics fm = ctx.getFontMetrics(); 298 int textAscent = fm.getAscent(); 299 int textYadjust = textAscent / 2; 300 int labelYPos = labelAhead ? axisPos - TICK_SIZE : axisPos + TICK_SIZE + textAscent; 301 final int labelSpacing; 302 303 if (vertical) { 304 ctx.drawLine(axisPos, Y_AXIS_TOP_SPACE, axisPos, axisSize - 1); 305 drawUpArrow(ctx, axisPos, Y_AXIS_TOP_SPACE, Math.min(ARROW_SIZE, axisSize - 2)); 306 labelSpacing = fm.getHeight() - textAscent; 307 } else { 308 ctx.drawLine(0, axisPos, axisSize - 1, axisPos); 309 labelSpacing = fm.charWidth(' ') * 2; 310 } 311 312 IRange<IQuantity> firstBucket = QuantityRange.createWithEnd(range.getSubdivider(0), range.getSubdivider(1)); 313 IQuantity lastShownTick = null; 314 final IFormatter<IQuantity> formatter = range.getStart().getType().getFormatterResolving(firstBucket); 315 final IIncrementalFormatter changeFormatter; 316 if (formatter instanceof IIncrementalFormatter) { 317 changeFormatter = (IIncrementalFormatter) formatter; 318 if (!vertical && (labelLimit < 0)) { 319 lastShownTick = range.getSubdivider(0); 320 if (lastShownTick.compareTo(range.getStart()) < 0) { 321 lastShownTick = range.getSubdivider(1); 322 } 323 String label = changeFormatter.formatContext(lastShownTick); 324 int labelWidth = fm.stringWidth(label); 325 ctx.drawString(label, labelLimit, labelYPos); 326 labelLimit += labelWidth + labelSpacing; 327 } 328 } else { 329 changeFormatter = null; 330 } 331 332 int numTicks = range.getNumSubdividers(); 333 for (int i = 0; i < numTicks; i++) { 334 int tickPos = (int) range.getSubdividerPixel(i); 335 if (tickPos >= axisSize) { 336 break; 337 } else if (tickPos >= 0) { 338 IQuantity currentTick = range.getSubdivider(i); 339 final String label; 340 if (vertical) { 341 ctx.drawLine(axisPos - TICK_LINE, axisSize - 1 - tickPos, axisPos + TICK_LINE, 342 axisSize - 1 - tickPos); 343 if ((tickPos + textYadjust) >= axisSize) { 344 break; 345 } else if ((tickPos - textYadjust) >= labelLimit) { 346 label = formatter.format(currentTick); 347 int labelXPos = labelAhead ? axisPos - TICK_SIZE - fm.stringWidth(label) : axisPos + TICK_SIZE; 348 ctx.drawString(label, labelXPos, axisSize - 1 - tickPos + textYadjust); 349 labelLimit = tickPos + textYadjust + labelSpacing; 350 } 351 } else { 352 if (changeFormatter != null) { 353 label = changeFormatter.formatAdjacent(lastShownTick, range.getSubdivider(i)); 354 } else { 355 label = formatter.format(currentTick); 356 } 357 ctx.drawLine(tickPos, axisPos - TICK_LINE, tickPos, axisPos + TICK_LINE); 358 int textXadjust = fm.stringWidth(label) / 2; 359 // FIXME: Decide if truncated labels should be shown 360 // if ((tickPos + textXadjust) >= axisSize) { 361 if (tickPos >= axisSize) { 362 break; 363 } else if ((tickPos - textXadjust) >= labelLimit) { 364 ctx.drawString(label, tickPos - textXadjust, labelYPos); 365 labelLimit = tickPos + textXadjust + labelSpacing; 366 lastShownTick = currentTick; 367 } 368 } 369 } 370 } 371 } 372 373 private static void drawUpArrow(Graphics2D ctx, int axisX, int axisYTop, int size) { 374 int yArrow = axisYTop + size; 375 ctx.drawLine(axisX - size, yArrow, axisX, axisYTop); 376 ctx.drawLine(axisX + size, yArrow, axisX, axisYTop); 377 } 378 379 public static void drawGrid(Graphics2D ctx, SubdividedQuantityRange range, int gridSize, boolean verticalAxis) { 380 int axisSize = range.getPixelExtent(); 381 Stroke oldStroke = ctx.getStroke(); 382 ctx.setStroke(DASH_STROKE); 383 int numTicks = range.getNumSubdividers(); 384 for (int i = 0; i < numTicks; i++) { 385 int pos = (int) range.getSubdividerPixel(i); 386 if (pos >= axisSize) { 387 break; 388 } else if (pos >= 0) { 389 if (verticalAxis) { 390 ctx.drawLine(0, axisSize - 1 - pos, gridSize - 1, axisSize - 1 - pos); 391 } else { 392 ctx.drawLine(pos, 0, pos, gridSize - 1); 393 } 394 395 } 396 } 397 ctx.setStroke(oldStroke); 398 } 399 400 /** 401 * Draw ranges by treating the coordinate pairs of {@code points} not as x and y, but as start 402 * and end on the x axis. As a consequence, {@link IXYDisplayableSet#getWidth() 403 * points.getWidth()} and {@link IXYDisplayableSet#getHeight() points.getHeight()} should return 404 * the same value. (Not to be confused with the {@code height} parameter, which is the actual 405 * number of y pixels that will be filled.) 406 * 407 * @param g2 408 * @param points 409 * @param height 410 * @param fill 411 */ 412 public static void drawRanges(Graphics2D g2, IXYDisplayableSet<?, ?> points, int height, boolean fill) { 413 int width = points.getWidth(); 414 Shape oldClip = g2.getClip(); 415 g2.setClip(new Rectangle(width, height)); 416 for (int n = 0; n < points.getSize(); n++) { 417 double x1 = points.getPixelX(n); 418 double x2 = points.getPixelY(n); 419 int start = x1 < 0 ? -1 : (int) x1; 420 int end = x2 > width ? width + 1 : (int) x2; 421 if (end > 0 && start < width) { 422 if (fill) { 423 g2.fillRect(start, 0, end - start, height); 424 } else { 425 g2.drawRect(start, 0, end - start, height - 1); 426 } 427 } 428 } 429 g2.setClip(oldClip); 430 } 431 432 /** 433 * Draw spans by treating the coordinate pairs of {@code points} not as x and y, but as start 434 * and end on the x axis. As a consequence, {@link IXYDisplayableSet#getWidth() 435 * points.getWidth()} and {@link IXYDisplayableSet#getHeight() points.getHeight()} should return 436 * the same value. (Not to be confused with the {@code height} parameter, which is the actual 437 * number of y pixels that will be filled.) 438 * 439 * @param g2 440 * @param points 441 * @param height 442 * @param markBoundaries 443 * @param cp 444 */ 445 public static <T> void drawSpan( 446 Graphics2D g2, IXYDisplayableSet<T[], ?> points, int height, boolean markBoundaries, 447 IColorProvider<? super T> cp) { 448 int width = points.getWidth(); 449 int[] buffer = new int[width]; 450 int[] secondBuffer = markBoundaries ? new int[width] : buffer; 451 T[] payload = points.getPayload(); 452 for (int n = 0; n < points.getSize(); n++) { 453 T item = payload[n]; 454 if (item != null) { 455 int x1 = (int) points.getPixelX(n); 456 int x2 = (int) points.getPixelY(n); 457 int start = Math.max(0, Math.min(x1, x2)); 458 int end = Math.min(width - 1, Math.max(x1, x2)); 459 int color = cp.getColor(item).getRGB(); 460 if (markBoundaries && (end - start) > 2) { 461 double damp = 0.85 - 3.0 / (end - start); 462 int shade = (int) (50 * damp * damp); 463 for (int i = start; i <= end; i++) { 464 if (shade > 0) { 465 buffer[i] = shade(color, shade); 466 shade = (int) (shade * damp); 467 } else { 468 buffer[i] = color; 469 } 470 secondBuffer[i] = i == start ? Color.BLACK.getRGB() : buffer[i]; 471 } 472 } else { 473 for (int i = start; i <= end; i++) { 474 secondBuffer[i] = buffer[i] = color; 475 } 476 } 477 } 478 } 479 BufferedImage image = new BufferedImage(width, 1, BufferedImage.TYPE_INT_ARGB); 480 BufferedImage cpImage = markBoundaries ? new BufferedImage(width, 1, BufferedImage.TYPE_INT_ARGB) : image; 481 image.setRGB(0, 0, width, 1, buffer, 0, width); 482 cpImage.setRGB(0, 0, width, 1, secondBuffer, 0, width); 483 484 for (int n = 0; n < height; n++) { 485 if ((n & 2) == 0) { 486 g2.drawImage(cpImage, 0, n, null, null); 487 } else { 488 g2.drawImage(image, 0, n, null, null); 489 } 490 } 491 } 492 493 private static int shade(int color, int shade) { 494 return 0xff000000 & color | shift(color, shade, 16) | shift(color, shade, 8) | shift(color, shade, 0); 495 } 496 497 private static int shift(int color, int shade, int componentOffset) { 498 int comp = ((color >>> componentOffset) & 0xff); 499 return (comp > shade ? comp - shade : 0) << componentOffset; 500 } 501 502 }