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.Color;
  36 import java.awt.FontMetrics;
  37 import java.awt.Graphics2D;
  38 import java.awt.Point;
  39 import java.awt.Rectangle;
  40 import java.awt.Shape;
  41 import java.awt.geom.Point2D;
  42 import java.util.ArrayList;
  43 import java.util.Collections;
  44 
  45 import org.openjdk.jmc.common.IDisplayable;
  46 import org.openjdk.jmc.common.unit.IQuantity;
  47 import org.openjdk.jmc.common.unit.LinearUnit;
  48 import org.openjdk.jmc.common.unit.UnitLookup;
  49 import org.openjdk.jmc.common.util.ColorToolkit;
  50 import org.openjdk.jmc.ui.charts.AWTChartToolkit.IColorProvider;
  51 import org.openjdk.jmc.ui.charts.IChartInfoVisitor.IBucket;
  52 import org.openjdk.jmc.ui.charts.IChartInfoVisitor.ILane;
  53 import org.openjdk.jmc.ui.charts.IChartInfoVisitor.IPoint;
  54 import org.openjdk.jmc.ui.charts.IChartInfoVisitor.ITick;
  55 
  56 public class XYDataRenderer implements IXDataRenderer {
  57 
  58         private static abstract class SeriesEntry<U> {
  59                 final IQuantitySeries<U> series;
  60                 transient XYQuantities<U> points;
  61 
  62                 SeriesEntry(IQuantitySeries<U> series) {
  63                         this.series = series;
  64                 }
  65 
  66                 void updatePointsCache(SubdividedQuantityRange xRange) {
  67                         // FIXME: Improve to simply adjust the XYQuantities.xRange if xRange is compatible.
  68                         if ((points == null) || !points.getXRange().equals(xRange)) {
  69                                 points = series.getQuantities(xRange);
  70                         }
  71                 }
  72 
  73                 abstract void infoAt(IChartInfoVisitor visitor, int x, Point2D offset);
  74         }
  75 
  76         private static class BarSeriesEntry<T> extends SeriesEntry<T[]> {
  77                 private final String title;
  78                 private final IColorProvider<? super T> colorProvider;
  79                 private final Color color;
  80 
  81                 BarSeriesEntry(String title, IQuantitySeries<T[]> series, IColorProvider<? super T> cp, Color color) {
  82                         super(series);
  83                         this.title = title;
  84                         this.color = color;
  85                         colorProvider = cp;
  86                 }
  87 
  88                 @Override
  89                 void infoAt(IChartInfoVisitor visitor, int x, Point2D offset) {
  90                         if (points != null) {
  91                                 int bucket = points.floorIndexAtX(x);
  92                                 if (bucket >= 0 && bucket < points.getSize()) {
  93                                         T[] payload = points.getPayload();
  94                                         Color col = color;
  95                                         if (colorProvider != null) {
  96                                                 col = colorProvider.getColor((payload != null) ? payload[bucket] : null);
  97                                         }
  98                                         IBucket bkt = new XYQuantities.Bucket(points, bucket, offset, title, col);
  99                                         visitor.visit(bkt);
 100                                 }
 101                         }
 102                 }
 103         }
 104 
 105         private static class LineSeriesEntry<T> extends SeriesEntry<T> {
 106                 private final String title;
 107                 private final boolean fill;
 108                 private final boolean connect;
 109                 private final Color color;
 110 
 111                 LineSeriesEntry(String title, IQuantitySeries<T> series, Color color, boolean fill, boolean connect) {
 112                         super(series);
 113                         this.title = title;
 114                         this.color = color;
 115                         this.fill = fill;
 116                         this.connect = connect;
 117                 }
 118 
 119                 @Override
 120                 void infoAt(IChartInfoVisitor visitor, int x, Point2D offset) {
 121                         if (points != null) {
 122                                 int index = Math.max(points.floorIndexAtX(x), 0);
 123                                 int size = points.getSize();
 124                                 if (index < size) {
 125                                         if (index < size - 1) {
 126                                                 // Check if the next index is closer.
 127                                                 double currentX = points.getPixelX(index);
 128                                                 double nextX = points.getPixelX(index + 1);
 129                                                 if ((currentX < 0) || (((nextX - x) < (x - currentX)) && (nextX < points.getWidth()))) {
 130                                                         index++;
 131                                                 }
 132                                         }
 133                                         IPoint point = new XYQuantities.Point(points, index, offset, title, color);
 134                                         visitor.visit(point);
 135                                 }
 136                         }
 137                 }
 138         }
 139 
 140         private final ArrayList<SeriesEntry<?>> entries = new ArrayList<>();
 141         private final boolean axisOnLeft;
 142         private final IQuantity includeLow;
 143         private final IQuantity includeHigh;
 144         private final String name;
 145         private final String description;
 146 
 147         public XYDataRenderer(IQuantity include) {
 148                 this(include, include);
 149         }
 150 
 151         public XYDataRenderer(IQuantity include, String name, String description) {
 152                 this(include, include, true, name, description);
 153         }
 154 
 155         public XYDataRenderer(IQuantity includeLow, IQuantity includeHigh) {
 156                 this(includeLow, includeHigh, true, null, null);
 157         }
 158 
 159         public XYDataRenderer(IQuantity includeLow, IQuantity includeHigh, boolean axisOnLeft, String name,
 160                         String description) {
 161                 this.axisOnLeft = axisOnLeft;
 162                 this.includeLow = includeLow;
 163                 this.includeHigh = includeHigh;
 164                 this.name = name;
 165                 this.description = description;
 166         }
 167 
 168         public <T> void addBarChart(String title, IQuantitySeries<T[]> series, Color color) {
 169                 entries.add(new BarSeriesEntry<>(title, series, null, color));
 170         }
 171 
 172         public <T> void addBarChart(String title, IQuantitySeries<T[]> series, IColorProvider<? super T> cp) {
 173                 entries.add(new BarSeriesEntry<>(title, series, cp, null));
 174         }
 175 
 176         public <T> void addLineChart(String title, IQuantitySeries<T> series, Color color, boolean fill) {
 177                 entries.add(new LineSeriesEntry<>(title, series, color, fill, true));
 178         }
 179 
 180         public <T> void addPlotChart(String title, IQuantitySeries<T> series, Color color, boolean fill) {
 181                 entries.add(new LineSeriesEntry<>(title, series, color, fill, false));
 182         }
 183 
 184         @Override
 185         public IRenderedRow render(Graphics2D context, SubdividedQuantityRange xRange, int height) {
 186                 int width = xRange.getPixelExtent();
 187                 IQuantity yAxisMin = includeLow;
 188                 IQuantity yAxisMax = includeHigh;
 189                 for (SeriesEntry<?> se : entries) {
 190                         se.updatePointsCache(xRange);
 191                         if (se.points.getSize() > 0) {
 192                                 IQuantity seriesMinY = se.points.getMinY();
 193                                 if (yAxisMin == null || yAxisMin.compareTo(seriesMinY) > 0) {
 194                                         yAxisMin = seriesMinY;
 195                                 }
 196                                 IQuantity seriesMaxY = se.points.getMaxY();
 197                                 if (yAxisMax == null || yAxisMax.compareTo(seriesMaxY) < 0) {
 198                                         yAxisMax = seriesMaxY;
 199                                 }
 200                         }
 201                 }
 202 
 203                 if (yAxisMin != null && yAxisMax != null) {
 204                         FontMetrics fm = context.getFontMetrics();
 205                         // If min=max, expand range to be [min, min+1], or [min, min+1024] in the case of 
 206                         //a graph measured in bytes
 207                         if (yAxisMin.compareTo(yAxisMax) == 0) {
 208                                 int offset = yAxisMin.getUnit() == UnitLookup.BYTE ? 1024 : 1;
 209                                 yAxisMax = yAxisMin.getUnit().quantity(yAxisMin.doubleValue() + offset);
 210                         } else {
 211                                 // Add sufficient padding to ensure that labels for ticks <= yAxisMax fit,
 212                                 // and constant value graphs are discernible.
 213                                 double padFactor = ((double) (height + 1 + fm.getAscent() / 2)) / height;
 214                                 yAxisMax = yAxisMin.add(yAxisMax.subtract(yAxisMin).multiply(padFactor));
 215                         }
 216                         SubdividedQuantityRange yRange = new SubdividedQuantityRange(yAxisMin, yAxisMax, height, fm.getHeight());
 217                         context.setPaint(Color.LIGHT_GRAY);
 218                         AWTChartToolkit.drawGrid(context, yRange, width, true);
 219                         Shape oldClip = context.getClip();
 220                         context.setClip(new Rectangle(width, height));
 221                         for (SeriesEntry<?> se : entries) {
 222                                 // Always set yRange since it is used in infoAt().
 223                                 se.points.setYRange(yRange);
 224                                 if (se.points.getSize() > 0) {
 225                                         if (se instanceof LineSeriesEntry) {
 226                                                 LineSeriesEntry<?> lse = (LineSeriesEntry<?>) se;
 227                                                 if (lse.connect) {
 228                                                         context.setPaint(lse.fill ? ColorToolkit.getGradientPaint(lse.color, height) : lse.color);
 229                                                         AWTChartToolkit.drawLineChart(context, se.points, width, height, lse.fill);
 230                                                 } else {
 231                                                         context.setPaint(lse.color);
 232                                                         AWTChartToolkit.drawPlot(context, se.points, height, lse.fill);
 233                                                 }
 234                                         } else if (se instanceof BarSeriesEntry) {
 235                                                 drawBarChart(context, (BarSeriesEntry<?>) se, width, height);
 236                                         }
 237                                 }
 238                         }
 239                         context.setClip(oldClip);
 240                         context.setPaint(Color.BLACK);
 241                         if (axisOnLeft) {
 242                                 AWTChartToolkit.drawAxis(context, yRange, 0, true, 1, true);
 243                         } else {
 244                                 AWTChartToolkit.drawAxis(context, yRange, width, false, 1, true);
 245                         }
 246                 }
 247                 return new RenderedResult(height);
 248         }
 249 
 250         // FIXME: Must NOT be dependent on mutable state from XYDataRenderer
 251         private class RenderedResult extends RenderedRowBase {
 252                 private static final int TICK_ZONE_WIDTH = 32;
 253 
 254                 public RenderedResult(int height) {
 255                         super(Collections.<IRenderedRow> emptyList(), height, name, null, null);
 256                 }
 257 
 258                 @Override
 259                 public void infoAt(IChartInfoVisitor visitor, int x, int y, final Point offset) {
 260                         if (x >= 0) {
 261                                 for (SeriesEntry<?> se : entries) {
 262                                         se.infoAt(visitor, x, offset);
 263                                 }
 264                         } else if (axisOnLeft && !entries.isEmpty() && x >= -TICK_ZONE_WIDTH) {
 265                                 // FIXME: Factor out to support axis on right
 266                                 final SubdividedQuantityRange yRange = entries.get(0).points.getYRange();
 267                                 final int index = yRange.getClosestSubdividerAtPixel(yRange.getPixelExtent() - 1 - y);
 268                                 visitor.visit(new ITick() {
 269                                         @Override
 270                                         public IDisplayable getValue() {
 271                                                 return yRange.getSubdivider(index);
 272                                         }
 273 
 274                                         @Override
 275                                         public Point2D getTarget() {
 276                                                 int y = offset.y + yRange.getPixelExtent() - 1 - ((int) yRange.getSubdividerPixel(index));
 277                                                 return new Point(offset.x, y);
 278                                         }
 279                                 });
 280                         } else {
 281                                 visitor.visit(new ILane() {
 282                                         @Override
 283                                         public String getLaneName() {
 284                                                 return name;
 285                                         }
 286 
 287                                         @Override
 288                                         public String getLaneDescription() {
 289                                                 return description;
 290                                         }
 291                                 });
 292                         }
 293                 }
 294         }
 295 
 296         private static <T> void drawBarChart(Graphics2D context, BarSeriesEntry<T> se, int width, int height) {
 297                 if (se.color != null) {
 298                         context.setPaint(ColorToolkit.getGradientPaint(se.color, height));
 299                 }
 300                 AWTChartToolkit.drawBarChart(context, se.points, se.colorProvider, width, height);
 301         }
 302 }