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